Skip to content
Draft
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
11 changes: 6 additions & 5 deletions apps/studio/src/main-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import fs from 'fs';
import * as path from 'path';
import { pathToFileURL } from 'url';
import { portFinder } from '@studio/common/lib/port-finder';
import { normalizeStudioUiMode } from '@studio/common/lib/studio-ui-mode';
import {
DEFAULT_HEIGHT,
DEFAULT_WIDTH,
Expand All @@ -27,7 +28,7 @@ import {
loadWindowBounds,
saveWindowBounds,
} from 'src/storage/user-data';
import type { StudioUiMode } from '@studio/common/types/desk';
import type { StoredStudioUiMode, StudioUiMode } from '@studio/common/types/desk';
import type { UserData, WindowBounds } from 'src/storage/storage-types';

let mainWindow: BrowserWindow | null;
Expand All @@ -40,8 +41,7 @@ interface RendererLocation {
}

export function getPreferredStudioUiMode( userData: Pick< UserData, 'desks' > ): StudioUiMode {
const preferredMode = userData.desks?.defaultUiMode;
return preferredMode === 'desks' || preferredMode === 'agentic' ? preferredMode : 'default';
return normalizeStudioUiMode( userData.desks?.defaultUiMode );
}

function getRendererFilePath( mode: StudioUiMode ) {
Expand Down Expand Up @@ -120,13 +120,14 @@ async function loadRendererLocation( window: BrowserWindow, location: RendererLo

export async function loadMainWindowRenderer(
window: BrowserWindow,
mode?: StudioUiMode
mode?: StoredStudioUiMode
): Promise< void > {
const userData = await loadUserData();
const normalizedMode = normalizeStudioUiMode( mode );
const location = getRendererLocation( {
desks: {
...userData.desks,
...( mode ? { defaultUiMode: mode } : {} ),
...( mode ? { defaultUiMode: normalizedMode } : {} ),
},
} );
await loadRendererLocation( window, location );
Expand Down
102 changes: 102 additions & 0 deletions apps/studio/src/modules/desks/lib/ipc-handlers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* @vitest-environment node
*/
import { BrowserWindow, type IpcMainInvokeEvent } from 'electron';
import { vi } from 'vitest';
import { loadMainWindowRenderer } from 'src/main-window';
import { loadUserData, lockAppdata, saveUserData, unlockAppdata } from 'src/storage/user-data';
import { getStudioUiMode, setStudioUiMode } from './ipc-handlers';
import type { UserData } from 'src/storage/storage-types';

vi.mock( 'electron', () => ( {
BrowserWindow: {
fromWebContents: vi.fn(),
},
dialog: {
showOpenDialog: vi.fn(),
showSaveDialog: vi.fn(),
},
} ) );

vi.mock( 'src/main-window', () => ( {
loadMainWindowRenderer: vi.fn().mockResolvedValue( undefined ),
} ) );

vi.mock( 'src/storage/user-data', () => ( {
loadUserData: vi.fn(),
lockAppdata: vi.fn().mockResolvedValue( undefined ),
saveUserData: vi.fn().mockResolvedValue( undefined ),
unlockAppdata: vi.fn().mockResolvedValue( undefined ),
} ) );

const mockEvent = {
sender: {},
frameId: 1,
} as IpcMainInvokeEvent;

const mockUserData: UserData = {
version: 1,
siteMetadata: {},
};

describe( 'desk Studio UI mode IPC handlers', () => {
beforeEach( () => {
vi.clearAllMocks();
vi.useRealTimers();
vi.mocked( loadUserData ).mockResolvedValue( mockUserData );
vi.mocked( BrowserWindow.fromWebContents ).mockReturnValue( {
isDestroyed: vi.fn().mockReturnValue( false ),
} as unknown as BrowserWindow );
} );

it.each( [
[ 'default', 'default' ],
[ 'studio2', 'studio2' ],
[ 'agentic', 'studio2' ],
[ 'desks', 'studio2' ],
] )( 'returns %s stored mode as %s', async ( storedMode, expectedMode ) => {
vi.mocked( loadUserData ).mockResolvedValue( {
...mockUserData,
desks: {
defaultUiMode: storedMode as never,
},
} );

await expect( getStudioUiMode( mockEvent ) ).resolves.toBe( expectedMode );
} );

it( 'normalizes legacy mode before persisting and reloading', async () => {
vi.useFakeTimers();
vi.mocked( loadUserData ).mockResolvedValue( {
...mockUserData,
desks: {
defaultUiMode: 'default',
},
} );
const parentWindow = {
isDestroyed: vi.fn().mockReturnValue( false ),
} as unknown as BrowserWindow;
vi.mocked( BrowserWindow.fromWebContents ).mockReturnValue( parentWindow );

await setStudioUiMode( mockEvent, 'agentic' );
await vi.runAllTimersAsync();

expect( saveUserData ).toHaveBeenCalledWith( {
...mockUserData,
desks: {
defaultUiMode: 'studio2',
},
} );
expect( loadMainWindowRenderer ).toHaveBeenCalledWith( parentWindow, 'studio2' );
} );

it( 'rejects invalid mode before locking or saving', async () => {
await expect( setStudioUiMode( mockEvent, 'invalid' as never ) ).rejects.toThrow(
'Invalid Studio UI mode.'
);

expect( lockAppdata ).not.toHaveBeenCalled();
expect( saveUserData ).not.toHaveBeenCalled();
expect( unlockAppdata ).not.toHaveBeenCalled();
} );
} );
29 changes: 16 additions & 13 deletions apps/studio/src/modules/desks/lib/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@ import fsPromises from 'fs/promises';
import nodePath from 'path';
import { assertDeskConfig } from '@studio/common/lib/desk-config';
import { normalizeDeskSettings } from '@studio/common/lib/desk-settings';
import { type DeskConfig, type DeskSettings, type StudioUiMode } from '@studio/common/types/desk';
import {
assertSupportedStudioUiMode,
normalizeStudioUiMode,
} from '@studio/common/lib/studio-ui-mode';
import {
type DeskConfig,
type DeskSettings,
type StoredStudioUiMode,
type StudioUiMode,
} from '@studio/common/types/desk';
import { __ } from '@wordpress/i18n';
import { loadMainWindowRenderer } from 'src/main-window';
import { loadUserData, lockAppdata, saveUserData, unlockAppdata } from 'src/storage/user-data';
Expand All @@ -18,12 +27,6 @@ function assertSiteId( siteId: unknown ): asserts siteId is string {
}
}

function assertStudioUiMode( mode: unknown ): asserts mode is StudioUiMode {
if ( mode !== 'default' && mode !== 'desks' && mode !== 'agentic' ) {
throw new Error( 'Invalid Studio UI mode.' );
}
}

function getParentWindow( event: IpcMainInvokeEvent, channel: string ) {
const parentWindow = BrowserWindow.fromWebContents( event.sender );
if ( ! parentWindow ) {
Expand Down Expand Up @@ -52,23 +55,23 @@ export async function getDeskSettings( _event: IpcMainInvokeEvent ): Promise< De

export async function getStudioUiMode( _event: IpcMainInvokeEvent ): Promise< StudioUiMode > {
const userData = await loadUserData();
const mode = userData.desks?.defaultUiMode;
return mode === 'desks' || mode === 'agentic' ? mode : 'default';
return normalizeStudioUiMode( userData.desks?.defaultUiMode );
}

export async function setStudioUiMode(
event: IpcMainInvokeEvent,
mode: StudioUiMode
mode: StoredStudioUiMode
): Promise< void > {
assertStudioUiMode( mode );
assertSupportedStudioUiMode( mode );
const normalizedMode = normalizeStudioUiMode( mode );
await lockAppdata();
try {
const userData = await loadUserData();
await saveUserData( {
...userData,
desks: {
...userData.desks,
defaultUiMode: mode,
defaultUiMode: normalizedMode,
},
} );
} finally {
Expand All @@ -78,7 +81,7 @@ export async function setStudioUiMode(
const parentWindow = BrowserWindow.fromWebContents( event.sender );
if ( parentWindow && ! parentWindow.isDestroyed() ) {
setTimeout( () => {
void loadMainWindowRenderer( parentWindow, mode );
void loadMainWindowRenderer( parentWindow, normalizedMode );
}, 0 );
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,8 @@ export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => {
}
};

const switchToDesksUi = () => {
void getIpcApi().setStudioUiMode( 'desks' );
};

const switchToAgenticUi = () => {
void getIpcApi().setStudioUiMode( 'agentic' );
const switchToStudio2Ui = () => {
void getIpcApi().setStudioUiMode( 'studio2' );
};

return (
Expand All @@ -161,14 +157,9 @@ export const PreferencesTab = ( { onClose }: { onClose: () => void } ) => {
) }
{ enableDesksUiSwitch && (
<SettingsFormField label={ __( 'Studio UI' ) }>
<div className="flex flex-wrap gap-3">
<Button variant="secondary" onClick={ switchToDesksUi }>
{ __( 'Switch to Desks UI' ) }
</Button>
<Button variant="secondary" onClick={ switchToAgenticUi }>
{ __( 'Switch to Agentic UI' ) }
</Button>
</div>
<Button variant="secondary" onClick={ switchToStudio2Ui }>
{ __( 'Switch to Studio 2.0' ) }
</Button>
</SettingsFormField>
) }
<div className="mt-auto pt-2 flex justify-end gap-3">
Expand Down
16 changes: 16 additions & 0 deletions apps/ui/src/app/use-ui-mode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { describe, expect, it } from 'vitest';
import { resolveLaunchUiMode } from './use-ui-mode';

describe( 'resolveLaunchUiMode', () => {
it( 'maps Studio 2 launch modes to the classic shell', () => {
expect( resolveLaunchUiMode( 'studio2' ) ).toBe( 'classic' );
expect( resolveLaunchUiMode( 'agentic' ) ).toBe( 'classic' );
expect( resolveLaunchUiMode( 'desks' ) ).toBe( 'classic' );
} );

it( 'ignores missing and unknown launch modes', () => {
expect( resolveLaunchUiMode( null ) ).toBeUndefined();
expect( resolveLaunchUiMode( 'default' ) ).toBeUndefined();
expect( resolveLaunchUiMode( 'bogus' ) ).toBeUndefined();
} );
} );
16 changes: 9 additions & 7 deletions apps/ui/src/app/use-ui-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ import { useCallback, useState } from 'react';
export type UiMode = 'classic' | 'desks';

const STUDIO_UI_MODE_PARAM = 'studio-ui-mode';
const DEFAULT_UI_MODE: UiMode = 'desks';
const DEFAULT_UI_MODE: UiMode = 'classic';

export function resolveLaunchUiMode( mode: string | null ): UiMode | undefined {
if ( mode === 'studio2' || mode === 'agentic' || mode === 'desks' ) {
return 'classic';
}
return undefined;
}

function readLaunchUiMode(): UiMode | undefined {
if ( typeof window === 'undefined' ) {
Expand All @@ -12,12 +19,7 @@ function readLaunchUiMode(): UiMode | undefined {

try {
const mode = new URLSearchParams( window.location.search ).get( STUDIO_UI_MODE_PARAM );
if ( mode === 'desks' ) {
return 'desks';
}
if ( mode === 'agentic' ) {
return 'classic';
}
return resolveLaunchUiMode( mode );
} catch {
return undefined;
}
Expand Down
Loading