diff --git a/apps/studio/package.json b/apps/studio/package.json index 02c41cf2a2..0d1ce39a5d 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -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" diff --git a/apps/studio/src/components/studio-code-session/query-client.ts b/apps/studio/src/components/studio-code-session/query-client.ts index 87ea1f7665..20f1394ce3 100644 --- a/apps/studio/src/components/studio-code-session/query-client.ts +++ b/apps/studio/src/components/studio-code-session/query-client.ts @@ -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( { diff --git a/apps/studio/src/components/studio-code-session/site-created-dialog.tsx b/apps/studio/src/components/studio-code-session/site-created-dialog.tsx index 5520589d5d..1bcb285d99 100644 --- a/apps/studio/src/components/studio-code-session/site-created-dialog.tsx +++ b/apps/studio/src/components/studio-code-session/site-created-dialog.tsx @@ -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`. diff --git a/apps/studio/src/components/studio-code-session/use-single-session.ts b/apps/studio/src/components/studio-code-session/use-single-session.ts index d610ee0766..2eaea602a7 100644 --- a/apps/studio/src/components/studio-code-session/use-single-session.ts +++ b/apps/studio/src/components/studio-code-session/use-single-session.ts @@ -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 { diff --git a/apps/studio/src/components/studio-code-session/use-site-creation-switch.ts b/apps/studio/src/components/studio-code-session/use-site-creation-switch.ts index 401625938d..d6b8f29908 100644 --- a/apps/studio/src/components/studio-code-session/use-site-creation-switch.ts +++ b/apps/studio/src/components/studio-code-session/use-site-creation-switch.ts @@ -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 diff --git a/apps/studio/src/ipc-handlers.ts b/apps/studio/src/ipc-handlers.ts index 0b2d00abee..eab4784138 100644 --- a/apps/studio/src/ipc-handlers.ts +++ b/apps/studio/src/ipc-handlers.ts @@ -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( @@ -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 ) ); @@ -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 ); diff --git a/apps/studio/src/ipc-types.d.ts b/apps/studio/src/ipc-types.d.ts index 7539ea97b3..b7803cdcd7 100644 --- a/apps/studio/src/ipc-types.d.ts +++ b/apps/studio/src/ipc-types.d.ts @@ -99,7 +99,6 @@ type IpcApi = { interface FeatureFlags { enableAgenticUi: boolean; - enableDesksUi: boolean; } interface BetaFeatures { diff --git a/apps/studio/src/lib/feature-flags.ts b/apps/studio/src/lib/feature-flags.ts index 80f8758b73..09896aa1fd 100644 --- a/apps/studio/src/lib/feature-flags.ts +++ b/apps/studio/src/lib/feature-flags.ts @@ -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 { diff --git a/apps/studio/src/main-window.ts b/apps/studio/src/main-window.ts index 1f174def5b..cdfbcf6dd8 100644 --- a/apps/studio/src/main-window.ts +++ b/apps/studio/src/main-window.ts @@ -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'; } @@ -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' ], }; } @@ -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, }; } @@ -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 ); diff --git a/apps/studio/src/menu.ts b/apps/studio/src/menu.ts index fd4aa875df..23c3691d62 100644 --- a/apps/studio/src/menu.ts +++ b/apps/studio/src/menu.ts @@ -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; diff --git a/apps/studio/src/migrations/06-remove-desks-config.ts b/apps/studio/src/migrations/06-remove-desks-config.ts new file mode 100644 index 0000000000..c944c125ad --- /dev/null +++ b/apps/studio/src/migrations/06-remove-desks-config.ts @@ -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(); + } + }, +}; diff --git a/apps/studio/src/migrations/index.ts b/apps/studio/src/migrations/index.ts index 8af7cab2f0..5e424e259c 100644 --- a/apps/studio/src/migrations/index.ts +++ b/apps/studio/src/migrations/index.ts @@ -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[] = [ @@ -11,4 +12,5 @@ export const migrations: Migration[] = [ copyHttpsCertsToWellKnown, migrateConnectedSitesToShared, removeOldServerFilesAndCertificates, + removeDesksConfig, ]; diff --git a/apps/studio/src/migrations/tests/06-remove-desks-config.test.ts b/apps/studio/src/migrations/tests/06-remove-desks-config.test.ts new file mode 100644 index 0000000000..515d88d9b0 --- /dev/null +++ b/apps/studio/src/migrations/tests/06-remove-desks-config.test.ts @@ -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 ); + } ); +} ); diff --git a/apps/studio/src/modules/desks/lib/ipc-handlers.ts b/apps/studio/src/modules/desks/lib/ipc-handlers.ts deleted file mode 100644 index 65f72bd458..0000000000 --- a/apps/studio/src/modules/desks/lib/ipc-handlers.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { BrowserWindow, dialog, type IpcMainInvokeEvent } from 'electron'; -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 } from '@studio/common/types/desk'; -import { __ } from '@wordpress/i18n'; -import { loadUserData, lockAppdata, saveUserData, unlockAppdata } from 'src/storage/user-data'; - -function isRecord( value: unknown ): value is Record< string, unknown > { - return Boolean( value ) && typeof value === 'object' && ! Array.isArray( value ); -} - -function assertSiteId( siteId: unknown ): asserts siteId is string { - if ( typeof siteId !== 'string' || ! siteId ) { - throw new Error( 'Invalid site desk config: expected site id.' ); - } -} - -function getParentWindow( event: IpcMainInvokeEvent, channel: string ) { - const parentWindow = BrowserWindow.fromWebContents( event.sender ); - if ( ! parentWindow ) { - throw new Error( `No window found for sender of ${ channel } message: ${ event.frameId }` ); - } - return parentWindow; -} - -function getDeskJsonFilename( suggestedFilename: string ) { - const fallbackFilename = 'studio-desk.json'; - const basename = nodePath.basename( suggestedFilename || fallbackFilename ); - return basename.toLowerCase().endsWith( '.json' ) ? basename : `${ basename }.json`; -} - -export async function getUserDeskConfig( - _event: IpcMainInvokeEvent -): Promise< DeskConfig | undefined > { - const userData = await loadUserData(); - return userData.desks?.user; -} - -export async function getDeskSettings( _event: IpcMainInvokeEvent ): Promise< DeskSettings > { - const userData = await loadUserData(); - return normalizeDeskSettings( userData.desks?.settings ); -} - -export async function saveDeskSettings( - _event: IpcMainInvokeEvent, - settings: DeskSettings -): Promise< void > { - if ( ! isRecord( settings ) ) { - throw new Error( 'Invalid desk settings: expected an object.' ); - } - - const normalizedSettings = normalizeDeskSettings( settings ); - await lockAppdata(); - try { - const userData = await loadUserData(); - await saveUserData( { - ...userData, - desks: { - ...userData.desks, - settings: normalizedSettings, - }, - } ); - } finally { - await unlockAppdata(); - } -} - -export async function exportDeskConfig( - event: IpcMainInvokeEvent, - config: DeskConfig, - suggestedFilename: string -): Promise< string | null > { - assertDeskConfig( config ); - - const { canceled, filePath } = await dialog.showSaveDialog( - getParentWindow( event, 'exportDeskConfig' ), - { - title: __( 'Export desk' ), - defaultPath: getDeskJsonFilename( suggestedFilename ), - filters: [ - { - name: __( 'JSON files' ), - extensions: [ 'json' ], - }, - ], - } - ); - if ( canceled || ! filePath ) { - return null; - } - - const targetPath = filePath.toLowerCase().endsWith( '.json' ) ? filePath : `${ filePath }.json`; - await fsPromises.writeFile( targetPath, `${ JSON.stringify( config, null, 2 ) }\n`, 'utf8' ); - return targetPath; -} - -export async function importDeskConfig( event: IpcMainInvokeEvent ): Promise< DeskConfig | null > { - const { canceled, filePaths } = await dialog.showOpenDialog( - getParentWindow( event, 'importDeskConfig' ), - { - title: __( 'Import desk' ), - filters: [ - { - name: __( 'JSON files' ), - extensions: [ 'json' ], - }, - ], - properties: [ 'openFile' ], - } - ); - if ( canceled || ! filePaths[ 0 ] ) { - return null; - } - - let parsedConfig: unknown; - try { - parsedConfig = JSON.parse( await fsPromises.readFile( filePaths[ 0 ], 'utf8' ) ); - } catch { - throw new Error( 'Could not parse that file. Expected a Studio desk JSON export.' ); - } - - assertDeskConfig( parsedConfig ); - return parsedConfig; -} - -export async function saveUserDeskConfig( - _event: IpcMainInvokeEvent, - config: DeskConfig -): Promise< void > { - assertDeskConfig( config ); - await lockAppdata(); - try { - const userData = await loadUserData(); - await saveUserData( { - ...userData, - desks: { - ...userData.desks, - user: config, - }, - } ); - } finally { - await unlockAppdata(); - } -} - -export async function getSiteDeskConfig( - _event: IpcMainInvokeEvent, - siteId: string -): Promise< DeskConfig | undefined > { - assertSiteId( siteId ); - const userData = await loadUserData(); - return userData.desks?.sites?.[ siteId ]; -} - -export async function saveSiteDeskConfig( - _event: IpcMainInvokeEvent, - siteId: string, - config: DeskConfig -): Promise< void > { - assertSiteId( siteId ); - assertDeskConfig( config ); - await lockAppdata(); - try { - const userData = await loadUserData(); - await saveUserData( { - ...userData, - desks: { - ...userData.desks, - sites: { - ...userData.desks?.sites, - [ siteId ]: config, - }, - }, - } ); - } finally { - await unlockAppdata(); - } -} diff --git a/apps/studio/src/preload.ts b/apps/studio/src/preload.ts index b92f50046d..a475b9d794 100644 --- a/apps/studio/src/preload.ts +++ b/apps/studio/src/preload.ts @@ -215,16 +215,6 @@ const api: IpcApi = { ipcRendererInvoke( 'answerAiAgentQuestion', runId, answers ), setSessionEnvironment: ( sessionId, environment ) => ipcRendererInvoke( 'setSessionEnvironment', sessionId, environment ), - getDeskSettings: () => ipcRendererInvoke( 'getDeskSettings' ), - saveDeskSettings: ( settings ) => ipcRendererInvoke( 'saveDeskSettings', settings ), - exportDeskConfig: ( config, suggestedFilename ) => - ipcRendererInvoke( 'exportDeskConfig', config, suggestedFilename ), - importDeskConfig: () => ipcRendererInvoke( 'importDeskConfig' ), - getUserDeskConfig: () => ipcRendererInvoke( 'getUserDeskConfig' ), - saveUserDeskConfig: ( config ) => ipcRendererInvoke( 'saveUserDeskConfig', config ), - getSiteDeskConfig: ( siteId ) => ipcRendererInvoke( 'getSiteDeskConfig', siteId ), - saveSiteDeskConfig: ( siteId, config ) => - ipcRendererInvoke( 'saveSiteDeskConfig', siteId, config ), fetchSiteRestApi: ( siteId, request ) => ipcRendererInvoke( 'fetchSiteRestApi', siteId, request ), }; diff --git a/apps/studio/src/storage/storage-types.ts b/apps/studio/src/storage/storage-types.ts index 317e7408cc..0e516ebae8 100644 --- a/apps/studio/src/storage/storage-types.ts +++ b/apps/studio/src/storage/storage-types.ts @@ -1,6 +1,5 @@ import { StatsMetric } from 'src/lib/bump-stats'; import { SupportedEditor } from 'src/modules/user-settings/lib/editor'; -import type { DesksConfig } from '@studio/common/types/desk'; import type { SupportedTerminal } from 'src/modules/user-settings/lib/terminal'; export interface WindowBounds { @@ -48,7 +47,6 @@ export interface UserData { cliAutoInstalled?: boolean; cliUserUninstalled?: boolean; wapuuScore?: number; - desks?: DesksConfig; aiSessionPlacements?: Record< string, AiSessionSitePlacement >; lastNightlyUpdateCheck?: number; nightlyPromptResult?: NightlyPromptResult; diff --git a/apps/studio/src/tests/main-window.test.ts b/apps/studio/src/tests/main-window.test.ts index 04bd93899c..e22f66d363 100644 --- a/apps/studio/src/tests/main-window.test.ts +++ b/apps/studio/src/tests/main-window.test.ts @@ -86,6 +86,12 @@ const mockUserData = { }; vi.mocked( readFile ).mockResolvedValue( Buffer.from( JSON.stringify( mockUserData ) ) ); +beforeEach( () => { + delete process.env.ENABLE_AGENTIC_UI; + delete process.env.ELECTRON_UI_RENDERER_URL; + delete process.env.ELECTRON_RENDERER_URL; +} ); + describe( 'getMainWindow', () => { let createdWindow: BrowserWindow; @@ -136,6 +142,31 @@ describe( 'getMainWindow', () => { } ); } ); +describe( 'renderer selection', () => { + afterEach( () => { + __resetMainWindow(); + } ); + + it( 'loads the legacy renderer by default', async () => { + const createdWindow = await createMainWindow(); + + expect( createdWindow.loadFile ).toHaveBeenCalledWith( + expect.stringContaining( 'renderer/index.html' ) + ); + expect( createdWindow.loadURL ).not.toHaveBeenCalled(); + } ); + + it( 'loads the UI dev server when the agentic UI flag is enabled', async () => { + process.env.ENABLE_AGENTIC_UI = 'true'; + process.env.ELECTRON_UI_RENDERER_URL = 'http://localhost:5200'; + + const createdWindow = await createMainWindow(); + + expect( createdWindow.loadURL ).toHaveBeenCalledWith( 'http://localhost:5200' ); + expect( createdWindow.loadFile ).not.toHaveBeenCalled(); + } ); +} ); + describe( 'fullscreen events', () => { let createdWindow: BrowserWindow; diff --git a/apps/ui/package.json b/apps/ui/package.json index 02ca4db048..1cf504dc4b 100644 --- a/apps/ui/package.json +++ b/apps/ui/package.json @@ -36,8 +36,7 @@ "react": "^19.2.6", "react-dom": "^19.2.6", "react-markdown": "^10.1.0", - "remark-gfm": "^4.0.1", - "tldraw": "3.13.1" + "remark-gfm": "^4.0.1" }, "devDependencies": { "@types/react": "^19.2.15", diff --git a/apps/ui/src/app/index.tsx b/apps/ui/src/app/index.tsx index 22c4b541a7..0cd61bca8c 100644 --- a/apps/ui/src/app/index.tsx +++ b/apps/ui/src/app/index.tsx @@ -1,7 +1,5 @@ import { AppProviders } from '@/app/app-providers'; -import { useUiMode } from '@/app/use-ui-mode'; import { ClassicUiApp } from '@/ui-classic/app'; -import { DesksUiApp } from '@/ui-desks/app'; import '@wordpress/components/build-style/style.css'; import '@wordpress/dataviews/build-style/style.css'; import '@wordpress/theme/design-tokens.css'; @@ -13,11 +11,9 @@ interface AppProps { } export function App( { connector }: AppProps ) { - const { mode } = useUiMode(); - return ( - { mode === 'desks' ? : } + ); } diff --git a/apps/ui/src/app/use-ui-mode.ts b/apps/ui/src/app/use-ui-mode.ts deleted file mode 100644 index fd234760ca..0000000000 --- a/apps/ui/src/app/use-ui-mode.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { useCallback, useState } from 'react'; - -export type UiMode = 'classic' | 'desks'; - -const STUDIO_UI_MODE_PARAM = 'studio-ui-mode'; -const DEFAULT_UI_MODE: UiMode = 'desks'; - -function readLaunchUiMode(): UiMode | undefined { - if ( typeof window === 'undefined' ) { - return undefined; - } - - try { - const mode = new URLSearchParams( window.location.search ).get( STUDIO_UI_MODE_PARAM ); - if ( mode === 'desks' ) { - return 'desks'; - } - if ( mode === 'agentic' ) { - return 'classic'; - } - } catch { - return undefined; - } -} - -function readInitialUiMode(): UiMode { - return readLaunchUiMode() ?? DEFAULT_UI_MODE; -} - -function resetRoute() { - if ( typeof window === 'undefined' ) { - return; - } - - if ( window.location.protocol === 'file:' ) { - if ( window.location.hash !== '#/' ) { - window.history.replaceState( window.history.state, '', '#/' ); - } - return; - } - - if ( window.location.pathname !== '/' || window.location.search || window.location.hash ) { - window.history.replaceState( window.history.state, '', '/' ); - } -} - -export function useUiMode() { - const [ mode, setModeState ] = useState< UiMode >( readInitialUiMode ); - - const setMode = useCallback( - ( nextMode: UiMode ) => { - if ( mode === nextMode ) { - return; - } - - setModeState( nextMode ); - resetRoute(); - }, - [ mode ] - ); - - return { mode, setMode }; -} diff --git a/apps/ui/src/components/site-list/index.tsx b/apps/ui/src/components/site-list/index.tsx index 9a63ff6fd0..585a17c393 100644 --- a/apps/ui/src/components/site-list/index.tsx +++ b/apps/ui/src/components/site-list/index.tsx @@ -115,7 +115,7 @@ function SessionActionsMenu( { session }: { session: AiSessionSummary } ) { const starred = !! session.starred; const archived = !! session.archived; - // Same persistence path as the ui-desks chats panel: optimistic + // Same persistence path as the assistant tab: optimistic // starred/archived patches through `connector.updateSessionMetadata`. const updateMetadata = ( patch: { starred: boolean; archived: boolean } ) => { updateSessionMetadata.mutate( { diff --git a/apps/ui/src/components/site-list/style.module.css b/apps/ui/src/components/site-list/style.module.css index eceb57acd4..0ea46f1020 100644 --- a/apps/ui/src/components/site-list/style.module.css +++ b/apps/ui/src/components/site-list/style.module.css @@ -192,7 +192,7 @@ } .siteStatus:focus-visible { - outline: 2px solid var(--ui-desks-focus, #3858e9); + outline: 2px solid var(--wpds-color-stroke-interactive-brand, #3858e9); outline-offset: -2px; } @@ -473,7 +473,7 @@ } .sessionLink:focus-visible { - outline: 2px solid var(--ui-desks-focus, #3858e9); + outline: 2px solid var(--wpds-color-stroke-interactive-brand, #3858e9); outline-offset: -2px; } diff --git a/apps/ui/src/data/core/connectors/ipc/index.ts b/apps/ui/src/data/core/connectors/ipc/index.ts index f4431f4ee4..77a4a440b8 100644 --- a/apps/ui/src/data/core/connectors/ipc/index.ts +++ b/apps/ui/src/data/core/connectors/ipc/index.ts @@ -7,8 +7,6 @@ import type { AuthUser, ColorScheme, Connector, - DeskConfig, - DeskSettings, ExtractedBlueprintBundle, FeaturedBlueprint, InstalledApps, @@ -631,38 +629,6 @@ export function createIpcConnector(): Connector { return ( await ipcApi.getInstalledAppsAndTerminals() ) as InstalledApps; }, - async getDeskSettings(): Promise< DeskSettings > { - return ( await ipcApi.getDeskSettings() ) as DeskSettings; - }, - - async saveDeskSettings( settings ): Promise< void > { - await ipcApi.saveDeskSettings( settings ); - }, - - async exportDeskConfig( config, suggestedFilename ): Promise< string | null > { - return ( await ipcApi.exportDeskConfig( config, suggestedFilename ) ) as string | null; - }, - - async importDeskConfig(): Promise< DeskConfig | null > { - return ( await ipcApi.importDeskConfig() ) as DeskConfig | null; - }, - - async getUserDeskConfig(): Promise< DeskConfig | undefined > { - return ( await ipcApi.getUserDeskConfig() ) as DeskConfig | undefined; - }, - - async saveUserDeskConfig( config ): Promise< void > { - await ipcApi.saveUserDeskConfig( config ); - }, - - async getSiteDeskConfig( siteId ): Promise< DeskConfig | undefined > { - return ( await ipcApi.getSiteDeskConfig( siteId ) ) as DeskConfig | undefined; - }, - - async saveSiteDeskConfig( siteId, config ): Promise< void > { - await ipcApi.saveSiteDeskConfig( siteId, config ); - }, - async fetchSiteRest( siteId, request ) { return await ipcApi.fetchSiteRestApi( siteId, request ); }, diff --git a/apps/ui/src/data/core/index.ts b/apps/ui/src/data/core/index.ts index b0eff4e788..78c93072e5 100644 --- a/apps/ui/src/data/core/index.ts +++ b/apps/ui/src/data/core/index.ts @@ -7,9 +7,6 @@ export type { ColorScheme, Connector, CreateSiteParams, - DeskConfig, - DeskSettings, - DeskWidgetBase, ExtractedBlueprintBundle, FeaturedBlueprint, InstalledApps, diff --git a/apps/ui/src/data/core/types.ts b/apps/ui/src/data/core/types.ts index 5e171dbe3e..ba8372b9a1 100644 --- a/apps/ui/src/data/core/types.ts +++ b/apps/ui/src/data/core/types.ts @@ -6,7 +6,6 @@ import type { AiSessionSummary, LoadedAiSession } from '@studio/common/ai/sessio import type { SupportedLocale } from '@studio/common/lib/locale'; import type { SupportedEditor } from '@studio/common/lib/user-settings/editor'; import type { SupportedTerminal } from '@studio/common/lib/user-settings/terminal'; -import type { DeskConfig, DeskSettings } from '@studio/common/types/desk'; import type { SupportedPHPVersion } from '@studio/common/types/php-versions'; import type { Snapshot } from '@studio/common/types/snapshot'; import type { SyncSite } from '@studio/common/types/sync'; @@ -32,7 +31,6 @@ export type { export type { AiModelId } from '@studio/common/ai/models'; export type { Snapshot } from '@studio/common/types/snapshot'; export type { SyncSite } from '@studio/common/types/sync'; -export type { DeskConfig, DeskSettings, DeskWidgetBase } from '@studio/common/types/desk'; export type { SupportedEditor } from '@studio/common/lib/user-settings/editor'; export type { SupportedTerminal } from '@studio/common/lib/user-settings/terminal'; export type { SupportedLocale } from '@studio/common/lib/locale'; @@ -228,7 +226,7 @@ export interface Connector { ): Promise< AiSessionSummary >; // Create an empty session file so it appears immediately. When `siteId` - // is omitted, the session is a user-desk chat with no owner site. + // is omitted, the session is a user chat with no owner site. createSession( siteId?: string ): Promise< AiSessionSummary >; // Continue an existing session by sending a new prompt. Returns a `runId` @@ -273,16 +271,6 @@ export interface Connector { // installed. getInstalledApps(): Promise< InstalledApps >; - // Desks - getDeskSettings(): Promise< DeskSettings >; - saveDeskSettings( settings: DeskSettings ): Promise< void >; - exportDeskConfig( config: DeskConfig, suggestedFilename: string ): Promise< string | null >; - importDeskConfig(): Promise< DeskConfig | null >; - getUserDeskConfig(): Promise< DeskConfig | undefined >; - saveUserDeskConfig( config: DeskConfig ): Promise< void >; - getSiteDeskConfig( siteId: string ): Promise< DeskConfig | undefined >; - saveSiteDeskConfig( siteId: string, config: DeskConfig ): Promise< void >; - // Site WordPress REST API. The renderer uses this as the transport for // @wordpress/api-fetch / @wordpress/core-data so WordPress entity semantics // stay in the WordPress packages while Studio owns site resolution and auth. diff --git a/apps/ui/src/data/queries/use-desk-config.ts b/apps/ui/src/data/queries/use-desk-config.ts deleted file mode 100644 index 0ef87f02e2..0000000000 --- a/apps/ui/src/data/queries/use-desk-config.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { createDefaultDeskSettings, normalizeDeskSettings } from '@studio/common/lib/desk-settings'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { useCallback, useMemo } from 'react'; -import { useConnector } from '@/data/core'; -import type { DeskConfig, DeskSettings } from '@/data/core'; - -const deskSettingsQueryKey = [ 'desk-settings' ] as const; -const userDeskConfigQueryKey = [ 'desk-config', 'user' ] as const; -const siteDeskConfigQueryKey = ( siteId: string ) => [ 'desk-config', 'site', siteId ] as const; -const deskConfigQueryKey = ( siteId?: string ) => - siteId ? siteDeskConfigQueryKey( siteId ) : userDeskConfigQueryKey; - -export function useDeskConfig( siteId?: string, enabled = true ) { - const connector = useConnector(); - return useQuery( { - queryKey: deskConfigQueryKey( siteId ), - queryFn: () => - siteId ? connector.getSiteDeskConfig( siteId ) : connector.getUserDeskConfig(), - enabled, - } ); -} - -export function useSaveDeskConfig( siteId?: string ) { - const connector = useConnector(); - const queryClient = useQueryClient(); - return useMutation( { - mutationFn: ( config: DeskConfig ) => - ( siteId - ? connector.saveSiteDeskConfig( siteId, config ) - : connector.saveUserDeskConfig( config ) - ).then( () => config ), - onSuccess: ( config ) => { - queryClient.setQueryData( deskConfigQueryKey( siteId ), config ); - }, - } ); -} - -export function useDeskSettings() { - const connector = useConnector(); - return useQuery( { - queryKey: deskSettingsQueryKey, - queryFn: async () => normalizeDeskSettings( await connector.getDeskSettings() ), - placeholderData: () => createDefaultDeskSettings(), - } ); -} - -export function useSaveDeskSettings() { - const connector = useConnector(); - const queryClient = useQueryClient(); - return useMutation( { - mutationFn: ( settings: DeskSettings ) => - connector.saveDeskSettings( settings ).then( () => settings ), - onMutate: ( settings ) => { - queryClient.setQueryData( deskSettingsQueryKey, settings ); - }, - onSuccess: ( settings ) => { - queryClient.setQueryData( deskSettingsQueryKey, settings ); - }, - } ); -} - -export function useUpdateDeskSettings() { - const { data: savedDeskSettings } = useDeskSettings(); - const fallbackDeskSettings = useMemo( () => createDefaultDeskSettings(), [] ); - const deskSettings = savedDeskSettings ?? fallbackDeskSettings; - const saveDeskSettings = useSaveDeskSettings(); - - return useCallback( - ( patch: Partial< DeskSettings > ) => { - saveDeskSettings.mutate( - normalizeDeskSettings( { - ...deskSettings, - ...patch, - updatedAt: new Date().toISOString(), - } ) - ); - }, - [ deskSettings, saveDeskSettings ] - ); -} diff --git a/apps/ui/src/index.css b/apps/ui/src/index.css index 93bc75beee..116bbb159e 100644 --- a/apps/ui/src/index.css +++ b/apps/ui/src/index.css @@ -7,31 +7,8 @@ toggle z-index: 10). Menus stay below modal surfaces; tooltips live above dialogs so help text on controls inside a modal still surfaces above the containing overlay. */ - --ui-desks-z-popover: 100; - --ui-desks-z-dialog: 700; - --ui-desks-z-tooltip: 800; - --wp-ui-dialog-z-index: var( --ui-desks-z-dialog ); - --wp-ui-tooltip-z-index: var( --ui-desks-z-tooltip ); - - --ui-desks-bg: rgb( 232, 234, 235 ); - --ui-desks-material: #fff; - --ui-desks-glass: rgba( 255, 255, 255, 0.85 ); - --ui-desks-glass-border: rgba( 255, 255, 255, 0.7 ); - --ui-desks-text: #14171a; - --ui-desks-muted: #6b7280; - --ui-desks-divider: rgba( 15, 23, 42, 0.06 ); - --ui-desks-control-hover: rgba( 15, 23, 42, 0.07 ); - --ui-desks-focus: #3858e9; - --ui-desks-radius-control: 16px; - --ui-desks-radius-surface: 18px; - --ui-desks-radius-panel: 22px; - --ui-desks-radius-button: 12px; - --ui-desks-corner-shape: superellipse( 1.42 ); - --ui-desks-shadow-control: 0 1px 2px rgba( 15, 23, 42, 0.04 ), 0 8px 24px rgba( 15, 23, 42, 0.06 ); - --ui-desks-shadow-control-raised: 0 1px 2px rgba( 15, 23, 42, 0.04 ), - 0 18px 44px rgba( 15, 23, 42, 0.1 ); - --ui-desks-shadow-surface-raised: 0 1px 2px rgba( 15, 23, 42, 0.04 ), - 0 18px 44px rgba( 15, 23, 42, 0.1 ); + --wp-ui-dialog-z-index: 700; + --wp-ui-tooltip-z-index: 800; } body { @@ -137,19 +114,6 @@ a:focus:not( :focus-visible ), height: 16px; } -/* Desks uses each shape's indicator as the visible selection outline. - Hide tldraw's default selection box and handle glyphs while keeping the - underlying resize/rotate wrappers interactive. */ -[data-ui-mode='desks'] .tl-selection__fg__outline { - stroke: transparent; -} - -[data-ui-mode='desks'] .tl-corner-handle, -[data-ui-mode='desks'] .tl-mobile-rotate__fg, -[data-ui-mode='desks'] .tl-text-handle { - opacity: 0; -} - /* @wordpress/components' ComboboxControl (used by DataForm's adaptive picker once a field has 10+ elements) leaves its wrapper without a background, so the body surface bleeds through around the inner input diff --git a/apps/ui/src/ui-desks/app.tsx b/apps/ui/src/ui-desks/app.tsx deleted file mode 100644 index f8403f2c0a..0000000000 --- a/apps/ui/src/ui-desks/app.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { RouterProvider, createRouter } from '@tanstack/react-router'; -import { useMemo } from 'react'; -import { createPackagedRouterHistory } from '@/app/router-history'; -import { - desksOnboardingBlueprintRoute, - desksOnboardingCreateRoute, - desksOnboardingHomeRoute, - desksOnboardingImportRoute, -} from './onboarding'; -import { desksRootRoute } from './router/root'; -import { siteDeskRoute } from './site-desk'; -import { desksSiteSettingsRoute } from './site-settings'; -import { userDeskRoute } from './user-desk'; - -const routeTree = desksRootRoute.addChildren( [ - userDeskRoute, - desksSiteSettingsRoute, - siteDeskRoute, - desksOnboardingHomeRoute, - desksOnboardingCreateRoute, - desksOnboardingBlueprintRoute, - desksOnboardingImportRoute, -] ); - -export function createDesksRouter() { - return createRouter( { - routeTree, - defaultPreload: 'intent', - history: createPackagedRouterHistory(), - } ); -} - -export function DesksUiApp() { - const router = useMemo( () => createDesksRouter(), [] ); - - return ( -
- -
- ); -} diff --git a/apps/ui/src/ui-desks/chats/chat-button/index.tsx b/apps/ui/src/ui-desks/chats/chat-button/index.tsx deleted file mode 100644 index 5ea83be888..0000000000 --- a/apps/ui/src/ui-desks/chats/chat-button/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { __ } from '@wordpress/i18n'; -import { comment } from '@wordpress/icons'; -import { Button } from '@/ui-desks/components'; - -interface ChatButtonProps { - onClick: () => void; -} - -export function ChatButton( { onClick }: ChatButtonProps ) { - return ( - - ); - } - - return ( - - - - ); -} - -export { pickLiveSite }; diff --git a/apps/ui/src/ui-desks/chats/composer/environment-pill/style.module.css b/apps/ui/src/ui-desks/chats/composer/environment-pill/style.module.css deleted file mode 100644 index 63da0e300e..0000000000 --- a/apps/ui/src/ui-desks/chats/composer/environment-pill/style.module.css +++ /dev/null @@ -1,15 +0,0 @@ -.environmentDot { - width: 6px; - height: 6px; - border-radius: 50%; - background-color: #16a34a; - flex: 0 0 auto; -} - -.environmentDotLive { - background-color: var(--ui-desks-focus, #3858e9); -} - -.environmentMenu { - min-width: 140px; -} diff --git a/apps/ui/src/ui-desks/chats/composer/family-switch-confirm-dialog/index.tsx b/apps/ui/src/ui-desks/chats/composer/family-switch-confirm-dialog/index.tsx deleted file mode 100644 index 5a1e77bb56..0000000000 --- a/apps/ui/src/ui-desks/chats/composer/family-switch-confirm-dialog/index.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { getAiModelLabel } from '@studio/common/ai/models'; -import { __, sprintf } from '@wordpress/i18n'; -import { Button, Dialog, DialogRow } from '@/ui-desks/components'; -import styles from './style.module.css'; -import type { AiModelId } from '@/data/core'; - -export function FamilySwitchConfirmDialog( { - currentModel, - pendingModel, - inFlight, - onCancel, - onConfirm, -}: { - currentModel: AiModelId; - pendingModel: AiModelId | null; - inFlight: boolean; - onCancel: () => void; - onConfirm: () => void; -} ) { - if ( pendingModel === null ) { - return null; - } - - const description = sprintf( - /* translators: 1: current model name, 2: new model name */ - __( - 'Switching from %1$s to %2$s starts a fresh conversation - the two model families do not share memory. Your current chat stays in the sidebar.' - ), - getAiModelLabel( currentModel ), - getAiModelLabel( pendingModel ) - ); - - return ( - { - if ( ! inFlight ) { - onCancel(); - } - } } - onSubmit={ ( event ) => { - event.preventDefault(); - if ( ! inFlight ) { - onConfirm(); - } - } } - size="narrow" - > -
-

{ __( 'Start a new conversation?' ) }

-

{ description }

-
- - - - -
- ); -} diff --git a/apps/ui/src/ui-desks/chats/composer/family-switch-confirm-dialog/style.module.css b/apps/ui/src/ui-desks/chats/composer/family-switch-confirm-dialog/style.module.css deleted file mode 100644 index f3bbc2d997..0000000000 --- a/apps/ui/src/ui-desks/chats/composer/family-switch-confirm-dialog/style.module.css +++ /dev/null @@ -1,30 +0,0 @@ -.dialog { - gap: 16px; -} - -.header { - display: flex; - flex-direction: column; - gap: 6px; -} - -.title { - margin: 0; - color: var(--ui-desks-text, #14171a); - font-size: 15px; - font-weight: 600; - line-height: 20px; - letter-spacing: 0; -} - -.description { - margin: 0; - color: var(--ui-desks-muted, #6b7280); - font-size: 13px; - line-height: 18px; - letter-spacing: 0; -} - -.actions { - justify-content: flex-end; -} diff --git a/apps/ui/src/ui-desks/chats/composer/index.test.tsx b/apps/ui/src/ui-desks/chats/composer/index.test.tsx deleted file mode 100644 index 25bbfbbc4a..0000000000 --- a/apps/ui/src/ui-desks/chats/composer/index.test.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import '@testing-library/jest-dom/vitest'; -import { fireEvent, render, screen } from '@testing-library/react'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { Composer } from './index'; -import type { ReactNode } from 'react'; - -const composerMocks = vi.hoisted( () => ( { - onSend: vi.fn(), - onInterrupt: vi.fn(), - onLogin: vi.fn(), - setQueryData: vi.fn(), - invalidateQueries: vi.fn(), - setSessionModel: vi.fn(), - createSession: vi.fn(), - consumeComposerWidgetAttachmentRequest: vi.fn(), -} ) ); - -vi.mock( '@wordpress/i18n', () => ( { - __: ( text: string ) => text, - sprintf: ( text: string, ...values: string[] ) => - values.reduce( - ( result, value, index ) => - result.replace( `%${ index + 1 }$s`, value ).replace( '%s', value ), - text - ), -} ) ); - -vi.mock( '@wordpress/icons', () => ( { - arrowUp: {}, - chevronDownSmall: {}, - closeSmall: {}, - code: {}, -} ) ); - -vi.mock( '@wordpress/ui', () => ( { - Icon: () => null, -} ) ); - -vi.mock( '@tanstack/react-query', () => ( { - useQueryClient: () => ( { - setQueryData: composerMocks.setQueryData, - invalidateQueries: composerMocks.invalidateQueries, - } ), -} ) ); - -vi.mock( '@/data/core', () => ( { - useConnector: () => ( { - setSessionModel: composerMocks.setSessionModel, - createSession: composerMocks.createSession, - } ), -} ) ); - -vi.mock( '@/ui-desks/chats/context', () => ( { - useChats: () => ( { - composerWidgetAttachmentRequest: undefined, - consumeComposerWidgetAttachmentRequest: composerMocks.consumeComposerWidgetAttachmentRequest, - isComposerWidgetDragTarget: false, - } ), -} ) ); - -vi.mock( '@/ui-desks/chats/widget-context', () => ( { - buildWidgetContextDisplayMessage: ( prompt: string ) => prompt, - buildWidgetContextPrompt: ( prompt: string ) => prompt, - getWidgetDisplayLabel: () => 'Widget', - MAX_VISIBLE_CHAT_WIDGETS: 3, - WidgetContextMoreThumbnail: () => null, - WidgetContextThumbnail: () => null, -} ) ); - -vi.mock( '@/ui-desks/components', async () => { - const React = await vi.importActual< typeof import('react') >( 'react' ); - const passthrough = ( { children }: { children?: ReactNode } ) => - React.createElement( React.Fragment, null, children ); - - return { - Button: ( { - children, - disabled, - label, - onClick, - type = 'button', - ...props - }: { - children?: ReactNode; - disabled?: boolean; - label: string; - onClick?: () => void; - type?: 'button' | 'submit'; - } ) => - React.createElement( - 'button', - { - ...props, - 'aria-label': label, - disabled, - onClick, - type, - }, - children ?? label - ), - Menu: { - Root: passthrough, - Trigger: ( { render }: { render: ReactNode } ) => render, - Popup: passthrough, - Item: ( { children, onClick }: { children?: ReactNode; onClick?: () => void } ) => - React.createElement( 'button', { type: 'button', onClick }, children ), - RadioGroup: passthrough, - RadioItem: ( { children }: { children?: ReactNode } ) => - React.createElement( 'div', null, children ), - }, - }; -} ); - -describe( 'desks chat Composer', () => { - beforeEach( () => { - composerMocks.onSend.mockReset().mockResolvedValue( undefined ); - composerMocks.onInterrupt.mockReset().mockResolvedValue( undefined ); - composerMocks.onLogin.mockReset(); - composerMocks.setQueryData.mockReset(); - composerMocks.invalidateQueries.mockReset(); - composerMocks.setSessionModel.mockReset().mockResolvedValue( undefined ); - composerMocks.createSession.mockReset().mockResolvedValue( { id: 'new-session' } ); - composerMocks.consumeComposerWidgetAttachmentRequest.mockReset(); - } ); - - it( 'keeps the idle send button visible even when the prompt is empty', () => { - renderComposer( { busy: false } ); - - expect( screen.getByRole( 'button', { name: 'Send' } ) ).toBeDisabled(); - } ); - - it( 'hides the queue button while busy until the user types a prompt', () => { - renderComposer( { busy: true } ); - - expect( screen.getByRole( 'button', { name: 'Stop' } ) ).toBeVisible(); - expect( screen.queryByRole( 'button', { name: 'Queue' } ) ).not.toBeInTheDocument(); - - fireEvent.change( screen.getByPlaceholderText( 'Ask Studio Desk…' ), { - target: { value: 'Follow up on this' }, - } ); - - expect( screen.getByRole( 'button', { name: 'Queue' } ) ).toBeEnabled(); - } ); - - it( 'shows a login requirement and blocks the composer when unauthenticated', () => { - renderComposer( { busy: false, authRequired: true } ); - - expect( - screen.getByText( 'Log in with WordPress.com to use Studio Desk chat.' ) - ).toBeVisible(); - expect( screen.getByPlaceholderText( 'Log in to use Studio Desk chat.' ) ).toBeDisabled(); - - fireEvent.click( screen.getByRole( 'button', { name: 'Log in with WordPress.com' } ) ); - - expect( composerMocks.onLogin ).toHaveBeenCalledTimes( 1 ); - expect( composerMocks.onSend ).not.toHaveBeenCalled(); - } ); -} ); - -function renderComposer( { - busy, - authRequired = false, -}: { - busy: boolean; - authRequired?: boolean; -} ) { - return render( - - ); -} diff --git a/apps/ui/src/ui-desks/chats/composer/index.tsx b/apps/ui/src/ui-desks/chats/composer/index.tsx deleted file mode 100644 index d3640db48f..0000000000 --- a/apps/ui/src/ui-desks/chats/composer/index.tsx +++ /dev/null @@ -1,499 +0,0 @@ -import { AI_MODELS, getAiModelFamily, getAiModelLabel } from '@studio/common/ai/models'; -import { isStudioCustomEntryOfType } from '@studio/common/ai/sessions/entry-types'; -import { AI_SKILL_COMMANDS } from '@studio/common/ai/slash-commands'; -import { useQueryClient } from '@tanstack/react-query'; -import { __ } from '@wordpress/i18n'; -import { arrowUp, chevronDownSmall, closeSmall, code } from '@wordpress/icons'; -import { Icon } from '@wordpress/ui'; -import { clsx } from 'clsx'; -import { useCallback, useEffect, useRef, useState } from 'react'; -import { useConnector } from '@/data/core'; -import { SESSIONS_QUERY_KEY } from '@/data/queries/use-sessions'; -import { useChats } from '@/ui-desks/chats/context'; -import { - buildWidgetContextDisplayMessage, - buildWidgetContextPrompt, - getWidgetDisplayLabel, - MAX_VISIBLE_CHAT_WIDGETS, - WidgetContextMoreThumbnail, - WidgetContextThumbnail, -} from '@/ui-desks/chats/widget-context'; -import { Button, Menu } from '@/ui-desks/components'; -import { EnvironmentPill } from './environment-pill'; -import { FamilySwitchConfirmDialog } from './family-switch-confirm-dialog'; -import styles from './style.module.css'; -import type { AiModelId, LoadedAiSession, SessionEntry, SyncSite } from '@/data/core'; -import type { DeskWidget } from '@/ui-desks/widgets/types'; - -export function ComposerSkeleton() { - return ( -