feat(settings,docs): vault subpath becomes the chats folder, panel screenshots in the readme
This commit is contained in:
@@ -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.
|
- **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.
|
- **Send the current note** to Beaver's markdown frontend (`POST /chat/stream`) and stream the agent's reply back into the buffer.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
- **Beaver: Open panel in a tab** - the wide layout with the conversation rail on the left (also the ribbon icon).
|
- **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.
|
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.
|
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`.
|
- **Base URL** - markdown frontend root, e.g. `https://host/md`.
|
||||||
- **API origin** - where `/api/…` lives; empty derives it from the base URL (`…/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).
|
- **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.
|
Changing settings remounts open panels.
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
+1
-1
@@ -54,11 +54,11 @@ async function main(): Promise<void> {
|
|||||||
mount(App, {
|
mount(App, {
|
||||||
props: {
|
props: {
|
||||||
app: app as never,
|
app: app as never,
|
||||||
|
chatsFolder: () => "💬 чаты",
|
||||||
client: scene === "offline" ? null : client,
|
client: scene === "offline" ? null : client,
|
||||||
component: new Component() as never,
|
component: new Component() as never,
|
||||||
onOpenSettings: () => console.log("settings"),
|
onOpenSettings: () => console.log("settings"),
|
||||||
state,
|
state,
|
||||||
vaultSubpath: () => "💬 чаты",
|
|
||||||
},
|
},
|
||||||
target: document.getElementById("app") as HTMLElement,
|
target: document.getElementById("app") as HTMLElement,
|
||||||
});
|
});
|
||||||
|
|||||||
+15
-6
@@ -230,7 +230,16 @@ export default class BeaverPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadSettings(): Promise<void> {
|
async loadSettings(): Promise<void> {
|
||||||
this.settings = { ...DEFAULT_SETTINGS, ...(await this.loadData()) };
|
const stored = ((await this.loadData()) ??
|
||||||
|
{}) as Partial<BeaverSettings> & {
|
||||||
|
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<void> {
|
async saveSettings(): Promise<void> {
|
||||||
@@ -431,16 +440,16 @@ export default class BeaverPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private gatewayFilename(file: TFile): string | null {
|
private gatewayFilename(file: TFile): string | null {
|
||||||
const subpath = this.settings.vaultSubpath;
|
const folder = this.settings.chatsFolder;
|
||||||
if (!subpath) {
|
if (!folder) {
|
||||||
return file.path;
|
return file.path;
|
||||||
}
|
}
|
||||||
const prefix = `${subpath}/`;
|
const prefix = `${folder}/`;
|
||||||
if (file.path === subpath || file.path.startsWith(prefix)) {
|
if (file.path === folder || file.path.startsWith(prefix)) {
|
||||||
return file.path.slice(prefix.length);
|
return file.path.slice(prefix.length);
|
||||||
}
|
}
|
||||||
new Notice(
|
new Notice(
|
||||||
`Beaver: file ${file.path} is outside the configured vault subpath (${subpath})`,
|
`Beaver: file ${file.path} is outside the chats folder (${folder})`,
|
||||||
8000
|
8000
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
+11
-8
@@ -5,18 +5,20 @@ import type BeaverPlugin from "./main";
|
|||||||
export interface BeaverSettings {
|
export interface BeaverSettings {
|
||||||
apiBaseUrl: string;
|
apiBaseUrl: string;
|
||||||
baseUrl: 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;
|
token: string;
|
||||||
vaultSubpath: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: BeaverSettings = {
|
export const DEFAULT_SETTINGS: BeaverSettings = {
|
||||||
apiBaseUrl: "",
|
apiBaseUrl: "",
|
||||||
baseUrl: "http://localhost:62993",
|
baseUrl: "http://localhost:62993",
|
||||||
|
chatsFolder: "",
|
||||||
token: "",
|
token: "",
|
||||||
vaultSubpath: "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function normalizeSubpath(raw: string): string {
|
export function normalizeFolder(raw: string): string {
|
||||||
return raw.trim().replace(/^\/+|\/+$/g, "");
|
return raw.trim().replace(/^\/+|\/+$/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,17 +95,18 @@ export class BeaverSettingsTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName("Vault subpath")
|
.setName("Chats folder")
|
||||||
.setDesc(
|
.setDesc(
|
||||||
"Folder in this Obsidian vault that maps to the gateway's vault root. " +
|
"Folder in this vault where the deep chats live - the one the gateway's " +
|
||||||
"Leave empty if the two vault roots match. Example: `💬 чаты`."
|
"markdown frontend writes to. Leave empty if the chats sit at the vault root. " +
|
||||||
|
"Example: `💬 чаты`."
|
||||||
)
|
)
|
||||||
.addText((text) =>
|
.addText((text) =>
|
||||||
text
|
text
|
||||||
.setPlaceholder("")
|
.setPlaceholder("")
|
||||||
.setValue(this.plugin.settings.vaultSubpath)
|
.setValue(this.plugin.settings.chatsFolder)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.vaultSubpath = normalizeSubpath(value);
|
this.plugin.settings.chatsFolder = normalizeFolder(value);
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
+3
-3
@@ -14,14 +14,14 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
app: ObsidianApp;
|
app: ObsidianApp;
|
||||||
|
chatsFolder: () => string;
|
||||||
client: ApiClient | null;
|
client: ApiClient | null;
|
||||||
component: Component;
|
component: Component;
|
||||||
onOpenSettings: () => void;
|
onOpenSettings: () => void;
|
||||||
state: PanelState;
|
state: PanelState;
|
||||||
vaultSubpath: () => string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let { app, client, component, onOpenSettings, state, vaultSubpath }: Props =
|
let { app, client, component, onOpenSettings, state, chatsFolder }: Props =
|
||||||
$props();
|
$props();
|
||||||
|
|
||||||
// Menus hang off body, not off the view: Obsidian's sidedocks paint above
|
// Menus hang off body, not off the view: Obsidian's sidedocks paint above
|
||||||
@@ -41,9 +41,9 @@
|
|||||||
const host = providePanelHost({
|
const host = providePanelHost({
|
||||||
...obsidianHost({
|
...obsidianHost({
|
||||||
app,
|
app,
|
||||||
|
chatsFolder,
|
||||||
component,
|
component,
|
||||||
sourcePath: () => state.sourcePath,
|
sourcePath: () => state.sourcePath,
|
||||||
vaultSubpath,
|
|
||||||
}),
|
}),
|
||||||
cache,
|
cache,
|
||||||
});
|
});
|
||||||
|
|||||||
+5
-4
@@ -4,11 +4,12 @@ import type { PanelHost } from "$lib/panel/host";
|
|||||||
|
|
||||||
export interface HostOptions {
|
export interface HostOptions {
|
||||||
app: App;
|
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;
|
component: Component;
|
||||||
// The note the panel currently follows; link resolution starts from it.
|
// The note the panel currently follows; link resolution starts from it.
|
||||||
sourcePath: () => string;
|
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,
|
// 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);
|
window.open(target);
|
||||||
},
|
},
|
||||||
openNote(path) {
|
openNote(path) {
|
||||||
const subpath = options.vaultSubpath();
|
const folder = options.chatsFolder();
|
||||||
const full = subpath ? `${subpath}/${path}` : path;
|
const full = folder ? `${folder}/${path}` : path;
|
||||||
app.workspace.openLinkText(full, "", false).catch(() => undefined);
|
app.workspace.openLinkText(full, "", false).catch(() => undefined);
|
||||||
},
|
},
|
||||||
touch: Platform.isMobile,
|
touch: Platform.isMobile,
|
||||||
|
|||||||
+1
-1
@@ -231,11 +231,11 @@ export class PanelView extends ItemView {
|
|||||||
this.root = mount(App, {
|
this.root = mount(App, {
|
||||||
props: {
|
props: {
|
||||||
app: this.app,
|
app: this.app,
|
||||||
|
chatsFolder: () => this.plugin.settings.chatsFolder,
|
||||||
client: this.plugin.apiClient(),
|
client: this.plugin.apiClient(),
|
||||||
component: this,
|
component: this,
|
||||||
onOpenSettings: () => this.plugin.openSettings(),
|
onOpenSettings: () => this.plugin.openSettings(),
|
||||||
state: this.state,
|
state: this.state,
|
||||||
vaultSubpath: () => this.plugin.settings.vaultSubpath,
|
|
||||||
},
|
},
|
||||||
target: this.contentEl,
|
target: this.contentEl,
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -137,11 +137,11 @@ const target = dom.window.document.body;
|
|||||||
const instance = mount(App, {
|
const instance = mount(App, {
|
||||||
props: {
|
props: {
|
||||||
app,
|
app,
|
||||||
|
chatsFolder: () => "💬 чаты",
|
||||||
client,
|
client,
|
||||||
component: new Component(),
|
component: new Component(),
|
||||||
onOpenSettings: () => {},
|
onOpenSettings: () => {},
|
||||||
state,
|
state,
|
||||||
vaultSubpath: () => "💬 чаты",
|
|
||||||
},
|
},
|
||||||
target,
|
target,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user