- { /* Equal-flex side tracks keep the address control truly centered
- in the toolbar regardless of what each side holds. */ }
-
- { /* Back/forward flank the address segments so history controls sit
- with the place they navigate; symmetric widths keep the segments
- (and the omnibox popup anchored to this element) centered. */ }
-
- { canPreview ? (
+ { /* Browser navigation stays at the start, the address field fills the
+ available middle track, and preview actions stay at the end. */ }
+
+ { canPreview && ! inspectorState.isPicking ? (
<>
-
sendBrowserCommand( 'back' ) }
+ onStep={ () => sendBrowserCommand( 'back' ) }
+ onJump={ goToHistoryIndex }
/>
- onPathChange?.( nextPath ) }
- onSwitchRealm={ handleSwitchRealm }
+ sendBrowserCommand( 'forward' ) }
+ onJump={ goToHistoryIndex }
/>
sendBrowserCommand( 'forward' ) }
+ icon={ refreshIcon }
+ label={ __( 'Refresh' ) }
+ shortcut={ browserShortcuts.reload }
+ onClick={ () => sendBrowserCommand( 'reload' ) }
/>
>
) : null }
+
+ { canPreview && ! inspectorState.isPicking ? (
+
onPathChange?.( nextPath ) }
+ onSwitchRealm={ handleSwitchRealm }
+ />
+ ) : null }
+
+ { canPreview ? (
+
+ ) : null }
{ canPreview && chatEnabled && connector.capabilities.annotatePreview ? (
) : null }
-
- { canPreview ? (
-
onFullscreenChange( ! fullscreen ) }
+ positioner={ }
/>
) : null }
@@ -1319,11 +1535,14 @@ export function SitePreview( {
inspector={ isResponsiveSurface( key ) }
onAnnotationsDone={ onAnnotationsDone }
onInspectorState={ ( state ) => patchSurface( key, { inspector: state } ) }
+ onInspectorCancelRequest={ () =>
+ setAnnotationCancelRequestId( ( current ) => current + 1 )
+ }
inspectorCommand={ surface.inspectorCommand }
browserCommand={ surface.browserCommand }
- onBrowserStateChange={ ( state ) =>
- patchSurface( key, { browser: state } )
- }
+ onBrowserStateChange={ ( state ) => {
+ patchSurface( key, { browser: state } );
+ } }
onBrowserCommand={ handleForwardedShortcut }
onNavigate={ ( url ) => handleSurfaceNavigation( key, url ) }
viewport={ viewport }
@@ -1340,13 +1559,14 @@ export function SitePreview( {
src={ surfaceUrl }
title={ site.name }
onLoad={ ( event ) => {
+ const title = getIframeTitle( event.currentTarget );
handleSurfaceNavigation( key, event.currentTarget.src );
patchSurface( key, {
browser: {
...surface.browser,
loading: false,
progress: 0,
- title: getIframeTitle( event.currentTarget ),
+ title,
},
} );
} }
@@ -1463,6 +1683,7 @@ interface WebviewSurfaceProps {
reloadNonce: number;
onAnnotationsDone?: ( annotations: Annotation[] ) => void;
onInspectorState?: ( state: InspectorState ) => void;
+ onInspectorCancelRequest?: () => void;
inspectorCommand?: InspectorCommand | null;
browserCommand?: BrowserCommand | null;
onBrowserStateChange?: ( state: BrowserNavigationState ) => void;
@@ -1488,6 +1709,7 @@ function WebviewSurface( {
reloadNonce,
onAnnotationsDone,
onInspectorState,
+ onInspectorCancelRequest,
inspectorCommand,
browserCommand,
onBrowserStateChange,
@@ -1503,6 +1725,7 @@ function WebviewSurface( {
const [ domReadyCount, setDomReadyCount ] = useState( 0 );
const onAnnotationsDoneRef = useRef( onAnnotationsDone );
const onInspectorStateRef = useRef( onInspectorState );
+ const onInspectorCancelRequestRef = useRef( onInspectorCancelRequest );
const onBrowserStateChangeRef = useRef( onBrowserStateChange );
const onBrowserCommandRef = useRef( onBrowserCommand );
const onNavigateRef = useRef( onNavigate );
@@ -1520,6 +1743,9 @@ function WebviewSurface( {
useEffect( () => {
onInspectorStateRef.current = onInspectorState;
}, [ onInspectorState ] );
+ useEffect( () => {
+ onInspectorCancelRequestRef.current = onInspectorCancelRequest;
+ }, [ onInspectorCancelRequest ] );
useEffect( () => {
onBrowserStateChangeRef.current = onBrowserStateChange;
}, [ onBrowserStateChange ] );
@@ -1618,6 +1844,25 @@ function WebviewSurface( {
const webview = ref.current as WebviewTag | null;
if ( ! webview ) return;
+ const publishNavigationHistory = () => {
+ const { ipcApi } = window as PreviewWindow;
+ let webContentsId: number;
+ try {
+ webContentsId = getWebviewContentsId( webview );
+ } catch {
+ return;
+ }
+ void ipcApi
+ ?.getWebviewNavigationHistory?.( webContentsId )
+ .then( ( history ) => {
+ publishBrowserState( {
+ historyEntries: history.entries,
+ activeHistoryIndex: history.activeIndex,
+ } );
+ } )
+ .catch( () => undefined );
+ };
+
const publishDocumentTitle = ( title?: unknown ) => {
if ( typeof title === 'string' ) {
publishBrowserState( { title: normalizeDocumentTitle( title ) } );
@@ -1637,6 +1882,7 @@ function WebviewSurface( {
setReady( true );
setDomReadyCount( ( count ) => count + 1 );
publishDocumentTitle();
+ publishNavigationHistory();
if ( ! inspectorEnabledRef.current ) {
return;
}
@@ -1653,6 +1899,7 @@ function WebviewSurface( {
ready: true,
isPicking: false,
annotationCount: stored.length,
+ hasUnsavedDraft: false,
} );
} )
.catch( () => {
@@ -1678,11 +1925,16 @@ function WebviewSurface( {
}
return;
}
+ if ( parsed.type === 'cancel-requested' ) {
+ onInspectorCancelRequestRef.current?.();
+ return;
+ }
if ( parsed.type === 'state' ) {
onInspectorStateRef.current?.( {
ready: true,
isPicking: Boolean( parsed.isPicking ),
annotationCount: typeof parsed.annotationCount === 'number' ? parsed.annotationCount : 0,
+ hasUnsavedDraft: Boolean( parsed.hasUnsavedDraft ),
} );
return;
}
@@ -1717,6 +1969,7 @@ function WebviewSurface( {
}
didReadTitleAfterLoad = false;
publishBrowserState();
+ publishNavigationHistory();
};
const handleStartLoading = () => {
didReadTitleAfterLoad = false;
@@ -1731,6 +1984,7 @@ function WebviewSurface( {
};
const handleStopLoading = () => {
finishProgress();
+ publishNavigationHistory();
if ( domReadyRef.current && ! didReadTitleAfterLoad ) {
didReadTitleAfterLoad = true;
publishDocumentTitle();
@@ -1835,6 +2089,16 @@ function WebviewSurface( {
webview.goBack?.();
} else if ( browserCommand.type === 'forward' && webview.canGoForward?.() ) {
webview.goForward?.();
+ } else if (
+ browserCommand.type === 'go-to-history' &&
+ typeof browserCommand.historyIndex === 'number'
+ ) {
+ const { ipcApi } = window as PreviewWindow;
+ const webContentsId = getWebviewContentsId( webview );
+ void ipcApi?.goToWebviewNavigationHistoryEntry?.(
+ webContentsId,
+ browserCommand.historyIndex
+ );
} else if ( browserCommand.type === 'reload' ) {
void reloadPreview( webview, urlRef.current, currentUrlRef.current );
}
diff --git a/apps/ui/src/components/site-preview/inspector-script.test.ts b/apps/ui/src/components/site-preview/inspector-script.test.ts
index 42fe1060b5..705fe5c46b 100644
--- a/apps/ui/src/components/site-preview/inspector-script.test.ts
+++ b/apps/ui/src/components/site-preview/inspector-script.test.ts
@@ -68,7 +68,7 @@ describe( 'site preview inspector sessions', () => {
expect( latestState( log ) ).toMatchObject( { isPicking: false, annotationCount: 0 } );
} );
- it( 'keeps saved notes when annotation mode is switched off', () => {
+ it( 'requests cancellation when Escape is pressed while annotating', () => {
const log = vi.spyOn( console, 'log' ).mockImplementation( () => undefined );
seedSavedNote();
@@ -78,12 +78,45 @@ describe( 'site preview inspector sessions', () => {
new KeyboardEvent( 'keydown', { key: 'Escape', bubbles: true, cancelable: true } )
);
- expect( latestState( log ) ).toMatchObject( { isPicking: false, annotationCount: 1 } );
+ expect( bridgeMessages( log ) ).toContainEqual( { type: 'cancel-requested' } );
+ expect( latestState( log ) ).toMatchObject( { isPicking: true, annotationCount: 1 } );
expect(
( window as Window & { __studioInspectorState?: unknown[] } ).__studioInspectorState
).toHaveLength( 1 );
} );
+ it( 'discards saved notes and unfinished drafts when cancelled', () => {
+ const log = vi.spyOn( console, 'log' ).mockImplementation( () => undefined );
+ document.body.innerHTML = '
Draft ';
+ const draft = document.querySelector( '#draft' ) as HTMLElement;
+ vi.spyOn( draft, 'getBoundingClientRect' ).mockReturnValue( rect( 10, 10 ) );
+ seedSavedNote();
+
+ new Function( INSPECTOR_PAGE_SCRIPT )();
+ const root = ( document.querySelector( '#__studio-inspector-host' ) as HTMLElement )
+ .shadowRoot as ShadowRoot;
+ command( 'toggle-picking' );
+ draft.dispatchEvent( new MouseEvent( 'click', { bubbles: true, cancelable: true } ) );
+ const textarea = root.querySelector( 'textarea' ) as HTMLTextAreaElement;
+ textarea.value = 'Unfinished note';
+ textarea.dispatchEvent( new InputEvent( 'input', { bubbles: true } ) );
+ expect( latestState( log ) ).toMatchObject( { hasUnsavedDraft: true } );
+
+ command( 'cancel' );
+
+ expect( latestState( log ) ).toMatchObject( {
+ isPicking: false,
+ annotationCount: 0,
+ hasUnsavedDraft: false,
+ } );
+ expect( root.querySelector( '.popup' ) ).toBeNull();
+ expect( root.querySelectorAll( '.marker' ) ).toHaveLength( 0 );
+ expect(
+ ( window as Window & { __studioInspectorState?: unknown[] } ).__studioInspectorState
+ ).toEqual( [] );
+ expect( bridgeMessages( log ).find( ( message ) => message.type === 'done' ) ).toBeUndefined();
+ } );
+
it( 'reopens an existing note without turning picking back on', () => {
const log = vi.spyOn( console, 'log' ).mockImplementation( () => undefined );
seedSavedNote();
diff --git a/apps/ui/src/components/site-preview/inspector-script.ts b/apps/ui/src/components/site-preview/inspector-script.ts
index c607537b81..aa1308e02b 100644
--- a/apps/ui/src/components/site-preview/inspector-script.ts
+++ b/apps/ui/src/components/site-preview/inspector-script.ts
@@ -280,6 +280,7 @@ export const INSPECTOR_PAGE_SCRIPT =
type: 'state',
isPicking,
annotationCount: annotations.length,
+ hasUnsavedDraft: hasDraft(),
} );
}
@@ -426,10 +427,23 @@ export const INSPECTOR_PAGE_SCRIPT =
render();
}
+ function cancelAnnotations() {
+ annotations = [];
+ activePopup = null;
+ isPicking = false;
+ hoveredEl = null;
+ persistAnnotations();
+ render();
+ }
+
window.addEventListener(
COMMAND_EVENT,
( event ) => {
const command = event.detail || {};
+ if ( command.type === 'cancel' ) {
+ cancelAnnotations();
+ return;
+ }
if ( command.type === 'toggle-picking' ) {
togglePicking();
return;
@@ -545,6 +559,7 @@ export const INSPECTOR_PAGE_SCRIPT =
ta.addEventListener( 'input', () => {
state.comment = ta.value;
syncActions();
+ sendState();
} );
ta.addEventListener( 'keydown', ( event ) => {
if ( event.key !== 'Enter' || event.isComposing || event.keyCode === 229 ) return;
@@ -662,15 +677,14 @@ export const INSPECTOR_PAGE_SCRIPT =
return;
}
if ( e.key !== 'Escape' ) return;
- if ( activePopup ) {
+ if ( isPicking ) {
+ e.preventDefault();
+ e.stopPropagation();
+ send( { type: 'cancel-requested' } );
+ } else if ( activePopup ) {
activePopup = null;
persistAnnotations();
render();
- } else if ( isPicking ) {
- isPicking = false;
- hoveredEl = null;
- persistAnnotations();
- render();
}
},
{ capture: true, signal: teardown.signal }
diff --git a/apps/ui/src/components/site-preview/style.module.css b/apps/ui/src/components/site-preview/style.module.css
index 5651e4dc15..7437884a56 100644
--- a/apps/ui/src/components/site-preview/style.module.css
+++ b/apps/ui/src/components/site-preview/style.module.css
@@ -19,12 +19,18 @@
border-inline-start: none;
}
+.header.browserChromeHidden {
+ display: none;
+}
+
.header {
position: relative;
- display: flex;
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: var(--wpds-dimension-padding-md);
padding: var(--wpds-dimension-padding-xs) var(--wpds-dimension-padding-lg);
+ padding-inline-start: var(--wpds-dimension-padding-md);
min-height: 32px;
flex-shrink: 0;
background-color: var(--wpds-color-bg-surface-neutral-strong, #fff);
@@ -47,30 +53,58 @@
-webkit-app-region: no-drag;
}
-/* Matching flexible side tracks: because both flex from the same 0 basis,
- the address control between them sits at the toolbar's true center no
- matter how many controls each side holds. They keep their automatic
- min-content floor, so a narrow toolbar shrinks the address segments
- between them rather than letting the (non-shrinking) button groups
- spill out of their track and over the address bar. */
+/* @wordpress/icons SVGs omit a root fill and otherwise fall back to black.
+ Map them to each control's theme-aware foreground; stroke-based custom
+ browser icons declare fill="none" and remain unchanged. */
+.header svg:not([fill]) {
+ fill: currentColor;
+}
+
+/* The outer tracks size to their controls so the middle selector can use all
+ remaining width instead of being squeezed by artificial equal halves. */
.headerSide {
display: flex;
- flex: 1 1 0;
align-items: center;
gap: var(--wpds-dimension-padding-md);
}
+.headerSideStart {
+ gap: var(--wpds-dimension-padding-xs);
+}
+
.headerSideEnd {
justify-content: flex-end;
}
.browserLocation {
display: flex;
- flex: 0 1 auto;
align-items: center;
justify-content: center;
- gap: var(--wpds-dimension-padding-xs);
min-width: 0;
+ padding-inline: var(--wpds-dimension-gap-sm);
+}
+
+.historyEntry {
+ display: flex;
+ width: min(320px, calc(100vw - 64px));
+ min-width: 0;
+ flex-direction: column;
+}
+
+.historyTitle,
+.historyPath {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.historyTitle {
+ color: var(--wpds-color-fg-content-neutral);
+}
+
+.historyPath {
+ color: var(--wpds-color-fg-content-neutral-weak);
+ font-size: var(--wpds-typography-font-size-xs);
}
.annotationControls {
@@ -80,12 +114,68 @@
flex-shrink: 0;
}
+.disabledControlTooltipTarget {
+ display: inline-flex;
+}
+
+.annotationToggle {
+ --wp-ui-button-font-size: var(--wpds-typography-font-size-sm);
+ --wp-ui-button-font-weight: var(--wpds-typography-font-weight-regular);
+}
+
.annotationMenu {
display: none;
align-items: center;
flex-shrink: 0;
}
+.responsiveModeIndicator {
+ width: 18px;
+ height: 18px;
+ flex: 0 0 18px;
+ color: var(--wpds-color-fg-content-neutral);
+ fill: currentColor;
+ display: grid;
+ place-items: center;
+}
+
+.responsiveModeTrigger {
+ --wp-ui-button-font-size: var(--wpds-typography-font-size-sm);
+ --wp-ui-button-font-weight: var(--wpds-typography-font-weight-regular);
+
+ white-space: nowrap;
+}
+
+.responsiveModeLabel {
+ white-space: nowrap;
+}
+
+.responsiveModeChevron {
+ color: var(--wpds-color-fg-content-neutral-weak);
+ fill: currentColor;
+}
+
+.responsiveModeItem[aria-current='true'] {
+ background: var(--wpds-color-bg-surface-neutral);
+}
+
+/* Preserve the URL field before toolbar actions begin colliding. The controls
+ keep their accessible names and tooltips; only the redundant visible labels
+ collapse, leaving the same icons and dropdown indicator. */
+@container studio-preview-toolbar (max-width: 640px) {
+ .toolbarLabel {
+ display: none;
+ }
+
+ .headerSide {
+ gap: var(--wpds-dimension-padding-xs);
+ }
+
+ .browserLocation {
+ padding-inline: 0;
+ }
+}
+
/* Below this width the inline "Send to chat" button starts eating into the
address segments, so the pair folds into a split button. Only applies to
the pending-notes layout: a lone annotate toggle always fits. The query
diff --git a/apps/ui/src/components/split-button/style.module.css b/apps/ui/src/components/split-button/style.module.css
index 7c9c06838a..eb84c8fdcd 100644
--- a/apps/ui/src/components/split-button/style.module.css
+++ b/apps/ui/src/components/split-button/style.module.css
@@ -3,23 +3,42 @@
"Open in…" control and the collapsed annotation control, which sit next
to each other and have to read as the same kind of thing. */
.splitTrigger {
+ position: relative;
display: inline-flex;
align-items: center;
gap: 1px;
flex: 0 0 auto;
- border: var(--wpds-border-width-xs) solid var(--wpds-color-stroke-surface-neutral-weak);
- /* The buttons' own radius plus the border width, so the halves' rounded
- outer corners nest flush against the frame. */
- border-radius: calc(var(--wpds-border-radius-sm) + var(--wpds-border-width-xs));
+ border-radius: var(--wpds-border-radius-sm);
-webkit-app-region: no-drag;
}
+/* WPDS buttons include their border inside the fixed control height. Draw the
+ shared split-button frame as an inset overlay too, so it stays 24px tall
+ rather than adding a pixel above and below the two button halves. */
+.splitTrigger::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ border: var(--wpds-border-width-xs) solid transparent;
+ border-radius: inherit;
+ pointer-events: none;
+ transition: border-color 100ms ease;
+}
+
+.splitTrigger:is(:hover, :focus-within)::after {
+ border-color: var(--wpds-color-stroke-surface-neutral-weak);
+}
+
/* Size the halves so the pair reads as one compact button: normal padding
on the outer edges, almost none along the shared seam, and a very narrow
chevron tab. Squaring off the shared corners makes the hover fills read
as one control split in two. */
.splitAction {
--wp-ui-button-min-width: unset;
+ --wp-ui-button-font-size: var(--wpds-typography-font-size-sm);
+ --wp-ui-button-font-weight: var(--wpds-typography-font-weight-regular);
+ --wp-ui-button-background-color: transparent;
+ --wp-ui-button-border-color: transparent;
padding-inline: var(--wpds-dimension-padding-xs) 1px;
border-start-end-radius: 0;
@@ -28,6 +47,8 @@
.splitMenuButton {
--wp-ui-button-min-width: unset;
+ --wp-ui-button-background-color: transparent;
+ --wp-ui-button-border-color: transparent;
padding-inline: 1px 3px;
border-start-start-radius: 0;
diff --git a/apps/ui/src/lib/icons.tsx b/apps/ui/src/lib/icons.tsx
index 4460fc5345..c0ce02f288 100644
--- a/apps/ui/src/lib/icons.tsx
+++ b/apps/ui/src/lib/icons.tsx
@@ -5,18 +5,16 @@ export const playIcon = (
);
export const refreshIcon = (
-
+
);
+export const browserBackIcon = (
+
+
+
+);
+
+export const browserForwardIcon = (
+
+
+
+);
+
export const drawerIcon = (
diff --git a/apps/ui/src/ui-classic/components/session-view/index.test.tsx b/apps/ui/src/ui-classic/components/session-view/index.test.tsx
index e6def04d55..f547ffe245 100644
--- a/apps/ui/src/ui-classic/components/session-view/index.test.tsx
+++ b/apps/ui/src/ui-classic/components/session-view/index.test.tsx
@@ -5,7 +5,10 @@ import { useSession } from '@/data/queries/use-sessions';
import { isScrolledAwayFromLatest, SessionView } from './index';
import type { LoadedAiSession } from '@/data/core';
-const { navigateMock } = vi.hoisted( () => ( { navigateMock: vi.fn() } ) );
+const { navigateMock, sitesState } = vi.hoisted( () => ( {
+ navigateMock: vi.fn(),
+ sitesState: { data: [] as Array< { id: string; name: string; path: string } > },
+} ) );
vi.mock( '@tanstack/react-router', () => ( {
useNavigate: () => navigateMock,
@@ -19,7 +22,21 @@ vi.mock( '@/data/queries/use-sessions', () => ( {
} ) );
vi.mock( '@/data/queries/use-sites', () => ( {
- useSites: () => ( { data: [] } ),
+ useSites: () => sitesState,
+} ) );
+
+vi.mock( '@/components/open-in-menu', () => ( {
+ OpenInMenu: ( { site, browserPath }: { site: { name: string }; browserPath: string } ) => (
+ { `${ site.name }:${ browserPath }` }
+ ),
+} ) );
+
+vi.mock( '@/components/site-dropdown', () => ( {
+ SiteDropdown: ( { site }: { site: { name: string } } ) => { site.name }
,
+} ) );
+
+vi.mock( '@/components/preview-toggle-button', () => ( {
+ PreviewToggleButton: () => null,
} ) );
vi.mock( '@/data/queries/use-assistant-quota', () => ( {
@@ -53,6 +70,11 @@ vi.mock( '@/hooks/use-session-commands', () => ( { useSessionCommands: vi.fn() }
vi.mock( '@/hooks/use-session-ui', () => ( {
SessionUIProvider: ( { children }: { children: React.ReactNode } ) => children,
useSessionPreviewAnnotations: vi.fn(),
+ useSessionPreviewUI: () => ( {
+ pathsBySiteId: { 'site-1': '/wp-admin/' },
+ } ),
+ pathForSite: ( pathsBySiteId: Record< string, string >, siteId: string ) =>
+ pathsBySiteId[ siteId ] ?? '/',
} ) );
vi.mock( '@/hooks/use-traffic-light-space', () => ( {
@@ -68,6 +90,11 @@ vi.mock( './conversation', () => ( {
Conversation: () =>
,
} ) );
+vi.mock( './session-chat-actions', async ( importOriginal ) => ( {
+ ...( await importOriginal< object >() ),
+ SessionChatActions: () => null,
+} ) );
+
const useSessionMock = vi.mocked( useSession, { partial: true } );
const useStudioAssistantQuotaMock = vi.mocked( useStudioAssistantQuota, { partial: true } );
@@ -108,6 +135,7 @@ function setScrollMetrics(
describe( 'SessionView', () => {
beforeEach( () => {
vi.clearAllMocks();
+ sitesState.data = [];
// Entitled account by default; individual tests override.
useStudioAssistantQuotaMock.mockReturnValue( {
data: makeQuota( {} ),
@@ -117,6 +145,28 @@ describe( 'SessionView', () => {
} );
} );
+ it( 'shows the Open in control at the top-right of the chat header', () => {
+ sitesState.data = [
+ { id: 'site-1', name: 'Example Site', path: '/Users/example/Studio/example-site' },
+ ];
+ useSessionMock.mockReturnValue( {
+ data: {
+ summary: {
+ id: 'session-1',
+ ownerSiteId: 'site-1',
+ ownerSiteName: 'Example Site',
+ },
+ entries: [],
+ } as unknown as LoadedAiSession,
+ isLoading: false,
+ error: null,
+ } );
+
+ render( );
+
+ expect( screen.getByTestId( 'open-in-menu' ) ).toHaveTextContent( 'Example Site:/wp-admin/' );
+ } );
+
it( 'redirects to the root instead of flashing the error when the session is gone', async () => {
useSessionMock.mockReturnValue( {
data: undefined,
diff --git a/apps/ui/src/ui-classic/components/session-view/index.tsx b/apps/ui/src/ui-classic/components/session-view/index.tsx
index b6d1be570a..32107d9bfd 100644
--- a/apps/ui/src/ui-classic/components/session-view/index.tsx
+++ b/apps/ui/src/ui-classic/components/session-view/index.tsx
@@ -17,6 +17,7 @@ import {
type Ref,
} from 'react';
import { OutOfCreditsNotice } from '@/components/ai-access-required-notice';
+import { OpenInMenu } from '@/components/open-in-menu';
import { PreviewToggleButton } from '@/components/preview-toggle-button';
import { ProgressiveBlur } from '@/components/progressive-blur';
import { SiteDropdown } from '@/components/site-dropdown';
@@ -33,7 +34,12 @@ import {
import { useSites } from '@/data/queries/use-sites';
import { useIsOutOfAiCredits } from '@/hooks/use-is-out-of-ai-credits';
import { useSessionCommands } from '@/hooks/use-session-commands';
-import { SessionUIProvider, useSessionPreviewAnnotations } from '@/hooks/use-session-ui';
+import {
+ pathForSite,
+ SessionUIProvider,
+ useSessionPreviewAnnotations,
+ useSessionPreviewUI,
+} from '@/hooks/use-session-ui';
import { useSidebarCollapsed } from '@/hooks/use-sidebar-collapsed';
import { useTrafficLightSpace } from '@/hooks/use-traffic-light-space';
import { formatComposerTextQuote, watchComposerTextQuote } from '@/lib/composer-text-quote';
@@ -73,6 +79,7 @@ function SessionHeader( { summary }: SessionHeaderProps ) {
const { data: sites } = useSites();
const site = findAiSessionOwnerSite( sites, summary );
const effectiveEnvironment = useSessionEffectiveEnvironment( summary, site?.id );
+ const preview = useSessionPreviewUI();
if ( ! siteName ) {
return null;
}
@@ -102,6 +109,15 @@ function SessionHeader( { summary }: SessionHeaderProps ) {
>
) }
+ { site ? (
+
+
+
+ ) : null }
);
}
diff --git a/apps/ui/src/ui-classic/components/session-view/style.module.css b/apps/ui/src/ui-classic/components/session-view/style.module.css
index 334aac57ff..23acd3b24b 100644
--- a/apps/ui/src/ui-classic/components/session-view/style.module.css
+++ b/apps/ui/src/ui-classic/components/session-view/style.module.css
@@ -42,7 +42,7 @@
align-items: center;
gap: var(--wpds-dimension-padding-sm);
padding-block: var(--wpds-dimension-padding-sm);
- padding-inline: var(--wpds-dimension-padding-sm) var(--wpds-dimension-padding-2xl);
+ padding-inline: var(--wpds-dimension-padding-sm) 6px;
min-height: 46px;
font-size: var(--wpds-typography-font-size-sm);
color: var(--wpds-color-fg-content-neutral);
@@ -50,11 +50,7 @@
}
.headerSidebarCollapsed {
- padding-top: calc(
- var(--wpds-dimension-padding-xl) + var(--wpds-dimension-padding-2xl) +
- var(--wpds-dimension-padding-lg)
- );
- padding-inline-start: var(--wpds-dimension-padding-xl);
+ padding-inline-start: 94px;
}
.headerSite {
@@ -85,6 +81,15 @@
flex: 1;
}
+.headerActions {
+ position: relative;
+ inset-block-start: -7px;
+ display: flex;
+ align-items: center;
+ margin-inline-end: var(--wpds-dimension-padding-sm);
+ -webkit-app-region: no-drag;
+}
+
.scroll {
position: absolute;
inset: 0;