Skip to content

Commit 454610f

Browse files
committed
Optimistically render conversation items before subscription confirms
1 parent 37db874 commit 454610f

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 trpcVanilla.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;
@@ -61,6 +68,7 @@ export interface AgentSession {
6168
cloudErrorMessage?: string | null;
6269
/** Cloud task branch */
6370
cloudBranch?: string | null;
71+
optimisticItems?: OptimisticItem[];
6472
}
6573

6674
// --- Config Option Helpers ---
@@ -189,6 +197,7 @@ export {
189197
useConfigOptionForTask,
190198
useModeConfigOptionForTask,
191199
useModelConfigOptionForTask,
200+
useOptimisticItemsForTask,
192201
usePendingPermissionsForTask,
193202
useQueuedMessagesForTask,
194203
useSessionForTask,
@@ -332,6 +341,41 @@ export const sessionStoreSetters = {
332341
return result;
333342
},
334343

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

0 commit comments

Comments
 (0)