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