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.