Skip to content

feat: clearer worktree status indicators - #265

Merged
centdix merged 8 commits into
mainfrom
glm/sticky-indicators
Jun 9, 2026
Merged

feat: clearer worktree status indicators#265
centdix merged 8 commits into
mainfrom
glm/sticky-indicators

Conversation

@Guilhem-lm

@Guilhem-lm Guilhem-lm commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Makes the sidebar worktree status indicators legible by removing the agent-status vs. unread-dot overlap, and adds floating bars that surface the statuses of rows scrolled out of view.

Three increments (see commits):

  1. fix — a notification firing for the conversation you already have open no longer lights its unread dot.
  2. feat — the per-row indicator now shows exactly one mark: the agent-status icon for working/waiting/error, a blue dot for a finished-but-unseen run (done + unread), and nothing for done+read / idle. The standalone blue dot beside the icon is gone.
  3. feat — two floating bars (top = hidden above, bottom = hidden below) summarise the attention-worthy statuses of off-screen rows; clicking a chip smooth-scrolls (cycling) through that direction's matching rows.

Demo

Screen.Recording.2026-06-08.at.14.40.01.mov

Changes

  • App.svelte: skip adding the open (selectedBranch) conversation to notifiedBranches.
  • AgentStatusIcon.svelte: animated working dots; done renders a blue accent dot only when unread, otherwise nothing; new unread prop.
  • WorktreeList.svelte: removed the separate unread <span> (folded into AgentStatusIcon); IntersectionObserver classifies each row as above/visible/below; two position-based floating bars rendered from a shared {#snippet}; per-direction click-to-cycle scrolling.
  • worktree-list.ts: pure helpers overflowStatusOf, countAgentStatusesIn, branchesWithAgentStatus (+ rowShowsAgentStatus). Bars count waiting, error, and done-unread; hidden-only, both directions.
  • worktree-list.test.ts: 8 new unit tests for the counting/cycling helpers.

Notes / scope

  • Bars count only hidden rows (live-updates as you scroll; bar hides when its direction is empty). Direction is conveyed by bar position — no arrows.
  • Frontend-only scroll affordance: no API/data change, so no CLI surface (AGENTS.md parity rule N/A).
  • Known edge: when a worktree's tmux session is fully closed (mux !== "✓"), its icon/dot is suppressed by the existing row guard; the "closed" badge carries the row instead.

Checks

  • bun run check (frontend) — 0 errors / 0 warnings
  • bunx vitest run src/lib/worktree-list.test.ts src/lib/WorktreeList.test.ts — 18 passing
  • No regressions: full suite 34 failed/75 passed both before and after (the 34 are pre-existing localStorage/happy-dom env failures unrelated to this branch)

Test plan

  • Open a worktree, trigger a notification for it — its row should not get a blue dot; another branch's notification should.
  • Confirm a running agent shows animated green dots; a finished-unseen run shows a blue dot that clears to nothing once opened.
  • Add/resize worktrees so waiting/error/done-unread rows sit off both ends — verify a top bar and a bottom bar appear independently and hide when nothing is hidden in that direction.
  • Click a chip and confirm it smooth-scrolls and cycles through that direction's matching rows.

Generated with Claude Code

Guilhem-lm and others added 3 commits June 8, 2026 13:15
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jun 8, 2026

Copy link
Copy Markdown

Claude finished @Guilhem-lm's task in 2m 11s —— View job


Code Review — PR #265: clearer worktree status indicators

Nice, well-scoped PR. The split into three commits, the pure-helper extraction with tests, and the position-based bars are all clean. Below are findings grouped by severity. Nothing here is a blocker — mostly minor robustness/UX notes.


🟡 Worth addressing

1. IntersectionObserver is torn down and rebuilt on every agent-status update
WorktreeList.svelte:89-115 — the effect depends on rows, and visibleWorktreeRows (App.svelte:407) is a brand-new array on every visibleWorktrees change, which fires on each WebSocket agent-status/service poll. So the observer is disconnect()-ed and recreated very frequently, re-observing every [data-branch] element each time. It works (because rowPositions is preserved across runs, so the bars don't flicker), but it's needless churn.

Consider keying the effect on the branch identity list rather than the whole row objects, so it only re-observes when rows are added/removed:

let branchKey = $derived(rows.map((r) => r.worktree.branch).join("\n"));
$effect(() => {
  const root = listEl;
  if (!root) return;
  branchKey; // re-observe only when the set of rows changes
  ...
});

Fix this →

2. rowPositions keeps stale entries for removed branches
WorktreeList.svelte:96-106 only ever sets keys; nothing prunes branches that leave rows. A removed worktree's entry lingers in the map (and in aboveBranches/belowBranches) for the session. It doesn't corrupt counts — countAgentStatusesIn / branchesWithAgentStatus both filter against rows — but it's unbounded growth and a latent footgun if a future consumer trusts those sets directly. Cheap to fix by intersecting next with the current branch set when rebuilding the observer, or pruning in the same effect.


🟢 Minor / nits

3. Empty icon slot still occupies a flex gap
WorktreeList.svelte:201-209 always renders <span class="shrink-0"><AgentStatusIcon .../></span> when the row isn't creating/initializing/closed. For agent === "done" + read, AgentStatusIcon renders nothing (AgentStatusIcon.svelte:47-59), leaving an empty flex child that still contributes the parent's gap-1.5 — a few px of trailing space with no glyph. Negligible visually, but you could gate the span on the icon actually rendering something to keep spacing uniform.

4. "open conversation" == selectedBranch is an approximation
App.svelte:241 correctly stops a notification for the currently-open branch from lighting its dot. Just note this equates "open" with "selected", so if selectedBranch is restored from localStorage but the user isn't actually looking at that pane, a finished run there won't surface as unread. Matches the PR's stated scope — flagging only so it's a conscious choice.

5. "above" cycling starts from the top-most row, not the nearest
cycleToStatus (WorktreeList.svelte:138-153) cycles branchesWithAgentStatus in document order. For the bottom bar (below) that's intuitive (nearest first). For the top bar (above), the first click jumps to the top-most hidden row rather than the one just above the fold. Arguably fine, but reversing order for the above direction would feel more "next nearest". Pure UX nit.


✅ Looks good

  • Pure helpers in worktree-list.ts are clean and the 8 new tests cover the meaningful branches (hidden-icon guard, done-unread gating, set restriction, ordering).
  • pointer-events-none container + pointer-events-auto chips is the right call so the bars don't eat list scroll/clicks.
  • prefers-reduced-motion handling on the working-dots animation is a nice touch.
  • The rowMargin: "-40px" to keep rows tucked behind the bars counted as hidden is a sensible detail.

I did not run the build/tests in this review; relying on the PR's stated bun run check (0/0) and vitest results. Happy to dig into any of the above if you'd like a patch.

  • Read changed files and diff
  • Review App.svelte (notification fix)
  • Review AgentStatusIcon.svelte (merged indicator)
  • Review WorktreeList.svelte (floating bars / observer)
  • Review worktree-list.ts + tests
  • Post review feedback

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 8, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
webmux 35d4a16 Commit Preview URL

Branch Preview URL
Jun 08 2026, 02:21 PM

Guilhem-lm and others added 3 commits June 8, 2026 14:33
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Guilhem-lm
Guilhem-lm marked this pull request as ready for review June 8, 2026 13:42
@claude

claude Bot commented Jun 8, 2026

Copy link
Copy Markdown

Claude finished @Guilhem-lm's task in 3m 32s —— View job


Code Review — PR #265: clearer worktree status indicators

Reviewed the diff against origin/main plus the surrounding code in App.svelte, AgentStatusIcon.svelte, WorktreeList.svelte, and worktree-list.ts. Overall this is a clean, well-scoped PR — the three-commit split, the pure-helper extraction with tests, and the position-based bars are all nicely done.

Notably, the two items from the earlier automated review are now resolved in the current commits:

  • ✅ Observer churn → the effect is keyed on branchKey (re-observes only on row add/remove), not the whole row array.
  • ✅ Stale rowPositions → now pruned against the present branch set each rebuild.
  • ✅ The -40px magic margin → now derived from the measured bar height (BAR_OFFSET + offsetHeight).

Nothing below is a blocker.


🟡 Worth a look

1. rootMargin ↔ observer is a self-referential loop (it converges, but worth confirming)
WorktreeList.svelte:96-115rootMargin is derived from topBarHeight/bottomBarHeight, which only become non-zero once the bars mount, and the bars mount based on the observer's own output. So each bar toggle tears down and rebuilds the observer one extra time. I worked through the cases and it converges (a row that only sits in the 8–40px band behind a bar never triggers the bar on its own — it stays visible until something genuinely off-screen brings the bar in), so there's no oscillation. Just flagging the coupling since it's subtle and easy to break in a future edit — a comment noting "bar height feeds the margin, so the observer rebuilds on bar toggle (bounded, converges)" would help the next reader.

2. unreadCount still increments for the open branch
App.svelte:241-245 — the new guard correctly skips notifiedBranches for n.branch === selectedBranch, but unreadCount++ (the bell badge, line 1232/reset at 336) runs unconditionally just below. So a notification for the conversation you're actively viewing won't light its row dot but will bump the global bell count. That may well be intended (the notification center still lists it), but it's a small inconsistency with the stated goal — consider whether the same guard should wrap unreadCount++.


🟢 Minor / nits

3. Empty icon slot still occupies the flex gap (carried from prior review, still applies)
WorktreeList.svelte:243-250 always renders the <span class="shrink-0"><AgentStatusIcon …/></span> for live rows. For agent === "done" + read, AgentStatusIcon renders nothing (AgentStatusIcon.svelte:47-59), leaving an empty flex child that still contributes the parent gap-1.5. Negligible, but you could gate the span on the icon actually rendering for uniform spacing.

4. Chip count has no accessible label
WorktreeList.svelte:445-449 — the chip's <svg> is decorative and the count is a bare <span>{counts[status]}</span>. The button's title covers hover, but a screen reader on the button reads just the number. An aria-label like "{counts[status]} {statusLabels[status]} {direction}" on the button would make it self-describing.

5. done pill variant now renders no glyph
AgentStatusIcon.svelte:13 still maps done → green pill background, but with the checkmark removed the pill body would show color + text and no icon. The pill prop isn't used anywhere today (only the inline variant is), so this is dead-path only — noting it so it isn't a surprise if the pill is ever reused for done.


✅ Looks good

  • Pure helpers in worktree-list.ts read cleanly and the 8 new tests cover the meaningful branches (hidden-icon guard, done-unread gating, set restriction, document-order). branches.reverse() in cycleToStatus is safe since branchesWithAgentStatus returns a fresh array.
  • pointer-events-none container + pointer-events-auto chips correctly keeps the bars from eating list scroll/clicks.
  • Above/below classification via boundingClientRect.top < rootBounds.top is correct, and the indexOf(...) + 1 cursor with the -1 → restart fallback (when the cursor row scrolls into view) is handled and well-commented.
  • prefers-reduced-motion handling on the working-dots animation, and the nth-child delays land correctly since the circles are the only children of the <svg>.

I did not install deps / run the build here (node_modules absent on this fresh checkout); relying on the PR's stated bun run check (0/0) and vitest results.

  • Read changed files and diff against origin/main
  • Review App.svelte (notification fix)
  • Review AgentStatusIcon.svelte (merged indicator)
  • Review WorktreeList.svelte (floating bars / observer)
  • Review worktree-list.ts + tests
  • Post review feedback

@Guilhem-lm
Guilhem-lm requested a review from centdix June 8, 2026 14:17
Guilhem-lm and others added 2 commits June 8, 2026 16:20
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@centdix
centdix merged commit 4b528a7 into main Jun 9, 2026
2 checks passed
@centdix
centdix deleted the glm/sticky-indicators branch June 9, 2026 08:21
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.

2 participants