Skip to content

fix(triggers): route to a window that already exists, and keep a highlight through a rewrite - #28

Merged
HarryCordewener merged 3 commits into
feat/window-activity-boundaryfrom
fix/trigger-routing-and-highlights
Aug 11, 2026
Merged

fix(triggers): route to a window that already exists, and keep a highlight through a rewrite#28
HarryCordewener merged 3 commits into
feat/window-activity-boundaryfrom
fix/trigger-routing-and-highlights

Conversation

@HarryCordewener

@HarryCordewener HarryCordewener commented Aug 11, 2026

Copy link
Copy Markdown
Member

Two reported defects. They are independent, and each is a single wrong line
in a different file.

1. "Triggers should not be restricted to only be able to go to spawns."

Root cause. Workspace.RouteSpawn (Workspace.cs:172-184 on main) was the
only destination resolver a matched rule had — SharpMUTermApp.OnSpawnLine
(SharpMUTermApp.cs:2905-2931) is its one caller. It computes
SpawnWindowId(sessionKey, target) and, when nothing answers to that id,
registers a brand-new WindowKind.Spawn window. There is no branch in it that
can reach a window that already exists under any other name, so "route this
into the window I already have open" was not a thing a rule could ask for
however it was spelt: a route naming a window on the screen opened a second
one beside it wearing the same label, and the one the user was looking at
stayed empty.

The screen could not express it either. SharpMUTermApp.SpawnTargets()
(SharpMUTermApp.cs:4942-4948) built the F2 route suggestion list out of the
other rules' SpawnTargets and nothing else, so the one place a user reads
what a route may say could name only spawn panes.

One arm of the report was already there and is worth stating precisely: main
in the route list (TriggersScreenRenderer.cs:44-47) is a real choice meaning
SpawnTarget = null, which does reach the main window — but only for a line
the rule does not also gag, because it is not routing rather than routing
there. A gagging rule aimed at the main window deleted the line.

Change. Workspace.RouteLine is the resolver now: a window the target
already names wins, and creating a capture pane is what happens when nothing
does. Workspace.FindRouteTarget is the same lookup with no side effects, so
the shell can still tell "this line opened a pane" from "this line went to one
that was already there" without routing twice.

What a target may reach is deliberately narrower than "any window with that
title", and the scoping is the design rather than caution:

  • this session's own windows and the windows nobody owns — always;
  • another character's main window — one alt's channel collected into the
    pane you actually read. A main window is admitted across the owner boundary
    because it is a window the user opened by connecting, rather than one a
    capture rule conjured;
  • never another session's spawn or auxiliary window. Two characters running
    one capture rule get a pane each, and a bare title lookup would collapse them
    back into one and file the second character's channel under the first — the
    exact defect SpawnWindowId was given an owner to fix. SpawnWindowPerSessionTests
    and TwoCharactersCapturingOneNameStillGetAPaneEach hold both ends of that.
  • only a placed window is a destination. Appending to a window no pane holds
    writes into a buffer nothing can draw, which from the reader's side is
    indistinguishable from the rule not firing at all.
  • finding never creates. A target is often a template with capture groups in
    it (Channel $1), so the name can be the server's text; the property that
    bounds that is that this arm can only ever land in a window the user already
    has. Making one out of a captured name still goes through RouteSpawn, which
    puts the matching session's own key on it.

Two consequences in the shell. OwnerLabel is stamped on this session's own
capture panes only — it prefixes a tab as Owner: Name, and writing our name
onto a window somebody else owns would rename their pane after whoever last
routed a line into it. And RouteTargets() (the old SpawnTargets()) now
offers the workspace's own windows as well as the other rules' targets, so the
route field can name them; they are suggestions and not the permitted set, so
this widens what is discoverable rather than what is legal.

2. "Highlight colours don't seem to actually work."

Root cause. TriggerEngine.Process (TriggerEngine.cs:256-267 on main)
applied the highlight to the matched region and then, four lines later,
replaced the whole line with StyledLine.FromText(text, TextStyle.Default)
no colour, no attributes, no left rule. A rule that both rewrote and
highlighted therefore threw its own highlight away on the very next statement.

Highlights work on their own; TriggerEngineTests.Highlight_RecoloursMatchedRegion
has always passed, and an end-to-end check of a highlight-only rule shows the
colour reaching the pane's markup. The combination is what fails, and it is not
an exotic one — it is what a channel rule looks like (route it, tidy it to
» $1, colour it), and it is the shape of this repository's own demo headline
rule (DemoScene.cs:161-174: teal, bold, Rewrite = "» $1"). The F2 screen
badges such a rule H and paints both swatches, so the client promised a
highlight it then discarded, and the only way to find one was to discover that
deleting the rewrite brought it back.

Change. The rewrite runs first, and the highlight then covers the whole of
what it produced. It cannot cover the match's own offsets, because after a
rewrite those address a string that no longer exists; the rewritten text is the
rule's product in its entirety, so colouring all of it is the only reading that
means anything. Without a rewrite nothing moves — the highlight covers the
match and only the match, as it always has, which is the property the
reordering could most easily have broken and is pinned separately.

A later rule's rewrite still replaces an earlier rule's highlighted text, and
that is correct rather than the same bug one rule over: those characters are
gone. Also pinned, so the ordering fix is not later "generalised" into
re-colouring text the first rule never saw.

On the snapshot. --view highlight is the F2 colour-picker screen, not
painted output, so it cannot show this defect — its frame is byte-identical
before and after the fix, which is itself the evidence that the screen was
never where the colour was lost. The defect is in the output path, and it is
proven where the colour actually has to survive: ARewrittenLineReachesThePaneWearingItsHighlight
reads the Spectre markup a pane is fed and asserts #ffd700 is in it.
--view route-edit does move, and correctly: the route field's suggestion list
grows from four entries to five as the workspace's own window joins it.

Tests

Written before the fixes, and each verified to fail on the unfixed behaviour —
3 of the 6 end-to-end tests fail without them (two routing, one highlight); the
other three are regression pins that pass either way by design.

  • HighlightRewriteTests (Core) — 7 tests over the rewrite/highlight ordering,
    including the two properties that must not move.
  • RouteToExistingWindowTests (Core) — 10 tests over RouteLine, half of them
    about what it must not reach.
  • TriggerRouteDestinationTests (Tui) — 6 end-to-end tests over a live
    connection with two characters, asserting the pane the line actually lands in
    and the markup it lands there wearing.

Verification

dotnet build SharpMUTerm.slnx     Build succeeded. 0 Warning(s) 0 Error(s)

Core        total: 900   failed: 0  succeeded: 900
Tui         total: 1607  failed: 0  succeeded: 1607
Graphics    total: 83    failed: 0  succeeded: 83
Scripting   total: 42    failed: 0  succeeded: 42
Web         total: 37    failed: 0  succeeded: 37

🤖 Generated with Claude Code

https://claude.ai/code/session_015nuKnWthnELNkrd86q5KWN

Summary by CodeRabbit

  • New Features

    • Trigger-routed lines can now appear in existing matching windows, with new capture windows created only when needed.
    • Routing respects session ownership and preserves existing window labels.
    • Trigger suggestions now include configured destinations and currently available windows.
    • Rewritten trigger text retains highlighting and applies styling across the rewritten line.
  • Bug Fixes

    • Improved unread indicators for routed background windows.
    • Closed or inaccessible windows are safely bypassed.
  • Tests

    • Added coverage for routing, window isolation, fallback creation, rewriting, and highlighting.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The change adds existing-window trigger routing with session and ownership precedence. It also applies highlighting after rewrites across the complete rewritten text. Trigger configuration now suggests realised workspace destinations.

Changes

Trigger rewrite and routing

Layer / File(s) Summary
Rewrite-before-highlight processing
src/SharpMUTerm.Core/Automation/Trigger.cs, src/SharpMUTerm.Core/Automation/TriggerEngine.cs, tests/SharpMUTerm.Core.Tests/Automation/HighlightRewriteTests.cs
Rewrites now occur before highlighting. Rewritten text receives full-length rule styling, while unchanged matches retain match-only highlighting.
Workspace destination resolution
src/SharpMUTerm.Core/Workspace/Workspace.cs, tests/SharpMUTerm.Core.Tests/Workspace/RouteToExistingWindowTests.cs
RouteLine and FindRouteTarget route lines to eligible existing windows or create session-specific spawn windows.
Trigger output route integration
src/SharpMUTerm.Tui/SharpMUTermApp.cs, tests/SharpMUTerm.Tui.Tests/TriggerRouteDestinationTests.cs
Trigger output uses workspace routing, preserves ownership labels, and includes realised windows in route suggestions.
Route target UI contract
src/SharpMUTerm.Tui/TriggersScreenRenderer.cs, src/SharpMUTerm.Tui/TriggersScreenView.cs, tests/SharpMUTerm.Tui.Tests/TriggersScreenRendererTests.cs
The routing collection parameter is renamed from spawnTargets to routeTargets across view, renderer, and tests.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 67.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes both main changes: routing triggers to existing windows and preserving highlights through rewrites.
✨ Finishing Touches 💡 1
📝 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: 1

🤖 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/Workspace/Workspace.cs`:
- Around line 205-232: Guard the renamed-window fallback in FindRouteTarget by
returning the SpawnWindowId(sessionKey, target) result only when
Layout.FindWindow(renamed.Id) is not null; otherwise return null. Add a
regression test covering an unplaced or removed renamed spawn window and verify
RouteLine does not route to its invisible buffer.
🪄 Autofix

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: 8f6b601f-3643-4de1-b440-ff8e760c3b62

📥 Commits

Reviewing files that changed from the base of the PR and between e8b42e8 and d865978.

📒 Files selected for processing (10)
  • src/SharpMUTerm.Core/Automation/Trigger.cs
  • src/SharpMUTerm.Core/Automation/TriggerEngine.cs
  • src/SharpMUTerm.Core/Workspace/Workspace.cs
  • src/SharpMUTerm.Tui/SharpMUTermApp.cs
  • src/SharpMUTerm.Tui/TriggersScreenRenderer.cs
  • src/SharpMUTerm.Tui/TriggersScreenView.cs
  • tests/SharpMUTerm.Core.Tests/Automation/HighlightRewriteTests.cs
  • tests/SharpMUTerm.Core.Tests/Workspace/RouteToExistingWindowTests.cs
  • tests/SharpMUTerm.Tui.Tests/TriggerRouteDestinationTests.cs
  • tests/SharpMUTerm.Tui.Tests/TriggersScreenRendererTests.cs

Comment thread src/SharpMUTerm.Core/Workspace/Workspace.cs
@HarryCordewener
HarryCordewener force-pushed the fix/trigger-routing-and-highlights branch from d865978 to 7addac7 Compare August 11, 2026 20:50
@HarryCordewener
HarryCordewener changed the base branch from main to feat/pane-search August 11, 2026 20:50
HarryCordewener and others added 3 commits August 11, 2026 16:01
The reported defect was "highlight colours don't seem to actually work".
They work alone — `Highlight_RecoloursMatchedRegion` has always passed —
and they were destroyed by the rule's *own* rewrite. `Process` applied the
highlight to the matched region and then, four lines later, replaced the
whole line with `StyledLine.FromText(text, TextStyle.Default)`: no colour,
no attributes, no left rule.

That combination is not exotic, it is what a channel rule looks like —
route it, tidy it to `» $1`, colour it — and it is the shape of the demo
configuration's own headline rule. The F2 screen badged such a rule `H` and
painted both swatches, so the client promised a highlight it then threw
away, and the only way to find one was to discover that deleting the
rewrite brought it back.

The rewrite now runs first and the highlight covers the whole of what it
produced. It cannot cover the match's own offsets, because after a rewrite
those address a string that no longer exists; the rewritten text is the
rule's product in its entirety, so colouring all of it is the only reading
that means anything. Without a rewrite nothing moves: the highlight covers
the match and only the match, as it always has.

A *later* rule's rewrite still replaces an earlier rule's highlighted text,
and that is correct rather than the same bug one rule over — those
characters are gone. Pinned, so the ordering fix is not later generalised
into re-colouring text the first rule never saw.

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

"Triggers should not be restricted to only be able to go to spawns."
They were, and the restriction was one method deep: `Workspace.RouteSpawn`
was the only destination resolver a matched rule had, and it computes
`SpawnWindowId(sessionKey, target)` and registers a new `WindowKind.Spawn`
window when nothing answers to that id. There is no branch in it that can
reach a window that already exists under any other name, so a route naming
a window on the screen opened a second one beside it wearing the same
label, and the one the user was looking at stayed empty.

`Workspace.RouteLine` is the resolver now: a window the target already
names wins, and creating a capture pane is what happens when nothing does.
`FindRouteTarget` is the same lookup without the side effects, so the shell
can still tell "this line opened a pane" from "this line went to one that
was already there" without routing twice.

What a target may reach is deliberately narrower than "any window with that
title": this session's own windows, the windows nobody owns, and another
character's *main* window — one alt's channel collected into the pane you
actually read. It is not another session's spawn or auxiliary window, because
two characters running one capture rule get a pane each and a bare title
lookup would collapse them back into one, which is the exact defect
`SpawnWindowId` was given an owner to fix. Only a placed window is a
destination: appending to a window no pane holds is indistinguishable from
the rule not firing. And finding never creates, which is the property that
bounds a capture-derived target — that arm can only ever land in a window
the user already has.

Two consequences in the shell. `OwnerLabel` is stamped on this session's own
capture panes only; writing our name onto a window somebody else owns would
rename their pane after whoever last routed a line into it. And the F2
`route` field now suggests the workspace's own windows as well as the other
rules' targets — while the list was the rules' targets alone, the one place
a user reads what a route may say could not name any window they had open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nuKnWthnELNkrd86q5KWN
Review catch. The title lookup already required a *placed* window, and the
renamed-spawn fallback beside it did not — so a spawn window whose pane the user
closed could be returned as a destination and the channel written into a buffer
nothing draws. That is reachable rather than theoretical: the registry outlives
the layout, and a restored workspace registers windows a saved layout no longer
places. It also contradicted RouteLine's own doc comment, which says a closed
window is passed over and the line goes somewhere visible.

Both halves, because guarding only the fallback moves the same defect one call
deeper: RouteSpawn returned a registered-but-unplaced window untouched, since it
only placed one it had just created. It now places on the way past whether or not
the window is new — which is what lets FindRouteTarget decline a closed window and
fall through, reopening the pane under the same id with its history in it.

The regression test was checked against the unfixed code and fails there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nuKnWthnELNkrd86q5KWN
@HarryCordewener
HarryCordewener force-pushed the fix/trigger-routing-and-highlights branch from 2bbe154 to ee02d8d Compare August 11, 2026 21:01
Base automatically changed from feat/pane-search to feat/window-activity-boundary August 11, 2026 23:21
@HarryCordewener
HarryCordewener merged commit d9c5815 into feat/window-activity-boundary Aug 11, 2026
3 checks passed
@HarryCordewener
HarryCordewener deleted the fix/trigger-routing-and-highlights branch August 11, 2026 23:22
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