claude-mem 分支记忆可视化:在 Viewer 观察卡片中展示 Git 分支与 Commit SHA
本文基于 claude-mem 的分支记忆(branch-memory)特性开发 playbook 中 BRANCH-PARITY-02-Viewer-Branch-Display.md 阶段文档展开,讲解如何把数据库中已存储的 branch 与 commit_sha 观察元数据,打通"类型定义 → 分页查询 → React 卡片渲染"三层链路,最终在 http://localhost:37777 的 Viewer UI 中以分支徽章的形式呈现。读完本文,你将掌握 claude-mem Viewer 的数据流全貌、观察卡片组件的渲染机制,以及单文件 Viewer 的构建与验证流程。
1. 背景:数据库有分支信息,Viewer 却没有
claude-mem 的核心工作流是:记录 Agent 会话中的操作 → 用 AI 压缩为"观察(Observation)" → 在未来会话中重新注入相关上下文。分支记忆特性进一步要求记忆与 Git 分支边界对齐——同一条观察应当能被追溯它是在哪个分支、哪个提交上产生的。
按 playbook 的原始描述,此时系统的状态是:
- 数据库已经在每条观察上存储了
branch与commit_sha(由 migrations 24-25 引入的列); - 但 React Viewer 中展示观察卡片时只有类型、项目、标题、副标题、事实、叙事和元数据,没有分支信息;
- 原因很具体:为 Viewer 供数的
PaginationHelper查询有意省略了这两个列,导致数据虽然入库却"断供"于界面。
因此这一阶段的目标非常聚焦:给观察卡片加上分支可见性,让用户一眼看出每条观察来自哪个 git 分支,从而补全分支记忆的"视觉层"。
从源码结构看,当前仓库快照中 PaginationHelper 的 SELECT 列表确实不含 branch/commit_sha,Viewer 类型定义 的 Observation 接口也尚未包含这两个字段——这与 playbook 描述的基线状态一致(该特性在 branch-memory 工作分支上开发,playbook 中标记了各任务的完成状态)。
2. 类型层:给 Observation 接口加上可选的分支字段
第一处改动发生在 Viewer 的 TypeScript 类型系统 src/ui/viewer/types.ts。当前 Observation 接口为:
export interface Observation {
id: number;
memory_session_id: string;
project: string;
merged_into_project?: string | null;
platform_source: string;
type: string;
title: string | null;
subtitle: string | null;
narrative: string | null;
text: string | null;
facts: string | null;
concepts: string | null;
files_read: string | null;
files_modified: string | null;
prompt_number: number | null;
created_at: string;
created_at_epoch: number;
}
需要追加两个可选且可为 null 的字段:
branch?: string | null;
commit_sha?: string | null;
关键设计决策是"可选性":branch?: string | null 而非必填。因为存在大量早于分支记忆特性的历史观察,它们的 branch 值为 NULL——若声明为必填字段,旧数据会被类型系统误判为非法。这一约定同时体现在 Viewer 侧接口和 Worker 侧接口 src/services/worker-types.ts(PaginationHelper 从后者导入 Observation 类型),playbook 在任务完成备注中特别指出:由于查询结果是泛型透传(paginate<Observation>()),类型更新后"其余部分自动生效",无需改动映射逻辑。
3. 查询层:PaginationHelper.getObservations() 补列
数据供给的"断点"在 src/services/worker/PaginationHelper.ts 的 getObservations() 方法。当前实现对 observations 表执行硬编码列列表的 SELECT,并按 created_at_epoch DESC 排序:
let query = `
SELECT
o.id,
o.memory_session_id,
o.project,
o.merged_into_project,
COALESCE(s.platform_source, 'claude') as platform_source,
o.type,
o.title,
o.subtitle,
o.narrative,
o.text,
o.facts,
o.concepts,
o.files_read,
o.files_modified,
o.prompt_number,
o.created_at,
o.created_at_epoch
FROM observations o
LEFT JOIN sdk_sessions s ON o.memory_session_id = s.memory_session_id
`;
改动只有一处:在列列表末尾追加 o.branch, o.commit_sha。由于结果集通过 db.prepare(query).all(...) 取出后直接断言为 Observation[],列补上之后新字段会自动出现在返回对象里,配合第 2 节的类型更新即完成整条链路。
理解这段查询的几个配套细节,有助于把握改动边界:
- 分页的 hasMore 探测:
params.push(limit + 1, offset)——每次多取一行,若返回行数超过limit则判定hasMore: true,随后slice(0, limit)截断。这是无COUNT(*)的轻量分页模式,新增列不影响该逻辑。 - 项目过滤:指定
project时匹配o.project = ? OR o.merged_into_project = ?(支持已合并项目的归属追溯);未指定时排除OBSERVER_SESSIONS_PROJECT(观察者自身会话,来自 src/shared/paths.ts 的常量)。 - 返回前的清洗:
sanitizeObservation()会用stripProjectPaths()把files_read/files_modified中的绝对路径剥离为相对路径,保证卡片不泄露本机目录结构。分支字段不经过任何清洗,原样透传。
4. 渲染层:ObservationCard 中的分支徽章
展示逻辑落在 src/ui/viewer/components/ObservationCard.tsx。当前卡片的头部结构(card-header-left 容器内)依次渲染四枚徽章:
<div className="card-header-left">
<span className={`card-type type-${observation.type}`}>{observation.type}</span>
<span className={`card-source source-${observation.platform_source || 'claude'}`}>
{observation.platform_source || 'claude'}
</span>
<span className="card-project">{observation.project}</span>
{observation.merged_into_project && (
<span className="card-merged-badge" title={`Merged into ${observation.merged_into_project}`}>
merged → {observation.merged_into_project}
</span>
)}
</div>
playbook 对该卡片的具体改造规格如下,逐条对应现有代码模式:
- 位置:分支徽章放在
card-header-left内、现有card-project徽章旁(对应上面代码中约第 51 行附近)。 - 条件渲染:仅在
observation.branch为 truthy 时渲染——迁移前的观察该字段为null,徽章必须静默缺省,不能占位。这与card-merged-badge的条件渲染写法({observation.merged_into_project && ...})完全同构,可直接参照。 - 图标与文本:一个简化的 git 分支分叉 SVG 图标(fork 造型)加分支名,整体样式仿照现有
card-project的 span 写法。 - Commit SHA 缩写(可选增强):在分支名旁/下方展示
commit_sha前 7 个字符(commit_sha.slice(0, 7)),使用等宽字体。 - 配色区分:徽章颜色需与项目徽章可区分——playbook 建议使用
var(--color-text-muted)文字搭配柔和背景。
配套的 .card-branch 新 CSS 类规格(写入 Viewer 样式模板 src/ui/viewer-template.html):
| 属性 | 取值 | 依据 |
|---|---|---|
| 字号 | ~11px | 与 .card-project 徽章一致 |
| 背景 | var(--color-surface-hover) 一类柔和表面色 |
playbook 建议值 |
| 圆角 | 与现有徽章一致(模板中徽章圆角为 3px) | 参照 viewer-template.html 中 .card-merged-badge 的 border-radius: 3px |
| SHA 部分 | 等宽字体 | 保证十六进制串等宽对齐 |
作为参照,现有徽章体系在模板中的定义是:.card-project 仅设 color: var(--color-text-muted);.card-merged-badge 则是 9px 小字号、background: var(--color-type-badge-bg)、1px 边框、opacity: 0.85 的弱化样式。分支徽章介于两者之间——比 merged 徽章醒目、比 type 徽章克制——符合"次要元数据"的视觉层级。
5. 构建与验证:单文件 Viewer 的产出链路
Viewer 不是独立部署的 Web 应用,而是构建为单文件 HTML(内嵌 CSS/JS):
- 源样式模板:src/ui/viewer-template.html(含
--color-text-muted等主题 CSS 变量,多套主题下各有取值); - 构建产物:plugin/ui/viewer.html——Worker 服务启动后由
http://localhost:37777直接提供。
验证流程(playbook 的任务清单):
- 在 package.json 中确认构建脚本:生产构建入口为
npm run build-and-sync,其展开为npm run build && npm run sync-marketplace && node scripts/restart-marketplace-worker.cjs; - 类型检查有独立脚本:
typecheck:viewer即tsc --noEmit -p src/ui/viewer/tsconfig.json,可在完整构建前单独快速校验 Viewer 侧改动(本例中类型层与组件层改动都在这里被覆盖); - 运行
npm test确认无回归; - 检查构建产物
plugin/ui/viewer.html中是否出现新的.card-branch样式与渲染分支。
6. 放到分支记忆全景中看
本 playbook 是 Branch-Parity 阶段的第 02 部分,同目录下还有:
- BRANCH-PARITY-01-Chroma-Branch-Sync.md:让向量检索层(ChromaDB)感知分支边界——在
StoredObservation/StoredSummary/ChromaMetadata三处接口加字段、在formatObservationDocs()中把分支元数据写进 Chroma 文档、在buildWhereFilter()中用{ commit_sha: { $in: commitShas } }加$or(兼容无 commit_sha 的旧文档)实现分支感知过滤; - BRANCH-PARITY-03-MCP-Output-And-Verification.md:MCP 输出层的收尾与整体验证。
三个阶段共同指向同一架构思路:分支元数据在 SQLite 中一次性落库(migrations 24-25 给 observations 加列,Phase 01 还补了 session_summaries 的同名列),此后每一层消费方——分页查询、Chroma 同步、MCP 工具输出——都只是"把已有列透传出去"。Viewer 展示(本文主题)是这条透传链路的最后一环,让分支记忆从"数据可查"变成"用户可见"。
7. 小结
- 改动极小、链路极清晰:可选字段进两个
Observation接口(types.ts 与 worker-types.ts),SELECT 补两列(PaginationHelper.ts),卡片条件渲染一枚徽章(ObservationCard.tsx),即可让分支元数据从数据库直达界面; - 向后兼容是硬性约束:
branch?: string | null的可选声明与"truthy 才渲染徽章"的组合,保证迁移前的旧观察在类型系统与 UI 上都自然降级为"无分支",不产生占位或报错; - 验证闭环完整:
typecheck:viewer→npm test→npm run build-and-sync产出 plugin/ui/viewer.html,构建产物可人工核验新徽章的渲染结果。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00