mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
refactor(worktrees): worktree create, listing, removal and repo hook/ref reads route by their owner (#27298)
* refactor(worktrees): worktree create, listing, removal and repo hook/ref reads route by their owner Worktree create, prefetch, detected listings and missing-terminal teardown, preserved-branch cleanup and removal follow the repo or workspace owner. Repo hook and base-ref reads take the repo's host id instead of settings. Composer, settings panes and the setup guide pass the selected repo's host. * test(hooks): build the duplicate-row state without a cast --------- Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
@@ -17,7 +17,6 @@
|
||||
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
|
||||
@@ -95,11 +94,10 @@
|
||||
1 src/renderer/src/components/new-workspace/use-jira-source-connection.ts
|
||||
4 src/renderer/src/components/new-workspace/use-linear-source-connection.ts
|
||||
1 src/renderer/src/components/new-workspace/use-smart-workspace-name-field-controller.ts
|
||||
2 src/renderer/src/components/new-workspace/use-smart-workspace-secondary-searches.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/quick-open-file-list-test-harness.ts
|
||||
3 src/renderer/src/components/repo/CreateFromPicker.tsx
|
||||
1 src/renderer/src/components/right-sidebar/ChecksPanel.tsx
|
||||
2 src/renderer/src/components/right-sidebar/FileExplorer.tsx
|
||||
1 src/renderer/src/components/right-sidebar/FileExplorerFilesTreePane.tsx
|
||||
@@ -114,23 +112,19 @@
|
||||
1 src/renderer/src/components/right-sidebar/git-status-file-watch-refresh.ts
|
||||
1 src/renderer/src/components/right-sidebar/source-control/listing/entry-context-menu.tsx
|
||||
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
|
||||
2 src/renderer/src/components/right-sidebar/use-create-pull-request-base-ref-discovery.ts
|
||||
1 src/renderer/src/components/right-sidebar/use-create-pull-request-base-ref-discovery.ts
|
||||
2 src/renderer/src/components/right-sidebar/use-file-explorer-tree-pane-state.ts
|
||||
1 src/renderer/src/components/right-sidebar/useCreatePullRequestDialogFields.ts
|
||||
4 src/renderer/src/components/right-sidebar/useFileSearchPanel.ts
|
||||
2 src/renderer/src/components/right-sidebar/useFileSearchRunner.ts
|
||||
2 src/renderer/src/components/right-sidebar/useFileSearchScope.ts
|
||||
2 src/renderer/src/components/right-sidebar/useGitStatusPolling.ts
|
||||
3 src/renderer/src/components/settings/BaseRefPicker.tsx
|
||||
1 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/GeneralPane.tsx
|
||||
1 src/renderer/src/components/settings/IntegrationsPane.tsx
|
||||
3 src/renderer/src/components/settings/LinearAgentSkillPane.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
|
||||
2 src/renderer/src/components/settings/Settings.tsx
|
||||
1 src/renderer/src/components/settings/SettingsSetupGuidePane.tsx
|
||||
@@ -150,7 +144,6 @@
|
||||
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
|
||||
@@ -158,8 +151,7 @@
|
||||
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
|
||||
2 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
|
||||
@@ -290,10 +282,7 @@
|
||||
1 src/renderer/src/hooks/composer-state/composer-attachment-drop-state.ts
|
||||
1 src/renderer/src/hooks/composer-state/composer-navigation-actions.ts
|
||||
2 src/renderer/src/hooks/composer-state/composer-source-state.ts
|
||||
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/provider-runtime-sync.ts
|
||||
1 src/renderer/src/hooks/composer-state/quick-creation-execution.ts
|
||||
1 src/renderer/src/hooks/composer-state/composer-target-state.ts
|
||||
1 src/renderer/src/hooks/composer-state/workspace-identity-state.ts
|
||||
1 src/renderer/src/hooks/editor-external-watch-target-rows.ts
|
||||
3 src/renderer/src/hooks/editor-external-watch-targets.ts
|
||||
@@ -336,7 +325,6 @@
|
||||
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
|
||||
1 src/renderer/src/lib/empty-workspace-default-agent-chat.ts
|
||||
3 src/renderer/src/lib/ensure-hooks-confirmed.ts
|
||||
3 src/renderer/src/lib/execute-open-editor-path-move.ts
|
||||
1 src/renderer/src/lib/execution-host-display-label.ts
|
||||
2 src/renderer/src/lib/execution-host-facts.ts
|
||||
@@ -351,7 +339,6 @@
|
||||
2 src/renderer/src/lib/launch-agent-web-host-tab.ts
|
||||
2 src/renderer/src/lib/launch-ai-vault-session.ts
|
||||
1 src/renderer/src/lib/launch-structured-agent-session.ts
|
||||
1 src/renderer/src/lib/launch-work-item-direct.ts
|
||||
1 src/renderer/src/lib/launch-worktree-background-terminals.ts
|
||||
1 src/renderer/src/lib/linear-issue-url-lookup.ts
|
||||
2 src/renderer/src/lib/local-preflight-context.ts
|
||||
@@ -386,10 +373,6 @@
|
||||
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-initial-terminal-seeding.ts
|
||||
1 src/renderer/src/lib/worktree-jump-navigation.ts
|
||||
2 src/renderer/src/lib/worktree-operation-route.ts
|
||||
@@ -399,7 +382,6 @@
|
||||
1 src/renderer/src/runtime/close-mirrored-editor-tab.ts
|
||||
1 src/renderer/src/runtime/local-structured-session-tabs-sync/snapshot-apply.ts
|
||||
1 src/renderer/src/runtime/runtime-client-target.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
|
||||
@@ -407,7 +389,6 @@
|
||||
8 src/renderer/src/runtime/runtime-linear-client.ts
|
||||
6 src/renderer/src/runtime/runtime-linear-issue-mutations.ts
|
||||
12 src/renderer/src/runtime/runtime-linear-project-client.ts
|
||||
3 src/renderer/src/runtime/runtime-repo-client.ts
|
||||
3 src/renderer/src/runtime/structured-agent-session-owner.ts
|
||||
1 src/renderer/src/runtime/structured-conversation-name.ts
|
||||
5 src/renderer/src/runtime/use-runtime-session-mirror-environment-key.ts
|
||||
@@ -496,17 +477,12 @@
|
||||
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
|
||||
12 src/renderer/src/store/slices/worktrees.ts
|
||||
3 src/renderer/src/store/slices/worktrees/create/create-worktree.ts
|
||||
7 src/renderer/src/store/slices/worktrees.ts
|
||||
1 src/renderer/src/store/slices/worktrees/create/create-worktree.ts
|
||||
1 src/renderer/src/store/slices/worktrees/create/created-worktree-state-merge.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
|
||||
7 src/renderer/src/store/slices/worktrees/listing/detected-worktree-refresh.ts
|
||||
4 src/renderer/src/store/slices/worktrees/listing/fetch-all-worktrees.ts
|
||||
4 src/renderer/src/store/slices/worktrees/listing/fetch-detected-worktrees.ts
|
||||
6 src/renderer/src/store/slices/worktrees/listing/fetch-worktrees.ts
|
||||
2 src/renderer/src/store/slices/worktrees/listing/fetch-detected-worktrees.ts
|
||||
2 src/renderer/src/store/slices/worktrees/listing/fetch-worktrees.ts
|
||||
1 src/renderer/src/store/slices/worktrees/listing/fetched-worktree-merge.ts
|
||||
2 src/renderer/src/store/slices/worktrees/listing/known-ssh-worktree-fetch.ts
|
||||
2 src/renderer/src/store/slices/worktrees/listing/worktree-host-ownership.ts
|
||||
3 src/renderer/src/store/slices/worktrees/listing/worktree-owner-settings.ts
|
||||
2 src/renderer/src/store/slices/worktrees/metadata/update-worktree-meta.ts
|
||||
@@ -515,10 +491,7 @@
|
||||
1 src/renderer/src/store/slices/worktrees/metadata/worktree-lineage-refresh.ts
|
||||
1 src/renderer/src/store/slices/worktrees/session/worktree-slice-lookups.ts
|
||||
3 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
|
||||
1 src/renderer/src/store/slices/worktrees/teardown/purge-stale-runtime-host-state.ts
|
||||
2 src/renderer/src/store/slices/worktrees/teardown/remove-worktree.ts
|
||||
1 src/renderer/src/store/slices/worktrees/teardown/worktree-purge-state.ts
|
||||
2 src/renderer/src/store/terminals/terminal-active-workspace-creation.ts
|
||||
1 src/renderer/src/store/terminals/terminal-pane-hibernation.ts
|
||||
|
||||
@@ -4,21 +4,6 @@
|
||||
# Prune after removing sites: pnpm check:owner-routing-ratchet --prune
|
||||
1 src/renderer/src/components/linear-api-key-dialog.tsx
|
||||
1 src/renderer/src/components/settings/GeneralPane.tsx
|
||||
1 src/renderer/src/components/settings/RepositoryGitHubAccountSection.tsx
|
||||
1 src/renderer/src/components/settings/RepositoryIconPicker.tsx
|
||||
1 src/renderer/src/components/setup-guide/setup-guide-progress-readiness.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/lib/worktree-creation-flow-startup.ts
|
||||
3 src/renderer/src/runtime/runtime-hooks-client.ts
|
||||
3 src/renderer/src/runtime/runtime-repo-client.ts
|
||||
1 src/renderer/src/store/slices/preflight.ts
|
||||
1 src/renderer/src/store/slices/worktree-visibility-owner-settings.ts
|
||||
3 src/renderer/src/store/slices/worktree-visibility-settings-write.ts
|
||||
1 src/renderer/src/store/slices/worktrees/create/create-worktree.ts
|
||||
1 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
|
||||
1 src/renderer/src/store/slices/worktrees/listing/detected-worktree-refresh.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
|
||||
1 src/renderer/src/store/slices/worktrees/teardown/remove-worktree.ts
|
||||
|
||||
@@ -5,7 +5,11 @@ import { checkRuntimeHooks } from '@/runtime/runtime-hooks-client'
|
||||
import type { AutomationDraft } from './AutomationEditorDialog'
|
||||
import { getAutomationHostTargetFromKey, type AutomationHostTarget } from './automation-host-client'
|
||||
import { getAutomationCreateRepos } from './automation-create-projects'
|
||||
import { toRuntimeExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host'
|
||||
import {
|
||||
getRepoExecutionHostId,
|
||||
toRuntimeExecutionHostId,
|
||||
type ExecutionHostId
|
||||
} from '../../../../shared/execution-host'
|
||||
import { getVisibleAutomationSetupDecision } from './automation-setup-decision'
|
||||
import { capturedAutomationOwner, capturedAutomationOwnerKey } from './automation-captured-owner'
|
||||
import type { AutomationsPageListState } from './use-automations-page-list-state'
|
||||
@@ -116,8 +120,15 @@ export function useAutomationsPageSetupState({
|
||||
if (existingPromise) {
|
||||
return (await existingPromise).hooks
|
||||
}
|
||||
const settingsForRepo = getSettingsForRepoRuntimeOwner({ repos, settings }, repoId)
|
||||
const promise = checkRuntimeHooks(settingsForRepo, repoId, hostId)
|
||||
// Why: a repo id on two hosts has no single owner to inspect; never let focus pick one.
|
||||
const ownerRows = hostId ? [] : repos.filter((repo) => repo.id === repoId)
|
||||
const ownerHostId =
|
||||
hostId ?? (ownerRows.length === 1 ? getRepoExecutionHostId(ownerRows[0]) : null)
|
||||
const promise = (
|
||||
ownerHostId
|
||||
? checkRuntimeHooks(ownerHostId, repoId)
|
||||
: Promise.reject(new Error('The project host is unknown.'))
|
||||
)
|
||||
.then((result) => ({
|
||||
hooks: result.status === 'error' ? null : ((result.hooks as OrcaHooks | null) ?? null),
|
||||
ok: result.status !== 'error'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getRepoExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { searchRuntimeRepoBaseRefDetails } from '@/runtime/runtime-repo-client'
|
||||
import { lookupLinearIssueUrl } from '@/lib/linear-issue-url-lookup'
|
||||
@@ -37,7 +38,6 @@ export function useSmartWorkspaceSecondarySearches({
|
||||
mode,
|
||||
selectedRepo,
|
||||
debouncedQuery,
|
||||
selectedRepoOwnerSettings,
|
||||
setBranches,
|
||||
setBranchResultsSource,
|
||||
setBranchesLoading,
|
||||
@@ -98,8 +98,10 @@ export function useSmartWorkspaceSecondarySearches({
|
||||
]
|
||||
)
|
||||
|
||||
// Why: branches are searched on the selected repo's own host.
|
||||
const selectedRepoHostId = selectedRepo ? getRepoExecutionHostId(selectedRepo) : null
|
||||
useEffect(() => {
|
||||
if (!branchSearchRequest) {
|
||||
if (!branchSearchRequest || !selectedRepoHostId) {
|
||||
setBranches([])
|
||||
setBranchResultsSource(null)
|
||||
setBranchesLoading(false)
|
||||
@@ -109,7 +111,7 @@ export function useSmartWorkspaceSecondarySearches({
|
||||
// Why: visibility retains prior rows while typing ahead of the debounced query.
|
||||
setBranchesLoading(true)
|
||||
void searchRuntimeRepoBaseRefDetails(
|
||||
selectedRepoOwnerSettings,
|
||||
selectedRepoHostId,
|
||||
branchSearchRequest.repoId,
|
||||
branchSearchRequest.query,
|
||||
branchSearchRequest.limit
|
||||
@@ -139,7 +141,7 @@ export function useSmartWorkspaceSecondarySearches({
|
||||
}
|
||||
}, [
|
||||
branchSearchRequest,
|
||||
selectedRepoOwnerSettings,
|
||||
selectedRepoHostId,
|
||||
setBranches,
|
||||
setBranchResultsSource,
|
||||
setBranchesLoading
|
||||
|
||||
@@ -110,11 +110,7 @@ describe('CreateFromPicker host routing', () => {
|
||||
|
||||
await renderPicker(repo)
|
||||
|
||||
expect(getRuntimeRepoBaseRefDefault).toHaveBeenCalledWith(
|
||||
{ activeRuntimeEnvironmentId: 'owner-runtime' },
|
||||
repo.id,
|
||||
'runtime:owner-runtime'
|
||||
)
|
||||
expect(getRuntimeRepoBaseRefDefault).toHaveBeenCalledWith('runtime:owner-runtime', repo.id)
|
||||
})
|
||||
|
||||
// Why: the composer's Branch tab already lists on an empty query; a minimum-length gate left
|
||||
@@ -131,13 +127,7 @@ describe('CreateFromPicker host routing', () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 250))
|
||||
})
|
||||
|
||||
expect(searchRuntimeRepoBaseRefs).toHaveBeenCalledWith(
|
||||
{ activeRuntimeEnvironmentId: null },
|
||||
repo.id,
|
||||
'',
|
||||
30,
|
||||
'local'
|
||||
)
|
||||
expect(searchRuntimeRepoBaseRefs).toHaveBeenCalledWith('local', repo.id, '', 30)
|
||||
})
|
||||
|
||||
it('keeps an explicit local repo on the local client even when a runtime is focused', async () => {
|
||||
@@ -146,11 +136,7 @@ describe('CreateFromPicker host routing', () => {
|
||||
|
||||
await renderPicker(repo)
|
||||
|
||||
expect(getRuntimeRepoBaseRefDefault).toHaveBeenCalledWith(
|
||||
{ activeRuntimeEnvironmentId: null },
|
||||
repo.id,
|
||||
'local'
|
||||
)
|
||||
expect(getRuntimeRepoBaseRefDefault).toHaveBeenCalledWith('local', repo.id)
|
||||
expect(searchRuntimeRepoBaseRefs).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -19,8 +19,6 @@ import { cn } from '@/lib/utils'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import type { Worktree } from '../../../../shared/worktree/types'
|
||||
import { getRepoExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { useAppStore } from '@/store'
|
||||
import { getRuntimeEnvironmentIdForRepo } from '@/lib/repo-runtime-owner'
|
||||
import {
|
||||
getRuntimeRepoBaseRefDefault,
|
||||
searchRuntimeRepoBaseRefs
|
||||
@@ -56,11 +54,6 @@ export function CreateFromPicker({
|
||||
onValueChange: (baseBranch: string) => void
|
||||
onSetDefault?: (baseBranch: string) => void | Promise<void>
|
||||
}): React.JSX.Element {
|
||||
// Per-repo evidence, not the ambient active-runtime setting; the base-ref helpers
|
||||
// just take a settings-shaped object, so it is synthesized at each call below.
|
||||
const repoRuntimeEnvironmentId = useAppStore((state) =>
|
||||
getRuntimeEnvironmentIdForRepo(state, repoId)
|
||||
)
|
||||
const repo = repoMap.get(repoId)
|
||||
const repoHostId = repo ? getRepoExecutionHostId(repo) : undefined
|
||||
const [open, setOpen] = React.useState(false)
|
||||
@@ -161,16 +154,12 @@ export function CreateFromPicker({
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!repoId) {
|
||||
if (!repoId || !repoHostId) {
|
||||
return
|
||||
}
|
||||
let stale = false
|
||||
setDefaultBaseRef(null)
|
||||
void getRuntimeRepoBaseRefDefault(
|
||||
{ activeRuntimeEnvironmentId: repoRuntimeEnvironmentId },
|
||||
repoId,
|
||||
repoHostId
|
||||
)
|
||||
void getRuntimeRepoBaseRefDefault(repoHostId, repoId)
|
||||
.then((result) => {
|
||||
if (!stale) {
|
||||
setDefaultBaseRef(result.defaultBaseRef)
|
||||
@@ -184,7 +173,7 @@ export function CreateFromPicker({
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
}, [repoHostId, repoRuntimeEnvironmentId, repoId])
|
||||
}, [repoHostId, repoId])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isRuntimeRepoRefSearchQueryWithinLimit(query)) {
|
||||
@@ -195,7 +184,7 @@ export function CreateFromPicker({
|
||||
const trimmedQuery = query.trim()
|
||||
// Why: an empty query lists the repo's branches, as the composer's Branch tab already does;
|
||||
// a minimum length left this picker showing only the default and worktree branches.
|
||||
if (!open || !repoId) {
|
||||
if (!open || !repoId || !repoHostId) {
|
||||
setSearchResults([])
|
||||
setIsSearching(false)
|
||||
return
|
||||
@@ -204,13 +193,7 @@ export function CreateFromPicker({
|
||||
let stale = false
|
||||
setIsSearching(true)
|
||||
const timer = window.setTimeout(() => {
|
||||
void searchRuntimeRepoBaseRefs(
|
||||
{ activeRuntimeEnvironmentId: repoRuntimeEnvironmentId },
|
||||
repoId,
|
||||
trimmedQuery,
|
||||
30,
|
||||
repoHostId
|
||||
)
|
||||
void searchRuntimeRepoBaseRefs(repoHostId, repoId, trimmedQuery, 30)
|
||||
.then((results) => {
|
||||
if (!stale) {
|
||||
setSearchResults(results)
|
||||
@@ -232,7 +215,7 @@ export function CreateFromPicker({
|
||||
stale = true
|
||||
window.clearTimeout(timer)
|
||||
}
|
||||
}, [repoHostId, repoRuntimeEnvironmentId, open, query, repoId])
|
||||
}, [repoHostId, open, query, repoId])
|
||||
|
||||
const compactTriggerContent = (
|
||||
<>
|
||||
|
||||
@@ -20,7 +20,6 @@ export function useSourceControlReviewContext(panelState: SourceControlPanelStat
|
||||
activeRepoConnectionId,
|
||||
activeRepoExecutionHostId,
|
||||
activeRepoId,
|
||||
activeRepoRuntimeEnvironmentId,
|
||||
activeRepoTarget,
|
||||
activeWorktree,
|
||||
activeWorktreeId,
|
||||
@@ -58,7 +57,6 @@ export function useSourceControlReviewContext(panelState: SourceControlPanelStat
|
||||
activeRepoConnectionId,
|
||||
activeRepoExecutionHostId,
|
||||
activeRepoId,
|
||||
activeRepoRuntimeEnvironmentId,
|
||||
activeRepoWorktreeBaseRef: activeRepo?.worktreeBaseRef,
|
||||
activeWorktreeBaseRef: activeWorktree?.baseRef,
|
||||
hostedReview,
|
||||
|
||||
+3
-10
@@ -6,32 +6,26 @@ export function useSourceControlBaseRefDefault({
|
||||
activeRepoConnectionId,
|
||||
activeRepoExecutionHostId,
|
||||
activeRepoId,
|
||||
activeRepoRuntimeEnvironmentId,
|
||||
isBranchVisible,
|
||||
isFolder
|
||||
}: {
|
||||
activeRepoConnectionId: string | null
|
||||
activeRepoExecutionHostId: ExecutionHostId | null
|
||||
activeRepoId: string | null
|
||||
activeRepoRuntimeEnvironmentId: string | null | undefined
|
||||
isBranchVisible: boolean
|
||||
isFolder: boolean
|
||||
}): string | null {
|
||||
// Why: start null (not 'origin/main') so branch compare doesn't fire with a fabricated ref before the IPC resolves.
|
||||
const [defaultBaseRef, setDefaultBaseRef] = useState<string | null>(null)
|
||||
useEffect(() => {
|
||||
if (!isBranchVisible || !activeRepoId || isFolder) {
|
||||
if (!isBranchVisible || !activeRepoId || !activeRepoExecutionHostId || isFolder) {
|
||||
return
|
||||
}
|
||||
// Why: reset to null so that effectiveBaseRef becomes falsy until the IPC resolves, so branch compare can't fire with a stale defaultBaseRef from a different repo (transient "invalid-base" on switch).
|
||||
setDefaultBaseRef(null)
|
||||
let stale = false
|
||||
void getRuntimeRepoBaseRefDefault(
|
||||
{ activeRuntimeEnvironmentId: activeRepoRuntimeEnvironmentId },
|
||||
activeRepoId,
|
||||
// Why: the direct-repo path resolves the record by OWNER host, not the focused runtime.
|
||||
activeRepoExecutionHostId ?? undefined
|
||||
)
|
||||
// Why: resolved on the repo's OWNER host, not the focused runtime.
|
||||
void getRuntimeRepoBaseRefDefault(activeRepoExecutionHostId, activeRepoId)
|
||||
.then((result) => {
|
||||
if (!stale) {
|
||||
// IPC returns a { defaultBaseRef, remoteCount } envelope; only defaultBaseRef is needed here (remoteCount powers BaseRefPicker's multi-remote hint).
|
||||
@@ -53,7 +47,6 @@ export function useSourceControlBaseRefDefault({
|
||||
activeRepoConnectionId,
|
||||
activeRepoExecutionHostId,
|
||||
activeRepoId,
|
||||
activeRepoRuntimeEnvironmentId,
|
||||
isBranchVisible,
|
||||
isFolder
|
||||
])
|
||||
|
||||
@@ -15,7 +15,6 @@ export function useSourceControlBaseRefs({
|
||||
activeRepoConnectionId,
|
||||
activeRepoExecutionHostId,
|
||||
activeRepoId,
|
||||
activeRepoRuntimeEnvironmentId,
|
||||
activeRepoWorktreeBaseRef,
|
||||
activeWorktreeBaseRef,
|
||||
hostedReview,
|
||||
@@ -27,7 +26,6 @@ export function useSourceControlBaseRefs({
|
||||
activeRepoConnectionId: SourceControlWorktreeContext['activeRepoConnectionId']
|
||||
activeRepoExecutionHostId: SourceControlWorktreeContext['activeRepoExecutionHostId']
|
||||
activeRepoId: string | null
|
||||
activeRepoRuntimeEnvironmentId: string | null
|
||||
activeRepoWorktreeBaseRef: string | undefined
|
||||
activeWorktreeBaseRef: string | undefined
|
||||
hostedReview: HostedReviewInfo | null
|
||||
@@ -40,7 +38,6 @@ export function useSourceControlBaseRefs({
|
||||
activeRepoConnectionId,
|
||||
activeRepoExecutionHostId,
|
||||
activeRepoId,
|
||||
activeRepoRuntimeEnvironmentId,
|
||||
isBranchVisible,
|
||||
isFolder
|
||||
})
|
||||
|
||||
+22
-17
@@ -1,5 +1,7 @@
|
||||
import { useEffect, useState, type Dispatch, type SetStateAction } from 'react'
|
||||
import type { RuntimeClientTarget } from '@/runtime/runtime-client-target'
|
||||
import { useAppStore } from '@/store'
|
||||
import { findRepoForHost } from '@/store/slices/repo-host-identity'
|
||||
import { getRepoExecutionHostId } from '../../../../shared/execution-host'
|
||||
import {
|
||||
getRuntimeRepoBaseRefDefault,
|
||||
searchRuntimeRepoBaseRefDetails
|
||||
@@ -12,8 +14,6 @@ import {
|
||||
type CreatePullRequestBaseRefDiscoveryOptions = {
|
||||
open: boolean
|
||||
repoId: string
|
||||
/** The repo owner's transport; `null` skips discovery. */
|
||||
target: RuntimeClientTarget | null
|
||||
base: string
|
||||
baseQuery: string
|
||||
setBase: Dispatch<SetStateAction<string>>
|
||||
@@ -22,17 +22,9 @@ type CreatePullRequestBaseRefDiscoveryOptions = {
|
||||
setBaseSearchError: Dispatch<SetStateAction<string | null>>
|
||||
}
|
||||
|
||||
// Why: the repo client still takes the settings shape; build it from the owner, never from focus.
|
||||
function repoClientRoute(target: RuntimeClientTarget) {
|
||||
return {
|
||||
activeRuntimeEnvironmentId: target.kind === 'environment' ? target.environmentId : null
|
||||
}
|
||||
}
|
||||
|
||||
export function useCreatePullRequestBaseRefDiscovery({
|
||||
open,
|
||||
repoId,
|
||||
target,
|
||||
base,
|
||||
baseQuery,
|
||||
setBase,
|
||||
@@ -44,6 +36,11 @@ export function useCreatePullRequestBaseRefDiscovery({
|
||||
// previous repo's default branch would silently suppress the stacked-PR lookup.
|
||||
const [repoDefault, setRepoDefault] = useState<{ repoId: string; baseRef: string } | null>(null)
|
||||
const repoDefaultBaseRef = repoDefault?.repoId === repoId ? repoDefault.baseRef : null
|
||||
// Why: refs live on the repo's own host, not the focused one.
|
||||
const repoHostId = useAppStore((s) => {
|
||||
const repo = findRepoForHost(s.repos, repoId, { settings: s.settings })
|
||||
return repo ? getRepoExecutionHostId(repo) : null
|
||||
})
|
||||
|
||||
// Why: resolved separately from eligibility's defaultBaseRef, which reports the
|
||||
// worktree's own base. Consumers that need "is this the repo's default branch?"
|
||||
@@ -51,11 +48,11 @@ export function useCreatePullRequestBaseRefDiscovery({
|
||||
useEffect(() => {
|
||||
// Why: the repo default doesn't move while a repo stays open, so skip the probe
|
||||
// once it is known — on a remote runtime it is an RPC round-trip per composer open.
|
||||
if (!open || repoDefaultBaseRef || !target) {
|
||||
if (!open || repoDefaultBaseRef || !repoHostId) {
|
||||
return
|
||||
}
|
||||
let stale = false
|
||||
void getRuntimeRepoBaseRefDefault(repoClientRoute(target), repoId)
|
||||
void getRuntimeRepoBaseRefDefault(repoHostId, repoId)
|
||||
.then((result) => {
|
||||
if (!stale && result.defaultBaseRef) {
|
||||
setRepoDefault({ repoId, baseRef: stripBaseRef(result.defaultBaseRef) })
|
||||
@@ -65,7 +62,7 @@ export function useCreatePullRequestBaseRefDiscovery({
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
}, [open, repoDefaultBaseRef, repoId, target])
|
||||
}, [open, repoDefaultBaseRef, repoHostId, repoId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || base || !repoDefaultBaseRef) {
|
||||
@@ -75,7 +72,7 @@ export function useCreatePullRequestBaseRefDiscovery({
|
||||
}, [base, open, repoDefaultBaseRef, setBase])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || baseQuery.trim().length < 2 || !target) {
|
||||
if (!open || !repoHostId || baseQuery.trim().length < 2) {
|
||||
setBaseResults([])
|
||||
setBaseSearchPending(false)
|
||||
setBaseSearchError(null)
|
||||
@@ -84,7 +81,7 @@ export function useCreatePullRequestBaseRefDiscovery({
|
||||
let stale = false
|
||||
setBaseSearchPending(true)
|
||||
const timer = window.setTimeout(() => {
|
||||
void searchRuntimeRepoBaseRefDetails(repoClientRoute(target), repoId, baseQuery.trim(), 20)
|
||||
void searchRuntimeRepoBaseRefDetails(repoHostId, repoId, baseQuery.trim(), 20)
|
||||
.then((results) => {
|
||||
if (!stale) {
|
||||
setBaseResults(normalizeCreateReviewBaseSearchResults(results))
|
||||
@@ -107,7 +104,15 @@ export function useCreatePullRequestBaseRefDiscovery({
|
||||
stale = true
|
||||
window.clearTimeout(timer)
|
||||
}
|
||||
}, [baseQuery, open, repoId, target, setBaseResults, setBaseSearchError, setBaseSearchPending])
|
||||
}, [
|
||||
baseQuery,
|
||||
open,
|
||||
repoHostId,
|
||||
repoId,
|
||||
setBaseResults,
|
||||
setBaseSearchError,
|
||||
setBaseSearchPending
|
||||
])
|
||||
|
||||
return repoDefaultBaseRef
|
||||
}
|
||||
|
||||
+9
@@ -13,6 +13,7 @@ vi.mock('@/runtime/runtime-repo-client', () => ({
|
||||
}))
|
||||
|
||||
const { useCreatePullRequestDialogFields } = await import('./useCreatePullRequestDialogFields')
|
||||
const { useAppStore } = await import('@/store')
|
||||
|
||||
type DialogFields = ReturnType<typeof useCreatePullRequestDialogFields>
|
||||
|
||||
@@ -87,6 +88,14 @@ function renderFields(initialRepoId: string): {
|
||||
|
||||
beforeEach(() => {
|
||||
getRuntimeRepoBaseRefDefault.mockReset()
|
||||
// Why: refs are read on the repo row's own host.
|
||||
const repo = { path: '/repo', displayName: 'Repo', badgeColor: '#000', addedAt: 0 }
|
||||
useAppStore.setState({
|
||||
repos: [
|
||||
{ ...repo, id: 'repo-1' },
|
||||
{ ...repo, id: 'repo-2' }
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
describe('useCreatePullRequestDialogFields repo default base ref', () => {
|
||||
|
||||
@@ -198,7 +198,6 @@ export function useCreatePullRequestDialogFields({
|
||||
const repoDefaultBaseRef = useCreatePullRequestBaseRefDiscovery({
|
||||
open,
|
||||
repoId,
|
||||
target,
|
||||
base,
|
||||
baseQuery,
|
||||
setBase,
|
||||
|
||||
@@ -3,14 +3,14 @@ import { useEffect, useRef, useState } from 'react'
|
||||
import { Button } from '../ui/button'
|
||||
import { Input } from '../ui/input'
|
||||
import { useAppStore } from '@/store'
|
||||
import { getRuntimeEnvironmentIdForRepo } from '@/lib/repo-runtime-owner'
|
||||
import { findRepoForHost } from '@/store/slices/repo-host-identity'
|
||||
import {
|
||||
getRuntimeRepoBaseRefDefault,
|
||||
searchRuntimeRepoBaseRefs
|
||||
} from '@/runtime/runtime-repo-client'
|
||||
import { isRuntimeRepoRefSearchQueryWithinLimit } from '@/runtime/runtime-repo-search-bounds'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { parseExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { getRepoExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host'
|
||||
|
||||
type BaseRefPickerProps = {
|
||||
repoId: string
|
||||
@@ -27,15 +27,14 @@ export function BaseRefPicker({
|
||||
onSelect,
|
||||
onUsePrimary
|
||||
}: BaseRefPickerProps): React.JSX.Element {
|
||||
const focusedRuntimeEnvironmentId = useAppStore((state) =>
|
||||
getRuntimeEnvironmentIdForRepo(state, repoId)
|
||||
)
|
||||
const selectedHost = parseExecutionHostId(hostId)
|
||||
const activeRuntimeEnvironmentId = hostId
|
||||
? selectedHost?.kind === 'runtime'
|
||||
? selectedHost.environmentId
|
||||
: null
|
||||
: focusedRuntimeEnvironmentId
|
||||
// Why: refs live on the repo's own host; with no host given, the repo row names it.
|
||||
const ownerHostId = useAppStore((state) => {
|
||||
if (hostId) {
|
||||
return hostId
|
||||
}
|
||||
const repo = findRepoForHost(state.repos, repoId, { settings: state.settings })
|
||||
return repo ? getRepoExecutionHostId(repo) : null
|
||||
})
|
||||
// Why: null until the IPC resolves (or when the repo has no default base ref
|
||||
// available). We avoid seeding with 'origin/main' because that would display
|
||||
// a fabricated default in repos that don't actually have origin/main.
|
||||
@@ -73,11 +72,10 @@ export function BaseRefPicker({
|
||||
|
||||
const loadDefaultBaseRef = async (): Promise<void> => {
|
||||
try {
|
||||
const result = await getRuntimeRepoBaseRefDefault(
|
||||
{ activeRuntimeEnvironmentId },
|
||||
repoId,
|
||||
hostId
|
||||
)
|
||||
if (!ownerHostId) {
|
||||
return
|
||||
}
|
||||
const result = await getRuntimeRepoBaseRefDefault(ownerHostId, repoId)
|
||||
if (!stale) {
|
||||
setDefaultBaseRef(result.defaultBaseRef)
|
||||
setRemoteCount(result.remoteCount)
|
||||
@@ -103,7 +101,7 @@ export function BaseRefPicker({
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
}, [activeRuntimeEnvironmentId, hostId, repoId])
|
||||
}, [ownerHostId, repoId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRuntimeRepoRefSearchQueryWithinLimit(baseRefQuery)) {
|
||||
@@ -112,7 +110,7 @@ export function BaseRefPicker({
|
||||
return
|
||||
}
|
||||
const trimmedQuery = baseRefQuery.trim()
|
||||
if (trimmedQuery.length < 2) {
|
||||
if (trimmedQuery.length < 2 || !ownerHostId) {
|
||||
setBaseRefResults([])
|
||||
setIsSearchingBaseRefs(false)
|
||||
return
|
||||
@@ -122,13 +120,7 @@ export function BaseRefPicker({
|
||||
setIsSearchingBaseRefs(true)
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
void searchRuntimeRepoBaseRefs(
|
||||
{ activeRuntimeEnvironmentId },
|
||||
repoId,
|
||||
trimmedQuery,
|
||||
20,
|
||||
hostId
|
||||
)
|
||||
void searchRuntimeRepoBaseRefs(ownerHostId, repoId, trimmedQuery, 20)
|
||||
.then((results) => {
|
||||
if (!stale) {
|
||||
setBaseRefResults(results)
|
||||
@@ -151,7 +143,7 @@ export function BaseRefPicker({
|
||||
stale = true
|
||||
window.clearTimeout(timer)
|
||||
}
|
||||
}, [activeRuntimeEnvironmentId, baseRefQuery, hostId, repoId])
|
||||
}, [baseRefQuery, ownerHostId, repoId])
|
||||
|
||||
const effectiveBaseRef = currentBaseRef ?? defaultBaseRef
|
||||
|
||||
|
||||
+6
-5
@@ -189,22 +189,23 @@ describe('repository account inventory request ownership', () => {
|
||||
expect(listAccountsMock).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('reloads for a changed legacy runtime target and keeps explicit refresh forced', async () => {
|
||||
it('keeps an unstamped repo on this computer when the default host changes', async () => {
|
||||
const repo: Repo = { ...BASE_REPO, executionHostId: undefined }
|
||||
storeState.settings = { activeRuntimeEnvironmentId: 'server-1' }
|
||||
await render(repo)
|
||||
storeState.settings = { activeRuntimeEnvironmentId: 'server-2' }
|
||||
await render(repo)
|
||||
expect(listAccountsMock).toHaveBeenCalledTimes(1)
|
||||
expect(listAccountsMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
{ kind: 'environment', environmentId: 'server-2' },
|
||||
1,
|
||||
{ kind: 'local' },
|
||||
{ id: repo.id, path: repo.path },
|
||||
{ refreshCapability: false }
|
||||
)
|
||||
await act(async () => getRefreshButton().click())
|
||||
expect(listAccountsMock).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
{ kind: 'environment', environmentId: 'server-2' },
|
||||
2,
|
||||
{ kind: 'local' },
|
||||
{ id: repo.id, path: repo.path },
|
||||
{ refreshCapability: true }
|
||||
)
|
||||
|
||||
@@ -7,8 +7,8 @@ import { Button } from '../ui/button'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
|
||||
import { SearchableSetting } from './SearchableSetting'
|
||||
import { useAppStore } from '../../store'
|
||||
import { getRepoOwnerRoutedSettings } from '@/lib/repo-runtime-owner'
|
||||
import { getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
|
||||
import { runtimeTargetForOwnerEnvironment } from '@/runtime/runtime-client-target'
|
||||
import { getRepoExecutionHostId, parseExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { searchKeywords } from './settings-search-keywords'
|
||||
import {
|
||||
@@ -35,7 +35,6 @@ export function RepositoryGitHubAccountSection({
|
||||
updateRepo,
|
||||
forceVisible
|
||||
}: RepositoryGitHubAccountSectionProps): React.JSX.Element | null {
|
||||
const settings = useAppStore((state) => state.settings)
|
||||
const [inventory, setInventory] = useState<GhAccountBindingInventory | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
@@ -44,13 +43,12 @@ export function RepositoryGitHubAccountSection({
|
||||
const loadGenerationRef = useRef(0)
|
||||
const selectLabelId = useId()
|
||||
|
||||
// Why: getRepoOwnerRoutedSettings returns a fresh object each render, and the target
|
||||
// depends on nothing else — key the memo on the id so it stays stable.
|
||||
const activeRuntimeEnvironmentId =
|
||||
getRepoOwnerRoutedSettings(settings, repo)?.activeRuntimeEnvironmentId ?? null
|
||||
// Why: the binding lives on the repo's own host; key the memo on its id so it stays stable.
|
||||
const ownerHost = parseExecutionHostId(getRepoExecutionHostId(repo))
|
||||
const ownerEnvironmentId = ownerHost?.kind === 'runtime' ? ownerHost.environmentId : null
|
||||
const runtimeTarget = useMemo(
|
||||
() => getActiveRuntimeTarget({ activeRuntimeEnvironmentId }),
|
||||
[activeRuntimeEnvironmentId]
|
||||
() => runtimeTargetForOwnerEnvironment(ownerEnvironmentId),
|
||||
[ownerEnvironmentId]
|
||||
)
|
||||
|
||||
const loadInventory = useCallback(
|
||||
|
||||
@@ -168,10 +168,6 @@ describe('RepositoryHooksSection execution ownership', () => {
|
||||
await Promise.resolve()
|
||||
})
|
||||
|
||||
expect(readRuntimeIssueCommand).toHaveBeenCalledWith(
|
||||
{ activeRuntimeEnvironmentId: 'hub' },
|
||||
repo.id,
|
||||
'runtime:hub'
|
||||
)
|
||||
expect(readRuntimeIssueCommand).toHaveBeenCalledWith('runtime:hub', repo.id)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
HookCommandSourcePolicy,
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
SetupRunPolicy
|
||||
} from '../../../../shared/orca-yaml-hook-types'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import { getRepoExecutionHostId, parseExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { getRepoExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { resolveHookCommandSourcePolicy } from '../../../../shared/hook-command-source-policy'
|
||||
import { useAppStore } from '@/store'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
@@ -61,12 +61,6 @@ export function RepositoryHooksSection({
|
||||
const settingsSearchQuery = useAppStore((state) => state.settingsSearchQuery)
|
||||
const selectedHostId = getRepoExecutionHostId(repo)
|
||||
const repoHostIdentity = `${selectedHostId}\0${repo.id}`
|
||||
const hookRuntimeSettings = useMemo(() => {
|
||||
const parsedHost = parseExecutionHostId(selectedHostId)
|
||||
return {
|
||||
activeRuntimeEnvironmentId: parsedHost?.kind === 'runtime' ? parsedHost.environmentId : null
|
||||
}
|
||||
}, [selectedHostId])
|
||||
const yamlState = yamlHooks
|
||||
? 'loaded'
|
||||
: hasHooksFile
|
||||
@@ -82,7 +76,6 @@ export function RepositoryHooksSection({
|
||||
updateHookSettingsPolicyDraft
|
||||
} = useRepositoryHookSettingsDraft({ repo, repoHostIdentity, onUpdateHookSettings })
|
||||
const issueCommand = useRepositoryIssueCommand({
|
||||
hookRuntimeSettings,
|
||||
repoId: repo.id,
|
||||
repoHostIdentity,
|
||||
selectedHostId
|
||||
|
||||
@@ -8,7 +8,7 @@ import { normalizeRepoBadgeColor } from '../../../../shared/repo-badge-color'
|
||||
import { Button } from '../ui/button'
|
||||
import { Label } from '../ui/label'
|
||||
import { RepoIconGlyph, getRepoLucideIconOptions } from '../repo/repo-icon'
|
||||
import { getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
|
||||
import { runtimeTargetForOwnerEnvironment } from '@/runtime/runtime-client-target'
|
||||
import { getRepoExecutionHostId, parseExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { useMountedRef } from '@/hooks/useMountedRef'
|
||||
import { RepositoryIconColorSection } from './RepositoryIconColorSection'
|
||||
@@ -33,16 +33,15 @@ export function RepositoryIconPicker({
|
||||
// Why: resolve this repo's upstream/avatar on the host that owns it, not the
|
||||
// focused runtime.
|
||||
const selectedHost = parseExecutionHostId(getRepoExecutionHostId(repo))
|
||||
const activeRuntimeEnvironmentId =
|
||||
selectedHost?.kind === 'runtime' ? selectedHost.environmentId : null
|
||||
const ownerEnvironmentId = selectedHost?.kind === 'runtime' ? selectedHost.environmentId : null
|
||||
const selectedLucideName = repo.repoIcon?.type === 'lucide' ? repo.repoIcon.name : null
|
||||
const selectedEmoji = repo.repoIcon?.type === 'emoji' ? repo.repoIcon.emoji : ''
|
||||
const selectedBadgeColor = normalizeRepoBadgeColor(repo.badgeColor) ?? DEFAULT_REPO_BADGE_COLOR
|
||||
const initialTab =
|
||||
repo.repoIcon?.type === 'emoji' ? 'emoji' : repo.repoIcon?.type === 'lucide' ? 'icon' : 'avatar'
|
||||
const runtimeTarget = useMemo(
|
||||
() => getActiveRuntimeTarget({ activeRuntimeEnvironmentId }),
|
||||
[activeRuntimeEnvironmentId]
|
||||
() => runtimeTargetForOwnerEnvironment(ownerEnvironmentId),
|
||||
[ownerEnvironmentId]
|
||||
)
|
||||
|
||||
const currentIconLabel = useMemo(() => {
|
||||
|
||||
@@ -3,15 +3,11 @@ import { toast } from 'sonner'
|
||||
import { readRuntimeIssueCommand, writeRuntimeIssueCommand } from '@/runtime/runtime-hooks-client'
|
||||
import type { ExecutionHostId } from '../../../../shared/execution-host'
|
||||
|
||||
type HookRuntimeSettings = { activeRuntimeEnvironmentId: string | null }
|
||||
|
||||
export function useRepositoryIssueCommand({
|
||||
hookRuntimeSettings,
|
||||
repoId,
|
||||
repoHostIdentity,
|
||||
selectedHostId
|
||||
}: {
|
||||
hookRuntimeSettings: HookRuntimeSettings
|
||||
repoId: string
|
||||
repoHostIdentity: string
|
||||
selectedHostId: ExecutionHostId
|
||||
@@ -37,7 +33,7 @@ export function useRepositoryIssueCommand({
|
||||
updateIssueCommandDraft('')
|
||||
setHasSharedIssueCommand(false)
|
||||
setIssueCommandSaveError(null)
|
||||
void readRuntimeIssueCommand(hookRuntimeSettings, repoId, selectedHostId)
|
||||
void readRuntimeIssueCommand(selectedHostId, repoId)
|
||||
.then((result) => {
|
||||
if (cancelled) {
|
||||
return
|
||||
@@ -60,23 +56,18 @@ export function useRepositoryIssueCommand({
|
||||
cancelled = true
|
||||
const draft = issueCommandDraftRef.current.trim()
|
||||
if (draft !== lastCommittedIssueCommandRef.current) {
|
||||
void writeRuntimeIssueCommand(hookRuntimeSettings, repoId, draft, selectedHostId).catch(
|
||||
(error) => {
|
||||
console.error(
|
||||
'[RepositoryHooksSection] Failed to save issue command on unmount:',
|
||||
error
|
||||
)
|
||||
}
|
||||
)
|
||||
void writeRuntimeIssueCommand(selectedHostId, repoId, draft).catch((error) => {
|
||||
console.error('[RepositoryHooksSection] Failed to save issue command on unmount:', error)
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [hookRuntimeSettings, repoHostIdentity, repoId, selectedHostId, updateIssueCommandDraft])
|
||||
}, [repoHostIdentity, repoId, selectedHostId, updateIssueCommandDraft])
|
||||
|
||||
const commitIssueCommand = useCallback(async (): Promise<void> => {
|
||||
const trimmed = issueCommandDraft.trim()
|
||||
updateIssueCommandDraft(trimmed)
|
||||
try {
|
||||
await writeRuntimeIssueCommand(hookRuntimeSettings, repoId, trimmed, selectedHostId)
|
||||
await writeRuntimeIssueCommand(selectedHostId, repoId, trimmed)
|
||||
lastCommittedIssueCommandRef.current = trimmed
|
||||
setIssueCommandSaveError(null)
|
||||
} catch (error) {
|
||||
@@ -86,7 +77,7 @@ export function useRepositoryIssueCommand({
|
||||
setIssueCommandSaveError(message)
|
||||
toast.error(message)
|
||||
}
|
||||
}, [hookRuntimeSettings, issueCommandDraft, repoId, selectedHostId, updateIssueCommandDraft])
|
||||
}, [issueCommandDraft, repoId, selectedHostId, updateIssueCommandDraft])
|
||||
|
||||
return {
|
||||
issueCommandDraft,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect } from 'react'
|
||||
import type { OrcaHooks } from '../../../../shared/orca-yaml-hook-types'
|
||||
import { isFolderRepo } from '../../../../shared/repo-kind'
|
||||
import { getRepoExecutionHostId, parseExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { getRepoExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { checkRuntimeHooks } from '@/runtime/runtime-hooks-client'
|
||||
import { getRepoHostIdentity } from '../../store/slices/repo-host-identity'
|
||||
import type { SettingsStoreModel } from './use-settings-store-model'
|
||||
@@ -80,15 +80,7 @@ export function useSettingsRepoScrollEffects(
|
||||
}
|
||||
try {
|
||||
const hostId = getRepoExecutionHostId(repo)
|
||||
const parsedHost = parseExecutionHostId(hostId)
|
||||
const result = await checkRuntimeHooks(
|
||||
{
|
||||
activeRuntimeEnvironmentId:
|
||||
parsedHost?.kind === 'runtime' ? parsedHost.environmentId : null
|
||||
},
|
||||
repo.id,
|
||||
hostId
|
||||
)
|
||||
const result = await checkRuntimeHooks(hostId, repo.id)
|
||||
if (stale || requestSeq !== repoHooksRequestSeqRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
|
||||
import { getRepoExecutionHostId } from '../../../../shared/execution-host'
|
||||
import type { ComputerUsePermissionStatusResult } from '../../../../shared/computer-use-permissions-types'
|
||||
import type { GlobalSettings } from '../../../../shared/global-settings-types'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
@@ -31,16 +31,16 @@ export const INITIAL_SETUP_SCRIPT_PROBE_STATE: SetupScriptProbeState = {
|
||||
|
||||
export function getSetupScriptProbeSignature(
|
||||
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined,
|
||||
orderedGitRepos: readonly Pick<Repo, 'id' | 'hookSettings'>[]
|
||||
orderedGitRepos: readonly Pick<Repo, 'id' | 'hookSettings' | 'connectionId' | 'executionHostId'>[]
|
||||
): string | null {
|
||||
if (!settings) {
|
||||
return null
|
||||
}
|
||||
const target = getActiveRuntimeTarget(settings)
|
||||
// Why: each repo's hooks are probed on its own host, so the host is part of the signature.
|
||||
return JSON.stringify({
|
||||
runtime: target.kind === 'environment' ? target.environmentId : 'local',
|
||||
repos: orderedGitRepos.map((repo) => ({
|
||||
id: repo.id,
|
||||
host: getRepoExecutionHostId(repo),
|
||||
commandSourcePolicy: repo.hookSettings?.commandSourcePolicy ?? null,
|
||||
setup: repo.hookSettings?.scripts?.setup ?? null
|
||||
}))
|
||||
|
||||
@@ -215,18 +215,16 @@ describe('getSetupGuideProgressReady', () => {
|
||||
})
|
||||
|
||||
describe('setup script probe readiness', () => {
|
||||
it('derives the probe signature from runtime and ordered git repo inputs', () => {
|
||||
it('derives the probe signature from each repo host and order', () => {
|
||||
const localSignature = getSetupScriptProbeSignature({ activeRuntimeEnvironmentId: null }, [
|
||||
{ id: 'repo-a', hookSettings: undefined },
|
||||
{ id: 'repo-b', hookSettings: undefined }
|
||||
])
|
||||
const remoteSignature = getSetupScriptProbeSignature(
|
||||
{ activeRuntimeEnvironmentId: 'runtime-1' },
|
||||
[
|
||||
{ id: 'repo-a', hookSettings: undefined },
|
||||
{ id: 'repo-b', hookSettings: undefined }
|
||||
]
|
||||
)
|
||||
// Why: hooks are probed on each repo's own host, so a repo moving host changes the signature.
|
||||
const remoteSignature = getSetupScriptProbeSignature({ activeRuntimeEnvironmentId: null }, [
|
||||
{ id: 'repo-a', hookSettings: undefined, executionHostId: 'runtime:runtime-1' },
|
||||
{ id: 'repo-b', hookSettings: undefined }
|
||||
])
|
||||
const reorderedSignature = getSetupScriptProbeSignature({ activeRuntimeEnvironmentId: null }, [
|
||||
{ id: 'repo-b', hookSettings: undefined },
|
||||
{ id: 'repo-a', hookSettings: undefined }
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
|
||||
import { useAppStore } from '@/store'
|
||||
import { isGitRepoKind } from '../../../../shared/repo-kind'
|
||||
import { getRepoExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { hasFeatureInteraction } from '../../../../shared/feature-interactions'
|
||||
import { checkRuntimeHooks } from '@/runtime/runtime-hooks-client'
|
||||
import { getLocalPreflightContext, localPreflightContextKey } from '@/lib/local-preflight-context'
|
||||
@@ -170,7 +171,9 @@ export function useSetupGuideProgress(
|
||||
|
||||
async function refreshSetupScriptState(): Promise<void> {
|
||||
for (const repo of orderedGitRepos) {
|
||||
const hooksResult = await checkRuntimeHooks(settings, repo.id).catch(() => null)
|
||||
const hooksResult = await checkRuntimeHooks(getRepoExecutionHostId(repo), repo.id).catch(
|
||||
() => null
|
||||
)
|
||||
if (stale) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -75,8 +75,8 @@ function SetupScriptPromptCard(): React.JSX.Element | null {
|
||||
const hostId = getRepoExecutionHostId(repo)
|
||||
const inspection = await inspectSetupScriptPromptState({
|
||||
repo,
|
||||
checkHooks: () => checkRuntimeHooks(settings, repo.id, hostId),
|
||||
inspectImports: () => inspectRuntimeSetupScriptImports(settings, repo.id, hostId)
|
||||
checkHooks: () => checkRuntimeHooks(hostId, repo.id),
|
||||
inspectImports: () => inspectRuntimeSetupScriptImports(hostId, repo.id)
|
||||
})
|
||||
if (!cancelled) {
|
||||
const nextState = {
|
||||
|
||||
@@ -112,7 +112,7 @@ export function useComposerSubmitOrchestration(
|
||||
selectedRepoExecutionHostId: target.runtimeTargetSelection.selectedRepoExecutionHostId,
|
||||
selectedRepoIsGit: target.runtimeTargetSelection.selectedRepoIsGit,
|
||||
selectedRepoIsRemote: target.runtimeTargetSelection.selectedRepoIsRemote,
|
||||
selectedRepoSettings: target.runtimeTargetSelection.selectedRepoSettings,
|
||||
selectedRepoTarget: target.runtimeTargetSelection.selectedRepoTarget,
|
||||
selectedRepoStartupShell: target.runtimeTargetSelection.selectedRepoStartupShell,
|
||||
selectedWorkspaceTarget: target.runtimeTargetSelection.selectedWorkspaceTarget,
|
||||
settings: target.composerTargetStore.settings,
|
||||
|
||||
@@ -115,8 +115,7 @@ export function useComposerTargetState(
|
||||
selectedRepoHookContextKey: runtimeTargetSelection.selectedRepoHookContextKey,
|
||||
selectedRepoIsGit: runtimeTargetSelection.selectedRepoIsGit,
|
||||
selectedRepoPath: asyncComposerState.selectedRepoPath,
|
||||
selectedRepoSettings: runtimeTargetSelection.selectedRepoSettings,
|
||||
selectedRepoSettingsRef: asyncComposerState.selectedRepoSettingsRef,
|
||||
selectedRepoTarget: runtimeTargetSelection.selectedRepoTarget,
|
||||
setCheckedHooksContextKey: asyncComposerState.setCheckedHooksContextKey,
|
||||
setSelectedRepoSlug: asyncComposerState.setSelectedRepoSlug,
|
||||
setSetupAgentStartupPolicy: asyncComposerState.setSetupAgentStartupPolicy,
|
||||
|
||||
@@ -50,8 +50,7 @@ describe('useComposerProviderRuntimeSync', () => {
|
||||
selectedRepoExecutionHostId: 'local',
|
||||
selectedRepoHookContextKey: 'local:repo',
|
||||
selectedRepoIsGit: true,
|
||||
selectedRepoSettings: null,
|
||||
selectedRepoSettingsRef: { current: null },
|
||||
selectedRepoTarget: { kind: 'local' },
|
||||
setCheckedHooksContextKey:
|
||||
vi.fn<ComposerProviderRuntimeSyncInput['setCheckedHooksContextKey']>(),
|
||||
setSelectedRepoSlug,
|
||||
|
||||
@@ -9,8 +9,7 @@ export type ComposerProviderRuntimeSyncInput = Pick<
|
||||
| 'selectedRepoHookContextKey'
|
||||
| 'selectedRepoIsGit'
|
||||
| 'selectedRepoPath'
|
||||
| 'selectedRepoSettings'
|
||||
| 'selectedRepoSettingsRef'
|
||||
| 'selectedRepoTarget'
|
||||
| 'setCheckedHooksContextKey'
|
||||
| 'setSelectedRepoSlug'
|
||||
| 'setSetupAgentStartupPolicy'
|
||||
@@ -28,7 +27,7 @@ import { isGitRepoKind } from '../../../../shared/repo-kind'
|
||||
import { toast } from 'sonner'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { type HookCheckResult, checkRuntimeHooks } from '@/runtime/runtime-hooks-client'
|
||||
import { getActiveRuntimeTarget, callRuntimeRpc } from '@/runtime/runtime-rpc-client'
|
||||
import { callRuntimeRpc } from '@/runtime/runtime-rpc-client'
|
||||
import type { GitHubRepositoryIdentity } from '../../../../shared/github/pull-request-types'
|
||||
import {
|
||||
getRepoSetupAgentStartupPolicy,
|
||||
@@ -44,8 +43,7 @@ export function useComposerProviderRuntimeSync(input: ComposerProviderRuntimeSyn
|
||||
selectedRepoHookContextKey,
|
||||
selectedRepoIsGit,
|
||||
selectedRepoPath,
|
||||
selectedRepoSettings,
|
||||
selectedRepoSettingsRef,
|
||||
selectedRepoTarget,
|
||||
setCheckedHooksContextKey,
|
||||
setSelectedRepoSlug,
|
||||
setSetupAgentStartupPolicy,
|
||||
@@ -197,10 +195,11 @@ export function useComposerProviderRuntimeSync(input: ComposerProviderRuntimeSyn
|
||||
}
|
||||
// Why: drop the cache entry on failure so a transient IPC error doesn't pin every later
|
||||
// check for this repo/host to the same rejection.
|
||||
const promise: Promise<HookCheckResult> = checkRuntimeHooks(
|
||||
selectedRepoSettingsRef.current,
|
||||
targetRepoId,
|
||||
selectedRepoExecutionHostId ?? undefined
|
||||
// Why: the repo row names its host; an unknown one is inspected nowhere.
|
||||
const promise: Promise<HookCheckResult> = (
|
||||
selectedRepoExecutionHostId
|
||||
? checkRuntimeHooks(selectedRepoExecutionHostId, targetRepoId)
|
||||
: Promise.reject(new Error('The project host is unknown.'))
|
||||
).catch((error: unknown) => {
|
||||
if (hookCheckRef.current?.promise === promise) {
|
||||
hookCheckRef.current = null
|
||||
@@ -210,7 +209,7 @@ export function useComposerProviderRuntimeSync(input: ComposerProviderRuntimeSyn
|
||||
hookCheckRef.current = { key, promise }
|
||||
return promise
|
||||
},
|
||||
[selectedRepoExecutionHostId, selectedRepoSettingsRef]
|
||||
[selectedRepoExecutionHostId]
|
||||
)
|
||||
|
||||
const commitHookCheckIfCurrent = useCallback(
|
||||
@@ -231,7 +230,11 @@ export function useComposerProviderRuntimeSync(input: ComposerProviderRuntimeSyn
|
||||
return
|
||||
}
|
||||
let cancelled = false
|
||||
const target = getActiveRuntimeTarget(selectedRepoSettings)
|
||||
const target = selectedRepoTarget
|
||||
if (!target) {
|
||||
setSelectedRepoSlug(null)
|
||||
return
|
||||
}
|
||||
const slugRequest =
|
||||
target.kind === 'environment'
|
||||
? callRuntimeRpc<GitHubRepositoryIdentity | null>(
|
||||
@@ -264,7 +267,7 @@ export function useComposerProviderRuntimeSync(input: ComposerProviderRuntimeSyn
|
||||
selectedRepo,
|
||||
selectedRepoIsGit,
|
||||
selectedRepoPath,
|
||||
selectedRepoSettings,
|
||||
selectedRepoTarget,
|
||||
setSelectedRepoSlug
|
||||
])
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ type QuickCreationExecutionInput = Pick<
|
||||
| 'selectedRepoExecutionHostId'
|
||||
| 'selectedRepoIsGit'
|
||||
| 'selectedRepoIsRemote'
|
||||
| 'selectedRepoSettings'
|
||||
| 'selectedRepoTarget'
|
||||
| 'selectedRepoStartupShell'
|
||||
| 'selectedWorkspaceTarget'
|
||||
| 'settings'
|
||||
@@ -37,7 +37,6 @@ import type { TuiAgent } from '../../../../shared/tui-agent'
|
||||
import type { WorktreeCreationRequest } from '@/lib/pending-worktree-creation'
|
||||
import { useAppStore } from '@/store'
|
||||
import { settleComposerSubmit } from '@/lib/composer-submit-cancellation'
|
||||
import { getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
|
||||
import { runBackgroundWorktreeCreation } from '@/lib/worktree-creation-flow'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { resolveQuickCreateLinkedWorkItemPrompt } from '@/lib/linked-work-item-context'
|
||||
@@ -69,7 +68,7 @@ export function useQuickCreationExecution(input: QuickCreationExecutionInput) {
|
||||
selectedRepoExecutionHostId,
|
||||
selectedRepoIsGit,
|
||||
selectedRepoIsRemote,
|
||||
selectedRepoSettings,
|
||||
selectedRepoTarget,
|
||||
selectedRepoStartupShell,
|
||||
selectedWorkspaceTarget,
|
||||
settings,
|
||||
@@ -194,8 +193,7 @@ export function useQuickCreationExecution(input: QuickCreationExecutionInput) {
|
||||
repoId,
|
||||
ephemeralVmRecipe,
|
||||
indeterminateProgress:
|
||||
Boolean(activeEphemeralVmRecipeId) ||
|
||||
getActiveRuntimeTarget(selectedRepoSettings).kind !== 'local',
|
||||
Boolean(activeEphemeralVmRecipeId) || selectedRepoTarget?.kind === 'environment',
|
||||
taskSourceContext,
|
||||
linkedWorkItem: submitLinkedWorkItem,
|
||||
workspaceRunContext,
|
||||
@@ -280,7 +278,7 @@ export function useQuickCreationExecution(input: QuickCreationExecutionInput) {
|
||||
selectedRepoExecutionHostId,
|
||||
selectedRepoIsGit,
|
||||
selectedRepoIsRemote,
|
||||
selectedRepoSettings,
|
||||
selectedRepoTarget,
|
||||
selectedRepoStartupShell,
|
||||
selectedWorkspaceTarget,
|
||||
settings,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RuntimeClientTarget } from '@/runtime/runtime-client-target'
|
||||
import type { RefObject } from 'react'
|
||||
import type { ExecutionHostId, ParsedExecutionHost } from '../../../../shared/execution-host'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
@@ -45,6 +46,8 @@ export type ComposerRuntimeTargetModel = {
|
||||
projectHostSetupOptions: ProjectHostSetupOption[]
|
||||
projectOptions: NewWorkspaceProjectOption[]
|
||||
selectedRepoSettings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null
|
||||
/** Transport to the selected repo's owner; `null` with no repo selected. */
|
||||
selectedRepoTarget: RuntimeClientTarget | null
|
||||
selectedRecipeRepoId: string | null
|
||||
selectedRecipeRepoConnectionId: string | null
|
||||
ephemeralVmsEnabled: boolean
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { runtimeTargetForOwnerHostId } from '@/runtime/runtime-client-target'
|
||||
import type { ComposerRuntimeTargetSelectionInput } from './composer-target-input-contracts'
|
||||
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
@@ -221,6 +222,11 @@ export function useComposerRuntimeTargetSelection(input: ComposerRuntimeTargetSe
|
||||
)
|
||||
}, [selectedRepo, settings])
|
||||
|
||||
const selectedRepoTarget = useMemo(
|
||||
() => (selectedRepo ? runtimeTargetForOwnerHostId(getRepoExecutionHostId(selectedRepo)) : null),
|
||||
[selectedRepo]
|
||||
)
|
||||
|
||||
// Why: key on repo id, not the repo object — updateRepo replaces it by reference and would re-run this effect, wiping the user's chosen recipe.
|
||||
const selectedRecipeRepoId = selectedRepo?.id ?? null
|
||||
|
||||
@@ -292,6 +298,7 @@ export function useComposerRuntimeTargetSelection(input: ComposerRuntimeTargetSe
|
||||
projectHostSetupOptions,
|
||||
projectOptions,
|
||||
selectedRepoSettings,
|
||||
selectedRepoTarget,
|
||||
selectedRecipeRepoId,
|
||||
selectedRecipeRepoConnectionId,
|
||||
ephemeralVmsEnabled,
|
||||
|
||||
@@ -225,7 +225,7 @@ describe('ensureHooksConfirmed', () => {
|
||||
const decision = await ensureHooksConfirmed(state, 'repo-1', 'archive')
|
||||
|
||||
expect(decision).toBe('run')
|
||||
expect(hooksCheckMock).toHaveBeenCalledWith({ repoId: 'repo-1' })
|
||||
expect(hooksCheckMock).toHaveBeenCalledWith({ repoId: 'repo-1', hostId: 'ssh:ssh-1' })
|
||||
expect(pending).toHaveLength(0)
|
||||
})
|
||||
|
||||
@@ -251,6 +251,23 @@ describe('ensureHooksConfirmed', () => {
|
||||
expect(runtimeEnvironmentCallMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('refuses to pick a host when the repo id is on two hosts and none is given', async () => {
|
||||
const { state, pending } = createTestState()
|
||||
Object.assign(state, {
|
||||
repos: [
|
||||
{ id: 'repo-1', displayName: 'Runtime', executionHostId: 'runtime:env-1' },
|
||||
{ id: 'repo-1', displayName: 'Local', executionHostId: 'local' }
|
||||
]
|
||||
})
|
||||
|
||||
const decision = await ensureHooksConfirmed(state, 'repo-1', 'setup')
|
||||
|
||||
expect(decision).toBe('skip')
|
||||
expect(hooksCheckMock).not.toHaveBeenCalled()
|
||||
expect(runtimeEnvironmentCallMock).not.toHaveBeenCalled()
|
||||
expect(pending).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('checks runtime-owned repo hooks through the repo owner runtime', async () => {
|
||||
const { state, pending } = createTestState({
|
||||
settings: { activeRuntimeEnvironmentId: 'focused-env' },
|
||||
|
||||
@@ -7,11 +7,10 @@ import {
|
||||
readRuntimeIssueCommand,
|
||||
type IssueCommandReadResult
|
||||
} from '@/runtime/runtime-hooks-client'
|
||||
import { getRuntimeEnvironmentIdForRepo } from './repo-runtime-owner'
|
||||
import { MODAL_DISMISSED_KEY } from '@/store/slices/modal-slot-dismissal'
|
||||
import {
|
||||
getRepoExecutionHostId,
|
||||
parseExecutionHostId,
|
||||
toRuntimeExecutionHostId,
|
||||
type ExecutionHostId
|
||||
} from '../../../shared/execution-host'
|
||||
|
||||
@@ -74,25 +73,31 @@ function findHookRepo(state: AppState, repoId: string, hostId?: ExecutionHostId)
|
||||
: state.repos.find((repo) => repo.id === repoId)
|
||||
}
|
||||
|
||||
function settingsForHookRepoOwner(
|
||||
/**
|
||||
* Host that inspects the repo's hooks: a runtime owner the caller captured (a server-owned SSH
|
||||
* repo runs there), else the given host, else the repo row's own. Never the focused host.
|
||||
*/
|
||||
function hookRepoOwnerHostId(
|
||||
state: AppState,
|
||||
repoId: string,
|
||||
hostId?: ExecutionHostId,
|
||||
runtimeOwnerEnvironmentId?: string | null
|
||||
): AppState['settings'] {
|
||||
const parsedHost = hostId ? parseExecutionHostId(hostId) : null
|
||||
const runtimeEnvironmentId =
|
||||
runtimeOwnerEnvironmentId?.trim() ||
|
||||
(hostId
|
||||
? parsedHost?.kind === 'runtime'
|
||||
? parsedHost.environmentId
|
||||
: null
|
||||
: getRuntimeEnvironmentIdForRepo(state, repoId))
|
||||
// Why: hook inspection must follow the repo owner. SSH/local repos execute
|
||||
// through desktop IPC, while runtime repos may differ from the focused host.
|
||||
return state.settings
|
||||
? { ...state.settings, activeRuntimeEnvironmentId: runtimeEnvironmentId }
|
||||
: ({ activeRuntimeEnvironmentId: runtimeEnvironmentId } as AppState['settings'])
|
||||
): ExecutionHostId {
|
||||
const ownerEnvironmentId = runtimeOwnerEnvironmentId?.trim()
|
||||
if (ownerEnvironmentId) {
|
||||
return toRuntimeExecutionHostId(ownerEnvironmentId)
|
||||
}
|
||||
const rows = hostId ? [] : state.repos.filter((entry) => entry.id === repoId)
|
||||
// Why: with the id on two hosts, trusting one row's script could run the other's unseen.
|
||||
if (rows.length > 1) {
|
||||
throw new Error('The project exists on more than one host.')
|
||||
}
|
||||
const repo = rows[0] ?? null
|
||||
const ownerHostId = hostId ?? (repo ? getRepoExecutionHostId(repo) : null)
|
||||
if (!ownerHostId) {
|
||||
throw new Error('The project host is unknown.')
|
||||
}
|
||||
return ownerHostId
|
||||
}
|
||||
|
||||
function canUseRepoWideTrust(state: AppState, repoId: string): boolean {
|
||||
@@ -215,11 +220,7 @@ export async function readAndConfirmRuntimeIssueCommand(
|
||||
): Promise<ConfirmedRuntimeIssueCommand> {
|
||||
let result: IssueCommandReadResult
|
||||
try {
|
||||
result = await readRuntimeIssueCommand(
|
||||
settingsForHookRepoOwner(state, repoId, hostId),
|
||||
repoId,
|
||||
hostId
|
||||
)
|
||||
result = await readRuntimeIssueCommand(hookRepoOwnerHostId(state, repoId, hostId), repoId)
|
||||
} catch {
|
||||
result = {
|
||||
status: 'error',
|
||||
@@ -258,9 +259,8 @@ export async function ensureHooksConfirmed(
|
||||
// Why: hostId disambiguates duplicate repo ids on the local IPC path,
|
||||
// matching the checkRuntimeHooks call below.
|
||||
const result = await readRuntimeIssueCommand(
|
||||
settingsForHookRepoOwner(state, repoId, hostId, runtimeOwnerEnvironmentId),
|
||||
repoId,
|
||||
hostId
|
||||
hookRepoOwnerHostId(state, repoId, hostId, runtimeOwnerEnvironmentId),
|
||||
repoId
|
||||
)
|
||||
if (result.source === 'local') {
|
||||
return 'run'
|
||||
@@ -285,9 +285,8 @@ export async function ensureHooksConfirmed(
|
||||
return 'run'
|
||||
}
|
||||
const result = await checkRuntimeHooks(
|
||||
settingsForHookRepoOwner(state, repoId, hostId, runtimeOwnerEnvironmentId),
|
||||
repoId,
|
||||
hostId
|
||||
hookRepoOwnerHostId(state, repoId, hostId, runtimeOwnerEnvironmentId),
|
||||
repoId
|
||||
)
|
||||
if (result.status === 'error') {
|
||||
return 'skip'
|
||||
|
||||
@@ -7,6 +7,7 @@ const mocks = vi.hoisted(() => {
|
||||
resolvePrBase: vi.fn(),
|
||||
checkRuntimeHooks: vi.fn(),
|
||||
callRuntimeRpc: vi.fn(),
|
||||
ensureHooksConfirmed: vi.fn(),
|
||||
store
|
||||
}
|
||||
})
|
||||
@@ -14,7 +15,7 @@ const mocks = vi.hoisted(() => {
|
||||
vi.mock('@/store', () => ({ useAppStore: { getState: () => mocks.store } }))
|
||||
vi.mock('sonner', () => ({ toast: { error: vi.fn(), message: vi.fn() } }))
|
||||
vi.mock('@/lib/ensure-hooks-confirmed', () => ({
|
||||
ensureHooksConfirmed: vi.fn().mockResolvedValue('run')
|
||||
ensureHooksConfirmed: mocks.ensureHooksConfirmed
|
||||
}))
|
||||
vi.mock('@/lib/connection-context', () => ({ getConnectionId: () => null }))
|
||||
vi.mock('@/runtime/runtime-hooks-client', () => ({ checkRuntimeHooks: mocks.checkRuntimeHooks }))
|
||||
@@ -27,6 +28,7 @@ import { launchWorkItemDirect } from './launch-work-item-direct'
|
||||
describe('launchWorkItemDirect owner row', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.ensureHooksConfirmed.mockResolvedValue('run')
|
||||
vi.stubGlobal('window', { api: { worktrees: { resolvePrBase: mocks.resolvePrBase } } })
|
||||
mocks.checkRuntimeHooks.mockResolvedValue({
|
||||
hasHooks: false,
|
||||
@@ -67,7 +69,13 @@ describe('launchWorkItemDirect owner row', () => {
|
||||
item: { type: 'pr', number: 7, title: 'Fix', url: 'https://github.com/o/r/pull/7' }
|
||||
})
|
||||
|
||||
expect(mocks.checkRuntimeHooks).toHaveBeenCalledWith(null, 'repo-1', 'runtime:env-b')
|
||||
expect(mocks.checkRuntimeHooks).toHaveBeenCalledWith('runtime:env-b', 'repo-1')
|
||||
expect(mocks.ensureHooksConfirmed).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'repo-1',
|
||||
'setup',
|
||||
'runtime:env-b'
|
||||
)
|
||||
expect(mocks.callRuntimeRpc).toHaveBeenCalledWith(
|
||||
{ kind: 'environment', environmentId: 'env-b' },
|
||||
'worktree.resolvePrBase',
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function resolveDirectSetupDecision(
|
||||
let yamlHooks: OrcaHooks | null = null
|
||||
try {
|
||||
// Why: the same owner host as the PR start point and createWorktree, never focus.
|
||||
const result = await checkRuntimeHooks(null, repoId, ownerHostId)
|
||||
const result = await checkRuntimeHooks(ownerHostId, repoId)
|
||||
yamlHooks = (result.hooks as OrcaHooks | null) ?? null
|
||||
} catch {
|
||||
yamlHooks = null
|
||||
|
||||
@@ -118,7 +118,12 @@ export async function launchWorkItemDirect(args: LaunchWorkItemDirectArgs): Prom
|
||||
return false
|
||||
}
|
||||
|
||||
const trustDecision = await ensureHooksConfirmed(useAppStore.getState(), repoId, 'setup')
|
||||
const trustDecision = await ensureHooksConfirmed(
|
||||
useAppStore.getState(),
|
||||
repoId,
|
||||
'setup',
|
||||
repoOwnerHostId
|
||||
)
|
||||
const finalSetupDecision: SetupDecision =
|
||||
trustDecision === 'skip' ? 'skip' : setupResolution.decision
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useAppStore } from '@/store'
|
||||
import { getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
|
||||
import { runtimeTargetForOwnerHostId } from '@/runtime/runtime-client-target'
|
||||
import { runtimeTargetForRepoOwner } from '@/lib/repo-runtime-owner'
|
||||
import type { WorktreeStartupPayload } from '@/lib/worktree-startup-payload'
|
||||
import type {
|
||||
WorktreeCreationPhase,
|
||||
@@ -39,7 +40,11 @@ export function getWorktreeCreationIndeterminate(request: WorktreeCreationReques
|
||||
if (request.worktreeCreateProgressMode) {
|
||||
return request.worktreeCreateProgressMode === 'indeterminate'
|
||||
}
|
||||
return getActiveRuntimeTarget(useAppStore.getState().settings).kind !== 'local'
|
||||
// Why: progress follows the host the workspace is created on, not the focused one.
|
||||
const target = request.executionHostId
|
||||
? runtimeTargetForOwnerHostId(request.executionHostId)
|
||||
: runtimeTargetForRepoOwner(useAppStore.getState(), request.repoId)
|
||||
return target?.kind !== 'local'
|
||||
}
|
||||
|
||||
export function getInitialWorktreeCreationPhase(
|
||||
|
||||
@@ -54,19 +54,26 @@ describe('runtime hooks client', () => {
|
||||
source: 'none'
|
||||
})
|
||||
|
||||
await checkRuntimeHooks({ activeRuntimeEnvironmentId: null }, 'repo-1')
|
||||
await inspectRuntimeSetupScriptImports({ activeRuntimeEnvironmentId: null }, 'repo-1')
|
||||
await readRuntimeIssueCommand({ activeRuntimeEnvironmentId: null }, 'repo-1')
|
||||
await writeRuntimeIssueCommand({ activeRuntimeEnvironmentId: null }, 'repo-1', 'Fix it')
|
||||
await checkRuntimeHooks('local', 'repo-1')
|
||||
await inspectRuntimeSetupScriptImports('local', 'repo-1')
|
||||
await readRuntimeIssueCommand('local', 'repo-1')
|
||||
await writeRuntimeIssueCommand('local', 'repo-1', 'Fix it')
|
||||
|
||||
expect(hooksCheck).toHaveBeenCalledWith({ repoId: 'repo-1' })
|
||||
expect(hooksInspectSetupScriptImports).toHaveBeenCalledWith({ repoId: 'repo-1' })
|
||||
expect(hooksReadIssueCommand).toHaveBeenCalledWith({ repoId: 'repo-1' })
|
||||
expect(hooksWriteIssueCommand).toHaveBeenCalledWith({ repoId: 'repo-1', content: 'Fix it' })
|
||||
expect(hooksCheck).toHaveBeenCalledWith({ repoId: 'repo-1', hostId: 'local' })
|
||||
expect(hooksInspectSetupScriptImports).toHaveBeenCalledWith({
|
||||
repoId: 'repo-1',
|
||||
hostId: 'local'
|
||||
})
|
||||
expect(hooksReadIssueCommand).toHaveBeenCalledWith({ repoId: 'repo-1', hostId: 'local' })
|
||||
expect(hooksWriteIssueCommand).toHaveBeenCalledWith({
|
||||
repoId: 'repo-1',
|
||||
content: 'Fix it',
|
||||
hostId: 'local'
|
||||
})
|
||||
expect(runtimeEnvironmentCall).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('routes hook operations through the active runtime environment', async () => {
|
||||
it('routes hook operations through the repo owner runtime environment', async () => {
|
||||
runtimeEnvironmentCall.mockResolvedValue({
|
||||
id: 'rpc-1',
|
||||
ok: true,
|
||||
@@ -74,10 +81,10 @@ describe('runtime hooks client', () => {
|
||||
_meta: { runtimeId: 'runtime-1' }
|
||||
})
|
||||
|
||||
await checkRuntimeHooks({ activeRuntimeEnvironmentId: 'env-1' }, 'repo-1')
|
||||
await inspectRuntimeSetupScriptImports({ activeRuntimeEnvironmentId: 'env-1' }, 'repo-1')
|
||||
await readRuntimeIssueCommand({ activeRuntimeEnvironmentId: 'env-1' }, 'repo-1')
|
||||
await writeRuntimeIssueCommand({ activeRuntimeEnvironmentId: 'env-1' }, 'repo-1', 'Fix it')
|
||||
await checkRuntimeHooks('runtime:env-1', 'repo-1')
|
||||
await inspectRuntimeSetupScriptImports('runtime:env-1', 'repo-1')
|
||||
await readRuntimeIssueCommand('runtime:env-1', 'repo-1')
|
||||
await writeRuntimeIssueCommand('runtime:env-1', 'repo-1', 'Fix it')
|
||||
|
||||
expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(1, {
|
||||
selector: 'env-1',
|
||||
@@ -120,23 +127,10 @@ describe('runtime hooks client', () => {
|
||||
source: 'none'
|
||||
})
|
||||
|
||||
await checkRuntimeHooks(
|
||||
{ activeRuntimeEnvironmentId: 'focused-elsewhere' },
|
||||
'same-repo',
|
||||
'ssh:server'
|
||||
)
|
||||
await inspectRuntimeSetupScriptImports(
|
||||
{ activeRuntimeEnvironmentId: 'focused-elsewhere' },
|
||||
'same-repo',
|
||||
'ssh:server'
|
||||
)
|
||||
await readRuntimeIssueCommand({ activeRuntimeEnvironmentId: null }, 'same-repo', 'ssh:server')
|
||||
await writeRuntimeIssueCommand(
|
||||
{ activeRuntimeEnvironmentId: null },
|
||||
'same-repo',
|
||||
'Fix it',
|
||||
'ssh:server'
|
||||
)
|
||||
await checkRuntimeHooks('ssh:server', 'same-repo')
|
||||
await inspectRuntimeSetupScriptImports('ssh:server', 'same-repo')
|
||||
await readRuntimeIssueCommand('ssh:server', 'same-repo')
|
||||
await writeRuntimeIssueCommand('ssh:server', 'same-repo', 'Fix it')
|
||||
|
||||
expect(hooksCheck).toHaveBeenCalledWith({ repoId: 'same-repo', hostId: 'ssh:server' })
|
||||
expect(hooksInspectSetupScriptImports).toHaveBeenCalledWith({
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import type { GlobalSettings } from '../../../shared/global-settings-types'
|
||||
import type { OrcaHooks } from '../../../shared/orca-yaml-hook-types'
|
||||
import { parseExecutionHostId, type ExecutionHostId } from '../../../shared/execution-host'
|
||||
import type { ExecutionHostId } from '../../../shared/execution-host'
|
||||
import type { SetupScriptImportCandidate } from '../../../shared/setup-script-imports'
|
||||
import { callRuntimeRpc, getActiveRuntimeTarget } from './runtime-rpc-client'
|
||||
import { callRuntimeRpc } from './runtime-rpc-client'
|
||||
import { runtimeTargetForOwnerHostId, type RuntimeClientTarget } from './runtime-client-target'
|
||||
|
||||
function getHookInspectionTarget(
|
||||
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined,
|
||||
hostId?: ExecutionHostId
|
||||
): ReturnType<typeof getActiveRuntimeTarget> {
|
||||
const parsedHost = parseExecutionHostId(hostId)
|
||||
if (parsedHost?.kind === 'runtime') {
|
||||
return { kind: 'environment', environmentId: parsedHost.environmentId }
|
||||
/** The repo's own host: a server's runtime, or this app (with `hostId`) for local and SSH repos. */
|
||||
function getHookInspectionTarget(hostId: ExecutionHostId): RuntimeClientTarget {
|
||||
const target = runtimeTargetForOwnerHostId(hostId)
|
||||
if (!target) {
|
||||
throw new Error('The project host is unresolved.')
|
||||
}
|
||||
return parsedHost ? { kind: 'local' } : getActiveRuntimeTarget(settings)
|
||||
return target
|
||||
}
|
||||
|
||||
export type HookCheckResult = {
|
||||
@@ -32,13 +30,12 @@ export type IssueCommandReadResult = {
|
||||
}
|
||||
|
||||
export async function checkRuntimeHooks(
|
||||
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined,
|
||||
repoId: string,
|
||||
hostId?: ExecutionHostId
|
||||
hostId: ExecutionHostId,
|
||||
repoId: string
|
||||
): Promise<HookCheckResult> {
|
||||
const target = getHookInspectionTarget(settings, hostId)
|
||||
const target = getHookInspectionTarget(hostId)
|
||||
if (target.kind !== 'environment') {
|
||||
return window.api.hooks.check({ repoId, ...(hostId ? { hostId } : {}) })
|
||||
return window.api.hooks.check({ repoId, hostId })
|
||||
}
|
||||
return callRuntimeRpc<HookCheckResult>(
|
||||
target,
|
||||
@@ -49,13 +46,12 @@ export async function checkRuntimeHooks(
|
||||
}
|
||||
|
||||
export async function inspectRuntimeSetupScriptImports(
|
||||
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined,
|
||||
repoId: string,
|
||||
hostId?: ExecutionHostId
|
||||
hostId: ExecutionHostId,
|
||||
repoId: string
|
||||
): Promise<SetupScriptImportCandidate[]> {
|
||||
const target = getHookInspectionTarget(settings, hostId)
|
||||
const target = getHookInspectionTarget(hostId)
|
||||
if (target.kind !== 'environment') {
|
||||
return window.api.hooks.inspectSetupScriptImports({ repoId, ...(hostId ? { hostId } : {}) })
|
||||
return window.api.hooks.inspectSetupScriptImports({ repoId, hostId })
|
||||
}
|
||||
return callRuntimeRpc<SetupScriptImportCandidate[]>(
|
||||
target,
|
||||
@@ -66,13 +62,12 @@ export async function inspectRuntimeSetupScriptImports(
|
||||
}
|
||||
|
||||
export async function readRuntimeIssueCommand(
|
||||
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined,
|
||||
repoId: string,
|
||||
hostId?: ExecutionHostId
|
||||
hostId: ExecutionHostId,
|
||||
repoId: string
|
||||
): Promise<IssueCommandReadResult> {
|
||||
const target = getActiveRuntimeTarget(settings)
|
||||
const target = getHookInspectionTarget(hostId)
|
||||
if (target.kind !== 'environment') {
|
||||
return window.api.hooks.readIssueCommand({ repoId, ...(hostId ? { hostId } : {}) })
|
||||
return window.api.hooks.readIssueCommand({ repoId, hostId })
|
||||
}
|
||||
return callRuntimeRpc<IssueCommandReadResult>(
|
||||
target,
|
||||
@@ -83,14 +78,13 @@ export async function readRuntimeIssueCommand(
|
||||
}
|
||||
|
||||
export async function writeRuntimeIssueCommand(
|
||||
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined,
|
||||
hostId: ExecutionHostId,
|
||||
repoId: string,
|
||||
content: string,
|
||||
hostId?: ExecutionHostId
|
||||
content: string
|
||||
): Promise<void> {
|
||||
const target = getActiveRuntimeTarget(settings)
|
||||
const target = getHookInspectionTarget(hostId)
|
||||
if (target.kind !== 'environment') {
|
||||
await window.api.hooks.writeIssueCommand({ repoId, content, ...(hostId ? { hostId } : {}) })
|
||||
await window.api.hooks.writeIssueCommand({ repoId, content, hostId })
|
||||
return
|
||||
}
|
||||
await callRuntimeRpc(
|
||||
|
||||
@@ -38,7 +38,7 @@ afterEach(() => {
|
||||
describe('runtime repo client search bounds', () => {
|
||||
it('rejects oversized local base-ref searches before IPC', async () => {
|
||||
await expect(
|
||||
searchRuntimeRepoBaseRefs(null, 'repo-1', 'x'.repeat(3 * 1024), 20)
|
||||
searchRuntimeRepoBaseRefs('local', 'repo-1', 'x'.repeat(3 * 1024), 20)
|
||||
).resolves.toEqual([])
|
||||
|
||||
expect(searchBaseRefs).not.toHaveBeenCalled()
|
||||
@@ -48,7 +48,7 @@ describe('runtime repo client search bounds', () => {
|
||||
it('rejects oversized runtime base-ref detail searches before RPC', async () => {
|
||||
await expect(
|
||||
searchRuntimeRepoBaseRefDetails(
|
||||
{ activeRuntimeEnvironmentId: 'env-1' },
|
||||
'runtime:env-1',
|
||||
'repo-1',
|
||||
'secret-token-value'.repeat(256),
|
||||
20
|
||||
@@ -63,8 +63,8 @@ describe('runtime repo client search bounds', () => {
|
||||
getBaseRefDefault.mockResolvedValue({ defaultBaseRef: 'origin/main', remoteCount: 1 })
|
||||
searchBaseRefs.mockResolvedValue(['origin/main'])
|
||||
|
||||
await getRuntimeRepoBaseRefDefault(null, 'same-repo', 'ssh:server')
|
||||
await searchRuntimeRepoBaseRefs(null, 'same-repo', 'main', 20, 'ssh:server')
|
||||
await getRuntimeRepoBaseRefDefault('ssh:server', 'same-repo')
|
||||
await searchRuntimeRepoBaseRefs('ssh:server', 'same-repo', 'main', 20)
|
||||
|
||||
expect(getBaseRefDefault).toHaveBeenCalledWith({
|
||||
repoId: 'same-repo',
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
import type { GlobalSettings } from '../../../shared/global-settings-types'
|
||||
import type { BaseRefSearchResult } from '../../../shared/repo-types'
|
||||
import { legacyBaseRefSearchResult } from '../../../shared/base-ref-search-result'
|
||||
import { callRuntimeRpc, getActiveRuntimeTarget } from './runtime-rpc-client'
|
||||
import { callRuntimeRpc } from './runtime-rpc-client'
|
||||
import { runtimeTargetForOwnerHostId, type RuntimeClientTarget } from './runtime-client-target'
|
||||
import { isRuntimeRepoRefSearchQueryWithinLimit } from './runtime-repo-search-bounds'
|
||||
import type { ExecutionHostId } from '../../../shared/execution-host'
|
||||
|
||||
/** The repo's own host: a server's runtime, or this app (with `hostId`) for local and SSH repos. */
|
||||
function repoHostTarget(hostId: ExecutionHostId): RuntimeClientTarget {
|
||||
const target = runtimeTargetForOwnerHostId(hostId)
|
||||
if (!target) {
|
||||
throw new Error('The project host is unresolved.')
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
export type RuntimeRepoBaseRefDefault = {
|
||||
defaultBaseRef: string | null
|
||||
remoteCount: number
|
||||
}
|
||||
|
||||
export async function getRuntimeRepoBaseRefDefault(
|
||||
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined,
|
||||
repoId: string,
|
||||
hostId?: ExecutionHostId
|
||||
hostId: ExecutionHostId,
|
||||
repoId: string
|
||||
): Promise<RuntimeRepoBaseRefDefault> {
|
||||
const target = getActiveRuntimeTarget(settings)
|
||||
const target = repoHostTarget(hostId)
|
||||
if (target.kind !== 'environment') {
|
||||
return window.api.repos.getBaseRefDefault({ repoId, ...(hostId ? { hostId } : {}) })
|
||||
return window.api.repos.getBaseRefDefault({ repoId, hostId })
|
||||
}
|
||||
return callRuntimeRpc<RuntimeRepoBaseRefDefault>(
|
||||
target,
|
||||
@@ -28,18 +36,17 @@ export async function getRuntimeRepoBaseRefDefault(
|
||||
}
|
||||
|
||||
export async function searchRuntimeRepoBaseRefs(
|
||||
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined,
|
||||
hostId: ExecutionHostId,
|
||||
repoId: string,
|
||||
query: string,
|
||||
limit: number,
|
||||
hostId?: ExecutionHostId
|
||||
limit: number
|
||||
): Promise<string[]> {
|
||||
if (!isRuntimeRepoRefSearchQueryWithinLimit(query)) {
|
||||
return []
|
||||
}
|
||||
const target = getActiveRuntimeTarget(settings)
|
||||
const target = repoHostTarget(hostId)
|
||||
if (target.kind !== 'environment') {
|
||||
return window.api.repos.searchBaseRefs({ repoId, query, limit, ...(hostId ? { hostId } : {}) })
|
||||
return window.api.repos.searchBaseRefs({ repoId, query, limit, hostId })
|
||||
}
|
||||
const result = await callRuntimeRpc<{ refs: string[]; truncated: boolean }>(
|
||||
target,
|
||||
@@ -51,22 +58,21 @@ export async function searchRuntimeRepoBaseRefs(
|
||||
}
|
||||
|
||||
export async function searchRuntimeRepoBaseRefDetails(
|
||||
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined,
|
||||
hostId: ExecutionHostId,
|
||||
repoId: string,
|
||||
query: string,
|
||||
limit: number,
|
||||
hostId?: ExecutionHostId
|
||||
limit: number
|
||||
): Promise<BaseRefSearchResult[]> {
|
||||
if (!isRuntimeRepoRefSearchQueryWithinLimit(query)) {
|
||||
return []
|
||||
}
|
||||
const target = getActiveRuntimeTarget(settings)
|
||||
const target = repoHostTarget(hostId)
|
||||
if (target.kind !== 'environment') {
|
||||
return window.api.repos.searchBaseRefDetails({
|
||||
repoId,
|
||||
query,
|
||||
limit,
|
||||
...(hostId ? { hostId } : {})
|
||||
hostId
|
||||
})
|
||||
}
|
||||
const result = await callRuntimeRpc<{
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { ExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { AppState } from '../types'
|
||||
import type { LocalBaseRefRefreshResult } from '../../../../shared/worktree/base-ref-drift-types'
|
||||
import { toast } from 'sonner'
|
||||
import type { RuntimeEnvironmentCallRequest } from '../../runtime/runtime-compatibility-test-fixture'
|
||||
import { folderWorkspaceKey, worktreeWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import { makeWorkspaceLineage, makeWorktree } from './worktrees-slice-test-fixtures'
|
||||
import { TEST_REPO, makeWorkspaceLineage, makeWorktree } from './worktrees-slice-test-fixtures'
|
||||
import {
|
||||
createTestStore,
|
||||
mockApi,
|
||||
@@ -39,7 +40,7 @@ describe('createWorktree base status merge', () => {
|
||||
})
|
||||
|
||||
it('prefetches create base through desktop IPC on the local runtime target', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
|
||||
await store.getState().prefetchWorktreeCreateBase('repo1', 'origin/main')
|
||||
|
||||
@@ -51,10 +52,11 @@ describe('createWorktree base status merge', () => {
|
||||
})
|
||||
|
||||
it('prefetches create base through runtime RPC for remote runtime targets', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
store.setState({
|
||||
settings: { activeRuntimeEnvironmentId: 'remote-runtime' }
|
||||
} as Partial<AppState>)
|
||||
seedRepoHost(store, 'runtime:remote-runtime')
|
||||
runtimeEnvironmentCall.mockResolvedValue({ id: 'req', ok: true, result: null })
|
||||
|
||||
await store.getState().prefetchWorktreeCreateBase('repo1')
|
||||
@@ -71,7 +73,7 @@ describe('createWorktree base status merge', () => {
|
||||
it('marks the create payload as a generated name only when the caller says so', async () => {
|
||||
// Why: the host retires generated names permanently, and the creature pool contains ordinary
|
||||
// words ("orca", "runner", "molly"). A name the user typed must stay reusable.
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
mockApi.worktrees.create.mockResolvedValue({
|
||||
worktree: makeWorktree({ id: 'repo1::/path/wt1', repoId: 'repo1', path: '/path/wt1' })
|
||||
})
|
||||
@@ -114,7 +116,7 @@ describe('createWorktree base status merge', () => {
|
||||
})
|
||||
|
||||
it('passes linked work item and creation agent metadata through the create IPC payload', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
const wt = makeWorktree({
|
||||
id: 'repo1::/path/wt1',
|
||||
repoId: 'repo1',
|
||||
@@ -174,7 +176,7 @@ describe('createWorktree base status merge', () => {
|
||||
})
|
||||
|
||||
it('adopts an explicit provisioned root without calling ordinary worktree create', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
const adopted = makeWorktree({
|
||||
id: 'repo1::/workspace/repo',
|
||||
repoId: 'repo1',
|
||||
@@ -237,7 +239,7 @@ describe('createWorktree base status merge', () => {
|
||||
})
|
||||
|
||||
it('stamps the owning runtime host onto worktrees created on a remote runtime', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
const created = makeWorktree({
|
||||
id: 'repo-remote::/remote/feature',
|
||||
repoId: 'repo-remote',
|
||||
@@ -277,7 +279,7 @@ describe('createWorktree base status merge', () => {
|
||||
})
|
||||
|
||||
it('passes the active folder workspace as parent for in-app worktree creates', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
const wt = makeWorktree({
|
||||
id: 'repo1::/path/wt1',
|
||||
repoId: 'repo1',
|
||||
@@ -310,7 +312,7 @@ describe('createWorktree base status merge', () => {
|
||||
})
|
||||
|
||||
it('merges create result metadata into a worktree inserted by the watcher race', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
const watcherWorktree = makeWorktree({
|
||||
id: 'repo1::/path/wt1',
|
||||
repoId: 'repo1',
|
||||
@@ -335,7 +337,7 @@ describe('createWorktree base status merge', () => {
|
||||
})
|
||||
|
||||
it('requests a warning dialog when creation falls back to a local base', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
const wt = makeWorktree({
|
||||
id: 'repo1::/path/wt1',
|
||||
repoId: 'repo1',
|
||||
@@ -369,7 +371,7 @@ describe('createWorktree base status merge', () => {
|
||||
status: LocalBaseRefRefreshResult['status']
|
||||
expectedReason: string
|
||||
}[])('warns when local base ref refresh returns $status', async ({ status, expectedReason }) => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
const wt = makeWorktree({
|
||||
id: 'repo1::/path/wt1',
|
||||
repoId: 'repo1',
|
||||
@@ -410,7 +412,7 @@ describe('createWorktree base status merge', () => {
|
||||
})
|
||||
|
||||
it('falls back to the generic dirty detail when ownerWorktreePath is missing', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
const wt = makeWorktree({
|
||||
id: 'repo1::/path/wt1',
|
||||
repoId: 'repo1',
|
||||
@@ -435,7 +437,7 @@ describe('createWorktree base status merge', () => {
|
||||
})
|
||||
|
||||
it('names the toast from branch when displayName is blank', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
const wt = makeWorktree({
|
||||
id: 'repo1::/path/wt1',
|
||||
repoId: 'repo1',
|
||||
@@ -464,7 +466,8 @@ describe('createWorktree base status merge', () => {
|
||||
|
||||
// Creates that joined one refresh report the same fact, so they share one toast per repo+branch.
|
||||
it('reuses one toast id for every create of the same repo and base branch', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
store.setState({ repos: [TEST_REPO, { ...TEST_REPO, id: 'repo2' }] })
|
||||
const refresh = {
|
||||
status: 'skipped_error',
|
||||
baseRef: 'origin/main',
|
||||
@@ -490,7 +493,7 @@ describe('createWorktree base status merge', () => {
|
||||
})
|
||||
|
||||
it('does not warn when the local base ref refresh succeeds', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
const wt = makeWorktree({
|
||||
id: 'repo1::/path/wt1',
|
||||
repoId: 'repo1',
|
||||
@@ -511,7 +514,7 @@ describe('createWorktree base status merge', () => {
|
||||
})
|
||||
|
||||
it('does not warn when local base ref refresh is omitted from the create result', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
const wt = makeWorktree({
|
||||
id: 'repo1::/path/wt1',
|
||||
repoId: 'repo1',
|
||||
@@ -525,7 +528,7 @@ describe('createWorktree base status merge', () => {
|
||||
})
|
||||
|
||||
it('suggests turning on local main freshness when the create result reports a stale local base', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
const wt = makeWorktree({
|
||||
id: 'repo1::/path/wt1',
|
||||
repoId: 'repo1',
|
||||
@@ -554,7 +557,7 @@ describe('createWorktree base status merge', () => {
|
||||
})
|
||||
|
||||
it('persists the dismissal flag when the suggestion toast is closed or swiped', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
store.setState({
|
||||
settings: { refreshLocalBaseRefOnWorktreeCreate: false } as AppState['settings'],
|
||||
updateSettings: vi.fn().mockResolvedValue(undefined)
|
||||
@@ -580,7 +583,7 @@ describe('createWorktree base status merge', () => {
|
||||
})
|
||||
|
||||
it('does not record a dismissal on close when the feature is already enabled', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
store.setState({
|
||||
settings: { refreshLocalBaseRefOnWorktreeCreate: true } as AppState['settings'],
|
||||
updateSettings: vi.fn().mockResolvedValue(undefined)
|
||||
@@ -606,7 +609,7 @@ describe('createWorktree base status merge', () => {
|
||||
})
|
||||
|
||||
it('stamps manualOrder on create while Manual sort is active', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
store.setState({ sortBy: 'manual' } as Partial<AppState>)
|
||||
const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(123_456)
|
||||
const wt = makeWorktree({
|
||||
@@ -634,7 +637,7 @@ describe('createWorktree base status merge', () => {
|
||||
})
|
||||
|
||||
it('passes branchNameOverride through the local create IPC payload', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
const wt = makeWorktree({
|
||||
id: 'repo1::/path/feature-something',
|
||||
repoId: 'repo1',
|
||||
@@ -672,7 +675,7 @@ describe('createWorktree base status merge', () => {
|
||||
})
|
||||
|
||||
it('retries a suffixed branchNameOverride when local IPC reports a branch conflict', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
const error = new Error(
|
||||
'Branch "feature/something" already exists. Pick a different worktree name.'
|
||||
)
|
||||
@@ -721,7 +724,7 @@ describe('createWorktree base status merge', () => {
|
||||
})
|
||||
|
||||
it('does not overwrite a newer reconcile status with the initial checking status', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
const wt = makeWorktree({ id: 'repo1::/path/wt1', repoId: 'repo1', path: '/path/wt1' })
|
||||
mockApi.worktrees.create.mockImplementation(async () => {
|
||||
store.getState().updateWorktreeBaseStatus({
|
||||
@@ -753,3 +756,14 @@ describe('createWorktree base status merge', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Creation routes by the project row's own host, so every test seeds that row.
|
||||
function seedRepoHost(store: ReturnType<typeof createTestStore>, hostId: ExecutionHostId): void {
|
||||
store.setState({ repos: [{ ...TEST_REPO, executionHostId: hostId }] })
|
||||
}
|
||||
|
||||
function createRepoTestStore(): ReturnType<typeof createTestStore> {
|
||||
const store = createTestStore()
|
||||
seedRepoHost(store, 'local')
|
||||
return store
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { ExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { AppState } from '../types'
|
||||
import type { Worktree } from '../../../../shared/worktree/types'
|
||||
import { toast } from 'sonner'
|
||||
import type { RuntimeEnvironmentCallRequest } from '../../runtime/runtime-compatibility-test-fixture'
|
||||
import { worktreeWorkspaceKey, folderWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import { makeLineage, makeWorktree } from './worktrees-slice-test-fixtures'
|
||||
import { TEST_REPO, makeLineage, makeWorktree } from './worktrees-slice-test-fixtures'
|
||||
import {
|
||||
createTestStore,
|
||||
mockApi,
|
||||
@@ -36,7 +37,7 @@ describe('createWorktree composer parent pick', () => {
|
||||
})
|
||||
|
||||
function createParentPickStore(parent?: Worktree) {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
store.setState({ worktreesByRepo: { repo1: parent ? [parent] : [] } } as Partial<AppState>)
|
||||
return store
|
||||
}
|
||||
@@ -211,7 +212,7 @@ describe('createWorktree parent pick on a remote runtime', () => {
|
||||
})
|
||||
|
||||
function createRemoteStore() {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
store.setState({
|
||||
repos: [
|
||||
{
|
||||
@@ -386,3 +387,14 @@ describe('createWorktree parent pick on a remote runtime', () => {
|
||||
expect(toast.warning).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// Creation routes by the project row's own host, so every test seeds that row.
|
||||
function seedRepoHost(store: ReturnType<typeof createTestStore>, hostId: ExecutionHostId): void {
|
||||
store.setState({ repos: [{ ...TEST_REPO, executionHostId: hostId }] })
|
||||
}
|
||||
|
||||
function createRepoTestStore(): ReturnType<typeof createTestStore> {
|
||||
const store = createTestStore()
|
||||
seedRepoHost(store, 'local')
|
||||
return store
|
||||
}
|
||||
|
||||
@@ -633,6 +633,10 @@ describe('fetchWorktrees', () => {
|
||||
{ id: 'repo2', path: '/r2', displayName: 'R2', badgeColor: '#000', addedAt: 0 }
|
||||
]
|
||||
} as Partial<AppState>)
|
||||
// Ingest stamps server rows; the listing follows the row's host.
|
||||
store.setState({
|
||||
repos: store.getState().repos.map((repo) => ({ ...repo, executionHostId: 'runtime:env-1' }))
|
||||
})
|
||||
// Why: a mobile-scope device token is denied non-allowlisted runtime RPCs.
|
||||
runtimeEnvironmentCall.mockResolvedValue({
|
||||
id: 'rpc-1',
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { ExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { AppState } from '../types'
|
||||
import {
|
||||
createCompatibleRuntimeStatusResponse,
|
||||
type RuntimeEnvironmentCallRequest
|
||||
} from '../../runtime/runtime-compatibility-test-fixture'
|
||||
import { makeWorktree } from './worktrees-slice-test-fixtures'
|
||||
import { TEST_REPO, makeWorktree } from './worktrees-slice-test-fixtures'
|
||||
import {
|
||||
createTestStore,
|
||||
mockApi,
|
||||
@@ -39,7 +40,7 @@ describe('worktree remote runtime mutations', () => {
|
||||
})
|
||||
|
||||
it('creates worktrees through the active remote runtime environment', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
const wt = makeWorktree({
|
||||
id: 'repo1::/path/feature',
|
||||
repoId: 'repo1',
|
||||
@@ -55,6 +56,7 @@ describe('worktree remote runtime mutations', () => {
|
||||
settings: { activeRuntimeEnvironmentId: 'env-1' } as never,
|
||||
worktreesByRepo: { repo1: [] }
|
||||
} as Partial<AppState>)
|
||||
seedRepoHost(store, 'runtime:env-1')
|
||||
|
||||
const result = await store
|
||||
.getState()
|
||||
@@ -100,7 +102,7 @@ describe('worktree remote runtime mutations', () => {
|
||||
})
|
||||
|
||||
it('forwards generated-name provenance through paired-runtime create', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
const wt = makeWorktree({ id: 'repo1::/path/nautilus', repoId: 'repo1' })
|
||||
runtimeEnvironmentCall.mockResolvedValue({
|
||||
id: 'rpc-create',
|
||||
@@ -112,6 +114,7 @@ describe('worktree remote runtime mutations', () => {
|
||||
settings: { activeRuntimeEnvironmentId: 'env-1' } as never,
|
||||
worktreesByRepo: { repo1: [] }
|
||||
} as Partial<AppState>)
|
||||
seedRepoHost(store, 'runtime:env-1')
|
||||
const createWorktree = store.getState().createWorktree
|
||||
const args: Parameters<typeof createWorktree> = ['repo1', 'nautilus']
|
||||
args[25] = { nameWasGenerated: true }
|
||||
@@ -127,7 +130,7 @@ describe('worktree remote runtime mutations', () => {
|
||||
})
|
||||
|
||||
it('persists Jira item and source context through paired-runtime create', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
const wt = makeWorktree({
|
||||
id: 'repo1::/path/jira-link',
|
||||
repoId: 'repo1',
|
||||
@@ -143,6 +146,7 @@ describe('worktree remote runtime mutations', () => {
|
||||
settings: { activeRuntimeEnvironmentId: 'env-1' } as never,
|
||||
worktreesByRepo: { repo1: [] }
|
||||
} as Partial<AppState>)
|
||||
seedRepoHost(store, 'runtime:env-1')
|
||||
const linkedWorkItem = {
|
||||
provider: 'jira' as const,
|
||||
type: 'issue' as const,
|
||||
@@ -187,11 +191,12 @@ describe('worktree remote runtime mutations', () => {
|
||||
runtimeEnvironmentTransportCall.mockImplementation((args: RuntimeEnvironmentCallRequest) =>
|
||||
args.method === 'status.get' ? oldRuntimeStatus : runtimeEnvironmentCall(args)
|
||||
)
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
store.setState({
|
||||
settings: { activeRuntimeEnvironmentId: 'env-1' } as never,
|
||||
worktreesByRepo: { repo1: [] }
|
||||
} as Partial<AppState>)
|
||||
seedRepoHost(store, 'runtime:env-1')
|
||||
const createWorktree = store.getState().createWorktree
|
||||
const args: Parameters<typeof createWorktree> = ['repo1', 'jira-link']
|
||||
args[25] = {
|
||||
@@ -210,7 +215,7 @@ describe('worktree remote runtime mutations', () => {
|
||||
})
|
||||
|
||||
it('passes startup commands through remote runtime worktree creation', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
const wt = makeWorktree({
|
||||
id: 'repo1::/path/agent-startup',
|
||||
repoId: 'repo1',
|
||||
@@ -226,6 +231,7 @@ describe('worktree remote runtime mutations', () => {
|
||||
settings: { activeRuntimeEnvironmentId: 'env-1' } as never,
|
||||
worktreesByRepo: { repo1: [] }
|
||||
} as Partial<AppState>)
|
||||
seedRepoHost(store, 'runtime:env-1')
|
||||
|
||||
await store
|
||||
.getState()
|
||||
@@ -281,7 +287,7 @@ describe('worktree remote runtime mutations', () => {
|
||||
})
|
||||
|
||||
it('passes task startup drafts only to the owning remote runtime', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
const wt = makeWorktree({
|
||||
id: 'repo1::/path/task-draft',
|
||||
repoId: 'repo1',
|
||||
@@ -297,6 +303,7 @@ describe('worktree remote runtime mutations', () => {
|
||||
settings: { activeRuntimeEnvironmentId: 'env-1' } as never,
|
||||
worktreesByRepo: { repo1: [] }
|
||||
} as Partial<AppState>)
|
||||
seedRepoHost(store, 'runtime:env-1')
|
||||
const createWorktree = store.getState().createWorktree
|
||||
const args: Parameters<typeof createWorktree> = ['repo1', 'task-draft', undefined, 'inherit']
|
||||
args[10] = 'codex'
|
||||
@@ -317,7 +324,7 @@ describe('worktree remote runtime mutations', () => {
|
||||
})
|
||||
|
||||
it('passes startup commands through local worktree creation IPC', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
const wt = makeWorktree({
|
||||
id: 'repo1::/path/local-agent-startup',
|
||||
repoId: 'repo1',
|
||||
@@ -384,7 +391,7 @@ describe('worktree remote runtime mutations', () => {
|
||||
})
|
||||
|
||||
it('retries a suffixed branchNameOverride when runtime create reports a branch conflict', async () => {
|
||||
const store = createTestStore()
|
||||
const store = createRepoTestStore()
|
||||
const wt = makeWorktree({
|
||||
id: 'repo1::/path/feature-something-2',
|
||||
repoId: 'repo1',
|
||||
@@ -402,6 +409,7 @@ describe('worktree remote runtime mutations', () => {
|
||||
settings: { activeRuntimeEnvironmentId: 'env-1' } as never,
|
||||
worktreesByRepo: { repo1: [] }
|
||||
} as Partial<AppState>)
|
||||
seedRepoHost(store, 'runtime:env-1')
|
||||
|
||||
await store
|
||||
.getState()
|
||||
@@ -442,3 +450,14 @@ describe('worktree remote runtime mutations', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// Creation routes by the project row's own host, so every test seeds that row.
|
||||
function seedRepoHost(store: ReturnType<typeof createTestStore>, hostId: ExecutionHostId): void {
|
||||
store.setState({ repos: [{ ...TEST_REPO, executionHostId: hostId }] })
|
||||
}
|
||||
|
||||
function createRepoTestStore(): ReturnType<typeof createTestStore> {
|
||||
const store = createTestStore()
|
||||
seedRepoHost(store, 'local')
|
||||
return store
|
||||
}
|
||||
|
||||
@@ -188,6 +188,13 @@ describe('worktree remote runtime mutations', () => {
|
||||
runtimeOwnerEnvironmentId: 'owner-hub'
|
||||
})
|
||||
runtimeEnvironmentCall
|
||||
// Why: the archive-hook check runs on the HUB too; this app cannot reach its SSH target.
|
||||
.mockResolvedValueOnce({
|
||||
id: 'rpc-hooks-check',
|
||||
ok: true,
|
||||
result: { hasHooks: false, hooks: null, mayNeedUpdate: false },
|
||||
_meta: { runtimeId: 'runtime-owner-hub' }
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
id: 'rpc-rm-nested',
|
||||
ok: true,
|
||||
@@ -219,6 +226,12 @@ describe('worktree remote runtime mutations', () => {
|
||||
}
|
||||
})
|
||||
expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(1, {
|
||||
selector: 'owner-hub',
|
||||
method: 'repo.hooksCheck',
|
||||
params: { repo: 'repo-ssh' },
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(2, {
|
||||
selector: 'owner-hub',
|
||||
method: 'worktree.rm',
|
||||
params: {
|
||||
@@ -239,7 +252,7 @@ describe('worktree remote runtime mutations', () => {
|
||||
.forceDeletePreservedBranch(wt.id, 'feature/nested', 'saved-head')
|
||||
|
||||
expect(forceResult).toEqual({ ok: true, deleted: true })
|
||||
expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(2, {
|
||||
expect(runtimeEnvironmentCall).toHaveBeenNthCalledWith(3, {
|
||||
selector: 'owner-hub',
|
||||
method: 'worktree.forceDeleteBranch',
|
||||
params: {
|
||||
|
||||
@@ -14,13 +14,13 @@ import {
|
||||
import {
|
||||
assertRuntimeEnvironmentCapability,
|
||||
callRuntimeRpc,
|
||||
getActiveRuntimeTarget
|
||||
type RuntimeClientTarget
|
||||
} from '../../../../runtime/runtime-rpc-client'
|
||||
import { runtimeTargetForRepoOwner } from '@/lib/repo-runtime-owner'
|
||||
import { WORKTREE_LINKED_WORK_ITEM_CONTEXT_RUNTIME_CAPABILITY } from '../../../../../../shared/protocol-version'
|
||||
import { showLocalBaseRefUpdateSuggestionToast } from '@/components/sidebar/local-base-ref-suggestion-toast'
|
||||
import { requestWorktreeBaseFallbackNotice } from '@/components/worktree-base-fallback-notice'
|
||||
import { showLocalBaseRefRefreshToast } from './local-base-ref-refresh-toast'
|
||||
import { settingsForRepoOwner } from '../listing/worktree-owner-settings'
|
||||
import { applyCreatedWorktree } from './created-worktree-state-merge'
|
||||
import { isRuntimeLineageParentMissingError } from '../listing/runtime-worktree-rpc-errors'
|
||||
import {
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
type WorktreeCreateParentPick
|
||||
} from './worktree-create-parent-pick'
|
||||
|
||||
type RuntimeTarget = ReturnType<typeof getActiveRuntimeTarget>
|
||||
type RuntimeTarget = RuntimeClientTarget
|
||||
|
||||
type CreateAttemptOutcome = {
|
||||
result: CreateWorktreeResult
|
||||
@@ -180,7 +180,10 @@ export function createCreateWorktree(
|
||||
// Direct SSH still uses desktop IPC; paired runtimes use their captured environment.
|
||||
const target = options?.executionHostId
|
||||
? (runtimeTargetForExecutionHostId(options.executionHostId) ?? { kind: 'local' as const })
|
||||
: getActiveRuntimeTarget(settingsForRepoOwner(get(), repoId))
|
||||
: runtimeTargetForRepoOwner(get(), repoId)
|
||||
if (!target) {
|
||||
throw new Error('The project host is unresolved. Choose a host and retry.')
|
||||
}
|
||||
const creationHostId = adoptFromEndpoint(target, {
|
||||
kind: 'createdWorktree',
|
||||
requestedHostId: options?.executionHostId,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { WorktreeSlice } from '../../worktree-helpers'
|
||||
import type { WorktreeSliceGet, WorktreeSliceSet } from '../listing/worktree-slice-types'
|
||||
import { callRuntimeRpc, getActiveRuntimeTarget } from '../../../../runtime/runtime-rpc-client'
|
||||
import { settingsForRepoOwner } from '../listing/worktree-owner-settings'
|
||||
import { callRuntimeRpc } from '../../../../runtime/runtime-rpc-client'
|
||||
import { runtimeTargetForRepoOwner } from '@/lib/repo-runtime-owner'
|
||||
|
||||
export function createPrefetchWorktreeCreateBase(
|
||||
_set: WorktreeSliceSet,
|
||||
@@ -9,7 +9,10 @@ export function createPrefetchWorktreeCreateBase(
|
||||
): WorktreeSlice['prefetchWorktreeCreateBase'] {
|
||||
return async (repoId, baseBranch) => {
|
||||
try {
|
||||
const target = getActiveRuntimeTarget(settingsForRepoOwner(get(), repoId))
|
||||
const target = runtimeTargetForRepoOwner(get(), repoId)
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
if (target.kind === 'local') {
|
||||
await window.api.worktrees.prefetchCreateBase({
|
||||
repoId,
|
||||
|
||||
+20
-10
@@ -1,8 +1,14 @@
|
||||
import { callRuntimeRpc, getActiveRuntimeTarget } from '../../../../runtime/runtime-rpc-client'
|
||||
import { callRuntimeRpc } from '../../../../runtime/runtime-rpc-client'
|
||||
import {
|
||||
runtimeTargetForOwnerHostId,
|
||||
type RuntimeClientTarget
|
||||
} from '../../../../runtime/runtime-client-target'
|
||||
import { getEnvironmentSshStateGeneration } from '../../runtime-environment-ssh'
|
||||
import { getRuntimeEnvironmentConnectionGeneration } from '../../runtime-status'
|
||||
import { LOCAL_EXECUTION_HOST_ID } from '../../../../../../shared/execution-host'
|
||||
import type { AppState } from '../../../types'
|
||||
import {
|
||||
LOCAL_EXECUTION_HOST_ID,
|
||||
type ExecutionHostId
|
||||
} from '../../../../../../shared/execution-host'
|
||||
import type { DetectedWorktreeListResult } from '../../../../../../shared/worktree/types'
|
||||
import type { RuntimeWorktreeListResult } from '../../../../../../shared/runtime-types'
|
||||
import type {
|
||||
@@ -18,15 +24,20 @@ import { isRuntimeMethodNotFoundError } from './runtime-worktree-rpc-errors'
|
||||
import { toLegacyDetectedWorktreeResult } from './worktree-host-ownership'
|
||||
import { isWorktreeScanFailureKind } from '../../../../../../shared/worktree-scan-failure'
|
||||
|
||||
/** The listing host is the one the caller asked about, never the focused one. */
|
||||
export function detectedWorktreeTarget(executionHostId: ExecutionHostId): RuntimeClientTarget {
|
||||
const target = runtimeTargetForOwnerHostId(executionHostId)
|
||||
if (!target) {
|
||||
throw new Error('The workspace host is unresolved.')
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
export async function listDetectedWorktreesForRepo(
|
||||
settings: AppState['settings'],
|
||||
target: Extract<RuntimeClientTarget, { kind: 'environment' }>,
|
||||
repoId: string,
|
||||
options: BackgroundRuntimeRefreshOptions = {}
|
||||
): Promise<DetectedWorktreeListResult> {
|
||||
const target = getActiveRuntimeTarget(settings)
|
||||
if (target.kind === 'local') {
|
||||
throw new Error('Local detected-worktree reads require a provider lease')
|
||||
}
|
||||
try {
|
||||
return await callRuntimeRpc<DetectedWorktreeListResult>(
|
||||
target,
|
||||
@@ -55,11 +66,10 @@ export async function listDetectedWorktreesForRepo(
|
||||
}
|
||||
|
||||
export function detectedWorktreeRefreshKey(
|
||||
settings: AppState['settings'],
|
||||
repoId: string,
|
||||
options: DetectedWorktreeRefreshOptions
|
||||
): string {
|
||||
const target = getActiveRuntimeTarget(settings)
|
||||
const target = detectedWorktreeTarget(options.executionHostId)
|
||||
const targetKey = target.kind === 'local' ? 'local' : `runtime:${target.environmentId}`
|
||||
const parts = [
|
||||
repoId,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getActiveRuntimeTarget } from '../../../../runtime/runtime-rpc-client'
|
||||
import { getEnvironmentSshStateGeneration } from '../../runtime-environment-ssh'
|
||||
import { getRuntimeEnvironmentConnectionGeneration } from '../../runtime-status'
|
||||
import {
|
||||
@@ -10,7 +9,6 @@ import {
|
||||
parseExecutionHostId,
|
||||
type ExecutionHostId
|
||||
} from '../../../../../../shared/execution-host'
|
||||
import type { AppState } from '../../../types'
|
||||
import type { DetectedWorktreeListResult } from '../../../../../../shared/worktree/types'
|
||||
import type {
|
||||
HostQualifiedDetectedWorktreeResult,
|
||||
@@ -25,6 +23,7 @@ import { teardownMissingWorktreeTerminalsBestEffort } from '../teardown/missing-
|
||||
import { directSshAuthorityIsComplete } from './direct-ssh-authority'
|
||||
import {
|
||||
detectedWorktreeRefreshKey,
|
||||
detectedWorktreeTarget,
|
||||
isDetectedWorktreeListResult,
|
||||
listDetectedWorktreesForRepo,
|
||||
startDetectedWorktreeProviderRequest
|
||||
@@ -54,7 +53,6 @@ export const detectedWorktreeRefreshLeaseRegistry = createDetectedWorktreeRefres
|
||||
})
|
||||
|
||||
export function acquireDetectedWorktreeRefreshLeaseForRepo(
|
||||
settings: AppState['settings'],
|
||||
repoId: string,
|
||||
options: DetectedWorktreeRefreshOptions
|
||||
): DetectedWorktreeRefreshLease {
|
||||
@@ -62,7 +60,7 @@ export function acquireDetectedWorktreeRefreshLeaseForRepo(
|
||||
if (!parsedHost || parsedHost.kind === 'runtime') {
|
||||
throw new Error('Provider leases require a local or direct SSH execution host')
|
||||
}
|
||||
const publicKey = detectedWorktreeRefreshKey(settings, repoId, options)
|
||||
const publicKey = detectedWorktreeRefreshKey(repoId, options)
|
||||
if (parsedHost.kind === 'local') {
|
||||
return detectedWorktreeRefreshLeaseRegistry.acquire(publicKey, {
|
||||
repoId,
|
||||
@@ -143,19 +141,18 @@ export function normalizeNotAdmittedProviderResult(
|
||||
}
|
||||
|
||||
async function listDetectedWorktreesForRuntimeRepoOnce(
|
||||
settings: AppState['settings'],
|
||||
repoId: string,
|
||||
options: DetectedWorktreeRefreshOptions,
|
||||
environmentId: string
|
||||
): Promise<DetectedWorktreeRefreshOutcome> {
|
||||
// Why recomputed per attempt: the key embeds both generations, so a retry after a
|
||||
// reconnect must not join the superseded connection's in-flight scan.
|
||||
const key = detectedWorktreeRefreshKey(settings, repoId, options)
|
||||
const key = detectedWorktreeRefreshKey(repoId, options)
|
||||
const connectionGeneration = getEnvironmentSshStateGeneration(environmentId)
|
||||
const runtimeConnectionGeneration = getRuntimeEnvironmentConnectionGeneration(environmentId)
|
||||
let refresh = runtimeDetectedWorktreeRefreshesInFlight.get(key)
|
||||
if (!refresh) {
|
||||
refresh = listDetectedWorktreesForRepo(settings, repoId, {
|
||||
refresh = listDetectedWorktreesForRepo({ kind: 'environment', environmentId }, repoId, {
|
||||
reuseRecentCompatibilityFailure: options.reuseRecentCompatibilityFailure
|
||||
})
|
||||
runtimeDetectedWorktreeRefreshesInFlight.set(key, refresh)
|
||||
@@ -175,7 +172,7 @@ async function listDetectedWorktreesForRuntimeRepoOnce(
|
||||
// not stop terminals either.
|
||||
if (!options.isStaleCatalogPublication?.(result)) {
|
||||
await teardownMissingWorktreeTerminalsBestEffort(
|
||||
settings,
|
||||
{ kind: 'environment', environmentId },
|
||||
repoId,
|
||||
options.connectionId,
|
||||
options.knownWorktreeIds,
|
||||
@@ -200,20 +197,14 @@ async function listDetectedWorktreesForRuntimeRepoOnce(
|
||||
}
|
||||
|
||||
export async function listDetectedWorktreesForRepoCoalesced(
|
||||
settings: AppState['settings'],
|
||||
repoId: string,
|
||||
options: DetectedWorktreeRefreshOptions
|
||||
): Promise<DetectedWorktreeRefreshOutcome> {
|
||||
const target = getActiveRuntimeTarget(settings)
|
||||
const target = detectedWorktreeTarget(options.executionHostId)
|
||||
if (target.kind === 'environment') {
|
||||
for (let attempt = 0; ; attempt += 1) {
|
||||
try {
|
||||
return await listDetectedWorktreesForRuntimeRepoOnce(
|
||||
settings,
|
||||
repoId,
|
||||
options,
|
||||
target.environmentId
|
||||
)
|
||||
return await listDetectedWorktreesForRuntimeRepoOnce(repoId, options, target.environmentId)
|
||||
} catch (err) {
|
||||
// Why re-read instead of surfacing: the fence proves this answer predates the
|
||||
// current connection, not that the repo has no worktrees. Callers drop the repo
|
||||
@@ -226,7 +217,7 @@ export async function listDetectedWorktreesForRepoCoalesced(
|
||||
}
|
||||
}
|
||||
|
||||
const lease = acquireDetectedWorktreeRefreshLeaseForRepo(settings, repoId, options)
|
||||
const lease = acquireDetectedWorktreeRefreshLeaseForRepo(repoId, options)
|
||||
let providerResult: HostQualifiedDetectedWorktreeResult
|
||||
try {
|
||||
providerResult = await lease.result
|
||||
@@ -258,7 +249,7 @@ export async function listDetectedWorktreesForRepoCoalesced(
|
||||
}
|
||||
if (!options.isStaleCatalogPublication?.(providerResult.result)) {
|
||||
await teardownMissingWorktreeTerminalsBestEffort(
|
||||
settings,
|
||||
target,
|
||||
repoId,
|
||||
options.connectionId,
|
||||
options.knownWorktreeIds,
|
||||
|
||||
@@ -19,7 +19,6 @@ import { fetchKnownSshWorktreesForRepo } from './known-ssh-worktree-fetch'
|
||||
import { mergeFetchedWorktrees } from './fetched-worktree-merge'
|
||||
import { notifyRuntimeScopeForbiddenIfNeeded } from './runtime-scope-forbidden-toast'
|
||||
import { mapReposForWorktreeRefresh } from './worktree-refresh-pool'
|
||||
import { settingsForKnownRepoOwner } from './worktree-owner-settings'
|
||||
|
||||
export function createFetchAllWorktrees(
|
||||
set: WorktreeSliceSet,
|
||||
@@ -44,7 +43,6 @@ export function createFetchAllWorktrees(
|
||||
const requestStartedWorktrees = requestStartedState.worktreesByRepo[r.id]
|
||||
const hostId = getRepoExecutionHostId(r)
|
||||
const setup = getProjectHostSetupForRepoHost(requestStartedState, r.id, hostId)
|
||||
const settings = settingsForKnownRepoOwner(requestStartedState.settings, r)
|
||||
const parsedHost = parseExecutionHostId(hostId)
|
||||
const directSshAuthority =
|
||||
parsedHost?.kind === 'ssh'
|
||||
@@ -54,7 +52,7 @@ export function createFetchAllWorktrees(
|
||||
await fetchKnownSshWorktreesForRepo(set, r.id, parsedHost.id)
|
||||
return
|
||||
}
|
||||
const refresh = await listDetectedWorktreesForRepoCoalesced(settings, r.id, {
|
||||
const refresh = await listDetectedWorktreesForRepoCoalesced(r.id, {
|
||||
executionHostId: hostId,
|
||||
reuseRecentCompatibilityFailure: true,
|
||||
directSshAuthority,
|
||||
@@ -113,19 +111,15 @@ export function createFetchAllWorktrees(
|
||||
await fetchKnownSshWorktreesForRepo(set, r.id, parsedHost.id)
|
||||
return { repoId: r.id, ok: false as const }
|
||||
}
|
||||
const refresh = await listDetectedWorktreesForRepoCoalesced(
|
||||
settingsForKnownRepoOwner(requestStartedState.settings, r),
|
||||
r.id,
|
||||
{
|
||||
executionHostId: hostId,
|
||||
reuseRecentCompatibilityFailure: true,
|
||||
directSshAuthority,
|
||||
connectionId: r.connectionId,
|
||||
knownWorktreeIds: getKnownWorktreeIdsForPurge(requestStartedState, r.id, hostId),
|
||||
isStaleCatalogPublication: (result) =>
|
||||
isStaleWorktreeCatalogPublication(get(), r.id, hostId, result.catalogVersion)
|
||||
}
|
||||
)
|
||||
const refresh = await listDetectedWorktreesForRepoCoalesced(r.id, {
|
||||
executionHostId: hostId,
|
||||
reuseRecentCompatibilityFailure: true,
|
||||
directSshAuthority,
|
||||
connectionId: r.connectionId,
|
||||
knownWorktreeIds: getKnownWorktreeIdsForPurge(requestStartedState, r.id, hostId),
|
||||
isStaleCatalogPublication: (result) =>
|
||||
isStaleWorktreeCatalogPublication(get(), r.id, hostId, result.catalogVersion)
|
||||
})
|
||||
if (refresh.status !== 'admitted') {
|
||||
return { repoId: r.id, ok: false as const }
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
} from './worktree-host-ownership'
|
||||
import { fetchKnownSshWorktreesForRepo } from './known-ssh-worktree-fetch'
|
||||
import { notifyRuntimeScopeForbiddenIfNeeded } from './runtime-scope-forbidden-toast'
|
||||
import { settingsForRepoOwner } from './worktree-owner-settings'
|
||||
|
||||
export function createFetchDetectedWorktrees(
|
||||
set: WorktreeSliceSet,
|
||||
@@ -46,18 +45,14 @@ export function createFetchDetectedWorktrees(
|
||||
await fetchKnownSshWorktreesForRepo(set, repoId, parsedHost.id)
|
||||
return null
|
||||
}
|
||||
const refresh = await listDetectedWorktreesForRepoCoalesced(
|
||||
settingsForRepoOwner(ownerState, repoId, hostId),
|
||||
repoId,
|
||||
{
|
||||
executionHostId: hostId,
|
||||
directSshAuthority,
|
||||
connectionId: repoOwner?.connectionId,
|
||||
knownWorktreeIds: getKnownWorktreeIdsForPurge(ownerState, repoId, hostId),
|
||||
isStaleCatalogPublication: (result) =>
|
||||
isStaleWorktreeCatalogPublication(get(), repoId, hostId, result.catalogVersion)
|
||||
}
|
||||
)
|
||||
const refresh = await listDetectedWorktreesForRepoCoalesced(repoId, {
|
||||
executionHostId: hostId,
|
||||
directSshAuthority,
|
||||
connectionId: repoOwner?.connectionId,
|
||||
knownWorktreeIds: getKnownWorktreeIdsForPurge(ownerState, repoId, hostId),
|
||||
isStaleCatalogPublication: (result) =>
|
||||
isStaleWorktreeCatalogPublication(get(), repoId, hostId, result.catalogVersion)
|
||||
})
|
||||
if (refresh.status !== 'admitted') {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import { fetchKnownSshWorktreesForRepo } from './known-ssh-worktree-fetch'
|
||||
import { mergeFetchedWorktrees } from './fetched-worktree-merge'
|
||||
import { notifyRuntimeScopeForbiddenIfNeeded } from './runtime-scope-forbidden-toast'
|
||||
import { refreshRemoteWorktreeLineageBestEffort } from '../metadata/worktree-lineage-refresh'
|
||||
import { settingsForRepoOwner } from './worktree-owner-settings'
|
||||
|
||||
export function createFetchWorktrees(
|
||||
set: WorktreeSliceSet,
|
||||
@@ -71,16 +70,6 @@ export function createFetchWorktrees(
|
||||
hostId,
|
||||
settings: ownerState.settings
|
||||
})
|
||||
const ownerSettings = settingsForRepoOwner(
|
||||
ownerState,
|
||||
repoId,
|
||||
hostId,
|
||||
ownerWasMissingAtStart && (useLocalOwner || options?.executionHostId !== undefined)
|
||||
)
|
||||
const settings =
|
||||
useLocalOwner && ownerSettings?.activeRuntimeEnvironmentId
|
||||
? { ...ownerSettings, activeRuntimeEnvironmentId: null }
|
||||
: ownerSettings
|
||||
const parsedHost = parseExecutionHostId(hostId)
|
||||
const directSshAuthority =
|
||||
parsedHost?.kind === 'ssh'
|
||||
@@ -94,7 +83,7 @@ export function createFetchWorktrees(
|
||||
}
|
||||
return false
|
||||
}
|
||||
const refresh = await listDetectedWorktreesForRepoCoalesced(settings, repoId, {
|
||||
const refresh = await listDetectedWorktreesForRepoCoalesced(repoId, {
|
||||
executionHostId: hostId,
|
||||
requireAuthoritative: options?.requireAuthoritative,
|
||||
directSshAuthority,
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
worktreeHostMatchOptions,
|
||||
worktreeMatchesHost
|
||||
} from './worktree-host-ownership'
|
||||
import { settingsForRepoOwner } from './worktree-owner-settings'
|
||||
import { getCurrentDirectSshAuthority } from './direct-ssh-authority'
|
||||
import {
|
||||
acquireDetectedWorktreeRefreshLeaseForRepo,
|
||||
@@ -210,17 +209,12 @@ export function acquireDirectSshDetectedWorktreeRefresh(
|
||||
request.repoId,
|
||||
request.executionHostId
|
||||
)
|
||||
const settings = settingsForRepoOwner(
|
||||
requestStartedState,
|
||||
request.repoId,
|
||||
request.executionHostId
|
||||
)
|
||||
const options: DetectedWorktreeRefreshOptions = {
|
||||
executionHostId: request.executionHostId,
|
||||
directSshAuthority: request.authority,
|
||||
requireAuthoritative: request.requireAuthoritative
|
||||
}
|
||||
const lease = acquireDetectedWorktreeRefreshLeaseForRepo(settings, request.repoId, options)
|
||||
const lease = acquireDetectedWorktreeRefreshLeaseForRepo(request.repoId, options)
|
||||
let mergedResult: HostQualifiedDetectedWorktreeResult | undefined
|
||||
|
||||
return {
|
||||
|
||||
@@ -3,13 +3,14 @@ import { parseExecutionHostId } from '../../../../../../shared/execution-host'
|
||||
import type { WorktreeSliceGet, WorktreeSliceSet } from '../listing/worktree-slice-types'
|
||||
import { toast } from 'sonner'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { callRuntimeRpc, getActiveRuntimeTarget } from '../../../../runtime/runtime-rpc-client'
|
||||
import { callRuntimeRpc } from '../../../../runtime/runtime-rpc-client'
|
||||
import { runtimeTargetForWorkspaceOwner } from '@/lib/resolve-owner'
|
||||
import { toRuntimeWorktreeSelector } from '../../../../runtime/runtime-worktree-selector'
|
||||
import { preservedBranchCleanupKey } from '../../../../../../shared/preserved-branch-cleanup'
|
||||
import type { ForceDeleteWorktreeBranchResult } from '../../../../../../shared/worktree/create-types'
|
||||
import type { PreservedBranchCleanup } from '../../../../../../shared/preserved-branch-cleanup'
|
||||
import { preservedBranchRuntimeTargetByCleanupKey } from './preserved-branch-cleanup-target'
|
||||
import { settingsForWorktreeOwner } from '../listing/worktree-owner-settings'
|
||||
import { WORKTREE_REMOVAL_AMBIGUOUS_ERROR } from '../listing/worktree-slice-constants'
|
||||
|
||||
export function createForceDeletePreservedBranch(
|
||||
_set: WorktreeSliceSet,
|
||||
@@ -59,8 +60,10 @@ export function createForceDeletePreservedBranch(
|
||||
const cleanupHostId = options?.hostId ?? retainedTarget?.cleanup.hostId
|
||||
// Why: the removed row no longer records its nested HUB owner, so retain the deletion-time route.
|
||||
const target =
|
||||
retainedTarget?.target ??
|
||||
getActiveRuntimeTarget(settingsForWorktreeOwner(get(), worktreeId))
|
||||
retainedTarget?.target ?? runtimeTargetForWorkspaceOwner(get(), { workspaceId: worktreeId })
|
||||
if (!target) {
|
||||
throw new Error(WORKTREE_REMOVAL_AMBIGUOUS_ERROR)
|
||||
}
|
||||
const parsedCleanupHost = parseExecutionHostId(cleanupHostId)
|
||||
const effectiveHostId =
|
||||
target.kind === 'environment' &&
|
||||
|
||||
+3
-4
@@ -1,5 +1,4 @@
|
||||
import { callRuntimeRpc, getActiveRuntimeTarget } from '../../../../runtime/runtime-rpc-client'
|
||||
import type { AppState } from '../../../types'
|
||||
import { callRuntimeRpc, type RuntimeClientTarget } from '../../../../runtime/runtime-rpc-client'
|
||||
import type { DetectedWorktreeListResult } from '../../../../../../shared/worktree/types'
|
||||
import { isRuntimeMethodNotFoundError } from '../listing/runtime-worktree-rpc-errors'
|
||||
|
||||
@@ -9,7 +8,8 @@ import { isRuntimeMethodNotFoundError } from '../listing/runtime-worktree-rpc-er
|
||||
const missingWorktreeTeardownsInFlight = new Map<string, Promise<void>>()
|
||||
|
||||
export async function teardownMissingWorktreeTerminalsBestEffort(
|
||||
settings: AppState['settings'],
|
||||
/** The host whose listing found the worktrees missing. */
|
||||
target: RuntimeClientTarget,
|
||||
repoId: string,
|
||||
connectionId: string | null | undefined,
|
||||
// Why: refreshes that never purge omit this; an absent snapshot means
|
||||
@@ -25,7 +25,6 @@ export async function teardownMissingWorktreeTerminalsBestEffort(
|
||||
if (missingIds.length === 0) {
|
||||
return
|
||||
}
|
||||
const target = getActiveRuntimeTarget(settings)
|
||||
const normalizedConnectionId = connectionId ?? null
|
||||
const key = [
|
||||
target.kind === 'local' ? 'local' : `runtime:${target.environmentId}`,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { RemoveWorktreeResult } from '../../../../../../shared/worktree/cre
|
||||
import { getRepoIdFromWorktreeId } from '../../worktree-helpers'
|
||||
import { parseExecutionHostId } from '../../../../../../shared/execution-host'
|
||||
import { ensureHooksConfirmed } from '@/lib/ensure-hooks-confirmed'
|
||||
import { getActiveRuntimeTarget } from '../../../../runtime/runtime-rpc-client'
|
||||
import { runtimeTargetForOwnerEnvironment } from '../../../../runtime/runtime-client-target'
|
||||
import { forgetHugeRepoWarningDismissalsForWorktrees } from '@/lib/source-control-huge-repo-warning-dismissals'
|
||||
import { forgetWorktreeSleepIntent } from '@/lib/worktree-sleep-intent'
|
||||
import { readIpcErrorDetail } from '@/lib/ipc-error'
|
||||
@@ -12,8 +12,7 @@ import { isArchiveHookRemovalError } from '../../../../../../shared/worktree/arc
|
||||
import { showPreservedBranchToast } from '@/components/sidebar/preserved-branch-toast'
|
||||
import {
|
||||
resolveWorktreeOperationRouteResult,
|
||||
resolveWorktreeOperationRouteResultForHost,
|
||||
settingsForWorktreeOperationRoute
|
||||
resolveWorktreeOperationRouteResultForHost
|
||||
} from '@/lib/worktree-operation-route'
|
||||
import {
|
||||
beginHostQualifiedRemoval,
|
||||
@@ -115,13 +114,7 @@ export function createRemoveWorktree(
|
||||
removalGenerationGuard?.assertCurrent()
|
||||
}
|
||||
// Why: forget-local clears Orca's records via local IPC regardless of host — the remote is gone or unreachable.
|
||||
const target = getActiveRuntimeTarget(
|
||||
removalRoute
|
||||
? settingsForWorktreeOperationRoute(get().settings, removalRoute)
|
||||
: get().settings
|
||||
? { ...get().settings, activeRuntimeEnvironmentId: null }
|
||||
: { activeRuntimeEnvironmentId: null }
|
||||
)
|
||||
const target = runtimeTargetForOwnerEnvironment(removalRoute?.runtimeEnvironmentId ?? null)
|
||||
const unprovableRemoteRouting = policy.forgetLocalOnly
|
||||
? null
|
||||
: refuseUnprovableRemoteHostRouting(get, worktreeId, target.kind)
|
||||
|
||||
Reference in New Issue
Block a user