Skip to content

Commit f2237d8

Browse files
authored
feat: Implement LLM task title generation and improve prompt timing (#964)
1 parent 625565c commit f2237d8

19 files changed

Lines changed: 215 additions & 376 deletions

File tree

apps/twig/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@
168168
"uuid": "^9.0.1",
169169
"vscode-icons-js": "^11.6.1",
170170
"zod": "^4.1.12",
171-
"zustand": "^4.5.0"
171+
"zustand": "^4.5.0",
172+
"striptags": "^3.2.0"
172173
}
173174
}

apps/twig/src/main/lib/timing.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.

apps/twig/src/main/services/agent/schemas.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,6 @@ export const startSessionInput = z.object({
4545
runMode: z.enum(["local", "cloud"]).optional(),
4646
adapter: z.enum(["claude", "codex"]).optional(),
4747
additionalDirectories: z.array(z.string()).optional(),
48-
/** Dev-only: timestamp from renderer when user submitted the task */
49-
submittedAt: z.number().optional(),
5048
});
5149

5250
export type StartSessionInput = z.infer<typeof startSessionInput>;

apps/twig/src/main/services/agent/service.ts

Lines changed: 62 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,8 @@ import { app } from "electron";
2525
import { inject, injectable, preDestroy } from "inversify";
2626
import { MAIN_TOKENS } from "../../di/tokens.js";
2727
import { logger } from "../../lib/logger.js";
28-
import { createMainTimingCollector } from "../../lib/timing.js";
2928
import { TypedEventEmitter } from "../../lib/typed-event-emitter.js";
3029
import type { FsService } from "../fs/service.js";
31-
import { getCurrentUserId, getPostHogClient } from "../posthog-analytics.js";
3230
import type { ProcessTrackingService } from "../process-tracking/service.js";
3331
import type { SleepService } from "../sleep/service.js";
3432
import {
@@ -179,8 +177,6 @@ interface SessionConfig {
179177
additionalDirectories?: string[];
180178
/** Permission mode to use for the session */
181179
permissionMode?: string;
182-
/** Dev-only: timestamp from renderer when user submitted the task */
183-
submittedAt?: number;
184180
}
185181

186182
interface ManagedSession {
@@ -421,8 +417,6 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
421417
isReconnect: boolean,
422418
isRetry = false,
423419
): Promise<ManagedSession | null> {
424-
const tc = createMainTimingCollector(log);
425-
426420
const {
427421
taskId,
428422
taskRunId,
@@ -432,13 +426,8 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
432426
adapter,
433427
additionalDirectories,
434428
permissionMode,
435-
submittedAt,
436429
} = config;
437430

438-
if (submittedAt) {
439-
tc.record("ipcTransit", Date.now() - submittedAt);
440-
}
441-
442431
// Preview sessions don't need a real repo — use a temp directory
443432
const repoPath = taskId === "__preview__" ? tmpdir() : rawRepoPath;
444433

@@ -449,125 +438,94 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
449438
}
450439

451440
// Kill any lingering processes from previous runs of this task
452-
tc.timeSync("killProcesses", () =>
453-
this.processTracking.killByTaskId(taskId),
454-
);
441+
this.processTracking.killByTaskId(taskId);
455442

456443
// Clean up any prior session for this taskRunId before creating a new one
457-
await tc.time("cleanup", () => this.cleanupSession(taskRunId));
444+
await this.cleanupSession(taskRunId);
458445
}
459446

460447
const channel = `agent-event:${taskRunId}`;
461-
const mockNodeDir = tc.timeSync("mockNode", () =>
462-
this.setupMockNodeEnvironment(taskRunId),
463-
);
464-
tc.timeSync("setupEnv", () =>
465-
this.setupEnvironment(credentials, mockNodeDir),
466-
);
448+
const mockNodeDir = this.setupMockNodeEnvironment(taskRunId);
449+
this.setupEnvironment(credentials, mockNodeDir);
467450

468451
// Preview sessions don't persist logs — no real task exists
469452
const isPreview = taskId === "__preview__";
470453

471-
// OTEL log pipeline or legacy S3 writer if FF false
472-
const useOtelPipeline = isPreview
473-
? false
474-
: await tc.time("featureFlag", () =>
475-
this.isFeatureFlagEnabled("twig-agent-logs-pipeline"),
476-
);
477-
478-
log.info("Agent log transport", {
479-
transport: isPreview ? "none" : useOtelPipeline ? "otel" : "s3",
480-
taskId,
481-
taskRunId,
482-
});
483-
484454
const agent = new Agent({
485455
posthog: {
486456
apiUrl: credentials.apiHost,
487457
getApiKey: () => this.getToken(credentials.apiKey),
488458
projectId: credentials.projectId,
489459
},
490-
otelTransport: useOtelPipeline
491-
? {
492-
host: credentials.apiHost,
493-
apiKey: this.getToken(credentials.apiKey),
494-
logsPath: "/i/v1/agent-logs",
495-
}
496-
: undefined,
497460
skipLogPersistence: isPreview,
498461
debug: !app.isPackaged,
499462
onLog: onAgentLog,
500463
});
501464

502465
try {
503-
const acpConnection = await tc.time("agentRun", () =>
504-
agent.run(taskId, taskRunId, {
505-
adapter,
506-
codexBinaryPath:
507-
adapter === "codex" ? getCodexBinaryPath() : undefined,
508-
processCallbacks: {
509-
onProcessSpawned: (info) => {
510-
this.processTracking.register(
511-
info.pid,
512-
"agent",
513-
`agent:${taskRunId}`,
514-
{
515-
taskRunId,
516-
taskId,
517-
command: info.command,
518-
},
466+
const acpConnection = await agent.run(taskId, taskRunId, {
467+
adapter,
468+
codexBinaryPath: adapter === "codex" ? getCodexBinaryPath() : undefined,
469+
processCallbacks: {
470+
onProcessSpawned: (info) => {
471+
this.processTracking.register(
472+
info.pid,
473+
"agent",
474+
`agent:${taskRunId}`,
475+
{
476+
taskRunId,
519477
taskId,
520-
);
521-
},
522-
onProcessExited: (pid) => {
523-
this.processTracking.unregister(pid, "agent-exited");
524-
},
478+
command: info.command,
479+
},
480+
taskId,
481+
);
525482
},
526-
}),
527-
);
483+
onProcessExited: (pid) => {
484+
this.processTracking.unregister(pid, "agent-exited");
485+
},
486+
},
487+
});
528488
const { clientStreams } = acpConnection;
529489

530-
const connection = tc.timeSync("clientConnection", () =>
531-
this.createClientConnection(taskRunId, channel, clientStreams),
490+
const connection = this.createClientConnection(
491+
taskRunId,
492+
channel,
493+
clientStreams,
532494
);
533495

534-
await tc.time("initialize", () =>
535-
connection.initialize({
536-
protocolVersion: PROTOCOL_VERSION,
537-
clientCapabilities: {
538-
fs: {
539-
readTextFile: true,
540-
writeTextFile: true,
541-
},
542-
terminal: true,
496+
await connection.initialize({
497+
protocolVersion: PROTOCOL_VERSION,
498+
clientCapabilities: {
499+
fs: {
500+
readTextFile: true,
501+
writeTextFile: true,
543502
},
544-
}),
545-
);
503+
terminal: true,
504+
},
505+
});
546506

547-
const mcpServers = tc.timeSync("buildMcp", () =>
548-
adapter === "codex" ? [] : this.buildMcpServers(credentials),
549-
);
507+
const mcpServers =
508+
adapter === "codex" ? [] : this.buildMcpServers(credentials);
550509

551510
let configOptions: SessionConfigOption[] | undefined;
552511
let agentSessionId: string;
553512

554513
if (isReconnect && adapter === "codex" && config.sessionId) {
555-
const loadResponse = await tc.time("loadSession", () =>
556-
connection.loadSession({
557-
sessionId: config.sessionId!,
558-
cwd: repoPath,
559-
mcpServers,
560-
}),
561-
);
514+
const loadResponse = await connection.loadSession({
515+
sessionId: config.sessionId!,
516+
cwd: repoPath,
517+
mcpServers,
518+
});
562519
configOptions = loadResponse.configOptions ?? undefined;
563520
agentSessionId = config.sessionId;
564521
} else if (isReconnect && adapter !== "codex") {
565522
if (!config.sessionId) {
566523
throw new Error("Cannot resume session without sessionId");
567524
}
568525
const systemPrompt = this.buildPostHogSystemPrompt(credentials);
569-
const resumeResponse = await tc.time("resumeSession", () =>
570-
connection.extMethod("_posthog/session/resume", {
526+
const resumeResponse = await connection.extMethod(
527+
"_posthog/session/resume",
528+
{
571529
sessionId: config.sessionId!,
572530
cwd: repoPath,
573531
mcpServers,
@@ -585,7 +543,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
585543
},
586544
}),
587545
},
588-
}),
546+
},
589547
);
590548
const resumeMeta = resumeResponse?._meta as
591549
| {
@@ -596,31 +554,26 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
596554
agentSessionId = config.sessionId;
597555
} else {
598556
const systemPrompt = this.buildPostHogSystemPrompt(credentials);
599-
const newSessionResponse = await tc.time("newSession", () =>
600-
connection.newSession({
601-
cwd: repoPath,
602-
mcpServers,
603-
_meta: {
604-
taskRunId,
605-
systemPrompt,
606-
...(permissionMode && { permissionMode }),
607-
...(additionalDirectories?.length && {
608-
claudeCode: {
609-
options: { additionalDirectories },
610-
},
611-
}),
612-
},
613-
}),
614-
);
557+
const newSessionResponse = await connection.newSession({
558+
cwd: repoPath,
559+
mcpServers,
560+
_meta: {
561+
taskRunId,
562+
systemPrompt,
563+
...(permissionMode && { permissionMode }),
564+
...(additionalDirectories?.length && {
565+
claudeCode: {
566+
options: { additionalDirectories },
567+
},
568+
}),
569+
},
570+
});
615571
configOptions = newSessionResponse.configOptions ?? undefined;
616572
agentSessionId = newSessionResponse.sessionId;
617573
}
618574

619575
config.sessionId = agentSessionId;
620576

621-
const sessionType = isReconnect ? "reconnect" : "create";
622-
tc.summarize(`getOrCreateSession(${sessionType})`);
623-
624577
const session: ManagedSession = {
625578
taskRunId,
626579
taskId,
@@ -1011,20 +964,6 @@ For git operations while detached:
1011964
return mockNodeDir;
1012965
}
1013966

1014-
private async isFeatureFlagEnabled(flagKey: string): Promise<boolean> {
1015-
try {
1016-
const client = getPostHogClient();
1017-
const userId = getCurrentUserId();
1018-
if (!client || !userId) {
1019-
return false;
1020-
}
1021-
return (await client.isFeatureEnabled(flagKey, userId)) ?? false;
1022-
} catch (error) {
1023-
log.warn(`Error checking feature flag "${flagKey}":`, error);
1024-
return false;
1025-
}
1026-
}
1027-
1028967
private cleanupMockNodeEnvironment(mockNodeDir: string): void {
1029968
try {
1030969
rmSync(mockNodeDir, { recursive: true, force: true });
@@ -1328,7 +1267,6 @@ For git operations while detached:
13281267
: undefined,
13291268
permissionMode:
13301269
"permissionMode" in params ? params.permissionMode : undefined,
1331-
submittedAt: "submittedAt" in params ? params.submittedAt : undefined,
13321270
};
13331271
}
13341272

apps/twig/src/renderer/features/sessions/components/ConversationView.stories.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,6 @@ export const WithPendingPrompt: Story = {
451451
return events;
452452
})(),
453453
isPromptPending: true,
454-
promptStartedAt: Date.now() - 5000,
455454
repoPath: "/Users/jonathan/dev/twig",
456455
},
457456
};

apps/twig/src/renderer/features/sessions/components/ConversationView.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ type VirtualizedItem = ConversationItem | QueuedItem;
6060
interface ConversationViewProps {
6161
events: AcpMessage[];
6262
isPromptPending: boolean;
63-
promptStartedAt?: number | null;
6463
repoPath?: string | null;
6564
taskId?: string;
6665
}
@@ -71,7 +70,6 @@ const ESTIMATE_SIZE = 200;
7170
export function ConversationView({
7271
events,
7372
isPromptPending,
74-
promptStartedAt,
7573
repoPath,
7674
taskId,
7775
}: ConversationViewProps) {
@@ -209,7 +207,6 @@ export function ConversationView({
209207
<div className="pb-16">
210208
<SessionFooter
211209
isPromptPending={isPromptPending}
212-
promptStartedAt={promptStartedAt}
213210
lastGenerationDuration={
214211
lastTurn?.isComplete ? lastTurn.durationMs : null
215212
}

apps/twig/src/renderer/features/sessions/components/SessionFooter.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { formatDuration, GeneratingIndicator } from "./GeneratingIndicator";
55

66
interface SessionFooterProps {
77
isPromptPending: boolean;
8-
promptStartedAt?: number | null;
98
lastGenerationDuration: number | null;
109
lastStopReason?: string;
1110
queuedCount?: number;
@@ -14,7 +13,6 @@ interface SessionFooterProps {
1413

1514
export function SessionFooter({
1615
isPromptPending,
17-
promptStartedAt,
1816
lastGenerationDuration,
1917
lastStopReason,
2018
queuedCount = 0,
@@ -41,7 +39,7 @@ export function SessionFooter({
4139
return (
4240
<Box className="pt-3 pb-1">
4341
<Flex align="center" gap="2">
44-
<GeneratingIndicator startedAt={promptStartedAt} />
42+
<GeneratingIndicator />
4543
{queuedCount > 0 && (
4644
<Text size="1" color="gray">
4745
({queuedCount} queued)

0 commit comments

Comments
 (0)