Skip to content

Order active workspaces newest-first and pin the needs-you strip#207

Open
Zeus-Deus wants to merge 2 commits into
mainfrom
sidebar-workspace-ordering
Open

Order active workspaces newest-first and pin the needs-you strip#207
Zeus-Deus wants to merge 2 commits into
mainfrom
sidebar-workspace-ordering

Conversation

@Zeus-Deus

Copy link
Copy Markdown
Owner

Problem

New workspaces were appended to the bottom of the sidebar. With a long list of still-open workspaces, reaching the one you just created meant scrolling to the bottom every time — even when the workspaces above it were idle with no agent running.

The obvious fix is to float working / needs-input workspaces to the top. I researched how a comparable agentic dev environment handles this first, and it turns out that's the wrong answer.

What the research found

That tool's sidebar sorts by creation time, newest first, and its ordering never reacts to agent state. The load-bearing comment in their sort helper:

v2 sort: static creation order, newest thread on top. Activity NEVER reorders the list — a row holds its position from open until settled, so the screen only moves at lifecycle transitions. Status (including pending approval) is carried by each card's edge strip, not by position.

They have status-priority ordering in exactly one place: a small glanceable mobile widget, ordered attention-first. Not the list you navigate by muscle memory. A second status-priority table exists in their sidebar but is used only to pick which badge a collapsed group shows — never for position.

The reasoning holds for us: a list that reorders itself as agents flip between working → done → needs-input moves under your cursor, destroys spatial memory, and breaks our digit jump shortcuts.

What this PR does

OrderingorderActiveWorkspaces reverses AppStateSnapshot.workspaces. Every backend creation path ends in a Vec::push and close uses Vec::remove, so that array is creation order; WorkspaceSnapshot has no created_at, making the reversal both the cheapest and the only lossless way to get newest-first. No backend change, no migration. The expanded inbox and the collapsed rail share the helper so they can't drift.

Order is otherwise frozen and status-blind, matching the rationale above.

Needs-you strip — this is where we go further than the tool I looked at. They have no equivalent in the sidebar at all; blocked work is found by scanning colored pills. We already had SidebarNeedsYouStrip fully built and unit-tested but never rendered. It's now mounted in the sidebar's sticky header block, so blocked work stays visible however far you scroll.

Entries are jump-links: they activate the blocked workspace and scroll its card into view without moving it. Surfacing attention by duplication rather than reordering is precisely what lets the list below stay status-blind — you get "attention is always at the top" without paying the reordering tax.

Adaptations needed to mount it: scoped by the active repo filter so a filtered sidebar never points at a hidden workspace; jump selector now matches the flat inbox's data-inbox-card (it was written for the removed project tree's data-ws-id); capped at 4 rows with a +N more below line, since a pinned strip could otherwise swallow the sidebar.

Active-workspace escape — the selected workspace now always renders even if it settled long ago and sits past the settled paging window, and the +N hidden count excludes the escaped row so it stays truthful. (Same guarantee the other tool makes, via a different mechanism.)

Verification

  • npm run check clean; 3056 tests / 210 files pass
  • Ordering confirmed against seed data: fixtures end wsSiteMain, wsSiteRedesign, wsScratchpad, and the sidebar now renders scratchpad → landing-refresh → personal-site
  • Visually confirmed in-app: strip renders NEEDS YOU · 2; clicking a jump-link activated workflow-approval (position 10 in the list) and scrolled its card into view; scrolling deep keeps the strip pinned with ages ticking
  • docs/features/sidebar.md updated — ordering rule, the status-blindness rationale, the strip, and the dead-code inventory that had listed the strip as unmounted

npm run verify could not complete locally: cargo check fails on a missing binaries/agent-browser-x86_64-unknown-linux-gnu sidecar in a fresh worktree. No Rust files are touched by this PR.

Review notes

Four test assertions changed. Three were straightforward old-order expectations. The fourth is worth a look: getByText("Waiting for your input") now finds two nodes because the strip mirrors the card's blocker text. I asserted exactly 2 rather than scoping the query, so the duplication is pinned as intended behaviour.

Follow-ups (not in this PR)

  • A keybind to settle the focused workspace — the real fix for "too many open" is finishing them, not sorting them
  • A snooze lifecycle ("not now, guaranteed to come back"), distinct from settle
  • The remaining dead tree components (sidebar-project-group.tsx, sidebar-live-section.tsx, sidebar-live-grouping.ts) are still unmounted and could be removed

…-you strip

New workspaces were appended to the bottom of the sidebar, so anyone holding
a long list of still-open workspaces had to scroll to the bottom to reach the
one they just created.

Ordering
- Add `orderActiveWorkspaces`, which reverses `AppStateSnapshot.workspaces`.
  Every backend creation path ends in a `Vec::push` and close uses
  `Vec::remove`, so that array is creation order; `WorkspaceSnapshot` carries
  no `created_at`, making the reversal both the cheapest and the only lossless
  way to get newest-first. No backend change, no migration.
- The expanded inbox and the collapsed rail share the helper, so the two
  surfaces can no longer disagree about order.
- Order stays deliberately status-blind: a card holds its position from
  creation until it settles, so the list only moves at lifecycle transitions
  and never shifts under the pointer because an agent started or finished.
  Sorting by agent activity would churn the list constantly and break both
  spatial memory and the digit jump shortcuts.

Needs-you strip
- Mount `SidebarNeedsYouStrip`, which was fully built and unit-tested but had
  never been rendered. It now lives in the sidebar's sticky header block, so
  blocked work stays reachable however far the list has scrolled.
- Entries are jump-links: they activate the blocked workspace and scroll its
  card into view without moving it. Surfacing attention by duplication rather
  than by reordering is what lets the list below stay status-blind.
- Scope it by the active repo filter so a filtered sidebar never points at a
  workspace the list is hiding; match the flat inbox's `data-inbox-card` in
  addition to the legacy `data-ws-id`; cap at four rows with a `+N more below`
  line so a pinned strip can never swallow the sidebar.

Also keep the active workspace rendered when it settled long ago and sits past
the settled paging window, and exclude that escaped row from the hidden count.
@Zeus-Deus

Copy link
Copy Markdown
Owner Author

Review — Opus 5 (high effort)

The implementation is correct. Two things I specifically went looking for and found clean:

  • The digit shortcuts follow the new order. setJumpTargets is fed from activeCards, which is now the reordered list, so getJumpTarget(n) and the visible order can't disagree. (This was my main worry — a shortcut indexing appState.workspaces directly would have inverted against the display.)
  • The +N hidden accounting with the escaped active row is right, and there's no duplicate-row bug: once settledVisibleCount grows past the active row, settledHead.some(...) is true and activeBelowFold goes undefined, so it can never render twice.

The rationale and the change pull in opposite directions

The PR's central argument is positional stability — "a card holds its position from open until settled, so the screen only moves at lifecycle transitions", and that this is what makes Alt+1..9 muscle memory work.

But newest-first is the ordering that maximises churn for existing rows. Appending put every new workspace at the bottom and left all N existing cards exactly where they were, with their digit bindings intact. Reversing means every workspace you create shifts all N existing cards down by one and re-maps every single Alt+digit. The list still only moves at lifecycle transitions — but "create" is the most frequent lifecycle transition there is, and it now moves everything instead of nothing.

That may still be the right trade (reaching the thing you just made matters more than a stable Alt+7), but the PR presents stability as the justification for a change that reduces it. Worth saying out loud in the doc, because "spatial memory" is doing a lot of load-bearing work in the current text. Same property applies to #206.

The strip can point at a settled workspace

entries walks every group.workspaces with no settled/snoozed filter, so a settled workspace sitting on permission gets a jump-link while its card is on the settled shelf — possibly past the fold. The jump does work, because activateWorkspace makes it the active workspace and your new activeBelowFold escape then forces the card to render inside the 30-frame retry budget. That's a nice accidental interaction between the two halves of this PR, and it deserves a test so a future change to either half doesn't quietly break it.

Overlap

#206 implements the same newest-first ordering independently, via compareNewestFirst over a captured storedIndex rather than orderActiveWorkspaces. Two implementations of one behaviour in the same file, plus overlapping edits to sidebar-inbox.tsx, sidebar-rail-workspaces.tsx and both test files. These two need to be sequenced deliberately, not merged independently.

@Zeus-Deus

Copy link
Copy Markdown
Owner Author

Review by GPT 5.6 Sol xhigh found an ordering bug in the Needs You strip.

SidebarNeedsYouStrip says it is oldest-blocked-first, but entries (sidebar-needs-you-strip.tsx:116-132) is only appended in projectGroups / workspace tree order and is never sorted by statusSince[workspace_id].at. Creation order is not blocked-since order: an old workspace that became blocked five seconds ago will outrank a newer workspace that has been blocked for an hour. With the four-row cap, the actual longest-waiting blocker can be hidden behind +N more below.

Please sort the derived entries by their permission-status timestamp before slicing (with a deterministic fallback for a timestamp not seeded yet), and add a regression where creation order and blocked-since order disagree. The focused suites passed 38/38 tests, confirming the current coverage does not exercise that case.

Coordination note: this head has pairwise textual conflicts with #206 and #213.

The strip collected entries in project-tree order and never sorted them,
while claiming oldest-blocked-first. Tree order is not blocked order (the
active list runs newest-first and a block can land on any card), so with
the four-row cap the longest-waiting blocker could hide behind the
'+N more below' line. Sort entries by the density-store permission
timestamp before slicing; an unseeded timestamp ranks as newest and ties
keep stable tree order.

Also pin two adjacent behaviors with tests: a blocked settled workspace
keeps both a strip jump-link and a rendered card (via the permission
resurface net), and the active workspace's settled row always renders
past the settled fold via the below-fold escape.

Docs: describe the actual strip sort, and acknowledge the newest-first
trade-off honestly — each creation shifts existing cards and their jump
digits down one; the win is the newest workspace always sits on slot 1.
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.

1 participant