Commit Graph

29 Commits

Author SHA1 Message Date
3d1a0e00c7 fix(chat): render multi-round assistant turns as separate bubbles
Smoke test surfaced a UX bug: when the model fired multiple tool
rounds with interleaved text, the client concatenated every text
SSE event into one growing assistantContent string and rendered it
as a single chat bubble. Result: 'now.Spinning up...first boot...
The dev container is ready!' — three distinct narrative beats
mashed into one wall of run-on text with no visual breaks.

Server (app/api/chat/route.ts):
- Added assistantTextSegments[] alongside the legacy assistantText.
  Each non-empty resp.text per round pushes one segment.
- assistantText is still produced (joined with blank lines) for
  backward compat — old consumers still get a single-string content.
- finalMsg now persists textSegments[] so reloaded threads can
  reconstruct per-round segmentation.
- Stop-marker / round-cap recovery / loop-break paths all push to
  segments AND content, with the leading '\n\n' stripped from the
  segment form so bubble joins look clean.

Client (components/vibn-chat/chat-panel.tsx):
- TimelineEntry gains a 'text' kind.
- text SSE events push a new TimelineEntry instead of growing a
  single content string. Subsequent tool/thought events land in
  between, so the renderer naturally groups text-tools-text-tools.
- New TimelineText component renders each segment as its own bubble
  inline with thoughts and tool pills.
- MessageBubble's bottom content slot is now skipped for assistant
  messages whose timeline has any text entries, so we don't
  duplicate the prose below the timeline.
- loadThread() rehydrates timeline from persisted textSegments +
  toolCalls so reload preserves bubble segmentation.

Backwards compat: messages without textSegments fall through to the
old single-bubble content rendering — no migration needed for
existing chat history.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 10:44:27 -07:00
836733536e feat(devcontainer): auto-clone Gitea repo + auto-commit on each AI turn
The smoke test caught the biggest beta-blocker yet: everything the AI
writes inside the dev container was invisible in the UI because the
Product/Hosting/Infrastructure tabs all read from Gitea + Coolify, not
from the dev container's volume. Plan tab worked; nothing else did.

Two-part fix:

1. lib/dev-container-git.ts — new module with two helpers:
   - ensureProjectRepoCloned(): clones the project's Gitea repo into
     /workspace/<slug>/ using the AI's gitea token, embedding the auth
     into the remote URL so subsequent pushes work without prompts.
     Idempotent: tri-state probe handles 'git' (real repo, no-op),
     'dir' (path exists from pre-fix AI work, init in place), and
     'absent' (full clone). Has an empty-repo fallback for fresh Gitea
     repos where 'git clone' warns and produces nothing checked out.
   - commitAndPushIfDirty(): stages all changes under /workspace/<slug>,
     commits with a one-line message + pushes to origin. Bails fast
     with reason='clean' when there's nothing to commit. Never throws.

2. app/api/chat/route.ts wiring:
   - Pre-loop: fire-and-forget ensureProjectRepoCloned so the repo is
     on disk before the AI's first filesystem-mutating tool call.
   - Post-loop: fire-and-forget commitAndPushIfDirty after the assistant
     message is persisted; commit message is the assistant's first
     sentence (≤180 chars) or 'AI checkpoint' fallback.
   - System prompt now tells the AI: project repo is at /workspace/<slug>,
     write everything you want in the UI under that path, and don't
     manually commit (harness handles it).

Cred plumbing: GITEA_API_URL/GITEA_USERNAME/GITEA_API_TOKEN are read
from process.env in the harness; the dev container never sees the
token outside of the embedded URL. Same blast radius as shell.exec.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-01 14:02:16 -07:00
9ddbe5b7d8 feat(sentry-as-product): auto-provision per-project + AI feedback loop
Implements all 4 stages from SENTRY_AS_PRODUCT.md:

Stage 1 — Auto-provision per-project Sentry:
- New module lib/integrations/sentry.ts with idempotent
  ensureSentryProject(): creates Sentry project under shared
  vibnai org, fetches DSN, persists to fs_projects.data.sentry.
- Wired into POST /api/projects/create (provision early so DSN is
  ready before first deploy) and into applyEnvsAndDeploy in MCP
  (lazy retry + env var injection on every apps.create).
- applySentryEnvToCoolifyApp upserts NEXT_PUBLIC_SENTRY_DSN +
  SENTRY_AUTH_TOKEN onto the Coolify app, so the very first build
  inlines the DSN into the client bundle and uploads source maps.

Stage 2 — Bake into scaffolds:
- New module lib/scaffold/sentry-snippets.ts exposes canonical
  Next.js + Vite+React snippets the AI copies verbatim (keeps
  outputs deterministic across chats).
- AI system prompt updated: explicit instructions to wire Sentry
  on every new app, env vars are guaranteed available, project
  Sentry slug comes from projects_get.
- projects.get MCP response now includes `sentry: {slug, dsn,
  provisionedAt}` so the AI can substitute the slug into
  withSentryConfig({ project: <slug> }).

Stage 3 — Expose error feed to the AI:
- Three new MCP tools registered:
    project_recent_errors  — list unresolved issues
    project_error_detail   — stack trace + breadcrumbs + replay url
    project_error_resolve  — mark resolved after a verified fix
- Tenant-safe: each tool re-checks projectId belongs to caller's
  workspace before talking to Sentry.

Stage 4 — Auto-surface at chat-turn start:
- chat/route.ts pulls listRecentSentryIssues for the active
  project (last 6h, count ≥ 2 to skip noise) and appends a
  [PROJECT HEALTH] block to the system prompt. AI decides
  whether to surface a one-liner; if user's message is about a
  broken thing, AI prefers Sentry stack trace over guessing.

End state: a Vibn user's deployed app crashes for a real user →
Sentry captures with source-mapped stack trace + Session Replay →
next AI chat turn the AI knows about it and can offer a fix
without the user pasting the error.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-01 12:52:17 -07:00
c105b42d0c feat(ai): tool-error recovery middleware
Pattern-matches known-recoverable MCP tool failures and injects a
synthetic imperative message into the conversation right after the
failing tool result. Static prompt rules lose to accumulated tool
reality (we've shipped 4 orphan twenty-* services because the model
ignored the "no delete-and-recreate" rule); a fresh role:'user'
message at decision time does not.

Initial rules cover the three highest-confidence Docker failure
patterns: orphan container conflict (use apps_unstick), image pull
denied (use apps_repair), port already allocated (identify holder).
Each rule names the wrong-but-tempting move explicitly.

See AI_HARNESS_GAPS.md §1 for the failure case this addresses.
2026-05-01 11:08:48 -07:00
f7fdc34af1 docs(prompt): tighten Vite HMR config to match verified-working shape
Spike on 2026-05-01 confirmed HMR works end-to-end through Traefik
when ALL of these are set:

  server: {
    host: '0.0.0.0',
    port: <3000-3009>,
    strictPort: true,
    hmr: { clientPort: 443, protocol: 'wss', host: '<previewUrl host>' },
  }

The previous prompt omitted hmr.host, which lets Vite's HMR client
guess the wrong host and silently fail the WS upgrade. Adding the
host explicitly. Verified test: 101 Switching Protocols, vite-hmr
subprotocol negotiated, js-update messages fire within ~1s.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-01 10:25:34 -07:00
b395546529 fix(chat): never end a turn silent + loop detection + status nudge
The big UX failure: model fires 20 tool calls in silence, persists turn
with content_len=0, user has to re-prompt to get any answer. Confirmed
in prod (Dr Dave / "are you able to give me a preview url?" thread).

Five changes:

1. Recovery summary now fires on ANY silent-tool-tray turn end (not just
   MAX_TOOL_ROUNDS): hit the cap, broke a detected loop, OR ended with
   empty assistantText. Previously the recovery was gated to round-cap
   only, so voluntary silent stops slipped through.

2. Recovery summary has a deterministic fallback. If Gemini returns
   empty text on the recovery call, emit a static "ran N tools, didn't
   reach a clean stopping point" message instead of silently swallowing
   the empty string. The user always gets something readable.

3. Loop detection: track tool-call fingerprints (name + first 120
   chars of args) per turn; if the same fingerprint fires 3× within
   the last 8 calls, break the loop and surface to user via recovery
   summary. Kills the dev_server.start → logs → stop → start → ...
   pattern at its root.

4. Status nudge every 4 silent rounds: inject a synthetic system
   instruction telling the model to send a one-liner before any more
   tool calls. The user's only signal of life on long chains.

5. Prompt: soften "don't narrate intent" → "don't narrate SINGLE
   calls; on chains 3+ deep send a one-liner before each batch".
   Adds explicit "never end a turn silent" rule.

Also: error-path now uses safeClose() instead of bare controller.close()
to honor the streamClosed guard like every other close site.

Made-with: Cursor
2026-04-30 23:18:46 -07:00
6586c8ae1d feat(chat): rewrite system prompt — sharper identity, leaner token cost
- Adds high-agency identity framing at the top ("you own the outcome")
- Adds explicit decision defaults (Postgres > Mongo, monoliths > microservices, etc.)
- Adds adaptive-communication rule (uncertain user → narrow choices; experienced user → denser)
- Removes stale instruction "preview URLs land in week 2" (they're live)
- Removes stale instruction "ship tool lands soon" (it shipped weeks ago)
- Tightens prose throughout — keeps every named tool, recipe, and earned-from-pain story
  (orphan-twenty-* recovery, anchor-on-current-state-first, trust idempotency, etc.)
- Drops dead streamGeminiChat import

Made-with: Cursor
2026-04-30 23:10:43 -07:00
60a04e48c1 feat(plan): Objective/Sessions/Tasks tab with markdown + AI scribe
- Objective: full markdown document editor with Write/Preview tabs
- Sessions: project-scoped chat threads with AI-generated summaries
- Tasks: master-detail view with markdown spec, status pills, agent
  delegation placeholder
- Chat threads now scoped per-project and auto-summarised after each
  assistant turn (powers Sessions list)
- AI MCP scribe tools: plan_get / plan_vision_set / plan_idea_add /
  plan_task_add (title + markdown desc) / plan_task_complete /
  plan_decision_log
- Chat panel clears stale project threads when navigating to workspace

Made-with: Cursor
2026-04-30 13:44:50 -07:00
eb4086d296 fix(ai): close remaining duplication + stale-context gaps
Round two of AI-hardening based on what bit us with the twenty-* fan-out:

1. apps_create idempotency now covers ALL four pathways (template /
   image / composeRaw / repo), not just templates. Same dedup-by-name
   check inside the project, same alreadyExisted: true response shape.
   Pass force: true to opt out for legitimate dev/staging duplicates.

2. databases_create gets the same idempotency treatment — and now
   also scopes to the per-project Coolify project when projectId is
   supplied (previously only apps_create did this).

3. New shared helper findExistingResourceByName scans apps + services
   + databases in a project and matches case-insensitively on name.

4. System prompt: three new hard rules teaching the model how to
   handle tool results and anchor on reality:
   - Tool results are authoritative; conversation history is not.
     If a tool contradicts an earlier assertion, discard the
     assertion. Don't keep telling the user it's broken when
     apps_get now says it's healthy.
   - When the user reports an error, FIRST tool call is a
     current-state read (apps_get / databases_get / apps_logs).
     Stop re-debugging problems that were already fixed.
   - Trust idempotency. alreadyExisted means done; don't loop
     trying a different name.

Made-with: Cursor
2026-04-30 11:07:14 -07:00
3d525afdf7 fix(ai): stop the AI from forking duplicate services to escape errors
Three changes that compound to fix the "4 orphan twenty-* services"
problem we just hit:

1. apps_create is now idempotent within a project. If a service from
   the same template already exists in the same Vibn projectId, return
   it with alreadyExisted: true instead of creating a clone. Pass
   { force: true } to opt out for legitimate dev/staging duplicates.

2. New apps_unstick tool. SSH-cleans orphan Docker containers
   matching the resource UUID so a deploy that hit "Conflict.
   The container name X is already in use" can recover without
   deleting the entire service.

3. System prompt hardened with two new hard rules:
   - ALWAYS apps_list before apps_create (idempotency in spirit, not
     just at the API boundary)
   - NEVER delete-and-recreate a service to escape an error. The
     recovery for container conflicts is apps_unstick + apps_deploy.

Already cleaned the 3 duplicate twenty-* services from prod
(kept twenty-live, freshest healthy). Frees ~9 GB RAM on the host.

Made-with: Cursor
2026-04-29 20:27:52 -07:00
14d0b04112 feat(ai): scribe tools — let AI write to the Plan tab
Adds MCP tools so the AI can capture decisions, tasks, ideas, and the
vision in the moment instead of just reading them:

- plan_get             read full plan for context
- plan_vision_set      update vision when user refines their pitch
- plan_decision_log    log a decision PROACTIVELY when one gets settled
                        (no permission ask) so the next session doesn't
                        re-litigate it
- plan_task_add        track multi-step work or user-side follow-ups
- plan_task_complete   mark done as we go
- plan_idea_add        park stray ideas

System prompt is updated with a "be the user's scribe" section that
instructs the model to use these proactively with brief acks instead
of long confirmations.

Also reorders the Plan tab UI to: Vision · Tasks · Decisions · Ideas
(Ideas moved to bottom — it's the lowest-signal pile).

Made-with: Cursor
2026-04-29 20:17:43 -07:00
5ecb0349d7 feat(plan): add Plan tab as the first project surface
A new home for everything that happens BEFORE building:
- Vision    — one-line elevator pitch (mirrors productVision)
- Ideas     — the "park-it" bin for raw thoughts
- Tasks     — what needs to happen next (open / done)
- Decisions — log of "we chose X over Y because Z"

Storage is appended under fs_projects.data.plan so no schema migration
is needed. CRUD lives at /api/projects/[projectId]/plan.

The bare project URL now redirects to /plan instead of /product, and
the AI chat receives decisions + open tasks in its active-project
context block — so it stops re-litigating settled questions and knows
what's queued up.

Made-with: Cursor
2026-04-29 18:02:02 -07:00
b706fa0e89 feat(chat): scope AI conversations to the active project
The chat panel now reads projectId from the URL and tags every thread
to it, so:
- Threads listed in the side panel are filtered to the project the user
  is currently viewing (workspace-level chats still work from /projects).
- New conversations started from a project page are persisted with that
  project_id, surviving page reloads.
- The system prompt prepends an ACTIVE PROJECT block so tool calls
  (apps_create, devcontainer_ensure, etc.) use the right projectId
  without the user having to name it.
- A small chip in the chat header shows which project the AI is
  currently talking about.

Schema migrates idempotently on first request (project_id column +
composite index on fs_chat_threads).

Made-with: Cursor
2026-04-29 17:41:45 -07:00
305516c7e4 feat(chat): rewrite system prompt for voice + proactive instinct
The current prompt reads like a runbook — operationally correct, but
it produces tool-call orchestrators rather than co-founders. Now that
the thinking pill streams reasoning between tool calls, the chat
bubble should be where opinion + judgment + push-back live.

What changed:

1. New "Voice" section right after the role declaration. Tells the
   model to:
   - Stop narrating intent before tool calls (the thinking pill
     already covers this).
   - Pack post-tool summaries with the actual answer + obvious next
     step, not a recap of which tools ran.
   - Have an opinion. Pick Postgres or Mongo, defend in one sentence,
     proceed. Don't bullet pros/cons unless asked.
   - Push back when it matters. Refuse "deploy without backups",
     suggest Pipedream over n8n if it fits better.
   - Surface adjacent risks unprompted (missing env vars, DNS not
     propagated, autosave overdue) — the model is protecting the
     user's work because the user trusts it to.
   - Honest about uncertainty: "I'm not sure but X" beats false
     confidence.
   - Length matches stakes — short for short Qs, paragraph for big
     decisions; never pad, never truncate.
   - Markdown sparingly: backticks always for paths/IDs/URLs;
     headings only when 3+ sections; otherwise prose.

2. Hard rules tightened:
   - "Infer projectId from context, only ask if genuinely ambiguous"
     replaces the rote "ask once, then proceed" — saves a tool round
     and feels less robotic.
   - Added explicit "ship/apps.deploy result is authoritative — don't
     verify with gitea_* or shell_exec" rule. Reinforces the fix from
     a896d07 at the prompt level so even older Gemini instances pick
     it up.
   - Added "don't loop blindly on tool errors" — if shell_exec fails
     twice, surface and ask. Prevents the 12-tool retry chains from
     earlier.
   - Removed redundant "be concise" + "summarize after every tool
     call" — both are now subsumed by the Voice section's richer
     guidance.

Operational middle (Vibn structure, deploy recipes, dev container
workflow, port slot rules, HMR config, troubleshooting) is unchanged.
Those are the guard rails that make Path B work.

Net length: +650 chars on a ~8k-char prompt. Worth it for the voice
shift.

Made-with: Cursor
2026-04-28 15:35:24 -07:00
4184baca77 feat(chat): expose Gemini's reasoning narration as a thinking pill
Today the chat shows ✓-icon tool trays with no narration between
calls — the user has no idea WHY the AI just called fs_edit or
ship. Meanwhile Gemini is producing 500-1000 chars of first-person
reasoning per round ("Updating the Express Server: A Quick
Production Deployment / Right, so we have a basic Express server
here, nothing fancy. I need to get a new version live...") and
billing us for those tokens — we just weren't asking for them.

Three layers:

1. lib/ai/gemini-chat.ts
   - generationConfig.thinkingConfig.includeThoughts = true (default
     true, opt-out via includeThoughts: false). We're already paying
     for thinking tokens regardless of this flag — it just controls
     whether the model returns the human-readable summary or only the
     compressed signature.
   - callGeminiChat now returns { text, thoughts, toolCalls,
     finishReason } and the parser splits parts by `part.thought`.
     CRITICAL bug avoided: previously `if (part.text) text += ...`
     would have lumped thoughts into the chat bubble verbatim.
   - streamGeminiChat yields `{ type: 'thinking' }` for thought parts.

2. app/api/chat/route.ts
   - New SSE event: `data: {"type":"thinking","text":"..."}`
   - Emitted on every round alongside text + tool_start.
   - Recovery-summary branch also emits thoughts so even when the
     model produces no user-facing prose, the user sees the model's
     reasoning instead of dead silence.

3. components/vibn-chat/chat-panel.tsx
   - Message gains optional `thoughts` field (in-memory only — we do
     NOT persist thoughts to fs_chat_messages; they're ephemeral and
     cheap to drop).
   - New ThinkingBubble component: dashed-border italic pill above
     the assistant bubble, collapsed by default to show one-line
     preview, click to expand for full chain. Strips Gemini's
     "**Section Heading**" prefixes from the preview.
   - SSE handler accumulates thinking chunks onto the in-flight
     assistant message.

UX impact: instead of staring at fs.read ✓ fs.edit ✓ ship ✓ icons,
the user sees "Examining the target server file..." → "Shipping the
twenty-crm project..." in real time. Costs zero additional tokens
(we already paid for the thoughts).

Cleanup: removed scripts/probe-gemini-raw.ts and
scripts/probe-recovery-summary.ts — diagnostic scripts that
identified this opportunity, no longer needed in-tree.

Made-with: Cursor
2026-04-28 15:24:49 -07:00
4f84a19e75 feat(chat): Stop button to cancel in-flight AI turns
Standard chat-app pattern: while the AI is streaming or running
tools, the Send button morphs into a Stop control (filled square
inside a faded spinner). Click it (or press Esc) to abort the turn.

Why: with MAX_TOOL_ROUNDS=18, a confused tool-loop can chew through
60-90s of compute and tokens. The user had no way to interrupt — they
just watched ✓ icons accumulate. Stop fixes that.

How:

Client (chat-panel.tsx):
- abortRef holds the in-flight AbortController; lives in a ref so the
  Stop button can reach it without re-rendering on every chunk.
- sendMessage creates a fresh controller and passes signal to fetch.
- cancelMessage calls .abort(); also bound to Escape while sending.
- Button morph: while `sending`, render lucide Square overlaid on a
  faded Loader2 spin, switch onClick to cancelMessage, swap aria/title
  to "Stop generating (Esc)".
- Catch DOMException AbortError separately from network errors and
  append "(stopped by user)" to the partial assistant message.
- Textarea no longer disabled during streaming so users can queue
  the next prompt; Enter still won't submit until the turn ends.

Server (app/api/chat/route.ts):
- request.signal is captured before the ReadableStream and an `aborted`
  flag is flipped on the addEventListener('abort', ...) callback.
- Loop checkpoints `aborted` (a) at the top of every round, (b) before
  the inner tool-call loop, (c) before each individual executeMcpTool
  call. Picks the next safe boundary instead of yanking mid-call.
- On abort: emit a "(stopped by user)" text chunk + an "aborted" event,
  skip the round-cap recovery summary (don't pay for tokens the user
  just canceled), persist the partial assistant message normally.
- Fetch errors that come from the abort propagating into Gemini's HTTP
  client are recognized and downgraded from "error" to "aborted".
- safeClose() guards against double controller.close() when the abort
  races with normal completion.

Made-with: Cursor
2026-04-28 14:56:35 -07:00
a897d07179 fix(ship): return commitSha + coolifyDeployUrl, prevent verification chain
After "ship" succeeded the AI was burning 7+ follow-up tool calls
(gitea_repos_list, gitea_credentials, shell.exec×4, apps_list) trying
to verify what actually got pushed and where it deployed. That ate
through MAX_TOOL_ROUNDS and the user got tool-icon spam with no
narrative summary.

Three fixes:

1. ship now returns commitSha (parsed from `git rev-parse HEAD`),
   giteaCommitUrl, giteaBranchUrl, coolifyDeployUrl, coolifyAppUuid,
   and a summaryHint string telling the AI exactly what to say next.
2. ship's tool description now explicitly tells Gemini "do NOT call
   gitea_*, shell_exec, or apps_* afterwards to verify — the result
   is authoritative."
3. MAX_TOOL_ROUNDS 12 → 18 as a safety net for genuinely long chains.

Net effect: ship goes from ~12 tool calls to verify a deploy down to
1 (just ship itself), and the next text turn has the SHA + URL
inline.

Made-with: Cursor
2026-04-28 14:46:18 -07:00
e0844b5f2e feat(path-b): preview-port slots, port-collision, gitea_file_* deprecation
Five focused improvements rolled into one deploy:

1. Pre-allocated preview ports + Traefik labels.
   Bake docker labels for ports 3000-3009 into every dev-container
   compose at ensureDevContainer() time. Each port has its own
   subdomain: preview-<slot>-<projectSlug>-<token>.preview.vibnai.com.
   Token is derived from projectId so URLs are stable across restarts
   but not enumerable across projects. Joins the coolify external
   network so Traefik can reach the container.

   This avoids the runtime compose-mutation approach (which would
   have required a Coolify redeploy on every dev_server.start, ~30s
   latency). The trade-off is a hard cap of 10 concurrent dev servers
   per project — fine for the "frontend + API" scenario, the only one
   we can practically envision.

   Wildcard DNS + Traefik DNS-01 cert remain a manual one-time setup
   (see vibn-dev/PREVIEWS.md).

2. dev_server.start: port-collision handling.
   Detect listeners via `ss` + `lsof` before launching. Three outcomes:
   - port out of slot range → PortOutOfRangeError → 400 with allowedRange
   - port owned by a different process → PortBusyError → 409
   - port owned by a tracked vibn dev server (same project) → kill
     the stale row and reuse the slot (most-recent-write-wins; matches
     AI mental model when it does an edit-restart loop)
   Surfaced via dedicated MCP error codes so the AI can recover
   intelligently instead of looping the same start call.

3. gitea_file_{read,write,delete}: hard-removed from AI tool list.
   These tools competed with fs.* and tempted the AI into the slow
   path. Pulled from VIBN_TOOL_DEFINITIONS but kept in the MCP
   dispatcher for 30 days for any external clients still using them.
   System prompt rewritten to make Path B the only documented way to
   author code; gitea_repo_* + gitea_branches_* remain because they
   handle one-time orchestration with no fs.* equivalent.

4. System prompt: HMR + preview-port discipline.
   New section covering Vite HMR (clientPort:443 wss), Next dev
   (-H 0.0.0.0), and Express (HOST=0.0.0.0). Explicit "ports must be
   3000-3009" rule + "if PORT_BUSY don't blindly retry" guidance.

5. Cron docs (vibn-dev/CRON.md).
   /etc/cron.d/vibn-path-b template + smoke commands for autosave
   and idle-sweep. Wires both 5-minute jobs that already have admin
   endpoints (POST /api/admin/path-b/{autosave,idle-sweep}).

MCP version bump 2.6.0 -> 2.7.0. Smoke test: 65 tool defs (down from
68 after gitea_file_* removal), all accepted by Gemini.

Made-with: Cursor
2026-04-28 14:39:59 -07:00
115cf7eb28 fix(chat): always emit narrative summary, even when tool-round cap is hit
Surfaced by the live Path B test: AI fired 7 tool calls (fs.read,
fs.edit, kill, dev_server.start, curl, dev_server.logs, ...) in a single
turn, the loop exited at MAX_TOOL_ROUNDS, and the user saw only a tray
of ✓ icons — no text reply.

Two changes:

1. Bump MAX_TOOL_ROUNDS 6 → 12. Path B iteration chains routinely run
   long; 6 was tuned for Path A's much-shorter Coolify-orchestration
   sequences.

2. When the loop exits because of the cap (the last assistant turn was
   a tool call, not a finish), force one more no-tools Gemini call
   with an explicit "summarize the result, do NOT call tools" prompt.
   That gives the user a sentence or two of context instead of a wall
   of green checkmarks. Wrapped in try/catch so the stream still
   terminates cleanly if Gemini errors.

Made-with: Cursor
2026-04-28 14:17:40 -07:00
4ba9407534 feat(path-b): persistent dev containers + shell.exec + fs.* tools
Kicks off Path B (AI_PATH_B_EXECUTION_PLAN.md): each Vibn project gets
its own vibn-dev Coolify service that the AI drives directly via shell
and filesystem tools. Sub-second iteration vs the 5-min Gitea redeploy
loop.

What's in this commit (week 1, slice 1):

- vibn-dev Dockerfile: small Ubuntu base (~500 MB target). git, ripgrep,
  python3, mise. Language toolchains lazy-install on first use.
- lib/dev-container.ts: ensureDevContainer / suspend / resume /
  execInDevContainer. Backed by a new fs_project_dev_containers table.
- lib/feature-flags.ts + /api/admin/path-b/{disable,enable}: kill switch.
  Bearer NEXTAUTH_SECRET flips path_b_disabled, propagates in ~10s.
- New MCP tools wired into /api/mcp: devcontainer.{ensure,status,suspend},
  shell.exec, fs.{read,write,edit,list,delete,glob,grep}. All enforce
  workspace isolation via fs_projects ownership check.
- vibn-tools.ts: 11 new Gemini tool defs (smoke test passes, 63 total).
- chat system prompt: shell-first guidance; gitea_file_* marked
  deprecated for iterative work (still available, removed week 3).

Safety nets baked in:
- pathBGuard() returns 503 from every Path B tool when the kill switch
  flips
- fs.* paths locked to /workspace
- ensureResourceInWorkspaceProjects via fs_project_dev_containers PK
- per-project resource limits (1 vCPU, 1 GiB RAM) on the compose spec

Still pending (queued):
- dev_server.* (preview URLs through Traefik)
- ship tool (push to Gitea + trigger prod deploy)
- auto-push autosave to vibn-autosave/main every 5 min
- idle-suspend cron after 30 min inactivity
- HMR-through-Traefik spike
- eval harness

Made-with: Cursor
2026-04-28 12:53:16 -07:00
c8dec7c656 feat(mcp): add gitea_* tools so the AI can write code, not just deploy it
Closes the AI's self-reported gap: "I cannot directly commit or push code".

New MCP capabilities (8) — all scoped to the workspace's Gitea org via
requireGiteaOrg + ensureRepoOwnerInOrg:

- gitea.repos.list           — discover existing repos
- gitea.repo.get             — metadata (default branch, clone URL)
- gitea.repo.create          — mint a new private repo with auto-init
- gitea.file.read            — read a file (or list a directory)
- gitea.file.write           — create/update one file in one commit
- gitea.file.delete          — delete a file (auto-resolves sha)
- gitea.branches.list        — list branches with head sha
- gitea.branch.create        — branch off an existing branch

Wired through:
- lib/gitea.ts: giteaReadFile, giteaListContents, giteaListBranches,
  giteaCreateBranch, giteaListOrgRepos, giteaDeleteFile.
- lib/ai/vibn-tools.ts: 8 new Gemini tool declarations (53 total).
- app/api/chat/route.ts: system prompt now teaches the end-to-end
  scaffold-then-deploy recipe so the AI stops deferring to the user.

MCP capability descriptor bumped to version 2.5.0.

Made-with: Cursor
2026-04-28 11:52:16 -07:00
766352ec00 feat(mcp): workspace-set-aware tenant safety + richer chat system prompt
Stage 2 of per-project Coolify isolation:
- Add getApplicationInWorkspace / getDatabaseInWorkspace / getServiceInWorkspace
  helpers that verify a resource belongs to ANY of the workspace's owned
  Coolify projects (legacy workspace project + per-Vibn-project projects).
- Replace all single-resource MCP lookups (apps.get/delete/deploy/exec/logs/
  domains/envs/volumes/repair, databases.*, services) to use the new
  workspace-set-aware variants. Single-resource tools now correctly find
  apps deployed under per-project Coolify namespaces.
- Fix missing queryOne import.

Chat system prompt overhaul:
- Add deployment recipes (third-party app, custom Docker image, database, domain)
- Add troubleshooting playbook (stuck deploys, 502s, tenant errors, repair)
- Restate hard rules: always pass projectId, always search templates first,
  destructive ops require name confirm, surface long-running op timing.

Made-with: Cursor
2026-04-27 19:21:20 -07:00
1a686c2a23 Per-project Coolify project isolation (Stage 1)
Each Vibn project now gets its OWN Coolify project named
vibn-{workspace-slug}-{project-slug}. All apps/databases/services
deployed for the project land inside that Coolify project, giving
us clean grouping, cascading delete, and per-project domain
namespaces.

Changes:
- New lib/projects.ts: ensureProjectCoolifyProject (idempotent
  create/lookup), getProjectCoolifyUuid, getOwnedCoolifyProjectUuids
- /api/projects/create: pre-insert row, mint per-project Coolify
  project, then complete the row with productData (preserves the
  coolifyProjectUuid that was just set)
- apps.list (MCP): without projectId, aggregates across ALL
  workspace-owned Coolify projects; with projectId, scopes to
  that project's Coolify project. Returns coolifyProjectUuid
  on each result so the AI knows where things live.
- apps.create (MCP): accepts projectId; auto-mints the Vibn
  project's Coolify project on first deploy if missing
- apps_list/apps_create tool defs: projectId param surfaced
- System prompt: Project as first-class — planning + live as
  facets of ONE thing, never as separate worlds. AI told to
  always pass projectId on apps_create.

Stage 2 (next): set-aware ensureResourceInProject across all
single-resource MCP tools (apps.get/delete/exec/etc.) and
cascading delete via projects.delete.

Made-with: Cursor
2026-04-27 19:02:43 -07:00
c4ef30066f Expand chat panel to full MCP tool surface (35+ tools)
vibn-tools.ts previously exposed only 12 of the 35+ MCP tools.
Now includes the complete surface from AI_CAPABILITIES.md:
- workspace.describe, gitea.credentials
- apps: get, update, rewire_git, delete, deploy, deployments, exec,
  volumes.list/wipe, containers.up/ps, repair, domains.list/set,
  envs.list/upsert/delete
- databases: list, create, get, update, delete
- auth: list, create, delete
- domains: search, get, attach (+ existing register, list)
- storage: describe, provision, inject_env

Action dispatch simplified: toolName.replace(/_/g, '.') maps any
tool name to the MCP action with no explicit lookup table needed.
System prompt updated to reflect full capability set.

Made-with: Cursor
2026-04-27 17:55:57 -07:00
0cb5d8bc50 Fix AI confusion: clean projects_get output, clarify project vs app in system prompt
projects_get was dumping raw JSONB including turborepo scaffold fields
(product/website/admin/storybook sub-app configs), which Gemini mistook
for live deployed services. Now returns a clean summary with only the
fields relevant to the AI. Also updated the system prompt to explicitly
distinguish Vibn project records (planning artifacts) from Coolify apps
(actual running services), instructing the model to call apps_list when
the user asks what's live.

Made-with: Cursor
2026-04-27 17:37:18 -07:00
7138f86427 Auto-create chat tables on first request (IF NOT EXISTS)
fs_chat_threads and fs_chat_messages were referenced in code but
never added to the migration script. Added ensureChatTables() called
at startup of both /api/chat and /api/chat/threads routes — safe,
idempotent, and runs once per process lifetime. Also backfilled the
SQL migration file for documentation.

Made-with: Cursor
2026-04-27 17:34:30 -07:00
8872ab606b Fix tool calling: use non-streaming generateContent for tool rounds
Gemini 3.1 Pro thinking model requires thought_signature to be echoed
in functionResponse. SSE stream doesn't reliably include it in individual
chunks. Switch tool-calling rounds to non-streaming generateContent which
always returns the complete response with thought_signature present.

Made-with: Cursor
2026-04-27 17:18:34 -07:00
d246cbaf75 Fix Gemini 3.1 Pro thought_signature in tool calls
Thinking models attach a thought_signature to functionCall parts.
Must be echoed back in functionResponse or API returns 400.
Carry it through ToolCall -> ChatMessage -> toGeminiContents().

Made-with: Cursor
2026-04-27 16:37:09 -07:00
5e07bbf39d Add Vibn AI chat panel powered by Gemini 3.1 Pro
- Right-docked chat panel on all workspace pages ([workspace]/layout.tsx)
- Streaming SSE responses with Gemini 3.1 Pro preview via generativelanguage API
- Full tool-calling loop (up to 6 rounds): deploys apps, lists projects, registers
  domains, fetches logs — all via existing MCP dispatcher
- Persistent conversation history: fs_chat_threads + fs_chat_messages tables (Postgres)
- Thread management: create, list, rename (auto-title from first message), delete
- Panel collapses to a tab; open state persisted to localStorage
- Read-only mode hint when no MCP token is present
- Graceful content margin shift when panel is open

Made-with: Cursor
2026-04-27 15:40:32 -07:00