mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
fix(mobile): keep the New Workspace sheet on a hidden repo's project
A last-visited per-workspace-environment repo is hidden, so the sheet fell back to the first listed project. It now maps it to its project's eligible copy with the desktop's resolver, moved to src/shared. The phone's mirror of the seeding rule is gone, since the hook imports the shared one. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
@@ -266,10 +266,13 @@ describe('NewWorktreeModal on a server SSH repo', () => {
|
||||
expect(runTargetPicker(renderer).props.selectedId).toBe('runtime:env-1')
|
||||
expect(server).toHaveBeenCalledWith('ssh.getState', { targetId: 'devbox' })
|
||||
expect(gate()).toMatchObject({ status: null, requiresConnection: true })
|
||||
expect(server).not.toHaveBeenCalledWith('worktree.create', expect.anything(), expect.anything())
|
||||
act(() => sourceInputs(renderer)[0]!.props.onChangeText('same-id-wt'))
|
||||
expect(
|
||||
renderer.root.find((node) => typeof node.props.onCreate === 'function').props.canCreate
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it("never offers a server's per-workspace-environment SSH repo, even last visited", async () => {
|
||||
it("never offers a server's per-workspace-environment SSH repo, and maps it to its project", async () => {
|
||||
const vmRepo = {
|
||||
...repos[0]!,
|
||||
id: 'vm-repo',
|
||||
@@ -277,6 +280,13 @@ describe('NewWorktreeModal on a server SSH repo', () => {
|
||||
connectionId: 'runtime-ssh-vm-1'
|
||||
}
|
||||
const serverRepo = { ...repos[0]!, id: 'server-repo', path: '/srv/orca' }
|
||||
const otherProject = {
|
||||
...repos[0]!,
|
||||
id: 'noqa',
|
||||
displayName: 'noqa',
|
||||
path: '/srv/noqa',
|
||||
upstream: { owner: 'stablyai', repo: 'noqa' }
|
||||
}
|
||||
asyncStorage.getItem.mockResolvedValueOnce(
|
||||
JSON.stringify({ hostId: 'host-vm', worktreeId: 'vm-repo::/home/dev/vm-orca/wt' })
|
||||
)
|
||||
@@ -292,8 +302,9 @@ describe('NewWorktreeModal on a server SSH repo', () => {
|
||||
createElement(NewWorktreeModal, {
|
||||
visible: true,
|
||||
client: requestsOnly(listing([])),
|
||||
// Listed first, it would otherwise win the server's row.
|
||||
serverClients: serversOf(requestsOnly(listing([vmRepo, serverRepo]))),
|
||||
// Listed before its project's copy, it would otherwise win the server's row; the last
|
||||
// visited, it picks its project's copy rather than the first project listed.
|
||||
serverClients: serversOf(requestsOnly(listing([otherProject, vmRepo, serverRepo]))),
|
||||
hostId: 'host-vm',
|
||||
onCreated: () => {},
|
||||
openExternalUrl: () => {},
|
||||
@@ -340,7 +351,7 @@ describe('NewWorktreeModal on a server SSH repo', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it("drops a connect reply that arrives after the server's target changed", async () => {
|
||||
it('drops a late connect reply, keeping the newly picked target connected', async () => {
|
||||
const sshRepo = (id: string, connectionId: string) => ({
|
||||
...repos[0]!,
|
||||
id,
|
||||
@@ -362,7 +373,8 @@ describe('NewWorktreeModal on a server SSH repo', () => {
|
||||
})
|
||||
}
|
||||
if (method === 'ssh.getState') {
|
||||
return Promise.resolve(state(params!.targetId!, 'disconnected'))
|
||||
const targetId = params!.targetId!
|
||||
return Promise.resolve(state(targetId, targetId === 'box-b' ? 'connected' : 'disconnected'))
|
||||
}
|
||||
if (method === 'ssh.connect') {
|
||||
return new Promise((resolve) => {
|
||||
@@ -404,11 +416,15 @@ describe('NewWorktreeModal on a server SSH repo', () => {
|
||||
const connectable = renderer.root.find((node) => typeof node.props.onConnect === 'function')
|
||||
await act(async () => connectable.props.onConnect())
|
||||
await pickProject('beta')
|
||||
expect(gate().status).toBe('disconnected')
|
||||
const canCreate = () =>
|
||||
renderer.root.find((node) => typeof node.props.onCreate === 'function').props.canCreate
|
||||
expect(gate().status).toBe('connected')
|
||||
expect(canCreate()).toBe(true)
|
||||
|
||||
await act(async () => finishConnect!(state('box-a', 'connected')))
|
||||
await flushUpdates()
|
||||
|
||||
expect(gate()).toMatchObject({ status: 'disconnected', requiresConnection: true })
|
||||
expect(gate()).toMatchObject({ status: 'connected', requiresConnection: false })
|
||||
expect(canCreate()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -47,7 +47,7 @@ export function useNewWorkspaceExecutionTarget(args: {
|
||||
const target = workspaceSshTargetKey(server, connectionId)
|
||||
const [sshState, setSshEntry] = useState<SshStateEntry | null>(null)
|
||||
const setSshState = (record: WorkspaceSshRecord) => setSshEntry({ target, record })
|
||||
// A connect reply lands only while its target is still the one shown, so it never overwrites another's.
|
||||
// Lands only if no other target's state replaced it.
|
||||
const settleSshState = (record: WorkspaceSshRecord) =>
|
||||
setSshEntry((current) => (current?.target === target ? { target, record } : current))
|
||||
const [connectingTarget, setConnectingTarget] = useState<string | null>(null)
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import type { ExecutionHostId } from '../../../src/shared/execution-host'
|
||||
import { getNewWorkspaceDialogEligibleRepos } from '../../../src/shared/new-workspace-dialog-repo'
|
||||
import {
|
||||
getNewWorkspaceDialogEligibleRepos,
|
||||
resolveNewWorkspaceDialogActiveRepoId,
|
||||
resolveNewWorkspaceDialogRepoId
|
||||
} from '../../../src/shared/new-workspace-dialog-repo'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import { nativeChatRepoListRead } from '../session/mobile-session-read-operations'
|
||||
import { getCachedRepos, setCachedRepos } from '../cache/repo-cache'
|
||||
import { useLastVisitedWorktreeRepoId } from '../worktree/use-last-visited-worktree-repo'
|
||||
import {
|
||||
refreshMobileNewWorkspaceDialogSelectedRepo,
|
||||
resolveMobileNewWorkspaceDialogRepoId
|
||||
} from '../worktree/new-workspace-dialog-repo-selection'
|
||||
import { refreshMobileNewWorkspaceDialogSelectedRepo } from '../worktree/new-workspace-dialog-repo-selection'
|
||||
import type { MobileWorkspaceRepo } from './new-worktree-modal-types'
|
||||
|
||||
const NO_SERVER_CLIENTS: ReadonlyMap<ExecutionHostId, RpcClient> = new Map()
|
||||
@@ -58,12 +59,12 @@ export function useNewWorkspaceRepositories(args: {
|
||||
const [pickedRepo, setSelectedRepo] = useState<MobileWorkspaceRepo | null>(null)
|
||||
const [loading, setLoading] = useState(initialRepos == null)
|
||||
const lastVisitedRepo = useLastVisitedWorktreeRepoId(hostId, visible)
|
||||
// The desktop's rule: a per-workspace-environment SSH repo is plumbing, never a pick.
|
||||
const repos = useMemo(
|
||||
() =>
|
||||
getNewWorkspaceDialogEligibleRepos([...desktopRepos, ...[...serverRepos.values()].flat()]),
|
||||
const listedRepos = useMemo(
|
||||
() => [...desktopRepos, ...[...serverRepos.values()].flat()],
|
||||
[desktopRepos, serverRepos]
|
||||
)
|
||||
// The desktop's rule: a per-workspace-environment SSH repo is plumbing, never a pick.
|
||||
const repos = useMemo(() => getNewWorkspaceDialogEligibleRepos(listedRepos), [listedRepos])
|
||||
// Why derived: the pick follows its repo's latest listing, and goes once its host stops listing it.
|
||||
const selectedRepo = useMemo(
|
||||
() => refreshMobileNewWorkspaceDialogSelectedRepo(repos, pickedRepo),
|
||||
@@ -74,15 +75,19 @@ export function useNewWorkspaceRepositories(args: {
|
||||
if (!visible || !lastVisitedRepo.loaded || selectedRepo || repos.length === 0) {
|
||||
return
|
||||
}
|
||||
const preferredRepoId = resolveMobileNewWorkspaceDialogRepoId({
|
||||
const preferredRepoId = resolveNewWorkspaceDialogRepoId({
|
||||
eligibleRepos: repos,
|
||||
activeRepoId: lastVisitedRepo.repoId
|
||||
activeRepoId: resolveNewWorkspaceDialogActiveRepoId(
|
||||
listedRepos,
|
||||
repos,
|
||||
lastVisitedRepo.repoId
|
||||
)
|
||||
})
|
||||
const preferredRepo = repos.find((repo) => repo.id === preferredRepoId) ?? null
|
||||
if (preferredRepo) {
|
||||
setSelectedRepo(preferredRepo)
|
||||
}
|
||||
}, [lastVisitedRepo.loaded, lastVisitedRepo.repoId, repos, selectedRepo, visible])
|
||||
}, [lastVisitedRepo.loaded, lastVisitedRepo.repoId, listedRepos, repos, selectedRepo, visible])
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible || !client) {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
refreshMobileNewWorkspaceDialogSelectedRepo,
|
||||
resolveMobileNewWorkspaceDialogRepoId
|
||||
} from './new-workspace-dialog-repo-selection'
|
||||
import { refreshMobileNewWorkspaceDialogSelectedRepo } from './new-workspace-dialog-repo-selection'
|
||||
|
||||
type Repo = {
|
||||
id: string
|
||||
@@ -20,56 +17,6 @@ function makeRepo(id: string, overrides: Partial<Repo> = {}): Repo {
|
||||
}
|
||||
|
||||
describe('mobile new workspace dialog repo selection', () => {
|
||||
it('matches the dialog repo priority order', () => {
|
||||
const eligibleRepos = [
|
||||
makeRepo('first'),
|
||||
makeRepo('active'),
|
||||
makeRepo('initial'),
|
||||
makeRepo('draft')
|
||||
]
|
||||
|
||||
expect(
|
||||
resolveMobileNewWorkspaceDialogRepoId({
|
||||
eligibleRepos,
|
||||
draftRepoId: 'draft',
|
||||
initialRepoId: 'initial',
|
||||
activeRepoId: 'active'
|
||||
})
|
||||
).toBe('draft')
|
||||
})
|
||||
|
||||
it('uses the last visited repo as the active repo before falling back', () => {
|
||||
const eligibleRepos = [makeRepo('first'), makeRepo('active')]
|
||||
|
||||
expect(resolveMobileNewWorkspaceDialogRepoId({ eligibleRepos, activeRepoId: 'active' })).toBe(
|
||||
'active'
|
||||
)
|
||||
expect(resolveMobileNewWorkspaceDialogRepoId({ eligibleRepos, activeRepoId: 'missing' })).toBe(
|
||||
'first'
|
||||
)
|
||||
})
|
||||
|
||||
it('defaults to a repo on the focused host when no explicit repo is chosen', () => {
|
||||
const eligibleRepos = [
|
||||
makeRepo('local-repo'),
|
||||
makeRepo('ssh-repo', { connectionId: 'win-vm' }),
|
||||
makeRepo('runtime-repo', { executionHostId: 'runtime:env-1' })
|
||||
]
|
||||
|
||||
expect(
|
||||
resolveMobileNewWorkspaceDialogRepoId({ eligibleRepos, focusedHostScope: 'ssh:win-vm' })
|
||||
).toBe('ssh-repo')
|
||||
expect(
|
||||
resolveMobileNewWorkspaceDialogRepoId({
|
||||
eligibleRepos,
|
||||
focusedHostScope: 'runtime:env-1'
|
||||
})
|
||||
).toBe('runtime-repo')
|
||||
expect(
|
||||
resolveMobileNewWorkspaceDialogRepoId({ eligibleRepos, focusedHostScope: 'local' })
|
||||
).toBe('local-repo')
|
||||
})
|
||||
|
||||
it('refreshes the selected repo from the fresh repo list', () => {
|
||||
const staleSelectedRepo = makeRepo('repo', { path: '/cached/repo' })
|
||||
const freshRepo = makeRepo('repo', { path: '/fresh/repo' })
|
||||
|
||||
@@ -1,49 +1,9 @@
|
||||
type ExecutionHostScope = 'all' | 'local' | `ssh:${string}` | `runtime:${string}`
|
||||
import { getRepoExecutionHostId } from '../../../src/shared/execution-host'
|
||||
|
||||
type MobileNewWorkspaceDialogRepo = {
|
||||
id: string
|
||||
path: string
|
||||
connectionId?: string | null
|
||||
executionHostId?: 'local' | `ssh:${string}` | `runtime:${string}` | null
|
||||
}
|
||||
|
||||
function getMobileRepoExecutionHostId(
|
||||
repo: MobileNewWorkspaceDialogRepo
|
||||
): Exclude<ExecutionHostScope, 'all'> {
|
||||
if (repo.executionHostId) {
|
||||
return repo.executionHostId
|
||||
}
|
||||
return repo.connectionId ? `ssh:${encodeURIComponent(repo.connectionId)}` : 'local'
|
||||
}
|
||||
|
||||
export function resolveMobileNewWorkspaceDialogRepoId({
|
||||
eligibleRepos,
|
||||
draftRepoId,
|
||||
initialRepoId,
|
||||
activeRepoId,
|
||||
focusedHostScope
|
||||
}: {
|
||||
eligibleRepos: readonly MobileNewWorkspaceDialogRepo[]
|
||||
draftRepoId?: string | null
|
||||
initialRepoId?: string | null
|
||||
activeRepoId?: string | null
|
||||
focusedHostScope?: ExecutionHostScope | null
|
||||
}): string {
|
||||
// Why: Metro cannot bundle runtime imports from the root shared tree today.
|
||||
// Keep this mirror in sync with src/shared/new-workspace-dialog-repo.ts.
|
||||
const focusedHostRepo =
|
||||
focusedHostScope && focusedHostScope !== 'all'
|
||||
? eligibleRepos.find((repo) => getMobileRepoExecutionHostId(repo) === focusedHostScope)
|
||||
: undefined
|
||||
|
||||
const resolvedRepo =
|
||||
(draftRepoId && eligibleRepos.find((repo) => repo.id === draftRepoId)) ||
|
||||
(initialRepoId && eligibleRepos.find((repo) => repo.id === initialRepoId)) ||
|
||||
(activeRepoId && eligibleRepos.find((repo) => repo.id === activeRepoId)) ||
|
||||
focusedHostRepo ||
|
||||
eligibleRepos[0]
|
||||
|
||||
return resolvedRepo?.id ?? ''
|
||||
executionHostId?: string | null
|
||||
}
|
||||
|
||||
/** Whether two listed repos are the same one: a repo id names one repo per host, not overall. */
|
||||
@@ -51,7 +11,7 @@ export function isSameMobileWorkspaceRepo(
|
||||
a: MobileNewWorkspaceDialogRepo,
|
||||
b: MobileNewWorkspaceDialogRepo
|
||||
): boolean {
|
||||
return a.id === b.id && getMobileRepoExecutionHostId(a) === getMobileRepoExecutionHostId(b)
|
||||
return a.id === b.id && getRepoExecutionHostId(a) === getRepoExecutionHostId(b)
|
||||
}
|
||||
|
||||
export function refreshMobileNewWorkspaceDialogSelectedRepo<T extends MobileNewWorkspaceDialogRepo>(
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { Repo } from '../../../shared/repo-types'
|
||||
import {
|
||||
getComposerEligibleRepos,
|
||||
resolveComposerActiveRepoId,
|
||||
resolveComposerRepoId
|
||||
} from './new-workspace-composer-repo'
|
||||
import { resolveComposerRepoId } from './new-workspace-composer-repo'
|
||||
|
||||
function makeRepo(id: string, overrides: Partial<Repo> = {}): Repo {
|
||||
return {
|
||||
@@ -25,38 +21,4 @@ describe('new-workspace-composer-repo', () => {
|
||||
'local-repo'
|
||||
)
|
||||
})
|
||||
|
||||
describe('resolveComposerActiveRepoId', () => {
|
||||
const localOrca = makeRepo('local-orca', { upstream: { owner: 'stablyai', repo: 'orca' } })
|
||||
const runtimeOrca = makeRepo('runtime-orca', {
|
||||
connectionId: 'runtime-ssh-orca-1',
|
||||
upstream: { owner: 'stablyai', repo: 'orca' }
|
||||
})
|
||||
const otherProject = makeRepo('noqa', { upstream: { owner: 'stablyai', repo: 'noqa' } })
|
||||
const repos = [otherProject, localOrca, runtimeOrca]
|
||||
const eligibleRepos = getComposerEligibleRepos(repos)
|
||||
|
||||
it('maps an active runtime-owned SSH repo to its local same-project sibling', () => {
|
||||
expect(resolveComposerActiveRepoId(repos, eligibleRepos, 'runtime-orca')).toBe('local-orca')
|
||||
})
|
||||
|
||||
it('leaves a normal active repo unchanged', () => {
|
||||
expect(resolveComposerActiveRepoId(repos, eligibleRepos, 'local-orca')).toBe('local-orca')
|
||||
})
|
||||
|
||||
it('keeps the runtime repo id when no same-project sibling is eligible', () => {
|
||||
const onlyRuntime = [runtimeOrca]
|
||||
expect(
|
||||
resolveComposerActiveRepoId(
|
||||
onlyRuntime,
|
||||
getComposerEligibleRepos(onlyRuntime),
|
||||
'runtime-orca'
|
||||
)
|
||||
).toBe('runtime-orca')
|
||||
})
|
||||
|
||||
it('passes through null/undefined active repo', () => {
|
||||
expect(resolveComposerActiveRepoId(repos, eligibleRepos, null)).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,37 +1,22 @@
|
||||
import { isRuntimeOwnedSshTargetId, type ExecutionHostScope } from '../../../shared/execution-host'
|
||||
import type { ExecutionHostScope } from '../../../shared/execution-host'
|
||||
import {
|
||||
getNewWorkspaceDialogEligibleRepos,
|
||||
resolveNewWorkspaceDialogActiveRepoId,
|
||||
resolveNewWorkspaceDialogGitRepoId,
|
||||
resolveNewWorkspaceDialogRepoId
|
||||
} from '../../../shared/new-workspace-dialog-repo'
|
||||
import { getProjectIdentityKey } from '../../../shared/project-host-setup-projection'
|
||||
import type { Repo } from '../../../shared/repo-types'
|
||||
|
||||
export function getComposerEligibleRepos(repos: readonly Repo[]): Repo[] {
|
||||
return getNewWorkspaceDialogEligibleRepos(repos)
|
||||
}
|
||||
|
||||
/**
|
||||
* After creating a per-workspace-env, its runtime-owned SSH repo becomes the active repo — but it's
|
||||
* excluded from the composer's eligible repos (hidden plumbing). Without this, the composer can't
|
||||
* match `activeRepoId`, so it falls back to the first eligible repo (a different project). Map the
|
||||
* active runtime repo to its local sibling in the same project so the composer stays on that project.
|
||||
*/
|
||||
export function resolveComposerActiveRepoId(
|
||||
repos: readonly Repo[],
|
||||
eligibleRepos: readonly Repo[],
|
||||
activeRepoId: string | null | undefined
|
||||
): string | null {
|
||||
if (!activeRepoId) {
|
||||
return activeRepoId ?? null
|
||||
}
|
||||
const activeRepo = repos.find((repo) => repo.id === activeRepoId)
|
||||
if (!activeRepo || !isRuntimeOwnedSshTargetId(activeRepo.connectionId)) {
|
||||
return activeRepoId
|
||||
}
|
||||
const projectKey = getProjectIdentityKey(activeRepo)
|
||||
const sibling = eligibleRepos.find((repo) => getProjectIdentityKey(repo) === projectKey)
|
||||
return sibling?.id ?? activeRepoId
|
||||
return resolveNewWorkspaceDialogActiveRepoId(repos, eligibleRepos, activeRepoId)
|
||||
}
|
||||
|
||||
export function resolveComposerRepoId({
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import type { Repo } from './repo-types'
|
||||
import {
|
||||
getNewWorkspaceDialogEligibleRepos,
|
||||
resolveNewWorkspaceDialogActiveRepoId,
|
||||
resolveNewWorkspaceDialogGitRepoId,
|
||||
resolveNewWorkspaceDialogRepoId
|
||||
} from './new-workspace-dialog-repo'
|
||||
@@ -106,4 +107,42 @@ describe('new workspace dialog repo selection', () => {
|
||||
'local-repo'
|
||||
)
|
||||
})
|
||||
|
||||
describe('resolveNewWorkspaceDialogActiveRepoId', () => {
|
||||
const localOrca = makeRepo('local-orca', { upstream: { owner: 'stablyai', repo: 'orca' } })
|
||||
const runtimeOrca = makeRepo('runtime-orca', {
|
||||
connectionId: 'runtime-ssh-orca-1',
|
||||
upstream: { owner: 'stablyai', repo: 'orca' }
|
||||
})
|
||||
const otherProject = makeRepo('noqa', { upstream: { owner: 'stablyai', repo: 'noqa' } })
|
||||
const repos = [otherProject, localOrca, runtimeOrca]
|
||||
const eligibleRepos = getNewWorkspaceDialogEligibleRepos(repos)
|
||||
|
||||
it('maps an active runtime-owned SSH repo to its local same-project sibling', () => {
|
||||
expect(resolveNewWorkspaceDialogActiveRepoId(repos, eligibleRepos, 'runtime-orca')).toBe(
|
||||
'local-orca'
|
||||
)
|
||||
})
|
||||
|
||||
it('leaves a normal active repo unchanged', () => {
|
||||
expect(resolveNewWorkspaceDialogActiveRepoId(repos, eligibleRepos, 'local-orca')).toBe(
|
||||
'local-orca'
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps the runtime repo id when no same-project sibling is eligible', () => {
|
||||
const onlyRuntime = [runtimeOrca]
|
||||
expect(
|
||||
resolveNewWorkspaceDialogActiveRepoId(
|
||||
onlyRuntime,
|
||||
getNewWorkspaceDialogEligibleRepos(onlyRuntime),
|
||||
'runtime-orca'
|
||||
)
|
||||
).toBe('runtime-orca')
|
||||
})
|
||||
|
||||
it('passes through null/undefined active repo', () => {
|
||||
expect(resolveNewWorkspaceDialogActiveRepoId(repos, eligibleRepos, null)).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
isRuntimeOwnedSshTargetId,
|
||||
type ExecutionHostScope
|
||||
} from './execution-host'
|
||||
import { getProjectIdentityKey } from './project-host-setup-projection'
|
||||
import { isGitRepoKind } from './repo-kind'
|
||||
import type { Repo } from './repo-types'
|
||||
|
||||
@@ -22,6 +23,34 @@ export function getNewWorkspaceDialogEligibleRepos<T extends Pick<Repo, 'path' |
|
||||
return repos.filter((repo) => Boolean(repo.path) && !isRuntimeOwnedSshTargetId(repo.connectionId))
|
||||
}
|
||||
|
||||
type ActiveRepoCandidate = Pick<
|
||||
Repo,
|
||||
'id' | 'connectionId' | 'upstream' | 'repoIcon' | 'gitRemoteIdentity'
|
||||
>
|
||||
|
||||
/**
|
||||
* After creating a per-workspace-env, its runtime-owned SSH repo becomes the active repo — but it's
|
||||
* excluded from the eligible repos (hidden plumbing). Without this, the dialog can't match
|
||||
* `activeRepoId`, so it falls back to the first eligible repo (a different project). Map the active
|
||||
* runtime repo to its sibling in the same project so the dialog stays on that project.
|
||||
*/
|
||||
export function resolveNewWorkspaceDialogActiveRepoId(
|
||||
repos: readonly ActiveRepoCandidate[],
|
||||
eligibleRepos: readonly ActiveRepoCandidate[],
|
||||
activeRepoId: string | null | undefined
|
||||
): string | null {
|
||||
if (!activeRepoId) {
|
||||
return activeRepoId ?? null
|
||||
}
|
||||
const activeRepo = repos.find((repo) => repo.id === activeRepoId)
|
||||
if (!activeRepo || !isRuntimeOwnedSshTargetId(activeRepo.connectionId)) {
|
||||
return activeRepoId
|
||||
}
|
||||
const projectKey = getProjectIdentityKey(activeRepo)
|
||||
const sibling = eligibleRepos.find((repo) => getProjectIdentityKey(repo) === projectKey)
|
||||
return sibling?.id ?? activeRepoId
|
||||
}
|
||||
|
||||
export function resolveNewWorkspaceDialogRepoId({
|
||||
eligibleRepos,
|
||||
draftRepoId,
|
||||
@@ -35,8 +64,7 @@ export function resolveNewWorkspaceDialogRepoId({
|
||||
activeRepoId?: string | null
|
||||
focusedHostScope?: ExecutionHostScope | null
|
||||
}): string {
|
||||
// Why: every new-workspace dialog should seed the repo the same way. Mobile
|
||||
// mirrors this locally because Metro cannot bundle root shared runtime modules.
|
||||
// Why: every new-workspace dialog, desktop and mobile, seeds the repo the same way.
|
||||
const focusedHostRepo =
|
||||
focusedHostScope && focusedHostScope !== ALL_EXECUTION_HOSTS_SCOPE
|
||||
? eligibleRepos.find((repo) => getRepoExecutionHostId(repo) === focusedHostScope)
|
||||
|
||||
Reference in New Issue
Block a user