From 625f106ec08f2b9bd260540891da604dad060d67 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sun, 5 Jul 2026 22:23:48 -0700 Subject: [PATCH] Mirror upstream bash-preexec so preexec dispatch test reflects real command The naive $BASH_COMMAND imitation captured Orca's chained __orca_osc133_epilogue instead of the user command, failing on CI. Read the command from history like real bash-preexec does. Co-authored-by: Orca --- src/main/daemon/shell-ready.test.ts | 25 ++++++++++++--- .../terminal-pane/pty-connection.ts | 32 +++++++++---------- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/main/daemon/shell-ready.test.ts b/src/main/daemon/shell-ready.test.ts index 599027b96dc..af3ced5ade5 100644 --- a/src/main/daemon/shell-ready.test.ts +++ b/src/main/daemon/shell-ready.test.ts @@ -590,24 +590,39 @@ describePosix('daemon shell-ready launch config', () => { ) itWithBash( - 'dispatches bash-preexec callbacks against the real command, not Orca hooks', + 'dispatches a non-empty preexec_functions against the real command, not Orca hooks', async () => { const { getDaemonBashShellReadyRcfileContent } = await importFreshShellReady() // Why: Orca's epilogue captures bash-preexec's re-armed DEBUG trap and - // chains it. A non-empty preexec_functions must still see the user's real - // command as $BASH_COMMAND — not __orca_osc133_epilogue or a __bp_* helper. + // chains it. A real preexec callback must fire against the user's command — + // not __orca_osc133_epilogue. Mirror upstream bash-preexec faithfully: it + // enables `functrace` (so Orca's `trap -p DEBUG` capture sees its trap), + // defers that install to the first prompt via PROMPT_COMMAND, and reads the + // command from `history` (so DEBUG fires on prompt hooks never dispatch a + // phantom). The naive `$BASH_COMMAND` imitation does none of these. writeFileSync( join(userDataPath, '.bash_profile'), [ 'preexec_functions=(__user_preexec)', '__user_preexec() { printf \'USER_PREEXEC:%s\\n\' "$1"; }', + '__bp_inside=0', + '__bp_last_hist=""', '__bp_preexec_invoke_exec() {', + ' (( __bp_inside > 0 )) && return', ' [[ -n "${__bp_interactive_mode:-}" ]] || return', + ' local __bp_inside=1', + ' local this_command', + ' this_command="$(builtin history 1)"', + ' this_command="${this_command#"${this_command%%[![:space:]]*}"}"', + ' this_command="${this_command#* }"', + ' this_command="${this_command#"${this_command%%[![:space:]]*}"}"', + ' [[ -n "$this_command" && "$this_command" != "$__bp_last_hist" ]] || return', + ' __bp_last_hist="$this_command"', ' __bp_interactive_mode=""', ' local f', - ' for f in "${preexec_functions[@]}"; do "$f" "$BASH_COMMAND"; done', + ' for f in "${preexec_functions[@]}"; do "$f" "$this_command"; done', '}', - "__bp_arm() { __bp_interactive_mode=1; trap '__bp_preexec_invoke_exec' DEBUG; }", + "__bp_arm() { set -o functrace; __bp_interactive_mode=1; trap '__bp_preexec_invoke_exec' DEBUG; }", 'PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND;}__bp_arm"' ].join('\n') ) diff --git a/src/renderer/src/components/terminal-pane/pty-connection.ts b/src/renderer/src/components/terminal-pane/pty-connection.ts index 364e848e5be..1dead4f1ca8 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.ts @@ -1757,15 +1757,14 @@ export function connectPanePty( } return pendingWrite.then(() => interruptInference.flushPending()) } - // Why: a launched/hook-known agent pane must confirm — not trust — a 133;D so a - // full-screen agent's leaked nested-shell 133;D can't clear its tab identity, - // even on a restore where no command-start read has recorded evidence yet. - const paneHasKnownAgentIdentity = (): boolean => { - const state = useAppStore.getState() + // Why: the 133;D confirmation guard and the visible-pane resampler both key off + // "does this pane expect an agent"; derive each signal once so the two callers + // can't drift and silently reintroduce the icon bug this fix closes. + const paneHasLiveHookAgentIcon = (state: ReturnType): boolean => { const entry = state.agentStatusByPaneKey[cacheKey] - if (entry?.state !== 'done' && agentTypeToIconAgent(entry?.agentType)) { - return true - } + return entry?.state !== 'done' && Boolean(agentTypeToIconAgent(entry?.agentType)) + } + const paneExpectsLaunchAgent = (state: ReturnType): boolean => { const tab = (state.tabsByWorktree[deps.worktreeId] ?? []).find( (candidate) => candidate.id === deps.tabId ) @@ -1773,6 +1772,13 @@ export function connectPanePty( tab?.launchAgent ?? paneStartup?.launchAgent ?? paneStartup?.initialAgentStatus?.agent ) } + // Why: a launched/hook-known agent pane must confirm — not trust — a 133;D so a + // full-screen agent's leaked nested-shell 133;D can't clear its tab identity, + // even on a restore where no command-start read has recorded evidence yet. + const paneHasKnownAgentIdentity = (): boolean => { + const state = useAppStore.getState() + return paneHasLiveHookAgentIcon(state) || paneExpectsLaunchAgent(state) + } // Why: a plain `codex`/`grok` sets its OSC title and the shell never repaints // it on exit, so a confirmed return-to-shell must clear a title that still // names an agent — otherwise the tab reads "grok" over a bare prompt. Only @@ -1811,16 +1817,10 @@ export function connectPanePty( if (foreground?.agent) { return } - const entry = state.agentStatusByPaneKey[cacheKey] - if (entry?.state !== 'done' && agentTypeToIconAgent(entry?.agentType)) { + if (paneHasLiveHookAgentIcon(state)) { return } - const tab = (state.tabsByWorktree[deps.worktreeId] ?? []).find( - (candidate) => candidate.id === deps.tabId - ) - const expectsAgent = Boolean( - tab?.launchAgent ?? paneStartup?.launchAgent ?? paneStartup?.initialAgentStatus?.agent - ) + const expectsAgent = paneExpectsLaunchAgent(state) // Why: with no shell mark yet, launchAgent bootstrap already paints the icon, // so a read is pointless. Otherwise probe the foreground: a shell mark (from a // reattach or a full-screen agent's leaked nested-shell 133;D) can hide a