Alt+1-9 selects a window, not a pane; and the capture header goes - #14
Conversation
"Do not show the capture line for capture panels." A spawn window drew a dim `⇱ capture ^\[Chat\]` row between its tab strip and its output, naming the trigger pattern that routed lines in. It is gone, and so is everything that existed only to put it there. That was a whole column of plumbing, verified unused before removal: the pattern was minted in TriggerEngine (SpawnRoute), carried through WorldSession's SpawnLine event (SpawnLineEventArgs.Pattern), written onto WorkspaceWindow.CapturePattern, and persisted in the saved workspace - and the only two reads anywhere were the header's own gate and the header itself. SpawnRoute existed for exactly this consumer, so TriggerResult's SpawnTargets goes back to being the list of resolved window names it was before the header needed the rule's identity; the capture-group expansion that made those names dynamic stays, because that is a separate feature. A config written with `capturePattern` still loads: the field is simply not mapped any more and System.Text.Json ignores what it does not know. The `spawn` snapshot view stays. It activates the Chat window, and two suites drive it for claims that have nothing to do with the header - a spawn tab being closable, and the timestamp gutter reaching a window whose history is markup and nothing else. Core 782, Tui 1335 (the three tests removed were the header's own). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GpL7Ht6sLBsSEtVNsYcXMM
"I mentioned before how I wanted Alt-1-9 etc to be able to switch between characters. I realize I may have used the wrong term. I want it to be able to go between tabs? Panes? Whichever it is that allows me to switch not just characters, but captures, etc." The thing that answers all of those at once is the *window*: a character's own window, a capture window, the web view. It was the pane, and a pane is a container - a capture sharing a pane with its character's main window had no number of its own and was reachable only while it happened to be that pane's active tab, which is precisely the half of the request the pane chord could not serve. The ordering. Windows get the same mechanism panes got this morning rather than a new one: WorkspaceWindow.Sequence, a per-workspace counter assigned at creation, persisted in WorkspaceWindowState, never reused, seeded from the saved order for a config written before the field. The number is the *index* in Workspace.PlacedWindows, so it compacts on a close and stays contiguous. Explicitly not the registry's own order: Workspace.Windows is a dictionary's values, whose enumeration is unspecified after a removal - the next insert reuses the freed slot and everything after it renumbers. Only *placed* windows are numbered, because a window no pane holds is drawn "closed" and a digit for it would name nowhere to go. The sidebar. Its second column was the hosting pane, on both window and character rows, and would have been wrong on both. It is now the chord that goes to the row it is drawn on: a window row carries its own window's Alt+N, a character row carries the chord of that character's own window (main if they have one, else any of theirs that is numbered), so the digit printed beside Bob reaches Bob. It appears once there are two windows rather than two panes, which on the demo scene is immediately - that is the point, since the capture is what the chord was asked to reach. Cost is three cells on the widest rail row (measured: 19 -> 22 columns, clamp is 44). RailRow.Pane is renamed to RailRow.Chord throughout, because a field named Pane holding a window chord is the two-spellings bug living in the source. The pane jump is kept, on Ctrl+B N. Every pane is reachable through Alt+N via whatever window it holds, so this is not the only route - but the pane *numbering* does not go away with the chord: move mode badges each pane with its digit, the move and drag prompts say "pane 2", the Ctrl+P entry says "Go to pane 2", and Ctrl+O counts in it. A numbering the client prints and asks you to press inside a mode, with no key outside that mode that acts on it, only half exists. It is also the one motion that reaches a pane without naming what is in it - the ordinal member of the Ctrl+O / Ctrl+arrow family. Ctrl+B is where the rest of the pane keymap already lives and the digits were the one part of it nothing claimed, so it costs no key; the which-key panel lists it, and Ctrl+B 1 on a single-pane workspace now refuses like the panel says it will. Ctrl+O is unchanged. It counts Layout.Panes because that is still what every pane surface says; what changed is which chord it has to agree with, and the doc comment that named Alt+N now names Ctrl+B N. Move mode stays pane-based - a window is what is being moved, not a destination - and three things keep the two numberings apart: they are never live at once (a mode, consuming bare digits, behind dimmed panes), they are spelt differently everywhere either is written (pane N versus Alt+N), and they are drawn in different places (a badge on the pane versus a column in the sidebar). Out of range still reports rather than falling through to the framework's own Alt+digit window selector; all nine digits stay claimed as application shortcuts, and Alt+0 stays free. Two suites rewritten to pin the new claim, not weakened: WindowJumpTests (the Alt+N claim, on a fixture whose capture window sits *second* in creation order so the window digits are not the pane digits wearing a new name) and PaneJumpTests (Ctrl+B N). PaneNumberingRailTests is folded into the first. WindowNumberingTests is new in Core - and its "a new window takes the last number" case needed a four-window fixture, because with fewer the dictionary and the sequence agree and the test proves nothing. Core 792, Graphics 83, Scripting 42, Web 37, Tui 1341. Build warning-free. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GpL7Ht6sLBsSEtVNsYcXMM
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 32 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (12)
WalkthroughThe change separates window and pane keyboard navigation, adds persisted creation-order window numbering, updates rail and command labels, and removes capture-pattern metadata and headers from spawn windows. ChangesNavigation and spawn behavior
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Keyboard
participant SharpMUTermApp
participant Workspace
participant RailRenderer
Keyboard->>SharpMUTermApp: Press window or pane shortcut
SharpMUTermApp->>Workspace: Resolve ordered target
Workspace-->>SharpMUTermApp: Return window or pane
SharpMUTermApp->>RailRenderer: Update navigation labels
RailRenderer-->>Keyboard: Render updated rail
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/SharpMUTerm.Core/Commands/CommandIds.cs`:
- Around line 45-52: Update the PaneJumpDigits constant to derive its value from
WindowJumpDigits instead of using the independent literal 9, preserving the
documented invariant that both counts always match.
In `@tests/SharpMUTerm.Tui.Tests/PaneJumpTests.cs`:
- Around line 311-313: Correct the summary comment in PaneJumpTests to match the
test assertions: describe two presses of ⌃O from pane 1 and compare the result
with Prefix(three.App, 3), avoiding any reference to a nonexistent fourth pane
or third press.
In `@tests/SharpMUTerm.Tui.Tests/WindowJumpTests.cs`:
- Around line 647-726: The ANSI helpers are duplicated across both jump suites
and should have one shared implementation. In
tests/SharpMUTerm.Tui.Tests/WindowJumpTests.cs lines 647-726, move Visible, Sgr,
Backgrounds, and CellsPaintedIn into a shared internal static test helper,
preserving the FocusIndicationTests cross-reference comment there, and update
the suite to call it. In tests/SharpMUTerm.Tui.Tests/PaneJumpTests.cs lines
457-467, remove the duplicate members and use the shared helper instead.
- Around line 305-324: Extract the duplicated single-window setup from
ASingleWindowWorkspaceNamesNoChordOnAnyRow and
OnOneWindowTheRefusalSaysHowToOpenAnother into a private static OneWindowApp
helper beside the other test harness members. Preserve all existing setup
statements, including removing spawn windows and tabs, creating the headless
app, and calling RenderSnapshot; update both tests to use the helper.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: e4485e7e-5377-46d1-95d3-a3cd537f6b29
📒 Files selected for processing (30)
CLAUDE.mddocs/design/README.mdsrc/SharpMUTerm.Core/Automation/TriggerEngine.cssrc/SharpMUTerm.Core/Commands/CommandCatalog.cssrc/SharpMUTerm.Core/Commands/CommandIds.cssrc/SharpMUTerm.Core/Session/SessionEvents.cssrc/SharpMUTerm.Core/Session/WorldSession.cssrc/SharpMUTerm.Core/Workspace/LayoutNode.cssrc/SharpMUTerm.Core/Workspace/Workspace.cssrc/SharpMUTerm.Core/Workspace/WorkspaceLayout.cssrc/SharpMUTerm.Core/Workspace/WorkspaceState.cssrc/SharpMUTerm.Core/Workspace/WorkspaceWindow.cssrc/SharpMUTerm.Core/Workspaces/RailModel.cssrc/SharpMUTerm.Tui/CaptureLineRenderer.cssrc/SharpMUTerm.Tui/DemoScene.cssrc/SharpMUTerm.Tui/MacroKeys.cssrc/SharpMUTerm.Tui/PrefixPanel.cssrc/SharpMUTerm.Tui/Program.cssrc/SharpMUTerm.Tui/RailRenderer.cssrc/SharpMUTerm.Tui/SharpMUTermApp.cstests/SharpMUTerm.Core.Tests/Automation/TriggerEngineTests.cstests/SharpMUTerm.Core.Tests/Commands/CommandCatalogTests.cstests/SharpMUTerm.Core.Tests/Workspace/RailModelTests.cstests/SharpMUTerm.Core.Tests/Workspace/WindowNumberingTests.cstests/SharpMUTerm.Core.Tests/Workspace/WorkspaceStateTests.cstests/SharpMUTerm.Tui.Tests/CaptureLineRendererTests.cstests/SharpMUTerm.Tui.Tests/PaneJumpTests.cstests/SharpMUTerm.Tui.Tests/PaneNumberingRailTests.cstests/SharpMUTerm.Tui.Tests/RailWindowRowTests.cstests/SharpMUTerm.Tui.Tests/WindowJumpTests.cs
💤 Files with no reviewable changes (5)
- src/SharpMUTerm.Tui/CaptureLineRenderer.cs
- tests/SharpMUTerm.Core.Tests/Workspace/WorkspaceStateTests.cs
- tests/SharpMUTerm.Tui.Tests/CaptureLineRendererTests.cs
- src/SharpMUTerm.Tui/DemoScene.cs
- tests/SharpMUTerm.Tui.Tests/PaneNumberingRailTests.cs
"I am looking for the characters to have different numbers? Am I not communicating something right here? Like, I would expect Riko's next window to be #5 for instance. But perhaps I should attack this a different way. Let's assume we keep this way. Pane 1-2-3-4- etc under the same connection. Let's create a different mechanic to easily be able to switch characters then!" - and then, choosing between the two: "Let's take option B for cycling characters & panes separately." Global window numbering failed the first day it was used. With three characters sharing pane 1 as tabs, every character's row read Alt-1, because their windows happened to be numbered from one run; and nine digits do not stretch over everybody's windows - six across three characters already crowds them. Alt+1-9 now selects a window of the *active character*, numbered from 1 within that character. Alt-1 is your own window whoever you are, Alt-2 your first capture. An unowned window (the web view) is in every character's list, so it wears a different digit under each - which is exactly the set the rail draws window rows for, so the sidebar and the chord are one list read twice. Characters are reached by Alt+J and Alt+K, forward and back. Letters and not a third digit row because there is no third digit-bearing modifier this terminal delivers: read off a pty, kitty writes Alt+Shift+1 as CSI 49;4u and Ctrl+Shift+N as CSI 110;6u - kitty-keyboard-protocol sequences the parser does not decode and drops - while Alt+j and Alt+k are a plain ESC j / ESC k. The cycle walks only the characters already open, because SwitchToCharacter *creates* a session and a window for one that is not, and a cycle key that did that per press would dial through a configuration by accident. The sidebar follows. A character row carries Alt-J or Alt-K when it is one of the two neighbours and nothing otherwise - including the row you are on, whose marker already says so. It used to carry the chord of that character's own window, which under a scoped numbering is Alt-1 against everybody. The chord now leads each row instead of trailing it. "There is still way too much room after a window's name before it hits 'alt-1'" - that gap was the reserved pen and unread fields, which cannot be removed (a cell that costs only when it has something to say resizes the sidebar, and the sidebar's width comes out of the panes, which every server is told over NAWS). So the chord moved to the front, against the name it names, and the badges ended up at the right edge. Measured: the demo rail is 22 columns before and after, and the gap is 5 cells before and 0 after. The field is reserved per row *kind*, because with fewer than two characters open no character row can hold a cycle chord and reserving across both spent three cells on every character row of the commonest client there is. Alt where Alt is safer, measured rather than preferred: - Disconnect moves from Ctrl+D to Alt+D, so it and Alt+R read as one pair. Ctrl+D is released outright rather than kept as an alias - a second key for one action is either a secret or a duplicate row on every surface that lists chords - and releasing it hands a clean Ctrl chord back for macros. Nothing takes it: HandleMoveInput is gated on IsMovable (false here) and acts only on the arrows and X, and InputBarControl's Ctrl table has no D. Pinned by a test that presses it and checks the connection and the window count. - Ctrl+Tab is removed, and it was never real. A terminal writes 0x09 for it, which *is* Tab, so the parser reports ConsoleKey.Tab with no Control bit and the claim could never have matched - while Claimed is consulted first, so F4 told users a chord was taken that cannot arrive. The byte is in ControlBytes now, so Verdict says so. - Deliberately left on Ctrl: Ctrl+R (readline's history search), Ctrl+P, Ctrl+Q (safe here - TerminalRawMode clears IXON, so it is not XON), Ctrl+B, Ctrl+O, Ctrl+N/W/F and the command line's A/E/K/U/L. A sweep that moved everything would be as wrong as one that moved nothing. - Flagged, not fixed: Ctrl+N and Ctrl+O have no reverse, and Ctrl+W and Ctrl+B x are two chords for one action. Both are behaviour changes rather than modifier moves. Two fixes found on the way. BuildCharacterRefs passed `_active?.SessionKey == key` as *Connected*, which is a different question - and since the catalog skips the focused character, every "Switch to ..." entry read `offline` however many worlds were live. And main's TabExpansionTests asserted two constants against their own literals, which TUnit's analyzer refuses; they are behavioural now. New `characters` snapshot view: two characters genuinely open, the one state the Alt-J/Alt-K column can be seen in and the one thing `connections` cannot fake. Core 813, Graphics 83, Scripting 42, Web 37, Tui 1355. Warning-free. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GpL7Ht6sLBsSEtVNsYcXMM
|
@copilot please fix the merge conflicts in this pull request. |
Resolved. There was one conflict in |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
tests/SharpMUTerm.Core.Tests/Workspace/WindowNumberingTests.cs (1)
226-239: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAttach each XML documentation comment to its intended declaration.
Move the first
<summary>block directly aboveOrder.Directory.Build.propsdisables documentation generation and warning-as-error handling, so this is a documentation defect rather than a CI warning risk.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/SharpMUTerm.Core.Tests/Workspace/WindowNumberingTests.cs` around lines 226 - 239, Move the detailed XML summary describing numbered window ordering so it is directly attached to the Order method, while keeping the Owner constant’s summary directly above Owner. Ensure each declaration has exactly its intended documentation comment and preserve the existing Order implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/SharpMUTerm.Tui/Program.cs`:
- Around line 331-333: Update the usage text in Program.cs to identify the two
chords using the sidebar’s exact ⌥J and ⌥K spellings, matching the strings
produced by CharacterChords and rendered by RailRenderer.ChordField; preserve
the surrounding navigation explanation.
In `@src/SharpMUTerm.Tui/SharpMUTermApp.cs`:
- Around line 998-1012: Guard the `characters` branch in the view-handling code
before the final `SwitchToCharacter` call: return or skip the character-specific
rendering when `_config.Worlds` is empty or its first world has no characters.
Preserve the existing loop and pane rebuild behavior for configurations
containing at least one world with a character, and eliminate the unguarded
indexing in `SwitchToCharacter`.
---
Outside diff comments:
In `@tests/SharpMUTerm.Core.Tests/Workspace/WindowNumberingTests.cs`:
- Around line 226-239: Move the detailed XML summary describing numbered window
ordering so it is directly attached to the Order method, while keeping the Owner
constant’s summary directly above Owner. Ensure each declaration has exactly its
intended documentation comment and preserve the existing Order implementation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: dae62c21-a6f2-4adc-b8e5-7360f3907930
📒 Files selected for processing (18)
CLAUDE.mdsrc/SharpMUTerm.Core/Commands/CommandCatalog.cssrc/SharpMUTerm.Core/Commands/CommandIds.cssrc/SharpMUTerm.Core/Session/WorldSession.cssrc/SharpMUTerm.Core/Workspace/Workspace.cssrc/SharpMUTerm.Core/Workspace/WorkspaceLayout.cssrc/SharpMUTerm.Core/Workspace/WorkspaceWindow.cssrc/SharpMUTerm.Tui/MacroKeys.cssrc/SharpMUTerm.Tui/Program.cssrc/SharpMUTerm.Tui/RailRenderer.cssrc/SharpMUTerm.Tui/SharpMUTermApp.cstests/SharpMUTerm.Core.Tests/Commands/CommandCatalogTests.cstests/SharpMUTerm.Core.Tests/Workspace/WindowNumberingTests.cstests/SharpMUTerm.Tui.Tests/ConnectionKeysTests.cstests/SharpMUTerm.Tui.Tests/MacroKeyCaptureTests.cstests/SharpMUTerm.Tui.Tests/RailChordColumnTests.cstests/SharpMUTerm.Tui.Tests/RailWindowRowTests.cstests/SharpMUTerm.Tui.Tests/WindowJumpTests.cs
# Conflicts: # tests/SharpMUTerm.Core.Tests/Text/TabExpansionTests.cs
Six comments, all accepted; two of them turned out to be narrower or
wider than reported and are noted below.
1. The `characters` snapshot view could throw. The loop that opens the two
sessions guarded `world.Characters`, and the line after it - which poses
the frame - indexed `_config.Worlds[0].Characters[0]` with nothing. A
snapshot without `--demo-config` renders whatever configuration is on the
machine, so an empty world list, or a first world nobody has put a
character in, threw out of RenderSnapshot. The keys are now gathered
through the same guard that opens them and the frame is posed from that
list, so there is no second, unguarded way to name the first character.
The review asked whether a sibling had the same shape. None does - `quit`
uses `ElementAtOrDefault(1) is { Characters.Count: > 0 }`, the MSSP screen
range-checks, and ActiveWorldIndex is a bounded loop - but "a guard on one
indexer and not its neighbour" is how this arrives, so the new suite walks
*every* view against two configurations: no worlds at all, and a world
with no characters. 88 cases. Re-introducing the bare indexer fails
exactly two of them with ArgumentOutOfRangeException.
2. --help quoted the sidebar and got it wrong. The page named 'Alt+J' and
'Alt+K' while the sidebar draws ⌥J and ⌥K, so a reader searching for the
quoted text found nothing. Narrower than reported, though: the page's
ASCII prose register is long-standing and deliberate - it is printed
before the TUI starts, into whatever is on the other end of stdout, and
its only ⌥/⌃ characters are in comments. What was broken is the four
*quotes*, all four of which I had added, and all four of which claim to
reproduce the screen. Those are verbatim now and the prose is untouched; a
test pins the rule and fails when the quote is put back.
3. PaneJumpDigits is derived from WindowJumpDigits rather than an independent
literal beside a comment claiming they match.
4. PaneJumpTests' cycle prose described three presses of ⌃O landing where
⌃B 4 does; the test pressed twice, compared against ⌃B 3, and ran on a
fixture with three panes. Taking the review's own advice: the prose
described the property worth pinning and the test was the weak half, so
the test now drives every step of the cycle including the wrap, and the
sentence says so.
5. Two single-window fixtures were the same five statements - three, in fact;
RailWindowRowTests had a copy as well. One DemoConfigs.SingleWindow().
6. Visible, Sgr, Backgrounds and CellsPaintedIn were copied verbatim into
PaneJumpTests, WindowJumpTests and FocusIndicationTests. They are on
FrameGrid now, which is where a "read what was painted" helper belongs and
already decodes the same escapes for characters. The settings-screen
suites keep their own older Visible copies - different suites, different
review.
Also merged main (b4eadf3). TabExpansionTests conflicted: main had fixed the
same analyzer warning I had, independently and under the same test name.
Took main's.
Core 815, Graphics 83, Scripting 42, Web 37, Tui 1444. Warning-free.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GpL7Ht6sLBsSEtVNsYcXMM
…at/tab-jump-alt-digit
#14 landed first, so this carries its rail rewrite, its window numbering and the removal of CapturePattern. The merge was textually clean and semantically not: SpawnWindowId takes an owner now, so #14's new tests called it with one argument, and its migration fixture asserted on a field #14 deleted. The review item on BothPanesAreStillCalledPublic was right that a frame-wide DoesNotContain is the wrong shape - it can pass for reasons unrelated to its claim - though not for the stated reason: the constant is the session key 'Convergence.Bob', not 'Bob', so the collections it warned about would not have matched it. Scoping the assertion found what the frame-wide one had been missing. A spawn tab reads 'Ann - Public (1)': owner label, name, unread count, in markup. So the test now reads that tab specifically, through StripMarkup, and pins the distinction that actually matters - the character's name is on the strip deliberately, so two characters capturing one target can be told apart, and the session key must never be. It still fails on the shared id: 4 of 5, including this one.
Two changes, one commit each.
1. The capture header is gone from spawn panes
"Do not show the capture line for capture panels." A spawn window drew a dim
⇱ capture ^\[Chat\]row between its tab strip and its output. It is gone, and so is everything that existed only to put it there.Verified unused before removing: the pattern was minted in
TriggerEngine(SpawnRoute), carried throughWorldSession'sSpawnLineevent (SpawnLineEventArgs.Pattern), written ontoWorkspaceWindow.CapturePattern, and persisted in the saved workspace — and the only two reads anywhere were the header's own gate and the header itself.SpawnRoutewas added for exactly this consumer, soTriggerResult.SpawnTargetsgoes back to being the list of resolved window names it was before; the capture-group expansion that made those names dynamic is a separate feature and stays.A config written with
capturePatternstill loads — the field is simply not mapped andSystem.Text.Jsonignores what it does not know.The
spawnsnapshot view stays: two suites drive it for claims that have nothing to do with the header.2. ⌥1–⌥9 select a window; the numbered pane jump moves to ⌃B N
The thing that answers all of those at once is the window — a character's own window, a capture window, the web view. It was the pane, and a pane is a container: a capture sharing a pane with its character's main window had no number of its own and was reachable only while it happened to be that pane's active tab. That is precisely the half of the request the pane chord could not serve.
The ordering
Windows get the same mechanism panes got, not a new one:
WorkspaceWindow.Sequence, a per-workspace counter assigned at creation, persisted inWorkspaceWindowState, never reused, seeded from the saved order for a config written before the field. The number is the index inWorkspace.PlacedWindows, so it compacts on a close and stays contiguous.Deliberately not the registry's own order:
Workspace.Windowsis a dictionary's values, and its enumeration is unspecified after a removal — the next insert reuses the freed slot and everything after it renumbers. Only placed windows are numbered; one no pane holds is drawnclosed, and a digit for it would name nowhere to go.What the sidebar shows
The rail's second column was the hosting pane, on both window and character rows, and would have been wrong on both. It is now the chord that goes to the row it is drawn on:
A character row carries the chord of that character's own window (their main window; else any of theirs that is numbered), so the digit printed beside Bob reaches Bob — window rows are drawn for the active character only, so without this a background character's digit is invisible from anywhere you could use it.
The column appears once there are two windows rather than two panes, which on the demo scene is immediately — that is the point, since the capture is what the chord was asked to reach. Measured cost: the widest rail row goes 19 → 22 columns (clamp is 44).
RailRow.Pane→RailRow.Chordthroughout, because a field namedPaneholding a window chord is the two-spellings bug living in the source.What happened to the pane jump
Kept, on ⌃B 1–⌃B 9. Every pane is reachable through ⌥N via whatever window it holds, so this is not the only route there — but the pane numbering does not go away with the chord. Move mode badges each pane with its digit, the move and drag prompts say
pane 2, the ⌃P entry saysGo to pane 2, and ⌃O counts in it. A numbering the client prints, and asks you to press inside a mode, with no key outside that mode that acts on it, is a numbering that only half exists. It is also the one motion that reaches a pane without naming what is in it — the ordinal member of the ⌃O / ⌃arrow family.⌃B is where the rest of the pane keymap already lives and the digits were the one part of it nothing claimed, so it costs no key. The which-key panel lists it, and ⌃B 1 on a single-pane workspace now refuses out loud, because the panel dims that row.
⌃O is unchanged. It counts
Layout.Panesbecause that is still what every pane surface says; what changed is which chord it has to agree with, and the doc comment that named ⌥N now names ⌃B N.Move mode
Stays pane-based — a window is what is being moved, not a destination. Three things keep the two numberings from reading as one: they are never live at the same time (a mode, consuming bare digits, behind dimmed panes), they are spelt differently everywhere either is written down (
pane Nvs⌥N), and they are drawn in different places (a badge on the pane vs a column in the sidebar).Out of range and the ⌃P entries
All nine digits stay claimed as application shortcuts, in range or not, so none falls through to
InputCoordinator.HandleAltInput. ⌥7 with four windows reportsthere is no window 7 — this workspace has 4. ⌥0 stays free. The ⌃PGo to <window>entries lead with their chord (⌥2 · Alfa.Ann); theGo to pane Nentries now say⌃B N.Tests
WindowJumpTestsandPaneJumpTestsrewritten to pin the new claim;PaneNumberingRailTestsfolded into the first;WindowNumberingTestsnew in Core.The ⌥N fixture puts the capture window second in creation order on purpose, so the window digits are not the pane digits wearing a new name — a suite where the two coincided would pass against either.
Two falsifications run:
RailChordLabelshifted by one → 5 failures, includingPressingTheDigitTheRailPrintsAgainstACharacterGoesToThatCharacter: "Expected to be equal toCara.Cal, because the rail said Cal was ⌥5". That test reads the digit off the rendered sidebar and presses it, which is the invariant.PlacedWindowsordered by the registry instead of the sequence →AWindowOpenedIntoAClosedOnesSlotStillTakesTheLastNumberfails withmain,spawn:Guild,spawn:Trade,spawn:Newbie. That case needed a four-window fixture; with fewer, the dictionary and the sequence agree and the test proves nothing. (TUnit'sIsEquivalentToalso compares collections as sets — the ordering assertions go through a joined string, the wayPaneNumberingTestsalready does.)Suites: Core 792 · Graphics 83 · Scripting 42 · Web 37 · Tui 1341, build warning-free.
UserDirectoryGuardgreen.🤖 Generated with Claude Code
https://claude.ai/code/session_01GpL7Ht6sLBsSEtVNsYcXMM
Summary by CodeRabbit
New Features
Alt+1–Alt+9shortcuts for switching between numbered windows.Ctrl+Bfollowed by1–9shortcuts for switching between panes.Alt+J/Alt+Kcharacter cycling andAlt+Dfor disconnecting.Bug Fixes