diff --git a/vibn-frontend/app/api/chat/route.ts b/vibn-frontend/app/api/chat/route.ts index e30d19a8..1fd7d8d1 100644 --- a/vibn-frontend/app/api/chat/route.ts +++ b/vibn-frontend/app/api/chat/route.ts @@ -1714,31 +1714,33 @@ export async function POST(request: Request) { temperature: 0.3, signal: clientSignal, }); - if (summary.thoughts) { - assistantTimeline.push({ - kind: "thought", - text: summary.thoughts, - }); - emit({ type: "thinking", text: summary.thoughts }); - } - if (summary.text && summary.text.trim()) { - assistantTimeline.push({ kind: "text", text: summary.text }); assistantText += (assistantText ? "\n\n" : "") + summary.text; assistantTextSegments.push(summary.text); emit({ type: "text", text: summary.text }); } else { + // Gemini returned empty — fall back to a deterministic but + // STRUCTURED build-health status (never a vague "didn't reach a + // clean stopping point"). It states what happened, what broke, + // and the next action, using the same signals as the telemetry + // stop_reason. const fallback = buildHealthStatus({ loopBreakReason, hitRoundCap: maxToolRounds > 0 && round >= maxToolRounds, lastError: extractLastToolFailure(messages), toolCount: assistantToolCalls.length, }); - assistantTimeline.push({ kind: "text", text: fallback }); assistantText += (assistantText ? "\n\n" : "") + fallback; assistantTextSegments.push(fallback); emit({ type: "text", text: fallback }); } + if (summary.thoughts) { + assistantTimeline.push({ + kind: "thought", + text: summary.thoughts, + }); + emit({ type: "thinking", text: summary.thoughts }); + } } catch { const fallback = buildHealthStatus({ loopBreakReason, @@ -1771,12 +1773,7 @@ export async function POST(request: Request) { temperature: 0.3, signal: clientSignal, }); - if (finalSummary.thoughts) { - assistantTimeline.push({ kind: "thought", text: finalSummary.thoughts }); - emit({ type: "thinking", text: finalSummary.thoughts }); - } if (finalSummary.text && finalSummary.text.trim()) { - assistantTimeline.push({ kind: "text", text: finalSummary.text }); assistantText += (assistantText ? "\n\n" : "") + finalSummary.text; assistantTextSegments.push(finalSummary.text); diff --git a/vibn-frontend/components/vibn-chat/chat-panel.tsx b/vibn-frontend/components/vibn-chat/chat-panel.tsx index 264d0567..81ee9d2e 100644 --- a/vibn-frontend/components/vibn-chat/chat-panel.tsx +++ b/vibn-frontend/components/vibn-chat/chat-panel.tsx @@ -284,11 +284,14 @@ function summarizeToolResult(result?: string): { } // Plain-text heuristics + // We explicitly ignore 'error' and 'exception' here because tools like dev_server_logs + // or browser_console legitimately return stack traces when working correctly. + // A raw string with 'error' inside it shouldn't auto-fail the tool execution pill. const lower = raw.toLowerCase(); if ( - /(econnrefused|enoent|error|failed|traceback|exception|not found|permission denied|cannot)/.test( + /(econnrefused|enoent|permission denied|command not found)/.test( lower, - ) + ) && !raw.includes("dev_server_logs") && !raw.includes("browser_console") ) { return { ok: false, label: `Failed — ${firstLine(raw)}` }; } @@ -665,8 +668,6 @@ function Timeline({ entries, isActiveStream }: { entries: TimelineEntry[], isAct * bubble so each round of multi-tool-loop output reads as a discrete * step instead of concatenating into a wall of text. */ - - function TimelineText({ text, isStreaming }: { text: string; isStreaming?: boolean }) { const proseWrap: React.CSSProperties = { overflowWrap: "anywhere", @@ -1062,7 +1063,6 @@ export function ChatPanel({ .catch(() => {}); }, [projectId, workspace, status]); const [sending, setSending] = useState(false); - const [showScrollButton, setShowScrollButton] = useState(false); const [currentPhaseLabel, setCurrentPhaseLabel] = useState( null, ); @@ -1551,12 +1551,15 @@ export function ChatPanel({ } } catch { // 2. If it's a raw string (like a bash crash), scan for fatal keywords + // We skip this check for log-reading tools since they legitimately contain errors. const lower = ev.result.toLowerCase(); if ( - lower.includes("error") || - lower.includes("failed") || - lower.includes("unexpected") || - lower.includes("not found") + !ev.name?.includes("logs") && + !ev.name?.includes("console") && + (lower.includes("econnrefused") || + lower.includes("enoent") || + lower.includes("permission denied") || + lower.includes("command not found")) ) { isToolErr = true; } @@ -1893,11 +1896,6 @@ export function ChatPanel({ {/* Messages */}
{ - const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; - const distanceToBottom = scrollHeight - scrollTop - clientHeight; - setShowScrollButton(distanceToBottom > 150); - }} style={{ flex: 1, minWidth: 0, @@ -2127,8 +2125,6 @@ export function ChatPanel({ - - {(selectToggle) => (