fix(setup): route prompts from active workspace host

This commit is contained in:
Brennan Benson
2026-07-29 18:57:50 -07:00
parent 406fb12001
commit 9d285155ea
8 changed files with 165 additions and 16 deletions
@@ -121,6 +121,7 @@ import {
buildRepoIdToRepresentative,
buildSettingsProjectList,
getSettingsProjectHostRepo,
getSettingsTargetHostSelection,
removeSettingsProjectFromAllHosts,
resolveSettingsTargetRepoId
} from './settings-project-list'
@@ -642,9 +643,21 @@ function Settings(): React.JSX.Element {
repoIdToHostSelection.keys()
)
if (targetRepoId) {
const hostSelection = repoIdToHostSelection.get(targetRepoId)
const hostSelection = settingsNavigationTarget.hostId
? getSettingsTargetHostSelection(
settingsProjectList,
targetRepoId,
settingsNavigationTarget.hostId
)
: repoIdToHostSelection.get(targetRepoId)
if (hostSelection) {
setSettingsProjectHostSelection(hostSelection.projectId, hostSelection.hostId)
setSettingsProjectHostSelection(
hostSelection.projectId,
hostSelection.hostId,
'setupId' in hostSelection && typeof hostSelection.setupId === 'string'
? hostSelection.setupId
: undefined
)
}
}
pendingNavSectionRef.current = paneSectionId
@@ -678,6 +691,7 @@ function Settings(): React.JSX.Element {
repoIdToRepresentative,
setSettingsProjectHostSelection,
settings,
settingsProjectList,
settingsNavigationTarget
])
@@ -6,6 +6,7 @@ import {
buildSettingsProjectList,
getSettingsProjectHostRepo,
getSettingsProjectRepresentativeRepoId,
getSettingsTargetHostSelection,
removeSettingsProjectFromAllHosts,
resolveEffectiveProjectHost,
resolveSettingsTargetRepoId
@@ -159,6 +160,26 @@ describe('deep-link resolution', () => {
})
})
it('uses an explicit host when same-id repo rows collide', () => {
const sameIdProjects = buildSettingsProjectList([
makeRepo({ id: 'same-repo', gitRemoteIdentity: gitRemote }),
makeRepo({
id: 'same-repo',
gitRemoteIdentity: gitRemote,
executionHostId: 'ssh:server',
connectionId: 'server',
path: '/remote/repo'
})
])
expect(getSettingsTargetHostSelection(sameIdProjects, 'same-repo', 'ssh:server')).toEqual(
expect.objectContaining({
projectId: sameIdProjects[0].projectId,
hostId: 'ssh:server'
})
)
})
it('parses a repoId from a host-specific subsection sectionId', () => {
const repoIds = [...buildRepoIdToHostSelection(projects).keys()]
expect(
@@ -123,6 +123,22 @@ export function buildRepoIdToHostSelection(
return map
}
export function getSettingsTargetHostSelection(
projects: readonly SettingsProject[],
repoId: string,
hostId: ExecutionHostId
): { projectId: string; hostId: ExecutionHostId; setupId: string } | null {
for (const settingsProject of projects) {
const setup = settingsProject.setups.find(
(candidate) => candidate.repoId === repoId && candidate.hostId === hostId
)
if (setup) {
return { projectId: settingsProject.projectId, hostId, setupId: setup.id }
}
}
return null
}
/**
* The repo row a Settings deep link points at, from either an explicit repoId
* or a `repo-<id>-<subsection>` sectionId. repo ids can contain hyphens, so the
@@ -1,10 +1,12 @@
import { describe, expect, it } from 'vitest'
import {
findSetupScriptPromptRepo,
getRenderedSetupScriptPromptState,
markSetupScriptPromptSaved
} from './setup-script-prompt-render-state'
import type { SetupScriptPromptInspection } from '@/lib/setup-script-prompt'
import { getRepoHostIdentityForParts } from '@/store/slices/repo-host-identity'
import type { Repo } from '../../../../shared/types'
function repoIdentity(repoId: string, hostId: string): string {
return getRepoHostIdentityForParts(repoId, hostId)
@@ -24,6 +26,64 @@ function prompt(
}
}
describe('findSetupScriptPromptRepo', () => {
it('uses the active direct-SSH worktree host when repo ids collide', () => {
const local: Repo = {
id: 'same-repo',
path: '/local',
displayName: 'Local',
badgeColor: '#000',
addedAt: 1
}
const ssh: Repo = {
...local,
path: '/ssh',
displayName: 'SSH',
connectionId: 'server',
executionHostId: 'ssh:server'
}
expect(
findSetupScriptPromptRepo({
repos: [local, ssh],
activeRepoId: 'same-repo',
activeWorktree: { repoId: 'same-repo', hostId: 'ssh:server' },
settings: { activeRuntimeEnvironmentId: null }
})
).toBe(ssh)
})
it('uses the runtime owner for a relayed SSH worktree', () => {
const directSsh: Repo = {
id: 'same-repo',
path: '/direct',
displayName: 'Direct SSH',
badgeColor: '#000',
addedAt: 1,
connectionId: 'private'
}
const runtime: Repo = {
...directSsh,
path: '/runtime',
displayName: 'Runtime',
executionHostId: 'runtime:hub'
}
expect(
findSetupScriptPromptRepo({
repos: [directSsh, runtime],
activeRepoId: 'same-repo',
activeWorktree: {
repoId: 'same-repo',
hostId: 'ssh:private',
runtimeOwnerEnvironmentId: 'hub'
},
settings: { activeRuntimeEnvironmentId: null }
})
).toBe(runtime)
})
})
describe('getRenderedSetupScriptPromptState', () => {
it('uses the current inspection when it belongs to the active repo and host', () => {
const current = prompt('repo-local', 'local')
@@ -20,6 +20,7 @@ import { showSavedInProjectSettingsToast } from './SetupScriptPromptToast'
import { openSetupScriptSettings } from './open-setup-script-settings'
import { trackSetupScriptPromptExposure } from './setup-script-prompt-exposure-telemetry'
import {
findSetupScriptPromptRepo,
getRenderedSetupScriptPromptState,
markSetupScriptPromptSaved,
type LastVisibleSetupScriptPrompt,
@@ -27,13 +28,16 @@ import {
} from './setup-script-prompt-render-state'
import { useSetupScriptPromptRevalidation } from './useSetupScriptPromptRevalidation'
import { translate } from '@/i18n/i18n'
import { findRepoForHost, getRepoHostIdentity } from '@/store/slices/repo-host-identity'
import { getRepoHostIdentity } from '@/store/slices/repo-host-identity'
import { getRepoExecutionHostId } from '../../../../shared/execution-host'
import { useWorktreeById } from '@/store/selectors'
function SetupScriptPromptCard(): React.JSX.Element | null {
const sidebarOpen = useAppStore((s) => s.sidebarOpen)
const repos = useAppStore((s) => s.repos)
const activeRepoId = useAppStore((s) => s.activeRepoId)
const activeWorktreeId = useAppStore((s) => s.activeWorktreeId)
const activeWorktree = useWorktreeById(activeWorktreeId)
const settings = useAppStore((s) => s.settings)
const updateRepo = useAppStore((s) => s.updateRepo)
const openSettingsPage = useAppStore((s) => s.openSettingsPage)
@@ -49,8 +53,8 @@ function SetupScriptPromptCard(): React.JSX.Element | null {
const mountedRef = useMountedRef()
const activeRepo = useMemo(
() => (activeRepoId ? findRepoForHost(repos, activeRepoId, { settings }) : null),
[activeRepoId, repos, settings]
() => findSetupScriptPromptRepo({ repos, activeRepoId, activeWorktree, settings }),
[activeRepoId, activeWorktree, repos, settings]
)
const activeRepoHostIdentity = activeRepo ? getRepoHostIdentity(activeRepo) : null
const isDismissed = activeRepoHostIdentity
@@ -98,14 +102,14 @@ function SetupScriptPromptCard(): React.JSX.Element | null {
}, [activeRepo, inspectionRetryKey, isDismissed, settings, sidebarOpen])
const openLocalCommandSettings = useCallback(
(repoId: string) => {
(repoId: string, hostId: ReturnType<typeof getRepoExecutionHostId>) =>
openSetupScriptSettings({
repoId,
hostId,
setSettingsSearchQuery,
openSettingsTarget,
openSettingsPage
})
},
}),
[openSettingsPage, openSettingsTarget, setSettingsSearchQuery]
)
@@ -162,7 +166,7 @@ function SetupScriptPromptCard(): React.JSX.Element | null {
})
)
}
openLocalCommandSettings(activeRepo.id)
openLocalCommandSettings(activeRepo.id, getRepoExecutionHostId(activeRepo))
}, [activeRepo, activeRepoHostIdentity, openLocalCommandSettings, promptState])
const handleDismiss = useCallback(() => {
@@ -249,7 +253,7 @@ function SetupScriptPromptCard(): React.JSX.Element | null {
markSetupScriptPromptSaved(current, importedRepoHostIdentity)
)
showSavedInProjectSettingsToast({
onOpenSettings: () => openLocalCommandSettings(importedRepoId),
onOpenSettings: () => openLocalCommandSettings(importedRepoId, importedHostId),
description: translate(
'auto.components.sidebar.SetupScriptPromptCard.a49196d538',
'Runs when Orca creates a new worktree.'
@@ -262,7 +266,7 @@ function SetupScriptPromptCard(): React.JSX.Element | null {
setPromptState((current) => markSetupScriptPromptSaved(current, importedRepoHostIdentity))
const skippedCount = candidate.unsupportedFields?.length ?? 0
showSavedInProjectSettingsToast({
onOpenSettings: () => openLocalCommandSettings(importedRepoId),
onOpenSettings: () => openLocalCommandSettings(importedRepoId, importedHostId),
description:
skippedCount > 0
? `${skippedCount} unsupported field${skippedCount === 1 ? '' : 's'} skipped. Saved the setup command.`
@@ -385,9 +389,7 @@ function SetupScriptPromptCard(): React.JSX.Element | null {
!renderedPromptState.hasEffectiveSetup &&
renderedPromptState.repoHostIdentity === activeRepoHostIdentity
) {
lastVisiblePromptRef.current = {
state: renderedPromptState
}
lastVisiblePromptRef.current = { state: renderedPromptState }
}
const isInspectionError = renderedPromptState.status === 'error'
@@ -1,18 +1,26 @@
import { getRepositoryLocalCommandsSectionId } from '@/components/settings/repository-settings-targets'
import type { ExecutionHostId } from '../../../../shared/execution-host'
export function openSetupScriptSettings(input: {
repoId: string
hostId: ExecutionHostId
setSettingsSearchQuery: (query: string) => void
openSettingsTarget: (target: { pane: 'repo'; repoId: string; sectionId: string }) => void
openSettingsTarget: (target: {
pane: 'repo'
repoId: string
hostId: ExecutionHostId
sectionId: string
}) => void
openSettingsPage: () => void
}): void {
const { openSettingsPage, openSettingsTarget, repoId, setSettingsSearchQuery } = input
const { hostId, openSettingsPage, openSettingsTarget, repoId, setSettingsSearchQuery } = input
// Why: imported setup commands are local repo settings; a stale Settings
// search should not hide the exact editor this action opens.
setSettingsSearchQuery('')
openSettingsTarget({
pane: 'repo',
repoId,
hostId,
sectionId: getRepositoryLocalCommandsSectionId(repoId)
})
openSettingsPage()
@@ -1,4 +1,7 @@
import type { GlobalSettings, Repo, Worktree } from '../../../../shared/types'
import type { SetupScriptPromptInspection } from '@/lib/setup-script-prompt'
import { findRepoForHost } from '@/store/slices/repo-host-identity'
import { toRuntimeExecutionHostId } from '../../../../shared/execution-host'
export type SetupScriptPromptState = SetupScriptPromptInspection & {
repoHostIdentity: string
@@ -8,6 +11,30 @@ export type LastVisibleSetupScriptPrompt = {
state: SetupScriptPromptState
}
export function findSetupScriptPromptRepo(input: {
repos: readonly Repo[]
activeRepoId: string | null
activeWorktree: Pick<Worktree, 'hostId' | 'repoId' | 'runtimeOwnerEnvironmentId'> | null
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null
}): Repo | null {
const { activeRepoId, activeWorktree, repos, settings } = input
if (!activeRepoId) {
return null
}
// Why: runtime-relayed SSH worktrees expose their transport host separately from the repo catalog owner.
const runtimeOwnerEnvironmentId = activeWorktree?.runtimeOwnerEnvironmentId?.trim()
const activeWorktreeHostId =
activeWorktree?.repoId === activeRepoId
? runtimeOwnerEnvironmentId
? toRuntimeExecutionHostId(runtimeOwnerEnvironmentId)
: activeWorktree.hostId
: undefined
return findRepoForHost(repos, activeRepoId, {
settings,
...(activeWorktreeHostId ? { hostId: activeWorktreeHostId } : {})
})
}
export function markSetupScriptPromptSaved(
current: SetupScriptPromptState | null,
savedRepoHostIdentity: string
+1
View File
@@ -744,6 +744,7 @@ export type UISlice = {
settingsNavigationTarget: {
pane: SettingsNavTarget
repoId: string | null
hostId?: ExecutionHostId
sectionId?: string
intent?: 'add-quick-command' | 'add-remote-orca-server' | 'add-ssh-host'
} | null