@cueloop/client 0.1.0-alpha.77 → 0.1.0-alpha.78

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 (63) hide show
  1. package/package.json +5 -5
  2. package/src/App.autoclose.test.tsx +3 -3
  3. package/src/App.diff.test.tsx +14 -2
  4. package/src/App.export.test.tsx +1 -2
  5. package/src/App.plan-review.test.tsx +16 -0
  6. package/src/App.submit-confirm.test.tsx +34 -5
  7. package/src/App.test.tsx +2 -2
  8. package/src/App.tsx +4 -0
  9. package/src/App.walk.test.tsx +2 -2
  10. package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
  11. package/src/ansi-screen.test.ts +28 -0
  12. package/src/ansi-screen.ts +72 -0
  13. package/src/app-screens.tsx +7 -3
  14. package/src/app-view-model.ts +3 -0
  15. package/src/components/AnnotatableFileView.tsx +1 -1
  16. package/src/components/AnnotationCards.tsx +18 -2
  17. package/src/components/AnsiScreen.stories.tsx +12 -0
  18. package/src/components/AnsiScreen.tsx +28 -0
  19. package/src/components/CompletionOverlay.stories.tsx +15 -1
  20. package/src/components/CompletionOverlay.tsx +22 -5
  21. package/src/components/ConfirmCard.stories.tsx +2 -0
  22. package/src/components/ConfirmCard.tsx +45 -18
  23. package/src/components/ConfirmDialog.tsx +11 -12
  24. package/src/components/DiffContentView.tsx +4 -4
  25. package/src/components/FileContentsView.tsx +1 -1
  26. package/src/components/ManageAccessDialog.tsx +13 -11
  27. package/src/components/PromptDialog.test.tsx +2 -2
  28. package/src/components/PromptDialog.tsx +9 -10
  29. package/src/components/ShareChoiceDialog.tsx +0 -2
  30. package/src/components/SlashComposer.stories.tsx +37 -0
  31. package/src/components/SlashComposer.tsx +87 -0
  32. package/src/components/ThreadTree.tsx +11 -6
  33. package/src/components/ThreadView.selection.test.tsx +88 -1
  34. package/src/components/ThreadView.slash.test.tsx +0 -1
  35. package/src/components/ThreadView.tsx +66 -7
  36. package/src/components/Toast.tsx +0 -5
  37. package/src/components/WalkWizard.stories.tsx +2 -2
  38. package/src/components/WalkWizard.tsx +10 -12
  39. package/src/components/__snapshots__/stories.test.tsx.snap +219 -89
  40. package/src/components/primitives/Card.tsx +3 -0
  41. package/src/components/primitives/DialogActions.stories.tsx +20 -0
  42. package/src/components/primitives/DialogActions.tsx +39 -0
  43. package/src/components/session-tree.ts +7 -6
  44. package/src/components/stories-app.tsx +13 -4
  45. package/src/components/stories.test.tsx +8 -1
  46. package/src/components/story-tree.test.ts +32 -24
  47. package/src/components/story.ts +32 -15
  48. package/src/diff-intraline.test.ts +75 -2
  49. package/src/diff-intraline.ts +95 -26
  50. package/src/keymap.test.ts +2 -2
  51. package/src/keymap.ts +0 -1
  52. package/src/pasted-image.test.ts +67 -0
  53. package/src/pasted-image.ts +60 -0
  54. package/src/run.ts +31 -19
  55. package/src/serve.ts +1 -1
  56. package/src/share.ts +1 -1
  57. package/src/slash-palette.test.ts +17 -0
  58. package/src/slash-palette.ts +11 -0
  59. package/src/thread-controller.derivation.test.ts +62 -0
  60. package/src/thread-controller.optimistic.test.ts +69 -0
  61. package/src/thread-controller.switch-cache.test.ts +146 -0
  62. package/src/thread-controller.ts +157 -24
  63. package/src/use-annotation-surface.tsx +43 -27
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import React, { useEffect, useRef } from "react";
16
+ import { useTerminalDimensions } from "@opentui/react";
16
17
  import type { KeyEvent, ScrollBoxRenderable } from "@opentui/core";
17
18
  import type { Annotation, Thread } from "@cueloop/schema";
18
19
  import { displayText, type DisplayBlock, type Mark } from "../view-plan";
@@ -23,10 +24,14 @@ import type { Theme } from "../theme";
23
24
  import { BOLD, CUT, UNDERLINE } from "../annotation-palette";
24
25
  import { lineMarkRanges, runsFor, wrapLines, type MarkRange, type VisualLine } from "../mark-runs";
25
26
  import { useFrameMeasure } from "../use-frame-measure";
27
+ import { useTerminalVirtualizer } from "../use-terminal-virtualizer";
26
28
  import { useAnnotationSurface, type LineSource } from "../use-annotation-surface";
27
29
  import { DiscussionMarkerRail } from "./DiscussionMarkerRail";
28
30
  import { useComponentTheme } from "./theme-context";
29
31
 
32
+ /** Blocks kept mounted beyond the viewport, so a scroll step or a drag past the edge never shows a gap. */
33
+ const OVERSCAN_BLOCKS = 8;
34
+
30
35
  export { lighten } from "../annotation-palette";
31
36
 
32
37
  /**
@@ -188,18 +193,36 @@ export function ThreadView({
188
193
  });
189
194
  const { palette, discussions } = surface;
190
195
  const scrollRef = useRef<ScrollBoxRenderable | null>(null);
196
+ const { height: terminalHeight } = useTerminalDimensions();
191
197
  const viewWidth = useFrameMeasure(
192
198
  () => scrollRef.current?.content?.width ?? 0,
193
199
  (left, right) => left === right,
194
200
  0,
195
201
  );
196
202
 
203
+ // only the viewport's blocks (plus an overscan) mount; a block is estimated at its wrapped-line
204
+ // count until its box, cards included, is measured. The initial viewport seeds the first render so a
205
+ // freshly mounted thread (a switch from a diff, another project) windows its first paint rather than
206
+ // building every block, and never flashes blank.
207
+ const estimateBlockRows = (index: number): number => {
208
+ const block = display[index];
209
+
210
+ if (!block) return 1;
211
+ const usable = viewWidth > 0 ? Math.max(1, viewWidth - 6) : 40;
212
+ const lines = Math.max(1, Math.ceil(displayText(block).length / usable));
213
+
214
+ return lines + (index === 0 ? 0 : 1);
215
+ };
216
+ const virtual = useTerminalVirtualizer({
217
+ scrollbox: scrollRef,
218
+ count: display.length,
219
+ estimateSize: estimateBlockRows,
220
+ overscan: OVERSCAN_BLOCKS,
221
+ });
222
+
197
223
  useEffect(() => {
198
- try {
199
- scrollRef.current?.scrollChildIntoView(`discussion-block-${surface.revealBlockIndex}`);
200
- } catch {
201
- // best-effort reveal
202
- }
224
+ virtual.scrollToIndex(surface.revealBlockIndex);
225
+ // eslint-disable-next-line react-hooks/exhaustive-deps
203
226
  }, [surface.revealBlockIndex]);
204
227
 
205
228
  interface LineContext {
@@ -295,18 +318,54 @@ export function ThreadView({
295
318
  </text>
296
319
  ) : null;
297
320
 
321
+ // the leading gap rides inside the measured box (not marginTop, which sits outside it) so the
322
+ // virtualizer's spacer math counts it
323
+ const leadingGap =
324
+ blockIndex === 0 || tight ? null : <box key="lead-gap" style={{ height: 1 }} />;
325
+
298
326
  return (
299
327
  <box
300
328
  key={`discussion-block-${blockIndex}`}
301
329
  id={`discussion-block-${blockIndex}`}
302
- style={{ flexDirection: "column", marginTop: blockIndex === 0 || tight ? 0 : 1 }}
330
+ ref={virtual.measureRef(blockIndex)}
331
+ style={{ flexDirection: "column" }}
303
332
  >
333
+ {leadingGap}
304
334
  {languageRow}
305
335
  {lineRows}
306
336
  </box>
307
337
  );
308
338
  };
309
339
 
340
+ const virtualBlocks = (): React.ReactNode[] => {
341
+ const nodes: React.ReactNode[] = [];
342
+ const firstItem = virtual.items[0];
343
+ const lastItem = virtual.items.at(-1);
344
+
345
+ // the very first render has no measured viewport yet, so the virtualizer's window is empty; paint a
346
+ // viewport-worth from the top rather than blank or the whole document, then it takes over next frame
347
+ if (!firstItem || !lastItem) {
348
+ let mountedRows = 0;
349
+
350
+ for (let index = 0; index < display.length; index++) {
351
+ nodes.push(blockNodeFor(index));
352
+ mountedRows += estimateBlockRows(index);
353
+ if (mountedRows > terminalHeight + OVERSCAN_BLOCKS) break;
354
+ }
355
+
356
+ return nodes;
357
+ }
358
+ if (firstItem.start > 0) {
359
+ nodes.push(<box key="spacer-above" style={{ height: firstItem.start }} />);
360
+ }
361
+ for (const item of virtual.items) nodes.push(blockNodeFor(item.index));
362
+ const below = virtual.totalSize - lastItem.end;
363
+
364
+ if (below > 0) nodes.push(<box key="spacer-below" style={{ height: below }} />);
365
+
366
+ return nodes;
367
+ };
368
+
310
369
  return (
311
370
  <box style={{ flexGrow: 1, flexDirection: "row" }} {...surface.rootMouseProps}>
312
371
  <box style={{ flexGrow: 1, flexDirection: "column" }}>
@@ -324,7 +383,7 @@ export function ThreadView({
324
383
  focused={false}
325
384
  verticalScrollbarOptions={{ visible: false }}
326
385
  >
327
- {display.map((_block, blockIndex) => blockNodeFor(blockIndex))}
386
+ {virtualBlocks()}
328
387
  </scrollbox>
329
388
  </box>
330
389
  <DiscussionMarkerRail
@@ -34,11 +34,6 @@ export function Toast({ title, body, theme }: ToastProps): React.ReactNode {
34
34
  }}
35
35
  >
36
36
  <text fg={tokens.text}>{body}</text>
37
- <box style={{ flexGrow: 1 }} />
38
- <box style={{ flexDirection: "row" }}>
39
- <box style={{ flexGrow: 1 }} />
40
- <text fg={tokens.textDim}>[esc]</text>
41
- </box>
42
37
  </box>
43
38
  </Dialog>
44
39
  );
@@ -81,7 +81,7 @@ export const EndCard: Story = {
81
81
  onBack={noop}
82
82
  />
83
83
  ),
84
- // the end card wears the green border and the solid submit button
84
+ // accent border and solid submit; the all-viewed line turns green
85
85
  expectedColors: [DARK.green, DARK.accent, DARK.accentInk],
86
86
  };
87
87
 
@@ -96,5 +96,5 @@ export const EndCardPartialPass: Story = {
96
96
  onBack={noop}
97
97
  />
98
98
  ),
99
- expectedColors: [DARK.green],
99
+ expectedColors: [DARK.accent, DARK.accentInk],
100
100
  };
@@ -13,8 +13,7 @@ import { useComponentTheme } from "./theme-context";
13
13
  import type { WalkFile } from "../walk";
14
14
  import { viewedCount } from "../walk";
15
15
  import { Card } from "./primitives/Card";
16
- import { Button } from "./primitives/Button";
17
- import { Toolbar } from "./primitives/Toolbar";
16
+ import { DialogActions } from "./primitives/DialogActions";
18
17
 
19
18
  export interface WalkWizardProps {
20
19
  files: WalkFile[];
@@ -75,24 +74,23 @@ export function WalkWizard({
75
74
  <Card
76
75
  title=" walk complete "
77
76
  contentRows={3}
78
- borderColor={tokens.green}
77
+ borderColor={tokens.accent}
79
78
  backgroundColor={tokens.elevated}
80
79
  theme={theme}
81
80
  >
82
- <text fg={tokens.text}>
81
+ <text fg={viewed === files.length ? tokens.green : tokens.text}>
83
82
  {viewed === files.length
84
83
  ? `every file viewed (${viewed}/${files.length})`
85
84
  : `${viewed} of ${files.length} files viewed`}
86
85
  </text>
87
86
  <box style={{ height: 1 }} />
88
- <Toolbar>
89
- <Button variant="solid" marginRight={2} onPress={onSubmitRequest} theme={theme}>
90
- {" Submit review "}
91
- </Button>
92
- <Button onPress={onBack} theme={theme}>
93
- {" [ back "}
94
- </Button>
95
- </Toolbar>
87
+ <DialogActions
88
+ confirmLabel="submit"
89
+ cancelLabel="back"
90
+ onConfirm={onSubmitRequest}
91
+ onCancel={onBack}
92
+ theme={theme}
93
+ />
96
94
  </Card>
97
95
  ) : (
98
96
  <>