@epoch-agent/view 4.1.1 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,13 +13,31 @@
13
13
 
14
14
  ## 导出
15
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 的判别联合 |
16
+ | 导出 | 说明 |
17
+ | --------------------------------------------------------- | -------------------------------------------------------------------------- |
18
+ | `applyStreamEvent(state, event)` | 折叠一个 `AgentEvent`,返回新的 item 列表 + 变更信息 |
19
+ | `activityOf(toolName)` | 工具名 → 人类动作类(`read` / `command` / `web` …)**(转发 protocol)** |
20
+ | `liveDetail(request)` | 从入参里挑一句人话(`description > command > path`…)**(转发 protocol)** |
21
+ | `runningToolActivity(tools)` / `currentActivity(pending)` | 此刻在跑的那条 → 类别 + 细节 |
22
+ | `ACTIVITY_KEYS` | 类别 → catalog key(epoch 两个宿主共用的唯一真源) |
23
+ | `diffLines(before, after)` | 行级 diff |
24
+ | `collapseContext(lines)` | 把连续的未改动行折叠成 `@@` 摘要 |
25
+ | `DIFF_CONTEXT_LINES` / `DIFF_MAX_LINES` | 折叠与截断的阈值(后者的口径见下) |
26
+ | `HistoryItem*` / `ToolDisplayItem` 等类型 | item 的判别联合 |
27
+
28
+ ## 「现在在干什么」(2026-11-09,2026-11-10 判据下移)
29
+
30
+ **类别与细节的判据住在 [protocol 的 activity.ts](../protocol/src/activity.ts)**:
31
+ 答这个问题的除 `tui` / `web`,还有网线另一头的第三方宿主(宿主桌宠、托盘、状态栏),
32
+ 而它们拿不到 `view`。`core` 发 `tool-call` 帧时用同一张表算好塞进 `activity` 字段,
33
+ 所以「`terminal` 算哪一类」全仓只有一个答案。这一层再导出一次,`tui` / `web` 的名字没变。
34
+
35
+ 留在这里的是**需要展示项形状**的那一半:`currentActivity()` 只认末尾那个工具组里
36
+ 还在跑的那条(并行时先回来的那个会留在原组里,照它画等于说到十分钟前的状态),
37
+ 以及 `ACTIVITY_KEYS`(类别 → epoch 自己那份 catalog 的 key)。
38
+
39
+ ⚠️ 它和 `web` 那边的 `live-verb.ts` 是两件事:那一族是「说法的轮换」(同一档状态
40
+ 换着词说,信息量为零),这里是「事实的陈述」。**事实不能轮换。**
23
41
 
24
42
  ## 展示项的 `at`:那句话是什么时候说的(2026-10-27)
25
43
 
@@ -60,10 +78,14 @@
60
78
 
61
79
  ```bash
62
80
  pnpm --filter @epoch-agent/view test
63
- pnpm --filter @epoch-agent/view build # tui / web 都从 dist 解析它,改完必须 build
81
+ pnpm --filter @epoch-agent/view build # 应用侧(vite build / tsx)从 dist 解析它
64
82
  ```
65
83
 
66
- > ⚠️ [web](../web) 和 [tui](../tui) 是按 package.json 的 `exports` 从**本包的 `dist/`**
67
- > 解析的。只改源码不 build,vite / tsx 那边看到的还是上一次的产物——症状是
68
- > `vite build` 报 `MISSING_EXPORT`,或者新加的字段在界面上永远不出现。
69
- > 根目录的 `pnpm build` 会自己排好顺序。
84
+ > ⚠️ [web](../web) 和 [tui](../tui) 的**应用侧**(`vite build`、`tsx`)是按
85
+ > package.json 的 `exports` 从**本包的 `dist/`** 解析的。只改源码不 build,那边看到的
86
+ > 还是上一次的产物——症状是 `vite build` 报 `MISSING_EXPORT`,或者新加的字段在界面上
87
+ > 永远不出现。根目录的 `pnpm build` 会自己排好顺序。
88
+ >
89
+ > ⚠️ **用例不是这一档**:web / tui 的 `vitest.config.ts` 把 `@epoch-agent/view`
90
+ > alias 到 `src/index.ts`,所以 `pnpm test` 不需要先 build。判据和实测在那两份配置的
91
+ > 注释里——那之前 `pnpm test` 读的是 `dist`,改了源码没重建时全绿但测的是旧代码。
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { WireSessionReference, WireFileReference, WireImageReference, EpochUserCommand, ToolArtifact, AgentEvent } from '@epoch-agent/protocol';
1
+ import { WireSessionReference, WireFileReference, WireImageReference, EpochUserCommand, ToolArtifact, AgentEvent, ActivityKind, ToolActivity } from '@epoch-agent/protocol';
2
+ export { ActivityKind, ToolActivity, activityOf, liveDetail } from '@epoch-agent/protocol';
2
3
 
3
4
  /**
4
5
  * 展示项类型 — 基于 Google Gemini CLI types.ts
@@ -115,6 +116,21 @@ type HistoryItemUser = HistoryItemBase & {
115
116
  * 缺席 = 普通消息,或者这条是 v15 之前的库写的(推不出来就不编)。
116
117
  */
117
118
  command?: EpochUserCommand;
119
+ /**
120
+ * 这一条**点名了哪条技能**(2026-11-09,会话库 v18)。
121
+ *
122
+ * 和 `command` 同一个位置、同一条判据:`text` 是用户打的那段字,而「他说要用
123
+ * 哪条技能」不在里面(技能正文走的是 system prompt 里的 `<pinned_skill>`)。
124
+ * 渲染层据这一格在气泡旁边画一枚「技能 · X」的小标记 —— 用户点了技能之后
125
+ * 得在对话里看得见自己点过,否则和「按了没反应」分不出来。
126
+ *
127
+ * 三个来源各有一条填它的路,逐字同 `command`:回放取 `messages.skill` 那一列 ·
128
+ * 刚发出去那一条取 `submit` action · 别的窗口发的那一条取 `user-message` 帧上的
129
+ * `skill`。
130
+ *
131
+ * 缺席 = 没点名技能,或者这条是 v18 之前的库写的。
132
+ */
133
+ skill?: string;
118
134
  };
119
135
  /**
120
136
  * 模型的回复正文。
@@ -162,6 +178,22 @@ type HistoryItemThinking = HistoryItemBase & {
162
178
  type HistoryItemDiff = HistoryItemBase & {
163
179
  type: 'diff';
164
180
  filePath: string;
181
+ /**
182
+ * 产出这张补丁的那次调用(`tool-call` 的 `callId`)。
183
+ *
184
+ * 补丁帧自己不带它(protocol 的 `type: 'diff'` 只有路径和两边内容),而它在流里
185
+ * 恒排在产出者那次调用的 `tool-result` **之前**,引擎又是**一次调用一次地**跑工具
186
+ * (`core/src/agent/loop.ts` 那个 for 循环)—— 于是收帧那一刻还没收口的那一次调用
187
+ * 就是它的产出者,`reducer` 据此补上。
188
+ *
189
+ * 有了这一格,宿主才能把补丁认回**那一张**卡;没有就只剩「同一路径的第 k 张」,
190
+ * 中间夹一个也带着这个路径、却不写文件的调用(读、或者名字认不出来的 MCP 工具)
191
+ * 时就认错一张 —— 补丁从自己那张卡后面跳到前一张后面。
192
+ *
193
+ * ⚠️ **只有实时流里补得出来**:落盘的补丁项没有这一格(库表里就没有),所以
194
+ * 渲染层必须容忍它缺席。
195
+ */
196
+ callId?: string;
165
197
  oldContent?: string;
166
198
  newContent?: string;
167
199
  /**
@@ -296,6 +328,73 @@ interface StreamEventMeta {
296
328
  */
297
329
  declare function applyStreamEvent(state: StreamReduceResult, event: AgentEvent, meta?: StreamEventMeta): StreamReduceResult;
298
330
 
331
+ /**
332
+ * 「这一轮现在在干什么」—— 折叠**这一侧**该管的那一半。
333
+ *
334
+ * ## 分家(2026-11-10)
335
+ *
336
+ * 判据(工具名 → 类别、入参 → 一句细节)**搬到 `@epoch-agent/protocol` 了**:
337
+ * 答这个问题的除了 `tui` 和 `web`,还有网线另一头的第三方宿主(桌宠、托栏、状态栏),
338
+ * 而它们拿不到 `view`。表只留一份,`core` 发 `tool-call` 帧时顺手算好塞进
339
+ * `activity` 字段,谁都能读。
340
+ *
341
+ * 留在这儿的只有**需要展示项**的那一半:
342
+ *
343
+ * - {@link runningToolActivity} / {@link currentActivity}:从 `tool_group` 里挑
344
+ * 「此刻还在跑的那条」。判据是「只认末尾那个工具组」—— 那需要 `ToolDisplayItem`
345
+ * 的形状,是展示层的事;
346
+ * - {@link ACTIVITY_KEYS}:类别 → epoch 自己的 catalog key(第三方宿主用自己那份)。
347
+ *
348
+ * ⚠️ **和 `web/src/thread/live-verb.ts` 那两族文案是两件事**:那两族是「说法的
349
+ * 轮换」(同一档状态换着词说,信息量为零),这里是「事实的陈述」(它真的在读文件)。
350
+ * 所以这里必须**给定**,不能轮换 —— 轮换事实就成了撒谎。
351
+ */
352
+
353
+ /**
354
+ * 一组调用里**此刻还在跑**的那条 → 活动。
355
+ *
356
+ * 取最后一条而不是第一条:并行三条时用户要看的是最近发起的那条(前两条早就
357
+ * 在跑、屏幕上那一行已经说过它了)。
358
+ */
359
+ declare function runningToolActivity(tools: readonly ToolDisplayItem[]): ToolActivity | undefined;
360
+ /**
361
+ * `currentActivity` 读得动的最小形状。
362
+ *
363
+ * **故意比 `HistoryItemWithoutId` 宽**:TUI 的展示项是它的超集(多一个 `plan`
364
+ * 那种只在终端存在的项),而这里根本不关心别的类型 —— 写窄了等于逼那个包
365
+ * 把一整棵联合类型递进来,再在这里写一遍它不认识的分支。
366
+ */
367
+ interface ActivitySource {
368
+ readonly type: string;
369
+ readonly tools?: readonly ToolDisplayItem[];
370
+ }
371
+ /**
372
+ * 流式列表里「此刻在干什么」。
373
+ *
374
+ * ⚠️ **只认末尾那个工具组**:更早的组里若还有 `pending`,那是结果没回来的残留
375
+ * (并行时先回来的那个会留在原组里),照着它画等于把一句话说成十分钟前的状态。
376
+ * 末尾那组都跑完了就**什么都不说** —— 那时模型在想,宿主该回到「在想」那一档。
377
+ */
378
+ declare function currentActivity(pending: readonly ActivitySource[]): ToolActivity | undefined;
379
+ /**
380
+ * 类别 → catalog key。
381
+ *
382
+ * ⚠️ **这张表是 epoch 两个宿主的唯一真源**:终端那一行和浏览器那一行说的是同一件事,
383
+ * 各抄一份等于同一句话有两个家。key 以**字面量**躺在源码里,i18n 门禁
384
+ * (`__tests__/i18n-catalog.test.ts`)就是靠这一点确认它们「被用过」的 —— 别改成
385
+ * 拼出来的 key。
386
+ *
387
+ * ⚠️ 第三方宿主**不要**用它:这是 epoch 自己那份 catalog 的 key,别的宿主要说的话,
388
+ * 按 `ActivityKind` 翻自己那份(`nebular-agent` 的 `pet.activity.<kind>` 就是这么做的)。
389
+ *
390
+ * ⚠️ `other` 那一档宿主**不要用它**:认不出来的工具(MCP 等)显示生名比显示
391
+ * 「正在调用工具」有用得多。它在这儿只是为了表是满的。
392
+ */
393
+ declare const ACTIVITY_KEYS: Readonly<Record<ActivityKind, {
394
+ readonly live: string;
395
+ readonly done: string;
396
+ }>>;
397
+
299
398
  /**
300
399
  * 行级 diff —— 纯函数,可单测。
301
400
  *
@@ -382,4 +481,4 @@ declare const DIFF_CONTEXT_LINES = 1;
382
481
  */
383
482
  declare function collapseContext(lines: readonly DiffLine[], context?: number): Array<DiffLine | null>;
384
483
 
385
- 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 };
484
+ export { ACTIVITY_KEYS, 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, currentActivity, diffLines, runningToolActivity };
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { liveDetail, activityOf } from '@epoch-agent/protocol';
2
+ export { activityOf, liveDetail } from '@epoch-agent/protocol';
3
+
1
4
  // src/reducer.ts
2
5
  function anchor(meta) {
3
6
  return {
@@ -27,6 +30,17 @@ function patchTool(pending, callId, patch) {
27
30
  }
28
31
  return null;
29
32
  }
33
+ function openCallId(pending) {
34
+ for (let i = pending.length - 1; i >= 0; i--) {
35
+ const item = pending[i];
36
+ if (item?.type !== "tool_group") continue;
37
+ for (let j = item.tools.length - 1; j >= 0; j--) {
38
+ const tool = item.tools[j];
39
+ if (tool?.status === "pending") return tool.callId;
40
+ }
41
+ }
42
+ return void 0;
43
+ }
30
44
  function applyStreamEvent(state, event, meta) {
31
45
  const { pending, thought } = state;
32
46
  const carry = (next) => state.sealed === void 0 || next.sealed !== void 0 ? next : { ...next, sealed: state.sealed };
@@ -85,7 +99,8 @@ function applyStreamEvent(state, event, meta) {
85
99
  });
86
100
  return next ? carry({ pending: next, thought }) : state;
87
101
  }
88
- case "diff":
102
+ case "diff": {
103
+ const producer = openCallId(pending);
89
104
  return carry({
90
105
  pending: [
91
106
  ...pending,
@@ -94,11 +109,13 @@ function applyStreamEvent(state, event, meta) {
94
109
  filePath: event.filePath,
95
110
  ...event.oldContent !== void 0 ? { oldContent: event.oldContent } : {},
96
111
  ...event.newContent !== void 0 ? { newContent: event.newContent } : {},
112
+ ...producer !== void 0 ? { callId: producer } : {},
97
113
  ...anchor(meta)
98
114
  }
99
115
  ],
100
116
  thought
101
117
  });
118
+ }
102
119
  case "notice":
103
120
  return carry({
104
121
  pending: [...pending, { type: "info", text: event.text, ...anchor(meta) }],
@@ -127,6 +144,40 @@ function applyStreamEvent(state, event, meta) {
127
144
  }
128
145
  }
129
146
  }
147
+ function runningToolActivity(tools) {
148
+ const running = tools.filter((tool) => tool.status === "pending");
149
+ const last = running[running.length - 1];
150
+ if (last === void 0) return void 0;
151
+ const detail = liveDetail(last.request);
152
+ return {
153
+ kind: activityOf(last.toolName),
154
+ ...detail !== void 0 ? { detail } : {}
155
+ };
156
+ }
157
+ function currentActivity(pending) {
158
+ for (let i = pending.length - 1; i >= 0; i--) {
159
+ const item = pending[i];
160
+ if (item?.type !== "tool_group" && item?.type !== "tool_display_group") continue;
161
+ return runningToolActivity(item.tools ?? []);
162
+ }
163
+ return void 0;
164
+ }
165
+ var ACTIVITY_KEYS = {
166
+ read: { live: "activity.read", done: "activity.read_done" },
167
+ read_image: { live: "activity.read_image", done: "activity.read_image_done" },
168
+ write: { live: "activity.write", done: "activity.write_done" },
169
+ edit: { live: "activity.edit", done: "activity.edit_done" },
170
+ search: { live: "activity.search", done: "activity.search_done" },
171
+ command: { live: "activity.command", done: "activity.command_done" },
172
+ shell: { live: "activity.shell", done: "activity.shell_done" },
173
+ code: { live: "activity.code", done: "activity.code_done" },
174
+ web: { live: "activity.web", done: "activity.web_done" },
175
+ team: { live: "activity.team", done: "activity.team_done" },
176
+ schedule: { live: "activity.schedule", done: "activity.schedule_done" },
177
+ plan: { live: "activity.plan", done: "activity.plan_done" },
178
+ ask: { live: "activity.ask", done: "activity.ask_done" },
179
+ other: { live: "activity.other", done: "activity.other_done" }
180
+ };
130
181
  // src/diff-lines.ts
131
182
  var DIFF_MAX_LINES = 2e3;
132
183
  function splitLines(text) {
@@ -219,4 +270,4 @@ function collapseContext(lines, context = DIFF_CONTEXT_LINES) {
219
270
  return out;
220
271
  }
221
272
 
222
- export { DIFF_CONTEXT_LINES, DIFF_MAX_LINES, applyStreamEvent, collapseContext, diffLines };
273
+ export { ACTIVITY_KEYS, DIFF_CONTEXT_LINES, DIFF_MAX_LINES, applyStreamEvent, collapseContext, currentActivity, diffLines, runningToolActivity };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epoch-agent/view",
3
- "version": "4.1.1",
3
+ "version": "6.0.0",
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.1.1"
25
+ "@epoch-agent/protocol": "6.0.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "tsup": "^8.3.6",