diff --git a/components/vibn-chat/chat-panel.tsx b/components/vibn-chat/chat-panel.tsx
index e38c9c82..d93ebea1 100644
--- a/components/vibn-chat/chat-panel.tsx
+++ b/components/vibn-chat/chat-panel.tsx
@@ -32,23 +32,23 @@ interface Message {
toolName?: string;
createdAt?: string;
/**
- * First-person reasoning narration streamed alongside tool calls.
- * Rendered as collapsed italic text above the message bubble; the
- * user can expand for the full chain of thought. Discarded on
- * persistence (we pay tokens regardless, but the bytes aren't
- * worth keeping in PG).
+ * Chronological turn timeline interleaving the model's thinking
+ * narration and the tool calls it fired. Rendered as a stack of
+ * pills INSIDE the bubble above the final text content, so the
+ * user sees the actual flow:
+ * [thought] [tool ×N] [thought] [tool] ... [summary]
+ * Each thought is its own collapsed pill (click to expand);
+ * adjacent runs of the same tool name collapse into one pill
+ * with a ×N counter. The final assistant text is rendered
+ * separately, below the timeline.
*/
- thoughts?: string;
- /**
- * Tool calls executed during this assistant turn, in chronological
- * order. Rendered as pills INSIDE the bubble above the text content
- * so the final summary lands where the user's eye naturally goes
- * (the bottom of the bubble) instead of being buried above the
- * tool tray.
- */
- toolEvents?: ToolEvent[];
+ timeline?: TimelineEntry[];
}
+type TimelineEntry =
+ | { kind: "thought"; text: string }
+ | { kind: "tool"; name: string; status: "running" | "done"; result?: string };
+
interface ToolEvent {
name: string;
status: "running" | "done";
@@ -180,9 +180,8 @@ function MessageBubble({ msg }: { msg: Message }) {
display: "flex",
flexDirection: "column",
}}>
- {!isUser && msg.thoughts && }
- {!isUser && msg.toolEvents && msg.toolEvents.length > 0 && (
-
+ {!isUser && msg.timeline && msg.timeline.length > 0 && (
+
)}
{(msg.content || isUser) && (
= [];
- for (const ev of events) {
- const last = groups[groups.length - 1];
- if (last && last.name === ev.name) last.events.push(ev);
- else groups.push({ name: ev.name, events: [ev] });
+function Timeline({ entries }: { entries: TimelineEntry[] }) {
+ // Walk the entries and emit a renderable list. Adjacent same-named
+ // tool entries get bundled into a TimelineToolGroup; everything
+ // else passes through as-is.
+ type Item =
+ | { kind: "thought"; text: string }
+ | { kind: "toolGroup"; name: string; entries: Array> };
+ const items: Item[] = [];
+ for (const e of entries) {
+ if (e.kind === "thought") {
+ items.push({ kind: "thought", text: e.text });
+ } else {
+ const last = items[items.length - 1];
+ if (last && last.kind === "toolGroup" && last.name === e.name) {
+ last.entries.push(e);
+ } else {
+ items.push({ kind: "toolGroup", name: e.name, entries: [e] });
+ }
+ }
}
return (