Skip to content

Commit be1f8fa

Browse files
authored
Merge pull request #97 from triggerdotdev/cursor-demo-improvements
Cursor CLI example improvements
2 parents e124c05 + 38543d8 commit be1f8fa

File tree

10 files changed

+445
-173
lines changed

10 files changed

+445
-173
lines changed

cursor-cli-demo/README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
# Cursor Agent using the Cursor CLI and Trigger.dev
1+
# Cursor background agent using the Cursor CLI and Trigger.dev
22

3-
Run Cursor's headless CLI agent inside a Trigger.dev task, parsing NDJSON stdout into a Realtime Stream that renders live in a browser terminal. Built with Next.js and Trigger.dev.
3+
Learn how to run Cursor's headless CLI agent inside a Trigger.dev task, parsing NDJSON stdout into a Realtime Stream that renders live in a browser terminal.
44

55
## Tech stack
66

77
- **[Next.js](https://nextjs.org)** – App Router frontend with server actions to trigger runs
88
- **[Cursor CLI](https://cursor.com)** – Headless AI coding agent spawned as a child process
99
- **[Trigger.dev](https://trigger.dev)** – Background task orchestration with real-time streaming to the frontend, observability, and deployment
10-
- **[Tailwind CSS](https://tailwindcss.com)** – Styling with Geist Mono for the terminal UI
10+
11+
## Video
12+
13+
https://github.com/user-attachments/assets/459aa160-6659-478e-868f-32e74f79d21a
1114

1215
## Running the project locally
1316

cursor-cli-demo/app/globals.css

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,72 @@
11
@import "tailwindcss";
22

3-
:root {
4-
--background: #0a0a0a;
5-
--foreground: #ededed;
6-
--terminal-bg: #1a1a1a;
7-
--terminal-border: #2a2a2a;
8-
--muted: #666;
9-
--green: #4ade80;
10-
--red: #f87171;
3+
@theme {
4+
--animate-fade-in-up: fade-in-up 0.7s cubic-bezier(0.16, 1, 0.3, 1) both;
5+
--animate-fade-in: fade-in-up 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
6+
--animate-event-enter: event-enter 180ms ease-out both;
7+
--animate-cursor-blink: cursor-blink 1s step-end infinite;
8+
--animate-pulse-glow: pulse-glow 3s ease-in-out infinite;
9+
10+
--color-bg: #050506;
11+
--color-surface: #0a0a0b;
12+
--color-surface-raised: #111113;
13+
--color-border: rgba(255, 255, 255, 0.06);
14+
--color-border-strong: rgba(255, 255, 255, 0.10);
15+
--color-text: #F0EDE8;
16+
--color-muted: #8B8680;
17+
--color-dim: #3D3A37;
18+
--color-accent: #2DD4BF;
19+
--color-accent-soft: rgba(45, 212, 191, 0.08);
20+
--color-success: #4ADE80;
21+
--color-error: #F87171;
22+
--color-warning: #FBBF24;
23+
--color-terminal-bg: #070708;
24+
25+
--font-sans: var(--font-geist-sans), system-ui, sans-serif;
26+
--font-mono: var(--font-geist-mono), ui-monospace, monospace;
1127
}
1228

1329
body {
14-
background: var(--background);
15-
color: var(--foreground);
16-
font-family: var(--font-geist-sans), system-ui, sans-serif;
30+
background: var(--color-bg);
31+
color: var(--color-text);
32+
font-family: var(--font-sans);
33+
}
34+
35+
/* Atmospheric background */
36+
.bg-grid {
37+
background-color: var(--color-bg);
38+
background-image:
39+
radial-gradient(ellipse at 50% 0%, rgba(45, 212, 191, 0.035) 0%, transparent 55%),
40+
radial-gradient(circle, rgba(255, 255, 255, 0.022) 1px, transparent 1px);
41+
background-size: 100% 100%, 32px 32px;
42+
}
43+
44+
/* Custom scrollbar */
45+
::-webkit-scrollbar { width: 5px; }
46+
::-webkit-scrollbar-track { background: transparent; }
47+
::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.06); border-radius: 3px; }
48+
::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.12); }
49+
50+
@keyframes fade-in-up {
51+
from { opacity: 0; transform: translateY(14px); }
52+
to { opacity: 1; transform: translateY(0); }
53+
}
54+
55+
@keyframes event-enter {
56+
from { opacity: 0; transform: translateY(6px); }
57+
to { opacity: 1; transform: translateY(0); }
58+
}
59+
60+
@keyframes cursor-blink {
61+
0%, 49% { opacity: 1; }
62+
50%, 100% { opacity: 0; }
63+
}
64+
65+
@keyframes pulse-glow {
66+
0%, 100% {
67+
border-color: rgba(45, 212, 191, 0.10);
68+
}
69+
50% {
70+
border-color: rgba(45, 212, 191, 0.22);
71+
}
1772
}

cursor-cli-demo/app/layout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ export default function RootLayout({
2323
children: React.ReactNode;
2424
}) {
2525
return (
26-
<html lang="en">
27-
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
26+
<html lang="en" className={`${geistSans.variable} ${geistMono.variable}`}>
27+
<body className="antialiased">
2828
{children}
2929
</body>
3030
</html>

cursor-cli-demo/app/page.tsx

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,30 @@ import { AgentRunner } from "@/components/agent-runner";
22

33
export default function Home() {
44
return (
5-
<main className="min-h-screen p-6 md:p-10 max-w-4xl mx-auto flex flex-col gap-6">
6-
<div>
7-
<h1 className="text-xl font-bold font-[family-name:var(--font-geist-mono)]">
8-
Cursor Agent Runner
9-
</h1>
10-
<p className="text-xs text-white/30 mt-1">
11-
Powered by Trigger.dev — watch an AI agent generate code in real time
12-
</p>
13-
</div>
5+
<div className="min-h-screen bg-grid">
6+
<main className="max-w-5xl mx-auto px-6 md:px-10 pt-16 md:pt-24 pb-16">
7+
<header className="animate-fade-in-up [animation-delay:0.05s] mb-6">
8+
<h1 className="text-[28px] md:text-[34px] font-mono font-bold tracking-tight text-text">
9+
<span className="text-accent mr-2 relative -top-[2px]">&gt;</span>
10+
background cursor
11+
</h1>
12+
<p className="text-[11px] tracking-[0.2em] uppercase text-accent mt-3 font-mono">
13+
Powered by{" "}
14+
<a
15+
href="https://trigger.dev"
16+
target="_blank"
17+
rel="noopener noreferrer"
18+
className="text-accent"
19+
>
20+
Trigger.dev
21+
</a>
22+
</p>
23+
</header>
1424

15-
<AgentRunner />
16-
</main>
25+
<div className="animate-fade-in-up [animation-delay:0.2s]">
26+
<AgentRunner />
27+
</div>
28+
</main>
29+
</div>
1730
);
1831
}

cursor-cli-demo/components/agent-runner.tsx

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,19 @@ import { Terminal } from "@/components/terminal";
66
export function AgentRunner() {
77
const { runState, startRun, reset, markComplete } = useRunState();
88

9-
const showTerminal = runState.status === "running" || runState.status === "complete";
10-
119
return (
12-
<>
10+
<div className="flex flex-col gap-6">
1311
<ControlBar runState={runState} onRun={startRun} onReset={reset} />
1412

15-
{showTerminal && (
16-
<Terminal
17-
runId={runState.runId}
18-
publicAccessToken={runState.publicAccessToken}
19-
onComplete={markComplete}
20-
/>
21-
)}
22-
23-
{runState.status === "failed" && (
24-
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-4 text-sm text-red-400 font-mono">
25-
{runState.error}
13+
{(runState.status === "running" || runState.status === "complete" || runState.status === "failed") && (
14+
<div className="animate-fade-in">
15+
<Terminal
16+
runId={runState.runId}
17+
publicAccessToken={runState.publicAccessToken}
18+
onComplete={markComplete}
19+
/>
2620
</div>
2721
)}
28-
</>
22+
</div>
2923
);
3024
}

cursor-cli-demo/components/control-bar.tsx

Lines changed: 98 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ export function ControlBar({
6666
onRun: (prompt: string, model: string) => void;
6767
onReset: () => void;
6868
}) {
69-
const [prompt, setPrompt] = useState("Create a TypeScript CLI tool that converts celsius to fahrenheit with input validation");
69+
const [prompt, setPrompt] = useState(
70+
"Create a TypeScript CLI tool that converts celsius to fahrenheit with input validation"
71+
);
7072
const [model, setModel] = useState("sonnet-4.5");
7173

7274
const isDisabled = runState.status === "starting" || runState.status === "running";
@@ -77,71 +79,103 @@ export function ControlBar({
7779
onRun(prompt.trim(), model);
7880
}
7981

82+
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
83+
if (e.key === "Enter" && !e.shiftKey) {
84+
e.preventDefault();
85+
if (!prompt.trim() || isDisabled) return;
86+
onRun(prompt.trim(), model);
87+
}
88+
}
89+
8090
return (
81-
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
82-
<div className="flex gap-3">
83-
<input
84-
type="text"
85-
value={prompt}
86-
onChange={(e) => setPrompt(e.target.value)}
87-
disabled={isDisabled}
88-
placeholder="Describe what to create..."
89-
className="flex-1 bg-[var(--terminal-bg)] border border-[var(--terminal-border)] rounded-lg px-4 py-2.5 text-sm text-white placeholder:text-white/30 focus:outline-none focus:border-white/30 disabled:opacity-50 font-[family-name:var(--font-geist-mono)]"
90-
/>
91-
92-
<select
93-
value={model}
94-
onChange={(e) => setModel(e.target.value)}
95-
disabled={isDisabled}
96-
className="bg-[var(--terminal-bg)] border border-[var(--terminal-border)] rounded-lg px-3 py-2.5 text-sm text-white/80 focus:outline-none focus:border-white/30 disabled:opacity-50"
97-
>
98-
{models.map((m) => (
99-
<option key={m.value} value={m.value}>{m.label}</option>
100-
))}
101-
</select>
102-
103-
{runState.status === "complete" || runState.status === "failed" ? (
104-
<button
105-
type="button"
106-
onClick={onReset}
107-
className="px-5 py-2.5 rounded-lg bg-white/10 text-white text-sm font-medium hover:bg-white/15 transition-colors"
108-
>
109-
New run
110-
</button>
111-
) : (
112-
<button
113-
type="submit"
114-
disabled={isDisabled || !prompt.trim()}
115-
className="px-5 py-2.5 rounded-lg bg-white text-black text-sm font-medium hover:bg-white/90 transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
116-
>
117-
{runState.status === "starting" ? "Starting..." : "Run"}
118-
</button>
119-
)}
120-
</div>
121-
122-
<div className="flex items-center gap-2">
123-
<StatusDot status={runState.status} />
124-
<span className="text-xs text-white/40">
125-
{runState.status === "idle" && "Ready"}
126-
{runState.status === "starting" && "Triggering task..."}
127-
{runState.status === "running" && "Agent is working..."}
128-
{runState.status === "complete" && "Complete"}
129-
{runState.status === "failed" && `Failed: ${runState.error}`}
130-
</span>
131-
</div>
132-
</form>
91+
<div>
92+
<form onSubmit={handleSubmit}>
93+
<div className="bg-white/[0.018] backdrop-blur-[16px] rounded-2xl border border-border p-1.5 transition-colors focus-within:border-border-strong">
94+
<textarea
95+
value={prompt}
96+
onChange={(e) => setPrompt(e.target.value)}
97+
onKeyDown={handleKeyDown}
98+
disabled={isDisabled}
99+
placeholder="Describe what to build..."
100+
rows={2}
101+
className="w-full bg-transparent resize-none px-4 py-3.5 text-sm text-text placeholder:text-dim focus:outline-none disabled:opacity-40 font-mono leading-relaxed"
102+
/>
103+
104+
<div className="flex items-center justify-between px-3 pb-2.5 pt-1">
105+
<div className="flex items-center gap-1.5">
106+
{models.map((m) => (
107+
<button
108+
key={m.value}
109+
type="button"
110+
disabled={isDisabled}
111+
onClick={() => setModel(m.value)}
112+
className={`border transition-all duration-150 ease cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed px-3 py-1.5 rounded-lg text-xs font-medium ${
113+
model === m.value
114+
? "bg-accent-soft border-[rgba(45,212,191,0.15)] text-accent"
115+
: "border-border text-muted hover:not-disabled:border-border-strong hover:not-disabled:bg-white/[0.025]"
116+
}`}
117+
>
118+
{m.label}
119+
</button>
120+
))}
121+
</div>
122+
123+
{runState.status === "complete" || runState.status === "failed" ? (
124+
<button
125+
type="button"
126+
onClick={onReset}
127+
className="px-4 py-2 rounded-lg border border-border text-muted text-xs font-medium hover:border-border-strong hover:text-text transition-all"
128+
>
129+
New run
130+
</button>
131+
) : (
132+
<button
133+
type="submit"
134+
disabled={isDisabled || !prompt.trim()}
135+
className="bg-linear-to-br from-accent to-[#25B5A3] text-surface font-semibold transition-all duration-200 ease-[cubic-bezier(0.16,1,0.3,1)] hover:not-disabled:shadow-[0_0_24px_rgba(45,212,191,0.3),0_2px_8px_rgba(0,0,0,0.3)] hover:not-disabled:-translate-y-px active:not-disabled:translate-y-0 disabled:opacity-25 disabled:cursor-not-allowed px-5 py-2 rounded-lg text-sm"
136+
>
137+
{runState.status === "starting" ? (
138+
<span className="flex items-center gap-2">
139+
<Spinner />
140+
Starting...
141+
</span>
142+
) : (
143+
"Run"
144+
)}
145+
</button>
146+
)}
147+
</div>
148+
</div>
149+
</form>
150+
151+
{runState.status === "starting" && (
152+
<div className="mt-3 flex items-center gap-2 px-2">
153+
<span className="w-1.5 h-1.5 rounded-full bg-accent animate-pulse" />
154+
<span className="text-xs text-dim">Triggering task...</span>
155+
</div>
156+
)}
157+
158+
{runState.status === "failed" && (
159+
<div className="mt-3 rounded-lg border border-red-500/20 bg-red-500/5 px-4 py-3 flex items-center gap-2.5">
160+
<span className="w-1.5 h-1.5 rounded-full bg-red-400 shrink-0" />
161+
<span className="text-xs text-red-400 font-mono">{runState.error}</span>
162+
</div>
163+
)}
164+
</div>
133165
);
134166
}
135167

136-
function StatusDot({ status }: { status: RunState["status"] }) {
137-
const color =
138-
status === "running" || status === "starting"
139-
? "bg-yellow-400 animate-pulse"
140-
: status === "complete"
141-
? "bg-green-400"
142-
: status === "failed"
143-
? "bg-red-400"
144-
: "bg-white/20";
145-
146-
return <div className={`w-2 h-2 rounded-full ${color}`} />;
168+
function Spinner() {
169+
return (
170+
<svg className="animate-spin h-3.5 w-3.5" viewBox="0 0 24 24" fill="none">
171+
<circle className="opacity-20" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="3" />
172+
<path
173+
className="opacity-80"
174+
d="M4 12a8 8 0 018-8"
175+
stroke="currentColor"
176+
strokeWidth="3"
177+
strokeLinecap="round"
178+
/>
179+
</svg>
180+
);
147181
}

0 commit comments

Comments
 (0)