@epoch-agent/view 0.1.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/LICENSE +219 -0
- package/README.md +42 -0
- package/dist/index.d.ts +257 -0
- package/dist/index.js +193 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
|
203
|
+
|
|
204
|
+
--------------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
epoch-agent
|
|
207
|
+
Copyright 2024-2026 bowen
|
|
208
|
+
|
|
209
|
+
Portions of this software are derived from the Gemini CLI
|
|
210
|
+
(https://github.com/google-gemini/gemini-cli), Copyright 2025 Google LLC,
|
|
211
|
+
licensed under the Apache License, Version 2.0. Those files have been modified;
|
|
212
|
+
each one retains its original copyright notice and SPDX-License-Identifier
|
|
213
|
+
header. See the NOTICE file in the project source repository for the complete
|
|
214
|
+
list of derived files.
|
|
215
|
+
|
|
216
|
+
This software also quotes, with attribution, two lines of prose from OpenAI
|
|
217
|
+
Codex (https://github.com/openai/codex), Copyright 2025 OpenAI, licensed under
|
|
218
|
+
the Apache License, Version 2.0. No source file is a derived work of Codex.
|
|
219
|
+
See the NOTICE file for details.
|
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @epoch-agent/view
|
|
2
|
+
|
|
3
|
+
展示层纯逻辑。把 `AgentEvent` 折叠成结构化的可渲染 item。
|
|
4
|
+
|
|
5
|
+
- ✅ **做**:事件 → item 的折叠(`applyStreamEvent`)、diff 计算、这些 item 的类型
|
|
6
|
+
- ❌ **不做**:任何组件、任何格式化、任何 I/O。既没有 `node:`,也没有 DOM
|
|
7
|
+
- **依赖**:只有 [protocol](../protocol)
|
|
8
|
+
|
|
9
|
+
## 存在的理由
|
|
10
|
+
|
|
11
|
+
只有一条:[tui](../tui) 和 [web](../web) 必须共用同一份折叠逻辑。它修掉的三个语义错误
|
|
12
|
+
——文本顺序、并行工具串台、reasoning 覆盖——两边都会踩,抄一份就是两份漂移。
|
|
13
|
+
|
|
14
|
+
## 导出
|
|
15
|
+
|
|
16
|
+
| 导出 | 说明 |
|
|
17
|
+
| ----------------------------------------- | ---------------------------------------------------- |
|
|
18
|
+
| `applyStreamEvent(state, event)` | 折叠一个 `AgentEvent`,返回新的 item 列表 + 变更信息 |
|
|
19
|
+
| `diffLines(before, after)` | 行级 diff |
|
|
20
|
+
| `collapseContext(lines)` | 把连续的未改动行折叠成 `@@` 摘要 |
|
|
21
|
+
| `DIFF_CONTEXT_LINES` / `DIFF_MAX_LINES` | 折叠与截断的阈值 |
|
|
22
|
+
| `HistoryItem*` / `ToolDisplayItem` 等类型 | item 的判别联合 |
|
|
23
|
+
|
|
24
|
+
## 分界线
|
|
25
|
+
|
|
26
|
+
> ⚠️ **这里进了 React 就说明分界画错了。** 真到那一步,该拆成 `view-core` + 各自的
|
|
27
|
+
> 组件层,而不是往这里塞组件。
|
|
28
|
+
|
|
29
|
+
`protocol` 的 `AgentEvent` 加成员时,这里的 reducer 要跟着处理,否则 TUI 和 Web 会
|
|
30
|
+
静默丢事件。
|
|
31
|
+
|
|
32
|
+
## 开发
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pnpm --filter @epoch-agent/view test
|
|
36
|
+
pnpm --filter @epoch-agent/view build # tui / web 都从 dist 解析它,改完必须 build
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
> ⚠️ [web](../web) 和 [tui](../tui) 是按 package.json 的 `exports` 从**本包的 `dist/`**
|
|
40
|
+
> 解析的。只改源码不 build,vite / tsx 那边看到的还是上一次的产物——症状是
|
|
41
|
+
> `vite build` 报 `MISSING_EXPORT`,或者新加的字段在界面上永远不出现。
|
|
42
|
+
> 根目录的 `pnpm build` 会自己排好顺序。
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { ToolArtifact, AgentEvent } from '@epoch-agent/protocol';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 展示项类型 — 基于 Google Gemini CLI types.ts
|
|
5
|
+
* @license Apache-2.0 (adapted from google-gemini/gemini-cli)
|
|
6
|
+
* Copyright 2025 Google LLC
|
|
7
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
8
|
+
* Modifications Copyright 2024-2026 bowen
|
|
9
|
+
*
|
|
10
|
+
* ---
|
|
11
|
+
*
|
|
12
|
+
* 这些类型原来住在 `packages/tui/src/types/message-types.ts`。提到 `@epoch-agent/view`
|
|
13
|
+
* 是因为浏览器端要用同一套(方案 20 §7):它们产出的是**结构化 display item**
|
|
14
|
+
* (`tool_group` / `diff` / 文本段),格式化在组件里,并不绑终端。
|
|
15
|
+
*
|
|
16
|
+
* `web` 不能依赖 `tui`(ink / react peer 会跟进浏览器包),所以落点是一个
|
|
17
|
+
* 两边都能依赖、且**只依赖 protocol** 的新包。晚一天提取,web 就只能先抄一份,
|
|
18
|
+
* 然后两份永远漂移。
|
|
19
|
+
*
|
|
20
|
+
* 本文件**不许**出现 node: 或 DOM 的东西 —— view 的 tsconfig 既不带
|
|
21
|
+
* `@types/node` 也不带 DOM lib,写了当场编译不过。
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
interface HistoryItemBase {
|
|
25
|
+
text?: string;
|
|
26
|
+
/**
|
|
27
|
+
* 这一项**从哪一帧开始**的广播序号(`WireEnvelope.seq`)。
|
|
28
|
+
*
|
|
29
|
+
* 只是**锚点**,不渲染成文本:浏览器把它落在 `<li class="ev" data-seq>` 上,
|
|
30
|
+
* 三处真在用 —— 检视面板的产物行(「这个文件是哪一步产出的」)、回退(检查点)
|
|
31
|
+
* 的目标、URL 锚点。设计稿决定 2 在 2026-08-11 撤掉的是「每行都印一个三位数」
|
|
32
|
+
* 那个**形态**,不是这个字段。
|
|
33
|
+
*
|
|
34
|
+
* ⚠️ 三处它**没有值**,调用方必须当它可能不存在:
|
|
35
|
+
* 1. TUI —— 那边直接吃 `AgentEvent`,压根没有信封,也不需要锚点;
|
|
36
|
+
* 2. 回放(`GET /messages` 折出来的历史)—— 落盘的消息里没有 seq;
|
|
37
|
+
* 3. 一项由多帧合成时,取的是**第一帧**(正文的第一个增量、工具组的第一次
|
|
38
|
+
* 调用)。锚点要回答的是「从哪儿开始的」,用最后一帧会让回退目标往后漂。
|
|
39
|
+
*/
|
|
40
|
+
seq?: number;
|
|
41
|
+
}
|
|
42
|
+
type HistoryItemUser = HistoryItemBase & {
|
|
43
|
+
type: 'user';
|
|
44
|
+
text: string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* 模型的回复正文。
|
|
48
|
+
*
|
|
49
|
+
* 判别值原来是 `'gemini'` —— 那是从 gemini-cli 抄过来的历史命名,在一个跑
|
|
50
|
+
* deepseek / anthropic / openai 的引擎里叫这个名字纯属误导。改成 `'assistant'`,
|
|
51
|
+
* 与 `EpochMessageRole` 对齐。
|
|
52
|
+
*/
|
|
53
|
+
type HistoryItemAssistant = HistoryItemBase & {
|
|
54
|
+
type: 'assistant';
|
|
55
|
+
text: string;
|
|
56
|
+
};
|
|
57
|
+
type HistoryItemInfo = HistoryItemBase & {
|
|
58
|
+
type: 'info';
|
|
59
|
+
text: string;
|
|
60
|
+
secondaryText?: string;
|
|
61
|
+
source?: string;
|
|
62
|
+
icon?: string;
|
|
63
|
+
color?: string;
|
|
64
|
+
marginBottom?: number;
|
|
65
|
+
};
|
|
66
|
+
type HistoryItemError = HistoryItemBase & {
|
|
67
|
+
type: 'error';
|
|
68
|
+
text: string;
|
|
69
|
+
};
|
|
70
|
+
type HistoryItemWarning = HistoryItemBase & {
|
|
71
|
+
type: 'warning';
|
|
72
|
+
text: string;
|
|
73
|
+
};
|
|
74
|
+
type HistoryItemModel = HistoryItemBase & {
|
|
75
|
+
type: 'model';
|
|
76
|
+
model: string;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* 模型的思考原文。
|
|
80
|
+
*
|
|
81
|
+
* **默认不产出**:运行期只显示 `Thinking…` 状态行,reasoning 在内存里累加。
|
|
82
|
+
* 只有 `/thinking` 打开时,本轮结束才把它作为一条历史项落下来 ——
|
|
83
|
+
* 边流边显示等于每个 token 都触发一次整帧重绘,而且没人读得下去。
|
|
84
|
+
*/
|
|
85
|
+
type HistoryItemThinking = HistoryItemBase & {
|
|
86
|
+
type: 'thinking';
|
|
87
|
+
thought: string;
|
|
88
|
+
};
|
|
89
|
+
/** 补丁预览。oldContent / newContent 缺一边表示新建或删除 */
|
|
90
|
+
type HistoryItemDiff = HistoryItemBase & {
|
|
91
|
+
type: 'diff';
|
|
92
|
+
filePath: string;
|
|
93
|
+
oldContent?: string;
|
|
94
|
+
newContent?: string;
|
|
95
|
+
};
|
|
96
|
+
type HistoryItemToolGroup = HistoryItemBase & {
|
|
97
|
+
type: 'tool_group';
|
|
98
|
+
tools: ToolDisplayItem[];
|
|
99
|
+
borderTop?: boolean;
|
|
100
|
+
borderBottom?: boolean;
|
|
101
|
+
borderColor?: string;
|
|
102
|
+
borderDimColor?: boolean;
|
|
103
|
+
};
|
|
104
|
+
type HistoryItemToolDisplayGroup = HistoryItemBase & {
|
|
105
|
+
type: 'tool_display_group';
|
|
106
|
+
tools: ToolDisplayItem[];
|
|
107
|
+
};
|
|
108
|
+
interface ToolDisplayItem {
|
|
109
|
+
callId: string;
|
|
110
|
+
toolName: string;
|
|
111
|
+
request: unknown;
|
|
112
|
+
status: 'pending' | 'done' | 'error';
|
|
113
|
+
result?: string;
|
|
114
|
+
/**
|
|
115
|
+
* 执行**期间**已经吐出来的输出(尾部若干行)。只在 `status === 'pending'` 时有值 ——
|
|
116
|
+
* `tool-result` 到达时会被清掉,由最终 `result` 顶上。
|
|
117
|
+
*
|
|
118
|
+
* 和 `result` 是**两个通道**且口径不同:这一份是「现在跑到哪了」的活尾巴,
|
|
119
|
+
* 已经剥过 ANSI、折叠过 `\r`、只保留尾部(见 protocol 的 `tool-output-delta`);
|
|
120
|
+
* `result` 是喂给模型的那份最终文本。渲染层自己决定尾巴里再取几行。
|
|
121
|
+
*/
|
|
122
|
+
liveOutput?: string;
|
|
123
|
+
/**
|
|
124
|
+
* 工具产出的二进制(截图 / 音频 / 文件)。
|
|
125
|
+
*
|
|
126
|
+
* 和 `result` 是**两个通道**:`result` 是喂给模型的那份文本(里面已经含了每个
|
|
127
|
+
* artifact 的 summary),这里是给人看的那份 —— 画缩略图行、按 Ctrl+O 打开。
|
|
128
|
+
* 见 protocol/src/events.ts 里 `tool-result` 的注释。
|
|
129
|
+
*/
|
|
130
|
+
artifacts?: ToolArtifact[];
|
|
131
|
+
}
|
|
132
|
+
type HistoryItemCompression = HistoryItemBase & {
|
|
133
|
+
type: 'compression';
|
|
134
|
+
compressedCount: number;
|
|
135
|
+
};
|
|
136
|
+
type HistoryItemSessionSummary = HistoryItemBase & {
|
|
137
|
+
type: 'session_summary';
|
|
138
|
+
duration: string;
|
|
139
|
+
};
|
|
140
|
+
type HistoryItemHint = HistoryItemBase & {
|
|
141
|
+
type: 'hint';
|
|
142
|
+
text: string;
|
|
143
|
+
};
|
|
144
|
+
/** 联合类型(无 ID) */
|
|
145
|
+
type HistoryItemWithoutId = HistoryItemUser | HistoryItemAssistant | HistoryItemInfo | HistoryItemError | HistoryItemWarning | HistoryItemModel | HistoryItemThinking | HistoryItemDiff | HistoryItemToolGroup | HistoryItemToolDisplayGroup | HistoryItemCompression | HistoryItemSessionSummary | HistoryItemHint;
|
|
146
|
+
/** 完整历史项(带 ID) */
|
|
147
|
+
type HistoryItem = HistoryItemWithoutId & {
|
|
148
|
+
id: number;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 流式事件 reducer —— 把 AgentEvent 折叠进 pending 消息列表。
|
|
153
|
+
*
|
|
154
|
+
* 抽成纯函数是为了能单测,也为了修掉原先散在 App.handleSubmit 里的三个语义错误:
|
|
155
|
+
* 1. text-delta 曾经 map 到**所有**文本项上,工具调用之后的文本会被追加进
|
|
156
|
+
* 第一段文本里,顺序错乱(文本跑到工具调用前面)。
|
|
157
|
+
* 2. tool-result 曾经把**所有** pending 工具一次性标记 done 并塞同一个 result,
|
|
158
|
+
* 多工具并行时结果串台。
|
|
159
|
+
* 3. reasoning-delta 曾经覆盖而非累加,思考过程只剩最后一个 chunk 的碎片。
|
|
160
|
+
*
|
|
161
|
+
* 事件类型来自 @epoch-agent/protocol。以前 TUI 自己定义一份 StreamEvent,
|
|
162
|
+
* 字段名和引擎不一样(args/isError vs input/success),靠 cli 里一段手写适配缝合;
|
|
163
|
+
* 而且协议里没有 callId,只能靠「toolName + 调用顺序」猜工具结果归属。现在
|
|
164
|
+
* callId 是一等字段,直接精确认领。
|
|
165
|
+
*
|
|
166
|
+
* 本文件原来住在 `packages/tui/src/streaming/reducer.ts`。提到 `@epoch-agent/view`
|
|
167
|
+
* 是因为它产出的是结构化 display item,格式化在组件里、**并不绑终端**,
|
|
168
|
+
* 浏览器端能原样用 —— 而它修掉的那三个语义错误,web 一样会踩。
|
|
169
|
+
*/
|
|
170
|
+
|
|
171
|
+
interface StreamReduceResult {
|
|
172
|
+
pending: HistoryItemWithoutId[];
|
|
173
|
+
/** 累加的 reasoning 文本(供状态栏显示) */
|
|
174
|
+
thought: string;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* 这一帧信封上、`AgentEvent` 里没有的东西。
|
|
178
|
+
*
|
|
179
|
+
* 今天只有 `seq`(导轨的锚点,见 `HistoryItemBase.seq`)。做成一个对象而不是
|
|
180
|
+
* 第三个位置参数,是因为下一个要透的多半是 `ts`(回合耗时),而那时候再改签名
|
|
181
|
+
* 会动到 TUI 那边所有调用点 —— 它一个都不传,正好什么都不用改。
|
|
182
|
+
*/
|
|
183
|
+
interface StreamEventMeta {
|
|
184
|
+
/** `WireEnvelope.seq`。TUI 不传(它吃的是裸事件,没有信封) */
|
|
185
|
+
seq?: number;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* 把一个事件折叠进当前状态。纯函数:不改入参,返回新数组。
|
|
189
|
+
*
|
|
190
|
+
* 不影响渲染的事件(turn-complete / finish / approval-request)原样返回同一个
|
|
191
|
+
* 引用,调用方可以用 `next === prev` 判断。
|
|
192
|
+
*
|
|
193
|
+
* `meta` 只有浏览器传(信封上的 `seq`)。**它只在新建项时落一次** ——
|
|
194
|
+
* 追加进已有项(正文的第二个增量、并进同一个工具组的第二次调用)时不覆盖,
|
|
195
|
+
* 否则锚点会跟着最后一帧往后漂,而回退要的是「从哪儿开始」。
|
|
196
|
+
*/
|
|
197
|
+
declare function applyStreamEvent(state: StreamReduceResult, event: AgentEvent, meta?: StreamEventMeta): StreamReduceResult;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 行级 diff —— 纯函数,可单测。
|
|
201
|
+
*
|
|
202
|
+
* 协议里的 `diff` 事件带 `oldContent` / `newContent`(
|
|
203
|
+
* [protocol/events.ts](../../protocol/src/events.ts) 的 `type: 'diff'`),
|
|
204
|
+
* 旧版 TUI 把它渲染成一行「📝 补丁预览: a.ts」,两段内容直接扔了。
|
|
205
|
+
*
|
|
206
|
+
* 用经典 LCS 动态规划,**但有上限**:DP 表是 O(n·m),几千行的文件会吃掉几十 MB
|
|
207
|
+
* 并且卡住渲染线程。超限就退回「N 行 → M 行」的摘要 —— 少显示细节可以接受,
|
|
208
|
+
* 界面卡死不行。
|
|
209
|
+
*
|
|
210
|
+
* ## 为什么住在 view 而不是 tui / web
|
|
211
|
+
*
|
|
212
|
+
* `diffLines` 原来在 `tui/src/streaming/`、`collapseContext` 原来在
|
|
213
|
+
* `tui/src/components/messages/diff-message.tsx`。Track B 落地时因为不能 import
|
|
214
|
+
* tui(ink / react 的 peer 会跟进浏览器包)而在 web 里各抄了一份**逐字副本**
|
|
215
|
+
* (连 `CONTEXT_LINES = 1` 都一样)。两份的漂移是真风险:改了一边忘了另一边,
|
|
216
|
+
* 终端和浏览器就会对同一个补丁画出不同的行数,而两边的用例各自都是绿的。
|
|
217
|
+
* 这里是两边都能依赖的地方(reducer 就在隔壁),并且这个文件**零依赖**——
|
|
218
|
+
* 没有 `node:`、没有 DOM、没有 React,符合本包的分界线。
|
|
219
|
+
*
|
|
220
|
+
* 留在各自组件里的**只有渲染约束**:终端没有滚动条,折叠完还要按可用高度硬裁到
|
|
221
|
+
* 固定行数并打一句「还有 N 行」;浏览器给 `max-height` + 内部滚动,一行不丢。
|
|
222
|
+
* 那不是 diff 语义,搬上来反而会让本包去关心它不该知道的终端高度。
|
|
223
|
+
*/
|
|
224
|
+
/** 超过这个行数就不做逐行 diff */
|
|
225
|
+
declare const DIFF_MAX_LINES = 400;
|
|
226
|
+
type DiffLineKind = 'add' | 'remove' | 'context';
|
|
227
|
+
interface DiffLine {
|
|
228
|
+
kind: DiffLineKind;
|
|
229
|
+
text: string;
|
|
230
|
+
}
|
|
231
|
+
interface DiffResult {
|
|
232
|
+
lines: DiffLine[];
|
|
233
|
+
added: number;
|
|
234
|
+
removed: number;
|
|
235
|
+
/** true = 内容太大,没有逐行比,`lines` 为空 */
|
|
236
|
+
truncated: boolean;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* 比较两段文本,产出带 add / remove / context 标记的行序列。
|
|
240
|
+
*
|
|
241
|
+
* 内容缺失(undefined)时按空串处理:新建文件只有 newContent,删除只有
|
|
242
|
+
* oldContent,两种都该显示成纯增 / 纯删而不是空 diff。
|
|
243
|
+
*/
|
|
244
|
+
declare function diffLines(oldContent?: string, newContent?: string): DiffResult;
|
|
245
|
+
/** 变更行前后各保留几行上下文 */
|
|
246
|
+
declare const DIFF_CONTEXT_LINES = 1;
|
|
247
|
+
/**
|
|
248
|
+
* 把全量行序列压成「变更行 + 上下文」,被跳过的位置插入一个 `null` 当作省略号锚点。
|
|
249
|
+
*
|
|
250
|
+
* coding agent 的补丁大多是几行改动散落在几百行文件里,全量打印等于把屏幕冲掉。
|
|
251
|
+
* 省略的位置必须留锚点,让调用方画出 `⋯` —— **不静默吞掉**。
|
|
252
|
+
*
|
|
253
|
+
* 抽成纯函数是为了能直接对着数组断言,不用把组件渲染出来数行数。
|
|
254
|
+
*/
|
|
255
|
+
declare function collapseContext(lines: readonly DiffLine[], context?: number): Array<DiffLine | null>;
|
|
256
|
+
|
|
257
|
+
export { DIFF_CONTEXT_LINES, DIFF_MAX_LINES, type DiffLine, type DiffLineKind, type DiffResult, type HistoryItem, type HistoryItemAssistant, type HistoryItemBase, type HistoryItemCompression, type HistoryItemDiff, type HistoryItemError, type HistoryItemHint, type HistoryItemInfo, type HistoryItemModel, type HistoryItemSessionSummary, type HistoryItemThinking, type HistoryItemToolDisplayGroup, type HistoryItemToolGroup, type HistoryItemUser, type HistoryItemWarning, type HistoryItemWithoutId, type StreamEventMeta, type StreamReduceResult, type ToolDisplayItem, applyStreamEvent, collapseContext, diffLines };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// src/reducer.ts
|
|
2
|
+
function anchor(meta) {
|
|
3
|
+
return meta?.seq !== void 0 ? { seq: meta.seq } : {};
|
|
4
|
+
}
|
|
5
|
+
var LIVE_TAIL_LINES = 20;
|
|
6
|
+
var LIVE_TAIL_CHARS = 4e3;
|
|
7
|
+
function tailOf(text) {
|
|
8
|
+
const lines = text.split("\n");
|
|
9
|
+
const kept = lines.length > LIVE_TAIL_LINES ? lines.slice(-LIVE_TAIL_LINES) : lines;
|
|
10
|
+
const joined = kept.join("\n");
|
|
11
|
+
return joined.length > LIVE_TAIL_CHARS ? joined.slice(-LIVE_TAIL_CHARS) : joined;
|
|
12
|
+
}
|
|
13
|
+
function patchTool(pending, callId, patch) {
|
|
14
|
+
for (let i = pending.length - 1; i >= 0; i--) {
|
|
15
|
+
const item = pending[i];
|
|
16
|
+
if (item?.type !== "tool_group") continue;
|
|
17
|
+
const idx = item.tools.findIndex((t) => t.callId === callId);
|
|
18
|
+
if (idx === -1) continue;
|
|
19
|
+
const tools = [...item.tools];
|
|
20
|
+
tools[idx] = patch(tools[idx]);
|
|
21
|
+
const next = [...pending];
|
|
22
|
+
next[i] = { ...item, tools };
|
|
23
|
+
return next;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
function applyStreamEvent(state, event, meta) {
|
|
28
|
+
const { pending, thought } = state;
|
|
29
|
+
switch (event.type) {
|
|
30
|
+
case "reasoning-delta":
|
|
31
|
+
return { pending, thought: thought + event.text };
|
|
32
|
+
case "text-delta": {
|
|
33
|
+
if (!event.text) return state;
|
|
34
|
+
const last = pending[pending.length - 1];
|
|
35
|
+
if (last?.type === "assistant") {
|
|
36
|
+
const next = [...pending];
|
|
37
|
+
next[next.length - 1] = { ...last, text: last.text + event.text };
|
|
38
|
+
return { pending: next, thought };
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
pending: [...pending, { type: "assistant", text: event.text, ...anchor(meta) }],
|
|
42
|
+
thought
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
case "tool-call": {
|
|
46
|
+
const tool = {
|
|
47
|
+
callId: event.callId,
|
|
48
|
+
toolName: event.toolName,
|
|
49
|
+
request: event.input,
|
|
50
|
+
status: "pending"
|
|
51
|
+
};
|
|
52
|
+
const last = pending[pending.length - 1];
|
|
53
|
+
if (last?.type === "tool_group") {
|
|
54
|
+
const next = [...pending];
|
|
55
|
+
next[next.length - 1] = { ...last, tools: [...last.tools, tool] };
|
|
56
|
+
return { pending: next, thought };
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
pending: [...pending, { type: "tool_group", tools: [tool], ...anchor(meta) }],
|
|
60
|
+
thought
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
case "tool-output-delta": {
|
|
64
|
+
if (!event.text) return state;
|
|
65
|
+
const next = patchTool(pending, event.callId, (tool) => ({
|
|
66
|
+
...tool,
|
|
67
|
+
liveOutput: tailOf((tool.liveOutput ?? "") + event.text)
|
|
68
|
+
}));
|
|
69
|
+
return next ? { pending: next, thought } : state;
|
|
70
|
+
}
|
|
71
|
+
case "tool-result": {
|
|
72
|
+
const next = patchTool(pending, event.callId, (tool) => {
|
|
73
|
+
const { liveOutput: _dropped, ...rest } = tool;
|
|
74
|
+
return {
|
|
75
|
+
...rest,
|
|
76
|
+
status: event.ok ? "done" : "error",
|
|
77
|
+
result: event.output,
|
|
78
|
+
// 空数组也不要落下去:`artifacts: []` 和「没有产物」在渲染上没区别,
|
|
79
|
+
// 但会让 `formatArtifact` 那一层多一次判空
|
|
80
|
+
...event.artifacts && event.artifacts.length > 0 ? { artifacts: event.artifacts } : {}
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
return next ? { pending: next, thought } : state;
|
|
84
|
+
}
|
|
85
|
+
case "diff":
|
|
86
|
+
return {
|
|
87
|
+
pending: [
|
|
88
|
+
...pending,
|
|
89
|
+
{
|
|
90
|
+
type: "diff",
|
|
91
|
+
filePath: event.filePath,
|
|
92
|
+
...event.oldContent !== void 0 ? { oldContent: event.oldContent } : {},
|
|
93
|
+
...event.newContent !== void 0 ? { newContent: event.newContent } : {},
|
|
94
|
+
...anchor(meta)
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
thought
|
|
98
|
+
};
|
|
99
|
+
case "error":
|
|
100
|
+
return {
|
|
101
|
+
pending: [...pending, { type: "error", text: event.message || "\u9519\u8BEF", ...anchor(meta) }],
|
|
102
|
+
thought
|
|
103
|
+
};
|
|
104
|
+
case "run-start":
|
|
105
|
+
case "finish":
|
|
106
|
+
case "usage":
|
|
107
|
+
case "turn-complete":
|
|
108
|
+
case "approval-request":
|
|
109
|
+
case "question":
|
|
110
|
+
return state;
|
|
111
|
+
default: {
|
|
112
|
+
return state;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/diff-lines.ts
|
|
118
|
+
var DIFF_MAX_LINES = 400;
|
|
119
|
+
function splitLines(text) {
|
|
120
|
+
if (text === "") return [];
|
|
121
|
+
const lines = text.split("\n");
|
|
122
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
123
|
+
return lines;
|
|
124
|
+
}
|
|
125
|
+
function lcsTable(a, b) {
|
|
126
|
+
const table = Array.from(
|
|
127
|
+
{ length: a.length + 1 },
|
|
128
|
+
() => new Uint32Array(b.length + 1)
|
|
129
|
+
);
|
|
130
|
+
for (let i = a.length - 1; i >= 0; i--) {
|
|
131
|
+
const row = table[i];
|
|
132
|
+
const next = table[i + 1];
|
|
133
|
+
for (let j = b.length - 1; j >= 0; j--) {
|
|
134
|
+
row[j] = a[i] === b[j] ? next[j + 1] + 1 : Math.max(next[j], row[j + 1]);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return table;
|
|
138
|
+
}
|
|
139
|
+
function diffLines(oldContent = "", newContent = "") {
|
|
140
|
+
const a = splitLines(oldContent);
|
|
141
|
+
const b = splitLines(newContent);
|
|
142
|
+
if (a.length > DIFF_MAX_LINES || b.length > DIFF_MAX_LINES) {
|
|
143
|
+
return { lines: [], added: b.length, removed: a.length, truncated: true };
|
|
144
|
+
}
|
|
145
|
+
const table = lcsTable(a, b);
|
|
146
|
+
const lines = [];
|
|
147
|
+
let added = 0;
|
|
148
|
+
let removed = 0;
|
|
149
|
+
let i = 0;
|
|
150
|
+
let j = 0;
|
|
151
|
+
while (i < a.length && j < b.length) {
|
|
152
|
+
if (a[i] === b[j]) {
|
|
153
|
+
lines.push({ kind: "context", text: a[i] });
|
|
154
|
+
i++;
|
|
155
|
+
j++;
|
|
156
|
+
} else if (table[i + 1][j] >= table[i][j + 1]) {
|
|
157
|
+
lines.push({ kind: "remove", text: a[i] });
|
|
158
|
+
removed++;
|
|
159
|
+
i++;
|
|
160
|
+
} else {
|
|
161
|
+
lines.push({ kind: "add", text: b[j] });
|
|
162
|
+
added++;
|
|
163
|
+
j++;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
for (; i < a.length; i++, removed++) lines.push({ kind: "remove", text: a[i] });
|
|
167
|
+
for (; j < b.length; j++, added++) lines.push({ kind: "add", text: b[j] });
|
|
168
|
+
return { lines, added, removed, truncated: false };
|
|
169
|
+
}
|
|
170
|
+
var DIFF_CONTEXT_LINES = 1;
|
|
171
|
+
function collapseContext(lines, context = DIFF_CONTEXT_LINES) {
|
|
172
|
+
const keep = /* @__PURE__ */ new Set();
|
|
173
|
+
lines.forEach((line, i) => {
|
|
174
|
+
if (line.kind === "context") return;
|
|
175
|
+
for (let k = i - context; k <= i + context; k++) {
|
|
176
|
+
if (k >= 0 && k < lines.length) keep.add(k);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
const out = [];
|
|
180
|
+
let skipping = false;
|
|
181
|
+
lines.forEach((line, i) => {
|
|
182
|
+
if (keep.has(i)) {
|
|
183
|
+
out.push(line);
|
|
184
|
+
skipping = false;
|
|
185
|
+
} else if (!skipping) {
|
|
186
|
+
out.push(null);
|
|
187
|
+
skipping = true;
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export { DIFF_CONTEXT_LINES, DIFF_MAX_LINES, applyStreamEvent, collapseContext, diffLines };
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@epoch-agent/view",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "epoch-agent 展示层纯逻辑:AgentEvent → 可渲染 item,tui 与 web 共用",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/Ddbor/epoch-agent.git"
|
|
9
|
+
},
|
|
10
|
+
"license": "Apache-2.0",
|
|
11
|
+
"author": "epoch-agent",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@epoch-agent/protocol": "0.1.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"tsup": "^8.3.6",
|
|
29
|
+
"typescript": "^5.8.3",
|
|
30
|
+
"vitest": "^3.0.0"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22.0.0"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"dev": "tsup --watch",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"typecheck": "tsc --noEmit"
|
|
40
|
+
}
|
|
41
|
+
}
|