fix(desktop): agent hand-overs of HTML open rendered; pane reads the tab's mode directly

Review follow-ups. An agent preview of an HTML file whose tab the user had switched to Source stayed in Source, so the agent could believe the page was on screen; the preview-tool route and the status-stack row now pass an explicit rendered mode (renderedHtmlTarget), while Files-pane re-opens still keep the user's pick. The pane read the store tab a second time and kept a test-only local mode; it now reads target.renderMode and offers the toggle only on a tab-backed pane. PreviewRenderMode names the mode once; canRenderHtmlFile is the reduced form; a stale load error or annotate draft no longer flashes for a frame on the switch to Source.
This commit is contained in:
yoniebans
2026-09-23 12:25:55 +02:00
committed by jonny
parent 1adad98d10
commit 2e1afdf0ec
5 changed files with 48 additions and 21 deletions

View File

@@ -9,7 +9,7 @@ import { isDesktopFsRemoteMode } from '@/lib/desktop-fs'
import { normalizeOrLocalPreviewTarget, openPreviewTargetInBrowser } from '@/lib/local-preview'
import { cn } from '@/lib/utils'
import { notifyError } from '@/store/notifications'
import { $previewTabSources, closePreviewForSource, openPreview } from '@/store/preview'
import { $previewTabSources, closePreviewForSource, openPreview, renderedHtmlTarget } from '@/store/preview'
import { type PreviewArtifact } from '@/store/preview-status'
interface PreviewStatusRowProps {
@@ -49,7 +49,7 @@ export const PreviewStatusRow = memo(function PreviewStatusRow({ item, onDismiss
setOpening(true)
try {
openPreview(await resolveTarget())
openPreview(renderedHtmlTarget(await resolveTarget()))
} catch (error) {
notifyError(error, t.preview.unavailable)
} finally {

View File

@@ -34,11 +34,11 @@ import { notify, notifyError } from '@/store/notifications'
import {
$browserPages,
$previewServerRestart,
$previewTabs,
commitBrowserTabLocation,
failPreviewServerRestart,
noteBrowserPage,
popOutBrowserTab,
type PreviewRenderMode,
type PreviewTarget,
setPreviewRenderMode
} from '@/store/preview'
@@ -251,7 +251,6 @@ export function PreviewPane({ embedded = false, onRestartServer, reloadRequest =
const previewContentRef = useRef<HTMLDivElement | null>(null)
const webviewRef = useRef<PreviewWebview | null>(null)
const previewServerRestart = useStore($previewServerRestart)
const previewTabs = useStore($previewTabs)
const consoleHeight = useStore(consoleState.$height)
const consoleOpen = useStore(consoleState.$open)
const selectedStoredSessionId = useStore($selectedStoredSessionId)
@@ -263,7 +262,6 @@ export function PreviewPane({ embedded = false, onRestartServer, reloadRequest =
const [loading, setLoading] = useState(true)
const [loadError, setLoadError] = useState<PreviewLoadErrorState | null>(null)
const [localReloadKey, setLocalReloadKey] = useState(0)
const [localRenderMode, setLocalRenderMode] = useState<'preview' | 'source' | undefined>()
const [annotate, setAnnotate] = useState(emptyAnnotateSession)
const [draftNote, setDraftNote] = useState('')
const annotateRef = useRef(annotate)
@@ -271,9 +269,7 @@ export function PreviewPane({ embedded = false, onRestartServer, reloadRequest =
const annotateConversationRef = useRef(selectedStoredSessionId)
annotateRef.current = annotate
const liveTarget = (tabId && previewTabs.find(tab => tab.id === tabId)?.target) || target
const renderMode = tabId ? liveTarget.renderMode : (localRenderMode ?? target.renderMode)
const isHtmlFileTarget = target.kind === 'file' && target.previewKind === 'html'
const renderMode = target.renderMode
// Artifacts have no URL to load — they render from the registry, never in a
// webview.
@@ -283,15 +279,18 @@ export function PreviewPane({ embedded = false, onRestartServer, reloadRequest =
const isRemoteHtmlTarget =
target.kind === 'file' && target.previewKind === 'html' && Boolean(target.dataUrl || target.transient)
// A remote HTML file whose data URL failed validation arrives as a
// source-only transient target; it has no rendered path to offer.
const canRenderHtmlFile = isHtmlFileTarget && (!isRemoteHtmlTarget || Boolean(target.dataUrl))
// The mode lives on the store tab, so only a tab-backed pane can flip it. A
// remote HTML file whose data URL failed validation arrives as a source-only
// transient target; it has no rendered path to offer.
const canRenderHtmlFile =
Boolean(tabId) &&
target.kind === 'file' &&
target.previewKind === 'html' &&
(!target.transient || Boolean(target.dataUrl))
const selectRenderMode = (next: 'preview' | 'source') => {
const selectRenderMode = (next: PreviewRenderMode) => {
if (tabId) {
setPreviewRenderMode(tabId, next)
} else {
setLocalRenderMode(next)
}
}
@@ -1405,7 +1404,7 @@ export function PreviewPane({ embedded = false, onRestartServer, reloadRequest =
<PanelEmpty description={copy.blankPageBody} icon="globe" />
</div>
)}
{loadError && (
{isWebPreview && loadError && (
<PreviewLoadError
consoleHeight={consoleOpen ? consoleHeight : 0}
error={loadError}
@@ -1415,7 +1414,7 @@ export function PreviewPane({ embedded = false, onRestartServer, reloadRequest =
/>
)}
{annotate.draft ? (
{isWebPreview && annotate.draft ? (
<PreviewAnnotateCard
{...placeAnnotateCard({
paneHeight: previewContentRef.current?.clientHeight || 360,

View File

@@ -12,6 +12,7 @@ import {
completePreviewServerRestart,
openPreview,
progressPreviewServerRestart,
renderedHtmlTarget,
requestPreviewReload
} from '@/store/preview'
import { $activeSessionId, $currentCwd } from '@/store/session'
@@ -100,7 +101,7 @@ export function usePreviewRouting({ baseHandleGatewayEvent, currentCwd, requestG
const url = resolved.kind === 'url' ? await reachablePreviewUrl(resolved.url) : resolved.url
const reached = url === resolved.url ? resolved : { ...resolved, label: resolved.label || target, url }
openPreview(trimmedLabel ? { ...reached, label: trimmedLabel } : reached)
openPreview(renderedHtmlTarget(trimmedLabel ? { ...reached, label: trimmedLabel } : reached))
}
)
}

View File

@@ -17,6 +17,7 @@ import {
previewTabId,
type PreviewTarget,
progressPreviewServerRestart,
renderedHtmlTarget,
setPreviewRenderMode
} from './preview'
@@ -203,6 +204,21 @@ describe('preview store', () => {
expect($previewTarget.get()?.renderMode).toBe('preview')
})
it('renders an agent hand-over of an HTML file even when its tab sits in Source', () => {
const target = fileTarget('/work/handed.html')
openPreview(target)
setPreviewRenderMode(previewTabId(target), 'source')
openPreview(renderedHtmlTarget(target))
expect($previewTabs.get()).toHaveLength(1)
expect($previewTarget.get()?.renderMode).toBe('preview')
// An explicit mode and non-HTML targets pass through untouched.
expect(renderedHtmlTarget({ ...target, renderMode: 'source' }).renderMode).toBe('source')
expect(renderedHtmlTarget({ ...fileTarget('/work/notes.md'), previewKind: 'text' }).renderMode).toBeUndefined()
})
it('falls back to a neighbouring tab when the active one closes, and clears the selection on the last', () => {
openPreview(fileTarget('/work/one.html'))
openPreview(fileTarget('/work/two.html'))

View File

@@ -20,6 +20,9 @@ import { canOpenBrowserWindow, openBrowserInNewWindow } from './windows'
* anywhere else — they close when you close them.
*/
/** How an HTML file target shows: the live page, or its source. */
export type PreviewRenderMode = 'preview' | 'source'
export interface PreviewTarget {
binary?: boolean
byteSize?: number
@@ -38,7 +41,7 @@ export interface PreviewTarget {
mimeType?: string
path?: string
previewKind?: 'binary' | 'html' | 'image' | 'pdf' | 'text'
renderMode?: 'preview' | 'source'
renderMode?: PreviewRenderMode
source: string
/** Runtime-only target that cannot be restored from persisted state. */
transient?: boolean
@@ -503,16 +506,24 @@ function browserTabId(tabs: PreviewTab[]): RightRailTabId {
/** HTML files open rendered unless the caller asks for a mode. A re-open keeps
* the mode the tab is already in, so refreshing the target never undoes a
* user's Source pick. */
function withRenderMode(target: PreviewTarget, open?: PreviewTarget): PreviewTarget {
function withRenderMode(target: PreviewTarget, existing?: PreviewTarget): PreviewTarget {
if (target.kind !== 'file' || target.previewKind !== 'html' || target.renderMode) {
return target
}
return { ...target, renderMode: open?.renderMode ?? 'preview' }
return { ...target, renderMode: existing?.renderMode ?? 'preview' }
}
/** An agent hand-over means "show the page": an HTML file opens rendered even
* when its tab is sitting in Source, unlike a re-open from the Files pane. */
export function renderedHtmlTarget(target: PreviewTarget): PreviewTarget {
return target.kind === 'file' && target.previewKind === 'html' && !target.renderMode
? { ...target, renderMode: 'preview' }
: target
}
/** Flip a tab between live Render and Source in place. Same tab id. */
export function setPreviewRenderMode(tabId: string, renderMode: 'preview' | 'source') {
export function setPreviewRenderMode(tabId: string, renderMode: PreviewRenderMode) {
const current = $previewTabs.get()
const index = current.findIndex(tab => tab.id === tabId)