Tolaria ADR-0135 深度解析:外部编辑后如何立即干净地刷新活动笔记
Tolaria ADR-0135 深度解析外部编辑后如何立即干净地刷新活动笔记【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolariaTolaria 是一款以文件系统为唯一事实来源filesystem-first的 Markdown 知识库桌面应用外部进程如 Codex 等 AI 编码代理、其他编辑器或 Git 拉取随时可能改写 vault 中的笔记。本文基于架构决策记录 ADR-0135 展开当外部变更批次命中当前打开且无未保存修改的笔记时编辑器必须立即从磁盘重新挂载而不是停留在陈旧内容上等待重启。读完后你会理解 Tolaria 单一外部刷新协调器refreshPulledVaultState()的完整决策规则、保护未保存缓冲的边界条件以及该策略相对前代方案ADR-0111演进背后的取舍。一、问题背景从 ADR-0111 到 ADR-0135 的演进要理解 ADR-0135 为什么存在需要先回顾它的前身。Tolaria 的外部刷新策略经历了三次迭代ADR-0071外部 vault 刷新与干净标签页重开确立了git 拉取、AI 代理写入等外部变更必须走同一条共享协调路径的原则ADR-0111路径感知的外部 vault 刷新与焦点编辑器保护把规则收窄为只有活动文件本身变化、且编辑器干净且未聚焦时才重挂载活动编辑器避免无关的 watcher 事件打散光标状态。ADR-0111 解决了无谓的重挂载抖动却引入了一个反向问题——正如 ADR-0135 的 Context 所述如果 Codex 或另一个外部进程编辑了当前正打开且干净的笔记编辑器保持聚焦那么这条规则将无限期地保住旧编辑器实例。由于没有任何后续的安全重挂载触发点有保证用户会看到陈旧内容直到整个应用重启。ADR-0135 的立场因此非常明确Tolaria 的文件系统优先模型要求干净的内存编辑器状态在当前会话内收敛到磁盘文件。未保存的本地缓冲仍然需要保护但编辑器持有焦点本身不构成在命中活动文件时继续显示陈旧内容的理由。二、核心决策命中活动文件就立即重挂载无论焦点状态ADR-0135 的决策一句话概括当外部变更批次changed-path batch包含该笔记时外部 vault 刷新会立即重挂载干净的活动笔记——无论编辑器焦点在哪里。该决策落在共享函数refreshPulledVaultState()上。它定义了 7 条规则按顺序执行对每个外部变更批次一起重载 vault 条目、文件夹和已保存视图如果没有活动笔记共享重载结束后即停止如果活动笔记在异步重载期间发生了变化用户切了标签停止操作而不是用陈旧上下文重开如果活动笔记有未保存的本地编辑保持当前编辑器缓冲挂载不动如果活动文件消失了关闭标签页而不是留一个陈旧编辑器如果变更批次命中干净的活跃文件关闭并重新从磁盘打开活动标签——即使焦点此刻在富文本或原始编辑器内部未知或不相关的变更批次只刷新 vault 派生状态不重挂载活动编辑器。Git 拉取、AI 代理刷新回调和文件系统 watcher 批次仍然通过这同一个协调助手收敛而不是各自发明一套重载策略。三、源码实现refreshPulledVaultState()的决策流水线实现位于 src/utils/pulledVaultRefresh.ts整个协调器就是一个纯函数通过PulledVaultRefreshOptions接口第 4–18 行接收全部依赖interface PulledVaultRefreshOptions { activeTabPath: string | null // 刷新开始时记录的活动标签路径 getActiveTabPath?: () string | null // 重载完成后读取最新活动路径 closeAllTabs: () void hasUnsavedChanges: (path: string) boolean isActiveTabContentCurrent?: (path: string) Promiseboolean | boolean reloadFolders: () Promiseunknown | unknown reloadVault: () PromiseVaultEntry[] reloadViews: () Promiseunknown | unknown replaceActiveTab: (entry: VaultEntry) Promisevoid refocusActiveEditor?: (path: string) void shouldRefocusActiveEditor?: () boolean updatedFiles: string[] // 变更路径批次外部刷新契约的一部分 vaultPath: string }几个设计要点值得注意updatedFiles是契约字段。调用方应尽量传入具体文件路径空数组表示不知道哪些文件变了例如某些 watcher 场景此时按规则 7 只刷派生状态、不动编辑器。activeTabPathgetActiveTabPath双通道。前者是刷新启动时的快照后者在异步重载之后再次读取当前活动标签用于检测规则 3 的重载期间用户切了标签场景。isActiveTabContentCurrent提供免重挂载快路径。如果调用方声明当前标签内容与磁盘一致且笔记未被外部移动则跳过重挂载进一步减少无谓抖动。shouldRefocusActiveEditor/refocusActiveEditor成对出现。重挂载后若焦点原本在编辑器内部通过自定义事件把焦点重新放回编辑器。3.1 并发重载条目、文件夹、视图一次到位主函数 refreshPulledVaultState() 的第一步第 164–168 行const [entries] await Promise.all([ reloadVault(), Promise.resolve(reloadFolders()), Promise.resolve(reloadViews()), ])三条重载路径并发执行保证外部刷新后侧边栏、文件夹树和自定义视图与笔记列表同时收敛——这对应 ADR 规则 1。3.2 阻塞判定何时到此为止重载完成后isActivePathBlocked()第 73–83 行判定是否放弃后续操作function isActivePathBlocked(options: { activeTabPath: string | null latestActiveTabPath: string | null hasUnsavedChanges: PulledVaultRefreshOptions[hasUnsavedChanges] }): boolean { const { activeTabPath, latestActiveTabPath, hasUnsavedChanges } options if (!activeTabPath) return true // 规则 2无活动笔记 if (!latestActiveTabPath) return true if (didActivePathChange({ initialPath: activeTabPath, latestPath: latestActiveTabPath })) return true // 规则 3 return hasUnsavedChanges(latestActiveTabPath) // 规则 4未保存编辑受保护 }注意规则 4 检查的是重载之后的最新活动路径而不是启动时的快照——这保证了用户恰好在重载期间切到一个有未保存编辑的标签这种情况也被正确保护。3.3 查找替身条目原路径命中 vs 外部移动未被阻塞后协调器先按原路径在重载后的条目里找笔记findByNotePath找不到时再调用findExternallyMovedActiveEntry()第 40–59 行当活动笔记被外部进程改名/移动到子文件夹时它通过文件名相同 新路径在变更批次中 唯一候选三个条件识别出移动后的条目。候选唯一才采信多个同名片段时保守放弃。这是测试用例retargets a focused active tab when the active note was moved externally覆盖的行为。shouldReplaceActiveEntry()第 85–102 行决定是否需要替换外部移动一律替换原路径条目存在时则以didPullUpdateActiveNote()为准——它把批次中的相对路径经resolveUpdatedFilePath()归一化含 vault 前缀拼接再用notePathsMatch()与活动路径比对。路径归一化还处理了 macOS 的/tmp与/private/tmp软链接别名问题对应测试matches macOS /tmp and /private/tmp aliases when reloading the active tab entry。3.4 应用替换关闭、重开、重新聚焦最终的applyActiveEntryReplacement()第 120–145 行执行 ADR 规则 6 的核心动作const shouldRefocus shouldRefocusActiveEditor?.() true if (!notePathsMatch(activePath, replacementEntry.path)) closeAllTabs() // 路径变了外部移动先清掉全部标签 await replaceActiveTab(replacementEntry) // 从磁盘重开活动标签 if (shouldRefocus) refocusActiveEditor?.(replacementEntry.path) // 焦点原本在编辑器内则放回 return true如果协调到最后发现活动文件在新条目列表中不存在!replacementEntry则执行closeAllTabs()——即 ADR 规则 5文件消失了就关标签不留陈旧编辑器。四、三条触发链路如何汇入同一个协调器ADR 强调git 拉取、AI 代理刷新回调、文件系统 watcher 批次继续收敛于这单一助手。在 src/App.tsx 中可以逐一验证4.1 统一入口handleVaultUpdatehandleVaultUpdate()第 619–654 行是所有链路的公共装配点const handleVaultUpdate useCallback(async ( updatedFiles: string[], options: { vaultPath?: string } {}, ) { const updateVaultPath options.vaultPath ?? resolvedPath const entries await refreshPulledVaultState({ activeTabPath: noteActiveTabPath, closeAllTabs, getActiveTabPath: () noteActiveTabPathRef.current, hasUnsavedChanges: (path) vault.unsavedPaths.has(path), isActiveTabContentCurrent, reloadFolders: vault.reloadFolders, reloadVault: vault.reloadVault, reloadViews: vault.reloadViews, replaceActiveTab: handleReplaceActiveTab, refocusActiveEditor, shouldRefocusActiveEditor: isActiveElementInsideEditorSurface, updatedFiles, vaultPath: updateVaultPath, }) await refreshGitModifiedFiles() return entries }, [ ... ])两处关键接线hasUnsavedChanges绑定vault.unsavedPaths——未保存路径集合是规则 4 的判定依据保证 watcher、pull 和代理刷新都不会冲掉未落盘的本地内容shouldRefocusActiveEditor绑定isActiveElementInsideEditorSurface——DOM 层判断焦点是否真的在富文本/原始编辑器表面内配合refocusActiveEditor派发laputa:focus-editor自定义事件第 589–591 行把焦点放回重开后的编辑器。isActiveTabContentCurrent的实现第 592–610 行通过 Tauri 命令get_note_content读磁盘原文与活动标签的内存内容逐字节比对——内容一致则跳过重挂载。比对失败时保守返回false即正常走重挂载流程。4.2 文件系统 watcheruseVaultWatcher第 719–724 行监听全部已打开 vault 的路径变更经handleFocusedVaultUpdate转发主窗口直接走handleVaultUpdate独立笔记窗口note window则先走 src/utils/noteWindowVaultRefresh.ts 中的轻量路径单条目标载、必要时回落到refreshFullVault。这正是 ADR 中文件 watcher 批次的来源也是 ADR-0135 要修正的核心场景——Codex 直接改写磁盘文件时没有任何 Git 参与唯一能感知到的信号就是 watcher 批次里的路径列表。4.3 Git 自动拉取与 AI 代理回调Git 自动同步useAutoSync第 725–737 行按settings.auto_pull_interval_minutes周期拉取拉取成功后以onVaultUpdated: handlePulledVaultUpdate回调携带具体变更文件列表进入同一协调器。AI 代理回调useAiActivity 的onVaultChanged回调第 876 行在代理写入笔记后调用handlePulledVaultUpdate(path ? [path] : [], resolvedPath)AI 工作区窗口侧则由 useVaultBridge 提供handleAgentFileModified/handleAgentVaultChanged第 132–141 行内部同样委托给refreshPulledVaultState()第 45–52 行。三条链路全部通过具体路径批次 单一协调函数收敛没有任何一处为外部刷新单独发明重载逻辑——这与 ADR 的结论一致也让策略演进只需修改一个文件。五、测试证据规则逐条可验证src/utils/pulledVaultRefresh.test.ts 的 11 个用例与 ADR 规则几乎一一对应测试用例验证的规则reloads vault-derived data and refreshes the active note when pull updated it规则 1 6并发重载三路命中活动文件则replaceActiveTabrefreshes the clean active tab even when focused after an external watcher update changed that noteADR-0135 的核心行为焦点在编辑器内也照样重挂载keeps the active tab mounted when updates do not include the active note/...unknown changed files规则 7无关或空批次不动编辑器skips tab replacement when the active note has unsaved edits规则 4未保存缓冲优先keeps the active tab mounted when its current content already matches diskisActiveTabContentCurrent免重挂载快路径refocuses the editor after refreshing a focused clean active tab重挂载后refocusActiveEditor被调用焦点恢复skips stale tab replacement when the active note changes during reload规则 3重载期间用户切标签则放弃closes the tab when the pulled note disappeared from the reloaded vault规则 5文件消失即关标签retargets a focused active tab when the active note was moved externally外部移动改名/换目录时重定向到新路径matches macOS /tmp and /private/tmp aliases ...路径归一化对符号链接别名的兼容其中refreshes the clean active tab even when focused after an external watcher update changed that note就是 ADR-0135 相对 ADR-0111 行为变化的直接回归保障同一输入干净活动笔记 批次命中 焦点在编辑器内0111 时代会保留旧挂载0135 之后必须replaceActiveTab。六、备选方案与取舍ADR 完整记录了三个候选方案立即重挂载干净活动笔记选定方案为 Codex 等外部笔记编辑恢复文件系统收敛性同时保留未保存本地编辑。代价是一个持有焦点的干净编辑器在自身文件被外部修改时会丢失光标状态。沿用 ADR-0111 的焦点编辑器保护不打扰光标但活动编辑器可能无限期陈旧。延迟到失去焦点时再重载减少焦点中断但要新增一套待刷新状态机且活动编辑器仍可能在无界的编辑会话中显示过期的磁盘内容。选定方案的取舍逻辑值得借鉴Tolaria 把编辑器连续性让位于文件即事实来源的一致性但用两个精确定制的边界条件控制破坏面——有未保存编辑则绝不重挂载hasUnsavedChanges内容已与磁盘一致则跳过重挂载isActiveTabContentCurrent。焦点丢失这类次要代价则通过重挂载后自动回焦refocusActiveEditor缓解。七、结论与影响面ADR-0135 带来的可验证后果对当前打开的干净笔记的外部编辑无需重启 Tolaria即可见未保存的本地内容始终拥有权威地位不会被 watcher、pull 或代理刷新替换变更路径批次成为外部刷新契约的一部分——调用方应尽可能传入具体文件路径watcher 提供路径、useAiActivity传入path、自动同步传入拉取变更列表空批次则安全退化为只刷派生状态无关的 watcher 事件仍不会重挂载活动编辑器整个 vault 的批量变动只有在命中活动文件本身时才会扰动编辑器ADR-0111 由此被更强的文件系统收敛规则取代。对维护者的实践启示同样来自这份 ADR外部刷新策略应该集中在单一协调助手中演进。从仓库结构看src/utils/pulledVaultRefresh.ts目前只被 src/App.tsx、src/hooks/useVaultBridge.ts 和 src/utils/noteWindowVaultRefresh.ts 引用正是这条单点收敛纪律的体现——未来任何新的外部变更来源新代理适配、新同步机制都应接入refreshPulledVaultState()而不是在各自模块里叠加临时重载逻辑。相关文档可进一步延伸阅读文件系统 watcher 设计ADR-0089、外部 vault 刷新与干净标签页重开ADR-0071、总体架构 与 抽象索引 中对refreshPulledVaultState的登记。【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolaria创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考