mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+20
@@ -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 }))
|
||||
|
||||
+19
-2
@@ -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('', '/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' }),
|
||||
|
||||
+3
-2
@@ -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({})
|
||||
|
||||
+1
-1
@@ -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',
|
||||
|
||||
+1
-1
@@ -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 ||
|
||||
|
||||
+6
-2
@@ -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
Reference in New Issue
Block a user