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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
16 changes: 8 additions & 8 deletions apps/studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@
"scripts": {
"start": "node ../../scripts/start-studio.mjs",
"prestart": "npm -w wp-studio run build && node ../../scripts/ensure-dev-bundle-id.mjs",
"make": "electron-vite build --config ./electron.vite.config.ts --outDir=dist && tsx ../../scripts/build-desks-renderer.ts && electron-forge make .",
"make:windows-x64": "electron-vite build --config ./electron.vite.config.ts --outDir=dist && tsx ../../scripts/build-desks-renderer.ts && electron-forge make . --arch=x64 --platform=win32",
"make:windows-arm64": "electron-vite build --config ./electron.vite.config.ts --outDir=dist && tsx ../../scripts/build-desks-renderer.ts && electron-forge make . --arch=arm64 --platform=win32",
"make:macos-x64": "electron-vite build --config ./electron.vite.config.ts --outDir=dist && tsx ../../scripts/build-desks-renderer.ts && SKIP_DMG=true FILE_ARCHITECTURE=x64 electron-forge make . --arch=x64 --platform=darwin",
"make:macos-arm64": "electron-vite build --config ./electron.vite.config.ts --outDir=dist && tsx ../../scripts/build-desks-renderer.ts && SKIP_DMG=true FILE_ARCHITECTURE=arm64 electron-forge make . --arch=arm64 --platform=darwin",
"make:linux-x64": "electron-vite build --config ./electron.vite.config.ts --outDir=dist && tsx ../../scripts/build-desks-renderer.ts && electron-forge make . --arch=x64 --platform=linux",
"make:linux-arm64": "electron-vite build --config ./electron.vite.config.ts --outDir=dist && tsx ../../scripts/build-desks-renderer.ts && electron-forge make . --arch=arm64 --platform=linux",
"make": "electron-vite build --config ./electron.vite.config.ts --outDir=dist && tsx ../../scripts/build-ui-renderer.ts && electron-forge make .",
"make:windows-x64": "electron-vite build --config ./electron.vite.config.ts --outDir=dist && tsx ../../scripts/build-ui-renderer.ts && electron-forge make . --arch=x64 --platform=win32",
"make:windows-arm64": "electron-vite build --config ./electron.vite.config.ts --outDir=dist && tsx ../../scripts/build-ui-renderer.ts && electron-forge make . --arch=arm64 --platform=win32",
"make:macos-x64": "electron-vite build --config ./electron.vite.config.ts --outDir=dist && tsx ../../scripts/build-ui-renderer.ts && SKIP_DMG=true FILE_ARCHITECTURE=x64 electron-forge make . --arch=x64 --platform=darwin",
"make:macos-arm64": "electron-vite build --config ./electron.vite.config.ts --outDir=dist && tsx ../../scripts/build-ui-renderer.ts && SKIP_DMG=true FILE_ARCHITECTURE=arm64 electron-forge make . --arch=arm64 --platform=darwin",
"make:linux-x64": "electron-vite build --config ./electron.vite.config.ts --outDir=dist && tsx ../../scripts/build-ui-renderer.ts && electron-forge make . --arch=x64 --platform=linux",
"make:linux-arm64": "electron-vite build --config ./electron.vite.config.ts --outDir=dist && tsx ../../scripts/build-ui-renderer.ts && electron-forge make . --arch=arm64 --platform=linux",
"install:bundle": "npm install --no-package-lock --no-progress --install-links --no-workspaces && patch-package && node ../../scripts/remove-fs-ext-other-platform-binaries.mjs",
"lint": "eslint src e2e",
"package": "electron-vite build --config ./electron.vite.config.ts --outDir=dist && tsx ../../scripts/build-desks-renderer.ts && electron-forge package .",
"package": "electron-vite build --config ./electron.vite.config.ts --outDir=dist && tsx ../../scripts/build-ui-renderer.ts && electron-forge package .",
"publish": "electron-forge publish .",
"start-wayland": "npm -w wp-studio run build && electron-forge start . -- --enable-features=UseOzonePlatform --ozone-platform=wayland",
"typecheck": "tsc -p tsconfig.json --noEmit"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { QueryClient } from '@tanstack/react-query';

// Minimal client for the assistant tab. Session transcripts are persisted on
// disk by the CLI, so we don't mirror the React Query cache to localStorage
// the way the standalone Desks UI does. `useAgentRun` mutates this cache
// disk by the CLI, so we don't mirror the React Query cache to localStorage.
// `useAgentRun` mutates this cache
// during a live run and invalidates explicitly on `run.exited`, so implicit
// refetches stay off to avoid racing those writes.
export const queryClient = new QueryClient( {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import type { PendingSiteCreation } from './use-site-creation-switch';
/**
* Shown when the agent creates a new site mid-conversation. The conversation
* has already been re-homed to the new site; this asks whether to follow it
* there or stay on the current site with a fresh chat. Mirrors the desk UI's
* "Continue in the site desk?" prompt, adapted to the single-site tab.
* there or stay on the current site with a fresh chat.
*
* Pure presentation: the switch/new-chat logic lives in
* `useSiteCreationSwitch`.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useCallback, useEffect, useState } from 'react';
import { getIpcApi } from 'src/lib/get-ipc-api';

// One Studio Code session per site. Unlike the standalone Desks UI there is no
// session list — we persist a single session id per site so the assistant tab
// reopens the same conversation, and "New conversation" just swaps in a fresh
// session id. The transcript itself lives on disk (CLI session store); this
// only remembers which session id belongs to which site.
// One Studio Code session per site. There is no session list in the tab, so
// we persist a single session id per site; the assistant tab reopens the same
// conversation, and "New conversation" swaps in a fresh session id. The
// transcript itself lives on disk (CLI session store); this only remembers
// which session id belongs to which site.
const STORAGE_KEY = 'studio_code_session_ids';

function loadStored( siteId: string ): string | null {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ export interface SiteCreationSwitch {
/**
* Reacts to a site being created mid-conversation. The agent runs in the CLI;
* when its `site_create` tool finishes, the main process re-homes the session
* onto the new site and emits `ai-session-placement-updated`. The desk UI
* (apps/ui) shows a "Continue in the site desk?" dialog off the same event;
* this is the embedded-tab equivalent.
* onto the new site and emits `ai-session-placement-updated`.
*
* The migration happens as soon as the event arrives, not on the user's click:
* the new site is mapped to the moved session and the current site is reset to
Expand Down
18 changes: 4 additions & 14 deletions apps/studio/src/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,16 +233,6 @@ export { getDefaultSiteDirectory, saveDefaultSiteDirectory };

export { importSite, exportSite } from 'src/modules/import-export/lib/ipc-handlers';

export {
exportDeskConfig,
getDeskSettings,
getSiteDeskConfig,
getUserDeskConfig,
importDeskConfig,
saveDeskSettings,
saveSiteDeskConfig,
saveUserDeskConfig,
} from 'src/modules/desks/lib/ipc-handlers';
export { fetchSiteRest as fetchSiteRestApi } from 'src/lib/wordpress-rest-api';

function hydrateAiSessionSummary(
Expand Down Expand Up @@ -316,14 +306,14 @@ export async function createAiSession(
const sitesRoot = getAiSessionsRootDirectory();
if ( ! siteId ) {
const existing = await listHydratedAiSessions( sitesRoot );
const emptyUserDeskSession = existing
const emptyUserSession = existing
.filter(
( session ) => ! session.ownerSitePath && ! session.firstPrompt && ! session.archived
)
.sort( ( a, b ) => Date.parse( b.updatedAt ) - Date.parse( a.updatedAt ) )[ 0 ];

if ( emptyUserDeskSession ) {
return emptyUserDeskSession;
if ( emptyUserSession ) {
return emptyUserSession;
}

return hydrateAiSessionSummary( await createAiSessionInStore( sitesRoot ) );
Expand Down Expand Up @@ -458,7 +448,7 @@ export async function continueAiSession(
} = {}
): Promise< { runId: string } > {
if ( ! ( await oauthClient.isAuthenticated() ) ) {
throw new Error( __( 'WordPress.com login required. Log in to use Studio Desk chat.' ) );
throw new Error( __( 'WordPress.com login required. Log in to use Studio Code.' ) );
}

await reconcileSessionEnvironmentBeforeRun( sessionId );
Expand Down
1 change: 0 additions & 1 deletion apps/studio/src/ipc-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ type IpcApi = {

interface FeatureFlags {
enableAgenticUi: boolean;
enableDesksUi: boolean;
}

interface BetaFeatures {
Expand Down
6 changes: 0 additions & 6 deletions apps/studio/src/lib/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,6 @@ export const FEATURE_FLAGS: Record< keyof FeatureFlags, FeatureFlagDefinition >
flag: 'enableAgenticUi',
default: false,
},
enableDesksUi: {
label: 'Enable Desks UI',
env: 'ENABLE_DESKS_UI',
flag: 'enableDesksUi',
default: false,
},
} as const;

export function getFeatureFlagFromEnv( flag: keyof FeatureFlags ): boolean {
Expand Down
41 changes: 7 additions & 34 deletions apps/studio/src/main-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,18 @@ import {
loadWindowBounds,
saveWindowBounds,
} from 'src/storage/user-data';
import type { StudioUiMode } from '@studio/common/types/desk';
import type { WindowBounds } from 'src/storage/storage-types';

let mainWindow: BrowserWindow | null;
let currentRendererUrl: string | undefined;
type StudioUiMode = 'default' | 'agentic';

interface RendererLocation {
url: string;
filePath?: string;
query?: Record< string, string >;
}

export function getPreferredStudioUiMode(): StudioUiMode {
if ( getFeatureFlagFromEnv( 'enableDesksUi' ) ) {
return 'desks';
}
if ( getFeatureFlagFromEnv( 'enableAgenticUi' ) ) {
return 'agentic';
}
Expand All @@ -53,36 +49,18 @@ export function getPreferredStudioUiMode(): StudioUiMode {
function getRendererFilePath( mode: StudioUiMode ) {
return path.join(
__dirname,
mode === 'default' ? '../renderer/index.html' : '../renderer-desks/index.html'
mode === 'default' ? '../renderer/index.html' : '../renderer-ui/index.html'
);
}

function getRendererQuery( mode: StudioUiMode ): Record< string, string > | undefined {
return mode === 'default' ? undefined : { 'studio-ui-mode': mode };
}

function appendRendererQuery( url: string, query: Record< string, string > | undefined ) {
if ( ! query ) {
return url;
}

const rendererUrl = new URL( url );
for ( const [ key, value ] of Object.entries( query ) ) {
rendererUrl.searchParams.set( key, value );
}
return rendererUrl.toString();
}

function getRendererLocation( preferredMode: StudioUiMode ): RendererLocation {
const preferredQuery = getRendererQuery( preferredMode );

if (
! app.isPackaged &&
preferredMode !== 'default' &&
process.env[ 'ELECTRON_DESKS_RENDERER_URL' ]
preferredMode === 'agentic' &&
process.env[ 'ELECTRON_UI_RENDERER_URL' ]
) {
return {
url: appendRendererQuery( process.env[ 'ELECTRON_DESKS_RENDERER_URL' ], preferredQuery ),
url: process.env[ 'ELECTRON_UI_RENDERER_URL' ],
};
}

Expand All @@ -98,12 +76,10 @@ function getRendererLocation( preferredMode: StudioUiMode ): RendererLocation {
mode = 'default';
filePath = getRendererFilePath( mode );
}
const query = getRendererQuery( mode );

return {
filePath,
query,
url: appendRendererQuery( pathToFileURL( filePath ).href, query ),
url: pathToFileURL( filePath ).href,
};
}

Expand All @@ -114,10 +90,7 @@ function rememberRendererLocation( location: RendererLocation ) {
async function loadRendererLocation( window: BrowserWindow, location: RendererLocation ) {
rememberRendererLocation( location );
if ( location.filePath ) {
await window.loadFile(
location.filePath,
location.query ? { query: location.query } : undefined
);
await window.loadFile( location.filePath );
return;
}
await window.loadURL( location.url );
Expand Down
2 changes: 1 addition & 1 deletion apps/studio/src/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { isUpdateReadyToInstall, manualCheckForUpdates } from 'src/updates';

// Feature flags that select which Studio UI is shown; toggling them requires
// reloading the main window renderer.
const UI_MODE_FEATURE_FLAGS: ( keyof FeatureFlags )[] = [ 'enableAgenticUi', 'enableDesksUi' ];
const UI_MODE_FEATURE_FLAGS: ( keyof FeatureFlags )[] = [ 'enableAgenticUi' ];

export async function setupMenu( config: {
needsOnboarding: boolean;
Expand Down
51 changes: 51 additions & 0 deletions apps/studio/src/migrations/06-remove-desks-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { existsSync } from 'node:fs';
import { getAppConfigPath } from '@studio/common/lib/well-known-paths';
import { readFile, writeFile } from 'atomically';
import { lockAppdata, unlockAppdata } from 'src/storage/user-data';
import type { Migration } from '@studio/common/lib/migration';

function isRecord( value: unknown ): value is Record< string, unknown > {
return Boolean( value ) && typeof value === 'object' && ! Array.isArray( value );
}

async function readAppConfig() {
const appConfigPath = getAppConfigPath();
if ( ! existsSync( appConfigPath ) ) {
return null;
}

try {
const raw = await readFile( appConfigPath, { encoding: 'utf8' } );
const parsed = JSON.parse( raw );
return isRecord( parsed ) ? parsed : null;
} catch {
return null;
}
}

function hasDesksConfig( config: Record< string, unknown > ) {
return Object.prototype.hasOwnProperty.call( config, 'desks' );
}

export const removeDesksConfig: Migration = {
async needsToRun() {
const config = await readAppConfig();
return !! config && hasDesksConfig( config );
},
async run() {
try {
await lockAppdata();
const config = await readAppConfig();
if ( ! config || ! hasDesksConfig( config ) ) {
return;
}

const { desks: _desks, ...rest } = config;
await writeFile( getAppConfigPath(), JSON.stringify( rest, null, 2 ) + '\n', {
encoding: 'utf8',
} );
} finally {
await unlockAppdata();
}
},
};
2 changes: 2 additions & 0 deletions apps/studio/src/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { migrateAppConfig } from './02-migrate-to-split-config';
import { copyHttpsCertsToWellKnown } from './03-copy-https-certs-to-well-known';
import { migrateConnectedSitesToShared } from './04-migrate-connected-sites-to-shared';
import { removeOldServerFilesAndCertificates } from './05-remove-old-server-files-and-certificates';
import { removeDesksConfig } from './06-remove-desks-config';
import type { Migration } from '@studio/common/lib/migration';

export const migrations: Migration[] = [
Expand All @@ -11,4 +12,5 @@ export const migrations: Migration[] = [
copyHttpsCertsToWellKnown,
migrateConnectedSitesToShared,
removeOldServerFilesAndCertificates,
removeDesksConfig,
];
90 changes: 90 additions & 0 deletions apps/studio/src/migrations/tests/06-remove-desks-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* @vitest-environment node
*/
import { existsSync } from 'node:fs';
import { getAppConfigPath } from '@studio/common/lib/well-known-paths';
import { readFile, writeFile } from 'atomically';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { removeDesksConfig } from 'src/migrations/06-remove-desks-config';
import { lockAppdata, unlockAppdata } from 'src/storage/user-data';

vi.mock( 'node:fs' );
vi.mock( 'atomically', () => ( {
readFile: vi.fn(),
writeFile: vi.fn(),
} ) );
vi.mock( '@studio/common/lib/well-known-paths', () => ( {
getAppConfigPath: vi.fn( () => '/mock/app.json' ),
getAppConfigLockFilePath: vi.fn( () => '/mock/app.json.lock' ),
} ) );
vi.mock( 'src/storage/user-data', () => ( {
lockAppdata: vi.fn(),
unlockAppdata: vi.fn(),
} ) );

const appConfig = {
version: 1,
siteMetadata: {},
colorScheme: 'dark',
desks: {
settings: { showSiteName: true },
user: { widgets: [] },
},
};

describe( 'removeDesksConfig', () => {
beforeEach( () => {
vi.clearAllMocks();
vi.mocked( existsSync ).mockReturnValue( true );
vi.mocked( readFile ).mockResolvedValue( Buffer.from( JSON.stringify( appConfig ) ) );
} );

it( 'needs to run when app config has desks data', async () => {
await expect( removeDesksConfig.needsToRun() ).resolves.toBe( true );
} );

it( 'does not need to run when app config has no desks data', async () => {
const { desks: _desks, ...configWithoutDesks } = appConfig;
vi.mocked( readFile ).mockResolvedValueOnce(
Buffer.from( JSON.stringify( configWithoutDesks ) )
);

await expect( removeDesksConfig.needsToRun() ).resolves.toBe( false );
} );

it( 'does not need to run when app config is missing or invalid', async () => {
vi.mocked( existsSync ).mockReturnValueOnce( false );
await expect( removeDesksConfig.needsToRun() ).resolves.toBe( false );

vi.mocked( existsSync ).mockReturnValueOnce( true );
vi.mocked( readFile ).mockResolvedValueOnce( Buffer.from( '{' ) );
await expect( removeDesksConfig.needsToRun() ).resolves.toBe( false );
} );

it( 'removes only desks data from app config under lock', async () => {
await removeDesksConfig.run();

expect( lockAppdata ).toHaveBeenCalledTimes( 1 );
expect( unlockAppdata ).toHaveBeenCalledTimes( 1 );
expect( writeFile ).toHaveBeenCalledWith(
getAppConfigPath(),
JSON.stringify(
{
version: 1,
siteMetadata: {},
colorScheme: 'dark',
},
null,
2
) + '\n',
{ encoding: 'utf8' }
);
} );

it( 'unlocks appdata when writing fails', async () => {
vi.mocked( writeFile ).mockRejectedValueOnce( new Error( 'write failed' ) );

await expect( removeDesksConfig.run() ).rejects.toThrow( 'write failed' );
expect( unlockAppdata ).toHaveBeenCalledTimes( 1 );
} );
} );
Loading
Loading