From 4131730b1de159376804ccb070d7a15abbbe34cc Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Mon, 23 Feb 2026 13:55:51 -0800 Subject: [PATCH 1/3] Optimize session startup, add prompt timing and clean up scripts --- apps/mobile/package.json | 8 +++++ .../components/ConversationView.stories.tsx | 1 + .../sessions/components/ConversationView.tsx | 3 ++ .../sessions/components/SessionFooter.tsx | 4 ++- .../sessions/components/SessionView.tsx | 3 ++ .../features/sessions/service/service.test.ts | 1 + .../features/sessions/service/service.ts | 12 +++++++ .../features/sessions/stores/sessionStore.ts | 1 + .../task-detail/components/TaskLogsPanel.tsx | 2 ++ package.json | 31 +++++-------------- packages/agent/src/adapters/acp-connection.ts | 4 ++- .../agent/src/adapters/claude/claude-agent.ts | 11 +++++-- packages/agent/src/agent.ts | 4 +-- 13 files changed, 55 insertions(+), 30 deletions(-) diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 5b5bc52b1..0cdb2c40f 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -4,10 +4,18 @@ "main": "expo-router/entry", "scripts": { "start": "expo start", + "start:clear": "expo start --clear", "android": "expo run:android", "ios": "expo run:ios", + "ios:device": "expo run:ios --device", "web": "expo start --web", "prebuild": "expo prebuild", + "prebuild:clean": "expo prebuild --clean", + "build:dev": "eas build --profile development --platform ios", + "build:dev:local": "eas build --profile development --platform ios --local", + "build:preview": "eas build --profile preview --platform ios", + "build:production": "eas build --profile production --platform ios", + "testflight": "npx testflight", "lint": "biome check .", "lint:fix": "biome check --write .", "format": "biome format --write .", diff --git a/apps/twig/src/renderer/features/sessions/components/ConversationView.stories.tsx b/apps/twig/src/renderer/features/sessions/components/ConversationView.stories.tsx index adc85112d..392dfbaeb 100644 --- a/apps/twig/src/renderer/features/sessions/components/ConversationView.stories.tsx +++ b/apps/twig/src/renderer/features/sessions/components/ConversationView.stories.tsx @@ -451,6 +451,7 @@ export const WithPendingPrompt: Story = { return events; })(), isPromptPending: true, + promptStartedAt: Date.now() - 5000, repoPath: "/Users/jonathan/dev/twig", }, }; diff --git a/apps/twig/src/renderer/features/sessions/components/ConversationView.tsx b/apps/twig/src/renderer/features/sessions/components/ConversationView.tsx index 55f8b7e61..b5d10ccb0 100644 --- a/apps/twig/src/renderer/features/sessions/components/ConversationView.tsx +++ b/apps/twig/src/renderer/features/sessions/components/ConversationView.tsx @@ -28,6 +28,7 @@ import { VirtualizedList, type VirtualizedListHandle } from "./VirtualizedList"; interface ConversationViewProps { events: AcpMessage[]; isPromptPending: boolean; + promptStartedAt?: number | null; repoPath?: string | null; taskId?: string; } @@ -38,6 +39,7 @@ const ESTIMATE_SIZE = 36; export function ConversationView({ events, isPromptPending, + promptStartedAt, repoPath, taskId, }: ConversationViewProps) { @@ -191,6 +193,7 @@ export function ConversationView({
- + {queuedCount > 0 && ( ({queuedCount} queued) diff --git a/apps/twig/src/renderer/features/sessions/components/SessionView.tsx b/apps/twig/src/renderer/features/sessions/components/SessionView.tsx index 50b2eb747..1c4bac9a6 100644 --- a/apps/twig/src/renderer/features/sessions/components/SessionView.tsx +++ b/apps/twig/src/renderer/features/sessions/components/SessionView.tsx @@ -38,6 +38,7 @@ interface SessionViewProps { taskId?: string; isRunning: boolean; isPromptPending?: boolean; + promptStartedAt?: number | null; onSendPrompt: (text: string) => void; onBashCommand?: (command: string) => void; onCancelPrompt: () => void; @@ -59,6 +60,7 @@ export function SessionView({ taskId, isRunning, isPromptPending = false, + promptStartedAt, onSendPrompt, onBashCommand, onCancelPrompt, @@ -369,6 +371,7 @@ export function SessionView({ diff --git a/apps/twig/src/renderer/features/sessions/service/service.test.ts b/apps/twig/src/renderer/features/sessions/service/service.test.ts index c42cf135b..fcf65f698 100644 --- a/apps/twig/src/renderer/features/sessions/service/service.test.ts +++ b/apps/twig/src/renderer/features/sessions/service/service.test.ts @@ -182,6 +182,7 @@ const createMockSession = ( startedAt: Date.now(), status: "connected", isPromptPending: false, + promptStartedAt: null, pendingPermissions: new Map(), messageQueue: [], ...overrides, diff --git a/apps/twig/src/renderer/features/sessions/service/service.ts b/apps/twig/src/renderer/features/sessions/service/service.ts index e0f46c596..9e37b31a7 100644 --- a/apps/twig/src/renderer/features/sessions/service/service.ts +++ b/apps/twig/src/renderer/features/sessions/service/service.ts @@ -801,6 +801,7 @@ export class SessionService { if (isJsonRpcRequest(msg) && msg.method === "session/prompt") { sessionStoreSetters.updateSession(taskRunId, { isPromptPending: true, + promptStartedAt: acpMsg.ts, }); } @@ -813,6 +814,7 @@ export class SessionService { ) { sessionStoreSetters.updateSession(taskRunId, { isPromptPending: false, + promptStartedAt: null, }); const stopReason = (msg.result as { stopReason?: string }).stopReason; @@ -1042,6 +1044,7 @@ export class SessionService { ): Promise<{ stopReason: string }> { sessionStoreSetters.updateSession(session.taskRunId, { isPromptPending: true, + promptStartedAt: Date.now(), }); try { @@ -1052,6 +1055,7 @@ export class SessionService { // Clear pending state on success sessionStoreSetters.updateSession(session.taskRunId, { isPromptPending: false, + promptStartedAt: null, }); return result; } catch (error) { @@ -1072,10 +1076,12 @@ export class SessionService { errorDetails || "Session connection lost. Please retry or start a new task.", isPromptPending: false, + promptStartedAt: null, }); } else { sessionStoreSetters.updateSession(session.taskRunId, { isPromptPending: false, + promptStartedAt: null, }); } @@ -1223,6 +1229,11 @@ export class SessionService { const previousValue = configOptions[optionIndex].currentValue; + // Skip if value is already set — avoids expensive IPC round-trip (e.g. setModel ~2s) + if (previousValue === value) { + return; + } + // Optimistic update const updatedOptions = configOptions.map((opt) => opt.id === configId ? { ...opt, currentValue: value } : opt, @@ -1623,6 +1634,7 @@ export class SessionService { startedAt: Date.now(), status: "connecting", isPromptPending: false, + promptStartedAt: null, pendingPermissions: new Map(), messageQueue: [], }; diff --git a/apps/twig/src/renderer/features/sessions/stores/sessionStore.ts b/apps/twig/src/renderer/features/sessions/stores/sessionStore.ts index 5a30b0df5..62e05e1e7 100644 --- a/apps/twig/src/renderer/features/sessions/stores/sessionStore.ts +++ b/apps/twig/src/renderer/features/sessions/stores/sessionStore.ts @@ -39,6 +39,7 @@ export interface AgentSession { errorTitle?: string; errorMessage?: string; isPromptPending: boolean; + promptStartedAt: number | null; logUrl?: string; processedLineCount?: number; framework?: "claude"; diff --git a/apps/twig/src/renderer/features/task-detail/components/TaskLogsPanel.tsx b/apps/twig/src/renderer/features/task-detail/components/TaskLogsPanel.tsx index 48c52146c..f9eba15ad 100644 --- a/apps/twig/src/renderer/features/task-detail/components/TaskLogsPanel.tsx +++ b/apps/twig/src/renderer/features/task-detail/components/TaskLogsPanel.tsx @@ -74,6 +74,7 @@ export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) { const events = session?.events ?? []; const isPromptPending = session?.isPromptPending ?? false; + const promptStartedAt = session?.promptStartedAt; const isNewSessionWithInitialPrompt = !task.latest_run?.id && !!task.description; @@ -317,6 +318,7 @@ export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) { taskId={taskId} isRunning={isCloud ? false : isRunning} isPromptPending={isCloud ? false : isPromptPending} + promptStartedAt={isCloud ? undefined : promptStartedAt} onSendPrompt={handleSendPrompt} onBashCommand={handleBashCommand} onCancelPrompt={handleCancelPrompt} diff --git a/package.json b/package.json index 00a7971bc..981041788 100644 --- a/package.json +++ b/package.json @@ -9,39 +9,24 @@ "packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b", "scripts": { "setup": "bash apps/twig/bin/setup", - "dev": "pnpm --filter @posthog/electron-trpc build && pnpm --filter shared build && pnpm --filter @twig/git build && pnpm --filter agent build && mprocs", + "prepare": "husky", + "dev": "pnpm build:deps && mprocs", "dev:agent": "pnpm --filter agent dev", "dev:git": "pnpm --filter @twig/git dev", - "dev:twig": "pnpm --filter twig dev", - "start": "pnpm --filter twig start", + "dev:twig": "pnpm --filter twig start", "build": "turbo build", + "build:deps": "turbo build --filter=@posthog/twig^...", "package": "turbo build && pnpm --filter twig package", - "typecheck": "turbo typecheck", - "lint": "biome check --write --unsafe", - "format": "biome format --write", "test": "turbo test", "test:bun": "turbo test --filter=@twig/core --filter=@twig/cli", "test:vitest": "pnpm --filter twig --filter @posthog/electron-trpc test", "test:e2e": "pnpm --filter twig test:e2e", "test:e2e:headed": "pnpm --filter twig test:e2e:headed", + "typecheck": "turbo typecheck", + "lint": "biome check --write --unsafe", + "format": "biome format --write", "clean": "pnpm -r clean", - "knip": "knip", - "prepare": "husky", - "mobile:start": "pnpm --filter @posthog/mobile exec expo start", - "mobile:start:clear": "pnpm --filter @posthog/mobile exec expo start --clear", - "mobile:run:ios": "pnpm --filter @posthog/mobile exec expo run:ios", - "mobile:run:ios:device": "pnpm --filter @posthog/mobile exec expo run:ios --device", - "mobile:run:android": "pnpm --filter @posthog/mobile exec expo run:android", - "mobile:prebuild": "pnpm --filter @posthog/mobile exec expo prebuild", - "mobile:prebuild:clean": "pnpm --filter @posthog/mobile exec expo prebuild --clean", - "mobile:build:dev": "pnpm --filter @posthog/mobile exec eas build --profile development --platform ios", - "mobile:build:dev:local": "pnpm --filter @posthog/mobile exec eas build --profile development --platform ios --local", - "mobile:build:preview": "pnpm --filter @posthog/mobile exec eas build --profile preview --platform ios", - "mobile:build:production": "pnpm --filter @posthog/mobile exec eas build --profile production --platform ios", - "mobile:testflight": "pnpm --filter @posthog/mobile exec npx testflight", - "mobile:lint": "pnpm --filter @posthog/mobile exec biome check .", - "mobile:format": "pnpm --filter @posthog/mobile exec biome format --write .", - "mobile:install": "pnpm install --filter @posthog/mobile" + "knip": "knip" }, "keywords": [ "posthog", diff --git a/packages/agent/src/adapters/acp-connection.ts b/packages/agent/src/adapters/acp-connection.ts index 2ba66e479..3b2793ed6 100644 --- a/packages/agent/src/adapters/acp-connection.ts +++ b/packages/agent/src/adapters/acp-connection.ts @@ -194,7 +194,9 @@ function createClaudeConnection(config: AcpConnectionConfig): AcpConnection { let agent: ClaudeAcpAgent | null = null; const agentConnection = new AgentSideConnection((client) => { - agent = new ClaudeAcpAgent(client, logWriter, config.processCallbacks); + agent = new ClaudeAcpAgent(client, logWriter, { + ...config.processCallbacks, + }); logger.info(`Created ${agent.adapterName} agent`); return agent; }, agentStream); diff --git a/packages/agent/src/adapters/claude/claude-agent.ts b/packages/agent/src/adapters/claude/claude-agent.ts index f1b25efd6..e76654568 100644 --- a/packages/agent/src/adapters/claude/claude-agent.ts +++ b/packages/agent/src/adapters/claude/claude-agent.ts @@ -48,7 +48,7 @@ import { fetchMcpToolMetadata } from "./mcp/tool-metadata.js"; import { canUseTool } from "./permissions/permission-handlers.js"; import { getAvailableSlashCommands } from "./session/commands.js"; import { parseMcpServers } from "./session/mcp-config.js"; -import { toSdkModelId } from "./session/models.js"; +import { DEFAULT_MODEL, toSdkModelId } from "./session/models.js"; import { buildSessionOptions, buildSystemPrompt, @@ -162,6 +162,8 @@ export class ClaudeAcpAgent extends BaseAcpAgent { }); const input = new Pushable(); + // Pass default model at construction to avoid expensive post-hoc setModel IPC + options.model = DEFAULT_MODEL; const q = query({ prompt: input, options }); const session = this.createSession( @@ -191,7 +193,12 @@ export class ClaudeAcpAgent extends BaseAcpAgent { this.deferBackgroundFetches(q, sessionId, mcpServers); session.modelId = modelOptions.currentModelId; - await this.trySetModel(q, modelOptions.currentModelId); + // Only call setModel if the resolved model differs from the default we + // already baked into the query options — avoids a ~2s IPC round-trip. + const resolvedSdkModel = toSdkModelId(modelOptions.currentModelId); + if (resolvedSdkModel !== DEFAULT_MODEL) { + await this.trySetModel(q, modelOptions.currentModelId); + } const configOptions = await this.buildConfigOptions(modelOptions); diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index de90c1a83..b87290c3a 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -18,12 +18,10 @@ export class Agent { private acpConnection?: InProcessAcpConnection; private taskRunId?: string; private sessionLogWriter?: SessionLogWriter; - public debug: boolean; constructor(config: AgentConfig) { - this.debug = config.debug || false; this.logger = new Logger({ - debug: this.debug, + debug: config.debug || false, prefix: "[PostHog Agent]", onLog: config.onLog, }); From e08c70322d03b03dba5af95629da184d29f9808d Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Mon, 23 Feb 2026 15:04:12 -0800 Subject: [PATCH 2/3] review notes --- .../features/sessions/components/GeneratingIndicator.tsx | 1 + apps/twig/src/renderer/features/sessions/service/service.ts | 1 + packages/agent/src/adapters/acp-connection.ts | 4 +--- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/twig/src/renderer/features/sessions/components/GeneratingIndicator.tsx b/apps/twig/src/renderer/features/sessions/components/GeneratingIndicator.tsx index c37fac175..159cd9dc8 100644 --- a/apps/twig/src/renderer/features/sessions/components/GeneratingIndicator.tsx +++ b/apps/twig/src/renderer/features/sessions/components/GeneratingIndicator.tsx @@ -17,6 +17,7 @@ export function formatDuration(ms: number): string { } interface GeneratingIndicatorProps { + /** Timestamp (ms) when the prompt started. Only render this component while a prompt is pending. */ startedAt?: number | null; } diff --git a/apps/twig/src/renderer/features/sessions/service/service.ts b/apps/twig/src/renderer/features/sessions/service/service.ts index 9e37b31a7..56fa9c9bf 100644 --- a/apps/twig/src/renderer/features/sessions/service/service.ts +++ b/apps/twig/src/renderer/features/sessions/service/service.ts @@ -799,6 +799,7 @@ export class SessionService { const msg = acpMsg.message; if (isJsonRpcRequest(msg) && msg.method === "session/prompt") { + // acpMsg.ts is local time (set in-process) — matches Date.now() used in sendLocalPrompt sessionStoreSetters.updateSession(taskRunId, { isPromptPending: true, promptStartedAt: acpMsg.ts, diff --git a/packages/agent/src/adapters/acp-connection.ts b/packages/agent/src/adapters/acp-connection.ts index 3b2793ed6..2ba66e479 100644 --- a/packages/agent/src/adapters/acp-connection.ts +++ b/packages/agent/src/adapters/acp-connection.ts @@ -194,9 +194,7 @@ function createClaudeConnection(config: AcpConnectionConfig): AcpConnection { let agent: ClaudeAcpAgent | null = null; const agentConnection = new AgentSideConnection((client) => { - agent = new ClaudeAcpAgent(client, logWriter, { - ...config.processCallbacks, - }); + agent = new ClaudeAcpAgent(client, logWriter, config.processCallbacks); logger.info(`Created ${agent.adapterName} agent`); return agent; }, agentStream); From de84062eb3e834921f9cd5d2dff0998d29406c27 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Mon, 23 Feb 2026 20:29:22 -0800 Subject: [PATCH 3/3] fix: restart preview session on project switch --- .../features/task-detail/hooks/usePreviewSession.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/twig/src/renderer/features/task-detail/hooks/usePreviewSession.ts b/apps/twig/src/renderer/features/task-detail/hooks/usePreviewSession.ts index 04bddc0be..4752af8e3 100644 --- a/apps/twig/src/renderer/features/task-detail/hooks/usePreviewSession.ts +++ b/apps/twig/src/renderer/features/task-detail/hooks/usePreviewSession.ts @@ -1,4 +1,5 @@ import type { SessionConfigOption } from "@agentclientprotocol/sdk"; +import { useAuthStore } from "@features/auth/stores/authStore"; import { getSessionService, PREVIEW_TASK_ID, @@ -30,14 +31,18 @@ interface PreviewSessionResult { export function usePreviewSession( adapter: "claude" | "codex", ): PreviewSessionResult { + const projectId = useAuthStore((s) => s.projectId); + useEffect(() => { + if (!projectId) return; + const service = getSessionService(); service.startPreviewSession({ adapter }); return () => { service.cancelPreviewSession(); }; - }, [adapter]); + }, [adapter, projectId]); const session = useSessionForTask(PREVIEW_TASK_ID); const modeOption = useModeConfigOptionForTask(PREVIEW_TASK_ID);