@stigmer/react 3.7.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.
- package/attachment/AttachmentChipList.d.ts.map +1 -1
- package/attachment/AttachmentChipList.js +30 -1
- package/attachment/AttachmentChipList.js.map +1 -1
- package/attachment/attachment-utils.d.ts +11 -0
- package/attachment/attachment-utils.d.ts.map +1 -1
- package/attachment/attachment-utils.js +23 -0
- package/attachment/attachment-utils.js.map +1 -1
- package/attachment/clipboard.d.ts +51 -0
- package/attachment/clipboard.d.ts.map +1 -0
- package/attachment/clipboard.js +89 -0
- package/attachment/clipboard.js.map +1 -0
- package/attachment/index.d.ts +6 -1
- package/attachment/index.d.ts.map +1 -1
- package/attachment/index.js +4 -1
- package/attachment/index.js.map +1 -1
- package/attachment/prepare-image.d.ts +43 -0
- package/attachment/prepare-image.d.ts.map +1 -0
- package/attachment/prepare-image.js +162 -0
- package/attachment/prepare-image.js.map +1 -0
- package/attachment/useAttachments.d.ts.map +1 -1
- package/attachment/useAttachments.js +23 -3
- package/attachment/useAttachments.js.map +1 -1
- package/attachment/vision-fit.d.ts +50 -0
- package/attachment/vision-fit.d.ts.map +1 -0
- package/attachment/vision-fit.js +76 -0
- package/attachment/vision-fit.js.map +1 -0
- package/channel/AgentChannelsPanel.d.ts.map +1 -1
- package/channel/AgentChannelsPanel.js +21 -2
- package/channel/AgentChannelsPanel.js.map +1 -1
- package/channel/ChannelConversationsDialog.d.ts +12 -4
- package/channel/ChannelConversationsDialog.d.ts.map +1 -1
- package/channel/ChannelConversationsDialog.js +18 -24
- package/channel/ChannelConversationsDialog.js.map +1 -1
- package/channel/providers.d.ts +23 -0
- package/channel/providers.d.ts.map +1 -1
- package/channel/providers.js +4 -0
- package/channel/providers.js.map +1 -1
- package/channel/useSaveAgentChannel.d.ts +5 -4
- package/channel/useSaveAgentChannel.d.ts.map +1 -1
- package/channel/useSaveAgentChannel.js +18 -4
- package/channel/useSaveAgentChannel.js.map +1 -1
- package/composer/ComposerToolbar.d.ts +1 -1
- package/composer/ComposerToolbar.d.ts.map +1 -1
- package/composer/SessionComposer.d.ts +4 -2
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +51 -5
- package/composer/SessionComposer.js.map +1 -1
- package/conversation/ConversationAttentionBanner.d.ts +22 -0
- package/conversation/ConversationAttentionBanner.d.ts.map +1 -0
- package/conversation/ConversationAttentionBanner.js +25 -0
- package/conversation/ConversationAttentionBanner.js.map +1 -0
- package/conversation/ConversationComposer.d.ts +41 -0
- package/conversation/ConversationComposer.d.ts.map +1 -0
- package/conversation/ConversationComposer.js +57 -0
- package/conversation/ConversationComposer.js.map +1 -0
- package/conversation/ConversationControlBanner.d.ts +52 -0
- package/conversation/ConversationControlBanner.d.ts.map +1 -0
- package/conversation/ConversationControlBanner.js +59 -0
- package/conversation/ConversationControlBanner.js.map +1 -0
- package/conversation/ConversationListPane.d.ts +68 -0
- package/conversation/ConversationListPane.d.ts.map +1 -0
- package/conversation/ConversationListPane.js +120 -0
- package/conversation/ConversationListPane.js.map +1 -0
- package/conversation/ConversationTimelineView.d.ts +46 -0
- package/conversation/ConversationTimelineView.d.ts.map +1 -0
- package/conversation/ConversationTimelineView.js +166 -0
- package/conversation/ConversationTimelineView.js.map +1 -0
- package/conversation/ConversationsWorkbench.d.ts +63 -0
- package/conversation/ConversationsWorkbench.d.ts.map +1 -0
- package/conversation/ConversationsWorkbench.js +165 -0
- package/conversation/ConversationsWorkbench.js.map +1 -0
- package/conversation/conversationPresentation.d.ts +137 -0
- package/conversation/conversationPresentation.d.ts.map +1 -0
- package/conversation/conversationPresentation.js +230 -0
- package/conversation/conversationPresentation.js.map +1 -0
- package/conversation/index.d.ts +26 -0
- package/conversation/index.d.ts.map +1 -0
- package/conversation/index.js +14 -0
- package/conversation/index.js.map +1 -0
- package/conversation/polling.d.ts +39 -0
- package/conversation/polling.d.ts.map +1 -0
- package/conversation/polling.js +39 -0
- package/conversation/polling.js.map +1 -0
- package/conversation/useConversation.d.ts +61 -0
- package/conversation/useConversation.d.ts.map +1 -0
- package/conversation/useConversation.js +153 -0
- package/conversation/useConversation.js.map +1 -0
- package/conversation/useConversationList.d.ts +98 -0
- package/conversation/useConversationList.d.ts.map +1 -0
- package/conversation/useConversationList.js +200 -0
- package/conversation/useConversationList.js.map +1 -0
- package/conversation/useConversationParticipation.d.ts +63 -0
- package/conversation/useConversationParticipation.d.ts.map +1 -0
- package/conversation/useConversationParticipation.js +84 -0
- package/conversation/useConversationParticipation.js.map +1 -0
- package/conversation/useConversationTimeline.d.ts +71 -0
- package/conversation/useConversationTimeline.d.ts.map +1 -0
- package/conversation/useConversationTimeline.js +152 -0
- package/conversation/useConversationTimeline.js.map +1 -0
- package/conversation/useConversationsWantsHumanCount.d.ts +42 -0
- package/conversation/useConversationsWantsHumanCount.d.ts.map +1 -0
- package/conversation/useConversationsWantsHumanCount.js +40 -0
- package/conversation/useConversationsWantsHumanCount.js.map +1 -0
- package/index.d.ts +6 -2
- package/index.d.ts.map +1 -1
- package/index.js +11 -2
- package/index.js.map +1 -1
- package/internal/tooltip.d.ts.map +1 -1
- package/internal/tooltip.js +7 -1
- package/internal/tooltip.js.map +1 -1
- package/internal/useAutoScroll.d.ts +25 -4
- package/internal/useAutoScroll.d.ts.map +1 -1
- package/internal/useAutoScroll.js +55 -15
- package/internal/useAutoScroll.js.map +1 -1
- package/internal/useFetch.d.ts.map +1 -1
- package/internal/useFetch.js +14 -4
- package/internal/useFetch.js.map +1 -1
- package/models/ModelSelector.d.ts +24 -5
- package/models/ModelSelector.d.ts.map +1 -1
- package/models/ModelSelector.js +31 -9
- package/models/ModelSelector.js.map +1 -1
- package/models/registry.d.ts +3 -2
- package/models/registry.d.ts.map +1 -1
- package/models/registry.js.map +1 -1
- package/models/service-tier.d.ts +1 -1
- package/models/service-tier.d.ts.map +1 -1
- package/models/service-tier.js +1 -1
- package/models/service-tier.js.map +1 -1
- package/package.json +4 -4
- package/schedule/ScheduleDetailView.js +1 -1
- package/schedule/ScheduleDetailView.js.map +1 -1
- package/schedule/ScheduleForm.d.ts.map +1 -1
- package/schedule/ScheduleForm.js +7 -6
- package/schedule/ScheduleForm.js.map +1 -1
- package/sharing/useSaveAgentShare.d.ts +10 -1
- package/sharing/useSaveAgentShare.d.ts.map +1 -1
- package/sharing/useSaveAgentShare.js +9 -0
- package/sharing/useSaveAgentShare.js.map +1 -1
- package/sidebar/WorkspaceSidebar.d.ts +12 -4
- package/sidebar/WorkspaceSidebar.d.ts.map +1 -1
- package/sidebar/WorkspaceSidebar.js +19 -7
- package/sidebar/WorkspaceSidebar.js.map +1 -1
- package/src/attachment/AttachmentChipList.tsx +45 -1
- package/src/attachment/__tests__/AttachmentChipList.test.tsx +103 -0
- package/src/attachment/__tests__/attachment-utils.test.ts +54 -0
- package/src/attachment/__tests__/clipboard.test.ts +110 -0
- package/src/attachment/__tests__/prepare-image.browser.test.ts +166 -0
- package/src/attachment/__tests__/prepare-image.test.ts +32 -0
- package/src/attachment/__tests__/vision-fit.test.ts +93 -0
- package/src/attachment/attachment-utils.ts +27 -0
- package/src/attachment/clipboard.ts +102 -0
- package/src/attachment/index.ts +13 -0
- package/src/attachment/prepare-image.ts +179 -0
- package/src/attachment/useAttachments.ts +26 -2
- package/src/attachment/vision-fit.ts +90 -0
- package/src/channel/AgentChannelsPanel.tsx +33 -2
- package/src/channel/ChannelConversationsDialog.tsx +22 -25
- package/src/channel/__tests__/AgentChannelsPanel.test.tsx +36 -5
- package/src/channel/__tests__/ChannelConversationsDialog.test.tsx +38 -1
- package/src/channel/__tests__/useSaveAgentChannel.test.tsx +26 -0
- package/src/channel/providers.ts +27 -0
- package/src/channel/useSaveAgentChannel.ts +18 -4
- package/src/composer/ComposerToolbar.tsx +1 -1
- package/src/composer/SessionComposer.tsx +75 -6
- package/src/composer/__tests__/SessionComposer-paste.test.tsx +263 -0
- package/src/composer/__tests__/SessionComposer-serviceTier.test.tsx +4 -3
- package/src/composer/__tests__/SessionComposer-uploadGate.test.tsx +239 -0
- package/src/conversation/ConversationAttentionBanner.tsx +77 -0
- package/src/conversation/ConversationComposer.tsx +156 -0
- package/src/conversation/ConversationControlBanner.tsx +193 -0
- package/src/conversation/ConversationListPane.tsx +459 -0
- package/src/conversation/ConversationTimelineView.tsx +435 -0
- package/src/conversation/ConversationsWorkbench.tsx +378 -0
- package/src/conversation/__tests__/ConversationAttentionBanner.test.tsx +82 -0
- package/src/conversation/__tests__/ConversationComposer.test.tsx +160 -0
- package/src/conversation/__tests__/ConversationControlBanner.test.tsx +190 -0
- package/src/conversation/__tests__/ConversationListPane.test.tsx +372 -0
- package/src/conversation/__tests__/ConversationTimelineView.layout.test.tsx +160 -0
- package/src/conversation/__tests__/ConversationTimelineView.test.tsx +405 -0
- package/src/conversation/__tests__/ConversationsWorkbench.test.tsx +860 -0
- package/src/conversation/__tests__/a11y/conversation-surfaces.a11y.test.tsx +235 -0
- package/src/conversation/__tests__/a11y/harness.tsx +100 -0
- package/src/conversation/__tests__/conversation-tooltips.layout.test.tsx +167 -0
- package/src/conversation/__tests__/conversationPollingComposition.test.tsx +138 -0
- package/src/conversation/__tests__/conversationPresentation.test.ts +289 -0
- package/src/conversation/__tests__/useConversation.test.tsx +243 -0
- package/src/conversation/__tests__/useConversationList.test.tsx +492 -0
- package/src/conversation/__tests__/useConversationParticipation.test.tsx +190 -0
- package/src/conversation/__tests__/useConversationTimeline.test.tsx +258 -0
- package/src/conversation/__tests__/useConversationsWantsHumanCount.test.tsx +102 -0
- package/src/conversation/conversationPresentation.ts +305 -0
- package/src/conversation/index.ts +80 -0
- package/src/conversation/polling.ts +41 -0
- package/src/conversation/useConversation.ts +221 -0
- package/src/conversation/useConversationList.ts +328 -0
- package/src/conversation/useConversationParticipation.ts +178 -0
- package/src/conversation/useConversationTimeline.ts +244 -0
- package/src/conversation/useConversationsWantsHumanCount.ts +86 -0
- package/src/index.ts +78 -1
- package/src/internal/__tests__/fake-timer-slices.ts +29 -0
- package/src/internal/__tests__/useAutoScroll.layout.test.tsx +143 -0
- package/src/internal/__tests__/useAutoScroll.test.tsx +62 -5
- package/src/internal/__tests__/useFetch-poll-under-render-pressure.test.tsx +79 -0
- package/src/internal/tooltip.tsx +6 -1
- package/src/internal/useAutoScroll.ts +66 -20
- package/src/internal/useFetch.ts +15 -4
- package/src/models/ModelSelector.tsx +81 -49
- package/src/models/__tests__/ModelSelector-serviceTier.test.tsx +105 -21
- package/src/models/registry.ts +3 -2
- package/src/models/service-tier.ts +1 -1
- package/src/schedule/ScheduleDetailView.tsx +1 -1
- package/src/schedule/ScheduleForm.tsx +7 -6
- package/src/schedule/__tests__/scheduleCreation.test.tsx +2 -2
- package/src/sharing/__tests__/useSaveAgentShare.test.tsx +55 -1
- package/src/sharing/__tests__/useShareToolReadiness.test.tsx +1 -0
- package/src/sharing/useSaveAgentShare.ts +19 -1
- package/src/sidebar/WorkspaceSidebar.tsx +62 -3
- package/src/sidebar/__tests__/WorkspaceSidebar.test.tsx +56 -1
- package/styles.css +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AttachmentChipList.d.ts","sourceRoot":"","sources":["../../src/attachment/AttachmentChipList.tsx"],"names":[],"mappings":"
|
|
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 &&
|
|
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":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;AAEpC,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAgBvD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,kBAAkB,CAAC,EACjC,OAAO,EACP,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,SAAS,GACe;IACxB,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEtC,OAAO,CACL,cACE,SAAS,EAAE,EAAE,CAAC,wBAAwB,EAAE,SAAS,CAAC,EAClD,IAAI,EAAC,MAAM,gBACA,gBAAgB,YAE1B,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CACtB,KAAC,cAAc,IAEb,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,EAClC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,EAChC,QAAQ,EAAE,QAAQ,IAJb,KAAK,CAAC,EAAE,CAKb,CACH,CAAC,GACE,CACP,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,kBAAkB;AAClB,8EAA8E;AAE9E,SAAS,cAAc,CAAC,EACtB,KAAK,EACL,QAAQ,EACR,OAAO,EACP,QAAQ,GAMT;IACC,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,KAAK,OAAO,CAAC;IACxC,MAAM,WAAW,GAAG,KAAK,CAAC,KAAK,KAAK,WAAW,CAAC;IAEhD,OAAO,CACL,gBACE,IAAI,EAAC,UAAU,gBACH,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,EAAE,EAC1I,SAAS,EAAE,EAAE,CACX,6EAA6E,EAC7E,OAAO;YACL,CAAC,CAAC,qEAAqE;YACvE,CAAC,CAAC,iCAAiC,EACrC,WAAW,IAAI,YAAY,CAC5B,aAEA,WAAW,IAAI,KAAC,WAAW,KAAG,EAC9B,OAAO,IAAI,KAAC,QAAQ,KAAG,EACvB,CAAC,WAAW,IAAI,CAAC,OAAO,IAAI,KAAC,QAAQ,KAAG,
|
|
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"}
|
package/attachment/index.d.ts
CHANGED
|
@@ -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"}
|
package/attachment/index.js
CHANGED
|
@@ -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
|
package/attachment/index.js.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
102
|
-
|
|
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]);
|