diff --git a/README.md b/README.md index 425f521..6d90b4c 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ Two things: - **The panel**: Beaver's conversations next to your notes - the master thread, branches, deep chats - with the live tree of tool calls and subagents, the composer, and the same actions the admin has (branch with a seed, return a branch to Telegram or hide it, the memory flag, close with a digest). A narrow sidedock column, a full tab, or the phone screen. It follows the active note's `conversation_id` unless you pin it. - **Send the current note** to Beaver's markdown frontend (`POST /chat/stream`) and stream the agent's reply back into the buffer. +![The panel as a tab: the conversation rail on the left, the master thread with its tool calls on the right](docs/panel-tab.webp) + +![The panel as a column in the right sidedock, next to the note](docs/panel-column.webp) + ## Commands - **Beaver: Open panel in a tab** - the wide layout with the conversation rail on the left (also the ribbon icon). @@ -20,7 +24,7 @@ The UI is `beaver-gateway/ui/src/lib/panel`, compiled into this plugin by `esbui Theme: `src/tailwind.css` maps the panel's tokens onto Obsidian's variables on `.beaver-root`, exactly as beaver-calendar does with `.bcal-root` - the app's font, accent, radii and the current theme, light or dark. Menus and dialogs portal to a `.beaver-root.beaver-portal` layer on `body`, above the sidedocks. -Markdown in the thread goes through Obsidian's `MarkdownRenderer`: `[[links]]` open the note, external links open the browser. A deep chat's row offers **Open note** (its markdown binding, under the vault subpath). +Markdown in the thread goes through Obsidian's `MarkdownRenderer`: `[[links]]` open the note, external links open the browser. A deep chat's row offers **Open note** (its markdown binding, under the chats folder). Layout: below 48rem of panel width the conversation switcher sits in the header (kind, title, live dot, follow toggle); wider than that the grouped rail (master, branches, deep chats, jobs) sits on the left. Right-click a row for its actions; on touch the `…` button is always visible. `↑`/`↓` walk the rows, `/` jumps to the search, `⌘↩` sends. @@ -29,7 +33,7 @@ Layout: below 48rem of panel width the conversation switcher sits in the header - **Base URL** - markdown frontend root, e.g. `https://host/md`. - **API origin** - where `/api/…` lives; empty derives it from the base URL (`…/md` → `…`). - **Bearer token** - `messages` scope for sending, `api` scope for the panel (or a `*` bootstrap token). -- **Vault subpath** - the folder in this vault that maps to the gateway's vault root. +- **Chats folder** - the folder in this vault where the deep chats live, the one the gateway's markdown frontend writes to. Paths the gateway hands out are relative to it. Changing settings remounts open panels. diff --git a/docs/panel-column.webp b/docs/panel-column.webp new file mode 100644 index 0000000..72006ec Binary files /dev/null and b/docs/panel-column.webp differ diff --git a/docs/panel-tab.webp b/docs/panel-tab.webp new file mode 100644 index 0000000..a04fd17 Binary files /dev/null and b/docs/panel-tab.webp differ diff --git a/preview/entry.ts b/preview/entry.ts index 67c7cb9..1929019 100644 --- a/preview/entry.ts +++ b/preview/entry.ts @@ -54,11 +54,11 @@ async function main(): Promise { mount(App, { props: { app: app as never, + chatsFolder: () => "💬 чаты", client: scene === "offline" ? null : client, component: new Component() as never, onOpenSettings: () => console.log("settings"), state, - vaultSubpath: () => "💬 чаты", }, target: document.getElementById("app") as HTMLElement, }); diff --git a/src/main.ts b/src/main.ts index 6805c8a..e893f84 100644 --- a/src/main.ts +++ b/src/main.ts @@ -230,7 +230,16 @@ export default class BeaverPlugin extends Plugin { } async loadSettings(): Promise { - this.settings = { ...DEFAULT_SETTINGS, ...(await this.loadData()) }; + const stored = ((await this.loadData()) ?? + {}) as Partial & { + vaultSubpath?: string; + }; + // The setting used to be called "vault subpath"; carry the value over. + const { vaultSubpath, ...rest } = stored; + this.settings = { ...DEFAULT_SETTINGS, ...rest }; + if (vaultSubpath && !rest.chatsFolder) { + this.settings.chatsFolder = vaultSubpath; + } } async saveSettings(): Promise { @@ -431,16 +440,16 @@ export default class BeaverPlugin extends Plugin { } private gatewayFilename(file: TFile): string | null { - const subpath = this.settings.vaultSubpath; - if (!subpath) { + const folder = this.settings.chatsFolder; + if (!folder) { return file.path; } - const prefix = `${subpath}/`; - if (file.path === subpath || file.path.startsWith(prefix)) { + const prefix = `${folder}/`; + if (file.path === folder || file.path.startsWith(prefix)) { return file.path.slice(prefix.length); } new Notice( - `Beaver: file ${file.path} is outside the configured vault subpath (${subpath})`, + `Beaver: file ${file.path} is outside the chats folder (${folder})`, 8000 ); return null; diff --git a/src/settings.ts b/src/settings.ts index 505d996..6d83e8f 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -5,18 +5,20 @@ import type BeaverPlugin from "./main"; export interface BeaverSettings { apiBaseUrl: string; baseUrl: string; + // Folder in this vault where the deep chats live: the gateway's markdown + // frontend reads and writes there, so paths it hands out are relative to it. + chatsFolder: string; token: string; - vaultSubpath: string; } export const DEFAULT_SETTINGS: BeaverSettings = { apiBaseUrl: "", baseUrl: "http://localhost:62993", + chatsFolder: "", token: "", - vaultSubpath: "", }; -export function normalizeSubpath(raw: string): string { +export function normalizeFolder(raw: string): string { return raw.trim().replace(/^\/+|\/+$/g, ""); } @@ -93,17 +95,18 @@ export class BeaverSettingsTab extends PluginSettingTab { }); new Setting(containerEl) - .setName("Vault subpath") + .setName("Chats folder") .setDesc( - "Folder in this Obsidian vault that maps to the gateway's vault root. " + - "Leave empty if the two vault roots match. Example: `💬 чаты`." + "Folder in this vault where the deep chats live - the one the gateway's " + + "markdown frontend writes to. Leave empty if the chats sit at the vault root. " + + "Example: `💬 чаты`." ) .addText((text) => text .setPlaceholder("") - .setValue(this.plugin.settings.vaultSubpath) + .setValue(this.plugin.settings.chatsFolder) .onChange(async (value) => { - this.plugin.settings.vaultSubpath = normalizeSubpath(value); + this.plugin.settings.chatsFolder = normalizeFolder(value); await this.plugin.saveSettings(); }) ); diff --git a/src/ui/app.svelte b/src/ui/app.svelte index 7735cd8..6617fa6 100644 --- a/src/ui/app.svelte +++ b/src/ui/app.svelte @@ -14,14 +14,14 @@ interface Props { app: ObsidianApp; + chatsFolder: () => string; client: ApiClient | null; component: Component; onOpenSettings: () => void; state: PanelState; - vaultSubpath: () => string; } - let { app, client, component, onOpenSettings, state, vaultSubpath }: Props = + let { app, client, component, onOpenSettings, state, chatsFolder }: Props = $props(); // Menus hang off body, not off the view: Obsidian's sidedocks paint above @@ -41,9 +41,9 @@ const host = providePanelHost({ ...obsidianHost({ app, + chatsFolder, component, sourcePath: () => state.sourcePath, - vaultSubpath, }), cache, }); diff --git a/src/ui/host.ts b/src/ui/host.ts index 0a0e11f..96b9592 100644 --- a/src/ui/host.ts +++ b/src/ui/host.ts @@ -4,11 +4,12 @@ import type { PanelHost } from "$lib/panel/host"; export interface HostOptions { app: App; + // Folder in this vault that maps to the gateway's vault root. + // Where the deep chats live in this vault; gateway paths are relative to it. + chatsFolder: () => string; component: Component; // The note the panel currently follows; link resolution starts from it. sourcePath: () => string; - // Folder in this vault that maps to the gateway's vault root. - vaultSubpath: () => string; } // The Obsidian side of the panel: markdown through the app's own renderer, @@ -47,8 +48,8 @@ export function obsidianHost(options: HostOptions): PanelHost { window.open(target); }, openNote(path) { - const subpath = options.vaultSubpath(); - const full = subpath ? `${subpath}/${path}` : path; + const folder = options.chatsFolder(); + const full = folder ? `${folder}/${path}` : path; app.workspace.openLinkText(full, "", false).catch(() => undefined); }, touch: Platform.isMobile, diff --git a/src/view.ts b/src/view.ts index 7bb6237..6e1fe99 100644 --- a/src/view.ts +++ b/src/view.ts @@ -231,11 +231,11 @@ export class PanelView extends ItemView { this.root = mount(App, { props: { app: this.app, + chatsFolder: () => this.plugin.settings.chatsFolder, client: this.plugin.apiClient(), component: this, onOpenSettings: () => this.plugin.openSettings(), state: this.state, - vaultSubpath: () => this.plugin.settings.vaultSubpath, }, target: this.contentEl, }); diff --git a/tests/smoke.mjs b/tests/smoke.mjs index 608a024..3050068 100644 --- a/tests/smoke.mjs +++ b/tests/smoke.mjs @@ -137,11 +137,11 @@ const target = dom.window.document.body; const instance = mount(App, { props: { app, + chatsFolder: () => "💬 чаты", client, component: new Component(), onOpenSettings: () => {}, state, - vaultSubpath: () => "💬 чаты", }, target, });