@agimon-ai/po-author 0.0.0-alpha.67 → 0.0.0-alpha.69

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.
@@ -1,7 +1,7 @@
1
1
  import { apiResponse, createApiClient, decodeBase64, defineChannelFile, defineFileLinks, defineFill, defineGlobalStore, defineNativePlugin, defineSessionStore, defineSlotFile, defineToolRenderer, defineWebLifecycle, encodeBase64, nativeApiTransport, sessionApiAddress } from "@agimon-ai/po-core/native";
2
2
  import { Store } from "@tanstack/store";
3
3
  import { Image } from "react-native-unistyles/components/native/Image";
4
- import { AnnotationSurface, Badge, Button, EmptyState, Markdown, MessageItem, MessageItemBody, MessageItemHeader, MessageItemStatus, SectionLabel, SegmentedControl, Spinner, Textarea, TrashIcon, collapseLines, fontOf, resultTextLines, toolTone } from "@agimon-ai/po-native-components";
4
+ import { AnnotationSurface, Badge, Button, EmptyState, Markdown, MessageItem, MessageItemBody, MessageItemHeader, MessageItemStatus, SectionLabel, SegmentedControl, Sheet, SheetBody, SheetContent, SheetDescription, SheetHeader, SheetTitle, Spinner, Textarea, TrashIcon, collapseLines, fontOf, resultTextLines, selectionTick, toolTone } from "@agimon-ai/po-native-components";
5
5
  import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
6
6
  import { AppState } from "react-native";
7
7
  import { View } from "react-native-unistyles/components/native/View";
@@ -656,7 +656,7 @@ const authorPreviewActionSlot = {
656
656
  };
657
657
  /** Keep freehand evidence small and source-relative. Reject malformed geometry rather than degrading it into a crop. */
658
658
  function validAuthorStroke(value) {
659
- return Array.isArray(value) && value.length >= 2 && value.length <= 128 && value.some((point) => point.x !== value[0]?.x || point.y !== value[0]?.y) && value.every((point) => typeof point === "object" && point !== null && Number.isFinite(point.x) && Number.isFinite(point.y) && point.x >= 0 && point.x <= 1 && point.y >= 0 && point.y <= 1);
659
+ return Array.isArray(value) && value.length >= 2 && value.length <= 128 && value.every((point) => typeof point === "object" && point !== null && Number.isFinite(point.x) && Number.isFinite(point.y) && point.x >= 0 && point.x <= 1 && point.y >= 0 && point.y <= 1) && value.some((point) => point.x !== value[0]?.x || point.y !== value[0]?.y);
660
660
  }
661
661
  function authorStrokeBounds(points) {
662
662
  if (!validAuthorStroke(points)) return null;
@@ -690,6 +690,64 @@ function normalizedAuthorRectangle(bounds, drag) {
690
690
  height: (bottom - top) / height
691
691
  };
692
692
  }
693
+ /** Shapes share the existing bounded stroke contract, including reverse drags and image aspect ratios. */
694
+ function authorAnnotationStroke(tool, points, aspectRatio) {
695
+ if (!validAuthorStroke(points) || !Number.isFinite(aspectRatio) || aspectRatio <= 0) return void 0;
696
+ const first = points[0];
697
+ const last = points.at(-1);
698
+ if (tool === "rectangle") {
699
+ const rect = normalizedAuthorRectangle({
700
+ left: 0,
701
+ top: 0,
702
+ right: 1,
703
+ bottom: 1
704
+ }, {
705
+ left: first.x,
706
+ top: first.y,
707
+ right: last.x,
708
+ bottom: last.y
709
+ });
710
+ return rect ? [
711
+ {
712
+ x: rect.x,
713
+ y: rect.y
714
+ },
715
+ {
716
+ x: rect.x + rect.width,
717
+ y: rect.y
718
+ },
719
+ {
720
+ x: rect.x + rect.width,
721
+ y: rect.y + rect.height
722
+ },
723
+ {
724
+ x: rect.x,
725
+ y: rect.y + rect.height
726
+ },
727
+ {
728
+ x: rect.x,
729
+ y: rect.y
730
+ }
731
+ ] : void 0;
732
+ }
733
+ if (tool !== "arrow") return points.map((point) => ({ ...point }));
734
+ const dx = (last.x - first.x) * aspectRatio;
735
+ const dy = last.y - first.y;
736
+ const length = Math.hypot(dx, dy);
737
+ if (length === 0) return void 0;
738
+ const head = Math.min(.04, length / 3);
739
+ const wing = (side) => ({
740
+ x: Math.max(0, Math.min(1, last.x - (dx + side * dy * .6) / length * head / aspectRatio)),
741
+ y: Math.max(0, Math.min(1, last.y - (dy - side * dx * .6) / length * head))
742
+ });
743
+ return [
744
+ { ...first },
745
+ { ...last },
746
+ wing(1),
747
+ { ...last },
748
+ wing(-1)
749
+ ];
750
+ }
693
751
  const AUTHOR_QUOTE_MAX_BYTES = 4096;
694
752
  function bytes$1(value) {
695
753
  let length = 0;
@@ -901,10 +959,14 @@ const IMAGE_SAVE_MAX_BYTES = 33554432;
901
959
  const PDF_MAX_BYTES = 33554432;
902
960
  const VIDEO_MAX_BYTES = 134217728;
903
961
  const extensionOf = (path) => path.toLowerCase().split("/").at(-1)?.split(".").slice(1).at(-1) ?? "";
962
+ /** Reuse the native decoder's supported image paths for canvas-specific host layout. */
963
+ function authorNativeIsImage(path) {
964
+ return IMAGE_EXTENSIONS.has(extensionOf(path));
965
+ }
904
966
  /** Whether the phone's canvas can mark this path up; anything else stays with its own viewer. */
905
967
  function authorNativeCanOpen(path) {
906
968
  const extension = extensionOf(path);
907
- return extension === "pdf" || VIDEO_MIMES[extension] !== void 0 || structuredFormat(path) !== void 0 || IMAGE_EXTENSIONS.has(extension) || TEXT_EXTENSIONS.has(extension);
969
+ return extension === "pdf" || VIDEO_MIMES[extension] !== void 0 || structuredFormat(path) !== void 0 || authorNativeIsImage(path) || TEXT_EXTENSIONS.has(extension);
908
970
  }
909
971
  /**
910
972
  * The raw reads Author's canvas already uses, restated for the native transport:
@@ -1438,6 +1500,10 @@ function note(value) {
1438
1500
  };
1439
1501
  });
1440
1502
  }
1503
+ if (record.highlight !== void 0) {
1504
+ if (typeof record.highlight !== "boolean") throw new Error("Invalid stored Author highlight.");
1505
+ result.highlight = record.highlight;
1506
+ }
1441
1507
  if (record.page !== void 0) {
1442
1508
  if (typeof record.page !== "number" || !Number.isSafeInteger(record.page) || record.page < 1) throw new Error("Invalid stored Author page.");
1443
1509
  result.page = record.page;
@@ -1624,6 +1690,671 @@ function startNativeAuthorRequests(runtime) {
1624
1690
  };
1625
1691
  }
1626
1692
  //#endregion
1693
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/AuthorCanvasWorkspace.native.tsx
1694
+ const TOOLS = [
1695
+ {
1696
+ id: "move",
1697
+ label: "Move",
1698
+ hint: "Drag to move. Pinch to zoom."
1699
+ },
1700
+ {
1701
+ id: "pin",
1702
+ label: "Pin",
1703
+ hint: "Tap to mark. Keep going, explain later."
1704
+ },
1705
+ {
1706
+ id: "rectangle",
1707
+ label: "Box",
1708
+ hint: "Drag around a region. Keep going, explain later."
1709
+ },
1710
+ {
1711
+ id: "arrow",
1712
+ label: "Arrow",
1713
+ hint: "Drag toward what should change."
1714
+ },
1715
+ {
1716
+ id: "draw",
1717
+ label: "Draw",
1718
+ hint: "Draw freely. Each stroke becomes a mark."
1719
+ },
1720
+ {
1721
+ id: "highlight",
1722
+ label: "Highlight",
1723
+ hint: "Draw over a detail. The original stays unchanged."
1724
+ },
1725
+ {
1726
+ id: "erase",
1727
+ label: "Erase",
1728
+ hint: "Tap a numbered mark to remove it, not image pixels."
1729
+ }
1730
+ ];
1731
+ const FIT = {
1732
+ width: 0,
1733
+ height: 0,
1734
+ zoom: 1,
1735
+ originX: 0,
1736
+ originY: 0
1737
+ };
1738
+ const SEND_LIMIT = 16;
1739
+ /** Image feedback is a canvas first. Explanations and delivery live in the review sheet. */
1740
+ function AuthorCanvasWorkspace(props) {
1741
+ const { body, notes, draft, status } = props;
1742
+ const [tool, setTool] = useState("move");
1743
+ const [reviewOpen, setReviewOpen] = useState(!!draft || !!props.selectedId);
1744
+ const [reviewId, setReviewId] = useState(props.selectedId ?? notes[0]?.id);
1745
+ const [area, setArea] = useState({
1746
+ width: 0,
1747
+ height: 0
1748
+ });
1749
+ const [viewport, setViewport] = useState(FIT);
1750
+ const sending = status.state === "sending";
1751
+ const sendAfterFit = useRef(false);
1752
+ useEffect(() => {
1753
+ if (sendAfterFit.current && viewport.zoom === 1 && viewport.originX === 0 && viewport.originY === 0) {
1754
+ sendAfterFit.current = false;
1755
+ props.onSend?.();
1756
+ }
1757
+ }, [viewport, props.onSend]);
1758
+ const send = () => {
1759
+ if (viewport.zoom === 1 && viewport.originX === 0 && viewport.originY === 0) props.onSend?.();
1760
+ else {
1761
+ sendAfterFit.current = true;
1762
+ setViewport((current) => ({
1763
+ ...current,
1764
+ zoom: 1,
1765
+ originX: 0,
1766
+ originY: 0
1767
+ }));
1768
+ }
1769
+ };
1770
+ const onViewportChange = useCallback((next) => {
1771
+ setViewport((previous) => previous.width === next.width && previous.height === next.height && previous.zoom === next.zoom && previous.originX === next.originX && previous.originY === next.originY ? previous : next);
1772
+ props.onViewportChange?.(next);
1773
+ }, [props.onViewportChange]);
1774
+ const ratio = Number.isFinite(body.aspectRatio) && body.aspectRatio > 0 ? body.aspectRatio : 1;
1775
+ const width = Math.min(area.width, area.height * ratio);
1776
+ const height = width / ratio;
1777
+ const overviewWidth = 64 * Math.min(1, ratio);
1778
+ const overviewHeight = 64 / Math.max(1, ratio);
1779
+ const zoomTo = (zoom) => {
1780
+ const next = Math.min(4, Math.max(1, zoom));
1781
+ setViewport((current) => ({
1782
+ ...current,
1783
+ zoom: next,
1784
+ originX: current.width / 2 - (current.width / 2 - current.originX) / current.zoom * next,
1785
+ originY: current.height / 2 - (current.height / 2 - current.originY) / current.zoom * next
1786
+ }));
1787
+ };
1788
+ const focus = (note) => {
1789
+ setReviewId(note.id);
1790
+ props.onSelect?.(note.id);
1791
+ setViewport((current) => ({
1792
+ ...current,
1793
+ zoom: 2,
1794
+ originX: current.width / 2 - note.x * current.width * 2,
1795
+ originY: current.height / 2 - note.y * current.height * 2
1796
+ }));
1797
+ };
1798
+ const batch = notes.slice(0, SEND_LIMIT);
1799
+ const needsExplanation = batch.some((note) => !note.text.trim());
1800
+ const stale = batch.some((note) => note.sourceSha256 !== void 0 && note.sourceSha256 !== props.sourceSha256);
1801
+ const selected = notes.find((note) => note.id === reviewId);
1802
+ const marks = notes.flatMap((note, index) => note.sourceSha256 !== void 0 && note.sourceSha256 !== props.sourceSha256 ? [] : [{
1803
+ ...note,
1804
+ label: String(index + 1),
1805
+ description: note.text || "Needs an explanation"
1806
+ }]);
1807
+ if (draft) marks.push({
1808
+ ...draft,
1809
+ id: "draft",
1810
+ label: String(notes.length + 1),
1811
+ description: "Unfinished note"
1812
+ });
1813
+ const annotation = /* @__PURE__ */ jsx(AnnotationSurface, {
1814
+ accessibilityLabel: props.name,
1815
+ zoomable: !sending,
1816
+ navigation: tool === "move" && !sending,
1817
+ viewport,
1818
+ onViewportChange,
1819
+ marks,
1820
+ selectedId: selected?.id ?? props.selectedId,
1821
+ drawingHighlight: tool === "highlight",
1822
+ onPlace: !sending && tool === "pin" ? (point) => {
1823
+ if (props.onAddMark) props.onAddMark({
1824
+ ...point,
1825
+ text: ""
1826
+ });
1827
+ else props.onPlace?.(point);
1828
+ } : void 0,
1829
+ onDraw: !sending && props.onAddMark && tool !== "move" && tool !== "pin" && tool !== "erase" ? (points) => {
1830
+ const stroke = authorAnnotationStroke(tool, points, ratio);
1831
+ const point = tool === "arrow" ? points.at(-1) : stroke?.at(-1);
1832
+ if (stroke && point) props.onAddMark?.({
1833
+ ...point,
1834
+ points: stroke,
1835
+ text: "",
1836
+ ...tool === "highlight" ? { highlight: true } : {}
1837
+ });
1838
+ } : void 0,
1839
+ onSelectMark: sending ? void 0 : (id) => {
1840
+ if (id === "draft") {
1841
+ setReviewOpen(true);
1842
+ return;
1843
+ }
1844
+ if (tool === "erase") {
1845
+ props.onDelete?.(id);
1846
+ return;
1847
+ }
1848
+ const note = notes.find((note) => note.id === id);
1849
+ if (note) focus(note);
1850
+ setReviewOpen(true);
1851
+ },
1852
+ children: /* @__PURE__ */ jsx(Image, {
1853
+ source: { uri: body.uri },
1854
+ style: {
1855
+ width,
1856
+ height
1857
+ },
1858
+ resizeMode: "contain"
1859
+ })
1860
+ });
1861
+ const sendMessage = status.state === "failed" ? `Couldn't send: ${status.message} Your notes are still here.` : status.state === "sent" ? "Sent to the agent. You can keep marking." : void 0;
1862
+ return /* @__PURE__ */ jsxs(View, {
1863
+ style: s$1.screen,
1864
+ children: [
1865
+ /* @__PURE__ */ jsxs(View, {
1866
+ style: s$1.header,
1867
+ children: [
1868
+ /* @__PURE__ */ jsx(Text, {
1869
+ style: s$1.meta,
1870
+ children: "Image feedback"
1871
+ }),
1872
+ /* @__PURE__ */ jsx(Text, {
1873
+ style: s$1.title,
1874
+ numberOfLines: 1,
1875
+ children: props.name
1876
+ }),
1877
+ props.ownedElsewhere ? /* @__PURE__ */ jsx(Badge, {
1878
+ tone: "magenta",
1879
+ children: "Also open on another device"
1880
+ }) : null,
1881
+ props.storageMessage ? /* @__PURE__ */ jsx(Text, {
1882
+ style: s$1.meta,
1883
+ accessibilityLiveRegion: "polite",
1884
+ children: props.storageMessage
1885
+ }) : null
1886
+ ]
1887
+ }),
1888
+ /* @__PURE__ */ jsxs(View, {
1889
+ style: s$1.navigation,
1890
+ children: [
1891
+ /* @__PURE__ */ jsx(Button, {
1892
+ size: "card",
1893
+ variant: "ghost",
1894
+ accessibilityLabel: "Zoom out",
1895
+ disabled: sending || viewport.zoom <= 1,
1896
+ onPress: () => zoomTo(viewport.zoom / 1.5),
1897
+ children: "-"
1898
+ }),
1899
+ /* @__PURE__ */ jsxs(Text, {
1900
+ style: s$1.zoom,
1901
+ accessibilityLabel: `Zoom ${Math.round(viewport.zoom * 100)} percent`,
1902
+ children: [Math.round(viewport.zoom * 100), "%"]
1903
+ }),
1904
+ /* @__PURE__ */ jsx(Button, {
1905
+ size: "card",
1906
+ variant: "ghost",
1907
+ accessibilityLabel: "Zoom in",
1908
+ disabled: sending || viewport.zoom >= 4,
1909
+ onPress: () => zoomTo(viewport.zoom * 1.5),
1910
+ children: "+"
1911
+ }),
1912
+ /* @__PURE__ */ jsx(Button, {
1913
+ size: "card",
1914
+ variant: "outline",
1915
+ disabled: sending,
1916
+ onPress: () => setViewport((current) => ({
1917
+ ...current,
1918
+ zoom: 1,
1919
+ originX: 0,
1920
+ originY: 0
1921
+ })),
1922
+ children: "Fit"
1923
+ }),
1924
+ /* @__PURE__ */ jsx(View, { style: s$1.grow }),
1925
+ /* @__PURE__ */ jsx(Button, {
1926
+ size: "card",
1927
+ variant: "ghost",
1928
+ disabled: sending || !props.canUndo,
1929
+ onPress: props.onUndo,
1930
+ children: "Undo"
1931
+ }),
1932
+ /* @__PURE__ */ jsx(Button, {
1933
+ size: "card",
1934
+ variant: "ghost",
1935
+ disabled: sending || !props.canRedo,
1936
+ onPress: props.onRedo,
1937
+ children: "Redo"
1938
+ })
1939
+ ]
1940
+ }),
1941
+ /* @__PURE__ */ jsxs(View, {
1942
+ style: s$1.canvas,
1943
+ onLayout: (event) => {
1944
+ const { width, height } = event.nativeEvent.layout;
1945
+ if (Number.isFinite(width) && Number.isFinite(height)) setArea({
1946
+ width: Math.max(0, width),
1947
+ height: Math.max(0, height)
1948
+ });
1949
+ },
1950
+ children: [width > 0 && height > 0 ? /* @__PURE__ */ jsx(View, {
1951
+ style: {
1952
+ width,
1953
+ height
1954
+ },
1955
+ children: props.captureContent ? props.captureContent(annotation) : annotation
1956
+ }) : null, viewport.zoom > 1 && width > 0 ? /* @__PURE__ */ jsxs(View, {
1957
+ pointerEvents: "none",
1958
+ accessible: false,
1959
+ style: s$1.overview,
1960
+ children: [/* @__PURE__ */ jsx(Image, {
1961
+ source: { uri: body.uri },
1962
+ style: {
1963
+ width: overviewWidth,
1964
+ height: overviewHeight
1965
+ },
1966
+ resizeMode: "contain"
1967
+ }), /* @__PURE__ */ jsx(View, { style: [s$1.overviewWindow, {
1968
+ left: -viewport.originX / (viewport.zoom * width) * overviewWidth,
1969
+ top: -viewport.originY / (viewport.zoom * height) * overviewHeight,
1970
+ width: overviewWidth / viewport.zoom,
1971
+ height: overviewHeight / viewport.zoom
1972
+ }] })]
1973
+ }) : null]
1974
+ }),
1975
+ /* @__PURE__ */ jsxs(View, {
1976
+ style: s$1.dock,
1977
+ children: [
1978
+ /* @__PURE__ */ jsx(Text, {
1979
+ style: s$1.hint,
1980
+ accessibilityLiveRegion: "polite",
1981
+ children: TOOLS.find((item) => item.id === tool)?.hint
1982
+ }),
1983
+ /* @__PURE__ */ jsx(View, {
1984
+ style: s$1.tools,
1985
+ children: TOOLS.map((item) => /* @__PURE__ */ jsx(Button, {
1986
+ size: "card",
1987
+ variant: "ghost",
1988
+ style: [s$1.tool, tool === item.id && s$1.toolSelected],
1989
+ accessibilityLabel: `${item.label} tool`,
1990
+ accessibilityState: { selected: tool === item.id },
1991
+ disabled: sending || item.id !== "move" && !props.onAddMark,
1992
+ onPress: () => {
1993
+ selectionTick();
1994
+ setTool(item.id);
1995
+ },
1996
+ children: /* @__PURE__ */ jsx(Text, {
1997
+ style: [s$1.toolLabel, tool === item.id && s$1.toolLabelSelected],
1998
+ children: item.label
1999
+ })
2000
+ }, item.id))
2001
+ }),
2002
+ sendMessage ? /* @__PURE__ */ jsx(Text, {
2003
+ style: status.state === "failed" ? s$1.failed : s$1.meta,
2004
+ accessibilityLiveRegion: "polite",
2005
+ children: sendMessage
2006
+ }) : null,
2007
+ /* @__PURE__ */ jsxs(View, {
2008
+ style: s$1.summary,
2009
+ children: [/* @__PURE__ */ jsxs(View, {
2010
+ style: s$1.grow,
2011
+ children: [/* @__PURE__ */ jsxs(Text, {
2012
+ style: s$1.title,
2013
+ children: [
2014
+ notes.length,
2015
+ " mark",
2016
+ notes.length === 1 ? "" : "s"
2017
+ ]
2018
+ }), /* @__PURE__ */ jsx(Text, {
2019
+ style: s$1.meta,
2020
+ children: "Original stays unchanged"
2021
+ })]
2022
+ }), /* @__PURE__ */ jsx(Button, {
2023
+ size: "card",
2024
+ variant: reviewOpen ? "outline" : "primary",
2025
+ disabled: sending || !notes.length && !draft && !props.requests?.length,
2026
+ onPress: () => setReviewOpen(true),
2027
+ children: "Review & send"
2028
+ })]
2029
+ })
2030
+ ]
2031
+ }),
2032
+ /* @__PURE__ */ jsx(Sheet, {
2033
+ open: reviewOpen,
2034
+ onOpenChange: (open) => {
2035
+ if (!sending) setReviewOpen(open);
2036
+ },
2037
+ children: /* @__PURE__ */ jsxs(SheetContent, {
2038
+ keep: true,
2039
+ footer: /* @__PURE__ */ jsxs(View, {
2040
+ style: s$1.sheetActions,
2041
+ children: [
2042
+ sendMessage ? /* @__PURE__ */ jsx(Text, {
2043
+ style: status.state === "failed" ? s$1.failed : s$1.meta,
2044
+ accessibilityLiveRegion: "polite",
2045
+ children: sendMessage
2046
+ }) : null,
2047
+ /* @__PURE__ */ jsx(Text, {
2048
+ style: s$1.meta,
2049
+ children: stale ? "Some notes refer to an earlier image. Keep them and reopen that version before sending." : needsExplanation ? "Add an explanation to each mark before sending." : notes.length > SEND_LIMIT ? `Sends the first ${SEND_LIMIT} notes. The rest stay here for the next batch.` : "Sends the annotated image and your explanations together."
2050
+ }),
2051
+ /* @__PURE__ */ jsxs(View, {
2052
+ style: s$1.row,
2053
+ children: [/* @__PURE__ */ jsx(Button, {
2054
+ size: "card",
2055
+ variant: "ghost",
2056
+ style: s$1.grow,
2057
+ disabled: sending,
2058
+ onPress: () => setReviewOpen(false),
2059
+ children: "Keep marking"
2060
+ }), /* @__PURE__ */ jsx(Button, {
2061
+ size: "card",
2062
+ variant: "primary",
2063
+ style: s$1.grow,
2064
+ loading: sending,
2065
+ loadingLabel: "Sending feedback",
2066
+ disabled: !batch.length || needsExplanation || stale || !!draft || !props.onSend,
2067
+ onPress: send,
2068
+ children: status.state === "failed" ? "Try again" : `Send ${batch.length} note${batch.length === 1 ? "" : "s"}`
2069
+ })]
2070
+ })
2071
+ ]
2072
+ }),
2073
+ children: [/* @__PURE__ */ jsx(SheetHeader, {
2074
+ closeLabel: "Close review",
2075
+ dismissible: !sending,
2076
+ children: /* @__PURE__ */ jsx(SheetTitle, { children: "Review your marks" })
2077
+ }), /* @__PURE__ */ jsxs(SheetBody, { children: [
2078
+ /* @__PURE__ */ jsx(SheetDescription, { children: "Mark first, explain here. Tap a note to find its place." }),
2079
+ draft ? /* @__PURE__ */ jsxs(View, {
2080
+ style: s$1.note,
2081
+ children: [
2082
+ /* @__PURE__ */ jsx(Text, {
2083
+ style: s$1.title,
2084
+ children: "Unfinished note"
2085
+ }),
2086
+ /* @__PURE__ */ jsx(Textarea, {
2087
+ rows: 3,
2088
+ maxLength: 2048,
2089
+ accessibilityLabel: "Unfinished note explanation",
2090
+ value: draft.text,
2091
+ disabled: sending,
2092
+ onChangeText: props.onDraftChange
2093
+ }),
2094
+ /* @__PURE__ */ jsxs(View, {
2095
+ style: s$1.row,
2096
+ children: [/* @__PURE__ */ jsx(Button, {
2097
+ size: "card",
2098
+ variant: "danger-outline",
2099
+ disabled: sending,
2100
+ onPress: props.onDraftCancel,
2101
+ children: "Discard draft"
2102
+ }), /* @__PURE__ */ jsx(Button, {
2103
+ size: "card",
2104
+ disabled: sending || !draft.text.trim(),
2105
+ onPress: props.onDraftSave,
2106
+ children: "Keep note"
2107
+ })]
2108
+ })
2109
+ ]
2110
+ }) : null,
2111
+ !notes.length && !draft ? /* @__PURE__ */ jsx(Text, {
2112
+ style: s$1.meta,
2113
+ children: "No marks yet. Keep marking whenever you are ready."
2114
+ }) : null,
2115
+ notes.map((note, index) => /* @__PURE__ */ jsxs(View, {
2116
+ style: [s$1.note, selected?.id === note.id && s$1.noteSelected],
2117
+ children: [
2118
+ /* @__PURE__ */ jsxs(Button, {
2119
+ size: "card",
2120
+ variant: "ghost",
2121
+ accessibilityLabel: `Review mark ${index + 1}`,
2122
+ accessibilityState: { selected: selected?.id === note.id },
2123
+ disabled: sending,
2124
+ onPress: () => focus(note),
2125
+ children: [/* @__PURE__ */ jsx(Text, {
2126
+ style: s$1.noteNumber,
2127
+ children: index + 1
2128
+ }), /* @__PURE__ */ jsx(Text, {
2129
+ style: s$1.noteText,
2130
+ children: note.text || "Add an explanation"
2131
+ })]
2132
+ }),
2133
+ note.sourceSha256 !== void 0 && note.sourceSha256 !== props.sourceSha256 ? /* @__PURE__ */ jsx(Text, {
2134
+ style: s$1.meta,
2135
+ children: "Earlier image version"
2136
+ }) : null,
2137
+ selected?.id === note.id ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Textarea, {
2138
+ rows: 3,
2139
+ maxLength: 2048,
2140
+ accessibilityLabel: `Mark ${index + 1} explanation`,
2141
+ placeholder: "What should change here?",
2142
+ value: note.text,
2143
+ disabled: sending,
2144
+ onChangeText: (text) => props.onNoteChange?.(note.id, text)
2145
+ }), /* @__PURE__ */ jsxs(View, {
2146
+ style: s$1.row,
2147
+ children: [/* @__PURE__ */ jsx(Button, {
2148
+ size: "card",
2149
+ variant: "danger-outline",
2150
+ disabled: sending || !props.onDelete,
2151
+ onPress: () => props.onDelete?.(note.id),
2152
+ children: "Remove mark"
2153
+ }), note.points && props.onCrop ? /* @__PURE__ */ jsx(Button, {
2154
+ size: "card",
2155
+ loading: props.cropping,
2156
+ disabled: sending,
2157
+ onPress: () => {
2158
+ setReviewOpen(false);
2159
+ props.onCrop?.();
2160
+ },
2161
+ children: "Preview crop"
2162
+ }) : null]
2163
+ })] }) : null
2164
+ ]
2165
+ }, note.id)),
2166
+ props.requests?.map((request, index) => /* @__PURE__ */ jsxs(View, { children: [/* @__PURE__ */ jsxs(Text, {
2167
+ style: request.status === "FAILED" ? s$1.failed : s$1.meta,
2168
+ children: [
2169
+ "Request ",
2170
+ index + 1,
2171
+ ": ",
2172
+ request.status.toLowerCase()
2173
+ ]
2174
+ }), request.error ? /* @__PURE__ */ jsx(Text, {
2175
+ style: s$1.failed,
2176
+ children: request.error
2177
+ }) : null] }, request.id))
2178
+ ] })]
2179
+ })
2180
+ })
2181
+ ]
2182
+ });
2183
+ }
2184
+ const s$1 = StyleSheet.create((theme, rt) => ({
2185
+ screen: {
2186
+ flex: 1,
2187
+ backgroundColor: theme.colors.bg,
2188
+ uni__dependencies: [0]
2189
+ },
2190
+ header: {
2191
+ paddingHorizontal: theme.spacing(4),
2192
+ paddingTop: theme.spacing(3),
2193
+ gap: theme.spacing(1),
2194
+ alignItems: "flex-start",
2195
+ uni__dependencies: [0]
2196
+ },
2197
+ title: {
2198
+ color: theme.colors.hi,
2199
+ fontFamily: fontOf(theme.fontFamily.sans),
2200
+ fontSize: theme.fontSize.lg,
2201
+ lineHeight: theme.lineHeight.lg,
2202
+ fontWeight: theme.fontWeight.semibold,
2203
+ uni__dependencies: [0]
2204
+ },
2205
+ meta: {
2206
+ color: theme.colors.dim,
2207
+ fontFamily: fontOf(theme.fontFamily.sans),
2208
+ fontSize: theme.fontSize.sm,
2209
+ lineHeight: theme.lineHeight.sm,
2210
+ uni__dependencies: [0]
2211
+ },
2212
+ navigation: {
2213
+ flexDirection: "row",
2214
+ flexWrap: "wrap",
2215
+ alignItems: "center",
2216
+ paddingHorizontal: theme.spacing(2),
2217
+ gap: theme.spacing(1),
2218
+ uni__dependencies: [0]
2219
+ },
2220
+ zoom: {
2221
+ color: theme.colors.hi,
2222
+ fontFamily: fontOf(theme.fontFamily.sans),
2223
+ fontSize: theme.fontSize.sm,
2224
+ minWidth: 40,
2225
+ textAlign: "center",
2226
+ uni__dependencies: [0]
2227
+ },
2228
+ canvas: {
2229
+ flex: 1,
2230
+ minHeight: 44,
2231
+ alignItems: "center",
2232
+ justifyContent: "center",
2233
+ marginHorizontal: theme.spacing(4),
2234
+ marginBottom: theme.spacing(3),
2235
+ backgroundColor: theme.colors.deep,
2236
+ overflow: "hidden",
2237
+ borderRadius: theme.borderRadius.md,
2238
+ uni__dependencies: [0]
2239
+ },
2240
+ overview: {
2241
+ position: "absolute",
2242
+ right: theme.spacing(2),
2243
+ top: theme.spacing(2),
2244
+ borderWidth: 1,
2245
+ borderColor: theme.colors.border,
2246
+ backgroundColor: theme.colors.rail,
2247
+ overflow: "hidden",
2248
+ uni__dependencies: [0]
2249
+ },
2250
+ overviewWindow: {
2251
+ position: "absolute",
2252
+ borderWidth: 2,
2253
+ borderColor: theme.colors.green,
2254
+ uni__dependencies: [0]
2255
+ },
2256
+ dock: {
2257
+ padding: theme.spacing(3),
2258
+ paddingBottom: Math.max(theme.spacing(3), rt.insets.bottom),
2259
+ gap: theme.spacing(3),
2260
+ borderTopWidth: 1,
2261
+ borderTopColor: theme.colors.border,
2262
+ backgroundColor: theme.colors.rail,
2263
+ uni__dependencies: [0, 9]
2264
+ },
2265
+ hint: {
2266
+ color: theme.colors.dim,
2267
+ fontFamily: fontOf(theme.fontFamily.sans),
2268
+ fontSize: theme.fontSize.sm,
2269
+ lineHeight: theme.lineHeight.sm,
2270
+ minHeight: theme.lineHeight.sm,
2271
+ uni__dependencies: [0]
2272
+ },
2273
+ tools: {
2274
+ flexDirection: "row",
2275
+ flexWrap: "wrap",
2276
+ gap: theme.spacing(2),
2277
+ uni__dependencies: [0]
2278
+ },
2279
+ tool: {
2280
+ flexBasis: "22%",
2281
+ flexGrow: 1,
2282
+ minHeight: 48,
2283
+ borderWidth: 1,
2284
+ borderColor: theme.colors.border,
2285
+ uni__dependencies: [0]
2286
+ },
2287
+ toolSelected: {
2288
+ borderColor: theme.colors["edge-green"],
2289
+ backgroundColor: theme.colors["tint-green"],
2290
+ uni__dependencies: [0]
2291
+ },
2292
+ toolLabel: {
2293
+ color: theme.colors.dim,
2294
+ fontFamily: fontOf(theme.fontFamily.sans),
2295
+ fontSize: theme.fontSize.base,
2296
+ uni__dependencies: [0]
2297
+ },
2298
+ toolLabelSelected: {
2299
+ color: theme.colors.green,
2300
+ fontWeight: theme.fontWeight.semibold,
2301
+ uni__dependencies: [0]
2302
+ },
2303
+ summary: {
2304
+ flexDirection: "row",
2305
+ alignItems: "center",
2306
+ gap: theme.spacing(3),
2307
+ uni__dependencies: [0]
2308
+ },
2309
+ row: {
2310
+ flexDirection: "row",
2311
+ flexWrap: "wrap",
2312
+ gap: theme.spacing(2),
2313
+ uni__dependencies: [0]
2314
+ },
2315
+ grow: { flex: 1 },
2316
+ sheetActions: {
2317
+ gap: theme.spacing(3),
2318
+ paddingHorizontal: theme.spacing(4),
2319
+ paddingVertical: theme.spacing(3),
2320
+ uni__dependencies: [0]
2321
+ },
2322
+ note: {
2323
+ borderWidth: 1,
2324
+ borderColor: theme.colors.border,
2325
+ borderRadius: theme.borderRadius.md,
2326
+ padding: theme.spacing(2),
2327
+ gap: theme.spacing(2),
2328
+ uni__dependencies: [0]
2329
+ },
2330
+ noteSelected: {
2331
+ borderColor: theme.colors["edge-magenta"],
2332
+ uni__dependencies: [0]
2333
+ },
2334
+ noteNumber: {
2335
+ color: theme.colors.magenta,
2336
+ fontFamily: fontOf(theme.fontFamily.sans),
2337
+ fontSize: theme.fontSize.base,
2338
+ fontWeight: theme.fontWeight.semibold,
2339
+ uni__dependencies: [0]
2340
+ },
2341
+ noteText: {
2342
+ flex: 1,
2343
+ color: theme.colors.hi,
2344
+ fontFamily: fontOf(theme.fontFamily.sans),
2345
+ fontSize: theme.fontSize.base,
2346
+ lineHeight: theme.lineHeight.base,
2347
+ uni__dependencies: [0]
2348
+ },
2349
+ failed: {
2350
+ color: theme.colors.red,
2351
+ fontFamily: fontOf(theme.fontFamily.sans),
2352
+ fontSize: theme.fontSize.sm,
2353
+ lineHeight: theme.lineHeight.sm,
2354
+ uni__dependencies: [0]
2355
+ }
2356
+ }));
2357
+ //#endregion
1627
2358
  //#region src/extensions/workspaces/sessions/(frontend)/_components/AuthorDocumentView.native.tsx
1628
2359
  const DRAFT_ID = "draft";
1629
2360
  /** One open document on the phone: the content to mark up, its notes, and one way to hand them to the agent. */
@@ -1638,6 +2369,10 @@ function AuthorDocumentView(props) {
1638
2369
  const [sourceSelection, setSourceSelection] = useState();
1639
2370
  const sourceKey = `${props.name}\n${props.sourceSha256 ?? ""}`;
1640
2371
  const selectedRange = body.kind === "text" && sourceSelection?.key === sourceKey ? nativeAuthorTextRange(body.text, sourceSelection) : void 0;
2372
+ if (body.kind === "image" && !props.edit) return /* @__PURE__ */ jsx(AuthorCanvasWorkspace, {
2373
+ ...props,
2374
+ body
2375
+ }, sourceKey);
1641
2376
  const canInspectSource = body.kind === "text" && props.nativeContent === void 0;
1642
2377
  const markdown = body.kind === "text" && /\.(?:md|markdown)$/iu.test(props.name);
1643
2378
  const rangeLabel = (note) => {
@@ -2386,7 +3121,21 @@ function AuthorDocumentPanel({ path, ...props }) {
2386
3121
  const setDraft = (value) => setForm(key, value);
2387
3122
  const [storageState, setStorageState] = useState();
2388
3123
  const [selectedId, setSelectedId] = useState();
2389
- const [status, setStatus] = useState({ state: "idle" });
3124
+ const [sendState, setSendState] = useState({
3125
+ key,
3126
+ status: { state: "idle" }
3127
+ });
3128
+ const status = sendState.key === key ? sendState.status : { state: "idle" };
3129
+ const setStatus = (status) => setSendState({
3130
+ key,
3131
+ status
3132
+ });
3133
+ const history = useRef({
3134
+ key,
3135
+ past: [],
3136
+ future: []
3137
+ });
3138
+ const noteSequence = useRef(0);
2390
3139
  const requests = useSyncExternalStore(subscribeRequests, () => nativeAuthorRequests.select(nativeAuthorRequests.store.state, sessionId));
2391
3140
  const sending = useRef(false);
2392
3141
  const [mediaPage, setMediaPage] = useState(1);
@@ -2411,6 +3160,41 @@ function AuthorDocumentPanel({ path, ...props }) {
2411
3160
  };
2412
3161
  else if (captureRef.current?.key === key) captureRef.current = void 0;
2413
3162
  }, [key]);
3163
+ const changeNotes = (change, group) => {
3164
+ if (sending.current === "submitting") return;
3165
+ const current = notesStore.store.state[key] ?? EMPTY;
3166
+ const next = change(current);
3167
+ if (next === current) return;
3168
+ const previous = history.current;
3169
+ const valid = previous.key === key && previous.present === current;
3170
+ history.current = {
3171
+ key,
3172
+ present: next,
3173
+ group,
3174
+ future: [],
3175
+ past: valid && group !== void 0 && previous.group === group ? previous.past : [...valid ? previous.past : [], current].slice(-50)
3176
+ };
3177
+ setNotes(key, () => next);
3178
+ setStatus({ state: "idle" });
3179
+ };
3180
+ const travelHistory = (direction) => {
3181
+ const timeline = history.current;
3182
+ const current = notesStore.store.state[key] ?? EMPTY;
3183
+ if (sending.current || timeline.key !== key || timeline.present !== current) return;
3184
+ const target = timeline[direction].at(-1);
3185
+ if (!target) return;
3186
+ const opposite = direction === "past" ? "future" : "past";
3187
+ history.current = {
3188
+ ...timeline,
3189
+ present: target,
3190
+ group: void 0,
3191
+ [direction]: timeline[direction].slice(0, -1),
3192
+ [opposite]: [...timeline[opposite], current].slice(-50)
3193
+ };
3194
+ setNotes(key, () => target);
3195
+ setSelectedId(void 0);
3196
+ setStatus({ state: "idle" });
3197
+ };
2414
3198
  useEffect(() => {
2415
3199
  if (sessionId === null) return;
2416
3200
  const controller = new AbortController();
@@ -2591,17 +3375,21 @@ function AuthorDocumentPanel({ path, ...props }) {
2591
3375
  const visibleNotes = nativeAuthorVisibleNotes(notes, current.file.kind, mediaPage, videoFrame?.timeSeconds);
2592
3376
  const send = async () => {
2593
3377
  if (sending.current) return;
2594
- sending.current = true;
2595
- const sent = visibleNotes.slice(0, 16);
3378
+ sending.current = "capturing";
3379
+ const sourceNotes = notesStore.store.state[key] ?? EMPTY;
3380
+ const sent = nativeAuthorVisibleNotes(sourceNotes, current.file.kind, mediaPage, videoFrame?.timeSeconds).slice(0, 16);
2596
3381
  const capturedAt = Date.now();
2597
3382
  const captureId = `native-author-${String(capturedAt)}-${sent[0]?.id ?? "notes"}`;
2598
3383
  let recorded = false;
2599
3384
  try {
2600
3385
  if (!sent.length) throw new Error("Open a marked page or capture its video frame before sending notes.");
3386
+ if (sent.some((note) => !note.text.trim())) throw new Error("Add an explanation to each mark before sending.");
2601
3387
  if (!props.submitCapture) throw new Error("This host cannot confirm feedback delivery. Your notes have not been sent.");
2602
3388
  const provider = captureRef.current;
2603
3389
  if (provider?.key !== key) throw new Error("The document is not ready to capture. Try again when it has finished opening.");
2604
3390
  const { capture, viewport } = await captureNativeAuthorViewport(provider.capture, () => geometry.current?.key === key ? geometry.current.viewport : void 0);
3391
+ if ((notesStore.store.state[key] ?? EMPTY) !== sourceNotes) throw new Error("The notes changed during capture. Keep them still and try again.");
3392
+ sending.current = "submitting";
2605
3393
  setStatus({ state: "sending" });
2606
3394
  if (capture.bytes.byteLength > 2097152 || Math.max(capture.width, capture.height) > 1600) throw new Error("This capture exceeds the Author evidence budget.");
2607
3395
  const { file, body } = current;
@@ -2624,13 +3412,18 @@ function AuthorDocumentPanel({ path, ...props }) {
2624
3412
  mimeType: capture.mimeType,
2625
3413
  context: authorCaptureContext(packet)
2626
3414
  });
3415
+ if (history.current.key === key) history.current = {
3416
+ key,
3417
+ past: [],
3418
+ future: []
3419
+ };
2627
3420
  setNotes(key, (notes) => notes.filter((note) => !sent.includes(note)));
2628
3421
  setSelectedId(void 0);
2629
- setStatus({ state: "sent" });
2630
3422
  await persistNativeDraft(key, path, props.nativeAuthor).catch((error) => setStorageState({
2631
3423
  key,
2632
3424
  message: `Sent, but couldn't update the stored draft: ${String(error)} It may reappear after restart.`
2633
3425
  }));
3426
+ setStatus({ state: "sent" });
2634
3427
  } catch (error) {
2635
3428
  if (recorded) failNativeAuthorRequest(sessionId, captureId, error);
2636
3429
  setStatus({
@@ -2904,17 +3697,56 @@ function AuthorDocumentPanel({ path, ...props }) {
2904
3697
  points
2905
3698
  });
2906
3699
  },
3700
+ onAddMark: current.file.kind === "image" ? (mark) => {
3701
+ if (sending.current === "submitting") return;
3702
+ if ((notesStore.store.state[key]?.length ?? 0) >= 100) {
3703
+ setStorageState({
3704
+ key,
3705
+ message: "This draft has 100 marks. Send or remove some before adding more."
3706
+ });
3707
+ return;
3708
+ }
3709
+ try {
3710
+ noteSequence.current += 1;
3711
+ const note = parseNativeAuthorDraft({
3712
+ version: 1,
3713
+ notes: [{
3714
+ ...mark,
3715
+ id: `note-${Date.now()}-${noteSequence.current}`,
3716
+ sourceSha256: current.file.sourceSha256
3717
+ }]
3718
+ }).notes[0];
3719
+ changeNotes((notes) => [...notes, note]);
3720
+ } catch (error) {
3721
+ setStorageState({
3722
+ key,
3723
+ message: `Couldn't add this mark: ${error instanceof Error ? error.message : String(error)} Your other notes are still here.`
3724
+ });
3725
+ }
3726
+ } : void 0,
3727
+ onNoteChange: (id, text) => {
3728
+ if (text.length > 2048) return;
3729
+ changeNotes((notes) => notes.map((note) => note.id === id ? {
3730
+ ...note,
3731
+ text
3732
+ } : note), id);
3733
+ },
3734
+ canUndo: history.current.key === key && history.current.present === notes && history.current.past.length > 0,
3735
+ canRedo: history.current.key === key && history.current.present === notes && history.current.future.length > 0,
3736
+ onUndo: () => travelHistory("past"),
3737
+ onRedo: () => travelHistory("future"),
2907
3738
  onSelect: (id) => {
3739
+ history.current.group = void 0;
2908
3740
  const note = notes.find((note) => note.id === id);
2909
3741
  if (note?.page !== void 0) setMediaPage(note.page);
2910
3742
  if (note?.timeSeconds !== void 0) {
2911
3743
  setVideoFrame(void 0);
2912
3744
  setSeekSeconds(note.timeSeconds);
2913
3745
  }
2914
- setSelectedId((value) => value === id ? void 0 : id);
3746
+ setSelectedId((value) => current.file.kind === "image" ? id : value === id ? void 0 : id);
2915
3747
  },
2916
3748
  onDelete: (id) => {
2917
- setNotes(key, (notes) => notes.filter((note) => note.id !== id));
3749
+ changeNotes((notes) => notes.filter((note) => note.id !== id));
2918
3750
  if (selectedId === id) setSelectedId(void 0);
2919
3751
  },
2920
3752
  onDraftChange: (text) => {
@@ -2926,9 +3758,18 @@ function AuthorDocumentPanel({ path, ...props }) {
2926
3758
  onDraftCancel: () => setDraft(void 0),
2927
3759
  onDraftSave: () => {
2928
3760
  if (draft === void 0 || draft.text.trim() === "") return;
2929
- setNotes(key, (notes) => [...notes, {
2930
- id: `note-${String(Date.now())}-${String(notes.length)}`,
2931
- ...draft
3761
+ if (sending.current) return;
3762
+ if (notes.length >= 100) {
3763
+ setStorageState({
3764
+ key,
3765
+ message: "This draft has 100 marks. Send or remove some before adding more."
3766
+ });
3767
+ return;
3768
+ }
3769
+ noteSequence.current += 1;
3770
+ changeNotes((notes) => [...notes, {
3771
+ ...draft,
3772
+ id: `note-${Date.now()}-${noteSequence.current}`
2932
3773
  }]);
2933
3774
  setDraft(void 0);
2934
3775
  if (status.state !== "sending") setStatus({ state: "idle" });
@@ -2952,7 +3793,7 @@ const authorNativeOpenSourceAction = {
2952
3793
  return {
2953
3794
  id: tabId(sessionId, valid),
2954
3795
  label: fileName(valid),
2955
- retainComposer: true,
3796
+ retainComposer: !authorNativeIsImage(valid),
2956
3797
  panel: (props) => {
2957
3798
  if (/\.stories\.(?:jsx?|tsx?)$/iu.test(valid)) {
2958
3799
  const source = {