diff --git a/config/focus-setting-read-baseline.txt b/config/focus-setting-read-baseline.txt new file mode 100644 index 00000000000..75fe522ca4f --- /dev/null +++ b/config/focus-setting-read-baseline.txt @@ -0,0 +1,648 @@ +# Renderer reads of the Active Server setting (member, element and destructuring reads of +# activeRuntimeEnvironmentId) plus calls of exported functions that read it, per file. +# This is a RATCHET: counts may only go DOWN. Only creation flows with no source row may read +# the default host, through defaultCreationHost. Everything else routes by the owner. +# Prune after removing reads: pnpm check:owner-routing-ratchet --prune +2 src/renderer/src/App.tsx +3 src/renderer/src/app-shell/use-app-shell-services.ts +1 src/renderer/src/app-shell/use-visible-review-refresh-reporting.ts +1 src/renderer/src/components/Landing.tsx +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 +1 src/renderer/src/components/activity/use-agent-pane-threads.ts +4 src/renderer/src/components/agent/AgentSettingsDialog.tsx +1 src/renderer/src/components/automations/automation-host-client.ts +1 src/renderer/src/components/automations/automation-orca-save.ts +1 src/renderer/src/components/automations/automations-page-fixtures.ts +1 src/renderer/src/components/automations/use-automation-source-availability.ts +1 src/renderer/src/components/automations/use-automation-source-host-availability.ts +2 src/renderer/src/components/automations/use-automations-page-controller.ts +1 src/renderer/src/components/automations/use-automations-page-destination-form.ts +2 src/renderer/src/components/automations/use-automations-page-setup-state.ts +1 src/renderer/src/components/browser-pane/ReopenBrowserPageOnServerButton.tsx +1 src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.tsx +1 src/renderer/src/components/browser-pane/browser-reopen-on-server.ts +1 src/renderer/src/components/browser-pane/stream-remote/remote-browser-page-pane.tsx +1 src/renderer/src/components/browser-pane/stream-remote/use-remote-browser-page-lifecycle.ts +1 src/renderer/src/components/browser-pane/stream-remote/use-remote-browser-page-stream.ts +1 src/renderer/src/components/browser-pane/stream-remote/use-remote-browser-stream-activation.ts +2 src/renderer/src/components/browser-pane/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-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 +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 +1 src/renderer/src/components/feature-wall/FeatureWallTourSurface.tsx +2 src/renderer/src/components/feature-wall/use-feature-wall-task-source-presentation.ts +2 src/renderer/src/components/feature-wall/use-integration-connection-status.ts +1 src/renderer/src/components/floating-terminal/use-floating-terminal-panel-controller.ts +1 src/renderer/src/components/floating-terminal/use-floating-terminal-panel-store-state.ts +1 src/renderer/src/components/github-item-dialog/discuss-item/conversation-tab.tsx +4 src/renderer/src/components/github-item-dialog/edit-item-fields/gh-edit-section-mutations.ts +7 src/renderer/src/components/github-item-dialog/edit-item-fields/gh-edit-section.tsx +2 src/renderer/src/components/github-item-dialog/land-pull-request/pr-actions-panel.tsx +5 src/renderer/src/components/github-item-dialog/land-pull-request/pr-reviewers-panel.tsx +6 src/renderer/src/components/github-item-dialog/land-pull-request/pr-reviewers-request-actions.ts +1 src/renderer/src/components/github-project/ProjectCellIssueType.tsx +2 src/renderer/src/components/github-project/ProjectCellRepositoryEditors.tsx +2 src/renderer/src/components/github-project/ProjectPicker.tsx +4 src/renderer/src/components/github-project/project-picker-runtime.ts +1 src/renderer/src/components/github-project/slug-dialog/AssigneesEditor.tsx +1 src/renderer/src/components/github-project/slug-dialog/Comments.tsx +1 src/renderer/src/components/github-project/slug-dialog/LabelsEditor.tsx +1 src/renderer/src/components/github-project/slug-dialog/SlugDialogBody.tsx +2 src/renderer/src/components/github-project/useProjectPickerBrowse.ts +2 src/renderer/src/components/github-project/useProjectViewTable.ts +3 src/renderer/src/components/github/PRAssigneesPanel.tsx +2 src/renderer/src/components/github/PRFilterDropdowns.tsx +3 src/renderer/src/components/github/github-rate-limit-display.tsx +5 src/renderer/src/components/github/github-work-item-edit-mutations.ts +3 src/renderer/src/components/gitlab/gitlab-rate-limit-display.tsx +1 src/renderer/src/components/jira-connect-dialog.tsx +1 src/renderer/src/components/landing-preflight-runtime.ts +1 src/renderer/src/components/linear-api-key-dialog.tsx +2 src/renderer/src/components/native-chat-resume-modal.test-support.tsx +1 src/renderer/src/components/native-chat-resume-tree.test-support.ts +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 +2 src/renderer/src/components/native-chat/native-chat-runtime-image-send.ts +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 +1 src/renderer/src/components/new-workspace/smart-workspace-repo-slug.ts +1 src/renderer/src/components/new-workspace/use-jira-source-connection.ts +2 src/renderer/src/components/new-workspace/use-linear-source-connection.ts +1 src/renderer/src/components/new-workspace/use-smart-workspace-secondary-searches.ts +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 +1 src/renderer/src/components/pull-request-page/actions/merge-actions.ts +1 src/renderer/src/components/pull-request-page/actions/panel.tsx +2 src/renderer/src/components/pull-request-page/conversation/tab.tsx +3 src/renderer/src/components/pull-request-page/edit/issue-updates.ts +8 src/renderer/src/components/pull-request-page/edit/section.tsx +4 src/renderer/src/components/pull-request-page/reviewers/panel.tsx +4 src/renderer/src/components/pull-request-page/reviewers/request-actions.ts +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 +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 +1 src/renderer/src/components/right-sidebar/FileExplorerFilesTreePane.tsx +1 src/renderer/src/components/right-sidebar/FolderWorkspacePrChecksRow.tsx +1 src/renderer/src/components/right-sidebar/ai-vault-original-pane-actions.ts +1 src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts +3 src/renderer/src/components/right-sidebar/checks-panel-hosted-review-click-routing.ts +1 src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-controller-state.tsx +1 src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-create-review.tsx +2 src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-generation.tsx +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-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 +1 src/renderer/src/components/right-sidebar/push-target-upstream-refresh-cache.ts +2 src/renderer/src/components/right-sidebar/source-control/ai/use-ai.ts +5 src/renderer/src/components/right-sidebar/source-control/commit/use-bulk-actions.ts +1 src/renderer/src/components/right-sidebar/source-control/commit/use-commit-action.ts +2 src/renderer/src/components/right-sidebar/source-control/commit/use-commit-message-generation.ts +1 src/renderer/src/components/right-sidebar/source-control/commit/use-discard-confirmation.ts +7 src/renderer/src/components/right-sidebar/source-control/commit/use-entry-mutations.ts +1 src/renderer/src/components/right-sidebar/source-control/listing/entry-context-menu.tsx +2 src/renderer/src/components/right-sidebar/source-control/listing/use-file-listing.ts +2 src/renderer/src/components/right-sidebar/source-control/listing/use-submodule-status.ts +1 src/renderer/src/components/right-sidebar/source-control/listing/use-worktree-context.ts +1 src/renderer/src/components/right-sidebar/source-control/panel/use-panel-foundation.ts +2 src/renderer/src/components/right-sidebar/source-control/panel/use-panel-model.ts +2 src/renderer/src/components/right-sidebar/source-control/panel/use-panel-state.ts +1 src/renderer/src/components/right-sidebar/source-control/review/create-pr-intent-run-snapshot.ts +1 src/renderer/src/components/right-sidebar/source-control/review/use-create-pr-intent-commit-message.ts +1 src/renderer/src/components/right-sidebar/source-control/review/use-create-pr-intent-probes.ts +1 src/renderer/src/components/right-sidebar/source-control/review/use-create-pr-intent-review.ts +2 src/renderer/src/components/right-sidebar/source-control/review/use-pull-request-generation.ts +1 src/renderer/src/components/right-sidebar/source-control/sync/git-history-commit-context-menu.tsx +1 src/renderer/src/components/right-sidebar/source-control/sync/use-base-ref-default.ts +1 src/renderer/src/components/right-sidebar/source-control/sync/use-branch-compare.ts +2 src/renderer/src/components/right-sidebar/source-control/sync/use-conflict-abort.ts +2 src/renderer/src/components/right-sidebar/source-control/sync/use-git-history-commit-actions.ts +2 src/renderer/src/components/right-sidebar/source-control/sync/use-git-history.ts +1 src/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.ts +1 src/renderer/src/components/right-sidebar/stale-conflict-operation-poll.ts +2 src/renderer/src/components/right-sidebar/use-create-pull-request-base-ref-discovery.ts +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 +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/useFileSearchScope.ts +2 src/renderer/src/components/right-sidebar/useGitStatusPolling.ts +3 src/renderer/src/components/settings/BaseRefPicker.tsx +2 src/renderer/src/components/settings/BrowserPane.tsx +1 src/renderer/src/components/settings/CliSkillRuntimeSetup.tsx +1 src/renderer/src/components/settings/CommitMessageAiPane.tsx +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 +1 src/renderer/src/components/settings/RepositoryIconPicker.tsx +4 src/renderer/src/components/settings/RuntimeEnvironmentsPane.tsx +1 src/renderer/src/components/settings/Settings.tsx +1 src/renderer/src/components/settings/SettingsSetupGuidePane.tsx +1 src/renderer/src/components/settings/ShortcutsPane.tsx +2 src/renderer/src/components/settings/TasksPane.tsx +1 src/renderer/src/components/settings/WorkspaceDirectorySetting.tsx +1 src/renderer/src/components/settings/bitbucket-credentials-dialog.tsx +1 src/renderer/src/components/settings/bitbucket-integration-card.tsx +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 +2 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 +1 src/renderer/src/components/settings/source-control-preflight-card-status.ts +2 src/renderer/src/components/settings/task-tracker-integration-cards.tsx +2 src/renderer/src/components/settings/token-source-control-integration-cards.tsx +2 src/renderer/src/components/settings/use-integration-provider-status-refresh.ts +3 src/renderer/src/components/settings/use-repository-issue-command.ts +1 src/renderer/src/components/settings/use-runtime-environment-mutation-actions.ts +2 src/renderer/src/components/settings/use-settings-navigation-model.ts +3 src/renderer/src/components/settings/use-settings-store-model.ts +2 src/renderer/src/components/settings/use-settings-terminal-model.ts +4 src/renderer/src/components/settings/use-task-source-provider-readiness.ts +1 src/renderer/src/components/setup-guide/SetupGuideModal.tsx +1 src/renderer/src/components/setup-guide/SetupGuideTelemetryObserver.tsx +1 src/renderer/src/components/setup-guide/setup-guide-progress-readiness.ts +3 src/renderer/src/components/setup-guide/use-setup-guide-progress.ts +1 src/renderer/src/components/sidebar/DeleteWorktreeDialog.tsx +3 src/renderer/src/components/sidebar/LinearAgentSkillSetupPrompt.tsx +1 src/renderer/src/components/sidebar/SavedHostScopeWidenGate.tsx +1 src/renderer/src/components/sidebar/SetupGuideSidebarEntry.tsx +1 src/renderer/src/components/sidebar/SidebarAgentsList.tsx +1 src/renderer/src/components/sidebar/SidebarSettingsHelpMenu.tsx +1 src/renderer/src/components/sidebar/SidebarTaskNavButton.tsx +1 src/renderer/src/components/sidebar/WorktreeCardAgents.tsx +2 src/renderer/src/components/sidebar/WorktreeList.tsx +1 src/renderer/src/components/sidebar/WorktreeRowOpenInSubMenu.tsx +1 src/renderer/src/components/sidebar/add-repo-runtime-owner.ts +1 src/renderer/src/components/sidebar/linear-agent-skill-runtime.ts +2 src/renderer/src/components/sidebar/rendered-sidebar-worktree-order.ts +2 src/renderer/src/components/sidebar/sidebar-host-options.ts +1 src/renderer/src/components/sidebar/sidebar-project-drop.ts +2 src/renderer/src/components/sidebar/use-add-repo-host-selection.ts +1 src/renderer/src/components/sidebar/use-delete-worktree-status-hydration.ts +1 src/renderer/src/components/sidebar/use-sidebar-host-scope-options.ts +1 src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.ts +1 src/renderer/src/components/sidebar/use-workspace-kanban-board-projection.ts +1 src/renderer/src/components/sidebar/use-workspace-reference-details.ts +1 src/renderer/src/components/sidebar/useAddRepoCloneFlow.ts +1 src/renderer/src/components/sidebar/useCreateRepo.ts +2 src/renderer/src/components/sidebar/useSidebarProjectDrop.ts +1 src/renderer/src/components/sidebar/visible-worktree-options-from-state.ts +2 src/renderer/src/components/sidebar/visible-worktrees.ts +1 src/renderer/src/components/sidebar/workspace-options-menu-items.tsx +2 src/renderer/src/components/sidebar/workspace-reference-details.ts +1 src/renderer/src/components/sidebar/workspace-reference-refresh-targets.ts +1 src/renderer/src/components/sidebar/worktree-card-secondary-rows.tsx +1 src/renderer/src/components/sidebar/worktree-filter-visibility.ts +2 src/renderer/src/components/sidebar/worktree-list/listing/use-filters.ts +2 src/renderer/src/components/sidebar/worktree-list/listing/use-folder-path-statuses.ts +1 src/renderer/src/components/sidebar/worktree-list/listing/use-section-rows.ts +1 src/renderer/src/components/sidebar/worktree-list/viewport/VirtualizedWorktreeViewport.tsx +1 src/renderer/src/components/skills/SkillsPage.tsx +7 src/renderer/src/components/status-bar/ClaudeSwitcherMenu.tsx +1 src/renderer/src/components/status-bar/CodexSwitcherMenu.tsx +1 src/renderer/src/components/status-bar/SshStatusSegment.tsx +1 src/renderer/src/components/status-bar/StatusBarSurface.tsx +2 src/renderer/src/components/status-bar/provider-account-sync-key.ts +1 src/renderer/src/components/status-bar/status-bar-claude-accounts.ts +1 src/renderer/src/components/status-bar/status-bar-codex-accounts.ts +1 src/renderer/src/components/status-bar/usage-provider-settings-target.ts +7 src/renderer/src/components/status-bar/use-codex-switcher-controller.ts +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 +1 src/renderer/src/components/tab-bar/windows-shell-menu-visibility.ts +1 src/renderer/src/components/tab-group/AiVaultSessionDropLayer.tsx +3 src/renderer/src/components/tab-group/useTabGroupActivationCommands.ts +5 src/renderer/src/components/tab-group/useTabGroupCreationCommands.ts +4 src/renderer/src/components/task-page-github-reviewer-actions.ts +2 src/renderer/src/components/task-page/TaskPage.tsx +2 src/renderer/src/components/task-page/github/AssigneesCell.tsx +2 src/renderer/src/components/task-page/github/MergeCell.tsx +2 src/renderer/src/components/task-page/github/ReviewCell.tsx +1 src/renderer/src/components/task-page/github/StatusCell.tsx +2 src/renderer/src/components/terminal-pane/pty-connection/pane-agent-identity.ts +1 src/renderer/src/components/terminal-pane/pty-connection/pane-serializer-settle.ts +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 +1 src/renderer/src/components/terminal-pane/use-terminal-pane-paste-listeners.ts +1 src/renderer/src/components/terminal-workspace-model.ts +1 src/renderer/src/components/terminal/pty-running-work-probe.ts +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 +3 src/renderer/src/components/use-task-page-github-issue-draft.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 +2 src/renderer/src/components/use-terminal-bulk-close-actions.ts +8 src/renderer/src/components/use-terminal-create-actions.ts +2 src/renderer/src/components/use-worktree-jump-palette-controller.ts +2 src/renderer/src/components/use-worktree-jump-palette-filter.ts +3 src/renderer/src/components/use-worktree-jump-palette-quick-actions.ts +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/composer-attachment-drop-state.ts +1 src/renderer/src/hooks/composer-state/composer-navigation-actions.ts +3 src/renderer/src/hooks/composer-state/composer-source-state.ts +1 src/renderer/src/hooks/composer-state/composer-submit-orchestration.ts +2 src/renderer/src/hooks/composer-state/composer-target-state.ts +1 src/renderer/src/hooks/composer-state/github-provider-selection.ts +2 src/renderer/src/hooks/composer-state/github-submit-resolution.ts +1 src/renderer/src/hooks/composer-state/gitlab-provider-selection.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/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 +2 src/renderer/src/hooks/ipc-events/app-lifetime-ipc-bridge.ts +1 src/renderer/src/hooks/ipc-events/browser-state-ipc-bridge.ts +1 src/renderer/src/hooks/ipc-events/runtime-environment-subscription-selection.ts +1 src/renderer/src/hooks/ipc-events/settings-sidebar-ipc-bridge.ts +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 +2 src/renderer/src/hooks/useGitHubRepoMetadata.ts +4 src/renderer/src/hooks/useGitHubSlugMetadata.ts +1 src/renderer/src/hooks/useInstalledAgentSkills.ts +1 src/renderer/src/hooks/useIpcEvents.ts +1 src/renderer/src/hooks/useIssueMetadata.ts +1 src/renderer/src/hooks/useLinearProviderConnected.ts +7 src/renderer/src/hooks/useSettingsNavigationMetadata.ts +1 src/renderer/src/hooks/useUnreadDockBadge.ts +2 src/renderer/src/hooks/useWindowsTerminalCapabilityOwnerKey.ts +10 src/renderer/src/lib/activate-ai-vault-structured-session.ts +1 src/renderer/src/lib/active-agent-note-send.ts +1 src/renderer/src/lib/active-agent-note-target.ts +1 src/renderer/src/lib/agent-background-session-launch-host.ts +1 src/renderer/src/lib/agent-draft-paste-content.ts +1 src/renderer/src/lib/agent-draft-readiness.ts +2 src/renderer/src/lib/agent-followup-delivery.ts +1 src/renderer/src/lib/agent-hibernation-coordinator-test-fixture.ts +3 src/renderer/src/lib/agent-hibernation-coordinator.ts +5 src/renderer/src/lib/agent-paste-draft.ts +1 src/renderer/src/lib/agent-ready-wait.ts +1 src/renderer/src/lib/automation-session-observer.ts +1 src/renderer/src/lib/client-creation-action-error.ts +3 src/renderer/src/lib/client-creation-action-policy.ts +3 src/renderer/src/lib/codex-pane-selection-lane.ts +2 src/renderer/src/lib/codex-session-restart.ts +1 src/renderer/src/lib/default-creation-host.ts # the sanctioned reader of the default host +1 src/renderer/src/lib/doc-preview-grants.ts +3 src/renderer/src/lib/ensure-hooks-confirmed.ts +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/github-source-runtime-context.ts +2 src/renderer/src/lib/github-work-item-source-lookup.ts +1 src/renderer/src/lib/host-mirrored-pane-liveness.ts +1 src/renderer/src/lib/hosted-review-repo-read.ts +6 src/renderer/src/lib/http-link-destinations.ts +3 src/renderer/src/lib/http-link-routing.ts +2 src/renderer/src/lib/launch-agent-background-session.ts +1 src/renderer/src/lib/launch-agent-execution-context.ts +2 src/renderer/src/lib/launch-agent-in-new-tab.ts +1 src/renderer/src/lib/launch-agent-web-host-tab.ts +2 src/renderer/src/lib/launch-ai-vault-session.ts +1 src/renderer/src/lib/launch-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/repo-slug-cache.ts +3 src/renderer/src/lib/repo-slug-index.ts +2 src/renderer/src/lib/resolve-owner.ts +1 src/renderer/src/lib/runtime-session-mirror-owners.ts +1 src/renderer/src/lib/runtime-session-mirror-targets.ts +1 src/renderer/src/lib/single-runtime-legacy-owner.ts +3 src/renderer/src/lib/structured-agent-session-tab-activation.ts +1 src/renderer/src/lib/tab-number-shortcuts.ts +1 src/renderer/src/lib/terminal-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 +3 src/renderer/src/lib/worktree-activation.ts +1 src/renderer/src/lib/worktree-creation-agent-seeds.ts +1 src/renderer/src/lib/worktree-creation-flow-execute.ts +1 src/renderer/src/lib/worktree-creation-flow-startup.ts +1 src/renderer/src/lib/worktree-creation-flow.ts +1 src/renderer/src/lib/worktree-creation-hook-preparation.ts +1 src/renderer/src/lib/worktree-operation-route.ts +1 src/renderer/src/lib/worktree-row-open-in-owner.ts +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 +6 src/renderer/src/runtime/runtime-git-status-client.ts +10 src/renderer/src/runtime/runtime-git-sync-client.ts +9 src/renderer/src/runtime/runtime-git-working-tree-client.ts +3 src/renderer/src/runtime/runtime-hooks-client.ts +17 src/renderer/src/runtime/runtime-jira-client.ts +2 src/renderer/src/runtime/runtime-jira-summary-client.ts +1 src/renderer/src/runtime/runtime-jira-target.ts +4 src/renderer/src/runtime/runtime-jira-user-fields-client.ts +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 +1 src/renderer/src/runtime/runtime-terminal-verified-input.ts +5 src/renderer/src/runtime/use-runtime-session-mirror-environment-key.ts +2 src/renderer/src/runtime/web-runtime-browser-creation.ts +1 src/renderer/src/runtime/web-runtime-initial-terminal-bootstrap-dispatch.ts +1 src/renderer/src/runtime/web-runtime-session-tab-lifecycle.ts +1 src/renderer/src/runtime/web-runtime-session-tab-move.ts +2 src/renderer/src/runtime/web-runtime-terminal-actions.ts +1 src/renderer/src/runtime/web-runtime-terminal-create-operation.ts +3 src/renderer/src/runtime/web-runtime-terminal-creation.ts +1 src/renderer/src/runtime/web-runtime-worktree-activation.ts +1 src/renderer/src/runtime/web-session-tabs-sync/active-session-subscription.ts +2 src/renderer/src/runtime/web-session-tabs-sync/global-session-subscription.ts +1 src/renderer/src/runtime/web-session-tabs-sync/tracking-decisions.ts +2 src/renderer/src/runtime/web-session-tabs-sync/use-web-session-tabs-sync.ts +2 src/renderer/src/store/folder-workspaces/folder-path-status-actions.ts +2 src/renderer/src/store/folder-workspaces/folder-workspace-catalog-actions.ts +5 src/renderer/src/store/folder-workspaces/folder-workspace-mutations.ts +1 src/renderer/src/store/folder-workspaces/folder-workspace-routing.ts +1 src/renderer/src/store/github/cache-identity.ts +3 src/renderer/src/store/github/project-actions.ts +2 src/renderer/src/store/github/project-field-mutations.ts +2 src/renderer/src/store/github/project-row-actions.ts +1 src/renderer/src/store/github/pull-request-execution.ts +2 src/renderer/src/store/github/refresh-routing-actions.ts +3 src/renderer/src/store/github/repository-routing.ts +2 src/renderer/src/store/github/visible-hosted-review-refresh-targets.ts +2 src/renderer/src/store/github/work-item-aggregate-actions.ts +3 src/renderer/src/store/github/work-item-fetch-actions.ts +1 src/renderer/src/store/github/work-item-mutation-actions.ts +3 src/renderer/src/store/github/work-item-routing.ts +9 src/renderer/src/store/index.ts +6 src/renderer/src/store/project-groups/nested-repository-operations.ts +2 src/renderer/src/store/project-groups/project-group-catalog-actions.ts +7 src/renderer/src/store/project-groups/project-group-mutations.ts +1 src/renderer/src/store/projects/project-host-routing.ts +1 src/renderer/src/store/projects/project-update.ts +2 src/renderer/src/store/repos/all-host-repo-catalog-actions.ts +1 src/renderer/src/store/repos/owner-routing.ts +2 src/renderer/src/store/repos/repo-add-actions.ts +2 src/renderer/src/store/repos/repo-catalog-actions.ts +2 src/renderer/src/store/repos/repo-removal.ts +2 src/renderer/src/store/repos/repo-update.ts +2 src/renderer/src/store/repos/runtime-repo-catalog-actions.ts +2 src/renderer/src/store/repos/safe-auto-fork-sync.ts +1 src/renderer/src/store/slices/browser-slice-test-harness.ts +14 src/renderer/src/store/slices/browser/browser-cookie-import-actions.ts +1 src/renderer/src/store/slices/browser/browser-host-actions.ts +7 src/renderer/src/store/slices/browser/browser-host-state.ts +1 src/renderer/src/store/slices/browser/browser-page-conversion-actions.ts +12 src/renderer/src/store/slices/browser/browser-profile-import-actions.ts +11 src/renderer/src/store/slices/browser/browser-profile-list-actions.ts +6 src/renderer/src/store/slices/browser/browser-tab-actions.ts +1 src/renderer/src/store/slices/browser/create-browser-slice.ts +1 src/renderer/src/store/slices/browser/paired-browser-tab-creator.ts +3 src/renderer/src/store/slices/diff-comment-persistence.ts +2 src/renderer/src/store/slices/editor-slice-test-harness.ts +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-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/github-cache-key.ts +2 src/renderer/src/store/slices/github-slice-test-harness.ts +1 src/renderer/src/store/slices/github.ts +1 src/renderer/src/store/slices/hosted-review-cache-identity.ts +4 src/renderer/src/store/slices/hosted-review.ts +1 src/renderer/src/store/slices/jira-collection-read-actions.ts +13 src/renderer/src/store/slices/jira-connection-actions.ts +4 src/renderer/src/store/slices/jira-issue-read-actions.ts +5 src/renderer/src/store/slices/jira-read-coordination.ts +3 src/renderer/src/store/slices/linear/linear-custom-view-detail-actions.ts +3 src/renderer/src/store/slices/linear/linear-custom-view-issue-actions.ts +3 src/renderer/src/store/slices/linear/linear-custom-view-list-actions.ts +3 src/renderer/src/store/slices/linear/linear-custom-view-project-actions.ts +3 src/renderer/src/store/slices/linear/linear-issue-detail-actions.ts +6 src/renderer/src/store/slices/linear/linear-issue-list-actions.ts +3 src/renderer/src/store/slices/linear/linear-project-detail-actions.ts +3 src/renderer/src/store/slices/linear/linear-project-issue-actions.ts +3 src/renderer/src/store/slices/linear/linear-project-list-actions.ts +13 src/renderer/src/store/slices/linear/linear-slice-connection-actions.ts +4 src/renderer/src/store/slices/linear/linear-slice-scope.ts +3 src/renderer/src/store/slices/linear/linear-slice-status-actions.ts +3 src/renderer/src/store/slices/linear/linear-team-actions.ts +2 src/renderer/src/store/slices/preflight.ts +1 src/renderer/src/store/slices/project-group-owner-routing.ts +1 src/renderer/src/store/slices/repo-host-identity.ts +1 src/renderer/src/store/slices/repo-reorder-host-split.ts +10 src/renderer/src/store/slices/repos.ts +1 src/renderer/src/store/slices/runtime-status.ts +1 src/renderer/src/store/slices/settings-owner-hydration-publication.ts +5 src/renderer/src/store/slices/settings.ts +8 src/renderer/src/store/slices/store-test-helpers.ts +2 src/renderer/src/store/slices/tabs/tabs-host-mirroring.ts +2 src/renderer/src/store/slices/terminal-tab-retirement.ts +1 src/renderer/src/store/slices/worktree-visibility-owner-settings.ts +3 src/renderer/src/store/slices/worktree-visibility-settings-write.ts +3 src/renderer/src/store/slices/worktrees-slice-test-harness.ts +13 src/renderer/src/store/slices/worktrees.ts +2 src/renderer/src/store/slices/worktrees/create/create-worktree.ts +2 src/renderer/src/store/slices/worktrees/create/prefetch-worktree-create-base.ts +2 src/renderer/src/store/slices/worktrees/listing/detected-worktree-provider-request.ts +6 src/renderer/src/store/slices/worktrees/listing/detected-worktree-refresh.ts +4 src/renderer/src/store/slices/worktrees/listing/fetch-all-worktrees.ts +2 src/renderer/src/store/slices/worktrees/listing/fetch-detected-worktrees.ts +5 src/renderer/src/store/slices/worktrees/listing/fetch-worktrees.ts +1 src/renderer/src/store/slices/worktrees/listing/known-ssh-worktree-fetch.ts +1 src/renderer/src/store/slices/worktrees/listing/worktree-host-ownership.ts +2 src/renderer/src/store/slices/worktrees/listing/worktree-owner-settings.ts +2 src/renderer/src/store/slices/worktrees/metadata/hosted-review-push-target-ensure.ts +4 src/renderer/src/store/slices/worktrees/metadata/hosted-review-push-target.ts +4 src/renderer/src/store/slices/worktrees/metadata/update-worktree-meta.ts +3 src/renderer/src/store/slices/worktrees/metadata/worktree-lineage-actions.ts +7 src/renderer/src/store/slices/worktrees/metadata/worktree-lineage-refresh.ts +1 src/renderer/src/store/slices/worktrees/metadata/worktree-meta-persist.ts +2 src/renderer/src/store/slices/worktrees/session/worktree-terminal-reference.ts +1 src/renderer/src/store/slices/worktrees/teardown/force-delete-preserved-branch.ts +1 src/renderer/src/store/slices/worktrees/teardown/missing-worktree-terminal-teardown.ts +2 src/renderer/src/store/slices/worktrees/teardown/remove-worktree.ts +2 src/renderer/src/store/terminals/terminal-active-workspace-creation.ts +1 src/renderer/src/store/terminals/terminal-pane-hibernation.ts +1 src/renderer/src/store/terminals/terminal-shutdown.ts +1 src/renderer/src/store/terminals/terminal-workspace-routing.ts +3 src/renderer/src/web/preload-api/web-preference-normalization.ts +16 src/renderer/src/web/preload-api/web-preferences-store.ts +6 src/renderer/src/web/preload-api/web-settings-api.ts +1 src/renderer/src/web/web-preload-api.ts diff --git a/config/scripts/check-owner-routing-ratchet.mjs b/config/scripts/check-owner-routing-ratchet.mjs index c3304373fa5..0c78a91b6c5 100644 --- a/config/scripts/check-owner-routing-ratchet.mjs +++ b/config/scripts/check-owner-routing-ratchet.mjs @@ -4,33 +4,351 @@ import path from 'node:path' import process from 'node:process' import { pathToFileURL } from 'node:url' -// Ratchet for renderer code that routes host-owned work by the "Active Server" focus setting -// instead of by the resource's own host. The three helpers are counted together so renaming one -// into another never lowers the count. Per-file counts may only go down. +// Two ratchets for renderer code that routes host-owned work by the "Active Server" setting +// instead of by the resource's own host. Per-file counts may only go down. +// - Owner routing: the three focus-routing helpers, counted together so renaming one into another +// never lowers the count. +// - Focus reads: every read of the setting plus every call of an exported function that reads it +// for the caller (discovered, not hand-listed), so swapping one form for another never lowers it. -const BASELINE_PATH = 'config/owner-routing-baseline.txt' const SCAN_ROOT = 'src/renderer/src' -const HELPERS = '(?:getActiveRuntimeTarget|legacyRouteFromSettings|settingsForRuntimeOwner)' +const HELPER_NAMES = 'getActiveRuntimeTarget|legacyRouteFromSettings|settingsForRuntimeOwner' +const HELPERS = `(?:${HELPER_NAMES})` const IMPORT_EXPORT_LIST = /\b(?:import|export)\s+(?:type\s+)?\{[^}]*\}/g // Calls and value uses (`.map(helper)`); definitions and type queries are not routing. const FOCUS_ROUTING_USE = new RegExp(`(? FOCUS_ROUTING_ALIAS.test(list)) +// Element reads after a PascalCase name or `>` are indexed types (`GlobalSettings['…']`), not reads. +const SETTING_MEMBER_READ = + /\??\.\s*activeRuntimeEnvironmentId\b|(?)\s*)\[\s*['"]activeRuntimeEnvironmentId['"]\s*\]/g +// `const { activeRuntimeEnvironmentId } = s`, `{ activeRuntimeEnvironmentId: id }: T = s` or nested +// `{ settings: { activeRuntimeEnvironmentId } } = state`; object literals and `({ … }) =>` +// parameters are not reads of the setting. +const SETTING_DESTRUCTURE_READ = + /\{[^{}]*\bactiveRuntimeEnvironmentId\b[^{}]*\}(?:\s*\})*\s*(?::[^=;{}]*)?=(?![=>])/g +// A top-level declaration: `[export] [async] function name` or `[export] const|let name =`. +const TOP_LEVEL_DECLARATION = + /^(export\s+)?(?:default\s+)?(?:async\s+)?(?:function\s*\*?\s*([\w$]+)|(?:const|let)\s+([\w$]+)\b)/gm +// Any new statement at column 0 ends a declaration; closing brackets and continuations do not. +const TOP_LEVEL_BOUNDARY = /^[^\s)}\]]/gm + +// Member calls only, so dotted string keys (`'auto.hooks.useX.abc'`) are not uses. +const MEMBER_NAME = /\.\s*([\w$]+)\s*(?:\?\.\s*)?\(/g + +function readerUsePattern(names) { + return names.size === 0 + ? null + : new RegExp( + `(? match.index) + return [...sourceText.matchAll(TOP_LEVEL_DECLARATION)].map((match) => { + const end = boundaries.find((index) => index > match.index) ?? sourceText.length + return { + name: match[2] ?? match[3], + exported: Boolean(match[1]), + text: sourceText.slice(match.index, end) + } + }) +} + +// Statements that hand a value on: `return …` or an arrow's expression body, each running to the +// first newline outside brackets. Object-literal results (`=> ({ … })`, `return { … }`) are +// skipped: slice creators and hooks return bags of actions, which pulls in the whole store. +const VALUE_STATEMENT_START = /\breturn\s+(?!\(?\s*\{)|=>\s*(?!\(?\s*\{)/g + +function valueStatements(body) { + const statements = [] + for (const match of body.matchAll(VALUE_STATEMENT_START)) { + let depth = 0 + let end = match.index + match[0].length + for (; end < body.length; end += 1) { + const char = body[end] + if ('([{'.includes(char)) { + depth += 1 + } else if (')]}'.includes(char)) { + if (depth === 0) { + break + } + depth -= 1 + } else if (char === '\n' && depth === 0) { + break + } + } + statements.push(body.slice(match.index, end)) + } + return statements.join('\n') +} + +const IMPORT_STATEMENT = + /\bimport\s+(?:type\s+)?(?:[\w$]+\s*,\s*)?\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g +const REEXPORT_STATEMENT = /\bexport\s+(?:type\s+)?\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g +// `const { reader } = (await import('…'))` and `import('…').then(({ reader }) =>` bind like a +// named import; `{ reader: local }` renames like `as`. +const DYNAMIC_IMPORT_DESTRUCTURE = + /\{([^{}]*)\}\s*=\s*\(?\s*await\s+import\(\s*['"]([^'"]+)['"]\s*\)(?:\s*\))?/g +const DYNAMIC_IMPORT_THEN = + /\bimport\(\s*['"]([^'"]+)['"]\s*\)\s*\.then\(\s*(?:async\s*)?\(\s*\{([^{}]*)\}/g +const STAR_REEXPORT_STATEMENT = /\bexport\s+\*\s+from\s*['"]([^'"]+)['"]/g +const LOCAL_EXPORT_LIST = /\bexport\s+\{([^}]*)\}(?!\s*from)/g + +function specifiers(list) { + return list + .split(',') + .map((part) => part.trim().replace(/^type\s+/, '')) + .filter(Boolean) + .map((part) => { + const [imported, local = imported] = part.split(/\s+as\s+|\s*:\s*/) + return { imported: imported.trim(), local: local.trim() } + }) +} + +function resolveModule(spec, fromRel, files) { + let base + if (spec.startsWith('@/')) { + base = `${SCAN_ROOT}/${spec.slice(2)}` + } else if (spec.startsWith('.')) { + base = path.posix.join(path.posix.dirname(fromRel), spec) + } else { + return null + } + base = base.replace(/\.(?:m?js|tsx?)$/, '') + return ( + ['.ts', '.tsx', '/index.ts', '/index.tsx'].map((ext) => base + ext).find((c) => files.has(c)) ?? + null + ) +} + +function parseModule(rel, text, files) { + const imports = new Map() + for (const [, list, spec] of [ + ...text.matchAll(IMPORT_STATEMENT), + ...text.matchAll(DYNAMIC_IMPORT_DESTRUCTURE), + ...[...text.matchAll(DYNAMIC_IMPORT_THEN)].map(([all, from, names]) => [all, names, from]) + ]) { + const from = resolveModule(spec, rel, files) + for (const { imported, local } of specifiers(list)) { + imports.set(local, { from, name: imported }) + } + } + const reexports = new Map() + for (const [, list, spec] of text.matchAll(REEXPORT_STATEMENT)) { + const from = resolveModule(spec, rel, files) + for (const { imported, local } of specifiers(list)) { + reexports.set(local, { from, name: imported }) + } + } + const starFrom = [...text.matchAll(STAR_REEXPORT_STATEMENT)].map(([, spec]) => + resolveModule(spec, rel, files) + ) + const listed = new Map() + for (const [, list] of text.matchAll(LOCAL_EXPORT_LIST)) { + for (const { imported, local } of specifiers(list)) { + listed.set(imported, local) + } + } + const declarations = topLevelDeclarations(text.replace(IMPORT_EXPORT_LIST, '')).map((decl) => ({ + ...decl, + exportedAs: decl.exported ? decl.name : (listed.get(decl.name) ?? null) + })) + return { imports, reexports, starFrom, declarations } +} + +/** + * Functions that read the setting for their caller, keyed by defining file so a same-named + * function elsewhere (a test harness's `useAppStore`) is not mistaken for one. Seeds, then every + * exported top-level function whose body reads the setting or calls a seed, then, to a fixpoint, + * every one that returns a reader's result. Hand lists miss look-alikes such as a copy of + * `getActiveRuntimeTarget` under another name; discovery does not. Following every call instead + * of returned values pulls in most of the renderer through the store. + */ +export function discoverFocusReaders(sources) { + const files = new Set(sources.keys()) + const modules = new Map([...sources].map(([rel, text]) => [rel, parseModule(rel, text, files)])) + const seeds = new Set(SEED_FOCUS_READERS) + const seedUse = readerUsePattern(seeds) + const readerKeys = new Set() + + const resolveExport = (rel, name, depth = 0) => { + const mod = rel ? modules.get(rel) : null + if (!mod || depth > 8) { + return null + } + if (mod.declarations.some((decl) => decl.exportedAs === name)) { + return `${rel}#${name}` + } + const re = mod.reexports.get(name) + if (re) { + return resolveExport(re.from, re.name, depth + 1) + } + for (const from of mod.starFrom) { + const key = resolveExport(from, name, depth + 1) + if (key) { + return key + } + } + return null + } + const resolveLocal = (rel, name) => { + const mod = modules.get(rel) + if (mod.declarations.some((decl) => decl.exportedAs && decl.name === name)) { + return `${rel}#${mod.declarations.find((decl) => decl.name === name).exportedAs}` + } + const imported = mod.imports.get(name) + return imported ? resolveExport(imported.from, imported.name) : null + } + const isReader = (rel, name) => seeds.has(name) || readerKeys.has(resolveLocal(rel, name)) + + const pending = [] + for (const [rel, mod] of modules) { + for (const { name, exportedAs, text } of mod.declarations) { + // Components render; a `` that reads focus inside is not a read by its parent. + if (!exportedAs || /^[A-Z]/.test(exportedAs)) { + continue + } + seedUse.lastIndex = 0 + if (countSettingReads(text) > 0 || seedUse.test(text)) { + readerKeys.add(`${rel}#${exportedAs}`) + continue + } + const statements = valueStatements(text) + const passed = new Set(statements.match(/[\w$]+/g)) + passed.delete(name) + const members = [...statements.matchAll(MEMBER_NAME)].map(([, member]) => member) + pending.push({ rel, key: `${rel}#${exportedAs}`, passed: [...passed], members }) + } + } + // Exported reader names, for `ns.reader(…)` through a namespace or dynamic import. + const memberNames = new Set(seeds) + const addReader = (key) => { + readerKeys.add(key) + memberNames.add(key.slice(key.indexOf('#') + 1)) + } + for (const key of readerKeys) { + addReader(key) + } + let changed = true + while (changed) { + changed = false + for (let i = pending.length - 1; i >= 0; i -= 1) { + const { rel, key, passed, members } = pending[i] + if ( + passed.some((id) => isReader(rel, id)) || + members.some((member) => memberNames.has(member)) + ) { + addReader(key) + pending.splice(i, 1) + changed = true + } + } + } + + const namesByFile = new Map() + return { + has: (name) => memberNames.has(name), + memberNames, + /** Names that refer to a reader inside `rel`: seeds, its own readers and imported ones. */ + namesFor(rel) { + if (!namesByFile.has(rel)) { + const mod = modules.get(rel) + const local = [ + ...(mod?.declarations.map((decl) => decl.name) ?? []), + ...(mod?.imports.keys() ?? []) + ] + namesByFile.set( + rel, + new Set([...seeds, ...local.filter((name) => mod && isReader(rel, name))]) + ) + } + return namesByFile.get(rel) + } + } +} + +/** + * Reads of the setting (member, element and destructuring reads) plus every use of a name that + * refers to a reader. Object-literal keys are writes and are not counted. + */ +export function countFocusSettingReads( + sourceText, + readerNames = new Set(SEED_FOCUS_READERS), + memberNames = new Set() +) { + const body = sourceText + .replace(IMPORT_EXPORT_LIST, '') + // A placeholder, so a dangling `const` does not read as a declaration of the next call. + .replace(DYNAMIC_IMPORT_DESTRUCTURE, 'dynamicImport') + .replace(DYNAMIC_IMPORT_THEN, 'dynamicImport') + const pattern = readerUsePattern(readerNames) + // `ns.reader(…)` through a namespace or dynamic import; names already counted bare are skipped. + const members = [...body.matchAll(MEMBER_NAME)].filter( + ([, member]) => memberNames.has(member) && !readerNames.has(member) + ).length + return countSettingReads(body) + (pattern ? (body.match(pattern)?.length ?? 0) : 0) + members +} + +/** An alias hides later calls from the name-based helper count, so it is refused outright. */ +export function hasFocusRoutingAlias(sourceText) { + const alias = new RegExp(`\\b(?:${SEED_FOCUS_READERS.join('|')})\\s+as\\b`) + return (sourceText.match(IMPORT_EXPORT_LIST) ?? []).some((list) => alias.test(list)) +} + +export const RATCHETS = [ + { + name: 'owner-routing', + baselinePath: 'config/owner-routing-baseline.txt', + count: (text) => countFocusRoutingCalls(text), + header: [ + '# Renderer call sites that route by the Active Server focus setting:', + '# 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' + ], + advice: + "Route by the resource's owner (resolveOwner + callHostRoute) instead of the Active Server setting." + }, + { + name: 'focus-setting-read', + baselinePath: 'config/focus-setting-read-baseline.txt', + count: (text, rel, readers) => + countFocusSettingReads(text, readers.namesFor(rel), readers.memberNames), + header: [ + '# Renderer reads of the Active Server setting (member, element and destructuring reads of', + '# activeRuntimeEnvironmentId) plus calls of exported functions that read it, per file.', + '# This is a RATCHET: counts may only go DOWN. Only creation flows with no source row may read', + '# the default host, through defaultCreationHost. Everything else routes by the owner.', + '# Prune after removing reads: pnpm check:owner-routing-ratchet --prune' + ], + advice: + "Route by the resource's owner. Only a creation flow with no source row may use defaultCreationHost." + } +] + export function isScannedPath(rel) { return /\.(ts|tsx)$/.test(rel) && !/\.(test|spec)\.tsx?$/.test(rel) } -/** ` ` lines; `#` comments and blanks ignored. */ +/** ` [# note]` lines; `#` comment lines and blanks ignored. */ export function parseBaseline(text) { const counts = new Map() for (const raw of text.split('\n')) { @@ -44,19 +362,26 @@ export function parseBaseline(text) { return counts } -export function formatBaseline(counts) { - const header = [ - '# Renderer call sites that route by the Active Server focus setting:', - '# 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', - '' - ].join('\n') +/** Trailing `# note` per row, so a pruned baseline keeps why an entry is still allowed. */ +export function parseBaselineNotes(text) { + const notes = new Map() + for (const raw of text.split('\n')) { + const match = /^\s*\d+\s+(\S+)\s+#\s*(.+?)\s*$/.exec(raw) + if (match) { + notes.set(match[1], match[2]) + } + } + return notes +} + +export function formatBaseline(counts, header = RATCHETS[0].header, notes = new Map()) { const rows = [...counts] .filter(([, count]) => count > 0) .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) - .map(([file, count]) => `${count} ${file}`) - return `${header}${rows.join('\n')}\n` + .map(([file, count]) => + notes.has(file) ? `${count} ${file} # ${notes.get(file)}` : `${count} ${file}` + ) + return `${[...header, ''].join('\n')}${rows.join('\n')}${rows.length > 0 ? '\n' : ''}` } /** `grown`: above baseline (fails). `shrunk`: below baseline, must be pruned so it cannot regrow. */ @@ -76,82 +401,120 @@ export function diffCounts(current, baseline) { return { grown: grown.sort(byFile), shrunk: shrunk.sort(byFile) } } -export function collectCurrentCounts(root = process.cwd()) { - const tracked = execFileSync('git', ['ls-files', SCAN_ROOT], { +function readTrackedSources(root, scanRoot) { + const tracked = execFileSync('git', ['ls-files', scanRoot], { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }) .split('\n') .filter((rel) => rel && isScannedPath(rel)) + const sources = new Map() + for (const rel of tracked) { + try { + sources.set(rel, fs.readFileSync(path.join(root, rel), 'utf8')) + } catch { + // Deleted in the working tree but still tracked. + } + } + return sources +} + +// One read and one discovery per root per run; `--prune` then `main` reuse them. +const scanCache = new Map() + +function scanSources(root) { + if (!scanCache.has(root)) { + const sources = new Map( + FOCUS_SCAN_ROOTS.flatMap((scanRoot) => [...readTrackedSources(root, scanRoot)]) + ) + scanCache.set(root, { sources, readers: discoverFocusReaders(sources) }) + } + return scanCache.get(root) +} + +export function collectCurrentCounts(root = process.cwd(), count = RATCHETS[0].count) { + const { sources, readers } = scanSources(root) const counts = new Map() const aliased = [] - for (const rel of tracked) { - let source - try { - source = fs.readFileSync(path.join(root, rel), 'utf8') - } catch { + for (const [rel, source] of sources) { + if (!rel.startsWith(`${SCAN_ROOT}/`)) { continue } - const count = countFocusRoutingCalls(source) - if (count > 0) { - counts.set(rel, count) + const found = count(source, rel, readers) + if (found > 0) { + counts.set(rel, found) } if (hasFocusRoutingAlias(source)) { aliased.push(rel) } } - return { counts, aliased } + return { counts, aliased, readers } } function total(counts) { return [...counts.values()].reduce((sum, count) => sum + count, 0) } -export function main(root = process.cwd()) { - const baselineFile = path.join(root, BASELINE_PATH) +function checkRatchet(root, ratchet) { + const baselineFile = path.join(root, ratchet.baselinePath) if (!fs.existsSync(baselineFile)) { - console.error(`::error::Missing ${BASELINE_PATH}.`) - return 1 + console.error(`::error::Missing ${ratchet.baselinePath}.`) + return { ok: false, total: 0 } } const baseline = parseBaseline(fs.readFileSync(baselineFile, 'utf8')) - const { counts: current, aliased } = collectCurrentCounts(root) + const { counts: current } = collectCurrentCounts(root, ratchet.count) const { grown, shrunk } = diffCounts(current, baseline) + for (const { file, now, allowed } of grown) { + console.error( + `::error file=${file}::${ratchet.name}: ${now} use(s), baseline allows ${allowed}. ${ratchet.advice}` + ) + } + for (const { file, now, allowed } of shrunk) { + console.error( + `::error file=${file}::${ratchet.name}: ${now} use(s), baseline still allows ${allowed}. Run: pnpm check:owner-routing-ratchet --prune` + ) + } + return { ok: grown.length === 0 && shrunk.length === 0, total: total(current) } +} + +export function main(root = process.cwd()) { + const { aliased } = collectCurrentCounts(root) for (const file of aliased) { console.error( `::error file=${file}::A focus-routing helper is imported or exported under another name, which hides its calls from this ratchet. Use the original name.` ) } - for (const { file, now, allowed } of grown) { - console.error( - `::error file=${file}::${now} focus-routed call(s), baseline allows ${allowed}. Route by the resource's owner (resolveOwner + callHostRoute) instead of the Active Server setting.` + let ok = aliased.length === 0 + for (const ratchet of RATCHETS) { + const result = checkRatchet(root, ratchet) + ok = ok && result.ok + if (result.ok) { + console.log(`${ratchet.name} ratchet OK — ${result.total} use(s).`) + } + } + return ok ? 0 : 1 +} + +export function prune(root = process.cwd()) { + for (const ratchet of RATCHETS) { + const baselineFile = path.join(root, ratchet.baselinePath) + const text = fs.existsSync(baselineFile) ? fs.readFileSync(baselineFile, 'utf8') : '' + const baseline = parseBaseline(text) + const { counts: current } = collectCurrentCounts(root, ratchet.count) + // Lowers entries only; growth still has to be fixed in the code. + const pruned = new Map( + [...baseline].map(([file, allowed]) => [file, Math.min(allowed, current.get(file) ?? 0)]) ) + fs.writeFileSync(baselineFile, formatBaseline(pruned, ratchet.header, parseBaselineNotes(text))) + console.log(`Pruned ${ratchet.baselinePath} to ${total(pruned)} use(s).`) } - for (const { file, now, allowed } of shrunk) { - console.error( - `::error file=${file}::${now} focus-routed call(s), baseline still allows ${allowed}. Run: pnpm check:owner-routing-ratchet --prune` - ) - } - if (aliased.length > 0 || grown.length > 0 || shrunk.length > 0) { - return 1 - } - console.log(`Owner-routing ratchet OK — ${total(current)} focus-routed call site(s).`) - return 0 } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { const root = process.cwd() if (process.argv[2] === '--prune') { - const baselineFile = path.join(root, BASELINE_PATH) - const baseline = parseBaseline(fs.readFileSync(baselineFile, 'utf8')) - const { counts: current } = collectCurrentCounts(root) - // Lowers entries only; growth still has to be fixed in the code. - const pruned = new Map( - [...baseline].map(([file, allowed]) => [file, Math.min(allowed, current.get(file) ?? 0)]) - ) - fs.writeFileSync(baselineFile, formatBaseline(pruned)) - console.log(`Pruned ${BASELINE_PATH} to ${total(pruned)} call site(s).`) - process.exit(main(root)) + prune(root) } process.exit(main(root)) } diff --git a/config/scripts/check-owner-routing-ratchet.test.mjs b/config/scripts/check-owner-routing-ratchet.test.mjs index c33b9304121..89dc6e2b5c2 100644 --- a/config/scripts/check-owner-routing-ratchet.test.mjs +++ b/config/scripts/check-owner-routing-ratchet.test.mjs @@ -2,11 +2,14 @@ import { describe, expect, it } from 'vitest' import { countFocusRoutingCalls, + countFocusSettingReads, diffCounts, + discoverFocusReaders, formatBaseline, hasFocusRoutingAlias, isScannedPath, - parseBaseline + parseBaseline, + parseBaselineNotes } from './check-owner-routing-ratchet.mjs' describe('countFocusRoutingCalls', () => { @@ -39,6 +42,172 @@ describe('countFocusRoutingCalls', () => { }) }) +describe('countFocusSettingReads', () => { + it('counts setting reads, the helpers and the creation default together', () => { + const src = [ + "import { defaultCreationHost } from './default-creation-host'", + 'const a = settings?.activeRuntimeEnvironmentId', + 'const b = state.settings.activeRuntimeEnvironmentId', + "const c = settings['activeRuntimeEnvironmentId']", + 'const d = getActiveRuntimeTarget(settings)', + 'const e = defaultCreationHost(settings)', + 'export function defaultCreationHost(settings) {}' + ].join('\n') + expect(countFocusSettingReads(src)).toBe(5) + }) + + it('discovers readers through imports, so a look-alike or a wrapper lowers nothing', () => { + const readers = discoverFocusReaders( + new Map([ + [ + 'src/shared/execution-host.ts', + 'export function getSettingsFocusedExecutionHostId(settings) {\n return settings?.activeRuntimeEnvironmentId\n}\n' + ], + [ + 'src/renderer/src/a.ts', + [ + "import { getSettingsFocusedExecutionHostId } from '../../shared/execution-host'", + 'export function getAutomationListTarget(settings) {', + ' const id = settings?.activeRuntimeEnvironmentId?.trim()', + " return id ? { kind: 'environment', environmentId: id } : { kind: 'local' }", + '}', + 'export const cacheKey = (s) => `k:${getActiveRuntimeTarget(s).kind}`', + 'export function browserHostId(state, override) {', + ' return override ?? getSettingsFocusedExecutionHostId(state.settings)', + '}', + 'export function wrappedTwice(state) {', + ' return browserHostId(state, null)', + '}', + 'export function ownerTarget(row) {', + ' return row.owner', + '}', + 'export function SettingsPane() {', + ' return getAutomationListTarget(null)', + '}' + ].join('\n') + ], + [ + 'src/renderer/src/harness.ts', + 'export const ownerTarget = (s) => s.activeRuntimeEnvironmentId' + ], + [ + 'src/renderer/src/b.ts', + [ + "import { browserHostId, wrappedTwice, ownerTarget, SettingsPane } from './a'", + 'const a = wrappedTwice(state)', + 'const b = ownerTarget(row)', + 'const c = ' + ].join('\n') + ] + ]) + ) + const inA = readers.namesFor('src/renderer/src/a.ts') + for (const name of ['getAutomationListTarget', 'cacheKey', 'browserHostId', 'wrappedTwice']) { + expect(inA.has(name)).toBe(true) + } + expect(inA.has('ownerTarget')).toBe(false) + expect(inA.has('SettingsPane')).toBe(false) + // `ownerTarget` from `./a` is not the harness's same-named reader. + expect( + [...readers.namesFor('src/renderer/src/b.ts')].filter((n) => !n.startsWith('get')) + ).toEqual(expect.arrayContaining(['browserHostId', 'wrappedTwice'])) + expect(readers.namesFor('src/renderer/src/b.ts').has('ownerTarget')).toBe(false) + expect( + countFocusSettingReads( + 'const t = runtimeTargetForOwnerHostId(getSettingsFocusedExecutionHostId(s))', + readers.namesFor('src/renderer/src/a.ts') + ) + ).toBe(1) + expect( + countFocusSettingReads( + "import { wrappedTwice } from './a'\nconst t = wrappedTwice(state)", + readers.namesFor('src/renderer/src/b.ts') + ) + ).toBe(1) + expect(countFocusSettingReads('export const cacheKey = (s) => s', inA)).toBe(0) + }) + + it('counts a reader called through a namespace or dynamic import', () => { + const readers = discoverFocusReaders( + new Map([ + [ + 'src/renderer/src/routing.ts', + 'export function focusedHostId(s) {\n return s.activeRuntimeEnvironmentId\n}\n' + ], + [ + 'src/renderer/src/wrap.ts', + "import * as rr from './routing'\nexport function hostOf(s) {\n return rr.focusedHostId(s)\n}\n" + ], + [ + 'src/renderer/src/use.ts', + "const { focusedHostId } = await import('./routing')\nconst a = focusedHostId(s)\n" + ] + ]) + ) + expect(readers.has('hostOf')).toBe(true) + expect( + countFocusSettingReads( + "import * as rr from './routing'\nconst a = rr.focusedHostId(s)\nt('auto.rr.focusedHostId.x')", + readers.namesFor('src/renderer/src/other.ts'), + readers.memberNames + ) + ).toBe(1) + expect( + countFocusSettingReads( + "const { focusedHostId } = await import('./routing')\nconst a = focusedHostId(s)\n", + readers.namesFor('src/renderer/src/use.ts'), + readers.memberNames + ) + ).toBe(1) + for (const [rel, text] of [ + ['src/renderer/src/r1.ts', "const { focusedHostId: g } = await import('./routing')\ng(s)"], + [ + 'src/renderer/src/r2.ts', + "const { focusedHostId } = (await import('./routing'))\nfocusedHostId(s)" + ], + [ + 'src/renderer/src/r3.ts', + "void import('./routing').then(({ focusedHostId }) => focusedHostId(s))" + ] + ]) { + const withFile = discoverFocusReaders( + new Map([ + [ + 'src/renderer/src/routing.ts', + 'export function focusedHostId(s) {\n return s.activeRuntimeEnvironmentId\n}\n' + ], + [rel, text] + ]) + ) + expect(countFocusSettingReads(text, withFile.namesFor(rel), withFile.memberNames)).toBe(1) + } + }) + + it('counts destructuring reads', () => { + expect(countFocusSettingReads('const { activeRuntimeEnvironmentId } = s')).toBe(1) + expect( + countFocusSettingReads('const { settings: { activeRuntimeEnvironmentId } } = state') + ).toBe(1) + expect( + countFocusSettingReads( + "const { theme, activeRuntimeEnvironmentId: id }: Pick = s" + ) + ).toBe(1) + }) + + it('does not count writes, keys, props or type positions', () => { + const src = [ + 'const owner = { activeRuntimeEnvironmentId: id }', + 'settings = { ...settings, activeRuntimeEnvironmentId: null }', + "type T = GlobalSettings['activeRuntimeEnvironmentId']", + 'updateSettings({ activeRuntimeEnvironmentId: null })', + 'const f = ({ activeRuntimeEnvironmentId }) => activeRuntimeEnvironmentId', + 'if (a === b) { x = { activeRuntimeEnvironmentId } }' + ].join('\n') + expect(countFocusSettingReads(src)).toBe(0) + }) +}) + describe('hasFocusRoutingAlias', () => { it('refuses an aliased import or re-export, which would hide its calls', () => { expect(hasFocusRoutingAlias("import { getActiveRuntimeTarget as route } from './rpc'")).toBe( @@ -47,6 +216,7 @@ describe('hasFocusRoutingAlias', () => { expect( hasFocusRoutingAlias("export {\n settingsForRuntimeOwner as owner\n} from './target'") ).toBe(true) + expect(hasFocusRoutingAlias("import { defaultCreationHost as host } from './d'")).toBe(true) expect(hasFocusRoutingAlias("import { getActiveRuntimeTarget } from './rpc'")).toBe(false) }) }) @@ -75,6 +245,17 @@ describe('baseline', () => { ) }) + it('keeps a row note through a prune', () => { + const text = formatBaseline( + new Map([['src/a.ts', 1]]), + ['# header'], + new Map([['src/a.ts', 'waits for V4b']]) + ) + expect(text).toBe('# header\n1 src/a.ts # waits for V4b\n') + expect(parseBaseline(text)).toEqual(new Map([['src/a.ts', 1]])) + expect(parseBaselineNotes(text)).toEqual(new Map([['src/a.ts', 'waits for V4b']])) + }) + it('fails growth, including a new file, and asks to prune shrinkage', () => { const { grown, shrunk } = diffCounts( new Map([ diff --git a/src/renderer/src/lib/default-creation-host.test.ts b/src/renderer/src/lib/default-creation-host.test.ts new file mode 100644 index 00000000000..05ee3573905 --- /dev/null +++ b/src/renderer/src/lib/default-creation-host.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest' +import { defaultCreationHost } from './default-creation-host' + +describe('defaultCreationHost', () => { + it('is this computer unless a server is chosen', () => { + expect(defaultCreationHost(null)).toEqual({ kind: 'local' }) + expect(defaultCreationHost({ activeRuntimeEnvironmentId: null })).toEqual({ kind: 'local' }) + expect(defaultCreationHost({ activeRuntimeEnvironmentId: ' env-a ' })).toEqual({ + kind: 'environment', + environmentId: 'env-a' + }) + }) +}) diff --git a/src/renderer/src/lib/default-creation-host.ts b/src/renderer/src/lib/default-creation-host.ts new file mode 100644 index 00000000000..9fe9e06cff8 --- /dev/null +++ b/src/renderer/src/lib/default-creation-host.ts @@ -0,0 +1,13 @@ +import type { GlobalSettings } from '../../../shared/global-settings-types' +import type { RuntimeClientTarget } from '@/runtime/runtime-client-target' + +/** + * The "Default host for new projects" setting. Only a creation flow with no source row reads it; + * everything else routes by the resource's owner. + */ +export function defaultCreationHost( + settings: Pick | null | undefined +): RuntimeClientTarget { + const environmentId = settings?.activeRuntimeEnvironmentId?.trim() + return environmentId ? { kind: 'environment', environmentId } : { kind: 'local' } +} diff --git a/src/renderer/src/lib/resolve-owner.test.ts b/src/renderer/src/lib/resolve-owner.test.ts index dab7faced3d..2c20288b1b4 100644 --- a/src/renderer/src/lib/resolve-owner.test.ts +++ b/src/renderer/src/lib/resolve-owner.test.ts @@ -7,6 +7,7 @@ import { resetLegacyFocusFallbackCountForTest, resolveOwner, resolveOwnerWithLegacyFocus, + runtimeTargetForWorkspaceOwner, type WorkspaceOwnerRef } from './resolve-owner' import type { @@ -241,3 +242,33 @@ describe('resolveOwner characterization', () => { }) }) }) + +describe('runtimeTargetForWorkspaceOwner', () => { + const settings = { activeRuntimeEnvironmentId: 'env-other' } + it('dials the owner, not the focused server', () => { + const rows: WorktreeOperationRouteState = { + settings, + repos: [{ id: 'repo-1', executionHostId: 'runtime:env-a' }], + worktreesByRepo: { 'repo-1': [worktree('runtime:env-a', 'env-a')] } + } + expect(runtimeTargetForWorkspaceOwner(rows, { workspaceId: WORKTREE_ID })).toEqual({ + kind: 'environment', + environmentId: 'env-a' + }) + }) + + it('rides this app for local and direct SSH owners', () => { + const sshRows: WorktreeOperationRouteState = { + settings, + repos: [{ id: 'repo-1', connectionId: 'box' }], + worktreesByRepo: { 'repo-1': [worktree('ssh:box')] } + } + expect(runtimeTargetForWorkspaceOwner(sshRows, { workspaceId: WORKTREE_ID })).toEqual({ + kind: 'local' + }) + }) + + it('has no transport when no row names the workspace', () => { + expect(runtimeTargetForWorkspaceOwner({ settings }, { workspaceId: WORKTREE_ID })).toBeNull() + }) +}) diff --git a/src/renderer/src/lib/resolve-owner.ts b/src/renderer/src/lib/resolve-owner.ts index 6462f79ac89..c2753f50980 100644 --- a/src/renderer/src/lib/resolve-owner.ts +++ b/src/renderer/src/lib/resolve-owner.ts @@ -26,6 +26,7 @@ import { } from './worktree-operation-route' import { getNestedSshTargetIdForFolderWorkspace } from './folder-workspace-runtime-owner' import { parseWorkspaceKey } from '../../../shared/workspace-scope' +import { hostRouteForAuthority, type RuntimeClientTarget } from '@/runtime/runtime-client-target' /** A worktree id, a `folder:` workspace key, or the floating workspace; `hostId` when the row names one. */ export type WorkspaceOwnerRef = { workspaceId: string; hostId?: ExecutionHostId } @@ -105,6 +106,15 @@ export function resolveOwner( return resolveRoute({ ...state, settings: null }, ref) } +/** Transport to the workspace's owner, or `null` when the rows name none or disagree. */ +export function runtimeTargetForWorkspaceOwner( + state: WorktreeOperationRouteState, + ref: WorkspaceOwnerRef +): RuntimeClientTarget | null { + const match = resolveOwner(state, ref) + return match.kind === 'resolved' ? hostRouteForAuthority(match.owner).target : null +} + let legacyFocusFallbackCount = 0 /** How many answers focus has changed since start; a slice drops its fallback once this stays 0. */ diff --git a/src/renderer/src/runtime/runtime-client-target.test.ts b/src/renderer/src/runtime/runtime-client-target.test.ts index 7519bd898b3..93101dbdeac 100644 --- a/src/renderer/src/runtime/runtime-client-target.test.ts +++ b/src/renderer/src/runtime/runtime-client-target.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it } from 'vitest' import { UNRESOLVED_OWNER_HOST_ID } from '../../../shared/execution-host' -import { hostRouteForAuthority, runtimeTargetForExecutionHostId } from './runtime-client-target' +import { + hostRouteForAuthority, + runtimeTargetForExecutionHostId, + runtimeTargetForOwnerEnvironment, + runtimeTargetForOwnerHostId +} from './runtime-client-target' describe('runtimeTargetForExecutionHostId', () => { it('keeps the answers for plain host ids', () => { @@ -35,3 +40,25 @@ describe('hostRouteForAuthority', () => { }) }) }) + +describe('owner transports', () => { + it('maps an owner environment id, with null as this app', () => { + expect(runtimeTargetForOwnerEnvironment(null)).toEqual({ kind: 'local' }) + expect(runtimeTargetForOwnerEnvironment(' ')).toEqual({ kind: 'local' }) + expect(runtimeTargetForOwnerEnvironment(' env-a ')).toEqual({ + kind: 'environment', + environmentId: 'env-a' + }) + }) + + it('routes local and direct SSH owners through this app and never dials the sentinel', () => { + expect(runtimeTargetForOwnerHostId('local')).toEqual({ kind: 'local' }) + expect(runtimeTargetForOwnerHostId('ssh:box')).toEqual({ kind: 'local' }) + expect(runtimeTargetForOwnerHostId('runtime:env%2Fa')).toEqual({ + kind: 'environment', + environmentId: 'env/a' + }) + expect(runtimeTargetForOwnerHostId(UNRESOLVED_OWNER_HOST_ID)).toBeNull() + expect(runtimeTargetForOwnerHostId(undefined)).toBeNull() + }) +}) diff --git a/src/renderer/src/runtime/runtime-client-target.ts b/src/renderer/src/runtime/runtime-client-target.ts index 47bc13c150c..e17f811f10d 100644 --- a/src/renderer/src/runtime/runtime-client-target.ts +++ b/src/renderer/src/runtime/runtime-client-target.ts @@ -1,4 +1,5 @@ import type { GlobalSettings } from '../../../shared/global-settings-types' +import { parseRoutableExecutionHostId, type ExecutionHostId } from '../../../shared/execution-host' import { parseHostAuthorityKey, type HostAuthority, @@ -14,6 +15,30 @@ export function getActiveRuntimeTarget( return environmentId ? { kind: 'environment', environmentId } : { kind: 'local' } } +/** Transport to an owner's runtime environment; `null` is this app (local or direct SSH). */ +export function runtimeTargetForOwnerEnvironment( + environmentId: string | null +): RuntimeClientTarget { + const ownerId = environmentId?.trim() + return ownerId ? { kind: 'environment', environmentId: ownerId } : { kind: 'local' } +} + +/** + * Transport to the host that owns a row: a server's own runtime, or this app for local and direct + * SSH (which ride this app's IPC with a `connectionId`). `null` when the id names no routable host. + */ +export function runtimeTargetForOwnerHostId( + hostId: ExecutionHostId | null | undefined +): RuntimeClientTarget | null { + const parsed = parseRoutableExecutionHostId(hostId) + if (!parsed) { + return null + } + return parsed.kind === 'runtime' + ? { kind: 'environment', environmentId: parsed.environmentId } + : { kind: 'local' } +} + /** * RPC target for a dispatchable host; direct SSH and the unresolved-owner sentinel have none. A * nested authority key routes to the server that owns the SSH target.