Skip to content

Commit 8ff7273

Browse files
committed
fix: keep optimistic user bubble through the cloud log echo
After the queue auto-flush dequeues a message, sendCloudPrompt fires the user_message command but the cloud log stream takes a moment to echo back the session/prompt event. Without an optimistic placeholder, the bubble vanishes during that window and the user thinks the message was dropped. Mirror the local optimistic flow: append a user_message item before the mutate, drop it when the echo arrives, and clear on send failure or retry exhaustion. Generated-By: PostHog Code Task-Id: 8aeaf6f9-8b18-426a-9453-c668ca17d227
1 parent 909d6c7 commit 8ff7273

1 file changed

Lines changed: 59 additions & 4 deletions

File tree

  • apps/code/src/renderer/features/sessions/service

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

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ const LOCAL_SESSION_RECOVERY_MESSAGE =
9292
const LOCAL_SESSION_RECOVERY_FAILED_MESSAGE =
9393
"Connecting to to the agent has been lost. Retry, or start a new session.";
9494

95+
function isUserPromptEcho(acpMsg: AcpMessage): boolean {
96+
return (
97+
isJsonRpcRequest(acpMsg.message) &&
98+
acpMsg.message.method === "session/prompt"
99+
);
100+
}
101+
95102
/**
96103
* Build default configOptions for cloud sessions so the mode switcher
97104
* is available in the UI even without a local agent connection.
@@ -1518,6 +1525,17 @@ export class SessionService {
15181525
isPromptPending: true,
15191526
});
15201527

1528+
// Show the bubble immediately while we wait for the cloud log stream to
1529+
// echo the user_message back. Without this the user sees a gap between
1530+
// submit (or queue drain) and the agent's response.
1531+
if (!options?.skipQueueGuard) {
1532+
sessionStoreSetters.appendOptimisticItem(session.taskRunId, {
1533+
type: "user_message",
1534+
content: transport.promptText,
1535+
timestamp: Date.now(),
1536+
});
1537+
}
1538+
15211539
track(ANALYTICS_EVENTS.PROMPT_SENT, {
15221540
task_id: session.taskId,
15231541
is_initial: session.events.length === 0,
@@ -1565,6 +1583,12 @@ export class SessionService {
15651583
sessionStoreSetters.updateSession(session.taskRunId, {
15661584
isPromptPending: false,
15671585
});
1586+
// Drop optimistic items so a failed send doesn't leave a ghost bubble.
1587+
// The combined-prompt path (skipQueueGuard) clears its own optimistic
1588+
// items in sendQueuedCloudMessages on retry exhaustion.
1589+
if (!options?.skipQueueGuard) {
1590+
sessionStoreSetters.clearOptimisticItems(session.taskRunId);
1591+
}
15681592
throw error;
15691593
}
15701594
}
@@ -1574,10 +1598,29 @@ export class SessionService {
15741598
attempt = 0,
15751599
pendingPrompt?: string | ContentBlock[],
15761600
): Promise<{ stopReason: string }> {
1577-
// First attempt: atomically dequeue. Retries reuse the already-dequeued prompt.
1578-
const combinedPrompt =
1579-
pendingPrompt ??
1580-
combineQueuedCloudPrompts(sessionStoreSetters.dequeueMessages(taskId));
1601+
// First attempt: atomically dequeue and convert each entry into an
1602+
// optimistic bubble. Retries reuse the already-dequeued prompt and must
1603+
// not stack additional bubbles.
1604+
let combinedPrompt: string | ContentBlock[] | null;
1605+
if (pendingPrompt) {
1606+
combinedPrompt = pendingPrompt;
1607+
} else {
1608+
const dequeued = sessionStoreSetters.dequeueMessages(taskId);
1609+
combinedPrompt = combineQueuedCloudPrompts(dequeued);
1610+
if (combinedPrompt) {
1611+
const taskRunId =
1612+
sessionStoreSetters.getSessionByTaskId(taskId)?.taskRunId;
1613+
if (taskRunId) {
1614+
for (const msg of dequeued) {
1615+
sessionStoreSetters.appendOptimisticItem(taskRunId, {
1616+
type: "user_message",
1617+
content: msg.content,
1618+
timestamp: msg.queuedAt,
1619+
});
1620+
}
1621+
}
1622+
}
1623+
}
15811624
if (!combinedPrompt) return { stopReason: "skipped" };
15821625

15831626
const session = sessionStoreSetters.getSessionByTaskId(taskId);
@@ -1632,6 +1675,10 @@ export class SessionService {
16321675
taskId,
16331676
attempts: attempt + 1,
16341677
});
1678+
const failedSession = sessionStoreSetters.getSessionByTaskId(taskId);
1679+
if (failedSession) {
1680+
sessionStoreSetters.clearOptimisticItems(failedSession.taskRunId);
1681+
}
16351682
toast.error("Failed to send follow-up message. Please try again.");
16361683
return { stopReason: "error" };
16371684
}
@@ -2901,6 +2948,9 @@ export class SessionService {
29012948
);
29022949
sessionStoreSetters.appendEvents(taskRunId, newEvents, expectedCount);
29032950
this.updatePromptStateFromEvents(taskRunId, newEvents);
2951+
if (newEvents.some(isUserPromptEcho)) {
2952+
sessionStoreSetters.clearOptimisticItems(taskRunId);
2953+
}
29042954
} else {
29052955
// Gap in data — append everything we have but don't jump processedLineCount
29062956
log.warn("Cloud task log count inconsistency", {
@@ -2921,6 +2971,9 @@ export class SessionService {
29212971
currentCount + update.newEntries.length,
29222972
);
29232973
this.updatePromptStateFromEvents(taskRunId, newEvents);
2974+
if (newEvents.some(isUserPromptEcho)) {
2975+
sessionStoreSetters.clearOptimisticItems(taskRunId);
2976+
}
29242977
}
29252978
}
29262979

@@ -2983,6 +3036,7 @@ export class SessionService {
29833036
if (session && session.messageQueue.length > 0) {
29843037
const queued = sessionStoreSetters.dequeueMessages(session.taskId);
29853038
const combinedPrompt = combineQueuedCloudPrompts(queued);
3039+
sessionStoreSetters.clearOptimisticItems(taskRunId);
29863040
sessionStoreSetters.updateSession(taskRunId, {
29873041
isPromptPending: false,
29883042
});
@@ -2998,6 +3052,7 @@ export class SessionService {
29983052
});
29993053
}
30003054
} else if (session?.isPromptPending) {
3055+
sessionStoreSetters.clearOptimisticItems(taskRunId);
30013056
sessionStoreSetters.updateSession(taskRunId, {
30023057
isPromptPending: false,
30033058
});

0 commit comments

Comments
 (0)