mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 16:03:26 +00:00
fix(setup): route prompts from active workspace host
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user