fix: clear stale workspace activity

This commit is contained in:
Federico Jaramillo Martinez
2026-05-19 21:41:58 +02:00
parent 1232610bf1
commit 619840a398
5 changed files with 111 additions and 9 deletions
@@ -32,6 +32,47 @@ describe("WorkspaceActivityService", () => {
expect(events.at(-1)).toMatchObject({ type: "workspace.activity", activity: { cwd: "/repo", hasSessionActivity: false, hasTerminalActivity: false } });
});
it("clears stale active activity when an idle status arrives", () => {
const events: RealtimeEvent[] = [];
const service = new WorkspaceActivityService({ publishRealtime: (event) => events.push(event) });
service.applySessionActivity("/repo", { sessionId: "s1", phase: "active", label: "running tool", detail: "read", at: "now" });
service.applySessionStatus("/repo", status({ isStreaming: false }));
expect(service.snapshot().workspaces).toEqual([]);
expect(events.at(-1)).toMatchObject({ type: "workspace.activity", activity: { cwd: "/repo", hasSessionActivity: false, hasTerminalActivity: false } });
});
it("publishes a clear event when removing an already-pruned session with a cwd", () => {
const events: RealtimeEvent[] = [];
const service = new WorkspaceActivityService({ publishRealtime: (event) => events.push(event) });
service.removeSession("missing-session", "/repo");
expect(events.at(-1)).toMatchObject({ type: "workspace.activity", activity: { cwd: "/repo", hasSessionActivity: false, hasTerminalActivity: false } });
});
it("reconciles stale session activity for a workspace", () => {
const events: RealtimeEvent[] = [];
const service = new WorkspaceActivityService({ publishRealtime: (event) => events.push(event) });
service.applySessionActivity("/repo", { sessionId: "s1", phase: "active", label: "running tool", at: "now" });
service.applySessionActivity("/repo", { sessionId: "s2", phase: "active", label: "running tool", at: "now" });
service.applySessionActivity("/other", { sessionId: "s3", phase: "active", label: "running tool", at: "now" });
service.reconcileSessionActivity("/repo", ["s2"]);
expect(service.snapshot().workspaces).toMatchObject([
{ cwd: "/other", hasSessionActivity: true, hasTerminalActivity: false },
{ cwd: "/repo", hasSessionActivity: true, hasTerminalActivity: false },
]);
service.reconcileSessionActivity("/repo", []);
expect(service.snapshot().workspaces).toMatchObject([{ cwd: "/other", hasSessionActivity: true, hasTerminalActivity: false }]);
expect(events.at(-1)).toMatchObject({ type: "workspace.activity", activity: { cwd: "/repo", hasSessionActivity: false, hasTerminalActivity: false } });
});
it("combines sessions and terminals and clears closed terminals", () => {
const events: RealtimeEvent[] = [];
const service = new WorkspaceActivityService({ publishRealtime: (event) => events.push(event) });
@@ -26,6 +26,7 @@ export class WorkspaceActivityService {
const record = this.sessions.get(status.sessionId) ?? { cwd };
record.cwd = cwd;
record.status = status;
if (!isSessionActive(status) && record.activity?.phase === "active") delete record.activity;
this.sessions.set(status.sessionId, record);
this.pruneIdleSession(status.sessionId);
this.publishChangedCwds(previousCwd, cwd);
@@ -41,10 +42,21 @@ export class WorkspaceActivityService {
this.publishChangedCwds(previousCwd, cwd);
}
removeSession(sessionId: string): void {
const cwd = this.sessions.get(sessionId)?.cwd;
removeSession(sessionId: string, cwd?: string): void {
const previousCwd = this.sessions.get(sessionId)?.cwd ?? cwd;
this.sessions.delete(sessionId);
this.publishCwd(cwd);
this.publishCwd(previousCwd);
}
reconcileSessionActivity(cwd: string, sessionIds: Iterable<string>): void {
const knownSessionIds = new Set(sessionIds);
let changed = false;
for (const [sessionId, record] of this.sessions.entries()) {
if (record.cwd !== cwd || knownSessionIds.has(sessionId)) continue;
this.sessions.delete(sessionId);
changed = true;
}
if (changed) this.publishCwd(cwd);
}
updateTerminal(terminal: Pick<TerminalInfo, "id" | "cwd" | "exited">): void {