fix(streaming): re-resolve the leaf per delta so deltas stay in the right file

This commit is contained in:
hh
2026-08-28 00:57:43 +02:00
parent d2e9d5911c
commit 755a475f27
+25 -21
View File
@@ -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<void> {
private async writeBack(file: TFile, newContent: string): Promise<void> {
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);
}