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:
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user