@cueloop/client 0.1.0-alpha.102 → 0.1.0-alpha.103
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/annotations/annotation-palette.ts +1 -0
- package/src/annotations/components/AnnotationCards.tsx +4 -0
- package/src/annotations/components/DiscussionMarkerRail.tsx +3 -0
- package/src/annotations/discussions.ts +1 -0
- package/src/annotations/thread-selection.ts +6 -0
- package/src/annotations/use-annotation-surface.tsx +97 -0
- package/src/app/App.deleted-scroll.test.tsx +3 -0
- package/src/app/App.harness-settings.test.tsx +77 -0
- package/src/app/App.plan-render.test.tsx +2 -0
- package/src/app/App.plan-review.test.tsx +1 -0
- package/src/app/App.test.tsx +1 -0
- package/src/app/App.tsx +67 -1
- package/src/app/App.visual.test.tsx +1 -0
- package/src/app/App.walk.test.tsx +1 -0
- package/src/app/App.workbench.test.tsx +1 -0
- package/src/app/app-screens.tsx +1 -0
- package/src/app/app-view-model.ts +7 -0
- package/src/app/components/WorkspacePanels.tsx +1 -0
- package/src/app/intent-dispatch.test.ts +1 -0
- package/src/app/intent-dispatch.ts +26 -0
- package/src/app/ready-signal.ts +2 -0
- package/src/app/run.ts +2 -0
- package/src/app/serve.test.ts +4 -0
- package/src/diff/components/AnnotatableFileView.tsx +1 -0
- package/src/diff/components/DiffContentView.scroll.test.tsx +9 -0
- package/src/diff/components/DiffContentView.syntax.test.tsx +1 -0
- package/src/diff/components/DiffContentView.tsx +25 -0
- package/src/diff/components/diff-content-view-layout.ts +3 -0
- package/src/diff/diff-fold.ts +4 -0
- package/src/diff/diff-hunk-curate.ts +3 -0
- package/src/diff/diff-intraline.ts +6 -0
- package/src/diff/diff-syntax.ts +8 -0
- package/src/diff/split-diff.ts +6 -0
- package/src/diff/view-diff.ts +18 -0
- package/src/diff/walk.ts +3 -0
- package/src/integrations/attribution.ts +2 -0
- package/src/integrations/client-extension-registry.ts +4 -0
- package/src/integrations/clipboard.ts +1 -0
- package/src/integrations/components/ShareDialog.agent.test.tsx +2 -0
- package/src/integrations/components/ShareDialog.tsx +28 -0
- package/src/integrations/editor.ts +3 -0
- package/src/integrations/github-identity.test.ts +1 -0
- package/src/integrations/github-identity.ts +1 -0
- package/src/integrations/herdr.ts +2 -0
- package/src/integrations/pasted-image.ts +2 -0
- package/src/integrations/share.ts +4 -0
- package/src/keyboard/components/HandleChipsInput.tsx +4 -0
- package/src/keyboard/components/SlashComposer.tsx +3 -0
- package/src/keyboard/key-bindings.ts +3 -0
- package/src/keyboard/keymap.ts +51 -0
- package/src/keyboard/skills.ts +2 -0
- package/src/keyboard/slash-palette.ts +8 -0
- package/src/keyboard/thread-chords.ts +5 -0
- package/src/keyboard/use-shared-keyboard.ts +1 -0
- package/src/markdown/components/MarkdownThreadEditor.tsx +3 -0
- package/src/markdown/components/ThreadView.selection.test.tsx +3 -0
- package/src/markdown/components/ThreadView.tsx +13 -0
- package/src/markdown/mark-runs.ts +4 -0
- package/src/markdown/markdown-editor-syntax-style.ts +1 -0
- package/src/markdown/markdown-grid.ts +6 -0
- package/src/markdown/markdown-highlight.ts +1 -0
- package/src/markdown/thread-code-syntax.ts +3 -0
- package/src/markdown/view-plan.ts +52 -0
- package/src/performance/perf-report.ts +1 -0
- package/src/performance/perf-timings.ts +1 -0
- package/src/prototype/components/PrototypeContentView.tsx +17 -0
- package/src/prototype/prototype-browser.ts +11 -0
- package/src/settings/components/SettingsDialog.tsx +3 -0
- package/src/settings/config.test.ts +41 -0
- package/src/settings/config.ts +44 -0
- package/src/settings/use-settings-dialog.test.tsx +98 -0
- package/src/settings/use-settings-dialog.tsx +38 -1
- package/src/settings/use-thread-harness-setting.test.tsx +87 -0
- package/src/settings/use-thread-harness-setting.ts +86 -0
- package/src/stories/stories.test.tsx +4 -0
- package/src/stories/story.ts +6 -0
- package/src/terminal/ansi-screen.ts +5 -0
- package/src/terminal/components/terminal-pane.ts +12 -0
- package/src/terminal/ghostty-terminal.ts +10 -0
- package/src/terminal/kitty-image.ts +3 -0
- package/src/terminal/pty.ts +9 -0
- package/src/testing/test-support.test.ts +2 -0
- package/src/testing/test-support.ts +16 -0
- package/src/thread/agent-continuation.test.ts +2 -0
- package/src/thread/agent-transcript.ts +7 -0
- package/src/thread/components/AgentConfigControls.tsx +1 -0
- package/src/thread/components/AgentThreadPane.test.tsx +31 -0
- package/src/thread/components/AgentThreadPane.tsx +12 -0
- package/src/thread/components/ThreadTree.tsx +1 -0
- package/src/thread/components/thread-tree.test.ts +1 -0
- package/src/thread/thread-controller.curate.test.ts +2 -0
- package/src/thread/thread-controller.ts +123 -0
- package/src/thread/tree-view.ts +4 -0
- package/src/thread/use-thread-agent.test.tsx +1 -0
- package/src/thread/use-thread-agent.ts +5 -0
- package/src/ui/components/OverlayScrollbar.test.tsx +3 -0
- package/src/ui/components/OverlayScrollbar.tsx +5 -0
- package/src/ui/components/PromptDialog.tsx +2 -0
- package/src/ui/components/RootOverlay.tsx +1 -0
- package/src/ui/components/ScrollArea.tsx +2 -0
- package/src/ui/components/primitives/Button.tsx +1 -0
- package/src/ui/components/primitives/Tabs.tsx +1 -0
- package/src/ui/components/primitives/Tree.tsx +4 -0
- package/src/ui/components/primitives/tree-model.ts +7 -0
- package/src/ui/components/syntax-highlight.ts +5 -0
- package/src/ui/components/truncate-title.ts +1 -0
- package/src/ui/use-frame-measure.ts +3 -0
- package/src/ui/use-terminal-virtualizer.ts +6 -0
- package/src/workbench/components/ChangesColumn.tsx +7 -0
- package/src/workbench/components/ChangesFileTree.test.tsx +3 -0
- package/src/workbench/components/EditorGrid.tsx +8 -0
- package/src/workbench/components/FileContentsView.tsx +2 -0
- package/src/workbench/components/GridTabContent.tsx +2 -0
- package/src/workbench/components/ProjectTreeView.test.tsx +3 -0
- package/src/workbench/components/ProjectTreeView.tsx +8 -0
- package/src/workbench/components/TreePane.tsx +2 -0
- package/src/workbench/components/editor-grid.ts +3 -0
- package/src/workbench/components/file-tree.ts +2 -0
- package/src/workbench/use-changes-workbench.ts +5 -0
- package/src/workbench/use-remember-layout.ts +2 -0
- package/src/workbench/use-repo-changes.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cueloop/client",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.103",
|
|
4
4
|
"description": "The cueloop review UI: projection renderer, keyboard grammar, review rail",
|
|
5
5
|
"homepage": "https://github.com/mmurakaru/cueloop#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"access": "public"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@cueloop/daemon": "0.1.0-alpha.
|
|
31
|
-
"@cueloop/extension-api": "0.1.0-alpha.
|
|
32
|
-
"@cueloop/integration-obsidian": "0.1.0-alpha.
|
|
33
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
30
|
+
"@cueloop/daemon": "0.1.0-alpha.103",
|
|
31
|
+
"@cueloop/extension-api": "0.1.0-alpha.103",
|
|
32
|
+
"@cueloop/integration-obsidian": "0.1.0-alpha.103",
|
|
33
|
+
"@cueloop/schema": "0.1.0-alpha.103",
|
|
34
34
|
"@opentui/core": "^0.5.14",
|
|
35
35
|
"@opentui/keymap": "0.5.14",
|
|
36
36
|
"@opentui/react": "^0.5.14",
|
|
@@ -79,6 +79,7 @@ export function Composer({
|
|
|
79
79
|
const editor = editorRef.current;
|
|
80
80
|
|
|
81
81
|
if (!editor || !editor.focused || !looksLikeBinaryPaste(event.bytes)) return;
|
|
82
|
+
|
|
82
83
|
event.preventDefault();
|
|
83
84
|
pastedImageCount.current += 1;
|
|
84
85
|
editor.editBuffer.insertText(imagePlaceholder(pastedImageCount.current));
|
|
@@ -100,6 +101,7 @@ export function Composer({
|
|
|
100
101
|
const editor = editorRef.current;
|
|
101
102
|
|
|
102
103
|
if (!editor) return;
|
|
104
|
+
|
|
103
105
|
editor.cursorOffset = seed.length;
|
|
104
106
|
editor.editBuffer.setSyntaxStyle(referenceStyleFor(tokens).style);
|
|
105
107
|
paintReferences(editor);
|
|
@@ -133,6 +135,7 @@ export function Composer({
|
|
|
133
135
|
const editor = editorRef.current;
|
|
134
136
|
|
|
135
137
|
if (!editor) return;
|
|
138
|
+
|
|
136
139
|
paintReferences(editor);
|
|
137
140
|
setRows(composeRowCount(editor.plainText, editor.width));
|
|
138
141
|
onInput(editor.plainText, editor.cursorOffset);
|
|
@@ -343,6 +346,7 @@ export function CommentRow({
|
|
|
343
346
|
style={{ flexDirection: "column" }}
|
|
344
347
|
onMouseDown={(event) => {
|
|
345
348
|
if (!onFocus) return;
|
|
349
|
+
|
|
346
350
|
event.stopPropagation();
|
|
347
351
|
onFocus();
|
|
348
352
|
}}
|
|
@@ -56,6 +56,7 @@ function ScrollMarkers({
|
|
|
56
56
|
const isActive = (index: number): boolean => index === hoveredIndex || index === focusedIndex;
|
|
57
57
|
const colorFor = (index: number): string => {
|
|
58
58
|
if (isActive(index)) return tokens.text;
|
|
59
|
+
|
|
59
60
|
if (hoveredIndex >= 0 && discussions.length >= 5 && Math.abs(index - hoveredIndex) === 1) {
|
|
60
61
|
return tokens.textMuted;
|
|
61
62
|
}
|
|
@@ -126,9 +127,11 @@ export function DiscussionMarkerRail({
|
|
|
126
127
|
|
|
127
128
|
const preview = (): React.ReactNode => {
|
|
128
129
|
if (hovered === null) return null;
|
|
130
|
+
|
|
129
131
|
const discussion = discussions.find((candidate) => candidate.key === hovered.key);
|
|
130
132
|
|
|
131
133
|
if (!discussion) return null;
|
|
134
|
+
|
|
132
135
|
const quote = spanQuote(discussion.span);
|
|
133
136
|
const lastComment = discussion.annotations.at(-1)!;
|
|
134
137
|
|
|
@@ -34,6 +34,7 @@ export function discussionsFrom(session: Thread, marks: Map<number, Mark[]>): Di
|
|
|
34
34
|
for (const [displayIndex, blockMarks] of marks) {
|
|
35
35
|
for (const mark of blockMarks) {
|
|
36
36
|
if (!mark.annotationId || spanOf.has(mark.annotationId)) continue;
|
|
37
|
+
|
|
37
38
|
spanOf.set(
|
|
38
39
|
mark.annotationId,
|
|
39
40
|
mark.span ?? {
|
|
@@ -69,6 +69,7 @@ export function wordIndexAt(text: string, char: number): number {
|
|
|
69
69
|
|
|
70
70
|
for (let index = 0; index < words.length; index++) {
|
|
71
71
|
if (char < words[index]!.start) return Math.max(0, index - 1);
|
|
72
|
+
|
|
72
73
|
if (char < words[index]!.end) return index;
|
|
73
74
|
}
|
|
74
75
|
|
|
@@ -91,6 +92,7 @@ export function wordRangeAt(text: string, char: number): CharRange | null {
|
|
|
91
92
|
*/
|
|
92
93
|
export function positionAt(lines: LineGeometry[], x: number, y: number): TextPosition | null {
|
|
93
94
|
if (lines.length === 0) return null;
|
|
95
|
+
|
|
94
96
|
const ordered = lines.toSorted((left, right) => left.y - right.y || left.x - right.x);
|
|
95
97
|
const onRow = ordered.find((line) => line.y === y);
|
|
96
98
|
|
|
@@ -99,9 +101,11 @@ export function positionAt(lines: LineGeometry[], x: number, y: number): TextPos
|
|
|
99
101
|
|
|
100
102
|
return { blockIndex: onRow.blockIndex, char: onRow.start + column };
|
|
101
103
|
}
|
|
104
|
+
|
|
102
105
|
const above = ordered.findLast((line) => line.y < y);
|
|
103
106
|
|
|
104
107
|
if (above) return { blockIndex: above.blockIndex, char: above.end };
|
|
108
|
+
|
|
105
109
|
const first = ordered[0]!;
|
|
106
110
|
|
|
107
111
|
return { blockIndex: first.blockIndex, char: first.start };
|
|
@@ -156,6 +160,7 @@ export function spanRangeInBlock(
|
|
|
156
160
|
textLength: number,
|
|
157
161
|
): CharRange | null {
|
|
158
162
|
if (blockIndex < span.start.blockIndex || blockIndex > span.end.blockIndex) return null;
|
|
163
|
+
|
|
159
164
|
const start = blockIndex === span.start.blockIndex ? span.start.char : 0;
|
|
160
165
|
const end = blockIndex === span.end.blockIndex ? Math.min(span.end.char, textLength) : textLength;
|
|
161
166
|
|
|
@@ -189,6 +194,7 @@ export function snapSpanToWords(span: TextSpan, textOf: (blockIndex: number) =>
|
|
|
189
194
|
end: { blockIndex: span.end.blockIndex, char: snapped.end },
|
|
190
195
|
};
|
|
191
196
|
}
|
|
197
|
+
|
|
192
198
|
const startText = textOf(span.start.blockIndex);
|
|
193
199
|
const endText = textOf(span.end.blockIndex);
|
|
194
200
|
|
|
@@ -176,6 +176,7 @@ function firstAnnotatable(source: LineSource): number {
|
|
|
176
176
|
/** The visual-row direction requested by an arrow or readline-style caret key. */
|
|
177
177
|
function verticalCaretDelta(key: KeyEvent): -1 | 0 | 1 {
|
|
178
178
|
if (key.name === "up" || (key.ctrl && key.name === "p")) return -1;
|
|
179
|
+
|
|
179
180
|
if (key.name === "down" || (key.ctrl && key.name === "n")) return 1;
|
|
180
181
|
|
|
181
182
|
return 0;
|
|
@@ -228,6 +229,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
228
229
|
|
|
229
230
|
useEffect(() => {
|
|
230
231
|
if (requestedBlock === undefined) return;
|
|
232
|
+
|
|
231
233
|
const next = { blockIndex: requestedBlock.blockIndex, char: 0 };
|
|
232
234
|
|
|
233
235
|
setCursor(requestedBlock.blockIndex);
|
|
@@ -259,7 +261,9 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
259
261
|
const { blockIndex } = caret.head;
|
|
260
262
|
|
|
261
263
|
if (source.count === 0) return;
|
|
264
|
+
|
|
262
265
|
if (blockIndex < source.count && source.annotatable(blockIndex)) return;
|
|
266
|
+
|
|
263
267
|
const clamped = Math.min(blockIndex, source.count - 1);
|
|
264
268
|
const next = source.annotatable(clamped)
|
|
265
269
|
? clamped
|
|
@@ -290,6 +294,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
290
294
|
const focusedDiscussion = getFocusedDiscussion();
|
|
291
295
|
const setFocusedDiscussion = (key: string | null): void => {
|
|
292
296
|
if (onFocusAnnotation === undefined) return setLocalFocus(key === null ? null : { key });
|
|
297
|
+
|
|
293
298
|
onFocusAnnotation(discussions.find((discussion) => discussion.key === key)?.rootId);
|
|
294
299
|
};
|
|
295
300
|
const focusSavedComment = (annotationId: string, key: string): void => {
|
|
@@ -339,7 +344,9 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
339
344
|
/** Agent comments require a held selection; legacy surfaces may use the caret word. */
|
|
340
345
|
const caretSpan = (): TextSpan | null => {
|
|
341
346
|
if (heldSpan) return heldSpan;
|
|
347
|
+
|
|
342
348
|
if (onInvoke) return isPromptBlock?.(head.blockIndex) ? { start: head, end: head } : null;
|
|
349
|
+
|
|
343
350
|
const word = wordRangeAt(blockText(head.blockIndex), head.char);
|
|
344
351
|
|
|
345
352
|
return word
|
|
@@ -389,15 +396,18 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
389
396
|
return (delta, top, bottom) => {
|
|
390
397
|
if (delta === 0 || oldHeadY === undefined || compose || focusedDiscussion || caretIsSelection)
|
|
391
398
|
return;
|
|
399
|
+
|
|
392
400
|
const shiftedHeadY = oldHeadY - delta;
|
|
393
401
|
|
|
394
402
|
if (shiftedHeadY >= top && shiftedHeadY <= bottom) return;
|
|
403
|
+
|
|
395
404
|
const visible = lines
|
|
396
405
|
.filter((line) => line.y - delta >= top && line.y - delta <= bottom)
|
|
397
406
|
.toSorted((left, right) => left.y - right.y || left.x - right.x);
|
|
398
407
|
const target = delta < 0 ? visible.at(-1) : visible[0];
|
|
399
408
|
|
|
400
409
|
if (!target) return;
|
|
410
|
+
|
|
401
411
|
const next = { blockIndex: target.blockIndex, char: target.start };
|
|
402
412
|
|
|
403
413
|
flushSync(() => {
|
|
@@ -424,6 +434,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
424
434
|
const target = currentIndex === -1 ? undefined : lines[currentIndex + direction];
|
|
425
435
|
|
|
426
436
|
if (!target) return null;
|
|
437
|
+
|
|
427
438
|
const current = lines[currentIndex]!;
|
|
428
439
|
const column = Math.max(0, position.char - current.start);
|
|
429
440
|
|
|
@@ -439,8 +450,11 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
439
450
|
const openCompose = (state: ComposeState): void => {
|
|
440
451
|
// a view-only surface (a non-diff thread's live diff) never opens a draft
|
|
441
452
|
if (!commentsEnabled) return;
|
|
453
|
+
|
|
442
454
|
if (observer) return onObserverBlocked?.("observer");
|
|
455
|
+
|
|
443
456
|
if (resolved) return onObserverBlocked?.("resolved");
|
|
457
|
+
|
|
444
458
|
composerReady.current = false;
|
|
445
459
|
readComposerText.current = null;
|
|
446
460
|
composeRef.current = state;
|
|
@@ -476,16 +490,22 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
476
490
|
const prompt = target?.kind === "prompt";
|
|
477
491
|
|
|
478
492
|
if (prompt && !invoke) return;
|
|
493
|
+
|
|
479
494
|
closeCompose();
|
|
480
495
|
collapseCaret();
|
|
496
|
+
|
|
481
497
|
if (!target || body.trim().length === 0) return;
|
|
498
|
+
|
|
482
499
|
if (target.editAnnotationId !== null) {
|
|
483
500
|
onUpdateAnnotation(target.editAnnotationId, body);
|
|
501
|
+
|
|
484
502
|
if (target.discussionKey) focusSavedComment(target.editAnnotationId, target.discussionKey);
|
|
503
|
+
|
|
485
504
|
if (invoke) onInvoke?.(target.editAnnotationId);
|
|
486
505
|
|
|
487
506
|
return;
|
|
488
507
|
}
|
|
508
|
+
|
|
489
509
|
if (target.discussionKey !== null) {
|
|
490
510
|
const discussion = discussions.find((candidate) => candidate.key === target.discussionKey);
|
|
491
511
|
|
|
@@ -493,11 +513,13 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
493
513
|
const commentId = onReply(discussion.rootId, body);
|
|
494
514
|
|
|
495
515
|
if (commentId) focusSavedComment(commentId, discussion.key);
|
|
516
|
+
|
|
496
517
|
if (invoke && commentId) onInvoke?.(commentId);
|
|
497
518
|
|
|
498
519
|
return;
|
|
499
520
|
}
|
|
500
521
|
}
|
|
522
|
+
|
|
501
523
|
const span = target.span ?? caretSpan();
|
|
502
524
|
|
|
503
525
|
if (span) {
|
|
@@ -515,6 +537,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
515
537
|
if (!prompt && commentId) {
|
|
516
538
|
focusSavedComment(commentId, spanKey(span));
|
|
517
539
|
}
|
|
540
|
+
|
|
518
541
|
if (invoke && (prompt || commentId)) onInvoke?.(commentId || undefined);
|
|
519
542
|
};
|
|
520
543
|
/** A new discussion on the typing anchor; the card renders under the span's last block. */
|
|
@@ -526,6 +549,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
526
549
|
agentInputTarget(Boolean(isPromptBlock?.(head.blockIndex)), Boolean(heldSpan)) === "none"
|
|
527
550
|
)
|
|
528
551
|
return;
|
|
552
|
+
|
|
529
553
|
if (isPromptBlock?.(head.blockIndex) && span) {
|
|
530
554
|
openCompose({
|
|
531
555
|
kind: "prompt",
|
|
@@ -545,6 +569,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
545
569
|
editAnnotationId: null,
|
|
546
570
|
});
|
|
547
571
|
}
|
|
572
|
+
|
|
548
573
|
if (isPromptBlock?.(head.blockIndex)) promptDraft.current = "";
|
|
549
574
|
};
|
|
550
575
|
|
|
@@ -554,14 +579,17 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
554
579
|
const target = composeRef.current;
|
|
555
580
|
|
|
556
581
|
if (!target) return;
|
|
582
|
+
|
|
557
583
|
if (target.kind === "prompt") {
|
|
558
584
|
promptDraft.current = composeTextRef.current;
|
|
559
585
|
|
|
560
586
|
return closeCompose();
|
|
561
587
|
}
|
|
588
|
+
|
|
562
589
|
if (isStandaloneSlashQuery(composeText) || composeText.trim().length === 0) {
|
|
563
590
|
return closeCompose();
|
|
564
591
|
}
|
|
592
|
+
|
|
565
593
|
saveComment(composeText, false);
|
|
566
594
|
};
|
|
567
595
|
|
|
@@ -570,6 +598,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
570
598
|
const drag = dragging.current;
|
|
571
599
|
|
|
572
600
|
if (!drag) return;
|
|
601
|
+
|
|
573
602
|
let next = { head: pointer, anchor: drag.anchor };
|
|
574
603
|
|
|
575
604
|
if (drag.wordMode) {
|
|
@@ -584,7 +613,9 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
584
613
|
: { anchor: snapped.end, head: snapped.start };
|
|
585
614
|
}
|
|
586
615
|
}
|
|
616
|
+
|
|
587
617
|
if (comparePositions(next.head, drag.head) === 0) return;
|
|
618
|
+
|
|
588
619
|
drag.head = next.head;
|
|
589
620
|
setCursor(next.head.blockIndex);
|
|
590
621
|
setCaret({ head: next.head, anchor: next.anchor });
|
|
@@ -597,19 +628,23 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
597
628
|
// on screen - the mark follows the pointer across blocks.
|
|
598
629
|
const handleRootDrag = (event: TerminalMouseEvent): void => {
|
|
599
630
|
if (!dragging.current) return;
|
|
631
|
+
|
|
600
632
|
dragPointer.current = { x: event.x, y: event.y };
|
|
601
633
|
const position = positionAt(allGeometry(), event.x, event.y);
|
|
602
634
|
|
|
603
635
|
if (position) extendSelectionTo(position);
|
|
636
|
+
|
|
604
637
|
if (edgeScrollTimer.current === null) {
|
|
605
638
|
edgeScrollTimer.current = setInterval(() => {
|
|
606
639
|
const pointer = dragPointer.current;
|
|
607
640
|
const viewport = dragViewportRef.current?.();
|
|
608
641
|
|
|
609
642
|
if (!dragging.current || !pointer || !viewport) return;
|
|
643
|
+
|
|
610
644
|
const direction = pointer.y <= viewport.top ? -1 : pointer.y >= viewport.bottom ? 1 : 0;
|
|
611
645
|
|
|
612
646
|
if (direction === 0 || !viewport.scrollBy(direction)) return;
|
|
647
|
+
|
|
613
648
|
const next = positionAt(allGeometry(), pointer.x, pointer.y);
|
|
614
649
|
|
|
615
650
|
if (next) extendSelectionTo(next);
|
|
@@ -619,7 +654,9 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
619
654
|
const endDrag = (): void => {
|
|
620
655
|
dragging.current = null;
|
|
621
656
|
dragPointer.current = null;
|
|
657
|
+
|
|
622
658
|
if (edgeScrollTimer.current !== null) clearInterval(edgeScrollTimer.current);
|
|
659
|
+
|
|
623
660
|
edgeScrollTimer.current = null;
|
|
624
661
|
};
|
|
625
662
|
|
|
@@ -629,6 +666,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
629
666
|
const target = discussions.find((discussion) => discussion.key === key);
|
|
630
667
|
|
|
631
668
|
if (!target) return;
|
|
669
|
+
|
|
632
670
|
blurSaveCompose();
|
|
633
671
|
setCursor(target.blockIndex);
|
|
634
672
|
setFocusedDiscussion(key);
|
|
@@ -638,6 +676,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
638
676
|
|
|
639
677
|
const handleSlashKey = (key: KeyEvent, activeCompose: ComposeState): boolean => {
|
|
640
678
|
if (!slashActive || slashItems.length === 0) return false;
|
|
679
|
+
|
|
641
680
|
const selected = Math.min(slashIndex, slashItems.length - 1);
|
|
642
681
|
|
|
643
682
|
if (key.name === "up") {
|
|
@@ -645,11 +684,13 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
645
684
|
|
|
646
685
|
return true;
|
|
647
686
|
}
|
|
687
|
+
|
|
648
688
|
if (key.name === "down") {
|
|
649
689
|
setSlashIndex(Math.min(slashItems.length - 1, selected + 1));
|
|
650
690
|
|
|
651
691
|
return true;
|
|
652
692
|
}
|
|
693
|
+
|
|
653
694
|
if (key.name === "return" || key.name === "tab") {
|
|
654
695
|
const inserted = insertSlashItem(composeText, caretOffset, slashItems[selected]!.name);
|
|
655
696
|
|
|
@@ -666,8 +707,10 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
666
707
|
const handlePremountKey = (key: KeyEvent, activeCompose: ComposeState): void => {
|
|
667
708
|
if (key.name === "return") {
|
|
668
709
|
if (key.super && onInvoke) return;
|
|
710
|
+
|
|
669
711
|
if (key.meta || key.ctrl || key.super)
|
|
670
712
|
return saveComment(activeCompose.seed, isAgentInvokeKey(key, onInvoke));
|
|
713
|
+
|
|
671
714
|
const grown = { ...activeCompose, seed: `${activeCompose.seed}\n` };
|
|
672
715
|
|
|
673
716
|
composeRef.current = grown;
|
|
@@ -676,6 +719,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
676
719
|
|
|
677
720
|
return setCompose(grown);
|
|
678
721
|
}
|
|
722
|
+
|
|
679
723
|
const sequence = printableSequence(key);
|
|
680
724
|
|
|
681
725
|
if (sequence) {
|
|
@@ -694,11 +738,13 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
694
738
|
|
|
695
739
|
return;
|
|
696
740
|
}
|
|
741
|
+
|
|
697
742
|
if (isAgentInvokeKey(key, onInvoke)) {
|
|
698
743
|
key.preventDefault();
|
|
699
744
|
|
|
700
745
|
return saveComment(readComposerText.current?.() ?? activeCompose.seed, true);
|
|
701
746
|
}
|
|
747
|
+
|
|
702
748
|
// the textarea owns every key while open; the view takes dismiss (which
|
|
703
749
|
// also releases the discussion focus), the slash palette, and pre-mount input
|
|
704
750
|
if (key.name === "escape") {
|
|
@@ -706,6 +752,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
706
752
|
|
|
707
753
|
return closeCompose();
|
|
708
754
|
}
|
|
755
|
+
|
|
709
756
|
// backspace on an already empty draft undoes it: the card dissolves
|
|
710
757
|
// and the caret sits back on the still-held selection, ready to re-type
|
|
711
758
|
// (an edit of an existing comment is never deleted this way)
|
|
@@ -716,7 +763,9 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
716
763
|
) {
|
|
717
764
|
return closeCompose();
|
|
718
765
|
}
|
|
766
|
+
|
|
719
767
|
if (handleSlashKey(key, activeCompose)) return;
|
|
768
|
+
|
|
720
769
|
if (!composerReady.current) handlePremountKey(key, activeCompose);
|
|
721
770
|
};
|
|
722
771
|
|
|
@@ -727,6 +776,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
727
776
|
|
|
728
777
|
for (const discussion of discussions) {
|
|
729
778
|
if (discussion.blockIndex !== cursor) continue;
|
|
779
|
+
|
|
730
780
|
if (next.has(discussion.key)) next.delete(discussion.key);
|
|
731
781
|
else next.add(discussion.key);
|
|
732
782
|
}
|
|
@@ -743,8 +793,10 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
743
793
|
|
|
744
794
|
return true;
|
|
745
795
|
}
|
|
796
|
+
|
|
746
797
|
if ((key.super || key.meta || key.ctrl) && (key.name === "[" || key.name === "]")) {
|
|
747
798
|
if (discussions.length === 0) return true;
|
|
799
|
+
|
|
748
800
|
const currentIndex = discussions.findIndex(
|
|
749
801
|
(discussion) => discussion.key === focusedDiscussion,
|
|
750
802
|
);
|
|
@@ -759,6 +811,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
759
811
|
|
|
760
812
|
return true;
|
|
761
813
|
}
|
|
814
|
+
|
|
762
815
|
// enter replies into the focused discussion, else the cursor block's discussion
|
|
763
816
|
if (key.name === "return") {
|
|
764
817
|
const replyTarget =
|
|
@@ -795,15 +848,19 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
795
848
|
};
|
|
796
849
|
|
|
797
850
|
if (target !== null) return moveTo(head.blockIndex, target);
|
|
851
|
+
|
|
798
852
|
if (extend && head.char !== (delta === 1 ? text.length : 0)) {
|
|
799
853
|
// the block edge first, then the next step crosses into the neighbour
|
|
800
854
|
return moveTo(head.blockIndex, delta === 1 ? text.length : 0);
|
|
801
855
|
}
|
|
856
|
+
|
|
802
857
|
// the neighbour is the next block the caret may rest on: headers are skipped
|
|
803
858
|
const neighbour = nearestAnnotatable(source, head.blockIndex, delta);
|
|
804
859
|
|
|
805
860
|
if (neighbour === head.blockIndex) return;
|
|
861
|
+
|
|
806
862
|
if (delta === 1) return moveTo(neighbour, 0);
|
|
863
|
+
|
|
807
864
|
const previousText = blockText(neighbour);
|
|
808
865
|
|
|
809
866
|
moveTo(
|
|
@@ -817,6 +874,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
817
874
|
const visualTarget = verticalTextPosition(head, vertical);
|
|
818
875
|
|
|
819
876
|
if (!visualTarget && nextBlock === head.blockIndex) return;
|
|
877
|
+
|
|
820
878
|
const nextHead = visualTarget?.position ?? {
|
|
821
879
|
blockIndex: nextBlock,
|
|
822
880
|
char: key.shift ? Math.min(head.char, textLengthOf(nextBlock)) : 0,
|
|
@@ -840,11 +898,13 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
840
898
|
|
|
841
899
|
return true;
|
|
842
900
|
}
|
|
901
|
+
|
|
843
902
|
if (key.name === "right" || (key.ctrl && key.name === "l")) {
|
|
844
903
|
moveCaretHorizontal(1, Boolean(key.shift || key.meta || key.ctrl));
|
|
845
904
|
|
|
846
905
|
return true;
|
|
847
906
|
}
|
|
907
|
+
|
|
848
908
|
if (key.name === "left" || (key.ctrl && key.name === "h")) {
|
|
849
909
|
moveCaretHorizontal(-1, Boolean(key.shift || key.meta || key.ctrl));
|
|
850
910
|
|
|
@@ -859,6 +919,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
859
919
|
const sequence = printableSequence(key);
|
|
860
920
|
|
|
861
921
|
if (!sequence) return;
|
|
922
|
+
|
|
862
923
|
const discussion = discussions.find((candidate) => candidate.key === focusedDiscussion);
|
|
863
924
|
|
|
864
925
|
if (discussion) {
|
|
@@ -874,6 +935,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
874
935
|
editAnnotationId: editingOwn ? last.id : null,
|
|
875
936
|
});
|
|
876
937
|
}
|
|
938
|
+
|
|
877
939
|
openNewCompose(sequence);
|
|
878
940
|
};
|
|
879
941
|
|
|
@@ -889,31 +951,41 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
889
951
|
|
|
890
952
|
useSharedKeyboard((key) => {
|
|
891
953
|
if (suspended) return;
|
|
954
|
+
|
|
892
955
|
if (key.ctrl && key.name === "q") return onExit();
|
|
956
|
+
|
|
893
957
|
const activeCompose = composeRef.current;
|
|
894
958
|
|
|
895
959
|
if (activeCompose) return handleComposeKey(key, activeCompose);
|
|
960
|
+
|
|
896
961
|
if (isAgentInvokeKey(key, onInvoke)) {
|
|
897
962
|
key.preventDefault();
|
|
898
963
|
|
|
899
964
|
return invokeFocusedComment();
|
|
900
965
|
}
|
|
966
|
+
|
|
901
967
|
if (key.name === "escape") {
|
|
902
968
|
// from type mode esc only enters nav, so a held mark survives for `c`
|
|
903
969
|
if (!navModeRef.current) return setNavMode(true);
|
|
970
|
+
|
|
904
971
|
if (focusedDiscussion !== null) setFocusedDiscussion(null);
|
|
905
972
|
|
|
906
973
|
return collapseCaret();
|
|
907
974
|
}
|
|
975
|
+
|
|
908
976
|
if (navModeRef.current) {
|
|
909
977
|
if (handleCaretKey(key)) return;
|
|
978
|
+
|
|
910
979
|
if (key.name === "z") return toggleFoldAtCursor();
|
|
980
|
+
|
|
911
981
|
if (onNavCommand?.(key, heldSpan)) return;
|
|
982
|
+
|
|
912
983
|
if (key.name === "c") {
|
|
913
984
|
setNavMode(false);
|
|
914
985
|
|
|
915
986
|
return openNewCompose("");
|
|
916
987
|
}
|
|
988
|
+
|
|
917
989
|
if (printableSequence(key)) {
|
|
918
990
|
setNavMode(false);
|
|
919
991
|
startTyping(key);
|
|
@@ -921,8 +993,11 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
921
993
|
|
|
922
994
|
return;
|
|
923
995
|
}
|
|
996
|
+
|
|
924
997
|
if (handleDiscussionVerb(key)) return;
|
|
998
|
+
|
|
925
999
|
if (handleCaretKey(key)) return;
|
|
1000
|
+
|
|
926
1001
|
startTyping(key);
|
|
927
1002
|
});
|
|
928
1003
|
|
|
@@ -1006,6 +1081,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1006
1081
|
focused={focusedDiscussion === discussion.key}
|
|
1007
1082
|
onFocus={() => {
|
|
1008
1083
|
if (composeRef.current?.discussionKey === discussion.key) return;
|
|
1084
|
+
|
|
1009
1085
|
blurSaveCompose();
|
|
1010
1086
|
setFocusedDiscussion(discussion.key);
|
|
1011
1087
|
setCursor(discussion.blockIndex);
|
|
@@ -1024,8 +1100,11 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1024
1100
|
};
|
|
1025
1101
|
|
|
1026
1102
|
for (const discussion of discussions) pushSpan(discussion.span);
|
|
1103
|
+
|
|
1027
1104
|
if (compose) pushSpan(compose.span);
|
|
1105
|
+
|
|
1028
1106
|
if (caretIsSelection && !compose) pushSpan(heldSpan);
|
|
1107
|
+
|
|
1029
1108
|
// the idle caret cell sits at the head, also at the end of a held mark;
|
|
1030
1109
|
// an open card takes the cursor with it, so no cell is painted then
|
|
1031
1110
|
if (blockIndex === head.blockIndex && !compose && textLength > 0) {
|
|
@@ -1063,6 +1142,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1063
1142
|
|
|
1064
1143
|
return;
|
|
1065
1144
|
}
|
|
1145
|
+
|
|
1066
1146
|
nodes.push(
|
|
1067
1147
|
<DiscussionCard
|
|
1068
1148
|
key="compose-new"
|
|
@@ -1079,6 +1159,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1079
1159
|
|
|
1080
1160
|
for (const discussion of discussions) {
|
|
1081
1161
|
if (discussion.blockIndex !== blockIndex || !endsInLine(discussion.span.end.char)) continue;
|
|
1162
|
+
|
|
1082
1163
|
if (
|
|
1083
1164
|
composePending &&
|
|
1084
1165
|
composeStart &&
|
|
@@ -1087,15 +1168,19 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1087
1168
|
pushComposeCard();
|
|
1088
1169
|
composePending = false;
|
|
1089
1170
|
}
|
|
1171
|
+
|
|
1090
1172
|
const composingHere = Boolean(composeHere && compose.discussionKey === discussion.key);
|
|
1091
1173
|
|
|
1092
1174
|
if (folded.has(discussion.key) && !composingHere) {
|
|
1093
1175
|
nodes.push(foldedSummaryFor(discussion));
|
|
1094
1176
|
continue;
|
|
1095
1177
|
}
|
|
1178
|
+
|
|
1096
1179
|
nodes.push(discussionCardFor(discussion, composingHere));
|
|
1180
|
+
|
|
1097
1181
|
if (composingHere) nodes.push(paletteNode);
|
|
1098
1182
|
}
|
|
1183
|
+
|
|
1099
1184
|
if (composePending) pushComposeCard();
|
|
1100
1185
|
|
|
1101
1186
|
return nodes;
|
|
@@ -1143,6 +1228,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1143
1228
|
active.blockIndex === blockIndex
|
|
1144
1229
|
)
|
|
1145
1230
|
return;
|
|
1231
|
+
|
|
1146
1232
|
const position = { blockIndex, char: 0 };
|
|
1147
1233
|
|
|
1148
1234
|
openCompose({
|
|
@@ -1161,12 +1247,15 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1161
1247
|
const restoredPrompt = useRef<number | null>(null);
|
|
1162
1248
|
const restorePrompt = useEffectEvent(() => {
|
|
1163
1249
|
if (!promptRestoreRequest || suspended || !onInvoke) return;
|
|
1250
|
+
|
|
1164
1251
|
if (restoredPrompt.current === promptRestoreRequest.id) return;
|
|
1252
|
+
|
|
1165
1253
|
restoredPrompt.current = promptRestoreRequest.id;
|
|
1166
1254
|
const active = composeRef.current;
|
|
1167
1255
|
const newerDraft = active?.kind === "prompt" ? composeTextRef.current : promptDraft.current;
|
|
1168
1256
|
|
|
1169
1257
|
if (active?.kind !== "prompt") blurSaveCompose();
|
|
1258
|
+
|
|
1170
1259
|
promptDraft.current = [promptRestoreRequest.text, newerDraft].filter(Boolean).join("\n");
|
|
1171
1260
|
focusPrompt(source.count - 1);
|
|
1172
1261
|
});
|
|
@@ -1177,10 +1266,14 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1177
1266
|
const focusedReply = useRef<string | null>(null);
|
|
1178
1267
|
const continueConversation = useEffectEvent(() => {
|
|
1179
1268
|
if (!promptFocusRequest || suspended || !onInvoke) return;
|
|
1269
|
+
|
|
1180
1270
|
if (focusedReply.current === promptFocusRequest.replyId) return;
|
|
1271
|
+
|
|
1181
1272
|
focusedReply.current = promptFocusRequest.replyId;
|
|
1273
|
+
|
|
1182
1274
|
// An arriving answer cannot replace a draft or a marked passage under review.
|
|
1183
1275
|
if (composeRef.current || heldSpan || dragging.current || promptDraft.current) return;
|
|
1276
|
+
|
|
1184
1277
|
focusPrompt(promptFocusRequest.blockIndex);
|
|
1185
1278
|
});
|
|
1186
1279
|
|
|
@@ -1189,6 +1282,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1189
1282
|
}, [promptFocusRequest?.replyId, promptFocusRequest?.blockIndex, suspended]);
|
|
1190
1283
|
const onPromptMouseDown = (event: TerminalMouseEvent): void => {
|
|
1191
1284
|
if (!onInvoke || suspended) return;
|
|
1285
|
+
|
|
1192
1286
|
const blockIndex = source.count - 1;
|
|
1193
1287
|
const geometry = allGeometry();
|
|
1194
1288
|
const promptLine = geometry.find((entry) => isPromptBlock?.(entry.blockIndex));
|
|
@@ -1206,9 +1300,11 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1206
1300
|
(viewport && event.y >= viewport.bottom)
|
|
1207
1301
|
)
|
|
1208
1302
|
return;
|
|
1303
|
+
|
|
1209
1304
|
if (composeRef.current?.kind === "prompt") {
|
|
1210
1305
|
promptDraft.current = composeTextRef.current;
|
|
1211
1306
|
} else blurSaveCompose();
|
|
1307
|
+
|
|
1212
1308
|
endDrag();
|
|
1213
1309
|
focusPrompt(blockIndex);
|
|
1214
1310
|
};
|
|
@@ -1218,6 +1314,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1218
1314
|
const pressed = positionAt(allGeometry(), event.x, event.y);
|
|
1219
1315
|
|
|
1220
1316
|
if (!pressed || !source.annotatable(pressed.blockIndex)) return;
|
|
1317
|
+
|
|
1221
1318
|
setNavMode(false);
|
|
1222
1319
|
const stamp = { time: Date.now(), x: event.x, y: event.y };
|
|
1223
1320
|
const wordMode = isDoubleClick(lastClick.current, stamp);
|