mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
fix(terminal): preserve Pi Shift+Enter through trust gaps (#12618)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -339,37 +339,8 @@ export function useTerminalKeyboardShortcuts({
|
||||
}
|
||||
}
|
||||
|
||||
// Why: this callback is installed once per active tab and invoked only for
|
||||
// Windows Shift+Enter, keeping store work and allocations off ordinary keys.
|
||||
const getActivePaneWindowsShiftEnterEncoding = () => {
|
||||
const manager = managerRef.current
|
||||
const activePane = manager?.getActivePane() ?? manager?.getPanes()[0]
|
||||
if (!activePane) {
|
||||
return 'alt-enter' as const
|
||||
}
|
||||
const state = useAppStore.getState()
|
||||
const paneKey = makePaneKey(tabId, activePane.leafId)
|
||||
return resolveWindowsShiftEnterEncodingForPane(state, paneKey)
|
||||
}
|
||||
|
||||
// Why: host metadata is live and can hydrate after the terminal mounts;
|
||||
// resolve it only when Shift+Enter needs to choose a byte protocol.
|
||||
const isActivePaneWindowsTerminalHost = (): boolean => {
|
||||
const manager = managerRef.current
|
||||
const activePane = manager?.getActivePane() ?? manager?.getPanes()[0]
|
||||
return (
|
||||
resolveTerminalInputHostPlatform({
|
||||
clientPlatform: shortcutPlatform,
|
||||
state: useAppStore.getState(),
|
||||
worktreeId,
|
||||
transport: activePane ? (paneTransportsRef.current.get(activePane.id) ?? null) : null
|
||||
}) === 'win32'
|
||||
)
|
||||
}
|
||||
|
||||
// Why: the active pane's live PTY session decides whether Ctrl+Arrow should
|
||||
// pass through as native \e[1;5C/\e[1;5D or be translated to \eb/\ef.
|
||||
// Resolved lazily so session/runtime lookups stay off other keystrokes.
|
||||
// Why: foreground proof and Ctrl+Arrow translation are local ConPTY-only;
|
||||
// resolve lazily so session/runtime lookups stay off ordinary keystrokes.
|
||||
const isLocalWindowsConptyPane = (): boolean => {
|
||||
const manager = managerRef.current
|
||||
const activePane = manager?.getActivePane() ?? manager?.getPanes()[0]
|
||||
@@ -390,6 +361,40 @@ export function useTerminalKeyboardShortcuts({
|
||||
})
|
||||
}
|
||||
|
||||
// Why: this callback is installed once per active tab and invoked only for
|
||||
// Windows Shift+Enter, keeping store work and allocations off ordinary keys.
|
||||
const getActivePaneWindowsShiftEnterEncoding = () => {
|
||||
const manager = managerRef.current
|
||||
const activePane = manager?.getActivePane() ?? manager?.getPanes()[0]
|
||||
if (!activePane) {
|
||||
return 'alt-enter' as const
|
||||
}
|
||||
const state = useAppStore.getState()
|
||||
const paneKey = makePaneKey(tabId, activePane.leafId)
|
||||
return resolveWindowsShiftEnterEncodingForPane(
|
||||
state,
|
||||
paneKey,
|
||||
isLocalWindowsConptyPane()
|
||||
? state.runtimePaneTitlesByTabId[tabId]?.[activePane.id]
|
||||
: undefined
|
||||
)
|
||||
}
|
||||
|
||||
// Why: host metadata is live and can hydrate after the terminal mounts;
|
||||
// resolve it only when Shift+Enter needs to choose a byte protocol.
|
||||
const isActivePaneWindowsTerminalHost = (): boolean => {
|
||||
const manager = managerRef.current
|
||||
const activePane = manager?.getActivePane() ?? manager?.getPanes()[0]
|
||||
return (
|
||||
resolveTerminalInputHostPlatform({
|
||||
clientPlatform: shortcutPlatform,
|
||||
state: useAppStore.getState(),
|
||||
worktreeId,
|
||||
transport: activePane ? (paneTransportsRef.current.get(activePane.id) ?? null) : null
|
||||
}) === 'win32'
|
||||
)
|
||||
}
|
||||
|
||||
// Why: the pane's TUI opted into kitty keyboard reporting via CSI > u;
|
||||
// the tracker mirrors that from PTY output so the policy can encode
|
||||
// Option chords the way the application negotiated.
|
||||
|
||||
@@ -7190,6 +7190,7 @@ describe('connectPanePty', () => {
|
||||
|
||||
expect(mockStoreState.paneForegroundAgentByPaneKey[paneKey]).toEqual({
|
||||
agent: 'droid',
|
||||
routingRevoked: true,
|
||||
shellForeground: false
|
||||
})
|
||||
expect(resolveMockPaneWindowsShiftEnterEncoding(mockStoreState, paneKey)).toBe('alt-enter')
|
||||
@@ -23218,6 +23219,7 @@ describe('connectPanePty', () => {
|
||||
await vi.advanceTimersByTimeAsync(1)
|
||||
expect(mockStoreState.paneForegroundAgentByPaneKey[cacheKey]).toEqual({
|
||||
agent: 'droid',
|
||||
routingRevoked: true,
|
||||
shellForeground: false
|
||||
})
|
||||
|
||||
@@ -23260,6 +23262,7 @@ describe('connectPanePty', () => {
|
||||
|
||||
expect(mockStoreState.paneForegroundAgentByPaneKey[cacheKey]).toEqual({
|
||||
agent: 'pi',
|
||||
routingRevoked: true,
|
||||
shellForeground: false
|
||||
})
|
||||
expect(resolveMockPaneWindowsShiftEnterEncoding(mockStoreState, cacheKey)).toBe('alt-enter')
|
||||
|
||||
@@ -2183,6 +2183,7 @@ export function connectPanePty(
|
||||
// as a hint, but revoke bytes until one current provider confirmation lands.
|
||||
useAppStore.getState().setPaneForegroundAgent(cacheKey, {
|
||||
agent: foreground.agent,
|
||||
routingRevoked: true,
|
||||
shellForeground: false
|
||||
})
|
||||
visibleForegroundSamplePending = false
|
||||
|
||||
@@ -23,7 +23,79 @@ describe('resolveWindowsShiftEnterEncoding', () => {
|
||||
expect(resolveWindowsShiftEnterEncoding({ launchAgentType: 'pi' })).toBe('alt-enter')
|
||||
})
|
||||
|
||||
it('does not let hook or OSC-derived status forge Droid input routing', () => {
|
||||
it('recovers Pi CSI-u from its explicit title when process trust is unavailable', () => {
|
||||
const state = {
|
||||
paneForegroundAgentByPaneKey: {
|
||||
'tab:pane': { agent: null, shellForeground: false }
|
||||
},
|
||||
agentLaunchConfigByPaneKey: {}
|
||||
}
|
||||
|
||||
expect(resolveWindowsShiftEnterEncodingForPane(state, 'tab:pane', '⠸ Pi')).toBe('csi-u')
|
||||
expect(
|
||||
resolveWindowsShiftEnterEncodingForPane(
|
||||
{ paneForegroundAgentByPaneKey: {}, agentLaunchConfigByPaneKey: {} },
|
||||
'tab:pane',
|
||||
'Pi ready'
|
||||
)
|
||||
).toBe('csi-u')
|
||||
})
|
||||
|
||||
it('keeps trusted process and shell evidence authoritative over titles', () => {
|
||||
const state = {
|
||||
paneForegroundAgentByPaneKey: {
|
||||
'tab:pane': {
|
||||
agent: 'codex' as const,
|
||||
routingTrusted: true,
|
||||
shellForeground: false
|
||||
}
|
||||
},
|
||||
agentLaunchConfigByPaneKey: {}
|
||||
}
|
||||
|
||||
expect(resolveWindowsShiftEnterEncodingForPane(state, 'tab:pane', 'Pi ready')).toBe('alt-enter')
|
||||
expect(
|
||||
resolveWindowsShiftEnterEncodingForPane(
|
||||
{
|
||||
paneForegroundAgentByPaneKey: {
|
||||
'tab:pane': { agent: null, shellForeground: true }
|
||||
},
|
||||
agentLaunchConfigByPaneKey: {}
|
||||
},
|
||||
'tab:pane',
|
||||
'Pi ready'
|
||||
)
|
||||
).toBe('alt-enter')
|
||||
})
|
||||
|
||||
it('does not let a stale title undo explicit routing revocation', () => {
|
||||
const state = {
|
||||
paneForegroundAgentByPaneKey: {
|
||||
'tab:pane': {
|
||||
agent: 'pi' as const,
|
||||
routingRevoked: true,
|
||||
shellForeground: false
|
||||
}
|
||||
},
|
||||
agentLaunchConfigByPaneKey: {}
|
||||
}
|
||||
|
||||
expect(resolveWindowsShiftEnterEncodingForPane(state, 'tab:pane', 'Pi ready')).toBe('alt-enter')
|
||||
})
|
||||
|
||||
it('keeps legacy bytes for plain shell and unsupported-agent titles', () => {
|
||||
const state = {
|
||||
paneForegroundAgentByPaneKey: {},
|
||||
agentLaunchConfigByPaneKey: {}
|
||||
}
|
||||
|
||||
expect(resolveWindowsShiftEnterEncodingForPane(state, 'tab:pane', 'C:\\work\\pi-project')).toBe(
|
||||
'alt-enter'
|
||||
)
|
||||
expect(resolveWindowsShiftEnterEncodingForPane(state, 'tab:pane', 'Codex')).toBe('alt-enter')
|
||||
})
|
||||
|
||||
it('does not let hook status route bytes without a pane title or process proof', () => {
|
||||
const state = {
|
||||
paneForegroundAgentByPaneKey: {},
|
||||
agentStatusByPaneKey: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { AgentType } from '../../../../shared/agent-status-types'
|
||||
import { TUI_AGENT_CONFIG } from '../../../../shared/tui-agent-config'
|
||||
import { resolveCommittedTitleAgentType } from '../../lib/pane-agent-evidence'
|
||||
import type { PaneForegroundAgentEntry } from '@/store/slices/pane-foreground-agent'
|
||||
|
||||
export type WindowsShiftEnterEncoding = 'alt-enter' | 'csi-u'
|
||||
@@ -34,10 +35,26 @@ export function resolveWindowsShiftEnterEncoding(
|
||||
/** Resolves only pane-keyed evidence so a split sibling cannot inherit tab ownership. */
|
||||
export function resolveWindowsShiftEnterEncodingForPane(
|
||||
state: WindowsShiftEnterPaneState,
|
||||
paneKey: string
|
||||
paneKey: string,
|
||||
terminalTitle?: string
|
||||
): WindowsShiftEnterEncoding {
|
||||
return resolveWindowsShiftEnterEncoding({
|
||||
const foreground = state.paneForegroundAgentByPaneKey[paneKey]
|
||||
const encoding = resolveWindowsShiftEnterEncoding({
|
||||
foreground: state.paneForegroundAgentByPaneKey[paneKey],
|
||||
launchAgentType: state.agentLaunchConfigByPaneKey[paneKey]?.identity.agentType
|
||||
})
|
||||
if (
|
||||
encoding === 'csi-u' ||
|
||||
!terminalTitle ||
|
||||
foreground?.routingTrusted === true ||
|
||||
foreground?.routingRevoked === true ||
|
||||
foreground?.shellForeground === true
|
||||
) {
|
||||
return encoding
|
||||
}
|
||||
// Why: strict pane-local titles recover Pi/Droid through process-scan gaps without overriding process or shell proof.
|
||||
const titleAgent = resolveCommittedTitleAgentType(terminalTitle)
|
||||
return titleAgent
|
||||
? (TUI_AGENT_CONFIG[titleAgent].windowsShiftEnterEncoding ?? 'alt-enter')
|
||||
: 'alt-enter'
|
||||
}
|
||||
|
||||
@@ -28,6 +28,14 @@ describe('pane foreground agent slice', () => {
|
||||
.setPaneForegroundAgent('tab-1:leaf-1', { agent: 'aider', shellForeground: false })
|
||||
expect(store.getState().paneForegroundAgentByPaneKey).toBe(first)
|
||||
|
||||
store.getState().setPaneForegroundAgent('tab-1:leaf-1', {
|
||||
agent: 'aider',
|
||||
routingRevoked: true,
|
||||
shellForeground: false
|
||||
})
|
||||
expect(store.getState().paneForegroundAgentByPaneKey).not.toBe(first)
|
||||
expect(store.getState().paneForegroundAgentByPaneKey['tab-1:leaf-1']?.routingRevoked).toBe(true)
|
||||
|
||||
store.getState().clearPaneForegroundAgent('tab-1:leaf-1')
|
||||
expect(store.getState().paneForegroundAgentByPaneKey).toEqual({})
|
||||
})
|
||||
|
||||
@@ -7,6 +7,8 @@ export type PaneForegroundAgentEntry = {
|
||||
agent: TuiAgent | null
|
||||
/** True only when fresh provider evidence is safe for input-byte routing. */
|
||||
routingTrusted?: boolean
|
||||
/** True after exit/input evidence revokes routing until provider confirmation. */
|
||||
routingRevoked?: boolean
|
||||
/** True once the foreground is proven back at the shell (OSC 133;D) —
|
||||
* process-grade launched-agent exit evidence, independent of titles. */
|
||||
shellForeground: boolean
|
||||
@@ -41,6 +43,7 @@ export const createPaneForegroundAgentSlice: StateCreator<
|
||||
current &&
|
||||
current.agent === entry.agent &&
|
||||
current.routingTrusted === entry.routingTrusted &&
|
||||
current.routingRevoked === entry.routingRevoked &&
|
||||
current.shellForeground === entry.shellForeground
|
||||
) {
|
||||
return s
|
||||
|
||||
Reference in New Issue
Block a user