Self-hosted dashboard for AI coding agents
Run Cursor Agent, Claude Code, and optionally Mistral Vibe (vibe CLI) against your own repos through a clean web UI — no cloud, no lock-in.
Workspace overview — all your projects and sessions at a glance
Session view — streaming chat alongside a live terminal
Screenshots are placeholders. Replace the images in
docs/screenshots/with real ones before publishing.
Nova Code was originally created by Jonah Fintz. The original repository (JonahFintzDev/novacode) has been deleted; development continues here at surfingbytes/novacode via Cursor.
- Coding conventions — dashboard/API style (imports, Vue script sections, boolean
bprefix, naming, braces). - Current functionality — canonical inventory of what is implemented today (API routes, WebSockets, UI, and known limitations). Prefer this over marketing copy when in doubt.
- Feature ideas — consolidated backlog of possible improvements (not a commitment); merges themes from
app/FEATURES.mdandapp/docs/improvement-plan.md. - Security audit checklist (2026-03-30) — scoped audit map covering auth, authorization, input/output handling, command execution, secrets, dependencies, CI/CD, and monitoring review paths.
- Security findings: auth/session/authz (2026-03-30) — focused findings for authentication, session handling, authorization, and data access controls.
- Security findings: appsec surfaces (2026-03-30) — findings for input/injection, XSS/CORS/header hardening, secrets/config, dependency risk, and deployment supply-chain posture.
- Security audit report (2026-03-30) — consolidated final audit report with executive summary, prioritized findings, risk themes, and phased remediation roadmap.
- Repository root
README.md— monorepo layout (app/+web/) and short status summary.
| Home | Session overview: busy and idle counts, recently active strip, optional compact list; jump to workspaces from there. |
| Workspaces | Map any directory on your host to a named project. Group, color-code, tag, and archive. The workspace grid supports a right-click menu (open, edit, archive, delete). |
| Sessions | Start a Cursor Agent, Claude Code, or Mistral Vibe session per workspace. Streaming chat over WebSocket, image attachments, tags, archive, and bulk actions via the sessions list multiselect bar. The in-workspace Sessions sidebar shows agent avatars, relative time, and a WhatsApp-style last-message preview (You: … for your messages). Session header action buttons use consistent square icon controls (edit/archive/delete). Right-click the sidebar, the sessions list, or the grid for open, edit (sessions), archive, and delete. |
| Global Search | Quickly find workspaces, sessions, automations, settings, and rule templates across your entire Nova Code instance. Accessible via the search bar in the top navigation (desktop) or sidebar (mobile). Press Ctrl+K to open the search modal from anywhere. Results are grouped by category and include relevant metadata like workspace names for sessions. |
| Terminal | Full PTY-backed terminal output via node-pty and xterm.js. |
| Orchestrators | Multi-step task plans: decompose a goal into subtasks, run each step in its own session. The orchestrator detail header matches session controls (sidebar toggle, workspace subtitle, edit/archive/delete actions). Deleting an orchestrator removes its step sessions too. |
| Automations | Schedule recurring agent prompts per workspace (cron-style intervals). |
| Git | Per-workspace Git status, diffs, and multi-repo discovery — right in the UI. |
| File browser | Browse and read/write files inside a workspace without leaving the app. |
| Workspace rules | Markdown rule files injected into every agent prompt for that workspace. |
| Role templates | Reusable instruction snippets for bootstrapping new rule files. |
| REST API | JSON API under /api with JWT bearer auth only (no separate API-token or API-key table today; see root feature-ideas.md for possible future programmatic keys). |
| Web Push | Browser notifications when sessions finish; the body previews the last assistant text or tool result (title still names workspace/session). Notifications include a Reply action that opens the PWA directly to that session. VAPID keys are created automatically in the config volume. |
| Health endpoint | GET /api/health — unauthenticated, ready for Docker HEALTHCHECK and uptime monitors. |
| MCP connectivity check | In Settings → MCP, Test connectivity dry-runs each registered MCP server (stdio spawn, HTTP GET) on the host before agents use them. |
- Docker + Docker Compose (recommended) — or Node.js 24 + PostgreSQL 17 for a manual install
- Cursor Agent and/or Claude Code CLI — installed and authenticated on the host (see Agent setup); optional Mistral Vibe (
vibeCLI + API key in Settings) - Directories you want to work on must appear under
/data-rootin the container (with the stock compose, that is everything under~/.novacode/dataon the host, or extra bind mounts you add)
For a published Docker image under ~/.novacode (install and updates use the same command):
curl -fsSL https://raw.githubusercontent.com/surfingbytes/novacode/main/scripts/install.sh | bashPrerequisites: Docker with Compose (docker compose or docker-compose), and openssl on first install.
The script writes ~/.novacode/.env with generated secrets, pulls ghcr.io/surfingbytes/novacode:latest, and starts Compose. Re-run the same command to update. Optional environment variables:
| Variable | Purpose |
|---|---|
NOVACODE_DIR |
Install root (default: ~/.novacode) |
NOVACODE_INSTALL_BASE_URL |
Raw URL of the repo root for fetched compose and .env.example (see scripts/install.sh) |
NOVACODE_IMAGE |
Image tag (default: ghcr.io/surfingbytes/novacode:latest) |
On first install you may be prompted for extra host directory mounts (workspaces under /data-root/...). Then open http://localhost:3030 and complete first-run setup.
# 1. Clone the application repository
git clone https://github.com/surfingbytes/novacode.git
cd novacode
# 2. Create your env file
cp .env.example .env
# → Edit .env: set POSTGRES_PASSWORD, JWT_SECRET, and your UID/GID
# 3. By default, ~/.novacode/data on the host is mounted at /data-root — put repos there
# (mkdir -p ~/.novacode/data) or edit docker-compose.yml to add more bind mounts.
# 4. Pull and start the published image
export UID=$(id -u) GID=$(id -g)
docker compose pull
docker compose up -d
# 5. Open the app and complete first-run setup
# http://localhost:3030 (or whatever PORT you set)Tip
On first launch the app shows a setup screen — create your admin account there. No pre-seeding required.
Note
The novacode container includes the Cursor Agent and Claude Code CLIs (Docker build). Log in to each from Settings → Integrations → Agent Authentication. For Mistral Vibe, install the vibe CLI on the image/host if you use it, and set the API key under Settings → Integrations (stored as ~/.vibe/.env on the config volume).
Copy .env.example to .env and edit the values below.
| Variable | Description |
|---|---|
POSTGRES_PASSWORD |
Password for the PostgreSQL user |
JWT_SECRET |
Long random string for signing JWTs — openssl rand -hex 32 (keep stable across restarts/upgrades; changing it invalidates existing browser tokens) |
| Variable | Default | Description |
|---|---|---|
POSTGRES_USER |
postgres |
Database user |
POSTGRES_DB |
novacode |
Database name |
POSTGRES_HOST |
postgres |
Hostname — use postgres in Docker Compose, or your external DB host |
POSTGRES_PORT |
5432 |
Port |
DATABASE_URL |
(unset) | Optional full connection URL — overrides all POSTGRES_* vars when set |
| Variable | Default | Description |
|---|---|---|
PORT |
3030 |
HTTP port the API listens on |
UID / GID |
1000 |
Host user/group for files written inside the container |
| Variable | Description |
|---|---|
AGENT_ENV_* |
Any env var prefixed with AGENT_ENV_ is forwarded to spawned agents with the prefix stripped |
VIBE_COMMAND |
(optional) Executable for Mistral Vibe (default: vibe on PATH) |
Nova Code spawns Cursor Agent and Claude Code as child processes inside the container (both installed in the Docker build). Mistral Vibe is optional: the vibe binary must be on PATH inside the container, and you configure the API key under Settings → Mistral Vibe (written to .vibe/.env under /config). Chat runs use vibe --prompt "…" --output streaming (JSONL, same stream shape as Cursor for the UI). The app does not rely on a session id from stdout: after each run it resolves the latest session_* folder under ~/.vibe/logs/session (with HOME=/config in the default deployment, that is /config/.vibe/logs/session) using the timestamp embedded in the folder name when it matches session_YYYYMMDD_HHMMSS_*, otherwise the directory mtime, then persists the suffix after the final underscore as the id for --resume on the next turn. Set VIBE_HOME in the environment (or AGENT_ENV_VIBE_HOME to forward into agents) if you use a non-default Vibe data directory. After starting the app:
- Go to Settings → Integrations → Agent Authentication
- Log in to Cursor and/or Claude — the app opens an interactive terminal session for the auth flow
- Credentials are stored under
/config(by default~/.novacode/configon the host via the stock compose file) and persist across restarts
For Mistral Vibe, install the vibe CLI, set the API key under Settings → Mistral Vibe, and ensure the process can write Vibe’s log tree (under /config when HOME points there). Example of what Nova Code runs for each user message (workspace rules may be prepended inside the prompt string):
vibe --prompt "Your request here" --output streamingFollow-up turns add --resume <id> once an id has been stored.
- Prompt appears to stop with no visible response — if Claude returns a rate-limit or other ACP request error, Nova Code now surfaces it inline in the chat and stores Claude's reset time (when provided) so auto-continue can resume after reset.
| Agent | Where the external session id comes from |
|---|---|
| Cursor | cursor-agent -f create-chat when the session is created |
| Claude | session_id in Claude’s streaming JSON on the first prompt |
| Mistral Vibe | Not from CLI stdout — after each run, the latest session_* folder under ~/.vibe/logs/session (or $VIBE_HOME/logs/session), using embedded YYYYMMDD_HHMMSS in the name when present, else directory mtime; the stored id is the suffix after the final _ (e.g. session_20260330_220714_85007cf6 → 85007cf6) |
- Agent unavailable in the UI —
vibemust passvibe --helpon the serverPATH, and the API key must be saved in Settings (seeGET /api/settings/agent-capabilities/mistralVibeAvailable). Override the binary withVIBE_COMMANDif needed. - Resume not applied — if no valid
session_*directory appears after a run (permissions, wrongHOME/VIBE_HOME), the server logs a warning and the next turn may run without--resume. Check that~/.vibe/logs/session(or$VIBE_HOME/logs/session) exists and is writable by the API process. - Wrong session picked — ensure no other
viberuns are racing to create folders in the same log directory; the server picks the latest folder by the rules above. - Assistant text repeated in one bubble — Vibe may emit the same final assistant chunk more than once or send cumulative full-text updates instead of token deltas. The dashboard merges consecutive assistant chunks (skip identical repeats; treat longer strings that extend the previous chunk as replacements) so you should not see doubled sentences like
Task completed.Task completed.
The API resolves workspace paths relative to /data-root inside the container.
The stock docker-compose.yml maps one host tree to /data-root and keeps app state on the host:
~/.novacode/config→/config(app state, agent credentials)~/.novacode/data→/data-root(your projects — workspace paths in the app are relative to this directory)
Put repositories under ~/.novacode/data on the host (for example ~/.novacode/data/acme-app), then create a workspace in the app with path acme-app.
If you prefer several host locations instead of one tree, add more lines under novacode.volumes, for example:
volumes:
- ~/.novacode/config:/config
- /home/yourname/projects:/data-root/projects
- /home/yourname/work:/data-root/workThen use workspace paths like projects/my-repo or work/client-site.
The API generates an ed25519 SSH keypair on startup (under /config/.ssh/ on the config volume) if it is not already present, and configures Git to use it for SSH remotes. To push from the UI or agents:
- Open Settings → Git and copy the public key into your Git host (GitHub, GitLab, Gitea, etc.).
- Ensure the remote uses an SSH URL (for example
git@github.com:org/repo.git). HTTPS remotes use the host’s credential helper, not this key.
The same screen lists the private key for advanced setups (treat it as a secret). Keys persist in ~/.novacode/config/.ssh/ on the host with the stock compose file.
The repo uses npm workspaces (shared/, api/, dashboard/) with a single root install:
npm install # once, at the repo root — also builds shared/ and generates the Prisma client# Copy and fill in env vars
cp .env.example .env
cd api && npx prisma migrate dev && cd ..
npm run dev:api # starts on PORT (default 3000 locally)# Point Vite at your local API
VITE_API_URL=http://localhost:3000/api npm run dev:dashboard@novacode/shared holds the canonical entity/WS-protocol types and the agent
stream-parsing logic used by both the API and the dashboard (previously two
hand-synced copies). It builds to shared/dist (dual CJS/ESM) via the root
postinstall; rebuild after editing with npm run build -w @novacode/shared.
Useful root scripts: npm run build, npm run typecheck, npm run test.
cp .env.example .env # edit as needed
export UID=$(id -u) GID=$(id -g)
docker compose -f dev.docker-compose.yaml up --build
# API → http://localhost:21000
# Dashboard → http://localhost:21001# Interactive helper (prompts for migration name)
./migrate-dev.sh
# Or directly
cd api && npx prisma migrate dev --name your_migration_nameThis README is the application package. The full repository also includes web/ (Astro + Starlight marketing and docs). Paths below are under app/.
app/
├── api/ # Fastify backend (TypeScript)
│ ├── src/
│ │ ├── classes/ # DB, auth, config, PTY, chat engine, …
│ │ └── routes/ # REST + WebSocket route handlers
│ └── prisma/ # Schema + migrations
├── dashboard/ # Vue 3 frontend (Vite + Tailwind)
│ └── src/
│ ├── views/ # Page-level components
│ ├── components/ # Shared UI components
│ │ ├── ui/ # Design primitives (UiButton, UiToggle, EmptyState, EntityDetailHeader, BottomTabBar)
│ │ ├── chat/ # Chat surface (ChatMessageList, ChatDisplayItems, ChatComposer)
│ │ ├── workspace/# Workspace-specific (SessionCard, OrchestratorCard, GitView, …)
│ │ └── automations/
│ ├── composables/ # useChatSocket, useAgentOptions, usePlanDocuments, useAgentCapabilities, useLongPress
│ ├── lib/ # Framework-free helpers (themes, notifications, wsClient, mermaid)
│ ├── utils/ # chatDisplayItems, tagColors, agentTypeMeta, relativeTime, …
│ └── stores/ # Pinia stores (workspaces, orchestrators, auth, toasts, apiHealth)
├── shared/ # @novacode/shared — canonical types + stream parsing (API ⇄ dashboard)
│ └── src/
│ ├── types.ts # Entity + WebSocket protocol types
│ ├── chatStreamPreview.ts
│ └── orchestratorPayload.ts
├── docs/ # Additional documentation
├── scripts/
│ ├── install.sh # One-line Docker install / update
│ └── docker-compose.install.yml
├── docker-compose.yml # Production compose (build from Dockerfile)
├── dev.docker-compose.yaml
└── Dockerfile
| Layer | Technology |
|---|---|
| API | Fastify · TypeScript · Prisma · PostgreSQL |
| Real-time | WebSocket (@fastify/websocket) |
| Terminal | node-pty · xterm.js |
| Frontend | Vue 3 · Pinia · Tailwind CSS v4 |
| Agents | Cursor Agent CLI · Claude Code CLI · Mistral Vibe CLI (vibe, optional) |
Pull requests are welcome. For larger changes, please open an issue first to discuss what you'd like to change.
Refactors and new code should follow the shared conventions in /data-root/personal/CODING_CONVENTIONS.md (import grouping, Vue section layout, boolean naming, and explicit control-flow braces).
Recent UI refactors standardize modal and menu scripts to the section-header layout (Props, Emits, Store, Constants, Refs, Computed, Watchers, Methods, Lifecycle as applicable), use the b prefix on local boolean refs in views such as Automations (bLoading, bShowCreateForm, …) and context-menu visibility (bCtxMenuOpen), merge duplicate @/classes/api imports where obvious, and replace inline control-flow one-liners with explicit {} blocks in script logic.
The stream-preview and orchestrator-payload logic now lives in shared/ (@novacode/shared) as the single source of truth for both API and dashboard; the old per-app files are thin re-export shims. New shared behavior should be added there (with tests in shared/src/*.test.ts), not in per-app copies.
The dashboard chat surface is decomposed: stream→display parsing is pure logic in dashboard/src/utils/chatDisplayItems.ts (regression tests alongside), connection/state in src/composables/, and presentation in src/components/chat/. Reusable UI primitives live in src/components/ui/ — use them for new UI instead of hand-rolling buttons/toggles/modals (BaseModal + ModalHeader for dialogs).
MIT © Jonah Fintz (original author). See Project status.