New cloud run
-- Send a repository-scoped coding task to Cursor cloud runtime. The - Cursor API key stays server-side. -
-diff --git a/AGENTS.md b/AGENTS.md index 8bd0e39..6446da9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,3 +3,17 @@ 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 Cookbook + +Use the official [Cursor Cookbook](https://github.com/cursor/cookbook) as the reference for SDK and cloud-agent UX. See `docs/cursor-cookbook.md` for how each example maps to this repo. + +Primary reference: **`sdk/agent-kanban`** (cloud agent board, artifacts, repositories). Compare `sdk/agent-kanban/src/lib/agents/server.ts` with `src/lib/cursor/agent-service.ts` when APIs change. + +SDK docs: https://cursor.com/docs/api/sdk/typescript + +## Cursor Cloud specific instructions + +- **Stack:** `pnpm`, Postgres via `docker compose` (port **55432**), `pnpm setup:dev`, dev server `pnpm dev` → http://localhost:3000 +- **UI:** Dark sidebar shell (`AppShell`), flat `DetailSection` panels, `cursor-panel` / `cursor-field` in `src/app/globals.css` +- **Verify:** `pnpm lint`, `pnpm typecheck`, `pnpm test`, `pnpm build` diff --git a/README.md b/README.md index 2a666aa..ab4db6e 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ but sign-in should be tested on the `preview` branch deployment. The server-side wrapper lives under `src/lib/cursor`. - `Cursor.models.list()` populates the model selector. -- `Cursor.repositories.list()` provides connected repository suggestions. +- `Cursor.repositories.list()` provides connected repository suggestions (cached ~55s per cookbook agent-kanban). - `Agent.create({ cloud })` creates repo-scoped cloud agents. - `agent.send(prompt, { idempotencyKey })` starts a run. - `Agent.getRun(runId, { runtime: "cloud", agentId })` reconnects. 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/docs/cursor-cookbook.md b/docs/cursor-cookbook.md new file mode 100644 index 0000000..15004c0 --- /dev/null +++ b/docs/cursor-cookbook.md @@ -0,0 +1,54 @@ +# Cursor Cookbook + +Chloei Code follows patterns from the official [Cursor Cookbook](https://github.com/cursor/cookbook) (`cursor/cookbook`). Use it as the reference implementation when extending cloud-agent UX, SDK calls, or infrastructure. + +## Which example maps here + +| Cookbook path | Use when | +|---------------|----------| +| [`sdk/agent-kanban`](https://github.com/cursor/cookbook/tree/main/sdk/agent-kanban) | Cloud agent listing, kanban-style grouping, artifact previews, `Agent.create({ cloud })`, repository/model pickers | +| [`sdk/quickstart`](https://github.com/cursor/cookbook/tree/main/sdk/quickstart) | Minimal `Agent` + streaming smoke tests | +| [`sdk/app-builder`](https://github.com/cursor/cookbook/tree/main/sdk/app-builder) | Local agent sessions and iframe preview loops (not used in this control plane) | +| [`sdk/coding-agent-cli`](https://github.com/cursor/cookbook/tree/main/sdk/coding-agent-cli) | Terminal workflows | +| [`sdk/dag-task-runner`](https://github.com/cursor/cookbook/tree/main/sdk/dag-task-runner) | Fan-out DAG orchestration + Cursor skill | +| [`self-hosted-cloud-agent`](https://github.com/cursor/cookbook/tree/main/self-hosted-cloud-agent) | Running cloud agent workers on your own AWS (EC2, ECS, EKS) | + +This app is closest to **agent-kanban**: a signed-in web UI over **cloud** agents with GitHub repos, status, PR links, and artifacts. Chloei Code adds Postgres persistence, Auth.js, GitHub OAuth repo/branch discovery, PR lifecycle/cleanup, and production cron/limits. + +## SDK practices (from cookbook) + +- Keep `CURSOR_API_KEY` **server-only** (never `NEXT_PUBLIC_*`). +- Create cloud runs with `Agent.create({ cloud: { repos, autoCreatePR } })` then `agent.send(prompt, { idempotencyKey })`. +- Reconnect with `Agent.getRun(runId, { runtime: "cloud", agentId, apiKey })`. +- List models via `Cursor.models.list()` and connected repos via `Cursor.repositories.list()` (cache briefly; see `src/lib/cursor/repositories.ts`). +- Stream with `run.stream()`, finalize with `run.wait()`, cancel with `run.cancel()` when supported. +- Artifacts: `agent.listArtifacts()` / `downloadArtifact()`; serve bytes through authenticated app routes (see cookbook `artifacts/media`). + +Official SDK docs: [Cursor SDK TypeScript](https://cursor.com/docs/api/sdk/typescript). + +## Local cookbook checkout + +To browse or diff against upstream examples: + +```bash +git clone --depth 1 https://github.com/cursor/cookbook.git /tmp/cursor-cookbook +``` + +Compare `sdk/agent-kanban/src/lib/agents/server.ts` with `src/lib/cursor/agent-service.ts` when SDK shapes change. + +## API key + +Create a key in the [Cursor integrations dashboard](https://cursor.com/dashboard/integrations) and set `CURSOR_API_KEY` in `.env` (this app uses one server key, not per-user keys like the kanban demo). + +## Implemented cookbook patterns in this repo + +- **Catalog cache (~55s):** `src/lib/cursor/repositories.ts`, `src/lib/cursor/models.ts` +- **Artifact inline media:** `src/lib/cursor/artifact-preview.ts` + `GET /api/agent-runs/:id/artifacts/*` +- **Cloud agent lifecycle:** `src/lib/cursor/agent-service.ts` (`Agent.create`, `send`, `getRun`, stream/wait/cancel) +- **UI reference:** sidebar agent list + search (kanban board filters), composer-style new-agent form + +## Possible next ports from agent-kanban + +- Group runs by status/repository in a kanban board view (optional route) +- Live `Agent.list({ runtime: "cloud" })` reconciliation against Postgres (operator tooling) +- Artifact thumbnails on the runs list (not only run detail) 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/api/agent-runs/[id]/artifacts/[...artifactPath]/route.ts b/src/app/api/agent-runs/[id]/artifacts/[...artifactPath]/route.ts index 6b06e8e..10682ee 100644 --- a/src/app/api/agent-runs/[id]/artifacts/[...artifactPath]/route.ts +++ b/src/app/api/agent-runs/[id]/artifacts/[...artifactPath]/route.ts @@ -1,13 +1,18 @@ import { ApiError, handleApiError } from "@/lib/api"; import { getRunForUser } from "@/lib/agent-runs/repository"; import { downloadCloudAgentArtifact } from "@/lib/cursor/agent-service"; +import { + contentTypeForArtifactPath, + getArtifactPreviewKind, + isInlineArtifactPreview, +} from "@/lib/cursor/artifact-preview"; import { requireCurrentUser } from "@/lib/auth"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; export async function GET( - _request: Request, + request: Request, { params }: { params: Promise<{ id: string; artifactPath: string[] }> } ) { try { @@ -31,11 +36,20 @@ export async function GET( buffer.byteOffset + buffer.byteLength ) as ArrayBuffer; + const contentType = contentTypeForArtifactPath(path); + const previewKind = getArtifactPreviewKind(path, contentType); + const url = new URL(request.url); + const forceInline = url.searchParams.get("inline") === "1"; + const inline = forceInline || isInlineArtifactPreview(previewKind); + const safeFilename = filename.replaceAll('"', ""); + return new Response(body, { headers: { - "Cache-Control": "private, no-store", - "Content-Disposition": `attachment; filename="${filename.replaceAll('"', "")}"`, - "Content-Type": "application/octet-stream", + "Cache-Control": inline ? "private, max-age=300" : "private, no-store", + "Content-Disposition": inline + ? `inline; filename="${safeFilename}"` + : `attachment; filename="${safeFilename}"`, + "Content-Type": contentType, }, }); } catch (error) { 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 (
-- Send a repository-scoped coding task to Cursor cloud runtime. The - Cursor API key stays server-side. -
-- Runtime checks for the control plane, external APIs, and active - Cursor runs. -
-{label}
-{value}
-{label}
+{value}
+{check.message}
No artifacts yet.
+ ) : ( +{artifact.name}
-- {artifact.artifactId} -
+ {inline ? ( +{artifact.name}
++ {artifact.artifactId} +
+- No events have been persisted yet. -
- ) : ( -Waiting for events…
+ ) : ( ++
{event.messageText}
) : null} -
- {JSON.stringify(event.rawPayload, null, 2)}
-
-
+ {JSON.stringify(event.rawPayload, null, 2)}
+
+