Skip to content

Alt+1-9 selects a window, not a pane; and the capture header goes - #14

Merged
HarryCordewener merged 9 commits into
mainfrom
feat/tab-jump-alt-digit
Jul 31, 2026
Merged

Alt+1-9 selects a window, not a pane; and the capture header goes#14
HarryCordewener merged 9 commits into
mainfrom
feat/tab-jump-alt-digit

Conversation

@HarryCordewener

@HarryCordewener HarryCordewener commented Jul 31, 2026

Copy link
Copy Markdown
Member

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 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 was added for exactly this consumer, so TriggerResult.SpawnTargets goes 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 capturePattern still loads — the field is simply not mapped and System.Text.Json ignores what it does not know.

The spawn snapshot 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

"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. 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 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.

Deliberately not the registry's own order: Workspace.Windows is 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 drawn closed, 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:

  ▸ ● Corvid  2 ⌥1      ← character row: the chord to Corvid's own window
      ▪ main ✎    ⌥1    ← window rows: each window's own chord
      ▪ Chat    2 ⌥2

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.PaneRailRow.Chord throughout, because a field named Pane holding 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 says Go 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.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 ⌥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 N vs ⌥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 reports there is no window 7 — this workspace has 4. ⌥0 stays free. The ⌃P Go to <window> entries lead with their chord (⌥2 · Alfa.Ann); the Go to pane N entries now say ⌃B N.

Tests

WindowJumpTests and PaneJumpTests rewritten to pin the new claim; PaneNumberingRailTests folded into the first; WindowNumberingTests new 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:

  • RailChordLabel shifted by one → 5 failures, including PressingTheDigitTheRailPrintsAgainstACharacterGoesToThatCharacter: "Expected to be equal to Cara.Cal, because the rail said Cal was ⌥5". That test reads the digit off the rendered sidebar and presses it, which is the invariant.
  • PlacedWindows ordered by the registry instead of the sequence → AWindowOpenedIntoAClosedOnesSlotStillTakesTheLastNumber fails with main,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's IsEquivalentTo also compares collections as sets — the ordering assertions go through a joined string, the way PaneNumberingTests already does.)

Suites: Core 792 · Graphics 83 · Scripting 42 · Web 37 · Tui 1341, build warning-free. UserDirectoryGuard green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GpL7Ht6sLBsSEtVNsYcXMM

Summary by CodeRabbit

  • New Features

    • Added Alt+1Alt+9 shortcuts for switching between numbered windows.
    • Added Ctrl+B followed by 19 shortcuts for switching between panes.
    • Added Alt+J/Alt+K character cycling and Alt+D for disconnecting.
    • Updated rail labels, command menus, help text, and move overlays to distinguish windows from panes.
    • Window numbering follows creation order, compacts after closures, and persists across workspace restoration.
  • Bug Fixes

    • Spawn windows now display standard output without capture-pattern headers.

HarryCordewener and others added 2 commits July 30, 2026 21:32
"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
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 32 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5e56834d-eee7-4842-bac5-a7344ba27765

📥 Commits

Reviewing files that changed from the base of the PR and between 63f13af and a9da5e8.

📒 Files selected for processing (12)
  • CLAUDE.md
  • src/SharpMUTerm.Core/Commands/CommandIds.cs
  • src/SharpMUTerm.Tui/Program.cs
  • src/SharpMUTerm.Tui/SharpMUTermApp.cs
  • tests/SharpMUTerm.Tui.Tests/DemoConfigs.cs
  • tests/SharpMUTerm.Tui.Tests/FocusIndicationTests.cs
  • tests/SharpMUTerm.Tui.Tests/FrameGrid.cs
  • tests/SharpMUTerm.Tui.Tests/PaneJumpTests.cs
  • tests/SharpMUTerm.Tui.Tests/RailChordColumnTests.cs
  • tests/SharpMUTerm.Tui.Tests/RailWindowRowTests.cs
  • tests/SharpMUTerm.Tui.Tests/SnapshotViewEmptyConfigTests.cs
  • tests/SharpMUTerm.Tui.Tests/WindowJumpTests.cs

Walkthrough

The 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.

Changes

Navigation and spawn behavior

Layer / File(s) Summary
Window numbering and persistence
src/SharpMUTerm.Core/Workspace/*, src/SharpMUTerm.Core/Commands/CommandIds.cs, tests/SharpMUTerm.Core.Tests/Workspace/WindowNumberingTests.cs
Windows receive creation sequences and expose ordered placed windows. Workspace capture and restore persist these sequences.
Window and pane navigation controls
src/SharpMUTerm.Core/Commands/CommandCatalog.cs, src/SharpMUTerm.Tui/MacroKeys.cs, src/SharpMUTerm.Tui/SharpMUTermApp.cs, src/SharpMUTerm.Tui/RailRenderer.cs, src/SharpMUTerm.Core/Workspaces/RailModel.cs
Alt+1Alt+9 target windows. Ctrl+B 1Ctrl+B 9 target panes. Rail rows and command entries display the matching chords.
Spawn routing and rendering
src/SharpMUTerm.Core/Automation/TriggerEngine.cs, src/SharpMUTerm.Core/Session/*, src/SharpMUTerm.Tui/SharpMUTermApp.cs, docs/design/README.md
Spawn targets and events no longer carry capture patterns. Spawn windows use standard output rendering.
Regression coverage
tests/SharpMUTerm.Core.Tests/*, tests/SharpMUTerm.Tui.Tests/*
Tests cover numbering, persistence, navigation, rail labels, command surfaces, zoom behavior, terminal input, and spawn target handling.

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
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes both primary changes: window selection moves from panes, and the capture header is removed.
Docstring Coverage ✅ Passed Docstring coverage is 85.89% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between e06cb58 and 78d59a7.

📒 Files selected for processing (30)
  • CLAUDE.md
  • docs/design/README.md
  • src/SharpMUTerm.Core/Automation/TriggerEngine.cs
  • src/SharpMUTerm.Core/Commands/CommandCatalog.cs
  • src/SharpMUTerm.Core/Commands/CommandIds.cs
  • src/SharpMUTerm.Core/Session/SessionEvents.cs
  • src/SharpMUTerm.Core/Session/WorldSession.cs
  • src/SharpMUTerm.Core/Workspace/LayoutNode.cs
  • src/SharpMUTerm.Core/Workspace/Workspace.cs
  • src/SharpMUTerm.Core/Workspace/WorkspaceLayout.cs
  • src/SharpMUTerm.Core/Workspace/WorkspaceState.cs
  • src/SharpMUTerm.Core/Workspace/WorkspaceWindow.cs
  • src/SharpMUTerm.Core/Workspaces/RailModel.cs
  • src/SharpMUTerm.Tui/CaptureLineRenderer.cs
  • src/SharpMUTerm.Tui/DemoScene.cs
  • src/SharpMUTerm.Tui/MacroKeys.cs
  • src/SharpMUTerm.Tui/PrefixPanel.cs
  • src/SharpMUTerm.Tui/Program.cs
  • src/SharpMUTerm.Tui/RailRenderer.cs
  • src/SharpMUTerm.Tui/SharpMUTermApp.cs
  • tests/SharpMUTerm.Core.Tests/Automation/TriggerEngineTests.cs
  • tests/SharpMUTerm.Core.Tests/Commands/CommandCatalogTests.cs
  • tests/SharpMUTerm.Core.Tests/Workspace/RailModelTests.cs
  • tests/SharpMUTerm.Core.Tests/Workspace/WindowNumberingTests.cs
  • tests/SharpMUTerm.Core.Tests/Workspace/WorkspaceStateTests.cs
  • tests/SharpMUTerm.Tui.Tests/CaptureLineRendererTests.cs
  • tests/SharpMUTerm.Tui.Tests/PaneJumpTests.cs
  • tests/SharpMUTerm.Tui.Tests/PaneNumberingRailTests.cs
  • tests/SharpMUTerm.Tui.Tests/RailWindowRowTests.cs
  • tests/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

Comment thread src/SharpMUTerm.Core/Commands/CommandIds.cs Outdated
Comment thread tests/SharpMUTerm.Tui.Tests/PaneJumpTests.cs Outdated
Comment thread tests/SharpMUTerm.Tui.Tests/WindowJumpTests.cs
Comment thread tests/SharpMUTerm.Tui.Tests/WindowJumpTests.cs Outdated
HarryCordewener and others added 2 commits July 30, 2026 22:26
"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
@HarryCordewener

Copy link
Copy Markdown
Member Author

@copilot please fix the merge conflicts in this pull request.

Copilot AI commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

@copilot please fix the merge conflicts in this pull request.

Resolved. There was one conflict in tests/SharpMUTerm.Core.Tests/Text/TabExpansionTests.cs — the TheDefaultIsFourAndTheCeilingIsSixteen test had diverged between the feature branch (using ExpandTabs to demonstrate the values) and main (testing via TextSettings property access directly to satisfy TUnitAssertions0005). Took the main version. Build is warning-free and all 815 Core tests pass.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Attach each XML documentation comment to its intended declaration.

Move the first <summary> block directly above Order. Directory.Build.props disables 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

📥 Commits

Reviewing files that changed from the base of the PR and between 78d59a7 and 63f13af.

📒 Files selected for processing (18)
  • CLAUDE.md
  • src/SharpMUTerm.Core/Commands/CommandCatalog.cs
  • src/SharpMUTerm.Core/Commands/CommandIds.cs
  • src/SharpMUTerm.Core/Session/WorldSession.cs
  • src/SharpMUTerm.Core/Workspace/Workspace.cs
  • src/SharpMUTerm.Core/Workspace/WorkspaceLayout.cs
  • src/SharpMUTerm.Core/Workspace/WorkspaceWindow.cs
  • src/SharpMUTerm.Tui/MacroKeys.cs
  • src/SharpMUTerm.Tui/Program.cs
  • src/SharpMUTerm.Tui/RailRenderer.cs
  • src/SharpMUTerm.Tui/SharpMUTermApp.cs
  • tests/SharpMUTerm.Core.Tests/Commands/CommandCatalogTests.cs
  • tests/SharpMUTerm.Core.Tests/Workspace/WindowNumberingTests.cs
  • tests/SharpMUTerm.Tui.Tests/ConnectionKeysTests.cs
  • tests/SharpMUTerm.Tui.Tests/MacroKeyCaptureTests.cs
  • tests/SharpMUTerm.Tui.Tests/RailChordColumnTests.cs
  • tests/SharpMUTerm.Tui.Tests/RailWindowRowTests.cs
  • tests/SharpMUTerm.Tui.Tests/WindowJumpTests.cs

Comment thread src/SharpMUTerm.Tui/Program.cs
Comment thread src/SharpMUTerm.Tui/SharpMUTermApp.cs
HarryCordewener and others added 3 commits July 31, 2026 10:26
# 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
@HarryCordewener
HarryCordewener merged commit 9d9f8d2 into main Jul 31, 2026
3 checks passed
@HarryCordewener
HarryCordewener deleted the feat/tab-jump-alt-digit branch July 31, 2026 16:23
HarryCordewener added a commit that referenced this pull request Jul 31, 2026
#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.
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