Skip to content

feat: shell integration channel (OSC 9001) for color, git, title — rebased (supersedes #19) - #119

Merged
AThraen merged 9 commits into
mainfrom
feat/osc-shell-integration-v2
Sep 6, 2026
Merged

feat: shell integration channel (OSC 9001) for color, git, title — rebased (supersedes #19)#119
AThraen merged 9 commits into
mainfrom
feat/osc-shell-integration-v2

Conversation

@AThraen

@AThraen AThraen commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Supersedes #19 (feat/osc-shell-integration, @martin-ottosen), rebuilt on current main. The original branch was 114 commits behind and no longer mergeable; the design is unchanged, the plumbing was redone where main had moved.

What this is

Programs running inside a CSM terminal can push session state to the host UI with a custom OSC sequence:

ESC ] 9001 ; color=#a6e3a1 ; git-branch=feat/foo ; git-dirty=1 ; title=my-repo  ST
  • color — overrides the session accent (sidebar stripe + active ring), persisted
  • git-branch / git-dirty — sets the sidebar git label, and the local poller stands down for the session
  • title — renames the session, persisted

Public integrator reference with bash/PowerShell/Python/Node/Rust/Go snippets: docs/shell-integration.md.

What was kept from #19 (cherry-picked, original authorship preserved)

  • OSC 9001 handler in terminal-init.js, TerminalBridge.ShellIntegrationReceived, SessionViewModel.ApplyShellIntegration
  • git-poller stand-down once a program has pushed git info
  • #rrggbbaa#aarrggbb conversion for WPF
  • docs + README cross-link

What was dropped and why

What was added

  • All OSC values are treated as untrusted (they come from whatever prints to the terminal, and color/title land in state.json). Validation moved into a WPF-free Services/ShellIntegrationPayload: title capped at 80 chars, control characters stripped, empty-after-cleanup ignored; branch stripped the same way. 25 unit tests.
  • AlertDetector now strips ESC \-terminated OSC. Its regex only knew BEL, so an ST-terminated OSC (every example in the docs) either leaked its payload into prompt matching or lazily swallowed real output up to the next BEL. Pre-existing bug, made hot by this feature. 4 tests.
  • "Reset accent color" in the sidebar context menu whenever an override exists — OSC is the only writer of ColorOverride and it survives sleep/wake/restart, so there was no way back.
  • Folder edit re-enables the git poller (ReloadGitInfoAsync clears the stand-down flag).
  • Debounced save (MainViewModel.SaveStateDebounced, 500ms idle) instead of a state.json write per emission.
  • ; in values documented as a limitation (the one unanswered review comment on feat: shell integration channel (OSC 9001) for color, git, title #19).

Test plan

  • dotnet build clean, dotnet test 343/343 (32 new)
  • printf '\e]9001;color=#a6e3a1\e\' — stripe + ring go green; right-click → "Reset accent color" appears and reverts
  • printf '\e]9001;git-branch=feat/foo;git-dirty=1\e\' — sidebar shows ⎇ feat/foo*; cd elsewhere no longer changes it
  • printf '\e]9001;title=Demo\e\' — title becomes Demo, survives restart
  • Run a Claude session with a prompt hook emitting OSC 9001 every prompt — no alert false-positives, no state.json churn beyond one write per burst
  • Edit session → change folder → git label follows the new folder

🤖 Generated with Claude Code

https://claude.ai/code/session_015BEpUD83C7UHLMxNm8XByu

martin-ottosen and others added 9 commits September 6, 2026 21:18
…d save

Forwards TerminalBridge.ShellIntegrationReceived to SessionViewModel.ApplyShellIntegration
and coalesces the resulting saves through a new MainViewModel.SaveStateDebounced (500ms idle).

This is the surviving part of the original wiring from PR #19. Its accent-repaint hunks are
dropped: main now repaints the sidebar stripe and pane ring on every AccentColor change
(worktree-sibling coloring), so they would have produced a duplicate case label. Its
StateService serialization is dropped too — superseded by the atomic write + SaveGate in #88.

Co-Authored-By: Martin Ottosen <github@lottosen.dk>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015BEpUD83C7UHLMxNm8XByu
AlertDetector's ANSI regex only knew the BEL terminator. An ST-terminated OSC either leaked
its payload into prompt matching or, worse, lazily swallowed real output up to the next BEL.
Pre-existing, but OSC 9001 makes it hot: every example in docs/shell-integration.md ends in
ESC \. Also accepts the `?` private-mode marker in CSI, matching OutputIndexer.AnsiPattern.

StripAnsi is now internal so the regex can be tested directly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015BEpUD83C7UHLMxNm8XByu
…our resettable

Everything on this channel comes from whatever is printing to the terminal — a remote host,
a `cat` of a file, a hook — and colour/title are persisted to state.json. Moves validation
into a WPF-free ShellIntegrationPayload so it is unit-tested:

- title: control characters stripped, trimmed, capped at 80 chars without splitting a
  surrogate pair; empty-after-cleanup leaves the existing name alone
- git-branch: same stripping; empty → null (detached HEAD / not a repo)
- colour: unchanged rules (#rgb/#rrggbb/#rrggbbaa → WPF #aarrggbb), now covered by tests

ReloadGitInfoAsync resets the OSC git stand-down flag: after a folder edit the pushed
info described the old folder, so the local poller gets to run again.

OSC 9001 is the only writer of ColorOverride and the value survives sleep/wake and restart,
so a session recoloured once had no way back. The sidebar context menu now offers
"Reset accent color" whenever an override exists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015BEpUD83C7UHLMxNm8XByu
…rusted-input rule

- `;` cannot appear in values (field separator, no escaping) — the one review comment on
  PR #19 that was never answered; recorded as a limitation rather than solved
- title cap + control-char stripping, "Reset accent color", debounced save
- CLAUDE.md: ShellIntegrationPayload in the services table; why MainWindow does no
  repainting of its own; why AlertDetector must strip both OSC terminators

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015BEpUD83C7UHLMxNm8XByu
@AThraen
AThraen force-pushed the feat/osc-shell-integration-v2 branch from 306711e to feaaebf Compare September 6, 2026 19:20
@AThraen
AThraen merged commit 658cf00 into main Sep 6, 2026
1 check passed
@AThraen
AThraen deleted the feat/osc-shell-integration-v2 branch September 6, 2026 19:20
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.

3 participants