design(chat): implement glass-box phase tracker and checkpoint rendering in timeline

This commit is contained in:
2026-06-09 19:01:44 -07:00
parent ca47d0643d
commit 662caf230a

View File

@@ -65,6 +65,8 @@ interface Message {
type TimelineEntry =
| { kind: "thought"; text: string }
| { kind: "phase"; phase: string; label: string }
| { kind: "checkpoint"; goal: string; findings: string }
| {
kind: "tool";
name: string;
@@ -604,6 +606,8 @@ function Timeline({ entries }: { entries: TimelineEntry[] }) {
type Item =
| { kind: "thought"; text: string }
| { kind: "text"; text: string }
| { kind: "phase"; phase: string; label: string }
| { kind: "checkpoint"; goal: string; findings: string }
| {
kind: "toolGroup";
category: string;
@@ -615,6 +619,10 @@ function Timeline({ entries }: { entries: TimelineEntry[] }) {
items.push({ kind: "thought", text: e.text });
} else if (e.kind === "text") {
items.push({ kind: "text", text: e.text });
} else if (e.kind === "phase") {
items.push({ kind: "phase", phase: e.phase, label: e.label });
} else if (e.kind === "checkpoint") {
items.push({ kind: "checkpoint", goal: e.goal, findings: e.findings });
} else {
const last = items[items.length - 1];
const category = getFriendlyCategory(e.name);
@@ -634,6 +642,69 @@ function Timeline({ entries }: { entries: TimelineEntry[] }) {
if (item.kind === "text") {
return <TimelineText key={i} text={item.text} />;
}
if (item.kind === "phase") {
return (
<div
key={i}
style={{
padding: "12px 14px",
margin: "12px 0 6px",
borderBottom: "1px solid var(--hairline)",
display: "flex",
alignItems: "center",
gap: 8,
color: "var(--fg)",
}}
>
<div
style={{
width: 8,
height: 8,
borderRadius: "50%",
background: "var(--accent)",
boxShadow: "0 0 10px var(--accent-glow)",
}}
/>
<span
style={{
fontSize: "0.85rem",
fontWeight: 600,
letterSpacing: "-0.01em",
}}
>
{item.label}
</span>
</div>
);
}
if (item.kind === "checkpoint") {
return (
<div
key={i}
style={{
margin: "6px 0 12px",
padding: "12px 14px",
background: "oklch(0.20 0.04 35 / 0.15)",
border: "1px dashed var(--accent)",
borderRadius: 8,
fontSize: "0.75rem",
color: "var(--fg-mute)",
fontFamily: "var(--font-mono), monospace",
}}
>
<div
style={{
color: "var(--accent)",
fontWeight: "bold",
marginBottom: 4,
}}
>
[Checkpoint Logged]
</div>
<div style={{ opacity: 0.8 }}>{item.goal}</div>
</div>
);
}
return (
<TimelineToolGroup
key={i}
@@ -1324,6 +1395,10 @@ export function ChatPanel({
name?: string;
result?: string;
error?: string;
phase?: string;
label?: string;
goal?: string;
findings?: string;
};
try {
ev = JSON.parse(line.slice(6));
@@ -1331,7 +1406,41 @@ export function ChatPanel({
continue;
}
if (ev.type === "text" && ev.text) {
if (ev.type === "phase" && ev.phase && ev.label) {
setMessages((prev) => {
const next = [...prev];
if (msgIndex >= 0 && next[msgIndex]) {
const tl = next[msgIndex].timeline ?? [];
next[msgIndex] = {
...next[msgIndex],
timeline: [
...tl,
{ kind: "phase", phase: ev.phase!, label: ev.label! },
],
};
}
return next;
});
} else if (ev.type === "checkpoint" && ev.goal) {
setMessages((prev) => {
const next = [...prev];
if (msgIndex >= 0 && next[msgIndex]) {
const tl = next[msgIndex].timeline ?? [];
next[msgIndex] = {
...next[msgIndex],
timeline: [
...tl,
{
kind: "checkpoint",
goal: ev.goal!,
findings: ev.findings!,
},
],
};
}
return next;
});
} else if (ev.type === "text" && ev.text) {
// Each text SSE event = one round of the model's text
// output. Push a new "text" timeline entry so the
// renderer can show multi-round turns as separate