From 67c43028dd1f6fa36d01667343cea6df9980dd38 Mon Sep 17 00:00:00 2001 From: mawkone Date: Fri, 15 May 2026 16:16:45 -0700 Subject: [PATCH] feat(ai): inject dynamic codebase summary into system prompt to eliminate blind structure searches --- app/api/chat/route.ts | 27 +++++++---- lib/ai/project-context/codebase-summary.ts | 56 ++++++++++++++++++++++ 2 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 lib/ai/project-context/codebase-summary.ts diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 67f0d9a3..ff6b8c34 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -30,6 +30,7 @@ import { commitAndPushIfDirty, } from "@/lib/dev-container-git"; import { buildDesignKitPromptSection } from "@/lib/design-kits/for-ai"; +import { buildCodebaseSummary } from "@/lib/ai/project-context/codebase-summary"; import type { ChatMessage, ToolCall } from "@/lib/ai/gemini-chat"; // Path B chains routinely fire 7-10 tool calls in one user turn. 18 @@ -69,11 +70,11 @@ async function ensureChatTables() { chatTablesReady = true; } -export function buildSystemPrompt( +export async function buildSystemPrompt( projects: any[], workspace: string, activeProject?: any, -): string { +): Promise { const projectsText = projects.length ? projects .map( @@ -124,6 +125,10 @@ export function buildSystemPrompt( const designKitBlock = buildDesignKitPromptSection(activeProject); + const codebaseBlock = activeProject?.slug + ? await buildCodebaseSummary(activeProject.id, activeProject.slug) + : ""; + const activeBlock = activeProject ? `\n## ACTIVE PROJECT — assume this for every tool call unless the user explicitly says otherwise @@ -134,7 +139,7 @@ The user is currently looking at: - Audience: ${activeProject.audience ?? "unspecified"} - Vision: ${activeProject.productVision ? activeProject.productVision.slice(0, 1500) : "(not yet captured)"} ${activeProject.kickoff ? `- Created via: ${activeProject.kickoff.mode} (${JSON.stringify(activeProject.kickoff.sourceData).slice(0, 200)})` : ""} -${decisionsBlock}${tasksBlock}${ideasBlock}${designKitBlock ? `\n${designKitBlock}\n` : ""} +${decisionsBlock}${tasksBlock}${ideasBlock}${designKitBlock ? `\n${designKitBlock}\n` : ""}${codebaseBlock} When you call tools that take a \`projectId\`, USE this id (\`${activeProject.id}\`) without asking. When the user says "this project" / "the app" / "deploy it" — they mean THIS project. Switch to a different project only if the user names one explicitly. **Project repo is auto-cloned at \`/workspace/${activeProject.slug ?? ""}/\` inside the dev container.** That path is the project's Gitea repo. ALL code, docs, configs, and other artifacts you intend the user to see in the Product tab MUST live under that path. Anything you write outside it (e.g. \`/workspace/scratch\`, \`/workspace/some-cloned-other-repo\`) is treated as scratch and is invisible in the UI. @@ -200,11 +205,11 @@ Each project has a persistent \`vibn-dev\` container. Edit files via \`fs_*\` an **Iterate:**\n- \`shell_exec { projectId, command }\` — anything: \`ls\`, \`npm install\`, \`npm test\`, \`npx create-next-app .\`, \`git status\`. Cwd defaults to \`/workspace\`. Node (LTS), Python 3.12, and Go 1.23 are pre-installed — no setup needed.\n- \`fs_read\` / \`fs_write\` / \`fs_edit { path, oldString, newString }\` (include 2–3 lines of context in \`oldString\` for uniqueness; fails fast if missing or non-unique).\n- \`fs_glob\` / \`fs_grep\` (ripgrep, respects .gitignore) / \`fs_list\` / \`fs_delete\`.\n -**Dev servers (preview URL via `*.preview.vibnai.com` wildcard):** -- `dev_server_start { projectId, command, port: 3000 }` is a **one-shot** call. It kills old processes on the port, checks the port is free, sets HOST=0.0.0.0 + PORT, launches your command, and returns a clickable `previewUrl`. Do NOT pre-flight with `devcontainer_status`, `fs_list`, `dev_server_logs`, or manual `shell_exec` kills — the function handles all of that. Just call it. The error tells you what to fix: `PORT_BUSY` → pick 3001–3009; `npm: command not found` → project needs `npm install` first. -- **Port:** The primary frontend service MUST ALWAYS be bound to port `3000`. Do not use any other port for the user-facing UI. If you are spinning up secondary services (like an API or Storybook) alongside it, you may bind them to ports `3001–3009`, but port `3000` is reserved exclusively for the primary visual preview. -- **Directory:** The command runs from the root `/workspace` directory, but your project code is inside `/workspace/${activeProject.slug ?? ""}/`. You MUST `cd` into your project folder first! Example: `command: "cd ${activeProject.slug ?? ""} && npm run dev"`. -- `dev_server_stop` / `dev_server_list` / `dev_server_logs` — use only AFTER a failed start, and only to diagnose the error the function returned. Never on success. +**Dev servers (preview URL via \`*.preview.vibnai.com\` wildcard):** +- \`dev_server_start { projectId, command, port: 3000 }\` is a **one-shot** call. It kills old processes on the port, checks the port is free, sets HOST=0.0.0.0 + PORT, launches your command, and returns a clickable \`previewUrl\`. Do NOT pre-flight with \`devcontainer_status\`, \`fs_list\`, \`dev_server_logs\`, or manual \`shell_exec\` kills — the function handles all of that. Just call it. The error tells you what to fix: \`PORT_BUSY\` → pick 3001–3009; \`npm: command not found\` → project needs \`npm install\` first. +- **Port:** The primary frontend service MUST ALWAYS be bound to port \`3000\`. Do not use any other port for the user-facing UI. If you are spinning up secondary services (like an API or Storybook) alongside it, you may bind them to ports \`3001–3009\`, but port \`3000\` is reserved exclusively for the primary visual preview. +- **Directory:** The command runs from the root \`/workspace\` directory, but your project code is inside \`/workspace/${activeProject.slug ?? ""}/\`. You MUST \`cd\` into your project folder first! Example: \`command: "cd ${activeProject.slug ?? ""} && npm run dev"\`. +- \`dev_server_stop\` / \`dev_server_list\` / \`dev_server_logs\` — use only AFTER a failed start, and only to diagnose the error the function returned. Never on success. **HMR through the proxy (apply when scaffolding):** - **Vite (verified working):** in \`vite.config\` set \`server: { host: '0.0.0.0', port: <3000-3009>, strictPort: true, hmr: { clientPort: 443, protocol: 'wss', host: '' } }\`. The \`hmr.host\` is REQUIRED — without it Vite's HMR client can guess the wrong host and the WS handshake fails through Traefik. Default localhost binding looks fine locally but breaks HMR through the proxy. @@ -375,7 +380,11 @@ export async function POST(request: Request) { } } - let systemPrompt = buildSystemPrompt(projects, workspace, activeProject); + let systemPrompt = await buildSystemPrompt( + projects, + workspace, + activeProject, + ); // Sentry-as-product Stage 4: auto-surface unresolved errors at // chat-turn start. We pull the last 6 hours' unresolved issues diff --git a/lib/ai/project-context/codebase-summary.ts b/lib/ai/project-context/codebase-summary.ts new file mode 100644 index 00000000..6cb2d1d4 --- /dev/null +++ b/lib/ai/project-context/codebase-summary.ts @@ -0,0 +1,56 @@ +import { NextResponse } from 'next/server'; +import { query } from '@/lib/db-postgres'; +import { execInDevContainer } from '@/lib/dev-container'; + +/** + * Builds a fast, high-level summary of the active project's codebase + * to inject into the system prompt. This prevents the AI from having + * to blind-search the repo to figure out the tech stack on every turn. + */ +export async function buildCodebaseSummary(projectId: string, projectSlug: string): Promise { + if (!projectId || !projectSlug) return ""; + + try { + // We run a fast bash script inside the dev container that finds package.json, + // checks for Prisma/Drizzle schemas, and lists the root folders. + // Time to execute: ~50ms. + const bashScript = ` + cd /workspace/${projectSlug} 2>/dev/null || exit 0 + + echo "=== DEPENDENCIES ===" + if [ -f package.json ]; then + node -e "const pkg=require('./package.json'); console.log('Dependencies:', Object.keys(pkg.dependencies||{}).join(', ')); console.log('DevDependencies:', Object.keys(pkg.devDependencies||{}).join(', '));" 2>/dev/null || echo "Found package.json" + else + echo "No package.json found" + fi + + echo -e "\n=== ARCHITECTURE ===" + if [ -d src/app ] || [ -d app ]; then echo "- Next.js App Router"; fi + if [ -f prisma/schema.prisma ]; then echo "- Prisma ORM (prisma/schema.prisma)"; fi + if [ -f drizzle.config.ts ]; then echo "- Drizzle ORM"; fi + if [ -d .svelte-kit ]; then echo "- SvelteKit"; fi + if [ -f vite.config.ts ] || [ -f vite.config.js ]; then echo "- Vite SPA"; fi + if [ -f docker-compose.yml ]; then echo "- Docker Compose deployed"; fi + + echo -e "\n=== ROOT STRUCTURE ===" + ls -la | awk '{print $9}' | grep -v "^$" | grep -v "^.$" | grep -v "^..$" | head -n 15 | tr '\n' ', ' + `; + + const result = await execInDevContainer(projectId, bashScript); + + if (result.code !== 0 || !result.stdout.trim()) { + return ""; + } + + return `\n## CODEBASE SUMMARY (Auto-detected) +This is a quick summary of what currently exists in \`/workspace/${projectSlug}/\`: +\`\`\`text +${result.stdout.trim().slice(0, 1000)} +\`\`\` +Use this to orient yourself. Do not guess the stack; if it says Next.js and Prisma, use Next.js and Prisma.`; + + } catch (error) { + console.warn("[Codebase Summary] Failed to generate summary:", error); + return ""; + } +}