@agimon-ai/doompi-author 0.0.1-alpha.14 → 0.0.1-alpha.16

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.
@@ -434,6 +434,9 @@ const TERMINAL_REQUEST_STATUSES = /* @__PURE__ */ new Set([
434
434
  const EMPTY_SESSION = {
435
435
  generation: 0,
436
436
  activeTool: "select",
437
+ annotationsByDocument: {},
438
+ annotations: [],
439
+ candidateText: "",
437
440
  regions: [],
438
441
  requests: []
439
442
  };
@@ -461,6 +464,46 @@ function authorSessionWorkspace(sessionId) {
461
464
  if (sessionId === null) return EMPTY_SESSION;
462
465
  return authorWorkspace.store.state.sessions[sessionId] ?? EMPTY_SESSION;
463
466
  }
467
+ function hydrateAuthorAnnotationCollections(records) {
468
+ authorWorkspace.update((state) => {
469
+ let sessions = state.sessions;
470
+ for (const record of records) {
471
+ const path = normalizeAuthorPath(record.documentPath);
472
+ if (path === "") continue;
473
+ const session = sessions[record.sessionId] ?? EMPTY_SESSION;
474
+ const previous = session.annotationsByDocument[path];
475
+ if (previous !== void 0 && previous.updatedAt >= record.collection.updatedAt) continue;
476
+ const document = state.documents[authorDocumentKey(record.sessionId, path)];
477
+ const stale = record.collection.stale || document !== void 0 && (document.version !== record.collection.revision || document.sourceSha256 !== record.collection.sourceSha256);
478
+ const collection = restoreCollectionEvidence({
479
+ ...record.collection,
480
+ stale
481
+ });
482
+ const visible = session.focusedDocument?.path === path && !stale;
483
+ const annotations = visible ? collection.annotations : session.annotations;
484
+ sessions = {
485
+ ...sessions,
486
+ [record.sessionId]: {
487
+ ...session,
488
+ annotationsByDocument: {
489
+ ...session.annotationsByDocument,
490
+ [path]: collection
491
+ },
492
+ ...visible ? {
493
+ annotations,
494
+ regions: annotations,
495
+ candidate: collection.candidate,
496
+ candidateText: collection.candidateText
497
+ } : {}
498
+ }
499
+ };
500
+ }
501
+ return sessions === state.sessions ? state : {
502
+ ...state,
503
+ sessions
504
+ };
505
+ });
506
+ }
464
507
  function putAuthorDocument(sessionId, input) {
465
508
  const path = normalizeAuthorPath(input.path);
466
509
  if (path === "") throw new Error("Author document path must not be empty");
@@ -484,9 +527,10 @@ function putAuthorDocument(sessionId, input) {
484
527
  const session = state.sessions[sessionId];
485
528
  let sessions = state.sessions;
486
529
  if (sourceChanged && session?.focusedDocument?.path === path) {
487
- staleThumbnails.push(...session.candidate?.thumbnailUrl === void 0 ? [] : [session.candidate.thumbnailUrl], ...session.regions.flatMap((region) => region.thumbnailUrl === void 0 ? [] : [region.thumbnailUrl]));
530
+ staleThumbnails.push(...session.candidate?.thumbnailUrl === void 0 ? [] : [session.candidate.thumbnailUrl], ...session.annotations.flatMap((annotation) => annotation.thumbnailUrl === void 0 ? [] : [annotation.thumbnailUrl]));
488
531
  const now = Date.now();
489
532
  const generation = session.generation + 1;
533
+ const staleCollection = staleAuthorCollection(session.annotationsByDocument[path], now);
490
534
  sessions = {
491
535
  ...sessions,
492
536
  [sessionId]: {
@@ -499,8 +543,14 @@ function putAuthorDocument(sessionId, input) {
499
543
  sourceSha256: input.sourceSha256,
500
544
  focusedAt: now
501
545
  },
546
+ annotationsByDocument: {
547
+ ...session.annotationsByDocument,
548
+ ...staleCollection && { [path]: staleCollection }
549
+ },
550
+ annotations: [],
502
551
  regions: [],
503
552
  candidate: void 0,
553
+ candidateText: "",
504
554
  requests: session.requests.map((request) => input.kind !== "story-preview" && request.documentPath === path && (request.status === "REQUESTED" || request.status === "CHANGING" || request.status === "CHANGED") ? {
505
555
  ...request,
506
556
  status: "FAILED",
@@ -510,6 +560,20 @@ function putAuthorDocument(sessionId, input) {
510
560
  } : request)
511
561
  }
512
562
  };
563
+ } else if (sourceChanged && session?.annotationsByDocument[path] !== void 0) {
564
+ const collection = session.annotationsByDocument[path];
565
+ if (collection.candidate?.thumbnailUrl !== void 0) staleThumbnails.push(collection.candidate.thumbnailUrl);
566
+ staleThumbnails.push(...collection.annotations.flatMap((annotation) => annotation.thumbnailUrl === void 0 ? [] : [annotation.thumbnailUrl]));
567
+ sessions = {
568
+ ...sessions,
569
+ [sessionId]: {
570
+ ...session,
571
+ annotationsByDocument: {
572
+ ...session.annotationsByDocument,
573
+ [path]: staleAuthorCollection(collection, Date.now())
574
+ }
575
+ }
576
+ };
513
577
  }
514
578
  return {
515
579
  documents: {
@@ -525,13 +589,13 @@ function putAuthorDocument(sessionId, input) {
525
589
  /** Claims focus and returns a generation token that makes cleanup race-safe. */
526
590
  function focusAuthorDocument(sessionId, path, revision, sourceSha256) {
527
591
  const normalized = normalizeAuthorPath(path);
528
- const staleThumbnails = [];
529
592
  let generation = 0;
530
593
  authorWorkspace.update((state) => {
531
594
  const session = state.sessions[sessionId] ?? EMPTY_SESSION;
532
595
  generation = session.generation + 1;
533
- const keepRegions = session.focusedDocument?.path === normalized;
534
- if (!keepRegions) staleThumbnails.push(...session.candidate?.thumbnailUrl === void 0 ? [] : [session.candidate.thumbnailUrl], ...session.regions.flatMap((region) => region.thumbnailUrl === void 0 ? [] : [region.thumbnailUrl]));
596
+ const stored = session.annotationsByDocument[normalized];
597
+ const current = stored !== void 0 && stored.revision === revision && stored.sourceSha256 === sourceSha256 && !stored.stale ? stored : void 0;
598
+ const annotations = current?.annotations ?? [];
535
599
  return {
536
600
  ...state,
537
601
  sessions: {
@@ -546,13 +610,14 @@ function focusAuthorDocument(sessionId, path, revision, sourceSha256) {
546
610
  sourceSha256,
547
611
  focusedAt: Date.now()
548
612
  },
549
- regions: keepRegions ? session.regions : [],
550
- candidate: keepRegions ? session.candidate : void 0
613
+ annotations,
614
+ regions: annotations,
615
+ candidate: current?.candidate,
616
+ candidateText: current?.candidateText ?? ""
551
617
  }
552
618
  }
553
619
  };
554
620
  });
555
- staleThumbnails.forEach(revokeThumbnail);
556
621
  return generation;
557
622
  }
558
623
  function syncAuthorDocumentFocus(sessionId, generation, revision, sourceSha256) {
@@ -571,18 +636,17 @@ function syncAuthorDocumentFocus(sessionId, generation, revision, sourceSha256)
571
636
  });
572
637
  }
573
638
  function releaseAuthorDocumentFocus(sessionId, generation) {
574
- const staleThumbnails = [];
575
639
  updateSession(sessionId, (session) => {
576
640
  if (session.focusedDocument?.generation !== generation) return session;
577
- staleThumbnails.push(...session.candidate?.thumbnailUrl === void 0 ? [] : [session.candidate.thumbnailUrl], ...session.regions.flatMap((region) => region.thumbnailUrl === void 0 ? [] : [region.thumbnailUrl]));
578
641
  return {
579
642
  ...session,
580
643
  focusedDocument: void 0,
581
644
  candidate: void 0,
645
+ candidateText: "",
646
+ annotations: [],
582
647
  regions: []
583
648
  };
584
649
  });
585
- staleThumbnails.forEach(revokeThumbnail);
586
650
  }
587
651
  function seekAuthorVideo(sessionId, timeSeconds) {
588
652
  const workspace = authorSessionWorkspace(sessionId);
@@ -614,51 +678,96 @@ function setAuthorRegionCandidate(sessionId, candidate) {
614
678
  if (focused === void 0 || focused.path !== normalizeAuthorPath(candidate.documentPath)) throw new Error("The Author selection does not belong to the focused document.");
615
679
  if (focused.revision !== candidate.revision || focused.sourceSha256 !== candidate.sourceSha256) throw new Error("The Author selection is stale for the focused document.");
616
680
  }
681
+ if (focused === void 0) return session;
617
682
  staleThumbnail = session.candidate?.thumbnailUrl;
618
- return {
683
+ const copied = candidate === void 0 ? void 0 : copyCandidate(candidate);
684
+ return withFocusedCollection({
619
685
  ...session,
620
- candidate: candidate === void 0 ? void 0 : structuredClone(candidate)
621
- };
686
+ candidate: copied
687
+ }, focused.path, { candidate: copied });
622
688
  });
623
689
  if (staleThumbnail !== void 0 && staleThumbnail !== candidate?.thumbnailUrl) revokeThumbnail(staleThumbnail);
624
690
  }
691
+ function setAuthorCandidateText(sessionId, candidateText) {
692
+ if (new TextEncoder().encode(candidateText).byteLength > 2048) throw new Error("Author candidate text must not exceed 2 KiB.");
693
+ updateSession(sessionId, (session) => {
694
+ const path = session.focusedDocument?.path;
695
+ return path === void 0 ? session : withFocusedCollection({
696
+ ...session,
697
+ candidateText
698
+ }, path, { candidateText });
699
+ });
700
+ }
625
701
  function commitAuthorRegion(sessionId, comment) {
626
702
  const id = crypto.randomUUID();
627
703
  const session = authorSessionWorkspace(sessionId);
628
- if (session.candidate === void 0) throw new Error("Select a document region before adding a comment.");
704
+ if (session.candidate === void 0) throw new Error("Select a document annotation before adding a comment.");
629
705
  addAuthorRegion(sessionId, {
630
706
  ...session.candidate,
631
707
  id,
632
- comment
708
+ comment,
709
+ version: 1
710
+ });
711
+ updateSession(sessionId, (current) => {
712
+ const path = current.focusedDocument?.path;
713
+ return path === void 0 ? current : withFocusedCollection({
714
+ ...current,
715
+ candidate: void 0,
716
+ candidateText: ""
717
+ }, path, {
718
+ candidate: void 0,
719
+ candidateText: ""
720
+ });
633
721
  });
634
- updateSession(sessionId, (current) => ({
635
- ...current,
636
- candidate: void 0
637
- }));
638
722
  return id;
639
723
  }
640
724
  function addAuthorRegion(sessionId, region) {
641
- if (region.comment.trim() === "") throw new Error("Every Author region requires a comment.");
725
+ if (region.comment.trim() === "") throw new Error("Every Author annotation requires a comment.");
642
726
  updateSession(sessionId, (session) => {
643
727
  const focused = session.focusedDocument;
644
- if (focused === void 0 || focused.path !== normalizeAuthorPath(region.documentPath)) throw new Error("The Author region does not belong to the focused document.");
645
- if (focused.revision !== region.revision || focused.sourceSha256 !== region.sourceSha256) throw new Error("The Author region is stale for the focused document.");
646
- if (session.regions.length >= 16) throw new Error(`Author requests support at most 16 regions.`);
647
- if (session.regions.some((candidate) => candidate.id === region.id)) throw new Error(`Author region '${region.id}' already exists.`);
648
- return {
728
+ if (focused === void 0 || focused.path !== normalizeAuthorPath(region.documentPath)) throw new Error("The Author annotation does not belong to the focused document.");
729
+ if (focused.revision !== region.revision || focused.sourceSha256 !== region.sourceSha256) throw new Error("The Author annotation is stale for the focused document.");
730
+ if (session.annotations.length >= 16) throw new Error(`Author requests support at most 16 annotations.`);
731
+ if (session.annotations.some((candidate) => candidate.id === region.id)) throw new Error(`Author annotation '${region.id}' already exists.`);
732
+ const annotations = [...session.annotations, copyRegion({
733
+ ...region,
734
+ version: region.version ?? 1
735
+ })];
736
+ return withFocusedCollection({
649
737
  ...session,
650
- regions: [...session.regions, copyRegion(region)]
651
- };
738
+ annotations,
739
+ regions: annotations
740
+ }, focused.path, { annotations });
652
741
  });
653
742
  }
654
- function removeAuthorRegion(sessionId, regionId) {
743
+ function removeAuthorRegion(sessionId, regionId, expectedVersion, documentPath) {
655
744
  let thumbnail;
656
745
  updateSession(sessionId, (session) => {
657
- thumbnail = session.regions.find((candidate) => candidate.id === regionId)?.thumbnailUrl;
658
- const regions = session.regions.filter((candidate) => candidate.id !== regionId);
659
- return regions.length === session.regions.length ? session : {
746
+ const path = documentPath === void 0 ? session.focusedDocument?.path : normalizeAuthorPath(documentPath);
747
+ if (path === void 0) return session;
748
+ const focused = session.focusedDocument?.path === path;
749
+ const collection = session.annotationsByDocument[path];
750
+ const current = focused ? session.annotations : collection?.annotations;
751
+ if (current === void 0) return session;
752
+ const annotation = current.find((candidate) => candidate.id === regionId);
753
+ if (annotation === void 0 || expectedVersion !== void 0 && (annotation.version ?? 1) !== expectedVersion) return session;
754
+ thumbnail = annotation.thumbnailUrl;
755
+ const annotations = current.filter((candidate) => candidate.id !== regionId);
756
+ if (focused) return withFocusedCollection({
757
+ ...session,
758
+ annotations,
759
+ regions: annotations
760
+ }, path, { annotations });
761
+ return {
660
762
  ...session,
661
- regions
763
+ annotationsByDocument: {
764
+ ...session.annotationsByDocument,
765
+ [path]: {
766
+ ...collection,
767
+ annotations,
768
+ updatedAt: Date.now()
769
+ }
770
+ }
662
771
  };
663
772
  });
664
773
  if (thumbnail !== void 0) revokeThumbnail(thumbnail);
@@ -774,10 +883,12 @@ function completeAuthorSave(sessionId, path, sourceSha256, savedVersion, savedFr
774
883
  function dropAuthorSession(sessionId) {
775
884
  const prefix = `${sessionId}\n`;
776
885
  const session = authorWorkspace.store.state.sessions[sessionId];
777
- if (session?.candidate?.thumbnailUrl !== void 0) revokeThumbnail(session.candidate.thumbnailUrl);
778
- session?.regions.forEach((region) => {
779
- if (region.thumbnailUrl !== void 0) revokeThumbnail(region.thumbnailUrl);
780
- });
886
+ for (const collection of Object.values(session?.annotationsByDocument ?? {})) {
887
+ if (collection.candidate?.thumbnailUrl !== void 0) revokeThumbnail(collection.candidate.thumbnailUrl);
888
+ collection.annotations.forEach((annotation) => {
889
+ if (annotation.thumbnailUrl !== void 0) revokeThumbnail(annotation.thumbnailUrl);
890
+ });
891
+ }
781
892
  authorWorkspace.update((state) => {
782
893
  const sessions = { ...state.sessions };
783
894
  delete sessions[sessionId];
@@ -787,6 +898,12 @@ function dropAuthorSession(sessionId) {
787
898
  };
788
899
  });
789
900
  }
901
+ function setAuthorStoryPreview(sessionId, path, storyPreview) {
902
+ updateDocument(sessionId, path, (document) => ({
903
+ ...document,
904
+ storyPreview: structuredClone(storyPreview)
905
+ }));
906
+ }
790
907
  function updateDocument(sessionId, path, update) {
791
908
  const key = authorDocumentKey(sessionId, path);
792
909
  const staleThumbnails = [];
@@ -797,7 +914,8 @@ function updateDocument(sessionId, path, update) {
797
914
  const session = state.sessions[sessionId];
798
915
  const focused = session?.focusedDocument;
799
916
  const identityChanged = focused?.path === document.path && (focused.revision !== next.version || focused.sourceSha256 !== next.sourceSha256);
800
- if (identityChanged) staleThumbnails.push(...session.candidate?.thumbnailUrl === void 0 ? [] : [session.candidate.thumbnailUrl], ...session.regions.flatMap((region) => region.thumbnailUrl === void 0 ? [] : [region.thumbnailUrl]));
917
+ if (identityChanged) staleThumbnails.push(...session.candidate?.thumbnailUrl === void 0 ? [] : [session.candidate.thumbnailUrl], ...session.annotations.flatMap((annotation) => annotation.thumbnailUrl === void 0 ? [] : [annotation.thumbnailUrl]));
918
+ const staleCollection = identityChanged ? staleAuthorCollection(session.annotationsByDocument[document.path], Date.now()) : void 0;
801
919
  const sessions = identityChanged ? {
802
920
  ...state.sessions,
803
921
  [sessionId]: {
@@ -807,7 +925,13 @@ function updateDocument(sessionId, path, update) {
807
925
  revision: next.version,
808
926
  sourceSha256: next.sourceSha256
809
927
  },
928
+ annotationsByDocument: {
929
+ ...session.annotationsByDocument,
930
+ ...staleCollection && { [document.path]: staleCollection }
931
+ },
810
932
  candidate: void 0,
933
+ candidateText: "",
934
+ annotations: [],
811
935
  regions: []
812
936
  }
813
937
  } : state.sessions;
@@ -834,16 +958,60 @@ function updateSession(sessionId, update) {
834
958
  };
835
959
  });
836
960
  }
837
- function copyRegion(region) {
838
- const anchor = "rect" in region.anchor ? {
839
- ...region.anchor,
840
- rect: { ...region.anchor.rect }
841
- } : { ...region.anchor };
961
+ function copyCandidate(candidate) {
962
+ const anchor = "rect" in candidate.anchor ? {
963
+ ...candidate.anchor,
964
+ rect: { ...candidate.anchor.rect }
965
+ } : "point" in candidate.anchor ? {
966
+ ...candidate.anchor,
967
+ point: { ...candidate.anchor.point }
968
+ } : { ...candidate.anchor };
842
969
  return {
843
- ...region,
970
+ ...candidate,
844
971
  anchor,
845
- viewport: { ...region.viewport },
846
- voiceGrid: region.voiceGrid && { ...region.voiceGrid }
972
+ viewport: { ...candidate.viewport },
973
+ voiceGrid: candidate.voiceGrid && { ...candidate.voiceGrid }
974
+ };
975
+ }
976
+ function copyRegion(region) {
977
+ return copyCandidate(region);
978
+ }
979
+ function staleAuthorCollection(collection, updatedAt) {
980
+ if (collection === void 0) return void 0;
981
+ return {
982
+ ...collection,
983
+ stale: true,
984
+ candidate: collection.candidate && {
985
+ ...copyCandidate(collection.candidate),
986
+ thumbnailUrl: void 0
987
+ },
988
+ annotations: collection.annotations.map((annotation) => ({
989
+ ...copyRegion(annotation),
990
+ thumbnailUrl: void 0
991
+ })),
992
+ updatedAt
993
+ };
994
+ }
995
+ function withFocusedCollection(session, path, update) {
996
+ const focused = session.focusedDocument;
997
+ if (focused === void 0 || focused.path !== path) return session;
998
+ const collection = {
999
+ ...session.annotationsByDocument[path],
1000
+ revision: focused.revision,
1001
+ sourceSha256: focused.sourceSha256,
1002
+ stale: false,
1003
+ candidate: session.candidate,
1004
+ candidateText: session.candidateText,
1005
+ annotations: session.annotations,
1006
+ updatedAt: Date.now(),
1007
+ ...update
1008
+ };
1009
+ return {
1010
+ ...session,
1011
+ annotationsByDocument: {
1012
+ ...session.annotationsByDocument,
1013
+ [path]: collection
1014
+ }
847
1015
  };
848
1016
  }
849
1017
  function boundedHistoryText(value) {
@@ -873,6 +1041,30 @@ function boundHistory(records) {
873
1041
  }
874
1042
  return next;
875
1043
  }
1044
+ function restoreCollectionEvidence(collection) {
1045
+ const restore = (item) => {
1046
+ const copied = copyCandidate(item);
1047
+ const existingVersion = Reflect.get(copied, "version");
1048
+ const versioned = "id" in copied ? {
1049
+ ...copied,
1050
+ version: typeof existingVersion === "number" ? existingVersion : 1
1051
+ } : copied;
1052
+ if (versioned.thumbnailUrl !== void 0 || versioned.evidence === void 0) return versioned;
1053
+ try {
1054
+ return {
1055
+ ...versioned,
1056
+ thumbnailUrl: URL.createObjectURL(versioned.evidence)
1057
+ };
1058
+ } catch {
1059
+ return versioned;
1060
+ }
1061
+ };
1062
+ return {
1063
+ ...collection,
1064
+ candidate: collection.candidate && restore(collection.candidate),
1065
+ annotations: collection.annotations.map(restore)
1066
+ };
1067
+ }
876
1068
  function revokeThumbnail(url) {
877
1069
  if (!url.startsWith("blob:")) return;
878
1070
  try {
@@ -1053,6 +1245,8 @@ function createAuthorCapturePacket(captureId, capturedAt, document, regions) {
1053
1245
  id: region.id,
1054
1246
  ordinal: index + 1,
1055
1247
  comment: region.comment,
1248
+ version: region.version ?? 1,
1249
+ mode: region.mode ?? ("point" in region.anchor ? "point" : "region"),
1056
1250
  ...region.quote === void 0 ? {} : { quote: utf8Prefix(region.quote, AUTHOR_QUOTE_MAX_BYTES) },
1057
1251
  anchor: structuredClone(region.anchor),
1058
1252
  viewport: { ...region.viewport },
@@ -1063,8 +1257,10 @@ function createAuthorCapturePacket(captureId, capturedAt, document, regions) {
1063
1257
  if (bytes(JSON.stringify(packet)) > 65536) throw new Error("Author capture packet exceeds 64 KiB.");
1064
1258
  return packet;
1065
1259
  }
1260
+ function percent(value) {
1261
+ return `${String(Math.round(value * 1e3) / 10)}%`;
1262
+ }
1066
1263
  function rectLocation(rect) {
1067
- const percent = (value) => `${String(Math.round(value * 1e3) / 10)}%`;
1068
1264
  return `x ${percent(rect.x)}, y ${percent(rect.y)}, w ${percent(rect.width)}, h ${percent(rect.height)}`;
1069
1265
  }
1070
1266
  function anchorLocation(anchor) {
@@ -1076,6 +1272,10 @@ function anchorLocation(anchor) {
1076
1272
  case "pdf-page-rect": return `page ${String(anchor.page)}, ${rectLocation(anchor.rect)}`;
1077
1273
  case "video-time-rect": return `time ${anchor.timeSeconds.toFixed(3)}s${anchor.frame === void 0 ? "" : `, frame ${String(anchor.frame)}`}, ${rectLocation(anchor.rect)}`;
1078
1274
  case "story-preview-rect": return `story preview ${rectLocation(anchor.rect)}`;
1275
+ case "image-point": return `image point x ${percent(anchor.point.x)}, y ${percent(anchor.point.y)}`;
1276
+ case "pdf-page-point": return `page ${String(anchor.page)}, point x ${percent(anchor.point.x)}, y ${percent(anchor.point.y)}`;
1277
+ case "video-time-point": return `time ${anchor.timeSeconds.toFixed(3)}s, point x ${percent(anchor.point.x)}, y ${percent(anchor.point.y)}`;
1278
+ case "story-preview-point": return `story preview point x ${percent(anchor.point.x)}, y ${percent(anchor.point.y)}`;
1079
1279
  }
1080
1280
  }
1081
1281
  function storyPreviewProvenance(packet) {
@@ -1103,7 +1303,7 @@ function authorCaptureContext(packet) {
1103
1303
  kind: "author-capture",
1104
1304
  source: "author",
1105
1305
  id: packet.captureId,
1106
- label: `${packet.regions.length} region${packet.regions.length === 1 ? "" : "s"} · ${packet.document.path}`,
1306
+ label: `${packet.regions.length} annotation${packet.regions.length === 1 ? "" : "s"} · ${packet.document.path}`,
1107
1307
  content,
1108
1308
  metadata
1109
1309
  };
@@ -1208,6 +1408,19 @@ function multiRegionCaptureProvider(regions) {
1208
1408
  context.strokeRect(rx, ry, rw, rh);
1209
1409
  labelX = rx;
1210
1410
  labelY = ry;
1411
+ } else if ("point" in region.anchor) {
1412
+ const px = drawX + region.anchor.point.x * drawWidth;
1413
+ const py = drawY + region.anchor.point.y * drawHeight;
1414
+ const radius = Math.max(6, Math.min(14, Math.min(drawWidth, drawHeight) / 12));
1415
+ context.beginPath();
1416
+ context.arc(px, py, radius, 0, Math.PI * 2);
1417
+ context.fillStyle = annotationColor;
1418
+ context.fill();
1419
+ context.strokeStyle = annotationForeground(annotationColor);
1420
+ context.lineWidth = 2;
1421
+ context.stroke();
1422
+ labelX = px + radius;
1423
+ labelY = py - radius;
1211
1424
  }
1212
1425
  }
1213
1426
  const ordinal = String(index + 1);
@@ -1235,32 +1448,35 @@ function multiRegionCaptureProvider(regions) {
1235
1448
  //#endregion
1236
1449
  //#region src/extensions/workspaces/sessions/(frontend)/dock/_components/AuthorRegionDrafts.tsx
1237
1450
  function AuthorRegionDrafts({ sessionId, workspace }) {
1238
- const [comment, setComment] = useState("");
1239
1451
  const [error, setError] = useState();
1240
1452
  const add = () => {
1241
1453
  try {
1242
- commitAuthorRegion(sessionId, comment);
1243
- setComment("");
1454
+ commitAuthorRegion(sessionId, workspace.candidateText);
1244
1455
  setError(void 0);
1245
1456
  } catch (reason) {
1246
1457
  setError(reason instanceof Error ? reason.message : String(reason));
1247
1458
  }
1248
1459
  };
1249
- if (!workspace.candidate && workspace.regions.length === 0) return null;
1460
+ if (!workspace.candidate && workspace.annotations.length === 0) return null;
1461
+ const candidateKind = workspace.candidate?.mode === "point" ? "point" : "region";
1250
1462
  return /* @__PURE__ */ jsxs("section", {
1251
1463
  className: "space-y-2",
1252
1464
  "data-testid": "author-region-drafts",
1253
1465
  children: [
1254
1466
  workspace.candidate ? /* @__PURE__ */ jsxs(Fragment, { children: [
1255
- /* @__PURE__ */ jsx("h3", {
1467
+ /* @__PURE__ */ jsxs("h3", {
1256
1468
  className: "text-base font-semibold text-doom-text",
1257
- children: "Current selection"
1469
+ children: ["Current ", candidateKind]
1258
1470
  }),
1259
- /* @__PURE__ */ jsx("p", {
1471
+ /* @__PURE__ */ jsxs("p", {
1260
1472
  className: "text-base text-doom-dim sm:text-sm",
1261
- children: "Add a comment to keep this selection as an unsent draft."
1473
+ children: [
1474
+ "Add feedback to keep this ",
1475
+ candidateKind,
1476
+ " as an unsent annotation."
1477
+ ]
1262
1478
  }),
1263
- workspace.candidate.anchor.kind === "video-time-rect" ? /* @__PURE__ */ jsxs("p", {
1479
+ workspace.candidate.anchor.kind === "video-time-rect" || workspace.candidate.anchor.kind === "video-time-point" ? /* @__PURE__ */ jsxs("p", {
1264
1480
  className: "text-base text-doom-dim sm:text-sm",
1265
1481
  children: [
1266
1482
  "Frame at ",
@@ -1269,9 +1485,9 @@ function AuthorRegionDrafts({ sessionId, workspace }) {
1269
1485
  ]
1270
1486
  }) : null,
1271
1487
  /* @__PURE__ */ jsx("textarea", {
1272
- "aria-label": "Region comment",
1273
- value: comment,
1274
- onChange: (event) => setComment(event.target.value),
1488
+ "aria-label": "Annotation comment",
1489
+ value: workspace.candidateText,
1490
+ onChange: (event) => setAuthorCandidateText(sessionId, event.target.value),
1275
1491
  placeholder: "What should change here?",
1276
1492
  rows: 3,
1277
1493
  className: "min-h-11 w-full resize-y rounded border border-doom-border bg-doom-deep p-2 text-lg leading-normal text-doom-text focus:border-doom-red focus:outline-none sm:text-sm"
@@ -1280,49 +1496,51 @@ function AuthorRegionDrafts({ sessionId, workspace }) {
1280
1496
  className: "min-h-11 min-w-11 text-base [@media(pointer:fine)]:min-h-8 sm:text-sm",
1281
1497
  variant: "outline",
1282
1498
  onClick: add,
1283
- disabled: !workspace.candidate || !comment.trim() || workspace.regions.length >= 16,
1284
- children: "add region"
1499
+ disabled: !workspace.candidate || !workspace.candidateText.trim() || workspace.annotations.length >= 16,
1500
+ children: "add annotation"
1285
1501
  }),
1286
1502
  /* @__PURE__ */ jsx(Button, {
1287
1503
  variant: "ghost",
1288
1504
  className: "min-h-11 min-w-11 text-base [@media(pointer:fine)]:min-h-8 sm:text-sm",
1289
1505
  onClick: () => {
1290
1506
  setAuthorRegionCandidate(sessionId, void 0);
1291
- setComment("");
1507
+ setAuthorCandidateText(sessionId, "");
1292
1508
  setError(void 0);
1293
1509
  },
1294
- children: "Discard selection"
1510
+ children: "Discard annotation"
1295
1511
  })
1296
1512
  ] }) : null,
1297
1513
  error ? /* @__PURE__ */ jsx("output", {
1298
1514
  className: "block text-base text-doom-red",
1299
1515
  children: error
1300
1516
  }) : null,
1301
- workspace.regions.length > 0 ? /* @__PURE__ */ jsxs("h3", {
1517
+ workspace.annotations.length > 0 ? /* @__PURE__ */ jsxs("h3", {
1302
1518
  className: "text-base font-semibold text-doom-text",
1303
1519
  children: [
1304
- "Unsent drafts (",
1305
- workspace.regions.length,
1520
+ "Unsent annotations (",
1521
+ workspace.annotations.length,
1306
1522
  ")"
1307
1523
  ]
1308
1524
  }) : null,
1309
1525
  /* @__PURE__ */ jsx("ol", {
1310
1526
  className: "space-y-2",
1311
- children: workspace.regions.map((region, index) => /* @__PURE__ */ jsxs("li", {
1527
+ children: workspace.annotations.map((region, index) => /* @__PURE__ */ jsxs("li", {
1312
1528
  className: "rounded border border-doom-border bg-doom-panel p-2 text-base leading-normal text-doom-text sm:text-sm",
1313
1529
  children: [
1314
1530
  /* @__PURE__ */ jsxs("span", { children: [
1315
1531
  "(",
1316
1532
  index + 1,
1317
1533
  ") ",
1534
+ region.mode === "point" ? "Point" : "Region",
1535
+ ": ",
1318
1536
  region.comment
1319
1537
  ] }),
1320
- region.anchor.kind === "video-time-rect" ? /* @__PURE__ */ jsxs(Button, {
1538
+ region.anchor.kind === "video-time-rect" || region.anchor.kind === "video-time-point" ? /* @__PURE__ */ jsxs(Button, {
1321
1539
  variant: "outline",
1322
1540
  className: "min-h-11 min-w-11 text-base [@media(pointer:fine)]:min-h-8 sm:text-sm",
1323
1541
  disabled: workspace.candidate !== void 0,
1324
1542
  onClick: () => {
1325
- if (region.anchor.kind === "video-time-rect") seekAuthorVideo(sessionId, region.anchor.timeSeconds);
1543
+ if (region.anchor.kind === "video-time-rect" || region.anchor.kind === "video-time-point") seekAuthorVideo(sessionId, region.anchor.timeSeconds);
1326
1544
  },
1327
1545
  children: [
1328
1546
  "Go to ",
@@ -1333,7 +1551,7 @@ function AuthorRegionDrafts({ sessionId, workspace }) {
1333
1551
  /* @__PURE__ */ jsx(Button, {
1334
1552
  className: "min-h-11 min-w-11 text-base [@media(pointer:fine)]:min-h-8 sm:text-sm",
1335
1553
  variant: "ghost",
1336
- "aria-label": `Remove region ${index + 1}`,
1554
+ "aria-label": `Remove annotation ${index + 1}`,
1337
1555
  onClick: () => removeAuthorRegion(sessionId, region.id),
1338
1556
  children: "remove"
1339
1557
  })
@@ -1377,18 +1595,22 @@ function anchorLabel(anchor) {
1377
1595
  case "pdf-page-rect": return `Page ${anchor.page}`;
1378
1596
  case "video-time-rect": return `Video ${Math.floor(anchor.timeSeconds / 60)}:${Math.floor(anchor.timeSeconds % 60).toString().padStart(2, "0")}`;
1379
1597
  case "story-preview-rect": return `${anchor.preview.storyPath}#${anchor.preview.storyExport}`;
1598
+ case "image-point": return "Image point";
1599
+ case "pdf-page-point": return `Page ${anchor.page} point`;
1600
+ case "video-time-point": return `Video ${Math.floor(anchor.timeSeconds / 60)}:${Math.floor(anchor.timeSeconds % 60).toString().padStart(2, "0")} point`;
1601
+ case "story-preview-point": return `${anchor.preview.storyPath}#${anchor.preview.storyExport} point`;
1380
1602
  }
1381
1603
  }
1382
1604
  function progressLabel(request) {
1383
1605
  const total = request.regions.length;
1384
1606
  const pending = request.pendingRegions?.length;
1385
1607
  if (request.status === "CHANGING" && request.currentOperation === "Agent is working on this request") return "Processing request";
1386
- if (request.status === "CHANGING" && pending !== void 0) return `${total - pending} of ${total} regions applied`;
1608
+ if (request.status === "CHANGING" && pending !== void 0) return `${total - pending} of ${total} annotations applied`;
1387
1609
  if (request.status === "CHANGED") return "Changes applied · awaiting save";
1388
1610
  if (request.status === "COMPLETE") return "Agent finished processing this request";
1389
1611
  if (request.status === "FAILED") return "Stopped with an error";
1390
1612
  if (request.status === "CANCELLED") return "Stopped before completion";
1391
- return `${total} region${total === 1 ? "" : "s"} queued`;
1613
+ return `${total} annotation${total === 1 ? "" : "s"} queued`;
1392
1614
  }
1393
1615
  function RequestRecord({ request, prominent = false }) {
1394
1616
  const instruction = requestInstruction(request.requestText);
@@ -1591,8 +1813,9 @@ function AuthorToolPalette({ sessionId, kind, activeTool }) {
1591
1813
  ] : [],
1592
1814
  {
1593
1815
  label: "Comment",
1594
- glyph: "□",
1595
- action: () => setAuthorToolMode(sessionId, "mark")
1816
+ glyph: "●",
1817
+ action: () => setAuthorToolMode(sessionId, activeTool === "comment" ? "select" : "comment"),
1818
+ active: activeTool === "comment"
1596
1819
  }
1597
1820
  ];
1598
1821
  return /* @__PURE__ */ jsxs("section", {
@@ -1623,7 +1846,7 @@ function AuthorToolPalette({ sessionId, kind, activeTool }) {
1623
1846
  }),
1624
1847
  /* @__PURE__ */ jsx("p", {
1625
1848
  className: "text-base leading-normal text-doom-dim sm:text-sm",
1626
- children: activeTool === "mark" ? "Drag over the document to mark a region." : "Choose Region to mark an area for a comment."
1849
+ children: activeTool === "mark" ? "Drag over the document to mark a region." : activeTool === "comment" ? "Click the document to place a comment pin." : "Choose Region to drag an area, or Comment to place a point."
1627
1850
  }),
1628
1851
  error ? /* @__PURE__ */ jsx("output", {
1629
1852
  className: "block text-base text-doom-red",
@@ -1661,7 +1884,7 @@ function AuthorPanel({ sessionId, activeMinorModes, submitCapture, statuses, ren
1661
1884
  }),
1662
1885
  /* @__PURE__ */ jsx("p", {
1663
1886
  className: "text-base leading-normal text-doom-dim sm:text-sm",
1664
- children: "Select a region, add a comment, then submit here. Requests queue while the agent is working."
1887
+ children: "Drag a region or place a point, add feedback, then submit here. Requests queue while the agent is working."
1665
1888
  }),
1666
1889
  focused.kind === "video" ? /* @__PURE__ */ jsx("p", {
1667
1890
  className: "text-base text-doom-dim sm:text-sm",
@@ -1687,7 +1910,7 @@ function AuthorPanel({ sessionId, activeMinorModes, submitCapture, statuses, ren
1687
1910
  sessionId,
1688
1911
  workspace
1689
1912
  }, focused.path),
1690
- workspace.regions.length > 0 ? /* @__PURE__ */ jsxs("div", {
1913
+ workspace.annotations.length > 0 ? /* @__PURE__ */ jsxs("div", {
1691
1914
  className: "space-y-1.5 border-b border-doom-border-soft pb-3",
1692
1915
  children: [/* @__PURE__ */ jsx(Button, {
1693
1916
  className: "min-h-11 min-w-11 w-full text-base [@media(pointer:fine)]:min-h-8 sm:text-sm",
@@ -1699,9 +1922,9 @@ function AuthorPanel({ sessionId, activeMinorModes, submitCapture, statuses, ren
1699
1922
  setCapturing(true);
1700
1923
  setCaptureStatus("Submitting annotations…");
1701
1924
  try {
1702
- const packet = createAuthorCapturePacket(crypto.randomUUID(), Date.now(), focused, workspace.regions);
1925
+ const packet = createAuthorCapturePacket(crypto.randomUUID(), Date.now(), focused, workspace.annotations);
1703
1926
  await submitCapture({
1704
- ...await multiRegionCaptureProvider(workspace.regions).capture(),
1927
+ ...await multiRegionCaptureProvider(workspace.annotations).capture(),
1705
1928
  context: authorCaptureContext(packet)
1706
1929
  });
1707
1930
  setCaptureStatus("Request submitted. You can keep annotating here.");
@@ -1711,10 +1934,10 @@ function AuthorPanel({ sessionId, activeMinorModes, submitCapture, statuses, ren
1711
1934
  setCapturing(false);
1712
1935
  }
1713
1936
  },
1714
- children: capturing ? "Submitting…" : `Submit ${workspace.regions.length} annotation${workspace.regions.length === 1 ? "" : "s"}`
1937
+ children: capturing ? "Submitting…" : `Submit ${workspace.annotations.length} annotation${workspace.annotations.length === 1 ? "" : "s"}`
1715
1938
  }), workspace.candidate ? /* @__PURE__ */ jsx("p", {
1716
1939
  className: "text-base text-doom-dim sm:text-sm",
1717
- children: "Add or discard the current selection before submitting."
1940
+ children: "Add or discard the current annotation before submitting."
1718
1941
  }) : null]
1719
1942
  }) : null,
1720
1943
  submitCapture === void 0 ? /* @__PURE__ */ jsx("output", {
@@ -2733,16 +2956,24 @@ function AuthorMediaView({ sessionId, document: source, activeTool, displayedReg
2733
2956
  start.current = void 0;
2734
2957
  if (!drag) return;
2735
2958
  const bounds = drag.element.getBoundingClientRect();
2736
- const rect = normalizedAuthorRectangle(bounds, {
2959
+ if (bounds.width <= 0 || bounds.height <= 0) return;
2960
+ const point = {
2961
+ x: Math.min(1, Math.max(0, (event.clientX - bounds.left) / bounds.width)),
2962
+ y: Math.min(1, Math.max(0, (event.clientY - bounds.top) / bounds.height))
2963
+ };
2964
+ const rect = drag.mode === "mark" ? normalizedAuthorRectangle(bounds, {
2737
2965
  left: drag.x,
2738
2966
  top: drag.y,
2739
2967
  right: event.clientX,
2740
2968
  bottom: event.clientY
2741
- });
2742
- if (!rect) return;
2743
- setSelection({
2969
+ }) : void 0;
2970
+ if (drag.mode === "mark" && !rect) return;
2971
+ setSelection(drag.mode === "mark" ? {
2744
2972
  path: source.path,
2745
2973
  rect
2974
+ } : {
2975
+ path: source.path,
2976
+ point
2746
2977
  });
2747
2978
  const focused = authorSessionWorkspace(sessionId).focusedDocument;
2748
2979
  if (!focused || focused.path !== source.path) return;
@@ -2753,11 +2984,16 @@ function AuthorMediaView({ sessionId, document: source, activeTool, displayedReg
2753
2984
  if (drag.element instanceof HTMLImageElement) {
2754
2985
  const image = drag.element;
2755
2986
  if (!image.naturalWidth || !image.naturalHeight) throw new Error("Image is not ready.");
2756
- anchor = {
2987
+ anchor = drag.mode === "mark" ? {
2757
2988
  kind: "image-rect",
2758
2989
  rect,
2759
2990
  naturalWidth: image.naturalWidth,
2760
2991
  naturalHeight: image.naturalHeight
2992
+ } : {
2993
+ kind: "image-point",
2994
+ point,
2995
+ naturalWidth: image.naturalWidth,
2996
+ naturalHeight: image.naturalHeight
2761
2997
  };
2762
2998
  const canvas = document.createElement("canvas");
2763
2999
  const scale = Math.min(1, 1600 / Math.max(image.naturalWidth, image.naturalHeight));
@@ -2770,21 +3006,31 @@ function AuthorMediaView({ sessionId, document: source, activeTool, displayedReg
2770
3006
  } else if (source.kind === "pdf") {
2771
3007
  const page = pdf.current?.getState();
2772
3008
  if (!page?.sourceWidth) throw new Error("PDF page is not ready.");
2773
- anchor = {
3009
+ anchor = drag.mode === "mark" ? {
2774
3010
  kind: "pdf-page-rect",
2775
3011
  page: page.page,
2776
3012
  rect
3013
+ } : {
3014
+ kind: "pdf-page-point",
3015
+ page: page.page,
3016
+ point
2777
3017
  };
2778
3018
  blob = await pdf.current.capturePage();
2779
3019
  } else {
2780
3020
  const frame = await video.current?.captureFrame();
2781
3021
  if (!frame) throw new Error("Video frame is not ready.");
2782
- anchor = {
3022
+ anchor = drag.mode === "mark" ? {
2783
3023
  kind: "video-time-rect",
2784
3024
  rect,
2785
3025
  timeSeconds: frame.timeSeconds,
2786
3026
  intrinsicWidth: frame.width,
2787
3027
  intrinsicHeight: frame.height
3028
+ } : {
3029
+ kind: "video-time-point",
3030
+ point,
3031
+ timeSeconds: frame.timeSeconds,
3032
+ intrinsicWidth: frame.width,
3033
+ intrinsicHeight: frame.height
2788
3034
  };
2789
3035
  blob = frame.blob;
2790
3036
  }
@@ -2795,12 +3041,14 @@ function AuthorMediaView({ sessionId, document: source, activeTool, displayedReg
2795
3041
  documentPath: source.path,
2796
3042
  revision: source.version,
2797
3043
  sourceSha256: source.sourceSha256,
3044
+ mode: drag.mode === "mark" ? "region" : "point",
2798
3045
  anchor,
2799
3046
  viewport: {
2800
3047
  width: bounds.width,
2801
3048
  height: bounds.height
2802
3049
  },
2803
3050
  thumbnailUrl,
3051
+ evidence: blob,
2804
3052
  createdAt: Date.now()
2805
3053
  });
2806
3054
  setError(void 0);
@@ -2810,6 +3058,7 @@ function AuthorMediaView({ sessionId, document: source, activeTool, displayedReg
2810
3058
  }
2811
3059
  };
2812
3060
  const selected = selection?.path === source.path ? selection.rect : void 0;
3061
+ const selectedPoint = selection?.path === source.path ? selection.point : void 0;
2813
3062
  const selectionOverlay = selected === void 0 ? null : /* @__PURE__ */ jsx("div", {
2814
3063
  "data-testid": "author-media-selection",
2815
3064
  className: "pointer-events-none absolute border-2 border-doom-red bg-doom-red/10",
@@ -2820,6 +3069,17 @@ function AuthorMediaView({ sessionId, document: source, activeTool, displayedReg
2820
3069
  height: `${selected.height * 100}%`
2821
3070
  }
2822
3071
  });
3072
+ const pointPin = (ordinal, point, key) => /* @__PURE__ */ jsx("span", {
3073
+ "data-author-point": ordinal,
3074
+ "data-testid": ordinal === void 0 ? "author-media-point-selection" : void 0,
3075
+ className: `pointer-events-none absolute z-10 flex h-5 min-w-5 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border-2 px-1 text-2xs font-bold ${ordinal === void 0 ? "border-white bg-doom-red text-white" : "border-doom-deep bg-doom-yellow text-doom-deep"}`,
3076
+ style: {
3077
+ left: `${String(point.x * 100)}%`,
3078
+ top: `${String(point.y * 100)}%`
3079
+ },
3080
+ children: ordinal ?? "•"
3081
+ }, key);
3082
+ const pointSelectionOverlay = selectedPoint === void 0 ? null : pointPin(void 0, selectedPoint, "selection");
2823
3083
  return /* @__PURE__ */ jsxs("div", {
2824
3084
  ref: host,
2825
3085
  className: "min-h-0 flex-1 overflow-auto p-4",
@@ -2829,7 +3089,7 @@ function AuthorMediaView({ sessionId, document: source, activeTool, displayedReg
2829
3089
  },
2830
3090
  onPointerMove: (event) => {
2831
3091
  const drag = start.current;
2832
- if (drag === void 0) return;
3092
+ if (drag === void 0 || drag.mode !== "mark") return;
2833
3093
  const rect = normalizedAuthorRectangle(drag.element.getBoundingClientRect(), {
2834
3094
  left: drag.x,
2835
3095
  top: drag.y,
@@ -2842,7 +3102,7 @@ function AuthorMediaView({ sessionId, document: source, activeTool, displayedReg
2842
3102
  });
2843
3103
  },
2844
3104
  onPointerDownCapture: (event) => {
2845
- if (activeTool === "select" || event.button !== 0 || pendingCandidate) return;
3105
+ if (activeTool !== "mark" && activeTool !== "comment" || event.button !== 0 || pendingCandidate) return;
2846
3106
  const element = event.target;
2847
3107
  if (!(element instanceof HTMLImageElement || element instanceof HTMLVideoElement || element instanceof HTMLCanvasElement)) return;
2848
3108
  event.preventDefault();
@@ -2857,6 +3117,7 @@ function AuthorMediaView({ sessionId, document: source, activeTool, displayedReg
2857
3117
  start.current = {
2858
3118
  x: event.clientX,
2859
3119
  y: event.clientY,
3120
+ mode: activeTool,
2860
3121
  element
2861
3122
  };
2862
3123
  event.currentTarget.setPointerCapture(event.pointerId);
@@ -2881,7 +3142,9 @@ function AuthorMediaView({ sessionId, document: source, activeTool, displayedReg
2881
3142
  className: "block max-w-full"
2882
3143
  }),
2883
3144
  selectionOverlay,
3145
+ pointSelectionOverlay,
2884
3146
  displayedRegions.flatMap(({ ordinal, region }) => {
3147
+ if (region.anchor.kind === "image-point") return [pointPin(ordinal, region.anchor.point, region.id)];
2885
3148
  if (region.anchor.kind !== "image-rect") return [];
2886
3149
  const { rect } = region.anchor;
2887
3150
  return [/* @__PURE__ */ jsx("div", {
@@ -2917,9 +3180,32 @@ function AuthorMediaView({ sessionId, document: source, activeTool, displayedReg
2917
3180
  if (state.playing) setSelection(void 0);
2918
3181
  }
2919
3182
  }),
2920
- source.kind === "video" ? selectionOverlay : null,
2921
- source.kind === "video" ? displayedRegions.map(({ ordinal, region }) => {
2922
- if (region.anchor.kind !== "video-time-rect" || playback.playing || Math.abs(region.anchor.timeSeconds - playback.currentTime) > .1) return null;
3183
+ selectionOverlay,
3184
+ pointSelectionOverlay,
3185
+ displayedRegions.map(({ ordinal, region }) => {
3186
+ if (source.kind === "pdf") {
3187
+ if (region.anchor.kind !== "pdf-page-point" && region.anchor.kind !== "pdf-page-rect") return null;
3188
+ if (region.anchor.page !== pdf.current?.getState().page) return null;
3189
+ if (region.anchor.kind === "pdf-page-point") return pointPin(ordinal, region.anchor.point, region.id);
3190
+ const { rect } = region.anchor;
3191
+ return /* @__PURE__ */ jsx("div", {
3192
+ "data-author-region": ordinal,
3193
+ className: "pointer-events-none absolute border border-doom-yellow bg-doom-yellow/10",
3194
+ style: {
3195
+ left: `${String(rect.x * 100)}%`,
3196
+ top: `${String(rect.y * 100)}%`,
3197
+ width: `${String(rect.width * 100)}%`,
3198
+ height: `${String(rect.height * 100)}%`
3199
+ },
3200
+ children: /* @__PURE__ */ jsx("span", {
3201
+ className: "bg-doom-yellow text-doom-deep",
3202
+ children: ordinal
3203
+ })
3204
+ }, region.id);
3205
+ }
3206
+ if (source.kind !== "video" || playback.playing) return null;
3207
+ if (region.anchor.kind === "video-time-point" && Math.abs(region.anchor.timeSeconds - playback.currentTime) <= .1) return pointPin(ordinal, region.anchor.point, region.id);
3208
+ if (region.anchor.kind !== "video-time-rect" || Math.abs(region.anchor.timeSeconds - playback.currentTime) > .1) return null;
2923
3209
  const { rect } = region.anchor;
2924
3210
  return /* @__PURE__ */ jsx("div", {
2925
3211
  "data-author-region": ordinal,
@@ -2935,7 +3221,7 @@ function AuthorMediaView({ sessionId, document: source, activeTool, displayedReg
2935
3221
  children: ordinal
2936
3222
  })
2937
3223
  }, region.id);
2938
- }) : null
3224
+ })
2939
3225
  ]
2940
3226
  }),
2941
3227
  source.kind === "video" ? /* @__PURE__ */ jsx(AuthorVideoControls, {
@@ -3267,6 +3553,20 @@ function ActiveAuthorDocumentPanel(props) {
3267
3553
  };
3268
3554
  const gridVisible = autonomousVoiceGridVisible(statuses);
3269
3555
  const displayedRegions = displayedAuthorRegions(workspace);
3556
+ const displayedPreviewAnnotations = displayedRegions.flatMap(({ ordinal, region }) => {
3557
+ if (region.anchor.kind === "story-preview-rect") return [{
3558
+ ordinal,
3559
+ mode: "region",
3560
+ point: region.anchor.rect,
3561
+ rect: region.anchor.rect
3562
+ }];
3563
+ if (region.anchor.kind === "story-preview-point") return [{
3564
+ ordinal,
3565
+ mode: "point",
3566
+ point: region.anchor.point
3567
+ }];
3568
+ return [];
3569
+ });
3270
3570
  const textPanel = /* @__PURE__ */ jsx(AuthorTextView, {
3271
3571
  sessionId,
3272
3572
  document,
@@ -3333,7 +3633,32 @@ function ActiveAuthorDocumentPanel(props) {
3333
3633
  className: "relative flex min-h-0 flex-1 overflow-hidden",
3334
3634
  children: [previewOpen && PreviewPanel !== void 0 && previewSource !== void 0 ? /* @__PURE__ */ jsx(PreviewPanel, {
3335
3635
  ...props,
3336
- source: previewSource
3636
+ source: previewSource,
3637
+ activeTool: activeTool === "crop" ? "select" : activeTool,
3638
+ displayedAnnotations: displayedPreviewAnnotations,
3639
+ pendingCandidate: workspace?.candidate !== void 0,
3640
+ onAnnotationCandidate: (candidate) => {
3641
+ setAuthorStoryPreview(sessionId, document.path, candidate.preview);
3642
+ setAuthorRegionCandidate(sessionId, {
3643
+ documentPath: document.path,
3644
+ revision: document.version,
3645
+ sourceSha256: document.sourceSha256,
3646
+ mode: candidate.mode,
3647
+ anchor: candidate.mode === "region" && candidate.rect !== void 0 ? {
3648
+ kind: "story-preview-rect",
3649
+ rect: candidate.rect,
3650
+ preview: candidate.preview
3651
+ } : {
3652
+ kind: "story-preview-point",
3653
+ point: candidate.point,
3654
+ preview: candidate.preview
3655
+ },
3656
+ viewport: candidate.preview.viewport,
3657
+ evidence: candidate.evidence,
3658
+ thumbnailUrl: candidate.thumbnailUrl,
3659
+ createdAt: Date.now()
3660
+ });
3661
+ }
3337
3662
  }) : sourcePanel, previewOpen && PreviewPanel !== void 0 && previewSource !== void 0 ? null : /* @__PURE__ */ jsx(AuthorGridOverlay, {
3338
3663
  sessionId,
3339
3664
  document,
@@ -3395,6 +3720,157 @@ var author_toggle_web_default = defineLeaderBinding({
3395
3720
  command: "minor author"
3396
3721
  });
3397
3722
  //#endregion
3723
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/authorAnnotationPersistence.ts
3724
+ const AUTHOR_ANNOTATION_DATABASE = "doompi-author-annotations";
3725
+ const STORE = "documents";
3726
+ const RECORD_VERSION = 1;
3727
+ function object(value) {
3728
+ return typeof value === "object" && value !== null;
3729
+ }
3730
+ function finite(value) {
3731
+ return typeof value === "number" && Number.isFinite(value);
3732
+ }
3733
+ function point(value) {
3734
+ return object(value) && finite(value.x) && finite(value.y) && value.x >= 0 && value.x <= 1 && value.y >= 0 && value.y <= 1;
3735
+ }
3736
+ function rect(value) {
3737
+ return point(value) && object(value) && finite(value.width) && finite(value.height) && value.width > 0 && value.height > 0;
3738
+ }
3739
+ function validAnchor(value) {
3740
+ if (!object(value) || typeof value.kind !== "string") return false;
3741
+ switch (value.kind) {
3742
+ case "text-range": return finite(value.startOffset) && finite(value.endOffset) && finite(value.startLine) && finite(value.endLine);
3743
+ case "cell": return typeof value.fragmentId === "string" && typeof value.location === "string";
3744
+ case "slide-element": return typeof value.fragmentId === "string" && finite(value.slide) && typeof value.location === "string";
3745
+ case "image-rect": return rect(value.rect) && finite(value.naturalWidth) && finite(value.naturalHeight);
3746
+ case "pdf-page-rect": return finite(value.page) && rect(value.rect);
3747
+ case "video-time-rect": return finite(value.timeSeconds) && rect(value.rect);
3748
+ case "story-preview-rect": return rect(value.rect) && object(value.preview);
3749
+ case "image-point": return point(value.point) && finite(value.naturalWidth) && finite(value.naturalHeight);
3750
+ case "pdf-page-point": return finite(value.page) && point(value.point);
3751
+ case "video-time-point": return finite(value.timeSeconds) && point(value.point);
3752
+ case "story-preview-point": return point(value.point) && object(value.preview);
3753
+ default: return false;
3754
+ }
3755
+ }
3756
+ function validEvidence(value) {
3757
+ return value === void 0 || typeof Blob !== "undefined" && value instanceof Blob && value.size <= 8388608;
3758
+ }
3759
+ function validCandidate(value) {
3760
+ if (!object(value)) return false;
3761
+ return typeof value.documentPath === "string" && normalizeAuthorPath(value.documentPath) !== "" && finite(value.revision) && (value.sourceSha256 === void 0 || typeof value.sourceSha256 === "string") && (value.mode === void 0 || value.mode === "region" || value.mode === "point") && validAnchor(value.anchor) && object(value.viewport) && finite(value.viewport.width) && finite(value.viewport.height) && finite(value.createdAt) && validEvidence(value.evidence);
3762
+ }
3763
+ function validAnnotation(value) {
3764
+ return validCandidate(value) && object(value) && typeof value.id === "string" && value.id !== "" && typeof value.comment === "string" && value.comment.trim() !== "" && (value.version === void 0 || Number.isInteger(value.version) && value.version >= 1);
3765
+ }
3766
+ function utf8Length(value) {
3767
+ return new TextEncoder().encode(value).byteLength;
3768
+ }
3769
+ function validateAuthorAnnotationRecord(input) {
3770
+ if (!object(input) || input.version !== RECORD_VERSION || typeof input.key !== "string") return void 0;
3771
+ if (typeof input.sessionId !== "string" || input.sessionId === "" || typeof input.documentPath !== "string") return void 0;
3772
+ const path = normalizeAuthorPath(input.documentPath);
3773
+ if (path === "" || input.key !== `${input.sessionId}\n${path}` || !object(input.collection)) return void 0;
3774
+ const collection = input.collection;
3775
+ if (!finite(collection.revision) || collection.sourceSha256 !== void 0 && typeof collection.sourceSha256 !== "string" || typeof collection.stale !== "boolean" || typeof collection.candidateText !== "string" || utf8Length(collection.candidateText) > 2048 || !Array.isArray(collection.annotations) || collection.annotations.length > 16 || !collection.annotations.every(validAnnotation) || collection.candidate !== void 0 && !validCandidate(collection.candidate) || !finite(collection.updatedAt)) return void 0;
3776
+ return input;
3777
+ }
3778
+ function durableCandidate(candidate) {
3779
+ const { thumbnailUrl: _thumbnailUrl, ...durable } = candidate;
3780
+ return durable;
3781
+ }
3782
+ function persistenceRecords() {
3783
+ const records = [];
3784
+ for (const [sessionId, session] of Object.entries(authorWorkspace.store.state.sessions)) for (const [documentPath, collection] of Object.entries(session.annotationsByDocument)) {
3785
+ if (collection.annotations.length === 0 && collection.candidate === void 0 && collection.candidateText === "") continue;
3786
+ records.push({
3787
+ key: `${sessionId}\n${documentPath}`,
3788
+ version: RECORD_VERSION,
3789
+ sessionId,
3790
+ documentPath,
3791
+ collection: {
3792
+ ...collection,
3793
+ candidate: collection.candidate && durableCandidate(collection.candidate),
3794
+ annotations: collection.annotations.map(durableCandidate)
3795
+ }
3796
+ });
3797
+ }
3798
+ return records.sort((a, b) => b.collection.updatedAt - a.collection.updatedAt).slice(0, 100);
3799
+ }
3800
+ var IndexedDbAuthorAnnotationRepository = class {
3801
+ #database;
3802
+ #writes = Promise.resolve();
3803
+ constructor(factory) {
3804
+ this.#database = new Promise((resolve, reject) => {
3805
+ const request = factory.open(AUTHOR_ANNOTATION_DATABASE, 1);
3806
+ request.onupgradeneeded = () => {
3807
+ if (!request.result.objectStoreNames.contains(STORE)) request.result.createObjectStore(STORE, { keyPath: "key" });
3808
+ };
3809
+ request.onsuccess = () => resolve(request.result);
3810
+ request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("Unable to open Author annotation storage."));
3811
+ });
3812
+ }
3813
+ async load() {
3814
+ const database = await this.#database;
3815
+ return await new Promise((resolve, reject) => {
3816
+ const request = database.transaction(STORE, "readonly").objectStore(STORE).getAll();
3817
+ request.onsuccess = () => resolve(request.result);
3818
+ request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("Unable to read Author annotations."));
3819
+ });
3820
+ }
3821
+ replace(records) {
3822
+ const write = async () => {
3823
+ const database = await this.#database;
3824
+ await new Promise((resolve, reject) => {
3825
+ const transaction = database.transaction(STORE, "readwrite");
3826
+ const store = transaction.objectStore(STORE);
3827
+ store.clear();
3828
+ records.forEach((record) => store.put(record));
3829
+ transaction.oncomplete = () => resolve();
3830
+ transaction.onerror = () => reject(transaction.error ?? /* @__PURE__ */ new Error("Unable to write Author annotations."));
3831
+ transaction.onabort = () => reject(transaction.error ?? /* @__PURE__ */ new Error("Author annotation write was aborted."));
3832
+ });
3833
+ };
3834
+ this.#writes = this.#writes.catch(() => void 0).then(write);
3835
+ return this.#writes;
3836
+ }
3837
+ async close() {
3838
+ try {
3839
+ await this.#writes;
3840
+ } finally {
3841
+ (await this.#database).close();
3842
+ }
3843
+ }
3844
+ };
3845
+ function startAuthorAnnotationPersistence(repository) {
3846
+ const factory = typeof indexedDB === "undefined" ? void 0 : indexedDB;
3847
+ if (repository === void 0 && factory === void 0) return async () => void 0;
3848
+ const storage = repository ?? new IndexedDbAuthorAnnotationRepository(factory);
3849
+ let stopped = false;
3850
+ let hydrated = false;
3851
+ let dirty = false;
3852
+ const subscription = authorWorkspace.store.subscribe(() => {
3853
+ if (!hydrated) dirty = true;
3854
+ else storage.replace(persistenceRecords()).catch(() => void 0);
3855
+ });
3856
+ storage.load().then((values) => {
3857
+ if (stopped) return;
3858
+ const records = values.map(validateAuthorAnnotationRecord).filter((record) => record !== void 0).sort((a, b) => b.collection.updatedAt - a.collection.updatedAt).slice(0, 100);
3859
+ hydrateAuthorAnnotationCollections(records);
3860
+ hydrated = true;
3861
+ if (dirty || records.length > 0) storage.replace(persistenceRecords()).catch(() => void 0);
3862
+ }).catch(() => {
3863
+ if (stopped) return;
3864
+ hydrated = true;
3865
+ if (dirty) storage.replace(persistenceRecords()).catch(() => void 0);
3866
+ });
3867
+ return async () => {
3868
+ stopped = true;
3869
+ subscription.unsubscribe();
3870
+ await storage.close();
3871
+ };
3872
+ }
3873
+ //#endregion
3398
3874
  //#region src/extensions/workspaces/sessions/(frontend)/lifecycle/_lib/authorRequestLifecycle.ts
3399
3875
  function capturePacket(content) {
3400
3876
  try {
@@ -3417,6 +3893,8 @@ function recordAuthorComposerSubmission(submission) {
3417
3893
  revision: packet.document.revision,
3418
3894
  sourceSha256: packet.document.sourceSha256,
3419
3895
  comment: region.comment,
3896
+ version: region.version ?? 1,
3897
+ mode: region.mode,
3420
3898
  quote: region.quote,
3421
3899
  anchor: structuredClone(region.anchor),
3422
3900
  viewport: { ...region.viewport },
@@ -3436,7 +3914,7 @@ function recordAuthorComposerSubmission(submission) {
3436
3914
  revision: packet.document.revision,
3437
3915
  sourceSha256: packet.document.sourceSha256
3438
3916
  });
3439
- for (const region of packet.regions) removeAuthorRegion(submission.sessionId, region.id);
3917
+ for (const region of packet.regions) removeAuthorRegion(submission.sessionId, region.id, region.version ?? 1, packet.document.path);
3440
3918
  }
3441
3919
  }
3442
3920
  /** Only the host's correlated execution events may settle a submitted capture. */
@@ -3461,12 +3939,14 @@ function recordAuthorCaptureStatus(event) {
3461
3939
  //#region src/extensions/workspaces/sessions/(frontend)/lifecycle/_lib/authorBrowserLifecycle.ts
3462
3940
  function startAuthorBrowserLifecycle(runtime) {
3463
3941
  const stopBridge = startAuthorBrowserBridge(runtime);
3942
+ const stopPersistence = startAuthorAnnotationPersistence();
3464
3943
  const stopSubmissions = runtime.onComposerSubmitted?.(recordAuthorComposerSubmission) ?? (() => void 0);
3465
3944
  const stopCaptureStatus = runtime.onCaptureStatus?.(recordAuthorCaptureStatus) ?? (() => void 0);
3466
- return () => {
3945
+ return async () => {
3467
3946
  stopSubmissions();
3468
3947
  stopCaptureStatus();
3469
3948
  stopBridge();
3949
+ await stopPersistence().catch(() => void 0);
3470
3950
  };
3471
3951
  }
3472
3952
  //#endregion
@@ -3575,13 +4055,18 @@ function resultPath(result) {
3575
4055
  if (typeof result?.details !== "object" || result.details === null || !("path" in result.details)) return void 0;
3576
4056
  return typeof result.details.path === "string" ? result.details.path : void 0;
3577
4057
  }
3578
- function OpenAuthoringFileToolCard({ args, result, running, isError, openTransientTab, openTab }) {
4058
+ function OpenAuthoringFileToolCard({ args, result, running, isError, output, openTransientTab, openTab }) {
3579
4059
  const path = resultPath(result);
4060
+ const unavailable = !running && !isError && result !== null && path === void 0;
4061
+ const errorLines = isError ? (output ?? "").trim().split(/\r?\n/u).filter(Boolean).slice(0, 4).map((text) => ({
4062
+ text,
4063
+ tone: "error"
4064
+ })) : [];
3580
4065
  const requestedPath = typeof args.path === "string" ? args.path : "document";
3581
4066
  return /* @__PURE__ */ jsx(MessageItem, {
3582
4067
  tone: toolTone({
3583
4068
  running,
3584
- isError
4069
+ isError: isError || unavailable
3585
4070
  }),
3586
4071
  children: () => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MessageItemHeader, {
3587
4072
  title: "open_authoring_file",
@@ -3592,16 +4077,21 @@ function OpenAuthoringFileToolCard({ args, result, running, isError, openTransie
3592
4077
  })
3593
4078
  }), /* @__PURE__ */ jsxs(MessageItemBody, {
3594
4079
  "data-testid": "tool-result-open_authoring_file",
3595
- children: [/* @__PURE__ */ jsx(MessageItemStatus, {
3596
- tone: isError ? "error" : running ? "running" : void 0,
3597
- children: isError ? "failed" : running ? "validating" : path === void 0 ? "waiting" : "ready"
3598
- }), !running && !isError && path !== void 0 ? /* @__PURE__ */ jsx(Button, {
3599
- size: "xs",
3600
- variant: "outline",
3601
- "data-testid": "author-open-file",
3602
- onClick: () => openAuthoringFileTab(path, openTransientTab, openTab),
3603
- children: "open in Author"
3604
- }) : null]
4080
+ className: "flex flex-col gap-1",
4081
+ children: [
4082
+ errorLines.length > 0 ? /* @__PURE__ */ jsx(MessageLines, { lines: errorLines }) : null,
4083
+ /* @__PURE__ */ jsx(MessageItemStatus, {
4084
+ tone: isError || unavailable ? "error" : running ? "running" : void 0,
4085
+ children: isError ? "failed" : running ? "validating" : unavailable ? "unavailable" : path === void 0 ? "waiting" : "ready"
4086
+ }),
4087
+ !running && !isError && path !== void 0 ? /* @__PURE__ */ jsx(Button, {
4088
+ size: "xs",
4089
+ variant: "outline",
4090
+ "data-testid": "author-open-file",
4091
+ onClick: () => openAuthoringFileTab(path, openTransientTab, openTab),
4092
+ children: "open in Author"
4093
+ }) : null
4094
+ ]
3605
4095
  })] })
3606
4096
  });
3607
4097
  }
@@ -3609,7 +4099,11 @@ function OpenAuthoringFileToolCard({ args, result, running, isError, openTransie
3609
4099
  //#region src/extensions/workspaces/sessions/(frontend)/tool/open-authoring-file.web.tsx
3610
4100
  var open_authoring_file_web_default = defineToolRenderer({
3611
4101
  tools: ["open_authoring_file"],
3612
- message: OpenAuthoringFileToolCard
4102
+ message: OpenAuthoringFileToolCard,
4103
+ completionTab: ({ result }) => {
4104
+ const path = resultPath(result);
4105
+ return path === void 0 ? void 0 : authorFileTab(path);
4106
+ }
3613
4107
  });
3614
4108
  //#endregion
3615
4109
  //#region src/extensions/workspaces/sessions/(frontend)/tool/_lib/useAuthorToolsToolRender.ts