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 (
-
- );
-}
diff --git a/apps/ui/src/ui-desks/chats/composer/environment-pill/index.tsx b/apps/ui/src/ui-desks/chats/composer/environment-pill/index.tsx
deleted file mode 100644
index 8634ae20f6..0000000000
--- a/apps/ui/src/ui-desks/chats/composer/environment-pill/index.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-import { __ } from '@wordpress/i18n';
-import { chevronDownSmall } from '@wordpress/icons';
-import { Icon } from '@wordpress/ui';
-import { useSetSessionEnvironment } from '@/data/queries/use-sessions';
-import { Button, Menu } from '@/ui-desks/components';
-import styles from './style.module.css';
-import type { SyncSite } from '@/data/core';
-
-interface EnvironmentPillProps {
- sessionId: string;
- effectiveEnvironment: 'local' | 'live';
- liveSite: SyncSite | undefined;
- disabled?: boolean;
-}
-
-function pickLiveSite( connectedSites: SyncSite[] | undefined ): SyncSite | undefined {
- if ( ! connectedSites || connectedSites.length === 0 ) {
- return undefined;
- }
- return connectedSites.find( ( site ) => ! site.isStaging ) ?? connectedSites[ 0 ];
-}
-
-export function EnvironmentPill( {
- sessionId,
- effectiveEnvironment,
- liveSite,
- disabled = false,
-}: EnvironmentPillProps ) {
- const setEnvironment = useSetSessionEnvironment( sessionId, liveSite?.id );
- const isLive = effectiveEnvironment === 'live';
- const label = isLive ? __( 'Live' ) : __( 'Local' );
- const canGoLive = !! liveSite;
-
- const onValueChange = ( value: string ) => {
- if ( value !== 'local' && value !== 'live' ) {
- return;
- }
- if ( value === effectiveEnvironment ) {
- return;
- }
- if ( value === 'live' && ! canGoLive ) {
- return;
- }
- setEnvironment.mutate( value );
- };
-
- if ( disabled ) {
- return (
-
- );
- }
-
- return (
-
-
-
- { label }
-
-
- }
- />
-
-
- { __( 'Local' ) }
-
- { __( 'Live' ) }
-
-
-
-
- );
-}
-
-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 (
-
- );
-}
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 (
-
- );
-}
-
-interface ComposerProps {
- busy: boolean;
- isInterrupting?: boolean;
- error: string | null;
- authRequired?: boolean;
- authLoading?: boolean;
- authPending?: boolean;
- onLogin?: () => void;
- model: AiModelId;
- onSend: ( prompt: string, options?: { displayMessage?: string } ) => Promise< void >;
- onInterrupt: () => Promise< void >;
- sessionId?: string;
- effectiveEnvironment?: 'local' | 'live';
- liveSite?: SyncSite;
- entries?: SessionEntry[];
- ownerSiteId?: string;
- onSwitchSession?: ( sessionId: string ) => void;
- autoFocus?: boolean;
- previewPrompt?: string | null;
- draftPrompt?: {
- id: number;
- prompt: string;
- } | null;
-}
-
-export function Composer( {
- busy,
- isInterrupting = false,
- error,
- authRequired = false,
- authLoading = false,
- authPending = false,
- onLogin,
- model,
- onSend,
- onInterrupt,
- sessionId,
- effectiveEnvironment = 'local',
- liveSite,
- entries,
- ownerSiteId,
- onSwitchSession,
- autoFocus = false,
- previewPrompt,
- draftPrompt,
-}: ComposerProps ) {
- const [ value, setValue ] = useState( '' );
- const [ contextWidgets, setContextWidgets ] = useState< DeskWidget[] >( [] );
- const textareaRef = useRef< HTMLTextAreaElement | null >( null );
- const appliedDraftPromptIdRef = useRef< number | null >( null );
- const connector = useConnector();
- const queryClient = useQueryClient();
- const {
- composerWidgetAttachmentRequest,
- consumeComposerWidgetAttachmentRequest,
- isComposerWidgetDragTarget,
- } = useChats();
- const [ pendingFamilyChange, setPendingFamilyChange ] = useState< AiModelId | null >( null );
- const [ familySwitchInFlight, setFamilySwitchInFlight ] = useState( false );
-
- useEffect( () => {
- if ( autoFocus ) {
- textareaRef.current?.focus();
- }
- }, [ autoFocus, sessionId ] );
-
- useEffect( () => {
- if ( ! draftPrompt || appliedDraftPromptIdRef.current === draftPrompt.id ) {
- return;
- }
- appliedDraftPromptIdRef.current = draftPrompt.id;
- setValue( draftPrompt.prompt );
- queueMicrotask( () => {
- const node = textareaRef.current;
- if ( ! node ) {
- return;
- }
- node.focus();
- const length = node.value.length;
- node.setSelectionRange( length, length );
- } );
- }, [ draftPrompt ] );
-
- const send = useCallback( async () => {
- if ( authRequired || authLoading ) {
- return;
- }
-
- const trimmed = value.trim();
- if ( ! trimmed ) {
- return;
- }
- const widgetsToSend = contextWidgets;
- setValue( '' );
- setContextWidgets( [] );
- try {
- if ( widgetsToSend.length > 0 ) {
- await onSend( buildWidgetContextPrompt( trimmed, widgetsToSend ), {
- displayMessage: buildWidgetContextDisplayMessage( trimmed, widgetsToSend ),
- } );
- return;
- }
-
- await onSend( trimmed );
- } catch {
- setValue( trimmed );
- setContextWidgets( widgetsToSend );
- }
- }, [ authLoading, authRequired, contextWidgets, onSend, value ] );
-
- useEffect( () => {
- if (
- ! composerWidgetAttachmentRequest ||
- composerWidgetAttachmentRequest.sessionId !== sessionId
- ) {
- return;
- }
-
- setContextWidgets( ( currentWidgets ) =>
- mergeWidgetAttachments( currentWidgets, composerWidgetAttachmentRequest.widgets )
- );
- consumeComposerWidgetAttachmentRequest( composerWidgetAttachmentRequest.id );
- textareaRef.current?.focus();
- }, [ composerWidgetAttachmentRequest, consumeComposerWidgetAttachmentRequest, sessionId ] );
-
- const removeContextWidget = useCallback( ( widgetId: string ) => {
- setContextWidgets( ( currentWidgets ) =>
- currentWidgets.filter( ( widget ) => widget.id !== widgetId )
- );
- }, [] );
-
- const applySameFamilyModel = useCallback(
- ( picked: AiModelId ) => {
- if ( ! sessionId ) {
- return;
- }
- const timestamp = new Date().toISOString();
- queryClient.setQueryData< LoadedAiSession >(
- [ ...SESSIONS_QUERY_KEY, sessionId ],
- ( prev ) =>
- prev
- ? {
- ...prev,
- entries: [
- ...( prev.entries ?? [] ),
- {
- type: 'model_change',
- id: Math.random().toString( 36 ).slice( 2, 10 ),
- parentId: null,
- timestamp,
- provider: '',
- modelId: picked,
- } as unknown as SessionEntry,
- ],
- }
- : prev
- );
- void connector.setSessionModel( sessionId, picked ).catch( () => {
- void queryClient.invalidateQueries( {
- queryKey: [ ...SESSIONS_QUERY_KEY, sessionId ],
- } );
- } );
- },
- [ connector, queryClient, sessionId ]
- );
-
- const handleModelChange = useCallback(
- ( picked: AiModelId ) => {
- if ( authRequired || authLoading ) {
- return;
- }
- if ( picked === model ) {
- return;
- }
- const hasTurns = ( entries ?? [] ).some( ( entry ) =>
- isStudioCustomEntryOfType( entry, 'studio.user_prompt' )
- );
- if (
- getAiModelFamily( model ) !== getAiModelFamily( picked ) &&
- onSwitchSession &&
- hasTurns
- ) {
- setPendingFamilyChange( picked );
- return;
- }
- applySameFamilyModel( picked );
- },
- [ applySameFamilyModel, authLoading, authRequired, entries, model, onSwitchSession ]
- );
-
- const cancelFamilyChange = useCallback( () => {
- if ( familySwitchInFlight ) {
- return;
- }
- setPendingFamilyChange( null );
- }, [ familySwitchInFlight ] );
-
- const confirmFamilyChange = useCallback( async () => {
- if ( ! pendingFamilyChange || ! onSwitchSession ) {
- return;
- }
- setFamilySwitchInFlight( true );
- try {
- const newSession = await connector.createSession( ownerSiteId );
- await connector
- .setSessionModel( newSession.id, pendingFamilyChange )
- .catch( () => undefined );
- await queryClient.invalidateQueries( { queryKey: SESSIONS_QUERY_KEY } );
- setPendingFamilyChange( null );
- onSwitchSession( newSession.id );
- } finally {
- setFamilySwitchInFlight( false );
- }
- }, [ connector, onSwitchSession, ownerSiteId, pendingFamilyChange, queryClient ] );
-
- const authBlocked = authRequired || authLoading;
- const canSend = ! authBlocked && value.trim().length > 0;
- const showSendButton = ! busy || canSend;
- const sendAriaLabel = busy ? __( 'Queue' ) : __( 'Send' );
- const visibleContextWidgets = contextWidgets.slice( 0, MAX_VISIBLE_CHAT_WIDGETS );
- const hiddenContextWidgetCount = contextWidgets.length - visibleContextWidgets.length;
-
- return (
- <>
- 0 ? 'true' : 'false' }
- >
- { authRequired ? (
-
- { __( 'Log in with WordPress.com to use Studio Desk chat.' ) }
-
-
- ) : null }
- { contextWidgets.length > 0 && (
-
- { visibleContextWidgets.map( ( widget ) => (
-
-
-
- ) ) }
- { hiddenContextWidgetCount > 0 && (
-
- ) }
-
- ) }
-
- { error ?
{ error }
: null }
-
- void confirmFamilyChange() }
- />
- >
- );
-}
-
-function mergeWidgetAttachments( currentWidgets: DeskWidget[], incomingWidgets: DeskWidget[] ) {
- const widgetsById = new Map( currentWidgets.map( ( widget ) => [ widget.id, widget ] ) );
- for ( const widget of incomingWidgets ) {
- widgetsById.set( widget.id, widget );
- }
- return Array.from( widgetsById.values() );
-}
-
-function getRemoveWidgetAttachmentLabel( widget: DeskWidget ) {
- return `${ __( 'Remove' ) } ${ getWidgetDisplayLabel( widget ) }`;
-}
diff --git a/apps/ui/src/ui-desks/chats/composer/style.module.css b/apps/ui/src/ui-desks/chats/composer/style.module.css
deleted file mode 100644
index 6a6ff86a57..0000000000
--- a/apps/ui/src/ui-desks/chats/composer/style.module.css
+++ /dev/null
@@ -1,263 +0,0 @@
-.root {
- margin-top: 8px;
- display: flex;
- flex-direction: column;
- gap: 8px;
-}
-
-.prompt {
- background: rgba(255, 255, 255, 0.85);
- border: 1px solid rgba(255, 255, 255, 0.7);
- border-radius: 18px;
- corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- -webkit-corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- padding: 12px 12px 8px 14px;
- display: flex;
- flex-direction: column;
- gap: 8px;
- box-shadow: 0 4px 16px rgba(15, 23, 42, 0.06);
-}
-
-.prompt:focus-within {
- box-shadow: 0 6px 18px rgba(15, 23, 42, 0.08);
-}
-
-.prompt[data-widget-drag-over='true'] {
- border-color: rgba(56, 88, 233, 0.72);
- box-shadow:
- 0 0 0 2px rgba(56, 88, 233, 0.16),
- 0 8px 22px rgba(56, 88, 233, 0.16);
-}
-
-.attachments {
- display: flex;
- flex-wrap: wrap;
- align-items: flex-start;
- gap: 8px;
- padding: 0 6px;
-}
-
-.authNotice {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 10px;
- padding: 10px 12px;
- border: 1px solid var(--ui-desks-divider, rgba(15, 23, 42, 0.06));
- border-radius: 12px;
- background: rgba(255, 255, 255, 0.72);
- color: var(--ui-desks-muted, #6b7280);
- font-size: 12px;
- line-height: 16px;
-}
-
-.authNotice span {
- min-width: 0;
-}
-
-.attachment {
- position: relative;
- flex: 0 0 auto;
-}
-
-.removeAttachment {
- position: absolute;
- top: -8px;
- right: -8px;
- opacity: 0;
- transform: scale(0.92);
- transition:
- opacity 120ms ease,
- transform 120ms ease;
-}
-
-.attachment:hover .removeAttachment,
-.attachment:focus-within .removeAttachment {
- opacity: 1;
- transform: scale(1);
-}
-
-.input {
- flex: 1 1 auto;
- min-width: 0;
- min-height: 30px;
- max-height: 220px;
- border: 0;
- background: transparent;
- padding: 4px 2px 0;
- margin-bottom: 6px;
- font-family: inherit;
- font-size: 14px;
- line-height: 1.45;
- color: var(--ui-desks-text, #14171a);
- outline: none;
- resize: none;
- overflow-y: auto;
- field-sizing: content;
-}
-
-.input::placeholder {
- color: var(--ui-desks-muted, #6b7280);
-}
-
-.input:disabled {
- cursor: not-allowed;
-}
-
-.input[data-preview='true'] {
- color: var(--ui-desks-muted, #6b7280);
-}
-
-.promptBar {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.promptTools {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 6px;
- min-width: 0;
- padding: 0;
-}
-
-.leftTools,
-.rightTools {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- min-width: 0;
-}
-
-.leftTools {
- flex: 0 0 auto;
-}
-
-.rightTools {
- flex: 1 1 auto;
- justify-content: flex-end;
-}
-
-.promptActions {
- display: flex;
- align-items: center;
- gap: 6px;
- flex: 0 0 auto;
-}
-
-.skeletonTool {
- display: inline-flex;
- height: 32px;
- border-radius: var(--ui-desks-radius-button, 12px);
-}
-
-.skeletonTextTool {
- width: 96px;
-}
-
-.skeletonAction {
- width: 32px;
-}
-
-.iconTool {
- width: 32px;
- padding: 0;
- justify-content: center;
- background: rgba(15, 23, 42, 0.06);
-}
-
-.iconTool:hover:not(:disabled),
-.iconTool:focus-visible:not(:disabled) {
- background: rgba(15, 23, 42, 0.1);
-}
-
-.modelTool {
- max-width: 160px;
-}
-
-.stopButton {
- width: 32px;
- height: 32px;
- border: 0;
- border-radius: var(--ui-desks-radius-button, 12px);
- corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- -webkit-corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- flex: 0 0 auto;
- padding: 0;
- transition: box-shadow 160ms ease, transform 160ms ease, opacity 140ms ease;
-}
-
-.stopButton {
- box-shadow: 0 4px 14px rgba(15, 23, 42, 0.18);
-}
-
-.stopButton:hover,
-.stopButton:focus-visible {
- box-shadow: 0 6px 18px rgba(15, 23, 42, 0.24);
- transform: translateY(-1px);
-}
-
-.stopButton[aria-busy='true'] {
- animation: stopPulse 1s ease-in-out infinite;
-}
-
-@keyframes stopPulse {
- 0%,
- 100% {
- opacity: 1;
- }
- 50% {
- opacity: 0.5;
- }
-}
-
-.stopGlyph {
- width: 10px;
- height: 10px;
- border-radius: 2px;
- background-color: currentColor;
- pointer-events: none;
-}
-
-.commandsMenuPopup {
- min-width: 280px;
-}
-
-.commandMenuItem > .commandItem {
- overflow: visible;
- text-overflow: initial;
- white-space: normal;
-}
-
-.commandItem {
- display: flex;
- flex-direction: column;
- gap: 2px;
- min-width: 0;
-}
-
-.commandName {
- font-family: var(--wpds-typography-font-family-mono, ui-monospace, monospace);
- font-size: 12px;
- color: var(--ui-desks-text, #14171a);
-}
-
-.commandDescription {
- font-size: 11px;
- color: var(--ui-desks-muted, #6b7280);
- white-space: normal;
-}
-
-.error {
- padding: 0 6px;
- color: var(--wpds-color-fg-content-error, #dc2626);
- font-size: 12px;
- line-height: 16px;
-}
diff --git a/apps/ui/src/ui-desks/chats/context.tsx b/apps/ui/src/ui-desks/chats/context.tsx
deleted file mode 100644
index 12840da922..0000000000
--- a/apps/ui/src/ui-desks/chats/context.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import { createContext, useContext } from 'react';
-import type { DeskWidget } from '@/ui-desks/widgets/types';
-import type { ReactNode } from 'react';
-
-export interface ChatPromptRequest {
- prompt: string;
- displayMessage?: string;
-}
-
-export interface PendingChatPrompt extends Required< ChatPromptRequest > {
- id: string;
- sessionId: string;
-}
-
-export interface ComposerWidgetAttachmentRequest {
- id: string;
- sessionId: string;
- widgets: DeskWidget[];
-}
-
-export interface ComposerWidgetDragPreview {
- widgets: DeskWidget[];
- x: number;
- y: number;
-}
-
-export interface ChatsContextValue {
- open: boolean;
- setOpen: ( open: boolean ) => void;
- selectedSessionId?: string;
- expanded: boolean;
- autoFocusSessionId?: string;
- isCreatingChat: boolean;
- pendingPrompt?: PendingChatPrompt;
- authRequiredPrompt?: ChatPromptRequest;
- composerWidgetAttachmentRequest?: ComposerWidgetAttachmentRequest;
- composerWidgetDragPreview?: ComposerWidgetDragPreview;
- isComposerWidgetDragTarget: boolean;
- selectSession: ( sessionId: string ) => void;
- switchSession: ( sessionId: string ) => void;
- clearSelection: () => void;
- startNewChat: () => Promise< void >;
- startChatWithPrompt: ( request: ChatPromptRequest ) => Promise< string >;
- consumePendingPrompt: ( promptId: string ) => void;
- attachWidgetsToComposer: ( widgets: DeskWidget[] ) => void;
- consumeComposerWidgetAttachmentRequest: ( requestId: string ) => void;
- setComposerWidgetDragPreview: ( preview: ComposerWidgetDragPreview | undefined ) => void;
- setComposerWidgetDragTarget: ( isTarget: boolean ) => void;
-}
-
-export interface ChatsProviderProps {
- siteId?: string;
- children: ReactNode;
-}
-
-const defaultChatsContext: ChatsContextValue = {
- open: false,
- setOpen: noopSetOpen,
- selectedSessionId: undefined,
- expanded: false,
- autoFocusSessionId: undefined,
- isCreatingChat: false,
- pendingPrompt: undefined,
- authRequiredPrompt: undefined,
- composerWidgetAttachmentRequest: undefined,
- composerWidgetDragPreview: undefined,
- isComposerWidgetDragTarget: false,
- selectSession: noopSelectSession,
- switchSession: noopSelectSession,
- clearSelection: noopClearSelection,
- startNewChat: noopStartChat,
- startChatWithPrompt: noopStartChatWithPrompt,
- consumePendingPrompt: noopConsumePendingPrompt,
- attachWidgetsToComposer: noopAttachWidgetsToComposer,
- consumeComposerWidgetAttachmentRequest: noopConsumePendingPrompt,
- setComposerWidgetDragPreview: noopSetComposerWidgetDragPreview,
- setComposerWidgetDragTarget: noopSetComposerWidgetDragTarget,
-};
-
-export const ChatsContext = createContext< ChatsContextValue >( defaultChatsContext );
-
-export function useChats() {
- return useContext( ChatsContext );
-}
-
-function noopSetOpen() {}
-function noopSelectSession() {}
-function noopClearSelection() {}
-async function noopStartChat() {}
-async function noopStartChatWithPrompt() {
- return '';
-}
-function noopConsumePendingPrompt() {}
-function noopAttachWidgetsToComposer() {}
-function noopSetComposerWidgetDragPreview() {}
-function noopSetComposerWidgetDragTarget() {}
diff --git a/apps/ui/src/ui-desks/chats/conversation/index.test.ts b/apps/ui/src/ui-desks/chats/conversation/index.test.ts
deleted file mode 100644
index 0aaa700e37..0000000000
--- a/apps/ui/src/ui-desks/chats/conversation/index.test.ts
+++ /dev/null
@@ -1,256 +0,0 @@
-import { STUDIO_CHAT_ARTIFACT_VERSION } from '@studio/common/ai/chat-artifacts';
-import { fireEvent, render, screen } from '@testing-library/react';
-import { createElement } from 'react';
-import { beforeEach, describe, expect, it, vi } from 'vitest';
-import { ChatArtifact, entriesToRenderItems } from './index';
-import type { SessionEntry } from '@earendil-works/pi-coding-agent';
-
-const deskMocks = vi.hoisted( () => ( {
- addWidget: vi.fn(),
- addWidgetAtScreenPoint: vi.fn(),
-} ) );
-
-vi.mock( '@/components/markdown', () => ( {
- Markdown: () => null,
-} ) );
-
-vi.mock( '@/ui-desks/components', () => ( {
- Button: () => null,
-} ) );
-
-vi.mock( '@/ui-desks/desk/provider', () => ( {
- useDesk: () => ( {
- addWidget: deskMocks.addWidget,
- addWidgetAtScreenPoint: deskMocks.addWidgetAtScreenPoint,
- canAddWidgets: true,
- } ),
-} ) );
-
-vi.mock( '@/ui-desks/widget-actions/create-widget', () => ( {
- createDeskWidget: vi.fn(
- ( options: {
- id: string;
- type: string;
- zIndex: string;
- shapeProps?: Record< string, unknown >;
- widgetProps?: Record< string, unknown >;
- } ) => ( {
- id: options.id,
- type: options.type,
- x: 0,
- y: 0,
- zIndex: options.zIndex,
- shapeProps: { w: 120, h: 96, ...options.shapeProps },
- widgetProps: options.widgetProps ?? {},
- } )
- ),
-} ) );
-
-vi.mock( '@wordpress/icons', () => ( {
- check: {},
- plus: {},
-} ) );
-
-vi.mock( '@wordpress/ui', () => ( {
- Icon: () => null,
-} ) );
-
-vi.mock( '../thinking-indicator', () => ( {
- ThinkingIndicator: () => null,
-} ) );
-
-vi.mock( '../widget-context', () => ( {
- summarizeWidgetList: () => '',
- getWidgetDisplayLabel: ( widget: { type: string } ) => `${ widget.type } widget`,
- WidgetContextThumbnail: () => null,
- WidgetContextThumbnailList: () => null,
-} ) );
-
-describe( 'desks conversation render items', () => {
- beforeEach( () => {
- deskMocks.addWidget.mockReset();
- deskMocks.addWidgetAtScreenPoint.mockReset();
- } );
-
- it( 'hides studio_present tool rows while keeping the artifact', () => {
- const items = entriesToRenderItems( [
- assistantToolCallEntry( 'studio_present' ),
- toolResultEntry( 'Presented 5 Studio widgets.' ),
- chatArtifactEntry(),
- ] );
-
- expect( items.some( ( item ) => item.kind === 'tool-use' ) ).toBe( false );
- expect( items.some( ( item ) => item.kind === 'chat-artifact' ) ).toBe( true );
- } );
-
- it( 'keeps regular tool rows visible', () => {
- const items = entriesToRenderItems( [
- assistantToolCallEntry( 'wp_cli' ),
- toolResultEntry( 'Success' ),
- ] );
-
- expect( items ).toEqual(
- expect.arrayContaining( [
- expect.objectContaining( {
- kind: 'tool-use',
- name: 'wp_cli',
- result: expect.objectContaining( { text: 'Success' } ),
- } ),
- ] )
- );
- } );
-
- it( 'adds widgets from multi-widget artifacts independently', () => {
- deskMocks.addWidget.mockReturnValue( true );
-
- render(
- createElement( ChatArtifact, {
- artifact: {
- version: STUDIO_CHAT_ARTIFACT_VERSION,
- id: 'artifact-2',
- widgets: [
- {
- type: 'note',
- widgetProps: { text: 'First note', tone: 'yellow' },
- },
- {
- type: 'bookmark',
- widgetProps: { url: 'https://example.com' },
- },
- ],
- },
- } )
- );
-
- fireEvent.click(
- screen.getByRole( 'button', { name: 'Add widget 2 to canvas: bookmark widget' } )
- );
-
- expect( deskMocks.addWidget ).toHaveBeenCalledTimes( 1 );
- expect( deskMocks.addWidget ).toHaveBeenCalledWith(
- 'bookmark',
- expect.objectContaining( {
- widgetProps: { url: 'https://example.com' },
- shouldStartEditing: false,
- } )
- );
- expect(
- screen.getByRole( 'button', { name: 'Added widget 2 to canvas: bookmark widget' } )
- ).toBeDisabled();
-
- fireEvent.click( screen.getByRole( 'button', { name: 'Add remaining' } ) );
-
- expect( deskMocks.addWidget ).toHaveBeenCalledTimes( 2 );
- expect( deskMocks.addWidget ).toHaveBeenLastCalledWith(
- 'note',
- expect.objectContaining( {
- widgetProps: { text: 'First note', tone: 'yellow' },
- shouldStartEditing: false,
- } )
- );
- } );
-
- it( 'renders scratchpad artifacts as a preview card and adds synced run metadata', () => {
- deskMocks.addWidget.mockReturnValue( true );
-
- render(
- createElement( ChatArtifact, {
- artifact: {
- version: STUDIO_CHAT_ARTIFACT_VERSION,
- id: 'scratchpad-artifact',
- widgets: [
- {
- type: 'scratchpad',
- widgetProps: {
- html: 'Draft
',
- title: 'Landing page draft',
- scope: 'page',
- description: 'Tighten the hero.',
- },
- },
- ],
- },
- } )
- );
-
- expect( screen.getByText( 'Landing page draft' ) ).toBeInTheDocument();
- expect( screen.getByText( 'Tighten the hero.' ) ).toBeInTheDocument();
-
- fireEvent.click( screen.getByRole( 'button', { name: 'Add to canvas' } ) );
-
- expect( deskMocks.addWidget ).toHaveBeenCalledWith(
- 'scratchpad',
- expect.objectContaining( {
- widgetProps: expect.objectContaining( {
- description: 'Tighten the hero.',
- agentStatus: 'idle',
- lastSyncedDescription: 'Tighten the hero.',
- } ),
- shouldStartEditing: false,
- } )
- );
- expect( screen.getByRole( 'button', { name: 'Added' } ) ).toBeDisabled();
- } );
-} );
-
-function assistantToolCallEntry( name: string ): SessionEntry {
- return {
- type: 'message',
- id: `assistant-${ name }`,
- parentId: null,
- timestamp: '2026-05-13T00:00:00.000Z',
- message: {
- role: 'assistant',
- content: [
- {
- type: 'toolCall',
- id: 'tool-call-1',
- name,
- arguments: {},
- },
- ],
- },
- } as unknown as SessionEntry;
-}
-
-function toolResultEntry( text: string ): SessionEntry {
- return {
- type: 'message',
- id: 'tool-result',
- parentId: null,
- timestamp: '2026-05-13T00:00:01.000Z',
- message: {
- role: 'toolResult',
- toolCallId: 'tool-call-1',
- content: [ { type: 'text', text } ],
- },
- } as unknown as SessionEntry;
-}
-
-function chatArtifactEntry(): SessionEntry {
- return {
- type: 'custom',
- id: 'artifact',
- parentId: null,
- timestamp: '2026-05-13T00:00:02.000Z',
- customType: 'studio.chat_artifact',
- data: {
- version: STUDIO_CHAT_ARTIFACT_VERSION,
- id: 'artifact-1',
- widgets: [
- {
- type: 'post-collection',
- widgetProps: {
- query: {
- postType: 'post',
- perPage: 5,
- status: 'publish',
- orderby: 'date',
- order: 'desc',
- },
- },
- },
- ],
- },
- } as unknown as SessionEntry;
-}
diff --git a/apps/ui/src/ui-desks/chats/conversation/index.tsx b/apps/ui/src/ui-desks/chats/conversation/index.tsx
deleted file mode 100644
index b089d3cd22..0000000000
--- a/apps/ui/src/ui-desks/chats/conversation/index.tsx
+++ /dev/null
@@ -1,799 +0,0 @@
-import {
- isStudioChatArtifactData,
- type StudioChatArtifactData,
-} from '@studio/common/ai/chat-artifacts';
-import {
- isStudioCustomEntryOfType,
- type StudioCustomEntry,
-} from '@studio/common/ai/sessions/entry-types';
-import {
- getToolDetail,
- getToolDisplayName,
- type NormalizedToolResult,
-} from '@studio/common/ai/tools';
-import { __, sprintf } from '@wordpress/i18n';
-import { check, plus } from '@wordpress/icons';
-import { Icon } from '@wordpress/ui';
-import { clsx } from 'clsx';
-import { useMemo, useState, type PointerEvent as ReactPointerEvent } from 'react';
-import { createPortal } from 'react-dom';
-import { Markdown } from '@/components/markdown';
-import { Button } from '@/ui-desks/components';
-import { useDesk } from '@/ui-desks/desk/provider';
-import { createDeskWidget } from '@/ui-desks/widget-actions/create-widget';
-import {
- SCRATCHPAD_WIDGET_TYPE,
- isScratchpadWidgetProps,
- type ScratchpadWidget,
- type ScratchpadWidgetProps,
-} from '@/ui-desks/widgets/scratchpad/types';
-import { ThinkingIndicator } from '../thinking-indicator';
-import {
- getWidgetDisplayLabel,
- summarizeWidgetList,
- WidgetContextThumbnail,
- WidgetContextThumbnailList,
-} from '../widget-context';
-import styles from './style.module.css';
-import type { LoadedAiSession } from '@/data/core';
-import type { DeskWidget } from '@/ui-desks/widgets/types';
-import type { SessionEntry } from '@earendil-works/pi-coding-agent';
-
-type RenderItem =
- | { kind: 'user-text'; key: string; text: string }
- | { kind: 'assistant-text'; key: string; text: string }
- | { kind: 'chat-artifact'; key: string; artifact: StudioChatArtifactData }
- | {
- kind: 'tool-use';
- key: string;
- name: string;
- input?: Record< string, unknown >;
- result?: NormalizedToolResult;
- }
- | {
- kind: 'agent-question';
- key: string;
- question: string;
- options: Array< { label: string; description: string } >;
- }
- | { kind: 'interrupted-marker'; key: string };
-
-interface PiAssistantContentBlock {
- type: 'text' | 'toolCall' | 'thinking';
- text?: string;
- id?: string;
- name?: string;
- arguments?: Record< string, unknown >;
-}
-
-interface PiAssistantMessageLike {
- role: 'assistant';
- content: PiAssistantContentBlock[];
-}
-
-interface PiToolResultLike {
- role: 'toolResult';
- toolCallId: string;
- content?: Array< { type: string; text?: string } >;
- isError?: boolean;
-}
-
-const HIDDEN_TOOL_ROWS = new Set( [ 'studio_present' ] );
-
-export function entriesToRenderItems( entries: SessionEntry[] ): RenderItem[] {
- const resultsByToolCallId = new Map< string, NormalizedToolResult >();
- for ( const entry of entries ) {
- if ( entry.type !== 'message' ) continue;
- const message = ( entry as { message?: unknown } ).message as PiToolResultLike | undefined;
- if ( ! message || message.role !== 'toolResult' ) continue;
- const text = ( message.content ?? [] )
- .filter( ( b ) => b.type === 'text' && typeof b.text === 'string' )
- .map( ( b ) => b.text as string )
- .join( '\n' );
- resultsByToolCallId.set( message.toolCallId, {
- text,
- isError: message.isError === true,
- } );
- }
-
- const items: RenderItem[] = [];
- entries.forEach( ( entry, entryIndex ) => {
- if ( isStudioCustomEntryOfType( entry, 'studio.user_prompt' ) ) {
- const data = ( entry as StudioCustomEntry< 'studio.user_prompt' > ).data;
- if ( ! data || data.source !== 'prompt' ) return;
- items.push( {
- kind: 'user-text',
- key: `${ entryIndex }:user`,
- text: data.text,
- } );
- return;
- }
-
- if ( entry.type === 'message' ) {
- const message = ( entry as { message?: unknown } ).message as
- | PiAssistantMessageLike
- | undefined;
- if ( ! message || message.role !== 'assistant' || ! Array.isArray( message.content ) ) {
- return;
- }
- message.content.forEach( ( block, blockIndex ) => {
- if ( block.type === 'text' && typeof block.text === 'string' ) {
- const text = block.text.trim();
- if ( text ) {
- items.push( {
- kind: 'assistant-text',
- key: `${ entryIndex }:${ blockIndex }:text`,
- text,
- } );
- }
- } else if (
- block.type === 'toolCall' &&
- typeof block.id === 'string' &&
- typeof block.name === 'string' &&
- ! HIDDEN_TOOL_ROWS.has( block.name )
- ) {
- items.push( {
- kind: 'tool-use',
- key: `${ entryIndex }:${ blockIndex }:tool`,
- name: block.name,
- input: ( block.arguments as Record< string, unknown > ) ?? {},
- result: resultsByToolCallId.get( block.id ),
- } );
- }
- } );
- return;
- }
-
- if ( isStudioCustomEntryOfType( entry, 'studio.chat_artifact' ) ) {
- const data = ( entry as StudioCustomEntry< 'studio.chat_artifact' > ).data;
- if ( ! isStudioChatArtifactData( data ) ) return;
- items.push( {
- kind: 'chat-artifact',
- key: `${ entryIndex }:artifact`,
- artifact: data,
- } );
- return;
- }
-
- if ( isStudioCustomEntryOfType( entry, 'studio.agent_question' ) ) {
- const data = ( entry as StudioCustomEntry< 'studio.agent_question' > ).data;
- if ( ! data ) return;
- items.push( {
- kind: 'agent-question',
- key: `${ entryIndex }:question`,
- question: data.question,
- options: data.options,
- } );
- return;
- }
-
- if ( isStudioCustomEntryOfType( entry, 'studio.turn_closed' ) ) {
- const data = ( entry as StudioCustomEntry< 'studio.turn_closed' > ).data;
- if ( data?.status === 'interrupted' ) {
- items.push( {
- kind: 'interrupted-marker',
- key: `${ entryIndex }:interrupted`,
- } );
- }
- }
- } );
-
- return items;
-}
-
-function findLatestProgressMessage( entries: SessionEntry[] ): string | null {
- for ( let i = entries.length - 1; i >= 0; i -= 1 ) {
- const entry = entries[ i ];
- if (
- isStudioCustomEntryOfType( entry, 'studio.user_prompt' ) ||
- isStudioCustomEntryOfType( entry, 'studio.turn_closed' )
- ) {
- return null;
- }
- if ( isStudioCustomEntryOfType( entry, 'studio.tool_progress' ) ) {
- const data = ( entry as StudioCustomEntry< 'studio.tool_progress' > ).data;
- if ( data ) return data.message;
- }
- }
- return null;
-}
-
-function findLatestToolUseKey( items: RenderItem[] ): string | null {
- for ( let i = items.length - 1; i >= 0; i -= 1 ) {
- const item = items[ i ];
- if ( item.kind === 'tool-use' ) {
- return item.key;
- }
- }
- return null;
-}
-
-function UserMessage( { text }: { text: string } ) {
- return (
-
- );
-}
-
-function AssistantMessage( { text }: { text: string } ) {
- return (
-
- );
-}
-
-const TOOL_RESULT_PREVIEW_MAX_LINES = 12;
-const ARTIFACT_DRAG_THRESHOLD = 4;
-const ARTIFACT_DRAG_SPACING = 32;
-
-interface ArtifactDragState {
- widgets: DeskWidget[];
- x: number;
- y: number;
- isOverCanvas: boolean;
-}
-
-function ToolUseRow( {
- name,
- input,
- result,
-}: {
- name: string;
- input?: Record< string, unknown >;
- result?: NormalizedToolResult;
-} ) {
- const label = getToolDisplayName( name );
- const detail = getToolDetail( name, input );
- const [ expanded, setExpanded ] = useState( false );
- const resultText = result?.text?.trim() ?? '';
- const hasOutput = resultText.length > 0;
- const isLong = resultText.split( '\n' ).length > TOOL_RESULT_PREVIEW_MAX_LINES;
-
- return (
-
-
-
- { label }
- { detail ? { detail } : null }
-
- { hasOutput ? (
-
-
- { resultText }
-
- { isLong ? (
-
- ) : null }
-
- ) : null }
-
-
- );
-}
-
-export function ChatArtifact( { artifact }: { artifact: StudioChatArtifactData } ) {
- const { addWidget, addWidgetAtScreenPoint, canAddWidgets } = useDesk();
- const [ addedWidgetIds, setAddedWidgetIds ] = useState< ReadonlySet< string > >(
- () => new Set()
- );
- const [ dragState, setDragState ] = useState< ArtifactDragState | null >( null );
- const widgets = useMemo(
- () =>
- artifact.widgets
- .map( ( widget, index ) =>
- createDeskWidget( {
- id: `${ artifact.id }-${ index }`,
- type: widget.type,
- center: { x: 0, y: 0 },
- zIndex: 'a1',
- shapeProps: widget.shapeProps,
- widgetProps: normalizeChatArtifactWidgetProps( widget.type, widget.widgetProps ),
- } )
- )
- .filter( ( widget ): widget is DeskWidget => widget !== null ),
- [ artifact ]
- );
-
- if ( widgets.length === 0 ) {
- return null;
- }
-
- const summary = summarizeWidgetList( widgets );
- const allAdded = widgets.every( ( widget ) => addedWidgetIds.has( widget.id ) );
- const hasAddedSome = widgets.some( ( widget ) => addedWidgetIds.has( widget.id ) );
- const scratchpadWidget =
- widgets.length === 1 && isScratchpadWidget( widgets[ 0 ] ) ? widgets[ 0 ] : null;
- const markWidgetsAdded = ( widgetIds: string[] ) => {
- if ( widgetIds.length === 0 ) {
- return;
- }
-
- setAddedWidgetIds( ( previousIds ) => {
- const nextIds = new Set( previousIds );
- widgetIds.forEach( ( widgetId ) => nextIds.add( widgetId ) );
- return nextIds;
- } );
- };
- const insertWidgetOnCanvas = ( widget: DeskWidget, screenPoint?: { x: number; y: number } ) => {
- const options = {
- shapeProps: widget.shapeProps,
- widgetProps: widget.widgetProps,
- shouldStartEditing: false,
- };
- if ( screenPoint ) {
- return addWidgetAtScreenPoint( widget.type, screenPoint, options );
- }
-
- return addWidget( widget.type, options );
- };
- const addWidgetToCanvas = ( widget: DeskWidget, screenPoint?: { x: number; y: number } ) => {
- if ( addedWidgetIds.has( widget.id ) ) {
- return false;
- }
-
- const didAdd = insertWidgetOnCanvas( widget, screenPoint );
- if ( didAdd ) {
- markWidgetsAdded( [ widget.id ] );
- }
- return didAdd;
- };
- const addWidgetsToCanvas = ( screenPoint?: { x: number; y: number } ) => {
- const addedIds: string[] = [];
- widgets.forEach( ( widget, index ) => {
- if ( addedWidgetIds.has( widget.id ) ) {
- return;
- }
-
- const didAdd = insertWidgetOnCanvas(
- widget,
- screenPoint
- ? {
- x: screenPoint.x + index * ARTIFACT_DRAG_SPACING,
- y: screenPoint.y,
- }
- : undefined
- );
- if ( didAdd ) {
- addedIds.push( widget.id );
- }
- } );
- markWidgetsAdded( addedIds );
- return addedIds.length > 0;
- };
-
- const handlePointerDown = ( event: ReactPointerEvent< HTMLDivElement > ) => {
- const draggableWidgets = widgets.filter( ( widget ) => ! addedWidgetIds.has( widget.id ) );
- if (
- event.button !== 0 ||
- ! canAddWidgets ||
- draggableWidgets.length === 0 ||
- isInteractiveArtifactTarget( event.target )
- ) {
- return;
- }
-
- event.preventDefault();
-
- const pointerId = event.pointerId;
- const startX = event.clientX;
- const startY = event.clientY;
- let didStartDrag = false;
-
- const cleanup = () => {
- window.removeEventListener( 'pointermove', handlePointerMove, true );
- window.removeEventListener( 'pointerup', handlePointerUp, true );
- window.removeEventListener( 'pointercancel', handlePointerCancel, true );
- setDragState( null );
- };
-
- const syncDragState = ( pointerEvent: PointerEvent ) => {
- setDragState( {
- widgets: draggableWidgets,
- x: pointerEvent.clientX,
- y: pointerEvent.clientY,
- isOverCanvas: isCanvasDropTargetAtPoint( pointerEvent.clientX, pointerEvent.clientY ),
- } );
- };
-
- const handlePointerMove = ( pointerEvent: PointerEvent ) => {
- if ( pointerEvent.pointerId !== pointerId ) {
- return;
- }
-
- if ( ! didStartDrag ) {
- const distance = Math.hypot( pointerEvent.clientX - startX, pointerEvent.clientY - startY );
- if ( distance < ARTIFACT_DRAG_THRESHOLD ) {
- return;
- }
- didStartDrag = true;
- }
-
- syncDragState( pointerEvent );
- pointerEvent.preventDefault();
- };
-
- const handlePointerUp = ( pointerEvent: PointerEvent ) => {
- if ( pointerEvent.pointerId !== pointerId ) {
- return;
- }
-
- const shouldDrop =
- didStartDrag && isCanvasDropTargetAtPoint( pointerEvent.clientX, pointerEvent.clientY );
- if ( shouldDrop ) {
- addWidgetsToCanvas( { x: pointerEvent.clientX, y: pointerEvent.clientY } );
- pointerEvent.preventDefault();
- }
- cleanup();
- };
-
- const handlePointerCancel = ( pointerEvent: PointerEvent ) => {
- if ( pointerEvent.pointerId === pointerId ) {
- cleanup();
- }
- };
-
- window.addEventListener( 'pointermove', handlePointerMove, true );
- window.addEventListener( 'pointerup', handlePointerUp, true );
- window.addEventListener( 'pointercancel', handlePointerCancel, true );
- };
-
- return (
-
- { scratchpadWidget ? (
-
addWidgetToCanvas( scratchpadWidget ) }
- onPointerDown={ handlePointerDown }
- />
- ) : (
- <>
-
-
- { widgets.map( ( widget, index ) => {
- const isAdded = addedWidgetIds.has( widget.id );
- const widgetLabel = getWidgetDisplayLabel( widget );
- const addLabel = getWidgetAddActionLabel( widget, index, isAdded );
- return (
-
-
- { widgets.length > 1 && (
-
- ) }
-
- );
- } ) }
-
-
-
- { widgets.length > 1 ? (
-
- ) : (
-
- ) }
-
- >
- ) }
- { dragState && typeof document !== 'undefined'
- ? createPortal( , document.body )
- : null }
-
- );
-}
-
-function ScratchpadArtifactCard( {
- widget,
- isAdded,
- canAddWidgets,
- onAdd,
- onPointerDown,
-}: {
- widget: ScratchpadWidget;
- isAdded: boolean;
- canAddWidgets: boolean;
- onAdd: () => void;
- onPointerDown: ( event: ReactPointerEvent< HTMLDivElement > ) => void;
-} ) {
- const title = widget.widgetProps.title || __( 'Scratchpad' );
- const description = widget.widgetProps.description?.trim();
-
- return (
- <>
-
-
-
{ title }
- { description ? (
-
{ description }
- ) : null }
-
-
-
-
-
-
- >
- );
-}
-
-function normalizeChatArtifactWidgetProps(
- type: string,
- widgetProps: Record< string, unknown >
-): Record< string, unknown > {
- if ( type !== SCRATCHPAD_WIDGET_TYPE || ! isScratchpadWidgetProps( widgetProps ) ) {
- return widgetProps;
- }
-
- return normalizeScratchpadArtifactWidgetProps( widgetProps );
-}
-
-function normalizeScratchpadArtifactWidgetProps(
- widgetProps: ScratchpadWidgetProps
-): ScratchpadWidgetProps {
- const description = widgetProps.description ?? '';
- return {
- ...widgetProps,
- agentStatus: widgetProps.agentStatus ?? 'idle',
- lastSyncedDescription: widgetProps.lastSyncedDescription ?? description,
- };
-}
-
-function isScratchpadWidget( widget: DeskWidget ): widget is ScratchpadWidget {
- return widget.type === SCRATCHPAD_WIDGET_TYPE && isScratchpadWidgetProps( widget.widgetProps );
-}
-
-function getWidgetAddActionLabel( widget: DeskWidget, index: number, isAdded: boolean ) {
- const widgetLabel = getWidgetDisplayLabel( widget );
- return isAdded
- ? sprintf(
- /* translators: 1: widget number in the artifact, 2: widget label. */
- __( 'Added widget %1$d to canvas: %2$s' ),
- index + 1,
- widgetLabel
- )
- : sprintf(
- /* translators: 1: widget number in the artifact, 2: widget label. */
- __( 'Add widget %1$d to canvas: %2$s' ),
- index + 1,
- widgetLabel
- );
-}
-
-function ArtifactDragOverlay( { state }: { state: ArtifactDragState } ) {
- return (
-
- );
-}
-
-function isCanvasDropTargetAtPoint( x: number, y: number ) {
- const element = document.elementFromPoint( x, y );
- return Boolean( element?.closest( '[data-ui-desks-canvas]' ) );
-}
-
-function isInteractiveArtifactTarget( target: EventTarget | null ) {
- return Boolean( ( target as HTMLElement | null )?.closest( 'button,a,input,textarea,select' ) );
-}
-
-function AgentQuestion( {
- question,
- options,
- isInteractive,
- pickedLabel,
- onAnswer,
-}: {
- question: string;
- options: Array< { label: string; description: string } >;
- isInteractive: boolean;
- pickedLabel: string | undefined;
- onAnswer: ( label: string ) => void;
-} ) {
- return (
-
-
-
{ question }
- { options.length > 0 ? (
-
- { options.map( ( option, index ) => {
- const picked = option.label === pickedLabel;
- return (
- -
-
-
- );
- } ) }
-
- ) : null }
-
-
- );
-}
-
-export function Conversation( {
- data,
- isRunning,
- startedAt,
- pendingQuestions,
- pendingAnswers,
- onAnswerQuestion,
-}: {
- data: LoadedAiSession;
- isRunning: boolean;
- startedAt: number | null;
- pendingQuestions: Set< string >;
- pendingAnswers: Record< string, string >;
- onAnswerQuestion: ( question: string, label: string ) => void;
-} ) {
- const entries = data.entries;
- const items = useMemo( () => entriesToRenderItems( entries ), [ entries ] );
- const progressMessage = useMemo(
- () => ( isRunning ? findLatestProgressMessage( entries ) : null ),
- [ entries, isRunning ]
- );
- const thinkingMessageKey = useMemo( () => findLatestToolUseKey( items ), [ items ] );
-
- return (
-
- { items.map( ( item ) => {
- switch ( item.kind ) {
- case 'user-text':
- return
;
- case 'assistant-text':
- return
;
- case 'chat-artifact':
- return
;
- case 'tool-use':
- return (
-
- );
- case 'agent-question':
- return (
-
onAnswerQuestion( item.question, label ) }
- />
- );
- case 'interrupted-marker':
- return (
-
- { __( 'Interrupted by you' ) }
-
- );
- default:
- return null;
- }
- } ) }
-
-
-
-
- );
-}
diff --git a/apps/ui/src/ui-desks/chats/conversation/style.module.css b/apps/ui/src/ui-desks/chats/conversation/style.module.css
deleted file mode 100644
index b0bc9a6980..0000000000
--- a/apps/ui/src/ui-desks/chats/conversation/style.module.css
+++ /dev/null
@@ -1,454 +0,0 @@
-.root {
- display: flex;
- flex-direction: column;
- gap: 10px;
- padding: 4px 4px 12px;
- color: var(--ui-desks-text, #14171a);
- font-size: 13px;
- line-height: 1.5;
-}
-
-.messageGroup {
- display: flex;
- flex-direction: column;
- gap: 6px;
- max-width: 100%;
-}
-
-.userGroup {
- align-items: flex-end;
-}
-
-.assistantGroup {
- align-items: flex-start;
-}
-
-.message {
- padding: 10px 14px;
- border-radius: 16px;
- corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- -webkit-corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- font-size: 13px;
- line-height: 1.5;
- max-width: 85%;
- word-wrap: break-word;
-}
-
-.userMessage {
- align-self: flex-end;
- background: rgba(255, 255, 255, 0.78);
- border: 1px solid rgba(255, 255, 255, 0.7);
- border-bottom-right-radius: 6px;
- color: var(--ui-desks-text, #14171a);
- white-space: pre-wrap;
-}
-
-.assistantMessage {
- align-self: flex-start;
- background: transparent;
- color: var(--ui-desks-text, #14171a);
- padding: 2px 0;
- max-width: 100%;
-}
-
-.assistantMarkdown {
- color: var(--ui-desks-text, #14171a);
- font-size: 13px;
- line-height: 1.5;
-}
-
-.assistantMarkdown p,
-.assistantMarkdown ul,
-.assistantMarkdown ol,
-.assistantMarkdown blockquote,
-.assistantMarkdown table {
- margin-bottom: 0.75em;
-}
-
-.assistantMarkdown pre {
- background: rgba(255, 255, 255, 0.7);
- border: 1px solid rgba(15, 23, 42, 0.06);
-}
-
-.toolBlock {
- display: flex;
- flex-direction: column;
- gap: 4px;
- max-width: 100%;
- color: var(--ui-desks-muted, #6b7280);
-}
-
-.toolRow {
- display: flex;
- align-items: baseline;
- gap: 8px;
- min-width: 0;
- padding: 2px 0;
- font-family: var(--wpds-typography-font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
- font-size: 12px;
- color: var(--ui-desks-muted, #6b7280);
-}
-
-.toolLabel {
- color: var(--ui-desks-text, #14171a);
- font-weight: 600;
- flex-shrink: 0;
-}
-
-.toolDetail {
- min-width: 0;
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- color: var(--ui-desks-text, #14171a);
-}
-
-.toolOutputWrap {
- display: flex;
- flex-direction: column;
- gap: 4px;
- margin-left: 12px;
-}
-
-.toolOutput {
- margin: 0;
- padding: 8px 10px;
- border-left: 2px solid var(--ui-desks-divider, rgba(15, 23, 42, 0.06));
- color: var(--ui-desks-text, #14171a);
- font-family: var(--wpds-typography-font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
- font-size: 11px;
- line-height: 1.45;
- white-space: pre-wrap;
- word-wrap: break-word;
- overflow: hidden;
-}
-
-.toolOutputCollapsed {
- max-height: calc(1.45em * 12);
- mask-image: linear-gradient(to bottom, black 60%, transparent 100%);
-}
-
-.toolOutputError {
- border-left-color: var(--wpds-color-fg-content-error, #dc2626);
- color: var(--wpds-color-fg-content-error, #dc2626);
-}
-
-.toolOutputToggle {
- --button-background: transparent;
- --button-foreground: var(--ui-desks-focus, #3858e9);
-
- align-self: flex-start;
- border: 0;
- font: inherit;
- font-size: 12px;
- cursor: pointer;
- padding: 0;
-}
-
-.toolOutputToggle.toolOutputToggle:hover:not(:disabled),
-.toolOutputToggle.toolOutputToggle:focus-visible {
- --button-background: transparent;
-
- text-decoration: underline;
-}
-
-.artifact {
- display: inline-flex;
- max-width: 100%;
- cursor: grab;
- user-select: none;
- touch-action: none;
-}
-
-.artifact:active {
- cursor: grabbing;
-}
-
-.artifactThumbnails {
- min-width: 0;
- flex: 0 1 auto;
- display: flex;
- flex-wrap: wrap;
- align-items: flex-start;
- gap: 8px;
-}
-
-.artifactThumbnail {
- position: relative;
- flex: 0 0 auto;
-}
-
-.artifactThumbnailAdd {
- position: absolute;
- top: -8px;
- right: -8px;
- width: 22px;
- height: 22px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- padding: 0;
- border: 1px solid rgba(15, 23, 42, 0.1);
- border-radius: 999px;
- background: rgba(255, 255, 255, 0.94);
- box-shadow: 0 3px 8px rgba(15, 23, 42, 0.12);
- color: var(--ui-desks-muted, #6b7280);
- cursor: pointer;
- opacity: 0;
- transform: scale(0.92);
- transition:
- opacity 120ms ease,
- transform 120ms ease;
- z-index: 1;
-}
-
-.artifactThumbnail:hover .artifactThumbnailAdd,
-.artifactThumbnail:focus-within .artifactThumbnailAdd,
-.artifactThumbnailAdd[data-added='true'] {
- opacity: 1;
- transform: scale(1);
-}
-
-.artifactThumbnailAdd:hover:not(:disabled),
-.artifactThumbnailAdd:focus-visible:not(:disabled) {
- background: rgba(255, 255, 255, 0.98);
- border-color: rgba(15, 23, 42, 0.18);
- color: var(--ui-desks-text, #14171a);
-}
-
-.artifactThumbnailAdd:disabled:not([data-added='true']) {
- cursor: default;
- opacity: 0.55;
-}
-
-.artifactThumbnailAdd[data-added='true'] {
- background: rgba(255, 255, 255, 0.98);
- border-color: rgba(15, 23, 42, 0.18);
- color: var(--ui-desks-text, #14171a);
- cursor: default;
- opacity: 1;
- transform: scale(1);
-}
-
-.artifactActions {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 8px;
- max-width: 100%;
-}
-
-.artifactAction {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- border: 0;
- border-radius: 10px;
- corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- -webkit-corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- background: #e5e7eb;
- color: var(--ui-desks-text, #14171a);
- font: inherit;
- font-size: 12px;
- font-weight: 500;
- line-height: 16px;
- padding: 6px 10px;
- cursor: pointer;
- transition: background 140ms ease;
-}
-
-.artifactAction span {
- white-space: nowrap;
-}
-
-.artifactAction:hover:not(:disabled),
-.artifactAction:focus-visible:not(:disabled) {
- background: #d1d5db;
-}
-
-.artifactAction:disabled {
- cursor: default;
- opacity: 0.55;
-}
-
-.scratchpadArtifact {
- width: 100%;
- max-width: 460px;
- display: flex;
- gap: 12px;
- padding: 12px;
- box-sizing: border-box;
- background-color: #fff;
- background-image: radial-gradient(circle, #c9cbce 1px, transparent 1.1px);
- background-size: 40px 40px;
- border: 1px solid rgba(15, 23, 42, 0.08);
- border-radius: 14px;
- cursor: grab;
- user-select: none;
- touch-action: none;
-}
-
-.scratchpadArtifact:active {
- cursor: grabbing;
-}
-
-.scratchpadArtifactBody {
- flex: 1 1 auto;
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 6px;
-}
-
-.scratchpadArtifactTitle {
- color: var(--ui-desks-text, #14171a);
- font-size: 13px;
- font-weight: 600;
- line-height: 18px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.scratchpadArtifactDescription {
- margin: 0;
- color: var(--ui-desks-muted, #6b7280);
- font-size: 12px;
- line-height: 1.4;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
-}
-
-.scratchpadArtifactThumbnail {
- position: relative;
- flex: 0 0 auto;
- width: 100px;
- height: 70px;
- border-radius: 8px;
- overflow: hidden;
- background: #fff;
- box-shadow:
- 0 1px 0 rgba(15, 23, 42, 0.06),
- 0 4px 10px rgba(15, 23, 42, 0.08);
-}
-
-.scratchpadArtifactFrame {
- position: absolute;
- inset: 0;
- width: calc(100% / 0.18);
- height: calc(100% / 0.18);
- border: 0;
- background: #fff;
- transform: scale(0.18);
- transform-origin: top left;
- pointer-events: none;
-}
-
-.scratchpadArtifactShield {
- position: absolute;
- inset: 0;
- background: transparent;
- cursor: inherit;
-}
-
-.artifactDragOverlay {
- position: fixed;
- z-index: 100000;
- transform: translate(-50%, -50%);
- pointer-events: none;
- opacity: 0;
- transition: opacity 120ms ease, transform 120ms ease;
-}
-
-.artifactDragOverlayInner {
- transform: scale(0.88);
- transform-origin: center;
- transition: transform 120ms ease;
- filter: drop-shadow(0 18px 28px rgba(15, 23, 42, 0.16));
-}
-
-.artifactDragOverlay[data-over='canvas'] {
- opacity: 1;
-}
-
-.artifactDragOverlay[data-over='canvas'] .artifactDragOverlayInner {
- transform: scale(1);
-}
-
-.question {
- display: flex;
- flex-direction: column;
- gap: 8px;
- max-width: 100%;
-}
-
-.questionText {
- margin: 0;
- color: var(--ui-desks-text, #14171a);
- font-size: 13px;
- line-height: 1.5;
-}
-
-.questionOptions {
- list-style: none;
- margin: 0;
- padding: 0;
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
-}
-
-.questionOption {
- --button-background: rgba(255, 255, 255, 0.6);
- --button-foreground: var(--ui-desks-text, #14171a);
-
- border: 1px solid var(--ui-desks-divider, rgba(15, 23, 42, 0.06));
- font: inherit;
- font-size: 12px;
- line-height: 16px;
- padding: 6px 10px;
- border-radius: 10px;
- cursor: pointer;
- transition: background 140ms ease, border-color 140ms ease;
-}
-
-.questionOption:hover:not(:disabled),
-.questionOption:focus-visible:not(:disabled) {
- --button-background: #fff;
-
- border-color: rgba(15, 23, 42, 0.16);
-}
-
-.questionOption:disabled {
- cursor: default;
- opacity: 0.55;
-}
-
-.questionOptionPicked,
-.questionOptionPicked:disabled {
- --button-background: rgba(56, 88, 233, 0.08);
- --button-foreground: var(--ui-desks-focus, #3858e9);
-
- border-color: rgba(56, 88, 233, 0.28);
- opacity: 1;
-}
-
-.interruptedMarker {
- align-self: center;
- margin: 4px 0;
- padding: 3px 10px;
- border-radius: 999px;
- background: rgba(15, 23, 42, 0.06);
- color: var(--ui-desks-muted, #6b7280);
- font-size: 12px;
- line-height: 16px;
- text-align: center;
-}
-
-.thinking {
- font-size: 13px;
- color: var(--ui-desks-muted, #6b7280);
-}
diff --git a/apps/ui/src/ui-desks/chats/index.ts b/apps/ui/src/ui-desks/chats/index.ts
deleted file mode 100644
index bbe32f7546..0000000000
--- a/apps/ui/src/ui-desks/chats/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { Chats, ChatsTrigger } from './panel';
-export { ChatsProvider } from './provider';
diff --git a/apps/ui/src/ui-desks/chats/panel/index.tsx b/apps/ui/src/ui-desks/chats/panel/index.tsx
deleted file mode 100644
index d4722647a1..0000000000
--- a/apps/ui/src/ui-desks/chats/panel/index.tsx
+++ /dev/null
@@ -1,466 +0,0 @@
-import { Dialog } from '@base-ui/react/dialog';
-import { __ } from '@wordpress/i18n';
-import { box, buttons, formatListBullets, plus } from '@wordpress/icons';
-import { Icon } from '@wordpress/ui';
-import { clsx } from 'clsx';
-import { useEffect, useState } from 'react';
-import motionStyles from '@/components/floating-surface-motion/style.module.css';
-import { useAuthUser, useLogin } from '@/data/queries/use-auth-user';
-import { useSessions, useUpdateSessionMetadata } from '@/data/queries/use-sessions';
-import { useSites } from '@/data/queries/use-sites';
-import { useFullscreen } from '@/hooks/use-fullscreen';
-import { ChatsButton } from '@/ui-desks/chrome/chats-button';
-import { Button } from '@/ui-desks/components';
-import { useChats } from '../context';
-import { SessionSurface } from '../session-surface';
-import { WidgetContextThumbnailList } from '../widget-context';
-import styles from './style.module.css';
-import type { ChatPromptRequest } from '../context';
-import type { ChatPanelResizeState, ChatPanelSide } from '../use-chat-panel-resize';
-import type { AiSessionSummary } from '@/data/core';
-import type { CSSProperties } from 'react';
-
-interface ChatsProps {
- siteId?: string;
- side: ChatPanelSide;
- panel: ChatPanelResizeState;
-}
-
-const COMPACT_STORAGE_KEY = 'ui-desks-chat-list-compact';
-
-function readStoredCompactPreference() {
- if ( typeof window === 'undefined' ) {
- return false;
- }
-
- return window.localStorage.getItem( COMPACT_STORAGE_KEY ) === '1';
-}
-
-function persistCompactPreference( compact: boolean ) {
- if ( typeof window === 'undefined' ) {
- return;
- }
-
- window.localStorage.setItem( COMPACT_STORAGE_KEY, compact ? '1' : '0' );
-}
-
-function getSessionTitle( session: AiSessionSummary ) {
- return session.firstPrompt?.trim() || __( 'New chat' );
-}
-
-function getSessionSubtitle( session: AiSessionSummary ) {
- return session.assistantReplyPreview ?? __( 'Ask Studio anything to get started.' );
-}
-
-function formatSessionTimeSince( value: string ) {
- const timestamp = Date.parse( value );
- if ( Number.isNaN( timestamp ) ) {
- return '';
- }
-
- const seconds = Math.max( 0, Math.floor( ( Date.now() - timestamp ) / 1000 ) );
- if ( seconds < 60 ) {
- return __( 'now' );
- }
-
- const minutes = Math.floor( seconds / 60 );
- if ( minutes < 60 ) {
- return `${ minutes }m`;
- }
-
- const hours = Math.floor( minutes / 60 );
- if ( hours < 24 ) {
- return `${ hours }h`;
- }
-
- const days = Math.floor( hours / 24 );
- if ( days < 7 ) {
- return `${ days }d`;
- }
-
- const weeks = Math.floor( days / 7 );
- if ( weeks < 4 ) {
- return `${ weeks }w`;
- }
-
- const months = Math.floor( days / 30 );
- if ( months < 12 ) {
- return `${ months }mo`;
- }
-
- return `${ Math.floor( days / 365 ) }y`;
-}
-
-function ChatSessionRow( {
- session,
- active,
- metadataPending,
- onSelect,
- onArchiveChange,
-}: {
- session: AiSessionSummary;
- active: boolean;
- metadataPending: boolean;
- onSelect: ( sessionId: string ) => void;
- onArchiveChange: ( session: AiSessionSummary, archived: boolean ) => void;
-} ) {
- const archived = !! session.archived;
-
- return (
-
-
-
-
- );
-}
-
-function EmptyChatState( {
- authRequiredPrompt,
- onContinuePrompt,
-}: {
- authRequiredPrompt?: ChatPromptRequest;
- onContinuePrompt: ( request: ChatPromptRequest ) => Promise< string >;
-} ) {
- const { data: authUser, isLoading: isLoadingAuthUser } = useAuthUser();
- const login = useLogin();
-
- if ( ! isLoadingAuthUser && ! authUser ) {
- return (
-
-
-
{ __( 'Log in to use Studio Desk chat' ) }
-
{ __( 'Studio Desk chat requires a WordPress.com account.' ) }
- { authRequiredPrompt ? (
-
-
{ __( 'Draft prompt' ) }
-
{ authRequiredPrompt.displayMessage ?? authRequiredPrompt.prompt }
-
- ) : null }
-
-
-
- );
- }
-
- if ( authUser && authRequiredPrompt ) {
- return (
-
-
-
{ __( 'Ready to continue' ) }
-
{ __( 'Your draft prompt is ready to send in a new chat.' ) }
-
-
{ __( 'Draft prompt' ) }
-
{ authRequiredPrompt.displayMessage ?? authRequiredPrompt.prompt }
-
-
-
-
- );
- }
-
- return { __( 'Ask Studio anything to get started.' ) }
;
-}
-
-export function ChatsTrigger() {
- const { open, setOpen } = useChats();
-
- return setOpen( ! open ) } />;
-}
-
-export function Chats( { siteId, side, panel }: ChatsProps ) {
- const {
- open,
- setOpen,
- selectedSessionId,
- expanded,
- autoFocusSessionId,
- isCreatingChat,
- pendingPrompt,
- authRequiredPrompt,
- composerWidgetDragPreview,
- selectSession,
- switchSession,
- clearSelection,
- startNewChat,
- startChatWithPrompt,
- consumePendingPrompt,
- } = useChats();
- const { data: sessions, isFetching: isFetchingSessions } = useSessions();
- const { data: sites } = useSites();
- const isFullscreen = useFullscreen();
- const updateSessionMetadata = useUpdateSessionMetadata();
- const { width, isResizing, listCollapsed, collapseList, expandList, startResize } = panel;
- const [ compact, setCompact ] = useState( readStoredCompactPreference );
- const [ archivedOpen, setArchivedOpen ] = useState( false );
- const site = siteId ? sites?.find( ( candidate ) => candidate.id === siteId ) : undefined;
- const scopedSessions = siteId
- ? site?.path
- ? ( sessions ?? [] ).filter( ( session ) => session.ownerSitePath === site.path )
- : []
- : ( sessions ?? [] ).filter( ( session ) => ! session.ownerSitePath );
- const chatSessions = [ ...scopedSessions ].sort(
- ( a, b ) => Date.parse( b.updatedAt ) - Date.parse( a.updatedAt )
- );
- const starredSessions = chatSessions.filter(
- ( session ) => session.starred && ! session.archived
- );
- const regularSessions = chatSessions.filter(
- ( session ) => ! session.starred && ! session.archived
- );
- const archivedSessions = chatSessions.filter( ( session ) => session.archived );
- const selectedSession =
- chatSessions.find( ( session ) => session.id === selectedSessionId ) ??
- ( sessions ?? [] ).find( ( session ) => session.id === selectedSessionId );
- const isListCollapsed = expanded && listCollapsed;
-
- useEffect( () => {
- if ( selectedSessionId && sessions && ! isFetchingSessions && ! selectedSession ) {
- clearSelection();
- }
- }, [ clearSelection, isFetchingSessions, selectedSession, selectedSessionId, sessions ] );
-
- useEffect( () => {
- persistCompactPreference( compact );
- }, [ compact ] );
-
- const updateSessionArchived = ( session: AiSessionSummary, archived: boolean ) => {
- void updateSessionMetadata.mutateAsync( {
- sessionId: session.id,
- patch: {
- archived,
- starred: session.starred,
- },
- } );
- };
-
- return (
-
-
-
- { ! isListCollapsed ? (
-
-
-
- { starredSessions.length > 0 ? (
-
- { __( 'Starred' ) }
- { starredSessions.map( ( session ) => (
-
- ) ) }
-
- ) : null }
-
- { regularSessions.map( ( session ) => (
-
- ) ) }
- { starredSessions.length === 0 && regularSessions.length === 0 ? (
- { __( 'No conversations yet.' ) }
- ) : null }
-
-
- { archivedSessions.length > 0 ? (
-
-
- { archivedOpen ? (
-
- { archivedSessions.map( ( session ) => (
-
- ) ) }
-
- ) : null }
-
- ) : null }
-
-
-
- ) : null }
- { expanded ? (
-
- { selectedSessionId ? (
-
- ) : (
-
- ) }
-
- ) : null }
-
-
- { composerWidgetDragPreview && (
-
-
-
- ) }
-
-
- );
-}
diff --git a/apps/ui/src/ui-desks/chats/panel/style.module.css b/apps/ui/src/ui-desks/chats/panel/style.module.css
deleted file mode 100644
index be295bcd14..0000000000
--- a/apps/ui/src/ui-desks/chats/panel/style.module.css
+++ /dev/null
@@ -1,508 +0,0 @@
-.panel {
- --desk-chats-panel-gap: 18px;
- --desk-chrome-button-size: 40px;
- --desk-chrome-titlebar-height: 46px;
- --desk-chrome-popup-offset: 8px;
- --desk-chats-list-pane-width: 300px;
- --desk-chats-panel-width: 760px;
- --desk-chats-session-padding: 14px;
-
- position: fixed;
- top: calc(
- var(--desk-chrome-titlebar-height) + var(--wpds-dimension-padding-sm) +
- var(--desk-chrome-button-size) + var(--desk-chrome-popup-offset)
- );
- left: var(--desk-chats-panel-gap);
- bottom: var(--desk-chats-panel-gap);
- z-index: 15;
- width: min(var(--desk-chats-list-pane-width), calc(100vw - 2 * var(--desk-chats-panel-gap)));
- display: flex;
- flex-direction: row;
- overflow: hidden;
- background: var(--ui-desks-glass, rgba(255, 255, 255, 0.85));
- border: 1px solid var(--ui-desks-glass-border, rgba(255, 255, 255, 0.7));
- border-radius: var(--ui-desks-radius-panel, 22px);
- corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- -webkit-corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- box-shadow: var(
- --ui-desks-shadow-surface-raised,
- 0 1px 2px rgba(15, 23, 42, 0.04),
- 0 18px 44px rgba(15, 23, 42, 0.1)
- );
- color: var(--ui-desks-text, #14171a);
- transform-origin: top left;
- -webkit-backdrop-filter: saturate(180%) blur(28px);
- backdrop-filter: saturate(180%) blur(28px);
- transition:
- width 280ms cubic-bezier(0.32, 0.72, 0, 1),
- transform 220ms cubic-bezier(0.32, 0.72, 0, 1),
- opacity 200ms cubic-bezier(0.32, 0.72, 0, 1);
- --studio-floating-surface-width-duration: 240ms;
- --studio-floating-surface-transform-duration: 220ms;
- --studio-floating-surface-opacity-duration: 200ms;
-}
-
-.panelExpanded {
- width: min(var(--desk-chats-panel-width), calc(100vw - 2 * var(--desk-chats-panel-gap)));
-}
-
-.panel[data-resizing='true'] {
- transition: none;
-}
-
-.panel[data-side='right'] {
- left: auto;
- right: var(--desk-chats-panel-gap);
- transform-origin: top right;
- flex-direction: row-reverse;
-}
-
-.panelFullscreen {
- top: calc(
- var(--desk-chats-panel-gap) + var(--desk-chrome-button-size) +
- var(--desk-chrome-popup-offset)
- );
-}
-
-.listPane {
- position: relative;
- display: flex;
- flex: 0 0 var(--desk-chats-list-pane-width);
- width: var(--desk-chats-list-pane-width);
- min-width: 0;
- min-height: 0;
- flex-direction: column;
-}
-
-.listDivider {
- position: absolute;
- top: 0;
- bottom: 0;
- right: 0;
- width: 1px;
- background: var(--ui-desks-divider, rgba(15, 23, 42, 0.06));
- opacity: 0;
- pointer-events: none;
- transition: opacity 200ms ease;
-}
-
-.panelExpanded .listDivider {
- opacity: 1;
-}
-
-.panel[data-side='right'] .listDivider {
- right: auto;
- left: 0;
-}
-
-.header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- padding: 16px 24px 12px;
- color: var(--ui-desks-text, #14171a);
-}
-
-.header h2 {
- margin: 0;
- font-size: 14px;
- line-height: 20px;
- font-weight: 600;
- letter-spacing: 0;
-}
-
-.headerActions {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- flex: 0 0 auto;
-}
-
-.headerAction {
- color: var(--ui-desks-muted, #6b7280);
-}
-
-.headerAction[data-active='true'] {
- color: var(--ui-desks-text, #14171a);
- background: var(--ui-desks-control-hover, rgba(15, 23, 42, 0.07));
-}
-
-.list {
- flex: 1;
- min-height: 0;
- overflow-y: auto;
- display: flex;
- flex-direction: column;
- gap: 2px;
- padding: 8px 12px;
- scrollbar-gutter: stable;
-}
-
-.emptyList {
- padding: 20px 12px;
- color: var(--ui-desks-muted, #6b7280);
- font-size: 13px;
- line-height: 18px;
-}
-
-.sessionSection {
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-
-.sessionSection + .sessionSection {
- margin-top: 12px;
-}
-
-.sessionSectionLabel {
- padding: 6px 12px 4px;
- color: var(--ui-desks-muted, #6b7280);
- font-size: 11px;
- line-height: 14px;
- font-weight: 600;
- letter-spacing: 0.04em;
- text-transform: uppercase;
-}
-
-.sessionItem {
- position: relative;
- min-width: 0;
- color: var(--ui-desks-text, #14171a);
-}
-
-.sessionSelectButton {
- --session-item-background: transparent;
- --wp-ui-button-background-color: var(--session-item-background);
- --wp-ui-button-background-color-active: var(--session-item-background);
- --wp-ui-button-border-color: transparent;
- --wp-ui-button-border-color-active: transparent;
-
- width: 100%;
- display: flex;
- flex-direction: column;
- gap: 4px;
- padding: 10px 12px;
- border: 0;
- border-radius: var(--ui-desks-radius-button, 12px);
- corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- -webkit-corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- background: var(--session-item-background);
- color: inherit;
- font: inherit;
- text-align: left;
- cursor: pointer;
- -webkit-app-region: no-drag;
-}
-
-.sessionSelectButton:hover,
-.sessionSelectButton:focus-visible {
- --session-item-background: rgba(15, 23, 42, 0.04);
- outline: none;
-}
-
-.sessionSelectButton:focus:not(:focus-visible) {
- outline: none;
- background-color: var(--session-item-background);
- border-color: transparent;
-}
-
-.sessionItem[data-active='true'] .sessionSelectButton {
- --session-item-background: var(--ui-desks-control-hover, rgba(15, 23, 42, 0.07));
-}
-
-.sessionItemRow {
- display: flex;
- align-items: center;
- gap: 8px;
- min-width: 0;
-}
-
-.sessionItemTitle,
-.sessionItemPreview {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.sessionItemTitle {
- flex: 1;
- font-size: 13px;
- line-height: 18px;
- font-weight: 500;
-}
-
-.sessionItemPreview {
- color: var(--ui-desks-muted, #6b7280);
- font-size: 12px;
- line-height: 16px;
-}
-
-.sessionItemMeta {
- flex: 0 0 auto;
- display: inline-flex;
- justify-content: flex-end;
- min-width: 22px;
- color: var(--ui-desks-muted, #6b7280);
- font-size: 11px;
- line-height: 16px;
- font-feature-settings: 'tnum' 1;
-}
-
-.sessionItemTime {
- transition: opacity 120ms ease;
-}
-
-.sessionItem:hover .sessionItemTime,
-.sessionItem:focus-within .sessionItemTime {
- opacity: 0;
-}
-
-.sessionItemArchive {
- position: absolute;
- top: 7px;
- right: 8px;
- z-index: 1;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 24px;
- height: 24px;
- padding: 0;
- border: 0;
- border-radius: 6px;
- background: transparent;
- color: var(--ui-desks-muted, #6b7280);
- cursor: pointer;
- opacity: 0;
- pointer-events: none;
- transition:
- opacity 120ms ease,
- background 120ms ease,
- color 120ms ease;
-}
-
-.sessionItemArchive svg {
- fill: currentColor;
-}
-
-.sessionItem:hover .sessionItemArchive,
-.sessionItem:focus-within .sessionItemArchive {
- opacity: 1;
- pointer-events: auto;
-}
-
-.sessionItemArchive:hover,
-.sessionItemArchive:focus-visible {
- background: rgba(15, 23, 42, 0.08);
- color: var(--ui-desks-text, #14171a);
- outline: none;
-}
-
-.sessionItemArchive:disabled {
- cursor: default;
- color: var(--ui-desks-muted, #6b7280);
- opacity: 0.45;
-}
-
-.listPane[data-compact='true'] .sessionItemPreview {
- display: none;
-}
-
-.listPane[data-compact='true'] .sessionSelectButton {
- padding-top: 6px;
- padding-bottom: 6px;
- border-radius: 7px;
-}
-
-.listPane[data-compact='true'] .sessionItemTitle {
- font-size: 12px;
- line-height: 16px;
-}
-
-.listPane[data-compact='true'] .sessionItemArchive {
- top: 3px;
-}
-
-.archived {
- display: flex;
- flex: 0 0 auto;
- min-height: 0;
- flex-direction: column;
- border-top: 1px solid var(--ui-desks-divider, rgba(15, 23, 42, 0.06));
- background: rgba(15, 23, 42, 0.02);
- transition: flex 240ms cubic-bezier(0.32, 0.72, 0.24, 1);
-}
-
-.archived[data-open='true'] {
- flex: 1 1 0;
-}
-
-.archivedToggle {
- display: flex;
- align-items: center;
- justify-content: space-between;
- width: 100%;
- padding: 12px 24px;
- border: 0;
- background: transparent;
- color: var(--ui-desks-muted, #6b7280);
- font: inherit;
- font-size: 12px;
- line-height: 16px;
- font-weight: 600;
- letter-spacing: 0.02em;
- cursor: pointer;
- -webkit-app-region: no-drag;
-}
-
-.archivedToggle:hover,
-.archivedToggle:focus-visible {
- color: var(--ui-desks-text, #14171a);
- outline: none;
-}
-
-.archivedCount {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- min-width: 18px;
- height: 16px;
- padding: 0 6px;
- border-radius: 999px;
- background: rgba(15, 23, 42, 0.08);
- color: var(--ui-desks-muted, #6b7280);
- font-size: 10px;
- line-height: 16px;
- font-weight: 700;
- font-feature-settings: 'tnum' 1;
-}
-
-.archivedList {
- display: flex;
- flex-direction: column;
- gap: 2px;
- overflow-y: auto;
- padding: 4px 12px 12px;
-}
-
-.footer {
- padding: 10px;
- border-top: 1px solid var(--ui-desks-divider, rgba(15, 23, 42, 0.06));
-}
-
-.newChatButton {
- width: 100%;
- justify-content: flex-start;
-}
-
-.chatPane {
- display: flex;
- flex: 1;
- min-width: 0;
- min-height: 0;
- position: relative;
-}
-
-.emptyChat {
- width: 100%;
- height: 100%;
- display: flex;
- align-items: flex-start;
- justify-content: center;
- padding-top: 48px;
- color: var(--ui-desks-muted, #6b7280);
- font-size: 13px;
-}
-
-.emptyChatContent {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 12px;
- width: min(320px, calc(100% - 32px));
- text-align: center;
-}
-
-.emptyChatContent p {
- margin: 0;
- color: var(--ui-desks-muted, #6b7280);
- font-size: 13px;
- line-height: 18px;
-}
-
-.emptyChatTitle {
- color: var(--ui-desks-text, #14171a);
- font-size: 14px;
- line-height: 20px;
- font-weight: 600;
-}
-
-.emptyChatDraft {
- width: 100%;
- box-sizing: border-box;
- padding: 10px 12px;
- border: 1px solid var(--ui-desks-divider, rgba(15, 23, 42, 0.06));
- border-radius: 12px;
- background: rgba(255, 255, 255, 0.64);
- text-align: left;
-}
-
-.emptyChatDraft span {
- display: block;
- margin-bottom: 4px;
- color: var(--ui-desks-muted, #6b7280);
- font-size: 11px;
- line-height: 14px;
- font-weight: 600;
- text-transform: uppercase;
-}
-
-.emptyChatDraft p {
- display: -webkit-box;
- -webkit-line-clamp: 3;
- -webkit-box-orient: vertical;
- overflow: hidden;
- color: var(--ui-desks-text, #14171a);
-}
-
-.resizeHandle {
- position: absolute;
- top: 12px;
- bottom: 12px;
- right: -3px;
- width: 8px;
- cursor: ew-resize;
- z-index: 2;
- touch-action: none;
- opacity: 0;
- pointer-events: none;
- transition: opacity 200ms ease;
-}
-
-.panel[data-side='right'] .resizeHandle {
- right: auto;
- left: -3px;
-}
-
-.panel[data-expanded='true'] .resizeHandle {
- opacity: 1;
- pointer-events: auto;
-}
-
-.widgetDragPreview {
- position: fixed;
- left: var(--desk-widget-drag-preview-x);
- top: var(--desk-widget-drag-preview-y);
- z-index: 30;
- pointer-events: none;
- transform: translate(-50%, -50%);
-}
-
-.widgetDragPreviewThumbnails {
- display: inline-flex;
- flex-wrap: nowrap;
- max-width: 260px;
-}
diff --git a/apps/ui/src/ui-desks/chats/provider.test.tsx b/apps/ui/src/ui-desks/chats/provider.test.tsx
deleted file mode 100644
index 4cdded7879..0000000000
--- a/apps/ui/src/ui-desks/chats/provider.test.tsx
+++ /dev/null
@@ -1,235 +0,0 @@
-import '@testing-library/jest-dom/vitest';
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
-import { beforeEach, describe, expect, it, vi } from 'vitest';
-import { ConnectorProvider } from '@/data/core';
-import { useChats } from './context';
-import { ChatsProvider } from './provider';
-import type { AuthUser, Connector } from '@/data/core';
-import type { AiSessionPlacementUpdatedEvent } from '@/data/core/types';
-
-const routerMock = vi.hoisted( () => ( {
- navigate: vi.fn(),
- search: { chats: true, session: 'session-1' } as Record< string, unknown >,
-} ) );
-
-vi.mock( '@tanstack/react-router', () => ( {
- useNavigate: () => routerMock.navigate,
- useSearch: () => routerMock.search,
-} ) );
-
-describe( 'ChatsProvider session placement changes', () => {
- beforeEach( () => {
- routerMock.navigate.mockReset();
- routerMock.search = { chats: true, session: 'session-1' };
- } );
-
- it( 'asks before switching desks when the selected chat moves to another site', async () => {
- const { emitPlacement } = renderProvider();
-
- await waitFor( () =>
- expect( screen.getByTestId( 'selected-session' ) ).toHaveTextContent( 'session-1' )
- );
- routerMock.navigate.mockClear();
-
- emitPlacement( createPlacementEvent() );
-
- expect(
- await screen.findByRole( 'dialog', { name: 'Continue in the site desk?' } )
- ).toBeVisible();
- expect( screen.getByText( /site desk for Created Site/ ) ).toBeVisible();
- expect( screen.getByRole( 'button', { name: 'Switch desks' } ) ).toHaveFocus();
- expect( routerMock.navigate ).not.toHaveBeenCalled();
- } );
-
- it( 'switches to the new site desk when confirmed', async () => {
- const { emitPlacement } = renderProvider();
-
- await waitFor( () =>
- expect( screen.getByTestId( 'selected-session' ) ).toHaveTextContent( 'session-1' )
- );
- routerMock.navigate.mockClear();
-
- emitPlacement( createPlacementEvent() );
- fireEvent.click( await screen.findByRole( 'button', { name: 'Switch desks' } ) );
-
- expect( routerMock.navigate ).toHaveBeenCalledTimes( 1 );
- const navigation = routerMock.navigate.mock.calls[ 0 ][ 0 ];
- expect( navigation.to ).toBe( '/sites/$siteId' );
- expect( navigation.params ).toEqual( { siteId: 'site-2' } );
- expect( navigation.search( { existing: true } ) ).toEqual( {
- existing: true,
- chats: true,
- session: 'session-1',
- } );
- } );
-
- it( 'stays on the current desk by closing the chat sidebar', async () => {
- const { emitPlacement } = renderProvider();
-
- await waitFor( () =>
- expect( screen.getByTestId( 'selected-session' ) ).toHaveTextContent( 'session-1' )
- );
- routerMock.navigate.mockClear();
-
- emitPlacement( createPlacementEvent() );
- fireEvent.click( await screen.findByRole( 'button', { name: 'Stay here' } ) );
-
- await waitFor( () =>
- expect( screen.getByTestId( 'selected-session' ) ).toHaveTextContent( 'none' )
- );
- expect(
- screen.queryByRole( 'dialog', { name: 'Continue in the site desk?' } )
- ).not.toBeInTheDocument();
- expect( routerMock.navigate ).toHaveBeenCalledTimes( 1 );
- const navigation = routerMock.navigate.mock.calls[ 0 ][ 0 ];
- expect( navigation.to ).toBe( '.' );
- expect( navigation.search( { chats: true, session: 'session-1', existing: true } ) ).toEqual( {
- chats: undefined,
- session: undefined,
- existing: true,
- } );
- } );
-
- it( 'does not create a chat when the user is logged out', async () => {
- routerMock.search = { chats: true };
- const { connector } = renderProvider( { authUser: null } );
-
- fireEvent.click( screen.getByRole( 'button', { name: 'Start new chat' } ) );
-
- await waitFor( () =>
- expect( screen.getByTestId( 'expanded' ) ).toHaveTextContent( 'expanded' )
- );
- expect( connector.createSession ).not.toHaveBeenCalled();
- expect( screen.getByTestId( 'selected-session' ) ).toHaveTextContent( 'none' );
- } );
-
- it( 'keeps a prompt for the login-required state instead of creating a chat', async () => {
- routerMock.search = { chats: true };
- const { connector } = renderProvider( { authUser: null } );
-
- fireEvent.click( screen.getByRole( 'button', { name: 'Start prompt chat' } ) );
-
- await waitFor( () =>
- expect( screen.getByTestId( 'auth-required-prompt' ) ).toHaveTextContent( 'Draft prompt' )
- );
- expect( connector.createSession ).not.toHaveBeenCalled();
- expect( screen.getByTestId( 'selected-session' ) ).toHaveTextContent( 'none' );
- } );
-
- it( 'creates and selects a chat when the user is logged in', async () => {
- routerMock.search = { chats: true };
- const { connector } = renderProvider();
-
- fireEvent.click( screen.getByRole( 'button', { name: 'Start new chat' } ) );
-
- await waitFor( () =>
- expect( screen.getByTestId( 'selected-session' ) ).toHaveTextContent( 'created-session' )
- );
- expect( connector.createSession ).toHaveBeenCalledTimes( 1 );
- } );
-} );
-
-function renderProvider( {
- siteId,
- authUser = createAuthUser(),
-}: {
- siteId?: string;
- authUser?: AuthUser | null;
-} = {} ) {
- let placementListener: ( ( event: AiSessionPlacementUpdatedEvent ) => void ) | undefined;
- const queryClient = new QueryClient( {
- defaultOptions: {
- queries: {
- retry: false,
- },
- },
- } );
- const connector = {
- getAuthUser: vi.fn().mockResolvedValue( authUser ),
- onAuthStateChanged: vi.fn( () => vi.fn() ),
- onSessionPlacementUpdated: vi.fn( ( listener ) => {
- placementListener = listener;
- return vi.fn();
- } ),
- createSession: vi.fn().mockResolvedValue( {
- id: 'created-session',
- } ),
- } as unknown as Connector;
-
- render(
-
-
-
-
-
-
-
-
- );
-
- return {
- connector,
- emitPlacement: ( event: AiSessionPlacementUpdatedEvent ) => {
- if ( ! placementListener ) {
- throw new Error( 'No placement listener registered.' );
- }
- act( () => {
- placementListener?.( event );
- } );
- },
- };
-}
-
-function ChatStateStatus() {
- const { selectedSessionId, expanded, authRequiredPrompt } = useChats();
- return (
- <>
- { selectedSessionId ?? 'none' }
- { expanded ? 'expanded' : 'collapsed' }
- { authRequiredPrompt?.displayMessage ?? 'none' }
- >
- );
-}
-
-function StartChatButtons() {
- const { startNewChat, startChatWithPrompt } = useChats();
- return (
- <>
-
-
- >
- );
-}
-
-function createPlacementEvent(): AiSessionPlacementUpdatedEvent {
- return {
- sessionId: 'session-1',
- placement: {
- kind: 'site',
- siteId: 'site-2',
- siteName: 'Created Site',
- sitePath: '/sites/created-site',
- },
- };
-}
-
-function createAuthUser(): AuthUser {
- return {
- id: 1,
- email: 'user@example.com',
- displayName: 'Studio User',
- };
-}
diff --git a/apps/ui/src/ui-desks/chats/provider.tsx b/apps/ui/src/ui-desks/chats/provider.tsx
deleted file mode 100644
index 7ad311e3e8..0000000000
--- a/apps/ui/src/ui-desks/chats/provider.tsx
+++ /dev/null
@@ -1,391 +0,0 @@
-import { useNavigate, useSearch } from '@tanstack/react-router';
-import { __, sprintf } from '@wordpress/i18n';
-import { useCallback, useEffect, useRef, useState } from 'react';
-import { useConnector } from '@/data/core';
-import { useAuthUser } from '@/data/queries/use-auth-user';
-import { useCreateSession } from '@/data/queries/use-sessions';
-import {
- Button,
- Dialog,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '../components';
-import {
- ChatsContext,
- type ChatPromptRequest,
- type ComposerWidgetDragPreview,
- type ComposerWidgetAttachmentRequest,
- type ChatsProviderProps,
- type PendingChatPrompt,
-} from './context';
-import { validateChatsSearch, type ChatsSearch } from './search';
-import type { DeskWidget } from '@/ui-desks/widgets/types';
-
-interface PendingPlacementSwitch {
- sessionId: string;
- siteId: string;
- siteName: string;
-}
-
-function useChatsSearch() {
- const search = validateChatsSearch( useSearch( { strict: false } ) as Record< string, unknown > );
- const navigate = useNavigate();
- const open = search.chats === true;
- const createChatRequestId = search.newChat ?? 0;
- const routeSessionId = search.session;
-
- const setOpen = useCallback(
- ( nextOpen: boolean ) => {
- void navigate( {
- to: '.',
- search: ( previous: ChatsSearch ) => ( {
- ...previous,
- chats: nextOpen ? true : undefined,
- session: nextOpen ? previous.session : undefined,
- } ),
- } );
- },
- [ navigate ]
- );
-
- return { open, setOpen, createChatRequestId, routeSessionId, navigate };
-}
-
-function createPendingPromptId() {
- return `chat-${ Date.now().toString( 36 ) }-${ Math.random().toString( 36 ).slice( 2, 8 ) }`;
-}
-
-function createComposerWidgetAttachmentRequestId() {
- return `widgets-${ Date.now().toString( 36 ) }-${ Math.random().toString( 36 ).slice( 2, 8 ) }`;
-}
-
-export function ChatsProvider( { siteId, children }: ChatsProviderProps ) {
- const { open, setOpen, createChatRequestId, routeSessionId, navigate } = useChatsSearch();
- const connector = useConnector();
- const createSession = useCreateSession();
- const { data: authUser, refetch: refetchAuthUser } = useAuthUser();
- const lastCreateChatRequestId = useRef( createChatRequestId );
- const [ selectedSessionId, setSelectedSessionId ] = useState< string | undefined >( undefined );
- const [ expanded, setExpanded ] = useState( false );
- const [ autoFocusSessionId, setAutoFocusSessionId ] = useState< string | undefined >( undefined );
- const [ pendingPrompt, setPendingPrompt ] = useState< PendingChatPrompt | undefined >(
- undefined
- );
- const [ authRequiredPrompt, setAuthRequiredPrompt ] = useState< ChatPromptRequest | undefined >(
- undefined
- );
- const [ composerWidgetAttachmentRequest, setComposerWidgetAttachmentRequest ] = useState<
- ComposerWidgetAttachmentRequest | undefined
- >( undefined );
- const [ composerWidgetDragPreview, setComposerWidgetDragPreview ] = useState<
- ComposerWidgetDragPreview | undefined
- >( undefined );
- const [ isComposerWidgetDragTarget, setComposerWidgetDragTarget ] = useState( false );
- const [ pendingPlacementSwitch, setPendingPlacementSwitch ] = useState<
- PendingPlacementSwitch | undefined
- >( undefined );
-
- const setRouteSession = useCallback(
- ( sessionId: string | undefined ) => {
- void navigate( {
- to: '.',
- search: ( previous: ChatsSearch ) => ( {
- ...previous,
- chats: sessionId ? true : previous.chats,
- session: sessionId,
- } ),
- } );
- },
- [ navigate ]
- );
-
- const selectSession = useCallback(
- ( sessionId: string ) => {
- setSelectedSessionId( sessionId );
- setExpanded( true );
- setAutoFocusSessionId( undefined );
- setAuthRequiredPrompt( undefined );
- setRouteSession( sessionId );
- },
- [ setRouteSession ]
- );
-
- const switchSession = useCallback(
- ( sessionId: string ) => {
- setSelectedSessionId( sessionId );
- setExpanded( true );
- setAutoFocusSessionId( sessionId );
- setAuthRequiredPrompt( undefined );
- setRouteSession( sessionId );
- },
- [ setRouteSession ]
- );
-
- const clearSelection = useCallback( () => {
- setSelectedSessionId( undefined );
- setExpanded( false );
- setAutoFocusSessionId( undefined );
- setAuthRequiredPrompt( undefined );
- setRouteSession( undefined );
- }, [ setRouteSession ] );
-
- const closeChatSidebar = useCallback( () => {
- setSelectedSessionId( undefined );
- setExpanded( false );
- setAutoFocusSessionId( undefined );
- setPendingPlacementSwitch( undefined );
- setAuthRequiredPrompt( undefined );
- void navigate( {
- to: '.',
- search: ( previous: ChatsSearch ) => ( {
- ...previous,
- chats: undefined,
- session: undefined,
- } ),
- } );
- }, [ navigate ] );
-
- const showAuthRequired = useCallback(
- ( prompt?: ChatPromptRequest ) => {
- setSelectedSessionId( undefined );
- setExpanded( true );
- setAutoFocusSessionId( undefined );
- setAuthRequiredPrompt( prompt );
- void navigate( {
- to: '.',
- search: ( previous: ChatsSearch ) => ( {
- ...previous,
- chats: true,
- session: undefined,
- } ),
- } );
- },
- [ navigate ]
- );
-
- const ensureAuthenticatedForChat = useCallback( async () => {
- if ( authUser ) {
- return true;
- }
-
- const result = await refetchAuthUser();
- return !! result.data;
- }, [ authUser, refetchAuthUser ] );
-
- const startNewChat = useCallback( async () => {
- if ( ! ( await ensureAuthenticatedForChat() ) ) {
- showAuthRequired();
- return;
- }
-
- setAuthRequiredPrompt( undefined );
- const session = await createSession.mutateAsync( siteId );
- setSelectedSessionId( session.id );
- setExpanded( true );
- setAutoFocusSessionId( session.id );
- setRouteSession( session.id );
- setOpen( true );
- }, [
- createSession,
- ensureAuthenticatedForChat,
- setOpen,
- setRouteSession,
- showAuthRequired,
- siteId,
- ] );
-
- const startChatWithPrompt = useCallback(
- async ( request: ChatPromptRequest ) => {
- if ( ! ( await ensureAuthenticatedForChat() ) ) {
- showAuthRequired( request );
- return '';
- }
-
- setAuthRequiredPrompt( undefined );
- const session = await createSession.mutateAsync( siteId );
- setSelectedSessionId( session.id );
- setExpanded( true );
- setAutoFocusSessionId( undefined );
- setPendingPrompt( {
- id: createPendingPromptId(),
- sessionId: session.id,
- prompt: request.prompt,
- displayMessage: request.displayMessage ?? request.prompt,
- } );
- setRouteSession( session.id );
- setOpen( true );
- return session.id;
- },
- [
- createSession,
- ensureAuthenticatedForChat,
- setOpen,
- setRouteSession,
- showAuthRequired,
- siteId,
- ]
- );
-
- const consumePendingPrompt = useCallback( ( promptId: string ) => {
- setPendingPrompt( ( current ) => ( current?.id === promptId ? undefined : current ) );
- }, [] );
-
- const attachWidgetsToComposer = useCallback(
- ( widgets: DeskWidget[] ) => {
- if ( ! selectedSessionId || widgets.length === 0 ) {
- return;
- }
-
- setComposerWidgetAttachmentRequest( {
- id: createComposerWidgetAttachmentRequestId(),
- sessionId: selectedSessionId,
- widgets,
- } );
- },
- [ selectedSessionId ]
- );
-
- const consumeComposerWidgetAttachmentRequest = useCallback( ( requestId: string ) => {
- setComposerWidgetAttachmentRequest( ( current ) =>
- current?.id === requestId ? undefined : current
- );
- }, [] );
-
- useEffect( () => {
- if ( createChatRequestId === lastCreateChatRequestId.current ) {
- return;
- }
-
- lastCreateChatRequestId.current = createChatRequestId;
- void startNewChat();
- }, [ createChatRequestId, startNewChat ] );
-
- useEffect( () => {
- if ( ! routeSessionId ) {
- return;
- }
- setSelectedSessionId( routeSessionId );
- setExpanded( true );
- setAutoFocusSessionId( undefined );
- setOpen( true );
- }, [ routeSessionId, setOpen ] );
-
- useEffect( () => {
- return connector.onSessionPlacementUpdated( ( event ) => {
- if ( event.sessionId !== selectedSessionId ) {
- return;
- }
- if ( event.placement.siteId === siteId ) {
- return;
- }
- setPendingPlacementSwitch( {
- sessionId: event.sessionId,
- siteId: event.placement.siteId,
- siteName: event.placement.siteName,
- } );
- } );
- }, [ connector, selectedSessionId, siteId ] );
-
- useEffect( () => {
- if ( pendingPlacementSwitch?.siteId === siteId ) {
- setPendingPlacementSwitch( undefined );
- }
- }, [ pendingPlacementSwitch?.siteId, siteId ] );
-
- useEffect( () => {
- if (
- pendingPlacementSwitch &&
- selectedSessionId &&
- pendingPlacementSwitch.sessionId !== selectedSessionId
- ) {
- setPendingPlacementSwitch( undefined );
- }
- }, [ pendingPlacementSwitch, selectedSessionId ] );
-
- const confirmPlacementSwitch = useCallback( () => {
- if ( ! pendingPlacementSwitch ) {
- return;
- }
- setPendingPlacementSwitch( undefined );
- void navigate( {
- to: '/sites/$siteId',
- params: { siteId: pendingPlacementSwitch.siteId },
- search: ( previous: ChatsSearch ) => ( {
- ...previous,
- chats: true,
- session: pendingPlacementSwitch.sessionId,
- } ),
- } );
- }, [ navigate, pendingPlacementSwitch ] );
-
- return (
-
- { children }
- { pendingPlacementSwitch && (
-
- ) }
-
- );
-}
diff --git a/apps/ui/src/ui-desks/chats/queued-prompts/index.tsx b/apps/ui/src/ui-desks/chats/queued-prompts/index.tsx
deleted file mode 100644
index b51d0c3708..0000000000
--- a/apps/ui/src/ui-desks/chats/queued-prompts/index.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { __ } from '@wordpress/i18n';
-import { closeSmall } from '@wordpress/icons';
-import { Button } from '@/ui-desks/components';
-import styles from './style.module.css';
-import type { QueuedPrompt } from '@/data/queries/use-agent-run';
-
-interface QueuedPromptsProps {
- prompts: QueuedPrompt[];
- onRemove: ( id: string ) => void;
-}
-
-export function QueuedPrompts( { prompts, onRemove }: QueuedPromptsProps ) {
- if ( prompts.length === 0 ) {
- return null;
- }
- return (
-
- { prompts.map( ( item ) => (
-
- { item.displayMessage ?? item.prompt }
-
- ) ) }
-
- );
-}
diff --git a/apps/ui/src/ui-desks/chats/queued-prompts/style.module.css b/apps/ui/src/ui-desks/chats/queued-prompts/style.module.css
deleted file mode 100644
index ffe382d034..0000000000
--- a/apps/ui/src/ui-desks/chats/queued-prompts/style.module.css
+++ /dev/null
@@ -1,37 +0,0 @@
-.root {
- display: flex;
- flex-direction: column;
- gap: var(--wpds-dimension-padding-sm);
- margin-bottom: var(--wpds-dimension-padding-md);
-}
-
-.item {
- position: relative;
- align-self: flex-end;
- display: flex;
- align-items: flex-start;
- gap: var(--wpds-dimension-padding-sm);
- padding: var(--wpds-dimension-padding-md) var(--wpds-dimension-padding-lg);
- /* Match the user bubble shape but lighter so staged follow-ups read as
- "drafted, not sent". Mirrors `.userText` (right-aligned, bottom-right
- tight corner). */
- background-color: color-mix(in srgb, var(--wpds-color-fg-interactive-brand, #2563eb) 4%, transparent);
- border: 1px dashed color-mix(in srgb, var(--wpds-color-fg-interactive-brand, #2563eb) 30%, transparent);
- border-radius: 18px 18px var(--wpds-border-radius-md, 8px) 18px;
- max-width: 80%;
- font-size: var(--wpds-typography-font-size-md);
- line-height: 1.55;
- color: var(--wpds-color-fg-content-neutral-weak);
-}
-
-.text {
- flex: 1;
- min-width: 0;
- white-space: pre-wrap;
- word-wrap: break-word;
-}
-
-.remove {
- flex-shrink: 0;
- margin: -2px -6px -2px 0;
-}
diff --git a/apps/ui/src/ui-desks/chats/search.ts b/apps/ui/src/ui-desks/chats/search.ts
deleted file mode 100644
index f754df2865..0000000000
--- a/apps/ui/src/ui-desks/chats/search.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-export interface ChatsSearch {
- chats?: boolean;
- newChat?: number;
- session?: string;
-}
-
-function parseChatsSearch( value: unknown ) {
- return value === true || value === 'true' || value === '1' || value === 'open';
-}
-
-function parseNewChatSearch( value: unknown ) {
- const parsed = typeof value === 'number' ? value : Number( value );
- return Number.isFinite( parsed ) && parsed > 0 ? parsed : undefined;
-}
-
-export function validateChatsSearch( search: Record< string, unknown > ): ChatsSearch {
- return {
- chats: parseChatsSearch( search.chats ) || undefined,
- newChat: parseNewChatSearch( search.newChat ),
- session: typeof search.session === 'string' && search.session ? search.session : undefined,
- };
-}
diff --git a/apps/ui/src/ui-desks/chats/selection-chat-dialog/index.tsx b/apps/ui/src/ui-desks/chats/selection-chat-dialog/index.tsx
deleted file mode 100644
index 8467d399fe..0000000000
--- a/apps/ui/src/ui-desks/chats/selection-chat-dialog/index.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-import { __ } from '@wordpress/i18n';
-import { arrowUp } from '@wordpress/icons';
-import { useEffect, useRef, useState } from 'react';
-import { useChats } from '@/ui-desks/chats/context';
-import {
- buildWidgetContextDisplayMessage,
- buildWidgetContextPrompt,
- WidgetContextThumbnailList,
-} from '@/ui-desks/chats/widget-context';
-import {
- Button,
- Dialog,
- DialogError,
- DialogRow,
- dialogInputClassName,
-} from '@/ui-desks/components';
-import type { DeskWidget } from '@/ui-desks/widgets/types';
-
-interface SelectionChatDialogProps {
- widgets: DeskWidget[];
- onClose: () => void;
-}
-
-export function SelectionChatDialog( { widgets, onClose }: SelectionChatDialogProps ) {
- const { startChatWithPrompt, isCreatingChat } = useChats();
- const textareaRef = useRef< HTMLTextAreaElement | null >( null );
- const [ prompt, setPrompt ] = useState( '' );
- const [ error, setError ] = useState< string | null >( null );
- const canSubmit = prompt.trim().length > 0 && ! isCreatingChat;
-
- useEffect( () => {
- textareaRef.current?.focus();
- }, [] );
-
- const submitPrompt = async () => {
- const userPrompt = prompt.trim();
- if ( ! userPrompt || isCreatingChat ) {
- return;
- }
-
- setError( null );
- try {
- await startChatWithPrompt( {
- prompt: buildWidgetContextPrompt( userPrompt, widgets ),
- displayMessage: buildWidgetContextDisplayMessage( userPrompt, widgets ),
- } );
- onClose();
- } catch ( submitError ) {
- setError(
- submitError instanceof Error ? submitError.message : __( 'Unable to start chat.' )
- );
- }
- };
-
- return (
-
- );
-}
diff --git a/apps/ui/src/ui-desks/chats/session-surface/empty-state.test.ts b/apps/ui/src/ui-desks/chats/session-surface/empty-state.test.ts
deleted file mode 100644
index 9566201c7b..0000000000
--- a/apps/ui/src/ui-desks/chats/session-surface/empty-state.test.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { describe, expect, it } from 'vitest';
-import { hasVisibleUserPrompt, shouldShowEmptyConversation } from './empty-state';
-import type { SessionEntry } from '@earendil-works/pi-coding-agent';
-
-describe( 'desks chat empty state', () => {
- it( 'detects user-visible prompt entries', () => {
- expect(
- hasVisibleUserPrompt( [
- {
- type: 'custom',
- customType: 'studio.user_prompt',
- timestamp: '2026-05-21T10:00:00.000Z',
- data: { source: 'prompt', text: 'Create a home page' },
- } as SessionEntry,
- ] )
- ).toBe( true );
- } );
-
- it( 'ignores ask-user answers because they are not rendered as user prompts', () => {
- expect(
- hasVisibleUserPrompt( [
- {
- type: 'custom',
- customType: 'studio.user_prompt',
- timestamp: '2026-05-21T10:00:00.000Z',
- data: { source: 'ask_user', text: 'Yes, continue' },
- } as SessionEntry,
- ] )
- ).toBe( false );
- } );
-
- it( 'hides suggestions once a prompt is submitted before entries update', () => {
- expect(
- shouldShowEmptyConversation( {
- hasVisibleUserPrompt: false,
- hasSubmittedPrompt: true,
- hasPendingInitialPrompt: false,
- hasActiveRun: false,
- queuedPromptCount: 0,
- } )
- ).toBe( false );
- } );
-
- it( 'hides suggestions for pending initial prompts and queued prompts', () => {
- expect(
- shouldShowEmptyConversation( {
- hasVisibleUserPrompt: false,
- hasSubmittedPrompt: false,
- hasPendingInitialPrompt: true,
- hasActiveRun: false,
- queuedPromptCount: 0,
- } )
- ).toBe( false );
-
- expect(
- shouldShowEmptyConversation( {
- hasVisibleUserPrompt: false,
- hasSubmittedPrompt: false,
- hasPendingInitialPrompt: false,
- hasActiveRun: false,
- queuedPromptCount: 1,
- } )
- ).toBe( false );
- } );
-} );
diff --git a/apps/ui/src/ui-desks/chats/session-surface/empty-state.ts b/apps/ui/src/ui-desks/chats/session-surface/empty-state.ts
deleted file mode 100644
index 002a75fdef..0000000000
--- a/apps/ui/src/ui-desks/chats/session-surface/empty-state.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import {
- isStudioCustomEntryOfType,
- type StudioCustomEntry,
-} from '@studio/common/ai/sessions/entry-types';
-import type { SessionEntry } from '@earendil-works/pi-coding-agent';
-
-interface EmptyConversationState {
- hasVisibleUserPrompt: boolean;
- hasSubmittedPrompt: boolean;
- hasPendingInitialPrompt: boolean;
- hasActiveRun: boolean;
- queuedPromptCount: number;
-}
-
-export function hasVisibleUserPrompt( entries: SessionEntry[] | undefined ): boolean {
- return ( entries ?? [] ).some( ( entry ) => {
- if ( ! isStudioCustomEntryOfType( entry, 'studio.user_prompt' ) ) {
- return false;
- }
- const data = ( entry as StudioCustomEntry< 'studio.user_prompt' > ).data;
- return data?.source === 'prompt' && typeof data.text === 'string' && data.text.trim() !== '';
- } );
-}
-
-export function shouldShowEmptyConversation( {
- hasVisibleUserPrompt,
- hasSubmittedPrompt,
- hasPendingInitialPrompt,
- hasActiveRun,
- queuedPromptCount,
-}: EmptyConversationState ): boolean {
- return (
- ! hasVisibleUserPrompt &&
- ! hasSubmittedPrompt &&
- ! hasPendingInitialPrompt &&
- ! hasActiveRun &&
- queuedPromptCount === 0
- );
-}
diff --git a/apps/ui/src/ui-desks/chats/session-surface/index.tsx b/apps/ui/src/ui-desks/chats/session-surface/index.tsx
deleted file mode 100644
index 2056cf5146..0000000000
--- a/apps/ui/src/ui-desks/chats/session-surface/index.tsx
+++ /dev/null
@@ -1,449 +0,0 @@
-import { resolveSessionModel } from '@studio/common/ai/models';
-import { __ } from '@wordpress/i18n';
-import { box, chevronLeft, chevronRight, previous, starEmpty, starFilled } from '@wordpress/icons';
-import { clsx } from 'clsx';
-import {
- useCallback,
- useEffect,
- useLayoutEffect,
- useMemo,
- useRef,
- useState,
- type ReactNode,
- type Ref,
-} from 'react';
-import { useAgentRun } from '@/data/queries/use-agent-run';
-import { useAuthUser, useLogin } from '@/data/queries/use-auth-user';
-import { useConnectedWpcomSites } from '@/data/queries/use-connected-wpcom-sites';
-import {
- useSession,
- useSessionEffectiveEnvironment,
- useUpdateSessionMetadata,
-} from '@/data/queries/use-sessions';
-import { useSites } from '@/data/queries/use-sites';
-import { useSessionCommands } from '@/hooks/use-session-commands';
-import { SessionUIProvider } from '@/hooks/use-session-ui';
-import { Button } from '@/ui-desks/components';
-import { Composer, ComposerSkeleton } from '../composer';
-import { pickLiveSite } from '../composer/environment-pill';
-import { Conversation } from '../conversation';
-import { QueuedPrompts } from '../queued-prompts';
-import { hasVisibleUserPrompt, shouldShowEmptyConversation } from './empty-state';
-import styles from './style.module.css';
-import type { PendingChatPrompt } from '../context';
-import type { SendMessageOptions } from '@/data/queries/use-agent-run';
-
-interface SessionSurfaceProps {
- siteId?: string;
- sessionId: string;
- side: 'left' | 'right';
- listCollapsed: boolean;
- onExpandList: () => void;
- onCollapseList: () => void;
- onSwitchSession: ( sessionId: string ) => void;
- autoFocus?: boolean;
- initialPrompt?: PendingChatPrompt;
- onInitialPromptConsumed?: ( promptId: string ) => void;
-}
-
-interface FrameProps {
- header?: ReactNode;
- composer?: ReactNode;
- scrollRef?: Ref< HTMLDivElement >;
- children?: ReactNode;
-}
-
-const chatDateFormatter = new Intl.DateTimeFormat( undefined, {
- month: 'short',
- day: 'numeric',
- year: 'numeric',
-} );
-
-const EXAMPLE_PROMPTS = [
- {
- short: __( 'Pull drafts' ),
- full: __(
- 'Pull my unfinished drafts onto the canvas so I can see what I’ve been working on. Group anything related into stacks, and surface the ones I haven’t touched in a while.'
- ),
- },
- {
- short: __( 'Draft a post' ),
- full: __(
- 'Help me draft a new blog post. Suggest a topic worth writing about right now, sketch an outline, then take a first pass at the opening paragraph so I can iterate on voice and structure.'
- ),
- },
- {
- short: __( 'Create a page' ),
- full: __(
- 'Walk me through creating a new page on my site. Help me plan the sections, draft the headings, and write a first pass at the body copy — then drop the result onto the canvas so I can edit it.'
- ),
- },
- {
- short: __( 'Design help' ),
- full: __(
- 'Take a look at my site’s current design and call out a handful of small improvements I could make — typography, spacing, colour, or layout — and explain why each change would help.'
- ),
- },
- {
- short: __( 'Top posts' ),
- full: __(
- 'Pull my most-viewed posts from the last 30 days onto the canvas so I can see them side by side. Sort by view count and group anything that shares a topic into a stack.'
- ),
- },
- {
- short: __( 'Write follow-up' ),
- full: __(
- 'Suggest a follow-up to my most recent post. Read what I wrote, find a natural next angle, then sketch an outline and draft an opening paragraph so I can pick up where it left off.'
- ),
- },
- {
- short: __( 'Build a plugin' ),
- full: __(
- 'Help me build a small WordPress plugin from scratch. Ask me what problem it should solve, scaffold the plugin folder and main file, then walk me through the hooks and code we need to wire it up.'
- ),
- },
-];
-
-function formatChatDate( value: string ) {
- const timestamp = Date.parse( value );
- if ( Number.isNaN( timestamp ) ) {
- return '';
- }
- return chatDateFormatter.format( new Date( timestamp ) );
-}
-
-function EmptyConversation( {
- authRequired,
- onPreviewPrompt,
- onClearPreview,
- onSelectPrompt,
-}: {
- authRequired: boolean;
- onPreviewPrompt: ( prompt: string ) => void;
- onClearPreview: () => void;
- onSelectPrompt: ( prompt: string ) => void;
-} ) {
- return (
-
-
- { authRequired
- ? __( 'Log in with WordPress.com to start a Studio Desk chat.' )
- : __( 'Ask Studio Desk anything to get started.' ) }
-
- { authRequired ? null : (
-
- { EXAMPLE_PROMPTS.map( ( example ) => (
-
- ) ) }
-
- ) }
-
- );
-}
-
-function Frame( { header, composer, scrollRef, children }: FrameProps ) {
- return (
-
-
- { header }
-
- { children }
-
-
{ composer }
-
-
- );
-}
-
-function SessionSurfaceContent( {
- siteId,
- sessionId,
- side,
- listCollapsed,
- onExpandList,
- onCollapseList,
- onSwitchSession,
- autoFocus = false,
- initialPrompt,
- onInitialPromptConsumed,
-}: SessionSurfaceProps ) {
- const { data, isLoading, error } = useSession( sessionId );
- const { data: authUser, isLoading: isLoadingAuthUser } = useAuthUser();
- const login = useLogin();
- const { data: sites } = useSites();
- const ownerSitePath = data?.summary.ownerSitePath;
- const ownerSite = ownerSitePath
- ? sites?.find( ( candidate ) => candidate.path === ownerSitePath )
- : undefined;
- const ownerSiteId = ownerSite?.id ?? siteId;
- const { data: connectedSites } = useConnectedWpcomSites( ownerSiteId );
- const liveSite = pickLiveSite( connectedSites );
- const effectiveEnvironment = useSessionEffectiveEnvironment( data?.summary, ownerSiteId );
- const {
- isRunning,
- hasActiveRun,
- isInterrupting,
- startedAt,
- error: runError,
- pendingQuestions,
- pendingAnswers,
- queuedPrompts,
- sendMessage,
- interrupt,
- answerQuestion,
- removeQueuedPrompt,
- } = useAgentRun( sessionId );
- const sentInitialPromptIdsRef = useRef< Set< string > >( new Set() );
- const updateSessionMetadata = useUpdateSessionMetadata();
- const currentModel = useMemo(
- () => resolveSessionModel( data?.entries ?? [] ),
- [ data?.entries ]
- );
- const pendingQuestionTexts = useMemo(
- () => new Set( pendingQuestions.map( ( question ) => question.question ) ),
- [ pendingQuestions ]
- );
- const composerBusy = hasActiveRun || pendingQuestions.length > 0;
- const authLoading = isLoadingAuthUser && ! authUser;
- const authRequired = ! isLoadingAuthUser && ! authUser;
- const hasRenderedUserPrompt = useMemo(
- () => hasVisibleUserPrompt( data?.entries ),
- [ data?.entries ]
- );
- const scrollRef = useRef< HTMLDivElement >( null );
- const exampleDraftIdRef = useRef( 0 );
- const [ previewPrompt, setPreviewPrompt ] = useState< string | null >( null );
- const [ exampleDraft, setExampleDraft ] = useState< { id: number; prompt: string } | null >(
- null
- );
- const [ hasSubmittedPrompt, setHasSubmittedPrompt ] = useState( false );
- const hasPendingInitialPrompt = initialPrompt?.sessionId === sessionId;
- const showEmptyConversation = shouldShowEmptyConversation( {
- hasVisibleUserPrompt: hasRenderedUserPrompt,
- hasSubmittedPrompt,
- hasPendingInitialPrompt,
- hasActiveRun,
- queuedPromptCount: queuedPrompts.length,
- } );
- useSessionCommands( sessionId );
-
- useEffect( () => {
- setHasSubmittedPrompt( false );
- }, [ sessionId ] );
-
- const sendMessageAndHideSuggestions = useCallback(
- async ( prompt: string, options?: SendMessageOptions ) => {
- setHasSubmittedPrompt( true );
- await sendMessage( prompt, options );
- },
- [ sendMessage ]
- );
-
- const toggleStar = () => {
- void updateSessionMetadata.mutateAsync( {
- sessionId,
- patch: { starred: ! data?.summary.starred },
- } );
- };
-
- const archiveConversation = () => {
- void updateSessionMetadata.mutateAsync( {
- sessionId,
- patch: { archived: true },
- } );
- };
-
- useEffect( () => {
- if ( ! data || ! initialPrompt || initialPrompt.sessionId !== sessionId ) {
- return;
- }
- if ( ! authUser ) {
- return;
- }
- if ( sentInitialPromptIdsRef.current.has( initialPrompt.id ) ) {
- return;
- }
-
- sentInitialPromptIdsRef.current.add( initialPrompt.id );
- void sendMessageAndHideSuggestions( initialPrompt.prompt, {
- displayMessage: initialPrompt.displayMessage,
- } )
- .then( () => onInitialPromptConsumed?.( initialPrompt.id ) )
- .catch( () => {
- sentInitialPromptIdsRef.current.delete( initialPrompt.id );
- } );
- }, [
- authUser,
- data,
- initialPrompt,
- onInitialPromptConsumed,
- sendMessageAndHideSuggestions,
- sessionId,
- ] );
-
- useLayoutEffect( () => {
- const node = scrollRef.current;
- if ( ! node ) {
- return;
- }
- node.scrollTop = node.scrollHeight;
- const id = requestAnimationFrame( () => {
- node.scrollTop = node.scrollHeight;
- } );
- return () => cancelAnimationFrame( id );
- }, [ sessionId, data, isRunning, queuedPrompts.length ] );
-
- if ( isLoading ) {
- return (
-
-
-
- }
- />
- );
- }
-
- if ( error || ! data ) {
- return (
-
-
{ __( 'Session not found' ) }
-
{ sessionId }
-
- );
- }
-
- return (
-
-
- { listCollapsed ? (
-
- ) : (
-
- ) }
-
-
- { formatChatDate( data.summary.createdAt ) }
-
-
-
-
-
-
- }
- scrollRef={ scrollRef }
- composer={
-
-
- login.mutate() }
- model={ currentModel }
- onSend={ sendMessageAndHideSuggestions }
- onInterrupt={ interrupt }
- sessionId={ sessionId }
- effectiveEnvironment={ effectiveEnvironment }
- liveSite={ liveSite }
- entries={ data.entries }
- ownerSiteId={ ownerSiteId }
- onSwitchSession={ onSwitchSession }
- autoFocus={ autoFocus }
- previewPrompt={ previewPrompt }
- draftPrompt={ exampleDraft }
- />
-
- }
- >
- { showEmptyConversation ? (
- setPreviewPrompt( null ) }
- onSelectPrompt={ ( prompt ) => {
- exampleDraftIdRef.current += 1;
- setExampleDraft( { id: exampleDraftIdRef.current, prompt } );
- } }
- />
- ) : null }
-
-
-
-
- );
-}
-
-export function SessionSurface( props: SessionSurfaceProps ) {
- return (
-
-
-
- );
-}
diff --git a/apps/ui/src/ui-desks/chats/session-surface/style.module.css b/apps/ui/src/ui-desks/chats/session-surface/style.module.css
deleted file mode 100644
index 7d06871d94..0000000000
--- a/apps/ui/src/ui-desks/chats/session-surface/style.module.css
+++ /dev/null
@@ -1,152 +0,0 @@
-.root {
- display: flex;
- flex-direction: row;
- height: 100%;
- min-height: 0;
-}
-
-.chatColumn {
- display: flex;
- flex-direction: column;
- flex: 1;
- min-width: 0;
- min-height: 0;
-}
-
-.state {
- padding: var(--desk-chats-session-padding, 14px);
- color: var(--ui-desks-muted, #6b7280);
- font-size: 13px;
- line-height: 18px;
-}
-
-.scroll {
- flex: 1;
- min-height: 0;
- overflow-y: auto;
- position: relative;
- scrollbar-gutter: stable both-edges;
-}
-
-.composerOuter {
- flex: 0 0 auto;
-}
-
-.sessionSurface {
- box-sizing: border-box;
- width: 100%;
- min-width: 0;
- height: 100%;
- padding: var(--desk-chats-session-padding);
-}
-
-.emptyConversation {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 16px;
- margin-top: 40px;
- padding: 0 12px;
-}
-
-.emptyConversationPrompt {
- color: var(--ui-desks-muted, #6b7280);
- font-size: 13px;
- text-align: center;
-}
-
-.emptyConversationExamples {
- display: flex;
- flex-wrap: wrap;
- justify-content: center;
- gap: 8px;
-}
-
-.emptyConversationExample {
- --button-background: rgba(255, 255, 255, 0.6);
- --button-foreground: var(--ui-desks-text, #14171a);
-
- border: 1px solid var(--ui-desks-divider, rgba(15, 23, 42, 0.06));
- font: inherit;
- font-size: 12px;
- line-height: 16px;
- padding: 8px 12px;
- border-radius: 10px;
- cursor: pointer;
- transition: background 140ms ease, border-color 140ms ease;
-}
-
-.emptyConversationExample:hover,
-.emptyConversationExample:focus-visible {
- --button-background: #fff;
-
- border-color: rgba(15, 23, 42, 0.16);
-}
-
-.conversationHeader {
- display: grid;
- grid-template-columns: 1fr auto 1fr;
- align-items: center;
- padding: 0 0 var(--desk-chats-session-padding);
- min-height: 32px;
- flex: 0 0 auto;
-}
-
-.conversationHeaderSlot {
- display: flex;
- align-items: center;
- justify-self: start;
- min-height: 32px;
-}
-
-.conversationDate {
- grid-column: 2;
- text-align: center;
- font-size: 12px;
- color: var(--ui-desks-muted, #6b7280);
-}
-
-.conversationActions {
- grid-column: 3;
- justify-self: end;
- display: inline-flex;
- align-items: center;
- gap: 2px;
-}
-
-.conversationAction,
-.conversationBackButton,
-.conversationCollapseButton {
- color: var(--ui-desks-muted, #6b7280);
- -webkit-app-region: no-drag;
-}
-
-.conversationAction,
-.conversationCollapseButton {
- transition: opacity 120ms ease;
-}
-
-.conversationBackButton {
- gap: 2px;
- padding: 4px 8px 4px 4px;
- font-size: 12px;
-}
-
-.conversationAction,
-.conversationCollapseButton {
- opacity: 0;
-}
-
-.conversationHeader:hover .conversationAction,
-.conversationHeader:hover .conversationCollapseButton,
-.conversationHeader:focus-within .conversationAction,
-.conversationHeader:focus-within .conversationCollapseButton {
- opacity: 1;
-}
-
-.conversationAction[data-active='true'],
-.conversationAction[data-active='true']:hover:not(:disabled),
-.conversationAction[data-active='true']:focus-visible {
- opacity: 1;
- color: #f5b400;
-}
diff --git a/apps/ui/src/ui-desks/chats/thinking-indicator/index.test.tsx b/apps/ui/src/ui-desks/chats/thinking-indicator/index.test.tsx
deleted file mode 100644
index b853b77424..0000000000
--- a/apps/ui/src/ui-desks/chats/thinking-indicator/index.test.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import { act, render, screen } from '@testing-library/react';
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-import { ThinkingIndicator } from './index';
-
-const thinkingMocks = vi.hoisted( () => ( {
- randomThinkingMessage: vi.fn(),
-} ) );
-
-vi.mock( '@studio/common/ai/thinking-messages', () => ( {
- randomThinkingMessage: thinkingMocks.randomThinkingMessage,
-} ) );
-
-describe( 'desks ThinkingIndicator', () => {
- beforeEach( () => {
- vi.useFakeTimers();
- vi.setSystemTime( new Date( '2026-01-01T00:00:00Z' ) );
- thinkingMocks.randomThinkingMessage.mockReset();
- } );
-
- afterEach( () => {
- vi.useRealTimers();
- } );
-
- it( 'keeps the same thinking message while elapsed time updates', () => {
- thinkingMocks.randomThinkingMessage.mockReturnValueOnce( 'Thinking once' );
- thinkingMocks.randomThinkingMessage.mockReturnValue( 'Rotated message' );
-
- render(
-
- );
-
- expect( screen.getByText( 'Thinking once' ) ).toBeVisible();
-
- act( () => {
- vi.advanceTimersByTime( 4000 );
- } );
-
- expect( screen.getByText( 'Thinking once' ) ).toBeVisible();
- expect( screen.queryByText( 'Rotated message' ) ).not.toBeInTheDocument();
- expect( thinkingMocks.randomThinkingMessage ).toHaveBeenCalledTimes( 1 );
- } );
-
- it( 'refreshes the thinking message when the active run step changes', () => {
- thinkingMocks.randomThinkingMessage
- .mockReturnValueOnce( 'Initial thinking' )
- .mockReturnValueOnce( 'Tool thinking' );
- const startedAt = Date.now();
-
- const { rerender } = render(
-
- );
-
- rerender(
-
- );
-
- expect( screen.getByText( 'Tool thinking' ) ).toBeVisible();
- expect( thinkingMocks.randomThinkingMessage ).toHaveBeenCalledTimes( 2 );
- } );
-} );
diff --git a/apps/ui/src/ui-desks/chats/thinking-indicator/index.tsx b/apps/ui/src/ui-desks/chats/thinking-indicator/index.tsx
deleted file mode 100644
index 23831753de..0000000000
--- a/apps/ui/src/ui-desks/chats/thinking-indicator/index.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import { randomThinkingMessage } from '@studio/common/ai/thinking-messages';
-import { useEffect, useRef, useState } from 'react';
-import styles from './style.module.css';
-
-export function ThinkingIndicator( {
- active,
- startedAt,
- messageKey,
- progressMessage,
-}: {
- active: boolean;
- startedAt: number | null;
- messageKey?: string | null;
- progressMessage: string | null;
-} ) {
- const rotationKey = active && startedAt !== null ? `${ startedAt }:${ messageKey ?? '' }` : null;
- const [ message, setMessage ] = useState( () =>
- rotationKey === null ? '' : randomThinkingMessage()
- );
- const [ elapsedSeconds, setElapsedSeconds ] = useState( 0 );
- const previousRotationKey = useRef( rotationKey );
-
- useEffect( () => {
- if ( rotationKey === null || startedAt === null ) {
- previousRotationKey.current = rotationKey;
- setElapsedSeconds( 0 );
- return;
- }
- if ( previousRotationKey.current !== rotationKey ) {
- previousRotationKey.current = rotationKey;
- setMessage( randomThinkingMessage() );
- }
- setElapsedSeconds( Math.floor( ( Date.now() - startedAt ) / 1000 ) );
- const tickInterval = window.setInterval( () => {
- setElapsedSeconds( Math.floor( ( Date.now() - startedAt ) / 1000 ) );
- }, 1000 );
- return () => {
- window.clearInterval( tickInterval );
- };
- }, [ rotationKey, startedAt ] );
-
- return (
-
- { active ? (
- <>
-
-
- { message }
- { elapsedSeconds > 0 ? (
- { `${ elapsedSeconds }s` }
- ) : null }
-
- { progressMessage ? (
-
{ progressMessage }
- ) : null }
- >
- ) : null }
-
- );
-}
diff --git a/apps/ui/src/ui-desks/chats/thinking-indicator/style.module.css b/apps/ui/src/ui-desks/chats/thinking-indicator/style.module.css
deleted file mode 100644
index 64aa3acacb..0000000000
--- a/apps/ui/src/ui-desks/chats/thinking-indicator/style.module.css
+++ /dev/null
@@ -1,56 +0,0 @@
-.root {
- display: flex;
- flex-direction: column;
- gap: 2px;
- color: var(--wpds-color-fg-content-neutral-weak);
- font-size: var(--wpds-typography-font-size-sm);
- /* Reserves space so mounting / unmounting the inner content doesn't
- shift the surrounding layout. Holds ~one head line + one progress line. */
- min-height: calc(1.5em * 2 + 2px);
-}
-
-.head {
- display: flex;
- align-items: center;
- gap: var(--wpds-dimension-padding-md);
-}
-
-.dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background-color: var(--wpds-color-fg-interactive-brand, #2563eb);
- animation: pulse 1.2s ease-in-out infinite;
- flex-shrink: 0;
-}
-
-.label {
- font-style: italic;
-}
-
-.elapsed {
- color: var(--wpds-color-fg-content-neutral-weak);
- opacity: 0.7;
- font-variant-numeric: tabular-nums;
-}
-
-.progress {
- padding-left: calc(8px + var(--wpds-dimension-padding-md));
- color: var(--wpds-color-fg-content-neutral-weak);
- opacity: 0.8;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-@keyframes pulse {
- 0%,
- 100% {
- opacity: 0.35;
- transform: scale(0.85);
- }
- 50% {
- opacity: 1;
- transform: scale(1);
- }
-}
diff --git a/apps/ui/src/ui-desks/chats/use-chat-panel-resize.test.ts b/apps/ui/src/ui-desks/chats/use-chat-panel-resize.test.ts
deleted file mode 100644
index 625960000e..0000000000
--- a/apps/ui/src/ui-desks/chats/use-chat-panel-resize.test.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { describe, expect, it } from 'vitest';
-import { CHAT_PANEL_LIST_WIDTH, getChatPanelShift } from '@/ui-desks/chats/use-chat-panel-resize';
-
-describe( 'getChatPanelShift', () => {
- it( 'does not shift the toolbar when the chat panel is closed', () => {
- expect(
- getChatPanelShift( {
- open: false,
- expanded: true,
- side: 'right',
- width: 760,
- } )
- ).toBe( 0 );
- } );
-
- it( 'centers around a left-side list-only chat panel', () => {
- expect(
- getChatPanelShift( {
- open: true,
- expanded: false,
- side: 'left',
- width: 760,
- } )
- ).toBe( CHAT_PANEL_LIST_WIDTH / 2 );
- } );
-
- it( 'centers around a right-side expanded chat panel', () => {
- expect(
- getChatPanelShift( {
- open: true,
- expanded: true,
- side: 'right',
- width: 640,
- } )
- ).toBe( -320 );
- } );
-} );
diff --git a/apps/ui/src/ui-desks/chats/use-chat-panel-resize.ts b/apps/ui/src/ui-desks/chats/use-chat-panel-resize.ts
deleted file mode 100644
index 87b3420631..0000000000
--- a/apps/ui/src/ui-desks/chats/use-chat-panel-resize.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-import { useCallback, useEffect, useState } from 'react';
-import type { PointerEvent as ReactPointerEvent } from 'react';
-
-export type ChatPanelSide = 'left' | 'right';
-
-export const CHAT_PANEL_EXPANDED_WIDTH = 760;
-export const CHAT_PANEL_COLLAPSED_WIDTH = 560;
-export const CHAT_PANEL_COLLAPSE_THRESHOLD = CHAT_PANEL_EXPANDED_WIDTH;
-export const CHAT_PANEL_MIN_WIDTH = 320;
-export const CHAT_PANEL_MAX_WIDTH = 1200;
-export const CHAT_PANEL_LIST_WIDTH = 300;
-
-const CHAT_PANEL_STORAGE_KEY = 'ui-desks-chat-panel-width';
-const CHAT_PANEL_VIEWPORT_MARGIN = 36;
-
-export interface ChatPanelResizeState {
- width: number;
- isResizing: boolean;
- listCollapsed: boolean;
- collapseList: () => void;
- expandList: () => void;
- startResize: ( event: ReactPointerEvent< HTMLDivElement > ) => void;
-}
-
-function getViewportWidth() {
- return typeof window === 'undefined' ? CHAT_PANEL_MAX_WIDTH : window.innerWidth;
-}
-
-export function clampChatPanelWidth( width: number, viewportWidth = getViewportWidth() ) {
- return Math.max(
- CHAT_PANEL_MIN_WIDTH,
- Math.min( width, CHAT_PANEL_MAX_WIDTH, viewportWidth - CHAT_PANEL_VIEWPORT_MARGIN )
- );
-}
-
-function readStoredChatPanelWidth() {
- if ( typeof window === 'undefined' ) {
- return CHAT_PANEL_EXPANDED_WIDTH;
- }
-
- const stored = window.localStorage.getItem( CHAT_PANEL_STORAGE_KEY );
- const parsed = stored ? Number.parseInt( stored, 10 ) : Number.NaN;
- return Number.isFinite( parsed ) ? parsed : CHAT_PANEL_EXPANDED_WIDTH;
-}
-
-function persistChatPanelWidth( width: number ) {
- if ( typeof window === 'undefined' ) {
- return;
- }
-
- window.localStorage.setItem( CHAT_PANEL_STORAGE_KEY, String( width ) );
-}
-
-export function getChatPanelShift( {
- open,
- expanded,
- side,
- width,
-}: {
- open: boolean;
- expanded: boolean;
- side: ChatPanelSide;
- width: number;
-} ) {
- if ( ! open ) {
- return 0;
- }
-
- const visibleWidth = expanded ? width : CHAT_PANEL_LIST_WIDTH;
- return ( side === 'right' ? -1 : 1 ) * ( visibleWidth / 2 );
-}
-
-export function useChatPanelResize( side: ChatPanelSide ): ChatPanelResizeState {
- const [ width, setWidth ] = useState( () =>
- clampChatPanelWidth( readStoredChatPanelWidth(), getViewportWidth() )
- );
- const [ isResizing, setIsResizing ] = useState( false );
-
- const updateWidth = useCallback( ( nextWidth: number ) => {
- const clamped = clampChatPanelWidth( nextWidth, getViewportWidth() );
- setWidth( clamped );
- persistChatPanelWidth( clamped );
- }, [] );
-
- const collapseList = useCallback( () => {
- updateWidth( CHAT_PANEL_COLLAPSED_WIDTH );
- }, [ updateWidth ] );
-
- const expandList = useCallback( () => {
- updateWidth( CHAT_PANEL_EXPANDED_WIDTH );
- }, [ updateWidth ] );
-
- useEffect( () => {
- const onResize = () => {
- setWidth( ( current ) => {
- const clamped = clampChatPanelWidth( current, getViewportWidth() );
- persistChatPanelWidth( clamped );
- return clamped;
- } );
- };
-
- window.addEventListener( 'resize', onResize );
- return () => window.removeEventListener( 'resize', onResize );
- }, [] );
-
- const startResize = useCallback(
- ( event: ReactPointerEvent< HTMLDivElement > ) => {
- event.preventDefault();
-
- const dragOriginX = event.clientX;
- const startWidth = width;
- const direction = side === 'left' ? 1 : -1;
-
- setIsResizing( true );
-
- const onPointerMove = ( pointerEvent: PointerEvent ) => {
- const delta = ( pointerEvent.clientX - dragOriginX ) * direction;
- updateWidth( startWidth + delta );
- };
-
- const onPointerUp = () => {
- setIsResizing( false );
- window.removeEventListener( 'pointermove', onPointerMove );
- window.removeEventListener( 'pointerup', onPointerUp );
- };
-
- window.addEventListener( 'pointermove', onPointerMove );
- window.addEventListener( 'pointerup', onPointerUp );
- },
- [ side, updateWidth, width ]
- );
-
- return {
- width,
- isResizing,
- listCollapsed: width < CHAT_PANEL_COLLAPSE_THRESHOLD,
- collapseList,
- expandList,
- startResize,
- };
-}
diff --git a/apps/ui/src/ui-desks/chats/widget-context/index.test.ts b/apps/ui/src/ui-desks/chats/widget-context/index.test.ts
deleted file mode 100644
index b45b3fdf7b..0000000000
--- a/apps/ui/src/ui-desks/chats/widget-context/index.test.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { describe, expect, it, vi } from 'vitest';
-import {
- buildWidgetContextDisplayMessage,
- buildWidgetContextPrompt,
- summarizeWidgetList,
-} from './index';
-import type { DeskWidget } from '@/ui-desks/widgets/types';
-
-vi.mock( '@wordpress/i18n', () => ( {
- __: ( text: string ) => text,
- _n: ( single: string, plural: string, count: number ) => ( count === 1 ? single : plural ),
- sprintf: ( format: string, ...args: Array< string | number > ) =>
- args.reduce< string >(
- ( message, arg, index ) =>
- message
- .replace( `%${ index + 1 }$s`, String( arg ) )
- .replace( `%${ index + 1 }$d`, String( arg ) ),
- format
- ),
-} ) );
-
-vi.mock( '@/ui-desks/widgets/registry', () => ( {
- getWidgetDefinition: ( type: string ) =>
- type === 'note'
- ? {
- name: () => 'Note',
- isWidgetProps: ( props: unknown ) =>
- props !== null && typeof props === 'object' && 'text' in props,
- getSummary: ( props: { text: string } ) => props.text,
- }
- : undefined,
-} ) );
-
-describe( 'widget chat context', () => {
- it( 'serializes attached widgets into the agent prompt', () => {
- const prompt = buildWidgetContextPrompt( 'What should change?', [
- createWidget( 'note-1', { text: 'Draft intro' } ),
- ] );
-
- expect( prompt ).toContain( 'Use the following Studio canvas selection as context.' );
- expect( prompt ).toContain( '"widgetId":"note-1"' );
- expect( prompt ).toContain( '"widgetProps":{"text":"Draft intro"}' );
- expect( prompt ).toContain( 'User request:\nWhat should change?' );
- } );
-
- it( 'summarizes visible widget labels for the display message', () => {
- const widgets = [
- createWidget( 'note-1', { text: 'Draft intro' } ),
- createWidget( 'note-2', { text: 'Pull quote' } ),
- createWidget( 'note-3', { text: 'CTA' } ),
- createWidget( 'note-4', { text: 'Footer' } ),
- createWidget( 'note-5', { text: 'Sidebar' } ),
- createWidget( 'note-6', { text: 'Archive' } ),
- ];
-
- expect( summarizeWidgetList( widgets ) ).toBe(
- 'Note: Draft intro, Note: Pull quote, Note: CTA, Note: Footer + 2 more'
- );
- expect( buildWidgetContextDisplayMessage( 'Review these', widgets ) ).toBe(
- 'Review these\n\nSelected context: Note: Draft intro, Note: Pull quote, Note: CTA, Note: Footer + 2 more'
- );
- } );
-} );
-
-function createWidget( id: string, widgetProps: Record< string, unknown > ): DeskWidget {
- return {
- id,
- type: 'note',
- x: 10,
- y: 20,
- shapeProps: {
- w: 200,
- h: 160,
- },
- widgetProps,
- } as DeskWidget;
-}
diff --git a/apps/ui/src/ui-desks/chats/widget-context/index.tsx b/apps/ui/src/ui-desks/chats/widget-context/index.tsx
deleted file mode 100644
index bca34d052f..0000000000
--- a/apps/ui/src/ui-desks/chats/widget-context/index.tsx
+++ /dev/null
@@ -1,212 +0,0 @@
-import { __, _n, sprintf } from '@wordpress/i18n';
-import { clsx } from 'clsx';
-import { type ComponentType, type CSSProperties } from 'react';
-import { getWidgetDefinition } from '@/ui-desks/widgets/registry';
-import styles from './style.module.css';
-import type { DeskWidget, DeskWidgetComponentProps } from '@/ui-desks/widgets/types';
-
-type WidgetThumbnailComponent = ComponentType<
- DeskWidgetComponentProps< Record< string, unknown > >
->;
-
-export const MAX_VISIBLE_CHAT_WIDGETS = 4;
-
-const WIDGET_THUMBNAIL_MAX_SIZE = 72;
-const WIDGET_THUMBNAIL_FALLBACK_SIZE = 96;
-
-interface WidgetContextThumbnailListProps {
- widgets: DeskWidget[];
- className?: string;
- ariaLabel?: string;
- maxVisible?: number;
-}
-
-export function WidgetContextThumbnailList( {
- widgets,
- className,
- ariaLabel = __( 'Selected widgets' ),
- maxVisible = MAX_VISIBLE_CHAT_WIDGETS,
-}: WidgetContextThumbnailListProps ) {
- const visibleWidgets = widgets.slice( 0, maxVisible );
- const hiddenWidgetCount = widgets.length - visibleWidgets.length;
-
- return (
-
- { visibleWidgets.map( ( widget ) => (
-
- ) ) }
- { hiddenWidgetCount > 0 && }
-
- );
-}
-
-export function WidgetContextThumbnail( { widget }: { widget: DeskWidget } ) {
- const definition = getWidgetDefinition( widget.type );
- if ( ! definition || ! definition.isWidgetProps( widget.widgetProps ) ) {
- return null;
- }
-
- const hasCustomThumbnail = Boolean( definition.thumbnail );
- const Thumbnail = ( definition.thumbnail ??
- definition.Component ) as unknown as WidgetThumbnailComponent;
- const geometry = getWidgetThumbnailGeometry( widget, hasCustomThumbnail );
- const frameStyle = {
- width: `${ geometry.width }px`,
- height: `${ geometry.height }px`,
- } satisfies CSSProperties;
- const innerStyle = {
- width: `${ geometry.sourceWidth }px`,
- height: `${ geometry.sourceHeight }px`,
- transform: `translate(-50%, -50%) scale(${ geometry.scale })`,
- } satisfies CSSProperties;
- const label = getWidgetDisplayLabel( widget );
-
- const thumbnail = (
-
- );
-
- return (
-
- { hasCustomThumbnail ? (
- thumbnail
- ) : (
-
- { thumbnail }
-
- ) }
-
- );
-}
-
-export function WidgetContextMoreThumbnail( { count }: { count: number } ) {
- return (
-
- { getMoreWidgetsLabel( count ) }
-
- );
-}
-
-export function buildWidgetContextPrompt( userPrompt: string, widgets: DeskWidget[] ) {
- const context = widgets
- .map(
- ( widget, index ) =>
- `${ index + 1 }. ${ JSON.stringify( {
- widgetId: widget.id,
- type: widget.type,
- position: {
- x: widget.x,
- y: widget.y,
- },
- widgetProps: widget.widgetProps,
- } ) }`
- )
- .join( '\n' );
-
- return [
- 'Use the following Studio canvas selection as context.',
- 'The selected items are canvas widgets. Refer to widget IDs and WordPress entity IDs when helpful.',
- '',
- context,
- '',
- 'User request:',
- userPrompt,
- ].join( '\n' );
-}
-
-export function buildWidgetContextDisplayMessage( userPrompt: string, widgets: DeskWidget[] ) {
- return sprintf(
- /* translators: 1: user prompt, 2: short selected widget summary. */
- __( '%1$s\n\nSelected context: %2$s' ),
- userPrompt,
- summarizeWidgetList( widgets )
- );
-}
-
-export function summarizeWidgetList( widgets: DeskWidget[] ) {
- const visibleWidgets = widgets.slice( 0, MAX_VISIBLE_CHAT_WIDGETS );
- const labels = visibleWidgets.map( getWidgetDisplayLabel );
- const hiddenCount = widgets.length - visibleWidgets.length;
- if ( hiddenCount <= 0 ) {
- return labels.join( ', ' );
- }
-
- return sprintf(
- /* translators: 1: comma-separated selected widget labels, 2: number of additional widgets. */
- _n( '%1$s + %2$d more', '%1$s + %2$d more', hiddenCount ),
- labels.join( ', ' ),
- hiddenCount
- );
-}
-
-export function getWidgetDisplayLabel( widget: DeskWidget ) {
- const summary = getWidgetSummary( widget );
- return summary ? `${ getWidgetTypeLabel( widget ) }: ${ summary }` : getWidgetTypeLabel( widget );
-}
-
-function getWidgetThumbnailGeometry( widget: DeskWidget, hasCustomThumbnail: boolean ) {
- if ( hasCustomThumbnail ) {
- return {
- sourceWidth: WIDGET_THUMBNAIL_MAX_SIZE,
- sourceHeight: WIDGET_THUMBNAIL_FALLBACK_SIZE,
- scale: 1,
- width: WIDGET_THUMBNAIL_MAX_SIZE,
- height: WIDGET_THUMBNAIL_FALLBACK_SIZE,
- };
- }
-
- const sourceWidth = getThumbnailSourceSize( widget.shapeProps.w );
- const sourceHeight = getThumbnailSourceSize( widget.shapeProps.h );
- const scale = Math.min( 1, WIDGET_THUMBNAIL_MAX_SIZE / Math.max( sourceWidth, sourceHeight ) );
-
- return {
- sourceWidth,
- sourceHeight,
- scale,
- width: WIDGET_THUMBNAIL_MAX_SIZE,
- height: WIDGET_THUMBNAIL_FALLBACK_SIZE,
- };
-}
-
-function getThumbnailSourceSize( value: number ) {
- if ( value < 24 ) {
- return WIDGET_THUMBNAIL_FALLBACK_SIZE;
- }
-
- return value;
-}
-
-function getMoreWidgetsLabel( count: number ) {
- return sprintf(
- /* translators: %d: number of additional selected widgets. */
- __( '+%d more' ),
- count
- );
-}
-
-function getWidgetTypeLabel( widget: DeskWidget ) {
- return getWidgetDefinition( widget.type )?.name() ?? widget.type;
-}
-
-function getWidgetSummary( widget: DeskWidget ) {
- const definition = getWidgetDefinition( widget.type );
- if ( ! definition || ! definition.isWidgetProps( widget.widgetProps ) ) {
- return '';
- }
-
- const getSummary = definition.getSummary as
- | ( ( widgetProps: DeskWidget[ 'widgetProps' ] ) => string )
- | undefined;
-
- return getSummary?.( widget.widgetProps ) ?? '';
-}
-
-function noopWidgetPropsChange() {}
-function noopWidgetEditComplete() {}
diff --git a/apps/ui/src/ui-desks/chats/widget-context/style.module.css b/apps/ui/src/ui-desks/chats/widget-context/style.module.css
deleted file mode 100644
index 78dd12510e..0000000000
--- a/apps/ui/src/ui-desks/chats/widget-context/style.module.css
+++ /dev/null
@@ -1,49 +0,0 @@
-.thumbnails {
- display: flex;
- flex-wrap: wrap;
- align-items: flex-start;
- gap: 8px;
-}
-
-.thumbnail {
- position: relative;
- flex: 0 0 auto;
- overflow: hidden;
- pointer-events: none;
- border-radius: 10px;
- corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- -webkit-corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
-}
-
-.thumbnailInner {
- position: absolute;
- top: 50%;
- left: 50%;
- transform-origin: center;
- pointer-events: none;
-}
-
-.moreThumbnail {
- width: 72px;
- height: 96px;
- flex: 0 0 auto;
- display: flex;
- align-items: center;
- justify-content: center;
- box-sizing: border-box;
- padding: 8px;
- border: 1px solid rgba(255, 255, 255, 0.72);
- border-radius: 10px;
- corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- -webkit-corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- background: rgba(255, 255, 255, 0.58);
- box-shadow:
- 0 1px 2px rgba(15, 23, 42, 0.04),
- 0 14px 30px rgba(15, 23, 42, 0.08);
- color: var(--ui-desks-text, #14171a);
- font-size: 12px;
- line-height: 1.2;
- font-weight: 600;
- letter-spacing: 0;
- text-align: center;
-}
diff --git a/apps/ui/src/ui-desks/chrome/chats-button.tsx b/apps/ui/src/ui-desks/chrome/chats-button.tsx
deleted file mode 100644
index d97a930cc4..0000000000
--- a/apps/ui/src/ui-desks/chrome/chats-button.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { __ } from '@wordpress/i18n';
-import { comment } from '@wordpress/icons';
-import { Button } from '@/ui-desks/components';
-
-interface ChatsButtonProps {
- open: boolean;
- onToggle: () => void;
-}
-
-export function ChatsButton( { open, onToggle }: ChatsButtonProps ) {
- return (
-
- );
-}
diff --git a/apps/ui/src/ui-desks/chrome/create-menu/index.tsx b/apps/ui/src/ui-desks/chrome/create-menu/index.tsx
deleted file mode 100644
index f1faee07e9..0000000000
--- a/apps/ui/src/ui-desks/chrome/create-menu/index.tsx
+++ /dev/null
@@ -1,377 +0,0 @@
-import { useNavigate } from '@tanstack/react-router';
-import { useRegistry } from '@wordpress/data';
-import { __ } from '@wordpress/i18n';
-import { chevronLeft, chevronRight, download, globe, link, plus, verse } from '@wordpress/icons';
-import { Icon } from '@wordpress/ui';
-import { useState, type ReactNode } from 'react';
-import { SiteIcon } from '@/components/site-icon';
-import { useSites } from '@/data/queries/use-sites';
-import { Button, Menu } from '@/ui-desks/components';
-import { useDesk } from '@/ui-desks/desk/provider';
-import {
- isWidgetAvailableInDeskContext,
- isWidgetCreationDisabled,
-} from '@/ui-desks/widget-actions/availability';
-import { getCreateWidgetOptions } from '@/ui-desks/widget-actions/create-widget-options';
-import {
- getExistingContentWidgetProps,
- getExistingContentWidgetType,
- useExistingContentPicker,
- type ExistingContentType,
-} from '@/ui-desks/widget-actions/existing-content-picker';
-import { pageWidgetDefinition } from '@/ui-desks/widgets/page/definition';
-import { postWidgetDefinition } from '@/ui-desks/widgets/post/definition';
-import { getCreatableWidgetDefinitions } from '@/ui-desks/widgets/registry';
-import { siteCardWidgetDefinition } from '@/ui-desks/widgets/site-card/definition';
-import { getThemePatterns, getThemeTemplates } from '@/ui-desks/widgets/theme/api';
-import { themeWidgetDefinition } from '@/ui-desks/widgets/theme/definition';
-import {
- createThemePatternBrowserMaterialization,
- themePatternBrowserWidgetDefinition,
-} from '@/ui-desks/widgets/theme-pattern-browser/definition';
-import {
- createThemeTemplateBrowserMaterialization,
- themeTemplateBrowserWidgetDefinition,
-} from '@/ui-desks/widgets/theme-template-browser/definition';
-import { LinkFromUrlDialog } from '../link-from-url-dialog';
-import styles from './style.module.css';
-import type { SiteDetails } from '@/data/core';
-import type { DeskWidgetDefinition } from '@/ui-desks/widgets/types';
-
-export function DeskCreateMenu() {
- const navigate = useNavigate();
- const desk = useDesk();
- const registry = useRegistry();
- const creatableWidgetDefinitions = getCreatableWidgetDefinitions().filter( ( definition ) =>
- isWidgetAvailableInDeskContext( definition, Boolean( desk.siteId ) )
- );
- const [ mode, setMode ] = useState< 'menu' | 'pick-post' | 'pick-page' | 'pick-site-card' >(
- 'menu'
- );
- const [ isLinkDialogOpen, setIsLinkDialogOpen ] = useState( false );
- const { data: sites } = useSites();
- const site = sites?.find( ( candidate ) => candidate.id === desk.siteId );
- const isSiteRunning = Boolean( site?.running );
- const addCreatableWidget = async (
- definition: ReturnType< typeof getCreatableWidgetDefinitions >[ number ]
- ) => {
- desk.addWidget(
- definition.type,
- await getCreateWidgetOptions( definition, registry, {
- shouldStartEditing: definition.shouldStartEditingOnCreate,
- } )
- );
- };
- const addPatternBrowser = async () => {
- const patterns = await getThemePatterns( { registry } );
- desk.addMaterializedDesk( ( context ) =>
- createThemePatternBrowserMaterialization( context, patterns )
- );
- };
- const addTemplateBrowser = async () => {
- const templates = await getThemeTemplates( { registry } );
- desk.addMaterializedDesk( ( context ) =>
- createThemeTemplateBrowserMaterialization( context, templates )
- );
- };
-
- const openCreateSite = () => {
- void navigate( { to: '/onboarding' } );
- };
-
- const openImportSite = () => {
- void navigate( { to: '/onboarding/import', search: { step: 'select' } } );
- };
-
- return (
- <>
- ! open && setMode( 'menu' ) }>
- } />
-
- { mode === 'menu' ? (
- <>
- { creatableWidgetDefinitions.map( ( definition ) => (
- void addCreatableWidget( definition ) }
- >
- { definition.type === themeWidgetDefinition.type && (
- <>
- void addPatternBrowser() }
- />
- void addTemplateBrowser() }
- />
- >
- ) }
-
- ) ) }
- { desk.siteId ? (
-
- desk.addWidget( siteCardWidgetDefinition.type, {
- shouldStartEditing: false,
- } )
- }
- >
- { siteCardWidgetDefinition.icon && (
-
- ) }
- { siteCardWidgetDefinition.labels.add() }
-
- ) : (
- {
- event.preventDefault();
- setMode( 'pick-site-card' );
- } }
- >
- { siteCardWidgetDefinition.icon && (
-
- ) }
- { siteCardWidgetDefinition.labels.add() }
-
-
- ) }
- setIsLinkDialogOpen( true ) }
- >
-
- { __( 'New link from URL' ) }
-
-
-
- { __( 'New drawing' ) }
-
- { desk.siteId && (
- <>
- {
- event.preventDefault();
- setMode( 'pick-post' );
- } }
- >
- { postWidgetDefinition.icon && }
- { postWidgetDefinition.labels.add() }
-
- {
- event.preventDefault();
- setMode( 'pick-page' );
- } }
- >
- { pageWidgetDefinition.icon && }
- { pageWidgetDefinition.labels.add() }
-
- >
- ) }
- { ( creatableWidgetDefinitions.length > 0 || sites?.length || desk.siteId ) && (
-
- ) }
-
-
- { __( 'New site' ) }
-
-
-
- { __( 'Import from…' ) }
-
- >
- ) : mode === 'pick-site-card' ? (
- setMode( 'menu' ) }
- onSelect={ ( selectedSiteId ) => {
- desk.addWidget( siteCardWidgetDefinition.type, {
- widgetProps: {
- siteId: selectedSiteId,
- },
- shouldStartEditing: false,
- } );
- } }
- />
- ) : (
- setMode( 'menu' ) }
- />
- ) }
-
-
- { isLinkDialogOpen && setIsLinkDialogOpen( false ) } /> }
- >
- );
-}
-
-function CreateWidgetMenuItem( {
- definition,
- canAddWidgets,
- isSiteRunning,
- onClick,
- children,
-}: {
- definition: DeskWidgetDefinition;
- canAddWidgets: boolean;
- isSiteRunning: boolean;
- onClick: () => void;
- children?: ReactNode;
-} ) {
- return (
- <>
-
- { definition.icon && }
- { definition.labels.add() }
-
- { children }
- >
- );
-}
-
-function SiteCardPickerMenuItems( {
- onBack,
- onSelect,
-}: {
- onBack: () => void;
- onSelect: ( siteId: string ) => void;
-} ) {
- const desk = useDesk();
- const { data: sites, isLoading } = useSites();
-
- return (
- <>
- {
- event.preventDefault();
- onBack();
- } }
- >
-
- { __( 'Back' ) }
-
-
- { isLoading && { __( 'Loading sites…' ) }
}
- { ! isLoading && ! sites?.length && (
- { __( 'No sites available.' ) }
- ) }
- { sites?.map( ( site ) => (
- onSelect( site.id ) }
- >
-
-
- ) ) }
- >
- );
-}
-
-function SiteCardPickerSite( { site }: { site: SiteDetails } ) {
- return (
- <>
-
-
- { site.name }
-
- { site.running ? __( 'Running' ) : __( 'Stopped' ) }
-
-
- >
- );
-}
-
-function ExistingContentPickerMenuItems( {
- type,
- onBack,
-}: {
- type: ExistingContentType;
- onBack: () => void;
-} ) {
- const desk = useDesk();
- const { items, statusMessage } = useExistingContentPicker( { type, siteId: desk.siteId } );
-
- return (
- <>
- {
- event.preventDefault();
- onBack();
- } }
- >
-
- { __( 'Back' ) }
-
-
- { statusMessage && { statusMessage }
}
- { items?.map( ( item ) => {
- return (
-
- desk.addWidget( getExistingContentWidgetType( type ), {
- widgetProps: getExistingContentWidgetProps( type, item.id ),
- shouldStartEditing: false,
- } )
- }
- >
-
- { item.title }
- { item.status && (
- { item.statusInfo.label }
- ) }
-
- { item.status && (
-
- ) }
-
- );
- } ) }
- >
- );
-}
diff --git a/apps/ui/src/ui-desks/chrome/create-menu/style.module.css b/apps/ui/src/ui-desks/chrome/create-menu/style.module.css
deleted file mode 100644
index e8d5af1356..0000000000
--- a/apps/ui/src/ui-desks/chrome/create-menu/style.module.css
+++ /dev/null
@@ -1,70 +0,0 @@
-.popup {
- min-width: 220px;
-}
-
-.postPickerPopup {
- width: 280px;
- max-height: 420px;
- overflow-y: auto;
-}
-
-.postPickerStatus {
- padding: var(--wpds-dimension-padding-sm);
- font-size: var(--wpds-typography-font-size-sm);
- line-height: var(--wpds-typography-line-height-sm);
- color: var(--wpds-color-fg-content-neutral-weak);
-}
-
-.postPickerItem {
- display: grid;
- grid-template-columns: minmax(0, 1fr) auto;
- align-items: center;
- gap: 8px;
-}
-
-.postPickerContent {
- display: flex;
- min-width: 0;
- flex-direction: column;
- gap: 2px;
-}
-
-.postPickerTitle,
-.postPickerMeta {
- max-width: 100%;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.postPickerTitle {
- font-size: var(--wpds-typography-font-size-sm);
- line-height: var(--wpds-typography-line-height-sm);
- color: var(--wpds-color-fg-content-neutral);
-}
-
-.postPickerMeta {
- font-size: var(--wpds-typography-font-size-xs);
- line-height: var(--wpds-typography-line-height-xs);
- color: var(--wpds-color-fg-content-neutral-weak);
-}
-
-.postPickerStatusDot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.9);
-}
-
-.sitePickerItem {
- display: grid;
- grid-template-columns: 28px minmax(0, 1fr);
- align-items: center;
- gap: 10px;
-}
-
-.sitePickerIcon {
- width: 28px;
- height: 28px;
- border-radius: 8px;
-}
diff --git a/apps/ui/src/ui-desks/chrome/header/index.tsx b/apps/ui/src/ui-desks/chrome/header/index.tsx
deleted file mode 100644
index be7f75f45d..0000000000
--- a/apps/ui/src/ui-desks/chrome/header/index.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { __ } from '@wordpress/i18n';
-import { clsx } from 'clsx';
-import { useFullscreen } from '@/hooks/use-fullscreen';
-import styles from './style.module.css';
-import type { ReactNode } from 'react';
-
-interface DeskHeaderProps {
- children: ReactNode;
- centerChildren?: ReactNode;
- rightChildren?: ReactNode;
-}
-
-export function DeskHeader( { children, centerChildren, rightChildren }: DeskHeaderProps ) {
- const isFullscreen = useFullscreen();
-
- return (
-
-
{ __( 'Studio' ) }
-
{ children }
- { centerChildren &&
{ centerChildren }
}
- { rightChildren &&
{ rightChildren }
}
-
- );
-}
diff --git a/apps/ui/src/ui-desks/chrome/header/style.module.css b/apps/ui/src/ui-desks/chrome/header/style.module.css
deleted file mode 100644
index f576c8b0b3..0000000000
--- a/apps/ui/src/ui-desks/chrome/header/style.module.css
+++ /dev/null
@@ -1,75 +0,0 @@
-.root {
- --desk-titlebar-height: 46px;
- --desk-title-y-nudge: 3px;
- --desk-macos-traffic-light-x: 20px;
- --desk-chrome-edge-offset: var(--wpds-dimension-padding-xl);
-
- position: fixed;
- top: 0;
- left: 0;
- z-index: 20;
- pointer-events: none;
-}
-
-.title {
- position: fixed;
- top: var(--desk-title-y-nudge);
- left: 94px;
- display: flex;
- align-items: center;
- height: var(--desk-titlebar-height);
- font-size: var(--wpds-typography-font-size-md);
- font-weight: 600;
- color: var(--wpds-color-fg-content-neutral);
- pointer-events: auto;
- -webkit-app-region: drag;
-}
-
-.actions {
- position: fixed;
- top: calc(var(--desk-titlebar-height) + var(--wpds-dimension-padding-sm));
- left: var(--desk-macos-traffic-light-x);
- display: flex;
- align-items: center;
- gap: var(--wpds-dimension-padding-md);
- pointer-events: auto;
-}
-
-.rightActions {
- position: fixed;
- top: calc(var(--desk-titlebar-height) + var(--wpds-dimension-padding-sm));
- right: var(--desk-chrome-edge-offset);
- display: flex;
- align-items: center;
- gap: var(--wpds-dimension-padding-md);
- pointer-events: auto;
-}
-
-.centerActions {
- position: fixed;
- top: calc(var(--desk-titlebar-height) + var(--wpds-dimension-padding-sm));
- left: 50%;
- display: flex;
- align-items: center;
- height: 36px;
- max-width: min(360px, calc(100vw - 320px));
- transform: translateX(-50%);
- pointer-events: none;
-}
-
-.fullscreen .title {
- display: none;
-}
-
-.fullscreen .actions {
- top: var(--desk-chrome-edge-offset);
- left: var(--desk-chrome-edge-offset);
-}
-
-.fullscreen .rightActions {
- top: var(--desk-chrome-edge-offset);
-}
-
-.fullscreen .centerActions {
- top: var(--desk-chrome-edge-offset);
-}
diff --git a/apps/ui/src/ui-desks/chrome/index.tsx b/apps/ui/src/ui-desks/chrome/index.tsx
deleted file mode 100644
index 0f118edbad..0000000000
--- a/apps/ui/src/ui-desks/chrome/index.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-import { createDefaultDeskSettings } from '@studio/common/lib/desk-settings';
-import { useCallback, useEffect, useMemo, useState } from 'react';
-import { useDeskSettings, useUpdateDeskSettings } from '@/data/queries/use-desk-config';
-import { useSites } from '@/data/queries/use-sites';
-import { ChatsTrigger } from '../chats';
-import { DeskCreateMenu } from './create-menu';
-import { DeskHeader } from './header';
-import { DeskSettingsButton } from './settings-button';
-import { SiteDetailsDropdown } from './site-details-dropdown';
-import { DeskSiteMapButton } from './site-map-button';
-import { DeskSiteMapTitle } from './site-map-title';
-import {
- moveDeskToolbarButton,
- normalizeDeskToolbarSettings,
- type DeskToolbarButtonId,
- type DeskToolbarLayout,
-} from './toolbar-layout';
-import { EMPTY_DRAG_STATE, ToolbarRow, type ToolbarDragState } from './toolbar-row';
-import { DeskMenu } from './user-menu';
-
-interface DeskChromeProps {
- siteId?: string;
- siteMapOpen?: boolean;
- siteMapPageCount?: number;
- settingsOpen: boolean;
- editingToolbar: boolean;
- onToggleSiteMap?: () => void;
- onToggleSettings: () => void;
-}
-
-export function DeskChrome( {
- siteId,
- siteMapOpen = false,
- siteMapPageCount,
- settingsOpen,
- editingToolbar,
- onToggleSiteMap,
- onToggleSettings,
-}: DeskChromeProps ) {
- const { data: savedSettings } = useDeskSettings();
- const { data: sites } = useSites();
- const activeSite = sites?.find( ( candidate ) => candidate.id === siteId );
- const fallbackSettings = useMemo( () => createDefaultDeskSettings(), [] );
- const settings = useMemo(
- () => normalizeDeskToolbarSettings( savedSettings ?? fallbackSettings ),
- [ fallbackSettings, savedSettings ]
- );
- const updateDeskSettings = useUpdateDeskSettings();
- const [ dragState, setDragState ] = useState< ToolbarDragState >( EMPTY_DRAG_STATE );
- const clearDragState = useCallback( () => setDragState( EMPTY_DRAG_STATE ), [] );
-
- useEffect( () => {
- window.addEventListener( 'dragend', clearDragState );
- return () => window.removeEventListener( 'dragend', clearDragState );
- }, [ clearDragState ] );
-
- const renderButton = ( buttonId: DeskToolbarButtonId ) => {
- switch ( buttonId ) {
- case 'chat':
- return ;
- case 'create':
- return ;
- case 'site-map':
- return siteId && onToggleSiteMap ? (
-
- ) : null;
- case 'settings':
- return ;
- }
- };
-
- const reorderButton = (
- buttonId: DeskToolbarButtonId,
- side: keyof DeskToolbarLayout,
- beforeButtonId: DeskToolbarButtonId | null
- ) => {
- updateDeskSettings( {
- toolbarLayout: moveDeskToolbarButton(
- settings.toolbarLayout,
- buttonId,
- side,
- beforeButtonId
- ),
- } );
- };
-
- return (
- : null }
- rightChildren={
-
- }
- >
-
-
- { activeSite ? (
-
- ) : null }
- >
- }
- />
-
- );
-}
diff --git a/apps/ui/src/ui-desks/chrome/link-from-url-dialog/index.tsx b/apps/ui/src/ui-desks/chrome/link-from-url-dialog/index.tsx
deleted file mode 100644
index 98a9fbf652..0000000000
--- a/apps/ui/src/ui-desks/chrome/link-from-url-dialog/index.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import { __ } from '@wordpress/i18n';
-import { arrowUp } from '@wordpress/icons';
-import { useEffect, useRef, useState } from 'react';
-import {
- Button,
- Dialog,
- DialogError,
- DialogRow,
- DialogTip,
- dialogInputClassName,
-} from '@/ui-desks/components';
-import { useDesk } from '@/ui-desks/desk/provider';
-import { createUrlPastePayload } from '@/ui-desks/widget-actions/paste-handlers';
-
-interface LinkFromUrlDialogProps {
- center?: {
- x: number;
- y: number;
- };
- onClose: () => void;
-}
-
-export function LinkFromUrlDialog( { center, onClose }: LinkFromUrlDialogProps ) {
- const desk = useDesk();
- const inputRef = useRef< HTMLInputElement | null >( null );
- const [ text, setText ] = useState( '' );
- const [ error, setError ] = useState< string | null >( null );
- const payload = createUrlPastePayload( text );
- const canSubmit = Boolean( payload ) && desk.canAddWidgets;
-
- useEffect( () => {
- inputRef.current?.focus();
- }, [] );
-
- const submit = async () => {
- if ( ! payload ) {
- setError( __( 'Enter a valid URL.' ) );
- return;
- }
-
- setError( null );
- const didAdd = await desk.addPastedContent( payload, center ? { center } : undefined );
- if ( didAdd ) {
- onClose();
- return;
- }
-
- setError( __( 'Unable to create a link from this URL.' ) );
- };
-
- return (
-
- );
-}
diff --git a/apps/ui/src/ui-desks/chrome/settings-button.tsx b/apps/ui/src/ui-desks/chrome/settings-button.tsx
deleted file mode 100644
index dcffb2020d..0000000000
--- a/apps/ui/src/ui-desks/chrome/settings-button.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { __ } from '@wordpress/i18n';
-import { cog } from '@wordpress/icons';
-import { Button } from '@/ui-desks/components';
-
-interface DeskSettingsButtonProps {
- open: boolean;
- onToggle: () => void;
-}
-
-export function DeskSettingsButton( { open, onToggle }: DeskSettingsButtonProps ) {
- return (
-
- );
-}
diff --git a/apps/ui/src/ui-desks/chrome/settings-modal/index.tsx b/apps/ui/src/ui-desks/chrome/settings-modal/index.tsx
deleted file mode 100644
index 218b1bd298..0000000000
--- a/apps/ui/src/ui-desks/chrome/settings-modal/index.tsx
+++ /dev/null
@@ -1,184 +0,0 @@
-import { createDefaultDeskSettings } from '@studio/common/lib/desk-settings';
-import { sanitizeFolderName } from '@studio/common/lib/sanitize-folder-name';
-import { __ } from '@wordpress/i18n';
-import { download, upload } from '@wordpress/icons';
-import { Field } from '@wordpress/ui';
-import { useEffect, useMemo, useState } from 'react';
-import { useConnector } from '@/data/core';
-import { useDeskSettings, useUpdateDeskSettings } from '@/data/queries/use-desk-config';
-import { useSites } from '@/data/queries/use-sites';
-import {
- Button,
- Dialog,
- DialogCloseButton,
- DialogContent,
- DialogError,
- DialogFooter,
- DialogHeader,
- DialogTip,
- DialogTitle,
-} from '@/ui-desks/components';
-import { useDesk } from '@/ui-desks/desk/provider';
-import styles from './style.module.css';
-import type { DeskConfig } from '@/ui-desks/desk/types';
-
-interface DeskSettingsModalProps {
- open: boolean;
- onOpenChange: ( open: boolean ) => void;
- onEditToolbar: () => void;
-}
-
-export function DeskSettingsModal( { open, onOpenChange, onEditToolbar }: DeskSettingsModalProps ) {
- const connector = useConnector();
- const desk = useDesk();
- const { data: sites } = useSites();
- const { data: savedDeskSettings } = useDeskSettings();
- const fallbackDeskSettings = useMemo( () => createDefaultDeskSettings(), [] );
- const settings = savedDeskSettings ?? fallbackDeskSettings;
- const updateDeskSettings = useUpdateDeskSettings();
- const [ status, setStatus ] = useState< { tone: 'success' | 'error'; message: string } | null >(
- null
- );
- const [ isExporting, setIsExporting ] = useState( false );
- const [ isImporting, setIsImporting ] = useState( false );
- const activeSiteName = sites?.find( ( site ) => site.id === desk.siteId )?.name;
- const isDeskDataActionDisabled =
- desk.isReadOnly || desk.isLoading || ! desk.canAddWidgets || isExporting || isImporting;
-
- useEffect( () => {
- if ( ! open ) {
- setStatus( null );
- }
- }, [ open ] );
-
- const exportDesk = async () => {
- const deskConfig = desk.getDeskConfigSnapshot();
- if ( ! deskConfig ) {
- setStatus( { tone: 'error', message: __( 'Could not export desk.' ) } );
- return;
- }
-
- setStatus( null );
- setIsExporting( true );
- try {
- const exportedPath = await connector.exportDeskConfig(
- deskConfig,
- getDeskExportFilename( activeSiteName )
- );
- if ( exportedPath ) {
- setStatus( { tone: 'success', message: __( 'Desk exported.' ) } );
- }
- } catch ( error ) {
- console.warn( 'Failed to export desk.', error );
- setStatus( { tone: 'error', message: __( 'Could not export desk.' ) } );
- } finally {
- setIsExporting( false );
- }
- };
-
- const importDesk = async () => {
- setStatus( null );
- setIsImporting( true );
- try {
- const deskConfig = await connector.importDeskConfig();
- if ( ! deskConfig ) {
- return;
- }
-
- if (
- ! window.confirm(
- __( 'Replace the current desk with the imported file? This cannot be undone.' )
- )
- ) {
- return;
- }
-
- const didImport = await desk.replaceDeskConfig( deskConfig as DeskConfig );
- setStatus(
- didImport
- ? { tone: 'success', message: __( 'Desk imported.' ) }
- : { tone: 'error', message: __( 'Could not import desk.' ) }
- );
- } catch ( error ) {
- console.warn( 'Failed to import desk.', error );
- setStatus( { tone: 'error', message: __( 'Could not import desk.' ) } );
- } finally {
- setIsImporting( false );
- }
- };
-
- return (
-
- );
-}
-
-function getDeskExportFilename( siteName?: string ) {
- const stamp = new Date().toISOString().slice( 0, 10 );
- const name = sanitizeFolderName( siteName ? `studio-desk-${ siteName }` : 'studio-desk-user' );
- return `${ name || 'studio-desk' }-${ stamp }.json`;
-}
diff --git a/apps/ui/src/ui-desks/chrome/settings-modal/style.module.css b/apps/ui/src/ui-desks/chrome/settings-modal/style.module.css
deleted file mode 100644
index 8b90b96cd4..0000000000
--- a/apps/ui/src/ui-desks/chrome/settings-modal/style.module.css
+++ /dev/null
@@ -1,49 +0,0 @@
-.settingsToggleRow {
- display: flex;
- width: fit-content;
- align-items: center;
- justify-content: flex-start;
- gap: 10px;
- color: var( --ui-desks-text, #14171a );
- font-size: var( --wpds-typography-font-size-sm );
- font-weight: var( --wpds-typography-font-weight-medium, 500 );
- line-height: var( --wpds-typography-line-height-sm );
- cursor: pointer;
-}
-
-.settingsToggleRow input[type='checkbox'] {
- flex: 0 0 auto;
- width: 16px;
- height: 16px;
- margin: 0;
- accent-color: var( --wp-admin-theme-color, #3858e9 );
- cursor: pointer;
-}
-
-.settingsSection {
- display: flex;
- flex-direction: column;
- gap: 10px;
- margin-top: 20px;
- padding-top: 14px;
- border-top: 1px solid var( --ui-desks-divider, rgba( 15, 23, 42, 0.06 ) );
-}
-
-.settingsSectionTitle {
- color: var( --ui-desks-text, #14171a );
- font-size: var( --wpds-typography-font-size-sm );
- font-weight: var( --wpds-typography-font-weight-semibold, 600 );
- line-height: var( --wpds-typography-line-height-sm );
-}
-
-.settingsActions {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
-}
-
-.settingsFooter {
- margin-top: 10px;
- padding-top: 14px;
- border-top: 1px solid var( --ui-desks-divider, rgba( 15, 23, 42, 0.06 ) );
-}
diff --git a/apps/ui/src/ui-desks/chrome/site-details-dropdown/index.test.tsx b/apps/ui/src/ui-desks/chrome/site-details-dropdown/index.test.tsx
deleted file mode 100644
index 5529f858a0..0000000000
--- a/apps/ui/src/ui-desks/chrome/site-details-dropdown/index.test.tsx
+++ /dev/null
@@ -1,328 +0,0 @@
-import '@testing-library/jest-dom/vitest';
-import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
-import { beforeEach, describe, expect, it, vi } from 'vitest';
-import { useConnector } from '@/data/core';
-import { useAuthUser, useLogin } from '@/data/queries/use-auth-user';
-import { useConnectedWpcomSites } from '@/data/queries/use-connected-wpcom-sites';
-import { usePublishPreviewSite } from '@/data/queries/use-preview-site';
-import {
- useDeleteSite,
- useIsSiteStarting,
- useIsSiteStopping,
- useStartSite,
- useStopSite,
-} from '@/data/queries/use-sites';
-import { useSnapshots } from '@/data/queries/use-snapshots';
-import { usePullSiteFromLive, usePushSiteToLive } from '@/data/queries/use-sync-site';
-import { usePickableWpcomSites } from '@/data/queries/use-wpcom-sites';
-import { SiteDetailsDropdown } from './index';
-import type { SiteDetails } from '@/data/core';
-
-const routerMock = vi.hoisted( () => ( {
- navigate: vi.fn(),
-} ) );
-
-vi.mock( '@tanstack/react-router', () => ( {
- useNavigate: () => routerMock.navigate,
-} ) );
-
-vi.mock( '@tanstack/react-query', () => ( {
- useIsMutating: () => 0,
-} ) );
-
-vi.mock( '@/data/core', () => ( {
- useConnector: vi.fn(),
-} ) );
-
-vi.mock( '@/data/queries/use-auth-user', () => ( {
- useAuthUser: vi.fn(),
- useLogin: vi.fn(),
-} ) );
-
-vi.mock( '@/data/queries/use-connected-wpcom-sites', () => ( {
- useConnectedWpcomSites: vi.fn(),
-} ) );
-
-vi.mock( '@/data/queries/use-preview-site', () => ( {
- usePublishPreviewSite: vi.fn(),
-} ) );
-
-vi.mock( '@/data/queries/use-sites', () => ( {
- useDeleteSite: vi.fn(),
- useIsSiteStarting: vi.fn(),
- useIsSiteStopping: vi.fn(),
- useStartSite: vi.fn(),
- useStopSite: vi.fn(),
-} ) );
-
-vi.mock( '@/data/queries/use-snapshots', () => ( {
- useSnapshots: vi.fn(),
-} ) );
-
-vi.mock( '@/data/queries/use-sync-site', () => ( {
- PULL_FROM_LIVE_MUTATION_KEY: [ 'pullSiteFromLive' ],
- PUSH_TO_LIVE_MUTATION_KEY: [ 'pushSiteToLive' ],
- usePullSiteFromLive: vi.fn(),
- usePushSiteToLive: vi.fn(),
-} ) );
-
-vi.mock( '@/data/queries/use-wpcom-sites', () => ( {
- usePickableWpcomSites: vi.fn(),
-} ) );
-
-const useConnectorMock = vi.mocked( useConnector );
-const useAuthUserMock = vi.mocked( useAuthUser );
-const useLoginMock = vi.mocked( useLogin );
-const useConnectedWpcomSitesMock = vi.mocked( useConnectedWpcomSites );
-const usePublishPreviewSiteMock = vi.mocked( usePublishPreviewSite );
-const useDeleteSiteMock = vi.mocked( useDeleteSite );
-const useIsSiteStartingMock = vi.mocked( useIsSiteStarting );
-const useIsSiteStoppingMock = vi.mocked( useIsSiteStopping );
-const useStartSiteMock = vi.mocked( useStartSite );
-const useStopSiteMock = vi.mocked( useStopSite );
-const useSnapshotsMock = vi.mocked( useSnapshots );
-const usePullSiteFromLiveMock = vi.mocked( usePullSiteFromLive );
-const usePushSiteToLiveMock = vi.mocked( usePushSiteToLive );
-const usePickableWpcomSitesMock = vi.mocked( usePickableWpcomSites );
-
-describe( 'SiteDetailsDropdown', () => {
- const openExternalUrl = vi.fn();
- const connectWpcomSite = vi.fn();
- const refetchConnectedSites = vi.fn();
- const publishPreviewMutate = vi.fn();
- const pullMutate = vi.fn();
- const pushMutate = vi.fn();
- const deleteMutate = vi.fn();
- const startMutate = vi.fn();
- const stopMutate = vi.fn();
-
- beforeEach( () => {
- openExternalUrl.mockReset();
- connectWpcomSite.mockReset();
- connectWpcomSite.mockResolvedValue( undefined );
- refetchConnectedSites.mockReset();
- refetchConnectedSites.mockResolvedValue( {} );
- publishPreviewMutate.mockReset();
- pullMutate.mockReset();
- pushMutate.mockReset();
- deleteMutate.mockReset();
- startMutate.mockReset();
- stopMutate.mockReset();
- routerMock.navigate.mockReset();
-
- useConnectorMock.mockReturnValue( {
- getPublishCheckoutUrl: () => 'https://wordpress.com/setup/studio',
- connectWpcomSite,
- openExternalUrl,
- } as never );
- useAuthUserMock.mockReturnValue( {
- data: { id: 1, email: 'person@example.com', displayName: 'Person' },
- isLoading: false,
- } as never );
- useLoginMock.mockReturnValue( {
- isPending: false,
- mutate: vi.fn(),
- } as never );
- useSnapshotsMock.mockReturnValue( {
- data: [
- {
- url: 'preview.example.wordpress.com',
- atomicSiteId: 123,
- localSiteId: 'site-1',
- date: Date.now(),
- },
- ],
- } as never );
- useConnectedWpcomSitesMock.mockReturnValue( {
- data: [
- {
- id: 456,
- localSiteId: 'site-1',
- name: 'Live Site',
- url: 'live.example.com',
- isStaging: false,
- isPressable: false,
- syncSupport: 'syncable',
- lastPullTimestamp: null,
- lastPushTimestamp: null,
- },
- ],
- refetch: refetchConnectedSites,
- } as never );
- usePickableWpcomSitesMock.mockReturnValue( {
- data: [
- {
- id: 789,
- localSiteId: '',
- name: 'Remote Site',
- url: 'https://remote.example.com',
- isStaging: false,
- isPressable: false,
- syncSupport: 'syncable',
- lastPullTimestamp: null,
- lastPushTimestamp: null,
- },
- ],
- isLoading: false,
- isFetching: false,
- error: null,
- refetch: vi.fn(),
- } as never );
- usePublishPreviewSiteMock.mockReturnValue( {
- isPending: false,
- mutate: publishPreviewMutate,
- } as never );
- usePushSiteToLiveMock.mockReturnValue( { mutate: pushMutate } as never );
- usePullSiteFromLiveMock.mockReturnValue( { mutate: pullMutate } as never );
- useDeleteSiteMock.mockReturnValue( {
- isPending: false,
- mutate: deleteMutate,
- } as never );
- useStartSiteMock.mockReturnValue( { mutate: startMutate } as never );
- useStopSiteMock.mockReturnValue( { mutate: stopMutate } as never );
- useIsSiteStartingMock.mockReturnValue( false );
- useIsSiteStoppingMock.mockReturnValue( false );
- } );
-
- it( 'renders site details and wires the primary row actions', async () => {
- const site = createSite();
- render( );
-
- fireEvent.click( screen.getByRole( 'button', { name: 'Site details for Local Studio Site' } ) );
-
- expect( await screen.findByText( 'Local' ) ).toBeVisible();
- expect( screen.getByText( 'Preview' ) ).toBeVisible();
- expect( screen.getByText( 'Live' ) ).toBeVisible();
- expect( screen.getAllByText( 'Running on localhost' )[ 0 ] ).toBeVisible();
- expect( screen.getByText( 'Connected' ) ).toBeVisible();
-
- fireEvent.click( screen.getByRole( 'button', { name: 'Stop site' } ) );
- expect( stopMutate ).toHaveBeenCalledWith( 'site-1' );
-
- fireEvent.click( screen.getByRole( 'button', { name: 'Push to Preview' } ) );
- expect( publishPreviewMutate ).toHaveBeenCalledWith( {
- siteId: 'site-1',
- existingHostname: 'preview.example.wordpress.com',
- } );
-
- fireEvent.click( screen.getByRole( 'button', { name: 'Push to Live' } ) );
- expect( pushMutate ).toHaveBeenCalledWith( { siteId: 'site-1', remoteSiteId: 456 } );
-
- fireEvent.click( screen.getByRole( 'button', { name: 'Pull from Live' } ) );
- expect( pullMutate ).toHaveBeenCalledWith( { siteId: 'site-1', remoteSiteId: 456 } );
-
- fireEvent.click( screen.getByRole( 'button', { name: 'Open local site' } ) );
- await waitFor( () =>
- expect( openExternalUrl ).toHaveBeenCalledWith( 'http://localhost:8881' )
- );
-
- fireEvent.click( screen.getByRole( 'button', { name: 'Site details for Local Studio Site' } ) );
- fireEvent.click( await screen.findByRole( 'button', { name: 'Open site settings' } ) );
- expect( routerMock.navigate ).toHaveBeenCalledWith( {
- to: '/sites/$siteId/settings',
- params: { siteId: 'site-1' },
- } );
- } );
-
- it( 'confirms before deleting the site from the dropdown', async () => {
- const site = createSite();
- render( );
-
- fireEvent.click( screen.getByRole( 'button', { name: 'Site details for Local Studio Site' } ) );
- fireEvent.click( await screen.findByRole( 'button', { name: 'Delete site' } ) );
-
- const dialog = await screen.findByRole( 'dialog', { name: 'Delete Local Studio Site' } );
- expect(
- within( dialog ).getByText(
- "The site's database will be lost, including all posts, pages, comments, and media."
- )
- ).toBeVisible();
- expect(
- within( dialog ).getByRole( 'checkbox', {
- name: 'Delete site files from my computer',
- } )
- ).toBeChecked();
-
- fireEvent.click( within( dialog ).getByRole( 'button', { name: 'Delete site' } ) );
-
- expect( deleteMutate ).toHaveBeenCalledWith(
- { id: 'site-1', deleteFiles: true },
- expect.objectContaining( {
- onSuccess: expect.any( Function ),
- onError: expect.any( Function ),
- } )
- );
-
- const options = deleteMutate.mock.calls[ 0 ][ 1 ];
- options.onSuccess();
- expect( routerMock.navigate ).toHaveBeenCalledWith( { to: '/' } );
- } );
-
- it( 'opens a desks modal to connect an existing WordPress.com site', async () => {
- useConnectedWpcomSitesMock.mockReturnValue( {
- data: [],
- refetch: refetchConnectedSites,
- } as never );
- const site = createSite();
- render( );
-
- fireEvent.click( screen.getByRole( 'button', { name: 'Site details for Local Studio Site' } ) );
- fireEvent.click( await screen.findByRole( 'button', { name: 'Connect live site' } ) );
-
- const dialog = await screen.findByRole( 'dialog', { name: 'Connect a live site' } );
- expect( within( dialog ).getByText( 'Remote Site' ) ).toBeVisible();
- expect( openExternalUrl ).not.toHaveBeenCalledWith( 'https://wordpress.com/setup/studio' );
-
- fireEvent.click( within( dialog ).getByText( 'Remote Site' ) );
- fireEvent.click( within( dialog ).getByRole( 'button', { name: 'Connect selected site' } ) );
-
- await waitFor( () =>
- expect( connectWpcomSite ).toHaveBeenCalledWith( 'site-1', {
- id: 789,
- localSiteId: 'site-1',
- name: 'Remote Site',
- url: 'https://remote.example.com',
- isStaging: false,
- isPressable: false,
- syncSupport: 'already-connected',
- lastPullTimestamp: null,
- lastPushTimestamp: null,
- } )
- );
- expect( refetchConnectedSites ).toHaveBeenCalled();
- } );
-
- it( 'keeps creating a new WordPress.com site available from the connect modal', async () => {
- useConnectedWpcomSitesMock.mockReturnValue( {
- data: [],
- refetch: refetchConnectedSites,
- } as never );
- const site = createSite();
- render( );
-
- fireEvent.click( screen.getByRole( 'button', { name: 'Site details for Local Studio Site' } ) );
- fireEvent.click( await screen.findByRole( 'button', { name: 'Connect live site' } ) );
-
- const dialog = await screen.findByRole( 'dialog', { name: 'Connect a live site' } );
- fireEvent.click(
- within( dialog ).getByRole( 'button', { name: 'Create a new WordPress.com site' } )
- );
-
- await waitFor( () =>
- expect( openExternalUrl ).toHaveBeenCalledWith( 'https://wordpress.com/setup/studio' )
- );
- expect( connectWpcomSite ).not.toHaveBeenCalled();
- } );
-} );
-
-function createSite( overrides: Partial< SiteDetails > = {} ): SiteDetails {
- return {
- id: 'site-1',
- name: 'Local Studio Site',
- path: '/tmp/local-studio-site',
- port: 8881,
- running: true,
- phpVersion: '8.4',
- ...overrides,
- };
-}
diff --git a/apps/ui/src/ui-desks/chrome/site-details-dropdown/index.tsx b/apps/ui/src/ui-desks/chrome/site-details-dropdown/index.tsx
deleted file mode 100644
index cd788ea510..0000000000
--- a/apps/ui/src/ui-desks/chrome/site-details-dropdown/index.tsx
+++ /dev/null
@@ -1,856 +0,0 @@
-import { useIsMutating } from '@tanstack/react-query';
-import { useNavigate } from '@tanstack/react-router';
-import { __, sprintf } from '@wordpress/i18n';
-import { cog, download, external, formatListBullets, plus, trash, upload } from '@wordpress/icons';
-import { clsx } from 'clsx';
-import { useEffect, useMemo, useState } from 'react';
-import {
- deriveSiteStatus,
- ensureProtocol,
- getSnapshotHostname,
- pickLatestSnapshot,
- pickLiveSite,
- stripProtocol,
-} from '@/components/site-dropdown/utils';
-import { SiteIcon } from '@/components/site-icon';
-import { useConnector } from '@/data/core';
-import { useAuthUser, useLogin } from '@/data/queries/use-auth-user';
-import { useConnectedWpcomSites } from '@/data/queries/use-connected-wpcom-sites';
-import { usePublishPreviewSite } from '@/data/queries/use-preview-site';
-import {
- useDeleteSite,
- useIsSiteStarting,
- useIsSiteStopping,
- useStartSite,
- useStopSite,
-} from '@/data/queries/use-sites';
-import { useSnapshots } from '@/data/queries/use-snapshots';
-import {
- PULL_FROM_LIVE_MUTATION_KEY,
- PUSH_TO_LIVE_MUTATION_KEY,
- usePullSiteFromLive,
- usePushSiteToLive,
-} from '@/data/queries/use-sync-site';
-import { usePickableWpcomSites } from '@/data/queries/use-wpcom-sites';
-import { formatRelativeTime } from '@/lib/format-relative-time';
-import { getSiteDisplayUrl, getSiteUrl } from '@/lib/get-site-url';
-import {
- Button,
- Dialog,
- DialogCloseButton,
- DialogContent,
- DialogError,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- List,
- ListItem,
- LoadingPlaceholder,
- Menu,
-} from '@/ui-desks/components';
-import styles from './style.module.css';
-import type { SiteDetails, SyncSite } from '@/data/core';
-import type { ReactNode } from 'react';
-
-interface SiteDetailsDropdownProps {
- site: SiteDetails;
- disabled?: boolean;
-}
-
-function useIsSiteSyncing( siteId: string ): { push: boolean; pull: boolean } {
- const push =
- useIsMutating( {
- mutationKey: PUSH_TO_LIVE_MUTATION_KEY,
- predicate: ( mutation ) =>
- ( mutation.state.variables as { siteId: string } | undefined )?.siteId === siteId,
- } ) > 0;
- const pull =
- useIsMutating( {
- mutationKey: PULL_FROM_LIVE_MUTATION_KEY,
- predicate: ( mutation ) =>
- ( mutation.state.variables as { siteId: string } | undefined )?.siteId === siteId,
- } ) > 0;
- return { push, pull };
-}
-
-export function SiteDetailsDropdown( { site, disabled = false }: SiteDetailsDropdownProps ) {
- const connector = useConnector();
- const navigate = useNavigate();
- const [ open, setOpen ] = useState( false );
- const [ deleteDialogOpen, setDeleteDialogOpen ] = useState( false );
- const [ connectDialogOpen, setConnectDialogOpen ] = useState( false );
- const [ deleteFiles, setDeleteFiles ] = useState( true );
- const [ deleteError, setDeleteError ] = useState< string | null >( null );
- const { data: snapshots } = useSnapshots();
- const { data: connectedSites, refetch: refetchConnectedSites } = useConnectedWpcomSites(
- site.id
- );
- const previewSnapshot = useMemo(
- () => pickLatestSnapshot( snapshots, site.id ),
- [ snapshots, site.id ]
- );
- const liveSite = useMemo( () => pickLiveSite( connectedSites ), [ connectedSites ] );
- const startSite = useStartSite();
- const stopSite = useStopSite();
- const publishPreviewSite = usePublishPreviewSite();
- const pushSiteToLive = usePushSiteToLive();
- const pullSiteFromLive = usePullSiteFromLive();
- const deleteSite = useDeleteSite();
- const isStarting = useIsSiteStarting( site.id );
- const isStopping = useIsSiteStopping( site.id );
- const { push: isPushPending, pull: isPullPending } = useIsSiteSyncing( site.id );
- const { status } = deriveSiteStatus( site, isStarting, isStopping );
- const checkoutUrl = connector.getPublishCheckoutUrl( site );
- const isPreviewPending = publishPreviewSite.isPending;
- const isSyncing = isPreviewPending || isPushPending || isPullPending;
- const previewActionLabel = isPreviewPending
- ? __( 'Pushing to Preview' )
- : __( 'Push to Preview' );
- const pushActionLabel = isPushPending ? __( 'Pushing to Live' ) : __( 'Push to Live' );
- const pullActionLabel = isPullPending ? __( 'Pulling from Live' ) : __( 'Pull from Live' );
-
- const openExternal = ( url: string ) => {
- setOpen( false );
- void connector.openExternalUrl( url );
- };
-
- const handleSiteSettingsClick = () => {
- setOpen( false );
- void navigate( { to: '/sites/$siteId/settings', params: { siteId: site.id } } );
- };
-
- const handleToggleServer = () => {
- if ( status === 'transitioning' ) {
- return;
- }
- if ( site.running ) {
- stopSite.mutate( site.id );
- } else {
- startSite.mutate( site.id );
- }
- };
-
- const handlePreviewClick = () => {
- if ( isSyncing ) {
- return;
- }
- publishPreviewSite.mutate( {
- siteId: site.id,
- existingHostname: previewSnapshot ? getSnapshotHostname( previewSnapshot ) : undefined,
- } );
- };
-
- const handlePushClick = () => {
- if ( ! liveSite || isSyncing ) {
- return;
- }
- pushSiteToLive.mutate( { siteId: site.id, remoteSiteId: liveSite.id } );
- };
-
- const handlePullClick = () => {
- if ( ! liveSite || isSyncing ) {
- return;
- }
- pullSiteFromLive.mutate( { siteId: site.id, remoteSiteId: liveSite.id } );
- };
-
- const handleDeleteClick = () => {
- setOpen( false );
- setDeleteFiles( true );
- setDeleteError( null );
- setDeleteDialogOpen( true );
- };
-
- const handleConnectClick = () => {
- setOpen( false );
- setConnectDialogOpen( true );
- };
-
- const closeConnectDialog = () => {
- setConnectDialogOpen( false );
- };
-
- const closeDeleteDialog = () => {
- if ( deleteSite.isPending ) {
- return;
- }
- setDeleteDialogOpen( false );
- setDeleteError( null );
- };
-
- const handleConfirmDelete = () => {
- setDeleteError( null );
- deleteSite.mutate(
- { id: site.id, deleteFiles },
- {
- onSuccess: () => {
- setDeleteDialogOpen( false );
- void navigate( { to: '/' } );
- },
- onError: ( error: Error ) => {
- setDeleteError( error.message || __( 'Unable to delete the site. Please try again.' ) );
- },
- }
- );
- };
-
- return (
- <>
-
-
- }
- />
-
-
-
-
-
-
- openExternal( getSiteUrl( site ) ) }
- />
- }
- />
-
-
- {
- if ( previewSnapshot ) {
- openExternal( ensureProtocol( previewSnapshot.url ) );
- }
- } }
- />
- >
- }
- />
-
- { __( 'Connected' ) }
-
- { __( 'WordPress.com' ) }
- >
- ) : (
- __( 'Not connected' )
- )
- }
- actions={
- liveSite ? (
- <>
-
-
- openExternal( ensureProtocol( liveSite.url ) ) }
- />
- >
- ) : (
-
- )
- }
- />
-
-
-
-
-
-
-
-
-
- { connectDialogOpen ? (
- {
- await refetchConnectedSites();
- } }
- onCreateNew={ () => {
- if ( checkoutUrl ) {
- closeConnectDialog();
- openExternal( checkoutUrl );
- }
- } }
- />
- ) : null }
- >
- );
-}
-
-function ConnectLiveSiteDialog( {
- site,
- canCreateNew,
- onClose,
- onConnected,
- onCreateNew,
-}: {
- site: SiteDetails;
- canCreateNew: boolean;
- onClose: () => void;
- onConnected: () => Promise< unknown >;
- onCreateNew: () => void;
-} ) {
- const connector = useConnector();
- const { data: user, isLoading: isLoadingUser } = useAuthUser();
- const login = useLogin();
- const pickableSites = usePickableWpcomSites( { enabled: Boolean( user ) } );
- const [ selectedSiteId, setSelectedSiteId ] = useState< number | null >( null );
- const [ searchQuery, setSearchQuery ] = useState( '' );
- const [ error, setError ] = useState< string | null >( null );
- const [ isConnecting, setIsConnecting ] = useState( false );
- const sites = useMemo( () => pickableSites.data ?? [], [ pickableSites.data ] );
- const filteredSites = useMemo( () => {
- const query = searchQuery.trim().toLowerCase();
- if ( ! query ) {
- return sites;
- }
- return sites.filter( ( candidate ) =>
- [ candidate.name, candidate.url ].some( ( value ) => value.toLowerCase().includes( query ) )
- );
- }, [ sites, searchQuery ] );
- const selectedSite = sites.find( ( candidate ) => candidate.id === selectedSiteId ) ?? null;
- const isLoadingSites = Boolean( user ) && pickableSites.isLoading;
- const isBusy = isConnecting || login.isPending;
-
- useEffect( () => {
- if ( selectedSiteId && ! sites.some( ( candidate ) => candidate.id === selectedSiteId ) ) {
- setSelectedSiteId( null );
- }
- }, [ selectedSiteId, sites ] );
-
- useEffect( () => {
- if ( ! selectedSiteId && sites.length === 1 ) {
- setSelectedSiteId( sites[ 0 ].id );
- }
- }, [ selectedSiteId, sites ] );
-
- const handleClose = () => {
- if ( isBusy ) {
- return;
- }
- onClose();
- };
-
- const handleConnect = async () => {
- if ( ! selectedSite || isConnecting ) {
- return;
- }
- setError( null );
- setIsConnecting( true );
- try {
- await connector.connectWpcomSite( site.id, {
- ...selectedSite,
- localSiteId: site.id,
- syncSupport: 'already-connected',
- } );
- await onConnected();
- setIsConnecting( false );
- onClose();
- } catch ( connectError ) {
- console.error( 'Failed to connect WordPress.com site:', connectError );
- setError( __( 'Unable to connect this site. Please try again.' ) );
- setIsConnecting( false );
- }
- };
-
- return (
-
- );
-}
-
-function ConnectSitesContent( {
- error,
- filteredSites,
- isLoading,
- onSelectSite,
- searchQuery,
- selectedSiteId,
-}: {
- error: unknown;
- filteredSites: SyncSite[];
- isLoading: boolean;
- onSelectSite: ( siteId: number ) => void;
- searchQuery: string;
- selectedSiteId: number | null;
-} ) {
- if ( isLoading ) {
- return (
-
-
-
-
- );
- }
-
- if ( error ) {
- return (
-
- { __( 'Unable to load WordPress.com sites.' ) }
-
- );
- }
-
- if ( filteredSites.length === 0 ) {
- return (
-
- { searchQuery
- ? __( 'No sites match your search.' )
- : __( 'No WordPress.com sites are available to connect.' ) }
-
- );
- }
-
- return (
-
- { filteredSites.map( ( candidate ) => (
- onSelectSite( candidate.id ) }
- />
- ) ) }
-
- );
-}
-
-function getConnectSiteDescription( site: SyncSite ) {
- const provider = site.isPressable ? __( 'Pressable' ) : __( 'WordPress.com' );
- const environment = site.isStaging ? __( 'Staging' ) : __( 'Production' );
- return `${ stripProtocol( site.url ) } - ${ provider } - ${ environment }`;
-}
-
-function DetailsRow( {
- label,
- status,
- statusText,
- actions,
-}: {
- label: string;
- status: SiteStatusTone;
- statusText: ReactNode;
- actions: ReactNode;
-} ) {
- return (
-
-
-
{ label }
-
{ statusText }
-
-
{ actions }
-
- );
-}
-
-function OpenButton( {
- label,
- disabled,
- onClick,
-}: {
- label: string;
- disabled?: boolean;
- onClick: () => void;
-} ) {
- return (
-
- );
-}
-
-type SiteStatusTone = 'running' | 'transitioning' | 'stopped';
-
-function StatusLine( { tone, children }: { tone: SiteStatusTone; children: ReactNode } ) {
- return (
-
-
- { children }
-
- );
-}
-
-function SiteBadge( {
- site,
- size,
- status,
-}: {
- site: SiteDetails;
- size: 'panel';
- status?: SiteStatusTone;
-} ) {
- const hasSiteIcon = Boolean( site.siteIcon );
- const content = hasSiteIcon ? (
-
- ) : (
- { getSiteInitials( site.name ) }
- );
-
- return (
-
- { content }
- { status ? (
-
- ) : null }
-
- );
-}
-
-function getSiteIconSeed( site: SiteDetails ) {
- return `${ site.id }:${ site.name }:${ site.path }`;
-}
-
-function getSiteInitials( name: string ) {
- const words = name.trim().split( /\s+/ ).filter( Boolean );
- const initials =
- words.length >= 2
- ? `${ words[ 0 ].charAt( 0 ) }${ words[ 1 ].charAt( 0 ) }`
- : words[ 0 ]?.slice( 0, 2 );
- return ( initials || 'S' ).toUpperCase();
-}
-
-function getHeaderStatusText( status: SiteStatusTone, site: SiteDetails ) {
- if ( status === 'transitioning' ) {
- return site.running ? __( 'Stopping site' ) : __( 'Starting site' );
- }
- return site.running ? __( 'Running on localhost' ) : __( 'Stopped' );
-}
-
-function getLocalStatusText( status: SiteStatusTone, site: SiteDetails ) {
- if ( status === 'transitioning' ) {
- return site.running ? __( 'Stopping...' ) : __( 'Starting...' );
- }
- if ( site.running ) {
- return site.customDomain
- ? sprintf(
- /* translators: %s: the local custom domain for the site. */
- __( 'Running at %s' ),
- getSiteDisplayUrl( site )
- )
- : __( 'Running on localhost' );
- }
- return __( 'Stopped' );
-}
-
-function getServerButtonLabel( isRunning: boolean, isStarting: boolean, isStopping: boolean ) {
- if ( isStarting ) {
- return __( 'Starting...' );
- }
- if ( isStopping ) {
- return __( 'Stopping...' );
- }
- return isRunning ? __( 'Stop' ) : __( 'Start' );
-}
-
-function formatRelativeTimestamp( timestamp: number ) {
- return formatRelativeTime( new Date( timestamp ).toISOString() );
-}
diff --git a/apps/ui/src/ui-desks/chrome/site-details-dropdown/style.module.css b/apps/ui/src/ui-desks/chrome/site-details-dropdown/style.module.css
deleted file mode 100644
index 41c1766318..0000000000
--- a/apps/ui/src/ui-desks/chrome/site-details-dropdown/style.module.css
+++ /dev/null
@@ -1,411 +0,0 @@
-.detailsTrigger {
- position: relative;
-}
-
-.detailsTrigger::after {
- content: '';
- position: absolute;
- right: 7px;
- bottom: 7px;
- width: 7px;
- height: 7px;
- border: 2px solid var(--ui-desks-material, #fff);
- border-radius: 999px;
- background: rgba(15, 23, 42, 0.32);
-}
-
-.detailsTrigger[data-status='running']::after {
- background: #57c565;
-}
-
-.detailsTrigger[data-status='transitioning']::after {
- background: #d69e2e;
-}
-
-.popup {
- box-sizing: border-box;
- width: min(418px, calc(100vw - 32px));
- padding: 12px;
- gap: 0;
- border-radius: var(--ui-desks-radius-surface, 18px);
- corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- -webkit-corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
-}
-
-.header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 16px;
- min-width: 0;
- padding: 4px 4px 10px;
-}
-
-.headerIdentity {
- display: flex;
- align-items: center;
- gap: 10px;
- min-width: 0;
-}
-
-.headerText {
- display: flex;
- flex-direction: column;
- min-width: 0;
-}
-
-.headerTitle {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- color: var(--ui-desks-text, #14171a);
- font-size: 13px;
- line-height: 18px;
- font-weight: 600;
-}
-
-.headerActions {
- display: flex;
- align-items: center;
- gap: 4px;
- flex: 0 0 auto;
-}
-
-.serverButton {
- flex: 0 0 auto;
- gap: 6px;
-}
-
-.stopGlyph {
- width: 7px;
- height: 7px;
- border-radius: 2px;
- background: currentColor;
-}
-
-.startGlyph {
- width: 0;
- height: 0;
- margin-left: 2px;
- border-top: 5px solid transparent;
- border-bottom: 5px solid transparent;
- border-left: 8px solid currentColor;
-}
-
-.divider {
- height: 1px;
- margin: 0 0 6px;
- background: var(--ui-desks-divider, rgba(15, 23, 42, 0.06));
-}
-
-.rows {
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-
-.footerActions {
- display: flex;
- padding: 6px 4px 0;
-}
-
-.deleteButton {
- width: 100%;
- justify-content: flex-start;
- color: #b32d2e;
-}
-
-.deleteButton:hover:not(:disabled) {
- background: rgba(179, 45, 46, 0.08);
-}
-
-.deleteDialogContent {
- display: flex;
- flex-direction: column;
- gap: 12px;
-}
-
-.deleteDialogText {
- margin: 0;
- color: var(--ui-desks-muted, #6b7280);
- font-size: 13px;
- line-height: 18px;
-}
-
-.deleteDialogCheckbox {
- display: flex;
- align-items: center;
- gap: 8px;
- color: var(--ui-desks-text, #14171a);
- font-size: 13px;
- line-height: 18px;
-}
-
-.deleteDialogCheckbox input {
- flex: 0 0 auto;
-}
-
-.confirmDeleteButton {
- --button-background: #b32d2e;
- --button-foreground: #fff;
-}
-
-.confirmDeleteButton:hover:not(:disabled) {
- --button-background: #8a2424;
-}
-
-.connectDialog {
- width: min(640px, calc(100vw - 48px));
-}
-
-.connectDialogContent {
- display: flex;
- flex-direction: column;
- gap: 12px;
- min-height: 268px;
-}
-
-.connectSearchRow {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.connectSearchInput {
- width: 100%;
- min-width: 0;
- height: 36px;
- box-sizing: border-box;
- padding: 0 12px;
- border: 1px solid rgba(15, 23, 42, 0.1);
- border-radius: var(--ui-desks-radius-button, 12px);
- corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- -webkit-corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- background: rgba(255, 255, 255, 0.56);
- color: var(--ui-desks-text, #14171a);
- font: inherit;
- font-size: 13px;
- line-height: 18px;
- outline: none;
-}
-
-.connectSearchInput::placeholder {
- color: var(--ui-desks-muted, #6b7280);
-}
-
-.connectSearchInput:focus {
- border-color: var(--ui-desks-focus, #3858e9);
- box-shadow: 0 0 0 1px var(--ui-desks-focus, #3858e9);
-}
-
-.connectSitesPanel {
- min-height: 0;
- max-height: 320px;
- overflow-y: auto;
- border: 1px solid rgba(15, 23, 42, 0.08);
- border-radius: var(--ui-desks-radius-surface, 18px);
- corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- -webkit-corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- background: rgba(255, 255, 255, 0.34);
-}
-
-.connectSitesList {
- padding: 6px;
-}
-
-.connectLoading {
- display: flex;
- flex-direction: column;
- gap: 14px;
- padding: 16px;
-}
-
-.connectEmptyState,
-.connectAuthPrompt {
- display: flex;
- min-height: 180px;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 12px;
- padding: 24px;
- color: var(--ui-desks-muted, #6b7280);
- font-size: 13px;
- line-height: 18px;
- text-align: center;
-}
-
-.connectAuthPrompt p {
- max-width: 34ch;
- margin: 0;
-}
-
-.connectFooter {
- justify-content: space-between;
- gap: 12px;
-}
-
-.connectFooterActions {
- display: flex;
- justify-content: flex-end;
- gap: 8px;
-}
-
-.createLiveSiteButton {
- max-width: 280px;
- justify-content: flex-start;
-}
-
-.row {
- display: grid;
- grid-template-columns: minmax(120px, 1fr) auto;
- align-items: center;
- gap: 12px;
- min-height: 54px;
- padding: 4px 4px;
-}
-
-.rowText {
- display: flex;
- flex-direction: column;
- min-width: 0;
-}
-
-.rowLabel {
- color: var(--ui-desks-text, #14171a);
- font-size: 14px;
- line-height: 20px;
- font-weight: 600;
-}
-
-.rowActions {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- gap: 4px;
- min-width: 0;
-}
-
-.syncButton,
-.openButton,
-.settingsButton {
- white-space: nowrap;
-}
-
-.openButton {
- min-width: 68px;
-}
-
-.statusLine {
- display: flex;
- align-items: center;
- gap: 5px;
- min-width: 0;
- color: var(--ui-desks-muted, #6b7280);
- font-size: 12px;
- line-height: 16px;
-}
-
-.statusDot {
- width: 5px;
- height: 5px;
- border-radius: 999px;
- background: rgba(15, 23, 42, 0.32);
- flex: 0 0 auto;
-}
-
-.statusDot_running {
- background: #57c565;
-}
-
-.statusDot_transitioning {
- background: #d69e2e;
-}
-
-.statusDot_stopped {
- background: rgba(15, 23, 42, 0.28);
-}
-
-.statusText {
- display: inline-flex;
- align-items: center;
- gap: 5px;
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.statusSeparator {
- width: 3px;
- height: 3px;
- border-radius: 999px;
- background: currentColor;
- opacity: 0.65;
-}
-
-.siteBadge {
- position: relative;
- display: inline-grid;
- place-items: center;
- flex: 0 0 auto;
- border-radius: 999px;
- background: #111318;
- color: #fff;
- box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
- overflow: visible;
-}
-
-.siteBadge_panel {
- width: 24px;
- height: 24px;
-}
-
-.siteInitials {
- font-size: 10px;
- line-height: 1;
- font-weight: 700;
- letter-spacing: 0;
-}
-
-.siteBadgeImage {
- width: 100%;
- height: 100%;
- border-radius: inherit;
-}
-
-.badgeStatus {
- position: absolute;
- right: -1px;
- bottom: -1px;
- width: 9px;
- height: 9px;
- border: 2px solid var(--ui-desks-material, #fff);
- border-radius: 999px;
- background: rgba(15, 23, 42, 0.32);
-}
-
-.badgeStatus_running {
- background: #57c565;
-}
-
-.badgeStatus_transitioning {
- background: #d69e2e;
-}
-
-.badgeStatus_stopped {
- background: rgba(15, 23, 42, 0.32);
-}
-
-@media (max-width: 460px) {
- .row {
- grid-template-columns: 1fr;
- gap: 8px;
- padding-block: 8px;
- }
-
- .rowActions {
- justify-content: flex-start;
- flex-wrap: wrap;
- }
-}
diff --git a/apps/ui/src/ui-desks/chrome/site-map-button.tsx b/apps/ui/src/ui-desks/chrome/site-map-button.tsx
deleted file mode 100644
index d49091f258..0000000000
--- a/apps/ui/src/ui-desks/chrome/site-map-button.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import { __ } from '@wordpress/i18n';
-import { category } from '@wordpress/icons';
-import { useSites } from '@/data/queries/use-sites';
-import { Button } from '@/ui-desks/components';
-
-interface DeskSiteMapButtonProps {
- siteId: string;
- open: boolean;
- onToggle: () => void;
-}
-
-export function DeskSiteMapButton( { siteId, open, onToggle }: DeskSiteMapButtonProps ) {
- const { data: sites, isLoading } = useSites();
- const site = sites?.find( ( candidate ) => candidate.id === siteId );
- const canOpen = Boolean( site?.running );
- const disabled = ! open && ( isLoading || ! canOpen );
-
- return (
-
- );
-}
-
-function getTooltipLabel( isLoading: boolean, hasSite: boolean, canOpen: boolean ) {
- if ( isLoading ) {
- return __( 'Checking site…' );
- }
-
- if ( ! hasSite ) {
- return __( 'Site map unavailable' );
- }
-
- if ( ! canOpen ) {
- return __( 'Start the site to view its site map' );
- }
-
- return __( 'Site map' );
-}
diff --git a/apps/ui/src/ui-desks/chrome/site-map-title/index.tsx b/apps/ui/src/ui-desks/chrome/site-map-title/index.tsx
deleted file mode 100644
index cbb1f21eb0..0000000000
--- a/apps/ui/src/ui-desks/chrome/site-map-title/index.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { __, _n, sprintf } from '@wordpress/i18n';
-import styles from './style.module.css';
-
-export function DeskSiteMapTitle( { pageCount }: { pageCount?: number } ) {
- return (
-
-
{ __( 'Site map' ) }
- { pageCount !== undefined && (
- { sprintf( _n( '%d page', '%d pages', pageCount ), pageCount ) }
- ) }
-
- );
-}
diff --git a/apps/ui/src/ui-desks/chrome/site-map-title/style.module.css b/apps/ui/src/ui-desks/chrome/site-map-title/style.module.css
deleted file mode 100644
index dd74235c3c..0000000000
--- a/apps/ui/src/ui-desks/chrome/site-map-title/style.module.css
+++ /dev/null
@@ -1,29 +0,0 @@
-.siteMapTitle {
- display: flex;
- min-width: 0;
- align-items: baseline;
- justify-content: center;
- gap: 8px;
- color: var(--wpds-color-fg-content-neutral);
- text-align: center;
- white-space: nowrap;
-}
-
-.siteMapTitle h1,
-.siteMapTitle span {
- margin: 0;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.siteMapTitle h1 {
- font-size: var(--wpds-typography-font-size-md);
- font-weight: var(--wpds-typography-font-weight-semibold, 600);
- line-height: var(--wpds-typography-line-height-md);
-}
-
-.siteMapTitle span {
- color: var(--wpds-color-fg-content-neutral-weak, rgba(15, 23, 42, 0.55));
- font-size: var(--wpds-typography-font-size-sm);
- line-height: var(--wpds-typography-line-height-sm);
-}
diff --git a/apps/ui/src/ui-desks/chrome/toolbar-layout/index.test.ts b/apps/ui/src/ui-desks/chrome/toolbar-layout/index.test.ts
deleted file mode 100644
index f62f734515..0000000000
--- a/apps/ui/src/ui-desks/chrome/toolbar-layout/index.test.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { describe, expect, it } from 'vitest';
-import {
- DEFAULT_DESK_TOOLBAR_LAYOUT,
- getDeskToolbarButtonSide,
- moveDeskToolbarButton,
- normalizeDeskToolbarLayout,
- normalizeDeskToolbarSettings,
-} from './index';
-
-describe( 'desk toolbar layout', () => {
- it( 'normalizes toolbar layouts and appends missing buttons to the right side', () => {
- expect(
- normalizeDeskToolbarLayout( {
- left: [ 'settings', 'chat', 'chat', 'unknown' ],
- right: [ 'create' ],
- } )
- ).toEqual( {
- left: [ 'settings', 'chat' ],
- right: [ 'create', 'site-map' ],
- } );
- } );
-
- it( 'falls back to the default toolbar layout for invalid shapes', () => {
- expect( normalizeDeskToolbarLayout( { left: [ 'chat' ], right: 'settings' } ) ).toEqual(
- DEFAULT_DESK_TOOLBAR_LAYOUT
- );
- } );
-
- it( 'normalizes persisted settings for toolbar rendering', () => {
- expect(
- normalizeDeskToolbarSettings( {
- version: 1,
- updatedAt: '2026-05-11T00:00:00.000Z',
- showSiteName: true,
- toolbarLayout: {
- left: [ 'settings' ],
- right: [ 'unknown', 'chat' ],
- },
- } )
- ).toEqual( {
- version: 1,
- updatedAt: '2026-05-11T00:00:00.000Z',
- showSiteName: true,
- toolbarLayout: {
- left: [ 'settings' ],
- right: [ 'chat', 'create', 'site-map' ],
- },
- } );
- } );
-
- it( 'moves a toolbar button between sides', () => {
- expect(
- moveDeskToolbarButton( DEFAULT_DESK_TOOLBAR_LAYOUT, 'settings', 'left', 'chat' )
- ).toEqual( {
- left: [ 'settings', 'chat', 'create' ],
- right: [ 'site-map' ],
- } );
- } );
-
- it( 'resolves the side for a toolbar button from normalized layout data', () => {
- expect(
- getDeskToolbarButtonSide(
- {
- left: [ 'settings' ],
- right: [ 'chat', 'create' ],
- },
- 'chat'
- )
- ).toBe( 'right' );
- expect( getDeskToolbarButtonSide( DEFAULT_DESK_TOOLBAR_LAYOUT, 'settings' ) ).toBe( 'right' );
- } );
-} );
diff --git a/apps/ui/src/ui-desks/chrome/toolbar-layout/index.ts b/apps/ui/src/ui-desks/chrome/toolbar-layout/index.ts
deleted file mode 100644
index ab58d9586c..0000000000
--- a/apps/ui/src/ui-desks/chrome/toolbar-layout/index.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import type {
- DeskSettings,
- DeskToolbarLayout as PersistedDeskToolbarLayout,
-} from '@studio/common/types/desk';
-
-export const DESK_TOOLBAR_BUTTONS = [ 'chat', 'create', 'site-map', 'settings' ] as const;
-
-export type DeskToolbarButtonId = ( typeof DESK_TOOLBAR_BUTTONS )[ number ];
-
-export interface DeskToolbarLayout {
- left: DeskToolbarButtonId[];
- right: DeskToolbarButtonId[];
-}
-
-export type DeskToolbarSettings = Omit< DeskSettings, 'toolbarLayout' > & {
- toolbarLayout: DeskToolbarLayout;
-};
-
-export const DEFAULT_DESK_TOOLBAR_LAYOUT: DeskToolbarLayout = {
- left: [ 'chat', 'create' ],
- right: [ 'site-map', 'settings' ],
-};
-
-function cloneToolbarLayout( layout: DeskToolbarLayout ): DeskToolbarLayout {
- return {
- left: [ ...layout.left ],
- right: [ ...layout.right ],
- };
-}
-
-function isRecord( value: unknown ): value is Record< string, unknown > {
- return Boolean( value ) && typeof value === 'object' && ! Array.isArray( value );
-}
-
-function isDeskToolbarButtonId( value: unknown ): value is DeskToolbarButtonId {
- return typeof value === 'string' && DESK_TOOLBAR_BUTTONS.includes( value as DeskToolbarButtonId );
-}
-
-export function normalizeDeskToolbarLayout( value: unknown ): DeskToolbarLayout {
- if ( ! isRecord( value ) || ! Array.isArray( value.left ) || ! Array.isArray( value.right ) ) {
- return cloneToolbarLayout( DEFAULT_DESK_TOOLBAR_LAYOUT );
- }
-
- const layout = value as Record< keyof PersistedDeskToolbarLayout, unknown[] >;
- const next: DeskToolbarLayout = { left: [], right: [] };
- const seen = new Set< DeskToolbarButtonId >();
-
- for ( const side of [ 'left', 'right' ] as const ) {
- for ( const buttonId of layout[ side ] ) {
- if ( isDeskToolbarButtonId( buttonId ) && ! seen.has( buttonId ) ) {
- next[ side ].push( buttonId );
- seen.add( buttonId );
- }
- }
- }
-
- for ( const buttonId of DESK_TOOLBAR_BUTTONS ) {
- if ( ! seen.has( buttonId ) ) {
- next.right.push( buttonId );
- }
- }
-
- return next;
-}
-
-export function normalizeDeskToolbarSettings( settings: DeskSettings ): DeskToolbarSettings {
- return {
- ...settings,
- toolbarLayout: normalizeDeskToolbarLayout( settings.toolbarLayout ),
- };
-}
-
-export function getDeskToolbarButtonSide(
- layout: PersistedDeskToolbarLayout,
- buttonId: DeskToolbarButtonId
-): keyof DeskToolbarLayout {
- const normalized = normalizeDeskToolbarLayout( layout );
- return normalized.left.includes( buttonId ) ? 'left' : 'right';
-}
-
-export function moveDeskToolbarButton(
- layout: PersistedDeskToolbarLayout,
- buttonId: DeskToolbarButtonId,
- side: keyof DeskToolbarLayout,
- beforeButtonId: DeskToolbarButtonId | null
-): DeskToolbarLayout {
- const next = normalizeDeskToolbarLayout( layout );
- const left = next.left.filter( ( id ) => id !== buttonId );
- const right = next.right.filter( ( id ) => id !== buttonId );
- const target = side === 'left' ? left : right;
-
- if ( beforeButtonId && beforeButtonId !== buttonId ) {
- const index = target.indexOf( beforeButtonId );
- if ( index >= 0 ) {
- target.splice( index, 0, buttonId );
- } else {
- target.push( buttonId );
- }
- } else {
- target.push( buttonId );
- }
-
- return { left, right };
-}
diff --git a/apps/ui/src/ui-desks/chrome/toolbar-row/index.tsx b/apps/ui/src/ui-desks/chrome/toolbar-row/index.tsx
deleted file mode 100644
index da58a5592b..0000000000
--- a/apps/ui/src/ui-desks/chrome/toolbar-row/index.tsx
+++ /dev/null
@@ -1,171 +0,0 @@
-import { clsx } from 'clsx';
-import { useRef } from 'react';
-import styles from './style.module.css';
-import type { DeskToolbarButtonId, DeskToolbarLayout } from '@/ui-desks/chrome/toolbar-layout';
-import type { Dispatch, DragEvent, ReactNode, SetStateAction } from 'react';
-
-const DRAG_MIME = 'application/x-studio-desk-toolbar-button';
-
-export interface ToolbarDragState {
- draggedId: DeskToolbarButtonId | null;
- insertBeforeId: DeskToolbarButtonId | null;
- insertSide: keyof DeskToolbarLayout | null;
-}
-
-export const EMPTY_DRAG_STATE: ToolbarDragState = {
- draggedId: null,
- insertBeforeId: null,
- insertSide: null,
-};
-
-interface ToolbarRowProps {
- side: keyof DeskToolbarLayout;
- buttonIds: DeskToolbarButtonId[];
- editing: boolean;
- renderButton: ( buttonId: DeskToolbarButtonId ) => ReactNode;
- onReorder: (
- buttonId: DeskToolbarButtonId,
- side: keyof DeskToolbarLayout,
- beforeButtonId: DeskToolbarButtonId | null
- ) => void;
- dragState: ToolbarDragState;
- setDragState: Dispatch< SetStateAction< ToolbarDragState > >;
- clearDragState: () => void;
- leading?: ReactNode;
-}
-
-export function ToolbarRow( {
- side,
- buttonIds,
- editing,
- renderButton,
- onReorder,
- dragState,
- setDragState,
- clearDragState,
- leading,
-}: ToolbarRowProps ) {
- const rowRef = useRef< HTMLDivElement | null >( null );
-
- const getInsertionTarget = ( clientX: number ) => {
- const row = rowRef.current;
- if ( ! row ) {
- return null;
- }
-
- const slots = row.querySelectorAll< HTMLElement >( `[data-toolbar-button-id]` );
- for ( const slot of slots ) {
- if ( slot.dataset.dragging === 'true' ) {
- continue;
- }
-
- const id = slot.dataset.toolbarButtonId as DeskToolbarButtonId | undefined;
- if ( ! id ) {
- continue;
- }
-
- const rect = slot.getBoundingClientRect();
- if ( clientX < rect.left + rect.width / 2 ) {
- return id;
- }
- }
-
- return null;
- };
-
- const onDragStart = ( event: DragEvent, buttonId: DeskToolbarButtonId ) => {
- event.dataTransfer.setData( DRAG_MIME, buttonId );
- event.dataTransfer.effectAllowed = 'move';
- window.setTimeout( () => {
- setDragState( { draggedId: buttonId, insertBeforeId: null, insertSide: null } );
- }, 0 );
- };
-
- const onDragOver = ( event: DragEvent ) => {
- if ( ! editing || ! event.dataTransfer.types.includes( DRAG_MIME ) ) {
- return;
- }
-
- event.preventDefault();
- event.dataTransfer.dropEffect = 'move';
- const insertBeforeId = getInsertionTarget( event.clientX );
- setDragState( ( previous ) =>
- previous.insertBeforeId === insertBeforeId && previous.insertSide === side
- ? previous
- : { ...previous, insertBeforeId, insertSide: side }
- );
- };
-
- const onDrop = ( event: DragEvent ) => {
- if ( ! editing ) {
- return;
- }
-
- const buttonId = event.dataTransfer.getData( DRAG_MIME ) as DeskToolbarButtonId;
- if ( ! buttonId ) {
- return;
- }
-
- event.preventDefault();
- onReorder( buttonId, side, getInsertionTarget( event.clientX ) );
- clearDragState();
- };
-
- const isTarget = dragState.insertSide === side;
- const slots: ReactNode[] = [];
- let ghostInserted = false;
-
- for ( const buttonId of buttonIds ) {
- const button = renderButton( buttonId );
- if ( ! button ) {
- continue;
- }
-
- if (
- isTarget &&
- ! ghostInserted &&
- dragState.insertBeforeId === buttonId &&
- dragState.draggedId !== buttonId
- ) {
- slots.push( );
- ghostInserted = true;
- }
-
- const isDragged = dragState.draggedId === buttonId;
- slots.push(
- onDragStart( event, buttonId ) }
- onDragEnd={ clearDragState }
- >
- { button }
-
- );
- }
-
- if ( isTarget && ! ghostInserted ) {
- slots.push( );
- }
-
- return (
-
- { leading }
- { slots }
-
- );
-}
-
-function DropGhost() {
- return ;
-}
diff --git a/apps/ui/src/ui-desks/chrome/toolbar-row/style.module.css b/apps/ui/src/ui-desks/chrome/toolbar-row/style.module.css
deleted file mode 100644
index fc3c832edf..0000000000
--- a/apps/ui/src/ui-desks/chrome/toolbar-row/style.module.css
+++ /dev/null
@@ -1,67 +0,0 @@
-.toolbarRow {
- display: flex;
- align-items: center;
- gap: var(--wpds-dimension-padding-md);
-}
-
-.toolbarRow[data-editing='true'] {
- padding: 10px;
- margin: -10px;
- border-radius: 22px;
- corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- -webkit-corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- outline: 2px dashed rgba(159, 159, 159, 0.85);
- outline-offset: -2px;
-}
-
-.toolbarSlot {
- display: inline-flex;
- align-items: center;
-}
-
-.toolbarSlot[draggable='true'] {
- cursor: grab;
-}
-
-.toolbarSlot[draggable='true']:active {
- cursor: grabbing;
-}
-
-.toolbarSlot[data-dragging='true'] {
- display: none;
-}
-
-.toolbarRow[data-editing='true'] .toolbarSlot > * {
- pointer-events: none;
-}
-
-.toolbarRow[data-editing='true'] .toolbarSlot:not(.toolbarDropGhost) > * {
- animation: toolbar-jiggle 0.55s ease-in-out infinite;
- transform-origin: center;
- box-shadow:
- 0 2px 4px rgba(15, 23, 42, 0.06),
- 0 18px 32px rgba(15, 23, 42, 0.18);
-}
-
-.toolbarDropGhost {
- box-sizing: border-box;
- width: 52px;
- height: 40px;
- border: 2px dashed rgba(159, 159, 159, 0.6);
- border-radius: var(--ui-desks-radius-control, 16px);
- corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- -webkit-corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- background: rgba(15, 23, 42, 0.04);
- pointer-events: none;
-}
-
-@keyframes toolbar-jiggle {
- 0%,
- 100% {
- transform: rotate(-3deg);
- }
-
- 50% {
- transform: rotate(3deg);
- }
-}
diff --git a/apps/ui/src/ui-desks/chrome/user-menu/index.tsx b/apps/ui/src/ui-desks/chrome/user-menu/index.tsx
deleted file mode 100644
index e211e18dfa..0000000000
--- a/apps/ui/src/ui-desks/chrome/user-menu/index.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import { useNavigate } from '@tanstack/react-router';
-import { __, sprintf } from '@wordpress/i18n';
-import { chevronDownSmall, commentAuthorAvatar } from '@wordpress/icons';
-import { Icon } from '@wordpress/ui';
-import { Gravatar } from '@/components/gravatar';
-import { SiteIcon } from '@/components/site-icon';
-import { useConnector } from '@/data/core';
-import { useAuthUser, useLogin, useLogout } from '@/data/queries/use-auth-user';
-import { useSites } from '@/data/queries/use-sites';
-import { useUserPreferences } from '@/data/queries/use-user-preferences';
-import { usePrefersColorScheme } from '@/hooks/use-prefers-color-scheme';
-import { Button, Menu } from '@/ui-desks/components';
-import styles from './style.module.css';
-import type { SiteDetails } from '@/data/core';
-
-const WPCOM_PROFILE_URL = 'https://wordpress.com/me';
-
-interface DeskMenuProps {
- siteId?: string;
- disabled?: boolean;
- showSiteName?: boolean;
-}
-
-function getSiteIconSeed( site: SiteDetails ) {
- return `${ site.id }:${ site.name }:${ site.path }`;
-}
-
-export function DeskMenu( { siteId, disabled = false, showSiteName = true }: DeskMenuProps ) {
- const navigate = useNavigate();
- const connector = useConnector();
- const { data: user } = useAuthUser();
- const { data: preferences } = useUserPreferences();
- const { data: sites, isLoading: sitesLoading } = useSites();
- const login = useLogin();
- const logout = useLogout();
- const effectiveScheme = usePrefersColorScheme();
- const savedScheme = preferences?.colorScheme;
- const themeIsDark =
- savedScheme === 'dark' || ( savedScheme !== 'light' && effectiveScheme === 'dark' );
- const activeSite = sites?.find( ( candidate ) => candidate.id === siteId );
- const activeSiteName = activeSite?.name ?? __( 'Site' );
- const activeSiteIconSeed = activeSite ? getSiteIconSeed( activeSite ) : siteId;
- const switcherSites = activeSite
- ? [ activeSite, ...( sites ?? [] ).filter( ( candidate ) => candidate.id !== activeSite.id ) ]
- : sites ?? [];
-
- const openLink = ( url: string ) => {
- void connector.openExternalUrl( url );
- };
-
- const openUserDashboard = () => {
- void navigate( { to: '/' } );
- };
-
- const openSite = ( nextSiteId: string ) => {
- if ( nextSiteId === siteId ) {
- return;
- }
- void navigate( { to: '/sites/$siteId', params: { siteId: nextSiteId } } );
- };
-
- const trigger = siteId ? (
-
- ) : (
-
- );
-
- return (
-
-
-
- { user ? (
- openLink( WPCOM_PROFILE_URL ) }
- >
- { user.email }
-
- ) : (
- login.mutate() }>
- { login.isPending ? __( 'Logging in…' ) : __( 'Log in with WordPress.com' ) }
-
- ) }
- { __( 'User desk' ) }
-
- { sitesLoading ? (
- { __( 'Loading…' ) }
- ) : switcherSites.length > 0 ? (
- switcherSites.map( ( site ) => (
- openSite( site.id ) }
- >
- { site.name }
-
- ) )
- ) : (
- { __( 'No sites yet' ) }
- ) }
- { user ? (
- <>
-
- logout.mutate() }>{ __( 'Log out' ) }
- >
- ) : null }
-
-
- );
-}
diff --git a/apps/ui/src/ui-desks/chrome/user-menu/style.module.css b/apps/ui/src/ui-desks/chrome/user-menu/style.module.css
deleted file mode 100644
index 96da8153bd..0000000000
--- a/apps/ui/src/ui-desks/chrome/user-menu/style.module.css
+++ /dev/null
@@ -1,65 +0,0 @@
-.siteTrigger {
- box-sizing: border-box;
- max-width: min(320px, 100%);
- min-width: 52px;
-}
-
-.siteIcon {
- width: 24px;
- height: 24px;
- border-radius: 9px;
- corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- -webkit-corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- flex-shrink: 0;
-}
-
-.siteName {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- font-size: 13px;
- line-height: 20px;
- font-weight: var(--wpds-typography-font-weight-medium);
-}
-
-.siteCaret {
- color: rgba(15, 23, 42, 0.4);
- margin-left: 2px;
-}
-
-.siteCaret svg {
- fill: currentColor;
-}
-
-.avatar,
-.loginAvatar {
- width: 24px;
- height: 24px;
-}
-
-.loginAvatar {
- color: var(--wpds-color-fg-content-neutral-weak);
-}
-
-.loginAvatar svg {
- fill: currentColor;
-}
-
-.popup {
- min-width: 220px;
-}
-
-.email {
- padding: 8px 12px 6px;
- font-weight: 400;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.popup .email {
- color: rgba(15, 23, 42, 0.42);
- font-size: 11px;
- line-height: 14px;
-}
diff --git a/apps/ui/src/ui-desks/components/button/index.tsx b/apps/ui/src/ui-desks/components/button/index.tsx
deleted file mode 100644
index d83a541a9e..0000000000
--- a/apps/ui/src/ui-desks/components/button/index.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-import { Icon, Tooltip } from '@wordpress/ui';
-import { clsx } from 'clsx';
-import { forwardRef } from 'react';
-import styles from './style.module.css';
-import type { ComponentProps, ComponentPropsWithoutRef, ReactNode } from 'react';
-
-type ButtonVariant = 'chrome' | 'quiet' | 'filled';
-type ButtonTone = 'neutral' | 'primary' | 'inverse';
-type ButtonSize = 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge';
-type ButtonIntent = 'default' | 'chat';
-
-const ICON_SIZE_BY_BUTTON_SIZE: Record< ButtonSize, number > = {
- xsmall: 16,
- small: 18,
- medium: 24,
- large: 24,
- xlarge: 24,
-};
-
-type ButtonProps = Omit< ComponentPropsWithoutRef< 'button' >, 'children' > & {
- children?: ReactNode;
- icon?: ComponentProps< typeof Icon >[ 'icon' ];
- intent?: ButtonIntent;
- label: string;
- size?: ButtonSize;
- tone?: ButtonTone;
- tooltipLabel?: string | false;
- tooltipSide?: 'top' | 'right' | 'bottom' | 'left';
- variant?: ButtonVariant;
-};
-
-export const Button = forwardRef< HTMLButtonElement, ButtonProps >( function Button(
- {
- children,
- className,
- disabled,
- icon,
- intent = 'default',
- label,
- size = 'large',
- tone = 'neutral',
- tooltipLabel,
- tooltipSide,
- type = 'button',
- variant = 'chrome',
- ...props
- },
- ref
-) {
- const resolvedTooltipLabel = tooltipLabel ?? ( icon && ! children ? label : false );
- const isIconOnly = Boolean( icon && ! children );
- const buttonClassName = clsx(
- styles.button,
- styles[ variant ],
- styles[ size ],
- tone !== 'neutral' && styles[ tone ],
- intent !== 'default' && styles[ `${ intent }Intent` ],
- className
- );
- const resolvedTooltipSide = tooltipSide ?? ( variant === 'quiet' ? 'top' : 'bottom' );
- const content = (
- <>
- { icon ? (
-
- ) : null }
- { children }
- >
- );
-
- if ( resolvedTooltipLabel === false ) {
- return (
-
- );
- }
-
- return (
-
-
-
- }
- >
- { content }
-
- }>
- { resolvedTooltipLabel }
-
-
-
- );
-} );
diff --git a/apps/ui/src/ui-desks/components/button/style.module.css b/apps/ui/src/ui-desks/components/button/style.module.css
deleted file mode 100644
index 65d4701b40..0000000000
--- a/apps/ui/src/ui-desks/components/button/style.module.css
+++ /dev/null
@@ -1,232 +0,0 @@
-.button {
- --button-background: transparent;
- --button-foreground: var(--ui-desks-text, #14171a);
- --wp-ui-button-background-color: var(--button-background);
- --wp-ui-button-background-color-active: var(--button-background);
- --wp-ui-button-border-color: transparent;
- --wp-ui-button-border-color-active: transparent;
- --wp-ui-button-foreground-color: var(--button-foreground);
- --wp-ui-button-foreground-color-active: var(--button-foreground);
-
- min-width: 0;
- border: 0;
- border-radius: var(--ui-desks-radius-button, 12px);
- corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- -webkit-corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- background-color: var(--button-background);
- color: var(--button-foreground);
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- font: inherit;
- -webkit-font-smoothing: antialiased;
- -webkit-app-region: no-drag;
- transition:
- background 140ms ease,
- box-shadow 160ms ease,
- transform 160ms ease,
- opacity 200ms ease;
-}
-
-.button:focus:not(:focus-visible) {
- outline: none;
- background-color: var(--button-background);
- border-color: transparent;
-}
-
-.button:disabled {
- cursor: default;
- opacity: 0.5;
-}
-
-.button:focus-visible {
- outline: 2px solid var(--ui-desks-focus, #3858e9);
- outline-offset: 2px;
-}
-
-.xsmall {
- min-height: 22px;
- padding: 0 6px;
- gap: 4px;
- font-size: 12px;
- line-height: 16px;
-}
-
-.small {
- min-height: 32px;
- padding: 0 8px;
- gap: 4px;
- font-size: 12px;
- line-height: 16px;
-}
-
-.medium {
- min-height: 36px;
- padding: 0 12px;
- gap: 6px;
- font-size: 13px;
- line-height: 18px;
-}
-
-.large {
- min-height: 40px;
- padding: 0 14px;
- gap: 6px;
- font-size: 13px;
- line-height: 18px;
-}
-
-.xlarge {
- min-height: 46px;
- padding: 0 14px;
- gap: 6px;
- font-size: 13px;
- line-height: 18px;
-}
-
-.quiet[data-icon-only='true'].xsmall,
-.filled[data-icon-only='true'].xsmall {
- width: 22px;
- min-width: 22px;
- height: 22px;
- padding: 0;
-}
-
-.quiet[data-icon-only='true'].small,
-.filled[data-icon-only='true'].small {
- width: 32px;
- min-width: 32px;
- height: 32px;
- padding: 0;
-}
-
-.quiet[data-icon-only='true'].medium,
-.filled[data-icon-only='true'].medium {
- width: 36px;
- min-width: 36px;
- height: 36px;
- padding: 0;
-}
-
-.quiet[data-icon-only='true'].large,
-.filled[data-icon-only='true'].large {
- width: 40px;
- min-width: 40px;
- height: 40px;
- padding: 0;
-}
-
-.quiet[data-icon-only='true'].xlarge,
-.filled[data-icon-only='true'].xlarge {
- width: 46px;
- min-width: 46px;
- height: 46px;
- padding: 0;
-}
-
-.chrome {
- min-width: 52px;
- --button-background: var(--ui-desks-material, #fff);
- border-radius: var(--ui-desks-radius-control, 16px);
- box-shadow: var(
- --ui-desks-shadow-control,
- 0 1px 2px rgba(15, 23, 42, 0.04),
- 0 8px 24px rgba(15, 23, 42, 0.06)
- );
-}
-
-.chrome:hover:not(:disabled) {
- box-shadow: var(
- --ui-desks-shadow-control-raised,
- 0 1px 2px rgba(15, 23, 42, 0.04),
- 0 18px 44px rgba(15, 23, 42, 0.1)
- );
- transform: translateY(-1px);
-}
-
-.chrome:active:not(:disabled) {
- transform: translateY(0);
-}
-
-.chrome[aria-pressed='true'] {
- --button-background: var(--ui-desks-text, #14171a);
- --button-foreground: #fff;
-}
-
-.quiet:hover:not(:disabled),
-.quiet[aria-pressed='true'] {
- --button-background: var(--ui-desks-control-hover, rgba(15, 23, 42, 0.07));
-}
-
-.filled {
- --button-background: rgba(15, 23, 42, 0.05);
- justify-content: flex-start;
-}
-
-.filled[data-icon-only='true'] {
- justify-content: center;
-}
-
-.filled:hover:not(:disabled) {
- --button-background: rgba(15, 23, 42, 0.08);
-}
-
-.primary {
- --button-foreground: #3858e9;
-}
-
-.filled.primary,
-.chrome.primary {
- --button-background: #3858e9;
- --button-foreground: #fff;
-}
-
-.filled.inverse,
-.chrome.inverse {
- --button-background: var(--ui-desks-text, #14171a);
- --button-foreground: #fff;
-}
-
-.filled.primary:hover:not(:disabled),
-.chrome.primary:hover:not(:disabled) {
- --button-background: #2448e2;
-}
-
-.filled.inverse:hover:not(:disabled),
-.chrome.inverse:hover:not(:disabled) {
- --button-background: #000;
-}
-
-.filled.primary.chatIntent {
- box-shadow: 0 6px 18px rgba(56, 88, 233, 0.45);
-}
-
-.filled.primary.chatIntent:hover:not(:disabled) {
- --button-background: #3858e9;
- box-shadow: 0 8px 22px rgba(56, 88, 233, 0.55);
- transform: translateY(-1px);
-}
-
-.filled.primary[data-icon-only='true'].large:disabled {
- opacity: 0;
- pointer-events: none;
- box-shadow: none;
- transform: scale(0.6);
-}
-
-.button > span {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.button svg {
- fill: currentColor;
- flex: 0 0 auto;
-}
-
-.icon {
- flex: 0 0 auto;
-}
diff --git a/apps/ui/src/ui-desks/components/dialog/index.tsx b/apps/ui/src/ui-desks/components/dialog/index.tsx
deleted file mode 100644
index b05d036265..0000000000
--- a/apps/ui/src/ui-desks/components/dialog/index.tsx
+++ /dev/null
@@ -1,205 +0,0 @@
-import { __ } from '@wordpress/i18n';
-import { closeSmall } from '@wordpress/icons';
-import { clsx } from 'clsx';
-import { useEffect, useRef } from 'react';
-import { Button } from '../button';
-import styles from './style.module.css';
-import type { ComponentPropsWithoutRef, ReactNode } from 'react';
-
-type DialogSize = 'default' | 'narrow' | 'small';
-type DialogGap = 'default' | 'compact';
-
-type DialogProps = Omit<
- ComponentPropsWithoutRef< 'form' >,
- 'children' | 'onKeyDown' | 'onPointerDown'
-> & {
- ariaLabel: string;
- as?: 'div' | 'form';
- children: ReactNode;
- gap?: DialogGap;
- onClose: () => void;
- open?: boolean;
- size?: DialogSize;
-};
-
-export function Dialog( {
- ariaLabel,
- as = 'div',
- children,
- className,
- gap = 'default',
- onClose,
- open = true,
- size = 'default',
- ...props
-}: DialogProps ) {
- const dialogRef = useRef< HTMLDivElement | HTMLFormElement | null >( null );
-
- useEffect( () => {
- if ( ! open ) {
- return;
- }
-
- const focusFrame = window.requestAnimationFrame( () => {
- if ( dialogRef.current && ! dialogRef.current.contains( document.activeElement ) ) {
- dialogRef.current.focus();
- }
- } );
- const handleKeyDown = ( event: KeyboardEvent ) => {
- if ( event.key === 'Escape' && ! event.defaultPrevented ) {
- event.preventDefault();
- onClose();
- }
- };
-
- document.addEventListener( 'keydown', handleKeyDown );
- return () => {
- window.cancelAnimationFrame( focusFrame );
- document.removeEventListener( 'keydown', handleKeyDown );
- };
- }, [ onClose, open ] );
-
- if ( ! open ) {
- return null;
- }
-
- const dialogProps = {
- ...props,
- className: clsx(
- styles.dialog,
- size === 'narrow' && styles.narrow,
- size === 'small' && styles.small,
- gap === 'compact' && styles.compact,
- className
- ),
- role: 'dialog',
- tabIndex: props.tabIndex ?? -1,
- 'aria-modal': true,
- 'aria-label': ariaLabel,
- };
-
- return (
- {
- event.stopPropagation();
- if ( event.target === event.currentTarget ) {
- onClose();
- }
- } }
- >
- { as === 'form' ? (
-
- ) : (
-
) }
- ref={ ( element ) => {
- dialogRef.current = element;
- } }
- >
- { children }
-
- ) }
-
- );
-}
-
-export function DialogHeader( {
- children,
- className,
-}: {
- children: ReactNode;
- className?: string;
-} ) {
- return { children }
;
-}
-
-export function DialogTitle( {
- children,
- className,
-}: {
- children: ReactNode;
- className?: string;
-} ) {
- return { children }
;
-}
-
-export function DialogCloseButton( {
- className,
- label = __( 'Close' ),
- onClose,
-}: {
- className?: string;
- label?: string;
- onClose: () => void;
-} ) {
- return (
-
- );
-}
-
-export function DialogContent( {
- children,
- className,
-}: {
- children: ReactNode;
- className?: string;
-} ) {
- return { children }
;
-}
-
-export function DialogFooter( {
- align = 'end',
- children,
- className,
-}: {
- align?: 'center' | 'end';
- children: ReactNode;
- className?: string;
-} ) {
- return (
-
- { children }
-
- );
-}
-
-export function DialogRow( {
- align = 'end',
- children,
- className,
-}: {
- align?: 'center' | 'end';
- children: ReactNode;
- className?: string;
-} ) {
- return (
-
- { children }
-
- );
-}
-
-export function DialogError( { children }: { children: ReactNode } ) {
- return { children }
;
-}
-
-export function DialogTip( { children }: { children: ReactNode } ) {
- return { children }
;
-}
-
-export const dialogInputClassName = styles.input;
diff --git a/apps/ui/src/ui-desks/components/dialog/style.module.css b/apps/ui/src/ui-desks/components/dialog/style.module.css
deleted file mode 100644
index 4a22db89e6..0000000000
--- a/apps/ui/src/ui-desks/components/dialog/style.module.css
+++ /dev/null
@@ -1,148 +0,0 @@
-.backdrop {
- position: fixed;
- inset: 0;
- z-index: var( --ui-desks-z-dialog, 700 );
- display: flex;
- align-items: center;
- justify-content: center;
- background: rgba( 15, 23, 42, 0.18 );
- -webkit-backdrop-filter: blur( 8px );
- backdrop-filter: blur( 8px );
- pointer-events: auto;
- animation: fadeIn 200ms ease;
-}
-
-.dialog {
- width: min( 560px, calc( 100vw - 48px ) );
- max-height: min( 80vh, calc( 100vh - 48px ) );
- display: flex;
- flex-direction: column;
- gap: 16px;
- box-sizing: border-box;
- padding: 20px;
- border: 1px solid rgba( 255, 255, 255, 0.7 );
- border-radius: var( --ui-desks-radius-panel, 22px );
- corner-shape: var( --ui-desks-corner-shape, superellipse( 1.42 ) );
- -webkit-corner-shape: var( --ui-desks-corner-shape, superellipse( 1.42 ) );
- background: rgba( 255, 255, 255, 0.62 );
- box-shadow:
- 0 1px 2px rgba( 15, 23, 42, 0.04 ),
- 0 18px 44px rgba( 15, 23, 42, 0.1 );
- color: var( --ui-desks-text, #14171a );
- -webkit-backdrop-filter: saturate( 180% ) blur( 28px );
- backdrop-filter: saturate( 180% ) blur( 28px );
- animation: rise 280ms cubic-bezier( 0.32, 0.72, 0.24, 1 );
-}
-
-.narrow {
- width: min( 520px, calc( 100vw - 48px ) );
-}
-
-.small {
- width: min( 440px, calc( 100vw - 48px ) );
-}
-
-.compact {
- gap: 10px;
-}
-
-.header,
-.footer,
-.row {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.header {
- justify-content: space-between;
-}
-
-.title {
- margin: 0;
- font-size: var( --wpds-typography-font-size-md );
- font-weight: var( --wpds-typography-font-weight-semibold, 600 );
- line-height: var( --wpds-typography-line-height-md );
-}
-
-.closeButton {
- margin-right: -6px;
-}
-
-.content {
- min-height: 0;
-}
-
-.footer {
- justify-content: flex-end;
-}
-
-.footerCenter {
- justify-content: center;
-}
-
-.row {
- align-items: flex-end;
-}
-
-.rowCenter {
- align-items: center;
-}
-
-.input {
- flex: 1;
- min-width: 0;
- max-height: 220px;
- box-sizing: border-box;
- margin: 0;
- padding: 8px 4px;
- border: 0;
- background: transparent;
- color: var( --ui-desks-text, #14171a );
- font: inherit;
- font-size: 15px;
- line-height: 1.45;
- letter-spacing: 0;
- outline: none;
- resize: none;
- overflow-y: auto;
-}
-
-.input::placeholder {
- color: var( --ui-desks-muted, #6b7280 );
-}
-
-.error {
- color: #b32d2e;
- font-size: 12px;
- line-height: 16px;
-}
-
-.tip {
- margin: 0;
- color: var( --ui-desks-muted, #6b7280 );
- font-size: 12px;
- line-height: 1.4;
-}
-
-@keyframes fadeIn {
- from {
- opacity: 0;
- }
-
- to {
- opacity: 1;
- }
-}
-
-@keyframes rise {
- from {
- opacity: 0;
- transform: translateY( 8px ) scale( 0.98 );
- }
-
- to {
- opacity: 1;
- transform: translateY( 0 ) scale( 1 );
- }
-}
diff --git a/apps/ui/src/ui-desks/components/index.ts b/apps/ui/src/ui-desks/components/index.ts
deleted file mode 100644
index e178a6f1e0..0000000000
--- a/apps/ui/src/ui-desks/components/index.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-export { Button } from './button';
-export {
- Dialog,
- DialogCloseButton,
- DialogContent,
- DialogError,
- DialogFooter,
- DialogHeader,
- DialogRow,
- DialogTip,
- DialogTitle,
- dialogInputClassName,
-} from './dialog';
-export { LoadingPlaceholder } from './loading-placeholder';
-export { List, ListItem } from './list';
-export { Divider, Surface } from './surface';
-export * as Menu from './menu';
diff --git a/apps/ui/src/ui-desks/components/list/index.tsx b/apps/ui/src/ui-desks/components/list/index.tsx
deleted file mode 100644
index 4cf3f1dcb3..0000000000
--- a/apps/ui/src/ui-desks/components/list/index.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { clsx } from 'clsx';
-import styles from './style.module.css';
-import type { ComponentPropsWithoutRef, ReactNode } from 'react';
-
-type ListProps = ComponentPropsWithoutRef< 'div' >;
-
-type ListItemProps = Omit< ComponentPropsWithoutRef< 'button' >, 'children' > & {
- active?: boolean;
- description?: ReactNode;
- label: ReactNode;
-};
-
-export function List( { className, ...props }: ListProps ) {
- return ;
-}
-
-export function ListItem( {
- active = false,
- className,
- description,
- label,
- type = 'button',
- ...props
-}: ListItemProps ) {
- return (
-
- );
-}
diff --git a/apps/ui/src/ui-desks/components/list/style.module.css b/apps/ui/src/ui-desks/components/list/style.module.css
deleted file mode 100644
index 3c718dd093..0000000000
--- a/apps/ui/src/ui-desks/components/list/style.module.css
+++ /dev/null
@@ -1,67 +0,0 @@
-.list {
- display: flex;
- flex-direction: column;
- gap: 2px;
- padding: 8px 12px;
- scrollbar-gutter: stable;
-}
-
-.item {
- --list-item-background: transparent;
- --wp-ui-button-background-color: var(--list-item-background);
- --wp-ui-button-background-color-active: var(--list-item-background);
- --wp-ui-button-border-color: transparent;
- --wp-ui-button-border-color-active: transparent;
-
- width: 100%;
- display: flex;
- flex-direction: column;
- gap: 4px;
- padding: 10px 12px;
- border: 0;
- border-radius: var(--ui-desks-radius-button, 12px);
- corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- -webkit-corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- background-color: var(--list-item-background);
- color: var(--ui-desks-text, #14171a);
- font: inherit;
- text-align: left;
- cursor: pointer;
- -webkit-app-region: no-drag;
-}
-
-.item:hover,
-.item:focus-visible {
- --list-item-background: rgba(15, 23, 42, 0.04);
- outline: none;
-}
-
-.item[data-active='true'] {
- --list-item-background: var(--ui-desks-control-hover, rgba(15, 23, 42, 0.07));
-}
-
-.item:focus:not(:focus-visible) {
- outline: none;
- background-color: var(--list-item-background);
- border-color: transparent;
-}
-
-.label,
-.description {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.label {
- font-size: 13px;
- line-height: 18px;
- font-weight: 500;
-}
-
-.description {
- color: var(--ui-desks-muted, #6b7280);
- font-size: 12px;
- line-height: 16px;
-}
diff --git a/apps/ui/src/ui-desks/components/loading-placeholder/index.tsx b/apps/ui/src/ui-desks/components/loading-placeholder/index.tsx
deleted file mode 100644
index c519d5524f..0000000000
--- a/apps/ui/src/ui-desks/components/loading-placeholder/index.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { clsx } from 'clsx';
-import styles from './style.module.css';
-import type { ComponentPropsWithoutRef } from 'react';
-
-type LoadingPlaceholderProps = ComponentPropsWithoutRef< 'div' > & {
- text?: string;
-};
-
-export function LoadingPlaceholder( { className, text, ...props }: LoadingPlaceholderProps ) {
- return (
-
- { text &&
{ text }
}
-
-
-
- );
-}
diff --git a/apps/ui/src/ui-desks/components/loading-placeholder/style.module.css b/apps/ui/src/ui-desks/components/loading-placeholder/style.module.css
deleted file mode 100644
index a643da522e..0000000000
--- a/apps/ui/src/ui-desks/components/loading-placeholder/style.module.css
+++ /dev/null
@@ -1,43 +0,0 @@
-.placeholder {
- display: flex;
- flex-direction: column;
- gap: 8px;
-}
-
-.title {
- font-size: 13px;
- line-height: 18px;
- font-weight: 500;
- color: var(--loading-placeholder-text-color, #50575e);
-}
-
-.line {
- width: 68%;
- height: 8px;
- border-radius: 999px;
- background: linear-gradient(
- 90deg,
- var(--loading-placeholder-line-color, #f0f0f0) 0%,
- var(--loading-placeholder-line-highlight-color, #fff) 45%,
- var(--loading-placeholder-line-color, #f0f0f0) 90%
- );
- background-size: 220% 100%;
- animation: shimmer 1.4s ease-in-out infinite;
-}
-
-.shortLine {
- width: 44%;
- height: 8px;
- border-radius: 999px;
- background: var(--loading-placeholder-line-color, #f0f0f0);
-}
-
-@keyframes shimmer {
- 0% {
- background-position: 120% 0;
- }
-
- 100% {
- background-position: -120% 0;
- }
-}
diff --git a/apps/ui/src/ui-desks/components/menu/index.tsx b/apps/ui/src/ui-desks/components/menu/index.tsx
deleted file mode 100644
index 010e60f511..0000000000
--- a/apps/ui/src/ui-desks/components/menu/index.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import { Menu as BaseMenu } from '@base-ui/react/menu';
-import { privateApis } from '@wordpress/theme';
-import { clsx } from 'clsx';
-import { forwardRef } from 'react';
-import motionStyles from '@/components/floating-surface-motion/style.module.css';
-import { unlock } from '@/lock-unlock';
-import styles from './style.module.css';
-import type { ComponentPropsWithoutRef, ElementRef, ReactNode } from 'react';
-
-const { ThemeProvider } = unlock( privateApis );
-
-export const Root = BaseMenu.Root;
-export const Trigger = BaseMenu.Trigger;
-export const RadioGroup = BaseMenu.RadioGroup;
-
-type PopupProps = {
- children: ReactNode;
- side?: 'top' | 'right' | 'bottom' | 'left';
- align?: 'start' | 'center' | 'end';
- sideOffset?: number;
- alignOffset?: number;
- className?: string;
- layout?: 'column' | 'row';
- width?: 'default' | 'content';
-};
-
-export function Popup( {
- children,
- side = 'bottom',
- align = 'start',
- sideOffset = 8,
- alignOffset,
- className,
- layout = 'column',
- width = 'default',
-}: PopupProps ) {
- return (
-
-
-
-
- { children }
-
-
-
-
- );
-}
-
-type ItemProps = ComponentPropsWithoutRef< typeof BaseMenu.Item > & {
- variant?: 'default' | 'custom';
-};
-
-export const Item = forwardRef< ElementRef< typeof BaseMenu.Item >, ItemProps >( function Item(
- { className, children, variant = 'default', ...props },
- ref
-) {
- return (
-
- { children }
-
- );
-} );
-
-type RadioItemProps = ComponentPropsWithoutRef< typeof BaseMenu.RadioItem >;
-
-export const RadioItem = forwardRef< ElementRef< typeof BaseMenu.RadioItem >, RadioItemProps >(
- function RadioItem( { className, children, ...props }, ref ) {
- return (
-
-
-
-
-
-
- { children }
-
- );
- }
-);
-
-export function Separator( { className }: { className?: string } ) {
- return ;
-}
diff --git a/apps/ui/src/ui-desks/components/menu/style.module.css b/apps/ui/src/ui-desks/components/menu/style.module.css
deleted file mode 100644
index ccdae6c946..0000000000
--- a/apps/ui/src/ui-desks/components/menu/style.module.css
+++ /dev/null
@@ -1,110 +0,0 @@
-.positioner {
- z-index: var(--ui-desks-z-popover, 100);
- outline: none;
-}
-
-.popup {
- min-width: 200px;
- padding: 6px;
- background: var(--ui-desks-material, #fff);
- border: 0;
- border-radius: var(--ui-desks-radius-control, 16px);
- corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- -webkit-corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- box-shadow: var(
- --ui-desks-shadow-control-raised,
- 0 1px 2px rgba(15, 23, 42, 0.04),
- 0 18px 44px rgba(15, 23, 42, 0.1)
- );
- color: var(--ui-desks-text, #14171a);
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-
-.contentWidth {
- min-width: auto;
-}
-
-.row {
- flex-direction: row;
- align-items: center;
- gap: 6px;
-}
-
-.item {
- display: flex;
- align-items: center;
- gap: 10px;
- min-width: 0;
- padding: 10px 12px;
- border-radius: var(--ui-desks-radius-button, 12px);
- corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- -webkit-corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- background: transparent;
- color: var(--ui-desks-text, #14171a);
- font-size: 13px;
- line-height: 18px;
- cursor: pointer;
- outline: none;
- user-select: none;
-}
-
-.item > span {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.item > svg {
- order: 2;
- margin-left: auto;
- flex: 0 0 auto;
-}
-
-.item[data-highlighted],
-.item:hover,
-.item[aria-current='page'],
-.item[data-active='true'] {
- background: var(--ui-desks-control-hover, rgba(15, 23, 42, 0.07));
-}
-
-.item[data-disabled] {
- cursor: default;
- opacity: 0.5;
-}
-
-.radioItem {
- padding-inline-start: 8px;
-}
-
-.indicator {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 18px;
- height: 18px;
- flex: 0 0 auto;
- color: var(--ui-desks-text, #14171a);
-}
-
-.indicatorMark {
- display: inline-flex;
- opacity: 0;
-}
-
-.indicatorMark[data-checked] {
- opacity: 1;
-}
-
-.itemLabel {
- flex: 1;
-}
-
-.separator {
- height: 1px;
- margin: 4px 6px;
- background: var(--ui-desks-divider, rgba(15, 23, 42, 0.06));
- border: 0;
-}
diff --git a/apps/ui/src/ui-desks/components/surface/index.tsx b/apps/ui/src/ui-desks/components/surface/index.tsx
deleted file mode 100644
index fb9e271033..0000000000
--- a/apps/ui/src/ui-desks/components/surface/index.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { clsx } from 'clsx';
-import styles from './style.module.css';
-import type { ComponentPropsWithoutRef } from 'react';
-
-type SurfaceVariant = 'glass' | 'material';
-
-type SurfaceProps = ComponentPropsWithoutRef< 'div' > & {
- variant?: SurfaceVariant;
-};
-
-export function Surface( { className, variant = 'material', ...props }: SurfaceProps ) {
- return ;
-}
-
-export function Divider( { className, ...props }: ComponentPropsWithoutRef< 'div' > ) {
- return ;
-}
diff --git a/apps/ui/src/ui-desks/components/surface/style.module.css b/apps/ui/src/ui-desks/components/surface/style.module.css
deleted file mode 100644
index b7a28c71e0..0000000000
--- a/apps/ui/src/ui-desks/components/surface/style.module.css
+++ /dev/null
@@ -1,41 +0,0 @@
-.surface {
- display: inline-flex;
- align-items: center;
- color: var(--ui-desks-text, #14171a);
-}
-
-.glass {
- gap: 4px;
- padding: 6px;
- background: var(--ui-desks-glass, rgba(255, 255, 255, 0.85));
- border: 1px solid var(--ui-desks-glass-border, rgba(255, 255, 255, 0.7));
- border-radius: var(--ui-desks-radius-surface, 18px);
- corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- -webkit-corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- box-shadow: var(
- --ui-desks-shadow-surface-raised,
- 0 1px 2px rgba(15, 23, 42, 0.04),
- 0 18px 44px rgba(15, 23, 42, 0.1)
- );
- -webkit-backdrop-filter: saturate(180%) blur(28px);
- backdrop-filter: saturate(180%) blur(28px);
-}
-
-.material {
- background: var(--ui-desks-material, #fff);
- border-radius: var(--ui-desks-radius-control, 16px);
- corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- -webkit-corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- box-shadow: var(
- --ui-desks-shadow-control,
- 0 1px 2px rgba(15, 23, 42, 0.04),
- 0 8px 24px rgba(15, 23, 42, 0.06)
- );
-}
-
-.divider {
- width: 1px;
- align-self: stretch;
- margin: 4px;
- background: var(--ui-desks-divider, rgba(15, 23, 42, 0.06));
-}
diff --git a/apps/ui/src/ui-desks/connectors/context.ts b/apps/ui/src/ui-desks/connectors/context.ts
deleted file mode 100644
index 9c944e2258..0000000000
--- a/apps/ui/src/ui-desks/connectors/context.ts
+++ /dev/null
@@ -1,250 +0,0 @@
-import { useValue, type Editor, type TLShape, type TLShapeId } from 'tldraw';
-import {
- RECTANGLE_WIDGET_SHAPE_TYPE,
- isRectangleWidgetShapeProps,
-} from '@/ui-desks/shapes/rectangle-widget/types';
-import { NOTE_WIDGET_TYPE, type NoteTone } from '@/ui-desks/widgets/note/types';
-import type { DeskConfig } from '@/ui-desks/desk/types';
-import type { DeskWidget } from '@/ui-desks/widgets/types';
-
-export interface DeskWidgetConnectionTarget {
- shapeId: TLShapeId;
- widget: DeskWidget;
- label: string;
- title: string;
- pillBg?: string;
-}
-
-export function useIncomingWidgetConnections(
- editor: Editor,
- targetShapeId: TLShapeId | undefined
-): DeskWidgetConnectionTarget[] {
- return useValue(
- `incoming-widget-connections:${ targetShapeId ?? 'none' }`,
- () => ( targetShapeId ? getIncomingWidgetConnections( editor, targetShapeId ) : [] ),
- [ editor, targetShapeId ]
- );
-}
-
-export function getIncomingWidgetConnections(
- editor: Editor,
- targetShapeId: TLShapeId
-): DeskWidgetConnectionTarget[] {
- const sources: DeskWidgetConnectionTarget[] = [];
- for ( const binding of editor.getBindingsToShape( targetShapeId, 'arrow' ) ) {
- if ( getArrowBindingTerminal( binding.props ) !== 'end' ) {
- continue;
- }
-
- const connectorShape = editor.getShape( binding.fromId );
- if ( ! isDeskConnectorCanvasShape( connectorShape ) ) {
- continue;
- }
-
- const startBinding = editor
- .getBindingsFromShape( connectorShape.id, 'arrow' )
- .find( ( candidate ) => getArrowBindingTerminal( candidate.props ) === 'start' );
- if ( ! startBinding ) {
- continue;
- }
-
- const sourceShape = editor.getShape( startBinding.toId );
- const widget = sourceShape ? canvasShapeToDeskWidgetForContext( sourceShape ) : null;
- if ( ! sourceShape || ! widget ) {
- continue;
- }
-
- const label = getDeskWidgetConnectionLabel( widget );
- sources.push( {
- shapeId: sourceShape.id,
- widget,
- label,
- title: getDeskWidgetConnectionTitle( widget, label ),
- pillBg: getDeskWidgetConnectionPillBg( widget ),
- } );
- }
- return sources;
-}
-
-export function focusOnDeskShape( editor: Editor, shapeId: TLShapeId ) {
- editor.setSelectedShapes( [ shapeId ] );
- const bounds = editor.getShapePageBounds( shapeId );
- if ( ! bounds ) {
- return false;
- }
-
- editor.centerOnPoint( bounds.center, { animation: { duration: 320 } } );
- editor.focus();
- return true;
-}
-
-export function appendIncomingConnectedWidgets(
- widgets: DeskWidget[],
- deskConfig: DeskConfig | null | undefined
-): DeskWidget[] {
- if ( widgets.length === 0 || ! deskConfig?.connectors?.length ) {
- return widgets;
- }
-
- const widgetsById = new Map( deskConfig.widgets.map( ( widget ) => [ widget.id, widget ] ) );
- const selectedIds = new Set( widgets.map( ( widget ) => widget.id ) );
- const output = [ ...widgets ];
-
- for ( const widget of widgets ) {
- for ( const connector of deskConfig.connectors ) {
- if ( connector.to.widgetId !== widget.id || selectedIds.has( connector.from.widgetId ) ) {
- continue;
- }
-
- const sourceWidget = widgetsById.get( connector.from.widgetId );
- if ( ! sourceWidget ) {
- continue;
- }
-
- selectedIds.add( sourceWidget.id );
- output.push( sourceWidget );
- }
- }
-
- return output;
-}
-
-export function getDeskWidgetConnectionLabel( widget: DeskWidget ) {
- const props = widget.widgetProps as Record< string, unknown >;
- switch ( widget.type as string ) {
- case 'post':
- return typeof props.postId === 'number' ? `Post #${ props.postId }` : 'Post';
- case 'page':
- return typeof props.pageId === 'number' ? `Page #${ props.pageId }` : 'Page';
- case NOTE_WIDGET_TYPE:
- return 'Note';
- case 'site-preview':
- return typeof props.path === 'string' && props.path ? props.path : 'Preview';
- case 'site-card':
- return 'Site card';
- case 'bookmark':
- case 'embed':
- return getUrlHostLabel( props.url ) ?? ( widget.type === 'embed' ? 'Embed' : 'Bookmark' );
- case 'media':
- return typeof props.mediaKind === 'string' && props.mediaKind === 'video' ? 'Video' : 'Image';
- case 'drawing':
- return 'Drawing';
- case 'scratchpad':
- return typeof props.title === 'string' && props.title ? props.title : 'Scratchpad';
- case 'blog':
- return 'Blog';
- case 'post-collection':
- return 'Posts';
- default:
- return widget.type;
- }
-}
-
-export function getDeskWidgetConnectionTitle(
- widget: DeskWidget,
- label = getDeskWidgetConnectionLabel( widget )
-) {
- const props = widget.widgetProps as Record< string, unknown >;
- if ( widget.type === 'media' ) {
- const alt = typeof props.alt === 'string' ? props.alt.trim() : '';
- return alt || label;
- }
-
- if ( widget.type === NOTE_WIDGET_TYPE ) {
- const text = typeof props.text === 'string' ? stripMarkup( props.text ).trim() : '';
- return text || label;
- }
-
- return label;
-}
-
-export function getDeskWidgetConnectionPillBg( widget: DeskWidget ) {
- if ( widget.type !== NOTE_WIDGET_TYPE ) {
- return undefined;
- }
-
- const tone = ( widget.widgetProps as { tone?: unknown } ).tone;
- return NOTE_CONNECTION_PILL_BG[ tone as NoteTone ];
-}
-
-const NOTE_CONNECTION_PILL_BG: Record< NoteTone, string > = {
- grey: '#6b7280',
- yellow: '#c4a300',
- mint: '#3ca56f',
- blue: '#2271b1',
- orange: '#c97223',
- violet: '#7b3fb6',
- 'neon-yellow': '#a18a00',
- 'neon-green': '#2e9e3a',
- 'neon-violet': '#6f2daa',
- 'neon-orange': '#b97917',
- 'neon-blue': '#1873c9',
-};
-
-function canvasShapeToDeskWidgetForContext( shape: TLShape ): DeskWidget | null {
- if ( shape.type !== RECTANGLE_WIDGET_SHAPE_TYPE ) {
- return null;
- }
-
- const props = shape.props as Partial< {
- widgetType: unknown;
- shapeProps: unknown;
- widgetProps: unknown;
- } >;
- if (
- typeof props.widgetType !== 'string' ||
- ! isRectangleWidgetShapeProps( props.shapeProps ) ||
- ! props.widgetProps ||
- typeof props.widgetProps !== 'object'
- ) {
- return null;
- }
-
- return {
- id: getWidgetIdFromShapeId( shape.id ),
- type: props.widgetType,
- x: shape.x,
- y: shape.y,
- rotation: shape.rotation || undefined,
- zIndex: String( shape.index ?? 'a1' ),
- shapeProps: props.shapeProps,
- widgetProps: props.widgetProps as Record< string, unknown >,
- } as DeskWidget;
-}
-
-function isDeskConnectorCanvasShape( shape: unknown ): shape is TLShape {
- return (
- Boolean( shape ) &&
- typeof shape === 'object' &&
- ( shape as Partial< TLShape > ).type === 'arrow' &&
- ( ( shape as Partial< TLShape > ).meta as { studioDeskConnector?: unknown } | undefined )
- ?.studioDeskConnector === true
- );
-}
-
-function getArrowBindingTerminal( props: object ) {
- return ( props as { terminal?: unknown } ).terminal;
-}
-
-function getUrlHostLabel( value: unknown ) {
- if ( typeof value !== 'string' || ! value ) {
- return null;
- }
-
- try {
- return new URL( value ).hostname.replace( /^www\./, '' );
- } catch {
- return value;
- }
-}
-
-function getWidgetIdFromShapeId( shapeId: string ) {
- return shapeId.startsWith( 'shape:' ) ? shapeId.slice( 'shape:'.length ) : shapeId;
-}
-
-function stripMarkup( value: string ) {
- return value
- .replace( /<[^>]*>/g, ' ' )
- .replace( /\s+/g, ' ' )
- .trim();
-}
diff --git a/apps/ui/src/ui-desks/connectors/editor-commands.ts b/apps/ui/src/ui-desks/connectors/editor-commands.ts
deleted file mode 100644
index 487fb1f69f..0000000000
--- a/apps/ui/src/ui-desks/connectors/editor-commands.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-import { createShapeId, type Editor, type TLArrowShape, type TLShapeId } from 'tldraw';
-import {
- CONNECTOR_COLOR,
- CONNECTOR_DASH,
- CONNECTOR_DEFAULT_BEND,
- CONNECTOR_SHAPE_ID_PREFIX,
- canvasShapeToDeskWidget,
-} from '@/ui-desks/desk/tldraw-adapter';
-import { focusOnDeskShape, getSelectedDeskConnectorToolbarItem } from './utils';
-
-export function createConnectorPreview(
- editor: Editor,
- sourceShapeId: TLShapeId,
- startPoint: { x: number; y: number },
- endPoint: { x: number; y: number }
-) {
- const arrowId = createShapeId(
- `${ CONNECTOR_SHAPE_ID_PREFIX }${ createConnectorId() }`
- ) as TLArrowShape[ 'id' ];
- editor.createShape< TLArrowShape >( {
- id: arrowId,
- type: 'arrow',
- meta: {
- studioDeskConnector: true,
- },
- props: {
- kind: 'arc',
- color: CONNECTOR_COLOR,
- dash: CONNECTOR_DASH,
- size: 'm',
- bend: CONNECTOR_DEFAULT_BEND,
- arrowheadStart: 'dot',
- arrowheadEnd: 'arrow',
- start: startPoint,
- end: endPoint,
- },
- } );
- editor.createBindings( [
- {
- type: 'arrow',
- fromId: arrowId,
- toId: sourceShapeId,
- props: {
- terminal: 'start' as const,
- normalizedAnchor: { x: 0.5, y: 0.5 },
- isExact: false,
- isPrecise: false,
- snap: 'none' as const,
- },
- },
- ] );
- return arrowId;
-}
-
-export function completeConnectorPreview(
- editor: Editor,
- connectorShapeId: TLArrowShape[ 'id' ],
- targetShapeId: TLShapeId
-) {
- editor.createBindings( [
- {
- type: 'arrow',
- fromId: connectorShapeId,
- toId: targetShapeId,
- props: {
- terminal: 'end' as const,
- normalizedAnchor: { x: 0.5, y: 0.5 },
- isExact: false,
- isPrecise: false,
- snap: 'none' as const,
- },
- },
- ] );
-}
-
-export function updateConnectorEnd(
- editor: Editor,
- connectorShapeId: TLArrowShape[ 'id' ],
- endPoint: { x: number; y: number }
-) {
- editor.updateShape< TLArrowShape >( {
- id: connectorShapeId,
- type: 'arrow',
- props: {
- end: endPoint,
- },
- } );
-}
-
-export function removeSelectedConnectorFromEditor( editor: Editor ) {
- const connector = getSelectedDeskConnectorToolbarItem( editor );
- if ( ! connector ) {
- return false;
- }
-
- editor.deleteShape( connector.shapeId );
- return true;
-}
-
-export function startConnectingWidgetInEditor( editor: Editor, shapeId: TLShapeId ) {
- const shape = editor.getShape( shapeId );
- if ( ! shape || ! canvasShapeToDeskWidget( shape ) ) {
- return false;
- }
-
- editor.focus();
- return true;
-}
-
-export function focusConnectedWidgetInEditor( editor: Editor, shapeId: TLShapeId ) {
- return focusOnDeskShape( editor, shapeId );
-}
-
-export function toPlainPoint( point: { x: number; y: number } ) {
- return {
- x: point.x,
- y: point.y,
- };
-}
-
-export function getInitialConnectorEndPoint(
- startPoint: { x: number; y: number },
- cursorPoint: { x: number; y: number }
-) {
- const distance = Math.hypot( cursorPoint.x - startPoint.x, cursorPoint.y - startPoint.y );
- if ( distance >= 8 ) {
- return cursorPoint;
- }
-
- return {
- x: startPoint.x + 96,
- y: startPoint.y,
- };
-}
-
-function createConnectorId() {
- return globalThis.crypto?.randomUUID?.() ?? `connector-${ Date.now().toString( 36 ) }`;
-}
diff --git a/apps/ui/src/ui-desks/connectors/use-connector-interactions.ts b/apps/ui/src/ui-desks/connectors/use-connector-interactions.ts
deleted file mode 100644
index 31c33e0706..0000000000
--- a/apps/ui/src/ui-desks/connectors/use-connector-interactions.ts
+++ /dev/null
@@ -1,553 +0,0 @@
-import { useEffect } from 'react';
-import {
- canvasShapeToDeskWidget,
- isDeskConnectorCanvasShape,
-} from '@/ui-desks/desk/tldraw-adapter';
-import {
- completeConnectorPreview,
- createConnectorPreview,
- getInitialConnectorEndPoint,
- toPlainPoint,
- updateConnectorEnd,
-} from './editor-commands';
-import {
- getConnectableShapeAtPagePoint,
- getWidgetDropTargetAtPagePoint,
- getWidgetShapeAtPagePoint,
- isShapePartOfMultiSelection,
-} from './utils';
-import type {
- ActiveWidgetDropFeedback,
- DeskWidget,
- WidgetCustomDropActionIntent,
- WidgetDropFeedback,
- WidgetDropFeedbackPhase,
- WidgetDropHandler,
-} from '@/ui-desks/widgets/types';
-import type { Editor, TLArrowShape, TLEventInfo, TLShape, TLShapeId } from 'tldraw';
-
-interface UseConnectorInteractionsOptions {
- editor: Editor | null;
- isHydrated: boolean;
- isReadOnly: boolean;
- pendingConnectorSourceId: TLShapeId | null;
- setPendingConnectorSourceId: ( shapeId: TLShapeId | null ) => void;
- onConnectorComplete?: ( connection: WidgetConnectorCompleteIntent ) => void;
- onCustomDrop?: ( drop: WidgetCustomDropIntent ) => void;
- onDropFeedbackChange?: ( feedback: ActiveWidgetDropFeedback | null ) => void;
-}
-
-export function useConnectorInteractions( {
- editor,
- isHydrated,
- isReadOnly,
- pendingConnectorSourceId,
- setPendingConnectorSourceId,
- onConnectorComplete,
- onCustomDrop,
- onDropFeedbackChange,
-}: UseConnectorInteractionsOptions ) {
- useEffect( () => {
- if ( ! editor ) {
- return;
- }
-
- return editor.sideEffects.registerBeforeChangeHandler(
- 'shape',
- ( previousShape, nextShape ) => {
- if ( ! isDeskConnectorCanvasShape( nextShape ) ) {
- return nextShape;
- }
- if ( previousShape.x === nextShape.x && previousShape.y === nextShape.y ) {
- return nextShape;
- }
- return {
- ...nextShape,
- x: previousShape.x,
- y: previousShape.y,
- };
- }
- );
- }, [ editor ] );
-
- useEffect( () => {
- if ( ! editor ) {
- return;
- }
-
- return editor.sideEffects.registerAfterDeleteHandler( 'binding', ( binding ) => {
- if ( binding.type !== 'arrow' ) {
- return;
- }
-
- const connectorShape = editor.getShape( binding.fromId );
- if ( ! isDeskConnectorCanvasShape( connectorShape ) ) {
- return;
- }
-
- editor.deleteShape( connectorShape.id );
- } );
- }, [ editor ] );
-
- useEffect( () => {
- if ( ! editor || ! isReadOnly ) {
- return;
- }
-
- return editor.store.listen(
- () => {
- const selectedShapeIds = editor.getSelectedShapeIds();
- if ( selectedShapeIds.length === 0 ) {
- return;
- }
-
- const nextSelectedShapeIds = selectedShapeIds.filter(
- ( shapeId ) => ! isDeskConnectorCanvasShape( editor.getShape( shapeId ) )
- );
- if ( nextSelectedShapeIds.length !== selectedShapeIds.length ) {
- editor.setSelectedShapes( nextSelectedShapeIds );
- }
- },
- { scope: 'session' }
- );
- }, [ editor, isReadOnly ] );
-
- useEffect( () => {
- if ( ! editor || ! pendingConnectorSourceId ) {
- return;
- }
-
- const sourceShape = editor.getShape( pendingConnectorSourceId );
- const sourceWidget = sourceShape ? canvasShapeToDeskWidget( sourceShape ) : null;
- if ( ! sourceShape || ! sourceWidget ) {
- setPendingConnectorSourceId( null );
- return;
- }
-
- const sourceBounds = editor.getShapePageBounds( pendingConnectorSourceId );
- const startPoint = toPlainPoint( sourceBounds?.center ?? editor.inputs.currentPagePoint );
- const initialEndPoint = getInitialConnectorEndPoint(
- startPoint,
- toPlainPoint( editor.inputs.currentPagePoint )
- );
- const arrowId = createConnectorPreview(
- editor,
- pendingConnectorSourceId,
- startPoint,
- initialEndPoint
- );
-
- let completed = false;
- const cancelConnection = () => {
- setPendingConnectorSourceId( null );
- };
-
- const syncConnectorEnd = ( point: { x: number; y: number } ) => {
- const hitShape = getConnectableShapeAtPagePoint( editor, point, pendingConnectorSourceId );
- const hitBounds = hitShape ? editor.getShapePageBounds( hitShape.id ) : null;
- updateConnectorEnd( editor, arrowId, toPlainPoint( hitBounds?.center ?? point ) );
- };
-
- const handleEvent = ( info: TLEventInfo ) => {
- if ( info.type !== 'pointer' || info.name !== 'pointer_move' ) {
- return;
- }
-
- syncConnectorEnd( toPlainPoint( editor.inputs.currentPagePoint ) );
- };
-
- const handlePointerMove = ( event: PointerEvent ) => {
- syncConnectorEnd(
- toPlainPoint( editor.screenToPage( { x: event.clientX, y: event.clientY } ) )
- );
- };
-
- const handleClick = ( event: MouseEvent ) => {
- const target = event.target as HTMLElement | null;
- if ( target?.closest( '[data-ui-desks-context-menu]' ) ) {
- return;
- }
-
- event.preventDefault();
- event.stopPropagation();
-
- const point = editor.screenToPage( { x: event.clientX, y: event.clientY } );
- const targetShape = getConnectableShapeAtPagePoint( editor, point, pendingConnectorSourceId );
- const targetWidget = targetShape ? canvasShapeToDeskWidget( targetShape ) : null;
- if ( ! targetShape || ! targetWidget ) {
- cancelConnection();
- return;
- }
-
- completed = true;
- completeConnectorPreview( editor, arrowId, targetShape.id );
- const targetBounds = editor.getShapePageBounds( targetShape.id );
- if ( targetBounds ) {
- updateConnectorEnd( editor, arrowId, toPlainPoint( targetBounds.center ) );
- }
- editor.setSelectedShapes( [ arrowId ] );
- editor.focus();
- onConnectorComplete?.( {
- sourceShapeId: pendingConnectorSourceId,
- targetShapeId: targetShape.id,
- sourceWidget,
- targetWidget,
- } );
- setPendingConnectorSourceId( null );
- };
-
- const handleKeyDown = ( event: KeyboardEvent ) => {
- if ( event.key === 'Escape' ) {
- cancelConnection();
- }
- };
-
- editor.on( 'event', handleEvent );
- window.addEventListener( 'pointermove', handlePointerMove, true );
- window.addEventListener( 'click', handleClick, true );
- window.addEventListener( 'keydown', handleKeyDown );
- return () => {
- editor.off( 'event', handleEvent );
- window.removeEventListener( 'pointermove', handlePointerMove, true );
- window.removeEventListener( 'click', handleClick, true );
- window.removeEventListener( 'keydown', handleKeyDown );
- if ( ! completed && editor.getShape( arrowId ) ) {
- editor.deleteShape( arrowId );
- }
- };
- }, [ editor, onConnectorComplete, pendingConnectorSourceId, setPendingConnectorSourceId ] );
-
- useEffect( () => {
- if ( ! editor || ! isHydrated || isReadOnly ) {
- return;
- }
-
- let source: {
- shapeId: TLShapeId;
- widget: DeskWidget;
- type: string;
- x: number;
- y: number;
- opacity: number;
- } | null = null;
- let connectorPreviewId: TLArrowShape[ 'id' ] | null = null;
- let activeTarget: WidgetDropTarget | null = null;
- let activeDropFeedbackKey: string | null = null;
- let hasSourceOpacityOverride = false;
- let didDrag = false;
- let completed = false;
-
- const getCustomDropFeedback = (
- target: WidgetDropTarget | null,
- phase: WidgetDropFeedbackPhase
- ): WidgetDropFeedback | null => {
- if ( ! source || ! target || target.handler.type !== 'custom' ) {
- return null;
- }
-
- return (
- target.handler.getFeedback?.( {
- sourceShapeId: source.shapeId,
- targetShapeId: target.shapeId,
- sourceWidget: source.widget,
- targetWidget: target.widget,
- screenPoint: getViewportScreenPoint( editor ),
- phase,
- } ) ?? null
- );
- };
-
- const toActiveDropFeedback = (
- target: WidgetDropTarget | null,
- feedback: WidgetDropFeedback | null,
- phase: WidgetDropFeedbackPhase
- ): ActiveWidgetDropFeedback | null => {
- if ( ! target || ! feedback?.target ) {
- return null;
- }
-
- return {
- targetShapeId: target.shapeId,
- feedback: {
- ...feedback.target,
- phase,
- },
- };
- };
-
- const syncDropFeedback = (
- target: WidgetDropTarget | null,
- phase: WidgetDropFeedbackPhase
- ) => {
- const feedback = getCustomDropFeedback( target, phase );
- const activeFeedback = toActiveDropFeedback( target, feedback, phase );
- const nextKey = activeFeedback
- ? `${ activeFeedback.targetShapeId }:${ activeFeedback.feedback.kind }:${ phase }`
- : null;
- if ( activeDropFeedbackKey !== nextKey ) {
- activeDropFeedbackKey = nextKey;
- onDropFeedbackChange?.( activeFeedback );
- }
- return feedback;
- };
-
- const setSourceOpacity = ( opacity: number ) => {
- if ( ! source ) {
- return;
- }
-
- const shape = editor.getShape( source.shapeId );
- if ( ! shape || Math.abs( shape.opacity - opacity ) <= 0.001 ) {
- return;
- }
-
- editor.updateShape( {
- id: source.shapeId,
- type: source.type as TLShape[ 'type' ],
- opacity,
- } );
- hasSourceOpacityOverride = true;
- };
-
- const restoreSourceOpacity = () => {
- if ( ! source || ! hasSourceOpacityOverride ) {
- return;
- }
-
- const shape = editor.getShape( source.shapeId );
- if ( shape && Math.abs( shape.opacity - source.opacity ) > 0.001 ) {
- editor.updateShape( {
- id: source.shapeId,
- type: source.type as TLShape[ 'type' ],
- opacity: source.opacity,
- } );
- }
- hasSourceOpacityOverride = false;
- };
-
- const removeConnectorPreview = ( options: { preserveSourceOpacity?: boolean } = {} ) => {
- if ( connectorPreviewId && editor.getShape( connectorPreviewId ) ) {
- editor.deleteShape( connectorPreviewId );
- }
- connectorPreviewId = null;
- activeTarget = null;
- syncDropFeedback( null, 'hover' );
- if ( ! options.preserveSourceOpacity ) {
- restoreSourceOpacity();
- }
- };
-
- const restoreSourcePosition = () => {
- if ( ! source ) {
- return;
- }
-
- const shape = editor.getShape( source.shapeId );
- if ( ! shape || ( shape.x === source.x && shape.y === source.y ) ) {
- return;
- }
-
- editor.updateShape( {
- id: source.shapeId,
- type: source.type as TLShape[ 'type' ],
- x: source.x,
- y: source.y,
- } );
- };
-
- const cleanup = ( options: { preserveSourceOpacity?: boolean } = {} ) => {
- if ( ! completed ) {
- removeConnectorPreview( options );
- }
- if ( ! options.preserveSourceOpacity ) {
- restoreSourceOpacity();
- }
- source = null;
- connectorPreviewId = null;
- activeTarget = null;
- activeDropFeedbackKey = null;
- hasSourceOpacityOverride = false;
- didDrag = false;
- completed = false;
- };
-
- const handleEvent = ( info: TLEventInfo ) => {
- if ( info.type !== 'pointer' ) {
- return;
- }
-
- if ( info.name === 'pointer_down' ) {
- const shape = getWidgetShapeAtPagePoint( editor, editor.inputs.currentPagePoint );
- const widget = shape ? canvasShapeToDeskWidget( shape ) : null;
- source =
- widget && shape && ! isShapePartOfMultiSelection( editor, shape.id )
- ? {
- shapeId: shape.id,
- widget,
- type: shape.type,
- x: shape.x,
- y: shape.y,
- opacity: shape.opacity,
- }
- : null;
- connectorPreviewId = null;
- activeTarget = null;
- syncDropFeedback( null, 'hover' );
- hasSourceOpacityOverride = false;
- didDrag = false;
- completed = false;
- return;
- }
-
- if ( ! source ) {
- return;
- }
-
- if ( isShapePartOfMultiSelection( editor, source.shapeId ) ) {
- cleanup();
- return;
- }
-
- if ( info.name === 'pointer_up' ) {
- if ( didDrag && activeTarget ) {
- if ( activeTarget.handler.type === 'connector' && connectorPreviewId ) {
- completed = true;
- completeConnectorPreview( editor, connectorPreviewId, activeTarget.shapeId );
- const targetBounds = editor.getShapePageBounds( activeTarget.shapeId );
- if ( targetBounds ) {
- updateConnectorEnd( editor, connectorPreviewId, toPlainPoint( targetBounds.center ) );
- }
- restoreSourcePosition();
- onConnectorComplete?.( {
- sourceShapeId: source.shapeId,
- targetShapeId: activeTarget.shapeId,
- sourceWidget: source.widget,
- targetWidget: activeTarget.widget,
- } );
- } else if ( activeTarget.handler.type === 'custom' ) {
- const menuFeedback = getCustomDropFeedback( activeTarget, 'menu' );
- const activeMenuFeedback = toActiveDropFeedback( activeTarget, menuFeedback, 'menu' );
- const customDrop: WidgetCustomDropIntent = {
- sourceShapeId: source.shapeId,
- targetShapeId: activeTarget.shapeId,
- sourceWidget: source.widget,
- targetWidget: activeTarget.widget,
- handler: activeTarget.handler,
- screenPoint: getViewportScreenPoint( editor ),
- sourceOpacity: source.opacity,
- };
- restoreSourcePosition();
- if ( typeof menuFeedback?.sourceOpacity === 'number' ) {
- setSourceOpacity( menuFeedback.sourceOpacity );
- } else {
- restoreSourceOpacity();
- }
- cleanup( { preserveSourceOpacity: typeof menuFeedback?.sourceOpacity === 'number' } );
- onDropFeedbackChange?.( activeMenuFeedback );
- onCustomDrop?.( customDrop );
- return;
- }
- }
- cleanup();
- return;
- }
-
- if ( info.name !== 'pointer_move' || ! editor.inputs.isDragging ) {
- return;
- }
-
- didDrag = true;
- const point = toPlainPoint( editor.inputs.currentPagePoint );
- const target = getWidgetDropTargetAtPagePoint( editor, point, source.shapeId, source.widget );
- if ( ! target ) {
- removeConnectorPreview();
- return;
- }
-
- activeTarget = {
- shapeId: target.shape.id,
- widget: target.widget,
- handler: target.handler,
- };
- restoreSourcePosition();
-
- if ( target.handler.type !== 'connector' ) {
- const customTarget = {
- shapeId: target.shape.id,
- widget: target.widget,
- handler: target.handler,
- };
- const feedback = syncDropFeedback( customTarget, 'hover' );
- if ( connectorPreviewId ) {
- removeConnectorPreview();
- activeTarget = customTarget;
- syncDropFeedback( activeTarget, 'hover' );
- }
- if ( typeof feedback?.sourceOpacity === 'number' ) {
- setSourceOpacity( feedback.sourceOpacity );
- } else {
- restoreSourceOpacity();
- }
- return;
- }
-
- syncDropFeedback( null, 'hover' );
- const targetBounds = editor.getShapePageBounds( target.shape.id );
- if ( ! targetBounds ) {
- removeConnectorPreview();
- return;
- }
-
- if ( ! connectorPreviewId ) {
- const sourceBounds = editor.getShapePageBounds( source.shapeId );
- connectorPreviewId = createConnectorPreview(
- editor,
- source.shapeId,
- toPlainPoint( sourceBounds?.center ?? point ),
- toPlainPoint( targetBounds.center )
- );
- } else {
- updateConnectorEnd( editor, connectorPreviewId, toPlainPoint( targetBounds.center ) );
- }
-
- activeTarget = {
- shapeId: target.shape.id,
- widget: target.widget,
- handler: target.handler,
- };
- };
-
- editor.on( 'event', handleEvent );
- return () => {
- editor.off( 'event', handleEvent );
- cleanup();
- };
- }, [ editor, isHydrated, isReadOnly, onConnectorComplete, onCustomDrop, onDropFeedbackChange ] );
-}
-
-export interface WidgetConnectorCompleteIntent {
- sourceShapeId: TLShapeId;
- targetShapeId: TLShapeId;
- sourceWidget: DeskWidget;
- targetWidget: DeskWidget;
-}
-
-export interface WidgetCustomDropIntent extends WidgetCustomDropActionIntent {
- handler: Extract< WidgetDropHandler, { type: 'custom' } >;
- sourceOpacity: number;
-}
-
-interface WidgetDropTarget {
- shapeId: TLShapeId;
- widget: DeskWidget;
- handler: WidgetDropHandler;
-}
-
-function getViewportScreenPoint( editor: Editor ) {
- const screenPoint = editor.inputs.currentScreenPoint;
- const bounds = editor.getContainer().getBoundingClientRect();
- return {
- x: screenPoint.x + bounds.left,
- y: screenPoint.y + bounds.top,
- };
-}
diff --git a/apps/ui/src/ui-desks/connectors/utils.test.ts b/apps/ui/src/ui-desks/connectors/utils.test.ts
deleted file mode 100644
index a9a13f33e2..0000000000
--- a/apps/ui/src/ui-desks/connectors/utils.test.ts
+++ /dev/null
@@ -1,136 +0,0 @@
-import { describe, expect, it, vi } from 'vitest';
-import { DESK_CONFIG_VERSION } from '@/ui-desks/desk/types';
-import { NOTE_WIDGET_TYPE, type NoteTone } from '@/ui-desks/widgets/note/types';
-import { SITE_CARD_WIDGET_TYPE } from '@/ui-desks/widgets/site-card/types';
-import {
- appendIncomingConnectedWidgets,
- getDeskWidgetConnectionLabel,
- getDeskWidgetConnectionPillBg,
- getDeskWidgetConnectionTitle,
-} from './context';
-import { isShapePartOfMultiSelection } from './utils';
-import type { DeskConfig } from '@/ui-desks/desk/types';
-import type { DeskWidget } from '@/ui-desks/widgets/types';
-import type { Editor, TLShapeId } from 'tldraw';
-
-vi.mock( '@wordpress/core-data', () => ( {
- store: {},
- useEntityRecord: () => ( { record: null, isResolving: false } ),
- useEntityRecords: () => ( { records: null, isResolving: false, status: 'IDLE' } ),
-} ) );
-
-describe( 'desk connections', () => {
- it( 'uses a compact note connection label and keeps note text as the title', () => {
- const widget = createNoteWidget( 'Hello world' );
-
- expect( getDeskWidgetConnectionLabel( widget ) ).toBe( 'Note' );
- expect( getDeskWidgetConnectionTitle( widget ) ).toBe( 'Hello world' );
- } );
-
- it( 'falls back to the default note label when note text is empty', () => {
- expect( getDeskWidgetConnectionLabel( createNoteWidget( '' ) ) ).toBe( 'Note' );
- } );
-
- it( 'uses note tone colors for source pills', () => {
- expect( getDeskWidgetConnectionPillBg( createNoteWidget( '', 'blue' ) ) ).toBe( '#2271b1' );
- } );
-
- it( 'uses the site card label for site card connections', () => {
- expect( getDeskWidgetConnectionLabel( createSiteCardWidget() ) ).toBe( 'Site card' );
- } );
-
- it( 'appends incoming connected sources to chat context in selection order', () => {
- const source = createSiteCardWidget();
- const target = createNoteWidget( 'Use this' );
- const otherTarget = createNoteWidget( 'Other' );
- const deskConfig: DeskConfig = {
- version: DESK_CONFIG_VERSION,
- updatedAt: '2026-05-15T00:00:00.000Z',
- widgets: [ source, target, otherTarget ],
- connectors: [
- {
- id: 'site-card-to-note',
- from: {
- widgetId: source.id,
- normalizedAnchor: { x: 0.5, y: 0.5 },
- },
- to: {
- widgetId: target.id,
- normalizedAnchor: { x: 0.5, y: 0.5 },
- },
- },
- ],
- };
-
- expect( appendIncomingConnectedWidgets( [ target, otherTarget ], deskConfig ) ).toEqual( [
- target,
- otherTarget,
- source,
- ] );
- } );
-
- it( 'detects when a connector drag source is part of a multi-selection', () => {
- const sourceShapeId = 'shape:source-note' as TLShapeId;
- const otherShapeId = 'shape:other-note' as TLShapeId;
-
- expect(
- isShapePartOfMultiSelection(
- createEditorSelection( [ sourceShapeId, otherShapeId ] ),
- sourceShapeId
- )
- ).toBe( true );
- expect(
- isShapePartOfMultiSelection( createEditorSelection( [ sourceShapeId ] ), sourceShapeId )
- ).toBe( false );
- expect(
- isShapePartOfMultiSelection(
- createEditorSelection( [ otherShapeId, 'shape:third-note' as TLShapeId ] ),
- sourceShapeId
- )
- ).toBe( false );
- } );
-} );
-
-function createNoteWidget( text: string, tone: NoteTone = 'yellow' ): DeskWidget {
- return {
- id: `note-${ text || tone }`,
- type: NOTE_WIDGET_TYPE,
- x: 0,
- y: 0,
- zIndex: 'a1',
- shapeProps: {
- w: 200,
- h: 200,
- },
- widgetProps: {
- text,
- tone,
- },
- };
-}
-
-function createSiteCardWidget(): DeskWidget {
- return {
- id: 'site-card-1',
- type: SITE_CARD_WIDGET_TYPE,
- x: 0,
- y: 0,
- zIndex: 'a2',
- shapeProps: {
- w: 360,
- h: 200,
- },
- widgetProps: {
- siteId: 'site-1',
- previewVisible: false,
- },
- };
-}
-
-function createEditorSelection(
- selectedShapeIds: TLShapeId[]
-): Pick< Editor, 'getSelectedShapeIds' > {
- return {
- getSelectedShapeIds: () => selectedShapeIds,
- };
-}
diff --git a/apps/ui/src/ui-desks/connectors/utils.ts b/apps/ui/src/ui-desks/connectors/utils.ts
deleted file mode 100644
index 6f4ab32e01..0000000000
--- a/apps/ui/src/ui-desks/connectors/utils.ts
+++ /dev/null
@@ -1,217 +0,0 @@
-import { sortByIndex, type Editor, type TLShape, type TLShapeId } from 'tldraw';
-import {
- getDeskWidgetConnectionLabel,
- getDeskWidgetConnectionPillBg,
- getDeskWidgetConnectionTitle,
- type DeskWidgetConnectionTarget,
-} from '@/ui-desks/connectors/context';
-import {
- canvasShapeToDeskWidget,
- isDeskConnectorCanvasShape,
-} from '@/ui-desks/desk/tldraw-adapter';
-import { getWidgetDropHandler } from '@/ui-desks/widget-actions/drop-handlers';
-import type { DeskWidget, WidgetDropHandler } from '@/ui-desks/widgets/types';
-
-export type { DeskWidgetConnectionTarget };
-
-export interface SelectedDeskConnectorToolbarItem {
- shapeId: TLShapeId;
- sourceLabel: string;
- targetLabel: string;
-}
-
-export function getOutgoingWidgetConnections(
- editor: Editor,
- sourceShapeId: TLShapeId
-): DeskWidgetConnectionTarget[] {
- const targets: DeskWidgetConnectionTarget[] = [];
- for ( const connectorShape of editor
- .getCurrentPageShapes()
- .filter( isDeskConnectorCanvasShape ) ) {
- const endpoints = getDeskConnectorEndpoints( editor, connectorShape.id );
- if ( ! endpoints || endpoints.sourceShapeId !== sourceShapeId ) {
- continue;
- }
-
- const targetShape = editor.getShape( endpoints.targetShapeId );
- const widget = targetShape ? canvasShapeToDeskWidget( targetShape ) : null;
- if ( ! widget || ! targetShape ) {
- continue;
- }
-
- targets.push( {
- shapeId: targetShape.id,
- widget,
- label: getDeskWidgetConnectionLabel( widget ),
- title: getDeskWidgetConnectionTitle( widget ),
- pillBg: getDeskWidgetConnectionPillBg( widget ),
- } );
- }
- return targets;
-}
-
-export function getCurrentSelectedWidgetConnectionTargets( editor: Editor ) {
- const selectedShapeIds = editor.getSelectedShapeIds();
- if ( selectedShapeIds.length !== 1 ) {
- return [];
- }
-
- const [ selectedShapeId ] = selectedShapeIds;
- const selectedShape = editor.getShape( selectedShapeId );
- if ( ! selectedShape || ! canvasShapeToDeskWidget( selectedShape ) ) {
- return [];
- }
-
- return getOutgoingWidgetConnections( editor, selectedShapeId );
-}
-
-export function getSelectedDeskConnectorToolbarItem(
- editor: Editor
-): SelectedDeskConnectorToolbarItem | null {
- const [ selectedShapeId ] = editor.getSelectedShapeIds();
- if ( ! selectedShapeId || editor.getSelectedShapeIds().length !== 1 ) {
- return null;
- }
-
- const connectorShape = editor.getShape( selectedShapeId );
- if ( ! isDeskConnectorCanvasShape( connectorShape ) ) {
- return null;
- }
-
- const endpoints = getDeskConnectorEndpoints( editor, connectorShape.id );
- if ( ! endpoints ) {
- return null;
- }
-
- const sourceWidget = getWidgetForShapeId( editor, endpoints.sourceShapeId );
- const targetWidget = getWidgetForShapeId( editor, endpoints.targetShapeId );
- if ( ! sourceWidget || ! targetWidget ) {
- return null;
- }
-
- return {
- shapeId: connectorShape.id,
- sourceLabel: getDeskWidgetConnectionLabel( sourceWidget ),
- targetLabel: getDeskWidgetConnectionLabel( targetWidget ),
- };
-}
-
-export function getWidgetShapeAtPagePoint( editor: Editor, point: { x: number; y: number } ) {
- return editor.getShapeAtPoint( point, {
- hitInside: true,
- renderingOnly: true,
- margin: editor.options.hitTestMargin / editor.getZoomLevel(),
- } ) as TLShape | undefined;
-}
-
-export function isShapePartOfMultiSelection(
- editor: Pick< Editor, 'getSelectedShapeIds' >,
- shapeId: TLShapeId
-) {
- const selectedShapeIds = editor.getSelectedShapeIds();
- return selectedShapeIds.length > 1 && selectedShapeIds.includes( shapeId );
-}
-
-export function getConnectableShapeAtPagePoint(
- editor: Editor,
- point: { x: number; y: number },
- sourceShapeId: TLShapeId
-) {
- const shape = getWidgetShapeAtPagePoint( editor, point );
-
- if ( ! shape || shape.id === sourceShapeId || isDeskConnectorCanvasShape( shape ) ) {
- return null;
- }
-
- return canvasShapeToDeskWidget( shape ) ? shape : null;
-}
-
-export function getWidgetDropTargetAtPagePoint(
- editor: Editor,
- point: { x: number; y: number },
- sourceShapeId: TLShapeId,
- sourceWidget: DeskWidget
-): { shape: TLShape; widget: DeskWidget; handler: WidgetDropHandler } | null {
- const target = editor
- .getCurrentPageShapes()
- .filter( ( shape ) => shape.id !== sourceShapeId && ! isDeskConnectorCanvasShape( shape ) )
- .map( ( shape ) => {
- const bounds = editor.getShapePageBounds( shape.id );
- const widget = canvasShapeToDeskWidget( shape );
- const handler = widget ? getWidgetDropHandler( sourceWidget, widget ) : null;
- return bounds && widget && handler && isPointInBounds( point, bounds )
- ? { shape, widget, handler }
- : null;
- } )
- .filter(
- ( item ): item is { shape: TLShape; widget: DeskWidget; handler: WidgetDropHandler } =>
- item !== null
- )
- .sort( ( first, second ) => sortByIndex( second.shape, first.shape ) )[ 0 ];
-
- return target ?? null;
-}
-
-export function getDeskConnectorEndpoints(
- editor: Editor,
- connectorShapeId: TLShapeId
-): DeskConnectorEndpoints | null {
- const connectorShape = editor.getShape( connectorShapeId );
- if ( ! isDeskConnectorCanvasShape( connectorShape ) ) {
- return null;
- }
-
- const bindings = editor.getBindingsFromShape( connectorShapeId, 'arrow' );
- const startBinding = bindings.find(
- ( binding ) => getArrowBindingTerminal( binding.props ) === 'start'
- );
- const endBinding = bindings.find(
- ( binding ) => getArrowBindingTerminal( binding.props ) === 'end'
- );
- if ( ! startBinding || ! endBinding ) {
- return null;
- }
-
- return {
- sourceShapeId: startBinding.toId,
- targetShapeId: endBinding.toId,
- };
-}
-
-export function focusOnDeskShape( editor: Editor, shapeId: TLShapeId ) {
- editor.setSelectedShapes( [ shapeId ] );
- const bounds = editor.getShapePageBounds( shapeId );
- if ( ! bounds ) {
- return false;
- }
-
- editor.centerOnPoint( bounds.center, { animation: { duration: 320 } } );
- editor.focus();
- return true;
-}
-
-interface DeskConnectorEndpoints {
- sourceShapeId: TLShapeId;
- targetShapeId: TLShapeId;
-}
-
-function getWidgetForShapeId( editor: Editor, shapeId: TLShapeId ) {
- const shape = editor.getShape( shapeId ) as TLShape | undefined;
- return shape ? canvasShapeToDeskWidget( shape ) : null;
-}
-
-function getArrowBindingTerminal( props: object ) {
- return ( props as { terminal?: unknown } ).terminal;
-}
-
-function isPointInBounds(
- point: { x: number; y: number },
- bounds: { minX: number; minY: number; maxX: number; maxY: number }
-) {
- return (
- point.x >= bounds.minX &&
- point.x <= bounds.maxX &&
- point.y >= bounds.minY &&
- point.y <= bounds.maxY
- );
-}
diff --git a/apps/ui/src/ui-desks/controls/color/index.tsx b/apps/ui/src/ui-desks/controls/color/index.tsx
deleted file mode 100644
index de53ecb04b..0000000000
--- a/apps/ui/src/ui-desks/controls/color/index.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import { color as colorIcon } from '@wordpress/icons';
-import { Button, Menu } from '@/ui-desks/components';
-import styles from './style.module.css';
-import type { AnyColorControlConfig, ControlRendererProps } from '../types';
-import type { CSSProperties } from 'react';
-
-type ColorControlProps = ControlRendererProps & {
- control: AnyColorControlConfig;
-};
-
-export function ColorControl( {
- control,
- isOpen,
- setIsOpen,
- updateProps,
- props,
-}: ColorControlProps ) {
- const currentValue = props[ control.property ];
-
- return (
-
-
- }
- />
-
- { control.options.map( ( option ) => (
- {
- updateProps( { [ control.property ]: option.value } );
- } }
- />
- ) ) }
-
-
- );
-}
-
-function getSwatchStyle( color: string ): CSSProperties {
- return {
- '--control-swatch-color': color,
- } as CSSProperties;
-}
diff --git a/apps/ui/src/ui-desks/controls/color/style.module.css b/apps/ui/src/ui-desks/controls/color/style.module.css
deleted file mode 100644
index d8d2dfbe10..0000000000
--- a/apps/ui/src/ui-desks/controls/color/style.module.css
+++ /dev/null
@@ -1,36 +0,0 @@
-.swatchMenu {
- border-radius: var(--ui-desks-radius-surface, 18px);
-}
-
-.swatch,
-.swatch[data-highlighted],
-.swatch:hover {
- width: 30px;
- height: 30px;
- box-sizing: border-box;
- display: inline-flex;
- padding: 0;
- border: 1px solid rgba( 15, 23, 42, 0.12 );
- border-radius: 10px;
- corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- -webkit-corner-shape: var(--ui-desks-corner-shape, superellipse(1.42));
- background: var(--control-swatch-color);
- box-shadow: 0 6px 18px rgba( 15, 23, 42, 0.1 );
- color: transparent;
- cursor: pointer;
- outline: none;
- transition:
- box-shadow 160ms ease,
- transform 160ms ease;
-}
-
-.swatch[data-highlighted],
-.swatch:hover {
- box-shadow: 0 8px 22px rgba( 15, 23, 42, 0.14 );
- transform: translateY( -1px );
-}
-
-.swatch[data-active='true'] {
- outline: 2px solid var(--ui-desks-focus, #3858e9);
- outline-offset: 2px;
-}
diff --git a/apps/ui/src/ui-desks/controls/registry.tsx b/apps/ui/src/ui-desks/controls/registry.tsx
deleted file mode 100644
index 304ba44e2a..0000000000
--- a/apps/ui/src/ui-desks/controls/registry.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { ColorControl } from './color';
-import { SelectControl } from './select';
-import type { ControlRendererProps } from './types';
-
-export function ControlRenderer( props: ControlRendererProps ) {
- switch ( props.control.type ) {
- case 'color':
- return ;
- case 'select':
- return ;
- case 'custom': {
- const Component = props.control.Component;
- return (
-
- );
- }
- }
-}
diff --git a/apps/ui/src/ui-desks/controls/select/index.tsx b/apps/ui/src/ui-desks/controls/select/index.tsx
deleted file mode 100644
index 39fd2fcfb9..0000000000
--- a/apps/ui/src/ui-desks/controls/select/index.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import { Button, Menu } from '@/ui-desks/components';
-import type { AnySelectControlConfig, ControlRendererProps } from '../types';
-
-type SelectControlProps = ControlRendererProps & {
- control: AnySelectControlConfig;
-};
-
-export function SelectControl( {
- control,
- isOpen,
- setIsOpen,
- updateProps,
- props,
-}: SelectControlProps ) {
- const currentValue =
- typeof props[ control.property ] === 'string'
- ? ( props[ control.property ] as string )
- : control.defaultValue;
-
- return (
-
-
- }
- />
-
- {
- updateProps( { [ control.property ]: value } );
- setIsOpen( false );
- } }
- >
- { control.options.map( ( option ) => (
-
- { option.label }
-
- ) ) }
-
-
-
- );
-}
diff --git a/apps/ui/src/ui-desks/controls/types.ts b/apps/ui/src/ui-desks/controls/types.ts
deleted file mode 100644
index 9b5cdcb47c..0000000000
--- a/apps/ui/src/ui-desks/controls/types.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import type { Icon } from '@wordpress/ui';
-import type { ComponentProps, ReactElement } from 'react';
-
-export interface ColorControlOption< TValue extends string = string > {
- value: TValue;
- label: string;
- color: string;
-}
-
-export interface SelectControlOption< TValue extends string = string > {
- value: TValue;
- label: string;
-}
-
-type StringControlPropKey< TProps extends Record< string, unknown > > = {
- [ TKey in Extract< keyof TProps, string > ]: NonNullable< TProps[ TKey ] > extends string
- ? TKey
- : never;
-}[ Extract< keyof TProps, string > ];
-
-type ControlIcon = ComponentProps< typeof Icon >[ 'icon' ];
-
-export type ColorControlConfig<
- TProps extends Record< string, unknown > = Record< string, string >,
-> = {
- [ TProperty in StringControlPropKey< TProps > ]: {
- type: 'color';
- id: string;
- property: TProperty;
- label: string;
- options: Array< ColorControlOption< Extract< TProps[ TProperty ], string > > >;
- };
-}[ StringControlPropKey< TProps > ];
-
-export type SelectControlConfig<
- TProps extends Record< string, unknown > = Record< string, string >,
-> = {
- [ TProperty in StringControlPropKey< TProps > ]: {
- type: 'select';
- id: string;
- property: TProperty;
- label: string;
- icon: ControlIcon;
- defaultValue: Extract< NonNullable< TProps[ TProperty ] >, string >;
- options: Array< SelectControlOption< Extract< NonNullable< TProps[ TProperty ] >, string > > >;
- };
-}[ StringControlPropKey< TProps > ];
-
-export interface ControlRenderContext<
- TProps extends Record< string, unknown > = Record< string, unknown >,
-> {
- isOpen: boolean;
- setIsOpen: ( isOpen: boolean ) => void;
- updateProps: ( props: Record< string, unknown > ) => void;
- props: TProps;
-}
-
-type CustomControlComponent< TProps extends Record< string, unknown > > = {
- bivarianceHack( props: ControlRenderContext< TProps > ): ReactElement | null;
-}[ 'bivarianceHack' ];
-
-export interface CustomControlConfig<
- TProps extends Record< string, unknown > = Record< string, unknown >,
-> {
- type: 'custom';
- id: string;
- Component: CustomControlComponent< TProps >;
-}
-
-export type ControlConfig< TProps extends Record< string, unknown > = Record< string, string > > =
- | ColorControlConfig< TProps >
- | SelectControlConfig< TProps >
- | CustomControlConfig< TProps >;
-
-export interface AnyColorControlConfig {
- type: 'color';
- id: string;
- property: string;
- label: string;
- options: Array< ColorControlOption< string > >;
-}
-
-export interface AnySelectControlConfig {
- type: 'select';
- id: string;
- property: string;
- label: string;
- icon: ControlIcon;
- defaultValue: string;
- options: Array< SelectControlOption< string > >;
-}
-
-export type AnyControlConfig =
- | AnyColorControlConfig
- | AnySelectControlConfig
- | CustomControlConfig< Record< string, unknown > >;
-
-export interface ControlRendererProps {
- control: AnyControlConfig;
- isOpen: boolean;
- setIsOpen: ( isOpen: boolean ) => void;
- updateProps: ( props: Record< string, unknown > ) => void;
- props: Record< string, unknown >;
-}
diff --git a/apps/ui/src/ui-desks/design-system.md b/apps/ui/src/ui-desks/design-system.md
deleted file mode 100644
index 855fe662fe..0000000000
--- a/apps/ui/src/ui-desks/design-system.md
+++ /dev/null
@@ -1,63 +0,0 @@
-# UI Desks Design System
-
-`ui-desks` has its own shared UI components in `components/`. These components are the preferred building blocks for desks UI. They keep interaction details, sizes, spacing, and visual treatment consistent without coupling desks screens to the broader Studio UI or to third-party component APIs.
-
-This guidance applies to components used inside `apps/ui/src/ui-desks`. Components outside `ui-desks` are not considered duplicates unless `ui-desks` imports and uses them.
-
-When adding or updating desks UI, prefer imports from `@/ui-desks/components`.
-
-## Current Shared Components
-
-- `Button`: shared desks button primitive for icon buttons, toolbar buttons, quiet actions, and filled actions.
-- `Dialog`: shared centered modal dialog primitive, including `DialogHeader`, `DialogTitle`, `DialogContent`, `DialogFooter`, `DialogCloseButton`, `DialogRow`, `DialogError`, and `DialogTip`.
-- `Menu`: shared menu primitives for desks popups and dropdown-style actions.
-- `Surface` and `Divider`: shared visual containers and separators.
-- `List` and `ListItem`: shared list navigation/action primitives.
-- `LoadingPlaceholder`: shared loading skeleton placeholder.
-
-## Buttons
-
-Use `Button` for user-visible button controls in desks UI. Do not import `Button` or `IconButton` directly from `@wordpress/ui` or `@wordpress/components` in `ui-desks`.
-
-Button size should drive icon size. Avoid adding per-call icon sizing unless there is a clear new component need.
-
-Use variants by intent:
-
-- `chrome`: floating chrome and toolbar controls.
-- `quiet`: low-emphasis actions, icon-only utility actions, and controls that should visually recede until hovered or active.
-- `filled`: emphasized local actions, form actions, and primary dialog actions.
-
-Prefer `tone` when the action needs a semantic color treatment instead of overriding button colors locally. Use `tone="primary"` for brand-colored primary actions and `tone="inverse"` for strong black/white actions.
-
-Use `intent="chat"` for primary actions that start, submit, or hand work to chat or the agent. The chat intent adds the shared blue glow only when combined with `variant="filled"` and `tone="primary"`.
-
-Local button classes are acceptable for layout constraints such as width, positioning, or contextual spacing. Avoid local classes that recreate button states, icon sizing, hover treatment, or disabled treatment. If the same visual override appears in multiple places, add a Button variant or a small shared wrapper instead.
-
-Raw `