mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
Fix paired web Add Project folder browsing (#13885)
This commit is contained in:
@@ -3233,6 +3233,7 @@
|
||||
"commands": [
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/settings.test.ts src/main/ipc/runtime-environments.test.ts src/renderer/src/store/slices/settings.test.ts src/renderer/src/store/slices/browser.test.ts src/renderer/src/components/settings/browser-session-host-selection.test.ts src/renderer/src/components/settings/RuntimeEnvironmentsPane.test.ts src/renderer/src/components/status-bar/SshStatusSegment.test.ts src/renderer/src/components/sidebar/use-add-repo-host-selection.test.ts src/renderer/src/hooks/useIpcEvents.test.ts src/renderer/src/web/web-preload-api.test.ts --maxWorkers=1",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/sidebar/AddProjectFromFolderDialog.test.tsx src/renderer/src/components/sidebar/NonGitFolderDialog.test.tsx src/renderer/src/components/sidebar/AddRepoDialog.default-checkout.test.ts src/renderer/src/components/sidebar/AddRepoSteps.default-checkout.test.ts src/renderer/src/components/sidebar/project-added-default-checkout.test.ts src/renderer/src/components/sidebar/useAddRepoLocalFolderFlow.test.ts src/renderer/src/components/sidebar/useAddRepoNestedImportFlow.test.ts src/renderer/src/components/sidebar/useAddRepoServerPathFlow.test.ts src/renderer/src/components/sidebar/useAddRepoCloneFlow.test.ts src/renderer/src/components/sidebar/useCreateRepo.default-checkout.test.ts src/renderer/src/components/sidebar/worktree-list-host-filtering.test.ts src/renderer/src/lib/resolved-worktree-execution-host.test.ts src/renderer/src/lib/worktree-runtime-owner.test.ts src/renderer/src/store/selectors.test.ts src/renderer/src/store/slices/repos-all-hosts-folder-workspaces.test.ts src/renderer/src/store/slices/repos-project-groups.test.ts src/renderer/src/store/slices/repos-selected-owner-routing.test.ts src/renderer/src/store/slices/selected-host-active-workspace-identity.test.ts src/renderer/src/store/slices/worktrees.test.ts --maxWorkers=1",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/sidebar/use-add-repo-host-selection.test.ts src/renderer/src/components/sidebar/add-repo-browse-authority.test.ts src/renderer/src/components/sidebar/AddRepoDialogStepContent.test.tsx src/renderer/src/components/sidebar/AddRepoStartSteps.test.tsx src/renderer/src/components/sidebar/useAddRepoServerPathFlow.test.ts src/renderer/src/runtime/runtime-server-directory-browser.test.ts src/renderer/src/store/slices/repos-paired-runtime-add.test.ts --maxWorkers=1",
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/sidebar/AddProjectFromFolderDialog.test.tsx src/renderer/src/components/sidebar/NonGitFolderDialog.test.tsx src/renderer/src/components/sidebar/AddRepoDialog.default-checkout.test.ts src/renderer/src/components/sidebar/AddRepoSteps.default-checkout.test.ts src/renderer/src/components/sidebar/project-added-default-checkout.test.ts src/renderer/src/components/sidebar/useAddRepoLocalFolderFlow.test.ts src/renderer/src/components/sidebar/useAddRepoNestedImportFlow.test.ts src/renderer/src/components/sidebar/useAddRepoServerPathFlow.test.ts src/renderer/src/components/sidebar/useAddRepoCloneFlow.test.ts src/renderer/src/components/sidebar/useCreateRepo.default-checkout.test.ts src/renderer/src/components/sidebar/worktree-list-host-filtering.test.ts src/renderer/src/lib/resolved-worktree-execution-host.test.ts src/renderer/src/lib/worktree-runtime-owner.test.ts src/renderer/src/store/selectors.test.ts src/renderer/src/store/slices/repos-all-hosts-folder-workspaces.test.ts src/renderer/src/store/slices/repos-project-groups.test.ts src/renderer/src/store/slices/repos-selected-owner-routing.test.ts src/renderer/src/store/slices/selected-host-active-workspace-identity.test.ts src/renderer/src/store/slices/worktrees.test.ts src/renderer/src/runtime/web-runtime-session.test.ts --maxWorkers=1",
|
||||
"pnpm run ensure:electron-runtime && npx playwright test tests/e2e/pr11346-selected-runtime-add.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1 --reporter=line",
|
||||
"pnpm run ensure:electron-runtime && npx playwright test tests/e2e/pr11346-selected-runtime-add.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --reporter=line"
|
||||
@@ -3246,11 +3247,14 @@
|
||||
"src/renderer/src/components/settings/RuntimeEnvironmentsPane.test.ts",
|
||||
"src/renderer/src/components/status-bar/SshStatusSegment.test.ts",
|
||||
"src/renderer/src/components/sidebar/use-add-repo-host-selection.test.ts",
|
||||
"src/renderer/src/components/sidebar/add-repo-browse-authority.test.ts",
|
||||
"src/renderer/src/hooks/useIpcEvents.test.ts",
|
||||
"src/renderer/src/web/web-preload-api.test.ts",
|
||||
"src/renderer/src/components/sidebar/AddProjectFromFolderDialog.test.tsx",
|
||||
"src/renderer/src/components/sidebar/NonGitFolderDialog.test.tsx",
|
||||
"src/renderer/src/components/sidebar/AddRepoDialog.default-checkout.test.ts",
|
||||
"src/renderer/src/components/sidebar/AddRepoDialogStepContent.test.tsx",
|
||||
"src/renderer/src/components/sidebar/AddRepoStartSteps.test.tsx",
|
||||
"src/renderer/src/components/sidebar/AddRepoSteps.default-checkout.test.ts",
|
||||
"src/renderer/src/components/sidebar/project-added-default-checkout.test.ts",
|
||||
"src/renderer/src/components/sidebar/useAddRepoLocalFolderFlow.test.ts",
|
||||
@@ -3262,6 +3266,7 @@
|
||||
"src/renderer/src/lib/resolved-worktree-execution-host.test.ts",
|
||||
"src/renderer/src/lib/worktree-runtime-owner.test.ts",
|
||||
"src/renderer/src/store/selectors.test.ts",
|
||||
"src/renderer/src/store/slices/repos-paired-runtime-add.test.ts",
|
||||
"src/renderer/src/store/slices/repos-all-hosts-folder-workspaces.test.ts",
|
||||
"src/renderer/src/store/slices/repos-project-groups.test.ts",
|
||||
"src/renderer/src/store/slices/repos-selected-owner-routing.test.ts",
|
||||
@@ -3271,6 +3276,34 @@
|
||||
"tests/e2e/pr11346-selected-runtime-add.spec.ts"
|
||||
],
|
||||
"assertionRefs": [
|
||||
{
|
||||
"file": "src/renderer/src/components/sidebar/use-add-repo-host-selection.test.ts",
|
||||
"assertions": [
|
||||
"paired web hides the viewing computer as an Add Project filesystem authority and selects the paired runtime"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/renderer/src/components/sidebar/add-repo-browse-authority.test.ts",
|
||||
"assertions": [
|
||||
"paired runtime browsing opens the host browser without invoking native pickFolders while desktop local and SSH routes remain intact"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/renderer/src/components/sidebar/AddRepoDialogStepContent.test.tsx",
|
||||
"assertions": ["paired runtime browsing renders the in-app host filesystem browser"]
|
||||
},
|
||||
{
|
||||
"file": "src/renderer/src/components/sidebar/AddRepoStartSteps.test.tsx",
|
||||
"assertions": [
|
||||
"host-scoped Add Project actions remain disabled until a paired runtime is selectable"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/renderer/src/store/slices/repos-paired-runtime-add.test.ts",
|
||||
"assertions": [
|
||||
"an explicitly selected paired runtime submits repo.add to that runtime without native repo or folder-picker calls"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/main/ipc/settings.test.ts",
|
||||
"assertions": [
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
useAddRepoHostedController,
|
||||
type AddRepoDialogHostedController
|
||||
} from './use-add-repo-hosted-controller'
|
||||
import { routeAddRepoBrowse } from './add-repo-browse-authority'
|
||||
|
||||
export default React.memo(function AddRepoDialog({
|
||||
hosted
|
||||
@@ -308,7 +309,7 @@ export default React.memo(function AddRepoDialog({
|
||||
selectedSshTargetId={hostSelection.selectedSshTargetId}
|
||||
selectedHostLabel={
|
||||
hostSelection.hostOptions.find((host) => host.id === hostSelection.selectedHostId)
|
||||
?.label ?? hostSelection.selectedHostId
|
||||
?.label ?? null
|
||||
}
|
||||
lockSshTargetSelection={hostSelection.selectedParsedHost?.kind === 'ssh'}
|
||||
remotePath={remotePath}
|
||||
@@ -324,26 +325,31 @@ export default React.memo(function AddRepoDialog({
|
||||
isCreating={isCreating}
|
||||
hostSelector={<AddRepoHostSelectorSlot hostSelection={hostSelection} />}
|
||||
showRemoteAction={false}
|
||||
browseHostKind={
|
||||
selectedHostKind === 'ssh' || selectedHostKind === 'runtime' ? selectedHostKind : 'local'
|
||||
}
|
||||
actionsDisabled={!hostSelection.selectedHostId}
|
||||
browseHostKind={selectedHostKind ?? 'runtime'}
|
||||
createDefaultParent={createDefaultParent}
|
||||
createGitAvailability={createGitAvailability}
|
||||
createRuntimeParentStatus={createRuntimeParentStatus}
|
||||
createParentDefaultPending={createParentDefaultPending}
|
||||
manualCreateParentEntry={isRuntimeEnvironmentActive || selectedHostKind === 'ssh'}
|
||||
onBrowse={
|
||||
selectedHostKind === 'ssh'
|
||||
? () => void handleOpenRemoteStep(hostSelection.selectedSshTargetId)
|
||||
: selectedHostKind === 'runtime'
|
||||
? () => setStep('server-path')
|
||||
: handleBrowse
|
||||
onBrowse={() =>
|
||||
routeAddRepoBrowse(hostSelection.selectedParsedHost, {
|
||||
browseLocal: () => void handleBrowse(),
|
||||
browseRuntime: () => setStep('server-path'),
|
||||
browseSsh: (targetId) => void handleOpenRemoteStep(targetId)
|
||||
})
|
||||
}
|
||||
onOpenCloneStep={() => {
|
||||
if (!hostSelection.selectedHostId) {
|
||||
return
|
||||
}
|
||||
setCloneError(null)
|
||||
setStep('clone')
|
||||
}}
|
||||
onOpenCreateStep={() => {
|
||||
if (!hostSelection.selectedHostId) {
|
||||
return
|
||||
}
|
||||
setCreateError(null)
|
||||
setStep('create')
|
||||
}}
|
||||
|
||||
@@ -281,4 +281,16 @@ describe('AddRepoDialogStepContent nested imports', () => {
|
||||
expect(html).not.toContain('Create on host')
|
||||
expect(html).not.toContain('Want to import many repos at once?')
|
||||
})
|
||||
|
||||
it('opens the in-app filesystem browser for a paired runtime', () => {
|
||||
const html = renderStepContent({
|
||||
step: 'server-path',
|
||||
isRuntimeEnvironmentActive: true,
|
||||
activeRuntimeEnvironmentId: 'paired-host'
|
||||
})
|
||||
|
||||
expect(html).toContain('Browse host filesystem')
|
||||
expect(html).toContain('Navigate to a directory and click Select to choose it.')
|
||||
expect(html).toContain('Select folder')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -46,6 +46,7 @@ type AddRepoDialogStepContentProps = {
|
||||
hostSelector?: ReactNode
|
||||
showRemoteAction?: boolean
|
||||
canCreateProject?: boolean
|
||||
actionsDisabled?: boolean
|
||||
manualCreateParentEntry?: boolean
|
||||
browseHostKind?: 'local' | 'ssh' | 'runtime'
|
||||
createDefaultParent: string
|
||||
@@ -115,6 +116,7 @@ export function AddRepoDialogStepContent({
|
||||
hostSelector,
|
||||
showRemoteAction = true,
|
||||
canCreateProject = true,
|
||||
actionsDisabled = false,
|
||||
manualCreateParentEntry = isRuntimeEnvironmentActive,
|
||||
browseHostKind = 'local',
|
||||
createDefaultParent,
|
||||
@@ -159,6 +161,7 @@ export function AddRepoDialogStepContent({
|
||||
hostSelector={hostSelector}
|
||||
showRemoteAction={showRemoteAction}
|
||||
canCreateProject={canCreateProject}
|
||||
actionsDisabled={actionsDisabled}
|
||||
browseHostKind={browseHostKind}
|
||||
onBrowse={onBrowse}
|
||||
onOpenCloneStep={onOpenCloneStep}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { canConnectAddRepoHost, canSelectAddRepoHost } from './add-repo-host-ava
|
||||
|
||||
type AddRepoHostSelectorProps = {
|
||||
hosts: SidebarHostOption[]
|
||||
selectedHostId: ExecutionHostId
|
||||
selectedHostId: ExecutionHostId | null
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onSelectHost: (hostId: ExecutionHostId) => void
|
||||
|
||||
@@ -53,6 +53,7 @@ function renderServerPathStartStep(runtimeEnvironmentId: string | null): string
|
||||
|
||||
type LocalStartStepDomOptions = {
|
||||
isAdding?: boolean
|
||||
actionsDisabled?: boolean
|
||||
addProjectBusyLabel?: string | null
|
||||
nestedScanInProgress?: boolean
|
||||
nestedScanId?: string | null
|
||||
@@ -76,6 +77,7 @@ async function renderLocalStartStepDom(
|
||||
repoCount={1}
|
||||
isSshLikely={isSshLikely}
|
||||
isAdding={options.isAdding ?? false}
|
||||
actionsDisabled={options.actionsDisabled ?? false}
|
||||
addProjectBusyLabel={options.addProjectBusyLabel ?? null}
|
||||
nestedScanInProgress={options.nestedScanInProgress ?? false}
|
||||
nestedScanId={options.nestedScanId ?? null}
|
||||
@@ -257,6 +259,20 @@ describe('AddRepoLocalStartStep', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('disables host-scoped actions until a host is selectable', async () => {
|
||||
const { container, root } = await renderLocalStartStepDom(false, { actionsDisabled: true })
|
||||
|
||||
expect(findButton(container, 'Browse folder').disabled).toBe(true)
|
||||
expect(findButton(container, 'Clone from URL').disabled).toBe(true)
|
||||
expect(findButton(container, 'Project on SSH host').disabled).toBe(true)
|
||||
expect(findButton(container, 'Create new project').disabled).toBe(true)
|
||||
expect(findButton(container, 'Browse folder').textContent).not.toContain('⏎')
|
||||
|
||||
await act(async () => {
|
||||
root.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
it('marks the autofocused Browse action as selected with the ⏎ chip', async () => {
|
||||
const { container, root } = await renderLocalStartStepDom(false)
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ type AddRepoLocalStartStepProps = {
|
||||
hostSelector?: ReactNode
|
||||
showRemoteAction?: boolean
|
||||
canCreateProject?: boolean
|
||||
actionsDisabled?: boolean
|
||||
browseHostKind?: 'local' | 'ssh' | 'runtime'
|
||||
onBrowse: () => void
|
||||
onOpenCloneStep: () => void
|
||||
@@ -87,6 +88,7 @@ export function AddRepoLocalStartStep({
|
||||
hostSelector,
|
||||
showRemoteAction = true,
|
||||
canCreateProject = true,
|
||||
actionsDisabled = false,
|
||||
browseHostKind = 'local',
|
||||
onBrowse,
|
||||
onOpenCloneStep,
|
||||
@@ -96,6 +98,7 @@ export function AddRepoLocalStartStep({
|
||||
}: AddRepoLocalStartStepProps): React.JSX.Element {
|
||||
const browseActionRef = useRef<HTMLButtonElement | null>(null)
|
||||
const actionsRef = useRef<HTMLDivElement | null>(null)
|
||||
const actionsUnavailable = isAdding || actionsDisabled
|
||||
const { primaryAction, secondaryActions } = getAddRepoLocalStartActions({
|
||||
isSshLikely,
|
||||
onBrowse,
|
||||
@@ -111,16 +114,13 @@ export function AddRepoLocalStartStep({
|
||||
// it follows keyboard focus so Enter always activates the highlighted action. Browse is
|
||||
// autofocused on open, so it starts selected; Tab and ↑/↓ move the highlight.
|
||||
const [selectedKind, setSelectedKind] = useState<string | null>(primaryAction.kind)
|
||||
const visibleSelectedKind = actionsUnavailable ? null : selectedKind
|
||||
|
||||
useEffect(() => {
|
||||
if (isAdding) {
|
||||
setSelectedKind(null)
|
||||
return
|
||||
}
|
||||
if (!isAdding) {
|
||||
if (!actionsUnavailable) {
|
||||
browseActionRef.current?.focus()
|
||||
}
|
||||
}, [isAdding])
|
||||
}, [actionsUnavailable])
|
||||
|
||||
// ↑/↓ rove focus across the action buttons in visual order; focus drives the selection.
|
||||
const handleArrowNavigation = (event: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||
@@ -177,8 +177,8 @@ export function AddRepoLocalStartStep({
|
||||
icon={primaryAction.icon}
|
||||
title={primaryAction.title}
|
||||
description={primaryAction.description}
|
||||
disabled={isAdding}
|
||||
selected={selectedKind === primaryAction.kind}
|
||||
disabled={actionsUnavailable}
|
||||
selected={visibleSelectedKind === primaryAction.kind}
|
||||
buttonRef={browseActionRef}
|
||||
onClick={primaryAction.onClick}
|
||||
onFocus={() => setSelectedKind(primaryAction.kind)}
|
||||
@@ -199,8 +199,8 @@ export function AddRepoLocalStartStep({
|
||||
icon={action.icon}
|
||||
title={action.title}
|
||||
description={action.description}
|
||||
disabled={isAdding || Boolean(action.disabled)}
|
||||
selected={selectedKind === action.kind}
|
||||
disabled={actionsUnavailable || Boolean(action.disabled)}
|
||||
selected={visibleSelectedKind === action.kind}
|
||||
onClick={action.onClick}
|
||||
onFocus={() => setSelectedKind(action.kind)}
|
||||
className={cn(
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { parseExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { routeAddRepoBrowse } from './add-repo-browse-authority'
|
||||
|
||||
describe('routeAddRepoBrowse', () => {
|
||||
it('opens paired host browsing without invoking native pickFolders', () => {
|
||||
const pickFolders = vi.fn()
|
||||
const browseRuntime = vi.fn()
|
||||
|
||||
routeAddRepoBrowse(parseExecutionHostId('runtime:paired-host'), {
|
||||
browseLocal: pickFolders,
|
||||
browseRuntime,
|
||||
browseSsh: vi.fn()
|
||||
})
|
||||
|
||||
expect(browseRuntime).toHaveBeenCalledOnce()
|
||||
expect(pickFolders).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('preserves native folder browsing for desktop local hosts', () => {
|
||||
const pickFolders = vi.fn()
|
||||
|
||||
routeAddRepoBrowse(parseExecutionHostId('local'), {
|
||||
browseLocal: pickFolders,
|
||||
browseRuntime: vi.fn(),
|
||||
browseSsh: vi.fn()
|
||||
})
|
||||
|
||||
expect(pickFolders).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('preserves SSH host browsing', () => {
|
||||
const browseSsh = vi.fn()
|
||||
|
||||
routeAddRepoBrowse(parseExecutionHostId('ssh:builder'), {
|
||||
browseLocal: vi.fn(),
|
||||
browseRuntime: vi.fn(),
|
||||
browseSsh
|
||||
})
|
||||
|
||||
expect(browseSsh).toHaveBeenCalledWith('builder')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { ParsedExecutionHost } from '../../../../shared/execution-host'
|
||||
|
||||
export type AddRepoBrowseAuthorityActions = {
|
||||
browseLocal: () => void
|
||||
browseRuntime: () => void
|
||||
browseSsh: (targetId: string) => void
|
||||
}
|
||||
|
||||
export function routeAddRepoBrowse(
|
||||
host: ParsedExecutionHost | null,
|
||||
actions: AddRepoBrowseAuthorityActions
|
||||
): void {
|
||||
if (host?.kind === 'runtime') {
|
||||
actions.browseRuntime()
|
||||
return
|
||||
}
|
||||
if (host?.kind === 'ssh') {
|
||||
actions.browseSsh(host.targetId)
|
||||
return
|
||||
}
|
||||
if (host?.kind === 'local') {
|
||||
actions.browseLocal()
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ export function useAddRepoHostChangeReset({
|
||||
onResetHostScopedState
|
||||
}: {
|
||||
isOpen: boolean
|
||||
selectedHostId: string
|
||||
selectedHostId: string | null
|
||||
onResetClosed: () => void
|
||||
onResetHostScopedState: () => void
|
||||
}) {
|
||||
|
||||
@@ -17,7 +17,8 @@ const mocks = vi.hoisted(() => ({
|
||||
runtimeEnvironments: [] as { id: string; name: string; source?: 'manual' | 'ephemeral-vm' }[]
|
||||
},
|
||||
sshConnect: vi.fn(),
|
||||
sshGetState: vi.fn()
|
||||
sshGetState: vi.fn(),
|
||||
isWebClient: false
|
||||
}))
|
||||
|
||||
vi.mock('react', async (importOriginal) => {
|
||||
@@ -64,6 +65,10 @@ vi.mock('./use-sidebar-host-scope-options', () => ({
|
||||
useSidebarHostScopeOptions: () => ({ hostOptions: mocks.hostOptions })
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/web-client-location', () => ({
|
||||
isWebClientLocation: () => mocks.isWebClient
|
||||
}))
|
||||
|
||||
describe('useAddRepoHostSelection', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -103,6 +108,7 @@ describe('useAddRepoHostSelection', () => {
|
||||
mocks.storeState.runtimeEnvironments = []
|
||||
mocks.sshConnect.mockReset()
|
||||
mocks.sshGetState.mockReset()
|
||||
mocks.isWebClient = false
|
||||
vi.stubGlobal('window', {
|
||||
api: {
|
||||
ssh: {
|
||||
@@ -137,6 +143,35 @@ describe('useAddRepoHostSelection', () => {
|
||||
expect(setStep).toHaveBeenCalledWith('add')
|
||||
})
|
||||
|
||||
it('uses the paired runtime as the only local filesystem authority in web clients', async () => {
|
||||
mocks.isWebClient = true
|
||||
mocks.stateValues = ['local', false]
|
||||
const { useAddRepoHostSelection } = await import('./use-add-repo-host-selection')
|
||||
|
||||
const result = useAddRepoHostSelection({ isOpen: true, setStep: vi.fn() })
|
||||
|
||||
expect(result.hostOptions.map((host) => host.id)).toEqual(['ssh:ssh-1', 'runtime:env-1'])
|
||||
expect(result.selectedHostId).toBe('runtime:env-1')
|
||||
expect(result.selectedParsedHost).toMatchObject({
|
||||
kind: 'runtime',
|
||||
environmentId: 'env-1'
|
||||
})
|
||||
})
|
||||
|
||||
it('has no local fallback while a paired web runtime is loading', async () => {
|
||||
mocks.isWebClient = true
|
||||
mocks.stateValues = ['local', false]
|
||||
mocks.hostOptions = [mocks.hostOptions[0]]
|
||||
const { useAddRepoHostSelection } = await import('./use-add-repo-host-selection')
|
||||
|
||||
const result = useAddRepoHostSelection({ isOpen: true, setStep: vi.fn() })
|
||||
|
||||
expect(result.hostOptions).toEqual([])
|
||||
expect(result.selectedHostId).toBeNull()
|
||||
expect(result.selectedParsedHost).toBeNull()
|
||||
expect(mocks.stateSetters[0]).not.toHaveBeenCalledWith('local')
|
||||
})
|
||||
|
||||
it('selects a local or SSH host without changing the durable active server', async () => {
|
||||
mocks.stateValues = ['runtime:env-1', false]
|
||||
mocks.storeState.settings = { activeRuntimeEnvironmentId: 'env-1' }
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { AddRepoDialogStep } from './add-repo-dialog-types'
|
||||
import { useSidebarHostScopeOptions } from './use-sidebar-host-scope-options'
|
||||
import { canSelectAddRepoHost } from './add-repo-host-availability'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { isWebClientLocation } from '@/lib/web-client-location'
|
||||
|
||||
export function useAddRepoHostSelection({
|
||||
isOpen,
|
||||
@@ -22,7 +23,7 @@ export function useAddRepoHostSelection({
|
||||
setStep: (step: AddRepoDialogStep) => void
|
||||
}): {
|
||||
hostOptions: ReturnType<typeof useSidebarHostScopeOptions>['hostOptions']
|
||||
selectedHostId: ExecutionHostId
|
||||
selectedHostId: ExecutionHostId | null
|
||||
selectedParsedHost: ReturnType<typeof parseExecutionHostId>
|
||||
selectedSshTargetId: string | null
|
||||
hostSelectorOpen: boolean
|
||||
@@ -35,6 +36,7 @@ export function useAddRepoHostSelection({
|
||||
const sshConnectionStates = useAppStore((s) => s.sshConnectionStates)
|
||||
const runtimeEnvironments = useAppStore((s) => s.runtimeEnvironments)
|
||||
const { hostOptions } = useSidebarHostScopeOptions()
|
||||
const isWebClient = isWebClientLocation()
|
||||
const ephemeralRuntimeEnvironmentIds = useMemo(
|
||||
() =>
|
||||
new Set(
|
||||
@@ -49,26 +51,31 @@ export function useAddRepoHostSelection({
|
||||
hostOptions.filter((host) => {
|
||||
const parsed = parseExecutionHostId(host.id)
|
||||
return (
|
||||
parsed?.kind !== 'runtime' || !ephemeralRuntimeEnvironmentIds.has(parsed.environmentId)
|
||||
!(isWebClient && parsed?.kind === 'local') &&
|
||||
(parsed?.kind !== 'runtime' || !ephemeralRuntimeEnvironmentIds.has(parsed.environmentId))
|
||||
)
|
||||
}),
|
||||
[ephemeralRuntimeEnvironmentIds, hostOptions]
|
||||
[ephemeralRuntimeEnvironmentIds, hostOptions, isWebClient]
|
||||
)
|
||||
const [selectedAddProjectHostId, setSelectedAddProjectHostId] =
|
||||
useState<ExecutionHostId>(LOCAL_EXECUTION_HOST_ID)
|
||||
const [hostSelectorOpen, setHostSelectorOpen] = useState(false)
|
||||
const previousOpenRef = useRef(false)
|
||||
const pairedWebRuntimeHost = isWebClient
|
||||
? selectableHostOptions.find((host) => host.kind === 'runtime' && canSelectAddRepoHost(host))
|
||||
: undefined
|
||||
|
||||
const selectedHost =
|
||||
selectableHostOptions.find(
|
||||
(host) => host.id === selectedAddProjectHostId && canSelectAddRepoHost(host)
|
||||
) ??
|
||||
pairedWebRuntimeHost ??
|
||||
selectableHostOptions.find(
|
||||
(host) => host.id === LOCAL_EXECUTION_HOST_ID && canSelectAddRepoHost(host)
|
||||
) ??
|
||||
selectableHostOptions.find((host) => canSelectAddRepoHost(host)) ??
|
||||
selectableHostOptions[0]
|
||||
const selectedHostId = selectedHost?.id ?? LOCAL_EXECUTION_HOST_ID
|
||||
const selectedHostId = selectedHost?.id ?? (isWebClient ? null : LOCAL_EXECUTION_HOST_ID)
|
||||
const selectedParsedHost = parseExecutionHostId(selectedHostId)
|
||||
const selectedSshTargetId =
|
||||
selectedParsedHost?.kind === 'ssh' ? selectedParsedHost.targetId : null
|
||||
@@ -80,14 +87,16 @@ export function useAddRepoHostSelection({
|
||||
(host) => host.id === focusedHostId && canSelectAddRepoHost(host)
|
||||
)
|
||||
? focusedHostId
|
||||
: LOCAL_EXECUTION_HOST_ID
|
||||
setSelectedAddProjectHostId(nextHostId)
|
||||
: (pairedWebRuntimeHost?.id ?? (isWebClient ? null : LOCAL_EXECUTION_HOST_ID))
|
||||
if (nextHostId) {
|
||||
setSelectedAddProjectHostId(nextHostId)
|
||||
}
|
||||
}
|
||||
if (!isOpen) {
|
||||
setHostSelectorOpen(false)
|
||||
}
|
||||
previousOpenRef.current = isOpen
|
||||
}, [isOpen, selectableHostOptions, settings])
|
||||
}, [isOpen, isWebClient, pairedWebRuntimeHost?.id, selectableHostOptions, settings])
|
||||
|
||||
const handleSelectAddProjectHost = useCallback(
|
||||
async (hostId: ExecutionHostId): Promise<void> => {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createTestStore } from './store-test-helpers'
|
||||
import {
|
||||
installReposRuntimeRoutingHarness,
|
||||
remoteRepo,
|
||||
reposAdd,
|
||||
reposPickFolder,
|
||||
runtimeEnvironmentCall
|
||||
} from './repos-runtime-routing-fixture'
|
||||
|
||||
vi.mock('sonner', () => ({
|
||||
toast: {
|
||||
error: vi.fn(),
|
||||
info: vi.fn(),
|
||||
success: vi.fn(),
|
||||
warning: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
installReposRuntimeRoutingHarness()
|
||||
|
||||
describe('paired runtime repo add routing', () => {
|
||||
it('submits the selected host path to repo.add without native filesystem calls', async () => {
|
||||
runtimeEnvironmentCall.mockResolvedValue({
|
||||
id: 'rpc-paired-add',
|
||||
ok: true,
|
||||
result: { repo: remoteRepo },
|
||||
_meta: { runtimeId: 'runtime-remote' }
|
||||
})
|
||||
const store = createTestStore()
|
||||
|
||||
await store.getState().addRepoPath('/srv/paired-project', 'folder', {
|
||||
runtimeEnvironmentId: 'paired-host'
|
||||
})
|
||||
|
||||
expect(runtimeEnvironmentCall).toHaveBeenCalledWith({
|
||||
selector: 'paired-host',
|
||||
method: 'repo.add',
|
||||
params: { path: '/srv/paired-project', kind: 'folder' },
|
||||
timeoutMs: 15_000
|
||||
})
|
||||
expect(reposAdd).not.toHaveBeenCalled()
|
||||
expect(reposPickFolder).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,7 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import type { ElectronApplication, Page } from '@stablyai/playwright-test'
|
||||
import { test, expect } from './helpers/orca-app'
|
||||
@@ -176,3 +178,49 @@ test('keeps two paired browser clients and the host on independent worktrees', a
|
||||
await clientA?.close()
|
||||
}
|
||||
})
|
||||
|
||||
test('routes Add Project folder browsing through the paired host', async ({
|
||||
electronApp,
|
||||
orcaPage,
|
||||
registerPostElectronShutdownCleanup
|
||||
}) => {
|
||||
const hostFolder = mkdtempSync(path.join(os.tmpdir(), 'orca-paired-web-folder-'))
|
||||
const folderName = path.basename(hostFolder)
|
||||
registerPostElectronShutdownCleanup(async () => {
|
||||
rmSync(hostFolder, { recursive: true, force: true })
|
||||
})
|
||||
const visibleWorktreeId = await orcaPage.evaluate(
|
||||
() => window.__store?.getState().activeWorktreeId
|
||||
)
|
||||
if (!visibleWorktreeId) {
|
||||
throw new Error('Host worktree was not active before paired web validation')
|
||||
}
|
||||
|
||||
const offer = await createPairingOffer(orcaPage)
|
||||
const client = await openPairedClient(electronApp, offer, visibleWorktreeId)
|
||||
try {
|
||||
await client
|
||||
.getByRole('button', { name: /Add Project/i })
|
||||
.first()
|
||||
.click()
|
||||
const addDialog = client.getByRole('dialog', { name: /Add a project/i })
|
||||
await expect(addDialog).toBeVisible()
|
||||
await expect(addDialog).not.toContainText('Local Mac')
|
||||
|
||||
await addDialog.getByRole('button', { name: /Browse folder/i }).click()
|
||||
const browser = client.getByRole('dialog', { name: /Browse host filesystem/i })
|
||||
await expect(browser).toBeVisible()
|
||||
await expect(browser.getByRole('button', { name: /Select folder/i })).toBeVisible()
|
||||
await browser.getByRole('button', { name: /^Cancel$/i }).click()
|
||||
|
||||
const manualPathDialog = client.getByRole('dialog', { name: /Open host project/i })
|
||||
await manualPathDialog.locator('#server-project-path').fill(hostFolder)
|
||||
await manualPathDialog.getByRole('button', { name: /Open as Folder/i }).click()
|
||||
await expect(manualPathDialog).toBeHidden({ timeout: 30_000 })
|
||||
await expect(
|
||||
client.locator('[data-worktree-sidebar]').getByText(folderName, { exact: true }).first()
|
||||
).toBeVisible({ timeout: 30_000 })
|
||||
} finally {
|
||||
await client.close()
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user