@cueloop/client 0.1.0-alpha.101 → 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 +14 -1
- 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 +154 -19
- 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 +72 -6
- 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 +3 -2
- 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 +28 -3
- 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 +16 -3
- 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 +18 -10
- package/src/thread/components/AgentConfigControls.tsx +1 -0
- package/src/thread/components/AgentThreadPane.test.tsx +257 -1
- package/src/thread/components/AgentThreadPane.tsx +48 -16
- 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
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
slashItemsFrom,
|
|
48
48
|
} from "../keyboard/slash-palette";
|
|
49
49
|
import { SlashSkillsContext } from "../keyboard/skills";
|
|
50
|
-
import { discussionsFrom, type Discussion } from "./discussions";
|
|
50
|
+
import { discussionsFrom, spanKey, type Discussion } from "./discussions";
|
|
51
51
|
import {
|
|
52
52
|
CommentRow,
|
|
53
53
|
Composer,
|
|
@@ -107,13 +107,13 @@ export interface AnnotationSurfaceOptions {
|
|
|
107
107
|
) => boolean;
|
|
108
108
|
focusedAnnotationId?: string;
|
|
109
109
|
onFocusAnnotation?: (annotationId: string | undefined) => void;
|
|
110
|
-
onAnnotate: (span: TextSpan, body: string) => void;
|
|
111
|
-
onReply: (rootAnnotationId: string, body: string) => void;
|
|
110
|
+
onAnnotate: (span: TextSpan, body: string) => string | void;
|
|
111
|
+
onReply: (rootAnnotationId: string, body: string) => string | void;
|
|
112
112
|
onUpdateAnnotation: (id: string, body: string) => void;
|
|
113
113
|
isAnnotationReadOnly?: (id: string) => boolean;
|
|
114
114
|
annotationAction?: (id: string) => { label: string; run: () => void } | undefined;
|
|
115
115
|
requestedBlock?: { blockIndex: number };
|
|
116
|
-
onInvoke?: () => void;
|
|
116
|
+
onInvoke?: (commentId?: string) => void;
|
|
117
117
|
isPromptBlock?: (blockIndex: number) => boolean;
|
|
118
118
|
promptFocusRequest?: PromptFocusRequest;
|
|
119
119
|
promptRestoreRequest?: PromptRestoreRequest;
|
|
@@ -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
|
|
@@ -277,17 +281,26 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
277
281
|
// app owns it (onFocusAnnotation), the focused discussion is the one holding
|
|
278
282
|
// the focused card and focusing here names the root comment; standalone, it
|
|
279
283
|
// is local state
|
|
280
|
-
const [localFocus, setLocalFocus] = useState<string | null>(null);
|
|
281
|
-
const
|
|
282
|
-
onFocusAnnotation === undefined
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
)
|
|
284
|
+
const [localFocus, setLocalFocus] = useState<{ key: string; annotationId?: string } | null>(null);
|
|
285
|
+
const getFocusedDiscussion = (): string | null => {
|
|
286
|
+
if (onFocusAnnotation === undefined) return localFocus?.key ?? null;
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
discussions.find((discussion) =>
|
|
290
|
+
discussion.annotations.some((annotation) => annotation.id === focusedAnnotationId),
|
|
291
|
+
)?.key ?? null
|
|
292
|
+
);
|
|
293
|
+
};
|
|
294
|
+
const focusedDiscussion = getFocusedDiscussion();
|
|
287
295
|
const setFocusedDiscussion = (key: string | null): void => {
|
|
288
|
-
if (onFocusAnnotation === undefined) return setLocalFocus(key);
|
|
296
|
+
if (onFocusAnnotation === undefined) return setLocalFocus(key === null ? null : { key });
|
|
297
|
+
|
|
289
298
|
onFocusAnnotation(discussions.find((discussion) => discussion.key === key)?.rootId);
|
|
290
299
|
};
|
|
300
|
+
const focusSavedComment = (annotationId: string, key: string): void => {
|
|
301
|
+
if (onFocusAnnotation) onFocusAnnotation(annotationId);
|
|
302
|
+
else setLocalFocus({ key, annotationId });
|
|
303
|
+
};
|
|
291
304
|
const [folded, setFolded] = useState<Set<string>>(new Set());
|
|
292
305
|
const [composeText, setComposeText] = useState("");
|
|
293
306
|
const [composerFocusRequest, setComposerFocusRequest] = useState(0);
|
|
@@ -331,7 +344,9 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
331
344
|
/** Agent comments require a held selection; legacy surfaces may use the caret word. */
|
|
332
345
|
const caretSpan = (): TextSpan | null => {
|
|
333
346
|
if (heldSpan) return heldSpan;
|
|
347
|
+
|
|
334
348
|
if (onInvoke) return isPromptBlock?.(head.blockIndex) ? { start: head, end: head } : null;
|
|
349
|
+
|
|
335
350
|
const word = wordRangeAt(blockText(head.blockIndex), head.char);
|
|
336
351
|
|
|
337
352
|
return word
|
|
@@ -381,15 +396,18 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
381
396
|
return (delta, top, bottom) => {
|
|
382
397
|
if (delta === 0 || oldHeadY === undefined || compose || focusedDiscussion || caretIsSelection)
|
|
383
398
|
return;
|
|
399
|
+
|
|
384
400
|
const shiftedHeadY = oldHeadY - delta;
|
|
385
401
|
|
|
386
402
|
if (shiftedHeadY >= top && shiftedHeadY <= bottom) return;
|
|
403
|
+
|
|
387
404
|
const visible = lines
|
|
388
405
|
.filter((line) => line.y - delta >= top && line.y - delta <= bottom)
|
|
389
406
|
.toSorted((left, right) => left.y - right.y || left.x - right.x);
|
|
390
407
|
const target = delta < 0 ? visible.at(-1) : visible[0];
|
|
391
408
|
|
|
392
409
|
if (!target) return;
|
|
410
|
+
|
|
393
411
|
const next = { blockIndex: target.blockIndex, char: target.start };
|
|
394
412
|
|
|
395
413
|
flushSync(() => {
|
|
@@ -416,6 +434,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
416
434
|
const target = currentIndex === -1 ? undefined : lines[currentIndex + direction];
|
|
417
435
|
|
|
418
436
|
if (!target) return null;
|
|
437
|
+
|
|
419
438
|
const current = lines[currentIndex]!;
|
|
420
439
|
const column = Math.max(0, position.char - current.start);
|
|
421
440
|
|
|
@@ -431,8 +450,11 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
431
450
|
const openCompose = (state: ComposeState): void => {
|
|
432
451
|
// a view-only surface (a non-diff thread's live diff) never opens a draft
|
|
433
452
|
if (!commentsEnabled) return;
|
|
453
|
+
|
|
434
454
|
if (observer) return onObserverBlocked?.("observer");
|
|
455
|
+
|
|
435
456
|
if (resolved) return onObserverBlocked?.("resolved");
|
|
457
|
+
|
|
436
458
|
composerReady.current = false;
|
|
437
459
|
readComposerText.current = null;
|
|
438
460
|
composeRef.current = state;
|
|
@@ -468,31 +490,55 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
468
490
|
const prompt = target?.kind === "prompt";
|
|
469
491
|
|
|
470
492
|
if (prompt && !invoke) return;
|
|
493
|
+
|
|
471
494
|
closeCompose();
|
|
472
495
|
collapseCaret();
|
|
496
|
+
|
|
473
497
|
if (!target || body.trim().length === 0) return;
|
|
498
|
+
|
|
474
499
|
if (target.editAnnotationId !== null) {
|
|
475
500
|
onUpdateAnnotation(target.editAnnotationId, body);
|
|
476
|
-
|
|
501
|
+
|
|
502
|
+
if (target.discussionKey) focusSavedComment(target.editAnnotationId, target.discussionKey);
|
|
503
|
+
|
|
504
|
+
if (invoke) onInvoke?.(target.editAnnotationId);
|
|
477
505
|
|
|
478
506
|
return;
|
|
479
507
|
}
|
|
508
|
+
|
|
480
509
|
if (target.discussionKey !== null) {
|
|
481
510
|
const discussion = discussions.find((candidate) => candidate.key === target.discussionKey);
|
|
482
511
|
|
|
483
512
|
if (discussion) {
|
|
484
|
-
onReply(discussion.rootId, body);
|
|
485
|
-
|
|
513
|
+
const commentId = onReply(discussion.rootId, body);
|
|
514
|
+
|
|
515
|
+
if (commentId) focusSavedComment(commentId, discussion.key);
|
|
516
|
+
|
|
517
|
+
if (invoke && commentId) onInvoke?.(commentId);
|
|
486
518
|
|
|
487
519
|
return;
|
|
488
520
|
}
|
|
489
521
|
}
|
|
522
|
+
|
|
490
523
|
const span = target.span ?? caretSpan();
|
|
491
524
|
|
|
492
525
|
if (span) {
|
|
493
|
-
onAnnotate(span, body);
|
|
494
|
-
|
|
526
|
+
const commentId = onAnnotate(span, body);
|
|
527
|
+
|
|
528
|
+
completeSavedComment(span, commentId, prompt, invoke);
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
const completeSavedComment = (
|
|
532
|
+
span: TextSpan,
|
|
533
|
+
commentId: string | void,
|
|
534
|
+
prompt: boolean,
|
|
535
|
+
invoke: boolean,
|
|
536
|
+
): void => {
|
|
537
|
+
if (!prompt && commentId) {
|
|
538
|
+
focusSavedComment(commentId, spanKey(span));
|
|
495
539
|
}
|
|
540
|
+
|
|
541
|
+
if (invoke && (prompt || commentId)) onInvoke?.(commentId || undefined);
|
|
496
542
|
};
|
|
497
543
|
/** A new discussion on the typing anchor; the card renders under the span's last block. */
|
|
498
544
|
const openNewCompose = (seed: string): void => {
|
|
@@ -503,6 +549,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
503
549
|
agentInputTarget(Boolean(isPromptBlock?.(head.blockIndex)), Boolean(heldSpan)) === "none"
|
|
504
550
|
)
|
|
505
551
|
return;
|
|
552
|
+
|
|
506
553
|
if (isPromptBlock?.(head.blockIndex) && span) {
|
|
507
554
|
openCompose({
|
|
508
555
|
kind: "prompt",
|
|
@@ -522,6 +569,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
522
569
|
editAnnotationId: null,
|
|
523
570
|
});
|
|
524
571
|
}
|
|
572
|
+
|
|
525
573
|
if (isPromptBlock?.(head.blockIndex)) promptDraft.current = "";
|
|
526
574
|
};
|
|
527
575
|
|
|
@@ -531,14 +579,17 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
531
579
|
const target = composeRef.current;
|
|
532
580
|
|
|
533
581
|
if (!target) return;
|
|
582
|
+
|
|
534
583
|
if (target.kind === "prompt") {
|
|
535
584
|
promptDraft.current = composeTextRef.current;
|
|
536
585
|
|
|
537
586
|
return closeCompose();
|
|
538
587
|
}
|
|
588
|
+
|
|
539
589
|
if (isStandaloneSlashQuery(composeText) || composeText.trim().length === 0) {
|
|
540
590
|
return closeCompose();
|
|
541
591
|
}
|
|
592
|
+
|
|
542
593
|
saveComment(composeText, false);
|
|
543
594
|
};
|
|
544
595
|
|
|
@@ -547,6 +598,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
547
598
|
const drag = dragging.current;
|
|
548
599
|
|
|
549
600
|
if (!drag) return;
|
|
601
|
+
|
|
550
602
|
let next = { head: pointer, anchor: drag.anchor };
|
|
551
603
|
|
|
552
604
|
if (drag.wordMode) {
|
|
@@ -561,7 +613,9 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
561
613
|
: { anchor: snapped.end, head: snapped.start };
|
|
562
614
|
}
|
|
563
615
|
}
|
|
616
|
+
|
|
564
617
|
if (comparePositions(next.head, drag.head) === 0) return;
|
|
618
|
+
|
|
565
619
|
drag.head = next.head;
|
|
566
620
|
setCursor(next.head.blockIndex);
|
|
567
621
|
setCaret({ head: next.head, anchor: next.anchor });
|
|
@@ -574,19 +628,23 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
574
628
|
// on screen - the mark follows the pointer across blocks.
|
|
575
629
|
const handleRootDrag = (event: TerminalMouseEvent): void => {
|
|
576
630
|
if (!dragging.current) return;
|
|
631
|
+
|
|
577
632
|
dragPointer.current = { x: event.x, y: event.y };
|
|
578
633
|
const position = positionAt(allGeometry(), event.x, event.y);
|
|
579
634
|
|
|
580
635
|
if (position) extendSelectionTo(position);
|
|
636
|
+
|
|
581
637
|
if (edgeScrollTimer.current === null) {
|
|
582
638
|
edgeScrollTimer.current = setInterval(() => {
|
|
583
639
|
const pointer = dragPointer.current;
|
|
584
640
|
const viewport = dragViewportRef.current?.();
|
|
585
641
|
|
|
586
642
|
if (!dragging.current || !pointer || !viewport) return;
|
|
643
|
+
|
|
587
644
|
const direction = pointer.y <= viewport.top ? -1 : pointer.y >= viewport.bottom ? 1 : 0;
|
|
588
645
|
|
|
589
646
|
if (direction === 0 || !viewport.scrollBy(direction)) return;
|
|
647
|
+
|
|
590
648
|
const next = positionAt(allGeometry(), pointer.x, pointer.y);
|
|
591
649
|
|
|
592
650
|
if (next) extendSelectionTo(next);
|
|
@@ -596,7 +654,9 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
596
654
|
const endDrag = (): void => {
|
|
597
655
|
dragging.current = null;
|
|
598
656
|
dragPointer.current = null;
|
|
657
|
+
|
|
599
658
|
if (edgeScrollTimer.current !== null) clearInterval(edgeScrollTimer.current);
|
|
659
|
+
|
|
600
660
|
edgeScrollTimer.current = null;
|
|
601
661
|
};
|
|
602
662
|
|
|
@@ -606,6 +666,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
606
666
|
const target = discussions.find((discussion) => discussion.key === key);
|
|
607
667
|
|
|
608
668
|
if (!target) return;
|
|
669
|
+
|
|
609
670
|
blurSaveCompose();
|
|
610
671
|
setCursor(target.blockIndex);
|
|
611
672
|
setFocusedDiscussion(key);
|
|
@@ -615,6 +676,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
615
676
|
|
|
616
677
|
const handleSlashKey = (key: KeyEvent, activeCompose: ComposeState): boolean => {
|
|
617
678
|
if (!slashActive || slashItems.length === 0) return false;
|
|
679
|
+
|
|
618
680
|
const selected = Math.min(slashIndex, slashItems.length - 1);
|
|
619
681
|
|
|
620
682
|
if (key.name === "up") {
|
|
@@ -622,11 +684,13 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
622
684
|
|
|
623
685
|
return true;
|
|
624
686
|
}
|
|
687
|
+
|
|
625
688
|
if (key.name === "down") {
|
|
626
689
|
setSlashIndex(Math.min(slashItems.length - 1, selected + 1));
|
|
627
690
|
|
|
628
691
|
return true;
|
|
629
692
|
}
|
|
693
|
+
|
|
630
694
|
if (key.name === "return" || key.name === "tab") {
|
|
631
695
|
const inserted = insertSlashItem(composeText, caretOffset, slashItems[selected]!.name);
|
|
632
696
|
|
|
@@ -643,8 +707,10 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
643
707
|
const handlePremountKey = (key: KeyEvent, activeCompose: ComposeState): void => {
|
|
644
708
|
if (key.name === "return") {
|
|
645
709
|
if (key.super && onInvoke) return;
|
|
710
|
+
|
|
646
711
|
if (key.meta || key.ctrl || key.super)
|
|
647
712
|
return saveComment(activeCompose.seed, isAgentInvokeKey(key, onInvoke));
|
|
713
|
+
|
|
648
714
|
const grown = { ...activeCompose, seed: `${activeCompose.seed}\n` };
|
|
649
715
|
|
|
650
716
|
composeRef.current = grown;
|
|
@@ -653,6 +719,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
653
719
|
|
|
654
720
|
return setCompose(grown);
|
|
655
721
|
}
|
|
722
|
+
|
|
656
723
|
const sequence = printableSequence(key);
|
|
657
724
|
|
|
658
725
|
if (sequence) {
|
|
@@ -671,11 +738,13 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
671
738
|
|
|
672
739
|
return;
|
|
673
740
|
}
|
|
741
|
+
|
|
674
742
|
if (isAgentInvokeKey(key, onInvoke)) {
|
|
675
743
|
key.preventDefault();
|
|
676
744
|
|
|
677
745
|
return saveComment(readComposerText.current?.() ?? activeCompose.seed, true);
|
|
678
746
|
}
|
|
747
|
+
|
|
679
748
|
// the textarea owns every key while open; the view takes dismiss (which
|
|
680
749
|
// also releases the discussion focus), the slash palette, and pre-mount input
|
|
681
750
|
if (key.name === "escape") {
|
|
@@ -683,6 +752,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
683
752
|
|
|
684
753
|
return closeCompose();
|
|
685
754
|
}
|
|
755
|
+
|
|
686
756
|
// backspace on an already empty draft undoes it: the card dissolves
|
|
687
757
|
// and the caret sits back on the still-held selection, ready to re-type
|
|
688
758
|
// (an edit of an existing comment is never deleted this way)
|
|
@@ -693,7 +763,9 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
693
763
|
) {
|
|
694
764
|
return closeCompose();
|
|
695
765
|
}
|
|
766
|
+
|
|
696
767
|
if (handleSlashKey(key, activeCompose)) return;
|
|
768
|
+
|
|
697
769
|
if (!composerReady.current) handlePremountKey(key, activeCompose);
|
|
698
770
|
};
|
|
699
771
|
|
|
@@ -704,6 +776,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
704
776
|
|
|
705
777
|
for (const discussion of discussions) {
|
|
706
778
|
if (discussion.blockIndex !== cursor) continue;
|
|
779
|
+
|
|
707
780
|
if (next.has(discussion.key)) next.delete(discussion.key);
|
|
708
781
|
else next.add(discussion.key);
|
|
709
782
|
}
|
|
@@ -720,8 +793,10 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
720
793
|
|
|
721
794
|
return true;
|
|
722
795
|
}
|
|
796
|
+
|
|
723
797
|
if ((key.super || key.meta || key.ctrl) && (key.name === "[" || key.name === "]")) {
|
|
724
798
|
if (discussions.length === 0) return true;
|
|
799
|
+
|
|
725
800
|
const currentIndex = discussions.findIndex(
|
|
726
801
|
(discussion) => discussion.key === focusedDiscussion,
|
|
727
802
|
);
|
|
@@ -736,6 +811,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
736
811
|
|
|
737
812
|
return true;
|
|
738
813
|
}
|
|
814
|
+
|
|
739
815
|
// enter replies into the focused discussion, else the cursor block's discussion
|
|
740
816
|
if (key.name === "return") {
|
|
741
817
|
const replyTarget =
|
|
@@ -772,15 +848,19 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
772
848
|
};
|
|
773
849
|
|
|
774
850
|
if (target !== null) return moveTo(head.blockIndex, target);
|
|
851
|
+
|
|
775
852
|
if (extend && head.char !== (delta === 1 ? text.length : 0)) {
|
|
776
853
|
// the block edge first, then the next step crosses into the neighbour
|
|
777
854
|
return moveTo(head.blockIndex, delta === 1 ? text.length : 0);
|
|
778
855
|
}
|
|
856
|
+
|
|
779
857
|
// the neighbour is the next block the caret may rest on: headers are skipped
|
|
780
858
|
const neighbour = nearestAnnotatable(source, head.blockIndex, delta);
|
|
781
859
|
|
|
782
860
|
if (neighbour === head.blockIndex) return;
|
|
861
|
+
|
|
783
862
|
if (delta === 1) return moveTo(neighbour, 0);
|
|
863
|
+
|
|
784
864
|
const previousText = blockText(neighbour);
|
|
785
865
|
|
|
786
866
|
moveTo(
|
|
@@ -794,6 +874,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
794
874
|
const visualTarget = verticalTextPosition(head, vertical);
|
|
795
875
|
|
|
796
876
|
if (!visualTarget && nextBlock === head.blockIndex) return;
|
|
877
|
+
|
|
797
878
|
const nextHead = visualTarget?.position ?? {
|
|
798
879
|
blockIndex: nextBlock,
|
|
799
880
|
char: key.shift ? Math.min(head.char, textLengthOf(nextBlock)) : 0,
|
|
@@ -817,11 +898,13 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
817
898
|
|
|
818
899
|
return true;
|
|
819
900
|
}
|
|
901
|
+
|
|
820
902
|
if (key.name === "right" || (key.ctrl && key.name === "l")) {
|
|
821
903
|
moveCaretHorizontal(1, Boolean(key.shift || key.meta || key.ctrl));
|
|
822
904
|
|
|
823
905
|
return true;
|
|
824
906
|
}
|
|
907
|
+
|
|
825
908
|
if (key.name === "left" || (key.ctrl && key.name === "h")) {
|
|
826
909
|
moveCaretHorizontal(-1, Boolean(key.shift || key.meta || key.ctrl));
|
|
827
910
|
|
|
@@ -836,6 +919,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
836
919
|
const sequence = printableSequence(key);
|
|
837
920
|
|
|
838
921
|
if (!sequence) return;
|
|
922
|
+
|
|
839
923
|
const discussion = discussions.find((candidate) => candidate.key === focusedDiscussion);
|
|
840
924
|
|
|
841
925
|
if (discussion) {
|
|
@@ -851,36 +935,57 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
851
935
|
editAnnotationId: editingOwn ? last.id : null,
|
|
852
936
|
});
|
|
853
937
|
}
|
|
938
|
+
|
|
854
939
|
openNewCompose(sequence);
|
|
855
940
|
};
|
|
856
941
|
|
|
942
|
+
const invokeFocusedComment = (): void => {
|
|
943
|
+
const discussion = discussions.find((candidate) => candidate.key === focusedDiscussion);
|
|
944
|
+
const focusedCommentId = focusedAnnotationId ?? localFocus?.annotationId;
|
|
945
|
+
const commentId =
|
|
946
|
+
discussion?.annotations.find((annotation) => annotation.id === focusedCommentId)?.id ??
|
|
947
|
+
discussion?.annotations.at(-1)?.id;
|
|
948
|
+
|
|
949
|
+
onInvoke?.(commentId);
|
|
950
|
+
};
|
|
951
|
+
|
|
857
952
|
useSharedKeyboard((key) => {
|
|
858
953
|
if (suspended) return;
|
|
954
|
+
|
|
859
955
|
if (key.ctrl && key.name === "q") return onExit();
|
|
956
|
+
|
|
860
957
|
const activeCompose = composeRef.current;
|
|
861
958
|
|
|
862
959
|
if (activeCompose) return handleComposeKey(key, activeCompose);
|
|
960
|
+
|
|
863
961
|
if (isAgentInvokeKey(key, onInvoke)) {
|
|
864
962
|
key.preventDefault();
|
|
865
963
|
|
|
866
|
-
return
|
|
964
|
+
return invokeFocusedComment();
|
|
867
965
|
}
|
|
966
|
+
|
|
868
967
|
if (key.name === "escape") {
|
|
869
968
|
// from type mode esc only enters nav, so a held mark survives for `c`
|
|
870
969
|
if (!navModeRef.current) return setNavMode(true);
|
|
970
|
+
|
|
871
971
|
if (focusedDiscussion !== null) setFocusedDiscussion(null);
|
|
872
972
|
|
|
873
973
|
return collapseCaret();
|
|
874
974
|
}
|
|
975
|
+
|
|
875
976
|
if (navModeRef.current) {
|
|
876
977
|
if (handleCaretKey(key)) return;
|
|
978
|
+
|
|
877
979
|
if (key.name === "z") return toggleFoldAtCursor();
|
|
980
|
+
|
|
878
981
|
if (onNavCommand?.(key, heldSpan)) return;
|
|
982
|
+
|
|
879
983
|
if (key.name === "c") {
|
|
880
984
|
setNavMode(false);
|
|
881
985
|
|
|
882
986
|
return openNewCompose("");
|
|
883
987
|
}
|
|
988
|
+
|
|
884
989
|
if (printableSequence(key)) {
|
|
885
990
|
setNavMode(false);
|
|
886
991
|
startTyping(key);
|
|
@@ -888,8 +993,11 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
888
993
|
|
|
889
994
|
return;
|
|
890
995
|
}
|
|
996
|
+
|
|
891
997
|
if (handleDiscussionVerb(key)) return;
|
|
998
|
+
|
|
892
999
|
if (handleCaretKey(key)) return;
|
|
1000
|
+
|
|
893
1001
|
startTyping(key);
|
|
894
1002
|
});
|
|
895
1003
|
|
|
@@ -951,6 +1059,11 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
951
1059
|
tokens={tokens}
|
|
952
1060
|
authorLabel={resolveAuthorLabel?.(annotation)}
|
|
953
1061
|
action={annotationAction?.(annotation.id)}
|
|
1062
|
+
onFocus={() => {
|
|
1063
|
+
blurSaveCompose();
|
|
1064
|
+
focusSavedComment(annotation.id, discussion.key);
|
|
1065
|
+
setCursor(discussion.blockIndex);
|
|
1066
|
+
}}
|
|
954
1067
|
/>
|
|
955
1068
|
),
|
|
956
1069
|
};
|
|
@@ -968,6 +1081,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
968
1081
|
focused={focusedDiscussion === discussion.key}
|
|
969
1082
|
onFocus={() => {
|
|
970
1083
|
if (composeRef.current?.discussionKey === discussion.key) return;
|
|
1084
|
+
|
|
971
1085
|
blurSaveCompose();
|
|
972
1086
|
setFocusedDiscussion(discussion.key);
|
|
973
1087
|
setCursor(discussion.blockIndex);
|
|
@@ -986,8 +1100,11 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
986
1100
|
};
|
|
987
1101
|
|
|
988
1102
|
for (const discussion of discussions) pushSpan(discussion.span);
|
|
1103
|
+
|
|
989
1104
|
if (compose) pushSpan(compose.span);
|
|
1105
|
+
|
|
990
1106
|
if (caretIsSelection && !compose) pushSpan(heldSpan);
|
|
1107
|
+
|
|
991
1108
|
// the idle caret cell sits at the head, also at the end of a held mark;
|
|
992
1109
|
// an open card takes the cursor with it, so no cell is painted then
|
|
993
1110
|
if (blockIndex === head.blockIndex && !compose && textLength > 0) {
|
|
@@ -1025,6 +1142,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1025
1142
|
|
|
1026
1143
|
return;
|
|
1027
1144
|
}
|
|
1145
|
+
|
|
1028
1146
|
nodes.push(
|
|
1029
1147
|
<DiscussionCard
|
|
1030
1148
|
key="compose-new"
|
|
@@ -1041,6 +1159,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1041
1159
|
|
|
1042
1160
|
for (const discussion of discussions) {
|
|
1043
1161
|
if (discussion.blockIndex !== blockIndex || !endsInLine(discussion.span.end.char)) continue;
|
|
1162
|
+
|
|
1044
1163
|
if (
|
|
1045
1164
|
composePending &&
|
|
1046
1165
|
composeStart &&
|
|
@@ -1049,15 +1168,19 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1049
1168
|
pushComposeCard();
|
|
1050
1169
|
composePending = false;
|
|
1051
1170
|
}
|
|
1171
|
+
|
|
1052
1172
|
const composingHere = Boolean(composeHere && compose.discussionKey === discussion.key);
|
|
1053
1173
|
|
|
1054
1174
|
if (folded.has(discussion.key) && !composingHere) {
|
|
1055
1175
|
nodes.push(foldedSummaryFor(discussion));
|
|
1056
1176
|
continue;
|
|
1057
1177
|
}
|
|
1178
|
+
|
|
1058
1179
|
nodes.push(discussionCardFor(discussion, composingHere));
|
|
1180
|
+
|
|
1059
1181
|
if (composingHere) nodes.push(paletteNode);
|
|
1060
1182
|
}
|
|
1183
|
+
|
|
1061
1184
|
if (composePending) pushComposeCard();
|
|
1062
1185
|
|
|
1063
1186
|
return nodes;
|
|
@@ -1105,6 +1228,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1105
1228
|
active.blockIndex === blockIndex
|
|
1106
1229
|
)
|
|
1107
1230
|
return;
|
|
1231
|
+
|
|
1108
1232
|
const position = { blockIndex, char: 0 };
|
|
1109
1233
|
|
|
1110
1234
|
openCompose({
|
|
@@ -1123,12 +1247,15 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1123
1247
|
const restoredPrompt = useRef<number | null>(null);
|
|
1124
1248
|
const restorePrompt = useEffectEvent(() => {
|
|
1125
1249
|
if (!promptRestoreRequest || suspended || !onInvoke) return;
|
|
1250
|
+
|
|
1126
1251
|
if (restoredPrompt.current === promptRestoreRequest.id) return;
|
|
1252
|
+
|
|
1127
1253
|
restoredPrompt.current = promptRestoreRequest.id;
|
|
1128
1254
|
const active = composeRef.current;
|
|
1129
1255
|
const newerDraft = active?.kind === "prompt" ? composeTextRef.current : promptDraft.current;
|
|
1130
1256
|
|
|
1131
1257
|
if (active?.kind !== "prompt") blurSaveCompose();
|
|
1258
|
+
|
|
1132
1259
|
promptDraft.current = [promptRestoreRequest.text, newerDraft].filter(Boolean).join("\n");
|
|
1133
1260
|
focusPrompt(source.count - 1);
|
|
1134
1261
|
});
|
|
@@ -1139,10 +1266,14 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1139
1266
|
const focusedReply = useRef<string | null>(null);
|
|
1140
1267
|
const continueConversation = useEffectEvent(() => {
|
|
1141
1268
|
if (!promptFocusRequest || suspended || !onInvoke) return;
|
|
1269
|
+
|
|
1142
1270
|
if (focusedReply.current === promptFocusRequest.replyId) return;
|
|
1271
|
+
|
|
1143
1272
|
focusedReply.current = promptFocusRequest.replyId;
|
|
1273
|
+
|
|
1144
1274
|
// An arriving answer cannot replace a draft or a marked passage under review.
|
|
1145
1275
|
if (composeRef.current || heldSpan || dragging.current || promptDraft.current) return;
|
|
1276
|
+
|
|
1146
1277
|
focusPrompt(promptFocusRequest.blockIndex);
|
|
1147
1278
|
});
|
|
1148
1279
|
|
|
@@ -1151,6 +1282,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1151
1282
|
}, [promptFocusRequest?.replyId, promptFocusRequest?.blockIndex, suspended]);
|
|
1152
1283
|
const onPromptMouseDown = (event: TerminalMouseEvent): void => {
|
|
1153
1284
|
if (!onInvoke || suspended) return;
|
|
1285
|
+
|
|
1154
1286
|
const blockIndex = source.count - 1;
|
|
1155
1287
|
const geometry = allGeometry();
|
|
1156
1288
|
const promptLine = geometry.find((entry) => isPromptBlock?.(entry.blockIndex));
|
|
@@ -1168,9 +1300,11 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1168
1300
|
(viewport && event.y >= viewport.bottom)
|
|
1169
1301
|
)
|
|
1170
1302
|
return;
|
|
1303
|
+
|
|
1171
1304
|
if (composeRef.current?.kind === "prompt") {
|
|
1172
1305
|
promptDraft.current = composeTextRef.current;
|
|
1173
1306
|
} else blurSaveCompose();
|
|
1307
|
+
|
|
1174
1308
|
endDrag();
|
|
1175
1309
|
focusPrompt(blockIndex);
|
|
1176
1310
|
};
|
|
@@ -1180,6 +1314,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1180
1314
|
const pressed = positionAt(allGeometry(), event.x, event.y);
|
|
1181
1315
|
|
|
1182
1316
|
if (!pressed || !source.annotatable(pressed.blockIndex)) return;
|
|
1317
|
+
|
|
1183
1318
|
setNavMode(false);
|
|
1184
1319
|
const stamp = { time: Date.now(), x: event.x, y: event.y };
|
|
1185
1320
|
const wordMode = isDoubleClick(lastClick.current, stamp);
|
|
@@ -1231,7 +1366,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1231
1366
|
|
|
1232
1367
|
function isAgentInvokeKey(
|
|
1233
1368
|
key: { name: string; ctrl?: boolean; meta?: boolean; super?: boolean },
|
|
1234
|
-
onInvoke?: () => void,
|
|
1369
|
+
onInvoke?: (commentId?: string) => void,
|
|
1235
1370
|
): boolean {
|
|
1236
1371
|
return Boolean(
|
|
1237
1372
|
onInvoke &&
|
|
@@ -73,9 +73,11 @@ test("deleted file caret scrolls one row and stays at the file boundaries", asyn
|
|
|
73
73
|
const box = findScrollbox(setup.renderer.root);
|
|
74
74
|
|
|
75
75
|
if (!box) throw new Error("file diff scrollbox missing");
|
|
76
|
+
|
|
76
77
|
for (let offset = 0; offset < 1000; offset += 20) {
|
|
77
78
|
box.scrollTo({ x: 0, y: offset });
|
|
78
79
|
await settle(setup);
|
|
80
|
+
|
|
79
81
|
if (setup.captureCharFrame().includes("row 130")) break;
|
|
80
82
|
}
|
|
81
83
|
const target = locateText(setup, "row 130");
|
|
@@ -127,6 +129,7 @@ test("deleted file caret scrolls one row and stays at the file boundaries", asyn
|
|
|
127
129
|
.findIndex((line) => line.includes("▎"));
|
|
128
130
|
|
|
129
131
|
if (marker === box.viewport.screenY) anchored = true;
|
|
132
|
+
|
|
130
133
|
if (anchored && box.scrollTop > 0) expect(marker).toBe(box.viewport.screenY);
|
|
131
134
|
}
|
|
132
135
|
expect(anchored).toBe(true);
|