Ver Fonte

fix(ci): refresh document preview metadata

imccyu há 2 semanas atrás
pai
commit
c78fdb583a
28 ficheiros alterados com 102 adições e 106 exclusões
  1. 2 2
      .agents/notes/implemented/architecture/2026-09-08-document-preview-operations.i18n.yaml
  2. 3 3
      .agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md
  3. 3 3
      .agents/notes/implemented/architecture/2026-09-08-document-preview-operations.zh.md
  4. 1 1
      apps/web/tests/document-preview.e2e.ts
  5. 2 2
      docs/config-catalog.i18n.yaml
  6. 2 2
      docs/config-catalog.md
  7. 2 2
      docs/config-catalog.zh.md
  8. 2 2
      docs/module-graph.i18n.yaml
  9. 2 2
      docs/module-graph.md
  10. 2 2
      docs/module-graph.zh.md
  11. 2 2
      docs/subsystems/sidebar-right.i18n.yaml
  12. 1 1
      docs/subsystems/sidebar-right.md
  13. 1 1
      docs/subsystems/sidebar-right.zh.md
  14. 2 2
      docs/subsystems/workspace.i18n.yaml
  15. 9 9
      docs/subsystems/workspace.md
  16. 9 9
      docs/subsystems/workspace.zh.md
  17. 2 2
      packages/api/workspace-files/README.i18n.yaml
  18. 8 8
      packages/api/workspace-files/README.md
  19. 8 8
      packages/api/workspace-files/README.zh.md
  20. 2 2
      packages/client/ui-sidebar-documentpreview/README.i18n.yaml
  21. 3 3
      packages/client/ui-sidebar-documentpreview/README.md
  22. 3 3
      packages/client/ui-sidebar-documentpreview/README.zh.md
  23. 2 8
      packages/client/ui-sidebar-documentpreview/package.json
  24. 2 1
      packages/extensions/cordis-client-runner/src/client/slot-catalog.ts
  25. 10 10
      packages/extensions/tool-cordis/src/api-catalog.ts
  26. 12 13
      pnpm-lock.yaml
  27. 4 2
      snapshots/web/document-preview/document.expected.md
  28. 1 1
      snapshots/web/document-preview/snapshot.yml

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.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 .agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md
-2026-09-08-document-preview-operations.md: 213534e36d1080463086cf0c78c03c12b30e3613
-2026-09-08-document-preview-operations.zh.md: ff4e8efcfb90395b87fe925eafe7b0331097ecef
+2026-09-08-document-preview-operations.md: 316f33371d393846cce5b598ee23289cecd6c178
+2026-09-08-document-preview-operations.zh.md: 97759db32d139a44fe33fd2c5e2eb7ec0c8960fd

+ 3 - 3
.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.md

@@ -12,11 +12,11 @@ File viewers need different loading policies and may offer several implementatio
 
 Document Preview separates resource observation from content reads. The [resource model](2026-09-05-client-resource-model.md) shares observations by address alone: `source(address)`, `pin(address, signal)`, and provider `open(address, { signal })` carry no consuming Session. Providers return `AsyncIterable<RemoteResult<ResourceProtocolMap[P]>>`; `useResource` exposes only `{ status, value, failure }`. Holds start and stop observation, not the underlying file or Session. Content reads use ordinary injected Preview callbacks.
 
-[Workspace Files](../../../../packages/api/workspace-files/README.md) retains Host line reads, byte windows, bounded complete reads, and bounded reads relative to another file's directory. Its Client `file` provider observes only `stat` and `changes`, with `ResourceProtocolMap.file` directly naming `WorkspaceFileStat`. The Host continues to resolve and confine every path under the [file-service decision](2026-09-05-workspace-files-service.md).
+[Workspace Files](../../../../packages/api/workspace-files/README.md) retains Host line reads, byte windows, bounded complete reads, and bounded reads relative to another file's directory. Its Client `file` provider observes only `stat` and `changes`, with `ResourceProtocolMap.file` directly naming `WorkspaceFileStat`. The Host resolves every path through the Session filesystem; file reads inherit that backend's read authority, while directory listing and change observation stay workspace-scoped.
 
 Readable files use `dsh-resource://file/session/<sessionId>/<path>`. The path may be workspace-relative or absolute; an encoded absolute path retains its leading slash. `fileAddressFor` always emits this Session-address form. The provider and Preview RPC take the Session only from that address, never from the current selection, first holder, or owning tab. A Session-less `absolute` URI cannot be read; the provider reports `workspace-file/unknown-workspace`. Session authorization is a file-protocol rule, not an additional Resource identity.
 
-[Document Preview](../../../../packages/client/ui-sidebar-textpreview/README.md) owns format selection and loading policy. Metadata registers with `ctx.documentPreviews`; components register separately into the keyed `sidebar.right.tab.document` Slot. Extension registrations precede builtins, then longer suffixes and registration order decide. The toolbar lists matching alternatives and remembers a manual choice per tab; plain text is the fallback. The child receives accumulated text or complete native bytes, the original resource address, and the standard `useResource` and `useTabInfo` hooks. Preview calls existing `read`, `readAll`, and `readRelated` through ordinary injection and decodes bytes in its own `rpc.ts`. Refresh remains per tab, with no resource reload, shared `changed` acknowledgement, extra resource wrapper, or content Session.
+[Document Preview](../../../../packages/client/ui-sidebar-documentpreview/README.md) owns format selection and loading policy. Metadata registers with `ctx.documentPreviews`; components register separately into the keyed `sidebar.right.tab.document` Slot. Extension registrations precede builtins, then longer suffixes and registration order decide. The toolbar lists matching alternatives and remembers a manual choice per tab; plain text is the fallback. The child receives accumulated text or complete native bytes, the original resource address, and the standard `useResource` and `useTabInfo` hooks. Preview calls existing `read`, `readAll`, and `readRelated` through ordinary injection and decodes bytes in its own `rpc.ts`. Refresh remains per tab, with no resource reload, shared `changed` acknowledgement, extra resource wrapper, or content Session.
 
 Markdown and code reuse the incremental primitives with cumulative paged text. HTML and PDF read complete `Uint8Array<ArrayBuffer>` data; Host transport remains base64. Published buffers are borrowed read-only and never persist into layout or Session JSON. PDF.js runs in an owned Worker with version-matched bundled font and decoder data, and copies input before transfer to preserve Preview's retained buffer. HTML runs in a Blob iframe with `sandbox="allow-scripts"`, without same-origin, popup, form, download, or top-navigation privileges. The browser retains its normal external-network rules. Bounded static local JS/CSS reads stay in the parent; the opaque frame creates its own asset Blobs, because it cannot load parent-origin Blobs. Replacing the document replaces the browsing context and revokes its root Blob.
 
@@ -36,4 +36,4 @@ Markdown and code reuse the incremental primitives with cumulative paged text. H
 
 ## Consequences
 
-Renderers can be replaced without changing the tab or file protocol. Full-file formats pay bounded whole-file memory and PDF adds bundled Worker/font/decoder bytes. Format selection and view state are page-local, not durable Session data. Preview owns RPC cancellation and native buffers independently of metadata observation. A tab retains its read version and the observation version captured at read start; refreshing it neither discards another tab's content nor clears its change notice. File reads remain non-transactional, and opaque versions are compared for equality, not ordering. The [recorded browser scenario](../../../../apps/web/tests/document-preview.e2e.ts) exercises the shared toolbar, incremental text, isolated HTML dependencies, and PDF Worker rendering with retained page state.
+Renderers can be replaced without changing the tab or file protocol. Full-file formats pay bounded whole-file memory and PDF adds bundled Worker/font/decoder bytes. Format selection and view state are page-local, not durable Session data. Preview owns RPC cancellation and native buffers independently of metadata observation. A tab retains its read version and the observation version captured at read start; refreshing it neither discards another tab's content nor clears its change notice. File reads remain non-transactional, and opaque versions are compared for equality, not ordering. The [recorded browser scenario](../../../../apps/web/tests/document-preview.e2e.ts) exercises the shared toolbar, incremental text, isolated HTML dependencies, and lazy continuous PDF Worker rendering.

+ 3 - 3
.agents/notes/implemented/architecture/2026-09-08-document-preview-operations.zh.md

@@ -12,11 +12,11 @@ Status: implemented
 
 Document Preview 将资源观察与内容读取分开。[资源模型](2026-09-05-client-resource-model.zh.md)只按地址共享观察:`source(address)`、`pin(address, signal)` 和提供方的 `open(address, { signal })` 均不携带消费 Session。提供方返回 `AsyncIterable<RemoteResult<ResourceProtocolMap[P]>>`;`useResource` 只暴露 `{ status, value, failure }`。持有只控制观察的启停,不控制底层文件或 Session 的生灭。内容通过普通注入的 Preview 回调读取。
 
-[Workspace Files](../../../../packages/api/workspace-files/README.zh.md) 保留 Host 的行读取、字节窗口、有上限的全文读取和相对另一文件目录的有界读取。Client `file` 提供方只观察 `stat` 与 `changes`,`ResourceProtocolMap.file` 直接为 `WorkspaceFileStat`。Host 继续按[文件服务决议](2026-09-05-workspace-files-service.zh.md)解析并限制每条路径
+[Workspace Files](../../../../packages/api/workspace-files/README.zh.md) 保留 Host 的行读取、字节窗口、有上限的全文读取和相对另一文件目录的有界读取。Client `file` 提供方只观察 `stat` 与 `changes`,`ResourceProtocolMap.file` 直接为 `WorkspaceFileStat`。Host 通过 Session 文件系统解析每条路径;文件读取继承该后端的读取权限,目录列举与变更观察仍限定于工作区
 
 可读取的文件使用 `dsh-resource://file/session/<sessionId>/<path>`。路径可以相对工作区,也可以是绝对路径;编码后的绝对路径保留前导斜杠。`fileAddressFor` 始终生成这种 Session 地址。提供方与 Preview RPC 只从该地址取 Session,不取当前选择、首个持有者或 tab 所属 Session。不带 Session 的 `absolute` URI 无法读取;提供方报告 `workspace-file/unknown-workspace`。Session 授权是文件协议规则,不是额外的 Resource 身份。
 
-[Document Preview](../../../../packages/client/ui-sidebar-textpreview/README.zh.md) 负责格式选择和加载策略。元数据通过 `ctx.documentPreviews` 注册;组件单独注册到 keyed `sidebar.right.tab.document` Slot。扩展注册优先于内置注册,其次比较后缀长度和注册顺序。工具栏列出匹配候选,按 tab 记住手动选择;纯文本是兜底。子组件收到累积文本或完整原生字节、原始资源地址,以及标准 `useResource` 和 `useTabInfo` 钩子。Preview 经普通注入调用既有 `read`、`readAll` 与 `readRelated`,在自己的 `rpc.ts` 解码字节。刷新仍按 tab 独立进行,不引入资源 reload、共享 `changed` 确认、额外资源包装层或内容 Session。
+[Document Preview](../../../../packages/client/ui-sidebar-documentpreview/README.zh.md) 负责格式选择和加载策略。元数据通过 `ctx.documentPreviews` 注册;组件单独注册到 keyed `sidebar.right.tab.document` Slot。扩展注册优先于内置注册,其次比较后缀长度和注册顺序。工具栏列出匹配候选,按 tab 记住手动选择;纯文本是兜底。子组件收到累积文本或完整原生字节、原始资源地址,以及标准 `useResource` 和 `useTabInfo` 钩子。Preview 经普通注入调用既有 `read`、`readAll` 与 `readRelated`,在自己的 `rpc.ts` 解码字节。刷新仍按 tab 独立进行,不引入资源 reload、共享 `changed` 确认、额外资源包装层或内容 Session。
 
 Markdown 和代码通过累积的分页文本复用增量渲染原语。HTML 和 PDF 读取完整 `Uint8Array<ArrayBuffer>` 数据;Host 传输保持 base64。发布后的缓冲区只读借用,绝不持久化进布局或 Session JSON。PDF.js 在自有 Worker 中运行,字体和解码数据以相同版本随包发布,转移输入前先复制,以保留 Preview 的缓冲区。HTML 在 Blob iframe 中运行,设置 `sandbox="allow-scripts"`,不授予同源、弹窗、表单、下载或顶层导航权限。浏览器保持正常的外部网络规则。有上限的静态本地 JS/CSS 读取由父页面负责;不透明源 iframe 创建自己的资源 Blob,因为它不能加载父源创建的 Blob。替换文档会替换浏览上下文,并撤销其根 Blob。
 
@@ -36,4 +36,4 @@ Markdown 和代码通过累积的分页文本复用增量渲染原语。HTML 和
 
 ## 影响
 
-替换渲染器不需要改变 Tab 或文件协议。全文格式承担有上限的整文件内存成本,PDF 增加随包发布的 Worker、字体和解码器字节。格式选择和查看状态仅属于当前页面,不是持久 Session 数据。Preview 独立于元数据观察,拥有 RPC 取消和原生缓冲区。tab 保留读取版本及读取开始时捕获的观察版本;刷新它既不丢弃其他 tab 的内容,也不清除其变更提示。文件读取仍非事务,不透明版本只比较相等性、不排序。[录制的浏览器场景](../../../../apps/web/tests/document-preview.e2e.ts) 覆盖共用工具栏、增量文本、隔离的 HTML 依赖,以及保留页码状态的 PDF Worker 渲染。
+替换渲染器不需要改变 Tab 或文件协议。全文格式承担有上限的整文件内存成本,PDF 增加随包发布的 Worker、字体和解码器字节。格式选择和查看状态仅属于当前页面,不是持久 Session 数据。Preview 独立于元数据观察,拥有 RPC 取消和原生缓冲区。tab 保留读取版本及读取开始时捕获的观察版本;刷新它既不丢弃其他 tab 的内容,也不清除其变更提示。文件读取仍非事务,不透明版本只比较相等性、不排序。[录制的浏览器场景](../../../../apps/web/tests/document-preview.e2e.ts) 覆盖共用工具栏、增量文本、隔离的 HTML 依赖,以及惰性连续 PDF Worker 渲染。

+ 1 - 1
apps/web/tests/document-preview.e2e.ts

@@ -10,7 +10,7 @@ import { pdfFixture } from '../../../packages/client/ui-sidebar-documentpreview/
 import { assertFixtureInventory, compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold } from './scaffold.ts'
 import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
 
-const FIXTURE = fileURLToPath(new URL('../../../snapshots/web/lifecycle-chrome/session.v2.jsonl', import.meta.url))
+const FIXTURE = fileURLToPath(new URL('../../../snapshots/web/lifecycle-chrome/session.v3.jsonl', import.meta.url))
 const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/document-preview', import.meta.url))
 const EXPECTED = join(SNAPSHOT_DIR, 'document.expected.md')
 const PAGING_PATCH = join(SNAPSHOT_DIR, 'paging.patch.yml')

+ 2 - 2
docs/config-catalog.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/config-catalog.md
-config-catalog.md: 01aecfe6037ae7125771046329cf758473245f13
-config-catalog.zh.md: 277f66830f94e5d12e1f532755956442ebd69c07
+config-catalog.md: 50626a7d85aa54f6d2e3cdb6140b3af8081d1461
+config-catalog.zh.md: d2ae35b91a4a83da882e8ec5af82a505aa5d5bd1

+ 2 - 2
docs/config-catalog.md

@@ -255,7 +255,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/api/workspace-files/src/index.ts:57`](../packages/api/workspace-files/src/index.ts)
+Source: [`packages/api/workspace-files/src/index.ts:50`](../packages/api/workspace-files/src/index.ts)
 
 <a id="deepseek-aidsh-attachment-local"></a>
 
@@ -3481,9 +3481,9 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
 - `@deepseek-ai/dsh-client-ui-settings-plugin-inventory` ([`packages/client/ui-settings-plugin-inventory/src/index.ts`](../packages/client/ui-settings-plugin-inventory/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-settings-plugins` ([`packages/client/ui-settings-plugins/src/index.ts`](../packages/client/ui-settings-plugins/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-sidebar` ([`packages/client/ui-sidebar/src/index.ts`](../packages/client/ui-sidebar/src/index.ts))
+- `@deepseek-ai/dsh-client-ui-sidebar-documentpreview` ([`packages/client/ui-sidebar-documentpreview/src/index.ts`](../packages/client/ui-sidebar-documentpreview/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-sidebar-files` ([`packages/client/ui-sidebar-files/src/index.ts`](../packages/client/ui-sidebar-files/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-sidebar-right` ([`packages/client/ui-sidebar-right/src/index.ts`](../packages/client/ui-sidebar-right/src/index.ts))
-- `@deepseek-ai/dsh-client-ui-sidebar-documentpreview` ([`packages/client/ui-sidebar-documentpreview/src/index.ts`](../packages/client/ui-sidebar-documentpreview/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-skill` ([`packages/client/ui-skill/src/index.ts`](../packages/client/ui-skill/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-subagent` ([`packages/client/ui-subagent/src/index.ts`](../packages/client/ui-subagent/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-theme` ([`packages/client/ui-theme/src/index.ts`](../packages/client/ui-theme/src/index.ts))

+ 2 - 2
docs/config-catalog.zh.md

@@ -257,7 +257,7 @@ export interface Config {
 }
 ```
 
-来源:[`packages/api/workspace-files/src/index.ts:57`](../packages/api/workspace-files/src/index.ts)
+来源:[`packages/api/workspace-files/src/index.ts:50`](../packages/api/workspace-files/src/index.ts)
 
 <a id="deepseek-aidsh-attachment-local"></a>
 
@@ -3483,9 +3483,9 @@ export interface Config {
 - `@deepseek-ai/dsh-client-ui-settings-plugin-inventory`([`packages/client/ui-settings-plugin-inventory/src/index.ts`](../packages/client/ui-settings-plugin-inventory/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-settings-plugins`([`packages/client/ui-settings-plugins/src/index.ts`](../packages/client/ui-settings-plugins/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-sidebar`([`packages/client/ui-sidebar/src/index.ts`](../packages/client/ui-sidebar/src/index.ts))
+- `@deepseek-ai/dsh-client-ui-sidebar-documentpreview`([`packages/client/ui-sidebar-documentpreview/src/index.ts`](../packages/client/ui-sidebar-documentpreview/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-sidebar-files`([`packages/client/ui-sidebar-files/src/index.ts`](../packages/client/ui-sidebar-files/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-sidebar-right`([`packages/client/ui-sidebar-right/src/index.ts`](../packages/client/ui-sidebar-right/src/index.ts))
-- `@deepseek-ai/dsh-client-ui-sidebar-documentpreview`([`packages/client/ui-sidebar-documentpreview/src/index.ts`](../packages/client/ui-sidebar-documentpreview/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-skill`([`packages/client/ui-skill/src/index.ts`](../packages/client/ui-skill/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-subagent`([`packages/client/ui-subagent/src/index.ts`](../packages/client/ui-subagent/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-theme`([`packages/client/ui-theme/src/index.ts`](../packages/client/ui-theme/src/index.ts))

+ 2 - 2
docs/module-graph.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/module-graph.md
-module-graph.md: 45e3490e95c86b025fde3f89ef3243b98625d012
-module-graph.zh.md: aacce9fb513e243a1ec27955d34aa82a2a6749be
+module-graph.md: e498227db7cc482beb2dd69aab147baec8eead22
+module-graph.zh.md: c3956f8b466f791b2b90aee12bedba7f95ff7cb9

+ 2 - 2
docs/module-graph.md

@@ -173,9 +173,9 @@ flowchart TD
     pkg_client_ui_settings_plugin_inventory["client-ui-settings-plugin-inventory"]
     pkg_client_ui_settings_plugins["client-ui-settings-plugins"]
     pkg_client_ui_sidebar["client-ui-sidebar"]
+    pkg_client_ui_sidebar_documentpreview["client-ui-sidebar-documentpreview"]
     pkg_client_ui_sidebar_files["client-ui-sidebar-files"]
     pkg_client_ui_sidebar_right["client-ui-sidebar-right"]
-    pkg_client_ui_sidebar_textpreview["client-ui-sidebar-documentpreview"]
     pkg_client_ui_skill["client-ui-skill"]
     pkg_client_ui_slots["client-ui-slots"]
     pkg_client_ui_subagent["client-ui-subagent"]
@@ -1239,9 +1239,9 @@ flowchart TD
 | [`client-ui-settings-plugin-inventory`](../packages/client/ui-settings-plugin-inventory) | `client` | — |
 | [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | — |
 | [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | — |
+| [`client-ui-sidebar-documentpreview`](../packages/client/ui-sidebar-documentpreview) | `client` | — |
 | [`client-ui-sidebar-files`](../packages/client/ui-sidebar-files) | `client` | — |
 | [`client-ui-sidebar-right`](../packages/client/ui-sidebar-right) | `client` | — |
-| [`client-ui-sidebar-documentpreview`](../packages/client/ui-sidebar-documentpreview) | `client` | — |
 | [`client-ui-skill`](../packages/client/ui-skill) | `client` | — |
 | [`client-ui-slots`](../packages/client/ui-slots) | `client` | — |
 | [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | — |

+ 2 - 2
docs/module-graph.zh.md

@@ -175,9 +175,9 @@ flowchart TD
     pkg_client_ui_settings_plugin_inventory["client-ui-settings-plugin-inventory"]
     pkg_client_ui_settings_plugins["client-ui-settings-plugins"]
     pkg_client_ui_sidebar["client-ui-sidebar"]
+    pkg_client_ui_sidebar_documentpreview["client-ui-sidebar-documentpreview"]
     pkg_client_ui_sidebar_files["client-ui-sidebar-files"]
     pkg_client_ui_sidebar_right["client-ui-sidebar-right"]
-    pkg_client_ui_sidebar_textpreview["client-ui-sidebar-documentpreview"]
     pkg_client_ui_skill["client-ui-skill"]
     pkg_client_ui_slots["client-ui-slots"]
     pkg_client_ui_subagent["client-ui-subagent"]
@@ -1241,9 +1241,9 @@ flowchart TD
 | [`client-ui-settings-plugin-inventory`](../packages/client/ui-settings-plugin-inventory) | `client` | — |
 | [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | — |
 | [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | — |
+| [`client-ui-sidebar-documentpreview`](../packages/client/ui-sidebar-documentpreview) | `client` | — |
 | [`client-ui-sidebar-files`](../packages/client/ui-sidebar-files) | `client` | — |
 | [`client-ui-sidebar-right`](../packages/client/ui-sidebar-right) | `client` | — |
-| [`client-ui-sidebar-documentpreview`](../packages/client/ui-sidebar-documentpreview) | `client` | — |
 | [`client-ui-skill`](../packages/client/ui-skill) | `client` | — |
 | [`client-ui-slots`](../packages/client/ui-slots) | `client` | — |
 | [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | — |

+ 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: 59eda91862282d240d49764d53c17f531ff4bf21
-sidebar-right.zh.md: 3ecb5e9ef496471316414b6aeb0c683a78188025
+sidebar-right.md: 60802fd868bb693f16a9ae6e5547a91737fd79e0
+sidebar-right.zh.md: 839012a96b3a4ec4ed85d9daafb1f618501573b0

+ 1 - 1
docs/subsystems/sidebar-right.md

@@ -124,7 +124,7 @@ A resource stays open while it has a holder — a subscribed `useResource` or a
 
 ## Workspace Files
 
-The Host `ctx.workspaceFiles` service and the generated `workspaceFiles` Remote namespace answer for files inside the addressed Session's workspace root: `stat(path)` returns `{ absolutePath, version, bytes? }`; `read(path, { offset?, limit? })` returns one page of lines (`offset` 1-based, `limit` capped by the configured page size) as `{ …stat, offset, text, eof }`; `readBytes(path, { offset?, length? })` returns one raw byte window (`offset` 0-based, `length` capped by the configured byte limit) as base64 `{ …stat, offset, data, eof }` with no text decoding; `list(path)` returns a directory's direct children (`name`, `type: 'file' | 'directory' | 'other'`, `size?`) cut to the configured cap with `truncated` set; `changes()` yields `{ kind: 'ready' }` once subscribed, then `{ kind: 'change', change }` frames whose payload is `{ absolutePath, version }` or `{ absolutePath, absent: true }` ([README](../../packages/api/workspace-files/README.md#use-this-package)). Every call passes the same four gates — the path is inside the workspace root, symlinks are refused, page, window, and entry caps hold, `read`'s text is UTF-8 — and fails with a `workspace-file/*` error code otherwise ([failures](../../packages/api/workspace-files/README.md)).
+The Host `ctx.workspaceFiles` service and generated `workspaceFiles` Remote namespace read files allowed by the Session filesystem backend: `stat(path)` returns `{ absolutePath, version, bytes? }`; `read(path, { offset?, limit? })` returns one page of lines (`offset` 1-based, `limit` capped by the configured page size) as `{ …stat, offset, text, eof }`; `readBytes(path, { offset?, length? })` returns one raw byte window (`offset` 0-based, `length` capped by the configured byte limit) as base64 `{ …stat, offset, data, eof }` with no text decoding. `list(path)` remains inside the workspace root and returns a directory's direct children (`name`, `type: 'file' | 'directory' | 'other'`, `size?`) cut to the configured cap with `truncated` set. `changes()` likewise remains workspace-scoped and yields `{ kind: 'ready' }` once subscribed, then `{ kind: 'change', change }` frames whose payload is `{ absolutePath, version }` or `{ absolutePath, absent: true }` ([README](../../packages/api/workspace-files/README.md#use-this-package)). File operations reject final symlinks and enforce transfer caps; `read` additionally requires UTF-8 text. Failures use `workspace-file/*` codes ([failures](../../packages/api/workspace-files/README.md)).
 
 [`dsh-api-workspace-files`](../../packages/api/workspace-files/README.md) registers the `file` provider, with `ResourceProtocolMap.file` directly naming `WorkspaceFileStat`. A Session address carries the authorizing Session and a relative or absolute path, passed unchanged to the Host for resolution. The provider waits for Host `ready` before stat and filters changes by `stat.absolutePath`. Bare `absolute` addresses have no authorizing Session and fail with `workspace-file/unknown-workspace`, without borrowing current or Tab Session. Any UI, including Global components, shares the observation for the same complete address. Preview's ordinary Remote callbacks use the Session in that address; Host `readAll` and `readRelated` remain, and Preview's `rpc.ts` decodes byte results.
 

+ 1 - 1
docs/subsystems/sidebar-right.zh.md

@@ -124,7 +124,7 @@ Preview 记录已载入版本和读取开始时的观察版本。刷新只重读
 
 ## Workspace Files
 
-Host 的 `ctx.workspaceFiles` 服务与生成的 `workspaceFiles` Remote 命名空间负责所寻址会话工作区根之内的文件:`stat(path)` 返回 `{ absolutePath, version, bytes? }`;`read(path, { offset?, limit? })` 返回一页行(`offset` 1 起,`limit` 受配置页长限制),形如 `{ …stat, offset, text, eof }`;`readBytes(path, { offset?, length? })` 返回一个原始字节窗口(`offset` 0 起,`length` 受配置字节上限限制),形如 base64 的 `{ …stat, offset, data, eof }`、不做文本解码;`list(path)` 返回目录的直接子项(`name`、`type: 'file' | 'directory' | 'other'`、`size?`),按配置上限截断并置 `truncated`;`changes()` 在订阅就绪后产出 `{ kind: 'ready' }`,随后产出 `{ kind: 'change', change }` 帧,其载荷为 `{ absolutePath, version }` 或 `{ absolutePath, absent: true }`([README](../../packages/api/workspace-files/README.zh.md#use-this-package))。每次调用都过同样四关——路径在工作区根内、拒绝符号链接、页、窗口与条目上限、`read` 的 UTF-8 文本——否则以 `workspace-file/*` 错误码失败([失败](../../packages/api/workspace-files/README.zh.md))。
+Host 的 `ctx.workspaceFiles` 服务与生成的 `workspaceFiles` Remote 命名空间读取 Session 文件系统后端允许的文件:`stat(path)` 返回 `{ absolutePath, version, bytes? }`;`read(path, { offset?, limit? })` 返回一页行(`offset` 1 起,`limit` 受配置页长限制),形如 `{ …stat, offset, text, eof }`;`readBytes(path, { offset?, length? })` 返回一个原始字节窗口(`offset` 0 起,`length` 受配置字节上限限制),形如 base64 的 `{ …stat, offset, data, eof }`、不做文本解码。`list(path)` 仍限定在工作区根内,返回目录的直接子项(`name`、`type: 'file' | 'directory' | 'other'`、`size?`),按配置上限截断并置 `truncated`。`changes()` 同样限定于工作区,订阅就绪后产出 `{ kind: 'ready' }`,随后产出 `{ kind: 'change', change }` 帧,其载荷为 `{ absolutePath, version }` 或 `{ absolutePath, absent: true }`([README](../../packages/api/workspace-files/README.zh.md#use-this-package))。文件操作拒绝末端符号链接并执行传输上限;`read` 还要求 UTF-8 文本。失败使用 `workspace-file/*` 错误码([失败](../../packages/api/workspace-files/README.zh.md))。
 
 [`dsh-api-workspace-files`](../../packages/api/workspace-files/README.zh.md) 注册 `file` 提供方,`ResourceProtocolMap.file` 直接是 `WorkspaceFileStat`。Session 地址携带授权 Session 与相对或绝对路径,Host 原样接收并解析。提供方在 stat 前等待 Host 的 `ready` 帧,并按 `stat.absolutePath` 过滤变更。裸 `absolute` 地址没有授权 Session,以 `workspace-file/unknown-workspace` 失败,不借用当前或 Tab Session。任何 UI(包括 Global)访问同一完整地址都共享观察。Preview 的普通 Remote 回调使用地址中的 Session;Host `readAll` 和 `readRelated` 保留,字节结果由 Preview 的 `rpc.ts` 解码。
 

+ 2 - 2
docs/subsystems/workspace.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/workspace.md
-workspace.md: d2327ce5616cc2aa4845ac2965cebea8c70424f3
-workspace.zh.md: 159f6cf0df4684d5080ee0db7d0197b6ec96e887
+workspace.md: a7c4b8843f0a0b2d2fb251157f7928a40df1e405
+workspace.zh.md: 3474cbf147848b04a4ec51147983a343bcc1e91d

+ 9 - 9
docs/subsystems/workspace.md

@@ -246,13 +246,13 @@ Source: [`packages/api/workspace-controller/src/index.ts`](../../packages/api/wo
 
 ### `ctx.workspaceFiles` — `WorkspaceFiles`
 
-Host Remote service over the composed filesystem, confined to one workspace.
+Host Remote file reads and workspace directory observations over the composed filesystem.
 
 ```ts cordis-catalog
 /**
- * Read one page of lines from a UTF-8 text file inside the Agent's workspace.
+ * Read one page of lines from a UTF-8 file readable by the filesystem backend.
  * @param agent - target Agent resolved from the Session identity on the wire.
- * @param path - workspace path, absolute or relative to the workspace root.
+ * @param path - absolute path or path relative to the workspace root; files outside it are allowed.
  * @param range - the line window; omitted fields take the page defaults.
  * @param signal - caller cancellation.
  * @returns the page, the file's version at the stat before it, and whether it reaches the last line.
@@ -260,10 +260,10 @@ Host Remote service over the composed filesystem, confined to one workspace.
 @Remote async read(agent: Agent, path: string, range: WorkspaceFileRange, signal: AbortSignal): Promise<WorkspaceFileText>
 
 /**
- * Read one byte window of a regular file inside the Agent's workspace: raw
+ * Read one byte window of a regular file readable by the filesystem backend: raw
  * bytes, no text decoding and no binary rejection.
  * @param agent - target Agent resolved from the Session identity on the wire.
- * @param path - workspace path, absolute or relative to the workspace root.
+ * @param path - absolute path or path relative to the workspace root; files outside it are allowed.
  * @param range - the byte window; omitted fields take the window defaults.
  * @param signal - caller cancellation.
  * @returns the window in base64, the file's version and size at the stat before it, and whether it reaches the last byte.
@@ -272,7 +272,7 @@ Host Remote service over the composed filesystem, confined to one workspace.
 
 /**
  * Read a complete regular file as bytes, subject to the configured full-file cap.
- * @param agent - target Agent whose workspace confines the read.
+ * @param agent - target Agent whose workspace resolves relative paths.
  * @param path - absolute or workspace-relative file path.
  * @param signal - caller cancellation.
  * @returns one complete base64 window with offset zero and eof true; oversized files fail with too-large.
@@ -280,8 +280,8 @@ Host Remote service over the composed filesystem, confined to one workspace.
 @Remote async readAll(agent: Agent, path: string, signal: AbortSignal): Promise<WorkspaceFileBytes>
 
 /**
- * Read a complete file relative to another file's directory within the same workspace.
- * @param agent - Agent whose workspace confines both files.
+ * Read a complete file relative to another file's directory, including outside the workspace.
+ * @param agent - Agent whose workspace resolves the base file's relative path.
  * @param path - base file, absolute or workspace-relative.
  * @param relativePath - relative filesystem path, not a URL or absolute path.
  * @param signal - caller cancellation.
@@ -292,7 +292,7 @@ Host Remote service over the composed filesystem, confined to one workspace.
 /**
  * Report one regular file's identity, version, and size without its content.
  * @param agent - target Agent resolved from the Session identity on the wire.
- * @param path - workspace path, absolute or relative to the workspace root.
+ * @param path - absolute path or path relative to the workspace root; files outside it are allowed.
  * @param signal - caller cancellation.
  * @returns the file's absolute path, current version, and byte size.
  */

+ 9 - 9
docs/subsystems/workspace.zh.md

@@ -246,13 +246,13 @@ Source: [`packages/api/workspace-controller/src/index.ts`](../../packages/api/wo
 
 ### `ctx.workspaceFiles` — `WorkspaceFiles`
 
-Host Remote service over the composed filesystem, confined to one workspace.
+Host Remote file reads and workspace directory observations over the composed filesystem.
 
 ```ts cordis-catalog
 /**
- * Read one page of lines from a UTF-8 text file inside the Agent's workspace.
+ * Read one page of lines from a UTF-8 file readable by the filesystem backend.
  * @param agent - target Agent resolved from the Session identity on the wire.
- * @param path - workspace path, absolute or relative to the workspace root.
+ * @param path - absolute path or path relative to the workspace root; files outside it are allowed.
  * @param range - the line window; omitted fields take the page defaults.
  * @param signal - caller cancellation.
  * @returns the page, the file's version at the stat before it, and whether it reaches the last line.
@@ -260,10 +260,10 @@ Host Remote service over the composed filesystem, confined to one workspace.
 @Remote async read(agent: Agent, path: string, range: WorkspaceFileRange, signal: AbortSignal): Promise<WorkspaceFileText>
 
 /**
- * Read one byte window of a regular file inside the Agent's workspace: raw
+ * Read one byte window of a regular file readable by the filesystem backend: raw
  * bytes, no text decoding and no binary rejection.
  * @param agent - target Agent resolved from the Session identity on the wire.
- * @param path - workspace path, absolute or relative to the workspace root.
+ * @param path - absolute path or path relative to the workspace root; files outside it are allowed.
  * @param range - the byte window; omitted fields take the window defaults.
  * @param signal - caller cancellation.
  * @returns the window in base64, the file's version and size at the stat before it, and whether it reaches the last byte.
@@ -272,7 +272,7 @@ Host Remote service over the composed filesystem, confined to one workspace.
 
 /**
  * Read a complete regular file as bytes, subject to the configured full-file cap.
- * @param agent - target Agent whose workspace confines the read.
+ * @param agent - target Agent whose workspace resolves relative paths.
  * @param path - absolute or workspace-relative file path.
  * @param signal - caller cancellation.
  * @returns one complete base64 window with offset zero and eof true; oversized files fail with too-large.
@@ -280,8 +280,8 @@ Host Remote service over the composed filesystem, confined to one workspace.
 @Remote async readAll(agent: Agent, path: string, signal: AbortSignal): Promise<WorkspaceFileBytes>
 
 /**
- * Read a complete file relative to another file's directory within the same workspace.
- * @param agent - Agent whose workspace confines both files.
+ * Read a complete file relative to another file's directory, including outside the workspace.
+ * @param agent - Agent whose workspace resolves the base file's relative path.
  * @param path - base file, absolute or workspace-relative.
  * @param relativePath - relative filesystem path, not a URL or absolute path.
  * @param signal - caller cancellation.
@@ -292,7 +292,7 @@ Host Remote service over the composed filesystem, confined to one workspace.
 /**
  * Report one regular file's identity, version, and size without its content.
  * @param agent - target Agent resolved from the Session identity on the wire.
- * @param path - workspace path, absolute or relative to the workspace root.
+ * @param path - absolute path or path relative to the workspace root; files outside it are allowed.
  * @param signal - caller cancellation.
  * @returns the file's absolute path, current version, and byte size.
  */

+ 2 - 2
packages/api/workspace-files/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/api/workspace-files/README.md
-README.md: 117926ab8c78cdfff79010d8e5d6db39e2c93b8d
-README.zh.md: 51c5fcd7fc3e53f54a56ae378b4f73109657317c
+README.md: e49f5bef7291233eb688bc9aeab44e56efb3d7fa
+README.zh.md: 1d3808969ac377408801a7516978bbebd6860b62

+ 8 - 8
packages/api/workspace-files/README.md

@@ -1,5 +1,5 @@
 ---
-description: "Workspace file service for the web GUI: paged read, byte windows, complete and related-file reads, stat, directory listing, and the Agent-write change feed inside the Session workspace root, exposed as the workspaceFiles Remote namespace."
+description: "Workspace file service for the web GUI: bounded file reads through the composed filesystem, plus directory listing and Agent-write observation inside the Session workspace root."
 kind: "package-reference"
 ---
 
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-Use this package to browse and inspect files within a Session's workspace from the web client. It reads UTF-8 text one page of lines at a time, reads raw bytes in bounded windows or complete files, resolves related files from a base file's directory, reports file versions and sizes, lists direct directory children, and streams changes caused by Agent file operations. Every operation stays within the workspace root selected for the addressed Session, independent of the filesystem backend's working directory. Client components can also follow live file metadata and build the Sidebar file tree through the shared Remote API.
+Use this package to preview files readable through a Session's filesystem from the web client. It reads UTF-8 text by page, reads bounded byte windows or complete files, resolves related files from a base file's directory, and reports file metadata. File reads may target paths outside the workspace; directory listing and Agent-write change observation remain workspace-scoped. The service exposes no mutation operation.
 
 ## Table of Contents
 
@@ -39,7 +39,7 @@ Mount the package beside `dsh-fs`, `dsh-sandbox-policy`, and the Typert Gateway;
 
 ### Addressing and paths
 
-`read`, `stat`, and `list` accept a workspace path that is absolute or relative to the Session's workspace root. Two path vocabularies leave the service, and each method uses exactly one: `read`, `stat`, and `changes` report a file as its absolute path in the filesystem's execution world, symlinks resolved (`WorkspaceFileStat.absolutePath`, `WorkspaceFileChange.absolutePath`), because their consumer is the Client resource system, which follows changes by that path; `list` reports the listed directory as a workspace path relative to the root — empty for the root itself — because its consumer is a tree rooted there, and a child's path is that value joined with the entry name by `/`. `readRelated` resolves a relative filesystem path from the base file's directory, not a URL or absolute path; both files pass Host access checks.
+`read`, `readBytes`, `readAll`, `readRelated`, and `stat` accept an absolute path or one relative to the Session's workspace root. The composed filesystem decides whether the path is readable; the service does not impose workspace containment on file reads. `readRelated` resolves a relative filesystem path from the base file's directory, including when either file is outside the workspace. These methods report the file's absolute path in the filesystem's execution world. `list` remains workspace-scoped and reports the listed directory relative to that root. `changes` likewise reports only Agent observations inside the workspace root.
 
 ### Pages
 
@@ -49,9 +49,9 @@ Mount the package beside `dsh-fs`, `dsh-sandbox-policy`, and the Typert Gateway;
 
 `read` pages by lines and never by bytes; a byte window is `readBytes`. `range.offset` is the 0-based first byte and defaults to 0; `range.length` is the largest number of bytes in the window and defaults to `maxBytes`, which it may not exceed — a longer window fails with `too-large` instead of arriving shortened, and an offset or length that is not an integer in range is a `gateway/bad-request`. The window comes back as base64 `data`, shorter than `length` at the end of the file and empty at or past it; `eof` is true when the window includes the file's last byte. Nothing is decoded and nothing is refused as binary, so an image or a NUL-laden file reads where `read` fails with `not-text`. The same `version` and `bytes` ride along as on a page.
 
-### The four gates
+### File-read and directory checks
 
-Every read, stat, and listing passes four gates in this order. First, `lstat` inspects the path itself before anything follows it: a symlink, wherever it points, fails `read` and `stat` with `not-regular-file` and `list` with `not-directory`, each carrying the entry's `kind`. Second, containment: the path resolves to a target and `ctx.fs.contains(root, target)` decides, so a `..` traversal or an absolute path outside the root fails with `outside-workspace` — never a string-prefix comparison, which cannot see a realpath that leaves the root. Third, the caps: a page whose text exceeds `maxBytes` fails with `too-large` instead of arriving shortened — paged reads have no total file cap, while complete reads use `maxFileBytes` — while `maxEntries` cuts a listing and sets `truncated`. Fourth, text: content that is not UTF-8 up to the end of the page, or a page that carries a NUL byte, fails with `not-text`; bytes past the page are not inspected. A missing path fails with `not-found`; an empty path is a `gateway/bad-request`.
+Every operation first uses `lstat` to reject a missing path, a final symlink, or the wrong file kind. File operations then resolve and read through the composed filesystem without an additional workspace-containment check. `list` alone requires the resolved directory to remain inside the workspace root. The configured page, window, complete-file, and listing caps still apply. Text pages additionally reject invalid UTF-8 and NUL bytes; byte reads do not decode content. An empty path is a `gateway/bad-request`.
 
 ### The change feed
 
@@ -70,7 +70,7 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
 
 ### Failures
 
-Each failure is one `RemoteError` code with typed details, declared in [`src/types.ts`](src/types.ts): `workspace-file/not-found`, `workspace-file/outside-workspace`, `workspace-file/too-large` (with `limit`, the applicable page, window, or complete-file cap), `workspace-file/not-text`, `workspace-file/not-regular-file` (`kind`: `directory`, `symlink`, or `other`), and `workspace-file/not-directory` (`kind`: `file`, `symlink`, or `other`). Callers branch on the code, never on message text.
+Each failure is one `RemoteError` code with typed details, declared in [`src/types.ts`](src/types.ts): `workspace-file/not-found`, `workspace-file/outside-workspace` (directory listing only), `workspace-file/too-large` (with `limit`, the applicable page, window, or complete-file cap), `workspace-file/not-text`, `workspace-file/not-regular-file` (`kind`: `directory`, `symlink`, or `other`), and `workspace-file/not-directory` (`kind`: `file`, `symlink`, or `other`). Callers branch on the code, never on message text.
 
 ### Client file resources
 
@@ -92,7 +92,7 @@ One supervised `changes` stream serves every followed file in a Session. Followe
 
 ### Design concept
 
-Reads through `ctx.fs` are deliberately unconfined — the sandboxing backend fences writes and edits only — so every constraint here is the service's own. A page is cut from `streamText`, which decodes and rejects non-UTF-8 chunk by chunk: the cutter counts lines before the window without keeping them, admits each in-window segment against the byte cap before buffering it, and returns at the first character past the window, so neither a huge file nor one giant line can hold more than a page in memory; the NUL scan then runs on the page. One `stat` before the stream names the version and size the page reports. The path gate runs before containment on purpose: `lstat` is path-shaped and sees the link, while `resolve` follows it; the price is that an entry outside the root reports its own kind before its position.
+Reads through `ctx.fs` use the backend's read authority; the sandboxing backend fences writes and edits, not reads. The service adds regular-file checks and bounded transfer, while workspace containment belongs only to directory listing and change observation. A page is cut from `streamText`, which decodes and rejects non-UTF-8 chunk by chunk: the cutter counts lines before the window without keeping them, admits each in-window segment against the byte cap before buffering it, and returns at the first character past the window. One `stat` before the stream names the version and size the page reports.
 
 ### Source map
 
@@ -137,7 +137,7 @@ None; this package neither assembles nor sends a provider request.
 <a id="known-limitations-and-deferred-work"></a>
 
 - **Agent writes only** — `changes` relays `fs/observed` emissions; a file changed by a subprocess, a shell command, or the user's editor produces no frame.
-- **Kind before position** — an entry outside the workspace whose type already disqualifies it reports `not-regular-file` or `not-directory`, not `outside-workspace`, because the path gate precedes containment.
+- **Directory scope only** — `list` and `changes` stay inside the Session workspace even though file preview reads may use any path readable by the filesystem backend.
 - **No total line count** — a page reports `eof`, not how many lines follow; a consumer that needs the total pages to the end or estimates from `bytes`.
 - **One giant line has no page** — a single line above `maxBytes` fails `too-large` at every window that includes it, because pages are cut by lines, not bytes.
 - **Reads are not transactional** — result metadata comes from stat before content is read; a concurrent write can make the reported version and returned contents differ.

+ 8 - 8
packages/api/workspace-files/README.zh.md

@@ -1,5 +1,5 @@
 ---
-description: "面向 Web GUI 的工作区文件服务:在 Session 工作区根内做分页读取、字节窗口、完整读取、关联文件读取、stat、目录列举与 Agent 写入变更流,以 workspaceFiles Remote 命名空间暴露。"
+description: "面向 Web GUI 的工作区文件服务:通过组合文件系统进行有界文件读取,并在 Session 工作区根内列举目录和观察 Agent 写入。"
 kind: "package-reference"
 ---
 
@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-使用本包可从 Web Client 浏览和检查 Session 工作区内的文件。它按行分页读取 UTF-8 文本、按有界窗口或完整文件读取原始字节、从基文件目录解析关联文件、报告文件版本与大小、列举目录的直接子项,并流式推送 Agent 文件操作造成的变更。每项操作都限定在为被寻址 Session 选择的工作区根内,不受文件系统后端工作目录影响。Client 组件还可经共享 Remote API 跟随实时文件元数据并构建 Sidebar 文件树
+使用本包可从 Web Client 预览 Session 文件系统允许读取的文件。它按页读取 UTF-8 文本、按有界窗口或完整文件读取原始字节、从基文件目录解析关联文件,并报告文件元数据。文件读取可以指向工作区外路径;目录列举与 Agent 写入变更观察仍限定于工作区。本服务不提供修改操作
 
 ## 目录
 
@@ -39,7 +39,7 @@ kind: "package-reference"
 
 ### 寻址与路径
 
-`read`、`stat` 与 `list` 接受工作区路径,可以是绝对路径,也可以是相对于 Session 工作区根的路径。离开服务的路径词汇有两套,每个方法只用其中一套:`read`、`stat` 与 `changes` 以文件系统执行环境中的绝对路径报告文件,符号链接已解析(`WorkspaceFileStat.absolutePath`、`WorkspaceFileChange.absolutePath`),因为其消费方是 Client 资源系统,它按这条路径跟随变更;`list` 以相对于根的工作区路径报告被列举目录——根自身为空串——因为其消费方是一棵以根为起点的树,子项路径就是该值与条目名以 `/` 连接。 `readRelated` 从基文件所在目录解析相对文件系统路径,不接受 URL 或绝对路径;基文件与目标均经过 Host 访问检查
+`read`、`readBytes`、`readAll`、`readRelated` 与 `stat` 接受绝对路径或相对于 Session 工作区根的路径。组合文件系统决定路径是否可读;本服务不额外要求文件读取限定于工作区。`readRelated` 从基文件所在目录解析相对文件系统路径,基文件或目标文件位于工作区外时同样适用。这些方法以文件系统执行环境中的绝对路径报告文件。`list` 仍限定于工作区,并以相对于该根的路径报告被列举目录。`changes` 同样只报告工作区根内的 Agent 观察
 
 ### 分页
 
@@ -49,9 +49,9 @@ kind: "package-reference"
 
 `read` 按行分页,绝不按字节;字节窗口走 `readBytes`。`range.offset` 是 0 起算的首字节,缺省为 0;`range.length` 是窗口最多的字节数,缺省为 `maxBytes` 且不得超过它——更长的窗口以 `too-large` 失败而不是被截短,不是整数或越界的 offset / length 则是 `gateway/bad-request`。窗口以 base64 的 `data` 返回,到文件末尾时短于 `length`,位于或越过末尾时为空;窗口含文件最后一个字节时 `eof` 为 true。不做任何解码,也不按二进制拒绝,因此图片或含 NUL 的文件在 `read` 以 `not-text` 失败之处仍可读出。与页一样附带同一 `version` 与 `bytes`。
 
-### 四道关
+### 文件读取与目录检查
 
-每次读取、stat 与列举依次过四道关。第一,`lstat` 在跟随任何东西之前检查路径本身:符号链接不论指向哪里,`read` 与 `stat` 都以 `not-regular-file`、`list` 都以 `not-directory` 拒绝,并带上条目的 `kind`。第二,包含判定:路径解析为目标后由 `ctx.fs.contains(root, target)` 裁决,所以 `..` 上溯或根外绝对路径都以 `outside-workspace` 失败——绝不做字符串前缀比较,那看不见离开根的 realpath。第三,上限:文本超过 `maxBytes` 的页以 `too-large` 失败而不是被截短送达——分页读取不限制文件总大小,完整读取则受 `maxFileBytes` 限制——`maxEntries` 则截断列举并置 `truncated`。第四,文本:到该页末尾为止非 UTF-8 的内容,或含 NUL 字节的页,以 `not-text` 失败;页之后的字节不检查。路径不存在以 `not-found` 失败;空路径是 `gateway/bad-request`。
+每项操作都先通过 `lstat` 拒绝不存在的路径、末端符号链接或错误的文件类型。文件操作随后通过组合文件系统解析和读取,不做额外的工作区包含检查。只有 `list` 要求解析后的目录仍位于工作区内。配置的分页、窗口、完整文件和目录列举上限仍然适用。文本页还拒绝无效 UTF-8 与 NUL 字节;字节读取不解码内容。空路径是 `gateway/bad-request`。
 
 ### 变更流
 
@@ -70,7 +70,7 @@ kind: "package-reference"
 
 ### 失败
 
-每种失败都是一个带类型化 details 的 `RemoteError` 代码,声明于 [`src/types.ts`](src/types.ts):`workspace-file/not-found`、`workspace-file/outside-workspace`、`workspace-file/too-large`(带 `limit`,即适用的页、窗口或完整文件上限)、`workspace-file/not-text`、`workspace-file/not-regular-file`(`kind` 为 `directory`、`symlink` 或 `other`)以及 `workspace-file/not-directory`(`kind` 为 `file`、`symlink` 或 `other`)。调用方按代码分支,绝不按消息文本。
+每种失败都是一个带类型化 details 的 `RemoteError` 代码,声明于 [`src/types.ts`](src/types.ts):`workspace-file/not-found`、`workspace-file/outside-workspace`(仅目录列举)、`workspace-file/too-large`(带 `limit`,即适用的页、窗口或完整文件上限)、`workspace-file/not-text`、`workspace-file/not-regular-file`(`kind` 为 `directory`、`symlink` 或 `other`)以及 `workspace-file/not-directory`(`kind` 为 `file`、`symlink` 或 `other`)。调用方按代码分支,绝不按消息文本。
 
 ### Client 文件资源
 
@@ -92,7 +92,7 @@ kind: "package-reference"
 
 ### 设计概念
 
-经 `ctx.fs` 的读取是有意不加限制的——沙箱后端只围栏写与编辑——所以这里的每条约束都是服务自己的。页从 `streamText` 切出,后者逐块解码并拒绝非 UTF-8:切页器对窗口之前的行只计数不保留,对窗口内的每个片段先按字节上限验收再缓冲,并在窗口之后的第一个字符处返回,所以无论多大的文件或多长的单行都不会在内存里超过一页;随后在该页上做 NUL 扫描。流之前的一次 `stat` 给出页所报告的版本与大小。路径关有意先于包含判定:`lstat` 面向路径、看得见链接,而 `resolve` 会跟随它;代价是根外条目会先报告自己的类型再报告位置。
+经 `ctx.fs` 的读取使用后端的读取权限;沙箱后端限制写与编辑,而不限制读取。本服务增加普通文件检查与有界传输,工作区包含要求只属于目录列举与变更观察。页从 `streamText` 切出,后者逐块解码并拒绝非 UTF-8:切页器对窗口之前的行只计数不保留,对窗口内的每个片段先按字节上限验收再缓冲,并在窗口之后的第一个字符处返回。流之前的一次 `stat` 给出页所报告的版本与大小。
 
 ### 源码地图
 
@@ -137,7 +137,7 @@ Typert 生成 `./typert` 与 `./remote` 暴露的 Host 与 Client Remote 产物
 <a id="known-limitations-and-deferred-work"></a>
 
 - **仅覆盖 Agent 写入**——`changes` 转发 `fs/observed` 的发射;子进程、shell 命令或用户编辑器改动的文件不产生任何帧。
-- **类型先于位置**——根外条目若类型本身就不合格,报告的是 `not-regular-file` 或 `not-directory` 而非 `outside-workspace`,因为路径关先于包含判定
+- **仅目录受限**——尽管文件预览可以读取文件系统后端允许的任意路径,`list` 与 `changes` 仍限定在 Session 工作区内
 - **没有总行数**——页只报告 `eof`,不报告后面还有多少行;需要总数的消费方要翻到末尾或按 `bytes` 估算。
 - **超长单行没有页**——超过 `maxBytes` 的单行在包含它的每个窗口都以 `too-large` 失败,因为页按行而非按字节切。
 - **读取不具备事务性**——结果元数据来自内容读取之前的 stat;并发写入可能使报告版本与返回内容不一致。

+ 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: 671367221b4b336bfccb1052054b210f5a42c5ad
-README.zh.md: 990b4175332ba1c72f98f4060c315ec8912cec8b
+README.md: b8098117e28501c64ceb9085e9e5b139c6387e8e
+README.zh.md: 68ee0c08a590ba7d96b4d6bbaa063867d67d6538

+ 3 - 3
packages/client/ui-sidebar-documentpreview/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-Preview workspace documents in the right Sidebar and choose among registered renderers without opening another tab. Markdown and code receive accumulated text pages; PDF and HTML receive complete bytes; unknown file extensions use plain text. The tab owns loading, file status, renderer selection, wrap, and reload, while document bodies register through the same metadata registry and child slot. The Sidebar tab kind is `text`.
+Preview readable files in the right Sidebar and choose among registered renderers without opening another tab. Markdown and code receive accumulated text pages; PDF and HTML receive complete bytes; unknown file extensions use plain text. The tab owns loading, file status, renderer selection, wrap, and reload, while document bodies register through the same metadata registry and child slot. The Sidebar tab kind is `text`.
 
 ## Table of Contents
 
@@ -45,7 +45,7 @@ HTML limits are fields on this package's root Client `Config`, configurable on i
 <a id="addresses"></a>
 ## Addresses
 
-A tab uses the Session address built by `fileAddressFor`, carrying a relative or absolute path. `hostFileOf(address)` takes the Session only from that address, with no external Session argument; neither current nor Tab Session is borrowed. The Host resolves file and related paths and enforces confinement. Metadata for the same complete address is shared by every UI, including Global components. The [Workspace Files README](../../api/workspace-files/README.md) owns these rules; renderer selection does not change the navigation address.
+A tab uses the Session address built by `fileAddressFor`, carrying a relative or absolute path. `hostFileOf(address)` takes the Session only from that address, with no external Session argument; neither current nor Tab Session is borrowed. The Host resolves file and related paths through the Session filesystem, whose backend controls read authority. Metadata for the same complete address is shared by every UI, including Global components. The [Workspace Files README](../../api/workspace-files/README.md) owns these rules; renderer selection does not change the navigation address.
 
 <a id="how-it-reads"></a>
 ## How it reads
@@ -61,7 +61,7 @@ HTML runs in a Blob iframe with exactly `sandbox="allow-scripts"`, without `allo
 
 Shared copy comes from `sidebarDocumentPreview`; each builtin renderer owns its localized labels.
 
-Initial reads, additional pages, and HTML/PDF preparation share a loading indicator that respects reduced-motion preferences. Loaded pages stay visible while another page loads. Code previews show source line numbers by default without including them in copied text; plain text uses the same font size and line height as code.
+Initial reads, additional pages, and HTML/PDF preparation share a loading indicator that respects reduced-motion preferences. Loaded pages stay visible while another page loads. PDF pages form one vertical, width-fitted sequence and render lazily near the viewport. Code previews show source line numbers by default without including them in copied text; plain text uses the same font size and line height as code.
 
 <a id="navigation"></a>
 ## Navigation

+ 3 - 3
packages/client/ui-sidebar-documentpreview/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-在右侧 Sidebar 预览工作区文档,无需另开 tab 即可切换已注册的渲染器。Markdown 和代码接收累计文本页;PDF 和 HTML 接收完整字节;未知文件扩展名使用纯文本。tab 负责加载、文件状态、渲染器选择、换行和重新载入,文档正文通过同一元数据注册表与子 slot 注册。Sidebar tab 的 kind 为 `text`。
+在右侧 Sidebar 预览可读文件,无需另开 tab 即可切换已注册的渲染器。Markdown 和代码接收累计文本页;PDF 和 HTML 接收完整字节;未知文件扩展名使用纯文本。tab 负责加载、文件状态、渲染器选择、换行和重新载入,文档正文通过同一元数据注册表与子 slot 注册。Sidebar tab 的 kind 为 `text`。
 
 ## 目录
 
@@ -45,7 +45,7 @@ HTML 上限是本包根 Client `Config` 的字段,可在现有插件配置项
 <a id="addresses"></a>
 ## 地址
 
-tab 使用 `fileAddressFor` 构造的 Session 地址,携带相对或绝对路径。`hostFileOf(address)` 仅从地址取得 Session,不接收外部 Session 参数,也不借用当前或 Tab Session。Host 解析文件及关联路径,并执行包含检查。任何 UI(包括 Global 组件)都共享同一完整地址的元数据。[Workspace Files README](../../api/workspace-files/README.zh.md)定义这些规则;渲染器选择不改变导航地址。
+tab 使用 `fileAddressFor` 构造的 Session 地址,携带相对或绝对路径。`hostFileOf(address)` 仅从地址取得 Session,不接收外部 Session 参数,也不借用当前或 Tab Session。Host 通过 Session 文件系统解析文件及关联路径,由该后端控制读取权限。任何 UI(包括 Global 组件)都共享同一完整地址的元数据。[Workspace Files README](../../api/workspace-files/README.zh.md)定义这些规则;渲染器选择不改变导航地址。
 
 <a id="how-it-reads"></a>
 ## 怎么读
@@ -61,7 +61,7 @@ HTML 在 Blob iframe 中运行,沙箱属性严格为 `sandbox="allow-scripts"`
 
 共享文案来自 `sidebarDocumentPreview`;各内置渲染器拥有自己的本地化标签。
 
-首次读取、追加页及 HTML/PDF 准备共用加载指示器,并遵循减少动态效果偏好。下一页加载期间保留已显示的内容。代码预览默认显示源码行号,但复制文本不包含行号;纯文本与代码使用相同字号和行高。
+首次读取、追加页及 HTML/PDF 准备共用加载指示器,并遵循减少动态效果偏好。下一页加载期间保留已显示的内容。PDF 页面组成一个纵向、适配宽度的连续序列,并在接近视口时惰性渲染。代码预览默认显示源码行号,但复制文本不包含行号;纯文本与代码使用相同字号和行高。
 
 <a id="navigation"></a>
 ## 导航

+ 2 - 8
packages/client/ui-sidebar-documentpreview/package.json

@@ -42,12 +42,6 @@
     "watch": "tsdown --watch"
   },
   "license": "MIT",
-  "dependencies": {
-    "clsx": "^2.0.0",
-    "pdfjs-dist": "6.3.289",
-    "react": "^18.2.0",
-    "react-dom": "^18.2.0"
-  },
   "peerDependencies": {
     "@deepseek-ai/cordis": "workspace:^"
   },
@@ -72,14 +66,14 @@
     "@testing-library/react": "^16.1.0",
     "@types/react": "~18.3.1",
     "@types/react-dom": "~18.3.0",
-    "react": "^18.2.0",
     "clsx": "^2.0.0",
+    "pdfjs-dist": "6.3.289",
+    "react": "^18.2.0",
     "react-dom": "^18.2.0"
   },
   "files": [
     "lib/index.js",
     "lib/client.js",
-    "lib/pdfjs-NOTICES.txt",
     "lib/types/**/*.d.ts"
   ]
 }

+ 2 - 1
packages/extensions/cordis-client-runner/src/client/slot-catalog.ts

@@ -2259,9 +2259,9 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
     slotInject: 'SidebarRightTabInjected',
     declaredBy: 'an entry in \'rightbar.session\' (client-ui-sidebar-right), so it exists while that entry is mounted',
     occupants: [
+      'client-ui-sidebar-documentpreview TextPreview',
       'client-ui-sidebar-files FilesBody',
       'client-ui-sidebar-right GuideBody',
-      'client-ui-sidebar-documentpreview TextPreview',
     ],
     replaceRisk: 'none',
     example: 'return {\n  inject: [\'slots\'],\n  apply(ctx) {\n    ctx.slots.inject(\'sidebar.right.pane.tab\', () => ctx.slots.register(\n      { name: \'sidebar.right.pane.tab\', key: \'<one key the owner dispatches>\' },\n      () => React.createElement(\'div\', null, \'hello\'),\n    ))\n  },\n}',
@@ -2331,6 +2331,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
     standardProps: [
       'useResource: UseResource',
       'useWorkspaces: SnapshotSelectorHook<WorkspaceSnapshot>',
+      'usePanelInfo: UsePanelInfo',
       'useSessions: UseSessions',
       'useSessionPendingInteraction: UseSessionPendingInteraction',
       'useWorkspaces: SnapshotSelectorHook<WorkspaceSnapshot>',

+ 10 - 10
packages/extensions/tool-cordis/src/api-catalog.ts

@@ -2899,37 +2899,37 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
   },
   {
     key: 'workspaceFiles',
-    summary: 'Host Remote service over the composed filesystem, confined to one workspace.',
-    description: 'Host Remote service over the composed filesystem, confined to one workspace.',
+    summary: 'Host Remote file reads and workspace directory observations over the composed filesystem.',
+    description: 'Host Remote file reads and workspace directory observations over the composed filesystem.',
     methods: [
       {
         signature: '@Remote async read(agent: Agent, path: string, range: WorkspaceFileRange, signal: AbortSignal): Promise<WorkspaceFileText>',
-        description: 'Read one page of lines from a UTF-8 text file inside the Agent\'s workspace.',
-        parameters: [{ name: 'agent', description: 'target Agent resolved from the Session identity on the wire.' }, { name: 'path', description: 'workspace path, absolute or relative to the workspace root.' }, { name: 'range', description: 'the line window; omitted fields take the page defaults.' }, { name: 'signal', description: 'caller cancellation.' }],
+        description: 'Read one page of lines from a UTF-8 file readable by the filesystem backend.',
+        parameters: [{ name: 'agent', description: 'target Agent resolved from the Session identity on the wire.' }, { name: 'path', description: 'absolute path or path relative to the workspace root; files outside it are allowed.' }, { name: 'range', description: 'the line window; omitted fields take the page defaults.' }, { name: 'signal', description: 'caller cancellation.' }],
         returns: 'the page, the file\'s version at the stat before it, and whether it reaches the last line.',
       },
       {
         signature: '@Remote async readBytes(agent: Agent, path: string, range: WorkspaceByteRange, signal: AbortSignal): Promise<WorkspaceFileBytes>',
-        description: 'Read one byte window of a regular file inside the Agent\'s workspace: raw bytes, no text decoding and no binary rejection.',
-        parameters: [{ name: 'agent', description: 'target Agent resolved from the Session identity on the wire.' }, { name: 'path', description: 'workspace path, absolute or relative to the workspace root.' }, { name: 'range', description: 'the byte window; omitted fields take the window defaults.' }, { name: 'signal', description: 'caller cancellation.' }],
+        description: 'Read one byte window of a regular file readable by the filesystem backend: raw bytes, no text decoding and no binary rejection.',
+        parameters: [{ name: 'agent', description: 'target Agent resolved from the Session identity on the wire.' }, { name: 'path', description: 'absolute path or path relative to the workspace root; files outside it are allowed.' }, { name: 'range', description: 'the byte window; omitted fields take the window defaults.' }, { name: 'signal', description: 'caller cancellation.' }],
         returns: 'the window in base64, the file\'s version and size at the stat before it, and whether it reaches the last byte.',
       },
       {
         signature: '@Remote async readAll(agent: Agent, path: string, signal: AbortSignal): Promise<WorkspaceFileBytes>',
         description: 'Read a complete regular file as bytes, subject to the configured full-file cap.',
-        parameters: [{ name: 'agent', description: 'target Agent whose workspace confines the read.' }, { name: 'path', description: 'absolute or workspace-relative file path.' }, { name: 'signal', description: 'caller cancellation.' }],
+        parameters: [{ name: 'agent', description: 'target Agent whose workspace resolves relative paths.' }, { name: 'path', description: 'absolute or workspace-relative file path.' }, { name: 'signal', description: 'caller cancellation.' }],
         returns: 'one complete base64 window with offset zero and eof true; oversized files fail with too-large.',
       },
       {
         signature: '@Remote async readRelated(agent: Agent, path: string, relativePath: string, signal: AbortSignal): Promise<WorkspaceFileBytes>',
-        description: 'Read a complete file relative to another file\'s directory within the same workspace.',
-        parameters: [{ name: 'agent', description: 'Agent whose workspace confines both files.' }, { name: 'path', description: 'base file, absolute or workspace-relative.' }, { name: 'relativePath', description: 'relative filesystem path, not a URL or absolute path.' }, { name: 'signal', description: 'caller cancellation.' }],
+        description: 'Read a complete file relative to another file\'s directory, including outside the workspace.',
+        parameters: [{ name: 'agent', description: 'Agent whose workspace resolves the base file\'s relative path.' }, { name: 'path', description: 'base file, absolute or workspace-relative.' }, { name: 'relativePath', description: 'relative filesystem path, not a URL or absolute path.' }, { name: 'signal', description: 'caller cancellation.' }],
         returns: 'the complete related file using the ordinary file-size and access checks.',
       },
       {
         signature: '@Remote async stat(agent: Agent, path: string, signal: AbortSignal): Promise<WorkspaceFileStat>',
         description: 'Report one regular file\'s identity, version, and size without its content.',
-        parameters: [{ name: 'agent', description: 'target Agent resolved from the Session identity on the wire.' }, { name: 'path', description: 'workspace path, absolute or relative to the workspace root.' }, { name: 'signal', description: 'caller cancellation.' }],
+        parameters: [{ name: 'agent', description: 'target Agent resolved from the Session identity on the wire.' }, { name: 'path', description: 'absolute path or path relative to the workspace root; files outside it are allowed.' }, { name: 'signal', description: 'caller cancellation.' }],
         returns: 'the file\'s absolute path, current version, and byte size.',
       },
       {

+ 12 - 13
pnpm-lock.yaml

@@ -3752,19 +3752,6 @@ importers:
         version: 18.3.1
 
   packages/client/ui-sidebar-documentpreview:
-    dependencies:
-      clsx:
-        specifier: ^2.0.0
-        version: 2.1.1
-      pdfjs-dist:
-        specifier: 6.3.289
-        version: 6.3.289
-      react:
-        specifier: ^18.2.0
-        version: 18.3.1
-      react-dom:
-        specifier: ^18.2.0
-        version: 18.3.1(react@18.3.1)
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -3826,6 +3813,18 @@ importers:
       '@types/react-dom':
         specifier: ~18.3.0
         version: 18.3.7(@types/react@18.3.31)
+      clsx:
+        specifier: ^2.0.0
+        version: 2.1.1
+      pdfjs-dist:
+        specifier: 6.3.289
+        version: 6.3.289
+      react:
+        specifier: ^18.2.0
+        version: 18.3.1
+      react-dom:
+        specifier: ^18.2.0
+        version: 18.3.1(react@18.3.1)
 
   packages/client/ui-sidebar-files:
     devDependencies:

+ 4 - 2
snapshots/web/document-preview/document.expected.md

@@ -20,6 +20,7 @@
 - Sandbox: allow-scripts
 - Inline script: INLINE_OK
 - Local script: LOCAL_JS_OK
+- Outside-workspace script: OUTSIDE_JS_OK
 - Local stylesheet: rgb(12, 34, 56)
 - Parent access: parent-blocked (SecurityError)
 - Parent unchanged: true
@@ -28,14 +29,15 @@
 
 - Viewer: PDF
 - Worker: dsh-pdf
-- Page count: of 2
+- Continuous pages: 2
+- Horizontal overflow: false
 - Canvas fills: red -> blue -> blue
-- Page after Files round trip: 2
 - Same tab: true
 
 ## Code paging
 
 - Viewer: Code
+- Initial reading indicator: true
 - Lines: 64 -> 65
 - Prefix retained: true
 - Tail: const tail = "CODE_TAIL";

+ 1 - 1
snapshots/web/document-preview/snapshot.yml

@@ -6,4 +6,4 @@ recording: authored
 header:
   class: web-default
 session:
-  source: ../lifecycle-chrome/session.v2.jsonl
+  source: ../lifecycle-chrome/session.v3.jsonl