feat(ui,api): strip board redesign - island, rail by day, context view, server search, vault graph

This commit is contained in:
hh
2026-09-02 03:48:26 +02:00
parent 6b7de6f03d
commit 256b2a68d6
60 changed files with 5071 additions and 2347 deletions
+71 -18
View File
@@ -1,15 +1,14 @@
<script lang="ts">
import { tick } from "svelte";
import { tick, untrack } from "svelte";
import type { ApiClient } from "$lib/api/client";
import type { ContentBlock, HistoryMessage } from "$lib/api/types";
import EmptyState from "$lib/components/empty-state.svelte";
import ErrorNote from "$lib/components/error-note.svelte";
import { Skeleton } from "$lib/components/ui/skeleton";
import { Switch } from "$lib/components/ui/switch";
import { clip } from "$lib/format";
import { cn } from "$lib/utils";
import type { ActivityModel } from "./activity.svelte";
import { summarizeInput, toolLabel } from "./activity.svelte";
import { usePanelHost } from "./host";
import Markdown from "./markdown.svelte";
import QuestionCard from "./question-card.svelte";
import TurnCard from "./turn-card.svelte";
@@ -27,6 +26,8 @@
} = $props();
const RESULT_CLIP = 600;
const CACHED_MESSAGES = 80;
const host = usePanelHost();
const NEAR_BOTTOM_PX = 120;
const TICK_MS = 1000;
@@ -53,9 +54,21 @@
async function load() {
failure = null;
if (messages === null) {
const cached = host.cache?.get<HistoryMessage[]>(
`history:${conversationId}`
);
if (cached) {
messages = cached;
}
}
try {
({ messages } = await client.history(conversationId));
loadedAt = new Date().toISOString();
host.cache?.set(
`history:${conversationId}`,
messages.slice(-CACHED_MESSAGES)
);
} catch (cause) {
failure = cause instanceof Error ? cause.message : String(cause);
}
@@ -80,7 +93,7 @@
$effect(() => {
if (refreshKey >= 0) {
pinned = true;
load();
untrack(() => load());
}
});
@@ -115,6 +128,28 @@
return () => clearInterval(timer);
});
const SYSTEM_HEAD = /^\[[^\]\n]+\]/;
let openSystem = $state<Set<number>>(new Set());
function systemText(message: HistoryMessage): string | null {
if (message.role !== "user" || typeof message.content !== "string") {
return null;
}
return SYSTEM_HEAD.test(message.content.trimStart())
? message.content
: null;
}
function toggleSystem(index: number) {
const next = new Set(openSystem);
if (next.has(index)) {
next.delete(index);
} else {
next.add(index);
}
openSystem = next;
}
function blocks(message: HistoryMessage): ContentBlock[] {
return typeof message.content === "string"
? [{ text: message.content, type: "text" }]
@@ -147,20 +182,22 @@
bind:this={scroller}
>
<div class="flex flex-col gap-3" bind:this={body}>
<span
class="flex items-center gap-2 self-end text-muted-foreground text-xs"
<button
class="rule-word self-end text-xs"
data-active={showResults}
onclick={() => {
showResults = !showResults;
}}
type="button"
>
<Switch aria-label="Show tool results" bind:checked={showResults} />
show tool results
</span>
{showResults ? "hide tool results" : "show tool results"}
</button>
{#if failure}
<ErrorNote message={failure} retry={load} />
{:else if messages === null}
<div class="flex flex-col gap-2">
<Skeleton class="h-10 w-2/3" />
<Skeleton class="h-16 w-full" />
<Skeleton class="h-10 w-1/2" />
</div>
<p class="py-6 text-center text-muted-foreground text-xs">
Loading the thread…
</p>
{:else if messages.length === 0 && tail.length === 0}
<EmptyState
hint="Nothing has been said here yet. Write below, or wait for the first inject."
@@ -170,7 +207,21 @@
{#each messages as message, index (index)}
{@const parts = blocks(message)}
{@const isResultOnly = parts.every((b) => b.type === "tool_result")}
{#if !(isResultOnly && !showResults)}
{@const system = systemText(message)}
{#if system}
<button
aria-expanded={openSystem.has(index)}
class="self-center max-w-[75ch] rounded-md px-2 py-1 text-left font-mono text-muted-foreground text-xs hover:bg-accent"
onclick={() => toggleSystem(index)}
type="button"
>
{#if openSystem.has(index)}
<span class="whitespace-pre-wrap break-words">{system}</span>
{:else}
<span class="truncate">{clip(system.split("\n")[0], 120)}</span>
{/if}
</button>
{:else if !(isResultOnly && !showResults)}
<div
class={cn(
"flex flex-col gap-1.5",
@@ -181,9 +232,11 @@
{#if block.type === "text" && block.text}
<Markdown
class={cn(
"max-w-[75ch] rounded-lg px-3 py-2",
message.role === "user" ? "bg-primary/10" : "bg-muted/50"
)}
"max-w-[75ch]",
message.role === "user"
? "rounded-2xl rounded-br-md bg-primary/10 px-3.5 py-2"
: "px-1 py-1"
)}
text={block.text}
/>
{:else if block.type === "tool_use"}