diff --git a/desktop/src/App.test.tsx b/desktop/src/App.test.tsx index a7183cc..608c678 100644 --- a/desktop/src/App.test.tsx +++ b/desktop/src/App.test.tsx @@ -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 () => { @@ -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(); }); @@ -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' })); diff --git a/desktop/src/App.tsx b/desktop/src/App.tsx index 1de0591..578b4bb 100644 --- a/desktop/src/App.tsx +++ b/desktop/src/App.tsx @@ -229,7 +229,7 @@ export function App() { {state.stage === 'local' && dispatch({ type: 'navigate', stage: 'choice' })} onActivated={(setup) => dispatch({ type: 'operationSettled', setup, stage: 'summary' })} />} {state.stage === 'managed-confirm' &&
SET UP VIDXPInstall and manage VidXP on this computer?You choose the features. VidXP checks the new setup before switching to it, so your current installation stays available.
} {state.stage === 'managed' && state.draft && dispatch({ type: 'operationSettled', setup, draft: null, stage: 'summary' })} />} - {state.stage === 'summary' && profile && 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 && recheck()} onManageManaged={() => void beginManaged()} onSetupChanged={(setup) => dispatch({ type: 'operationSettled', setup, stage: 'summary' })} onChooseAnother={() => dispatch({ type: 'navigate', stage: 'choice', choice: null })} onOpen={openBrowser} />}
Your VidXP settings stay on this computer.Desktop only stops services that it starts.
diff --git a/desktop/src/components/ManagedSetup.tsx b/desktop/src/components/ManagedSetup.tsx index 5e4da2d..59b9d86 100644 --- a/desktop/src/components/ManagedSetup.tsx +++ b/desktop/src/components/ManagedSetup.tsx @@ -344,6 +344,28 @@ export function ManagedSetup({ draftId, selectedManagedRuntimeProfile, onBack, o {failure && !installFailure &&
} + {status?.state === 'broken' && operation !== 'install' && ( + <> + + + {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.'} + + + )} + {!manifest ? (
Loading setup options…
) : ( @@ -424,20 +446,6 @@ export function ManagedSetup({ draftId, selectedManagedRuntimeProfile, onBack, o
{status.detail}
)} - {status?.state === 'broken' && operation !== 'install' && ( - - )} - {status?.state === 'ready' && (
VidXP is installed @@ -447,7 +455,7 @@ export function ManagedSetup({ draftId, selectedManagedRuntimeProfile, onBack, o
)} - {recoverableConfiguration && (dirty || status?.state === 'broken') && {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.'}} + {recoverableConfiguration && dirty && status?.state !== 'broken' && VidXP switches to the updated setup only after it has been installed and checked.} {status?.ready && !displayedRuntimeSelected && Switch back to this installation before preparing models or opening VidXP.}
diff --git a/desktop/src/components/TargetSummary.tsx b/desktop/src/components/TargetSummary.tsx index c647ab6..bfb8520 100644 --- a/desktop/src/components/TargetSummary.tsx +++ b/desktop/src/components/TargetSummary.tsx @@ -355,6 +355,9 @@ export function TargetSummary({ profile, validationError, checking, operationPen {validationError.message}
Technical details{validationError.code}
+ {profile.kind === 'managed' && ( + + )}
)} {runtimeFailure && {runtimeFailure}} diff --git a/docs/desktop.md b/docs/desktop.md index 43a73e1..2253813 100644 --- a/docs/desktop.md +++ b/docs/desktop.md @@ -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.