Commit Graph

114 Commits

Author SHA1 Message Date
c77f3fbc7f fix(chat-ui): render tool tray inside the bubble, collapse repeats
Two UX wins from this change:

1. Tool pills now render INSIDE the assistant message bubble, ABOVE
   the streaming text. Previously: messages list rendered first, tool
   tray rendered after — so the user saw the summary text mid-screen
   and the tool pills below it, having to scroll UP to read the
   summary they actually cared about. Now the visual flow is:
     [user question] → [tool pills, chronological] → [summary text]
   and the user's eye lands on the summary at the bottom.

2. Adjacent runs of the same tool name collapse into a single pill
   with ×N counter. Click to expand. Kills the "8× shell.exec ✓"
   wall of identical pills the user saw in prod (Dr Dave thread).

Tool events are now scoped to their owning assistant message
(message.toolEvents) rather than a global state — also means
stop/restart can't bleed events across turns.

Made-with: Cursor
2026-04-30 23:21:31 -07:00
8969371134 fix: redirect to /product after project creation instead of /overview
/overview does not exist as a route; the correct landing tab is /product.

Made-with: Cursor
2026-04-30 18:50:55 -07:00
41fbed31f3 fix(prod): ESM transpile, healthcheck, hosting UX, settings, error msgs
- next.config.ts: add react-markdown + entire unified/remark/rehype
  ecosystem to transpilePackages — fixes TypeError 'z'/'j'/'aa' prod
  crashes caused by ESM-only packages not being bundled for webpack
- Dockerfile: bake HEALTHCHECK --start-period=60s on 127.0.0.1 so
  rolling deploys pass on first health probe (was failing on ::1 IPv6)
- Hosting tab: full rewrite — live URL chip, copy button, redeploy
  button, inline log viewer, domain list, empty state with prompt
  nudge. Single-card layout replaces master-detail for 1-3 endpoints.
- Settings page: new /project/:id/settings route with danger zone +
  typed "delete" confirmation for project deletion
- Status pill: "View logs" link appears on build failures
- URL chips: collapse extras into "+N more" pill when >2 visible
- Chat errors: structured "Tool error:" prefix; network errors
  distinguished from server errors

Made-with: Cursor
2026-04-30 17:12:48 -07:00
996b875983 feat(project-header): smart status pill + live/preview URL chips
- Status pill derives richer states (Empty / Deploying / Build failed /
  Down / Live) from anatomy with self-polling while deploying
- Tooltip explains what's happening (last build status, transient
  containers, Coolify build phase)
- New ProjectHeaderUrls component renders clickable chips for live
  domains and active dev-server preview URLs to the left of the pill
- useAnatomy gains pollMs option for client-driven refresh

Made-with: Cursor
2026-04-30 13:44:57 -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
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
90bed6ab31 feat(github): OAuth integration + repo picker for Import flow
User can now click "Connect GitHub" inside the Import-existing-code
flow, sign in via GitHub, and pick a repo from a searchable list of
their own + collaborator + org repos. Both public and private repos
work — the encrypted access token on the user's account is auto-
attached when the create endpoint runs the agent-runner mirror.

OAuth flow:
  - GET  /api/integrations/github/connect    — generates state, sets
         a 10-min httpOnly cookie, 302s to GitHub authorize.
  - GET  /api/integrations/github/callback   — verifies state,
         exchanges code for token, fetches /user, encrypts the
         token with secret-box (AES-256-GCM, VIBN_SECRETS_KEY) and
         persists it on fs_users.data.integrations.github.
         Bounces back to ?gh_connected=login or ?gh_error=msg.
  - GET  /api/integrations/github/repos      — server-side fetches
         the connected user's repos (per_page=100, sort=pushed,
         affiliation=owner+collaborator+org_member). Returns the
         GitHub login + a stripped repo summary; never the token.
  - POST /api/integrations/github/disconnect — drops the integration
         from fs_users (does NOT revoke on github.com).

Scopes requested: repo, read:user.

Token storage:
  - Encrypted at rest with secret-box (lib/auth/secret-box.ts) using
    VIBN_SECRETS_KEY. Tokens never leave the server.
  - One token per fs_users row, keyed by email.

ImportSetup UI:
  - On mount, fires /repos to detect connection state.
  - If connected: shows a connected-as-@login chip with disconnect
    link, a search-as-you-type repo picker (max 220px scroll, badges
    for Private / language), and a "paste a different URL instead"
    escape hatch.
  - If not connected: shows a Connect GitHub card with a public-URL
    fallback inline.
  - On return from OAuth (?gh_connected=… or ?gh_error=…), surfaces
    a toast and silently refreshes the repo list.
  - Selected repo carries default_branch + repo id into the create
    payload so we can store them on the project for later UI hints.

/api/projects/create:
  - When a githubRepoUrl is mirrored, falls back to the user's
    OAuth-linked token if no PAT is explicitly passed. Means the
    flow "just works" for private repos once GitHub is connected.

Required env (already set in production):
  - GITHUB_CLIENT_ID
  - GITHUB_CLIENT_SECRET

Made-with: Cursor
2026-04-29 16:44:13 -07:00
c7bb0eea58 feat(project-creation): replace owner-style picker with audience picker
"Myself / A client" was about who *owns* the project (a billing
concern), but at creation time we want to know who *uses* it — that's
what determines which Infrastructure providers we should pre-stage.

  team       = internal users (your team / employees)
               → SSO-style auth, no payments by default, simple roles
  customers  = external users (the public)
               → public sign-up + payments + transactional email by
                 default, custom domain matters

Both choices are reversible from the Infrastructure tab later — the
selector copy makes that explicit so users don't feel locked in.

Changes: - setup-shared: ForWhomSelector ("Myself" / "A client") replaced by
    AudienceSelector ("My team" / "Customers"), with an "you can
    change this later" hint underneath. New Audience union type
    exported for the three setup screens to share.
  - BuildSetup / OssSetup / ImportSetup: swap state + import + payload.
    Defaults: BuildSetup → customers (most "vibe coder" projects are
    public products), ImportSetup → customers (existing repos usually
    are too), OssSetup → team (Twenty / n8n / Plausible style tools
    are most often deployed for internal use).
  - /api/projects/create: drop isForClient (we never read it
    anywhere), persist audience as a first-class field on the
    project record so the AI can branch on it during the first chat.
Made-with: Cursor
2026-04-29 16:24:54 -07:00
7a9cd68ea8 feat(project-creation): 3-path wizard — Build / OSS / Import
User feedback: the previous flow was a single-screen "name + audience"
dialog that gave AI no context about what the user actually wanted to
make. That worked for the demo but produced messy projects in practice
because everything was decided after the fact in chat.

The new flow asks the user one human question first ("How would you
like to begin?") and then captures the minimum context needed to seed
the AI's first conversation in the project.

Three paths, each is a 2-step setup screen with internal step dots:

  - Build your own idea  — Step 1: name + audience.  Step 2: free-text
    "what do you want to build". Becomes the project's vision and the
    AI's first-message context.

  - Run an open source tool  — Step 1: name + audience.  Step 2:
    segmented tabs to either (a) paste a GitHub link or (b) describe
    the kind of tool you want and have Vibn find one. Vision is set
    to either "Install and host this open-source project: <url>" or
    "Find and install an open-source tool that fits this need: <desc>"
    so the AI knows which mode to operate in on first chat.

  - Import existing code  — Step 1: name + audience + repo URL.
    Step 2: optional "what do you want to do with it" textarea.
    Public repos only for v1; private-repo OAuth lands later.

Backend:
  - /api/projects/create now accepts and persists `creationMode` and
    `sourceData` on the project record under a `kickoff` blob:
      { mode, sourceData, vision, createdAt }
    The chat endpoint will read this on first turn to seed the AI
    with the user's stated intent rather than asking them to re-type
    it in chat.

Cleanup:
  - Removed FreshIdeaSetup, CodeImportSetup, ChatImportSetup,
    MigrateSetup — replaced by BuildSetup, OssSetup, ImportSetup.
  - Removed the unused initialWorkspacePath prop from
    project-association-prompt (the new flow doesn't take it).
  - TypeSelector defaults are restored — the modal opens on the
    type-picker step now, not directly on a setup form.

UI building blocks added to setup-shared:
  - TextArea (multi-line input)
  - StepDots (page indicator)
  - SegmentedTabs (generic-typed tab selector, used in OSS Step 2)
  - SecondaryButton (used as ← Back inside Step 2)

Made-with: Cursor
2026-04-29 16:16:53 -07:00
7b359e399e feat(infra): collapse to 7 categories + live Postgres table inspection
UX rework after iteration with the user:

  - Drop SMS, Analytics, Search, Monitoring categories from the rail.
    They were detection-only with no first-class UX behind them; surface
    is cleaner without them and they can return when each gets real
    flows (auth-style "edit configurables", payment-style "connect").
  - Storage no longer tries to detect S3/R2/GCS env vars. Instead it
    surfaces the workspace's bundled Vibn-provisioned GCS bucket
    (S3-compatible HMAC), with status, region, access id, and a
    one-shot env snippet for app config.
  - Email category no longer mixes in SMS providers.
  - LLM renamed to "Models"; empty state mentions BYOK as upcoming.
  - Payments empty state has a "Connect Stripe (coming soon)" CTA;
    Stripe detail surfaces the webhook URL guidance.
  - Secrets detail now lists actual env-var key names per resource,
    grouped by detected provider (Stripe block, OpenAI block, etc.)
    with an "Other (project-defined)" catch-all. Each row has Edit +
    Rotate icon buttons (currently disabled with tooltips — wire-up
    to apps.envs.upsert / services.envs.upsert lands in iter 2).

Live database inspection (Postgres only for now):

  - New /api/projects/[id]/databases/[uuid]/tables — auth-scoped, lists
    user-tables across non-system schemas via SSH-exec into the
    database container's psql. Hard caps: 50 tables, 8s timeout, no
    mutating queries possible (only SELECT row_to_json with LIMIT).
  - New /api/projects/[id]/databases/[uuid]/preview — returns first 50
    rows of a single table. Identifiers locked to /[A-Za-z0-9_]+/ so
    splicing them into the SELECT is safe.
  - DatabaseTableTree (lazy-fetch, schema-grouped, public-flat,
    approximate row counts from pg_class.reltuples) and TableViewer
    (sticky-header data grid, zebra rows, per-cell ellipsis at 360px).
  - Fix in lib/coolify.ts: listDatabasesInProject was flattening every
    db endpoint array (postgresqls, redises, mongodbs…) without
    tagging the output rows with the engine. Every consumer was
    seeing type=undefined which then bucketed as "unknown" and
    blocked the table inspector. Now we tag at the flatten step so
    every CoolifyDatabase has a stable type.
  - Infrastructure tab: database tile is now expandable inline like
    Codebases on Product. Auto-expands the first DB; click any table
    to preview rows on the right.

Made-with: Cursor
2026-04-29 15:22:58 -07:00
63f18d46a5 feat(project): wire Infrastructure tab to live Coolify data
Three sub-areas, all real, no static placeholders:

  Databases — listDatabasesInProject(coolifyProjectUuid). Type is
              normalised (postgresql / redis / mongodb / mysql / keydb
              / dragonfly / clickhouse) so the tile subtitle is stable
              regardless of how Coolify spells the engine.

  Providers — auto-detected from env-var keys across every app + service
              in the project. 35+ patterns covering Auth (Clerk, Auth0,
              Supabase, NextAuth, SuperTokens, WorkOS, Firebase Auth),
              Email (Resend, Mailgun, Postmark, SendGrid, SES, Loops),
              SMS (Twilio, Vonage), Payments (Stripe, LemonSqueezy,
              Paddle), Analytics (PostHog, Mixpanel, Amplitude, Plausible,
              Umami), LLM (OpenAI, Anthropic, Google AI, Mistral, Cohere,
              Groq, OpenRouter), Storage (S3, R2, GCS, Supabase),
              Search (Algolia, Meilisearch, Typesense), Monitoring
              (Sentry, Datadog, LogSnag). Each tile drills down to show
              which app/service the keys live in and which keys matched.

  Secrets   — env-var totals per app/service, sorted by count. Values
              are never read or returned from this surface — keys only.
              The detail pane explains how to read/edit (via AI chat
              with services.envs.* / apps.envs.* MCP tools).

Anatomy endpoint extended in the same single-fetch shape: env vars are
loaded once, then both detectProviders() and summariseSecrets() run
against that one source so we don't double-fetch.

The static What-lives-here grid is gone — every tile shown corresponds
to something that actually exists in the project.

Made-with: Cursor
2026-04-29 14:42:23 -07:00
307c3ca858 feat(project): unify Product+Hosting around code/images and live/previews
Anatomy + UI rewrite — locked the conceptual model after user feedback:

Product = "what makes up the thing you're shipping":
  - Codebases (Gitea repos)
  - Images (Coolify services backed by upstream Docker images: Twenty
    CRM, n8n, etc.)
  - Dev containers no longer surface here. The vibn-dev-* container is
    the AI's workshop, not a product surface; previews it serves still
    appear under Hosting → Previews.

Hosting = "where it lives + how it gets there", unified:
  - Live: every running endpoint as one list. Each item carries a
    source badge ("repo" | "image"), status dot, attached domain, and
    last-build summary inline. No separate Build, Domains or Services
    categories — those are properties on each Live item.
  - Previews: dev container preview URLs (unchanged).

Anatomy endpoint reshaped accordingly:
  - product.{codebases, images}
  - hosting.{live, previews}  (was production/services/previewUrls/domains)
  - lastBuild summary fetched per repo-app via listApplicationDeployments
    in parallel.

ProjectStagePill rewired to derive Live/Down/Building from hosting.live
+ hosting.previews. dev-container-detail.tsx removed.

services.* MCP tools added so AI agents can manage Coolify services
(Twenty CRM, n8n, …) the same way they manage apps:
  - services.list, services.get
  - services.start, services.stop
  - services.envs.list, services.envs.upsert
All tenant-scoped via getServiceInWorkspace + getOwnedCoolifyProjectUuids.
vibn-dev-* containers stay hidden from services.list.

Made-with: Cursor
2026-04-28 19:36:35 -07:00
3db7191146 feat(project): split dev containers into Product; convert Hosting to tile-rail
The vibn-dev-* services that the AI authors code in conceptually
belong to Product (build surface), not Hosting (runtime + reach).
Anatomy endpoint now splits Coolify services by name prefix:
  - vibn-dev-* → product.devContainers[]
  - everything else → hosting.services[]

Product tab gains a "Workspace" section above the codebases stack
with a single dev-container tile. Selecting it shows status +
active dev servers in the right pane. Codebase + file selection
behaves the same as before.

Hosting tab restructured from a stack of always-visible cards to
the same tile-rail pattern Product uses: left rail has 4 always-
present categories (Production / Services / Previews / Domains)
each with a count badge, items inside are clickable tiles, right
pane shows details for the selected item. Empty categories show a
one-liner explaining what would appear there — teaches the user
the model on a brand-new project without being preachy.

Made-with: Cursor
2026-04-28 18:54:19 -07:00
6fca78dca9 feat(project): unified anatomy endpoint + live Hosting tab + truthful Live pill
Adds GET /api/projects/[id]/anatomy returning the full project shape
in one shot — codebases (Gitea), production apps (Coolify
applications matched by repo URL), dev services (Coolify services in
the project's coolifyProjectUuid), preview URLs (active fs_dev_servers
rows), and aggregated domains. Each tab reads its own slice via the
new useAnatomy() hook so the page never fans out 3+ requests.

Hosting tab is now real: surfaces production / dev services / preview
URLs / domains with empty-state CTAs explaining what each means and
why it's empty when applicable. Includes a banner when nothing at all
is deployed for the project.

Project header pill (previously hard-coded from data.status, which
historically lied) now derives stage from hosting reality:
  - any production app running → Live (green)
  - any failed app             → Down (red)
  - any service / preview      → Building (blue)
  - else                       → fallback to data.status

Product tab refactored onto the same useAnatomy hook so we no longer
maintain two near-identical fetchers.

Made-with: Cursor
2026-04-28 17:38:57 -07:00
56d4cc36c7 feat(project): IDE-style Product tab — codebase tile expands inline, files preview right
Each codebase becomes its own panel with a header and an expandable
Gitea file tree inside. Clicking any file selects it and renders its
content in the right-hand preview panel (monospaced; no syntax
highlight yet). Single-codebase projects auto-expand the only
codebase on load so the tree is visible immediately.

Tree leaves are now interactive when an onSelectFile callback is
provided; selected rows highlight subtly so the user can tell where
the right pane's content came from.

Made-with: Cursor
2026-04-28 17:08:27 -07:00
69c3a1258c feat(project): Product/Infrastructure/Hosting tab shell with live Gitea preview
Replaces the old two-tile project landing with a tabbed shell anchored
on three sections: Product (codebases), Infrastructure (swappable
services), Hosting (runtime + reachability). Bare project URL
redirects to /product so the founder always lands on the most
actionable surface.

Product tab is the only one wired with real data so far: each
codebase tile is selectable and renders a lazy-loading Gitea file
tree for apps/<codebase>/ in the right column. Both columns share
height + a heading slot so panels stay visually aligned even when
the right side is sparse.

Infrastructure and Hosting are stubs ready for Phase 2 wiring (no
behavioural change vs today). The old (workspace)/infrastructure
route is removed in favour of the new tab; the other 15 sidebar
routes are untouched and still reachable for the migration window.

Made-with: Cursor
2026-04-28 16:37:38 -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
afd76b394f fix(chat): preserve thread history across page reloads
Two bugs, one symptom (every reload silently spawned a blank thread,
the previous conversation was orphaned in the sidebar):

1. Race in the auto-thread effect.
   On mount: threads = [], activeThread = null, /api/chat/threads
   fetch in flight. The auto-create effect re-ran the moment the
   workspace + auth resolved, saw threads.length === 0, and called
   newThread() before the fetch ever returned. When the historical
   threads finally landed, activeThread was already pinned to the new
   empty one.

   Gate on a `threadsLoaded` flag that flips true after the first
   loadThreads() resolves. Auto-create can no longer fire before
   history is known.

2. activeThread wasn't persisted.
   Even with the race fixed, refreshing the page would reset the
   sidebar to the top thread (most recently updated). After a deploy
   that's usually the brand-new empty thread we just spawned, not the
   conversation the user was actually in.

   Persist activeThread to localStorage keyed by workspace. Reload
   restores the same thread; switching workspaces resets cleanly.

Made-with: Cursor
2026-04-28 14:26:34 -07:00
210fba4e08 Fix chat panel token fetch: use /api/workspaces not URL slug
URL param 'mark-account' != workspace slug 'mark'. Fetch default token
from /api/workspaces?include_default_token=true which resolves the real
slug server-side.

Made-with: Cursor
2026-04-27 16:26:55 -07:00
56e7c2fb5c Fix auto-token fetch: use keys?include_default_token=true instead of /keys/default
Avoids the Next.js dynamic route conflict with [keyId].
Chat panel now correctly bootstraps MCP token on mount with no user action.

Made-with: Cursor
2026-04-27 16:04:38 -07:00
1e138d69d6 Auto-mint default MCP token on workspace creation
- ensureWorkspaceForUser() now calls mintWorkspaceApiKey('default') on first workspace creation
- Legacy workspaces without a default key get one minted on first request
- GET /api/workspaces/[slug]/keys/default reveals (or mints) the default token for session users
- Chat panel fetches the token automatically on mount, caches it in localStorage
- No manual setup needed — tool calling works immediately on first sign-in

Made-with: Cursor
2026-04-27 15:43:27 -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
3192e0f7b9 fix(coolify): strip is_build_time from env writes; add reveal + GCS
Coolify v4's POST/PATCH /applications/{uuid}/envs only accepts key,
value, is_preview, is_literal, is_multiline, is_shown_once. Sending
is_build_time triggers a 422 "This field is not allowed." — it's now
a derived read-only flag (is_buildtime) computed from Dockerfile ARG
usage. Breaks agents trying to upsert env vars.

Three-layer fix so this can't regress:
  - lib/coolify.ts: COOLIFY_ENV_WRITE_FIELDS whitelist enforced at the
    network boundary, regardless of caller shape
  - app/api/workspaces/[slug]/apps/[uuid]/envs: stops forwarding the
    field; returns a deprecation warning when callers send it; GET
    reads both is_buildtime and is_build_time for version parity
  - app/api/mcp/route.ts: same treatment in the MCP dispatcher;
    AI_CAPABILITIES.md doc corrected

Also bundles (not related to the above):
  - Workspace API keys are now revealable from settings. New
    key_encrypted column stores AES-256-GCM(VIBN_SECRETS_KEY, token).
    POST /api/workspaces/[slug]/keys/[keyId]/reveal returns plaintext
    for session principals only; API-key principals cannot reveal
    siblings. Legacy keys stay valid for auth but can't reveal.
  - P5.3 Object storage: lib/gcp/storage.ts + lib/workspace-gcs.ts
    idempotently provision a per-workspace GCS bucket, service
    account, IAM binding and HMAC key. New POST /api/workspaces/
    [slug]/storage/buckets endpoint. Migration script + smoke test
    included. Proven end-to-end against prod master-ai-484822.

Made-with: Cursor
2026-04-23 11:46:50 -07:00
651ddf1e11 Rip out Theia, ship P5.1 attach E2E + Justine UI work-in-progress
Theia rip-out:
- Delete app/api/theia-auth/route.ts (Traefik ForwardAuth shim)
- Delete app/api/projects/[projectId]/workspace/route.ts and
  app/api/projects/prewarm/route.ts (Cloud Run Theia provisioning)
- Delete lib/cloud-run-workspace.ts and lib/coolify-workspace.ts
- Strip provisionTheiaWorkspace + theiaWorkspaceUrl/theiaAppUuid/
  theiaError from app/api/projects/create/route.ts response
- Remove Theia callbackUrl branch in app/auth/page.tsx
- Drop "Open in Theia" button + xterm/Theia PTY copy in build/page.tsx
- Drop theiaWorkspaceUrl from deployment/page.tsx Project type
- Strip Theia IDE line + theia-code-os from advisor + agent-chat
  context strings
- Scrub Theia mention from lib/auth/workspace-auth.ts comment

P5.1 (custom apex domains + DNS):
- lib/coolify.ts + lib/opensrs.ts: nameserver normalization, OpenSRS
  XML auth, Cloud DNS plumbing
- scripts/smoke-attach-e2e.ts: full prod GCP + sandbox OpenSRS +
  prod Coolify smoke covering register/zone/A/NS/PATCH/cleanup

In-progress (Justine onboarding/build, MVP setup, agent telemetry):
- New (justine)/stories, project (home) layouts, mvp-setup, run, tasks
  routes + supporting components
- Project shell + sidebar + nav refactor for the Stackless palette
- Agent session API hardening (sessions, events, stream, approve,
  retry, stop) + atlas-chat, advisor, design-surfaces refresh
- New scripts/sync-db-url-from-coolify.mjs +
  scripts/prisma-db-push.mjs + docker-compose.local-db.yml for
  local Prisma workflows
- lib/dev-bypass.ts, lib/chat-context-refs.ts, lib/prd-sections.ts
- Misc: stories CSS, debug/prisma route, modal-theme, BuildLivePlanPanel

Made-with: Cursor
2026-04-22 18:05:01 -07:00
14835e2e0a Revert "fix(gitea-bot): add write:organization scope so bot can create repos"
This reverts commit 6f79a88abd.

Made-with: Cursor
2026-04-21 11:12:20 -07:00
6f79a88abd fix(gitea-bot): add write:organization scope so bot can create repos
Without this the bot PAT 403s on POST /orgs/{org}/repos, which is
the single most important operation — creating new project repos
inside the workspace's Gitea org.

Made-with: Cursor
2026-04-21 11:05:55 -07:00
b9511601bc feat(ai-access): per-workspace Gitea bot + tenant-safe Coolify proxy + MCP
Ship Phases 1–3 of the multi-tenant AI access plan so an AI agent can
act on a Vibn workspace with one bearer token and zero admin reach.

Phase 1 — Gitea bot per workspace
- Add gitea_bot_username / gitea_bot_user_id / gitea_bot_token_encrypted
  columns to vibn_workspaces (migrate route).
- New lib/auth/secret-box.ts (AES-256-GCM, VIBN_SECRETS_KEY) for PAT at rest.
- Extend lib/gitea.ts with createUser, createAccessTokenFor (Sudo PAT),
  createOrgTeam, addOrgTeamMember, ensureOrgTeamMembership.
- ensureWorkspaceProvisioned now mints a vibn-bot-<slug> user, adds it to
  a Writers team (write perms only) on the workspace's org, and stores
  its PAT encrypted.
- GET /api/workspaces/[slug]/gitea-credentials returns a workspace-scoped
  bot PAT + clone URL template; session or vibn_sk_ bearer auth.

Phase 2 — Tenant-safe Coolify proxy + real MCP
- lib/coolify.ts: projectUuidOf, listApplicationsInProject,
  getApplicationInProject, TenantError, env CRUD, deployments list.
- Workspace-scoped REST endpoints (all filtered by coolify_project_uuid):
  GET/POST /api/workspaces/[slug]/apps/[uuid](/deploy|/envs|/deployments),
  GET /api/workspaces/[slug]/deployments/[deploymentUuid]/logs.
- Full rewrite of /api/mcp off legacy Firebase onto Postgres vibn_sk_
  keys, exposing workspace.describe, gitea.credentials, projects.*,
  apps.* (list/get/deploy/deployments, envs.list/upsert/delete).

Phase 3 — Settings UI AI bundle
- GET /api/workspaces/[slug]/bootstrap.sh: curl|sh installer that writes
  .cursor/rules, .cursor/mcp.json and appends VIBN_* to .env.local.
  Embeds the caller's vibn_sk_ token when invoked with bearer auth.
- WorkspaceKeysPanel: single AiAccessBundleCard with system-prompt block,
  one-line bootstrap, Reveal-bot-PAT button, collapsible manual-setup
  fallback. Minted-key modal also shows the bootstrap one-liner.

Ops prerequisites:
  - Set VIBN_SECRETS_KEY (>=16 chars) on the frontend.
  - Run /api/admin/migrate to add the three bot columns.
  - GITEA_API_TOKEN must be a site-admin token (needed for admin/users
    + Sudo PAT mint); otherwise provision_status lands on 'partial'.

Made-with: Cursor
2026-04-21 10:49:17 -07:00
0bdf598984 fix(workspace-panel): resolve workspace via /api/workspaces, not URL slug
The panel was fetching /api/workspaces/{urlSlug} where {urlSlug}
is whatever is in the `[workspace]` dynamic segment (e.g.
"mark-account"). That slug has nothing to do with vibn_workspaces.slug,
which is derived from the user's email — so the fetch 404'd, the
component showed "Loading workspace…" forever, and minting/revoking
would target a non-existent workspace.

Now:
- GET /api/workspaces lazy-creates a workspace row if the signed-in
  user has none (migration path for accounts created before the
  signIn hook was added).
- WorkspaceKeysPanel discovers the user's actual workspace from that
  list and uses *its* slug for all subsequent calls (details, keys,
  provisioning, revocation).
- Empty / error states render a proper card with a retry button
  instead of a bare "Workspace not found." line.

Made-with: Cursor
2026-04-20 20:43:46 -07:00
acb63a2a5a feat(workspaces): per-account tenancy + AI access keys + Cursor integration
Adds logical multi-tenancy on top of Coolify + Gitea so every Vibn
account gets its own isolated tenant boundary, and exposes that
boundary to AI agents (Cursor, Claude Code, scripts) through
per-workspace bearer tokens.

Schema (additive, idempotent — run /api/admin/migrate once after deploy)
  - vibn_workspaces: slug, name, owner, coolify_project_uuid,
    coolify_team_id (reserved for when Coolify ships POST /teams),
    gitea_org, provision_status
  - vibn_workspace_members: room for multi-user workspaces later
  - vibn_workspace_api_keys: sha256-hashed bearer tokens
  - fs_projects.vibn_workspace_id: nullable FK linking projects
    to their workspace

Provisioning
  - On first sign-in, ensureWorkspaceForUser() inserts the row
    (no network calls — keeps signin fast).
  - On first project create, ensureWorkspaceProvisioned() lazily
    creates a Coolify Project (vibn-ws-{slug}) and a Gitea org
    (vibn-{slug}). Failures are recorded on the row, not thrown,
    and POST /api/workspaces/{slug}/provision retries.

Auth surface
  - lib/auth/workspace-auth.ts: requireWorkspacePrincipal() accepts
    either a NextAuth session or "Authorization: Bearer vibn_sk_...".
    The bearer key is hard-pinned to one workspace — it cannot
    reach any other tenant.
  - mintWorkspaceApiKey / listWorkspaceApiKeys / revokeWorkspaceApiKey

Routes
  - GET    /api/workspaces                         list
  - GET    /api/workspaces/[slug]                  details
  - POST   /api/workspaces/[slug]/provision        retry provisioning
  - GET    /api/workspaces/[slug]/keys             list keys
  - POST   /api/workspaces/[slug]/keys             mint key (token shown once)
  - DELETE /api/workspaces/[slug]/keys/[keyId]     revoke

UI
  - components/workspace/WorkspaceKeysPanel.tsx: identity card,
    keys CRUD with one-time secret reveal, and a "Connect Cursor"
    block with copy/download for:
      .cursor/rules/vibn-workspace.mdc — rule telling the agent
        about the API + workspace IDs + house rules
      ~/.cursor/mcp.json — MCP server registration with key
        embedded (server URL is /api/mcp; HTTP MCP route lands next)
      .env.local — VIBN_API_KEY + smoke-test curl
  - Slotted into existing /[workspace]/settings between Workspace
    and Notifications cards (no other layout changes).

projects/create
  - Resolves the user's workspace (creating + provisioning lazily).
  - Repos go under workspace.gitea_org (falls back to GITEA_ADMIN_USER
    for backwards compat).
  - Coolify services are created inside workspace.coolify_project_uuid
    (renamed {slug}-{appName} to stay unique within the namespace) —
    no more per-Vibn-project Coolify Project sprawl.
  - Stamps vibn_workspace_id on fs_projects.

lib/gitea
  - createOrg, getOrg, addOrgOwner, getUser
  - createRepo now routes /orgs/{owner}/repos when owner != admin

Also includes prior-turn auth hardening that was already in
authOptions.ts (CredentialsProvider for dev-local, isLocalNextAuth
cookie config) bundled in to keep the auth layer in one consistent
state.

.env.example
  - Documents GITEA_API_URL / GITEA_API_TOKEN / GITEA_ADMIN_USER /
    GITEA_WEBHOOK_SECRET and COOLIFY_URL / COOLIFY_API_TOKEN /
    COOLIFY_SERVER_UUID, with the canonical hostnames
    (git.vibnai.com, coolify.vibnai.com).

Post-deploy
  - Run once: curl -X POST https://vibnai.com/api/admin/migrate \\
      -H "x-admin-secret: \$ADMIN_MIGRATE_SECRET"
  - Existing users get a workspace row on next sign-in.
  - Existing fs_projects keep working (legacy gitea owner + their
    own per-project Coolify Projects); new projects use the
    workspace-scoped path.

Not in this commit (follow-ups)
  - Wiring requireWorkspacePrincipal into the rest of /api/projects/*
    so API keys can drive existing routes
  - HTTP MCP server at /api/mcp (the mcp.json snippet already
    points at the right URL — no client re-setup when it lands)
  - Backfill script to assign legacy fs_projects to a workspace

Made-with: Cursor
2026-04-20 17:17:12 -07:00
74f8dc4282 fix(ui): make Justine palette visible on marketing + trim rainbow chrome
- Replace blue/purple gradients with ink gradient text and cream/parch CTA surface
- Step badges and transformation icons use primary (ink) fills
- /features page icons unified to text-primary; Lora section titles
- Tree view status colors use semantic tokens instead of blue/green

Made-with: Cursor
2026-04-01 21:09:18 -07:00
bada63452f feat(ui): apply Justine ink & parchment design system
- Map Justine tokens to shadcn CSS variables (--vibn-* aliases)
- Switch fonts to Inter + Lora via next/font (IBM Plex Mono for code)
- Base typography: body Inter, h1–h3 Lora; marketing hero + wordmark serif
- Project shell and global chrome use semantic colors
- Replace Outfit/Newsreader references across TSX inline styles

Made-with: Cursor
2026-04-01 21:03:40 -07:00
8eb6c149cb fix: map Non-Functional Reqs to features_scope phase, remove circular 'Generated when PRD finalized' hint
Made-with: Cursor
2026-03-17 17:15:26 -07:00
062e836ff9 fix: always show chat on Vibn tab — swap empty 'done' state for thin PRD-ready notice bar
Made-with: Cursor
2026-03-17 17:10:06 -07:00
d9bea73032 feat: add quick-action chips above chat input (suggestions, importance, move on)
Made-with: Cursor
2026-03-17 17:02:40 -07:00
532f851d1f ux: skip type selector — new project goes straight to name input
- CreateProjectFlow now defaults to setup/fresh mode; type selector never shown
- FreshIdeaSetup simplified to just project name + Start button
  (removed description field, 6-phase explanation copy, SetupHeader)

Made-with: Cursor
2026-03-17 16:58:35 -07:00
f1b4622043 fix: make content wrapper a flex column so child pages can scroll
Made-with: Cursor
2026-03-17 16:40:24 -07:00
f47205c473 rename: replace all user-facing 'Atlas' references with 'Vibn'
Updated UI text in: project-shell (tab label), AtlasChat (sender name),
FreshIdeaMain, TypeSelector, MigrateSetup, ChatImportSetup, FreshIdeaSetup,
CodeImportSetup, prd/page, build/page, projects/page, deployment/page,
activity/page, layout (page title/description), atlas-chat API route.
Code identifiers (AtlasChat component name, file names) unchanged.

Made-with: Cursor
2026-03-17 16:25:41 -07:00
f9f3156d49 fix: PRD tracker shows sections complete (0 of 12), not internal phases
Made-with: Cursor
2026-03-17 16:22:02 -07:00
2e3b405893 feat: restore PRD section tracker on the right side of Atlas chat
Two-column layout on the Atlas tab:
- Left: Atlas discovery chat (full height, flex 1)
- Right: 240px PRD section panel showing all 12 sections with
  live status dots (filled = phase saved, empty = pending)
  plus a progress bar showing phases complete out of 6
- Discovery banner (all 6 done) now lives inside the left column
- "Generate PRD" footer CTA appears in right panel when all done

Made-with: Cursor
2026-03-17 16:15:06 -07:00
9e20125938 revert: restore Atlas|PRD|Build|Growth|Assist|Analytics tab nav, remove COO sidebar
- SECTIONS back to 6 tabs: Atlas → /overview, PRD, Build, Growth, Assist, Analytics
- Remove persistent CooChat left panel and drag-resize handle
- Content area is now full-width again (no 320px sidebar eating space)
- Clean up unused imports (useSearchParams, useRouter, CooChat, Lucide icons, TOOLS constant)

Made-with: Cursor
2026-03-17 16:03:19 -07:00
c35e7dbe56 feat: draggable resize handle on the CooChat sidebar
Made-with: Cursor
2026-03-10 16:38:13 -07:00
99c1a83b9f feat: load Atlas discovery history into CooChat sidebar
Eliminates the two-chat experience on the overview page:

- CooChat now pre-loads Atlas conversation history on mount, showing
  the full discovery conversation in the left panel. Atlas messages
  render with a blue "A" avatar; COO messages use the dark "◈" icon.
  A "Discovery · COO" divider separates historical from new messages.
- FreshIdeaMain detects when a PRD already exists and replaces the
  duplicate AtlasChat with a clean completion view ("Discovery complete")
  that links to the PRD and Build pages. Atlas chat only shows when
  discovery is still in progress.

Made-with: Cursor
2026-03-10 16:28:44 -07:00
1af5595e35 feat(tasks): move Tasks first in toolbar, add Tasks+PRD left nav and content
Made-with: Cursor
2026-03-09 22:02:01 -07:00
e3c6b9a9b4 feat(create): show only Fresh Idea and Import Chats project types
Made-with: Cursor
2026-03-09 19:02:25 -07:00
6901a97db3 feat(migrate): wire GitHub PAT through to agent runner mirror call
MigrateSetup now sends the PAT field to the API; create route
forwards it as github_token so the agent runner can clone private repos.

Made-with: Cursor
2026-03-09 18:05:12 -07:00
70c94dc60c feat: tool icons drive left nav section, remove inner pills
Made-with: Cursor
2026-03-09 16:49:47 -07:00
57c0744b56 feat: move tool icons adjacent to section pills, add active toggle state
Made-with: Cursor
2026-03-09 16:31:38 -07:00
aa23a552c4 feat: replace unicode tool icons with Lucide icons (Globe, Cloud, Palette, Code2, BarChart2)
Made-with: Cursor
2026-03-09 16:28:19 -07:00