Skip to content

feat(web): show First Mate's fleet as a tree in the sidebar - #162

Merged
ImZoomBoy merged 4 commits into
mainfrom
feat/fleet-sidebar-tree
Sep 25, 2026
Merged

ImZoomBoy merged 4 commits into
mainfrom
feat/fleet-sidebar-tree

Conversation

@ImZoomBoy

@ImZoomBoy ImZoomBoy commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Fleet threads were scattered among ordinary threads. A second mate moved whenever its activity changed. Nothing tied a worker to its second mate. The roles showed only as small grey labels, so First Mate, a second mate and a worker looked alike at a glance.

The sidebar now shows the fleet as a tree above the other threads.

  • First Mate always sits at the top, pinned or not, above a divider. The pin marks the main First Mate when there are several: the pinned one leads, or else the newest live one, and the others list under it. First Mate rows show Pin on hover.
  • With no First Mate live, a Start First Mate row takes the top spot. It replaces the old New First Mate button and does the same thing.
  • Second mates follow in repository-name order. Their place never changes with activity.
  • Each worker is indented under the second mate that shares its fleetRepo, settled and snoozed ones included. Those stay full rows, dimmed, with their un-settle or wake control. An arrow beside a second mate folds its workers away.
  • Ordinary threads list exactly as before.

Each fleet row keeps today's row shape and font. Its model line names the role in its own colour, then the model and the reasoning level. Each second mate takes its colour from its project icon as the sidebar shows it: a monogram badge's own colour, or an image logo's main colour, read once from the image, with only its lightness set for light and dark. Only an icon with no usable colour, such as an emoji or a grey logo, falls back to a stable colour from the repository name. First Mate keeps its pink. Workers take their second mate's colour, faded. Fleet rows leave the branch to the tooltip.

No one-way doors

A second mate that was settled, snoozed or pinned before this change keeps the control that undoes it: un-settle, wake, or unpin. The row does not offer a new settle, snooze or pin. The row menu no longer offers settle or snooze for First Mate, because the top slot has no way back from either.

How a worker finds its second mate

By fleetRepo, which the fleet already records. First Mate sends the repository key as fleetRepo on thread.create for both a second mate and its workers. No contract change was needed.

Screenshots

Before and after screenshots in light and dark were reviewed outside GitHub and are not attached here.

Options considered

The branch prototype/fleet-sidebar records every layout tried before this one was picked. It is not for merging.

Surfaces

Web and desktop. The mobile sidebar is unchanged.

The model and harness line that AGENTS.md asks for is left out, because this repository's owner does not allow an agent name in pull request text.

Fleet threads were scattered among ordinary threads. A second mate moved
whenever its activity changed. Nothing tied a worker to its second mate. The
roles showed only as small grey labels.

The sidebar now shows the fleet as a tree above the other threads.

- A pinned First Mate sits at the top, above a divider. Unpinned, it lists
  among the other threads. The pin is today's pin.
- Second mates follow in repository-name order. Their place never changes
  with activity, and they cannot be settled, snoozed or pinned out of it.
- Each worker is indented under the second mate that shares its fleetRepo.
  First Mate already sends that key for both, so no new field is needed.
- An arrow beside a second mate folds its workers away.
- A worker that is settled or snoozed moves to the usual shelf.

Each fleet row keeps today's row shape. Its model line reads First Mate,
Second mate or Worker in its own colour, then the model and the reasoning
level. A ship, sailboat or pickaxe sits beside the project icon in the same
colour. First Mate is pink. Each second mate gets one of four cool colours
from its repository name, so the colour stays the same across reloads.
Workers take their second mate's colour, faded. Fleet rows leave the branch
to the tooltip.
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Sep 24, 2026
@ImZoomBoy

Copy link
Copy Markdown
Owner Author

Code review: Standards and Spec

Two separate reviewers read this pull request. Their reports follow, lightly cleaned. Each finding has a confidence and a severity.

Standards

I found 4 breaks of the written standards and 9 judgement calls in PR 162's one commit (558aa48784). The other two commits on this branch against main are not in the PR, so I did not review them.

Hard violations

  1. The user doc describes icons and layout. In docs/user/thread-sidebar.md, the new text lists "ship, sailboat or pickaxe", "pink", "in its own colour" and "the arrow beside a second mate". AGENTS.md "Documentation" says descriptions of icons, layouts and UI states are not useful in user docs. High confidence, low severity.
  2. The PR body has no model and harness line at the end. AGENTS.md "Pull requests" requires one. High confidence, low severity.
  3. The screenshots are committed to the repo. They live in pr-evidence/ on the prototype/fleet-sidebar branch of the same repo. AGENTS.md says to upload PR evidence to GitHub and never commit PR-only assets. Medium confidence, low severity.
  4. Second mates get a way in with no way out. A second mate that is already settled, snoozed or pinned joins the tree with settlementSupported, snoozeSupported and pinningSupported all off. Nothing in the UI can clear that state again. AGENTS.md "Reverse states" calls a one-way door a bug. Medium confidence, medium severity.

Judgement calls (smells)

  1. Duplicated Code. FIRST_MATE_TONE in fleetSidebar.logic.ts repeats the pink colour classes written directly into FirstMateIcon. High confidence, low severity.
  2. Repeated Switches. Three places branch on the same fleetRole value: FLEET_ROLE_ICONS, fleetRoleWord, and the existing fleetRoleLabel in packages/client-runtime/src/fleetThreads.ts. One role map would cover all three. Medium confidence, low severity.
  3. Mysterious Name / Divergent Change. FleetRoleIcon sits in FirstMateIcon.tsx. The file now serves every fleet role. Medium confidence, low severity.
  4. Too many loose parameters (Data Clumps). renderThreadRowInner(thread, "pinned", undefined, true, FIRST_MATE_PLACEMENT) now takes 5 positional parameters. firstMateSlot repeats what the placement already says. Medium confidence, low severity.
  5. One prop switches the whole row (Shotgun Surgery). The fleet prop turns off separate pieces of SidebarThreadRow: isFirstMate, roleLabel, readOnlyModelBadges and the branch line each get their own fleet === undefined check. Medium confidence, low severity.
  6. Logic in the UI. The fleetRows memo builds rows and picks colours inside Sidebar.tsx. AGENTS.md "Taste" says the UI stays dumb, and this code could move into fleetSidebar.logic.ts with tests. Medium confidence, low severity.
  7. Primitive Obsession. tone: string holds raw class names. fleetRepo ?? "" uses an empty string to mean "no repository". Low confidence, low severity.
  8. Tests that mirror the code. The fleetRoleWord test only restates the switch. The "pinned First Mate" test in fleetSidebar.logic.test.ts repeats what Sidebar.logic.test.ts already covers. AGENTS.md "Verifying" says not to write tests that mirror the implementation. Medium confidence, low severity.
  9. Hit every surface. The mobile sidebar is unchanged, and the PR body says so. There is no stated decision on whether mobile should get the tree. Low confidence, low severity.

What checks out

  • The commit title follows the conventional format.
  • The fork-features.json entry has a keep line, and its test path exists.
  • There is no continuously running animation; the arrow's transition-transform runs only when it is toggled.

Decide whether finding 4 (second mates stuck settled, snoozed or pinned) blocks the merge, since it is the only medium-severity finding.

Spec

PR 162 leaves the "New First Mate" button in place with its old label. It also moves First Mate off the top of the sidebar unless the thread is pinned.

Note: the local main is behind. git diff main...HEAD also picks up #160 and the release commit. I reviewed only commit 558aa48, which matches the pull request's file list.

(a) Missing or partial

  1. "New First Mate" button not replaced. High confidence, high severity. Spec: "the 'New First Mate' button makes no sense to the captain and needs a replacement that does." The button has the same label, test id and handler (Sidebar.tsx, first-mate-start). The only change is that it hides while an unpinned First Mate is live. The user docs still point to New First Mate.
  2. Parked workers leave their second mate. High confidence, medium severity. Spec: "have workers always go underneath their respective secondmate." A settled or snoozed worker goes to the ordinary shelves (buildFleetTree's parked).
  3. Pinning First Mate is not made clear. Medium confidence, medium severity. Spec: "The captain needs a clear way to pin a First Mate thread." The only way to pin is today's pin in the row menu. Nothing new points the captain to it.
  4. Colours run out. Low confidence, low severity. Spec: "each main thread ... has its own text colour." The palette has four colours, so a fifth second mate reuses one.

(b) Not asked for

  1. First Mate reaches the top only when pinned. High confidence, high severity. Spec: "First Mate row at the top on its own." Before this change, First Mate always had the top slot. Now a never-pinned First Mate sorts among ordinary threads by activity. The pull request body flags this, but the spec did not ask for it.
  2. An unpinned First Mate can be settled and snoozed by hand. Medium confidence, low severity. It renders with firstMateSlot=false, so those actions show.
  3. Folded state resets on reload (useState). Low confidence, low severity. The spec did not mention persistence either way.

(c) Looks implemented but may be wrong

  1. A pinned second mate cannot be unpinned. Medium confidence, low severity. pinningSupported is false for second mates, but isPinned still reads pinnedAt. A second mate pinned before this change shows the pin mark with no way to remove it.
  2. Rows are still today's ordinary thread row. High confidence, no defect. Rows reuse SidebarThreadRow. Nesting is pl-4 plus the fold arrow. The role word replaces the branch, and the branch is still in the tooltip. This matches "Every row is today's ordinary thread row; nesting is indent plus a fold arrow."
  3. The link is tied to the repository, not to the launching second mate. Low confidence, low severity. Spec: "under the second mate that launched it." Workers group by fleetRepo. That is correct only while each repository has one second mate. With duplicates, the older second mate drops to the ordinary list and its workers move to the newer one.
  4. The test for pinning covers only the partition. Medium confidence, low severity. Spec: "pinning First Mate keeps it at the top." The test checks partitionFirstMateThreads, not where the row renders.

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ℹ️ The exact PR base did not have a successful artifact. Baseline uses the latest successful main measurement shown below.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 13.5 KiB 13.5 KiB −7 B (−0.1%) 15.1 KiB ✅
Codex Thread snapshot wire 7.1 KiB 7.1 KiB −2 B (−0.0%) 7.3 KiB ✅
Codex Live turn WebSocket wire 6.4 KiB 6.4 KiB −5 B (−0.1%) 7.8 KiB ✅
Codex Live turn WebSocket decoded 56.1 KiB 56.1 KiB 0 B (0.0%) 66.4 KiB ✅
Codex Live turn messages 7 7 0 (0.0%) 21 ✅
Claude Total thread wire 13.5 KiB 13.5 KiB −32 B (−0.2%) 15.1 KiB ✅
Claude Thread snapshot wire 7.1 KiB 7.1 KiB −1 B (−0.0%) 7.3 KiB ✅
Claude Live turn WebSocket wire 6.4 KiB 6.4 KiB −31 B (−0.5%) 7.8 KiB ✅
Claude Live turn WebSocket decoded 56.9 KiB 56.9 KiB 0 B (0.0%) 66.4 KiB ✅
Claude Live turn messages 7 7 0 (0.0%) 21 ✅

Baseline: ef6e2f7 · PR result: 05004f0 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 114.0 KiB
  • Claude decoded thread snapshot: 114.7 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

First Mate now sits at the top whether or not it is pinned. The pin marks
the main First Mate when an environment list has several; the others list
under it. With none live, a Start First Mate row replaces the old New
First Mate button. First Mate rows offer Pin on hover, and the row menu no
longer offers settle or snooze for First Mate, since the top slot has no
way back from either.

Settled and snoozed workers stay under their second mate as dimmed rows
with their un-settle or wake control. A second mate that was already
settled, snoozed or pinned keeps the control that undoes it. The second
mate palette grows to six colours, and the row layout moves into
buildFleetRows so the sidebar only renders it.
@github-actions github-actions Bot added size:XL and removed size:L labels Sep 24, 2026
@ImZoomBoy

Copy link
Copy Markdown
Owner Author

Fix round for the review above, in d97accbd0e.

What changed

  1. First Mate always holds the top, pinned or not, above the divider.
  2. The New First Mate button is gone. With no First Mate live, a Start First Mate row stands at the top and does what the button did.
  3. The pin marks the main First Mate. The pinned one leads, or else the newest live one. Others list under it as ordinary First Mate rows.
  4. Settled and snoozed workers stay under their second mate as dimmed rows.
  5. A second mate that was already settled, snoozed or pinned keeps the control that undoes it. I kept the control rather than clearing the state, so nothing changes on the server without a click.
  6. First Mate rows show Pin on hover.
  7. The user doc no longer describes icons, colours or layout, and points to Start First Mate. pr-evidence/ is removed from prototype/fleet-sidebar (c4c6ce8e72). The palette has six colours, and a test checks six second mates get six.

Also: the row menu no longer offers settle or snooze for First Mate, since the top slot has no way back from either. The Settings text for the First Mate folder now says Start First Mate. FleetRoleIcon moved to its own file, and the row layout moved into buildFleetRows in the logic module.

The model and harness line is left out of the PR body, because this repository's owner does not allow an agent name in pull request text.

Tests seen failing first

$ vp test run src/components/sidebar/fleetSidebar.logic.test.ts   (before the logic change)
TypeError: planFirstMateSlot is not a function
TypeError: buildFleetRows is not a function
 Test Files  1 failed (1)
      Tests  10 failed | 3 passed (13)

Checks

$ vp fmt --check <changed files>
All matched files use the correct format.
Finished in 430ms on 11 files using 24 threads.
$ vp lint <changed code files>
exit 0, 0 errors, 39 warnings, none on changed lines
$ vp run typecheck   (apps/web)
exit 0, 0 errors
$ vp test run src/components/sidebar/fleetSidebar.logic.test.ts src/components/Sidebar.logic.test.ts src/components/Sidebar.drag.test.ts src/components/Sidebar.motion.test.ts src/components/Sidebar.pointer.test.ts src/components/Sidebar.snooze.test.ts
 Test Files  6 passed (6)
      Tests  282 passed (282)

node scripts/check-fork-features.ts: 11 tests failed, all in scripts/build-desktop-artifact.test.ts, which stages Linux archives and fails on Windows. Every other fork feature test passed, including fleetSidebar.logic.test.ts.

Screenshots

Not attached yet. The screenshots are taken, but I found no way to upload them to GitHub without committing them or using a browser signed in to GitHub. They will be added here once uploaded.

A second mate's role word and role icon, and its workers' quieter ones,
now take the colour of the project icon the sidebar shows. A monogram
badge or coloured icon gives its own colour. An image logo gives its
main colour, read once from the image and cached, with only its
lightness set for light and dark. Only an icon with no usable colour,
such as an emoji or a grey logo, falls back to a stable colour from the
repository name. First Mate keeps its pink. The fixed six-colour palette
is gone.

A settled or snoozed fleet row now stays a full row, dimmed until
hovered, so it keeps its role line and its title is no shorter than any
other row's. Its Un-settle or Wake control sits where Settle would.
@ImZoomBoy

Copy link
Copy Markdown
Owner Author

Fleet colours now come from each project's icon, in 13c26c6a65.

What changed

  1. The fixed six-colour palette is gone. A second mate's role word and role icon take the colour of its project icon, as the sidebar shows it. Its workers take the same colour, quieter.
  2. A monogram badge (such as FE or LA) or a coloured icon gives its own colour, with the badge's own text classes.
  3. An image logo (such as the T3 logo) gives its main colour: the hue covering the most coloured area, read once from the image and cached per image. Only the lightness changes: OKLCH 0.52 in light, 0.80 in dark. Hue and chroma are the logo's own.
  4. Only an icon with no usable colour falls back to a stable colour from the repository name. That covers an emoji icon, a logo with under 10% coloured pixels, and an image the browser will not let us read. In the test data no project fell back: firstmate and lavish-axi use their badges, and t3code uses its logo (blue, hue 262.5).
  5. First Mate keeps its pink.
  6. A settled or snoozed fleet row stays a full row, dimmed until hovered. It keeps its role line, and its title cuts at the same width as other rows. Un-settle or Wake sits where Settle would.

Tests seen failing first

Run against a stub that always returned the fallback:

$ vp test run src/components/sidebar/fleetTone.logic.test.ts
     × takes a monogram badge's own colour 4ms
     × reads a logo's main colour and keeps its hue in light and dark 1ms
     × falls back to the repository's stable colour when the logo has no colour 1ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 3 ⎯⎯⎯⎯⎯⎯⎯
AssertionError: expected { kind: 'fallback', color: 'gray' } to deeply equal { kind: 'badge', color: 'lime' }
AssertionError: expected 'fallback' to be 'image' // Object.is equality
AssertionError: expected { kind: 'fallback', color: 'gray' } to match object { kind: 'fallback', color: 'green' }
 Test Files  1 failed (1)
      Tests  3 failed (3)

Checks

$ vp fmt --check <changed files>
All matched files use the correct format.
Finished in 189ms on 8 files using 24 threads.
$ vp lint <changed code files>
exit 0, 0 errors, 26 warnings, none on changed lines or new files
$ vp run typecheck   (apps/web)
exit 0, 0 errors
$ vp test run src/components/sidebar/fleetTone.logic.test.ts src/components/sidebar/fleetSidebar.logic.test.ts src/components/Sidebar.logic.test.ts src/components/Sidebar.drag.test.ts src/components/Sidebar.motion.test.ts src/components/Sidebar.pointer.test.ts src/components/Sidebar.snooze.test.ts
 Test Files  7 passed (7)
      Tests  284 passed (284)

node scripts/check-fork-features.ts: 11 tests failed, all in scripts/build-desktop-artifact.test.ts, which stages Linux archives and fails on Windows. The other 218 passed, including both fleet sidebar test files.

Screenshots

New light and dark screenshots were taken from a local dev build and reviewed outside GitHub. They are not attached here.

Knip flagged mainColorOfPixels as an unused export, which failed the Check job. Only fleetTone.logic.ts uses it.
@ImZoomBoy
ImZoomBoy merged commit 989e35a into main Sep 25, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant