Sessions show which harness they run, and the row has room to say so - #16
Merged
Conversation
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Resumepill restating what clicking the row already did. Names truncated to about nine characters — on a title the agent itself had authored viasession_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_AGENTnow 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
+and⟳buttons with it. Recovery is to clear the filter; the filter is deliberately transient and resets on restart.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