From 4f8ba8ded42cded56a274eb130af64dc18bd5fa5 Mon Sep 17 00:00:00 2001 From: Merge Sim Date: Mon, 31 Aug 2026 16:07:29 -0700 Subject: [PATCH] fix(native-chat): route structured sessions by execution host --- .../components/terminal-pane/TerminalPane.tsx | 5 ++- .../lib/launch-structured-agent-session.ts | 20 ++++++++++-- .../lib/structured-agent-session-launch.ts | 31 ++++++++++++++++--- .../structured-native-chat-availability.ts | 8 +++++ 4 files changed, 56 insertions(+), 8 deletions(-) diff --git a/src/renderer/src/components/terminal-pane/TerminalPane.tsx b/src/renderer/src/components/terminal-pane/TerminalPane.tsx index 1780385bd11..60662d4cb73 100644 --- a/src/renderer/src/components/terminal-pane/TerminalPane.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalPane.tsx @@ -3121,7 +3121,10 @@ function TerminalPane( structuredSessionAgent, terminalAgent: chatPaneResolvedAgent ?? chatPaneLaunchAgent }) - const structuredChatTarget = useMemo(() => ({ kind: 'local' as const }), []) + const structuredChatTarget = useMemo(() => { + const environmentId = getRuntimeEnvironmentIdForWorktree(useAppStore.getState(), worktreeId) + return environmentId ? { kind: 'environment' as const, environmentId } : { kind: 'local' as const } + }, [worktreeId]) // The launch draft is keyed by tab, so gate it on the same pane ownership the // launch agent uses: a split sibling must not inherit the seeded text. const chatPaneOwnsTabWideLaunchDraft = nativeChatLeafOwnsTabWideEvidence({ diff --git a/src/renderer/src/lib/launch-structured-agent-session.ts b/src/renderer/src/lib/launch-structured-agent-session.ts index 8d695311319..f77b0f29155 100644 --- a/src/renderer/src/lib/launch-structured-agent-session.ts +++ b/src/renderer/src/lib/launch-structured-agent-session.ts @@ -16,11 +16,18 @@ import { resolveWebSessionVisibleTabId } from '@/runtime/web-session-focus-intent' import { LOCAL_STRUCTURED_SESSION_OWNER } from '@/runtime/local-structured-session-tabs-sync' +import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' +import type { RuntimeClientTarget } from '@/runtime/runtime-client-target' function newSessionId(agent: AgentSessionHandleProvider): string { return `${agent}_${crypto.randomUUID().replaceAll('-', '_')}` } +function targetForWorktree(worktreeId: string): RuntimeClientTarget { + const environmentId = getRuntimeEnvironmentIdForWorktree(useAppStore.getState(), worktreeId) + return environmentId ? { kind: 'environment', environmentId } : { kind: 'local' } +} + export async function launchStructuredAgentSession( worktreeId: string, agent: AgentSessionHandleProvider @@ -28,8 +35,12 @@ export async function launchStructuredAgentSession( const sessionId = newSessionId(agent) const fields = { worktree: toRuntimeWorktreeSelector(worktreeId), agent } const state = useAppStore.getState() + const target = targetForWorktree(worktreeId) recordWebSessionFocusIntent( - { environmentId: LOCAL_STRUCTURED_SESSION_OWNER }, + { + environmentId: + target.kind === 'environment' ? target.environmentId : LOCAL_STRUCTURED_SESSION_OWNER + }, worktreeId, `agent-session:${sessionId}`, undefined, @@ -38,7 +49,7 @@ export async function launchStructuredAgentSession( try { const result = await callStructuredAgentSession< AgentSessionMutationResult - >({ kind: 'local' }, 'agentSession.create', { + >(target, 'agentSession.create', { envelope: { sessionId, clientOperationId: createStructuredAgentSessionOperationId(() => crypto.randomUUID()), @@ -59,7 +70,10 @@ export async function launchStructuredAgentSession( // A concurrent create may have replaced this intent. Only clear the failed // session's slot; never erase a later successful create's focus request. clearWebSessionFocusIntentIfMatches( - { environmentId: LOCAL_STRUCTURED_SESSION_OWNER }, + { + environmentId: + target.kind === 'environment' ? target.environmentId : LOCAL_STRUCTURED_SESSION_OWNER + }, worktreeId, `agent-session:${sessionId}` ) diff --git a/src/renderer/src/lib/structured-agent-session-launch.ts b/src/renderer/src/lib/structured-agent-session-launch.ts index 0ce3e715fbb..1223040276d 100644 --- a/src/renderer/src/lib/structured-agent-session-launch.ts +++ b/src/renderer/src/lib/structured-agent-session-launch.ts @@ -12,10 +12,14 @@ import { refreshLocalStructuredSessionTabs } from '@/runtime/local-structured-se import { callStructuredAgentSession } from '@/runtime/structured-agent-session-client' import type { RuntimeMobileSessionTabsResult } from '../../../shared/runtime-types' import { translate } from '@/i18n/i18n' +import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' +import type { RuntimeClientTarget } from '@/runtime/runtime-client-target' +import { useAppStore } from '@/store' type StructuredLaunchState = { promise: Promise sessionId?: string + target: RuntimeClientTarget visibilityUnknown: boolean } @@ -48,8 +52,19 @@ function trackLaunchSettlement( ) } -async function verifyPublishedSession(worktreeId: string, sessionId: string): Promise { - const snapshots = await refreshLocalStructuredSessionTabs() +async function verifyPublishedSession( + worktreeId: string, + sessionId: string, + target: RuntimeClientTarget +): Promise { + const snapshots = + target.kind === 'local' + ? await refreshLocalStructuredSessionTabs() + : await callStructuredAgentSession<{ snapshots?: RuntimeMobileSessionTabsResult[] }>( + target, + 'session.tabs.listAll', + {} + ).then((result) => result.snapshots ?? []) const published = snapshots.some( (snapshot) => snapshot.worktree === worktreeId && @@ -70,7 +85,11 @@ function launchStructuredAgentSessionOnce( if (existing) { if (existing.visibilityUnknown && existing.sessionId) { existing.visibilityUnknown = false - existing.promise = verifyPublishedSession(worktreeId, existing.sessionId).catch((error) => { + existing.promise = verifyPublishedSession( + worktreeId, + existing.sessionId, + existing.target + ).catch((error) => { existing.visibilityUnknown = true throw error }) @@ -83,12 +102,16 @@ function launchStructuredAgentSessionOnce( // renderer; clearing here lets a rapid second click create a sibling chat. const state: StructuredLaunchState = { promise: Promise.resolve(''), + target: (() => { + const environmentId = getRuntimeEnvironmentIdForWorktree(useAppStore.getState(), worktreeId) + return environmentId ? { kind: 'environment', environmentId } : { kind: 'local' } + })(), visibilityUnknown: false } state.promise = launchStructuredAgentSession(worktreeId, agent) .then((sessionId) => { state.sessionId = sessionId - return verifyPublishedSession(worktreeId, sessionId) + return verifyPublishedSession(worktreeId, sessionId, state.target) }) .catch((error) => { if (state.sessionId) { diff --git a/src/renderer/src/lib/structured-native-chat-availability.ts b/src/renderer/src/lib/structured-native-chat-availability.ts index 9b737a7b158..8d1c4c7dfbd 100644 --- a/src/renderer/src/lib/structured-native-chat-availability.ts +++ b/src/renderer/src/lib/structured-native-chat-availability.ts @@ -4,6 +4,7 @@ import { getExecutionHostIdForWorktree, getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' +import { getRendererAppPlatform } from '@/lib/renderer-app-platform' import { STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY } from '../../../shared/protocol-version' export function canUseStructuredNativeChat(state: AppState, worktreeId: string): boolean { @@ -30,6 +31,13 @@ export function canUseStructuredNativeChat(state: AppState, worktreeId: string): : undefined return status?.capabilities?.includes(STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY) === true } + // The shipped Windows process-tree addon may not expose creation time. Until + // the host advertises that proof, refuse every local Windows execution path — + // windows-host, WSL, and keys that resolve no project runtime (folder + // workspaces, floating terminal) — so create cannot fail after the click. + if (getRendererAppPlatform() === 'win32') { + return false + } // Refuse WSL and repair-required runtimes; Windows native execution is // supported when the host advertises the process identity capability. const projectRuntime = getLocalProjectExecutionRuntimeContext(state, worktreeId)