Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions desktop/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -394,9 +394,41 @@ describe('desktop target lifecycle', () => {
it('directs a managed target without browser surface back to managed setup', async () => {
const setup = { profiles: [managedProfile], selected_profile_id: managedProfile.id, issues: [] };
mocks.targetSetupState.mockResolvedValue(setup); mocks.recheckTargetState.mockResolvedValue(setup);
renderApp();
const user = userEvent.setup(); renderApp();
expect(await screen.findByText('The browser interface is not enabled')).toBeVisible();
expect(screen.getByRole('button', { name: 'Setup options' })).toBeEnabled();
const setupOptions = screen.getByRole('button', { name: 'Setup options' });
await waitFor(() => expect(setupOptions).toBeEnabled());
await user.click(setupOptions);
expect(await screen.findByRole('heading', { name: 'Choose your VidXP features' })).toBeVisible();
expect(screen.queryByRole('heading', { name: 'Install and manage VidXP on this computer?' })).not.toBeInTheDocument();
});

it('opens a broken managed installation directly on a visible repair action', async () => {
const broken = {
...managedProfile,
validation_error: {
code: 'validation_required',
message: 'The desktop runtime needs to be installed for this app version.',
},
};
const setup = { profiles: [broken], selected_profile_id: broken.id, issues: [] };
mocks.targetSetupState.mockResolvedValue(setup);
mocks.recheckTargetState.mockResolvedValue(setup);
mocks.runtimeStatus.mockResolvedValue({
state: 'broken', ready: false, runtime_profile: 'runtime-a', package_version: '0.4.0',
capabilities: ['scene'], surfaces: [], model_directory: 'C:\\Models',
detail: 'The desktop runtime needs to be installed for this app version.',
});
const user = userEvent.setup(); renderApp();

const repair = await screen.findByRole('button', { name: 'Repair VidXP' });
await waitFor(() => expect(repair).toBeEnabled());
await user.click(repair);

expect(await screen.findByRole('heading', { name: 'Choose your VidXP features' })).toBeVisible();
expect(screen.queryByRole('heading', { name: 'Install and manage VidXP on this computer?' })).not.toBeInTheDocument();
expect(screen.getByText('VidXP needs attention')).toBeVisible();
expect(screen.getByRole('button', { name: 'Repair now' })).toBeVisible();
});

it('keeps ready managed settings read-only until a draft is dirty, then offers Apply and Reset', async () => {
Expand Down Expand Up @@ -431,6 +463,7 @@ describe('desktop target lifecycle', () => {
expect(screen.getByRole('checkbox', { name: /VidXP app|Browser interface/i })).not.toBeChecked();
await user.click(screen.getByText('Storage location'));
expect(screen.getByText('D:\\CustomModels')).toBeVisible();
expect(screen.getByRole('button', { name: 'Repair now' })).toBeVisible();
expect(screen.getByRole('button', { name: 'Repair VidXP' })).toBeEnabled();
});

Expand Down Expand Up @@ -581,7 +614,6 @@ describe('desktop target lifecycle', () => {
mocks.runtimeStatus.mockResolvedValue({ state: 'ready', ready: true, runtime_profile: 'runtime-a', package_version: '0.4.0', capabilities: ['scene'], surfaces: [], model_directory: 'C:\\Models', detail: 'Ready.' });
const user = userEvent.setup(); renderApp();
await user.click(await screen.findByRole('button', { name: 'Setup options' }));
await user.click(screen.getByRole('button', { name: 'Choose features' }));
await screen.findByRole('heading', { name: 'Choose your VidXP features' });
expect(screen.getByRole('button', { name: 'Apply update' })).toBeDisabled();
await user.click(screen.getByRole('button', { name: 'Check downloaded models' }));
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export function App() {
{state.stage === 'local' && <LocalSetup onBack={() => dispatch({ type: 'navigate', stage: 'choice' })} onActivated={(setup) => dispatch({ type: 'operationSettled', setup, stage: 'summary' })} />}
{state.stage === 'managed-confirm' && <section aria-labelledby="managed-confirm-title"><Button variant="subtle" leftSection={<IconArrowLeft size={17} />} disabled={operationPending} onClick={() => dispatch({ type: 'navigate', stage: 'choice' })}>Back</Button><div className="confirmationPanel"><ThemeIcon size={54} radius="xl" variant="light"><IconDownload size={28} /></ThemeIcon><Text className="eyebrow" mt="xl">SET UP VIDXP</Text><Title id="managed-confirm-title" order={1} className="pageTitle">Install and manage VidXP on this computer?</Title><Text className="lede centeredCopy">You choose the features. VidXP checks the new setup before switching to it, so your current installation stays available.</Text><Group justify="center" mt="xl"><Button variant="default" disabled={operationPending} onClick={() => dispatch({ type: 'navigate', stage: 'choice' })}>Cancel</Button><Button loading={state.operation === 'begin-managed'} disabled={operationPending} onClick={() => void beginManaged()}>Choose features</Button></Group></div></section>}
{state.stage === 'managed' && state.draft && <ManagedSetup draftId={state.draft.id} selectedManagedRuntimeProfile={profile?.kind === 'managed' ? profile.managed_runtime_profile ?? null : null} onBack={cancelManaged} onCommitted={(setup) => dispatch({ type: 'operationSettled', setup, draft: null, stage: 'summary' })} />}
{state.stage === 'summary' && profile && <TargetSummary profile={profile} validationError={profile.validation_error} checking={state.operation === 'startup-check' || state.operation === 'recheck'} opening={state.operation === 'open-browser'} operationPending={operationPending} onRecheck={() => recheck()} onManageManaged={() => dispatch({ type: 'navigate', stage: 'managed-confirm' })} onSetupChanged={(setup) => dispatch({ type: 'operationSettled', setup, stage: 'summary' })} onChooseAnother={() => dispatch({ type: 'navigate', stage: 'choice', choice: null })} onOpen={openBrowser} />}
{state.stage === 'summary' && profile && <TargetSummary profile={profile} validationError={profile.validation_error} checking={state.operation === 'startup-check' || state.operation === 'recheck'} opening={state.operation === 'open-browser'} operationPending={operationPending} onRecheck={() => recheck()} onManageManaged={() => void beginManaged()} onSetupChanged={(setup) => dispatch({ type: 'operationSettled', setup, stage: 'summary' })} onChooseAnother={() => dispatch({ type: 'navigate', stage: 'choice', choice: null })} onOpen={openBrowser} />}
</div><footer className="appFooter"><span>Your VidXP settings stay on this computer.</span><span>Desktop only stops services that it starts.</span></footer></main></div>
</div>
</DesktopViewport>
Expand Down
38 changes: 23 additions & 15 deletions desktop/src/components/ManagedSetup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,28 @@ export function ManagedSetup({ draftId, selectedManagedRuntimeProfile, onBack, o

{failure && !installFailure && <div ref={failureAlert} tabIndex={-1}><Alert mb="md" icon={<IconAlertCircle aria-hidden="true" />} color="red" title="Could not continue" role="alert">{failure}</Alert></div>}

{status?.state === 'broken' && operation !== 'install' && (
<>
<Alert
className="managedAttention"
icon={<IconAlertCircle aria-hidden="true" />}
color="yellow"
title={corruptPointer ? 'VidXP could not read the saved setup' : attentionTitle}
role="alert"
>
{corruptPointer
? <Text size="sm">Review the options below and rebuild VidXP. Your saved setup is not changed until the new one is ready.</Text>
: <><Text size="sm">Repair this Desktop-managed installation now, or review its saved features below first.</Text><details className="technicalDetails"><summary>Technical details</summary>{message}</details></>}
<Button mt="md" disabled={!manifest || isBusy} onClick={() => void install()}>
{corruptPointer ? 'Rebuild now' : dirty ? 'Apply update now' : 'Repair now'}
</Button>
</Alert>
<Alert color="yellow" title={dirty ? 'Your current setup stays available during the update' : 'Repair keeps your selected features'}>
{dirty ? 'VidXP switches to the updated setup only after it has been installed and checked.' : 'VidXP first repairs the video tools, then restores this installation only if needed.'}
</Alert>
</>
)}

{!manifest ? (
<div className="emptyState" role="status" aria-live="polite"><Loader size="sm" /> Loading setup options…</div>
) : (
Expand Down Expand Up @@ -424,20 +446,6 @@ export function ManagedSetup({ draftId, selectedManagedRuntimeProfile, onBack, o
<div className="neutralSetupNote" role="status">{status.detail}</div>
)}

{status?.state === 'broken' && operation !== 'install' && (
<Alert
className="managedAttention"
icon={<IconAlertCircle aria-hidden="true" />}
color="yellow"
title={corruptPointer ? 'VidXP could not read the saved setup' : attentionTitle}
role="alert"
>
{corruptPointer
? 'Review the options above and rebuild VidXP. Your saved setup is not changed until the new one is ready.'
: <><Text size="sm">Use the repair action below to check and restore this installation.</Text><details className="technicalDetails"><summary>Technical details</summary>{message}</details></>}
</Alert>
)}

{status?.state === 'ready' && (
<div className="runtimeSummary">
<Text fw={700}>VidXP is installed</Text>
Expand All @@ -447,7 +455,7 @@ export function ManagedSetup({ draftId, selectedManagedRuntimeProfile, onBack, o
</div>
)}

{recoverableConfiguration && (dirty || status?.state === 'broken') && <Alert color="yellow" title={dirty ? 'Your current setup stays available during the update' : 'Repair keeps your selected features'}>{dirty ? 'VidXP switches to the updated setup only after it has been installed and checked.' : 'VidXP first repairs the video tools, then restores this installation only if needed.'}</Alert>}
{recoverableConfiguration && dirty && status?.state !== 'broken' && <Alert color="yellow" title="Your current setup stays available during the update">VidXP switches to the updated setup only after it has been installed and checked.</Alert>}
{status?.ready && !displayedRuntimeSelected && <Alert color="yellow" title="This is not your active installation">Switch back to this installation before preparing models or opening VidXP.</Alert>}

<div className="managedFooter">
Expand Down
3 changes: 3 additions & 0 deletions desktop/src/components/TargetSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,9 @@ export function TargetSummary({ profile, validationError, checking, operationPen
<Alert color="red" title="This setup needs attention" role="alert" mb="md">
{validationError.message}
<details className="technicalDetails"><summary>Technical details</summary><Code>{validationError.code}</Code></details>
{profile.kind === 'managed' && (
<Button mt="md" variant="light" loading={operationPending} onClick={onManageManaged}>Repair VidXP</Button>
)}
</Alert>
)}
{runtimeFailure && <Alert color="red" title="That did not work" role="alert" mb="md">{runtimeFailure}</Alert>}
Expand Down
4 changes: 4 additions & 0 deletions docs/desktop.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ implementation:
- managed repositories and models use the same platform VidXP data directory
as the CLI; adopted targets retain their reported roots, while managed Python
and package environments use private desktop application-data directories;
- Desktop-managed launchers stay private and are restored through their saved
managed profile rather than being added to `PATH` or offered for external
adoption. This preserves Desktop's lifecycle ownership, version checks, and
controlled launch environment without shadowing a separately installed CLI;
- the existing DBOS worker remains the durable execution boundary; and
- closing the desktop process stops the exact browser and API processes it launched,
while broad worker shutdown remains limited to desktop-owned runtimes.
Expand Down