@cueloop/client 0.1.0-alpha.59 → 0.1.0-alpha.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/App.autoclose.test.tsx +7 -0
- package/src/App.diff.test.tsx +11 -0
- package/src/App.export.test.tsx +5 -0
- package/src/App.plan-review.test.tsx +42 -0
- package/src/App.prototype.test.tsx +11 -0
- package/src/App.readonly.test.tsx +10 -0
- package/src/App.rendering.test.tsx +4 -0
- package/src/App.review-panel.test.tsx +7 -0
- package/src/App.submit-confirm.test.tsx +11 -0
- package/src/App.test.tsx +13 -0
- package/src/App.toast.test.tsx +2 -0
- package/src/App.tsx +27 -0
- package/src/App.walk.test.tsx +10 -0
- package/src/app-key-state.ts +1 -0
- package/src/app-screens.tsx +4 -0
- package/src/app-view-model.ts +21 -0
- package/src/attribution.ts +4 -0
- package/src/clipboard.ts +3 -0
- package/src/components/AnnotationCard.tsx +9 -0
- package/src/components/Breadcrumb.tsx +1 -0
- package/src/components/CodeBlock.tsx +1 -0
- package/src/components/CompletionOverlay.tsx +1 -0
- package/src/components/ConfirmCard.tsx +2 -0
- package/src/components/ConfirmDialog.tsx +2 -0
- package/src/components/DiffSheet.scroll.test.tsx +12 -0
- package/src/components/DiffSheet.syntax.test.tsx +3 -0
- package/src/components/DiffSheet.tsx +19 -0
- package/src/components/InboxList.tsx +2 -0
- package/src/components/KeybindsDialog.tsx +1 -0
- package/src/components/MarkerPopover.tsx +1 -0
- package/src/components/MenuBar.tsx +1 -0
- package/src/components/PlanSheet.tsx +26 -0
- package/src/components/PromptDialog.tsx +2 -0
- package/src/components/PrototypeSheet.tsx +27 -0
- package/src/components/ReviewPanel.tsx +2 -0
- package/src/components/ReviewRail.tsx +1 -0
- package/src/components/SettingsDialog.tsx +6 -0
- package/src/components/SettingsRows.tsx +4 -0
- package/src/components/Toast.tsx +1 -0
- package/src/components/agent-launcher.test.tsx +1 -0
- package/src/components/agent-launcher.tsx +2 -0
- package/src/components/diff-sheet-layout.test.ts +1 -0
- package/src/components/diff-sheet-layout.ts +17 -0
- package/src/components/plan-sheet-run-style.ts +4 -0
- package/src/components/primitives/Button.tsx +1 -0
- package/src/components/primitives/Card.tsx +1 -0
- package/src/components/primitives/Dialog.tsx +2 -0
- package/src/components/primitives/StatusBar.tsx +1 -0
- package/src/components/primitives/Tabs.tsx +4 -0
- package/src/components/quick-actions-editor.tsx +2 -0
- package/src/components/stories-app.tsx +1 -0
- package/src/components/stories.test.tsx +4 -0
- package/src/components/story.ts +6 -0
- package/src/components/syntax-highlight.ts +10 -0
- package/src/components/terminal-pane.ts +12 -0
- package/src/components/theme-context.tsx +1 -0
- package/src/components/truncate-text.ts +1 -0
- package/src/config.test.ts +18 -0
- package/src/config.ts +27 -0
- package/src/diff-hunk-curate.ts +20 -0
- package/src/diff-intraline.test.ts +1 -0
- package/src/diff-intraline.ts +14 -0
- package/src/diff-syntax.ts +22 -0
- package/src/editor.test.ts +6 -0
- package/src/editor.ts +12 -0
- package/src/ghostty-terminal.test.ts +2 -0
- package/src/ghostty-terminal.ts +8 -0
- package/src/herdr.test.ts +2 -0
- package/src/herdr.ts +3 -0
- package/src/integrations.ts +2 -0
- package/src/intent-dispatch.test.ts +6 -0
- package/src/intent-dispatch.ts +37 -0
- package/src/key-bindings.test.ts +4 -0
- package/src/key-bindings.ts +22 -0
- package/src/keymap.test.ts +13 -0
- package/src/keymap.ts +43 -0
- package/src/kitty-image.ts +9 -0
- package/src/prototype-browser.test.ts +1 -0
- package/src/prototype-browser.ts +22 -0
- package/src/pty.test.ts +3 -0
- package/src/pty.ts +15 -0
- package/src/run.ts +1 -0
- package/src/serve.test.ts +1 -0
- package/src/serve.ts +3 -0
- package/src/session-controller.curate.test.ts +7 -0
- package/src/session-controller.share.test.ts +11 -0
- package/src/session-controller.ts +79 -0
- package/src/share.ts +7 -0
- package/src/test-support.test.ts +5 -0
- package/src/test-support.ts +10 -0
- package/src/theme-presets.ts +1 -0
- package/src/use-settings-dialog.tsx +12 -0
- package/src/view-diff.test.ts +3 -0
- package/src/view-diff.ts +5 -0
- package/src/view-plan.test.ts +7 -0
- package/src/view-plan.ts +59 -0
- package/src/walk.ts +4 -0
|
@@ -88,9 +88,11 @@ export function composeRowCount(
|
|
|
88
88
|
): number {
|
|
89
89
|
const usableWidth = contentWidth > 0 ? contentWidth : Number.MAX_SAFE_INTEGER;
|
|
90
90
|
let visualRowCount = 0;
|
|
91
|
+
|
|
91
92
|
for (const line of text.split("\n")) {
|
|
92
93
|
visualRowCount += Math.max(1, Math.ceil(line.length / usableWidth));
|
|
93
94
|
}
|
|
95
|
+
|
|
94
96
|
return Math.min(cap, Math.max(1, visualRowCount));
|
|
95
97
|
}
|
|
96
98
|
|
|
@@ -110,13 +112,16 @@ function DraftEditor({
|
|
|
110
112
|
const editorRef = useRef<TextareaRenderable | null>(null);
|
|
111
113
|
// re-edit opens with the caret after the existing body, like a text field
|
|
112
114
|
const initialTextLength = useRef(draft.text.length);
|
|
115
|
+
|
|
113
116
|
useEffect(() => {
|
|
114
117
|
const editor = editorRef.current;
|
|
118
|
+
|
|
115
119
|
if (!editor) return;
|
|
116
120
|
editor.cursorOffset = initialTextLength.current;
|
|
117
121
|
// the box is laid out now, so its width is known: size to the wrapped body
|
|
118
122
|
onRowsChange(composeRowCount(editor.plainText, editor.width));
|
|
119
123
|
}, [onRowsChange]);
|
|
124
|
+
|
|
120
125
|
return (
|
|
121
126
|
<>
|
|
122
127
|
<textarea
|
|
@@ -128,6 +133,7 @@ function DraftEditor({
|
|
|
128
133
|
{...(draft.onSubmit ? { onSubmit: draft.onSubmit } : {})}
|
|
129
134
|
onContentChange={() => {
|
|
130
135
|
const editor = editorRef.current;
|
|
136
|
+
|
|
131
137
|
if (!editor) return;
|
|
132
138
|
draft.onInput(editor.plainText);
|
|
133
139
|
onRowsChange(composeRowCount(editor.plainText, editor.width));
|
|
@@ -173,8 +179,10 @@ export function AnnotationCard({
|
|
|
173
179
|
// until the mounted textarea reports its measured width below.
|
|
174
180
|
composeRowCount(activeDraft?.text ?? "", 0),
|
|
175
181
|
);
|
|
182
|
+
|
|
176
183
|
if (draft) {
|
|
177
184
|
const verb = "comment on";
|
|
185
|
+
|
|
178
186
|
return (
|
|
179
187
|
<Card
|
|
180
188
|
title={` ${verb} "${truncateToSingleLine(quote, 40)}" `}
|
|
@@ -200,6 +208,7 @@ export function AnnotationCard({
|
|
|
200
208
|
const borderInset = 4;
|
|
201
209
|
// a bordered box must declare a height or it collapses: quote + body, or the editor
|
|
202
210
|
const contentRows = card.editing ? editorRowCount + 2 : 2;
|
|
211
|
+
|
|
203
212
|
return (
|
|
204
213
|
<box
|
|
205
214
|
id={id}
|
|
@@ -30,6 +30,7 @@ export function Breadcrumb({ items, theme }: BreadcrumbProps): React.ReactNode {
|
|
|
30
30
|
: tone === "text"
|
|
31
31
|
? tokens.text
|
|
32
32
|
: tokens.textDim;
|
|
33
|
+
|
|
33
34
|
return (
|
|
34
35
|
<box style={{ height: 1, backgroundColor: tokens.panel, paddingLeft: 1, flexDirection: "row" }}>
|
|
35
36
|
<text fg={tokens.text}>
|
|
@@ -43,6 +43,7 @@ export function CodeBlock({
|
|
|
43
43
|
}: CodeBlockProps): React.ReactNode {
|
|
44
44
|
const tokens = useComponentTheme(theme);
|
|
45
45
|
const tagColor = changeTag === "new" ? tokens.green : tokens.accent;
|
|
46
|
+
|
|
46
47
|
return (
|
|
47
48
|
<box id={id} style={{ flexDirection: "column", marginTop }}>
|
|
48
49
|
<text>
|
|
@@ -64,6 +64,7 @@ function VerdictSelector({
|
|
|
64
64
|
theme?: Theme;
|
|
65
65
|
}): React.ReactNode {
|
|
66
66
|
const tokens = useComponentTheme(theme);
|
|
67
|
+
|
|
67
68
|
return (
|
|
68
69
|
<box style={{ flexDirection: "row", height: 1 }}>
|
|
69
70
|
{VERDICTS.map((candidate) => (
|
|
@@ -94,6 +95,7 @@ export function ConfirmCard({
|
|
|
94
95
|
theme,
|
|
95
96
|
}: ConfirmCardProps): React.ReactNode {
|
|
96
97
|
const tokens = useComponentTheme(theme);
|
|
98
|
+
|
|
97
99
|
return (
|
|
98
100
|
<Card
|
|
99
101
|
title=" submit review "
|
|
@@ -36,7 +36,9 @@ export function ConfirmDialog({
|
|
|
36
36
|
}: ConfirmDialogProps): React.ReactNode {
|
|
37
37
|
const tokens = useComponentTheme(theme);
|
|
38
38
|
const { width: terminalWidth } = useTerminalDimensions();
|
|
39
|
+
|
|
39
40
|
if (!isOpen) return null;
|
|
41
|
+
|
|
40
42
|
return (
|
|
41
43
|
<Dialog isOpen title={title} width={Math.min(54, terminalWidth - 6)} height={7} theme={theme}>
|
|
42
44
|
<box
|
|
@@ -16,13 +16,16 @@ function findById(node: Renderable, id: string): Renderable | undefined {
|
|
|
16
16
|
if (node.id === id) return node;
|
|
17
17
|
for (const child of node.getChildren()) {
|
|
18
18
|
const found = findById(child, id);
|
|
19
|
+
|
|
19
20
|
if (found) return found;
|
|
20
21
|
}
|
|
22
|
+
|
|
21
23
|
return undefined;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
function hex(color: { toInts(): [number, number, number, number] }): string {
|
|
25
27
|
const [red, green, blue] = color.toInts();
|
|
28
|
+
|
|
26
29
|
return "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
27
30
|
}
|
|
28
31
|
|
|
@@ -32,6 +35,7 @@ function tallPatch(count: number): string {
|
|
|
32
35
|
{ length: count },
|
|
33
36
|
(_, index) => `+line ${String(index).padStart(2, "0")}`,
|
|
34
37
|
);
|
|
38
|
+
|
|
35
39
|
return [
|
|
36
40
|
"diff --git a/f.ts b/f.ts",
|
|
37
41
|
"--- a/f.ts",
|
|
@@ -51,9 +55,11 @@ function ScrollHarness({
|
|
|
51
55
|
annotations: Annotation[];
|
|
52
56
|
}): React.ReactNode {
|
|
53
57
|
const [cursor, setCursor] = useState(0);
|
|
58
|
+
|
|
54
59
|
useKeyboard((key) => {
|
|
55
60
|
if (key.name === "j") setCursor((current) => Math.min(rows.length - 1, current + 1));
|
|
56
61
|
});
|
|
62
|
+
|
|
57
63
|
return <DiffSheet rows={rows} cursor={cursor} annotations={annotations} />;
|
|
58
64
|
}
|
|
59
65
|
|
|
@@ -74,21 +80,25 @@ test("scrolling down past wrapped annotation cards keeps the cursor pinned and t
|
|
|
74
80
|
width: 60,
|
|
75
81
|
height: 12,
|
|
76
82
|
});
|
|
83
|
+
|
|
77
84
|
await setup.waitForVisualIdle();
|
|
78
85
|
const scrollbox = findById(setup.renderer.root, "diff-scroll") as ScrollBoxRenderable;
|
|
79
86
|
|
|
80
87
|
// the cursor row is the one painted with the cursor background
|
|
81
88
|
const cursorScreenRow = (): number => {
|
|
82
89
|
const lines = setup.captureSpans().lines;
|
|
90
|
+
|
|
83
91
|
for (let index = 0; index < lines.length; index++) {
|
|
84
92
|
if (lines[index]!.spans.some((span) => hex(span.bg) === DARK.cursorBackground)) return index;
|
|
85
93
|
}
|
|
94
|
+
|
|
86
95
|
return -1;
|
|
87
96
|
};
|
|
88
97
|
|
|
89
98
|
// Act - walk the cursor down through the whole diff, sampling the scroll each step
|
|
90
99
|
const scrollTops: number[] = [];
|
|
91
100
|
const screenRows: number[] = [];
|
|
101
|
+
|
|
92
102
|
for (let step = 0; step < rows.length; step++) {
|
|
93
103
|
scrollTops.push(scrollbox.scrollTop);
|
|
94
104
|
screenRows.push(cursorScreenRow());
|
|
@@ -111,10 +121,12 @@ test("scrolling down past wrapped annotation cards keeps the cursor pinned and t
|
|
|
111
121
|
const steadyRows = screenRows.filter(
|
|
112
122
|
(_, step) => scrollTops[step]! > 0 && scrollTops[step]! < maxScroll,
|
|
113
123
|
);
|
|
124
|
+
|
|
114
125
|
expect(steadyRows.length).toBeGreaterThan(0);
|
|
115
126
|
expect(new Set(steadyRows).size).toBe(1);
|
|
116
127
|
const pinnedRow = steadyRows[0]!;
|
|
117
128
|
const viewportBottom = scrollbox.height;
|
|
129
|
+
|
|
118
130
|
// the cursor sits a couple of rows inside the bottom edge, per the follow rule
|
|
119
131
|
expect(viewportBottom - pinnedRow).toBeGreaterThanOrEqual(2);
|
|
120
132
|
expect(viewportBottom - pinnedRow).toBeLessThanOrEqual(3);
|
|
@@ -19,6 +19,7 @@ const PATCH = `diff --git a/src/x.ts b/src/x.ts
|
|
|
19
19
|
|
|
20
20
|
function hex(color: RGBA): string {
|
|
21
21
|
const [red, green, blue] = color.toInts();
|
|
22
|
+
|
|
22
23
|
return "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -35,6 +36,7 @@ test("renders tree-sitter colors, with the changed word in the diff color on top
|
|
|
35
36
|
// Act
|
|
36
37
|
// syntax highlighting resolves off the render path; pump until a keyword paints
|
|
37
38
|
let keywordAccent = false;
|
|
39
|
+
|
|
38
40
|
for (let attempt = 0; attempt < 60 && !keywordAccent; attempt++) {
|
|
39
41
|
await setup.renderOnce();
|
|
40
42
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
@@ -49,6 +51,7 @@ test("renders tree-sitter colors, with the changed word in the diff color on top
|
|
|
49
51
|
expect(keywordAccent).toBe(true);
|
|
50
52
|
// the intra-line changed number keeps the diff color over its syntax color
|
|
51
53
|
const changedColors = new Set<string>();
|
|
54
|
+
|
|
52
55
|
for (const line of setup.captureSpans().lines) {
|
|
53
56
|
for (const span of line.spans) {
|
|
54
57
|
if (span.text.includes("250")) changedColors.add(hex(span.fg));
|
|
@@ -78,6 +78,7 @@ function rowStyle(
|
|
|
78
78
|
: isAnnotatedRow
|
|
79
79
|
? tokens.markCommentBackground
|
|
80
80
|
: undefined;
|
|
81
|
+
|
|
81
82
|
return { sign, baseColor, background };
|
|
82
83
|
}
|
|
83
84
|
|
|
@@ -104,27 +105,33 @@ function DiffChunk({
|
|
|
104
105
|
|
|
105
106
|
const lineNumbers = useMemo(() => {
|
|
106
107
|
const numbers = new Map<number, number>();
|
|
108
|
+
|
|
107
109
|
segment.rows.forEach((row, lineIndex) => {
|
|
108
110
|
const gutterNumber = row.kind === "del" ? row.oldLine : row.newLine;
|
|
111
|
+
|
|
109
112
|
if (gutterNumber !== undefined) numbers.set(lineIndex, gutterNumber);
|
|
110
113
|
});
|
|
114
|
+
|
|
111
115
|
return numbers;
|
|
112
116
|
}, [segment]);
|
|
113
117
|
|
|
114
118
|
const lineSigns = useMemo(() => {
|
|
115
119
|
const signs = new Map<number, LineSign>();
|
|
120
|
+
|
|
116
121
|
if (cursorInChunk >= 0 && cursorInChunk < segment.rows.length) {
|
|
117
122
|
signs.set(cursorInChunk, { before: "▎", beforeColor: tokens.accent });
|
|
118
123
|
}
|
|
119
124
|
if (segment.annotation) {
|
|
120
125
|
signs.set(segment.rows.length - 1, { after: "◆", afterColor: tokens.accent });
|
|
121
126
|
}
|
|
127
|
+
|
|
122
128
|
return signs;
|
|
123
129
|
}, [segment, cursorInChunk, tokens]);
|
|
124
130
|
|
|
125
131
|
const lineColors = useMemo(() => {
|
|
126
132
|
// the renderable's config parser requires the gutter key to be present
|
|
127
133
|
const colors = new Map<number, { gutter: string | undefined; content: string }>();
|
|
134
|
+
|
|
128
135
|
if (segment.annotation) {
|
|
129
136
|
colors.set(segment.rows.length - 1, {
|
|
130
137
|
gutter: undefined,
|
|
@@ -137,12 +144,14 @@ function DiffChunk({
|
|
|
137
144
|
content: tokens.cursorBackground,
|
|
138
145
|
});
|
|
139
146
|
}
|
|
147
|
+
|
|
140
148
|
return colors;
|
|
141
149
|
}, [segment, cursorInChunk, tokens]);
|
|
142
150
|
|
|
143
151
|
// the gutter renderable has no prop setters for these maps, so they land via an effect
|
|
144
152
|
useEffect(() => {
|
|
145
153
|
const gutter = gutterRef.current;
|
|
154
|
+
|
|
146
155
|
if (!gutter) return;
|
|
147
156
|
gutter.setLineNumbers(lineNumbers);
|
|
148
157
|
gutter.setLineSigns(lineSigns);
|
|
@@ -197,6 +206,7 @@ function DiffChunk({
|
|
|
197
206
|
baseColor,
|
|
198
207
|
tokens,
|
|
199
208
|
);
|
|
209
|
+
|
|
200
210
|
return (
|
|
201
211
|
<React.Fragment key={lineIndex}>
|
|
202
212
|
<span fg={baseColor} bg={background}>
|
|
@@ -236,15 +246,19 @@ function useSyntaxHighlights(rows: DiffRow[]): Map<number, SyntaxSpan[]> {
|
|
|
236
246
|
rows,
|
|
237
247
|
byRow: EMPTY_SYNTAX,
|
|
238
248
|
});
|
|
249
|
+
|
|
239
250
|
useEffect(() => {
|
|
240
251
|
let active = true;
|
|
252
|
+
|
|
241
253
|
void highlightDiffRows(rows).then((byRow) => {
|
|
242
254
|
if (active) setHighlighted({ rows, byRow });
|
|
243
255
|
});
|
|
256
|
+
|
|
244
257
|
return () => {
|
|
245
258
|
active = false;
|
|
246
259
|
};
|
|
247
260
|
}, [rows]);
|
|
261
|
+
|
|
248
262
|
return highlighted.rows === rows ? highlighted.byRow : EMPTY_SYNTAX;
|
|
249
263
|
}
|
|
250
264
|
|
|
@@ -276,8 +290,10 @@ export function DiffSheet({
|
|
|
276
290
|
useEffect(() => {
|
|
277
291
|
const scrollbox = scrollRef.current;
|
|
278
292
|
const cursorOffset = rowOffsets[cursor];
|
|
293
|
+
|
|
279
294
|
if (!scrollbox || cursorOffset === undefined) return;
|
|
280
295
|
const viewportHeight = Math.max(1, scrollbox.height);
|
|
296
|
+
|
|
281
297
|
if (cursorOffset < scrollbox.scrollTop + 2) {
|
|
282
298
|
scrollbox.scrollTo({ x: 0, y: Math.max(0, cursorOffset - 2) });
|
|
283
299
|
} else if (cursorOffset > scrollbox.scrollTop + viewportHeight - 3) {
|
|
@@ -301,6 +317,7 @@ export function DiffSheet({
|
|
|
301
317
|
{segments.map((segment, segmentIndex) => {
|
|
302
318
|
if (segment.kind === "header") {
|
|
303
319
|
const isCursor = segment.rowIndex === cursor;
|
|
320
|
+
|
|
304
321
|
if (segment.row.kind === "file") {
|
|
305
322
|
return (
|
|
306
323
|
<text
|
|
@@ -313,6 +330,7 @@ export function DiffSheet({
|
|
|
313
330
|
</text>
|
|
314
331
|
);
|
|
315
332
|
}
|
|
333
|
+
|
|
316
334
|
return (
|
|
317
335
|
<text
|
|
318
336
|
key={segmentIndex}
|
|
@@ -327,6 +345,7 @@ export function DiffSheet({
|
|
|
327
345
|
}
|
|
328
346
|
|
|
329
347
|
const lastRowIndex = segment.firstRowIndex + segment.rows.length - 1;
|
|
348
|
+
|
|
330
349
|
return (
|
|
331
350
|
<React.Fragment key={segmentIndex}>
|
|
332
351
|
<DiffChunk
|
|
@@ -27,6 +27,7 @@ export function InboxList({
|
|
|
27
27
|
theme,
|
|
28
28
|
}: InboxListProps): React.ReactNode {
|
|
29
29
|
const tokens = useComponentTheme(theme);
|
|
30
|
+
|
|
30
31
|
return (
|
|
31
32
|
<box style={{ flexGrow: 1, flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
|
|
32
33
|
{inbox.length === 0 ? (
|
|
@@ -35,6 +36,7 @@ export function InboxList({
|
|
|
35
36
|
inbox.map((session, index) => {
|
|
36
37
|
const selected = index === cursor;
|
|
37
38
|
const title = session.artifact.meta.title ?? session.id;
|
|
39
|
+
|
|
38
40
|
return (
|
|
39
41
|
<box
|
|
40
42
|
key={session.id}
|
|
@@ -17,6 +17,7 @@ export function KeybindsDialog({ sections, theme }: KeybindsDialogProps): React.
|
|
|
17
17
|
sections.reduce((sum, section) => sum + 1 + section.entries.length + 1, 0) + 1;
|
|
18
18
|
const width = Math.min(52, terminalWidth - 6);
|
|
19
19
|
const height = Math.min(terminalHeight - 4, contentRows + 3);
|
|
20
|
+
|
|
20
21
|
return (
|
|
21
22
|
<Dialog
|
|
22
23
|
isOpen
|
|
@@ -100,10 +100,12 @@ function positionOfRenderedOffset(
|
|
|
100
100
|
): { x: number; y: number } {
|
|
101
101
|
const info = renderable.lineInfo;
|
|
102
102
|
let lineIndex = 0;
|
|
103
|
+
|
|
103
104
|
for (let index = 0; index < info.lineStartCols.length; index++) {
|
|
104
105
|
if (info.lineStartCols[index]! <= renderedOffset) lineIndex = index;
|
|
105
106
|
else break;
|
|
106
107
|
}
|
|
108
|
+
|
|
107
109
|
return {
|
|
108
110
|
x: renderable.x + renderedOffset - (info.lineStartCols[lineIndex] ?? 0),
|
|
109
111
|
y: renderable.y + lineIndex,
|
|
@@ -135,37 +137,47 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
|
|
|
135
137
|
if (!renderer?.hasSelection) return null;
|
|
136
138
|
const readBlock = (displayIndex: number): PlanSelection | null => {
|
|
137
139
|
const blockRef = blockRefs.current.get(displayIndex);
|
|
140
|
+
|
|
138
141
|
if (!blockRef) return null;
|
|
139
142
|
const selection = blockRef.renderable.getSelection();
|
|
143
|
+
|
|
140
144
|
if (!selection || selection.end <= selection.start) return null;
|
|
141
145
|
const range = workRangeForRendered(blockRef.runs, selection.start, selection.end);
|
|
146
|
+
|
|
142
147
|
return range ? { displayIndex, ...range } : null;
|
|
143
148
|
};
|
|
149
|
+
|
|
144
150
|
// the block the mouse released on wins, so a fresh drag re-anchors the span
|
|
145
151
|
// (one marker at a time) instead of sticking to the topmost prior selection
|
|
146
152
|
if (preferredIndex !== undefined) {
|
|
147
153
|
const preferred = readBlock(preferredIndex);
|
|
154
|
+
|
|
148
155
|
if (preferred) return preferred;
|
|
149
156
|
}
|
|
150
157
|
for (const displayIndex of [...blockRefs.current.keys()].sort(
|
|
151
158
|
(left, right) => left - right,
|
|
152
159
|
)) {
|
|
153
160
|
const found = readBlock(displayIndex);
|
|
161
|
+
|
|
154
162
|
if (found) return found;
|
|
155
163
|
}
|
|
164
|
+
|
|
156
165
|
return null;
|
|
157
166
|
},
|
|
158
167
|
driveSpanSelection: (span: SpanState): void => {
|
|
159
168
|
if (!renderer) return;
|
|
160
169
|
const blockRef = blockRefs.current.get(span.displayIndex);
|
|
170
|
+
|
|
161
171
|
if (!blockRef) return;
|
|
162
172
|
// a span over an emphasized word starts/ends on concealed markers with
|
|
163
173
|
// no rendered cell; snap to the visible characters inside them
|
|
164
174
|
const renderedStart = renderedOffsetAtOrAfter(blockRef.runs, span.start);
|
|
165
175
|
const renderedEnd = renderedOffsetAtOrBefore(blockRef.runs, span.end - 1);
|
|
176
|
+
|
|
166
177
|
if (renderedStart === null || renderedEnd === null || renderedEnd < renderedStart) return;
|
|
167
178
|
const startPosition = positionOfRenderedOffset(blockRef.renderable, renderedStart);
|
|
168
179
|
const endPosition = positionOfRenderedOffset(blockRef.renderable, renderedEnd);
|
|
180
|
+
|
|
169
181
|
renderer.startSelection(blockRef.renderable, startPosition.x, startPosition.y);
|
|
170
182
|
renderer.updateSelection(blockRef.renderable, endPosition.x + 1, endPosition.y);
|
|
171
183
|
},
|
|
@@ -192,16 +204,20 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
|
|
|
192
204
|
// a terminal resize rewraps every block; re-run the anchor math against the
|
|
193
205
|
// new geometry instead of leaving the card at its pre-resize cell
|
|
194
206
|
const [resizeEpoch, setResizeEpoch] = useState(0);
|
|
207
|
+
|
|
195
208
|
useEffect(() => {
|
|
196
209
|
if (!renderer) return;
|
|
197
210
|
const onResize = (): void => setResizeEpoch((epoch) => epoch + 1);
|
|
211
|
+
|
|
198
212
|
renderer.on("resize", onResize);
|
|
213
|
+
|
|
199
214
|
return () => void renderer.off("resize", onResize);
|
|
200
215
|
}, [renderer]);
|
|
201
216
|
useEffect(() => {
|
|
202
217
|
if (!popover) return;
|
|
203
218
|
const blockRef = blockRefs.current.get(popover.displayIndex);
|
|
204
219
|
const scrollbox = scrollRef.current;
|
|
220
|
+
|
|
205
221
|
if (!blockRef || !scrollbox) return;
|
|
206
222
|
const renderedStart =
|
|
207
223
|
activeSpan && activeSpan.displayIndex === popover.displayIndex
|
|
@@ -213,9 +229,11 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
|
|
|
213
229
|
const anchorTop = startPosition.y - content.y;
|
|
214
230
|
// clamp so the toolbar card never runs past the content's right edge
|
|
215
231
|
const maxLeft = Math.max(0, content.width - POPOVER_TOOLBAR_COLUMNS);
|
|
232
|
+
|
|
216
233
|
setPopoverLeft(Math.min(anchorLeft, maxLeft));
|
|
217
234
|
// only the toolbar (3 rows) + gap must fit above; the dropdown flows down
|
|
218
235
|
const lineViewportRow = startPosition.y - scrollbox.y;
|
|
236
|
+
|
|
219
237
|
setPopoverTop(anchorTop + (lineViewportRow < POPOVER_ROWS_ABOVE ? 1 : -POPOVER_ROWS_ABOVE));
|
|
220
238
|
}, [popover, activeSpan, display, cursor, resizeEpoch]);
|
|
221
239
|
|
|
@@ -227,6 +245,7 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
|
|
|
227
245
|
if (renderable) blockRefs.current.set(displayIndex, { renderable, runs });
|
|
228
246
|
else blockRefs.current.delete(displayIndex);
|
|
229
247
|
};
|
|
248
|
+
|
|
230
249
|
// stale refs must not survive a shrinking display list
|
|
231
250
|
useEffect(() => {
|
|
232
251
|
for (const displayIndex of blockRefs.current.keys()) {
|
|
@@ -235,10 +254,12 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
|
|
|
235
254
|
}, [display]);
|
|
236
255
|
|
|
237
256
|
const children: React.ReactNode[] = [];
|
|
257
|
+
|
|
238
258
|
for (let displayIndex = 0; displayIndex < display.length; displayIndex++) {
|
|
239
259
|
const block = display[displayIndex]!;
|
|
240
260
|
const isCursor = displayIndex === cursor;
|
|
241
261
|
const gap = topGap(display[displayIndex - 1], block);
|
|
262
|
+
|
|
242
263
|
if (block.kind === "code") {
|
|
243
264
|
children.push(
|
|
244
265
|
<CodeBlock
|
|
@@ -256,6 +277,7 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
|
|
|
256
277
|
);
|
|
257
278
|
} else {
|
|
258
279
|
const blockMarks = [...(marks.get(displayIndex) ?? [])];
|
|
280
|
+
|
|
259
281
|
if (activeSpan && activeSpan.displayIndex === displayIndex) {
|
|
260
282
|
blockMarks.push({ start: activeSpan.start, end: activeSpan.end, role: "kspan" });
|
|
261
283
|
}
|
|
@@ -265,6 +287,7 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
|
|
|
265
287
|
const mappedRuns: StyleRun[] = showsChangeTag(block)
|
|
266
288
|
? [...runs, { text: ` [${tagLabel(block)}]`, role: "plain", start: null }]
|
|
267
289
|
: runs;
|
|
290
|
+
|
|
268
291
|
children.push(
|
|
269
292
|
<box
|
|
270
293
|
key={displayIndex}
|
|
@@ -288,6 +311,7 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
|
|
|
288
311
|
>
|
|
289
312
|
{runs.map((run, runIndex) => {
|
|
290
313
|
const url = run.role === "link" ? safeLinkHref(run.href) : undefined;
|
|
314
|
+
|
|
291
315
|
return (
|
|
292
316
|
<span
|
|
293
317
|
key={runIndex}
|
|
@@ -372,6 +396,7 @@ function topGap(previous: DisplayBlock | undefined, current: DisplayBlock): numb
|
|
|
372
396
|
const tightPair =
|
|
373
397
|
(current.kind === "li" && previous.kind === "li") ||
|
|
374
398
|
(current.kind === "oli" && previous.kind === "oli");
|
|
399
|
+
|
|
375
400
|
return tightPair ? 0 : 1;
|
|
376
401
|
}
|
|
377
402
|
|
|
@@ -379,6 +404,7 @@ function marker(block: DisplayBlock): string {
|
|
|
379
404
|
if (block.kind === "li") return "- ";
|
|
380
405
|
if (block.kind === "oli") return `${block.orderedItemNumber ?? 1}. `;
|
|
381
406
|
if (block.kind === "quote") return "▏ ";
|
|
407
|
+
|
|
382
408
|
return "";
|
|
383
409
|
}
|
|
384
410
|
|
|
@@ -37,12 +37,14 @@ export function PromptDialog({
|
|
|
37
37
|
const tokens = useComponentTheme(theme);
|
|
38
38
|
const { width: terminalWidth } = useTerminalDimensions();
|
|
39
39
|
const inputRef = useRef<TextareaRenderable | null>(null);
|
|
40
|
+
|
|
40
41
|
useEffect(() => {
|
|
41
42
|
// open with the caret after the seeded value, like a text field
|
|
42
43
|
if (inputRef.current) inputRef.current.cursorOffset = value.length;
|
|
43
44
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
44
45
|
}, []);
|
|
45
46
|
if (!isOpen) return null;
|
|
47
|
+
|
|
46
48
|
return (
|
|
47
49
|
<Dialog isOpen title={title} width={Math.min(54, terminalWidth - 6)} height={7} theme={theme}>
|
|
48
50
|
<box
|