@cueloop/client 0.1.0-alpha.5 → 0.1.0-alpha.51

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 (152) hide show
  1. package/native/build-pty.sh +51 -0
  2. package/native/build.sh +82 -0
  3. package/native/darwin-arm64/libcuelooppty.dylib +0 -0
  4. package/native/darwin-arm64/libcueloopvt.dylib +0 -0
  5. package/native/src/pty.zig +193 -0
  6. package/native/src/shim.zig +114 -0
  7. package/package.json +36 -31
  8. package/src/App.autoclose.test.tsx +137 -0
  9. package/src/App.diff.test.tsx +158 -22
  10. package/src/App.export.test.tsx +44 -37
  11. package/src/App.plan-review.test.tsx +672 -0
  12. package/src/App.prototype.test.tsx +155 -0
  13. package/src/App.readonly.test.tsx +60 -36
  14. package/src/App.rendering.test.tsx +132 -0
  15. package/src/App.review-panel.test.tsx +136 -0
  16. package/src/App.submit-confirm.test.tsx +218 -0
  17. package/src/App.test.tsx +128 -69
  18. package/src/App.toast.test.tsx +75 -0
  19. package/src/App.tsx +0 -0
  20. package/src/App.walk.test.tsx +289 -0
  21. package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
  22. package/src/attribution.test.ts +31 -0
  23. package/src/attribution.ts +38 -0
  24. package/src/clipboard.test.ts +23 -0
  25. package/src/clipboard.ts +30 -0
  26. package/src/components/AnnotationCard.compose-rows.test.ts +49 -0
  27. package/src/components/AnnotationCard.stories.tsx +94 -0
  28. package/src/components/AnnotationCard.tsx +241 -0
  29. package/src/components/Breadcrumb.stories.tsx +19 -0
  30. package/src/components/Breadcrumb.tsx +44 -0
  31. package/src/components/CodeBlock.stories.tsx +42 -0
  32. package/src/components/CodeBlock.tsx +83 -0
  33. package/src/components/CompletionOverlay.stories.tsx +23 -0
  34. package/src/components/CompletionOverlay.tsx +59 -0
  35. package/src/components/ConfirmCard.stories.tsx +35 -0
  36. package/src/components/ConfirmCard.tsx +127 -0
  37. package/src/components/ConfirmDialog.stories.tsx +21 -0
  38. package/src/components/ConfirmDialog.tsx +64 -0
  39. package/src/components/DiffSheet.scroll.test.tsx +123 -0
  40. package/src/components/DiffSheet.stories.tsx +78 -0
  41. package/src/components/DiffSheet.syntax.test.tsx +62 -0
  42. package/src/components/DiffSheet.tsx +355 -0
  43. package/src/components/InboxList.stories.tsx +31 -0
  44. package/src/components/InboxList.tsx +62 -0
  45. package/src/components/KeybindsDialog.stories.tsx +31 -0
  46. package/src/components/KeybindsDialog.tsx +58 -0
  47. package/src/components/MarkerPopover.stories.tsx +58 -0
  48. package/src/components/MarkerPopover.tsx +108 -0
  49. package/src/components/MenuBar.stories.tsx +37 -0
  50. package/src/components/MenuBar.tsx +74 -0
  51. package/src/components/PlanSheet.stories.tsx +113 -0
  52. package/src/components/PlanSheet.tsx +456 -0
  53. package/src/components/PromptDialog.stories.tsx +36 -0
  54. package/src/components/PromptDialog.tsx +78 -0
  55. package/src/components/PrototypeSheet.stories.tsx +20 -0
  56. package/src/components/PrototypeSheet.tsx +358 -0
  57. package/src/components/ReviewPanel.stories.tsx +86 -0
  58. package/src/components/ReviewPanel.tsx +101 -0
  59. package/src/components/ReviewRail.stories.tsx +218 -0
  60. package/src/components/ReviewRail.tsx +317 -0
  61. package/src/components/SettingsDialog.stories.tsx +111 -0
  62. package/src/components/SettingsDialog.tsx +141 -0
  63. package/src/components/SettingsRows.stories.tsx +18 -0
  64. package/src/components/SettingsRows.tsx +108 -0
  65. package/src/components/Toast.stories.tsx +12 -0
  66. package/src/components/Toast.tsx +44 -0
  67. package/src/components/WalkWizard.stories.tsx +100 -0
  68. package/src/components/WalkWizard.tsx +152 -0
  69. package/src/components/__snapshots__/stories.test.tsx.snap +1857 -0
  70. package/src/components/agent-launcher.stories.tsx +26 -0
  71. package/src/components/agent-launcher.test.tsx +49 -0
  72. package/src/components/agent-launcher.tsx +147 -0
  73. package/src/components/diff-sheet-layout.test.ts +127 -0
  74. package/src/components/diff-sheet-layout.ts +167 -0
  75. package/src/components/primitives/Button.stories.tsx +32 -0
  76. package/src/components/primitives/Button.tsx +51 -0
  77. package/src/components/primitives/Card.stories.tsx +25 -0
  78. package/src/components/primitives/Card.tsx +59 -0
  79. package/src/components/primitives/Dialog.stories.tsx +24 -0
  80. package/src/components/primitives/Dialog.tsx +64 -0
  81. package/src/components/primitives/StatusBar.stories.tsx +11 -0
  82. package/src/components/primitives/StatusBar.tsx +22 -0
  83. package/src/components/primitives/Tabs.stories.tsx +29 -0
  84. package/src/components/primitives/Tabs.tsx +98 -0
  85. package/src/components/primitives/Toolbar.stories.tsx +17 -0
  86. package/src/components/primitives/Toolbar.tsx +14 -0
  87. package/src/components/primitives/frame.test.ts +12 -0
  88. package/src/components/primitives/frame.ts +14 -0
  89. package/src/components/quick-actions-editor.stories.tsx +54 -0
  90. package/src/components/quick-actions-editor.tsx +136 -0
  91. package/src/components/stories-app.tsx +123 -0
  92. package/src/components/stories.test.tsx +62 -0
  93. package/src/components/story-fixtures.ts +98 -0
  94. package/src/components/story.ts +69 -0
  95. package/src/components/syntax-highlight.ts +92 -0
  96. package/src/components/terminal-pane.test.tsx +40 -0
  97. package/src/components/terminal-pane.ts +181 -0
  98. package/src/components/theme-context.stories.tsx +46 -0
  99. package/src/components/theme-context.tsx +36 -0
  100. package/src/components/truncate-text.ts +6 -0
  101. package/src/config.test.ts +430 -31
  102. package/src/config.ts +304 -26
  103. package/src/diff-hunk-curate.test.ts +315 -0
  104. package/src/diff-hunk-curate.ts +233 -0
  105. package/src/diff-intraline.test.ts +164 -0
  106. package/src/diff-intraline.ts +132 -0
  107. package/src/diff-syntax.test.ts +96 -0
  108. package/src/diff-syntax.ts +154 -0
  109. package/src/editor.test.ts +143 -0
  110. package/src/editor.ts +123 -9
  111. package/src/ghostty-terminal.test.ts +82 -0
  112. package/src/ghostty-terminal.ts +168 -0
  113. package/src/herdr.test.ts +41 -0
  114. package/src/herdr.ts +27 -0
  115. package/src/index.ts +10 -0
  116. package/src/integrations.test.ts +41 -0
  117. package/src/integrations.ts +30 -0
  118. package/src/intent-dispatch.test.ts +493 -0
  119. package/src/intent-dispatch.ts +428 -0
  120. package/src/key-bindings.test.ts +152 -0
  121. package/src/key-bindings.ts +430 -0
  122. package/src/keymap.test.ts +622 -0
  123. package/src/keymap.ts +339 -0
  124. package/src/kitty-image.test.ts +20 -0
  125. package/src/kitty-image.ts +75 -0
  126. package/src/prototype-browser.test.ts +55 -0
  127. package/src/prototype-browser.ts +243 -0
  128. package/src/pty.test.ts +78 -0
  129. package/src/pty.ts +222 -0
  130. package/src/review-panel.test.ts +113 -0
  131. package/src/review-panel.ts +83 -0
  132. package/src/run.ts +9 -5
  133. package/src/serve.test.ts +24 -9
  134. package/src/serve.ts +7 -7
  135. package/src/session-controller.curate.test.ts +253 -0
  136. package/src/session-controller.share.test.ts +309 -0
  137. package/src/session-controller.ts +914 -0
  138. package/src/share.ts +113 -0
  139. package/src/test-support.test.ts +55 -0
  140. package/src/test-support.ts +187 -0
  141. package/src/theme-presets.ts +168 -0
  142. package/src/theme.ts +38 -17
  143. package/src/version.ts +5 -0
  144. package/src/view-diff.test.ts +19 -8
  145. package/src/view-diff.ts +34 -23
  146. package/src/view-plan.regression.test.ts +60 -0
  147. package/src/view-plan.test.ts +399 -0
  148. package/src/view-plan.ts +495 -0
  149. package/src/walk.test.ts +121 -0
  150. package/src/walk.ts +70 -0
  151. package/src/view.test.ts +0 -157
  152. package/src/view.ts +0 -0
@@ -0,0 +1,355 @@
1
+ /**
2
+ * The diff review sheet: a thin renderer over diff-sheet-layout. Rows render (no
3
+ * window cap) in a scrollbox with file/hunk headers and contiguous code chunks.
4
+ * A chunk draws through the native line-number gutter - custom numbers per row,
5
+ * the cursor as a gutter sign, annotation markers as line signs - and closes
6
+ * after an annotated row so the annotation body sits under its line. A
7
+ * curated-out row renders struck through.
8
+ */
9
+
10
+ import React, { useEffect, useMemo, useRef, useState } from "react";
11
+ import {
12
+ createTextAttributes,
13
+ type LineNumberRenderable,
14
+ type LineSign,
15
+ type ScrollBoxRenderable,
16
+ } from "@opentui/core";
17
+ import type { Annotation } from "@cueloop/schema";
18
+ import type { DiffRow } from "../view-diff";
19
+ import type { Theme } from "../theme";
20
+ import { useComponentTheme } from "./theme-context";
21
+ import { truncateToSingleLine } from "./truncate-text";
22
+ import { AnnotationCard, type AnnotationDraft } from "./AnnotationCard";
23
+ import { FRAME_BORDER_STYLE } from "./primitives/frame";
24
+ import { intralineRunsByRow, type IntralineRun } from "../diff-intraline";
25
+ import { highlightDiffRows, type SyntaxSpan } from "../diff-syntax";
26
+ import {
27
+ annotatedRowsByIndex,
28
+ coloredRowSpans,
29
+ rowContentOffsets,
30
+ rowLine,
31
+ segmentRows,
32
+ type DiffSegment,
33
+ } from "./diff-sheet-layout";
34
+
35
+ /** Shared empty map so an unresolved/stale highlight state is a stable value. */
36
+ const EMPTY_SYNTAX: Map<number, SyntaxSpan[]> = new Map();
37
+
38
+ /** Shared empty set so "nothing rejected" is a stable identity for renders. */
39
+ const EMPTY_REJECTED: Set<number> = new Set();
40
+
41
+ /** A rejected (curated-out) change row renders struck through and dimmed. */
42
+ const REJECTED_ATTRIBUTES = createTextAttributes({ strikethrough: true, dim: true });
43
+
44
+ export interface DiffComposeState {
45
+ kind: "comment";
46
+ rowIndex: number;
47
+ quote: string;
48
+ draft: AnnotationDraft;
49
+ }
50
+
51
+ export interface DiffSheetProps {
52
+ rows: DiffRow[];
53
+ cursor: number;
54
+ annotations: Annotation[];
55
+ focusedAnnotationId?: string;
56
+ /** Row indices the owner rejected during curation; drawn struck through. */
57
+ rejectedRows?: Set<number>;
58
+ compose?: DiffComposeState | null;
59
+ theme?: Theme;
60
+ }
61
+
62
+ /** The sign, base color, and background a row draws with. */
63
+ function rowStyle(
64
+ row: DiffRow,
65
+ isCursorRow: boolean,
66
+ isAnnotatedRow: boolean,
67
+ tokens: Theme,
68
+ ): { sign: string; baseColor: string; background: string | undefined } {
69
+ const sign = row.kind === "add" ? "+" : row.kind === "del" ? "-" : " ";
70
+ const baseColor =
71
+ row.kind === "add"
72
+ ? tokens.insertedForeground
73
+ : row.kind === "del"
74
+ ? tokens.deletedForeground
75
+ : tokens.textMuted;
76
+ const background = isCursorRow
77
+ ? tokens.cursorBackground
78
+ : isAnnotatedRow
79
+ ? tokens.markCommentBackground
80
+ : undefined;
81
+ return { sign, baseColor, background };
82
+ }
83
+
84
+ function DiffChunk({
85
+ segment,
86
+ cursor,
87
+ focusedAnnotationId,
88
+ intralineByRow,
89
+ syntaxByRow,
90
+ rejectedRows,
91
+ theme,
92
+ }: {
93
+ segment: Extract<DiffSegment, { kind: "chunk" }>;
94
+ cursor: number;
95
+ focusedAnnotationId?: string;
96
+ intralineByRow: Map<number, IntralineRun[]>;
97
+ syntaxByRow: Map<number, SyntaxSpan[]>;
98
+ rejectedRows: Set<number>;
99
+ theme?: Theme;
100
+ }): React.ReactNode {
101
+ const tokens = useComponentTheme(theme);
102
+ const gutterRef = useRef<LineNumberRenderable | null>(null);
103
+ const cursorInChunk = cursor - segment.firstRowIndex;
104
+
105
+ const lineNumbers = useMemo(() => {
106
+ const numbers = new Map<number, number>();
107
+ segment.rows.forEach((row, lineIndex) => {
108
+ const gutterNumber = row.kind === "del" ? row.oldLine : row.newLine;
109
+ if (gutterNumber !== undefined) numbers.set(lineIndex, gutterNumber);
110
+ });
111
+ return numbers;
112
+ }, [segment]);
113
+
114
+ const lineSigns = useMemo(() => {
115
+ const signs = new Map<number, LineSign>();
116
+ if (cursorInChunk >= 0 && cursorInChunk < segment.rows.length) {
117
+ signs.set(cursorInChunk, { before: "▎", beforeColor: tokens.accent });
118
+ }
119
+ if (segment.annotation) {
120
+ signs.set(segment.rows.length - 1, { after: "◆", afterColor: tokens.accent });
121
+ }
122
+ return signs;
123
+ }, [segment, cursorInChunk, tokens]);
124
+
125
+ const lineColors = useMemo(() => {
126
+ // the renderable's config parser requires the gutter key to be present
127
+ const colors = new Map<number, { gutter: string | undefined; content: string }>();
128
+ if (segment.annotation) {
129
+ colors.set(segment.rows.length - 1, {
130
+ gutter: undefined,
131
+ content: tokens.markCommentBackground,
132
+ });
133
+ }
134
+ if (cursorInChunk >= 0 && cursorInChunk < segment.rows.length) {
135
+ colors.set(cursorInChunk, {
136
+ gutter: tokens.cursorBackground,
137
+ content: tokens.cursorBackground,
138
+ });
139
+ }
140
+ return colors;
141
+ }, [segment, cursorInChunk, tokens]);
142
+
143
+ // the gutter renderable has no prop setters for these maps, so they land via an effect
144
+ useEffect(() => {
145
+ const gutter = gutterRef.current;
146
+ if (!gutter) return;
147
+ gutter.setLineNumbers(lineNumbers);
148
+ gutter.setLineSigns(lineSigns);
149
+ gutter.clearAllLineColors();
150
+ for (const [line, colorConfig] of lineColors) gutter.setLineColor(line, colorConfig);
151
+ }, [lineNumbers, lineSigns, lineColors]);
152
+
153
+ return (
154
+ <box style={{ flexDirection: "column" }}>
155
+ <line-number
156
+ ref={gutterRef}
157
+ fg={tokens.textDim}
158
+ bg={tokens.background}
159
+ minWidth={4}
160
+ paddingRight={1}
161
+ lineNumbers={lineNumbers}
162
+ lineSigns={lineSigns}
163
+ >
164
+ <text style={{ wrapMode: "none" }} selectable={false}>
165
+ {segment.rows.map((row, lineIndex) => {
166
+ const isCursorRow = lineIndex === cursorInChunk;
167
+ const isAnnotatedRow =
168
+ segment.annotation !== null && lineIndex === segment.rows.length - 1;
169
+ const { sign, baseColor, background } = rowStyle(
170
+ row,
171
+ isCursorRow,
172
+ isAnnotatedRow,
173
+ tokens,
174
+ );
175
+ const prefix = (lineIndex > 0 ? "\n" : "") + sign;
176
+ const absoluteRowIndex = segment.firstRowIndex + lineIndex;
177
+
178
+ // A rejected change collapses to one struck-through, dimmed span, so
179
+ // it reads as excluded regardless of its syntax or intra-line colors.
180
+ if (rejectedRows.has(absoluteRowIndex)) {
181
+ return (
182
+ <span
183
+ key={lineIndex}
184
+ fg={tokens.textDim}
185
+ bg={background}
186
+ attributes={REJECTED_ATTRIBUTES}
187
+ >
188
+ {prefix + rowLine(row)}
189
+ </span>
190
+ );
191
+ }
192
+
193
+ const spans = coloredRowSpans(
194
+ rowLine(row),
195
+ intralineByRow.get(absoluteRowIndex),
196
+ syntaxByRow.get(absoluteRowIndex),
197
+ baseColor,
198
+ tokens,
199
+ );
200
+ return (
201
+ <React.Fragment key={lineIndex}>
202
+ <span fg={baseColor} bg={background}>
203
+ {prefix}
204
+ </span>
205
+ {spans.map((span, spanIndex) => (
206
+ <span key={spanIndex} fg={span.foreground} bg={background}>
207
+ {span.text}
208
+ </span>
209
+ ))}
210
+ </React.Fragment>
211
+ );
212
+ })}
213
+ </text>
214
+ </line-number>
215
+
216
+ {segment.annotation ? (
217
+ // one row exactly: wrapMode none keeps the model's 1-line-per-card offset
218
+ // honest, so the cursor-follow scroll never drifts past a wrapped card
219
+ <text style={{ wrapMode: "none" }}>
220
+ <span fg={tokens.textDim}>{" "}</span>
221
+ <span fg={segment.annotation.id === focusedAnnotationId ? tokens.text : tokens.accent}>
222
+ ◆ {truncateToSingleLine(segment.annotation.body, 70)}
223
+ </span>
224
+ </text>
225
+ ) : null}
226
+ </box>
227
+ );
228
+ }
229
+
230
+ /** Async tree-sitter highlights, discarded when they belong to superseded rows. */
231
+ function useSyntaxHighlights(rows: DiffRow[]): Map<number, SyntaxSpan[]> {
232
+ const [highlighted, setHighlighted] = useState<{
233
+ rows: DiffRow[];
234
+ byRow: Map<number, SyntaxSpan[]>;
235
+ }>({
236
+ rows,
237
+ byRow: EMPTY_SYNTAX,
238
+ });
239
+ useEffect(() => {
240
+ let active = true;
241
+ void highlightDiffRows(rows).then((byRow) => {
242
+ if (active) setHighlighted({ rows, byRow });
243
+ });
244
+ return () => {
245
+ active = false;
246
+ };
247
+ }, [rows]);
248
+ return highlighted.rows === rows ? highlighted.byRow : EMPTY_SYNTAX;
249
+ }
250
+
251
+ export function DiffSheet({
252
+ rows,
253
+ cursor,
254
+ annotations,
255
+ focusedAnnotationId,
256
+ rejectedRows = EMPTY_REJECTED,
257
+ compose,
258
+ theme,
259
+ }: DiffSheetProps): React.ReactNode {
260
+ const tokens = useComponentTheme(theme);
261
+ const scrollRef = useRef<ScrollBoxRenderable | null>(null);
262
+
263
+ const annotatedByRow = useMemo(
264
+ () => annotatedRowsByIndex(rows, annotations),
265
+ [rows, annotations],
266
+ );
267
+ const segments = useMemo(
268
+ () => segmentRows(rows, annotatedByRow, compose?.rowIndex),
269
+ [rows, annotatedByRow, compose?.rowIndex],
270
+ );
271
+ const intralineByRow = useMemo(() => intralineRunsByRow(rows), [rows]);
272
+ const syntaxByRow = useSyntaxHighlights(rows);
273
+ const rowOffsets = useMemo(() => rowContentOffsets(segments), [segments]);
274
+
275
+ // follow the cursor: keep it a couple of rows inside the viewport
276
+ useEffect(() => {
277
+ const scrollbox = scrollRef.current;
278
+ const cursorOffset = rowOffsets[cursor];
279
+ if (!scrollbox || cursorOffset === undefined) return;
280
+ const viewportHeight = Math.max(1, scrollbox.height);
281
+ if (cursorOffset < scrollbox.scrollTop + 2) {
282
+ scrollbox.scrollTo({ x: 0, y: Math.max(0, cursorOffset - 2) });
283
+ } else if (cursorOffset > scrollbox.scrollTop + viewportHeight - 3) {
284
+ scrollbox.scrollTo({ x: 0, y: cursorOffset - viewportHeight + 3 });
285
+ }
286
+ }, [cursor, rowOffsets]);
287
+
288
+ return (
289
+ <box
290
+ style={{
291
+ flexGrow: 1,
292
+ flexDirection: "column",
293
+ border: true,
294
+ borderStyle: FRAME_BORDER_STYLE,
295
+ borderColor: tokens.text,
296
+ paddingLeft: 1,
297
+ paddingTop: 0,
298
+ }}
299
+ >
300
+ <scrollbox id="diff-scroll" ref={scrollRef} style={{ flexGrow: 1 }} focused={false}>
301
+ {segments.map((segment, segmentIndex) => {
302
+ if (segment.kind === "header") {
303
+ const isCursor = segment.rowIndex === cursor;
304
+ if (segment.row.kind === "file") {
305
+ return (
306
+ <text
307
+ key={segmentIndex}
308
+ fg={tokens.text}
309
+ bg={isCursor ? tokens.cursorBackground : tokens.panel}
310
+ style={{ wrapMode: "none" }}
311
+ >
312
+ {isCursor ? "▎" : " "}■ {rowLine(segment.row)}
313
+ </text>
314
+ );
315
+ }
316
+ return (
317
+ <text
318
+ key={segmentIndex}
319
+ fg={tokens.blue}
320
+ bg={isCursor ? tokens.cursorBackground : undefined}
321
+ style={{ wrapMode: "none" }}
322
+ >
323
+ {isCursor ? "▎" : " "}
324
+ {rowLine(segment.row)}
325
+ </text>
326
+ );
327
+ }
328
+
329
+ const lastRowIndex = segment.firstRowIndex + segment.rows.length - 1;
330
+ return (
331
+ <React.Fragment key={segmentIndex}>
332
+ <DiffChunk
333
+ segment={segment}
334
+ cursor={cursor}
335
+ focusedAnnotationId={focusedAnnotationId}
336
+ intralineByRow={intralineByRow}
337
+ syntaxByRow={syntaxByRow}
338
+ rejectedRows={rejectedRows}
339
+ theme={theme}
340
+ />
341
+ {compose && compose.rowIndex === lastRowIndex ? (
342
+ <AnnotationCard
343
+ kind={compose.kind}
344
+ quote={compose.quote}
345
+ draft={compose.draft}
346
+ theme={theme}
347
+ />
348
+ ) : null}
349
+ </React.Fragment>
350
+ );
351
+ })}
352
+ </scrollbox>
353
+ </box>
354
+ );
355
+ }
@@ -0,0 +1,31 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { InboxList } from "./InboxList";
5
+ import { fixtureDiffSession, fixturePlanSession } from "./story-fixtures";
6
+
7
+ export const meta: StoryMeta = { title: "InboxList" };
8
+
9
+ /** InboxList is the body under App's header/menu chrome; a frame gives it height. */
10
+ function BodyFrame({ children }: { children: React.ReactNode }): React.ReactNode {
11
+ return <box style={{ flexDirection: "column", width: "100%", height: "100%" }}>{children}</box>;
12
+ }
13
+
14
+ export const TwoPending: Story = {
15
+ render: () => (
16
+ <BodyFrame>
17
+ <InboxList inbox={[fixturePlanSession(), fixtureDiffSession()]} cursor={0} />
18
+ </BodyFrame>
19
+ ),
20
+ expectedColors: [DARK.cursorBackground],
21
+ size: { width: 80, height: 12 },
22
+ };
23
+
24
+ export const Empty: Story = {
25
+ render: () => (
26
+ <BodyFrame>
27
+ <InboxList inbox={[]} cursor={0} />
28
+ </BodyFrame>
29
+ ),
30
+ size: { width: 80, height: 8 },
31
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The pending-review inbox: a cursor-driven session list. Selection stays with
3
+ * the keyboard grammar; the list only renders the snapshot. The selected row
4
+ * carries a [delete] word-button (click, or `d` in the grammar) that asks the
5
+ * caller to confirm removal. App supplies the surrounding header and MenuBar
6
+ * chrome (the same as plan/diff review); this is the flex-growing body.
7
+ */
8
+
9
+ import React from "react";
10
+ import type { ReviewSession } from "@cueloop/schema";
11
+ import type { Theme } from "../theme";
12
+ import { useComponentTheme } from "./theme-context";
13
+ import { Button } from "./primitives/Button";
14
+
15
+ export interface InboxListProps {
16
+ inbox: ReviewSession[];
17
+ cursor: number;
18
+ /** Ask to delete a plan (the selected row's [delete] button). */
19
+ onRequestDelete?: (id: string, title: string) => void;
20
+ theme?: Theme;
21
+ }
22
+
23
+ export function InboxList({
24
+ inbox,
25
+ cursor,
26
+ onRequestDelete,
27
+ theme,
28
+ }: InboxListProps): React.ReactNode {
29
+ const tokens = useComponentTheme(theme);
30
+ return (
31
+ <box style={{ flexGrow: 1, flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
32
+ {inbox.length === 0 ? (
33
+ <text fg={tokens.textDim}>nothing waiting for review</text>
34
+ ) : (
35
+ inbox.map((session, index) => {
36
+ const selected = index === cursor;
37
+ const title = session.artifact.meta.title ?? session.id;
38
+ return (
39
+ <box
40
+ key={session.id}
41
+ style={{
42
+ flexDirection: "row",
43
+ backgroundColor: selected ? tokens.cursorBackground : undefined,
44
+ }}
45
+ >
46
+ <text fg={selected ? tokens.text : tokens.textMuted}>
47
+ {selected ? "> " : " "}
48
+ {title} · {session.workspace.branch} · {session.artifact.type}
49
+ </text>
50
+ <box style={{ flexGrow: 1 }} />
51
+ {selected && onRequestDelete ? (
52
+ <Button onPress={() => onRequestDelete(session.id, title)} theme={theme}>
53
+ {" [delete] "}
54
+ </Button>
55
+ ) : null}
56
+ </box>
57
+ );
58
+ })
59
+ )}
60
+ </box>
61
+ );
62
+ }
@@ -0,0 +1,31 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { KeybindsDialog } from "./KeybindsDialog";
5
+
6
+ export const meta: StoryMeta = { title: "KeybindsDialog" };
7
+
8
+ const SECTIONS = [
9
+ {
10
+ title: "Review",
11
+ entries: [
12
+ { keys: "j / k", label: "move" },
13
+ { keys: "v", label: "span" },
14
+ { keys: "c", label: "comment" },
15
+ { keys: "enter", label: "submit" },
16
+ ],
17
+ },
18
+ {
19
+ title: "Submit",
20
+ entries: [
21
+ { keys: "← / →", label: "verdict" },
22
+ { keys: "esc", label: "cancel" },
23
+ ],
24
+ },
25
+ ];
26
+
27
+ export const Cheatsheet: Story = {
28
+ render: () => <KeybindsDialog sections={SECTIONS} />,
29
+ expectedColors: [DARK.accent],
30
+ size: { width: 70, height: 24 },
31
+ };
@@ -0,0 +1,58 @@
1
+ import React from "react";
2
+ import { useTerminalDimensions } from "@opentui/react";
3
+ import type { Theme } from "../theme";
4
+ import { useComponentTheme } from "./theme-context";
5
+ import { Dialog } from "./primitives/Dialog";
6
+ import type { CheatsheetSection } from "../key-bindings";
7
+
8
+ export interface KeybindsDialogProps {
9
+ sections: CheatsheetSection[];
10
+ theme?: Theme;
11
+ }
12
+
13
+ export function KeybindsDialog({ sections, theme }: KeybindsDialogProps): React.ReactNode {
14
+ const tokens = useComponentTheme(theme);
15
+ const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
16
+ const contentRows =
17
+ sections.reduce((sum, section) => sum + 1 + section.entries.length + 1, 0) + 1;
18
+ const width = Math.min(52, terminalWidth - 6);
19
+ const height = Math.min(terminalHeight - 4, contentRows + 3);
20
+ return (
21
+ <Dialog
22
+ isOpen
23
+ title=" keybinds "
24
+ width={width}
25
+ height={height}
26
+ background={tokens.elevated}
27
+ theme={theme}
28
+ >
29
+ <box
30
+ style={{
31
+ flexDirection: "column",
32
+ flexGrow: 1,
33
+ paddingLeft: 1,
34
+ paddingRight: 1,
35
+ paddingTop: 1,
36
+ }}
37
+ >
38
+ {sections.map((section) => (
39
+ <box key={section.title} style={{ flexDirection: "column" }}>
40
+ <text fg={tokens.accent}>{section.title}</text>
41
+ {section.entries.map((entry, index) => (
42
+ <text key={index}>
43
+ <span fg={tokens.text}>{entry.keys.padEnd(12)}</span>
44
+ <span fg={tokens.textMuted}>{entry.label}</span>
45
+ </text>
46
+ ))}
47
+ <text> </text>
48
+ </box>
49
+ ))}
50
+ <box style={{ flexGrow: 1 }} />
51
+ <box style={{ flexDirection: "row" }}>
52
+ <box style={{ flexGrow: 1 }} />
53
+ <text fg={tokens.textDim}>[esc]</text>
54
+ </box>
55
+ </box>
56
+ </Dialog>
57
+ );
58
+ }
@@ -0,0 +1,58 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import { DEFAULT_QUICK_ACTIONS } from "../config";
4
+ import type { Story, StoryMeta } from "./story";
5
+ import { MarkerPopover } from "./MarkerPopover";
6
+
7
+ export const meta: StoryMeta = { title: "MarkerPopover" };
8
+
9
+ const callbacks = {
10
+ onComment: () => {},
11
+ onCut: () => {},
12
+ onOpenActions: () => {},
13
+ onClose: () => {},
14
+ onPickAction: () => {},
15
+ onBack: () => {},
16
+ };
17
+
18
+ export const Toolbar: Story = {
19
+ render: () => (
20
+ <MarkerPopover
21
+ view="toolbar"
22
+ actions={DEFAULT_QUICK_ACTIONS}
23
+ actionIndex={0}
24
+ canCut
25
+ {...callbacks}
26
+ />
27
+ ),
28
+ expectedColors: [DARK.accent, DARK.red, DARK.textMuted, DARK.textDim],
29
+ size: { width: 60, height: 4 },
30
+ };
31
+
32
+ export const ToolbarNoCut: Story = {
33
+ render: () => (
34
+ <MarkerPopover
35
+ view="toolbar"
36
+ actions={DEFAULT_QUICK_ACTIONS}
37
+ actionIndex={0}
38
+ canCut={false}
39
+ {...callbacks}
40
+ />
41
+ ),
42
+ expectedColors: [DARK.accent, DARK.textMuted, DARK.textDim],
43
+ size: { width: 60, height: 4 },
44
+ };
45
+
46
+ export const ActionsList: Story = {
47
+ render: () => (
48
+ <MarkerPopover
49
+ view="actions"
50
+ actions={DEFAULT_QUICK_ACTIONS}
51
+ actionIndex={2}
52
+ canCut
53
+ {...callbacks}
54
+ />
55
+ ),
56
+ expectedColors: [DARK.accent, DARK.textMuted, DARK.textDim],
57
+ size: { width: 60, height: 8 },
58
+ };