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

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/api/workspace-files/README.md
5
+ README.md: ec15f52bf17fffca2b225fa427a7405922aaf2b3
6
+ README.zh.md: 7e45d5c9a6073435e26e1237791a328db654d839
package/README.md ADDED
@@ -0,0 +1,154 @@
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."
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-api-workspace-files
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
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.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
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.
29
+
30
+ | Method | Returns | Purpose |
31
+ |---|---|---|
32
+ | `stat(path)` | `WorkspaceFileStat { absolutePath, version, bytes? }` | Identity, version, and size of one regular file, without content |
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
+ | `readBytes(path, { offset?, length? })` | `WorkspaceFileBytes` = stat + `{ offset, data, eof }` | One window of raw bytes from any regular file, base64-encoded |
35
+ | `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 |
37
+
38
+ ### Addressing and paths
39
+
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 `/`.
41
+
42
+ ### Pages
43
+
44
+ `read` returns one line window, never the whole file. `range.offset` is the 1-based first line and defaults to 1; `range.limit` is the largest number of lines on the page and defaults to `maxLines`, which it may not exceed — a larger limit, or an offset or limit that is not a positive integer, is a `gateway/bad-request`. Lines end at `\n`, and a final `\n` terminates the last line rather than starting an empty one, so a two-line file has two lines. The page's `text` joins its lines with `\n` and carries no terminator after the last; `eof` is true when the page includes the file's last line, and an offset past the end returns an empty page with `eof` true. Every page also carries the file's `version` from the stat that preceded it, so a consumer can tell a fresh page from a stale one, and `bytes`, the complete file's size when the backend reports it. The service reads the file only up to the first character past the page, so a very large file costs one page of memory per request.
45
+
46
+ ### Byte windows
47
+
48
+ `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
+
50
+ ### The four gates
51
+
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`.
53
+
54
+ ### The change feed
55
+
56
+ `changes` is a `stream` Remote. A generation registers its observation queue and resolves the Session workspace root before yielding `{ kind: 'ready' }`. It then yields `{ kind: 'change', change }`, where `change` is `{ absolutePath, version }` for a present file or `{ absolutePath, absent: true }` for one observed gone. The source is `fs/observed`, filtered to targets inside that root; the operating system is not watched. Observations after the generation's first pull are queued, including while the root resolves. The generation ends on cancellation or plugin disposal.
57
+
58
+ ### Configuration
59
+
60
+ | Field | Default | Meaning |
61
+ |---|---|---|
62
+ | `maxBytes` | `2097152` (2 MiB) | Inclusive byte cap on one page's text and on one byte window; a larger page or window fails |
63
+ | `maxLines` | `5000` | Default and largest page size in lines; a larger `limit` is refused |
64
+ | `maxEntries` | `2000` | Cap on returned directory entries; the rest is dropped and reported cut |
65
+
66
+ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-api-workspace-files) is the exhaustive source for every accepted field and its JSDoc.
67
+
68
+ ### Failures
69
+
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.
71
+
72
+ ### Client file resources
73
+
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.
75
+
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.
77
+
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.
79
+
80
+ 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
+
82
+ -----
83
+
84
+ <a id="understand-the-implementation"></a>
85
+ ## Understand the implementation
86
+
87
+ <details>
88
+ <summary>Implementation internals — click to expand</summary>
89
+
90
+ ### Design concept
91
+
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.
93
+
94
+ ### Source map
95
+
96
+ | File | Role |
97
+ |---|---|
98
+ | [`src/index.ts`](src/index.ts) | `WorkspaceFiles`: the `workspaceFiles` service and Remote namespace, `Config`, the gates, the page cutter, `read`, `readBytes`, `stat`, `list` |
99
+ | [`src/changes.ts`](src/changes.ts) | `WorkspaceChangeFeed`: `fs/observed` subscription and one queue per open `changes` generation |
100
+ | [`src/types.ts`](src/types.ts) | Wire types and the `RemoteErrorDetailsMap` codes, published as `./types` for Client packages |
101
+ | [`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 |
102
+ | [`src/client/types.ts`](src/client/types.ts), [`remote.ts`](src/client/remote.ts) | Resource values, parameters, Client error codes, and generated Remote types |
103
+ | — | No runtime invariant companion is published; every Host answer is derived from `ctx.fs` and the sandbox policy at call time. |
104
+
105
+ Typert generates the Host and Client Remote artifacts exposed by `./typert` and `./remote`.
106
+
107
+ </details>
108
+
109
+ -----
110
+
111
+ <a id="further-exploration"></a>
112
+ ## Further Exploration
113
+
114
+ - [Filesystem capability](../../fs/fs/README.md) — the `ctx.fs` contract this service reads through, including `fs/observed` and `readByteRange`.
115
+ - [Sandbox policy](../../sandbox/sandbox-policy/README.md) — where the Session's workspace root comes from.
116
+ - [Remote assembly](../../api/remotes/README.md) — how Client packages reach the `workspaceFiles` namespace.
117
+ - [Client resources](../../client/resources/README.md) — the resource model, `useResource`, pins, and provider lifetime.
118
+ - [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.
120
+
121
+ -----
122
+
123
+ <a id="model-experience"></a>
124
+ ## Model Experience
125
+
126
+ None, as this package registers no tool, contributes no prompt section, and appends no session event.
127
+
128
+ #### KV Cache effect
129
+
130
+ None; this package neither assembles nor sends a provider request.
131
+
132
+ ## Known Limitations and Deferred Work
133
+
134
+ <a id="known-limitations-and-deferred-work"></a>
135
+
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.
138
+ - **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
+ - **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.
141
+ - **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
+ - **`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.
145
+
146
+ <a id="dev-note"></a>
147
+ ### Dev Note
148
+
149
+ <details>
150
+ <summary>Working context for maintainers — click to expand</summary>
151
+
152
+ None.
153
+
154
+ </details>
package/README.zh.md ADDED
@@ -0,0 +1,154 @@
1
+ ---
2
+ description: "面向 Web GUI 的工作区文件服务:在 Session 工作区根内做分页读取、字节窗口、stat、目录列举与 Agent 写入变更流,以 workspaceFiles Remote 命名空间暴露。"
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-api-workspace-files
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ 使用本包可从 Web Client 浏览和检查 Session 工作区内的文件。它按行分页读取 UTF-8 文本、按有界窗口读取原始字节、报告文件版本与大小、列举目录的直接子项,并流式推送 Agent 文件操作造成的变更。每项操作都限定在为被寻址 Session 选择的工作区根内,不受文件系统后端工作目录影响。Client 组件还可经共享 Remote API 跟随实时文件元数据并构建 Sidebar 文件树。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
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)`,从不自己指定根。
29
+
30
+ | 方法 | 返回 | 用途 |
31
+ |---|---|---|
32
+ | `stat(path)` | `WorkspaceFileStat { absolutePath, version, bytes? }` | 一个普通文件的身份、版本与大小,不含内容 |
33
+ | `read(path, { offset?, limit? })` | `WorkspaceFileText` = stat + `{ offset, text, lines, eof }` | UTF-8 文本文件的一个行窗口;`lines` 计行数,使单个空行与越过文件末尾的页可区分 |
34
+ | `readBytes(path, { offset?, length? })` | `WorkspaceFileBytes` = stat + `{ offset, data, eof }` | 任意普通文件的一个原始字节窗口,base64 编码 |
35
+ | `list(path)` | `WorkspaceDirectoryListing { path, entries, truncated }` | 一个目录的直接子项 |
36
+ | `changes()` | `WorkspaceFileWatchFrame` 流 | 订阅就绪确认,随后为工作区根内的 Agent 观察 |
37
+
38
+ ### 寻址与路径
39
+
40
+ `read`、`stat` 与 `list` 接受工作区路径,可以是绝对路径,也可以是相对于 Session 工作区根的路径。离开服务的路径词汇有两套,每个方法只用其中一套:`read`、`stat` 与 `changes` 以文件系统执行环境中的绝对路径报告文件,符号链接已解析(`WorkspaceFileStat.absolutePath`、`WorkspaceFileChange.absolutePath`),因为其消费方是 Client 资源系统,它按这条路径跟随变更;`list` 以相对于根的工作区路径报告被列举目录——根自身为空串——因为其消费方是一棵以根为起点的树,子项路径就是该值与条目名以 `/` 连接。
41
+
42
+ ### 分页
43
+
44
+ `read` 返回一个行窗口,绝不返回整个文件。`range.offset` 是 1 起算的首行,缺省为 1;`range.limit` 是该页最多的行数,缺省为 `maxLines` 且不得超过它——更大的 limit,或不是正整数的 offset / limit,都是 `gateway/bad-request`。行以 `\n` 结束,末尾的 `\n` 是最后一行的终止符而不是再起一空行,所以两行文件就是两行。页的 `text` 以 `\n` 连接各行,最后一行之后不带终止符;`eof` 在该页含文件最后一行时为 true,offset 越过末尾则返回空页且 `eof` 为 true。每页还带上前置 stat 得到的文件 `version`,消费方据此分辨新页与旧页,以及 `bytes`——后端能报告时的整文件大小。服务只把文件读到该页之后的第一个字符为止,所以再大的文件每次请求也只占一页内存。
45
+
46
+ ### 字节窗口
47
+
48
+ `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
+
50
+ ### 四道关
51
+
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`。
53
+
54
+ ### 变更流
55
+
56
+ `changes` 是 `stream` 模式的 Remote。一代流注册观察队列并解析 Session 工作区根之后,才产出 `{ kind: 'ready' }`。随后产出 `{ kind: 'change', change }`,其中 `change` 对存在的文件为 `{ absolutePath, version }`,对被观察到已消失的文件为 `{ absolutePath, absent: true }`。来源是按该根内目标过滤的 `fs/observed`;操作系统并未被监视。一代流首次拉取后的观察都会排队,包括解析根期间的观察。流在取消或插件释放时结束。
57
+
58
+ ### 配置
59
+
60
+ | 字段 | 默认值 | 含义 |
61
+ |---|---|---|
62
+ | `maxBytes` | `2097152`(2 MiB) | 单页文本与单个字节窗口的字节上限(含);更大的页或窗口失败 |
63
+ | `maxLines` | `5000` | 页大小的缺省值与上限(行);更大的 `limit` 被拒绝 |
64
+ | `maxEntries` | `2000` | 返回目录条目数上限;其余丢弃并报告截断 |
65
+
66
+ 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-api-workspace-files)是每个可接受字段及其 JSDoc 的完备来源。
67
+
68
+ ### 失败
69
+
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`)。调用方按代码分支,绝不按消息文本。
71
+
72
+ ### Client 文件资源
73
+
74
+ 浏览器导出向 `ctx.resources` 注册 `file` 提供者,要求 `resources`、`remote`、`remote.workspaceFiles` 和 `sessions` 在场。bundle 中单个 `workspace-files` 条目供应两面;Client 没有单独配置。组件经标准 prop `useResource<'file'>(address)` 跟随文件,读取 `{ absolutePath, version, bytes?, changed }`;内容通过分页方法另行获取。
75
+
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 失败会结束流,并使刷新无动作。
77
+
78
+ 提供者等到 Host 的 `ready` 帧后才发首次 `stat`,读取期间将变更排队,随后将跟随者绑定到 `stat.absolutePath`。排队与实时变更都按该 Host 返回路径匹配。新的写入版本置 `changed`,并保留最近的字节大小;重复版本被忽略。消失通知或刷新会重新 stat 文件。stat 失败后仍跟随地址,后续写入或刷新可使其恢复;首次成功绑定路径前,Session 内任何写入都可触发重试。刷新清除 `changed`,由 Host 触发的重新 stat 保留标记。帧是 `RemoteResult` 值,编程异常不被捕获。
79
+
80
+ 每个 Session 的所有被跟随文件共用一条受监督的 `changes` 流。跟随者按反斜杠归一为斜杠的绝对路径匹配。载体掉线由 Gateway 监督器重连;Host 结束或终态失败的流会结束其跟随者,最后的元数据仍可读取,直到重新打开。最后一个跟随者离开时释放流,后继流等待该释放完成,插件拆除等待所有在途关闭。提供者声明 `ResourceProtocolMap.file`;文本预览声明其 Sidebar 行号导航参数。
81
+
82
+ -----
83
+
84
+ <a id="understand-the-implementation"></a>
85
+ ## 理解实现
86
+
87
+ <details>
88
+ <summary>实现内幕——点击展开</summary>
89
+
90
+ ### 设计概念
91
+
92
+ 经 `ctx.fs` 的读取是有意不加限制的——沙箱后端只围栏写与编辑——所以这里的每条约束都是服务自己的。页从 `streamText` 切出,后者逐块解码并拒绝非 UTF-8:切页器对窗口之前的行只计数不保留,对窗口内的每个片段先按字节上限验收再缓冲,并在窗口之后的第一个字符处返回,所以无论多大的文件或多长的单行都不会在内存里超过一页;随后在该页上做 NUL 扫描。流之前的一次 `stat` 给出页所报告的版本与大小。路径关有意先于包含判定:`lstat` 面向路径、看得见链接,而 `resolve` 会跟随它;代价是根外条目会先报告自己的类型再报告位置。
93
+
94
+ ### 源码地图
95
+
96
+ | 文件 | 职责 |
97
+ |---|---|
98
+ | [`src/index.ts`](src/index.ts) | `WorkspaceFiles`:`workspaceFiles` 服务与 Remote 命名空间、`Config`、四道关、切页器、`read`、`readBytes`、`stat`、`list` |
99
+ | [`src/changes.ts`](src/changes.ts) | `WorkspaceChangeFeed`:`fs/observed` 订阅与每个打开的 `changes` generation 各一条队列 |
100
+ | [`src/types.ts`](src/types.ts) | 线路类型与 `RemoteErrorDetailsMap` 错误码,以 `./types` 发布给 Client 包 |
101
+ | [`src/client/index.ts`](src/client/index.ts)、[`provider.ts`](src/client/provider.ts)、[`change-feed.ts`](src/client/change-feed.ts) | 浏览器插件、文件元数据与每 Session 变更流 |
102
+ | [`src/client/types.ts`](src/client/types.ts)、[`remote.ts`](src/client/remote.ts) | 资源值、参数、Client 错误码与生成的 Remote 类型 |
103
+ | — | 不发布运行时 invariant 伴生件;每个 Host 答案都在调用时由 `ctx.fs` 与沙箱策略推导。 |
104
+
105
+ Typert 生成 `./typert` 与 `./remote` 暴露的 Host 与 Client Remote 产物。
106
+
107
+ </details>
108
+
109
+ -----
110
+
111
+ <a id="further-exploration"></a>
112
+ ## 进一步探索
113
+
114
+ - [文件系统能力](../../fs/fs/README.zh.md)——本服务经由读取的 `ctx.fs` 契约,含 `fs/observed` 与 `readByteRange`。
115
+ - [沙箱策略](../../sandbox/sandbox-policy/README.zh.md)——Session 工作区根的来源。
116
+ - [Remote 装配](../../api/remotes/README.zh.md)——Client 包如何触达 `workspaceFiles` 命名空间。
117
+ - [Client 资源](../../client/resources/README.zh.md)——资源模型、`useResource`、pin 与提供者生命周期。
118
+ - [工作区路径辅助](../../util/workspace-path/README.zh.md)——`fileAddressFor` 与 `parseFileAddress`,两端共享的 `dsh-resource://file/…` 地址语法。
119
+ - [Sidebar 文本预览](../../client/ui-sidebar-textpreview/README.zh.md)——经 `file` 提供者跟随文件并读取其页的 tab 类型。
120
+
121
+ -----
122
+
123
+ <a id="model-experience"></a>
124
+ ## 模型体验
125
+
126
+ 无,本包不注册任何工具、不贡献提示词章节、不追加任何会话事件。
127
+
128
+ #### KV Cache 影响
129
+
130
+ 无;本包既不装配也不发送提供方请求。
131
+
132
+ ## 已知限制与延期工作
133
+
134
+ <a id="known-limitations-and-deferred-work"></a>
135
+
136
+ - **仅覆盖 Agent 写入**——`changes` 转发 `fs/observed` 的发射;子进程、shell 命令或用户编辑器改动的文件不产生任何帧。
137
+ - **类型先于位置**——根外条目若类型本身就不合格,报告的是 `not-regular-file` 或 `not-directory` 而非 `outside-workspace`,因为路径关先于包含判定。
138
+ - **没有总行数**——页只报告 `eof`,不报告后面还有多少行;需要总数的消费方要翻到末尾或按 `bytes` 估算。
139
+ - **超长单行没有页**——超过 `maxBytes` 的单行在包含它的每个窗口都以 `too-large` 失败,因为页按行而非按字节切。
140
+ - **版本先于内容**——页上的 `version` 来自流之前的 stat;两者之间落地的写入会让该页落后一个版本,下一帧 `changes` 会报告它。
141
+ - **generation 队列无界**——一个 `changes` generation 会缓冲每一条被包含的观察直到消费方 pull;停滞的消费方会在流的生命期内持续增长 Host 内存。
142
+ - **`maxEntries` 限制的是答案,不是列举**——`list` 让 `ctx.fs.listDir` 列出全部子项后再截断数组,远超上限的目录仍让 Host 付出整个列举的代价(`fs-local` 上每个子项一次 stat);要限制这份工作,需要文件系统 seam 的 `listDir` 支持上限。
143
+ - **失效流保留元数据**——Host 结束 `changes` 或流终态失败后,已打开的值保持最后已知状态,直到重新打开;刷新不会重开流。
144
+ - **刷新按路径共享**——同一会话中,一次刷新会重新 stat 此绝对路径的全部跟随者并清除其 `changed` 标记,包括没有重读内容的其它读者。按记录投递刷新仍是延期工作。
145
+
146
+ <a id="dev-note"></a>
147
+ ### 开发备注
148
+
149
+ <details>
150
+ <summary>维护者工作上下文——点击展开</summary>
151
+
152
+ 无。
153
+
154
+ </details>