@stigmer/react 3.8.0 → 3.9.0

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.
Files changed (52) hide show
  1. package/attachment/AttachmentChipList.d.ts.map +1 -1
  2. package/attachment/AttachmentChipList.js +30 -1
  3. package/attachment/AttachmentChipList.js.map +1 -1
  4. package/attachment/attachment-utils.d.ts +11 -0
  5. package/attachment/attachment-utils.d.ts.map +1 -1
  6. package/attachment/attachment-utils.js +23 -0
  7. package/attachment/attachment-utils.js.map +1 -1
  8. package/attachment/clipboard.d.ts +51 -0
  9. package/attachment/clipboard.d.ts.map +1 -0
  10. package/attachment/clipboard.js +89 -0
  11. package/attachment/clipboard.js.map +1 -0
  12. package/attachment/index.d.ts +6 -1
  13. package/attachment/index.d.ts.map +1 -1
  14. package/attachment/index.js +4 -1
  15. package/attachment/index.js.map +1 -1
  16. package/attachment/prepare-image.d.ts +43 -0
  17. package/attachment/prepare-image.d.ts.map +1 -0
  18. package/attachment/prepare-image.js +162 -0
  19. package/attachment/prepare-image.js.map +1 -0
  20. package/attachment/useAttachments.d.ts.map +1 -1
  21. package/attachment/useAttachments.js +23 -3
  22. package/attachment/useAttachments.js.map +1 -1
  23. package/attachment/vision-fit.d.ts +50 -0
  24. package/attachment/vision-fit.d.ts.map +1 -0
  25. package/attachment/vision-fit.js +76 -0
  26. package/attachment/vision-fit.js.map +1 -0
  27. package/composer/SessionComposer.d.ts +3 -1
  28. package/composer/SessionComposer.d.ts.map +1 -1
  29. package/composer/SessionComposer.js +50 -4
  30. package/composer/SessionComposer.js.map +1 -1
  31. package/index.d.ts +2 -2
  32. package/index.d.ts.map +1 -1
  33. package/index.js +3 -2
  34. package/index.js.map +1 -1
  35. package/package.json +4 -4
  36. package/src/attachment/AttachmentChipList.tsx +45 -1
  37. package/src/attachment/__tests__/AttachmentChipList.test.tsx +103 -0
  38. package/src/attachment/__tests__/attachment-utils.test.ts +54 -0
  39. package/src/attachment/__tests__/clipboard.test.ts +110 -0
  40. package/src/attachment/__tests__/prepare-image.browser.test.ts +166 -0
  41. package/src/attachment/__tests__/prepare-image.test.ts +32 -0
  42. package/src/attachment/__tests__/vision-fit.test.ts +93 -0
  43. package/src/attachment/attachment-utils.ts +27 -0
  44. package/src/attachment/clipboard.ts +102 -0
  45. package/src/attachment/index.ts +13 -0
  46. package/src/attachment/prepare-image.ts +179 -0
  47. package/src/attachment/useAttachments.ts +26 -2
  48. package/src/attachment/vision-fit.ts +90 -0
  49. package/src/composer/SessionComposer.tsx +73 -4
  50. package/src/composer/__tests__/SessionComposer-paste.test.tsx +263 -0
  51. package/src/composer/__tests__/SessionComposer-uploadGate.test.tsx +239 -0
  52. package/src/index.ts +11 -1
@@ -1 +1 @@
1
- {"version":3,"file":"AttachmentChipList.d.ts","sourceRoot":"","sources":["../../src/attachment/AttachmentChipList.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAG3D,4CAA4C;AAC5C,MAAM,WAAW,uBAAuB;IACtC,sDAAsD;IACtD,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C,qCAAqC;IACrC,QAAQ,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,uDAAuD;IACvD,QAAQ,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,yCAAyC;IACzC,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,yDAAyD;IACzD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,OAAO,EACP,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,SAAS,GACV,EAAE,uBAAuB,kDAoBzB"}
1
+ {"version":3,"file":"AttachmentChipList.d.ts","sourceRoot":"","sources":["../../src/attachment/AttachmentChipList.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAG3D,4CAA4C;AAC5C,MAAM,WAAW,uBAAuB;IACtC,sDAAsD;IACtD,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C,qCAAqC;IACrC,QAAQ,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,uDAAuD;IACvD,QAAQ,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,yCAAyC;IACzC,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,yDAAyD;IACzD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,OAAO,EACP,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,SAAS,GACV,EAAE,uBAAuB,kDAoBzB"}
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from "react";
3
4
  import { cn } from "@stigmer/theme";
4
5
  import { formatFileSize } from "./attachment-utils.js";
5
6
  /**
@@ -40,7 +41,35 @@ function AttachmentChip({ entry, onRemove, onRetry, disabled, }) {
40
41
  const isUploading = entry.phase === "uploading";
41
42
  return (_jsxs("span", { role: "listitem", "aria-label": `${entry.file.name}, ${formatFileSize(entry.file.size)}${isUploading ? ", uploading" : ""}${isError ? ", upload failed" : ""}`, className: cn("inline-flex max-w-[200px] items-center gap-1 rounded-md px-2 py-0.5 text-xs", isError
42
43
  ? "border border-destructive/30 bg-destructive-subtle text-destructive"
43
- : "bg-muted-subtle text-foreground", isUploading && "opacity-70"), children: [isUploading && _jsx(ChipSpinner, {}), isError && _jsx(ErrorDot, {}), !isUploading && !isError && _jsx(FileIcon, {}), _jsx("span", { className: "truncate", children: entry.file.name }), _jsx("span", { className: "shrink-0 text-[0.6rem] tabular-nums text-muted-foreground", children: formatFileSize(entry.file.size) }), isError && (_jsx("button", { type: "button", onClick: onRetry, disabled: disabled, className: "shrink-0 text-[0.6rem] font-medium text-destructive underline hover:text-destructive-muted disabled:pointer-events-none", "aria-label": `Retry uploading ${entry.file.name}`, children: "Retry" })), _jsx("button", { type: "button", onClick: onRemove, disabled: disabled, className: "ml-0.5 shrink-0 text-muted-foreground hover:text-destructive disabled:pointer-events-none", "aria-label": `Remove ${entry.file.name}`, children: _jsx(XIcon, {}) })] }));
44
+ : "bg-muted-subtle text-foreground", isUploading && "opacity-70"), children: [isUploading && _jsx(ChipSpinner, {}), isError && _jsx(ErrorDot, {}), !isUploading && !isError &&
45
+ (entry.contentType.startsWith("image/") ? (_jsx(ChipThumbnail, { file: entry.file })) : (_jsx(FileIcon, {}))), _jsx("span", { className: "truncate", children: entry.file.name }), _jsx("span", { className: "shrink-0 text-[0.6rem] tabular-nums text-muted-foreground", children: formatFileSize(entry.file.size) }), isError && (_jsx("button", { type: "button", onClick: onRetry, disabled: disabled, className: "shrink-0 text-[0.6rem] font-medium text-destructive underline hover:text-destructive-muted disabled:pointer-events-none", "aria-label": `Retry uploading ${entry.file.name}`, children: "Retry" })), _jsx("button", { type: "button", onClick: onRemove, disabled: disabled, className: "ml-0.5 shrink-0 text-muted-foreground hover:text-destructive disabled:pointer-events-none", "aria-label": `Remove ${entry.file.name}`, children: _jsx(XIcon, {}) })] }));
46
+ }
47
+ // ---------------------------------------------------------------------------
48
+ // Image thumbnail — a #284 ask: a pasted screenshot's chip shows the image,
49
+ // not a generic file glyph
50
+ // ---------------------------------------------------------------------------
51
+ /**
52
+ * Tiny preview of an image attachment, driven by an object URL over the
53
+ * locally-held `File` (no fetch — the bytes are already in memory).
54
+ *
55
+ * The URL is created in an effect, not a memo: StrictMode double-invokes
56
+ * memos and would leak the first URL without a revoke. The effect cleanup
57
+ * revokes on removal, unmount, and file change. Until the effect runs (or
58
+ * if it never gets a URL) the generic icon renders instead.
59
+ *
60
+ * `aria-hidden`: the chip's own `aria-label` already names the file; a
61
+ * 16px thumbnail adds nothing for screen readers.
62
+ */
63
+ function ChipThumbnail({ file }) {
64
+ const [url, setUrl] = useState(null);
65
+ useEffect(() => {
66
+ const objectUrl = URL.createObjectURL(file);
67
+ setUrl(objectUrl);
68
+ return () => URL.revokeObjectURL(objectUrl);
69
+ }, [file]);
70
+ if (!url)
71
+ return _jsx(FileIcon, {});
72
+ return (_jsx("img", { src: url, alt: "", "aria-hidden": "true", className: "h-4 w-4 shrink-0 rounded-sm object-cover" }));
44
73
  }
45
74
  // ---------------------------------------------------------------------------
46
75
  // Inline SVG icons — kept minimal and consistent with SessionComposer icons
@@ -1 +1 @@
1
- {"version":3,"file":"AttachmentChipList.js","sourceRoot":"","sources":["../../src/attachment/AttachmentChipList.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AttachmentChipList.js","sourceRoot":"","sources":["../../src/attachment/AttachmentChipList.tsx"],"names":[],"mappings":"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"}
@@ -29,4 +29,15 @@ export declare function formatFileSize(bytes: number): string;
29
29
  * file exceeds {@link MAX_ATTACHMENT_BYTES}.
30
30
  */
31
31
  export declare function validateAttachmentSize(file: File): string | null;
32
+ /**
33
+ * Returns `name` unchanged when it is not in `taken`, otherwise the first
34
+ * free `stem-2.ext`, `stem-3.ext`, … variant.
35
+ *
36
+ * Duplicate filenames within one turn are not a cosmetic problem:
37
+ * attachments materialize at `.stigmer/inputs/{filename}`, where the
38
+ * deep-agent harness fails the whole execution on a mount-path collision
39
+ * and the Cursor harness silently overwrites the earlier file. A visible
40
+ * rename on the attachment chip is strictly better than either outcome.
41
+ */
42
+ export declare function uniquifyFilename(name: string, taken: ReadonlySet<string>): string;
32
43
  //# sourceMappingURL=attachment-utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"attachment-utils.d.ts","sourceRoot":"","sources":["../../src/attachment/attachment-utils.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,QAAmB,CAAC;AAiDrD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAapD;AAID;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAapD;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAKhE"}
1
+ {"version":3,"file":"attachment-utils.d.ts","sourceRoot":"","sources":["../../src/attachment/attachment-utils.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,QAAmB,CAAC;AAiDrD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAapD;AAID;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAapD;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAKhE;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,GACzB,MAAM,CAYR"}
@@ -101,4 +101,27 @@ export function validateAttachmentSize(file) {
101
101
  }
102
102
  return null;
103
103
  }
104
+ /**
105
+ * Returns `name` unchanged when it is not in `taken`, otherwise the first
106
+ * free `stem-2.ext`, `stem-3.ext`, … variant.
107
+ *
108
+ * Duplicate filenames within one turn are not a cosmetic problem:
109
+ * attachments materialize at `.stigmer/inputs/{filename}`, where the
110
+ * deep-agent harness fails the whole execution on a mount-path collision
111
+ * and the Cursor harness silently overwrites the earlier file. A visible
112
+ * rename on the attachment chip is strictly better than either outcome.
113
+ */
114
+ export function uniquifyFilename(name, taken) {
115
+ if (!taken.has(name))
116
+ return name;
117
+ const dotIndex = name.lastIndexOf(".");
118
+ // A leading dot (".env") is a hidden-file prefix, not an extension.
119
+ const stem = dotIndex > 0 ? name.slice(0, dotIndex) : name;
120
+ const ext = dotIndex > 0 ? name.slice(dotIndex) : "";
121
+ for (let n = 2;; n++) {
122
+ const candidate = `${stem}-${n}${ext}`;
123
+ if (!taken.has(candidate))
124
+ return candidate;
125
+ }
126
+ }
104
127
  //# sourceMappingURL=attachment-utils.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"attachment-utils.js","sourceRoot":"","sources":["../../src/attachment/attachment-utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"attachment-utils.js","sourceRoot":"","sources":["../../src/attachment/attachment-utils.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Clipboard file extraction for composer surfaces — the paste half of
3
+ * "paste a screenshot and the agent sees it" (stigmer/stigmer#284).
4
+ *
5
+ * Extraction is deliberately SYNCHRONOUS. Clipboard file handles are only
6
+ * reliable while the paste event is being dispatched, and the caller must
7
+ * decide `preventDefault()` in the same tick (a copied image usually carries
8
+ * an HTML/text flavor that would otherwise paste as junk markup). An async
9
+ * "one call" API here would pass in tests and lose pastes in real browsers.
10
+ * Async work on the extracted files (e.g. {@link prepareImageForVision})
11
+ * happens after extraction, on plain `File` objects that stay valid.
12
+ */
13
+ /**
14
+ * Structural source for {@link extractClipboardFiles}: both the native
15
+ * `ClipboardEvent` and React's synthetic clipboard event satisfy it.
16
+ */
17
+ export interface ClipboardFilesSource {
18
+ readonly clipboardData: {
19
+ readonly files: FileList;
20
+ } | null;
21
+ }
22
+ /**
23
+ * Extracts files from a paste event, giving clipboard images that carry the
24
+ * browser's generic `image.png` name a unique, human-readable one
25
+ * (`pasted-image-<HHMMSS>-<n>.<ext>`).
26
+ *
27
+ * Unique names are load-bearing, not cosmetic: attachments mount at
28
+ * `.stigmer/inputs/{filename}`, and two same-named attachments either fail
29
+ * the execution (deep-agent harness) or silently overwrite each other
30
+ * (Cursor harness). Files with real names (pasted from a file manager) are
31
+ * returned unchanged.
32
+ *
33
+ * Returns an empty array for a text-only paste — callers use that to let
34
+ * the default text insertion proceed untouched.
35
+ *
36
+ * Must be called synchronously from the paste event handler. Call
37
+ * `event.preventDefault()` in the same tick when the result is non-empty;
38
+ * the returned `File` objects remain valid afterwards.
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * function handlePaste(e: React.ClipboardEvent<HTMLTextAreaElement>) {
43
+ * const files = extractClipboardFiles(e);
44
+ * if (files.length === 0) return; // plain text paste — leave it alone
45
+ * e.preventDefault();
46
+ * attachments.addFiles(files);
47
+ * }
48
+ * ```
49
+ */
50
+ export declare function extractClipboardFiles(event: ClipboardFilesSource): File[];
51
+ //# sourceMappingURL=clipboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clipboard.d.ts","sourceRoot":"","sources":["../../src/attachment/clipboard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,aAAa,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAA;KAAE,GAAG,IAAI,CAAC;CAC7D;AAuCD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,oBAAoB,GAAG,IAAI,EAAE,CAezE"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Clipboard file extraction for composer surfaces — the paste half of
3
+ * "paste a screenshot and the agent sees it" (stigmer/stigmer#284).
4
+ *
5
+ * Extraction is deliberately SYNCHRONOUS. Clipboard file handles are only
6
+ * reliable while the paste event is being dispatched, and the caller must
7
+ * decide `preventDefault()` in the same tick (a copied image usually carries
8
+ * an HTML/text flavor that would otherwise paste as junk markup). An async
9
+ * "one call" API here would pass in tests and lose pastes in real browsers.
10
+ * Async work on the extracted files (e.g. {@link prepareImageForVision})
11
+ * happens after extraction, on plain `File` objects that stay valid.
12
+ */
13
+ /**
14
+ * Names browsers assign to clipboard images that never had a real filename
15
+ * (a screenshot, an image copied off a web page). Chrome, Firefox, and
16
+ * Safari all use `image.<ext>`. A file pasted from the OS file manager
17
+ * keeps its real name and never matches.
18
+ */
19
+ const GENERIC_CLIPBOARD_IMAGE_NAME = /^image\.(png|jpe?g|gif|webp|tiff?|bmp|avif)$/i;
20
+ /** Extension for a synthesized name, keyed by the clipboard MIME type. */
21
+ const IMAGE_MIME_EXTENSIONS = {
22
+ "image/png": "png",
23
+ "image/jpeg": "jpg",
24
+ "image/gif": "gif",
25
+ "image/webp": "webp",
26
+ "image/tiff": "tiff",
27
+ "image/bmp": "bmp",
28
+ "image/avif": "avif",
29
+ "image/svg+xml": "svg",
30
+ };
31
+ /**
32
+ * Monotonic per-page-session counter. Combined with the time component this
33
+ * keeps synthesized names unique even across page reloads within one agent
34
+ * session — required because attachments materialize at
35
+ * `.stigmer/inputs/{filename}` in a session-scoped directory, where a
36
+ * repeated name from a later turn silently replaces the earlier turn's file.
37
+ */
38
+ let pasteSequence = 0;
39
+ function synthesizePastedImageName(mimeType, now) {
40
+ const pad = (n) => String(n).padStart(2, "0");
41
+ const time = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
42
+ const ext = IMAGE_MIME_EXTENSIONS[mimeType.toLowerCase()] ?? "png";
43
+ pasteSequence += 1;
44
+ return `pasted-image-${time}-${pasteSequence}.${ext}`;
45
+ }
46
+ /**
47
+ * Extracts files from a paste event, giving clipboard images that carry the
48
+ * browser's generic `image.png` name a unique, human-readable one
49
+ * (`pasted-image-<HHMMSS>-<n>.<ext>`).
50
+ *
51
+ * Unique names are load-bearing, not cosmetic: attachments mount at
52
+ * `.stigmer/inputs/{filename}`, and two same-named attachments either fail
53
+ * the execution (deep-agent harness) or silently overwrite each other
54
+ * (Cursor harness). Files with real names (pasted from a file manager) are
55
+ * returned unchanged.
56
+ *
57
+ * Returns an empty array for a text-only paste — callers use that to let
58
+ * the default text insertion proceed untouched.
59
+ *
60
+ * Must be called synchronously from the paste event handler. Call
61
+ * `event.preventDefault()` in the same tick when the result is non-empty;
62
+ * the returned `File` objects remain valid afterwards.
63
+ *
64
+ * @example
65
+ * ```tsx
66
+ * function handlePaste(e: React.ClipboardEvent<HTMLTextAreaElement>) {
67
+ * const files = extractClipboardFiles(e);
68
+ * if (files.length === 0) return; // plain text paste — leave it alone
69
+ * e.preventDefault();
70
+ * attachments.addFiles(files);
71
+ * }
72
+ * ```
73
+ */
74
+ export function extractClipboardFiles(event) {
75
+ const fileList = event.clipboardData?.files;
76
+ if (!fileList || fileList.length === 0)
77
+ return [];
78
+ return Array.from(fileList).map((file) => {
79
+ const isGenericImage = file.type.toLowerCase().startsWith("image/") &&
80
+ (file.name === "" || GENERIC_CLIPBOARD_IMAGE_NAME.test(file.name));
81
+ if (!isGenericImage)
82
+ return file;
83
+ return new File([file], synthesizePastedImageName(file.type, new Date()), {
84
+ type: file.type,
85
+ lastModified: file.lastModified,
86
+ });
87
+ });
88
+ }
89
+ //# sourceMappingURL=clipboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clipboard.js","sourceRoot":"","sources":["../../src/attachment/clipboard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAUH;;;;;GAKG;AACH,MAAM,4BAA4B,GAAG,+CAA+C,CAAC;AAErF,0EAA0E;AAC1E,MAAM,qBAAqB,GAA2B;IACpD,WAAW,EAAE,KAAK;IAClB,YAAY,EAAE,KAAK;IACnB,WAAW,EAAE,KAAK;IAClB,YAAY,EAAE,MAAM;IACpB,YAAY,EAAE,MAAM;IACpB,WAAW,EAAE,KAAK;IAClB,YAAY,EAAE,MAAM;IACpB,eAAe,EAAE,KAAK;CACvB,CAAC;AAEF;;;;;;GAMG;AACH,IAAI,aAAa,GAAG,CAAC,CAAC;AAEtB,SAAS,yBAAyB,CAAC,QAAgB,EAAE,GAAS;IAC5D,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACtD,MAAM,IAAI,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,EAAE,CAAC;IACtF,MAAM,GAAG,GAAG,qBAAqB,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,IAAI,KAAK,CAAC;IACnE,aAAa,IAAI,CAAC,CAAC;IACnB,OAAO,gBAAgB,IAAI,IAAI,aAAa,IAAI,GAAG,EAAE,CAAC;AACxD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAA2B;IAC/D,MAAM,QAAQ,GAAG,KAAK,CAAC,aAAa,EAAE,KAAK,CAAC;IAC5C,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAElD,OAAO,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACvC,MAAM,cAAc,GAClB,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;YAC5C,CAAC,IAAI,CAAC,IAAI,KAAK,EAAE,IAAI,4BAA4B,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACrE,IAAI,CAAC,cAAc;YAAE,OAAO,IAAI,CAAC;QAEjC,OAAO,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,yBAAyB,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,EAAE;YACxE,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,YAAY,EAAE,IAAI,CAAC,YAAY;SAChC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -2,5 +2,10 @@ export { useAttachments } from "./useAttachments.js";
2
2
  export type { AttachmentPhase, AttachmentEntry, UseAttachmentsOptions, UseAttachmentsReturn, } from "./useAttachments.js";
3
3
  export { AttachmentChipList } from "./AttachmentChipList.js";
4
4
  export type { AttachmentChipListProps } from "./AttachmentChipList.js";
5
- export { MAX_ATTACHMENT_BYTES, detectContentType, formatFileSize, validateAttachmentSize, } from "./attachment-utils.js";
5
+ export { MAX_ATTACHMENT_BYTES, detectContentType, formatFileSize, uniquifyFilename, validateAttachmentSize, } from "./attachment-utils.js";
6
+ export { extractClipboardFiles } from "./clipboard.js";
7
+ export type { ClipboardFilesSource } from "./clipboard.js";
8
+ export { prepareImageForVision } from "./prepare-image.js";
9
+ export { MAX_VISION_LONG_EDGE_PX, MAX_VISION_PIXELS, exceedsVisionResolution, fitToVisionResolution, } from "./vision-fit.js";
10
+ export type { VisionFitSize } from "./vision-fit.js";
6
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/attachment/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,YAAY,EACV,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,YAAY,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAEvE,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,cAAc,EACd,sBAAsB,GACvB,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/attachment/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,YAAY,EACV,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,YAAY,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAEvE,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,YAAY,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC"}
@@ -1,4 +1,7 @@
1
1
  export { useAttachments } from "./useAttachments.js";
2
2
  export { AttachmentChipList } from "./AttachmentChipList.js";
3
- export { MAX_ATTACHMENT_BYTES, detectContentType, formatFileSize, validateAttachmentSize, } from "./attachment-utils.js";
3
+ export { MAX_ATTACHMENT_BYTES, detectContentType, formatFileSize, uniquifyFilename, validateAttachmentSize, } from "./attachment-utils.js";
4
+ export { extractClipboardFiles } from "./clipboard.js";
5
+ export { prepareImageForVision } from "./prepare-image.js";
6
+ export { MAX_VISION_LONG_EDGE_PX, MAX_VISION_PIXELS, exceedsVisionResolution, fitToVisionResolution, } from "./vision-fit.js";
4
7
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/attachment/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAQrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAG7D,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,cAAc,EACd,sBAAsB,GACvB,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/attachment/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAQrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAG7D,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAGvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Browser-side image preparation for agent vision — the canvas half of
3
+ * "paste a screenshot and the agent sees it" (stigmer/stigmer#284).
4
+ *
5
+ * A screenshot from a large display is routinely a 4-10 MB PNG. The runner
6
+ * delivers images to the model inline only under its per-image byte cap
7
+ * (3 MiB raw; see the runner's `shared/attachment-vision.ts`), so an
8
+ * unprepared paste degrades to "I can't see this file". Bounding the image
9
+ * to the resolution providers actually process (see vision-fit.ts) is
10
+ * quality-neutral and brings real screenshots far under that cap — while
11
+ * also making the upload roughly 20× faster.
12
+ *
13
+ * Applied to PASTED images only, by the composer's paste handler. Picked
14
+ * and dragged files are never re-encoded: a chosen file may be the subject
15
+ * of the task ("read the EXIF", "embed this logo"), and silently altering
16
+ * it would be a regression. A pasted image has no file identity to preserve.
17
+ *
18
+ * Failure policy: this module never throws and never returns a broken file.
19
+ * Every failure path — no canvas API (old browsers, privacy extensions,
20
+ * non-browser test environments), undecodable bytes, encoder failure —
21
+ * returns the ORIGINAL file, which still works end to end: it uploads,
22
+ * mounts in the workspace, and merely degrades at the runner with the
23
+ * standard "not viewable inline" disclosure.
24
+ */
25
+ /**
26
+ * Bounds a pasted image to the resolution vision providers actually
27
+ * process, re-encoding to a format both harnesses can display inline.
28
+ *
29
+ * - PNG and JPEG within the resolution limits pass through byte-identical
30
+ * (also guards edit-and-resubmit flows against generation loss).
31
+ * - GIF within the limits passes through untouched — re-encoding would
32
+ * silently flatten an animation the agent also receives as a file.
33
+ * - Oversized images resize to {@link fitToVisionResolution}; other formats
34
+ * (WebP, TIFF, BMP…) re-encode even when small, for harness visibility.
35
+ * - Output format: JPEG stays JPEG; everything else prefers PNG (crisp UI
36
+ * text), switching to JPEG only for photographic content — detected by
37
+ * PNG byte density, where PNG can exceed the runner's inline byte cap.
38
+ *
39
+ * Never throws; every failure path returns the original file (see module
40
+ * doc). Non-image files are returned unchanged.
41
+ */
42
+ export declare function prepareImageForVision(file: File): Promise<File>;
43
+ //# sourceMappingURL=prepare-image.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prepare-image.d.ts","sourceRoot":"","sources":["../../src/attachment/prepare-image.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAqDH;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAqFrE"}
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Browser-side image preparation for agent vision — the canvas half of
3
+ * "paste a screenshot and the agent sees it" (stigmer/stigmer#284).
4
+ *
5
+ * A screenshot from a large display is routinely a 4-10 MB PNG. The runner
6
+ * delivers images to the model inline only under its per-image byte cap
7
+ * (3 MiB raw; see the runner's `shared/attachment-vision.ts`), so an
8
+ * unprepared paste degrades to "I can't see this file". Bounding the image
9
+ * to the resolution providers actually process (see vision-fit.ts) is
10
+ * quality-neutral and brings real screenshots far under that cap — while
11
+ * also making the upload roughly 20× faster.
12
+ *
13
+ * Applied to PASTED images only, by the composer's paste handler. Picked
14
+ * and dragged files are never re-encoded: a chosen file may be the subject
15
+ * of the task ("read the EXIF", "embed this logo"), and silently altering
16
+ * it would be a regression. A pasted image has no file identity to preserve.
17
+ *
18
+ * Failure policy: this module never throws and never returns a broken file.
19
+ * Every failure path — no canvas API (old browsers, privacy extensions,
20
+ * non-browser test environments), undecodable bytes, encoder failure —
21
+ * returns the ORIGINAL file, which still works end to end: it uploads,
22
+ * mounts in the workspace, and merely degrades at the runner with the
23
+ * standard "not viewable inline" disclosure.
24
+ */
25
+ import { exceedsVisionResolution, fitToVisionResolution, } from "./vision-fit.js";
26
+ /**
27
+ * Formats the harnesses can display inline everywhere: the Cursor harness
28
+ * re-sniffs magic bytes and recognizes ONLY PNG and JPEG (the deep-agent
29
+ * harness also takes WebP/GIF). Anything else that must ride as pixels is
30
+ * re-encoded to PNG so a Safari TIFF or Chrome WebP paste is not invisible
31
+ * on one harness.
32
+ */
33
+ const UNIVERSAL_VISION_TYPES = new Set(["image/png", "image/jpeg"]);
34
+ /**
35
+ * PNG density (encoded bytes per pixel) above which content is treated as
36
+ * photographic and re-encoded as JPEG instead.
37
+ *
38
+ * PNG keeps UI text crisp and compresses flat screenshot content to
39
+ * 0.1-0.5 B/px, but photographic content in PNG runs 1.5-3 B/px and can
40
+ * exceed the runner's 3 MiB inline cap even at the fitted resolution
41
+ * (measured: ~3.4 MB for worst-case noise at 1.15 MP). Density is the
42
+ * content signal — scale-invariant, so small UI images never flip to JPEG
43
+ * just because their absolute sizes are tiny (a pure size ratio fails
44
+ * exactly that way) — and it bounds every kept PNG by construction:
45
+ * 1.15 MP × 1.0 B/px ≈ 1.1 MB, far inside the runner's cap, without this
46
+ * module ever referencing the runner's byte constant.
47
+ */
48
+ const PHOTOGRAPHIC_PNG_BYTES_PER_PIXEL = 1.0;
49
+ const JPEG_QUALITY = 0.9;
50
+ function replaceExtension(name, ext) {
51
+ const dotIndex = name.lastIndexOf(".");
52
+ const stem = dotIndex > 0 ? name.slice(0, dotIndex) : name;
53
+ return `${stem}.${ext}`;
54
+ }
55
+ function encodeCanvas(canvas, type, quality) {
56
+ return new Promise((resolve) => {
57
+ try {
58
+ canvas.toBlob(resolve, type, quality);
59
+ }
60
+ catch {
61
+ resolve(null);
62
+ }
63
+ });
64
+ }
65
+ /**
66
+ * Bounds a pasted image to the resolution vision providers actually
67
+ * process, re-encoding to a format both harnesses can display inline.
68
+ *
69
+ * - PNG and JPEG within the resolution limits pass through byte-identical
70
+ * (also guards edit-and-resubmit flows against generation loss).
71
+ * - GIF within the limits passes through untouched — re-encoding would
72
+ * silently flatten an animation the agent also receives as a file.
73
+ * - Oversized images resize to {@link fitToVisionResolution}; other formats
74
+ * (WebP, TIFF, BMP…) re-encode even when small, for harness visibility.
75
+ * - Output format: JPEG stays JPEG; everything else prefers PNG (crisp UI
76
+ * text), switching to JPEG only for photographic content — detected by
77
+ * PNG byte density, where PNG can exceed the runner's inline byte cap.
78
+ *
79
+ * Never throws; every failure path returns the original file (see module
80
+ * doc). Non-image files are returned unchanged.
81
+ */
82
+ export async function prepareImageForVision(file) {
83
+ const sourceType = file.type.toLowerCase();
84
+ if (!sourceType.startsWith("image/"))
85
+ return file;
86
+ if (typeof createImageBitmap !== "function" || typeof document === "undefined") {
87
+ return file;
88
+ }
89
+ let bitmap;
90
+ try {
91
+ // "from-image" applies EXIF orientation, so a pasted phone photo lands
92
+ // upright instead of sideways (most browsers default to this, older
93
+ // Safari does not).
94
+ bitmap = await createImageBitmap(file, { imageOrientation: "from-image" });
95
+ }
96
+ catch {
97
+ return file;
98
+ }
99
+ try {
100
+ const needsResize = exceedsVisionResolution(bitmap.width, bitmap.height);
101
+ const isGif = sourceType === "image/gif";
102
+ // Within limits and already displayable everywhere (or an animated GIF,
103
+ // which only the resize case may flatten): leave every byte alone.
104
+ if (!needsResize && (UNIVERSAL_VISION_TYPES.has(sourceType) || isGif)) {
105
+ return file;
106
+ }
107
+ const target = needsResize
108
+ ? fitToVisionResolution(bitmap.width, bitmap.height)
109
+ : { width: bitmap.width, height: bitmap.height };
110
+ const canvas = document.createElement("canvas");
111
+ canvas.width = target.width;
112
+ canvas.height = target.height;
113
+ const ctx = canvas.getContext("2d");
114
+ if (!ctx)
115
+ return file;
116
+ ctx.drawImage(bitmap, 0, 0, target.width, target.height);
117
+ let blob;
118
+ let outType;
119
+ if (sourceType === "image/jpeg") {
120
+ outType = "image/jpeg";
121
+ blob = await encodeCanvas(canvas, outType, JPEG_QUALITY);
122
+ }
123
+ else {
124
+ const png = await encodeCanvas(canvas, "image/png");
125
+ const pngDensity = png ? png.size / (target.width * target.height) : Infinity;
126
+ if (png && pngDensity <= PHOTOGRAPHIC_PNG_BYTES_PER_PIXEL) {
127
+ outType = "image/png";
128
+ blob = png;
129
+ }
130
+ else {
131
+ // Photographic content (or a failed PNG encode): take JPEG. JPEG
132
+ // cannot represent alpha, so composite onto white first —
133
+ // destination-over paints beneath the already-drawn image, and the
134
+ // alpha-preserving PNG encode above is already done.
135
+ ctx.globalCompositeOperation = "destination-over";
136
+ ctx.fillStyle = "#ffffff";
137
+ ctx.fillRect(0, 0, target.width, target.height);
138
+ const jpeg = await encodeCanvas(canvas, "image/jpeg", JPEG_QUALITY);
139
+ blob = jpeg ?? png;
140
+ outType = jpeg ? "image/jpeg" : "image/png";
141
+ }
142
+ }
143
+ if (!blob)
144
+ return file;
145
+ // A pure resize that somehow grew the file is a regression, not a win:
146
+ // the runner caps bytes, not pixels, so the smaller original is the
147
+ // better payload. (Cross-format re-encodes are exempt — growing a WebP
148
+ // into a PNG is the price of being visible on the Cursor harness.)
149
+ if (outType === sourceType && blob.size >= file.size)
150
+ return file;
151
+ const extension = outType === "image/jpeg" ? "jpg" : "png";
152
+ const name = outType === sourceType ? file.name : replaceExtension(file.name, extension);
153
+ return new File([blob], name, {
154
+ type: outType,
155
+ lastModified: file.lastModified,
156
+ });
157
+ }
158
+ finally {
159
+ bitmap.close();
160
+ }
161
+ }
162
+ //# sourceMappingURL=prepare-image.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prepare-image.js","sourceRoot":"","sources":["../../src/attachment/prepare-image.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"useAttachments.d.ts","sourceRoot":"","sources":["../../src/attachment/useAttachments.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAapD;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,OAAO,GAAG,OAAO,CAAC;AAE9D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,eAAe;IAC9B,yDAAyD;IACzD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,sCAAsC;IACtC,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC,kDAAkD;IAClD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,mEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,0CAA0C;AAC1C,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACxD;AAED,8CAA8C;AAC9C,MAAM,WAAW,oBAAoB;IACnC,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C,kEAAkE;IAClE,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,uDAAuD;IACvD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,4DAA4D;IAC5D,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,EAAE,KAAK,IAAI,CAAC;IACtD,kEAAkE;IAClE,QAAQ,CAAC,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,6BAA6B;IAC7B,QAAQ,CAAC,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,yDAAyD;IACzD,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,kBAAkB,EAAE,MAAM,eAAe,EAAE,CAAC;CACtD;AAWD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,wBAAgB,cAAc,CAC5B,OAAO,CAAC,EAAE,qBAAqB,GAC9B,oBAAoB,CAqJtB"}
1
+ {"version":3,"file":"useAttachments.d.ts","sourceRoot":"","sources":["../../src/attachment/useAttachments.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAcpD;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,OAAO,GAAG,OAAO,CAAC;AAE9D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,eAAe;IAC9B,yDAAyD;IACzD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,sCAAsC;IACtC,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC,kDAAkD;IAClD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,mEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,0CAA0C;AAC1C,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACxD;AAED,8CAA8C;AAC9C,MAAM,WAAW,oBAAoB;IACnC,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C,kEAAkE;IAClE,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,uDAAuD;IACvD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,4DAA4D;IAC5D,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,EAAE,KAAK,IAAI,CAAC;IACtD,kEAAkE;IAClE,QAAQ,CAAC,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,6BAA6B;IAC7B,QAAQ,CAAC,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,yDAAyD;IACzD,QAAQ,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,kBAAkB,EAAE,MAAM,eAAe,EAAE,CAAC;CACtD;AAWD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,wBAAgB,cAAc,CAC5B,OAAO,CAAC,EAAE,qBAAqB,GAC9B,oBAAoB,CA4KtB"}
@@ -4,7 +4,7 @@ import { create } from "@bufbuild/protobuf";
4
4
  import { UploadAttachmentRequestSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/io_pb";
5
5
  import { useStigmer } from "../hooks.js";
6
6
  import { toError } from "../internal/toError.js";
7
- import { detectContentType, validateAttachmentSize, } from "./attachment-utils.js";
7
+ import { detectContentType, uniquifyFilename, validateAttachmentSize, } from "./attachment-utils.js";
8
8
  // ---------------------------------------------------------------------------
9
9
  // Hook
10
10
  // ---------------------------------------------------------------------------
@@ -65,6 +65,13 @@ export function useAttachments(options) {
65
65
  const stigmer = useStigmer();
66
66
  const [entries, setEntries] = useState([]);
67
67
  const abortControllers = useRef(new Map());
68
+ // Latest-entries mirror so `addFiles` can uniquify filenames against
69
+ // current entries without depending on `entries` (which would give the
70
+ // callback a new identity every upload-phase change). Re-assigned each
71
+ // render to reconcile removals, and synchronously inside `addFiles` so
72
+ // two adds in one tick still see each other's names.
73
+ const entriesRef = useRef(entries);
74
+ entriesRef.current = entries;
68
75
  const uploadFile = useCallback(async (id, file, contentType) => {
69
76
  const controller = new AbortController();
70
77
  abortControllers.current.set(id, controller);
@@ -98,12 +105,24 @@ export function useAttachments(options) {
98
105
  const addFiles = useCallback((files) => {
99
106
  const fileArray = Array.from(files);
100
107
  const validEntries = [];
101
- for (const file of fileArray) {
102
- const sizeError = validateAttachmentSize(file);
108
+ const takenNames = new Set(entriesRef.current.map((e) => e.file.name));
109
+ for (const rawFile of fileArray) {
110
+ const sizeError = validateAttachmentSize(rawFile);
103
111
  if (sizeError) {
104
112
  options?.onValidationError?.(sizeError);
105
113
  continue;
106
114
  }
115
+ // Duplicate names within a turn break the execution downstream
116
+ // (see uniquifyFilename) — rename before the bytes ever upload,
117
+ // so the chip, the upload, and the mounted file all agree.
118
+ const uniqueName = uniquifyFilename(rawFile.name, takenNames);
119
+ takenNames.add(uniqueName);
120
+ const file = uniqueName === rawFile.name
121
+ ? rawFile
122
+ : new File([rawFile], uniqueName, {
123
+ type: rawFile.type,
124
+ lastModified: rawFile.lastModified,
125
+ });
107
126
  const id = generateId();
108
127
  const contentType = detectContentType(file);
109
128
  validEntries.push({
@@ -117,6 +136,7 @@ export function useAttachments(options) {
117
136
  uploadFile(id, file, contentType);
118
137
  }
119
138
  if (validEntries.length > 0) {
139
+ entriesRef.current = [...entriesRef.current, ...validEntries];
120
140
  setEntries((prev) => [...prev, ...validEntries]);
121
141
  }
122
142
  }, [options, uploadFile]);