@cueloop/client 0.1.0-alpha.32 → 0.1.0-alpha.34

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 (101) hide show
  1. package/package.json +27 -26
  2. package/src/App.autoclose.test.tsx +7 -4
  3. package/src/App.diff.test.tsx +131 -5
  4. package/src/App.export.test.tsx +23 -5
  5. package/src/App.plan-review.test.tsx +133 -32
  6. package/src/App.readonly.test.tsx +35 -13
  7. package/src/App.rendering.test.tsx +14 -4
  8. package/src/App.review-panel.test.tsx +27 -15
  9. package/src/App.submit-confirm.test.tsx +14 -13
  10. package/src/App.test.tsx +50 -13
  11. package/src/App.toast.test.tsx +75 -0
  12. package/src/App.tsx +505 -101
  13. package/src/App.walk.test.tsx +31 -11
  14. package/src/attribution.ts +8 -2
  15. package/src/clipboard.ts +5 -1
  16. package/src/components/AnnotationCard.stories.tsx +29 -10
  17. package/src/components/AnnotationCard.tsx +57 -37
  18. package/src/components/Breadcrumb.tsx +7 -1
  19. package/src/components/CodeBlock.stories.tsx +19 -2
  20. package/src/components/CodeBlock.tsx +24 -9
  21. package/src/components/CompletionOverlay.tsx +13 -3
  22. package/src/components/ConfirmCard.stories.tsx +22 -3
  23. package/src/components/ConfirmCard.tsx +23 -5
  24. package/src/components/ConfirmDialog.tsx +10 -2
  25. package/src/components/DiffSheet.scroll.test.tsx +123 -0
  26. package/src/components/DiffSheet.stories.tsx +35 -1
  27. package/src/components/DiffSheet.syntax.test.tsx +62 -0
  28. package/src/components/DiffSheet.tsx +213 -101
  29. package/src/components/InboxList.stories.tsx +16 -3
  30. package/src/components/InboxList.tsx +17 -10
  31. package/src/components/KeybindsDialog.stories.tsx +31 -0
  32. package/src/components/KeybindsDialog.tsx +58 -0
  33. package/src/components/MarkerPopover.stories.tsx +58 -0
  34. package/src/components/MarkerPopover.tsx +102 -0
  35. package/src/components/MenuBar.stories.tsx +37 -0
  36. package/src/components/MenuBar.tsx +74 -0
  37. package/src/components/PlanSheet.stories.tsx +35 -3
  38. package/src/components/PlanSheet.tsx +160 -100
  39. package/src/components/PromptDialog.tsx +19 -3
  40. package/src/components/ReviewPanel.stories.tsx +17 -19
  41. package/src/components/ReviewPanel.tsx +17 -32
  42. package/src/components/ReviewRail.stories.tsx +159 -47
  43. package/src/components/ReviewRail.tsx +206 -62
  44. package/src/components/SettingsDialog.stories.tsx +51 -3
  45. package/src/components/SettingsDialog.tsx +11 -5
  46. package/src/components/SettingsRows.stories.tsx +1 -1
  47. package/src/components/SettingsRows.tsx +19 -3
  48. package/src/components/Toast.stories.tsx +12 -0
  49. package/src/components/Toast.tsx +44 -0
  50. package/src/components/WalkWizard.stories.tsx +13 -2
  51. package/src/components/WalkWizard.tsx +9 -3
  52. package/src/components/__snapshots__/stories.test.tsx.snap +853 -623
  53. package/src/components/diff-sheet-layout.test.ts +127 -0
  54. package/src/components/diff-sheet-layout.ts +167 -0
  55. package/src/components/primitives/Button.stories.tsx +2 -2
  56. package/src/components/primitives/Button.tsx +5 -1
  57. package/src/components/primitives/Dialog.tsx +12 -1
  58. package/src/components/primitives/Tabs.tsx +40 -39
  59. package/src/components/stories-app.tsx +34 -6
  60. package/src/components/story-fixtures.ts +11 -3
  61. package/src/components/story.ts +3 -1
  62. package/src/components/syntax-highlight.ts +47 -12
  63. package/src/components/theme-context.stories.tsx +6 -1
  64. package/src/components/theme-context.tsx +7 -1
  65. package/src/config.test.ts +178 -2
  66. package/src/config.ts +121 -23
  67. package/src/diff-hunk-curate.test.ts +315 -0
  68. package/src/diff-hunk-curate.ts +233 -0
  69. package/src/diff-intraline.test.ts +164 -0
  70. package/src/diff-intraline.ts +132 -0
  71. package/src/diff-syntax.test.ts +96 -0
  72. package/src/diff-syntax.ts +154 -0
  73. package/src/editor.test.ts +13 -4
  74. package/src/editor.ts +31 -5
  75. package/src/herdr.ts +8 -2
  76. package/src/index.ts +9 -1
  77. package/src/integrations.ts +3 -1
  78. package/src/intent-dispatch.test.ts +193 -9
  79. package/src/intent-dispatch.ts +151 -18
  80. package/src/key-bindings.test.ts +19 -9
  81. package/src/key-bindings.ts +77 -16
  82. package/src/keymap.test.ts +171 -26
  83. package/src/keymap.ts +83 -16
  84. package/src/review-panel.test.ts +3 -1
  85. package/src/review-panel.ts +9 -2
  86. package/src/serve.test.ts +13 -5
  87. package/src/session-controller.curate.test.ts +253 -0
  88. package/src/session-controller.share.test.ts +77 -16
  89. package/src/session-controller.ts +319 -26
  90. package/src/share.ts +41 -8
  91. package/src/test-support.ts +97 -23
  92. package/src/theme-presets.ts +168 -0
  93. package/src/theme.ts +1 -3
  94. package/src/version.ts +5 -0
  95. package/src/view-diff.ts +15 -4
  96. package/src/view-plan.test.ts +40 -6
  97. package/src/view-plan.ts +71 -28
  98. package/src/walk.test.ts +7 -2
  99. package/src/walk.ts +7 -2
  100. package/src/components/ComposeBar.stories.tsx +0 -18
  101. package/src/components/ComposeBar.tsx +0 -54
@@ -8,16 +8,15 @@
8
8
  * which keeps quote anchors char-precise.
9
9
  */
10
10
 
11
- import React, { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
11
+ import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
12
12
  import { useRenderer } from "@opentui/react";
13
- import type { ScrollBoxRenderable, TextRenderable } from "@opentui/core";
13
+ import { createTextAttributes, type ScrollBoxRenderable, type TextRenderable } from "@opentui/core";
14
14
  import type { ReviewSession } from "@cueloop/schema";
15
15
  import {
16
16
  blockRuns,
17
17
  displayText,
18
18
  overlayMarks,
19
19
  renderedOffsetFor,
20
- revisionDelta,
21
20
  workRangeForRendered,
22
21
  type DisplayBlock,
23
22
  type Mark,
@@ -28,8 +27,11 @@ import type { Theme } from "../theme";
28
27
  import { useComponentTheme } from "./theme-context";
29
28
  import { CodeBlock } from "./CodeBlock";
30
29
  import { AnnotationCard, type AnnotationDraft } from "./AnnotationCard";
31
- import { Button } from "./primitives/Button";
32
- import { Toolbar } from "./primitives/Toolbar";
30
+ import { MarkerPopover, type MarkerPopoverProps } from "./MarkerPopover";
31
+ import { FRAME_BORDER_STYLE } from "./primitives/frame";
32
+
33
+ /** A cut block reads as removed: struck through and grayed, never red. */
34
+ const CUT_ATTRIBUTES = createTextAttributes({ strikethrough: true, dim: true });
33
35
 
34
36
  export interface PlanSelection {
35
37
  displayIndex: number;
@@ -39,7 +41,8 @@ export interface PlanSelection {
39
41
 
40
42
  export interface PlanSheetHandle {
41
43
  /** Work-text range of the current native (mouse) selection, if any. */
42
- readSelection(): PlanSelection | null;
44
+ /** The current native selection; `preferredIndex` (the mouse-up block) re-anchors a fresh drag. */
45
+ readSelection(preferredIndex?: number): PlanSelection | null;
43
46
  /** Anchor/extend the renderer's native selection from keyboard span offsets. */
44
47
  driveSpanSelection(span: SpanState): void;
45
48
  clearSelection(): void;
@@ -47,12 +50,17 @@ export interface PlanSheetHandle {
47
50
  }
48
51
 
49
52
  export interface PlanComposeState {
50
- kind: "comment" | "suggestion";
53
+ kind: "comment";
51
54
  displayIndex: number;
52
55
  quote: string;
53
56
  draft: AnnotationDraft;
54
57
  }
55
58
 
59
+ /** The marker-actions popover, rendered inline at its block while span mode is live. */
60
+ export interface PlanPopoverState extends MarkerPopoverProps {
61
+ displayIndex: number;
62
+ }
63
+
56
64
  export interface PlanSheetProps {
57
65
  session: ReviewSession;
58
66
  display: DisplayBlock[];
@@ -61,12 +69,10 @@ export interface PlanSheetProps {
61
69
  /** Extra selection-style paint on one block (keyboard span or compose anchor). */
62
70
  activeSpan: { displayIndex: number; start: number; end: number } | null;
63
71
  compose: PlanComposeState | null;
72
+ /** The marker-actions popover for the span's block; null when not in span mode. */
73
+ popover: PlanPopoverState | null;
64
74
  editOrphanCount: number;
65
75
  onLineActivate: (displayIndex: number) => void;
66
- onEditRequest: () => void;
67
- onShareRequest: () => void;
68
- /** Owner-only: show the Edit and Share affordances. A share viewer never sees either. */
69
- canEdit: boolean;
70
76
  theme?: Theme;
71
77
  }
72
78
 
@@ -76,7 +82,10 @@ interface BlockRef {
76
82
  }
77
83
 
78
84
  /** Screen position of a rendered-text offset inside a wrapped text. */
79
- function positionOfRenderedOffset(renderable: TextRenderable, renderedOffset: number): { x: number; y: number } {
85
+ function positionOfRenderedOffset(
86
+ renderable: TextRenderable,
87
+ renderedOffset: number,
88
+ ): { x: number; y: number } {
80
89
  const info = renderable.lineInfo;
81
90
  let lineIndex = 0;
82
91
  for (let index = 0; index < info.lineStartCols.length; index++) {
@@ -91,17 +100,14 @@ function positionOfRenderedOffset(renderable: TextRenderable, renderedOffset: nu
91
100
 
92
101
  export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function PlanSheet(
93
102
  {
94
- session,
95
103
  display,
96
104
  marks,
97
105
  cursor,
98
106
  activeSpan,
99
107
  compose,
108
+ popover,
100
109
  editOrphanCount,
101
110
  onLineActivate,
102
- onEditRequest,
103
- onShareRequest,
104
- canEdit,
105
111
  theme,
106
112
  }: PlanSheetProps,
107
113
  handleRef,
@@ -112,14 +118,27 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
112
118
  const scrollRef = useRef<ScrollBoxRenderable | null>(null);
113
119
 
114
120
  useImperativeHandle(handleRef, () => ({
115
- readSelection: (): PlanSelection | null => {
121
+ readSelection: (preferredIndex?: number): PlanSelection | null => {
116
122
  if (!renderer?.hasSelection) return null;
117
- const ordered = [...blockRefs.current.entries()].sort(([a], [b]) => a - b);
118
- for (const [displayIndex, blockRef] of ordered) {
123
+ const readBlock = (displayIndex: number): PlanSelection | null => {
124
+ const blockRef = blockRefs.current.get(displayIndex);
125
+ if (!blockRef) return null;
119
126
  const selection = blockRef.renderable.getSelection();
120
- if (!selection || selection.end <= selection.start) continue;
127
+ if (!selection || selection.end <= selection.start) return null;
121
128
  const range = workRangeForRendered(blockRef.runs, selection.start, selection.end);
122
- if (range) return { displayIndex, ...range };
129
+ return range ? { displayIndex, ...range } : null;
130
+ };
131
+ // the block the mouse released on wins, so a fresh drag re-anchors the span
132
+ // (one marker at a time) instead of sticking to the topmost prior selection
133
+ if (preferredIndex !== undefined) {
134
+ const preferred = readBlock(preferredIndex);
135
+ if (preferred) return preferred;
136
+ }
137
+ for (const displayIndex of [...blockRefs.current.keys()].sort(
138
+ (left, right) => left - right,
139
+ )) {
140
+ const found = readBlock(displayIndex);
141
+ if (found) return found;
123
142
  }
124
143
  return null;
125
144
  },
@@ -147,7 +166,30 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
147
166
  },
148
167
  }));
149
168
 
150
- const registerBlock = (displayIndex: number, renderable: TextRenderable | null, runs: StyleRun[]): void => {
169
+ // the popover floats above its block, anchored to the selection start; flip
170
+ // below when the block sits too near the viewport top to fit above it
171
+ const [popoverFlipBelow, setPopoverFlipBelow] = useState(false);
172
+ const [popoverLeft, setPopoverLeft] = useState(2);
173
+ useEffect(() => {
174
+ if (!popover) return;
175
+ const blockRef = blockRefs.current.get(popover.displayIndex);
176
+ const scrollbox = scrollRef.current;
177
+ if (!blockRef || !scrollbox) return;
178
+ // only the toolbar (3 rows) + gap must fit above; the dropdown flows down
179
+ setPopoverFlipBelow(blockRef.renderable.y - scrollbox.y < 4);
180
+ // left-anchor to the start of the selection: gutter width + its column
181
+ const startColumn =
182
+ activeSpan && activeSpan.displayIndex === popover.displayIndex
183
+ ? (renderedOffsetFor(blockRef.runs, activeSpan.start) ?? 0)
184
+ : 0;
185
+ setPopoverLeft(2 + startColumn);
186
+ }, [popover, activeSpan, display, cursor]);
187
+
188
+ const registerBlock = (
189
+ displayIndex: number,
190
+ renderable: TextRenderable | null,
191
+ runs: StyleRun[],
192
+ ): void => {
151
193
  if (renderable) blockRefs.current.set(displayIndex, { renderable, runs });
152
194
  else blockRefs.current.delete(displayIndex);
153
195
  };
@@ -173,7 +215,8 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
173
215
  isCursor={isCursor}
174
216
  marginTop={gap}
175
217
  isAnnotated={(marks.get(displayIndex) ?? []).length > 0}
176
- changeTag={block.type !== "same" ? tagLabel(block) : undefined}
218
+ changeTag={showsChangeTag(block) ? tagLabel(block) : undefined}
219
+ cut={block.type === "del"}
177
220
  theme={theme}
178
221
  />,
179
222
  );
@@ -183,11 +226,23 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
183
226
  blockMarks.push({ start: activeSpan.start, end: activeSpan.end, role: "kspan" });
184
227
  }
185
228
  const runs = overlayMarks(blockRuns(block, true), blockMarks);
186
- // the change tag participates in the rendered text but carries no offsets
187
- const mappedRuns: StyleRun[] =
188
- block.type !== "same" ? [...runs, { text: ` [${tagLabel(block)}]`, role: "plain", start: null }] : runs;
229
+ // the change tag participates in the rendered text but carries no offsets;
230
+ // a cut block shows no tag (its strikethrough already reads as removed)
231
+ const mappedRuns: StyleRun[] = showsChangeTag(block)
232
+ ? [...runs, { text: ` [${tagLabel(block)}]`, role: "plain", start: null }]
233
+ : runs;
189
234
  children.push(
190
- <box key={displayIndex} id={`plan-block-${displayIndex}`} style={{ flexDirection: "row", marginTop: gap }}>
235
+ <box
236
+ key={displayIndex}
237
+ id={`plan-block-${displayIndex}`}
238
+ // raise the block holding the popover so its overlay paints over the
239
+ // later blocks it floats across (siblings paint in z-index order)
240
+ style={{
241
+ flexDirection: "row",
242
+ marginTop: gap,
243
+ zIndex: popover?.displayIndex === displayIndex ? 10 : undefined,
244
+ }}
245
+ >
191
246
  <text selectable={false}>
192
247
  <span fg={isCursor ? tokens.accent : tokens.textDim}>{isCursor ? "▎ " : " "}</span>
193
248
  <span fg={tokens.textDim}>{marker(block)}</span>
@@ -198,7 +253,9 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
198
253
  selectionBg={tokens.accent}
199
254
  selectionFg={tokens.accentInk}
200
255
  style={{ wrapMode: "word", flexGrow: 1, flexShrink: 1 }}
201
- ref={(renderable: TextRenderable | null) => registerBlock(displayIndex, renderable, mappedRuns)}
256
+ ref={(renderable: TextRenderable | null) =>
257
+ registerBlock(displayIndex, renderable, mappedRuns)
258
+ }
202
259
  onMouseUp={() => onLineActivate(displayIndex)}
203
260
  >
204
261
  {runs.map((run, runIndex) => (
@@ -206,89 +263,79 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
206
263
  {run.text}
207
264
  </span>
208
265
  ))}
209
- {block.type !== "same" ? <span fg={tagColor(block, tokens)}> [{tagLabel(block)}]</span> : null}
266
+ {showsChangeTag(block) ? (
267
+ <span fg={tagColor(block, tokens)}> [{tagLabel(block)}]</span>
268
+ ) : null}
210
269
  </text>
270
+ {popover && popover.displayIndex === displayIndex ? (
271
+ // float the card over the block, centered, with a one-row gap; flip
272
+ // below only when there is no room above (near the viewport top)
273
+ <box
274
+ style={{
275
+ position: "absolute",
276
+ left: popoverLeft,
277
+ // fixed offset: the toolbar sits one row above the selection and
278
+ // stays put; the dropdown flows down from it, over the selection
279
+ top: popoverFlipBelow ? 1 : -4,
280
+ flexDirection: "column",
281
+ }}
282
+ >
283
+ <MarkerPopover {...popover} theme={theme} />
284
+ </box>
285
+ ) : null}
211
286
  </box>,
212
287
  );
213
288
  }
214
289
  if (compose && compose.displayIndex === displayIndex) {
215
290
  children.push(
216
- <AnnotationCard key={`compose-${displayIndex}`} kind={compose.kind} quote={compose.quote} draft={compose.draft} theme={theme} />,
291
+ <AnnotationCard
292
+ key={`compose-${displayIndex}`}
293
+ kind={compose.kind}
294
+ quote={compose.quote}
295
+ draft={compose.draft}
296
+ theme={theme}
297
+ />,
217
298
  );
218
299
  }
219
300
  }
220
301
 
221
302
  return (
222
303
  <box style={{ flexGrow: 1, flexDirection: "column" }}>
223
- <SheetHeader
224
- session={session}
225
- onEditRequest={onEditRequest}
226
- onShareRequest={onShareRequest}
227
- canEdit={canEdit}
228
- theme={theme}
229
- />
230
- {editOrphanCount > 0 ? (
231
- <box style={{ height: 1, backgroundColor: tokens.markCommentBackground, paddingLeft: 2 }}>
232
- <text fg={tokens.red}>
233
- {editOrphanCount} annotation{editOrphanCount === 1 ? "" : "s"} no longer match - the passage was removed.
234
- </text>
235
- </box>
236
- ) : null}
237
- <scrollbox ref={scrollRef} style={{ flexGrow: 1, paddingLeft: 2, paddingTop: 1 }} focused={false}>
238
- {children}
239
- </scrollbox>
240
- </box>
241
- );
242
- });
243
-
244
- /** Sheet chrome: submitted-by + revision delta left, the Edit and Share word-buttons right. */
245
- function SheetHeader({
246
- session,
247
- onEditRequest,
248
- onShareRequest,
249
- canEdit,
250
- theme,
251
- }: {
252
- session: ReviewSession;
253
- onEditRequest: () => void;
254
- onShareRequest: () => void;
255
- canEdit: boolean;
256
- theme?: Theme;
257
- }): React.ReactNode {
258
- const tokens = useComponentTheme(theme);
259
- const revisionCount = session.revisions.length;
260
- const previous = revisionCount > 1 ? session.revisions[revisionCount - 2] : undefined;
261
- const delta = previous ? revisionDelta(previous.content, session.artifact.content) : null;
262
- return (
263
- <box style={{ height: 1, flexDirection: "row", paddingLeft: 2, paddingRight: 1 }}>
264
- <text fg={tokens.textDim}>
265
- submitted by <span fg={tokens.textMuted}>{session.artifact.meta.agent ?? "unknown"}</span> · revision {revisionCount}
266
- {delta ? (
267
- <span fg={tokens.green}>
268
- {" "}· v{revisionCount - 1}→v{revisionCount} +{delta.added} -{delta.removed}
269
- </span>
304
+ <box
305
+ style={{
306
+ flexGrow: 1,
307
+ flexDirection: "column",
308
+ border: true,
309
+ borderStyle: FRAME_BORDER_STYLE,
310
+ borderColor: tokens.text,
311
+ }}
312
+ >
313
+ {editOrphanCount > 0 ? (
314
+ <box style={{ height: 1, backgroundColor: tokens.markCommentBackground, paddingLeft: 1 }}>
315
+ <text fg={tokens.red}>
316
+ {editOrphanCount} annotation{editOrphanCount === 1 ? "" : "s"} no longer match - the
317
+ passage was removed.
318
+ </text>
319
+ </box>
270
320
  ) : null}
271
- </text>
272
- <box style={{ flexGrow: 1 }} />
273
- {canEdit && session.status !== "resolved" ? (
274
- <Toolbar>
275
- <Button onPress={onEditRequest} theme={theme}>
276
- {" Edit "}
277
- </Button>
278
- <Button onPress={onShareRequest} theme={theme}>
279
- {" Share "}
280
- </Button>
281
- </Toolbar>
282
- ) : null}
321
+ <scrollbox
322
+ ref={scrollRef}
323
+ style={{ flexGrow: 1, paddingLeft: 1, paddingTop: 0 }}
324
+ focused={false}
325
+ >
326
+ {children}
327
+ </scrollbox>
328
+ </box>
283
329
  </box>
284
330
  );
285
- }
331
+ });
286
332
 
287
333
  /** Vertical rhythm: gaps live ABOVE blocks so boundaries never collapse. */
288
334
  function topGap(previous: DisplayBlock | undefined, current: DisplayBlock): number {
289
335
  if (!previous) return 0;
290
336
  const tightPair =
291
- (current.kind === "li" && previous.kind === "li") || (current.kind === "oli" && previous.kind === "oli");
337
+ (current.kind === "li" && previous.kind === "li") ||
338
+ (current.kind === "oli" && previous.kind === "oli");
292
339
  return tightPair ? 0 : 1;
293
340
  }
294
341
 
@@ -299,17 +346,32 @@ function marker(block: DisplayBlock): string {
299
346
  return "";
300
347
  }
301
348
 
302
- function tagLabel(block: DisplayBlock): "cut" | "new" | "edited" {
303
- return block.type === "del" ? "cut" : block.type === "add" ? "new" : "edited";
349
+ /** An added or edited block earns a tag; a cut block relies on its strikethrough. */
350
+ function showsChangeTag(block: DisplayBlock): boolean {
351
+ return block.type === "add" || block.type === "mod";
352
+ }
353
+
354
+ function tagLabel(block: DisplayBlock): "new" | "edited" {
355
+ return block.type === "add" ? "new" : "edited";
304
356
  }
305
357
 
306
358
  function tagColor(block: DisplayBlock, tokens: Theme): string {
307
- return block.type === "del" ? tokens.red : block.type === "add" ? tokens.green : tokens.accent;
359
+ return block.type === "add" ? tokens.green : tokens.accent;
308
360
  }
309
361
 
310
- function runStyle(run: StyleRun, block: DisplayBlock, tokens: Theme): { fg?: string; bg?: string } {
311
- const headingFg = block.kind === "h1" ? tokens.text : block.kind === "h2" || block.kind === "h3" ? tokens.accent : undefined;
312
- const struck = block.type === "del";
362
+ function runStyle(
363
+ run: StyleRun,
364
+ block: DisplayBlock,
365
+ tokens: Theme,
366
+ ): { fg?: string; bg?: string; attributes?: number } {
367
+ // a cut block reads as removed: every run struck through and grayed, never red
368
+ if (block.type === "del") return { fg: tokens.textDim, attributes: CUT_ATTRIBUTES };
369
+ const headingFg =
370
+ block.kind === "h1"
371
+ ? tokens.text
372
+ : block.kind === "h2" || block.kind === "h3"
373
+ ? tokens.accent
374
+ : undefined;
313
375
  switch (run.role) {
314
376
  case "ins":
315
377
  return { fg: tokens.insertedForeground };
@@ -317,13 +379,11 @@ function runStyle(run: StyleRun, block: DisplayBlock, tokens: Theme): { fg?: str
317
379
  return { fg: tokens.deletedForeground };
318
380
  case "mark-comment":
319
381
  return { fg: tokens.text, bg: tokens.markCommentBackground };
320
- case "mark-suggestion":
321
- return { fg: tokens.text, bg: tokens.markSuggestionBackground };
322
382
  case "mark-focus":
323
383
  return { fg: tokens.accentInk, bg: tokens.accent };
324
384
  case "kspan":
325
385
  return { fg: tokens.accentInk, bg: tokens.accent };
326
386
  default:
327
- return { fg: struck ? tokens.red : (headingFg ?? tokens.text) };
387
+ return { fg: headingFg ?? tokens.text };
328
388
  }
329
389
  }
@@ -25,7 +25,15 @@ export interface PromptDialogProps {
25
25
  theme?: Theme;
26
26
  }
27
27
 
28
- export function PromptDialog({ isOpen, title, label, value, placeholder, onInput, theme }: PromptDialogProps): React.ReactNode {
28
+ export function PromptDialog({
29
+ isOpen,
30
+ title,
31
+ label,
32
+ value,
33
+ placeholder,
34
+ onInput,
35
+ theme,
36
+ }: PromptDialogProps): React.ReactNode {
29
37
  const tokens = useComponentTheme(theme);
30
38
  const { width: terminalWidth } = useTerminalDimensions();
31
39
  const inputRef = useRef<TextareaRenderable | null>(null);
@@ -37,7 +45,15 @@ export function PromptDialog({ isOpen, title, label, value, placeholder, onInput
37
45
  if (!isOpen) return null;
38
46
  return (
39
47
  <Dialog isOpen title={title} width={Math.min(54, terminalWidth - 6)} height={7} theme={theme}>
40
- <box style={{ flexDirection: "column", flexGrow: 1, paddingLeft: 1, paddingRight: 1, paddingTop: 1 }}>
48
+ <box
49
+ style={{
50
+ flexDirection: "column",
51
+ flexGrow: 1,
52
+ paddingLeft: 1,
53
+ paddingRight: 1,
54
+ paddingTop: 1,
55
+ }}
56
+ >
41
57
  <text fg={tokens.textDim}>{label}</text>
42
58
  <textarea
43
59
  ref={inputRef}
@@ -55,7 +71,7 @@ export function PromptDialog({ isOpen, title, label, value, placeholder, onInput
55
71
  }}
56
72
  />
57
73
  <box style={{ flexGrow: 1 }} />
58
- <text fg={tokens.textDim}>⏎ save · esc cancel</text>
74
+ <text fg={tokens.textDim}>enter save · esc cancel</text>
59
75
  </box>
60
76
  </Dialog>
61
77
  );
@@ -12,6 +12,7 @@ const rail = {
12
12
  authorNames: {},
13
13
  selectedId: FIXTURE_ANNOTATIONS[0]!.id,
14
14
  resolvedIds: new Set(FIXTURE_ANNOTATIONS.map((annotation) => annotation.id)),
15
+ curationItems: [],
15
16
  railTab: "review" as const,
16
17
  pendingCount: 2,
17
18
  cardEdit: null,
@@ -19,6 +20,8 @@ const rail = {
19
20
  onTabChange: () => {},
20
21
  onSelectCard: () => {},
21
22
  onActivateCard: () => {},
23
+ onSelectCuration: () => {},
24
+ onUndoCuration: () => {},
22
25
  onSubmitRequest: () => {},
23
26
  };
24
27
 
@@ -27,7 +30,14 @@ const panelCallbacks = { onDividerGrab: () => {}, onToggle: () => {} };
27
30
  /** The plan area to the left of the divider, so the layout reads in context. */
28
31
  function PanelFrame({ children }: { children: React.ReactNode }): React.ReactNode {
29
32
  return (
30
- <box style={{ flexDirection: "row", width: "100%", height: "100%", backgroundColor: DARK.background }}>
33
+ <box
34
+ style={{
35
+ flexDirection: "row",
36
+ width: "100%",
37
+ height: "100%",
38
+ backgroundColor: DARK.background,
39
+ }}
40
+ >
31
41
  <box style={{ flexGrow: 1, paddingLeft: 1 }}>
32
42
  <text fg={DARK.textMuted}>The plan takes the width the panel hands back.</text>
33
43
  </box>
@@ -39,48 +49,36 @@ function PanelFrame({ children }: { children: React.ReactNode }): React.ReactNod
39
49
  export const ExpandedNarrow: Story = {
40
50
  render: () => (
41
51
  <PanelFrame>
42
- <ReviewPanel mode="expanded" width={REVIEW_MIN_WIDTH} height={24} dragging={false} rail={rail} {...panelCallbacks} />
52
+ <ReviewPanel mode="expanded" width={REVIEW_MIN_WIDTH} rail={rail} {...panelCallbacks} />
43
53
  </PanelFrame>
44
54
  ),
45
- expectedColors: [DARK.border],
46
55
  size: { width: 80, height: 24 },
47
56
  };
48
57
 
49
58
  export const ExpandedWide: Story = {
50
59
  render: () => (
51
60
  <PanelFrame>
52
- <ReviewPanel mode="expanded" width={REVIEW_MAX_WIDTH} height={24} dragging={false} rail={rail} {...panelCallbacks} />
53
- </PanelFrame>
54
- ),
55
- expectedColors: [DARK.green],
56
- size: { width: 90, height: 24 },
57
- };
58
-
59
- /** Dragging accents the divider glyph column. */
60
- export const DraggingDivider: Story = {
61
- render: () => (
62
- <PanelFrame>
63
- <ReviewPanel mode="expanded" width={34} height={24} dragging={true} rail={rail} {...panelCallbacks} />
61
+ <ReviewPanel mode="expanded" width={REVIEW_MAX_WIDTH} rail={rail} {...panelCallbacks} />
64
62
  </PanelFrame>
65
63
  ),
66
64
  expectedColors: [DARK.accent],
67
- size: { width: 80, height: 24 },
65
+ size: { width: 90, height: 24 },
68
66
  };
69
67
 
70
68
  export const Compact: Story = {
71
69
  render: () => (
72
70
  <PanelFrame>
73
- <ReviewPanel mode="compact" width={34} height={24} dragging={false} rail={rail} {...panelCallbacks} />
71
+ <ReviewPanel mode="compact" width={34} rail={rail} {...panelCallbacks} />
74
72
  </PanelFrame>
75
73
  ),
76
- expectedColors: [DARK.accent, DARK.green],
74
+ expectedColors: [DARK.accent],
77
75
  size: { width: 60, height: 24 },
78
76
  };
79
77
 
80
78
  export const Hidden: Story = {
81
79
  render: () => (
82
80
  <PanelFrame>
83
- <ReviewPanel mode="hidden" width={34} height={24} dragging={false} rail={rail} {...panelCallbacks} />
81
+ <ReviewPanel mode="hidden" width={34} rail={rail} {...panelCallbacks} />
84
82
  </PanelFrame>
85
83
  ),
86
84
  size: { width: 60, height: 24 },
@@ -14,16 +14,12 @@ import type { Annotation } from "@cueloop/schema";
14
14
  import type { Theme } from "../theme";
15
15
  import { useComponentTheme } from "./theme-context";
16
16
  import { ReviewRail, type ReviewRailHandle, type ReviewRailProps } from "./ReviewRail";
17
- import { REVIEW_COMPACT_WIDTH, reviewRowsToDivider, type ReviewPanelMode } from "../review-panel";
17
+ import { REVIEW_COMPACT_WIDTH, type ReviewPanelMode } from "../review-panel";
18
18
 
19
19
  export interface ReviewPanelProps {
20
20
  mode: ReviewPanelMode;
21
21
  /** Expanded-rail width in columns (already clamped by the app). */
22
22
  width: number;
23
- /** Rows for the divider glyph column - the height of the plan/rail row. */
24
- height: number;
25
- /** Accent the divider while a drag is live. */
26
- dragging: boolean;
27
23
  /** Arm a divider drag; the app tracks the drag itself on the container. */
28
24
  onDividerGrab: () => void;
29
25
  /** Chevron click: expanded <-> compact (never hidden). */
@@ -34,28 +30,15 @@ export interface ReviewPanelProps {
34
30
  theme?: Theme;
35
31
  }
36
32
 
37
- /** A single-column stack of `│`, accent while dragging. Grabbing arms a drag. */
38
- function ReviewDivider({
39
- dragging,
40
- rows,
41
- onGrab,
42
- theme,
43
- }: {
44
- dragging: boolean;
45
- rows: number;
46
- onGrab: () => void;
47
- theme?: Theme;
48
- }): React.ReactNode {
49
- const tokens = useComponentTheme(theme);
50
- return (
51
- <box style={{ width: 1 }} onMouseDown={onGrab}>
52
- <text fg={dragging ? tokens.accent : tokens.border}>{reviewRowsToDivider(rows)}</text>
53
- </box>
54
- );
33
+ /** An invisible, full-height grab column: the plan's right border is the seam. */
34
+ function ReviewDivider({ onGrab }: { onGrab: () => void }): React.ReactNode {
35
+ // childless: it stretches to the row height like the plan and rail beside it,
36
+ // so the three columns share one bottom edge. Invisible, but full-height grab.
37
+ return <box style={{ width: 1 }} onMouseDown={onGrab} />;
55
38
  }
56
39
 
57
40
  /** The compact strip: the card count, one kind-colored dot per card, and the
58
- * `«` chevron left-bound to match the expanded `»` gap. */
41
+ * `<` expand chevron, bottom-padded to sit at the same row as the expanded `>`. */
59
42
  function CompactRail({
60
43
  annotations,
61
44
  onExpand,
@@ -66,11 +49,15 @@ function CompactRail({
66
49
  theme?: Theme;
67
50
  }): React.ReactNode {
68
51
  const tokens = useComponentTheme(theme);
69
- const dotColor = (annotation: Annotation): string =>
70
- annotation.kind === "suggestion" ? tokens.green : tokens.accent;
71
52
  return (
72
53
  <box
73
- style={{ width: REVIEW_COMPACT_WIDTH, backgroundColor: tokens.panel, flexDirection: "column", paddingTop: 1 }}
54
+ style={{
55
+ width: REVIEW_COMPACT_WIDTH,
56
+ backgroundColor: tokens.panel,
57
+ flexDirection: "column",
58
+ paddingTop: 1,
59
+ paddingBottom: 1,
60
+ }}
74
61
  >
75
62
  <box style={{ alignItems: "center" }}>
76
63
  <text fg={tokens.accent}>{String(annotations.length)}</text>
@@ -78,14 +65,14 @@ function CompactRail({
78
65
  <box style={{ height: 1 }} />
79
66
  <box style={{ flexDirection: "column", alignItems: "center" }}>
80
67
  {annotations.map((annotation) => (
81
- <text key={annotation.id} fg={dotColor(annotation)}>
68
+ <text key={annotation.id} fg={tokens.accent}>
82
69
  ●
83
70
  </text>
84
71
  ))}
85
72
  </box>
86
73
  <box style={{ flexGrow: 1 }} />
87
74
  <box style={{ paddingLeft: 1 }} onMouseUp={onExpand}>
88
- <text fg={tokens.textDim}>«</text>
75
+ <text fg={tokens.textDim}>{"<"}</text>
89
76
  </box>
90
77
  </box>
91
78
  );
@@ -94,8 +81,6 @@ function CompactRail({
94
81
  export function ReviewPanel({
95
82
  mode,
96
83
  width,
97
- height,
98
- dragging,
99
84
  onDividerGrab,
100
85
  onToggle,
101
86
  rail,
@@ -105,7 +90,7 @@ export function ReviewPanel({
105
90
  if (mode === "hidden") return null;
106
91
  return (
107
92
  <>
108
- <ReviewDivider dragging={dragging} rows={height} onGrab={onDividerGrab} theme={theme} />
93
+ <ReviewDivider onGrab={onDividerGrab} />
109
94
  {mode === "expanded" ? (
110
95
  <ReviewRail ref={railRef} {...rail} width={width} onCollapse={onToggle} theme={theme} />
111
96
  ) : (