-
Notifications
You must be signed in to change notification settings - Fork 134
Feat/enhance previous message #569
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
- Add RestoreOptionsModal component for user to select restore strategy - Implement snapshot deletion when clearing operation records - Add snapshot-based message rollback functionality - Support restoring specific message history through snapshots - Integrate double ESC press to trigger operation record cleanup
- Move snapshot creation logic from onToolUse callback to createSnapshotBeforeToolUse method - Improve code organization and maintainability - Add comprehensive error handling and debug logging - No functional changes, pure refactoring
- Extract duplicate message utility functions to ui/utils/messageUtils.ts - Refactor fork function: split 260-line function into focused helpers - Add ui/utils/forkHelpers.ts with snapshot collection and restoration logic - Optimize collectSnapshots to use parallel queries with Promise.all - Add session.getSnapshotSummary API for efficient snapshot metadata retrieval - Fix type safety: remove 'as any' casts in restoreConversationToTargetPoint - Display actual file counts in RestoreOptionsModal - Remove debug console.log statements, keep essential logging - Add null checks for cwd and sessionId in fork operation Performance improvements: - Parallel snapshot queries reduce N sequential requests to 1 batch - Single API call for snapshot summary instead of N individual calls Code quality: - Reduce codebase by 165 lines while improving maintainability - All 216 tests passing - Type-safe implementation without runtime behavior changes
- Add deleteSnapshot method to SnapshotManager - Implement session.deleteSnapshot handler in nodeBridge - Auto-delete snapshots after code-only restoration to prevent sync issues - Update ForkModal snapshot indicator from emoji to text '(code changed)' - Add comprehensive tests for snapshot deletion
- Remove history truncation logic in fork restore - Keep global command history intact for Ctrl+R reverse search - Delete unused truncateHistory function from forkHelpers - Users can now access full history even after forking to previous messages This fixes an issue where forking to a previous message would truncate the command history, breaking the ability to search historical commands from other sessions using Ctrl+R or arrow key navigation.
- Add SnapshotManager with physical file backup support - Implement VIA/FIA dual mode for snapshot creation - Add global file tracking for complete state snapshots - Optimize restore strategy with reverse processing - Add comprehensive tests for snapshot functionality - Support cross-session backup sharing via hard links - Enhance ForkModal with snapshot indicators - Implement batch file restoration for better performance Changes: - 13 files modified (+2372, -597) - Core implementation in src/utils/snapshot.ts - Integration in src/project.ts and src/session.ts - UI enhancements in src/ui/ForkModal.tsx and store.ts - Complete test coverage added
- Fix SessionConfigManager singleton pattern in nodeBridge - Add proper snapshot entry tracking with isSnapshotUpdate flag - Improve JSONL logging to record snapshot updates separately - Add snapshot state reconstruction from JSONL entries - Enhance session resume to properly rebuild snapshot state - Add comprehensive tests for snapshot reconstruction logic - Fix tracked files set rebuilding during deserialization This improves fork/resume reliability by properly handling snapshot updates and ensuring consistent state across session operations.
…tection The mtime comparison optimization was causing test failures and could lead to incorrect behavior in fast consecutive file modifications. Problem: - In hasFileChanged(), we used 'fileStats.mtimeMs <= backupStats.mtimeMs' to early-return false (file unchanged) - This failed in scenarios where files are modified rapidly (e.g., tests) - File modification time might be <= backup time due to: * Low filesystem time precision * Fast consecutive writes * System clock adjustments Solution: - Removed the mtime early-return optimization - Always compare file content after metadata checks (size, mode) - Prioritizes correctness over minor performance gain Impact: - All integration tests now pass - Snapshot accuracy improved in edge cases - Minimal performance impact (most changes alter file size anyway) Ref: Claude Code's implementation also has this optimization but their usage scenarios may be more controlled. For our test scenarios and general reliability, content comparison is more appropriate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements a comprehensive snapshot system for tracking file history and enabling code rollback functionality. The implementation follows Claude Code's design patterns and adds ~4300+ lines of code (45% tests and documentation).
Key Changes:
- Physical backup system with global file tracking
- Dual-mode snapshot operations (VIA/FIA)
- Complete restoration capabilities
- UI enhancements for snapshot visualization
- Comprehensive test coverage
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/utils/snapshot.ts |
Core snapshot manager with 1119 lines implementing backup/restore logic |
src/utils/snapshot.test.ts |
Unit tests for snapshot manager (615 lines) |
src/session.snapshot.integration.test.ts |
Integration tests for session snapshot workflows |
src/ui/RestoreOptionsModal.tsx |
Modal for selecting restore modes (conversation/code/both) |
src/ui/utils/forkHelpers.ts |
Helper functions for fork operations and file restoration |
src/ui/utils/messageUtils.ts |
Message preview and timestamp formatting utilities |
src/ui/store.ts |
Enhanced fork logic with code/conversation restoration options |
src/ui/ForkModal.tsx |
Enhanced to display snapshot indicators |
src/ui/App.tsx |
Integration of RestoreOptionsModal and snapshot cache |
src/session.ts |
SessionConfigManager integration with SnapshotManager |
src/project.ts |
Snapshot creation before write/edit tool execution |
src/nodeBridge.ts |
RPC handlers for snapshot operations |
src/nodeBridge.types.ts |
Type definitions for snapshot RPC |
src/message.ts |
SnapshotMessage type definition |
src/jsonl.ts |
Snapshot message logging |
src/commands/log.ts |
Snapshot visualization in HTML logs |
docs/designs/2025-12-23-fork-code-rollback.md |
Comprehensive design documentation |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import { mkdirSync, rmSync, writeFileSync, readFileSync } from 'fs'; | ||
| import { join } from 'pathe'; | ||
| import { randomUUID } from './utils/randomUUID'; | ||
| import { SnapshotManager } from './utils/snapshot'; |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused import SnapshotManager.
| import { SnapshotManager } from './utils/snapshot'; |
src/utils/snapshot.ts
Outdated
| const newLines = newContent.split('\n'); | ||
|
|
||
| // Simple line-based diff | ||
| const maxLines = Math.max(oldLines.length, newLines.length); |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable maxLines.
| const maxLines = Math.max(oldLines.length, newLines.length); |
src/utils/snapshot.ts
Outdated
| let deletedFilesCount = 0; | ||
| let failedFilesCount = 0; | ||
|
|
||
| for (const [relativePath, backup] of Object.entries( |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable relativePath.
| for (const [relativePath, backup] of Object.entries( | |
| for (const [, backup] of Object.entries( |
src/utils/snapshot.ts
Outdated
| for (const [relativePath, backup] of Object.entries( | ||
| snapshot.trackedFileBackups, | ||
| )) { | ||
| if (!backup.backupFileName) continue; | ||
|
|
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable relativePath.
| for (const [relativePath, backup] of Object.entries( | |
| snapshot.trackedFileBackups, | |
| )) { | |
| if (!backup.backupFileName) continue; | |
| for (const backup of Object.values(snapshot.trackedFileBackups)) { | |
| if (!backup.backupFileName) continue; |
- Optimize file change detection with intelligent comparison: existence -> metadata -> content - Remove mtime optimization due to unreliability in fast consecutive writes and clock adjustments - Add comprehensive debug logging for snapshot operations - Enhance backup file management with proper permission preservation - Improve snapshot restoration with detailed diff statistics - Add session backup copying for resume/continuation operations - Refine snapshot entry tracking with isSnapshotUpdate flag support
…lity modules - Move loadSnapshotEntries() from session.ts to utils/snapshot.ts - Move restoreCodeToTargetPoint() and buildRestoreConversationState() from store.ts to ui/utils/forkHelpers.ts - Move RestoreConversationState interface to forkHelpers.ts - Update imports accordingly to reflect new locations - Improve code organization by grouping related functions in their respective modules
|
对以上问题进行了修复,辛苦继续 CR |
| // This is similar to Claude Code's H81 function | ||
| if (currentSessionId && sessionId !== currentSessionId) { | ||
| try { | ||
| const snapshotManager = sessionConfigManager.getSnapshotManager(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
需要考虑跨端
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
跨端这个确实没考虑到
src/nodeBridge.ts
Outdated
| const sessionConfigManager = new SessionConfigManager({ | ||
| logPath: context.paths.getSessionLogPath(sessionId), | ||
| }); | ||
| const sessionConfigManager = await this.getSessionConfigManager( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pr 无关吧
快照系统完整分析报告
这套快照系统通过参考 Claude Code 的成熟设计,结合项目特点进行优化,实现了:
系统已在生产环境中稳定运行,为用户提供了强大的代码回滚和对话分支功能。
虽然新增代码量4300+,但是45%是测试文件和设计文档。
考虑分PR提交,在拆分过程中,发现强行拆分会破坏逻辑,对测试也十分不友好,所以还是整合提交了。
为了方便CR这里罗列一下该PR涉及的代码变更以及实现方式
一、文件变更清单
新增文件(A)
src/utils/snapshot.tssrc/utils/snapshot.test.tssrc/session.snapshot.integration.test.tssrc/ui/RestoreOptionsModal.tsxsrc/ui/utils/forkHelpers.tssrc/ui/utils/messageUtils.tsdocs/designs/2025-12-23-fork-code-rollback.md修改文件(M)
src/session.tsloadSnapshotEntries()src/jsonl.tsaddSnapshotMessage()方法src/project.tscreateSnapshotBeforeToolUse()src/commands/log.tssrc/ui/App.tsxsrc/ui/ForkModal.tsxsrc/ui/store.tssrc/message.tsSnapshotMessage类型src/nodeBridge.ts二、核心类与方法详解
2.1 SnapshotManager 类
核心属性
数据结构
2.2 核心方法分类详解
A. 快照创建流程
1.
trackFileEdit(filePaths, messageUuid)核心追踪方法,支持增量更新
流程图:
关键逻辑:
trackedFiles集合isSnapshotUpdate用于日志重建2.
createNewSnapshot(filePaths, messageUuid)创建完整快照(Claude Code FIA 模式)
核心特性:
详细流程:
示例场景:
3.
createBackupFile(filePath, version)创建物理备份文件
执行步骤:
生成备份文件名
确保备份目录存在
读取并写入备份
复制文件权限
返回备份元数据
4.
hasFileChanged(filePath, backupFileName)智能文件变更检测(同步方法)
三级检测策略:
为什么不用 mtime?
❌ 已移除 mtime 优化
✅ 使用内容对比
5.
findMaxVersionAndBackup(relativePath)查找文件的最高版本号
用途:
算法:
B. 快照恢复流程
6.
restoreSnapshot(messageUuid, dryRun)恢复文件到快照时的状态
参数说明:
messageUuid: 要恢复到的消息快照dryRun: 预览模式,不实际修改文件详细流程:
diff 计算逻辑:
简化的逐行比较:
7.
restoreSnapshotFiles(messageUuid, filePaths)部分文件恢复
用途:
实现:
C. 快照重建与序列化
8.
static rebuildSnapshotState(snapshotEntries)从 JSONL 日志重建快照状态(Claude Code qH0 模式)
核心原理:
算法详解:
示例:
为什么从后向前查找?
9.
loadSnapshotEntries(entries)加载快照条目并重建状态
执行步骤:
10.
serialize()/deserialize()快照数据序列化
压缩效果:
D. 会话备份复制
11.
copyBackupsFromSession(snapshots, sourceSessionId)跨会话复制备份文件(用于 fork/resume)
使用场景:
优化策略:
硬链接优势:
降级策略:
2.3 工具函数
createToolSnapshot()工具层快照创建入口
完整流程:
调用链:
copySessionBackups()独立的会话备份复制函数
与
SnapshotManager.copyBackupsFromSession()的区别:三、完整流程图
3.1 快照保存流程
graph TB A[用户执行 write/edit 工具] --> B[Project.createSnapshotBeforeToolUse] B --> C{工具是 write/edit?} C -->|否| Z[跳过快照] C -->|是| D[提取 file_path 参数] D --> E[createToolSnapshot] E --> F[sessionConfigManager.getSnapshotManager] F --> G[snapshotManager.trackFileEdit] G --> H{快照已存在?} H -->|否| I[createNewSnapshot] H -->|是| J[增量更新] I --> K[遍历所有 trackedFiles] K --> L{文件是否存在?} L -->|否| M[标记删除: backupFileName=null] L -->|是| N[hasFileChanged 检测] N -->|未变化| O[复用旧备份] N -->|已变化| P[createBackupFile] P --> Q[生成备份文件名: hash@v版本] Q --> R[读取文件内容] R --> S[写入备份目录: ~/.neovate/file-history/sessionId/] S --> T[复制文件权限] J --> U[仅为新文件创建备份] U --> P M --> V[构造 MessageSnapshot] O --> V T --> V V --> W[返回 snapshot + isUpdate 标记] W --> X[sessionConfigManager.saveSnapshots] X --> Y[序列化: JSON → gzip → base64] Y --> AA[写入 session.jsonl config 行] W --> AB[jsonlLogger.addSnapshotMessage] AB --> AC[构造 SnapshotMessage] AC --> AD[追加到 session.jsonl] style I fill:#e1f5ff style J fill:#fff4e1 style P fill:#ffe1e1 style AB fill:#f0e1ff关键节点说明:
Project.createSnapshotBeforeToolUse()在工具执行前拦截3.2 快照恢复流程
graph TB A[用户触发恢复操作] --> B[ForkModal 选择消息] B --> C[RestoreOptionsModal 显示选项] C --> D{选择模式} D -->|both| E[Fork 对话 + 恢复代码] D -->|conversation| F[仅 Fork 对话] D -->|code| G[仅恢复代码] D -->|cancel| H[取消操作] E --> I[App.tsx 处理 restore] G --> I I --> J[snapshotManager.restoreSnapshot dryRun=true] J --> K[预览模式:计算 diff 统计] K --> L[显示预览信息] L --> M[用户确认] M -->|确认| N[snapshotManager.restoreSnapshot dryRun=false] M -->|取消| H N --> O[遍历快照中的文件备份] O --> P{backupFileName?} P -->|null| Q[删除工作目录文件: unlink] P -->|存在| R[读取备份文件内容] R --> S{当前文件存在?} S -->|是| T[calculateDiff 计算差异] S -->|否| U[insertions = 备份行数] T --> V[写入工作目录] U --> V V --> W[恢复文件权限: chmod] Q --> X[统计变更] W --> X X --> Y[返回 RestoreResult] Y --> Z[UI 显示结果] Z --> AA[显示文件数、insertions、deletions] style C fill:#fff4e1 style J fill:#e1f5ff style N fill:#ffe1e1 style Y fill:#e1ffe1流程特点:
dryRun=true预览dryRun=false实际恢复3.3 日志重建流程(会话恢复)
graph TB A[Session.resume 加载会话] --> B[SessionConfigManager 构造] B --> C[getSnapshotManager 初始化] C --> D[加载 config.snapshots] D --> E[deserialize 反序列化] E --> F[base64 → gzip → JSON] F --> G[创建 SnapshotManager 实例] G --> H[loadSnapshotEntries 从 JSONL] H --> I[解析 session.jsonl] I --> J{逐行解析} J --> K{type?} K -->|file-history-snapshot| L[提取快照条目] K -->|其他| M[跳过] L --> N[收集所有 SnapshotEntry] N --> O[rebuildSnapshotState 重建] O --> P{遍历条目} P --> Q{isSnapshotUpdate?} Q -->|false| R[直接追加新快照] Q -->|true| S[查找匹配的 messageUuid] S --> T{找到?} T -->|是| U[替换旧快照] T -->|否| V[当作新快照追加] R --> W[构建最终快照数组] U --> W V --> W W --> X[加载到 snapshots Map] X --> Y[rebuildTrackedFilesSet] Y --> Z[从所有快照提取文件路径] Z --> AA[重建全局 trackedFiles] AA --> AB[快照状态完全恢复] AB --> AC[snapshots Map: 最新状态] AC --> AD[snapshotEntries Map: 原始条目] AD --> AE[trackedFiles Set: 全局追踪] style O fill:#e1f5ff style S fill:#fff4e1 style AA fill:#e1ffe1 style AB fill:#f0e1ff重建原理:
isSnapshotUpdate=false→ 新快照isSnapshotUpdate=true→ 替换更新3.4 数据流图
四、日志系统
4.1 JSONL 日志结构
快照消息格式
示例:
{ "type": "file-history-snapshot", "messageId": "a1b2c3d4", "snapshot": { "messageId": "a1b2c3d4", "timestamp": "2025-12-30T15:30:00.000Z", "trackedFileBackups": { "src/App.tsx": { "backupFileName": "f1e2d3c4b5a69870@v2", "version": 2, "backupTime": "2025-12-30T15:30:00.000Z" }, "src/utils/helper.ts": { "backupFileName": "a9b8c7d6e5f41230@v1", "version": 1, "backupTime": "2025-12-30T15:30:00.000Z" } } }, "isSnapshotUpdate": false }配置行格式
示例:
{ "type": "config", "config": { "snapshots": "H4sIAAAAAAAAA+2W...(base64 字符串)...==", "model": "claude-3-5-sonnet-20241022", "approvalMode": "default" } }4.2 日志写入策略
JsonlLogger.addSnapshotMessage()
设计哲学:
原理:
优势:
4.3 日志可视化
commands/log.ts 增强
buildRenderableItems() 算法
时间线合并:
HTML 渲染特性
快照条目显示:
样式:
图标说明:
isSnapshotUpdate: trueisSnapshotUpdate: false交互功能
点击消息查看详情
快照折叠/展开
时间线导航
五、UI 交互
5.1 RestoreOptionsModal 组件
选项列表
hasSnapshot === truehasSnapshot === true组件接口
UI 布局
关键提示:
说明快照只追踪通过 write/edit 工具修改的文件。
5.2 ForkModal 组件
快照状态指示
显示效果:
用途:
5.3 App.tsx 恢复流程
恢复处理逻辑
六、关键技术细节
6.1 版本号管理
全局递增策略
特性:
示例:
6.2 备份文件命名
命名规则
格式:
{hash16}@v{version}示例:
设计优势
@v后缀清晰标识版本6.3 备份存储结构
目录层次
空间优化
1. 硬链接(跨会话复用)
2. 版本复用(会话内复用)
空间节省示例:
6.4 智能变更检测
三级检测策略
性能分析
为什么不用 mtime?
问题:
解决方案:
✅ 始终进行内容对比,确保 100% 准确
6.5 压缩存储
序列化流程
数据转换链:
压缩效果
测试数据:
优势:
6.6 并发安全
写入锁(TODO)
当前状态:
未来改进:
追加式日志安全性
优势:
appendFileSync通常是原子的风险:
七、设计模式
7.1 Claude Code 模式映射
trackFileEditVIAcreateNewSnapshotFIArebuildSnapshotStateqH0copyBackupsFromSessionH81hasFileChanged参考价值:
7.2 快照更新策略
首次创建(isSnapshotUpdate: false)
日志输出:
{ "type": "file-history-snapshot", "messageId": "msg-uuid-1", "isSnapshotUpdate": false, // ← 新建 "snapshot": { "trackedFileBackups": { "src/App.tsx": { "backupFileName": "hash@v1", ... } } } }增量更新(isSnapshotUpdate: true)
日志输出:
{ "type": "file-history-snapshot", "messageId": "msg-uuid-1", "isSnapshotUpdate": true, // ← 更新 "snapshot": { "trackedFileBackups": { "src/App.tsx": { "backupFileName": "hash@v1", ... }, // 保留 "src/utils/helper.ts": { "backupFileName": "hash@v1", ... } // 新增 } } }重建时处理
效果:
7.3 全局文件追踪
trackedFiles 集合设计
核心原则:
完整快照效果
优势分析
重建逻辑
从快照重建追踪集合:
trackedFiles集合7.4 追加式日志设计
设计原则
JSONL 文件演进:
重建后结果:
对比其他设计
为什么选择追加式?
八、总结
8.1 核心特性
✅ 完整的文件历史追踪
实现方式:
Set<string> trackedFiles✅ 智能增量更新
优势:
✅ 可靠的状态重建
保证:
✅ 高效的存储优化
综合效果:
✅ 精确的变更检测
特点:
✅ 完整的日志审计
审计能力:
✅ 友好的 UI 交互
ForkModal:
RestoreOptionsModal:
Log 可视化:
8.2 架构优势
参考 Claude Code 成熟设计
createNewSnapshot()trackFileEdit()rebuildSnapshotState()copyBackupsFromSession()借鉴优势:
针对项目特点优化
创新点:
双重存储策略
智能变更检测
灵活的恢复选项
完整的可视化
8.3 使用场景
场景 1: 代码回滚
场景 2: 对话分支
场景 3: 完整恢复
场景 4: 会话恢复
8.4 性能指标
时间复杂度
空间复杂度
8.5 未来改进
短期优化
中期功能
长期愿景
8.6 技术栈
8.7 关键指标总结
附录
A. 常用调试环境变量
调试输出示例:
B. 文件路径约定
C. 类型定义速查