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:
OrcaWin
2026-08-04 19:16:32 -07:00
committed by GitHub
co-authored by OrcaWin
parent 5f187e0836
commit 7287ca8ae2
7 changed files with 143 additions and 34 deletions
@@ -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