From cb17a9efb2dffb35ab5f827f0766d17b94fab91f Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 22 Jun 2026 19:21:20 -0500 Subject: [PATCH] fix(desktop): stop auto-opening tool previews Drop gateway-event preview registration so HTML artifacts from tool results no longer pop the rail. De-dupe the inline preview card label. --- .../hooks/use-preview-routing.test.tsx | 32 +----- .../app/session/hooks/use-preview-routing.ts | 108 ++---------------- .../components/chat/preview-attachment.tsx | 13 +-- 3 files changed, 19 insertions(+), 134 deletions(-) diff --git a/apps/desktop/src/app/session/hooks/use-preview-routing.test.tsx b/apps/desktop/src/app/session/hooks/use-preview-routing.test.tsx index 1134ffe4fae..119bb51a040 100644 --- a/apps/desktop/src/app/session/hooks/use-preview-routing.test.tsx +++ b/apps/desktop/src/app/session/hooks/use-preview-routing.test.tsx @@ -120,31 +120,7 @@ describe('usePreviewRouting', () => { expect(window.hermesDesktop.normalizePreviewTarget).not.toHaveBeenCalled() }) - it('registers structured tool-result preview targets', async () => { - render( - { - handleEvent = handler - }} - /> - ) - - act(() => - handleEvent({ - payload: { path: './dist/index.html' }, - session_id: 'session-1', - type: 'tool.complete' - }) - ) - - await waitFor(() => { - expect($previewTarget.get()?.source).toBe('./dist/index.html') - }) - - expect(window.localStorage.getItem('hermes.desktop.sessionPreviews.v1')).toContain('./dist/index.html') - }) - - it('registers html previews from edit inline diffs', async () => { + it('does not auto-open a preview from tool results', async () => { render( { @@ -160,9 +136,9 @@ describe('usePreviewRouting', () => { type: 'tool.complete' }) ) + act(() => handleEvent({ payload: { path: './dist/index.html' }, session_id: 'session-1', type: 'tool.complete' })) - await waitFor(() => { - expect($previewTarget.get()?.source).toBe('preview-demo.html') - }) + expect($previewTarget.get()).toBeNull() + expect(window.localStorage.getItem('hermes.desktop.sessionPreviews.v1')).toBeNull() }) }) diff --git a/apps/desktop/src/app/session/hooks/use-preview-routing.ts b/apps/desktop/src/app/session/hooks/use-preview-routing.ts index 0d48927af5e..d2c13ba56ab 100644 --- a/apps/desktop/src/app/session/hooks/use-preview-routing.ts +++ b/apps/desktop/src/app/session/hooks/use-preview-routing.ts @@ -10,8 +10,7 @@ import { getSessionPreviewRecord, progressPreviewServerRestart, requestPreviewReload, - setPreviewTarget, - setSessionPreviewTarget + setPreviewTarget } from '@/store/preview' import { $currentCwd } from '@/store/session' import type { RpcEvent } from '@/types/hermes' @@ -40,53 +39,6 @@ function activePreviewSessionId( return selectedStoredSessionId || routedSessionId || activeSessionIdRef.current || '' } -function looksLikePreviewTarget(value: string): boolean { - return /^https?:\/\//i.test(value) || /^file:\/\//i.test(value) || /^(?:\/|\.{1,2}\/|~\/).+/.test(value) -} - -function stripAnsi(value: string): string { - return value.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g'), '') -} - -function htmlPathFromInlineDiff(value: string): string { - const cleaned = stripAnsi(value).replace(/^\s*┊\s*review diff\s*\n/i, '') - - for (const match of cleaned.matchAll(/(?:^|\s)(?:[ab]\/)?([^\s]+\.html?)(?=\s|$)/gi)) { - const candidate = match[1]?.trim() - - if (candidate) { - return candidate - } - } - - return '' -} - -function structuredPreviewCandidate(payload: unknown): string { - const record = asRecord(payload) - const fields = ['url', 'target', 'path', 'file', 'filepath', 'preview'] - - for (const field of fields) { - const value = record[field] - - if (typeof value === 'string') { - const target = value.trim() - - if (target && looksLikePreviewTarget(target)) { - return target - } - } - } - - const inlineDiff = record.inline_diff - - if (typeof inlineDiff === 'string') { - return htmlPathFromInlineDiff(inlineDiff) - } - - return '' -} - export function usePreviewRouting({ activeSessionIdRef, baseHandleGatewayEvent, @@ -99,6 +51,10 @@ export function usePreviewRouting({ const previewRegistry = useStore($sessionPreviewRegistry) const previewSessionId = activePreviewSessionId(activeSessionIdRef, routedSessionId, selectedStoredSessionId) + // Restore a *user-opened* preview when its session becomes active. Tool + // results no longer auto-register/open a preview — the inline preview card in + // the tool row is the only entry point, so HTML artifacts never pop the rail + // open on their own. useEffect(() => { if (currentView !== 'chat' || !previewSessionId) { setPreviewTarget(null) @@ -111,53 +67,6 @@ export function usePreviewRouting({ setPreviewTarget(record?.normalized ?? null) }, [currentView, previewRegistry, previewSessionId]) - const registerStructuredPreview = useCallback( - async (event: RpcEvent) => { - if ( - event.session_id && - event.session_id !== activeSessionIdRef.current && - event.session_id !== previewSessionId - ) { - return - } - - if (!event.type.startsWith('tool.')) { - return - } - - if (!previewSessionId) { - return - } - - const candidate = structuredPreviewCandidate(event.payload) - - if (!candidate) { - return - } - - const desktop = window.hermesDesktop - - if (!desktop?.normalizePreviewTarget) { - return - } - - const sessionId = previewSessionId - const cwd = currentCwd || '' - const target = await desktop.normalizePreviewTarget(candidate, cwd || undefined).catch(() => null) - - if ( - !target || - sessionId !== activePreviewSessionId(activeSessionIdRef, routedSessionId, selectedStoredSessionId) || - $currentCwd.get() !== cwd - ) { - return - } - - setSessionPreviewTarget(sessionId, target, 'tool-result', candidate) - }, - [activeSessionIdRef, currentCwd, previewSessionId, routedSessionId, selectedStoredSessionId] - ) - const restartPreviewServer = useCallback( async (url: string, context?: string) => { const sessionId = activeSessionIdRef.current @@ -210,13 +119,14 @@ export function usePreviewRouting({ return } - void registerStructuredPreview(event) - + // Only refresh an already-open live preview when a file changes; never + // open one unprompted. (Preview links are surfaced from the tool row into + // the status stack — see tool-fallback.tsx.) if ($previewTarget.get()?.kind === 'url' && gatewayEventCompletedFileDiff(event)) { requestPreviewReload() } }, - [activeSessionIdRef, baseHandleGatewayEvent, registerStructuredPreview] + [activeSessionIdRef, baseHandleGatewayEvent] ) return { handleDesktopGatewayEvent, restartPreviewServer } diff --git a/apps/desktop/src/components/chat/preview-attachment.tsx b/apps/desktop/src/components/chat/preview-attachment.tsx index b85d1b8b057..9cc90dff53e 100644 --- a/apps/desktop/src/components/chat/preview-attachment.tsx +++ b/apps/desktop/src/components/chat/preview-attachment.tsx @@ -104,16 +104,15 @@ export function PreviewAttachment({ source = 'manual', target }: { source?: Prev } return ( -
- +
+ -
-
{name}
-
{target}
-
+ + {name} +