@cueloop/client 0.1.0-alpha.32 → 0.1.0-alpha.34

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 (101) hide show
  1. package/package.json +27 -26
  2. package/src/App.autoclose.test.tsx +7 -4
  3. package/src/App.diff.test.tsx +131 -5
  4. package/src/App.export.test.tsx +23 -5
  5. package/src/App.plan-review.test.tsx +133 -32
  6. package/src/App.readonly.test.tsx +35 -13
  7. package/src/App.rendering.test.tsx +14 -4
  8. package/src/App.review-panel.test.tsx +27 -15
  9. package/src/App.submit-confirm.test.tsx +14 -13
  10. package/src/App.test.tsx +50 -13
  11. package/src/App.toast.test.tsx +75 -0
  12. package/src/App.tsx +505 -101
  13. package/src/App.walk.test.tsx +31 -11
  14. package/src/attribution.ts +8 -2
  15. package/src/clipboard.ts +5 -1
  16. package/src/components/AnnotationCard.stories.tsx +29 -10
  17. package/src/components/AnnotationCard.tsx +57 -37
  18. package/src/components/Breadcrumb.tsx +7 -1
  19. package/src/components/CodeBlock.stories.tsx +19 -2
  20. package/src/components/CodeBlock.tsx +24 -9
  21. package/src/components/CompletionOverlay.tsx +13 -3
  22. package/src/components/ConfirmCard.stories.tsx +22 -3
  23. package/src/components/ConfirmCard.tsx +23 -5
  24. package/src/components/ConfirmDialog.tsx +10 -2
  25. package/src/components/DiffSheet.scroll.test.tsx +123 -0
  26. package/src/components/DiffSheet.stories.tsx +35 -1
  27. package/src/components/DiffSheet.syntax.test.tsx +62 -0
  28. package/src/components/DiffSheet.tsx +213 -101
  29. package/src/components/InboxList.stories.tsx +16 -3
  30. package/src/components/InboxList.tsx +17 -10
  31. package/src/components/KeybindsDialog.stories.tsx +31 -0
  32. package/src/components/KeybindsDialog.tsx +58 -0
  33. package/src/components/MarkerPopover.stories.tsx +58 -0
  34. package/src/components/MarkerPopover.tsx +102 -0
  35. package/src/components/MenuBar.stories.tsx +37 -0
  36. package/src/components/MenuBar.tsx +74 -0
  37. package/src/components/PlanSheet.stories.tsx +35 -3
  38. package/src/components/PlanSheet.tsx +160 -100
  39. package/src/components/PromptDialog.tsx +19 -3
  40. package/src/components/ReviewPanel.stories.tsx +17 -19
  41. package/src/components/ReviewPanel.tsx +17 -32
  42. package/src/components/ReviewRail.stories.tsx +159 -47
  43. package/src/components/ReviewRail.tsx +206 -62
  44. package/src/components/SettingsDialog.stories.tsx +51 -3
  45. package/src/components/SettingsDialog.tsx +11 -5
  46. package/src/components/SettingsRows.stories.tsx +1 -1
  47. package/src/components/SettingsRows.tsx +19 -3
  48. package/src/components/Toast.stories.tsx +12 -0
  49. package/src/components/Toast.tsx +44 -0
  50. package/src/components/WalkWizard.stories.tsx +13 -2
  51. package/src/components/WalkWizard.tsx +9 -3
  52. package/src/components/__snapshots__/stories.test.tsx.snap +853 -623
  53. package/src/components/diff-sheet-layout.test.ts +127 -0
  54. package/src/components/diff-sheet-layout.ts +167 -0
  55. package/src/components/primitives/Button.stories.tsx +2 -2
  56. package/src/components/primitives/Button.tsx +5 -1
  57. package/src/components/primitives/Dialog.tsx +12 -1
  58. package/src/components/primitives/Tabs.tsx +40 -39
  59. package/src/components/stories-app.tsx +34 -6
  60. package/src/components/story-fixtures.ts +11 -3
  61. package/src/components/story.ts +3 -1
  62. package/src/components/syntax-highlight.ts +47 -12
  63. package/src/components/theme-context.stories.tsx +6 -1
  64. package/src/components/theme-context.tsx +7 -1
  65. package/src/config.test.ts +178 -2
  66. package/src/config.ts +121 -23
  67. package/src/diff-hunk-curate.test.ts +315 -0
  68. package/src/diff-hunk-curate.ts +233 -0
  69. package/src/diff-intraline.test.ts +164 -0
  70. package/src/diff-intraline.ts +132 -0
  71. package/src/diff-syntax.test.ts +96 -0
  72. package/src/diff-syntax.ts +154 -0
  73. package/src/editor.test.ts +13 -4
  74. package/src/editor.ts +31 -5
  75. package/src/herdr.ts +8 -2
  76. package/src/index.ts +9 -1
  77. package/src/integrations.ts +3 -1
  78. package/src/intent-dispatch.test.ts +193 -9
  79. package/src/intent-dispatch.ts +151 -18
  80. package/src/key-bindings.test.ts +19 -9
  81. package/src/key-bindings.ts +77 -16
  82. package/src/keymap.test.ts +171 -26
  83. package/src/keymap.ts +83 -16
  84. package/src/review-panel.test.ts +3 -1
  85. package/src/review-panel.ts +9 -2
  86. package/src/serve.test.ts +13 -5
  87. package/src/session-controller.curate.test.ts +253 -0
  88. package/src/session-controller.share.test.ts +77 -16
  89. package/src/session-controller.ts +319 -26
  90. package/src/share.ts +41 -8
  91. package/src/test-support.ts +97 -23
  92. package/src/theme-presets.ts +168 -0
  93. package/src/theme.ts +1 -3
  94. package/src/version.ts +5 -0
  95. package/src/view-diff.ts +15 -4
  96. package/src/view-plan.test.ts +40 -6
  97. package/src/view-plan.ts +71 -28
  98. package/src/walk.test.ts +7 -2
  99. package/src/walk.ts +7 -2
  100. package/src/components/ComposeBar.stories.tsx +0 -18
  101. package/src/components/ComposeBar.tsx +0 -54
@@ -10,7 +10,13 @@ import { DaemonServer } from "@cueloop/daemon";
10
10
  import type { ReviewSession } from "@cueloop/schema";
11
11
  import { App } from "./App";
12
12
  import { DARK } from "./theme";
13
- import { isolateUserConfig, press, waitForState, waitForText, waitForTextGone } from "./test-support";
13
+ import {
14
+ isolateUserConfig,
15
+ press,
16
+ waitForState,
17
+ waitForText,
18
+ waitForTextGone,
19
+ } from "./test-support";
14
20
 
15
21
  const PATCH = `diff --git a/src/a.ts b/src/a.ts
16
22
  index 111..222 100644
@@ -59,7 +65,10 @@ afterEach(() => {
59
65
  });
60
66
 
61
67
  async function renderApp() {
62
- const setup = await testRender(<App home={home} sessionId={session.id} />, { width: 120, height: 34 });
68
+ const setup = await testRender(<App home={home} sessionId={session.id} />, {
69
+ width: 120,
70
+ height: 34,
71
+ });
63
72
  // Wait for session content, not just the chrome: pressing w before the diff
64
73
  // loads drops the walk intent (no session, no diff view) and hangs the test.
65
74
  await waitForText(setup, "src/a.ts");
@@ -75,7 +84,9 @@ function foregroundsOf(setup: Setup, needle: string): string[] {
75
84
  for (const span of line.spans) {
76
85
  if (!span.text.includes(needle)) continue;
77
86
  const [red, green, blue] = span.fg.toInts();
78
- foregrounds.push("#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""));
87
+ foregrounds.push(
88
+ "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""),
89
+ );
79
90
  }
80
91
  }
81
92
  return foregrounds;
@@ -100,7 +111,9 @@ describe("the guided walk", () => {
100
111
  // Assert
101
112
  await waitForText(setup, "file 2 of 3 · 1 viewed");
102
113
  // the viewed mark rides the session record, not the client
103
- await waitForState(setup, () => (server.core.sessionGet(session.id).viewedPaths ?? []).includes("src/a.ts"));
114
+ await waitForState(setup, () =>
115
+ (server.core.sessionGet(session.id).viewedPaths ?? []).includes("src/a.ts"),
116
+ );
104
117
 
105
118
  // Act
106
119
  // [ steps back; the revisited card shows its viewed marker
@@ -128,7 +141,11 @@ describe("the guided walk", () => {
128
141
  // Assert
129
142
  await waitForText(setup, "walk complete");
130
143
  expect(setup.captureCharFrame()).toContain("every file viewed (3/3)");
131
- expect(server.core.sessionGet(session.id).viewedPaths).toEqual(["src/a.ts", "src/b.ts", "src/c.ts"]);
144
+ expect(server.core.sessionGet(session.id).viewedPaths).toEqual([
145
+ "src/a.ts",
146
+ "src/b.ts",
147
+ "src/c.ts",
148
+ ]);
132
149
 
133
150
  // Act
134
151
  // the end card's return leaves the walk and opens the rail confirm
@@ -176,7 +193,10 @@ describe("the guided walk", () => {
176
193
  await press(first, "]");
177
194
  await press(first, "]");
178
195
  await waitForText(first, "file 3 of 3 · 2 viewed");
179
- await waitForState(first, () => (server.core.sessionGet(session.id).viewedPaths ?? []).length === 2);
196
+ await waitForState(
197
+ first,
198
+ () => (server.core.sessionGet(session.id).viewedPaths ?? []).length === 2,
199
+ );
180
200
  first.renderer.destroy();
181
201
 
182
202
  // Act
@@ -190,7 +210,7 @@ describe("the guided walk", () => {
190
210
  // two full App boots with daemon round-trips need more than the default
191
211
  // budget, and the whole-suite publish lane has timed even 15s out; give the
192
212
  // double-boot chain generous headroom so runner load cannot flake it.
193
- }, 30_000);
213
+ }, 60_000);
194
214
 
195
215
  test("the agent-note block renders only for files carrying a note", async () => {
196
216
  // Arrange
@@ -237,17 +257,17 @@ describe("the guided walk", () => {
237
257
  const setup = await renderApp();
238
258
 
239
259
  // Assert
240
- // before the walk the sheet's added line wears the insertion color
260
+ // before the walk the changed word on the added line wears the insertion color
241
261
  await waitForText(setup, "+new line");
242
- expect(foregroundsOf(setup, "+new line")).toContain(DARK.insertedForeground);
262
+ expect(foregroundsOf(setup, "new")).toContain(DARK.insertedForeground);
243
263
 
244
264
  // Act
245
265
  await press(setup, "w");
246
266
 
247
267
  // Assert
248
268
  await waitForText(setup, "file 1 of 3 · 0 viewed");
249
- // dimmed: the sheet line drops to the dim token...
250
- expect(foregroundsOf(setup, "+new line")).toEqual([DARK.textDim]);
269
+ // dimmed: the sheet's changed word drops to the dim token...
270
+ expect(foregroundsOf(setup, "new")).toEqual([DARK.textDim]);
251
271
  // ...while the wizard preview keeps the insertion color
252
272
  expect(foregroundsOf(setup, "+export const b = 3;")).toContain(DARK.insertedForeground);
253
273
  });
@@ -13,7 +13,9 @@ export const ANONYMOUS_LABEL = "-- anonymous --";
13
13
 
14
14
  /** A short, stable handle from a raw author id when no name is known. */
15
15
  export function shortHandle(authorId: string): string {
16
- const base = authorId.startsWith(FINGERPRINT_PREFIX) ? authorId.slice(FINGERPRINT_PREFIX.length) : authorId;
16
+ const base = authorId.startsWith(FINGERPRINT_PREFIX)
17
+ ? authorId.slice(FINGERPRINT_PREFIX.length)
18
+ : authorId;
17
19
  return base.slice(0, 8) || authorId;
18
20
  }
19
21
 
@@ -22,7 +24,11 @@ export function shortHandle(authorId: string): string {
22
24
  * own name, then anonymous for a known-but-unnamed participant, then a short
23
25
  * handle for an id we hold no identity for.
24
26
  */
25
- export function resolveDisplayName(authorId: string, participants: Identity[] | undefined, overrides: Record<string, string>): string {
27
+ export function resolveDisplayName(
28
+ authorId: string,
29
+ participants: Identity[] | undefined,
30
+ overrides: Record<string, string>,
31
+ ): string {
26
32
  const override = overrides[authorId];
27
33
  if (override) return override;
28
34
  const identity = participants?.find((participant) => participant.id === authorId);
package/src/clipboard.ts CHANGED
@@ -16,7 +16,11 @@ export function clipboardCommands(platform: NodeJS.Platform = process.platform):
16
16
  export async function copyToClipboard(text: string): Promise<boolean> {
17
17
  for (const command of clipboardCommands()) {
18
18
  try {
19
- const proc = Bun.spawn(command, { stdin: Buffer.from(text), stdout: "ignore", stderr: "ignore" });
19
+ const proc = Bun.spawn(command, {
20
+ stdin: Buffer.from(text),
21
+ stdout: "ignore",
22
+ stderr: "ignore",
23
+ });
20
24
  if ((await proc.exited) === 0) return true;
21
25
  } catch {
22
26
  // tool not installed - try the next one
@@ -9,34 +9,51 @@ const draftCallbacks = { onInput: () => {}, onSave: () => {}, onCancel: () => {}
9
9
 
10
10
  export const CommentDraft: Story = {
11
11
  render: () => (
12
- <AnnotationCard kind="comment" quote="persists sessions" draft={{ text: "Which daemon?", ...draftCallbacks }} />
12
+ <AnnotationCard
13
+ kind="comment"
14
+ quote="persists sessions"
15
+ draft={{ text: "Which daemon?", ...draftCallbacks }}
16
+ />
13
17
  ),
14
- expectedColors: [DARK.accent, DARK.elevated],
18
+ expectedColors: [DARK.accent],
15
19
  };
16
20
 
17
- export const SuggestionDraft: Story = {
21
+ export const SavedSelected: Story = {
18
22
  render: () => (
19
- <AnnotationCard kind="suggestion" quote="move the store" draft={{ text: "", ...draftCallbacks }} />
23
+ <AnnotationCard
24
+ kind="comment"
25
+ quote="persists sessions"
26
+ saved={{
27
+ body: "Which daemon owns this?",
28
+ isSelected: true,
29
+ isOrphan: false,
30
+ isBlocking: false,
31
+ author: "me",
32
+ editing: null,
33
+ onPress: () => {},
34
+ }}
35
+ />
20
36
  ),
21
- expectedColors: [DARK.green],
37
+ expectedColors: [DARK.accent],
22
38
  };
23
39
 
24
- export const SavedSelected: Story = {
40
+ export const SavedCollaborator: Story = {
25
41
  render: () => (
26
42
  <AnnotationCard
27
43
  kind="comment"
28
44
  quote="persists sessions"
29
45
  saved={{
30
46
  body: "Which daemon owns this?",
31
- isSelected: true,
47
+ isSelected: false,
32
48
  isOrphan: false,
33
49
  isBlocking: false,
50
+ author: "Priya",
34
51
  editing: null,
35
52
  onPress: () => {},
36
53
  }}
37
54
  />
38
55
  ),
39
- expectedColors: [DARK.elevated],
56
+ expectedColors: [DARK.border],
40
57
  };
41
58
 
42
59
  export const SavedOrphanBlocking: Story = {
@@ -49,24 +66,26 @@ export const SavedOrphanBlocking: Story = {
49
66
  isSelected: false,
50
67
  isOrphan: true,
51
68
  isBlocking: true,
69
+ author: "me",
52
70
  editing: null,
53
71
  onPress: () => {},
54
72
  }}
55
73
  />
56
74
  ),
57
- expectedColors: [DARK.red],
75
+ expectedColors: [DARK.border],
58
76
  };
59
77
 
60
78
  export const SavedEditing: Story = {
61
79
  render: () => (
62
80
  <AnnotationCard
63
- kind="suggestion"
81
+ kind="comment"
64
82
  quote="move the store"
65
83
  saved={{
66
84
  body: "old body",
67
85
  isSelected: true,
68
86
  isOrphan: false,
69
87
  isBlocking: false,
88
+ author: "me",
70
89
  editing: { text: "new body", ...draftCallbacks },
71
90
  onPress: () => {},
72
91
  }}
@@ -32,8 +32,8 @@ export interface AnnotationSaved {
32
32
  isSelected: boolean;
33
33
  isOrphan: boolean;
34
34
  isBlocking: boolean;
35
- /** Resolved author display name; own notes carry none. Set = name-in-border. */
36
- authorLabel?: string;
35
+ /** Border-title author: a collaborator's display name, or "me" for the reviewer's own note. */
36
+ author: string;
37
37
  /** Non-null while the card body is being rewritten in place. */
38
38
  editing: AnnotationDraft | null;
39
39
  onPress: () => void;
@@ -74,7 +74,11 @@ export const COMPOSE_MAX_ROWS = 4;
74
74
  * newlines only. Beyond the cap the textarea scrolls internally and keeps the
75
75
  * caret line in view.
76
76
  */
77
- export function composeRowCount(text: string, contentWidth: number, cap: number = COMPOSE_MAX_ROWS): number {
77
+ export function composeRowCount(
78
+ text: string,
79
+ contentWidth: number,
80
+ cap: number = COMPOSE_MAX_ROWS,
81
+ ): number {
78
82
  const usableWidth = contentWidth > 0 ? contentWidth : Number.MAX_SAFE_INTEGER;
79
83
  let visualRowCount = 0;
80
84
  for (const line of text.split("\n")) {
@@ -105,7 +109,7 @@ function DraftEditor({
105
109
  editor.cursorOffset = initialTextLength.current;
106
110
  // the box is laid out now, so its width is known: size to the wrapped body
107
111
  onRowsChange(composeRowCount(editor.plainText, editor.width));
108
- }, []);
112
+ }, [onRowsChange]);
109
113
  return (
110
114
  <>
111
115
  <textarea
@@ -122,27 +126,34 @@ function DraftEditor({
122
126
  }}
123
127
  style={{
124
128
  height: rows,
125
- backgroundColor: tokens.elevated,
126
- focusedBackgroundColor: tokens.elevated,
129
+ backgroundColor: "transparent",
130
+ focusedBackgroundColor: "transparent",
127
131
  textColor: tokens.text,
128
132
  focusedTextColor: tokens.text,
129
133
  }}
130
134
  />
131
135
  <Toolbar>
132
136
  <Button variant="solid" marginRight={2} onPress={draft.onSave} theme={theme}>
133
- {" Save ⏎ "}
137
+ {" Save "}
134
138
  </Button>
135
139
  <Button onPress={draft.onCancel} theme={theme}>
136
- {" Cancel esc "}
140
+ {" Cancel "}
137
141
  </Button>
138
142
  </Toolbar>
139
143
  </>
140
144
  );
141
145
  }
142
146
 
143
- export function AnnotationCard({ id, kind, quote, draft, saved, theme }: AnnotationCardProps): React.ReactNode {
147
+ export function AnnotationCard({
148
+ id,
149
+ kind,
150
+ quote,
151
+ draft,
152
+ saved,
153
+ theme,
154
+ }: AnnotationCardProps): React.ReactNode {
144
155
  const tokens = useComponentTheme(theme);
145
- const kindColor = kind === "suggestion" ? tokens.green : tokens.accent;
156
+ const kindColor = tokens.accent;
146
157
  const activeDraft = draft ?? saved?.editing ?? null;
147
158
  // The composer's visible height, grown from the wrapped text. It is shared
148
159
  // with the card frame (contentRows) so the border never clips the textarea
@@ -154,54 +165,63 @@ export function AnnotationCard({ id, kind, quote, draft, saved, theme }: Annotat
154
165
  composeRowCount(activeDraft?.text ?? "", 0),
155
166
  );
156
167
  if (draft) {
157
- const verb = kind === "suggestion" ? "suggest replacement for" : "comment on";
168
+ const verb = "comment on";
158
169
  return (
159
170
  <Card
160
171
  title={` ${verb} "${truncateToSingleLine(quote, 40)}" `}
161
172
  contentRows={editorRowCount + 1}
162
173
  borderColor={kindColor}
163
- marginLeft={2}
164
- marginRight={2}
174
+ backgroundColor="transparent"
165
175
  theme={theme}
166
176
  >
167
- <DraftEditor draft={draft} rows={editorRowCount} onRowsChange={setEditorRowCount} theme={theme} />
177
+ <DraftEditor
178
+ draft={draft}
179
+ rows={editorRowCount}
180
+ onRowsChange={setEditorRowCount}
181
+ theme={theme}
182
+ />
168
183
  </Card>
169
184
  );
170
185
  }
171
186
  const card = saved!;
172
- // A collaborator's note (author set) renders as a named, bordered card; own notes stay borderless.
173
- const authored = card.authorLabel !== undefined && card.authorLabel !== "";
174
- const indent = authored ? "" : " ";
175
- const marker = card.isSelected ? "▸ " : indent;
176
- // Fixed content rows (a bordered box must declare a height or it collapses): header + quote + body, or the editor.
177
- const contentRows = card.editing ? editorRowCount + 3 : 3;
178
- // The border + its side padding eat this much content width, so text truncates shorter.
179
- const borderInset = authored ? 4 : 0;
187
+ // border = "what + who" (+ any exceptional flag); body = the content only
188
+ const flags = [card.isBlocking ? "BLOCKING" : null, card.isOrphan ? "ORPHANED" : null].filter(
189
+ (flag): flag is string => flag !== null,
190
+ );
191
+ const title = ` ${[kind.toUpperCase(), card.author, ...flags].join(" · ")} `;
192
+ // every saved card is bordered, so the border + side padding always eat this width
193
+ const borderInset = 4;
194
+ // a bordered box must declare a height or it collapses: quote + body, or the editor
195
+ const contentRows = card.editing ? editorRowCount + 2 : 2;
180
196
  return (
181
197
  <box
182
198
  id={id}
183
- title={authored ? ` ${card.authorLabel} ` : undefined}
199
+ title={title}
184
200
  style={{
185
201
  flexDirection: "column",
186
- marginBottom: 1,
187
- backgroundColor: card.isSelected ? tokens.elevated : undefined,
188
- ...(authored
189
- ? { height: cardHeight(contentRows), border: true, borderStyle: FRAME_BORDER_STYLE, borderColor: tokens.blue, paddingLeft: 1, paddingRight: 1 }
190
- : {}),
202
+ height: cardHeight(contentRows),
203
+ border: true,
204
+ borderStyle: FRAME_BORDER_STYLE,
205
+ // cards stay transparent in every theme; selection is the bright
206
+ // kind-colored border, while unselected cards recede to a dim border
207
+ borderColor: card.isSelected || card.editing ? kindColor : tokens.border,
208
+ paddingLeft: 1,
209
+ paddingRight: 1,
191
210
  }}
192
211
  onMouseUp={card.onPress}
193
212
  >
194
- <text fg={card.isSelected ? tokens.text : kindColor}>
195
- {marker}
196
- {kind.toUpperCase()}
197
- {card.isBlocking ? <span fg={tokens.red}> · BLOCKING</span> : null}
198
- <span fg={tokens.textDim}>{card.isOrphan ? " · ORPHANED" : " · pending"}</span>
199
- </text>
200
- <text fg={tokens.textDim}>{indent}"{truncateToSingleLine(quote, 26 - borderInset)}"</text>
213
+ <text fg={tokens.textDim}>"{truncateToSingleLine(quote, 26 - borderInset)}"</text>
201
214
  {card.editing ? (
202
- <DraftEditor draft={card.editing} rows={editorRowCount} onRowsChange={setEditorRowCount} theme={theme} />
215
+ <DraftEditor
216
+ draft={card.editing}
217
+ rows={editorRowCount}
218
+ onRowsChange={setEditorRowCount}
219
+ theme={theme}
220
+ />
203
221
  ) : (
204
- <text fg={card.isOrphan ? tokens.textDim : tokens.textMuted}>{indent}{truncateToSingleLine(card.body, 28 - borderInset)}</text>
222
+ <text fg={card.isOrphan ? tokens.textDim : tokens.textMuted}>
223
+ {truncateToSingleLine(card.body, 28 - borderInset)}
224
+ </text>
205
225
  )}
206
226
  </box>
207
227
  );
@@ -23,7 +23,13 @@ export interface BreadcrumbProps {
23
23
  export function Breadcrumb({ items, theme }: BreadcrumbProps): React.ReactNode {
24
24
  const tokens = useComponentTheme(theme);
25
25
  const toneColor = (tone?: BreadcrumbTone): string =>
26
- tone === "accent" ? tokens.accent : tone === "green" ? tokens.green : tone === "text" ? tokens.text : tokens.textDim;
26
+ tone === "accent"
27
+ ? tokens.accent
28
+ : tone === "green"
29
+ ? tokens.green
30
+ : tone === "text"
31
+ ? tokens.text
32
+ : tokens.textDim;
27
33
  return (
28
34
  <box style={{ height: 1, backgroundColor: tokens.panel, paddingLeft: 1, flexDirection: "row" }}>
29
35
  <text fg={tokens.text}>
@@ -16,10 +16,27 @@ export const Typescript: Story = {
16
16
  };
17
17
 
18
18
  export const CursorAnnotatedEdited: Story = {
19
- render: () => <CodeBlock language="ts" content={SNIPPET} isCursor isAnnotated changeTag="edited" marginTop={1} />,
19
+ render: () => (
20
+ <CodeBlock
21
+ language="ts"
22
+ content={SNIPPET}
23
+ isCursor
24
+ isAnnotated
25
+ changeTag="edited"
26
+ marginTop={1}
27
+ />
28
+ ),
20
29
  expectedColors: [DARK.accent],
21
30
  };
22
31
 
23
32
  export const UnknownLanguage: Story = {
24
- render: () => <CodeBlock language="brainfog" content={"plain verbatim lines\n with indentation"} />,
33
+ render: () => (
34
+ <CodeBlock language="brainfog" content={"plain verbatim lines\n with indentation"} />
35
+ ),
36
+ };
37
+
38
+ /** A cut code block: struck-through gray content, no [cut] tag. */
39
+ export const Cut: Story = {
40
+ render: () => <CodeBlock language="ts" content={SNIPPET} cut />,
41
+ expectedColors: [DARK.elevated],
25
42
  };
@@ -6,10 +6,14 @@
6
6
  */
7
7
 
8
8
  import React from "react";
9
+ import { createTextAttributes } from "@opentui/core";
9
10
  import type { Theme } from "../theme";
10
11
  import { useComponentTheme } from "./theme-context";
11
12
  import { filetypeFor, syntaxStyleFor } from "./syntax-highlight";
12
13
 
14
+ /** A cut code block reads as removed: struck through and grayed, never red. */
15
+ const CUT_ATTRIBUTES = createTextAttributes({ strikethrough: true, dim: true });
16
+
13
17
  export interface CodeBlockProps {
14
18
  id?: string;
15
19
  /** Markdown fence info string ("tsx", "python", ...). */
@@ -20,7 +24,9 @@ export interface CodeBlockProps {
20
24
  marginTop?: number;
21
25
  isAnnotated?: boolean;
22
26
  /** Working-copy change tag rendered after the language label. */
23
- changeTag?: "cut" | "new" | "edited";
27
+ changeTag?: "new" | "edited";
28
+ /** A cut (removed) block: struck-through gray content, no tag. */
29
+ cut?: boolean;
24
30
  theme?: Theme;
25
31
  }
26
32
 
@@ -32,10 +38,11 @@ export function CodeBlock({
32
38
  marginTop = 0,
33
39
  isAnnotated = false,
34
40
  changeTag,
41
+ cut = false,
35
42
  theme,
36
43
  }: CodeBlockProps): React.ReactNode {
37
44
  const tokens = useComponentTheme(theme);
38
- const tagColor = changeTag === "cut" ? tokens.red : changeTag === "new" ? tokens.green : tokens.accent;
45
+ const tagColor = changeTag === "new" ? tokens.green : tokens.accent;
39
46
  return (
40
47
  <box id={id} style={{ flexDirection: "column", marginTop }}>
41
48
  <text>
@@ -55,13 +62,21 @@ export function CodeBlock({
55
62
  marginLeft: 2,
56
63
  }}
57
64
  >
58
- <code
59
- content={content}
60
- filetype={filetypeFor(language)}
61
- syntaxStyle={syntaxStyleFor(tokens)}
62
- selectable={false}
63
- style={{ wrapMode: "none", fg: tokens.textMuted, bg: tokens.elevated }}
64
- />
65
+ {cut ? (
66
+ <text style={{ wrapMode: "none" }} selectable={false}>
67
+ <span fg={tokens.textDim} attributes={CUT_ATTRIBUTES}>
68
+ {content}
69
+ </span>
70
+ </text>
71
+ ) : (
72
+ <code
73
+ content={content}
74
+ filetype={filetypeFor(language)}
75
+ syntaxStyle={syntaxStyleFor(tokens)}
76
+ selectable={false}
77
+ style={{ wrapMode: "none", fg: tokens.textMuted, bg: tokens.elevated }}
78
+ />
79
+ )}
65
80
  </box>
66
81
  </box>
67
82
  );
@@ -19,7 +19,13 @@ export interface CompletionOverlayProps {
19
19
  theme?: Theme;
20
20
  }
21
21
 
22
- export function CompletionOverlay({ verdict, completion, status, returnsTo, theme }: CompletionOverlayProps): React.ReactNode {
22
+ export function CompletionOverlay({
23
+ verdict,
24
+ completion,
25
+ status,
26
+ returnsTo,
27
+ theme,
28
+ }: CompletionOverlayProps): React.ReactNode {
23
29
  const tokens = useComponentTheme(theme);
24
30
  const approved = verdict === "approve";
25
31
  return (
@@ -33,9 +39,13 @@ export function CompletionOverlay({ verdict, completion, status, returnsTo, them
33
39
  alignItems: "center",
34
40
  }}
35
41
  >
36
- <text fg={approved ? tokens.green : tokens.accent}>{approved ? "review approved" : "feedback sent"}</text>
42
+ <text fg={approved ? tokens.green : tokens.accent}>
43
+ {approved ? "review approved" : "feedback sent"}
44
+ </text>
37
45
  <text> </text>
38
- <text fg={tokens.text}>The agent has your {approved ? "approval" : "feedback"} and is unblocked.</text>
46
+ <text fg={tokens.text}>
47
+ The agent has your {approved ? "approval" : "feedback"} and is unblocked.
48
+ </text>
39
49
  {returnsTo ? <text fg={tokens.textDim}>returning to {returnsTo} on close</text> : null}
40
50
  {status ? <text fg={tokens.textDim}>{status}</text> : null}
41
51
  <text> </text>
@@ -5,17 +5,36 @@ import { ConfirmCard } from "./ConfirmCard";
5
5
 
6
6
  export const meta: StoryMeta = { title: "ConfirmCard" };
7
7
 
8
- const callbacks = { onInput: () => {}, onSelectVerdict: () => {}, onSubmit: () => {}, onCancel: () => {} };
8
+ const callbacks = {
9
+ onInput: () => {},
10
+ onSelectVerdict: () => {},
11
+ onSubmit: () => {},
12
+ onCancel: () => {},
13
+ };
9
14
 
10
15
  export const ApproveDefault: Story = {
11
- render: () => <ConfirmCard verdict="approve" summary="" annotationCount={0} blockingCount={0} {...callbacks} />,
16
+ render: () => (
17
+ <ConfirmCard
18
+ verdict="approve"
19
+ summary=""
20
+ annotationCount={0}
21
+ blockingCount={0}
22
+ {...callbacks}
23
+ />
24
+ ),
12
25
  expectedColors: [DARK.green, DARK.accent],
13
26
  size: { width: 40, height: 14 },
14
27
  };
15
28
 
16
29
  export const RequestChanges: Story = {
17
30
  render: () => (
18
- <ConfirmCard verdict="request_changes" summary="Tighten the steps." annotationCount={3} blockingCount={1} {...callbacks} />
31
+ <ConfirmCard
32
+ verdict="request_changes"
33
+ summary="Tighten the steps."
34
+ annotationCount={3}
35
+ blockingCount={1}
36
+ {...callbacks}
37
+ />
19
38
  ),
20
39
  expectedColors: [DARK.red],
21
40
  size: { width: 40, height: 14 },
@@ -44,7 +44,11 @@ export interface ConfirmCardProps {
44
44
  const CONFIRM_CONTENT_ROWS = 7;
45
45
 
46
46
  export function verdictColor(verdict: VerdictKind, tokens: Theme): string {
47
- return verdict === "approve" ? tokens.green : verdict === "request_changes" ? tokens.red : tokens.blue;
47
+ return verdict === "approve"
48
+ ? tokens.green
49
+ : verdict === "request_changes"
50
+ ? tokens.red
51
+ : tokens.blue;
48
52
  }
49
53
 
50
54
  /**
@@ -66,9 +70,15 @@ function VerdictSelector({
66
70
  return (
67
71
  <box style={{ flexDirection: "row", height: 1 }}>
68
72
  {VERDICTS.map((candidate) => (
69
- <box key={candidate} style={{ paddingRight: 1 }} onMouseUp={() => onSelectVerdict(candidate)}>
73
+ <box
74
+ key={candidate}
75
+ style={{ paddingRight: 1 }}
76
+ onMouseUp={() => onSelectVerdict(candidate)}
77
+ >
70
78
  <text fg={candidate === verdict ? verdictColor(candidate, tokens) : tokens.textDim}>
71
- {candidate === verdict ? `[${VERDICT_LABEL[candidate]}]` : ` ${VERDICT_LABEL[candidate]} `}
79
+ {candidate === verdict
80
+ ? `[${VERDICT_LABEL[candidate]}]`
81
+ : ` ${VERDICT_LABEL[candidate]} `}
72
82
  </text>
73
83
  </box>
74
84
  ))}
@@ -94,15 +104,23 @@ export function ConfirmCard({
94
104
  title=" submit review "
95
105
  contentRows={CONFIRM_CONTENT_ROWS + (viewedSummary !== undefined ? 1 : 0)}
96
106
  borderColor={tokens.accent}
107
+ backgroundColor="transparent"
97
108
  marginRight={1}
98
109
  theme={theme}
99
110
  >
100
- <text fg={tokens.textDim}>{`${annotationCount} annotations · ${blockingCount} blocking`}</text>
111
+ <text
112
+ fg={tokens.textDim}
113
+ >{`${annotationCount} annotations · ${blockingCount} blocking`}</text>
101
114
  {viewedSummary !== undefined ? <text fg={tokens.textDim}>{viewedSummary}</text> : null}
102
115
  <box style={{ height: 1 }} />
103
116
  <VerdictSelector verdict={verdict} onSelectVerdict={onSelectVerdict} theme={theme} />
104
117
  <box style={{ height: 1 }} />
105
- <input focused value={summary} onInput={onInput} placeholder="summary for the agent (optional)" />
118
+ <input
119
+ focused
120
+ value={summary}
121
+ onInput={onInput}
122
+ placeholder="summary for the agent (optional)"
123
+ />
106
124
  <box style={{ height: 1 }} />
107
125
  <Toolbar>
108
126
  <Button variant="solid" marginRight={2} onPress={onSubmit} theme={theme}>
@@ -39,12 +39,20 @@ export function ConfirmDialog({
39
39
  if (!isOpen) return null;
40
40
  return (
41
41
  <Dialog isOpen title={title} width={Math.min(54, terminalWidth - 6)} height={7} theme={theme}>
42
- <box style={{ flexDirection: "column", flexGrow: 1, paddingLeft: 1, paddingRight: 1, paddingTop: 1 }}>
42
+ <box
43
+ style={{
44
+ flexDirection: "column",
45
+ flexGrow: 1,
46
+ paddingLeft: 1,
47
+ paddingRight: 1,
48
+ paddingTop: 1,
49
+ }}
50
+ >
43
51
  <text fg={tokens.text}>{message}</text>
44
52
  <box style={{ flexGrow: 1 }} />
45
53
  <Toolbar>
46
54
  <Button variant="solid" onPress={onConfirm} theme={theme}>
47
- {` ${confirmLabel} ⏎ `}
55
+ {` ${confirmLabel} `}
48
56
  </Button>
49
57
  <Button onPress={onCancel} theme={theme}>
50
58
  {` ${cancelLabel} esc `}