Skip to content

Add OSC 133/633 shell-integration marks in interactive mode#42

Merged
carldebilly merged 39 commits into
mainfrom
dev/cdb/shell-integration-marks
Jul 8, 2026
Merged

Add OSC 133/633 shell-integration marks in interactive mode#42
carldebilly merged 39 commits into
mainfrom
dev/cdb/shell-integration-marks

Conversation

@carldebilly

@carldebilly carldebilly commented Jul 5, 2026

Copy link
Copy Markdown
Member

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 E command-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.

  • Opt-in umbrella API: app.UseTerminalIntegration(options => options.ShellIntegration = ShellIntegrationMode.Auto) on both CoreReplApp and ReplApp. 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.)
  • Lifecycle: A before the prompt, B before the line read, 633;E (VS Code, spec-compliant escaping) + C after a non-empty commit, and at most one D per cycle with the real exit code — the interactive loop now stops discarding ExecuteMatchedCommandAsync's computed code. Cancelled commands report 130 (128+SIGINT); aborted cycles (Escape, EOF, empty line) report D without an exit code; dispatched protocol-passthrough cycles are abandoned with no D by design (no byte may trail a protocol payload).
  • Protocol-passthrough contract: interactive dispatch of .AsProtocolPassthrough() routes now runs through the same execution contract as CLI one-shot (PushProtocolPassthrough scope, hosted-capability guard, stream isolation), so handlers observe IsProtocolPassthrough identically in both modes.
  • Gating mirrors advanced progress (OSC 9;4): never in protocol passthrough (incl. MCP stdio), never without ANSI, never on redirected local output; Auto uses the new TerminalCapabilities.ShellIntegrationMarks flag (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 (internal ShellIntegrationGate) and the gate order is documented for exact triage.
  • VS Code dialect extras: the 633 backend declares 633;P;Prompt=<text> (and 633;P;IsWindows=True on 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.
  • Consolidation: the env-var sniffing previously private to the progress presenter moved verbatim to a shared TerminalEnvironmentClassifier (existing progress tests pass unmodified), and the hosted-ANSI capability fallback is now shared between marks and advanced progress (TerminalAnsiCapability).
  • CLI one-shot mode and nested IReplInteractionChannel prompts emit nothing by construction.
  • Observability: 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 new terminal command displays it.

Public API added (all additive): ShellIntegrationMode, TerminalIntegrationOptions, TerminalCapabilities.ShellIntegrationMarks, the two UseTerminalIntegration extensions, and the IReplSessionInfo.ShellIntegrationStatus default interface member.

Behavior changes

Two deliberate behavior changes apply to existing apps even without UseTerminalIntegration:

  • Interactive protocol passthrough now honors the documented contract (also noted in docs/commands.md): handlers observe IsProtocolPassthrough == true, framework diagnostics are isolated from the protocol stream, and the hosted IReplIoContext requirement is enforced interactively — a hosted interactive passthrough handler without an IReplIoContext parameter now reports protocol_passthrough_hosted_not_supported instead of running without isolation.
  • Advanced progress (OSC 9;4) fix: a hosted client advertising ANSI purely through capability flags now receives progress sequences (previously suppressed by the server console's redirection state), and the ANSI decision is re-evaluated per event. The environment escape hatches keep their precedence (NO_COLOR > CLICOLOR_FORCE > TERM=dumb) through a predicate now shared with styled output.

Verification

  • TDD throughout: red tests first at each stage (classifier, capability flag, emitter, loop integration, and every review-round fix — including the capability-downgrade and interactive-passthrough regressions).
  • dotnet test src/Repl.slnx -c Release — full suite green (1 pre-existing MCP inspector skip, also present on main).
  • New tests include: exact escape-byte assertions through the XTerm-emulator harness (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-cli2 on modified docs — 0 errors.

Docs

New docs/terminal-shell-integration.md (incl. troubleshooting gate order, 633;E privacy note, disable paths) plus cross-links from configuration-reference.md, interactive-loop.md, and terminal-metadata.md.

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
@carldebilly

Copy link
Copy Markdown
Member Author

@codex please review

chatgpt-codex-connector[bot]

This comment was marked as resolved.

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
@carldebilly

Copy link
Copy Markdown
Member Author

@codex please review

chatgpt-codex-connector[bot]

This comment was marked as resolved.

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
@carldebilly

Copy link
Copy Markdown
Member Author

@codex please review

Comment thread src/Repl.Tests/Given_InteractiveSession_ShellIntegrationMarks.cs Fixed
chatgpt-codex-connector[bot]

This comment was marked as resolved.

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
@carldebilly

Copy link
Copy Markdown
Member Author

@codex please review

chatgpt-codex-connector[bot]

This comment was marked as resolved.

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
@carldebilly

Copy link
Copy Markdown
Member Author

@codex please review

chatgpt-codex-connector[bot]

This comment was marked as resolved.

…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
@carldebilly

Copy link
Copy Markdown
Member Author

@codex please review

@chatgpt-codex-connector

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
@carldebilly

This comment was marked as resolved.

Comment thread src/Repl.Core/Session/InteractiveSession.cs Fixed
Comment thread src/Repl.Tests/Given_InteractiveSession_ShellIntegrationMarks.cs Fixed
chatgpt-codex-connector[bot]

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).
Comment thread src/Repl.Defaults/StreamedReplHost.cs Fixed
Comment thread src/Repl.Core/Session/ReplSessionIO.cs Fixed
…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.
@carldebilly

Copy link
Copy Markdown
Member Author

@codex please review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread src/Repl.Core/Terminal/TerminalAnsiCapability.cs
Comment thread src/Repl.Core/Terminal/ShellIntegrationStatusAmbient.cs Outdated
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.
@carldebilly carldebilly added the to be reviewed Ready for a human review pass label Jul 8, 2026
Comment thread src/Repl.Core/Session/InteractiveSession.cs Dismissed
@carldebilly carldebilly marked this pull request as ready for review July 8, 2026 16:44
@carldebilly carldebilly merged commit a6276ed into main Jul 8, 2026
10 checks passed
@carldebilly carldebilly deleted the dev/cdb/shell-integration-marks branch July 8, 2026 16:46

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +221 to 223
if (TerminalEnvironmentClassifier.IsAnsiOptOutEnvironment())
{
return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

to be reviewed Ready for a human review pass

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants