@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
@@ -88,9 +88,11 @@ export function composeRowCount(
88
88
  ): number {
89
89
  const usableWidth = contentWidth > 0 ? contentWidth : Number.MAX_SAFE_INTEGER;
90
90
  let visualRowCount = 0;
91
+
91
92
  for (const line of text.split("\n")) {
92
93
  visualRowCount += Math.max(1, Math.ceil(line.length / usableWidth));
93
94
  }
95
+
94
96
  return Math.min(cap, Math.max(1, visualRowCount));
95
97
  }
96
98
 
@@ -110,13 +112,16 @@ function DraftEditor({
110
112
  const editorRef = useRef<TextareaRenderable | null>(null);
111
113
  // re-edit opens with the caret after the existing body, like a text field
112
114
  const initialTextLength = useRef(draft.text.length);
115
+
113
116
  useEffect(() => {
114
117
  const editor = editorRef.current;
118
+
115
119
  if (!editor) return;
116
120
  editor.cursorOffset = initialTextLength.current;
117
121
  // the box is laid out now, so its width is known: size to the wrapped body
118
122
  onRowsChange(composeRowCount(editor.plainText, editor.width));
119
123
  }, [onRowsChange]);
124
+
120
125
  return (
121
126
  <>
122
127
  <textarea
@@ -128,6 +133,7 @@ function DraftEditor({
128
133
  {...(draft.onSubmit ? { onSubmit: draft.onSubmit } : {})}
129
134
  onContentChange={() => {
130
135
  const editor = editorRef.current;
136
+
131
137
  if (!editor) return;
132
138
  draft.onInput(editor.plainText);
133
139
  onRowsChange(composeRowCount(editor.plainText, editor.width));
@@ -173,8 +179,10 @@ export function AnnotationCard({
173
179
  // until the mounted textarea reports its measured width below.
174
180
  composeRowCount(activeDraft?.text ?? "", 0),
175
181
  );
182
+
176
183
  if (draft) {
177
184
  const verb = "comment on";
185
+
178
186
  return (
179
187
  <Card
180
188
  title={` ${verb} "${truncateToSingleLine(quote, 40)}" `}
@@ -200,6 +208,7 @@ export function AnnotationCard({
200
208
  const borderInset = 4;
201
209
  // a bordered box must declare a height or it collapses: quote + body, or the editor
202
210
  const contentRows = card.editing ? editorRowCount + 2 : 2;
211
+
203
212
  return (
204
213
  <box
205
214
  id={id}
@@ -30,6 +30,7 @@ export function Breadcrumb({ items, theme }: BreadcrumbProps): React.ReactNode {
30
30
  : tone === "text"
31
31
  ? tokens.text
32
32
  : tokens.textDim;
33
+
33
34
  return (
34
35
  <box style={{ height: 1, backgroundColor: tokens.panel, paddingLeft: 1, flexDirection: "row" }}>
35
36
  <text fg={tokens.text}>
@@ -43,6 +43,7 @@ export function CodeBlock({
43
43
  }: CodeBlockProps): React.ReactNode {
44
44
  const tokens = useComponentTheme(theme);
45
45
  const tagColor = changeTag === "new" ? tokens.green : tokens.accent;
46
+
46
47
  return (
47
48
  <box id={id} style={{ flexDirection: "column", marginTop }}>
48
49
  <text>
@@ -28,6 +28,7 @@ export function CompletionOverlay({
28
28
  }: CompletionOverlayProps): React.ReactNode {
29
29
  const tokens = useComponentTheme(theme);
30
30
  const approved = verdict === "approve";
31
+
31
32
  return (
32
33
  <box
33
34
  style={{
@@ -64,6 +64,7 @@ function VerdictSelector({
64
64
  theme?: Theme;
65
65
  }): React.ReactNode {
66
66
  const tokens = useComponentTheme(theme);
67
+
67
68
  return (
68
69
  <box style={{ flexDirection: "row", height: 1 }}>
69
70
  {VERDICTS.map((candidate) => (
@@ -94,6 +95,7 @@ export function ConfirmCard({
94
95
  theme,
95
96
  }: ConfirmCardProps): React.ReactNode {
96
97
  const tokens = useComponentTheme(theme);
98
+
97
99
  return (
98
100
  <Card
99
101
  title=" submit review "
@@ -36,7 +36,9 @@ export function ConfirmDialog({
36
36
  }: ConfirmDialogProps): React.ReactNode {
37
37
  const tokens = useComponentTheme(theme);
38
38
  const { width: terminalWidth } = useTerminalDimensions();
39
+
39
40
  if (!isOpen) return null;
41
+
40
42
  return (
41
43
  <Dialog isOpen title={title} width={Math.min(54, terminalWidth - 6)} height={7} theme={theme}>
42
44
  <box
@@ -16,13 +16,16 @@ function findById(node: Renderable, id: string): Renderable | undefined {
16
16
  if (node.id === id) return node;
17
17
  for (const child of node.getChildren()) {
18
18
  const found = findById(child, id);
19
+
19
20
  if (found) return found;
20
21
  }
22
+
21
23
  return undefined;
22
24
  }
23
25
 
24
26
  function hex(color: { toInts(): [number, number, number, number] }): string {
25
27
  const [red, green, blue] = color.toInts();
28
+
26
29
  return "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
27
30
  }
28
31
 
@@ -32,6 +35,7 @@ function tallPatch(count: number): string {
32
35
  { length: count },
33
36
  (_, index) => `+line ${String(index).padStart(2, "0")}`,
34
37
  );
38
+
35
39
  return [
36
40
  "diff --git a/f.ts b/f.ts",
37
41
  "--- a/f.ts",
@@ -51,9 +55,11 @@ function ScrollHarness({
51
55
  annotations: Annotation[];
52
56
  }): React.ReactNode {
53
57
  const [cursor, setCursor] = useState(0);
58
+
54
59
  useKeyboard((key) => {
55
60
  if (key.name === "j") setCursor((current) => Math.min(rows.length - 1, current + 1));
56
61
  });
62
+
57
63
  return <DiffSheet rows={rows} cursor={cursor} annotations={annotations} />;
58
64
  }
59
65
 
@@ -74,21 +80,25 @@ test("scrolling down past wrapped annotation cards keeps the cursor pinned and t
74
80
  width: 60,
75
81
  height: 12,
76
82
  });
83
+
77
84
  await setup.waitForVisualIdle();
78
85
  const scrollbox = findById(setup.renderer.root, "diff-scroll") as ScrollBoxRenderable;
79
86
 
80
87
  // the cursor row is the one painted with the cursor background
81
88
  const cursorScreenRow = (): number => {
82
89
  const lines = setup.captureSpans().lines;
90
+
83
91
  for (let index = 0; index < lines.length; index++) {
84
92
  if (lines[index]!.spans.some((span) => hex(span.bg) === DARK.cursorBackground)) return index;
85
93
  }
94
+
86
95
  return -1;
87
96
  };
88
97
 
89
98
  // Act - walk the cursor down through the whole diff, sampling the scroll each step
90
99
  const scrollTops: number[] = [];
91
100
  const screenRows: number[] = [];
101
+
92
102
  for (let step = 0; step < rows.length; step++) {
93
103
  scrollTops.push(scrollbox.scrollTop);
94
104
  screenRows.push(cursorScreenRow());
@@ -111,10 +121,12 @@ test("scrolling down past wrapped annotation cards keeps the cursor pinned and t
111
121
  const steadyRows = screenRows.filter(
112
122
  (_, step) => scrollTops[step]! > 0 && scrollTops[step]! < maxScroll,
113
123
  );
124
+
114
125
  expect(steadyRows.length).toBeGreaterThan(0);
115
126
  expect(new Set(steadyRows).size).toBe(1);
116
127
  const pinnedRow = steadyRows[0]!;
117
128
  const viewportBottom = scrollbox.height;
129
+
118
130
  // the cursor sits a couple of rows inside the bottom edge, per the follow rule
119
131
  expect(viewportBottom - pinnedRow).toBeGreaterThanOrEqual(2);
120
132
  expect(viewportBottom - pinnedRow).toBeLessThanOrEqual(3);
@@ -19,6 +19,7 @@ const PATCH = `diff --git a/src/x.ts b/src/x.ts
19
19
 
20
20
  function hex(color: RGBA): string {
21
21
  const [red, green, blue] = color.toInts();
22
+
22
23
  return "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
23
24
  }
24
25
 
@@ -35,6 +36,7 @@ test("renders tree-sitter colors, with the changed word in the diff color on top
35
36
  // Act
36
37
  // syntax highlighting resolves off the render path; pump until a keyword paints
37
38
  let keywordAccent = false;
39
+
38
40
  for (let attempt = 0; attempt < 60 && !keywordAccent; attempt++) {
39
41
  await setup.renderOnce();
40
42
  await new Promise((resolve) => setTimeout(resolve, 20));
@@ -49,6 +51,7 @@ test("renders tree-sitter colors, with the changed word in the diff color on top
49
51
  expect(keywordAccent).toBe(true);
50
52
  // the intra-line changed number keeps the diff color over its syntax color
51
53
  const changedColors = new Set<string>();
54
+
52
55
  for (const line of setup.captureSpans().lines) {
53
56
  for (const span of line.spans) {
54
57
  if (span.text.includes("250")) changedColors.add(hex(span.fg));
@@ -78,6 +78,7 @@ function rowStyle(
78
78
  : isAnnotatedRow
79
79
  ? tokens.markCommentBackground
80
80
  : undefined;
81
+
81
82
  return { sign, baseColor, background };
82
83
  }
83
84
 
@@ -104,27 +105,33 @@ function DiffChunk({
104
105
 
105
106
  const lineNumbers = useMemo(() => {
106
107
  const numbers = new Map<number, number>();
108
+
107
109
  segment.rows.forEach((row, lineIndex) => {
108
110
  const gutterNumber = row.kind === "del" ? row.oldLine : row.newLine;
111
+
109
112
  if (gutterNumber !== undefined) numbers.set(lineIndex, gutterNumber);
110
113
  });
114
+
111
115
  return numbers;
112
116
  }, [segment]);
113
117
 
114
118
  const lineSigns = useMemo(() => {
115
119
  const signs = new Map<number, LineSign>();
120
+
116
121
  if (cursorInChunk >= 0 && cursorInChunk < segment.rows.length) {
117
122
  signs.set(cursorInChunk, { before: "▎", beforeColor: tokens.accent });
118
123
  }
119
124
  if (segment.annotation) {
120
125
  signs.set(segment.rows.length - 1, { after: "◆", afterColor: tokens.accent });
121
126
  }
127
+
122
128
  return signs;
123
129
  }, [segment, cursorInChunk, tokens]);
124
130
 
125
131
  const lineColors = useMemo(() => {
126
132
  // the renderable's config parser requires the gutter key to be present
127
133
  const colors = new Map<number, { gutter: string | undefined; content: string }>();
134
+
128
135
  if (segment.annotation) {
129
136
  colors.set(segment.rows.length - 1, {
130
137
  gutter: undefined,
@@ -137,12 +144,14 @@ function DiffChunk({
137
144
  content: tokens.cursorBackground,
138
145
  });
139
146
  }
147
+
140
148
  return colors;
141
149
  }, [segment, cursorInChunk, tokens]);
142
150
 
143
151
  // the gutter renderable has no prop setters for these maps, so they land via an effect
144
152
  useEffect(() => {
145
153
  const gutter = gutterRef.current;
154
+
146
155
  if (!gutter) return;
147
156
  gutter.setLineNumbers(lineNumbers);
148
157
  gutter.setLineSigns(lineSigns);
@@ -197,6 +206,7 @@ function DiffChunk({
197
206
  baseColor,
198
207
  tokens,
199
208
  );
209
+
200
210
  return (
201
211
  <React.Fragment key={lineIndex}>
202
212
  <span fg={baseColor} bg={background}>
@@ -236,15 +246,19 @@ function useSyntaxHighlights(rows: DiffRow[]): Map<number, SyntaxSpan[]> {
236
246
  rows,
237
247
  byRow: EMPTY_SYNTAX,
238
248
  });
249
+
239
250
  useEffect(() => {
240
251
  let active = true;
252
+
241
253
  void highlightDiffRows(rows).then((byRow) => {
242
254
  if (active) setHighlighted({ rows, byRow });
243
255
  });
256
+
244
257
  return () => {
245
258
  active = false;
246
259
  };
247
260
  }, [rows]);
261
+
248
262
  return highlighted.rows === rows ? highlighted.byRow : EMPTY_SYNTAX;
249
263
  }
250
264
 
@@ -276,8 +290,10 @@ export function DiffSheet({
276
290
  useEffect(() => {
277
291
  const scrollbox = scrollRef.current;
278
292
  const cursorOffset = rowOffsets[cursor];
293
+
279
294
  if (!scrollbox || cursorOffset === undefined) return;
280
295
  const viewportHeight = Math.max(1, scrollbox.height);
296
+
281
297
  if (cursorOffset < scrollbox.scrollTop + 2) {
282
298
  scrollbox.scrollTo({ x: 0, y: Math.max(0, cursorOffset - 2) });
283
299
  } else if (cursorOffset > scrollbox.scrollTop + viewportHeight - 3) {
@@ -301,6 +317,7 @@ export function DiffSheet({
301
317
  {segments.map((segment, segmentIndex) => {
302
318
  if (segment.kind === "header") {
303
319
  const isCursor = segment.rowIndex === cursor;
320
+
304
321
  if (segment.row.kind === "file") {
305
322
  return (
306
323
  <text
@@ -313,6 +330,7 @@ export function DiffSheet({
313
330
  </text>
314
331
  );
315
332
  }
333
+
316
334
  return (
317
335
  <text
318
336
  key={segmentIndex}
@@ -327,6 +345,7 @@ export function DiffSheet({
327
345
  }
328
346
 
329
347
  const lastRowIndex = segment.firstRowIndex + segment.rows.length - 1;
348
+
330
349
  return (
331
350
  <React.Fragment key={segmentIndex}>
332
351
  <DiffChunk
@@ -27,6 +27,7 @@ export function InboxList({
27
27
  theme,
28
28
  }: InboxListProps): React.ReactNode {
29
29
  const tokens = useComponentTheme(theme);
30
+
30
31
  return (
31
32
  <box style={{ flexGrow: 1, flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
32
33
  {inbox.length === 0 ? (
@@ -35,6 +36,7 @@ export function InboxList({
35
36
  inbox.map((session, index) => {
36
37
  const selected = index === cursor;
37
38
  const title = session.artifact.meta.title ?? session.id;
39
+
38
40
  return (
39
41
  <box
40
42
  key={session.id}
@@ -17,6 +17,7 @@ export function KeybindsDialog({ sections, theme }: KeybindsDialogProps): React.
17
17
  sections.reduce((sum, section) => sum + 1 + section.entries.length + 1, 0) + 1;
18
18
  const width = Math.min(52, terminalWidth - 6);
19
19
  const height = Math.min(terminalHeight - 4, contentRows + 3);
20
+
20
21
  return (
21
22
  <Dialog
22
23
  isOpen
@@ -56,6 +56,7 @@ export function MarkerPopover({
56
56
  paddingRight: 1,
57
57
  };
58
58
  const open = view === "actions";
59
+
59
60
  return (
60
61
  <box
61
62
  style={{ flexDirection: "column", alignItems: "flex-start" }}
@@ -29,6 +29,7 @@ export function MenuBar({
29
29
  const tokens = useComponentTheme(theme);
30
30
  const { height: terminalHeight } = useTerminalDimensions();
31
31
  const dropupHeight = DROPUP_OPTION_ROWS + 2;
32
+
32
33
  return (
33
34
  <>
34
35
  {open ? (
@@ -100,10 +100,12 @@ function positionOfRenderedOffset(
100
100
  ): { x: number; y: number } {
101
101
  const info = renderable.lineInfo;
102
102
  let lineIndex = 0;
103
+
103
104
  for (let index = 0; index < info.lineStartCols.length; index++) {
104
105
  if (info.lineStartCols[index]! <= renderedOffset) lineIndex = index;
105
106
  else break;
106
107
  }
108
+
107
109
  return {
108
110
  x: renderable.x + renderedOffset - (info.lineStartCols[lineIndex] ?? 0),
109
111
  y: renderable.y + lineIndex,
@@ -135,37 +137,47 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
135
137
  if (!renderer?.hasSelection) return null;
136
138
  const readBlock = (displayIndex: number): PlanSelection | null => {
137
139
  const blockRef = blockRefs.current.get(displayIndex);
140
+
138
141
  if (!blockRef) return null;
139
142
  const selection = blockRef.renderable.getSelection();
143
+
140
144
  if (!selection || selection.end <= selection.start) return null;
141
145
  const range = workRangeForRendered(blockRef.runs, selection.start, selection.end);
146
+
142
147
  return range ? { displayIndex, ...range } : null;
143
148
  };
149
+
144
150
  // the block the mouse released on wins, so a fresh drag re-anchors the span
145
151
  // (one marker at a time) instead of sticking to the topmost prior selection
146
152
  if (preferredIndex !== undefined) {
147
153
  const preferred = readBlock(preferredIndex);
154
+
148
155
  if (preferred) return preferred;
149
156
  }
150
157
  for (const displayIndex of [...blockRefs.current.keys()].sort(
151
158
  (left, right) => left - right,
152
159
  )) {
153
160
  const found = readBlock(displayIndex);
161
+
154
162
  if (found) return found;
155
163
  }
164
+
156
165
  return null;
157
166
  },
158
167
  driveSpanSelection: (span: SpanState): void => {
159
168
  if (!renderer) return;
160
169
  const blockRef = blockRefs.current.get(span.displayIndex);
170
+
161
171
  if (!blockRef) return;
162
172
  // a span over an emphasized word starts/ends on concealed markers with
163
173
  // no rendered cell; snap to the visible characters inside them
164
174
  const renderedStart = renderedOffsetAtOrAfter(blockRef.runs, span.start);
165
175
  const renderedEnd = renderedOffsetAtOrBefore(blockRef.runs, span.end - 1);
176
+
166
177
  if (renderedStart === null || renderedEnd === null || renderedEnd < renderedStart) return;
167
178
  const startPosition = positionOfRenderedOffset(blockRef.renderable, renderedStart);
168
179
  const endPosition = positionOfRenderedOffset(blockRef.renderable, renderedEnd);
180
+
169
181
  renderer.startSelection(blockRef.renderable, startPosition.x, startPosition.y);
170
182
  renderer.updateSelection(blockRef.renderable, endPosition.x + 1, endPosition.y);
171
183
  },
@@ -192,16 +204,20 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
192
204
  // a terminal resize rewraps every block; re-run the anchor math against the
193
205
  // new geometry instead of leaving the card at its pre-resize cell
194
206
  const [resizeEpoch, setResizeEpoch] = useState(0);
207
+
195
208
  useEffect(() => {
196
209
  if (!renderer) return;
197
210
  const onResize = (): void => setResizeEpoch((epoch) => epoch + 1);
211
+
198
212
  renderer.on("resize", onResize);
213
+
199
214
  return () => void renderer.off("resize", onResize);
200
215
  }, [renderer]);
201
216
  useEffect(() => {
202
217
  if (!popover) return;
203
218
  const blockRef = blockRefs.current.get(popover.displayIndex);
204
219
  const scrollbox = scrollRef.current;
220
+
205
221
  if (!blockRef || !scrollbox) return;
206
222
  const renderedStart =
207
223
  activeSpan && activeSpan.displayIndex === popover.displayIndex
@@ -213,9 +229,11 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
213
229
  const anchorTop = startPosition.y - content.y;
214
230
  // clamp so the toolbar card never runs past the content's right edge
215
231
  const maxLeft = Math.max(0, content.width - POPOVER_TOOLBAR_COLUMNS);
232
+
216
233
  setPopoverLeft(Math.min(anchorLeft, maxLeft));
217
234
  // only the toolbar (3 rows) + gap must fit above; the dropdown flows down
218
235
  const lineViewportRow = startPosition.y - scrollbox.y;
236
+
219
237
  setPopoverTop(anchorTop + (lineViewportRow < POPOVER_ROWS_ABOVE ? 1 : -POPOVER_ROWS_ABOVE));
220
238
  }, [popover, activeSpan, display, cursor, resizeEpoch]);
221
239
 
@@ -227,6 +245,7 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
227
245
  if (renderable) blockRefs.current.set(displayIndex, { renderable, runs });
228
246
  else blockRefs.current.delete(displayIndex);
229
247
  };
248
+
230
249
  // stale refs must not survive a shrinking display list
231
250
  useEffect(() => {
232
251
  for (const displayIndex of blockRefs.current.keys()) {
@@ -235,10 +254,12 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
235
254
  }, [display]);
236
255
 
237
256
  const children: React.ReactNode[] = [];
257
+
238
258
  for (let displayIndex = 0; displayIndex < display.length; displayIndex++) {
239
259
  const block = display[displayIndex]!;
240
260
  const isCursor = displayIndex === cursor;
241
261
  const gap = topGap(display[displayIndex - 1], block);
262
+
242
263
  if (block.kind === "code") {
243
264
  children.push(
244
265
  <CodeBlock
@@ -256,6 +277,7 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
256
277
  );
257
278
  } else {
258
279
  const blockMarks = [...(marks.get(displayIndex) ?? [])];
280
+
259
281
  if (activeSpan && activeSpan.displayIndex === displayIndex) {
260
282
  blockMarks.push({ start: activeSpan.start, end: activeSpan.end, role: "kspan" });
261
283
  }
@@ -265,6 +287,7 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
265
287
  const mappedRuns: StyleRun[] = showsChangeTag(block)
266
288
  ? [...runs, { text: ` [${tagLabel(block)}]`, role: "plain", start: null }]
267
289
  : runs;
290
+
268
291
  children.push(
269
292
  <box
270
293
  key={displayIndex}
@@ -288,6 +311,7 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
288
311
  >
289
312
  {runs.map((run, runIndex) => {
290
313
  const url = run.role === "link" ? safeLinkHref(run.href) : undefined;
314
+
291
315
  return (
292
316
  <span
293
317
  key={runIndex}
@@ -372,6 +396,7 @@ function topGap(previous: DisplayBlock | undefined, current: DisplayBlock): numb
372
396
  const tightPair =
373
397
  (current.kind === "li" && previous.kind === "li") ||
374
398
  (current.kind === "oli" && previous.kind === "oli");
399
+
375
400
  return tightPair ? 0 : 1;
376
401
  }
377
402
 
@@ -379,6 +404,7 @@ function marker(block: DisplayBlock): string {
379
404
  if (block.kind === "li") return "- ";
380
405
  if (block.kind === "oli") return `${block.orderedItemNumber ?? 1}. `;
381
406
  if (block.kind === "quote") return "▏ ";
407
+
382
408
  return "";
383
409
  }
384
410
 
@@ -37,12 +37,14 @@ export function PromptDialog({
37
37
  const tokens = useComponentTheme(theme);
38
38
  const { width: terminalWidth } = useTerminalDimensions();
39
39
  const inputRef = useRef<TextareaRenderable | null>(null);
40
+
40
41
  useEffect(() => {
41
42
  // open with the caret after the seeded value, like a text field
42
43
  if (inputRef.current) inputRef.current.cursorOffset = value.length;
43
44
  // eslint-disable-next-line react-hooks/exhaustive-deps
44
45
  }, []);
45
46
  if (!isOpen) return null;
47
+
46
48
  return (
47
49
  <Dialog isOpen title={title} width={Math.min(54, terminalWidth - 6)} height={7} theme={theme}>
48
50
  <box