feat(refactor): premium zed-style chat UI, collapsible reasoning, and comprehensive strict type sweeps

This commit is contained in:
2026-05-21 17:05:42 -07:00
parent 180aa9b311
commit 8049a7f1ab
35 changed files with 5144 additions and 5789 deletions

View File

@@ -1,10 +1,15 @@
import { GoogleGenAI } from '@google/genai';
import { GoogleGenAI } from "@google/genai";
const GEMINI_API_KEY = process.env.GOOGLE_API_KEY || "";
const GEMINI_MODEL = process.env.VIBN_CHAT_MODEL || "gemini-3.1-pro-preview";
// Add a clear visual log so we always know exactly which model is active in the terminal
console.log(`[GeminiChat] Initialized — Model: ${GEMINI_MODEL}`);
if (!GEMINI_API_KEY) {
console.warn(`[GeminiChat] WARNING: GOOGLE_API_KEY is not set. Chat stream will fail with 403 Forbidden.`);
console.warn(
`[GeminiChat] WARNING: GOOGLE_API_KEY is not set. Chat stream will fail with 403 Forbidden.`,
);
}
const ai = new GoogleGenAI({ apiKey: GEMINI_API_KEY });
@@ -38,18 +43,24 @@ export interface ChatChunk {
error?: string;
}
type GeminiPart = Record<string, unknown>;
type GeminiContent = {
role: string;
parts: GeminiPart[];
};
function toGeminiContents(messages: ChatMessage[]) {
const contents: any[] = [];
const contents: GeminiContent[] = [];
for (const msg of messages) {
if (msg.role === "user") {
contents.push({ role: "user", parts: [{ text: msg.content }] });
} else if (msg.role === "assistant") {
const parts: any[] = [];
const parts: GeminiPart[] = [];
if (msg.content) parts.push({ text: msg.content });
if (msg.toolCalls?.length) {
for (const tc of msg.toolCalls) {
const part: any = {
const part: GeminiPart = {
functionCall: { name: tc.name, args: tc.args },
};
if (tc.thoughtSignature) {
@@ -84,7 +95,7 @@ function toGeminiFunctions(tools: ToolDefinition[]) {
functionDeclarations: tools.map((t) => ({
name: t.name,
description: t.description,
parameters: t.parameters as any,
parameters: t.parameters as Record<string, unknown>,
})),
},
];
@@ -104,16 +115,14 @@ export async function callGeminiChat(opts: {
error?: string;
}> {
try {
const config: any = {
const config: Record<string, unknown> = {
temperature: opts.temperature ?? 0.7,
maxOutputTokens: 8192,
};
if (opts.systemPrompt) {
config.systemInstruction = opts.systemPrompt;
}
if (opts.systemPrompt) {
config.systemInstruction = opts.systemPrompt;
}
const fns = toGeminiFunctions(opts.tools ?? []);
if (fns) config.tools = fns;
@@ -121,37 +130,37 @@ export async function callGeminiChat(opts: {
const response = await ai.models.generateContent({
model: GEMINI_MODEL,
contents: toGeminiContents(opts.messages),
config
config,
});
let text = "";
let thoughts = "";
const toolCalls: ToolCall[] = [];
const parts = response.candidates?.[0]?.content?.parts ?? [];
for (const part of parts) {
if (part.text) {
if ((part as any).thought) thoughts += part.text;
if ((part as { thought?: unknown }).thought) thoughts += part.text;
else text += part.text;
}
if (part.functionCall) {
toolCalls.push({
id: `tc-${Date.now()}-${Math.random().toString(36).slice(2)}`,
name: part.functionCall.name,
args: part.functionCall.args as Record<string, unknown> ?? {},
thoughtSignature: (part as any).thoughtSignature,
args: (part.functionCall.args as Record<string, unknown>) ?? {},
thoughtSignature: (part as { thoughtSignature?: string })
.thoughtSignature,
});
}
}
return {
text,
thoughts,
toolCalls,
finishReason: response.candidates?.[0]?.finishReason
return {
text,
thoughts,
toolCalls,
finishReason: response.candidates?.[0]?.finishReason,
};
} catch (error) {
return {
text: "",
@@ -169,14 +178,13 @@ export async function* streamGeminiChat(opts: {
temperature?: number;
}): AsyncGenerator<ChatChunk> {
try {
const config: any = {
const config: Record<string, unknown> = {
temperature: opts.temperature ?? 0.7,
maxOutputTokens: 8192,
};
if (opts.systemPrompt) {
config.systemInstruction = opts.systemPrompt;
config.systemInstruction = opts.systemPrompt;
}
const fns = toGeminiFunctions(opts.tools ?? []);
@@ -185,14 +193,14 @@ export async function* streamGeminiChat(opts: {
const streamResult = await ai.models.generateContentStream({
model: GEMINI_MODEL,
contents: toGeminiContents(opts.messages),
config
config,
});
for await (const chunk of streamResult) {
const parts = chunk.candidates?.[0]?.content?.parts ?? [];
for (const part of parts) {
if (part.text) {
yield (part as any).thought
yield (part as { thought?: unknown }).thought
? { type: "thinking", text: part.text }
: { type: "text", text: part.text };
}
@@ -200,7 +208,6 @@ export async function* streamGeminiChat(opts: {
}
yield { type: "done" };
} catch (error) {
yield {
type: "error",

View File

@@ -289,17 +289,24 @@ export async function callOpenAiCompatibleChat(opts: {
if (tools?.length) body.tools = tools;
// ── Request logging (DeepSeek 400 debug) ──────────────────────────────
const msgSummary = oaiMessages.map((m: any) => ({
role: m.role,
has_tool_calls:
m.role === "assistant" ? Boolean(m.tool_calls?.length) : undefined,
tool_calls_ids:
m.role === "assistant" && m.tool_calls?.length
? m.tool_calls.map((tc: any) => tc.id)
: undefined,
tool_call_id: m.role === "tool" ? m.tool_call_id : undefined,
content_len: typeof m.content === "string" ? m.content.length : 0,
}));
const msgSummary = oaiMessages.map(
(m: {
role: string;
content?: unknown;
tool_calls?: Array<{ id: string }>;
tool_call_id?: string;
}) => ({
role: m.role,
has_tool_calls:
m.role === "assistant" ? Boolean(m.tool_calls?.length) : undefined,
tool_calls_ids:
m.role === "assistant" && m.tool_calls?.length
? m.tool_calls.map((tc: { id: string }) => tc.id)
: undefined,
tool_call_id: m.role === "tool" ? m.tool_call_id : undefined,
content_len: typeof m.content === "string" ? m.content.length : 0,
}),
);
console.error(
"[deepseek] request",
JSON.stringify({
@@ -309,7 +316,7 @@ export async function callOpenAiCompatibleChat(opts: {
has_tools: Boolean(tools?.length),
tool_count: tools?.length ?? 0,
msg_summary: msgSummary,
last_5_roles: msgSummary.slice(-5).map((m: any) => m.role),
last_5_roles: msgSummary.slice(-5).map((m: { role: string }) => m.role),
}),
);
// ───────────────────────────────────────────────────────────────────────

View File

@@ -1675,28 +1675,35 @@ After this returns, ALWAYS call apps_deploy { uuid } to regenerate the live Trae
},
},
{
name: "plan_decision_log",
name: "plan_document_update",
description:
"Log a decision the user has made. Call this PROACTIVELY whenever a non-trivial choice gets settled in conversation (database engine, auth approach, framework, pricing model, copy, branding…) — so it shows up in the Plan tab and you stop re-asking it next session. Don't ask permission; log it and move on.",
"Overwrite the content of one of the Blueprint documents in the Plan tab. These documents define the specifications for the product. ALWAYS use this instead of `fs_write` when a user asks you to update the PRD, Spec, or Architecture plan.",
parameters: {
type: "OBJECT",
properties: {
projectId: { type: "STRING", description: "The Vibn project ID." },
title: {
projectId: { type: "STRING" },
docId: {
type: "STRING",
description: "The specific document to update.",
enum: [
"stories",
"acceptance",
"success",
"ui_design",
"tech_context",
"data_model",
"file_structure",
"tasks",
"checklist",
],
},
content: {
type: "STRING",
description:
'Short topic of the decision (e.g. "Database engine", "Auth provider").',
},
choice: {
type: "STRING",
description: 'What was chosen (e.g. "Postgres", "Stripe Checkout").',
},
why: {
type: "STRING",
description: "Optional 1-2 sentence reasoning. Strongly recommended.",
"The full markdown content for the document. This will completely overwrite the existing document.",
},
},
required: ["projectId", "title", "choice"],
required: ["projectId", "docId", "content"],
},
},
{

View File

@@ -0,0 +1,81 @@
import Mailgun from "mailgun.js";
import formData from "form-data";
import mjml2html from "mjml";
const mailgun = new Mailgun(formData);
const API_KEY = process.env.MAILGUN_API_KEY || "";
const DOMAIN = process.env.MAILGUN_DOMAIN || "";
const API_URL = process.env.MAILGUN_API_URL || "https://api.mailgun.net";
// Initialize the Mailgun client
const mg = mailgun.client({
username: "api",
key: API_KEY,
url: API_URL,
});
export interface SendEmailOptions {
to: string | string[];
subject: string;
text: string;
html?: string;
mjml?: string; // Optional: If provided, this will be compiled to HTML and override the `html` field.
from?: string; // Optional: If not provided, defaults to the Mailgun system domain
}
/**
* Sends an email using the Mailgun API.
* If `mjml` is provided, it compiles it to responsive HTML before sending.
*/
export async function sendEmail(options: SendEmailOptions) {
// Use the provided from address, or fallback to a default on the live domain
const fromAddress = options.from || `Vibn AI <mailgun@${DOMAIN}>`;
// Format the 'to' address
const toAddresses = Array.isArray(options.to)
? options.to.join(",")
: options.to;
// Process MJML if it exists
let finalHtml = options.html;
if (options.mjml) {
try {
const compiled = mjml2html(options.mjml, {
validationLevel: "soft", // Warn on errors but try to compile anyway
minify: true,
});
if (compiled.errors && compiled.errors.length > 0) {
console.warn("[MJML Compiler Warnings]:", compiled.errors);
}
finalHtml = compiled.html;
} catch (e) {
console.error("[MJML Compiler Error]:", e);
// Fallback to plain text if MJML fails catastrophically
finalHtml = undefined;
}
}
try {
const response = await mg.messages.create(DOMAIN, {
from: fromAddress,
to: toAddresses,
subject: options.subject,
text: options.text,
html: finalHtml,
});
return {
success: true,
id: response.id,
message: response.message,
};
} catch (error: any) {
console.error("[Mailgun Error]:", error);
return {
success: false,
error: error.message || "Failed to send email via Mailgun",
status: error.status,
};
}
}