fix: better filename resolution, choosing another agent works

This commit is contained in:
h
2026-05-21 20:11:52 +02:00
parent 2b00fa44d5
commit 93b52f0621
4 changed files with 86 additions and 46 deletions
+1 -1
View File
@@ -4,6 +4,6 @@
"version": "0.1.0", "version": "0.1.0",
"minAppVersion": "1.5.0", "minAppVersion": "1.5.0",
"description": "Send the current note to a Beaver agent via the markdown frontend.", "description": "Send the current note to a Beaver agent via the markdown frontend.",
"author": "Beaver", "author": "h",
"isDesktopOnly": false "isDesktopOnly": false
} }
+30 -36
View File
@@ -1,42 +1,36 @@
import { App, FuzzySuggestModal } from "obsidian"; import { App, FuzzySuggestModal } from "obsidian";
export class AgentPickerModal extends FuzzySuggestModal<string> {
private agents: string[];
private resolve: (agent: string | null) => void;
private resolved = false;
constructor(
app: App,
agents: string[],
resolve: (agent: string | null) => void,
) {
super(app);
this.agents = agents;
this.resolve = resolve;
this.setPlaceholder("Pick a Beaver agent…");
}
getItems(): string[] {
return this.agents;
}
getItemText(item: string): string {
return item;
}
onChooseItem(item: string): void {
this.resolved = true;
this.resolve(item);
}
onClose(): void {
super.onClose();
if (!this.resolved) this.resolve(null);
}
}
export function pickAgent(app: App, agents: string[]): Promise<string | null> { export function pickAgent(app: App, agents: string[]): Promise<string | null> {
return new Promise((resolve) => { return new Promise((resolve) => {
new AgentPickerModal(app, agents, resolve).open(); let resolved = false;
const settle = (value: string | null) => {
if (resolved) return;
resolved = true;
resolve(value);
};
const Picker = class extends FuzzySuggestModal<string> {
getItems(): string[] {
return agents;
}
getItemText(item: string): string {
return item;
}
onChooseItem(item: string): void {
settle(item);
}
onClose(): void {
super.onClose();
// onClose fires BEFORE onChooseItem in Obsidian's modal pipeline,
// so we can't resolve null synchronously here — it'd race the
// legitimate pick and steal the slot. Defer to the next tick so
// onChooseItem (if any) settles first; if no pick happened
// (Escape / click-away), this fires and we resolve null.
setTimeout(() => settle(null), 0);
}
};
const modal = new Picker(app);
modal.setPlaceholder("Pick a Beaver agent…");
modal.open();
}); });
} }
+33 -9
View File
@@ -84,15 +84,22 @@ export default class BeaverPlugin extends Plugin {
} }
private async runDifferent(file: TFile): Promise<void> { private async runDifferent(file: TFile): Promise<void> {
const agents = await this.getAgents(); try {
if (!agents) return; const agents = await this.getAgents();
if (agents.length === 0) { if (!agents) {
new Notice("Beaver: no agents available"); new Notice("Beaver: couldn't fetch agents (see console)", 6000);
return; return;
}
if (agents.length === 0) {
new Notice("Beaver: no agents available");
return;
}
const picked = await pickAgent(this.app, agents);
if (!picked) return;
await this.dispatch(file, picked);
} catch (err) {
this.notifyError("send-different", err);
} }
const picked = await pickAgent(this.app, agents);
if (!picked) return;
await this.dispatch(file, picked);
} }
private async getAgents(): Promise<string[] | null> { private async getAgents(): Promise<string[] | null> {
@@ -113,6 +120,9 @@ export default class BeaverPlugin extends Plugin {
} }
private async dispatch(file: TFile, agent: string): Promise<void> { private async dispatch(file: TFile, agent: string): Promise<void> {
const filename = this.gatewayFilename(file);
if (!filename) return;
const { editor, view } = this.findEditorFor(file); const { editor, view } = this.findEditorFor(file);
const content = editor const content = editor
? editor.getValue() ? editor.getValue()
@@ -122,7 +132,7 @@ export default class BeaverPlugin extends Plugin {
let response: ChatResponse; let response: ChatResponse;
try { try {
response = await sendChat(this.settings, { response = await sendChat(this.settings, {
filename: file.path, filename,
content, content,
agent, agent,
}); });
@@ -149,6 +159,20 @@ export default class BeaverPlugin extends Plugin {
new Notice(`Beaver: ${agent} replied`); new Notice(`Beaver: ${agent} replied`);
} }
private gatewayFilename(file: TFile): string | null {
const subpath = this.settings.vaultSubpath;
if (!subpath) return file.path;
const prefix = subpath + "/";
if (file.path === subpath || file.path.startsWith(prefix)) {
return file.path.slice(prefix.length);
}
new Notice(
`Beaver: file ${file.path} is outside the configured vault subpath (${subpath})`,
8000,
);
return null;
}
private findEditorFor(file: TFile): { private findEditorFor(file: TFile): {
editor: Editor | null; editor: Editor | null;
view: MarkdownView | null; view: MarkdownView | null;
+22
View File
@@ -5,13 +5,19 @@ import type BeaverPlugin from "./main";
export interface BeaverSettings { export interface BeaverSettings {
baseUrl: string; baseUrl: string;
token: string; token: string;
vaultSubpath: string;
} }
export const DEFAULT_SETTINGS: BeaverSettings = { export const DEFAULT_SETTINGS: BeaverSettings = {
baseUrl: "http://localhost:62993", baseUrl: "http://localhost:62993",
token: "", token: "",
vaultSubpath: "",
}; };
export function normalizeSubpath(raw: string): string {
return raw.trim().replace(/^\/+|\/+$/g, "");
}
export class BeaverSettingsTab extends PluginSettingTab { export class BeaverSettingsTab extends PluginSettingTab {
plugin: BeaverPlugin; plugin: BeaverPlugin;
@@ -51,6 +57,22 @@ export class BeaverSettingsTab extends PluginSettingTab {
}); });
}); });
new Setting(containerEl)
.setName("Vault subpath")
.setDesc(
"Folder in this Obsidian vault that maps to the gateway's vault root. " +
"Leave empty if the two vault roots match. Example: `💬 чаты`.",
)
.addText((text) =>
text
.setPlaceholder("")
.setValue(this.plugin.settings.vaultSubpath)
.onChange(async (value) => {
this.plugin.settings.vaultSubpath = normalizeSubpath(value);
await this.plugin.saveSettings();
}),
);
new Setting(containerEl) new Setting(containerEl)
.setName("Test connection") .setName("Test connection")
.setDesc("Calls GET /agents and reports the count.") .setDesc("Calls GET /agents and reports the count.")