Skip to content

feat(workspaces): settle quiet workspaces onto a per-project shelf - #663

Merged
danyaberezun merged 26 commits into
mainfrom
workspace-archive-research
Oct 9, 2026
Merged

danyaberezun merged 26 commits into
mainfrom
workspace-archive-research

Conversation

@danyaberezun

@danyaberezun danyaberezun commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The per-project workspace list only grows. Every worktree ever created stays in the rail in creation order until it is removed, and Remove is too destructive for the common case — it deletes chats, terminals and the worktree. On a daily-use machine that is dozens of rows (48 on the author's), most of them finished or dormant, with the live handful buried among them. There was no way to say "I'm done with this, keep it" and no way for the rail to notice that a workspace's PR merged two weeks ago.

A survey of how agentic IDEs handle the same list (Conductor, Vibe Kanban, T3 Code, Cursor, the Codex app, Claude Code desktop, Zed, Mux, Superset) plus the classic references (GitHub Desktop, Arc, Linear, Codespaces) showed two failure modes to avoid: hiding rows somewhere unreachable (Codex's Settings → Archived, Claude Code desktop's auto-archive with no shelf — both with bug reports to match) and leaving all the housekeeping to the user. The design chosen from a mockup gallery is T3 Code's: rows settle themselves on signals, into a visible shelf, with manual overrides.

Approach

A Settled shelf per project, derived from host-owned facts by one client selector.

  • Facts live on the record, the partition does not. Workspace gains lastActiveAt, settledOverride/settledAt and a host-kept review snapshot; which rows are live and which sit on the shelf is a client selector over those plus session state. No "settled" boolean can disagree with the rules.
  • Rules, in order: agent working / needs attention / Default → live; your override (Settle / Keep active) wins both ways; the workspace you selected while it was live stays live (re-selecting it never re-judges it); PR merged or closed with no newer activity → shelf; PR open → live however idle; idle longer than the window (default 7 days, Settings → Workspaces: 1/3/7/14/Never) → shelf. A week, not a long weekend: merged/closed PRs already settle at once, so the idle rule only catches dormant work and should err toward keeping a paused workspace live.
  • Activity is work, not looking. Chat/agent turns, terminal input and a HEAD move stamp lastActiveAt (coalesced to one write per minute); selecting or viewing a workspace does not, so opening a settled workspace keeps it on the shelf — force-revealed and announced as WORKSPACE · settled — until you Keep active or actually work in it. Real activity also clears either override, so a pin or a park never goes stale.
  • One PR lookup, two consumers. branch-review asks for an open review first (one call, as before) and pages the newest merged/closed rows only when there is none — a combined page is ordered by creation and could bury an older open PR under newer closed ones — and surfaces a reliable bit; the host refreshes snapshots on list, focus and a 5-minute timer — live rows every pass, idle-settled rows at most every 30 minutes so a PR opened for a dormant branch from outside still brings it back — and only writes a reliable answer, so a gh outage never erases a known merged state. Its remote-host detection (git config / git remote) is asynchronous, so that pass never blocks the host (the synchronous reads cost ~45 ms of frozen event loop per row). The topbar PULL REQUEST chip reads the same fact (Merged / Closed tones) and falls back to the snapshot when its own lookup has none. Because workspace.openReview now also returns merged/closed reviews, the plan pane's PR actions key off isOpenBranchReview — a merged PR no longer reads as "PR exists → push updates".
  • The shelf is a list, not an archive; Remove stays the only teardown. Nothing about settling touches disk, and each row keeps its own Remove. A bulk Remove all settled… is deliberately not in this PR: today's teardown runs a synchronous git worktree remove per row (~4 s of frozen host for a worktree with a 70k-file node_modules) and drops the record before an in-memory cleanup chain, so a bulk version would freeze the host for minutes and orphan worktrees on a mid-way stop. Make workspace removal non-blocking and crash-safe, then bring back bulk removal for the Settled shelf #688 tracks the non-blocking, crash-safe two-phase removal (mark now, async cleanup, resume on restart) that bulk removal will sit on.
  • Upgrade path, announced once. Records without a stamp take their newest chat's time (else the worktree's .git gitfile mtime), so an existing backlog settles itself on first launch; open-PR rows stay live. Because that empties most of a long rail at once, the first automatic move is announced once per browser — Moved N quiet workspaces to Settled, with the rules and a Show action that opens those shelves. The count waits for the shelf to stop changing for two seconds (the backfill lands as a burst of pushes), and rows you parked by hand never trigger it.
  • Live order is most-recent-first (sort row: Recent activity · Created · Name, per browser). This changed the rail's order, so existing E2E specs that addressed rows by position were moved to name-based lookup (worktreeRow(page, name)), including the two added to feat(web): redesign the topbar location strip as captioned segments #659 after this branch first rebased onto it.

Changes

  • contracts (v78): Workspace.lastActiveAt / settledOverride / settledAt / review, OpenBranchReview.state / changedAt, workspace.settle / unsettle, AppConfig.settleIdleDays (+ SETTLE_IDLE_DAYS with default 7, isSettleIdleDays), WORKSPACE_SETTLE_PROTOCOL_VERSION.
  • server/branch-review: all-state lookup, findBranchReviewOutcome { value, reliable } replaces findOpenBranchReview (value-only form removed; pr tests migrated); remote-host detection runs through gitAsync under the lookup timeout.
  • server/workspaces: recordWorkspaceActivity, backfillWorkspaceActivity, recordWorkspaceHead (sha-based commit proxy — the git-dir watcher also fires on index refreshes), settleWorkspace / unsettleWorkspace (Default refused), setWorkspaceReview (branch-fenced); creation stamps lastActiveAt.
  • server/host: new settledLifecycle.ts (session-state stamp, reviewRefreshRows, per-project backfill + review refresh pass, 5-minute timer), handlers for the two methods, terminal.write stamps the workspace writeTerminal returns, repo-metadata callback calls recordWorkspaceHead, pr.open stamps activity and persists the created PR against the branch it pushed; settings/persistence validate settleIdleDays.
  • web/store: selectWorkspaceSettledReason / selectWorkspacePartition(state, projectId, now) / selectAutoSettledCounts + settledReasonLabel/Title; workspaceSort (persisted by panels/workspaceSort.ts), settledShelfExpanded/Shown, activeWorkspaceLiveLatch (set in workspaceActivationPatch, which activateWorkspaceFromRoute now reuses; re-armed by a push that advances the active row's lastActiveAt).
  • web/panels: ProjectTree sort row, live rows with hover ✓ Settle, Settled · N shelf with reason chips, hover ↩ Keep active, 10-row paging, force-reveal, 30-second useNow tick, the one-time first-move notice (settledShelfNotice.ts); WorkspacesSettings section; workspaceActions gains canSettleWorkspace / settleWorkspace / unsettleWorkspace; lib.compactAge.
  • web/shell/locationBar: Settle / Keep active in the WORKSPACE menu, · settled caption fragment, Switch to = live siblings in rail order + Settled · N › submenu, PR chip tones (reviewTone / reviewChipLabel), Remote row only for open reviews, snapshot fallback in LocationBar.
  • Specs updated where the boundary moved: contracts, server workspaces / branch-review / settings / host / plan review / terminal / watch, web store / panels / locationBar.
  • README.md: the Settled shelf in What you get; workbench-{light,dark}.png recaptured on the current UI (captioned topbar from feat(web): redesign the topbar location strip as captioned segments #659, sort row, shelf).
  • E2E: new settled-shelf.spec.ts (manual overrides, topbar mirror, force-reveal, the first-move notice once per browser, pre-v78 host, idle-window setting); worktreeRow(page, name) fixture; history-jump, terminals, workspace-tabs, new-workspace.live, topbar-location moved off positional row lookups.

Review follow-ups

Rounds one to five (automated findings plus independent re-reviews of the whole diff) hardened the lifecycle facts — idle-settled rows back in the review refresh on a 30-minute pace; .git mtime backfill for external worktrees; the plan pane's PR stage ignoring merged/closed reviews; the live latch carried through re-activation and history jumps; HEAD baselines seeded at watch start; open reviews asked for before settled pages; a stamp on a review-settled row never coalesced away; review snapshots branch-fenced and dropped on every branch transition; coalesced activity keeping a high-water mark; incomplete settled-review timestamps failing live-safe; pre-v78 clients answered with null for merged/closed reviews; and pr.open persisting the created PR against the exact branch it pushed. Those rounds also hardened a guarded bulk removal, which has since moved out (below).

Sixth round (Rinat S (@rsolmano)'s review):

  • Bulk Remove all settled… → Make workspace removal non-blocking and crash-safe, then bring back bulk removal for the Settled shelf #688. The review was right on every count: the request rechecked every row before replying (past the client's 60 s timeout on a slow connection), teardown runs a synchronous git worktree remove per row, and the record is dropped before an in-memory cleanup chain, so a stop mid-way orphans worktrees and transcripts. The dialog, both methods, the host guard and its guard-only busy probes are removed from this PR; the issue carries the two-phase design, and the removed code is kept on settled-shelf-bulk-remove.
  • Idle window defaults to 7 days (1/3/7/14/Never unchanged).
  • First launch after upgrade is announced with the one-time notice above, as suggested. Not taken: starting the idle clock at upgrade time — every backfilled row would get the same stamp, so the whole backlog would still move at once, just a week later and further from the cause.
  • README gains the shelf, and the workbench screenshots are recaptured (they also still showed the topbar from before feat(web): redesign the topbar location strip as captioned segments #659).
  • Also found while measuring: remote-host detection in branch-review was synchronous and now runs every five minutes for every candidate row; it is asynchronous now.
  • Not taken: the terminal.write disk-read nit — reading and parsing the real 26 KB / 62-row workspaces.json costs ~42 µs per input chunk, and the suggested in-memory flag would add a second owner of override/review state that every writer must keep in sync. Answered on the thread.

Screenshots

1280×820, light theme. The rail and menu pairs use the seeded project from the earlier rounds (six worktrees: one fresh, one merged PR, one closed PR, one open-but-stale PR, one parked by hand, one idle 20 days); before = origin/redesign-top-panel-breadcrumb (since merged to main as #659), after = this branch. The new-surface shots use a disposable clone with five worktrees, three of them backdated.

Rail + topbar — before: every worktree in one flat creation-ordered list; no PR state for a merged branch.

rail before: flat list

Rail + topbar — after: sort row, live rows, Settled · 4 shelf with reason chips (BY YOU · MERGED · CLOSED · IDLE 2W); the opened merged workspace stays on the shelf, the caption reads WORKSPACE · SETTLED, the chip reads Merged #618.

rail after: settled shelf

WORKSPACE menu — before / after: Settle ↔ Keep active beside Reveal; Switch to shows live siblings then a Settled · 3 › submenu with reasons.

topbar menu before
topbar menu after

New surfaces (after only): the one-time first-move notice and what Show opens (staged by backdating three rows), and Settings → Workspaces with the week default.

first-move notice
after Show
settings workspaces

The README's workbench-{light,dark}.png are in the diff: the same composition as before (rail · chat with its tool steps · args.ts diff · Specs + Changes) on the current UI, staged on a disposable clone in the isolated e2e host with a seeded transcript rather than a live model run.

Related issues

None — design request from the author; the brainstorm, survey and mockups live in the workspace's gitignored .thinkrail/context/mockups/workspace-archive/. Follow-up: #688 (non-blocking, crash-safe removal, then bulk removal for the shelf).

Checklist

  • Fast gates pass: bun run lint, bun run typecheck, bun run test — final tree lint clean (1412 files); typecheck green for all 19 packages excluding @thinkrail/desktop, whose electrobun prepare waits on the running app's build lock on this machine (no desktop sources change here); bun run test 20/20 tasks (server 1399 pass / 2 skip, web 1598, contracts 69). Also check:deps, check:boundaries, check:seams, check:spec-surface and the typography/colors/spacing/provider-glyphs checks OK.
  • E2E suite passes for app-affecting changes (bun run e2e, or bun run e2e:full when touching agent behavior) — final tree (935502da): all 8 shards / 494 tests passed. The run before it had one failure — activity-breadcrumbs "oldest-first disclosure expansion preserves … anchors", a 245 px scroll-anchor deviation under 8-lane load — which passed 5/5 in isolation and in the full rerun; this PR touches no chat or scroll code. The new settled-shelf journeys passed in focused runs at every step. No agent execution semantics changed.
  • Before/after screenshots are included for frontend changes, or marked not applicable
  • Relevant SPEC.md / top-level specs updated to reflect any boundary, contract, or behavior change
  • I have read the Contributing guide and agree to the Code of Conduct

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes — please address the blocking inline findings.

Comment thread packages/server/src/host/settledLifecycle.ts Outdated
Comment thread packages/server/src/host/settledLifecycle.ts Outdated
Comment thread apps/web/src/panels/RemoveSettledDialog.tsx Outdated
@danyaberezun
danyaberezun added this pull request to stack #664 October 6, 2026 16:03
@danyaberezun
danyaberezun marked this pull request as draft October 6, 2026 20:30
@danyaberezun
danyaberezun marked this pull request as ready for review October 6, 2026 20:30

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes — please address the blocking inline finding.

Comment thread apps/web/src/panels/RemoveSettledDialog.tsx Outdated
Comment thread packages/server/src/workspaces/workspaces.ts
Comment thread packages/server/src/branch-review/branchReview.ts Outdated

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes — please address the blocking inline finding.

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes — please address the blocking inline finding.

Comment thread packages/server/src/workspaces/workspaces.ts

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved — ready to merge.

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved — ready to merge.

@danyaberezun
danyaberezun force-pushed the workspace-archive-research branch from 4f29eb8 to 16e29f4 Compare October 8, 2026 06:55

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved — ready to merge.

@danyaberezun
danyaberezun marked this pull request as draft October 8, 2026 07:39
@danyaberezun
danyaberezun marked this pull request as ready for review October 8, 2026 07:39
@danyaberezun
danyaberezun force-pushed the workspace-archive-research branch 2 times, most recently from 16e29f4 to b4c46b6 Compare October 8, 2026 07:42

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved — ready to merge.

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved — ready to merge.

@danyaberezun
danyaberezun force-pushed the workspace-archive-research branch from b4c46b6 to 5f57262 Compare October 8, 2026 07:59

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved — ready to merge.

@rsolmano Rinat S (rsolmano) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! The shelf idea and the "facts on the host, one selector decides" design look good to me. Requesting changes for a few product points and for the bulk removal. Details are inline:

  1. Bulk "Remove all settled…": this is the main one. The current shape can freeze the host and isn't crash-safe. I suggest splitting it into "mark now, clean up in the background" (inline on removeSettledWorkspaces).
  2. Default idle window of 3 days: too aggressive as a default; please use 7 (inline on SETTLE_IDLE_DAYS).
  3. First launch after upgrade: the backlog moves to the shelf silently, all at once; please make that visible (inline on backfillActivity).
  4. README: this adds a user-visible capability, but README.md is unchanged. Per AGENTS.md (a change that adds a user-visible capability reviews README claims), please add a line about the Settled shelf, and refresh the rail screenshot in .github/readme-assets/ if it no longer matches.
  5. (optional) terminal.write hot path: inline on recordWorkspaceActivity.

Comment thread packages/server/src/host/handlers.ts Outdated
Comment thread packages/contracts/src/domain.ts Outdated
Comment thread packages/server/src/host/settledLifecycle.ts
Comment thread packages/server/src/workspaces/workspaces.ts
Base automatically changed from redesign-top-panel-breadcrumb to main October 8, 2026 13:24
…HEAD baselines at watch start

The bulk Remove used to issue N blind workspace.remove calls from the client's snapshot; it is now one
workspace.removeSettled whose host side refuses a row that a session is busy in, that the user pinned live
since, or whose activity stamp moved, and tears the rest down one worktree at a time. Watchers now take a
HEAD baseline the moment they start, so the first commit after a host restart counts as activity instead
of merely seeding the observed sha.
…close the review-fact gaps

The host guard behind workspace.removeSettled now also compares the review state the client judged a row
settled from, so a PR opened between preview and confirm keeps the row. Around the same facts: an activity
stamp on a row that a merged/closed review currently settles is never coalesced away (it is the one fact
that says "worked on since the merge"); review snapshots are fenced to the branch the lookup was made for
and dropped when folder truth moves an external worktree's branch; the never-pushed fallback of the
removal preview counts commits no remote-tracking ref reaches instead of the Changes panel's compare
base; a history jump into another workspace judges the destination through the shared activation patch
instead of inheriting the previous row's live latch; and workspace.openReview answers a client older than
the settled protocol with null for a merged or closed review, since such a client reads any review as an
open PR.
@danyaberezun
danyaberezun force-pushed the workspace-archive-research branch from 5f57262 to 935502d Compare October 8, 2026 16:15
@danyaberezun

Copy link
Copy Markdown
Collaborator Author

Thanks for the review, Rinat S (@rsolmano). All five points are addressed; the branch is also rebased onto main now that #659 has merged.

  1. Bulk removal: moved out of this PR. It will come back on top of the two-phase, non-blocking, crash-safe removal tracked in Make workspace removal non-blocking and crash-safe, then bring back bulk removal for the Settled shelf #688 (details on the thread).
  2. Idle window: now defaults to 7 days (c18f4c5e).
  3. First launch after upgrade: the first automatic move now shows a one-time Moved N quiet workspaces to Settled — Show notice (28579217).
  4. README: 935502da adds the Settled shelf to What you get, updates the hero alt text, and recaptures workbench-{light,dark}.png. The old images also still showed the topbar from before feat(web): redesign the topbar location strip as captioned segments #659. The new ones keep the same composition on the current UI: sort row, Settled · 2 shelf, captioned topbar.
  5. terminal.write nit: I'd rather not change it (~42 µs per chunk). Reasoning is on the thread.

One more change: branch-review's remote detection is now asynchronous (cfc09a11). The 5-minute lifecycle pass runs it for every candidate row, and the synchronous reads blocked the host for ~45 ms per row.

@jetbrains-air jetbrains-air Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved — ready to merge.

@danyaberezun
danyaberezun added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit 830de94 Oct 9, 2026
33 of 34 checks passed
@danyaberezun
danyaberezun deleted the workspace-archive-research branch October 9, 2026 10:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants