Skip to content

feat: shell integration channel (OSC 9001) for color, git, title#19

Open
martin-ottosen wants to merge 8 commits into
mainfrom
feat/osc-shell-integration
Open

feat: shell integration channel (OSC 9001) for color, git, title#19
martin-ottosen wants to merge 8 commits into
mainfrom
feat/osc-shell-integration

Conversation

@martin-ottosen
Copy link
Copy Markdown
Contributor

What changed

  • New shell-integration channel via OSC 9001. Programs running inside a CSM terminal can push session state up to the host UI:
    • color=#hex — overrides the session accent. Repaints the sidebar stripe and the active-pane ring immediately, persists to state.json.
    • git-branch=…, git-dirty=0|1 — updates the sidebar git label, bypassing CSM's local git status polling. Especially useful for SSH overlays whose remote repo state CSM can't inspect.
    • title=… — renames the session (same as double-clicking the sidebar entry), persisted.
  • Mid-flight changes are live. Every emission triggers a full repaint. Multiple keys in one sequence are applied atomically.
  • Local git poller stands down once OSC takes over. Once a session has pushed git info via OSC, the 10s local poller stops touching GitBranch/GitIsDirty for the lifetime of the session — otherwise the local CWD's git state would clobber the OSC value every poll.
  • Public integrator docs at docs/shell-integration.md: wire format, key reference, copy-pasteable snippets for bash/zsh, PowerShell, Python, Node, Rust, Go. Cross-linked from README and CLAUDE.md.

Testing

  • Open a session, then run printf '\e]9001;color=#a6e3a1\e\'. Sidebar stripe + active ring go green.
  • Run printf '\e]9001;git-branch=feat/foo;git-dirty=1\e\'. Sidebar shows ⎇ feat/foo*. cd-ing the local shell to a different repo no longer updates the label (poller is suppressed).
  • Run printf '\e]9001;title=Demo\e\'. Tab title becomes Demo; persists across app restart.
  • Restart CSM. Color and title stay; git label reverts to local poll (poller suppression is per-session, not persisted).
  • Multi-field in one sequence: printf '\e]9001;color=#cba6f7;git-branch=main;git-dirty=0;title=ok\e\' — all four apply at once.

Developer notes

  • OSC 9001 was picked as the namespace to avoid collisions with iTerm2's 1337, VS Code's 633, and the shell-integration 133 range. xterm.js's parser.registerOscHandler requires allowProposedApi: true (already enabled).
  • The handler returns true so xterm consumes the bytes — the sequence never renders. This is the documented signal in xterm.js for "I handled it".
  • SaveStateAsync runs after every OSC application. If shells get chatty (e.g. animating colors at high rate), this should be debounced — currently not throttled.
  • _gitOverriddenByOsc is sticky for the session's lifetime, not persisted. Idea: once a program declares itself the source of truth, trust it. On next launch the local poller starts fresh until the program re-declares.
  • Color values are validated (#rgb/#rrggbb/#rrggbbaa only). CSS named colors and rgb() are rejected silently — IsValidHexColor won't set them, and the rest of the payload still applies.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a custom xterm.js OSC 9001 “shell integration” channel so programs running inside a CodeShellManager terminal can push session UI state (accent color, git branch/dirty, and title) up to the WPF host, with documentation for integrators.

Changes:

  • Added OSC 9001 handler in terminal-init.js that parses key=value fields and forwards them to WPF via WebView2 messages.
  • Introduced a WPF bridge event + SessionViewModel.ApplyShellIntegration(...) to apply color, git-branch, git-dirty, and title, including suppressing local git polling once OSC provides git info.
  • Added public/internal documentation and a README mention for the new protocol.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/CodeShellManager/ViewModels/SessionViewModel.cs Applies OSC-provided fields (color/git/title) and disables local git polling once OSC overrides git state.
src/CodeShellManager/Terminal/TerminalBridge.cs Adds a ShellIntegrationReceived event and parses {type:"shellIntegration", fields:{...}} messages from WebView2.
src/CodeShellManager/MainWindow.xaml.cs Wires bridge → VM shell integration handling; triggers state save and repaints accent-driven UI.
src/CodeShellManager/Assets/terminal-init.js Registers OSC 9001 handler in xterm.js and forwards parsed fields to the host.
README.md Mentions the shell integration feature and links to the new docs.
docs/shell-integration.md Adds integrator-facing wire format + key reference + example snippets.
CLAUDE.md Adds internal contributor notes describing the OSC 9001 pipeline and keys.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +142 to +149
private static bool IsValidHexColor(string s)
{
if (string.IsNullOrEmpty(s) || s[0] != '#') return false;
if (s.Length != 4 && s.Length != 7 && s.Length != 9) return false;
for (int i = 1; i < s.Length; i++)
if (!Uri.IsHexDigit(s[i])) return false;
return true;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fixed in 70d4099. Added a ToWpfHexColor helper in SessionViewModel that converts 8-digit #rrggbbaa (alpha-last, integrator convention) to WPF's expected #aarrggbb (alpha-first) before storing. 3-digit and 6-digit values pass through unchanged. Both docs/shell-integration.md and CLAUDE.md now document this conversion so integrators know to emit alpha-last.

Comment on lines +349 to +355
// Shell programs (e.g. nexus over SSH) push session state via OSC 9001;
// forward those payloads to the VM, then save so accent/title persist.
bridge.ShellIntegrationReceived += fields =>
{
Dispatcher.Invoke(() => vm.ApplyShellIntegration(fields));
_ = _vm.SaveStateAsync();
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fixed in 2aba7a7 and 8f801cf. Added SaveStateDebounced() to MainViewModel that coalesces rapid OSC 9001 events into a single write after 500ms idle. The timer callback uses Dispatcher.InvokeAsync for thread-safe state access, and a lock prevents race conditions on timer replacement. Also added SemaphoreSlim to StateService.SaveAsync() to serialize concurrent writes. Both MainViewModel and StateService now implement IDisposable to clean up resources properly.

Comment thread docs/shell-integration.md Outdated

| Key | Value format | Effect |
|--------------|-------------------------|--------|
| `color` | `#rgb`, `#rrggbb`, `#rrggbbaa` | Override the session accent. Repaints the sidebar stripe and the active-pane ring immediately. |
Comment on lines +37 to +45
// ESC ] 9001 ; color=#89b4fa ; git-branch=main ; git-dirty=1 ; title=foo ST
// Recognised keys: color, git-branch, git-dirty (0/1), title.
// Returning true tells xterm we consumed the sequence so it isn't rendered.
term.parser.registerOscHandler(9001, data => {
try {
const fields = {};
for (const part of String(data).split(';')) {
const eq = part.indexOf('=');
if (eq > 0) fields[part.slice(0, eq).trim()] = part.slice(eq + 1).trim();
Comment thread CLAUDE.md Outdated

| Key | Effect |
|---|---|
| `color` | Override the session accent (`#rrggbb` / `#rgb` / `#rrggbbaa`). Repaints sidebar stripe + active ring. |
@AThraen
Copy link
Copy Markdown
Contributor

AThraen commented May 14, 2026

Parking this temporarily — capturing the current state so it's easy to pick back up.

Status

  • CONFLICTING / DIRTY against main (last activity 2026-05-11; main has moved substantially since)
  • Branched from 978a7e47, 8 commits ahead, 380 / -4 lines
  • No CI run on the current head

Conflicts

9 files diverged. Main has churned heavily on the central ones since this branched:

File Main churn since base
MainWindow.xaml.cs +2563 lines
MainViewModel.cs +241 lines
SessionViewModel.cs +31 lines

Also touched: StateService.cs, TerminalBridge.cs, Assets/terminal-init.js, CLAUDE.md, README.md, docs/shell-integration.md. Resolving MainWindow.xaml.cs will be the bulk of the work — sleep/wake, per-session profile overrides, run-commands, restore staggers, and the launching-placeholder restore flow have all landed in the meantime, and the OSC handler needs to play nicely with all of them.

Review feedback (5 Copilot inline comments)

# Concern Status
1 #rrggbbaa vs WPF's #aarrggbb color swap (SessionViewModel.cs:151) Fixed in 70d4099 (ToWpfHexColor helper)
2 Fire-and-forget SaveStateAsync per OSC emission risks overlapping writes Fixed in 2aba7a7 + 8f801cf (debounced save + serialized writes)
3 Docs say #rrggbbaa (docs/shell-integration.md) Likely fixed alongside #1 — verify on next pass
4 ; separator with no escaping prevents semicolons in title= etc. (terminal-init.js:45) Not addressed — no follow-up commit, no reply
5 CLAUDE.md color-format table claims #rrggbbaa Likely fixed alongside #1 — verify on next pass

Open integration questions for the rebase

  • OSC color= vs ProfileColorSchemeJson precedence — which wins, and does OSC-set color survive sleep→wake?
  • Where the OSC handler hooks into the new LaunchSessionAsync after profile-overrides + restore-placeholders rearranged the call site
  • Whether the local git poller suppression flag should be persisted across restart, or stay session-lifetime as today

To finish

  1. Rebase / merge main and resolve conflicts (the OnLoaded restore loop and the bridge wiring are the tricky bits)
  2. Address comment Notifications not being cleared #4 — either document the ; limitation or add percent-encoding in terminal-init.js
  3. Verify docs fixes (Code signing to remove Windows SmartScreen warning #3 and Option to dial down notification behavior #5) actually landed
  4. Re-run CI
  5. Fresh review pass against current architecture

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.

4 participants