diff --git a/vibn-frontend/components/vibn-chat/chat-panel.tsx b/vibn-frontend/components/vibn-chat/chat-panel.tsx index 33333d1..c097c1f 100644 --- a/vibn-frontend/components/vibn-chat/chat-panel.tsx +++ b/vibn-frontend/components/vibn-chat/chat-panel.tsx @@ -69,6 +69,7 @@ type TimelineEntry = name: string; status: "running" | "done" | "error"; result?: string; + args?: Record; } // A text segment from one round of the assistant's tool loop. // Each text SSE event from the server starts a new entry; subsequent @@ -617,7 +618,7 @@ function Timeline({ entries }: { entries: TimelineEntry[] }) { } 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 }); + // ignore } else { const last = items[items.length - 1]; const category = getFriendlyCategory(e.name); @@ -639,48 +640,16 @@ function Timeline({ entries }: { entries: TimelineEntry[] }) { if (item.kind === "text") { return ; } - if (item.kind === "phase") { + if (item.kind === "toolGroup") { return ( -
-
- - {item.label} - -
+ category={item.category} + entries={item.entries} + /> ); } - return ( - - ); + return null; })}
); @@ -722,126 +691,102 @@ function TimelineText({ text }: { text: string }) { } function TimelineToolGroup({ - category, entries, }: { category: string; entries: Array>; }) { - const [expanded, setExpanded] = useState(false); - const count = entries.length; - const allDone = entries.every((e) => e.status === "done"); - const hasError = entries.some((e) => e.status === "error"); - return (
- + {entries.map((e, i) => { + const isError = e.status === "error"; + const isDone = e.status === "done"; - {expanded && ( -
- {entries.map((e, i) => ( -
+ + {isError ? ( + "✗" + ) : !isDone ? ( + + ) : ( + + )} + + -
+ {argSummary && ( + - - {friendlyToolName(e.name)} + > + {argSummary} - {!e.result && e.status === "running" && ( - ... - )} - {(() => { - // Render a clean, human summary of the outcome — never raw JSON. - const summary = summarizeToolResult(e.result); - if (!summary) return null; - return ( - - — {summary.label} - - ); - })()} -
- ))} -
- )} + )} + {(() => { + const summary = summarizeToolResult(e.result); + if (!summary) return null; + return ( + + — {summary.label} + + ); + })()} +
+ ); + })}
); } @@ -1057,6 +1002,9 @@ export function ChatPanel({ .catch(() => {}); }, [projectId, workspace, status]); const [sending, setSending] = useState(false); + const [currentPhaseLabel, setCurrentPhaseLabel] = useState( + null, + ); const [showThreads, setShowThreads] = useState(false); const [mcpToken, setMcpToken] = useState(null); const messagesEndRef = useRef(null); @@ -1286,6 +1234,7 @@ export function ChatPanel({ const text = raw; if (!override) setInput(""); setSending(true); + setCurrentPhaseLabel("Starting..."); const userMsg: Message = { role: "user", content: text }; setMessages((prev) => [...prev, userMsg]); @@ -1374,6 +1323,7 @@ export function ChatPanel({ label?: string; goal?: string; findings?: string; + args?: Record; }; try { ev = JSON.parse(line.slice(6)); @@ -1382,20 +1332,8 @@ export function ChatPanel({ } 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; - }); + setCurrentPhaseLabel(ev.label); + // Legacy phase entries in history are ignored } 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 @@ -1441,6 +1379,13 @@ export function ChatPanel({ return next; }); } else if (ev.type === "tool_start") { + // Update bottom status label dynamically based on tool name + const toolLabel = + friendlyToolName(ev.name ?? "") + .replace(/^[^\w\s]+/, "") + .trim() + "..."; + setCurrentPhaseLabel(toolLabel); + setMessages((prev) => { const next = [...prev]; if (msgIndex >= 0 && next[msgIndex]) { @@ -1449,7 +1394,12 @@ export function ChatPanel({ ...next[msgIndex], timeline: [ ...tl, - { kind: "tool", name: ev.name, status: "running" }, + { + kind: "tool", + name: ev.name, + args: ev.args, + status: "running", + }, ], }; } @@ -1595,6 +1545,7 @@ export function ChatPanel({ } finally { abortRef.current = null; setSending(false); + setCurrentPhaseLabel(null); } }, [ @@ -2052,6 +2003,33 @@ export function ChatPanel({ )} + {/* Action Status Bar anchored above composer */} + {sending && currentPhaseLabel && ( +
+ + + {currentPhaseLabel} + +
+ )} + {(selectToggle) => (