@superxd-studio/adptr 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/GENERATIVE.md +1 -1
  2. package/README.md +20 -11
  3. package/REUSE.md +11 -1
  4. package/REVIEW.md +2 -2
  5. package/THIRD-PARTY-NOTICES.md +1 -1
  6. package/chunks/{callout-DKMHSnjc.js → callout-B89MDU9m.js} +31 -35
  7. package/chunks/{canvas-BJ15-XxE.js → canvas-Dd78FPe0.js} +9 -9
  8. package/chunks/{text-field-BLtoaZFn.js → text-field-mMHllx03.js} +1 -1
  9. package/chunks/thinking-DlBmZsgc.js +13 -0
  10. package/chunks/{ui-9DyNsYXY.js → ui-WR0qTNm7.js} +309 -260
  11. package/generative/index.js +156 -155
  12. package/interactions/index.js +1073 -499
  13. package/package.json +2 -1
  14. package/source/components/icons.tsx +14 -0
  15. package/source/components/peek.tsx +113 -0
  16. package/source/components/thinking.tsx +13 -2
  17. package/source/reusable/generative/index.tsx +14 -9
  18. package/source/reusable/generative/vocabulary.tsx +4 -2
  19. package/source/reusable/interactions/context.tsx +4 -0
  20. package/source/reusable/interactions/copy.ts +39 -0
  21. package/source/reusable/interactions/gate.tsx +246 -0
  22. package/source/reusable/interactions/index.ts +14 -0
  23. package/source/reusable/interactions/presentation.tsx +4 -1
  24. package/source/reusable/interactions/progress.tsx +240 -0
  25. package/source/reusable/interactions/receipt.tsx +230 -0
  26. package/source/reusable/interactions/run.tsx +206 -0
  27. package/source/reusable/ui/index.ts +1 -0
  28. package/source/reusable/web/review/index.tsx +21 -10
  29. package/source/styles/library.css +56 -0
  30. package/styles.css +1 -1
  31. package/types/components/icons.d.ts +8 -0
  32. package/types/components/peek.d.ts +17 -0
  33. package/types/components/thinking.d.ts +3 -1
  34. package/types/reusable/interactions/context.d.ts +39 -0
  35. package/types/reusable/interactions/copy.d.ts +43 -0
  36. package/types/reusable/interactions/gate.d.ts +39 -0
  37. package/types/reusable/interactions/index.d.ts +8 -0
  38. package/types/reusable/interactions/progress.d.ts +45 -0
  39. package/types/reusable/interactions/receipt.d.ts +54 -0
  40. package/types/reusable/interactions/run.d.ts +83 -0
  41. package/types/reusable/ui/index.d.ts +1 -0
  42. package/ui/index.js +3 -3
  43. package/web/index.js +350 -353
@@ -0,0 +1,240 @@
1
+ import { useId, useState, type ReactNode } from "react";
2
+ import { Button } from "../../components/button.js";
3
+ import { IconAlert, IconCheck, IconChevronDown } from "../../components/icons.js";
4
+ import { Thinking } from "../../components/thinking.js";
5
+ import { Progress } from "../../components/ui/display.js";
6
+ import { cn } from "../../lib/cn.js";
7
+ import { useInteractionCopy } from "./context.js";
8
+
9
+ /** Working, possibly stuck, waiting for the person, or done */
10
+ export type TaskProgressStatus = "working" | "stuck" | "needs-you" | "done";
11
+
12
+ export interface TaskProgressMove {
13
+ id: string;
14
+ label: string;
15
+ onPress: () => void;
16
+ primary?: boolean;
17
+ }
18
+
19
+ export interface TaskProgressProps {
20
+ /** The task, in words: "Building the new pricing page". Shown once it's done. */
21
+ task: string;
22
+ /** What it's doing now, in human words: "Testing the annual toggle", not "Running command" */
23
+ now: string;
24
+ /** The agent's plan */
25
+ stages: readonly string[];
26
+ /** The stage it's on, from 0. Stages before it are done; stages.length means all done. */
27
+ stage: number;
28
+ status?: TaskProgressStatus;
29
+ /** Time as a range, never a countdown: "~3–8 min left" */
30
+ estimate: string;
31
+ /** What the estimate is based on: "From the last 20 runs" */
32
+ basis?: string;
33
+ /** Counted progress, when the work can be counted */
34
+ counted?: { label: string; value: number; max: number; valueLabel?: string };
35
+ /** Why the estimate changed */
36
+ changed?: ReactNode;
37
+ /** Whether it needs the person, in words. Defaults to "No action needed". */
38
+ needs?: string;
39
+ /** Shows Review on the line while it needs the person */
40
+ onReview?: () => void;
41
+ /** Moves such as Keep waiting or Stop, at the right end */
42
+ moves?: readonly TaskProgressMove[];
43
+ defaultExpanded?: boolean;
44
+ }
45
+
46
+ /**
47
+ * Agent task progress, in one line: what it's doing, how many stages are done and roughly how
48
+ * long is left. Progress is a fact; time is a range with its basis. It opens to the stages.
49
+ */
50
+ export function TaskProgress({
51
+ task,
52
+ now,
53
+ stages,
54
+ stage,
55
+ status = "working",
56
+ estimate,
57
+ basis,
58
+ counted,
59
+ changed,
60
+ needs,
61
+ onReview,
62
+ moves = [],
63
+ defaultExpanded = false,
64
+ }: TaskProgressProps) {
65
+ const copy = useInteractionCopy().progress;
66
+ const [open, setOpen] = useState(defaultExpanded);
67
+ const panel = useId();
68
+ const total = stages.length;
69
+ const done = status === "done";
70
+ const sorted = [...moves].sort((a, b) => Number(Boolean(a.primary)) - Number(Boolean(b.primary)));
71
+
72
+ return (
73
+ <article className="flex w-full min-w-0 flex-col overflow-hidden rounded-surface border border-line-subtle bg-surface-muted">
74
+ <div className="flex flex-wrap items-center gap-x-3 gap-y-2 p-3">
75
+ <span
76
+ aria-hidden="true"
77
+ className={cn(
78
+ "flex size-8 shrink-0 items-center justify-center rounded-control border border-line-subtle bg-surface",
79
+ done && "border-done bg-done text-on-done",
80
+ status === "stuck" && "border-attention-line bg-attention-subtle text-attention",
81
+ )}
82
+ >
83
+ {done ? (
84
+ <IconCheck className="size-4" />
85
+ ) : status === "stuck" ? (
86
+ <IconAlert className="size-4" />
87
+ ) : (
88
+ <Thinking tone={status === "needs-you" ? "info" : "brand"} />
89
+ )}
90
+ </span>
91
+ <div role="status" className="flex min-w-0 flex-1 flex-col gap-1">
92
+ <span className="truncate type-label text-fg-emphasis">{done ? task : now}</span>
93
+ <span className="flex flex-wrap gap-1.5 type-caption text-fg-subtle">
94
+ <span>{copy.stagesDone(Math.min(stage, total), total)}</span>
95
+ <span aria-hidden="true">·</span>
96
+ <span
97
+ className={cn(
98
+ status === "stuck" && "text-attention",
99
+ status === "needs-you" && "text-info",
100
+ )}
101
+ >
102
+ {estimate}
103
+ </span>
104
+ </span>
105
+ </div>
106
+ {status === "needs-you" && onReview ? (
107
+ <Button size="sm" variant="primary" onPress={onReview}>
108
+ {copy.review}
109
+ </Button>
110
+ ) : null}
111
+ <Button
112
+ size="sm"
113
+ variant="ghost"
114
+ className="group/trigger"
115
+ aria-label={copy.showStages}
116
+ aria-expanded={open}
117
+ aria-controls={panel}
118
+ onPress={() => setOpen((value) => !value)}
119
+ >
120
+ <IconChevronDown className="transition-[rotate] duration-(--duration-settle) ease-settle group-aria-expanded/trigger:rotate-180 motion-reduce:transition-none" />
121
+ </Button>
122
+ </div>
123
+ {open ? (
124
+ <div
125
+ id={panel}
126
+ className="flex animate-reveal-in flex-col gap-4 border-t border-line-subtle bg-surface p-4"
127
+ >
128
+ {/* The plan: done ✓, now ●, still to come ○ */}
129
+ <ol className="flex flex-col gap-2.5">
130
+ {stages.map((name, index) => {
131
+ const past = index < stage;
132
+ const current = index === stage && !done;
133
+ return (
134
+ <li key={name} className="flex gap-3 type-body">
135
+ {/* A marker column one line high, so every stage's words line up */}
136
+ <span aria-hidden="true" className="flex h-5 shrink-0 items-center">
137
+ <span className="flex size-4 items-center justify-center">
138
+ {past ? (
139
+ <IconCheck className="size-3.5 text-success" />
140
+ ) : current ? (
141
+ status === "stuck" ? (
142
+ <IconAlert className="size-3.5 text-attention" />
143
+ ) : (
144
+ <Thinking tone={status === "needs-you" ? "info" : "brand"} />
145
+ )
146
+ ) : (
147
+ <span className="size-2 rounded-pill border border-line-emphasis" />
148
+ )}
149
+ </span>
150
+ </span>
151
+ <div className="flex min-w-0 flex-1 flex-col gap-1.5">
152
+ <span
153
+ className={cn(
154
+ current
155
+ ? "font-medium text-fg-emphasis"
156
+ : past
157
+ ? "text-fg"
158
+ : "text-fg-subtle",
159
+ )}
160
+ >
161
+ {name}
162
+ <span className="sr-only">
163
+ {" "}
164
+ ·{" "}
165
+ {past
166
+ ? copy.stageWords.done
167
+ : current
168
+ ? copy.stageWords.now
169
+ : copy.stageWords.next}
170
+ </span>
171
+ </span>
172
+ {current ? (
173
+ <>
174
+ <span className="type-caption text-fg-subtle">{now}</span>
175
+ {counted ? (
176
+ <Progress
177
+ label={counted.label}
178
+ value={counted.value}
179
+ maxValue={counted.max}
180
+ valueLabel={counted.valueLabel}
181
+ />
182
+ ) : null}
183
+ </>
184
+ ) : null}
185
+ </div>
186
+ </li>
187
+ );
188
+ })}
189
+ </ol>
190
+ {changed ? (
191
+ <p className="rounded-control border border-attention-line bg-attention-subtle px-3 py-2 type-body text-attention">
192
+ {changed}
193
+ </p>
194
+ ) : null}
195
+ {/* The estimate far left, whether it needs you far right */}
196
+ <div className="flex flex-wrap items-start justify-between gap-4 border-t border-line-subtle pt-3">
197
+ <div className="flex min-w-0 flex-col gap-0.5">
198
+ <span className="type-label text-fg-emphasis">{estimate}</span>
199
+ {basis ? <span className="type-caption text-fg-subtle">{basis}</span> : null}
200
+ </div>
201
+ <span
202
+ className={cn(
203
+ "flex items-center gap-1.5 type-label",
204
+ status === "stuck"
205
+ ? "text-attention"
206
+ : status === "needs-you"
207
+ ? "text-info"
208
+ : "text-fg-subtle",
209
+ )}
210
+ >
211
+ {status === "stuck" ? (
212
+ <IconAlert className="size-4" />
213
+ ) : status === "needs-you" ? (
214
+ <Thinking tone="info" />
215
+ ) : (
216
+ <IconCheck className="size-4" />
217
+ )}
218
+ {needs ?? copy.noAction}
219
+ </span>
220
+ </div>
221
+ {/* Review is already on the line itself */}
222
+ {sorted.length && status !== "needs-you" ? (
223
+ <div className="flex flex-wrap justify-end gap-2">
224
+ {sorted.map((move) => (
225
+ <Button
226
+ key={move.id}
227
+ size="sm"
228
+ variant={move.primary ? "primary" : "secondary"}
229
+ onPress={move.onPress}
230
+ >
231
+ {move.label}
232
+ </Button>
233
+ ))}
234
+ </div>
235
+ ) : null}
236
+ </div>
237
+ ) : null}
238
+ </article>
239
+ );
240
+ }
@@ -0,0 +1,230 @@
1
+ import { useEffect, useState, type ReactNode } from "react";
2
+ import { Button } from "../../components/button.js";
3
+ import { IconAlert, IconCheck, IconHourglass, IconRotate, IconX } from "../../components/icons.js";
4
+ import { Thinking } from "../../components/thinking.js";
5
+ import { cn } from "../../lib/cn.js";
6
+ import { useInteractionCopy } from "./context.js";
7
+
8
+ /** How it went: as approved, differently, partly, not at all, not known yet, or undone */
9
+ export type ReceiptOutcome = "done" | "different" | "partial" | "failed" | "unknown" | "undone";
10
+ export type ReceiptChangeState = "changed" | "not-changed" | "unknown" | "reverted";
11
+
12
+ export interface ReceiptChange {
13
+ id: string;
14
+ /** What changed, in a word or two: "Padding" */
15
+ what: string;
16
+ /** How: "12 → 16px" */
17
+ detail: string;
18
+ state?: ReceiptChangeState;
19
+ /** Your own word for the state, such as "Held back" */
20
+ stateLabel?: string;
21
+ /** This line's own way back, available while undo is open */
22
+ back?: { label: string; onPress: () => void };
23
+ }
24
+
25
+ export interface ReceiptAction {
26
+ id: string;
27
+ label: string;
28
+ onPress: () => void;
29
+ primary?: boolean;
30
+ }
31
+
32
+ export interface ActionReceiptProps {
33
+ outcome: ReceiptOutcome;
34
+ /** What happened, as a fact with its scale: "Updated the shared Button on 34 screens" */
35
+ title: string;
36
+ /** When, and who approved it: "3:41pm · approved by you" */
37
+ meta?: string;
38
+ /** What differs from what was approved, or why: said before anything else */
39
+ note?: ReactNode;
40
+ changes: readonly ReceiptChange[];
41
+ /**
42
+ * A choice with too many answers for buttons, handed back to the person to answer in the
43
+ * conversation: "What should happen to the 3 locked screens? Reply to the build agent."
44
+ */
45
+ ask?: ReactNode;
46
+ /** Next moves such as Try again or Check what happened; they sit at the right end */
47
+ actions?: readonly ReceiptAction[];
48
+ /** When the ways back close. Shows a quiet countdown in whole minutes. */
49
+ undoClosesAt?: Date;
50
+ /** When undo closes, in words, for screen readers: "to undo, until 3:51pm" */
51
+ undoClosesLabel?: string;
52
+ /** While the host checks what happened */
53
+ checking?: boolean;
54
+ }
55
+
56
+ /** Undo closes at a set time: count the minutes quietly, never the seconds */
57
+ function useMinutesLeft(closesAt?: Date) {
58
+ const stamp = closesAt?.getTime();
59
+ const deadline = stamp !== undefined && Number.isFinite(stamp) ? stamp : undefined;
60
+ const [clock, setClock] = useState(() => ({ deadline, now: Date.now() }));
61
+ // A new host deadline must take effect immediately, before the next quiet clock tick.
62
+ if (clock.deadline !== deadline) setClock(() => ({ deadline, now: Date.now() }));
63
+ useEffect(() => {
64
+ if (deadline === undefined) return;
65
+ const update = () =>
66
+ setClock((current) =>
67
+ current.deadline === deadline ? { deadline, now: Date.now() } : current,
68
+ );
69
+ const interval = window.setInterval(update, 15_000);
70
+ // Remove ways back at the actual expiry, including between the quarter-minute ticks.
71
+ const expiry = window.setTimeout(update, Math.max(0, deadline - Date.now()));
72
+ return () => {
73
+ window.clearInterval(interval);
74
+ window.clearTimeout(expiry);
75
+ };
76
+ }, [deadline]);
77
+ return deadline === undefined ? 0 : Math.max(0, Math.ceil((deadline - clock.now) / 60_000));
78
+ }
79
+
80
+ /** The tile says the outcome at a glance; the title says it in words */
81
+ function Tile({ outcome, checking }: { outcome: ReceiptOutcome; checking: boolean }) {
82
+ const done = !checking && (outcome === "done" || outcome === "different");
83
+ return (
84
+ <span
85
+ aria-hidden="true"
86
+ className={cn(
87
+ "flex size-8 shrink-0 items-center justify-center rounded-control border",
88
+ checking && "border-line-subtle bg-surface",
89
+ done && "border-done bg-done text-on-done",
90
+ !checking &&
91
+ outcome === "partial" &&
92
+ "border-attention-line bg-attention-subtle text-attention",
93
+ !checking && outcome === "failed" && "border-error-line bg-error-subtle text-error",
94
+ !checking &&
95
+ (outcome === "unknown" || outcome === "undone") &&
96
+ "border-line-subtle bg-surface text-fg-subtle",
97
+ )}
98
+ >
99
+ {checking ? (
100
+ <Thinking />
101
+ ) : done ? (
102
+ <IconCheck className="size-4" />
103
+ ) : outcome === "partial" ? (
104
+ <IconAlert className="size-4" />
105
+ ) : outcome === "failed" ? (
106
+ <IconX className="size-4" />
107
+ ) : outcome === "unknown" ? (
108
+ <IconHourglass className="size-4" />
109
+ ) : (
110
+ <IconRotate className="size-4" />
111
+ )}
112
+ </span>
113
+ );
114
+ }
115
+
116
+ /**
117
+ * The Action Receipt: what happened after the person approved, what changed, and how to undo
118
+ * it. Each change is a line with its own way back while undo is open, and a quiet countdown
119
+ * says how long that lasts. Facts, not reassurance; the host proves what happened.
120
+ */
121
+ export function ActionReceipt({
122
+ outcome,
123
+ title,
124
+ meta,
125
+ note,
126
+ changes,
127
+ ask,
128
+ actions = [],
129
+ undoClosesAt,
130
+ undoClosesLabel,
131
+ checking = false,
132
+ }: ActionReceiptProps) {
133
+ const copy = useInteractionCopy().receipt;
134
+ const minutesLeft = useMinutesLeft(undoClosesAt);
135
+ const open = Boolean(undoClosesAt) && minutesLeft > 0 && outcome !== "undone";
136
+ const tone =
137
+ outcome === "different" || outcome === "partial"
138
+ ? "text-attention"
139
+ : outcome === "failed"
140
+ ? "text-error"
141
+ : "text-fg";
142
+ // Next moves sit at the right end, the main one furthest right
143
+ const moves = [...actions].sort(
144
+ (a, b) => Number(Boolean(a.primary)) - Number(Boolean(b.primary)),
145
+ );
146
+
147
+ return (
148
+ <article className="flex w-full min-w-0 flex-col gap-4 rounded-surface border border-line-subtle bg-surface p-5">
149
+ <p role="status" className="sr-only">
150
+ {checking ? copy.checking : title}
151
+ {!checking && undoClosesAt && !open && outcome !== "undone" ? ` ${copy.closed}` : null}
152
+ </p>
153
+ <div className="flex items-start gap-3">
154
+ <Tile outcome={outcome} checking={checking} />
155
+ <div className="flex min-w-0 flex-col gap-0.5">
156
+ <span className="type-label text-pretty text-fg-emphasis">
157
+ {checking ? copy.checking : title}
158
+ </span>
159
+ {meta ? <span className="font-mono type-caption text-fg-subtle">{meta}</span> : null}
160
+ </div>
161
+ </div>
162
+ {checking ? null : (
163
+ <>
164
+ {note ? <p className={cn("type-body text-pretty", tone)}>{note}</p> : null}
165
+ <div className="flex flex-col gap-2">
166
+ <span className="font-mono type-caption text-fg-subtle">{copy.whatChanged}</span>
167
+ <ul className="flex flex-col divide-y divide-line-subtle rounded-control border border-line-subtle bg-surface">
168
+ {changes.map((change) => {
169
+ const state = change.state ?? "changed";
170
+ return (
171
+ <li key={change.id} className="flex flex-wrap items-center gap-x-3 p-3 type-body">
172
+ <span className="font-medium text-fg-emphasis">{change.what}</span>
173
+ <span className="min-w-0 flex-1 text-fg-subtle">{change.detail}</span>
174
+ {/* The same slot on every line, so rows keep one height whatever they hold */}
175
+ <span className="flex h-control-sm shrink-0 items-center">
176
+ {state !== "changed" || change.stateLabel ? (
177
+ <span
178
+ className={cn(
179
+ "type-caption",
180
+ state === "not-changed" ? "text-attention" : "text-fg-subtle",
181
+ )}
182
+ >
183
+ {change.stateLabel ?? copy.states[state]}
184
+ </span>
185
+ ) : change.back && open ? (
186
+ <Button
187
+ size="sm"
188
+ aria-label={`${change.back.label} ${change.what.toLowerCase()}`}
189
+ onPress={() => {
190
+ if (undoClosesAt && Date.now() < undoClosesAt.getTime())
191
+ change.back?.onPress();
192
+ }}
193
+ >
194
+ {change.back.label}
195
+ </Button>
196
+ ) : null}
197
+ </span>
198
+ </li>
199
+ );
200
+ })}
201
+ </ul>
202
+ </div>
203
+ {ask ? <p className="type-body text-pretty text-fg-emphasis">{ask}</p> : null}
204
+ {moves.length || undoClosesAt ? (
205
+ <div className="flex flex-wrap items-center justify-end gap-2">
206
+ {moves.map((action) => (
207
+ <Button
208
+ key={action.id}
209
+ size="sm"
210
+ variant={action.primary ? "primary" : "secondary"}
211
+ onPress={action.onPress}
212
+ >
213
+ {action.label}
214
+ </Button>
215
+ ))}
216
+ {undoClosesAt && outcome !== "undone" ? (
217
+ <span className="font-mono type-label text-fg-subtle">
218
+ {open ? copy.minutesLeft(minutesLeft) : copy.closed}
219
+ {open && undoClosesLabel ? (
220
+ <span className="sr-only"> {undoClosesLabel}</span>
221
+ ) : null}
222
+ </span>
223
+ ) : null}
224
+ </div>
225
+ ) : null}
226
+ </>
227
+ )}
228
+ </article>
229
+ );
230
+ }
@@ -0,0 +1,206 @@
1
+ import { createContext, useContext, type ReactNode } from "react";
2
+ import { IconBookOpen, IconEdit, IconTerminal } from "../../components/icons.js";
3
+ import { cn } from "../../lib/cn.js";
4
+ import { Peek } from "../../components/peek.js";
5
+ import { Thinking } from "../../components/thinking.js";
6
+ import { useInteractionCopy } from "./context.js";
7
+
8
+ /**
9
+ * What a dot says: green when a step succeeded, grey for thoughts and the agent's words (smaller
10
+ * for thoughts), blue while waiting for the person, the pixel matrix while working.
11
+ */
12
+ export type RunDot = "success" | "note" | "thought" | "waiting" | "working";
13
+
14
+ /** Inside a run, a Decision Gate becomes the run's next entry */
15
+ export const RunContext = createContext(false);
16
+ export const useInRun = () => useContext(RunContext);
17
+
18
+ /**
19
+ * An agent's run as a timeline, like Claude Code or VS Code: one rail down the left through a
20
+ * dot for each entry. Put RunStep, RunThought, RunMessage, a DecisionGate and RunWorking inside.
21
+ */
22
+ export function RunTimeline({ label, children }: { label?: string; children: ReactNode }) {
23
+ return (
24
+ <RunContext.Provider value={true}>
25
+ <ol aria-label={label} className="run-timeline flex flex-col gap-4">
26
+ {children}
27
+ </ol>
28
+ </RunContext.Provider>
29
+ );
30
+ }
31
+
32
+ /**
33
+ * One entry: a dot and what happened. The rail runs from the first dot to the last. A text
34
+ * entry's dot sits on its first line; a card's sits level with the card's tile.
35
+ */
36
+ export function RunEntry({
37
+ dot,
38
+ card = false,
39
+ children,
40
+ }: {
41
+ dot: RunDot;
42
+ card?: boolean;
43
+ children: ReactNode;
44
+ }) {
45
+ return (
46
+ // The columns, the rail and the dot's height come from .run-entry in library.css
47
+ <li data-card={card || undefined} className="run-entry grid gap-x-3 type-body">
48
+ <span aria-hidden="true" className="run-dot flex items-center justify-center">
49
+ {dot === "working" ? (
50
+ <Thinking />
51
+ ) : (
52
+ <span
53
+ className={cn(
54
+ "relative rounded-pill",
55
+ dot === "thought" ? "size-1" : "size-1.5",
56
+ dot === "success" && "bg-success-line",
57
+ (dot === "note" || dot === "thought") && "bg-line-emphasis",
58
+ dot === "waiting" && "animate-pulse-soft bg-info",
59
+ )}
60
+ />
61
+ )}
62
+ </span>
63
+ <div className="min-w-0 type-body wrap-anywhere">{children}</div>
64
+ </li>
65
+ );
66
+ }
67
+
68
+ /** A file the agent touched */
69
+ export interface RunFile {
70
+ name: string;
71
+ path: string;
72
+ /** Its contents, or the part worth showing: opens when the name is pressed */
73
+ lines: readonly string[];
74
+ }
75
+
76
+ /** A file's name, as a link to it: ultramarine and monospaced, opening when pressed */
77
+ export function RunFileLink({ file }: { file: RunFile }) {
78
+ return (
79
+ <Peek look="file" label={file.name} title={file.path}>
80
+ <div className="flex flex-col gap-2">
81
+ <span className="font-mono type-caption text-fg-subtle">{file.path}</span>
82
+ <pre className="rounded-control bg-surface-muted px-3 py-2 font-mono type-caption whitespace-pre-wrap text-fg">
83
+ {file.lines.join("\n")}
84
+ </pre>
85
+ </div>
86
+ </Peek>
87
+ );
88
+ }
89
+
90
+ /** The icon beside what a step did, as in Claude Code */
91
+ const kindIcons = {
92
+ read: IconBookOpen,
93
+ edit: IconEdit,
94
+ run: IconTerminal,
95
+ };
96
+
97
+ /**
98
+ * A step that succeeded: an icon and what it did, to which file or with which command, and what
99
+ * came of it underneath. An edit can show how many lines changed and its first lines.
100
+ */
101
+ export function RunStep({
102
+ kind,
103
+ icon,
104
+ verb,
105
+ file,
106
+ command,
107
+ result,
108
+ added,
109
+ removed,
110
+ preview = false,
111
+ }: {
112
+ /** Picks the icon beside the verb: an open book, a pencil or a terminal */
113
+ kind?: keyof typeof kindIcons;
114
+ /** Your own icon, for any other kind of step, sized like the text */
115
+ icon?: ReactNode;
116
+ verb: string;
117
+ file?: RunFile;
118
+ command?: string;
119
+ result?: string;
120
+ added?: number;
121
+ removed?: number;
122
+ /** Show the file's first lines under the step, fading out, as VS Code does for an edit */
123
+ preview?: boolean;
124
+ }) {
125
+ const copy = useInteractionCopy().run;
126
+ const changed = added !== undefined && removed !== undefined;
127
+ const Kind = kind ? kindIcons[kind] : undefined;
128
+ const glyph = icon ?? (Kind ? <Kind className="size-3.5" /> : null);
129
+ return (
130
+ <RunEntry dot="success">
131
+ {/* The icon has its own column; the name, file, result and preview line up beside it */}
132
+ <div className="flex gap-2.5">
133
+ {glyph ? (
134
+ <span aria-hidden="true" className="flex h-5 shrink-0 items-center text-fg-subtle">
135
+ {glyph}
136
+ </span>
137
+ ) : null}
138
+ <div className="flex min-w-0 flex-1 flex-col gap-1">
139
+ <div>
140
+ <span className="run-verb font-medium text-fg-emphasis">{verb}</span>{" "}
141
+ {file ? (
142
+ <RunFileLink file={file} />
143
+ ) : command ? (
144
+ <code className="font-mono text-fg">{command}</code>
145
+ ) : null}
146
+ </div>
147
+ {changed || result ? (
148
+ <p className="type-caption text-fg-subtle">
149
+ {changed ? (
150
+ <>
151
+ <span aria-hidden="true">
152
+ <span className="text-success">+{copy.number(added)}</span>{" "}
153
+ <span className="text-error">−{copy.number(removed)}</span>
154
+ </span>
155
+ <span className="sr-only">
156
+ {copy.lines(copy.number(added), copy.number(removed))}
157
+ </span>
158
+ </>
159
+ ) : null}
160
+ {result}
161
+ </p>
162
+ ) : null}
163
+ {preview && file ? (
164
+ // The first lines, fading out; the file's name opens all of it
165
+ <div
166
+ aria-hidden="true"
167
+ className="mt-1 rounded-control border border-line-subtle bg-surface-muted px-3 py-2"
168
+ >
169
+ <pre className="run-preview-fade max-h-[3lh] overflow-hidden font-mono type-caption whitespace-pre-wrap text-fg">
170
+ {file.lines.join("\n")}
171
+ </pre>
172
+ </div>
173
+ ) : null}
174
+ </div>
175
+ </div>
176
+ </RunEntry>
177
+ );
178
+ }
179
+
180
+ /** A thought, such as "Thought for 12s": a smaller grey dot and quiet text */
181
+ export function RunThought({ children }: { children: ReactNode }) {
182
+ return (
183
+ <RunEntry dot="thought">
184
+ <span className="text-fg-subtle">{children}</span>
185
+ </RunEntry>
186
+ );
187
+ }
188
+
189
+ /** The agent, in its own words */
190
+ export function RunMessage({ children }: { children: ReactNode }) {
191
+ return (
192
+ <RunEntry dot="note">
193
+ {/* A div, not a p: the agent's words can hold a Peek, whose detail isn't phrasing */}
194
+ <div className="text-pretty text-fg">{children}</div>
195
+ </RunEntry>
196
+ );
197
+ }
198
+
199
+ /** What the agent is doing now, with the working matrix */
200
+ export function RunWorking({ children }: { children: ReactNode }) {
201
+ return (
202
+ <RunEntry dot="working">
203
+ <span className="text-fg-emphasis">{children}</span>
204
+ </RunEntry>
205
+ );
206
+ }
@@ -19,6 +19,7 @@ export { Avatar } from "../../components/avatar.js";
19
19
  export { Skeleton } from "../../components/skeleton.js";
20
20
  export { Spinner } from "../../components/spinner.js";
21
21
  export { ScrollArea } from "../../components/scroll-area.js";
22
+ export { Peek } from "../../components/peek.js";
22
23
  export { ResponsiveOverlay } from "../../components/responsive-overlay.js";
23
24
  export { DateTimeField } from "../../components/date-time-field.js";
24
25