refactor(files): file reads and edits route by the file's owner (#27278)

* refactor(files): file reads and edits route by the file's owner

The runtime file clients took a settings object and turned its Active Server
field into a connection, so any caller that passed raw settings silently read
or wrote on the focused server. They now take `target: RuntimeClientTarget`,
and every caller derives it from the owner: the tab's stamped owner, else its
worktree's owner (getRuntimeTargetForFileOwner / getRuntimeTargetForWorktreeOwner),
the file-explorer operation owner, or an already captured owner id.

Behavior: single-host (local-only) users see no change. An editor tab opened
without an owner stamp, a terminal link with no PTY owner, an OS file drop and
a notebook link now go to the workspace's owner instead of whichever server is
focused when the read happens.

Removes getEditorFileDropSettingsForWorktree and the native-chat settings clone
cache; settingsForWorktreeOperationRoute loses its editor caller.

* fix(files): file owner targets come from the rows, never the focused server

getRuntimeTargetForWorktreeOwner/getRuntimeTargetForFileOwner move to
lib/file-owner-runtime-target.ts and resolve through the focus-free
resolveOwner. A workspace with no row yet stays on this app (tabs and drops
stamp a server owner when one exists); rows that disagree return null, and
callers with an error path refuse via requireRuntimeTargetForFileOwner.

* test: keep the editor content-state fixture within the line limit

* fix(files): an unresolved document owner blocks local image reads

A null owner target became runtimeContext undefined, which image readers take
as a plain local file, so a server-owned document's images were read from this
machine. It is now runtimeContext null, the readers' do-not-load sentinel, in
the rich editor and the preview, and prewarm skips it.

* fix(files): read the image node view's storage through a named type

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
OrcaWin
2026-10-10 11:23:18 -07:00
committed by GitHub
co-authored by m4air
parent 2eb6a920dd
commit 7de3710dbe
153 changed files with 1090 additions and 734 deletions
+13 -105
View File
@@ -10,7 +10,6 @@
1 src/renderer/src/components/LinearIssueWorkspace.tsx
1 src/renderer/src/components/NativeChatResumeOnRestartGroups.tsx
1 src/renderer/src/components/NativeChatResumeOnRestartModal.tsx
2 src/renderer/src/components/QuickOpen.tsx
1 src/renderer/src/components/activity/ActivityPrototypePage.tsx
1 src/renderer/src/components/activity/activity-scope-filter-controls.tsx
3 src/renderer/src/components/activity/activity-thread-actions.ts
@@ -31,54 +30,23 @@
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/workspace-doc/doc-preview-document-actions.ts
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
1 src/renderer/src/components/editor/EditorAutosaveController.tsx
2 src/renderer/src/components/editor/EditorContent.tsx
1 src/renderer/src/components/editor/EditorDiffFileSurface.tsx
1 src/renderer/src/components/editor/EditorEditFileSurface.tsx
1 src/renderer/src/components/editor/EditorMarkdownFileSurface.tsx
2 src/renderer/src/components/editor/EditorPanel.tsx
4 src/renderer/src/components/editor/ExternalFileChangeBanner.tsx
3 src/renderer/src/components/editor/ExternalFileChangeCompareDialog.tsx
1 src/renderer/src/components/editor/MarkdownPreview.tsx
1 src/renderer/src/components/editor/MonacoGutterContextMenu.tsx
4 src/renderer/src/components/editor/RichMarkdownEditor.tsx
1 src/renderer/src/components/editor/combined-diff/CombinedDiffViewer.tsx
4 src/renderer/src/components/editor/combined-diff/load-sections/fetch-combined-diff-section.ts
1 src/renderer/src/components/editor/combined-diff/load-sections/use-combined-diff-section-loader.ts
2 src/renderer/src/components/editor/csv/CsvPagedViewer.tsx
1 src/renderer/src/components/editor/csv/csv-file-content.ts
1 src/renderer/src/components/editor/csv/csv-link-routing.ts
2 src/renderer/src/components/editor/editor-autosave-controller-test-fixture.ts
7 src/renderer/src/components/editor/editor-dropped-file-open.ts
1 src/renderer/src/components/editor/editor-header-file-rename.ts
1 src/renderer/src/components/editor/editor-model-lifetime-fixture.ts
6 src/renderer/src/components/editor/editor-restored-tab-conflict-scan.ts
1 src/renderer/src/components/editor/editor-save-queue.ts
1 src/renderer/src/components/editor/local-image-src-reader.ts
3 src/renderer/src/components/editor/markdown-document-list-request.ts
2 src/renderer/src/components/editor/markdown-preview-link-actions.ts
1 src/renderer/src/components/editor/markdown-preview-local-images.ts
1 src/renderer/src/components/editor/markdown-preview-link-actions.ts
1 src/renderer/src/components/editor/markdown-preview-system-link-action.ts
1 src/renderer/src/components/editor/restored-editor-workspace-runtime-owner.ts
1 src/renderer/src/components/editor/rich-markdown-editor-config.ts
2 src/renderer/src/components/editor/rich-markdown-image-context.ts
4 src/renderer/src/components/editor/rich-markdown-image-insert.ts
1 src/renderer/src/components/editor/rich-markdown-paste-handler.ts
3 src/renderer/src/components/editor/rich-markdown-paste-image.ts
1 src/renderer/src/components/editor/use-markdown-preview-components.tsx
1 src/renderer/src/components/editor/use-markdown-preview-foundation.ts
1 src/renderer/src/components/editor/use-markdown-preview-source-foundation.ts
4 src/renderer/src/components/editor/useEditorPanelContentState.ts
5 src/renderer/src/components/editor/useEditorPanelDiffContentLoader.ts
7 src/renderer/src/components/editor/useEditorPanelFileContentLoader.ts
1 src/renderer/src/components/editor/useLocalImagePick.ts
5 src/renderer/src/components/editor/useLocalImageSrc.ts
5 src/renderer/src/components/editor/useMarkdownDocuments.ts
1 src/renderer/src/components/editor/useRichMarkdownProgrammaticSync.ts
1 src/renderer/src/components/editor/useRichMarkdownReconcileRoundTrip.ts
1 src/renderer/src/components/editor/useEditorPanelFileContentLoader.ts
1 src/renderer/src/components/editor/useMarkdownDocuments.ts
2 src/renderer/src/components/feature-wall/ConnectIntegrationsList.tsx
1 src/renderer/src/components/feature-wall/FeatureWallBrowserAction.tsx
1 src/renderer/src/components/feature-wall/FeatureWallSetupChecklist.tsx
@@ -94,10 +62,8 @@
1 src/renderer/src/components/native-chat-resume-tree.test-support.ts
2 src/renderer/src/components/native-chat/NativeChatComposer.tsx
1 src/renderer/src/components/native-chat/NativeChatResolvedView.tsx
1 src/renderer/src/components/native-chat/NativeChatTranscriptChrome.tsx
1 src/renderer/src/components/native-chat/NativeChatVisualFrame.tsx
1 src/renderer/src/components/native-chat/claude-model-switch-confirmation.ts
4 src/renderer/src/components/native-chat/native-chat-file-link-existence.ts
1 src/renderer/src/components/native-chat/native-chat-file-link.ts
1 src/renderer/src/components/native-chat/native-chat-http-link-source-owner.ts
1 src/renderer/src/components/native-chat/native-chat-observed-send.ts
@@ -105,10 +71,7 @@
2 src/renderer/src/components/native-chat/native-chat-runtime-owner.ts
6 src/renderer/src/components/native-chat/native-chat-runtime-send.ts
2 src/renderer/src/components/native-chat/native-chat-session-option-discovery.ts
2 src/renderer/src/components/native-chat/use-native-chat-file-link-click.ts
1 src/renderer/src/components/native-chat/use-native-chat-file-link-existence.tsx
6 src/renderer/src/components/native-chat/use-native-chat-interactive-send.ts
1 src/renderer/src/components/native-chat/use-native-chat-mention-files.ts
2 src/renderer/src/components/native-chat/use-native-chat-picker-command-dispatch.ts
4 src/renderer/src/components/native-chat/use-native-chat-pty-composer-send.ts
2 src/renderer/src/components/native-chat/use-native-chat-session-option-command.ts
@@ -118,13 +81,10 @@
2 src/renderer/src/components/new-workspace/use-work-item-source-foundation.ts
1 src/renderer/src/components/new-workspace/use-work-item-source-search.ts
1 src/renderer/src/components/ports/WorkspacePortScanner.tsx
2 src/renderer/src/components/quick-open-file-list-test-harness.ts
6 src/renderer/src/components/quick-open-file-list.ts
4 src/renderer/src/components/quick-open-file-navigation.ts
2 src/renderer/src/components/quick-open-recent-validation.ts
1 src/renderer/src/components/quick-open-file-list-test-harness.ts
3 src/renderer/src/components/repo/CreateFromPicker.tsx
1 src/renderer/src/components/right-sidebar/ChecksPanel.tsx
3 src/renderer/src/components/right-sidebar/FileExplorer.tsx
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
@@ -136,9 +96,7 @@
3 src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-git-status-effects.tsx
3 src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-manual-refresh.tsx
1 src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-review-state.tsx
2 src/renderer/src/components/right-sidebar/file-explorer-deletion-editors.ts
1 src/renderer/src/components/right-sidebar/file-explorer-directory-listing.ts
4 src/renderer/src/components/right-sidebar/file-explorer-operation-owner.ts
1 src/renderer/src/components/right-sidebar/file-explorer-operation-owner.ts
1 src/renderer/src/components/right-sidebar/file-explorer-row-context-menu.tsx
1 src/renderer/src/components/right-sidebar/git-status-file-watch-refresh.ts
5 src/renderer/src/components/right-sidebar/git-status-refresh.ts
@@ -173,21 +131,10 @@
2 src/renderer/src/components/right-sidebar/use-create-pull-request-field-generation.ts
2 src/renderer/src/components/right-sidebar/use-create-pull-request-field-seeding.ts
1 src/renderer/src/components/right-sidebar/use-file-explorer-ignored-paths.ts
1 src/renderer/src/components/right-sidebar/use-file-explorer-name-filter.ts
2 src/renderer/src/components/right-sidebar/use-file-explorer-node-commands.ts
6 src/renderer/src/components/right-sidebar/use-file-explorer-tree-pane-state.ts
2 src/renderer/src/components/right-sidebar/use-file-explorer-tree-pane-state.ts
3 src/renderer/src/components/right-sidebar/use-git-status-upstream-ref-watch.ts
3 src/renderer/src/components/right-sidebar/useFileDeletion.ts
1 src/renderer/src/components/right-sidebar/useFileDuplicate.ts
2 src/renderer/src/components/right-sidebar/useFileExplorerDragDrop.ts
2 src/renderer/src/components/right-sidebar/useFileExplorerHandlers.ts
4 src/renderer/src/components/right-sidebar/useFileExplorerImport.ts
3 src/renderer/src/components/right-sidebar/useFileExplorerInlineInput.ts
1 src/renderer/src/components/right-sidebar/useFileExplorerMoveDrop.ts
2 src/renderer/src/components/right-sidebar/useFileExplorerTree.ts
2 src/renderer/src/components/right-sidebar/useFileExplorerWatch.ts
4 src/renderer/src/components/right-sidebar/useFileSearchPanel.ts
4 src/renderer/src/components/right-sidebar/useFileSearchRunner.ts
1 src/renderer/src/components/right-sidebar/useFileSearchRunner.ts
1 src/renderer/src/components/right-sidebar/useFileSearchScope.ts
2 src/renderer/src/components/right-sidebar/useGitStatusPolling.ts
3 src/renderer/src/components/settings/BaseRefPicker.tsx
@@ -197,7 +144,6 @@
1 src/renderer/src/components/settings/GeneralPane.tsx
1 src/renderer/src/components/settings/IntegrationsPane.tsx
3 src/renderer/src/components/settings/LinearAgentSkillPane.tsx
1 src/renderer/src/components/settings/McpConfigSection.tsx
1 src/renderer/src/components/settings/RepositoryForkSyncSection.tsx
2 src/renderer/src/components/settings/RepositoryGitHubAccountSection.tsx
1 src/renderer/src/components/settings/RepositoryHooksSection.tsx
@@ -213,7 +159,6 @@
3 src/renderer/src/components/settings/cli-source-control-integration-cards.tsx
3 src/renderer/src/components/settings/jira-integration-card.tsx
1 src/renderer/src/components/settings/linear-agent-skill-install-cta.tsx
1 src/renderer/src/components/settings/mcp-config-inspection.ts
1 src/renderer/src/components/settings/provider-account-scope.ts
1 src/renderer/src/components/settings/settings-persistence-test-fixture.ts
2 src/renderer/src/components/settings/settings-setup-guide-progress.ts
@@ -279,12 +224,8 @@
1 src/renderer/src/components/status-bar/use-status-bar-controller.ts
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/tab-bar/EditorFileTab.tsx
1 src/renderer/src/components/tab-bar/TabBar.tsx
2 src/renderer/src/components/tab-bar/TabBarCreateEntry.tsx
3 src/renderer/src/components/tab-bar/tab-bar-windows-shell-launch-test-harness.ts
2 src/renderer/src/components/tab-bar/tab-create-entry-action.ts
3 src/renderer/src/components/tab-bar/tab-create-entry-local-path.ts
1 src/renderer/src/components/tab-bar/use-tab-bar-runtime-model-worktree-write-probe.ts
4 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
@@ -298,26 +239,14 @@
1 src/renderer/src/components/terminal-pane/pty-connection/terminal-keydown-fit.ts
1 src/renderer/src/components/terminal-pane/retire-unbound-runtime-terminal-pane.ts
2 src/renderer/src/components/terminal-pane/terminal-drop-runtime-owner.ts
8 src/renderer/src/components/terminal-pane/terminal-file-link-actions.ts
1 src/renderer/src/components/terminal-pane/terminal-file-link-hit-testing.ts
2 src/renderer/src/components/terminal-pane/terminal-file-link-target.ts
6 src/renderer/src/components/terminal-pane/terminal-file-open-routing.ts
3 src/renderer/src/components/terminal-pane/terminal-file-path-mapping.ts
1 src/renderer/src/components/terminal-pane/terminal-fit-restore.ts
2 src/renderer/src/components/terminal-pane/terminal-host-workspace-file.ts
1 src/renderer/src/components/terminal-pane/terminal-input-host-platform.ts
2 src/renderer/src/components/terminal-pane/terminal-link-handlers.ts
1 src/renderer/src/components/terminal-pane/terminal-link-open-hints.ts
1 src/renderer/src/components/terminal-pane/terminal-link-provider-buffer-fixtures.ts
1 src/renderer/src/components/terminal-pane/terminal-native-file-drop.ts
2 src/renderer/src/components/terminal-pane/terminal-pane-close-admission.ts
1 src/renderer/src/components/terminal-pane/terminal-pane-menu-paste.ts
1 src/renderer/src/components/terminal-pane/terminal-pane-mount-preparation.ts
1 src/renderer/src/components/terminal-pane/terminal-pane-pane-links.ts
1 src/renderer/src/components/terminal-pane/terminal-pane-paste-execution.ts
1 src/renderer/src/components/terminal-pane/terminal-pane-paste-listeners.ts
3 src/renderer/src/components/terminal-pane/terminal-path-existence-batch.ts
2 src/renderer/src/components/terminal-pane/terminal-worktree-path-link.ts
1 src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts
1 src/renderer/src/components/terminal-pane/use-terminal-pane-controller.ts
1 src/renderer/src/components/terminal-pane/use-terminal-pane-mount-lifecycle.ts
@@ -327,7 +256,6 @@
1 src/renderer/src/components/terminal/running-terminal-close-guard.ts
1 src/renderer/src/components/terminal/terminal-tab-create.ts
1 src/renderer/src/components/terminal/window-close-running-work.ts
2 src/renderer/src/components/use-quick-open-interaction.ts
2 src/renderer/src/components/use-task-page-source-availability.ts
2 src/renderer/src/components/use-task-page-store-bindings.ts
2 src/renderer/src/components/use-terminal-activation-actions.ts
@@ -339,7 +267,7 @@
2 src/renderer/src/components/use-worktree-jump-palette-store-state.ts
2 src/renderer/src/hooks/automation-dispatch-handler.ts
1 src/renderer/src/hooks/automation-dispatch-workspace.ts
4 src/renderer/src/hooks/composer-state/attachment-drop-state.ts
1 src/renderer/src/hooks/composer-state/attachment-drop-state.ts
1 src/renderer/src/hooks/composer-state/composer-attachment-drop-state.ts
1 src/renderer/src/hooks/composer-state/composer-navigation-actions.ts
2 src/renderer/src/hooks/composer-state/composer-source-state.ts
@@ -350,9 +278,7 @@
1 src/renderer/src/hooks/composer-state/provider-runtime-sync.ts
1 src/renderer/src/hooks/composer-state/quick-creation-execution.ts
1 src/renderer/src/hooks/composer-state/workspace-identity-state.ts
1 src/renderer/src/hooks/editor-external-watch-disk-verification.ts
2 src/renderer/src/hooks/editor-external-watch-targets.ts
1 src/renderer/src/hooks/editor-runtime-file-watch.ts
1 src/renderer/src/hooks/ipc-events-close-routing-test-harness.ts
1 src/renderer/src/hooks/ipc-events-terminal-create-test-harness.ts
6 src/renderer/src/hooks/ipc-events-test-harness.ts
@@ -363,7 +289,7 @@
2 src/renderer/src/hooks/ipc-events/tab-lifecycle-ipc-bridge.ts
3 src/renderer/src/hooks/use-active-skill-discovery-runtime-target.ts
1 src/renderer/src/hooks/useActiveProjectSkillRuntime.ts
2 src/renderer/src/hooks/useEditorExternalWatch.ts
1 src/renderer/src/hooks/useEditorExternalWatch.ts
1 src/renderer/src/hooks/useInstalledAgentSkills.ts
1 src/renderer/src/hooks/useIpcEvents.ts
1 src/renderer/src/hooks/useIssueMetadata.ts
@@ -393,7 +319,6 @@
1 src/renderer/src/lib/execute-open-editor-path-move.ts
1 src/renderer/src/lib/execution-host-facts.ts
6 src/renderer/src/lib/file-preview.ts
1 src/renderer/src/lib/file-search-result-owner.ts
2 src/renderer/src/lib/folder-workspace-runtime-owner.ts
1 src/renderer/src/lib/github-pr-start-point.ts
1 src/renderer/src/lib/host-mirrored-pane-liveness.ts
@@ -407,14 +332,11 @@
1 src/renderer/src/lib/launch-work-item-direct-preflight.ts
2 src/renderer/src/lib/launch-work-item-direct.ts
1 src/renderer/src/lib/launch-worktree-background-terminals.ts
3 src/renderer/src/lib/local-file-access.ts
2 src/renderer/src/lib/local-preflight-context.ts
1 src/renderer/src/lib/markdown-document-templates.ts
1 src/renderer/src/lib/new-workspace.ts
1 src/renderer/src/lib/open-agent-message-sender.ts
2 src/renderer/src/lib/provider-runtime-context.ts
1 src/renderer/src/lib/remote-workspace-drop-test-fixtures.ts
2 src/renderer/src/lib/rename-file.ts
4 src/renderer/src/lib/repo-runtime-owner.ts
2 src/renderer/src/lib/resolve-owner.ts
1 src/renderer/src/lib/runtime-session-mirror-owners.ts
@@ -425,11 +347,9 @@
1 src/renderer/src/lib/terminal-tab-owner-activation.ts
4 src/renderer/src/lib/terminal-worktree-route.ts
3 src/renderer/src/lib/unread-badge-count-selector.ts
1 src/renderer/src/lib/user-opened-local-path.ts
2 src/renderer/src/lib/web-runtime-worktree-terminal-after-wake.ts
1 src/renderer/src/lib/workspace-browser-tab-close.ts
4 src/renderer/src/lib/workspace-browser-tab-open.ts
3 src/renderer/src/lib/workspace-file-host-routing.ts
1 src/renderer/src/lib/workspace-tab-palette-activation.ts
1 src/renderer/src/lib/workspace-terminal-host-authority.ts
1 src/renderer/src/lib/worktree-activation-pty-inventory.ts
@@ -444,17 +364,7 @@
3 src/renderer/src/lib/worktree-runtime-owner.ts
1 src/renderer/src/lib/worktree-sort-order-host-split.ts
1 src/renderer/src/runtime/close-mirrored-editor-tab.ts
3 src/renderer/src/runtime/mobile-markdown-bridge.ts
1 src/renderer/src/runtime/runtime-client-target.ts
2 src/renderer/src/runtime/runtime-file-import-client.ts
3 src/renderer/src/runtime/runtime-file-metadata-client.ts
8 src/renderer/src/runtime/runtime-file-mutation-client.ts
2 src/renderer/src/runtime/runtime-file-range-client.ts
10 src/renderer/src/runtime/runtime-file-read-client.ts
6 src/renderer/src/runtime/runtime-file-routing.ts
5 src/renderer/src/runtime/runtime-file-search-client.ts
1 src/renderer/src/runtime/runtime-file-upload-client.ts
1 src/renderer/src/runtime/runtime-file-watch-client.ts
2 src/renderer/src/runtime/runtime-git-client-context.ts
5 src/renderer/src/runtime/runtime-git-diff-client.ts
5 src/renderer/src/runtime/runtime-git-generation-client.ts
@@ -469,7 +379,6 @@
8 src/renderer/src/runtime/runtime-linear-client.ts
5 src/renderer/src/runtime/runtime-linear-issue-mutations.ts
12 src/renderer/src/runtime/runtime-linear-project-client.ts
1 src/renderer/src/runtime/runtime-path-existence-batch.ts
3 src/renderer/src/runtime/runtime-repo-client.ts
3 src/renderer/src/runtime/runtime-terminal-inspection.ts
1 src/renderer/src/runtime/runtime-terminal-stream.ts
@@ -527,12 +436,11 @@
4 src/renderer/src/store/slices/editor/actions/git-remote-push-pull.ts
1 src/renderer/src/store/slices/editor/actions/git-remote-status.ts
8 src/renderer/src/store/slices/editor/actions/git-remote-sync.ts
2 src/renderer/src/store/slices/editor/actions/markdown-link-action.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
1 src/renderer/src/store/slices/editor/actions/open-file-action.ts
1 src/renderer/src/store/slices/editor/actions/open-file-apply.ts
2 src/renderer/src/store/slices/editor/create-editor-slice.ts
1 src/renderer/src/store/slices/editor/tabs/untitled-file-cleanup.ts
1 src/renderer/src/store/slices/editor/create-editor-slice.ts
1 src/renderer/src/store/slices/github-cache-key.ts
1 src/renderer/src/store/slices/github-slice-test-harness.ts
1 src/renderer/src/store/slices/hosted-review-cache-identity.ts
-27
View File
@@ -2,22 +2,10 @@
# getActiveRuntimeTarget( + legacyRouteFromSettings( + settingsForRuntimeOwner(, per file.
# This is a RATCHET: counts may only go DOWN. Route new work by the resource owner instead.
# Prune after removing sites: pnpm check:owner-routing-ratchet --prune
1 src/renderer/src/components/editor/EditorPanel.tsx
1 src/renderer/src/components/editor/ExternalFileChangeBanner.tsx
1 src/renderer/src/components/editor/ExternalFileChangeCompareDialog.tsx
1 src/renderer/src/components/editor/combined-diff/load-sections/fetch-combined-diff-section.ts
2 src/renderer/src/components/editor/editor-restored-tab-conflict-scan.ts
1 src/renderer/src/components/editor/markdown-preview-link-actions.ts
1 src/renderer/src/components/editor/rich-markdown-image-context.ts
1 src/renderer/src/components/editor/rich-markdown-image-insert.ts
1 src/renderer/src/components/editor/rich-markdown-paste-image.ts
1 src/renderer/src/components/editor/use-markdown-preview-source-foundation.ts
1 src/renderer/src/components/editor/useEditorPanelDiffContentLoader.ts
1 src/renderer/src/components/editor/useEditorPanelFileContentLoader.ts
2 src/renderer/src/components/editor/useMarkdownDocuments.ts
1 src/renderer/src/components/linear-api-key-dialog.tsx
1 src/renderer/src/components/ports/WorkspacePortScanner.tsx
1 src/renderer/src/components/quick-open-file-navigation.ts
1 src/renderer/src/components/settings/GeneralPane.tsx
1 src/renderer/src/components/settings/RepositoryGitHubAccountSection.tsx
1 src/renderer/src/components/settings/RepositoryIconPicker.tsx
@@ -27,9 +15,6 @@
1 src/renderer/src/components/status-bar/use-codex-switcher-controller.ts
1 src/renderer/src/components/tab-bar/use-tab-bar-runtime-model-worktree-write-probe.ts
1 src/renderer/src/components/tab-bar/use-tab-bar-runtime-model.ts
1 src/renderer/src/components/terminal-pane/terminal-file-path-mapping.ts
1 src/renderer/src/components/terminal-pane/terminal-path-existence-batch.ts
1 src/renderer/src/components/terminal-pane/terminal-worktree-path-link.ts
1 src/renderer/src/hooks/composer-state/provider-runtime-sync.ts
1 src/renderer/src/hooks/composer-state/quick-creation-execution.ts
1 src/renderer/src/hooks/use-active-skill-discovery-runtime-target.ts
@@ -43,19 +28,8 @@
1 src/renderer/src/lib/github-pr-start-point.ts
1 src/renderer/src/lib/launch-agent-background-session.ts
1 src/renderer/src/lib/launch-worktree-background-terminals.ts
1 src/renderer/src/lib/local-file-access.ts
1 src/renderer/src/lib/structured-agent-session-tab-activation.ts
1 src/renderer/src/lib/workspace-file-host-routing.ts
1 src/renderer/src/lib/worktree-creation-flow-startup.ts
1 src/renderer/src/runtime/mobile-markdown-bridge.ts
1 src/renderer/src/runtime/runtime-file-import-client.ts
1 src/renderer/src/runtime/runtime-file-metadata-client.ts
1 src/renderer/src/runtime/runtime-file-mutation-client.ts
1 src/renderer/src/runtime/runtime-file-range-client.ts
2 src/renderer/src/runtime/runtime-file-read-client.ts
4 src/renderer/src/runtime/runtime-file-routing.ts
4 src/renderer/src/runtime/runtime-file-search-client.ts
1 src/renderer/src/runtime/runtime-file-watch-client.ts
1 src/renderer/src/runtime/runtime-git-client-context.ts
5 src/renderer/src/runtime/runtime-git-diff-client.ts
5 src/renderer/src/runtime/runtime-git-generation-client.ts
@@ -85,7 +59,6 @@
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/diff-comment-persistence.ts
1 src/renderer/src/store/slices/editor/actions/markdown-link-action.ts
1 src/renderer/src/store/slices/hosted-review.ts
1 src/renderer/src/store/slices/preflight.ts
1 src/renderer/src/store/slices/worktree-visibility-owner-settings.ts
@@ -22,6 +22,7 @@ import type {
} from '../describe-page/browser-page-types'
import type { MutableRefObject } from 'react'
import { statUserOpenedPath } from '@/lib/user-opened-local-path'
import { requireRuntimeTargetForFileOwner } from '@/lib/file-owner-runtime-target'
export type NavigateBrowserPageToUrlArgs = {
url: string
@@ -100,7 +101,7 @@ export function navigateBrowserPageToUrl({
// Why getKnownWorktreeById: folder and floating workspaces are absent from allWorktrees().
const activeWorktree = store.getKnownWorktreeById(worktreeId)
const fileContext: RuntimeFileOperationArgs = {
settings: store.settings,
target: requireRuntimeTargetForFileOwner(store, worktreeId, undefined),
worktreeId,
worktreePath: activeWorktree?.path,
connectionId: undefined
@@ -27,6 +27,26 @@ vi.mock('@/lib/connection-owner-resolution', () => ({
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: () => mocks.worktreeRuntimeOwnerId
}))
vi.mock('@/lib/file-owner-runtime-target', () => {
const getRuntimeTargetForFileOwner = (
_state: unknown,
_worktreeId: string | null | undefined,
runtimeEnvironmentId: string | null | undefined
) => {
const owner =
runtimeEnvironmentId === undefined ? mocks.worktreeRuntimeOwnerId : runtimeEnvironmentId
return owner
? { kind: 'environment' as const, environmentId: owner }
: { kind: 'local' as const }
}
return {
FILE_OWNER_UNRESOLVED_MESSAGE: 'unresolved',
getRuntimeTargetForFileOwner,
requireRuntimeTargetForFileOwner: getRuntimeTargetForFileOwner,
getRuntimeTargetForWorktreeOwner: (state: unknown, worktreeId: string | null | undefined) =>
getRuntimeTargetForFileOwner(state, worktreeId, undefined)
}
})
vi.mock('@/lib/connection-context', () => ({
getConnectionId: () => mocks.connectionIdForWorkspace,
getConnectionIdForFile: () => mocks.connectionIdForFile
@@ -4,7 +4,7 @@ import { getConnectionId } from '@/lib/connection-context'
import { detectLanguage } from '@/lib/language-detect'
import { canShowWorkspaceFileBrowserAction, openFilePreviewToSide } from '@/lib/file-preview'
import { getEditorHeaderCopyState } from './editor-header'
import { settingsForRuntimeOwner } from '@/runtime/runtime-rpc-client'
import { getRuntimeTargetForFileOwner } from '@/lib/file-owner-runtime-target'
import { exportActiveMarkdownToPdf } from './export-active-markdown'
import type { EditorToggleValue } from './EditorViewToggle'
import { EditorPanelShell } from './EditorPanelShell'
@@ -142,6 +142,16 @@ function EditorPanelInner({
closeRenameDialog,
handleRenameConfirm
} = useUntitledFileRename({ openFiles, clearUntitled })
// Why not local: an unresolved owner must not offer this machine's folder picker either.
const renameDialogFileOffClient = useAppStore(
(s) =>
!!renameDialogFile &&
getRuntimeTargetForFileOwner(
s,
renameDialogFile.worktreeId,
renameDialogFile.runtimeEnvironmentId
)?.kind !== 'local'
)
useMarkdownPreviewShortcut({ activeFile, panelRef, openMarkdownPreview })
@@ -279,10 +289,7 @@ function EditorPanelInner({
)
}
const disableRenameBrowse = Boolean(
settingsForRuntimeOwner(
settings,
renameDialogFile?.runtimeEnvironmentId
)?.activeRuntimeEnvironmentId?.trim() ||
renameDialogFileOffClient ||
(renameDialogFile ? getConnectionId(renameDialogFile.worktreeId) : null)
)
const markdownDocumentStateFileId =
@@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button'
import { getConnectionIdForFile } from '@/lib/connection-context'
import { editorTabFileAccess } from '@/lib/local-file-access'
import { readRuntimeFileContent } from '@/runtime/runtime-file-client'
import { settingsForRuntimeOwner } from '@/runtime/runtime-rpc-client'
import { getRuntimeTargetForFileOwner } from '@/lib/file-owner-runtime-target'
import { useAppStore } from '@/store'
import { translate } from '@/i18n/i18n'
import type { OpenFile } from '@/store/slices/editor'
@@ -87,8 +87,12 @@ export function keepTabEditsOverExternalChange(file: OpenFile): void {
// the dismissed conflict from the stale signature on every launch.
// Best-effort: a failed read leaves the old signature, which can only
// re-surface the banner — never lose data.
const target = getRuntimeTargetForFileOwner(state, file.worktreeId, file.runtimeEnvironmentId)
if (!target) {
return
}
void readRuntimeFileContent({
settings: settingsForRuntimeOwner(state.settings, file.runtimeEnvironmentId),
target,
filePath: file.filePath,
relativePath: file.relativePath,
worktreeId: file.worktreeId,
@@ -14,7 +14,10 @@ import { getConnectionIdForFile } from '@/lib/connection-context'
import { editorTabFileAccess } from '@/lib/local-file-access'
import { detectLanguage } from '@/lib/language-detect'
import { readRuntimeFileContent } from '@/runtime/runtime-file-client'
import { settingsForRuntimeOwner } from '@/runtime/runtime-rpc-client'
import {
FILE_OWNER_UNRESOLVED_MESSAGE,
getRuntimeTargetForFileOwner
} from '@/lib/file-owner-runtime-target'
import { useAppStore } from '@/store'
import type { OpenFile } from '@/store/slices/editor'
import { translate } from '@/i18n/i18n'
@@ -57,8 +60,17 @@ export function ExternalFileChangeCompareDialog({
setDiskState({ kind: 'loading' })
// Why: read at open time — the banner can be minutes old and the agent
// may have written again since; the comparison must show current disk.
const target = getRuntimeTargetForFileOwner(
useAppStore.getState(),
file.worktreeId,
file.runtimeEnvironmentId
)
if (!target) {
setDiskState({ kind: 'error', message: FILE_OWNER_UNRESOLVED_MESSAGE })
return
}
void readRuntimeFileContent({
settings: settingsForRuntimeOwner(useAppStore.getState().settings, file.runtimeEnvironmentId),
target,
filePath: file.filePath,
relativePath: file.relativePath,
worktreeId: file.worktreeId,
@@ -39,8 +39,25 @@ vi.mock('@/store', () => {
return { useAppStore }
})
vi.mock('@/store/slices/worktree-helpers', () => ({ findWorktreeById: () => null }))
vi.mock('@/runtime/runtime-rpc-client', () => ({
settingsForRuntimeOwner: (settings: unknown) => settings
vi.mock('@/lib/file-owner-runtime-target', () => {
const getRuntimeTargetForFileOwner = (
_state: unknown,
_worktreeId: string | null | undefined,
owner: string | null | undefined
) => (owner ? { kind: 'environment' as const, environmentId: owner } : { kind: 'local' as const })
return {
FILE_OWNER_UNRESOLVED_MESSAGE: 'unresolved',
getRuntimeTargetForFileOwner,
requireRuntimeTargetForFileOwner: getRuntimeTargetForFileOwner,
getRuntimeTargetForWorktreeOwner: (state: unknown, worktreeId: string | null | undefined) =>
getRuntimeTargetForFileOwner(state, worktreeId, undefined)
}
})
vi.mock('@/runtime/use-worktree-runtime-target', () => ({
useFileOwnerRuntimeTarget: (_worktreeId: unknown, runtimeEnvironmentId?: string | null) =>
runtimeEnvironmentId
? { kind: 'environment', environmentId: runtimeEnvironmentId }
: { kind: 'local' }
}))
vi.mock('@/runtime/runtime-file-client', () => ({
statRuntimePath: vi.fn(async () => ({ isDirectory: false }))
@@ -59,8 +59,25 @@ vi.mock('@/store/slices/worktree-helpers', () => ({
findWorktreeById: (_worktrees: unknown, id: string) =>
worktreeLookup.value.find((worktree) => worktree.id === id) ?? null
}))
vi.mock('@/runtime/runtime-rpc-client', () => ({
settingsForRuntimeOwner: (settings: unknown) => settings
vi.mock('@/lib/file-owner-runtime-target', () => {
const getRuntimeTargetForFileOwner = (
_state: unknown,
_worktreeId: string | null | undefined,
owner: string | null | undefined
) => (owner ? { kind: 'environment' as const, environmentId: owner } : { kind: 'local' as const })
return {
FILE_OWNER_UNRESOLVED_MESSAGE: 'unresolved',
getRuntimeTargetForFileOwner,
requireRuntimeTargetForFileOwner: getRuntimeTargetForFileOwner,
getRuntimeTargetForWorktreeOwner: (state: unknown, worktreeId: string | null | undefined) =>
getRuntimeTargetForFileOwner(state, worktreeId, undefined)
}
})
vi.mock('@/runtime/use-worktree-runtime-target', () => ({
useFileOwnerRuntimeTarget: (_worktreeId: unknown, runtimeEnvironmentId?: string | null) =>
runtimeEnvironmentId
? { kind: 'environment', environmentId: runtimeEnvironmentId }
: { kind: 'local' }
}))
vi.mock('@/runtime/runtime-file-client', () => ({
statRuntimePath: statRuntimePathMock
@@ -45,8 +45,25 @@ vi.mock('@/store', () => {
return { useAppStore }
})
vi.mock('@/store/slices/worktree-helpers', () => ({ findWorktreeById: () => null }))
vi.mock('@/runtime/runtime-rpc-client', () => ({
settingsForRuntimeOwner: (settings: unknown) => settings
vi.mock('@/lib/file-owner-runtime-target', () => {
const getRuntimeTargetForFileOwner = (
_state: unknown,
_worktreeId: string | null | undefined,
owner: string | null | undefined
) => (owner ? { kind: 'environment' as const, environmentId: owner } : { kind: 'local' as const })
return {
FILE_OWNER_UNRESOLVED_MESSAGE: 'unresolved',
getRuntimeTargetForFileOwner,
requireRuntimeTargetForFileOwner: getRuntimeTargetForFileOwner,
getRuntimeTargetForWorktreeOwner: (state: unknown, worktreeId: string | null | undefined) =>
getRuntimeTargetForFileOwner(state, worktreeId, undefined)
}
})
vi.mock('@/runtime/use-worktree-runtime-target', () => ({
useFileOwnerRuntimeTarget: (_worktreeId: unknown, runtimeEnvironmentId?: string | null) =>
runtimeEnvironmentId
? { kind: 'environment', environmentId: runtimeEnvironmentId }
: { kind: 'local' }
}))
vi.mock('@/runtime/runtime-file-client', () => ({
statRuntimePath: vi.fn(async () => ({ isDirectory: false }))
@@ -1,4 +1,5 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useFileOwnerRuntimeTarget } from '@/runtime/use-worktree-runtime-target'
import { useEditorState, type Editor } from '@tiptap/react'
import type { DiffComment } from '../../../../shared/diff-comment-types'
import { useAppStore } from '@/store'
@@ -56,8 +57,10 @@ export default function RichMarkdownEditor({
headerSlot
}: RichMarkdownEditorProps): React.JSX.Element {
const rootRef = useRef<HTMLDivElement | null>(null)
const settings = useAppStore((s) => s.settings)
const richMarkdownSpellcheckEnabled = settings?.richMarkdownSpellcheckEnabled ?? true
const richMarkdownSpellcheckEnabled = useAppStore(
(s) => s.settings?.richMarkdownSpellcheckEnabled ?? true
)
const runtimeTarget = useFileOwnerRuntimeTarget(worktreeId, runtimeEnvironmentId)
const editorFontZoomLevel = useAppStore((s) => s.editorFontZoomLevel)
const activateMarkdownLink = useAppStore((s) => s.activateMarkdownLink)
const addDiffComment = useAppStore((s) => s.addDiffComment)
@@ -200,7 +203,7 @@ export default function RichMarkdownEditor({
runtimeEnvironmentId,
isMac,
richMarkdownSpellcheckEnabled,
settings,
runtimeTarget,
activateMarkdownLink,
rootRef,
editorRef,
@@ -296,8 +299,7 @@ export default function RichMarkdownEditor({
baseCanonicalRef,
markdownDocuments,
rootRef,
runtimeEnvironmentId,
settings,
runtimeTarget,
slashMenuSetter: menu.setSlashMenu,
worktreeId,
worktreeRoot
@@ -11,6 +11,7 @@ import { EditorFileLoadErrorView } from '../EditorFileLoadErrorView'
import { useCsvColumnWidths } from './useCsvColumnWidths'
import { buildFileViewPreferenceKey } from '../file-view-preference-storage'
import { openCsvHttpLink } from './csv-link-routing'
import { runtimeTargetEnvironmentId } from '@/runtime/runtime-client-target'
export default function CsvPagedViewer({
file,
@@ -97,7 +98,7 @@ function CsvPagedTable({
preferenceKey ??
buildFileViewPreferenceKey({
worktreeId: file.readArgs.worktreeId ?? '',
runtimeEnvironmentId: file.readArgs.settings?.activeRuntimeEnvironmentId,
runtimeEnvironmentId: runtimeTargetEnvironmentId(file.readArgs.target),
externalSshTargetId: file.readArgs.connectionId,
filePath: file.readArgs.filePath
}),
@@ -124,7 +125,7 @@ function CsvPagedTable({
openCsvHttpLink(url, event, {
filePath: file.readArgs.filePath,
worktreeId: file.readArgs.worktreeId,
runtimeEnvironmentId: file.readArgs.settings?.activeRuntimeEnvironmentId,
runtimeEnvironmentId: runtimeTargetEnvironmentId(file.readArgs.target),
connectionId: file.readArgs.connectionId ?? null
})
}
@@ -15,7 +15,7 @@ vi.mock('./csv-preview-worker-client', () => ({
}))
const snapshot = { size: 1024 * 1024, mtime: 1, isDirectory: false }
const file = { readArgs: { settings: null, filePath: '/repo/a.csv' }, snapshot }
const file = { readArgs: { target: { kind: 'local' as const }, filePath: '/repo/a.csv' }, snapshot }
beforeEach(() => {
vi.resetAllMocks()
mocks.stat.mockResolvedValue(snapshot)
@@ -39,7 +39,7 @@ it('keeps the new viewport loader alive when a canceled delimiter scan rejects l
])
)
const file = {
readArgs: { settings: null, filePath: '/repo/a.csv' },
readArgs: { target: { kind: 'local' as const }, filePath: '/repo/a.csv' },
snapshot: { size: 20, mtime: 1, isDirectory: false }
}
const { result, rerender } = renderHook(({ delimiter }) => useCsvPagedPreview(file, delimiter), {
@@ -79,7 +79,7 @@ it('coalesces rapid scrolls and releases rows outside the latest viewport', asyn
)
)
const file = {
readArgs: { settings: null, filePath: '/repo/a.csv' },
readArgs: { target: { kind: 'local' as const }, filePath: '/repo/a.csv' },
snapshot: { size: 80, mtime: 1, isDirectory: false }
}
const { result } = renderHook(() => useCsvPagedPreview(file, ','))
@@ -11,9 +11,20 @@ vi.mock('@/lib/connection-context', () => ({ getConnectionId: () => null }))
vi.mock('@/lib/ssh-mutation-expectation', () => ({
captureWorktreeSshMutationExpectation: () => ({})
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: () => null
}))
vi.mock('@/lib/file-owner-runtime-target', () => {
const getRuntimeTargetForFileOwner = (
_state: unknown,
_worktreeId: string | null | undefined,
_runtimeEnvironmentId: string | null | undefined
) => ({ kind: 'local' as const })
return {
FILE_OWNER_UNRESOLVED_MESSAGE: 'unresolved',
getRuntimeTargetForFileOwner,
requireRuntimeTargetForFileOwner: getRuntimeTargetForFileOwner,
getRuntimeTargetForWorktreeOwner: (state: unknown, worktreeId: string | null | undefined) =>
getRuntimeTargetForFileOwner(state, worktreeId, undefined)
}
})
vi.mock('@/lib/user-opened-local-path', () => ({ statUserOpenedPath: mocks.statUserOpenedPath }))
vi.mock('@/store', () => ({
useAppStore: {
@@ -1,30 +1,28 @@
import { describe, expect, it } from 'vitest'
import {
getEditorFileDropOperationContext,
getEditorFileDropSettingsForWorktree,
shouldUploadRemoteEditorFileDrop
} from './editor-dropped-file-open'
import { getRuntimeTargetForWorktreeOwner } from '@/lib/file-owner-runtime-target'
describe('shouldUploadRemoteEditorFileDrop', () => {
it('does not upload editor drops for local workspaces', () => {
expect(shouldUploadRemoteEditorFileDrop({ activeRuntimeEnvironmentId: null }, null)).toBe(false)
expect(shouldUploadRemoteEditorFileDrop({ kind: 'local' }, null)).toBe(false)
})
it('uploads editor drops while a runtime environment is active', () => {
expect(shouldUploadRemoteEditorFileDrop({ activeRuntimeEnvironmentId: 'env-1' }, null)).toBe(
true
)
expect(
shouldUploadRemoteEditorFileDrop({ kind: 'environment', environmentId: 'env-1' }, null)
).toBe(true)
})
it('uploads editor drops for SSH workspaces', () => {
expect(shouldUploadRemoteEditorFileDrop({ activeRuntimeEnvironmentId: null }, 'ssh-1')).toBe(
true
)
expect(shouldUploadRemoteEditorFileDrop({ kind: 'local' }, 'ssh-1')).toBe(true)
})
it('uses the worktree owner runtime instead of the focused runtime', () => {
expect(
getEditorFileDropSettingsForWorktree(
getRuntimeTargetForWorktreeOwner(
{
settings: { activeRuntimeEnvironmentId: 'focused-runtime' },
repos: [{ id: 'repo-1', connectionId: null, executionHostId: 'runtime:owner-runtime' }],
@@ -32,12 +30,12 @@ describe('shouldUploadRemoteEditorFileDrop', () => {
},
'wt-1'
)
).toEqual({ activeRuntimeEnvironmentId: 'owner-runtime' })
).toEqual({ kind: 'environment', environmentId: 'owner-runtime' })
})
it('keeps explicit local worktree editor drops local while a runtime is focused', () => {
expect(
getEditorFileDropSettingsForWorktree(
getRuntimeTargetForWorktreeOwner(
{
settings: { activeRuntimeEnvironmentId: 'focused-runtime' },
repos: [{ id: 'repo-1', connectionId: null, executionHostId: 'local' }],
@@ -45,7 +43,7 @@ describe('shouldUploadRemoteEditorFileDrop', () => {
},
'wt-1'
)
).toEqual({ activeRuntimeEnvironmentId: null })
).toEqual({ kind: 'local' })
})
it('builds file operation context from the worktree owner instead of global focus', () => {
@@ -61,7 +59,7 @@ describe('shouldUploadRemoteEditorFileDrop', () => {
undefined
)
).toEqual({
settings: { activeRuntimeEnvironmentId: null },
target: { kind: 'local' as const },
worktreeId: 'wt-1',
worktreePath: '/repos/repo-1',
connectionId: undefined
@@ -81,7 +79,7 @@ describe('shouldUploadRemoteEditorFileDrop', () => {
'ssh-1'
)
).toEqual({
settings: { activeRuntimeEnvironmentId: null },
target: { kind: 'local' as const },
worktreeId: 'wt-1',
worktreePath: '/home/orca/repo-1',
connectionId: 'ssh-1'
@@ -4,39 +4,27 @@ import { isPathInsideWorktree, toWorktreeRelativePath } from '@/lib/terminal-lin
import { useAppStore } from '@/store'
import { getConnectionId } from '@/lib/connection-context'
import { joinPath } from '@/lib/path'
import {
getRuntimeEnvironmentIdForWorktree,
type WorktreeRuntimeOwnerState
} from '@/lib/worktree-runtime-owner'
import type { WorktreeRuntimeOwnerState } from '@/lib/worktree-runtime-owner'
import {
importExternalPathsToRuntime,
type RuntimeFileOperationArgs
} from '@/runtime/runtime-file-client'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import {
runtimeTargetEnvironmentId,
type RuntimeClientTarget
} from '@/runtime/runtime-client-target'
import { translate } from '@/i18n/i18n'
import { captureWorktreeSshMutationExpectation } from '@/lib/ssh-mutation-expectation'
import { statUserOpenedPath } from '@/lib/user-opened-local-path'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants'
import { openDocumentInFloatingWorkspace } from '@/lib/open-document-in-floating-workspace'
export function getEditorFileDropSettingsForWorktree(
store: WorktreeRuntimeOwnerState,
worktreeId: string
): Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> {
const runtimeEnvironmentId = getRuntimeEnvironmentIdForWorktree(store, worktreeId)
// Why: OS drops target the selected worktree. Use that worktree's host owner
// so a focused runtime cannot hijack local/SSH editor drops.
return {
...store.settings,
activeRuntimeEnvironmentId: runtimeEnvironmentId
}
}
import { requireRuntimeTargetForFileOwner } from '@/lib/file-owner-runtime-target'
export function shouldUploadRemoteEditorFileDrop(
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined,
target: RuntimeClientTarget,
connectionId: string | null | undefined
): boolean {
return Boolean(settings?.activeRuntimeEnvironmentId?.trim() || connectionId?.trim())
return target.kind === 'environment' || Boolean(connectionId?.trim())
}
export function getEditorFileDropOperationContext(
@@ -46,7 +34,8 @@ export function getEditorFileDropOperationContext(
connectionId: string | undefined
): RuntimeFileOperationArgs {
return {
settings: getEditorFileDropSettingsForWorktree(store, worktreeId),
// Why: OS drops target the selected worktree's owner, never the focused server.
target: requireRuntimeTargetForFileOwner(store, worktreeId, undefined),
worktreeId,
worktreePath,
connectionId
@@ -89,7 +78,7 @@ function sameEditorFileDropOwner(a: EditorFileDropContext, b: EditorFileDropCont
return (
a.worktreePath === b.worktreePath &&
a.connectionId === b.connectionId &&
left.settings?.activeRuntimeEnvironmentId === right.settings?.activeRuntimeEnvironmentId &&
runtimeTargetEnvironmentId(left.target) === runtimeTargetEnvironmentId(right.target) &&
left.expectedExecutionHostId === right.expectedExecutionHostId &&
left.expectedSshTargetId === right.expectedSshTargetId &&
left.expectedSshConnectionGeneration === right.expectedSshConnectionGeneration
@@ -149,9 +138,8 @@ async function openEditorFileDropPaths(
const store = useAppStore.getState()
const { worktreeId, groupId } = destination
const groupOptions = groupId ? { targetGroupId: groupId } : null
const dropSettings = fileContext.settings
const runtimeEnvironmentId = dropSettings?.activeRuntimeEnvironmentId ?? null
if (shouldUploadRemoteEditorFileDrop(dropSettings, connectionId)) {
const runtimeEnvironmentId = runtimeTargetEnvironmentId(fileContext.target)
if (shouldUploadRemoteEditorFileDrop(fileContext.target, connectionId)) {
if (!worktreePath) {
toast.error(
translate(
@@ -5,7 +5,10 @@ import type { OpenFile } from '@/store/slices/editor'
import { getConnectionIdForFile } from '@/lib/connection-context'
import { editorTabFileAccess } from '@/lib/local-file-access'
import { readRuntimeFileContent } from '@/runtime/runtime-file-client'
import { settingsForRuntimeOwner } from '@/runtime/runtime-rpc-client'
import {
getRuntimeTargetForFileOwner,
requireRuntimeTargetForFileOwner
} from '@/lib/file-owner-runtime-target'
import { canAutoSaveOpenFile } from './editor-autosave'
import { getDiskBaselineSignature } from './diff-content-signature'
import { markFileChangedOnDisk } from './editor-changed-on-disk-mark'
@@ -40,8 +43,13 @@ export function attachRestoredTabConflictScan(store: AppStoreApi): () => void {
// Only local/SSH paths can be probed: for runtime-owned files window.api.fs would stat the client path and misreport it as gone.
const probeFileMissing = async (file: OpenFile): Promise<boolean> => {
const settings = settingsForRuntimeOwner(store.getState().settings, file.runtimeEnvironmentId)
if (settings?.activeRuntimeEnvironmentId?.trim()) {
const owner = getRuntimeTargetForFileOwner(
store.getState(),
file.worktreeId,
file.runtimeEnvironmentId
)
// Why `!owner`: with no single owner, a client stat cannot prove the file gone.
if (owner?.kind !== 'local') {
return false
}
try {
@@ -65,7 +73,7 @@ export function attachRestoredTabConflictScan(store: AppStoreApi): () => void {
try {
const state = store.getState()
const result = await readRuntimeFileContent({
settings: settingsForRuntimeOwner(state.settings, file.runtimeEnvironmentId),
target: requireRuntimeTargetForFileOwner(state, file.worktreeId, file.runtimeEnvironmentId),
filePath: file.filePath,
relativePath: file.relativePath,
worktreeId: file.worktreeId,
@@ -8,7 +8,7 @@ import {
function context(overrides: Partial<RuntimeFileOperationArgs> = {}): RuntimeFileOperationArgs {
return {
settings: { activeRuntimeEnvironmentId: null },
target: { kind: 'local' as const },
worktreeId: 'worktree-1',
worktreePath: '/repo',
...overrides
@@ -171,7 +171,9 @@ describe('shared Markdown document list requests', () => {
it('keeps runtime routes isolated and encodes unusual paths losslessly', () => {
const local = context()
const ssh = context({ connectionId: 'ssh-1' })
const runtime = context({ settings: { activeRuntimeEnvironmentId: ' runtime-1 ' } })
const runtime = context({
target: { kind: 'environment' as const, environmentId: ' runtime-1 ' }
})
const unusualRoot = '/repo\nwith-newline'
const windowsRoot = 'C:\\repo\\docs'
const uncRoot = '\\\\server\\share\\repo'
@@ -27,7 +27,7 @@ export function getMarkdownDocumentListRequestKey(
rootPath: string
): string {
return JSON.stringify([
context.settings?.activeRuntimeEnvironmentId?.trim() ?? '',
context.target.kind === 'environment' ? context.target.environmentId : '',
context.connectionId ?? '',
context.worktreeId ?? '',
context.worktreePath ?? '',
@@ -6,7 +6,7 @@ import { isLocalPathOpenBlocked, showLocalPathOpenBlockedToast } from '@/lib/loc
import { getLinkSourceLocalOpenOwner } from '@/lib/link-source-local-open-owner'
import { openHttpLink } from '@/lib/http-link-routing'
import { translate } from '@/i18n/i18n'
import { settingsForRuntimeOwner } from '@/runtime/runtime-rpc-client'
import { requireRuntimeTargetForFileOwner } from '@/lib/file-owner-runtime-target'
import { statRuntimePath } from '@/runtime/runtime-file-client'
import { useAppStore } from '@/store'
import { relativePathInsideRoot } from '../../../../shared/cross-platform-path'
@@ -171,8 +171,9 @@ export async function handleMarkdownPreviewLinkClick({
try {
const stats = await statRuntimePath(
{
settings: settingsForRuntimeOwner(
useAppStore.getState().settings,
target: requireRuntimeTargetForFileOwner(
useAppStore.getState(),
targetWorktree.id,
resolvedSourceRuntimeEnvironmentId
),
worktreeId: targetWorktree.id,
@@ -130,7 +130,7 @@ describe('extractMarkdownPreviewLocalImageCandidates', () => {
'/repo/docs/readme.md',
{
runtimeContext: {
settings: { activeRuntimeEnvironmentId: 'env-1' },
target: { kind: 'environment' as const, environmentId: 'env-1' },
worktreeId: 'wt-1',
worktreePath: '/repo'
}
@@ -141,7 +141,7 @@ describe('extractMarkdownPreviewLocalImageCandidates', () => {
'/repo/docs/readme.md',
{
runtimeContext: {
settings: { activeRuntimeEnvironmentId: 'env-2' },
target: { kind: 'environment' as const, environmentId: 'env-2' },
worktreeId: 'wt-1',
worktreePath: '/repo'
}
@@ -31,7 +31,8 @@ type MarkdownImageAstNode = {
type ExtractLocalImageCandidatesOptions = {
connectionId?: string | null
limit?: number
runtimeContext?: MarkdownImageRuntimeContext
/** `null`: the owner is unresolved, so nothing is read. */
runtimeContext?: MarkdownImageRuntimeContext | null
}
type PrewarmMarkdownPreviewLocalImagesOptions = ExtractLocalImageCandidatesOptions & {
@@ -70,7 +71,7 @@ export function extractMarkdownPreviewLocalImageCandidates(
options: ExtractLocalImageCandidatesOptions = {}
): MarkdownPreviewLocalImageCandidate[] {
const limit = Math.max(0, options.limit ?? MARKDOWN_PREVIEW_LOCAL_IMAGE_PREWARM_LIMIT)
if (limit === 0 || !markdown.includes('!')) {
if (limit === 0 || options.runtimeContext === null || !markdown.includes('!')) {
return []
}
@@ -98,7 +99,8 @@ export function extractMarkdownPreviewLocalImageCandidates(
const cacheKey = getLocalImageCacheKey(
absolutePath,
options.connectionId,
options.runtimeContext,
// Null returned early above.
options.runtimeContext ?? undefined,
documentResourceAccess(filePath)
)
if (seenCacheKeys.has(cacheKey)) {
@@ -125,7 +125,7 @@ describe('a restored tab whose workspace moved to its managed server', () => {
await act(async () => load!(FILE_PATH, file.id, WORKTREE, 'README.md'))
expect(mocks.readRuntimeFileContent).toHaveBeenCalledTimes(1)
expect(mocks.readRuntimeFileContent.mock.calls[0]?.[0]).toMatchObject({
settings: { activeRuntimeEnvironmentId: 'env-1' },
target: { kind: 'environment', environmentId: 'env-1' },
worktreeId: WORKTREE
})
expect(fileContents[file.id]).toMatchObject({ content: '# repo' })
@@ -18,7 +18,7 @@ vi.mock('@/lib/connection-context', () => ({ getConnectionId: vi.fn(() => null)
vi.mock('@/store', () => ({
useAppStore: {
getState: vi.fn(() => ({
settings: null,
target: { kind: 'local' as const },
folderWorkspaces: [],
worktreesByRepo: {},
openFiles: []
@@ -460,7 +460,7 @@ describe('rich Markdown asynchronous image paste', () => {
for (const phase of ['saving', 'importing']) {
const editor = richEditor()
const runtimeContext: RichMarkdownImageRuntimeContext = {
settings: null,
target: { kind: 'local' as const },
worktreeId: 'wt-1',
worktreePath: '/repo',
connectionId: null
@@ -491,7 +491,10 @@ describe('rich Markdown asynchronous image paste', () => {
} else if (field === 'root') {
nextContext.runtimeContext.worktreePath = '/other-repo'
} else if (field === 'runtime') {
nextContext.runtimeContext.settings = { activeRuntimeEnvironmentId: 'other-runtime' }
nextContext.runtimeContext.target = {
kind: 'environment',
environmentId: 'other-runtime'
}
} else if (field === 'connection') {
nextContext.runtimeContext.connectionId = 'other-connection'
} else {
@@ -11,7 +11,6 @@ import {
} from '@/lib/workspace-browser-tab-open'
import { isLocalPathOpenBlocked, showLocalPathOpenBlockedToast } from '@/lib/local-path-open-guard'
import { getLinkSourceLocalOpenOwner } from '@/lib/link-source-local-open-owner'
import type { settingsForRuntimeOwner } from '@/runtime/runtime-rpc-client'
import {
absolutePathToFileUri as toFileUrlForOsEscape,
resolveMarkdownLinkTarget
@@ -40,8 +39,6 @@ export type ActivateMarkdownLink = (
}
) => void | Promise<unknown>
export type RichMarkdownRuntimeSettings = Parameters<typeof settingsForRuntimeOwner>[0]
type RichMarkdownEditorClickRoutingOptions = {
activateMarkdownLink: ActivateMarkdownLink
editorRef: MutableRefObject<Editor | null>
@@ -51,7 +51,7 @@ function createConfigParams(overrides: Partial<EditorConfigParams> = {}): Editor
runtimeEnvironmentId: null,
isMac: false,
richMarkdownSpellcheckEnabled: true,
settings: { activeRuntimeEnvironmentId: null },
runtimeTarget: { kind: 'local' },
activateMarkdownLink: vi.fn(),
rootRef: ref<HTMLDivElement | null>(null),
editorRef: ref<Editor | null>(null),
@@ -19,8 +19,7 @@ import { isSingleEmptyTopLevelOrderedList } from './rich-markdown-list-continuat
import type { LinkBubbleState } from './RichMarkdownLinkBubble'
import {
handleRichMarkdownEditorClick,
type ActivateMarkdownLink,
type RichMarkdownRuntimeSettings
type ActivateMarkdownLink
} from './rich-markdown-editor-click-routing'
import { createRichMarkdownKeyHandler } from './rich-markdown-key-handler'
import { commitRichMarkdownSerialization } from './rich-markdown-serialization-commit'
@@ -28,6 +27,7 @@ import {
createRichMarkdownImageResolverContext,
setRichMarkdownImageResolverContext
} from './rich-markdown-image-context'
import type { RuntimeClientTarget } from '@/runtime/runtime-client-target'
import { getRichMarkdownSpellcheckAttribute } from './rich-markdown-spellcheck'
import type { MutableRefObject, Dispatch, SetStateAction } from 'react'
import type { DiffComment } from '../../../../shared/diff-comment-types'
@@ -49,7 +49,8 @@ export type EditorConfigParams = {
runtimeEnvironmentId?: string | null
isMac: boolean
richMarkdownSpellcheckEnabled: boolean
settings: RichMarkdownRuntimeSettings
/** The document owner's transport, for image reads. */
runtimeTarget: RuntimeClientTarget | null
activateMarkdownLink: ActivateMarkdownLink
rootRef: MutableRefObject<HTMLDivElement | null>
editorRef: MutableRefObject<Editor | null>
@@ -103,7 +104,7 @@ export function createRichMarkdownEditorConfig(params: EditorConfigParams): UseE
runtimeEnvironmentId,
isMac,
richMarkdownSpellcheckEnabled,
settings,
runtimeTarget,
activateMarkdownLink,
rootRef,
editorRef,
@@ -247,8 +248,7 @@ export function createRichMarkdownEditorConfig(params: EditorConfigParams): UseE
createRichMarkdownImageResolverContext({
filePath,
externalSshTargetId,
runtimeEnvironmentId,
settings,
runtimeTarget,
worktreeId,
worktreeRoot
})
@@ -44,7 +44,10 @@ import { RichMarkdownCodeBlockLowlight } from './rich-markdown-lowlight'
import { RichMarkdownTaskList } from './rich-markdown-task-list'
import { createCachedLowlight } from './rich-markdown-lowlight-cache'
import { documentResourceAccess } from '@/lib/local-file-access'
import { resolveRichMarkdownImageUrl } from './rich-markdown-image-context'
import {
readRichMarkdownImageRuntimeContext,
resolveRichMarkdownImageUrl
} from './rich-markdown-image-context'
const lowlight = createCachedLowlight(createLowlight(common))
@@ -143,9 +146,7 @@ export function createRichMarkdownExtensions({
releaseImageLease?.()
releaseImageLease = undefined
const fp = this.storage.filePath as string
const runtimeContext = this.storage.runtimeContext as
| RuntimeFileOperationArgs
| undefined
const runtimeContext = readRichMarkdownImageRuntimeContext(this.storage)
const contextVersionAtLoad = getImageContextVersion(this.storage)
if (src && fp) {
const access = documentResourceAccess(fp)
@@ -0,0 +1,58 @@
// @vitest-environment happy-dom
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
createRichMarkdownImageResolverContext,
readRichMarkdownImageRuntimeContext
} from './rich-markdown-image-context'
import { prewarmMarkdownPreviewLocalImages } from './markdown-preview-local-images'
import { loadLocalImageSrc, resetLocalImageSrcStateForTests } from './useLocalImageSrc'
vi.mock('@/lib/connection-context', () => ({ getConnectionId: () => null }))
describe('image reads for a document whose owner is unresolved', () => {
afterEach(() => {
resetLocalImageSrcStateForTests()
vi.unstubAllGlobals()
})
it('never reads the image from this machine', async () => {
const readFile = vi.fn(async () => ({ content: '', isBinary: true }))
vi.stubGlobal('window', { ...window, api: { fs: { readFile } } })
const context = createRichMarkdownImageResolverContext({
filePath: '/srv/repo/doc.md',
runtimeTarget: null,
worktreeId: 'repo::/srv/repo',
worktreeRoot: '/srv/repo'
})
// `undefined` would mean a plain local file; an unresolved owner must block instead.
expect(context.runtimeContext).toBeNull()
// The editor's image node view reads the stored context back; null must survive that.
expect(
readRichMarkdownImageRuntimeContext({ runtimeContext: context.runtimeContext })
).toBeNull()
await expect(
loadLocalImageSrc('./shot.png', '/srv/repo/doc.md', undefined, context.runtimeContext)
).resolves.toBeNull()
const loadImage = vi.fn(async () => null)
prewarmMarkdownPreviewLocalImages('![shot](./shot.png)', '/srv/repo/doc.md', {
runtimeContext: null,
loadImage
})
await Promise.resolve()
expect(loadImage).not.toHaveBeenCalled()
expect(readFile).not.toHaveBeenCalled()
})
it('keeps no workspace distinct from an unresolved owner', () => {
expect(
createRichMarkdownImageResolverContext({
filePath: '/notes/doc.md',
runtimeTarget: { kind: 'local' },
worktreeId: '',
worktreeRoot: null
}).runtimeContext
).toBeUndefined()
})
})
@@ -1,6 +1,6 @@
import type { Editor } from '@tiptap/core'
import { getConnectionId } from '@/lib/connection-context'
import { settingsForRuntimeOwner } from '@/runtime/runtime-rpc-client'
import type { RuntimeClientTarget } from '@/runtime/runtime-client-target'
import type { RuntimeFileOperationArgs } from '@/runtime/runtime-file-client'
export type RichMarkdownImageRuntimeContext = Omit<RuntimeFileOperationArgs, 'connectionId'> & {
@@ -10,11 +10,10 @@ export type RichMarkdownImageRuntimeContext = Omit<RuntimeFileOperationArgs, 'co
export type RichMarkdownImageResolverContext = {
filePath: string
imageUrls?: Record<string, string>
runtimeContext?: RichMarkdownImageRuntimeContext
/** `null`: the owner is unresolved, so local images must not load; `undefined`: no workspace. */
runtimeContext?: RichMarkdownImageRuntimeContext | null
}
export type RichMarkdownImageResolverSettings = Parameters<typeof settingsForRuntimeOwner>[0]
type RichMarkdownImageUrls = Record<string, string>
function isRichMarkdownImageUrls(value: unknown): value is RichMarkdownImageUrls {
@@ -40,10 +39,30 @@ type RichMarkdownImageStorage = {
filePath: string
imageUrls?: Record<string, string>
reloadListeners?: Set<() => void>
runtimeContext?: RichMarkdownImageRuntimeContext
runtimeContext?: RichMarkdownImageRuntimeContext | null
}
}
/** The image node view's storage; its context is validated on read. */
export type RichMarkdownImageNodeStorage = { runtimeContext?: unknown }
/** The stored image context; `null` (unresolved owner) is kept so readers refuse instead of going local. */
export function readRichMarkdownImageRuntimeContext(
storage: RichMarkdownImageNodeStorage
): RichMarkdownImageRuntimeContext | null | undefined {
const value = storage.runtimeContext
if (value === null) {
return null
}
return isRichMarkdownImageRuntimeContext(value) ? value : undefined
}
function isRichMarkdownImageRuntimeContext(
value: unknown
): value is RichMarkdownImageRuntimeContext {
return typeof value === 'object' && value !== null && 'target' in value && 'worktreeId' in value
}
export function getRichMarkdownImageResolverContextVersion(editor: Editor): number {
const image: unknown = 'image' in editor.storage ? editor.storage.image : null
if (!image || typeof image !== 'object') {
@@ -56,29 +75,30 @@ export function getRichMarkdownImageResolverContextVersion(editor: Editor): numb
export function createRichMarkdownImageResolverContext({
filePath,
externalSshTargetId,
runtimeEnvironmentId,
settings,
runtimeTarget,
worktreeId,
worktreeRoot
}: {
filePath: string
externalSshTargetId?: string
runtimeEnvironmentId?: string | null
settings: RichMarkdownImageResolverSettings
/** The document owner's transport; `null` while unresolved, which blocks local image reads. */
runtimeTarget: RuntimeClientTarget | null
worktreeId: string
worktreeRoot: string | null
}): RichMarkdownImageResolverContext {
return {
filePath,
runtimeContext: worktreeRoot
? {
settings: settingsForRuntimeOwner(settings, runtimeEnvironmentId),
worktreeId,
worktreePath: worktreeRoot,
connectionId: getConnectionId(worktreeId),
expectedExternalSshTargetId: externalSshTargetId
}
: undefined
runtimeContext: !worktreeRoot
? undefined
: runtimeTarget
? {
target: runtimeTarget,
worktreeId,
worktreePath: worktreeRoot,
connectionId: getConnectionId(worktreeId),
expectedExternalSshTargetId: externalSshTargetId
}
: null
}
}
@@ -117,7 +137,11 @@ function getRichMarkdownImageContextSignature(context: RichMarkdownImageResolver
return [
context.filePath,
JSON.stringify(context.imageUrls ?? {}),
context.runtimeContext?.settings?.activeRuntimeEnvironmentId?.trim() ?? 'client',
context.runtimeContext === null
? 'unresolved'
: context.runtimeContext?.target.kind === 'environment'
? context.runtimeContext.target.environmentId
: 'client',
context.runtimeContext?.connectionId ?? 'local',
context.runtimeContext?.expectedExternalSshTargetId ?? '',
context.runtimeContext?.worktreeId ?? 'unknown-worktree',
@@ -6,7 +6,7 @@ import { useAppStore } from '@/store'
import { importExternalPathsToRuntime } from '@/runtime/runtime-file-client'
import { getEditorFileOperationContext } from '@/lib/editor-file-operation-owner'
import { editorTabDocumentFolderAccess } from '@/lib/local-file-access'
import { settingsForRuntimeOwner } from '@/runtime/runtime-rpc-client'
import { requireRuntimeTargetForFileOwner } from '@/lib/file-owner-runtime-target'
import { captureDirectSshMutationExpectation } from '@/lib/ssh-mutation-expectation'
import { translate } from '@/i18n/i18n'
import { parseWorkspaceKey } from '../../../../shared/workspace-scope'
@@ -46,7 +46,7 @@ export async function insertRichMarkdownImageFromPath({
worktreeId && parsedWorkspace?.type !== 'folder'
? getEditorFileOperationContext(state, { worktreeId, runtimeEnvironmentId }, worktreePath)
: {
settings: settingsForRuntimeOwner(state.settings, runtimeEnvironmentId),
target: requireRuntimeTargetForFileOwner(state, worktreeId, runtimeEnvironmentId),
worktreeId,
worktreePath,
connectionId,
@@ -57,8 +57,7 @@ export async function insertRichMarkdownImageFromPath({
? captureDirectSshMutationExpectation(state, connectionId, runtimeEnvironmentId)
: {})
}
const settings = fileContext.settings
if (settings?.activeRuntimeEnvironmentId?.trim() && !worktreePath) {
if (fileContext.target.kind === 'environment' && !worktreePath) {
toast.error(
translate(
'auto.components.editor.useLocalImagePick.91d835dc88',
@@ -185,7 +185,7 @@ describe('large Markdown paste selection during yields', () => {
...context,
runtimeContext: {
connectionId: 'another-target',
settings: null,
target: { kind: 'local' as const },
worktreeId: 'workspace',
worktreePath: '/repo'
}
@@ -2,7 +2,7 @@ import type { Editor } from '@tiptap/react'
import { toast } from 'sonner'
import { getConnectionId } from '@/lib/connection-context'
import { useAppStore } from '@/store'
import { settingsForRuntimeOwner } from '@/runtime/runtime-rpc-client'
import { requireRuntimeTargetForFileOwner } from '@/lib/file-owner-runtime-target'
import { extractIpcErrorMessage } from './rich-markdown-ipc-error-message'
import { insertRichMarkdownImageFromPath } from './rich-markdown-image-insert'
import { captureRichMarkdownImageInsertionTarget } from './rich-markdown-image-insertion-target'
@@ -73,8 +73,9 @@ async function saveClipboardImageForMarkdownPaste(
worktreeId: string | null,
runtimeEnvironmentId?: string | null
): Promise<string | null> {
const settings = settingsForRuntimeOwner(useAppStore.getState().settings, runtimeEnvironmentId)
const hasRuntimeOwner = Boolean(settings?.activeRuntimeEnvironmentId?.trim())
const hasRuntimeOwner =
requireRuntimeTargetForFileOwner(useAppStore.getState(), worktreeId, runtimeEnvironmentId)
.kind === 'environment'
// Why: runtime-owned notes use runtime-side clipboard import; routing this
// temp save through SSH would put the source file on the wrong machine.
const connectionId = hasRuntimeOwner ? undefined : (getConnectionId(worktreeId) ?? undefined)
@@ -77,7 +77,7 @@ describe('editor group OS file drops on remote workspaces', () => {
worktreeId: 'wt-ssh',
worktreePath: SSH_PATH,
connectionId: 'ssh-1',
settings: { activeRuntimeEnvironmentId: null },
target: { kind: 'local' },
expectedExecutionHostId: 'ssh:ssh-1',
expectedSshTargetId: 'ssh-1',
expectedSshConnectionGeneration: 3
@@ -99,7 +99,7 @@ describe('editor group OS file drops on remote workspaces', () => {
await waitFor(() => expect(mocks.openFile).toHaveBeenCalledTimes(1))
expect(mocks.importPaths.mock.calls[0][0]).toMatchObject({
worktreeId: 'wt-runtime',
settings: { activeRuntimeEnvironmentId: 'owner-runtime' }
target: { kind: 'environment', environmentId: 'owner-runtime' }
})
expect(mocks.importPaths.mock.calls[0][2]).toBe(joinPath(RUNTIME_PATH, '.orca/drops'))
expect(mocks.openFile).toHaveBeenCalledWith(
@@ -34,9 +34,20 @@ vi.mock('@/lib/ssh-mutation-expectation', () => ({
return { expectedExecutionHostId: 'local' }
}
}))
vi.mock('@/lib/worktree-runtime-owner', () => ({
getRuntimeEnvironmentIdForWorktree: () => null
}))
vi.mock('@/lib/file-owner-runtime-target', () => {
const getRuntimeTargetForFileOwner = (
_state: unknown,
_worktreeId: string | null | undefined,
_runtimeEnvironmentId: string | null | undefined
) => ({ kind: 'local' as const })
return {
FILE_OWNER_UNRESOLVED_MESSAGE: 'unresolved',
getRuntimeTargetForFileOwner,
requireRuntimeTargetForFileOwner: getRuntimeTargetForFileOwner,
getRuntimeTargetForWorktreeOwner: (state: unknown, worktreeId: string | null | undefined) =>
getRuntimeTargetForFileOwner(state, worktreeId, undefined)
}
})
vi.mock('@/lib/user-opened-local-path', () => ({ statUserOpenedPath: mocks.stat }))
vi.mock('@/store', () => ({
useAppStore: {
@@ -3,7 +3,7 @@ import { createConnectionIdForFileSelector } from '@/lib/connection-owner-resolu
import { computeEditorFontSize } from '@/lib/editor-font-zoom'
import type { HttpLinkSourceOwner } from '@/lib/http-link-routing'
import { isMarkdownComment } from '@/lib/diff-comment-compat'
import { settingsForRuntimeOwner } from '@/runtime/runtime-rpc-client'
import { useFileOwnerRuntimeTarget } from '@/runtime/use-worktree-runtime-target'
import { useAppStore } from '@/store'
import { exceedsMarkdownRichModeSizeLimit } from './markdown-rich-size-limit'
import { prewarmMarkdownPreviewLocalImages } from './markdown-preview-local-images'
@@ -124,23 +124,28 @@ export function useMarkdownPreviewSourceFoundation({
),
[allDiffComments, sourceRelativePath]
)
const settings = useAppStore((s) => s.settings)
const sourceRuntimeTarget = useFileOwnerRuntimeTarget(
sourceRoutingWorktreeId,
resolvedSourceRuntimeEnvironmentId
)
const imageRuntimeContext = useMemo(
() =>
sourceRoutingWorktreeId && worktreeRoot
? {
settings: settingsForRuntimeOwner(settings, resolvedSourceRuntimeEnvironmentId),
worktreeId: sourceRoutingWorktreeId,
worktreePath: worktreeRoot,
connectionId: sourceConnectionId,
expectedExternalSshTargetId: sourceOpenFile?.externalSshTargetId
}
: undefined,
!sourceRoutingWorktreeId || !worktreeRoot
? undefined
: sourceRuntimeTarget
? {
target: sourceRuntimeTarget,
worktreeId: sourceRoutingWorktreeId,
worktreePath: worktreeRoot,
connectionId: sourceConnectionId,
expectedExternalSshTargetId: sourceOpenFile?.externalSshTargetId
}
: // Why null, not undefined: undefined reads as a plain local file and would read here.
null,
[
settings,
sourceRuntimeTarget,
sourceConnectionId,
sourceOpenFile?.externalSshTargetId,
resolvedSourceRuntimeEnvironmentId,
sourceRoutingWorktreeId,
worktreeRoot
]
@@ -20,9 +20,9 @@ const mocks = vi.hoisted(() => ({
vi.mock('@/runtime/runtime-file-client', () => ({
getRuntimeFileReadScope: vi.fn(
(
settings: { activeRuntimeEnvironmentId?: string | null } | null | undefined,
target: { kind: 'local' } | { kind: 'environment'; environmentId: string },
connectionId?: string
) => connectionId ?? settings?.activeRuntimeEnvironmentId ?? null
) => connectionId ?? (target.kind === 'environment' ? target.environmentId : null)
),
readRuntimeFileContent: mocks.readRuntimeFileContent,
subscribeRuntimeFileChanges: vi.fn()
@@ -47,6 +47,18 @@ vi.mock('@/lib/runtime-workspace-file-route', () => ({
findWorkspaceFileRoute: vi.fn(() => null)
}))
// Fixture owners: `repo-runtime::` workspaces live on runtime-1, everything else here is local.
vi.mock('@/lib/file-owner-runtime-target', () => {
const ownerTarget = (_s: unknown, worktreeId?: string, owner?: string | null) => {
const id = owner === undefined && worktreeId?.startsWith('repo-runtime::') ? 'runtime-1' : owner
return id ? { kind: 'environment', environmentId: id } : { kind: 'local' }
}
return {
getRuntimeTargetForFileOwner: ownerTarget,
requireRuntimeTargetForFileOwner: ownerTarget
}
})
vi.mock('@/store', () => ({
useAppStore: {
getState: mocks.getState
@@ -5,7 +5,9 @@ import { useAppStore } from '@/store'
import { getDiskBaselineSignature } from './diff-content-signature'
import { getRuntimeFileReadScope } from '@/runtime/runtime-file-client'
import { readEditorCsvFileContent } from './csv/csv-file-content'
import { RuntimeRpcCallError, settingsForRuntimeOwner } from '@/runtime/runtime-rpc-client'
import { RuntimeRpcCallError } from '@/runtime/runtime-rpc-client'
import type { RuntimeClientTarget } from '@/runtime/runtime-client-target'
import { requireRuntimeTargetForFileOwner } from '@/lib/file-owner-runtime-target'
import { findWorkspaceFileRoute } from '@/lib/runtime-workspace-file-route'
import { selectWorktreeHostConnectionPhase } from '@/lib/worktree-host-connection-phase'
import {
@@ -92,11 +94,6 @@ export function useEditorPanelFileContentLoader({
const resolvedConnectionId = getConnectionIdForFile(worktreeId ?? null, filePath)
const connectionId = resolvedConnectionId ?? undefined
const restoredOpenFile = openFilesRef.current.find((file) => file.id === id)
const activeSettings = useAppStore.getState().settings
const readSettings = settingsForRuntimeOwner(
activeSettings,
restoredOpenFile?.runtimeEnvironmentId
)
const isLiveTailLogTab =
restoredOpenFile?.readOnly === true && restoredOpenFile.liveTail === true
// Why: these tabs name a file on this client, so the worktree's SSH or runtime owner must
@@ -104,6 +101,16 @@ export function useEditorPanelFileContentLoader({
const isClientLocalTab = restoredOpenFile
? isClientLocalReadOnlyTab(restoredOpenFile)
: false
const readTarget: RuntimeClientTarget =
isClientLocalTab && restoredOpenFile?.runtimeEnvironmentId === undefined
? { kind: 'local' }
: requireRuntimeTargetForFileOwner(
useAppStore.getState(),
worktreeId,
restoredOpenFile?.runtimeEnvironmentId
)
const readOwnerEnvironmentId =
readTarget.kind === 'environment' ? readTarget.environmentId : undefined
readConnectionId = connectionId
let readWorktreeId = worktreeId
let readRelativePath = restoredOpenFile?.relativePath ?? relativePath
@@ -145,7 +152,7 @@ export function useEditorPanelFileContentLoader({
if (
!isClientLocalTab &&
resolvedConnectionId === undefined &&
!readSettings?.activeRuntimeEnvironmentId?.trim() &&
!readOwnerEnvironmentId &&
!isWorktreeConnectionResolved(worktreeId ?? null)
) {
// Why: the backing repo hasn't hydrated yet (SSH still connecting), so
@@ -160,9 +167,7 @@ export function useEditorPanelFileContentLoader({
const externalSshOwnerId =
restoredOpenFile.externalSshTargetId?.trim() ||
(isClientLocalTab ? undefined : connectionId)
const runtimeEnvironmentId = isClientLocalTab
? undefined
: readSettings?.activeRuntimeEnvironmentId?.trim()
const runtimeEnvironmentId = isClientLocalTab ? undefined : readOwnerEnvironmentId
if (isClientLocalTab) {
readConnectionId = undefined
} else {
@@ -190,7 +195,7 @@ export function useEditorPanelFileContentLoader({
}
}
}
const readScope = getRuntimeFileReadScope(readSettings, readConnectionId)
const readScope = getRuntimeFileReadScope(readTarget, readConnectionId)
const access = restoredOpenFile
? editorTabFileAccess(useAppStore.getState(), restoredOpenFile)
: undefined
@@ -213,7 +218,7 @@ export function useEditorPanelFileContentLoader({
if (!pending) {
const promise = readEditorCsvFileContent(
{
settings: readSettings,
target: readTarget,
filePath,
relativePath: readRelativePath,
worktreeId: readWorktreeId,
@@ -52,6 +52,25 @@ vi.mock('@/lib/worktree-runtime-owner', () => ({
getExplicitRuntimeEnvironmentIdForWorktree: vi.fn(() => null)
}))
// These fixtures' workspaces live on runtime-1, so an unstamped tab resolves there.
vi.mock('@/lib/file-owner-runtime-target', () => {
const getRuntimeTargetForFileOwner = (
_state: unknown,
_worktreeId: string | null | undefined,
runtimeEnvironmentId: string | null | undefined
) =>
runtimeEnvironmentId === null
? { kind: 'local' as const }
: { kind: 'environment' as const, environmentId: runtimeEnvironmentId ?? 'runtime-1' }
return {
FILE_OWNER_UNRESOLVED_MESSAGE: 'unresolved',
getRuntimeTargetForFileOwner,
requireRuntimeTargetForFileOwner: getRuntimeTargetForFileOwner,
getRuntimeTargetForWorktreeOwner: (state: unknown, worktreeId: string | null | undefined) =>
getRuntimeTargetForFileOwner(state, worktreeId, undefined)
}
})
vi.mock('@/store', () => ({ useAppStore: { getState: mocks.getState } }))
vi.mock('./useEditorPanelExternalContentEvents', () => ({
@@ -105,17 +105,17 @@ afterEach(() => {
describe('getLocalImageCacheKey', () => {
it('scopes local markdown image cache entries by runtime owner', () => {
const localKey = getLocalImageCacheKey('/repo/docs/logo.png', null, {
settings: { activeRuntimeEnvironmentId: null },
target: { kind: 'local' as const },
worktreeId: 'wt-1',
worktreePath: '/repo'
})
const remoteKey = getLocalImageCacheKey('/repo/docs/logo.png', null, {
settings: { activeRuntimeEnvironmentId: 'env-1' },
target: { kind: 'environment' as const, environmentId: 'env-1' },
worktreeId: 'wt-1',
worktreePath: '/repo'
})
const otherRemoteKey = getLocalImageCacheKey('/repo/docs/logo.png', null, {
settings: { activeRuntimeEnvironmentId: 'env-2' },
target: { kind: 'environment' as const, environmentId: 'env-2' },
worktreeId: 'wt-1',
worktreePath: '/repo'
})
@@ -365,7 +365,7 @@ describe('loadLocalImageSrc', () => {
await expect(
loadLocalImageSrc('diagram.png', '/repo/docs/readme.md', null, {
settings: { activeRuntimeEnvironmentId: null },
target: { kind: 'local' as const },
worktreeId: 'wt-1',
worktreePath: '/repo',
connectionId: 'ssh-1'
@@ -373,7 +373,7 @@ describe('loadLocalImageSrc', () => {
).resolves.toBe('blob:runtime-one')
await expect(
loadLocalImageSrc('diagram.png', '/repo/docs/readme.md', null, {
settings: { activeRuntimeEnvironmentId: null },
target: { kind: 'local' as const },
worktreeId: 'wt-2',
worktreePath: '/repo',
connectionId: 'ssh-2'
@@ -388,7 +388,7 @@ describe('loadLocalImageSrc', () => {
await expect(
loadLocalImageSrc('diagram.png', '/tmp/readme.md', null, {
settings: { activeRuntimeEnvironmentId: null },
target: { kind: 'local' as const },
worktreeId: 'wt-1',
worktreePath: '/repo',
connectionId: 'ssh-2',
@@ -454,7 +454,7 @@ describe('useLocalImageSrc runtime owner', () => {
function sshOwner(overrides: Partial<RuntimeFileOperationArgs> = {}): RuntimeFileOperationArgs {
return {
settings: { activeRuntimeEnvironmentId: null },
target: { kind: 'local' as const },
worktreeId: 'wt-1',
worktreePath: '/repo',
connectionId: 'ssh-1',
@@ -17,6 +17,7 @@ import {
subscribeToLocalImageCacheInvalidation,
unpinLocalImageCache
} from './local-image-src-cache'
import { runtimeTargetEnvironmentId } from '@/runtime/runtime-client-target'
export function getLocalImageCacheKey(
absolutePath: string,
@@ -26,8 +27,9 @@ export function getLocalImageCacheKey(
},
access?: LocalFileAccess
): string {
const runtimeEnvironmentId =
runtimeContext?.settings?.activeRuntimeEnvironmentId?.trim() ?? 'client'
const runtimeEnvironmentId = runtimeContext
? (runtimeTargetEnvironmentId(runtimeContext.target) ?? 'client')
: 'client'
return [
runtimeEnvironmentId,
runtimeContext?.connectionId ?? connectionId ?? 'local',
@@ -33,9 +33,20 @@ vi.mock('@/store', () => ({
}))
vi.mock('@/lib/connection-context', () => ({ getConnectionIdFromState: () => runtimeConnectionId }))
vi.mock('@/runtime/runtime-file-client', () => ({ statRuntimePath: runtime.stat }))
vi.mock('@/runtime/runtime-rpc-client', () => ({
settingsForRuntimeOwner: (_settings: unknown, owner: string | null | undefined) => ({ owner })
}))
vi.mock('@/lib/file-owner-runtime-target', () => {
const getRuntimeTargetForFileOwner = (
_state: unknown,
_worktreeId: string | null | undefined,
owner: string | null | undefined
) => (owner ? { kind: 'environment' as const, environmentId: owner } : { kind: 'local' as const })
return {
FILE_OWNER_UNRESOLVED_MESSAGE: 'unresolved',
getRuntimeTargetForFileOwner,
requireRuntimeTargetForFileOwner: getRuntimeTargetForFileOwner,
getRuntimeTargetForWorktreeOwner: (state: unknown, worktreeId: string | null | undefined) =>
getRuntimeTargetForFileOwner(state, worktreeId, undefined)
}
})
vi.mock('./markdown-document-list-request', () => ({
requestSharedMarkdownDocumentList: runtime.list
}))
@@ -225,7 +236,7 @@ describe('Markdown document navigation', () => {
expect(runtime.stat).toHaveBeenCalledWith(
{
settings: { owner: 'runtime-owner' },
target: { kind: 'environment', environmentId: 'runtime-owner' },
worktreeId: 'wt',
worktreePath: '/repo',
connectionId: 'ssh-owner'
@@ -85,7 +85,10 @@ it('waits for the retained editor to take its managed workspace owner before lis
await waitFor(() => expect(result.current.markdownDocuments).toEqual([document]))
expect(mocks.list).toHaveBeenCalledTimes(1)
expect(mocks.list).toHaveBeenCalledWith(
expect.objectContaining({ settings: { activeRuntimeEnvironmentId: 'env-1' }, worktreeId }),
expect.objectContaining({
target: { kind: 'environment', environmentId: 'env-1' },
worktreeId
}),
'/remote/repo',
{ requireFresh: false }
)
@@ -122,7 +125,7 @@ it('uses an explicit server owner even before its desktop repository catalog is
await waitFor(() => expect(result.current.markdownDocuments).toEqual([document]))
expect(mocks.list).toHaveBeenCalledTimes(1)
expect(mocks.list).toHaveBeenCalledWith(
expect.objectContaining({ settings: { activeRuntimeEnvironmentId: 'env-1' } }),
expect.objectContaining({ target: { kind: 'environment', environmentId: 'env-1' } }),
'/remote/repo',
{ requireFresh: false }
)
@@ -173,7 +176,7 @@ it('waits for ownership migration in a non-git folder workspace too', async () =
await waitFor(() => expect(result.current.markdownDocuments).toEqual([document]))
expect(mocks.list).toHaveBeenCalledWith(
expect.objectContaining({
settings: { activeRuntimeEnvironmentId: 'env-1' },
target: { kind: 'environment', environmentId: 'env-1' },
worktreeId: 'folder:notes'
}),
'/notes',
@@ -5,7 +5,10 @@ import { useAppStore } from '@/store'
import { translate } from '@/i18n/i18n'
import { getConnectionIdFromState } from '@/lib/connection-context'
import { statRuntimePath } from '@/runtime/runtime-file-client'
import { settingsForRuntimeOwner } from '@/runtime/runtime-rpc-client'
import {
getRuntimeTargetForFileOwner,
requireRuntimeTargetForFileOwner
} from '@/lib/file-owner-runtime-target'
import type { MarkdownViewMode, OpenFile } from '@/store/slices/editor'
import {
createMarkdownDocumentIndex,
@@ -81,9 +84,14 @@ export function useMarkdownDocuments(
return
}
const state = useAppStore.getState()
const settings = settingsForRuntimeOwner(state.settings, activeFile.runtimeEnvironmentId)
const target = getRuntimeTargetForFileOwner(
state,
worktreeId,
activeFile.runtimeEnvironmentId
)
// The content loader reowns retained tabs before metadata may use their new host.
if (
!target ||
findRestoredEditorWorkspaceRuntimeOwner(
state,
{
@@ -95,7 +103,7 @@ export function useMarkdownDocuments(
},
worktreeId
) ||
(connectionId === undefined && !settings?.activeRuntimeEnvironmentId?.trim())
(connectionId === undefined && target.kind === 'local')
) {
return
}
@@ -105,7 +113,7 @@ export function useMarkdownDocuments(
try {
const documents = await requestSharedMarkdownDocumentList(
{
settings,
target,
worktreeId,
worktreePath,
connectionId: connectionId ?? undefined
@@ -161,8 +169,9 @@ export function useMarkdownDocuments(
try {
const stats = await statRuntimePath(
{
settings: settingsForRuntimeOwner(
useAppStore.getState().settings,
target: requireRuntimeTargetForFileOwner(
useAppStore.getState(),
worktreeId,
activeFile.runtimeEnvironmentId
),
worktreeId,
@@ -24,11 +24,20 @@ vi.mock('@/runtime/runtime-file-client', () => ({
listRuntimeMarkdownDocuments: runtime.list,
statRuntimePath: runtime.stat
}))
vi.mock('@/runtime/runtime-rpc-client', () => ({
settingsForRuntimeOwner: (_settings: unknown, owner: string | null | undefined) => ({
activeRuntimeEnvironmentId: owner
})
}))
vi.mock('@/lib/file-owner-runtime-target', () => {
const getRuntimeTargetForFileOwner = (
_state: unknown,
_worktreeId: string | null | undefined,
owner: string | null | undefined
) => (owner ? { kind: 'environment' as const, environmentId: owner } : { kind: 'local' as const })
return {
FILE_OWNER_UNRESOLVED_MESSAGE: 'unresolved',
getRuntimeTargetForFileOwner,
requireRuntimeTargetForFileOwner: getRuntimeTargetForFileOwner,
getRuntimeTargetForWorktreeOwner: (state: unknown, worktreeId: string | null | undefined) =>
getRuntimeTargetForFileOwner(state, worktreeId, undefined)
}
})
const toastError = vi.hoisted(() => vi.fn())
vi.mock('sonner', () => ({ toast: { error: toastError } }))
vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string) => fallback }))
@@ -147,7 +156,7 @@ describe('Markdown metadata from the existing worktree watcher', () => {
await settle()
expect(controllers[0].markdownDocuments).toEqual([target])
expect(runtime.list).toHaveBeenLastCalledWith(
expect.objectContaining({ settings: { activeRuntimeEnvironmentId: null } }),
expect.objectContaining({ target: { kind: 'local' } }),
'/repo'
)
})
@@ -33,7 +33,7 @@ function createParams(content = ''): EditorConfigParams {
worktreeRoot: '/repo',
isMac: false,
richMarkdownSpellcheckEnabled: true,
settings: {} as EditorConfigParams['settings'],
runtimeTarget: { kind: 'local' },
activateMarkdownLink: vi.fn(),
rootRef: { current: null },
editorRef: { current: null },
@@ -70,8 +70,7 @@ describe('useRichMarkdownProgrammaticSync external-reload baseline adoption (#60
baseCanonicalRef,
markdownDocuments: undefined,
rootRef: { current: null },
runtimeEnvironmentId: null,
settings: null,
runtimeTarget: { kind: 'local' },
slashMenuSetter: vi.fn(),
worktreeId: 'w1',
worktreeRoot: null
@@ -8,9 +8,9 @@ import { normalizeEmptyListItems } from './rich-markdown-normalize'
import { syncSlashMenu, type SlashMenuState } from './rich-markdown-slash-commands'
import {
createRichMarkdownImageResolverContext,
setRichMarkdownImageResolverContext,
type RichMarkdownImageResolverSettings
setRichMarkdownImageResolverContext
} from './rich-markdown-image-context'
import type { RuntimeClientTarget } from '@/runtime/runtime-client-target'
import type { RichMarkdownEditorCodec } from './rich-markdown-source-transport'
type RichMarkdownProgrammaticSyncOptions = {
@@ -27,8 +27,7 @@ type RichMarkdownProgrammaticSyncOptions = {
baseCanonicalRef: MutableRefObject<string>
markdownDocuments?: MarkdownDocument[]
rootRef: MutableRefObject<HTMLDivElement | null>
runtimeEnvironmentId?: string | null
settings: RichMarkdownImageResolverSettings
runtimeTarget: RuntimeClientTarget | null
slashMenuSetter: Dispatch<SetStateAction<SlashMenuState | null>>
worktreeId: string
worktreeRoot: string | null
@@ -54,8 +53,7 @@ export function useRichMarkdownProgrammaticSync({
baseCanonicalRef,
markdownDocuments,
rootRef,
runtimeEnvironmentId,
settings,
runtimeTarget,
slashMenuSetter,
worktreeId,
worktreeRoot
@@ -71,8 +69,7 @@ export function useRichMarkdownProgrammaticSync({
createRichMarkdownImageResolverContext({
filePath,
externalSshTargetId,
runtimeEnvironmentId,
settings,
runtimeTarget,
worktreeId,
worktreeRoot
})
@@ -85,8 +82,7 @@ export function useRichMarkdownProgrammaticSync({
externalSshTargetId,
filePath,
isApplyingProgrammaticUpdateRef,
runtimeEnvironmentId,
settings,
runtimeTarget,
worktreeId,
worktreeRoot
])
@@ -1,8 +1,8 @@
import { useRef, type MutableRefObject } from 'react'
import { useAppStore } from '@/store'
import { serializeRichMarkdownForReconcile } from './rich-markdown-reconcile-serializer'
import { createRichMarkdownImageResolverContext } from './rich-markdown-image-context'
import type { RichMarkdownHtmlSuperscriptLinkContext } from './rich-markdown-html-superscript-link-context'
import { useFileOwnerRuntimeTarget } from '@/runtime/use-worktree-runtime-target'
type ReconcileRoundTripParams = {
htmlSuperscriptLinkContext: RichMarkdownHtmlSuperscriptLinkContext
@@ -27,7 +27,7 @@ export function useRichMarkdownReconcileRoundTrip({
worktreeId,
worktreeRoot
}: ReconcileRoundTripParams): MutableRefObject<(markdown: string) => string | null> {
const settings = useAppStore((s) => s.settings)
const runtimeTarget = useFileOwnerRuntimeTarget(worktreeId, runtimeEnvironmentId)
const ref = useRef<(markdown: string) => string | null>(() => null)
ref.current = (markdown) =>
serializeRichMarkdownForReconcile(markdown, {
@@ -35,8 +35,7 @@ export function useRichMarkdownReconcileRoundTrip({
imageResolverContext: createRichMarkdownImageResolverContext({
filePath,
externalSshTargetId,
runtimeEnvironmentId,
settings,
runtimeTarget,
worktreeId,
worktreeRoot
})
@@ -690,7 +690,7 @@ describe('FloatingTerminalPanel markdown editor', () => {
'/tmp/orca/floating-notes',
FLOATING_TERMINAL_WORKTREE_ID,
undefined,
{ activeRuntimeEnvironmentId: null }
{ kind: 'local' }
)
expect(mocks.openFile).toHaveBeenCalledWith(
expect.objectContaining({ filePath: '/tmp/orca/floating-notes/untitled.md' }),
@@ -13,8 +13,9 @@ import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants'
import type { FloatingWorkspaceChromeModel } from './use-floating-workspace-chrome-model'
import type { FloatingTerminalPanelLocalState } from './use-floating-terminal-panel-local-state'
import type { FloatingTerminalPanelStoreState } from './use-floating-terminal-panel-store-state'
import type { RuntimeClientTarget } from '@/runtime/runtime-client-target'
const LOCAL_RUNTIME_SETTINGS = { activeRuntimeEnvironmentId: null } as const
const LOCAL_TARGET: RuntimeClientTarget = { kind: 'local' }
type FloatingTerminalCreateActionsInput = Pick<
FloatingTerminalPanelStoreState,
@@ -97,7 +98,7 @@ export function useFloatingTerminalCreateActions({
markdownCwd,
FLOATING_TERMINAL_WORKTREE_ID,
getConnectionId(FLOATING_TERMINAL_WORKTREE_ID) ?? undefined,
LOCAL_RUNTIME_SETTINGS
LOCAL_TARGET
)
if (!fileInfo) {
return
@@ -44,7 +44,9 @@ function renderUserMessage(source?: AgentMessageSource, worktreeId: string | nul
expandSignal={false}
onScrollMessageToTop={vi.fn()}
runtimeContext={
worktreeId ? { settings: null, worktreeId, worktreePath: '/repo' } : undefined
worktreeId
? { target: { kind: 'local' as const }, worktreeId, worktreePath: '/repo' }
: undefined
}
/>
)
@@ -82,7 +82,7 @@ describe('NativeChatImageAttachmentPreview', () => {
for (const call of mocks.useLocalImageSrc.mock.calls) {
expect(call[3]).toEqual({
settings: { activeRuntimeEnvironmentId: 'env-1' },
target: { kind: 'environment', environmentId: 'env-1' },
worktreeId: null,
worktreePath: null
})
@@ -13,6 +13,7 @@ import type { NativeChatComposerImageAttachment } from './NativeChatComposerFiel
import { isAgentSessionAttachmentStorePath } from '../../../../shared/agent-session-attachments'
import { chatImageAccess } from '@/lib/local-file-access'
import { LoadingSpinner } from '@/components/LoadingSpinner'
import { runtimeTargetForOwnerEnvironment } from '@/runtime/runtime-client-target'
type Props = {
attachment: NativeChatComposerImageAttachment
@@ -151,7 +152,7 @@ function NativeChatImageThumbnail({
() =>
readEnvironmentId
? {
settings: { activeRuntimeEnvironmentId: readEnvironmentId },
target: runtimeTargetForOwnerEnvironment(readEnvironmentId),
worktreeId: null,
worktreePath: null
}
@@ -17,7 +17,7 @@ globalThis.IS_REACT_ACT_ENVIRONMENT = true
function runtimeContext(worktreeId: string): RuntimeFileOperationArgs {
return {
settings: { activeRuntimeEnvironmentId: null },
target: { kind: 'local' as const },
worktreeId,
worktreePath: `/repo/${worktreeId}`,
expectedExecutionHostId: 'local'
@@ -24,7 +24,7 @@ vi.mock('@/components/terminal-pane/terminal-file-link-target', async (importOri
absolutePath: /^[\\/]{2}/.test(link.pathText) ? link.pathText : `/repo/${link.pathText}`,
line: null,
column: null,
fileContext: { sourceHostResolved: connection.sourceHostResolved },
fileContext: { target: { kind: 'local' }, sourceHostResolved: connection.sourceHostResolved },
isRemoteRuntimePath: false,
cacheKey: link.pathText,
isKnownWorktreeRoot: link.pathText === 'ROOT'
@@ -33,7 +33,10 @@ vi.mock('@/components/terminal-pane/terminal-file-link-target', async (importOri
vi.mock('@/components/terminal-pane/terminal-file-path-mapping', async (importOriginal) => ({
...(await importOriginal<typeof FilePathMappingModule>()),
getTerminalFileContext: () => ({ sourceHostResolved: connection.currentOwnerResolved })
getTerminalFileContext: () => ({
target: { kind: 'local' },
sourceHostResolved: connection.currentOwnerResolved
})
}))
const host = { cwd: '/repo', worktreeId: 'wt-1', worktreePath: '/repo', connectionId: null }
@@ -15,6 +15,7 @@ import {
type FileLinkTarget
} from '@/components/terminal-pane/terminal-file-link-target'
import { getTerminalFileContext } from '@/components/terminal-pane/terminal-file-path-mapping'
import { runtimeTargetEnvironmentId } from '@/runtime/runtime-client-target'
// Why: a host that could not answer is retried a few times, then waits for the next recheck.
export const UNVERIFIABLE_RETRY_DELAYS_MS = [2_000, 10_000, 30_000] as const
@@ -131,8 +132,8 @@ export function createNativeChatFileLinkExistence(
if (
!current.sourceHostResolved ||
current.connectionId !== target.fileContext.connectionId ||
current.settings?.activeRuntimeEnvironmentId !==
target.fileContext.settings?.activeRuntimeEnvironmentId
runtimeTargetEnvironmentId(current.target) !==
runtimeTargetEnvironmentId(target.fileContext.target)
) {
throw new Error('The workspace host changed or could not be determined')
}
@@ -64,7 +64,7 @@ describe('resolveNativeChatImageRuntimeContext', () => {
const second = resolveNativeChatImageRuntimeContext(storeState, 'tab-1')
expect(first).not.toBeNull()
expect(second?.settings).toBe(first?.settings)
expect(second?.target).toBe(first?.target)
expect(shallow(second, first)).toBe(true)
})
@@ -91,7 +91,7 @@ describe('resolveNativeChatImageRuntimeContext', () => {
worktreeId: 'wt-1',
worktreePath: '/repo/worktree',
expectedExecutionHostId: 'local',
settings: { activeRuntimeEnvironmentId: 'owner-a' }
target: { kind: 'environment' as const, environmentId: 'owner-a' }
})
})
@@ -136,7 +136,7 @@ describe('resolveNativeChatImageRuntimeContext', () => {
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
worktreePath: '/home/me/pinned',
expectedExecutionHostId: 'local',
settings: { activeRuntimeEnvironmentId: null }
target: { kind: 'local' as const }
})
})
})
@@ -2,7 +2,10 @@ import { useAppStore } from '@/store'
import type { AppState } from '@/store/types'
import type { RuntimeFileOperationArgs } from '@/runtime/runtime-file-client'
import {
settingsForWorktreeOperationRoute,
runtimeTargetForOwnerEnvironment,
type RuntimeClientTarget
} from '@/runtime/runtime-client-target'
import {
resolveWorktreeOperationRouteResult,
type WorktreeOperationRouteResolution
} from '@/lib/worktree-operation-route'
@@ -70,38 +73,21 @@ const FLOATING_WORKSPACE_ROUTE: WorktreeOperationRouteResolution = {
route: { executionHostId: 'local', runtimeEnvironmentId: null }
}
// Route settings are cloned for the runtime operation contract. Reuse that
// clone while the store's source settings and selected runtime are unchanged so
// consumers do not treat an unrelated store update as a new image owner.
const settingsBySource = new WeakMap<object, Map<string, AppState['settings']>>()
// Why stable: consumers compare by identity, so an unrelated store update must not read as a
// new image owner.
const LOCAL_TARGET: RuntimeClientTarget = { kind: 'local' }
const environmentTargets = new Map<string, RuntimeClientTarget>()
function stableSettingsForRoute(
settings: AppState['settings'],
runtimeEnvironmentId: string | null
): AppState['settings'] {
if (!settings) {
return settingsForWorktreeOperationRoute(settings, {
executionHostId: null,
runtimeEnvironmentId
})
function stableTargetForRoute(runtimeEnvironmentId: string | null): RuntimeClientTarget {
if (!runtimeEnvironmentId) {
return LOCAL_TARGET
}
const source = settings as object
let byRuntime = settingsBySource.get(source)
if (!byRuntime) {
byRuntime = new Map()
settingsBySource.set(source, byRuntime)
let target = environmentTargets.get(runtimeEnvironmentId)
if (!target) {
target = runtimeTargetForOwnerEnvironment(runtimeEnvironmentId)
environmentTargets.set(runtimeEnvironmentId, target)
}
const cacheKey = runtimeEnvironmentId ?? ''
const cached = byRuntime.get(cacheKey)
if (cached) {
return cached
}
const resolved = settingsForWorktreeOperationRoute(settings, {
executionHostId: null,
runtimeEnvironmentId
})
byRuntime.set(cacheKey, resolved)
return resolved
return target
}
export function resolveNativeChatImageRuntimeContext(
@@ -139,7 +125,7 @@ export function resolveNativeChatImageRuntimeContext(
return null
}
const context: RuntimeFileOperationArgs = {
settings: stableSettingsForRoute(state.settings, route.runtimeEnvironmentId),
target: stableTargetForRoute(route.runtimeEnvironmentId),
worktreeId: linkContext.worktreeId,
worktreePath,
expectedExecutionHostId: host.kind === 'ssh' ? host.id : 'local'
@@ -57,7 +57,10 @@ describe.each(['paired-detected', 'legacy-ssh'] as const)('%s file catalog', (ow
expect.objectContaining({
worktreeId: WORKTREE_ID,
worktreePath: '/srv/worktree',
settings: { activeRuntimeEnvironmentId: owner === 'paired-detected' ? 'hub' : null },
target:
owner === 'paired-detected'
? { kind: 'environment', environmentId: 'hub' }
: { kind: 'local' },
connectionId: owner === 'legacy-ssh' ? 'box' : undefined
}),
expect.objectContaining({ rootPath: '/srv/worktree' })
@@ -98,7 +98,7 @@ describe('useRuntimeFileListForWorktree', () => {
worktreeId: workspaceKey,
worktreePath: '/srv/platform',
connectionId: 'ssh-1',
settings: expect.objectContaining({ activeRuntimeEnvironmentId: null })
target: { kind: 'local' }
}),
{
includeIgnored: true,
@@ -170,7 +170,7 @@ describe('useRuntimeFileListForWorktree', () => {
expect(searchRuntimeFilePathsMock).toHaveBeenCalledWith(
expect.objectContaining({
settings: { activeRuntimeEnvironmentId: 'env-1' },
target: { kind: 'environment', environmentId: 'env-1' },
worktreeId: workspaceKey,
worktreePath: '/srv/platform'
}),
@@ -300,7 +300,7 @@ describe('useRuntimeFileListForWorktree', () => {
expect(searchRuntimeFilePathsMock).toHaveBeenCalledWith(
expect.objectContaining({
settings: { activeRuntimeEnvironmentId: 'env-1' },
target: { kind: 'environment', environmentId: 'env-1' },
worktreeId: 'wt-remote',
worktreePath: '/srv/remote'
}),
@@ -38,6 +38,7 @@ import {
getFileExplorerOwnerUnresolvedMessage,
getFileExplorerOperationRoute
} from '@/components/right-sidebar/file-explorer-operation-owner'
import { runtimeTargetForOwnerEnvironment } from '@/runtime/runtime-client-target'
export type RuntimeFileListState = {
files: string[]
@@ -116,7 +117,8 @@ export function useRuntimeFileListForWorktree({
const operationRoute = getFileExplorerOperationRoute(operationOwner)
const operationRouteAvailable = operationRoute !== null
const connectionId = operationRoute?.connectionId
const runtimeEnvironmentId = operationRoute?.settings.activeRuntimeEnvironmentId ?? null
const runtimeEnvironmentId =
operationOwner.kind === 'runtime' ? operationOwner.environmentId : null
const activeTargetStatus = useAppStore((state) =>
connectionId ? state.sshConnectionStates.get(connectionId)?.status : undefined
)
@@ -189,7 +191,7 @@ export function useRuntimeFileListForWorktree({
const requestAbortController = new AbortController()
const requestOperationOwner = operationOwnerRef.current
const requestContext = {
settings: { activeRuntimeEnvironmentId: runtimeEnvironmentId },
target: runtimeTargetForOwnerEnvironment(runtimeEnvironmentId),
worktreeId,
worktreePath,
connectionId
@@ -2,10 +2,10 @@ import { beforeEach, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => {
const route: {
current: {
settings: { activeRuntimeEnvironmentId: string | null }
target: { kind: 'local' } | { kind: 'environment'; environmentId: string }
connectionId: string | undefined
}
} = { current: { settings: { activeRuntimeEnvironmentId: null }, connectionId: undefined } }
} = { current: { target: { kind: 'local' }, connectionId: undefined } }
return {
stat: vi.fn(),
authorize: vi.fn(),
@@ -40,10 +40,6 @@ vi.mock('./right-sidebar/file-explorer-operation-owner', () => ({
})
}))
vi.mock('@/runtime/runtime-rpc-client', () => ({
getActiveRuntimeTarget: (settings: { activeRuntimeEnvironmentId: string | null }) =>
settings.activeRuntimeEnvironmentId
? { kind: 'environment', environmentId: settings.activeRuntimeEnvironmentId }
: { kind: 'local' },
callRuntimeRpc: mocks.rpc
}))
vi.mock('@/store/slices/editor/focus/editor-focus-reveal', () => ({
@@ -53,7 +49,7 @@ import { openQuickOpenFile } from './quick-open-file-navigation'
beforeEach(() => {
vi.clearAllMocks()
mocks.route.current = { settings: { activeRuntimeEnvironmentId: null }, connectionId: undefined }
mocks.route.current = { target: { kind: 'local' }, connectionId: undefined }
mocks.stat.mockResolvedValue({ isDirectory: false })
mocks.assertCurrent.mockReset()
vi.stubGlobal('window', { api: { fs: { authorizeExternalPath: mocks.authorize } } })
@@ -129,7 +125,7 @@ it('probes SSH paths only on their owning host', async () => {
})
it('resolves paired paths on the host and never falls back to client filesystem access', async () => {
mocks.route.current.settings.activeRuntimeEnvironmentId = 'env-a'
mocks.route.current.target = { kind: 'environment', environmentId: 'env-a' }
mocks.rpc.mockResolvedValue({
exists: true,
isDirectory: false,
@@ -189,7 +185,7 @@ it.each(['C:\\repo\\file.ts', '\\\\server\\share\\file.ts'])(
)
it('resolves literal colon names on a paired host before treating the suffix as navigation', async () => {
mocks.route.current.settings.activeRuntimeEnvironmentId = 'env-a'
mocks.route.current.target = { kind: 'environment', environmentId: 'env-a' }
mocks.rpc.mockResolvedValueOnce({
exists: true,
isDirectory: false,
@@ -4,7 +4,7 @@ import { detectLanguage } from '@/lib/language-detect'
import { joinPath, getRelativePathInsideRoot } from '@/lib/path'
import { useAppStore } from '@/store'
import { isMissingRuntimePathError } from '@/runtime/runtime-file-client'
import { getActiveRuntimeTarget, callRuntimeRpc } from '@/runtime/runtime-rpc-client'
import { callRuntimeRpc } from '@/runtime/runtime-rpc-client'
import { toRuntimeWorktreeSelector } from '@/runtime/runtime-worktree-selector'
import type { RuntimeTerminalPathResolution } from '../../../shared/runtime-file-contracts'
import { resolveWslRepoWorktreeBasePath } from '../../../shared/wsl-paths'
@@ -36,7 +36,7 @@ export async function openQuickOpenFile(
}
assertCurrent()
const route = guard.route
const target = getActiveRuntimeTarget(route.settings)
const target = route.target
const resolvePastedPath = (path: string): string =>
target.kind !== 'environment' &&
!route.connectionId &&
@@ -125,7 +125,7 @@ export async function openQuickOpenFile(
filePath,
relativePath,
worktreeId,
runtimeEnvironmentId: route.settings.activeRuntimeEnvironmentId,
runtimeEnvironmentId: target.kind === 'environment' ? target.environmentId : null,
...(route.connectionId && getRelativePathInsideRoot(filePath, root) === null
? { externalSshTargetId: route.connectionId }
: {}),
@@ -20,7 +20,7 @@ beforeEach(() => {
vi.clearAllMocks()
})
const context = {
settings: { activeRuntimeEnvironmentId: null },
target: { kind: 'local' as const },
worktreeId: 'wt',
worktreePath: '/repo'
}
@@ -89,7 +89,7 @@ describe('FileExplorerRow collapse folder action', () => {
it('shows remote download only for desktop SSH rows and file-like Remote Host rows', () => {
const runtimeContext = {
settings: { activeRuntimeEnvironmentId: 'runtime-1' },
target: { kind: 'environment' as const, environmentId: 'runtime-1' },
worktreeId: 'wt-1',
worktreePath: '/repo'
}
@@ -290,7 +290,7 @@ describe('FileExplorerRow collapse folder action', () => {
it('downloads Remote Host rows through the runtime download path', async () => {
const runtimeContext = {
settings: { activeRuntimeEnvironmentId: 'runtime-1' },
target: { kind: 'environment' as const, environmentId: 'runtime-1' },
worktreeId: 'wt-1',
worktreePath: '/repo'
}
@@ -3,6 +3,7 @@ import type { OpenFile } from '@/store/slices/editor'
import { getEditorFileOperationContext } from '@/lib/editor-file-operation-owner'
import type { FileExplorerOperationRoute } from './file-explorer-operation-owner'
import { isPathEqualOrDescendant } from './file-explorer-paths'
import { runtimeTargetEnvironmentId } from '@/runtime/runtime-client-target'
export type FileExplorerDeletionEditor = Pick<
OpenFile,
@@ -29,8 +30,7 @@ export function getFileExplorerDeletionEditors(
const owner = getEditorFileOperationContext(state, file, null)
// Identical paths on different hosts must not trigger another editor's save or close.
return (
(owner.settings?.activeRuntimeEnvironmentId?.trim() || null) ===
(route.settings.activeRuntimeEnvironmentId?.trim() || null) &&
runtimeTargetEnvironmentId(owner.target) === runtimeTargetEnvironmentId(route.target) &&
owner.expectedExecutionHostId === route.expectedExecutionHostId &&
owner.connectionId === route.connectionId
)
@@ -49,7 +49,7 @@ export async function readFileExplorerDirectory(
}
const entries = await readRuntimeDirectory(
{
settings: route.settings,
target: route.target,
worktreeId: activeWorktreeId,
worktreePath,
connectionId: route.connectionId
@@ -11,7 +11,7 @@ import type { TreeNode } from './file-explorer-types'
vi.mock('./file-explorer-operation-owner', () => ({
getFileExplorerOwnerUnresolvedMessage: () => 'unresolved',
requireMatchingFileExplorerOperationRoute: () => ({ settings: {} })
requireMatchingFileExplorerOperationRoute: () => ({ target: { kind: 'local' } })
}))
vi.mock('@/store', () => ({
useAppStore: (selector: (state: Record<string, unknown>) => unknown) => selector({})
@@ -41,7 +41,7 @@ describe('file explorer operation generations', () => {
expect(
getFileExplorerOperationOwnerFromState(useAppStore.getState(), FLOATING_TERMINAL_WORKTREE_ID)
).toEqual(owner)
expect(guard.route.settings.activeRuntimeEnvironmentId).toBeNull()
expect(guard.route.target).toEqual({ kind: 'local' })
expect(guard.route.expectedExecutionHostId).toBe('local')
expect(() => guard.assertCurrent()).not.toThrow()
})
@@ -11,8 +11,9 @@ import { parseWorkspaceKey } from '../../../../shared/workspace-scope'
import { translate } from '@/i18n/i18n'
import {
getExplicitRuntimeEnvironmentIdForWorktree,
getSettingsForWorktreeRuntimeOwner
getRuntimeEnvironmentIdForWorktree
} from '@/lib/worktree-runtime-owner'
import type { RuntimeClientTarget } from '@/runtime/runtime-client-target'
import type { FileExplorerOperationOwner } from './file-explorer-types'
import {
getFloatingWorkspaceOperationRoute,
@@ -24,7 +25,7 @@ import { captureWorktreeOperationGenerationGuard } from '@/lib/worktree-operatio
const ownerUnresolved = (): Error => new Error(getFileExplorerOwnerUnresolvedMessage())
export type FileExplorerOperationRoute = {
settings: { activeRuntimeEnvironmentId: string | null }
target: RuntimeClientTarget
connectionId?: string
expectedExecutionHostId?: 'local' | `ssh:${string}`
expectedSshTargetId?: string
@@ -82,13 +83,12 @@ export function getFileExplorerOperationOwnerFromState(
if (connectionId === undefined && explicitRuntimeEnvironmentId === null) {
return { kind: 'unresolved' }
}
const settings = getSettingsForWorktreeRuntimeOwner(state, worktreeId)
// Why: inferred SSH ownership outranks global runtime focus, but an explicit
// Why: inferred SSH ownership outranks a projected runtime owner, but an explicit
// workspace runtime still owns its files.
const runtimeEnvironmentId =
connectionId && explicitRuntimeEnvironmentId === null
? null
: settings.activeRuntimeEnvironmentId?.trim()
: getRuntimeEnvironmentIdForWorktree(state, worktreeId)?.trim()
if (runtimeEnvironmentId) {
return {
kind: 'runtime',
@@ -114,19 +114,19 @@ export function getFileExplorerOperationRoute(
switch (owner.kind) {
case 'local':
return {
settings: { activeRuntimeEnvironmentId: null },
target: { kind: 'local' },
expectedExecutionHostId: 'local'
}
case 'ssh':
return {
settings: { activeRuntimeEnvironmentId: null },
target: { kind: 'local' },
connectionId: owner.connectionId,
expectedExecutionHostId: toSshExecutionHostId(owner.connectionId)
}
case 'runtime': {
const host = parseExecutionHostId(owner.executionHostId)
return {
settings: { activeRuntimeEnvironmentId: owner.environmentId },
target: { kind: 'environment', environmentId: owner.environmentId },
...(host?.kind === 'ssh'
? { expectedExecutionHostId: host.id }
: { expectedExecutionHostId: 'local' as const })
@@ -20,6 +20,7 @@ import { useFileExplorerTreeLoadEffects } from './use-file-explorer-tree-load-ef
import { useFileExplorerWatch } from './useFileExplorerWatch'
import type { useFileExplorerSelection } from './useFileExplorerSelection'
import type { useFileExplorerTree } from './useFileExplorerTree'
import { runtimeTargetForOwnerEnvironment } from '@/runtime/runtime-client-target'
type UseFileExplorerTreePaneStateParams = {
onRevealOutsideRoot?: () => void
@@ -127,7 +128,7 @@ export function useFileExplorerTreePaneState({
? s.sshConnectionStates.get(connectionId)?.supportsFolderDownload === true
: false
})
const activeRuntimeEnvironmentId = useAppStore((s) =>
const ownerRuntimeEnvironmentId = useAppStore((s) =>
getRuntimeEnvironmentIdForWorktree(s, activeWorktreeId)
)
const toggleDir = useAppStore((s) => s.toggleDir)
@@ -138,15 +139,15 @@ export function useFileExplorerTreePaneState({
const runtimeDownloadContext = useMemo(
() =>
activeRuntimeEnvironmentId && activeWorktreeId && worktreePath
ownerRuntimeEnvironmentId && activeWorktreeId && worktreePath
? {
settings: { activeRuntimeEnvironmentId },
target: runtimeTargetForOwnerEnvironment(ownerRuntimeEnvironmentId),
worktreeId: activeWorktreeId,
worktreePath,
connectionId: activeRepo?.connectionId ?? undefined
}
: null,
[activeRepo?.connectionId, activeRuntimeEnvironmentId, activeWorktreeId, worktreePath]
[activeRepo?.connectionId, ownerRuntimeEnvironmentId, activeWorktreeId, worktreePath]
)
const isWindows = useMemo(() => navigator.userAgent.includes('Windows'), [])
@@ -249,7 +250,7 @@ export function useFileExplorerTreePaneState({
const handlers = useFileExplorerHandlers({
activeWorktreeId,
runtimeEnvironmentId: activeRuntimeEnvironmentId,
runtimeEnvironmentId: ownerRuntimeEnvironmentId,
openFile,
makePreviewFilePermanent,
toggleDir: hasNameFilter ? handleToggleNameFilterDir : toggleDir,
@@ -130,7 +130,7 @@ export function useFileDeletion({
const state = useAppStore.getState()
const worktree = activeWorktreeId ? state.getKnownWorktreeById(activeWorktreeId) : null
const fileContext = {
settings: operationRoute.settings,
target: operationRoute.target,
worktreeId: activeWorktreeId,
worktreePath: worktree?.path ?? null,
connectionId: operationRoute.connectionId,
@@ -147,7 +147,7 @@ export function useFileDeletion({
if (!node.isDirectory) {
try {
const rf = await readRuntimeFileContent({
settings: fileContext.settings,
target: fileContext.target,
filePath: node.path,
relativePath: node.relativePath,
worktreeId: activeWorktreeId ?? undefined,
@@ -172,7 +172,7 @@ export function useFileDeletion({
await writeRuntimeFile(
{
...fileContext,
settings: currentRoute.settings,
target: currentRoute.target,
connectionId: currentRoute.connectionId
},
node.path,
@@ -185,7 +185,7 @@ export function useFileDeletion({
await deleteRuntimePath(
{
...fileContext,
settings: currentRoute.settings,
target: currentRoute.target,
connectionId: currentRoute.connectionId
},
node.path,
@@ -49,7 +49,7 @@ export function useFileDuplicate({
return
}
const context = {
settings: operationGuard.route.settings,
target: operationGuard.route.target,
worktreeId: activeWorktreeId,
worktreePath,
connectionId: operationGuard.route.connectionId,
@@ -11,6 +11,7 @@ import {
getFileExplorerOwnerUnresolvedMessage,
requireMatchingFileExplorerOperationRoute
} from './file-explorer-operation-owner'
import { runtimeTargetEnvironmentId } from '@/runtime/runtime-client-target'
type UseFileExplorerHandlersParams = {
activeWorktreeId: string | null
@@ -133,7 +134,7 @@ export async function activateFileExplorerNode(args: {
let fileRuntimeEnvironmentId: string | null
try {
const route = requireMatchingFileExplorerOperationRoute(activeWorktreeId, node.operationOwner)
fileRuntimeEnvironmentId = route.settings.activeRuntimeEnvironmentId?.trim() || null
fileRuntimeEnvironmentId = runtimeTargetEnvironmentId(route.target)
} catch {
toast.error(getFileExplorerOwnerUnresolvedMessage())
return
@@ -89,7 +89,7 @@ describe('file explorer OS file drops on remote workspaces', () => {
worktreeId: 'wt-ssh',
worktreePath: SSH_ROOT,
connectionId: 'ssh-1',
settings: { activeRuntimeEnvironmentId: null },
target: { kind: 'local' },
expectedExecutionHostId: 'ssh:ssh-1',
expectedSshTargetId: 'ssh-1',
expectedSshConnectionGeneration: 3
@@ -104,7 +104,7 @@ describe('file explorer OS file drops on remote workspaces', () => {
await waitFor(() => expect(mocks.importPaths).toHaveBeenCalledTimes(1))
expect(mocks.importPaths.mock.calls[0][0]).toMatchObject({
worktreeId: 'wt-runtime',
settings: { activeRuntimeEnvironmentId: 'owner-runtime' }
target: { kind: 'environment', environmentId: 'owner-runtime' }
})
expect(mocks.importPaths.mock.calls[0][2]).toBe(joinPath(RUNTIME_ROOT, 'src'))
})
@@ -102,7 +102,7 @@ export function useFileExplorerImport({
guard.assertCurrent()
const { results } = await importExternalPathsToRuntime(
{
settings: guard.route.settings,
target: guard.route.target,
worktreeId: destination.worktreeId,
worktreePath: destination.worktreePath,
connectionId: guard.route.connectionId,
@@ -14,6 +14,7 @@ import {
captureFileExplorerOperationGuard,
getFileExplorerOperationOwner
} from './file-explorer-operation-owner'
import { runtimeTargetEnvironmentId } from '@/runtime/runtime-client-target'
type UseFileExplorerInlineInputParams = {
activeWorktreeId: string | null
@@ -138,7 +139,7 @@ export function useFileExplorerInlineInput({
)
const operationRoute = operationGuard.route
const fileContext = {
settings: operationRoute.settings,
target: operationRoute.target,
worktreeId: activeWorktreeId,
worktreePath,
connectionId: operationRoute.connectionId,
@@ -160,7 +161,7 @@ export function useFileExplorerInlineInput({
await deleteRuntimePath(
{
...fileContext,
settings: currentRoute.settings,
target: currentRoute.target,
connectionId: currentRoute.connectionId
},
fullPath,
@@ -173,7 +174,7 @@ export function useFileExplorerInlineInput({
await createRuntimePath(
{
...fileContext,
settings: currentRoute.settings,
target: currentRoute.target,
connectionId: currentRoute.connectionId
},
fullPath,
@@ -189,7 +190,7 @@ export function useFileExplorerInlineInput({
await deleteRuntimePath(
{
...fileContext,
settings: currentRoute.settings,
target: currentRoute.target,
connectionId: currentRoute.connectionId
},
fullPath
@@ -201,7 +202,7 @@ export function useFileExplorerInlineInput({
await createRuntimePath(
{
...fileContext,
settings: currentRoute.settings,
target: currentRoute.target,
connectionId: currentRoute.connectionId
},
fullPath,
@@ -213,8 +214,7 @@ export function useFileExplorerInlineInput({
}
await refreshDir(inlineInput.parentPath)
if (inlineInput.type === 'file') {
const runtimeEnvironmentId =
fileContext.settings.activeRuntimeEnvironmentId?.trim() || null
const runtimeEnvironmentId = runtimeTargetEnvironmentId(fileContext.target)
openFile(
{
filePath: fullPath,
@@ -58,7 +58,7 @@ export function useFileExplorerMoveDrop({
const operationGuard = captureFileExplorerOperationGuard(activeWorktreeId, operationOwner)
const operationRoute = operationGuard.route
const fileContext = {
settings: operationRoute.settings,
target: operationRoute.target,
worktreeId: activeWorktreeId,
worktreePath,
connectionId: operationRoute.connectionId,
@@ -195,7 +195,7 @@ export function useFileExplorerTree(
}
return statUserOpenedPath(
{
settings: route.settings,
target: route.target,
worktreeId: activeWorktreeId,
worktreePath,
connectionId: route.connectionId
@@ -121,7 +121,7 @@ it('invalidates completed results and errors when the execution owner changes at
)
await act(async () => vi.advanceTimersByTimeAsync(300))
expect(search).toHaveBeenCalledTimes(2)
expect(search.mock.calls[1][0].settings.activeRuntimeEnvironmentId).toBe('remote-a')
expect(search.mock.calls[1][0].target).toEqual({ kind: 'environment', environmentId: 'remote-a' })
expect(
useAppStore.getState().fileSearchStateByWorktree['folder:a'].resultOwner?.executionHostId
).toBe('runtime:remote-a')
@@ -84,7 +84,7 @@ describe('useFileSearchRunner result ownership', () => {
expect(mocks.searchRuntimeFiles).toHaveBeenCalledWith(
expect.objectContaining({
settings: { activeRuntimeEnvironmentId: 'search-runtime-a' },
target: { kind: 'environment', environmentId: 'search-runtime-a' },
worktreeId
}),
expect.any(Object),
@@ -116,7 +116,7 @@ describe('useFileSearchRunner result ownership', () => {
await finishSearch(hook.result.current.executeSearch)
expect(mocks.searchRuntimeFiles).toHaveBeenCalledWith(
expect.objectContaining({ settings: { activeRuntimeEnvironmentId: null }, worktreeId }),
expect.objectContaining({ target: { kind: 'local' }, worktreeId }),
expect.any(Object),
expect.any(AbortSignal)
)
@@ -148,7 +148,7 @@ describe('useFileSearchRunner result ownership', () => {
expect(mocks.searchRuntimeFiles).toHaveBeenCalledWith(
expect.objectContaining({
settings: { activeRuntimeEnvironmentId: null },
target: { kind: 'local' },
worktreeId,
connectionId: 'ssh-target'
}),
@@ -17,6 +17,7 @@ import {
getRuntimeEnvironmentIdForWorktree
} from '@/lib/worktree-runtime-owner'
import type { SearchResult } from '../../../../shared/code-search-types'
import { runtimeTargetForOwnerEnvironment } from '@/runtime/runtime-client-target'
const SEARCH_DEBOUNCE_MS = 300
const SEARCH_MAX_RESULTS = 2000
@@ -95,10 +96,9 @@ export function useFileSearchRunner({
excludePattern: currentSearchState?.excludePattern || undefined
})
) {
const runtimeSettings = { activeRuntimeEnvironmentId: runtimeEnvironmentId }
updateActiveSearchState({
results: createEmptyRuntimeFileSearchResult(),
resultOwner: createFileSearchResultOwner(activeWorktreeId, runtimeSettings, {
resultOwner: createFileSearchResultOwner(activeWorktreeId, runtimeEnvironmentId, {
rootPath: worktreePath,
executionHostId
}),
@@ -116,8 +116,7 @@ export function useFileSearchRunner({
searchTimerRef.current = setTimeout(async () => {
searchTimerRef.current = null
// Why: results can outlive the selected worktree; clicks must reuse the route that produced them.
const runtimeSettings = { activeRuntimeEnvironmentId: runtimeEnvironmentId }
const resultOwner = createFileSearchResultOwner(activeWorktreeId, runtimeSettings, {
const resultOwner = createFileSearchResultOwner(activeWorktreeId, runtimeEnvironmentId, {
rootPath: worktreePath,
executionHostId
})
@@ -145,7 +144,7 @@ export function useFileSearchRunner({
}
const results = await searchRuntimeFiles(
{
settings: runtimeSettings,
target: runtimeTargetForOwnerEnvironment(runtimeEnvironmentId),
worktreeId: activeWorktreeId,
worktreePath,
connectionId
@@ -75,7 +75,7 @@ it('inspects and creates a managed-host repo config on that host, never the desk
await waitFor(() => expect(runtimeFiles.readRuntimeDirectory).toHaveBeenCalled())
expect(runtimeFiles.readRuntimeDirectory).toHaveBeenCalledWith(
expect.objectContaining({
settings: { activeRuntimeEnvironmentId: 'host-a' },
target: { kind: 'environment', environmentId: 'host-a' },
worktreeId,
worktreePath: root
}),
@@ -89,7 +89,7 @@ it('inspects and creates a managed-host repo config on that host, never the desk
await waitFor(() => expect(runtimeFiles.writeRuntimeFile).toHaveBeenCalled())
expect(runtimeFiles.writeRuntimeFile).toHaveBeenCalledWith(
expect.objectContaining({ settings: { activeRuntimeEnvironmentId: 'host-a' } }),
expect.objectContaining({ target: { kind: 'environment', environmentId: 'host-a' } }),
`${root}/.mcp.json`,
expect.any(String)
)
@@ -109,7 +109,7 @@ it('keeps a desktop repo on the desktop while a managed host is focused', async
await waitFor(() => expect(runtimeFiles.readRuntimeDirectory).toHaveBeenCalled())
expect(runtimeFiles.readRuntimeDirectory).toHaveBeenCalledWith(
expect.objectContaining({ settings: { activeRuntimeEnvironmentId: null } }),
expect.objectContaining({ target: { kind: 'local' } }),
root
)
})
@@ -33,6 +33,7 @@ import { loadMcpConfigInspections } from './mcp-config-inspection'
import { translate } from '@/i18n/i18n'
import { captureDirectSshMutationExpectation } from '@/lib/ssh-mutation-expectation'
import { writeRuntimeFile, type RuntimeFileOperationArgs } from '@/runtime/runtime-file-client'
import { runtimeTargetForOwnerEnvironment } from '@/runtime/runtime-client-target'
type McpConfigSectionProps = {
repo: Repo
@@ -100,7 +101,7 @@ export function McpConfigSection({ repo }: McpConfigSectionProps): React.JSX.Ele
const targetHostId = getWorktreeExecutionHostId(targetWorktree, repo)
const fileContext = useMemo(
(): RuntimeFileOperationArgs => ({
settings: { activeRuntimeEnvironmentId: runtimeEnvironmentId },
target: runtimeTargetForOwnerEnvironment(runtimeEnvironmentId),
worktreeId: targetWorktreeId,
worktreePath: targetRootPath,
connectionId
@@ -77,7 +77,7 @@ export async function loadMcpConfigInspections(
try {
const result = await readRuntimeFileContent({
settings: context.settings,
target: context.target,
worktreeId: context.worktreeId ?? undefined,
filePath: absolutePath,
relativePath: candidate.relativePath,
@@ -26,7 +26,7 @@ describe('openTabEntryWithOperations', () => {
groupId: 'group-1',
worktreePath: '/repo',
runtimeContext: {
settings: null,
target: { kind: 'local' as const },
worktreeId: 'wt-1',
worktreePath: '/repo'
},
@@ -5,6 +5,7 @@ import { getResolvedExecutionHostIdForWorktree } from '@/lib/resolved-worktree-e
import type { RuntimeFileOperationArgs } from '@/runtime/runtime-file-client'
import type { useAppStore } from '@/store'
import { parseWorkspaceKey } from '../../../../shared/workspace-scope'
import { runtimeTargetEnvironmentId } from '@/runtime/runtime-client-target'
type TabEntryAbsolutePathOwnerState = Pick<
ReturnType<typeof useAppStore.getState>,
@@ -23,12 +24,11 @@ type TabEntryAbsolutePathOwnerState = Pick<
>
export function isTabEntryAbsolutePathAllowed(
context: Pick<RuntimeFileOperationArgs, 'connectionId' | 'settings'>
context: Pick<RuntimeFileOperationArgs, 'connectionId' | 'target'>
): boolean {
// Why: file-operation contexts pin `settings` to the route owner, never to the focused server.
return !isLocalPathOpenBlocked(
getLocalPathOpenOwnerForRoute({
runtimeEnvironmentId: context.settings?.activeRuntimeEnvironmentId,
runtimeEnvironmentId: runtimeTargetEnvironmentId(context.target),
connectionId: context.connectionId
})
)
@@ -69,9 +69,7 @@ export function getTabEntryFileOperationContext(
getFolderWorkspaceConnectionId(state, workspaceKey.folderWorkspaceId) === null
) {
return {
settings: state.settings
? { ...state.settings, activeRuntimeEnvironmentId: null }
: { activeRuntimeEnvironmentId: null },
target: { kind: 'local' },
worktreeId,
worktreePath,
expectedExecutionHostId: 'local'
@@ -159,7 +159,7 @@ describe('handleTerminalFileDrop', () => {
expect(mocks.importExternalPathsToRuntime).toHaveBeenCalledWith(
{
settings: { activeRuntimeEnvironmentId: 'env-1' },
target: { kind: 'environment', environmentId: 'env-1' },
worktreeId: 'wt-1',
worktreePath: '/remote/repo',
expectedExecutionHostId: 'local',
@@ -262,7 +262,7 @@ describe('handleTerminalFileDrop', () => {
expect(mocks.importExternalPathsToRuntime).toHaveBeenCalledWith(
{
settings: { activeRuntimeEnvironmentId: 'env-1' },
target: { kind: 'environment', environmentId: 'env-1' },
worktreeId: 'wt-1',
worktreePath: '//server/share/repo',
expectedExecutionHostId: 'local',
@@ -326,7 +326,7 @@ describe('handleTerminalFileDrop', () => {
expect(mocks.importExternalPathsToRuntime).toHaveBeenCalledWith(
{
settings: { activeRuntimeEnvironmentId: 'owner-runtime' },
target: { kind: 'environment', environmentId: 'owner-runtime' },
worktreeId: 'wt-1',
worktreePath: '/remote/repo',
expectedExecutionHostId: 'local',
@@ -167,7 +167,7 @@ describe.each(['native', 'internal'])('%s runtime terminal catalog ownership', (
if (lane === 'native') {
expect(mocks.importPaths).toHaveBeenCalledExactlyOnceWith(
{
settings: { activeRuntimeEnvironmentId: 'owner-runtime' },
target: { kind: 'environment', environmentId: 'owner-runtime' },
worktreeId: workspaceId,
worktreePath: '/owner/workspace',
expectedExecutionHostId: executionHostId,
@@ -15,7 +15,7 @@ const routing = vi.hoisted(() => {
vi.mock('./terminal-file-path-mapping', async (importOriginal) => ({
...(await importOriginal<typeof FilePathMappingModule>()),
getTerminalFileContext: (worktreeId: string, worktreePath: string) => ({
settings: null,
target: { kind: 'local' },
worktreeId,
worktreePath,
connectionId: routing.connectionId
@@ -23,7 +23,7 @@ import {
} from '../../../../shared/execution-host'
import { statUserOpenedPath } from '@/lib/user-opened-local-path'
import {
CLIENT_LOCAL_FILE_SETTINGS,
CLIENT_LOCAL_FILE_TARGET,
resolveHostWorkspaceFile,
type HostWorkspaceFile
} from './terminal-host-workspace-file'
@@ -36,6 +36,7 @@ export {
terminalPathWslDistro,
type TerminalFileContext
} from './terminal-file-path-mapping'
import { runtimeTargetEnvironmentId } from '@/runtime/runtime-client-target'
export type FileOpenFailure = {
/** `missing` is a verified absence; `unverifiable` means the host could not answer (dropped SSH, timeout, denied path). */
@@ -164,7 +165,7 @@ export function openDetectedFilePath(
clientLocal = true
fileContext = {
...fileContext,
settings: CLIENT_LOCAL_FILE_SETTINGS,
target: CLIENT_LOCAL_FILE_TARGET,
connectionId: undefined
}
statResult = { isDirectory: linkOwner.isDirectory, escapesWorktree: false }
@@ -254,7 +255,7 @@ export function openDetectedFilePath(
)
) {
// Why: early resolution is only needed to avoid an existing sibling-tab collision.
const runtimeOwnerId = fileContext.settings?.activeRuntimeEnvironmentId?.trim()
const runtimeOwnerId = runtimeTargetEnvironmentId(fileContext.target)
const executionHostId = runtimeOwnerId
? toRuntimeExecutionHostId(runtimeOwnerId)
: getConnectionExecutionHostId(fileContext.connectionId)
@@ -289,7 +290,7 @@ export function openDetectedFilePath(
// Why: absolute SSH paths outside the worktree otherwise look identical
// to client-local external files when the editor reloads or restores.
...(relativePath === filePath &&
!fileContext.settings?.activeRuntimeEnvironmentId?.trim() &&
fileContext.target.kind === 'local' &&
fileContext.connectionId
? { externalSshTargetId: fileContext.connectionId }
: {})
@@ -2,8 +2,8 @@ import { useAppStore } from '@/store'
import { getConnectionId } from '@/lib/connection-context'
import { resolveWorktreeOperationRouteResult } from '@/lib/worktree-operation-route'
import { buildWorkspaceFileContext } from '@/lib/workspace-file-host-routing'
import { getRuntimeTargetForWorktreeOwner } from '@/lib/file-owner-runtime-target'
import type { RuntimeFileOperationArgs } from '@/runtime/runtime-file-client'
import { getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
import { parseExecutionHostId } from '../../../../shared/execution-host'
import { parseWslUncPath, toWindowsWslPath } from '../../../../shared/wsl-paths'
import { terminalFileSourceHost } from './terminal-worktree-path-link'
@@ -19,8 +19,21 @@ export function getTerminalFileContext(
worktreePath: string,
runtimeEnvironmentId?: string | null
): TerminalFileContext {
const context = buildWorkspaceFileContext(worktreeId, worktreePath, runtimeEnvironmentId)
const state = useAppStore.getState()
if (
runtimeEnvironmentId === undefined &&
worktreeId &&
!getRuntimeTargetForWorktreeOwner(state, worktreeId)
) {
// Why: rows disagree on the owner; the local target is a placeholder every caller refuses.
return {
target: { kind: 'local' },
worktreeId,
worktreePath,
sourceHostResolved: false
}
}
const context = buildWorkspaceFileContext(worktreeId, worktreePath, runtimeEnvironmentId)
const sourceHost = terminalFileSourceHost(state, context)
if (!sourceHost) {
// Why: the connection lookup ignores detected rows, so its local `null` must not override an
@@ -35,10 +48,7 @@ export function getTerminalFileContext(
}
// Why: same-id rows on several hosts leave connectionId unset, which reads downstream as local;
// the resolved owner names the direct SSH host. A paired runtime keeps its own transport.
const sshHost =
getActiveRuntimeTarget(context.settings).kind === 'environment'
? null
: parseExecutionHostId(sourceHost)
const sshHost = context.target.kind === 'environment' ? null : parseExecutionHostId(sourceHost)
return sshHost?.kind === 'ssh' && !context.connectionId
? { ...context, connectionId: sshHost.targetId, sourceHostResolved: true }
: { ...context, sourceHostResolved: true }
@@ -1,5 +1,9 @@
import { callHostRoute } from '@/runtime/host-route-call'
import { hostRouteForAuthority } from '@/runtime/runtime-client-target'
import {
hostRouteForAuthority,
runtimeTargetEnvironmentId,
type RuntimeClientTarget
} from '@/runtime/runtime-client-target'
import type { RuntimeFileOperationArgs } from '@/runtime/runtime-file-client-types'
import { statRuntimePath } from '@/runtime/runtime-file-client'
import { isRemoteRuntimeFileOperation } from '@/runtime/runtime-file-routing'
@@ -26,7 +30,7 @@ export type HostWorkspaceFile = {
/** A path the host disowned that this computer has; the user's click here is the only authority to read it. */
export type ClientLocalFile = { kind: 'client'; isDirectory: boolean }
export const CLIENT_LOCAL_FILE_SETTINGS = { activeRuntimeEnvironmentId: null }
export const CLIENT_LOCAL_FILE_TARGET: RuntimeClientTarget = { kind: 'local' }
/** Strips the host's relative suffix; separators are swapped 1:1, so the slice stays byte-exact. */
function workspaceRootOf(absolutePath: string, relativePath: string): string | null {
@@ -56,7 +60,7 @@ async function statClientLocalFile(
}
try {
const stat = await statRuntimePath(
{ settings: CLIENT_LOCAL_FILE_SETTINGS, worktreeId: null, worktreePath: null },
{ target: CLIENT_LOCAL_FILE_TARGET, worktreeId: null, worktreePath: null },
absolutePath,
userNamedFileAccess()
)
@@ -77,7 +81,7 @@ export async function resolveHostWorkspaceFile(
context: RuntimeFileOperationArgs,
absolutePath: string
): Promise<HostWorkspaceFile | ClientLocalFile | null> {
const environmentId = context.settings?.activeRuntimeEnvironmentId?.trim()
const environmentId = runtimeTargetEnvironmentId(context.target)
if (
!environmentId ||
!context.worktreeId ||
@@ -53,9 +53,13 @@ vi.mock('@/lib/file-preview', () => ({
installTerminalLinkTestEnvironment(doubles)
describe('handleOscLink', () => {
it('stats remote-runtime file links through the active runtime environment', async () => {
it("stats remote-runtime file links through the workspace owner's runtime, not the focused one", async () => {
setPlatform('Macintosh')
storeState.settings = { activeRuntimeEnvironmentId: 'env-1' }
// Before: the focused server (env-2) answered for a link the workspace owner (env-1) holds.
storeState.settings = { activeRuntimeEnvironmentId: 'env-2' }
storeState.worktreesByRepo = {
'repo-1': [{ id: 'wt-1', path: '/tmp', repoId: 'repo-1', hostId: 'runtime:env-1' }]
}
runtimeEnvironmentCallMock.mockResolvedValueOnce({
id: 'rpc-1',
ok: true,
@@ -48,7 +48,6 @@ function captureNativeTerminalFileDropDestination(
}
const dropTarget = captureTerminalDropTarget(pane, transport)
const state = useAppStore.getState()
const settings = state.settings
const owner = captureTerminalDropTransportOwner(transport)
const worktreePath = resolveTerminalDropWorktreePath(
worktreeId,
@@ -80,7 +79,6 @@ function captureNativeTerminalFileDropDestination(
pane,
dataPaths: paths,
dropTarget,
settings,
owner,
worktreePath,
localWslDrop
@@ -2,7 +2,6 @@ import { toast } from 'sonner'
import { extractIpcErrorMessage } from '@/lib/ipc-error'
import type { PaneManager } from '@/lib/pane-manager/pane-manager'
import { importExternalPathsToRuntime } from '@/runtime/runtime-file-client'
import type { useAppStore } from '@/store'
import { translate } from '@/i18n/i18n'
import { isWslUncPath } from '../../../../shared/wsl-paths'
import type { PtyTransport } from './pty-transport'
@@ -23,10 +22,10 @@ import { showTerminalDropWriteFailure } from './terminal-drop-write-failure'
import { joinRuntimeTerminalDropDir } from './terminal-drop-worktree-path'
import type { captureTerminalDropTransportOwner } from './terminal-drop-transport-owner'
import { toLocalWslDropPath } from './terminal-drop-local-wsl'
import { runtimeTargetForOwnerEnvironment } from '@/runtime/runtime-client-target'
export async function deliverNativeTerminalFileDrop(
args: NativeDropFlowArgs & {
settings: ReturnType<typeof useAppStore.getState>['settings']
owner: ReturnType<typeof captureTerminalDropTransportOwner>
worktreeId: string
localWslDrop: boolean
@@ -40,7 +39,6 @@ export async function deliverNativeTerminalFileDrop(
pane,
dataPaths,
dropTarget,
settings,
owner,
worktreePath,
localWslDrop
@@ -52,7 +50,6 @@ export async function deliverNativeTerminalFileDrop(
manager,
paneTransports,
pane,
settings,
tabId,
worktreeId,
worktreePath,
@@ -127,7 +124,6 @@ export type NativeDropFlowArgs = {
async function uploadRuntimeDropPaths(
args: NativeDropFlowArgs & {
runtimeEnvironmentId: string
settings: ReturnType<typeof useAppStore.getState>['settings']
worktreeId: string
}
): Promise<void> {
@@ -145,7 +141,7 @@ async function uploadRuntimeDropPaths(
{
// Why: drops into existing worktrees must follow the worktree owner,
// not the currently focused host in the sidebar.
settings: { ...args.settings, activeRuntimeEnvironmentId: args.runtimeEnvironmentId },
target: runtimeTargetForOwnerEnvironment(args.runtimeEnvironmentId),
worktreeId: args.worktreeId,
worktreePath: args.worktreePath,
expectedExecutionHostId: args.expectedExecutionHostId,
@@ -459,7 +459,7 @@ describe('terminal element file drops', () => {
await settle()
expect(mocks.importPaths).toHaveBeenCalledWith(
expect.objectContaining({
settings: { activeRuntimeEnvironmentId: 'owner-runtime' },
target: { kind: 'environment', environmentId: 'owner-runtime' },
worktreePath: '/owner/workspace',
expectedExecutionHostId: 'local'
}),
@@ -3,7 +3,11 @@ import { createTerminalPathExistenceBatch } from '@/components/terminal-pane/ter
import { requirePathExistenceResults } from '../../../../shared/path-existence-batch'
const remote = vi.hoisted(() => vi.fn())
vi.mock('@/runtime/runtime-path-existence-batch', () => ({ runtimePathsExist: remote }))
const context = { settings: null, worktreeId: 'folder:/work', worktreePath: '/work' }
const context = {
target: { kind: 'local' as const },
worktreeId: 'folder:/work',
worktreePath: '/work'
}
afterEach(() => {
vi.unstubAllGlobals()
remote.mockReset()
@@ -26,11 +30,11 @@ it('isolates identical paths by runtime, worktree, connection, and hover turn',
)
const enqueue = createTerminalPathExistenceBatch()
const contexts = [
{ ...context, settings: { activeRuntimeEnvironmentId: 'a' } },
{ ...context, settings: { activeRuntimeEnvironmentId: 'b' } },
{ ...context, target: { kind: 'environment' as const, environmentId: 'a' } },
{ ...context, target: { kind: 'environment' as const, environmentId: 'b' } },
{
...context,
settings: { activeRuntimeEnvironmentId: 'a' },
target: { kind: 'environment' as const, environmentId: 'a' },
worktreeId: 'folder:/other',
worktreePath: '/other'
},

Some files were not shown because too many files have changed in this diff Show More