diff --git a/AGENTS.md b/AGENTS.md
index 8bd0e39..26dd101 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -3,3 +3,9 @@
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
+
+## Cursor Cloud specific instructions
+
+- **Stack:** `pnpm`, Postgres via `docker compose` (port **55432**), `pnpm setup:dev`, dev server `pnpm dev` → http://localhost:3000.
+- **UI:** Dark-first shell with sidebar (`AppShell`), flat `DetailSection` panels on run detail, `cursor-panel` / `cursor-field` utilities in `src/app/globals.css`. Read `node_modules/next/dist/docs/` before changing Next.js APIs.
+- **Verify:** `pnpm lint`, `pnpm typecheck`, `pnpm test`, `pnpm build`.
diff --git a/__tests__/cursor/events.test.ts b/__tests__/cursor/events.test.ts
new file mode 100644
index 0000000..8ec0a89
--- /dev/null
+++ b/__tests__/cursor/events.test.ts
@@ -0,0 +1,138 @@
+import { describe, expect, it } from "vitest";
+
+import {
+ getAssistantMessageText,
+ getCursorEventMessage,
+ getCursorEventType,
+ getUserMessageText,
+} from "@/lib/cursor/events";
+
+const baseIds = { agent_id: "bc-1", run_id: "run-1" };
+
+describe("getCursorEventType", () => {
+ it("returns SDK message type", () => {
+ expect(
+ getCursorEventType({
+ type: "status",
+ ...baseIds,
+ status: "RUNNING",
+ })
+ ).toBe("status");
+ });
+
+ it("returns unknown for malformed payloads", () => {
+ expect(getCursorEventType({})).toBe("unknown");
+ });
+});
+
+describe("getCursorEventMessage", () => {
+ it("extracts assistant text blocks", () => {
+ expect(
+ getCursorEventMessage({
+ type: "assistant",
+ ...baseIds,
+ message: {
+ role: "assistant",
+ content: [
+ { type: "text", text: "Hello" },
+ { type: "tool_use", id: "t1", name: "shell", input: {} },
+ ],
+ },
+ })
+ ).toBe("Hello");
+ });
+
+ it("formats thinking with duration", () => {
+ expect(
+ getCursorEventMessage({
+ type: "thinking",
+ ...baseIds,
+ text: "Planning",
+ thinking_duration_ms: 1200,
+ })
+ ).toBe("Planning (1200ms)");
+ });
+
+ it("formats tool_call lifecycle", () => {
+ expect(
+ getCursorEventMessage({
+ type: "tool_call",
+ ...baseIds,
+ call_id: "c1",
+ name: "read",
+ status: "completed",
+ truncated: { result: true },
+ })
+ ).toBe("Tool read completed (truncated)");
+ });
+
+ it("formats status with optional message", () => {
+ expect(
+ getCursorEventMessage({
+ type: "status",
+ ...baseIds,
+ status: "CREATING",
+ message: "Provisioning VM",
+ })
+ ).toBe("CREATING: Provisioning VM");
+ });
+
+ it("formats task milestones", () => {
+ expect(
+ getCursorEventMessage({
+ type: "task",
+ ...baseIds,
+ status: "done",
+ text: "Summary ready",
+ })
+ ).toBe("done: Summary ready");
+ });
+
+ it("extracts user prompt echo", () => {
+ expect(
+ getCursorEventMessage({
+ type: "user",
+ ...baseIds,
+ message: {
+ role: "user",
+ content: [{ type: "text", text: "Fix the bug" }],
+ },
+ })
+ ).toBe("Fix the bug");
+ });
+
+ it("handles system init", () => {
+ expect(
+ getCursorEventMessage({
+ type: "system",
+ subtype: "init",
+ ...baseIds,
+ })
+ ).toBe("Run initialized");
+ });
+});
+
+describe("message helpers", () => {
+ it("exports typed assistant and user extractors", () => {
+ const assistant = {
+ type: "assistant" as const,
+ ...baseIds,
+ message: {
+ role: "assistant" as const,
+ content: [{ type: "text" as const, text: "Done" }],
+ },
+ };
+
+ expect(getAssistantMessageText(assistant)).toBe("Done");
+ expect(
+ getUserMessageText({
+ type: "user",
+ ...baseIds,
+ message: {
+ role: "user",
+ content: [{ type: "text", text: "Hi" }],
+ },
+ })
+ ).toBe("Hi");
+ });
+});
diff --git a/__tests__/cursor/results.test.ts b/__tests__/cursor/results.test.ts
index 7fc89d5..68030e8 100644
--- a/__tests__/cursor/results.test.ts
+++ b/__tests__/cursor/results.test.ts
@@ -4,6 +4,7 @@ import {
artifactToRecord,
extractGitResultMetadata,
extractRunResult,
+ type ExtractableRunPayload,
} from "@/lib/cursor/results";
describe("Cursor result extraction", () => {
@@ -31,12 +32,28 @@ describe("Cursor result extraction", () => {
id: "run-1",
status: "finished",
result: "Done",
+ durationMs: 23000,
git: { branches: [] },
- });
+ } satisfies ExtractableRunPayload);
expect(result.rawCursorStatus).toBe("finished");
expect(result.resultSummary).toBe("Done");
- expect(result.resultRawPayload).toMatchObject({ id: "run-1" });
+ expect(result.resultRawPayload).toMatchObject({
+ id: "run-1",
+ durationMs: 23000,
+ });
+ });
+
+ it("does not summarize partial result while run is still active", () => {
+ const result = extractRunResult({
+ id: "run-1",
+ status: "running",
+ result: "Partial output so far",
+ git: { branches: [] },
+ } satisfies ExtractableRunPayload);
+
+ expect(result.resultSummary).toBeUndefined();
+ expect(result.rawCursorStatus).toBe("running");
});
it("falls back to pull request URLs and Cursor branches in result text", () => {
diff --git a/package.json b/package.json
index d2f8222..0d3b8b9 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,8 @@
"test:watch": "vitest",
"db:generate": "prisma generate",
"db:migrate": "prisma migrate dev",
- "db:studio": "prisma studio"
+ "db:studio": "prisma studio",
+ "setup:dev": "bash scripts/setup-dev-environment.sh"
},
"dependencies": {
"@auth/prisma-adapter": "^2.11.2",
@@ -29,11 +30,13 @@
"lucide-react": "^1.16.0",
"next": "16.2.6",
"next-auth": "5.0.0-beta.31",
+ "next-themes": "^0.4.6",
"pg": "^8.21.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"server-only": "^0.0.1",
"shadcn": "^4.8.0",
+ "sonner": "^2.0.7",
"tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0",
"zod": "^4.4.3"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9240458..71f0cd1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -60,6 +60,9 @@ importers:
next-auth:
specifier: 5.0.0-beta.31
version: 5.0.0-beta.31(next@16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)
+ next-themes:
+ specifier: ^0.4.6
+ version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
pg:
specifier: ^8.21.0
version: 8.21.0
@@ -75,6 +78,9 @@ importers:
shadcn:
specifier: ^4.8.0
version: 4.8.0(@types/node@20.19.41)(typescript@5.9.3)
+ sonner:
+ specifier: ^2.0.7
+ version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
tailwind-merge:
specifier: ^3.6.0
version: 3.6.0
@@ -3329,6 +3335,12 @@ packages:
nodemailer:
optional: true
+ next-themes@0.4.6:
+ resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
+ peerDependencies:
+ react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+
next@16.2.6:
resolution: {integrity: sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==}
engines: {node: '>=20.9.0'}
@@ -3963,6 +3975,12 @@ packages:
resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==}
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
+ sonner@2.0.7:
+ resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
+ peerDependencies:
+ react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -7737,6 +7755,11 @@ snapshots:
next: 16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
+ next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+
next@16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@next/env': 16.2.6
@@ -8535,6 +8558,11 @@ snapshots:
smart-buffer: 4.2.0
optional: true
+ sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ dependencies:
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+
source-map-js@1.2.1: {}
source-map@0.6.1: {}
diff --git a/scripts/setup-dev-environment.sh b/scripts/setup-dev-environment.sh
new file mode 100755
index 0000000..af6dbeb
--- /dev/null
+++ b/scripts/setup-dev-environment.sh
@@ -0,0 +1,75 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT"
+
+compose() {
+ if docker info >/dev/null 2>&1; then
+ docker compose "$@"
+ else
+ sudo docker compose "$@"
+ fi
+}
+
+upsert_env() {
+ local key="$1"
+ local value="$2"
+ local escaped="${value//\\/\\\\}"
+ escaped="${escaped//\"/\\\"}"
+ if grep -q "^${key}=" .env 2>/dev/null; then
+ sed -i "s|^${key}=.*|${key}=\"${escaped}\"|" .env
+ else
+ echo "${key}=\"${escaped}\"" >> .env
+ fi
+}
+
+if [[ ! -f .env ]]; then
+ cp .env.example .env
+ echo "Created .env from .env.example."
+fi
+
+current_auth_secret="$(grep '^AUTH_SECRET=' .env | sed 's/^AUTH_SECRET=//' | tr -d '"' || true)"
+if [[ -z "${current_auth_secret}" || "${current_auth_secret}" == replace-with-a-random-secret* ]]; then
+ upsert_env AUTH_SECRET "$(openssl rand -base64 32)"
+fi
+
+if grep -q '^CRON_SECRET=""' .env 2>/dev/null || ! grep -q '^CRON_SECRET=' .env; then
+ upsert_env CRON_SECRET "$(openssl rand -hex 32)"
+fi
+
+if ! grep -q '^ALLOW_DEV_AUTH_BYPASS=' .env; then
+ upsert_env ALLOW_DEV_AUTH_BYPASS "true"
+fi
+
+# Merge Cursor Cloud Environment secrets when injected into the VM.
+[[ -n "${DATABASE_URL:-}" ]] && upsert_env DATABASE_URL "${DATABASE_URL}"
+[[ -n "${AUTH_SECRET:-}" && "${AUTH_SECRET}" != replace-with-a-random-secret* ]] && upsert_env AUTH_SECRET "${AUTH_SECRET}"
+[[ -n "${CURSOR_API_KEY:-}" && "${CURSOR_API_KEY}" != key_xxx ]] && upsert_env CURSOR_API_KEY "${CURSOR_API_KEY}"
+[[ -n "${AUTH_GITHUB_ID:-}" && "${AUTH_GITHUB_ID}" != github-oauth-client-id ]] && upsert_env AUTH_GITHUB_ID "${AUTH_GITHUB_ID}"
+[[ -n "${AUTH_GITHUB_SECRET:-}" && "${AUTH_GITHUB_SECRET}" != github-oauth-client-secret ]] && upsert_env AUTH_GITHUB_SECRET "${AUTH_GITHUB_SECRET}"
+[[ -n "${CRON_SECRET:-}" ]] && upsert_env CRON_SECRET "${CRON_SECRET}"
+[[ -n "${ALLOW_DEV_AUTH_BYPASS:-}" ]] && upsert_env ALLOW_DEV_AUTH_BYPASS "${ALLOW_DEV_AUTH_BYPASS}"
+
+echo "Starting PostgreSQL..."
+compose up -d
+
+echo "Waiting for Postgres..."
+for _ in $(seq 1 30); do
+ if compose exec -T postgres pg_isready -U chloei -d chloei_code >/dev/null 2>&1; then
+ break
+ fi
+ sleep 1
+done
+
+echo "Installing dependencies..."
+pnpm install --frozen-lockfile
+
+echo "Applying migrations..."
+pnpm exec prisma migrate deploy
+
+echo ""
+echo "Dev stack is ready."
+echo " Start app: pnpm dev"
+echo " Open: http://localhost:3000/runs"
+echo " Health: curl -s http://localhost:3000/api/health"
diff --git a/src/app/globals.css b/src/app/globals.css
index 7e1e579..319d19b 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -48,121 +48,116 @@
--radius-4xl: calc(var(--radius) * 2.6);
}
-:root {
- --background: oklch(0.99 0.003 95);
- --foreground: oklch(0.17 0.01 95);
- --card: oklch(1 0 0);
- --card-foreground: oklch(0.17 0.01 95);
- --popover: oklch(1 0 0);
- --popover-foreground: oklch(0.17 0.01 95);
- --primary: oklch(0.27 0.05 185);
- --primary-foreground: oklch(0.985 0.003 95);
- --secondary: oklch(0.965 0.006 95);
- --secondary-foreground: oklch(0.22 0.02 95);
- --muted: oklch(0.955 0.005 95);
- --muted-foreground: oklch(0.49 0.015 95);
- --accent: oklch(0.94 0.025 185);
- --accent-foreground: oklch(0.22 0.04 185);
- --destructive: oklch(0.577 0.245 27.325);
- --border: oklch(0.89 0.006 95);
- --input: oklch(0.92 0.006 95);
- --ring: oklch(0.62 0.08 185);
- --chart-1: oklch(0.62 0.12 185);
- --chart-2: oklch(0.6 0.16 145);
- --chart-3: oklch(0.7 0.16 75);
- --chart-4: oklch(0.59 0.18 25);
- --chart-5: oklch(0.52 0.11 285);
- --radius: 0.5rem;
- --sidebar: oklch(0.985 0.003 95);
- --sidebar-foreground: oklch(0.17 0.01 95);
- --sidebar-primary: oklch(0.27 0.05 185);
- --sidebar-primary-foreground: oklch(0.985 0.003 95);
- --sidebar-accent: oklch(0.955 0.005 95);
- --sidebar-accent-foreground: oklch(0.22 0.02 95);
- --sidebar-border: oklch(0.89 0.006 95);
- --sidebar-ring: oklch(0.62 0.08 185);
- --control-canvas: oklch(0.972 0.006 95);
- --status-creating-bg: oklch(0.95 0.03 250);
- --status-creating-border: oklch(0.8 0.08 250);
- --status-creating-fg: oklch(0.44 0.13 250);
- --status-running-bg: oklch(0.94 0.045 185);
- --status-running-border: oklch(0.76 0.09 185);
- --status-running-fg: oklch(0.36 0.1 185);
- --status-finished-bg: oklch(0.94 0.055 145);
- --status-finished-border: oklch(0.77 0.11 145);
- --status-finished-fg: oklch(0.36 0.11 145);
- --status-error-bg: oklch(0.96 0.04 25);
- --status-error-border: oklch(0.78 0.13 25);
- --status-error-fg: oklch(0.48 0.17 25);
- --status-cancelled-bg: oklch(0.94 0.006 95);
- --status-cancelled-border: oklch(0.78 0.01 95);
- --status-cancelled-fg: oklch(0.43 0.01 95);
- --status-expired-bg: oklch(0.96 0.06 80);
- --status-expired-border: oklch(0.78 0.14 80);
- --status-expired-fg: oklch(0.48 0.13 80);
-}
-
+/* Cursor Cloud — flat dark, minimal chrome */
+:root,
.dark {
- --background: oklch(0.145 0 0);
- --foreground: oklch(0.985 0 0);
- --card: oklch(0.205 0 0);
- --card-foreground: oklch(0.985 0 0);
- --popover: oklch(0.205 0 0);
- --popover-foreground: oklch(0.985 0 0);
- --primary: oklch(0.922 0 0);
- --primary-foreground: oklch(0.205 0 0);
- --secondary: oklch(0.269 0 0);
- --secondary-foreground: oklch(0.985 0 0);
- --muted: oklch(0.269 0 0);
- --muted-foreground: oklch(0.708 0 0);
- --accent: oklch(0.269 0 0);
- --accent-foreground: oklch(0.985 0 0);
- --destructive: oklch(0.704 0.191 22.216);
- --border: oklch(1 0 0 / 10%);
- --input: oklch(1 0 0 / 15%);
- --ring: oklch(0.556 0 0);
- --chart-1: oklch(0.87 0 0);
- --chart-2: oklch(0.556 0 0);
- --chart-3: oklch(0.439 0 0);
- --chart-4: oklch(0.371 0 0);
- --chart-5: oklch(0.269 0 0);
- --sidebar: oklch(0.205 0 0);
- --sidebar-foreground: oklch(0.985 0 0);
- --sidebar-primary: oklch(0.488 0.243 264.376);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.269 0 0);
- --sidebar-accent-foreground: oklch(0.985 0 0);
- --sidebar-border: oklch(1 0 0 / 10%);
- --sidebar-ring: oklch(0.556 0 0);
- --control-canvas: oklch(0.17 0.01 95);
- --status-creating-bg: oklch(0.26 0.045 250);
- --status-creating-border: oklch(0.42 0.09 250);
- --status-creating-fg: oklch(0.78 0.08 250);
- --status-running-bg: oklch(0.24 0.04 185);
- --status-running-border: oklch(0.43 0.08 185);
- --status-running-fg: oklch(0.79 0.09 185);
- --status-finished-bg: oklch(0.23 0.04 145);
- --status-finished-border: oklch(0.42 0.08 145);
- --status-finished-fg: oklch(0.78 0.1 145);
- --status-error-bg: oklch(0.25 0.05 25);
- --status-error-border: oklch(0.48 0.12 25);
- --status-error-fg: oklch(0.82 0.12 25);
- --status-cancelled-bg: oklch(0.26 0.006 95);
- --status-cancelled-border: oklch(0.42 0.01 95);
- --status-cancelled-fg: oklch(0.75 0.01 95);
- --status-expired-bg: oklch(0.27 0.05 80);
- --status-expired-border: oklch(0.5 0.1 80);
- --status-expired-fg: oklch(0.82 0.12 80);
+ --background: oklch(0.12 0.003 285);
+ --foreground: oklch(0.92 0.003 285);
+ --card: oklch(0.14 0.004 285);
+ --card-foreground: oklch(0.92 0.003 285);
+ --popover: oklch(0.15 0.004 285);
+ --popover-foreground: oklch(0.92 0.003 285);
+ --primary: oklch(0.96 0.002 285);
+ --primary-foreground: oklch(0.12 0.004 285);
+ --secondary: oklch(0.18 0.006 285);
+ --secondary-foreground: oklch(0.88 0.004 285);
+ --muted: oklch(0.17 0.005 285);
+ --muted-foreground: oklch(0.58 0.01 285);
+ --accent: oklch(0.19 0.008 285);
+ --accent-foreground: oklch(0.94 0.003 285);
+ --destructive: oklch(0.62 0.18 25);
+ --border: oklch(1 0 0 / 7%);
+ --input: oklch(1 0 0 / 9%);
+ --ring: oklch(0.55 0.02 285);
+ --chart-1: oklch(0.72 0.05 285);
+ --chart-2: oklch(0.65 0.08 160);
+ --chart-3: oklch(0.68 0.1 85);
+ --chart-4: oklch(0.62 0.14 25);
+ --chart-5: oklch(0.58 0.08 300);
+ --radius: 0.375rem;
+ --sidebar: oklch(0.105 0.004 285);
+ --sidebar-foreground: oklch(0.86 0.005 285);
+ --sidebar-primary: oklch(0.96 0.002 285);
+ --sidebar-primary-foreground: oklch(0.12 0.004 285);
+ --sidebar-accent: oklch(0.17 0.008 285);
+ --sidebar-accent-foreground: oklch(0.94 0.003 285);
+ --sidebar-border: oklch(1 0 0 / 6%);
+ --sidebar-ring: oklch(0.5 0.02 285);
+ --control-canvas: oklch(0.12 0.003 285);
+ --status-creating-bg: oklch(0.2 0.03 275);
+ --status-creating-border: oklch(0.35 0.06 275);
+ --status-creating-fg: oklch(0.72 0.07 275);
+ --status-running-bg: oklch(0.19 0.025 285);
+ --status-running-border: oklch(0.32 0.05 285);
+ --status-running-fg: oklch(0.82 0.03 285);
+ --status-finished-bg: oklch(0.18 0.03 155);
+ --status-finished-border: oklch(0.32 0.06 155);
+ --status-finished-fg: oklch(0.72 0.08 155);
+ --status-error-bg: oklch(0.2 0.04 25);
+ --status-error-border: oklch(0.38 0.1 25);
+ --status-error-fg: oklch(0.78 0.1 25);
+ --status-cancelled-bg: oklch(0.17 0.005 285);
+ --status-cancelled-border: oklch(0.28 0.01 285);
+ --status-cancelled-fg: oklch(0.62 0.01 285);
+ --status-expired-bg: oklch(0.19 0.04 75);
+ --status-expired-border: oklch(0.34 0.08 75);
+ --status-expired-fg: oklch(0.76 0.09 75);
}
@layer base {
* {
@apply border-border outline-ring/50;
+ scrollbar-width: thin;
+ scrollbar-color: oklch(0.28 0.01 285) transparent;
}
- body {
- @apply bg-background text-foreground;
+ *::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+ }
+ *::-webkit-scrollbar-thumb {
+ background: oklch(0.26 0.01 285);
+ border-radius: 999px;
+ }
+ ::selection {
+ background: oklch(0.32 0.03 285);
+ color: oklch(0.98 0 0);
}
html {
@apply font-sans;
+ color-scheme: dark;
+ }
+ body {
+ @apply bg-background text-foreground antialiased;
+ font-feature-settings: "rlig" 1, "calt" 1;
+ }
+}
+
+@layer utilities {
+ .cursor-panel {
+ @apply rounded-md border border-border/60 bg-card/40;
+ }
+ .cursor-sidebar-item {
+ @apply flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-[13px] transition-colors;
+ }
+ .cursor-sidebar-item-active {
+ @apply bg-sidebar-accent text-foreground;
+ }
+ .cursor-sidebar-item-inactive {
+ @apply text-muted-foreground hover:bg-sidebar-accent/50 hover:text-foreground;
+ }
+ .cursor-content {
+ @apply mx-auto w-full max-w-3xl;
+ }
+ .cursor-content-wide {
+ @apply mx-auto w-full max-w-4xl;
+ }
+ .cursor-field {
+ @apply rounded-md border border-border/60 bg-input/30 shadow-none focus-visible:border-border focus-visible:bg-input/45 focus-visible:ring-1 focus-visible:ring-ring/40;
+ }
+ .cursor-composer {
+ @apply rounded-lg border border-border/60 bg-card/30 focus-within:border-border focus-within:bg-card/50;
+ }
+ .cursor-divider {
+ @apply border-border/50;
}
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index d5e84b8..0722dab 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -2,7 +2,8 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Analytics } from "@vercel/analytics/next";
import { SpeedInsights } from "@vercel/speed-insights/next";
-import { TooltipProvider } from "@/components/ui/tooltip";
+
+import { AppProviders } from "@/components/providers/app-providers";
import "./globals.css";
const geistSans = Geist({
@@ -28,10 +29,11 @@ export default function RootLayout({
return (
- {children}
+ {children}
diff --git a/src/app/runs/[id]/page.tsx b/src/app/runs/[id]/page.tsx
index d599e93..32e4503 100644
--- a/src/app/runs/[id]/page.tsx
+++ b/src/app/runs/[id]/page.tsx
@@ -46,11 +46,11 @@ export default async function RunDetailPage({
: { pullRequest: null, error: null };
return (
-
-
+
+
-
-
);
}
diff --git a/src/app/runs/new/page.tsx b/src/app/runs/new/page.tsx
index fc73903..5e8d24b 100644
--- a/src/app/runs/new/page.tsx
+++ b/src/app/runs/new/page.tsx
@@ -16,17 +16,8 @@ export default async function NewRunPage() {
const runLimits = await getRunCreationLimits(user.id);
return (
-
-
-
-
New cloud run
-
- Send a repository-scoped coding task to Cursor cloud runtime. The
- Cursor API key stays server-side.
-
-
-
-
+
+
);
}
diff --git a/src/app/runs/page.tsx b/src/app/runs/page.tsx
index 9165fd2..a4db117 100644
--- a/src/app/runs/page.tsx
+++ b/src/app/runs/page.tsx
@@ -1,9 +1,10 @@
import { AgentRunFilters } from "@/components/agent-runs/agent-run-filters";
import { AgentRunsTable } from "@/components/agent-runs/agent-runs-table";
+import { AgentsHome } from "@/components/agent-runs/agents-home";
import { AppShell } from "@/components/agent-runs/app-shell";
-import { NewAgentRunButton } from "@/components/agent-runs/new-agent-run-button";
import { RefreshButton } from "@/components/agent-runs/refresh-button";
import { SignInPanel } from "@/components/auth/sign-in-panel";
+import { PageHeader } from "@/components/ui/page-header";
import { listRunsForUser } from "@/lib/agent-runs/repository";
import { runStatusFilters } from "@/lib/agent-runs/types";
import { getCurrentUser } from "@/lib/auth";
@@ -23,27 +24,24 @@ export default async function RunsPage({
const { status } = await searchParams;
const activeStatus = runStatusFilters.find((item) => item === status);
- const runs = await listRunsForUser(user.id, activeStatus);
+ const allRuns = await listRunsForUser(user.id);
+ const runs = activeStatus
+ ? allRuns.filter((run) => run.normalizedStatus === activeStatus)
+ : allRuns;
+
+ if (!activeStatus) {
+ return (
+
+
+
+ );
+ }
return (
-
-
-
-
Agent runs
-
- Create, monitor, cancel, retry, and review Cursor cloud coding
- runs across connected GitHub repositories.
-
-
-
-
-
-
-
-
-
-
+ } />
+
+
);
}
diff --git a/src/app/status/page.tsx b/src/app/status/page.tsx
index d5887a6..f337e59 100644
--- a/src/app/status/page.tsx
+++ b/src/app/status/page.tsx
@@ -1,3 +1,5 @@
+import type { ReactNode } from "react";
+
import {
ActivityIcon,
CheckCircle2Icon,
@@ -8,7 +10,7 @@ import {
import { AppShell } from "@/components/agent-runs/app-shell";
import { SignInPanel } from "@/components/auth/sign-in-panel";
import { Badge } from "@/components/ui/badge";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { PageHeader } from "@/components/ui/page-header";
import { getRunCreationLimits } from "@/lib/agent-runs/limits";
import {
getRunHealthStats,
@@ -38,14 +40,8 @@ export default async function StatusPage() {
return (
-
-
-
Production status
-
- Runtime checks for the control plane, external APIs, and active
- Cursor runs.
-
-
+
+
@@ -69,118 +65,128 @@ export default async function StatusPage() {
-
-
- Runtime configuration
-
-
-
-
-
- 0
- ? `${env.ALLOWED_GITHUB_ORGS.length} configured`
- : "Any repository you can access"
- }
- />
-
-
-
-
-
-
-
-
-
- Refresh activity
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ 0
+ ? `${env.ALLOWED_GITHUB_ORGS.length} configured`
+ : "Any repository you can access"
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
Service checks
-
-
-
-
- {health.checks.map((check) => (
-
- ))}
-
-
-
+
+ }
+ action={
}
+ bodyClassName="divide-y divide-border/50 p-0"
+ >
+ {health.checks.map((check) => (
+
+ ))}
+
+
);
}
+function StatusPanel({
+ title,
+ action,
+ children,
+ bodyClassName = "p-4",
+}: {
+ title: ReactNode;
+ action?: ReactNode;
+ children: ReactNode;
+ bodyClassName?: string;
+}) {
+ return (
+
+
+
{title}
+ {action}
+
+ {children}
+
+ );
+}
+
function Metric({ label, value }: { label: string; value: number | string }) {
return (
-
-
- {label}
- {value}
-
-
+
);
}
function ConfigMetric({ label, value }: { label: string; value: string }) {
return (
-
{label}
- {value}
+ {label}
+ {value}
);
}
@@ -194,10 +200,12 @@ function HealthRow({ check }: { check: HealthCheck }) {
: CircleAlertIcon;
return (
-
+
-
- {check.name.replaceAll("_", " ")}
+
+
+ {check.name.replaceAll("_", " ")}
+
{check.message}
diff --git a/src/components/agent-runs/agent-run-actions.tsx b/src/components/agent-runs/agent-run-actions.tsx
index dfe18c0..26f3f90 100644
--- a/src/components/agent-runs/agent-run-actions.tsx
+++ b/src/components/agent-runs/agent-run-actions.tsx
@@ -59,7 +59,8 @@ export function AgentRunActions({
{prUrl ? (
}
>
diff --git a/src/components/agent-runs/agent-run-artifacts-panel.tsx b/src/components/agent-runs/agent-run-artifacts-panel.tsx
index d7d0260..06cf83e 100644
--- a/src/components/agent-runs/agent-run-artifacts-panel.tsx
+++ b/src/components/agent-runs/agent-run-artifacts-panel.tsx
@@ -1,7 +1,6 @@
import type { AgentRunArtifact } from "@prisma/client";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
-import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "@/components/ui/empty";
+import { DetailSection } from "@/components/agent-runs/detail-section";
function artifactHref(runId: string, artifactId: string) {
return `/api/agent-runs/${runId}/artifacts/${artifactId
@@ -19,44 +18,32 @@ export function AgentRunArtifactsPanel({
artifacts: AgentRunArtifact[];
}) {
return (
-
-
- Artifacts
-
-
- {artifacts.length === 0 ? (
-
-
- No artifacts stored
-
- Cursor artifact support depends on SDK/runtime availability.
-
-
-
- ) : (
-
- {artifacts.map((artifact) => (
- -
+ {artifacts.length === 0 ? (
+
No artifacts yet.
+ ) : (
+
- )}
-
-
+ Download
+
+
+ ))}
+
+ )}
+
);
}
diff --git a/src/components/agent-runs/agent-run-event-log.tsx b/src/components/agent-runs/agent-run-event-log.tsx
index 9708429..8ded326 100644
--- a/src/components/agent-runs/agent-run-event-log.tsx
+++ b/src/components/agent-runs/agent-run-event-log.tsx
@@ -2,12 +2,18 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useRouter } from "next/navigation";
-import { CableIcon, UnplugIcon } from "lucide-react";
+import { CableIcon, ChevronDownIcon, UnplugIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "@/components/ui/collapsible";
+import { ScrollArea } from "@/components/ui/scroll-area";
import { formatDateTime } from "@/lib/format";
import { isTerminalStatus } from "@/lib/cursor/status";
+import { cn } from "@/lib/utils";
export type EventLogItem = {
id: string;
@@ -101,57 +107,87 @@ export function AgentRunEventLog({
);
return (
-
-
- Event log
-
+
+
+
Stream
+
{connected ? (
-
+
) : (
-
+
)}
- {connected ? "Streaming" : "Refreshable"}
+ {connected ? "Live" : "Idle"}
-
-
- {orderedEvents.length === 0 ? (
-
- No events have been persisted yet.
-
- ) : (
-
+
+ {orderedEvents.length === 0 ? (
+ Waiting for events…
+ ) : (
+
+
{orderedEvents.map((event) => (
-
-
-
-
#{event.sequenceNumber}
-
{event.eventType}
-
- {formatDateTime(event.createdAt)}
-
+
+
+ #{event.sequenceNumber}
+ {event.eventType}
+ {formatDateTime(event.createdAt)}
{event.messageText ? (
-
+
{event.messageText}
) : null}
-
-
- Raw event JSON
-
-
- {JSON.stringify(event.rawPayload, null, 2)}
-
-
+
+
+
+ Raw
+
+
+
+ {JSON.stringify(event.rawPayload, null, 2)}
+
+
+
))}
- )}
-
-
+
+ )}
+
);
}
+function eventTypeDotClass(eventType: string) {
+ switch (eventType) {
+ case "assistant":
+ return "bg-foreground/80";
+ case "thinking":
+ return "bg-chart-5";
+ case "tool_call":
+ return "bg-chart-3";
+ case "status":
+ return "bg-chart-2";
+ case "error":
+ case "app.stream_error":
+ return "bg-destructive";
+ default:
+ return "bg-muted-foreground";
+ }
+}
+
function mergeEvents(current: EventLogItem[], incoming: EventLogItem[]) {
const byId = new Map(current.map((event) => [event.id, event]));
diff --git a/src/components/agent-runs/agent-run-filters.tsx b/src/components/agent-runs/agent-run-filters.tsx
index 143c882..b19477d 100644
--- a/src/components/agent-runs/agent-run-filters.tsx
+++ b/src/components/agent-runs/agent-run-filters.tsx
@@ -1,34 +1,45 @@
+"use client";
+
import Link from "next/link";
import { runStatusFilters, runStatusLabels } from "@/lib/agent-runs/types";
-import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
export function AgentRunFilters({ activeStatus }: { activeStatus?: string }) {
+ const items = [
+ { href: "/runs", label: "Overview", active: !activeStatus },
+ ...runStatusFilters.map((status) => ({
+ href: `/runs?status=${status}`,
+ label: runStatusLabels[status],
+ active: activeStatus === status,
+ })),
+ ];
+
return (
-