Skip to content

Commit b60e2a4

Browse files
fix: show generating indicator during cloud runs (#1891)
1 parent d7b8cbb commit b60e2a4

2 files changed

Lines changed: 125 additions & 2 deletions

File tree

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

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,11 +242,15 @@ vi.mock("@utils/queryClient", () => ({
242242
vi.mock("@shared/utils/urls", () => ({
243243
getCloudUrlFromRegion: () => "https://api.anthropic.com",
244244
}));
245+
const mockConvertStoredEntriesToEvents = vi.hoisted(() =>
246+
vi.fn<(entries: unknown[]) => unknown[]>(() => []),
247+
);
248+
245249
vi.mock("@utils/session", async () => {
246250
const actual =
247251
await vi.importActual<typeof import("@utils/session")>("@utils/session");
248252
return {
249-
convertStoredEntriesToEvents: vi.fn(() => []),
253+
convertStoredEntriesToEvents: mockConvertStoredEntriesToEvents,
250254
createUserPromptEvent: vi.fn((prompt, ts) => ({
251255
type: "acp_message",
252256
ts,
@@ -319,6 +323,7 @@ const createMockSession = (
319323
describe("SessionService", () => {
320324
beforeEach(() => {
321325
vi.clearAllMocks();
326+
mockConvertStoredEntriesToEvents.mockImplementation(() => []);
322327
resetSessionService();
323328
mockSettingsState.customInstructions = "";
324329
mockGetIsOnline.mockReturnValue(true);
@@ -716,6 +721,119 @@ describe("SessionService", () => {
716721
});
717722
});
718723

724+
it("flips isPromptPending on hydration when the log tail has an in-flight prompt", async () => {
725+
const service = getSessionService();
726+
const hydratedSession = createMockSession({
727+
taskRunId: "run-123",
728+
taskId: "task-123",
729+
status: "disconnected",
730+
isCloud: true,
731+
events: [],
732+
});
733+
mockSessionStoreSetters.getSessionByTaskId.mockReturnValue(
734+
hydratedSession,
735+
);
736+
mockTrpcLogs.readLocalLogs.query.mockResolvedValue("");
737+
mockTrpcLogs.fetchS3Logs.query.mockResolvedValue("{}");
738+
mockTrpcLogs.writeLocalLogs.mutate.mockResolvedValue(undefined);
739+
740+
const inFlightPrompt = {
741+
type: "acp_message" as const,
742+
ts: 1700000000,
743+
message: {
744+
jsonrpc: "2.0" as const,
745+
id: 42,
746+
method: "session/prompt",
747+
params: { prompt: [{ type: "text", text: "hi" }] },
748+
},
749+
};
750+
mockConvertStoredEntriesToEvents.mockReturnValueOnce([inFlightPrompt]);
751+
752+
service.watchCloudTask(
753+
"task-123",
754+
"run-123",
755+
"https://api.anthropic.com",
756+
123,
757+
undefined,
758+
"https://logs.example.com/run-123",
759+
);
760+
761+
await vi.waitFor(() => {
762+
expect(mockSessionStoreSetters.updateSession).toHaveBeenCalledWith(
763+
"run-123",
764+
expect.objectContaining({
765+
isPromptPending: true,
766+
promptStartedAt: inFlightPrompt.ts,
767+
currentPromptId: 42,
768+
}),
769+
);
770+
});
771+
});
772+
773+
it("leaves isPromptPending false on hydration when the log tail has a completed prompt", async () => {
774+
const service = getSessionService();
775+
const hydratedSession = createMockSession({
776+
taskRunId: "run-123",
777+
taskId: "task-123",
778+
status: "disconnected",
779+
isCloud: true,
780+
events: [],
781+
});
782+
mockSessionStoreSetters.getSessionByTaskId.mockReturnValue(
783+
hydratedSession,
784+
);
785+
mockSessionStoreSetters.getSessions.mockReturnValue({
786+
"run-123": { ...hydratedSession, currentPromptId: 42 },
787+
});
788+
mockTrpcLogs.readLocalLogs.query.mockResolvedValue("");
789+
mockTrpcLogs.fetchS3Logs.query.mockResolvedValue("{}");
790+
mockTrpcLogs.writeLocalLogs.mutate.mockResolvedValue(undefined);
791+
792+
const promptRequest = {
793+
type: "acp_message" as const,
794+
ts: 1700000000,
795+
message: {
796+
jsonrpc: "2.0" as const,
797+
id: 42,
798+
method: "session/prompt",
799+
params: { prompt: [{ type: "text", text: "hi" }] },
800+
},
801+
};
802+
const promptResponse = {
803+
type: "acp_message" as const,
804+
ts: 1700000005,
805+
message: {
806+
jsonrpc: "2.0" as const,
807+
id: 42,
808+
result: { stopReason: "end_turn" },
809+
},
810+
};
811+
mockConvertStoredEntriesToEvents.mockReturnValueOnce([
812+
promptRequest,
813+
promptResponse,
814+
]);
815+
816+
service.watchCloudTask(
817+
"task-123",
818+
"run-123",
819+
"https://api.anthropic.com",
820+
123,
821+
undefined,
822+
"https://logs.example.com/run-123",
823+
);
824+
825+
await vi.waitFor(() => {
826+
expect(mockSessionStoreSetters.updateSession).toHaveBeenCalledWith(
827+
"run-123",
828+
expect.objectContaining({
829+
isPromptPending: false,
830+
promptStartedAt: null,
831+
currentPromptId: null,
832+
}),
833+
);
834+
});
835+
});
836+
719837
it("ignores stale async starts when the same watcher is replaced", async () => {
720838
const service = getSessionService();
721839
let resolveFirstWatchStart!: () => void;

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2585,12 +2585,17 @@ export class SessionService {
25852585
return;
25862586
}
25872587

2588+
const events = convertStoredEntriesToEvents(rawEntries);
25882589
sessionStoreSetters.updateSession(taskRunId, {
2589-
events: convertStoredEntriesToEvents(rawEntries),
2590+
events,
25902591
isCloud: true,
25912592
logUrl: logUrl ?? session.logUrl,
25922593
processedLineCount: rawEntries.length,
25932594
});
2595+
// Without this the "Galumphing…" indicator stays hidden when the hydrated
2596+
// baseline already contains an in-flight session/prompt — the live delta
2597+
// path otherwise sees delta <= 0 and never re-evaluates the tail.
2598+
this.updatePromptStateFromEvents(taskRunId, events);
25942599
})().catch((err: unknown) => {
25952600
log.warn("Failed to hydrate cloud task session from logs", {
25962601
taskId,

0 commit comments

Comments
 (0)