@cueloop/client 0.1.0-alpha.15 → 0.1.0-alpha.16

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.
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@cueloop/client",
3
- "version": "0.1.0-alpha.15",
3
+ "version": "0.1.0-alpha.16",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./src/index.ts"
8
8
  },
9
9
  "dependencies": {
10
- "@cueloop/daemon": "0.1.0-alpha.15",
11
- "@cueloop/extension-api": "0.1.0-alpha.15",
12
- "@cueloop/integration-obsidian": "0.1.0-alpha.15",
13
- "@cueloop/schema": "0.1.0-alpha.15",
10
+ "@cueloop/daemon": "0.1.0-alpha.16",
11
+ "@cueloop/extension-api": "0.1.0-alpha.16",
12
+ "@cueloop/integration-obsidian": "0.1.0-alpha.16",
13
+ "@cueloop/schema": "0.1.0-alpha.16",
14
14
  "@opentui/core": "^0.5.1",
15
15
  "@opentui/keymap": "0.5.1",
16
16
  "@opentui/react": "^0.5.1",
package/src/App.tsx CHANGED
@@ -16,9 +16,10 @@
16
16
  import React, { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
17
17
  import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react";
18
18
  import type { Clock } from "@opentui/core";
19
- import { type ReviewSession, type VerdictKind } from "@cueloop/schema";
19
+ import { isAgentNote, type ReviewSession, type VerdictKind } from "@cueloop/schema";
20
20
  import { displayText, marksByDisplay, spanKey, startSpan, type Mark, type SpanState } from "./view";
21
- import { DARK } from "./theme";
21
+ import { noteForFile, viewedCount } from "./walk";
22
+ import { DARK, dimmedTheme } from "./theme";
22
23
  import { DEFAULT_KEYS, loadConfig } from "./config";
23
24
  import { returnPaneFor } from "./herdr";
24
25
  import { createReviewController } from "./session-controller";
@@ -34,6 +35,7 @@ import { VERDICTS } from "./components/ConfirmCard";
34
35
  import { CompletionOverlay } from "./components/CompletionOverlay";
35
36
  import { InboxList } from "./components/InboxList";
36
37
  import { ComposeBar } from "./components/ComposeBar";
38
+ import { WalkWizard } from "./components/WalkWizard";
37
39
 
38
40
  export interface AppProps {
39
41
  home?: string;
@@ -55,8 +57,13 @@ type Mode =
55
57
  | { m: "railEdit"; id: string; text: string }
56
58
  | { m: "submit"; verdict: VerdictKind; summary: string };
57
59
 
60
+ /** Reviewer-authored annotations only: agent notes never count as feedback. */
61
+ function reviewerAnnotations(session: ReviewSession) {
62
+ return session.annotations.filter((annotation) => !isAgentNote(annotation));
63
+ }
64
+
58
65
  function defaultVerdict(session: ReviewSession): VerdictKind {
59
- return session.annotations.length || session.workingCopy !== undefined ? "request_changes" : "approve";
66
+ return reviewerAnnotations(session).length || session.workingCopy !== undefined ? "request_changes" : "approve";
60
67
  }
61
68
 
62
69
  export function App({ home, sessionId, readOnly = false, onExit, clock }: AppProps): React.ReactNode {
@@ -69,7 +76,7 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
69
76
  controller.connect();
70
77
  return () => controller.close();
71
78
  }, [controller]);
72
- const { session, inbox, status, error, completion, editOrphanCount } = useSyncExternalStore(
79
+ const { session, inbox, status, error, completion, editOrphanCount, walk } = useSyncExternalStore(
73
80
  controller.subscribe,
74
81
  controller.getSnapshot,
75
82
  controller.getSnapshot,
@@ -127,6 +134,11 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
127
134
  const resolved = session?.status === "resolved";
128
135
  const isDiff = session?.artifact.type === "diff";
129
136
 
137
+ // ── the guided walk's view model ────────────
138
+ const walkFileList = controller.files();
139
+ const walking = isDiff && walk !== null;
140
+ const viewedPaths = useMemo(() => new Set(session?.viewedPaths ?? []), [session]);
141
+
130
142
  // driving needs committed layout, so it runs after render; any transition
131
143
  // out of span mode clears the renderer selection (compose paints its own
132
144
  // mark, and a mouse drag never changes the mode, so it survives)
@@ -267,6 +279,14 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
267
279
  const next = idx === -1 ? 0 : (idx + (intent.t === "nextAnn" ? 1 : -1) + anns.length) % anns.length;
268
280
  return void selectCardFromDocument(anns[next]!.id);
269
281
  }
282
+ case "walkStart":
283
+ return controller.walkStart();
284
+ case "walkForward":
285
+ return controller.walkForward();
286
+ case "walkBack":
287
+ return controller.walkBack();
288
+ case "walkLeave":
289
+ return controller.walkLeave();
270
290
  case "removeAnnotation":
271
291
  if (focusedAnn) {
272
292
  controller.removeAnnotation(focusedAnn);
@@ -319,7 +339,9 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
319
339
  ? "completion-prompt"
320
340
  : completion.phase === "counting"
321
341
  ? "completion-counting"
322
- : "none";
342
+ : walking
343
+ ? "walk"
344
+ : "none";
323
345
 
324
346
  useKeyboard((key) => {
325
347
  const state: KeyState = {
@@ -332,6 +354,7 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
332
354
  hasInboxItems: !!inbox?.length,
333
355
  annotationCount: session?.annotations.length ?? 0,
334
356
  hasFocusedAnnotation: focusedAnn !== undefined,
357
+ walkAtEnd: walk !== null && walk.index >= walkFileList.length,
335
358
  cursorAnnotatable: isDiff
336
359
  ? rows[cursor] !== undefined && rows[cursor]!.t !== "file" && rows[cursor]!.t !== "hunk"
337
360
  : !!display[cursor]?.work,
@@ -365,7 +388,7 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
365
388
  }
366
389
 
367
390
  const s = session!;
368
- const pendingCount = s.annotations.length + (s.workingCopy !== undefined ? 1 : 0);
391
+ const pendingCount = reviewerAnnotations(s).length + (s.workingCopy !== undefined ? 1 : 0);
369
392
 
370
393
  if ((completion.phase === "prompt" || completion.phase === "counting") && s.verdict) {
371
394
  return (
@@ -432,8 +455,13 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
432
455
  ? {
433
456
  verdict: mode.verdict,
434
457
  summary: mode.summary,
435
- annotationCount: s.annotations.length,
436
- blockingCount: s.annotations.filter(annotationBlocking).length,
458
+ annotationCount: reviewerAnnotations(s).length,
459
+ blockingCount: reviewerAnnotations(s).filter(annotationBlocking).length,
460
+ // walk coverage keeps partial passes honest at the verdict
461
+ viewedSummary:
462
+ isDiff && s.viewedPaths !== undefined
463
+ ? `${viewedCount(walkFileList, viewedPaths)}/${walkFileList.length} files viewed`
464
+ : undefined,
437
465
  onInput: (summary: string) => {
438
466
  liveInput.current = summary;
439
467
  setMode({ ...mode, summary });
@@ -475,9 +503,11 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
475
503
  ? "span"
476
504
  : mode.m === "compose" || mode.m === "railEdit"
477
505
  ? "compose"
478
- : focusedAnn !== undefined
479
- ? "card"
480
- : "normal";
506
+ : walking
507
+ ? "walk"
508
+ : focusedAnn !== undefined
509
+ ? "card"
510
+ : "normal";
481
511
  const railWidth = terminalWidth >= 100 ? 34 : 28;
482
512
 
483
513
  return (
@@ -486,7 +516,14 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
486
516
  <Breadcrumb items={headerItems} />
487
517
  <box style={{ flexGrow: 1, flexDirection: "row" }}>
488
518
  {isDiff ? (
489
- <DiffSheet rows={rows} cursor={cursor} annotations={s.annotations} focusedAnnotationId={focusedAnn} />
519
+ // the sheet dims to reading-quiet colors while the wizard has focus
520
+ <DiffSheet
521
+ rows={rows}
522
+ cursor={cursor}
523
+ annotations={s.annotations}
524
+ focusedAnnotationId={focusedAnn}
525
+ theme={walking ? dimmedTheme(theme) : undefined}
526
+ />
490
527
  ) : (
491
528
  <PlanSheet
492
529
  ref={planSheetRef}
@@ -530,6 +567,24 @@ export function App({ home, sessionId, readOnly = false, onExit, clock }: AppPro
530
567
  ) : (
531
568
  <StatusBar>{keyBindings.statusHint(hintMode)}</StatusBar>
532
569
  )}
570
+ {walking && walk !== null ? (
571
+ <WalkWizard
572
+ files={walkFileList}
573
+ index={walk.index}
574
+ viewedPaths={viewedPaths}
575
+ note={
576
+ walkFileList[walk.index] !== undefined
577
+ ? noteForFile(s.annotations, walkFileList[walk.index]!.path)
578
+ : undefined
579
+ }
580
+ terminalWidth={terminalWidth}
581
+ onSubmitRequest={() => {
582
+ dispatch({ t: "walkLeave" });
583
+ dispatch({ t: "openSubmit" });
584
+ }}
585
+ onBack={() => dispatch({ t: "walkBack" })}
586
+ />
587
+ ) : null}
533
588
  </box>
534
589
  </ThemeProvider>
535
590
  );
@@ -0,0 +1,208 @@
1
+ /**
2
+ * The guided walk in the virtual terminal (#83): the wizard steps every
3
+ * changed file with a plain progress title, ] marks viewed and persists with
4
+ * the session, esc keeps progress, w resumes at the first unviewed file
5
+ * (across App instances - the daemon round-trip), the agent-note block
6
+ * renders only for files carrying a note, and the end card hands over to the
7
+ * submit confirm with the honest viewed count.
8
+ */
9
+
10
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
11
+ import { mkdtempSync, rmSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import React from "react";
15
+ import { testRender } from "@opentui/react/test-utils";
16
+ import { DaemonServer } from "@cueloop/daemon";
17
+ import type { ReviewSession } from "@cueloop/schema";
18
+ import { App } from "./App";
19
+ import { DARK } from "./theme";
20
+ import { press, waitForState, waitForText, waitForTextGone } from "./test-support";
21
+
22
+ const PATCH = `diff --git a/src/a.ts b/src/a.ts
23
+ index 111..222 100644
24
+ --- a/src/a.ts
25
+ +++ b/src/a.ts
26
+ @@ -1,2 +1,2 @@
27
+ export const a = 1;
28
+ -export const b = 2;
29
+ +export const b = 3;
30
+ diff --git a/src/b.ts b/src/b.ts
31
+ index 333..444 100644
32
+ --- a/src/b.ts
33
+ +++ b/src/b.ts
34
+ @@ -1,2 +1,2 @@
35
+ // header
36
+ -old line
37
+ +new line
38
+ diff --git a/src/c.ts b/src/c.ts
39
+ index 555..666 100644
40
+ --- a/src/c.ts
41
+ +++ b/src/c.ts
42
+ @@ -1,1 +1,2 @@
43
+ keep
44
+ +added tail
45
+ `;
46
+
47
+ let home: string;
48
+ let server: DaemonServer;
49
+ let session: ReviewSession;
50
+
51
+ beforeEach(() => {
52
+ home = mkdtempSync(join(tmpdir(), "cueloop-walk-"));
53
+ server = new DaemonServer({ home, idleExitMs: 0 });
54
+ server.start();
55
+ session = server.core.sessionCreate({
56
+ workspace: { repoRoot: "/repo", branch: "main" },
57
+ artifact: { type: "diff", content: PATCH, meta: { title: "working tree" } },
58
+ });
59
+ });
60
+ afterEach(() => {
61
+ server.stop();
62
+ rmSync(home, { recursive: true, force: true });
63
+ });
64
+
65
+ async function renderApp() {
66
+ const setup = await testRender(<App home={home} sessionId={session.id} />, { width: 120, height: 34 });
67
+ // Wait for session content, not just the chrome: pressing w before the diff
68
+ // loads drops the walk intent (no session, no diff view) and hangs the test.
69
+ await waitForText(setup, "src/a.ts");
70
+ return setup;
71
+ }
72
+
73
+ type Setup = Awaited<ReturnType<typeof renderApp>>;
74
+
75
+ /** Foreground colors (hex) of every styled span containing the needle. */
76
+ function foregroundsOf(setup: Setup, needle: string): string[] {
77
+ const foregrounds: string[] = [];
78
+ for (const line of setup.captureSpans().lines) {
79
+ for (const span of line.spans) {
80
+ if (!span.text.includes(needle)) continue;
81
+ const [red, green, blue] = span.fg.toInts();
82
+ foregrounds.push("#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""));
83
+ }
84
+ }
85
+ return foregrounds;
86
+ }
87
+
88
+ describe("the guided walk", () => {
89
+ test("w walks all three files: progress title, persisted marks, end card, submit hand-over", async () => {
90
+ const setup = await renderApp();
91
+ await press(setup, "w");
92
+ await waitForText(setup, "file 1 of 3 · 0 viewed");
93
+ expect(setup.captureCharFrame()).toContain("src/a.ts");
94
+ expect(setup.captureCharFrame()).toContain("+1 -1");
95
+
96
+ await press(setup, "]");
97
+ await waitForText(setup, "file 2 of 3 · 1 viewed");
98
+ // the viewed mark rides the session record, not the client
99
+ await waitForState(setup, () => (server.core.sessionGet(session.id).viewedPaths ?? []).includes("src/a.ts"));
100
+
101
+ // [ steps back; the revisited card shows its viewed marker
102
+ await press(setup, "[");
103
+ await waitForText(setup, "file 1 of 3 · 1 viewed");
104
+ expect(setup.captureCharFrame()).toContain("· viewed");
105
+ await press(setup, "]");
106
+ await waitForText(setup, "file 2 of 3 · 1 viewed");
107
+
108
+ await press(setup, "]");
109
+ await waitForText(setup, "file 3 of 3 · 2 viewed");
110
+ await press(setup, "]");
111
+ await waitForText(setup, "walk complete");
112
+ expect(setup.captureCharFrame()).toContain("every file viewed (3/3)");
113
+ expect(server.core.sessionGet(session.id).viewedPaths).toEqual(["src/a.ts", "src/b.ts", "src/c.ts"]);
114
+
115
+ // the end card's return leaves the walk and opens the rail confirm
116
+ await press(setup, "enter");
117
+ await waitForText(setup, "submit review");
118
+ expect(setup.captureCharFrame()).toContain("3/3 files viewed");
119
+ await press(setup, "enter");
120
+ await waitForState(setup, () => server.core.sessionGet(session.id).status === "resolved");
121
+ });
122
+
123
+ test("esc keeps progress and w resumes at the first unviewed file", async () => {
124
+ const setup = await renderApp();
125
+ await press(setup, "w");
126
+ await waitForText(setup, "file 1 of 3 · 0 viewed");
127
+ await press(setup, "]");
128
+ await waitForText(setup, "file 2 of 3 · 1 viewed");
129
+
130
+ await press(setup, "escape");
131
+ await waitForTextGone(setup, "file 2 of 3");
132
+ // the sheet is back at full strength: no wizard card on screen
133
+ expect(setup.captureCharFrame()).not.toContain("· 1 viewed");
134
+
135
+ await press(setup, "w");
136
+ await waitForText(setup, "file 2 of 3 · 1 viewed");
137
+ });
138
+
139
+ test("a half-walked review resumes across App instances via the daemon", async () => {
140
+ const first = await renderApp();
141
+ await press(first, "w");
142
+ await waitForText(first, "file 1 of 3 · 0 viewed");
143
+ await press(first, "]");
144
+ await press(first, "]");
145
+ await waitForText(first, "file 3 of 3 · 2 viewed");
146
+ await waitForState(first, () => (server.core.sessionGet(session.id).viewedPaths ?? []).length === 2);
147
+ first.renderer.destroy();
148
+
149
+ // a fresh client reads the viewed set back from the session record
150
+ const second = await renderApp();
151
+ await press(second, "w");
152
+ await waitForText(second, "file 3 of 3 · 2 viewed");
153
+ expect(second.captureCharFrame()).toContain("src/c.ts");
154
+ // two full App boots with daemon round-trips need more than the default budget
155
+ }, 15_000);
156
+
157
+ test("the agent-note block renders only for files carrying a note", async () => {
158
+ server.core.sessionAnnotate(session.id, {
159
+ id: "note-b",
160
+ kind: "note",
161
+ anchor: { quote: "src/b.ts", prefix: "", suffix: "" },
162
+ body: "Swaps the stale line for the new constant.",
163
+ });
164
+ const setup = await renderApp();
165
+ await press(setup, "w");
166
+ await waitForText(setup, "file 1 of 3");
167
+ expect(setup.captureCharFrame()).not.toContain("agent note");
168
+
169
+ await press(setup, "]");
170
+ await waitForText(setup, "agent note");
171
+ expect(setup.captureCharFrame()).toContain("Swaps the stale line for the new constant.");
172
+
173
+ // a note is agent context, not reviewer feedback: it neither counts nor
174
+ // flips the default verdict, and never comes back in the feedback doc
175
+ await press(setup, "escape");
176
+ await waitForTextGone(setup, "agent note");
177
+ await press(setup, "enter");
178
+ await waitForText(setup, "0 annotations");
179
+ await press(setup, "enter");
180
+ await waitForState(setup, () => server.core.sessionGet(session.id).status === "resolved");
181
+ const resolved = server.core.sessionGet(session.id);
182
+ expect(resolved.verdict!.kind).toBe("approve");
183
+ expect(resolved.verdict!.feedback).not.toContain("Swaps the stale line");
184
+ });
185
+
186
+ test("the sheet dims behind the wizard; the preview keeps the diff colors", async () => {
187
+ const setup = await renderApp();
188
+ // before the walk the sheet's added line wears the insertion color
189
+ await waitForText(setup, "+new line");
190
+ expect(foregroundsOf(setup, "+new line")).toContain(DARK.insFg);
191
+
192
+ await press(setup, "w");
193
+ await waitForText(setup, "file 1 of 3 · 0 viewed");
194
+ // dimmed: the sheet line drops to the dim token...
195
+ expect(foregroundsOf(setup, "+new line")).toEqual([DARK.textDim]);
196
+ // ...while the wizard preview keeps the insertion color
197
+ expect(foregroundsOf(setup, "+export const b = 3;")).toContain(DARK.insFg);
198
+ });
199
+
200
+ test("walking a resolved review answers read-only", async () => {
201
+ server.core.sessionResolve(session.id, "approve", "");
202
+ const setup = await renderApp();
203
+ await waitForText(setup, "resolved");
204
+ await press(setup, "w");
205
+ await waitForText(setup, "review submitted - read-only");
206
+ expect(setup.captureCharFrame()).not.toContain("file 1 of 3");
207
+ });
208
+ });
@@ -20,3 +20,18 @@ export const RequestChanges: Story = {
20
20
  expectedColors: [DARK.red],
21
21
  size: { width: 40, height: 14 },
22
22
  };
23
+
24
+ export const WithViewedSummary: Story = {
25
+ render: () => (
26
+ <ConfirmCard
27
+ verdict="approve"
28
+ summary=""
29
+ annotationCount={1}
30
+ blockingCount={0}
31
+ viewedSummary="2/3 files viewed"
32
+ {...callbacks}
33
+ />
34
+ ),
35
+ expectedColors: [DARK.green, DARK.textDim],
36
+ size: { width: 40, height: 14 },
37
+ };
@@ -28,6 +28,11 @@ export interface ConfirmCardProps {
28
28
  summary: string;
29
29
  annotationCount: number;
30
30
  blockingCount: number;
31
+ /**
32
+ * The guided walk's honest coverage line for diff sessions, e.g.
33
+ * "2/3 files viewed". Undefined = no walk data, the row does not render.
34
+ */
35
+ viewedSummary?: string;
31
36
  onInput: (summary: string) => void;
32
37
  onSelectVerdict: (verdict: VerdictKind) => void;
33
38
  onSubmit: () => void;
@@ -76,6 +81,7 @@ export function ConfirmCard({
76
81
  summary,
77
82
  annotationCount,
78
83
  blockingCount,
84
+ viewedSummary,
79
85
  onInput,
80
86
  onSelectVerdict,
81
87
  onSubmit,
@@ -86,12 +92,13 @@ export function ConfirmCard({
86
92
  return (
87
93
  <Card
88
94
  title=" submit review "
89
- contentRows={CONFIRM_CONTENT_ROWS}
95
+ contentRows={CONFIRM_CONTENT_ROWS + (viewedSummary !== undefined ? 1 : 0)}
90
96
  borderColor={tokens.accent}
91
97
  marginRight={1}
92
98
  theme={theme}
93
99
  >
94
100
  <text fg={tokens.textDim}>{`${annotationCount} annotations · ${blockingCount} blocking`}</text>
101
+ {viewedSummary !== undefined ? <text fg={tokens.textDim}>{viewedSummary}</text> : null}
95
102
  <box style={{ height: 1 }} />
96
103
  <VerdictSelector verdict={verdict} onSelectVerdict={onSelectVerdict} theme={theme} />
97
104
  <box style={{ height: 1 }} />
@@ -0,0 +1,89 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { WalkFile } from "../walk";
4
+ import type { Story, StoryMeta } from "./story";
5
+ import { WalkWizard } from "./WalkWizard";
6
+
7
+ export const meta: StoryMeta = { title: "WalkWizard" };
8
+
9
+ const FILES: WalkFile[] = [
10
+ {
11
+ path: "packages/daemon/src/store.ts",
12
+ added: 12,
13
+ removed: 3,
14
+ preview: [
15
+ { sign: "+", text: "const viewed = new Set<string>();" },
16
+ { sign: "+", text: "record.viewedPaths = [...viewed];" },
17
+ { sign: "-", text: "// viewed tracking lives in the client only" },
18
+ ],
19
+ },
20
+ {
21
+ path: "packages/client/src/walk.ts",
22
+ added: 48,
23
+ removed: 0,
24
+ preview: [{ sign: "+", text: "export function firstUnviewedIndex(files, viewed) {" }],
25
+ },
26
+ { path: "packages/schema/src/types.ts", added: 3, removed: 0, preview: [{ sign: "+", text: "viewedPaths?: string[];" }] },
27
+ ];
28
+
29
+ const noop = (): void => {};
30
+
31
+ export const FirstFile: Story = {
32
+ render: () => (
33
+ <WalkWizard
34
+ files={FILES}
35
+ index={0}
36
+ viewedPaths={new Set()}
37
+ terminalWidth={80}
38
+ onSubmitRequest={noop}
39
+ onBack={noop}
40
+ />
41
+ ),
42
+ // the main card wears the accent border; stats carry the +/- colors
43
+ expectedColors: [DARK.accent, DARK.green, DARK.red, DARK.insFg, DARK.delFg],
44
+ };
45
+
46
+ export const WithAgentNote: Story = {
47
+ render: () => (
48
+ <WalkWizard
49
+ files={FILES}
50
+ index={1}
51
+ viewedPaths={new Set(["packages/daemon/src/store.ts"])}
52
+ note="New module: pure helpers that pick the next unviewed file. No IO, unit tested."
53
+ terminalWidth={80}
54
+ onSubmitRequest={noop}
55
+ onBack={noop}
56
+ />
57
+ ),
58
+ // the agent-note block wears the plain gray border token, not the accent
59
+ expectedColors: [DARK.accent, DARK.border, DARK.textMuted],
60
+ };
61
+
62
+ export const EndCard: Story = {
63
+ render: () => (
64
+ <WalkWizard
65
+ files={FILES}
66
+ index={3}
67
+ viewedPaths={new Set(FILES.map((file) => file.path))}
68
+ terminalWidth={80}
69
+ onSubmitRequest={noop}
70
+ onBack={noop}
71
+ />
72
+ ),
73
+ // the end card wears the green border and the solid submit button
74
+ expectedColors: [DARK.green, DARK.accent, DARK.accentInk],
75
+ };
76
+
77
+ export const EndCardPartialPass: Story = {
78
+ render: () => (
79
+ <WalkWizard
80
+ files={FILES}
81
+ index={3}
82
+ viewedPaths={new Set(["packages/daemon/src/store.ts"])}
83
+ terminalWidth={80}
84
+ onSubmitRequest={noop}
85
+ onBack={noop}
86
+ />
87
+ ),
88
+ expectedColors: [DARK.green],
89
+ };