@agimon-ai/po-author 0.0.0-alpha.43 → 0.0.0-alpha.45

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.
@@ -800,6 +800,27 @@ function authorCaptureContext(packet) {
800
800
  };
801
801
  }
802
802
  //#endregion
803
+ //#region src/extensions/workspaces/sessions/(frontend)/_lib/authorFileSave.ts
804
+ /** Web and native commit text, encoded images and serialized documents through this one conditional save. */
805
+ async function saveAuthorFileBytes(transport, url, path, body, expectedSha256, signal) {
806
+ if (path.startsWith("artifact:")) throw new Error("Artifacts are read-only.");
807
+ if (!/^[a-f0-9]{64}$/.test(expectedSha256)) throw new Error("This Author view has no valid source digest.");
808
+ const response = await transport(url, {
809
+ method: "PUT",
810
+ headers: {
811
+ "Content-Type": "application/octet-stream",
812
+ "X-Expected-SHA256": expectedSha256
813
+ },
814
+ body,
815
+ signal
816
+ });
817
+ if (response.status === 409) throw new Error("This file changed elsewhere. Keep your edits and reopen it before saving.");
818
+ if (!response.ok) throw new Error(`Author save failed (${String(response.status)})`);
819
+ const digest = response.headers.get("X-File-SHA256");
820
+ if (digest === null || !/^[a-f0-9]{64}$/.test(digest)) throw new Error("Author save returned an invalid digest.");
821
+ return digest;
822
+ }
823
+ //#endregion
803
824
  //#region src/extensions/workspaces/sessions/(frontend)/_lib/authorFiles.native.ts
804
825
  const MAX_FILE_BYTES = 5242880;
805
826
  const IMAGE_TYPES = /* @__PURE__ */ new Set([
@@ -856,8 +877,8 @@ function authorNativeCanOpen(path) {
856
877
  }
857
878
  /**
858
879
  * The raw reads Author's canvas already uses, restated for the native transport:
859
- * po-artifact's guarded artifact read and the host's session file, plus the two
860
- * canvas routes from apiRoutes.ts, whose definer the phone does not load.
880
+ * po-artifact's guarded artifact read and Author's session file API, plus the
881
+ * document routes from apiRoutes.ts, whose definer the phone does not load.
861
882
  */
862
883
  const artifactFiles = createApiClient({ content: {
863
884
  method: "GET",
@@ -875,14 +896,12 @@ const sessionFiles = createApiClient({
875
896
  method: "GET",
876
897
  path: "/file",
877
898
  query: ["path"],
878
- host: true,
879
899
  raw: true
880
900
  },
881
901
  fileWrite: {
882
902
  method: "PUT",
883
903
  path: "/file",
884
904
  query: ["path"],
885
- host: true,
886
905
  raw: true
887
906
  },
888
907
  documentsOpen: {
@@ -958,22 +977,7 @@ async function saveAuthorNativeImage(sessionId, path, image, sourceSha256, signa
958
977
  return digest;
959
978
  }
960
979
  async function saveAuthorNativeBytes(sessionId, path, body, sourceSha256, signal) {
961
- const result = await sessionFiles.session(sessionId).fileWrite({
962
- query: { path },
963
- headers: {
964
- "Content-Type": "application/octet-stream",
965
- "X-Expected-SHA256": sourceSha256
966
- },
967
- body,
968
- signal
969
- });
970
- if (!result.ok) {
971
- if (result.status === 409) throw new Error("This file changed elsewhere. Keep your edits and reopen it before saving.");
972
- throw new Error(result.error || `Author save failed (${String(result.status)})`);
973
- }
974
- const digest = result.response.headers.get("X-File-SHA256");
975
- if (digest === null || !/^[a-f0-9]{64}$/.test(digest)) throw new Error("Author save returned an invalid digest.");
976
- return digest;
980
+ return await saveAuthorFileBytes(nativeApiTransport, sessionFiles.session(sessionId).fileWrite.url({ query: { path } }), path, body, sourceSha256, signal);
977
981
  }
978
982
  /** Reads one document for marking up on the phone: an image as base64, anything textual as a string. */
979
983
  async function loadAuthorFile(sessionId, path, signal) {
@@ -1247,6 +1251,18 @@ function authorNotesContent(path, notes) {
1247
1251
  }
1248
1252
  //#endregion
1249
1253
  //#region src/extensions/workspaces/sessions/(frontend)/_lib/authorNativeCapture.ts
1254
+ /** Snapshot layout-unit geometry before asynchronous raster capture and reject any intervening transform change. */
1255
+ async function captureNativeAuthorViewport(capture, getViewport) {
1256
+ const viewport = getViewport();
1257
+ if (!viewport || ![viewport.width, viewport.height].every((value) => Number.isFinite(value) && value > 0)) throw new Error("The document geometry is not ready to capture.");
1258
+ const snapshot = { ...viewport };
1259
+ const result = await capture();
1260
+ if (getViewport() !== viewport) throw new Error("The document moved during capture. Keep it still and try again.");
1261
+ return {
1262
+ capture: result,
1263
+ viewport: snapshot
1264
+ };
1265
+ }
1250
1266
  /** A native mark is feedback on what was displayed, never source-edit authority. */
1251
1267
  function nativeAuthorCapture(captureId, capturedAt, document, notes, viewport, alias) {
1252
1268
  const regions = notes.map((note) => {
@@ -1632,14 +1648,15 @@ function AuthorDocumentView(props) {
1632
1648
  }] : []].filter((mark) => mark.textRange === void 0 && (mark.sourceSha256 === void 0 || mark.sourceSha256 === props.sourceSha256)), body.kind, props.page, props.timeSeconds);
1633
1649
  const sending = status.state === "sending";
1634
1650
  const inspectingSource = canInspectSource && readingMode === "source";
1651
+ const drawing = !inspectingSource && !draft && !sending && props.onDraw !== void 0 && tool !== "pin";
1635
1652
  const annotation = /* @__PURE__ */ jsx(AnnotationSurface, {
1636
1653
  accessibilityLabel: props.name,
1637
- zoomable: body.kind === "image" && !sending,
1638
- onViewportChange: (viewport) => props.onZoomChange?.(viewport.zoom),
1654
+ zoomable: body.kind === "image",
1655
+ onViewportChange: props.onViewportChange,
1639
1656
  marks: inspectingSource ? [] : marks,
1640
1657
  selectedId: inspectingSource ? void 0 : draft ? DRAFT_ID : props.selectedId,
1641
1658
  onPlace: inspectingSource || draft || sending || tool !== "pin" ? void 0 : props.onPlace,
1642
- onDraw: !inspectingSource && !draft && !sending && props.onDraw && tool !== "pin" ? (points) => {
1659
+ onDraw: drawing ? (points) => {
1643
1660
  if (tool === "draw") {
1644
1661
  props.onDraw?.(points);
1645
1662
  return;
@@ -1695,7 +1712,7 @@ function AuthorDocumentView(props) {
1695
1712
  accessibilityRole: "button",
1696
1713
  accessibilityLabel: `Comment on ${fragment.location}`,
1697
1714
  disabled: !!draft || sending || !props.onPlace,
1698
- style: [s.notes, s.fragment],
1715
+ style: [s.notes, s.touchTarget],
1699
1716
  onPress: (event) => {
1700
1717
  event.stopPropagation();
1701
1718
  props.onPlace?.({
@@ -1759,6 +1776,7 @@ function AuthorDocumentView(props) {
1759
1776
  }) : null,
1760
1777
  props.onDraw && !props.edit && !inspectingSource ? /* @__PURE__ */ jsx(SegmentedControl, {
1761
1778
  accessibilityLabel: "Mark tool",
1779
+ style: s.touchTarget,
1762
1780
  options: [
1763
1781
  {
1764
1782
  value: "pin",
@@ -1794,6 +1812,7 @@ function AuthorDocumentView(props) {
1794
1812
  }) : null,
1795
1813
  canInspectSource && !props.edit ? /* @__PURE__ */ jsx(SegmentedControl, {
1796
1814
  accessibilityLabel: "Document view",
1815
+ style: s.touchTarget,
1797
1816
  options: [{
1798
1817
  value: "read",
1799
1818
  label: "Read"
@@ -1829,6 +1848,7 @@ function AuthorDocumentView(props) {
1829
1848
  style: s.scroll,
1830
1849
  contentContainerStyle: s.content,
1831
1850
  keyboardShouldPersistTaps: "handled",
1851
+ scrollEnabled: !drawing,
1832
1852
  children: [
1833
1853
  props.edit?.image ? /* @__PURE__ */ jsx(Image, {
1834
1854
  accessibilityLabel: "Crop preview",
@@ -2059,7 +2079,7 @@ const s = StyleSheet.create((theme) => ({
2059
2079
  gap: theme.spacing(2),
2060
2080
  uni__dependencies: [0]
2061
2081
  },
2062
- fragment: { minHeight: 44 },
2082
+ touchTarget: { minHeight: 44 },
2063
2083
  note: {
2064
2084
  minHeight: 44,
2065
2085
  flexDirection: "row",
@@ -2300,7 +2320,14 @@ function AuthorDocumentPanel({ path, ...props }) {
2300
2320
  const [cropping, setCropping] = useState(false);
2301
2321
  const [videoFrame, setVideoFrame] = useState();
2302
2322
  const mediaTime = useRef(0);
2303
- const imageZoom = useRef(1);
2323
+ const geometry = useRef(void 0);
2324
+ const onViewportChange = useCallback((viewport) => {
2325
+ const previous = geometry.current;
2326
+ if (previous?.key !== key || previous.viewport.width !== viewport.width || previous.viewport.height !== viewport.height || previous.viewport.zoom !== viewport.zoom || previous.viewport.originX !== viewport.originX || previous.viewport.originY !== viewport.originY) geometry.current = {
2327
+ key,
2328
+ viewport: { ...viewport }
2329
+ };
2330
+ }, [key]);
2304
2331
  const edit = useSyncExternalStore(subscribeEdits, () => editsStore.store.state[key]);
2305
2332
  const captureRef = useRef(void 0);
2306
2333
  const onCaptureReady = useCallback((capture) => {
@@ -2494,7 +2521,7 @@ function AuthorDocumentPanel({ path, ...props }) {
2494
2521
  if (!props.submitCapture) throw new Error("This host cannot confirm feedback delivery. Your notes have not been sent.");
2495
2522
  const provider = captureRef.current;
2496
2523
  if (provider?.key !== key) throw new Error("The document is not ready to capture. Try again when it has finished opening.");
2497
- const capture = await provider.capture();
2524
+ const { capture, viewport } = await captureNativeAuthorViewport(provider.capture, () => geometry.current?.key === key ? geometry.current.viewport : void 0);
2498
2525
  setStatus({ state: "sending" });
2499
2526
  if (capture.bytes.byteLength > 2097152 || Math.max(capture.width, capture.height) > 1600) throw new Error("This capture exceeds the Author evidence budget.");
2500
2527
  const { file, body } = current;
@@ -2509,11 +2536,7 @@ function AuthorDocumentPanel({ path, ...props }) {
2509
2536
  naturalWidth: body.naturalWidth,
2510
2537
  naturalHeight: body.naturalHeight
2511
2538
  } : {}
2512
- }, sent, {
2513
- width: capture.width,
2514
- height: capture.height,
2515
- ...file.kind === "image" ? { zoom: imageZoom.current } : {}
2516
- }, alias);
2539
+ }, sent, viewport, alias);
2517
2540
  recordNativeAuthorRequest(sessionId, packet, regions);
2518
2541
  recorded = true;
2519
2542
  await props.submitCapture({
@@ -2704,9 +2727,7 @@ function AuthorDocumentPanel({ path, ...props }) {
2704
2727
  children,
2705
2728
  onReady: onCaptureReady
2706
2729
  }) ?? children,
2707
- onZoomChange: (zoom) => {
2708
- imageZoom.current = zoom;
2709
- },
2730
+ onViewportChange,
2710
2731
  onResumeVideo: videoFrame === void 0 ? void 0 : () => {
2711
2732
  setSeekSeconds(videoFrame.timeSeconds);
2712
2733
  setVideoFrame(void 0);