@stigmer/react 3.9.0 → 3.10.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 (116) hide show
  1. package/attachment/AttachmentChipList.d.ts +16 -4
  2. package/attachment/AttachmentChipList.d.ts.map +1 -1
  3. package/attachment/AttachmentChipList.js +101 -35
  4. package/attachment/AttachmentChipList.js.map +1 -1
  5. package/attachment/AttachmentImageLightbox.d.ts +39 -0
  6. package/attachment/AttachmentImageLightbox.d.ts.map +1 -0
  7. package/attachment/AttachmentImageLightbox.js +57 -0
  8. package/attachment/AttachmentImageLightbox.js.map +1 -0
  9. package/attachment/useObjectUrl.d.ts +15 -0
  10. package/attachment/useObjectUrl.d.ts.map +1 -0
  11. package/attachment/useObjectUrl.js +25 -0
  12. package/attachment/useObjectUrl.js.map +1 -0
  13. package/conversation/ConversationMediaAttachment.d.ts +45 -0
  14. package/conversation/ConversationMediaAttachment.d.ts.map +1 -0
  15. package/conversation/ConversationMediaAttachment.js +153 -0
  16. package/conversation/ConversationMediaAttachment.js.map +1 -0
  17. package/conversation/ConversationTimelineView.d.ts +18 -1
  18. package/conversation/ConversationTimelineView.d.ts.map +1 -1
  19. package/conversation/ConversationTimelineView.js +14 -5
  20. package/conversation/ConversationTimelineView.js.map +1 -1
  21. package/conversation/ConversationsWorkbench.d.ts.map +1 -1
  22. package/conversation/ConversationsWorkbench.js +1 -1
  23. package/conversation/ConversationsWorkbench.js.map +1 -1
  24. package/conversation/conversationPresentation.d.ts +10 -3
  25. package/conversation/conversationPresentation.d.ts.map +1 -1
  26. package/conversation/conversationPresentation.js +10 -3
  27. package/conversation/conversationPresentation.js.map +1 -1
  28. package/conversation/index.d.ts +2 -0
  29. package/conversation/index.d.ts.map +1 -1
  30. package/conversation/index.js +1 -0
  31. package/conversation/index.js.map +1 -1
  32. package/conversation/useConversationMediaUrl.d.ts +65 -0
  33. package/conversation/useConversationMediaUrl.d.ts.map +1 -0
  34. package/conversation/useConversationMediaUrl.js +61 -0
  35. package/conversation/useConversationMediaUrl.js.map +1 -0
  36. package/execution/MessageAttachments.d.ts +52 -0
  37. package/execution/MessageAttachments.d.ts.map +1 -0
  38. package/execution/MessageAttachments.js +142 -0
  39. package/execution/MessageAttachments.js.map +1 -0
  40. package/execution/MessageEntry.d.ts +18 -0
  41. package/execution/MessageEntry.d.ts.map +1 -1
  42. package/execution/MessageEntry.js +5 -4
  43. package/execution/MessageEntry.js.map +1 -1
  44. package/execution/MessageThread.d.ts +15 -2
  45. package/execution/MessageThread.d.ts.map +1 -1
  46. package/execution/MessageThread.js +24 -7
  47. package/execution/MessageThread.js.map +1 -1
  48. package/execution/index.d.ts +2 -0
  49. package/execution/index.d.ts.map +1 -1
  50. package/execution/index.js +1 -0
  51. package/execution/index.js.map +1 -1
  52. package/github/decodeGitHubContent.d.ts +10 -5
  53. package/github/decodeGitHubContent.d.ts.map +1 -1
  54. package/github/decodeGitHubContent.js +21 -7
  55. package/github/decodeGitHubContent.js.map +1 -1
  56. package/github/useGitHubFileReader.d.ts.map +1 -1
  57. package/github/useGitHubFileReader.js +5 -5
  58. package/github/useGitHubFileReader.js.map +1 -1
  59. package/index.d.ts +3 -3
  60. package/index.d.ts.map +1 -1
  61. package/index.js +2 -2
  62. package/index.js.map +1 -1
  63. package/internal/form-primitives.d.ts +13 -0
  64. package/internal/form-primitives.d.ts.map +1 -1
  65. package/internal/form-primitives.js +13 -0
  66. package/internal/form-primitives.js.map +1 -1
  67. package/package.json +4 -4
  68. package/session/SessionViewer.js +1 -1
  69. package/session/SessionViewer.js.map +1 -1
  70. package/session/useSessionConversation.d.ts +8 -0
  71. package/session/useSessionConversation.d.ts.map +1 -1
  72. package/session/useSessionConversation.js +12 -2
  73. package/session/useSessionConversation.js.map +1 -1
  74. package/src/attachment/AttachmentChipList.tsx +318 -72
  75. package/src/attachment/AttachmentImageLightbox.tsx +156 -0
  76. package/src/attachment/__tests__/AttachmentChipList.test.tsx +240 -17
  77. package/src/attachment/useObjectUrl.ts +28 -0
  78. package/src/conversation/ConversationMediaAttachment.tsx +308 -0
  79. package/src/conversation/ConversationTimelineView.tsx +60 -10
  80. package/src/conversation/ConversationsWorkbench.tsx +2 -0
  81. package/src/conversation/__tests__/ConversationTimelineView.test.tsx +233 -6
  82. package/src/conversation/__tests__/a11y/conversation-surfaces.a11y.test.tsx +67 -1
  83. package/src/conversation/__tests__/useConversationMediaUrl.test.tsx +66 -0
  84. package/src/conversation/conversationPresentation.ts +10 -3
  85. package/src/conversation/index.ts +6 -0
  86. package/src/conversation/useConversationMediaUrl.ts +121 -0
  87. package/src/execution/MessageAttachments.tsx +364 -0
  88. package/src/execution/MessageEntry.tsx +38 -0
  89. package/src/execution/MessageThread.tsx +44 -4
  90. package/src/execution/__tests__/MessageEntry-attachments.test.tsx +228 -0
  91. package/src/execution/__tests__/buildThreadItems-attachments.test.ts +174 -0
  92. package/src/execution/index.ts +6 -0
  93. package/src/github/__tests__/decodeGitHubContent.test.ts +42 -1
  94. package/src/github/decodeGitHubContent.ts +22 -5
  95. package/src/github/useGitHubFileReader.ts +13 -3
  96. package/src/index.ts +5 -0
  97. package/src/internal/form-primitives.tsx +14 -0
  98. package/src/session/SessionViewer.tsx +1 -0
  99. package/src/session/__tests__/useSessionConversation.test.tsx +62 -0
  100. package/src/session/useSessionConversation.ts +21 -2
  101. package/src/workspace/FileViewer.tsx +109 -0
  102. package/src/workspace/WorkspaceFileReader.ts +55 -1
  103. package/src/workspace/__tests__/FileViewer.test.tsx +98 -0
  104. package/src/workspace/index.ts +2 -0
  105. package/styles.css +1 -1
  106. package/workspace/FileViewer.d.ts.map +1 -1
  107. package/workspace/FileViewer.js +40 -2
  108. package/workspace/FileViewer.js.map +1 -1
  109. package/workspace/WorkspaceFileReader.d.ts +29 -1
  110. package/workspace/WorkspaceFileReader.d.ts.map +1 -1
  111. package/workspace/WorkspaceFileReader.js +42 -0
  112. package/workspace/WorkspaceFileReader.js.map +1 -1
  113. package/workspace/index.d.ts +1 -1
  114. package/workspace/index.d.ts.map +1 -1
  115. package/workspace/index.js +1 -1
  116. package/workspace/index.js.map +1 -1
@@ -7,14 +7,26 @@ export interface AttachmentChipListProps {
7
7
  readonly onRemove: (id: string) => void;
8
8
  /** Retry callback for failed uploads (by entry ID). */
9
9
  readonly onRetry: (id: string) => void;
10
- /** Disables remove and retry actions. */
10
+ /**
11
+ * Called when the user clicks an image chip's preview target. When
12
+ * provided, it **replaces** the built-in lightbox — platform builders
13
+ * route the click to their own viewer. Omit for the default behavior:
14
+ * a full-size image lightbox over the locally-held file bytes.
15
+ */
16
+ readonly onPreview?: (entry: AttachmentEntry) => void;
17
+ /** Disables remove and retry actions. Preview stays active — it is read-only. */
11
18
  readonly disabled?: boolean;
12
19
  /** Additional CSS class names for the root container. */
13
20
  readonly className?: string;
14
21
  }
15
22
  /**
16
- * Renders a horizontal list of compact attachment chips showing
17
- * filename, size, upload status, and remove/retry actions.
23
+ * Renders a horizontal list of attachment chips: image attachments as
24
+ * preview-only thumbnail tiles (the image IS the chip — no filename
25
+ * text; visible from the moment of paste, through upload, and on error
26
+ * — stigmer/stigmer#371, #372), other files as compact filename chips.
27
+ * Filename, size, and upload status stay available on every chip via
28
+ * the tooltip and the accessible label; clicking an image tile opens
29
+ * the full image.
18
30
  *
19
31
  * Designed to sit between the context chips and toolbar in
20
32
  * {@link SessionComposer}, but usable standalone by platform builders
@@ -37,5 +49,5 @@ export interface AttachmentChipListProps {
37
49
  * }
38
50
  * ```
39
51
  */
40
- export declare function AttachmentChipList({ entries, onRemove, onRetry, disabled, className, }: AttachmentChipListProps): import("react/jsx-runtime").JSX.Element | null;
52
+ export declare function AttachmentChipList({ entries, onRemove, onRetry, onPreview, disabled, className, }: AttachmentChipListProps): import("react/jsx-runtime").JSX.Element | null;
41
53
  //# sourceMappingURL=AttachmentChipList.d.ts.map
@@ -1 +1 @@
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
+ {"version":3,"file":"AttachmentChipList.d.ts","sourceRoot":"","sources":["../../src/attachment/AttachmentChipList.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAM3D,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;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IACtD,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,yDAAyD;IACzD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,OAAO,EACP,QAAQ,EACR,OAAO,EACP,SAAS,EACT,QAAQ,EACR,SAAS,GACV,EAAE,uBAAuB,kDAgDzB"}
@@ -1,11 +1,19 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useEffect, useState } from "react";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
4
  import { cn } from "@stigmer/theme";
5
5
  import { formatFileSize } from "./attachment-utils.js";
6
+ import { useObjectUrl } from "./useObjectUrl.js";
7
+ import { AttachmentImageLightbox } from "./AttachmentImageLightbox.js";
8
+ import { UNSTYLED_BUTTON } from "../internal/form-primitives.js";
6
9
  /**
7
- * Renders a horizontal list of compact attachment chips showing
8
- * filename, size, upload status, and remove/retry actions.
10
+ * Renders a horizontal list of attachment chips: image attachments as
11
+ * preview-only thumbnail tiles (the image IS the chip — no filename
12
+ * text; visible from the moment of paste, through upload, and on error
13
+ * — stigmer/stigmer#371, #372), other files as compact filename chips.
14
+ * Filename, size, and upload status stay available on every chip via
15
+ * the tooltip and the accessible label; clicking an image tile opens
16
+ * the full image.
9
17
  *
10
18
  * Designed to sit between the context chips and toolbar in
11
19
  * {@link SessionComposer}, but usable standalone by platform builders
@@ -28,54 +36,112 @@ import { formatFileSize } from "./attachment-utils.js";
28
36
  * }
29
37
  * ```
30
38
  */
31
- export function AttachmentChipList({ entries, onRemove, onRetry, disabled, className, }) {
39
+ export function AttachmentChipList({ entries, onRemove, onRetry, onPreview, disabled, className, }) {
40
+ // The built-in lightbox tracks the previewed entry by ID (direct identity,
41
+ // never object reference), and the entry itself is derived from `entries`
42
+ // on render — so removing an attachment while its preview is open closes
43
+ // the lightbox without any effect-based synchronization.
44
+ const [previewId, setPreviewId] = useState(null);
45
+ const previewEntry = previewId !== null ? (entries.find((e) => e.id === previewId) ?? null) : null;
32
46
  if (entries.length === 0)
33
47
  return null;
34
- return (_jsx("div", { className: cn("flex flex-wrap gap-1.5", className), role: "list", "aria-label": "Attached files", children: entries.map((entry) => (_jsx(AttachmentChip, { entry: entry, onRemove: () => onRemove(entry.id), onRetry: () => onRetry(entry.id), disabled: disabled }, entry.id))) }));
48
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: cn("flex flex-wrap items-center gap-1.5", className), role: "list", "aria-label": "Attached files", children: entries.map((entry) => (_jsx(AttachmentChip, { entry: entry, onRemove: () => onRemove(entry.id), onRetry: () => onRetry(entry.id), onPreview: onPreview
49
+ ? () => onPreview(entry)
50
+ : () => setPreviewId(entry.id), disabled: disabled }, entry.id))) }), previewEntry && (_jsx(ChipPreviewLightbox, { entry: previewEntry, onClose: () => setPreviewId(null) }))] }));
35
51
  }
36
52
  // ---------------------------------------------------------------------------
37
- // Individual chip
53
+ // Individual chip — dispatches on content type
38
54
  // ---------------------------------------------------------------------------
39
- function AttachmentChip({ entry, onRemove, onRetry, disabled, }) {
40
- const isError = entry.phase === "error";
55
+ function AttachmentChip({ entry, onRemove, onRetry, onPreview, disabled, }) {
56
+ return entry.contentType.startsWith("image/") ? (_jsx(ImageAttachmentChip, { entry: entry, onRemove: onRemove, onRetry: onRetry, onPreview: onPreview, disabled: disabled })) : (_jsx(FileAttachmentChip, { entry: entry, onRemove: onRemove, onRetry: onRetry, disabled: disabled }));
57
+ }
58
+ /** Shared accessible name: filename, size, and upload status. */
59
+ function chipAriaLabel(entry) {
41
60
  const isUploading = entry.phase === "uploading";
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
43
- ? "border border-destructive/30 bg-destructive-subtle text-destructive"
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, {}) })] }));
61
+ const isError = entry.phase === "error";
62
+ return `${entry.file.name}, ${formatFileSize(entry.file.size)}${isUploading ? ", uploading" : ""}${isError ? ", upload failed" : ""}`;
46
63
  }
47
64
  // ---------------------------------------------------------------------------
48
- // Image thumbnail — a #284 ask: a pasted screenshot's chip shows the image,
49
- // not a generic file glyph
65
+ // Image chip — a preview-only thumbnail tile (#371, #372)
50
66
  // ---------------------------------------------------------------------------
51
67
  /**
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).
68
+ * The preview-only image tile: the thumbnail IS the chip. No filename
69
+ * text, no size caption — the image is its own identity (the grammar
70
+ * users know from Cursor/ChatGPT attachment tiles), while filename,
71
+ * size, and upload status stay one hover away in the tooltip and are
72
+ * always in the accessible label.
73
+ *
74
+ * The tile is alive through every upload phase (the spinner overlays
75
+ * the image during upload, never replaces it — the bytes are local, so
76
+ * the preview never waits on the network) and is one large click
77
+ * target that opens the full image. Remove is an always-visible corner
78
+ * badge — never hover-revealed, so it exists on touch devices and is
79
+ * discoverable without a pointer.
54
80
  *
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.
81
+ * On upload error the tile stays loud: destructive ring, dimmed image,
82
+ * and a centered Retry pill failure never hides behind hover.
59
83
  *
60
- * `aria-hidden`: the chip's own `aria-label` already names the file; a
61
- * 16px thumbnail adds nothing for screen readers.
84
+ * If the file claims `image/*` but the browser can't decode it (corrupt,
85
+ * zero-byte), the chip degrades to the compact file-chip treatment — a
86
+ * generic icon and no preview affordance, never a broken-image glyph.
87
+ */
88
+ function ImageAttachmentChip({ entry, onRemove, onRetry, onPreview, disabled, }) {
89
+ const url = useObjectUrl(entry.file);
90
+ const [loadFailed, setLoadFailed] = useState(false);
91
+ if (loadFailed) {
92
+ return (_jsx(FileAttachmentChip, { entry: entry, onRemove: onRemove, onRetry: onRetry, disabled: disabled }));
93
+ }
94
+ const isError = entry.phase === "error";
95
+ const isUploading = entry.phase === "uploading";
96
+ return (
97
+ // The remove badge and retry pill are SIBLINGS of the preview button,
98
+ // absolutely positioned over it — buttons cannot nest inside buttons.
99
+ _jsxs("span", { role: "listitem", "aria-label": chipAriaLabel(entry), title: entry.file.name, className: "relative inline-flex", children: [_jsxs("button", { type: "button", onClick: onPreview, "aria-label": `Preview ${entry.file.name}`, className: cn(UNSTYLED_BUTTON, "relative block h-14 w-14 overflow-hidden rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", isError && "ring-1 ring-destructive"), children: [url ? (_jsx("img", { src: url, alt: "", "aria-hidden": "true", onError: () => setLoadFailed(true), className: cn("h-full w-full object-cover", (isUploading || isError) && "opacity-50") })) : (
100
+ // One-frame placeholder until the object-URL effect runs; same
101
+ // footprint as the image, so no layout shift.
102
+ _jsx("span", { className: "block h-full w-full animate-pulse bg-muted", "aria-hidden": "true" })), isUploading && (_jsx("span", { className: "absolute inset-0 flex items-center justify-center", children: _jsx(ChipSpinner, { size: 16 }) }))] }), isError && (_jsx("button", { type: "button", onClick: onRetry, disabled: disabled, "aria-label": `Retry uploading ${entry.file.name}`, className: cn(UNSTYLED_BUTTON, "absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-destructive px-1.5 py-0.5 text-[0.6rem] font-medium leading-none text-destructive-foreground shadow-sm [font-family:inherit] hover:bg-destructive-hover disabled:pointer-events-none"), children: "Retry" })), _jsx("button", { type: "button", onClick: onRemove, disabled: disabled, "aria-label": `Remove ${entry.file.name}`, className: cn(UNSTYLED_BUTTON,
103
+ // The established corner-badge geometry (ContextPopover,
104
+ // ComposerToolbar). Solid bg + border keep it legible over any
105
+ // image without opacity-modified tokens (Dont-Do #4).
106
+ "absolute -right-1.5 -top-1.5 flex h-4 w-4 items-center justify-center rounded-full border border-border bg-background text-muted-foreground shadow-sm hover:text-destructive disabled:pointer-events-none"), children: _jsx(XIcon, {}) })] }));
107
+ }
108
+ // ---------------------------------------------------------------------------
109
+ // File chip — the compact treatment for non-image attachments
110
+ // ---------------------------------------------------------------------------
111
+ function FileAttachmentChip({ entry, onRemove, onRetry, disabled, }) {
112
+ const isError = entry.phase === "error";
113
+ const isUploading = entry.phase === "uploading";
114
+ return (_jsxs("span", { role: "listitem", "aria-label": chipAriaLabel(entry), className: cn("inline-flex max-w-[200px] items-center gap-1 rounded-md px-2 py-0.5 text-xs", isError
115
+ ? "border border-destructive/30 bg-destructive-subtle text-destructive"
116
+ : "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(RetryButton, { filename: entry.file.name, onRetry: onRetry, disabled: disabled }), _jsx(RemoveButton, { filename: entry.file.name, onRemove: onRemove, disabled: disabled })] }));
117
+ }
118
+ // ---------------------------------------------------------------------------
119
+ // Shared chip actions
120
+ // ---------------------------------------------------------------------------
121
+ function RetryButton({ filename, onRetry, disabled, }) {
122
+ return (_jsx("button", { type: "button", onClick: onRetry, disabled: disabled, className: cn(UNSTYLED_BUTTON, "shrink-0 text-[0.6rem] font-medium text-destructive underline [font-family:inherit] hover:text-destructive-muted disabled:pointer-events-none"), "aria-label": `Retry uploading ${filename}`, children: "Retry" }));
123
+ }
124
+ function RemoveButton({ filename, onRemove, disabled, }) {
125
+ return (_jsx("button", { type: "button", onClick: onRemove, disabled: disabled, className: cn(UNSTYLED_BUTTON, "ml-0.5 shrink-0 text-muted-foreground hover:text-destructive disabled:pointer-events-none"), "aria-label": `Remove ${filename}`, children: _jsx(XIcon, {}) }));
126
+ }
127
+ // ---------------------------------------------------------------------------
128
+ // Built-in lightbox mount
129
+ // ---------------------------------------------------------------------------
130
+ /**
131
+ * Bridges the previewed entry to the dumb {@link AttachmentImageLightbox}:
132
+ * mounted only while a preview is open, so its object URL is created on
133
+ * open and revoked on close (independent of the chip's own URL, whose
134
+ * lifetime tracks the chip).
62
135
  */
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" }));
136
+ function ChipPreviewLightbox({ entry, onClose, }) {
137
+ const src = useObjectUrl(entry.file);
138
+ return (_jsx(AttachmentImageLightbox, { src: src, filename: entry.file.name, open: true, onClose: onClose }));
73
139
  }
74
140
  // ---------------------------------------------------------------------------
75
141
  // Inline SVG icons — kept minimal and consistent with SessionComposer icons
76
142
  // ---------------------------------------------------------------------------
77
- function ChipSpinner() {
78
- return (_jsx("svg", { width: "10", height: "10", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", className: "shrink-0 animate-spin text-muted-foreground", "aria-hidden": "true", children: _jsx("path", { d: "M8 2a6 6 0 1 0 6 6" }) }));
143
+ function ChipSpinner({ size = 10 }) {
144
+ return (_jsx("svg", { width: size, height: size, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", className: "shrink-0 animate-spin text-muted-foreground", "aria-hidden": "true", children: _jsx("path", { d: "M8 2a6 6 0 1 0 6 6" }) }));
79
145
  }
80
146
  function ErrorDot() {
81
147
  return (_jsx("span", { className: "inline-block h-1.5 w-1.5 shrink-0 rounded-full bg-destructive", "aria-hidden": "true" }));
@@ -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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"}
@@ -0,0 +1,39 @@
1
+ /** Props for {@link AttachmentImageLightbox}. */
2
+ export interface AttachmentImageLightboxProps {
3
+ /**
4
+ * Image source URL. `null` for the first-paint frame before the caller's
5
+ * object URL exists — the lightbox shows a pulse placeholder, never a
6
+ * broken-image glyph.
7
+ */
8
+ readonly src: string | null;
9
+ /** Filename shown in the header and used as the image alt text. */
10
+ readonly filename: string;
11
+ /** Controls visibility. `true` opens the dialog via `showModal()`. */
12
+ readonly open: boolean;
13
+ /** Called when the lightbox should close (Escape, backdrop click, close button). */
14
+ readonly onClose: () => void;
15
+ /** Additional CSS classes for the dialog element. */
16
+ readonly className?: string;
17
+ }
18
+ /**
19
+ * Full-size image preview in a native `<dialog>` — the click-to-open target
20
+ * for composer attachment chips (stigmer/stigmer#371).
21
+ *
22
+ * A thin modal shell following the SDK's established dialog pattern
23
+ * ({@link ArtifactPreviewModal}): `showModal()`/`close()` driven by the
24
+ * `open` prop, Escape via the intercepted `cancel` event, focus trap and
25
+ * top layer native — no portal plumbing, no `@base-ui/react` dependency.
26
+ * Backdrop click closes via the canonical `e.target === dialog` check
27
+ * (clicks on content children never match the dialog element itself).
28
+ *
29
+ * Deliberately dumb (`src` + `filename`, no attachment types): liftable to
30
+ * `internal/` unchanged if other image surfaces (e.g. tool-output
31
+ * screenshots in `ResultView`) later want in-app preview instead of
32
+ * open-in-new-tab.
33
+ *
34
+ * The backdrop uses the `--stgm-backdrop` theme token ("modal backdrop
35
+ * overlay behind dialogs") rather than the `bg-black/50` hardcode found in
36
+ * older dialogs — the token is the designed value (Dont-Do #3).
37
+ */
38
+ export declare function AttachmentImageLightbox({ src, filename, open, onClose, className, }: AttachmentImageLightboxProps): import("react/jsx-runtime").JSX.Element;
39
+ //# sourceMappingURL=AttachmentImageLightbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AttachmentImageLightbox.d.ts","sourceRoot":"","sources":["../../src/attachment/AttachmentImageLightbox.tsx"],"names":[],"mappings":"AAKA,iDAAiD;AACjD,MAAM,WAAW,4BAA4B;IAC3C;;;;OAIG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,mEAAmE;IACnE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,oFAAoF;IACpF,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;IAC7B,qDAAqD;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,GAAG,EACH,QAAQ,EACR,IAAI,EACJ,OAAO,EACP,SAAS,GACV,EAAE,4BAA4B,2CAmF9B"}
@@ -0,0 +1,57 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useRef } from "react";
4
+ import { cn } from "@stigmer/theme";
5
+ /**
6
+ * Full-size image preview in a native `<dialog>` — the click-to-open target
7
+ * for composer attachment chips (stigmer/stigmer#371).
8
+ *
9
+ * A thin modal shell following the SDK's established dialog pattern
10
+ * ({@link ArtifactPreviewModal}): `showModal()`/`close()` driven by the
11
+ * `open` prop, Escape via the intercepted `cancel` event, focus trap and
12
+ * top layer native — no portal plumbing, no `@base-ui/react` dependency.
13
+ * Backdrop click closes via the canonical `e.target === dialog` check
14
+ * (clicks on content children never match the dialog element itself).
15
+ *
16
+ * Deliberately dumb (`src` + `filename`, no attachment types): liftable to
17
+ * `internal/` unchanged if other image surfaces (e.g. tool-output
18
+ * screenshots in `ResultView`) later want in-app preview instead of
19
+ * open-in-new-tab.
20
+ *
21
+ * The backdrop uses the `--stgm-backdrop` theme token ("modal backdrop
22
+ * overlay behind dialogs") rather than the `bg-black/50` hardcode found in
23
+ * older dialogs — the token is the designed value (Dont-Do #3).
24
+ */
25
+ export function AttachmentImageLightbox({ src, filename, open, onClose, className, }) {
26
+ const dialogRef = useRef(null);
27
+ useEffect(() => {
28
+ const dialog = dialogRef.current;
29
+ if (!dialog)
30
+ return;
31
+ if (open && !dialog.open) {
32
+ dialog.showModal();
33
+ }
34
+ else if (!open && dialog.open) {
35
+ dialog.close();
36
+ }
37
+ }, [open]);
38
+ // Escape arrives as the native `cancel` event; intercept it so the
39
+ // caller's `open` prop stays the single source of truth for visibility.
40
+ const handleCancel = useCallback((e) => {
41
+ e.preventDefault();
42
+ onClose();
43
+ }, [onClose]);
44
+ const handleBackdropClick = useCallback((e) => {
45
+ if (e.target === dialogRef.current) {
46
+ onClose();
47
+ }
48
+ }, [onClose]);
49
+ return (_jsx("dialog", { ref: dialogRef, onCancel: handleCancel, onClick: handleBackdropClick, "aria-label": `Preview ${filename}`, className: cn("fixed inset-0 m-auto max-h-[85vh] max-w-[85vw] rounded-lg border border-border bg-background p-0 text-foreground shadow-lg outline-none", "backdrop:bg-backdrop", className), children: open && (_jsxs("div", { className: "flex flex-col", children: [_jsxs("div", { className: "flex items-center gap-2 px-3 py-2", children: [_jsx("span", { className: "min-w-0 flex-1 truncate text-xs font-semibold text-foreground", title: filename, children: filename }), _jsx("button", { type: "button", onClick: onClose, "aria-label": "Close image preview", className: "shrink-0 cursor-pointer rounded bg-transparent p-0.5 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", children: _jsx(CloseIcon, {}) })] }), _jsx("div", { className: "min-h-0 overflow-auto border-t border-border", children: src ? (_jsx("img", { src: src, alt: filename, className: "block max-h-[75vh] max-w-full object-contain" })) : (_jsx("div", { className: "h-48 w-72 animate-pulse bg-muted", "aria-hidden": "true" })) })] })) }));
50
+ }
51
+ // ---------------------------------------------------------------------------
52
+ // Inline SVG icons (SDK pattern: no external icon dependency)
53
+ // ---------------------------------------------------------------------------
54
+ function CloseIcon() {
55
+ return (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("path", { d: "M3 3L11 11" }), _jsx("path", { d: "M11 3L3 11" })] }));
56
+ }
57
+ //# sourceMappingURL=AttachmentImageLightbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AttachmentImageLightbox.js","sourceRoot":"","sources":["../../src/attachment/AttachmentImageLightbox.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AACvD,OAAO,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;AAoBpC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,uBAAuB,CAAC,EACtC,GAAG,EACH,QAAQ,EACR,IAAI,EACJ,OAAO,EACP,SAAS,GACoB;IAC7B,MAAM,SAAS,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAC;IAElD,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC;QACjC,IAAI,CAAC,MAAM;YAAE,OAAO;QAEpB,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YACzB,MAAM,CAAC,SAAS,EAAE,CAAC;QACrB,CAAC;aAAM,IAAI,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;YAChC,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,CAAC;IACH,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAEX,mEAAmE;IACnE,wEAAwE;IACxE,MAAM,YAAY,GAAG,WAAW,CAC9B,CAAC,CAAuB,EAAE,EAAE;QAC1B,CAAC,CAAC,cAAc,EAAE,CAAC;QACnB,OAAO,EAAE,CAAC;IACZ,CAAC,EACD,CAAC,OAAO,CAAC,CACV,CAAC;IAEF,MAAM,mBAAmB,GAAG,WAAW,CACrC,CAAC,CAAsC,EAAE,EAAE;QACzC,IAAI,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,OAAO,EAAE,CAAC;YACnC,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC,EACD,CAAC,OAAO,CAAC,CACV,CAAC;IAEF,OAAO,CACL,iBACE,GAAG,EAAE,SAAS,EACd,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE,mBAAmB,gBAChB,WAAW,QAAQ,EAAE,EACjC,SAAS,EAAE,EAAE,CACX,yIAAyI,EACzI,sBAAsB,EACtB,SAAS,CACV,YAEA,IAAI,IAAI,CACP,eAAK,SAAS,EAAC,eAAe,aAC5B,eAAK,SAAS,EAAC,mCAAmC,aAChD,eACE,SAAS,EAAC,+DAA+D,EACzE,KAAK,EAAE,QAAQ,YAEd,QAAQ,GACJ,EAGP,iBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,OAAO,gBACL,qBAAqB,EAChC,SAAS,EAAC,4LAA4L,YAEtM,KAAC,SAAS,KAAG,GACN,IACL,EACN,cAAK,SAAS,EAAC,8CAA8C,YAC1D,GAAG,CAAC,CAAC,CAAC,CACL,cACE,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,QAAQ,EACb,SAAS,EAAC,8CAA8C,GACxD,CACH,CAAC,CAAC,CAAC,CACF,cACE,SAAS,EAAC,kCAAkC,iBAChC,MAAM,GAClB,CACH,GACG,IACF,CACP,GACM,CACV,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,8DAA8D;AAC9D,8EAA8E;AAE9E,SAAS,SAAS;IAChB,OAAO,CACL,eACE,KAAK,EAAC,IAAI,EACV,MAAM,EAAC,IAAI,EACX,OAAO,EAAC,WAAW,EACnB,IAAI,EAAC,MAAM,EACX,MAAM,EAAC,cAAc,EACrB,WAAW,EAAC,KAAK,EACjB,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,iBACV,MAAM,aAElB,eAAM,CAAC,EAAC,YAAY,GAAG,EACvB,eAAM,CAAC,EAAC,YAAY,GAAG,IACnB,CACP,CAAC;AACJ,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Object URL over a locally-held `Blob` (or `File`), alive for the lifetime
3
+ * of the mounted component (no fetch — the bytes are already in memory).
4
+ *
5
+ * The URL is created in an effect, not a memo: StrictMode double-invokes
6
+ * memos and would leak the first URL without a revoke. The effect cleanup
7
+ * revokes on unmount and on blob change. Returns `null` until the effect
8
+ * runs (first paint) — callers render a fallback for that frame.
9
+ *
10
+ * Deliberately display-oriented: the SDK's other `createObjectURL` calls
11
+ * (`useExportResource`, `useExportCSV`) are fire-and-forget download
12
+ * helpers with an immediate revoke, a different lifecycle from this one.
13
+ */
14
+ export declare function useObjectUrl(blob: Blob): string | null;
15
+ //# sourceMappingURL=useObjectUrl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useObjectUrl.d.ts","sourceRoot":"","sources":["../../src/attachment/useObjectUrl.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAUtD"}
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { useEffect, useState } from "react";
3
+ /**
4
+ * Object URL over a locally-held `Blob` (or `File`), alive for the lifetime
5
+ * of the mounted component (no fetch — the bytes are already in memory).
6
+ *
7
+ * The URL is created in an effect, not a memo: StrictMode double-invokes
8
+ * memos and would leak the first URL without a revoke. The effect cleanup
9
+ * revokes on unmount and on blob change. Returns `null` until the effect
10
+ * runs (first paint) — callers render a fallback for that frame.
11
+ *
12
+ * Deliberately display-oriented: the SDK's other `createObjectURL` calls
13
+ * (`useExportResource`, `useExportCSV`) are fire-and-forget download
14
+ * helpers with an immediate revoke, a different lifecycle from this one.
15
+ */
16
+ export function useObjectUrl(blob) {
17
+ const [url, setUrl] = useState(null);
18
+ useEffect(() => {
19
+ const objectUrl = URL.createObjectURL(blob);
20
+ setUrl(objectUrl);
21
+ return () => URL.revokeObjectURL(objectUrl);
22
+ }, [blob]);
23
+ return url;
24
+ }
25
+ //# sourceMappingURL=useObjectUrl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useObjectUrl.js","sourceRoot":"","sources":["../../src/attachment/useObjectUrl.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAE5C;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAAC,IAAU;IACrC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAC;IAEpD,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,SAAS,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;QAC5C,MAAM,CAAC,SAAS,CAAC,CAAC;QAClB,OAAO,GAAG,EAAE,CAAC,GAAG,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC;IAC9C,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAEX,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,45 @@
1
+ import type { ConversationMediaRef } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/conversation_io_pb";
2
+ /**
3
+ * The full address of one timeline item's media — what
4
+ * `getMediaDownloadUrl` is addressed by. The storage key deliberately
5
+ * never rides the wire (whatsapp-media DD-001 D4); the server resolves
6
+ * it from its own row, so this address is all a client ever holds.
7
+ */
8
+ export interface ConversationMediaAddress {
9
+ /** AgentChannel the conversation belongs to. */
10
+ readonly agentChannelId: string;
11
+ /** Conversation key within the channel (WhatsApp: the customer's wa_id). */
12
+ readonly conversationKey: string;
13
+ /** The timeline item carrying the media (its item_id, e.g. `"wa:<wamid>"`). */
14
+ readonly itemId: string;
15
+ }
16
+ /** Props for {@link ConversationMediaAttachment}. */
17
+ export interface ConversationMediaAttachmentProps {
18
+ /** The item's media reference (filename, content type, size). */
19
+ readonly media: ConversationMediaRef;
20
+ /** Where to fetch the bytes from. */
21
+ readonly address: ConversationMediaAddress;
22
+ /** Additional CSS classes for the container. */
23
+ readonly className?: string;
24
+ }
25
+ /**
26
+ * One inbound timeline item's ingested media (stigmer/stigmer#367):
27
+ * images as a WhatsApp-style inline thumbnail (click opens the shared
28
+ * {@link AttachmentImageLightbox}), everything else as a document chip
29
+ * (filename + size, click opens the file in a new tab).
30
+ *
31
+ * This component calls `useStigmer()`, so it must only be MOUNTED when
32
+ * the timeline view holds a conversation address — that conditional
33
+ * mount is what keeps `ConversationTimelineView` renderable without a
34
+ * `StigmerProvider` (its presentational contract; see the view's
35
+ * `agentChannelId` prop).
36
+ *
37
+ * Deliberately a sibling of the execution thread's `MessageAttachments`,
38
+ * not a reuse of it: that row renders storage-key-addressed artifacts of
39
+ * an execution the viewer owns, while this one renders
40
+ * conversation-viewer-scoped media addressed by timeline position — the
41
+ * two read paths have different trust models (DD-001 D4) and different
42
+ * bubble treatments (chat thumbnail vs. attachment chip row).
43
+ */
44
+ export declare function ConversationMediaAttachment({ media, address, className, }: ConversationMediaAttachmentProps): import("react/jsx-runtime").JSX.Element;
45
+ //# sourceMappingURL=ConversationMediaAttachment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConversationMediaAttachment.d.ts","sourceRoot":"","sources":["../../src/conversation/ConversationMediaAttachment.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uEAAuE,CAAC;AASlH;;;;;GAKG;AACH,MAAM,WAAW,wBAAwB;IACvC,gDAAgD;IAChD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,4EAA4E;IAC5E,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,qDAAqD;AACrD,MAAM,WAAW,gCAAgC;IAC/C,iEAAiE;IACjE,QAAQ,CAAC,KAAK,EAAE,oBAAoB,CAAC;IACrC,qCAAqC;IACrC,QAAQ,CAAC,OAAO,EAAE,wBAAwB,CAAC;IAC3C,gDAAgD;IAChD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,2BAA2B,CAAC,EAC1C,KAAK,EACL,OAAO,EACP,SAAS,GACV,EAAE,gCAAgC,2CAOlC"}
@@ -0,0 +1,153 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useMemo, useState } from "react";
4
+ import { create } from "@bufbuild/protobuf";
5
+ import { cn } from "@stigmer/theme";
6
+ import { getUserMessage } from "@stigmer/sdk";
7
+ import { GetConversationMediaDownloadUrlInputSchema } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/conversation_io_pb";
8
+ import { AttachmentImageLightbox } from "../attachment/AttachmentImageLightbox.js";
9
+ import { formatFileSize } from "../attachment/attachment-utils.js";
10
+ import { UNSTYLED_BUTTON } from "../internal/form-primitives.js";
11
+ import { toError } from "../internal/toError.js";
12
+ import { useStigmer } from "../hooks.js";
13
+ import { useConversationMediaUrl } from "./useConversationMediaUrl.js";
14
+ /**
15
+ * One inbound timeline item's ingested media (stigmer/stigmer#367):
16
+ * images as a WhatsApp-style inline thumbnail (click opens the shared
17
+ * {@link AttachmentImageLightbox}), everything else as a document chip
18
+ * (filename + size, click opens the file in a new tab).
19
+ *
20
+ * This component calls `useStigmer()`, so it must only be MOUNTED when
21
+ * the timeline view holds a conversation address — that conditional
22
+ * mount is what keeps `ConversationTimelineView` renderable without a
23
+ * `StigmerProvider` (its presentational contract; see the view's
24
+ * `agentChannelId` prop).
25
+ *
26
+ * Deliberately a sibling of the execution thread's `MessageAttachments`,
27
+ * not a reuse of it: that row renders storage-key-addressed artifacts of
28
+ * an execution the viewer owns, while this one renders
29
+ * conversation-viewer-scoped media addressed by timeline position — the
30
+ * two read paths have different trust models (DD-001 D4) and different
31
+ * bubble treatments (chat thumbnail vs. attachment chip row).
32
+ */
33
+ export function ConversationMediaAttachment({ media, address, className, }) {
34
+ const isImage = media.contentType.startsWith("image/");
35
+ return isImage ? (_jsx(MediaImageThumbnail, { media: media, address: address, className: className })) : (_jsx(MediaDocumentChip, { media: media, address: address, className: className }));
36
+ }
37
+ /**
38
+ * Display name for the media: the ingest-recorded filename (documents
39
+ * keep the provider's name; camera images get a platform-synthesized
40
+ * one, so this is never empty in practice), with a defensive fallback.
41
+ */
42
+ function displayName(media) {
43
+ return media.filename || "attachment";
44
+ }
45
+ // ---------------------------------------------------------------------------
46
+ // Image — WhatsApp-style inline thumbnail, click-to-open lightbox
47
+ // ---------------------------------------------------------------------------
48
+ /**
49
+ * A bounded preview frame in the bubble, minted from the item address at
50
+ * view time. The frame is fixed-size for BOTH the pulse placeholder and
51
+ * the loaded image, so the load causes no layout shift (the
52
+ * content-agnostic auto-scroll then needs no special case). If the URL
53
+ * can't be minted (revoked media, transient API failure, the server's
54
+ * uniform NOT_FOUND), the treatment degrades to the document chip —
55
+ * never a broken-image glyph (the `MessageAttachments` precedent).
56
+ */
57
+ function MediaImageThumbnail({ media, address, className, }) {
58
+ const { url, error } = useConversationMediaUrl(address.agentChannelId, address.conversationKey, address.itemId);
59
+ const [previewOpen, setPreviewOpen] = useState(false);
60
+ const name = displayName(media);
61
+ if (error) {
62
+ return _jsx(MediaDocumentChip, { media: media, address: address, className: className });
63
+ }
64
+ return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: () => setPreviewOpen(true), "aria-label": `Preview ${name}`, className: cn(UNSTYLED_BUTTON, "block rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", className), children: _jsx("span", { className: "relative block h-40 w-56 max-w-full overflow-hidden rounded-md", children: url ? (_jsx("img", { src: url, alt: "", "aria-hidden": "true", loading: "lazy", className: "h-full w-full object-cover" })) : (
65
+ // Pulse placeholder while the presigned URL is minted; same
66
+ // footprint as the image, so no layout shift.
67
+ _jsx("span", { className: "block h-full w-full animate-pulse bg-muted", "aria-hidden": "true" })) }) }), previewOpen && (_jsx(AttachmentImageLightbox, { src: url, filename: name, open: true, onClose: () => setPreviewOpen(false) }))] }));
68
+ }
69
+ // ---------------------------------------------------------------------------
70
+ // Document — compact chip, click-to-open in a new tab
71
+ // ---------------------------------------------------------------------------
72
+ /**
73
+ * The compact treatment for documents (and the degraded arm for images
74
+ * whose URL failed): file glyph, filename, size. A click mints a FRESH
75
+ * URL — presigned URLs expire, so the stable item address is the thing
76
+ * to hold, not a URL (the `useArtifactDownload` rationale) — and opens
77
+ * it in a new tab: the media contract presigns inline disposition only,
78
+ * so the browser previews what it can (PDFs) and downloads the rest.
79
+ * A failed mint reports below the chip in the surface's own error
80
+ * grammar (DD-006 — never a silent dead click).
81
+ */
82
+ function MediaDocumentChip({ media, address, className, }) {
83
+ const { open, isOpening, error } = useOpenConversationMedia(address);
84
+ const name = displayName(media);
85
+ const size = media.sizeBytes > BigInt(0)
86
+ ? // WhatsApp caps media far below 2^53 bytes, so the narrowing is exact.
87
+ formatFileSize(Number(media.sizeBytes))
88
+ : null;
89
+ return (_jsxs("div", { className: className, children: [_jsx("span", { title: name, className: "inline-flex max-w-[240px] items-center rounded-md bg-muted-subtle px-2 py-1 text-xs text-foreground", children: _jsxs("button", { type: "button", onClick: () => void open(), disabled: isOpening, "aria-label": `Open ${name}`, className: cn(UNSTYLED_BUTTON, "flex min-w-0 items-center gap-1.5 rounded-sm text-inherit [font:inherit] hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", isOpening && "opacity-70"), children: [_jsx(FileGlyph, {}), _jsx("span", { className: "min-w-0 truncate", children: name }), size !== null && (_jsx("span", { className: "shrink-0 text-muted-foreground", children: size }))] }) }), error !== null && (_jsx("p", { className: "mt-1 break-words text-xs text-destructive", children: getUserMessage(error) }))] }));
90
+ }
91
+ /**
92
+ * Click → mint fresh URL → open in a new tab. Kept local to this file:
93
+ * unlike {@link useConversationMediaUrl} (render data), an open is a
94
+ * user action — and it must complete within the browser's
95
+ * user-activation window, which the single fast unary mint does.
96
+ */
97
+ function useOpenConversationMedia(address) {
98
+ const stigmer = useStigmer();
99
+ const [isOpening, setIsOpening] = useState(false);
100
+ const [error, setError] = useState(null);
101
+ const open = useCallback(async () => {
102
+ if (isOpening)
103
+ return;
104
+ setIsOpening(true);
105
+ setError(null);
106
+ try {
107
+ const result = await stigmer.agentChannel.getMediaDownloadUrl(create(GetConversationMediaDownloadUrlInputSchema, {
108
+ agentChannelId: address.agentChannelId,
109
+ conversationKey: address.conversationKey,
110
+ itemId: address.itemId,
111
+ }));
112
+ if (result.url) {
113
+ openInNewTab(result.url);
114
+ }
115
+ }
116
+ catch (err) {
117
+ setError(toError(err));
118
+ }
119
+ finally {
120
+ setIsOpening(false);
121
+ }
122
+ }, [stigmer, address.agentChannelId, address.conversationKey, address.itemId, isOpening]);
123
+ return useMemo(() => ({ open, isOpening, error }), [open, isOpening, error]);
124
+ }
125
+ /**
126
+ * Open a URL in a new tab via a transient anchor click (the
127
+ * `triggerBrowserDownload` shape, minus the `download` hint — the URL's
128
+ * disposition is inline and cross-origin anchors ignore the attribute
129
+ * anyway). Falls back to `window.open` in non-DOM environments.
130
+ */
131
+ function openInNewTab(url) {
132
+ if (typeof document === "undefined") {
133
+ if (typeof window !== "undefined") {
134
+ window.open(url, "_blank", "noopener,noreferrer");
135
+ }
136
+ return;
137
+ }
138
+ const anchor = document.createElement("a");
139
+ anchor.href = url;
140
+ anchor.rel = "noopener noreferrer";
141
+ anchor.target = "_blank";
142
+ document.body.appendChild(anchor);
143
+ anchor.click();
144
+ document.body.removeChild(anchor);
145
+ }
146
+ /**
147
+ * Generic document glyph — the same geometry as the attachment chips'
148
+ * icon so the surfaces read as one family.
149
+ */
150
+ function FileGlyph() {
151
+ return (_jsxs("svg", { width: "10", height: "10", viewBox: "0 0 14 14", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", className: "shrink-0 text-muted-foreground", "aria-hidden": "true", children: [_jsx("path", { d: "M8 1H4C3.45 1 3 1.45 3 2V12C3 12.55 3.45 13 4 13H10C10.55 13 11 12.55 11 12V4L8 1Z" }), _jsx("path", { d: "M8 1V4H11" })] }));
152
+ }
153
+ //# sourceMappingURL=ConversationMediaAttachment.js.map