diff --git a/src/main.ts b/src/main.ts index e7d27ec..d14bb22 100644 --- a/src/main.ts +++ b/src/main.ts @@ -252,9 +252,14 @@ export default class BeaverPlugin extends Plugin { const filename = this.gatewayFilename(file); if (!filename) return; - const { editor, view } = this.findEditorFor(file); - const content = editor - ? editor.getValue() + // Snapshot the buffer if the file is open, otherwise read disk. We + // don't keep the editor: an ``Editor`` belongs to a leaf, not a + // file, so if the user opens another note in the same tab mid- + // stream the old handle now points at the wrong document. Every + // delta and the final write re-resolve the leaf by ``file.path``. + const initialEditor = this.findEditorFor(file); + const content = initialEditor + ? initialEditor.getValue() : await this.app.vault.read(file); const notice = new Notice(`Beaver: sending to ${agent}…`, 0); @@ -269,7 +274,10 @@ export default class BeaverPlugin extends Plugin { // write the partial to disk. The gateway also skips its own // intermediate file writes on this endpoint, so the local // file (and Obsidian Sync's copy) only sees the final state - // once. + // once. Re-resolved per delta: if ``file`` is no longer + // open anywhere, the delta is dropped and the final + // ``vault.modify`` catches the buffer up. + const editor = this.findEditorFor(file); if (editor) spliceIntoEditor(editor, newContent); }, onDone: (resp) => { @@ -305,7 +313,7 @@ export default class BeaverPlugin extends Plugin { } if (typeof resp.new_content === "string") { - await this.writeBack(file, editor, view, resp.new_content); + await this.writeBack(file, resp.new_content); } new Notice(`Beaver: ${agent} replied`); } @@ -324,29 +332,22 @@ export default class BeaverPlugin extends Plugin { return null; } - private findEditorFor(file: TFile): { - editor: Editor | null; - view: MarkdownView | null; - } { + private findEditorFor(file: TFile): Editor | null { // Walk open markdown views — we want the editor instance that owns - // ``file`` so we can use setValue (keeps the buffer's edit history - // intact) instead of falling back to vault.modify. + // ``file`` *right now* so we can splice (keeps the buffer's edit + // history intact) instead of falling back to vault.modify. Never + // cached by callers: ``view.file`` changes when the user navigates + // within the same leaf, and is ``null`` between switches. const leaves = this.app.workspace.getLeavesOfType("markdown"); for (const leaf of leaves) { const view = leaf.view as MarkdownView; - if (view?.file?.path === file.path) { - return { editor: view.editor, view }; - } + if (view?.file?.path === file.path) return view.editor; } - return { editor: null, view: null }; + return null; } - private async writeBack( - file: TFile, - editor: Editor | null, - _view: MarkdownView | null, - newContent: string, - ): Promise { + private async writeBack(file: TFile, newContent: string): Promise { + const editor = this.findEditorFor(file); if (editor) { // Reuse the splice path so the final write (frontmatter refresh // at the top + USER_SCAFFOLD appended at the bottom) preserves @@ -354,6 +355,9 @@ export default class BeaverPlugin extends Plugin { spliceIntoEditor(editor, newContent); return; } + // File not open (anymore): write to disk. Obsidian refreshes any + // buffer that opens it later, and a leaf that shows it in another + // pane gets the update through the vault event. await this.app.vault.modify(file, newContent); }