design(chat): implement glass-box phase tracker and checkpoint rendering in timeline
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user