feat(panel): island, rail by day, companion mode beside a deep note, cached index and threads, obsidian skin for strips
This commit is contained in:
@@ -18,3 +18,4 @@ data.json
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
preview/shots/
|
||||||
|
|||||||
+10
-6
@@ -64,11 +64,6 @@ async function main(): Promise<void> {
|
|||||||
});
|
});
|
||||||
await wait(80);
|
await wait(80);
|
||||||
|
|
||||||
if (scene.startsWith("closed")) {
|
|
||||||
press(document.querySelector('[aria-label="Switch conversation"]'));
|
|
||||||
await wait(60);
|
|
||||||
press(document.querySelector('[title="Show closed conversations"]'));
|
|
||||||
}
|
|
||||||
if (scene.startsWith("switcher")) {
|
if (scene.startsWith("switcher")) {
|
||||||
document
|
document
|
||||||
.querySelector<HTMLButtonElement>('[aria-label="Switch conversation"]')
|
.querySelector<HTMLButtonElement>('[aria-label="Switch conversation"]')
|
||||||
@@ -104,13 +99,22 @@ async function main(): Promise<void> {
|
|||||||
}
|
}
|
||||||
if (scene.startsWith("activity")) {
|
if (scene.startsWith("activity")) {
|
||||||
for (const trigger of document.querySelectorAll<HTMLButtonElement>(
|
for (const trigger of document.querySelectorAll<HTMLButtonElement>(
|
||||||
'[data-slot="tabs-trigger"]'
|
".rule-word"
|
||||||
)) {
|
)) {
|
||||||
if (trigger.textContent?.trim() === "Activity") {
|
if (trigger.textContent?.trim() === "Activity") {
|
||||||
press(trigger);
|
press(trigger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (scene.startsWith("context")) {
|
||||||
|
for (const trigger of document.querySelectorAll<HTMLButtonElement>(
|
||||||
|
".rule-word"
|
||||||
|
)) {
|
||||||
|
if (trigger.textContent?.trim() === "Context") {
|
||||||
|
press(trigger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (scene.startsWith("branch")) {
|
if (scene.startsWith("branch")) {
|
||||||
document
|
document
|
||||||
.querySelector<HTMLButtonElement>(
|
.querySelector<HTMLButtonElement>(
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ function summary(
|
|||||||
): ConversationSummary {
|
): ConversationSummary {
|
||||||
return {
|
return {
|
||||||
agent: "beaver-dispatcher",
|
agent: "beaver-dispatcher",
|
||||||
|
context_tokens: 84_000,
|
||||||
created_at: ago(9 * HOUR),
|
created_at: ago(9 * HOUR),
|
||||||
flags: {},
|
flags: {},
|
||||||
last_activity_at: ago(4 * MINUTE),
|
last_activity_at: ago(4 * MINUTE),
|
||||||
@@ -66,6 +67,7 @@ const ROWS: ConversationSummary[] = [
|
|||||||
status: "done",
|
status: "done",
|
||||||
text: "правило для 62990 добавлено, проверь снаружи",
|
text: "правило для 62990 добавлено, проверь снаружи",
|
||||||
},
|
},
|
||||||
|
parent: MASTER,
|
||||||
parent_row: 1,
|
parent_row: 1,
|
||||||
title: "ufw на dell",
|
title: "ufw на dell",
|
||||||
}),
|
}),
|
||||||
@@ -73,6 +75,7 @@ const ROWS: ConversationSummary[] = [
|
|||||||
id: BRANCH_BLASTER,
|
id: BRANCH_BLASTER,
|
||||||
kind: "branch",
|
kind: "branch",
|
||||||
last_activity_at: ago(3 * HOUR),
|
last_activity_at: ago(3 * HOUR),
|
||||||
|
parent: MASTER,
|
||||||
parent_row: 1,
|
parent_row: 1,
|
||||||
pending_question: true,
|
pending_question: true,
|
||||||
title: "бластер - что заказать",
|
title: "бластер - что заказать",
|
||||||
@@ -292,6 +295,87 @@ export class FakeClient extends ApiClient {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
pattern: /^\/api\/agents$/,
|
pattern: /^\/api\/agents$/,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
handle: () => ({ conversations: [], files: [], query: "" }),
|
||||||
|
method: "GET",
|
||||||
|
pattern: /^\/api\/search/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: () => ({ edges: [], nodes: [], root: "" }),
|
||||||
|
method: "GET",
|
||||||
|
pattern: /^\/api\/vault\/graph/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: ([, id]) => ({
|
||||||
|
agent: {
|
||||||
|
effort: "high",
|
||||||
|
model: "claude-opus-5",
|
||||||
|
name: "beaver-dispatcher",
|
||||||
|
},
|
||||||
|
context_tokens: 84_000,
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
last_at: ago(MINUTE),
|
||||||
|
other: 0,
|
||||||
|
path: "📅 дни/2026-08-29.md",
|
||||||
|
reads: 1,
|
||||||
|
writes: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
last_at: ago(5 * MINUTE),
|
||||||
|
other: 0,
|
||||||
|
path: "👤 люди/Марта Ветрова.md",
|
||||||
|
reads: 2,
|
||||||
|
writes: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
history_tokens_est: 60_000,
|
||||||
|
id,
|
||||||
|
kind: "master",
|
||||||
|
prompt: {
|
||||||
|
bytes: 24_000,
|
||||||
|
granules: [
|
||||||
|
{
|
||||||
|
bytes: 9000,
|
||||||
|
path: "мета/бобер/промпты/гранулы/голос/role.md",
|
||||||
|
tag: "role",
|
||||||
|
tokens_est: 2800,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bytes: 15_000,
|
||||||
|
path: "мета/бобер/промпты/гранулы/диспетчер.md",
|
||||||
|
tag: "dispatcher",
|
||||||
|
tokens_est: 4700,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tokens_est: 7500,
|
||||||
|
},
|
||||||
|
skills: [
|
||||||
|
{
|
||||||
|
path: "мета/бобер/скиллы/общие",
|
||||||
|
set: "общие",
|
||||||
|
skills: [
|
||||||
|
{
|
||||||
|
description: "серверы и стеки",
|
||||||
|
name: "ops",
|
||||||
|
path: "мета/бобер/скиллы/общие/ops",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
skills_tokens_est: 20,
|
||||||
|
tool_counts: { Bash: 1, Read: 3 },
|
||||||
|
tools: {
|
||||||
|
allowed: null,
|
||||||
|
disallowed: [],
|
||||||
|
gateway: ["say", "spawn"],
|
||||||
|
mcps: ["komodo"],
|
||||||
|
},
|
||||||
|
turns: 4,
|
||||||
|
}),
|
||||||
|
method: "GET",
|
||||||
|
pattern: /^\/api\/conversations\/([^/]+)\/context/,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
handle: () => ({ conversations: this.rows }),
|
handle: () => ({ conversations: this.rows }),
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
|||||||
@@ -5,4 +5,6 @@ export class PanelState {
|
|||||||
selected = $state<string | null>(null);
|
selected = $state<string | null>(null);
|
||||||
follow = $state(true);
|
follow = $state(true);
|
||||||
sourcePath = $state("");
|
sourcePath = $state("");
|
||||||
|
// The selected conversation is the open note's own: show activity, not a copy.
|
||||||
|
companion = $state(false);
|
||||||
}
|
}
|
||||||
|
|||||||
+84
-1
@@ -47,10 +47,21 @@
|
|||||||
|
|
||||||
--sidebar: var(--background-secondary);
|
--sidebar: var(--background-secondary);
|
||||||
--sidebar-accent: var(--background-modifier-hover);
|
--sidebar-accent: var(--background-modifier-hover);
|
||||||
|
--rack: var(--background-secondary);
|
||||||
|
--strip: var(--background-primary);
|
||||||
|
--attention: oklch(0.72 0.16 72);
|
||||||
|
--attention-foreground: oklch(0.5 0.14 70);
|
||||||
|
--shadow-lift:
|
||||||
|
0 6px 16px -10px rgb(0 0 0 / 0.35), 0 1px 2px rgb(0 0 0 / 0.08);
|
||||||
|
--shadow-float:
|
||||||
|
0 20px 50px -20px rgb(0 0 0 / 0.5), 0 2px 6px rgb(0 0 0 / 0.1);
|
||||||
--radius: 0.45rem;
|
--radius: 0.45rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-dark .beaver-root {
|
.theme-dark .beaver-root {
|
||||||
|
--attention: oklch(0.8 0.15 75);
|
||||||
|
--attention-foreground: oklch(0.85 0.14 78);
|
||||||
|
--strip: var(--background-primary-alt);
|
||||||
--note: oklch(0.84 0.14 88);
|
--note: oklch(0.84 0.14 88);
|
||||||
--warn: oklch(0.8 0.13 75);
|
--warn: oklch(0.8 0.13 75);
|
||||||
--status-done: oklch(0.74 0.14 155);
|
--status-done: oklch(0.74 0.14 155);
|
||||||
@@ -75,6 +86,12 @@
|
|||||||
--color-note: var(--note);
|
--color-note: var(--note);
|
||||||
--color-sidebar: var(--sidebar);
|
--color-sidebar: var(--sidebar);
|
||||||
--color-sidebar-accent: var(--sidebar-accent);
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-rack: var(--rack);
|
||||||
|
--color-strip: var(--strip);
|
||||||
|
--color-attention: var(--attention);
|
||||||
|
--color-attention-foreground: var(--attention-foreground);
|
||||||
|
--shadow-lift: var(--shadow-lift);
|
||||||
|
--shadow-float: var(--shadow-float);
|
||||||
|
|
||||||
--color-status-new: var(--status-new);
|
--color-status-new: var(--status-new);
|
||||||
--color-status-done: var(--status-done);
|
--color-status-done: var(--status-done);
|
||||||
@@ -178,7 +195,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.beaver-root.beaver-root button {
|
.beaver-root.beaver-root button {
|
||||||
text-align: inherit;
|
justify-content: flex-start;
|
||||||
|
width: auto;
|
||||||
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,3 +416,67 @@
|
|||||||
font-weight: var(--font-medium, 500) !important;
|
font-weight: var(--font-medium, 500) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Strips and the island are buttons here; the reset above flattens buttons,
|
||||||
|
so the vocabulary is restated at the reset's own specificity. */
|
||||||
|
.beaver-root.beaver-root .strip {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.5rem minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: stretch;
|
||||||
|
justify-items: stretch;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.375rem 0.875rem 0.375rem 0.625rem;
|
||||||
|
text-align: left;
|
||||||
|
background: var(--strip);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root .strip:hover,
|
||||||
|
.beaver-root.beaver-root .strip:focus-visible {
|
||||||
|
background: var(--strip);
|
||||||
|
box-shadow: var(--shadow-lift);
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root .strip:first-child {
|
||||||
|
border-top-left-radius: var(--radius-md);
|
||||||
|
border-top-right-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root .strip:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
border-bottom-right-radius: var(--radius-md);
|
||||||
|
border-bottom-left-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root .strip[aria-current="true"] {
|
||||||
|
background: color-mix(in oklab, var(--strip) 85%, var(--primary) 15%);
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root .strip[data-state="waiting"] {
|
||||||
|
box-shadow: var(--shadow-lift);
|
||||||
|
transform: translateX(0.5rem);
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root .strip.strip-child {
|
||||||
|
padding-left: 1.75rem;
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root button.island {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 0.625rem;
|
||||||
|
align-items: center;
|
||||||
|
height: 2.125rem;
|
||||||
|
padding: 0 0.875rem 0 0.75rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: var(--strip);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root button.island:hover {
|
||||||
|
background: var(--strip);
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root button.island:disabled {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root .bay-rack {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: calc(var(--radius-md) + 1px);
|
||||||
|
}
|
||||||
|
.beaver-root.beaver-root .strip-child::before {
|
||||||
|
left: 1.125rem;
|
||||||
|
}
|
||||||
|
|||||||
+10
-5
@@ -5,6 +5,7 @@
|
|||||||
import type { ApiClient } from "$lib/api/client";
|
import type { ApiClient } from "$lib/api/client";
|
||||||
import EmptyState from "$lib/components/empty-state.svelte";
|
import EmptyState from "$lib/components/empty-state.svelte";
|
||||||
import { Button } from "$lib/components/ui/button";
|
import { Button } from "$lib/components/ui/button";
|
||||||
|
import { panelCache } from "$lib/panel/cache";
|
||||||
import { providePanelHost } from "$lib/panel/host";
|
import { providePanelHost } from "$lib/panel/host";
|
||||||
import { ConversationIndex } from "$lib/panel/index.svelte";
|
import { ConversationIndex } from "$lib/panel/index.svelte";
|
||||||
import PanelShell from "$lib/panel/panel-shell.svelte";
|
import PanelShell from "$lib/panel/panel-shell.svelte";
|
||||||
@@ -36,17 +37,19 @@
|
|||||||
|
|
||||||
// Props are read once: the view remounts the tree when settings change.
|
// Props are read once: the view remounts the tree when settings change.
|
||||||
// svelte-ignore state_referenced_locally
|
// svelte-ignore state_referenced_locally
|
||||||
providePanelHost(
|
const cache = panelCache(`beaver.panel:${client?.base ?? "none"}`);
|
||||||
obsidianHost({
|
const host = providePanelHost({
|
||||||
|
...obsidianHost({
|
||||||
app,
|
app,
|
||||||
component,
|
component,
|
||||||
sourcePath: () => state.sourcePath,
|
sourcePath: () => state.sourcePath,
|
||||||
vaultSubpath,
|
vaultSubpath,
|
||||||
})
|
}),
|
||||||
);
|
cache,
|
||||||
|
});
|
||||||
|
|
||||||
// svelte-ignore state_referenced_locally
|
// svelte-ignore state_referenced_locally
|
||||||
const index = client ? new ConversationIndex(() => client) : null;
|
const index = client ? new ConversationIndex(() => client, cache) : null;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<BitsConfig defaultPortalTo={layer}>
|
<BitsConfig defaultPortalTo={layer}>
|
||||||
@@ -54,7 +57,9 @@
|
|||||||
{#if client && index}
|
{#if client && index}
|
||||||
<PanelShell
|
<PanelShell
|
||||||
{client}
|
{client}
|
||||||
|
companion={state.companion}
|
||||||
{index}
|
{index}
|
||||||
|
onOpenFile={(path) => host.openNote?.(`мета/бобер/${path}`)}
|
||||||
showFollow
|
showFollow
|
||||||
bind:follow={state.follow}
|
bind:follow={state.follow}
|
||||||
bind:selected={state.selected}
|
bind:selected={state.selected}
|
||||||
|
|||||||
@@ -119,11 +119,17 @@ export class PanelView extends ItemView {
|
|||||||
const id = this.plugin.conversationIdOf(file);
|
const id = this.plugin.conversationIdOf(file);
|
||||||
if (id) {
|
if (id) {
|
||||||
this.state.selected = id;
|
this.state.selected = id;
|
||||||
|
this.state.companion = !this.inTabs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
show(id: string): void {
|
show(id: string): void {
|
||||||
this.state.selected = id;
|
this.state.selected = id;
|
||||||
|
this.state.companion = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private inTabs(): boolean {
|
||||||
|
return this.leaf.getRoot() === this.app.workspace.rootSplit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Settings changed: the client is built from them, so rebuild the tree.
|
// Settings changed: the client is built from them, so rebuild the tree.
|
||||||
|
|||||||
+2
-4
@@ -226,10 +226,8 @@ flushSync();
|
|||||||
await settle();
|
await settle();
|
||||||
const rows = [...dom.window.document.querySelectorAll("[data-row]")];
|
const rows = [...dom.window.document.querySelectorAll("[data-row]")];
|
||||||
check(
|
check(
|
||||||
"switcher lists open conversations grouped",
|
"switcher lists open conversations grouped by day",
|
||||||
rows.length >= 4 &&
|
rows.length >= 4 && text().includes("Today"),
|
||||||
text().includes("Branches") &&
|
|
||||||
text().includes("Deep chats"),
|
|
||||||
`${rows.length}`
|
`${rows.length}`
|
||||||
);
|
);
|
||||||
check(
|
check(
|
||||||
|
|||||||
Reference in New Issue
Block a user