@comergehq/studio 0.1.23 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/index.d.mts +3 -1
  2. package/dist/index.d.ts +3 -1
  3. package/dist/index.js +694 -306
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +721 -330
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +1 -1
  8. package/src/components/bubble/Bubble.tsx +9 -0
  9. package/src/components/bubble/types.ts +2 -0
  10. package/src/components/chat/ChatComposer.tsx +4 -21
  11. package/src/components/chat/ChatMessageBubble.tsx +33 -2
  12. package/src/components/chat/ChatMessageList.tsx +12 -1
  13. package/src/components/chat/ChatPage.tsx +8 -14
  14. package/src/components/merge-requests/ReviewMergeRequestCard.tsx +1 -1
  15. package/src/components/primitives/MarkdownText.tsx +134 -35
  16. package/src/components/studio-sheet/StudioBottomSheet.tsx +26 -29
  17. package/src/core/services/http/index.ts +64 -1
  18. package/src/core/services/supabase/realtimeManager.ts +55 -1
  19. package/src/data/agent/types.ts +1 -0
  20. package/src/data/apps/bundles/remote.ts +4 -3
  21. package/src/data/users/types.ts +1 -1
  22. package/src/index.ts +1 -0
  23. package/src/studio/ComergeStudio.tsx +6 -2
  24. package/src/studio/hooks/useApp.ts +24 -6
  25. package/src/studio/hooks/useBundleManager.ts +12 -1
  26. package/src/studio/hooks/useForegroundSignal.ts +2 -4
  27. package/src/studio/hooks/useMergeRequests.ts +6 -1
  28. package/src/studio/hooks/useOptimisticChatMessages.ts +55 -3
  29. package/src/studio/hooks/useStudioActions.ts +60 -6
  30. package/src/studio/hooks/useThreadMessages.ts +26 -5
  31. package/src/studio/ui/ChatPanel.tsx +6 -3
  32. package/src/studio/ui/StudioOverlay.tsx +7 -2
package/dist/index.mjs CHANGED
@@ -363,6 +363,41 @@ var log = logger.createLogger(
363
363
  );
364
364
 
365
365
  // src/core/services/http/index.ts
366
+ var RETRYABLE_MAX_ATTEMPTS = 3;
367
+ var RETRYABLE_BASE_DELAY_MS = 500;
368
+ var RETRYABLE_MAX_DELAY_MS = 4e3;
369
+ var RETRYABLE_JITTER_MS = 250;
370
+ function sleep(ms) {
371
+ return new Promise((resolve) => setTimeout(resolve, ms));
372
+ }
373
+ function isRetryableNetworkError(e) {
374
+ var _a;
375
+ const err = e;
376
+ const code = typeof (err == null ? void 0 : err.code) === "string" ? err.code : "";
377
+ const message = typeof (err == null ? void 0 : err.message) === "string" ? err.message : "";
378
+ if (code === "ERR_NETWORK" || code === "ECONNABORTED") return true;
379
+ if (message.toLowerCase().includes("network error")) return true;
380
+ if (message.toLowerCase().includes("timeout")) return true;
381
+ const status = typeof ((_a = err == null ? void 0 : err.response) == null ? void 0 : _a.status) === "number" ? err.response.status : void 0;
382
+ if (status && (status === 429 || status >= 500)) return true;
383
+ return false;
384
+ }
385
+ function computeBackoffDelay(attempt) {
386
+ const exp = Math.min(RETRYABLE_MAX_DELAY_MS, RETRYABLE_BASE_DELAY_MS * Math.pow(2, attempt - 1));
387
+ const jitter = Math.floor(Math.random() * RETRYABLE_JITTER_MS);
388
+ return exp + jitter;
389
+ }
390
+ function parseRetryAfterMs(value) {
391
+ if (typeof value !== "string") return null;
392
+ const trimmed = value.trim();
393
+ if (!trimmed) return null;
394
+ const seconds = Number(trimmed);
395
+ if (!Number.isNaN(seconds) && seconds >= 0) return seconds * 1e3;
396
+ const parsed = Date.parse(trimmed);
397
+ if (Number.isNaN(parsed)) return null;
398
+ const delta = parsed - Date.now();
399
+ return delta > 0 ? delta : 0;
400
+ }
366
401
  var createApiClient = (baseURL) => {
367
402
  const apiClient = axios2.create({
368
403
  baseURL,
@@ -420,7 +455,7 @@ var createApiClient = (baseURL) => {
420
455
  return response;
421
456
  },
422
457
  async (error) => {
423
- var _a, _b, _c, _d, _e, _f, _g;
458
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
424
459
  const originalRequest = error.config;
425
460
  log.error("Response Error:", {
426
461
  message: error.message,
@@ -456,6 +491,23 @@ var createApiClient = (baseURL) => {
456
491
  return Promise.reject(refreshErr);
457
492
  }
458
493
  }
494
+ const method = ((_h = originalRequest.method) == null ? void 0 : _h.toLowerCase()) ?? "";
495
+ const isGet = method === "get";
496
+ const retryable = isRetryableNetworkError(error);
497
+ const retryCount = originalRequest._retryCount ?? 0;
498
+ const skipRetry = originalRequest.skipRetry === true;
499
+ if (isGet && retryable && !skipRetry && retryCount < RETRYABLE_MAX_ATTEMPTS) {
500
+ const retryAfterMs = parseRetryAfterMs((_j = (_i = error.response) == null ? void 0 : _i.headers) == null ? void 0 : _j["retry-after"]);
501
+ originalRequest._retryCount = retryCount + 1;
502
+ const delayMs = retryAfterMs ?? computeBackoffDelay(retryCount + 1);
503
+ log.warn("Retrying GET request after transient error", {
504
+ url: originalRequest.url,
505
+ attempt: originalRequest._retryCount,
506
+ delayMs
507
+ });
508
+ await sleep(delayMs);
509
+ return apiClient(originalRequest);
510
+ }
459
511
  return Promise.reject(error);
460
512
  }
461
513
  );
@@ -590,6 +642,25 @@ function subscribeChannel(entry) {
590
642
  scheduleResubscribe(entry, "SUBSCRIBE_FAILED");
591
643
  }
592
644
  }
645
+ function unsubscribeChannel(entry) {
646
+ var _a, _b;
647
+ if (!entry.channel) return;
648
+ try {
649
+ (_b = (_a = entry.channel).unsubscribe) == null ? void 0 : _b.call(_a);
650
+ } catch (error) {
651
+ realtimeLog.warn("[realtime] unsubscribe failed", error);
652
+ }
653
+ entry.channel = null;
654
+ }
655
+ function resetRealtimeState(reason) {
656
+ realtimeLog.warn(`[realtime] reset state ${reason}`);
657
+ entries.forEach((entry) => {
658
+ clearTimer(entry);
659
+ entry.backoffMs = INITIAL_BACKOFF_MS;
660
+ unsubscribeChannel(entry);
661
+ });
662
+ entries.clear();
663
+ }
593
664
  function subscribeManagedChannel(key, configure) {
594
665
  let entry = entries.get(key);
595
666
  if (!entry) {
@@ -747,14 +818,12 @@ function useForegroundSignal(enabled = true) {
747
818
  React2.useEffect(() => {
748
819
  if (!enabled) return;
749
820
  const sub = AppState.addEventListener("change", (nextState) => {
750
- var _a, _b;
751
821
  const prevState = lastStateRef.current;
752
822
  lastStateRef.current = nextState;
753
823
  const didResume = (prevState === "background" || prevState === "inactive") && nextState === "active";
754
824
  if (!didResume) return;
755
825
  try {
756
- const supabase = getSupabaseClient();
757
- (_b = (_a = supabase == null ? void 0 : supabase.realtime) == null ? void 0 : _a.connect) == null ? void 0 : _b.call(_a);
826
+ resetRealtimeState("APP_RESUME");
758
827
  } catch {
759
828
  }
760
829
  setSignal((s) => s + 1);
@@ -769,8 +838,13 @@ function useApp(appId, options) {
769
838
  const enabled = (options == null ? void 0 : options.enabled) ?? true;
770
839
  const [app, setApp] = React3.useState(null);
771
840
  const [loading, setLoading] = React3.useState(false);
841
+ const [refreshing, setRefreshing] = React3.useState(false);
772
842
  const [error, setError] = React3.useState(null);
773
843
  const foregroundSignal = useForegroundSignal(enabled && Boolean(appId));
844
+ const hasLoadedOnceRef = React3.useRef(false);
845
+ React3.useEffect(() => {
846
+ hasLoadedOnceRef.current = false;
847
+ }, [appId]);
774
848
  const mergeApp = React3.useCallback((prev, next) => {
775
849
  const merged = {
776
850
  ...prev ?? {},
@@ -780,21 +854,32 @@ function useApp(appId, options) {
780
854
  };
781
855
  return merged;
782
856
  }, []);
783
- const fetchOnce = React3.useCallback(async () => {
857
+ const fetchOnce = React3.useCallback(async (opts) => {
784
858
  if (!enabled) return;
785
859
  if (!appId) return;
786
- setLoading(true);
860
+ const isBackground = Boolean(opts == null ? void 0 : opts.background);
861
+ const useBackgroundRefresh = isBackground && hasLoadedOnceRef.current;
862
+ if (useBackgroundRefresh) {
863
+ setRefreshing(true);
864
+ } else {
865
+ setLoading(true);
866
+ }
787
867
  setError(null);
788
868
  try {
789
869
  const next = await appsRepository.getById(appId);
870
+ hasLoadedOnceRef.current = true;
790
871
  setApp((prev) => mergeApp(prev, next));
791
872
  } catch (e) {
792
873
  setError(e instanceof Error ? e : new Error(String(e)));
793
874
  setApp(null);
794
875
  } finally {
795
- setLoading(false);
876
+ if (useBackgroundRefresh) {
877
+ setRefreshing(false);
878
+ } else {
879
+ setLoading(false);
880
+ }
796
881
  }
797
- }, [appId, enabled]);
882
+ }, [appId, enabled, mergeApp]);
798
883
  React3.useEffect(() => {
799
884
  if (!enabled) return;
800
885
  void fetchOnce();
@@ -819,9 +904,9 @@ function useApp(appId, options) {
819
904
  if (!enabled) return;
820
905
  if (!appId) return;
821
906
  if (foregroundSignal <= 0) return;
822
- void fetchOnce();
907
+ void fetchOnce({ background: true });
823
908
  }, [appId, enabled, fetchOnce, foregroundSignal]);
824
- return { app, loading, error, refetch: fetchOnce };
909
+ return { app, loading, refreshing, error, refetch: fetchOnce };
825
910
  }
826
911
 
827
912
  // src/studio/hooks/useThreadMessages.ts
@@ -955,33 +1040,52 @@ function mapMessageToChatMessage(m) {
955
1040
  function useThreadMessages(threadId) {
956
1041
  const [raw, setRaw] = React4.useState([]);
957
1042
  const [loading, setLoading] = React4.useState(false);
1043
+ const [refreshing, setRefreshing] = React4.useState(false);
958
1044
  const [error, setError] = React4.useState(null);
959
1045
  const activeRequestIdRef = React4.useRef(0);
960
1046
  const foregroundSignal = useForegroundSignal(Boolean(threadId));
1047
+ const hasLoadedOnceRef = React4.useRef(false);
1048
+ React4.useEffect(() => {
1049
+ hasLoadedOnceRef.current = false;
1050
+ }, [threadId]);
961
1051
  const upsertSorted = React4.useCallback((prev, m) => {
962
1052
  const next = prev.filter((x) => x.id !== m.id);
963
1053
  next.push(m);
964
1054
  next.sort(compareMessages);
965
1055
  return next;
966
1056
  }, []);
967
- const refetch = React4.useCallback(async () => {
1057
+ const refetch = React4.useCallback(async (opts) => {
968
1058
  if (!threadId) {
969
1059
  setRaw([]);
1060
+ setLoading(false);
1061
+ setRefreshing(false);
970
1062
  return;
971
1063
  }
972
1064
  const requestId = ++activeRequestIdRef.current;
973
- setLoading(true);
1065
+ const isBackground = Boolean(opts == null ? void 0 : opts.background);
1066
+ const useBackgroundRefresh = isBackground && hasLoadedOnceRef.current;
1067
+ if (useBackgroundRefresh) {
1068
+ setRefreshing(true);
1069
+ } else {
1070
+ setLoading(true);
1071
+ }
974
1072
  setError(null);
975
1073
  try {
976
1074
  const list = await messagesRepository.list(threadId);
977
1075
  if (activeRequestIdRef.current !== requestId) return;
1076
+ hasLoadedOnceRef.current = true;
978
1077
  setRaw([...list].sort(compareMessages));
979
1078
  } catch (e) {
980
1079
  if (activeRequestIdRef.current !== requestId) return;
981
1080
  setError(e instanceof Error ? e : new Error(String(e)));
982
1081
  setRaw([]);
983
1082
  } finally {
984
- if (activeRequestIdRef.current === requestId) setLoading(false);
1083
+ if (activeRequestIdRef.current !== requestId) return;
1084
+ if (useBackgroundRefresh) {
1085
+ setRefreshing(false);
1086
+ } else {
1087
+ setLoading(false);
1088
+ }
985
1089
  }
986
1090
  }, [threadId]);
987
1091
  React4.useEffect(() => {
@@ -999,14 +1103,14 @@ function useThreadMessages(threadId) {
999
1103
  React4.useEffect(() => {
1000
1104
  if (!threadId) return;
1001
1105
  if (foregroundSignal <= 0) return;
1002
- void refetch();
1106
+ void refetch({ background: true });
1003
1107
  }, [foregroundSignal, refetch, threadId]);
1004
1108
  const messages = React4.useMemo(() => {
1005
1109
  const visible = raw.filter((m) => !isQueuedHiddenMessage(m));
1006
1110
  const resolved = visible.length > 0 ? visible : raw;
1007
1111
  return resolved.map(mapMessageToChatMessage);
1008
1112
  }, [raw]);
1009
- return { raw, messages, loading, error, refetch };
1113
+ return { raw, messages, loading, refreshing, error, refetch };
1010
1114
  }
1011
1115
 
1012
1116
  // src/studio/hooks/useBundleManager.ts
@@ -1026,21 +1130,22 @@ var BundlesRemoteDataSourceImpl = class extends BaseRemote {
1026
1130
  }
1027
1131
  async getById(appId, bundleId) {
1028
1132
  const { data } = await api.get(
1029
- `/v1/apps/${encodeURIComponent(appId)}/bundles/${encodeURIComponent(bundleId)}`
1133
+ `/v1/apps/${encodeURIComponent(appId)}/bundles/${encodeURIComponent(bundleId)}`,
1134
+ { skipRetry: true }
1030
1135
  );
1031
1136
  return data;
1032
1137
  }
1033
1138
  async getSignedDownloadUrl(appId, bundleId, options) {
1034
1139
  const { data } = await api.get(
1035
1140
  `/v1/apps/${encodeURIComponent(appId)}/bundles/${encodeURIComponent(bundleId)}/download`,
1036
- { params: { redirect: (options == null ? void 0 : options.redirect) ?? false } }
1141
+ { params: { redirect: (options == null ? void 0 : options.redirect) ?? false }, skipRetry: true }
1037
1142
  );
1038
1143
  return data;
1039
1144
  }
1040
1145
  async getSignedAssetsDownloadUrl(appId, bundleId, options) {
1041
1146
  const { data } = await api.get(
1042
1147
  `/v1/apps/${encodeURIComponent(appId)}/bundles/${encodeURIComponent(bundleId)}/assets/download`,
1043
- { params: { redirect: (options == null ? void 0 : options.redirect) ?? false, kind: options == null ? void 0 : options.kind } }
1148
+ { params: { redirect: (options == null ? void 0 : options.redirect) ?? false, kind: options == null ? void 0 : options.kind }, skipRetry: true }
1044
1149
  );
1045
1150
  return data;
1046
1151
  }
@@ -1073,10 +1178,10 @@ var BundlesRepositoryImpl = class extends BaseRepository {
1073
1178
  var bundlesRepository = new BundlesRepositoryImpl(bundlesRemoteDataSource);
1074
1179
 
1075
1180
  // src/studio/hooks/useBundleManager.ts
1076
- function sleep(ms) {
1181
+ function sleep2(ms) {
1077
1182
  return new Promise((r) => setTimeout(r, ms));
1078
1183
  }
1079
- function isRetryableNetworkError(e) {
1184
+ function isRetryableNetworkError2(e) {
1080
1185
  var _a;
1081
1186
  const err = e;
1082
1187
  const code = typeof (err == null ? void 0 : err.code) === "string" ? err.code : "";
@@ -1095,13 +1200,13 @@ async function withRetry(fn, opts) {
1095
1200
  return await fn();
1096
1201
  } catch (e) {
1097
1202
  lastErr = e;
1098
- const retryable = isRetryableNetworkError(e);
1203
+ const retryable = isRetryableNetworkError2(e);
1099
1204
  if (!retryable || attempt >= opts.attempts) {
1100
1205
  throw e;
1101
1206
  }
1102
1207
  const exp = Math.min(opts.maxDelayMs, opts.baseDelayMs * Math.pow(2, attempt - 1));
1103
1208
  const jitter = Math.floor(Math.random() * 250);
1104
- await sleep(exp + jitter);
1209
+ await sleep2(exp + jitter);
1105
1210
  }
1106
1211
  }
1107
1212
  throw lastErr;
@@ -1378,14 +1483,14 @@ async function pollBundle(appId, bundleId, opts) {
1378
1483
  const bundle = await bundlesRepository.getById(appId, bundleId);
1379
1484
  if (bundle.status === "succeeded" || bundle.status === "failed") return bundle;
1380
1485
  } catch (e) {
1381
- if (!isRetryableNetworkError(e)) {
1486
+ if (!isRetryableNetworkError2(e)) {
1382
1487
  throw e;
1383
1488
  }
1384
1489
  }
1385
1490
  if (Date.now() - start > opts.timeoutMs) {
1386
1491
  throw new Error("Bundle build timed out.");
1387
1492
  }
1388
- await sleep(opts.intervalMs);
1493
+ await sleep2(opts.intervalMs);
1389
1494
  }
1390
1495
  }
1391
1496
  async function resolveBundlePath(src, platform, mode) {
@@ -1454,6 +1559,7 @@ function useBundleManager({
1454
1559
  const baseOpIdRef = React5.useRef(0);
1455
1560
  const testOpIdRef = React5.useRef(0);
1456
1561
  const activeLoadModeRef = React5.useRef(null);
1562
+ const desiredModeRef = React5.useRef("base");
1457
1563
  const canRequestLatestRef = React5.useRef(canRequestLatest);
1458
1564
  React5.useEffect(() => {
1459
1565
  canRequestLatestRef.current = canRequestLatest;
@@ -1541,6 +1647,10 @@ function useBundleManager({
1541
1647
  );
1542
1648
  const load = React5.useCallback(async (src, mode) => {
1543
1649
  if (!src.appId) return;
1650
+ if (mode === "test") {
1651
+ desiredModeRef.current = "test";
1652
+ baseOpIdRef.current += 1;
1653
+ }
1544
1654
  const canRequestLatest2 = canRequestLatestRef.current;
1545
1655
  if (mode === "base" && !canRequestLatest2) {
1546
1656
  await activateCachedBase(src.appId);
@@ -1552,13 +1662,14 @@ function useBundleManager({
1552
1662
  setLoadingMode(mode);
1553
1663
  setError(null);
1554
1664
  setStatusLabel(mode === "test" ? "Loading test bundle\u2026" : "Loading latest build\u2026");
1555
- if (mode === "base") {
1665
+ if (mode === "base" && desiredModeRef.current === "base") {
1556
1666
  void activateCachedBase(src.appId);
1557
1667
  }
1558
1668
  try {
1559
1669
  const { bundlePath: path, bundle } = await resolveBundlePath(src, platform, mode);
1560
1670
  if (mode === "base" && opId !== baseOpIdRef.current) return;
1561
1671
  if (mode === "test" && opId !== testOpIdRef.current) return;
1672
+ if (desiredModeRef.current !== mode) return;
1562
1673
  setBundlePath(path);
1563
1674
  const fingerprint = bundle.checksumSha256 ?? `id:${bundle.id}`;
1564
1675
  const shouldSkipInitialBaseRemount = mode === "base" && initialHydratedBaseFromDiskRef.current && !hasCompletedFirstNetworkBaseLoadRef.current && Boolean(lastBaseFingerprintRef.current) && lastBaseFingerprintRef.current === fingerprint;
@@ -1614,6 +1725,8 @@ function useBundleManager({
1614
1725
  const restoreBase = React5.useCallback(async () => {
1615
1726
  const src = baseRef.current;
1616
1727
  if (!src.appId) return;
1728
+ desiredModeRef.current = "base";
1729
+ testOpIdRef.current += 1;
1617
1730
  await activateCachedBase(src.appId);
1618
1731
  if (canRequestLatestRef.current) {
1619
1732
  await load(src, "base");
@@ -1775,9 +1888,12 @@ function useMergeRequests(params) {
1775
1888
  const { appId } = params;
1776
1889
  const [incoming, setIncoming] = React6.useState([]);
1777
1890
  const [outgoing, setOutgoing] = React6.useState([]);
1778
- const [loading, setLoading] = React6.useState(false);
1891
+ const [loading, setLoading] = React6.useState(() => Boolean(appId));
1779
1892
  const [error, setError] = React6.useState(null);
1780
1893
  const [creatorStatsById, setCreatorStatsById] = React6.useState({});
1894
+ React6.useEffect(() => {
1895
+ setLoading(Boolean(appId));
1896
+ }, [appId]);
1781
1897
  const pollUntilMerged = React6.useCallback(async (mrId) => {
1782
1898
  const startedAt = Date.now();
1783
1899
  const timeoutMs = 2 * 60 * 1e3;
@@ -1793,6 +1909,7 @@ function useMergeRequests(params) {
1793
1909
  setIncoming([]);
1794
1910
  setOutgoing([]);
1795
1911
  setCreatorStatsById({});
1912
+ setLoading(false);
1796
1913
  return;
1797
1914
  }
1798
1915
  setLoading(true);
@@ -2027,6 +2144,45 @@ var AgentRepositoryImpl = class extends BaseRepository {
2027
2144
  var agentRepository = new AgentRepositoryImpl(agentRemoteDataSource);
2028
2145
 
2029
2146
  // src/studio/hooks/useStudioActions.ts
2147
+ function sleep3(ms) {
2148
+ return new Promise((resolve) => setTimeout(resolve, ms));
2149
+ }
2150
+ function isRetryableNetworkError3(e) {
2151
+ var _a;
2152
+ const err = e;
2153
+ const code = typeof (err == null ? void 0 : err.code) === "string" ? err.code : "";
2154
+ const message = typeof (err == null ? void 0 : err.message) === "string" ? err.message : "";
2155
+ if (code === "ERR_NETWORK" || code === "ECONNABORTED") return true;
2156
+ if (message.toLowerCase().includes("network error")) return true;
2157
+ if (message.toLowerCase().includes("timeout")) return true;
2158
+ const status = typeof ((_a = err == null ? void 0 : err.response) == null ? void 0 : _a.status) === "number" ? err.response.status : void 0;
2159
+ if (status && (status === 429 || status >= 500)) return true;
2160
+ return false;
2161
+ }
2162
+ async function withRetry2(fn, opts) {
2163
+ let lastErr = null;
2164
+ for (let attempt = 1; attempt <= opts.attempts; attempt += 1) {
2165
+ try {
2166
+ return await fn();
2167
+ } catch (e) {
2168
+ lastErr = e;
2169
+ const retryable = isRetryableNetworkError3(e);
2170
+ if (!retryable || attempt >= opts.attempts) {
2171
+ throw e;
2172
+ }
2173
+ const exp = Math.min(opts.maxDelayMs, opts.baseDelayMs * Math.pow(2, attempt - 1));
2174
+ const jitter = Math.floor(Math.random() * 250);
2175
+ await sleep3(exp + jitter);
2176
+ }
2177
+ }
2178
+ throw lastErr;
2179
+ }
2180
+ function generateIdempotencyKey() {
2181
+ var _a, _b;
2182
+ const rnd = (_b = (_a = globalThis.crypto) == null ? void 0 : _a.randomUUID) == null ? void 0 : _b.call(_a);
2183
+ if (rnd) return `edit:${rnd}`;
2184
+ return `edit:${Date.now()}-${Math.random().toString(16).slice(2)}`;
2185
+ }
2030
2186
  function useStudioActions({
2031
2187
  userId,
2032
2188
  app,
@@ -2065,12 +2221,19 @@ function useStudioActions({
2065
2221
  if (attachments && attachments.length > 0 && uploadAttachments) {
2066
2222
  attachmentMetas = await uploadAttachments({ threadId, appId: targetApp.id, dataUrls: attachments });
2067
2223
  }
2068
- const editResult = await agentRepository.editApp({
2069
- prompt,
2070
- thread_id: threadId,
2071
- app_id: targetApp.id,
2072
- attachments: attachmentMetas && attachmentMetas.length > 0 ? attachmentMetas : void 0
2073
- });
2224
+ const idempotencyKey = generateIdempotencyKey();
2225
+ const editResult = await withRetry2(
2226
+ async () => {
2227
+ return await agentRepository.editApp({
2228
+ prompt,
2229
+ thread_id: threadId,
2230
+ app_id: targetApp.id,
2231
+ attachments: attachmentMetas && attachmentMetas.length > 0 ? attachmentMetas : void 0,
2232
+ idempotencyKey
2233
+ });
2234
+ },
2235
+ { attempts: 3, baseDelayMs: 500, maxDelayMs: 4e3 }
2236
+ );
2074
2237
  onEditQueued == null ? void 0 : onEditQueued({
2075
2238
  queueItemId: editResult.queueItemId ?? null,
2076
2239
  queuePosition: editResult.queuePosition ?? null
@@ -2128,12 +2291,14 @@ function RuntimeRenderer({
2128
2291
 
2129
2292
  // src/studio/ui/StudioOverlay.tsx
2130
2293
  import * as React44 from "react";
2131
- import { Keyboard as Keyboard5, Platform as Platform11, View as View45, useWindowDimensions as useWindowDimensions4 } from "react-native";
2294
+ import { Keyboard as Keyboard4, Platform as Platform10, View as View45, useWindowDimensions as useWindowDimensions4 } from "react-native";
2132
2295
 
2133
2296
  // src/components/studio-sheet/StudioBottomSheet.tsx
2134
2297
  import * as React12 from "react";
2135
2298
  import { AppState as AppState3, Keyboard, View as View4 } from "react-native";
2136
- import BottomSheet from "@gorhom/bottom-sheet";
2299
+ import {
2300
+ BottomSheetModal
2301
+ } from "@gorhom/bottom-sheet";
2137
2302
  import { useSafeAreaInsets } from "react-native-safe-area-context";
2138
2303
 
2139
2304
  // src/components/studio-sheet/StudioSheetBackground.tsx
@@ -2252,25 +2417,16 @@ function StudioBottomSheet({
2252
2417
  const insets = useSafeAreaInsets();
2253
2418
  const internalSheetRef = React12.useRef(null);
2254
2419
  const resolvedSheetRef = sheetRef ?? internalSheetRef;
2255
- const currentIndexRef = React12.useRef(open ? snapPoints.length - 1 : -1);
2420
+ const resolvedSnapPoints = React12.useMemo(() => [...snapPoints], [snapPoints]);
2421
+ const currentIndexRef = React12.useRef(open ? resolvedSnapPoints.length - 1 : -1);
2256
2422
  const lastAppStateRef = React12.useRef(AppState3.currentState);
2257
2423
  React12.useEffect(() => {
2258
2424
  const sub = AppState3.addEventListener("change", (state) => {
2259
- const prev = lastAppStateRef.current;
2260
2425
  lastAppStateRef.current = state;
2261
2426
  if (state === "background" || state === "inactive") {
2262
2427
  Keyboard.dismiss();
2263
2428
  return;
2264
2429
  }
2265
- if (state !== "active") return;
2266
- const sheet = resolvedSheetRef.current;
2267
- if (!sheet) return;
2268
- const idx = currentIndexRef.current;
2269
- if (open && idx >= 0) {
2270
- Keyboard.dismiss();
2271
- requestAnimationFrame(() => sheet.snapToIndex(idx));
2272
- setTimeout(() => sheet.snapToIndex(idx), 120);
2273
- }
2274
2430
  });
2275
2431
  return () => sub.remove();
2276
2432
  }, [open, resolvedSheetRef]);
@@ -2278,11 +2434,11 @@ function StudioBottomSheet({
2278
2434
  const sheet = resolvedSheetRef.current;
2279
2435
  if (!sheet) return;
2280
2436
  if (open) {
2281
- sheet.snapToIndex(snapPoints.length - 1);
2437
+ sheet.present();
2282
2438
  } else {
2283
- sheet.close();
2439
+ sheet.dismiss();
2284
2440
  }
2285
- }, [open, resolvedSheetRef, snapPoints.length]);
2441
+ }, [open, resolvedSheetRef, resolvedSnapPoints.length]);
2286
2442
  const handleChange = React12.useCallback(
2287
2443
  (index) => {
2288
2444
  currentIndexRef.current = index;
@@ -2291,21 +2447,24 @@ function StudioBottomSheet({
2291
2447
  [onOpenChange]
2292
2448
  );
2293
2449
  return /* @__PURE__ */ jsx7(
2294
- BottomSheet,
2450
+ BottomSheetModal,
2295
2451
  {
2296
2452
  ref: resolvedSheetRef,
2297
- index: open ? snapPoints.length - 1 : -1,
2298
- snapPoints,
2453
+ index: resolvedSnapPoints.length - 1,
2454
+ snapPoints: resolvedSnapPoints,
2299
2455
  enableDynamicSizing: false,
2300
2456
  enablePanDownToClose: true,
2301
2457
  enableContentPanningGesture: false,
2458
+ keyboardBehavior: "interactive",
2459
+ keyboardBlurBehavior: "restore",
2302
2460
  android_keyboardInputMode: "adjustResize",
2303
2461
  backgroundComponent: (props) => /* @__PURE__ */ jsx7(StudioSheetBackground, { ...props, renderBackground: background == null ? void 0 : background.renderBackground }),
2304
2462
  topInset: insets.top,
2305
2463
  bottomInset: 0,
2306
2464
  handleIndicatorStyle: { backgroundColor: theme.colors.handleIndicator },
2307
- onChange: handleChange,
2308
2465
  ...bottomSheetProps,
2466
+ onChange: handleChange,
2467
+ onDismiss: () => onOpenChange == null ? void 0 : onOpenChange(false),
2309
2468
  children: /* @__PURE__ */ jsx7(View4, { style: { flex: 1, overflow: "hidden" }, children })
2310
2469
  }
2311
2470
  );
@@ -2366,7 +2525,7 @@ function StudioSheetPager({ activePage, width, preview, chat, style }) {
2366
2525
  }
2367
2526
 
2368
2527
  // src/components/bubble/Bubble.tsx
2369
- import { useCallback as useCallback8, useEffect as useEffect11, useMemo as useMemo3, useRef as useRef7 } from "react";
2528
+ import { useCallback as useCallback8, useEffect as useEffect11, useMemo as useMemo4, useRef as useRef8 } from "react";
2370
2529
  import {
2371
2530
  PanResponder,
2372
2531
  Pressable,
@@ -2400,6 +2559,21 @@ var ENTER_SCALE_FROM = 0.3;
2400
2559
  var ENTER_ROTATION_FROM_DEG = -180;
2401
2560
  var PULSE_DURATION_MS = 900;
2402
2561
 
2562
+ // src/components/utils/color.ts
2563
+ function withAlpha(color, alpha) {
2564
+ const a = Math.max(0, Math.min(1, alpha));
2565
+ const hex = color.trim();
2566
+ if (!hex.startsWith("#")) return color;
2567
+ const raw = hex.slice(1);
2568
+ const expanded = raw.length === 3 ? raw.split("").map((c) => c + c).join("") : raw;
2569
+ if (expanded.length !== 6) return color;
2570
+ const r = Number.parseInt(expanded.slice(0, 2), 16);
2571
+ const g = Number.parseInt(expanded.slice(2, 4), 16);
2572
+ const b = Number.parseInt(expanded.slice(4, 6), 16);
2573
+ if ([r, g, b].some((n) => Number.isNaN(n))) return color;
2574
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
2575
+ }
2576
+
2403
2577
  // src/components/bubble/Bubble.tsx
2404
2578
  import { jsx as jsx9, jsxs as jsxs3 } from "react/jsx-runtime";
2405
2579
  var HIDDEN_OFFSET_X = 20;
@@ -2428,6 +2602,7 @@ function Bubble({
2428
2602
  disabled = false,
2429
2603
  ariaLabel,
2430
2604
  isLoading = false,
2605
+ loadingBorderTone = "default",
2431
2606
  visible = true,
2432
2607
  badgeCount = 0,
2433
2608
  offset = DEFAULT_OFFSET,
@@ -2442,22 +2617,26 @@ function Bubble({
2442
2617
  const theme = useTheme();
2443
2618
  const { width, height } = useWindowDimensions();
2444
2619
  const isDanger = variant === "danger";
2445
- const onPressRef = useRef7(onPress);
2620
+ const onPressRef = useRef8(onPress);
2446
2621
  useEffect11(() => {
2447
2622
  onPressRef.current = onPress;
2448
2623
  }, [onPress]);
2449
- const fallbackBgColor = useMemo3(() => {
2624
+ const fallbackBgColor = useMemo4(() => {
2450
2625
  if (backgroundColor) return backgroundColor;
2451
2626
  if (isDanger) return "rgba(239, 68, 68, 0.9)";
2452
2627
  return theme.scheme === "dark" ? "rgba(0, 0, 0, 0.6)" : "rgba(255, 255, 255, 0.6)";
2453
2628
  }, [backgroundColor, isDanger, theme.scheme]);
2629
+ const warningRingColors = useMemo4(
2630
+ () => [withAlpha(theme.colors.warning, 0.35), withAlpha(theme.colors.warning, 1)],
2631
+ [theme.colors.warning]
2632
+ );
2454
2633
  const translateX = useSharedValue(getHiddenTranslateX(size));
2455
2634
  const translateY = useSharedValue(getHiddenTranslateY(height));
2456
2635
  const scale = useSharedValue(ENTER_SCALE_FROM);
2457
2636
  const rotation = useSharedValue(ENTER_ROTATION_FROM_DEG);
2458
2637
  const borderPulse = useSharedValue(0);
2459
- const startPos = useRef7({ x: 0, y: 0 });
2460
- const isAnimatingOut = useRef7(false);
2638
+ const startPos = useRef8({ x: 0, y: 0 });
2639
+ const isAnimatingOut = useRef8(false);
2461
2640
  const animateToHidden = useCallback8(
2462
2641
  (options) => {
2463
2642
  const finish = options == null ? void 0 : options.onFinish;
@@ -2528,7 +2707,7 @@ function Bubble({
2528
2707
  animateIn();
2529
2708
  }
2530
2709
  }, [forceShowTrigger, visible, animateIn]);
2531
- const panResponder = useRef7(
2710
+ const panResponder = useRef8(
2532
2711
  PanResponder.create({
2533
2712
  onStartShouldSetPanResponder: () => true,
2534
2713
  onMoveShouldSetPanResponder: () => true,
@@ -2564,10 +2743,11 @@ function Bubble({
2564
2743
  ]
2565
2744
  }));
2566
2745
  const borderAnimatedStyle = useAnimatedStyle(() => {
2746
+ const isWarning = loadingBorderTone === "warning";
2567
2747
  const borderColor = interpolateColor(
2568
2748
  borderPulse.value,
2569
2749
  [0, 1],
2570
- isDanger ? ["rgba(239,68,68,0.4)", "rgba(239,68,68,1)"] : theme.scheme === "dark" ? ["rgba(255,255,255,0.2)", "rgba(255,255,255,0.9)"] : ["rgba(55,0,179,0.2)", "rgba(55,0,179,0.9)"]
2750
+ isDanger ? ["rgba(239,68,68,0.4)", "rgba(239,68,68,1)"] : isWarning ? warningRingColors : theme.scheme === "dark" ? ["rgba(255,255,255,0.2)", "rgba(255,255,255,0.9)"] : ["rgba(55,0,179,0.2)", "rgba(55,0,179,0.9)"]
2571
2751
  );
2572
2752
  return {
2573
2753
  borderWidth: isLoading ? 2 : 0,
@@ -2647,23 +2827,6 @@ var styles = StyleSheet.create({
2647
2827
  import * as React14 from "react";
2648
2828
  import { Animated as Animated3, View as View6 } from "react-native";
2649
2829
  import { LinearGradient } from "expo-linear-gradient";
2650
-
2651
- // src/components/utils/color.ts
2652
- function withAlpha(color, alpha) {
2653
- const a = Math.max(0, Math.min(1, alpha));
2654
- const hex = color.trim();
2655
- if (!hex.startsWith("#")) return color;
2656
- const raw = hex.slice(1);
2657
- const expanded = raw.length === 3 ? raw.split("").map((c) => c + c).join("") : raw;
2658
- if (expanded.length !== 6) return color;
2659
- const r = Number.parseInt(expanded.slice(0, 2), 16);
2660
- const g = Number.parseInt(expanded.slice(2, 4), 16);
2661
- const b = Number.parseInt(expanded.slice(4, 6), 16);
2662
- if ([r, g, b].some((n) => Number.isNaN(n))) return color;
2663
- return `rgba(${r}, ${g}, ${b}, ${a})`;
2664
- }
2665
-
2666
- // src/components/overlays/EdgeGlowFrame.tsx
2667
2830
  import { jsx as jsx10, jsxs as jsxs4 } from "react/jsx-runtime";
2668
2831
  function baseColor(role, theme) {
2669
2832
  switch (role) {
@@ -3258,10 +3421,10 @@ var styles3 = StyleSheet3.create({
3258
3421
 
3259
3422
  // src/components/comments/AppCommentsSheet.tsx
3260
3423
  import * as React24 from "react";
3261
- import { ActivityIndicator as ActivityIndicator3, Keyboard as Keyboard3, Platform as Platform6, Pressable as Pressable5, View as View14 } from "react-native";
3424
+ import { ActivityIndicator as ActivityIndicator3, Keyboard as Keyboard3, Platform as Platform5, Pressable as Pressable5, View as View14 } from "react-native";
3262
3425
  import {
3263
3426
  BottomSheetBackdrop,
3264
- BottomSheetModal,
3427
+ BottomSheetModal as BottomSheetModal2,
3265
3428
  BottomSheetScrollView
3266
3429
  } from "@gorhom/bottom-sheet";
3267
3430
  import { useSafeAreaInsets as useSafeAreaInsets3 } from "react-native-safe-area-context";
@@ -3279,7 +3442,7 @@ import {
3279
3442
  ScrollView,
3280
3443
  View as View11
3281
3444
  } from "react-native";
3282
- import { isLiquidGlassSupported as isLiquidGlassSupported3 } from "@callstack/liquid-glass";
3445
+ import { isLiquidGlassSupported as isLiquidGlassSupported3, LiquidGlassView as LiquidGlassView2 } from "@callstack/liquid-glass";
3283
3446
  import { Plus } from "lucide-react-native";
3284
3447
 
3285
3448
  // src/components/chat/MultilineTextInput.tsx
@@ -3417,7 +3580,6 @@ function ChatComposer({
3417
3580
  disabled = false,
3418
3581
  sendDisabled = false,
3419
3582
  sending = false,
3420
- autoFocus = false,
3421
3583
  onSend,
3422
3584
  attachments = [],
3423
3585
  onRemoveAttachment,
@@ -3440,18 +3602,6 @@ function ChatComposer({
3440
3602
  const maxInputHeight = React19.useMemo(() => Dimensions.get("window").height * 0.5, []);
3441
3603
  const shakeAnim = React19.useRef(new Animated5.Value(0)).current;
3442
3604
  const [sendPressed, setSendPressed] = React19.useState(false);
3443
- const inputRef = React19.useRef(null);
3444
- const prevAutoFocusRef = React19.useRef(false);
3445
- React19.useEffect(() => {
3446
- const shouldFocus = autoFocus && !prevAutoFocusRef.current && !disabled && !sending;
3447
- prevAutoFocusRef.current = autoFocus;
3448
- if (!shouldFocus) return;
3449
- const t = setTimeout(() => {
3450
- var _a;
3451
- (_a = inputRef.current) == null ? void 0 : _a.focus();
3452
- }, 75);
3453
- return () => clearTimeout(t);
3454
- }, [autoFocus, disabled, sending]);
3455
3605
  const triggerShake = React19.useCallback(() => {
3456
3606
  shakeAnim.setValue(0);
3457
3607
  Animated5.sequence([
@@ -3482,7 +3632,7 @@ function ChatComposer({
3482
3632
  onLayout: (e) => onLayout == null ? void 0 : onLayout({ height: e.nativeEvent.layout.height }),
3483
3633
  children: /* @__PURE__ */ jsxs8(View11, { style: { flexDirection: "row", alignItems: "flex-end", gap: 8 }, children: [
3484
3634
  /* @__PURE__ */ jsx17(Animated5.View, { style: { flex: 1, transform: [{ translateX: shakeAnim }] }, children: /* @__PURE__ */ jsxs8(
3485
- ResettableLiquidGlassView,
3635
+ LiquidGlassView2,
3486
3636
  {
3487
3637
  style: [
3488
3638
  // LiquidGlassView doesn't reliably auto-size to children; ensure enough height for the
@@ -3534,13 +3684,12 @@ function ChatComposer({
3534
3684
  /* @__PURE__ */ jsx17(
3535
3685
  MultilineTextInput,
3536
3686
  {
3537
- ref: inputRef,
3538
3687
  value: text,
3539
3688
  onChangeText: setText,
3540
3689
  placeholder,
3541
3690
  editable: !disabled && !sending,
3542
3691
  useBottomSheetTextInput,
3543
- autoFocus,
3692
+ autoFocus: false,
3544
3693
  placeholderTextColor,
3545
3694
  scrollEnabled: true,
3546
3695
  style: {
@@ -3865,11 +4014,11 @@ function useAppDetails(appId) {
3865
4014
 
3866
4015
  // src/components/comments/useIosKeyboardSnapFix.ts
3867
4016
  import * as React23 from "react";
3868
- import { Keyboard as Keyboard2, Platform as Platform5 } from "react-native";
4017
+ import { Keyboard as Keyboard2, Platform as Platform4 } from "react-native";
3869
4018
  function useIosKeyboardSnapFix(sheetRef, options) {
3870
4019
  const [keyboardVisible, setKeyboardVisible] = React23.useState(false);
3871
4020
  React23.useEffect(() => {
3872
- if (Platform5.OS !== "ios") return;
4021
+ if (Platform4.OS !== "ios") return;
3873
4022
  const show = Keyboard2.addListener("keyboardWillShow", () => setKeyboardVisible(true));
3874
4023
  const hide = Keyboard2.addListener("keyboardWillHide", () => {
3875
4024
  var _a;
@@ -3937,7 +4086,7 @@ function AppCommentsSheet({ appId, onClose, onCountChange, onPlayApp }) {
3937
4086
  onClose();
3938
4087
  }, [appId, onClose, onPlayApp]);
3939
4088
  return /* @__PURE__ */ jsx20(
3940
- BottomSheetModal,
4089
+ BottomSheetModal2,
3941
4090
  {
3942
4091
  ref: sheetRef,
3943
4092
  index: 1,
@@ -3947,8 +4096,8 @@ function AppCommentsSheet({ appId, onClose, onCountChange, onPlayApp }) {
3947
4096
  onChange: handleChange,
3948
4097
  backgroundStyle: {
3949
4098
  backgroundColor: theme.scheme === "dark" ? "#0B080F" : "#FFFFFF",
3950
- borderTopLeftRadius: Platform6.OS === "ios" ? 39 : 16,
3951
- borderTopRightRadius: Platform6.OS === "ios" ? 39 : 16
4099
+ borderTopLeftRadius: Platform5.OS === "ios" ? 39 : 16,
4100
+ borderTopRightRadius: Platform5.OS === "ios" ? 39 : 16
3952
4101
  },
3953
4102
  handleIndicatorStyle: { backgroundColor: theme.colors.handleIndicator },
3954
4103
  keyboardBehavior: "interactive",
@@ -4054,7 +4203,7 @@ function AppCommentsSheet({ appId, onClose, onCountChange, onPlayApp }) {
4054
4203
  bottom: 0,
4055
4204
  paddingHorizontal: theme.spacing.lg,
4056
4205
  paddingTop: theme.spacing.sm,
4057
- paddingBottom: Platform6.OS === "ios" ? keyboardVisible ? theme.spacing.lg : insets.bottom : insets.bottom + 10,
4206
+ paddingBottom: Platform5.OS === "ios" ? keyboardVisible ? theme.spacing.lg : insets.bottom : insets.bottom + 10,
4058
4207
  borderTopWidth: 1,
4059
4208
  borderTopColor: withAlpha(theme.colors.border, 0.1),
4060
4209
  backgroundColor: withAlpha(theme.colors.background, 0.8)
@@ -4081,7 +4230,7 @@ function AppCommentsSheet({ appId, onClose, onCountChange, onPlayApp }) {
4081
4230
 
4082
4231
  // src/studio/ui/PreviewPanel.tsx
4083
4232
  import * as React35 from "react";
4084
- import { ActivityIndicator as ActivityIndicator7, Platform as Platform8, Share, View as View33 } from "react-native";
4233
+ import { ActivityIndicator as ActivityIndicator7, Platform as Platform7, Share, View as View33 } from "react-native";
4085
4234
 
4086
4235
  // src/components/preview/PreviewPage.tsx
4087
4236
  import { View as View15 } from "react-native";
@@ -4968,16 +5117,48 @@ import { RefreshCw, Send as Send2 } from "lucide-react-native";
4968
5117
 
4969
5118
  // src/components/merge-requests/MergeRequestStatusCard.tsx
4970
5119
  import * as React28 from "react";
4971
- import { Animated as Animated7, Pressable as Pressable9, View as View28 } from "react-native";
5120
+ import { Animated as Animated7, Pressable as Pressable10, View as View28 } from "react-native";
4972
5121
  import { Ban, Check as Check3, CheckCheck, ChevronDown as ChevronDown2 } from "lucide-react-native";
4973
5122
 
4974
5123
  // src/components/primitives/MarkdownText.tsx
4975
- import { Platform as Platform7, View as View27 } from "react-native";
5124
+ import { Platform as Platform6, Pressable as Pressable9, Text as Text3, View as View27 } from "react-native";
4976
5125
  import Markdown from "react-native-markdown-display";
4977
- import { jsx as jsx39 } from "react/jsx-runtime";
5126
+ import { useEffect as useEffect22, useRef as useRef15, useState as useState21 } from "react";
5127
+ import { jsx as jsx39, jsxs as jsxs22 } from "react/jsx-runtime";
5128
+ function copyMarkdownToClipboard(markdown) {
5129
+ var _a;
5130
+ if (!markdown) {
5131
+ return;
5132
+ }
5133
+ const navigatorClipboard = (_a = globalThis == null ? void 0 : globalThis.navigator) == null ? void 0 : _a.clipboard;
5134
+ if (navigatorClipboard == null ? void 0 : navigatorClipboard.writeText) {
5135
+ void navigatorClipboard.writeText(markdown);
5136
+ return;
5137
+ }
5138
+ try {
5139
+ const expoClipboard = __require("expo-clipboard");
5140
+ if (expoClipboard == null ? void 0 : expoClipboard.setStringAsync) {
5141
+ void expoClipboard.setStringAsync(markdown);
5142
+ return;
5143
+ }
5144
+ } catch {
5145
+ }
5146
+ try {
5147
+ const rnClipboard = __require("@react-native-clipboard/clipboard");
5148
+ if (rnClipboard == null ? void 0 : rnClipboard.setString) {
5149
+ rnClipboard.setString(markdown);
5150
+ }
5151
+ } catch {
5152
+ }
5153
+ }
4978
5154
  function MarkdownText({ markdown, variant = "chat", bodyColor, style }) {
4979
5155
  const theme = useTheme();
4980
5156
  const isDark = theme.scheme === "dark";
5157
+ const [showCopied, setShowCopied] = useState21(false);
5158
+ const [tooltipPosition, setTooltipPosition] = useState21(null);
5159
+ const [tooltipWidth, setTooltipWidth] = useState21(0);
5160
+ const hideTimerRef = useRef15(null);
5161
+ const containerRef = useRef15(null);
4981
5162
  const baseBodyColor = variant === "mergeRequest" ? theme.colors.textMuted : theme.colors.text;
4982
5163
  const linkColor = variant === "mergeRequest" ? isDark ? theme.colors.primary : "#3700B3" : theme.colors.link;
4983
5164
  const linkWeight = variant === "mergeRequest" ? theme.typography.fontWeight.semibold : void 0;
@@ -4985,40 +5166,96 @@ function MarkdownText({ markdown, variant = "chat", bodyColor, style }) {
4985
5166
  const codeTextColor = isDark ? "#FFFFFF" : theme.colors.text;
4986
5167
  const paragraphBottom = variant === "mergeRequest" ? 8 : 6;
4987
5168
  const baseLineHeight = variant === "mergeRequest" ? 22 : 20;
4988
- return /* @__PURE__ */ jsx39(View27, { style, children: /* @__PURE__ */ jsx39(
4989
- Markdown,
4990
- {
4991
- style: {
4992
- body: { color: bodyColor ?? baseBodyColor, fontSize: 14, lineHeight: baseLineHeight },
4993
- paragraph: { marginTop: 0, marginBottom: paragraphBottom },
4994
- link: { color: linkColor, fontWeight: linkWeight },
4995
- code_inline: {
4996
- backgroundColor: codeBgColor,
4997
- color: codeTextColor,
4998
- paddingHorizontal: variant === "mergeRequest" ? 6 : 4,
4999
- paddingVertical: variant === "mergeRequest" ? 2 : 0,
5000
- borderRadius: variant === "mergeRequest" ? 6 : 4,
5001
- fontFamily: Platform7.OS === "ios" ? "Menlo" : "monospace",
5002
- fontSize: 13
5169
+ useEffect22(() => {
5170
+ return () => {
5171
+ if (hideTimerRef.current) {
5172
+ clearTimeout(hideTimerRef.current);
5173
+ }
5174
+ };
5175
+ }, []);
5176
+ const handleLongPress = (event) => {
5177
+ var _a;
5178
+ const { locationX, locationY, pageX, pageY } = event.nativeEvent;
5179
+ if ((_a = containerRef.current) == null ? void 0 : _a.measureInWindow) {
5180
+ containerRef.current.measureInWindow((x, y) => {
5181
+ setTooltipPosition({ x: pageX - x, y: pageY - y });
5182
+ });
5183
+ } else {
5184
+ setTooltipPosition({ x: locationX, y: locationY });
5185
+ }
5186
+ copyMarkdownToClipboard(markdown);
5187
+ setShowCopied(true);
5188
+ if (hideTimerRef.current) {
5189
+ clearTimeout(hideTimerRef.current);
5190
+ }
5191
+ hideTimerRef.current = setTimeout(() => {
5192
+ setShowCopied(false);
5193
+ }, 1200);
5194
+ };
5195
+ return /* @__PURE__ */ jsx39(Pressable9, { style, onLongPress: handleLongPress, children: /* @__PURE__ */ jsxs22(View27, { ref: containerRef, style: { position: "relative" }, children: [
5196
+ /* @__PURE__ */ jsx39(
5197
+ Markdown,
5198
+ {
5199
+ style: {
5200
+ body: { color: bodyColor ?? baseBodyColor, fontSize: 14, lineHeight: baseLineHeight },
5201
+ paragraph: { marginTop: 0, marginBottom: paragraphBottom },
5202
+ link: { color: linkColor, fontWeight: linkWeight },
5203
+ code_inline: {
5204
+ backgroundColor: codeBgColor,
5205
+ color: codeTextColor,
5206
+ paddingHorizontal: variant === "mergeRequest" ? 6 : 4,
5207
+ paddingVertical: variant === "mergeRequest" ? 2 : 0,
5208
+ borderRadius: variant === "mergeRequest" ? 6 : 4,
5209
+ fontFamily: Platform6.OS === "ios" ? "Menlo" : "monospace",
5210
+ fontSize: 13
5211
+ },
5212
+ code_block: {
5213
+ backgroundColor: codeBgColor,
5214
+ color: codeTextColor,
5215
+ padding: variant === "mergeRequest" ? 12 : 8,
5216
+ borderRadius: variant === "mergeRequest" ? 8 : 6,
5217
+ marginVertical: variant === "mergeRequest" ? 8 : 0
5218
+ },
5219
+ fence: {
5220
+ backgroundColor: codeBgColor,
5221
+ color: codeTextColor,
5222
+ padding: variant === "mergeRequest" ? 12 : 8,
5223
+ borderRadius: variant === "mergeRequest" ? 8 : 6,
5224
+ marginVertical: variant === "mergeRequest" ? 8 : 0
5225
+ }
5003
5226
  },
5004
- code_block: {
5005
- backgroundColor: codeBgColor,
5006
- color: codeTextColor,
5007
- padding: variant === "mergeRequest" ? 12 : 8,
5008
- borderRadius: variant === "mergeRequest" ? 8 : 6,
5009
- marginVertical: variant === "mergeRequest" ? 8 : 0
5227
+ children: markdown
5228
+ }
5229
+ ),
5230
+ showCopied && tooltipPosition ? /* @__PURE__ */ jsx39(
5231
+ View27,
5232
+ {
5233
+ pointerEvents: "none",
5234
+ style: {
5235
+ position: "absolute",
5236
+ left: tooltipPosition.x,
5237
+ top: tooltipPosition.y - theme.spacing.lg - 32,
5238
+ backgroundColor: theme.colors.success,
5239
+ borderRadius: theme.radii.pill,
5240
+ paddingHorizontal: theme.spacing.sm,
5241
+ paddingVertical: theme.spacing.xs,
5242
+ transform: [{ translateX: tooltipWidth ? -tooltipWidth / 2 : 0 }]
5010
5243
  },
5011
- fence: {
5012
- backgroundColor: codeBgColor,
5013
- color: codeTextColor,
5014
- padding: variant === "mergeRequest" ? 12 : 8,
5015
- borderRadius: variant === "mergeRequest" ? 8 : 6,
5016
- marginVertical: variant === "mergeRequest" ? 8 : 0
5017
- }
5018
- },
5019
- children: markdown
5020
- }
5021
- ) });
5244
+ onLayout: (event) => setTooltipWidth(event.nativeEvent.layout.width),
5245
+ children: /* @__PURE__ */ jsx39(
5246
+ Text3,
5247
+ {
5248
+ style: {
5249
+ color: theme.colors.onSuccess,
5250
+ fontSize: theme.typography.fontSize.xs,
5251
+ fontWeight: theme.typography.fontWeight.medium
5252
+ },
5253
+ children: "Copied"
5254
+ }
5255
+ )
5256
+ }
5257
+ ) : null
5258
+ ] }) });
5022
5259
  }
5023
5260
 
5024
5261
  // src/components/merge-requests/mergeRequestStatusDisplay.ts
@@ -5065,7 +5302,7 @@ function useControlledExpansion(props) {
5065
5302
  }
5066
5303
 
5067
5304
  // src/components/merge-requests/MergeRequestStatusCard.tsx
5068
- import { jsx as jsx40, jsxs as jsxs22 } from "react/jsx-runtime";
5305
+ import { jsx as jsx40, jsxs as jsxs23 } from "react/jsx-runtime";
5069
5306
  function MergeRequestStatusCard({
5070
5307
  mergeRequest,
5071
5308
  expanded: expandedProp,
@@ -5118,7 +5355,7 @@ function MergeRequestStatusCard({
5118
5355
  useNativeDriver: true
5119
5356
  }).start();
5120
5357
  }, [expanded, rotate]);
5121
- return /* @__PURE__ */ jsx40(Pressable9, { onPress: () => setExpanded(!expanded), style: ({ pressed }) => [{ opacity: pressed ? 0.95 : 1 }], children: /* @__PURE__ */ jsxs22(
5358
+ return /* @__PURE__ */ jsx40(Pressable10, { onPress: () => setExpanded(!expanded), style: ({ pressed }) => [{ opacity: pressed ? 0.95 : 1 }], children: /* @__PURE__ */ jsxs23(
5122
5359
  Card,
5123
5360
  {
5124
5361
  padded: false,
@@ -5131,10 +5368,10 @@ function MergeRequestStatusCard({
5131
5368
  style
5132
5369
  ],
5133
5370
  children: [
5134
- /* @__PURE__ */ jsxs22(View28, { style: { flexDirection: "row", alignItems: "center", gap: theme.spacing.lg }, children: [
5371
+ /* @__PURE__ */ jsxs23(View28, { style: { flexDirection: "row", alignItems: "center", gap: theme.spacing.lg }, children: [
5135
5372
  /* @__PURE__ */ jsx40(View28, { style: { width: 40, height: 40, borderRadius: 999, alignItems: "center", justifyContent: "center", backgroundColor: bgColor }, children: /* @__PURE__ */ jsx40(StatusIcon, { size: 20, color: iconColor }) }),
5136
- /* @__PURE__ */ jsxs22(View28, { style: { flex: 1, minWidth: 0 }, children: [
5137
- /* @__PURE__ */ jsxs22(View28, { style: { flexDirection: "row", alignItems: "center", justifyContent: "space-between" }, children: [
5373
+ /* @__PURE__ */ jsxs23(View28, { style: { flex: 1, minWidth: 0 }, children: [
5374
+ /* @__PURE__ */ jsxs23(View28, { style: { flexDirection: "row", alignItems: "center", justifyContent: "space-between" }, children: [
5138
5375
  /* @__PURE__ */ jsx40(
5139
5376
  Text,
5140
5377
  {
@@ -5167,7 +5404,7 @@ function MergeRequestStatusCard({
5167
5404
  }
5168
5405
  )
5169
5406
  ] }),
5170
- expanded ? /* @__PURE__ */ jsxs22(View28, { style: { marginTop: 16, marginLeft: 56 }, children: [
5407
+ expanded ? /* @__PURE__ */ jsxs23(View28, { style: { marginTop: 16, marginLeft: 56 }, children: [
5171
5408
  /* @__PURE__ */ jsx40(
5172
5409
  Text,
5173
5410
  {
@@ -5207,12 +5444,12 @@ import { Animated as Animated9, FlatList, View as View31, useWindowDimensions as
5207
5444
 
5208
5445
  // src/components/merge-requests/ReviewMergeRequestCard.tsx
5209
5446
  import * as React30 from "react";
5210
- import { ActivityIndicator as ActivityIndicator5, Animated as Animated8, Pressable as Pressable11, View as View30 } from "react-native";
5447
+ import { ActivityIndicator as ActivityIndicator5, Animated as Animated8, Pressable as Pressable12, View as View30 } from "react-native";
5211
5448
  import { Check as Check4, ChevronDown as ChevronDown3, Play as Play3, X as X3 } from "lucide-react-native";
5212
5449
 
5213
5450
  // src/components/merge-requests/ReviewMergeRequestActionButton.tsx
5214
5451
  import * as React29 from "react";
5215
- import { Pressable as Pressable10, View as View29 } from "react-native";
5452
+ import { Pressable as Pressable11, View as View29 } from "react-native";
5216
5453
  import { jsx as jsx41 } from "react/jsx-runtime";
5217
5454
  function ReviewMergeRequestActionButton({
5218
5455
  accessibilityLabel,
@@ -5244,7 +5481,7 @@ function ReviewMergeRequestActionButton({
5244
5481
  justifyContent: "center"
5245
5482
  },
5246
5483
  children: /* @__PURE__ */ jsx41(
5247
- Pressable10,
5484
+ Pressable11,
5248
5485
  {
5249
5486
  accessibilityRole: "button",
5250
5487
  accessibilityLabel,
@@ -5267,7 +5504,7 @@ function ReviewMergeRequestActionButton({
5267
5504
  }
5268
5505
 
5269
5506
  // src/components/merge-requests/ReviewMergeRequestCard.tsx
5270
- import { jsx as jsx42, jsxs as jsxs23 } from "react/jsx-runtime";
5507
+ import { jsx as jsx42, jsxs as jsxs24 } from "react/jsx-runtime";
5271
5508
  function ReviewMergeRequestCard({
5272
5509
  mr,
5273
5510
  index,
@@ -5291,7 +5528,7 @@ function ReviewMergeRequestCard({
5291
5528
  Animated8.timing(rotate, { toValue: isExpanded ? 1 : 0, duration: 200, useNativeDriver: true }).start();
5292
5529
  }, [isExpanded, rotate]);
5293
5530
  const position = total > 1 ? `${index + 1}/${total}` : "Merge request";
5294
- return /* @__PURE__ */ jsx42(Pressable11, { onPress: onToggle, style: ({ pressed }) => ({ opacity: pressed ? 0.95 : 1 }), children: /* @__PURE__ */ jsxs23(
5531
+ return /* @__PURE__ */ jsx42(Pressable12, { onPress: onToggle, style: ({ pressed }) => ({ opacity: pressed ? 0.95 : 1 }), children: /* @__PURE__ */ jsxs24(
5295
5532
  Card,
5296
5533
  {
5297
5534
  padded: false,
@@ -5304,9 +5541,9 @@ function ReviewMergeRequestCard({
5304
5541
  }
5305
5542
  ],
5306
5543
  children: [
5307
- /* @__PURE__ */ jsxs23(View30, { style: { flexDirection: "row", alignItems: "center", gap: 12 }, children: [
5544
+ /* @__PURE__ */ jsxs24(View30, { style: { flexDirection: "row", alignItems: "center", gap: 12 }, children: [
5308
5545
  /* @__PURE__ */ jsx42(Avatar, { size: 40, uri: (creator == null ? void 0 : creator.avatar) ?? null, name: (creator == null ? void 0 : creator.name) ?? void 0 }),
5309
- /* @__PURE__ */ jsxs23(View30, { style: { flex: 1, minWidth: 0 }, children: [
5546
+ /* @__PURE__ */ jsxs24(View30, { style: { flex: 1, minWidth: 0 }, children: [
5310
5547
  /* @__PURE__ */ jsx42(
5311
5548
  Text,
5312
5549
  {
@@ -5315,7 +5552,7 @@ function ReviewMergeRequestCard({
5315
5552
  children: mr.title ?? "Untitled merge request"
5316
5553
  }
5317
5554
  ),
5318
- /* @__PURE__ */ jsxs23(Text, { style: { color: theme.colors.textMuted, fontSize: 12, lineHeight: 16 }, numberOfLines: 1, children: [
5555
+ /* @__PURE__ */ jsxs24(Text, { style: { color: theme.colors.textMuted, fontSize: 12, lineHeight: 16 }, numberOfLines: 1, children: [
5319
5556
  (creator == null ? void 0 : creator.name) ?? "Loading...",
5320
5557
  " \xB7 ",
5321
5558
  position
@@ -5331,7 +5568,7 @@ function ReviewMergeRequestCard({
5331
5568
  }
5332
5569
  )
5333
5570
  ] }),
5334
- isExpanded ? /* @__PURE__ */ jsxs23(View30, { style: { marginTop: 16 }, children: [
5571
+ isExpanded ? /* @__PURE__ */ jsxs24(View30, { style: { marginTop: 16 }, children: [
5335
5572
  /* @__PURE__ */ jsx42(
5336
5573
  Text,
5337
5574
  {
@@ -5346,12 +5583,12 @@ function ReviewMergeRequestCard({
5346
5583
  children: status.text
5347
5584
  }
5348
5585
  ),
5349
- /* @__PURE__ */ jsx42(Text, { style: { color: theme.colors.textMuted, fontSize: 12, lineHeight: 16, marginBottom: 12 }, children: creator ? `${creator.approvedOpenedMergeRequests} approved merge${creator.approvedOpenedMergeRequests !== 1 ? "s" : ""}` : "Loading stats..." }),
5586
+ /* @__PURE__ */ jsx42(Text, { style: { color: theme.colors.textMuted, fontSize: 12, lineHeight: 16, marginBottom: 12 }, children: creator ? `${creator.approvedOrMergedMergeRequests} approved merge${creator.approvedOrMergedMergeRequests !== 1 ? "s" : ""}` : "Loading stats..." }),
5350
5587
  mr.description ? /* @__PURE__ */ jsx42(MarkdownText, { markdown: mr.description, variant: "mergeRequest" }) : null
5351
5588
  ] }) : null,
5352
5589
  /* @__PURE__ */ jsx42(View30, { style: { height: 1, backgroundColor: withAlpha(theme.colors.borderStrong, 0.5), marginTop: 12, marginBottom: 12 } }),
5353
- /* @__PURE__ */ jsxs23(View30, { style: { flexDirection: "row", alignItems: "center", justifyContent: "space-between" }, children: [
5354
- /* @__PURE__ */ jsxs23(View30, { style: { flexDirection: "row", gap: 8 }, children: [
5590
+ /* @__PURE__ */ jsxs24(View30, { style: { flexDirection: "row", alignItems: "center", justifyContent: "space-between" }, children: [
5591
+ /* @__PURE__ */ jsxs24(View30, { style: { flexDirection: "row", gap: 8 }, children: [
5355
5592
  /* @__PURE__ */ jsx42(
5356
5593
  ReviewMergeRequestActionButton,
5357
5594
  {
@@ -5360,7 +5597,7 @@ function ReviewMergeRequestCard({
5360
5597
  disabled: !canAct || isAnyProcessing,
5361
5598
  onPress: onReject,
5362
5599
  iconOnly: !isExpanded,
5363
- children: /* @__PURE__ */ jsxs23(View30, { style: { flexDirection: "row", alignItems: "center", gap: isExpanded ? 4 : 0 }, children: [
5600
+ children: /* @__PURE__ */ jsxs24(View30, { style: { flexDirection: "row", alignItems: "center", gap: isExpanded ? 4 : 0 }, children: [
5364
5601
  /* @__PURE__ */ jsx42(X3, { size: 18, color: "#FFFFFF" }),
5365
5602
  isExpanded ? /* @__PURE__ */ jsx42(Text, { style: { fontSize: 13, color: "#FFFFFF", fontWeight: theme.typography.fontWeight.semibold }, children: "Reject" }) : null
5366
5603
  ] })
@@ -5374,10 +5611,10 @@ function ReviewMergeRequestCard({
5374
5611
  disabled: !canAct || isAnyProcessing,
5375
5612
  onPress: onApprove,
5376
5613
  iconOnly: !isExpanded,
5377
- children: isProcessing ? /* @__PURE__ */ jsxs23(View30, { style: { flexDirection: "row", alignItems: "center", gap: isExpanded ? 4 : 0 }, children: [
5614
+ children: isProcessing ? /* @__PURE__ */ jsxs24(View30, { style: { flexDirection: "row", alignItems: "center", gap: isExpanded ? 4 : 0 }, children: [
5378
5615
  /* @__PURE__ */ jsx42(ActivityIndicator5, { size: "small", color: "#FFFFFF" }),
5379
5616
  isExpanded ? /* @__PURE__ */ jsx42(Text, { style: { fontSize: 13, color: "#FFFFFF", fontWeight: theme.typography.fontWeight.semibold }, children: "Processing" }) : null
5380
- ] }) : /* @__PURE__ */ jsxs23(View30, { style: { flexDirection: "row", alignItems: "center", gap: isExpanded ? 4 : 0 }, children: [
5617
+ ] }) : /* @__PURE__ */ jsxs24(View30, { style: { flexDirection: "row", alignItems: "center", gap: isExpanded ? 4 : 0 }, children: [
5381
5618
  /* @__PURE__ */ jsx42(Check4, { size: 18, color: "#FFFFFF" }),
5382
5619
  isExpanded ? /* @__PURE__ */ jsx42(Text, { style: { fontSize: 13, color: "#FFFFFF", fontWeight: theme.typography.fontWeight.semibold }, children: "Approve" }) : null
5383
5620
  ] })
@@ -5392,7 +5629,7 @@ function ReviewMergeRequestCard({
5392
5629
  disabled: isBuilding || isTestingThis,
5393
5630
  onPress: onTest,
5394
5631
  iconOnly: !isExpanded,
5395
- children: isTestingThis ? /* @__PURE__ */ jsx42(ActivityIndicator5, { size: "small", color: "#888" }) : /* @__PURE__ */ jsxs23(View30, { style: { flexDirection: "row", alignItems: "center", gap: isExpanded ? 4 : 0 }, children: [
5632
+ children: isTestingThis ? /* @__PURE__ */ jsx42(ActivityIndicator5, { size: "small", color: "#888" }) : /* @__PURE__ */ jsxs24(View30, { style: { flexDirection: "row", alignItems: "center", gap: isExpanded ? 4 : 0 }, children: [
5396
5633
  /* @__PURE__ */ jsx42(Play3, { size: 14, color: theme.colors.text }),
5397
5634
  isExpanded ? /* @__PURE__ */ jsx42(Text, { style: { fontSize: 13, color: theme.colors.text, fontWeight: theme.typography.fontWeight.semibold }, children: "Test" }) : null
5398
5635
  ] })
@@ -5405,7 +5642,7 @@ function ReviewMergeRequestCard({
5405
5642
  }
5406
5643
 
5407
5644
  // src/components/merge-requests/ReviewMergeRequestCarousel.tsx
5408
- import { jsx as jsx43, jsxs as jsxs24 } from "react/jsx-runtime";
5645
+ import { jsx as jsx43, jsxs as jsxs25 } from "react/jsx-runtime";
5409
5646
  function ReviewMergeRequestCarousel({
5410
5647
  mergeRequests,
5411
5648
  creatorStatsById,
@@ -5427,7 +5664,7 @@ function ReviewMergeRequestCarousel({
5427
5664
  const snapInterval = cardWidth + gap;
5428
5665
  const dotColor = theme.scheme === "dark" ? "#FFFFFF" : "#000000";
5429
5666
  if (mergeRequests.length === 0) return null;
5430
- return /* @__PURE__ */ jsxs24(View31, { style: [{ marginHorizontal: -theme.spacing.lg }, style], children: [
5667
+ return /* @__PURE__ */ jsxs25(View31, { style: [{ marginHorizontal: -theme.spacing.lg }, style], children: [
5431
5668
  /* @__PURE__ */ jsx43(
5432
5669
  FlatList,
5433
5670
  {
@@ -5506,7 +5743,7 @@ function ReviewMergeRequestCarousel({
5506
5743
  }
5507
5744
 
5508
5745
  // src/studio/ui/preview-panel/PreviewCollaborateSection.tsx
5509
- import { Fragment as Fragment5, jsx as jsx44, jsxs as jsxs25 } from "react/jsx-runtime";
5746
+ import { Fragment as Fragment5, jsx as jsx44, jsxs as jsxs26 } from "react/jsx-runtime";
5510
5747
  function PreviewCollaborateSection({
5511
5748
  canSubmitMergeRequest,
5512
5749
  canSyncUpstream,
@@ -5532,7 +5769,7 @@ function PreviewCollaborateSection({
5532
5769
  if (!hasSection) return null;
5533
5770
  const isSyncing = Boolean(syncingUpstream || syncingLocal);
5534
5771
  const showActionsSubtitle = canSubmitMergeRequest && onSubmitMergeRequest || canSyncUpstream && onSyncUpstream || onTestMr && incomingMergeRequests.length > 0;
5535
- return /* @__PURE__ */ jsxs25(Fragment5, { children: [
5772
+ return /* @__PURE__ */ jsxs26(Fragment5, { children: [
5536
5773
  /* @__PURE__ */ jsx44(SectionTitle, { marginTop: theme.spacing.xl, children: "Collaborate" }),
5537
5774
  showActionsSubtitle ? /* @__PURE__ */ jsx44(
5538
5775
  Text,
@@ -5671,7 +5908,7 @@ function PreviewCollaborateSection({
5671
5908
  onTest: (mr) => onTestMr ? onTestMr(mr) : void 0
5672
5909
  }
5673
5910
  ) : null,
5674
- outgoingMergeRequests.length > 0 ? /* @__PURE__ */ jsxs25(Fragment5, { children: [
5911
+ outgoingMergeRequests.length > 0 ? /* @__PURE__ */ jsxs26(Fragment5, { children: [
5675
5912
  /* @__PURE__ */ jsx44(
5676
5913
  Text,
5677
5914
  {
@@ -5986,7 +6223,7 @@ function usePreviewPanelData(params) {
5986
6223
  }
5987
6224
 
5988
6225
  // src/studio/ui/PreviewPanel.tsx
5989
- import { jsx as jsx45, jsxs as jsxs26 } from "react/jsx-runtime";
6226
+ import { jsx as jsx45, jsxs as jsxs27 } from "react/jsx-runtime";
5990
6227
  function PreviewPanel({
5991
6228
  app,
5992
6229
  loading,
@@ -6021,7 +6258,7 @@ ${shareUrl}` : `Check out this app on Remix
6021
6258
  ${shareUrl}`;
6022
6259
  try {
6023
6260
  const title = app.name ?? "Remix app";
6024
- const payload = Platform8.OS === "ios" ? {
6261
+ const payload = Platform7.OS === "ios" ? {
6025
6262
  title,
6026
6263
  message
6027
6264
  } : {
@@ -6063,13 +6300,13 @@ ${shareUrl}`;
6063
6300
  }
6064
6301
  );
6065
6302
  if (loading || !app) {
6066
- return /* @__PURE__ */ jsx45(PreviewPage, { header, children: /* @__PURE__ */ jsxs26(View33, { style: { flex: 1, justifyContent: "center", alignItems: "center", padding: 24 }, children: [
6303
+ return /* @__PURE__ */ jsx45(PreviewPage, { header, children: /* @__PURE__ */ jsxs27(View33, { style: { flex: 1, justifyContent: "center", alignItems: "center", padding: 24 }, children: [
6067
6304
  /* @__PURE__ */ jsx45(ActivityIndicator7, {}),
6068
6305
  /* @__PURE__ */ jsx45(View33, { style: { height: 12 } }),
6069
6306
  /* @__PURE__ */ jsx45(Text, { variant: "bodyMuted", children: "Loading app\u2026" })
6070
6307
  ] }) });
6071
6308
  }
6072
- return /* @__PURE__ */ jsxs26(PreviewPage, { header, children: [
6309
+ return /* @__PURE__ */ jsxs27(PreviewPage, { header, children: [
6073
6310
  /* @__PURE__ */ jsx45(
6074
6311
  PreviewHeroSection,
6075
6312
  {
@@ -6130,7 +6367,7 @@ import { ActivityIndicator as ActivityIndicator9, View as View42 } from "react-n
6130
6367
 
6131
6368
  // src/components/chat/ChatPage.tsx
6132
6369
  import * as React38 from "react";
6133
- import { Keyboard as Keyboard4, Platform as Platform10, View as View37 } from "react-native";
6370
+ import { Platform as Platform9, View as View37 } from "react-native";
6134
6371
  import { useSafeAreaInsets as useSafeAreaInsets4 } from "react-native-safe-area-context";
6135
6372
 
6136
6373
  // src/components/chat/ChatMessageList.tsx
@@ -6140,9 +6377,76 @@ import { BottomSheetFlatList } from "@gorhom/bottom-sheet";
6140
6377
 
6141
6378
  // src/components/chat/ChatMessageBubble.tsx
6142
6379
  import { View as View34 } from "react-native";
6143
- import { CheckCheck as CheckCheck2, GitMerge as GitMerge2 } from "lucide-react-native";
6144
- import { jsx as jsx46, jsxs as jsxs27 } from "react/jsx-runtime";
6145
- function ChatMessageBubble({ message, renderContent, style }) {
6380
+ import { CheckCheck as CheckCheck2, GitMerge as GitMerge2, RotateCcw } from "lucide-react-native";
6381
+
6382
+ // src/components/primitives/Button.tsx
6383
+ import {
6384
+ Pressable as Pressable13
6385
+ } from "react-native";
6386
+ import { jsx as jsx46 } from "react/jsx-runtime";
6387
+ function backgroundFor2(variant, theme, pressed, disabled) {
6388
+ const { colors } = theme;
6389
+ if (variant === "ghost") return "transparent";
6390
+ if (disabled) {
6391
+ return colors.neutral;
6392
+ }
6393
+ const base = variant === "primary" ? colors.primary : variant === "danger" ? colors.danger : colors.neutral;
6394
+ if (!pressed) return base;
6395
+ return base;
6396
+ }
6397
+ function borderFor(variant, theme) {
6398
+ if (variant !== "ghost") return {};
6399
+ return { borderWidth: 1, borderColor: theme.colors.border };
6400
+ }
6401
+ function paddingFor(size, theme) {
6402
+ switch (size) {
6403
+ case "sm":
6404
+ return { paddingHorizontal: theme.spacing.md, paddingVertical: theme.spacing.sm, minHeight: 36 };
6405
+ case "icon":
6406
+ return { paddingHorizontal: 0, paddingVertical: 0, minHeight: 44, minWidth: 44 };
6407
+ case "md":
6408
+ default:
6409
+ return { paddingHorizontal: theme.spacing.lg, paddingVertical: theme.spacing.md, minHeight: 44 };
6410
+ }
6411
+ }
6412
+ function Button({
6413
+ variant = "neutral",
6414
+ size = "md",
6415
+ disabled,
6416
+ style,
6417
+ children,
6418
+ ...props
6419
+ }) {
6420
+ const theme = useTheme();
6421
+ const isDisabled = disabled ?? void 0;
6422
+ return /* @__PURE__ */ jsx46(
6423
+ Pressable13,
6424
+ {
6425
+ ...props,
6426
+ disabled: isDisabled,
6427
+ style: (state) => {
6428
+ const pressed = state.pressed;
6429
+ const base = {
6430
+ alignItems: "center",
6431
+ justifyContent: "center",
6432
+ flexDirection: "row",
6433
+ borderRadius: size === "icon" ? theme.radii.pill : theme.radii.pill,
6434
+ backgroundColor: backgroundFor2(variant, theme, pressed, isDisabled),
6435
+ opacity: pressed && !isDisabled ? 0.92 : 1,
6436
+ ...paddingFor(size, theme),
6437
+ ...borderFor(variant, theme)
6438
+ };
6439
+ const resolved = typeof style === "function" ? style({ pressed, disabled: isDisabled }) : style;
6440
+ return [base, resolved];
6441
+ },
6442
+ children
6443
+ }
6444
+ );
6445
+ }
6446
+
6447
+ // src/components/chat/ChatMessageBubble.tsx
6448
+ import { jsx as jsx47, jsxs as jsxs28 } from "react/jsx-runtime";
6449
+ function ChatMessageBubble({ message, renderContent, isLast, retrying, onRetry, style }) {
6146
6450
  var _a, _b;
6147
6451
  const theme = useTheme();
6148
6452
  const metaEvent = ((_a = message.meta) == null ? void 0 : _a.event) ?? null;
@@ -6157,34 +6461,62 @@ function ChatMessageBubble({ message, renderContent, style }) {
6157
6461
  const bubbleVariant = isHuman ? "surface" : "surfaceRaised";
6158
6462
  const cornerStyle = isHuman ? { borderTopRightRadius: 0 } : { borderTopLeftRadius: 0 };
6159
6463
  const bodyColor = metaStatus === "success" ? theme.colors.success : metaStatus === "error" ? theme.colors.danger : void 0;
6160
- return /* @__PURE__ */ jsx46(View34, { style: [align, style], children: /* @__PURE__ */ jsx46(
6161
- Surface,
6162
- {
6163
- variant: bubbleVariant,
6164
- style: [
6165
- {
6166
- maxWidth: "85%",
6167
- borderRadius: theme.radii.lg,
6168
- paddingHorizontal: theme.spacing.lg,
6169
- paddingVertical: theme.spacing.md,
6170
- borderWidth: 1,
6171
- borderColor: theme.colors.border
6172
- },
6173
- cornerStyle
6174
- ],
6175
- children: /* @__PURE__ */ jsxs27(View34, { style: { flexDirection: "row", alignItems: "center" }, children: [
6176
- isMergeCompleted || isSyncCompleted ? /* @__PURE__ */ jsx46(CheckCheck2, { size: 16, color: theme.colors.success, style: { marginRight: theme.spacing.sm } }) : null,
6177
- isMergeApproved || isSyncStarted ? /* @__PURE__ */ jsx46(GitMerge2, { size: 16, color: theme.colors.text, style: { marginRight: theme.spacing.sm } }) : null,
6178
- /* @__PURE__ */ jsx46(View34, { style: { flexShrink: 1, minWidth: 0 }, children: renderContent ? renderContent(message) : /* @__PURE__ */ jsx46(MarkdownText, { markdown: message.content, variant: "chat", bodyColor }) })
6179
- ] })
6180
- }
6181
- ) });
6464
+ const showRetry = Boolean(onRetry) && isLast && metaStatus === "error" && message.author === "human";
6465
+ const retryLabel = retrying ? "Retrying..." : "Retry";
6466
+ return /* @__PURE__ */ jsxs28(View34, { style: [align, style], children: [
6467
+ /* @__PURE__ */ jsx47(
6468
+ Surface,
6469
+ {
6470
+ variant: bubbleVariant,
6471
+ style: [
6472
+ {
6473
+ maxWidth: "85%",
6474
+ borderRadius: theme.radii.lg,
6475
+ paddingHorizontal: theme.spacing.lg,
6476
+ paddingVertical: theme.spacing.md,
6477
+ borderWidth: 1,
6478
+ borderColor: theme.colors.border
6479
+ },
6480
+ cornerStyle
6481
+ ],
6482
+ children: /* @__PURE__ */ jsxs28(View34, { style: { flexDirection: "row", alignItems: "center" }, children: [
6483
+ isMergeCompleted || isSyncCompleted ? /* @__PURE__ */ jsx47(CheckCheck2, { size: 16, color: theme.colors.success, style: { marginRight: theme.spacing.sm } }) : null,
6484
+ isMergeApproved || isSyncStarted ? /* @__PURE__ */ jsx47(GitMerge2, { size: 16, color: theme.colors.text, style: { marginRight: theme.spacing.sm } }) : null,
6485
+ /* @__PURE__ */ jsx47(View34, { style: { flexShrink: 1, minWidth: 0 }, children: renderContent ? renderContent(message) : /* @__PURE__ */ jsx47(MarkdownText, { markdown: message.content, variant: "chat", bodyColor }) })
6486
+ ] })
6487
+ }
6488
+ ),
6489
+ showRetry ? /* @__PURE__ */ jsx47(View34, { style: { marginTop: theme.spacing.xs, alignSelf: align.alignSelf }, children: /* @__PURE__ */ jsx47(
6490
+ Button,
6491
+ {
6492
+ variant: "ghost",
6493
+ size: "sm",
6494
+ onPress: onRetry,
6495
+ disabled: retrying,
6496
+ style: { borderColor: theme.colors.danger },
6497
+ accessibilityLabel: "Retry send",
6498
+ children: /* @__PURE__ */ jsxs28(View34, { style: { flexDirection: "row", alignItems: "center" }, children: [
6499
+ !retrying ? /* @__PURE__ */ jsx47(RotateCcw, { size: 14, color: theme.colors.danger }) : null,
6500
+ /* @__PURE__ */ jsx47(
6501
+ Text,
6502
+ {
6503
+ variant: "caption",
6504
+ color: theme.colors.danger,
6505
+ style: { marginLeft: retrying ? 0 : theme.spacing.xs },
6506
+ numberOfLines: 1,
6507
+ children: retryLabel
6508
+ }
6509
+ )
6510
+ ] })
6511
+ }
6512
+ ) }) : null
6513
+ ] });
6182
6514
  }
6183
6515
 
6184
6516
  // src/components/chat/TypingIndicator.tsx
6185
6517
  import * as React36 from "react";
6186
6518
  import { Animated as Animated10, View as View35 } from "react-native";
6187
- import { jsx as jsx47 } from "react/jsx-runtime";
6519
+ import { jsx as jsx48 } from "react/jsx-runtime";
6188
6520
  function TypingIndicator({ style }) {
6189
6521
  const theme = useTheme();
6190
6522
  const dotColor = theme.colors.textSubtle;
@@ -6207,7 +6539,7 @@ function TypingIndicator({ style }) {
6207
6539
  loops.forEach((l) => l.stop());
6208
6540
  };
6209
6541
  }, [anims]);
6210
- return /* @__PURE__ */ jsx47(View35, { style: [{ flexDirection: "row", alignItems: "center" }, style], children: anims.map((a, i) => /* @__PURE__ */ jsx47(
6542
+ return /* @__PURE__ */ jsx48(View35, { style: [{ flexDirection: "row", alignItems: "center" }, style], children: anims.map((a, i) => /* @__PURE__ */ jsx48(
6211
6543
  Animated10.View,
6212
6544
  {
6213
6545
  style: {
@@ -6225,12 +6557,14 @@ function TypingIndicator({ style }) {
6225
6557
  }
6226
6558
 
6227
6559
  // src/components/chat/ChatMessageList.tsx
6228
- import { jsx as jsx48, jsxs as jsxs28 } from "react/jsx-runtime";
6560
+ import { jsx as jsx49, jsxs as jsxs29 } from "react/jsx-runtime";
6229
6561
  var ChatMessageList = React37.forwardRef(
6230
6562
  ({
6231
6563
  messages,
6232
6564
  showTypingIndicator = false,
6233
6565
  renderMessageContent,
6566
+ onRetryMessage,
6567
+ isRetryingMessage,
6234
6568
  contentStyle,
6235
6569
  bottomInset = 0,
6236
6570
  onNearBottomChange,
@@ -6244,6 +6578,7 @@ var ChatMessageList = React37.forwardRef(
6244
6578
  const data = React37.useMemo(() => {
6245
6579
  return [...messages].reverse();
6246
6580
  }, [messages]);
6581
+ const lastMessageId = messages.length > 0 ? messages[messages.length - 1].id : null;
6247
6582
  const scrollToBottom = React37.useCallback((options) => {
6248
6583
  var _a;
6249
6584
  const animated = (options == null ? void 0 : options.animated) ?? true;
@@ -6279,7 +6614,7 @@ var ChatMessageList = React37.forwardRef(
6279
6614
  }
6280
6615
  return void 0;
6281
6616
  }, [showTypingIndicator, scrollToBottom]);
6282
- return /* @__PURE__ */ jsx48(
6617
+ return /* @__PURE__ */ jsx49(
6283
6618
  BottomSheetFlatList,
6284
6619
  {
6285
6620
  ref: listRef,
@@ -6305,11 +6640,20 @@ var ChatMessageList = React37.forwardRef(
6305
6640
  },
6306
6641
  contentStyle
6307
6642
  ],
6308
- ItemSeparatorComponent: () => /* @__PURE__ */ jsx48(View36, { style: { height: theme.spacing.sm } }),
6309
- renderItem: ({ item }) => /* @__PURE__ */ jsx48(ChatMessageBubble, { message: item, renderContent: renderMessageContent }),
6310
- ListHeaderComponent: /* @__PURE__ */ jsxs28(View36, { children: [
6311
- showTypingIndicator ? /* @__PURE__ */ jsx48(View36, { style: { marginTop: theme.spacing.sm, alignSelf: "flex-start", paddingHorizontal: theme.spacing.lg }, children: /* @__PURE__ */ jsx48(TypingIndicator, {}) }) : null,
6312
- bottomInset > 0 ? /* @__PURE__ */ jsx48(View36, { style: { height: bottomInset } }) : null
6643
+ ItemSeparatorComponent: () => /* @__PURE__ */ jsx49(View36, { style: { height: theme.spacing.sm } }),
6644
+ renderItem: ({ item }) => /* @__PURE__ */ jsx49(
6645
+ ChatMessageBubble,
6646
+ {
6647
+ message: item,
6648
+ renderContent: renderMessageContent,
6649
+ isLast: Boolean(lastMessageId && item.id === lastMessageId),
6650
+ retrying: (isRetryingMessage == null ? void 0 : isRetryingMessage(item.id)) ?? false,
6651
+ onRetry: onRetryMessage ? () => onRetryMessage(item.id) : void 0
6652
+ }
6653
+ ),
6654
+ ListHeaderComponent: /* @__PURE__ */ jsxs29(View36, { children: [
6655
+ showTypingIndicator ? /* @__PURE__ */ jsx49(View36, { style: { marginTop: theme.spacing.sm, alignSelf: "flex-start", paddingHorizontal: theme.spacing.lg }, children: /* @__PURE__ */ jsx49(TypingIndicator, {}) }) : null,
6656
+ bottomInset > 0 ? /* @__PURE__ */ jsx49(View36, { style: { height: bottomInset } }) : null
6313
6657
  ] })
6314
6658
  }
6315
6659
  );
@@ -6318,12 +6662,14 @@ var ChatMessageList = React37.forwardRef(
6318
6662
  ChatMessageList.displayName = "ChatMessageList";
6319
6663
 
6320
6664
  // src/components/chat/ChatPage.tsx
6321
- import { jsx as jsx49, jsxs as jsxs29 } from "react/jsx-runtime";
6665
+ import { jsx as jsx50, jsxs as jsxs30 } from "react/jsx-runtime";
6322
6666
  function ChatPage({
6323
6667
  header,
6324
6668
  messages,
6325
6669
  showTypingIndicator,
6326
6670
  renderMessageContent,
6671
+ onRetryMessage,
6672
+ isRetryingMessage,
6327
6673
  topBanner,
6328
6674
  composerTop,
6329
6675
  composer,
@@ -6337,17 +6683,7 @@ function ChatPage({
6337
6683
  const insets = useSafeAreaInsets4();
6338
6684
  const [composerHeight, setComposerHeight] = React38.useState(0);
6339
6685
  const [composerTopHeight, setComposerTopHeight] = React38.useState(0);
6340
- const [keyboardVisible, setKeyboardVisible] = React38.useState(false);
6341
- React38.useEffect(() => {
6342
- if (Platform10.OS !== "ios") return;
6343
- const show = Keyboard4.addListener("keyboardWillShow", () => setKeyboardVisible(true));
6344
- const hide = Keyboard4.addListener("keyboardWillHide", () => setKeyboardVisible(false));
6345
- return () => {
6346
- show.remove();
6347
- hide.remove();
6348
- };
6349
- }, []);
6350
- const footerBottomPadding = Platform10.OS === "ios" ? keyboardVisible ? 0 : insets.bottom : insets.bottom + 10;
6686
+ const footerBottomPadding = Platform9.OS === "ios" ? insets.bottom : insets.bottom + 10;
6351
6687
  const totalComposerHeight = composerHeight + composerTopHeight;
6352
6688
  const overlayBottom = totalComposerHeight + footerBottomPadding + theme.spacing.lg;
6353
6689
  const bottomInset = totalComposerHeight + footerBottomPadding + theme.spacing.xl;
@@ -6364,22 +6700,24 @@ function ChatPage({
6364
6700
  if (composerTop) return;
6365
6701
  setComposerTopHeight(0);
6366
6702
  }, [composerTop]);
6367
- return /* @__PURE__ */ jsxs29(View37, { style: [{ flex: 1 }, style], children: [
6368
- header ? /* @__PURE__ */ jsx49(View37, { children: header }) : null,
6369
- topBanner ? /* @__PURE__ */ jsx49(View37, { style: { paddingHorizontal: theme.spacing.lg, paddingTop: theme.spacing.sm }, children: topBanner }) : null,
6370
- /* @__PURE__ */ jsxs29(View37, { style: { flex: 1 }, children: [
6371
- /* @__PURE__ */ jsxs29(
6703
+ return /* @__PURE__ */ jsxs30(View37, { style: [{ flex: 1 }, style], children: [
6704
+ header ? /* @__PURE__ */ jsx50(View37, { children: header }) : null,
6705
+ topBanner ? /* @__PURE__ */ jsx50(View37, { style: { paddingHorizontal: theme.spacing.lg, paddingTop: theme.spacing.sm }, children: topBanner }) : null,
6706
+ /* @__PURE__ */ jsxs30(View37, { style: { flex: 1 }, children: [
6707
+ /* @__PURE__ */ jsxs30(
6372
6708
  View37,
6373
6709
  {
6374
6710
  style: { flex: 1 },
6375
6711
  children: [
6376
- /* @__PURE__ */ jsx49(
6712
+ /* @__PURE__ */ jsx50(
6377
6713
  ChatMessageList,
6378
6714
  {
6379
6715
  ref: listRef,
6380
6716
  messages,
6381
6717
  showTypingIndicator,
6382
6718
  renderMessageContent,
6719
+ onRetryMessage,
6720
+ isRetryingMessage,
6383
6721
  onNearBottomChange,
6384
6722
  bottomInset
6385
6723
  }
@@ -6388,7 +6726,7 @@ function ChatPage({
6388
6726
  ]
6389
6727
  }
6390
6728
  ),
6391
- /* @__PURE__ */ jsxs29(
6729
+ /* @__PURE__ */ jsxs30(
6392
6730
  View37,
6393
6731
  {
6394
6732
  style: {
@@ -6401,7 +6739,7 @@ function ChatPage({
6401
6739
  paddingBottom: footerBottomPadding
6402
6740
  },
6403
6741
  children: [
6404
- composerTop ? /* @__PURE__ */ jsx49(
6742
+ composerTop ? /* @__PURE__ */ jsx50(
6405
6743
  View37,
6406
6744
  {
6407
6745
  style: { marginBottom: theme.spacing.sm },
@@ -6409,7 +6747,7 @@ function ChatPage({
6409
6747
  children: composerTop
6410
6748
  }
6411
6749
  ) : null,
6412
- /* @__PURE__ */ jsx49(
6750
+ /* @__PURE__ */ jsx50(
6413
6751
  ChatComposer,
6414
6752
  {
6415
6753
  ...composer,
@@ -6426,9 +6764,9 @@ function ChatPage({
6426
6764
 
6427
6765
  // src/components/chat/ScrollToBottomButton.tsx
6428
6766
  import * as React39 from "react";
6429
- import { Pressable as Pressable12, View as View38 } from "react-native";
6767
+ import { Pressable as Pressable14, View as View38 } from "react-native";
6430
6768
  import Animated11, { Easing as Easing2, useAnimatedStyle as useAnimatedStyle2, useSharedValue as useSharedValue2, withTiming as withTiming2 } from "react-native-reanimated";
6431
- import { jsx as jsx50 } from "react/jsx-runtime";
6769
+ import { jsx as jsx51 } from "react/jsx-runtime";
6432
6770
  function ScrollToBottomButton({ visible, onPress, children, style }) {
6433
6771
  const theme = useTheme();
6434
6772
  const progress = useSharedValue2(visible ? 1 : 0);
@@ -6442,7 +6780,7 @@ function ScrollToBottomButton({ visible, onPress, children, style }) {
6442
6780
  }));
6443
6781
  const bg = theme.scheme === "dark" ? "rgba(39,39,42,0.9)" : "rgba(244,244,245,0.95)";
6444
6782
  const border = theme.scheme === "dark" ? withAlpha("#FFFFFF", 0.12) : withAlpha("#000000", 0.08);
6445
- return /* @__PURE__ */ jsx50(
6783
+ return /* @__PURE__ */ jsx51(
6446
6784
  Animated11.View,
6447
6785
  {
6448
6786
  pointerEvents: visible ? "auto" : "none",
@@ -6456,7 +6794,7 @@ function ScrollToBottomButton({ visible, onPress, children, style }) {
6456
6794
  style,
6457
6795
  animStyle
6458
6796
  ],
6459
- children: /* @__PURE__ */ jsx50(
6797
+ children: /* @__PURE__ */ jsx51(
6460
6798
  View38,
6461
6799
  {
6462
6800
  style: {
@@ -6475,8 +6813,8 @@ function ScrollToBottomButton({ visible, onPress, children, style }) {
6475
6813
  elevation: 5,
6476
6814
  opacity: pressed ? 0.85 : 1
6477
6815
  },
6478
- children: /* @__PURE__ */ jsx50(
6479
- Pressable12,
6816
+ children: /* @__PURE__ */ jsx51(
6817
+ Pressable14,
6480
6818
  {
6481
6819
  onPress,
6482
6820
  onPressIn: () => setPressed(true),
@@ -6494,7 +6832,7 @@ function ScrollToBottomButton({ visible, onPress, children, style }) {
6494
6832
 
6495
6833
  // src/components/chat/ChatHeader.tsx
6496
6834
  import { StyleSheet as StyleSheet4 } from "react-native";
6497
- import { jsx as jsx51 } from "react/jsx-runtime";
6835
+ import { jsx as jsx52 } from "react/jsx-runtime";
6498
6836
  function ChatHeader({ left, right, center, style }) {
6499
6837
  const flattenedStyle = StyleSheet4.flatten([
6500
6838
  {
@@ -6502,7 +6840,7 @@ function ChatHeader({ left, right, center, style }) {
6502
6840
  },
6503
6841
  style
6504
6842
  ]);
6505
- return /* @__PURE__ */ jsx51(
6843
+ return /* @__PURE__ */ jsx52(
6506
6844
  StudioSheetHeader,
6507
6845
  {
6508
6846
  left,
@@ -6515,12 +6853,12 @@ function ChatHeader({ left, right, center, style }) {
6515
6853
 
6516
6854
  // src/components/chat/ForkNoticeBanner.tsx
6517
6855
  import { View as View40 } from "react-native";
6518
- import { jsx as jsx52, jsxs as jsxs30 } from "react/jsx-runtime";
6856
+ import { jsx as jsx53, jsxs as jsxs31 } from "react/jsx-runtime";
6519
6857
  function ForkNoticeBanner({ isOwner = true, title, description, style }) {
6520
6858
  const theme = useTheme();
6521
6859
  const resolvedTitle = title ?? (isOwner ? "Remixed app" : "Remix app");
6522
6860
  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.");
6523
- return /* @__PURE__ */ jsx52(
6861
+ return /* @__PURE__ */ jsx53(
6524
6862
  Card,
6525
6863
  {
6526
6864
  variant: "surfaceRaised",
@@ -6535,8 +6873,8 @@ function ForkNoticeBanner({ isOwner = true, title, description, style }) {
6535
6873
  },
6536
6874
  style
6537
6875
  ],
6538
- children: /* @__PURE__ */ jsxs30(View40, { style: { minWidth: 0 }, children: [
6539
- /* @__PURE__ */ jsx52(
6876
+ children: /* @__PURE__ */ jsxs31(View40, { style: { minWidth: 0 }, children: [
6877
+ /* @__PURE__ */ jsx53(
6540
6878
  Text,
6541
6879
  {
6542
6880
  style: {
@@ -6550,7 +6888,7 @@ function ForkNoticeBanner({ isOwner = true, title, description, style }) {
6550
6888
  children: resolvedTitle
6551
6889
  }
6552
6890
  ),
6553
- /* @__PURE__ */ jsx52(
6891
+ /* @__PURE__ */ jsx53(
6554
6892
  Text,
6555
6893
  {
6556
6894
  style: {
@@ -6569,8 +6907,8 @@ function ForkNoticeBanner({ isOwner = true, title, description, style }) {
6569
6907
 
6570
6908
  // src/components/chat/ChatQueue.tsx
6571
6909
  import * as React40 from "react";
6572
- import { ActivityIndicator as ActivityIndicator8, Pressable as Pressable13, View as View41 } from "react-native";
6573
- import { jsx as jsx53, jsxs as jsxs31 } from "react/jsx-runtime";
6910
+ import { ActivityIndicator as ActivityIndicator8, Pressable as Pressable15, View as View41 } from "react-native";
6911
+ import { jsx as jsx54, jsxs as jsxs32 } from "react/jsx-runtime";
6574
6912
  function ChatQueue({ items, onRemove }) {
6575
6913
  const theme = useTheme();
6576
6914
  const [expanded, setExpanded] = React40.useState({});
@@ -6602,7 +6940,7 @@ ${trimmedLine2}\u2026 `;
6602
6940
  setRemoving((prev) => Object.fromEntries(Object.entries(prev).filter(([id]) => ids.has(id))));
6603
6941
  }, [items]);
6604
6942
  if (items.length === 0) return null;
6605
- return /* @__PURE__ */ jsxs31(
6943
+ return /* @__PURE__ */ jsxs32(
6606
6944
  View41,
6607
6945
  {
6608
6946
  style: {
@@ -6614,15 +6952,15 @@ ${trimmedLine2}\u2026 `;
6614
6952
  backgroundColor: "transparent"
6615
6953
  },
6616
6954
  children: [
6617
- /* @__PURE__ */ jsx53(Text, { variant: "caption", style: { marginBottom: theme.spacing.sm }, children: "Queue" }),
6618
- /* @__PURE__ */ jsx53(View41, { style: { gap: theme.spacing.sm }, children: items.map((item) => {
6955
+ /* @__PURE__ */ jsx54(Text, { variant: "caption", style: { marginBottom: theme.spacing.sm }, children: "Queue" }),
6956
+ /* @__PURE__ */ jsx54(View41, { style: { gap: theme.spacing.sm }, children: items.map((item) => {
6619
6957
  const isExpanded = Boolean(expanded[item.id]);
6620
6958
  const showToggle = Boolean(canExpand[item.id]);
6621
6959
  const prompt = item.prompt ?? "";
6622
6960
  const moreLabel = "more";
6623
6961
  const displayPrompt = !isExpanded && showToggle && collapsedText[item.id] ? collapsedText[item.id] : prompt;
6624
6962
  const isRemoving = Boolean(removing[item.id]);
6625
- return /* @__PURE__ */ jsxs31(
6963
+ return /* @__PURE__ */ jsxs32(
6626
6964
  View41,
6627
6965
  {
6628
6966
  style: {
@@ -6635,8 +6973,8 @@ ${trimmedLine2}\u2026 `;
6635
6973
  backgroundColor: withAlpha(theme.colors.surface, theme.scheme === "dark" ? 0.8 : 0.9)
6636
6974
  },
6637
6975
  children: [
6638
- /* @__PURE__ */ jsxs31(View41, { style: { flex: 1 }, children: [
6639
- !canExpand[item.id] ? /* @__PURE__ */ jsx53(
6976
+ /* @__PURE__ */ jsxs32(View41, { style: { flex: 1 }, children: [
6977
+ !canExpand[item.id] ? /* @__PURE__ */ jsx54(
6640
6978
  Text,
6641
6979
  {
6642
6980
  style: { position: "absolute", opacity: 0, zIndex: -1, width: "100%" },
@@ -6655,14 +6993,14 @@ ${trimmedLine2}\u2026 `;
6655
6993
  children: prompt
6656
6994
  }
6657
6995
  ) : null,
6658
- /* @__PURE__ */ jsxs31(
6996
+ /* @__PURE__ */ jsxs32(
6659
6997
  Text,
6660
6998
  {
6661
6999
  variant: "bodyMuted",
6662
7000
  numberOfLines: isExpanded ? void 0 : 2,
6663
7001
  children: [
6664
7002
  displayPrompt,
6665
- !isExpanded && showToggle ? /* @__PURE__ */ jsx53(
7003
+ !isExpanded && showToggle ? /* @__PURE__ */ jsx54(
6666
7004
  Text,
6667
7005
  {
6668
7006
  color: theme.colors.text,
@@ -6674,18 +7012,18 @@ ${trimmedLine2}\u2026 `;
6674
7012
  ]
6675
7013
  }
6676
7014
  ),
6677
- showToggle && isExpanded ? /* @__PURE__ */ jsx53(
6678
- Pressable13,
7015
+ showToggle && isExpanded ? /* @__PURE__ */ jsx54(
7016
+ Pressable15,
6679
7017
  {
6680
7018
  onPress: () => setExpanded((prev) => ({ ...prev, [item.id]: false })),
6681
7019
  hitSlop: 6,
6682
7020
  style: { alignSelf: "flex-start", marginTop: 4 },
6683
- children: /* @__PURE__ */ jsx53(Text, { variant: "captionMuted", color: theme.colors.text, children: "less" })
7021
+ children: /* @__PURE__ */ jsx54(Text, { variant: "captionMuted", color: theme.colors.text, children: "less" })
6684
7022
  }
6685
7023
  ) : null
6686
7024
  ] }),
6687
- /* @__PURE__ */ jsx53(
6688
- Pressable13,
7025
+ /* @__PURE__ */ jsx54(
7026
+ Pressable15,
6689
7027
  {
6690
7028
  onPress: () => {
6691
7029
  if (!onRemove || isRemoving) return;
@@ -6700,7 +7038,7 @@ ${trimmedLine2}\u2026 `;
6700
7038
  },
6701
7039
  hitSlop: 8,
6702
7040
  style: { alignSelf: "center" },
6703
- children: isRemoving ? /* @__PURE__ */ jsx53(ActivityIndicator8, { size: "small", color: theme.colors.text }) : /* @__PURE__ */ jsx53(IconClose, { size: 14, colorToken: "text" })
7041
+ children: isRemoving ? /* @__PURE__ */ jsx54(ActivityIndicator8, { size: "small", color: theme.colors.text }) : /* @__PURE__ */ jsx54(IconClose, { size: 14, colorToken: "text" })
6704
7042
  }
6705
7043
  )
6706
7044
  ]
@@ -6714,10 +7052,9 @@ ${trimmedLine2}\u2026 `;
6714
7052
  }
6715
7053
 
6716
7054
  // src/studio/ui/ChatPanel.tsx
6717
- import { jsx as jsx54, jsxs as jsxs32 } from "react/jsx-runtime";
7055
+ import { jsx as jsx55, jsxs as jsxs33 } from "react/jsx-runtime";
6718
7056
  function ChatPanel({
6719
7057
  title = "Chat",
6720
- autoFocusComposer = false,
6721
7058
  messages,
6722
7059
  showTypingIndicator,
6723
7060
  loading,
@@ -6733,6 +7070,8 @@ function ChatPanel({
6733
7070
  onNavigateHome,
6734
7071
  onStartDraw,
6735
7072
  onSend,
7073
+ onRetryMessage,
7074
+ isRetryingMessage,
6736
7075
  queueItems = [],
6737
7076
  onRemoveQueueItem
6738
7077
  }) {
@@ -6756,21 +7095,21 @@ function ChatPanel({
6756
7095
  var _a;
6757
7096
  (_a = listRef.current) == null ? void 0 : _a.scrollToBottom({ animated: true });
6758
7097
  }, []);
6759
- const header = /* @__PURE__ */ jsx54(
7098
+ const header = /* @__PURE__ */ jsx55(
6760
7099
  ChatHeader,
6761
7100
  {
6762
- left: /* @__PURE__ */ jsxs32(View42, { style: { flexDirection: "row", alignItems: "center" }, children: [
6763
- /* @__PURE__ */ jsx54(StudioSheetHeaderIconButton, { onPress: onBack, accessibilityLabel: "Back", style: { marginRight: 8 }, children: /* @__PURE__ */ jsx54(IconBack, { size: 20, colorToken: "floatingContent" }) }),
6764
- onNavigateHome ? /* @__PURE__ */ jsx54(StudioSheetHeaderIconButton, { onPress: onNavigateHome, accessibilityLabel: "Home", children: /* @__PURE__ */ jsx54(IconHome, { size: 20, colorToken: "floatingContent" }) }) : null
7101
+ left: /* @__PURE__ */ jsxs33(View42, { style: { flexDirection: "row", alignItems: "center" }, children: [
7102
+ /* @__PURE__ */ jsx55(StudioSheetHeaderIconButton, { onPress: onBack, accessibilityLabel: "Back", style: { marginRight: 8 }, children: /* @__PURE__ */ jsx55(IconBack, { size: 20, colorToken: "floatingContent" }) }),
7103
+ onNavigateHome ? /* @__PURE__ */ jsx55(StudioSheetHeaderIconButton, { onPress: onNavigateHome, accessibilityLabel: "Home", children: /* @__PURE__ */ jsx55(IconHome, { size: 20, colorToken: "floatingContent" }) }) : null
6765
7104
  ] }),
6766
- right: /* @__PURE__ */ jsxs32(View42, { style: { flexDirection: "row", alignItems: "center" }, children: [
6767
- onStartDraw ? /* @__PURE__ */ jsx54(StudioSheetHeaderIconButton, { onPress: onStartDraw, accessibilityLabel: "Draw", intent: "danger", style: { marginRight: 8 }, children: /* @__PURE__ */ jsx54(IconDraw, { size: 20, colorToken: "onDanger" }) }) : null,
6768
- /* @__PURE__ */ jsx54(StudioSheetHeaderIconButton, { onPress: onClose, accessibilityLabel: "Close", children: /* @__PURE__ */ jsx54(IconClose, { size: 20, colorToken: "floatingContent" }) })
7105
+ right: /* @__PURE__ */ jsxs33(View42, { style: { flexDirection: "row", alignItems: "center" }, children: [
7106
+ onStartDraw ? /* @__PURE__ */ jsx55(StudioSheetHeaderIconButton, { onPress: onStartDraw, accessibilityLabel: "Draw", intent: "danger", style: { marginRight: 8 }, children: /* @__PURE__ */ jsx55(IconDraw, { size: 20, colorToken: "onDanger" }) }) : null,
7107
+ /* @__PURE__ */ jsx55(StudioSheetHeaderIconButton, { onPress: onClose, accessibilityLabel: "Close", children: /* @__PURE__ */ jsx55(IconClose, { size: 20, colorToken: "floatingContent" }) })
6769
7108
  ] }),
6770
7109
  center: null
6771
7110
  }
6772
7111
  );
6773
- const topBanner = shouldForkOnEdit ? /* @__PURE__ */ jsx54(
7112
+ const topBanner = shouldForkOnEdit ? /* @__PURE__ */ jsx55(
6774
7113
  ForkNoticeBanner,
6775
7114
  {
6776
7115
  isOwner: !shouldForkOnEdit,
@@ -6779,35 +7118,37 @@ function ChatPanel({
6779
7118
  ) : null;
6780
7119
  const showMessagesLoading = Boolean(loading) && messages.length === 0 || forking;
6781
7120
  if (showMessagesLoading) {
6782
- return /* @__PURE__ */ jsxs32(View42, { style: { flex: 1 }, children: [
6783
- /* @__PURE__ */ jsx54(View42, { children: header }),
6784
- topBanner ? /* @__PURE__ */ jsx54(View42, { style: { paddingHorizontal: 16, paddingTop: 8 }, children: topBanner }) : null,
6785
- /* @__PURE__ */ jsxs32(View42, { style: { flex: 1, alignItems: "center", justifyContent: "center", paddingHorizontal: 24, paddingVertical: 12 }, children: [
6786
- /* @__PURE__ */ jsx54(ActivityIndicator9, {}),
6787
- /* @__PURE__ */ jsx54(View42, { style: { height: 12 } }),
6788
- /* @__PURE__ */ jsx54(Text, { variant: "bodyMuted", children: forking ? "Creating your copy\u2026" : "Loading messages\u2026" })
7121
+ return /* @__PURE__ */ jsxs33(View42, { style: { flex: 1 }, children: [
7122
+ /* @__PURE__ */ jsx55(View42, { children: header }),
7123
+ topBanner ? /* @__PURE__ */ jsx55(View42, { style: { paddingHorizontal: 16, paddingTop: 8 }, children: topBanner }) : null,
7124
+ /* @__PURE__ */ jsxs33(View42, { style: { flex: 1, alignItems: "center", justifyContent: "center", paddingHorizontal: 24, paddingVertical: 12 }, children: [
7125
+ /* @__PURE__ */ jsx55(ActivityIndicator9, {}),
7126
+ /* @__PURE__ */ jsx55(View42, { style: { height: 12 } }),
7127
+ /* @__PURE__ */ jsx55(Text, { variant: "bodyMuted", children: forking ? "Creating your copy\u2026" : "Loading messages\u2026" })
6789
7128
  ] })
6790
7129
  ] });
6791
7130
  }
6792
- const queueTop = queueItems.length > 0 ? /* @__PURE__ */ jsx54(ChatQueue, { items: queueItems, onRemove: onRemoveQueueItem }) : null;
6793
- return /* @__PURE__ */ jsx54(
7131
+ const queueTop = queueItems.length > 0 ? /* @__PURE__ */ jsx55(ChatQueue, { items: queueItems, onRemove: onRemoveQueueItem }) : null;
7132
+ return /* @__PURE__ */ jsx55(
6794
7133
  ChatPage,
6795
7134
  {
6796
7135
  header,
6797
7136
  messages,
6798
7137
  showTypingIndicator,
7138
+ onRetryMessage,
7139
+ isRetryingMessage,
6799
7140
  topBanner,
6800
7141
  composerTop: queueTop,
6801
7142
  composerHorizontalPadding: 0,
6802
7143
  listRef,
6803
7144
  onNearBottomChange: setNearBottom,
6804
- overlay: /* @__PURE__ */ jsx54(
7145
+ overlay: /* @__PURE__ */ jsx55(
6805
7146
  ScrollToBottomButton,
6806
7147
  {
6807
7148
  visible: !nearBottom,
6808
7149
  onPress: handleScrollToBottom,
6809
7150
  style: { bottom: 80 },
6810
- children: /* @__PURE__ */ jsx54(IconArrowDown, { size: 20, colorToken: "floatingContent" })
7151
+ children: /* @__PURE__ */ jsx55(IconArrowDown, { size: 20, colorToken: "floatingContent" })
6811
7152
  }
6812
7153
  ),
6813
7154
  composer: {
@@ -6816,7 +7157,6 @@ function ChatPanel({
6816
7157
  disabled: Boolean(loading) || Boolean(forking),
6817
7158
  sendDisabled: Boolean(sendDisabled) || Boolean(loading) || Boolean(forking),
6818
7159
  sending: Boolean(sending),
6819
- autoFocus: autoFocusComposer,
6820
7160
  onSend: handleSend,
6821
7161
  attachments,
6822
7162
  onRemoveAttachment,
@@ -6829,15 +7169,15 @@ function ChatPanel({
6829
7169
 
6830
7170
  // src/components/dialogs/ConfirmMergeRequestDialog.tsx
6831
7171
  import * as React42 from "react";
6832
- import { Pressable as Pressable15, View as View44 } from "react-native";
7172
+ import { Pressable as Pressable17, View as View44 } from "react-native";
6833
7173
 
6834
7174
  // src/components/primitives/Modal.tsx
6835
7175
  import {
6836
7176
  Modal as RNModal,
6837
- Pressable as Pressable14,
7177
+ Pressable as Pressable16,
6838
7178
  View as View43
6839
7179
  } from "react-native";
6840
- import { jsx as jsx55, jsxs as jsxs33 } from "react/jsx-runtime";
7180
+ import { jsx as jsx56, jsxs as jsxs34 } from "react/jsx-runtime";
6841
7181
  function Modal({
6842
7182
  visible,
6843
7183
  onRequestClose,
@@ -6846,30 +7186,30 @@ function Modal({
6846
7186
  contentStyle
6847
7187
  }) {
6848
7188
  const theme = useTheme();
6849
- return /* @__PURE__ */ jsx55(
7189
+ return /* @__PURE__ */ jsx56(
6850
7190
  RNModal,
6851
7191
  {
6852
7192
  visible,
6853
7193
  transparent: true,
6854
7194
  animationType: "fade",
6855
7195
  onRequestClose,
6856
- children: /* @__PURE__ */ jsxs33(View43, { style: { flex: 1, backgroundColor: theme.colors.backdrop, justifyContent: "center", padding: theme.spacing.lg }, children: [
6857
- /* @__PURE__ */ jsx55(
6858
- Pressable14,
7196
+ children: /* @__PURE__ */ jsxs34(View43, { style: { flex: 1, backgroundColor: theme.colors.backdrop, justifyContent: "center", padding: theme.spacing.lg }, children: [
7197
+ /* @__PURE__ */ jsx56(
7198
+ Pressable16,
6859
7199
  {
6860
7200
  accessibilityRole: "button",
6861
7201
  onPress: dismissOnBackdropPress ? onRequestClose : void 0,
6862
7202
  style: { position: "absolute", inset: 0 }
6863
7203
  }
6864
7204
  ),
6865
- /* @__PURE__ */ jsx55(Card, { variant: "surfaceRaised", padded: true, style: [{ borderRadius: theme.radii.xl }, contentStyle], children })
7205
+ /* @__PURE__ */ jsx56(Card, { variant: "surfaceRaised", padded: true, style: [{ borderRadius: theme.radii.xl }, contentStyle], children })
6866
7206
  ] })
6867
7207
  }
6868
7208
  );
6869
7209
  }
6870
7210
 
6871
7211
  // src/components/dialogs/ConfirmMergeRequestDialog.tsx
6872
- import { jsx as jsx56, jsxs as jsxs34 } from "react/jsx-runtime";
7212
+ import { jsx as jsx57, jsxs as jsxs35 } from "react/jsx-runtime";
6873
7213
  function ConfirmMergeRequestDialog({
6874
7214
  visible,
6875
7215
  onOpenChange,
@@ -6899,7 +7239,7 @@ function ConfirmMergeRequestDialog({
6899
7239
  justifyContent: "center",
6900
7240
  alignSelf: "stretch"
6901
7241
  };
6902
- return /* @__PURE__ */ jsxs34(
7242
+ return /* @__PURE__ */ jsxs35(
6903
7243
  Modal,
6904
7244
  {
6905
7245
  visible,
@@ -6910,7 +7250,7 @@ function ConfirmMergeRequestDialog({
6910
7250
  backgroundColor: theme.colors.background
6911
7251
  },
6912
7252
  children: [
6913
- /* @__PURE__ */ jsx56(View44, { children: /* @__PURE__ */ jsx56(
7253
+ /* @__PURE__ */ jsx57(View44, { children: /* @__PURE__ */ jsx57(
6914
7254
  Text,
6915
7255
  {
6916
7256
  style: {
@@ -6922,8 +7262,8 @@ function ConfirmMergeRequestDialog({
6922
7262
  children: "Are you sure you want to approve this merge request?"
6923
7263
  }
6924
7264
  ) }),
6925
- /* @__PURE__ */ jsxs34(View44, { style: { marginTop: 16 }, children: [
6926
- /* @__PURE__ */ jsx56(
7265
+ /* @__PURE__ */ jsxs35(View44, { style: { marginTop: 16 }, children: [
7266
+ /* @__PURE__ */ jsx57(
6927
7267
  View44,
6928
7268
  {
6929
7269
  style: [
@@ -6933,21 +7273,21 @@ function ConfirmMergeRequestDialog({
6933
7273
  opacity: canConfirm ? 1 : 0.5
6934
7274
  }
6935
7275
  ],
6936
- children: /* @__PURE__ */ jsx56(
6937
- Pressable15,
7276
+ children: /* @__PURE__ */ jsx57(
7277
+ Pressable17,
6938
7278
  {
6939
7279
  accessibilityRole: "button",
6940
7280
  accessibilityLabel: "Approve Merge",
6941
7281
  disabled: !canConfirm,
6942
7282
  onPress: handleConfirm,
6943
7283
  style: [fullWidthButtonBase, { flex: 1 }],
6944
- children: /* @__PURE__ */ jsx56(Text, { style: { textAlign: "center", color: theme.colors.onPrimary }, children: "Approve Merge" })
7284
+ children: /* @__PURE__ */ jsx57(Text, { style: { textAlign: "center", color: theme.colors.onPrimary }, children: "Approve Merge" })
6945
7285
  }
6946
7286
  )
6947
7287
  }
6948
7288
  ),
6949
- /* @__PURE__ */ jsx56(View44, { style: { height: 8 } }),
6950
- /* @__PURE__ */ jsx56(
7289
+ /* @__PURE__ */ jsx57(View44, { style: { height: 8 } }),
7290
+ /* @__PURE__ */ jsx57(
6951
7291
  View44,
6952
7292
  {
6953
7293
  style: [
@@ -6959,21 +7299,21 @@ function ConfirmMergeRequestDialog({
6959
7299
  opacity: isBuilding || !mergeRequest ? 0.5 : 1
6960
7300
  }
6961
7301
  ],
6962
- children: /* @__PURE__ */ jsx56(
6963
- Pressable15,
7302
+ children: /* @__PURE__ */ jsx57(
7303
+ Pressable17,
6964
7304
  {
6965
7305
  accessibilityRole: "button",
6966
7306
  accessibilityLabel: isBuilding ? "Preparing\u2026" : "Test edits first",
6967
7307
  disabled: isBuilding || !mergeRequest,
6968
7308
  onPress: handleTestFirst,
6969
7309
  style: [fullWidthButtonBase, { flex: 1 }],
6970
- children: /* @__PURE__ */ jsx56(Text, { style: { textAlign: "center", color: theme.colors.text }, children: isBuilding ? "Preparing\u2026" : "Test edits first" })
7310
+ children: /* @__PURE__ */ jsx57(Text, { style: { textAlign: "center", color: theme.colors.text }, children: isBuilding ? "Preparing\u2026" : "Test edits first" })
6971
7311
  }
6972
7312
  )
6973
7313
  }
6974
7314
  ),
6975
- /* @__PURE__ */ jsx56(View44, { style: { height: 8 } }),
6976
- /* @__PURE__ */ jsx56(
7315
+ /* @__PURE__ */ jsx57(View44, { style: { height: 8 } }),
7316
+ /* @__PURE__ */ jsx57(
6977
7317
  View44,
6978
7318
  {
6979
7319
  style: [
@@ -6984,14 +7324,14 @@ function ConfirmMergeRequestDialog({
6984
7324
  borderColor: theme.colors.border
6985
7325
  }
6986
7326
  ],
6987
- children: /* @__PURE__ */ jsx56(
6988
- Pressable15,
7327
+ children: /* @__PURE__ */ jsx57(
7328
+ Pressable17,
6989
7329
  {
6990
7330
  accessibilityRole: "button",
6991
7331
  accessibilityLabel: "Cancel",
6992
7332
  onPress: close,
6993
7333
  style: [fullWidthButtonBase, { flex: 1 }],
6994
- children: /* @__PURE__ */ jsx56(Text, { style: { textAlign: "center", color: theme.colors.text }, children: "Cancel" })
7334
+ children: /* @__PURE__ */ jsx57(Text, { style: { textAlign: "center", color: theme.colors.text }, children: "Cancel" })
6995
7335
  }
6996
7336
  )
6997
7337
  }
@@ -7003,7 +7343,7 @@ function ConfirmMergeRequestDialog({
7003
7343
  }
7004
7344
 
7005
7345
  // src/studio/ui/ConfirmMergeFlow.tsx
7006
- import { jsx as jsx57 } from "react/jsx-runtime";
7346
+ import { jsx as jsx58 } from "react/jsx-runtime";
7007
7347
  function ConfirmMergeFlow({
7008
7348
  visible,
7009
7349
  onOpenChange,
@@ -7014,7 +7354,7 @@ function ConfirmMergeFlow({
7014
7354
  onConfirm,
7015
7355
  onTestFirst
7016
7356
  }) {
7017
- return /* @__PURE__ */ jsx57(
7357
+ return /* @__PURE__ */ jsx58(
7018
7358
  ConfirmMergeRequestDialog,
7019
7359
  {
7020
7360
  visible,
@@ -7112,14 +7452,56 @@ function useOptimisticChatMessages({
7112
7452
  const createdAtIso = (/* @__PURE__ */ new Date()).toISOString();
7113
7453
  const baseServerLastId = chatMessages.length > 0 ? chatMessages[chatMessages.length - 1].id : null;
7114
7454
  const id = makeOptimisticId();
7115
- setOptimisticChat((prev) => [...prev, { id, content: text, createdAtIso, baseServerLastId, failed: false }]);
7455
+ const normalizedAttachments = attachments && attachments.length > 0 ? [...attachments] : void 0;
7456
+ setOptimisticChat((prev) => [
7457
+ ...prev,
7458
+ {
7459
+ id,
7460
+ content: text,
7461
+ attachments: normalizedAttachments,
7462
+ createdAtIso,
7463
+ baseServerLastId,
7464
+ failed: false,
7465
+ retrying: false
7466
+ }
7467
+ ]);
7116
7468
  void Promise.resolve(onSendChat(text, attachments)).catch(() => {
7117
7469
  setOptimisticChat((prev) => prev.map((m) => m.id === id ? { ...m, failed: true } : m));
7118
7470
  });
7119
7471
  },
7120
7472
  [chatMessages, disableOptimistic, onSendChat, shouldForkOnEdit]
7121
7473
  );
7122
- return { messages, onSend };
7474
+ const onRetry = React43.useCallback(
7475
+ async (messageId) => {
7476
+ if (shouldForkOnEdit || disableOptimistic) return;
7477
+ const target = optimisticChat.find((m) => m.id === messageId);
7478
+ if (!target || target.retrying) return;
7479
+ const baseServerLastId = chatMessages.length > 0 ? chatMessages[chatMessages.length - 1].id : null;
7480
+ setOptimisticChat(
7481
+ (prev) => prev.map(
7482
+ (m) => m.id === messageId ? { ...m, failed: false, retrying: true, baseServerLastId } : m
7483
+ )
7484
+ );
7485
+ try {
7486
+ await onSendChat(target.content, target.attachments);
7487
+ setOptimisticChat(
7488
+ (prev) => prev.map((m) => m.id === messageId ? { ...m, retrying: false } : m)
7489
+ );
7490
+ } catch {
7491
+ setOptimisticChat(
7492
+ (prev) => prev.map((m) => m.id === messageId ? { ...m, failed: true, retrying: false } : m)
7493
+ );
7494
+ }
7495
+ },
7496
+ [chatMessages, disableOptimistic, onSendChat, optimisticChat, shouldForkOnEdit]
7497
+ );
7498
+ const isRetrying = React43.useCallback(
7499
+ (messageId) => {
7500
+ return optimisticChat.some((m) => m.id === messageId && m.retrying);
7501
+ },
7502
+ [optimisticChat]
7503
+ );
7504
+ return { messages, onSend, onRetry, isRetrying };
7123
7505
  }
7124
7506
 
7125
7507
  // src/studio/ui/StudioOverlay.tsx
@@ -7127,7 +7509,7 @@ import {
7127
7509
  publishComergeStudioUIState,
7128
7510
  startStudioControlPolling
7129
7511
  } from "@comergehq/studio-control";
7130
- import { Fragment as Fragment6, jsx as jsx58, jsxs as jsxs35 } from "react/jsx-runtime";
7512
+ import { Fragment as Fragment6, jsx as jsx59, jsxs as jsxs36 } from "react/jsx-runtime";
7131
7513
  function StudioOverlay({
7132
7514
  captureTargetRef,
7133
7515
  app,
@@ -7135,6 +7517,7 @@ function StudioOverlay({
7135
7517
  isOwner,
7136
7518
  shouldForkOnEdit,
7137
7519
  isTesting,
7520
+ isBaseBundleDownloading = false,
7138
7521
  onRestoreBase,
7139
7522
  incomingMergeRequests,
7140
7523
  outgoingMergeRequests,
@@ -7190,7 +7573,7 @@ function StudioOverlay({
7190
7573
  );
7191
7574
  const handleSheetOpenChange = React44.useCallback((open) => {
7192
7575
  setSheetOpen(open);
7193
- if (!open) Keyboard5.dismiss();
7576
+ if (!open) Keyboard4.dismiss();
7194
7577
  }, []);
7195
7578
  const closeSheet = React44.useCallback(() => {
7196
7579
  handleSheetOpenChange(false);
@@ -7201,8 +7584,8 @@ function StudioOverlay({
7201
7584
  openSheet();
7202
7585
  }, [openSheet]);
7203
7586
  const backToPreview = React44.useCallback(() => {
7204
- if (Platform11.OS !== "ios") {
7205
- Keyboard5.dismiss();
7587
+ if (Platform10.OS !== "ios") {
7588
+ Keyboard4.dismiss();
7206
7589
  setActivePage("preview");
7207
7590
  return;
7208
7591
  }
@@ -7214,9 +7597,9 @@ function StudioOverlay({
7214
7597
  clearTimeout(t);
7215
7598
  setActivePage("preview");
7216
7599
  };
7217
- const sub = Keyboard5.addListener("keyboardDidHide", finalize);
7600
+ const sub = Keyboard4.addListener("keyboardDidHide", finalize);
7218
7601
  const t = setTimeout(finalize, 350);
7219
- Keyboard5.dismiss();
7602
+ Keyboard4.dismiss();
7220
7603
  }, []);
7221
7604
  const startDraw = React44.useCallback(() => {
7222
7605
  setDrawing(true);
@@ -7265,14 +7648,14 @@ function StudioOverlay({
7265
7648
  React44.useEffect(() => {
7266
7649
  void publishComergeStudioUIState(sheetOpen, studioControlOptions);
7267
7650
  }, [sheetOpen, studioControlOptions]);
7268
- return /* @__PURE__ */ jsxs35(Fragment6, { children: [
7269
- /* @__PURE__ */ jsx58(EdgeGlowFrame, { visible: isTesting, role: "accent", thickness: 40, intensity: 1 }),
7270
- /* @__PURE__ */ jsx58(StudioBottomSheet, { open: sheetOpen, onOpenChange: handleSheetOpenChange, children: /* @__PURE__ */ jsx58(
7651
+ return /* @__PURE__ */ jsxs36(Fragment6, { children: [
7652
+ /* @__PURE__ */ jsx59(EdgeGlowFrame, { visible: isTesting, role: "accent", thickness: 40, intensity: 1 }),
7653
+ /* @__PURE__ */ jsx59(StudioBottomSheet, { open: sheetOpen, onOpenChange: handleSheetOpenChange, children: /* @__PURE__ */ jsx59(
7271
7654
  StudioSheetPager,
7272
7655
  {
7273
7656
  activePage,
7274
7657
  width,
7275
- preview: /* @__PURE__ */ jsx58(
7658
+ preview: /* @__PURE__ */ jsx59(
7276
7659
  PreviewPanel,
7277
7660
  {
7278
7661
  app,
@@ -7301,7 +7684,7 @@ function StudioOverlay({
7301
7684
  commentCountOverride: commentsCount ?? void 0
7302
7685
  }
7303
7686
  ),
7304
- chat: /* @__PURE__ */ jsx58(
7687
+ chat: /* @__PURE__ */ jsx59(
7305
7688
  ChatPanel,
7306
7689
  {
7307
7690
  messages: optimistic.messages,
@@ -7310,7 +7693,6 @@ function StudioOverlay({
7310
7693
  sendDisabled: chatSendDisabled,
7311
7694
  forking: chatForking,
7312
7695
  sending: chatSending,
7313
- autoFocusComposer: sheetOpen && activePage === "chat",
7314
7696
  shouldForkOnEdit,
7315
7697
  attachments: chatAttachments,
7316
7698
  onRemoveAttachment: (idx) => setChatAttachments((prev) => prev.filter((_, i) => i !== idx)),
@@ -7320,24 +7702,27 @@ function StudioOverlay({
7320
7702
  onNavigateHome,
7321
7703
  onStartDraw: startDraw,
7322
7704
  onSend: optimistic.onSend,
7705
+ onRetryMessage: optimistic.onRetry,
7706
+ isRetryingMessage: optimistic.isRetrying,
7323
7707
  queueItems: queueItemsForChat,
7324
7708
  onRemoveQueueItem
7325
7709
  }
7326
7710
  )
7327
7711
  }
7328
7712
  ) }),
7329
- showBubble && /* @__PURE__ */ jsx58(
7713
+ showBubble && /* @__PURE__ */ jsx59(
7330
7714
  Bubble,
7331
7715
  {
7332
7716
  visible: !sheetOpen && !drawing,
7333
7717
  ariaLabel: sheetOpen ? "Hide studio" : "Show studio",
7334
7718
  badgeCount: incomingMergeRequests.length,
7335
7719
  onPress: toggleSheet,
7336
- isLoading: (app == null ? void 0 : app.status) === "editing",
7337
- children: /* @__PURE__ */ jsx58(View45, { style: { width: 28, height: 28, alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsx58(MergeIcon, { width: 24, height: 24, color: theme.colors.floatingContent }) })
7720
+ isLoading: (app == null ? void 0 : app.status) === "editing" || isBaseBundleDownloading,
7721
+ loadingBorderTone: isBaseBundleDownloading ? "warning" : "default",
7722
+ children: /* @__PURE__ */ jsx59(View45, { style: { width: 28, height: 28, alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsx59(MergeIcon, { width: 24, height: 24, color: theme.colors.floatingContent }) })
7338
7723
  }
7339
7724
  ),
7340
- /* @__PURE__ */ jsx58(
7725
+ /* @__PURE__ */ jsx59(
7341
7726
  DrawModeOverlay,
7342
7727
  {
7343
7728
  visible: drawing,
@@ -7346,7 +7731,7 @@ function StudioOverlay({
7346
7731
  onCapture: handleDrawCapture
7347
7732
  }
7348
7733
  ),
7349
- /* @__PURE__ */ jsx58(
7734
+ /* @__PURE__ */ jsx59(
7350
7735
  ConfirmMergeFlow,
7351
7736
  {
7352
7737
  visible: Boolean(confirmMr),
@@ -7355,11 +7740,12 @@ function StudioOverlay({
7355
7740
  },
7356
7741
  mergeRequest: confirmMr,
7357
7742
  toSummary: toMergeRequestSummary,
7743
+ isBuilding: isBuildingMrTest,
7358
7744
  onConfirm: (mr) => onApprove == null ? void 0 : onApprove(mr),
7359
7745
  onTestFirst: handleTestMr
7360
7746
  }
7361
7747
  ),
7362
- /* @__PURE__ */ jsx58(
7748
+ /* @__PURE__ */ jsx59(
7363
7749
  AppCommentsSheet,
7364
7750
  {
7365
7751
  appId: commentsAppId,
@@ -7552,7 +7938,7 @@ function useEditQueueActions(appId) {
7552
7938
  }
7553
7939
 
7554
7940
  // src/studio/ComergeStudio.tsx
7555
- import { jsx as jsx59, jsxs as jsxs36 } from "react/jsx-runtime";
7941
+ import { jsx as jsx60, jsxs as jsxs37 } from "react/jsx-runtime";
7556
7942
  function ComergeStudio({
7557
7943
  appId,
7558
7944
  clientKey: clientKey2,
@@ -7573,7 +7959,7 @@ function ComergeStudio({
7573
7959
  setPendingRuntimeTargetAppId(null);
7574
7960
  }, [appId]);
7575
7961
  const captureTargetRef = React47.useRef(null);
7576
- return /* @__PURE__ */ jsx59(StudioBootstrap, { clientKey: clientKey2, fallback: /* @__PURE__ */ jsx59(View46, { style: { flex: 1 } }), children: ({ userId }) => /* @__PURE__ */ jsx59(BottomSheetModalProvider, { children: /* @__PURE__ */ jsx59(LiquidGlassResetProvider, { resetTriggers: [appId, activeAppId, runtimeAppId], children: /* @__PURE__ */ jsx59(
7962
+ return /* @__PURE__ */ jsx60(StudioBootstrap, { clientKey: clientKey2, fallback: /* @__PURE__ */ jsx60(View46, { style: { flex: 1 } }), children: ({ userId }) => /* @__PURE__ */ jsx60(BottomSheetModalProvider, { children: /* @__PURE__ */ jsx60(LiquidGlassResetProvider, { resetTriggers: [appId, activeAppId, runtimeAppId], children: /* @__PURE__ */ jsx60(
7577
7963
  ComergeStudioInner,
7578
7964
  {
7579
7965
  userId,
@@ -7722,6 +8108,8 @@ function ComergeStudioInner({
7722
8108
  const [testingMrId, setTestingMrId] = React47.useState(null);
7723
8109
  const [syncingUpstream, setSyncingUpstream] = React47.useState(false);
7724
8110
  const [upstreamSyncStatus, setUpstreamSyncStatus] = React47.useState(null);
8111
+ const isMrTestBuildInProgress = bundle.loading && bundle.loadingMode === "test";
8112
+ const isBaseBundleDownloading = bundle.loading && bundle.loadingMode === "base" && !bundle.isTesting;
7725
8113
  const chatShowTypingIndicator = React47.useMemo(() => {
7726
8114
  var _a;
7727
8115
  if (!thread.raw || thread.raw.length === 0) return false;
@@ -7768,8 +8156,8 @@ function ComergeStudioInner({
7768
8156
  }
7769
8157
  return editQueue.items;
7770
8158
  }, [editQueue.items, lastEditQueueInfo, suppressQueueUntilResponse]);
7771
- return /* @__PURE__ */ jsx59(View46, { style: [{ flex: 1 }, style], children: /* @__PURE__ */ jsxs36(View46, { ref: captureTargetRef, style: { flex: 1 }, collapsable: false, children: [
7772
- /* @__PURE__ */ jsx59(
8159
+ return /* @__PURE__ */ jsx60(View46, { style: [{ flex: 1 }, style], children: /* @__PURE__ */ jsxs37(View46, { ref: captureTargetRef, style: { flex: 1 }, collapsable: false, children: [
8160
+ /* @__PURE__ */ jsx60(
7773
8161
  RuntimeRenderer,
7774
8162
  {
7775
8163
  appKey,
@@ -7779,7 +8167,7 @@ function ComergeStudioInner({
7779
8167
  allowInitialPreparing: !embeddedBaseBundles
7780
8168
  }
7781
8169
  ),
7782
- /* @__PURE__ */ jsx59(
8170
+ /* @__PURE__ */ jsx60(
7783
8171
  StudioOverlay,
7784
8172
  {
7785
8173
  captureTargetRef,
@@ -7788,6 +8176,7 @@ function ComergeStudioInner({
7788
8176
  isOwner: actions.isOwner,
7789
8177
  shouldForkOnEdit: actions.shouldForkOnEdit,
7790
8178
  isTesting: bundle.isTesting,
8179
+ isBaseBundleDownloading,
7791
8180
  onRestoreBase: async () => {
7792
8181
  setTestingMrId(null);
7793
8182
  await bundle.restoreBase();
@@ -7796,10 +8185,10 @@ function ComergeStudioInner({
7796
8185
  outgoingMergeRequests: mergeRequests.lists.outgoing,
7797
8186
  creatorStatsById: mergeRequests.creatorStatsById,
7798
8187
  processingMrId,
7799
- isBuildingMrTest: bundle.loading,
8188
+ isBuildingMrTest: isMrTestBuildInProgress,
7800
8189
  testingMrId,
7801
8190
  toMergeRequestSummary: mergeRequests.toSummary,
7802
- onSubmitMergeRequest: (app == null ? void 0 : app.forkedFromAppId) && actions.isOwner && !hasOpenOutgoingMr ? async () => {
8191
+ onSubmitMergeRequest: (app == null ? void 0 : app.forkedFromAppId) && actions.isOwner && !mergeRequests.loading && !hasOpenOutgoingMr ? async () => {
7803
8192
  await mergeRequests.actions.openMergeRequest(activeAppId);
7804
8193
  } : void 0,
7805
8194
  onSyncUpstream: actions.isOwner && (app == null ? void 0 : app.forkedFromAppId) ? handleSyncUpstream : void 0,
@@ -7824,6 +8213,7 @@ function ComergeStudioInner({
7824
8213
  }
7825
8214
  },
7826
8215
  onTestMr: async (mr) => {
8216
+ if (testingMrId === mr.id || bundle.loadingMode === "test") return;
7827
8217
  setTestingMrId(mr.id);
7828
8218
  await bundle.loadTest({ appId: mr.sourceAppId, commitId: mr.sourceTipCommitId ?? mr.sourceCommitId });
7829
8219
  },
@@ -7845,6 +8235,7 @@ function ComergeStudioInner({
7845
8235
  }
7846
8236
  export {
7847
8237
  ComergeStudio,
8238
+ resetRealtimeState,
7848
8239
  setSupabaseClient
7849
8240
  };
7850
8241
  //# sourceMappingURL=index.mjs.map