@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
|
@@ -281,6 +281,7 @@ class Controller implements ReviewController {
|
|
|
281
281
|
|
|
282
282
|
subscribe = (listener: () => void): (() => void) => {
|
|
283
283
|
this.listeners.add(listener);
|
|
284
|
+
|
|
284
285
|
return () => this.listeners.delete(listener);
|
|
285
286
|
};
|
|
286
287
|
|
|
@@ -298,11 +299,13 @@ class Controller implements ReviewController {
|
|
|
298
299
|
this.options.openClient ??
|
|
299
300
|
(() => DaemonClient.connect({ home: this.options.home, autostart: true }));
|
|
300
301
|
const client = await openClient();
|
|
302
|
+
|
|
301
303
|
if (this.closed) return void client.close();
|
|
302
304
|
this.client = client;
|
|
303
305
|
client.onEvent((event) => {
|
|
304
306
|
// another controller/observer changed state: re-fetch
|
|
305
307
|
const session = this.snapshot.session;
|
|
308
|
+
|
|
306
309
|
if (session && event.sessionId === session.id) void this.refreshSession(session.id);
|
|
307
310
|
if (!this.options.sessionId) void this.refreshInbox();
|
|
308
311
|
});
|
|
@@ -353,11 +356,13 @@ class Controller implements ReviewController {
|
|
|
353
356
|
// ── derived projections ─────────────────────
|
|
354
357
|
private ensureDerived(): void {
|
|
355
358
|
const session = this.snapshot.session;
|
|
359
|
+
|
|
356
360
|
if (this.derivedFor === session) return;
|
|
357
361
|
this.derivedFor = session;
|
|
358
362
|
const rows =
|
|
359
363
|
session && session.artifact.type === "diff" ? diffRows(session.artifact.content) : [];
|
|
360
364
|
const models = new Map<string, FileDiffMetadata>();
|
|
365
|
+
|
|
361
366
|
for (const file of session?.artifact.files ?? []) models.set(file.path, parseFileDiff(file));
|
|
362
367
|
this.derived = {
|
|
363
368
|
display: session ? buildDisplay(session.artifact.content, session.workingCopy) : [],
|
|
@@ -369,21 +374,25 @@ class Controller implements ReviewController {
|
|
|
369
374
|
|
|
370
375
|
display(): DisplayBlock[] {
|
|
371
376
|
this.ensureDerived();
|
|
377
|
+
|
|
372
378
|
return this.derived.display;
|
|
373
379
|
}
|
|
374
380
|
|
|
375
381
|
rows(): DiffRow[] {
|
|
376
382
|
this.ensureDerived();
|
|
383
|
+
|
|
377
384
|
return this.derived.rows;
|
|
378
385
|
}
|
|
379
386
|
|
|
380
387
|
files(): WalkFile[] {
|
|
381
388
|
this.ensureDerived();
|
|
389
|
+
|
|
382
390
|
return this.derived.files;
|
|
383
391
|
}
|
|
384
392
|
|
|
385
393
|
working(): string {
|
|
386
394
|
const session = this.snapshot.session;
|
|
395
|
+
|
|
387
396
|
return session ? (session.workingCopy ?? session.artifact.content) : "";
|
|
388
397
|
}
|
|
389
398
|
|
|
@@ -420,6 +429,7 @@ class Controller implements ReviewController {
|
|
|
420
429
|
this.locallyViewed.clear();
|
|
421
430
|
this.rejections = [];
|
|
422
431
|
const cached = this.snapshot.inbox?.find((candidate) => candidate.id === id);
|
|
432
|
+
|
|
423
433
|
if (cached) this.update({ session: cached });
|
|
424
434
|
else void this.refreshSession(id);
|
|
425
435
|
}
|
|
@@ -435,16 +445,20 @@ class Controller implements ReviewController {
|
|
|
435
445
|
|
|
436
446
|
setSelfName(name: string): void {
|
|
437
447
|
const session = this.snapshot.session;
|
|
448
|
+
|
|
438
449
|
if (!session) return;
|
|
439
450
|
this.apply(this.client!.sessionSetSelfName(session.id, name));
|
|
440
451
|
}
|
|
441
452
|
|
|
442
453
|
cut(displayIndex: number): void {
|
|
443
454
|
const session = this.snapshot.session;
|
|
455
|
+
|
|
444
456
|
if (!session || session.status === "resolved") return;
|
|
445
457
|
const block = this.display()[displayIndex];
|
|
458
|
+
|
|
446
459
|
if (!block) return;
|
|
447
460
|
const working = this.working();
|
|
461
|
+
|
|
448
462
|
if (block.type === "del") {
|
|
449
463
|
this.restoreDelBlock(block, displayIndex);
|
|
450
464
|
} else if (block.work) {
|
|
@@ -456,6 +470,7 @@ class Controller implements ReviewController {
|
|
|
456
470
|
/** Re-insert a cut plan block at the next surviving block's line (Cut toggle + rail undo). */
|
|
457
471
|
private restoreDelBlock(block: DisplayBlock, displayIndex: number): void {
|
|
458
472
|
const session = this.snapshot.session;
|
|
473
|
+
|
|
459
474
|
if (!session || !block.base) return;
|
|
460
475
|
const working = this.working();
|
|
461
476
|
const line = restoreLine(
|
|
@@ -465,6 +480,7 @@ class Controller implements ReviewController {
|
|
|
465
480
|
// restoreBlock returns undefined when the block structure round-trips to the
|
|
466
481
|
// submitted revision - the working copy is back to pristine and is dropped
|
|
467
482
|
const restored = restoreBlock(session.artifact.content, working, block.base, line);
|
|
483
|
+
|
|
468
484
|
this.setWorkingCopy(restored);
|
|
469
485
|
this.setStatus(REMOVAL_RESTORED_STATUS);
|
|
470
486
|
}
|
|
@@ -473,27 +489,36 @@ class Controller implements ReviewController {
|
|
|
473
489
|
/** The parsed model and row for a curation action, or null with a status set. */
|
|
474
490
|
private curationRow(rowIndex: number): { row: DiffRow; model: FileDiffMetadata } | null {
|
|
475
491
|
const session = this.snapshot.session;
|
|
492
|
+
|
|
476
493
|
if (!session || session.status === "resolved") return null;
|
|
477
494
|
if (!session.artifact.files) {
|
|
478
495
|
this.setStatus("hunk curation needs full file contents (PR diffs cannot be curated)");
|
|
496
|
+
|
|
479
497
|
return null;
|
|
480
498
|
}
|
|
481
499
|
const row = this.rows()[rowIndex];
|
|
500
|
+
|
|
482
501
|
if (!row || row.kind === "file" || row.kind === "hunk") {
|
|
483
502
|
this.setStatus("move to a code line to curate");
|
|
503
|
+
|
|
484
504
|
return null;
|
|
485
505
|
}
|
|
486
506
|
const model = this.derived.models.get(row.file);
|
|
507
|
+
|
|
487
508
|
if (!model) return null;
|
|
509
|
+
|
|
488
510
|
return { row, model };
|
|
489
511
|
}
|
|
490
512
|
|
|
491
513
|
toggleRejectHunk(rowIndex: number): void {
|
|
492
514
|
const located = this.curationRow(rowIndex);
|
|
515
|
+
|
|
493
516
|
if (!located) return;
|
|
494
517
|
const target = hunkRejectionForRow(located.row.file, located.model, located.row);
|
|
518
|
+
|
|
495
519
|
if (!target) return this.setStatus("no hunk under the cursor");
|
|
496
520
|
const wholeHunk = (rejection: HunkRejection): boolean => rejectsWholeHunk(rejection, target);
|
|
521
|
+
|
|
497
522
|
if (this.rejections.some(wholeHunk)) {
|
|
498
523
|
this.rejections = this.rejections.filter((rejection) => !wholeHunk(rejection));
|
|
499
524
|
this.setStatus("hunk restored");
|
|
@@ -511,12 +536,15 @@ class Controller implements ReviewController {
|
|
|
511
536
|
|
|
512
537
|
toggleRejectChange(rowIndex: number): void {
|
|
513
538
|
const located = this.curationRow(rowIndex);
|
|
539
|
+
|
|
514
540
|
if (!located) return;
|
|
515
541
|
if (located.row.kind !== "add" && located.row.kind !== "del")
|
|
516
542
|
return this.setStatus("move to a changed line to reject a change");
|
|
517
543
|
const target = changeRejectionForRow(located.row.file, located.model, located.row);
|
|
544
|
+
|
|
518
545
|
if (!target) return this.setStatus("no change under the cursor");
|
|
519
546
|
const wholeCovers = this.rejections.some((rejection) => rejectsWholeHunk(rejection, target));
|
|
547
|
+
|
|
520
548
|
if (wholeCovers) return this.setStatus("the whole hunk is rejected - restore it first");
|
|
521
549
|
if (this.rejections.some((rejection) => sameRejection(rejection, target))) {
|
|
522
550
|
this.rejections = this.rejections.filter((rejection) => !sameRejection(rejection, target));
|
|
@@ -531,6 +559,7 @@ class Controller implements ReviewController {
|
|
|
531
559
|
/** Recompute the curated patch and push it as the working copy (or clear it). */
|
|
532
560
|
private recomputeCuration(): void {
|
|
533
561
|
const files = this.snapshot.session?.artifact.files;
|
|
562
|
+
|
|
534
563
|
if (!files) return;
|
|
535
564
|
// no decisions left = the working copy reverts to the full submitted diff
|
|
536
565
|
this.setWorkingCopy(this.rejections.length ? curateDiff(files, this.rejections) : undefined);
|
|
@@ -542,17 +571,22 @@ class Controller implements ReviewController {
|
|
|
542
571
|
this.ensureDerived();
|
|
543
572
|
if (!this.rejections.length) return EMPTY_REJECTED_ROWS;
|
|
544
573
|
const rejected = new Set<number>();
|
|
574
|
+
|
|
545
575
|
this.derived.rows.forEach((row, index) => {
|
|
546
576
|
const model = this.derived.models.get(row.file);
|
|
577
|
+
|
|
547
578
|
if (model && isRowRejected(row.file, model, row, this.rejections)) rejected.add(index);
|
|
548
579
|
});
|
|
580
|
+
|
|
549
581
|
return rejected;
|
|
550
582
|
}
|
|
551
583
|
|
|
552
584
|
curationItems(): CurationItem[] {
|
|
553
585
|
this.ensureDerived();
|
|
554
586
|
const session = this.snapshot.session;
|
|
587
|
+
|
|
555
588
|
if (!session) return EMPTY_CURATION_ITEMS;
|
|
589
|
+
|
|
556
590
|
return session.artifact.type === "diff" ? this.diffCurationItems() : this.planCurationItems();
|
|
557
591
|
}
|
|
558
592
|
|
|
@@ -560,16 +594,19 @@ class Controller implements ReviewController {
|
|
|
560
594
|
// diff rejections drop from the reject list; a plan cut re-inserts its block
|
|
561
595
|
if (id.startsWith("diff:")) {
|
|
562
596
|
const kept = this.rejections.filter((rejection) => curationItemId(rejection) !== id);
|
|
597
|
+
|
|
563
598
|
if (kept.length === this.rejections.length) return;
|
|
564
599
|
this.rejections = kept;
|
|
565
600
|
this.setStatus(REMOVAL_RESTORED_STATUS);
|
|
566
601
|
this.recomputeCuration();
|
|
602
|
+
|
|
567
603
|
return;
|
|
568
604
|
}
|
|
569
605
|
const display = this.display();
|
|
570
606
|
const displayIndex = display.findIndex(
|
|
571
607
|
(block) => block.type === "del" && block.base && planCutId(block.base) === id,
|
|
572
608
|
);
|
|
609
|
+
|
|
573
610
|
if (displayIndex !== -1) this.restoreDelBlock(display[displayIndex]!, displayIndex);
|
|
574
611
|
}
|
|
575
612
|
|
|
@@ -577,9 +614,11 @@ class Controller implements ReviewController {
|
|
|
577
614
|
private diffCurationItems(): CurationItem[] {
|
|
578
615
|
if (!this.rejections.length) return EMPTY_CURATION_ITEMS;
|
|
579
616
|
const items: CurationItem[] = [];
|
|
617
|
+
|
|
580
618
|
for (const rejection of this.rejections) {
|
|
581
619
|
const rows = this.rowsForRejection(rejection);
|
|
582
620
|
const firstRow = rows[0];
|
|
621
|
+
|
|
583
622
|
items.push({
|
|
584
623
|
id: curationItemId(rejection),
|
|
585
624
|
source: "diff",
|
|
@@ -589,12 +628,14 @@ class Controller implements ReviewController {
|
|
|
589
628
|
revealIndex: firstRow ? this.derived.rows.indexOf(firstRow) : 0,
|
|
590
629
|
});
|
|
591
630
|
}
|
|
631
|
+
|
|
592
632
|
return items;
|
|
593
633
|
}
|
|
594
634
|
|
|
595
635
|
/** The cut plan blocks as removal cards, in document order. */
|
|
596
636
|
private planCurationItems(): CurationItem[] {
|
|
597
637
|
const items: CurationItem[] = [];
|
|
638
|
+
|
|
598
639
|
this.derived.display.forEach((block, displayIndex) => {
|
|
599
640
|
if (block.type !== "del" || !block.base) return;
|
|
600
641
|
items.push({
|
|
@@ -604,32 +645,40 @@ class Controller implements ReviewController {
|
|
|
604
645
|
revealIndex: displayIndex,
|
|
605
646
|
});
|
|
606
647
|
});
|
|
648
|
+
|
|
607
649
|
return items.length ? items : EMPTY_CURATION_ITEMS;
|
|
608
650
|
}
|
|
609
651
|
|
|
610
652
|
/** The change/deletion rows a rejection covers, for its preview and reveal row. */
|
|
611
653
|
private rowsForRejection(rejection: HunkRejection): DiffRow[] {
|
|
612
654
|
const model = this.derived.models.get(rejection.path);
|
|
655
|
+
|
|
613
656
|
if (!model) return [];
|
|
614
657
|
const rows: DiffRow[] = [];
|
|
658
|
+
|
|
615
659
|
for (const row of this.derived.rows) {
|
|
616
660
|
if (row.file !== rejection.path || (row.kind !== "add" && row.kind !== "del")) continue;
|
|
617
661
|
if (rejection.changeIndex === undefined) {
|
|
618
662
|
const target = hunkRejectionForRow(rejection.path, model, row);
|
|
663
|
+
|
|
619
664
|
if (target && target.hunkIndex === rejection.hunkIndex) rows.push(row);
|
|
620
665
|
} else {
|
|
621
666
|
const target = changeRejectionForRow(rejection.path, model, row);
|
|
667
|
+
|
|
622
668
|
if (target && sameRejection(target, rejection)) rows.push(row);
|
|
623
669
|
}
|
|
624
670
|
}
|
|
671
|
+
|
|
625
672
|
return rows;
|
|
626
673
|
}
|
|
627
674
|
|
|
628
675
|
edit(): void {
|
|
629
676
|
const session = this.snapshot.session;
|
|
677
|
+
|
|
630
678
|
if (!session || session.status === "resolved") return;
|
|
631
679
|
try {
|
|
632
680
|
const result = editInEditor(this.working(), "plan.md", { editor: this.editor });
|
|
681
|
+
|
|
633
682
|
if (result.changed) {
|
|
634
683
|
this.setWorkingCopy(result.content);
|
|
635
684
|
this.reconcileAnnotations(session, result.content);
|
|
@@ -651,6 +700,7 @@ class Controller implements ReviewController {
|
|
|
651
700
|
const orphanCount = session.annotations.filter(
|
|
652
701
|
(annotation) => resolveAnchor(annotation.anchor, editedBlocks) === null,
|
|
653
702
|
).length;
|
|
703
|
+
|
|
654
704
|
this.update({ editOrphanCount: orphanCount });
|
|
655
705
|
}
|
|
656
706
|
|
|
@@ -662,8 +712,10 @@ class Controller implements ReviewController {
|
|
|
662
712
|
body: string,
|
|
663
713
|
): string | undefined {
|
|
664
714
|
const session = this.snapshot.session;
|
|
715
|
+
|
|
665
716
|
if (!session) return undefined;
|
|
666
717
|
let anchor;
|
|
718
|
+
|
|
667
719
|
if (session.artifact.type === "diff") {
|
|
668
720
|
anchor = { ...diffRowAnchor(this.rows(), displayIndex), blockIndex: displayIndex };
|
|
669
721
|
} else {
|
|
@@ -671,36 +723,45 @@ class Controller implements ReviewController {
|
|
|
671
723
|
const workBlocks = display.filter((entry) => entry.work).map((entry) => entry.work!);
|
|
672
724
|
const workBlockIndex =
|
|
673
725
|
display.slice(0, displayIndex + 1).filter((entry) => entry.work).length - 1;
|
|
726
|
+
|
|
674
727
|
anchor = makeAnchor(workBlocks, workBlockIndex, start, end);
|
|
675
728
|
}
|
|
676
729
|
const wire = { id: newAnnotationId(), kind, anchor, body };
|
|
677
730
|
const persisted = this.client!.sessionAnnotate(session.id, wire);
|
|
731
|
+
|
|
678
732
|
this.apply(persisted);
|
|
679
733
|
this.mirrorAnnotation(persisted, wire);
|
|
680
734
|
this.setStatus("comment added");
|
|
735
|
+
|
|
681
736
|
return wire.id;
|
|
682
737
|
}
|
|
683
738
|
|
|
684
739
|
annotatePrototype(selector: string, quote: string, body: string): string | undefined {
|
|
685
740
|
const session = this.snapshot.session;
|
|
741
|
+
|
|
686
742
|
if (!session) return undefined;
|
|
687
743
|
const anchor = { quote, prefix: "", suffix: "", selector };
|
|
688
744
|
const wire = { id: newAnnotationId(), kind: "comment", anchor, body };
|
|
689
745
|
const persisted = this.client!.sessionAnnotate(session.id, wire);
|
|
746
|
+
|
|
690
747
|
this.apply(persisted);
|
|
691
748
|
this.mirrorAnnotation(persisted, wire);
|
|
692
749
|
this.setStatus("comment added");
|
|
750
|
+
|
|
693
751
|
return wire.id;
|
|
694
752
|
}
|
|
695
753
|
|
|
696
754
|
updateAnnotation(id: string, body: string): void {
|
|
697
755
|
const session = this.snapshot.session;
|
|
756
|
+
|
|
698
757
|
if (!session) return;
|
|
699
758
|
const existing = session.annotations.find((annotation) => annotation.id === id);
|
|
759
|
+
|
|
700
760
|
if (!existing) return;
|
|
701
761
|
// the daemon's annotate verb upserts by id: same id + anchor, new body
|
|
702
762
|
const wire = { id: existing.id, kind: existing.kind, anchor: existing.anchor, body };
|
|
703
763
|
const persisted = this.client!.sessionAnnotate(session.id, wire);
|
|
764
|
+
|
|
704
765
|
this.apply(persisted);
|
|
705
766
|
this.mirrorAnnotation(persisted, wire);
|
|
706
767
|
this.setStatus("annotation updated");
|
|
@@ -712,12 +773,14 @@ class Controller implements ReviewController {
|
|
|
712
773
|
annotation: Omit<Annotation, "createdAt">,
|
|
713
774
|
): void {
|
|
714
775
|
const shareId = this.snapshot.session?.shareId;
|
|
776
|
+
|
|
715
777
|
if (!shareId) return;
|
|
716
778
|
void persisted.then(() => pushShare(shareId, [annotation])).catch(() => {});
|
|
717
779
|
}
|
|
718
780
|
|
|
719
781
|
removeAnnotation(id: string): void {
|
|
720
782
|
const session = this.snapshot.session;
|
|
783
|
+
|
|
721
784
|
if (!session) return;
|
|
722
785
|
this.apply(this.client!.sessionRemoveAnnotation(session.id, id));
|
|
723
786
|
this.setStatus("annotation deleted");
|
|
@@ -725,6 +788,7 @@ class Controller implements ReviewController {
|
|
|
725
788
|
|
|
726
789
|
setWorkingCopy(content: string | undefined): void {
|
|
727
790
|
const session = this.snapshot.session;
|
|
791
|
+
|
|
728
792
|
if (!session) return;
|
|
729
793
|
this.apply(this.client!.sessionSetWorkingCopy(session.id, content));
|
|
730
794
|
}
|
|
@@ -740,8 +804,10 @@ class Controller implements ReviewController {
|
|
|
740
804
|
|
|
741
805
|
walkStart(): void {
|
|
742
806
|
const session = this.snapshot.session;
|
|
807
|
+
|
|
743
808
|
if (!session || session.artifact.type !== "diff") return;
|
|
744
809
|
const files = this.files();
|
|
810
|
+
|
|
745
811
|
if (files.length === 0) return this.setStatus("nothing to walk - the diff is empty");
|
|
746
812
|
// resume at the first unviewed file; a finished walk reopens on the end card
|
|
747
813
|
this.update({ walk: { index: firstUnviewedIndex(files, this.viewedSet()) } });
|
|
@@ -750,9 +816,11 @@ class Controller implements ReviewController {
|
|
|
750
816
|
walkForward(): void {
|
|
751
817
|
const walk = this.snapshot.walk;
|
|
752
818
|
const session = this.snapshot.session;
|
|
819
|
+
|
|
753
820
|
if (!walk || !session) return;
|
|
754
821
|
const files = this.files();
|
|
755
822
|
const current = files[walk.index];
|
|
823
|
+
|
|
756
824
|
if (!current) return; // already on the end card
|
|
757
825
|
// advancing IS the viewed mark: the step is complete once you move past
|
|
758
826
|
// it; the daemon verb merges, so only the new path travels
|
|
@@ -765,6 +833,7 @@ class Controller implements ReviewController {
|
|
|
765
833
|
|
|
766
834
|
walkBack(): void {
|
|
767
835
|
const walk = this.snapshot.walk;
|
|
836
|
+
|
|
768
837
|
if (!walk) return;
|
|
769
838
|
this.update({ walk: { index: Math.max(0, walk.index - 1) } });
|
|
770
839
|
}
|
|
@@ -775,6 +844,7 @@ class Controller implements ReviewController {
|
|
|
775
844
|
|
|
776
845
|
submit(verdict: VerdictKind, summary: string): void {
|
|
777
846
|
const session = this.snapshot.session;
|
|
847
|
+
|
|
778
848
|
if (!session) return;
|
|
779
849
|
this.walkLeave();
|
|
780
850
|
this.client!.sessionResolve(session.id, verdict, summary)
|
|
@@ -797,6 +867,7 @@ class Controller implements ReviewController {
|
|
|
797
867
|
// countdown from DEFAULT_AUTO_CLOSE that closes on its own; esc stays,
|
|
798
868
|
// a remembers the choice. A configured delay overrides; 0 closes now.
|
|
799
869
|
const delay = this.autoClose;
|
|
870
|
+
|
|
800
871
|
if (delay === 0) this.finishReview();
|
|
801
872
|
else if (typeof delay === "number") this.startCounting(delay);
|
|
802
873
|
else this.startCounting(DEFAULT_AUTO_CLOSE);
|
|
@@ -808,12 +879,14 @@ class Controller implements ReviewController {
|
|
|
808
879
|
|
|
809
880
|
share(): void {
|
|
810
881
|
const session = this.snapshot.session;
|
|
882
|
+
|
|
811
883
|
if (!session) return;
|
|
812
884
|
this.setStatus("sharing…");
|
|
813
885
|
publishShare(session)
|
|
814
886
|
.then(async ({ line, copied }) => {
|
|
815
887
|
// Stamp the id back so a later pull knows which share to collect from.
|
|
816
888
|
const shareId = shareIdFromLine(line);
|
|
889
|
+
|
|
817
890
|
if (shareId && this.client) await this.client.sessionSetShareId(session.id, shareId);
|
|
818
891
|
this.setStatus("");
|
|
819
892
|
this.showToast(line, copied ? "share link copied" : "share link");
|
|
@@ -831,8 +904,10 @@ class Controller implements ReviewController {
|
|
|
831
904
|
*/
|
|
832
905
|
pullShared(): Promise<void> {
|
|
833
906
|
const session = this.snapshot.session;
|
|
907
|
+
|
|
834
908
|
if (!session?.shareId || !this.client) return Promise.resolve();
|
|
835
909
|
const client = this.client;
|
|
910
|
+
|
|
836
911
|
return pullShare(session.shareId)
|
|
837
912
|
.then((remote) =>
|
|
838
913
|
client.sessionMergeShared(session.id, {
|
|
@@ -853,6 +928,7 @@ class Controller implements ReviewController {
|
|
|
853
928
|
this.stopSharePoll();
|
|
854
929
|
// per-run token so a stale run's in-flight pull never re-arms the poll
|
|
855
930
|
const run = {};
|
|
931
|
+
|
|
856
932
|
this.shareRun = run;
|
|
857
933
|
const tick = (): void => {
|
|
858
934
|
void this.pullShared().finally(() => {
|
|
@@ -860,7 +936,9 @@ class Controller implements ReviewController {
|
|
|
860
936
|
this.sharePoll = this.clock.setTimeout(tick, SHARE_POLL_MS);
|
|
861
937
|
});
|
|
862
938
|
};
|
|
939
|
+
|
|
863
940
|
tick();
|
|
941
|
+
|
|
864
942
|
return () => {
|
|
865
943
|
if (this.shareRun === run) this.stopSharePoll();
|
|
866
944
|
};
|
|
@@ -883,6 +961,7 @@ class Controller implements ReviewController {
|
|
|
883
961
|
this.clearCountdown();
|
|
884
962
|
const herdr = detectHerdr();
|
|
885
963
|
const pane = returnPaneFor(this.snapshot.session?.artifact.meta.herdrPane);
|
|
964
|
+
|
|
886
965
|
if (herdr && pane) focusHerdrPane(herdr.binPath, pane);
|
|
887
966
|
this.options.onExit?.(0);
|
|
888
967
|
}
|
package/src/share.ts
CHANGED
|
@@ -42,11 +42,14 @@ export async function publishShare(
|
|
|
42
42
|
packSessionBlob(session),
|
|
43
43
|
target,
|
|
44
44
|
);
|
|
45
|
+
|
|
45
46
|
if (code !== 0)
|
|
46
47
|
throw new Error(`gateway upload failed: ${stderr.trim() || `ssh exited ${code}`}`);
|
|
47
48
|
const line = stdout.trim();
|
|
49
|
+
|
|
48
50
|
if (!line.startsWith("ssh ")) throw new Error(`unexpected gateway reply: ${line || "(empty)"}`);
|
|
49
51
|
const copied = await copyToClipboard(line);
|
|
52
|
+
|
|
50
53
|
return { line, copied };
|
|
51
54
|
}
|
|
52
55
|
|
|
@@ -57,7 +60,9 @@ export async function publishShare(
|
|
|
57
60
|
*/
|
|
58
61
|
export async function pullShare(shareId: string, target: ShareTarget = {}): Promise<ReviewSession> {
|
|
59
62
|
const { stdout, stderr, code } = await runShareSsh("cueloop-pull", Buffer.from(shareId), target);
|
|
63
|
+
|
|
60
64
|
if (code !== 0) throw new Error(`gateway pull failed: ${stderr.trim() || `ssh exited ${code}`}`);
|
|
65
|
+
|
|
61
66
|
return JSON.parse(stdout) as ReviewSession;
|
|
62
67
|
}
|
|
63
68
|
|
|
@@ -81,6 +86,7 @@ export async function pushShare(
|
|
|
81
86
|
Buffer.from(JSON.stringify({ shareId, annotations })),
|
|
82
87
|
target,
|
|
83
88
|
);
|
|
89
|
+
|
|
84
90
|
if (code !== 0) throw new Error(`gateway push failed: ${stderr.trim() || `ssh exited ${code}`}`);
|
|
85
91
|
}
|
|
86
92
|
|
|
@@ -109,5 +115,6 @@ async function runShareSsh(
|
|
|
109
115
|
new Response(proc.stderr).text(),
|
|
110
116
|
proc.exited,
|
|
111
117
|
]);
|
|
118
|
+
|
|
112
119
|
return { stdout, stderr, code };
|
|
113
120
|
}
|
package/src/test-support.test.ts
CHANGED
|
@@ -10,14 +10,17 @@ describe("isolateUserConfig", () => {
|
|
|
10
10
|
// Arrange
|
|
11
11
|
const home = mkdtempSync(join(tmpdir(), "cueloop-isolate-"));
|
|
12
12
|
const userConfigPath = join(home, "config.toml");
|
|
13
|
+
|
|
13
14
|
writeFileSync(userConfigPath, '[ui]\nreview_state = "compact"\n');
|
|
14
15
|
const priorEnv = process.env.CUELOOP_CONFIG;
|
|
16
|
+
|
|
15
17
|
process.env.CUELOOP_CONFIG = userConfigPath;
|
|
16
18
|
|
|
17
19
|
try {
|
|
18
20
|
// Act
|
|
19
21
|
const restoreUserConfig = isolateUserConfig(home);
|
|
20
22
|
const isolated = loadConfig({ repoRoot: home });
|
|
23
|
+
|
|
21
24
|
restoreUserConfig();
|
|
22
25
|
const restored = loadConfig({ repoRoot: home });
|
|
23
26
|
|
|
@@ -36,12 +39,14 @@ describe("isolateUserConfig", () => {
|
|
|
36
39
|
// Arrange
|
|
37
40
|
const home = mkdtempSync(join(tmpdir(), "cueloop-isolate-"));
|
|
38
41
|
const priorEnv = process.env.CUELOOP_CONFIG;
|
|
42
|
+
|
|
39
43
|
delete process.env.CUELOOP_CONFIG;
|
|
40
44
|
|
|
41
45
|
try {
|
|
42
46
|
// Act
|
|
43
47
|
const restoreUserConfig = isolateUserConfig(home);
|
|
44
48
|
const pointedAt = process.env.CUELOOP_CONFIG ?? "";
|
|
49
|
+
|
|
45
50
|
restoreUserConfig();
|
|
46
51
|
|
|
47
52
|
// Assert
|
package/src/test-support.ts
CHANGED
|
@@ -22,7 +22,9 @@ export const WAIT_PASSES = { maxPasses: 400 };
|
|
|
22
22
|
*/
|
|
23
23
|
export function isolateUserConfig(home: string, fileName = "no-config.toml"): () => void {
|
|
24
24
|
const priorUserConfig = process.env.CUELOOP_CONFIG;
|
|
25
|
+
|
|
25
26
|
process.env.CUELOOP_CONFIG = join(home, fileName);
|
|
27
|
+
|
|
26
28
|
return () => {
|
|
27
29
|
if (priorUserConfig === undefined) delete process.env.CUELOOP_CONFIG;
|
|
28
30
|
else process.env.CUELOOP_CONFIG = priorUserConfig;
|
|
@@ -102,9 +104,11 @@ async function waitForFramePredicate(
|
|
|
102
104
|
let frameChanges = 0;
|
|
103
105
|
let lastFrame = "";
|
|
104
106
|
let lastLog = start;
|
|
107
|
+
|
|
105
108
|
for (;;) {
|
|
106
109
|
await setup.renderOnce();
|
|
107
110
|
const frame = setup.captureCharFrame();
|
|
111
|
+
|
|
108
112
|
polls++;
|
|
109
113
|
if (frame !== lastFrame) {
|
|
110
114
|
frameChanges++;
|
|
@@ -112,8 +116,10 @@ async function waitForFramePredicate(
|
|
|
112
116
|
}
|
|
113
117
|
if (predicate(frame)) return frame;
|
|
114
118
|
const now = Date.now();
|
|
119
|
+
|
|
115
120
|
if (now > deadline) {
|
|
116
121
|
const frozen = frameChanges <= 1 ? " - render never advanced (frozen)" : "";
|
|
122
|
+
|
|
117
123
|
throw new Error(
|
|
118
124
|
`waitFor ${label} timed out after ${now - start}ms (${polls} polls, ${frameChanges} frame changes${frozen}).\nlast frame:\n${frame}`,
|
|
119
125
|
);
|
|
@@ -141,10 +147,12 @@ export async function waitForState(
|
|
|
141
147
|
let frameChanges = 0;
|
|
142
148
|
let lastFrame = "";
|
|
143
149
|
let lastLog = start;
|
|
150
|
+
|
|
144
151
|
for (;;) {
|
|
145
152
|
if (predicate()) return;
|
|
146
153
|
await setup.renderOnce();
|
|
147
154
|
const frame = setup.captureCharFrame();
|
|
155
|
+
|
|
148
156
|
polls++;
|
|
149
157
|
if (frame !== lastFrame) {
|
|
150
158
|
frameChanges++;
|
|
@@ -152,8 +160,10 @@ export async function waitForState(
|
|
|
152
160
|
}
|
|
153
161
|
if (predicate()) return;
|
|
154
162
|
const now = Date.now();
|
|
163
|
+
|
|
155
164
|
if (now > deadline) {
|
|
156
165
|
const frozen = frameChanges <= 1 ? " - render never advanced (frozen)" : "";
|
|
166
|
+
|
|
157
167
|
throw new Error(
|
|
158
168
|
`waitForState ${label} timed out after ${now - start}ms (${polls} polls, ${frameChanges} frame changes${frozen}).\nlast frame:\n${frame}`,
|
|
159
169
|
);
|
package/src/theme-presets.ts
CHANGED
|
@@ -168,6 +168,7 @@ export const THEME_LABELS: Record<ThemeName, string> = {
|
|
|
168
168
|
*/
|
|
169
169
|
export function themeForName(name: string, appearance: Appearance = "dark"): Theme {
|
|
170
170
|
if (name === "cueloop") return brandedTheme(appearance);
|
|
171
|
+
|
|
171
172
|
return THEME_PRESETS[name as ThemeName] ?? brandedTheme(appearance);
|
|
172
173
|
}
|
|
173
174
|
|
|
@@ -57,6 +57,7 @@ function moveNavZone(
|
|
|
57
57
|
return { categoryId: categories[Math.max(0, categoryIndex - 1)]!.id, rowIndex: 0, zone: "nav" };
|
|
58
58
|
if (ENTER_BODY_KEYS.has(name))
|
|
59
59
|
return { categoryId: categories[categoryIndex]!.id, rowIndex: 0, zone: "body" };
|
|
60
|
+
|
|
60
61
|
return null;
|
|
61
62
|
}
|
|
62
63
|
|
|
@@ -64,6 +65,7 @@ function moveBodyRow(name: string, nav: SettingsNav, rowCount: number): Settings
|
|
|
64
65
|
if (DOWN_KEYS.has(name)) return { ...nav, rowIndex: Math.min(rowCount - 1, nav.rowIndex + 1) };
|
|
65
66
|
if (UP_KEYS.has(name)) return { ...nav, rowIndex: Math.max(0, nav.rowIndex - 1) };
|
|
66
67
|
if (name === "h" || name === "tab") return { ...nav, zone: "nav" };
|
|
68
|
+
|
|
67
69
|
return null;
|
|
68
70
|
}
|
|
69
71
|
|
|
@@ -198,15 +200,18 @@ export function useSettingsDialog(params: {
|
|
|
198
200
|
const cycleSetting = (rowKey: string): void => {
|
|
199
201
|
if (rowKey === "autoClose") {
|
|
200
202
|
const next: AutoClose = autoClose === "off" ? 3 : autoClose === 3 ? 10 : "off";
|
|
203
|
+
|
|
201
204
|
setAutoClose(next);
|
|
202
205
|
persistAutoClose(next);
|
|
203
206
|
} else if (rowKey === "reviewPanel") {
|
|
204
207
|
const order: ReviewPanelMode[] = ["expanded", "compact", "hidden"];
|
|
205
208
|
const next = order[(order.indexOf(reviewMode) + 1) % order.length]!;
|
|
209
|
+
|
|
206
210
|
setReviewMode(next);
|
|
207
211
|
controller.saveReviewPanel({ mode: next });
|
|
208
212
|
} else if (rowKey === "theme") {
|
|
209
213
|
const next = THEME_NAMES[(THEME_NAMES.indexOf(themeName) + 1) % THEME_NAMES.length]!;
|
|
214
|
+
|
|
210
215
|
setThemeName(next);
|
|
211
216
|
setTheme(composeTheme(next, themeOverrides, appearance));
|
|
212
217
|
persistTheme(next);
|
|
@@ -222,6 +227,7 @@ export function useSettingsDialog(params: {
|
|
|
222
227
|
// an open system-prompt input owns typing; only esc (close it) escapes here
|
|
223
228
|
if (actionsExpandedIndex !== null) {
|
|
224
229
|
if (name === "escape") setActionsExpandedIndex(null);
|
|
230
|
+
|
|
225
231
|
return;
|
|
226
232
|
}
|
|
227
233
|
if (name === "escape") return void setMenuDialog(null);
|
|
@@ -229,22 +235,28 @@ export function useSettingsDialog(params: {
|
|
|
229
235
|
(category) => category.id === settingsNav.categoryId,
|
|
230
236
|
);
|
|
231
237
|
const category = settingsCategories[categoryIndex]!;
|
|
238
|
+
|
|
232
239
|
if (settingsNav.zone === "nav") {
|
|
233
240
|
const next = moveNavZone(name, categoryIndex, settingsCategories);
|
|
241
|
+
|
|
234
242
|
if (next) setSettingsNav(next);
|
|
243
|
+
|
|
235
244
|
return;
|
|
236
245
|
}
|
|
237
246
|
// the Actions category is a list of quick actions plus a trailing "add" row
|
|
238
247
|
if (category.id === "actions") {
|
|
239
248
|
const moved = moveBodyRow(name, settingsNav, quickActions.length + 1);
|
|
249
|
+
|
|
240
250
|
if (moved) return void setSettingsNav(moved);
|
|
241
251
|
if (ACTIVATE_KEYS.has(name)) {
|
|
242
252
|
if (settingsNav.rowIndex === quickActions.length) addAction();
|
|
243
253
|
else setActionsExpandedIndex(settingsNav.rowIndex);
|
|
244
254
|
}
|
|
255
|
+
|
|
245
256
|
return;
|
|
246
257
|
}
|
|
247
258
|
const moved = moveBodyRow(name, settingsNav, category.rows.length);
|
|
259
|
+
|
|
248
260
|
if (moved) return void setSettingsNav(moved);
|
|
249
261
|
if (name === "return" || name === "space")
|
|
250
262
|
cycleSetting(category.rows[settingsNav.rowIndex]!.key);
|
package/src/view-diff.test.ts
CHANGED
|
@@ -28,9 +28,11 @@ describe("diffRows", () => {
|
|
|
28
28
|
expect(rows[0]).toMatchObject({ kind: "file", file: "src/store.ts" });
|
|
29
29
|
expect(rows[1]!.kind).toBe("hunk");
|
|
30
30
|
const del = rows.find((row) => row.kind === "del")!;
|
|
31
|
+
|
|
31
32
|
expect(del.text).toContain("private items = [];");
|
|
32
33
|
expect(del.oldLine).toBe(3);
|
|
33
34
|
const adds = rows.filter((row) => row.kind === "add");
|
|
35
|
+
|
|
34
36
|
expect(adds[0]!.text).toContain("new Map()");
|
|
35
37
|
expect(adds[0]!.newLine).toBe(3);
|
|
36
38
|
expect(adds[1]!.newLine).toBe(4);
|
|
@@ -44,6 +46,7 @@ describe("diffRows", () => {
|
|
|
44
46
|
|
|
45
47
|
// Assert
|
|
46
48
|
const ctx = rows.find((row) => row.kind === "ctx")!;
|
|
49
|
+
|
|
47
50
|
expect(ctx.oldLine).toBe(1);
|
|
48
51
|
expect(ctx.newLine).toBe(1);
|
|
49
52
|
});
|