()
+ const readRelative = createReadHtmlRelative(readRelated, resourceAddress, signal, (address) => {
+ resources.add(address)
+ addResource(address)
+ })
let url: string | undefined
void (async () => {
try {
@@ -44,18 +48,20 @@ function HtmlFrame({ data, readRelative, t }: FrameInput & { t: HtmlBodyProps['t
controller.signal.throwIfAborted()
const html = createHtmlDocument(bundle)
url = URL.createObjectURL(new Blob([html], { type: 'text/html' }))
- setFrame({ data, readRelative, url })
+ setFrame({ data, readRelated, url })
} catch {
- if (!controller.signal.aborted) setFrame({ data, readRelative, url: undefined })
+ if (!controller.signal.aborted) setFrame({ data, readRelated, url: undefined })
+ } finally {
+ if (!controller.signal.aborted && !signal.aborted) setResources([...resources])
}
})()
return () => {
controller.abort()
if (url !== undefined) URL.revokeObjectURL(url)
}
- }, [data, readRelative])
+ }, [data, readRelated, resourceAddress, addResource, setResources, signal])
- if (frame?.data !== data || frame.readRelative !== readRelative) {
+ if (frame?.data !== data || frame.readRelated !== readRelated) {
return
}
if (frame.url === undefined) return {t('failed')}
@@ -67,16 +73,18 @@ function HtmlFrame({ data, readRelative, t }: FrameInput & { t: HtmlBodyProps['t
* @param props - document bytes, hooks, related-file reader and locale.
* @returns an isolated HTML document, or nothing for text delivery.
*/
-export function HtmlBody({ content, resourceAddress, readRelated, useTabInfo, useInteractivePreview, t }: HtmlBodyProps): ReactNode {
+export function HtmlBody({
+ content, resourceAddress, readRelated, useTabInfo, useInteractivePreview, addResource, setResources, t,
+}: HtmlBodyProps): ReactNode {
const interactivePreview = useInteractivePreview(value => value)
const { tab } = useTabInfo()
- const readRelative = useMemo(
- () => createReadHtmlRelative(readRelated, resourceAddress, tab.signal),
- [readRelated, resourceAddress, tab.signal],
- )
+ useEffect(() => {
+ if (!interactivePreview) setResources([])
+ }, [interactivePreview, setResources])
if (content.kind !== 'bytes') return null
if (!interactivePreview) return
- return
+ return
}
/** Static preview mounts a separate browsing context so a mode change retires running scripts. */
diff --git a/packages/client/ui-sidebar-documentpreview/src/client/html/read-relative.ts b/packages/client/ui-sidebar-documentpreview/src/client/html/read-relative.ts
index dfbdeb42c7..5bbf23c94e 100644
--- a/packages/client/ui-sidebar-documentpreview/src/client/html/read-relative.ts
+++ b/packages/client/ui-sidebar-documentpreview/src/client/html/read-relative.ts
@@ -3,6 +3,8 @@ import type { RemoteResult } from '@deepseek-ai/dsh-api-remotes/client'
import type { WorkspaceFileBytes } from '@deepseek-ai/dsh-api-workspace-files/types'
import { documentFileBytes } from '../rpc.ts'
import type { ReadHtmlRelative } from './pack.ts'
+import { sessionFileAddress } from '@deepseek-ai/dsh-util-workspace-path'
+import { hostFileOf } from '../rpc.ts'
/**
* Read one dependency relative to the addressed HTML file through the Host.
@@ -18,9 +20,15 @@ export type ReadHtmlRelated = (address: string, relativePath: string, signal: Ab
* @param readRelated - Remote reader using the Session in the root HTML address.
* @param address - root HTML file address.
* @param lifetime - tab lifetime.
+ * @param addResource - subscribes to the dependency path reported by the Host.
* @returns a reader that strips URL query/fragment, decodes one path, and preserves Host failures.
*/
-export function createReadHtmlRelative(readRelated: ReadHtmlRelated, address: string, lifetime: AbortSignal): ReadHtmlRelative {
+export function createReadHtmlRelative(
+ readRelated: ReadHtmlRelated,
+ address: string,
+ lifetime: AbortSignal,
+ addResource: (address: string) => void,
+): ReadHtmlRelative {
return async (reference, signal) => {
const suffix = reference.search(/[?#]/u)
const path = decodeURIComponent(suffix === -1 ? reference : reference.slice(0, suffix))
@@ -29,9 +37,16 @@ export function createReadHtmlRelative(readRelated: ReadHtmlRelated, address: st
}
const combined = AbortSignal.any([lifetime, signal])
combined.throwIfAborted()
+ const file = hostFileOf(address)
const result = await readRelated(address, path, combined)
combined.throwIfAborted()
- if (!result.ok) throw new Error(result.error.message)
+ if (!result.ok) {
+ if ('path' in result.error.details && typeof result.error.details.path === 'string') {
+ addResource(sessionFileAddress(file.sessionId, result.error.details.path))
+ }
+ throw new Error(result.error.message)
+ }
+ addResource(sessionFileAddress(file.sessionId, result.value.absolutePath))
return documentFileBytes(result.value)
}
}
diff --git a/packages/client/ui-sidebar-documentpreview/src/client/index.ts b/packages/client/ui-sidebar-documentpreview/src/client/index.ts
index f4129e1104..c5edbc05a5 100644
--- a/packages/client/ui-sidebar-documentpreview/src/client/index.ts
+++ b/packages/client/ui-sidebar-documentpreview/src/client/index.ts
@@ -78,7 +78,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
* Required browser services: the tab registry, the slot registry, copy, and the
* Remote carrier with its `workspaceFiles` namespace.
*/
-export const inject = ['slots', 'locale', 'sidebarRightTabs', 'remote', 'remote.workspaceFiles', 'settingsScope']
+export const inject = ['slots', 'locale', 'sidebarRightTabs', 'remote', 'remote.workspaceFiles', 'settingsScope', 'resources']
/**
* Client plugin body: register the type, its dictionaries, its body, and its chip title.
@@ -99,6 +99,7 @@ export function apply(ctx: ClientContext): void {
const result = await ctx.remote.workspaceFiles.readAll(file.sessionId, file.path, signal)
return result.ok ? { ok: true, value: documentFileBytes(result.value) } : result
},
+ ctx.resources,
)
const source = { getSnapshot: previews.getSnapshot, subscribe: previews.subscribe }
ctx.effect(() => ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register(
diff --git a/packages/client/ui-sidebar-documentpreview/src/client/locales.ts b/packages/client/ui-sidebar-documentpreview/src/client/locales.ts
index 6a8f512596..5322be9224 100644
--- a/packages/client/ui-sidebar-documentpreview/src/client/locales.ts
+++ b/packages/client/ui-sidebar-documentpreview/src/client/locales.ts
@@ -13,6 +13,9 @@ export const zh = {
changed: '文件已更新,当前显示为旧内容',
reloadNow: '重新载入',
reload: '重新读取文件',
+ autoRefresh: '自动刷新',
+ 'autoRefresh.enable': '开启自动刷新',
+ 'autoRefresh.disable': '关闭自动刷新',
'wrap.enable': '自动换行',
'wrap.disable': '取消换行',
'wrap.aria': '自动换行',
@@ -39,6 +42,9 @@ export const en = {
changed: 'The file has changed, showing the previous content.',
reloadNow: 'Reload',
reload: 'Read the file again',
+ autoRefresh: 'Auto refresh',
+ 'autoRefresh.enable': 'Enable auto refresh',
+ 'autoRefresh.disable': 'Disable auto refresh',
'wrap.enable': 'Turn on line wrap',
'wrap.disable': 'Turn off line wrap',
'wrap.aria': 'Line wrap',
diff --git a/packages/client/ui-sidebar-documentpreview/src/client/office/OfficeBody.tsx b/packages/client/ui-sidebar-documentpreview/src/client/office/OfficeBody.tsx
index 74069d9f9c..b921f9269f 100644
--- a/packages/client/ui-sidebar-documentpreview/src/client/office/OfficeBody.tsx
+++ b/packages/client/ui-sidebar-documentpreview/src/client/office/OfficeBody.tsx
@@ -71,7 +71,11 @@ export function OfficeBody(props: OfficeBodyProps): ReactNode {
return () => { controller.abort() }
}, [revision, resourceAddress, tab.id, tab.signal, read, actions, describeFailure, settled])
const file = view?.file
- useEffect(() => { if (file !== undefined) request?.loaded(file.version) }, [file, request?.loaded])
+ useEffect(() => {
+ if (request === undefined) return
+ if (file !== undefined) request.loaded(file.version)
+ else if (view?.failure !== undefined) request.failed()
+ }, [file, view?.failure, request])
if (request === undefined) return null
if (view?.failure !== undefined) {
const { name } = pathPartsOf(resourceAddress)
@@ -86,6 +90,7 @@ export function OfficeBody(props: OfficeBodyProps): ReactNode {
{props.renderSlot('sidebar.right.tab.document.office.pdf', {
resourceAddress, content: { kind: 'bytes', data: file.data }, wrap: props.wrap, scrollportRef: props.scrollportRef,
+ addResource: props.addResource, setResources: props.setResources,
}, { entryKey: '@deepseek-ai/dsh-client-ui-sidebar-documentpreview/office', hookContext: props.useTabInfo })}