Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
<!-- END:nextjs-agent-rules -->

## 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`.
138 changes: 138 additions & 0 deletions __tests__/cursor/events.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
21 changes: 19 additions & 2 deletions __tests__/cursor/results.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
artifactToRecord,
extractGitResultMetadata,
extractRunResult,
type ExtractableRunPayload,
} from "@/lib/cursor/results";

describe("Cursor result extraction", () => {
Expand Down Expand Up @@ -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", () => {
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand Down
28 changes: 28 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

75 changes: 75 additions & 0 deletions scripts/setup-dev-environment.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading