feat(terminal): record shell integration (OSC 7 cwd + OSC 133 marks) (#956)
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.
This commit is contained in:
parent
2819cdc081
commit
953a6cdb4a
|
|
@ -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 (
|
||||
<DropdownMenu
|
||||
|
|
@ -139,6 +152,12 @@ export default function TerminalContextMenu({
|
|||
<Pencil />
|
||||
Set Title…
|
||||
</DropdownMenuItem>
|
||||
{menuPaneCwd && (
|
||||
<DropdownMenuItem onSelect={onRevealCwd}>
|
||||
<FolderOpen />
|
||||
{revealLabel}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{canClosePane && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
|
|
|
|||
|
|
@ -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<Map<number, PtyTransport>>(new Map())
|
||||
const paneMode2031Ref = useRef<Map<number, boolean>>(new Map())
|
||||
const paneLastThemeModeRef = useRef<Map<number, 'dark' | 'light'>>(new Map())
|
||||
// Why: populated by OSC 7 / OSC 133 handlers registered per-pane in
|
||||
// use-terminal-pane-lifecycle. Consumers (context menu "Reveal in Finder",
|
||||
// future block-navigation) read these refs at fire time — no React
|
||||
// subscription so a chatty `cd` loop doesn't trigger re-renders.
|
||||
const paneCwdRef = useRef<Map<number, string>>(new Map())
|
||||
const paneSemanticMarksRef = useRef<Map<number, RecordedSemanticMark[]>>(new Map())
|
||||
const panePtyBindingsRef = useRef<Map<number, IDisposable>>(new Map())
|
||||
const pendingWritesRef = useRef<Map<number, string>>(new Map())
|
||||
const isActiveRef = useRef(isActive)
|
||||
|
|
@ -345,6 +352,8 @@ export default function TerminalPane({
|
|||
paneTransportsRef,
|
||||
paneMode2031Ref,
|
||||
paneLastThemeModeRef,
|
||||
paneCwdRef,
|
||||
paneSemanticMarksRef,
|
||||
panePtyBindingsRef,
|
||||
pendingWritesRef,
|
||||
isActiveRef,
|
||||
|
|
@ -791,7 +800,8 @@ export default function TerminalPane({
|
|||
toggleExpandPane,
|
||||
onRequestClosePane: handleRequestClosePane,
|
||||
onSetTitle: handleStartRename,
|
||||
rightClickToPaste
|
||||
rightClickToPaste,
|
||||
paneCwdRef
|
||||
})
|
||||
|
||||
const effectiveAppearance = settings
|
||||
|
|
@ -880,6 +890,8 @@ export default function TerminalPane({
|
|||
onClearScreen={contextMenu.onClearScreen}
|
||||
onToggleExpand={contextMenu.onToggleExpand}
|
||||
onSetTitle={contextMenu.onSetTitle}
|
||||
menuPaneCwd={contextMenu.menuPaneCwd}
|
||||
onRevealCwd={contextMenu.onRevealCwd}
|
||||
/>
|
||||
{/* Title bar overlays — portaled into each pane container that has a title
|
||||
or is currently being renamed (so the inline input appears even for
|
||||
|
|
|
|||
|
|
@ -0,0 +1,117 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
parseOsc7Path,
|
||||
parseOsc133Payload,
|
||||
pushSemanticMark,
|
||||
SEMANTIC_MARK_RING_CAPACITY,
|
||||
type RecordedSemanticMark
|
||||
} from './shell-integration'
|
||||
|
||||
describe('parseOsc7Path', () => {
|
||||
it('extracts the path from a standard file:// URI', () => {
|
||||
expect(parseOsc7Path('file://host/Users/alice/code')).toBe('/Users/alice/code')
|
||||
})
|
||||
|
||||
it('ignores the host segment on Unix', () => {
|
||||
expect(parseOsc7Path('file://remote.example.com/home/bob')).toBe('/home/bob')
|
||||
})
|
||||
|
||||
it('percent-decodes encoded characters', () => {
|
||||
expect(parseOsc7Path('file:///Users/alice/my%20project/src')).toBe(
|
||||
'/Users/alice/my project/src'
|
||||
)
|
||||
})
|
||||
|
||||
it('percent-decodes unicode codepoints', () => {
|
||||
expect(parseOsc7Path('file:///Users/alice/caf%C3%A9')).toBe('/Users/alice/café')
|
||||
})
|
||||
|
||||
it('returns null for empty input', () => {
|
||||
expect(parseOsc7Path('')).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null for non-file scheme', () => {
|
||||
expect(parseOsc7Path('https://example.com/foo')).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null for malformed URIs', () => {
|
||||
expect(parseOsc7Path('not a uri at all')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseOsc133Payload', () => {
|
||||
it('maps A to prompt-start', () => {
|
||||
expect(parseOsc133Payload('A')).toEqual({ kind: 'prompt-start' })
|
||||
})
|
||||
|
||||
it('maps B to prompt-end', () => {
|
||||
expect(parseOsc133Payload('B')).toEqual({ kind: 'prompt-end' })
|
||||
})
|
||||
|
||||
it('maps C to command-end', () => {
|
||||
expect(parseOsc133Payload('C')).toEqual({ kind: 'command-end' })
|
||||
})
|
||||
|
||||
it('maps bare D to done with no exit code', () => {
|
||||
expect(parseOsc133Payload('D')).toEqual({ kind: 'done', exitCode: null })
|
||||
})
|
||||
|
||||
it('maps D;0 to done with exit code 0', () => {
|
||||
expect(parseOsc133Payload('D;0')).toEqual({ kind: 'done', exitCode: 0 })
|
||||
})
|
||||
|
||||
it('maps D;127 to done with exit code 127', () => {
|
||||
expect(parseOsc133Payload('D;127')).toEqual({ kind: 'done', exitCode: 127 })
|
||||
})
|
||||
|
||||
it('ignores key=value hints after the leading letter', () => {
|
||||
// iTerm emits things like A;cl=m to signal the prompt kind — we don't
|
||||
// consume the hint today, but must not crash on it.
|
||||
expect(parseOsc133Payload('A;cl=m')).toEqual({ kind: 'prompt-start' })
|
||||
})
|
||||
|
||||
it('ignores extra hints after the exit code on D', () => {
|
||||
expect(parseOsc133Payload('D;0;cl=m')).toEqual({ kind: 'done', exitCode: 0 })
|
||||
})
|
||||
|
||||
it('returns null for unknown letters', () => {
|
||||
expect(parseOsc133Payload('Z')).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null for empty input', () => {
|
||||
expect(parseOsc133Payload('')).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null exit code when the token is not numeric', () => {
|
||||
expect(parseOsc133Payload('D;oops')).toEqual({ kind: 'done', exitCode: null })
|
||||
})
|
||||
})
|
||||
|
||||
describe('pushSemanticMark ring buffer', () => {
|
||||
it('appends marks under the capacity limit', () => {
|
||||
const marks: RecordedSemanticMark[] = []
|
||||
pushSemanticMark(marks, { kind: 'prompt-start', row: 1 })
|
||||
pushSemanticMark(marks, { kind: 'command-end', row: 5 })
|
||||
expect(marks).toHaveLength(2)
|
||||
expect(marks[0].row).toBe(1)
|
||||
expect(marks[1].row).toBe(5)
|
||||
})
|
||||
|
||||
it('evicts oldest marks once past capacity', () => {
|
||||
const marks: RecordedSemanticMark[] = []
|
||||
const cap = 3
|
||||
for (let i = 0; i < 5; i++) {
|
||||
pushSemanticMark(marks, { kind: 'prompt-start', row: i }, cap)
|
||||
}
|
||||
expect(marks).toHaveLength(3)
|
||||
expect(marks.map((m) => m.row)).toEqual([2, 3, 4])
|
||||
})
|
||||
|
||||
it('uses a sensible default capacity', () => {
|
||||
// Why: the cap exists to bound long-running-session memory. The exact
|
||||
// number is a product choice, not a correctness invariant — but it
|
||||
// must be positive and at least large enough for a realistic day's
|
||||
// worth of commands.
|
||||
expect(SEMANTIC_MARK_RING_CAPACITY).toBeGreaterThanOrEqual(1_000)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
// Shell-integration escape sequences that xterm.js does not handle natively.
|
||||
// Host wires these per-pane in use-terminal-pane-lifecycle.ts (register in
|
||||
// onPaneCreated, dispose in onPaneClosed) following the DEC 2031 / OSC 52
|
||||
// pattern. See docs/shell-integration-design.md.
|
||||
//
|
||||
// OSC 7: shell cwd notification — emitted on every `cd` by a shell whose
|
||||
// rc includes a `precmd` hook. Payload is a file:// URI.
|
||||
// OSC 133: FinalTerm/iTerm semantic prompt marks. Payload starts with one
|
||||
// of A/B/C/D and may carry `;key=value` suffixes we ignore.
|
||||
|
||||
export type SemanticMarkKind = 'prompt-start' | 'prompt-end' | 'command-end' | 'done'
|
||||
|
||||
export type SemanticMark =
|
||||
| { kind: 'prompt-start' }
|
||||
| { kind: 'prompt-end' }
|
||||
| { kind: 'command-end' }
|
||||
| { kind: 'done'; exitCode: number | null }
|
||||
|
||||
// Why: URL parsing is the most robust way to strip the file://[host] prefix
|
||||
// and percent-decode. Matches the main-side parser in
|
||||
// src/main/daemon/history-manager.ts; the duplication is intentional —
|
||||
// renderer code does not import from main. A later cleanup can hoist both
|
||||
// into src/shared/.
|
||||
export function parseOsc7Path(data: string): string | null {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const url = new URL(data)
|
||||
if (url.protocol !== 'file:') {
|
||||
return null
|
||||
}
|
||||
const decodedPath = decodeURIComponent(url.pathname)
|
||||
|
||||
const isWindows = typeof navigator !== 'undefined' && navigator.userAgent.includes('Windows')
|
||||
if (!isWindows) {
|
||||
return decodedPath
|
||||
}
|
||||
|
||||
// Why: OSC 7 on Windows can carry UNC paths (\\host\share) or
|
||||
// drive-letter paths (/C:/Users/x). Preserve both so "Reveal in
|
||||
// Explorer" opens the right directory.
|
||||
if (url.hostname) {
|
||||
return `\\\\${url.hostname}${decodedPath.replace(/\//g, '\\')}`
|
||||
}
|
||||
if (/^\/[A-Za-z]:/.test(decodedPath)) {
|
||||
return decodedPath.slice(1).replace(/\//g, '\\')
|
||||
}
|
||||
return decodedPath.replace(/\//g, '\\')
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function parseOsc133Payload(data: string): SemanticMark | null {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
// Why: some emitters append `;key=value` hints (e.g. `A;cl=m`, `D;0`).
|
||||
// We only care about the leading letter and, for `D`, the first numeric
|
||||
// token — everything after the first `;` beyond the exit code is hint
|
||||
// data we do not consume today.
|
||||
const letter = data[0]
|
||||
switch (letter) {
|
||||
case 'A':
|
||||
return { kind: 'prompt-start' }
|
||||
case 'B':
|
||||
return { kind: 'prompt-end' }
|
||||
case 'C':
|
||||
return { kind: 'command-end' }
|
||||
case 'D': {
|
||||
const rest = data.slice(1)
|
||||
if (rest === '' || rest[0] !== ';') {
|
||||
return { kind: 'done', exitCode: null }
|
||||
}
|
||||
const codeToken = rest.slice(1).split(';', 1)[0]
|
||||
const code = Number.parseInt(codeToken, 10)
|
||||
return { kind: 'done', exitCode: Number.isFinite(code) ? code : null }
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export type RecordedSemanticMark = SemanticMark & { row: number }
|
||||
|
||||
export const SEMANTIC_MARK_RING_CAPACITY = 10_000
|
||||
|
||||
// Why: long-running sessions with millions of commands would grow the array
|
||||
// without bound. 10 000 marks at ~40 bytes each caps per-pane footprint at
|
||||
// ~400 KB and matches what block-navigation consumers will reasonably scroll.
|
||||
export function pushSemanticMark(
|
||||
marks: RecordedSemanticMark[],
|
||||
mark: RecordedSemanticMark,
|
||||
capacity: number = SEMANTIC_MARK_RING_CAPACITY
|
||||
): RecordedSemanticMark[] {
|
||||
marks.push(mark)
|
||||
if (marks.length > capacity) {
|
||||
marks.splice(0, marks.length - capacity)
|
||||
}
|
||||
return marks
|
||||
}
|
||||
|
|
@ -9,6 +9,10 @@ type UseTerminalPaneContextMenuDeps = {
|
|||
onRequestClosePane: (paneId: number) => void
|
||||
onSetTitle: (paneId: number) => void
|
||||
rightClickToPaste: boolean
|
||||
/** Populated by the OSC 7 handler in use-terminal-pane-lifecycle. `null` /
|
||||
* missing when the shell has not emitted a cwd update, which controls
|
||||
* whether the "Reveal in Finder" context-menu item is shown. */
|
||||
paneCwdRef: React.RefObject<Map<number, string>>
|
||||
}
|
||||
|
||||
type TerminalMenuState = {
|
||||
|
|
@ -18,6 +22,10 @@ type TerminalMenuState = {
|
|||
menuOpenedAtRef: React.RefObject<number>
|
||||
paneCount: number
|
||||
menuPaneId: number | null
|
||||
/** Last OSC 7 cwd observed for the menu-target pane, or null if the shell
|
||||
* has not emitted one. Read at menu-open time so the item reflects the
|
||||
* pane's current directory without re-rendering on every `cd`. */
|
||||
menuPaneCwd: string | null
|
||||
onContextMenuCapture: (event: React.MouseEvent<HTMLDivElement>) => void
|
||||
onCopy: () => Promise<void>
|
||||
onPaste: () => Promise<void>
|
||||
|
|
@ -27,6 +35,7 @@ type TerminalMenuState = {
|
|||
onClearScreen: () => void
|
||||
onToggleExpand: () => void
|
||||
onSetTitle: () => void
|
||||
onRevealCwd: () => void
|
||||
}
|
||||
|
||||
export function useTerminalPaneContextMenu({
|
||||
|
|
@ -34,7 +43,8 @@ export function useTerminalPaneContextMenu({
|
|||
toggleExpandPane,
|
||||
onRequestClosePane,
|
||||
onSetTitle,
|
||||
rightClickToPaste
|
||||
rightClickToPaste,
|
||||
paneCwdRef
|
||||
}: UseTerminalPaneContextMenuDeps): TerminalMenuState {
|
||||
const contextPaneIdRef = useRef<number | null>(null)
|
||||
const menuOpenedAtRef = useRef(0)
|
||||
|
|
@ -150,6 +160,22 @@ export function useTerminalPaneContextMenu({
|
|||
}
|
||||
}
|
||||
|
||||
const onRevealCwd = (): void => {
|
||||
const pane = resolveMenuPane()
|
||||
if (!pane) {
|
||||
return
|
||||
}
|
||||
const cwd = paneCwdRef.current?.get(pane.id)
|
||||
if (!cwd) {
|
||||
return
|
||||
}
|
||||
// Why: shell.showItemInFolder() selects the *item* in its parent — passing
|
||||
// a directory path opens the parent with the dir selected, which is what
|
||||
// users want from "Reveal in Finder/Explorer" and matches native app
|
||||
// behavior on all three platforms.
|
||||
void window.api.shell.openPath(cwd)
|
||||
}
|
||||
|
||||
const onContextMenuCapture = (event: React.MouseEvent<HTMLDivElement>): void => {
|
||||
event.preventDefault()
|
||||
window.dispatchEvent(new Event(CLOSE_ALL_CONTEXT_MENUS_EVENT))
|
||||
|
|
@ -190,6 +216,7 @@ export function useTerminalPaneContextMenu({
|
|||
|
||||
const paneCount = managerRef.current?.getPanes().length ?? 1
|
||||
const menuPaneId = resolveMenuPane()?.id ?? null
|
||||
const menuPaneCwd = menuPaneId !== null ? (paneCwdRef.current?.get(menuPaneId) ?? null) : null
|
||||
|
||||
return {
|
||||
open,
|
||||
|
|
@ -198,6 +225,7 @@ export function useTerminalPaneContextMenu({
|
|||
menuOpenedAtRef,
|
||||
paneCount,
|
||||
menuPaneId,
|
||||
menuPaneCwd,
|
||||
onContextMenuCapture,
|
||||
onCopy,
|
||||
onPaste,
|
||||
|
|
@ -206,6 +234,7 @@ export function useTerminalPaneContextMenu({
|
|||
onClosePane,
|
||||
onClearScreen,
|
||||
onToggleExpand,
|
||||
onSetTitle: handleSetTitle
|
||||
onSetTitle: handleSetTitle,
|
||||
onRevealCwd
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,12 @@ import {
|
|||
import { applyExpandedLayoutTo, restoreExpandedLayoutFrom } from './expand-collapse'
|
||||
import { applyTerminalAppearance, mode2031SequenceFor } from './terminal-appearance'
|
||||
import { parseOsc52 } from './osc52-clipboard'
|
||||
import {
|
||||
parseOsc133Payload,
|
||||
parseOsc7Path,
|
||||
pushSemanticMark,
|
||||
type RecordedSemanticMark
|
||||
} from './shell-integration'
|
||||
import type { EffectiveMacOptionAsAlt } from '@/lib/keyboard-layout/detect-option-as-alt'
|
||||
import { resolveEffectiveTerminalAppearance } from '@/lib/terminal-theme'
|
||||
import { connectPanePty } from './pty-connection'
|
||||
|
|
@ -68,6 +74,8 @@ type UseTerminalPaneLifecycleDeps = {
|
|||
paneTransportsRef: React.RefObject<Map<number, PtyTransport>>
|
||||
paneMode2031Ref: React.RefObject<Map<number, boolean>>
|
||||
paneLastThemeModeRef: React.RefObject<Map<number, 'dark' | 'light'>>
|
||||
paneCwdRef: React.RefObject<Map<number, string>>
|
||||
paneSemanticMarksRef: React.RefObject<Map<number, RecordedSemanticMark[]>>
|
||||
panePtyBindingsRef: React.RefObject<Map<number, IDisposable>>
|
||||
pendingWritesRef: React.RefObject<Map<number, string>>
|
||||
isActiveRef: React.RefObject<boolean>
|
||||
|
|
@ -146,6 +154,8 @@ export function useTerminalPaneLifecycle({
|
|||
paneTransportsRef,
|
||||
paneMode2031Ref,
|
||||
paneLastThemeModeRef,
|
||||
paneCwdRef,
|
||||
paneSemanticMarksRef,
|
||||
panePtyBindingsRef,
|
||||
pendingWritesRef,
|
||||
isActiveRef,
|
||||
|
|
@ -179,6 +189,7 @@ export function useTerminalPaneLifecycle({
|
|||
const selectionDisposablesRef = useRef(new Map<number, IDisposable>())
|
||||
const mode2031DisposablesRef = useRef(new Map<number, IDisposable[]>())
|
||||
const osc52DisposablesRef = useRef(new Map<number, IDisposable>())
|
||||
const shellIntegrationDisposablesRef = useRef(new Map<number, IDisposable[]>())
|
||||
|
||||
const applyAppearance = (manager: PaneManager): void => {
|
||||
const currentSettings = settingsRef.current
|
||||
|
|
@ -362,6 +373,33 @@ export function useTerminalPaneLifecycle({
|
|||
})
|
||||
osc52DisposablesRef.current.set(pane.id, osc52Disposable)
|
||||
|
||||
// Shell integration: OSC 7 (cwd) and OSC 133 (semantic prompt marks).
|
||||
// Both update per-pane refs — consumers (context menu "Reveal in
|
||||
// Finder", future block-navigation) read at fire time. Return true
|
||||
// because we fully own these sequences; no other handler needs them.
|
||||
const shellIntegrationDisposables: IDisposable[] = [
|
||||
pane.terminal.parser.registerOscHandler(7, (data) => {
|
||||
const path = parseOsc7Path(data)
|
||||
if (path) {
|
||||
paneCwdRef.current.set(pane.id, path)
|
||||
}
|
||||
return true
|
||||
}),
|
||||
pane.terminal.parser.registerOscHandler(133, (data) => {
|
||||
const mark = parseOsc133Payload(data)
|
||||
if (!mark) {
|
||||
return true
|
||||
}
|
||||
const buffer = pane.terminal.buffer.active
|
||||
const row = buffer.baseY + buffer.cursorY
|
||||
const marks = paneSemanticMarksRef.current.get(pane.id) ?? []
|
||||
pushSemanticMark(marks, { ...mark, row })
|
||||
paneSemanticMarksRef.current.set(pane.id, marks)
|
||||
return true
|
||||
})
|
||||
]
|
||||
shellIntegrationDisposablesRef.current.set(pane.id, shellIntegrationDisposables)
|
||||
|
||||
const linkProviderDisposable = pane.terminal.registerLinkProvider(
|
||||
createFilePathLinkProvider(pane.id, linkDeps, pane.linkTooltip, fileOpenLinkHint)
|
||||
)
|
||||
|
|
@ -443,6 +481,15 @@ export function useTerminalPaneLifecycle({
|
|||
osc52Disposable.dispose()
|
||||
osc52DisposablesRef.current.delete(paneId)
|
||||
}
|
||||
const shellIntegrationDisposables = shellIntegrationDisposablesRef.current.get(paneId)
|
||||
if (shellIntegrationDisposables) {
|
||||
for (const d of shellIntegrationDisposables) {
|
||||
d.dispose()
|
||||
}
|
||||
shellIntegrationDisposablesRef.current.delete(paneId)
|
||||
}
|
||||
paneCwdRef.current.delete(paneId)
|
||||
paneSemanticMarksRef.current.delete(paneId)
|
||||
const transport = paneTransportsRef.current.get(paneId)
|
||||
const panePtyBinding = panePtyBindings.get(paneId)
|
||||
if (panePtyBinding) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue