@comergehq/studio 0.1.22 → 0.1.24

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 +697 -312
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +724 -336
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +1 -1
  8. package/src/components/bubble/Bubble.tsx +11 -5
  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.js CHANGED
@@ -31,13 +31,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  ComergeStudio: () => ComergeStudio,
34
+ resetRealtimeState: () => resetRealtimeState,
34
35
  setSupabaseClient: () => setSupabaseClient
35
36
  });
36
37
  module.exports = __toCommonJS(index_exports);
37
38
 
38
39
  // src/studio/ComergeStudio.tsx
39
40
  var React47 = __toESM(require("react"));
40
- var import_react_native56 = require("react-native");
41
+ var import_react_native57 = require("react-native");
41
42
  var import_bottom_sheet6 = require("@gorhom/bottom-sheet");
42
43
 
43
44
  // src/studio/bootstrap/StudioBootstrap.tsx
@@ -393,6 +394,41 @@ var log = import_react_native_logs.logger.createLogger(
393
394
  );
394
395
 
395
396
  // src/core/services/http/index.ts
397
+ var RETRYABLE_MAX_ATTEMPTS = 3;
398
+ var RETRYABLE_BASE_DELAY_MS = 500;
399
+ var RETRYABLE_MAX_DELAY_MS = 4e3;
400
+ var RETRYABLE_JITTER_MS = 250;
401
+ function sleep(ms) {
402
+ return new Promise((resolve) => setTimeout(resolve, ms));
403
+ }
404
+ function isRetryableNetworkError(e) {
405
+ var _a;
406
+ const err = e;
407
+ const code = typeof (err == null ? void 0 : err.code) === "string" ? err.code : "";
408
+ const message = typeof (err == null ? void 0 : err.message) === "string" ? err.message : "";
409
+ if (code === "ERR_NETWORK" || code === "ECONNABORTED") return true;
410
+ if (message.toLowerCase().includes("network error")) return true;
411
+ if (message.toLowerCase().includes("timeout")) return true;
412
+ const status = typeof ((_a = err == null ? void 0 : err.response) == null ? void 0 : _a.status) === "number" ? err.response.status : void 0;
413
+ if (status && (status === 429 || status >= 500)) return true;
414
+ return false;
415
+ }
416
+ function computeBackoffDelay(attempt) {
417
+ const exp = Math.min(RETRYABLE_MAX_DELAY_MS, RETRYABLE_BASE_DELAY_MS * Math.pow(2, attempt - 1));
418
+ const jitter = Math.floor(Math.random() * RETRYABLE_JITTER_MS);
419
+ return exp + jitter;
420
+ }
421
+ function parseRetryAfterMs(value) {
422
+ if (typeof value !== "string") return null;
423
+ const trimmed = value.trim();
424
+ if (!trimmed) return null;
425
+ const seconds = Number(trimmed);
426
+ if (!Number.isNaN(seconds) && seconds >= 0) return seconds * 1e3;
427
+ const parsed = Date.parse(trimmed);
428
+ if (Number.isNaN(parsed)) return null;
429
+ const delta = parsed - Date.now();
430
+ return delta > 0 ? delta : 0;
431
+ }
396
432
  var createApiClient = (baseURL) => {
397
433
  const apiClient = import_axios2.default.create({
398
434
  baseURL,
@@ -450,7 +486,7 @@ var createApiClient = (baseURL) => {
450
486
  return response;
451
487
  },
452
488
  async (error) => {
453
- var _a, _b, _c, _d, _e, _f, _g;
489
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
454
490
  const originalRequest = error.config;
455
491
  log.error("Response Error:", {
456
492
  message: error.message,
@@ -486,6 +522,23 @@ var createApiClient = (baseURL) => {
486
522
  return Promise.reject(refreshErr);
487
523
  }
488
524
  }
525
+ const method = ((_h = originalRequest.method) == null ? void 0 : _h.toLowerCase()) ?? "";
526
+ const isGet = method === "get";
527
+ const retryable = isRetryableNetworkError(error);
528
+ const retryCount = originalRequest._retryCount ?? 0;
529
+ const skipRetry = originalRequest.skipRetry === true;
530
+ if (isGet && retryable && !skipRetry && retryCount < RETRYABLE_MAX_ATTEMPTS) {
531
+ const retryAfterMs = parseRetryAfterMs((_j = (_i = error.response) == null ? void 0 : _i.headers) == null ? void 0 : _j["retry-after"]);
532
+ originalRequest._retryCount = retryCount + 1;
533
+ const delayMs = retryAfterMs ?? computeBackoffDelay(retryCount + 1);
534
+ log.warn("Retrying GET request after transient error", {
535
+ url: originalRequest.url,
536
+ attempt: originalRequest._retryCount,
537
+ delayMs
538
+ });
539
+ await sleep(delayMs);
540
+ return apiClient(originalRequest);
541
+ }
489
542
  return Promise.reject(error);
490
543
  }
491
544
  );
@@ -620,6 +673,25 @@ function subscribeChannel(entry) {
620
673
  scheduleResubscribe(entry, "SUBSCRIBE_FAILED");
621
674
  }
622
675
  }
676
+ function unsubscribeChannel(entry) {
677
+ var _a, _b;
678
+ if (!entry.channel) return;
679
+ try {
680
+ (_b = (_a = entry.channel).unsubscribe) == null ? void 0 : _b.call(_a);
681
+ } catch (error) {
682
+ realtimeLog.warn("[realtime] unsubscribe failed", error);
683
+ }
684
+ entry.channel = null;
685
+ }
686
+ function resetRealtimeState(reason) {
687
+ realtimeLog.warn(`[realtime] reset state ${reason}`);
688
+ entries.forEach((entry) => {
689
+ clearTimer(entry);
690
+ entry.backoffMs = INITIAL_BACKOFF_MS;
691
+ unsubscribeChannel(entry);
692
+ });
693
+ entries.clear();
694
+ }
623
695
  function subscribeManagedChannel(key, configure) {
624
696
  let entry = entries.get(key);
625
697
  if (!entry) {
@@ -777,14 +849,12 @@ function useForegroundSignal(enabled = true) {
777
849
  React2.useEffect(() => {
778
850
  if (!enabled) return;
779
851
  const sub = import_react_native4.AppState.addEventListener("change", (nextState) => {
780
- var _a, _b;
781
852
  const prevState = lastStateRef.current;
782
853
  lastStateRef.current = nextState;
783
854
  const didResume = (prevState === "background" || prevState === "inactive") && nextState === "active";
784
855
  if (!didResume) return;
785
856
  try {
786
- const supabase = getSupabaseClient();
787
- (_b = (_a = supabase == null ? void 0 : supabase.realtime) == null ? void 0 : _a.connect) == null ? void 0 : _b.call(_a);
857
+ resetRealtimeState("APP_RESUME");
788
858
  } catch {
789
859
  }
790
860
  setSignal((s) => s + 1);
@@ -799,8 +869,13 @@ function useApp(appId, options) {
799
869
  const enabled = (options == null ? void 0 : options.enabled) ?? true;
800
870
  const [app, setApp] = React3.useState(null);
801
871
  const [loading, setLoading] = React3.useState(false);
872
+ const [refreshing, setRefreshing] = React3.useState(false);
802
873
  const [error, setError] = React3.useState(null);
803
874
  const foregroundSignal = useForegroundSignal(enabled && Boolean(appId));
875
+ const hasLoadedOnceRef = React3.useRef(false);
876
+ React3.useEffect(() => {
877
+ hasLoadedOnceRef.current = false;
878
+ }, [appId]);
804
879
  const mergeApp = React3.useCallback((prev, next) => {
805
880
  const merged = {
806
881
  ...prev ?? {},
@@ -810,21 +885,32 @@ function useApp(appId, options) {
810
885
  };
811
886
  return merged;
812
887
  }, []);
813
- const fetchOnce = React3.useCallback(async () => {
888
+ const fetchOnce = React3.useCallback(async (opts) => {
814
889
  if (!enabled) return;
815
890
  if (!appId) return;
816
- setLoading(true);
891
+ const isBackground = Boolean(opts == null ? void 0 : opts.background);
892
+ const useBackgroundRefresh = isBackground && hasLoadedOnceRef.current;
893
+ if (useBackgroundRefresh) {
894
+ setRefreshing(true);
895
+ } else {
896
+ setLoading(true);
897
+ }
817
898
  setError(null);
818
899
  try {
819
900
  const next = await appsRepository.getById(appId);
901
+ hasLoadedOnceRef.current = true;
820
902
  setApp((prev) => mergeApp(prev, next));
821
903
  } catch (e) {
822
904
  setError(e instanceof Error ? e : new Error(String(e)));
823
905
  setApp(null);
824
906
  } finally {
825
- setLoading(false);
907
+ if (useBackgroundRefresh) {
908
+ setRefreshing(false);
909
+ } else {
910
+ setLoading(false);
911
+ }
826
912
  }
827
- }, [appId, enabled]);
913
+ }, [appId, enabled, mergeApp]);
828
914
  React3.useEffect(() => {
829
915
  if (!enabled) return;
830
916
  void fetchOnce();
@@ -849,9 +935,9 @@ function useApp(appId, options) {
849
935
  if (!enabled) return;
850
936
  if (!appId) return;
851
937
  if (foregroundSignal <= 0) return;
852
- void fetchOnce();
938
+ void fetchOnce({ background: true });
853
939
  }, [appId, enabled, fetchOnce, foregroundSignal]);
854
- return { app, loading, error, refetch: fetchOnce };
940
+ return { app, loading, refreshing, error, refetch: fetchOnce };
855
941
  }
856
942
 
857
943
  // src/studio/hooks/useThreadMessages.ts
@@ -985,33 +1071,52 @@ function mapMessageToChatMessage(m) {
985
1071
  function useThreadMessages(threadId) {
986
1072
  const [raw, setRaw] = React4.useState([]);
987
1073
  const [loading, setLoading] = React4.useState(false);
1074
+ const [refreshing, setRefreshing] = React4.useState(false);
988
1075
  const [error, setError] = React4.useState(null);
989
1076
  const activeRequestIdRef = React4.useRef(0);
990
1077
  const foregroundSignal = useForegroundSignal(Boolean(threadId));
1078
+ const hasLoadedOnceRef = React4.useRef(false);
1079
+ React4.useEffect(() => {
1080
+ hasLoadedOnceRef.current = false;
1081
+ }, [threadId]);
991
1082
  const upsertSorted = React4.useCallback((prev, m) => {
992
1083
  const next = prev.filter((x) => x.id !== m.id);
993
1084
  next.push(m);
994
1085
  next.sort(compareMessages);
995
1086
  return next;
996
1087
  }, []);
997
- const refetch = React4.useCallback(async () => {
1088
+ const refetch = React4.useCallback(async (opts) => {
998
1089
  if (!threadId) {
999
1090
  setRaw([]);
1091
+ setLoading(false);
1092
+ setRefreshing(false);
1000
1093
  return;
1001
1094
  }
1002
1095
  const requestId = ++activeRequestIdRef.current;
1003
- setLoading(true);
1096
+ const isBackground = Boolean(opts == null ? void 0 : opts.background);
1097
+ const useBackgroundRefresh = isBackground && hasLoadedOnceRef.current;
1098
+ if (useBackgroundRefresh) {
1099
+ setRefreshing(true);
1100
+ } else {
1101
+ setLoading(true);
1102
+ }
1004
1103
  setError(null);
1005
1104
  try {
1006
1105
  const list = await messagesRepository.list(threadId);
1007
1106
  if (activeRequestIdRef.current !== requestId) return;
1107
+ hasLoadedOnceRef.current = true;
1008
1108
  setRaw([...list].sort(compareMessages));
1009
1109
  } catch (e) {
1010
1110
  if (activeRequestIdRef.current !== requestId) return;
1011
1111
  setError(e instanceof Error ? e : new Error(String(e)));
1012
1112
  setRaw([]);
1013
1113
  } finally {
1014
- if (activeRequestIdRef.current === requestId) setLoading(false);
1114
+ if (activeRequestIdRef.current !== requestId) return;
1115
+ if (useBackgroundRefresh) {
1116
+ setRefreshing(false);
1117
+ } else {
1118
+ setLoading(false);
1119
+ }
1015
1120
  }
1016
1121
  }, [threadId]);
1017
1122
  React4.useEffect(() => {
@@ -1029,14 +1134,14 @@ function useThreadMessages(threadId) {
1029
1134
  React4.useEffect(() => {
1030
1135
  if (!threadId) return;
1031
1136
  if (foregroundSignal <= 0) return;
1032
- void refetch();
1137
+ void refetch({ background: true });
1033
1138
  }, [foregroundSignal, refetch, threadId]);
1034
1139
  const messages = React4.useMemo(() => {
1035
1140
  const visible = raw.filter((m) => !isQueuedHiddenMessage(m));
1036
1141
  const resolved = visible.length > 0 ? visible : raw;
1037
1142
  return resolved.map(mapMessageToChatMessage);
1038
1143
  }, [raw]);
1039
- return { raw, messages, loading, error, refetch };
1144
+ return { raw, messages, loading, refreshing, error, refetch };
1040
1145
  }
1041
1146
 
1042
1147
  // src/studio/hooks/useBundleManager.ts
@@ -1056,21 +1161,22 @@ var BundlesRemoteDataSourceImpl = class extends BaseRemote {
1056
1161
  }
1057
1162
  async getById(appId, bundleId) {
1058
1163
  const { data } = await api.get(
1059
- `/v1/apps/${encodeURIComponent(appId)}/bundles/${encodeURIComponent(bundleId)}`
1164
+ `/v1/apps/${encodeURIComponent(appId)}/bundles/${encodeURIComponent(bundleId)}`,
1165
+ { skipRetry: true }
1060
1166
  );
1061
1167
  return data;
1062
1168
  }
1063
1169
  async getSignedDownloadUrl(appId, bundleId, options) {
1064
1170
  const { data } = await api.get(
1065
1171
  `/v1/apps/${encodeURIComponent(appId)}/bundles/${encodeURIComponent(bundleId)}/download`,
1066
- { params: { redirect: (options == null ? void 0 : options.redirect) ?? false } }
1172
+ { params: { redirect: (options == null ? void 0 : options.redirect) ?? false }, skipRetry: true }
1067
1173
  );
1068
1174
  return data;
1069
1175
  }
1070
1176
  async getSignedAssetsDownloadUrl(appId, bundleId, options) {
1071
1177
  const { data } = await api.get(
1072
1178
  `/v1/apps/${encodeURIComponent(appId)}/bundles/${encodeURIComponent(bundleId)}/assets/download`,
1073
- { params: { redirect: (options == null ? void 0 : options.redirect) ?? false, kind: options == null ? void 0 : options.kind } }
1179
+ { params: { redirect: (options == null ? void 0 : options.redirect) ?? false, kind: options == null ? void 0 : options.kind }, skipRetry: true }
1074
1180
  );
1075
1181
  return data;
1076
1182
  }
@@ -1103,10 +1209,10 @@ var BundlesRepositoryImpl = class extends BaseRepository {
1103
1209
  var bundlesRepository = new BundlesRepositoryImpl(bundlesRemoteDataSource);
1104
1210
 
1105
1211
  // src/studio/hooks/useBundleManager.ts
1106
- function sleep(ms) {
1212
+ function sleep2(ms) {
1107
1213
  return new Promise((r) => setTimeout(r, ms));
1108
1214
  }
1109
- function isRetryableNetworkError(e) {
1215
+ function isRetryableNetworkError2(e) {
1110
1216
  var _a;
1111
1217
  const err = e;
1112
1218
  const code = typeof (err == null ? void 0 : err.code) === "string" ? err.code : "";
@@ -1125,13 +1231,13 @@ async function withRetry(fn, opts) {
1125
1231
  return await fn();
1126
1232
  } catch (e) {
1127
1233
  lastErr = e;
1128
- const retryable = isRetryableNetworkError(e);
1234
+ const retryable = isRetryableNetworkError2(e);
1129
1235
  if (!retryable || attempt >= opts.attempts) {
1130
1236
  throw e;
1131
1237
  }
1132
1238
  const exp = Math.min(opts.maxDelayMs, opts.baseDelayMs * Math.pow(2, attempt - 1));
1133
1239
  const jitter = Math.floor(Math.random() * 250);
1134
- await sleep(exp + jitter);
1240
+ await sleep2(exp + jitter);
1135
1241
  }
1136
1242
  }
1137
1243
  throw lastErr;
@@ -1408,14 +1514,14 @@ async function pollBundle(appId, bundleId, opts) {
1408
1514
  const bundle = await bundlesRepository.getById(appId, bundleId);
1409
1515
  if (bundle.status === "succeeded" || bundle.status === "failed") return bundle;
1410
1516
  } catch (e) {
1411
- if (!isRetryableNetworkError(e)) {
1517
+ if (!isRetryableNetworkError2(e)) {
1412
1518
  throw e;
1413
1519
  }
1414
1520
  }
1415
1521
  if (Date.now() - start > opts.timeoutMs) {
1416
1522
  throw new Error("Bundle build timed out.");
1417
1523
  }
1418
- await sleep(opts.intervalMs);
1524
+ await sleep2(opts.intervalMs);
1419
1525
  }
1420
1526
  }
1421
1527
  async function resolveBundlePath(src, platform, mode) {
@@ -1484,6 +1590,7 @@ function useBundleManager({
1484
1590
  const baseOpIdRef = React5.useRef(0);
1485
1591
  const testOpIdRef = React5.useRef(0);
1486
1592
  const activeLoadModeRef = React5.useRef(null);
1593
+ const desiredModeRef = React5.useRef("base");
1487
1594
  const canRequestLatestRef = React5.useRef(canRequestLatest);
1488
1595
  React5.useEffect(() => {
1489
1596
  canRequestLatestRef.current = canRequestLatest;
@@ -1571,6 +1678,10 @@ function useBundleManager({
1571
1678
  );
1572
1679
  const load = React5.useCallback(async (src, mode) => {
1573
1680
  if (!src.appId) return;
1681
+ if (mode === "test") {
1682
+ desiredModeRef.current = "test";
1683
+ baseOpIdRef.current += 1;
1684
+ }
1574
1685
  const canRequestLatest2 = canRequestLatestRef.current;
1575
1686
  if (mode === "base" && !canRequestLatest2) {
1576
1687
  await activateCachedBase(src.appId);
@@ -1582,13 +1693,14 @@ function useBundleManager({
1582
1693
  setLoadingMode(mode);
1583
1694
  setError(null);
1584
1695
  setStatusLabel(mode === "test" ? "Loading test bundle\u2026" : "Loading latest build\u2026");
1585
- if (mode === "base") {
1696
+ if (mode === "base" && desiredModeRef.current === "base") {
1586
1697
  void activateCachedBase(src.appId);
1587
1698
  }
1588
1699
  try {
1589
1700
  const { bundlePath: path, bundle } = await resolveBundlePath(src, platform, mode);
1590
1701
  if (mode === "base" && opId !== baseOpIdRef.current) return;
1591
1702
  if (mode === "test" && opId !== testOpIdRef.current) return;
1703
+ if (desiredModeRef.current !== mode) return;
1592
1704
  setBundlePath(path);
1593
1705
  const fingerprint = bundle.checksumSha256 ?? `id:${bundle.id}`;
1594
1706
  const shouldSkipInitialBaseRemount = mode === "base" && initialHydratedBaseFromDiskRef.current && !hasCompletedFirstNetworkBaseLoadRef.current && Boolean(lastBaseFingerprintRef.current) && lastBaseFingerprintRef.current === fingerprint;
@@ -1644,6 +1756,8 @@ function useBundleManager({
1644
1756
  const restoreBase = React5.useCallback(async () => {
1645
1757
  const src = baseRef.current;
1646
1758
  if (!src.appId) return;
1759
+ desiredModeRef.current = "base";
1760
+ testOpIdRef.current += 1;
1647
1761
  await activateCachedBase(src.appId);
1648
1762
  if (canRequestLatestRef.current) {
1649
1763
  await load(src, "base");
@@ -1805,9 +1919,12 @@ function useMergeRequests(params) {
1805
1919
  const { appId } = params;
1806
1920
  const [incoming, setIncoming] = React6.useState([]);
1807
1921
  const [outgoing, setOutgoing] = React6.useState([]);
1808
- const [loading, setLoading] = React6.useState(false);
1922
+ const [loading, setLoading] = React6.useState(() => Boolean(appId));
1809
1923
  const [error, setError] = React6.useState(null);
1810
1924
  const [creatorStatsById, setCreatorStatsById] = React6.useState({});
1925
+ React6.useEffect(() => {
1926
+ setLoading(Boolean(appId));
1927
+ }, [appId]);
1811
1928
  const pollUntilMerged = React6.useCallback(async (mrId) => {
1812
1929
  const startedAt = Date.now();
1813
1930
  const timeoutMs = 2 * 60 * 1e3;
@@ -1823,6 +1940,7 @@ function useMergeRequests(params) {
1823
1940
  setIncoming([]);
1824
1941
  setOutgoing([]);
1825
1942
  setCreatorStatsById({});
1943
+ setLoading(false);
1826
1944
  return;
1827
1945
  }
1828
1946
  setLoading(true);
@@ -2057,6 +2175,45 @@ var AgentRepositoryImpl = class extends BaseRepository {
2057
2175
  var agentRepository = new AgentRepositoryImpl(agentRemoteDataSource);
2058
2176
 
2059
2177
  // src/studio/hooks/useStudioActions.ts
2178
+ function sleep3(ms) {
2179
+ return new Promise((resolve) => setTimeout(resolve, ms));
2180
+ }
2181
+ function isRetryableNetworkError3(e) {
2182
+ var _a;
2183
+ const err = e;
2184
+ const code = typeof (err == null ? void 0 : err.code) === "string" ? err.code : "";
2185
+ const message = typeof (err == null ? void 0 : err.message) === "string" ? err.message : "";
2186
+ if (code === "ERR_NETWORK" || code === "ECONNABORTED") return true;
2187
+ if (message.toLowerCase().includes("network error")) return true;
2188
+ if (message.toLowerCase().includes("timeout")) return true;
2189
+ const status = typeof ((_a = err == null ? void 0 : err.response) == null ? void 0 : _a.status) === "number" ? err.response.status : void 0;
2190
+ if (status && (status === 429 || status >= 500)) return true;
2191
+ return false;
2192
+ }
2193
+ async function withRetry2(fn, opts) {
2194
+ let lastErr = null;
2195
+ for (let attempt = 1; attempt <= opts.attempts; attempt += 1) {
2196
+ try {
2197
+ return await fn();
2198
+ } catch (e) {
2199
+ lastErr = e;
2200
+ const retryable = isRetryableNetworkError3(e);
2201
+ if (!retryable || attempt >= opts.attempts) {
2202
+ throw e;
2203
+ }
2204
+ const exp = Math.min(opts.maxDelayMs, opts.baseDelayMs * Math.pow(2, attempt - 1));
2205
+ const jitter = Math.floor(Math.random() * 250);
2206
+ await sleep3(exp + jitter);
2207
+ }
2208
+ }
2209
+ throw lastErr;
2210
+ }
2211
+ function generateIdempotencyKey() {
2212
+ var _a, _b;
2213
+ const rnd = (_b = (_a = globalThis.crypto) == null ? void 0 : _a.randomUUID) == null ? void 0 : _b.call(_a);
2214
+ if (rnd) return `edit:${rnd}`;
2215
+ return `edit:${Date.now()}-${Math.random().toString(16).slice(2)}`;
2216
+ }
2060
2217
  function useStudioActions({
2061
2218
  userId,
2062
2219
  app,
@@ -2095,12 +2252,19 @@ function useStudioActions({
2095
2252
  if (attachments && attachments.length > 0 && uploadAttachments) {
2096
2253
  attachmentMetas = await uploadAttachments({ threadId, appId: targetApp.id, dataUrls: attachments });
2097
2254
  }
2098
- const editResult = await agentRepository.editApp({
2099
- prompt,
2100
- thread_id: threadId,
2101
- app_id: targetApp.id,
2102
- attachments: attachmentMetas && attachmentMetas.length > 0 ? attachmentMetas : void 0
2103
- });
2255
+ const idempotencyKey = generateIdempotencyKey();
2256
+ const editResult = await withRetry2(
2257
+ async () => {
2258
+ return await agentRepository.editApp({
2259
+ prompt,
2260
+ thread_id: threadId,
2261
+ app_id: targetApp.id,
2262
+ attachments: attachmentMetas && attachmentMetas.length > 0 ? attachmentMetas : void 0,
2263
+ idempotencyKey
2264
+ });
2265
+ },
2266
+ { attempts: 3, baseDelayMs: 500, maxDelayMs: 4e3 }
2267
+ );
2104
2268
  onEditQueued == null ? void 0 : onEditQueued({
2105
2269
  queueItemId: editResult.queueItemId ?? null,
2106
2270
  queuePosition: editResult.queuePosition ?? null
@@ -2158,12 +2322,12 @@ function RuntimeRenderer({
2158
2322
 
2159
2323
  // src/studio/ui/StudioOverlay.tsx
2160
2324
  var React44 = __toESM(require("react"));
2161
- var import_react_native55 = require("react-native");
2325
+ var import_react_native56 = require("react-native");
2162
2326
 
2163
2327
  // src/components/studio-sheet/StudioBottomSheet.tsx
2164
2328
  var React12 = __toESM(require("react"));
2165
2329
  var import_react_native9 = require("react-native");
2166
- var import_bottom_sheet = __toESM(require("@gorhom/bottom-sheet"));
2330
+ var import_bottom_sheet = require("@gorhom/bottom-sheet");
2167
2331
  var import_react_native_safe_area_context = require("react-native-safe-area-context");
2168
2332
 
2169
2333
  // src/components/studio-sheet/StudioSheetBackground.tsx
@@ -2282,25 +2446,16 @@ function StudioBottomSheet({
2282
2446
  const insets = (0, import_react_native_safe_area_context.useSafeAreaInsets)();
2283
2447
  const internalSheetRef = React12.useRef(null);
2284
2448
  const resolvedSheetRef = sheetRef ?? internalSheetRef;
2285
- const currentIndexRef = React12.useRef(open ? snapPoints.length - 1 : -1);
2449
+ const resolvedSnapPoints = React12.useMemo(() => [...snapPoints], [snapPoints]);
2450
+ const currentIndexRef = React12.useRef(open ? resolvedSnapPoints.length - 1 : -1);
2286
2451
  const lastAppStateRef = React12.useRef(import_react_native9.AppState.currentState);
2287
2452
  React12.useEffect(() => {
2288
2453
  const sub = import_react_native9.AppState.addEventListener("change", (state) => {
2289
- const prev = lastAppStateRef.current;
2290
2454
  lastAppStateRef.current = state;
2291
2455
  if (state === "background" || state === "inactive") {
2292
2456
  import_react_native9.Keyboard.dismiss();
2293
2457
  return;
2294
2458
  }
2295
- if (state !== "active") return;
2296
- const sheet = resolvedSheetRef.current;
2297
- if (!sheet) return;
2298
- const idx = currentIndexRef.current;
2299
- if (open && idx >= 0) {
2300
- import_react_native9.Keyboard.dismiss();
2301
- requestAnimationFrame(() => sheet.snapToIndex(idx));
2302
- setTimeout(() => sheet.snapToIndex(idx), 120);
2303
- }
2304
2459
  });
2305
2460
  return () => sub.remove();
2306
2461
  }, [open, resolvedSheetRef]);
@@ -2308,11 +2463,11 @@ function StudioBottomSheet({
2308
2463
  const sheet = resolvedSheetRef.current;
2309
2464
  if (!sheet) return;
2310
2465
  if (open) {
2311
- sheet.snapToIndex(snapPoints.length - 1);
2466
+ sheet.present();
2312
2467
  } else {
2313
- sheet.close();
2468
+ sheet.dismiss();
2314
2469
  }
2315
- }, [open, resolvedSheetRef, snapPoints.length]);
2470
+ }, [open, resolvedSheetRef, resolvedSnapPoints.length]);
2316
2471
  const handleChange = React12.useCallback(
2317
2472
  (index) => {
2318
2473
  currentIndexRef.current = index;
@@ -2321,21 +2476,24 @@ function StudioBottomSheet({
2321
2476
  [onOpenChange]
2322
2477
  );
2323
2478
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2324
- import_bottom_sheet.default,
2479
+ import_bottom_sheet.BottomSheetModal,
2325
2480
  {
2326
2481
  ref: resolvedSheetRef,
2327
- index: open ? snapPoints.length - 1 : -1,
2328
- snapPoints,
2482
+ index: resolvedSnapPoints.length - 1,
2483
+ snapPoints: resolvedSnapPoints,
2329
2484
  enableDynamicSizing: false,
2330
2485
  enablePanDownToClose: true,
2331
2486
  enableContentPanningGesture: false,
2487
+ keyboardBehavior: "interactive",
2488
+ keyboardBlurBehavior: "restore",
2332
2489
  android_keyboardInputMode: "adjustResize",
2333
2490
  backgroundComponent: (props) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(StudioSheetBackground, { ...props, renderBackground: background == null ? void 0 : background.renderBackground }),
2334
2491
  topInset: insets.top,
2335
2492
  bottomInset: 0,
2336
2493
  handleIndicatorStyle: { backgroundColor: theme.colors.handleIndicator },
2337
- onChange: handleChange,
2338
2494
  ...bottomSheetProps,
2495
+ onChange: handleChange,
2496
+ onDismiss: () => onOpenChange == null ? void 0 : onOpenChange(false),
2339
2497
  children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react_native9.View, { style: { flex: 1, overflow: "hidden" }, children })
2340
2498
  }
2341
2499
  );
@@ -2413,6 +2571,21 @@ var ENTER_SCALE_FROM = 0.3;
2413
2571
  var ENTER_ROTATION_FROM_DEG = -180;
2414
2572
  var PULSE_DURATION_MS = 900;
2415
2573
 
2574
+ // src/components/utils/color.ts
2575
+ function withAlpha(color, alpha) {
2576
+ const a = Math.max(0, Math.min(1, alpha));
2577
+ const hex = color.trim();
2578
+ if (!hex.startsWith("#")) return color;
2579
+ const raw = hex.slice(1);
2580
+ const expanded = raw.length === 3 ? raw.split("").map((c) => c + c).join("") : raw;
2581
+ if (expanded.length !== 6) return color;
2582
+ const r = Number.parseInt(expanded.slice(0, 2), 16);
2583
+ const g = Number.parseInt(expanded.slice(2, 4), 16);
2584
+ const b = Number.parseInt(expanded.slice(4, 6), 16);
2585
+ if ([r, g, b].some((n) => Number.isNaN(n))) return color;
2586
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
2587
+ }
2588
+
2416
2589
  // src/components/bubble/Bubble.tsx
2417
2590
  var import_jsx_runtime9 = require("react/jsx-runtime");
2418
2591
  var HIDDEN_OFFSET_X = 20;
@@ -2441,6 +2614,7 @@ function Bubble({
2441
2614
  disabled = false,
2442
2615
  ariaLabel,
2443
2616
  isLoading = false,
2617
+ loadingBorderTone = "default",
2444
2618
  visible = true,
2445
2619
  badgeCount = 0,
2446
2620
  offset = DEFAULT_OFFSET,
@@ -2464,6 +2638,10 @@ function Bubble({
2464
2638
  if (isDanger) return "rgba(239, 68, 68, 0.9)";
2465
2639
  return theme.scheme === "dark" ? "rgba(0, 0, 0, 0.6)" : "rgba(255, 255, 255, 0.6)";
2466
2640
  }, [backgroundColor, isDanger, theme.scheme]);
2641
+ const warningRingColors = (0, import_react.useMemo)(
2642
+ () => [withAlpha(theme.colors.warning, 0.35), withAlpha(theme.colors.warning, 1)],
2643
+ [theme.colors.warning]
2644
+ );
2467
2645
  const translateX = (0, import_react_native_reanimated.useSharedValue)(getHiddenTranslateX(size));
2468
2646
  const translateY = (0, import_react_native_reanimated.useSharedValue)(getHiddenTranslateY(height));
2469
2647
  const scale = (0, import_react_native_reanimated.useSharedValue)(ENTER_SCALE_FROM);
@@ -2484,18 +2662,15 @@ function Bubble({
2484
2662
  [height, rotation, scale, size, translateX, translateY]
2485
2663
  );
2486
2664
  const animateOut = (0, import_react.useCallback)(() => {
2665
+ var _a;
2487
2666
  if (isAnimatingOut.current) return;
2488
2667
  isAnimatingOut.current = true;
2489
2668
  try {
2490
2669
  void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
2491
2670
  } catch {
2492
2671
  }
2493
- animateToHidden({
2494
- onFinish: () => {
2495
- var _a;
2496
- (_a = onPressRef.current) == null ? void 0 : _a.call(onPressRef);
2497
- }
2498
- });
2672
+ (_a = onPressRef.current) == null ? void 0 : _a.call(onPressRef);
2673
+ animateToHidden();
2499
2674
  }, [animateToHidden]);
2500
2675
  (0, import_react.useEffect)(() => {
2501
2676
  if (isLoading) {
@@ -2580,10 +2755,11 @@ function Bubble({
2580
2755
  ]
2581
2756
  }));
2582
2757
  const borderAnimatedStyle = (0, import_react_native_reanimated.useAnimatedStyle)(() => {
2758
+ const isWarning = loadingBorderTone === "warning";
2583
2759
  const borderColor = (0, import_react_native_reanimated.interpolateColor)(
2584
2760
  borderPulse.value,
2585
2761
  [0, 1],
2586
- 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)"]
2762
+ 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)"]
2587
2763
  );
2588
2764
  return {
2589
2765
  borderWidth: isLoading ? 2 : 0,
@@ -2663,23 +2839,6 @@ var styles = import_react_native11.StyleSheet.create({
2663
2839
  var React14 = __toESM(require("react"));
2664
2840
  var import_react_native12 = require("react-native");
2665
2841
  var import_expo_linear_gradient = require("expo-linear-gradient");
2666
-
2667
- // src/components/utils/color.ts
2668
- function withAlpha(color, alpha) {
2669
- const a = Math.max(0, Math.min(1, alpha));
2670
- const hex = color.trim();
2671
- if (!hex.startsWith("#")) return color;
2672
- const raw = hex.slice(1);
2673
- const expanded = raw.length === 3 ? raw.split("").map((c) => c + c).join("") : raw;
2674
- if (expanded.length !== 6) return color;
2675
- const r = Number.parseInt(expanded.slice(0, 2), 16);
2676
- const g = Number.parseInt(expanded.slice(2, 4), 16);
2677
- const b = Number.parseInt(expanded.slice(4, 6), 16);
2678
- if ([r, g, b].some((n) => Number.isNaN(n))) return color;
2679
- return `rgba(${r}, ${g}, ${b}, ${a})`;
2680
- }
2681
-
2682
- // src/components/overlays/EdgeGlowFrame.tsx
2683
2842
  var import_jsx_runtime10 = require("react/jsx-runtime");
2684
2843
  function baseColor(role, theme) {
2685
2844
  switch (role) {
@@ -3401,7 +3560,6 @@ function ChatComposer({
3401
3560
  disabled = false,
3402
3561
  sendDisabled = false,
3403
3562
  sending = false,
3404
- autoFocus = false,
3405
3563
  onSend,
3406
3564
  attachments = [],
3407
3565
  onRemoveAttachment,
@@ -3424,18 +3582,6 @@ function ChatComposer({
3424
3582
  const maxInputHeight = React19.useMemo(() => import_react_native18.Dimensions.get("window").height * 0.5, []);
3425
3583
  const shakeAnim = React19.useRef(new import_react_native18.Animated.Value(0)).current;
3426
3584
  const [sendPressed, setSendPressed] = React19.useState(false);
3427
- const inputRef = React19.useRef(null);
3428
- const prevAutoFocusRef = React19.useRef(false);
3429
- React19.useEffect(() => {
3430
- const shouldFocus = autoFocus && !prevAutoFocusRef.current && !disabled && !sending;
3431
- prevAutoFocusRef.current = autoFocus;
3432
- if (!shouldFocus) return;
3433
- const t = setTimeout(() => {
3434
- var _a;
3435
- (_a = inputRef.current) == null ? void 0 : _a.focus();
3436
- }, 75);
3437
- return () => clearTimeout(t);
3438
- }, [autoFocus, disabled, sending]);
3439
3585
  const triggerShake = React19.useCallback(() => {
3440
3586
  shakeAnim.setValue(0);
3441
3587
  import_react_native18.Animated.sequence([
@@ -3466,7 +3612,7 @@ function ChatComposer({
3466
3612
  onLayout: (e) => onLayout == null ? void 0 : onLayout({ height: e.nativeEvent.layout.height }),
3467
3613
  children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_react_native18.View, { style: { flexDirection: "row", alignItems: "flex-end", gap: 8 }, children: [
3468
3614
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_react_native18.Animated.View, { style: { flex: 1, transform: [{ translateX: shakeAnim }] }, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
3469
- ResettableLiquidGlassView,
3615
+ import_liquid_glass4.LiquidGlassView,
3470
3616
  {
3471
3617
  style: [
3472
3618
  // LiquidGlassView doesn't reliably auto-size to children; ensure enough height for the
@@ -3518,13 +3664,12 @@ function ChatComposer({
3518
3664
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
3519
3665
  MultilineTextInput,
3520
3666
  {
3521
- ref: inputRef,
3522
3667
  value: text,
3523
3668
  onChangeText: setText,
3524
3669
  placeholder,
3525
3670
  editable: !disabled && !sending,
3526
3671
  useBottomSheetTextInput,
3527
- autoFocus,
3672
+ autoFocus: false,
3528
3673
  placeholderTextColor,
3529
3674
  scrollEnabled: true,
3530
3675
  style: {
@@ -4950,10 +5095,42 @@ var import_lucide_react_native7 = require("lucide-react-native");
4950
5095
  // src/components/primitives/MarkdownText.tsx
4951
5096
  var import_react_native37 = require("react-native");
4952
5097
  var import_react_native_markdown_display = __toESM(require("react-native-markdown-display"));
5098
+ var import_react2 = require("react");
4953
5099
  var import_jsx_runtime39 = require("react/jsx-runtime");
5100
+ function copyMarkdownToClipboard(markdown) {
5101
+ var _a;
5102
+ if (!markdown) {
5103
+ return;
5104
+ }
5105
+ const navigatorClipboard = (_a = globalThis == null ? void 0 : globalThis.navigator) == null ? void 0 : _a.clipboard;
5106
+ if (navigatorClipboard == null ? void 0 : navigatorClipboard.writeText) {
5107
+ void navigatorClipboard.writeText(markdown);
5108
+ return;
5109
+ }
5110
+ try {
5111
+ const expoClipboard = require("expo-clipboard");
5112
+ if (expoClipboard == null ? void 0 : expoClipboard.setStringAsync) {
5113
+ void expoClipboard.setStringAsync(markdown);
5114
+ return;
5115
+ }
5116
+ } catch {
5117
+ }
5118
+ try {
5119
+ const rnClipboard = require("@react-native-clipboard/clipboard");
5120
+ if (rnClipboard == null ? void 0 : rnClipboard.setString) {
5121
+ rnClipboard.setString(markdown);
5122
+ }
5123
+ } catch {
5124
+ }
5125
+ }
4954
5126
  function MarkdownText({ markdown, variant = "chat", bodyColor, style }) {
4955
5127
  const theme = useTheme();
4956
5128
  const isDark = theme.scheme === "dark";
5129
+ const [showCopied, setShowCopied] = (0, import_react2.useState)(false);
5130
+ const [tooltipPosition, setTooltipPosition] = (0, import_react2.useState)(null);
5131
+ const [tooltipWidth, setTooltipWidth] = (0, import_react2.useState)(0);
5132
+ const hideTimerRef = (0, import_react2.useRef)(null);
5133
+ const containerRef = (0, import_react2.useRef)(null);
4957
5134
  const baseBodyColor = variant === "mergeRequest" ? theme.colors.textMuted : theme.colors.text;
4958
5135
  const linkColor = variant === "mergeRequest" ? isDark ? theme.colors.primary : "#3700B3" : theme.colors.link;
4959
5136
  const linkWeight = variant === "mergeRequest" ? theme.typography.fontWeight.semibold : void 0;
@@ -4961,40 +5138,96 @@ function MarkdownText({ markdown, variant = "chat", bodyColor, style }) {
4961
5138
  const codeTextColor = isDark ? "#FFFFFF" : theme.colors.text;
4962
5139
  const paragraphBottom = variant === "mergeRequest" ? 8 : 6;
4963
5140
  const baseLineHeight = variant === "mergeRequest" ? 22 : 20;
4964
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_react_native37.View, { style, children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
4965
- import_react_native_markdown_display.default,
4966
- {
4967
- style: {
4968
- body: { color: bodyColor ?? baseBodyColor, fontSize: 14, lineHeight: baseLineHeight },
4969
- paragraph: { marginTop: 0, marginBottom: paragraphBottom },
4970
- link: { color: linkColor, fontWeight: linkWeight },
4971
- code_inline: {
4972
- backgroundColor: codeBgColor,
4973
- color: codeTextColor,
4974
- paddingHorizontal: variant === "mergeRequest" ? 6 : 4,
4975
- paddingVertical: variant === "mergeRequest" ? 2 : 0,
4976
- borderRadius: variant === "mergeRequest" ? 6 : 4,
4977
- fontFamily: import_react_native37.Platform.OS === "ios" ? "Menlo" : "monospace",
4978
- fontSize: 13
5141
+ (0, import_react2.useEffect)(() => {
5142
+ return () => {
5143
+ if (hideTimerRef.current) {
5144
+ clearTimeout(hideTimerRef.current);
5145
+ }
5146
+ };
5147
+ }, []);
5148
+ const handleLongPress = (event) => {
5149
+ var _a;
5150
+ const { locationX, locationY, pageX, pageY } = event.nativeEvent;
5151
+ if ((_a = containerRef.current) == null ? void 0 : _a.measureInWindow) {
5152
+ containerRef.current.measureInWindow((x, y) => {
5153
+ setTooltipPosition({ x: pageX - x, y: pageY - y });
5154
+ });
5155
+ } else {
5156
+ setTooltipPosition({ x: locationX, y: locationY });
5157
+ }
5158
+ copyMarkdownToClipboard(markdown);
5159
+ setShowCopied(true);
5160
+ if (hideTimerRef.current) {
5161
+ clearTimeout(hideTimerRef.current);
5162
+ }
5163
+ hideTimerRef.current = setTimeout(() => {
5164
+ setShowCopied(false);
5165
+ }, 1200);
5166
+ };
5167
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_react_native37.Pressable, { style, onLongPress: handleLongPress, children: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_react_native37.View, { ref: containerRef, style: { position: "relative" }, children: [
5168
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
5169
+ import_react_native_markdown_display.default,
5170
+ {
5171
+ style: {
5172
+ body: { color: bodyColor ?? baseBodyColor, fontSize: 14, lineHeight: baseLineHeight },
5173
+ paragraph: { marginTop: 0, marginBottom: paragraphBottom },
5174
+ link: { color: linkColor, fontWeight: linkWeight },
5175
+ code_inline: {
5176
+ backgroundColor: codeBgColor,
5177
+ color: codeTextColor,
5178
+ paddingHorizontal: variant === "mergeRequest" ? 6 : 4,
5179
+ paddingVertical: variant === "mergeRequest" ? 2 : 0,
5180
+ borderRadius: variant === "mergeRequest" ? 6 : 4,
5181
+ fontFamily: import_react_native37.Platform.OS === "ios" ? "Menlo" : "monospace",
5182
+ fontSize: 13
5183
+ },
5184
+ code_block: {
5185
+ backgroundColor: codeBgColor,
5186
+ color: codeTextColor,
5187
+ padding: variant === "mergeRequest" ? 12 : 8,
5188
+ borderRadius: variant === "mergeRequest" ? 8 : 6,
5189
+ marginVertical: variant === "mergeRequest" ? 8 : 0
5190
+ },
5191
+ fence: {
5192
+ backgroundColor: codeBgColor,
5193
+ color: codeTextColor,
5194
+ padding: variant === "mergeRequest" ? 12 : 8,
5195
+ borderRadius: variant === "mergeRequest" ? 8 : 6,
5196
+ marginVertical: variant === "mergeRequest" ? 8 : 0
5197
+ }
4979
5198
  },
4980
- code_block: {
4981
- backgroundColor: codeBgColor,
4982
- color: codeTextColor,
4983
- padding: variant === "mergeRequest" ? 12 : 8,
4984
- borderRadius: variant === "mergeRequest" ? 8 : 6,
4985
- marginVertical: variant === "mergeRequest" ? 8 : 0
5199
+ children: markdown
5200
+ }
5201
+ ),
5202
+ showCopied && tooltipPosition ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
5203
+ import_react_native37.View,
5204
+ {
5205
+ pointerEvents: "none",
5206
+ style: {
5207
+ position: "absolute",
5208
+ left: tooltipPosition.x,
5209
+ top: tooltipPosition.y - theme.spacing.lg - 32,
5210
+ backgroundColor: theme.colors.success,
5211
+ borderRadius: theme.radii.pill,
5212
+ paddingHorizontal: theme.spacing.sm,
5213
+ paddingVertical: theme.spacing.xs,
5214
+ transform: [{ translateX: tooltipWidth ? -tooltipWidth / 2 : 0 }]
4986
5215
  },
4987
- fence: {
4988
- backgroundColor: codeBgColor,
4989
- color: codeTextColor,
4990
- padding: variant === "mergeRequest" ? 12 : 8,
4991
- borderRadius: variant === "mergeRequest" ? 8 : 6,
4992
- marginVertical: variant === "mergeRequest" ? 8 : 0
4993
- }
4994
- },
4995
- children: markdown
4996
- }
4997
- ) });
5216
+ onLayout: (event) => setTooltipWidth(event.nativeEvent.layout.width),
5217
+ children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
5218
+ import_react_native37.Text,
5219
+ {
5220
+ style: {
5221
+ color: theme.colors.onSuccess,
5222
+ fontSize: theme.typography.fontSize.xs,
5223
+ fontWeight: theme.typography.fontWeight.medium
5224
+ },
5225
+ children: "Copied"
5226
+ }
5227
+ )
5228
+ }
5229
+ ) : null
5230
+ ] }) });
4998
5231
  }
4999
5232
 
5000
5233
  // src/components/merge-requests/mergeRequestStatusDisplay.ts
@@ -5322,7 +5555,7 @@ function ReviewMergeRequestCard({
5322
5555
  children: status.text
5323
5556
  }
5324
5557
  ),
5325
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(Text, { style: { color: theme.colors.textMuted, fontSize: 12, lineHeight: 16, marginBottom: 12 }, children: creator ? `${creator.approvedOpenedMergeRequests} approved merge${creator.approvedOpenedMergeRequests !== 1 ? "s" : ""}` : "Loading stats..." }),
5558
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(Text, { style: { color: theme.colors.textMuted, fontSize: 12, lineHeight: 16, marginBottom: 12 }, children: creator ? `${creator.approvedOrMergedMergeRequests} approved merge${creator.approvedOrMergedMergeRequests !== 1 ? "s" : ""}` : "Loading stats..." }),
5326
5559
  mr.description ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(MarkdownText, { markdown: mr.description, variant: "mergeRequest" }) : null
5327
5560
  ] }) : null,
5328
5561
  /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_react_native40.View, { style: { height: 1, backgroundColor: withAlpha(theme.colors.borderStrong, 0.5), marginTop: 12, marginBottom: 12 } }),
@@ -6102,23 +6335,88 @@ ${shareUrl}`;
6102
6335
 
6103
6336
  // src/studio/ui/ChatPanel.tsx
6104
6337
  var React41 = __toESM(require("react"));
6105
- var import_react_native52 = require("react-native");
6338
+ var import_react_native53 = require("react-native");
6106
6339
 
6107
6340
  // src/components/chat/ChatPage.tsx
6108
6341
  var React38 = __toESM(require("react"));
6109
- var import_react_native47 = require("react-native");
6342
+ var import_react_native48 = require("react-native");
6110
6343
  var import_react_native_safe_area_context4 = require("react-native-safe-area-context");
6111
6344
 
6112
6345
  // src/components/chat/ChatMessageList.tsx
6113
6346
  var React37 = __toESM(require("react"));
6114
- var import_react_native46 = require("react-native");
6347
+ var import_react_native47 = require("react-native");
6115
6348
  var import_bottom_sheet5 = require("@gorhom/bottom-sheet");
6116
6349
 
6117
6350
  // src/components/chat/ChatMessageBubble.tsx
6118
- var import_react_native44 = require("react-native");
6351
+ var import_react_native45 = require("react-native");
6119
6352
  var import_lucide_react_native10 = require("lucide-react-native");
6353
+
6354
+ // src/components/primitives/Button.tsx
6355
+ var import_react_native44 = require("react-native");
6120
6356
  var import_jsx_runtime46 = require("react/jsx-runtime");
6121
- function ChatMessageBubble({ message, renderContent, style }) {
6357
+ function backgroundFor2(variant, theme, pressed, disabled) {
6358
+ const { colors } = theme;
6359
+ if (variant === "ghost") return "transparent";
6360
+ if (disabled) {
6361
+ return colors.neutral;
6362
+ }
6363
+ const base = variant === "primary" ? colors.primary : variant === "danger" ? colors.danger : colors.neutral;
6364
+ if (!pressed) return base;
6365
+ return base;
6366
+ }
6367
+ function borderFor(variant, theme) {
6368
+ if (variant !== "ghost") return {};
6369
+ return { borderWidth: 1, borderColor: theme.colors.border };
6370
+ }
6371
+ function paddingFor(size, theme) {
6372
+ switch (size) {
6373
+ case "sm":
6374
+ return { paddingHorizontal: theme.spacing.md, paddingVertical: theme.spacing.sm, minHeight: 36 };
6375
+ case "icon":
6376
+ return { paddingHorizontal: 0, paddingVertical: 0, minHeight: 44, minWidth: 44 };
6377
+ case "md":
6378
+ default:
6379
+ return { paddingHorizontal: theme.spacing.lg, paddingVertical: theme.spacing.md, minHeight: 44 };
6380
+ }
6381
+ }
6382
+ function Button({
6383
+ variant = "neutral",
6384
+ size = "md",
6385
+ disabled,
6386
+ style,
6387
+ children,
6388
+ ...props
6389
+ }) {
6390
+ const theme = useTheme();
6391
+ const isDisabled = disabled ?? void 0;
6392
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
6393
+ import_react_native44.Pressable,
6394
+ {
6395
+ ...props,
6396
+ disabled: isDisabled,
6397
+ style: (state) => {
6398
+ const pressed = state.pressed;
6399
+ const base = {
6400
+ alignItems: "center",
6401
+ justifyContent: "center",
6402
+ flexDirection: "row",
6403
+ borderRadius: size === "icon" ? theme.radii.pill : theme.radii.pill,
6404
+ backgroundColor: backgroundFor2(variant, theme, pressed, isDisabled),
6405
+ opacity: pressed && !isDisabled ? 0.92 : 1,
6406
+ ...paddingFor(size, theme),
6407
+ ...borderFor(variant, theme)
6408
+ };
6409
+ const resolved = typeof style === "function" ? style({ pressed, disabled: isDisabled }) : style;
6410
+ return [base, resolved];
6411
+ },
6412
+ children
6413
+ }
6414
+ );
6415
+ }
6416
+
6417
+ // src/components/chat/ChatMessageBubble.tsx
6418
+ var import_jsx_runtime47 = require("react/jsx-runtime");
6419
+ function ChatMessageBubble({ message, renderContent, isLast, retrying, onRetry, style }) {
6122
6420
  var _a, _b;
6123
6421
  const theme = useTheme();
6124
6422
  const metaEvent = ((_a = message.meta) == null ? void 0 : _a.event) ?? null;
@@ -6133,49 +6431,77 @@ function ChatMessageBubble({ message, renderContent, style }) {
6133
6431
  const bubbleVariant = isHuman ? "surface" : "surfaceRaised";
6134
6432
  const cornerStyle = isHuman ? { borderTopRightRadius: 0 } : { borderTopLeftRadius: 0 };
6135
6433
  const bodyColor = metaStatus === "success" ? theme.colors.success : metaStatus === "error" ? theme.colors.danger : void 0;
6136
- return /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_react_native44.View, { style: [align, style], children: /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
6137
- Surface,
6138
- {
6139
- variant: bubbleVariant,
6140
- style: [
6141
- {
6142
- maxWidth: "85%",
6143
- borderRadius: theme.radii.lg,
6144
- paddingHorizontal: theme.spacing.lg,
6145
- paddingVertical: theme.spacing.md,
6146
- borderWidth: 1,
6147
- borderColor: theme.colors.border
6148
- },
6149
- cornerStyle
6150
- ],
6151
- children: /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(import_react_native44.View, { style: { flexDirection: "row", alignItems: "center" }, children: [
6152
- isMergeCompleted || isSyncCompleted ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_lucide_react_native10.CheckCheck, { size: 16, color: theme.colors.success, style: { marginRight: theme.spacing.sm } }) : null,
6153
- isMergeApproved || isSyncStarted ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_lucide_react_native10.GitMerge, { size: 16, color: theme.colors.text, style: { marginRight: theme.spacing.sm } }) : null,
6154
- /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_react_native44.View, { style: { flexShrink: 1, minWidth: 0 }, children: renderContent ? renderContent(message) : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(MarkdownText, { markdown: message.content, variant: "chat", bodyColor }) })
6155
- ] })
6156
- }
6157
- ) });
6434
+ const showRetry = Boolean(onRetry) && isLast && metaStatus === "error";
6435
+ const retryLabel = retrying ? "Retrying..." : "Retry";
6436
+ return /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(import_react_native45.View, { style: [align, style], children: [
6437
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
6438
+ Surface,
6439
+ {
6440
+ variant: bubbleVariant,
6441
+ style: [
6442
+ {
6443
+ maxWidth: "85%",
6444
+ borderRadius: theme.radii.lg,
6445
+ paddingHorizontal: theme.spacing.lg,
6446
+ paddingVertical: theme.spacing.md,
6447
+ borderWidth: 1,
6448
+ borderColor: theme.colors.border
6449
+ },
6450
+ cornerStyle
6451
+ ],
6452
+ children: /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(import_react_native45.View, { style: { flexDirection: "row", alignItems: "center" }, children: [
6453
+ isMergeCompleted || isSyncCompleted ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react_native10.CheckCheck, { size: 16, color: theme.colors.success, style: { marginRight: theme.spacing.sm } }) : null,
6454
+ isMergeApproved || isSyncStarted ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react_native10.GitMerge, { size: 16, color: theme.colors.text, style: { marginRight: theme.spacing.sm } }) : null,
6455
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_react_native45.View, { style: { flexShrink: 1, minWidth: 0 }, children: renderContent ? renderContent(message) : /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(MarkdownText, { markdown: message.content, variant: "chat", bodyColor }) })
6456
+ ] })
6457
+ }
6458
+ ),
6459
+ showRetry ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_react_native45.View, { style: { marginTop: theme.spacing.xs, alignSelf: align.alignSelf }, children: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
6460
+ Button,
6461
+ {
6462
+ variant: "ghost",
6463
+ size: "sm",
6464
+ onPress: onRetry,
6465
+ disabled: retrying,
6466
+ style: { borderColor: theme.colors.danger },
6467
+ accessibilityLabel: "Retry send",
6468
+ children: /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(import_react_native45.View, { style: { flexDirection: "row", alignItems: "center" }, children: [
6469
+ !retrying ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_lucide_react_native10.RotateCcw, { size: 14, color: theme.colors.danger }) : null,
6470
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
6471
+ Text,
6472
+ {
6473
+ variant: "caption",
6474
+ color: theme.colors.danger,
6475
+ style: { marginLeft: retrying ? 0 : theme.spacing.xs },
6476
+ numberOfLines: 1,
6477
+ children: retryLabel
6478
+ }
6479
+ )
6480
+ ] })
6481
+ }
6482
+ ) }) : null
6483
+ ] });
6158
6484
  }
6159
6485
 
6160
6486
  // src/components/chat/TypingIndicator.tsx
6161
6487
  var React36 = __toESM(require("react"));
6162
- var import_react_native45 = require("react-native");
6163
- var import_jsx_runtime47 = require("react/jsx-runtime");
6488
+ var import_react_native46 = require("react-native");
6489
+ var import_jsx_runtime48 = require("react/jsx-runtime");
6164
6490
  function TypingIndicator({ style }) {
6165
6491
  const theme = useTheme();
6166
6492
  const dotColor = theme.colors.textSubtle;
6167
6493
  const anims = React36.useMemo(
6168
- () => [new import_react_native45.Animated.Value(0.3), new import_react_native45.Animated.Value(0.3), new import_react_native45.Animated.Value(0.3)],
6494
+ () => [new import_react_native46.Animated.Value(0.3), new import_react_native46.Animated.Value(0.3), new import_react_native46.Animated.Value(0.3)],
6169
6495
  []
6170
6496
  );
6171
6497
  React36.useEffect(() => {
6172
6498
  const loops = [];
6173
6499
  anims.forEach((a, idx) => {
6174
- const seq = import_react_native45.Animated.sequence([
6175
- import_react_native45.Animated.timing(a, { toValue: 1, duration: 420, useNativeDriver: true, delay: idx * 140 }),
6176
- import_react_native45.Animated.timing(a, { toValue: 0.3, duration: 420, useNativeDriver: true })
6500
+ const seq = import_react_native46.Animated.sequence([
6501
+ import_react_native46.Animated.timing(a, { toValue: 1, duration: 420, useNativeDriver: true, delay: idx * 140 }),
6502
+ import_react_native46.Animated.timing(a, { toValue: 0.3, duration: 420, useNativeDriver: true })
6177
6503
  ]);
6178
- const loop = import_react_native45.Animated.loop(seq);
6504
+ const loop = import_react_native46.Animated.loop(seq);
6179
6505
  loops.push(loop);
6180
6506
  loop.start();
6181
6507
  });
@@ -6183,8 +6509,8 @@ function TypingIndicator({ style }) {
6183
6509
  loops.forEach((l) => l.stop());
6184
6510
  };
6185
6511
  }, [anims]);
6186
- return /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(import_react_native45.View, { style: [{ flexDirection: "row", alignItems: "center" }, style], children: anims.map((a, i) => /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
6187
- import_react_native45.Animated.View,
6512
+ return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_react_native46.View, { style: [{ flexDirection: "row", alignItems: "center" }, style], children: anims.map((a, i) => /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
6513
+ import_react_native46.Animated.View,
6188
6514
  {
6189
6515
  style: {
6190
6516
  width: 8,
@@ -6193,7 +6519,7 @@ function TypingIndicator({ style }) {
6193
6519
  marginHorizontal: 3,
6194
6520
  backgroundColor: dotColor,
6195
6521
  opacity: a,
6196
- transform: [{ translateY: import_react_native45.Animated.multiply(import_react_native45.Animated.subtract(a, 0.3), 2) }]
6522
+ transform: [{ translateY: import_react_native46.Animated.multiply(import_react_native46.Animated.subtract(a, 0.3), 2) }]
6197
6523
  }
6198
6524
  },
6199
6525
  i
@@ -6201,12 +6527,14 @@ function TypingIndicator({ style }) {
6201
6527
  }
6202
6528
 
6203
6529
  // src/components/chat/ChatMessageList.tsx
6204
- var import_jsx_runtime48 = require("react/jsx-runtime");
6530
+ var import_jsx_runtime49 = require("react/jsx-runtime");
6205
6531
  var ChatMessageList = React37.forwardRef(
6206
6532
  ({
6207
6533
  messages,
6208
6534
  showTypingIndicator = false,
6209
6535
  renderMessageContent,
6536
+ onRetryMessage,
6537
+ isRetryingMessage,
6210
6538
  contentStyle,
6211
6539
  bottomInset = 0,
6212
6540
  onNearBottomChange,
@@ -6220,6 +6548,7 @@ var ChatMessageList = React37.forwardRef(
6220
6548
  const data = React37.useMemo(() => {
6221
6549
  return [...messages].reverse();
6222
6550
  }, [messages]);
6551
+ const lastMessageId = messages.length > 0 ? messages[messages.length - 1].id : null;
6223
6552
  const scrollToBottom = React37.useCallback((options) => {
6224
6553
  var _a;
6225
6554
  const animated = (options == null ? void 0 : options.animated) ?? true;
@@ -6255,7 +6584,7 @@ var ChatMessageList = React37.forwardRef(
6255
6584
  }
6256
6585
  return void 0;
6257
6586
  }, [showTypingIndicator, scrollToBottom]);
6258
- return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
6587
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
6259
6588
  import_bottom_sheet5.BottomSheetFlatList,
6260
6589
  {
6261
6590
  ref: listRef,
@@ -6281,11 +6610,20 @@ var ChatMessageList = React37.forwardRef(
6281
6610
  },
6282
6611
  contentStyle
6283
6612
  ],
6284
- ItemSeparatorComponent: () => /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_react_native46.View, { style: { height: theme.spacing.sm } }),
6285
- renderItem: ({ item }) => /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ChatMessageBubble, { message: item, renderContent: renderMessageContent }),
6286
- ListHeaderComponent: /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(import_react_native46.View, { children: [
6287
- showTypingIndicator ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_react_native46.View, { style: { marginTop: theme.spacing.sm, alignSelf: "flex-start", paddingHorizontal: theme.spacing.lg }, children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(TypingIndicator, {}) }) : null,
6288
- bottomInset > 0 ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(import_react_native46.View, { style: { height: bottomInset } }) : null
6613
+ ItemSeparatorComponent: () => /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_react_native47.View, { style: { height: theme.spacing.sm } }),
6614
+ renderItem: ({ item }) => /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
6615
+ ChatMessageBubble,
6616
+ {
6617
+ message: item,
6618
+ renderContent: renderMessageContent,
6619
+ isLast: Boolean(lastMessageId && item.id === lastMessageId),
6620
+ retrying: (isRetryingMessage == null ? void 0 : isRetryingMessage(item.id)) ?? false,
6621
+ onRetry: onRetryMessage ? () => onRetryMessage(item.id) : void 0
6622
+ }
6623
+ ),
6624
+ ListHeaderComponent: /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(import_react_native47.View, { children: [
6625
+ showTypingIndicator ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_react_native47.View, { style: { marginTop: theme.spacing.sm, alignSelf: "flex-start", paddingHorizontal: theme.spacing.lg }, children: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(TypingIndicator, {}) }) : null,
6626
+ bottomInset > 0 ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_react_native47.View, { style: { height: bottomInset } }) : null
6289
6627
  ] })
6290
6628
  }
6291
6629
  );
@@ -6294,12 +6632,14 @@ var ChatMessageList = React37.forwardRef(
6294
6632
  ChatMessageList.displayName = "ChatMessageList";
6295
6633
 
6296
6634
  // src/components/chat/ChatPage.tsx
6297
- var import_jsx_runtime49 = require("react/jsx-runtime");
6635
+ var import_jsx_runtime50 = require("react/jsx-runtime");
6298
6636
  function ChatPage({
6299
6637
  header,
6300
6638
  messages,
6301
6639
  showTypingIndicator,
6302
6640
  renderMessageContent,
6641
+ onRetryMessage,
6642
+ isRetryingMessage,
6303
6643
  topBanner,
6304
6644
  composerTop,
6305
6645
  composer,
@@ -6313,17 +6653,7 @@ function ChatPage({
6313
6653
  const insets = (0, import_react_native_safe_area_context4.useSafeAreaInsets)();
6314
6654
  const [composerHeight, setComposerHeight] = React38.useState(0);
6315
6655
  const [composerTopHeight, setComposerTopHeight] = React38.useState(0);
6316
- const [keyboardVisible, setKeyboardVisible] = React38.useState(false);
6317
- React38.useEffect(() => {
6318
- if (import_react_native47.Platform.OS !== "ios") return;
6319
- const show = import_react_native47.Keyboard.addListener("keyboardWillShow", () => setKeyboardVisible(true));
6320
- const hide = import_react_native47.Keyboard.addListener("keyboardWillHide", () => setKeyboardVisible(false));
6321
- return () => {
6322
- show.remove();
6323
- hide.remove();
6324
- };
6325
- }, []);
6326
- const footerBottomPadding = import_react_native47.Platform.OS === "ios" ? keyboardVisible ? 0 : insets.bottom : insets.bottom + 10;
6656
+ const footerBottomPadding = import_react_native48.Platform.OS === "ios" ? insets.bottom : insets.bottom + 10;
6327
6657
  const totalComposerHeight = composerHeight + composerTopHeight;
6328
6658
  const overlayBottom = totalComposerHeight + footerBottomPadding + theme.spacing.lg;
6329
6659
  const bottomInset = totalComposerHeight + footerBottomPadding + theme.spacing.xl;
@@ -6340,22 +6670,24 @@ function ChatPage({
6340
6670
  if (composerTop) return;
6341
6671
  setComposerTopHeight(0);
6342
6672
  }, [composerTop]);
6343
- return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(import_react_native47.View, { style: [{ flex: 1 }, style], children: [
6344
- header ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_react_native47.View, { children: header }) : null,
6345
- topBanner ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(import_react_native47.View, { style: { paddingHorizontal: theme.spacing.lg, paddingTop: theme.spacing.sm }, children: topBanner }) : null,
6346
- /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(import_react_native47.View, { style: { flex: 1 }, children: [
6347
- /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(
6348
- import_react_native47.View,
6673
+ return /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(import_react_native48.View, { style: [{ flex: 1 }, style], children: [
6674
+ header ? /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_react_native48.View, { children: header }) : null,
6675
+ topBanner ? /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(import_react_native48.View, { style: { paddingHorizontal: theme.spacing.lg, paddingTop: theme.spacing.sm }, children: topBanner }) : null,
6676
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(import_react_native48.View, { style: { flex: 1 }, children: [
6677
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(
6678
+ import_react_native48.View,
6349
6679
  {
6350
6680
  style: { flex: 1 },
6351
6681
  children: [
6352
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
6682
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
6353
6683
  ChatMessageList,
6354
6684
  {
6355
6685
  ref: listRef,
6356
6686
  messages,
6357
6687
  showTypingIndicator,
6358
6688
  renderMessageContent,
6689
+ onRetryMessage,
6690
+ isRetryingMessage,
6359
6691
  onNearBottomChange,
6360
6692
  bottomInset
6361
6693
  }
@@ -6364,8 +6696,8 @@ function ChatPage({
6364
6696
  ]
6365
6697
  }
6366
6698
  ),
6367
- /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)(
6368
- import_react_native47.View,
6699
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(
6700
+ import_react_native48.View,
6369
6701
  {
6370
6702
  style: {
6371
6703
  position: "absolute",
@@ -6377,15 +6709,15 @@ function ChatPage({
6377
6709
  paddingBottom: footerBottomPadding
6378
6710
  },
6379
6711
  children: [
6380
- composerTop ? /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
6381
- import_react_native47.View,
6712
+ composerTop ? /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
6713
+ import_react_native48.View,
6382
6714
  {
6383
6715
  style: { marginBottom: theme.spacing.sm },
6384
6716
  onLayout: (e) => setComposerTopHeight(e.nativeEvent.layout.height),
6385
6717
  children: composerTop
6386
6718
  }
6387
6719
  ) : null,
6388
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
6720
+ /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
6389
6721
  ChatComposer,
6390
6722
  {
6391
6723
  ...composer,
@@ -6402,9 +6734,9 @@ function ChatPage({
6402
6734
 
6403
6735
  // src/components/chat/ScrollToBottomButton.tsx
6404
6736
  var React39 = __toESM(require("react"));
6405
- var import_react_native48 = require("react-native");
6737
+ var import_react_native49 = require("react-native");
6406
6738
  var import_react_native_reanimated2 = __toESM(require("react-native-reanimated"));
6407
- var import_jsx_runtime50 = require("react/jsx-runtime");
6739
+ var import_jsx_runtime51 = require("react/jsx-runtime");
6408
6740
  function ScrollToBottomButton({ visible, onPress, children, style }) {
6409
6741
  const theme = useTheme();
6410
6742
  const progress = (0, import_react_native_reanimated2.useSharedValue)(visible ? 1 : 0);
@@ -6418,7 +6750,7 @@ function ScrollToBottomButton({ visible, onPress, children, style }) {
6418
6750
  }));
6419
6751
  const bg = theme.scheme === "dark" ? "rgba(39,39,42,0.9)" : "rgba(244,244,245,0.95)";
6420
6752
  const border = theme.scheme === "dark" ? withAlpha("#FFFFFF", 0.12) : withAlpha("#000000", 0.08);
6421
- return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
6753
+ return /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
6422
6754
  import_react_native_reanimated2.default.View,
6423
6755
  {
6424
6756
  pointerEvents: visible ? "auto" : "none",
@@ -6432,8 +6764,8 @@ function ScrollToBottomButton({ visible, onPress, children, style }) {
6432
6764
  style,
6433
6765
  animStyle
6434
6766
  ],
6435
- children: /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
6436
- import_react_native48.View,
6767
+ children: /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
6768
+ import_react_native49.View,
6437
6769
  {
6438
6770
  style: {
6439
6771
  width: 44,
@@ -6451,8 +6783,8 @@ function ScrollToBottomButton({ visible, onPress, children, style }) {
6451
6783
  elevation: 5,
6452
6784
  opacity: pressed ? 0.85 : 1
6453
6785
  },
6454
- children: /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(
6455
- import_react_native48.Pressable,
6786
+ children: /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
6787
+ import_react_native49.Pressable,
6456
6788
  {
6457
6789
  onPress,
6458
6790
  onPressIn: () => setPressed(true),
@@ -6469,16 +6801,16 @@ function ScrollToBottomButton({ visible, onPress, children, style }) {
6469
6801
  }
6470
6802
 
6471
6803
  // src/components/chat/ChatHeader.tsx
6472
- var import_react_native49 = require("react-native");
6473
- var import_jsx_runtime51 = require("react/jsx-runtime");
6804
+ var import_react_native50 = require("react-native");
6805
+ var import_jsx_runtime52 = require("react/jsx-runtime");
6474
6806
  function ChatHeader({ left, right, center, style }) {
6475
- const flattenedStyle = import_react_native49.StyleSheet.flatten([
6807
+ const flattenedStyle = import_react_native50.StyleSheet.flatten([
6476
6808
  {
6477
6809
  paddingTop: 0
6478
6810
  },
6479
6811
  style
6480
6812
  ]);
6481
- return /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
6813
+ return /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
6482
6814
  StudioSheetHeader,
6483
6815
  {
6484
6816
  left,
@@ -6490,13 +6822,13 @@ function ChatHeader({ left, right, center, style }) {
6490
6822
  }
6491
6823
 
6492
6824
  // src/components/chat/ForkNoticeBanner.tsx
6493
- var import_react_native50 = require("react-native");
6494
- var import_jsx_runtime52 = require("react/jsx-runtime");
6825
+ var import_react_native51 = require("react-native");
6826
+ var import_jsx_runtime53 = require("react/jsx-runtime");
6495
6827
  function ForkNoticeBanner({ isOwner = true, title, description, style }) {
6496
6828
  const theme = useTheme();
6497
6829
  const resolvedTitle = title ?? (isOwner ? "Remixed app" : "Remix app");
6498
6830
  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.");
6499
- return /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
6831
+ return /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
6500
6832
  Card,
6501
6833
  {
6502
6834
  variant: "surfaceRaised",
@@ -6511,8 +6843,8 @@ function ForkNoticeBanner({ isOwner = true, title, description, style }) {
6511
6843
  },
6512
6844
  style
6513
6845
  ],
6514
- children: /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)(import_react_native50.View, { style: { minWidth: 0 }, children: [
6515
- /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
6846
+ children: /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)(import_react_native51.View, { style: { minWidth: 0 }, children: [
6847
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
6516
6848
  Text,
6517
6849
  {
6518
6850
  style: {
@@ -6526,7 +6858,7 @@ function ForkNoticeBanner({ isOwner = true, title, description, style }) {
6526
6858
  children: resolvedTitle
6527
6859
  }
6528
6860
  ),
6529
- /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
6861
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
6530
6862
  Text,
6531
6863
  {
6532
6864
  style: {
@@ -6545,8 +6877,8 @@ function ForkNoticeBanner({ isOwner = true, title, description, style }) {
6545
6877
 
6546
6878
  // src/components/chat/ChatQueue.tsx
6547
6879
  var React40 = __toESM(require("react"));
6548
- var import_react_native51 = require("react-native");
6549
- var import_jsx_runtime53 = require("react/jsx-runtime");
6880
+ var import_react_native52 = require("react-native");
6881
+ var import_jsx_runtime54 = require("react/jsx-runtime");
6550
6882
  function ChatQueue({ items, onRemove }) {
6551
6883
  const theme = useTheme();
6552
6884
  const [expanded, setExpanded] = React40.useState({});
@@ -6578,8 +6910,8 @@ ${trimmedLine2}\u2026 `;
6578
6910
  setRemoving((prev) => Object.fromEntries(Object.entries(prev).filter(([id]) => ids.has(id))));
6579
6911
  }, [items]);
6580
6912
  if (items.length === 0) return null;
6581
- return /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)(
6582
- import_react_native51.View,
6913
+ return /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)(
6914
+ import_react_native52.View,
6583
6915
  {
6584
6916
  style: {
6585
6917
  borderWidth: 1,
@@ -6590,16 +6922,16 @@ ${trimmedLine2}\u2026 `;
6590
6922
  backgroundColor: "transparent"
6591
6923
  },
6592
6924
  children: [
6593
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(Text, { variant: "caption", style: { marginBottom: theme.spacing.sm }, children: "Queue" }),
6594
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_react_native51.View, { style: { gap: theme.spacing.sm }, children: items.map((item) => {
6925
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(Text, { variant: "caption", style: { marginBottom: theme.spacing.sm }, children: "Queue" }),
6926
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(import_react_native52.View, { style: { gap: theme.spacing.sm }, children: items.map((item) => {
6595
6927
  const isExpanded = Boolean(expanded[item.id]);
6596
6928
  const showToggle = Boolean(canExpand[item.id]);
6597
6929
  const prompt = item.prompt ?? "";
6598
6930
  const moreLabel = "more";
6599
6931
  const displayPrompt = !isExpanded && showToggle && collapsedText[item.id] ? collapsedText[item.id] : prompt;
6600
6932
  const isRemoving = Boolean(removing[item.id]);
6601
- return /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)(
6602
- import_react_native51.View,
6933
+ return /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)(
6934
+ import_react_native52.View,
6603
6935
  {
6604
6936
  style: {
6605
6937
  flexDirection: "row",
@@ -6611,8 +6943,8 @@ ${trimmedLine2}\u2026 `;
6611
6943
  backgroundColor: withAlpha(theme.colors.surface, theme.scheme === "dark" ? 0.8 : 0.9)
6612
6944
  },
6613
6945
  children: [
6614
- /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)(import_react_native51.View, { style: { flex: 1 }, children: [
6615
- !canExpand[item.id] ? /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
6946
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)(import_react_native52.View, { style: { flex: 1 }, children: [
6947
+ !canExpand[item.id] ? /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
6616
6948
  Text,
6617
6949
  {
6618
6950
  style: { position: "absolute", opacity: 0, zIndex: -1, width: "100%" },
@@ -6631,14 +6963,14 @@ ${trimmedLine2}\u2026 `;
6631
6963
  children: prompt
6632
6964
  }
6633
6965
  ) : null,
6634
- /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)(
6966
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)(
6635
6967
  Text,
6636
6968
  {
6637
6969
  variant: "bodyMuted",
6638
6970
  numberOfLines: isExpanded ? void 0 : 2,
6639
6971
  children: [
6640
6972
  displayPrompt,
6641
- !isExpanded && showToggle ? /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
6973
+ !isExpanded && showToggle ? /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
6642
6974
  Text,
6643
6975
  {
6644
6976
  color: theme.colors.text,
@@ -6650,18 +6982,18 @@ ${trimmedLine2}\u2026 `;
6650
6982
  ]
6651
6983
  }
6652
6984
  ),
6653
- showToggle && isExpanded ? /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
6654
- import_react_native51.Pressable,
6985
+ showToggle && isExpanded ? /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
6986
+ import_react_native52.Pressable,
6655
6987
  {
6656
6988
  onPress: () => setExpanded((prev) => ({ ...prev, [item.id]: false })),
6657
6989
  hitSlop: 6,
6658
6990
  style: { alignSelf: "flex-start", marginTop: 4 },
6659
- children: /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(Text, { variant: "captionMuted", color: theme.colors.text, children: "less" })
6991
+ children: /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(Text, { variant: "captionMuted", color: theme.colors.text, children: "less" })
6660
6992
  }
6661
6993
  ) : null
6662
6994
  ] }),
6663
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
6664
- import_react_native51.Pressable,
6995
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
6996
+ import_react_native52.Pressable,
6665
6997
  {
6666
6998
  onPress: () => {
6667
6999
  if (!onRemove || isRemoving) return;
@@ -6676,7 +7008,7 @@ ${trimmedLine2}\u2026 `;
6676
7008
  },
6677
7009
  hitSlop: 8,
6678
7010
  style: { alignSelf: "center" },
6679
- children: isRemoving ? /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_react_native51.ActivityIndicator, { size: "small", color: theme.colors.text }) : /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(IconClose, { size: 14, colorToken: "text" })
7011
+ children: isRemoving ? /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(import_react_native52.ActivityIndicator, { size: "small", color: theme.colors.text }) : /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(IconClose, { size: 14, colorToken: "text" })
6680
7012
  }
6681
7013
  )
6682
7014
  ]
@@ -6690,10 +7022,9 @@ ${trimmedLine2}\u2026 `;
6690
7022
  }
6691
7023
 
6692
7024
  // src/studio/ui/ChatPanel.tsx
6693
- var import_jsx_runtime54 = require("react/jsx-runtime");
7025
+ var import_jsx_runtime55 = require("react/jsx-runtime");
6694
7026
  function ChatPanel({
6695
7027
  title = "Chat",
6696
- autoFocusComposer = false,
6697
7028
  messages,
6698
7029
  showTypingIndicator,
6699
7030
  loading,
@@ -6709,6 +7040,8 @@ function ChatPanel({
6709
7040
  onNavigateHome,
6710
7041
  onStartDraw,
6711
7042
  onSend,
7043
+ onRetryMessage,
7044
+ isRetryingMessage,
6712
7045
  queueItems = [],
6713
7046
  onRemoveQueueItem
6714
7047
  }) {
@@ -6732,21 +7065,21 @@ function ChatPanel({
6732
7065
  var _a;
6733
7066
  (_a = listRef.current) == null ? void 0 : _a.scrollToBottom({ animated: true });
6734
7067
  }, []);
6735
- const header = /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
7068
+ const header = /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
6736
7069
  ChatHeader,
6737
7070
  {
6738
- left: /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)(import_react_native52.View, { style: { flexDirection: "row", alignItems: "center" }, children: [
6739
- /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(StudioSheetHeaderIconButton, { onPress: onBack, accessibilityLabel: "Back", style: { marginRight: 8 }, children: /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(IconBack, { size: 20, colorToken: "floatingContent" }) }),
6740
- onNavigateHome ? /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(StudioSheetHeaderIconButton, { onPress: onNavigateHome, accessibilityLabel: "Home", children: /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(IconHome, { size: 20, colorToken: "floatingContent" }) }) : null
7071
+ left: /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)(import_react_native53.View, { style: { flexDirection: "row", alignItems: "center" }, children: [
7072
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(StudioSheetHeaderIconButton, { onPress: onBack, accessibilityLabel: "Back", style: { marginRight: 8 }, children: /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(IconBack, { size: 20, colorToken: "floatingContent" }) }),
7073
+ onNavigateHome ? /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(StudioSheetHeaderIconButton, { onPress: onNavigateHome, accessibilityLabel: "Home", children: /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(IconHome, { size: 20, colorToken: "floatingContent" }) }) : null
6741
7074
  ] }),
6742
- right: /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)(import_react_native52.View, { style: { flexDirection: "row", alignItems: "center" }, children: [
6743
- onStartDraw ? /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(StudioSheetHeaderIconButton, { onPress: onStartDraw, accessibilityLabel: "Draw", intent: "danger", style: { marginRight: 8 }, children: /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(IconDraw, { size: 20, colorToken: "onDanger" }) }) : null,
6744
- /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(StudioSheetHeaderIconButton, { onPress: onClose, accessibilityLabel: "Close", children: /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(IconClose, { size: 20, colorToken: "floatingContent" }) })
7075
+ right: /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)(import_react_native53.View, { style: { flexDirection: "row", alignItems: "center" }, children: [
7076
+ onStartDraw ? /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(StudioSheetHeaderIconButton, { onPress: onStartDraw, accessibilityLabel: "Draw", intent: "danger", style: { marginRight: 8 }, children: /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(IconDraw, { size: 20, colorToken: "onDanger" }) }) : null,
7077
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(StudioSheetHeaderIconButton, { onPress: onClose, accessibilityLabel: "Close", children: /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(IconClose, { size: 20, colorToken: "floatingContent" }) })
6745
7078
  ] }),
6746
7079
  center: null
6747
7080
  }
6748
7081
  );
6749
- const topBanner = shouldForkOnEdit ? /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
7082
+ const topBanner = shouldForkOnEdit ? /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
6750
7083
  ForkNoticeBanner,
6751
7084
  {
6752
7085
  isOwner: !shouldForkOnEdit,
@@ -6755,35 +7088,37 @@ function ChatPanel({
6755
7088
  ) : null;
6756
7089
  const showMessagesLoading = Boolean(loading) && messages.length === 0 || forking;
6757
7090
  if (showMessagesLoading) {
6758
- return /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)(import_react_native52.View, { style: { flex: 1 }, children: [
6759
- /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(import_react_native52.View, { children: header }),
6760
- topBanner ? /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(import_react_native52.View, { style: { paddingHorizontal: 16, paddingTop: 8 }, children: topBanner }) : null,
6761
- /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)(import_react_native52.View, { style: { flex: 1, alignItems: "center", justifyContent: "center", paddingHorizontal: 24, paddingVertical: 12 }, children: [
6762
- /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(import_react_native52.ActivityIndicator, {}),
6763
- /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(import_react_native52.View, { style: { height: 12 } }),
6764
- /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(Text, { variant: "bodyMuted", children: forking ? "Creating your copy\u2026" : "Loading messages\u2026" })
7091
+ return /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)(import_react_native53.View, { style: { flex: 1 }, children: [
7092
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(import_react_native53.View, { children: header }),
7093
+ topBanner ? /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(import_react_native53.View, { style: { paddingHorizontal: 16, paddingTop: 8 }, children: topBanner }) : null,
7094
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)(import_react_native53.View, { style: { flex: 1, alignItems: "center", justifyContent: "center", paddingHorizontal: 24, paddingVertical: 12 }, children: [
7095
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(import_react_native53.ActivityIndicator, {}),
7096
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(import_react_native53.View, { style: { height: 12 } }),
7097
+ /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(Text, { variant: "bodyMuted", children: forking ? "Creating your copy\u2026" : "Loading messages\u2026" })
6765
7098
  ] })
6766
7099
  ] });
6767
7100
  }
6768
- const queueTop = queueItems.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(ChatQueue, { items: queueItems, onRemove: onRemoveQueueItem }) : null;
6769
- return /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
7101
+ const queueTop = queueItems.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(ChatQueue, { items: queueItems, onRemove: onRemoveQueueItem }) : null;
7102
+ return /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
6770
7103
  ChatPage,
6771
7104
  {
6772
7105
  header,
6773
7106
  messages,
6774
7107
  showTypingIndicator,
7108
+ onRetryMessage,
7109
+ isRetryingMessage,
6775
7110
  topBanner,
6776
7111
  composerTop: queueTop,
6777
7112
  composerHorizontalPadding: 0,
6778
7113
  listRef,
6779
7114
  onNearBottomChange: setNearBottom,
6780
- overlay: /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
7115
+ overlay: /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
6781
7116
  ScrollToBottomButton,
6782
7117
  {
6783
7118
  visible: !nearBottom,
6784
7119
  onPress: handleScrollToBottom,
6785
7120
  style: { bottom: 80 },
6786
- children: /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(IconArrowDown, { size: 20, colorToken: "floatingContent" })
7121
+ children: /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(IconArrowDown, { size: 20, colorToken: "floatingContent" })
6787
7122
  }
6788
7123
  ),
6789
7124
  composer: {
@@ -6792,7 +7127,6 @@ function ChatPanel({
6792
7127
  disabled: Boolean(loading) || Boolean(forking),
6793
7128
  sendDisabled: Boolean(sendDisabled) || Boolean(loading) || Boolean(forking),
6794
7129
  sending: Boolean(sending),
6795
- autoFocus: autoFocusComposer,
6796
7130
  onSend: handleSend,
6797
7131
  attachments,
6798
7132
  onRemoveAttachment,
@@ -6805,11 +7139,11 @@ function ChatPanel({
6805
7139
 
6806
7140
  // src/components/dialogs/ConfirmMergeRequestDialog.tsx
6807
7141
  var React42 = __toESM(require("react"));
6808
- var import_react_native54 = require("react-native");
7142
+ var import_react_native55 = require("react-native");
6809
7143
 
6810
7144
  // src/components/primitives/Modal.tsx
6811
- var import_react_native53 = require("react-native");
6812
- var import_jsx_runtime55 = require("react/jsx-runtime");
7145
+ var import_react_native54 = require("react-native");
7146
+ var import_jsx_runtime56 = require("react/jsx-runtime");
6813
7147
  function Modal({
6814
7148
  visible,
6815
7149
  onRequestClose,
@@ -6818,30 +7152,30 @@ function Modal({
6818
7152
  contentStyle
6819
7153
  }) {
6820
7154
  const theme = useTheme();
6821
- return /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
6822
- import_react_native53.Modal,
7155
+ return /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
7156
+ import_react_native54.Modal,
6823
7157
  {
6824
7158
  visible,
6825
7159
  transparent: true,
6826
7160
  animationType: "fade",
6827
7161
  onRequestClose,
6828
- children: /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)(import_react_native53.View, { style: { flex: 1, backgroundColor: theme.colors.backdrop, justifyContent: "center", padding: theme.spacing.lg }, children: [
6829
- /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(
6830
- import_react_native53.Pressable,
7162
+ children: /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)(import_react_native54.View, { style: { flex: 1, backgroundColor: theme.colors.backdrop, justifyContent: "center", padding: theme.spacing.lg }, children: [
7163
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
7164
+ import_react_native54.Pressable,
6831
7165
  {
6832
7166
  accessibilityRole: "button",
6833
7167
  onPress: dismissOnBackdropPress ? onRequestClose : void 0,
6834
7168
  style: { position: "absolute", inset: 0 }
6835
7169
  }
6836
7170
  ),
6837
- /* @__PURE__ */ (0, import_jsx_runtime55.jsx)(Card, { variant: "surfaceRaised", padded: true, style: [{ borderRadius: theme.radii.xl }, contentStyle], children })
7171
+ /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(Card, { variant: "surfaceRaised", padded: true, style: [{ borderRadius: theme.radii.xl }, contentStyle], children })
6838
7172
  ] })
6839
7173
  }
6840
7174
  );
6841
7175
  }
6842
7176
 
6843
7177
  // src/components/dialogs/ConfirmMergeRequestDialog.tsx
6844
- var import_jsx_runtime56 = require("react/jsx-runtime");
7178
+ var import_jsx_runtime57 = require("react/jsx-runtime");
6845
7179
  function ConfirmMergeRequestDialog({
6846
7180
  visible,
6847
7181
  onOpenChange,
@@ -6871,7 +7205,7 @@ function ConfirmMergeRequestDialog({
6871
7205
  justifyContent: "center",
6872
7206
  alignSelf: "stretch"
6873
7207
  };
6874
- return /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)(
7208
+ return /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)(
6875
7209
  Modal,
6876
7210
  {
6877
7211
  visible,
@@ -6882,7 +7216,7 @@ function ConfirmMergeRequestDialog({
6882
7216
  backgroundColor: theme.colors.background
6883
7217
  },
6884
7218
  children: [
6885
- /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(import_react_native54.View, { children: /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
7219
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(import_react_native55.View, { children: /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(
6886
7220
  Text,
6887
7221
  {
6888
7222
  style: {
@@ -6894,9 +7228,9 @@ function ConfirmMergeRequestDialog({
6894
7228
  children: "Are you sure you want to approve this merge request?"
6895
7229
  }
6896
7230
  ) }),
6897
- /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)(import_react_native54.View, { style: { marginTop: 16 }, children: [
6898
- /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
6899
- import_react_native54.View,
7231
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsxs)(import_react_native55.View, { style: { marginTop: 16 }, children: [
7232
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(
7233
+ import_react_native55.View,
6900
7234
  {
6901
7235
  style: [
6902
7236
  fullWidthButtonBase,
@@ -6905,22 +7239,22 @@ function ConfirmMergeRequestDialog({
6905
7239
  opacity: canConfirm ? 1 : 0.5
6906
7240
  }
6907
7241
  ],
6908
- children: /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
6909
- import_react_native54.Pressable,
7242
+ children: /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(
7243
+ import_react_native55.Pressable,
6910
7244
  {
6911
7245
  accessibilityRole: "button",
6912
7246
  accessibilityLabel: "Approve Merge",
6913
7247
  disabled: !canConfirm,
6914
7248
  onPress: handleConfirm,
6915
7249
  style: [fullWidthButtonBase, { flex: 1 }],
6916
- children: /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(Text, { style: { textAlign: "center", color: theme.colors.onPrimary }, children: "Approve Merge" })
7250
+ children: /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(Text, { style: { textAlign: "center", color: theme.colors.onPrimary }, children: "Approve Merge" })
6917
7251
  }
6918
7252
  )
6919
7253
  }
6920
7254
  ),
6921
- /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(import_react_native54.View, { style: { height: 8 } }),
6922
- /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
6923
- import_react_native54.View,
7255
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(import_react_native55.View, { style: { height: 8 } }),
7256
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(
7257
+ import_react_native55.View,
6924
7258
  {
6925
7259
  style: [
6926
7260
  fullWidthButtonBase,
@@ -6931,22 +7265,22 @@ function ConfirmMergeRequestDialog({
6931
7265
  opacity: isBuilding || !mergeRequest ? 0.5 : 1
6932
7266
  }
6933
7267
  ],
6934
- children: /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
6935
- import_react_native54.Pressable,
7268
+ children: /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(
7269
+ import_react_native55.Pressable,
6936
7270
  {
6937
7271
  accessibilityRole: "button",
6938
7272
  accessibilityLabel: isBuilding ? "Preparing\u2026" : "Test edits first",
6939
7273
  disabled: isBuilding || !mergeRequest,
6940
7274
  onPress: handleTestFirst,
6941
7275
  style: [fullWidthButtonBase, { flex: 1 }],
6942
- children: /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(Text, { style: { textAlign: "center", color: theme.colors.text }, children: isBuilding ? "Preparing\u2026" : "Test edits first" })
7276
+ children: /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(Text, { style: { textAlign: "center", color: theme.colors.text }, children: isBuilding ? "Preparing\u2026" : "Test edits first" })
6943
7277
  }
6944
7278
  )
6945
7279
  }
6946
7280
  ),
6947
- /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(import_react_native54.View, { style: { height: 8 } }),
6948
- /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
6949
- import_react_native54.View,
7281
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(import_react_native55.View, { style: { height: 8 } }),
7282
+ /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(
7283
+ import_react_native55.View,
6950
7284
  {
6951
7285
  style: [
6952
7286
  fullWidthButtonBase,
@@ -6956,14 +7290,14 @@ function ConfirmMergeRequestDialog({
6956
7290
  borderColor: theme.colors.border
6957
7291
  }
6958
7292
  ],
6959
- children: /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(
6960
- import_react_native54.Pressable,
7293
+ children: /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(
7294
+ import_react_native55.Pressable,
6961
7295
  {
6962
7296
  accessibilityRole: "button",
6963
7297
  accessibilityLabel: "Cancel",
6964
7298
  onPress: close,
6965
7299
  style: [fullWidthButtonBase, { flex: 1 }],
6966
- children: /* @__PURE__ */ (0, import_jsx_runtime56.jsx)(Text, { style: { textAlign: "center", color: theme.colors.text }, children: "Cancel" })
7300
+ children: /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(Text, { style: { textAlign: "center", color: theme.colors.text }, children: "Cancel" })
6967
7301
  }
6968
7302
  )
6969
7303
  }
@@ -6975,7 +7309,7 @@ function ConfirmMergeRequestDialog({
6975
7309
  }
6976
7310
 
6977
7311
  // src/studio/ui/ConfirmMergeFlow.tsx
6978
- var import_jsx_runtime57 = require("react/jsx-runtime");
7312
+ var import_jsx_runtime58 = require("react/jsx-runtime");
6979
7313
  function ConfirmMergeFlow({
6980
7314
  visible,
6981
7315
  onOpenChange,
@@ -6986,7 +7320,7 @@ function ConfirmMergeFlow({
6986
7320
  onConfirm,
6987
7321
  onTestFirst
6988
7322
  }) {
6989
- return /* @__PURE__ */ (0, import_jsx_runtime57.jsx)(
7323
+ return /* @__PURE__ */ (0, import_jsx_runtime58.jsx)(
6990
7324
  ConfirmMergeRequestDialog,
6991
7325
  {
6992
7326
  visible,
@@ -7084,19 +7418,61 @@ function useOptimisticChatMessages({
7084
7418
  const createdAtIso = (/* @__PURE__ */ new Date()).toISOString();
7085
7419
  const baseServerLastId = chatMessages.length > 0 ? chatMessages[chatMessages.length - 1].id : null;
7086
7420
  const id = makeOptimisticId();
7087
- setOptimisticChat((prev) => [...prev, { id, content: text, createdAtIso, baseServerLastId, failed: false }]);
7421
+ const normalizedAttachments = attachments && attachments.length > 0 ? [...attachments] : void 0;
7422
+ setOptimisticChat((prev) => [
7423
+ ...prev,
7424
+ {
7425
+ id,
7426
+ content: text,
7427
+ attachments: normalizedAttachments,
7428
+ createdAtIso,
7429
+ baseServerLastId,
7430
+ failed: false,
7431
+ retrying: false
7432
+ }
7433
+ ]);
7088
7434
  void Promise.resolve(onSendChat(text, attachments)).catch(() => {
7089
7435
  setOptimisticChat((prev) => prev.map((m) => m.id === id ? { ...m, failed: true } : m));
7090
7436
  });
7091
7437
  },
7092
7438
  [chatMessages, disableOptimistic, onSendChat, shouldForkOnEdit]
7093
7439
  );
7094
- return { messages, onSend };
7440
+ const onRetry = React43.useCallback(
7441
+ async (messageId) => {
7442
+ if (shouldForkOnEdit || disableOptimistic) return;
7443
+ const target = optimisticChat.find((m) => m.id === messageId);
7444
+ if (!target || target.retrying) return;
7445
+ const baseServerLastId = chatMessages.length > 0 ? chatMessages[chatMessages.length - 1].id : null;
7446
+ setOptimisticChat(
7447
+ (prev) => prev.map(
7448
+ (m) => m.id === messageId ? { ...m, failed: false, retrying: true, baseServerLastId } : m
7449
+ )
7450
+ );
7451
+ try {
7452
+ await onSendChat(target.content, target.attachments);
7453
+ setOptimisticChat(
7454
+ (prev) => prev.map((m) => m.id === messageId ? { ...m, retrying: false } : m)
7455
+ );
7456
+ } catch {
7457
+ setOptimisticChat(
7458
+ (prev) => prev.map((m) => m.id === messageId ? { ...m, failed: true, retrying: false } : m)
7459
+ );
7460
+ }
7461
+ },
7462
+ [chatMessages, disableOptimistic, onSendChat, optimisticChat, shouldForkOnEdit]
7463
+ );
7464
+ const isRetrying = React43.useCallback(
7465
+ (messageId) => {
7466
+ return optimisticChat.some((m) => m.id === messageId && m.retrying);
7467
+ },
7468
+ [optimisticChat]
7469
+ );
7470
+ return { messages, onSend, onRetry, isRetrying };
7095
7471
  }
7096
7472
 
7097
7473
  // src/studio/ui/StudioOverlay.tsx
7098
7474
  var import_studio_control = require("@comergehq/studio-control");
7099
- var import_jsx_runtime58 = require("react/jsx-runtime");
7475
+ var import_jsx_runtime59 = require("react/jsx-runtime");
7100
7476
  function StudioOverlay({
7101
7477
  captureTargetRef,
7102
7478
  app,
@@ -7104,6 +7480,7 @@ function StudioOverlay({
7104
7480
  isOwner,
7105
7481
  shouldForkOnEdit,
7106
7482
  isTesting,
7483
+ isBaseBundleDownloading = false,
7107
7484
  onRestoreBase,
7108
7485
  incomingMergeRequests,
7109
7486
  outgoingMergeRequests,
@@ -7133,7 +7510,7 @@ function StudioOverlay({
7133
7510
  studioControlOptions
7134
7511
  }) {
7135
7512
  const theme = useTheme();
7136
- const { width } = (0, import_react_native55.useWindowDimensions)();
7513
+ const { width } = (0, import_react_native56.useWindowDimensions)();
7137
7514
  const [sheetOpen, setSheetOpen] = React44.useState(false);
7138
7515
  const sheetOpenRef = React44.useRef(sheetOpen);
7139
7516
  const [activePage, setActivePage] = React44.useState("preview");
@@ -7159,7 +7536,7 @@ function StudioOverlay({
7159
7536
  );
7160
7537
  const handleSheetOpenChange = React44.useCallback((open) => {
7161
7538
  setSheetOpen(open);
7162
- if (!open) import_react_native55.Keyboard.dismiss();
7539
+ if (!open) import_react_native56.Keyboard.dismiss();
7163
7540
  }, []);
7164
7541
  const closeSheet = React44.useCallback(() => {
7165
7542
  handleSheetOpenChange(false);
@@ -7170,8 +7547,8 @@ function StudioOverlay({
7170
7547
  openSheet();
7171
7548
  }, [openSheet]);
7172
7549
  const backToPreview = React44.useCallback(() => {
7173
- if (import_react_native55.Platform.OS !== "ios") {
7174
- import_react_native55.Keyboard.dismiss();
7550
+ if (import_react_native56.Platform.OS !== "ios") {
7551
+ import_react_native56.Keyboard.dismiss();
7175
7552
  setActivePage("preview");
7176
7553
  return;
7177
7554
  }
@@ -7183,9 +7560,9 @@ function StudioOverlay({
7183
7560
  clearTimeout(t);
7184
7561
  setActivePage("preview");
7185
7562
  };
7186
- const sub = import_react_native55.Keyboard.addListener("keyboardDidHide", finalize);
7563
+ const sub = import_react_native56.Keyboard.addListener("keyboardDidHide", finalize);
7187
7564
  const t = setTimeout(finalize, 350);
7188
- import_react_native55.Keyboard.dismiss();
7565
+ import_react_native56.Keyboard.dismiss();
7189
7566
  }, []);
7190
7567
  const startDraw = React44.useCallback(() => {
7191
7568
  setDrawing(true);
@@ -7234,14 +7611,14 @@ function StudioOverlay({
7234
7611
  React44.useEffect(() => {
7235
7612
  void (0, import_studio_control.publishComergeStudioUIState)(sheetOpen, studioControlOptions);
7236
7613
  }, [sheetOpen, studioControlOptions]);
7237
- return /* @__PURE__ */ (0, import_jsx_runtime58.jsxs)(import_jsx_runtime58.Fragment, { children: [
7238
- /* @__PURE__ */ (0, import_jsx_runtime58.jsx)(EdgeGlowFrame, { visible: isTesting, role: "accent", thickness: 40, intensity: 1 }),
7239
- /* @__PURE__ */ (0, import_jsx_runtime58.jsx)(StudioBottomSheet, { open: sheetOpen, onOpenChange: handleSheetOpenChange, children: /* @__PURE__ */ (0, import_jsx_runtime58.jsx)(
7614
+ return /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(import_jsx_runtime59.Fragment, { children: [
7615
+ /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(EdgeGlowFrame, { visible: isTesting, role: "accent", thickness: 40, intensity: 1 }),
7616
+ /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(StudioBottomSheet, { open: sheetOpen, onOpenChange: handleSheetOpenChange, children: /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(
7240
7617
  StudioSheetPager,
7241
7618
  {
7242
7619
  activePage,
7243
7620
  width,
7244
- preview: /* @__PURE__ */ (0, import_jsx_runtime58.jsx)(
7621
+ preview: /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(
7245
7622
  PreviewPanel,
7246
7623
  {
7247
7624
  app,
@@ -7270,7 +7647,7 @@ function StudioOverlay({
7270
7647
  commentCountOverride: commentsCount ?? void 0
7271
7648
  }
7272
7649
  ),
7273
- chat: /* @__PURE__ */ (0, import_jsx_runtime58.jsx)(
7650
+ chat: /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(
7274
7651
  ChatPanel,
7275
7652
  {
7276
7653
  messages: optimistic.messages,
@@ -7279,7 +7656,6 @@ function StudioOverlay({
7279
7656
  sendDisabled: chatSendDisabled,
7280
7657
  forking: chatForking,
7281
7658
  sending: chatSending,
7282
- autoFocusComposer: sheetOpen && activePage === "chat",
7283
7659
  shouldForkOnEdit,
7284
7660
  attachments: chatAttachments,
7285
7661
  onRemoveAttachment: (idx) => setChatAttachments((prev) => prev.filter((_, i) => i !== idx)),
@@ -7289,24 +7665,27 @@ function StudioOverlay({
7289
7665
  onNavigateHome,
7290
7666
  onStartDraw: startDraw,
7291
7667
  onSend: optimistic.onSend,
7668
+ onRetryMessage: optimistic.onRetry,
7669
+ isRetryingMessage: optimistic.isRetrying,
7292
7670
  queueItems: queueItemsForChat,
7293
7671
  onRemoveQueueItem
7294
7672
  }
7295
7673
  )
7296
7674
  }
7297
7675
  ) }),
7298
- showBubble && /* @__PURE__ */ (0, import_jsx_runtime58.jsx)(
7676
+ showBubble && /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(
7299
7677
  Bubble,
7300
7678
  {
7301
7679
  visible: !sheetOpen && !drawing,
7302
7680
  ariaLabel: sheetOpen ? "Hide studio" : "Show studio",
7303
7681
  badgeCount: incomingMergeRequests.length,
7304
7682
  onPress: toggleSheet,
7305
- isLoading: (app == null ? void 0 : app.status) === "editing",
7306
- children: /* @__PURE__ */ (0, import_jsx_runtime58.jsx)(import_react_native55.View, { style: { width: 28, height: 28, alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ (0, import_jsx_runtime58.jsx)(MergeIcon, { width: 24, height: 24, color: theme.colors.floatingContent }) })
7683
+ isLoading: (app == null ? void 0 : app.status) === "editing" || isBaseBundleDownloading,
7684
+ loadingBorderTone: isBaseBundleDownloading ? "warning" : "default",
7685
+ children: /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(import_react_native56.View, { style: { width: 28, height: 28, alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(MergeIcon, { width: 24, height: 24, color: theme.colors.floatingContent }) })
7307
7686
  }
7308
7687
  ),
7309
- /* @__PURE__ */ (0, import_jsx_runtime58.jsx)(
7688
+ /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(
7310
7689
  DrawModeOverlay,
7311
7690
  {
7312
7691
  visible: drawing,
@@ -7315,7 +7694,7 @@ function StudioOverlay({
7315
7694
  onCapture: handleDrawCapture
7316
7695
  }
7317
7696
  ),
7318
- /* @__PURE__ */ (0, import_jsx_runtime58.jsx)(
7697
+ /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(
7319
7698
  ConfirmMergeFlow,
7320
7699
  {
7321
7700
  visible: Boolean(confirmMr),
@@ -7324,11 +7703,12 @@ function StudioOverlay({
7324
7703
  },
7325
7704
  mergeRequest: confirmMr,
7326
7705
  toSummary: toMergeRequestSummary,
7706
+ isBuilding: isBuildingMrTest,
7327
7707
  onConfirm: (mr) => onApprove == null ? void 0 : onApprove(mr),
7328
7708
  onTestFirst: handleTestMr
7329
7709
  }
7330
7710
  ),
7331
- /* @__PURE__ */ (0, import_jsx_runtime58.jsx)(
7711
+ /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(
7332
7712
  AppCommentsSheet,
7333
7713
  {
7334
7714
  appId: commentsAppId,
@@ -7521,7 +7901,7 @@ function useEditQueueActions(appId) {
7521
7901
  }
7522
7902
 
7523
7903
  // src/studio/ComergeStudio.tsx
7524
- var import_jsx_runtime59 = require("react/jsx-runtime");
7904
+ var import_jsx_runtime60 = require("react/jsx-runtime");
7525
7905
  function ComergeStudio({
7526
7906
  appId,
7527
7907
  clientKey: clientKey2,
@@ -7535,14 +7915,14 @@ function ComergeStudio({
7535
7915
  const [activeAppId, setActiveAppId] = React47.useState(appId);
7536
7916
  const [runtimeAppId, setRuntimeAppId] = React47.useState(appId);
7537
7917
  const [pendingRuntimeTargetAppId, setPendingRuntimeTargetAppId] = React47.useState(null);
7538
- const platform = React47.useMemo(() => import_react_native56.Platform.OS === "ios" ? "ios" : "android", []);
7918
+ const platform = React47.useMemo(() => import_react_native57.Platform.OS === "ios" ? "ios" : "android", []);
7539
7919
  React47.useEffect(() => {
7540
7920
  setActiveAppId(appId);
7541
7921
  setRuntimeAppId(appId);
7542
7922
  setPendingRuntimeTargetAppId(null);
7543
7923
  }, [appId]);
7544
7924
  const captureTargetRef = React47.useRef(null);
7545
- return /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(StudioBootstrap, { clientKey: clientKey2, fallback: /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(import_react_native56.View, { style: { flex: 1 } }), children: ({ userId }) => /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(import_bottom_sheet6.BottomSheetModalProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(LiquidGlassResetProvider, { resetTriggers: [appId, activeAppId, runtimeAppId], children: /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(
7925
+ return /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(StudioBootstrap, { clientKey: clientKey2, fallback: /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(import_react_native57.View, { style: { flex: 1 } }), children: ({ userId }) => /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(import_bottom_sheet6.BottomSheetModalProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(LiquidGlassResetProvider, { resetTriggers: [appId, activeAppId, runtimeAppId], children: /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(
7546
7926
  ComergeStudioInner,
7547
7927
  {
7548
7928
  userId,
@@ -7691,6 +8071,8 @@ function ComergeStudioInner({
7691
8071
  const [testingMrId, setTestingMrId] = React47.useState(null);
7692
8072
  const [syncingUpstream, setSyncingUpstream] = React47.useState(false);
7693
8073
  const [upstreamSyncStatus, setUpstreamSyncStatus] = React47.useState(null);
8074
+ const isMrTestBuildInProgress = bundle.loading && bundle.loadingMode === "test";
8075
+ const isBaseBundleDownloading = bundle.loading && bundle.loadingMode === "base" && !bundle.isTesting;
7694
8076
  const chatShowTypingIndicator = React47.useMemo(() => {
7695
8077
  var _a;
7696
8078
  if (!thread.raw || thread.raw.length === 0) return false;
@@ -7737,8 +8119,8 @@ function ComergeStudioInner({
7737
8119
  }
7738
8120
  return editQueue.items;
7739
8121
  }, [editQueue.items, lastEditQueueInfo, suppressQueueUntilResponse]);
7740
- return /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(import_react_native56.View, { style: [{ flex: 1 }, style], children: /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(import_react_native56.View, { ref: captureTargetRef, style: { flex: 1 }, collapsable: false, children: [
7741
- /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(
8122
+ return /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(import_react_native57.View, { style: [{ flex: 1 }, style], children: /* @__PURE__ */ (0, import_jsx_runtime60.jsxs)(import_react_native57.View, { ref: captureTargetRef, style: { flex: 1 }, collapsable: false, children: [
8123
+ /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(
7742
8124
  RuntimeRenderer,
7743
8125
  {
7744
8126
  appKey,
@@ -7748,7 +8130,7 @@ function ComergeStudioInner({
7748
8130
  allowInitialPreparing: !embeddedBaseBundles
7749
8131
  }
7750
8132
  ),
7751
- /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(
8133
+ /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(
7752
8134
  StudioOverlay,
7753
8135
  {
7754
8136
  captureTargetRef,
@@ -7757,6 +8139,7 @@ function ComergeStudioInner({
7757
8139
  isOwner: actions.isOwner,
7758
8140
  shouldForkOnEdit: actions.shouldForkOnEdit,
7759
8141
  isTesting: bundle.isTesting,
8142
+ isBaseBundleDownloading,
7760
8143
  onRestoreBase: async () => {
7761
8144
  setTestingMrId(null);
7762
8145
  await bundle.restoreBase();
@@ -7765,10 +8148,10 @@ function ComergeStudioInner({
7765
8148
  outgoingMergeRequests: mergeRequests.lists.outgoing,
7766
8149
  creatorStatsById: mergeRequests.creatorStatsById,
7767
8150
  processingMrId,
7768
- isBuildingMrTest: bundle.loading,
8151
+ isBuildingMrTest: isMrTestBuildInProgress,
7769
8152
  testingMrId,
7770
8153
  toMergeRequestSummary: mergeRequests.toSummary,
7771
- onSubmitMergeRequest: (app == null ? void 0 : app.forkedFromAppId) && actions.isOwner && !hasOpenOutgoingMr ? async () => {
8154
+ onSubmitMergeRequest: (app == null ? void 0 : app.forkedFromAppId) && actions.isOwner && !mergeRequests.loading && !hasOpenOutgoingMr ? async () => {
7772
8155
  await mergeRequests.actions.openMergeRequest(activeAppId);
7773
8156
  } : void 0,
7774
8157
  onSyncUpstream: actions.isOwner && (app == null ? void 0 : app.forkedFromAppId) ? handleSyncUpstream : void 0,
@@ -7793,6 +8176,7 @@ function ComergeStudioInner({
7793
8176
  }
7794
8177
  },
7795
8178
  onTestMr: async (mr) => {
8179
+ if (testingMrId === mr.id || bundle.loadingMode === "test") return;
7796
8180
  setTestingMrId(mr.id);
7797
8181
  await bundle.loadTest({ appId: mr.sourceAppId, commitId: mr.sourceTipCommitId ?? mr.sourceCommitId });
7798
8182
  },
@@ -7815,6 +8199,7 @@ function ComergeStudioInner({
7815
8199
  // Annotate the CommonJS export names for ESM import in node:
7816
8200
  0 && (module.exports = {
7817
8201
  ComergeStudio,
8202
+ resetRealtimeState,
7818
8203
  setSupabaseClient
7819
8204
  });
7820
8205
  //# sourceMappingURL=index.js.map