fix(cloud): unreachable GitHub repository on a remote-server project says so with More access (UNREACHABLE-REPO-NO-ROUTE) (#730)

* fix(cloud): Orca Cloud on a remote-server project whose GitHub repository it can't reach says so with More access, not the folder wording (UNREACHABLE-REPO-NO-ROUTE)

* fix(cloud): UNREACHABLE-REPO-NO-ROUTE design-squad: 'Give Orca access' link, muted (not red) precondition line, fixture

* test(cloud): OrcaCloudNeedsRepoSubmit fixtures press the footer's Create

* fix(cloud): UNREACHABLE-REPO-NO-ROUTE the Project field's ring is amber while Orca Cloud waits for a GitHub repository (design-squad); aria-invalid kept

* fix(cloud): UNREACHABLE-REPO-NO-ROUTE ring classes stay literal (localization audit), drop the unreferenced remoteServerDetail key
This commit is contained in:
Neil
2026-11-13 10:06:25 -08:00
committed by GitHub
parent 438982b0ef
commit 3ec2ff557a
13 changed files with 169 additions and 25 deletions
@@ -25,6 +25,7 @@ function cloudOnly(
picked: null,
folderProject: false,
needsGitHubPick: false,
remoteServerProject: false,
githubStep: null,
pick: () => {},
clear: () => {},
@@ -99,6 +99,7 @@ const handoffCloudStart = (githubHandoff: 'waiting' | 'timed-out'): CloudStartVi
picked: null,
folderProject: false,
needsGitHubPick: false,
remoteServerProject: false,
githubStep: null,
pick: () => {},
clear: () => {},
@@ -236,6 +236,8 @@ export function NewWorkspaceComposerProjectSection({
}
triggerClassName="h-9 w-full border-input text-sm focus-visible:border-focus-ring focus-visible:ring-1 focus-visible:ring-focus-ring"
invalid={Boolean(projectError) || Boolean(needsRepo)}
// Waiting for a GitHub repository is a fixable precondition: amber, not red.
invalidTone={!projectError && needsRepo ? 'warning' : 'error'}
describedBy={
!projectError && needsRepo ? ORCA_CLOUD_NEEDS_REPO_ID : projectDescriptionId
}
@@ -255,6 +257,8 @@ export function NewWorkspaceComposerProjectSection({
<OrcaCloudNeedsRepoLine
hasRepos={needsRepo.hasRepos}
onAddRepository={onAddProject}
unreachable={needsRepo.unreachable === true}
onMoreAccess={cloudOnly?.moreAccess}
/>
) : projectOptions.length === 0 ? (
<p id={projectDescriptionId} className="text-[11px] text-muted-foreground">
@@ -23,7 +23,7 @@ import {
import { ProjectOptionMark, ProjectOptionRow } from './ProjectComboboxRow'
import { useRecentProjectIds } from './use-recent-project-ids'
import { isWithinComboboxRoot, useTypeAheadCombobox } from './use-type-ahead-combobox'
import type { ProjectComboboxProps } from './project-combobox-props'
import { projectFieldInvalidClass, type ProjectComboboxProps } from './project-combobox-props'
import { COMBOBOX_FIELD_SHELL, COMBOBOX_POPOVER_SURFACE } from './type-ahead-combobox-styles'
import {
cloudRepoKey,
@@ -48,6 +48,7 @@ export default function ProjectCombobox({
placeholder = 'Choose project',
triggerClassName,
invalid = false,
invalidTone = 'error',
describedBy,
cloudRepos = null,
cloudRepoValue = null,
@@ -218,9 +219,8 @@ export default function ProjectCombobox({
}}
className={cn(
COMBOBOX_FIELD_SHELL,
// Invalid stays red while focused, so the field and its error read as one.
invalid &&
'border-destructive ring-destructive/20 focus-within:border-destructive focus-within:ring-destructive/40 dark:ring-destructive/40',
// Invalid stays marked while focused, so the field and its line read as one.
invalid && projectFieldInvalidClass(invalidTone),
triggerClassName
)}
>
@@ -13,6 +13,11 @@ export type ProjectComboboxProps = {
placeholder?: string
triggerClassName?: string
invalid?: boolean
/**
* `warning`: a fixable precondition (Orca Cloud waiting for a GitHub repository), ringed amber
* rather than red; still aria-invalid for screen readers. Defaults to `error`.
*/
invalidTone?: 'error' | 'warning'
describedBy?: string
/** D4: GitHub repositories with no clone here, listed while Orca Cloud is the Run on target. */
cloudRepos?: readonly OrcaCloudGitHubRepository[] | null
@@ -32,3 +37,10 @@ export type ProjectComboboxProps = {
*/
focusSignal?: number
}
/** The field's invalid ring: red for an error, amber for a fixable precondition (glossary). */
export function projectFieldInvalidClass(tone: 'error' | 'warning'): string {
return tone === 'warning'
? 'border-status-warning ring-status-warning/20 focus-within:border-status-warning focus-within:ring-status-warning/40 dark:ring-status-warning/40'
: 'border-destructive ring-destructive/20 focus-within:border-destructive focus-within:ring-destructive/40 dark:ring-destructive/40'
}
@@ -10,12 +10,44 @@ import { ORCA_CLOUD_NEEDS_REPO_ID } from './cloud-start-view'
*/
export function OrcaCloudNeedsRepoLine({
hasRepos,
onAddRepository
onAddRepository,
unreachable = false,
onMoreAccess
}: {
hasRepos: boolean
onAddRepository: () => void
/** UNREACHABLE-REPO-NO-ROUTE: the project's own GitHub repository is out of Orca Cloud's reach. */
unreachable?: boolean
/** Opens GitHub to give Orca access to more repositories ("Give Orca access"). */
onMoreAccess?: () => void
}): React.JSX.Element {
const t = useTranslate()
if (unreachable) {
return (
<div
id={ORCA_CLOUD_NEEDS_REPO_ID}
role="alert"
className="flex flex-col items-start text-[11px]"
data-orca-cloud-needs-repo="unreachable"
>
<p className="text-muted-foreground">
{t(
'components.newWorkspace.cloudOnly.remoteServerUnreachable',
'Orca Cloud starts from a GitHub repository and can’t reach this project’s. Give Orca access to it, or pick another repository.'
)}
</p>
{onMoreAccess ? (
<div className="-mx-1">
<Button type="button" variant="link" size="statusAction" onClick={onMoreAccess}>
<span className="underline underline-offset-4">
{t('worktreeCreation.cloneAccess.giveAccess', 'Give Orca access')}
</span>
</Button>
</div>
) : null}
</div>
)
}
return (
<div
id={ORCA_CLOUD_NEEDS_REPO_ID}
@@ -23,7 +55,7 @@ export function OrcaCloudNeedsRepoLine({
className="flex flex-col items-start text-[11px]"
data-orca-cloud-needs-repo
>
<p className="text-destructive">
<p className="text-muted-foreground">
{t(
'components.newWorkspace.cloudOnly.needsRepository',
'Orca Cloud needs a GitHub repository.'
@@ -37,17 +37,40 @@ const orcaCloud: EphemeralVmRecipeOption = {
create: 'orca-cloud create'
}
const NEEDS_REPO = { hasRepos: true }
// UNREACHABLE-REPO-NO-ROUTE: Hello-World cloned only on a paired server, out of Orca Cloud's reach.
const UNREACHABLE = { hasRepos: true, unreachable: true }
function Composer(): React.JSX.Element {
function Composer({ unreachable = false }: { unreachable?: boolean }): React.JSX.Element {
const [project, setProject] = useState('folder')
const [created, setCreated] = useState(false)
const needsRepo = project === 'folder' ? NEEDS_REPO : null
const needsRepo = project === 'folder' ? (unreachable ? UNREACHABLE : NEEDS_REPO) : null
const guard = useNeedsRepoSubmitGuard(needsRepo, () => setCreated(true))
const cloudStart = {
onSelectOrcaCloud: () => {},
onUserPick: () => {},
folderProject: project === 'folder',
needsGitHubRepo: guard.shown
needsGitHubRepo: guard.shown,
...(unreachable
? {
cloudOnly: {
repos: [],
showMore: null,
retryRepos: null,
retryingRepos: false,
picked: null,
folderProject: false,
needsGitHubPick: true,
remoteServerProject: true,
githubStep: null,
pick: () => {},
clear: () => {},
moreAccess: () => {},
githubHandoff: 'idle' as const,
checkGitHubAgain: () => {},
start: async () => false
}
}
: {})
}
return (
<div className="w-[520px] space-y-4 rounded-xl border border-border bg-background p-4">
@@ -99,7 +122,8 @@ function Composer(): React.JSX.Element {
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
const clickCreate = (frame: HTMLElement): void =>
[...frame.querySelectorAll<HTMLButtonElement>('button')]
.find((button) => button.textContent?.includes('Create workspace'))
// The footer's primary reads "Create" (its shortcut chip follows).
.find((button) => /^Create\b/.test(button.textContent?.trim() ?? ''))
?.click()
const base = {
@@ -131,3 +155,12 @@ export const fixed = defineFixture({
await wait(200)
}
})
/** A remote-server project whose GitHub repository Orca Cloud can't reach, after Create. */
export const unreachableAfterSubmit = defineFixture({
...base,
render: () => <Composer unreachable />,
interact: async (frame) => {
clickCreate(frame)
await wait(200)
}
})
@@ -16,7 +16,7 @@ export type CloudStartView = {
* Project. `hasRepos`: any to choose from. `attempt` counts those stopped Creates (focus moves to
* Project on each); absent until the first.
*/
needsGitHubRepo?: { hasRepos: boolean; attempt?: number } | null
needsGitHubRepo?: { hasRepos: boolean; unreachable?: boolean; attempt?: number } | null
/** Bumped to open the Project picker at its GitHub repositories (CLONE-ACCESS-ERROR's Choose another). */
openRepoPickerSignal?: number
}
@@ -32,10 +32,19 @@ export function orcaCloudNeedsGitHubRepo(args: {
pickedRepo: boolean
repoCount: number
githubProjectCount: number
}): { hasRepos: boolean } | null {
return args.cloudSelected && args.folderProject && !args.pickedRepo
? { hasRepos: args.repoCount > 0 || args.githubProjectCount > 0 }
: null
/**
* UNREACHABLE-REPO-NO-ROUTE: a GitHub project cloned only on a remote server whose repository
* Orca Cloud can't reach. The line says so and offers More access, instead of the folder wording.
*/
remoteServerProject?: boolean
}): { hasRepos: boolean; unreachable?: true } | null {
if (!args.cloudSelected || !args.folderProject || args.pickedRepo) {
return null
}
return {
hasRepos: args.repoCount > 0 || args.githubProjectCount > 0,
...(args.remoteServerProject ? { unreachable: true as const } : {})
}
}
/** A project that can start in Orca Cloud: git, with a GitHub remote (or one not read yet). */
@@ -0,0 +1,55 @@
// @vitest-environment happy-dom
// UNREACHABLE-REPO-NO-ROUTE: Orca Cloud on a GitHub project cloned only on a remote server, whose
// repository the team's GitHub access doesn't cover. Create waits, and the line under Project says
// why (not the folder wording) with More access; the Project picker lists repositories to pick.
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { orcaCloudNeedsGitHubRepo } from './cloud-start-view'
import { OrcaCloudNeedsRepoLine } from './OrcaCloudNeedsRepoLine'
afterEach(cleanup)
const base = {
cloudSelected: true,
folderProject: true,
pickedRepo: false,
repoCount: 2,
githubProjectCount: 1
}
describe('unreachable repository on a remote-server project', () => {
it('marks the wait as unreachable only for a remote-server project', () => {
expect(orcaCloudNeedsGitHubRepo({ ...base, remoteServerProject: true })).toEqual({
hasRepos: true,
unreachable: true
})
expect(orcaCloudNeedsGitHubRepo(base)).toEqual({ hasRepos: true })
// Reachable (auto-picked): nothing waits.
expect(
orcaCloudNeedsGitHubRepo({ ...base, remoteServerProject: true, pickedRepo: true })
).toBeNull()
})
it('says Orca Cloud cannot reach the repository and offers Give Orca access, in a muted tone', () => {
const onMoreAccess = vi.fn()
render(
<OrcaCloudNeedsRepoLine
hasRepos
onAddRepository={() => {}}
unreachable
onMoreAccess={onMoreAccess}
/>
)
expect(screen.getByRole('alert').textContent).toContain('can’t reach this project’s')
expect(screen.queryByText('Orca Cloud needs a GitHub repository.')).toBeNull()
// A fixable access precondition, not an error: never red.
expect(screen.getByRole('alert').querySelector('p')?.className).not.toContain('destructive')
fireEvent.click(screen.getByRole('button', { name: 'Give Orca access' }))
expect(onMoreAccess).toHaveBeenCalledTimes(1)
})
it('a folder keeps its own wording', () => {
render(<OrcaCloudNeedsRepoLine hasRepos onAddRepository={() => {}} />)
expect(screen.getByRole('alert').textContent).toBe('Orca Cloud needs a GitHub repository.')
})
})
@@ -34,6 +34,8 @@ export type CloudOnlyRepo = {
* cloned only on a remote server (RUNON-NO-NEW-SANDBOX). Create waits until one is picked.
*/
needsGitHubPick: boolean
/** A GitHub project cloned only on a remote server (RUNON-NO-NEW-SANDBOX). */
remoteServerProject: boolean
/** The GitHub row is done for a repository listed from the team's installations. */
githubStep: CloudSetupStep | null
pick: (fullName: string) => void
@@ -140,6 +142,7 @@ export function useCloudOnlyRepo({
githubStep: githubStep ?? folderStep ?? (remoteServer ? remoteServerStep() : null),
folderProject,
needsGitHubPick: folderProject || remoteServer,
remoteServerProject: remoteServer,
pick,
clear,
moreAccess,
@@ -159,8 +162,8 @@ function remoteServerStep(): CloudSetupStep {
label: translate('orcaCloud.setup.github.repositoryLabel', 'GitHub repository'),
status: translate('orcaCloud.setup.github.neededStatus', 'Needed'),
detail: translate(
'components.newWorkspace.cloudOnly.remoteServerDetail',
'Orca Cloud starts from a GitHub repository and can’t reach this project’s. Give Orca access with More access, or pick another repository.'
'components.newWorkspace.cloudOnly.remoteServerUnreachable',
'Orca Cloud starts from a GitHub repository and can’t reach this project’s. Give Orca access to it, or pick another repository.'
)
}
}
@@ -222,6 +222,7 @@ export function useCloudStartComposer({
needsGitHubRepo: orcaCloudNeedsGitHubRepo({
cloudSelected,
folderProject: cloudOnly.needsGitHubPick,
remoteServerProject: cloudOnly.remoteServerProject,
pickedRepo: cloudOnly.picked !== null,
repoCount: cloudOnly.repos?.length ?? 0,
githubProjectCount
-7
View File
@@ -2731,13 +2731,6 @@
"sendingNotConfigured": "sending diagnostics is not configured for this build"
}
},
"enterprise": {
"audit": {
"action": {
"wsOwnerNotified": "Notified the previous owner about an owner change"
}
}
},
"groups": {
"orcaCloud": "Orca Cloud"
},
+1 -1
View File
@@ -24287,7 +24287,7 @@
"theAgent": "The agent",
"showMore": "Show more repositories…",
"folderDetail": "Orca Cloud starts from a GitHub repository. This project is a folder on this computer.",
"remoteServerDetail": "Orca Cloud starts from a GitHub repository and can’t reach this project’s. Give Orca access with More access, or pick another repository.",
"remoteServerUnreachable": "Orca Cloud starts from a GitHub repository and can’t reach this project’s. Give Orca access to it, or pick another repository.",
"headingShort": "GitHub",
"badge": "Cloud only. Orca Cloud clones it, so no copy on this computer is needed.",
"rowPrivate": "{{repo}}, private, cloud only",