omo-senpi Memory TUI 条目渲染统一:基于 Senpi notice 视觉契约的 before/after 工程实践
2026/9/19 13:03:11 网站建设 项目流程

omo-senpi Memory TUI 条目渲染统一:基于 Senpi notice 视觉契约的 before/after 工程实践

【免费下载链接】oh-my-openagentOmO: Just type "mass ulw" keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent

oh-my-openagent(OmO)是一个将"mass ulw"关键词与提示词结合、围绕图工程展开的 Agent 编排框架,其中packages/omo-senpi承载 Senpi 模式下的任务与记忆反射(memory reflection)运行机制。本文以仓库内证据文档 .omo/evidence/20260813-memory-tui-polish/README.md 为主体,完整还原一次针对memory reflection 转录(transcript)条目的 TUI 渲染改造:将原本"扁平、无色、key:value"的开发期渲染器,统一为 Senpi 自身既有的notice 视觉契约(glyph + 语义色调标题 + 弱化 prose why 行 + 展开时可见的斜体 detail 行),并顺带修复了截断与着色交互产生的 ANSI 泄漏缺陷。读完本文,你将掌握该改造的完整 before/after 差异、六类条目的渲染结果、窄终端降级策略、源码实现位置与可复现的截图取证流程。

背景:transcript 条目与 Senpi 自身条目"两张皮"的问题

在 Senpi 模式的会话转录(transcript)中,memory reflection(记忆反射)运行会产生若干种条目(entry):启动(launched)、完成(completion,含 merged / failed / timed out 等结局)、批量摘要(summary),以及健康告警(senpi-memory.health)。改造前,这些条目由开发期(dev)renderer 渲染为扁平的、无色的key:value文本,例如:

reflection-launched memory reflection started run:reflection-run-2 trigger:step-count (+25 steps) reflection-completion (merged) memory reflection merged run:reflection-run-2 category:quick

这类输出与 Senpi 自身条目(renderCacheKeepAliveEntryrenderRuleActivationEntry等 notice 风格条目)放在同一个 transcript 里显得格格不入——"looked foreign next to Senpi's own entries"。本次改造的目标就是把 memory 系条目重新样式化(restyle)到 Senpi 已经确立的notice 视觉契约之上,使转录整体拥有一致的可读性与语义色彩。

值得强调的是,改造前存在一个隐蔽的功能缺口:reflection-summarysenpi-memory.health虽然被作为条目 append 进转录,但从未注册过对应的 renderer,因此它们实际上从未在 transcript 中渲染出来((no renderer registered on dev: entry never rendered in transcript))。本次改造同时补齐了这两个 renderer 的注册,属于"从无到有"的修复而非单纯的样式重绘。

证据体系:真实 xterm.js 截图与 ANSI 字节流

证据文档 README.md 随附的 artifacts 构成了一个"可复现、可 diff"的取证体系:

文件作用
before/terminal.png开发期 renderer 的真实 xterm.js 截图(110x34)
after/terminal.png新 renderer 的真实 xterm.js 截图(110x64)
before-ansi.txt/after-ansi.txt喂给 xterm.js 的原始 ANSI 字节流
before-plain.txt/after-plain.txt同一内容剥掉 ANSI 后的纯文本,用于 diff
scripts/before-preview.ts逐字复刻 dev renderer 以采集 BEFORE 快照
scripts/entry-render-preview.ts驱动真实新 renderer 以采集 AFTER 快照

截图并非 mock-up,而是通过 script/qa/web-terminal-visual-qa.mjs 的replay 模式(--from-file生成:该脚本把原始 ANSI 字节流灌入 headless Chrome 中运行的真实 xterm.js 终端,再截取 true-color 画面。metadata 也证实了两张 PNG 均为connector: "xterm-replay"colorPath: "xterm.js (true color; not tmux)"browserCapture: "captured"的真实捕获。

复现命令如下(entry-render-preview.ts会以 ANSI theme 在 60 / 100 两种宽度下逐条驱动真实 renderer):

bun run .omo/evidence/20260813-memory-tui-polish/scripts/before-preview.ts > before-ansi.txt bun run .omo/evidence/20260813-memory-tui-polish/scripts/entry-render-preview.ts > after-ansi.txt node script/qa/web-terminal-visual-qa.mjs --title "Memory TUI entries AFTER" \ --from-file after-ansi.txt --cols 110 --rows 64 --evidence-dir after

web-terminal-visual-qa.mjs支持--from-file(回放原始字节流,无交互)、--command(真实 node-pty 运行并实时渲染)、--input(脚本化按键,如{Enter}{ArrowDown})、--cols/--rows(终端几何,默认 120x32)、--dwell-ms(截屏前停留时长)、--redact/--redact-regex(对 PNG 与文本证据统一脱敏)等参数;--self-test可无 Chrome 自检 pty 与 ANSI/CJK 输出能力。entry-render-preview.ts内部把success/error/warning/accent/muted/dim分别映射为 32/31/33/36/90/2 号 ANSI 转义序列,与终端实际着色一致。

Before:开发期 renderer 的四个具体问题

before-preview.ts可以看出 dev renderer 只是把字段用linesComponent逐行拼出来,其问题可归纳为四点:

  1. Uniform white,无色彩、无层级:所有行都是同一白色,标题与正文、成功与失败无法一眼区分。
  2. key:value字段汤run:reflection-run-2category:quickdetail:...直接裸露,阅读负担重。
  3. truncateToWidth泄漏裸\e[0m:截断函数会在被截断行的中间塞入自己的 SGR reset 转义序列,破坏后续着色状态(该问题详见下文"顺带修复的 Bug"一节)。
  4. 两类条目从未被渲染reflection-summarysenpi-memory.health没有注册 renderer,transcript 中直接缺位。

BEFORE 快照的典型输出如下:

reflection-completion (failed) memory reflection failed run:reflection-run-2 category:quick detail:worktree merge refused because memory had uncommitted changes on the target branch reflection-summary (no renderer registered on dev: entry never rendered in transcript) senpi-memory.health (no renderer registered on dev: entry never rendered in transcript)

After:Senpi notice 视觉契约的具体形态

改造后的渲染统一采用Senpi notice 家规(house style),核心形态为:

  • glyph + 色调标题(tone-coloured title):每条目以状态符号开头(稳态、警示、失败、进行中),标题按语义着色(accent / success / error / warning);
  • 弱化的 prose "why" 行:用一句完整的英文句子说明"发生了什么/将要发生什么";
  • 展开时可见的斜体 detail 行:默认折叠隐藏,展开(expanded)后才出现;
  • 字段一律用·分隔:绝不使用key:value汤。

以六类条目为例,AFTER 快照(100 列宽)的展开形态分别是:

reflection-launched [expanded] (title: accent / cyan) ◐ Memory reflection started · reflection-run-2 Triggered by step-count after 25 new steps. category quick · model anthropic/claude-sonnet-4 · thinking high · identity project-a1b2c3d4 reflection-completion (merged) [expanded] (title: success / green) ● Memory reflection merged · reflection-run-2 Reflection merged its findings into memory. category quick · files 3 · commit 9f2c1ab · took 1m12s reflection-completion (failed) [expanded] (title: error / red) ✗ Memory reflection failed · reflection-run-2 Reflection did not finish; the transcript cursor was not advanced. category quick · took 4.3s · reason child_exit · worktree merge refused because memory had uncomm... reflection-completion (timed out) [expanded] (title: warning / yellow) ⚠ Memory reflection timed out · reflection-run-2 Reflection hit its deadline; the transcript cursor was not advanced. category quick · took 10m00s · reason deadline_exceeded reflection-summary [expanded] (title: warning / yellow) ⚠ Memory reflection · 7 older completions collapsed Delivered while this session was away; 2 need attention. most common child_exit:worktree merge refused because memory was dirty · oldest 2026-08-11T04:00:... senpi-memory.health [expanded] (title: error / red) ✗ Memory reflection failing · 4 runs in a row Commit or stash the memory worktree, then rerun /memory reflect. reason child_exit · worktree merge refused because memory had uncommitted changes · since 2026-08...

折叠(collapsed)形态只保留标题 + why 行,detail 行被隐藏——例如reflection-completion (failed) [collapsed]只有两行:✗ Memory reflection failed · reflection-run-2Reflection did not finish; the transcript cursor was not ...。这与 Senpi 自身 notice 的折叠/展开行为完全一致。

源码实现:四份文件构成渲染层

改造后的实现分散在packages/omo-senpi/src/components/memory/worker/下的四份文件中(PR #6814 拆分 completion 模块后的布局):

文件职责
completion-renderers.tsreflection-launchedreflection-completionreflection-summary三个 renderer 及注册
health-alert.tssenpi-memory.healthrenderer 及注册,紧邻它渲染的条目形状
entry-renderers.ts共享 notice 契约:noticeComponentfitjoinFields、结局 glyph/颜色/label/summary 映射表
entry-renderers.test.ts字面字符串断言与 recording-theme 断言

worker/completion.ts是 barrel,re-exportcompletion-renderers.tsworker/index.tsre-exporthealth-alert.ts,因此既有 import 站点无需改动;README 声明最终渲染输出已与上述最终布局逐字节复核(byte-for-byte re-verified)。

共享契约层:entry-renderers.ts

entry-renderers.ts 定义了所有条目的共享词汇:

  • OUTCOME_COLORS映射表(L35-L43):merged/no_changessuccessparent_dirty/merge_conflict/dirty_uncommitted/timed_outwarningfailederror;未知结局回退muted。这套命名严格跟随 senpi-task 的statusThemeColor约定,没有发明新颜色名
  • outcomeGlyph(L50-L56):successerrorwarning,其余·,与 Senpi 自身 notice 标题的符号词汇保持一致。
  • outcomeLabel/outcomeSummary(L59-L100):把 snake_case 结局翻译成人类可读短语与一句完整 prose,例如no_changes→ "no changes" / "Reflection finished with nothing new worth keeping."。
  • noticeComponent(L106-L134):把{glyph, title, tone, why, extra, detail}委托给 senpi-task 的buildNoticeBox,保留既有公共形状,布局全部下沉到共享 notice box。
  • fit(L143-L148):先normalizeRendererText再按可见宽度截断;宽度不足时用truncateToWidth+ELLIPSIS再次 normalize以剥离控制序列(这是修复 ANSI 泄漏的关键,见下文)。
  • joinFields(L151-L153):过滤空字段后用FIELD_SEPARATOR·,L26)连接。
  • runLabel/detailExcerpt(L156-L163):run id 截到 28 列、detail 截到 72 列,长标识符以省略号降级而非换行。

三个 completion renderer:completion-renderers.ts

completion-renderers.ts 实现了三个 renderer:

  • renderReflectionLaunchedEntry(L35-L67):进行中状态,glyph、toneaccent(与statusThemeColor中"running"一致);why 行通过triggerPhrase(L69-L71)区分manualstep-count等触发器;extra 行带 conversation 数、category、model、thinking;detail 行展开时携带 trigger、identity、started。
  • renderReflectionCompletionEntry(L73-L132):merged且有 recap 时进入"Memory updated"成功分支,展示 report 预览、变更文件数与短 commit;其余结局走统一分支——glyph/颜色/label/summary 全部来自共享映射表,payoff 行拼接files changedcommittookformatDuration在 L165-L172,支持 ms / s / m:ss)、reason、截断后的 detail 摘录与预算补救文案。
  • renderReflectionSummaryEntry(L134-L157):把离线期间堆积的 N 条 completion 折叠成一条摘要;failedCount === 0时 glyph、tonemuted,否则+warning,并把 dominant fingerprint 放在可见行。
  • registerReflectionCompletionRenderer(L159-L163):一次性注册三个条目类型,补齐了改造前"从未注册 renderer"的缺口。

健康告警:health-alert.ts

health-alert.ts 承载senpi-memory.health

  • REFLECTION_HEALTH_ENTRY_TYPE = "senpi-memory.health"(L16),条目形状ReflectionHealthEntry(L18-L30)含 streak(连续失败次数)、fingerprint、lastReason、recommendation、launcher/runtime 等。
  • renderReflectionHealthEntry(L35-L63):失败连击是"需要吸引注意"的状态——glyph、toneerror,并把可执行的 recommendation 提升到始终可见的 why 行,而不是藏在 detail 里;detail 行展开时携带 reason、cause、launcher、跨 runtime 信息与 since 时间。
  • recommendationWhy(L70-L75):保证 why 行是完整英文句子——即使 remediation 文案是片段(如run /login <provider>),也会被补成Run /login <provider>.
  • emitReflectionHealthAlert(L81-L132):告警的写侧——读取派生健康状态、过滤(streak < 3 或指纹不稳定则不发)、用once去重后appendEntrysafeNotify。它被刻意放在health-alert.ts而非health.ts,从而保证worker/health.ts保持纯派生(purely derivational):PR #6812 已让health.ts只读计算、无写入、无appendEntryhealth.test.ts通过检查health.ts的 import 列表强制执行这一约束。

测试保障:entry-renderers.test.ts

entry-renderers.test.ts 的断言策略有两个要点:

  1. 字面字符串断言(literal-string assertions):期望值直接写成bold("◐ Memory reflection started · reflection-run-2")这样的完整字符串,刻意不从 renderer 使用的常量/helper 反推——否则一旦 glyph/颜色表损坏,同源推导的同义反复也会让测试保持绿色。
  2. recording-theme 断言:用recordingTheme()记录每次fg调用,直接证明语义色真实生效,例如 launched 折叠态断言["accent", "dim", "dim", "dim"]、merged 断言["success", "dim"]、failed 断言["error", "dim", "error"]、summary 失败态断言["warning", "dim", "warning"]TAGGING_THEME则验证"颜色包裹文本而非替换文本"([success]● Memory reflection merged ...[/success])。

窄终端(60 cols):ellipsis 降级与无溢出承诺

窄终端下,长标识符与长 detail 以省略号降级而非换行(换行会破坏 notice 的块结构):

✗ Memory reflection failed · reflection-run-2 Reflection did not finish; the transcript cursor was not ... category quick · took 4.3s · reason child_exit · worktree...

这一点被程序化验证:对于宽度 1 到 200 的任意请求宽度,包括 300 字符的 detail 字符串,渲染出的每一行都不会超过请求宽度。对应到测试,就是 entry-renderers.test.ts 的 "hostile widths" 用例:runId90 字符、category40 字符、reason40 字符、detail300 字符,在宽度[1, 5, 20, 40, 60, 80, 100]下逐行断言visibleWidth(line) <= width。同时还有针对截断行为的专项断言:60 列下 why 行折成两行、超长 run id 被 excerpt(reflection-run-with-an-ex...)、折叠态不出现 detail 行、展开态 detail 行才携带时间范围(oldest ... · newest ...)。

设计系统注意事项:不越界、不硬编码

证据文档给出了三条设计纪律,均与仓库代码相互印证:

  1. 颜色只来自真实存在的ThemeColor名称successerrorwarningaccentmuteddim),通过theme.fg应用,遵循仓库内statusThemeColor映射约定,不硬编码任何转义序列——ANSI 码只出现在证据脚本的测试 theme 中(entry-render-preview.ts)。
  2. 布局复用 senpi-task 的现成 helpernormalizeRendererTextexcerptRendererTextrendererVisibleWidthtruncateToWidthELLIPSISlinesComponent均来自@oh-my-opencode/senpi-task,与任务/控制类 renderer 同源。
  3. 视觉契约在仓库内复刻而非深度导入:参考对象是 Senpi 自身的noticeEntryRenderer/NoticeSpec契约(renderCacheKeepAliveEntryrenderRuleActivationEntry),但这些符号是senpi 内部实现——不在其 145 个公共导出中,也不在package.jsonexportsmap 里。因此本改造基于仓库内linesComponent/buildNoticeBox复刻该视觉契约,而不是去深度导入私有路径;同时不加任何 box drawing 或边框,因为 Senpi 参考实现此处本来就不用。

顺带修复的 Bug:truncateToWidth 的 SGR reset 泄漏

改造过程中发现并修复了一个隐蔽的 ANSI 状态 bug:

truncateToWidth会把省略号包在自己的 SGR reset 里(\e[0m...\e[0m)。如果在截断之后再做着色,这个 reset 就会提前终止theme.fg包裹的颜色跨度,导致省略号之后的所有文本渲染成无色。修复方式是fit()(entry-renderers.ts)在theme.fg包裹之前先对截断结果重新规范化(re-normalise)以剥离控制序列。测试侧有专门的断言:60 列 +TAGGING_THEME下,why 行仍被[dim]...[/dim]正确包裹,且整段输出不包含任何\u001b[0m(entry-renderers.test.ts);加粗标题在窄宽度下同样要求\u001b[1m/\u001b[22m完整包裹整个 fitted 标题(L517-L525)。

如何验证与深入阅读

若要在本地复现并自行截图取证,按证据文档 README.md 的步骤执行即可(需要 bun 与可用的 Chrome/Chromium,--chrome-bin可指定可执行文件路径;无 Chrome 的 CI 可用--no-browser只保留 ANSI 流)。产物会写入--evidence-dir指定的目录,包含terminal.pngterminal.txtterminal-ansi.txtmetadata.json,其中terminal-ansi.txt是原始字节流,terminal.txt是剥 ANSI 后的纯文本,方便对 before/after 做 diff。

进一步阅读建议:

  • 渲染层实现:entry-renderers.ts、completion-renderers.ts、health-alert.ts;
  • 渲染层测试:entry-renderers.test.ts;
  • 纯派生约束:worker/health.ts与其测试worker/health.test.ts
  • 证据采集:证据脚本 entry-render-preview.ts / before-preview.ts,以及浏览器级截图工具 script/qa/web-terminal-visual-qa.mjs(依赖 xterm-live-terminal.mjs 完成真实 xterm.js 渲染)。

这条改造路径本身也提供了可复用的方法论:为 transcript 中的事件类条目定义统一的"标题 + why + 可见 payoff + 展开 detail"四层契约,用共享映射表收敛结局语义,用字面字符串测试锁定视觉输出,用真实终端截图作为最终验收证据——对任何希望为 Agent 运行时转录建立一致视觉语言的工程团队都值得借鉴。

【免费下载链接】oh-my-openagentOmO: Just type "mass ulw" keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询