@cueloop/client 0.1.0-alpha.59 → 0.1.0-alpha.60

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 (98) hide show
  1. package/package.json +5 -5
  2. package/src/App.autoclose.test.tsx +7 -0
  3. package/src/App.diff.test.tsx +11 -0
  4. package/src/App.export.test.tsx +5 -0
  5. package/src/App.plan-review.test.tsx +42 -0
  6. package/src/App.prototype.test.tsx +11 -0
  7. package/src/App.readonly.test.tsx +10 -0
  8. package/src/App.rendering.test.tsx +4 -0
  9. package/src/App.review-panel.test.tsx +7 -0
  10. package/src/App.submit-confirm.test.tsx +11 -0
  11. package/src/App.test.tsx +13 -0
  12. package/src/App.toast.test.tsx +2 -0
  13. package/src/App.tsx +27 -0
  14. package/src/App.walk.test.tsx +10 -0
  15. package/src/app-key-state.ts +1 -0
  16. package/src/app-screens.tsx +4 -0
  17. package/src/app-view-model.ts +21 -0
  18. package/src/attribution.ts +4 -0
  19. package/src/clipboard.ts +3 -0
  20. package/src/components/AnnotationCard.tsx +9 -0
  21. package/src/components/Breadcrumb.tsx +1 -0
  22. package/src/components/CodeBlock.tsx +1 -0
  23. package/src/components/CompletionOverlay.tsx +1 -0
  24. package/src/components/ConfirmCard.tsx +2 -0
  25. package/src/components/ConfirmDialog.tsx +2 -0
  26. package/src/components/DiffSheet.scroll.test.tsx +12 -0
  27. package/src/components/DiffSheet.syntax.test.tsx +3 -0
  28. package/src/components/DiffSheet.tsx +19 -0
  29. package/src/components/InboxList.tsx +2 -0
  30. package/src/components/KeybindsDialog.tsx +1 -0
  31. package/src/components/MarkerPopover.tsx +1 -0
  32. package/src/components/MenuBar.tsx +1 -0
  33. package/src/components/PlanSheet.tsx +26 -0
  34. package/src/components/PromptDialog.tsx +2 -0
  35. package/src/components/PrototypeSheet.tsx +27 -0
  36. package/src/components/ReviewPanel.tsx +2 -0
  37. package/src/components/ReviewRail.tsx +1 -0
  38. package/src/components/SettingsDialog.tsx +6 -0
  39. package/src/components/SettingsRows.tsx +4 -0
  40. package/src/components/Toast.tsx +1 -0
  41. package/src/components/agent-launcher.test.tsx +1 -0
  42. package/src/components/agent-launcher.tsx +2 -0
  43. package/src/components/diff-sheet-layout.test.ts +1 -0
  44. package/src/components/diff-sheet-layout.ts +17 -0
  45. package/src/components/plan-sheet-run-style.ts +4 -0
  46. package/src/components/primitives/Button.tsx +1 -0
  47. package/src/components/primitives/Card.tsx +1 -0
  48. package/src/components/primitives/Dialog.tsx +2 -0
  49. package/src/components/primitives/StatusBar.tsx +1 -0
  50. package/src/components/primitives/Tabs.tsx +4 -0
  51. package/src/components/quick-actions-editor.tsx +2 -0
  52. package/src/components/stories-app.tsx +1 -0
  53. package/src/components/stories.test.tsx +4 -0
  54. package/src/components/story.ts +6 -0
  55. package/src/components/syntax-highlight.ts +10 -0
  56. package/src/components/terminal-pane.ts +12 -0
  57. package/src/components/theme-context.tsx +1 -0
  58. package/src/components/truncate-text.ts +1 -0
  59. package/src/config.test.ts +18 -0
  60. package/src/config.ts +27 -0
  61. package/src/diff-hunk-curate.ts +20 -0
  62. package/src/diff-intraline.test.ts +1 -0
  63. package/src/diff-intraline.ts +14 -0
  64. package/src/diff-syntax.ts +22 -0
  65. package/src/editor.test.ts +6 -0
  66. package/src/editor.ts +12 -0
  67. package/src/ghostty-terminal.test.ts +2 -0
  68. package/src/ghostty-terminal.ts +8 -0
  69. package/src/herdr.test.ts +2 -0
  70. package/src/herdr.ts +3 -0
  71. package/src/integrations.ts +2 -0
  72. package/src/intent-dispatch.test.ts +6 -0
  73. package/src/intent-dispatch.ts +37 -0
  74. package/src/key-bindings.test.ts +4 -0
  75. package/src/key-bindings.ts +22 -0
  76. package/src/keymap.test.ts +13 -0
  77. package/src/keymap.ts +43 -0
  78. package/src/kitty-image.ts +9 -0
  79. package/src/prototype-browser.test.ts +1 -0
  80. package/src/prototype-browser.ts +22 -0
  81. package/src/pty.test.ts +3 -0
  82. package/src/pty.ts +15 -0
  83. package/src/run.ts +1 -0
  84. package/src/serve.test.ts +1 -0
  85. package/src/serve.ts +3 -0
  86. package/src/session-controller.curate.test.ts +7 -0
  87. package/src/session-controller.share.test.ts +11 -0
  88. package/src/session-controller.ts +79 -0
  89. package/src/share.ts +7 -0
  90. package/src/test-support.test.ts +5 -0
  91. package/src/test-support.ts +10 -0
  92. package/src/theme-presets.ts +1 -0
  93. package/src/use-settings-dialog.tsx +12 -0
  94. package/src/view-diff.test.ts +3 -0
  95. package/src/view-diff.ts +5 -0
  96. package/src/view-plan.test.ts +7 -0
  97. package/src/view-plan.ts +59 -0
  98. package/src/walk.ts +4 -0
@@ -69,14 +69,17 @@ function cellHeightOverWidth(renderer: {
69
69
  terminalHeight: number;
70
70
  }): number {
71
71
  const resolution = renderer.resolution;
72
+
72
73
  if (!resolution || renderer.terminalWidth < 1 || renderer.terminalHeight < 1) {
73
74
  return FALLBACK_CELL_HEIGHT_OVER_WIDTH;
74
75
  }
76
+
75
77
  return resolution.height / renderer.terminalHeight / (resolution.width / renderer.terminalWidth);
76
78
  }
77
79
 
78
80
  function regionOf(box: BoxRenderable | null): CellRegion | null {
79
81
  if (!box || box.width < 1 || box.height < 1) return null;
82
+
80
83
  return { column: box.x, row: box.y, columns: box.width, rows: box.height };
81
84
  }
82
85
 
@@ -97,9 +100,11 @@ function captureConfig(
97
100
  renderer: CaptureRenderer,
98
101
  ): { viewport: PrototypeViewport; deviceScaleFactor: number } {
99
102
  const resolution = renderer.resolution;
103
+
100
104
  if (resolution && renderer.terminalWidth >= 1 && renderer.terminalHeight >= 1) {
101
105
  const cellWidth = resolution.width / renderer.terminalWidth;
102
106
  const cellHeight = resolution.height / renderer.terminalHeight;
107
+
103
108
  return {
104
109
  viewport: {
105
110
  width: Math.max(1, Math.round(region.columns * cellWidth)),
@@ -108,6 +113,7 @@ function captureConfig(
108
113
  deviceScaleFactor: 1,
109
114
  };
110
115
  }
116
+
111
117
  return {
112
118
  viewport: {
113
119
  width: CAPTURE_WIDTH,
@@ -139,6 +145,7 @@ function PrototypeSheetImpl({
139
145
  const paintRef = useRef<() => void>(() => undefined);
140
146
  const launchRef = useRef<() => void>(() => undefined);
141
147
  const hiddenRef = useRef(hidden);
148
+
142
149
  useEffect(() => {
143
150
  hiddenRef.current = hidden;
144
151
  });
@@ -182,10 +189,12 @@ function PrototypeSheetImpl({
182
189
  deleteKittyImage(write, PROTOTYPE_IMAGE_ID);
183
190
  transmittedRef.current = null;
184
191
  }
192
+
185
193
  return;
186
194
  }
187
195
  const region = regionOf(regionRef.current);
188
196
  const png = pngRef.current;
197
+
189
198
  if (!region || !png) return;
190
199
  if (transmittedRef.current !== png) {
191
200
  transmitKittyImage(write, png, region, PROTOTYPE_IMAGE_ID, medium);
@@ -194,12 +203,15 @@ function PrototypeSheetImpl({
194
203
  placeKittyImage(write, region, PROTOTYPE_IMAGE_ID);
195
204
  }
196
205
  };
206
+
197
207
  paintRef.current = paint;
198
208
  const onFrame = (): void => {
199
209
  launchRef.current();
200
210
  paint();
201
211
  };
212
+
202
213
  renderer.on("frame", onFrame);
214
+
203
215
  return () => {
204
216
  renderer.off("frame", onFrame);
205
217
  if (write) deleteKittyImage(write, PROTOTYPE_IMAGE_ID);
@@ -211,6 +223,7 @@ function PrototypeSheetImpl({
211
223
  }, [composing, onComposingChange]);
212
224
 
213
225
  const unmountedRef = useRef(false);
226
+
214
227
  useEffect(() => {
215
228
  return () => {
216
229
  unmountedRef.current = true;
@@ -221,13 +234,16 @@ function PrototypeSheetImpl({
221
234
  const launch = (): void => {
222
235
  const region = regionOf(regionRef.current);
223
236
  const caps = renderer?.capabilities;
237
+
224
238
  if (launchedRef.current || !region) return;
225
239
  if (caps && caps.kitty_graphics === false) {
226
240
  setStatus("unsupported");
241
+
227
242
  return;
228
243
  }
229
244
  launchedRef.current = true;
230
245
  const capture = captureConfig(region, renderer!);
246
+
231
247
  viewportRef.current = capture.viewport;
232
248
  void (async () => {
233
249
  try {
@@ -236,10 +252,12 @@ function PrototypeSheetImpl({
236
252
  viewport: capture.viewport,
237
253
  deviceScaleFactor: capture.deviceScaleFactor,
238
254
  });
255
+
239
256
  // the sheet may have unmounted while Chromium was launching; close the
240
257
  // late arrival instead of leaking the process
241
258
  if (unmountedRef.current) {
242
259
  void browser.close();
260
+
243
261
  return;
244
262
  }
245
263
  browserRef.current = browser;
@@ -251,12 +269,14 @@ function PrototypeSheetImpl({
251
269
  }
252
270
  })();
253
271
  };
272
+
254
273
  useEffect(() => {
255
274
  launchRef.current = launch;
256
275
  });
257
276
 
258
277
  const refresh = async (): Promise<void> => {
259
278
  const browser = browserRef.current;
279
+
260
280
  if (browser) setPng(await browser.screenshot());
261
281
  };
262
282
 
@@ -278,6 +298,7 @@ function PrototypeSheetImpl({
278
298
  const onRegionMouseDown = (event: { x: number; y: number }): void => {
279
299
  const browser = browserRef.current;
280
300
  const region = regionOf(regionRef.current);
301
+
281
302
  if (status !== "ready" || !browser || !region) return;
282
303
  const geometry = {
283
304
  x: region.column,
@@ -286,12 +307,15 @@ function PrototypeSheetImpl({
286
307
  height: region.rows,
287
308
  };
288
309
  const cssPoint = imageCellToCss(event, geometry, viewportRef.current);
310
+
289
311
  if (!cssPoint) return;
290
312
  void (async () => {
291
313
  const element = await browser.elementAt(cssPoint.x, cssPoint.y);
314
+
292
315
  if (!element) return;
293
316
  const cell = cssBoxToCell(element.box, geometry, viewportRef.current);
294
317
  const topRaw = cell.row - geometry.y;
318
+
295
319
  setSelected(element);
296
320
  setOverlayCell({
297
321
  left: Math.max(0, cell.column - geometry.x),
@@ -308,8 +332,10 @@ function PrototypeSheetImpl({
308
332
 
309
333
  const onRegionScroll = (event: { scroll?: { direction: string } }): void => {
310
334
  const browser = browserRef.current;
335
+
311
336
  if (status !== "ready" || !browser) return;
312
337
  const delta = event.scroll?.direction === "up" ? -SCROLL_STEP : SCROLL_STEP;
338
+
313
339
  // a selection would drift once the page scrolls under it, so drop it
314
340
  clearSelection();
315
341
  void browser
@@ -419,5 +445,6 @@ function statusLine(status: SheetStatus, errorMessage: string): string {
419
445
  if (status === "loading") return "rendering prototype…";
420
446
  if (status === "unsupported")
421
447
  return "prototype preview needs a graphics terminal (kitty or ghostty)";
448
+
422
449
  return `prototype preview failed: ${errorMessage}`;
423
450
  }
@@ -49,6 +49,7 @@ function CompactRail({
49
49
  theme?: Theme;
50
50
  }): React.ReactNode {
51
51
  const tokens = useComponentTheme(theme);
52
+
52
53
  return (
53
54
  <box
54
55
  style={{
@@ -88,6 +89,7 @@ export function ReviewPanel({
88
89
  theme,
89
90
  }: ReviewPanelProps): React.ReactNode {
90
91
  if (mode === "hidden") return null;
92
+
91
93
  return (
92
94
  <>
93
95
  <ReviewDivider onGrab={onDividerGrab} />
@@ -283,6 +283,7 @@ function RemovalCard({
283
283
  Math.max(1, previewLines.length) + (overflow > 0 ? 1 : 0) + (isSelected ? 1 : 0);
284
284
  // border = "what + who": a diff reject or a plan cut, always the reviewer's own
285
285
  const title = ` ${item.source === "diff" ? "REJECT" : "CUT"} · me `;
286
+
286
287
  return (
287
288
  <box id={`removal-card-${item.id}`} onMouseUp={() => onSelect(item.id)}>
288
289
  <Card
@@ -50,10 +50,13 @@ export function SettingsDialog({
50
50
  }: SettingsDialogProps): React.ReactNode {
51
51
  const tokens = useComponentTheme(theme);
52
52
  const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
53
+
53
54
  if (!isOpen) return null;
54
55
  const category =
55
56
  categories.find((candidate) => candidate.id === activeCategoryId) ?? categories[0];
57
+
56
58
  if (!category) return null;
59
+
57
60
  return (
58
61
  <Dialog
59
62
  isOpen
@@ -68,6 +71,7 @@ export function SettingsDialog({
68
71
  <text fg={tokens.textDim}>SETTINGS</text>
69
72
  {categories.map((candidate) => {
70
73
  const isActive = candidate.id === category.id;
74
+
71
75
  return (
72
76
  <box
73
77
  key={candidate.id}
@@ -92,6 +96,7 @@ export function SettingsDialog({
92
96
  {category.rows.map((row, rowIndex) => {
93
97
  const isActive = activeZone === "body" && rowIndex === activeRowIndex;
94
98
  const value = values[row.key];
99
+
95
100
  if (row.kind === "toggle") {
96
101
  return (
97
102
  <ToggleRow
@@ -116,6 +121,7 @@ export function SettingsDialog({
116
121
  />
117
122
  );
118
123
  }
124
+
119
125
  return (
120
126
  <TextRow
121
127
  key={row.key}
@@ -27,6 +27,7 @@ interface RowShellProps {
27
27
 
28
28
  function RowShell({ label, isActive, onPress, theme, children }: RowShellProps): React.ReactNode {
29
29
  const tokens = useComponentTheme(theme);
30
+
30
31
  return (
31
32
  <box
32
33
  style={{
@@ -60,6 +61,7 @@ export function ToggleRow({
60
61
  theme,
61
62
  }: ToggleRowProps): React.ReactNode {
62
63
  const tokens = useComponentTheme(theme);
64
+
63
65
  return (
64
66
  <RowShell label={label} isActive={isActive} onPress={onPress} theme={theme}>
65
67
  <text fg={value ? tokens.green : tokens.accent}>{value ? "on" : "off"}</text>
@@ -83,6 +85,7 @@ export function CycleRow({
83
85
  theme,
84
86
  }: CycleRowProps): React.ReactNode {
85
87
  const tokens = useComponentTheme(theme);
88
+
86
89
  return (
87
90
  <RowShell label={label} isActive={isActive} onPress={onPress} theme={theme}>
88
91
  <text fg={tokens.accent}>{value}</text>
@@ -100,6 +103,7 @@ export interface TextRowProps {
100
103
 
101
104
  export function TextRow({ label, value, isActive, onPress, theme }: TextRowProps): React.ReactNode {
102
105
  const tokens = useComponentTheme(theme);
106
+
103
107
  return (
104
108
  <RowShell label={label} isActive={isActive} onPress={onPress} theme={theme}>
105
109
  <text fg={tokens.accent}>{value}</text>
@@ -14,6 +14,7 @@ export function Toast({ title, body, theme }: ToastProps): React.ReactNode {
14
14
  const tokens = useComponentTheme(theme);
15
15
  const { width: terminalWidth } = useTerminalDimensions();
16
16
  const width = Math.min(terminalWidth - 6, Math.max(28, body.length + 4));
17
+
17
18
  return (
18
19
  <Dialog
19
20
  isOpen
@@ -43,6 +43,7 @@ describe("AgentLauncher", () => {
43
43
  // Assert - all three launcher names present, in launch order
44
44
  const names = HARNESS_LAUNCHERS.map((harness) => harness.name);
45
45
  const positions = names.map((name) => frame.indexOf(name));
46
+
46
47
  expect(positions.every((position) => position >= 0)).toBe(true);
47
48
  expect(positions).toEqual(positions.toSorted((left, right) => left - right));
48
49
  });
@@ -81,6 +81,7 @@ export function AgentLauncher({
81
81
 
82
82
  const launch = (harness: HarnessLauncher): void => {
83
83
  const seed = seedContext ? planHandoffBriefing(session.id) : undefined;
84
+
84
85
  if (embeddedTerminalAvailable()) setRunning({ harness, seed });
85
86
  else onLaunchHarness(harness.command, seed);
86
87
  };
@@ -94,6 +95,7 @@ export function AgentLauncher({
94
95
  useEffect(() => {
95
96
  if (!running) return onAgentTerminal?.(null);
96
97
  onAgentTerminal?.({ write: (data) => paneRef.current?.write(data), detach });
98
+
97
99
  return () => onAgentTerminal?.(null);
98
100
  // eslint-disable-next-line react-hooks/exhaustive-deps
99
101
  }, [running]);
@@ -63,6 +63,7 @@ describe("segmentRows", () => {
63
63
  // Assert
64
64
  expect(segments.map((segment) => segment.kind)).toEqual(["header", "header", "chunk"]);
65
65
  const chunk = segments[2];
66
+
66
67
  expect(chunk?.kind).toBe("chunk");
67
68
  if (chunk?.kind === "chunk") expect(chunk.annotation?.id).toBe("a1");
68
69
  });
@@ -35,14 +35,17 @@ export function annotatedRowsByIndex(
35
35
  annotations: Annotation[],
36
36
  ): Map<number, Annotation> {
37
37
  const byRow = new Map<number, Annotation>();
38
+
38
39
  for (const annotation of annotations) {
39
40
  const rowIndex = rows.findIndex(
40
41
  (row) =>
41
42
  row.text === annotation.anchor.quote &&
42
43
  (row.kind === "ctx" || row.kind === "add" || row.kind === "del"),
43
44
  );
45
+
44
46
  if (rowIndex !== -1) byRow.set(rowIndex, annotation);
45
47
  }
48
+
46
49
  return byRow;
47
50
  }
48
51
 
@@ -66,6 +69,7 @@ export function segmentRows(
66
69
 
67
70
  for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
68
71
  const row = rows[rowIndex]!;
72
+
69
73
  if (row.kind === "file" || row.kind === "hunk") {
70
74
  closeChunk(null);
71
75
  segments.push({ kind: "header", rowIndex, row });
@@ -76,6 +80,7 @@ export function segmentRows(
76
80
  chunk.push(row);
77
81
 
78
82
  const annotation = annotatedByRow.get(rowIndex);
83
+
79
84
  if (annotation) {
80
85
  closeChunk(annotation);
81
86
  chunkStart = rowIndex + 1;
@@ -85,6 +90,7 @@ export function segmentRows(
85
90
  }
86
91
  }
87
92
  closeChunk(null);
93
+
88
94
  return segments;
89
95
  }
90
96
 
@@ -93,6 +99,7 @@ export function segmentRows(
93
99
  export function rowContentOffsets(segments: DiffSegment[]): number[] {
94
100
  const offsets: number[] = [];
95
101
  let contentY = 0;
102
+
96
103
  for (const segment of segments) {
97
104
  if (segment.kind === "header") {
98
105
  offsets[segment.rowIndex] = contentY;
@@ -104,6 +111,7 @@ export function rowContentOffsets(segments: DiffSegment[]): number[] {
104
111
  contentY += segment.rows.length + (segment.annotation ? 1 : 0);
105
112
  }
106
113
  }
114
+
107
115
  return offsets;
108
116
  }
109
117
 
@@ -125,27 +133,33 @@ export function coloredRowSpans(
125
133
  const isModifiedLine = intralineRuns !== undefined;
126
134
 
127
135
  const spans: ColoredSpan[] = [];
136
+
128
137
  for (let column = 0; column < text.length; column++) {
129
138
  const color = changed[column]
130
139
  ? baseColor
131
140
  : (syntaxColorByColumn[column] ?? (isModifiedLine ? tokens.textDim : baseColor));
132
141
  const previous = spans[spans.length - 1];
142
+
133
143
  if (previous && previous.foreground === color) previous.text += text[column];
134
144
  else spans.push({ text: text[column]!, foreground: color });
135
145
  }
146
+
136
147
  return spans;
137
148
  }
138
149
 
139
150
  /** Which columns are the intra-line change on a modified line. */
140
151
  function changedColumns(length: number, intralineRuns: IntralineRun[] | undefined): boolean[] {
141
152
  const changed = Array.from({ length }, () => false);
153
+
142
154
  if (!intralineRuns) return changed;
143
155
  let offset = 0;
156
+
144
157
  for (const run of intralineRuns) {
145
158
  if (run.changed)
146
159
  for (let index = 0; index < run.text.length; index++) changed[offset + index] = true;
147
160
  offset += run.text.length;
148
161
  }
162
+
149
163
  return changed;
150
164
  }
151
165
 
@@ -156,12 +170,15 @@ function syntaxColorColumns(
156
170
  tokens: Theme,
157
171
  ): Array<string | undefined> {
158
172
  const colors: Array<string | undefined> = Array.from({ length }, () => undefined);
173
+
159
174
  for (const span of syntaxSpans ?? []) {
160
175
  const color = colorForSyntaxGroup(span.group, tokens);
176
+
161
177
  if (!color) continue;
162
178
  for (let column = span.start; column < span.end && column < length; column++) {
163
179
  colors[column] = color;
164
180
  }
165
181
  }
182
+
166
183
  return colors;
167
184
  }
@@ -29,6 +29,7 @@ function headingForeground(block: DisplayBlock, tokens: Theme): string | undefin
29
29
  if (block.kind === "h1") return tokens.text;
30
30
  if (block.kind === "h2") return tokens.textMuted;
31
31
  if (block.kind === "h3") return tokens.textDim;
32
+
32
33
  return undefined;
33
34
  }
34
35
 
@@ -39,6 +40,7 @@ function blockBaseStyle(block: DisplayBlock, tokens: Theme): BlockBaseStyle {
39
40
  // block-level base: headings bold, quotes muted italic; inline roles compose on top
40
41
  const baseFg = headingFg ?? (isQuote ? tokens.textMuted : tokens.text);
41
42
  const baseAttributes = (isHeading ? HEADING_ATTRIBUTES : 0) | (isQuote ? QUOTE_ATTRIBUTES : 0);
43
+
42
44
  return { baseFg, baseAttributes };
43
45
  }
44
46
 
@@ -49,6 +51,7 @@ function inlineRoleStyle(
49
51
  ): { fg?: string; bg?: string; attributes?: number } {
50
52
  const { baseFg, baseAttributes } = base;
51
53
  const withBase = (roleAttributes: number): number => baseAttributes | roleAttributes;
54
+
52
55
  switch (run.role) {
53
56
  case "ins":
54
57
  return { fg: tokens.insertedForeground, attributes: baseAttributes || undefined };
@@ -82,5 +85,6 @@ export function runStyle(
82
85
  ): { fg?: string; bg?: string; attributes?: number } {
83
86
  // a cut block reads as removed: every run struck through and grayed, never red
84
87
  if (block.type === "del") return { fg: tokens.textDim, attributes: CUT_ATTRIBUTES };
88
+
85
89
  return inlineRoleStyle(run, blockBaseStyle(block, tokens), tokens);
86
90
  }
@@ -36,6 +36,7 @@ export function Button({
36
36
  : variant === "accent-text"
37
37
  ? tokens.accent
38
38
  : tokens.textDim;
39
+
39
40
  return (
40
41
  <box
41
42
  style={{ backgroundColor, marginRight }}
@@ -38,6 +38,7 @@ export function Card({
38
38
  children,
39
39
  }: CardProps): React.ReactNode {
40
40
  const tokens = useComponentTheme(theme);
41
+
41
42
  return (
42
43
  <box
43
44
  style={{
@@ -31,7 +31,9 @@ export function Dialog({
31
31
  children,
32
32
  }: DialogProps): React.ReactNode {
33
33
  const tokens = useComponentTheme(theme);
34
+
34
35
  if (!isOpen) return null;
36
+
35
37
  return (
36
38
  <box
37
39
  style={{
@@ -14,6 +14,7 @@ export interface StatusBarProps {
14
14
 
15
15
  export function StatusBar({ theme, children }: StatusBarProps): React.ReactNode {
16
16
  const tokens = useComponentTheme(theme);
17
+
17
18
  return (
18
19
  <box style={{ height: 1, backgroundColor: tokens.panel, paddingLeft: 1 }}>
19
20
  <text fg={tokens.textDim}>{children}</text>
@@ -59,14 +59,17 @@ export interface TabListProps {
59
59
 
60
60
  export function TabList({ children }: TabListProps): React.ReactNode {
61
61
  const tabs = useContext(TabsContext);
62
+
62
63
  if (!tabs) throw new Error("TabList must render inside Tabs");
63
64
  const tokens = useComponentTheme(tabs.theme);
64
65
  const items: { id: string; label: string }[] = [];
66
+
65
67
  React.Children.forEach(children, (child) => {
66
68
  if (React.isValidElement<TabProps>(child) && typeof child.props.id === "string") {
67
69
  items.push({ id: child.props.id, label: child.props.children });
68
70
  }
69
71
  });
72
+
70
73
  return (
71
74
  <box
72
75
  style={{
@@ -81,6 +84,7 @@ export function TabList({ children }: TabListProps): React.ReactNode {
81
84
  >
82
85
  {items.map((item) => {
83
86
  const selected = item.id === tabs.selectedKey;
87
+
84
88
  return (
85
89
  <box
86
90
  key={item.id}
@@ -40,6 +40,7 @@ export function QuickActionsEditor({
40
40
  theme,
41
41
  }: QuickActionsEditorProps): React.ReactNode {
42
42
  const tokens = useComponentTheme(theme);
43
+
43
44
  return (
44
45
  <box style={{ flexDirection: "column", flexGrow: 1 }}>
45
46
  <box style={{ flexDirection: "row" }}>
@@ -89,6 +90,7 @@ function ActionRow({
89
90
  }): React.ReactNode {
90
91
  const tokens = useComponentTheme(theme);
91
92
  const inputRef = useRef<TextareaRenderable | null>(null);
93
+
92
94
  return (
93
95
  <box style={{ flexDirection: "column" }}>
94
96
  <box
@@ -111,6 +111,7 @@ function StoriesApp({
111
111
  if (import.meta.main) {
112
112
  const stories = await loadStories();
113
113
  const renderer = await createCliRenderer();
114
+
114
115
  createRoot(renderer).render(
115
116
  <StoriesApp
116
117
  stories={stories}
@@ -17,6 +17,7 @@ const stories = await loadStories();
17
17
  function hexOf(color: RGBA | null | undefined): string | undefined {
18
18
  if (!color) return undefined;
19
19
  const [red, green, blue] = color.toInts();
20
+
20
21
  return "#" + [red, green, blue].map((part) => part!.toString(16).padStart(2, "0")).join("");
21
22
  }
22
23
 
@@ -36,6 +37,7 @@ describe("stories catalog", () => {
36
37
 
37
38
  // Act
38
39
  const setup = await testRender(story.render(), size);
40
+
39
41
  allowEventLoopUpdates();
40
42
  await setup.waitForVisualIdle();
41
43
  const frame = setup.captureCharFrame();
@@ -44,10 +46,12 @@ describe("stories catalog", () => {
44
46
  expect(frame).toMatchSnapshot(`${moduleTitle}/${storyName}`);
45
47
  if (story.expectedColors?.length) {
46
48
  const seenColors = new Set<string>();
49
+
47
50
  for (const line of setup.captureSpans().lines) {
48
51
  for (const span of line.spans) {
49
52
  const foregroundHex = hexOf(span.fg);
50
53
  const backgroundHex = hexOf(span.bg);
54
+
51
55
  if (foregroundHex) seenColors.add(foregroundHex);
52
56
  if (backgroundHex) seenColors.add(backgroundHex);
53
57
  }
@@ -41,15 +41,18 @@ export async function loadStories(): Promise<LoadedStory[]> {
41
41
  const glob = new Bun.Glob("**/*.stories.tsx");
42
42
  const files = [...glob.scanSync({ cwd: import.meta.dir })].sort();
43
43
  const loaded: LoadedStory[] = [];
44
+
44
45
  for (const file of files) {
45
46
  const moduleExports = (await import(`${import.meta.dir}/${file}`)) as Record<string, unknown>;
46
47
  const meta = moduleExports["meta"] as StoryMeta | undefined;
47
48
  const moduleTitle = meta?.title ?? file.replace(/\.stories\.tsx$/, "");
49
+
48
50
  for (const [exportName, exported] of Object.entries(moduleExports)) {
49
51
  if (exportName === "meta" || !isStory(exported)) continue;
50
52
  loaded.push({ moduleTitle, storyName: exportName, story: exported });
51
53
  }
52
54
  }
55
+
53
56
  return loaded;
54
57
  }
55
58
 
@@ -58,12 +61,15 @@ export function componentFilesMissingStories(): string[] {
58
61
  const componentGlob = new Bun.Glob("**/*.tsx");
59
62
  const files = [...componentGlob.scanSync({ cwd: import.meta.dir })];
60
63
  const missing: string[] = [];
64
+
61
65
  for (const file of files) {
62
66
  if (file.endsWith(".stories.tsx") || file.endsWith(".test.tsx")) continue;
63
67
  if (file.endsWith(".prototype.tsx")) continue;
64
68
  if (file === "stories-app.tsx") continue;
65
69
  const storiesSibling = file.replace(/\.tsx$/, ".stories.tsx");
70
+
66
71
  if (!files.includes(storiesSibling)) missing.push(file);
67
72
  }
73
+
68
74
  return missing;
69
75
  }
@@ -17,6 +17,7 @@ import type { Theme } from "../theme";
17
17
  /** Markdown fence info -> tree-sitter filetype, with the aliases we accept. */
18
18
  export function filetypeFor(language?: string): string | undefined {
19
19
  const info = (language ?? "").toLowerCase();
20
+
20
21
  if (!info) return undefined;
21
22
  const aliases: Record<string, string> = {
22
23
  ts: "typescript",
@@ -29,6 +30,7 @@ export function filetypeFor(language?: string): string | undefined {
29
30
  zsh: "bash",
30
31
  yml: "yaml",
31
32
  };
33
+
32
34
  return infoStringToFiletype(aliases[info] ?? info) ?? aliases[info] ?? info;
33
35
  }
34
36
 
@@ -36,9 +38,12 @@ export function filetypeFor(language?: string): string | undefined {
36
38
  export function filetypeForPath(path: string): string | undefined {
37
39
  const basename = path.split("/").pop() ?? path;
38
40
  const byBasename = basenameToFiletype.get(basename);
41
+
39
42
  if (byBasename) return byBasename;
40
43
  const dotIndex = basename.lastIndexOf(".");
44
+
41
45
  if (dotIndex <= 0) return undefined;
46
+
42
47
  return extensionToFiletype.get(basename.slice(dotIndex + 1).toLowerCase());
43
48
  }
44
49
 
@@ -75,9 +80,12 @@ const syntaxStyleCache = new WeakMap<Theme, SyntaxStyle>();
75
80
 
76
81
  export function syntaxStyleFor(theme: Theme): SyntaxStyle {
77
82
  const cached = syntaxStyleCache.get(theme);
83
+
78
84
  if (cached) return cached;
79
85
  const style = SyntaxStyle.fromStyles(syntaxGroupStyles(theme));
86
+
80
87
  syntaxStyleCache.set(theme, style);
88
+
81
89
  return style;
82
90
  }
83
91
 
@@ -85,8 +93,10 @@ export function syntaxStyleFor(theme: Theme): SyntaxStyle {
85
93
  export function colorForSyntaxGroup(group: string, theme: Theme): string | undefined {
86
94
  const styles = syntaxGroupStyles(theme);
87
95
  const direct = styles[group];
96
+
88
97
  if (direct) return direct.fg;
89
98
  // capture names are dotted (e.g. "keyword.control"); match the broadest prefix
90
99
  const base = group.split(".")[0]!;
100
+
91
101
  return styles[base]?.fg;
92
102
  }