From be3a1ea6c3ca64f83c3cecf160853fab79b9333f Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 16 Jun 2026 16:41:07 +0700 Subject: [PATCH] Keep new workspaces recent in status groups --- packages/server/src/server/session.ts | 2 +- .../src/server/session.workspaces.test.ts | 12 ++++--- .../server/src/server/workspace-directory.ts | 32 +++++++++++++++---- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 88d083cf2..7c18b8de0 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -6736,7 +6736,7 @@ export class Session { title: result.workspace.title, archivingAt: null, status: "done", - statusEnteredAt: null, + statusEnteredAt: result.workspace.createdAt, activityAt: null, diffStat: { additions: 0, deletions: 0 }, scripts: [], diff --git a/packages/server/src/server/session.workspaces.test.ts b/packages/server/src/server/session.workspaces.test.ts index a839b58c3..72c77a389 100644 --- a/packages/server/src/server/session.workspaces.test.ts +++ b/packages/server/src/server/session.workspaces.test.ts @@ -2894,6 +2894,8 @@ test("archiving the last workspace emits a remove carrying the now-empty project test("create paseo worktree request returns a registered workspace descriptor", async () => { const emitted: SessionOutboundMessage[] = []; + const createdAt = "2026-05-12T12:00:00.000Z"; + vi.setSystemTime(new Date(createdAt)); const tempDir = realpathSync(mkdtempSync(path.join(tmpdir(), "session-worktree-test-"))); const repoDir = path.join(tempDir, "repo"); const paseoHome = path.join(tempDir, "paseo-home"); @@ -2981,6 +2983,7 @@ test("create paseo worktree request returns a registered workspace descriptor", requestId: "req-worktree", }); } finally { + vi.useRealTimers(); rmSync(tempDir, { recursive: true, force: true }); } @@ -2993,6 +2996,7 @@ test("create paseo worktree request returns a registered workspace descriptor", workspaceKind: "worktree", name: "worktree-123", status: "done", + statusEnteredAt: createdAt, }); expect(response?.payload.workspace?.id).toMatch(/^wks_[0-9a-f]{16}$/); expect(response?.payload.workspace?.workspaceDirectory).toContain(path.join("worktree-123")); @@ -4365,7 +4369,7 @@ test("listWorkspaceDescriptorsSnapshot keeps git workspaces on the baseline desc name: "main", archivingAt: null, status: "done", - statusEnteredAt: null, + statusEnteredAt: workspace.createdAt, activityAt: null, diffStat: null, } as const; @@ -4424,14 +4428,14 @@ test("buildWorkspaceDescriptorMap computes statusEnteredAt from runtime agent fi return descriptor!; }); - // 1. Empty workspace — no agents contribute. statusEnteredAt must be null - // and the workspace status is "done". + // 1. Empty workspace — no agents contribute. The workspace entered its + // initial "done" bucket when it was created. { const { session, workspace } = setupSession(); session.listAgentPayloads = async () => []; const descriptor = await buildDescriptor(session, workspace.workspaceId); expect(descriptor.status).toBe("done"); - expect(descriptor.statusEnteredAt).toBeNull(); + expect(descriptor.statusEnteredAt).toBe(workspace.createdAt); } // Agents own the workspace by id; cwd is incidental. diff --git a/packages/server/src/server/workspace-directory.ts b/packages/server/src/server/workspace-directory.ts index f7cf898c9..30ab8828d 100644 --- a/packages/server/src/server/workspace-directory.ts +++ b/packages/server/src/server/workspace-directory.ts @@ -32,8 +32,9 @@ const FETCH_WORKSPACES_SORT_KEYS = [ * `statusEnteredAt`: when the winning bucket changes from a higher-priority * mask to a lower-priority bucket, the new entry time is the unmask time * (i.e., the moment the higher-priority bucket cleared), not when the - * underlying agent originally entered the lower-priority bucket. Cleared when - * the workspace has never had contributing agents. + * underlying agent originally entered the lower-priority bucket. A fresh + * workspace enters its initial `done` bucket at creation time, even before any + * agent or terminal contributes activity. */ interface WorkspaceBucketHistoryEntry { bucket: WorkspaceStateBucket; @@ -205,6 +206,9 @@ export class WorkspaceDirectory { const includedWorkspaces = activeRecords.filter( (workspace) => !workspaceIds || workspaceIds.has(workspace.workspaceId), ); + const activeRecordsByWorkspaceId = new Map( + activeRecords.map((workspace) => [workspace.workspaceId, workspace] as const), + ); const workspaceDescriptors = await Promise.all( includedWorkspaces.map((workspace) => this.deps.buildWorkspaceDescriptor({ @@ -253,6 +257,7 @@ export class WorkspaceDirectory { contributingAgents, terminalEntries, previous: this.bucketHistoryByWorkspaceId.get(workspaceId) ?? null, + workspaceCreatedAt: activeRecordsByWorkspaceId.get(workspaceId)?.createdAt ?? null, nowIso, }); descriptor.statusEnteredAt = result.statusEnteredAt; @@ -360,8 +365,8 @@ export class WorkspaceDirectory { // - priority unmasking: when the winning bucket transitions (e.g. a // higher-priority bucket cleared), the new entry time is "now"; // - same-bucket emits reuse the previous entered-at; - // - empty workspaces that never had contributing agents or terminals get - // `statusEnteredAt: null`. + // - empty workspaces that never had contributing agents or terminals use + // their workspace creation time as their initial `done` entry time. // - when archived agents leave a previously active workspace empty, keep // the previous done timestamp or stamp the transition to done now. private resolveStatusEnteredAt(params: { @@ -370,17 +375,32 @@ export class WorkspaceDirectory { contributingAgents: AgentSnapshotPayload[]; terminalEntries: Array<{ bucket: WorkspaceStateBucket; changedAtIso: string }>; previous: WorkspaceBucketHistoryEntry | null; + workspaceCreatedAt: string | null; nowIso: string; }): { statusEnteredAt: string | null; recordUpdate?: WorkspaceBucketHistoryEntry; recordDelete?: true; } { - const { winningBucket, contributingAgents, terminalEntries, previous, nowIso } = params; + const { + winningBucket, + contributingAgents, + terminalEntries, + previous, + workspaceCreatedAt, + nowIso, + } = params; if (contributingAgents.length === 0 && terminalEntries.length === 0) { if (!previous) { - return { statusEnteredAt: null }; + if (!workspaceCreatedAt) { + return { statusEnteredAt: null }; + } + + return { + statusEnteredAt: workspaceCreatedAt, + recordUpdate: { bucket: "done", enteredAt: workspaceCreatedAt }, + }; } const enteredAt = previous.bucket === "done" ? previous.enteredAt : nowIso;