Procházet zdrojové kódy

refactor(preview): align Office content discriminant

yudshj před 5 dny
rodič
revize
bbd5fa2cf4

+ 2 - 2
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

+ 1 - 1
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
 

+ 1 - 1
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)定义加载生命周期。
 
 ## 资源模型
 

+ 2 - 2
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

+ 1 - 1
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.
 
 <a id="addresses"></a>
 ## Addresses

+ 1 - 1
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。
 
 <a id="addresses"></a>
 ## 地址

+ 1 - 1
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') {

+ 3 - 3
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<ArrayBuffer> }
   | {
-    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

+ 2 - 2
packages/client/ui-sidebar-documentpreview/src/client/office/OfficeBody.tsx

@@ -43,13 +43,13 @@ export type OfficeBodyProps = DocumentPreviewProps & PropsStore<OfficeStore> & 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

+ 1 - 1
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'}
           </div>
         )
       },

+ 1 - 1
packages/client/ui-sidebar-documentpreview/tests/document-toolbar.client.spec.tsx

@@ -174,7 +174,7 @@ describe('document toolbar', () => {
     view.rerender(<TextPreview {...h.props()} useDocumentPreviews={selector => 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()

+ 2 - 2
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<DocumentContent, { kind: 'source' }> | undefined
+  let request: Extract<DocumentContent, { kind: 'office' }> | 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 <p>Raw bytes</p>
+    if (owner.content.kind !== 'office') return <p>Raw bytes</p>
     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,

+ 2 - 2
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<HTMLElement>\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<HTMLElement>\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<HTMLElement>\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<HTMLElement>\n}',
     ],
     ownerPropsReferences: [
       'DocumentContent',