Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@
"@types/ws": "^8.18.1",
"@vitejs/plugin-react": "^5.2.0",
"@xterm/addon-fit": "0.12.0-beta.287",
"@xterm/addon-image": "0.10.0-beta.287",
"@xterm/addon-ligatures": "0.11.0-beta.287",
"@xterm/addon-search": "0.17.0-beta.287",
"@xterm/addon-unicode11": "0.10.0-beta.287",
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/main/daemon/pty-subprocess.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ describe('createPtySubprocess', () => {
name: 'xterm-256color'
})
)
const spawnEnv = spawnMock.mock.calls.at(-1)![2].env as Record<string, string>
expect(spawnEnv.ORCA_IMAGE_PROTOCOL).toBe('kitty')
})

it('appends Git prompt guards after the detached daemon inherited config', () => {
Expand Down
1 change: 1 addition & 0 deletions src/main/daemon/pty-subprocess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,7 @@ export function createPtySubprocess(opts: PtySubprocessOptions): SubprocessHandl
TERM: 'xterm-256color',
COLORTERM: 'truecolor',
TERM_PROGRAM: 'Orca',
ORCA_IMAGE_PROTOCOL: 'kitty',
// Why: TUIs feature-gate on TERM_PROGRAM_VERSION; ORCA_APP_VERSION is inherited from the forking main process.
TERM_PROGRAM_VERSION: process.env.ORCA_APP_VERSION ?? '0.0.0-dev',
// Why: `supports-hyperlinks` gates OSC 8 on a TERM_PROGRAM allowlist excluding Orca; force it since xterm.js parses OSC 8 for clickable links.
Expand Down
7 changes: 7 additions & 0 deletions src/main/providers/local-pty-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,13 @@ describe('LocalPtyProvider', () => {
expect(spawnCall[2].env.CUSTOM_VAR).toBe('custom-value')
})

it('advertises Kitty image support to spawned terminals', async () => {
await provider.spawn({ cols: 80, rows: 24 })

const spawnCall = spawnMock.mock.calls.at(-1)!
expect(spawnCall[2].env.ORCA_IMAGE_PROTOCOL).toBe('kitty')
})

it('does not inherit NODE_ENV from the Orca process env', async () => {
// Why: NODE_ENV in Orca's process is Orca's build mode (electron-vite sets
// `development` in dev runs); leaking it breaks `next build` and Vitest.
Expand Down
1 change: 1 addition & 0 deletions src/main/providers/local-pty-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,7 @@ export class LocalPtyProvider implements IPtyProvider {
TERM: 'xterm-256color',
COLORTERM: 'truecolor',
TERM_PROGRAM: 'Orca',
ORCA_IMAGE_PROTOCOL: 'kitty',
// Why: TUIs feature-gate on TERM_PROGRAM_VERSION; the fallback keeps tests and non-Electron runs working.
TERM_PROGRAM_VERSION: process.env.ORCA_APP_VERSION ?? '0.0.0-dev',
// Why: supports-hyperlinks rejects TERM_PROGRAM=Orca, so tools drop OSC 8 links; force it since xterm.js parses them.
Expand Down
1 change: 1 addition & 0 deletions src/relay/pty-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2501,6 +2501,7 @@ describe('PtyHandler', () => {
expect(spawnEnv.name).toBe('xterm-256color')
expect(spawnEnv.env.TERM).toBe('xterm-256color')
expect(spawnEnv.env.TERM_PROGRAM).toBe('Orca')
expect(spawnEnv.env.ORCA_IMAGE_PROTOCOL).toBe('kitty')
})

it('uses the safe terminal default when TERM is deleted without a custom value', async () => {
Expand Down
1 change: 1 addition & 0 deletions src/relay/pty-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ export class PtyHandler {
TERM: 'xterm-256color',
COLORTERM: 'truecolor',
TERM_PROGRAM: 'Orca',
ORCA_IMAGE_PROTOCOL: 'kitty',
TERM_PROGRAM_VERSION:
rendererEnv?.ORCA_APP_VERSION || process.env.ORCA_APP_VERSION || '0.0.0-dev',
FORCE_HYPERLINK: '1'
Expand Down
37 changes: 37 additions & 0 deletions src/renderer/src/lib/pane-manager/pane-dom-creation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ import { describe, expect, it, vi } from 'vitest'
import type { TerminalLeafId } from '../../../../shared/stable-pane-id'
import { createPaneDOM } from './pane-dom-creation'

const imageAddonMock = vi.hoisted(() => ({
options: null as Record<string, unknown> | null
}))

vi.mock('@xterm/addon-image', () => ({
ImageAddon: vi.fn().mockImplementation(function ImageAddon(options) {
imageAddonMock.options = options
return {}
})
}))

const webLinksAddonMock = vi.hoisted(() => ({
options: null as { hover?: (event: MouseEvent, uri: string) => void; leave?: () => void } | null
}))
Expand Down Expand Up @@ -49,6 +60,32 @@ vi.mock('@xterm/xterm', () => ({
}))

describe('createPaneDOM link tooltips', () => {
it('creates bounded Kitty and iTerm2 image support for each pane', () => {
const leafId = '11111111-1111-4111-8111-111111111111' as TerminalLeafId
const pane = createPaneDOM(
1,
leafId,
{ linkOpenHint: () => 'open hint' },
{ active: null } as never,
{} as never,
vi.fn(),
vi.fn()
)

expect(pane.imageAddon).toBeDefined()
expect(imageAddonMock.options).toEqual({
enableSizeReports: true,
pixelLimit: 4_194_304,
storageLimit: 16,
showPlaceholder: true,
sixelSupport: false,
iipSupport: true,
iipSizeLimit: 8_388_608,
kittySupport: true,
kittySizeLimit: 8_388_608
})
})

it('anchors WebLinks hover text to the unpadded terminal window corner', () => {
const leafId = '11111111-1111-4111-8111-111111111111' as TerminalLeafId
const pane = createPaneDOM(
Expand Down
15 changes: 15 additions & 0 deletions src/renderer/src/lib/pane-manager/pane-dom-creation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { FitAddon } from '@xterm/addon-fit'
import { ImageAddon } from '@xterm/addon-image'
import { SearchAddon } from '@xterm/addon-search'
import { SerializeAddon } from '@xterm/addon-serialize'
import { Unicode11Addon } from '@xterm/addon-unicode11'
Expand Down Expand Up @@ -52,6 +53,19 @@ export function createPaneDOM(
installGuardedLinkProviderRegistration(terminal)
installWindowsCtrlAltChordRepair(terminal)
const fitAddon = new FitAddon()
// Inline-image decoders can transiently hold multiple RGBA buffers, so keep
// both each image and the retained per-pane cache bounded.
const imageAddon = new ImageAddon({
enableSizeReports: true,
pixelLimit: 4_194_304,
storageLimit: 16,
showPlaceholder: true,
sixelSupport: false,
iipSupport: true,
iipSizeLimit: 8_388_608,
kittySupport: true,
kittySizeLimit: 8_388_608
})
const searchAddon = new SearchAddon()
const unicode11Addon = new Unicode11Addon()
// Why: async tooltip formatting can resolve after hover changes, so stale
Expand Down Expand Up @@ -123,6 +137,7 @@ export function createPaneDOM(
webglDisabledAfterContextLoss: false,
hasComplexScriptOutput: false,
fitAddon,
imageAddon,
fitResizeObserver: null,
pendingInitialFitRafId: null,
pendingWebglRefreshRafId: null,
Expand Down
16 changes: 16 additions & 0 deletions src/renderer/src/lib/pane-manager/pane-lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,9 @@ describe('openTerminal — addon and provider wiring', () => {
fit: vi.fn()
} as unknown as ManagedPaneInternal['fitAddon']
const searchAddon = {} as unknown as ManagedPaneInternal['searchAddon']
const imageAddon = {
dispose: vi.fn()
} as unknown as ManagedPaneInternal['imageAddon']
const serializeAddon = {} as unknown as ManagedPaneInternal['serializeAddon']
const unicode11Addon = {} as unknown as ManagedPaneInternal['unicode11Addon']
const webLinksAddon = {} as unknown as ManagedPaneInternal['webLinksAddon']
Expand Down Expand Up @@ -506,6 +509,8 @@ describe('openTerminal — addon and provider wiring', () => {
events.push('loadAddon:fit')
} else if (addon === searchAddon) {
events.push('loadAddon:search')
} else if (addon === imageAddon) {
events.push('loadAddon:image')
} else if (addon === serializeAddon) {
events.push('loadAddon:serialize')
} else if (addon === unicode11Addon) {
Expand Down Expand Up @@ -549,6 +554,7 @@ describe('openTerminal — addon and provider wiring', () => {
fitResizeObserver: null,
pendingObservedFitRafId: null,
searchAddon,
imageAddon,
serializeAddon,
unicode11Addon,
ligaturesAddon: null,
Expand All @@ -562,6 +568,16 @@ describe('openTerminal — addon and provider wiring', () => {
return { pane, events, getRegisteredJoinHandler: () => registeredJoinHandler }
}

it('loads image protocol support and releases its per-pane storage on dispose', () => {
const { pane, events } = createOpenTerminalHarness()

openTerminal(pane)

expect(events).toContain('loadAddon:image')
disposePane(pane, new Map([[pane.id, pane]]))
expect(pane.imageAddon!.dispose).toHaveBeenCalledTimes(1)
})

// Why: CJK / emoji / ZWJ widths get baked into the buffer at the active
// unicode version on write. If anything writes bytes through xterm before
// unicode v11 is activated (still on default v6 width tables), wide chars
Expand Down
9 changes: 9 additions & 0 deletions src/renderer/src/lib/pane-manager/pane-lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export function openTerminal(pane: ManagedPaneInternal): void {
linkTooltip,
terminalTuiScrollSensitivity,
fitAddon,
imageAddon,
searchAddon,
serializeAddon,
unicode11Addon,
Expand All @@ -46,6 +47,9 @@ export function openTerminal(pane: ManagedPaneInternal): void {

// Load addons (order matters: WebGL must be after open())
terminal.loadAddon(fitAddon)
if (imageAddon) {
terminal.loadAddon(imageAddon)
}
terminal.loadAddon(searchAddon)
terminal.loadAddon(serializeAddon)
terminal.loadAddon(unicode11Addon)
Expand Down Expand Up @@ -217,6 +221,11 @@ export function disposePane(
/* ignore */
}
disposeWebgl(pane)
try {
pane.imageAddon?.dispose()
} catch {
/* ignore */
}
try {
pane.searchAddon.dispose()
} catch {
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/src/lib/pane-manager/pane-manager-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { IDisposable, IMarker, Terminal, ITerminalOptions } from '@xterm/xterm'
import type { FitAddon } from '@xterm/addon-fit'
import type { ImageAddon } from '@xterm/addon-image'
import type { LigaturesAddon } from '@xterm/addon-ligatures'
import type { SearchAddon } from '@xterm/addon-search'
import type { Unicode11Addon } from '@xterm/addon-unicode11'
Expand Down Expand Up @@ -156,6 +157,7 @@ export type ManagedPaneInternal = {
// Stored so disposePane() can cancel the post-WebGL-teardown refresh frame.
pendingWebglRefreshRafId?: number | null
pendingObservedFitRafId: number | null
imageAddon?: ImageAddon
serializeAddon: SerializeAddon
unicode11Addon: Unicode11Addon
webLinksAddon: WebLinksAddon
Expand Down