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

Commit d7ca30a

Browse files
committed
fix(ui): keep resumed prompt before restore progress
1 parent d441802 commit d7ca30a

3 files changed

Lines changed: 55 additions & 3 deletions

File tree

packages/ui/src/features/sessions/components/mergeConversationItems.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,24 @@ import { describe, expect, it } from "vitest";
22
import type { ConversationItem } from "./buildConversationItems";
33
import { mergeConversationItems } from "./mergeConversationItems";
44

5+
function progressGroup(id: string): ConversationItem {
6+
return {
7+
type: "session_update",
8+
id,
9+
update: {
10+
sessionUpdate: "progress_group",
11+
steps: [],
12+
isActive: true,
13+
},
14+
turnContext: {
15+
toolCalls: new Map(),
16+
childItems: new Map(),
17+
turnCancelled: false,
18+
turnComplete: false,
19+
},
20+
};
21+
}
22+
523
function userMessage(
624
id: string,
725
content: string,
@@ -160,6 +178,19 @@ describe("mergeConversationItems", () => {
160178
expect(result.map((i) => i.id)).toEqual(["setup", "opt"]);
161179
});
162180

181+
it("cloud: keeps a resumed prompt before trailing setup progress", () => {
182+
const result = mergeConversationItems({
183+
conversationItems: [
184+
userMessage("old", "previous prompt"),
185+
progressGroup("restore"),
186+
],
187+
optimisticItems: [userMessage("opt", "follow up", false)],
188+
isCloud: true,
189+
});
190+
191+
expect(result.map((item) => item.id)).toEqual(["old", "opt", "restore"]);
192+
});
193+
163194
it("cloud: does not dedupe historical messages against tail follow-up optimistics", () => {
164195
const result = mergeConversationItems({
165196
conversationItems: [

packages/ui/src/features/sessions/components/mergeConversationItems.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ function strippedUserContent(content: string): string {
2828
}
2929

3030
// Cloud's initial optimistic is pinned to the top so the user's prompt stays
31-
// visible above setup progress. Follow-up optimistics render at the tail until
32-
// the streamed `session/prompt` arrives and replaces them.
31+
// visible above setup progress. Follow-up optimistics render at the tail, but
32+
// before trailing progress cards, to match where the streamed `session/prompt`
33+
// will appear.
3334
//
3435
// Local sessions keep optimistic at the chronological end — they rely on
3536
// `replaceOptimisticWithEvent` to swap optimistic↔real in place.
@@ -103,9 +104,23 @@ export function mergeConversationItems({
103104
};
104105
});
105106

107+
let tailInsertionIndex = dedupedConversation.length;
108+
while (tailInsertionIndex > 0) {
109+
const item = dedupedConversation[tailInsertionIndex - 1];
110+
if (
111+
item.type === "session_update" &&
112+
item.update.sessionUpdate === "progress_group"
113+
) {
114+
tailInsertionIndex--;
115+
} else {
116+
break;
117+
}
118+
}
119+
106120
return [
107121
...resolvedPinnedItems,
108-
...dedupedConversation,
122+
...dedupedConversation.slice(0, tailInsertionIndex),
109123
...tailOptimisticItems,
124+
...dedupedConversation.slice(tailInsertionIndex),
110125
];
111126
}

packages/ui/src/features/sessions/sessionServiceHost.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,9 @@ const mockConvertStoredEntriesToEvents = vi.hoisted(() =>
356356
) => unknown[]
357357
>(() => []),
358358
);
359+
const mockHasSessionPromptEventForTaskRun = vi.hoisted(() =>
360+
vi.fn(() => false),
361+
);
359362

360363
vi.mock("@posthog/core/sessions/sessionEvents", async () => {
361364
const actual = await vi.importActual<
@@ -387,6 +390,7 @@ vi.mock("@posthog/core/sessions/sessionEvents", async () => {
387390
getStoredLogEventPosition: actual.getStoredLogEventPosition,
388391
getUserShellExecutesSinceLastPrompt: vi.fn(() => []),
389392
hasSessionPromptEvent: actual.hasSessionPromptEvent,
393+
hasSessionPromptEventForTaskRun: mockHasSessionPromptEventForTaskRun,
390394
isAbsoluteFolderPath: actual.isAbsoluteFolderPath,
391395
isFatalSessionError: actual.isFatalSessionError,
392396
isRateLimitError: actual.isRateLimitError,
@@ -446,6 +450,7 @@ describe("SessionService", () => {
446450
beforeEach(() => {
447451
vi.clearAllMocks();
448452
mockConvertStoredEntriesToEvents.mockImplementation(() => []);
453+
mockHasSessionPromptEventForTaskRun.mockReturnValue(false);
449454
resetSessionService();
450455
mockSettingsState.customInstructions = "";
451456
mockSettingsState.spokenNotifications = false;
@@ -4374,6 +4379,7 @@ describe("SessionService", () => {
43744379
resumePrompt,
43754380
resumeCompletion,
43764381
]);
4382+
mockHasSessionPromptEventForTaskRun.mockReturnValueOnce(true);
43774383

43784384
service.watchCloudTask(
43794385
"task-123",

0 commit comments

Comments
 (0)