feat(accumulator): vite+React lab UI at crates/buzz-accumulator/ui

The ergonomics lab for the exploration client: a standalone SPA that
talks to the daemon through vite's dev proxy (/api → 127.0.0.1:4640),
so the daemon stays loopback-pure and zero-config.

One screen, three panes:
- Selection composer (channels/authors/kinds + window presets), with
  URL-shareable state (?channels=…&since=… and ?fold=…&v=…).
- Explore: live zero-spend preview (count/chars/est tokens), the
  daily rhythm strip, and a paged browser of the matching events.
- Folds board: create-from-current-selection, and per-fold cards with
  the priced loop — preflight ($0) → run pinned to the exact priced
  window → outcome (refusals salvage the model output visibly).
- Artifact reader: Working Context / Log rendering with [event:id]
  citation chips that pin the source message into an evidence rail,
  plus a version scrubber.

Deliberately its own pnpm workspace root: not in the repo workspace,
invisible to just ci. Run: cd crates/buzz-accumulator/ui && pnpm
install && pnpm dev (daemon running on :4640).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Riley Crane <rileycrane@squareup.com>
This commit is contained in:
Riley Crane
2026-08-31 22:26:30 -07:00
committed by Sol
parent 6baffde9df
commit c9c4fd941e
15 changed files with 2926 additions and 0 deletions

2
crates/buzz-accumulator/ui/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
dist/

View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>accumulator</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,24 @@
{
"name": "accumulator-ui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"check": "tsc",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/react-query": "^5.90.5",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^5.0.0",
"typescript": "~5.8.3",
"vite": "^7.1.0"
}
}

1163
crates/buzz-accumulator/ui/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
# Deliberately its own workspace root: the lab is standalone tooling and must
# not join the repo-level pnpm workspace (or `just ci`'s desktop/web checks).
packages:
- "."
allowBuilds:
esbuild: true

View File

@@ -0,0 +1,119 @@
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import { api, type Selection, type TimeWindow } from "./api";
import { ArtifactReader } from "./components/ArtifactReader";
import { Explore } from "./components/Explore";
import { FoldsBoard } from "./components/FoldsBoard";
import { SelectionComposer } from "./components/SelectionComposer";
export type View =
| { kind: "explore" }
| { kind: "artifact"; fold: string; version: number };
/** Initial selection/window from URL query params, so a selection is
* shareable: `?channels=<uuid,uuid>&authors=<hex>&kinds=9&since=<ts>`. */
function fromUrl(): { selection: Selection; window: TimeWindow } {
const q = new URLSearchParams(location.search);
const list = (key: string) =>
(q.get(key) ?? "").split(",").map((s) => s.trim()).filter(Boolean);
const num = (key: string) => {
const n = Number.parseInt(q.get(key) ?? "", 10);
return Number.isFinite(n) ? n : undefined;
};
return {
selection: {
channels: list("channels"),
authors: list("authors"),
kinds: list("kinds")
.map((k) => Number.parseInt(k, 10))
.filter((k) => Number.isFinite(k) && k >= 0),
},
window: { since: num("since"), until_exclusive: num("until") },
};
}
/** Initial view from `?fold=<name>&v=<version>`, so artifacts are shareable. */
function viewFromUrl(): View {
const q = new URLSearchParams(location.search);
const fold = q.get("fold");
const v = Number.parseInt(q.get("v") ?? "1", 10);
return fold && Number.isFinite(v) && v >= 1
? { kind: "artifact", fold, version: v }
: { kind: "explore" };
}
export function App() {
const initial = fromUrl();
const [selection, setSelection] = useState<Selection>(initial.selection);
const [window, setWindow] = useState<TimeWindow>(initial.window);
const [view, setView] = useState<View>(viewFromUrl);
const status = useQuery({
queryKey: ["status"],
queryFn: api.status,
refetchInterval: 5000,
});
const s = status.data;
const dotClass = status.isError
? "error"
: s?.connection === "connected"
? "connected"
: "";
return (
<>
<header className="app-header">
<h1>🐝 accumulator</h1>
<span className="status-pill">
<span className={`status-dot ${dotClass}`} />
{status.isError
? "daemon unreachable — is it running on :4640?"
: s
? `${s.connection} · ${s.total_events.toLocaleString()} events · ${
s.folds
} folds · ${s.artifacts} artifacts${
s.backfill_complete ? "" : " · backfilling…"
}`
: "connecting…"}
</span>
{view.kind === "artifact" && (
<button onClick={() => setView({ kind: "explore" })}>
← back to explore
</button>
)}
</header>
<div className="columns">
<aside className="pane left">
<SelectionComposer
selection={selection}
onChange={setSelection}
window={window}
onWindowChange={setWindow}
/>
</aside>
<main className="pane">
{view.kind === "explore" ? (
<Explore selection={selection} window={window} />
) : (
<ArtifactReader
fold={view.fold}
version={view.version}
onVersion={(v) =>
setView({ kind: "artifact", fold: view.fold, version: v })
}
/>
)}
</main>
<aside className="pane right">
<FoldsBoard
selection={selection}
onOpenArtifact={(fold, version) =>
setView({ kind: "artifact", fold, version })
}
/>
</aside>
</div>
</>
);
}

View File

@@ -0,0 +1,245 @@
// Typed client for the daemon's loopback HTTP API (proxied at /api by vite).
export type Selection = {
channels: string[];
authors: string[];
kinds: number[];
};
/** Half-open run window; omitted bounds mean "everything up to now". */
export type TimeWindow = { since?: number; until_exclusive?: number };
export type Status = {
relay: string;
pubkey: string;
started_at: number;
connection: string;
connected_at: number | null;
reconnect_attempts: number;
last_error: string | null;
last_event_at: number | null;
backfill_complete: boolean;
channels: Record<string, unknown>;
total_events: number;
folds: number;
artifacts: number;
db_path: string;
};
export type Channel = {
id: string;
name: string | null;
channel_type: string;
backfill_cursor: number | null;
backfill_done: boolean;
active: boolean;
discovered_at: number;
};
export type Bucket = { day: number; count: number };
export type Preview = {
selection: Selection;
window: { since: number; until_exclusive: number };
count: number;
total_chars: number;
oldest_ts: number | null;
newest_ts: number | null;
buckets: Bucket[];
};
export type EventItem = {
id: string;
channel: string | null;
pubkey: string;
author_name: string | null;
kind: number;
created_at: number;
content: string;
};
export type PageCursor = { created_at: number; id: string };
export type EventsPage = {
window: { since: number; until_exclusive: number };
count: number;
next: PageCursor | null;
events: EventItem[];
};
export type FoldSpec = {
name: string;
selection: Selection;
schema: string;
model: string;
instructions: string;
meta?: unknown;
};
export type FoldRow = {
name: string;
spec: FoldSpec;
versions: number;
latest_version: number | null;
created_at: number;
updated_at: number;
};
export type WindowFit = {
model_window: number | null;
est_input_tokens: number;
fits: boolean | null;
headroom_tokens: number | null;
};
export type Estimate = { est_input_tokens: number; window_fit: WindowFit };
export type Preflight =
| { plan: "cached" }
| { plan: "stalled"; reason: string; pending: number }
| {
plan: "ready";
shown: number;
pending: number;
truncated: boolean;
estimate: Estimate;
window: [number, number];
};
export type Artifact = {
fold: string;
version: number;
output: string;
shown_ids: string[];
coverage_since: number | null;
coverage_until: number | null;
selection: Selection;
channels: string[];
model: string;
schema: string;
prompt_sha256: string;
truncated: boolean;
created_at: number;
};
export type ArtifactSummary = {
version: number;
created_at: number;
shown: number;
coverage_since: number | null;
coverage_until: number | null;
channels: string[];
model: string;
schema: string;
truncated: boolean;
};
export type RunOutcome =
| { status: "cached" }
| { status: "stalled"; reason: string; pending: number }
| { status: "refused"; reason: string; model_output: string }
| { status: "unpublished"; reason: string; model_output: string }
| {
status: "folded";
artifact: Artifact;
shown: number;
pending: number;
truncated: boolean;
};
const BASE = "/api";
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(BASE + path, init);
if (!res.ok) {
let message = `${res.status} ${res.statusText}`;
try {
const body = (await res.json()) as { error?: string };
if (body.error) message = body.error;
} catch {
// non-JSON error body; keep the status line
}
throw new Error(message);
}
return (await res.json()) as T;
}
function json(body: unknown): Pick<RequestInit, "headers" | "body"> {
return {
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
};
}
function post(body: unknown): RequestInit {
return { method: "POST", ...json(body) };
}
export const api = {
status: () => request<Status>("/status"),
channels: () => request<{ channels: Channel[] }>("/channels"),
event: (id: string) => request<EventItem>(`/events/${id}`),
preview: (selection: Selection, window: TimeWindow) =>
request<Preview>("/select/preview", post({ selection, ...window })),
events: (
selection: Selection,
window: TimeWindow,
limit: number,
after: PageCursor | null,
) =>
request<EventsPage>(
"/select/events",
post({ selection, ...window, limit, after }),
),
folds: () => request<{ folds: FoldRow[] }>("/folds"),
putFold: (
name: string,
body: {
selection: Selection;
model: string;
instructions?: string;
meta?: unknown;
},
) => request<{ saved: FoldSpec }>(`/folds/${name}`, { method: "PUT", ...json(body) }),
deleteFold: (name: string) =>
request<{ deleted: string }>(`/folds/${name}`, { method: "DELETE" }),
preflight: (name: string, window: TimeWindow) =>
request<Preflight>(`/folds/${name}/preflight`, post(window)),
run: (name: string, window: TimeWindow) =>
request<RunOutcome>(`/folds/${name}/run`, post(window)),
artifacts: (name: string) =>
request<{ fold: string; artifacts: ArtifactSummary[] }>(
`/folds/${name}/artifacts`,
),
artifact: (name: string, version: number) =>
request<Artifact>(`/folds/${name}/artifacts/${version}`),
};
export function selectionIsRunnable(s: Selection): boolean {
return s.channels.length > 0 || s.authors.length > 0;
}
export function shortId(id: string): string {
return id.slice(0, 8);
}
export function fmtTime(ts: number): string {
return new Date(ts * 1000).toLocaleString(undefined, {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
export function fmtDay(ts: number): string {
return new Date(ts * 1000).toLocaleDateString(undefined, {
timeZone: "UTC",
month: "short",
day: "numeric",
});
}
export function fmtTokens(n: number): string {
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`;
}

View File

@@ -0,0 +1,182 @@
import { useQuery } from "@tanstack/react-query";
import { useState, type ReactNode } from "react";
import { api, fmtTime, shortId } from "../api";
type Props = {
fold: string;
version: number;
onVersion: (v: number) => void;
};
/** Renders one artifact version: Working Context + Log with citation chips,
* a version scrubber, and a pinned-evidence rail. */
export function ArtifactReader({ fold, version, onVersion }: Props) {
const [pinned, setPinned] = useState<string[]>([]);
const chain = useQuery({
queryKey: ["artifacts", fold],
queryFn: () => api.artifacts(fold),
});
const artifact = useQuery({
queryKey: ["artifact", fold, version],
queryFn: () => api.artifact(fold, version),
});
const pin = (id: string) =>
setPinned((p) => (p.includes(id) ? p : [id, ...p]));
const a = artifact.data;
return (
<div>
<div className="toolbar">
<strong className="mono">{fold}</strong>
<div className="version-strip">
{chain.data?.artifacts.map((s) => (
<button
key={s.version}
className={s.version === version ? "active" : ""}
onClick={() => onVersion(s.version)}
title={`${fmtTime(s.created_at)} · ${s.shown} shown${
s.truncated ? " · chunked" : ""
}`}
>
v{s.version}
</button>
))}
</div>
</div>
{a && (
<div className="muted" style={{ fontSize: 12, marginBottom: 10 }}>
{fmtTime(a.created_at)} · model {a.model} · {a.shown_ids.length} shown
{a.truncated ? " · chunked window" : ""}
{a.coverage_since !== null && a.coverage_until !== null && (
<>
{" "}
· covers {fmtTime(a.coverage_since)} → {fmtTime(a.coverage_until)}
</>
)}
</div>
)}
{artifact.isError && (
<div className="error-box">{(artifact.error as Error).message}</div>
)}
<div className="artifact">
<div className="artifact-body">
{a ? renderOutput(a.output, pin) : <span className="faint">loading…</span>}
</div>
<div className="evidence">
<h2>Evidence</h2>
{pinned.length === 0 && (
<div className="faint" style={{ fontSize: 12 }}>
Click a citation chip to pin the source message here — every
claim is one click from its evidence.
</div>
)}
{pinned.map((id) => (
<EvidenceCard
key={id}
id={id}
onClose={() => setPinned((p) => p.filter((x) => x !== id))}
/>
))}
</div>
</div>
</div>
);
}
function EvidenceCard({ id, onClose }: { id: string; onClose: () => void }) {
const ev = useQuery({
queryKey: ["event", id],
queryFn: () => api.event(id),
retry: false,
});
return (
<div className="evidence-card">
<div className="head">
<span>
{ev.data ? (
<>
<strong>{ev.data.author_name ?? shortId(ev.data.pubkey)}</strong>{" "}
· {fmtTime(ev.data.created_at)}
</>
) : (
<span className="mono">{shortId(id)}…</span>
)}
</span>
<button onClick={onClose} style={{ padding: "0 6px" }}>
×
</button>
</div>
<div className="body">
{ev.isError
? `not in the mirror: ${(ev.error as Error).message}`
: (ev.data?.content ?? "loading…")}
</div>
</div>
);
}
const CITATION = /\[event:([0-9a-f]{64})\]/g;
/** Minimal renderer for the artifact's markdown-shaped output: headings,
* list items, paragraphs — with `[event:<id>]` turned into citation chips. */
function renderOutput(output: string, pin: (id: string) => void): ReactNode {
const blocks: ReactNode[] = [];
let list: ReactNode[] = [];
let key = 0;
const flushList = () => {
if (list.length > 0) {
blocks.push(<ul key={key++}>{list}</ul>);
list = [];
}
};
for (const line of output.split("\n")) {
const trimmed = line.trim();
if (trimmed.startsWith("- ") || trimmed.startsWith("* ")) {
list.push(<li key={key++}>{inline(trimmed.slice(2), pin)}</li>);
continue;
}
flushList();
if (trimmed.startsWith("## ")) {
blocks.push(<h4 key={key++}>{inline(trimmed.slice(3), pin)}</h4>);
} else if (trimmed.startsWith("# ")) {
blocks.push(<h3 key={key++}>{inline(trimmed.slice(2), pin)}</h3>);
} else if (trimmed.length > 0) {
blocks.push(<p key={key++}>{inline(trimmed, pin)}</p>);
}
}
flushList();
return blocks;
}
function inline(text: string, pin: (id: string) => void): ReactNode[] {
const out: ReactNode[] = [];
let last = 0;
let key = 0;
CITATION.lastIndex = 0;
let m = CITATION.exec(text);
while (m !== null) {
if (m.index > last) out.push(text.slice(last, m.index));
const id = m[1];
if (id) {
out.push(
<button
key={key++}
className="chip"
title={id}
onClick={() => pin(id)}
>
{shortId(id)}
</button>,
);
}
last = m.index + m[0].length;
m = CITATION.exec(text);
}
if (last < text.length) out.push(text.slice(last));
return out;
}

View File

@@ -0,0 +1,198 @@
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { useState } from "react";
import {
api,
fmtDay,
fmtTime,
fmtTokens,
selectionIsRunnable,
shortId,
type Bucket,
type PageCursor,
type Selection,
type TimeWindow,
} from "../api";
type Props = { selection: Selection; window: TimeWindow };
/** Selection preview: stats, daily rhythm, and the actual matching events. */
export function Explore({ selection, window }: Props) {
const runnable = selectionIsRunnable(selection);
const preview = useQuery({
queryKey: ["preview", selection, window],
queryFn: () => api.preview(selection, window),
enabled: runnable,
});
const events = useInfiniteQuery({
queryKey: ["events", selection, window],
queryFn: ({ pageParam }) => api.events(selection, window, 50, pageParam),
initialPageParam: null as PageCursor | null,
getNextPageParam: (last) => last.next,
enabled: runnable,
});
if (!runnable) {
return (
<div className="center-note">
Pick at least one channel or author on the left to preview a selection.
<br />
Everything here is free — no model is called until you press Run on a
fold.
</div>
);
}
const p = preview.data;
const allEvents = events.data?.pages.flatMap((page) => page.events) ?? [];
return (
<div>
{preview.isError && (
<div className="error-box">{(preview.error as Error).message}</div>
)}
{p && (
<div className="stat-row">
<div className="stat">
<div className="num">{p.count.toLocaleString()}</div>
<div className="cap">events</div>
</div>
<div className="stat">
<div className="num">{p.total_chars.toLocaleString()}</div>
<div className="cap">chars</div>
</div>
<div className="stat">
<div className="num">~{fmtTokens(Math.ceil(p.total_chars / 4))}</div>
<div className="cap">est. input tokens</div>
</div>
{p.oldest_ts !== null && p.newest_ts !== null && (
<div className="stat">
<div className="num" style={{ fontSize: 14, paddingTop: 5 }}>
{fmtTime(p.oldest_ts)} → {fmtTime(p.newest_ts)}
</div>
<div className="cap">span</div>
</div>
)}
</div>
)}
{p && p.buckets.length > 0 && <RhythmStrip buckets={p.buckets} />}
{allEvents.map((ev) => (
<EventRow
key={ev.id}
id={ev.id}
author={ev.author_name ?? shortId(ev.pubkey)}
time={ev.created_at}
content={ev.content}
/>
))}
{events.hasNextPage && (
<button
onClick={() => events.fetchNextPage()}
disabled={events.isFetchingNextPage}
>
{events.isFetchingNextPage ? "loading…" : "load more"}
</button>
)}
{events.data && allEvents.length === 0 && (
<div className="center-note">No events match this selection.</div>
)}
</div>
);
}
function EventRow({
id,
author,
time,
content,
}: {
id: string;
author: string;
time: number;
content: string;
}) {
const [expanded, setExpanded] = useState(false);
return (
<div
className={`event-row${expanded ? " expanded" : ""}`}
onClick={() => setExpanded((e) => !e)}
>
<div className="head">
<strong>{author}</strong>
<span>{fmtTime(time)}</span>
<span className="mono faint">{shortId(id)}</span>
</div>
<div className="body">{content}</div>
</div>
);
}
/** Daily activity histogram. Single series: one hue, gaps filled with
* zero-height days so rhythm (and silence) is visible. */
function RhythmStrip({ buckets }: { buckets: Bucket[] }) {
const DAY = 86_400;
const first = buckets[0];
const last = buckets[buckets.length - 1];
if (!first || !last) return null;
const byDay = new Map(buckets.map((b) => [b.day, b.count]));
const days: Bucket[] = [];
for (let d = first.day; d <= last.day; d += DAY) {
days.push({ day: d, count: byDay.get(d) ?? 0 });
}
// Beyond ~a year of days, group whole weeks so bars stay visible.
const perWeek = days.length > 366;
const cells: Bucket[] = perWeek
? Array.from({ length: Math.ceil(days.length / 7) }, (_, i) => ({
day: days[i * 7]?.day ?? 0,
count: days
.slice(i * 7, i * 7 + 7)
.reduce((acc, b) => acc + b.count, 0),
}))
: days;
const w = 800;
const h = 64;
const gap = 2;
const bw = Math.max(1, w / cells.length - gap);
const max = Math.max(1, ...cells.map((c) => c.count));
return (
<div className="rhythm">
<div className="cap faint" style={{ marginBottom: 6, fontSize: 11 }}>
ACTIVITY BY {perWeek ? "WEEK" : "DAY"} · {fmtDay(first.day)} –{" "}
{fmtDay(last.day)}
</div>
<svg
viewBox={`0 0 ${w} ${h}`}
width="100%"
height={h}
role="img"
aria-label="activity histogram"
>
{cells.map((c, i) => {
const bh = Math.max(c.count > 0 ? 2 : 0, (c.count / max) * (h - 4));
return (
<rect
key={c.day}
className="bar"
x={(i * w) / cells.length}
y={h - bh}
width={bw}
height={bh}
rx={Math.min(2, bw / 2)}
fill="var(--accent)"
>
<title>
{fmtDay(c.day)}: {c.count} events
</title>
</rect>
);
})}
</svg>
</div>
);
}

View File

@@ -0,0 +1,262 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import {
api,
fmtTime,
fmtTokens,
selectionIsRunnable,
type FoldRow,
type Preflight,
type RunOutcome,
type Selection,
} from "../api";
type Props = {
selection: Selection;
onOpenArtifact: (fold: string, version: number) => void;
};
export function FoldsBoard({ selection, onOpenArtifact }: Props) {
const folds = useQuery({ queryKey: ["folds"], queryFn: api.folds });
return (
<div>
<h2>Folds</h2>
<CreateFold selection={selection} />
{folds.isError && (
<div className="error-box">{(folds.error as Error).message}</div>
)}
{folds.data?.folds.length === 0 && (
<div className="faint">
No folds yet. Compose a selection, then create one above — runs are
always priced before they spend.
</div>
)}
{folds.data?.folds.map((f) => (
<FoldCard key={f.name} fold={f} onOpenArtifact={onOpenArtifact} />
))}
</div>
);
}
function CreateFold({ selection }: { selection: Selection }) {
const qc = useQueryClient();
const [name, setName] = useState("");
const [model, setModel] = useState("haiku");
const [instructions, setInstructions] = useState("");
const create = useMutation({
mutationFn: () =>
api.putFold(name.trim(), {
selection,
model,
...(instructions.trim() ? { instructions } : {}),
}),
onSuccess: () => {
setName("");
setInstructions("");
qc.invalidateQueries({ queryKey: ["folds"] });
},
});
const ready = name.trim().length > 0 && selectionIsRunnable(selection);
return (
<div className="create-fold">
<div className="field">
<label>New fold (uses the current selection)</label>
<input
className="mono"
placeholder="name — [a-z0-9-], e.g. digest--2026-w36"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="field">
<label>Model</label>
<select value={model} onChange={(e) => setModel(e.target.value)}>
<option value="haiku">haiku</option>
<option value="sonnet">sonnet</option>
<option value="opus">opus</option>
</select>
</div>
<div className="field">
<label>Instructions</label>
<textarea
rows={2}
placeholder="empty = built-in channel-digest prompt"
value={instructions}
onChange={(e) => setInstructions(e.target.value)}
/>
</div>
<button
className="primary"
disabled={!ready || create.isPending}
onClick={() => create.mutate()}
>
{create.isPending ? "saving…" : "create fold"}
</button>
{!selectionIsRunnable(selection) && (
<span className="faint" style={{ marginLeft: 8 }}>
needs a selection
</span>
)}
{create.isError && (
<div className="plan-box err">{(create.error as Error).message}</div>
)}
</div>
);
}
function FoldCard({
fold,
onOpenArtifact,
}: {
fold: FoldRow;
onOpenArtifact: (fold: string, version: number) => void;
}) {
const qc = useQueryClient();
const [plan, setPlan] = useState<Preflight | null>(null);
const [outcome, setOutcome] = useState<RunOutcome | null>(null);
const preflight = useMutation({
mutationFn: () => api.preflight(fold.name, {}),
onSuccess: (p) => {
setPlan(p);
setOutcome(null);
},
});
const run = useMutation({
mutationFn: () => {
if (plan?.plan !== "ready") throw new Error("preflight first");
// Pin the run to the exact window that was priced.
const [since, until_exclusive] = plan.window;
return api.run(fold.name, { since, until_exclusive });
},
onSuccess: (o) => {
setOutcome(o);
setPlan(null);
qc.invalidateQueries({ queryKey: ["folds"] });
qc.invalidateQueries({ queryKey: ["artifacts", fold.name] });
if (o.status === "folded") {
onOpenArtifact(fold.name, o.artifact.version);
}
},
onError: () => setPlan(null),
});
const sel = fold.spec.selection;
const selDesc = [
sel.channels.length > 0 ? `${sel.channels.length} channel(s)` : null,
sel.authors.length > 0 ? `${sel.authors.length} author(s)` : null,
]
.filter(Boolean)
.join(" · ");
return (
<div className="fold-card">
<div className="name mono">{fold.name}</div>
<div className="desc">
{selDesc || "—"} · {fold.spec.model} ·{" "}
{fold.versions > 0
? `v${fold.latest_version} · updated ${fmtTime(fold.updated_at)}`
: "never run"}
</div>
<div className="actions">
<button
onClick={() => preflight.mutate()}
disabled={preflight.isPending || run.isPending}
>
{preflight.isPending ? "pricing…" : "preflight ($0)"}
</button>
<button
className="primary"
disabled={plan?.plan !== "ready" || run.isPending}
onClick={() => run.mutate()}
>
{run.isPending ? "folding…" : "run"}
</button>
{fold.latest_version !== null && (
<button onClick={() => onOpenArtifact(fold.name, fold.latest_version ?? 1)}>
read v{fold.latest_version}
</button>
)}
</div>
{preflight.isError && (
<div className="plan-box err">{(preflight.error as Error).message}</div>
)}
{run.isError && (
<div className="plan-box err">{(run.error as Error).message}</div>
)}
{plan && <PlanBox plan={plan} />}
{outcome && <OutcomeBox outcome={outcome} />}
</div>
);
}
function PlanBox({ plan }: { plan: Preflight }) {
if (plan.plan === "cached") {
return <div className="plan-box ok">Up to date — nothing new to fold.</div>;
}
if (plan.plan === "stalled") {
return (
<div className="plan-box warn">
Stalled: {plan.reason} ({plan.pending} pending)
</div>
);
}
const est = plan.estimate;
const fits = est.window_fit.fits;
return (
<div className="plan-box ok">
Ready: {plan.shown} shown / {plan.pending} pending
{plan.truncated ? " (chunked)" : ""} · ~
{fmtTokens(est.est_input_tokens)} input tokens
{fits === false ? " · OVER the model window" : ""}
<br />
<span className="faint">
Run is pinned to this priced window; press run to spend.
</span>
</div>
);
}
function OutcomeBox({ outcome }: { outcome: RunOutcome }) {
switch (outcome.status) {
case "cached":
return <div className="plan-box ok">Already up to date.</div>;
case "stalled":
return (
<div className="plan-box warn">
Stalled: {outcome.reason} ({outcome.pending} pending)
</div>
);
case "refused":
return (
<div className="plan-box err">
Model output refused ({outcome.reason}). Nothing persisted; raw
output:
<pre className="mono" style={{ whiteSpace: "pre-wrap" }}>
{outcome.model_output.slice(0, 2000)}
</pre>
</div>
);
case "unpublished":
return (
<div className="plan-box err">
Not persisted: {outcome.reason}
</div>
);
case "folded":
return (
<div className="plan-box ok">
Folded v{outcome.artifact.version} — {outcome.shown} shown
{outcome.truncated
? ` (chunked; ${outcome.pending - outcome.shown} remain)`
: ""}
.
</div>
);
}
}

View File

@@ -0,0 +1,189 @@
import { useQuery } from "@tanstack/react-query";
import { api, type Selection, type TimeWindow } from "../api";
const DAY = 86_400;
const PRESETS: { label: string; days: number | null }[] = [
{ label: "7d", days: 7 },
{ label: "30d", days: 30 },
{ label: "90d", days: 90 },
{ label: "all", days: null },
];
type Props = {
selection: Selection;
onChange: (s: Selection) => void;
window: TimeWindow;
onWindowChange: (w: TimeWindow) => void;
};
export function SelectionComposer({
selection,
onChange,
window,
onWindowChange,
}: Props) {
const channels = useQuery({ queryKey: ["channels"], queryFn: api.channels });
const toggleChannel = (id: string) => {
const has = selection.channels.includes(id);
onChange({
...selection,
channels: has
? selection.channels.filter((c) => c !== id)
: [...selection.channels, id],
});
};
const activePreset = (days: number | null) => {
if (days === null) return window.since === undefined;
if (window.since === undefined) return false;
const target = Math.floor(Date.now() / 1000) - days * DAY;
// A preset stays highlighted for the minute it was clicked in.
return Math.abs(window.since - target) < 60;
};
const list = channels.data?.channels.filter((c) => c.active) ?? [];
const sorted = [...list].sort((a, b) =>
(a.name ?? a.id).localeCompare(b.name ?? b.id),
);
return (
<div>
<h2>Selection</h2>
<div className="field">
<label>
Channels{" "}
<span className="faint">
({selection.channels.length || "none"} selected)
</span>
</label>
{channels.isLoading ? (
<div className="faint">loading…</div>
) : (
<div className="channel-list">
{sorted.map((c) => (
<label key={c.id}>
<input
type="checkbox"
checked={selection.channels.includes(c.id)}
onChange={() => toggleChannel(c.id)}
/>
<span>{c.name ?? `${c.id.slice(0, 8)}…`}</span>
{!c.backfill_done && <span className="faint">(syncing)</span>}
</label>
))}
</div>
)}
<div style={{ marginTop: 4 }}>
<button
onClick={() => onChange({ ...selection, channels: [] })}
disabled={selection.channels.length === 0}
>
clear
</button>
</div>
</div>
<div className="field">
<label>Authors (64-hex pubkeys, one per line)</label>
<textarea
rows={2}
className="mono"
placeholder="optional — person-across-channels"
value={selection.authors.join("\n")}
onChange={(e) =>
onChange({
...selection,
authors: e.target.value
.split(/[\s,]+/)
.map((a) => a.trim())
.filter(Boolean),
})
}
/>
</div>
<div className="field">
<label>Kinds (comma-separated; empty = 9, chat)</label>
<input
className="mono"
placeholder="9"
value={selection.kinds.join(",")}
onChange={(e) =>
onChange({
...selection,
kinds: e.target.value
.split(",")
.map((k) => Number.parseInt(k.trim(), 10))
.filter((k) => Number.isFinite(k) && k >= 0),
})
}
/>
</div>
<h2>Window</h2>
<div className="field">
<div className="preset-row">
{PRESETS.map((p) => (
<button
key={p.label}
className={activePreset(p.days) ? "active" : ""}
onClick={() =>
onWindowChange(
p.days === null
? {}
: {
since:
Math.floor(Date.now() / 1000) - (p.days ?? 0) * DAY,
},
)
}
>
{p.label}
</button>
))}
</div>
</div>
<div className="field">
<label>Since</label>
<input
type="datetime-local"
value={toLocalInput(window.since)}
onChange={(e) =>
onWindowChange({ ...window, since: fromLocalInput(e.target.value) })
}
/>
</div>
<div className="field">
<label>Until (exclusive)</label>
<input
type="datetime-local"
value={toLocalInput(window.until_exclusive)}
onChange={(e) =>
onWindowChange({
...window,
until_exclusive: fromLocalInput(e.target.value),
})
}
/>
</div>
</div>
);
}
function toLocalInput(ts: number | undefined): string {
if (ts === undefined) return "";
const d = new Date(ts * 1000);
const pad = (n: number) => `${n}`.padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(
d.getHours(),
)}:${pad(d.getMinutes())}`;
}
function fromLocalInput(v: string): number | undefined {
if (!v) return undefined;
const ms = new Date(v).getTime();
return Number.isFinite(ms) ? Math.floor(ms / 1000) : undefined;
}

View File

@@ -0,0 +1,22 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";
import "./styles.css";
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: 1, refetchOnWindowFocus: false },
},
});
const root = document.getElementById("root");
if (root) {
createRoot(root).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</StrictMode>,
);
}

View File

@@ -0,0 +1,468 @@
:root {
--bg: #0f1115;
--surface: #171a21;
--surface-2: #1e222c;
--border: #2a2f3b;
--ink: #e6e9ef;
--ink-2: #9aa3b2;
--ink-3: #6b7484;
--accent: #e3ab47;
--accent-ink: #1a1405;
--good: #6fbf85;
--bad: #e07a6b;
color-scheme: dark;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font: 14px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
#root {
height: 100vh;
display: flex;
flex-direction: column;
}
button {
font: inherit;
color: inherit;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 6px;
padding: 4px 10px;
cursor: pointer;
}
button:hover:not(:disabled) {
border-color: var(--ink-3);
}
button:disabled {
opacity: 0.45;
cursor: default;
}
button.primary {
background: var(--accent);
color: var(--accent-ink);
border-color: transparent;
font-weight: 600;
}
input,
textarea,
select {
font: inherit;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 5px 8px;
}
input:focus,
textarea:focus,
select:focus {
outline: none;
border-color: var(--accent);
}
/* Outranks the `.field input { width: 100% }` block-input default. */
input[type="checkbox"],
.field input[type="checkbox"] {
width: 14px;
height: 14px;
padding: 0;
flex: none;
accent-color: var(--accent);
}
.app-header {
display: flex;
align-items: center;
gap: 14px;
padding: 10px 16px;
border-bottom: 1px solid var(--border);
background: var(--surface);
}
.app-header h1 {
font-size: 15px;
margin: 0;
letter-spacing: 0.03em;
}
.status-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 2px 10px;
border-radius: 999px;
border: 1px solid var(--border);
color: var(--ink-2);
font-size: 12px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--ink-3);
}
.status-dot.connected {
background: var(--good);
}
.status-dot.error {
background: var(--bad);
}
.muted {
color: var(--ink-2);
}
.faint {
color: var(--ink-3);
}
.columns {
flex: 1;
display: grid;
grid-template-columns: 300px 1fr 360px;
min-height: 0;
}
.pane {
overflow-y: auto;
padding: 14px;
min-height: 0;
}
.pane.left {
border-right: 1px solid var(--border);
}
.pane.right {
border-left: 1px solid var(--border);
}
.pane h2 {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--ink-3);
margin: 0 0 10px;
}
.field {
margin-bottom: 12px;
}
.field label {
display: block;
font-size: 12px;
color: var(--ink-2);
margin-bottom: 4px;
}
.field input,
.field textarea,
.field select {
width: 100%;
}
.channel-list {
display: flex;
flex-direction: column;
gap: 2px;
max-height: 320px;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: 6px;
padding: 6px;
background: var(--surface);
}
.channel-list label {
display: flex;
gap: 8px;
align-items: baseline;
padding: 3px 4px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.channel-list label:hover {
background: var(--surface-2);
}
.preset-row {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.preset-row button.active {
border-color: var(--accent);
color: var(--accent);
}
.stat-row {
display: flex;
gap: 20px;
padding: 12px 14px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.stat .num {
font-size: 20px;
font-weight: 650;
font-variant-numeric: tabular-nums;
}
.stat .cap {
font-size: 11px;
color: var(--ink-3);
text-transform: uppercase;
letter-spacing: 0.06em;
}
.rhythm {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 14px;
margin-bottom: 12px;
}
.rhythm .bar:hover {
fill: var(--ink);
}
.event-row {
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 8px;
margin-bottom: 6px;
background: var(--surface);
cursor: pointer;
}
.event-row .head {
display: flex;
gap: 10px;
font-size: 12px;
color: var(--ink-2);
margin-bottom: 3px;
}
.event-row .body {
white-space: pre-wrap;
word-break: break-word;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.event-row.expanded .body {
display: block;
-webkit-line-clamp: none;
}
.fold-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px;
margin-bottom: 10px;
}
.fold-card .name {
font-weight: 650;
margin-bottom: 2px;
}
.fold-card .desc {
font-size: 12px;
color: var(--ink-2);
margin-bottom: 8px;
}
.fold-card .actions {
display: flex;
gap: 6px;
flex-wrap: wrap;
align-items: center;
}
.plan-box {
margin-top: 8px;
padding: 8px 10px;
border-radius: 6px;
background: var(--surface-2);
font-size: 12.5px;
}
.plan-box.err {
border-left: 3px solid var(--bad);
}
.plan-box.ok {
border-left: 3px solid var(--good);
}
.plan-box.warn {
border-left: 3px solid var(--accent);
}
.create-fold {
border: 1px dashed var(--border);
border-radius: 8px;
padding: 12px;
margin-bottom: 14px;
}
.chip {
display: inline-block;
padding: 0 6px;
margin: 0 2px;
border-radius: 999px;
border: 1px solid var(--border);
background: var(--surface-2);
color: var(--accent);
font: 11.5px/1.6 ui-monospace, "SF Mono", Menlo, monospace;
cursor: pointer;
vertical-align: baseline;
}
.chip:hover {
border-color: var(--accent);
}
.artifact {
display: grid;
grid-template-columns: minmax(0, 1fr) 300px;
gap: 14px;
align-items: start;
}
.artifact-body {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px 18px;
word-break: break-word;
}
.artifact-body h3 {
font-size: 16px;
border-bottom: 1px solid var(--border);
padding-bottom: 6px;
margin: 18px 0 10px;
}
.artifact-body h3:first-child {
margin-top: 0;
}
.artifact-body h4 {
font-size: 14px;
margin: 14px 0 6px;
}
.artifact-body ul {
margin: 6px 0;
padding-left: 20px;
}
.artifact-body li {
margin-bottom: 4px;
}
.artifact-body p {
margin: 6px 0;
}
.version-strip {
display: flex;
gap: 4px;
flex-wrap: wrap;
margin-bottom: 10px;
}
.version-strip button.active {
border-color: var(--accent);
color: var(--accent);
}
.evidence {
position: sticky;
top: 0;
}
.evidence-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 12px;
margin-bottom: 8px;
font-size: 13px;
}
.evidence-card .head {
display: flex;
justify-content: space-between;
gap: 8px;
font-size: 12px;
color: var(--ink-2);
margin-bottom: 4px;
}
.evidence-card .body {
white-space: pre-wrap;
word-break: break-word;
max-height: 200px;
overflow-y: auto;
}
.toolbar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.error-box {
padding: 10px 12px;
border: 1px solid var(--bad);
border-radius: 8px;
color: var(--bad);
margin-bottom: 12px;
font-size: 13px;
}
.center-note {
color: var(--ink-3);
padding: 40px 20px;
text-align: center;
}
code,
.mono {
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 12px;
}

View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noEmit": true,
"skipLibCheck": true,
"isolatedModules": true,
"useDefineForClassFields": true
},
"include": ["src", "vite.config.ts"]
}

View File

@@ -0,0 +1,16 @@
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
// The lab talks to the daemon through this dev proxy: same origin, no CORS,
// and the daemon's HTTP API stays loopback-pure with zero UI knowledge.
export default defineConfig({
plugins: [react()],
server: {
proxy: {
"/api": {
target: "http://127.0.0.1:4640",
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
});