@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
@@ -56,13 +56,16 @@ async function renderApp(options: { readOnly?: boolean } = {}) {
56
56
  height: 32,
57
57
  },
58
58
  );
59
+
59
60
  await waitForText(setup, "cueloop");
61
+
60
62
  return setup;
61
63
  }
62
64
 
63
65
  /** Seed annotations directly through the daemon core (all on one block). */
64
66
  function seedAnnotations(count: number): void {
65
67
  const blocks = parseBlocks(PLAN);
68
+
66
69
  for (let index = 1; index <= count; index++) {
67
70
  server.core.sessionAnnotate(session.id, {
68
71
  id: `a_confirm_${index}`,
@@ -87,6 +90,7 @@ describe("rail submit confirm", () => {
87
90
  // Assert
88
91
  await waitForText(setup, "submit review");
89
92
  const frame = setup.captureCharFrame();
93
+
90
94
  // the card: title, verdict selector, word-buttons
91
95
  expect(frame).toContain("submit review");
92
96
  expect(frame).toContain("[Approve]"); // nothing pending: approve default
@@ -97,6 +101,7 @@ describe("rail submit confirm", () => {
97
101
  test("left/right cycles the verdict selector in the card", async () => {
98
102
  // Arrange
99
103
  const setup = await renderApp();
104
+
100
105
  await press(setup, "enter");
101
106
 
102
107
  // Assert
@@ -124,6 +129,7 @@ describe("rail submit confirm", () => {
124
129
  test("esc cancels the card and restores the plain Submit button", async () => {
125
130
  // Arrange
126
131
  const setup = await renderApp();
132
+
127
133
  await press(setup, "enter");
128
134
 
129
135
  // Assert
@@ -135,6 +141,7 @@ describe("rail submit confirm", () => {
135
141
  // Assert
136
142
  // a bare ESC settles after the parser's escape-sequence window
137
143
  const frame = await waitForTextGone(setup, "[Approve]");
144
+
138
145
  expect(frame).not.toContain("submit review");
139
146
  expect(frame).toContain("Submit review");
140
147
  });
@@ -150,6 +157,7 @@ describe("rail submit confirm", () => {
150
157
  // Assert
151
158
  await waitForText(setup, "[Changes]"); // pending items: request changes default
152
159
  const frame = setup.captureCharFrame();
160
+
153
161
  expect(frame).toContain("submit review");
154
162
 
155
163
  // Act
@@ -160,6 +168,7 @@ describe("rail submit confirm", () => {
160
168
  // the completion flow after submit is unchanged
161
169
  await waitForText(setup, "feedback sent");
162
170
  const stored = server.core.sessionGet(session.id);
171
+
163
172
  expect(stored.status).toBe("resolved");
164
173
  expect(stored.verdict!.kind).toBe("request_changes");
165
174
  });
@@ -194,6 +203,7 @@ describe("rail submit confirm", () => {
194
203
  // Assert
195
204
  await waitForText(setup, "note 12");
196
205
  const scrolledWithCard = setup.captureCharFrame();
206
+
197
207
  expect(scrolledWithCard).toContain("submit review");
198
208
  expect(scrolledWithCard).not.toContain("note 01");
199
209
  });
@@ -209,6 +219,7 @@ describe("rail submit confirm", () => {
209
219
  // Assert
210
220
  await waitForText(setup, "observer - read-only");
211
221
  const frame = setup.captureCharFrame();
222
+
212
223
  expect(frame).toContain("observer - read-only");
213
224
  expect(frame).not.toContain("1 annotations");
214
225
  expect(frame).not.toContain("[Changes]");
package/src/App.test.tsx CHANGED
@@ -59,8 +59,10 @@ async function renderApp(sessionId?: string) {
59
59
  width: 120,
60
60
  height: 32,
61
61
  });
62
+
62
63
  // the async daemon connect + first fetch land within the frame wait
63
64
  await waitForText(setup, "cueloop");
65
+
64
66
  return setup;
65
67
  }
66
68
 
@@ -71,6 +73,7 @@ describe("plan rendering", () => {
71
73
 
72
74
  // Assert
73
75
  const frame = setup.captureCharFrame();
76
+
74
77
  expect(frame).toContain("Migration Plan");
75
78
  expect(frame).toContain("Context");
76
79
  expect(frame).toContain("persists sessions to disk atomically");
@@ -93,6 +96,7 @@ describe("keyboard grammar", () => {
93
96
  // Assert
94
97
  const lines = setup.captureCharFrame().split("\n");
95
98
  const cursorLine = lines.find((line) => line.includes("▎"))!;
99
+
96
100
  expect(cursorLine).toContain("persists sessions");
97
101
  });
98
102
 
@@ -116,6 +120,7 @@ describe("keyboard grammar", () => {
116
120
  // Assert
117
121
  await waitForText(setup, "COMMENT");
118
122
  const stored = server.core.sessionGet(session.id);
123
+
119
124
  expect(stored.annotations.length).toBe(1);
120
125
  expect(stored.annotations[0]!.body).toBe("Define atomically.");
121
126
  });
@@ -136,12 +141,14 @@ describe("keyboard grammar", () => {
136
141
  // Assert
137
142
  await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
138
143
  const stored = server.core.sessionGet(session.id);
144
+
139
145
  expect(stored.annotations[0]!.anchor.quote).toBe("The daemon");
140
146
  });
141
147
 
142
148
  test("x cuts a block into the working copy; x restores it", async () => {
143
149
  // Arrange
144
150
  const setup = await renderApp();
151
+
145
152
  // move to "- move the store" (h1, h2, p, h2 = 4 steps in)
146
153
  for (let i = 0; i < 4; i++) await press(setup, "j");
147
154
 
@@ -161,6 +168,7 @@ describe("keyboard grammar", () => {
161
168
 
162
169
  test("e runs $EDITOR on the working copy and tracks the diff", async () => {
163
170
  const script = join(home, "fake-editor.sh");
171
+
164
172
  await Bun.write(script, `#!/bin/sh\nsed -i '' 's/atomically/very atomically/' "$1"\n`);
165
173
  Bun.spawnSync(["chmod", "+x", script]);
166
174
  process.env.CUELOOP_EDITOR = script;
@@ -184,6 +192,7 @@ describe("submit", () => {
184
192
  test("⏎ opens the rail confirm card; verdict + summary resolve the session", async () => {
185
193
  // Arrange
186
194
  const setup = await renderApp();
195
+
187
196
  await press(setup, "j");
188
197
  await press(setup, "j");
189
198
  await press(setup, "c");
@@ -204,6 +213,7 @@ describe("submit", () => {
204
213
  // Assert
205
214
  await waitForText(setup, "feedback sent");
206
215
  const stored = server.core.sessionGet(session.id);
216
+
207
217
  expect(stored.status).toBe("resolved");
208
218
  expect(stored.verdict!.kind).toBe("request_changes");
209
219
  expect(stored.verdict!.feedback).toContain("Needs a phase list.");
@@ -234,10 +244,12 @@ describe("inbox", () => {
234
244
  test("inbox mode wears the review chrome (header + menu bar) and opens a session", async () => {
235
245
  // Arrange
236
246
  const setup = await testRender(<App home={home} />, { width: 120, height: 32 });
247
+
237
248
  await waitForText(setup, "cueloop");
238
249
 
239
250
  // Assert - the same header/menu chrome as review, plus the session row
240
251
  const frame = setup.captureCharFrame();
252
+
241
253
  expect(frame).toContain("cueloop · resume");
242
254
  expect(frame).toContain("Migration Plan");
243
255
  expect(frame).toContain("menu"); // the shared MenuBar sits at the bottom
@@ -253,6 +265,7 @@ describe("inbox", () => {
253
265
  test("the menu opens from the inbox and escape is not a trap", async () => {
254
266
  // Arrange
255
267
  const setup = await testRender(<App home={home} />, { width: 120, height: 32 });
268
+
256
269
  await waitForText(setup, "cueloop · resume");
257
270
  const lines = setup.captureCharFrame().split("\n");
258
271
  const menuRow = lines.findIndex((line) => line.includes("menu"));
@@ -11,6 +11,7 @@ import type { ReviewSession } from "@cueloop/schema";
11
11
 
12
12
  // the controller reaches the gateway through "./share"; stub it so `S` yields a toast
13
13
  const publishShare = mock(async () => ({ line: "ssh p_share01@cueloop.dev", copied: true }));
14
+
14
15
  mock.module("./share", () => ({
15
16
  publishShare,
16
17
  pullShare: mock(async () => null),
@@ -56,6 +57,7 @@ describe("share toast", () => {
56
57
  width: 120,
57
58
  height: 32,
58
59
  });
60
+
59
61
  await waitForText(setup, "cueloop");
60
62
 
61
63
  // Act: share raises the toast, then open a composer under it
package/src/App.tsx CHANGED
@@ -142,8 +142,10 @@ export function App({
142
142
  // eslint-disable-next-line react-hooks/exhaustive-deps
143
143
  [home, sessionId],
144
144
  );
145
+
145
146
  useEffect(() => {
146
147
  controller.connect();
148
+
147
149
  return () => controller.close();
148
150
  }, [controller]);
149
151
  // stable across renders so the memoized PrototypeSheet is not re-rendered by
@@ -155,10 +157,12 @@ export function App({
155
157
  );
156
158
  const { session, inbox, status, toast, error, completion, editOrphanCount, walk } =
157
159
  useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
160
+
158
161
  // A shared plan you own polls for collaborator notes while it is open; the
159
162
  // merge refreshes through the normal event path. Stops on leave.
160
163
  useEffect(() => {
161
164
  if (!isOwner || !session?.shareId) return;
165
+
162
166
  return controller.startSharePoll();
163
167
  }, [isOwner, session?.id, session?.shareId, controller]);
164
168
  const renderer = useRenderer();
@@ -200,8 +204,10 @@ export function App({
200
204
  const [themeOverrides, setThemeOverrides] = useState<Partial<Theme>>({});
201
205
  const [authorNames, setAuthorNames] = useState<Record<string, string>>({});
202
206
  const [quickActions, setQuickActions] = useState<QuickAction[]>(DEFAULT_QUICK_ACTIONS);
207
+
203
208
  useEffect(() => {
204
209
  const config = loadConfig({ repoRoot: session?.workspace.repoRoot });
210
+
205
211
  keysRef.current = config.keys;
206
212
  keyBindings.setKeys(config.keys);
207
213
  setTheme(composeTheme(config.ui.theme, config.themeOverrides, appearance));
@@ -225,16 +231,19 @@ export function App({
225
231
  useEffect(() => {
226
232
  if (!toast) return;
227
233
  const timer = setTimeout(() => controller.dismissToast(), TOAST_DISMISS_MS);
234
+
228
235
  return () => clearTimeout(timer);
229
236
  }, [toast, controller]);
230
237
 
231
238
  // First open of a share: ask the collaborator for a display name once, unless
232
239
  // a past visit already recorded one. esc skips and their notes read anonymous.
233
240
  const promptedSelfRef = useRef(false);
241
+
234
242
  useEffect(() => {
235
243
  if (promptedSelfRef.current || role !== "collaborator" || !selfAuthor || !session) return;
236
244
  promptedSelfRef.current = true;
237
245
  const known = session.participants?.find((participant) => participant.id === selfAuthor)?.name;
246
+
238
247
  if (!known) setMode({ type: "nameSelf", text: "" });
239
248
  }, [role, selfAuthor, session]);
240
249
 
@@ -278,9 +287,11 @@ export function App({
278
287
  /** Annotation ids whose anchor resolved against the working copy. */
279
288
  const resolvedIds = useMemo(() => {
280
289
  const ids = new Set<string>();
290
+
281
291
  for (const blockMarks of marks.values()) {
282
292
  for (const mark of blockMarks) if (mark.annotationId) ids.add(mark.annotationId);
283
293
  }
294
+
284
295
  return ids;
285
296
  }, [marks]);
286
297
  const { isDiff, isPrototype, resolved } = deriveReviewFlags(session);
@@ -290,10 +301,12 @@ export function App({
290
301
  // annotation resolves to the display index it marked
291
302
  const annotationPositions = useMemo(() => {
292
303
  const positions = new Map<string, number>();
304
+
293
305
  if (!session) return positions;
294
306
  if (isDiff) {
295
307
  for (const annotation of session.annotations) {
296
308
  const blockIndex = (annotation.anchor as { blockIndex?: number }).blockIndex;
309
+
297
310
  if (blockIndex !== undefined) positions.set(annotation.id, blockIndex);
298
311
  }
299
312
  } else {
@@ -302,6 +315,7 @@ export function App({
302
315
  if (mark.annotationId) positions.set(mark.annotationId, displayIndex);
303
316
  }
304
317
  }
318
+
305
319
  return positions;
306
320
  }, [session, isDiff, marks]);
307
321
 
@@ -317,6 +331,7 @@ export function App({
317
331
  // one marker at a time: clear any prior selection, then paint the current
318
332
  // span (span and its quick-actions sub-mode both keep it painted)
319
333
  const span = activeSpanState(mode);
334
+
320
335
  planSheetRef.current?.clearSelection();
321
336
  if (span) planSheetRef.current?.driveSpanSelection(span);
322
337
  }, [mode]);
@@ -334,6 +349,7 @@ export function App({
334
349
  if (!blockMarks.some((mark) => mark.annotationId === annotationId)) continue;
335
350
  setCursor(displayIndex);
336
351
  planSheetRef.current?.revealBlock(displayIndex);
352
+
337
353
  return;
338
354
  }
339
355
  };
@@ -355,6 +371,7 @@ export function App({
355
371
  const selectCurationFromRail = (curationId: string): void => {
356
372
  setSelectedCurationId(curationId);
357
373
  const item = curationItems.find((candidate) => candidate.id === curationId);
374
+
358
375
  if (!item) return;
359
376
  setCursor(item.revealIndex);
360
377
  if (item.source === "plan") planSheetRef.current?.revealBlock(item.revealIndex);
@@ -371,11 +388,13 @@ export function App({
371
388
  const openCardEdit = (annotationId: string): void => {
372
389
  if (observer) return controller.setStatus("observer - read-only");
373
390
  const annotation = session?.annotations.find((candidate) => candidate.id === annotationId);
391
+
374
392
  if (!annotation) return;
375
393
  // a collaborator's note is theirs to word: activating it (click or e) renames
376
394
  // the author rather than editing the body the planner does not own
377
395
  if (annotation.author) {
378
396
  setFocusedAnnotationId(annotationId);
397
+
379
398
  return void setMode({
380
399
  type: "rename",
381
400
  authorId: annotation.author,
@@ -447,6 +466,7 @@ export function App({
447
466
  // with ctrl+] as the detach chord back to the review.
448
467
  if (agentTerminal) {
449
468
  if (key.ctrl && key.name === "]") return void agentTerminal.detach();
469
+
450
470
  return void agentTerminal.write(key.sequence);
451
471
  }
452
472
  if (menuDialog === "settings") return void handleSettingsKey(key.name);
@@ -473,8 +493,10 @@ export function App({
473
493
  cursor,
474
494
  display,
475
495
  });
496
+
476
497
  keyBindings.setContext({ overlay: state.overlay, spanMode: state.spanMode });
477
498
  const action = keyBindings.resolveAction({ name: key.name, shift: !!key.shift });
499
+
478
500
  for (const intent of reduceKey(
479
501
  state,
480
502
  { name: key.name, shift: !!key.shift, meta: !!key.meta },
@@ -590,7 +612,9 @@ export function App({
590
612
  selection && block
591
613
  ? spanFromRange(selection.displayIndex, displayText(block), selection.start, selection.end)
592
614
  : null;
615
+
593
616
  if (span) setMode({ type: "span", span });
617
+
594
618
  return true;
595
619
  };
596
620
 
@@ -598,6 +622,7 @@ export function App({
598
622
  if (activateSpanFromSelection(displayIndex)) return;
599
623
  setCursor(displayIndex);
600
624
  const annotationId = marks.get(displayIndex)?.[0]?.annotationId;
625
+
601
626
  if (annotationId) selectCardFromDocument(annotationId);
602
627
  };
603
628
 
@@ -634,6 +659,7 @@ export function App({
634
659
  // cycles all three including hidden
635
660
  const onToggleReviewPanel = (): void => {
636
661
  const next = toggleReviewPanelMode(reviewMode);
662
+
637
663
  setReviewMode(next);
638
664
  controller.saveReviewPanel({ mode: next });
639
665
  };
@@ -654,6 +680,7 @@ export function App({
654
680
  onMouseDrag={(event: MouseEvent) => {
655
681
  if (!dividerDragging || reviewMode !== "expanded") return;
656
682
  const next = widthFromMouseColumn(event.x, terminalWidth);
683
+
657
684
  // many raw mouse-moves land in the same column: only re-render when the
658
685
  // width actually changes, so the drag does not reconcile per pixel
659
686
  if (next === reviewWidthRef.current) return;
@@ -69,9 +69,11 @@ async function renderApp() {
69
69
  width: 120,
70
70
  height: 34,
71
71
  });
72
+
72
73
  // Wait for session content, not just the chrome: pressing w before the diff
73
74
  // loads drops the walk intent (no session, no diff view) and hangs the test.
74
75
  await waitForText(setup, "src/a.ts");
76
+
75
77
  return setup;
76
78
  }
77
79
 
@@ -80,15 +82,18 @@ type Setup = Awaited<ReturnType<typeof renderApp>>;
80
82
  /** Foreground colors (hex) of every styled span containing the needle. */
81
83
  function foregroundsOf(setup: Setup, needle: string): string[] {
82
84
  const foregrounds: string[] = [];
85
+
83
86
  for (const line of setup.captureSpans().lines) {
84
87
  for (const span of line.spans) {
85
88
  if (!span.text.includes(needle)) continue;
86
89
  const [red, green, blue] = span.fg.toInts();
90
+
87
91
  foregrounds.push(
88
92
  "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""),
89
93
  );
90
94
  }
91
95
  }
96
+
92
97
  return foregrounds;
93
98
  }
94
99
 
@@ -165,6 +170,7 @@ describe("the guided walk", () => {
165
170
  test("esc keeps progress and w resumes at the first unviewed file", async () => {
166
171
  // Arrange
167
172
  const setup = await renderApp();
173
+
168
174
  await press(setup, "w");
169
175
  await waitForText(setup, "file 1 of 3 · 0 viewed");
170
176
  await press(setup, "]");
@@ -188,6 +194,7 @@ describe("the guided walk", () => {
188
194
  test("a half-walked review resumes across App instances via the daemon", async () => {
189
195
  // Arrange
190
196
  const first = await renderApp();
197
+
191
198
  await press(first, "w");
192
199
  await waitForText(first, "file 1 of 3 · 0 viewed");
193
200
  await press(first, "]");
@@ -202,6 +209,7 @@ describe("the guided walk", () => {
202
209
  // Act
203
210
  // a fresh client reads the viewed set back from the session record
204
211
  const second = await renderApp();
212
+
205
213
  await press(second, "w");
206
214
 
207
215
  // Assert
@@ -249,6 +257,7 @@ describe("the guided walk", () => {
249
257
  // Assert
250
258
  await waitForState(setup, () => server.core.sessionGet(session.id).status === "resolved");
251
259
  const resolved = server.core.sessionGet(session.id);
260
+
252
261
  expect(resolved.verdict!.kind).toBe("approve");
253
262
  expect(resolved.verdict!.feedback).not.toContain("Swaps the stale line");
254
263
  });
@@ -277,6 +286,7 @@ describe("the guided walk", () => {
277
286
  // Arrange
278
287
  server.core.sessionResolve(session.id, "approve", "");
279
288
  const setup = await renderApp();
289
+
280
290
  await waitForText(setup, "resolved");
281
291
 
282
292
  // Act
@@ -39,6 +39,7 @@ export function buildKeyState(params: {
39
39
  cursor,
40
40
  display,
41
41
  } = params;
42
+
42
43
  return {
43
44
  keys,
44
45
  readOnly: observer,
@@ -66,6 +66,7 @@ export function MenuChrome(props: {
66
66
  onCategorySelect,
67
67
  cycleSetting,
68
68
  } = props;
69
+
69
70
  return (
70
71
  <>
71
72
  <MenuBar
@@ -114,6 +115,7 @@ export function InboxScreen(props: {
114
115
  }): React.ReactNode {
115
116
  const { inbox, inboxCursor, mode, theme, controller, setMode, menuChrome } = props;
116
117
  const confirming = mode.type === "confirmDelete" ? mode : null;
118
+
117
119
  return (
118
120
  <ThemeProvider theme={theme}>
119
121
  <box
@@ -176,6 +178,7 @@ export function CompletionScreen(props: {
176
178
  status: string;
177
179
  }): React.ReactNode {
178
180
  const { theme, session, verdict, completion, status } = props;
181
+
179
182
  return (
180
183
  <ThemeProvider theme={theme}>
181
184
  <CompletionOverlay
@@ -218,6 +221,7 @@ export function TrailingOverlays(props: {
218
221
  setMode,
219
222
  dispatch,
220
223
  } = props;
224
+
221
225
  return (
222
226
  <>
223
227
  {walking && walk !== null ? (
@@ -20,6 +20,7 @@ export function computeRoleCapabilities(
20
20
  role: "owner" | "observer" | "collaborator",
21
21
  ): { observer: boolean; isOwner: boolean } {
22
22
  const observer = readOnly || role === "observer";
23
+
23
24
  return { observer, isOwner: !observer && role === "owner" };
24
25
  }
25
26
 
@@ -52,6 +53,7 @@ export function resolveOverlay(
52
53
  if (completionPhase === "prompt") return "completion-prompt";
53
54
  if (completionPhase === "counting") return "completion-counting";
54
55
  if (walking) return "walk";
56
+
55
57
  return "none";
56
58
  }
57
59
 
@@ -71,6 +73,7 @@ export function computeRailFootprint(
71
73
  terminalWidth: number,
72
74
  ): number {
73
75
  if (reviewMode === "hidden") return 0;
76
+
74
77
  return (
75
78
  1 +
76
79
  (reviewMode === "compact"
@@ -86,6 +89,7 @@ export function buildHeaderItems(params: {
86
89
  role: "owner" | "observer" | "collaborator";
87
90
  }): BreadcrumbItem[] {
88
91
  const { session, resolved, observer, role } = params;
92
+
89
93
  return [
90
94
  { label: "cueloop", tone: "accent" },
91
95
  ...(resolved
@@ -126,6 +130,7 @@ export function buildRenderFlags(params: {
126
130
  } {
127
131
  const { session, isOwner, isDiff, isPrototype, resolved, menuOpen, menuDialog, resolvedIds } =
128
132
  params;
133
+
129
134
  return {
130
135
  showOwnerActions: isOwner && !isDiff && !resolved,
131
136
  prototypeCanComment: isOwner && !resolved,
@@ -140,10 +145,12 @@ export function buildActiveSpan(
140
145
  isDiff: boolean,
141
146
  ): { displayIndex: number; start: number; end: number } | null {
142
147
  const markedSpan = activeSpanState(mode);
148
+
143
149
  if (markedSpan)
144
150
  return { displayIndex: markedSpan.displayIndex, start: markedSpan.start, end: markedSpan.end };
145
151
  if (mode.type === "compose" && !isDiff)
146
152
  return { displayIndex: mode.displayIndex, start: mode.start, end: mode.end };
153
+
147
154
  return null;
148
155
  }
149
156
 
@@ -157,7 +164,9 @@ export function buildComposeState(
157
164
  deps: DraftHandlerDeps & { mode: Mode; isDiff: boolean; display: DisplayBlock[] },
158
165
  ): PlanComposeState | null {
159
166
  const { mode, isDiff, display, liveInput, setMode, dispatch } = deps;
167
+
160
168
  if (mode.type !== "compose" || isDiff) return null;
169
+
161
170
  return {
162
171
  kind: mode.kind,
163
172
  displayIndex: mode.displayIndex,
@@ -178,7 +187,9 @@ export function buildDiffComposeState(
178
187
  deps: DraftHandlerDeps & { mode: Mode; isDiff: boolean; rows: DiffRow[] },
179
188
  ): DiffComposeState | null {
180
189
  const { mode, isDiff, rows, liveInput, setMode, dispatch } = deps;
190
+
181
191
  if (mode.type !== "compose" || !isDiff) return null;
192
+
182
193
  return {
183
194
  kind: mode.kind,
184
195
  rowIndex: mode.displayIndex,
@@ -207,9 +218,11 @@ export function buildPopoverState(deps: {
207
218
  }): PlanPopoverState | null {
208
219
  const { mode, isDiff, quickActions, isOwner, observer, resolved, controller, dispatch } = deps;
209
220
  const markedSpan = activeSpanState(mode);
221
+
210
222
  if (!markedSpan || isDiff) return null;
211
223
  const spanMutationBlock = (): string | null =>
212
224
  observer ? "observer - read-only" : resolved ? "review submitted - read-only" : null;
225
+
213
226
  return {
214
227
  displayIndex: markedSpan.displayIndex,
215
228
  view: mode.type === "spanActions" ? ("actions" as const) : ("toolbar" as const),
@@ -218,23 +231,27 @@ export function buildPopoverState(deps: {
218
231
  canCut: isOwner,
219
232
  onComment: () => {
220
233
  const blocked = spanMutationBlock();
234
+
221
235
  if (blocked) return controller.setStatus(blocked);
222
236
  dispatch({ type: "openCompose", kind: "comment", from: "span" });
223
237
  },
224
238
  onCut: () => {
225
239
  const blocked = spanMutationBlock();
240
+
226
241
  if (blocked) return controller.setStatus(blocked);
227
242
  if (!isOwner) return;
228
243
  dispatch({ type: "spanCut" });
229
244
  },
230
245
  onOpenActions: () => {
231
246
  const blocked = spanMutationBlock();
247
+
232
248
  if (blocked) return controller.setStatus(blocked);
233
249
  dispatch({ type: "openSpanActions" });
234
250
  },
235
251
  onClose: () => dispatch({ type: "closeOverlay" }),
236
252
  onPickAction: (index: number) => {
237
253
  const blocked = spanMutationBlock();
254
+
238
255
  if (blocked) return controller.setStatus(blocked);
239
256
  dispatch({ type: "pickSpanAction", index });
240
257
  },
@@ -252,7 +269,9 @@ export function buildSubmitConfirmState(
252
269
  },
253
270
  ): Omit<ConfirmCardProps, "theme"> | null {
254
271
  const { mode, isDiff, session, walkFileList, viewedPaths, liveInput, setMode, dispatch } = deps;
272
+
255
273
  if (mode.type !== "submit") return null;
274
+
256
275
  return {
257
276
  verdict: mode.verdict,
258
277
  summary: mode.summary,
@@ -272,7 +291,9 @@ export function buildSubmitConfirmState(
272
291
 
273
292
  export function buildCardEditState(deps: DraftHandlerDeps & { mode: Mode }): RailCardEdit | null {
274
293
  const { mode, liveInput, setMode, dispatch } = deps;
294
+
275
295
  if (mode.type !== "railEdit") return null;
296
+
276
297
  return {
277
298
  id: mode.id,
278
299
  text: mode.text,
@@ -16,6 +16,7 @@ export function shortHandle(authorId: string): string {
16
16
  const base = authorId.startsWith(FINGERPRINT_PREFIX)
17
17
  ? authorId.slice(FINGERPRINT_PREFIX.length)
18
18
  : authorId;
19
+
19
20
  return base.slice(0, 8) || authorId;
20
21
  }
21
22
 
@@ -30,9 +31,12 @@ export function resolveDisplayName(
30
31
  overrides: Record<string, string>,
31
32
  ): string {
32
33
  const override = overrides[authorId];
34
+
33
35
  if (override) return override;
34
36
  const identity = participants?.find((participant) => participant.id === authorId);
37
+
35
38
  if (identity?.name) return identity.name;
36
39
  if (identity) return ANONYMOUS_LABEL;
40
+
37
41
  return shortHandle(authorId);
38
42
  }
package/src/clipboard.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  export function clipboardCommands(platform: NodeJS.Platform = process.platform): string[][] {
10
10
  if (platform === "darwin") return [["pbcopy"]];
11
11
  if (platform === "win32") return [["clip.exe"]];
12
+
12
13
  return [["wl-copy"], ["xclip", "-selection", "clipboard"], ["xsel", "--clipboard", "--input"]];
13
14
  }
14
15
 
@@ -21,10 +22,12 @@ export async function copyToClipboard(text: string): Promise<boolean> {
21
22
  stdout: "ignore",
22
23
  stderr: "ignore",
23
24
  });
25
+
24
26
  if ((await proc.exited) === 0) return true;
25
27
  } catch {
26
28
  // tool not installed - try the next one
27
29
  }
28
30
  }
31
+
29
32
  return false;
30
33
  }