Skip to content

Sessions show which harness they run, and the row has room to say so - #16

Merged
jbachorik merged 35 commits into
mainfrom
feat/harness_icon
Aug 9, 2026
Merged

Sessions show which harness they run, and the row has room to say so#16
jbachorik merged 35 commits into
mainfrom
feat/harness_icon

Conversation

@jbachorik

@jbachorik jbachorik commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Why

Drydock manages sessions across three agent CLIs, and a sidebar row never said which one it was running. Claude, Codex and Pi rows were typographically identical; the only way to find out was to open the session and read the agent sub-tab. With more than a handful of sessions the list also had no way to narrow itself — the text filter matched names, branches and paths, but not "what is still running" or "which of these are Codex".

Adding the mark exposed a second problem. At the default 320px sidebar the row had ~84px left for the session name, because 70px went to action buttons that reserved layout width while invisible and ~62px to a Resume pill restating what clicking the row already did. Names truncated to about nine characters — on a title the agent itself had authored via session_rename. The mark didn't cause that, but it made the row unusable enough to be worth fixing properly.

Options

Identity. A per-agent glyph, a letter chip (C/X/P), or the agent's name spelled out. The name is unambiguous but costs the horizontal room the name column needed; letters collide (Claude and Codex both start with C).

Filtering. Toggle chips, two dropdown menus, or typed tokens (status:running agent:codex) in the existing field. Menus hide the active filter until opened; tokens are composable but undiscoverable.

Filter scope. Hide non-matching rows including repositories with no match; keep repositories and hide only sessions; or dim non-matching rows in place.

Row width. Keep two lines and accept truncation; unmanage the hover buttons when hidden; or move them out of the layout entirely. Unmanaging reclaims the same 70px but reflows every row as the cursor crosses it.

What this does

A per-agent glyph Claude, Codex, π Pi, ? for an agent this build doesn't recognise — in the row's leading gutter and on the session's agent sub-tab, each with its own theme token. Shape carries the identity and colour reinforces it, so the row still reads in greyscale. The sub-tab takes the glyph but not the colour: it already spells the name out, and its accent already means "this terminal owns the keyboard".

Toggle chips under the text filter, for status (running / idle / error) and harness. Facets OR within an axis and AND across them. An empty axis means no constraint — and so does a fully selected one, otherwise "select all three agents" would be the single selection that hides a session whose agent is unknown. When nothing matches, the sidebar says so and offers a way back.

Single-line rows, name first. The branch tag is capped so it yields characters before the name does, and the hover actions float in an overlay layer that costs no layout width, so nothing shifts as the cursor moves down the list. Names went from about nine characters to complete; roughly a third more sessions fit on screen.

UNSUPPORTED_AGENT now classifies as an error, not idle, and the status→facet mapping moved into the domain so the chip you click and the dot you see cannot disagree.

Drawbacks

  • The relative-time line is gone. Ordering already carries it — live band first, needs-attention pinned to its front, then idle, each most-recent-first — and the exact time is in the row tooltip. Anyone who scanned that column loses a glance-level signal, and the fix if that turns out to matter is a right-aligned time in place of the branch tag, not the second line coming back.
  • A chip filter hides worktree rows. It filters sessions, and an unopened worktree has none. Searching an exact worktree path and then clicking a chip makes the row you searched for disappear.
  • A repository with no matching session disappears entirely, taking its + and buttons with it. Recovery is to clear the filter; the filter is deliberately transient and resets on restart.
  • Hovering covers the row's trailing content. The floating buttons sit over the branch tag and any chip, behind a wash. That is the cost of not reserving their width permanently.
  • Existing sessions change appearance on first launch. Anything persisted with an unrecognised agent turns from an idle dot to a red one — intended, but it is a visible change to sessions nobody touched.
  • Colour is a weak channel here. Three new hues join a palette that already spends green, red, amber, blue and purple, and a Claude-heavy list is mostly one colour. The glyphs are what actually distinguish the rows.

Closing

The sidebar now answers two questions it couldn't before — what is this session running, and show me only the ones I care about — and the row has the width to say so without truncating the one field a human reads first.

Worth a look with a real cursor before merging: hover a session row and confirm the buttons fade in cleanly without the row's contents shifting, and that clicking the row's trailing edge still opens the session.

🤖 Generated with Claude Code

jbachorik and others added 30 commits August 6, 2026 14:33
A sidebar row today reads the same whether Claude, Codex or Pi is behind
it, and the only filter is free text. This design gives each agent a
glyph and a color, and adds a chip row that narrows the tree by session
status and by harness.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every claim the spec made about existing code was checked against it, and
most of them were wrong in some load-bearing way: the status mapping the
filter wanted to share is package-private, the sub-tab has no AgentKind to
render, the leading column is a StackPane that stacks rather than lays out,
and the row-membership rule ignored the exemption it had just introduced.
Two features were cut rather than fixed -- a waiting facet that is Claude-only
by construction and empty in half its combinations, and a rescan guard for a
click that was never actually silent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ordered so each task ends somewhere testable: the shared status mapping and
the pure filter record first, since everything downstream needs them and
neither requires a JavaFX toolkit to prove correct; the sidebar surgery
after, where the tests can no longer help and the visual pass takes over.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… error

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The old header restated the status table by hand and omitted
UNSUPPORTED_AGENT, so it silently disagreed with isError once that
status moved to :error. Point at SessionStatusFacet as the single
source of truth instead of duplicating the mapping in prose.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds AgentMarks: per-AgentKind glyph and style-class lookups, plus a
Label factory for session rows. Unrecognized agents (SessionStatus
UNSUPPORTED_AGENT) render the unknown mark rather than the CLAUDE
placeholder kind. Adds the three -drydock-agent-* tokens to both theme
sheets and the .agent-mark styling to app.css.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Converts the session row's leading status column from a StackPane to an HBox
to display both the session status dot and the agent mark side-by-side. Widened
the shared .child-row-status gutter from 16px to 30px min-width and adjusted
bucket path indents accordingly (34px → 48px). The row tooltip now
unconditionally displays the agent name, since the mark carries no tooltip.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The statusCol.setAlignment(Pos.CENTER_LEFT) was dead code, overridden by
the -fx-alignment: center CSS rule on .child-row-status.

The row tooltip now clearly separates agent identity from current activity:
- Status: <status>
- Agent: <agent name>
- Activity: <activity> (when known)
instead of doubling the agent name on the activity line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…erage

Assert reference identity (not List.equals) for the inactive-filter case,
and exercise all three non-session SidebarNode variants — not just
UnopenedWorktreeNode — in the active-filter case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four holes that only open once a session filter exists:
- sessionRowChanged now re-checks membership (matches || exempt) before
  deciding between an in-place row update and a full rebuild, so a
  session crossing the filter boundary appears/disappears instead of
  rendering stale.
- activeSessionChanged now rebuilds when the exemption moves onto or
  off of a session, since the exemption is a function of which session
  is active.
- focusAdjacentLiveSession now restricts its live-session cycle to
  filter.matches(candidate), so keyboard cycling can't land on a
  session the filter hides.
- rebuildTree force-expands every repo while filtering (stashing the
  user's collapse set) so a collapsed repo can no longer swallow its
  only match with no empty state. Re-assertion is gated on
  filterChangedSinceLastRebuild, set only in the two places a filter
  can change, so the disclosure triangle stays live between changes.
- buildSessionRow appends a tooltip line when a row survives only by
  exemption, so it admits why it doesn't match.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the sidebar empty state for when chip/text filters leave no
repository items to show. Two forms: a full-panel swap-in when the
tree is truly empty, and a compact banner above the tree when the
active-session exemption keeps exactly one row on screen (swapping
there would delete the very row the exemption protects). A "Clear
filters" button resets both the text field and the chips in one
change notification, and the state is suppressed entirely when the
workspace has no repositories at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round-1 review fixes for the sidebar empty state: matchCount now
counts every surviving child row (session, unopened worktree,
stale/locked bucket), not just non-exempt sessions, so a text-only
query that matches a branch or worktree path no longer gets the
"Nothing matches" banner painted over its own result. Clear filters
now stops the re-armed filterDebounce before clearing the chips, so
only one rebuild fires instead of two. Also generalizes the
focus-ownership check into a reusable isDescendantOf(node, ancestor)
helper used on both the swap-in and swap-back paths.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The footer suffix, filter prompt, and repo header aggregates all used
to describe an unfiltered sidebar. The footer now appends "· filtered"
whenever filtering() is true, the prompt text says "repos & sessions"
instead of "repos & worktrees" (chips hide worktree rows), and the
repo header's session badge/running dot read the surviving children
from the tree instead of recounting through filter.matches. The
worktree/locked/stale counts suppress only under filter.isActive(),
since a text query narrows without removing those rows.

Also adds RepositorySidebar.diagToggleFacet and registers the "facet"
verb in DrydockApplication's diagTabStep switch, so a scripted visual
pass can drive filter-chip combinations.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
At 320px the row gave the name ~84px and spent 132px on an invisible
control and a pill restating what clicking the row already does. The row
goes single-line, the actions move to an overlay that costs no layout
width, and the branch tag yields before the name does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The width rule and the overlay mechanic come first as standalone units --
one is a pure function with a real test, the other a helper two builders
share -- so the row rebuild that follows is assembly rather than invention.

Two spec assumptions did not survive reading the builders and are recorded
as deviations: the unopened-worktree row has no reserved-width defect to
fix, and extracting the builders into a companion class would drag ten
private fields along behind them for no gain the overlay helper does not
already give.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The scripted diag driver has no pointer, and expanding a bucket is a click
on its summary row -- so the one state where the shared child-row gutter
can be checked against every row type at once was unreachable from a
script.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous formula had a discontinuity at 72px, causing the branch tag to snap
into existence abruptly as the sidebar widened. Now it measures the branch's share
of the width ABOVE the floor, creating a smooth ramp that prevents popping.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Group the repo row's rescan and new-session buttons into an actions
strip and hand them to RowOverlay.wrap, matching the session row fix
from the previous task. Each button keeps its own hover-visibility
binding; the rescan button's INDEFINITE spin still stops via its
unchanged sceneProperty listener since detachment from the scene is
unaffected by which HBox the button sits in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ath indent

Round 1 visual review found two defects in the repo row overlay:

- The repo row's actions container (rescan + new-session) had no
  visibility binding of its own, so RowOverlay's .row-overlay-actions
  fade gradient painted on every row at all times instead of only on
  hover. Bind actions.visibleProperty() to hoverProperty(), matching
  buildSessionRow's existing container-level binding. Per-button
  bindings are left in place.
- The expanded stale/locked bucket's path rows used a left inset of 48,
  tuned for when .child-row-status centered its content. Now that the
  gutter is center-left aligned, the summary label starts at
  .child-row padding-left (16) + .child-row-status min-width (30) +
  the row's HBox spacing (7) = 53; updated both path.setPadding calls
  to match.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…fication

Node.hoverProperty (and the CSS :hover pseudo-class it drives) is set by
Scene's real mouse-move picking, not reachable through a synthetic
fireEvent-style MouseEvent, so the row-overlay hover fade and the
pickOnBounds=false click passthrough had no way to be exercised from a
script. hover:<kind>:<n> drives a real OS pointer move via java.awt.Robot
onto the nth .session-row/.repo-row; clickedge:<kind>:<n> follows up with
a real click near the row's trailing edge, past the overlay's buttons.
Both are diag-only, reachable only from app.drydock.diag.tabScript.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Match buildSessionRow's shape: drop the short-path meta line (the row's
tooltip already carries the full path), let the branch name grow and
ellipsize into the remaining width with a min-width(0) clamp, and pin
the Start pill to its own pref width so it stops ellipsizing into "…".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The class comment still described the old two-line shape (branch on
one line, short path as a sub line). Update it to describe the row
as it is now and say why the path moved to the tooltip instead of
just that it did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jbachorik and others added 5 commits August 8, 2026 15:40
The hover-action strip's pickOnBounds=false never worked: it only makes a
Region click-through where it paints no background, and .row-overlay-actions
painted a gradient across its whole ~102px, so gaps between buttons (and the
~30px past the delete button) silently swallowed clicks that used to open a
session or expand a repo row. Split the strip into two nodes: a mouse-
transparent .row-overlay-fade Region that carries the gradient, and the
actions HBox itself, which now paints nothing so its real gaps fall through
to the row beneath. RowOverlay.wrap's signature tightens to
`(Region row, Region actions)`, dropping the instanceof branch that could
silently skip the styling.

Also fixes, from the same review pass:
- the fade color never matched the row it covered: .session-row.active /
  .repo-row.active were unreachable descendant selectors on a strip that is
  now a StackPane sibling, and the strip is only ever visible while hovered,
  not resting, so both fade tokens were sampled against the wrong state.
  Re-derived -drydock-row-fade(-active) in both themes by compositing
  -drydock-sidebar against -drydock-hover / -drydock-active-bg.
- SessionStatusStyles.updateDot and RepositorySidebar.updateFooter each
  hand-rolled the RUNNING/STARTING running check; both now route through
  SessionStatusStyles.isRunning.
- a repo matched by name alone (no matching children yet, e.g. worktree
  discovery still in flight) was dropped from a filtered tree; it now
  survives and counts toward matchCount.
- buildRepoRow's name/branch labels get setMinWidth(0) like every other
  growing text column in the file.
- buildRepoRow's rescan/newSession buttons no longer duplicate the
  hover-visibility binding their parent `actions` container already owns.
- deleted the unreferenced .resume-pill rule; added a .sidebar-empty-clear
  rule (ghost-bordered, matching .header-icon-button/.worktree-refresh-button)
  for the class Java already applies but no rule styled.
- sessionRowChanged and activeSessionChanged now gate membershipChanged on
  the same predicate (filter.isActive(), chip-only) instead of disagreeing
  with `filtering()` (chip+text), which fired rebuilds a text-only query
  could never change the outcome of.
- collapsedBeforeFilter is pruned alongside `collapsed` so a repo removed
  while filtered and later re-added returns expanded, not collapsed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both verbs only printed what Robot DID, never what happened -- so they
could not fail, and did not, when a click-swallowing regression passed
five reviews. Robot events also silently no-op without a macOS
Accessibility grant, which an agent-run session lacks. hover now checks
row.isHover() after a settle delay and prints VERIFIED/FAILED; clickedge
compares the active session before/after and prints
VERIFIED/FAILED/INCONCLUSIVE, via two new diag-only RepositorySidebar
accessors (diagActiveSession, diagSessionIdForRow).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
hover and clickedge both need java.awt.Robot, which delivers nothing
without a macOS Accessibility grant, so both currently print FAILED in
an automated run. forcehover renders the row-overlay's hovered state
without a pointer, by folding a diag-only override into the same
hoverProperty() binding instead of unbinding it, and stamping the
row's own :hover pseudo-class so the fade is photographed against the
background it is meant to match. pick proves pickOnBounds=false
click-passthrough structurally, via Node.contains() at fixed probe
points, instead of firing a real click. quit shuts the app down
through the same closeAllSessions -> Stage.close() path a real window
close uses, so a scripted tabScript run no longer leaks a process.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Its width was bound and its height was not. An empty Region's preferred
height is 0 and StackPane sizes an unconstrained child to its preferred
size, so the gradient had been painting over zero pixels since the day it
was introduced -- the action buttons drew straight onto the branch tag
with nothing behind them, which is the illegibility the fade exists to
prevent. A structural pick probe could not catch it: a zero-height node is
mouse-transparent either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The wash never hid the branch tag it exists to hide. `-drydock-row-fade
24px` looks like "opaque within 24 pixels"; JavaFX parses it into a
PROPORTIONAL gradient whose opaque stop lands at 1.0, so the ramp spanned
the node's whole 102px width and the tag -- sitting 41%-92% across it --
stayed legible through a mostly transparent wash. Proportional stops say
what was meant.

Adds a `fadeinfo` diag verb that prints the fade's measured bounds beside
the row's and the tag's, its z-order among its siblings, and its RESOLVED
paint. That is what found this, after two rounds lost to a plausible
theory the data had already contradicted.

Corrects 606be3a, whose message claimed the gradient had been painting
over zero pixels. It had not: the probe shows the node was 102x25 with
correct z-order all along. The explicit height stretch that commit added
is worth keeping -- leaning on StackPane's default sizing for a node with
no content is fragile -- but its stated reason was a guess, not a finding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jbachorik
jbachorik merged commit 731c257 into main Aug 9, 2026
1 check passed
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