Skip to content

Commit 49b9b03

Browse files
committed
Optimistically render conversation items before subscription confirms
1 parent 83bb341 commit 49b9b03

4 files changed

Lines changed: 79 additions & 7 deletions

File tree

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
sessionStoreSetters,
3+
useOptimisticItemsForTask,
34
usePendingPermissionsForTask,
45
useQueuedMessagesForTask,
56
} from "@features/sessions/stores/sessionStore";
@@ -68,6 +69,7 @@ export function ConversationView({
6869
const pendingPermissions = usePendingPermissionsForTask(taskId ?? "");
6970
const pendingPermissionsCount = pendingPermissions.size;
7071
const queuedMessages = useQueuedMessagesForTask(taskId);
72+
const optimisticItems = useOptimisticItemsForTask(taskId);
7173

7274
const queuedItems = useMemo<Extract<ConversationItem, { type: "queued" }>[]>(
7375
() =>
@@ -79,13 +81,10 @@ export function ConversationView({
7981
[queuedMessages],
8082
);
8183

82-
const items = useMemo<ConversationItem[]>(
83-
() =>
84-
queuedItems.length > 0
85-
? [...conversationItems, ...queuedItems]
86-
: conversationItems,
87-
[conversationItems, queuedItems],
88-
);
84+
const items = useMemo<ConversationItem[]>(() => {
85+
const result: ConversationItem[] = [...conversationItems, ...optimisticItems];
86+
return queuedItems.length > 0 ? [...result, ...queuedItems] : result;
87+
}, [conversationItems, optimisticItems, queuedItems]);
8988

9089
const handleScrollStateChange = useCallback((isAtBottom: boolean) => {
9190
setShowScrollButton(!isAtBottom);

apps/code/src/renderer/features/sessions/hooks/useSession.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
type Adapter,
1111
type AgentSession,
1212
getConfigOptionByCategory,
13+
type OptimisticItem,
1314
type QueuedMessage,
1415
useSessionStore,
1516
} from "../stores/sessionStore";
@@ -98,6 +99,17 @@ export const useQueuedMessagesForTask = (
9899
});
99100
};
100101

102+
export const useOptimisticItemsForTask = (
103+
taskId: string | undefined,
104+
): OptimisticItem[] => {
105+
return useSessionStore((s) => {
106+
if (!taskId) return [];
107+
const taskRunId = s.taskIdIndex[taskId];
108+
if (!taskRunId) return [];
109+
return s.sessions[taskRunId]?.optimisticItems ?? [];
110+
});
111+
};
112+
101113
// --- Config Option Hooks ---
102114

103115
/** Get a config option by category for a task */

apps/code/src/renderer/features/sessions/service/service.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,15 @@ export class SessionService {
819819
const session = sessionStoreSetters.getSessions()[taskRunId];
820820
if (!session) return;
821821

822+
if (
823+
isJsonRpcRequest(acpMsg.message) &&
824+
acpMsg.message.method === "session/prompt"
825+
) {
826+
sessionStoreSetters.appendEventAndClearOptimisticItems(taskRunId, acpMsg);
827+
this.updatePromptStateFromEvents(taskRunId, [acpMsg]);
828+
return;
829+
}
830+
822831
sessionStoreSetters.appendEvents(taskRunId, [acpMsg]);
823832
this.updatePromptStateFromEvents(taskRunId, [acpMsg]);
824833

@@ -1065,6 +1074,12 @@ export class SessionService {
10651074
promptStartedAt: Date.now(),
10661075
});
10671076

1077+
sessionStoreSetters.appendOptimisticItem(session.taskRunId, {
1078+
type: "user_message",
1079+
content: extractPromptText(blocks),
1080+
timestamp: Date.now(),
1081+
});
1082+
10681083
try {
10691084
const result = await trpcClient.agent.prompt.mutate({
10701085
sessionId: session.taskRunId,
@@ -1082,6 +1097,8 @@ export class SessionService {
10821097
const errorDetails = (error as { data?: { details?: string } }).data
10831098
?.details;
10841099

1100+
sessionStoreSetters.clearOptimisticItems(session.taskRunId);
1101+
10851102
if (isFatalSessionError(errorMessage, errorDetails)) {
10861103
log.error("Fatal prompt error, setting session to error state", {
10871104
taskRunId: session.taskRunId,

apps/code/src/renderer/features/sessions/stores/sessionStore.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ export type TaskRunStatus =
2828
| "failed"
2929
| "cancelled";
3030

31+
export type OptimisticItem = {
32+
type: "user_message";
33+
id: string;
34+
content: string;
35+
timestamp: number;
36+
};
37+
3138
export interface AgentSession {
3239
taskRunId: string;
3340
taskId: string;
@@ -63,6 +70,7 @@ export interface AgentSession {
6370
cloudBranch?: string | null;
6471
/** Number of session/prompt events to skip from polled logs (set during resume) */
6572
skipPolledPromptCount?: number;
73+
optimisticItems?: OptimisticItem[];
6674
}
6775

6876
// --- Config Option Helpers ---
@@ -191,6 +199,7 @@ export {
191199
useConfigOptionForTask,
192200
useModeConfigOptionForTask,
193201
useModelConfigOptionForTask,
202+
useOptimisticItemsForTask,
194203
usePendingPermissionsForTask,
195204
useQueuedMessagesForTask,
196205
useSessionForTask,
@@ -334,6 +343,41 @@ export const sessionStoreSetters = {
334343
return result;
335344
},
336345

346+
appendOptimisticItem: (
347+
taskRunId: string,
348+
item: Omit<OptimisticItem, "id">,
349+
): void => {
350+
const id = `optimistic-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
351+
useSessionStore.setState((state) => {
352+
const session = state.sessions[taskRunId];
353+
if (session) {
354+
(session.optimisticItems ??= []).push({ ...item, id });
355+
}
356+
});
357+
},
358+
359+
clearOptimisticItems: (taskRunId: string): void => {
360+
useSessionStore.setState((state) => {
361+
const session = state.sessions[taskRunId];
362+
if (session) {
363+
session.optimisticItems = [];
364+
}
365+
});
366+
},
367+
368+
appendEventAndClearOptimisticItems: (
369+
taskRunId: string,
370+
event: AcpMessage,
371+
): void => {
372+
useSessionStore.setState((state) => {
373+
const session = state.sessions[taskRunId];
374+
if (session) {
375+
session.events.push(event);
376+
session.optimisticItems = [];
377+
}
378+
});
379+
},
380+
337381
/** O(1) lookup using taskIdIndex */
338382
getSessionByTaskId: (taskId: string): AgentSession | undefined => {
339383
const state = useSessionStore.getState();

0 commit comments

Comments
 (0)