virlen-remote 0.5.0 → 0.7.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +373 -169
  2. package/README.md +389 -201
  3. package/dist/index.d.ts +10 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +10 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/protocol/agents.d.ts +14 -0
  8. package/dist/protocol/agents.d.ts.map +1 -0
  9. package/dist/protocol/agents.js +14 -0
  10. package/dist/protocol/agents.js.map +1 -0
  11. package/dist/protocol/api.d.ts +370 -1
  12. package/dist/protocol/api.d.ts.map +1 -1
  13. package/dist/protocol/api.js.map +1 -1
  14. package/dist/protocol/compress.d.ts +55 -0
  15. package/dist/protocol/compress.d.ts.map +1 -0
  16. package/dist/protocol/compress.js +60 -0
  17. package/dist/protocol/compress.js.map +1 -0
  18. package/dist/protocol/files.d.ts +261 -0
  19. package/dist/protocol/files.d.ts.map +1 -0
  20. package/dist/protocol/files.js +506 -0
  21. package/dist/protocol/files.js.map +1 -0
  22. package/dist/protocol/host.d.ts +24 -1
  23. package/dist/protocol/host.d.ts.map +1 -1
  24. package/dist/protocol/host.js +9 -1
  25. package/dist/protocol/host.js.map +1 -1
  26. package/dist/protocol/message-files.d.ts +79 -0
  27. package/dist/protocol/message-files.d.ts.map +1 -0
  28. package/dist/protocol/message-files.js +118 -0
  29. package/dist/protocol/message-files.js.map +1 -0
  30. package/dist/protocol/tool-args.d.ts +95 -0
  31. package/dist/protocol/tool-args.d.ts.map +1 -0
  32. package/dist/protocol/tool-args.js +439 -0
  33. package/dist/protocol/tool-args.js.map +1 -0
  34. package/dist/testing/mock-host.d.ts +44 -1
  35. package/dist/testing/mock-host.d.ts.map +1 -1
  36. package/dist/testing/mock-host.js +439 -10
  37. package/dist/testing/mock-host.js.map +1 -1
  38. package/dist/transport/rtc.d.ts +17 -0
  39. package/dist/transport/rtc.d.ts.map +1 -1
  40. package/dist/transport/rtc.js +49 -12
  41. package/dist/transport/rtc.js.map +1 -1
  42. package/dist/transport/signaling.d.ts +12 -1
  43. package/dist/transport/signaling.d.ts.map +1 -1
  44. package/dist/transport/signaling.js +17 -3
  45. package/dist/transport/signaling.js.map +1 -1
  46. package/package.json +69 -69
  47. package/src/index.ts +270 -184
  48. package/src/protocol/agents.ts +13 -0
  49. package/src/protocol/api.ts +974 -615
  50. package/src/protocol/compress.ts +65 -0
  51. package/src/protocol/files.ts +544 -0
  52. package/src/protocol/host.ts +205 -165
  53. package/src/protocol/message-actions.ts +60 -60
  54. package/src/protocol/message-files.ts +161 -0
  55. package/src/protocol/pairing.ts +254 -254
  56. package/src/protocol/tool-args.ts +478 -0
  57. package/src/testing/mock-host.ts +1285 -740
  58. package/src/transport/link-kind.ts +222 -222
  59. package/src/transport/rtc.ts +381 -344
  60. package/src/transport/signaling.ts +331 -317
  61. package/src/transport/transfer-tier.ts +64 -64
@@ -0,0 +1,79 @@
1
+ /**
2
+ * 手机端声明它 = **文件面板能给「引用」入口**,并能渲染 `MessageDTO.files` 的 chip。
3
+ * 电脑端列出它 = **本机认识 `SendParams.files` 与 `MessageDTO.files`**。
4
+ */
5
+ export declare const MESSAGE_FILE_CAPABILITY = "message.file";
6
+ /**
7
+ * 一条消息最多能带几个文件引用。
8
+ *
9
+ * 上限的意义有两层:① 超过之后模型也只是「看得到一堆路径」,边际收益为零;
10
+ * ② 它是**载荷上界**(每个引用都在消息里,且会随消息一起持久化 / 导出 / 重放)。
11
+ * 超限**拒整条**而不是截断:截断是静默的 —— 手机端的 chip 还在,用户会以为都带上了。
12
+ */
13
+ export declare const MESSAGE_FILE_MAX = 20;
14
+ /**
15
+ * 单个引用路径的长度上限(1024 字符)。
16
+ *
17
+ * 与 `MESSAGE_FILE_MAX` 同一条纪律:路径是**手机端给的字符串**,没有上界就等于允许
18
+ * 一个(有 bug 或被改写的)客户端把任意长的内容塞进消息里。正常路径远低于这个数
19
+ * (Windows 的 `MAX_PATH` 是 260)。
20
+ */
21
+ export declare const MESSAGE_FILE_PATH_MAX = 1024;
22
+ /**
23
+ * 一条**文件引用**(手机端 → 电脑端 → 引擎内容块)。
24
+ *
25
+ * ⚠️ 与 `MessageQuote` 不同,这里**不是快照**:存的是路径,AI 读的那一刻磁盘上是什么就是什么。
26
+ * 这正是「不搬运内容」的代价,也是它的好处(引用的永远是**当前**内容,不会拿一份过期副本
27
+ * 去回答)。桌面端的文件附件同此语义。
28
+ */
29
+ export interface MessageFileRef {
30
+ /** 文件绝对路径(分隔符按契约统一为 `/`,见 `sanitizeFileRefs`)。 */
31
+ path: string;
32
+ /** 文件名(含扩展名)—— chip 上显示的就是它。 */
33
+ name: string;
34
+ /** 是否为目录(目录也能被引用:模型用 `list_files` 去看里面有什么)。 */
35
+ isDir?: boolean;
36
+ /** 字节数(目录没有这一项)。只用于展示,**不作为任何校验依据**(它不是文件的事实来源)。 */
37
+ size?: number;
38
+ }
39
+ /**
40
+ * 校验结果。
41
+ *
42
+ * ⚠️ **刻意不写成判别联合**(`| { ok: true; files } | { ok: false; reason }`):
43
+ * 布尔字面量的判别联合靠 `strictNullChecks` 才能收窄,而消费方 `virlen-app` 的 tsconfig 是
44
+ * `strictNullChecks: false` —— 写成判别联合的话,电脑侧那句 `if (!r.ok) { …r.reason… }`
45
+ * 根本收窄不了(直接 `TS2339: Property 'reason' does not exist`)。契约放在共享包里,
46
+ * 就不能只在一边成立。
47
+ *
48
+ * 不变式:`ok === (reason === '')`(通过时 `reason` 是空串,`files` 是被归一化的引用;
49
+ * 被拒时 `files` 恒为空数组)。
50
+ */
51
+ export interface FileRefSanitizeResult {
52
+ /** 是否通过校验。 */
53
+ ok: boolean;
54
+ /** 归一化后的引用;被拒时恒为空数组(调用方那时应该直接抛错)。 */
55
+ files: MessageFileRef[];
56
+ /** 被拒原因(可直接显示给用户);通过时为空串。 */
57
+ reason: string;
58
+ }
59
+ /**
60
+ * 校验并归一化手机传来的文件引用(**电脑侧与演示宿主共用这一份**)。
61
+ *
62
+ * 规则:
63
+ * - 缺省 / `null` → 空数组(不带文件引用是完全正常的一条消息);
64
+ * - 不是数组 / 条目不是对象 / `path` 不是非空字符串 → **整条拒**(返回 `ok:false`);
65
+ * - 路径超过 `MESSAGE_FILE_PATH_MAX`、条目数超过 `MESSAGE_FILE_MAX` → 整条拒;
66
+ * - `name` 缺失或为空 → 用路径末段兜底(电脑侧从内容块投影回来时也会用到这条);
67
+ * - `isDir` / `size` 形状不对 → **丢掉该字段**(它们是展示元数据,说谎也只是 chip 上少一行字),
68
+ * 但路径非法必须拒 —— 那是这条引用**唯一有语义的部分**。
69
+ * - 路径分隔符 `\` → `/`(与全项目的路径口径一致,两侧拼串才不会出现 `a\/b`);
70
+ * - 同一路径只保留第一次出现的(手机端连点两下不该变成两条引用)。
71
+ *
72
+ * ## 为什么非法就整条拒(而不是「丢掉那一条、其余照发」)
73
+ *
74
+ * §36 的教训:**假绿灯比报错难查得多**。丢掉一条引用时,手机端那边的 chip 还在输入框里、
75
+ * 发完消息里也有它 —— 用户会以为带上了,而 AI 从未看到。拒掉整条之后,用户看到的是
76
+ * 一条明确的失败提示,芯片还在,可以改完再发。
77
+ */
78
+ export declare function sanitizeFileRefs(input: unknown): FileRefSanitizeResult;
79
+ //# sourceMappingURL=message-files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-files.d.ts","sourceRoot":"","sources":["../../src/protocol/message-files.ts"],"names":[],"mappings":"AAiCA;;;GAGG;AACH,eAAO,MAAM,uBAAuB,iBAAiB,CAAA;AAErD;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,KAAK,CAAA;AAElC;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,OAAO,CAAA;AAEzC;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAA;IACZ,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,+CAA+C;IAC/C,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,oDAAoD;IACpD,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,qBAAqB;IACpC,cAAc;IACd,EAAE,EAAE,OAAO,CAAA;IACX,qCAAqC;IACrC,KAAK,EAAE,cAAc,EAAE,CAAA;IACvB,6BAA6B;IAC7B,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,qBAAqB,CAuCtE"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * 消息里的**文件引用** —— **两端共用的契约**:能力名 + 引用体 + 校验口径。
3
+ *
4
+ * ## 这是什么(与桌面输入框的「文件附件」同一条口径)
5
+ *
6
+ * 手机端在文件面板里挑一个电脑上的文件,把它的**路径 + 展示元数据**挂到要发的那条消息上;
7
+ * 电脑端把它组装成 `{type:'file'}` 内容块交给引擎。**不搬运文件内容** ——
8
+ * 真正的读取交给 AI 用 `read_file` 工具按需完成(桌面端 `FileAttachment` 的文件头
9
+ * 写着同一条:「只存路径,不拷贝文件内容」)。
10
+ *
11
+ * ## 为什么必须放在共享包里
12
+ *
13
+ * 与 §36 的引用(`message-actions.ts`)同一条理由:它是**两端各表一半**的约定 ——
14
+ * 手机端发 `SendParams.files` 并把 `MessageDTO.files` 渲染成 chip,电脑端把前者组装成内容块、
15
+ * 把后者投影出来。字段名(`path` / `isDir` / `size`)与「路径分隔符统一成 `/`」的归一化
16
+ * 各写一份必然漂移,而漂移的症状是**静默的**:元数据对不上时 chip 显示成另一个东西,
17
+ * 没有任何报错可查。
18
+ *
19
+ * ## 校验为什么也在这里(而不是电脑侧自己写一份)
20
+ *
21
+ * `sanitizeFileRefs` 是**两端唯一的校验口径**:真电脑侧(`virlen-app/src/bridge/host-source.ts`)
22
+ * 与演示宿主(`testing/mock-host.ts`)都调它。若各写一份,mock 与真机会在「什么样的 `files`
23
+ * 算非法」上分叉 —— 而 mock 是单测的唯一参照物,分叉等于测试对着假行为发绿灯。
24
+ *
25
+ * ## 能力位的语义(与 §36 的 `MESSAGE_QUOTE_CAPABILITY` 一致)
26
+ *
27
+ * `MESSAGE_FILE_CAPABILITY` 是**功能标记**,不是权限:文件引用本身就是 `session.send` 的一个
28
+ * 参数(越权防线仍是 ACL 里的 `session.send`)。写进本表只为让手机端知道
29
+ * 「本机电脑端认识 `files`」—— 旧电脑端会把未知字段**静默丢掉**,用户侧的表现是
30
+ * 「我明明附了文件,AI 却当没看见」,且没有任何报错可查。宁可对旧电脑端不显示这个入口。
31
+ */
32
+ import { FILE_NAME_MAX_LEN } from './files.js';
33
+ /**
34
+ * 手机端声明它 = **文件面板能给「引用」入口**,并能渲染 `MessageDTO.files` 的 chip。
35
+ * 电脑端列出它 = **本机认识 `SendParams.files` 与 `MessageDTO.files`**。
36
+ */
37
+ export const MESSAGE_FILE_CAPABILITY = 'message.file';
38
+ /**
39
+ * 一条消息最多能带几个文件引用。
40
+ *
41
+ * 上限的意义有两层:① 超过之后模型也只是「看得到一堆路径」,边际收益为零;
42
+ * ② 它是**载荷上界**(每个引用都在消息里,且会随消息一起持久化 / 导出 / 重放)。
43
+ * 超限**拒整条**而不是截断:截断是静默的 —— 手机端的 chip 还在,用户会以为都带上了。
44
+ */
45
+ export const MESSAGE_FILE_MAX = 20;
46
+ /**
47
+ * 单个引用路径的长度上限(1024 字符)。
48
+ *
49
+ * 与 `MESSAGE_FILE_MAX` 同一条纪律:路径是**手机端给的字符串**,没有上界就等于允许
50
+ * 一个(有 bug 或被改写的)客户端把任意长的内容塞进消息里。正常路径远低于这个数
51
+ * (Windows 的 `MAX_PATH` 是 260)。
52
+ */
53
+ export const MESSAGE_FILE_PATH_MAX = 1024;
54
+ /**
55
+ * 校验并归一化手机传来的文件引用(**电脑侧与演示宿主共用这一份**)。
56
+ *
57
+ * 规则:
58
+ * - 缺省 / `null` → 空数组(不带文件引用是完全正常的一条消息);
59
+ * - 不是数组 / 条目不是对象 / `path` 不是非空字符串 → **整条拒**(返回 `ok:false`);
60
+ * - 路径超过 `MESSAGE_FILE_PATH_MAX`、条目数超过 `MESSAGE_FILE_MAX` → 整条拒;
61
+ * - `name` 缺失或为空 → 用路径末段兜底(电脑侧从内容块投影回来时也会用到这条);
62
+ * - `isDir` / `size` 形状不对 → **丢掉该字段**(它们是展示元数据,说谎也只是 chip 上少一行字),
63
+ * 但路径非法必须拒 —— 那是这条引用**唯一有语义的部分**。
64
+ * - 路径分隔符 `\` → `/`(与全项目的路径口径一致,两侧拼串才不会出现 `a\/b`);
65
+ * - 同一路径只保留第一次出现的(手机端连点两下不该变成两条引用)。
66
+ *
67
+ * ## 为什么非法就整条拒(而不是「丢掉那一条、其余照发」)
68
+ *
69
+ * §36 的教训:**假绿灯比报错难查得多**。丢掉一条引用时,手机端那边的 chip 还在输入框里、
70
+ * 发完消息里也有它 —— 用户会以为带上了,而 AI 从未看到。拒掉整条之后,用户看到的是
71
+ * 一条明确的失败提示,芯片还在,可以改完再发。
72
+ */
73
+ export function sanitizeFileRefs(input) {
74
+ /** 被拒(`files` 恒为空数组 —— 失败就是**整条**失败,没有「部分成功」这个态) */
75
+ const reject = (reason) => ({ ok: false, files: [], reason });
76
+ if (input == null)
77
+ return { ok: true, files: [], reason: '' };
78
+ if (!Array.isArray(input))
79
+ return reject('文件引用的形状不对');
80
+ if (input.length > MESSAGE_FILE_MAX) {
81
+ return reject(`一次最多引用 ${MESSAGE_FILE_MAX} 个文件`);
82
+ }
83
+ const files = [];
84
+ const seen = new Set();
85
+ for (const raw of input) {
86
+ if (typeof raw !== 'object' || raw === null) {
87
+ return reject('文件引用的形状不对');
88
+ }
89
+ const item = raw;
90
+ if (typeof item.path !== 'string' || !item.path.trim()) {
91
+ return reject('文件引用缺少路径');
92
+ }
93
+ // 归一化分隔符:`E:\a\b` → `E:/a/b`(与 `normalizeWorkspace` / 文件面板同一口径)
94
+ const path = item.path.replace(/\\/g, '/');
95
+ if (path.length > MESSAGE_FILE_PATH_MAX) {
96
+ return reject('文件路径过长');
97
+ }
98
+ if (seen.has(path))
99
+ continue;
100
+ seen.add(path);
101
+ const name = typeof item.name === 'string' ? item.name.trim() : '';
102
+ files.push({
103
+ path,
104
+ name: (name || baseName(path)).slice(0, FILE_NAME_MAX_LEN),
105
+ ...(typeof item.isDir === 'boolean' ? { isDir: item.isDir } : {}),
106
+ ...(typeof item.size === 'number' && Number.isFinite(item.size) && item.size >= 0
107
+ ? { size: item.size }
108
+ : {}),
109
+ });
110
+ }
111
+ return { ok: true, files, reason: '' };
112
+ }
113
+ /** 路径末段(`name` 缺失时的兜底;本地实现以免把 `protocol/files` 的整套路径工具拖进来)。 */
114
+ function baseName(path) {
115
+ const parts = path.replace(/\/+$/, '').split('/');
116
+ return parts[parts.length - 1] || path;
117
+ }
118
+ //# sourceMappingURL=message-files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-files.js","sourceRoot":"","sources":["../../src/protocol/message-files.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * 工具入参摘要 —— 「这一步工具到底在干什么」的**唯一格式化口径**。
3
+ *
4
+ * 为什么单列一个模块:手机屏上一行只放得下一步工具调用,而**哪几个键才是关键入参**是随
5
+ * 工具而变的(`read_file` 看路径、`execute_command` 看命令、`search_text_in_files` 看
6
+ * 「在哪儿搜什么」)。这份知识原先只存在于桌面端的 28 个 `getShortText()` 组件里 ——
7
+ * 手机端拿不到,于是真机反馈是「工具气泡只有工具名,看不到编辑的是哪个文件、执行的是什么命令」。
8
+ *
9
+ * 三条纪律(与桌面 `tool-call/primary-path.ts` 同源):
10
+ *
11
+ * 1. **按工具名取键**:键名以 `tool_defs/definitions.json` 为准(那是权威契约,本文件
12
+ * 只挑「主参数」)——不用「给每个工具加一个方法」那种写法,那是把同一件事抄 28 遍;
13
+ * 2. **拿不到就不猜**:没有可用的关键入参 → `undefined`,手机端只显示工具名(宁可不显示,
14
+ * 也不要显示一句听起来像那么回事的假话);
15
+ * 3. **只给摘要,不给原文**:`write_file.content` / `edit_file.edits[].old_string` 可能是
16
+ * 整篇文章,原样下行就是流量事故(§7-⑦ 同一条纪律)。长度有硬上限,超长一律截断。
17
+ *
18
+ * 产出是**纯文本、纯函数**:同一份实现同时供真实电脑侧(`bridge/dto.ts`)与演示宿主
19
+ * (`testing/mock-host.ts`)使用,两端不会漂移。
20
+ *
21
+ * 本模块承担工具调用的**两种呈现**(两级粒度,别混用):
22
+ * - {@link summarizeToolArgs} —— **折叠态**的一行摘要(硬上限 {@link TOOL_ARGS_MAX} = 160);
23
+ * - {@link formatToolArgs} —— **展开态**的完整入参(上限 {@link TOOL_DETAIL_MAX} = 5000,
24
+ * 超出走中间省略 {@link elideMiddle})。
25
+ * 两级共用同一个 `shortenPath` 回调:同一份路径不能在折叠态与展开态显示成两个样子。
26
+ */
27
+ /** 摘要长度硬上限:手机一行放不下更多,也避免长命令 / 长路径把帧撑大。 */
28
+ export declare const TOOL_ARGS_MAX = 160;
29
+ export interface ToolArgsSummaryOptions {
30
+ /**
31
+ * 路径缩短回调(电脑侧传 `toShortPath(path, workspace)`,与桌面卡片同一口径)。
32
+ * 不传 = 原样显示 —— 演示宿主 / 单测不需要工作目录上下文。
33
+ */
34
+ shortenPath?: (path: string) => string;
35
+ /** 摘要上限,默认 {@link TOOL_ARGS_MAX}。 */
36
+ maxLength?: number;
37
+ }
38
+ /**
39
+ * 生成一行入参摘要;没有可显示的内容时返回 `undefined`。
40
+ *
41
+ * 未知工具走兜底键序;一个都不命中时用「键=值」拼(最多两个键)——那是桌面
42
+ * `DefaultMessage` 的行为,手机端不需要比它更聪明,但需要至少能看出「调了什么」。
43
+ */
44
+ export declare function summarizeToolArgs(name: string, input: unknown, options?: ToolArgsSummaryOptions): string | undefined;
45
+ /**
46
+ * **展开区**文本的长度上限 —— 入参详情与工具输出**共用同一条线**。
47
+ *
48
+ * 与 {@link TOOL_ARGS_MAX} 不是一个量级,也不是一回事:那个是**折叠态一行**能放下多少字,
49
+ * 这个是**展开后**愿意渲染多少字(用户 2026-10 拍板:超了就中间省略)。
50
+ *
51
+ * 为什么是 5000:手机一屏约 30 行 × 40 字,5000 字符已是四屏以上;再长就不叫「看」,
52
+ * 叫「拖着滚」—— 而拖动过程中真正需要的信息(开头在做什么、结尾成没成)都在两头,
53
+ * 中间那段从来没人读完过。
54
+ */
55
+ export declare const TOOL_DETAIL_MAX = 5000;
56
+ /**
57
+ * 中间省略 —— 头尾都留,挖掉中间。
58
+ *
59
+ * 为什么不「从尾部截断」:工具输出与入参的**结论都在两头** —— 开头是命令 / 标题 / 第一个
60
+ * 文件,结尾是报错 / 汇总 / 最后一个 diff 块。只留头等于把结论切掉,用户还得回电脑前看。
61
+ *
62
+ * ⚠️ 被省略的字符数写进正文里(`…(中间省略 N 字符)…`)——这是「这里少了东西」的
63
+ * **唯一凭证**:只看长度看不出来,一段刚好的 5000 字符与被砍过的 5000 字符无法区分。
64
+ */
65
+ export declare function elideMiddle(text: string, max?: number): string;
66
+ /**
67
+ * {@link formatToolArgs} 的可选项 —— 与 {@link ToolArgsSummaryOptions} 是同一种上下文。
68
+ */
69
+ export interface ToolArgsFormatOptions {
70
+ /**
71
+ * 路径缩短回调(电脑侧传 `toShortPath(path, workspace)`)。
72
+ *
73
+ * ⚠️ 必须与 {@link summarizeToolArgs} 传的是**同一个**回调:折叠态显示 `src/a.ts`、
74
+ * 展开后变成 `E:/code/demo/src/a.ts`,用户会以为点开卡片改坏了什么。
75
+ */
76
+ shortenPath?: (path: string) => string;
77
+ /** 上限,默认 {@link TOOL_DETAIL_MAX}(超出中间省略)。 */
78
+ maxLength?: number;
79
+ }
80
+ /**
81
+ * 工具入参的**完整文本**(展开区用):两空格缩进的 JSON,路径按工作目录缩短,超长中间省略。
82
+ *
83
+ * 为什么不把 {@link summarizeToolArgs} 的输出「加长」:摘要只挑**主参数**,而用户点开卡片
84
+ * 想看的恰恰是那行没显示完的部分 —— 还有哪些键、`write_file` 到底写了什么、`edit_file` 的
85
+ * old/new 是什么。唯一忠实的做法是把入参本身摆出来,于是这里与桌面导出
86
+ * (`ui/pages/chat/components/sidebar` 的 `JSON.stringify(tc.input, null, 2)`)用同一种形态。
87
+ *
88
+ * ⚠️ 与摘要「只给摘要、不给原文」的纪律**不同**:这里允许出现正文(`write_file.content`
89
+ * 会原样出现)—— 前提是用户**主动点开**才渲染,总量由 {@link TOOL_DETAIL_MAX} 兜住。
90
+ * 折叠态绝不带它,那才是流量事故的现场。
91
+ *
92
+ * 没有可显示的内容 → `undefined`(展开区不渲染这一块)。
93
+ */
94
+ export declare function formatToolArgs(input: unknown, options?: ToolArgsFormatOptions): string | undefined;
95
+ //# sourceMappingURL=tool-args.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-args.d.ts","sourceRoot":"","sources":["../../src/protocol/tool-args.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,0CAA0C;AAC1C,eAAO,MAAM,aAAa,MAAM,CAAA;AAgDhC,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC,qCAAqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,OAAO,EACd,OAAO,GAAE,sBAA2B,GACnC,MAAM,GAAG,SAAS,CAiBpB;AAiOD;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,OAAO,CAAA;AAEnC;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAE,MAAwB,GAAG,MAAM,CAW/E;AASD;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,OAAO,EACd,OAAO,GAAE,qBAA0B,GAClC,MAAM,GAAG,SAAS,CAWpB"}