fix(native-chat): route structured sessions by execution host

This commit is contained in:
Merge Sim
2026-08-31 17:44:31 -07:00
parent b7201a8051
commit 4f8ba8ded4
4 changed files with 56 additions and 8 deletions
@@ -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({
@@ -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<AgentSessionAttachResult>
>({ 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}`
)
@@ -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<string>
sessionId?: string
target: RuntimeClientTarget
visibilityUnknown: boolean
}
@@ -48,8 +52,19 @@ function trackLaunchSettlement(
)
}
async function verifyPublishedSession(worktreeId: string, sessionId: string): Promise<string> {
const snapshots = await refreshLocalStructuredSessionTabs()
async function verifyPublishedSession(
worktreeId: string,
sessionId: string,
target: RuntimeClientTarget
): Promise<string> {
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) {
@@ -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)