@cueloop/client 0.1.0-alpha.76 → 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 (82) 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.identity.test.tsx +78 -0
  6. package/src/App.plan-review.test.tsx +16 -0
  7. package/src/App.share-choice.test.tsx +105 -0
  8. package/src/App.submit-confirm.test.tsx +34 -5
  9. package/src/App.test.tsx +2 -2
  10. package/src/App.toast.test.tsx +3 -1
  11. package/src/App.tsx +180 -5
  12. package/src/App.walk.test.tsx +2 -2
  13. package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
  14. package/src/ansi-screen.test.ts +28 -0
  15. package/src/ansi-screen.ts +72 -0
  16. package/src/app-screens.tsx +20 -3
  17. package/src/app-view-model.ts +4 -0
  18. package/src/components/AnnotatableFileView.tsx +10 -2
  19. package/src/components/AnnotationCards.tsx +40 -6
  20. package/src/components/AnsiScreen.stories.tsx +12 -0
  21. package/src/components/AnsiScreen.tsx +28 -0
  22. package/src/components/CompletionOverlay.stories.tsx +15 -1
  23. package/src/components/CompletionOverlay.tsx +22 -5
  24. package/src/components/ConfirmCard.stories.tsx +2 -0
  25. package/src/components/ConfirmCard.tsx +45 -18
  26. package/src/components/ConfirmDialog.tsx +11 -12
  27. package/src/components/DiffContentView.tsx +9 -5
  28. package/src/components/FileContentsView.tsx +1 -1
  29. package/src/components/GridTabContent.tsx +2 -0
  30. package/src/components/ManageAccessDialog.stories.tsx +36 -0
  31. package/src/components/ManageAccessDialog.test.tsx +94 -0
  32. package/src/components/ManageAccessDialog.tsx +122 -0
  33. package/src/components/PromptDialog.test.tsx +2 -2
  34. package/src/components/PromptDialog.tsx +9 -10
  35. package/src/components/ShareChoiceDialog.stories.tsx +34 -0
  36. package/src/components/ShareChoiceDialog.test.tsx +103 -0
  37. package/src/components/ShareChoiceDialog.tsx +80 -0
  38. package/src/components/SlashComposer.stories.tsx +37 -0
  39. package/src/components/SlashComposer.tsx +87 -0
  40. package/src/components/ThreadTree.tsx +11 -6
  41. package/src/components/ThreadView.selection.test.tsx +88 -1
  42. package/src/components/ThreadView.slash.test.tsx +0 -1
  43. package/src/components/ThreadView.tsx +71 -8
  44. package/src/components/Toast.tsx +0 -5
  45. package/src/components/WalkWizard.stories.tsx +2 -2
  46. package/src/components/WalkWizard.tsx +10 -12
  47. package/src/components/__snapshots__/stories.test.tsx.snap +277 -61
  48. package/src/components/primitives/Card.tsx +3 -0
  49. package/src/components/primitives/DialogActions.stories.tsx +20 -0
  50. package/src/components/primitives/DialogActions.tsx +39 -0
  51. package/src/components/session-tree.ts +7 -6
  52. package/src/components/stories-app.tsx +13 -4
  53. package/src/components/stories.test.tsx +8 -1
  54. package/src/components/story-tree.test.ts +32 -24
  55. package/src/components/story.ts +32 -15
  56. package/src/config-identity.test.ts +78 -0
  57. package/src/config.ts +44 -1
  58. package/src/diff-intraline.test.ts +75 -2
  59. package/src/diff-intraline.ts +95 -26
  60. package/src/github-identity.test.ts +58 -0
  61. package/src/github-identity.ts +41 -0
  62. package/src/intent-dispatch.test.ts +17 -2
  63. package/src/intent-dispatch.ts +7 -1
  64. package/src/keymap.test.ts +2 -2
  65. package/src/keymap.ts +1 -1
  66. package/src/pasted-image.test.ts +67 -0
  67. package/src/pasted-image.ts +60 -0
  68. package/src/run.ts +31 -19
  69. package/src/serve.ts +1 -1
  70. package/src/share.ts +11 -3
  71. package/src/slash-palette.test.ts +17 -0
  72. package/src/slash-palette.ts +11 -0
  73. package/src/thread-controller.curate.test.ts +1 -0
  74. package/src/thread-controller.derivation.test.ts +62 -0
  75. package/src/thread-controller.live-diff.test.ts +1 -0
  76. package/src/thread-controller.optimistic.test.ts +69 -0
  77. package/src/thread-controller.share.test.ts +36 -2
  78. package/src/thread-controller.switch-cache.test.ts +146 -0
  79. package/src/thread-controller.tree.test.ts +1 -0
  80. package/src/thread-controller.ts +171 -24
  81. package/src/use-annotation-surface.tsx +53 -29
  82. package/src/use-settings-dialog.tsx +24 -0
@@ -90,7 +90,6 @@ describe("the command leader", () => {
90
90
 
91
91
  // the command fired and no composer opened (a bare "x" would have started a draft)
92
92
  expect(commands).toEqual(["x"]);
93
- expect(setup.captureCharFrame()).not.toContain("enter save");
94
93
 
95
94
  setup.renderer.destroy();
96
95
  });
@@ -13,8 +13,9 @@
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
- import type { Thread } from "@cueloop/schema";
18
+ import type { Annotation, Thread } from "@cueloop/schema";
18
19
  import { displayText, type DisplayBlock, type Mark } from "../view-plan";
19
20
  import type { TextSpan } from "../thread-selection";
20
21
  import type { QuickAction } from "../config";
@@ -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
  /**
@@ -127,6 +132,8 @@ export interface ThreadViewProps {
127
132
  onAnnotate: (span: TextSpan, body: string) => void;
128
133
  onReply: (rootAnnotationId: string, body: string) => void;
129
134
  onUpdateAnnotation: (id: string, body: string) => void;
135
+ /** The author's display name for a comment's hover tooltip. */
136
+ resolveAuthorLabel?: (annotation: Annotation) => string | undefined;
130
137
  leaderCombos?: readonly string[];
131
138
  onLeaderCommand?: (key: KeyEvent) => void;
132
139
  onExit: () => void;
@@ -150,6 +157,7 @@ export function ThreadView({
150
157
  onAnnotate,
151
158
  onReply,
152
159
  onUpdateAnnotation,
160
+ resolveAuthorLabel,
153
161
  leaderCombos,
154
162
  onLeaderCommand,
155
163
  onExit,
@@ -178,24 +186,43 @@ export function ThreadView({
178
186
  onAnnotate,
179
187
  onReply,
180
188
  onUpdateAnnotation,
189
+ resolveAuthorLabel,
181
190
  leaderCombos,
182
191
  onLeaderCommand,
183
192
  onExit,
184
193
  });
185
194
  const { palette, discussions } = surface;
186
195
  const scrollRef = useRef<ScrollBoxRenderable | null>(null);
196
+ const { height: terminalHeight } = useTerminalDimensions();
187
197
  const viewWidth = useFrameMeasure(
188
198
  () => scrollRef.current?.content?.width ?? 0,
189
199
  (left, right) => left === right,
190
200
  0,
191
201
  );
192
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
+
193
223
  useEffect(() => {
194
- try {
195
- scrollRef.current?.scrollChildIntoView(`discussion-block-${surface.revealBlockIndex}`);
196
- } catch {
197
- // best-effort reveal
198
- }
224
+ virtual.scrollToIndex(surface.revealBlockIndex);
225
+ // eslint-disable-next-line react-hooks/exhaustive-deps
199
226
  }, [surface.revealBlockIndex]);
200
227
 
201
228
  interface LineContext {
@@ -291,18 +318,54 @@ export function ThreadView({
291
318
  </text>
292
319
  ) : null;
293
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
+
294
326
  return (
295
327
  <box
296
328
  key={`discussion-block-${blockIndex}`}
297
329
  id={`discussion-block-${blockIndex}`}
298
- style={{ flexDirection: "column", marginTop: blockIndex === 0 || tight ? 0 : 1 }}
330
+ ref={virtual.measureRef(blockIndex)}
331
+ style={{ flexDirection: "column" }}
299
332
  >
333
+ {leadingGap}
300
334
  {languageRow}
301
335
  {lineRows}
302
336
  </box>
303
337
  );
304
338
  };
305
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
+
306
369
  return (
307
370
  <box style={{ flexGrow: 1, flexDirection: "row" }} {...surface.rootMouseProps}>
308
371
  <box style={{ flexGrow: 1, flexDirection: "column" }}>
@@ -320,7 +383,7 @@ export function ThreadView({
320
383
  focused={false}
321
384
  verticalScrollbarOptions={{ visible: false }}
322
385
  >
323
- {display.map((_block, blockIndex) => blockNodeFor(blockIndex))}
386
+ {virtualBlocks()}
324
387
  </scrollbox>
325
388
  </box>
326
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
  <>