@deepseek-ai/dsh-api-workspace-files 0.1.5-alpha.1 → 0.1.5-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/api/workspace-files/README.md
5
- README.md: ec15f52bf17fffca2b225fa427a7405922aaf2b3
6
- README.zh.md: 7e45d5c9a6073435e26e1237791a328db654d839
5
+ README.md: f0f9cb1532fa65954415e10a5e248b775cdff83a
6
+ README.zh.md: c33d0632fde318c330da4102211b69ad2be048f7
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "Workspace file service for the web GUI: paged read, byte windows, stat, directory listing, and the Agent-write change feed inside the Session workspace root, exposed as the workspaceFiles Remote namespace."
2
+ description: "Workspace file service for the web GUI: bounded file reads through the composed filesystem, plus directory listing and instrumented filesystem observation inside the Session workspace root."
3
3
  kind: "package-reference"
4
4
  ---
5
5
 
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
9
9
 
10
10
  ## Summary
11
11
 
12
- 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, 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.
12
+ 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 instrumented filesystem observations remain workspace-scoped. The service exposes no mutation operation.
13
13
 
14
14
  ## Table of Contents
15
15
 
@@ -25,19 +25,21 @@ Use this package to browse and inspect files within a Session's workspace from t
25
25
  <a id="use-this-package"></a>
26
26
  ## Use this package
27
27
 
28
- Mount the package beside `dsh-fs`, `dsh-sandbox-policy`, and the Typert Gateway; the bundle does so right after the Session Controller. Every method takes the Session identity on the wire, so a Client calls `remote.workspaceFiles.read(agent, path, range, signal)`, `stat(agent, path, signal)`, `readBytes(agent, path, range, signal)`, `list(agent, path, signal)`, or `changes(agent, signal)` and never names a root itself.
28
+ Mount the package beside `dsh-fs`, `dsh-sandbox-policy`, the Session store, and the Typert Gateway; the bundle does so right after the Session Controller. Every method takes the Session identity on the wire, so a Client calls `remote.workspaceFiles.read(sessionId, path, range, signal)`, `stat(sessionId, path, signal)`, `readBytes(sessionId, path, range, signal)`, `list(sessionId, path, signal)`, or `changes(sessionId, signal)` and never names a root itself. The Host reads a live Session header or uses persistence `stat` for a cold Session; it does not activate an Agent, read the event body, or borrow a parent Session's root. Session persistence is optional for live reads, but without it a cold Session cannot resolve and the Gateway returns `gateway/lookup-not-found`.
29
29
 
30
30
  | Method | Returns | Purpose |
31
31
  |---|---|---|
32
32
  | `stat(path)` | `WorkspaceFileStat { absolutePath, version, bytes? }` | Identity, version, and size of one regular file, without content |
33
33
  | `read(path, { offset?, limit? })` | `WorkspaceFileText` = stat + `{ offset, text, lines, eof }` | One window of lines from a UTF-8 text file; `lines` counts them, so one empty line and a page past the end read differently |
34
34
  | `readBytes(path, { offset?, length? })` | `WorkspaceFileBytes` = stat + `{ offset, data, eof }` | One window of raw bytes from any regular file, base64-encoded |
35
+ | `readAll(path)` | `WorkspaceFileBytes` with `offset: 0`, `eof: true` | Complete raw bytes under `maxFileBytes`; oversized files fail instead of being truncated |
36
+ | `readRelated(path, relativePath)` | `WorkspaceFileBytes` | Complete bytes of a file resolved from the base file's directory on the Host |
35
37
  | `list(path)` | `WorkspaceDirectoryListing { path, entries, truncated }` | Direct children of one directory |
36
- | `changes()` | stream of `WorkspaceFileWatchFrame` | Subscription readiness, then Agent observations inside the workspace root |
38
+ | `changes()` | stream of `WorkspaceFileWatchFrame` | Subscription readiness, then filesystem observations inside the workspace root |
37
39
 
38
40
  ### Addressing and paths
39
41
 
40
- `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 `/`.
42
+ `read`, `readBytes`, `readAll`, `readRelated`, and `stat` accept an absolute path or one relative to the selected 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 instrumented filesystem observations inside the workspace root.
41
43
 
42
44
  ### Pages
43
45
 
@@ -47,9 +49,9 @@ Mount the package beside `dsh-fs`, `dsh-sandbox-policy`, and the Typert Gateway;
47
49
 
48
50
  `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.
49
51
 
50
- ### The four gates
52
+ ### File-read and directory checks
51
53
 
52
- 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 — the file itself has no size cap — 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`.
54
+ 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`.
53
55
 
54
56
  ### The change feed
55
57
 
@@ -60,6 +62,7 @@ Every read, stat, and listing passes four gates in this order. First, `lstat` in
60
62
  | Field | Default | Meaning |
61
63
  |---|---|---|
62
64
  | `maxBytes` | `2097152` (2 MiB) | Inclusive byte cap on one page's text and on one byte window; a larger page or window fails |
65
+ | `maxFileBytes` | `33554432` (32 MiB) | Inclusive complete-file cap for `readAll` and `readRelated`; larger files fail with `too-large` |
63
66
  | `maxLines` | `5000` | Default and largest page size in lines; a larger `limit` is refused |
64
67
  | `maxEntries` | `2000` | Cap on returned directory entries; the rest is dropped and reported cut |
65
68
 
@@ -67,15 +70,15 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
67
70
 
68
71
  ### Failures
69
72
 
70
- 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 page and window 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.
73
+ 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.
71
74
 
72
75
  ### Client file resources
73
76
 
74
- The browser export registers the `file` provider into `ctx.resources` and requires `resources`, `remote`, `remote.workspaceFiles`, and `sessions`. The bundle's single `workspace-files` row supplies both faces; the Client has no separate configuration. A component follows a file through its standard `useResource<'file'>(address)` prop and reads `{ absolutePath, version, bytes?, changed }`; content is fetched separately through the paged methods.
77
+ The browser export registers the `file` provider into `ctx.resources` and requires `resources`, `remote`, and `remote.workspaceFiles`. The bundle's single `workspace-files` row supplies both faces; the Client has no separate configuration. A component reads `WorkspaceFileStat { absolutePath, version, bytes? }` metadata through `useResource<'file'>(address)` and fetches content separately through Remote reads. Any UI, including Global components, shares the observation for the same complete address.
75
78
 
76
- A `session/<sessionId>/<path>` resource address sends its relative path unchanged to the Host, which resolves and confines it against that Session's workspace root; the Client needs no Session `cwd`. An `absolute/<path>` address reads through the current Session. Both use the `dsh-resource://file/` grammar in [workspace-path](../../util/workspace-path/README.md). An absolute address without a current Session produces `workspace-file/unknown-workspace`; an unsupported address produces `workspace-file/unsupported-address`. These Client failures end the stream and make reload a no-op.
79
+ A `session/<sessionId>/<path>` address carries the authorizing Session and a relative or absolute path; leading slashes are preserved, as in `dsh-resource://file/session/s//etc/hosts`. The Host receives the path unchanged and owns resolution and access checks; the Client needs no Session `cwd`. `absolute/<path>` remains parseable but has no authorizing Session and fails with `workspace-file/unknown-workspace`, without borrowing current or Tab Session. Unsupported addresses fail with `workspace-file/unsupported-address`. [Workspace-path](../../util/workspace-path/README.md) owns the grammar; the generic Resource layer knows only the address and `signal`.
77
80
 
78
- The provider waits for the Host's `ready` frame before its first `stat`, queues changes during the read, then binds the follower to `stat.absolutePath`. Both queued and live changes match that Host-returned path. A new write version raises `changed` while retaining the last byte size; duplicate versions are ignored. An absent notice or reload re-stats the file. A failed stat keeps the address followed; a later write or reload can recover it, and any Session write can trigger a retry before the first successful path binding. Reload clears `changed`; a Host-triggered re-stat keeps it raised. Frames are `RemoteResult` values, and programming exceptions remain uncaught.
81
+ The provider waits for the Host's `ready` frame before its first `stat`, queues changes during the read, then binds the follower to `stat.absolutePath`. Both queued and live changes match that Host-returned path. A new write version updates metadata while retaining the last byte size; duplicate versions are ignored. An absent notice re-stats the file. A failed stat keeps the address followed; a later write can recover it, and any Session write can trigger a retry before the first successful path binding. Frames are `RemoteResult` values, and programming exceptions remain uncaught.
79
82
 
80
83
  One supervised `changes` stream serves every followed file in a Session. Followers match absolute paths with backslashes normalized to slashes. Carrier loss reconnects through the Gateway supervisor; a Host-ended or terminally failed feed ends its followers and leaves their last metadata readable until reopened. The last follower leaving disposes the stream, a successor waits for that disposal, and plugin teardown awaits all pending closes. The provider declares `ResourceProtocolMap.file`; the text preview declares its Sidebar line-navigation parameters.
81
84
 
@@ -89,13 +92,13 @@ One supervised `changes` stream serves every followed file in a Session. Followe
89
92
 
90
93
  ### Design concept
91
94
 
92
- 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.
95
+ Reads through `ctx.fs` use the backend's read authority; the sandboxing backend fences writes and edits, not reads. A Typert lookup derives `WorkspaceFileScope` from a live Session header or the persistence service's header-only `stat`, so cold subagent Sessions need neither Agent activation nor event-body 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.
93
96
 
94
97
  ### Source map
95
98
 
96
99
  | File | Role |
97
100
  |---|---|
98
- | [`src/index.ts`](src/index.ts) | `WorkspaceFiles`: the `workspaceFiles` service and Remote namespace, `Config`, the gates, the page cutter, `read`, `readBytes`, `stat`, `list` |
101
+ | [`src/index.ts`](src/index.ts) | `WorkspaceFiles`: the `workspaceFiles` service and Remote namespace, `Config`, the gates, the page cutter, `read`, `readBytes`, `readAll`, `readRelated`, `stat`, `list` |
99
102
  | [`src/changes.ts`](src/changes.ts) | `WorkspaceChangeFeed`: `fs/observed` subscription and one queue per open `changes` generation |
100
103
  | [`src/types.ts`](src/types.ts) | Wire types and the `RemoteErrorDetailsMap` codes, published as `./types` for Client packages |
101
104
  | [`src/client/index.ts`](src/client/index.ts), [`provider.ts`](src/client/provider.ts), [`change-feed.ts`](src/client/change-feed.ts) | Browser plugin, file metadata, and per-Session change feed |
@@ -116,7 +119,7 @@ Typert generates the Host and Client Remote artifacts exposed by `./typert` and
116
119
  - [Remote assembly](../../api/remotes/README.md) — how Client packages reach the `workspaceFiles` namespace.
117
120
  - [Client resources](../../client/resources/README.md) — the resource model, `useResource`, pins, and provider lifetime.
118
121
  - [Workspace path helpers](../../util/workspace-path/README.md) — `fileAddressFor` and `parseFileAddress`, the `dsh-resource://file/…` address grammar both ends share.
119
- - [Sidebar text preview](../../client/ui-sidebar-textpreview/README.md) — the tab type that follows a file through the `file` provider and reads its pages.
122
+ - [Sidebar text preview](../../client/ui-sidebar-documentpreview/README.md) — the tab type that follows a file through the `file` provider and reads its pages.
120
123
 
121
124
  -----
122
125
 
@@ -133,15 +136,14 @@ None; this package neither assembles nor sends a provider request.
133
136
 
134
137
  <a id="known-limitations-and-deferred-work"></a>
135
138
 
136
- - **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.
137
- - **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.
139
+ - **Instrumented operations only** — `changes` relays `fs/observed` emissions; a file changed by a subprocess, a shell command, or the user's editor produces no frame.
140
+ - **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.
138
141
  - **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`.
139
142
  - **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.
140
- - **Version precedes content** — the `version` on a page is the stat's, taken before the stream; a write landing between the two leaves the page one version behind, which the next `changes` frame reports.
143
+ - **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.
141
144
  - **Unbounded generation queue** — a `changes` generation buffers every contained observation until its consumer pulls; a stalled consumer grows Host memory for the life of the stream.
142
145
  - **`maxEntries` bounds the answer, not the listing** — `list` asks `ctx.fs.listDir` for every child and cuts the array afterwards, so a directory far above the cap still costs the Host the whole listing (on `fs-local`, one stat per child); bounding that work needs a limit on the filesystem seam's `listDir`.
143
- - **Dead feeds retain metadata** — after the Host ends `changes` or the stream fails terminally, open values retain their last state until reopened; reload does not reopen the stream.
144
- - **Reload is shared by path** — a reload re-stats every follower of that absolute path in the Session and clears their `changed` flags, including readers that did not reload their content. Per-record reload delivery remains deferred.
146
+ - **Dead feeds retain metadata** — after the Host ends `changes` or the stream fails terminally, open values retain their last state until reopened.
145
147
 
146
148
  <a id="dev-note"></a>
147
149
  ### Dev Note
package/README.zh.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "面向 Web GUI 的工作区文件服务:在 Session 工作区根内做分页读取、字节窗口、stat、目录列举与 Agent 写入变更流,以 workspaceFiles Remote 命名空间暴露。"
2
+ description: "面向 Web GUI 的工作区文件服务:通过组合文件系统进行有界文件读取,并在 Session 工作区根内列举目录和观察已埋点的文件系统操作。"
3
3
  kind: "package-reference"
4
4
  ---
5
5
 
@@ -9,7 +9,7 @@ kind: "package-reference"
9
9
 
10
10
  ## 概述
11
11
 
12
- 使用本包可从 Web Client 浏览和检查 Session 工作区内的文件。它按行分页读取 UTF-8 文本、按有界窗口读取原始字节、报告文件版本与大小、列举目录的直接子项,并流式推送 Agent 文件操作造成的变更。每项操作都限定在为被寻址 Session 选择的工作区根内,不受文件系统后端工作目录影响。Client 组件还可经共享 Remote API 跟随实时文件元数据并构建 Sidebar 文件树。
12
+ 使用本包可从 Web Client 预览 Session 文件系统允许读取的文件。它按页读取 UTF-8 文本、按有界窗口或完整文件读取原始字节、从基文件目录解析关联文件,并报告文件元数据。文件读取可以指向工作区外路径;目录列举与已埋点的文件系统观察仍限定于工作区。本服务不提供修改操作。
13
13
 
14
14
  ## 目录
15
15
 
@@ -25,19 +25,21 @@ kind: "package-reference"
25
25
  <a id="use-this-package"></a>
26
26
  ## 使用本包
27
27
 
28
- 把本包与 `dsh-fs`、`dsh-sandbox-policy` 和 Typert Gateway 一起挂载;bundle 把它紧随 Session Controller 之后挂载。每个方法都在线路上携带 Session 身份,Client 调用 `remote.workspaceFiles.read(agent, path, range, signal)`、`stat(agent, path, signal)`、`list(agent, path, signal)` 或 `changes(agent, signal)`,从不自己指定根。
28
+ 把本包与 `dsh-fs`、`dsh-sandbox-policy`、Session store 和 Typert Gateway 一起挂载;bundle 把它紧随 Session Controller 之后挂载。每个方法都在线路上携带 Session 身份,Client 调用 `remote.workspaceFiles.read(sessionId, path, range, signal)`、`stat(sessionId, path, signal)`、`readBytes(sessionId, path, range, signal)`、`list(sessionId, path, signal)` 或 `changes(sessionId, signal)`,从不自己指定根。Host 读取 live Session header,cold Session 则使用持久层 `stat`;它不会激活 Agent、读取事件正文或借用父 Session 的根。live 读取不要求挂载 Session persistence;未挂载时 cold Session 无法解析,Gateway 返回 `gateway/lookup-not-found`。
29
29
 
30
30
  | 方法 | 返回 | 用途 |
31
31
  |---|---|---|
32
32
  | `stat(path)` | `WorkspaceFileStat { absolutePath, version, bytes? }` | 一个普通文件的身份、版本与大小,不含内容 |
33
33
  | `read(path, { offset?, limit? })` | `WorkspaceFileText` = stat + `{ offset, text, lines, eof }` | UTF-8 文本文件的一个行窗口;`lines` 计行数,使单个空行与越过文件末尾的页可区分 |
34
34
  | `readBytes(path, { offset?, length? })` | `WorkspaceFileBytes` = stat + `{ offset, data, eof }` | 任意普通文件的一个原始字节窗口,base64 编码 |
35
+ | `readAll(path)` | `WorkspaceFileBytes`,其中 `offset: 0`、`eof: true` | `maxFileBytes` 内的完整原始字节;超大文件失败,不截断 |
36
+ | `readRelated(path, relativePath)` | `WorkspaceFileBytes` | Host 从基文件目录解析出的文件的完整字节 |
35
37
  | `list(path)` | `WorkspaceDirectoryListing { path, entries, truncated }` | 一个目录的直接子项 |
36
- | `changes()` | `WorkspaceFileWatchFrame` 流 | 订阅就绪确认,随后为工作区根内的 Agent 观察 |
38
+ | `changes()` | `WorkspaceFileWatchFrame` 流 | 订阅就绪确认,随后为工作区根内的文件系统观察 |
37
39
 
38
40
  ### 寻址与路径
39
41
 
40
- `read`、`stat` 与 `list` 接受工作区路径,可以是绝对路径,也可以是相对于 Session 工作区根的路径。离开服务的路径词汇有两套,每个方法只用其中一套:`read`、`stat` `changes` 以文件系统执行环境中的绝对路径报告文件,符号链接已解析(`WorkspaceFileStat.absolutePath`、`WorkspaceFileChange.absolutePath`),因为其消费方是 Client 资源系统,它按这条路径跟随变更;`list` 以相对于根的工作区路径报告被列举目录——根自身为空串——因为其消费方是一棵以根为起点的树,子项路径就是该值与条目名以 `/` 连接。
42
+ `read`、`readBytes`、`readAll`、`readRelated` 与 `stat` 接受绝对路径或相对于所选 Session 工作区根的路径。组合文件系统决定路径是否可读;本服务不额外要求文件读取限定于工作区。`readRelated` 从基文件所在目录解析相对文件系统路径,基文件或目标文件位于工作区外时同样适用。这些方法以文件系统执行环境中的绝对路径报告文件。`list` 仍限定于工作区,并以相对于该根的路径报告被列举目录。`changes` 同样只报告工作区根内已埋点的文件系统观察。
41
43
 
42
44
  ### 分页
43
45
 
@@ -47,9 +49,9 @@ kind: "package-reference"
47
49
 
48
50
  `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`。
49
51
 
50
- ### 四道关
52
+ ### 文件读取与目录检查
51
53
 
52
- 每次读取、stat 与列举依次过四道关。第一,`lstat` 在跟随任何东西之前检查路径本身:符号链接不论指向哪里,`read` `stat` 都以 `not-regular-file`、`list` 都以 `not-directory` 拒绝,并带上条目的 `kind`。第二,包含判定:路径解析为目标后由 `ctx.fs.contains(root, target)` 裁决,所以 `..` 上溯或根外绝对路径都以 `outside-workspace` 失败——绝不做字符串前缀比较,那看不见离开根的 realpath。第三,上限:文本超过 `maxBytes` 的页以 `too-large` 失败而不是被截短送达——文件本身没有大小上限——`maxEntries` 则截断列举并置 `truncated`。第四,文本:到该页末尾为止非 UTF-8 的内容,或含 NUL 字节的页,以 `not-text` 失败;页之后的字节不检查。路径不存在以 `not-found` 失败;空路径是 `gateway/bad-request`。
54
+ 每项操作都先通过 `lstat` 拒绝不存在的路径、末端符号链接或错误的文件类型。文件操作随后通过组合文件系统解析和读取,不做额外的工作区包含检查。只有 `list` 要求解析后的目录仍位于工作区内。配置的分页、窗口、完整文件和目录列举上限仍然适用。文本页还拒绝无效 UTF-8 NUL 字节;字节读取不解码内容。空路径是 `gateway/bad-request`。
53
55
 
54
56
  ### 变更流
55
57
 
@@ -60,6 +62,7 @@ kind: "package-reference"
60
62
  | 字段 | 默认值 | 含义 |
61
63
  |---|---|---|
62
64
  | `maxBytes` | `2097152`(2 MiB) | 单页文本与单个字节窗口的字节上限(含);更大的页或窗口失败 |
65
+ | `maxFileBytes` | `33554432`(32 MiB) | `readAll` 和 `readRelated` 的完整文件字节上限(含);更大文件以 `too-large` 失败 |
63
66
  | `maxLines` | `5000` | 页大小的缺省值与上限(行);更大的 `limit` 被拒绝 |
64
67
  | `maxEntries` | `2000` | 返回目录条目数上限;其余丢弃并报告截断 |
65
68
 
@@ -67,15 +70,15 @@ kind: "package-reference"
67
70
 
68
71
  ### 失败
69
72
 
70
- 每种失败都是一个带类型化 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`)。调用方按代码分支,绝不按消息文本。
73
+ 每种失败都是一个带类型化 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`)。调用方按代码分支,绝不按消息文本。
71
74
 
72
75
  ### Client 文件资源
73
76
 
74
- 浏览器导出向 `ctx.resources` 注册 `file` 提供者,要求 `resources`、`remote`、`remote.workspaceFiles` 和 `sessions` 在场。bundle 中单个 `workspace-files` 条目供应两面;Client 没有单独配置。组件经标准 prop `useResource<'file'>(address)` 跟随文件,读取 `{ absolutePath, version, bytes?, changed }`;内容通过分页方法另行获取。
77
+ 浏览器导出向 `ctx.resources` 注册 `file` 提供方,要求 `resources`、`remote` 和 `remote.workspaceFiles` 在场。bundle 中单个 `workspace-files` 条目供应两面;Client 没有单独配置。组件通过 `useResource<'file'>(address)` 读取 `WorkspaceFileStat { absolutePath, version, bytes? }` 元数据,内容另经 Remote 读取。任何 UI(包括 Global)访问同一完整地址都共享观察。
75
78
 
76
- `session/<sessionId>/<path>` 资源地址把相对路径原样发送给 Host,由 Host 按该 Session 的工作区根解析并检查包含关系;Client 不需要 Session `cwd`。`absolute/<path>` 地址经当前 Session 读取。两者都使用[workspace-path](../../util/workspace-path/README.zh.md)规定的 `dsh-resource://file/` 语法。没有当前 Session 的绝对地址产生 `workspace-file/unknown-workspace`;不支持的地址产生 `workspace-file/unsupported-address`。这些 Client 失败会结束流,并使刷新无动作。
79
+ `session/<sessionId>/<path>` 地址携带授权 Session,以及相对或绝对路径;前导斜杠保留,例如 `dsh-resource://file/session/s//etc/hosts`。Host 原样接收路径,负责解析与权限检查;Client 不需要 Session `cwd`。`absolute/<path>` 仍可解析,但没有授权 Session,以 `workspace-file/unknown-workspace` 失败,不借用当前或 Tab Session。不支持的地址以 `workspace-file/unsupported-address` 失败。语法由 [workspace-path](../../util/workspace-path/README.zh.md) 定义;Resource 泛型层只认地址和 `signal`。
77
80
 
78
- 提供者等到 Host 的 `ready` 帧后才发首次 `stat`,读取期间将变更排队,随后将跟随者绑定到 `stat.absolutePath`。排队与实时变更都按该 Host 返回路径匹配。新的写入版本置 `changed`,并保留最近的字节大小;重复版本被忽略。消失通知或刷新会重新 stat 文件。stat 失败后仍跟随地址,后续写入或刷新可使其恢复;首次成功绑定路径前,Session 内任何写入都可触发重试。刷新清除 `changed`,由 Host 触发的重新 stat 保留标记。帧是 `RemoteResult` 值,编程异常不被捕获。
81
+ 提供者等到 Host 的 `ready` 帧后才发首次 `stat`,读取期间将变更排队,随后将跟随者绑定到 `stat.absolutePath`。排队与实时变更都按该 Host 返回路径匹配。新的写入版本更新元数据并保留最近的字节大小;重复版本被忽略。消失通知会重新 stat 文件。stat 失败后仍跟随地址,后续写入可使其恢复;首次成功绑定路径前,Session 内任何写入都可触发重试。帧是 `RemoteResult` 值,编程异常不被捕获。
79
82
 
80
83
  每个 Session 的所有被跟随文件共用一条受监督的 `changes` 流。跟随者按反斜杠归一为斜杠的绝对路径匹配。载体掉线由 Gateway 监督器重连;Host 结束或终态失败的流会结束其跟随者,最后的元数据仍可读取,直到重新打开。最后一个跟随者离开时释放流,后继流等待该释放完成,插件拆除等待所有在途关闭。提供者声明 `ResourceProtocolMap.file`;文本预览声明其 Sidebar 行号导航参数。
81
84
 
@@ -89,13 +92,13 @@ kind: "package-reference"
89
92
 
90
93
  ### 设计概念
91
94
 
92
- 经 `ctx.fs` 的读取是有意不加限制的——沙箱后端只围栏写与编辑——所以这里的每条约束都是服务自己的。页从 `streamText` 切出,后者逐块解码并拒绝非 UTF-8:切页器对窗口之前的行只计数不保留,对窗口内的每个片段先按字节上限验收再缓冲,并在窗口之后的第一个字符处返回,所以无论多大的文件或多长的单行都不会在内存里超过一页;随后在该页上做 NUL 扫描。流之前的一次 `stat` 给出页所报告的版本与大小。路径关有意先于包含判定:`lstat` 面向路径、看得见链接,而 `resolve` 会跟随它;代价是根外条目会先报告自己的类型再报告位置。
95
+ 经 `ctx.fs` 的读取使用后端的读取权限;沙箱后端限制写与编辑,而不限制读取。Typert lookup live Session header 或持久层的 header-only `stat` 导出 `WorkspaceFileScope`,所以 cold subagent Session 不需要激活 Agent 或读取事件正文。本服务增加普通文件检查与有界传输,工作区包含要求只属于目录列举与变更观察。页从 `streamText` 切出,后者逐块解码并拒绝非 UTF-8:切页器对窗口之前的行只计数不保留,对窗口内的每个片段先按字节上限验收再缓冲,并在窗口之后的第一个字符处返回。流之前的一次 `stat` 给出页所报告的版本与大小。
93
96
 
94
97
  ### 源码地图
95
98
 
96
99
  | 文件 | 职责 |
97
100
  |---|---|
98
- | [`src/index.ts`](src/index.ts) | `WorkspaceFiles`:`workspaceFiles` 服务与 Remote 命名空间、`Config`、四道关、切页器、`read`、`readBytes`、`stat`、`list` |
101
+ | [`src/index.ts`](src/index.ts) | `WorkspaceFiles`:`workspaceFiles` 服务与 Remote 命名空间、`Config`、四道关、切页器、`read`、`readBytes`、`readAll`、`readRelated`、`stat`、`list` |
99
102
  | [`src/changes.ts`](src/changes.ts) | `WorkspaceChangeFeed`:`fs/observed` 订阅与每个打开的 `changes` generation 各一条队列 |
100
103
  | [`src/types.ts`](src/types.ts) | 线路类型与 `RemoteErrorDetailsMap` 错误码,以 `./types` 发布给 Client 包 |
101
104
  | [`src/client/index.ts`](src/client/index.ts)、[`provider.ts`](src/client/provider.ts)、[`change-feed.ts`](src/client/change-feed.ts) | 浏览器插件、文件元数据与每 Session 变更流 |
@@ -116,7 +119,7 @@ Typert 生成 `./typert` 与 `./remote` 暴露的 Host 与 Client Remote 产物
116
119
  - [Remote 装配](../../api/remotes/README.zh.md)——Client 包如何触达 `workspaceFiles` 命名空间。
117
120
  - [Client 资源](../../client/resources/README.zh.md)——资源模型、`useResource`、pin 与提供者生命周期。
118
121
  - [工作区路径辅助](../../util/workspace-path/README.zh.md)——`fileAddressFor` 与 `parseFileAddress`,两端共享的 `dsh-resource://file/…` 地址语法。
119
- - [Sidebar 文本预览](../../client/ui-sidebar-textpreview/README.zh.md)——经 `file` 提供者跟随文件并读取其页的 tab 类型。
122
+ - [Sidebar 文本预览](../../client/ui-sidebar-documentpreview/README.zh.md)——经 `file` 提供者跟随文件并读取其页的 tab 类型。
120
123
 
121
124
  -----
122
125
 
@@ -133,15 +136,14 @@ Typert 生成 `./typert` 与 `./remote` 暴露的 Host 与 Client Remote 产物
133
136
 
134
137
  <a id="known-limitations-and-deferred-work"></a>
135
138
 
136
- - **仅覆盖 Agent 写入**——`changes` 转发 `fs/observed` 的发射;子进程、shell 命令或用户编辑器改动的文件不产生任何帧。
137
- - **类型先于位置**——根外条目若类型本身就不合格,报告的是 `not-regular-file` `not-directory` 而非 `outside-workspace`,因为路径关先于包含判定。
139
+ - **仅覆盖已埋点操作**——`changes` 转发 `fs/observed` 的发射;子进程、shell 命令或用户编辑器改动的文件不产生任何帧。
140
+ - **仅目录受限**——尽管文件预览可以读取文件系统后端允许的任意路径,`list` `changes` 仍限定在 Session 工作区内。
138
141
  - **没有总行数**——页只报告 `eof`,不报告后面还有多少行;需要总数的消费方要翻到末尾或按 `bytes` 估算。
139
142
  - **超长单行没有页**——超过 `maxBytes` 的单行在包含它的每个窗口都以 `too-large` 失败,因为页按行而非按字节切。
140
- - **版本先于内容**——页上的 `version` 来自流之前的 stat;两者之间落地的写入会让该页落后一个版本,下一帧 `changes` 会报告它。
143
+ - **读取不具备事务性**——结果元数据来自内容读取之前的 stat;并发写入可能使报告版本与返回内容不一致。
141
144
  - **generation 队列无界**——一个 `changes` generation 会缓冲每一条被包含的观察直到消费方 pull;停滞的消费方会在流的生命期内持续增长 Host 内存。
142
145
  - **`maxEntries` 限制的是答案,不是列举**——`list` 让 `ctx.fs.listDir` 列出全部子项后再截断数组,远超上限的目录仍让 Host 付出整个列举的代价(`fs-local` 上每个子项一次 stat);要限制这份工作,需要文件系统 seam 的 `listDir` 支持上限。
143
- - **失效流保留元数据**——Host 结束 `changes` 或流终态失败后,已打开的值保持最后已知状态,直到重新打开;刷新不会重开流。
144
- - **刷新按路径共享**——同一会话中,一次刷新会重新 stat 此绝对路径的全部跟随者并清除其 `changed` 标记,包括没有重读内容的其它读者。按记录投递刷新仍是延期工作。
146
+ - **失效流保留元数据**——Host 结束 `changes` 或流终态失败后,已打开的值保持最后已知状态,直到重新打开。
145
147
 
146
148
  <a id="dev-note"></a>
147
149
  ### 开发备注
package/lib/client.js CHANGED
@@ -16,7 +16,6 @@ window.__ModuleLoader__.load({
16
16
  }
17
17
  /** Notices of one follower, delivered in order and pulled by its consumer. */
18
18
  var Follower = class {
19
- address;
20
19
  leave;
21
20
  pending = [];
22
21
  started = Promise.withResolvers();
@@ -29,17 +28,11 @@ window.__ModuleLoader__.load({
29
28
  */
30
29
  ready = this.started.promise;
31
30
  /**
32
- * @param address - resource address used for reload lookup.
33
31
  * @param leave - unregisters this follower and its abort listener.
34
32
  */
35
- constructor(address, leave) {
36
- this.address = address;
33
+ constructor(leave) {
37
34
  this.leave = leave;
38
35
  }
39
- /** The normalized Host path, absent until a successful stat. */
40
- get key() {
41
- return this.hostKey;
42
- }
43
36
  /**
44
37
  * Select the Host path for queued and future changes.
45
38
  * @param absolutePath - the successful stat's absolute path.
@@ -54,7 +47,7 @@ window.__ModuleLoader__.load({
54
47
  /**
55
48
  * Queue one notice.
56
49
  * @param notice - what the consumer receives next.
57
- * @param key - normalized Host path for a change; absent for a reload.
50
+ * @param key - normalized Host path for the change.
58
51
  */
59
52
  push(notice, key) {
60
53
  this.pending.push({
@@ -79,7 +72,7 @@ window.__ModuleLoader__.load({
79
72
  while (true) {
80
73
  const next = this.pending.shift();
81
74
  if (next !== void 0) {
82
- if (next.key === void 0 || this.hostKey === void 0 || next.key === this.hostKey) yield next.notice;
75
+ if (this.hostKey === void 0 || next.key === this.hostKey) yield next.notice;
83
76
  continue;
84
77
  }
85
78
  if (this.ended) return;
@@ -134,15 +127,6 @@ window.__ModuleLoader__.load({
134
127
  this.followers.delete(follower);
135
128
  if (this.followers.size === 0) this.close();
136
129
  }
137
- /**
138
- * Reload an address and every follower bound to the same Host path.
139
- * @param address - the resource address requesting a reload.
140
- */
141
- requestRestat(address) {
142
- const keys = /* @__PURE__ */ new Set();
143
- for (const follower of this.followers) if (follower.address === address && follower.key !== void 0) keys.add(follower.key);
144
- for (const follower of this.followers) if (follower.address === address || follower.key !== void 0 && keys.has(follower.key)) follower.push({ kind: "restat" });
145
- }
146
130
  async pump() {
147
131
  try {
148
132
  for await (const item of this.stream) {
@@ -217,7 +201,7 @@ window.__ModuleLoader__.load({
217
201
  this.remote = remote;
218
202
  }
219
203
  /**
220
- * Follow one resource address in one session before its Host path is known.
204
+ * Follow one resource in one session before its Host path is known.
221
205
  *
222
206
  * The follower is registered on call, not on first pull. Changes delivered
223
207
  * to this Client are queued while stat is pending. The first follower starts
@@ -229,18 +213,17 @@ window.__ModuleLoader__.load({
229
213
  * any session write can trigger a retry; after binding, only matching queued
230
214
  * and live changes pass.
231
215
  * @param sessionId - the session whose workspace holds the file.
232
- * @param address - the resource address, used only for reload lookup.
233
216
  * @param signal - ends the follow.
234
217
  * @returns a single-consumer subscription with Host-path binding and explicit disposal.
235
218
  */
236
- follow(sessionId, address, signal) {
219
+ follow(sessionId, signal) {
237
220
  const feed = signal.aborted ? void 0 : this.feedOf(sessionId);
238
221
  const leave = () => {
239
222
  signal.removeEventListener("abort", leave);
240
223
  follower.end();
241
224
  feed?.remove(follower);
242
225
  };
243
- const follower = new Follower(address, leave);
226
+ const follower = new Follower(leave);
244
227
  if (feed === void 0) follower.end();
245
228
  else {
246
229
  feed.add(follower);
@@ -249,14 +232,6 @@ window.__ModuleLoader__.load({
249
232
  return follower;
250
233
  }
251
234
  /**
252
- * Ask an address and its same-session Host-path peers to `stat` again.
253
- * @param sessionId - the session whose workspace holds the file.
254
- * @param address - the resource address requesting a reload.
255
- */
256
- requestRestat(sessionId, address) {
257
- this.sessions.get(sessionId)?.requestRestat(address);
258
- }
259
- /**
260
235
  * Wait for every stream that is still closing, so an owner tearing down
261
236
  * leaves no Host stream behind.
262
237
  * @returns resolves once no stream of this feed is closing.
@@ -308,20 +283,29 @@ window.__ModuleLoader__.load({
308
283
  };
309
284
  //#endregion
310
285
  //#region ../../util/workspace-path/lib/index.js
286
+ /**
287
+ * The `dsh-resource://file/…` address grammar: how a file is named across the
288
+ * Sidebar and the resource model, built and parsed without touching a
289
+ * filesystem.
290
+ * @module
291
+ */
292
+ /** The scheme and type every file address opens with. */
293
+ const FILE_ADDRESS_PREFIX = "dsh-resource://file/";
311
294
  /** Whether a decoded first path segment is a Windows drive (`C:`). */
312
295
  function isDriveSegment(segment) {
313
296
  return segment !== void 0 && /^[A-Za-z]:$/.test(segment);
314
297
  }
315
298
  /**
316
- * Read a file address back into its parts.
299
+ * Read a file address back into its parts without resolving `.` or `..`.
300
+ * Query and fragment suffixes are ignored; encoded path segments are decoded.
317
301
  * @param address - a candidate address.
318
302
  * @returns the parts, or `undefined` when the string is not a `dsh-resource://file/` URI in a known scope with a path, or a segment is not validly encoded.
319
303
  */
320
304
  function parseFileAddress(address) {
321
305
  try {
322
- const url = new URL(address);
323
- if (url.protocol !== "dsh-resource:" || url.host !== "file") return void 0;
324
- const [, scope, ...rest] = url.pathname.split("/");
306
+ if (!address.startsWith(FILE_ADDRESS_PREFIX)) return void 0;
307
+ const end = address.search(/[?#]/);
308
+ const [scope, ...rest] = address.slice(20, end === -1 ? void 0 : end).split("/");
325
309
  if (scope === "session") {
326
310
  const [id, ...segments] = rest;
327
311
  if (id === void 0 || id === "" || segments.length === 0) return void 0;
@@ -352,23 +336,22 @@ window.__ModuleLoader__.load({
352
336
  //#endregion
353
337
  //#region lib/types/client/provider.js
354
338
  /**
355
- * Build the `file` provider over one Remote face, one change feed, and the Client's Session list.
339
+ * Build the `file` provider over one Remote face and one change feed.
356
340
  * @param remote - the Remote face carrying `workspaceFiles.stat`.
357
341
  * @param changes - the per-session change fan-out.
358
- * @param sessions - the current Session, read for absolute addresses on every open and reload.
359
342
  * @returns the provider to register into `ctx.resources`.
360
343
  */
361
- function createFileResourceProvider(remote, changes, sessions) {
344
+ function createFileResourceProvider(remote, changes) {
362
345
  return {
363
346
  protocol: "file",
364
347
  async *open(address, { signal }) {
365
- const resolved = resolve(address, sessions);
348
+ const resolved = resolve(address);
366
349
  if (!resolved.ok) {
367
350
  yield resolved;
368
351
  return;
369
352
  }
370
353
  const { sessionId, path } = resolved.value;
371
- const notices = changes.follow(sessionId, address, signal);
354
+ const notices = changes.follow(sessionId, signal);
372
355
  const stat = () => remote.workspaceFiles.stat(sessionId, path, signal);
373
356
  const aborted = () => signal.aborted;
374
357
  let current;
@@ -378,7 +361,7 @@ window.__ModuleLoader__.load({
378
361
  if (aborted()) return;
379
362
  if (first.ok) {
380
363
  notices.bind(first.value.absolutePath);
381
- current = metadataOf(first.value, false);
364
+ current = first.value;
382
365
  yield {
383
366
  ok: true,
384
367
  value: current
@@ -391,8 +374,7 @@ window.__ModuleLoader__.load({
391
374
  if (notice.version === current.version) continue;
392
375
  current = {
393
376
  ...current,
394
- version: notice.version,
395
- changed: true
377
+ version: notice.version
396
378
  };
397
379
  yield {
398
380
  ok: true,
@@ -408,7 +390,7 @@ window.__ModuleLoader__.load({
408
390
  continue;
409
391
  }
410
392
  notices.bind(again.value.absolutePath);
411
- current = metadataOf(again.value, notice.kind !== "restat");
393
+ current = again.value;
412
394
  yield {
413
395
  ok: true,
414
396
  value: current
@@ -417,20 +399,15 @@ window.__ModuleLoader__.load({
417
399
  } finally {
418
400
  notices.dispose();
419
401
  }
420
- },
421
- reload(address) {
422
- const resolved = resolve(address, sessions);
423
- if (resolved.ok) changes.requestRestat(resolved.value.sessionId, address);
424
402
  }
425
403
  };
426
404
  }
427
405
  /**
428
406
  * Resolve one address to the Host call it stands for, or to the failure frame it earns.
429
407
  * @param address - the full address, scheme included.
430
- * @param sessions - the Client's Session list.
431
408
  * @returns the Host file, or the `unsupported-address` / `unknown-workspace` failure.
432
409
  */
433
- function resolve(address, sessions) {
410
+ function resolve(address) {
434
411
  const parsed = parseFileAddress(address);
435
412
  if (parsed === void 0) return {
436
413
  ok: false,
@@ -443,18 +420,10 @@ window.__ModuleLoader__.load({
443
420
  path: parsed.path
444
421
  }
445
422
  };
446
- const sessionId = sessions.current();
447
- if (sessionId === void 0) return {
423
+ return {
448
424
  ok: false,
449
425
  error: unknownWorkspace(address)
450
426
  };
451
- return {
452
- ok: true,
453
- value: {
454
- sessionId,
455
- path: parsed.path
456
- }
457
- };
458
427
  }
459
428
  /**
460
429
  * The failure frame's error for an address this provider does not serve.
@@ -465,45 +434,28 @@ window.__ModuleLoader__.load({
465
434
  return new RemoteError("workspace-file/unsupported-address", `${address} is not a dsh-resource://file/session/<sessionId>/<path> or dsh-resource://file/absolute/<path> address`, { address });
466
435
  }
467
436
  /**
468
- * The failure frame's error for an absolute address with no current Session.
437
+ * The failure frame's error for an absolute address with no Session.
469
438
  * @param address - the offending address.
470
439
  * @returns the typed error.
471
440
  */
472
441
  function unknownWorkspace(address) {
473
- return new RemoteError("workspace-file/unknown-workspace", `${address} requires a current Session`, { address });
474
- }
475
- /**
476
- * The resource value one `stat` result amounts to.
477
- * @param stat - what the Host reported.
478
- * @param changed - whether the consumer's content may be stale: `true` after a
479
- * Host notice prompted the stat, `false` for the opening stat and a reload's.
480
- * @returns the metadata frame value.
481
- */
482
- function metadataOf(stat, changed) {
483
- return {
484
- absolutePath: stat.absolutePath,
485
- version: stat.version,
486
- changed,
487
- ...stat.bytes === void 0 ? {} : { bytes: stat.bytes }
488
- };
442
+ return new RemoteError("workspace-file/unknown-workspace", `${address} requires a dsh-resource://file/session/<sessionId>/<path> address`, { address });
489
443
  }
490
444
  //#endregion
491
445
  //#region lib/types/client/index.js
492
- /** Required browser services: the resource model, the Remote carrier and its namespace, and the Session list. */
446
+ /** Required browser services: the resource model, the Remote carrier and its namespace. */
493
447
  const inject = [
494
448
  "resources",
495
449
  "remote",
496
- "remote.workspaceFiles",
497
- "sessions"
450
+ "remote.workspaceFiles"
498
451
  ];
499
452
  /**
500
453
  * Client plugin body: register the `file` provider for this plugin's lifetime.
501
- * @param ctx - client root context carrying `resources`, the Remote face, and `sessions`.
454
+ * @param ctx - client root context carrying `resources` and the Remote face.
502
455
  */
503
456
  function apply(ctx) {
504
- const sessions = { current: () => ctx.sessions.list.getSnapshot().current };
505
457
  const changes = new ChangeFeed(ctx.remote);
506
- const provider = createFileResourceProvider(ctx.remote, changes, sessions);
458
+ const provider = createFileResourceProvider(ctx.remote, changes);
507
459
  ctx.effect(() => {
508
460
  const release = ctx.resources.register(provider);
509
461
  return async () => {