From bbd5fa2cf48f4cfe0d2effa142b459c12f5406ef Mon Sep 17 00:00:00 2001 From: yudshj Date: Thu, 17 Sep 2026 13:12:02 +0800 Subject: [PATCH] refactor(preview): align Office content discriminant --- docs/subsystems/sidebar-right.i18n.yaml | 4 ++-- docs/subsystems/sidebar-right.md | 2 +- docs/subsystems/sidebar-right.zh.md | 2 +- packages/client/ui-sidebar-documentpreview/README.i18n.yaml | 4 ++-- packages/client/ui-sidebar-documentpreview/README.md | 2 +- packages/client/ui-sidebar-documentpreview/README.zh.md | 2 +- .../ui-sidebar-documentpreview/src/client/TextPreview.tsx | 2 +- .../src/client/document/contract.ts | 6 +++--- .../src/client/office/OfficeBody.tsx | 4 ++-- .../tests/document-seat.client.spec.tsx | 2 +- .../tests/document-toolbar.client.spec.tsx | 2 +- .../tests/renderer-loading.client.spec.tsx | 4 ++-- .../cordis-client-runner/src/client/slot-catalog.ts | 4 ++-- 13 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/subsystems/sidebar-right.i18n.yaml b/docs/subsystems/sidebar-right.i18n.yaml index bfc5b24b89..20929580f0 100644 --- a/docs/subsystems/sidebar-right.i18n.yaml +++ b/docs/subsystems/sidebar-right.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/sidebar-right.md -sidebar-right.md: e7c6b30cd798d00a9465241f5ddf407267d306c1 -sidebar-right.zh.md: 067f082d628c7c92978eb229d92e8734500c3641 +sidebar-right.md: c4e117653564d11b006e911bfb7f3900ff505c33 +sidebar-right.zh.md: f6d64964565197d8ae89e9367f1e9cb49b68efd2 diff --git a/docs/subsystems/sidebar-right.md b/docs/subsystems/sidebar-right.md index e7c6b30cd7..c4e1176535 100644 --- a/docs/subsystems/sidebar-right.md +++ b/docs/subsystems/sidebar-right.md @@ -114,7 +114,7 @@ The [registry](../../packages/client/ui-sidebar-documentpreview/src/client/docum Preview records its loaded version and the version observed when a read starts. Refresh rereads only that tab, without changing shared metadata or another tab's content. Reads are non-transactional; versions are opaque equality tokens, not ordered timestamps ([resource observation and Preview RPC](../../.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md)). -The Office body receives `{ kind: 'source', revision, loaded, reload }` instead of file bytes. The body loads through its own injected callbacks, cancels on revision changes and unmount, and reports its displayed source version through `loaded(version)`. The parent ignores stale reports and retains the shared reload and source-change controls. Office uses this mode to request [Host-rendered PDFs](office-to-pdf.md); its own store and bounded cache retain converted bytes, and its body owns font notices above a nested PDF view. The [package README](../../packages/client/ui-sidebar-documentpreview/README.md#what-it-registers) defines the loading lifecycle. +The Office body receives `{ kind: 'office', revision, loaded, reload }` instead of file bytes. The body loads through its own injected callbacks, cancels on revision changes and unmount, and reports its displayed source version through `loaded(version)`. The parent ignores stale reports and retains the shared reload and source-change controls. Office uses this mode to request [Host-rendered PDFs](office-to-pdf.md); its own store and bounded cache retain converted bytes, and its body owns font notices above a nested PDF view. The [package README](../../packages/client/ui-sidebar-documentpreview/README.md#what-it-registers) defines the loading lifecycle. ## Resource model diff --git a/docs/subsystems/sidebar-right.zh.md b/docs/subsystems/sidebar-right.zh.md index 067f082d62..f6d6496456 100644 --- a/docs/subsystems/sidebar-right.zh.md +++ b/docs/subsystems/sidebar-right.zh.md @@ -114,7 +114,7 @@ Sidebar 声明四个扩展 slot;其文档 tab 另行声明下表中的 keyed Preview 记录已载入版本和读取开始时的观察版本。刷新只重读当前 tab,不改变共享元数据或其他 tab 的内容。读取不具备事务性;版本是不透明的相等性令牌,不是可排序的时间戳([资源观察与 Preview RPC](../../.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.zh.md))。 -Office 正文接收 `{ kind: 'source', revision, loaded, reload }`,而不是文件字节。正文通过自己的注入回调加载,在 revision 变化和卸载时取消请求,并通过 `loaded(version)` 报告已展示的源版本。父组件忽略过期报告,保留共享的重新加载与源文件变更控件。Office 使用此模式请求 [Host 渲染的 PDF](office-to-pdf.zh.md);自己的 store 和有界缓存保留转换字节,正文在嵌套 PDF 视图上方管理字体提示。[包 README](../../packages/client/ui-sidebar-documentpreview/README.zh.md#what-it-registers)定义加载生命周期。 +Office 正文接收 `{ kind: 'office', revision, loaded, reload }`,而不是文件字节。正文通过自己的注入回调加载,在 revision 变化和卸载时取消请求,并通过 `loaded(version)` 报告已展示的源版本。父组件忽略过期报告,保留共享的重新加载与源文件变更控件。Office 使用此模式请求 [Host 渲染的 PDF](office-to-pdf.zh.md);自己的 store 和有界缓存保留转换字节,正文在嵌套 PDF 视图上方管理字体提示。[包 README](../../packages/client/ui-sidebar-documentpreview/README.zh.md#what-it-registers)定义加载生命周期。 ## 资源模型 diff --git a/packages/client/ui-sidebar-documentpreview/README.i18n.yaml b/packages/client/ui-sidebar-documentpreview/README.i18n.yaml index 99934b4e40..fc0aa8c693 100644 --- a/packages/client/ui-sidebar-documentpreview/README.i18n.yaml +++ b/packages/client/ui-sidebar-documentpreview/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-sidebar-documentpreview/README.md -README.md: 2ba539d02123d4bca500a548540a7c4edc98c951 -README.zh.md: 545ea0e0d3ef44c8d166b386dd12d0383dbedba5 +README.md: 90a39bf6a5825587912b9c2e31804357cbdfb39d +README.zh.md: 0a404ff5bf0fd4e1849da0562a6385f688f2ebb4 diff --git a/packages/client/ui-sidebar-documentpreview/README.md b/packages/client/ui-sidebar-documentpreview/README.md index 2ba539d021..90a39bf6a5 100644 --- a/packages/client/ui-sidebar-documentpreview/README.md +++ b/packages/client/ui-sidebar-documentpreview/README.md @@ -33,7 +33,7 @@ Preview readable files in the right Sidebar and choose among registered renderer Document implementations register metadata with `ctx.documentPreviews.register({ id, extensions, binaryExtensions?, priority, title, loading, wrap? })` and a body under the same `id` in the keyed, Session-scoped `sidebar.right.tab.document` child slot. `binaryExtensions` lists suffixes in `extensions` that cannot be read as text and omit the plain-text option. Own both registrations with effects and wait for the child slot through `ctx.slots.inject`. Bodies receive `resourceAddress`, `content`, `wrap`, `scrollportRef`, and the standard `useTabInfo`/`useResource` hooks. An inner scrolling element attaches `scrollportRef`; unmounting restores the shared body as scroll owner. The registry retains all matching alternatives: `extension` (the default) ranks above `builtin`, then longer suffixes rank first, then registration order. The dropdown preserves a selected implementation while it remains available. HTML, SVG, and unmatched extensions retain the plain-text fallback independently of loading mode. -`loading: 'text-pages'` and `'bytes-complete'` use the shared file reader. With `'office'`, the Office body mounts before any bytes are read and receives `content: { kind: 'source', revision, loaded, reload }`. Its injected callbacks own content loading, errors, and cancellation. `loaded(version)` reports the displayed source version for the shared change notice; reports from replaced revisions are ignored. `reload()` increments the revision, which the body observes to cancel and replace its request. The body also cancels on unmount and tab closure, retains settled content in its declared tab store, and releases that state when the tab ends. [Office previews](#office-preview) use this mode without putting converted bytes or font metadata in the shared file store. +`loading: 'text-pages'` and `'bytes-complete'` use the shared file reader. With `'office'`, the Office body mounts before any bytes are read and receives `content: { kind: 'office', revision, loaded, reload }`. Its injected callbacks own content loading, errors, and cancellation. `loaded(version)` reports the displayed source version for the shared change notice; reports from replaced revisions are ignored. `reload()` increments the revision, which the body observes to cancel and replace its request. The body also cancels on unmount and tab closure, retains settled content in its declared tab store, and releases that state when the tab ends. [Office previews](#office-preview) use this mode without putting converted bytes or font metadata in the shared file store. ## Addresses diff --git a/packages/client/ui-sidebar-documentpreview/README.zh.md b/packages/client/ui-sidebar-documentpreview/README.zh.md index 545ea0e0d3..0a404ff5bf 100644 --- a/packages/client/ui-sidebar-documentpreview/README.zh.md +++ b/packages/client/ui-sidebar-documentpreview/README.zh.md @@ -33,7 +33,7 @@ kind: "package-reference" 文档实现在 `ctx.documentPreviews.register({ id, extensions, binaryExtensions?, priority, title, loading, wrap? })` 注册元数据,并以相同 `id` 向 keyed、Session 作用域的子 slot `sidebar.right.tab.document` 注册正文。`binaryExtensions` 列出 `extensions` 中不可按文本阅读的后缀,这些后缀不提供纯文本选项。两处注册都由 effect 持有,通过 `ctx.slots.inject` 等待子 slot。正文接收 `resourceAddress`、`content`、`wrap`、`scrollportRef` 和标准 `useTabInfo`/`useResource` 钩子。内部滚动元素挂载 `scrollportRef`;卸载时恢复共享正文的滚动职责。注册表保留所有匹配备选:`extension`(默认)优先于 `builtin`,随后按更长的后缀、再按注册顺序排列。所选实现仍可用时,下拉选择保持不变。HTML、SVG 和未匹配的扩展名保留纯文本回退,与加载方式无关。 -`loading: 'text-pages'` 和 `'bytes-complete'` 使用共享文件读取器。选择 `'office'` 时,Office 正文在读取任何字节前挂载,并接收 `content: { kind: 'source', revision, loaded, reload }`。其注入回调负责内容加载、错误和取消。`loaded(version)` 为共享变更提示报告已展示的源版本;已被替换的 revision 所发出的报告会被忽略。`reload()` 增加 revision,正文据此取消并替换当前请求。正文也在卸载和 tab 关闭时取消请求,将已完成内容保留在自己声明的 tab store 中,并在 tab 结束时释放。[Office 预览](#office-preview) 使用此模式,转换后的字节和字体元数据不会进入共享文件 store。 +`loading: 'text-pages'` 和 `'bytes-complete'` 使用共享文件读取器。选择 `'office'` 时,Office 正文在读取任何字节前挂载,并接收 `content: { kind: 'office', revision, loaded, reload }`。其注入回调负责内容加载、错误和取消。`loaded(version)` 为共享变更提示报告已展示的源版本;已被替换的 revision 所发出的报告会被忽略。`reload()` 增加 revision,正文据此取消并替换当前请求。正文也在卸载和 tab 关闭时取消请求,将已完成内容保留在自己声明的 tab store 中,并在 tab 结束时释放。[Office 预览](#office-preview) 使用此模式,转换后的字节和字体元数据不会进入共享文件 store。 ## 地址 diff --git a/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx b/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx index a239048cbb..e7c743c89c 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx +++ b/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx @@ -195,7 +195,7 @@ export function TextPreview({ if (mode === 'office') { if (current === undefined) return undefined const revision = current.loadRevision - return { kind: 'source', revision, reload: officeReload, + return { kind: 'office', revision, reload: officeReload, loaded: (version) => { actions.rendered(tab.id, revision, version) } } } if (mode === 'bytes-complete') { diff --git a/packages/client/ui-sidebar-documentpreview/src/client/document/contract.ts b/packages/client/ui-sidebar-documentpreview/src/client/document/contract.ts index 1114ca0053..02f3275d53 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/document/contract.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/document/contract.ts @@ -11,14 +11,14 @@ export interface DocumentTextPage { } /** - * Ordinary file contents, or a request for a renderer to load its own source. + * Ordinary file contents, or a request for the Office body to load its document. * Byte arrays are transient UI input, never persisted layout or Session data. */ export type DocumentContent = | { readonly kind: 'text'; readonly text: string; readonly pages: readonly DocumentTextPage[]; readonly eof: boolean } | { readonly kind: 'bytes'; readonly data: Uint8Array } | { - readonly kind: 'source' + readonly kind: 'office' /** Changes on reload or implementation replacement; retained contents belong to one revision. */ readonly revision: number /** Report the displayed source version; stale revisions cannot update the owner. @param version - loaded source version. */ @@ -31,7 +31,7 @@ export type DocumentContent = export interface DocumentBodyOwner { /** Original file address, also readable through the standard useResource hook. */ readonly resourceAddress: string - /** Ordinary file content or a renderer-owned source request; text accumulates until eof. */ + /** Ordinary file content or an Office loading request; text accumulates until eof. */ readonly content: DocumentContent /** The document toolbar's current wrapping preference. */ readonly wrap: boolean 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 6ade8c53b3..e36c815c9f 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/office/OfficeBody.tsx +++ b/packages/client/ui-sidebar-documentpreview/src/client/office/OfficeBody.tsx @@ -43,13 +43,13 @@ export type OfficeBodyProps = DocumentPreviewProps & PropsStore & O /** * Load one Office revision and preserve its result while its tab remains open. - * @param props - source request, tab state, conversion callbacks, and PDF slot. + * @param props - Office loading request, tab state, conversion callbacks, and PDF slot. * @returns conversion status or the font notice and PDF scrollport. */ export function OfficeBody(props: OfficeBodyProps): ReactNode { const { tab } = props.useTabInfo() const { actions, read, retainTab, describeFailure, resourceAddress, t } = props - const request = props.content.kind === 'source' ? props.content : undefined + const request = props.content.kind === 'office' ? props.content : undefined const revision = request?.revision const held = props.useStore(state => state.byTab[tab.id]) const view = held?.revision === revision ? held : undefined diff --git a/packages/client/ui-sidebar-documentpreview/tests/document-seat.client.spec.tsx b/packages/client/ui-sidebar-documentpreview/tests/document-seat.client.spec.tsx index 6b9f89d3ea..9c18921644 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/document-seat.client.spec.tsx +++ b/packages/client/ui-sidebar-documentpreview/tests/document-seat.client.spec.tsx @@ -83,7 +83,7 @@ async function boot() { data-renderer={id} data-renderer-tab={tab.id} data-renderer-path={resource.value?.absolutePath} data-renderer-version={resource.value?.version} > - {props.content.kind === 'text' ? props.content.text : props.content.kind === 'bytes' ? new TextDecoder().decode(props.content.data) : 'source'} + {props.content.kind === 'text' ? props.content.text : props.content.kind === 'bytes' ? new TextDecoder().decode(props.content.data) : 'office'} ) }, diff --git a/packages/client/ui-sidebar-documentpreview/tests/document-toolbar.client.spec.tsx b/packages/client/ui-sidebar-documentpreview/tests/document-toolbar.client.spec.tsx index 80b34c325a..ce6a116c5b 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/document-toolbar.client.spec.tsx +++ b/packages/client/ui-sidebar-documentpreview/tests/document-toolbar.client.spec.tsx @@ -174,7 +174,7 @@ describe('document toolbar', () => { view.rerender( selector([{ ...binary, loading: 'office' }])} renderSlot={renderSlot} />) expect(view.container.textContent).not.toContain('previous reader content') expect(renderSlot).toHaveBeenCalledWith('sidebar.right.tab.document', expect.objectContaining({ - content: expect.objectContaining({ kind: 'source' }) as unknown, + content: expect.objectContaining({ kind: 'office' }) as unknown, }), expect.any(Object)) expect(h.instance.getSnapshot().byTab[TAB_ID]?.complete).toBeUndefined() expect(h.bytes).not.toHaveBeenCalled() diff --git a/packages/client/ui-sidebar-documentpreview/tests/renderer-loading.client.spec.tsx b/packages/client/ui-sidebar-documentpreview/tests/renderer-loading.client.spec.tsx index 8558b30335..51610e6fc7 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/renderer-loading.client.spec.tsx +++ b/packages/client/ui-sidebar-documentpreview/tests/renderer-loading.client.spec.tsx @@ -50,10 +50,10 @@ function setup() { return selector(useSyncExternalStore(subscribe, snapshot)) } const describeFailure: OfficeBodyProps['describeFailure'] = error => error.message - let request: Extract | undefined + let request: Extract | undefined const slots: TextPreviewProps['renderSlot'] = (_key, input, options) => { const owner = input as unknown as OwnerOf<'sidebar.right.tab.document'> - if (owner.content.kind !== 'source') return

Raw bytes

+ if (owner.content.kind !== 'office') return

Raw bytes

request = owner.content // The component fixture supplies the standard seats used by Office; the real slot binding is exercised by the browser scenario. const props = { ...h.props(), ...owner, useTabInfo: options.hookContext, useStore: useOffice, diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts index 4077c43192..31001d7f96 100644 --- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts @@ -2557,7 +2557,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ }, ], ownerProps: [ - '/** Content and viewing inputs shared by document bodies and nested PDF presentation. */\nexport interface DocumentBodyOwner {\n /** Original file address, also readable through the standard useResource hook. */\n readonly resourceAddress: string\n /** Ordinary file content or a renderer-owned source request; text accumulates until eof. */\n readonly content: DocumentContent\n /** The document toolbar\'s current wrapping preference. */\n readonly wrap: boolean\n /** Report a renderer-owned scrollport; passing `null` restores the shared body as the owner. */\n readonly scrollportRef: RefCallback\n}', + '/** Content and viewing inputs shared by document bodies and nested PDF presentation. */\nexport interface DocumentBodyOwner {\n /** Original file address, also readable through the standard useResource hook. */\n readonly resourceAddress: string\n /** Ordinary file content or an Office loading request; text accumulates until eof. */\n readonly content: DocumentContent\n /** The document toolbar\'s current wrapping preference. */\n readonly wrap: boolean\n /** Report a renderer-owned scrollport; passing `null` restores the shared body as the owner. */\n readonly scrollportRef: RefCallback\n}', ], ownerPropsReferences: [ 'DocumentContent', @@ -2611,7 +2611,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ }, ], ownerProps: [ - '/** Content and viewing inputs shared by document bodies and nested PDF presentation. */\nexport interface DocumentBodyOwner {\n /** Original file address, also readable through the standard useResource hook. */\n readonly resourceAddress: string\n /** Ordinary file content or a renderer-owned source request; text accumulates until eof. */\n readonly content: DocumentContent\n /** The document toolbar\'s current wrapping preference. */\n readonly wrap: boolean\n /** Report a renderer-owned scrollport; passing `null` restores the shared body as the owner. */\n readonly scrollportRef: RefCallback\n}', + '/** Content and viewing inputs shared by document bodies and nested PDF presentation. */\nexport interface DocumentBodyOwner {\n /** Original file address, also readable through the standard useResource hook. */\n readonly resourceAddress: string\n /** Ordinary file content or an Office loading request; text accumulates until eof. */\n readonly content: DocumentContent\n /** The document toolbar\'s current wrapping preference. */\n readonly wrap: boolean\n /** Report a renderer-owned scrollport; passing `null` restores the shared body as the owner. */\n readonly scrollportRef: RefCallback\n}', ], ownerPropsReferences: [ 'DocumentContent',