feat: shell integration channel (OSC 9001) for color, git, title#19
feat: shell integration channel (OSC 9001) for color, git, title#19martin-ottosen wants to merge 8 commits into
Conversation
There was a problem hiding this comment.
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.jsthat parseskey=valuefields and forwards them to WPF via WebView2 messages. - Introduced a WPF bridge event +
SessionViewModel.ApplyShellIntegration(...)to applycolor,git-branch,git-dirty, andtitle, 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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| // 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(); | ||
| }; |
There was a problem hiding this comment.
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.
|
|
||
| | Key | Value format | Effect | | ||
| |--------------|-------------------------|--------| | ||
| | `color` | `#rgb`, `#rrggbb`, `#rrggbbaa` | Override the session accent. Repaints the sidebar stripe and the active-pane ring immediately. | |
| // 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(); |
|
|
||
| | Key | Effect | | ||
| |---|---| | ||
| | `color` | Override the session accent (`#rrggbb` / `#rgb` / `#rrggbbaa`). Repaints sidebar stripe + active ring. | |
Agent-Logs-Url: https://github.com/umage-ai/CodeShellManager/sessions/050b565f-3dd1-4e3a-b1d1-71a3ce9afdab Co-authored-by: AThraen <5888420+AThraen@users.noreply.github.com>
Agent-Logs-Url: https://github.com/umage-ai/CodeShellManager/sessions/7099de3e-461b-4286-ae83-7db99b33d7c3 Co-authored-by: AThraen <5888420+AThraen@users.noreply.github.com>
Agent-Logs-Url: https://github.com/umage-ai/CodeShellManager/sessions/7099de3e-461b-4286-ae83-7db99b33d7c3 Co-authored-by: AThraen <5888420+AThraen@users.noreply.github.com>
|
Parking this temporarily — capturing the current state so it's easy to pick back up. Status
Conflicts9 files diverged. Main has churned heavily on the central ones since this branched:
Also touched: Review feedback (5 Copilot inline comments)
Open integration questions for the rebase
To finish
|
What changed
color=#hex— overrides the session accent. Repaints the sidebar stripe and the active-pane ring immediately, persists tostate.json.git-branch=…,git-dirty=0|1— updates the sidebar git label, bypassing CSM's localgit statuspolling. 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.GitBranch/GitIsDirtyfor the lifetime of the session — otherwise the local CWD's git state would clobber the OSC value every poll.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
printf '\e]9001;color=#a6e3a1\e\'. Sidebar stripe + active ring go green.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).printf '\e]9001;title=Demo\e\'. Tab title becomesDemo; persists across app restart.printf '\e]9001;color=#cba6f7;git-branch=main;git-dirty=0;title=ok\e\'— all four apply at once.Developer notes
parser.registerOscHandlerrequiresallowProposedApi: true(already enabled).trueso xterm consumes the bytes — the sequence never renders. This is the documented signal in xterm.js for "I handled it".SaveStateAsyncruns after every OSC application. If shells get chatty (e.g. animating colors at high rate), this should be debounced — currently not throttled._gitOverriddenByOscis 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.#rgb/#rrggbb/#rrggbbaaonly). CSS named colors andrgb()are rejected silently —IsValidHexColorwon't set them, and the rest of the payload still applies.