feat(ui,docs): the open tab lives in the conversation url, admin screenshots in the readme

This commit is contained in:
hh
2026-09-05 04:55:54 +02:00
parent 421248b0ad
commit 60782d6276
6 changed files with 35 additions and 2 deletions
+9 -1
View File
@@ -7,6 +7,8 @@ boundary - without an opinion about what the agent is for. A setup is one
holds the primitives, [beaver-agent](https://git.kotikot.com/beaver/beaver-agent) holds the primitives, [beaver-agent](https://git.kotikot.com/beaver/beaver-agent)
is a full setup built on them. is a full setup built on them.
![The admin: what is in motion, what waits for an answer, the context and the quotas, the graph around the master](preview/shots/admin-now.webp)
## What it gives a setup ## What it gives a setup
- **agents/** - what an agent is: a Claude agent with a prompt per - **agents/** - what an agent is: a Claude agent with a prompt per
@@ -32,7 +34,13 @@ is a full setup built on them.
- **storage/** - SQLModel tables and the Postgres session store. - **storage/** - SQLModel tables and the Postgres session store.
`config.py` defines `Gateway` and loads a setup; `app.py` builds the runtime `config.py` defines `Gateway` and loads a setup; `app.py` builds the runtime
from it; `cli.py` is the entrypoint. Every string the gateway puts in front of from it; `cli.py` is the entrypoint. The admin SPA in `ui/` is the browser
window on all of it: conversations by day, the thread with the tree of tool
calls, the context of the last turn, memory, jobs, usage, tokens and the audit.
![A conversation in the admin: the thread, the queue, the details](preview/shots/admin-thread.webp)
![The Context tab: what the model held on the last turn - harness, prompt granules, skills, the notes the thread reached](preview/shots/admin-context.webp) Every string the gateway puts in front of
a model or a user has an English default and is overridable a model or a user has an English default and is overridable
(`ConversationTexts`, `TelegramTexts`). (`ConversationTexts`, `TelegramTexts`).
Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

@@ -18,6 +18,7 @@
onOpen, onOpen,
showTitle = true, showTitle = true,
initialView = "chat", initialView = "chat",
onViewChange,
}: { }: {
client: ApiClient; client: ApiClient;
id: string; id: string;
@@ -27,6 +28,8 @@
showTitle?: boolean; showTitle?: boolean;
// "activity" beside a deep chat's own note: the file is the thread. // "activity" beside a deep chat's own note: the file is the thread.
initialView?: string; initialView?: string;
// The page keeps the open tab in the URL so it survives a reload.
onViewChange?: (view: string) => void;
} = $props(); } = $props();
const host = usePanelHost(); const host = usePanelHost();
@@ -50,6 +53,10 @@
historyKey += 1; historyKey += 1;
} }
$effect(() => {
onViewChange?.(view);
});
$effect(() => { $effect(() => {
const { running } = feed.model; const { running } = feed.model;
if (!running) { if (!running) {
+19 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; import { goto, replaceState } from "$app/navigation";
import { base } from "$app/paths"; import { base } from "$app/paths";
import { page } from "$app/state"; import { page } from "$app/state";
import ConversationView from "$lib/panel/conversation-view.svelte"; import ConversationView from "$lib/panel/conversation-view.svelte";
@@ -7,6 +7,22 @@
const id = $derived(page.params.id ?? ""); const id = $derived(page.params.id ?? "");
const href = (target: string) => `${base}/conversations/${target}`; const href = (target: string) => `${base}/conversations/${target}`;
// ``?view=context`` opens that tab and stays in the address bar, so a
// reload (or a restart) lands where the reader was. Thread is the default
// and keeps the URL clean.
const initialView = $derived(page.url.searchParams.get("view") ?? "chat");
function remember(view: string) {
const url = new URL(page.url);
if (view === "chat") {
url.searchParams.delete("view");
} else {
url.searchParams.set("view", view);
}
if (url.href !== page.url.href) {
replaceState(url, page.state);
}
}
</script> </script>
<svelte:head><title>Conversation · Beaver</title></svelte:head> <svelte:head><title>Conversation · Beaver</title></svelte:head>
@@ -17,7 +33,9 @@
client={session.client} client={session.client}
{href} {href}
{id} {id}
{initialView}
onOpen={(target) => goto(href(target))} onOpen={(target) => goto(href(target))}
onViewChange={remember}
/> />
{/key} {/key}
{/if} {/if}