Files
vibn-frontend/app/api/projects/[projectId]/agent-chat/route.ts
Mark Henderson b2b3424b05 fix: clean up orchestrator chat UX
- Tool call names now show human-readable labels ("Dispatched agent"
  instead of "spawn_agent"), deduped if called multiple times
- Model label only shown when a real value is returned; "unknown"
  and null are suppressed; model names shortened (GLM-5, Gemini)

Made-with: Cursor
2026-02-27 18:15:50 -08:00

112 lines
3.5 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/lib/auth/authOptions";
import { query } from "@/lib/db-postgres";
const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? "http://localhost:3333";
export async function POST(
req: NextRequest,
{ params }: { params: Promise<{ projectId: string }> }
) {
const session = await getServerSession(authOptions);
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { projectId } = await params;
const { message } = await req.json();
if (!message?.trim()) {
return NextResponse.json({ error: '"message" is required' }, { status: 400 });
}
// Load project context to inject into the orchestrator session
let projectContext = "";
try {
const rows = await query<{ data: any }>(
`SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`,
[projectId]
);
if (rows.length > 0) {
const p = rows[0].data;
const lines = [
`Project: ${p.productName ?? p.name ?? "Unnamed"}`,
p.productVision ? `Vision: ${p.productVision}` : null,
p.giteaRepo ? `Gitea repo: ${p.giteaRepo}` : null,
p.coolifyAppUuid ? `Coolify app UUID: ${p.coolifyAppUuid}` : null,
p.deploymentUrl ? `Live URL: ${p.deploymentUrl}` : null,
p.theiaWorkspaceUrl ? `IDE: ${p.theiaWorkspaceUrl}` : null,
].filter(Boolean);
projectContext = lines.join("\n");
}
} catch {
// Non-fatal — orchestrator still works without extra context
}
// Use projectId as the session ID so each project has its own conversation
const sessionId = `project_${projectId}`;
// First message in a new session? Prepend project context
const enrichedMessage = projectContext
? `[Project context]\n${projectContext}\n\n[User message]\n${message}`
: message;
try {
const res = await fetch(`${AGENT_RUNNER_URL}/orchestrator/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: enrichedMessage, session_id: sessionId }),
signal: AbortSignal.timeout(120_000), // 2 min — agents can take time
});
if (!res.ok) {
const errText = await res.text();
return NextResponse.json(
{ error: `Agent runner error: ${res.status}${errText.slice(0, 200)}` },
{ status: 502 }
);
}
const data = await res.json();
return NextResponse.json({
reply: data.reply,
reasoning: data.reasoning ?? null,
toolCalls: data.toolCalls ?? [],
turns: data.turns ?? 0,
model: data.model || null,
sessionId,
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return NextResponse.json(
{ error: msg.includes("fetch") ? "Agent runner is offline" : msg },
{ status: 503 }
);
}
}
// Clear session for this project
export async function DELETE(
_req: NextRequest,
{ params }: { params: Promise<{ projectId: string }> }
) {
const session = await getServerSession(authOptions);
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { projectId } = await params;
const sessionId = `project_${projectId}`;
try {
await fetch(`${AGENT_RUNNER_URL}/orchestrator/sessions/${sessionId}`, {
method: "DELETE",
});
} catch {
// Best-effort
}
return NextResponse.json({ cleared: true });
}