dsh-remote-file-system 0.1.2 → 0.2.0

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.md CHANGED
@@ -1,30 +1,50 @@
1
- # dsh-remote-file-system
1
+ # DSH·远程文件系统
2
2
 
3
- 模型可见的远程文件工具 read_remote、write_remote、edit_remote,经 ssh 读写远程主机文件。标准可安装的 dsh host 插件。
3
+ 模型可见的远程文件工具 `read_remote`、`write_remote`、`edit_remote`,经 ssh 读写远程主机文件;并在 Web 右侧栏以 `remote` tab 预览远程文本。标准可安装的 dsh host 与 client 插件。
4
4
 
5
5
  ## 引言
6
6
 
7
7
  DSH 只有本地文件系统的读写能力,而读写远程文件只能用 PowerShell 再套 bash 的 ssh 命令,非常容易出错。这个插件提供了三条可在模型中直接调用的远程文件工具,读写远程文件无需再套 ssh 命令。并将兼容 [dsh-edit-diff](https://github.com/better-er/dsh-edit-diff) 和 [dsh-write-rule-guard](https://github.com/better-er/dsh-write-rule-guard) 的优化显示与规则检查。
8
8
 
9
+ ## 与本地三件的关系
10
+
11
+ 前面三件只管本地。一旦要改别的机器上的文件,差异显示与内容规则一条都不在场,本地也没有远端文件的旧快照,比不出改了什么,只能套着 ssh 拼命令盯着回显猜。本插件把这套显示与规则一起带过去。
12
+
9
13
  ## 能力
10
14
 
11
15
  | 工具 | 作用 | 关键语义 |
12
16
  | --- | --- | --- |
13
- | read_remote | 读远端 UTF-8 文本,行号分页 | offset、limit、totalLines,与原生 read 一致 |
14
- | write_remote | 远端新建文件 | 只允许新建,目标已存在即拒绝,绝无覆写分支 |
15
- | edit_remote | 远端字面替换 | 唯一匹配,多现拒绝,未命中报错;支持 replace_all |
17
+ | `read_remote` | 读远端 UTF-8 文本,行号分页 | offset、limit、totalLines,与原生 read 一致 |
18
+ | `write_remote` | 远端新建文件 | 只允许新建,目标已存在即拒绝,绝无覆写分支 |
19
+ | `edit_remote` | 远端字面替换 | 唯一匹配,多现拒绝,未命中报错;支持 replace_all |
20
+
21
+ ### 右侧栏预览
22
+
23
+ 内置查看器只认主机本地文件:点开 `wsl:~/x.md` 时地址是 `dsh-resource://file/session/<id>/wsl:~/x.md`,内置 `text` 类型经 `ctx.fs` 在本地查找而失败。
24
+
25
+ client 半身注册 `remote` tab 类型,以 extension 优先级认领这类能解析出远程别名的地址,经 Host 自注册的 `/remote-file-system` 通道读取远端文本。外观与内置文档预览一致:38px 路径栏、同一组换行与重新加载图标按钮;代码文件交给共享的 `CodeBlock` 渲染,可切换代码与纯文本,其他文件按源码行渲染。支持 `params.line` 定位与手动刷新。本地文件仍由内置查看器处理。
26
+
27
+ `read_remote` 的工具卡按工具名接管,卡内路径是可点击链接,点击即在右侧栏打开对应的 remote tab;点整行可展开结果文本。内置 read 卡只按工具名 `read` 注册,`read_remote` 没有条目而走通用行,也就没有打开文件的入口,故由本插件补上。
16
28
 
17
29
  ## file_path 写法
18
30
 
19
- ssh 目标与远程路径合在一个 file_path 字段,沿用「别名:路径」前缀,别名取自 ~/.ssh/config:
31
+ ssh 目标与远程路径合在一个 `file_path` 字段,沿用「别名:路径」前缀,别名取自 ~/.ssh/config:
20
32
 
21
- ```
33
+ ```text
22
34
  wsl:~/x.md
23
35
  myserver:/etc/hosts
24
36
  ```
25
37
 
26
38
  只收绝对路径或 ~/ 开头;裸相对路径直接拒绝。
27
39
 
40
+ ## 宣传片
41
+
42
+ 四件一起讲的宣传片,中文与日语配音两版同在一个投稿里:
43
+
44
+ | 文件四件套宣传片 · 一个核心,三个辅助 |
45
+ | :---: |
46
+ | [![文件四件套宣传片](https://i2.hdslb.com/bfs/archive/8a1e21d2afe4c23f587ed11bd94ef9172ea5651e.jpg)](https://www.bilibili.com/video/BV1EUar6YEp3/) |
47
+
28
48
  ## 安装
29
49
 
30
50
  **从 GitHub 安装**:源码在 `src/`,`lib/` 不入仓库,安装时 npm 会触发 `prepare` 脚本现场构建。
@@ -33,7 +53,7 @@ myserver:/etc/hosts
33
53
  dsh plugin --profile web add github:better-er/dsh-remote-file-system
34
54
  ```
35
55
 
36
- **从 npm 安装**:包内已含构建产物 `lib/index.js`,安装时不再构建。
56
+ **从 npm 安装**:包内已含构建产物 `lib/index.js` 与 `lib/client.js`,安装时不再构建。
37
57
 
38
58
  ```powershell
39
59
  dsh plugin --profile web add dsh-remote-file-system
@@ -51,17 +71,24 @@ dsh plugin --profile web remove dsh-remote-file-system
51
71
 
52
72
  ## 后端
53
73
 
54
- 文件后端 RemoteFileSystem 刻意不继承 @deepseek-ai/dsh-fs 的 FileSystem 与 cordis Service,避免「继承即注册」污染会话 ctx.fs;只借用 dsh-fs 类型词表与 FsError。字节经 base64 再经 ssh 传输。
74
+ 文件后端 `RemoteFileSystem` 刻意不继承 `@deepseek-ai/dsh-fs` 的 `FileSystem` 与 cordis `Service`,避免「继承即注册」污染会话 `ctx.fs`;只借用 `dsh-fs` 类型词表与 `FsError`。字节经 base64 再经 ssh 传输。
75
+
76
+ 浏览器半身经 Host 自注册的 `/remote-file-system` 通道读取远端文本,复用 `connection.requestRejection` 做 Host 校验与浏览器鉴权,与 dsh-classic-coding 的 `src/rpc-channel.ts` 同源。
55
77
 
56
78
  ## 开发
57
79
 
58
- ```
80
+ ```powershell
59
81
  pnpm install # 装依赖,含 devDeps 类型与 esbuild
60
- pnpm run build # esbuild 打包 host 半身到 lib/
61
- pnpm run typecheck
62
- node smoke/verify.mjs # 手动组装最小 cordis 组合,对 wsl 冒烟三工具
82
+ pnpm build # esbuild 打包 host 与 client 半身到 lib/
83
+ pnpm typecheck
84
+ node smoke/verify.mjs # 手动组装最小 cordis 组合,对 wsl 冒烟三工具
85
+ node smoke/rpc-smoke.mjs # 对源码的 RPC 通道做信封级冒烟,不依赖 @deepseek-ai 包
63
86
  ```
64
87
 
65
88
  ## 依赖与构建注意
66
89
 
67
- @deepseek-ai/dsh-fs、dsh-tools、dsh-system-prompt 版本 0.1.2-rc.1,@deepseek-ai/cordis 版本 4.0.2。构建必须把这些包 external,与 host 共享单一 cordis 与 dsh-fs 实例;否则双原型会让 instanceof FsError 与 Service 标识错位。
90
+ `@deepseek-ai/dsh-fs`、`@deepseek-ai/dsh-tools`、`@deepseek-ai/dsh-system-prompt` 版本 `0.2.0-rc.2`,`@deepseek-ai/cordis` 版本 `4.0.2`。构建必须把这些包 external,与 host 共享单一 cordis 与 `dsh-fs` 实例;否则双原型会让 `instanceof FsError` 与 `Service` 标识错位。
91
+
92
+ ## License
93
+
94
+ [MIT](./LICENSE)
package/lib/client.js ADDED
@@ -0,0 +1,553 @@
1
+ window.__ModuleLoader__.load({ id: "dsh-remote-file-system", factory: (require) => { var module = { exports: {} }; var exports = module.exports;
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name2 in all)
11
+ __defProp(target, name2, { get: all[name2], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/client.ts
32
+ var client_exports = {};
33
+ __export(client_exports, {
34
+ apply: () => apply,
35
+ default: () => client_default,
36
+ inject: () => inject,
37
+ name: () => name
38
+ });
39
+ module.exports = __toCommonJS(client_exports);
40
+ var React = __toESM(require("react"), 1);
41
+ var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
42
+ var TYPE_ID = "dsh-remote-file-system";
43
+ var KIND = "remote";
44
+ var FILE_PREFIX = "dsh-resource://file/session/";
45
+ var ALIAS_RE = /^[A-Za-z0-9._-]+$/;
46
+ var SINGLE_LETTER_RE = /^[A-Za-z]$/;
47
+ var MAX_RENDER_LINES = 5e3;
48
+ function parseRemoteAddress(address) {
49
+ if (!address.startsWith(FILE_PREFIX)) return null;
50
+ const rest = address.slice(FILE_PREFIX.length);
51
+ const slash = rest.indexOf("/");
52
+ if (slash <= 0) return null;
53
+ let path;
54
+ try {
55
+ path = rest.slice(slash + 1).split("/").map(decodeURIComponent).join("/");
56
+ } catch {
57
+ return null;
58
+ }
59
+ const colon = path.indexOf(":");
60
+ if (colon <= 0) return null;
61
+ const alias = path.slice(0, colon);
62
+ const rawPath = path.slice(colon + 1);
63
+ if (!ALIAS_RE.test(alias) || SINGLE_LETTER_RE.test(alias)) return null;
64
+ if (!(rawPath.startsWith("/") || rawPath.startsWith("~/"))) return null;
65
+ return { address, alias, rawPath, target: alias + ":" + rawPath };
66
+ }
67
+ var rpcIdCounter = 0;
68
+ async function rpcCall(method, payload, signal) {
69
+ const rpcId = "rfs-" + String(++rpcIdCounter);
70
+ const body = JSON.stringify({
71
+ type: "client-request",
72
+ rpcId,
73
+ method,
74
+ payload
75
+ });
76
+ const res = await fetch("/remote-file-system/" + method, {
77
+ method: "POST",
78
+ headers: { "content-type": "application/json" },
79
+ body,
80
+ ...signal !== void 0 ? { signal } : {}
81
+ });
82
+ if (!res.ok) throw new Error("HTTP " + res.status);
83
+ const envelope = await res.json();
84
+ if (envelope.result && envelope.result.ok) return envelope.result.value;
85
+ const errMsg = envelope.result && envelope.result.error ? envelope.result.error.message : "RPC 调用失败";
86
+ throw new Error(errMsg);
87
+ }
88
+ var CSS = [
89
+ ".dsh-rfs-preview{flex-direction:column;flex:auto;height:100%;min-height:0;display:flex}",
90
+ ".dsh-rfs-header{box-sizing:border-box;border-bottom:.5px solid var(--dsw-alias-border-l3);flex:none;align-items:center;gap:4px;height:38px;padding:0 6px 0 16px;display:flex}",
91
+ ".dsh-rfs-path{margin-right:12px}",
92
+ ".dsh-rfs-titleIcon{flex:none}",
93
+ ".dsh-rfs-notice{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);border-bottom:.5px solid var(--dsw-alias-border-l1);flex:none;align-items:center;gap:10px;margin:0;padding:6px 10px;font-size:12px;display:flex}",
94
+ ".dsh-rfs-body{--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);min-height:0;color:var(--dsw-alias-label-primary);font-size:var(--dsh-content-font-size-secondary,13px);font-family:var(--dsw-font-mono,ui-monospace,monospace);white-space:pre;flex:auto;margin:0;padding:0;line-height:1.6;position:relative;overflow:auto}",
95
+ ".dsh-rfs-body::-webkit-scrollbar-track{margin:2px}",
96
+ ".dsh-rfs-body[data-code]{flex-direction:column;display:flex;overflow:hidden}",
97
+ ".dsh-rfs-wrap{white-space:pre-wrap;word-break:break-word}",
98
+ ".dsh-rfs-textDocument{box-sizing:border-box;min-width:100%;min-height:100%;padding:8px}",
99
+ ".dsh-rfs-page{font:var(--dsw-font-markdown-code-block);white-space:inherit;margin:0}",
100
+ ".dsh-rfs-line{padding:0 10px}",
101
+ ".dsh-rfs-lineTarget{background:var(--dsw-alias-interactive-bg-hover)}",
102
+ ".dsh-rfs-empty{box-sizing:border-box;height:100%;color:var(--dsw-alias-label-secondary);font-size:var(--dsh-content-font-size-secondary,13px);font-family:var(--dsw-font-family);text-align:center;white-space:normal;flex-direction:column;justify-content:center;align-items:center;gap:16px;padding:0 24px;line-height:1.6;display:flex}",
103
+ '.dsh-rfs-empty:after{content:"";flex:0 12%}',
104
+ ".dsh-rfs-emptyIcon{opacity:.6;filter:grayscale();flex:none}",
105
+ ".dsh-rfs-emptyLine{margin:0}",
106
+ ".dsh-rfs-retry{height:32px;color:var(--dsw-alias-label-primary);font-size:var(--dsh-content-font-size-secondary,13px);border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);cursor:pointer;background:0 0;flex:none;align-items:center;gap:6px;padding:0 14px 0 12px;font-family:inherit;display:inline-flex}",
107
+ ".dsh-rfs-retry:hover{background:var(--dsw-alias-interactive-bg-hover)}",
108
+ ".dsh-rfs-status{box-sizing:border-box;flex-direction:column;justify-content:center;align-items:center;gap:8px;height:100%;padding:12px 10px;display:flex}",
109
+ ".dsh-rfs-statusLine{color:var(--dsw-alias-label-secondary);font-size:var(--dsh-content-font-size-secondary,13px);white-space:normal;align-items:center;gap:10px;margin:0;padding:6px 10px;line-height:1.6;display:flex}",
110
+ ".dsh-rfs-tool{width:28px;height:28px;color:var(--dsw-alias-label-secondary);border-radius:var(--dsw-radius-sm);cursor:pointer;background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:6px;line-height:1;display:inline-flex}",
111
+ ".dsh-rfs-tool svg{width:15px;height:15px}",
112
+ ".dsh-rfs-tool:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}",
113
+ ".dsh-rfs-viewerTool{width:auto;max-width:160px;color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;flex:none;padding:0 6px;font-size:12px;overflow:hidden}",
114
+ ".dsh-rfs-code{white-space:normal;flex-direction:column;flex:auto;width:100%;min-width:0;height:100%;min-height:0;display:flex;overflow:hidden}",
115
+ ".dsh-rfs-code .dsh-rfs-codeblock{--dsl-code-block-border-radius:0px;--dsl-code-block-line-white-space:pre;--dsl-code-block-background:transparent;flex-direction:column;flex:auto;min-width:0;height:100%;min-height:0;margin:0;display:flex;position:static}",
116
+ ".dsh-rfs-code .dsh-rfs-codeblock>[data-code-block-content]{flex:auto;min-width:0;min-height:0;display:block;position:relative;overflow:auto}",
117
+ ".dsh-rfs-code .dsh-rfs-codeblock>[data-code-block-content]::-webkit-scrollbar-track{margin:2px}",
118
+ ".dsh-rfs-code .dsh-rfs-codeblock pre{box-sizing:border-box;white-space:pre;word-break:normal;overflow-wrap:normal;min-width:100%;padding:16px;overflow:visible}",
119
+ ".dsh-rfs-code[data-wrap=true] .dsh-rfs-codeblock{--dsl-code-block-line-white-space:pre-wrap}",
120
+ ".dsh-rfs-code[data-wrap=true] .dsh-rfs-codeblock pre{white-space:pre-wrap;overflow-wrap:anywhere}",
121
+ ".dsh-rfs-root{display:flex;flex-direction:column;width:100%;min-width:0}",
122
+ ".dsh-rfs-row{position:relative;overflow:hidden;display:flex;align-items:center;height:calc(24px + var(--dsh-content-font-delta,0px));min-width:0;color:var(--dsw-alias-label-tertiary);transition:color 100ms ease}",
123
+ ".dsh-rfs-row[data-expandable]{cursor:pointer}",
124
+ ".dsh-rfs-row:hover{color:var(--dsw-alias-label-secondary)}",
125
+ ".dsh-rfs-leading{position:relative;flex:none;width:calc(16px + var(--dsh-content-font-delta,0px));height:calc(16px + var(--dsh-content-font-delta,0px));display:inline-flex;align-items:center;justify-content:center;margin-right:6px;padding:0;border:none;background:none;color:inherit}",
126
+ ".dsh-rfs-leading svg:not([data-state]){width:calc(14px + var(--dsh-content-font-delta,0px));height:calc(14px + var(--dsh-content-font-delta,0px))}",
127
+ ".dsh-rfs-content{display:flex;align-items:center;flex:1 1 auto;min-width:0}",
128
+ ".dsh-rfs-rowtitle{flex:none;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:inherit}",
129
+ ".dsh-rfs-sep{flex:none;width:2px;height:2px;border-radius:1px;margin:0 8px;background:var(--dsw-alias-label-caption)}",
130
+ ".dsh-rfs-summary{flex:1 1 auto;min-width:0;display:flex;align-items:center;overflow:hidden;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px))}",
131
+ ".dsh-rfs-summarytext{flex:none;white-space:nowrap}",
132
+ ".dsh-rfs-summarylink{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin:0;padding:0;border:none;background:none;font:inherit;color:inherit;text-align:left;cursor:pointer}",
133
+ ".dsh-rfs-summarylink:hover{text-decoration:underline;text-underline-offset:3px}",
134
+ ".dsh-rfs-iocard{display:flex;flex-direction:column;margin:4px 0 4px 4px;border:0.5px solid var(--dsw-alias-border-l1);border-radius:var(--dsw-radius-lg);background:var(--dsw-alias-markdown-code-block);font:var(--dsw-font-markdown-code-block-small)}",
135
+ ".dsh-rfs-iosection{display:grid;grid-template-columns:max-content 1fr;column-gap:14px;align-items:baseline;padding:12px 16px;max-height:150px;overflow-y:auto}",
136
+ ".dsh-rfs-iolabel{position:sticky;top:0;align-self:start;color:var(--dsw-alias-label-caption)}",
137
+ ".dsh-rfs-iodivider{flex:none;height:0.5px;background:var(--dsw-alias-border-l2)}",
138
+ ".dsh-rfs-iotext{min-width:0;white-space:pre-wrap;word-break:break-word;color:var(--dsw-alias-label-secondary)}",
139
+ ".dsh-rfs-iotext[data-error]{color:var(--dsw-alias-state-error-primary)}"
140
+ ].join("\n");
141
+ var styleTag = null;
142
+ function ensureStyle() {
143
+ if (styleTag) return;
144
+ const tag = document.createElement("style");
145
+ tag.dataset.plugin = "dsh-remote-file-system";
146
+ tag.dataset.pluginCss = "dsh-remote-file-system/client.css";
147
+ tag.textContent = CSS;
148
+ document.head.appendChild(tag);
149
+ styleTag = tag;
150
+ }
151
+ function removeStyle() {
152
+ if (styleTag && styleTag.parentNode) {
153
+ styleTag.parentNode.removeChild(styleTag);
154
+ }
155
+ styleTag = null;
156
+ }
157
+ var DOCUMENT_LANGUAGES = /* @__PURE__ */ new Set(["markdown", "mdx", "rst", "latex", "bibtex", "asciidoc"]);
158
+ function codeLanguageOf(path) {
159
+ const language = (0, import_dsh_client_ui_primitives.languageForPath)(path);
160
+ if (language === void 0 || DOCUMENT_LANGUAGES.has(language)) return null;
161
+ return language;
162
+ }
163
+ function basenameOf(path) {
164
+ const name2 = path.slice(path.lastIndexOf("/") + 1);
165
+ return name2 === "" ? path : name2;
166
+ }
167
+ function RemoteFileBody(props) {
168
+ const info = props.useTabInfo();
169
+ const address = info.tab.navigation.address;
170
+ const params = info.tab.navigation.params;
171
+ const line = params && typeof params.line === "number" ? params.line : void 0;
172
+ const target = React.useMemo(function() {
173
+ return parseRemoteAddress(address);
174
+ }, [address]);
175
+ const language = React.useMemo(function() {
176
+ return target ? codeLanguageOf(target.rawPath) : null;
177
+ }, [target]);
178
+ const [phase, setPhase] = React.useState("loading");
179
+ const [text, setText] = React.useState("");
180
+ const [truncated, setTruncated] = React.useState(false);
181
+ const [message, setMessage] = React.useState("");
182
+ const [revision, setRevision] = React.useState(0);
183
+ const [wrap, setWrap] = React.useState(true);
184
+ const [plain, setPlain] = React.useState(false);
185
+ const [menuOpen, setMenuOpen] = React.useState(false);
186
+ const showCode = language !== null && !plain;
187
+ const bodyRef = React.useRef(null);
188
+ React.useEffect(function() {
189
+ if (!target) {
190
+ setPhase("error");
191
+ setMessage("无法识别的远程文件地址");
192
+ return;
193
+ }
194
+ let cancelled = false;
195
+ setPhase("loading");
196
+ setMessage("");
197
+ rpcCall("read", { target: target.target }, info.tab.signal).then(function(result) {
198
+ if (cancelled) return;
199
+ setText(result.text);
200
+ setTruncated(result.truncated);
201
+ setPhase("ready");
202
+ }).catch(function(e) {
203
+ if (cancelled) return;
204
+ setPhase("error");
205
+ setMessage(e instanceof Error ? e.message : String(e));
206
+ });
207
+ return function() {
208
+ cancelled = true;
209
+ };
210
+ }, [address, revision]);
211
+ React.useEffect(function() {
212
+ if (phase !== "ready" || line === void 0) return;
213
+ const body2 = bodyRef.current;
214
+ if (body2 === null) return;
215
+ const scroller = showCode ? body2.querySelector("[data-code-block-content]") : body2;
216
+ if (!(scroller instanceof HTMLElement)) return;
217
+ const row = showCode ? body2.querySelectorAll(".dsh-rfs-codeblock pre .line").item(line - 1) : body2.querySelector('[data-line="' + line + '"]');
218
+ if (row instanceof HTMLElement) scroller.scrollTop = Math.max(0, row.offsetTop);
219
+ }, [phase, text, line, showCode]);
220
+ const view = React.useMemo(function() {
221
+ if (text === "") return { rows: [], capped: false };
222
+ const parts = text.split("\n");
223
+ if (parts.length > 0 && parts[parts.length - 1] === "") parts.pop();
224
+ return { rows: parts.slice(0, MAX_RENDER_LINES), capped: parts.length > MAX_RENDER_LINES };
225
+ }, [text]);
226
+ function refresh() {
227
+ setRevision(function(r) {
228
+ return r + 1;
229
+ });
230
+ }
231
+ const notice = phase === "ready" && (truncated || view.capped) ? view.capped ? "仅显示前 " + MAX_RENDER_LINES + " 行" : "文件较大,已截断" : null;
232
+ const body = React.createElement(
233
+ "div",
234
+ {
235
+ ref: bodyRef,
236
+ className: wrap ? "dsh-rfs-body dsh-rfs-wrap" : "dsh-rfs-body",
237
+ ...showCode ? { "data-code": true } : {}
238
+ },
239
+ phase === "loading" ? React.createElement(
240
+ "div",
241
+ { className: "dsh-rfs-status" },
242
+ React.createElement(import_dsh_client_ui_primitives.StateDot, { state: "ongoing", size: 28 }),
243
+ React.createElement("p", { className: "dsh-rfs-statusLine" }, "文档渲染中...")
244
+ ) : null,
245
+ phase === "error" ? React.createElement(
246
+ "div",
247
+ { className: "dsh-rfs-empty" },
248
+ React.createElement(import_dsh_client_ui_primitives.FileTypeIcon, {
249
+ kind: (0, import_dsh_client_ui_primitives.classifyFileType)(target ? target.rawPath : ""),
250
+ size: 36,
251
+ className: "dsh-rfs-emptyIcon"
252
+ }),
253
+ React.createElement("p", { className: "dsh-rfs-emptyLine" }, message),
254
+ React.createElement(
255
+ "button",
256
+ { type: "button", className: "dsh-rfs-retry", onClick: refresh },
257
+ React.createElement(import_dsh_client_ui_primitives.IconRefreshOutlineRegular, { size: 14 }),
258
+ "重试"
259
+ )
260
+ ) : null,
261
+ phase === "ready" && showCode ? React.createElement(
262
+ "div",
263
+ {
264
+ className: "dsh-rfs-code",
265
+ "data-code-preview": true,
266
+ "data-wrap": wrap
267
+ },
268
+ React.createElement(import_dsh_client_ui_primitives.CodeBlock, {
269
+ className: "dsh-rfs-codeblock",
270
+ code: view.rows.join("\n"),
271
+ lang: language,
272
+ streaming: false,
273
+ lineNumbers: true,
274
+ wrap,
275
+ copyLabel: "复制",
276
+ copiedLabel: "已复制",
277
+ toolbarLabels: {
278
+ codeLabel: "代码",
279
+ wrapLabel: "启用自动换行",
280
+ unwrapLabel: "关闭自动换行"
281
+ }
282
+ })
283
+ ) : null,
284
+ phase === "ready" && !showCode ? React.createElement(
285
+ "div",
286
+ {
287
+ className: "dsh-rfs-textDocument",
288
+ "data-textpreview-plain": true
289
+ },
290
+ React.createElement(
291
+ "pre",
292
+ { className: "dsh-rfs-page" },
293
+ view.rows.map(function(row, index) {
294
+ const number = index + 1;
295
+ return React.createElement("div", {
296
+ key: number,
297
+ "data-line": String(number),
298
+ className: line === number ? "dsh-rfs-line dsh-rfs-lineTarget" : "dsh-rfs-line"
299
+ }, row, "\n");
300
+ })
301
+ )
302
+ ) : null
303
+ );
304
+ return React.createElement(
305
+ "div",
306
+ { className: "dsh-rfs-preview" },
307
+ notice !== null ? React.createElement(
308
+ "p",
309
+ { className: "dsh-rfs-notice" },
310
+ React.createElement("span", null, notice)
311
+ ) : null,
312
+ React.createElement(
313
+ "div",
314
+ { className: "dsh-rfs-header" },
315
+ React.createElement(import_dsh_client_ui_primitives.PathLabel, {
316
+ path: target ? target.target : address,
317
+ className: "dsh-rfs-path"
318
+ }),
319
+ language !== null ? React.createElement(import_dsh_client_ui_primitives.Menu, {
320
+ open: menuOpen,
321
+ anchor: React.createElement("button", {
322
+ type: "button",
323
+ className: "dsh-rfs-tool dsh-rfs-viewerTool",
324
+ "aria-label": "打开方式",
325
+ title: plain ? "纯文本" : "代码",
326
+ "data-document-viewer-menu": true,
327
+ onClick: function() {
328
+ setMenuOpen(function(v) {
329
+ return !v;
330
+ });
331
+ }
332
+ }, plain ? "纯文本" : "代码"),
333
+ items: [
334
+ { id: "code", label: "代码" },
335
+ { id: "plain", label: "纯文本" }
336
+ ],
337
+ selectedId: plain ? "plain" : "code",
338
+ onSelect: function(id) {
339
+ setPlain(id === "plain");
340
+ setMenuOpen(false);
341
+ },
342
+ onClose: function() {
343
+ setMenuOpen(false);
344
+ },
345
+ align: "end",
346
+ portal: true,
347
+ dense: true
348
+ }) : null,
349
+ React.createElement(import_dsh_client_ui_primitives.Tooltip, {
350
+ label: wrap ? "关闭自动换行" : "启用自动换行",
351
+ side: "bottom",
352
+ delayMs: 500,
353
+ children: React.createElement("button", {
354
+ type: "button",
355
+ className: "dsh-rfs-tool",
356
+ "aria-pressed": wrap,
357
+ "aria-label": "自动换行",
358
+ onClick: function() {
359
+ setWrap(function(w) {
360
+ return !w;
361
+ });
362
+ }
363
+ }, wrap ? React.createElement(import_dsh_client_ui_primitives.IconNowrapFillRegular, {}) : React.createElement(import_dsh_client_ui_primitives.IconWrapFillRegular, {}))
364
+ }),
365
+ React.createElement(import_dsh_client_ui_primitives.Tooltip, {
366
+ label: "重新加载",
367
+ side: "bottom",
368
+ delayMs: 500,
369
+ children: React.createElement("button", {
370
+ type: "button",
371
+ className: "dsh-rfs-tool",
372
+ "aria-label": "重新加载",
373
+ onClick: refresh
374
+ }, React.createElement(import_dsh_client_ui_primitives.IconRefreshOutlineRegular, {}))
375
+ })
376
+ ),
377
+ body
378
+ );
379
+ }
380
+ function readRemotePath(block) {
381
+ const settled = "kind" in block;
382
+ const raw = settled ? block.call?.argsRaw : block.argsRaw;
383
+ if (typeof raw !== "string") return void 0;
384
+ try {
385
+ const parsed = JSON.parse(raw);
386
+ if (parsed !== null && typeof parsed === "object") {
387
+ const p = parsed.file_path;
388
+ if (typeof p === "string" && p !== "") return p;
389
+ }
390
+ } catch {
391
+ }
392
+ return void 0;
393
+ }
394
+ function readRemoteText(block) {
395
+ const content = Array.isArray(block.content) ? block.content : [];
396
+ for (const item of content) {
397
+ if (item !== null && typeof item === "object") {
398
+ const record = item;
399
+ if (record.type === "text" && typeof record.text === "string") return record.text;
400
+ }
401
+ }
402
+ return null;
403
+ }
404
+ function readRemoteInput(block) {
405
+ const settled = "kind" in block;
406
+ const raw = settled ? block.call?.argsRaw : block.argsRaw;
407
+ if (typeof raw !== "string" || raw === "") return null;
408
+ try {
409
+ return JSON.stringify(JSON.parse(raw), null, 2);
410
+ } catch {
411
+ return raw;
412
+ }
413
+ }
414
+ function ReadRemoteRow(props) {
415
+ const block = props.block;
416
+ const filePath = readRemotePath(block);
417
+ const settled = "kind" in block;
418
+ const failed = settled && block.isError === true;
419
+ const [open, setOpen] = React.useState(false);
420
+ const input = readRemoteInput(block);
421
+ const output = settled ? readRemoteText(block) : null;
422
+ const expandable = input !== null || output !== null;
423
+ const canOpen = filePath !== void 0 && props.openFile !== void 0 && !failed;
424
+ function onOpenFile(event) {
425
+ event.stopPropagation();
426
+ if (filePath === void 0 || props.openFile === void 0) return;
427
+ const opened = props.openFile(filePath);
428
+ if (opened instanceof Promise) opened.catch(function(e) {
429
+ console.error("[remote-file-system] 打开远端文件失败", e);
430
+ });
431
+ }
432
+ function onToggle() {
433
+ setOpen(function(v) {
434
+ return !v;
435
+ });
436
+ }
437
+ const icon = React.createElement("svg", {
438
+ width: 14,
439
+ height: 14,
440
+ viewBox: "0 0 16 16",
441
+ fill: "none",
442
+ xmlns: "http://www.w3.org/2000/svg",
443
+ "aria-hidden": "true",
444
+ strokeWidth: 1
445
+ }, React.createElement("path", {
446
+ d: "M12 10L8.70711 6.70711C8.31658 6.31658 7.68342 6.31658 7.29289 6.70711L4 10",
447
+ stroke: "currentColor"
448
+ }));
449
+ return React.createElement(
450
+ "div",
451
+ { className: "dsh-rfs-root" },
452
+ React.createElement(
453
+ "div",
454
+ {
455
+ className: "dsh-rfs-row",
456
+ "data-disclosure-row": true,
457
+ "data-expandable": expandable ? true : void 0,
458
+ role: expandable ? "button" : void 0,
459
+ tabIndex: expandable ? 0 : void 0,
460
+ "aria-expanded": expandable ? open : void 0,
461
+ onClick: expandable ? onToggle : void 0
462
+ },
463
+ React.createElement("span", { className: "dsh-rfs-leading" }, icon),
464
+ React.createElement(
465
+ "span",
466
+ { className: "dsh-rfs-content" },
467
+ React.createElement("span", { className: "dsh-rfs-rowtitle" }, "工具调用"),
468
+ React.createElement("span", { className: "dsh-rfs-sep", "data-shimmer-decoration": "true", "aria-hidden": true }),
469
+ React.createElement(
470
+ "span",
471
+ { className: "dsh-rfs-summary" },
472
+ React.createElement("span", { className: "dsh-rfs-summarytext" }, filePath !== void 0 ? "read_remote · " : "read_remote"),
473
+ filePath !== void 0 ? canOpen ? React.createElement("button", { type: "button", className: "dsh-rfs-summarylink", onClick: onOpenFile }, filePath) : React.createElement("span", { className: "dsh-rfs-summarylink" }, filePath) : null
474
+ )
475
+ )
476
+ ),
477
+ expandable && open ? React.createElement(
478
+ "div",
479
+ { className: "dsh-rfs-iocard" },
480
+ input !== null ? React.createElement(
481
+ "div",
482
+ { className: "dsh-rfs-iosection" },
483
+ React.createElement("span", { className: "dsh-rfs-iolabel" }, "输入"),
484
+ React.createElement("span", { className: "dsh-rfs-iotext" }, input)
485
+ ) : null,
486
+ input !== null && output !== null ? React.createElement("span", { className: "dsh-rfs-iodivider", "aria-hidden": true }) : null,
487
+ output !== null ? React.createElement(
488
+ "div",
489
+ { className: "dsh-rfs-iosection" },
490
+ React.createElement("span", { className: "dsh-rfs-iolabel" }, "输出"),
491
+ React.createElement("span", { className: "dsh-rfs-iotext", ...failed ? { "data-error": true } : {} }, output)
492
+ ) : null
493
+ ) : null
494
+ );
495
+ }
496
+ function RemoteFileTitle(props) {
497
+ const info = props.useTabInfo();
498
+ return React.createElement(
499
+ React.Fragment,
500
+ null,
501
+ React.createElement(import_dsh_client_ui_primitives.FileTypeIcon, {
502
+ kind: (0, import_dsh_client_ui_primitives.classifyFileType)(info.tab.title),
503
+ size: 16,
504
+ className: "dsh-rfs-titleIcon"
505
+ }),
506
+ info.tab.title
507
+ );
508
+ }
509
+ var name = "dsh-remote-file-system";
510
+ var inject = ["slots", "sidebarRightTabs", "sidebarRight"];
511
+ function apply(ctx) {
512
+ const slots = ctx.get("slots");
513
+ if (!slots) return;
514
+ const tabs = ctx.get("sidebarRightTabs");
515
+ if (!tabs) throw new Error("[remote-file-system] 缺少 sidebarRightTabs 服务,请确认 dsh-client-ui-sidebar-right 已加载");
516
+ const right = ctx.get("sidebarRight");
517
+ if (!right) throw new Error("[remote-file-system] 缺少 sidebarRight 服务,请确认 dsh-client-ui-sidebar-right 已加载");
518
+ ctx.effect(function() {
519
+ ensureStyle();
520
+ return function() {
521
+ removeStyle();
522
+ };
523
+ });
524
+ ctx.effect(function() {
525
+ return tabs.register({
526
+ id: TYPE_ID,
527
+ kind: KIND,
528
+ priority: "extension",
529
+ patterns: ["dsh-resource://file/**"],
530
+ canOpen: function(address) {
531
+ return parseRemoteAddress(address) !== null;
532
+ },
533
+ title: function(address) {
534
+ const parsed = parseRemoteAddress(address);
535
+ return parsed ? basenameOf(parsed.rawPath) : address;
536
+ },
537
+ keepMounted: true
538
+ });
539
+ });
540
+ slots.inject("sidebar.right.pane.tab", function() {
541
+ return slots.register({ name: "sidebar.right.pane.tab", key: TYPE_ID }, RemoteFileBody);
542
+ });
543
+ slots.inject("sidebar.right.pane.tab.title", function() {
544
+ return slots.register({ name: "sidebar.right.pane.tab.title", key: TYPE_ID }, RemoteFileTitle);
545
+ });
546
+ slots.inject("tool.call.toolview", function() {
547
+ return slots.register({ name: "tool.call.toolview", key: "read_remote", priority: 0 }, ReadRemoteRow);
548
+ });
549
+ }
550
+ var plugin = { name, inject, apply };
551
+ var client_default = plugin;
552
+ return module.exports; } });
553
+ //# sourceMappingURL=client.js.map