@cueloop/client 0.1.0-alpha.4 → 0.1.0-alpha.41
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.
- package/native/build.sh +56 -0
- package/native/darwin-arm64/libcueloopvt.dylib +0 -0
- package/native/src/shim.c +107 -0
- package/package.json +34 -24
- package/src/App.autoclose.test.tsx +137 -0
- package/src/App.diff.test.tsx +158 -22
- package/src/App.export.test.tsx +44 -37
- package/src/App.plan-review.test.tsx +632 -0
- package/src/App.readonly.test.tsx +60 -36
- package/src/App.rendering.test.tsx +132 -0
- package/src/App.review-panel.test.tsx +136 -0
- package/src/App.submit-confirm.test.tsx +220 -0
- package/src/App.test.tsx +128 -69
- package/src/App.toast.test.tsx +75 -0
- package/src/App.tsx +0 -0
- package/src/App.walk.test.tsx +288 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +38 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +30 -0
- package/src/components/AnnotationCard.compose-rows.test.ts +49 -0
- package/src/components/AnnotationCard.stories.tsx +94 -0
- package/src/components/AnnotationCard.tsx +228 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +44 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +83 -0
- package/src/components/CompletionOverlay.stories.tsx +23 -0
- package/src/components/CompletionOverlay.tsx +59 -0
- package/src/components/ConfirmCard.stories.tsx +56 -0
- package/src/components/ConfirmCard.tsx +135 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +64 -0
- package/src/components/DiffSheet.scroll.test.tsx +123 -0
- package/src/components/DiffSheet.stories.tsx +78 -0
- package/src/components/DiffSheet.syntax.test.tsx +62 -0
- package/src/components/DiffSheet.tsx +355 -0
- package/src/components/InboxList.stories.tsx +31 -0
- package/src/components/InboxList.tsx +62 -0
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +58 -0
- package/src/components/MarkerPopover.stories.tsx +58 -0
- package/src/components/MarkerPopover.tsx +102 -0
- package/src/components/MenuBar.stories.tsx +37 -0
- package/src/components/MenuBar.tsx +74 -0
- package/src/components/PlanSheet.stories.tsx +113 -0
- package/src/components/PlanSheet.tsx +389 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.tsx +78 -0
- package/src/components/ReviewPanel.stories.tsx +86 -0
- package/src/components/ReviewPanel.tsx +101 -0
- package/src/components/ReviewRail.stories.tsx +220 -0
- package/src/components/ReviewRail.tsx +324 -0
- package/src/components/SettingsDialog.stories.tsx +111 -0
- package/src/components/SettingsDialog.tsx +141 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +108 -0
- package/src/components/Toast.stories.tsx +12 -0
- package/src/components/Toast.tsx +44 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +152 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +1829 -0
- package/src/components/agent-launcher.stories.tsx +26 -0
- package/src/components/agent-launcher.test.tsx +21 -0
- package/src/components/agent-launcher.tsx +180 -0
- package/src/components/diff-sheet-layout.test.ts +127 -0
- package/src/components/diff-sheet-layout.ts +167 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +48 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +59 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.tsx +64 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +22 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +98 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +14 -0
- package/src/components/quick-actions-editor.stories.tsx +54 -0
- package/src/components/quick-actions-editor.tsx +136 -0
- package/src/components/stories-app.tsx +123 -0
- package/src/components/stories.test.tsx +62 -0
- package/src/components/story-fixtures.ts +98 -0
- package/src/components/story.ts +69 -0
- package/src/components/syntax-highlight.ts +92 -0
- package/src/components/terminal-pane.test.tsx +40 -0
- package/src/components/terminal-pane.ts +174 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +36 -0
- package/src/components/truncate-text.ts +6 -0
- package/src/config.test.ts +430 -31
- package/src/config.ts +304 -26
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +233 -0
- package/src/diff-intraline.test.ts +164 -0
- package/src/diff-intraline.ts +132 -0
- package/src/diff-syntax.test.ts +96 -0
- package/src/diff-syntax.ts +154 -0
- package/src/editor.test.ts +143 -0
- package/src/editor.ts +123 -9
- package/src/ghostty-terminal.test.ts +45 -0
- package/src/ghostty-terminal.ts +168 -0
- package/src/herdr.test.ts +41 -0
- package/src/herdr.ts +27 -0
- package/src/index.ts +10 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +30 -0
- package/src/intent-dispatch.test.ts +492 -0
- package/src/intent-dispatch.ts +428 -0
- package/src/key-bindings.test.ts +152 -0
- package/src/key-bindings.ts +424 -0
- package/src/keymap.test.ts +622 -0
- package/src/keymap.ts +339 -0
- package/src/review-panel.test.ts +113 -0
- package/src/review-panel.ts +83 -0
- package/src/run.ts +9 -5
- package/src/serve.test.ts +24 -9
- package/src/serve.ts +7 -7
- package/src/session-controller.curate.test.ts +253 -0
- package/src/session-controller.share.test.ts +309 -0
- package/src/session-controller.ts +900 -0
- package/src/share.ts +113 -0
- package/src/test-support.test.ts +55 -0
- package/src/test-support.ts +187 -0
- package/src/theme-presets.ts +168 -0
- package/src/theme.ts +36 -15
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +19 -8
- package/src/view-diff.ts +34 -23
- package/src/view-plan.test.ts +294 -0
- package/src/view-plan.ts +408 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +70 -0
- package/src/view.test.ts +0 -157
- 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
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The marker-actions popover: the visible form of plan span mode, floated as a
|
|
3
|
+
* bordered card above the marked block. The toolbar card is always shown -
|
|
4
|
+
* `comment · cut · actions · [x]`, each label keyboard-shortcut-backed and
|
|
5
|
+
* clickable. Pressing `actions` drops a second card below it listing the
|
|
6
|
+
* quick-actions; picking one inserts its preset comment. Plan-only: the diff
|
|
7
|
+
* review has no span.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from "react";
|
|
11
|
+
import type { Theme } from "../theme";
|
|
12
|
+
import type { QuickAction } from "../config";
|
|
13
|
+
import { useComponentTheme } from "./theme-context";
|
|
14
|
+
import { cardHeight } from "./primitives/Card";
|
|
15
|
+
import { FRAME_BORDER_STYLE } from "./primitives/frame";
|
|
16
|
+
|
|
17
|
+
export interface MarkerPopoverProps {
|
|
18
|
+
/** "toolbar" is the bar alone; "actions" also drops the quick-actions card. */
|
|
19
|
+
view: "toolbar" | "actions";
|
|
20
|
+
actions: QuickAction[];
|
|
21
|
+
/** Highlighted action in the dropdown (accent); ignored in toolbar view. */
|
|
22
|
+
actionIndex: number;
|
|
23
|
+
/** Show the cut affordance: owner-only, like every other plan-edit control. */
|
|
24
|
+
canCut: boolean;
|
|
25
|
+
onComment: () => void;
|
|
26
|
+
onCut: () => void;
|
|
27
|
+
onOpenActions: () => void;
|
|
28
|
+
onClose: () => void;
|
|
29
|
+
onPickAction: (index: number) => void;
|
|
30
|
+
onBack: () => void;
|
|
31
|
+
theme?: Theme;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function MarkerPopover({
|
|
35
|
+
view,
|
|
36
|
+
actions,
|
|
37
|
+
actionIndex,
|
|
38
|
+
canCut,
|
|
39
|
+
onComment,
|
|
40
|
+
onCut,
|
|
41
|
+
onOpenActions,
|
|
42
|
+
onClose,
|
|
43
|
+
onPickAction,
|
|
44
|
+
onBack,
|
|
45
|
+
theme,
|
|
46
|
+
}: MarkerPopoverProps): React.ReactNode {
|
|
47
|
+
const tokens = useComponentTheme(theme);
|
|
48
|
+
const cardStyle = {
|
|
49
|
+
border: true,
|
|
50
|
+
borderStyle: FRAME_BORDER_STYLE,
|
|
51
|
+
borderColor: tokens.border,
|
|
52
|
+
backgroundColor: tokens.elevated,
|
|
53
|
+
flexDirection: "column" as const,
|
|
54
|
+
paddingLeft: 1,
|
|
55
|
+
paddingRight: 1,
|
|
56
|
+
};
|
|
57
|
+
const open = view === "actions";
|
|
58
|
+
return (
|
|
59
|
+
<box style={{ flexDirection: "column", alignItems: "flex-start" }}>
|
|
60
|
+
<box style={{ ...cardStyle, height: cardHeight(1) }}>
|
|
61
|
+
<box style={{ flexDirection: "row" }}>
|
|
62
|
+
<text fg={tokens.accent} onMouseUp={onComment}>
|
|
63
|
+
comment
|
|
64
|
+
</text>
|
|
65
|
+
<text fg={tokens.textDim}> · </text>
|
|
66
|
+
{canCut ? (
|
|
67
|
+
<>
|
|
68
|
+
<text fg={tokens.red} onMouseUp={onCut}>
|
|
69
|
+
cut
|
|
70
|
+
</text>
|
|
71
|
+
<text fg={tokens.textDim}> · </text>
|
|
72
|
+
</>
|
|
73
|
+
) : null}
|
|
74
|
+
<text fg={open ? tokens.accent : tokens.textMuted} onMouseUp={onOpenActions}>
|
|
75
|
+
actions
|
|
76
|
+
</text>
|
|
77
|
+
<text fg={tokens.textDim}> · </text>
|
|
78
|
+
<text fg={tokens.textDim} onMouseUp={onClose}>
|
|
79
|
+
[x]
|
|
80
|
+
</text>
|
|
81
|
+
</box>
|
|
82
|
+
</box>
|
|
83
|
+
{open ? (
|
|
84
|
+
<box style={{ ...cardStyle, height: cardHeight(actions.length + 1) }}>
|
|
85
|
+
{actions.map((action, index) => (
|
|
86
|
+
<text
|
|
87
|
+
key={index}
|
|
88
|
+
fg={index === actionIndex ? tokens.accent : tokens.textMuted}
|
|
89
|
+
onMouseUp={() => onPickAction(index)}
|
|
90
|
+
>
|
|
91
|
+
{index === actionIndex ? "› " : " "}
|
|
92
|
+
{action.prompt}
|
|
93
|
+
</text>
|
|
94
|
+
))}
|
|
95
|
+
<text fg={tokens.textDim} onMouseUp={onBack}>
|
|
96
|
+
{" "}‹ back
|
|
97
|
+
</text>
|
|
98
|
+
</box>
|
|
99
|
+
) : null}
|
|
100
|
+
</box>
|
|
101
|
+
);
|
|
102
|
+
}
|