From 7bff50b45745473c1bfb964f5cb32ffaa593b5ce Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:58:12 -0700 Subject: [PATCH] fix(native-chat): a new chat in a twin folder, or a twin with no copy picked, refuses instead of starting on this computer (#30838) --- .../runtime/structured-agent-session-owner.ts | 8 ++-- .../store/host-twin-owner-refusals.test.ts | 47 +++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/runtime/structured-agent-session-owner.ts b/src/renderer/src/runtime/structured-agent-session-owner.ts index bd3dcc09a7b..fecda534f39 100644 --- a/src/renderer/src/runtime/structured-agent-session-owner.ts +++ b/src/renderer/src/runtime/structured-agent-session-owner.ts @@ -7,7 +7,7 @@ import { import type { Tab } from '../../../shared/tab-types' import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner' import type { WorktreeRuntimeOwnerState } from '@/lib/worktree-runtime-owner-state' -import { resolveIndexedWorktreeOwner } from '@/lib/worktree-runtime-owner-index' +import { isTwinWorkspace, namedHostForOwnerRef } from '@/lib/resolve-owner' import { LOCAL_STRUCTURED_SESSION_OWNER } from './local-structured-session-owner' import { runtimeRpcTargetForAuthorityKey } from './runtime-client-target' import type { RuntimeClientTarget } from '../../../shared/runtime-client-target' @@ -33,9 +33,11 @@ export function resolveStructuredAgentSessionOwner( state: WorktreeRuntimeOwnerState, worktreeId: string ): ExecutionHostId | null { + // Why resolveOwner's twin rule: an id-only check missed folder twins and an active twin with no + // host picked, and the fallback below reads both as this machine. if ( - state.activeWorktreeId !== worktreeId && - resolveIndexedWorktreeOwner(state.worktreesByRepo, worktreeId).kind === 'ambiguous' + !namedHostForOwnerRef(state, { workspaceId: worktreeId }) && + isTwinWorkspace(state, worktreeId) ) { return null } diff --git a/src/renderer/src/store/host-twin-owner-refusals.test.ts b/src/renderer/src/store/host-twin-owner-refusals.test.ts index a009a9ca55c..05699e3d755 100644 --- a/src/renderer/src/store/host-twin-owner-refusals.test.ts +++ b/src/renderer/src/store/host-twin-owner-refusals.test.ts @@ -45,6 +45,8 @@ const { resolveNativeChatSkillDiscoveryContext, selectNativeChatSkillStateInputs await import('@/components/native-chat/native-chat-skill-discovery-context') const { mirroredRuntimeEnvironmentIdForWorkspace } = await import('@/runtime/web-session-tabs-sync/mirrored-workspace-owner') +const { resolveStructuredAgentSessionOwner } = + await import('@/runtime/structured-agent-session-owner') const RUNTIME_HOST: ExecutionHostId = `runtime:${TWIN_RUNTIME_ENV}` @@ -213,6 +215,51 @@ describe('folder workspace twin', () => { ) }) +describe('a new structured chat', () => { + it.each(['folder local+ssh', 'folder group local+ssh'])( + 'refuses a %s twin instead of starting on this computer', + (kind) => { + const { store, workspaceKey } = bind(createHostTwinFixture(kind)) + expect(resolveStructuredAgentSessionOwner(store.getState(), workspaceKey)).toBeNull() + store.setState({ activeWorktreeId: workspaceKey, activeWorkspaceExecutionHostId: null }) + expect(resolveStructuredAgentSessionOwner(store.getState(), workspaceKey)).toBeNull() + } + ) + + it('refuses an active worktree twin with no copy picked, and starts on the picked one', () => { + const { store, workspaceKey } = bind(createHostTwinFixture('local+runtime')) + store.setState({ activeWorktreeId: workspaceKey, activeWorkspaceExecutionHostId: null }) + expect(resolveStructuredAgentSessionOwner(store.getState(), workspaceKey)).toBeNull() + store.setState({ activeWorkspaceExecutionHostId: RUNTIME_HOST }) + expect(resolveStructuredAgentSessionOwner(store.getState(), workspaceKey)).toBe(RUNTIME_HOST) + }) + + it("starts on the copy the user picked, and on a lone unstamped folder's computer", () => { + const { store, workspaceKey, projectId } = bind(createHostTwinFixture('folder local+ssh')) + store.setState({ activeWorktreeId: workspaceKey, activeWorkspaceExecutionHostId: 'local' }) + expect(resolveStructuredAgentSessionOwner(store.getState(), workspaceKey)).toBe('local') + store.setState({ + activeWorktreeId: null, + activeWorkspaceExecutionHostId: null, + projectGroups: [], + folderWorkspaces: [makeFolderWorkspace({ id: projectId, folderPath: '/work/twin' })] + }) + expect(resolveStructuredAgentSessionOwner(store.getState(), workspaceKey)).toBe('local') + }) + + it('starts a lone unstamped worktree on this computer', () => { + const fixture = bind(createHostTwinFixture('local+ssh')) + const { store, workspaceKey, projectId } = fixture + store.setState({ + repos: store.getState().repos.filter((repo) => !repo.connectionId), + worktreesByRepo: { + [projectId]: [makeWorktree({ id: workspaceKey, repoId: projectId, path: '/work/twin' })] + } + }) + expect(resolveStructuredAgentSessionOwner(store.getState(), workspaceKey)).toBe('local') + }) +}) + describe('a workspace selected on the unresolved-owner sentinel', () => { it('neither reopens a terminal locally nor uploads an attachment', () => { const { store, workspaceKey } = runtimeOnly()