diff --git a/config/focus-setting-read-baseline.txt b/config/focus-setting-read-baseline.txt index d7aba00c714..789b4b18901 100644 --- a/config/focus-setting-read-baseline.txt +++ b/config/focus-setting-read-baseline.txt @@ -4,7 +4,7 @@ # the default host, through defaultCreationHost. Everything else routes by the owner. # Prune after removing reads: pnpm check:focus-read-ratchet --prune 2 src/renderer/src/App.tsx -5 src/renderer/src/app-shell/use-app-shell-services.ts +3 src/renderer/src/app-shell/use-app-shell-services.ts 1 src/renderer/src/components/JiraIssueWorkspace.tsx 1 src/renderer/src/components/Landing.tsx 2 src/renderer/src/components/LinearIssueTextEditor.tsx @@ -24,23 +24,16 @@ 1 src/renderer/src/components/automations/use-automations-page-destination-form.ts 2 src/renderer/src/components/automations/use-automations-page-setup-state.ts 1 src/renderer/src/components/browser-pane/ReopenBrowserPageOnServerButton.tsx -1 src/renderer/src/components/browser-pane/assemble-chrome/browser-egress-indicator.tsx -1 src/renderer/src/components/browser-pane/assemble-chrome/browser-page-viewport-overlays.tsx 1 src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.tsx -1 src/renderer/src/components/browser-pane/assemble-chrome/ssh-routed-browser-page-gate.tsx 1 src/renderer/src/components/browser-pane/browser-reopen-on-server.ts 1 src/renderer/src/components/browser-pane/stream-remote/remote-browser-page-pane.tsx 1 src/renderer/src/components/browser-pane/stream-remote/use-remote-browser-page-lifecycle.ts 1 src/renderer/src/components/browser-pane/stream-remote/use-remote-browser-page-stream.ts 1 src/renderer/src/components/browser-pane/stream-remote/use-remote-browser-stream-activation.ts -2 src/renderer/src/components/browser-pane/use-ssh-workspace-browser-route.ts -1 src/renderer/src/components/browser-pane/workspace-doc/HtmlDocPreview.tsx 1 src/renderer/src/components/browser-pane/workspace-doc/doc-preview-document-actions.ts 1 src/renderer/src/components/browser-pane/workspace-doc/workspace-doc-page-pane.tsx 2 src/renderer/src/components/cmd-j/quick-action-context.ts -2 src/renderer/src/components/dashboard/dashboard-card-terminal-input.ts -1 src/renderer/src/components/dashboard/launch-dashboard-agent.ts -1 src/renderer/src/components/dashboard/use-agent-row-conversation-name.ts +1 src/renderer/src/components/dashboard/dashboard-card-terminal-input.ts 2 src/renderer/src/components/editor/EditorContent.tsx 1 src/renderer/src/components/editor/EditorDiffFileSurface.tsx 1 src/renderer/src/components/editor/EditorEditFileSurface.tsx @@ -76,21 +69,12 @@ 1 src/renderer/src/components/linear-item-drawer-edit-section.tsx 2 src/renderer/src/components/native-chat-resume-modal.test-support.tsx 1 src/renderer/src/components/native-chat-resume-tree.test-support.ts -1 src/renderer/src/components/native-chat/NativeChatAgentMessageSenders.tsx 1 src/renderer/src/components/native-chat/NativeChatResolvedView.tsx 1 src/renderer/src/components/native-chat/NativeChatVisualFrame.tsx -1 src/renderer/src/components/native-chat/StructuredAgentSessionAttentionBridge.tsx -1 src/renderer/src/components/native-chat/StructuredAgentSessionPaneOverlayLayer.tsx -1 src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.tsx -5 src/renderer/src/components/native-chat/native-chat-agent-message-sender-label.ts -2 src/renderer/src/components/native-chat/native-chat-attachment-upload.ts 1 src/renderer/src/components/native-chat/native-chat-file-link.ts 3 src/renderer/src/components/native-chat/native-chat-http-link-source-owner.ts 1 src/renderer/src/components/native-chat/native-chat-link-target-actions.ts 2 src/renderer/src/components/native-chat/native-chat-runtime-owner.ts -1 src/renderer/src/components/native-chat/native-chat-skill-discovery-context.ts -1 src/renderer/src/components/native-chat/use-native-chat-file-drops.ts -2 src/renderer/src/components/native-chat/use-native-chat-workspace-file-drop.ts 1 src/renderer/src/components/new-workspace/use-jira-source-connection.ts 4 src/renderer/src/components/new-workspace/use-linear-source-connection.ts 1 src/renderer/src/components/new-workspace/use-smart-workspace-name-field-controller.ts @@ -102,8 +86,6 @@ 2 src/renderer/src/components/right-sidebar/FileExplorer.tsx 1 src/renderer/src/components/right-sidebar/FileExplorerFilesTreePane.tsx 1 src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksRow.tsx -1 src/renderer/src/components/right-sidebar/ai-vault-original-pane-actions.ts -1 src/renderer/src/components/right-sidebar/ai-vault-session-resume-in-chat-workspace.ts 3 src/renderer/src/components/right-sidebar/checks-panel-hosted-review-click-routing.ts 1 src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-controller-state.tsx 1 src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-create-review.tsx @@ -115,9 +97,9 @@ 2 src/renderer/src/components/right-sidebar/use-file-explorer-tree-pane-state.ts 1 src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.ts 4 src/renderer/src/components/right-sidebar/useFileSearchPanel.ts -2 src/renderer/src/components/right-sidebar/useFileSearchRunner.ts -2 src/renderer/src/components/right-sidebar/useFileSearchScope.ts -2 src/renderer/src/components/right-sidebar/useGitStatusPolling.ts +1 src/renderer/src/components/right-sidebar/useFileSearchRunner.ts +1 src/renderer/src/components/right-sidebar/useFileSearchScope.ts +1 src/renderer/src/components/right-sidebar/useGitStatusPolling.ts 1 src/renderer/src/components/settings/BaseRefPicker.tsx 2 src/renderer/src/components/settings/BrowserPane.tsx 1 src/renderer/src/components/settings/CliSkillRuntimeSetup.tsx @@ -161,7 +143,6 @@ 1 src/renderer/src/components/sidebar/SidebarTaskNavButton.tsx 1 src/renderer/src/components/sidebar/WorkspaceAttachmentSearch.tsx 1 src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.tsx -1 src/renderer/src/components/sidebar/WorktreeCardPorts.tsx 1 src/renderer/src/components/sidebar/WorktreeList.tsx 1 src/renderer/src/components/sidebar/WorktreeRowOpenInSubMenu.tsx # reads focus through resolveOwnerWithLegacyFallback, newly counted (round 3 F2); F8 nets it down 1 src/renderer/src/components/sidebar/WorktreeVisibilityDialog.tsx @@ -196,10 +177,8 @@ 1 src/renderer/src/components/skills/SkillsPage.tsx 2 src/renderer/src/components/status-bar/ClaudeSwitcherMenu.tsx 1 src/renderer/src/components/status-bar/CodexSwitcherMenu.tsx -1 src/renderer/src/components/status-bar/PortsStatusSegment.tsx 1 src/renderer/src/components/status-bar/SshStatusSegment.tsx 1 src/renderer/src/components/status-bar/StatusBarSurface.tsx -1 src/renderer/src/components/status-bar/ports-status-popover-rows.tsx 2 src/renderer/src/components/status-bar/provider-account-sync-key.ts 1 src/renderer/src/components/status-bar/usage-provider-settings-target.ts 2 src/renderer/src/components/status-bar/use-codex-switcher-controller.ts @@ -207,11 +186,9 @@ 1 src/renderer/src/components/status-bar/use-workspace-space-git-refresh-action.ts 1 src/renderer/src/components/status-bar/use-workspace-space-manager-panel.ts 1 src/renderer/src/components/status-bar/workspace-space-decision-details.ts -1 src/renderer/src/components/tab-bar/QuickLaunchButton.tsx 1 src/renderer/src/components/tab-bar/TabBar.tsx -1 src/renderer/src/components/tab-bar/TabBarItemRow.tsx 3 src/renderer/src/components/tab-bar/tab-bar-windows-shell-launch-test-harness.ts -4 src/renderer/src/components/tab-bar/use-tab-bar-runtime-model.ts +3 src/renderer/src/components/tab-bar/use-tab-bar-runtime-model.ts 2 src/renderer/src/components/tab-bar/web-runtime-tab-move-mirror.ts 1 src/renderer/src/components/tab-bar/windows-shell-menu-visibility.ts 2 src/renderer/src/components/tab-group/useTabGroupActivationCommands.ts @@ -221,21 +198,15 @@ 9 src/renderer/src/components/task-page/TaskPage.tsx 2 src/renderer/src/components/task-page/jira/IssueAssigneeField.tsx 1 src/renderer/src/components/task-page/linear/Filters.tsx -1 src/renderer/src/components/terminal-pane/TerminalPaneSurface.tsx 1 src/renderer/src/components/terminal-pane/pending-split-close-test-fixture.ts 1 src/renderer/src/components/terminal-pane/pty-connection/agent-idle-working-handlers.ts -1 src/renderer/src/components/terminal-pane/pty-connection/interrupt-input-intent.ts -1 src/renderer/src/components/terminal-pane/pty-connection/pane-agent-identity.ts -1 src/renderer/src/components/terminal-pane/pty-connection/pane-host-capability-flags.ts 1 src/renderer/src/components/terminal-pane/retire-unbound-runtime-terminal-pane.ts -1 src/renderer/src/components/terminal-pane/terminal-ctrl-arrow-conpty.ts 1 src/renderer/src/components/terminal-pane/terminal-fit-restore.ts -2 src/renderer/src/components/terminal-pane/terminal-input-host-platform.ts +1 src/renderer/src/components/terminal-pane/terminal-input-host-platform.ts 1 src/renderer/src/components/terminal-pane/terminal-keyboard-hook.ts 1 src/renderer/src/components/terminal-pane/terminal-keyboard-runtime.ts 1 src/renderer/src/components/terminal-pane/terminal-link-open-hints.ts 2 src/renderer/src/components/terminal-pane/terminal-pane-close-admission.ts -1 src/renderer/src/components/terminal-pane/terminal-pane-manager-options.ts 2 src/renderer/src/components/terminal-pane/terminal-pane-menu-paste.ts 3 src/renderer/src/components/terminal-pane/terminal-pane-mount-preparation.ts 1 src/renderer/src/components/terminal-pane/terminal-pane-paste-execution.ts @@ -286,9 +257,7 @@ 1 src/renderer/src/hooks/ipc-events/tab-lifecycle-ipc-bridge.ts 1 src/renderer/src/hooks/ipc-events/terminal-request-ipc-bridge.ts 2 src/renderer/src/hooks/use-active-skill-discovery-runtime-target.ts -1 src/renderer/src/hooks/use-terminal-quick-command-hosts.ts 1 src/renderer/src/hooks/useActiveProjectSkillRuntime.ts -2 src/renderer/src/hooks/useAgentDetectionTarget.ts 1 src/renderer/src/hooks/useEditorExternalWatch.ts 1 src/renderer/src/hooks/useInstalledAgentSkills.ts 1 src/renderer/src/hooks/useIpcEvents.ts @@ -297,28 +266,18 @@ 7 src/renderer/src/hooks/useSettingsNavigationMetadata.ts 1 src/renderer/src/hooks/useUnreadDockBadge.ts 2 src/renderer/src/hooks/useWindowsTerminalCapabilityOwnerKey.ts -4 src/renderer/src/lib/activate-ai-vault-structured-session.ts +3 src/renderer/src/lib/activate-ai-vault-structured-session.ts 1 src/renderer/src/lib/active-agent-note-send.ts 1 src/renderer/src/lib/active-agent-note-target.ts -1 src/renderer/src/lib/agent-account-restart-startup.ts 1 src/renderer/src/lib/agent-background-session-launch-host.ts -1 src/renderer/src/lib/agent-detection-target-inventory.ts 1 src/renderer/src/lib/agent-hibernation-coordinator-test-fixture.ts -2 src/renderer/src/lib/agent-launch-route-input.ts -1 src/renderer/src/lib/agent-message-send.ts -1 src/renderer/src/lib/ai-vault-resume-platform.ts -1 src/renderer/src/lib/ai-vault-tab-title-requests.ts -2 src/renderer/src/lib/ai-vault-tab-title-sync-inputs.ts 1 src/renderer/src/lib/client-creation-action-error.ts 3 src/renderer/src/lib/client-creation-action-policy.ts 1 src/renderer/src/lib/default-creation-host.ts # the sanctioned reader of the default host 1 src/renderer/src/lib/doc-preview-grants.ts # reads focus through resolveOwnerWithLegacyFallback, newly counted (round 3 F2); F8 nets it down -1 src/renderer/src/lib/empty-workspace-default-agent-chat.ts -3 src/renderer/src/lib/execute-open-editor-path-move.ts -1 src/renderer/src/lib/execution-host-display-label.ts -2 src/renderer/src/lib/execution-host-facts.ts # reads focus through resolveOwnerWithLegacyFallback, newly counted (round 3 F2); F8 nets it down +1 src/renderer/src/lib/execute-open-editor-path-move.ts +1 src/renderer/src/lib/execution-host-facts.ts # reads focus through resolveOwnerWithLegacyFallback, newly counted (round 3 F2); F8 nets it down 5 src/renderer/src/lib/file-preview.ts -2 src/renderer/src/lib/folder-workspace-runtime-owner.ts 6 src/renderer/src/lib/http-link-destinations.ts 3 src/renderer/src/lib/http-link-routing.ts 1 src/renderer/src/lib/launch-agent-background-session.ts @@ -326,42 +285,32 @@ 2 src/renderer/src/lib/launch-agent-in-new-tab.ts 2 src/renderer/src/lib/launch-agent-web-host-tab.ts 2 src/renderer/src/lib/launch-ai-vault-session.ts -1 src/renderer/src/lib/launch-structured-agent-session.ts 1 src/renderer/src/lib/linear-issue-url-lookup.ts 1 src/renderer/src/lib/local-preflight-context.ts -3 src/renderer/src/lib/open-agent-message-sender.ts 2 src/renderer/src/lib/provider-runtime-context.ts -2 src/renderer/src/lib/remap-open-editor-tabs-for-path-change.ts 1 src/renderer/src/lib/remote-workspace-drop-test-fixtures.ts 2 src/renderer/src/lib/repo-runtime-owner.ts 1 src/renderer/src/lib/runtime-session-mirror-owners.ts 1 src/renderer/src/lib/runtime-session-mirror-targets.ts -1 src/renderer/src/lib/runtime-workspace-file-route.ts 1 src/renderer/src/lib/single-runtime-legacy-owner.ts -1 src/renderer/src/lib/sleeping-agent-session-launch.ts -1 src/renderer/src/lib/structured-agent-session-launch.ts -1 src/renderer/src/lib/structured-agent-session-provisional-tab.ts -4 src/renderer/src/lib/structured-agent-session-tab-activation.ts +1 src/renderer/src/lib/structured-agent-session-tab-activation.ts 1 src/renderer/src/lib/tab-number-shortcuts.ts 1 src/renderer/src/lib/terminal-close-route.ts 1 src/renderer/src/lib/terminal-tab-owner-activation.ts 6 src/renderer/src/lib/terminal-worktree-route.ts # resolveTerminalPtyOwner: teardown, close and PTY identity read spawn's attribution (round 3 F8), which retired the deprecated resolver's readers 3 src/renderer/src/lib/unread-badge-count-selector.ts 2 src/renderer/src/lib/web-runtime-worktree-terminal-after-wake.ts -5 src/renderer/src/lib/workspace-browser-tab-open.ts +4 src/renderer/src/lib/workspace-browser-tab-open.ts 1 src/renderer/src/lib/workspace-file-browser-open-target.ts # moved out of file-preview.ts (round 3 F7), same read -1 src/renderer/src/lib/workspace-file-drag-source.ts 1 src/renderer/src/lib/workspace-tab-palette-activation.ts 1 src/renderer/src/lib/worktree-activation-pty-inventory.ts # reads focus through resolveOwnerWithLegacyFallback, newly counted (round 3 F2); F8 nets it down -3 src/renderer/src/lib/worktree-activation.ts +2 src/renderer/src/lib/worktree-activation.ts 1 src/renderer/src/lib/worktree-creation-agent-seeds.ts 1 src/renderer/src/lib/worktree-jump-navigation.ts -2 src/renderer/src/lib/worktree-operation-route.ts 1 src/renderer/src/lib/worktree-row-open-in-owner.ts # reads focus through resolveOwnerWithLegacyFallback, newly counted (round 3 F2); F8 nets it down -8 src/renderer/src/lib/worktree-runtime-owner.ts # typed owner resolver and the legacy-fallback adapter the folds moved to (round 3 F0); F8 deleted the deprecated pair +3 src/renderer/src/lib/worktree-runtime-owner.ts # typed owner resolver and the legacy-fallback adapter the folds moved to (round 3 F0); F8 deleted the deprecated pair 1 src/renderer/src/lib/worktree-sort-order-host-split.ts 1 src/renderer/src/lib/worktree-sort-order-persistence.ts # its split helper calls resolveOwnerWithLegacyFallback directly (round 3 F4); F8 nets it down -1 src/renderer/src/runtime/local-structured-session-tabs-sync/snapshot-apply.ts 17 src/renderer/src/runtime/runtime-jira-client.ts 2 src/renderer/src/runtime/runtime-jira-summary-client.ts 1 src/renderer/src/runtime/runtime-jira-target.ts @@ -369,10 +318,7 @@ 8 src/renderer/src/runtime/runtime-linear-client.ts 6 src/renderer/src/runtime/runtime-linear-issue-mutations.ts 12 src/renderer/src/runtime/runtime-linear-project-client.ts -3 src/renderer/src/runtime/structured-agent-session-owner.ts -1 src/renderer/src/runtime/structured-conversation-name.ts 5 src/renderer/src/runtime/use-runtime-session-mirror-environment-key.ts -1 src/renderer/src/runtime/use-worktree-runtime-target.ts 2 src/renderer/src/runtime/web-runtime-browser-creation.ts 1 src/renderer/src/runtime/web-runtime-initial-terminal-bootstrap-dispatch.ts 1 src/renderer/src/runtime/web-runtime-session-tab-lifecycle.ts @@ -384,7 +330,6 @@ 2 src/renderer/src/runtime/web-session-tabs-sync/global-session-subscription.ts 1 src/renderer/src/runtime/web-session-tabs-sync/tracking-decisions.ts 3 src/renderer/src/runtime/web-session-tabs-sync/use-web-session-tabs-sync.ts -1 src/renderer/src/store/folder-workspaces/folder-workspace-mutations.ts 1 src/renderer/src/store/github/repository-routing.ts 2 src/renderer/src/store/github/visible-hosted-review-refresh-targets.ts 2 src/renderer/src/store/github/work-item-aggregate-actions.ts @@ -393,25 +338,19 @@ 9 src/renderer/src/store/index.ts 2 src/renderer/src/store/project-groups/project-group-mutations.ts 1 src/renderer/src/store/repos/all-host-repo-catalog-actions.ts -1 src/renderer/src/store/repos/converted-ssh-browser-pages.ts 2 src/renderer/src/store/repos/repo-add-actions.ts 1 src/renderer/src/store/repos/repo-update.ts 1 src/renderer/src/store/repos/runtime-repo-catalog-actions.ts 1 src/renderer/src/store/slices/browser-slice-test-harness.ts -1 src/renderer/src/store/slices/browser/browser-close-actions.ts 16 src/renderer/src/store/slices/browser/browser-cookie-import-actions.ts 1 src/renderer/src/store/slices/browser/browser-host-actions.ts -10 src/renderer/src/store/slices/browser/browser-host-state.ts -2 src/renderer/src/store/slices/browser/browser-page-conversion-actions.ts -1 src/renderer/src/store/slices/browser/browser-page-focus-actions.ts +7 src/renderer/src/store/slices/browser/browser-host-state.ts +1 src/renderer/src/store/slices/browser/browser-page-conversion-actions.ts 14 src/renderer/src/store/slices/browser/browser-profile-import-actions.ts 14 src/renderer/src/store/slices/browser/browser-profile-list-actions.ts -7 src/renderer/src/store/slices/browser/browser-tab-actions.ts -1 src/renderer/src/store/slices/browser/browser-tab-focus-actions.ts +6 src/renderer/src/store/slices/browser/browser-tab-actions.ts 1 src/renderer/src/store/slices/browser/create-browser-slice.ts 1 src/renderer/src/store/slices/browser/paired-browser-tab-creator.ts -2 src/renderer/src/store/slices/diff-comment-host-write.ts -1 src/renderer/src/store/slices/diff-comment-persistence.ts 2 src/renderer/src/store/slices/editor-slice-test-harness.ts 1 src/renderer/src/store/slices/editor/actions/markdown-link-action.ts 1 src/renderer/src/store/slices/editor/actions/markdown-preview-actions.ts @@ -444,12 +383,11 @@ 2 src/renderer/src/store/slices/preflight.ts 1 src/renderer/src/store/slices/repo-host-identity.ts 1 src/renderer/src/store/slices/repo-reorder-host-split.ts -4 src/renderer/src/store/slices/repos.ts +3 src/renderer/src/store/slices/repos.ts 1 src/renderer/src/store/slices/runtime-status.ts 2 src/renderer/src/store/slices/settings-owner-hydration-publication.ts 5 src/renderer/src/store/slices/settings.ts 8 src/renderer/src/store/slices/store-test-helpers.ts -1 src/renderer/src/store/slices/tabs/tabs-close-actions.ts 2 src/renderer/src/store/slices/tabs/tabs-host-mirroring.ts 1 src/renderer/src/store/slices/terminal-tab-retirement.ts 3 src/renderer/src/store/slices/workspace-attention/workspace-unread-characterization-fixture.ts @@ -466,7 +404,6 @@ 1 src/renderer/src/store/slices/worktrees/metadata/worktree-lineage-actions.ts 3 src/renderer/src/store/slices/worktrees/metadata/worktree-lineage-host-merge.ts 3 src/renderer/src/store/slices/worktrees/session/worktree-terminal-reference.ts -1 src/renderer/src/store/slices/worktrees/teardown/purged-structured-launch-retirement.ts 2 src/renderer/src/store/terminals/terminal-active-workspace-creation.ts 1 src/renderer/src/store/terminals/terminal-tab-appearance.ts 1 src/renderer/src/store/terminals/terminal-tab-close-providers.ts diff --git a/src/renderer/src/components/automations/use-automations-page-setup-state.test.tsx b/src/renderer/src/components/automations/use-automations-page-setup-state.test.tsx index 47befa87f4f..61bb1ddae58 100644 --- a/src/renderer/src/components/automations/use-automations-page-setup-state.test.tsx +++ b/src/renderer/src/components/automations/use-automations-page-setup-state.test.tsx @@ -46,7 +46,7 @@ describe('automation hooks cache key', () => { 'owner:repo-1' ], ['local owner', [{ id: 'repo-1', executionHostId: 'local' }], 'focused', 'local:repo-1'], - ['unstamped row follows focus', [{ id: 'repo-1' }], 'focused', 'focused:repo-1'], + ['unstamped row stays local', [{ id: 'repo-1' }], 'focused', 'local:repo-1'], ['missing row follows focus', [], 'focused', 'focused:repo-1'], ['missing row, local focus', [], null, 'local:repo-1'], [ diff --git a/src/renderer/src/components/editor/editor-model-owner.ts b/src/renderer/src/components/editor/editor-model-owner.ts index 75f1ab422e1..9115c03b521 100644 --- a/src/renderer/src/components/editor/editor-model-owner.ts +++ b/src/renderer/src/components/editor/editor-model-owner.ts @@ -1,6 +1,6 @@ import type { OpenFile } from '@/store/slices/editor' import type { WorktreeOperationRouteState } from '@/lib/worktree-operation-route' -import { resolveWriteRoute } from '@/lib/resolve-owner' +import { resolveWorktreeOperationRoute } from '@/lib/worktree-operation-route' import { toRuntimeExecutionHostId, toSshExecutionHostId } from '../../../../shared/execution-host' export function getEditorModelOwnerKey(file: OpenFile, state: WorktreeOperationRouteState): string { @@ -8,7 +8,7 @@ export function getEditorModelOwnerKey(file: OpenFile, state: WorktreeOperationR const route = captured ?? // The same focus-free owner the file's writes take, so retained text keys on that host. - resolveWriteRoute( + resolveWorktreeOperationRoute( state.activeWorktreeId === file.worktreeId ? { ...state, activeWorktreeId: null } : state, file.worktreeId ) diff --git a/src/renderer/src/components/native-chat/native-chat-file-link.test.ts b/src/renderer/src/components/native-chat/native-chat-file-link.test.ts index b14fb0f653e..1abade7d057 100644 --- a/src/renderer/src/components/native-chat/native-chat-file-link.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-file-link.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest' import type { Tab } from '../../../../shared/tab-types' import type { TerminalTab } from '../../../../shared/terminal-tab-types' -import type { AppState } from '@/store/types' import { folderWorkspaceKey } from '../../../../shared/workspace-scope' import { FLOATING_TERMINAL_WORKTREE_ID, getDefaultSettings } from '../../../../shared/constants' import { createNativeChatFileHref } from '../../../../shared/native-chat-href-routing' @@ -58,7 +57,10 @@ describe('resolveNativeChatFileLinkContext', () => { expect( resolveNativeChatFileLinkContext( state({ - settings: { activeRuntimeEnvironmentId: 'env-1' } as AppState['settings'] + // The client stamps a server's rows on arrival; focus alone never names the owner. + worktreesByRepo: { + repo: [{ ...worktreeFixture('wt-1', '/repo/worktree'), hostId: 'runtime:env-1' }] + } }), 'tab-1' ) diff --git a/src/renderer/src/components/sidebar/run-worktree-delete-with-toast.ts b/src/renderer/src/components/sidebar/run-worktree-delete-with-toast.ts index 1c70a89476e..84300d165cc 100644 --- a/src/renderer/src/components/sidebar/run-worktree-delete-with-toast.ts +++ b/src/renderer/src/components/sidebar/run-worktree-delete-with-toast.ts @@ -2,12 +2,14 @@ import { toast } from 'sonner' import { useAppStore } from '@/store' import { activateAndRevealWorktree } from '@/lib/worktree-activation' import { translate } from '@/i18n/i18n' -import { resolveWorktreeOperationRouteForHost } from '@/lib/worktree-operation-route' +import { + resolveWorktreeOperationRoute, + resolveWorktreeOperationRouteForHost +} from '@/lib/worktree-operation-route' import type { WorktreeRemovalTarget } from '../../../../shared/worktree/removal' import { showDeleteWorktreeFailureToast } from './delete-worktree-failure-toast' import type { WorktreeDeleteWithToastOptions } from './worktree-delete-request' import { getDeleteStateForWorktreeHost } from './worktree-delete-state-host-match' -import { resolveWriteRoute } from '@/lib/resolve-owner' // A failed delete usually means unresolved changes, so land on the diff panel. function viewWorktreeDiff( @@ -44,7 +46,7 @@ export function runWorktreeDeleteWithToast( const currentState = useAppStore.getState() const route = target.executionHostId ? resolveWorktreeOperationRouteForHost(currentState, worktreeId, target.executionHostId) - : resolveWriteRoute(currentState, worktreeId) + : resolveWorktreeOperationRoute(currentState, worktreeId) const hasKnownChanges = (useAppStore.getState().gitStatusByWorktree[worktreeId]?.length ?? 0) > 0 showDeleteWorktreeFailureToast({ diff --git a/src/renderer/src/components/terminal-pane/pty-connection-remote-runtime-attach.spec.ts b/src/renderer/src/components/terminal-pane/pty-connection-remote-runtime-attach.spec.ts index 096207fa92e..3eade27f9f2 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-remote-runtime-attach.spec.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-remote-runtime-attach.spec.ts @@ -451,9 +451,10 @@ export function registerPtyConnectionRemoteRuntimeAttachTests(): void { expect(createIpcPtyTransport).toHaveBeenCalled() }) - it('uses the focused runtime only for ownerless mixed-version publications', async () => { + it('spawns an unstamped local worktree locally while a single server is focused', async () => { const { connectPanePty } = await import('./pty-connection') const { createRemoteRuntimePtyTransport } = await import('./remote-runtime-pty-transport') + const { createIpcPtyTransport } = await import('./pty-transport') const transport = createMockTransport() transportFactoryQueue.push(transport) mockStoreState = { @@ -468,7 +469,9 @@ export function registerPtyConnectionRemoteRuntimeAttachTests(): void { connectPanePty(createPane(1) as never, createManager(1) as never, createDeps() as never) - expect(createRemoteRuntimePtyTransport).toHaveBeenCalledWith('legacy-hub', expect.any(Object)) + // Why: the client stamps every server row on arrival, so an unstamped row is this machine's. + expect(createRemoteRuntimePtyTransport).not.toHaveBeenCalled() + expect(createIpcPtyTransport).toHaveBeenCalled() }) it('runs an inline setup terminal locally instead of failing its host closed', async () => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection-test-store-fixtures.ts b/src/renderer/src/components/terminal-pane/pty-connection-test-store-fixtures.ts index 2aa6e3c7ce5..7fdaec460dc 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-test-store-fixtures.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-test-store-fixtures.ts @@ -114,9 +114,14 @@ export function buildActiveRuntimeEnvironmentState( ): StoreState { return { ...current, + // The client stamps a server's rows on arrival; focus alone never names the owner. + repos: current.repos.map((repo) => ({ + ...repo, + executionHostId: repo.executionHostId ?? `runtime:${environmentId}` + })), settings: { ...current.settings, activeRuntimeEnvironmentId: environmentId } - } as StoreState + } } diff --git a/src/renderer/src/hooks/useAgentDetectionTarget.test.ts b/src/renderer/src/hooks/useAgentDetectionTarget.test.ts index 833d2d6cc5e..5bafe42a62f 100644 --- a/src/renderer/src/hooks/useAgentDetectionTarget.test.ts +++ b/src/renderer/src/hooks/useAgentDetectionTarget.test.ts @@ -105,17 +105,23 @@ describe('getAgentDetectionTargetKeyForWorktree', () => { expect(getAgentDetectionTargetKeyForWorktree(state, 'repo-1::/remote/worktree')).toBeUndefined() }) - it('keeps the active runtime fallback for hydrated legacy worktrees', () => { + it('reads a server row by its stamp, never by the focused runtime', () => { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the resolver reads only these owner fields. const state = { settings: { activeRuntimeEnvironmentId: 'env-1' }, folderWorkspaces: [], projectGroups: [], - repos: [{ id: 'repo-1', connectionId: null, executionHostId: null }], + repos: [{ id: 'repo-1', connectionId: null, executionHostId: 'runtime:env-1' }], worktreesByRepo: { 'repo-1': [{ id: 'repo-1::worktree-1', repoId: 'repo-1' }] } } as unknown as Parameters[0] + const unstamped = { + ...state, + repos: state.repos?.map((repo) => ({ ...repo, executionHostId: null })) + } + expect(getAgentDetectionTargetKeyForWorktree(unstamped, 'repo-1::worktree-1')).toBe('local') const key = getAgentDetectionTargetKeyForWorktree(state, 'repo-1::worktree-1') // The workspace rides along so the paired host resolves its runtime (#19885). expect(key).toBe('runtime:env-1:repo-1%3A%3Aworktree-1') diff --git a/src/renderer/src/lib/agent-background-session-test-state.ts b/src/renderer/src/lib/agent-background-session-test-state.ts index 815c55945b3..64d2d942643 100644 --- a/src/renderer/src/lib/agent-background-session-test-state.ts +++ b/src/renderer/src/lib/agent-background-session-test-state.ts @@ -24,7 +24,13 @@ export type AgentBackgroundSessionTestState = { | { kind: 'windows-host' } | { kind: 'wsl'; distro: string | null } }[] - repos: { id: string; connectionId: string | null; path: string; projectGroupId?: string | null }[] + repos: { + id: string + connectionId: string | null + path: string + projectGroupId?: string | null + executionHostId?: `runtime:${string}` + }[] folderWorkspaces: { id: string projectGroupId: string @@ -163,6 +169,8 @@ export function useRemoteAgentBackgroundRuntime(state: AgentBackgroundSessionTes activeRuntimeEnvironmentId: 'env-1', terminalMainSideEffectAuthority: undefined } + // The client stamps a server's rows on arrival; focus alone never names the owner. + state.repos = state.repos.map((repo) => ({ ...repo, executionHostId: 'runtime:env-1' })) } /** The tab id reserved before the spawn; the run tab adopts it once the PTY is live. */ diff --git a/src/renderer/src/lib/editor-file-operation-owner.ts b/src/renderer/src/lib/editor-file-operation-owner.ts index bcbb3777845..7427ccd211c 100644 --- a/src/renderer/src/lib/editor-file-operation-owner.ts +++ b/src/renderer/src/lib/editor-file-operation-owner.ts @@ -12,9 +12,9 @@ import { } from './worktree-operation-generation' import { getFloatingWorkspaceOperationRoute, + resolveWorktreeOperationRoute, type WorktreeOperationRoute } from './worktree-operation-route' -import { resolveWriteRoute } from './resolve-owner' import { resolveExplicitWorktreeOperationRouteResult } from './worktree-operation-catalog-route' export type EditorFileOperationProvenance = { @@ -89,7 +89,7 @@ function resolveEditorOwnerRoute( ): WorktreeOperationRoute | null { // Neither workspace focus nor the Active Server focus can replace a file's catalog owner. const ownerState = { ...state, activeWorktreeId: null, activeWorkspaceExecutionHostId: null } - const route = resolveWriteRoute(ownerState, worktreeId) + const route = resolveWorktreeOperationRoute(ownerState, worktreeId) const scope = parseWorkspaceKey(worktreeId) const hostId = capturedHostId ?? @@ -100,7 +100,7 @@ function resolveEditorOwnerRoute( // Same-id folders may use a host qualifier only when the catalog confirms that owner. const folder = findFolderWorkspaceOwner(ownerState, scope.folderWorkspaceId, hostId) const qualifiedRoute = folder - ? resolveWriteRoute({ ...ownerState, folderWorkspaces: [folder] }, worktreeId) + ? resolveWorktreeOperationRoute({ ...ownerState, folderWorkspaces: [folder] }, worktreeId) : null return qualifiedRoute?.executionHostId === hostId ? qualifiedRoute : null } diff --git a/src/renderer/src/lib/folder-workspace-runtime-owner.ts b/src/renderer/src/lib/folder-workspace-runtime-owner.ts index 4ee299282cc..5d32701baf5 100644 --- a/src/renderer/src/lib/folder-workspace-runtime-owner.ts +++ b/src/renderer/src/lib/folder-workspace-runtime-owner.ts @@ -1,4 +1,8 @@ -import { parseExecutionHostId, toSshExecutionHostId } from '../../../shared/execution-host' +import { + parseExecutionHostId, + toRuntimeExecutionHostId, + toSshExecutionHostId +} from '../../../shared/execution-host' import type { ExecutionHostId, ParsedExecutionHost } from '../../../shared/execution-host' import type { FolderWorkspace } from '../../../shared/folder-workspace-types' import type { ProjectGroup } from '../../../shared/project-group-types' @@ -7,10 +11,7 @@ import { findIndexedFolderWorkspaceOwner, findIndexedProjectGroupOwner } from './worktree-runtime-owner-index' -import { - getSingleFocusedRuntimeEnvironmentId, - type SingleRuntimeLegacyOwnerState -} from './single-runtime-legacy-owner' +import type { SingleRuntimeLegacyOwnerState } from './single-runtime-legacy-owner' import { getPairedWebClientEnvironmentId } from './paired-web-client-host' type RuntimeExecutionHost = Extract @@ -113,7 +114,8 @@ export function getRuntimeEnvironmentIdForFolderWorkspace( if (restoredRuntimeHost) { return restoredRuntimeHost.environmentId } - return getSingleFocusedRuntimeEnvironmentId(state) + // Why not the Active Server focus: the client stamps every server folder on arrival. + return getPairedWebClientEnvironmentId(state) } export function getExplicitRuntimeEnvironmentIdForFolderWorkspace( @@ -179,6 +181,6 @@ export function getExecutionHostIdForFolderWorkspace( if (restoredRuntimeHost) { return restoredRuntimeHost.id } - const environmentId = getSingleFocusedRuntimeEnvironmentId(state) - return environmentId ? `runtime:${encodeURIComponent(environmentId)}` : 'local' + const environmentId = getPairedWebClientEnvironmentId(state) + return environmentId ? toRuntimeExecutionHostId(environmentId) : 'local' } diff --git a/src/renderer/src/lib/launch-agent-in-new-tab-web-runtime.test.ts b/src/renderer/src/lib/launch-agent-in-new-tab-web-runtime.test.ts index 024755a0d76..0f5f4895907 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab-web-runtime.test.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab-web-runtime.test.ts @@ -17,7 +17,10 @@ const store = { activeRuntimeEnvironmentId: 'web-runtime' as string | null }, projects: [{ id: 'repo-1', localWindowsRuntimePreference: { kind: 'inherit-global' as const } }], - repos: [{ id: 'repo-1', connectionId: null, path: '/repo' }], + // The client stamps a server's rows on arrival; focus alone never names the owner. + repos: [ + { id: 'repo-1', connectionId: null, path: '/repo', executionHostId: 'runtime:web-runtime' } + ], worktreesByRepo: { 'repo-1': [ { 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 3b7d4051ad0..219e82188be 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 @@ -540,6 +540,8 @@ describe('launchAgentInNewTab', () => { activeRuntimeEnvironmentId: 'web-runtime', experimentalNativeChat: true } + // The client stamps a server's rows on arrival; focus alone never names the owner. + Object.assign(store.repos[0], { executionHostId: 'runtime:web-runtime' }) const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab') launchAgentInNewTab({ requestId: 'request-12', agent: 'codex', worktreeId: 'wt-1' }) @@ -562,6 +564,8 @@ describe('launchAgentInNewTab', () => { activeRuntimeEnvironmentId: 'web-runtime', experimentalNativeChat: true } + // The client stamps a server's rows on arrival; focus alone never names the owner. + Object.assign(store.repos[0], { executionHostId: 'runtime:web-runtime' }) const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab') launchAgentInNewTab({ requestId: 'request-13', agent: 'codex', worktreeId: 'wt-1' }) diff --git a/src/renderer/src/lib/repo-runtime-owner.test.ts b/src/renderer/src/lib/repo-runtime-owner.test.ts index db85a7de548..90824275b54 100644 --- a/src/renderer/src/lib/repo-runtime-owner.test.ts +++ b/src/renderer/src/lib/repo-runtime-owner.test.ts @@ -43,7 +43,7 @@ describe('getRuntimeEnvironmentIdForRepo', () => { ).toBeNull() }) - it('falls back to the focused runtime for legacy repos without an owner', () => { + it('keeps legacy repos without an owner local while a runtime is focused', () => { expect( getRuntimeEnvironmentIdForRepo( { @@ -52,7 +52,7 @@ describe('getRuntimeEnvironmentIdForRepo', () => { }, 'repo-1' ) - ).toBe('focused-runtime') + ).toBeNull() }) it('uses the focused host row when duplicate repo ids exist', () => { @@ -102,7 +102,7 @@ describe('getRuntimeEnvironmentIdForRepo owner matrix', () => { ['local row', [row('local')], 'focused', null], ['direct SSH row', [row(null, 'ssh-1')], 'focused', null], ['nested runtime row', [row('runtime:owner', 'ssh-1')], 'focused', 'owner'], - ['unstamped row', [row(null)], 'focused', 'focused'], + ['unstamped row', [row(null)], 'focused', null], ['missing row', [], 'focused', 'focused'], ['duplicates, local focus', [row('local'), row('runtime:owner')], null, null], ['duplicates, matching focus', [row('local'), row('runtime:owner')], 'owner', 'owner'], diff --git a/src/renderer/src/lib/repo-runtime-owner.ts b/src/renderer/src/lib/repo-runtime-owner.ts index 4550e890666..7c24b52e611 100644 --- a/src/renderer/src/lib/repo-runtime-owner.ts +++ b/src/renderer/src/lib/repo-runtime-owner.ts @@ -53,8 +53,8 @@ export function getRuntimeEnvironmentIdForRepo( return null } const repo = findRepoOwner(state, repoId) - const hasExplicitOwner = Boolean(repo?.executionHostId?.trim() || repo?.connectionId?.trim()) - if (repo && hasExplicitOwner) { + if (repo) { + // Why: an unstamped row is local; the client stamps every server row on arrival. const parsed = parseExecutionHostId(getRepoExecutionHostId(repo)) return parsed?.kind === 'runtime' ? parsed.environmentId : null } diff --git a/src/renderer/src/lib/resolve-owner-acceptance.test-fixture.ts b/src/renderer/src/lib/resolve-owner-acceptance.test-fixture.ts index af9a9ea2c47..b36ba137248 100644 --- a/src/renderer/src/lib/resolve-owner-acceptance.test-fixture.ts +++ b/src/renderer/src/lib/resolve-owner-acceptance.test-fixture.ts @@ -46,7 +46,7 @@ export const LEGACY_FALLBACK_OWNER_ACCEPTANCE: Record = { 'no focus | folder:missing-folder': 'self@local', 'single focus | global-floating-terminal': 'self@local', 'single focus | local-repo::wt': 'self@local', - 'single focus | legacy-repo::wt': 'env:focused-env@local', + 'single focus | legacy-repo::wt': 'self@local', 'single focus | runtime-repo::wt': 'env:owner-env@local', 'single focus | ssh-repo::wt': 'self@ssh:box', 'single focus | hub-repo::wt': 'env:hub-env@ssh:box', @@ -63,9 +63,9 @@ export const LEGACY_FALLBACK_OWNER_ACCEPTANCE: Record = { 'single focus | folder:ssh-folder': 'self@ssh:box', 'single focus | folder:twin-folder': 'ambiguous', 'single focus | folder:group-twin-folder': 'ambiguous', - 'single focus | folder:unstamped-folder': 'env:focused-env@local', + 'single focus | folder:unstamped-folder': 'self@local', 'single focus | folder:restored-folder': 'env:owner-env@local', - 'single focus | folder:missing-folder': 'env:focused-env@local', + 'single focus | folder:missing-folder': 'self@local', 'active local | twin-repo::wt': 'self@local', 'active ssh:box | twin-repo::wt': 'self@ssh:box', 'active local | rival-repo::wt': 'self@local', diff --git a/src/renderer/src/lib/resolve-owner-acceptance.test.ts b/src/renderer/src/lib/resolve-owner-acceptance.test.ts index aa0233914ca..4678ac46eda 100644 --- a/src/renderer/src/lib/resolve-owner-acceptance.test.ts +++ b/src/renderer/src/lib/resolve-owner-acceptance.test.ts @@ -27,7 +27,10 @@ const DELIBERATE_CHANGES: Record = { 'folder:group-twin-folder': 'D77', 'rival-repo::wt': 'B: servers contest the SSH host', 'conflict-repo::wt': 'B', - 'stale-hub-repo::wt': 'C: reached through the server whose repo row names the target' + 'stale-hub-repo::wt': 'C: reached through the server whose repo row names the target', + 'legacy-repo::wt': 'an unstamped row is local; the Active Server focus never claims it', + 'folder:unstamped-folder': 'same', + 'folder:missing-folder': 'same' } describe('resolveOwnerWithLegacyFallback acceptance (round-3 design §5)', () => { @@ -85,12 +88,12 @@ describe('resolveOwnerWithLegacyFallback probes', () => { expect(ownerOf(single, 'onboarding-setup::/tmp/skill')).toBe('self@local') }) - it('sends a known unstamped repo with no worktree row to the single focused runtime, as spawn does', () => { + it('keeps a known unstamped repo with no worktree row local under single focus', () => { const noWorktreeRow = { ...single, worktreesByRepo: { ...single.worktreesByRepo, 'legacy-repo': [] } } - expect(ownerOf(noWorktreeRow, 'legacy-repo::wt')).toBe('env:focused-env@local') + expect(ownerOf(noWorktreeRow, 'legacy-repo::wt')).toBe('self@local') }) it('lets a named host win over the single focused runtime', () => { diff --git a/src/renderer/src/lib/resolve-owner.ts b/src/renderer/src/lib/resolve-owner.ts index 579981cce4a..8b124bf3024 100644 --- a/src/renderer/src/lib/resolve-owner.ts +++ b/src/renderer/src/lib/resolve-owner.ts @@ -1,6 +1,5 @@ /** - * The owner of a workspace as a {@link HostAuthority}, decided from the rows alone. It never reads - * the "Active Server" focus setting. + * The owner of a workspace as a {@link HostAuthority}, decided from the rows alone. */ import { getRepoSshConnectionId, @@ -25,7 +24,6 @@ import { getFloatingWorkspaceOperationRoute, resolveWorktreeOperationRouteResultForHost, resolveWorktreeOperationRouteResult, - type WorktreeOperationRoute, type WorktreeOperationRouteResolution, type WorktreeOperationRouteState } from './worktree-operation-route' @@ -92,7 +90,7 @@ export function namedHostForOwnerRef( ) } -function resolveRoute( +export function resolveOwner( state: WorktreeOperationRouteState, ref: WorkspaceOwnerRef ): ExecutionHostOwnerMatch { @@ -149,56 +147,6 @@ function nestedRepoTargetId( return repo ? getRepoSshConnectionId(repo) : null } -export function resolveOwner( - state: WorktreeOperationRouteState, - ref: WorkspaceOwnerRef -): ExecutionHostOwnerMatch { - return resolveRoute(focusFreeRouteState(state), ref) -} - -/** - * The route for a write that names no host, decided by the rows alone: the "Active Server" focus - * fallback that terminal reads keep is never reachable for a write. - */ -export function resolveWriteRouteResult( - state: WorktreeOperationRouteState, - workspaceId: string -): WorktreeOperationRouteResolution { - return resolveWorktreeOperationRouteResult(focusFreeRouteState(state), workspaceId) -} - -/** {@link resolveWriteRouteResult}, refusing ambiguity and absence alike. */ -export function resolveWriteRoute( - state: WorktreeOperationRouteState, - workspaceId: string -): WorktreeOperationRoute | null { - const resolution = resolveWriteRouteResult(state, workspaceId) - return resolution.kind === 'resolved' ? resolution.route : null -} - -/** - * Only the fields the route resolvers read, so per-row selectors do not copy the whole store. - * Focus is the only settings field they read; blanking it makes this focus-free. - */ -function focusFreeRouteState(state: WorktreeOperationRouteState): { - [K in keyof Required]: WorktreeOperationRouteState[K] -} { - return { - settings: null, - runtimeEnvironments: state.runtimeEnvironments, - folderWorkspaces: state.folderWorkspaces, - projectGroups: state.projectGroups, - restoredRuntimeHostIdByWorkspaceSessionKey: state.restoredRuntimeHostIdByWorkspaceSessionKey, - activeWorktreeId: state.activeWorktreeId, - activeWorkspaceExecutionHostId: state.activeWorkspaceExecutionHostId, - repos: state.repos, - worktreesByRepo: state.worktreesByRepo, - detectedWorktreesByRepo: state.detectedWorktreesByRepo, - runtimeEnvironmentCatalogHydrated: state.runtimeEnvironmentCatalogHydrated, - removedRuntimeEnvironmentIds: state.removedRuntimeEnvironmentIds - } -} - /** Two hosts publish this workspace (or contest it), so no host-routed action may pick one. */ export function isTwinWorkspace( state: WorktreeOperationRouteState, diff --git a/src/renderer/src/lib/terminal-pty-owner-acceptance.test.ts b/src/renderer/src/lib/terminal-pty-owner-acceptance.test.ts index 9d5d261ecac..f19f94c67f9 100644 --- a/src/renderer/src/lib/terminal-pty-owner-acceptance.test.ts +++ b/src/renderer/src/lib/terminal-pty-owner-acceptance.test.ts @@ -23,7 +23,11 @@ const DELIBERATE_CHANGES: Record = { 'single focus | folder:group-twin-folder': 'ambiguous', // B: two servers contest the selected SSH host, so spawn refuses and teardown must too. 'active ssh:box | rival-repo::wt': 'ambiguous', - 'active ssh:box | conflict-repo::wt': 'ambiguous' + 'active ssh:box | conflict-repo::wt': 'ambiguous', + // An unstamped row is this machine's; the Active Server focus never claims its PTYs. + 'single focus | legacy-repo::wt': null, + 'single focus | folder:unstamped-folder': null, + 'single focus | folder:missing-folder': null } describe('resolveTerminalPtyOwner acceptance', () => { diff --git a/src/renderer/src/lib/terminal-worktree-route.test.ts b/src/renderer/src/lib/terminal-worktree-route.test.ts index 34598739157..672ae8a38ea 100644 --- a/src/renderer/src/lib/terminal-worktree-route.test.ts +++ b/src/renderer/src/lib/terminal-worktree-route.test.ts @@ -179,17 +179,16 @@ describe('resolveTerminalHostOwnership teardown', () => { }) }) - it('keeps a hostless worktree on the focused runtime for teardown too', () => { - // Why: unlike the host-agnostic surfaces, an ownerless worktree row really does spawn on the - // focused HUB, and its wake hint is `ssh:`-shaped — downgrading it would kill the wrong host. + it('spawns and tears down a hostless worktree locally, never on the focused runtime', () => { + // Why: the client stamps every server row on arrival, so a row with no host is this machine's. const state = focusedState({ repos: [{ id: 'repo-1', connectionId: null, executionHostId: null }], worktreesByRepo: { 'repo-1': [{ id: 'repo-1::/w', repoId: 'repo-1' }] } } as unknown as Partial) for (const purpose of ['spawn', 'teardown'] as const) { expect(resolveTerminalHostOwnership(state, 'repo-1::/w', purpose)).toEqual({ - kind: 'runtime', - runtimeEnvironmentId: 'hub-a' + kind: 'local-or-ssh', + runtimeEnvironmentId: null }) } }) diff --git a/src/renderer/src/lib/terminal-worktree-route.ts b/src/renderer/src/lib/terminal-worktree-route.ts index 48605f2d1e2..53160cd7de9 100644 --- a/src/renderer/src/lib/terminal-worktree-route.ts +++ b/src/renderer/src/lib/terminal-worktree-route.ts @@ -33,9 +33,9 @@ export type TerminalHostOwnership = | { kind: 'unresolved'; runtimeEnvironmentId: null } /** - * `spawn` asks which host should start a new process, so a merely focused runtime is a fine guess. - * `teardown` asks which provider can kill an existing one, where that same guess strands the local - * PTY the surface was actually spawned on. + * `spawn` asks which host should start a new process, so a setup terminal may follow the focused + * runtime. `teardown` asks which provider can kill an existing one, where that guess strands the + * local PTY the surface was actually spawned on. */ export type TerminalHostOwnershipPurpose = 'spawn' | 'teardown' @@ -214,8 +214,9 @@ function resolveFloatingScopeOwnership( if (runtimeEnvironmentIdUnlessTwin(state, worktreeId)) { return ownershipForRuntimeEnvironmentId(runtimeEnvironmentId) } - // Why: this surface publishes no runtime owner and only looks runtime-owned because exactly one - // runtime is focused — a guess that flips as catalogs hydrate, stranding the local PTY (STA-2639). + // Why: this surface publishes no runtime owner and only looks runtime-owned through a guess (the + // focused runtime, a web client's server) that flips as catalogs hydrate, stranding the local + // PTY (STA-2639). return LOCAL_OR_SSH_TERMINAL_HOST } diff --git a/src/renderer/src/lib/worktree-operation-route.test.ts b/src/renderer/src/lib/worktree-operation-route.test.ts index 48ff464ea87..a4115002dbe 100644 --- a/src/renderer/src/lib/worktree-operation-route.test.ts +++ b/src/renderer/src/lib/worktree-operation-route.test.ts @@ -119,7 +119,7 @@ describe('resolveWorktreeOperationRouteResult', () => { }) }) - it('uses the focused runtime only for legacy publications with no owner evidence', () => { + it('routes an unstamped row local, never to the focused runtime', () => { expect( resolveWorktreeOperationRouteResult( { @@ -131,10 +131,7 @@ describe('resolveWorktreeOperationRouteResult', () => { ) ).toEqual({ kind: 'resolved', - route: { - executionHostId: 'runtime:legacy-hub', - runtimeEnvironmentId: 'legacy-hub' - } + route: { executionHostId: 'local', runtimeEnvironmentId: null } }) }) @@ -239,7 +236,7 @@ describe('resolveWorktreeOperationRouteResult', () => { ).toEqual({ kind: 'ambiguous' }) }) - it('routes a focused runtime only when it is the single saved runtime', () => { + it('never routes an unstamped row to the single saved, focused runtime', () => { expect( resolveWorktreeOperationRouteResult( { @@ -250,10 +247,7 @@ describe('resolveWorktreeOperationRouteResult', () => { }, WORKTREE_ID ) - ).toEqual({ - kind: 'resolved', - route: { executionHostId: 'runtime:hub-a', runtimeEnvironmentId: 'hub-a' } - }) + ).toEqual({ kind: 'missing' }) }) it('ignores unrelated removed-runtime tombstones for positive local identity', () => { @@ -298,7 +292,7 @@ describe('resolveWorktreeOperationRouteResult', () => { ).toEqual({ kind: 'missing' }) }) - it('does not treat runtime focus as ownership while the saved-runtime catalog is loading', () => { + it('keeps an unstamped row local while the saved-runtime catalog is loading, focus or not', () => { expect( resolveWorktreeOperationRouteResult( { @@ -310,7 +304,10 @@ describe('resolveWorktreeOperationRouteResult', () => { }, WORKTREE_ID ) - ).toEqual({ kind: 'missing' }) + ).toEqual({ + kind: 'resolved', + route: { executionHostId: 'local', runtimeEnvironmentId: null } + }) }) it('preserves ownerless local compatibility after an empty catalog hydrates', () => { @@ -539,7 +536,7 @@ describe('resolveWorktreeOperationRouteResult', () => { ).toEqual({ kind: 'missing' }) }) - it('fails closed when the active runtime is ambiguous', () => { + it('keeps an unstamped row local whichever of several runtimes is focused', () => { expect( resolveWorktreeOperationRouteResult( { @@ -551,10 +548,13 @@ describe('resolveWorktreeOperationRouteResult', () => { }, WORKTREE_ID ) - ).toEqual({ kind: 'missing' }) + ).toEqual({ + kind: 'resolved', + route: { executionHostId: 'local', runtimeEnvironmentId: null } + }) }) - it('keeps an unambiguous active runtime authoritative over the local fallback', () => { + it('keeps an unstamped row local when the single saved runtime is focused', () => { expect( resolveWorktreeOperationRouteResult( { @@ -568,7 +568,7 @@ describe('resolveWorktreeOperationRouteResult', () => { ) ).toEqual({ kind: 'resolved', - route: { executionHostId: 'runtime:hub-a', runtimeEnvironmentId: 'hub-a' } + route: { executionHostId: 'local', runtimeEnvironmentId: null } }) }) @@ -773,7 +773,7 @@ describe('resolveWorktreeOperationRouteResult', () => { }) }) - it('scopes an ownerless folder workspace to the single focused runtime', () => { + it('keeps an ownerless folder workspace local when the single saved runtime is focused', () => { expect( resolveWorktreeOperationRouteResult( { @@ -785,7 +785,7 @@ describe('resolveWorktreeOperationRouteResult', () => { ) ).toEqual({ kind: 'resolved', - route: { executionHostId: 'runtime:hub-a', runtimeEnvironmentId: 'hub-a' } + route: { executionHostId: 'local', runtimeEnvironmentId: null } }) }) diff --git a/src/renderer/src/lib/worktree-operation-route.ts b/src/renderer/src/lib/worktree-operation-route.ts index 63bd2721a73..b6b5027c4ad 100644 --- a/src/renderer/src/lib/worktree-operation-route.ts +++ b/src/renderer/src/lib/worktree-operation-route.ts @@ -295,24 +295,9 @@ function resolveOwnerRouteResult( return { kind: 'missing' } } - // Why: pre-owner-projection runtimes published no host fields; terminal routing retains their single focused-runtime behavior. - const legacyRuntimeEnvironmentId = state.settings?.activeRuntimeEnvironmentId?.trim() + // Why no Active Server focus here: the client stamps every server row on arrival, so an + // unstamped row is never a server's. const savedRuntimeIds = state.runtimeEnvironments?.map((environment) => environment.id.trim()) - const legacyRuntimeIsUnambiguous = - savedRuntimeIds === undefined || - (savedRuntimeIds.length === 1 && savedRuntimeIds[0] === legacyRuntimeEnvironmentId) - if (legacyRuntimeEnvironmentId && !legacyRuntimeIsUnambiguous) { - return { kind: 'missing' } - } - if (legacyRuntimeEnvironmentId) { - return { - kind: 'resolved', - route: { - executionHostId: `runtime:${encodeURIComponent(legacyRuntimeEnvironmentId)}`, - runtimeEnvironmentId: legacyRuntimeEnvironmentId - } - } - } // Why: a found repo/worktree record is positive identity evidence, so keep terminal-owner // parity with the folder branch below. Every stamped row already routed above, so an unstamped // repo row here is a legacy pre-owner-projection row — local by construction, as diff --git a/src/renderer/src/lib/worktree-runtime-owner.test.ts b/src/renderer/src/lib/worktree-runtime-owner.test.ts index 296e9417166..4c614d22185 100644 --- a/src/renderer/src/lib/worktree-runtime-owner.test.ts +++ b/src/renderer/src/lib/worktree-runtime-owner.test.ts @@ -364,10 +364,18 @@ describe('the explicit owner environment (resolveOwner)', () => { }) it('does not treat the focused runtime as ownership for legacy-local worktrees', () => { - expect(runtimeEnvironmentIdWithLegacyFallbackUnlessTwin(state, 'legacy-repo::wt-legacy')).toBe( - 'focused-env' - ) + expect( + runtimeEnvironmentIdWithLegacyFallbackUnlessTwin(state, 'legacy-repo::wt-legacy') + ).toBeNull() expect(ownerEnvironmentId(state, 'legacy-repo::wt-legacy')).toBeNull() + expect(getExecutionHostIdForWorktree(state, 'legacy-repo::wt-legacy')).toBe('local') + expect(getExecutionHostIdForWorktree(state, 'unknown-repo::wt')).toBe('local') + }) + + it('keeps a setup terminal on the focused runtime, where it spawns', () => { + expect(getExecutionHostIdForWorktree(state, 'ephemeral-setup-terminal:skill-panel')).toBe( + 'runtime:focused-env' + ) }) it('returns the runtime owner when the repo or folder explicitly names one', () => { diff --git a/src/renderer/src/lib/worktree-runtime-owner.ts b/src/renderer/src/lib/worktree-runtime-owner.ts index 1ab194ce2a5..39810bbe26a 100644 --- a/src/renderer/src/lib/worktree-runtime-owner.ts +++ b/src/renderer/src/lib/worktree-runtime-owner.ts @@ -5,108 +5,39 @@ import { UNRESOLVED_OWNER_HOST_ID } from '../../../shared/execution-host' import type { ExecutionHostId } from '../../../shared/execution-host' -import type { Worktree } from '../../../shared/worktree/types' import { parseWorkspaceKey } from '../../../shared/workspace-scope' import { getRepoIdFromWorktreeId } from '@/store/slices/worktree-helpers' import { findIndexedRepoOwner as findRepoRecord, findIndexedWorktreeOwner as findWorktreeRecord, - hasIndexedDetectedWorktree, - resolveIndexedProjectGroupOwner, - resolveIndexedRepoOwner, - resolveIndexedWorktreeOwner + hasIndexedDetectedWorktree } from './worktree-runtime-owner-index' -import { getSingleFocusedRuntimeEnvironmentId } from './single-runtime-legacy-owner' import { getActiveWorkspaceExecutionHostId } from './active-workspace-host' +import { getSingleFocusedRuntimeEnvironmentId } from './single-runtime-legacy-owner' +import { isEphemeralSetupTerminalWorktreeId } from '../../../shared/ephemeral-setup-terminal-worktree-id' import { getPairedWebClientEnvironmentId } from './paired-web-client-host' import { findFolderWorkspaceOwner, getExecutionHostIdForFolderWorkspace, getRuntimeEnvironmentIdForFolderWorkspace } from './folder-workspace-runtime-owner' -import { - namedHostForOwnerRef, - resolveOwner, - someWorktreeCopy, - type WorkspaceOwnerRef -} from './resolve-owner' +import { namedHostForOwnerRef, resolveOwner, type WorkspaceOwnerRef } from './resolve-owner' import { SELF_LOCAL_AUTHORITY, type HostAuthority } from '../../../shared/host-authority' import type { ExecutionHostOwnerMatch } from '../../../shared/worktree-execution-host-resolution' -import { - getFloatingWorkspaceOperationRoute, - resolveWorktreeOperationRouteResult -} from './worktree-operation-route' +import { getFloatingWorkspaceOperationRoute } from './worktree-operation-route' import { resolveExplicitWorktreeOperationRouteResult } from './worktree-operation-catalog-route' import type { WorktreeRuntimeOwnerState } from './worktree-runtime-owner-state' -function getProjectedRuntimeOwnerEnvironmentId( - worktree: Pick | null | undefined -): string | null { - return worktree?.runtimeOwnerEnvironmentId?.trim() || null -} - function getExecutionHostIdFromWorktreeHost( hostId: string | null | undefined ): ExecutionHostId | null { return parseExecutionHostId(hostId)?.id ?? null } -/** - * The server the pre-`resolveOwner` resolver picked for a row that names no host (`null`: this - * app or a twin). Only {@link resolveOwnerWithLegacyFallback} reads it, after a named host, - * a twin and every stamped row have already been answered. - */ -function legacyRuntimeEnvironmentId( - state: WorktreeRuntimeOwnerState, - worktreeId: string -): string | null { - const workspaceScope = parseWorkspaceKey(worktreeId) - if (workspaceScope?.type === 'folder') { - return getRuntimeEnvironmentIdForFolderWorkspace(state, workspaceScope.folderWorkspaceId) - } - const indexedOwner = resolveIndexedWorktreeOwner(state.worktreesByRepo, worktreeId) - if (indexedOwner.kind === 'ambiguous') { - return null - } - if (indexedOwner.kind === 'resolved') { - const owner = indexedOwner.owner - const projectedRuntimeOwner = getProjectedRuntimeOwnerEnvironmentId(owner) - const parsedHost = parseExecutionHostId(owner.hostId) - const hasDetectedOwner = hasIndexedDetectedWorktree(state.detectedWorktreesByRepo, worktreeId) - // Why: a row on a web client that names no server is still reached through its only one. - const onlyHost = getPairedWebClientEnvironmentId(state) - if (!hasDetectedOwner && (projectedRuntimeOwner || parsedHost)) { - return ( - projectedRuntimeOwner || - (parsedHost?.kind === 'runtime' ? parsedHost.environmentId : onlyHost) || - null - ) - } - if (!hasDetectedOwner) { - const repoResolution = resolveIndexedRepoOwner(state.repos, owner.repoId) - if (repoResolution.kind === 'ambiguous') { - return null - } - if ( - repoResolution.kind === 'resolved' && - (repoResolution.owner.executionHostId?.trim() || repoResolution.owner.connectionId?.trim()) - ) { - const repoHost = parseExecutionHostId(getRepoExecutionHostId(repoResolution.owner)) - if (repoHost) { - return (repoHost.kind === 'runtime' ? repoHost.environmentId : onlyHost) || null - } - } - } - } - const resolution = resolveWorktreeOperationRouteResult(state, worktreeId) - return resolution.kind === 'resolved' ? resolution.route.runtimeEnvironmentId || null : null -} - -function getFocusedRuntimeOrLocalExecutionHostId( - state: WorktreeRuntimeOwnerState -): ExecutionHostId { - const environmentId = getSingleFocusedRuntimeEnvironmentId(state) - return environmentId ? `runtime:${encodeURIComponent(environmentId)}` : 'local' +/** A row that names no host is this machine's: the client stamps every server row on arrival. */ +function getUnstampedExecutionHostId(state: WorktreeRuntimeOwnerState): ExecutionHostId { + const environmentId = getPairedWebClientEnvironmentId(state) + return environmentId ? toRuntimeExecutionHostId(environmentId) : 'local' } /** @@ -114,8 +45,7 @@ function getFocusedRuntimeOrLocalExecutionHostId( * worktree without a repo row, a folder workspace without a folder-workspace row) and nothing more * specific — active-workspace host, detected owner, per-worktree host — applies either. A row that * exists and names no owner is a positive `'local'`; a row that has not landed is silence. - * {@link getExecutionHostIdForWorktree} papers over that silence with the focused-runtime-or-local - * default, which is the right answer for routing an operation and the wrong one for a caller that + * {@link getExecutionHostIdForWorktree} papers over that silence with the local default, which is the right answer for routing an operation and the wrong one for a caller that * reads the host as evidence. */ export function getKnownExecutionHostIdForWorktree( @@ -171,76 +101,50 @@ export function getKnownExecutionHostIdForWorktree( if (hasExplicitOwner) { return getRepoExecutionHostId(repo) } - return getFocusedRuntimeOrLocalExecutionHostId(state) + return getUnstampedExecutionHostId(state) } export function getExecutionHostIdForWorktree( state: WorktreeRuntimeOwnerState, worktreeId: string | null | undefined ): ExecutionHostId { - return ( - getKnownExecutionHostIdForWorktree(state, worktreeId) ?? - getFocusedRuntimeOrLocalExecutionHostId(state) - ) -} - -function hasRowHost(row: { executionHostId?: string | null; connectionId?: string | null }) { - return Boolean(row.executionHostId?.trim() || row.connectionId?.trim()) -} - -/** Through the owner index: rows on two hosts always include a stamped one. */ -function hasStampedRepoRow(state: WorktreeRuntimeOwnerState, repoId: string): boolean { - const repo = resolveIndexedRepoOwner(state.repos, repoId) - return repo.kind === 'ambiguous' || (repo.kind === 'resolved' && hasRowHost(repo.owner)) -} - -/** A row that names its host; without one the deprecated resolver's guess still applies. */ -function hasHostEvidence(state: WorktreeRuntimeOwnerState, workspaceId: string): boolean { - if (getFloatingWorkspaceOperationRoute(workspaceId)) { - return true + const known = getKnownExecutionHostIdForWorktree(state, worktreeId) + if (known) { + return known } - const scope = parseWorkspaceKey(workspaceId) - if (scope?.type === 'folder') { - const folder = findFolderWorkspaceOwner(state, scope.folderWorkspaceId) - if (!folder || hasRowHost(folder)) { - return Boolean(folder) - } - const group = resolveIndexedProjectGroupOwner(state.projectGroups, folder.projectGroupId) - return group.kind === 'ambiguous' || (group.kind === 'resolved' && hasRowHost(group.owner)) - } - const repoId = getRepoIdFromWorktreeId(workspaceId) - return ( - someWorktreeCopy(state, workspaceId, (row) => - Boolean(row.hostId?.trim() || row.runtimeOwnerEnvironmentId?.trim()) - ) || hasStampedRepoRow(state, repoId) - ) + // Why: a setup terminal has no row and spawns on the focused runtime (resolveTerminalHostOwnership). + const setupEnvironmentId = + worktreeId && isEphemeralSetupTerminalWorktreeId(worktreeId) + ? getSingleFocusedRuntimeEnvironmentId(state) + : null + return setupEnvironmentId + ? toRuntimeExecutionHostId(setupEnvironmentId) + : getUnstampedExecutionHostId(state) } /** - * `resolveOwner` for callers folding off the deprecated resolvers: a named host and stamped rows - * resolve as `resolveOwner` does (twins and contested hosts stay `ambiguous`), while `missing` and - * rows with no host stamp keep the deprecated answer, single focused runtime included. + * {@link resolveOwner}, reading an id no row places as this app's — or, for a folder, the server + * its saved session names, or a web client's only server. */ export function resolveOwnerWithLegacyFallback( state: WorktreeRuntimeOwnerState, ref: WorkspaceOwnerRef ): ExecutionHostOwnerMatch { const match = resolveOwner(state, ref) - if ( - namedHostForOwnerRef(state, ref) || - match.kind === 'ambiguous' || - (match.kind === 'resolved' && hasHostEvidence(state, ref.workspaceId)) - ) { + if (match.kind !== 'missing' || namedHostForOwnerRef(state, ref)) { return match } - const legacyEnvironmentId = legacyRuntimeEnvironmentId(state, ref.workspaceId) - if (legacyEnvironmentId) { - return { - kind: 'resolved', - owner: { endpoint: { kind: 'environment', environmentId: legacyEnvironmentId }, at: 'local' } - } + const scope = parseWorkspaceKey(ref.workspaceId) + const environmentId = + scope?.type === 'folder' + ? getRuntimeEnvironmentIdForFolderWorkspace(state, scope.folderWorkspaceId) + : null + return { + kind: 'resolved', + owner: environmentId + ? { endpoint: { kind: 'environment', environmentId }, at: 'local' } + : SELF_LOCAL_AUTHORITY } - return match.kind === 'resolved' ? match : { kind: 'resolved', owner: SELF_LOCAL_AUTHORITY } } /** diff --git a/src/renderer/src/lib/worktree-sort-order-host-split.test.ts b/src/renderer/src/lib/worktree-sort-order-host-split.test.ts index 76a894a1117..0b568682602 100644 --- a/src/renderer/src/lib/worktree-sort-order-host-split.test.ts +++ b/src/renderer/src/lib/worktree-sort-order-host-split.test.ts @@ -23,7 +23,7 @@ describe('splitWorktreeSortOrderByHost', () => { ]) }) - it('routes legacy worktrees without an explicit owner to the focused host', () => { + it('keeps legacy worktrees without an explicit owner local, whatever server is focused', () => { const groups = splitWorktreeSortOrderByHost( { settings: { activeRuntimeEnvironmentId: 'focused-env' }, @@ -32,9 +32,7 @@ describe('splitWorktreeSortOrderByHost', () => { }, ['legacy::wt'] ) - expect(groups).toEqual([ - { hostId: 'runtime:focused-env', orderedIds: ['legacy::wt'], rowHostIds: ['local'] } - ]) + expect(groups).toEqual([{ hostId: 'local', orderedIds: ['legacy::wt'], rowHostIds: ['local'] }]) }) it('leaves out a twin and a row only a repo row places on a server', () => { diff --git a/src/renderer/src/store/slices/browser-remote-page-lifecycle.test.ts b/src/renderer/src/store/slices/browser-remote-page-lifecycle.test.ts index 6d0631dd960..57da462aeac 100644 --- a/src/renderer/src/store/slices/browser-remote-page-lifecycle.test.ts +++ b/src/renderer/src/store/slices/browser-remote-page-lifecycle.test.ts @@ -5,6 +5,7 @@ import { resetBrowserRuntimeMocks, settingsWithRuntime } from './browser-slice-test-harness' +import { makeWorktree } from './worktrees-slice-test-fixtures' const createWebRuntimeSessionBrowserTabMock = vi.hoisted(() => vi.fn()) const runtimeEnvironmentCall = vi.fn() @@ -36,6 +37,10 @@ describe('createBrowserSlice runtime guard', () => { const store = createTestStore() store.setState({ settings: settingsWithRuntime('env-1'), + // The client stamps a server's rows on arrival; focus alone never names the owner. + worktreesByRepo: { + 'repo-1': [makeWorktree({ id: 'wt-1', repoId: 'repo-1', hostId: 'runtime:env-1' })] + }, unifiedTabsByWorktree: {}, browserTabsByWorktree: { 'wt-1': [ diff --git a/src/renderer/src/store/slices/terminal-tab-retirement.test.ts b/src/renderer/src/store/slices/terminal-tab-retirement.test.ts index 6c0446c8590..90a8f5ba369 100644 --- a/src/renderer/src/store/slices/terminal-tab-retirement.test.ts +++ b/src/renderer/src/store/slices/terminal-tab-retirement.test.ts @@ -157,8 +157,8 @@ describe('terminal tab retirement planning', () => { settings: { activeRuntimeEnvironmentId: 'env-1' }, worktreesByRepo: { repo: [ - { id: 'wt-1', repoId: 'repo' }, - { id: 'wt-2', repoId: 'repo' } + { id: 'wt-1', repoId: 'repo', hostId: 'runtime:env-1' }, + { id: 'wt-2', repoId: 'repo', hostId: 'runtime:env-1' } ] }, tabsByWorktree: { @@ -176,7 +176,7 @@ describe('terminal tab retirement planning', () => { it('deduplicates legacy and scoped aliases owned by the closing tab', () => { const state = makeState({ settings: { activeRuntimeEnvironmentId: 'env-1' }, - worktreesByRepo: { repo: [{ id: 'wt-1', repoId: 'repo' }] }, + worktreesByRepo: { repo: [{ id: 'wt-1', repoId: 'repo', hostId: 'runtime:env-1' }] }, tabsByWorktree: { 'wt-1': [makeTab('tab-1', 'wt-1', 'remote:terminal-1')] }, diff --git a/src/renderer/src/store/slices/worktrees/teardown/host-qualified-worktree-removal.ts b/src/renderer/src/store/slices/worktrees/teardown/host-qualified-worktree-removal.ts index cb76db2b3fb..30bfa94883f 100644 --- a/src/renderer/src/store/slices/worktrees/teardown/host-qualified-worktree-removal.ts +++ b/src/renderer/src/store/slices/worktrees/teardown/host-qualified-worktree-removal.ts @@ -12,10 +12,10 @@ import type { RuntimeClientTarget } from '../../../../../../shared/runtime-clien import type { ExecutionHostId } from '../../../../../../shared/execution-host' import { getWorktreeOperationOwnerHostIds, + resolveWorktreeOperationRoute, resolveWorktreeOperationRouteForHost, type WorktreeOperationRoute } from '@/lib/worktree-operation-route' -import { resolveWriteRoute } from '@/lib/resolve-owner' import { captureWorktreeOperationGenerationGuard } from '@/lib/worktree-operation-generation' import { getRepoIdFromWorktreeId } from '../../worktree-helpers' import { appliedWorktreeCatalogVersionPatch } from '../listing/worktree-catalog-version-state' @@ -51,7 +51,7 @@ function resolveHostQualifiedRemovalRoute( ): WorktreeOperationRoute | null { return requiredExecutionHostId ? resolveWorktreeOperationRouteForHost(get(), worktreeId, requiredExecutionHostId) - : resolveWriteRoute(get(), worktreeId) + : resolveWorktreeOperationRoute(get(), worktreeId) } export type HostQualifiedRemovalStart = diff --git a/src/renderer/src/store/slices/worktrees/teardown/remove-worktree.ts b/src/renderer/src/store/slices/worktrees/teardown/remove-worktree.ts index 5bdbd484509..29c230be14c 100644 --- a/src/renderer/src/store/slices/worktrees/teardown/remove-worktree.ts +++ b/src/renderer/src/store/slices/worktrees/teardown/remove-worktree.ts @@ -9,8 +9,10 @@ import { forgetHugeRepoWarningDismissalsForWorktrees } from '@/lib/source-contro import { forgetWorktreeSleepIntent } from '@/lib/worktree-sleep-intent' import { readIpcErrorDetail } from '@/lib/ipc-error' import { isArchiveHookRemovalError } from '../../../../../../shared/worktree/archive-hook-removal-gate' -import { resolveWorktreeOperationRouteResultForHost } from '@/lib/worktree-operation-route' -import { resolveWriteRouteResult } from '@/lib/resolve-owner' +import { + resolveWorktreeOperationRouteResult, + resolveWorktreeOperationRouteResultForHost +} from '@/lib/worktree-operation-route' import { beginHostQualifiedRemoval, completeSameIdHostScopedRemoval, @@ -139,7 +141,7 @@ export function createRemoveWorktree( // Missing means stale mirror; ambiguous or changed ownership must fail closed. const currentResolution = requiredExecutionHostId ? resolveWorktreeOperationRouteResultForHost(get(), worktreeId, requiredExecutionHostId) - : resolveWriteRouteResult(get(), worktreeId) + : resolveWorktreeOperationRouteResult(get(), worktreeId) if (currentResolution.kind === 'ambiguous') { throw error }