Skip to content

feat(search): ⌃F searches the output across the panes - #29

Merged
HarryCordewener merged 6 commits into
feat/window-activity-boundaryfrom
feat/pane-search
Aug 11, 2026
Merged

feat(search): ⌃F searches the output across the panes#29
HarryCordewener merged 6 commits into
feat/window-activity-boundaryfrom
feat/pane-search

Conversation

@HarryCordewener

Copy link
Copy Markdown
Member

Third of a three-PR stack — based on feat/window-activity-boundary (#27), which is based on
feat/find-chords (#26). Review those first.
Design: docs/superpowers/specs/2026-08-11-pane-search-and-activity-design.md, part 2.

⌃F opens a modal results surface over the panes. Typing filters, ⌥E reads the query as a pattern,
⌥A widens from the focused window to every window, goes to a hit and marks it with a bar, and
⌥G walks to the next one.

What is searched, and what is said about it

The pane buffer — not WorldSession.Scrollback, not the file-backed spill. RestoreLog's
reasoning one layer over: a spawn window's lines reach neither, so a session-keyed search would find
nothing in exactly the windows people search hardest. The bound is stated rather than implied
(12 found · 4,812 lines held), so a reader who cannot find an old line can see why.

Matching runs over the visible text, held per line and computed once at append. Over the markup, a
world changing colour mid-word would split a match — the same defect a wrapped URL has, one layer down
— and #ff0000 would find every red line. Chrome rows carry no plain text, so a search cannot find its
own bars.

Case is ignored in both modes (HistorySearch already is; (?-i) is the way back, which is why there
is no third toggle). An invalid pattern is a state, because a regex is unparseable most of the time
it is being typed, and there is a match timeout, because this runs on the UI thread per keystroke.

Landing

goes through Activate — the one activation path — so a hit in a background pane brings the pane,
the tab and the session forward together rather than scrolling something nobody is looking at. The bar
is the fourth of the boundary bars and points rather than repaints: the line is worth having because it
is the game's own text in the game's own colours.

Three things a reviewer might otherwise ask about

  • No backward chord, and that was measured. ⌥⇧G decodes fine, but kitty writes it as
    CSI 103;4u — a kitty-keyboard-protocol sequence AnsiInputParser.DispatchCsi drops. ⌥⇧1's story
    one letter over. A decode test is not an arrival test.
  • Escape does not clear the bar. A claimed Escape does not set _escapeAt, and TryAltEnter pairs
    an unclaimed one with a following Enter to make ⌥⏎. Binding it here would break the newline chord
    for as long as a bar was on screen.
  • ⌥G removes the bar before re-running the search. The bar is itself a row, so a search run around
    it returns indices in a buffer about to lose one, and every hit below it lands a row early — which is
    what the test caught.

Two kinds of client chrome now live in the line buffers, so InsertChromeRow/RemoveChromeRow are the
one place that fixes up everything indexing into one. The demo scene also loads with _watching off:
it pours a spawn window's whole history in before the first frame, and every line otherwise counted as
missed.

Verification

dotnet build -c Release SharpMUTerm.slnx clean and warning-free; five suites green (2,722 tests).
Four new snapshot views rendered and looked at: search, search-regex, search-all,
search-landed (over a split, so the pane is narrower than the terminal — the geometry that catches a
landing scrolled to the wrong row).

🤖 Generated with Claude Code

https://claude.ai/code/session_015nuKnWthnELNkrd86q5KWN

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 808e25f8-bd6c-4c9b-acca-582bff2fc1cb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

HarryCordewener and others added 6 commits August 11, 2026 15:59
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nuKnWthnELNkrd86q5KWN
Plain text in, matching lines out: nothing here knows about panes, markup or
windows, which is what keeps it in Core and what makes a match mean what it
looks like. A colour tag mid-word must not split a match, and nobody should be
able to search for #ff0000 and find every red line.

Five decisions, each pinned: case ignored in both modes (HistorySearch already
is, and (?-i) is the way back, which is why there is no third toggle); one
match per line, the first, with offsets so a row can show why it is listed; an
empty query matches nothing, unlike history, because a pane buffer is thousands
of lines and "everything, oldest first" is the pane you are already looking at;
an invalid pattern is a state, because a regex is unparseable most of the time
it is being typed; and a match timeout, because this runs on the UI thread on
every keystroke over every line of every window.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nuKnWthnELNkrd86q5KWN
MarkupText.Plain is what a markup line actually puts on the screen, and PaneLine
holds it beside the markup, computed once at append. On demand would restrip
every line of every window on every keystroke of a query, and an incremental
surface is the whole point.

Plain rather than markup because a match has to mean what it looks like: a world
may change colour mid-word, and matching the markup would find neither half —
the same defect a URL split by a colour change has, one layer down — while a
query for #ff0000 must not find every red line.

Chrome rows carry no plain text, so a search cannot find its own boundary bars.

Plain and VisibleLength are pinned against each other over a table of inputs:
they share a protect-then-strip shape, and a divergence would put a match's
offsets in a different coordinate system from the width renderers measure with.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nuKnWthnELNkrd86q5KWN
…t says

The HistorySearchPrompt/HistorySurface split verbatim: the rules and the
wording are exactly the part a headless test can pin. ⌥E and ⌥A both re-search
from the top, because each changes what the list is and a pointer kept at row 12
of a different result set points at nothing the reader chose.

The header states the bound it searched — '12 found · 4,812 lines held' — because
⌃F sees the pane buffer and not a session's whole history, and a reader who
cannot find an old line should be able to see why rather than concluding the
search is broken. The window column is drawn only when every window is searched;
with one window it would be the same word on every row.

The footer names only keys that work, pinned by a test that presses every one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nuKnWthnELNkrd86q5KWN
The chord, the scope and the landing. ⏎ activates the window the line is really
in — through Activate, the one activation path, so the pane, the tab and the
session move together rather than a pane being scrolled behind the reader's
back — and marks the line with a bar, fourth of the boundary bars, which points
rather than repaints: the line is worth having because it is the game's own text
in the game's own colours.

Two things measured rather than chosen. ⌥⇧G is *not* bound: kitty writes it as
CSI 103;4u, a kitty-keyboard-protocol sequence AnsiInputParser drops, so it
could never arrive — ⌥⇧1's story one letter over. And Escape does not clear the
bar: a claimed Escape does not set _escapeAt, and TryAltEnter pairs an unclaimed
one with a following Enter to make Alt+⏎, so binding it would break the newline
chord for as long as a bar was on screen.

⌥G removes the bar *before* re-running the search. The bar is itself a row, so a
search run around it returns indices in a buffer about to lose one and every hit
below it lands a row early — which is what the test caught.

Two kinds of inserted chrome now, so the index bookkeeping every buffer mark
depends on is one pair of methods (InsertChromeRow/RemoveChromeRow) rather than
written out at each site: the freeze point, the pending boundary, the activity
bar and the search bar all move together or none of them do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nuKnWthnELNkrd86q5KWN
Four views, because four things about ⌃F are only visible in a frame: a plain
query with its hits marked, the same query read as a pattern (the header is the
only place either state is said — compose/compose-literal's reasoning), the
widened scope where the window column appears, and what ⏎ leaves behind. The
last is over a split, so the pane is narrower than the terminal: that is the
geometry that catches a landing scrolled to the wrong row.

The demo scene now loads with _watching off. It pours a spawn window's whole
history in before the first frame, so every line counted as missed and any frame
that later made such a window visible carried an activity bar reporting the
client's own setup as news — which is exactly what the first cut of the landed
frame showed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nuKnWthnELNkrd86q5KWN
@HarryCordewener
HarryCordewener force-pushed the feat/window-activity-boundary branch from 037ae39 to 7659a87 Compare August 11, 2026 21:00
@HarryCordewener
HarryCordewener merged commit 62142b1 into feat/window-activity-boundary Aug 11, 2026
3 checks passed
@HarryCordewener
HarryCordewener deleted the feat/pane-search branch August 11, 2026 23:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant