From 1876a91d1a59fb1c9dac4490976aa9108fab1179 Mon Sep 17 00:00:00 2001 From: Neil Date: Mon, 9 Nov 2026 15:23:12 -0800 Subject: [PATCH 1/4] fix(build-sheet): W10 follow-ups: member spend-cap copy, Restarting the sandbox, sidebar toggle explains itself during a build, clock resumes after Retry checks, layout-effect focus, announced local error and sign-in row, Intl durations, one spinner style, fresh timing per sheet --- src/main/hosted-sandbox/hosted-account-ipc.ts | 27 +++++++++---- .../src/app-shell/TitlebarMainStrip.tsx | 19 +++++++--- .../cloud-environment/build-log-model.test.ts | 4 +- .../cloud-api/cloud-api-format.test.ts | 2 +- .../VmBuildAgentSignInRow.tsx | 3 +- .../VmBuildFailureActions.tsx | 4 +- .../worktree-creation/VmBuildFailureCard.tsx | 3 +- .../worktree-creation/VmBuildSheet.tsx | 5 +-- .../WorktreeCreationPanel.tsx | 27 ++++++++++++- .../worktree-creation/WorktreeSetupSteps.tsx | 10 ++--- .../worktree-creation/orca-cloud-build-log.ts | 4 +- .../vm-build-failure-copy.ts | 4 +- .../worktree-creation/vm-build-timing.ts | 9 +++-- .../worktree-creation-clock.ts | 38 +++++++++++++++++-- src/renderer/src/i18n/locales/en.json | 15 ++++++-- 15 files changed, 128 insertions(+), 46 deletions(-) diff --git a/src/main/hosted-sandbox/hosted-account-ipc.ts b/src/main/hosted-sandbox/hosted-account-ipc.ts index fcb9c52db80..5c704ce9152 100644 --- a/src/main/hosted-sandbox/hosted-account-ipc.ts +++ b/src/main/hosted-sandbox/hosted-account-ipc.ts @@ -5,20 +5,31 @@ import type { HostedSandboxConfigResponse } from '../../shared/hosted-sandbox/ho import type { HostedSandboxControlPlaneClient } from './hosted-sandbox-control-plane-client' import { failure } from './hosted-workspace-runtime' +const CONFIG_TTL_MS = 5 * 60_000 + export const HOSTED_ACCOUNT_CHANNELS = [CH.getConfig, CH.connectGitHub] as const export function registerHostedAccountHandlers( client: Pick ): void { - // Timings move slowly and the Worker caches them for 5 minutes; one read per app session is plenty. - let config: Promise | null = null + // The Worker re-measures every 5 minutes; match it so each new build sheet sees a fresh median. + let config: { at: number; value: Promise } | null = null ipcMain.handle(CH.getConfig, () => { - config ??= client.config().catch(() => { - // Offline now says nothing about later: ask again next time. - config = null - return null - }) - return config + const now = Date.now() + if (!config || now - config.at >= CONFIG_TTL_MS) { + const entry = { + at: now, + value: client.config().catch(() => { + // Offline now says nothing about later: ask again next time. + if (config === entry) { + config = null + } + return null + }) + } + config = entry + } + return config.value }) ipcMain.handle(CH.connectGitHub, async () => { diff --git a/src/renderer/src/app-shell/TitlebarMainStrip.tsx b/src/renderer/src/app-shell/TitlebarMainStrip.tsx index bf9b5576350..4c85673d892 100644 --- a/src/renderer/src/app-shell/TitlebarMainStrip.tsx +++ b/src/renderer/src/app-shell/TitlebarMainStrip.tsx @@ -5,6 +5,7 @@ import { TOGGLE_TERMINAL_PANE_EXPAND_EVENT } from '@/constants/terminal' import { ActivityTitlebarControls } from '../components/activity/ActivityTitlebarControls' import { useShortcutLabel } from '../hooks/useShortcutLabel' import { useAppStore } from '../store' +import { selectVmBuildSurfaceShown } from '@/lib/workspace/creation/worktree-creation-surface' import { selectActiveTerminalChromeState } from './active-terminal-chrome-selector' import { hasCustomTitleBar } from './app-window-chrome' import type { AppChromeLayout } from './use-app-chrome-layout' @@ -13,21 +14,29 @@ export function RightSidebarToggle(): React.JSX.Element { const t = useTranslate() const toggleRightSidebar = useAppStore((s) => s.toggleRightSidebar) const rightSidebarShortcutLabel = useShortcutLabel('sidebar.right.toggle') + // A build sheet keeps the sidebar closed (it has no workspace to show yet), so say why instead of doing nothing. + const building = useAppStore(selectVmBuildSurfaceShown) return ( - {t('auto.App.c184e056de', 'Toggle right sidebar ({{value0}})', { - value0: rightSidebarShortcutLabel - })} + {building + ? t( + 'auto.app.shell.TitlebarMainStrip.sidebarAfterBuild', + 'Available when the workspace is ready' + ) + : t('auto.App.c184e056de', 'Toggle right sidebar ({{value0}})', { + value0: rightSidebarShortcutLabel + })} ) diff --git a/src/renderer/src/components/cloud-environment/build-log-model.test.ts b/src/renderer/src/components/cloud-environment/build-log-model.test.ts index a4ca69afa54..5f34ee60e27 100644 --- a/src/renderer/src/components/cloud-environment/build-log-model.test.ts +++ b/src/renderer/src/components/cloud-environment/build-log-model.test.ts @@ -299,7 +299,7 @@ describe('formatStepDuration', () => { it('formats compactly', () => { expect(formatStepDuration(null)).toBe('') expect(formatStepDuration(4_200)).toBe('4s') - expect(formatStepDuration(125_000)).toBe('2m 05s') - expect(formatStepDuration(3_725_000)).toBe('62m 05s') + expect(formatStepDuration(125_000)).toBe('2m 5s') + expect(formatStepDuration(3_725_000)).toBe('1h 2m 5s') }) }) diff --git a/src/renderer/src/components/settings/cloud-api/cloud-api-format.test.ts b/src/renderer/src/components/settings/cloud-api/cloud-api-format.test.ts index 7b46a14fb26..1312784f4ab 100644 --- a/src/renderer/src/components/settings/cloud-api/cloud-api-format.test.ts +++ b/src/renderer/src/components/settings/cloud-api/cloud-api-format.test.ts @@ -41,7 +41,7 @@ describe('formatting', () => { expect(formatRunDuration(null)).toBe('—') expect(formatRunDuration(38_000)).toBe('38s') expect(formatRunDuration(252_000)).toBe('4m 12s') - expect(formatRunDuration(1_800_000)).toBe('30m 00s') + expect(formatRunDuration(1_800_000)).toBe('30m 0s') expect(formatCountdown(582_000)).toBe('9:42') expect(formatCountdown(-5)).toBe('0:00') }) diff --git a/src/renderer/src/components/worktree-creation/VmBuildAgentSignInRow.tsx b/src/renderer/src/components/worktree-creation/VmBuildAgentSignInRow.tsx index 177f18441ff..a108d170c0f 100644 --- a/src/renderer/src/components/worktree-creation/VmBuildAgentSignInRow.tsx +++ b/src/renderer/src/components/worktree-creation/VmBuildAgentSignInRow.tsx @@ -155,7 +155,8 @@ export function VmBuildAgentSignInRowView({
  • {icon} -
    + {/* Sign-in progresses on its own (waiting, code shown, done); screen readers hear each step. */} +
    {t('vmBuildAgentSignIn.title', 'Agent sign-in')} {tag ? · {tag} : null} diff --git a/src/renderer/src/components/worktree-creation/VmBuildFailureActions.tsx b/src/renderer/src/components/worktree-creation/VmBuildFailureActions.tsx index b77b0e63c48..49ce22447c9 100644 --- a/src/renderer/src/components/worktree-creation/VmBuildFailureActions.tsx +++ b/src/renderer/src/components/worktree-creation/VmBuildFailureActions.tsx @@ -129,7 +129,7 @@ function failureActions( label: signingIn ? t('auto.components.OrcaSignInProgress.signingIn', 'Signing in…') : t('auto.components.worktree.creation.VmBuildFailureCard.signIn', 'Sign in'), - icon: signingIn ? : , + icon: signingIn ? : , onClick: actions.onSignIn, pending: signingIn } @@ -216,7 +216,7 @@ function failureActions( 'auto.components.worktree.creation.VmBuildFailureCard.connectGitHub', 'Connect GitHub' ), - icon: connectingGitHub ? : , + icon: connectingGitHub ? : , onClick: actions.onConnectGitHub, pending: connectingGitHub }, diff --git a/src/renderer/src/components/worktree-creation/VmBuildFailureCard.tsx b/src/renderer/src/components/worktree-creation/VmBuildFailureCard.tsx index fd5245ced1b..80d170a416e 100644 --- a/src/renderer/src/components/worktree-creation/VmBuildFailureCard.tsx +++ b/src/renderer/src/components/worktree-creation/VmBuildFailureCard.tsx @@ -69,7 +69,8 @@ export function VmBuildFailureCard({ const headingRef = React.useRef(null) const cardRef = React.useRef(null) // Move focus to the card when it opens, so keyboard and screen-reader users land on the way forward. - React.useEffect(() => { + // Layout effect: focus lands before paint, so the collapsing card never strands it on . + React.useLayoutEffect(() => { const card = cardRef.current const sheet = card?.closest('[data-vm-build-sheet]') const active = document.activeElement diff --git a/src/renderer/src/components/worktree-creation/VmBuildSheet.tsx b/src/renderer/src/components/worktree-creation/VmBuildSheet.tsx index a16219c229a..652cb0bdb16 100644 --- a/src/renderer/src/components/worktree-creation/VmBuildSheet.tsx +++ b/src/renderer/src/components/worktree-creation/VmBuildSheet.tsx @@ -173,10 +173,7 @@ export function VmBuildSheet({
    {building ? ( - + ) : state === 'ready' ? ( ) : gated ? ( diff --git a/src/renderer/src/components/worktree-creation/WorktreeCreationPanel.tsx b/src/renderer/src/components/worktree-creation/WorktreeCreationPanel.tsx index 51033961eba..d84d5379784 100644 --- a/src/renderer/src/components/worktree-creation/WorktreeCreationPanel.tsx +++ b/src/renderer/src/components/worktree-creation/WorktreeCreationPanel.tsx @@ -32,6 +32,31 @@ export default function WorktreeCreationPanel({ const now = useWorktreeCreationClock( entry?.status === 'creating' && entry.environmentCheck?.status !== 'failed' ) + // Time spent waiting on the user after failed checks: Retry checks resumes the clock from where it + // stopped instead of jumping ahead by the wait. + const [paused, setPaused] = React.useState<{ + run: number | undefined + ms: number + waitingSince: number | undefined + }>({ run: entry?.startedAt, ms: 0, waitingSince: undefined }) + const restartedChecksAt = + entry?.environmentCheck?.status === 'running' ? entry.environmentCheck.startedAt : undefined + if (paused.run !== entry?.startedAt) { + // Try again starts a new run with its own clock. + setPaused({ run: entry?.startedAt, ms: 0, waitingSince: checksFinishedAt }) + } else if (checksFinishedAt !== undefined && paused.waitingSince !== checksFinishedAt) { + setPaused({ ...paused, waitingSince: checksFinishedAt }) + } else if ( + checksFinishedAt === undefined && + paused.waitingSince !== undefined && + restartedChecksAt !== undefined + ) { + setPaused({ + ...paused, + ms: paused.ms + Math.max(0, restartedChecksAt - paused.waitingSince), + waitingSince: undefined + }) + } if (!entry) { return null } @@ -47,7 +72,7 @@ export default function WorktreeCreationPanel({ const isVmCreation = Boolean(entry.request.ephemeralVmRecipe) || entry.phase === 'provisioning-vm' const title = entry.request.displayName || entry.request.name const stoppedAt = checksFinishedAt ?? (isError ? entry.errorAt : undefined) - const elapsedLabel = formatElapsedTime((stoppedAt ?? now) - entry.startedAt) + const elapsedLabel = formatElapsedTime((stoppedAt ?? now) - entry.startedAt - paused.ms) const checkoutProgress = getVisibleCheckoutProgress(entry) return ( diff --git a/src/renderer/src/components/worktree-creation/WorktreeSetupSteps.tsx b/src/renderer/src/components/worktree-creation/WorktreeSetupSteps.tsx index a1575b6951a..44d33ab243b 100644 --- a/src/renderer/src/components/worktree-creation/WorktreeSetupSteps.tsx +++ b/src/renderer/src/components/worktree-creation/WorktreeSetupSteps.tsx @@ -32,10 +32,7 @@ export function StepIcon({ state }: { state: WorktreeSetupStep['state'] }): Reac switch (state) { case 'running': return ( - + ) case 'done': return @@ -102,7 +99,10 @@ export function WorktreeSetupSteps({ {error !== null ? (
    -

    {error}

    + {/* The failure is the one thing that changed; announce it like the VM sheet's card does. */} +

    + {error} +