fix: clear stale workspace activity indicators

This commit is contained in:
Federico Jaramillo Martinez
2026-05-24 08:15:47 +02:00
parent 56fa641a8d
commit 679008d132
5 changed files with 128 additions and 11 deletions
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it } from "vitest";
import { api as defaultApi, type MessagePage, type SessionInfo, type SessionStatus, type Workspace } from "../api";
import { api as defaultApi, type MessagePage, type SessionActivity, type SessionInfo, type SessionStatus, type Workspace } from "../api";
import { loadCachedNewSessions, markCachedNewSessionInfo, rememberCachedNewSession } from "../cachedNewSessions";
import { initialAppState, type AppState } from "../appState";
import { loadDraft, saveDraft } from "../promptDraftStorage";
@@ -96,6 +96,30 @@ describe("SessionController", () => {
Object.defineProperty(globalThis, "localStorage", { value: undefined, configurable: true });
});
it("clears stale active activity when an idle status arrives", () => {
const activeActivity: SessionActivity = { sessionId: oldSession.id, phase: "active", label: "running tool", at: "2026-05-15T00:00:00.000Z" };
let state: AppState = {
...initialAppState(),
selectedSession: oldSession,
sessions: [oldSession],
activity: activeActivity,
sessionActivities: { [oldSession.id]: activeActivity },
};
const controller = new SessionController(
() => state,
(patch) => { state = { ...state, ...patch }; },
() => undefined,
undefined,
{ socket: new FakeSocket() },
);
controller.applyGlobalEvent({ type: "status.update", status: status(oldSession.id) });
expect(state.activity).toBeUndefined();
expect(state.sessionActivities[oldSession.id]).toBeUndefined();
expect(state.sessionStatuses[oldSession.id]).toMatchObject({ sessionId: oldSession.id, isStreaming: false });
});
it("recreates missing browser-cached new sessions and moves their draft", async () => {
const storage = new MemoryStorage();
Object.defineProperty(globalThis, "localStorage", { value: storage, configurable: true });
@@ -5,6 +5,7 @@ import { clearDraft, moveDraft, saveDraft } from "../promptDraftStorage";
import { ChatTranscriptStore } from "../chatTranscriptStore";
import { isShellInput } from "../inputModes";
import { SessionSocket, type GlobalSessionEvent, type SessionUiEvent } from "../sessionSocket";
import { isSessionActive } from "../../../shared/activity";
import { InMemorySessionSelectionMemory, markSessionArchived, markSessionsArchived, selectPreferredSession, selectionAfterArchivingSession, selectionAfterArchivingSessions, shouldDeselectAfterArchivedCollapse, type SessionSelectionMemory } from "./sessionSelection";
import type { GetState, SetState, UpdateUrl } from "./types";
@@ -443,9 +444,13 @@ export class SessionController {
}
private applyStatus(status: SessionStatus) {
const state = this.getState();
const clearsStaleActivity = state.sessionActivities[status.sessionId]?.phase === "active" && !isSessionActive(status);
this.setState({
sessionStatuses: { ...this.getState().sessionStatuses, [status.sessionId]: status },
status: this.getState().selectedSession?.id === status.sessionId ? status : this.getState().status,
sessionStatuses: { ...state.sessionStatuses, [status.sessionId]: status },
...(clearsStaleActivity ? { sessionActivities: omitSessionActivity(state.sessionActivities, status.sessionId) } : {}),
status: state.selectedSession?.id === status.sessionId ? status : state.status,
activity: state.selectedSession?.id === status.sessionId && clearsStaleActivity ? undefined : state.activity,
});
if (this.catchupStreamSessionId === status.sessionId && !status.isStreaming) this.finishStreamCatchup(status.sessionId);
}
@@ -536,6 +541,10 @@ export class SessionController {
}
}
function omitSessionActivity(activities: Record<string, SessionActivity>, sessionId: string): Record<string, SessionActivity> {
return Object.fromEntries(Object.entries(activities).filter(([id]) => id !== sessionId));
}
function isTranscriptEvent(event: SessionUiEvent): boolean {
return ["message.append", "assistant.delta", "assistant.thinking.delta", "tool.start", "tool.update", "tool.end", "shell.start", "shell.chunk", "shell.end", "command.output", "session.error"].includes(event.type);
}