feat(tui): scrollable sidebar panes and a corrected attention model - #137
Merged
Conversation
ackFocusedPane now clears blockedSince/blockedReason alongside unseen: focusing a parked pane is the acknowledgement, so a pane no longer sits marked blocked once the user is looking straight at its prompt. pinnedAttention is untouched — it is the explicit "don't let me forget" mark and only the context-menu actions own it. newTestModelWithTabs now takes a panes-per-tab count so a two-pane tab fixture doesn't need a second builder; existing callers updated. Also: - correct the turnActive/unseen/blockedSince field comments, which described a park clearing turnActive/setting unseen — no longer true since the spinner now survives a park - assert the resume-after-park test's actual point: PostToolUse clears the blocked mark it set, not just that unseen stays clear
pinnedAttention already drove the pane border and the tab label; the sidebar row -- the one place that lists every pane at once -- never showed it. Adds a dedicated glyph/style and renders it as the pane's own state glyph when nothing outranks it, or as a trailing mark alongside blocked/working when it does, so a pin never disappears behind a transient state.
The tab heading and the idle pane rows beneath it both painted with sidebarDimStyle (color 243), so the grouping the PANES section exists to show was invisible. Headings now default to a bright foreground and carry the tab's own colour when it has one, applied as foreground rather than the tab bar's background block, which reads as noise at 22 columns. Also prefixes each heading with its 1-based ordinal, matching the tab bar's "N:name" convention and the Alt+1..9 keys, placed before the name so a narrow strip elides the name and keeps the number. Collapses paneRow's duplicated doc comment into one, now naming the pinned-attention glyph it already renders. Extends the paneRow width sweep with pinned and pinned+state combinations.
Right-clicking a pane row in the project sidebar fell through to a no-op: the MouseRight branch in the sidebar swallow only handled project rows. Add a sidebarRowPane case that opens the same pane context menu the in-pane right-click uses, without focusing the pane first (a right-click that silently switches tabs would surprise the user). That alone was not enough: executeCtxMenuItem resolved its target through the active tab, so a menu opened on a pane sitting in a background tab would render and then act on nothing (its FindLeaf guard never matched). Resolve the owning tab from findPaneAndTab instead, indexing the pane's own project rather than the active project's tab slice.
Right-click on a sidebar pane row opened the context menu without focusing the pane, so a menu opened on a background-tab pane looked right but acted wrong: eight of the ten menu items (Rename, Restart, Close, Mute, Notes, Focus, Lazygit, History) are shared with the keybinding and command-palette paths and resolve their target through the active tab's active pane internally, not through the clicked pane's id. Choosing one of those from a background-pane menu silently acted on whatever pane was on screen instead. Mirror left-click and focus the pane first via the same activateSidebarRow/focusSidebarPane path, carrying its switchTab IPC command forward instead of discarding it. executeCtxMenuItem's findPaneAndTab-based tab resolution stays as a second line of defense for any future entry point that opens a menu without focusing.
The comment above executeCtxMenuItem's findPaneAndTab resolution still described the reversed pre-focus-first design: it claimed a background-tab pane menu is possible and that pane-id resolution is what makes it work. Neither is true after the sidebar entry point started focusing the pane first. Worse than stale, the old wording named the wrong safety property: this block only covers the two paneID-resolving attention items, not the eight that resolve through the active tab/pane. A reader trusting the comment could add a non-focusing entry point believing this block would redirect all ten items, when in fact eight would silently act on whatever pane is on screen.
The PANES list was hard-truncated at the strip's height with a dim " …",
so with enough tabs and panes the tail was simply unreachable. The
PROJECTS block stays pinned — it is the navigation the sidebar exists
for — and the body now windows at Model.sidebarScroll, marking whatever
it hides above and below with ⋯ rather than just stopping.
The offset lives inside sidebarVisibleRows because the paint and the hit
test both call that one function with the same height; an offset applied
at a render site is the row-drift bug ("click project 3, select project
2") in another form. It clamps a local copy and writes nothing: View has
a value receiver, so a clamp written back from the render path would be
discarded there and kept on the hit-test path, and the two would hold
different offsets. sidebarRows now returns the section boundary rather
than having the windowing re-derive it by matching the heading text.
A strip too short to leave minPaneRows for the body keeps the old tail
cap: eight projects and no panes is worse than a truncated list of both.
Also fixes sidebarTabHeading, which summed marker + ordinal + name and
let one closing truncateCells arbitrate — that cut the ORDINAL, the part
its own comment promises survives and the part that maps to Alt+1..9
(" 10:name" came out " 10" at width 4). The ordinal is budgeted first
now, and the heading pads to the full width like every other row.
Wire the wheel event already swallowed over the project sidebar's strip into scrollSidebar, which moves the PANES body by one notch (m.cfg.UI.MouseScrollLines, default 3) and clamps against the same body geometry the paint windows against. Extract sidebarBodyGeometry so scrollSidebar and sidebarVisibleRows derive bodyLen/bodyH from one place instead of two, rename the local that shadowed the max builtin in maxSidebarScrollFor, and comment the three provably-unreachable totality guards in sidebarVisibleRows. Also reset sidebarScroll on project switch, so the incoming project's PANES body always starts at the top rather than carrying over an offset scrolled deep into the outgoing project's pane list.
switchProject was not the only path that changes the active project — jumpToPane sets it directly and is the shared choke point for MCP set_active_pane, notification-sidebar navigate, pane-history back, and the palette's goToPane. None of them reset the PANES scroll offset on a project-crossing jump. Reset it in jumpToPane's project-index loop, gated on the project actually changing so a same-project jump keeps the user's scroll position.
Add scrollSidebarToPane, wired into jumpToPane (the shared cross-tab choke point for MCP set_active_pane, notification jumps, pane-history back and the palette) and focusSidebarPane (the sidebar's own click path). It moves the PANES section the minimum distance needed to bring a pane's row into the visible window, using bodyH-2 as the worst-case window when picking a new offset so the target can never land under a scroll marker. In jumpToPane it runs after the existing project-boundary scroll reset, not before, so a cross-project jump doesn't have its target thrown away. In focusSidebarPane the row clicked is always already on screen, so the call must be a no-op there. Checking that against the same conservative bodyH-2 span used to pick a new offset is wrong: that span is one row narrower than what's actually painted whenever only one scroll marker is showing, so it wrongly judged the last visible row off-screen and scrolled the sidebar out from under the click that just landed on it. scrollSidebarToPane instead checks the real painted window (sidebarVisibleRows) first and only falls back to the conservative span once it knows a scroll is genuinely needed.
Add the changelog entry for the nine shipped fixes: sidebar scrolling and pinned-attention markers, tab heading ordinals/colour, amber blocked tabs, the permission-prompt spinner fix, and the sidebar right-click context menu. Correct four documentation defects the implementation exposed: - hooks-and-sessions.md still said a park stops the spinner and marks the pane unseen, in two places. Both are false since the park fix: a park now sets blockedSince without touching turnActive, and the same paragraph's tabStyle precedence description was stale too (missing the blocked tab style entirely). - projects.md's sidebar layout notes claimed markers line up on both project and tab rows with no exception; the tab row's marker now gives way below ~5 cells to keep the ordinal. - Three sidebar.go comments (plus two test-file comments repeating the same line) described sidebarRowAt as mapping to rows[y-1]; it maps to rows[y], as the function's own doc comment already says. - docs/features.md's sidebar glyph list still showed the pre-#132 warning-sign glyph for a blocked pane instead of the current one. Record the two invariants a future sidebar change must not break: the PANES section scrolls while PROJECTS stays pinned, and a park must never clear turnActive again.
WorkEventPark's comments still described the pre-fix model: park clears the spinner and marks the pane unseen, same as a stop. That changed when workPark stopped clearing turnActive. Update both comments to say what the classifier still does (nothing changed here) and what the consumer now does with the result.
ackFocusedPane runs at the top of every Update, including the shared 100 ms work-spinner tick that is guaranteed to be running because the pane is working. Clearing blockedSince there set the mark and dropped it about 100 ms later, so for a pane that was focused when it parked the sidebar's marker, the amber tab, the project badge's blocked count and the attention-queue entry were none of them ever observable — and that is the commonest park there is: the agent asks for permission while you are sitting in its pane. With a park no longer clearing turnActive, the pane then went on claiming to be working the whole time it waited. The state is kept and the presentation gives way instead: paneRow suppresses the blocked glyph and its reason for the focused pane, so "you are looking straight at the prompt" costs a glyph rather than a fact. tabBlocked, counts() and the attention queue keep reading the same blockedSince, and leaving the pane restores every signal with no hook edge required. unseen still clears on focus — it is a "you missed something" flag that looking genuinely answers, where blockedSince is a fact about the agent. The wide-glyph sidebar fixture gains an idle pane ahead of its blocked one: with a single pane in the tab that pane is focused, so its CJK blocked reason stopped reaching the row and the fixture quietly stopped exercising a wide-glyph suffix.
scrollSidebar added the notch to the STORED offset, which can legitimately sit past the current maximum: nothing clamps it when the geometry changes underneath, and sidebarContentHeight is height-1, so any vertical resize moves the window height while closing panes moves the body length. The paint stayed correct — sidebarVisibleRows clamps a local copy, which is the purity rule — so the symptom was not row drift but a dead scroll plateau: the next several notches computed from the stale value, clamped back to the same visible maximum, and the strip did not move. Measured on a 30-pane project resized from 20 rows to 30: the offset stuck at 9 for three notches before the fourth moved anything. Clamping the stored value first makes the first notch move the strip. The new test changes the terminal height between two wheel events, which is the case no existing test covered.
jumpToNextBlocked sets activeTab and ActivePane by hand rather than routing through jumpToPane, so it never reached scrollSidebarToPane and could land the user on a pane below the fold — on the one navigation most tied to the attention model, and the one whose premise is that the sidebar is where you look to see where you landed. A same-project jump kept whatever offset was there; a cross-project one reset to 0, which is not the same as bringing the target into view. Placed after switchProject for the reason jumpToPane records: a cross-project switch zeroes the offset, so computing it earlier would throw the work away.
Six statements across the package and three rules files still taught rules this branch changed, each true when written: - applyWorkTransition's own doc comment claimed a park sets an unseen mark on an unfocused pane. A park keeps turnActive, so the falling edge never fires; tabBlocked is what carries a parked background pane. - PaneModel.blockedSince and tabBlocked both named ackFocusedPane as a route to a clear. It no longer is. - The pane border's "finished/parked/pinned" comment listed a reason that can no longer set the flag. The colour is unchanged. - The context menu's justification for its Clear attention row is correct again now that focus does not clear the mark, and says so explicitly rather than leaving the reader to re-derive it from the glyph vanishing off the focused row. - The rules files taught the reversed ack rule and did not mention the render-side suppression at all. The sidebar right-click also resolves its row once, before focusing, instead of once on each side of it. Both resolutions agreed, but only via two separate arguments about a strip that moves — row order being independent of the active tab, and the scroll being a no-op for a row already on screen — and the menu then acts on the pane it names.
features.md's Projects section described the sidebar's pane rows and its right-click behaviour incompletely, and the changelog claimed a behaviour that was reversed before merge. - the amber tab for a parked pane, the numbered tab headings, the ◆ pinned-attention marker, and the scrolling PANES section with its above/below markers and wheel support - right-clicking a pane row FOCUSES that pane, switching tabs if it lives on another one, before the menu opens — a side effect that should be read rather than discovered. Also noted in keybindings.md, which enumerated right-click on a pane and not on the sidebar row. - the marker being hidden on the pane you are in while every other level keeps showing it, replacing the changelog line that said focus clears the mark outright
Approving a Bash/Edit/Write permission prompt fires no hook of its own — promptToolMatcher is AskUserQuestion|ExitPlanMode, so PostToolUse does not cover it, and the pane's next event is the turn's Stop. With focus no longer clearing the mark, an ANSWERED prompt therefore kept its tab amber, kept counting as blocked rather than working in the project badge, kept being offered by Alt+Shift+A, and put the marker back the moment the user switched away. Real user input routed to a pane is the one signal that distinguishes answering the prompt from looking at it, so it clears the mark. A glance is not an answer; a keystroke is. Wired at the producers that represent a human acting on the pane — the two handleKey forward paths and both paste paths — rather than at enqueueInput or forwardInputBytes, which both look like the single choke point and are not. enqueueInput also carries forwarded wheel notches, and forwardInputBytes is used by the selection handler to walk the shell cursor during a mouse drag, emitting arrow-key escapes a permission prompt would consume as a choice. Scrolling a parked pane or dragging a selection across it is a glance with a mouse, and a test pins both. The trigger is input reaching a pane rather than focus, so the asynchronous paste path answers a pane that is no longer the active one. The ordered-input queue is untouched.
Eight of the ten pane context-menu items resolve their target through activeTabModel().ActivePaneModel(), so they are correct only while the menu's target sits in the active tab. Every entry point focuses the pane before opening the menu, which establishes that at OPEN time and not at EXECUTE time: MCP set_active_pane moves the active project and tab, and the Update-entry guard only closes a menu whose target has vanished, not one whose active tab moved. Rename then seeded the on-screen pane's name, Mute toggled it, and Restart/Close armed a confirm for it. Refuse instead, before the focus sync so nothing is half-applied. The refusal covers all ten rows, including the two attention items that resolve paneID directly and could still have acted correctly — one menu is one surface, and the remedy is a second right-click.
tea.MouseWheelMsg also carries MouseWheelLeft/Right — a trackpad swipe or shift-scroll emits them — and the handler passed the button to scrollSidebar as `msg.Button == tea.MouseWheelUp`. Both horizontal buttons read as "not up" and scrolled the pane list down. Match the two vertical buttons explicitly, as every other wheel consumer here does, keeping the swallow outside the switch so a horizontal notch still does not reach the pane underneath. Three smaller fixes to the surrounding code: - Give the "is the body windowed at all" gate one definition (sidebarBodyWindowed) instead of three copies across the paint and both writers, the same drift hazard sidebarBodyGeometry exists for. - scrollSidebarToPane did arithmetic on the unclamped stored offset, standing next to scrollSidebar's comment on why that must not be done. It was safe by a non-obvious proof; now both read the same way. - Cap the wheel notch at the window height. MouseScrollLines is a hand-editable int and nothing bounded it, so off+lines wrapped negative at the top of the int range and a wheel-DOWN jumped the strip to the top. Also comma-ok the one unchecked Model assertion in package code, which panicked inside the Update loop if it ever failed.
blockedTabStyle replaces the tab background wholesale, and the background is the only thing activeTabStyle and inactiveTabStyle differ by. With two or more agents parked — the ordinary case the amber mark exists for — the tab bar stopped saying which tab you were on. Add an underlined active variant. renderTabBar measures style.Render(name) to build its click zones, so the variant must render at exactly the same width: an SGR attribute costs no cells where extra padding would shift every hit zone after it.
The rule file auto-loads for ctxmenu.go and still described the menu as though every dispatched item reached its target. Document the refusal and the distinction that makes it necessary: entry points focus the pane at OPEN time, which is not the same as its tab still being active at EXECUTE time, and MCP set_active_pane is the one producer that can move it in between.
artyomsv
added a commit
that referenced
this pull request
Aug 9, 2026
## Summary
A tab was painted amber ("blocked on you") while its agent was still
working with three background subagents running. Reported from a live
v1.53.0 session; the pane's own hook spool shows why:
```
UserPromptSubmit → turn starts
SubagentStart ×4 → 4 background agents
Stop "Reply ready"
Notification "Claude is waiting for your input" ← sets blockedSince
SubagentStop "cr-rules done" → 3 remain
Stop, Stop "Reply ready"
Notification "Claude is waiting for your input" ← re-sets it
```
That `Notification` is Claude's **idle nudge** — its own message says so
— arriving *after* `Stop`, while subagents still run.
`ClassifyWorkEvent` mapped every `Notification` to `WorkEventPark`, so
`blockedSince` was set; `tabBlocked` then painted the tab amber and
`paneRow`'s blocked-outranks-working precedence hid the `◐ ⋯3` the row
should have shown.
**Root cause:** `Notification` is ambiguous. Claude fires it both for a
permission prompt and for an idle nudge, and the classifier collapsed
both into one kind.
## The fix
The hook already knows which is which — `runhook.go` decodes Claude's
`message` field and forwards it as the event Title. So the
disambiguation happens at the **producer**, where the information
exists, and the consumer defaults to the safe answer:
- **Producer** (`internal/claudehook/runhook.go`): positively identifies
the idle nudge from the message and marks it with
`data[hookevents.DataNotifyKind] = NotifyKindIdle`. Nothing is emitted
for a message it does not recognise.
- **Classifier** (`internal/hookevents/workstate.go`):
`hook.claude.Notification` becomes `WorkEventNotify`.
`PermissionRequest` and opencode `permission.ask` stay `WorkEventPark` —
unambiguous, always a real block.
- **Consumer** (`internal/tui/workstate.go`): parks **unless** the event
is positively marked idle *and* the turn is inactive.
**The direction of the match is the point.** Matching upstream English
prose is fragile, so the idle phrase is matched positively and
everything unrecognised is treated as a park. If Claude rewords the
nudge, the regression is the old visible amber tab — not a permission
prompt that never surfaces. The `turnActive` condition is retained as
the second gate so the sequence above still resolves correctly against
an older hook binary that doesn't yet emit the marker (mixed-version
daemon/TUI, and the `off`/`verbose` hook-mode paths).
| Situation | Before | After |
|---|---|---|
| Permission prompt mid-turn | `▲` blocked | `▲` blocked (unchanged) |
| `PermissionRequest` after `Stop` | `▲` blocked | `▲` blocked
(unchanged — unambiguous) |
| Idle nudge after `Stop`, subagents outstanding | `▲` blocked, **tab
amber** | `◐ ⋯N` working, tab clean |
| Idle nudge after `Stop`, nothing outstanding | `▲` blocked | `✓`
unseen (what `Stop` set) |
| Unrecognised `Notification` message, any turn state | `▲` blocked |
`▲` blocked (safe default) |
The wrong state predates #137 — what #137 added was `tabBlocked`, which
made it loud: an idle nudge now repainted the whole tab.
## Review history
The first iteration gated purely on `turnActive`. Both the security and
code-quality reviewers independently found that this trades a **visible,
self-clearing false positive** for a **silent, terminal false
negative**: `turnActive` is false whenever a background subagent raises
a prompt after the main turn's `Stop`, after a reattach
(`resetWorkStateForReattach` zeroes it), or when ring eviction drops the
start edge — and a parked agent emits no further hook to recover the
mark. Notably the production trace contains no `PermissionRequest` at
all, so on that Claude version `Notification` is the sole permission
signal and that gate was the only guard on it. Hence the producer-side
marker and the park-by-default.
Ten findings were fixed in the follow-up commits, including two rule
files left contradicting each other and a replay-safety comment that
understated eviction risk once a second edge began depending on a
preceding start edge.
## Test plan
- `./scripts/dev.sh test` — 28/28 packages
- `./scripts/dev.sh vet` — clean
- `./scripts/dev.sh docs-size` — within limits
New tests, RED before GREEN, covering both directions of the gap:
1. The full production incident replayed, including the `SubagentStop` —
not blocked, still working, `tabBlocked` false.
2. `Notification` mid-turn still blocks (must not regress).
3. `PermissionRequest` after `Stop` still blocks (unambiguous signal
stays ungated).
4. A `Notification` whose message is **not** the idle phrasing, with
`turnActive` false → blocks. This is the case the first iteration
silently dropped.
5. A `Notification` carrying **no marker at all** (older producer) →
blocks. Pins mixed-version safety.
6. Producer-side classification in `runhook.go`, plus the
`ClassifyWorkEvent` table.
Manual, against a dev build (confirm `[dev]` in the status bar):
1. Let an agent's main turn finish while background subagents run, then
leave the pane until Claude's "waiting for your input" nudge fires.
Expect `◐ ⋯N` and no amber tab.
2. Trigger a real Bash permission prompt mid-turn — expect `▲` and an
amber tab exactly as today.
artyomsv
added a commit
that referenced
this pull request
Sep 6, 2026
## What Commits 28 files that were sitting untracked in the working tree, plus 7 updates to notes already tracked. **`docs/superpowers/` — 17 plan and spec records (2026-08-05 … 2026-08-19)** The tracked record jumps from `2026-08-01` straight to `2026-08-15`. Every one of the missing documents describes work that has since shipped: | Record | Shipped as | |---|---| | worktree-panes stage A + ledger, worktree-owned-panes design | #133 (v1.51.0) | | worktree-panes stage B | #134 | | sidebar-width-control, sidebar-worktree-name | #137 | | sidebar-attention-fixes (+ design) | #137 / #139 | | keybinding-registry-stage-1, keybinding-presets design | #138 | | overlay-lifecycle (+ design) | #153 | | desktop-notifications (+ design) | #154 / #193 | | keybinding-sequences-presets (+ design) | #164 | | alt-screen-replay-detection | #173 (issue #172) | **`.claude/agent-memory/` — 11 new review-agent notes, 7 updated** Same shape as the 33 notes already tracked and the `chore(memory)` commits in #206 / #207. ## Why now Found during a branch and worktree cleanup pass. Two of the memory notes existed only inside a merged worktree that was about to be deleted. ## Risk Documentation only. No file under `cmd/` or `internal/` is touched, so the changelog gate does not apply and the release workflow's denylist skips this entirely — no version bump.
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.
Summary
Notification/PermissionRequestpark with a match-all trigger while the resume edge (PostToolUse) is narrowed toAskUserQuestion|ExitPlanMode, so approving a Bash/Edit/Write prompt fires no hook at all.workParkno longer clearsturnActive— the deleted assignment was a no-op exactly when it was right (an idle-wait nudge, whereStopalready cleared it) and wrong exactly when it wasn't (a permission prompt mid-turn).⋯ N above/⋯ N belowmarkers, driven by the mouse wheel, and a pane reached from the palette, MCP, the notification sidebar or the attention queue scrolls into view.tabBlocked, amber); focus no longer destroys the mark, but the row hides it for the pane you're in; answering — a real keystroke or paste — clears it everywhere; andpinnedAttention, implemented since the context menu shipped, is finally rendered (◆).Alt+1..9.Reported issue "cannot switch panes within the same tab from the sidebar" was retested and withdrawn — no change here.
Notes for the reviewer
Two design decisions reversed during implementation, both recorded in the spec with their reasoning:
executeCtxMenuItemwas an equivalent fix. It isn't: only the two attention items resolve viafindPaneAndTab; the other eight — including Restart and Close — re-resolve throughactiveTabModel().ActivePaneModel(). Since this is the first entry point that can open the menu on a background-tab pane, "Close pane…" would have armed the confirm for whatever pane was on screen.ackFocusedPaneruns on everyUpdate, including the 100 ms spinner tick that is running because the pane is working — so clearing on focus destroyed the mark ~100 ms after a park, before it could be seen, while the spinner claimed progress. The mark now survives focus (tab, badge and attention queue stay truthful) andpaneRowsuppresses the glyph for the focused pane only. Real input to the pane is what clears it: a glance is not an answer, a keystroke is.executeCtxMenuItemrefuses all ten items when the owning tab is no longer active. Entry points focus at open time, which is not the same as still being active at execute time — MCPset_active_panecan move it in between, and nothing closes an open menu when it does.This is larger than the 400-line guideline (+2.4k). It is one logical concern — the sidebar and the attention model are the same surface, and items 1 and 6 could not be split (dropping the
turnActiveclear removes the unseen mark, sotabBlockedhad to land with it). Happy to restructure into stacked PRs if you'd rather review it in pieces.Test plan
Automated — all run in Docker via
scripts/dev.sh:./scripts/dev.sh test— 28/28 packages, 1261 tests ininternal/tui, output pristine./scripts/dev.sh vet— clean./scripts/dev.sh docs-size— within limitsManual, against a dev build (
./quil-dev.exe, confirm[dev]in the status bar):▲on its own row, spinner still running, but its tab goes amber and the project badge counts it blocked. Switch away →▲appears. Come back and answer it → the mark clears everywhere.▲amber on its sidebar row and an amber tab; focusing it hides the row glyph but keeps the tab; answering clears both.⋯ N below; wheel over the strip scrolls the PANES body while PROJECTS stays put; a horizontal/shift wheel does nothing. Resize taller while scrolled to the bottom → the next wheel notch moves the strip.◆on the row, surviving focus.Alt+Shift+Awith a blocked pane below the fold → it scrolls into view.1:name,2:name, in white or the tab's colour.One open question worth watching in use: whether Claude re-fires
Notificationwhile a prompt is still outstanding. Nothing in the code establishes it either way, and it changes how long an unanswered pane stays marked in edge cases — noted in the plan's checklist rather than assumed.