Conversation
…ebar Closes #903 and #904. Retires the chrome-bar workspace switcher and the hover-opened search popup; the canonical live-terminal navigator now lives in the left-edge activity dock with three progressive levels of detail (rail / cards / mega). Cards is the new default — surfaces real context first, ambient compression on opt-in. - Rail: narrow strip of repo-colored swatches; click to jump. - Cards: awaiting → full card with xterm tail + reply input; working → compact pill; idle → faded one-liner; parked → tiniest dim row. - Mega: search + repo facets + agent-state columns (Idle, Awaiting, Working, No agent), inlined from the retired chrome-bar panel. Opens via Mod+Shift+K with the search auto-focused. In maximized-tile mode (#904) the dock renders as an opaque flush-left sidebar and the maximized terminal reflows next to it (`left: dock-width` on `CanvasTile`). Non-active tiles stay mounted (visibility: hidden) so their xterm instances keep consuming the PTY stream and the dock's buffer previews remain populated regardless of which terminal is active. The active terminal is filtered out of the tiled section when maximized to avoid double-mounting alongside the maximized branch — `terminalRefs` is keyed by id and a second registration would race the first cleanup.
CanvasTile used to import both `dockMaximizedWidth` and the raw `dockMode` signal to compute its maximized-mode left inset. That leaked the dock's mode-to-pixel policy across a module boundary — any future change (user-resizable sidebar, density-responsive widths) would force edits in two modules. Encapsulate the mapping inside ActivityDock and expose only the derived inset accessor.
ActivityDock used to hold the mega search query, repo filter, focus-on- open impulse, and the workspace-switcher model alongside its rail/cards orchestration. That conflated two activities (level-transition sequence vs mega search state) in one component body. Mega is also where the openMegaRequest impulse logically lands — opening was already done by the orchestrator, but the focus half belongs to mega's own state machine. Move all four signals + the focus-impulse effect into MegaBody so the mega activity encapsulates its own volatility; the dock just orchestrates the level mount and the onSelect/onClose callbacks.
Hickey/Lowy Analysis
Hickey rationaleFour findings on the diff. In the cross-validation pass, Hickey accepted Lowy's Lowy rationaleThree findings on volatility boundaries. In the cross-validation pass, Lowy reinforced Hickey's |
|
| Step | Status | Duration | Verification |
|---|---|---|---|
| sync | ✓ | 0s | git fetch ok; forge=github |
| research | ✓ | 5m 19s | Mapped ActivityDock, WorkspaceSwitcher, TerminalCanvas, CanvasTile, terminalRefs, posture, xterm visibility lifecycle |
| branch | ✓ | 8s | Feature branch thin-sash |
| implement | ✓ | 14m 45s | Rewrote ActivityDock (tri-state mode, sidebar in maximized, idle/parked rows, mega via SearchPanel); retired chrome-bar switcher; tile reflow + visibility:hidden tiles for buffer previews; test migration |
| check | ✓ | 2m 2s | just check exit 0 |
| docs | ✓ | 2m 14s | README.md + packages/surface/README.md |
| fmt | ✓ | 13s | just fmt clean |
| commit | ✓ | 25s | Primary commit, 18 files / +848 / -794 |
| hickey+lowy | ✓ | 12m 17s | 4 + 3 first-pass + 1 cross-validation finding; 5 commits, no deferrals |
| police | ✓ | 8m 41s | 3 elegance fixes, separate commits |
| test | ✓ | 53s | 24 scenarios / 142 steps in 6.6s |
| create-pr | ✓ | 1m 16s | This PR + hickey/lowy comment |
| ci | ✓ | 53s | nix build .#default exit 0 at HEAD (CI scope limited per user) |
| evidence | — | 16s | Skipped (user-deferred) |
| Total | 49m 40s |
Slowest step: implement (14m 45s)
Optimization suggestions
implementdominated at 30% — the volume came from migrating e2e test selectors (workspace-switcher-pill→activity-dock-row) and rewritingworkspace-switcher.featureafter deleting the chrome-bar surface. Pre-cataloguing which step definitions and feature files reference a retired test-id (e.g.rg 'workspace-switcher-pill' packages/testsbefore starting) would have let the rewrite happen as one batch instead of interleaving with implementation.hickey+lowyandpolicecombined to 21 minutes for 8 + 3 commits across two files. Most of the time was in serial commit-and-push cycles. For PRs with many structural-review findings, batching per-skill (onegit pushafter all hickey commits, one after all police commits) without dropping the per-finding granularity ingit logwould cut push overhead — but per/do's explicit "push after each commit" rule, this would need a workflow flag rather than ad-hoc behavior.researchran in 5+ minutes because the change touched five tightly-coupled UI components. Pre-loadingExploreagent with a sharper question (e.g. "map every consumer ofdata-testid=\"workspace-switcher-pill\"along with the dock's collapse state machine") instead of a broad architecture sweep would have produced the same map in less context.
Workflow completed at 2026-05-14.
Previously the dock in maximized mode was absolutely positioned inside the canvas (`inset-y-0 left-0`) and the maximized tile was offset by `left: dockTileInset()px`. Two visible failures: (1) the maximized tile at z-40 covered the dock at z-30, so mega's right columns disappeared behind the terminal; (2) rail mode's button overflow leaked canvas-grid background through the gap between the 28px-wide aside and the 40px-ish effective content width. Make the dock a real flex sibling of the canvas in maximized posture (mirror of `RightPanelLayout`'s right panel). The canvas takes the remaining width via `flex-1`, so the tile no longer needs an inset and mega/cards/rail widths are honored without z-index gymnastics. Bump rail width from 28→40px so the 24px header buttons + 8px padding actually fit inside the aside. Drop `overflow-hidden` from the classList exception list — apply it uniformly. `dockTileInset` and `dockMaximizedWidth` exports retired (no more external readers); replaced by an internal `dockWidth(mode)` that drives only the aside's own `width` style.
Reported: the dock occasionally disappears completely until a full page refresh. Trigger: enough posture toggles between tiled and maximized. Root cause: the previous wiring stored the dock JSX in a `const dock = <ActivityDock … />` and referenced it from two sibling `<Show>` blocks. SolidJS JSX expressions are evaluated eagerly — `<ActivityDock />` is a DOM node creation call, not a deferred template. Putting the same evaluated node behind two Shows means one Show's cleanup tears down the reactive scope (effects disposed, listeners removed) before the other Show appends the now-corpse node into its branch. The element ends up in the DOM but renders empty, and the only way back to a live reactive scope is a full reload. Fix: mount the dock once, unconditionally, as a flex sibling of the canvas. The dock's outer aside already owns its posture-conditional positioning — `relative shrink-0` in maximized (real left-panel flex flow) vs `absolute z-30 top-20 left-4` in tiled (floats over the canvas). The parent flex container becomes `relative` so the absolute coordinates in tiled mode resolve to the same `top: 5rem, left: 1rem` they did when the dock lived inside the canvas div.
Adds a dock-toggle button to the chrome bar's control cluster (mirror of the inspector toggle, sits just left of it) plus a `Cmd+B` keyboard shortcut. Both drive the same rail/cards toggle the dock-header chevron does — and when mega is open, they close mega back to its prior level first. - `DockToggleIcon` mirrors `InspectorToggleIcon`: square with a divider on the left edge; left half fills when the dock is expanded (cards or mega) and goes empty in rail mode. - `Cmd+B` is the same shortcut VS Code uses for its primary sidebar. The inspector (right panel) keeps `Cmd+Alt+B`. - `toggleRailCards` and `dockExpanded` exported from ActivityDock so the chrome bar drives them without re-implementing the state machine.
Mobile mirror of the desktop dock split (#903): the chrome drawer (top pull-down) keeps the global controls — palette, settings, inspector — while a new left-edge swipe drawer hosts the terminal navigator. Native iOS/Android nav-drawer pattern. - `MobileDockDrawer`: recency-sorted terminal list (matches the desktop dock's "what just changed?" ordering, not the previous alphabetical-by-repo grouping), bucket-classified rows with repo color + agent indicator + unread dot. No reply input or buffer-tail preview by design — the v1 intent on mobile is "switch to that other terminal", not "respond inline". - `MobileChromeSheet`: stripped to identity + control cluster. The terminal list moved out entirely. - `MobileTileView`: two Corvu drawers as siblings (not nested — nesting put the chrome trigger in the dock-drawer context, breaking tap-to-open). Plain buttons drive each `open` signal; the chrome handle keeps its drag-down-to-open behavior. - Left-edge handle: thin colored strip (`bg-fg-3/30`) pinned to the viewport's left middle, just enough affordance for "open the nav" without competing with iOS Safari's back-swipe. E2e: 8 scenarios across `mobile-drawer.feature` (chrome) and the new `mobile-dock-drawer.feature`, all passing.
Mirrors the desktop dock's awaiting-card / working-pill / quiet-row hierarchy on the mobile drawer. Awaiting and working rows now get: - Larger headline type (0.95rem vs 0.8rem) - More vertical padding (py-3 vs py-2) - An agent-indicator + time-ago meta line - A PR line (#N + title) when one is resolved Idle, parked, and no-agent rows stay as compact one-liners — they're the "navigate to it later" bucket, not the "needs you now" bucket, so they don't earn the extra height. The cap (parked = lastActivityAt > 4h ago) routes long-stale rows to the quiet variant regardless of prior agent state.
"Activity" was a vestige of #897 when the surface only carried awaiting/working agents. With idle and parked terminals now first- class citizens, the name overstates the scope. The mobile drawer already dropped "activity" (MobileDockDrawer, mobile-dock-handle); the desktop surface follows suit. - File: canvas/ActivityDock.tsx → canvas/Dock.tsx - Component: ActivityDock → Dock - localStorage key: kolu-activity-dock-mode → kolu-dock-mode (forces a reset for anyone with `rail` persisted, naturally delivering the expanded-by-default experience) - Test IDs: activity-dock-* → dock-* - Feature/step files: activity-dock.feature → dock.feature, activity_dock_steps.ts → dock_steps.ts - README + package READMEs updated `useActivityAlerts` keeps its name — "activity" there refers to the alert system (Badging API, OS notifications), not the dock.
…rigger Two refinements: 1. **Foreground process surfaced for plain shells**. Non-agent rows (idle / parked / no-agent buckets) used to read as bare `repo · branch` — a `~ ~` home-dir shell looked indistinguishable from any other. Now they carry the foreground process title (e.g. `pu connect srid1`, `nix build`) as a second line, pulled from `meta.foreground.title || meta.foreground.name`. Mirror on the mobile dock too: non-live rows (idle/parked/none) get the same process line, while live rows (awaiting/working) keep the existing agent-indicator + PR line treatment. 2. **Mega-search button switched from `⏵` glyph to `SearchIcon`**. The dock header had two chevron-shaped icons sitting next to each other — `⏵` for "open mega search" and a real `ChevronDownIcon` (rotated) for "collapse to rail". Both read as right-pointing arrows, ambiguous at a glance. The mega trigger *is* a search affordance, so a magnifier icon distinguishes intent visually: magnifier = search, chevron = collapse.
# Conflicts: # packages/client/src/canvas/TerminalCanvas.tsx
…itcher/ The `canvas/workspace-switcher/` folder was a vestige of the chrome-bar surface that retired with #903. After the dock refactor and the mobile-dock-drawer addition, its content had drifted into two unrelated responsibilities (domain model + dock mega renderer), and the surrounding dock files were scattered across the source tree (`canvas/Dock.tsx`, `src/MobileDockDrawer.tsx`, dock mega in `workspace-switcher/`). Two structural moves: 1. **Co-locate dock UI under `canvas/dock/`.** Holds: - `Dock.tsx` (was `canvas/Dock.tsx`) - `MobileDockDrawer.tsx` (was at `src/` root) - `DockMega.tsx` (was `SearchPanel.tsx`, the dock's mega renderer) - `dockRowChrome.ts` (was `chrome.ts` — display-string helpers; the old name collided semantically with the unrelated `ChromeBar` component) 2. **Hoist the domain model to `canvas/dockModel.ts`.** Terminal classification (`agentBucket`, `entryBucket`, `buildDockModel`), recency sort, and the search/facet/column types are consumed by `Dock`, `MobileDockDrawer`, and `CanvasMinimap` — none of them "owns" the model, so it sits as their peer at the canvas root. Type renames to drop the stale "WorkspaceSwitcher" prefix: - `WorkspaceSwitcherEntry` → `DockEntry` - `WorkspaceSwitcherSourceEntry` → `DockSourceEntry` - `WorkspaceSwitcherModel` → `DockModel` - `WorkspaceSwitcherColumn` → `DockColumn` - `WorkspaceSwitcherIdleSubBucket` → `IdleSubBucket` - `WorkspaceRepoFacet` → `RepoFacet` - `WorkspaceAgentBucket` → `AgentBucketKind` (avoids collision with `agentBucket()` and Dock.tsx's local `DockBucket` which adds "parked") - `WORKSPACE_AGENT_BUCKETS` → `AGENT_BUCKETS` - `buildWorkspaceSwitcherModel` → `buildDockModel` - `sortBySwitcherOrder` → `sortDockEntriesByRecency` Dead code removed in the same pass: - `prLine` (string-form PR helper; only `prSummary` is in use) - `WorkspaceSwitcherRepoGroup`, `WorkspaceSwitcherCompactItem` types - `compactGroups` field on the model - `compactGroupsFor` helper + `IDLE_PILLS_PER_REPO` / `COMPACT_VISIBLE_PER_REPO` caps. The "compact pill strip" they served retired with the chrome-bar switcher; mobile no longer consumes the field either. - 6 tests in `dockModel.test.ts` that exercised the removed compact-groups behavior - `activeId` plumbing through `Dock` / `MegaBody` (was only used to feed compact-groups' active-hoist) After the move, `canvas/workspace-switcher/` is gone entirely. E2E unchanged (test-IDs still use `workspace-switcher-*` for the dock's mega panel — those are a separate cleanup tracked for follow-up). 32/32 scenarios pass.
…ound row dock.feature had 4 scenarios covering rail/cards defaults and the Claude mocking path. The new bug-fix surface from this PR sat without e2e coverage. Add scenarios for: - `Cmd+B` keyboard shortcut → toggles rail ↔ cards - Chrome-bar `[data-testid="dock-toggle"]` button → toggles rail ↔ cards - Maximized-tile mode → dock stays visible and carries `data-maximized=""` (asserts the real left-panel sidebar render path from #904) - Foreground process line on quiet rows → `sleep 5` populates the new `dock-quiet-foreground` line on a no-agent shell Four new step definitions in dock_steps.ts; total dock coverage is now 8 scenarios / 37 steps. Full sweep (dock + workspace-switcher + activity-alerts + mobile-drawer + mobile-dock-drawer): 36/36 passing.
Per the #903 open question (which order should the positional shortcuts target?), retarget `Cmd+1..9` from `store.terminalIds()` (insertion order) to `dockOrderedIds` (the same recency-sorted list the dock and mobile drawer render from). The shortcut now activates whatever the user visibly sees at row N, instead of a stable-but- invisible "first terminal I opened". Because the dock's recency ordering shifts as agents transition, the visible mapping needs an on-demand affordance — held-Alt paints a small `Cmd+N` hint badge on the first nine rows. Module-scope `altHeld` signal driven by window `keydown`/`keyup` + a `blur` / `visibilitychange` reset (the latter catches tab-aways where the keyup never reaches the page and the badge would otherwise stick). Each `DockRow` consumes its `index` (zero-based) from the `For` iterator and renders the hint when `altHeld() && index < 9`. E2E: three new scenarios on `dock.feature` — Cmd+1 returning to the recency-leading row, hints visible while Alt is held, hints clear on release. 10 dock scenarios / 49 steps, full sweep 38 scenarios / 216 steps, all passing.
…match Hickey #1 (= Lowy #2): `App.tsx`'s `orderedIds` and `Dock.tsx`'s `liveIds` were two derivations of the same concept with different secondary-sort rules. The Alt-held Cmd+N hint painted onto dock rows indexed into the dock's bucket-prioritized order, but the actual `Cmd+N` action read App.tsx's pure-recency order — so the badge could lie about which terminal a shortcut would target as soon as parked terminals existed. Extract `rankDockRows` into a new `canvas/dock/dockRowRanking.ts` sibling (not `dockModel.ts` — that file owns the mega-level four-bucket scheme that collapses parked into idle, while rows use five buckets with `parked` distinct; co-locating would invite label-collision bugs). Both `Dock.tsx`'s `ranked()` and `App.tsx`'s `orderedIds` now read from `rankDockRows`, so the Cmd+N target and the row that paints the hint can never diverge.
Lowy #1: the row classification + ranking loop (`parked` check → `agentBucket` → `idle` upgrade → recency-sorted with bucket-priority tiebreak) was byte-equivalent in `Dock.tsx` and `MobileDockDrawer.tsx`. Now that commit 0e98f0f extracted `rankDockRows` into a shared sibling module, MobileDockDrawer can drop its local `BUCKET_PRIORITY` table, `MobileDockBucket` alias, and the duplicate loop — any change to bucket vocabulary, parked threshold semantics, or tiebreak rules lands in one place.
Lowy #3 + Hickey cross-validation B: the `pr.kind === "ok" ? pr.value : null` extraction lived in three places (Dock.tsx's `PrLine`, MobileDockDrawer.tsx's `PrLine`, and dockRowChrome.ts's `prSummary`). Add `resolvedPr(pr)` to `dockRowChrome.ts` and have `prSummary` call it internally, so a future kind added to the PR carrier union (e.g. "loading" or "error") forces one edit, not three. The structured `PrSummary` shape layers on top of the resolved-PR predicate rather than re-implementing it.
`classifyDockRow` and `DOCK_ROW_BUCKET_PRIORITY` were exported from `dockRowRanking.ts` but imported nowhere outside that file. Module- private now — no speculative public surface.
The Alt-held hint chips were a modifier-mismatch: the chord they preview is `Cmd+1..9` (or `Ctrl+1..9` on Linux/Windows), but the hint surfaced under a *different* key. Holding the shortcut's own modifier to reveal the mapping is the standard discoverability pattern (System Settings, Linear, etc.) — same key for "what would this do" and "do it". Swap `altHeld` for `modHeld`, route the keydown/keyup listeners through `isPlatformModifier` from `input/keyboard.ts`, and update the matching e2e scenario from `press/release Alt` to `press/release Mod` so it exercises the actual chord modifier on each platform.
The dock had `data-active` on each row but no visible treatment — the active terminal looked indistinguishable from its neighbors. Add a 2px accent strip pinned to the row's left edge, visible in both rail and cards modes. The strip sits outside per-body theming so it reads as "selected" against any tile background. E2E: covers the indicator's presence after terminal creation.
Hickey/Lowy AnalysisPolish-pass review on the cumulative branch diff (
Hickey rationaleThe The fix extracts Lowy rationale
Cross-validation C: Hickey's instinct to centralize at Dock.tsx was structurally inverted — UI rendering volatility (which row variants exist, what they look like) is independent from row-ordering policy (recency + bucket priority). Placing the source in Dock.tsx braids them. The right home is one layer below — a sibling module dedicated to ranking that both UI surfaces depend on. Follow-on UX fixes during polishTwo UX issues surfaced during review and landed as follow-on commits:
|
`Ctrl+B` was released back to the terminal in #821, then reclaimed as the dock toggle shortcut when the dock shipped as canonical in #903. The unit test still asserted "does not capture Ctrl+B" — flip to assert it matches, since `toggleDock` carries `{key:"b", mod:true}` and `mod` resolves to `ctrlKey` on Linux/Windows.
After #904 the canvas tiles split across two render branches: the tiled section keeps non-active tiles mounted at \`visibility: hidden\` so their PTY streams keep filling buffers, while the maximized tile renders in its own \`Show\` branch. \`locator(TILE_SELECTOR).nth(0)\` picks the first DOM-order tile — a hidden tiled-section tile — so \`waitFor({state: "visible"})\` times out even though the maximized tile is visible elsewhere. Match on \`[data-maximized="true"]\` directly. The \`index\` parameter was already informational only (the existing \`waitForFunction\` checked "some tile has data-maximized=true" without verifying which).
The "Restored terminals appear in the predictable alphabetical pill order" scenario tested an explicit property of the chrome-bar workspace switcher (#830): pills were alphabetized by repo group. The dock that replaced that surface (#903) uses recency-sorted order with a bucket-priority tiebreak — alphabetical placement is no longer a property the system preserves. Drop the scenario and its companion step + helpers; the remaining session-restore scenarios still exercise the meaningful behavior (count + theme).
Switching the active terminal triggers an xterm fit/resize on the
newly-focused tile. During that window two things can happen on the
peek-tick that paints the awaiting card's tail preview:
1. The terminal's refs are transiently absent (component still
reconciling), so `getTerminalRefs(id)` returns undefined.
2. `buffer.active.length` is briefly small enough that every row
reads as alt-screen chrome, so `tailBuffer` returns `[]`.
The old effect overwrote `tail` to `[]` in both cases. The card's
`<Show when={tail().length > 0}>` then unmounted the tail div, and
the user saw a flash to 0 lines before the next tick repainted N
lines. Treat both shapes as transient: hold the cached tail and let
the next tick replace it from a populated buffer.
|
| Step | Status | Duration | Verification |
|---|---|---|---|
| hickey+lowy | ✓ | 16m 12s | 4 findings, 3 commits (0e98f0f9, 14e1226b, 8874440b). Cross-validators converged on a new sibling-module destination. |
| police | ✓ | 9m 3s | 1 violation (dead exports in dockRowRanking), fixed in 5a125c80. Re-run on subsequent feature commits clean. |
| test | ✓ | 45s | dock.feature + mobile-dock-drawer.feature: 14/14 scenarios. |
| create-pr | ✓ | 1m 22s | PR body refreshed (modifier-held hints, active indicator, dockRowRanking.ts); Hickey/Lowy analysis posted. |
| ci | ✓ | 33m 53s | 14/14 contexts green on a4c1a90c. Linux e2e failed once on the prior SHA (Terminal survives browser refresh — flaky, logged to #320), passed on targeted retry with no source change. |
| evidence | skipped | — | User directive — "No need for evidence". |
| Total | 64m 15s |
Optimization suggestions
- CI dominates wall-clock (53%). This run hit the flaky
Terminal survives browser refreshscenario one2e@x86_64-linux, which forced a single-step retry and then a full re-run against HEAD for SHA coverage. Tracking the flake in Flaky tests log #320 is the right place; landing a fix there shaves ~7 min off the average/doround. (The full re-run was also needed because an earlier background CI process was killed mid-flight — worth investigating the harness reaping behavior separately.) hickey+lowywas the second-largest bucket (25%). Two-pass review (parallel reviewers + cross-validation) on a large cumulative diff. Cost was warranted — the cross-validation surfaced the third-destination compromise (dockRowRanking.tssibling module) that neither reviewer reached alone. Worth keeping the two-pass shape on diffs of similar scope.policeran 9 min for one finding. All three passes (rules / fact-check / elegance) ran on the cumulative branch diff; only the rules pass surfaced anything. On a polish-pass entry point where prior commits already cleared police, scoping subsequent invocations to just the new commits since the last green police pass would compress this substantially.- In-stream UX feedback landed two follow-on commits (
4459a2ba,98e21311,a4c1a90c). Captured as separate commits so the PR history reads as a sequence; this is the right shape for review but extends the loop. Consider pre-flighting visible-affordance changes (modifier semantics, active-state styling) before invoking/do --follow polish.
Workflow completed at 2026-05-15T16:36:46Z.
The canvas first-mount centre-on-active effect guarded the bbox fallback only against the COLD-LOAD race (`savedSessionSub.pending()`). W1.R6 moved restore host-side, so a restore-from-card arrives with `session.get` already yielded (`pending()` false) while the restored tiles land on the `terminals` collection a tick before `useSessionRestore` assigns the active via `setActiveSilently`. In that window the bbox fallback ran, panned off-origin, and `isDefaultViewport()` then blocked the real centre-on-active — leaving a restored multi-tile session centred on the bbox (0,0) instead of its active tile (session-restore.feature:46 'preserves active terminal and centers viewport', pre-existing since #909). Widen the guard: wait for the active to hydrate whenever there's no active yet AND one is expected — session still pending OR the saved session names an `activeTerminalId`. Hydration always assigns some active when top-level tiles exist, so it can't wait forever.
The Dock is now the canonical live-terminal navigator across both desktop and mobile. Closes #903 and #904. On desktop it sits on the left edge as a three-level surface (rail / cards / mega); on mobile the same model lives behind a left-edge swipe drawer. In maximized-tile mode it renders as a real flex-sibling sidebar — the maximized terminal flows next to it instead of underneath. Cards is the default level, so context shows up immediately on first load.
Three levels of detail (desktop)
pu connect srid1)⌘B toggles rail ↔ cards from anywhere; the chrome bar carries a matching dock-toggle button (mirror of the inspector toggle).
Keyboard navigation
Cmd+1..9(Ctrl+1..9 on Linux/Windows) targets dock row order (recency-sorted, bucket-priority tiebreak) instead of insertion order — the shortcut activates whatever the user visibly sees at row N. Because that mapping shifts as agents transition, holding the same modifier paints a small numeric badge on the first nine rows so the binding is legible on demand. Same modifier for "what would this do" and "do it" — release to clear the hints.Active-terminal indicator
A 2px accent strip pinned to each dock row's left edge marks the active terminal — visible in both rail and cards modes, outside per-body theming so it reads as "selected" against any tile background.
Mobile
MobileChromeSheet(top pull-down) keeps global controls — palette, settings, inspector. A newMobileDockDrawerlives behind a thin left-edge handle and renders the same recency-sorted terminal list as the desktop dock. Awaiting/working rows get richer treatment (larger height, agent indicator, time-ago, PR line); idle/parked rows stay compact one-liners; no-agent rows surface the foreground process. No reply input or buffer-tail on mobile by design — the v1 intent is "switch to that other terminal", not "respond inline".Maximized-mode sidebar (#904)
The dock renders as a flush left-panel flex sibling of the canvas in maximized posture (mirror of
RightPanelLayout's right panel). The maximized terminal naturally takes the remaining width viaflex-1. Non-active xterm instances stay mounted (visibility: hidden) inside the tiled section so their PTY streams keep filling buffers — the dock's preview rows stay populated regardless of which terminal you're focused on.File layout & naming
canvas/dock/now holdsDock.tsx,MobileDockDrawer.tsx,DockMega.tsx,dockRowChrome.ts, anddockRowRanking.ts. The mega search panel was previouslySearchPanel.tsx; the row display helpers were previouslychrome.ts(confusingly close to the unrelatedChromeBarcomponent).dockRowRanking.tsis the single source of "what terminals does the dock show, in what order" — read byDock.tsx,MobileDockDrawer.tsx, and theCmd+1..9shortcut path, so the visual row order and the chord's activation target can never disagree.canvas/dockModel.ts(hoisted fromcanvas/workspace-switcher/model.ts) — terminal classification + search model for the mega level (4-bucket scheme). Distinct fromdockRowRanking.ts's 5-bucket row scheme; co-locating the two would invite label-collision bugs because both share four labels but disagree on the fifth (parked).canvas/workspace-switcher/is gone entirely — the chrome-bar surface it served retired in Consolidate workspace switcher + Activity dock into a single 3-level surface #903.WorkspaceSwitcher*prefix:DockEntry,DockSourceEntry,DockModel,DockColumn,IdleSubBucket,RepoFacet,AgentBucketKind,AGENT_BUCKETS,buildDockModel,sortDockEntriesByRecency.What's gone
WorkspaceSwitcher.tsx,Collapsed.tsx)compactGroupsfield +compactGroupsForhelper +IDLE_PILLS_PER_REPO/COMPACT_VISIBLE_PER_REPOcaps — the compact pill strip retired with the chrome-bar switcherprLinestring helper (onlyprSummaryis in use)workspaceSwitcherSwitchTip(unused export).chrome-bar-surface:has([data-open])frosted-surface CSS ruleactiveIdplumbing throughDock/MegaBody(only fed the retired compact-groups active-hoist)Test coverage
dock.feature(11 scenarios) covers rail/cards defaults, Claude-mock surfacing, mode toggles via header chevron /⌘Bshortcut / chrome-bar button, the maximized-tile sidebar render path, the foreground-process line on no-agent rows,Cmd+1targeting the recency-leading row, the modifier-held shortcut hints, and the new active-terminal indicator. Mobile drawer scenarios live inmobile-dock-drawer.feature.workspace-switcher.featurewas rewired to operate on the dock's mega level. All scenarios pass locally.Try it locally
Generated by
/doon Claude Code (modelclaude-opus-4-7).