From 5cfce7510f44533ee39e93eb4e47a1899f76fc40 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Sun, 7 Jun 2026 12:49:16 -0700 Subject: [PATCH] Fix typecheck for host-backed agent launches and address review feedback. Guard null tab ids from web host routing, narrow runtime environment ids for tab close, and respect mobile resize locks in web refit retries. Co-authored-by: Cursor --- .../src/components/GitHubItemDialog.tsx | 4 +++- .../src/components/PullRequestPage.tsx | 4 +++- .../components/right-sidebar/ChecksPanel.tsx | 8 ++++++-- .../right-sidebar/SourceControl.tsx | 8 ++++++-- .../components/tab-bar/QuickLaunchButton.tsx | 10 ++++++++-- .../components/terminal-pane/TerminalPane.tsx | 20 ++++++++++++++++--- .../terminal/terminal-tab-actions.test.ts | 2 +- .../terminal/terminal-tab-actions.ts | 2 +- .../src/hooks/useAutomationDispatchEvents.ts | 8 ++++++-- .../src/lib/launch-agent-in-new-tab.test.ts | 2 +- .../src/lib/sidebar-worktree-activation.ts | 1 + 11 files changed, 53 insertions(+), 16 deletions(-) diff --git a/src/renderer/src/components/GitHubItemDialog.tsx b/src/renderer/src/components/GitHubItemDialog.tsx index a884f7fe541..0e2985de627 100644 --- a/src/renderer/src/components/GitHubItemDialog.tsx +++ b/src/renderer/src/components/GitHubItemDialog.tsx @@ -3502,7 +3502,9 @@ function ChecksTab({ toast.error('Could not build the agent launch command.') return } - focusTerminalTabSurface(result.tabId) + if (result.tabId) { + focusTerminalTabSurface(result.tabId) + } toast.success('Started an AI agent for the broken checks.') } finally { setFixingChecks(false) diff --git a/src/renderer/src/components/PullRequestPage.tsx b/src/renderer/src/components/PullRequestPage.tsx index 1467e40147c..a35f247d056 100644 --- a/src/renderer/src/components/PullRequestPage.tsx +++ b/src/renderer/src/components/PullRequestPage.tsx @@ -3756,7 +3756,9 @@ function ChecksTab({ toast.error('Could not build the agent launch command.') return } - focusTerminalTabSurface(result.tabId) + if (result.tabId) { + focusTerminalTabSurface(result.tabId) + } toast.success('Started an AI agent for the broken checks.') } finally { setFixingChecks(false) diff --git a/src/renderer/src/components/right-sidebar/ChecksPanel.tsx b/src/renderer/src/components/right-sidebar/ChecksPanel.tsx index 2ecc473bf06..d626d454f90 100644 --- a/src/renderer/src/components/right-sidebar/ChecksPanel.tsx +++ b/src/renderer/src/components/right-sidebar/ChecksPanel.tsx @@ -1837,7 +1837,9 @@ export default function ChecksPanel(): React.JSX.Element { toast.error('Could not build the agent launch command.') return } - focusTerminalTabSurface(result.tabId) + if (result.tabId) { + focusTerminalTabSurface(result.tabId) + } toast.success('Started an AI agent for the conflicts.') } finally { setIsResolvingConflictsWithAI(false) @@ -1939,7 +1941,9 @@ export default function ChecksPanel(): React.JSX.Element { toast.error('Could not build the agent launch command.') return } - focusTerminalTabSurface(result.tabId) + if (result.tabId) { + focusTerminalTabSurface(result.tabId) + } } finally { setIsFixingChecksWithAI(false) } diff --git a/src/renderer/src/components/right-sidebar/SourceControl.tsx b/src/renderer/src/components/right-sidebar/SourceControl.tsx index 83b0572923b..cebcd464c33 100644 --- a/src/renderer/src/components/right-sidebar/SourceControl.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControl.tsx @@ -1770,7 +1770,9 @@ function SourceControlInner(): React.JSX.Element { return } - focusTerminalTabSurface(result.tabId) + if (result.tabId) { + focusTerminalTabSurface(result.tabId) + } toast.success('Started an AI agent for the conflicts.') } finally { setIsLaunchingConflictAgent(false) @@ -1843,7 +1845,9 @@ function SourceControlInner(): React.JSX.Element { return false } - focusTerminalTabSurface(result.tabId) + if (result.tabId) { + focusTerminalTabSurface(result.tabId) + } toast.success('Started an AI agent for the commit failure.') return true } finally { diff --git a/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx b/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx index e8004507211..8489eb55b74 100644 --- a/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx +++ b/src/renderer/src/components/tab-bar/QuickLaunchButton.tsx @@ -138,16 +138,22 @@ function QuickLaunchAgentMenuItemsInner({ toast.error(`Could not build launch command for ${label}.`) return } + if (!result.tabId) { + // Why: paired web clients create the tab on the host; focus follows the + // next session-tabs snapshot instead of a local tab id. + return + } onFocusTerminal(result.tabId) // Why: launch success means the terminal session exists. Agent readiness // can lag behind on slow machines, and prompt paste flows already own // their own readiness timeout once a PTY exists. - void waitForTerminalPty(result.tabId, 5000).then((hasPty) => { + const launchedTabId = result.tabId + void waitForTerminalPty(launchedTabId, 5000).then((hasPty) => { if (hasPty) { return } - const launchState = getTerminalLaunchState(result.tabId) + const launchState = getTerminalLaunchState(launchedTabId) if (!launchState.stillOpen) { return } diff --git a/src/renderer/src/components/terminal-pane/TerminalPane.tsx b/src/renderer/src/components/terminal-pane/TerminalPane.tsx index 27e7503f644..8ae1adfe552 100644 --- a/src/renderer/src/components/terminal-pane/TerminalPane.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalPane.tsx @@ -47,7 +47,11 @@ import { useNotificationDispatch } from './use-notification-dispatch' import { connectPanePty } from './pty-connection' import { shouldPreserveTerminalScrollbackBuffers } from '../../../../shared/workspace-session-terminal-buffers' import { getFitOverrideForPty, onOverrideChange } from '@/lib/pane-manager/mobile-fit-overrides' -import { getDriverForPty, onDriverChange } from '@/lib/pane-manager/mobile-driver-state' +import { + getDriverForPty, + isPtyLocked, + onDriverChange +} from '@/lib/pane-manager/mobile-driver-state' import { resolvePaneKeyForManager } from '@/lib/pane-manager/pane-key-resolution' import { safeFit } from '@/lib/pane-manager/pane-tree-ops' import { captureTerminalShutdownLayout } from './terminal-shutdown-layout-capture' @@ -1132,9 +1136,19 @@ export default function TerminalPane({ for (const pane of manager.getPanes()) { safeFit(pane) const transport = paneTransportsRef.current.get(pane.id) - if (transport?.isConnected()) { - transport.resize(pane.terminal.cols, pane.terminal.rows) + if (!transport?.isConnected()) { + continue } + const ptyId = transport.getPtyId() + if (!ptyId) { + continue + } + // Why: match pty-connection resize guards so web refit retries do not + // forward SIGWINCH while mobile-lock or phone-fit overrides are active. + if (getFitOverrideForPty(ptyId) || isPtyLocked(ptyId)) { + continue + } + transport.resize(pane.terminal.cols, pane.terminal.rows) } } const scheduleFrame = (): void => { diff --git a/src/renderer/src/components/terminal/terminal-tab-actions.test.ts b/src/renderer/src/components/terminal/terminal-tab-actions.test.ts index 790a1e36469..34b0caea979 100644 --- a/src/renderer/src/components/terminal/terminal-tab-actions.test.ts +++ b/src/renderer/src/components/terminal/terminal-tab-actions.test.ts @@ -15,7 +15,7 @@ const { getStateMock: vi.fn(), isWebRuntimeSessionActiveMock: vi.fn(), isWebTerminalSurfaceTabIdMock: vi.fn(() => false), - resolveHostSessionTabIdForWebSessionTabMock: vi.fn(() => null) + resolveHostSessionTabIdForWebSessionTabMock: vi.fn<() => string | null>(() => null) })) vi.mock('@/store', () => ({ diff --git a/src/renderer/src/components/terminal/terminal-tab-actions.ts b/src/renderer/src/components/terminal/terminal-tab-actions.ts index 3918acea991..5eba2fd7e39 100644 --- a/src/renderer/src/components/terminal/terminal-tab-actions.ts +++ b/src/renderer/src/components/terminal/terminal-tab-actions.ts @@ -84,7 +84,7 @@ export function closeTerminalTab(tabId: string): void { } const runtimeEnvironmentId = state.settings?.activeRuntimeEnvironmentId?.trim() - if (isWebRuntimeSessionActive(runtimeEnvironmentId)) { + if (runtimeEnvironmentId && isWebRuntimeSessionActive(runtimeEnvironmentId)) { const hostBackedTabId = resolveHostSessionTabIdForWebSessionTab(state, { environmentId: runtimeEnvironmentId, diff --git a/src/renderer/src/hooks/useAutomationDispatchEvents.ts b/src/renderer/src/hooks/useAutomationDispatchEvents.ts index 6836b695141..d0a948bc658 100644 --- a/src/renderer/src/hooks/useAutomationDispatchEvents.ts +++ b/src/renderer/src/hooks/useAutomationDispatchEvents.ts @@ -378,14 +378,18 @@ export function useAutomationDispatchEvents(): void { if (!result) { throw new Error('Unable to build an agent launch plan.') } - observeAgentStatus(result.tabId, dispatchStartedAt) + if (!result.tabId) { + throw new Error('Host-backed agent tabs are not yet trackable for automation dispatch.') + } + const launchedTabId = result.tabId + observeAgentStatus(launchedTabId, dispatchStartedAt) try { await markDispatchResult({ runId: run.id, status: 'dispatched', workspaceId: worktree.id, workspaceDisplayName: worktree.displayName, - terminalSessionId: result.tabId, + terminalSessionId: launchedTabId, precheckResult, error: null }) diff --git a/src/renderer/src/lib/launch-agent-in-new-tab.test.ts b/src/renderer/src/lib/launch-agent-in-new-tab.test.ts index 1a868322cf6..bebe6c714e8 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab.test.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab.test.ts @@ -11,7 +11,7 @@ const mockTrack = vi.fn() const LEAF_ID = '11111111-1111-4111-8111-111111111111' const store = { - settings: { agentCmdOverrides: {} }, + settings: { agentCmdOverrides: {}, activeRuntimeEnvironmentId: null as string | null }, tabsByWorktree: { 'wt-1': [{ id: 'tab-1' }] }, diff --git a/src/renderer/src/lib/sidebar-worktree-activation.ts b/src/renderer/src/lib/sidebar-worktree-activation.ts index fdbbe49e711..ba0d160b2b6 100644 --- a/src/renderer/src/lib/sidebar-worktree-activation.ts +++ b/src/renderer/src/lib/sidebar-worktree-activation.ts @@ -17,6 +17,7 @@ export function cancelPendingSidebarWorktreeActivation(): void { } function shouldDeferSidebarWorktreeActivation(worktreeId: string): boolean { + // Why: web clients should activate immediately to avoid host/session churn and wake lag. if ((globalThis as { __ORCA_WEB_CLIENT__?: boolean }).__ORCA_WEB_CLIENT__ === true) { return false }