Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 90d56a5

Browse files
authored
perf: Optimize session startup, add prompt timing and clean up scripts (#991)
1 parent 15e94b1 commit 90d56a5

14 files changed

Lines changed: 60 additions & 30 deletions

File tree

‎apps/mobile/package.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,18 @@
44
"main": "expo-router/entry",
55
"scripts": {
66
"start": "expo start",
7+
"start:clear": "expo start --clear",
78
"android": "expo run:android",
89
"ios": "expo run:ios",
10+
"ios:device": "expo run:ios --device",
911
"web": "expo start --web",
1012
"prebuild": "expo prebuild",
13+
"prebuild:clean": "expo prebuild --clean",
14+
"build:dev": "eas build --profile development --platform ios",
15+
"build:dev:local": "eas build --profile development --platform ios --local",
16+
"build:preview": "eas build --profile preview --platform ios",
17+
"build:production": "eas build --profile production --platform ios",
18+
"testflight": "npx testflight",
1119
"lint": "biome check .",
1220
"lint:fix": "biome check --write .",
1321
"format": "biome format --write .",

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

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

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { VirtualizedList, type VirtualizedListHandle } from "./VirtualizedList";
2828
interface ConversationViewProps {
2929
events: AcpMessage[];
3030
isPromptPending: boolean;
31+
promptStartedAt?: number | null;
3132
repoPath?: string | null;
3233
taskId?: string;
3334
}
@@ -38,6 +39,7 @@ const ESTIMATE_SIZE = 36;
3839
export function ConversationView({
3940
events,
4041
isPromptPending,
42+
promptStartedAt,
4143
repoPath,
4244
taskId,
4345
}: ConversationViewProps) {
@@ -191,6 +193,7 @@ export function ConversationView({
191193
<div className="pb-16">
192194
<SessionFooter
193195
isPromptPending={isPromptPending}
196+
promptStartedAt={promptStartedAt}
194197
lastGenerationDuration={
195198
lastTurnInfo?.isComplete ? lastTurnInfo.durationMs : null
196199
}

‎apps/twig/src/renderer/features/sessions/components/GeneratingIndicator.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export function formatDuration(ms: number): string {
1717
}
1818

1919
interface GeneratingIndicatorProps {
20+
/** Timestamp (ms) when the prompt started. Only render this component while a prompt is pending. */
2021
startedAt?: number | null;
2122
}
2223

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

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

66
interface SessionFooterProps {
77
isPromptPending: boolean;
8+
promptStartedAt?: number | null;
89
lastGenerationDuration: number | null;
910
lastStopReason?: string;
1011
queuedCount?: number;
@@ -13,6 +14,7 @@ interface SessionFooterProps {
1314

1415
export function SessionFooter({
1516
isPromptPending,
17+
promptStartedAt,
1618
lastGenerationDuration,
1719
lastStopReason,
1820
queuedCount = 0,
@@ -39,7 +41,7 @@ export function SessionFooter({
3941
return (
4042
<Box className="pt-3 pb-1">
4143
<Flex align="center" gap="2">
42-
<GeneratingIndicator />
44+
<GeneratingIndicator startedAt={promptStartedAt} />
4345
{queuedCount > 0 && (
4446
<Text size="1" color="gray">
4547
({queuedCount} queued)

‎apps/twig/src/renderer/features/sessions/components/SessionView.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ interface SessionViewProps {
3838
taskId?: string;
3939
isRunning: boolean;
4040
isPromptPending?: boolean;
41+
promptStartedAt?: number | null;
4142
onSendPrompt: (text: string) => void;
4243
onBashCommand?: (command: string) => void;
4344
onCancelPrompt: () => void;
@@ -59,6 +60,7 @@ export function SessionView({
5960
taskId,
6061
isRunning,
6162
isPromptPending = false,
63+
promptStartedAt,
6264
onSendPrompt,
6365
onBashCommand,
6466
onCancelPrompt,
@@ -369,6 +371,7 @@ export function SessionView({
369371
<ConversationView
370372
events={events}
371373
isPromptPending={isPromptPending}
374+
promptStartedAt={promptStartedAt}
372375
repoPath={repoPath}
373376
taskId={taskId}
374377
/>

‎apps/twig/src/renderer/features/sessions/service/service.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ const createMockSession = (
182182
startedAt: Date.now(),
183183
status: "connected",
184184
isPromptPending: false,
185+
promptStartedAt: null,
185186
pendingPermissions: new Map(),
186187
messageQueue: [],
187188
...overrides,

‎apps/twig/src/renderer/features/sessions/service/service.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,8 +799,10 @@ export class SessionService {
799799
const msg = acpMsg.message;
800800

801801
if (isJsonRpcRequest(msg) && msg.method === "session/prompt") {
802+
// acpMsg.ts is local time (set in-process) — matches Date.now() used in sendLocalPrompt
802803
sessionStoreSetters.updateSession(taskRunId, {
803804
isPromptPending: true,
805+
promptStartedAt: acpMsg.ts,
804806
});
805807
}
806808

@@ -813,6 +815,7 @@ export class SessionService {
813815
) {
814816
sessionStoreSetters.updateSession(taskRunId, {
815817
isPromptPending: false,
818+
promptStartedAt: null,
816819
});
817820

818821
const stopReason = (msg.result as { stopReason?: string }).stopReason;
@@ -1042,6 +1045,7 @@ export class SessionService {
10421045
): Promise<{ stopReason: string }> {
10431046
sessionStoreSetters.updateSession(session.taskRunId, {
10441047
isPromptPending: true,
1048+
promptStartedAt: Date.now(),
10451049
});
10461050

10471051
try {
@@ -1052,6 +1056,7 @@ export class SessionService {
10521056
// Clear pending state on success
10531057
sessionStoreSetters.updateSession(session.taskRunId, {
10541058
isPromptPending: false,
1059+
promptStartedAt: null,
10551060
});
10561061
return result;
10571062
} catch (error) {
@@ -1072,10 +1077,12 @@ export class SessionService {
10721077
errorDetails ||
10731078
"Session connection lost. Please retry or start a new task.",
10741079
isPromptPending: false,
1080+
promptStartedAt: null,
10751081
});
10761082
} else {
10771083
sessionStoreSetters.updateSession(session.taskRunId, {
10781084
isPromptPending: false,
1085+
promptStartedAt: null,
10791086
});
10801087
}
10811088

@@ -1223,6 +1230,11 @@ export class SessionService {
12231230

12241231
const previousValue = configOptions[optionIndex].currentValue;
12251232

1233+
// Skip if value is already set — avoids expensive IPC round-trip (e.g. setModel ~2s)
1234+
if (previousValue === value) {
1235+
return;
1236+
}
1237+
12261238
// Optimistic update
12271239
const updatedOptions = configOptions.map((opt) =>
12281240
opt.id === configId ? { ...opt, currentValue: value } : opt,
@@ -1623,6 +1635,7 @@ export class SessionService {
16231635
startedAt: Date.now(),
16241636
status: "connecting",
16251637
isPromptPending: false,
1638+
promptStartedAt: null,
16261639
pendingPermissions: new Map(),
16271640
messageQueue: [],
16281641
};

‎apps/twig/src/renderer/features/sessions/stores/sessionStore.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export interface AgentSession {
3939
errorTitle?: string;
4040
errorMessage?: string;
4141
isPromptPending: boolean;
42+
promptStartedAt: number | null;
4243
logUrl?: string;
4344
processedLineCount?: number;
4445
framework?: "claude";

‎apps/twig/src/renderer/features/task-detail/components/TaskLogsPanel.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) {
7474

7575
const events = session?.events ?? [];
7676
const isPromptPending = session?.isPromptPending ?? false;
77+
const promptStartedAt = session?.promptStartedAt;
7778

7879
const isNewSessionWithInitialPrompt =
7980
!task.latest_run?.id && !!task.description;
@@ -317,6 +318,7 @@ export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) {
317318
taskId={taskId}
318319
isRunning={isCloud ? false : isRunning}
319320
isPromptPending={isCloud ? false : isPromptPending}
321+
promptStartedAt={isCloud ? undefined : promptStartedAt}
320322
onSendPrompt={handleSendPrompt}
321323
onBashCommand={handleBashCommand}
322324
onCancelPrompt={handleCancelPrompt}

0 commit comments

Comments
 (0)