@cueloop/client 0.1.0-alpha.66 → 0.1.0-alpha.67

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 (115) hide show
  1. package/package.json +6 -5
  2. package/src/App.autoclose.test.tsx +2 -2
  3. package/src/App.diff.test.tsx +71 -39
  4. package/src/App.plan-review.test.tsx +20 -21
  5. package/src/App.readonly.test.tsx +7 -3
  6. package/src/App.test.tsx +73 -14
  7. package/src/App.tsx +375 -129
  8. package/src/App.walk.test.tsx +14 -15
  9. package/src/App.workbench.test.tsx +390 -0
  10. package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
  11. package/src/annotation-palette.ts +61 -0
  12. package/src/app-screens.tsx +176 -45
  13. package/src/app-view-model.ts +7 -65
  14. package/src/components/AnnotationCard.tsx +1 -1
  15. package/src/components/AnnotationCards.stories.tsx +87 -0
  16. package/src/components/AnnotationCards.tsx +303 -0
  17. package/src/components/AppHeader.stories.tsx +50 -0
  18. package/src/components/AppHeader.tsx +83 -0
  19. package/src/components/AppShell.stories.tsx +47 -42
  20. package/src/components/AppShell.tsx +212 -100
  21. package/src/components/AppShell.zoom.test.tsx +94 -0
  22. package/src/components/ChangesColumn.tsx +49 -26
  23. package/src/components/DiffSheet.scroll.test.tsx +38 -55
  24. package/src/components/DiffSheet.split.test.tsx +140 -0
  25. package/src/components/DiffSheet.stories.tsx +55 -53
  26. package/src/components/DiffSheet.syntax.test.tsx +24 -7
  27. package/src/components/DiffSheet.tsx +607 -276
  28. package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
  29. package/src/components/DiscussionMarkerRail.tsx +181 -0
  30. package/src/components/EditorGrid.stories.tsx +59 -0
  31. package/src/components/EditorGrid.tabs.test.tsx +105 -0
  32. package/src/components/EditorGrid.tsx +338 -0
  33. package/src/components/EditorGrid.zoom.test.tsx +68 -0
  34. package/src/components/FileContentsView.stories.tsx +16 -0
  35. package/src/components/FileContentsView.test.tsx +29 -0
  36. package/src/components/FileContentsView.tsx +78 -0
  37. package/src/components/InboxList.tsx +159 -30
  38. package/src/components/PaneGrid.stories.tsx +84 -0
  39. package/src/components/PaneGrid.tsx +128 -0
  40. package/src/components/PanelColumn.stories.tsx +25 -0
  41. package/src/components/PanelColumn.tsx +95 -0
  42. package/src/components/ProjectTreeView.stories.tsx +25 -0
  43. package/src/components/ProjectTreeView.test.tsx +32 -0
  44. package/src/components/ProjectTreeView.tsx +82 -0
  45. package/src/components/PromptDialog.tsx +8 -1
  46. package/src/components/RootOverlay.stories.tsx +44 -0
  47. package/src/components/RootOverlay.tsx +35 -0
  48. package/src/components/SettingsDialog.tsx +2 -1
  49. package/src/components/SurfaceScrollbar.stories.tsx +34 -0
  50. package/src/components/SurfaceScrollbar.test.tsx +111 -0
  51. package/src/components/SurfaceScrollbar.tsx +93 -0
  52. package/src/components/ThreadFooter.stories.tsx +2 -2
  53. package/src/components/ThreadFooter.test.tsx +31 -0
  54. package/src/components/ThreadFooter.tsx +12 -7
  55. package/src/components/ThreadView.tsx +62 -1440
  56. package/src/components/ThreadsSidebar.tsx +14 -1
  57. package/src/components/Tooltip.stories.tsx +26 -0
  58. package/src/components/Tooltip.tsx +72 -0
  59. package/src/components/WelcomeSurface.stories.tsx +12 -0
  60. package/src/components/WelcomeSurface.tsx +52 -0
  61. package/src/components/__snapshots__/stories.test.tsx.snap +509 -559
  62. package/src/components/diff-sheet-layout.test.ts +125 -55
  63. package/src/components/diff-sheet-layout.ts +3 -103
  64. package/src/components/editor-grid.test.ts +86 -0
  65. package/src/components/editor-grid.ts +138 -0
  66. package/src/components/fade-title.test.ts +49 -0
  67. package/src/components/fade-title.ts +61 -0
  68. package/src/components/file-tree.test.ts +15 -0
  69. package/src/components/file-tree.ts +59 -4
  70. package/src/components/pages/Workspace.stories.tsx +2 -2
  71. package/src/components/primitives/IconButton.tsx +21 -2
  72. package/src/components/primitives/Tree.tsx +13 -4
  73. package/src/components/primitives/icons.ts +27 -4
  74. package/src/components/session-tree.test.ts +20 -0
  75. package/src/components/session-tree.ts +16 -2
  76. package/src/components/stories-app.tsx +2 -2
  77. package/src/config.test.ts +20 -49
  78. package/src/config.ts +26 -28
  79. package/src/diff-fold.test.ts +78 -0
  80. package/src/diff-fold.ts +126 -0
  81. package/src/diff-syntax.test.ts +16 -0
  82. package/src/diff-syntax.ts +6 -3
  83. package/src/discussions.ts +79 -0
  84. package/src/intent-dispatch.test.ts +31 -11
  85. package/src/intent-dispatch.ts +93 -48
  86. package/src/key-bindings.test.ts +1 -1
  87. package/src/keymap.test.ts +3 -25
  88. package/src/keymap.ts +17 -18
  89. package/src/mark-runs.ts +120 -0
  90. package/src/pty.ts +10 -2
  91. package/src/run.ts +17 -7
  92. package/src/serve.test.ts +4 -8
  93. package/src/session-controller.curate.test.ts +75 -0
  94. package/src/session-controller.share.test.ts +1 -0
  95. package/src/session-controller.tree.test.ts +26 -0
  96. package/src/session-controller.ts +153 -29
  97. package/src/slash-palette.ts +83 -0
  98. package/src/split-diff.test.ts +110 -0
  99. package/src/split-diff.ts +149 -0
  100. package/src/test-support.test.ts +3 -3
  101. package/src/test-support.ts +39 -8
  102. package/src/thread-chords.test.ts +25 -9
  103. package/src/thread-chords.ts +33 -8
  104. package/src/use-annotation-surface.tsx +837 -0
  105. package/src/use-changes-workbench.ts +151 -0
  106. package/src/use-frame-measure.ts +38 -0
  107. package/src/use-settings-dialog.tsx +14 -24
  108. package/src/use-terminal-virtualizer.ts +233 -0
  109. package/src/view-diff.ts +114 -0
  110. package/src/components/ReviewPanel.stories.tsx +0 -100
  111. package/src/components/ReviewPanel.tsx +0 -103
  112. package/src/components/ReviewRail.stories.tsx +0 -232
  113. package/src/components/ReviewRail.tsx +0 -327
  114. package/src/review-panel.test.ts +0 -113
  115. package/src/review-panel.ts +0 -83
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Shared driving helpers for the virtual-terminal App suites: key presses
3
- * settle through a macrotask yield (input parser + React scheduler), one
4
- * render pass, and the renderer's visual-idle wait - no fixed-duration
2
+ * Shared driving helpers for the virtual-terminal App suites: an input settles
3
+ * when its painted frame stops changing across consecutive event-loop turns
4
+ * (macrotask yield, render pass, visual-idle wait per turn) - no fixed-duration
5
5
  * sleeps. Waits on daemon round-trips go through waitForFrame/waitFor with
6
6
  * generous pass budgets.
7
7
  */
@@ -40,15 +40,44 @@ export function allowEventLoopUpdates(): void {
40
40
  globalThis.IS_REACT_ACT_ENVIRONMENT = false;
41
41
  }
42
42
 
43
+ /** Turns of the event loop a frame must survive unchanged before an input counts as settled. */
44
+ const SETTLE_QUIET_TURNS = 2;
45
+
46
+ /** Upper bound on settle turns, so a surface that keeps changing never stalls a test. */
47
+ const SETTLE_MAX_TURNS = 40;
48
+
49
+ /** The painted frame with its colors, so a caret cell (background only) counts as a change. */
50
+ function paintedFrame(setup: TestRendererSetup): string {
51
+ return JSON.stringify(
52
+ setup
53
+ .captureSpans()
54
+ .lines.map((line) =>
55
+ line.spans.map((span) => [span.text, span.fg?.toInts(), span.bg?.toInts()]),
56
+ ),
57
+ );
58
+ }
59
+
43
60
  /**
44
- * One macrotask yield lets the input parser and the React scheduler run,
45
- * then a render pass commits the result before the visual-idle wait.
61
+ * Wait for an input to land: React commits its state on its own scheduler, which the renderer's
62
+ * idle check cannot see, so on a slow machine a single idle wait returns between the key and its
63
+ * paint. Instead, run turns of the loop until the painted frame is unchanged across
64
+ * `SETTLE_QUIET_TURNS` consecutive turns.
46
65
  */
47
66
  export async function settle(setup: TestRendererSetup): Promise<void> {
48
67
  allowEventLoopUpdates();
49
- await new Promise((resolve) => setTimeout(resolve, 0));
50
- await setup.renderOnce();
51
- await setup.waitForVisualIdle();
68
+ let previous = "";
69
+ let quietTurns = 0;
70
+
71
+ for (let turn = 0; turn < SETTLE_MAX_TURNS; turn++) {
72
+ await new Promise((resolve) => setTimeout(resolve, 0));
73
+ await setup.renderOnce();
74
+ await setup.waitForVisualIdle();
75
+ const frame = paintedFrame(setup);
76
+
77
+ quietTurns = frame === previous ? quietTurns + 1 : 0;
78
+ if (quietTurns >= SETTLE_QUIET_TURNS) return;
79
+ previous = frame;
80
+ }
52
81
  }
53
82
 
54
83
  /** Drive one key press: letters type as text; named keys use KeyCodes ids. */
@@ -58,6 +87,8 @@ export async function press(setup: TestRendererSetup, key: string): Promise<void
58
87
  else if (key === "backspace") setup.mockInput.pressKey("BACKSPACE");
59
88
  else if (key === "left") setup.mockInput.pressKey("ARROW_LEFT");
60
89
  else if (key === "right") setup.mockInput.pressKey("ARROW_RIGHT");
90
+ else if (key === "up") setup.mockInput.pressKey("ARROW_UP");
91
+ else if (key === "down") setup.mockInput.pressKey("ARROW_DOWN");
61
92
  else return typeText(setup, key);
62
93
  await settle(setup);
63
94
  }
@@ -21,8 +21,31 @@ describe("resolveThreadChord", () => {
21
21
  // Assert
22
22
  expect(resolveThreadChord({ name: "e", ctrl: true }, owner)).toEqual({ type: "edit" });
23
23
  expect(resolveThreadChord({ name: "s", ctrl: true }, owner)).toEqual({ type: "share" });
24
- expect(resolveThreadChord({ name: "r", ctrl: true }, owner)).toEqual({
25
- type: "cycleReviewPanel",
24
+ });
25
+
26
+ test("in a diff, option chords act on the caret's row and file; the rest still reach the rail", () => {
27
+ // Arrange
28
+ const diff = { ...owner, isDiff: true };
29
+
30
+ // Assert - the code-row primitives
31
+ expect(resolveThreadChord({ name: "x", meta: true }, diff)).toEqual({ type: "rejectChange" });
32
+ expect(resolveThreadChord({ name: "X", meta: true }, diff)).toEqual({ type: "rejectHunk" });
33
+ expect(resolveThreadChord({ name: "u", meta: true }, diff)).toEqual({
34
+ type: "restoreCuration",
35
+ });
36
+ expect(resolveThreadChord({ name: "c", meta: true }, diff)).toEqual({ type: "foldFile" });
37
+ expect(resolveThreadChord({ name: "d", meta: true }, diff)).toEqual({ type: "toggleDiffView" });
38
+ expect(resolveThreadChord({ name: "k", meta: true }, diff)).toEqual({ type: "walkStart" });
39
+ // the rail letters are untouched
40
+ expect(resolveThreadChord({ name: "n", meta: true }, diff)).toEqual({ type: "nextAnnotation" });
41
+ // rejecting is the owner's, and closed once resolved
42
+ expect(resolveThreadChord({ name: "x", meta: true }, { ...diff, isOwner: false })).toEqual({
43
+ type: "status",
44
+ message: "observer - read-only",
45
+ });
46
+ expect(resolveThreadChord({ name: "k", meta: true }, { ...diff, resolved: true })).toEqual({
47
+ type: "status",
48
+ message: "review submitted - read-only",
26
49
  });
27
50
  });
28
51
 
@@ -43,10 +66,6 @@ describe("resolveThreadChord", () => {
43
66
  expect(resolveThreadChord({ name: "u", meta: true }, owner)).toEqual({
44
67
  type: "restoreCuration",
45
68
  });
46
- expect(resolveThreadChord({ name: "w", meta: true }, owner)).toEqual({
47
- type: "resizeReviewPanel",
48
- direction: 1,
49
- });
50
69
  expect(resolveThreadChord({ name: "x", meta: true }, { ...owner, isOwner: false })).toEqual({
51
70
  type: "status",
52
71
  message: "observer - read-only",
@@ -76,9 +95,6 @@ describe("resolveThreadChord", () => {
76
95
  expect(resolveThreadChord({ name: "return", ctrl: true }, collaborator)).toEqual(readOnly);
77
96
  expect(resolveThreadChord({ name: "e", ctrl: true }, collaborator)).toEqual(readOnly);
78
97
  expect(resolveThreadChord({ name: "s", ctrl: true }, collaborator)).toEqual(readOnly);
79
- expect(resolveThreadChord({ name: "r", ctrl: true }, collaborator)).toEqual({
80
- type: "cycleReviewPanel",
81
- });
82
98
  expect(
83
99
  resolveThreadChord({ name: "return", ctrl: true }, { ...owner, resolved: true }),
84
100
  ).toBeNull();
@@ -24,8 +24,19 @@ export interface ThreadChordContext {
24
24
  resolved: boolean;
25
25
  /** The rail shows the tree: next / previous move its selection instead of the cards. */
26
26
  treeActive: boolean;
27
+ /** A diff review: the block chords act on the code row under the caret (reject, fold, walk). */
28
+ isDiff?: boolean;
27
29
  }
28
30
 
31
+ /** The diff-review chords: option plus a letter, acting on the caret's row or file. */
32
+ export const DIFF_CHORD_ENTRIES = [
33
+ { keys: "⌥x / ⌥X", label: "reject the change / the hunk" },
34
+ { keys: "⌥u", label: "restore the last rejection" },
35
+ { keys: "⌥c", label: "collapse the file to its band" },
36
+ { keys: "⌥d", label: "unified / split (when zoomed)" },
37
+ { keys: "⌥k", label: "start the guided walk" },
38
+ ] as const;
39
+
29
40
  /** The cheatsheet rows for the chords, in the same order they are resolved. */
30
41
  export const THREAD_CHORD_ENTRIES = [
31
42
  { keys: "⌃enter", label: "submit the review" },
@@ -70,11 +81,32 @@ export function resolveThreadChord(
70
81
  return context.resolved ? null : { type: "openSubmit" };
71
82
  }
72
83
  if (key.ctrl) return resolveSessionChord(key.name, context);
73
- if (key.meta) return resolveRailChord(key.name, context);
84
+ if (key.meta)
85
+ return (
86
+ (context.isDiff && resolveDiffChord(key.name, context)) || resolveRailChord(key.name, context)
87
+ );
74
88
 
75
89
  return null;
76
90
  }
77
91
 
92
+ /** The diff's row and file chords; null lets the rail chords answer the letter. */
93
+ function resolveDiffChord(name: string, context: ThreadChordContext): Intent | null {
94
+ switch (name) {
95
+ case "x":
96
+ return mutating({ type: "rejectChange" }, context);
97
+ case "X":
98
+ return mutating({ type: "rejectHunk" }, context);
99
+ case "c":
100
+ return { type: "foldFile" };
101
+ case "d":
102
+ return { type: "toggleDiffView" };
103
+ case "k":
104
+ return context.resolved ? RESOLVED : { type: "walkStart" };
105
+ default:
106
+ return null;
107
+ }
108
+ }
109
+
78
110
  /** The answers a blocked primitive gets - the same words the keymap uses. */
79
111
  const READ_ONLY: Intent = { type: "status", message: "observer - read-only" };
80
112
  const RESOLVED: Intent = { type: "status", message: "review submitted - read-only" };
@@ -93,8 +125,6 @@ function resolveSessionChord(name: string, context: ThreadChordContext): Intent
93
125
  return mutating({ type: "edit" }, context);
94
126
  case "s":
95
127
  return context.isOwner ? { type: "share" } : READ_ONLY;
96
- case "r":
97
- return { type: "cycleReviewPanel" };
98
128
  default:
99
129
  return null;
100
130
  }
@@ -133,11 +163,6 @@ function resolveRailChord(name: string, context: ThreadChordContext): Intent | n
133
163
  return mutating({ type: "cut" }, context);
134
164
  case "u":
135
165
  return mutating({ type: "restoreCuration" }, context);
136
- // letters only: escape-prefixed punctuation reads as control sequences, not keys
137
- case "w":
138
- return { type: "resizeReviewPanel", direction: 1 };
139
- case "s":
140
- return { type: "resizeReviewPanel", direction: -1 };
141
166
  default:
142
167
  return null;
143
168
  }