mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
macOS: Option+Forward-Delete sends ESC d (delete word forward) for shells and inline agent TUIs, so Claude Code deletes a word instead of the whole line; full-screen non-agent apps (vim, nano, less) keep the native CSI 3;3~. Linux/Windows unchanged. Fixes #21491. Supersedes #21893. Co-authored-by: bbingz <11211020+bbingz@users.noreply.github.com>
This commit is contained in:
@@ -28,6 +28,8 @@ import { useAppStore } from '@/store'
|
||||
import { makePaneKey } from '../../../../shared/stable-pane-id'
|
||||
import { resolveWindowsShiftEnterEncodingForPane } from './terminal-windows-shift-enter'
|
||||
import { hasCtrlEnterCsiUAuthorityForPane } from './terminal-ctrl-enter'
|
||||
import { titleHasExplicitAgentIdentity } from './title-agent-identity'
|
||||
import { isTuiAgent } from '../../../../shared/tui-agent-config'
|
||||
import { resolveTerminalInputHostPlatform } from './terminal-input-host-platform'
|
||||
import { isLocalWindowsConptyPaneForCtrlArrow } from './terminal-ctrl-arrow-conpty'
|
||||
import { keyboardEventBelongsToScope } from './terminal-keyboard-scope'
|
||||
@@ -194,6 +196,27 @@ export function createTerminalKeyboardRuntime(options: RuntimeOptions) {
|
||||
isLocalWindowsConptyPane() ? state.runtimePaneTitlesByTabId[tabId]?.[pane.id] : undefined
|
||||
)
|
||||
}
|
||||
const isActivePaneNonAgentFullScreen = (): boolean => {
|
||||
const manager = managerRef.current
|
||||
const pane = manager?.getActivePane() ?? manager?.getPanes()[0]
|
||||
if (pane?.terminal.buffer.active.type !== 'alternate') {
|
||||
return false
|
||||
}
|
||||
const state = useAppStore.getState()
|
||||
const paneKey = makePaneKey(tabId, pane.leafId)
|
||||
const foreground = state.paneForegroundAgentByPaneKey[paneKey]
|
||||
if (foreground?.agent && !foreground.shellForeground) {
|
||||
return false
|
||||
}
|
||||
const hookRow = state.agentStatusByPaneKey[paneKey]
|
||||
if (hookRow && hookRow.state !== 'done') {
|
||||
return false
|
||||
}
|
||||
if (isTuiAgent(state.agentLaunchConfigByPaneKey[paneKey]?.identity?.agentType)) {
|
||||
return false
|
||||
}
|
||||
return !titleHasExplicitAgentIdentity(state.runtimePaneTitlesByTabId[tabId]?.[pane.id] ?? '')
|
||||
}
|
||||
const resolveShortcutEvent = (
|
||||
event: Parameters<typeof resolveTerminalKeyboardShortcutAction>[0]
|
||||
) =>
|
||||
@@ -211,7 +234,8 @@ export function createTerminalKeyboardRuntime(options: RuntimeOptions) {
|
||||
isActivePaneWindowsTerminalHost,
|
||||
terminalShortcutPolicy,
|
||||
hasActivePaneCtrlEnterCsiUAuthority,
|
||||
getModifyOtherKeysActivePane
|
||||
getModifyOtherKeysActivePane,
|
||||
isActivePaneNonAgentFullScreen
|
||||
)
|
||||
const createCapturedInputSender = (pane: { id: number; leafId: string }, data: string) => {
|
||||
const capturedTransport = paneTransportsRef.current.get(pane.id)
|
||||
|
||||
@@ -24,7 +24,8 @@ export function resolveTerminalKeyboardShortcutAction(
|
||||
isWindowsTerminalHost: NonNullable<Parameters<typeof resolveTerminalShortcutAction>[10]>,
|
||||
terminalShortcutPolicy: Parameters<typeof resolveTerminalShortcutAction>[11] = 'orca-first',
|
||||
hasCtrlEnterCsiUAuthority?: Parameters<typeof resolveTerminalShortcutAction>[12],
|
||||
getModifyOtherKeysActivePane?: Parameters<typeof resolveTerminalShortcutAction>[13]
|
||||
getModifyOtherKeysActivePane?: Parameters<typeof resolveTerminalShortcutAction>[13],
|
||||
isActivePaneNonAgentFullScreen?: Parameters<typeof resolveTerminalShortcutAction>[14]
|
||||
): ReturnType<typeof resolveTerminalShortcutAction> {
|
||||
return resolveTerminalShortcutAction(
|
||||
event,
|
||||
@@ -40,7 +41,8 @@ export function resolveTerminalKeyboardShortcutAction(
|
||||
isWindowsTerminalHost,
|
||||
terminalShortcutPolicy,
|
||||
hasCtrlEnterCsiUAuthority,
|
||||
getModifyOtherKeysActivePane
|
||||
getModifyOtherKeysActivePane,
|
||||
isActivePaneNonAgentFullScreen
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { resolveTerminalShortcutAction } from './terminal-shortcut-policy'
|
||||
|
||||
const altDelete = {
|
||||
key: 'Delete',
|
||||
code: 'Delete',
|
||||
metaKey: false,
|
||||
ctrlKey: false,
|
||||
altKey: true,
|
||||
shiftKey: false
|
||||
}
|
||||
|
||||
describe('Option/Alt + Forward-Delete', () => {
|
||||
it('sends ESC d on macOS because Claude Code reads CSI 3;3~ as kill-to-end-of-line', () => {
|
||||
expect(resolveTerminalShortcutAction(altDelete, true)).toEqual({
|
||||
type: 'sendInput',
|
||||
data: '\x1bd'
|
||||
})
|
||||
})
|
||||
|
||||
it('leaves Linux and Windows on xterm native CSI 3;3~', () => {
|
||||
expect(resolveTerminalShortcutAction(altDelete, false)).toBeNull()
|
||||
expect(resolveTerminalShortcutAction(altDelete, false, 'false', 0, true)).toBeNull()
|
||||
})
|
||||
|
||||
it('defers to xterm kitty encoding when the app negotiated it', () => {
|
||||
expect(
|
||||
resolveTerminalShortcutAction(
|
||||
altDelete,
|
||||
true,
|
||||
'false',
|
||||
0,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
() => 1
|
||||
)
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps CSI 3;3~ for a non-agent full-screen app such as vim', () => {
|
||||
const resolve = (nonAgentFullScreen: boolean) =>
|
||||
resolveTerminalShortcutAction(
|
||||
altDelete,
|
||||
true,
|
||||
'false',
|
||||
0,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
() => 0,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
() => nonAgentFullScreen
|
||||
)
|
||||
expect(resolve(true)).toBeNull()
|
||||
expect(resolve(false)).toEqual({ type: 'sendInput', data: '\x1bd' })
|
||||
})
|
||||
})
|
||||
@@ -107,7 +107,9 @@ export function resolveTerminalShortcutAction(
|
||||
// Why: query-only Droid/Grok consumers need CSI-u even when the live kitty flags remain inactive.
|
||||
hasCtrlEnterCsiUAuthority?: () => boolean,
|
||||
// Why: pi arms modifyOtherKeys when its kitty query goes unanswered; see encodeModifyOtherKeysEnter.
|
||||
getModifyOtherKeysActivePane?: () => number
|
||||
getModifyOtherKeysActivePane?: () => number,
|
||||
// Why: vim/nano treat ESC d as Escape + d, so a non-agent full-screen app keeps xterm's CSI 3;3~.
|
||||
isActivePaneNonAgentFullScreen?: () => boolean
|
||||
): TerminalShortcutAction | null {
|
||||
const platform: NodeJS.Platform = isMac ? 'darwin' : isWindows ? 'win32' : 'linux'
|
||||
const matchOptions: KeybindingMatchOptions = { context: 'terminal', terminalShortcutPolicy }
|
||||
@@ -270,13 +272,18 @@ export function resolveTerminalShortcutAction(
|
||||
!event.ctrlKey &&
|
||||
event.altKey &&
|
||||
!event.shiftKey &&
|
||||
event.key === 'Backspace'
|
||||
(event.key === 'Backspace' ||
|
||||
(isMac &&
|
||||
event.key === 'Delete' &&
|
||||
event.code?.startsWith('Numpad') !== true &&
|
||||
isActivePaneNonAgentFullScreen?.() !== true))
|
||||
) {
|
||||
// Why: a kitty-protocol TUI binds the CSI 127;3u xterm emits natively; the legacy \x1b\x7f fallback would bypass it.
|
||||
// Why: a kitty-protocol TUI binds xterm's native CSI 127;3u / CSI 3;3~; legacy fallbacks would bypass them.
|
||||
if ((getKittyKeyboardFlagsActivePane?.() ?? 0) > 0) {
|
||||
return null
|
||||
}
|
||||
return { type: 'sendInput', data: '\x1b\x7f' }
|
||||
// Why: ⌥⌦ mirrors iTerm2's Natural Text Editing ESC d (#21491); Linux/Windows keep xterm's native CSI 3;3~.
|
||||
return { type: 'sendInput', data: event.key === 'Backspace' ? '\x1b\x7f' : '\x1bd' }
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user