@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 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` 会自己排好顺序。
@@ -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
+ }