Add OSC 133/633 shell-integration marks in interactive mode#42
Conversation
ConsoleReplInteractionPresenter carried private env-var sniffing (WT_SESSION, ConEmuANSI, TERM_PROGRAM, TMUX, TERM prefixes) that the upcoming shell-integration marks would have had to duplicate. Move the logic verbatim to an internal TerminalEnvironmentClassifier shared by terminal-specific emitters. - No behavior change: existing advanced-progress tests pass unmodified. - Promote EnvironmentVariableScope to a shared test helper. - Add focused classifier tests (tmux, screen, WT, ConEmu, tmux-wrapped). Claude-Session: https://claude.ai/code/session_01AopmWBr1VknLrUqerBEWsB
- New TerminalCapabilities.ShellIntegrationMarks flag (1 << 5). - TerminalCapabilitiesClassifier recognizes vscode identities and infers the marks flag for wezterm, iterm, ghostty, windows terminal, and vscode. ConEmu keeps ProgressReporting but never gets marks (it has no OSC 133 support). - TerminalEnvironmentClassifier gains IsKnownShellIntegrationTerminal (WT_SESSION, TERM_PROGRAM=vscode/WezTerm, multiplexers excluded) and IsVsCodeTerminal. Claude-Session: https://claude.ai/code/session_01AopmWBr1VknLrUqerBEWsB
- ShellIntegrationMarkEmitter: OSC 133 lifecycle marks (A/B/C/D) with an OSC 633 backend under VS Code, including the 633;E command-line report with spec-compliant escaping (backslash, semicolon, control chars). A phase state machine guarantees one D per prompt cycle; gating mirrors advanced progress (passthrough, ANSI, redirection, Auto/Always/Never). - Public surface: ShellIntegrationMode, TerminalIntegrationOptions, and UseTerminalIntegration extensions on CoreReplApp and ReplApp. - ReplOptions carries the integration options internally; null keeps the feature disabled (opt-in). Claude-Session: https://claude.ai/code/session_01AopmWBr1VknLrUqerBEWsB
InteractiveSession now drives a ShellIntegrationMarkEmitter through the prompt cycle: A before the prompt text, B before the line read, 633;E (VS Code backend) plus C after a non-empty commit, and a single D per cycle owned by the loop. - Exit codes now flow to the D mark: DispatchInteractiveCommandAsync, ExecuteWithCancellationAsync, and ExecuteInteractiveInputAsync return the exit code instead of discarding it (help renders 0/1, matched commands surface ExecuteMatchedCommandAsync's computed code, context navigation 0/1, route failures 1, ambient errors 1). - Cancelled commands report 130 (128+SIGINT) alongside the existing Cancelled. message. - Aborted cycles (Escape, EOF, empty commit) emit D without an exit code and skip C, matching the FinalTerm aborted-command form. - Ambient commands, ambiguous prefixes, and failures all run inside the C..D region because C is emitted by the loop before dispatch. Claude-Session: https://claude.ai/code/session_01AopmWBr1VknLrUqerBEWsB
- New docs/terminal-shell-integration.md: marks, modes, gating, backend selection, exit-code conventions, hosted sessions. - Cross-links from configuration-reference (TerminalIntegrationOptions), interactive-loop (lifecycle positions), and terminal-metadata (capability gating). Claude-Session: https://claude.ai/code/session_01AopmWBr1VknLrUqerBEWsB
|
@codex please review |
Address review feedback on the shell-integration marks: - Auto mode and 633-backend selection now respect the session boundary: hosted sessions rely solely on the client-advertised capability and terminal identity; WT_SESSION/TERM_PROGRAM describe the terminal the server runs in and no longer leak marks (or the VS Code dialect) to remote clients that never advertised support. - A failed interactive 'complete' ambient command now propagates HandledError so the command-end mark reports exit code 1 instead of decorating the failure as success. - Regression tests for both: server-env suppression, backend selection under a vscode server env, and the complete-failure exit code. Claude-Session: https://claude.ai/code/session_01AopmWBr1VknLrUqerBEWsB
|
@codex please review |
Address second-round review feedback: - Protocol-passthrough routes run interactively no longer get output marks: the input is pre-resolved before opening the output region, and the cycle is abandoned silently (no D after the payload; the next prompt-start implicitly closes it on the terminal side). - A dispatch that throws (e.g. history with a non-numeric --limit) now emits D;1 before the exception propagates, so the terminal never keeps an unterminated command segment. - A failed ambient help render (unknown output format) propagates HandledError and reports D;1, matching the non-ambient --help path. - Committed-input handling extracted to ExecuteCommittedInputAsync to keep the session loop within analyzer limits. Claude-Session: https://claude.ai/code/session_01AopmWBr1VknLrUqerBEWsB
|
@codex please review |
Third-round review feedback: - Enablement and backend are re-resolved at each prompt start instead of frozen at session start, so hosted clients that advertise capabilities mid-session (Telnet TTYPE, @@repl:* control messages) get marks from the next cycle. Environment variables are still only consulted when no hosted session is active. - 633;E now escapes spaces too: the VS Code contract requires escaping characters 0x20 and below, so multi-word command lines round-trip. - A passthrough route invoked with --help only renders help; the pre-resolution now treats it as a normal lifecycle (C and D emitted). - Test helper uses a typed catch (code-quality feedback). Claude-Session: https://claude.ai/code/session_01AopmWBr1VknLrUqerBEWsB
|
@codex please review |
Fourth-round review feedback: - A protocol-passthrough invocation that fails (validation error, unsupported host, or a thrown exception) never streams a payload: it now keeps its command-end mark and exit code instead of abandoning the cycle, preserving failure decorations. Only a successful passthrough dispatch abandons the cycle silently. - Hosted clients advertising ANSI purely through capability flags (no AnsiSupport override) no longer lose marks to the server console's redirection fallback; explicit opt-outs (AnsiSupport=false, AnsiMode.Never) still win. Claude-Session: https://claude.ai/code/session_01AopmWBr1VknLrUqerBEWsB
|
@codex please review |
…sification Fifth-round review feedback: - Once a passthrough invocation dispatches, no command-end mark is emitted regardless of exit code or exception: handlers may emit raw bytes and then fail (e.g. Results.Exit(7)), and an exit code cannot prove the payload never started. This supersedes the previous validation-error decoration in favor of protocol integrity. - Passthrough classification now rules out ambient commands first: an ambient command sharing its token with a passthrough route (help, history, custom ambient) keeps the normal lifecycle. Token-only mirror helper cross-referenced with the ambient dispatch table. - Red-first regression tests for all three scenarios. Claude-Session: https://claude.ai/code/session_01AopmWBr1VknLrUqerBEWsB
|
@codex please review |
This comment was marked as resolved.
This comment was marked as resolved.
Review-panel security finding (HIGH): EscapeCommandLine only covered backslash, semicolon, and chars <= 0x20. DEL (0x7f) and the C1 controls 0x80-0x9f passed through verbatim — an unescaped ST (U+009C), OSC (U+009D), or CSI (U+009B) in a pasted command line breaks out of the \x1b]633;E;... sequence and lets attacker-supplied text forge terminal control sequences on xterm.js/VTE (OSC 52 clipboard write, title spoof). - Extend the escape predicate and the SearchValues set to cover DEL and 0x80-0x9f (68 chars total). - Red-first regression test embedding DEL/ST/OSC/CSI and a >0x9f accent. Claude-Session: https://claude.ai/code/session_01AopmWBr1VknLrUqerBEWsB
Review-panel findings (HIGH H2, MEDIUM M1/M2). H2 (flagged by architect/skeptic/security/quality/style): the passthrough pre-check parsed and resolved the input independently of dispatch, each calling ResolveActiveRoutingGraph separately — a concurrent routing-graph invalidation between them could make the pre-check say 'not passthrough' while dispatch matched a passthrough route, emitting a C mark + 633;E payload into a protocol stream. Now a single ResolveCommittedInput captures one graph snapshot + one parse into a CommittedResolution record reused by both the mark decision and dispatch (which no longer re-parses or re-resolves). Removes the double/triple parse besides closing the race. M2 (architect/skeptic/quality/style): IsAmbientCommandInvocation was a hand-maintained token mirror of the dispatch table. It is now the single classification authority — TryHandleAmbientCommandAsync guards on it, so the two can never disagree. M1 (operability/skeptic): the cleanup command-end write on the exception path is now best-effort (TryWriteCommandEndAsync) so a torn-down transport failing the D-mark write can no longer mask the original dispatch exception; host-shutdown OperationCanceledException closes the cycle with an aborted D (no exit code) instead of a failure D;1. - Red-first test: a failing mark write surfaces the original exception. - Characterization test: passthrough route with a leading global option. Claude-Session: https://claude.ai/code/session_01AopmWBr1VknLrUqerBEWsB
Review-panel cleanup findings (style/skeptic/contract, LOW): - Precompute the constant A/B/C and D-without-code mark strings per backend (MarkSet) so the per-prompt path allocates no throw-away interpolated strings; only D-with-exit-code still formats. - Add exact ESC/BEL framing tests (\x1b]133;A\x07 ...) so a dropped introducer or a wrong terminator is caught, not just the ]133;X substring. - Delete the private EnvironmentVariableScope copy in the advanced-progress test; use the shared Repl.Tests.TerminalSupport one. - Replace the two duplicated CountOccurrences helpers with a shared TerminalMarks.Count over MemoryExtensions.Count. Claude-Session: https://claude.ai/code/session_01AopmWBr1VknLrUqerBEWsB
Review-panel docs findings (quality/operability, MEDIUM): - Correct the passthrough section: A/B precede a passthrough command (written around the prompt before it is known), then E/C/D are suppressed and the cycle is abandoned regardless of exit code; the next prompt-start closes it. Note the ambient/--help exceptions. - Add a symptom -> gate troubleshooting table so the black-box enablement decision can be triaged without reading ResolveEnabled. - Document the disable paths: ShellIntegrationMode.Never per app, and NO_COLOR/TERM=dumb as the end-user escape hatch (with the all-ANSI collateral called out). Claude-Session: https://claude.ai/code/session_01AopmWBr1VknLrUqerBEWsB
This comment was marked as resolved.
This comment was marked as resolved.
…ypass red Review finding: da19857 (apply parsed globals before resolving the committed route) landed without an executable red because the routing graph cached at banner time masks the interactive observable. The regression drives the cache-bypass path: a module gated on a per-command global (--env prod) plus a command calling InvalidateRouting, so the next committed line is the first resolution at the new cache version and the presence predicate reads the just-applied global. Autocomplete is off so nothing re-caches a stale graph between the two. Red was executed and observed by temporarily moving the snapshot Update back after route resolution (module absent, route failure); the committed order keeps it green. StaticWindowSizeProvider promoted from a private lifecycle-test helper to a shared IntegrationTests helper - the streamed host needs it to skip DTTERM VT probing, which never answers in tests.
Review finding (operability): ResolveEnabled folded 7+ inputs into a bool with no way to tell which gate decided, leaving black-box symptom guessing as the only triage path. The resolution now returns ShellIntegrationGate - an internal enum naming the deciding gate in its fixed evaluation order - recorded per cycle as ShellIntegrationMarkEmitter.LastGate. Deliberately internal and knob-free: no new public surface, no env vars; tests pin the tricky decisions (passthrough, hosted not-advertising vs enabled, not-configured vs mode-never) and the troubleshooting doc now states the exact gate order instead of calling the decision a black box. Red-first: the gate tests were written against the missing member.
Review finding (architect/quality/style): the resolution record was a union-by-nullable-fields consumed through five null-forgiving operators in production code, so a new kind or field drift would compile silently and NRE at runtime. Per-kind factories (Ambient/Ambiguous/Help/Routed) now own which fields are populated, and guarded accessors throw a descriptive InvalidOperationException on a kind mismatch instead of relying on `!`. No behavior change; existing interactive-loop tests cover all four kinds.
Review finding (quality): the hosted-ANSI fallback added for marks (a4d945a) was never applied to the OSC 9;4 progress gate, so the two emitters diverged on the exact bug it fixed - a hosted client advertising ANSI purely through capability flags (identity inference, control messages) got marks but no advanced progress. The fallback now lives in a shared TerminalAnsiCapability helper consumed by both emitters, and the presenter evaluates it per event instead of freezing IsAnsiEnabled at construction - aligning progress with the marks per-cycle re-resolution contract for mid-session advertisement. Red-first regression: hosted client, AnsiMode.Auto, capabilities inferred from a Windows Terminal identity, no AnsiSupport override -> OSC 9;4 emitted.
Review finding (contract): "one-shot / MCP stdio emits no marks" was only structural (the interactive loop owns the emitter); a refactor wiring marks into the one-shot path would have regressed protocol streams with no red test. Pins Always mode + capable hosted terminal + matched command: payload rendered, zero ]133;/]633; bytes.
…ures Review-panel LOW batch (style/quality/contract): - Loop-stable state (mark emitter, scope list, history provider, services, cancel handler) now travels as one PromptCycleContext instead of seven positional parameters through every per-cycle method. - Ambient command tokens promoted to shared constants consumed by IsAmbientCommandInvocation, TryHandleAmbientCommandAsync, and the non-interactive ambient path, so classification and dispatch cannot drift by editing one token list and not the other. - Read-only inputTokens parameters accept IReadOnlyList<string>. - TerminalIntegrationOptions documents that ShellIntegration is re-read each prompt cycle (runtime changes take effect on the next prompt) and no longer promises unimplemented future intents. - MarkSet perf comment corrected: 633;E also formats per call, not just D-with-exit-code.
…lings Review-panel LOW batch (style/contract/skeptic): - RunInteractiveSession and CaptureInteractiveRun now share one session-scope core, and the helper no longer sits between test methods. - NeutralTerminalEnvironment moved to shared TerminalTestEnvironments, removing the per-class copies. - MarkFailingWriter observes every write shape through a rolling-window detector and records Threw; the mask-prevention test asserts it fired, so the fault injection cannot silently stop and pass vacuously. - The mask-prevention assertion no longer couples to the exact internal exception message (type + stable "--limit" marker instead). - Magic Substring(index, 8) windows replaced by MarkPayloadAt (slice to the BEL terminator). - New mark test: an ambiguous prefix reports D;1 inside the normal lifecycle (previously untested exit-code branch).
Review-panel INFO findings: document that the VS Code 633;E report transmits the committed command line verbatim to the terminal (secrets typed as arguments included, matching VS Code shell-integration behavior for regular shells), and that any handler OperationCanceledException - not only Ctrl+C - decorates the command as interrupted (130).
…ration Field report: in the VS Code integrated terminal on Windows, the gutter decoration of the FIRST interactive command landed on the banner line (later commands were fine). Root cause: VS Code anchors decorations at parse-time cursor positions, which ConPTY rewrites - worst right at process start. Real shells compensate by declaring properties VS Code uses to switch its command detection to ConPTY-tolerant heuristics; we declared none, so the position-trusting Unix path was used. The 633 backend now reports, before the first input-start mark (so the heuristics already cover the first command): - 633;P;IsWindows=True on a local Windows console only - hosted transports deliver bytes verbatim (no ConPTY), so they keep the position-trusting default; - 633;P;Prompt=<text> (re-declared when scope navigation changes the prompt), letting the marker-adjustment heuristic recognize our custom prompt line, which matches none of VS Code''s built-in prompt patterns. Red-first tests: Prompt property precedes the first B in a vscode session, hosted sessions never report IsWindows, the OSC 133 backend reports no P sequence at all, and the local-Windows-only predicate.
…Info A sample (or any handler) can now show which mode the terminal-integration layer negotiated: IReplSessionInfo.ShellIntegrationStatus returns "OSC 133", "OSC 633 (VS Code)", or "off (<gate>)" naming the deciding gate - informational strings, format not contractual. Implemented as a default interface member (source-compatible for external implementors) backed by a session-scoped ambient slot: the interactive loop opens it at loop scope so the async-local flows into handlers, and the mark emitter updates it at each per-cycle re-resolution. The spectre sample gains a `terminal` command rendering the detection (status, identity, capabilities, ANSI, window size) as a Spectre table - run it under Windows Terminal vs the VS Code integrated terminal to see the dialect switch. Red-first tests: a handler observes "OSC 633 (VS Code)" in a vscode session, and "off (NotConfigured)" without UseTerminalIntegration.
Field retest: with IsWindows/Prompt declared, the first gutter decoration still landed on the banner. VS Code source shows why, deterministically: when the app is launched from an integrated shell, that shell''s command (this process) is still open, and handlePromptStart anchors our first prompt by CLONING the last command''s end marker - which VS Code just repositioned to executedMarker+1, i.e. the first banner line. The Windows command-start heuristic then scans only 10 lines down for the prompt; a figlet banner is taller, so the adjustment gives up and the decoration sticks to the banner. The 633 backend now opens the session (once, before the very first A) with a lone command-end in the aborted form - the outer command''s exit code is unknowable from inside it. That closes the outer command at the true cursor position, so the first prompt-start anchors on the prompt line and the 10-line scan finds the prompt immediately. Scoped to the VS Code dialect: the stale-anchor behavior is specific to its command detection, and a leading D would skew FinalTerm mark counts. Red-first: the opener precedes the first 633;A (aborted form, no exit code), and the 133 backend emits no D before its first A.
Field report: marks emitted under Windows Terminal but no scrollbar pips and no command navigation. Unlike VS Code, WT (stable since 1.21) only exposes mark features through configuration: showMarksOnScrollbar on the profile, and scrollToMark actions bound to keys. Added to the troubleshooting table.
|
@codex please review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 58f102ab93
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
PR-review finding (P2): the hosted-ANSI capability fallback re-enabled terminal sequences whenever the client advertised the Ansi flag, even when the process environment carried the documented end-user escape hatches (NO_COLOR, TERM=dumb) - IsAnsiEnabled returned false for the right reason and the fallback overrode it. The fallback now bypasses only the server console''s own state (redirection, host detection): explicit opt-outs win, including the environment ones, matching how styled output honors them. The opt-out predicate lives on TerminalEnvironmentClassifier next to the other environment sniffing. Red-first: hosted client advertising Ansi via capability flags with NO_COLOR=1 (and TERM=dumb) gets no marks and the gate names AnsiUnsupported.
PR-review finding (P2): the ambient status slot opened by the interactive loop was never restored, so a nested interactive session leaked its status into the outer scope for the rest of the flow. ShellIntegrationStatusAmbient.Open now returns a disposable scope that restores the previously ambient slot; the loop disposes it with the session, so nesting shadows cleanly and a closed scope leaves null behind. Red-first: nested scope restores the outer status on dispose, and closing the only scope leaves Current null.
Static-analysis review findings: hoist the bool? into a non-null local before capturing it in the UpdateAnsiSupport lambda, and replace the bool? switch in the AnsiSupport setter with an is-pattern unwrap the analyzer can follow. No behavior change.
Review finding: the opener D was gated on the VS Code backend being active AND not yet opened, so a hosted client re-identifying to vscode mid-session received the lone aborted D between live commands - the opener only fixes a process-start anchor and must never fire once cycles have run. The latch is now taken on the first ENABLED prompt whatever the backend. Also pins the exact 633;P;IsWindows=True bytes as an internal constant with a byte-level test (the emission branch only runs on a local Windows console, which the hosted harness cannot exercise), and rewords the escape-set comment that contradicted IsForbiddenControl about the 0x9f ceiling. Red-first: WT-then-vscode flip asserts no 633;D precedes cycle 2''s A.
Review findings: the hosted capability fallback re-encoded the NO_COLOR/TERM=dumb opt-out that IsAnsiEnabled already encodes inline, and the two disagreed on CLICOLOR_FORCE - styled output lets CLICOLOR_FORCE=1 override TERM=dumb, the fallback treated dumb as absolute. TerminalEnvironmentClassifier.IsAnsiOptOutEnvironment now carries the full precedence (NO_COLOR > CLICOLOR_FORCE > TERM=dumb) and both gates consume it, so the documented escape hatches cannot drift between styled output and marks/progress. TerminalTestEnvironments.Neutral also neutralizes NO_COLOR/CLICOLOR_FORCE so capability-fallback tests cannot fail spuriously on a machine with NO_COLOR in its profile. Red-first: precedence pinned at the classifier level, and an ANSI-incapable server console with TERM=dumb + CLICOLOR_FORCE=1 emits marks for a capable hosted client.
Review findings (architect SRP/OCP): - The committed-input resolution (global parsing, prefix expansion, help scoping, single-snapshot capture) moves out of the interactive loop to a Routing-side CommittedInputResolver; ambient classification stays with the loop and is injected as a predicate, so the loop keeps only lifecycle orchestration. - Ambient commands are now one table of (match, handler) entries driving BOTH IsAmbientCommandInvocation and TryHandleAmbientCommandAsync: a dispatch branch can no longer exist without its classification or vice versa, the MA0051 suppression on the dispatch ladder disappears, and custom ambients share the options dictionary between match and dispatch so token casing always agrees by construction. Pure refactor - the ambient and marks suites cover all entries.
… scope Review findings (operability HIGH + mediums): - commands.md documents the interactive passthrough contract change (IsProtocolPassthrough observable, stream isolation, hosted IReplIoContext requirement now enforced interactively) with the migration path. - The troubleshooting section now leads with IReplSessionInfo.ShellIntegrationStatus and a three-line probe command instead of claiming the decision emits no runtime diagnostics. - The NO_COLOR/TERM=dumb escape hatch states its hosted blast radius (server-side variable, affects every session) and points hosted misdetection at the advertised identity/capabilities instead; the CLICOLOR_FORCE precedence is mentioned where the hatch is documented.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8bf8332956
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| && ReplSessionIO.AnsiSupport is null | ||
| && outputOptions.AnsiMode != AnsiMode.Never | ||
| && !TerminalEnvironmentClassifier.IsAnsiOptOutEnvironment() | ||
| && ReplSessionIO.TerminalCapabilities.HasFlag(TerminalCapabilities.Ansi); |
There was a problem hiding this comment.
Treat ShellIntegrationMarks as ANSI-capable
When a hosted app is run with TerminalSessionOverrides.TerminalCapabilities = TerminalCapabilities.ShellIntegrationMarks but does not also set AnsiSupported or the separate Ansi bit, this gate still rejects the session before Auto can honor the advertised mark support. In redirected/service hosts that means the documented explicit ShellIntegrationMarks override produces no marks at all; the mark-specific capability is itself an OSC/ANSI capability, so the fallback should either accept it here or normalize it together with Ansi.
Useful? React with 👍 / 👎.
| if (TerminalEnvironmentClassifier.IsAnsiOptOutEnvironment()) | ||
| { | ||
| return false; |
There was a problem hiding this comment.
Apply ANSI opt-outs before session ANSI overrides
In hosted runs where the session sets AnsiSupport=true before the prompt starts — for example Telnet forces AnsiMode.Always, and non-DTTERM StreamedReplHost providers default Auto detection to true — OutputOptions.IsAnsiEnabled() returns at the session override before reaching this new NO_COLOR/TERM=dumb check. Fresh evidence beyond the earlier fallback fix is that these hosted paths populate ReplSessionIO.AnsiSupport, so server-side NO_COLOR=1 still allows shell marks/progress sequences despite the docs saying that opt-out disables marks for every connected session.
Useful? React with 👍 / 👎.
Summary
Implements the shell-lifecycle slice of the terminal-integration layer: in interactive REPL mode, each prompt cycle is bracketed with FinalTerm semantic marks (OSC 133), or the VS Code shell-integration dialect (OSC 633, including the
Ecommand-line report) when the VS Code integrated terminal is detected. This unlocks command navigation, command-aware selection/copy, and success/failure decorations in Windows Terminal, VS Code, WezTerm, and other capable terminals.app.UseTerminalIntegration(options => options.ShellIntegration = ShellIntegrationMode.Auto)on bothCoreReplAppandReplApp. No marks are emitted without the call — the no-marks guarantee is test-pinned for one-shot runs too. (See Behavior changes below for the two deliberate non-mark changes that ride along.)Abefore the prompt,Bbefore the line read,633;E(VS Code, spec-compliant escaping) +Cafter a non-empty commit, and at most oneDper cycle with the real exit code — the interactive loop now stops discardingExecuteMatchedCommandAsync's computed code. Cancelled commands report 130 (128+SIGINT); aborted cycles (Escape, EOF, empty line) reportDwithout an exit code; dispatched protocol-passthrough cycles are abandoned with noDby design (no byte may trail a protocol payload)..AsProtocolPassthrough()routes now runs through the same execution contract as CLI one-shot (PushProtocolPassthroughscope, hosted-capability guard, stream isolation), so handlers observeIsProtocolPassthroughidentically in both modes.Autouses the newTerminalCapabilities.ShellIntegrationMarksflag (hosted sessions) or environment detection (WT_SESSION, TERM_PROGRAM=vscode/WezTerm), with multiplexers conservatively off; ConEmu is excluded (no OSC 133 support). Identity-inferred capabilities are recalculated on re-identification, so a Windows Terminal → dumb downgrade stops the marks. The deciding gate is recorded per cycle (internalShellIntegrationGate) and the gate order is documented for exact triage.633;P;Prompt=<text>(and633;P;IsWindows=Trueon a local Windows console) before the first input-start, so VS Code's ConPTY-compensating heuristics anchor the very first command decoration correctly — verified against a field report where the first command's gutter dot landed on the banner.TerminalEnvironmentClassifier(existing progress tests pass unmodified), and the hosted-ANSI capability fallback is now shared between marks and advanced progress (TerminalAnsiCapability).IReplInteractionChannelprompts emit nothing by construction.IReplSessionInfo.ShellIntegrationStatus(default interface member, source-compatible) exposes the per-cycle detection outcome ("OSC 133","OSC 633 (VS Code)","off (<gate>)"); the spectre sample's newterminalcommand displays it.Public API added (all additive):
ShellIntegrationMode,TerminalIntegrationOptions,TerminalCapabilities.ShellIntegrationMarks, the twoUseTerminalIntegrationextensions, and theIReplSessionInfo.ShellIntegrationStatusdefault interface member.Behavior changes
Two deliberate behavior changes apply to existing apps even without
UseTerminalIntegration:docs/commands.md): handlers observeIsProtocolPassthrough == true, framework diagnostics are isolated from the protocol stream, and the hostedIReplIoContextrequirement is enforced interactively — a hosted interactive passthrough handler without anIReplIoContextparameter now reportsprotocol_passthrough_hosted_not_supportedinstead of running without isolation.NO_COLOR>CLICOLOR_FORCE>TERM=dumb) through a predicate now shared with styled output.Verification
dotnet test src/Repl.slnx -c Release— full suite green (1 pre-existing MCP inspector skip, also present on main).TerminalHarness.RawOutput), mark ordering, backend selection, 633;E escaping (\,\x3b, control chars incl. DEL/C1), double-D guard, tmux/passthrough/redirection gates, WT→dumb downgrade, CLI-vs-interactive passthrough contract, gate-reason diagnostics, one-shot no-marks pin, and full interactive-loop scenarios (success, error, unknown command, ambiguous prefix, help, empty line, Escape, exit, cancellation, VS Code).npx markdownlint-cli2on modified docs — 0 errors.Docs
New
docs/terminal-shell-integration.md(incl. troubleshooting gate order, 633;E privacy note, disable paths) plus cross-links fromconfiguration-reference.md,interactive-loop.md, andterminal-metadata.md.