From 953a6cdb4a75edee56bd56b2af6c3c3295a31ff6 Mon Sep 17 00:00:00 2001
From: Neil <4138956+nwparker@users.noreply.github.com>
Date: Wed, 22 Apr 2026 15:07:33 -0700
Subject: [PATCH] feat(terminal): record shell integration (OSC 7 cwd + OSC 133
marks) (#956)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Wire the two escape sequences that TUIs and modern shells use to tell
the terminal about state that xterm.js doesn't handle natively:
- OSC 7 — shell emits its current working directory on every `cd`.
- OSC 133 — FinalTerm / iTerm semantic prompt marks (A/B/C/D with
optional exit code on D).
Parsing lives in a new pure `shell-integration.ts` helper with 21 unit
tests covering URI decoding, Windows UNC / drive-letter paths, all four
OSC 133 letters, the `key=value` hint suffix some emitters append, and
the ring-buffer cap on recorded marks.
Wiring follows the DEC 2031 / OSC 52 pattern from #896 and #952:
- Handlers registered per-pane in onPaneCreated, disposed symmetrically
in onPaneClosed.
- Two new refs (`paneCwdRef`, `paneSemanticMarksRef`) live on
TerminalPane and are passed through UseTerminalPaneLifecycleDeps.
- Semantic marks record the absolute buffer row (`baseY + cursorY`) so
future block-navigation consumers can scroll to them.
- A 10 000-entry ring buffer caps per-pane memory.
Shipped consumer: the terminal context menu grows a platform-aware
"Reveal in Finder / Show in Explorer / Open Folder" item that opens
the shell's current directory in the native file manager. The item
is hidden when no OSC 7 has been received, consistent with how other
terminal emulators surface shell-integration-dependent features.
No consumer yet for OSC 133 — recording is cheap and the consumer UX
(block-nav keybinds, exit-code gutters) deserves its own design pass.
---
.../terminal-pane/TerminalContextMenu.tsx | 21 +++-
.../components/terminal-pane/TerminalPane.tsx | 14 ++-
.../terminal-pane/shell-integration.test.ts | 117 ++++++++++++++++++
.../terminal-pane/shell-integration.ts | 102 +++++++++++++++
.../use-terminal-pane-context-menu.ts | 33 ++++-
.../use-terminal-pane-lifecycle.ts | 47 +++++++
6 files changed, 330 insertions(+), 4 deletions(-)
create mode 100644 src/renderer/src/components/terminal-pane/shell-integration.test.ts
create mode 100644 src/renderer/src/components/terminal-pane/shell-integration.ts
diff --git a/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx b/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx
index 318fb7a2a..292e7bbcb 100644
--- a/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx
+++ b/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx
@@ -2,6 +2,7 @@ import {
Clipboard,
Copy,
Eraser,
+ FolderOpen,
Maximize2,
Minimize2,
PanelBottomClose,
@@ -35,6 +36,11 @@ type TerminalContextMenuProps = {
onClearScreen: () => void
onToggleExpand: () => void
onSetTitle: () => void
+ /** Last OSC 7 cwd reported by the shell for the menu-target pane. `null`
+ * hides the "Reveal" item — same behavior as other shell-integration-
+ * dependent terminal features (tmux/iTerm). */
+ menuPaneCwd: string | null
+ onRevealCwd: () => void
}
export default function TerminalContextMenu({
@@ -52,11 +58,18 @@ export default function TerminalContextMenu({
onClosePane,
onClearScreen,
onToggleExpand,
- onSetTitle
+ onSetTitle,
+ menuPaneCwd,
+ onRevealCwd
}: TerminalContextMenuProps): React.JSX.Element {
const isMac = navigator.userAgent.includes('Mac')
+ const isWindows = navigator.userAgent.includes('Windows')
const mod = isMac ? '⌘' : 'Ctrl+'
const shift = isMac ? '⇧' : 'Shift+'
+ // Why platform-specific label: users recognize their OS's file manager by
+ // name. On Linux the verb is "Open" because there's no single canonical
+ // name (Nautilus / Files / Dolphin / Thunar).
+ const revealLabel = isMac ? 'Reveal in Finder' : isWindows ? 'Show in Explorer' : 'Open Folder'
return (
Set Title…
+ {menuPaneCwd && (
+
+
+ {revealLabel}
+
+ )}
{canClosePane && (
<>
diff --git a/src/renderer/src/components/terminal-pane/TerminalPane.tsx b/src/renderer/src/components/terminal-pane/TerminalPane.tsx
index 00721eaae..2b94533dc 100644
--- a/src/renderer/src/components/terminal-pane/TerminalPane.tsx
+++ b/src/renderer/src/components/terminal-pane/TerminalPane.tsx
@@ -12,6 +12,7 @@ import {
import type { PaneManager } from '@/lib/pane-manager/pane-manager'
import TerminalSearch from '@/components/TerminalSearch'
import type { PtyTransport } from './pty-transport'
+import type { RecordedSemanticMark } from './shell-integration'
import { fitPanes, isWindowsUserAgent, shellEscapePath } from './pane-helpers'
import { EMPTY_LAYOUT, paneLeafId, serializeTerminalLayout } from './layout-serialization'
import { createExpandCollapseActions } from './expand-collapse'
@@ -65,6 +66,12 @@ export default function TerminalPane({
const paneTransportsRef = useRef