mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 18:37:24 +00:00
fix(cloud-create): repository step reads 'Get repository' until the boot plan answers, then Clone/Update in place; no placeholder bar
design-squad via punchlist-lead: live, the skeleton bar was nearly invisible, so while 'Start sandbox · Checking your plan…' ran the second step read as an empty, unlabeled row. The row now names itself neutrally (the N-12 string) until the mode is known, with its empty circle; the 150 ms delay and the waitedMs/stopped props go.
This commit is contained in:
@@ -293,11 +293,7 @@ export function VmBuildSheet({
|
||||
: null
|
||||
}
|
||||
timing={timings[step.id] ?? null}
|
||||
stopped={failure !== null}
|
||||
t={t}
|
||||
waitedMs={
|
||||
steps.find((candidate) => candidate.id === 'provision')?.elapsedMs ?? 0
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{/* Orca Cloud: agent setup happens after create (the setup page and the workspace's
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act, cleanup, render } from '@testing-library/react'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { cleanup, render } from '@testing-library/react'
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import type { Translate } from '@/i18n/use-translate'
|
||||
import { VmBuildStepRow } from './VmBuildStepRow'
|
||||
import type { VmBuildStep } from './vm-build-steps'
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
afterEach(cleanup)
|
||||
|
||||
const t: Translate = (_key, fallback) => fallback
|
||||
const pendingClone = (cloneMode?: 'clone' | 'update'): VmBuildStep => ({
|
||||
@@ -22,50 +19,19 @@ const pendingClone = (cloneMode?: 'clone' | 'update'): VmBuildStep => ({
|
||||
...(cloneMode ? { cloneMode } : {})
|
||||
})
|
||||
|
||||
it('names the repository row once: no label guessed, a placeholder only after 150 ms (PC-4)', () => {
|
||||
vi.useFakeTimers()
|
||||
const row = (step: VmBuildStep) => (
|
||||
<ol>
|
||||
<VmBuildStepRow step={step} cloud detail={null} timing={null} t={t} />
|
||||
</ol>
|
||||
)
|
||||
const { container, rerender } = render(row(pendingClone()))
|
||||
// Accessible name only; no visible label, no placeholder yet (the plan usually answers by now).
|
||||
expect(container.textContent).toContain('Repository')
|
||||
expect(container.textContent).not.toContain('Clone repository')
|
||||
expect(container.querySelector('[data-slot="skeleton"]')).toBeNull()
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(160)
|
||||
})
|
||||
expect(container.querySelector('[data-slot="skeleton"]')).not.toBeNull()
|
||||
// The plan answers: the one label, never changed afterwards.
|
||||
rerender(row(pendingClone('update')))
|
||||
expect(container.querySelector('[data-slot="skeleton"]')).toBeNull()
|
||||
expect(container.textContent).toContain('Update repository')
|
||||
})
|
||||
const row = (step: VmBuildStep) => (
|
||||
<ol>
|
||||
<VmBuildStepRow step={step} cloud detail={null} timing={null} t={t} />
|
||||
</ol>
|
||||
)
|
||||
|
||||
it('names an undecided repository step neutrally once the start has failed (N-12)', () => {
|
||||
const { container } = render(
|
||||
<ol>
|
||||
<VmBuildStepRow step={pendingClone()} cloud detail={null} timing={null} stopped t={t} />
|
||||
</ol>
|
||||
)
|
||||
it('reads "Get repository" until the boot plan answers, then switches in place, no placeholder bar', () => {
|
||||
const { container, rerender } = render(row(pendingClone()))
|
||||
expect(container.textContent).toContain('Get repository')
|
||||
expect(container.querySelector('[data-slot="skeleton"]')).toBeNull()
|
||||
})
|
||||
|
||||
it('shows the placeholder at once when the create has already waited past 150 ms (no blank row)', () => {
|
||||
const { container } = render(
|
||||
<ol>
|
||||
<VmBuildStepRow
|
||||
step={pendingClone()}
|
||||
cloud
|
||||
detail={null}
|
||||
timing={null}
|
||||
t={t}
|
||||
waitedMs={12_000}
|
||||
/>
|
||||
</ol>
|
||||
)
|
||||
expect(container.querySelector('[data-slot="skeleton"]')).not.toBeNull()
|
||||
rerender(row(pendingClone('update')))
|
||||
expect(container.textContent).toContain('Update repository')
|
||||
expect(container.textContent).not.toContain('Get repository')
|
||||
rerender(row(pendingClone('clone')))
|
||||
expect(container.textContent).toContain('Clone repository')
|
||||
})
|
||||
|
||||
@@ -3,7 +3,6 @@ import { CircleSlash } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { Translate } from '@/i18n/use-translate'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { StepIcon } from './WorktreeSetupSteps'
|
||||
import type { VmBuildStep } from './vm-build-steps'
|
||||
import {
|
||||
@@ -14,47 +13,21 @@ import {
|
||||
import { formatElapsedTime } from './worktree-creation-clock'
|
||||
import { stepLabel, stepStateLabel } from './vm-build-sheet-copy'
|
||||
|
||||
/** The boot plan usually answers within this; only a slower answer shows the placeholder. */
|
||||
const PLACEHOLDER_DELAY_MS = 150
|
||||
|
||||
/** True once `active` has held for `delayMs` (the row's mode is unknown only once per create). */
|
||||
function useDelayedFlag(active: boolean, delayMs: number): boolean {
|
||||
const [due, setDue] = React.useState(false)
|
||||
React.useEffect(() => {
|
||||
if (!active) {
|
||||
return
|
||||
}
|
||||
const timer = window.setTimeout(() => setDue(true), delayMs)
|
||||
return () => window.clearTimeout(timer)
|
||||
}, [active, delayMs])
|
||||
return active && due
|
||||
}
|
||||
|
||||
/** One timed row of the build checklist; the failure card, not the row, carries the red. */
|
||||
export function VmBuildStepRow({
|
||||
step,
|
||||
cloud,
|
||||
detail,
|
||||
timing,
|
||||
stopped = false,
|
||||
t,
|
||||
waitedMs = 0
|
||||
t
|
||||
}: {
|
||||
step: VmBuildStep
|
||||
cloud: boolean
|
||||
/** The build stopped (a failure card shows): an undecided clone/update never will be decided. */
|
||||
stopped?: boolean
|
||||
/** The step's measured duration, when there is one. */
|
||||
timing: VmBuildStepTiming | null
|
||||
/** What the running step is doing right now, in plain words. */
|
||||
detail: string | null
|
||||
t: Translate
|
||||
/**
|
||||
* How long the create has been running (the sandbox step's time). Past PLACEHOLDER_DELAY_MS the
|
||||
* boot plan had its chance to answer, so a still-unknown row shows its placeholder at once: no
|
||||
* blank row, also not in a fixture or a remount.
|
||||
*/
|
||||
waitedMs?: number
|
||||
}): React.JSX.Element {
|
||||
const labelId = React.useId()
|
||||
// A live percent already says how far along the step is.
|
||||
@@ -65,11 +38,9 @@ export function VmBuildStepRow({
|
||||
timing?.p95Ms ?? (cloud && step.id === 'provision' ? SLOW_WITHOUT_MEASURE_MS : null)
|
||||
const slow =
|
||||
measuring && slowAfterMs !== null && step.elapsedMs !== null && step.elapsedMs > slowAfterMs
|
||||
// Clone or update is known once the boot plan answers; until then (or for good, if the start
|
||||
// failed first) the row reads "Get repository" with its empty circle, then switches in place.
|
||||
const undecided = cloud && step.id === 'clone' && step.state === 'pending' && !step.cloneMode
|
||||
// N-12: once the start has failed, the mode stays unknown for good: name the step neutrally.
|
||||
const modeUnknown = undecided && !stopped
|
||||
const placeholderDelayed = useDelayedFlag(modeUnknown, PLACEHOLDER_DELAY_MS)
|
||||
const placeholderDue = modeUnknown && (placeholderDelayed || waitedMs >= PLACEHOLDER_DELAY_MS)
|
||||
|
||||
return (
|
||||
<li
|
||||
@@ -89,29 +60,9 @@ export function VmBuildStepRow({
|
||||
(step.state === 'pending' || step.skipped) && 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{modeUnknown ? (
|
||||
// Clone or update is decided in a moment (boot plan): a placeholder, never a guessed label.
|
||||
// Usually decided within ~150 ms, so the placeholder shows only after that (no blink).
|
||||
<>
|
||||
<span className="sr-only">
|
||||
{t(
|
||||
'auto.components.worktree.creation.VmBuildSheet.repositoryPending',
|
||||
'Repository'
|
||||
)}
|
||||
</span>
|
||||
{placeholderDue ? (
|
||||
<Skeleton className="inline-block h-3 w-28 align-middle" />
|
||||
) : (
|
||||
<span aria-hidden className="invisible">
|
||||
{'\u00a0'}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : undecided ? (
|
||||
t('auto.components.worktree.creation.VmBuildSheet.getRepository', 'Get repository')
|
||||
) : (
|
||||
stepLabel(step, cloud, t)
|
||||
)}
|
||||
{undecided
|
||||
? t('auto.components.worktree.creation.VmBuildSheet.getRepository', 'Get repository')
|
||||
: stepLabel(step, cloud, t)}
|
||||
</span>
|
||||
<span className="sr-only">
|
||||
{step.skipped
|
||||
|
||||
@@ -4258,7 +4258,6 @@
|
||||
"cloneCounting": "Counting objects…",
|
||||
"updateCloud": "Update repository",
|
||||
"updateFetching": "Fetching changes…",
|
||||
"repositoryPending": "Repository",
|
||||
"getRepository": "Get repository"
|
||||
},
|
||||
"VmBuildFailureCard": {
|
||||
|
||||
Reference in New Issue
Block a user