@epoch-agent/view
展示层纯逻辑。把 AgentEvent 折叠成结构化的可渲染 item。
- ✅ 做:事件 → item 的折叠(
applyStreamEvent)、diff 计算、这些 item 的类型 - ❌ 不做:任何组件、任何格式化、任何 I/O。既没有
node:,也没有 DOM - 依赖:只有 protocol
存在的理由
只有一条:tui 和 web 必须共用同一份折叠逻辑。它修掉的三个语义错误 ——文本顺序、并行工具串台、reasoning 覆盖——两边都会踩,抄一份就是两份漂移。
导出
| 导出 | 说明 |
|---|---|
applyStreamEvent(state, event) | 折叠一个 AgentEvent,返回新的 item 列表 + 变更信息 |
activityOf(toolName) | 工具名 → 人类动作类(read / command / web …)(转发 protocol) |
liveDetail(request) | 从入参里挑一句人话(description > command > path…)(转发 protocol) |
runningToolActivity(tools) / currentActivity(pending) | 此刻在跑的那条 → 类别 + 细节 |
ACTIVITY_KEYS | 类别 → catalog key(epoch 两个宿主共用的唯一真源) |
diffLines(before, after) | 行级 diff |
collapseContext(lines) | 把连续的未改动行折叠成 @@ 摘要 |
DIFF_CONTEXT_LINES / DIFF_MAX_LINES | 折叠与截断的阈值(后者的口径见下) |
HistoryItem* / ToolDisplayItem 等类型 | item 的判别联合 |
「现在在干什么」(2026-11-09,2026-11-10 判据下移)
类别与细节的判据住在 protocol 的 activity.ts:
答这个问题的除 tui / web,还有网线另一头的第三方宿主(宿主桌宠、托盘、状态栏),
而它们拿不到 view。core 发 tool-call 帧时用同一张表算好塞进 activity 字段,
所以「terminal 算哪一类」全仓只有一个答案。这一层再导出一次,tui / web 的名字没变。
留在这里的是需要展示项形状的那一半:currentActivity() 只认末尾那个工具组里
还在跑的那条(并行时先回来的那个会留在原组里,照它画等于说到十分钟前的状态),
以及 ACTIVITY_KEYS(类别 → epoch 自己那份 catalog 的 key)。
⚠️ 它和 web 那边的 live-verb.ts 是两件事:那一族是「说法的轮换」(同一档状态
换着词说,信息量为零),这里是「事实的陈述」。事实不能轮换。
展示项的 at:那句话是什么时候说的(2026-10-27)
HistoryItemBase 上多了一格 at?: number(epoch 毫秒),和 seq 并列:seq 是
锚点(从哪儿开始),at 是人读的那一半(屏幕上那句 8月25日 13:31)。
- 它从
StreamEventMeta.at进来(applyStreamEvent的第三个参数),只在新建项 时落一次 —— 同seq:追加进已有项时不覆盖,否则时间会跟着流式往后漂 - TUI 一个都不传(它吃裸事件,没有信封也没有那个形态),所以那边恒缺席
- 缺席时不补:那是「不知道」,不是「现在」—— 宿主不许拿
Date.now()兜底
回放(GET /messages)那条路不经过 applyStreamEvent,它从
WireReplayMessage.timestamp 自己填一次(web/src/state/replay.ts)。
行级 diff 的两个口径(2026-10-09)
| 事 | 口径 |
|---|---|
DIFF_MAX_LINES 量什么 | 剥掉公共前后缀之后剩下的那一段,不是整份文件。6000 行的文件改一行照画,只有中间那段真大才撞线 |
| 截断那一档报什么 | 只有两侧整份的 oldLines / newLines,没有 added / removed(DiffTruncated) |
两条都是同一条规矩:宁可少说,不说错的。 以前量的是整份行数,于是「600 行改
两行」和「整份重写 600 行」落进同一档、一行都看不到;以前那两个数装的是文件行数,
而两个宿主都把它画成 +N −M,于是「1000 行改 2 行」显示成 +1000 −1000。
上限取 2000 的读数、剥前后缀的等价性、以及哪一支上有哪些字段,都在
diff-lines.ts 的文件头和 DiffTruncated 上 —— 这里不抄第二份。
分界线
⚠️ 这里进了 React 就说明分界画错了。 真到那一步,该拆成
view-core+ 各自的 组件层,而不是往这里塞组件。
protocol 的 AgentEvent 加成员时,这里的 reducer 要跟着处理,否则 TUI 和 Web 会
静默丢事件。
开发
pnpm --filter @epoch-agent/view test
pnpm --filter @epoch-agent/view build # 应用侧(vite build / tsx)从 dist 解析它
⚠️ web 和 tui 的应用侧(
vite build、tsx)是按 package.json 的exports从本包的dist/解析的。只改源码不 build,那边看到的 还是上一次的产物——症状是vite build报MISSING_EXPORT,或者新加的字段在界面上 永远不出现。根目录的pnpm build会自己排好顺序。⚠️ 用例不是这一档:web / tui 的
vitest.config.ts把@epoch-agent/viewalias 到src/index.ts,所以pnpm test不需要先 build。判据和实测在那两份配置的 注释里——那之前pnpm test读的是dist,改了源码没重建时全绿但测的是旧代码。