Skip to content

Commit 5688fd6

Browse files
Rewrite Loops empty state with guided explanation and examples
Replace the bare "No loops yet" placeholder with a hand-holding intro: a plain-English explanation of what a loop is, a 4-step "how it works" walkthrough, and a grid of real-world example loops (failing tests, coverage, type/lint, feature build, dep upgrade, flaky test) that prefill the builder when clicked. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 717ebb7 commit 5688fd6

1 file changed

Lines changed: 159 additions & 13 deletions

File tree

packages/app/src/pages/Loops.tsx

Lines changed: 159 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,28 @@
11
import React, { useEffect, useState } from 'react';
2-
import { Check, Pause, Play, Square, X } from 'lucide-react';
2+
import {
3+
ArrowUpCircle,
4+
Bug,
5+
Check,
6+
FlaskConical,
7+
Gauge,
8+
Hammer,
9+
ListChecks,
10+
type LucideIcon,
11+
Pause,
12+
PenLine,
13+
Play,
14+
Repeat,
15+
ShieldCheck,
16+
Square,
17+
X,
18+
} from 'lucide-react';
319
import type { LoopIteration, LoopRecord, LoopSpec, LoopValidation } from '@coderouter/core';
420
import { api, type PresetInfo, type ProjectSummary } from '../lib/api';
521
import { useLoopEvents } from '../lib/events';
6-
import { EmptyState, Section, Spinner, StatusBadge, cls, money, timeAgo } from '../components/common';
22+
import { Section, Spinner, StatusBadge, cls, money, timeAgo } from '../components/common';
723
import { Dropdown } from '../components/Dropdown';
824

9-
type View = { kind: 'list' } | { kind: 'new' } | { kind: 'detail'; cwd: string; id: string };
25+
type View = { kind: 'list' } | { kind: 'new'; request?: string } | { kind: 'detail'; cwd: string; id: string };
1026

1127
export function LoopsPage({
1228
projects,
@@ -22,13 +38,19 @@ export function LoopsPage({
2238
<NewLoop
2339
projects={projects}
2440
defaultProject={project}
41+
initialRequest={view.request}
2542
onCancel={() => setView({ kind: 'list' })}
2643
onOpen={(cwd, id) => setView({ kind: 'detail', cwd, id })}
2744
/>
2845
);
2946
if (view.kind === 'detail')
3047
return <LoopDetail cwd={view.cwd} id={view.id} onBack={() => setView({ kind: 'list' })} />;
31-
return <LoopList onNew={() => setView({ kind: 'new' })} onOpen={(cwd, id) => setView({ kind: 'detail', cwd, id })} />;
48+
return (
49+
<LoopList
50+
onNew={(request) => setView({ kind: 'new', request })}
51+
onOpen={(cwd, id) => setView({ kind: 'detail', cwd, id })}
52+
/>
53+
);
3254
}
3355

3456
// ---- list ----------------------------------------------------------
@@ -37,7 +59,7 @@ function LoopList({
3759
onNew,
3860
onOpen,
3961
}: {
40-
onNew: () => void;
62+
onNew: (request?: string) => void;
4163
onOpen: (cwd: string, id: string) => void;
4264
}): React.ReactElement {
4365
const [loops, setLoops] = useState<Array<LoopRecord & { project: string }> | null>(null);
@@ -48,23 +70,22 @@ function LoopList({
4870
useEffect(refresh, []);
4971
useLoopEvents(() => refresh(), []);
5072

73+
if (!loops) return <Spinner />;
74+
if (loops.length === 0) return <LoopsEmpty onStart={onNew} />;
75+
5176
return (
5277
<div>
53-
<div className="mb-5 flex items-center justify-between">
78+
<div className="mb-5 flex items-center justify-between gap-4">
5479
<p className="max-w-2xl text-sm text-muted">
5580
Describe an outcome in plain English. CodeRouter generates a bounded, self-verifying loop, you approve it,
5681
and it runs — verify → plan → edit → review → re-verify — until the check passes or a limit is hit.
5782
</p>
58-
<button className="btn btn-primary" onClick={onNew}>
83+
<button className="btn btn-primary shrink-0" onClick={() => onNew()}>
5984
+ New loop
6085
</button>
6186
</div>
62-
{!loops && <Spinner />}
63-
{loops && loops.length === 0 && (
64-
<EmptyState title="No loops yet" hint="Create one with “New loop” — e.g. “fix the failing auth tests”." />
65-
)}
6687
<div className="grid gap-3">
67-
{loops?.map((l) => (
88+
{loops.map((l) => (
6889
<button
6990
key={l.id}
7091
onClick={() => onOpen(l.cwd, l.id)}
@@ -88,21 +109,146 @@ function LoopList({
88109
);
89110
}
90111

112+
const LOOP_STEPS: Array<{ icon: LucideIcon; title: string; text: string }> = [
113+
{ icon: PenLine, title: 'Describe it', text: 'Say the outcome you want in plain English.' },
114+
{ icon: ListChecks, title: 'Approve the plan', text: 'Check the goal, success test, and safety limits it drafts.' },
115+
{ icon: Repeat, title: 'It loops', text: 'Edit → run your checks → fix what failed, on repeat.' },
116+
{ icon: Check, title: 'Stops when done', text: 'Ends the moment the check passes or a cap is reached.' },
117+
];
118+
119+
const LOOP_EXAMPLES: Array<{ icon: LucideIcon; title: string; desc: string; prompt: string }> = [
120+
{
121+
icon: Bug,
122+
title: 'Make the failing tests pass',
123+
desc: 'Fix what’s red and keep running the suite until it’s all green.',
124+
prompt:
125+
'Fix the failing tests in this project with the smallest changes possible. Run the full test suite after every change and keep going until every test passes.',
126+
},
127+
{
128+
icon: Gauge,
129+
title: 'Get test coverage to 80%',
130+
desc: 'Add tests for the weakest files, re-checking coverage each round.',
131+
prompt:
132+
'Raise test coverage to at least 80%. Each round, find the least-covered files, add meaningful unit tests for them, re-run the coverage report, and continue until the target is met.',
133+
},
134+
{
135+
icon: ShieldCheck,
136+
title: 'Clear every type & lint error',
137+
desc: 'Fix the codebase until type-check and lint come back clean.',
138+
prompt:
139+
'Resolve every TypeScript and ESLint error in the codebase. After each batch of fixes, re-run the type-checker and linter, and keep going until both report zero errors.',
140+
},
141+
{
142+
icon: Hammer,
143+
title: 'Build a feature until it ships',
144+
desc: 'Implement a spec, looping build + tests until it all passes.',
145+
prompt:
146+
'Implement the following feature: <describe what you want>. Keep editing, then run the build and tests after each step, and don’t stop until it compiles and all tests pass.',
147+
},
148+
{
149+
icon: ArrowUpCircle,
150+
title: 'Upgrade a dependency safely',
151+
desc: 'Bump a package and fix the fallout until everything passes.',
152+
prompt:
153+
'Upgrade <package name> to its latest version. Fix every breakage the upgrade causes, re-running the build and tests after each fix, until everything passes on the new version.',
154+
},
155+
{
156+
icon: FlaskConical,
157+
title: 'Squash a flaky test',
158+
desc: 'Reproduce it, fix the root cause, confirm it passes consistently.',
159+
prompt:
160+
'Track down and fix the flaky test <test name>. Run it repeatedly to reproduce the intermittent failure, fix the underlying cause, then re-run it many times to confirm it passes consistently.',
161+
},
162+
];
163+
164+
/** Hand-holding empty state: what a loop is, how it works, and ready-to-run examples. */
165+
function LoopsEmpty({ onStart }: { onStart: (request?: string) => void }): React.ReactElement {
166+
return (
167+
<div className="mx-auto max-w-3xl pb-10">
168+
<div className="mb-3 inline-flex items-center gap-2 rounded-full border border-border bg-panel2 px-3 py-1 text-xs font-medium text-muted">
169+
<Repeat className="h-3.5 w-3.5 text-accent" strokeWidth={2.5} />
170+
Autonomous loops
171+
</div>
172+
<h2 className="text-2xl font-semibold tracking-tight">Hand off a goal — CodeRouter works until it’s actually done.</h2>
173+
<p className="mt-3 text-sm leading-relaxed text-muted">
174+
A loop is an agent on autopilot <span className="text-text">with a finish line</span>. You describe the outcome you
175+
want — “make the tests pass”, “get coverage to 80%”, “clear all the type errors”. CodeRouter turns it into a plan
176+
you approve, then repeats <span className="text-text">edit → run your checks → fix what failed</span> over and
177+
over, stopping the instant the check truly passes (or it hits the cost and iteration caps you set). You see every
178+
step and can pause or stop at any time — it can’t run away with your repo or your budget.
179+
</p>
180+
181+
<div className="mt-6 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
182+
{LOOP_STEPS.map((s, i) => {
183+
const Icon = s.icon;
184+
return (
185+
<div key={s.title} className="card">
186+
<div className="flex items-center gap-2">
187+
<span className="flex h-7 w-7 items-center justify-center rounded-lg bg-panel2 text-accent">
188+
<Icon className="h-4 w-4" strokeWidth={2} />
189+
</span>
190+
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted">Step {i + 1}</span>
191+
</div>
192+
<div className="mt-2 text-sm font-medium">{s.title}</div>
193+
<div className="mt-0.5 text-xs leading-relaxed text-muted">{s.text}</div>
194+
</div>
195+
);
196+
})}
197+
</div>
198+
199+
<div className="mt-8 mb-3 flex items-center justify-between">
200+
<h3 className="text-sm font-semibold">Start from an example</h3>
201+
<button className="text-xs text-muted transition-colors hover:text-text" onClick={() => onStart()}>
202+
or start from a blank loop →
203+
</button>
204+
</div>
205+
<div className="grid gap-3 sm:grid-cols-2">
206+
{LOOP_EXAMPLES.map((ex) => {
207+
const Icon = ex.icon;
208+
return (
209+
<button
210+
key={ex.title}
211+
onClick={() => onStart(ex.prompt)}
212+
className="card group flex items-start gap-3 text-left transition-colors hover:border-accent"
213+
>
214+
<span className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-panel2 text-accent transition-colors group-hover:bg-accent/15">
215+
<Icon className="h-4 w-4" strokeWidth={2} />
216+
</span>
217+
<span className="min-w-0">
218+
<span className="block text-sm font-medium">{ex.title}</span>
219+
<span className="mt-0.5 block text-xs leading-relaxed text-muted">{ex.desc}</span>
220+
</span>
221+
</button>
222+
);
223+
})}
224+
</div>
225+
226+
<div className="mt-8 flex justify-center">
227+
<button className="btn btn-primary" onClick={() => onStart()}>
228+
+ New loop
229+
</button>
230+
</div>
231+
</div>
232+
);
233+
}
234+
91235
// ---- new loop ------------------------------------------------------
92236

93237
function NewLoop({
94238
projects,
95239
defaultProject,
240+
initialRequest,
96241
onCancel,
97242
onOpen,
98243
}: {
99244
projects: ProjectSummary[];
100245
defaultProject: string | null;
246+
initialRequest?: string;
101247
onCancel: () => void;
102248
onOpen: (cwd: string, id: string) => void;
103249
}): React.ReactElement {
104250
const [cwd, setCwd] = useState(defaultProject ?? projects[0]?.cwd ?? '');
105-
const [request, setRequest] = useState('');
251+
const [request, setRequest] = useState(initialRequest ?? '');
106252
const [preset, setPreset] = useState('safe');
107253
const [presets, setPresets] = useState<PresetInfo[]>([]);
108254
const [busy, setBusy] = useState(false);

0 commit comments

Comments
 (0)