@comergehq/studio 0.1.35 → 0.1.37

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/dist/index.js CHANGED
@@ -516,8 +516,8 @@ __export(index_exports, {
516
516
  module.exports = __toCommonJS(index_exports);
517
517
 
518
518
  // src/studio/ComergeStudio.tsx
519
- var React51 = __toESM(require("react"));
520
- var import_react_native61 = require("react-native");
519
+ var React52 = __toESM(require("react"));
520
+ var import_react_native63 = require("react-native");
521
521
  var import_bottom_sheet6 = require("@gorhom/bottom-sheet");
522
522
  var import_runtime2 = require("@comergehq/runtime");
523
523
 
@@ -730,6 +730,7 @@ publicApi.interceptors.request.use((config) => {
730
730
 
731
731
  // src/core/services/supabase/client.ts
732
732
  var import_supabase_js = require("@supabase/supabase-js");
733
+ var import_async_storage = __toESM(require("@react-native-async-storage/async-storage"));
733
734
  var clientSingleton = null;
734
735
  var injectedClient = null;
735
736
  var hasInjectedClient = false;
@@ -756,6 +757,7 @@ function getSupabaseClient() {
756
757
  }
757
758
  clientSingleton = (0, import_supabase_js.createClient)(runtimeConfig.url, runtimeConfig.anonKey, {
758
759
  auth: {
760
+ storage: import_async_storage.default,
759
761
  autoRefreshToken: true,
760
762
  persistSession: true,
761
763
  detectSessionInUrl: false
@@ -1635,6 +1637,34 @@ function compareMessages(a, b) {
1635
1637
  if (aMs !== bMs) return aMs - bMs;
1636
1638
  return String(a.createdAt).localeCompare(String(b.createdAt));
1637
1639
  }
1640
+ function parseAttachments(payload) {
1641
+ const raw = payload == null ? void 0 : payload.attachments;
1642
+ if (!Array.isArray(raw)) return [];
1643
+ const out = [];
1644
+ for (const item of raw) {
1645
+ if (!item || typeof item !== "object") continue;
1646
+ const id = typeof item.id === "string" ? String(item.id) : "";
1647
+ const name = typeof item.name === "string" ? String(item.name) : "";
1648
+ const mimeType = typeof item.mimeType === "string" ? String(item.mimeType) : "";
1649
+ const size = typeof item.size === "number" ? Number(item.size) : 0;
1650
+ if (!id || !name || !mimeType || !Number.isFinite(size) || size <= 0) continue;
1651
+ out.push({
1652
+ id,
1653
+ name,
1654
+ mimeType,
1655
+ size,
1656
+ uri: typeof item.downloadUrl === "string" ? String(item.downloadUrl) : void 0,
1657
+ width: typeof item.width === "number" ? Number(item.width) : void 0,
1658
+ height: typeof item.height === "number" ? Number(item.height) : void 0
1659
+ });
1660
+ }
1661
+ return out;
1662
+ }
1663
+ function hasAttachmentWithoutUrl(payload) {
1664
+ const attachments = parseAttachments(payload);
1665
+ if (attachments.length === 0) return false;
1666
+ return attachments.some((att) => !att.uri);
1667
+ }
1638
1668
  function mapMessageToChatMessage(m) {
1639
1669
  var _a, _b;
1640
1670
  const kind = typeof ((_a = m.payload) == null ? void 0 : _a.type) === "string" ? String(m.payload.type) : null;
@@ -1644,7 +1674,8 @@ function mapMessageToChatMessage(m) {
1644
1674
  content: typeof ((_b = m.payload) == null ? void 0 : _b.content) === "string" ? m.payload.content : "",
1645
1675
  createdAt: m.createdAt,
1646
1676
  kind,
1647
- meta: extractMeta(m.payload)
1677
+ meta: extractMeta(m.payload),
1678
+ attachments: parseAttachments(m.payload)
1648
1679
  };
1649
1680
  }
1650
1681
  function useThreadMessages(threadId) {
@@ -1655,9 +1686,19 @@ function useThreadMessages(threadId) {
1655
1686
  const activeRequestIdRef = React4.useRef(0);
1656
1687
  const foregroundSignal = useForegroundSignal(Boolean(threadId));
1657
1688
  const hasLoadedOnceRef = React4.useRef(false);
1689
+ const attachmentRecoveryTimerRef = React4.useRef(null);
1690
+ const lastAttachmentRecoveryAtRef = React4.useRef(0);
1658
1691
  React4.useEffect(() => {
1659
1692
  hasLoadedOnceRef.current = false;
1660
1693
  }, [threadId]);
1694
+ React4.useEffect(() => {
1695
+ return () => {
1696
+ if (attachmentRecoveryTimerRef.current) {
1697
+ clearTimeout(attachmentRecoveryTimerRef.current);
1698
+ attachmentRecoveryTimerRef.current = null;
1699
+ }
1700
+ };
1701
+ }, []);
1661
1702
  const upsertSorted = React4.useCallback((prev, m) => {
1662
1703
  const next = prev.filter((x) => x.id !== m.id);
1663
1704
  next.push(m);
@@ -1698,29 +1739,56 @@ function useThreadMessages(threadId) {
1698
1739
  }
1699
1740
  }
1700
1741
  }, [threadId]);
1742
+ const recoverAttachmentUrls = React4.useCallback(() => {
1743
+ if (!threadId) return;
1744
+ if (attachmentRecoveryTimerRef.current) return;
1745
+ const now = Date.now();
1746
+ if (now - lastAttachmentRecoveryAtRef.current < 2e3) return;
1747
+ attachmentRecoveryTimerRef.current = setTimeout(() => {
1748
+ attachmentRecoveryTimerRef.current = null;
1749
+ lastAttachmentRecoveryAtRef.current = Date.now();
1750
+ void refetch({ background: true });
1751
+ }, 250);
1752
+ }, [refetch, threadId]);
1701
1753
  React4.useEffect(() => {
1702
1754
  void refetch();
1703
1755
  }, [refetch]);
1704
1756
  React4.useEffect(() => {
1705
1757
  if (!threadId) return;
1706
1758
  const unsubscribe = messagesRepository.subscribeThread(threadId, {
1707
- onInsert: (m) => setRaw((prev) => upsertSorted(prev, m)),
1708
- onUpdate: (m) => setRaw((prev) => upsertSorted(prev, m)),
1759
+ onInsert: (m) => {
1760
+ setRaw((prev) => upsertSorted(prev, m));
1761
+ if (hasAttachmentWithoutUrl(m.payload)) {
1762
+ recoverAttachmentUrls();
1763
+ }
1764
+ },
1765
+ onUpdate: (m) => {
1766
+ setRaw((prev) => upsertSorted(prev, m));
1767
+ if (hasAttachmentWithoutUrl(m.payload)) {
1768
+ recoverAttachmentUrls();
1769
+ }
1770
+ },
1709
1771
  onDelete: (m) => setRaw((prev) => prev.filter((x) => x.id !== m.id))
1710
1772
  });
1711
1773
  return unsubscribe;
1712
- }, [threadId, upsertSorted, foregroundSignal]);
1774
+ }, [threadId, upsertSorted, foregroundSignal, recoverAttachmentUrls]);
1713
1775
  React4.useEffect(() => {
1714
1776
  if (!threadId) return;
1715
1777
  if (foregroundSignal <= 0) return;
1716
1778
  void refetch({ background: true });
1717
1779
  }, [foregroundSignal, refetch, threadId]);
1780
+ React4.useEffect(() => {
1781
+ if (!threadId) return;
1782
+ if (raw.length === 0) return;
1783
+ if (!raw.some((m) => hasAttachmentWithoutUrl(m.payload))) return;
1784
+ recoverAttachmentUrls();
1785
+ }, [raw, recoverAttachmentUrls, threadId]);
1718
1786
  const messages = React4.useMemo(() => {
1719
1787
  const visible = raw.filter((m) => !isQueuedHiddenMessage(m));
1720
1788
  const resolved = visible.length > 0 ? visible : raw;
1721
1789
  return resolved.map(mapMessageToChatMessage);
1722
1790
  }, [raw]);
1723
- return { raw, messages, loading, refreshing, error, refetch };
1791
+ return { raw, messages, loading, refreshing, error, refetch, recoverAttachmentUrls };
1724
1792
  }
1725
1793
 
1726
1794
  // src/studio/hooks/useBundleManager.ts
@@ -2983,6 +3051,23 @@ function getMimeTypeFromDataUrl(dataUrl) {
2983
3051
  const mimeMatch = header.match(/data:(.*?);base64/i);
2984
3052
  return (mimeMatch == null ? void 0 : mimeMatch[1]) ?? "image/png";
2985
3053
  }
3054
+ async function getImageDimensionsFromDataUrl(dataUrl) {
3055
+ try {
3056
+ const normalized = dataUrl.startsWith("data:") ? dataUrl : `data:image/png;base64,${dataUrl}`;
3057
+ const dims = await new Promise((resolve, reject) => {
3058
+ import_react_native6.Image.getSize(
3059
+ normalized,
3060
+ (width, height) => resolve({ width, height }),
3061
+ (err) => reject(err)
3062
+ );
3063
+ });
3064
+ if (dims.width > 0 && dims.height > 0) {
3065
+ return { width: Math.round(dims.width), height: Math.round(dims.height) };
3066
+ }
3067
+ } catch {
3068
+ }
3069
+ return {};
3070
+ }
2986
3071
  function useAttachmentUpload() {
2987
3072
  const [uploading, setUploading] = React7.useState(false);
2988
3073
  const [error, setError] = React7.useState(null);
@@ -2997,17 +3082,24 @@ function useAttachmentUpload() {
2997
3082
  const normalized = dataUrl.startsWith("data:") ? dataUrl : `data:image/png;base64,${dataUrl}`;
2998
3083
  const blob = import_react_native6.Platform.OS === "android" ? await dataUrlToBlobAndroid(normalized) : await (await fetch(normalized)).blob();
2999
3084
  const mimeType = getMimeTypeFromDataUrl(normalized);
3000
- return { blob, idx, mimeType };
3085
+ const dimensions = mimeType.startsWith("image/") ? await getImageDimensionsFromDataUrl(normalized) : {};
3086
+ return { blob, idx, mimeType, ...dimensions };
3001
3087
  })
3002
3088
  );
3003
- const files = blobs.map(({ blob, mimeType }, idx) => ({
3089
+ const files = blobs.map(({ blob, mimeType, width, height }, idx) => ({
3004
3090
  name: `attachment-${Date.now()}-${idx}.png`,
3005
3091
  size: blob.size,
3006
- mimeType
3092
+ mimeType,
3093
+ width,
3094
+ height
3007
3095
  }));
3008
3096
  const presign = await attachmentRepository.presign({ threadId, appId, files });
3009
3097
  await Promise.all(presign.uploads.map((u, index) => attachmentRepository.upload(u, blobs[index].blob)));
3010
- return presign.uploads.map((u) => u.attachment);
3098
+ return presign.uploads.map((u, index) => ({
3099
+ ...u.attachment,
3100
+ width: blobs[index].width,
3101
+ height: blobs[index].height
3102
+ }));
3011
3103
  } catch (e) {
3012
3104
  const err = e instanceof Error ? e : new Error(String(e));
3013
3105
  setError(err);
@@ -3026,13 +3118,16 @@ function useAttachmentUpload() {
3026
3118
  const normalized = dataUrl.startsWith("data:") ? dataUrl : `data:image/png;base64,${dataUrl}`;
3027
3119
  const blob = import_react_native6.Platform.OS === "android" ? await dataUrlToBlobAndroid(normalized) : await (await fetch(normalized)).blob();
3028
3120
  const mimeType = getMimeTypeFromDataUrl(normalized);
3029
- return { blob, mimeType };
3121
+ const dimensions = mimeType.startsWith("image/") ? await getImageDimensionsFromDataUrl(normalized) : {};
3122
+ return { blob, mimeType, ...dimensions };
3030
3123
  })
3031
3124
  );
3032
- const files = blobs.map(({ blob, mimeType }, idx) => ({
3125
+ const files = blobs.map(({ blob, mimeType, width, height }, idx) => ({
3033
3126
  name: `attachment-${Date.now()}-${idx}.png`,
3034
3127
  size: blob.size,
3035
- mimeType
3128
+ mimeType,
3129
+ width,
3130
+ height
3036
3131
  }));
3037
3132
  const presign = await attachmentRepository.stagePresign({ files });
3038
3133
  await Promise.all(presign.uploads.map((u, index) => attachmentRepository.uploadStaged(u, blobs[index].blob)));
@@ -3315,8 +3410,8 @@ function RuntimeRenderer({
3315
3410
  }
3316
3411
 
3317
3412
  // src/studio/ui/StudioOverlay.tsx
3318
- var React46 = __toESM(require("react"));
3319
- var import_react_native60 = require("react-native");
3413
+ var React47 = __toESM(require("react"));
3414
+ var import_react_native62 = require("react-native");
3320
3415
 
3321
3416
  // src/components/studio-sheet/StudioBottomSheet.tsx
3322
3417
  var React12 = __toESM(require("react"));
@@ -3856,6 +3951,7 @@ function EdgeGlowFrame({
3856
3951
  role = "accent",
3857
3952
  thickness = 40,
3858
3953
  intensity = 1,
3954
+ animationDurationMs = 300,
3859
3955
  style
3860
3956
  }) {
3861
3957
  const theme = useTheme();
@@ -3864,10 +3960,10 @@ function EdgeGlowFrame({
3864
3960
  React14.useEffect(() => {
3865
3961
  import_react_native13.Animated.timing(anim, {
3866
3962
  toValue: visible ? 1 : 0,
3867
- duration: 300,
3963
+ duration: animationDurationMs,
3868
3964
  useNativeDriver: true
3869
3965
  }).start();
3870
- }, [anim, visible]);
3966
+ }, [anim, visible, animationDurationMs]);
3871
3967
  const c = baseColor(role, theme);
3872
3968
  const strong = withAlpha(c, 0.6 * alpha);
3873
3969
  const soft = withAlpha(c, 0.22 * alpha);
@@ -4386,7 +4482,16 @@ function DrawModeOverlay({
4386
4482
  }, [captureTargetRef, capturing, onCapture]);
4387
4483
  if (!visible) return null;
4388
4484
  return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native17.View, { style: [import_react_native17.StyleSheet.absoluteFill, styles3.root, style], pointerEvents: "box-none", children: [
4389
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(EdgeGlowFrame, { visible: !hideUi, role: "danger", thickness: 50, intensity: 1 }),
4485
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
4486
+ EdgeGlowFrame,
4487
+ {
4488
+ visible: !hideUi,
4489
+ role: "danger",
4490
+ thickness: 50,
4491
+ intensity: 1,
4492
+ animationDurationMs: hideUi ? 0 : 300
4493
+ }
4494
+ ),
4390
4495
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
4391
4496
  DrawSurface,
4392
4497
  {
@@ -5874,7 +5979,10 @@ function PreviewRelatedAppsSection({
5874
5979
  }) {
5875
5980
  var _a;
5876
5981
  const theme = useTheme();
5982
+ const { height: windowHeight } = (0, import_react_native35.useWindowDimensions)();
5877
5983
  const [relatedAppsOpen, setRelatedAppsOpen] = React27.useState(false);
5984
+ const modalMaxHeight = Math.max(240, windowHeight * 0.5);
5985
+ const modalScrollMaxHeight = Math.max(140, modalMaxHeight - 96);
5878
5986
  const relatedAppItems = React27.useMemo(() => {
5879
5987
  if (!relatedApps) return [];
5880
5988
  const items = [];
@@ -6007,10 +6115,7 @@ function PreviewRelatedAppsSection({
6007
6115
  }
6008
6116
  )
6009
6117
  ] }),
6010
- /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_react_native35.View, { style: { alignItems: "flex-end", gap: 6 }, children: [
6011
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_react_native35.View, { style: { minHeight: 20, justifyContent: "center" }, children: item.app.status !== "ready" ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(PreviewStatusBadge, { status: item.app.status }) : null }),
6012
- isSwitching ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_react_native35.ActivityIndicator, { size: "small", color: theme.colors.primary }) : null
6013
- ] })
6118
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_react_native35.View, { style: { alignItems: "flex-end", gap: 6 }, children: isSwitching ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_react_native35.ActivityIndicator, { size: "small", color: theme.colors.primary }) : null })
6014
6119
  ] })
6015
6120
  },
6016
6121
  item.app.id
@@ -6053,17 +6158,35 @@ function PreviewRelatedAppsSection({
6053
6158
  children: inlineItems.map((item) => renderRelatedCard(item))
6054
6159
  }
6055
6160
  ),
6056
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(Modal, { visible: relatedAppsOpen, onRequestClose: closeRelatedApps, children: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_react_native35.View, { style: { gap: theme.spacing.sm }, children: [
6057
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(Text, { style: { color: theme.colors.text, fontSize: 18, fontWeight: theme.typography.fontWeight.semibold }, children: "Related apps" }),
6058
- sectionedRelatedApps.original.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_react_native35.View, { children: [
6059
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(Text, { style: { color: theme.colors.textMuted, marginBottom: theme.spacing.xs }, children: "Original" }),
6060
- sectionedRelatedApps.original.map((item) => renderRelatedCard(item, { fullWidth: true }))
6061
- ] }) : null,
6062
- sectionedRelatedApps.remixes.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_react_native35.View, { children: [
6063
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(Text, { style: { color: theme.colors.textMuted, marginBottom: theme.spacing.xs }, children: "Remixes" }),
6064
- sectionedRelatedApps.remixes.map((item) => renderRelatedCard(item, { fullWidth: true }))
6065
- ] }) : null
6066
- ] }) })
6161
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
6162
+ Modal,
6163
+ {
6164
+ visible: relatedAppsOpen,
6165
+ onRequestClose: closeRelatedApps,
6166
+ contentStyle: { maxHeight: modalMaxHeight, overflow: "hidden" },
6167
+ children: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_react_native35.View, { style: { gap: theme.spacing.sm, minHeight: 0 }, children: [
6168
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(Text, { style: { color: theme.colors.text, fontSize: 18, fontWeight: theme.typography.fontWeight.semibold }, children: "Related apps" }),
6169
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
6170
+ import_react_native35.ScrollView,
6171
+ {
6172
+ style: { maxHeight: modalScrollMaxHeight },
6173
+ contentContainerStyle: { paddingBottom: theme.spacing.xs, gap: theme.spacing.sm },
6174
+ showsVerticalScrollIndicator: true,
6175
+ children: [
6176
+ sectionedRelatedApps.original.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_react_native35.View, { children: [
6177
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(Text, { style: { color: theme.colors.textMuted, marginBottom: theme.spacing.xs }, children: "Original" }),
6178
+ sectionedRelatedApps.original.map((item) => renderRelatedCard(item, { fullWidth: true }))
6179
+ ] }) : null,
6180
+ sectionedRelatedApps.remixes.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_react_native35.View, { children: [
6181
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(Text, { style: { color: theme.colors.textMuted, marginBottom: theme.spacing.xs }, children: "Remixes" }),
6182
+ sectionedRelatedApps.remixes.map((item) => renderRelatedCard(item, { fullWidth: true }))
6183
+ ] }) : null
6184
+ ]
6185
+ }
6186
+ )
6187
+ ] })
6188
+ }
6189
+ )
6067
6190
  ] });
6068
6191
  }
6069
6192
 
@@ -7636,22 +7759,22 @@ ${shareUrl}`;
7636
7759
  }
7637
7760
 
7638
7761
  // src/studio/ui/ChatPanel.tsx
7639
- var React43 = __toESM(require("react"));
7640
- var import_react_native58 = require("react-native");
7762
+ var React44 = __toESM(require("react"));
7763
+ var import_react_native59 = require("react-native");
7641
7764
 
7642
7765
  // src/components/chat/ChatPage.tsx
7643
- var React40 = __toESM(require("react"));
7644
- var import_react_native51 = require("react-native");
7766
+ var React41 = __toESM(require("react"));
7767
+ var import_react_native52 = require("react-native");
7645
7768
  var import_react_native_safe_area_context4 = require("react-native-safe-area-context");
7646
7769
 
7647
7770
  // src/components/chat/ChatMessageList.tsx
7648
- var React39 = __toESM(require("react"));
7649
- var import_react_native50 = require("react-native");
7771
+ var React40 = __toESM(require("react"));
7772
+ var import_react_native51 = require("react-native");
7650
7773
  var import_bottom_sheet5 = require("@gorhom/bottom-sheet");
7651
7774
 
7652
7775
  // src/components/chat/ChatMessageBubble.tsx
7653
- var React37 = __toESM(require("react"));
7654
- var import_react_native48 = require("react-native");
7776
+ var React38 = __toESM(require("react"));
7777
+ var import_react_native49 = require("react-native");
7655
7778
  var import_lucide_react_native10 = require("lucide-react-native");
7656
7779
 
7657
7780
  // src/components/primitives/Button.tsx
@@ -7717,19 +7840,261 @@ function Button({
7717
7840
  );
7718
7841
  }
7719
7842
 
7720
- // src/components/chat/ChatMessageBubble.tsx
7843
+ // src/components/chat/ChatMessageAttachments.tsx
7844
+ var React37 = __toESM(require("react"));
7845
+ var import_react_native48 = require("react-native");
7846
+ var import_expo_image = require("expo-image");
7721
7847
  var import_jsx_runtime49 = require("react/jsx-runtime");
7848
+ function ChatMessageAttachments({
7849
+ messageId,
7850
+ attachments,
7851
+ align = "left",
7852
+ onAttachmentLoadError
7853
+ }) {
7854
+ const theme = useTheme();
7855
+ const [viewerVisible, setViewerVisible] = React37.useState(false);
7856
+ const [viewerIndex, setViewerIndex] = React37.useState(0);
7857
+ const failedKeysRef = React37.useRef(/* @__PURE__ */ new Set());
7858
+ const [loadingById, setLoadingById] = React37.useState({});
7859
+ const [modalLoadingById, setModalLoadingById] = React37.useState({});
7860
+ const pulse = React37.useRef(new import_react_native48.Animated.Value(0.45)).current;
7861
+ const imageAttachments = React37.useMemo(
7862
+ () => attachments.filter(
7863
+ (att) => att.mimeType.startsWith("image/") && typeof att.uri === "string" && att.uri.length > 0
7864
+ ),
7865
+ [attachments]
7866
+ );
7867
+ const itemHeight = imageAttachments.length === 1 ? 180 : 124;
7868
+ const maxItemWidth = imageAttachments.length === 1 ? 280 : 180;
7869
+ const getAspectRatio = (att) => {
7870
+ const width = typeof att.width === "number" ? att.width : 0;
7871
+ const height = typeof att.height === "number" ? att.height : 0;
7872
+ if (width > 0 && height > 0) {
7873
+ return Math.max(0.35, Math.min(2.4, width / height));
7874
+ }
7875
+ return 0.8;
7876
+ };
7877
+ React37.useEffect(() => {
7878
+ import_react_native48.Animated.loop(
7879
+ import_react_native48.Animated.sequence([
7880
+ import_react_native48.Animated.timing(pulse, { toValue: 0.85, duration: 650, useNativeDriver: true }),
7881
+ import_react_native48.Animated.timing(pulse, { toValue: 0.45, duration: 650, useNativeDriver: true })
7882
+ ])
7883
+ ).start();
7884
+ }, [pulse]);
7885
+ React37.useEffect(() => {
7886
+ if (imageAttachments.length === 0) {
7887
+ setLoadingById({});
7888
+ setModalLoadingById({});
7889
+ return;
7890
+ }
7891
+ setLoadingById((prev) => {
7892
+ const next = {};
7893
+ for (const att of imageAttachments) {
7894
+ next[att.id] = prev[att.id] ?? true;
7895
+ }
7896
+ return next;
7897
+ });
7898
+ }, [imageAttachments]);
7899
+ React37.useEffect(() => {
7900
+ if (!viewerVisible) return;
7901
+ if (imageAttachments.length === 0) {
7902
+ setModalLoadingById({});
7903
+ return;
7904
+ }
7905
+ setModalLoadingById(() => {
7906
+ const next = {};
7907
+ for (const att of imageAttachments) {
7908
+ next[att.id] = true;
7909
+ }
7910
+ return next;
7911
+ });
7912
+ }, [viewerVisible, imageAttachments]);
7913
+ if (imageAttachments.length === 0) return null;
7914
+ const handleError = (attachmentId) => {
7915
+ const key = `${messageId}:${attachmentId}`;
7916
+ if (failedKeysRef.current.has(key)) return;
7917
+ failedKeysRef.current.add(key);
7918
+ onAttachmentLoadError == null ? void 0 : onAttachmentLoadError(messageId, attachmentId);
7919
+ };
7920
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(import_jsx_runtime49.Fragment, { children: [
7921
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
7922
+ import_react_native48.View,
7923
+ {
7924
+ style: {
7925
+ flexDirection: "row",
7926
+ flexWrap: "wrap",
7927
+ justifyContent: align === "right" ? "flex-end" : "flex-start",
7928
+ alignSelf: align === "right" ? "flex-end" : "flex-start",
7929
+ gap: theme.spacing.sm,
7930
+ marginBottom: theme.spacing.sm
7931
+ },
7932
+ children: imageAttachments.map((att, index) => /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(
7933
+ import_react_native48.Pressable,
7934
+ {
7935
+ onPress: () => {
7936
+ setViewerIndex(index);
7937
+ setViewerVisible(true);
7938
+ },
7939
+ accessibilityRole: "button",
7940
+ accessibilityLabel: `Attachment ${index + 1} of ${imageAttachments.length}`,
7941
+ style: {
7942
+ height: itemHeight,
7943
+ aspectRatio: getAspectRatio(att),
7944
+ maxWidth: maxItemWidth,
7945
+ borderRadius: theme.radii.lg,
7946
+ overflow: "hidden"
7947
+ },
7948
+ children: [
7949
+ loadingById[att.id] ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
7950
+ import_react_native48.Animated.View,
7951
+ {
7952
+ style: {
7953
+ ...import_react_native48.StyleSheet.absoluteFillObject,
7954
+ opacity: pulse,
7955
+ backgroundColor: theme.colors.border
7956
+ }
7957
+ }
7958
+ ) : null,
7959
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
7960
+ import_expo_image.Image,
7961
+ {
7962
+ source: { uri: att.uri },
7963
+ style: { width: "100%", height: "100%" },
7964
+ contentFit: "contain",
7965
+ transition: 140,
7966
+ cachePolicy: "memory-disk",
7967
+ onLoadStart: () => {
7968
+ setLoadingById((prev) => ({ ...prev, [att.id]: true }));
7969
+ },
7970
+ onLoadEnd: () => {
7971
+ setLoadingById((prev) => ({ ...prev, [att.id]: false }));
7972
+ },
7973
+ onError: () => handleError(att.id)
7974
+ }
7975
+ )
7976
+ ]
7977
+ },
7978
+ att.id
7979
+ ))
7980
+ }
7981
+ ),
7982
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_react_native48.Modal, { visible: viewerVisible, transparent: true, animationType: "fade", onRequestClose: () => setViewerVisible(false), children: /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(import_react_native48.View, { style: { flex: 1, backgroundColor: "rgba(0,0,0,0.95)" }, children: [
7983
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
7984
+ import_react_native48.View,
7985
+ {
7986
+ style: {
7987
+ position: "absolute",
7988
+ top: 56,
7989
+ right: 16,
7990
+ zIndex: 2
7991
+ },
7992
+ children: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
7993
+ import_react_native48.Pressable,
7994
+ {
7995
+ accessibilityRole: "button",
7996
+ accessibilityLabel: "Close attachment viewer",
7997
+ onPress: () => setViewerVisible(false),
7998
+ style: {
7999
+ width: 44,
8000
+ height: 44,
8001
+ borderRadius: 22,
8002
+ alignItems: "center",
8003
+ justifyContent: "center",
8004
+ backgroundColor: "rgba(255,255,255,0.15)"
8005
+ },
8006
+ children: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(IconClose, { size: 18, colorToken: "onPrimary" })
8007
+ }
8008
+ )
8009
+ }
8010
+ ),
8011
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
8012
+ import_react_native48.FlatList,
8013
+ {
8014
+ data: imageAttachments,
8015
+ horizontal: true,
8016
+ pagingEnabled: true,
8017
+ initialScrollIndex: viewerIndex,
8018
+ keyExtractor: (item) => item.id,
8019
+ showsHorizontalScrollIndicator: false,
8020
+ getItemLayout: (_, index) => {
8021
+ const width = import_react_native48.Dimensions.get("window").width;
8022
+ return { length: width, offset: width * index, index };
8023
+ },
8024
+ renderItem: ({ item, index }) => /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(import_react_native48.View, { style: { width: import_react_native48.Dimensions.get("window").width, height: "100%", justifyContent: "center" }, children: [
8025
+ modalLoadingById[item.id] ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
8026
+ import_react_native48.Animated.View,
8027
+ {
8028
+ style: {
8029
+ ...import_react_native48.StyleSheet.absoluteFillObject,
8030
+ opacity: pulse,
8031
+ backgroundColor: theme.colors.border
8032
+ }
8033
+ }
8034
+ ) : null,
8035
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
8036
+ import_expo_image.Image,
8037
+ {
8038
+ source: { uri: item.uri },
8039
+ style: { width: "100%", height: "78%" },
8040
+ contentFit: "contain",
8041
+ transition: 140,
8042
+ cachePolicy: "memory-disk",
8043
+ onLoadStart: () => {
8044
+ setModalLoadingById((prev) => ({ ...prev, [item.id]: true }));
8045
+ },
8046
+ onLoadEnd: () => {
8047
+ setModalLoadingById((prev) => ({ ...prev, [item.id]: false }));
8048
+ },
8049
+ onError: () => handleError(item.id)
8050
+ }
8051
+ ),
8052
+ /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(
8053
+ Text,
8054
+ {
8055
+ variant: "caption",
8056
+ color: "#FFFFFF",
8057
+ style: { textAlign: "center", marginTop: theme.spacing.sm },
8058
+ children: [
8059
+ index + 1,
8060
+ " / ",
8061
+ imageAttachments.length
8062
+ ]
8063
+ }
8064
+ )
8065
+ ] })
8066
+ }
8067
+ )
8068
+ ] }) })
8069
+ ] });
8070
+ }
8071
+
8072
+ // src/components/chat/ChatMessageBubble.tsx
8073
+ var import_jsx_runtime50 = require("react/jsx-runtime");
7722
8074
  function areMessageMetaEqual(a, b) {
7723
8075
  if (a === b) return true;
7724
8076
  if (!a || !b) return a === b;
7725
8077
  return a.kind === b.kind && a.event === b.event && a.status === b.status && a.mergeRequestId === b.mergeRequestId && a.sourceAppId === b.sourceAppId && a.targetAppId === b.targetAppId && a.appId === b.appId && a.threadId === b.threadId;
7726
8078
  }
8079
+ function areMessageAttachmentsEqual(a, b) {
8080
+ if (a === b) return true;
8081
+ const left = a ?? [];
8082
+ const right = b ?? [];
8083
+ if (left.length !== right.length) return false;
8084
+ for (let i = 0; i < left.length; i += 1) {
8085
+ if (left[i].id !== right[i].id || left[i].name !== right[i].name || left[i].mimeType !== right[i].mimeType || left[i].size !== right[i].size || left[i].uri !== right[i].uri || left[i].width !== right[i].width || left[i].height !== right[i].height) {
8086
+ return false;
8087
+ }
8088
+ }
8089
+ return true;
8090
+ }
7727
8091
  function ChatMessageBubbleInner({
7728
8092
  message,
7729
8093
  renderContent,
7730
8094
  isLast,
7731
8095
  retrying,
7732
8096
  onRetryMessage,
8097
+ onAttachmentLoadError,
7733
8098
  style
7734
8099
  }) {
7735
8100
  var _a, _b;
@@ -7748,11 +8113,36 @@ function ChatMessageBubbleInner({
7748
8113
  const bodyColor = metaStatus === "success" ? theme.colors.success : metaStatus === "error" ? theme.colors.danger : void 0;
7749
8114
  const showRetry = Boolean(onRetryMessage) && isLast && metaStatus === "error" && message.author === "human";
7750
8115
  const retryLabel = retrying ? "Retrying..." : "Retry";
7751
- const handleRetryPress = React37.useCallback(() => {
8116
+ const hasText = message.content.trim().length > 0;
8117
+ const attachments = message.attachments ?? [];
8118
+ const hasAttachments = attachments.length > 0;
8119
+ const hasStatusIcon = isMergeCompleted || isSyncCompleted || isMergeApproved || isSyncStarted;
8120
+ const shouldRenderBubble = hasText || hasStatusIcon;
8121
+ const handleRetryPress = React38.useCallback(() => {
7752
8122
  onRetryMessage == null ? void 0 : onRetryMessage(message.id);
7753
8123
  }, [message.id, onRetryMessage]);
7754
- return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(import_react_native48.View, { style: [align, style], children: [
7755
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
8124
+ return /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(import_react_native49.View, { style: [align, style], children: [
8125
+ hasAttachments ? /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
8126
+ import_react_native49.View,
8127
+ {
8128
+ style: {
8129
+ maxWidth: "85%",
8130
+ marginBottom: shouldRenderBubble ? theme.spacing.xs : 0,
8131
+ alignSelf: isHuman ? "flex-end" : "flex-start",
8132
+ alignItems: isHuman ? "flex-end" : "flex-start"
8133
+ },
8134
+ children: /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
8135
+ ChatMessageAttachments,
8136
+ {
8137
+ messageId: message.id,
8138
+ attachments,
8139
+ align: isHuman ? "right" : "left",
8140
+ onAttachmentLoadError
8141
+ }
8142
+ )
8143
+ }
8144
+ ) : null,
8145
+ shouldRenderBubble ? /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
7756
8146
  Surface,
7757
8147
  {
7758
8148
  variant: bubbleVariant,
@@ -7767,14 +8157,14 @@ function ChatMessageBubbleInner({
7767
8157
  },
7768
8158
  cornerStyle
7769
8159
  ],
7770
- children: /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(import_react_native48.View, { style: { flexDirection: "row", alignItems: "center" }, children: [
7771
- isMergeCompleted || isSyncCompleted ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_lucide_react_native10.CheckCheck, { size: 16, color: theme.colors.success, style: { marginRight: theme.spacing.sm } }) : null,
7772
- isMergeApproved || isSyncStarted ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_lucide_react_native10.GitMerge, { size: 16, color: theme.colors.text, style: { marginRight: theme.spacing.sm } }) : null,
7773
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_react_native48.View, { style: { flexShrink: 1, minWidth: 0 }, children: renderContent ? renderContent(message) : /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(MarkdownText, { markdown: message.content, variant: "chat", bodyColor }) })
8160
+ children: /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(import_react_native49.View, { style: { flexDirection: "row", alignItems: "center" }, children: [
8161
+ isMergeCompleted || isSyncCompleted ? /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_lucide_react_native10.CheckCheck, { size: 16, color: theme.colors.success, style: { marginRight: theme.spacing.sm } }) : null,
8162
+ isMergeApproved || isSyncStarted ? /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_lucide_react_native10.GitMerge, { size: 16, color: theme.colors.text, style: { marginRight: theme.spacing.sm } }) : null,
8163
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_react_native49.View, { style: { flexShrink: 1, minWidth: 0 }, children: renderContent ? renderContent(message) : /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(MarkdownText, { markdown: message.content, variant: "chat", bodyColor }) })
7774
8164
  ] })
7775
8165
  }
7776
- ),
7777
- showRetry ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_react_native48.View, { style: { marginTop: theme.spacing.xs, alignSelf: align.alignSelf }, children: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
8166
+ ) : null,
8167
+ showRetry ? /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_react_native49.View, { style: { marginTop: theme.spacing.xs, alignSelf: align.alignSelf }, children: /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
7778
8168
  Button,
7779
8169
  {
7780
8170
  variant: "ghost",
@@ -7783,9 +8173,9 @@ function ChatMessageBubbleInner({
7783
8173
  disabled: retrying,
7784
8174
  style: { borderColor: theme.colors.danger },
7785
8175
  accessibilityLabel: "Retry send",
7786
- children: /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(import_react_native48.View, { style: { flexDirection: "row", alignItems: "center" }, children: [
7787
- !retrying ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_lucide_react_native10.RotateCcw, { size: 14, color: theme.colors.danger }) : null,
7788
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
8176
+ children: /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(import_react_native49.View, { style: { flexDirection: "row", alignItems: "center" }, children: [
8177
+ !retrying ? /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_lucide_react_native10.RotateCcw, { size: 14, color: theme.colors.danger }) : null,
8178
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
7789
8179
  Text,
7790
8180
  {
7791
8181
  variant: "caption",
@@ -7801,30 +8191,30 @@ function ChatMessageBubbleInner({
7801
8191
  ] });
7802
8192
  }
7803
8193
  function areEqual(prev, next) {
7804
- return prev.message.id === next.message.id && prev.message.author === next.message.author && prev.message.content === next.message.content && prev.message.kind === next.message.kind && String(prev.message.createdAt) === String(next.message.createdAt) && areMessageMetaEqual(prev.message.meta, next.message.meta) && prev.renderContent === next.renderContent && prev.isLast === next.isLast && prev.retrying === next.retrying && prev.onRetryMessage === next.onRetryMessage && prev.style === next.style;
8194
+ return prev.message.id === next.message.id && prev.message.author === next.message.author && prev.message.content === next.message.content && areMessageAttachmentsEqual(prev.message.attachments, next.message.attachments) && prev.message.kind === next.message.kind && String(prev.message.createdAt) === String(next.message.createdAt) && areMessageMetaEqual(prev.message.meta, next.message.meta) && prev.renderContent === next.renderContent && prev.isLast === next.isLast && prev.retrying === next.retrying && prev.onRetryMessage === next.onRetryMessage && prev.onAttachmentLoadError === next.onAttachmentLoadError && prev.style === next.style;
7805
8195
  }
7806
- var ChatMessageBubble = React37.memo(ChatMessageBubbleInner, areEqual);
8196
+ var ChatMessageBubble = React38.memo(ChatMessageBubbleInner, areEqual);
7807
8197
  ChatMessageBubble.displayName = "ChatMessageBubble";
7808
8198
 
7809
8199
  // src/components/chat/TypingIndicator.tsx
7810
- var React38 = __toESM(require("react"));
7811
- var import_react_native49 = require("react-native");
7812
- var import_jsx_runtime50 = require("react/jsx-runtime");
8200
+ var React39 = __toESM(require("react"));
8201
+ var import_react_native50 = require("react-native");
8202
+ var import_jsx_runtime51 = require("react/jsx-runtime");
7813
8203
  function TypingIndicator({ style }) {
7814
8204
  const theme = useTheme();
7815
8205
  const dotColor = theme.colors.textSubtle;
7816
- const anims = React38.useMemo(
7817
- () => [new import_react_native49.Animated.Value(0.3), new import_react_native49.Animated.Value(0.3), new import_react_native49.Animated.Value(0.3)],
8206
+ const anims = React39.useMemo(
8207
+ () => [new import_react_native50.Animated.Value(0.3), new import_react_native50.Animated.Value(0.3), new import_react_native50.Animated.Value(0.3)],
7818
8208
  []
7819
8209
  );
7820
- React38.useEffect(() => {
8210
+ React39.useEffect(() => {
7821
8211
  const loops = [];
7822
8212
  anims.forEach((a, idx) => {
7823
- const seq = import_react_native49.Animated.sequence([
7824
- import_react_native49.Animated.timing(a, { toValue: 1, duration: 420, useNativeDriver: true, delay: idx * 140 }),
7825
- import_react_native49.Animated.timing(a, { toValue: 0.3, duration: 420, useNativeDriver: true })
8213
+ const seq = import_react_native50.Animated.sequence([
8214
+ import_react_native50.Animated.timing(a, { toValue: 1, duration: 420, useNativeDriver: true, delay: idx * 140 }),
8215
+ import_react_native50.Animated.timing(a, { toValue: 0.3, duration: 420, useNativeDriver: true })
7826
8216
  ]);
7827
- const loop = import_react_native49.Animated.loop(seq);
8217
+ const loop = import_react_native50.Animated.loop(seq);
7828
8218
  loops.push(loop);
7829
8219
  loop.start();
7830
8220
  });
@@ -7832,8 +8222,8 @@ function TypingIndicator({ style }) {
7832
8222
  loops.forEach((l) => l.stop());
7833
8223
  };
7834
8224
  }, [anims]);
7835
- return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_react_native49.View, { style: [{ flexDirection: "row", alignItems: "center" }, style], children: anims.map((a, i) => /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
7836
- import_react_native49.Animated.View,
8225
+ return /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(import_react_native50.View, { style: [{ flexDirection: "row", alignItems: "center" }, style], children: anims.map((a, i) => /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
8226
+ import_react_native50.Animated.View,
7837
8227
  {
7838
8228
  style: {
7839
8229
  width: 8,
@@ -7842,7 +8232,7 @@ function TypingIndicator({ style }) {
7842
8232
  marginHorizontal: 3,
7843
8233
  backgroundColor: dotColor,
7844
8234
  opacity: a,
7845
- transform: [{ translateY: import_react_native49.Animated.multiply(import_react_native49.Animated.subtract(a, 0.3), 2) }]
8235
+ transform: [{ translateY: import_react_native50.Animated.multiply(import_react_native50.Animated.subtract(a, 0.3), 2) }]
7846
8236
  }
7847
8237
  },
7848
8238
  i
@@ -7850,36 +8240,37 @@ function TypingIndicator({ style }) {
7850
8240
  }
7851
8241
 
7852
8242
  // src/components/chat/ChatMessageList.tsx
7853
- var import_jsx_runtime51 = require("react/jsx-runtime");
7854
- var ChatMessageList = React39.forwardRef(
8243
+ var import_jsx_runtime52 = require("react/jsx-runtime");
8244
+ var ChatMessageList = React40.forwardRef(
7855
8245
  ({
7856
8246
  messages,
7857
8247
  showTypingIndicator = false,
7858
8248
  renderMessageContent,
7859
8249
  onRetryMessage,
7860
8250
  isRetryingMessage,
8251
+ onAttachmentLoadError,
7861
8252
  contentStyle,
7862
8253
  bottomInset = 0,
7863
8254
  onNearBottomChange,
7864
8255
  nearBottomThreshold = 200
7865
8256
  }, ref) => {
7866
8257
  const theme = useTheme();
7867
- const listRef = React39.useRef(null);
7868
- const nearBottomRef = React39.useRef(true);
7869
- const initialScrollDoneRef = React39.useRef(false);
7870
- const lastMessageIdRef = React39.useRef(null);
7871
- const data = React39.useMemo(() => {
8258
+ const listRef = React40.useRef(null);
8259
+ const nearBottomRef = React40.useRef(true);
8260
+ const initialScrollDoneRef = React40.useRef(false);
8261
+ const lastMessageIdRef = React40.useRef(null);
8262
+ const data = React40.useMemo(() => {
7872
8263
  return [...messages].reverse();
7873
8264
  }, [messages]);
7874
8265
  const lastMessageId = messages.length > 0 ? messages[messages.length - 1].id : null;
7875
- const keyExtractor = React39.useCallback((m) => m.id, []);
7876
- const scrollToBottom = React39.useCallback((options) => {
8266
+ const keyExtractor = React40.useCallback((m) => m.id, []);
8267
+ const scrollToBottom = React40.useCallback((options) => {
7877
8268
  var _a;
7878
8269
  const animated = (options == null ? void 0 : options.animated) ?? true;
7879
8270
  (_a = listRef.current) == null ? void 0 : _a.scrollToOffset({ offset: 0, animated });
7880
8271
  }, []);
7881
- React39.useImperativeHandle(ref, () => ({ scrollToBottom }), [scrollToBottom]);
7882
- const handleScroll = React39.useCallback(
8272
+ React40.useImperativeHandle(ref, () => ({ scrollToBottom }), [scrollToBottom]);
8273
+ const handleScroll = React40.useCallback(
7883
8274
  (e) => {
7884
8275
  const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;
7885
8276
  const distanceFromBottom = Math.max(contentOffset.y - Math.max(bottomInset, 0), 0);
@@ -7891,7 +8282,7 @@ var ChatMessageList = React39.forwardRef(
7891
8282
  },
7892
8283
  [bottomInset, nearBottomThreshold, onNearBottomChange]
7893
8284
  );
7894
- React39.useEffect(() => {
8285
+ React40.useEffect(() => {
7895
8286
  if (!initialScrollDoneRef.current) return;
7896
8287
  const lastId = messages.length > 0 ? messages[messages.length - 1].id : null;
7897
8288
  const prevLastId = lastMessageIdRef.current;
@@ -7901,14 +8292,14 @@ var ChatMessageList = React39.forwardRef(
7901
8292
  const id = requestAnimationFrame(() => scrollToBottom({ animated: true }));
7902
8293
  return () => cancelAnimationFrame(id);
7903
8294
  }, [messages, scrollToBottom]);
7904
- React39.useEffect(() => {
8295
+ React40.useEffect(() => {
7905
8296
  if (showTypingIndicator && nearBottomRef.current) {
7906
8297
  const id = requestAnimationFrame(() => scrollToBottom({ animated: true }));
7907
8298
  return () => cancelAnimationFrame(id);
7908
8299
  }
7909
8300
  return void 0;
7910
8301
  }, [showTypingIndicator, scrollToBottom]);
7911
- const handleContentSizeChange = React39.useCallback(() => {
8302
+ const handleContentSizeChange = React40.useCallback(() => {
7912
8303
  if (initialScrollDoneRef.current) return;
7913
8304
  initialScrollDoneRef.current = true;
7914
8305
  lastMessageIdRef.current = messages.length > 0 ? messages[messages.length - 1].id : null;
@@ -7916,7 +8307,7 @@ var ChatMessageList = React39.forwardRef(
7916
8307
  onNearBottomChange == null ? void 0 : onNearBottomChange(true);
7917
8308
  requestAnimationFrame(() => scrollToBottom({ animated: false }));
7918
8309
  }, [messages, onNearBottomChange, scrollToBottom]);
7919
- const contentContainerStyle = React39.useMemo(
8310
+ const contentContainerStyle = React40.useMemo(
7920
8311
  () => [
7921
8312
  {
7922
8313
  paddingHorizontal: theme.spacing.lg,
@@ -7926,28 +8317,29 @@ var ChatMessageList = React39.forwardRef(
7926
8317
  ],
7927
8318
  [contentStyle, theme.spacing.lg, theme.spacing.sm]
7928
8319
  );
7929
- const renderSeparator = React39.useCallback(() => /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(import_react_native50.View, { style: { height: theme.spacing.sm } }), [theme.spacing.sm]);
7930
- const listHeader = React39.useMemo(
7931
- () => /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)(import_react_native50.View, { children: [
7932
- showTypingIndicator ? /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(import_react_native50.View, { style: { marginTop: theme.spacing.sm, alignSelf: "flex-start", paddingHorizontal: theme.spacing.lg }, children: /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(TypingIndicator, {}) }) : null,
7933
- bottomInset > 0 ? /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(import_react_native50.View, { style: { height: bottomInset } }) : null
8320
+ const renderSeparator = React40.useCallback(() => /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(import_react_native51.View, { style: { height: theme.spacing.sm } }), [theme.spacing.sm]);
8321
+ const listHeader = React40.useMemo(
8322
+ () => /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)(import_react_native51.View, { children: [
8323
+ showTypingIndicator ? /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(import_react_native51.View, { style: { marginTop: theme.spacing.sm, alignSelf: "flex-start", paddingHorizontal: theme.spacing.lg }, children: /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(TypingIndicator, {}) }) : null,
8324
+ bottomInset > 0 ? /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(import_react_native51.View, { style: { height: bottomInset } }) : null
7934
8325
  ] }),
7935
8326
  [bottomInset, showTypingIndicator, theme.spacing.lg, theme.spacing.sm]
7936
8327
  );
7937
- const renderItem = React39.useCallback(
7938
- ({ item }) => /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
8328
+ const renderItem = React40.useCallback(
8329
+ ({ item }) => /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
7939
8330
  ChatMessageBubble,
7940
8331
  {
7941
8332
  message: item,
7942
8333
  renderContent: renderMessageContent,
7943
8334
  isLast: Boolean(lastMessageId && item.id === lastMessageId),
7944
8335
  retrying: (isRetryingMessage == null ? void 0 : isRetryingMessage(item.id)) ?? false,
7945
- onRetryMessage
8336
+ onRetryMessage,
8337
+ onAttachmentLoadError
7946
8338
  }
7947
8339
  ),
7948
- [isRetryingMessage, lastMessageId, onRetryMessage, renderMessageContent]
8340
+ [isRetryingMessage, lastMessageId, onAttachmentLoadError, onRetryMessage, renderMessageContent]
7949
8341
  );
7950
- return /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
8342
+ return /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
7951
8343
  import_bottom_sheet5.BottomSheetFlatList,
7952
8344
  {
7953
8345
  ref: listRef,
@@ -7970,7 +8362,7 @@ var ChatMessageList = React39.forwardRef(
7970
8362
  ChatMessageList.displayName = "ChatMessageList";
7971
8363
 
7972
8364
  // src/components/chat/ChatPage.tsx
7973
- var import_jsx_runtime52 = require("react/jsx-runtime");
8365
+ var import_jsx_runtime53 = require("react/jsx-runtime");
7974
8366
  function ChatPage({
7975
8367
  header,
7976
8368
  messages,
@@ -7978,6 +8370,7 @@ function ChatPage({
7978
8370
  renderMessageContent,
7979
8371
  onRetryMessage,
7980
8372
  isRetryingMessage,
8373
+ onAttachmentLoadError,
7981
8374
  topBanner,
7982
8375
  composerTop,
7983
8376
  composer,
@@ -7989,35 +8382,35 @@ function ChatPage({
7989
8382
  }) {
7990
8383
  const theme = useTheme();
7991
8384
  const insets = (0, import_react_native_safe_area_context4.useSafeAreaInsets)();
7992
- const [composerHeight, setComposerHeight] = React40.useState(0);
7993
- const [composerTopHeight, setComposerTopHeight] = React40.useState(0);
7994
- const footerBottomPadding = import_react_native51.Platform.OS === "ios" ? insets.bottom - 24 : insets.bottom + 10;
8385
+ const [composerHeight, setComposerHeight] = React41.useState(0);
8386
+ const [composerTopHeight, setComposerTopHeight] = React41.useState(0);
8387
+ const footerBottomPadding = import_react_native52.Platform.OS === "ios" ? insets.bottom - 24 : insets.bottom + 10;
7995
8388
  const totalComposerHeight = composerHeight + composerTopHeight;
7996
8389
  const overlayBottom = totalComposerHeight + footerBottomPadding + theme.spacing.lg;
7997
8390
  const bottomInset = totalComposerHeight + footerBottomPadding + theme.spacing.xl;
7998
- const resolvedOverlay = React40.useMemo(() => {
8391
+ const resolvedOverlay = React41.useMemo(() => {
7999
8392
  var _a;
8000
8393
  if (!overlay) return null;
8001
- if (!React40.isValidElement(overlay)) return overlay;
8394
+ if (!React41.isValidElement(overlay)) return overlay;
8002
8395
  const prevStyle = (_a = overlay.props) == null ? void 0 : _a.style;
8003
- return React40.cloneElement(overlay, {
8396
+ return React41.cloneElement(overlay, {
8004
8397
  style: [prevStyle, { bottom: overlayBottom }]
8005
8398
  });
8006
8399
  }, [overlay, overlayBottom]);
8007
- React40.useEffect(() => {
8400
+ React41.useEffect(() => {
8008
8401
  if (composerTop) return;
8009
8402
  setComposerTopHeight(0);
8010
8403
  }, [composerTop]);
8011
- return /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)(import_react_native51.View, { style: [{ flex: 1 }, style], children: [
8012
- header ? /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(import_react_native51.View, { children: header }) : null,
8013
- topBanner ? /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(import_react_native51.View, { style: { paddingHorizontal: theme.spacing.lg, paddingTop: theme.spacing.sm }, children: topBanner }) : null,
8014
- /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)(import_react_native51.View, { style: { flex: 1 }, children: [
8015
- /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)(
8016
- import_react_native51.View,
8404
+ return /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)(import_react_native52.View, { style: [{ flex: 1 }, style], children: [
8405
+ header ? /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_react_native52.View, { children: header }) : null,
8406
+ topBanner ? /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_react_native52.View, { style: { paddingHorizontal: theme.spacing.lg, paddingTop: theme.spacing.sm }, children: topBanner }) : null,
8407
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)(import_react_native52.View, { style: { flex: 1 }, children: [
8408
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)(
8409
+ import_react_native52.View,
8017
8410
  {
8018
8411
  style: { flex: 1 },
8019
8412
  children: [
8020
- /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
8413
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
8021
8414
  ChatMessageList,
8022
8415
  {
8023
8416
  ref: listRef,
@@ -8026,6 +8419,7 @@ function ChatPage({
8026
8419
  renderMessageContent,
8027
8420
  onRetryMessage,
8028
8421
  isRetryingMessage,
8422
+ onAttachmentLoadError,
8029
8423
  onNearBottomChange,
8030
8424
  bottomInset
8031
8425
  }
@@ -8034,8 +8428,8 @@ function ChatPage({
8034
8428
  ]
8035
8429
  }
8036
8430
  ),
8037
- /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)(
8038
- import_react_native51.View,
8431
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)(
8432
+ import_react_native52.View,
8039
8433
  {
8040
8434
  style: {
8041
8435
  position: "absolute",
@@ -8047,15 +8441,15 @@ function ChatPage({
8047
8441
  paddingBottom: footerBottomPadding
8048
8442
  },
8049
8443
  children: [
8050
- composerTop ? /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
8051
- import_react_native51.View,
8444
+ composerTop ? /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
8445
+ import_react_native52.View,
8052
8446
  {
8053
8447
  style: { marginBottom: theme.spacing.sm },
8054
8448
  onLayout: (e) => setComposerTopHeight(e.nativeEvent.layout.height),
8055
8449
  children: composerTop
8056
8450
  }
8057
8451
  ) : null,
8058
- /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
8452
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
8059
8453
  ChatComposer,
8060
8454
  {
8061
8455
  ...composer,
@@ -8071,15 +8465,15 @@ function ChatPage({
8071
8465
  }
8072
8466
 
8073
8467
  // src/components/chat/ScrollToBottomButton.tsx
8074
- var React41 = __toESM(require("react"));
8075
- var import_react_native52 = require("react-native");
8468
+ var React42 = __toESM(require("react"));
8469
+ var import_react_native53 = require("react-native");
8076
8470
  var import_react_native_reanimated2 = __toESM(require("react-native-reanimated"));
8077
- var import_jsx_runtime53 = require("react/jsx-runtime");
8471
+ var import_jsx_runtime54 = require("react/jsx-runtime");
8078
8472
  function ScrollToBottomButton({ visible, onPress, children, style }) {
8079
8473
  const theme = useTheme();
8080
8474
  const progress = (0, import_react_native_reanimated2.useSharedValue)(visible ? 1 : 0);
8081
- const [pressed, setPressed] = React41.useState(false);
8082
- React41.useEffect(() => {
8475
+ const [pressed, setPressed] = React42.useState(false);
8476
+ React42.useEffect(() => {
8083
8477
  progress.value = (0, import_react_native_reanimated2.withTiming)(visible ? 1 : 0, { duration: 200, easing: import_react_native_reanimated2.Easing.out(import_react_native_reanimated2.Easing.ease) });
8084
8478
  }, [progress, visible]);
8085
8479
  const animStyle = (0, import_react_native_reanimated2.useAnimatedStyle)(() => ({
@@ -8088,7 +8482,7 @@ function ScrollToBottomButton({ visible, onPress, children, style }) {
8088
8482
  }));
8089
8483
  const bg = theme.scheme === "dark" ? "rgba(39,39,42,0.9)" : "rgba(244,244,245,0.95)";
8090
8484
  const border = theme.scheme === "dark" ? withAlpha("#FFFFFF", 0.12) : withAlpha("#000000", 0.08);
8091
- return /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
8485
+ return /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
8092
8486
  import_react_native_reanimated2.default.View,
8093
8487
  {
8094
8488
  pointerEvents: visible ? "auto" : "none",
@@ -8102,8 +8496,8 @@ function ScrollToBottomButton({ visible, onPress, children, style }) {
8102
8496
  style,
8103
8497
  animStyle
8104
8498
  ],
8105
- children: /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
8106
- import_react_native52.View,
8499
+ children: /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
8500
+ import_react_native53.View,
8107
8501
  {
8108
8502
  style: {
8109
8503
  width: 44,
@@ -8121,8 +8515,8 @@ function ScrollToBottomButton({ visible, onPress, children, style }) {
8121
8515
  elevation: 5,
8122
8516
  opacity: pressed ? 0.85 : 1
8123
8517
  },
8124
- children: /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
8125
- import_react_native52.Pressable,
8518
+ children: /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
8519
+ import_react_native53.Pressable,
8126
8520
  {
8127
8521
  onPress,
8128
8522
  onPressIn: () => setPressed(true),
@@ -8139,16 +8533,16 @@ function ScrollToBottomButton({ visible, onPress, children, style }) {
8139
8533
  }
8140
8534
 
8141
8535
  // src/components/chat/ChatHeader.tsx
8142
- var import_react_native53 = require("react-native");
8143
- var import_jsx_runtime54 = require("react/jsx-runtime");
8536
+ var import_react_native54 = require("react-native");
8537
+ var import_jsx_runtime55 = require("react/jsx-runtime");
8144
8538
  function ChatHeader({ left, right, center, style }) {
8145
- const flattenedStyle = import_react_native53.StyleSheet.flatten([
8539
+ const flattenedStyle = import_react_native54.StyleSheet.flatten([
8146
8540
  {
8147
8541
  paddingTop: 0
8148
8542
  },
8149
8543
  style
8150
8544
  ]);
8151
- return /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
8545
+ return /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
8152
8546
  StudioSheetHeader,
8153
8547
  {
8154
8548
  left,
@@ -8160,13 +8554,13 @@ function ChatHeader({ left, right, center, style }) {
8160
8554
  }
8161
8555
 
8162
8556
  // src/components/chat/ForkNoticeBanner.tsx
8163
- var import_react_native54 = require("react-native");
8164
- var import_jsx_runtime55 = require("react/jsx-runtime");
8557
+ var import_react_native55 = require("react-native");
8558
+ var import_jsx_runtime56 = require("react/jsx-runtime");
8165
8559
  function ForkNoticeBanner({ isOwner = true, title, description, style }) {
8166
8560
  const theme = useTheme();
8167
8561
  const resolvedTitle = title ?? (isOwner ? "Remixed app" : "Remix app");
8168
8562
  const resolvedDescription = description ?? (isOwner ? "Any changes you make will be a remix of the original app. You can view the edited version in the Remix tab in your apps page." : "Once you make edits, this remixed version will appear on your Remixed apps page.");
8169
- return /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
8563
+ return /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
8170
8564
  Card,
8171
8565
  {
8172
8566
  variant: "surfaceRaised",
@@ -8181,8 +8575,8 @@ function ForkNoticeBanner({ isOwner = true, title, description, style }) {
8181
8575
  },
8182
8576
  style
8183
8577
  ],
8184
- children: /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)(import_react_native54.View, { style: { minWidth: 0 }, children: [
8185
- /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
8578
+ children: /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)(import_react_native55.View, { style: { minWidth: 0 }, children: [
8579
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
8186
8580
  Text,
8187
8581
  {
8188
8582
  style: {
@@ -8196,7 +8590,7 @@ function ForkNoticeBanner({ isOwner = true, title, description, style }) {
8196
8590
  children: resolvedTitle
8197
8591
  }
8198
8592
  ),
8199
- /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
8593
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
8200
8594
  Text,
8201
8595
  {
8202
8596
  style: {
@@ -8214,16 +8608,16 @@ function ForkNoticeBanner({ isOwner = true, title, description, style }) {
8214
8608
  }
8215
8609
 
8216
8610
  // src/components/chat/ChatQueue.tsx
8217
- var React42 = __toESM(require("react"));
8218
- var import_react_native55 = require("react-native");
8219
- var import_jsx_runtime56 = require("react/jsx-runtime");
8611
+ var React43 = __toESM(require("react"));
8612
+ var import_react_native56 = require("react-native");
8613
+ var import_jsx_runtime57 = require("react/jsx-runtime");
8220
8614
  function ChatQueue({ items, onRemove }) {
8221
8615
  const theme = useTheme();
8222
- const [expanded, setExpanded] = React42.useState({});
8223
- const [canExpand, setCanExpand] = React42.useState({});
8224
- const [collapsedText, setCollapsedText] = React42.useState({});
8225
- const [removing, setRemoving] = React42.useState({});
8226
- const buildCollapsedText = React42.useCallback((lines) => {
8616
+ const [expanded, setExpanded] = React43.useState({});
8617
+ const [canExpand, setCanExpand] = React43.useState({});
8618
+ const [collapsedText, setCollapsedText] = React43.useState({});
8619
+ const [removing, setRemoving] = React43.useState({});
8620
+ const buildCollapsedText = React43.useCallback((lines) => {
8227
8621
  var _a, _b;
8228
8622
  const line1 = ((_a = lines[0]) == null ? void 0 : _a.text) ?? "";
8229
8623
  const line2 = ((_b = lines[1]) == null ? void 0 : _b.text) ?? "";
@@ -8239,7 +8633,7 @@ function ChatQueue({ items, onRemove }) {
8239
8633
  return `${line1}
8240
8634
  ${trimmedLine2}\u2026 `;
8241
8635
  }, []);
8242
- React42.useEffect(() => {
8636
+ React43.useEffect(() => {
8243
8637
  if (items.length === 0) return;
8244
8638
  const ids = new Set(items.map((item) => item.id));
8245
8639
  setExpanded((prev) => Object.fromEntries(Object.entries(prev).filter(([id]) => ids.has(id))));
@@ -8248,8 +8642,8 @@ ${trimmedLine2}\u2026 `;
8248
8642
  setRemoving((prev) => Object.fromEntries(Object.entries(prev).filter(([id]) => ids.has(id))));
8249
8643
  }, [items]);
8250
8644
  if (items.length === 0) return null;
8251
- return /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)(
8252
- import_react_native55.View,
8645
+ return /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)(
8646
+ import_react_native56.View,
8253
8647
  {
8254
8648
  style: {
8255
8649
  borderWidth: 1,
@@ -8260,16 +8654,16 @@ ${trimmedLine2}\u2026 `;
8260
8654
  backgroundColor: "transparent"
8261
8655
  },
8262
8656
  children: [
8263
- /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(Text, { variant: "caption", style: { marginBottom: theme.spacing.sm }, children: "Queue" }),
8264
- /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(import_react_native55.View, { style: { gap: theme.spacing.sm }, children: items.map((item) => {
8657
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(Text, { variant: "caption", style: { marginBottom: theme.spacing.sm }, children: "Queue" }),
8658
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(import_react_native56.View, { style: { gap: theme.spacing.sm }, children: items.map((item) => {
8265
8659
  const isExpanded = Boolean(expanded[item.id]);
8266
8660
  const showToggle = Boolean(canExpand[item.id]);
8267
8661
  const prompt = item.prompt ?? "";
8268
8662
  const moreLabel = "more";
8269
8663
  const displayPrompt = !isExpanded && showToggle && collapsedText[item.id] ? collapsedText[item.id] : prompt;
8270
8664
  const isRemoving = Boolean(removing[item.id]);
8271
- return /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)(
8272
- import_react_native55.View,
8665
+ return /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)(
8666
+ import_react_native56.View,
8273
8667
  {
8274
8668
  style: {
8275
8669
  flexDirection: "row",
@@ -8281,8 +8675,8 @@ ${trimmedLine2}\u2026 `;
8281
8675
  backgroundColor: withAlpha(theme.colors.surface, theme.scheme === "dark" ? 0.8 : 0.9)
8282
8676
  },
8283
8677
  children: [
8284
- /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)(import_react_native55.View, { style: { flex: 1 }, children: [
8285
- !canExpand[item.id] ? /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
8678
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)(import_react_native56.View, { style: { flex: 1 }, children: [
8679
+ !canExpand[item.id] ? /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(
8286
8680
  Text,
8287
8681
  {
8288
8682
  style: { position: "absolute", opacity: 0, zIndex: -1, width: "100%" },
@@ -8301,14 +8695,14 @@ ${trimmedLine2}\u2026 `;
8301
8695
  children: prompt
8302
8696
  }
8303
8697
  ) : null,
8304
- /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)(
8698
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)(
8305
8699
  Text,
8306
8700
  {
8307
8701
  variant: "bodyMuted",
8308
8702
  numberOfLines: isExpanded ? void 0 : 2,
8309
8703
  children: [
8310
8704
  displayPrompt,
8311
- !isExpanded && showToggle ? /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
8705
+ !isExpanded && showToggle ? /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(
8312
8706
  Text,
8313
8707
  {
8314
8708
  color: theme.colors.text,
@@ -8320,18 +8714,18 @@ ${trimmedLine2}\u2026 `;
8320
8714
  ]
8321
8715
  }
8322
8716
  ),
8323
- showToggle && isExpanded ? /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
8324
- import_react_native55.Pressable,
8717
+ showToggle && isExpanded ? /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(
8718
+ import_react_native56.Pressable,
8325
8719
  {
8326
8720
  onPress: () => setExpanded((prev) => ({ ...prev, [item.id]: false })),
8327
8721
  hitSlop: 6,
8328
8722
  style: { alignSelf: "flex-start", marginTop: 4 },
8329
- children: /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(Text, { variant: "captionMuted", color: theme.colors.text, children: "less" })
8723
+ children: /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(Text, { variant: "captionMuted", color: theme.colors.text, children: "less" })
8330
8724
  }
8331
8725
  ) : null
8332
8726
  ] }),
8333
- /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
8334
- import_react_native55.Pressable,
8727
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(
8728
+ import_react_native56.Pressable,
8335
8729
  {
8336
8730
  onPress: () => {
8337
8731
  if (!onRemove || isRemoving) return;
@@ -8346,7 +8740,7 @@ ${trimmedLine2}\u2026 `;
8346
8740
  },
8347
8741
  hitSlop: 8,
8348
8742
  style: { alignSelf: "center" },
8349
- children: isRemoving ? /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(import_react_native55.ActivityIndicator, { size: "small", color: theme.colors.text }) : /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(IconClose, { size: 14, colorToken: "text" })
8743
+ children: isRemoving ? /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(import_react_native56.ActivityIndicator, { size: "small", color: theme.colors.text }) : /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(IconClose, { size: 14, colorToken: "text" })
8350
8744
  }
8351
8745
  )
8352
8746
  ]
@@ -8360,15 +8754,15 @@ ${trimmedLine2}\u2026 `;
8360
8754
  }
8361
8755
 
8362
8756
  // src/components/chat/AgentProgressCard.tsx
8363
- var import_react_native56 = require("react-native");
8757
+ var import_react_native57 = require("react-native");
8364
8758
 
8365
8759
  // src/components/icons/RemixXLoopLottie.tsx
8366
8760
  var import_lottie_react_native = __toESM(require("lottie-react-native"));
8367
- var import_jsx_runtime57 = require("react/jsx-runtime");
8761
+ var import_jsx_runtime58 = require("react/jsx-runtime");
8368
8762
  var remixXLoopSource = require_remix_x_loop_lottie();
8369
8763
  var Lottie = import_lottie_react_native.default;
8370
8764
  function RemixXLoopLottie({ size = 24, style }) {
8371
- return /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(
8765
+ return /* @__PURE__ */ (0, import_jsx_runtime58.jsx)(
8372
8766
  Lottie,
8373
8767
  {
8374
8768
  source: remixXLoopSource,
@@ -8380,7 +8774,7 @@ function RemixXLoopLottie({ size = 24, style }) {
8380
8774
  }
8381
8775
 
8382
8776
  // src/components/chat/AgentProgressCard.tsx
8383
- var import_jsx_runtime58 = require("react/jsx-runtime");
8777
+ var import_jsx_runtime59 = require("react/jsx-runtime");
8384
8778
  function titleForPhase(phase) {
8385
8779
  if (phase === "planning") return "Planning";
8386
8780
  if (phase === "reasoning") return "Reasoning";
@@ -8405,8 +8799,8 @@ function AgentProgressCard({ progress }) {
8405
8799
  const showAnimatedStatusIcon = progress.status === "running";
8406
8800
  const subtitle = progress.latestMessage || `Agent is ${phaseLabel.toLowerCase()}...`;
8407
8801
  const todo = progress.todoSummary;
8408
- return /* @__PURE__ */ (0, import_jsx_runtime58.jsxs)(
8409
- import_react_native56.View,
8802
+ return /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(
8803
+ import_react_native57.View,
8410
8804
  {
8411
8805
  style: {
8412
8806
  borderWidth: 1,
@@ -8417,23 +8811,23 @@ function AgentProgressCard({ progress }) {
8417
8811
  backgroundColor: withAlpha(theme.colors.surface, theme.scheme === "dark" ? 0.84 : 0.94)
8418
8812
  },
8419
8813
  children: [
8420
- /* @__PURE__ */ (0, import_jsx_runtime58.jsxs)(import_react_native56.View, { style: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", marginBottom: 4 }, children: [
8421
- /* @__PURE__ */ (0, import_jsx_runtime58.jsx)(Text, { variant: "caption", children: statusLabel }),
8422
- /* @__PURE__ */ (0, import_jsx_runtime58.jsxs)(import_react_native56.View, { style: { flexDirection: "row", alignItems: "center" }, children: [
8423
- /* @__PURE__ */ (0, import_jsx_runtime58.jsx)(Text, { variant: "captionMuted", children: phaseLabel }),
8424
- showAnimatedStatusIcon ? /* @__PURE__ */ (0, import_jsx_runtime58.jsx)(RemixXLoopLottie, { size: 20, style: { marginLeft: 8 } }) : null
8814
+ /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(import_react_native57.View, { style: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", marginBottom: 4 }, children: [
8815
+ /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(Text, { variant: "caption", children: statusLabel }),
8816
+ /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(import_react_native57.View, { style: { flexDirection: "row", alignItems: "center" }, children: [
8817
+ /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(Text, { variant: "captionMuted", children: phaseLabel }),
8818
+ showAnimatedStatusIcon ? /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(RemixXLoopLottie, { size: 20, style: { marginLeft: 8 } }) : null
8425
8819
  ] })
8426
8820
  ] }),
8427
- /* @__PURE__ */ (0, import_jsx_runtime58.jsx)(Text, { variant: "bodyMuted", children: subtitle }),
8428
- progress.changedFilesCount > 0 ? /* @__PURE__ */ (0, import_jsx_runtime58.jsxs)(Text, { variant: "captionMuted", style: { marginTop: 8 }, children: [
8821
+ /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(Text, { variant: "bodyMuted", children: subtitle }),
8822
+ progress.changedFilesCount > 0 ? /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(Text, { variant: "captionMuted", style: { marginTop: 8 }, children: [
8429
8823
  "Updated files: ",
8430
8824
  progress.changedFilesCount
8431
8825
  ] }) : null,
8432
- progress.recentFiles.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime58.jsx)(import_react_native56.View, { style: { marginTop: 6 }, children: progress.recentFiles.map((path) => /* @__PURE__ */ (0, import_jsx_runtime58.jsxs)(Text, { variant: "captionMuted", numberOfLines: 1, children: [
8826
+ progress.recentFiles.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(import_react_native57.View, { style: { marginTop: 6 }, children: progress.recentFiles.map((path) => /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(Text, { variant: "captionMuted", numberOfLines: 1, children: [
8433
8827
  "\u2022 ",
8434
8828
  path
8435
8829
  ] }, path)) }) : null,
8436
- todo ? /* @__PURE__ */ (0, import_jsx_runtime58.jsxs)(Text, { variant: "captionMuted", style: { marginTop: 8 }, children: [
8830
+ todo ? /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(Text, { variant: "captionMuted", style: { marginTop: 8 }, children: [
8437
8831
  "Todos: ",
8438
8832
  todo.completed,
8439
8833
  "/",
@@ -8447,8 +8841,8 @@ function AgentProgressCard({ progress }) {
8447
8841
  }
8448
8842
 
8449
8843
  // src/components/chat/BundleProgressCard.tsx
8450
- var import_react_native57 = require("react-native");
8451
- var import_jsx_runtime59 = require("react/jsx-runtime");
8844
+ var import_react_native58 = require("react-native");
8845
+ var import_jsx_runtime60 = require("react/jsx-runtime");
8452
8846
  function titleForStatus2(status) {
8453
8847
  if (status === "succeeded") return "Completed";
8454
8848
  if (status === "failed") return "Failed";
@@ -8460,8 +8854,8 @@ function BundleProgressCard({ progress }) {
8460
8854
  const percent = Math.round(Math.max(0, Math.min(1, progress.progressValue)) * 100);
8461
8855
  const fillColor = progress.status === "failed" ? theme.colors.danger : progress.status === "succeeded" ? theme.colors.success : theme.colors.warning;
8462
8856
  const detail = progress.errorMessage || progress.phaseLabel;
8463
- return /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(
8464
- import_react_native57.View,
8857
+ return /* @__PURE__ */ (0, import_jsx_runtime60.jsxs)(
8858
+ import_react_native58.View,
8465
8859
  {
8466
8860
  accessible: true,
8467
8861
  accessibilityRole: "progressbar",
@@ -8476,15 +8870,15 @@ function BundleProgressCard({ progress }) {
8476
8870
  backgroundColor: withAlpha(theme.colors.surface, theme.scheme === "dark" ? 0.84 : 0.94)
8477
8871
  },
8478
8872
  children: [
8479
- /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(import_react_native57.View, { style: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", marginBottom: 8 }, children: [
8480
- /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(Text, { variant: "caption", children: statusLabel }),
8481
- /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(Text, { variant: "captionMuted", children: [
8873
+ /* @__PURE__ */ (0, import_jsx_runtime60.jsxs)(import_react_native58.View, { style: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", marginBottom: 8 }, children: [
8874
+ /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(Text, { variant: "caption", children: statusLabel }),
8875
+ /* @__PURE__ */ (0, import_jsx_runtime60.jsxs)(Text, { variant: "captionMuted", children: [
8482
8876
  percent,
8483
8877
  "%"
8484
8878
  ] })
8485
8879
  ] }),
8486
- /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(
8487
- import_react_native57.View,
8880
+ /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(
8881
+ import_react_native58.View,
8488
8882
  {
8489
8883
  style: {
8490
8884
  width: "100%",
@@ -8493,8 +8887,8 @@ function BundleProgressCard({ progress }) {
8493
8887
  backgroundColor: withAlpha(theme.colors.border, theme.scheme === "dark" ? 0.5 : 0.6),
8494
8888
  overflow: "hidden"
8495
8889
  },
8496
- children: /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(
8497
- import_react_native57.View,
8890
+ children: /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(
8891
+ import_react_native58.View,
8498
8892
  {
8499
8893
  style: {
8500
8894
  width: `${percent}%`,
@@ -8505,14 +8899,14 @@ function BundleProgressCard({ progress }) {
8505
8899
  )
8506
8900
  }
8507
8901
  ),
8508
- /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(Text, { variant: "captionMuted", numberOfLines: 1, style: { marginTop: 8, minHeight: 16 }, children: detail })
8902
+ /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(Text, { variant: "captionMuted", numberOfLines: 1, style: { marginTop: 8, minHeight: 16 }, children: detail })
8509
8903
  ]
8510
8904
  }
8511
8905
  );
8512
8906
  }
8513
8907
 
8514
8908
  // src/studio/ui/ChatPanel.tsx
8515
- var import_jsx_runtime60 = require("react/jsx-runtime");
8909
+ var import_jsx_runtime61 = require("react/jsx-runtime");
8516
8910
  function ChatPanel({
8517
8911
  title = "Chat",
8518
8912
  messages,
@@ -8532,14 +8926,15 @@ function ChatPanel({
8532
8926
  onSend,
8533
8927
  onRetryMessage,
8534
8928
  isRetryingMessage,
8929
+ onAttachmentLoadError,
8535
8930
  queueItems = [],
8536
8931
  onRemoveQueueItem,
8537
8932
  progress = null
8538
8933
  }) {
8539
8934
  const theme = useTheme();
8540
- const listRef = React43.useRef(null);
8541
- const [nearBottom, setNearBottom] = React43.useState(true);
8542
- const handleSend = React43.useCallback(
8935
+ const listRef = React44.useRef(null);
8936
+ const [nearBottom, setNearBottom] = React44.useState(true);
8937
+ const handleSend = React44.useCallback(
8543
8938
  async (text, composerAttachments) => {
8544
8939
  const all = composerAttachments ?? attachments;
8545
8940
  await onSend(text, all.length > 0 ? all : void 0);
@@ -8553,25 +8948,25 @@ function ChatPanel({
8553
8948
  },
8554
8949
  [attachments, nearBottom, onClearAttachments, onSend]
8555
8950
  );
8556
- const handleScrollToBottom = React43.useCallback(() => {
8951
+ const handleScrollToBottom = React44.useCallback(() => {
8557
8952
  var _a;
8558
8953
  (_a = listRef.current) == null ? void 0 : _a.scrollToBottom({ animated: true });
8559
8954
  }, []);
8560
- const header = /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(
8955
+ const header = /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(
8561
8956
  ChatHeader,
8562
8957
  {
8563
- left: /* @__PURE__ */ (0, import_jsx_runtime60.jsxs)(import_react_native58.View, { style: { flexDirection: "row", alignItems: "center" }, children: [
8564
- /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(StudioSheetHeaderIconButton, { onPress: onBack, accessibilityLabel: "Back", style: { marginRight: 8 }, children: /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(IconBack, { size: 20, colorToken: "floatingContent" }) }),
8565
- onNavigateHome ? /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(StudioSheetHeaderIconButton, { onPress: onNavigateHome, accessibilityLabel: "Home", children: /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(IconHome, { size: 20, colorToken: "floatingContent" }) }) : null
8958
+ left: /* @__PURE__ */ (0, import_jsx_runtime61.jsxs)(import_react_native59.View, { style: { flexDirection: "row", alignItems: "center" }, children: [
8959
+ /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(StudioSheetHeaderIconButton, { onPress: onBack, accessibilityLabel: "Back", style: { marginRight: 8 }, children: /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(IconBack, { size: 20, colorToken: "floatingContent" }) }),
8960
+ onNavigateHome ? /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(StudioSheetHeaderIconButton, { onPress: onNavigateHome, accessibilityLabel: "Home", children: /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(IconHome, { size: 20, colorToken: "floatingContent" }) }) : null
8566
8961
  ] }),
8567
- right: /* @__PURE__ */ (0, import_jsx_runtime60.jsxs)(import_react_native58.View, { style: { flexDirection: "row", alignItems: "center" }, children: [
8568
- onStartDraw ? /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(StudioSheetHeaderIconButton, { onPress: onStartDraw, accessibilityLabel: "Draw", intent: "danger", style: { marginRight: 8 }, children: /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(IconDraw, { size: 20, colorToken: "onDanger" }) }) : null,
8569
- /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(StudioSheetHeaderIconButton, { onPress: onClose, accessibilityLabel: "Close", children: /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(IconClose, { size: 20, colorToken: "floatingContent" }) })
8962
+ right: /* @__PURE__ */ (0, import_jsx_runtime61.jsxs)(import_react_native59.View, { style: { flexDirection: "row", alignItems: "center" }, children: [
8963
+ onStartDraw ? /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(StudioSheetHeaderIconButton, { onPress: onStartDraw, accessibilityLabel: "Draw", intent: "danger", style: { marginRight: 8 }, children: /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(IconDraw, { size: 20, colorToken: "onDanger" }) }) : null,
8964
+ /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(StudioSheetHeaderIconButton, { onPress: onClose, accessibilityLabel: "Close", children: /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(IconClose, { size: 20, colorToken: "floatingContent" }) })
8570
8965
  ] }),
8571
8966
  center: null
8572
8967
  }
8573
8968
  );
8574
- const topBanner = shouldForkOnEdit ? /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(
8969
+ const topBanner = shouldForkOnEdit ? /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(
8575
8970
  ForkNoticeBanner,
8576
8971
  {
8577
8972
  isOwner: !shouldForkOnEdit,
@@ -8580,22 +8975,22 @@ function ChatPanel({
8580
8975
  ) : null;
8581
8976
  const showMessagesLoading = Boolean(loading) && messages.length === 0;
8582
8977
  if (showMessagesLoading) {
8583
- return /* @__PURE__ */ (0, import_jsx_runtime60.jsxs)(import_react_native58.View, { style: { flex: 1 }, children: [
8584
- /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(import_react_native58.View, { children: header }),
8585
- topBanner ? /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(import_react_native58.View, { style: { paddingHorizontal: 16, paddingTop: 8 }, children: topBanner }) : null,
8586
- /* @__PURE__ */ (0, import_jsx_runtime60.jsxs)(import_react_native58.View, { style: { flex: 1, alignItems: "center", justifyContent: "center", paddingHorizontal: 24, paddingVertical: 12 }, children: [
8587
- /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(import_react_native58.ActivityIndicator, {}),
8588
- /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(import_react_native58.View, { style: { height: 12 } }),
8589
- /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(Text, { variant: "bodyMuted", children: "Loading messages\u2026" })
8978
+ return /* @__PURE__ */ (0, import_jsx_runtime61.jsxs)(import_react_native59.View, { style: { flex: 1 }, children: [
8979
+ /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(import_react_native59.View, { children: header }),
8980
+ topBanner ? /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(import_react_native59.View, { style: { paddingHorizontal: 16, paddingTop: 8 }, children: topBanner }) : null,
8981
+ /* @__PURE__ */ (0, import_jsx_runtime61.jsxs)(import_react_native59.View, { style: { flex: 1, alignItems: "center", justifyContent: "center", paddingHorizontal: 24, paddingVertical: 12 }, children: [
8982
+ /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(import_react_native59.ActivityIndicator, {}),
8983
+ /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(import_react_native59.View, { style: { height: 12 } }),
8984
+ /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(Text, { variant: "bodyMuted", children: "Loading messages\u2026" })
8590
8985
  ] })
8591
8986
  ] });
8592
8987
  }
8593
8988
  const bundleProgress = (progress == null ? void 0 : progress.bundle) ?? null;
8594
- const queueTop = progress || queueItems.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime60.jsxs)(import_react_native58.View, { style: { gap: theme.spacing.sm }, children: [
8595
- progress ? bundleProgress ? /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(BundleProgressCard, { progress: bundleProgress }) : /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(AgentProgressCard, { progress }) : null,
8596
- !progress && queueItems.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(ChatQueue, { items: queueItems, onRemove: onRemoveQueueItem }) : null
8989
+ const queueTop = progress || queueItems.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime61.jsxs)(import_react_native59.View, { style: { gap: theme.spacing.sm }, children: [
8990
+ progress ? bundleProgress ? /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(BundleProgressCard, { progress: bundleProgress }) : /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(AgentProgressCard, { progress }) : null,
8991
+ !progress && queueItems.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(ChatQueue, { items: queueItems, onRemove: onRemoveQueueItem }) : null
8597
8992
  ] }) : null;
8598
- return /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(
8993
+ return /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(
8599
8994
  ChatPage,
8600
8995
  {
8601
8996
  header,
@@ -8603,18 +8998,19 @@ function ChatPanel({
8603
8998
  showTypingIndicator,
8604
8999
  onRetryMessage,
8605
9000
  isRetryingMessage,
9001
+ onAttachmentLoadError,
8606
9002
  topBanner,
8607
9003
  composerTop: queueTop,
8608
9004
  composerHorizontalPadding: 0,
8609
9005
  listRef,
8610
9006
  onNearBottomChange: setNearBottom,
8611
- overlay: /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(
9007
+ overlay: /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(
8612
9008
  ScrollToBottomButton,
8613
9009
  {
8614
9010
  visible: !nearBottom,
8615
9011
  onPress: handleScrollToBottom,
8616
9012
  style: { bottom: 80 },
8617
- children: /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(IconArrowDown, { size: 20, colorToken: "floatingContent" })
9013
+ children: /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(IconArrowDown, { size: 20, colorToken: "floatingContent" })
8618
9014
  }
8619
9015
  ),
8620
9016
  composer: {
@@ -8634,9 +9030,9 @@ function ChatPanel({
8634
9030
  }
8635
9031
 
8636
9032
  // src/components/dialogs/ConfirmMergeRequestDialog.tsx
8637
- var React44 = __toESM(require("react"));
8638
- var import_react_native59 = require("react-native");
8639
- var import_jsx_runtime61 = require("react/jsx-runtime");
9033
+ var React45 = __toESM(require("react"));
9034
+ var import_react_native60 = require("react-native");
9035
+ var import_jsx_runtime62 = require("react/jsx-runtime");
8640
9036
  function ConfirmMergeRequestDialog({
8641
9037
  visible,
8642
9038
  onOpenChange,
@@ -8647,14 +9043,14 @@ function ConfirmMergeRequestDialog({
8647
9043
  onTestFirst
8648
9044
  }) {
8649
9045
  const theme = useTheme();
8650
- const close = React44.useCallback(() => onOpenChange(false), [onOpenChange]);
9046
+ const close = React45.useCallback(() => onOpenChange(false), [onOpenChange]);
8651
9047
  const canConfirm = Boolean(mergeRequest) && !approveDisabled;
8652
- const handleConfirm = React44.useCallback(() => {
9048
+ const handleConfirm = React45.useCallback(() => {
8653
9049
  if (!mergeRequest) return;
8654
9050
  onOpenChange(false);
8655
9051
  void onConfirm();
8656
9052
  }, [mergeRequest, onConfirm, onOpenChange]);
8657
- const handleTestFirst = React44.useCallback(() => {
9053
+ const handleTestFirst = React45.useCallback(() => {
8658
9054
  if (!mergeRequest) return;
8659
9055
  onOpenChange(false);
8660
9056
  void onTestFirst(mergeRequest);
@@ -8666,7 +9062,7 @@ function ConfirmMergeRequestDialog({
8666
9062
  justifyContent: "center",
8667
9063
  alignSelf: "stretch"
8668
9064
  };
8669
- return /* @__PURE__ */ (0, import_jsx_runtime61.jsxs)(
9065
+ return /* @__PURE__ */ (0, import_jsx_runtime62.jsxs)(
8670
9066
  Modal,
8671
9067
  {
8672
9068
  visible,
@@ -8677,7 +9073,7 @@ function ConfirmMergeRequestDialog({
8677
9073
  backgroundColor: theme.colors.background
8678
9074
  },
8679
9075
  children: [
8680
- /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(import_react_native59.View, { children: /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(
9076
+ /* @__PURE__ */ (0, import_jsx_runtime62.jsx)(import_react_native60.View, { children: /* @__PURE__ */ (0, import_jsx_runtime62.jsx)(
8681
9077
  Text,
8682
9078
  {
8683
9079
  style: {
@@ -8689,9 +9085,9 @@ function ConfirmMergeRequestDialog({
8689
9085
  children: "Are you sure you want to approve this merge request?"
8690
9086
  }
8691
9087
  ) }),
8692
- /* @__PURE__ */ (0, import_jsx_runtime61.jsxs)(import_react_native59.View, { style: { marginTop: 16 }, children: [
8693
- /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(
8694
- import_react_native59.View,
9088
+ /* @__PURE__ */ (0, import_jsx_runtime62.jsxs)(import_react_native60.View, { style: { marginTop: 16 }, children: [
9089
+ /* @__PURE__ */ (0, import_jsx_runtime62.jsx)(
9090
+ import_react_native60.View,
8695
9091
  {
8696
9092
  style: [
8697
9093
  fullWidthButtonBase,
@@ -8700,22 +9096,22 @@ function ConfirmMergeRequestDialog({
8700
9096
  opacity: canConfirm ? 1 : 0.5
8701
9097
  }
8702
9098
  ],
8703
- children: /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(
8704
- import_react_native59.Pressable,
9099
+ children: /* @__PURE__ */ (0, import_jsx_runtime62.jsx)(
9100
+ import_react_native60.Pressable,
8705
9101
  {
8706
9102
  accessibilityRole: "button",
8707
9103
  accessibilityLabel: "Approve Merge",
8708
9104
  disabled: !canConfirm,
8709
9105
  onPress: handleConfirm,
8710
9106
  style: [fullWidthButtonBase, { flex: 1 }],
8711
- children: /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(Text, { style: { textAlign: "center", color: theme.colors.onPrimary }, children: "Approve Merge" })
9107
+ children: /* @__PURE__ */ (0, import_jsx_runtime62.jsx)(Text, { style: { textAlign: "center", color: theme.colors.onPrimary }, children: "Approve Merge" })
8712
9108
  }
8713
9109
  )
8714
9110
  }
8715
9111
  ),
8716
- /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(import_react_native59.View, { style: { height: 8 } }),
8717
- /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(
8718
- import_react_native59.View,
9112
+ /* @__PURE__ */ (0, import_jsx_runtime62.jsx)(import_react_native60.View, { style: { height: 8 } }),
9113
+ /* @__PURE__ */ (0, import_jsx_runtime62.jsx)(
9114
+ import_react_native60.View,
8719
9115
  {
8720
9116
  style: [
8721
9117
  fullWidthButtonBase,
@@ -8726,22 +9122,22 @@ function ConfirmMergeRequestDialog({
8726
9122
  opacity: isBuilding || !mergeRequest ? 0.5 : 1
8727
9123
  }
8728
9124
  ],
8729
- children: /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(
8730
- import_react_native59.Pressable,
9125
+ children: /* @__PURE__ */ (0, import_jsx_runtime62.jsx)(
9126
+ import_react_native60.Pressable,
8731
9127
  {
8732
9128
  accessibilityRole: "button",
8733
9129
  accessibilityLabel: isBuilding ? "Preparing\u2026" : "Test edits first",
8734
9130
  disabled: isBuilding || !mergeRequest,
8735
9131
  onPress: handleTestFirst,
8736
9132
  style: [fullWidthButtonBase, { flex: 1 }],
8737
- children: /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(Text, { style: { textAlign: "center", color: theme.colors.text }, children: isBuilding ? "Preparing\u2026" : "Test edits first" })
9133
+ children: /* @__PURE__ */ (0, import_jsx_runtime62.jsx)(Text, { style: { textAlign: "center", color: theme.colors.text }, children: isBuilding ? "Preparing\u2026" : "Test edits first" })
8738
9134
  }
8739
9135
  )
8740
9136
  }
8741
9137
  ),
8742
- /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(import_react_native59.View, { style: { height: 8 } }),
8743
- /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(
8744
- import_react_native59.View,
9138
+ /* @__PURE__ */ (0, import_jsx_runtime62.jsx)(import_react_native60.View, { style: { height: 8 } }),
9139
+ /* @__PURE__ */ (0, import_jsx_runtime62.jsx)(
9140
+ import_react_native60.View,
8745
9141
  {
8746
9142
  style: [
8747
9143
  fullWidthButtonBase,
@@ -8751,14 +9147,14 @@ function ConfirmMergeRequestDialog({
8751
9147
  borderColor: theme.colors.border
8752
9148
  }
8753
9149
  ],
8754
- children: /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(
8755
- import_react_native59.Pressable,
9150
+ children: /* @__PURE__ */ (0, import_jsx_runtime62.jsx)(
9151
+ import_react_native60.Pressable,
8756
9152
  {
8757
9153
  accessibilityRole: "button",
8758
9154
  accessibilityLabel: "Cancel",
8759
9155
  onPress: close,
8760
9156
  style: [fullWidthButtonBase, { flex: 1 }],
8761
- children: /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(Text, { style: { textAlign: "center", color: theme.colors.text }, children: "Cancel" })
9157
+ children: /* @__PURE__ */ (0, import_jsx_runtime62.jsx)(Text, { style: { textAlign: "center", color: theme.colors.text }, children: "Cancel" })
8762
9158
  }
8763
9159
  )
8764
9160
  }
@@ -8770,7 +9166,7 @@ function ConfirmMergeRequestDialog({
8770
9166
  }
8771
9167
 
8772
9168
  // src/studio/ui/ConfirmMergeFlow.tsx
8773
- var import_jsx_runtime62 = require("react/jsx-runtime");
9169
+ var import_jsx_runtime63 = require("react/jsx-runtime");
8774
9170
  function ConfirmMergeFlow({
8775
9171
  visible,
8776
9172
  onOpenChange,
@@ -8781,7 +9177,7 @@ function ConfirmMergeFlow({
8781
9177
  onConfirm,
8782
9178
  onTestFirst
8783
9179
  }) {
8784
- return /* @__PURE__ */ (0, import_jsx_runtime62.jsx)(
9180
+ return /* @__PURE__ */ (0, import_jsx_runtime63.jsx)(
8785
9181
  ConfirmMergeRequestDialog,
8786
9182
  {
8787
9183
  visible,
@@ -8803,7 +9199,8 @@ function ConfirmMergeFlow({
8803
9199
  }
8804
9200
 
8805
9201
  // src/studio/hooks/useOptimisticChatMessages.ts
8806
- var React45 = __toESM(require("react"));
9202
+ var React46 = __toESM(require("react"));
9203
+ var import_react_native61 = require("react-native");
8807
9204
  function makeOptimisticId() {
8808
9205
  return `optimistic:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 10)}`;
8809
9206
  }
@@ -8814,6 +9211,28 @@ function toEpochMs2(createdAt) {
8814
9211
  const t = Date.parse(String(createdAt));
8815
9212
  return Number.isFinite(t) ? t : 0;
8816
9213
  }
9214
+ async function resolveAttachmentDimensions(uris) {
9215
+ return Promise.all(
9216
+ uris.map(
9217
+ async (uri) => {
9218
+ try {
9219
+ const { width, height } = await new Promise((resolve, reject) => {
9220
+ import_react_native61.Image.getSize(
9221
+ uri,
9222
+ (w, h) => resolve({ width: w, height: h }),
9223
+ (err) => reject(err)
9224
+ );
9225
+ });
9226
+ if (width > 0 && height > 0) {
9227
+ return { uri, width: Math.round(width), height: Math.round(height) };
9228
+ }
9229
+ } catch {
9230
+ }
9231
+ return { uri };
9232
+ }
9233
+ )
9234
+ );
9235
+ }
8817
9236
  function isOptimisticResolvedByServer(chatMessages, o) {
8818
9237
  if (o.failed) return false;
8819
9238
  const normalize = (s) => s.trim();
@@ -8842,11 +9261,11 @@ function useOptimisticChatMessages({
8842
9261
  chatMessages,
8843
9262
  onSendChat
8844
9263
  }) {
8845
- const [optimisticChat, setOptimisticChat] = React45.useState([]);
8846
- React45.useEffect(() => {
9264
+ const [optimisticChat, setOptimisticChat] = React46.useState([]);
9265
+ React46.useEffect(() => {
8847
9266
  setOptimisticChat([]);
8848
9267
  }, [threadId]);
8849
- const messages = React45.useMemo(() => {
9268
+ const messages = React46.useMemo(() => {
8850
9269
  if (!optimisticChat || optimisticChat.length === 0) return chatMessages;
8851
9270
  const unresolved = optimisticChat.filter((o) => !isOptimisticResolvedByServer(chatMessages, o));
8852
9271
  if (unresolved.length === 0) return chatMessages;
@@ -8856,13 +9275,22 @@ function useOptimisticChatMessages({
8856
9275
  content: o.content,
8857
9276
  createdAt: o.createdAtIso,
8858
9277
  kind: "optimistic",
9278
+ attachments: (o.attachments ?? []).map((attachment, index) => ({
9279
+ id: `${o.id}:attachment:${index}`,
9280
+ name: `attachment-${index + 1}.png`,
9281
+ mimeType: "image/png",
9282
+ size: 1,
9283
+ uri: attachment.uri,
9284
+ width: attachment.width,
9285
+ height: attachment.height
9286
+ })),
8859
9287
  meta: o.failed ? { kind: "optimistic", event: "send.failed", status: "error" } : { kind: "optimistic", event: "send.pending", status: "info" }
8860
9288
  }));
8861
9289
  const merged = [...chatMessages, ...optimisticAsChat];
8862
9290
  merged.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
8863
9291
  return merged;
8864
9292
  }, [chatMessages, optimisticChat]);
8865
- React45.useEffect(() => {
9293
+ React46.useEffect(() => {
8866
9294
  if (optimisticChat.length === 0) return;
8867
9295
  setOptimisticChat((prev) => {
8868
9296
  if (prev.length === 0) return prev;
@@ -8870,7 +9298,7 @@ function useOptimisticChatMessages({
8870
9298
  return next.length === prev.length ? prev : next;
8871
9299
  });
8872
9300
  }, [chatMessages, optimisticChat.length]);
8873
- const onSend = React45.useCallback(
9301
+ const onSend = React46.useCallback(
8874
9302
  async (text, attachments) => {
8875
9303
  if (disableOptimistic) {
8876
9304
  await onSendChat(text, attachments);
@@ -8879,7 +9307,7 @@ function useOptimisticChatMessages({
8879
9307
  const createdAtIso = (/* @__PURE__ */ new Date()).toISOString();
8880
9308
  const baseServerLastId = chatMessages.length > 0 ? chatMessages[chatMessages.length - 1].id : null;
8881
9309
  const id = makeOptimisticId();
8882
- const normalizedAttachments = attachments && attachments.length > 0 ? [...attachments] : void 0;
9310
+ const normalizedAttachments = attachments && attachments.length > 0 ? await resolveAttachmentDimensions(attachments) : void 0;
8883
9311
  setOptimisticChat((prev) => [
8884
9312
  ...prev,
8885
9313
  {
@@ -8898,8 +9326,9 @@ function useOptimisticChatMessages({
8898
9326
  },
8899
9327
  [chatMessages, disableOptimistic, onSendChat]
8900
9328
  );
8901
- const onRetry = React45.useCallback(
9329
+ const onRetry = React46.useCallback(
8902
9330
  async (messageId) => {
9331
+ var _a;
8903
9332
  if (disableOptimistic) return;
8904
9333
  const target = optimisticChat.find((m) => m.id === messageId);
8905
9334
  if (!target || target.retrying) return;
@@ -8910,7 +9339,10 @@ function useOptimisticChatMessages({
8910
9339
  )
8911
9340
  );
8912
9341
  try {
8913
- await onSendChat(target.content, target.attachments);
9342
+ await onSendChat(
9343
+ target.content,
9344
+ (_a = target.attachments) == null ? void 0 : _a.map((att) => att.uri)
9345
+ );
8914
9346
  setOptimisticChat(
8915
9347
  (prev) => prev.map((m) => m.id === messageId ? { ...m, retrying: false } : m)
8916
9348
  );
@@ -8922,7 +9354,7 @@ function useOptimisticChatMessages({
8922
9354
  },
8923
9355
  [chatMessages, disableOptimistic, onSendChat, optimisticChat]
8924
9356
  );
8925
- const isRetrying = React45.useCallback(
9357
+ const isRetrying = React46.useCallback(
8926
9358
  (messageId) => {
8927
9359
  return optimisticChat.some((m) => m.id === messageId && m.retrying);
8928
9360
  },
@@ -8936,24 +9368,24 @@ var import_studio_control = require("@comergehq/studio-control");
8936
9368
 
8937
9369
  // src/components/icons/RemixUpIcon.tsx
8938
9370
  var import_react_native_svg3 = __toESM(require("react-native-svg"));
8939
- var import_jsx_runtime63 = require("react/jsx-runtime");
9371
+ var import_jsx_runtime64 = require("react/jsx-runtime");
8940
9372
  function RemixUpIcon({ width = 24, height = 24, ...props }) {
8941
- return /* @__PURE__ */ (0, import_jsx_runtime63.jsxs)(import_react_native_svg3.default, { viewBox: "0 0 70 49", width, height, fill: "none", ...props, children: [
8942
- /* @__PURE__ */ (0, import_jsx_runtime63.jsx)(
9373
+ return /* @__PURE__ */ (0, import_jsx_runtime64.jsxs)(import_react_native_svg3.default, { viewBox: "0 0 70 49", width, height, fill: "none", ...props, children: [
9374
+ /* @__PURE__ */ (0, import_jsx_runtime64.jsx)(
8943
9375
  import_react_native_svg3.Path,
8944
9376
  {
8945
9377
  d: "M34.706 7.62939e-05L34.7656 2.28882e-05L21.44 13.2661L0 34.8401L13.266 48.1061L34.706 26.5321L21.44 13.2661L34.706 7.62939e-05Z",
8946
9378
  fill: "#00CBC0"
8947
9379
  }
8948
9380
  ),
8949
- /* @__PURE__ */ (0, import_jsx_runtime63.jsx)(
9381
+ /* @__PURE__ */ (0, import_jsx_runtime64.jsx)(
8950
9382
  import_react_native_svg3.Path,
8951
9383
  {
8952
9384
  d: "M47.972 13.266L34.7656 2.28882e-05L34.706 7.62939e-05L47.972 13.266L34.706 26.5321L56.28 48.106L69.546 34.84L47.972 13.266Z",
8953
9385
  fill: "#FF1820"
8954
9386
  }
8955
9387
  ),
8956
- /* @__PURE__ */ (0, import_jsx_runtime63.jsx)(
9388
+ /* @__PURE__ */ (0, import_jsx_runtime64.jsx)(
8957
9389
  import_react_native_svg3.Path,
8958
9390
  {
8959
9391
  d: "M34.7656 2.28882e-05L21.44 13.2661L34.706 26.5321L47.972 13.266L34.7656 2.28882e-05Z",
@@ -8964,7 +9396,7 @@ function RemixUpIcon({ width = 24, height = 24, ...props }) {
8964
9396
  }
8965
9397
 
8966
9398
  // src/studio/ui/StudioOverlay.tsx
8967
- var import_jsx_runtime64 = require("react/jsx-runtime");
9399
+ var import_jsx_runtime65 = require("react/jsx-runtime");
8968
9400
  function StudioOverlay({
8969
9401
  captureTargetRef,
8970
9402
  app,
@@ -8995,6 +9427,7 @@ function StudioOverlay({
8995
9427
  chatSending,
8996
9428
  chatShowTypingIndicator,
8997
9429
  onSendChat,
9430
+ onChatAttachmentLoadError,
8998
9431
  chatQueueItems,
8999
9432
  onRemoveQueueItem,
9000
9433
  chatProgress,
@@ -9008,15 +9441,15 @@ function StudioOverlay({
9008
9441
  onSwitchRelatedApp
9009
9442
  }) {
9010
9443
  const theme = useTheme();
9011
- const { width } = (0, import_react_native60.useWindowDimensions)();
9012
- const [sheetOpen, setSheetOpen] = React46.useState(false);
9013
- const sheetOpenRef = React46.useRef(sheetOpen);
9014
- const pendingNavigateHomeRef = React46.useRef(false);
9015
- const [activePage, setActivePage] = React46.useState("preview");
9016
- const [drawing, setDrawing] = React46.useState(false);
9017
- const [chatAttachments, setChatAttachments] = React46.useState([]);
9018
- const [commentsAppId, setCommentsAppId] = React46.useState(null);
9019
- const [commentsCount, setCommentsCount] = React46.useState(null);
9444
+ const { width } = (0, import_react_native62.useWindowDimensions)();
9445
+ const [sheetOpen, setSheetOpen] = React47.useState(false);
9446
+ const sheetOpenRef = React47.useRef(sheetOpen);
9447
+ const pendingNavigateHomeRef = React47.useRef(false);
9448
+ const [activePage, setActivePage] = React47.useState("preview");
9449
+ const [drawing, setDrawing] = React47.useState(false);
9450
+ const [chatAttachments, setChatAttachments] = React47.useState([]);
9451
+ const [commentsAppId, setCommentsAppId] = React47.useState(null);
9452
+ const [commentsCount, setCommentsCount] = React47.useState(null);
9020
9453
  const threadId = (app == null ? void 0 : app.threadId) ?? null;
9021
9454
  const isForking = chatForking || (app == null ? void 0 : app.status) === "forking";
9022
9455
  const isBubbleLoading = (app == null ? void 0 : app.status) === "editing" || isBaseBundleDownloading;
@@ -9029,26 +9462,26 @@ function StudioOverlay({
9029
9462
  chatMessages,
9030
9463
  onSendChat
9031
9464
  });
9032
- const [confirmMrId, setConfirmMrId] = React46.useState(null);
9033
- const confirmMr = React46.useMemo(
9465
+ const [confirmMrId, setConfirmMrId] = React47.useState(null);
9466
+ const confirmMr = React47.useMemo(
9034
9467
  () => confirmMrId ? incomingMergeRequests.find((m) => m.id === confirmMrId) ?? null : null,
9035
9468
  [confirmMrId, incomingMergeRequests]
9036
9469
  );
9037
- const handleSheetOpenChange = React46.useCallback((open) => {
9470
+ const handleSheetOpenChange = React47.useCallback((open) => {
9038
9471
  setSheetOpen(open);
9039
- if (!open) import_react_native60.Keyboard.dismiss();
9472
+ if (!open) import_react_native62.Keyboard.dismiss();
9040
9473
  }, []);
9041
- const closeSheet = React46.useCallback(() => {
9474
+ const closeSheet = React47.useCallback(() => {
9042
9475
  handleSheetOpenChange(false);
9043
9476
  }, [handleSheetOpenChange]);
9044
- const openSheet = React46.useCallback(() => setSheetOpen(true), []);
9045
- const goToChat = React46.useCallback(() => {
9477
+ const openSheet = React47.useCallback(() => setSheetOpen(true), []);
9478
+ const goToChat = React47.useCallback(() => {
9046
9479
  setActivePage("chat");
9047
9480
  openSheet();
9048
9481
  }, [openSheet]);
9049
- const backToPreview = React46.useCallback(() => {
9050
- if (import_react_native60.Platform.OS !== "ios") {
9051
- import_react_native60.Keyboard.dismiss();
9482
+ const backToPreview = React47.useCallback(() => {
9483
+ if (import_react_native62.Platform.OS !== "ios") {
9484
+ import_react_native62.Keyboard.dismiss();
9052
9485
  setActivePage("preview");
9053
9486
  return;
9054
9487
  }
@@ -9060,15 +9493,15 @@ function StudioOverlay({
9060
9493
  clearTimeout(t);
9061
9494
  setActivePage("preview");
9062
9495
  };
9063
- const sub = import_react_native60.Keyboard.addListener("keyboardDidHide", finalize);
9496
+ const sub = import_react_native62.Keyboard.addListener("keyboardDidHide", finalize);
9064
9497
  const t = setTimeout(finalize, 350);
9065
- import_react_native60.Keyboard.dismiss();
9498
+ import_react_native62.Keyboard.dismiss();
9066
9499
  }, []);
9067
- const startDraw = React46.useCallback(() => {
9500
+ const startDraw = React47.useCallback(() => {
9068
9501
  setDrawing(true);
9069
9502
  closeSheet();
9070
9503
  }, [closeSheet]);
9071
- const handleDrawCapture = React46.useCallback(
9504
+ const handleDrawCapture = React47.useCallback(
9072
9505
  (dataUrl) => {
9073
9506
  setChatAttachments((prev) => [...prev, dataUrl]);
9074
9507
  setDrawing(false);
@@ -9077,7 +9510,7 @@ function StudioOverlay({
9077
9510
  },
9078
9511
  [openSheet]
9079
9512
  );
9080
- const toggleSheet = React46.useCallback(async () => {
9513
+ const toggleSheet = React47.useCallback(async () => {
9081
9514
  if (!sheetOpen) {
9082
9515
  const shouldExitTest = Boolean(testingMrId) || isTesting;
9083
9516
  if (shouldExitTest) {
@@ -9089,7 +9522,7 @@ function StudioOverlay({
9089
9522
  closeSheet();
9090
9523
  }
9091
9524
  }, [closeSheet, isTesting, onRestoreBase, sheetOpen, testingMrId]);
9092
- const handleTestMr = React46.useCallback(
9525
+ const handleTestMr = React47.useCallback(
9093
9526
  async (mr) => {
9094
9527
  if (!onTestMr) return;
9095
9528
  await onTestMr(mr);
@@ -9097,46 +9530,46 @@ function StudioOverlay({
9097
9530
  },
9098
9531
  [closeSheet, onTestMr]
9099
9532
  );
9100
- const handleNavigateHome = React46.useCallback(() => {
9533
+ const handleNavigateHome = React47.useCallback(() => {
9101
9534
  if (!onNavigateHome) return;
9102
- if (import_react_native60.Platform.OS !== "android") {
9535
+ if (import_react_native62.Platform.OS !== "android") {
9103
9536
  onNavigateHome();
9104
9537
  return;
9105
9538
  }
9106
9539
  if (!sheetOpenRef.current) {
9107
- import_react_native60.InteractionManager.runAfterInteractions(() => {
9540
+ import_react_native62.InteractionManager.runAfterInteractions(() => {
9108
9541
  onNavigateHome();
9109
9542
  });
9110
9543
  return;
9111
9544
  }
9112
9545
  pendingNavigateHomeRef.current = true;
9113
- import_react_native60.Keyboard.dismiss();
9546
+ import_react_native62.Keyboard.dismiss();
9114
9547
  setActivePage("preview");
9115
9548
  closeSheet();
9116
9549
  }, [closeSheet, onNavigateHome]);
9117
- const handleSheetDismiss = React46.useCallback(() => {
9118
- if (import_react_native60.Platform.OS !== "android") return;
9550
+ const handleSheetDismiss = React47.useCallback(() => {
9551
+ if (import_react_native62.Platform.OS !== "android") return;
9119
9552
  if (!pendingNavigateHomeRef.current) return;
9120
9553
  pendingNavigateHomeRef.current = false;
9121
- import_react_native60.InteractionManager.runAfterInteractions(() => {
9554
+ import_react_native62.InteractionManager.runAfterInteractions(() => {
9122
9555
  onNavigateHome == null ? void 0 : onNavigateHome();
9123
9556
  });
9124
9557
  }, [onNavigateHome]);
9125
- React46.useEffect(() => {
9558
+ React47.useEffect(() => {
9126
9559
  if (!sheetOpen) {
9127
9560
  return;
9128
9561
  }
9129
9562
  pendingNavigateHomeRef.current = false;
9130
9563
  }, [sheetOpen]);
9131
- React46.useEffect(() => {
9564
+ React47.useEffect(() => {
9132
9565
  return () => {
9133
9566
  pendingNavigateHomeRef.current = false;
9134
9567
  };
9135
9568
  }, []);
9136
- React46.useEffect(() => {
9569
+ React47.useEffect(() => {
9137
9570
  sheetOpenRef.current = sheetOpen;
9138
9571
  }, [sheetOpen]);
9139
- React46.useEffect(() => {
9572
+ React47.useEffect(() => {
9140
9573
  const poller = (0, import_studio_control.startStudioControlPolling)((action) => {
9141
9574
  if (action === "show" && !sheetOpenRef.current) openSheet();
9142
9575
  if (action === "hide" && sheetOpenRef.current) closeSheet();
@@ -9144,17 +9577,17 @@ function StudioOverlay({
9144
9577
  }, studioControlOptions);
9145
9578
  return () => poller.stop();
9146
9579
  }, [closeSheet, openSheet, studioControlOptions, toggleSheet]);
9147
- React46.useEffect(() => {
9580
+ React47.useEffect(() => {
9148
9581
  void (0, import_studio_control.publishComergeStudioUIState)(sheetOpen, studioControlOptions);
9149
9582
  }, [sheetOpen, studioControlOptions]);
9150
- return /* @__PURE__ */ (0, import_jsx_runtime64.jsxs)(import_jsx_runtime64.Fragment, { children: [
9151
- /* @__PURE__ */ (0, import_jsx_runtime64.jsx)(EdgeGlowFrame, { visible: isTesting, role: "accent", thickness: 40, intensity: 1 }),
9152
- /* @__PURE__ */ (0, import_jsx_runtime64.jsx)(StudioBottomSheet, { open: sheetOpen, onOpenChange: handleSheetOpenChange, onDismiss: handleSheetDismiss, children: /* @__PURE__ */ (0, import_jsx_runtime64.jsx)(
9583
+ return /* @__PURE__ */ (0, import_jsx_runtime65.jsxs)(import_jsx_runtime65.Fragment, { children: [
9584
+ /* @__PURE__ */ (0, import_jsx_runtime65.jsx)(EdgeGlowFrame, { visible: isTesting, role: "accent", thickness: 40, intensity: 1 }),
9585
+ /* @__PURE__ */ (0, import_jsx_runtime65.jsx)(StudioBottomSheet, { open: sheetOpen, onOpenChange: handleSheetOpenChange, onDismiss: handleSheetDismiss, children: /* @__PURE__ */ (0, import_jsx_runtime65.jsx)(
9153
9586
  StudioSheetPager,
9154
9587
  {
9155
9588
  activePage,
9156
9589
  width,
9157
- preview: /* @__PURE__ */ (0, import_jsx_runtime64.jsx)(
9590
+ preview: /* @__PURE__ */ (0, import_jsx_runtime65.jsx)(
9158
9591
  PreviewPanel,
9159
9592
  {
9160
9593
  app,
@@ -9188,7 +9621,7 @@ function StudioOverlay({
9188
9621
  onSwitchRelatedApp
9189
9622
  }
9190
9623
  ),
9191
- chat: /* @__PURE__ */ (0, import_jsx_runtime64.jsx)(
9624
+ chat: /* @__PURE__ */ (0, import_jsx_runtime65.jsx)(
9192
9625
  ChatPanel,
9193
9626
  {
9194
9627
  messages: optimistic.messages,
@@ -9208,6 +9641,7 @@ function StudioOverlay({
9208
9641
  onSend: optimistic.onSend,
9209
9642
  onRetryMessage: optimistic.onRetry,
9210
9643
  isRetryingMessage: optimistic.isRetrying,
9644
+ onAttachmentLoadError: onChatAttachmentLoadError,
9211
9645
  queueItems: queueItemsForChat,
9212
9646
  onRemoveQueueItem,
9213
9647
  progress: chatProgress
@@ -9215,7 +9649,7 @@ function StudioOverlay({
9215
9649
  )
9216
9650
  }
9217
9651
  ) }),
9218
- showBubble && /* @__PURE__ */ (0, import_jsx_runtime64.jsx)(
9652
+ showBubble && /* @__PURE__ */ (0, import_jsx_runtime65.jsx)(
9219
9653
  Bubble,
9220
9654
  {
9221
9655
  visible: !sheetOpen && !drawing,
@@ -9224,10 +9658,10 @@ function StudioOverlay({
9224
9658
  onPress: toggleSheet,
9225
9659
  isLoading: isBubbleLoading,
9226
9660
  loadingBorderTone: isBaseBundleDownloading ? "warning" : "default",
9227
- children: /* @__PURE__ */ (0, import_jsx_runtime64.jsx)(import_react_native60.View, { style: { width: 28, height: 28, alignItems: "center", justifyContent: "center" }, children: isBubbleLoading ? /* @__PURE__ */ (0, import_jsx_runtime64.jsx)(RemixXLoopLottie, { size: 24 }) : /* @__PURE__ */ (0, import_jsx_runtime64.jsx)(RemixUpIcon, { width: 24, height: 24 }) })
9661
+ children: /* @__PURE__ */ (0, import_jsx_runtime65.jsx)(import_react_native62.View, { style: { width: 28, height: 28, alignItems: "center", justifyContent: "center" }, children: isBubbleLoading ? /* @__PURE__ */ (0, import_jsx_runtime65.jsx)(RemixXLoopLottie, { size: 24 }) : /* @__PURE__ */ (0, import_jsx_runtime65.jsx)(RemixUpIcon, { width: 24, height: 24 }) })
9228
9662
  }
9229
9663
  ),
9230
- /* @__PURE__ */ (0, import_jsx_runtime64.jsx)(
9664
+ /* @__PURE__ */ (0, import_jsx_runtime65.jsx)(
9231
9665
  DrawModeOverlay,
9232
9666
  {
9233
9667
  visible: drawing,
@@ -9236,7 +9670,7 @@ function StudioOverlay({
9236
9670
  onCapture: handleDrawCapture
9237
9671
  }
9238
9672
  ),
9239
- /* @__PURE__ */ (0, import_jsx_runtime64.jsx)(
9673
+ /* @__PURE__ */ (0, import_jsx_runtime65.jsx)(
9240
9674
  ConfirmMergeFlow,
9241
9675
  {
9242
9676
  visible: Boolean(confirmMr),
@@ -9250,7 +9684,7 @@ function StudioOverlay({
9250
9684
  onTestFirst: handleTestMr
9251
9685
  }
9252
9686
  ),
9253
- /* @__PURE__ */ (0, import_jsx_runtime64.jsx)(
9687
+ /* @__PURE__ */ (0, import_jsx_runtime65.jsx)(
9254
9688
  AppCommentsSheet,
9255
9689
  {
9256
9690
  appId: commentsAppId,
@@ -9263,7 +9697,7 @@ function StudioOverlay({
9263
9697
  }
9264
9698
 
9265
9699
  // src/studio/hooks/useEditQueue.ts
9266
- var React47 = __toESM(require("react"));
9700
+ var React48 = __toESM(require("react"));
9267
9701
 
9268
9702
  // src/data/apps/edit-queue/remote.ts
9269
9703
  var EditQueueRemoteDataSourceImpl = class extends BaseRemote {
@@ -9372,17 +9806,17 @@ var editQueueRepository = new EditQueueRepositoryImpl(
9372
9806
 
9373
9807
  // src/studio/hooks/useEditQueue.ts
9374
9808
  function useEditQueue(appId) {
9375
- const [items, setItems] = React47.useState([]);
9376
- const [loading, setLoading] = React47.useState(false);
9377
- const [error, setError] = React47.useState(null);
9378
- const activeRequestIdRef = React47.useRef(0);
9809
+ const [items, setItems] = React48.useState([]);
9810
+ const [loading, setLoading] = React48.useState(false);
9811
+ const [error, setError] = React48.useState(null);
9812
+ const activeRequestIdRef = React48.useRef(0);
9379
9813
  const foregroundSignal = useForegroundSignal(Boolean(appId));
9380
- const upsertSorted = React47.useCallback((prev, nextItem) => {
9814
+ const upsertSorted = React48.useCallback((prev, nextItem) => {
9381
9815
  const next = prev.some((x) => x.id === nextItem.id) ? prev.map((x) => x.id === nextItem.id ? nextItem : x) : [...prev, nextItem];
9382
9816
  next.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
9383
9817
  return next;
9384
9818
  }, []);
9385
- const refetch = React47.useCallback(async () => {
9819
+ const refetch = React48.useCallback(async () => {
9386
9820
  if (!appId) {
9387
9821
  setItems([]);
9388
9822
  return;
@@ -9402,10 +9836,10 @@ function useEditQueue(appId) {
9402
9836
  if (activeRequestIdRef.current === requestId) setLoading(false);
9403
9837
  }
9404
9838
  }, [appId]);
9405
- React47.useEffect(() => {
9839
+ React48.useEffect(() => {
9406
9840
  void refetch();
9407
9841
  }, [refetch]);
9408
- React47.useEffect(() => {
9842
+ React48.useEffect(() => {
9409
9843
  if (!appId) return;
9410
9844
  const unsubscribe = editQueueRepository.subscribeEditQueue(appId, {
9411
9845
  onInsert: (item) => setItems((prev) => upsertSorted(prev, item)),
@@ -9414,7 +9848,7 @@ function useEditQueue(appId) {
9414
9848
  });
9415
9849
  return unsubscribe;
9416
9850
  }, [appId, upsertSorted, foregroundSignal]);
9417
- React47.useEffect(() => {
9851
+ React48.useEffect(() => {
9418
9852
  if (!appId) return;
9419
9853
  if (foregroundSignal <= 0) return;
9420
9854
  void refetch();
@@ -9423,16 +9857,16 @@ function useEditQueue(appId) {
9423
9857
  }
9424
9858
 
9425
9859
  // src/studio/hooks/useEditQueueActions.ts
9426
- var React48 = __toESM(require("react"));
9860
+ var React49 = __toESM(require("react"));
9427
9861
  function useEditQueueActions(appId) {
9428
- const update = React48.useCallback(
9862
+ const update = React49.useCallback(
9429
9863
  async (queueItemId, payload) => {
9430
9864
  if (!appId) return;
9431
9865
  await editQueueRepository.update(appId, queueItemId, payload);
9432
9866
  },
9433
9867
  [appId]
9434
9868
  );
9435
- const cancel = React48.useCallback(
9869
+ const cancel = React49.useCallback(
9436
9870
  async (queueItemId) => {
9437
9871
  if (!appId) return;
9438
9872
  await editQueueRepository.cancel(appId, queueItemId);
@@ -9443,7 +9877,7 @@ function useEditQueueActions(appId) {
9443
9877
  }
9444
9878
 
9445
9879
  // src/studio/hooks/useAgentRunProgress.ts
9446
- var React49 = __toESM(require("react"));
9880
+ var React50 = __toESM(require("react"));
9447
9881
 
9448
9882
  // src/data/agent-progress/repository.ts
9449
9883
  function mapRun(row) {
@@ -9709,23 +10143,23 @@ function deriveView(run, events, nowMs) {
9709
10143
  function useAgentRunProgress(threadId, opts) {
9710
10144
  var _a;
9711
10145
  const enabled = Boolean((opts == null ? void 0 : opts.enabled) ?? true);
9712
- const [run, setRun] = React49.useState(null);
9713
- const [events, setEvents] = React49.useState([]);
9714
- const [loading, setLoading] = React49.useState(false);
9715
- const [error, setError] = React49.useState(null);
9716
- const activeRequestIdRef = React49.useRef(0);
9717
- const lastSeqRef = React49.useRef(0);
9718
- const runRef = React49.useRef(null);
10146
+ const [run, setRun] = React50.useState(null);
10147
+ const [events, setEvents] = React50.useState([]);
10148
+ const [loading, setLoading] = React50.useState(false);
10149
+ const [error, setError] = React50.useState(null);
10150
+ const activeRequestIdRef = React50.useRef(0);
10151
+ const lastSeqRef = React50.useRef(0);
10152
+ const runRef = React50.useRef(null);
9719
10153
  const foregroundSignal = useForegroundSignal(Boolean(threadId) && enabled);
9720
- const [bundleTick, setBundleTick] = React49.useState(0);
9721
- React49.useEffect(() => {
10154
+ const [bundleTick, setBundleTick] = React50.useState(0);
10155
+ React50.useEffect(() => {
9722
10156
  lastSeqRef.current = 0;
9723
10157
  runRef.current = null;
9724
10158
  }, [threadId]);
9725
- React49.useEffect(() => {
10159
+ React50.useEffect(() => {
9726
10160
  runRef.current = run;
9727
10161
  }, [run]);
9728
- const refetch = React49.useCallback(async () => {
10162
+ const refetch = React50.useCallback(async () => {
9729
10163
  if (!threadId || !enabled) {
9730
10164
  setRun(null);
9731
10165
  setEvents([]);
@@ -9761,15 +10195,15 @@ function useAgentRunProgress(threadId, opts) {
9761
10195
  if (activeRequestIdRef.current === requestId) setLoading(false);
9762
10196
  }
9763
10197
  }, [enabled, threadId]);
9764
- React49.useEffect(() => {
10198
+ React50.useEffect(() => {
9765
10199
  void refetch();
9766
10200
  }, [refetch]);
9767
- React49.useEffect(() => {
10201
+ React50.useEffect(() => {
9768
10202
  if (!threadId || !enabled) return;
9769
10203
  if (foregroundSignal <= 0) return;
9770
10204
  void refetch();
9771
10205
  }, [enabled, foregroundSignal, refetch, threadId]);
9772
- React49.useEffect(() => {
10206
+ React50.useEffect(() => {
9773
10207
  if (!threadId || !enabled) return;
9774
10208
  const unsubRuns = agentProgressRepository.subscribeThreadRuns(threadId, {
9775
10209
  onInsert: (nextRun) => {
@@ -9799,7 +10233,7 @@ function useAgentRunProgress(threadId, opts) {
9799
10233
  });
9800
10234
  return unsubRuns;
9801
10235
  }, [enabled, threadId, foregroundSignal]);
9802
- React49.useEffect(() => {
10236
+ React50.useEffect(() => {
9803
10237
  if (!enabled || !(run == null ? void 0 : run.id)) return;
9804
10238
  const runId = run.id;
9805
10239
  const processIncoming = (incoming) => {
@@ -9831,8 +10265,8 @@ function useAgentRunProgress(threadId, opts) {
9831
10265
  });
9832
10266
  return unsubscribe;
9833
10267
  }, [enabled, run == null ? void 0 : run.id, foregroundSignal]);
9834
- const view = React49.useMemo(() => deriveView(run, events, Date.now()), [bundleTick, events, run]);
9835
- React49.useEffect(() => {
10268
+ const view = React50.useMemo(() => deriveView(run, events, Date.now()), [bundleTick, events, run]);
10269
+ React50.useEffect(() => {
9836
10270
  var _a2;
9837
10271
  if (!((_a2 = view.bundle) == null ? void 0 : _a2.active)) return;
9838
10272
  const interval = setInterval(() => {
@@ -9845,13 +10279,13 @@ function useAgentRunProgress(threadId, opts) {
9845
10279
  }
9846
10280
 
9847
10281
  // src/studio/hooks/useRelatedApps.ts
9848
- var React50 = __toESM(require("react"));
10282
+ var React51 = __toESM(require("react"));
9849
10283
  function useRelatedApps(appId) {
9850
- const [relatedApps, setRelatedApps] = React50.useState(null);
9851
- const [loading, setLoading] = React50.useState(false);
9852
- const [error, setError] = React50.useState(null);
9853
- const requestSeqRef = React50.useRef(0);
9854
- const fetchRelatedApps = React50.useCallback(async () => {
10284
+ const [relatedApps, setRelatedApps] = React51.useState(null);
10285
+ const [loading, setLoading] = React51.useState(false);
10286
+ const [error, setError] = React51.useState(null);
10287
+ const requestSeqRef = React51.useRef(0);
10288
+ const fetchRelatedApps = React51.useCallback(async () => {
9855
10289
  if (!appId) {
9856
10290
  setRelatedApps(null);
9857
10291
  setError(null);
@@ -9877,7 +10311,7 @@ function useRelatedApps(appId) {
9877
10311
  }
9878
10312
  }
9879
10313
  }, [appId]);
9880
- React50.useEffect(() => {
10314
+ React51.useEffect(() => {
9881
10315
  void fetchRelatedApps();
9882
10316
  }, [fetchRelatedApps]);
9883
10317
  return {
@@ -9889,7 +10323,7 @@ function useRelatedApps(appId) {
9889
10323
  }
9890
10324
 
9891
10325
  // src/studio/ComergeStudio.tsx
9892
- var import_jsx_runtime65 = require("react/jsx-runtime");
10326
+ var import_jsx_runtime66 = require("react/jsx-runtime");
9893
10327
  function ComergeStudio({
9894
10328
  appId,
9895
10329
  clientKey: clientKey2,
@@ -9905,13 +10339,13 @@ function ComergeStudio({
9905
10339
  embeddedBaseBundles,
9906
10340
  onSystemEvent
9907
10341
  }) {
9908
- const [activeAppId, setActiveAppId] = React51.useState(appId);
9909
- const [runtimeAppId, setRuntimeAppId] = React51.useState(appId);
9910
- const [pendingRuntimeTargetAppId, setPendingRuntimeTargetAppId] = React51.useState(null);
9911
- const didSyncFromHostRef = React51.useRef(false);
9912
- const lastNotifiedRef = React51.useRef(null);
9913
- const platform = React51.useMemo(() => import_react_native61.Platform.OS === "ios" ? "ios" : "android", []);
9914
- const notifyActiveAppChanged = React51.useCallback(
10342
+ const [activeAppId, setActiveAppId] = React52.useState(appId);
10343
+ const [runtimeAppId, setRuntimeAppId] = React52.useState(appId);
10344
+ const [pendingRuntimeTargetAppId, setPendingRuntimeTargetAppId] = React52.useState(null);
10345
+ const didSyncFromHostRef = React52.useRef(false);
10346
+ const lastNotifiedRef = React52.useRef(null);
10347
+ const platform = React52.useMemo(() => import_react_native63.Platform.OS === "ios" ? "ios" : "android", []);
10348
+ const notifyActiveAppChanged = React52.useCallback(
9915
10349
  (nextAppId, source) => {
9916
10350
  if (!onActiveAppChanged) return;
9917
10351
  const trimmedAppId = nextAppId.trim();
@@ -9924,28 +10358,28 @@ function ComergeStudio({
9924
10358
  },
9925
10359
  [appKey, onActiveAppChanged]
9926
10360
  );
9927
- const setActiveAppIdWithSource = React51.useCallback(
10361
+ const setActiveAppIdWithSource = React52.useCallback(
9928
10362
  (nextAppId, source) => {
9929
10363
  setActiveAppId(nextAppId);
9930
10364
  notifyActiveAppChanged(nextAppId, source);
9931
10365
  },
9932
10366
  [notifyActiveAppChanged]
9933
10367
  );
9934
- React51.useEffect(() => {
10368
+ React52.useEffect(() => {
9935
10369
  const source = didSyncFromHostRef.current ? "host_route_sync" : "initial";
9936
10370
  didSyncFromHostRef.current = true;
9937
10371
  setActiveAppIdWithSource(appId, source);
9938
10372
  setRuntimeAppId(appId);
9939
10373
  setPendingRuntimeTargetAppId(null);
9940
10374
  }, [appId, setActiveAppIdWithSource]);
9941
- const captureTargetRef = React51.useRef(null);
9942
- return /* @__PURE__ */ (0, import_jsx_runtime65.jsx)(
10375
+ const captureTargetRef = React52.useRef(null);
10376
+ return /* @__PURE__ */ (0, import_jsx_runtime66.jsx)(
9943
10377
  StudioBootstrap,
9944
10378
  {
9945
10379
  clientKey: clientKey2,
9946
10380
  analyticsEnabled,
9947
- fallback: /* @__PURE__ */ (0, import_jsx_runtime65.jsx)(import_react_native61.View, { style: { flex: 1 } }),
9948
- children: ({ userId }) => /* @__PURE__ */ (0, import_jsx_runtime65.jsx)(import_bottom_sheet6.BottomSheetModalProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime65.jsx)(LiquidGlassResetProvider, { resetTriggers: [appId, activeAppId, runtimeAppId], children: /* @__PURE__ */ (0, import_jsx_runtime65.jsx)(
10381
+ fallback: /* @__PURE__ */ (0, import_jsx_runtime66.jsx)(import_react_native63.View, { style: { flex: 1 } }),
10382
+ children: ({ userId }) => /* @__PURE__ */ (0, import_jsx_runtime66.jsx)(import_bottom_sheet6.BottomSheetModalProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime66.jsx)(LiquidGlassResetProvider, { resetTriggers: [appId, activeAppId, runtimeAppId], children: /* @__PURE__ */ (0, import_jsx_runtime66.jsx)(
9949
10383
  ComergeStudioInner,
9950
10384
  {
9951
10385
  userId,
@@ -9995,11 +10429,11 @@ function ComergeStudioInner({
9995
10429
  const { app, loading: appLoading } = useApp(activeAppId);
9996
10430
  const { app: runtimeAppFromHook } = useApp(runtimeAppId, { enabled: runtimeAppId !== activeAppId });
9997
10431
  const runtimeApp = runtimeAppId === activeAppId ? app : runtimeAppFromHook;
9998
- const sawEditingOnPendingTargetRef = React51.useRef(false);
9999
- React51.useEffect(() => {
10432
+ const sawEditingOnPendingTargetRef = React52.useRef(false);
10433
+ React52.useEffect(() => {
10000
10434
  sawEditingOnPendingTargetRef.current = false;
10001
10435
  }, [pendingRuntimeTargetAppId]);
10002
- React51.useEffect(() => {
10436
+ React52.useEffect(() => {
10003
10437
  if (!pendingRuntimeTargetAppId) return;
10004
10438
  if (activeAppId !== pendingRuntimeTargetAppId) return;
10005
10439
  if ((app == null ? void 0 : app.status) === "editing") {
@@ -10017,13 +10451,13 @@ function ComergeStudioInner({
10017
10451
  canRequestLatest: (runtimeApp == null ? void 0 : runtimeApp.status) === "ready",
10018
10452
  embeddedBaseBundles
10019
10453
  });
10020
- const sawEditingOnActiveAppRef = React51.useRef(false);
10021
- const [showPostEditPreparing, setShowPostEditPreparing] = React51.useState(false);
10022
- React51.useEffect(() => {
10454
+ const sawEditingOnActiveAppRef = React52.useRef(false);
10455
+ const [showPostEditPreparing, setShowPostEditPreparing] = React52.useState(false);
10456
+ React52.useEffect(() => {
10023
10457
  sawEditingOnActiveAppRef.current = false;
10024
10458
  setShowPostEditPreparing(false);
10025
10459
  }, [activeAppId]);
10026
- React51.useEffect(() => {
10460
+ React52.useEffect(() => {
10027
10461
  if (!(app == null ? void 0 : app.id)) return;
10028
10462
  if (app.status === "editing") {
10029
10463
  sawEditingOnActiveAppRef.current = true;
@@ -10035,7 +10469,7 @@ function ComergeStudioInner({
10035
10469
  sawEditingOnActiveAppRef.current = false;
10036
10470
  }
10037
10471
  }, [app == null ? void 0 : app.id, app == null ? void 0 : app.status]);
10038
- React51.useEffect(() => {
10472
+ React52.useEffect(() => {
10039
10473
  if (!showPostEditPreparing) return;
10040
10474
  const stillProcessingBaseBundle = bundle.loading && bundle.loadingMode === "base" && !bundle.isTesting;
10041
10475
  if (!stillProcessingBaseBundle) {
@@ -10047,19 +10481,19 @@ function ComergeStudioInner({
10047
10481
  const editQueue = useEditQueue(activeAppId);
10048
10482
  const agentProgress = useAgentRunProgress(threadId, { enabled: enableAgentProgress });
10049
10483
  const editQueueActions = useEditQueueActions(activeAppId);
10050
- const [lastEditQueueInfo, setLastEditQueueInfo] = React51.useState(null);
10051
- const lastEditQueueInfoRef = React51.useRef(null);
10052
- const [suppressQueueUntilResponse, setSuppressQueueUntilResponse] = React51.useState(false);
10484
+ const [lastEditQueueInfo, setLastEditQueueInfo] = React52.useState(null);
10485
+ const lastEditQueueInfoRef = React52.useRef(null);
10486
+ const [suppressQueueUntilResponse, setSuppressQueueUntilResponse] = React52.useState(false);
10053
10487
  const mergeRequests = useMergeRequests({ appId: activeAppId });
10054
- const hasOpenOutgoingMr = React51.useMemo(() => {
10488
+ const hasOpenOutgoingMr = React52.useMemo(() => {
10055
10489
  return mergeRequests.lists.outgoing.some((mr) => mr.status === "open");
10056
10490
  }, [mergeRequests.lists.outgoing]);
10057
- const incomingReviewMrs = React51.useMemo(() => {
10491
+ const incomingReviewMrs = React52.useMemo(() => {
10058
10492
  if (!userId) return mergeRequests.lists.incoming;
10059
10493
  return mergeRequests.lists.incoming.filter((mr) => mr.createdBy !== userId);
10060
10494
  }, [mergeRequests.lists.incoming, userId]);
10061
10495
  const uploader = useAttachmentUpload();
10062
- const updateLastEditQueueInfo = React51.useCallback(
10496
+ const updateLastEditQueueInfo = React52.useCallback(
10063
10497
  (info) => {
10064
10498
  lastEditQueueInfoRef.current = info;
10065
10499
  setLastEditQueueInfo(info);
@@ -10101,13 +10535,13 @@ function ComergeStudioInner({
10101
10535
  }
10102
10536
  });
10103
10537
  const chatSendDisabled = false;
10104
- const [processingMrId, setProcessingMrId] = React51.useState(null);
10105
- const [testingMrId, setTestingMrId] = React51.useState(null);
10106
- const [syncingUpstream, setSyncingUpstream] = React51.useState(false);
10107
- const [upstreamSyncStatus, setUpstreamSyncStatus] = React51.useState(null);
10538
+ const [processingMrId, setProcessingMrId] = React52.useState(null);
10539
+ const [testingMrId, setTestingMrId] = React52.useState(null);
10540
+ const [syncingUpstream, setSyncingUpstream] = React52.useState(false);
10541
+ const [upstreamSyncStatus, setUpstreamSyncStatus] = React52.useState(null);
10108
10542
  const isMrTestBuildInProgress = bundle.loading && bundle.loadingMode === "test";
10109
10543
  const isBaseBundleDownloading = bundle.loading && bundle.loadingMode === "base" && !bundle.isTesting;
10110
- const runtimePreparingText = React51.useMemo(() => {
10544
+ const runtimePreparingText = React52.useMemo(() => {
10111
10545
  const status = app == null ? void 0 : app.status;
10112
10546
  if (status === "ready" && bundle.bundleStatus === "pending") {
10113
10547
  return "Bundling app\u2026 this may take a few minutes";
@@ -10127,7 +10561,7 @@ function ComergeStudioInner({
10127
10561
  return "Preparing app\u2026";
10128
10562
  }
10129
10563
  }, [app == null ? void 0 : app.status, bundle.bundleStatus]);
10130
- const chatShowTypingIndicator = React51.useMemo(() => {
10564
+ const chatShowTypingIndicator = React52.useMemo(() => {
10131
10565
  var _a2;
10132
10566
  if (agentProgress.hasLiveProgress) return false;
10133
10567
  if (!thread.raw || thread.raw.length === 0) return false;
@@ -10136,12 +10570,12 @@ function ComergeStudioInner({
10136
10570
  return payloadType !== "outcome";
10137
10571
  }, [agentProgress.hasLiveProgress, thread.raw]);
10138
10572
  const showChatProgress = agentProgress.hasLiveProgress || Boolean((_a = agentProgress.view.bundle) == null ? void 0 : _a.active);
10139
- React51.useEffect(() => {
10573
+ React52.useEffect(() => {
10140
10574
  updateLastEditQueueInfo(null);
10141
10575
  setSuppressQueueUntilResponse(false);
10142
10576
  setUpstreamSyncStatus(null);
10143
10577
  }, [activeAppId, updateLastEditQueueInfo]);
10144
- const handleSyncUpstream = React51.useCallback(async () => {
10578
+ const handleSyncUpstream = React52.useCallback(async () => {
10145
10579
  if (!(app == null ? void 0 : app.id)) {
10146
10580
  throw new Error("Missing app");
10147
10581
  }
@@ -10154,7 +10588,7 @@ function ComergeStudioInner({
10154
10588
  setSyncingUpstream(false);
10155
10589
  }
10156
10590
  }, [activeAppId, app == null ? void 0 : app.id]);
10157
- React51.useEffect(() => {
10591
+ React52.useEffect(() => {
10158
10592
  if (!(lastEditQueueInfo == null ? void 0 : lastEditQueueInfo.queueItemId)) return;
10159
10593
  const stillPresent = editQueue.items.some((item) => item.id === lastEditQueueInfo.queueItemId);
10160
10594
  if (!stillPresent) {
@@ -10162,7 +10596,7 @@ function ComergeStudioInner({
10162
10596
  setSuppressQueueUntilResponse(false);
10163
10597
  }
10164
10598
  }, [editQueue.items, lastEditQueueInfo == null ? void 0 : lastEditQueueInfo.queueItemId]);
10165
- const chatQueueItems = React51.useMemo(() => {
10599
+ const chatQueueItems = React52.useMemo(() => {
10166
10600
  var _a2;
10167
10601
  if (suppressQueueUntilResponse && editQueue.items.length <= 1) {
10168
10602
  return [];
@@ -10176,8 +10610,8 @@ function ComergeStudioInner({
10176
10610
  return editQueue.items;
10177
10611
  }, [editQueue.items, lastEditQueueInfo, suppressQueueUntilResponse]);
10178
10612
  const { relatedApps, loading: relatedAppsLoading } = useRelatedApps(activeAppId);
10179
- const [switchingRelatedAppId, setSwitchingRelatedAppId] = React51.useState(null);
10180
- const handleOpenRelatedApps = React51.useCallback(() => {
10613
+ const [switchingRelatedAppId, setSwitchingRelatedAppId] = React52.useState(null);
10614
+ const handleOpenRelatedApps = React52.useCallback(() => {
10181
10615
  var _a2;
10182
10616
  if (!relatedApps) return;
10183
10617
  const ids = /* @__PURE__ */ new Set();
@@ -10186,7 +10620,7 @@ function ComergeStudioInner({
10186
10620
  for (const remix of relatedApps.remixes) ids.add(remix.id);
10187
10621
  void trackRelatedAppsOpened({ appId: relatedApps.current.id, relatedCount: ids.size });
10188
10622
  }, [relatedApps]);
10189
- const handleSwitchRelatedApp = React51.useCallback(
10623
+ const handleSwitchRelatedApp = React52.useCallback(
10190
10624
  async (targetAppId) => {
10191
10625
  var _a2;
10192
10626
  if (!targetAppId || targetAppId === activeAppId) return;
@@ -10247,8 +10681,8 @@ function ComergeStudioInner({
10247
10681
  setRuntimeAppId
10248
10682
  ]
10249
10683
  );
10250
- return /* @__PURE__ */ (0, import_jsx_runtime65.jsx)(import_react_native61.View, { style: [{ flex: 1 }, style], children: /* @__PURE__ */ (0, import_jsx_runtime65.jsxs)(import_react_native61.View, { ref: captureTargetRef, style: { flex: 1 }, collapsable: false, children: [
10251
- /* @__PURE__ */ (0, import_jsx_runtime65.jsx)(
10684
+ return /* @__PURE__ */ (0, import_jsx_runtime66.jsx)(import_react_native63.View, { style: [{ flex: 1 }, style], children: /* @__PURE__ */ (0, import_jsx_runtime66.jsxs)(import_react_native63.View, { ref: captureTargetRef, style: { flex: 1 }, collapsable: false, children: [
10685
+ /* @__PURE__ */ (0, import_jsx_runtime66.jsx)(
10252
10686
  RuntimeRenderer,
10253
10687
  {
10254
10688
  appKey,
@@ -10272,7 +10706,7 @@ function ComergeStudioInner({
10272
10706
  }
10273
10707
  }
10274
10708
  ),
10275
- /* @__PURE__ */ (0, import_jsx_runtime65.jsx)(
10709
+ /* @__PURE__ */ (0, import_jsx_runtime66.jsx)(
10276
10710
  StudioOverlay,
10277
10711
  {
10278
10712
  captureTargetRef,
@@ -10329,6 +10763,9 @@ function ComergeStudioInner({
10329
10763
  chatSending: actions.sending,
10330
10764
  chatShowTypingIndicator,
10331
10765
  onSendChat: (text, attachments) => actions.sendEdit({ prompt: text, attachments }),
10766
+ onChatAttachmentLoadError: () => {
10767
+ thread.recoverAttachmentUrls();
10768
+ },
10332
10769
  chatQueueItems,
10333
10770
  onRemoveQueueItem: (id) => editQueueActions.cancel(id),
10334
10771
  chatProgress: showChatProgress ? agentProgress.view : null,