@epoch-agent/view 4.0.0 → 4.1.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/dist/index.d.ts +114 -219
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -9,37 +9,26 @@ import { WireSessionReference, WireFileReference, WireImageReference, EpochUserC
|
|
|
9
9
|
*
|
|
10
10
|
* ---
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* 两个宿主(tui / web)共用同一套**结构化 display item**(`tool_group` / `diff` /
|
|
13
|
+
* 文本段),格式化在组件里,不绑终端。`web` 不能依赖 `tui`(ink / react peer 会
|
|
14
|
+
* 跟进浏览器包),所以落在这个**只依赖 protocol** 的包 —— 两处各抄一份会一直漂移。
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* 然后两份永远漂移。
|
|
19
|
-
*
|
|
20
|
-
* 本文件**不许**出现 node: 或 DOM 的东西 —— view 的 tsconfig 既不带
|
|
21
|
-
* `@types/node` 也不带 DOM lib,写了当场编译不过。
|
|
16
|
+
* 本文件**不许**出现 node: 或 DOM 的东西 —— view 的 tsconfig 既不带 `@types/node`
|
|
17
|
+
* 也不带 DOM lib,写了当场编译不过。
|
|
22
18
|
*/
|
|
23
19
|
|
|
24
20
|
interface HistoryItemBase {
|
|
25
21
|
text?: string;
|
|
26
22
|
/**
|
|
27
|
-
* 这一项**是什么时候**的(epoch 毫秒,来自 `WireEnvelope.ts`)。
|
|
28
|
-
*
|
|
29
|
-
* ## 只为一件事:把它印在屏幕上
|
|
23
|
+
* 这一项**是什么时候**的(epoch 毫秒,来自 `WireEnvelope.ts`)。
|
|
30
24
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* 已经问不出来了(`history` 只存展示项,不存帧)。
|
|
25
|
+
* 只为把它印在屏幕上:`seq` 回答「从哪儿开始」,这一个是**人读的那一半**(用户
|
|
26
|
+
* 那一段话底下那句 `8月25日 13:31`、回合脚注右端那一格)。必须在项**被造出来的
|
|
27
|
+
* 那一刻**冻进来 —— 事后再问已经问不出来(`history` 只存展示项,不存帧)。
|
|
35
28
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* 2. **回放**(`GET /messages`)—— 走 `WireReplayMessage.timestamp` 另填一次,
|
|
40
|
-
* 不是「没有」;真的没有的是老版本宿主自己拼的项;
|
|
41
|
-
* 3. **一轮里由多帧合成的项**取的是**第一帧**(同 `seq` 的口径:要回答
|
|
42
|
-
* 「这项从哪开始的」,用最后一帧会让时间跟着流式往后漂)。
|
|
29
|
+
* 三处它**没有值**,调用方必须当它可能不存在:TUI(吃裸 `AgentEvent`,没有信封)·
|
|
30
|
+
* 回放(`GET /messages`,走 `WireReplayMessage.timestamp` 另填)· 一轮里由多帧
|
|
31
|
+
* 合成的项取**第一帧**(用最后一帧会让时间跟着流式往后漂)。
|
|
43
32
|
*
|
|
44
33
|
* ⚠️ **缺席时不许补一个「现在」**:老历史的时间是**不知道**,印一个刚读的
|
|
45
34
|
* 挂钟等于给一条两年前的记录盖上今天的日期 —— 那比不画贵得多。
|
|
@@ -50,14 +39,11 @@ interface HistoryItemBase {
|
|
|
50
39
|
*
|
|
51
40
|
* 只是**锚点**,不渲染成文本:浏览器把它落在 `<li class="ev" data-seq>` 上,
|
|
52
41
|
* 三处真在用 —— 检视面板的产物行(「这个文件是哪一步产出的」)、回退(检查点)
|
|
53
|
-
* 的目标、URL
|
|
54
|
-
* 那个**形态**,不是这个字段。
|
|
42
|
+
* 的目标、URL 锚点。
|
|
55
43
|
*
|
|
56
|
-
* ⚠️
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* 3. 一项由多帧合成时,取的是**第一帧**(正文的第一个增量、工具组的第一次
|
|
60
|
-
* 调用)。锚点要回答的是「从哪儿开始的」,用最后一帧会让回退目标往后漂。
|
|
44
|
+
* ⚠️ 同样三处它**没有值**:TUI 压根没有信封(也不需要锚点)· 回放折出来的历史
|
|
45
|
+
* (落盘的消息里没有 seq)· 多项合成时取**第一帧**(正文的第一个增量、工具组的
|
|
46
|
+
* 第一次调用)—— 用最后一帧会让回退目标往后漂。
|
|
61
47
|
*/
|
|
62
48
|
seq?: number;
|
|
63
49
|
}
|
|
@@ -65,106 +51,76 @@ type HistoryItemUser = HistoryItemBase & {
|
|
|
65
51
|
type: 'user';
|
|
66
52
|
text: string;
|
|
67
53
|
/**
|
|
68
|
-
*
|
|
54
|
+
* 浏览器发这一条时自己起的编号—— **只有本地乐观上屏的那一份有**。
|
|
69
55
|
*
|
|
70
|
-
*
|
|
56
|
+
* 服务端把它原样带回 `user-message` / `steer` 帧(判据在 protocol 的
|
|
71
57
|
* `WireSendMessageRequest.clientMessageId` 上)。web 拿它认出「这条回执是我那条」:
|
|
72
|
-
*
|
|
58
|
+
* 开轮的不再画第二遍,插话的撤掉这一份、换成事件流给的(位置才对)。
|
|
73
59
|
* 回放出来的、事件流画出来的、TUI 的,一律缺席。
|
|
74
60
|
*/
|
|
75
61
|
clientMessageId?: string;
|
|
76
62
|
/**
|
|
77
|
-
* 这条消息里 `@:`
|
|
63
|
+
* 这条消息里 `@:` 引用到的那几段会话。
|
|
78
64
|
*
|
|
79
65
|
* **缺席 = 这条消息里没有引用**,不是「有但没查到」—— 后者是数组里一条带
|
|
80
66
|
* `omitted` 的记录(那五个稳定错误码之一)。
|
|
81
67
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
68
|
+
* 挂在**展示项**上而不是让浏览器自己去翻 `parts`:这一格有**两个**填它的地方 ——
|
|
69
|
+
* 回放从那条消息的 `parts` 里 `type: 'session'` 读,刚发出去那一条从
|
|
70
|
+
* `POST /messages` 的 202 上那个 `references` 读(后者压根没有 `parts`:用户自己
|
|
71
|
+
* 那条是本地乐观回显的,SSE 上不回它)。合到一格之后那张卡只有一处渲染 —— 各画
|
|
72
|
+
* 一次的话,刷新前后同一条消息会长得不一样。
|
|
85
73
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
74
|
+
* ⚠️ **带不带正文在契约那一层就定了**:`WireSessionReference` 是那个部件**摘掉
|
|
75
|
+
* `text`**(判据在 protocol 的 `wire-rest.ts` 上),所以这一格上永远没有另一段
|
|
76
|
+
* 会话的转录 —— 那也是「这张卡不展开」的由来(`web/src/mention/reference-card.tsx`
|
|
77
|
+
* 的文件头)。
|
|
90
78
|
*
|
|
91
|
-
*
|
|
92
|
-
* 两条路合到这一格之后,那张卡的渲染只有一处 —— 而它们要是各画一次,
|
|
93
|
-
* 刷新前后的同一条消息会长得不一样。
|
|
94
|
-
*
|
|
95
|
-
* ⚠️ **带不带正文这件事在契约那一层就定了**:`WireSessionReference` 是那个部件
|
|
96
|
-
* **摘掉 `text`**(判据在 protocol 的 `wire-rest.ts` 上),所以这一格上永远
|
|
97
|
-
* 没有另一段会话的转录 —— 那也是「这张卡不展开」的由来
|
|
98
|
-
* (`web/src/mention/reference-card.tsx` 的文件头)。
|
|
99
|
-
*
|
|
100
|
-
* TUI 那一侧今天不画它(方案 53 验收记录第六章:发出去之后屏幕上就是用户打的
|
|
101
|
-
* 那行字),所以这一格在那边恒缺席 —— 那是一次判断,不是漏接。
|
|
79
|
+
* TUI 那一侧恒缺席(发出去之后屏幕上就是用户打的那行字),是一次判断不是漏接。
|
|
102
80
|
*/
|
|
103
81
|
references?: readonly WireSessionReference[];
|
|
104
82
|
/**
|
|
105
|
-
* 这条消息里 `@路径`
|
|
83
|
+
* 这条消息里 `@路径` 附上的那几个文件。
|
|
106
84
|
*
|
|
107
|
-
* 缺席 / 两条路合到一格 /
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* 202 的 `files` 读(键不同、判据同一条)。
|
|
85
|
+
* 缺席 / 两条路合到一格 / 不带正文三条判据**逐字同**上面 `references`(
|
|
86
|
+
* `WireFileReference` 就是那个部件摘掉 `text`),不再抄一遍。差别只有填它的键:
|
|
87
|
+
* 回放从 `parts` 里 `type: 'file'` 读,刚发出去那一条从 202 的 `files` 读。
|
|
111
88
|
*/
|
|
112
89
|
files?: readonly WireFileReference[];
|
|
113
90
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* ## 这一格是 `state/types.ts` 和 `state/replay.ts` 记的同一笔债
|
|
91
|
+
* 这条消息里附的那几张图。
|
|
117
92
|
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* 在它之前,用户自己那条带图的消息在界面上是一行**文本占位符**
|
|
122
|
-
* (`[图片 image/jpeg]`,`contentToDisplayText` 的口径)—— 屏幕上说
|
|
123
|
-
* 「这里有张图」,但人看不到那张图,而它明明就在服务端的 artifact 目录里、
|
|
124
|
-
* 也已经有一个能取到它的端点。
|
|
125
|
-
*
|
|
126
|
-
* 缺席 / 两条路合到一格那两条判据**逐字同**上面 `references`:
|
|
127
|
-
*
|
|
128
|
-
* | 谁填 | 从哪来 |
|
|
129
|
-
* | --- | --- |
|
|
130
|
-
* | 回放(`GET /messages`) | 那条消息的 `parts` 里 `type: 'image'`(`image` 是服务端路径) |
|
|
131
|
-
* | 刚发出去那一条 | 浏览器自己刚读的那个文件(`image` 是 `data:` URL) |
|
|
93
|
+
* 缺席 / 两条路合到一格那两条判据**逐字同**上面 `references`:回放读那条消息的
|
|
94
|
+
* `parts` 里 `type: 'image'`(`image` 是服务端路径),刚发出去那一条是浏览器自己
|
|
95
|
+
* 刚读的那个文件(`image` 是 `data:` URL)。
|
|
132
96
|
*
|
|
133
97
|
* ⚠️ **和上面两格的区别:这一格带「内容」**。`WireImageReference` 只摘了 `type`、
|
|
134
|
-
* **没摘 `image`** ——
|
|
135
|
-
*
|
|
136
|
-
* 判据全文在 `WireImageReference` 上。
|
|
98
|
+
* **没摘 `image`** —— 界面要的就是那张图。但那一格永远是**引用**(路径 / http URL /
|
|
99
|
+
* 本地 data URL),服务端绝不在这条线上发 base64(判据全文在那个类型上)。
|
|
137
100
|
*
|
|
138
|
-
* TUI 那一侧恒缺席 ——
|
|
139
|
-
* 那一格的处置,是一次判断不是漏接)。
|
|
101
|
+
* TUI 那一侧恒缺席 —— 终端里画不了图,照旧走文本占位(同 `references`)。
|
|
140
102
|
*/
|
|
141
103
|
images?: readonly WireImageReference[];
|
|
142
104
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* 和 `text` 不重复:`text` 是展开之后的正文(模型看到的那一份),这一格是
|
|
146
|
-
* 「这行字是怎么来的」—— 渲染层据此把它画成一行 `/命令名 参数`,而不是把
|
|
147
|
-
* 几十行工作表指令冒充成用户说过的话。
|
|
105
|
+
* 这一条是**按哪条命令**发出来的。
|
|
148
106
|
*
|
|
149
|
-
*
|
|
107
|
+
* 和 `text` 不重复:`text` 是展开之后的正文(模型看到的那一份),这一格是「这行字
|
|
108
|
+
* 是怎么来的」—— 渲染层据此画成一行 `/命令名 参数`,而不是把几十行工作表指令冒充
|
|
109
|
+
* 成用户说过的话。
|
|
150
110
|
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* | 刚发出去那一条 | `submit` action(浏览器只能带 `name` + 参数) |
|
|
155
|
-
* | 别的窗口发的那一条 | `user-message` 帧上的 `command` |
|
|
111
|
+
* 三个来源各有一条填它的路,缺一不可:回放取 `messages.command` 那一列 · 刚发出去
|
|
112
|
+
* 那一条取 `submit` action(浏览器只能带 `name` + 参数)· 别的窗口发的那一条取
|
|
113
|
+
* `user-message` 帧上的 `command`。
|
|
156
114
|
*
|
|
157
|
-
* 缺席 = 普通消息,或者这条是 v15
|
|
158
|
-
* 推不出来就不编)。
|
|
115
|
+
* 缺席 = 普通消息,或者这条是 v15 之前的库写的(推不出来就不编)。
|
|
159
116
|
*/
|
|
160
117
|
command?: EpochUserCommand;
|
|
161
118
|
};
|
|
162
119
|
/**
|
|
163
120
|
* 模型的回复正文。
|
|
164
121
|
*
|
|
165
|
-
*
|
|
166
|
-
* deepseek / anthropic / openai
|
|
167
|
-
* 与 `EpochMessageRole` 对齐。
|
|
122
|
+
* 判别值 `'assistant'` 与 `EpochMessageRole` 对齐(不是 gemini-cli 那个 `'gemini'`
|
|
123
|
+
* —— 这个引擎跑的是 deepseek / anthropic / openai)。
|
|
168
124
|
*/
|
|
169
125
|
type HistoryItemAssistant = HistoryItemBase & {
|
|
170
126
|
type: 'assistant';
|
|
@@ -194,9 +150,9 @@ type HistoryItemModel = HistoryItemBase & {
|
|
|
194
150
|
/**
|
|
195
151
|
* 模型的思考原文。
|
|
196
152
|
*
|
|
197
|
-
* **默认不产出**:运行期只显示 `Thinking…` 状态行,reasoning
|
|
198
|
-
*
|
|
199
|
-
*
|
|
153
|
+
* **默认不产出**:运行期只显示 `Thinking…` 状态行,reasoning 在内存里累加;只有
|
|
154
|
+
* `/thinking` 打开时,本轮结束才把它作为一条历史项落下来 —— 边流边显示等于每个
|
|
155
|
+
* token 都触发一次整帧重绘,而且没人读得下去。
|
|
200
156
|
*/
|
|
201
157
|
type HistoryItemThinking = HistoryItemBase & {
|
|
202
158
|
type: 'thinking';
|
|
@@ -209,7 +165,7 @@ type HistoryItemDiff = HistoryItemBase & {
|
|
|
209
165
|
oldContent?: string;
|
|
210
166
|
newContent?: string;
|
|
211
167
|
/**
|
|
212
|
-
*
|
|
168
|
+
* 内容太大、没存下来。
|
|
213
169
|
*
|
|
214
170
|
* 落盘那一侧对单侧内容设了上限(`runtime/src/turn-steps.ts` 的
|
|
215
171
|
* `MAX_DIFF_SIDE_BYTES`),超了的新旧内容一份都不存、只留路径和这一格。
|
|
@@ -239,11 +195,11 @@ interface ToolDisplayItem {
|
|
|
239
195
|
result?: string;
|
|
240
196
|
/**
|
|
241
197
|
* 执行**期间**已经吐出来的输出(尾部若干行)。只在 `status === 'pending'` 时有值 ——
|
|
242
|
-
* `tool-result`
|
|
198
|
+
* `tool-result` 到达时清掉,由最终 `result` 顶上。
|
|
243
199
|
*
|
|
244
|
-
* 和 `result`
|
|
245
|
-
*
|
|
246
|
-
*
|
|
200
|
+
* 和 `result` 是**两个通道**且口径不同:这一份是「现在跑到哪了」的活尾巴,已经剥过
|
|
201
|
+
* ANSI、折叠过 `\r`、只保留尾部(见 protocol 的 `tool-output-delta`);`result` 是
|
|
202
|
+
* 喂给模型的那份最终文本。渲染层自己决定尾巴里再取几行。
|
|
247
203
|
*/
|
|
248
204
|
liveOutput?: string;
|
|
249
205
|
/**
|
|
@@ -277,21 +233,13 @@ type HistoryItem = HistoryItemWithoutId & {
|
|
|
277
233
|
/**
|
|
278
234
|
* 流式事件 reducer —— 把 AgentEvent 折叠进 pending 消息列表。
|
|
279
235
|
*
|
|
280
|
-
*
|
|
281
|
-
* 1. text-delta 曾经 map 到**所有**文本项上,工具调用之后的文本会被追加进
|
|
282
|
-
* 第一段文本里,顺序错乱(文本跑到工具调用前面)。
|
|
283
|
-
* 2. tool-result 曾经把**所有** pending 工具一次性标记 done 并塞同一个 result,
|
|
284
|
-
* 多工具并行时结果串台。
|
|
285
|
-
* 3. reasoning-delta 曾经覆盖而非累加,思考过程只剩最后一个 chunk 的碎片。
|
|
236
|
+
* 抽成纯函数是为了能单测:不改入参,返回新数组。
|
|
286
237
|
*
|
|
287
|
-
* 事件类型来自 @epoch-agent/protocol
|
|
288
|
-
*
|
|
289
|
-
* 而且协议里没有 callId,只能靠「toolName + 调用顺序」猜工具结果归属。现在
|
|
290
|
-
* callId 是一等字段,直接精确认领。
|
|
238
|
+
* 事件类型来自 @epoch-agent/protocol,`callId` 是一等字段,工具结果按它精确认领
|
|
239
|
+
* (不靠「toolName + 调用顺序」猜 —— 那样并行工具会串台)。
|
|
291
240
|
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
* 浏览器端能原样用 —— 而它修掉的那三个语义错误,web 一样会踩。
|
|
241
|
+
* 落在 `@epoch-agent/view` 而不是 `tui`:它产出的是结构化 display item,
|
|
242
|
+
* 格式化在组件里、**并不绑终端**,浏览器端能原样用。
|
|
295
243
|
*/
|
|
296
244
|
|
|
297
245
|
interface StreamReduceResult {
|
|
@@ -299,44 +247,31 @@ interface StreamReduceResult {
|
|
|
299
247
|
/** 累加的 reasoning 文本(供状态栏显示) */
|
|
300
248
|
thought: string;
|
|
301
249
|
/**
|
|
302
|
-
* 末尾那个工具组**已经封口**了 —— 下一次 `tool-call`
|
|
303
|
-
*
|
|
304
|
-
* ## 它治的是「收口那一刻屏幕上的顺序和刷新之后不一样」
|
|
305
|
-
*
|
|
306
|
-
* `turn-complete` 是一个 ReAct 步的结束(`core/src/agent/loop.ts`,判据在
|
|
307
|
-
* `runtime/src/turn-steps.ts` 的文件头)。以前这一帧在 `applyStreamEvent` 里
|
|
308
|
-
* 是**空操作**,于是「第 1 步调了 todo、第 2 步调了 schedule_draft」这两次调用
|
|
309
|
-
* 被并进**同一个** `tool_group`。
|
|
310
|
-
*
|
|
311
|
-
* 而落盘是**一步一行**、回放按行走(`web/src/state/replay.ts` 的 `pushAssistant`),
|
|
312
|
-
* 刷出来的是两个分开的工具组。两条路对同一轮给出两种形状,后果不是「长得不一样」:
|
|
250
|
+
* 末尾那个工具组**已经封口**了 —— 下一次 `tool-call` 要新起一组。
|
|
313
251
|
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
252
|
+
* 少了这个封口,跨步的两次工具调用会被并进**同一个** `tool_group`,而落盘是
|
|
253
|
+
* **一步一行**、回放按行走(`web/src/state/replay.ts` 的 `pushAssistant`)——
|
|
254
|
+
* 两条路对同一轮给出两种形状。后果不只是「长得不一样」:步与步之间那几段思考
|
|
255
|
+
* 按 `pending.length` 定位(`web/src/state/thought-parts.ts` 的 `trackThought`),
|
|
256
|
+
* 两个组合并后它们撞在同一个下标上,还一起落到合并后的工具组后面;而收口那一刻
|
|
257
|
+
* (`session-state: idle`)照的也是这份合并过的 pending —— 同一轮在收口后、
|
|
258
|
+
* 切走切回后、刷新后长成三种样子。
|
|
320
259
|
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
* 往展示项里加「步边界」等于新增一种展示项,两个宿主都得跟着改(判据在
|
|
324
|
-
* `notice` 那一支的注释末尾);而这两条路真正需要的只有一句话:
|
|
325
|
-
* **这一步的工具已经调完了**。它就落在这个标志上,不落在任何会被渲染的项上。
|
|
260
|
+
* 它是一个标志而不是一种展示项:加一种「步边界」展示项等于两个宿主都得改,而
|
|
261
|
+
* 这两条路真正需要的只有一句话 —— **这一步的工具已经调完了**。
|
|
326
262
|
*
|
|
327
263
|
* ⚠️ **同一步之内的连续调用照旧合并**(`tool-call` 那一支的判据是
|
|
328
|
-
* `!state.sealed
|
|
329
|
-
*
|
|
330
|
-
* 带着它那一步的全部 `toolCalls`)。
|
|
264
|
+
* `!state.sealed`):一次请求里并行调三个工具仍是一张卡,回放的「一步一行」
|
|
265
|
+
* 也是同一个口径(一条 assistant 行带着那一步的全部 `toolCalls`)。
|
|
331
266
|
*/
|
|
332
267
|
sealed?: boolean;
|
|
333
268
|
}
|
|
334
269
|
/**
|
|
335
|
-
* 这一帧信封上、`AgentEvent`
|
|
270
|
+
* 这一帧信封上、`AgentEvent` 里没有的东西(今天只有 `seq` 和 `at`,见
|
|
271
|
+
* `HistoryItemBase` 那两格)。
|
|
336
272
|
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
* 会动到 TUI 那边所有调用点 —— 它一个都不传,正好什么都不用改。
|
|
273
|
+
* 做成一个对象而不是第三个位置参数:以后要透新字段不必动所有调用点,
|
|
274
|
+
* 而 TUI 一个都不传,正好什么都不用改。
|
|
340
275
|
*/
|
|
341
276
|
interface StreamEventMeta {
|
|
342
277
|
/** `WireEnvelope.seq`。TUI 不传(它吃的是裸事件,没有信封) */
|
|
@@ -351,12 +286,11 @@ interface StreamEventMeta {
|
|
|
351
286
|
* 以及末尾不是工具组时的 `turn-complete`)原样返回同一个引用,调用方可以用
|
|
352
287
|
* `next === prev` 判断。
|
|
353
288
|
*
|
|
354
|
-
* ⚠️ **`turn-complete`
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
* 是两个问题,别拿后者当前者。
|
|
289
|
+
* ⚠️ **`turn-complete` 可能返回新对象**:它要封口末尾那个工具组,但不改
|
|
290
|
+
* `pending` 数组本身(引用不变),只翻一个标志 —— 「`pending` 变了没有」和
|
|
291
|
+
* 「`state` 变了没有」在这一帧上是两个问题,别拿后者当前者。
|
|
358
292
|
*
|
|
359
|
-
* `meta` 只有浏览器传(信封上的 `seq`)。**它只在新建项时落一次** ——
|
|
293
|
+
* `meta` 只有浏览器传(信封上的 `seq` / `at`)。**它只在新建项时落一次** ——
|
|
360
294
|
* 追加进已有项(正文的第二个增量、并进同一个工具组的第二次调用)时不覆盖,
|
|
361
295
|
* 否则锚点会跟着最后一帧往后漂,而回退要的是「从哪儿开始」。
|
|
362
296
|
*/
|
|
@@ -365,67 +299,36 @@ declare function applyStreamEvent(state: StreamReduceResult, event: AgentEvent,
|
|
|
365
299
|
/**
|
|
366
300
|
* 行级 diff —— 纯函数,可单测。
|
|
367
301
|
*
|
|
368
|
-
* 协议里的 `diff` 事件带 `oldContent` / `newContent
|
|
369
|
-
* [protocol/events.ts](../../protocol/src/events.ts) 的 `type: 'diff'
|
|
370
|
-
* 旧版 TUI 把它渲染成一行「📝 补丁预览: a.ts」,两段内容直接扔了。
|
|
302
|
+
* 协议里的 `diff` 事件带 `oldContent` / `newContent`
|
|
303
|
+
* ([protocol/events.ts](../../protocol/src/events.ts) 的 `type: 'diff'`)。
|
|
371
304
|
*
|
|
372
305
|
* 用经典 LCS 动态规划,**但有上限**:DP 表是 O(n·m),几千行的文件会吃掉几十 MB
|
|
373
306
|
* 并且卡住渲染线程。超限就退回「整份 N 行 → M 行」的摘要 —— 少显示细节可以接受,
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
* ⚠️ **这一处改的是 view,两个宿主跟着一起变**(`tui` / `web` 用的是同一份,
|
|
387
|
-
* 正是本文件存在的理由)—— 别在某一侧再改一遍。
|
|
388
|
-
*
|
|
389
|
-
* ## 为什么住在 view 而不是 tui / web
|
|
390
|
-
*
|
|
391
|
-
* `diffLines` 原来在 `tui/src/streaming/`、`collapseContext` 原来在
|
|
392
|
-
* `tui/src/components/messages/diff-message.tsx`。Track B 落地时因为不能 import
|
|
393
|
-
* tui(ink / react 的 peer 会跟进浏览器包)而在 web 里各抄了一份**逐字副本**
|
|
394
|
-
* (连 `CONTEXT_LINES = 1` 都一样)。两份的漂移是真风险:改了一边忘了另一边,
|
|
395
|
-
* 终端和浏览器就会对同一个补丁画出不同的行数,而两边的用例各自都是绿的。
|
|
396
|
-
* 这里是两边都能依赖的地方(reducer 就在隔壁),并且这个文件**零依赖**——
|
|
397
|
-
* 没有 `node:`、没有 DOM、没有 React,符合本包的分界线。
|
|
398
|
-
*
|
|
399
|
-
* 留在各自组件里的**只有渲染约束**:终端没有滚动条,折叠完还要按可用高度硬裁到
|
|
400
|
-
* 固定行数并打一句「还有 N 行」;浏览器给 `max-height` + 内部滚动,一行不丢。
|
|
401
|
-
* 那不是 diff 语义,搬上来反而会让本包去关心它不该知道的终端高度。
|
|
307
|
+
* 界面卡死不行。上限只量**剥掉公共前后缀之后那一段**:公共前后缀一定可以出现在某个
|
|
308
|
+
* 最优 LCS 里,先摘掉它们、算完中间再拼回去得到的就是完整解。
|
|
309
|
+
*
|
|
310
|
+
* ⚠️ **这一处改的是 view,两个宿主跟着一起变**(`tui` / `web` 用的是同一份,正是本
|
|
311
|
+
* 文件存在的理由)—— 别在某一侧再改一遍。
|
|
312
|
+
*
|
|
313
|
+
* 住在 view 而不是 tui / web:web 不能 import tui(ink / react 的 peer 会跟进浏览器
|
|
314
|
+
* 包),两边各存一份副本的话改了一边忘了另一边,同一个补丁在终端和浏览器上画出不同
|
|
315
|
+
* 的行数,而两边用例各自都是绿的。本文件**零依赖** —— 没有 `node:`、没有 DOM、没有
|
|
316
|
+
* React,符合本包的分界线;留在各自组件里的**只有渲染约束**(终端没有滚动条,折叠完
|
|
317
|
+
* 还要按可用高度硬裁并打一句「还有 N 行」;浏览器给 `max-height` + 内部滚动),那不是
|
|
318
|
+
* diff 语义。
|
|
402
319
|
*/
|
|
403
320
|
/**
|
|
404
321
|
* 超过这个行数就不做逐行 diff。
|
|
405
322
|
*
|
|
406
|
-
* ⚠️
|
|
407
|
-
* 改的,判据在文件头)。
|
|
408
|
-
*
|
|
409
|
-
* 400 那个值当初是按最坏情况定的:DP 表是 O(n·m),几千行要几十 MB 并且卡住渲染
|
|
410
|
-
* 线程 —— **那句话本身没错,取值偏保守了一个数量级**。本机实测(照抄本文件的
|
|
411
|
-
* `lcsTable`,含建表和回溯):
|
|
412
|
-
*
|
|
413
|
-
* | 行数 | 小改动(10 处) | 整份重写 | DP 表 |
|
|
414
|
-
* | ---- | --------------- | -------- | ------ |
|
|
415
|
-
* | 400 | 1.9ms | 0.4ms | 0.6MB |
|
|
416
|
-
* | 600 | 1.9ms | 1.0ms | 1.4MB |
|
|
417
|
-
* | 2000 | 15.3ms | 7.8ms | 16MB |
|
|
418
|
-
* | 3000 | 34.2ms | 21.7ms | 36MB |
|
|
419
|
-
* | 5000 | 106ms | 51ms | 100MB |
|
|
323
|
+
* ⚠️ **量的是「剥掉公共前后缀之后剩下的那一段」**,不是整份文件的行数。
|
|
420
324
|
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
325
|
+
* DP 表是 O(n·m) —— 单张卡 2000 行约 15ms / 16MB,还在一帧 `useMemo` 的预算里;再往
|
|
326
|
+
* 上内存按 n² 涨,那一步该换算法而不是继续抬这个数。⚠️ **别只看单张卡**:一屏十几张
|
|
327
|
+
* 补丁卡时这个数是乘起来的。
|
|
423
328
|
*
|
|
424
|
-
* ⚠️
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
* 是**服务端发不发内容**的线 —— 它比这一条宽得多。两个数**不需要相等**
|
|
428
|
-
* (发得过来不等于画得动),但知道它们差多少,才判得出「发过来却被丢掉」有多少。
|
|
329
|
+
* ⚠️ 和上游那个闸门对一下:core 的 `MAX_FILE_BYTES = 128KB`(约三千行)是**服务端发
|
|
330
|
+
* 不发内容**的线,它比这一条宽得多。两个数**不需要相等**(发得过来不等于画得动),
|
|
331
|
+
* 但知道它们差多少,才判得出「发过来却被丢掉」有多少。
|
|
429
332
|
*/
|
|
430
333
|
declare const DIFF_MAX_LINES = 2000;
|
|
431
334
|
type DiffLineKind = 'add' | 'remove' | 'context';
|
|
@@ -451,14 +354,9 @@ interface DiffAligned extends DiffSides {
|
|
|
451
354
|
*
|
|
452
355
|
* ## 这一支上**刻意没有** `added` / `removed`
|
|
453
356
|
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
* 糟得多,所以它在这一支上**在结构上就不存在**,不是「算了但不画」
|
|
458
|
-
* (同 `thread/patch.tsx` 的 `PatchShape` 那条判据)。
|
|
459
|
-
*
|
|
460
|
-
* 要看这一份到底改了什么,得让 `lines` 那一支回来 —— 也就是把要逐行比的那一段
|
|
461
|
-
* 压到 {@link DIFF_MAX_LINES} 以内。
|
|
357
|
+
* 装 `b.length` / `a.length` 的话,宿主会把它画成头部的 `+N −M`:「1000 行的文件改
|
|
358
|
+
* 2 行」显示成 `+1000 −1000`。一个算错的数字比没有数字糟得多,所以它在这一支上**在
|
|
359
|
+
* 结构上就不存在**,不是「算了但不画」(同 `thread/patch.tsx` 的 `PatchShape`)。
|
|
462
360
|
*/
|
|
463
361
|
interface DiffTruncated extends DiffSides {
|
|
464
362
|
truncated: true;
|
|
@@ -467,12 +365,11 @@ type DiffResult = DiffAligned | DiffTruncated;
|
|
|
467
365
|
/**
|
|
468
366
|
* 比较两段文本,产出带 add / remove / context 标记的行序列。
|
|
469
367
|
*
|
|
470
|
-
* 内容缺失(undefined)时按空串处理:新建文件只有 newContent,删除只有
|
|
471
|
-
*
|
|
368
|
+
* 内容缺失(undefined)时按空串处理:新建文件只有 newContent,删除只有 oldContent,
|
|
369
|
+
* 两种都该显示成纯增 / 纯删而不是空 diff。
|
|
472
370
|
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
* 剥只是省掉一次没必要的大表,不是「只给中间那一截」。
|
|
371
|
+
* 返回的 `lines` **永远是整份文件的**(公共前后缀作为 context 拼在前后)—— 剥只是
|
|
372
|
+
* 省掉一次没必要的大表,不是「只给中间那一截」。
|
|
476
373
|
*/
|
|
477
374
|
declare function diffLines(oldContent?: string, newContent?: string): DiffResult;
|
|
478
375
|
/** 变更行前后各保留几行上下文 */
|
|
@@ -480,10 +377,8 @@ declare const DIFF_CONTEXT_LINES = 1;
|
|
|
480
377
|
/**
|
|
481
378
|
* 把全量行序列压成「变更行 + 上下文」,被跳过的位置插入一个 `null` 当作省略号锚点。
|
|
482
379
|
*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
*
|
|
486
|
-
* 抽成纯函数是为了能直接对着数组断言,不用把组件渲染出来数行数。
|
|
380
|
+
* 补丁大多是几行改动散落在几百行文件里,全量打印等于把屏幕冲掉。省略的位置必须留
|
|
381
|
+
* 锚点让调用方画出 `⋯` —— **不静默吞掉**。抽成纯函数是为了直接对着数组断言。
|
|
487
382
|
*/
|
|
488
383
|
declare function collapseContext(lines: readonly DiffLine[], context?: number): Array<DiffLine | null>;
|
|
489
384
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@epoch-agent/view",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "epoch-agent 展示层纯逻辑:AgentEvent → 可渲染 item,tui 与 web 共用",
|
|
6
6
|
"repository": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"dist"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@epoch-agent/protocol": "4.
|
|
25
|
+
"@epoch-agent/protocol": "4.1.1"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"tsup": "^8.3.6",
|