diff --git a/vibn-agent-runner/src/tools/vibn-tools.ts b/vibn-agent-runner/src/tools/vibn-tools.ts index b03e6144..52456a37 100644 --- a/vibn-agent-runner/src/tools/vibn-tools.ts +++ b/vibn-agent-runner/src/tools/vibn-tools.ts @@ -831,6 +831,17 @@ After this returns, ALWAYS call apps_deploy { uuid } to regenerate the live Trae // ── Databases ───────────────────────────────────────────────────────────── + { + name: "workspace_db_query", + description: "Run a read-only SQL query against the workspace's main production/telemetry database (the one powering Next.js + Telemetry). ONLY USE THIS IF THE USER ASKS FOR LOGS OR TELEMETRY USAGE DATA.", + parameters: { + type: "OBJECT", + properties: { + sql: { type: "STRING", description: "The SQL query to execute." }, + }, + required: ["sql"], + }, + }, { name: "databases_list", description: @@ -896,6 +907,30 @@ After this returns, ALWAYS call apps_deploy { uuid } to regenerate the live Trae required: ["uuid", "patchJson"], }, }, + { + name: "databases_logs", + description: "Get runtime logs from a running database cluster. Use this to diagnose database crashes or connection issues.", + parameters: { + type: "OBJECT", + properties: { + uuid: { type: "STRING", description: "The UUID of the database cluster." }, + lines: { type: "INTEGER", description: "Number of lines to fetch. Default is 100." }, + }, + required: ["uuid"], + }, + }, + { + name: "deployments_logs", + description: "Get build/deployment logs for a specific application deployment (e.g. Next.js build errors).", + parameters: { + type: "OBJECT", + properties: { + uuid: { type: "STRING", description: "The UUID of the deployment." }, + lines: { type: "INTEGER", description: "Number of lines to fetch. Default is 100." }, + }, + required: ["uuid"], + }, + }, { name: "databases_delete", description: diff --git a/vibn-frontend/app/api/mcp/route.ts b/vibn-frontend/app/api/mcp/route.ts index 20498662..a0cc27d8 100644 --- a/vibn-frontend/app/api/mcp/route.ts +++ b/vibn-frontend/app/api/mcp/route.ts @@ -192,6 +192,7 @@ export async function GET() { "apps.envs.list", "apps.envs.upsert", "apps.envs.delete", + "workspace.db_query", "databases.list", "databases.create", "databases.get", @@ -366,6 +367,8 @@ export async function POST(request: Request) { case "apps.templates.search": return await toolAppsTemplatesSearch(params); + case "workspace.db_query": + return await toolWorkspaceDbQuery(principal, params); case "databases.list": return await toolDatabasesList(principal); case "databases.create": @@ -3255,6 +3258,41 @@ const DB_TYPES: readonly CoolifyDatabaseType[] = [ "clickhouse", ]; + +import { query } from "@/lib/db-postgres"; + +async function toolWorkspaceDbQuery( + principal: Principal, + params: Record +) { + const { sql } = params; + if (typeof sql !== "string") { + return NextResponse.json({ error: "Missing 'sql' string parameter" }, { status: 400 }); + } + + // Safety check: Prevent modifying the database + const upperSql = sql.toUpperCase().trim(); + if ( + upperSql.startsWith("INSERT") || + upperSql.startsWith("UPDATE") || + upperSql.startsWith("DELETE") || + upperSql.startsWith("DROP") || + upperSql.startsWith("ALTER") || + upperSql.startsWith("TRUNCATE") || + upperSql.startsWith("GRANT") || + upperSql.startsWith("REVOKE") + ) { + return NextResponse.json({ error: "Only SELECT queries are allowed." }, { status: 403 }); + } + + try { + const result = await query(sql, []); + return NextResponse.json({ result: result.slice(0, 100) }); // Limit to 100 rows for safety + } catch (err) { + return NextResponse.json({ error: String(err) }, { status: 500 }); + } +} + async function toolDatabasesList(principal: Principal) { const projectUuid = requireCoolifyProject(principal); if (projectUuid instanceof NextResponse) return projectUuid; diff --git a/vibn-frontend/components/vibn-chat/chat-panel.tsx b/vibn-frontend/components/vibn-chat/chat-panel.tsx index e10b2862..c0eb0aef 100644 --- a/vibn-frontend/components/vibn-chat/chat-panel.tsx +++ b/vibn-frontend/components/vibn-chat/chat-panel.tsx @@ -640,7 +640,7 @@ function Timeline({ entries, isActiveStream }: { entries: TimelineEntry[], isAct {items.map((item, i) => { const isLast = i === items.length - 1; if (item.kind === "thought") { - return ; + return ; } if (item.kind === "text") { return ; @@ -665,92 +665,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 TimelineThought({ text, isStreaming }: { text: string; isStreaming?: boolean }) { - const [expanded, setExpanded] = React.useState(true); - const textLenRef = React.useRef(text.length); - - React.useEffect(() => { - // 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 && ( -
- ` : ""), - }} - /> -
- )} -
- ); -} - function TimelineText({ text, isStreaming }: { text: string; isStreaming?: boolean }) { const proseWrap: React.CSSProperties = { overflowWrap: "anywhere", @@ -1146,7 +1060,6 @@ export function ChatPanel({ .catch(() => {}); }, [projectId, workspace, status]); const [sending, setSending] = useState(false); - const [showScrollButton, setShowScrollButton] = useState(false); const [currentPhaseLabel, setCurrentPhaseLabel] = useState( null, ); @@ -1977,11 +1890,6 @@ export function ChatPanel({ {/* Messages */}
{ - const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; - const distanceToBottom = scrollHeight - scrollTop - clientHeight; - setShowScrollButton(distanceToBottom > 150); - }} style={{ flex: 1, minWidth: 0, @@ -2211,35 +2119,6 @@ export function ChatPanel({ - {/* Scroll to bottom button */} - {showScrollButton && ( - - )} - {(selectToggle) => (