Fix private-marker stripping that misroutes CSI commands - #62
Conversation
Agent-Logs-Url: https://github.com/tomlm/XTerm.NET/sessions/b4d549a6-26ff-497d-b648-c9bbe4852c49 Co-authored-by: tomlm <17789481+tomlm@users.noreply.github.com>
Perf comparison3 run(s) of each side, alternating on one machine. Allocation is a count and is gated exactly. Time is a measurement, so its gate is derived from the spread this job just observed in itself rather than fixed in advance.
Each corpus is gated at assemblies measured
|
|
@claude can you take over this issue/PR? It's now yours |
|
Claude finished @tomlm's task in 11m 15s —— View job Took over the PR — finished and pushedPushed
What the bug actually was
The branch as I found it fixed the lookup but only carried the one already-known symptom (XTSMGRAPHICS) into a test. The remaining six were live and untested. What I changed on top of the existing work
What I could not verify
To re-enable local validation, add |
ToCsiCommand trimmed a leading '?' or '>' and looked up what was left, which made every DEC private sequence an alias for whichever non-private command shared its final character. For h, l, n and $p that is right -- the two forms are the same handler with a flag. For everything else the final character is a coincidence, and the alias ran the wrong command on input ordinary programs emit at startup: XTMODKEYS arrived as SGR and underlined the screen, the Kitty keyboard protocol restored the cursor, XTSAVE clobbered the saved cursor, XTRESTORE reset the scroll region and homed, XTVERSION changed the cursor shape, and XTSMTITLE minimised the window. XTSMGRAPHICS had already been patched in the dispatcher with an isPrivate check inside the SCROLL UP case. That fixed the symptom anyone had noticed and left the other six, which is the argument for fixing the lookup rather than the case: the aliasing is the defect, and it makes a new one for every final character the two namespaces share. The map is now keyed on the whole identifier the parser built, private entries listed explicitly: ?J, ?K, ?S, ?h, ?l, ?n, >c and ?$p. ?c is gone -- CSI ? c is not a sequence, and answering it as a secondary DA was an artefact of the stripping. XTSMGRAPHICS gets its own CsiCommand.GraphicsAttributes rather than borrowing ScrollUp, so the dispatcher no longer re-decides which command it is holding. Anything else private falls out as Unknown and is ignored, which is what an unimplemented sequence should do. Co-authored-by: Tom Laird-McConnell <17789481+tomlm@users.noreply.github.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@claude I have enabled dotnet tool and when you are done, change title to remove [WIP] and remove Draft status |
|
Claude finished @tomlm's task in 2m 51s —— View job Verified green — but I still can't run
|
|
@claude I fixed permissions for gh |
|
Claude finished @tomlm's task in 1m 15s —— View job Done — retitled, undrafted, and verified locally this time
Both tools work now, so the caveats from my last two comments are gone. Local build + test ( This is the first run I've done in-job rather than reading CI's result. It matches what CI reported on One warning came out of the build, and it is not from this PR:
PR state now: {"number":62,"title":"Fix private-marker stripping that misroutes CSI commands","isDraft":false,"state":"OPEN"}No code changes were needed this round, so nothing was pushed — the branch is still at It's out of draft and ready for your review. |
|
@claude fix conflicts |
|
Claude finished @tomlm's task in 10m 54s —— View job Conflicts resolved and pushed — one command left for you
What conflicted
Everything else from Tests the merge invalidated
Verified locally, not read off CI: (1507, up from 1265 — the 242 new ones are #48's Kitty and left/right-margin tests, now running on this branch.) The one warning, The one thing left, and why
The tree is already correct. I checked it directly: Nothing of git fetch origin
git checkout copilot/fix-private-marker-misroute
git merge -s ours --no-edit origin/main
git push origin copilot/fix-private-marker-misroute
Until that lands, Files changed will show #48's Kitty work as part of this PR. That's the same stale merge base, and it clears up the moment the parent is recorded. To stop this recurring: |
…I lookup main gained the Kitty keyboard protocol (PR #48) while this branch was open, and the two changes touch the same three files. The overlap is not only textual: - main added "=u", "?u", ">u" and "<u" to _commandMap and made ToCsiCommand try an exact match before falling back to stripping the private marker. This branch removes the stripping entirely, so the exact match is the only lookup. The four Kitty identifiers are carried over and grouped with "u", where the rest of the private entries live. - CsiCommand keeps main's four Kitty members alongside this branch's GraphicsAttributes. - InputHandler keeps main's Kitty and left/right-margin work; the isPrivate branch inside "case ScrollUp:" is still replaced by a real "case GraphicsAttributes:". Tests follow the same split. CsiCommandExtensionsTests now pins "?u" and ">u" to the Kitty commands rather than asserting they are Unknown -- the misroute they used to prove (both landed on RESTORE CURSOR) is now proven by them reaching the Kitty handler instead. "<u" moves out of the "never recognised" theory for the same reason, and "<c", "=S" and "<m" take its place. The dispatch test that checks no Kitty sequence moves the cursor still holds and still means what it said. dotnet test src/XTerm.NET.slnx -> 1507 passed, 0 failed. Co-authored-by: Tom Laird-McConnell <17789481+tomlm@users.noreply.github.com>
|
@claude fix merge |
|
Claude encountered an error after 2m 18s —— View job Recording the merge parent
|
|
@claude please fix the merge confclit |
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
main gained XTVERSION, the real device attributes reply, DECRQSS and XTGETTCAP while this branch was open. XTVERSION is the one that collides: "CSI > Ps q" reached ReportVersion only because ToCsiCommand stripped the '>' and matched "q", which is exactly the stripping this branch removes. Taking either side alone would have dropped a feature -- keeping main's lookup keeps the misrouting this branch exists to fix, dropping it silently unanswers every version query -- so ">q" is now listed in the command map in its own right and the SelectCursorStyle case tells the two apart by marker as main already does. Two of this branch's tests were written before those changes landed: - Secondary_device_attributes_still_answers pinned the old placeholder reply "CSI > 0 ; 10 ; 0 c". main now answers with the terminal's real attributes. The test cares that ">c" still reaches the DA handler once the marker is not stripped, not what the handler says, so it now asserts the shape of the reply and leaves the contents to InputHandlerTests. - ">q" was listed as an unmapped private identifier that should resolve to Unknown. It is implemented now, so it moves to the mapped theory. Comments in InputHandler that explained the routing in terms of the stripped identifier are updated to describe the exact match that replaced it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015yLt7uosRs4EnvTpR3MSRD
tomlm
left a comment
There was a problem hiding this comment.
Review: exact-identifier CSI lookup
Reviewed at 9ecd342 (after the merge of main landed mid-review — the earlier head fa60fa6 was ~40 commits behind and I have re-checked everything against the current one).
The core change is right, and it is structural rather than a patch of the three known cases. ToCsiCommand is now a single GetValueOrDefault against a map keyed on the whole identifier the parser built. There is no fallback, so an unlisted private form resolves to Unknown by construction — the next CSI ? X cannot alias onto X because there is no code path that could make it. That is what #49 asked for.
Findings
Medium
CommandExtensions.cs:71—{ "q", SelectCursorStyle }is the one entry left that is keyed on the final character alone, and it collides with DECLL. Verified:CSI 0 qflips the cursor from steady to blinking. Same defect class as the PR's subject, on the intermediate-byte axis instead of the marker axis, and it contradicts the summary this PR added two lines above the map.FIXES.md:731— "What is mapped now" omits>q, andFIXES.md:742claims the dispatcher no longer re-decides which command it holds, whichInputHandler.cs:880contradicts.InputHandler.cs:876— the comment (and the switch's implicit default arm) guards againstCSI ? Ps qreaching the DECSCUSR case. After this change it cannot;?qis not in the map.
Low
CsiCommand.cs:119— the reworded doc callsCSI c/CSI > cthe replies. They are the requests.CsiCommandExtensionsTests.cs:65— the "stays Unknown" theory is a hand-written list, not derived from the map.
Notes — what I checked that is fine
- Enumerated the whole map, not spot-checked. Every private form
maincurrently reaches viaTrimStartwas compared against the new exact table. The eight legitimate shared-handler forms are all listed (?JDECSED,?KDECSEL,?hDECSET,?lDECRST,?nDEC DSR,?$pprivate DECRQM,>cDA2, plus?Snow split off to its own command). Everything else that changes is a form that had no business resolving at all:?s,?r,>m,>n,>t,>T,?c,=c, and the?<letter>combinations for@ A B C D E F G H I L M P T X Z b d f g. None of those is a sequence this terminal implements. No legitimate private form is lost. - Conflict with #48: none remaining. #48's exact-match-first is subsumed — the exact match is now the only lookup — and the four Kitty identifiers are carried over verbatim.
- Hot path / allocation: measured, not reasoned about. 1.3M
ToCsiCommandcalls over a mix of 13 plain and marked identifiers allocate 0 bytes on this branch against 45.6 MB onmain(mainstill runsTrimStartfor every marked identifier that misses the exact map —?h,?l,>c,?$p,>q,?S,>t,?rall miss today). This is strictly better than #48, not a reintroduction.new(StringComparer.Ordinal)keeps the non-randomised string fast path. - The
>qentry is load-bearing and covered: I removed it against the merged tree andVersionReportTestsgoes red on three tests. XTVERSION survives the merge. dotnet test src/XTerm.NET.slnx→ 1597 passed, 0 failed at9ecd342.- The dispatch tests are real, not vacuous: each one fails if the map entry is reverted (
>4;2msets underline,>0qsetsCursorBlink,>2traisesWindowMinimized,?1049rhomes the cursor).XtSmTitle_...andXtsmgraphics_...also assert the unmarked twin still works, which is the half that is easy to forget. IsPrivateModestill returns false for<and=while its summary says "represents a DEC private mode sequence" — the map now uses both markers. Harmless today (<u/=uroute to commands that never consult the flag) and pre-existing, but the summary overpromises.PrivateMarkeris the one to reach for.- Moving
GraphicsAttributesup in theCsiCommandenum renumbers eleven members. Checked: nothing castsCsiCommandto or from an integer, and it is not persisted. Safe.
The map had { "q", SelectCursorStyle } alongside { " q", SelectCursorStyle }.
The bare final character is not DECSCUSR -- DECSCUSR is "CSI Ps SP q" and
carries the SP intermediate, which the parser collects, so it arrives as " q".
Bare "q" is DECLL (Load LEDs), which this terminal does not implement.
Traced on this tree before the change:
ESC [ 2 SP q -> (CursorStyle.Block, CursorBlink=False)
ESC [ 0 q -> (CursorStyle.Block, CursorBlink=True)
An application clearing its LEDs on startup got a blinking cursor it never
asked for -- the same user-visible failure as "CSI > Ps q" reshaping the
cursor, on the intermediate-byte axis rather than the private-marker axis,
and the last row in the table where one final character stood in for two
different sequences. It also contradicted the invariant the summary above
the map states: the key is the WHOLE identifier the parser built,
intermediate as well as marker.
Removing it, rather than keeping it and documenting the alias, is what the
evidence supports: nothing depended on it. Every DECSCUSR test in the tree
(InputHandlerTests, VersionReportTests.Decscusr_still_works, the DECRQSS
cursor style theory, PrivateCsiDispatchTests) already writes the SP form,
and the full suite is green with the entry gone. A comment in its place
records why the row is absent so the next audit does not add it back. If
DECLL is wanted later it gets its own CsiCommand, the same way XTSMGRAPHICS
just got GraphicsAttributes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015yLt7uosRs4EnvTpR3MSRD
The last paragraph justified reading the marker rather than isPrivate with "a CSI ? Ps q is neither of these sequences", which was the argument when ToCsiCommand stripped the marker: "?q" became "q", resolved to SelectCursorStyle and arrived here, and the switch was the only thing between it and the wrong handler. With the exact map "?q" is not a key, so it resolves to Unknown and is dropped in HandleCsi before this case is entered. Enumerating what can reach the case now: the map lists " q" and ">q", whose markers are '\0' and '>'. Both arms are covered and the implicit default is unreachable. As written the next reader concludes CSI ? Ps q is routed here and handled, which is not what happens. The switch stays -- it is cheap and it is what makes a marked form added to the map later fall out silently instead of reshaping the cursor -- but the comment now says the map is what keeps '?' out and the switch is defence in depth. No behaviour change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015yLt7uosRs4EnvTpR3MSRD
The reworded summary said "CSI c for the primary reply, CSI > c for the secondary". Those are what an application sends to ask. The replies are CSI ? 62 ; 4 ; 22 c and CSI > 1 ; 200 ; 0 c, which DeviceAttributes(string, Params) is the code that sends. The previous wording was incomplete rather than wrong; this makes it right. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015yLt7uosRs4EnvTpR3MSRD
"What is mapped now" left >q out of the list of explicitly mapped private identifiers, while the Summary says everything not listed falls out as Unknown and is ignored. >q is in the map, resolves to SelectCursorStyle, and InputHandler splits it back out to XTVERSION -- so a reader following this document concluded CSI > Ps q is ignored when it is answered, on the one row the table above says this change exists to fix. It is now listed, and called out as the one deliberate shared entry. "the dispatcher no longer needs to re-decide which command it is holding" was true of XTSMGRAPHICS and false as the general claim it read as, since >q re-decides on identifier.PrivateMarker() in this same branch. The sentence now says what the difference is: an isPrivate flag cannot tell '?' from '>', while the marker is the same byte the map matched on. Also stale or missing: - "left the other six" predates main fixing XTVERSION (#63) and the ?c/=c device attributes case (#64) in the dispatcher. Three of the seven had been patched one at a time, which is a better version of the same point. - The table omitted CSI > Pm T (XTRESTTITLE), which this change also stops running as SCROLL DOWN. - The mapped list said nothing about the bare "q" now being absent, which is the intermediate-byte half of the same exactness rule. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015yLt7uosRs4EnvTpR3MSRD

Uh oh!
There was an error while loading. Please reload this page.