Dreams are currently a black box — to benchmark Tier L models on dreamer
induction quality we need to see what each model produced. This adds a
workspace-scoped /dreams route that surfaces every dream run as a
group of conclusions split into explicit / deductive / inductive
columns, with click-to-expand premise trees.
What's here:
- `lib/dreams.ts`: pure helpers. `clusterConclusionsIntoDreams` groups a
raw conclusions list into per-pair bursts using a configurable time
window (default 60s). `expandPremiseTree` walks `reasoning_tree` first
and falls back to a flat `premises` ID list, with cycle detection and
a depth cap. Defines an `ExtendedConclusion` type that augments the
generated schema with `conclusion_type`, `premises`, and
`reasoning_tree` — Honcho's migration f1a2b3c4d5e6 added those
columns but `schema.d.ts` doesn't expose them yet, so the UI degrades
gracefully (unknown types fall into "explicit").
- `api/queries.ts`: new `useDreams` hook that paginates the conclusions
list endpoint up to a configurable cap (default 400) and hands the
raw list to the UI for clustering. New `dreams` query key.
- `components/dreams/`:
- `DreamList.tsx` — route entry. Shows recent dreams as rows with
timestamp, observer→observed pair, and per-type counts. Selecting
a row expands an inline detail panel above the list.
- `DreamDetail.tsx` — three-column view (explicit / deductive /
inductive). Each inductive conclusion has a "Show premises" button
that expands the reasoning chain.
- `PremiseTree.tsx` — recursive premise renderer with type badges,
cycle indicator, and graceful handling of premises that fall
outside the loaded page.
- Routing: `routes/workspaces_.$workspaceId_.dreams.tsx` registers the
page; routeTree.gen.ts regenerated.
- Sidebar: new "Dreams" entry between Conclusions and Webhooks
(MoonStar icon to distinguish from the dark-mode Moon).
- Breadcrumb: "dreams" added to SECTION_LABELS so the trail resolves.
Tests (vitest, 14 new):
- clustering: empty input; same-pair burst within window; gap exceeds
threshold → split; different pairs don't merge even with overlapping
timestamps; custom gap window; newest-first ordering; counts default
unknown types to explicit.
- premise tree: empty children for no premises; flat list → direct
children; multi-level reasoning_tree recursion; missing premise
flagged (not in loaded page); cycle detection halts recursion;
maxDepth cap; reasoning_tree preferred over flat premises.
Verified manually in the preview: route mounts, sidebar/breadcrumb
resolve, error path renders cleanly, typecheck + lint + dream tests
all clean. The unrelated `app.test.tsx` failure is pre-existing on
main.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
106 lines
2.9 KiB
TypeScript
106 lines
2.9 KiB
TypeScript
import { Link, useRouter } from "@tanstack/react-router";
|
|
import { ChevronRight } from "lucide-react";
|
|
import { useDemo } from "@/hooks/useDemo";
|
|
|
|
const SECTION_LABELS: Record<string, string> = {
|
|
peers: "Peers",
|
|
sessions: "Sessions",
|
|
conclusions: "Conclusions",
|
|
dreams: "Dreams",
|
|
webhooks: "Webhooks",
|
|
chat: "Chat",
|
|
};
|
|
|
|
const KNOWN_SECTIONS = new Set(Object.keys(SECTION_LABELS));
|
|
|
|
type Segment = { label: string; href: string | null; mono?: boolean };
|
|
|
|
function buildSegments(pathname: string, mask: (v: string) => string): Segment[] {
|
|
if (!pathname.startsWith("/workspaces")) return [];
|
|
|
|
const rest = pathname.slice("/workspaces".length); // "" | "/wid" | "/wid/peers" | ...
|
|
|
|
if (!rest) return [{ label: "Workspaces", href: null }];
|
|
|
|
const parts = rest.slice(1).split("/"); // ["wid"] | ["wid", "peers"] | ...
|
|
const wid = parts[0];
|
|
if (!wid) return [{ label: "Workspaces", href: null }];
|
|
|
|
const segments: Segment[] = [{ label: "Workspaces", href: "/workspaces" }];
|
|
|
|
if (parts.length === 1) {
|
|
segments.push({ label: mask(wid), href: null, mono: true });
|
|
return segments;
|
|
}
|
|
segments.push({ label: mask(wid), href: `/workspaces/${wid}`, mono: true });
|
|
|
|
const section = parts[1];
|
|
if (!section || !KNOWN_SECTIONS.has(section)) return segments;
|
|
|
|
if (parts.length === 2) {
|
|
segments.push({ label: SECTION_LABELS[section], href: null });
|
|
return segments;
|
|
}
|
|
segments.push({ label: SECTION_LABELS[section], href: `/workspaces/${wid}/${section}` });
|
|
|
|
const subId = parts[2];
|
|
if (!subId) return segments;
|
|
|
|
if (parts.length === 3) {
|
|
segments.push({ label: mask(subId), href: null, mono: true });
|
|
return segments;
|
|
}
|
|
segments.push({
|
|
label: mask(subId),
|
|
href: `/workspaces/${wid}/${section}/${subId}`,
|
|
mono: true,
|
|
});
|
|
|
|
const subSection = parts[3];
|
|
if (subSection && SECTION_LABELS[subSection]) {
|
|
segments.push({ label: SECTION_LABELS[subSection], href: null });
|
|
}
|
|
|
|
return segments;
|
|
}
|
|
|
|
export function Breadcrumb() {
|
|
const { state } = useRouter();
|
|
const { mask } = useDemo();
|
|
const segments = buildSegments(state.location.pathname, mask);
|
|
|
|
if (segments.length <= 1) return null;
|
|
|
|
return (
|
|
<nav aria-label="Breadcrumb" className="flex items-center gap-1 mb-4 flex-wrap">
|
|
{segments.map((seg, i) => (
|
|
<span key={i} className="flex items-center gap-1">
|
|
{i > 0 && (
|
|
<ChevronRight
|
|
className="w-3 h-3 shrink-0"
|
|
style={{ color: "var(--text-4)" }}
|
|
strokeWidth={2}
|
|
/>
|
|
)}
|
|
{seg.href ? (
|
|
<Link
|
|
to={seg.href as never}
|
|
className={`text-xs transition-colors hover:text-[color:var(--accent-text)] ${seg.mono ? "font-mono" : ""}`}
|
|
style={{ color: "var(--text-3)" }}
|
|
>
|
|
{seg.label}
|
|
</Link>
|
|
) : (
|
|
<span
|
|
className={`text-xs font-medium ${seg.mono ? "font-mono" : ""}`}
|
|
style={{ color: "var(--text-2)" }}
|
|
>
|
|
{seg.label}
|
|
</span>
|
|
)}
|
|
</span>
|
|
))}
|
|
</nav>
|
|
);
|
|
}
|