Handle OSC 133 shell integration and the OSC 9 ConEmu extensions - #20
Conversation
Both codes reached the unknown-sequence branch and were discarded.
OSC 133 (FinalTerm/FTCS) is how a shell says where its prompt ends, where a
command starts, and how that command exited. Terminal exposes the last mark and
the last exit code, plus an event per mark.
ShellIntegrationState is NULLABLE and starts null. Integration has to be
configured in the shell, so a shell without it is indistinguishable from one at a
prompt; defaulting to PromptStart would assert the shell is idle on no evidence.
Null means "cannot say".
The exit code is nullable for the same reason. OSC 133;D carries one only if the
shell can supply it, and cmd.exe cannot read the previous command's status from
its prompt, so a bare D is normal rather than malformed. Defaulting to 0 would
make every cmd.exe command report success. A later bare D also CLEARS a previous
code rather than leaving it stale.
OSC 9 is three unrelated features multiplexed on the first parameter, not on the
code, so dispatch happens there:
9 ; 9 ; path working directory, the ConEmu convention. Microsoft's own
documented Windows prompts emit this rather than OSC 7, so
reading only 7 loses the cwd on Windows entirely. The path
is bare rather than a file:// URI, and pwsh sends it quoted.
9 ; 4 ; state ; % progress, which Windows Terminal renders on the taskbar.
9 ; text desktop notification.
Falling through to the notification case is only correct once the sub-parameters
are claimed first: otherwise every progress tick raises a toast reading "4;1;50".
There is a test for exactly that.
Progress values are clamped rather than rejected -- a sender that overshoots still
means "as far as it goes" -- and zeroed for None and Indeterminate, which carry no
percentage and would otherwise leave a bar sitting at a stale position while
claiming the extent is unknown.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟢 Approval recommended
The implementation matches the described behavior and includes thorough test coverage, with only a minor misleading comment wording to adjust.
Pull request overview
Adds first-class handling for OSC 133 (FinalTerm/FTCS shell integration marks) and OSC 9 (ConEmu extensions) so these sequences update terminal state and raise dedicated events instead of falling through to the unknown-sequence branch.
Changes:
- Add OSC 133 handling to track shell integration marks and last command exit code, plus a
ShellIntegrationMarkReceivedevent. - Add OSC 9 ConEmu handling for working directory (
9;9), progress (9;4), and desktop notifications, with progress/notification events and state stored onTerminal. - Add new enums (
ShellIntegrationMark,ProgressState) and comprehensive tests covering the new behavior.
File summaries
| File | Description |
|---|---|
| src/XTerm.NET/Terminal.cs | Adds terminal state and events for OSC 133 shell integration and OSC 9 progress/notification. |
| src/XTerm.NET/InputHandler.cs | Dispatches and parses OSC 9 (ConEmu) and OSC 133 sequences and updates Terminal accordingly. |
| src/XTerm.NET/Events/TerminalEvents.cs | Introduces event-args types for shell integration, progress, and notifications. |
| src/XTerm.NET/Common/ShellIntegrationMark.cs | Defines the OSC 133 mark enum. |
| src/XTerm.NET/Common/ProgressState.cs | Defines the OSC 9;4 progress state enum. |
| src/XTerm.NET/Common/OscCommand.cs | Adds enum entries/docs for OSC 9 (ConEmu) and OSC 133 (shell integration). |
| src/XTerm.NET.Tests/ShellIntegrationTests.cs | Adds tests covering OSC 133 marks/exit codes and OSC 9 cwd/progress/notification behavior. |
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // The sub-parameter decides everything, so an unrecognised one must not be treated as a | ||
| // notification -- OSC 9;4;... would then pop a toast reading "4;1;50" on every progress tick. |
|
ah shit, this looks like it's stacked again. |
Dang it - it's not supposed to be, there are just conflicting commits. I'll fix it up. |
|
oh shit, I broke it in my merge. Argh. I'm fixing. |
|
give me a min :) Also, I have async for port.pty coming too LOL - I'm on a tear. |
BUILD WAS BROKEN. The merge of main into this branch dropped the closing brace of NotificationEventArgs and the opening line of the next doc comment, so every class after it nested inside it and the file would not compile. tomlm#19 and tomlm#20 both inserted at the same anchor in TerminalEvents.cs, which is what the merge had to reconcile. COMMENT DESCRIBED BEHAVIOUR THE CODE DOES NOT HAVE. It said an unrecognised sub-parameter "must not be treated as a notification", and the code does exactly that on purpose -- the notification form is OSC 9 ; text, with no sub-parameter at all, so it can only be the fallback. The real requirement is about ORDER: claimed sub-commands have to be matched first, or OSC 9;4;1;50 pops a toast reading "4;1;50" on every progress tick. Reworded to say that, and to say that an unclaimed sub-parameter being a notification is the intended reading of a permissive extension space. A CLAIMED SUB-COMMAND WITH NO PAYLOAD is now ignored rather than shown. "OSC 9;9" carries a sub-command and nothing else; the fallback turned it into a notification whose entire body was "9". A TEST FROM tomlm#19 WENT STALE, CORRECTLY. It asserted OSC 133 was unrecognised, which was true when the passthrough event was added and is not any more -- this branch is what implements it. That is the Recognized contract working rather than a test rotting: a listener filling the gap stops doing so once a code lands in HandleOsc. Moved to OSC 1337, which really is unimplemented, and added the opposite assertion for 133 so both halves are pinned. 698 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both codes currently reach the unknown-sequence branch and are discarded.
OSC 133 — shell integration (FinalTerm/FTCS)
How a shell reports where its prompt ends, where a command starts, and how it exited.
TerminalgainsShellIntegrationState,LastCommandExitCode, and aShellIntegrationMarkReceivedevent.ShellIntegrationStateis nullable and starts null. Integration has to be configured in the shell, so a shell without it is indistinguishable from one sitting at a prompt — defaulting toPromptStartwould assert the shell is idle on no evidence at all. Null means "cannot say".The exit code is nullable for the same reason.
cmd.execannot read the previous command's status from its prompt, so it always emits a bareD; that is normal, not malformed. Defaulting to0would make everycmd.execommand report success. A later bareDalso clears a previous code rather than leaving it stale.OSC 9 — ConEmu extensions
Three unrelated features multiplexed on the first parameter, not on the code, so dispatch happens there:
9 ; 9 ; path9 ; 4 ; state ; pct9 ; text9;9matters more than it looks: Microsoft's own documented Windows prompts emit it rather than OSC 7, so a terminal reading only 7 loses the working directory on Windows entirely. The path arrives bare rather than as afile://URI, and pwsh sends it quoted.Falling through to the notification case is only correct once the sub-parameters are claimed first — otherwise every progress tick raises a toast reading
"4;1;50". There's a test for exactly that.Progress values are clamped rather than rejected (a sender that overshoots still means "as far as it goes"), and zeroed for
NoneandIndeterminate, which carry no percentage and would otherwise leave a bar at a stale position while claiming the extent is unknown.Testing
29 new tests in
ShellIntegrationTests.cs: every mark, a full prompt→command→exit cycle, present/absent/negative exit codes, stale-code clearing,STas well asBELtermination, unknown marks and empty payloads, quoted and empty paths, every progress state, clamping, and the notification/sub-parameter split.Full suite: 636 passed, 0 failed.
Context
One of two independent PRs; neither is stacked on the other, and either can merge first. The companion (#19) adds a general
Terminal.OscReceivedpassthrough event.They do both touch the
HandleOscswitch, so whichever merges second will want a trivial rebase — the changes are adjacent rather than overlapping (this one addscasearms; #19 adds a flag and a trailing raise).🤖 Generated with Claude Code