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 (
-
- );
-}