diff --git a/vibn-frontend/components/vibn-chat/chat-panel.tsx b/vibn-frontend/components/vibn-chat/chat-panel.tsx index 559e8fd2..e10b2862 100644 --- a/vibn-frontend/components/vibn-chat/chat-panel.tsx +++ b/vibn-frontend/components/vibn-chat/chat-panel.tsx @@ -9,7 +9,7 @@ import React, { type CSSProperties, } from "react"; import Link from "next/link"; -import { ArrowDown, useSession } from "next-auth/react"; +import { useSession } from "next-auth/react"; import { useParams, usePathname } from "next/navigation"; import { MessageSquare, @@ -284,14 +284,11 @@ 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|permission denied|command not found)/.test( + /(econnrefused|enoent|error|failed|traceback|exception|not found|permission denied|cannot)/.test( lower, - ) && !raw.includes("dev_server_logs") && !raw.includes("browser_console") + ) ) { return { ok: false, label: `Failed — ${firstLine(raw)}` }; } @@ -520,21 +517,28 @@ const MessageBubble = React.memo(function MessageBubble({ style={{ width: 24, height: 24, + borderRadius: "50%", + background: "#f4f4f5", // Zinc-100 instead of black display: "flex", alignItems: "center", justifyContent: "center", marginRight: 8, flexShrink: 0, marginTop: 2, + border: "1px solid #e4e4e7", }} > - - - - - - - + + V. + )}
{ - let t = setTimeout(() => { - if (text.length === textLenRef.current && !isStreaming) { - setExpanded(false); - } - }, 1500); - return () => clearTimeout(t); - }, [text, isStreaming]); React.useEffect(() => { - textLenRef.current = text.length; - }, [text]); + // If not streaming, auto-collapse after a short delay so the user isn't stuck with huge thinking blocks + if (!isStreaming) { + const t = setTimeout(() => setExpanded(false), 500); + return () => clearTimeout(t); + } + }, [isStreaming]); + + const proseWrap: React.CSSProperties = { + overflowWrap: "anywhere", + wordBreak: "break-word", + minWidth: 0, + }; return (
- {expanded && (
` : ""), }} @@ -1139,13 +1146,12 @@ export function ChatPanel({ .catch(() => {}); }, [projectId, workspace, status]); const [sending, setSending] = useState(false); + const [showScrollButton, setShowScrollButton] = useState(false); const [currentPhaseLabel, setCurrentPhaseLabel] = useState( null, ); const [showThreads, setShowThreads] = useState(false); const [mcpToken, setMcpToken] = useState(null); - const [showScrollButton, setShowScrollButton] = useState(false); - const scrollContainerRef = useRef(null); const [isChatMinimized, setIsChatMinimized] = useState(false); // Auto-minimize when navigating to dashboard, auto-open when navigating to preview @@ -1629,15 +1635,12 @@ 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 ( - !ev.name?.includes("logs") && - !ev.name?.includes("console") && - (lower.includes("econnrefused") || - lower.includes("enoent") || - lower.includes("permission denied") || - lower.includes("command not found")) + lower.includes("error") || + lower.includes("failed") || + lower.includes("unexpected") || + lower.includes("not found") ) { isToolErr = true; } @@ -1974,6 +1977,11 @@ export function ChatPanel({ {/* Messages */}
{ + const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; + const distanceToBottom = scrollHeight - scrollTop - clientHeight; + setShowScrollButton(distanceToBottom > 150); + }} style={{ flex: 1, minWidth: 0, @@ -2203,6 +2211,35 @@ export function ChatPanel({ + {/* Scroll to bottom button */} + {showScrollButton && ( + + )} + {(selectToggle) => (
void, visible: boolean }) { - if (!visible) return null; - return ( - - ); -}