@comergehq/studio 0.1.34 → 0.1.36

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