Skip to content

Commit c9e500e

Browse files
serge-the-devclaude
andcommitted
refactor(coder-web): the Coder UI takes the console's button vocabulary, vendored, and a measurement decides which sites cannot (#366)
47 → 23. The Coder UI renders INSIDE the console — `index.css` `@source`s its directory so its classes compile against the console's tokens — so until now one screen carried two button systems: four steps in the shell, 47 hand-authored shapes in the body. ## The vocabulary transfers; the module cannot Checked before applying it, because "a different app with its own conventions" was the open question: - **Not by importing the console's.** `store/console` depends on `@proagentstore/coder-web` (`package.json`, `workspace:*`). The arrow already points that way; reversing it is a cycle. - **Not by moving it into `@proagentstore/sdk`,** which both do depend on. Tailwind v4 finds classes by scanning source text and skips `node_modules` — which is precisely why `store/console/src/index.css` carries an explicit `@source` for this directory, after classes used only here were silently never generated and its desktop controls vanished. Strings in a built `dist/` would walk straight back into that. The SDK is also published to npm for agent authors, and the console's private design decisions are not their API. So it is **vendored** — the same bargain this repo already strikes for the design tokens, which `store/admin` carries as a byte-identical `@theme` block held equal by `designTokens.test.ts` (DESIGN-SYSTEM.md §4, "three copies"). Duplication a machine can police, rather than an abstraction that cannot be built. The table now sits between `vendored:button-vocabulary` markers in both files and `control-classes.test.ts` requires the region byte-identical, plus a third assertion tying the text back to the exported records so a stale paste cannot pass while the runtime table has changed underneath it. ## Eight sites were disqualified by a measurement, not a judgement, and that is the finding `.hidden{display:none}` is emitted **before** `.inline-flex` in the built stylesheet. So `className="hidden sm:flex"` on a `<Button>` does not hide it: `BUTTON_BASE` wins. Silently, with no error, on mobile only — and it looks correct on the desktop the author is testing on. The whole coding-session header is responsive that way (`hidden sm:flex`, `sm:hidden`). Those eight are not unmigrated work; they are a gap in the table. `Button.tsx`'s docstring said "colour handed in there does not reliably win" without saying how to tell, and now carries the test: safe when the vocabulary sets no rule for that CSS property (`border-dashed`, `active:scale-95`, position); otherwise only when the incoming utility sorts later — measured, with `transition-transform` as the counter-example that DOES win, while replacing `transition-property` and taking the hover colour transition with it. ## What moved, and one accessibility side effect worth naming `CodingTab` 7, `CopilotView` 6, `ReposList` 4, `RepoSettingsModal` 3, `EnginesModal` 3, `AddRepoForm` 1. Same rule as every previous pass: `text-sm` → `lg`, `text-xs` → `md` or `sm` by padding; icon-only controls → `size="icon"`. Repo delete and End session became `variant="danger"` rather than a red hover. The five-icon solo header was `px-1.5 py-1` around a 13px glyph — about 21px tall, under the 24px WCAG floor #389 put into `BUTTON_SIZE`. This package never had that floor because it never had the table; `size="icon"`'s `min-h-6 min-w-6` gives it to them. `ReposList`'s Play/Stop stays hand-written (an accent active arm no variant has) and so stays under the floor — recorded rather than left to be discovered. The 23 that remain: 8 responsive-display, 4 active-arm toggles, 2 pills, 2 press-affordance composer controls (`active:scale-95`, a #163 requirement), 1 overlay, 1 card-shaped, 1 responsive-padding, 1 composer send, 1 jump-to-bottom pill, 2 modal buttons with the same press affordance. Pin lowered 47 → 23 in this commit. Verified in a throwaway worktree at `origin/main`: typecheck 11/11 · vitest **7816 passed / 449 files** · biome --error-on-warnings · all 8 check-* gates · docs:drift · **e2e 365 passed** (chromium + webkit, including the #431 320/390px Coder tab-row specs that exercise exactly the responsive header this commit left alone). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 7f66324 commit c9e500e

13 files changed

Lines changed: 323 additions & 42 deletions

‎DESIGN-SYSTEM.md‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,34 @@ excludes on purpose. The ~84 ordinary ones were simply unmigrated, and separatin
266266
per-screen work: the Board's chips look migratable and are not, because `size="sm"` would raise
267267
their type step and change the radius in the densest rows in the app.
268268

269-
**The console is now at 69**, and the excluded third is most of what is left — `BoardTab` (18),
270-
`AgentDetail` (15) and `InstanceDetail` (11) hold three quarters of it. When migrating the rest:
269+
**The console is now at 42 and every one of those is in an excluded class** — 10 below the smallest
270+
step's type size, 5 pills, 5 card-shaped, 4 overlaid, 3 segmented, 15 recorded judgements. So the
271+
console is finished against this vocabulary, and the next question about it is whether the table
272+
should *grow* a step, not which call sites are left.
273+
274+
**`agents/coder/web` is at 23, and the table is vendored there** (`agents/coder/web/src/control-classes.ts`).
275+
It could not import the console's: `store/console` depends on `@proagentstore/coder-web`, so the
276+
arrow only goes one way, and the SDK is not a home for it because Tailwind v4 skips `node_modules`
277+
— the same reason `index.css` carries an explicit `@source` for that directory. The region between
278+
the `vendored:button-vocabulary` markers is held byte-identical by `control-classes.test.ts`, the
279+
bargain §4 already strikes for the tokens in `store/admin`.
280+
281+
### What may be handed to `<Button className>`, measured (#366)
282+
283+
The docstring said "colour handed in there does not reliably win" without saying how to tell. The
284+
test:
285+
286+
- **Safe** if the vocabulary sets no rule for that CSS property — `border-dashed` (the variant sets
287+
border *width* and *colour*, not style), `active:scale-95`, every position utility.
288+
- **Otherwise**, the incoming utility only wins if it is emitted later in the built stylesheet.
289+
`.hidden{display:none}` is emitted **before** `.inline-flex`, so `className="hidden sm:flex"` on a
290+
`<Button>` does not hide it: `BUTTON_BASE` wins, silently, on mobile only, and it looks right on
291+
the desktop the author is testing on. Eight controls in the Coder session header are responsive
292+
that way and stay hand-written for that reason — a gap in the table, not unmigrated work.
293+
`.transition-transform` does win over `.transition-colors`, but it *replaces* `transition-property`
294+
rather than adding to it, so the hover colour transition goes with it.
295+
296+
When migrating the rest:
271297

272298
- **Preserve the rendered type step; collapse padding and radius.** `text-sm` → `size="lg"`,
273299
`text-xs` → `md` or `sm` by padding. Mapping every `px-3 py-1.5` onto `md` regardless would shrink

‎agents/coder/web/src/AddRepoForm.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import Button from "./Button";
2+
13
/**
24
* "Point this agent at a repo" — ONE form, wherever the question is asked (#411).
35
*
@@ -35,7 +37,7 @@ export default function AddRepoForm({ value, onChange, onAdd, heading, className
3537
autoCorrect="off"
3638
className="flex-1 min-w-[180px] bg-panel border border-line rounded-xl px-3 py-2 text-sm"
3739
/>
38-
<button type="button" onClick={onAdd} className="text-xs px-3 py-1.5 rounded-lg bg-accent text-white font-bold">Add</button>
40+
<Button variant="primary" size="md" onClick={onAdd}>Add</Button>
3941
</div>
4042
<p className="text-xs text-muted mt-1.5 text-left">
4143
<b>Best for dev:</b> point at a repo you already have (<code>~/dev/my-repo</code>) — the agent works in your real checkout.

‎agents/coder/web/src/Button.tsx‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { ButtonHTMLAttributes, ReactNode } from "react";
2+
import { type ButtonSize, type ButtonVariant, buttonClass } from "./control-classes";
3+
4+
/**
5+
* The Coder UI's button. Shape and colour come from the vendored table in `control-classes.ts`;
6+
* nothing about either is decided here (#366).
7+
*
8+
* This is a copy of `store/console/src/components/Button.tsx` for the reason set out in
9+
* `control-classes.ts`'s header — the console depends on this package, not the other way round.
10+
* The table is held byte-identical by a test; this wrapper is not, because it is nine lines of
11+
* plumbing rather than a decision. What matters is that both render `buttonClass(variant, size)`.
12+
*
13+
* `type="button"` is the default on purpose. React does not override the HTML default of
14+
* `submit`, so a bare `<button>` inside a `<form>` submits it, which here means a full page
15+
* reload — the reason nearly every existing call site writes `type="button"` by hand.
16+
*
17+
* `className` is passthrough for POSITION only — `shrink-0`, `ml-auto`, `w-full`, `self-start`.
18+
* Do not pass padding, radius or colour: Tailwind resolves same-property utilities by their order
19+
* in the generated stylesheet, not by their order in the class attribute, so a `px-4` handed in
20+
* here does not reliably beat the size's `px-3`. If a shape is missing, add a step to the table —
21+
* in BOTH copies.
22+
*
23+
* ── Why eight controls in this package still draw their own box
24+
*
25+
* `.hidden{display:none}` is emitted BEFORE `.inline-flex` in the built stylesheet, so
26+
* `className="hidden sm:flex"` on a Button does NOT hide it on mobile — `BUTTON_BASE`'s
27+
* `inline-flex` wins, silently, and it looks right on the desktop the author is testing on. The
28+
* session header's controls are all responsive that way (`hidden sm:flex`, `sm:hidden`), so they
29+
* are hand-written on purpose until the table grows a way to say it. Measured in
30+
* `store/console/dist/assets/index.css`, not reasoned about; the same note is in the console's copy.
31+
*/
32+
export default function Button({
33+
variant = "secondary",
34+
size = "md",
35+
className = "",
36+
type = "button",
37+
children,
38+
...rest
39+
}: Omit<ButtonHTMLAttributes<HTMLButtonElement>, "className"> & {
40+
variant?: ButtonVariant;
41+
size?: ButtonSize;
42+
className?: string;
43+
children?: ReactNode;
44+
}) {
45+
return (
46+
<button type={type} className={buttonClass(variant, size, className)} {...rest}>
47+
{children}
48+
</button>
49+
);
50+
}

‎agents/coder/web/src/CodingTab.tsx‎

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import BuildsPanel from "./BuildsPanel";
2828
import PullsPanel from "./PullsPanel";
2929
import { engineAuthBadge, isClaudeSignedOut, type EngineAuthReport } from "./engine-auth-view";
3030
import { ArrowLeft, Copy, Settings, FolderCog, ChevronDown, Eye, Square, SquareTerminal, Plus, FolderGit2, Hammer, CircleDot, GitPullRequest, Cpu, RotateCw } from "lucide-react";
31+
import Button from "./Button";
3132

3233
interface Props {
3334
instanceId: string;
@@ -1021,13 +1022,13 @@ export default function CodingTab({ instanceId, initialSessionId, onHeaderOverri
10211022
the table and the one-line assertion are in `e2e/console.spec.ts`'s #431 block. */}
10221023
{solo && <span title={`${repoTitle(solo)} · ${repoLabel(solo)}`} className="text-xs text-muted truncate min-w-0 flex-1 basis-0">{repoTitle(solo)} · {repoLabel(solo)}</span>}
10231024
<div className="ml-auto flex gap-1 shrink-0">
1024-
<button type="button" onClick={() => setShowEngines(true)} title="CLI engines & sign-in" aria-label="CLI engines" className="text-xs px-1.5 py-1 rounded-md border border-line text-muted hover:border-accent hover:text-accent"><Cpu size={13} /></button>
1025-
{solo && <button type="button" onClick={() => setSettingsRepoId(solo.id)} title="Repo settings" aria-label="Repo settings" className="text-xs px-1.5 py-1 rounded-md border border-line text-muted hover:border-accent hover:text-accent"><FolderCog size={13} /></button>}
1025+
<Button size="icon" onClick={() => setShowEngines(true)} title="CLI engines & sign-in" aria-label="CLI engines"><Cpu size={13} /></Button>
1026+
{solo && <Button size="icon" onClick={() => setSettingsRepoId(solo.id)} title="Repo settings" aria-label="Repo settings"><FolderCog size={13} /></Button>}
10261027
{/* The session actions the header takeover used to hold. Without these the solo
10271028
view could start a session and never stop it. */}
1028-
{openSession && <button type="button" onClick={copySummaryJson} title="Copy conversation as JSON" aria-label="Copy conversation as JSON" className="text-xs px-1.5 py-1 rounded-md border border-line text-muted hover:border-accent hover:text-accent"><Copy size={13} /></button>}
1029-
{openSession && <button type="button" onClick={restartSession} title="Restart the CLI" aria-label="Restart the CLI" className="text-xs px-1.5 py-1 rounded-md border border-line text-muted hover:border-accent hover:text-accent"><RotateCw size={13} /></button>}
1030-
{openSession && <button type="button" onClick={endSession} title="End session" aria-label="End session" className="text-xs px-1.5 py-1 rounded-md border border-danger text-danger font-semibold"><Square size={13} /></button>}
1029+
{openSession && <Button size="icon" onClick={copySummaryJson} title="Copy conversation as JSON" aria-label="Copy conversation as JSON"><Copy size={13} /></Button>}
1030+
{openSession && <Button size="icon" onClick={restartSession} title="Restart the CLI" aria-label="Restart the CLI"><RotateCw size={13} /></Button>}
1031+
{openSession && <Button variant="danger" size="icon" onClick={endSession} title="End session" aria-label="End session"><Square size={13} /></Button>}
10311032
</div>
10321033
</div>
10331034

@@ -1073,10 +1074,9 @@ export default function CodingTab({ instanceId, initialSessionId, onHeaderOverri
10731074
{(() => {
10741075
const a = repoOpenAction({ hasActiveSession: false, opening: openingRepoId === solo.id, runnerOnline });
10751076
return (
1076-
<button type="button" onClick={() => openRepoSession(solo.id)} disabled={a.disabled} title={a.title}
1077-
className="text-sm px-3 py-1.5 rounded-lg bg-accent text-white font-bold disabled:opacity-60">
1077+
<Button variant="primary" size="lg" onClick={() => openRepoSession(solo.id)} disabled={a.disabled} title={a.title}>
10781078
{a.label}
1079-
</button>
1079+
</Button>
10801080
);
10811081
})()}
10821082
{/* The runner warning goes BESIDE the action, never instead of it (#241).
@@ -1202,13 +1202,9 @@ export default function CodingTab({ instanceId, initialSessionId, onHeaderOverri
12021202
<pre className="text-2xs text-muted mt-1 whitespace-pre-wrap break-all">{authPrompt.evidence}</pre>
12031203
)}
12041204
{authPrompt.kind === "oauth-url" && (
1205-
<button
1206-
type="button"
1207-
onClick={startSignin}
1208-
className="mt-2 text-xs px-3 py-1.5 rounded-lg bg-accent text-white font-bold"
1209-
>
1205+
<Button variant="primary" size="md" className="mt-2" onClick={startSignin}>
12101206
Open sign-in on my runner
1211-
</button>
1207+
</Button>
12121208
)}
12131209
{signinMsg && <div className="text-xs text-muted mt-1.5">{signinMsg}</div>}
12141210
</div>

‎agents/coder/web/src/CopilotView.tsx‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { resolveComposer, resolveVoiceStatus } from "@proagentstore/sdk/hooks";
55
import { API, getToken } from "@proagentstore/sdk/client";
66
import type { LoopPreset } from "./types";
77
import { Trash2, Copy, Check, Repeat, Square, Mic, MicOff, Volume2, MessageSquare, Headphones, Send, Wrench, Settings, Loader2, Pencil, CircleDot, ArrowDown, X } from "lucide-react";
8+
import Button from "./Button";
89

910
/** Double-tap a message: replay its SAVED voice recording (voice turns), else speak
1011
* the text via TTS. Owner-scoped fetch of the R2 blob. */
@@ -311,8 +312,8 @@ export default function CopilotView({
311312
<div className="flex items-center gap-2 justify-between">
312313
<label className="text-xs text-muted flex items-center gap-1.5">Max iterations: <input type="number" value={loop.loopMax} onChange={(e) => loop.setLoopMax(Math.max(1, Math.min(50, parseInt(e.target.value, 10) || 10)))} className="w-14 bg-panel border border-line rounded px-2 py-1 text-xs" min={1} max={50} /></label>
313314
<div className="flex gap-1.5">
314-
<button type="button" onClick={() => loop.setShowLoopForm(false)} className="text-xs px-3 py-1.5 rounded-lg border border-line text-muted font-semibold">Cancel</button>
315-
<button type="button" onClick={loop.start} disabled={!loop.loopObjective.trim()} className="text-xs px-3 py-1.5 rounded-lg bg-accent text-white font-bold disabled:opacity-40">Start Loop</button>
315+
<Button size="md" onClick={() => loop.setShowLoopForm(false)}>Cancel</Button>
316+
<Button variant="primary" size="md" onClick={loop.start} disabled={!loop.loopObjective.trim()}>Start Loop</Button>
316317
</div>
317318
</div>
318319
</div>
@@ -336,17 +337,17 @@ export default function CopilotView({
336337
<div className="flex items-center gap-2 justify-between">
337338
<label className="text-xs text-muted flex items-center gap-1.5">Max iterations: <input type="number" value={loop.loopMax} onChange={(e) => loop.setLoopMax(Math.max(1, Math.min(50, parseInt(e.target.value, 10) || 10)))} className="w-14 bg-panel border border-line rounded px-2 py-1 text-xs" min={1} max={50} /></label>
338339
<div className="flex gap-1.5">
339-
<button type="button" onClick={loop.skipProposedIssue} className="text-xs px-3 py-1.5 rounded-lg border border-line text-muted font-semibold">Skip</button>
340-
<button type="button" onClick={loop.approveProposedIssue} className="text-xs px-3 py-1.5 rounded-lg bg-accent text-white font-bold">Approve &amp; run #{loop.proposedIssue.number}</button>
340+
<Button size="md" onClick={loop.skipProposedIssue}>Skip</Button>
341+
<Button variant="primary" size="md" onClick={loop.approveProposedIssue}>Approve &amp; run #{loop.proposedIssue.number}</Button>
341342
</div>
342343
</div>
343344
</>
344345
) : (
345346
<div className="flex items-center justify-between gap-2">
346347
<div className="text-xs text-muted">No open issue to work — the backlog looks clear.</div>
347348
<div className="flex gap-1.5 shrink-0">
348-
<button type="button" onClick={() => loop.setShowLoopForm(false)} className="text-xs px-3 py-1.5 rounded-lg border border-line text-muted font-semibold">Close</button>
349-
<button type="button" onClick={loop.proposeNextIssue} className="text-xs px-3 py-1.5 rounded-lg bg-accent text-white font-bold">Check again</button>
349+
<Button size="md" onClick={() => loop.setShowLoopForm(false)}>Close</Button>
350+
<Button variant="primary" size="md" onClick={loop.proposeNextIssue}>Check again</Button>
350351
</div>
351352
</div>
352353
)}

‎agents/coder/web/src/EnginesModal.tsx‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { isClaudeEngine, missingWriteFlag } from "@proagentstore/sdk/ui";
44
import type { CodingEngine, EngineAuth } from "./types";
55
import { engineContinuityNote } from "./engine-continuity";
66
import { Cpu, History, Trash2 } from "lucide-react";
7+
import Button from "./Button";
78

89
/** The engine's vault-key name shown in the api-key option label. */
910
function apiKeyName(command: string): string {
@@ -156,17 +157,13 @@ export default function EnginesModal({ instanceId, engines: initial, defaultEngi
156157
})}
157158
</div>
158159

159-
<button
160-
type="button"
161-
onClick={() => setEngines((prev) => [...prev, { id: `engine-${prev.length + 1}`, label: "", command: "" }])}
162-
className="text-xs px-2.5 py-1.5 rounded-lg border border-line text-muted font-semibold mt-2"
163-
>
160+
<Button size="md" className="mt-2" onClick={() => setEngines((prev) => [...prev, { id: `engine-${prev.length + 1}`, label: "", command: "" }])}>
164161
+ Add engine
165-
</button>
162+
</Button>
166163

167164
<div className="flex gap-2 justify-end mt-4">
168-
<button type="button" onClick={onClose} className="text-xs px-3 py-1.5 rounded-md border border-line text-muted font-semibold">Cancel</button>
169-
<button type="button" onClick={save} disabled={saving} className="text-xs px-3 py-1.5 rounded-md bg-accent text-white font-bold disabled:opacity-50">{saving ? "Saving…" : "Save"}</button>
165+
<Button size="md" onClick={onClose}>Cancel</Button>
166+
<Button variant="primary" size="md" onClick={save} disabled={saving}>{saving ? "Saving…" : "Save"}</Button>
170167
</div>
171168
</div>
172169
</div>

‎agents/coder/web/src/RepoSettingsModal.tsx‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { api } from "@proagentstore/sdk/client";
33
import type { CodingRepo } from "./types";
44
import { repoProviderLabel } from "./repo-title";
55
import { Settings, Trash2, Lock } from "lucide-react";
6+
import Button from "./Button";
67

78
/**
89
* Per-repo settings sheet: name, FOLDER, special instructions (rules), launch URLs, and delete.
@@ -175,10 +176,10 @@ export default function RepoSettingsModal({ repo, instanceId, onClose, onSaved,
175176
<input id="repo-settings-production-url" value={prod} onChange={(e) => setProd(e.target.value)} placeholder="Production URL" className="w-full bg-panel border border-line rounded-lg px-3 py-2 text-xs" />
176177

177178
<div className="flex gap-2 justify-between items-center mt-4">
178-
<button type="button" onClick={del} disabled={deleting || saving} className="text-xs px-3 py-1.5 rounded-md text-danger font-semibold hover:bg-danger-soft disabled:opacity-50 flex items-center gap-1"><Trash2 size={13} />{deleting ? "Deleting…" : "Delete"}</button>
179+
<Button variant="danger" size="md" onClick={del} disabled={deleting || saving}><Trash2 size={13} />{deleting ? "Deleting…" : "Delete"}</Button>
179180
<div className="flex gap-2">
180-
<button type="button" onClick={onClose} className="text-xs px-3 py-1.5 rounded-md border border-line text-muted font-semibold">Cancel</button>
181-
<button type="button" onClick={save} disabled={saving || deleting} className="text-xs px-3 py-1.5 rounded-md bg-accent text-white font-bold disabled:opacity-50">{saving ? "Saving…" : "Save"}</button>
181+
<Button size="md" onClick={onClose}>Cancel</Button>
182+
<Button variant="primary" size="md" onClick={save} disabled={saving || deleting}>{saving ? "Saving…" : "Save"}</Button>
182183
</div>
183184
</div>
184185
</div>

‎agents/coder/web/src/ReposList.tsx‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { repoProviderBadge, repoTitle } from "./repo-title";
1010
import { isEngineBusy } from "./engine-busy";
1111
import { repoOpenAction } from "./repo-open";
1212
import { repoFreshnessLabel, staleListNotice, type RecheckReport } from "./repo-freshness";
13+
import Button from "./Button";
1314

1415
type TimelineEntry = { type?: string; content?: string; text?: string };
1516

@@ -178,10 +179,9 @@ export default function ReposList({
178179
// No aria-label: the visible word IS the accessible name, and overriding it with
179180
// "Open <repo>" would make the control unaddressable by the name a user (or a
180181
// test) reads off the screen — the row already announces which repo it is.
181-
<button type="button" onClick={() => openRepo(r.id)} disabled={a.disabled} title={a.title}
182-
className="text-xs px-2.5 py-1 rounded-md bg-accent text-white font-bold disabled:opacity-60">
182+
<Button variant="primary" size="sm" onClick={() => openRepo(r.id)} disabled={a.disabled} title={a.title}>
183183
{a.label}
184-
</button>
184+
</Button>
185185
);
186186
})()}
187187
<button
@@ -194,7 +194,7 @@ export default function ReposList({
194194
>
195195
{phase === "loading" ? <Loader2 size={14} className="animate-spin" /> : phase === "playing" ? <Square size={14} /> : <Play size={14} />}
196196
</button>
197-
<button type="button" onClick={() => setSettingsRepoId(r.id)} title="Repo settings" className="text-xs px-1.5 py-1 rounded-md border border-line text-muted hover:border-accent hover:text-accent"><Settings size={14} /></button>
197+
<Button size="icon" onClick={() => setSettingsRepoId(r.id)} title="Repo settings"><Settings size={14} /></Button>
198198
</div>
199199
</div>
200200
{playErr?.id === r.id && (
@@ -260,9 +260,9 @@ export default function ReposList({
260260
);
261261

262262
const enginesButton = (
263-
<button type="button" onClick={onOpenEngines} title="CLI engines & sign-in" className="text-xs px-2.5 py-1.5 rounded-lg border border-line text-muted font-semibold flex items-center gap-1">
263+
<Button size="md" onClick={onOpenEngines} title="CLI engines & sign-in">
264264
<Cpu size={13} /><span className="hidden sm:inline">Engines</span>
265-
</button>
265+
</Button>
266266
);
267267

268268
/**
@@ -284,7 +284,7 @@ export default function ReposList({
284284
{enginesButton}
285285
{/* A one-repo agent that HAS its repo cannot use this — one is its maximum. */}
286286
{!singleRepo && (
287-
<button type="button" onClick={() => setShowAddRepo(!showAddRepo)} className="text-xs px-2.5 py-1.5 rounded-lg border border-line text-muted font-semibold">+ Add</button>
287+
<Button size="md" onClick={() => setShowAddRepo(!showAddRepo)}>+ Add</Button>
288288
)}
289289
</div>
290290
</div>

0 commit comments

Comments
 (0)