@comergehq/studio 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +608 -386
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +328 -106
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/ChatMessageList.tsx +21 -35
- package/src/components/chat/ChatPage.tsx +3 -1
- package/src/components/studio-sheet/StudioBottomSheet.tsx +3 -3
- package/src/studio/ComergeStudio.tsx +34 -1
- package/src/studio/hooks/useAttachmentUpload.ts +51 -5
- package/src/studio/hooks/useBundleManager.ts +91 -17
- package/src/studio/hooks/useOptimisticChatMessages.ts +128 -0
- package/src/studio/ui/ChatPanel.tsx +1 -0
- package/src/studio/ui/RuntimeRenderer.tsx +7 -2
- package/src/studio/ui/StudioOverlay.tsx +11 -2
package/dist/index.mjs
CHANGED
|
@@ -6,7 +6,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
6
6
|
});
|
|
7
7
|
|
|
8
8
|
// src/studio/ComergeStudio.tsx
|
|
9
|
-
import * as
|
|
9
|
+
import * as React40 from "react";
|
|
10
10
|
import { Platform as RNPlatform, View as View45 } from "react-native";
|
|
11
11
|
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
|
|
12
12
|
|
|
@@ -952,6 +952,39 @@ var BundlesRepositoryImpl = class extends BaseRepository {
|
|
|
952
952
|
var bundlesRepository = new BundlesRepositoryImpl(bundlesRemoteDataSource);
|
|
953
953
|
|
|
954
954
|
// src/studio/hooks/useBundleManager.ts
|
|
955
|
+
function sleep(ms) {
|
|
956
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
957
|
+
}
|
|
958
|
+
function isRetryableNetworkError(e) {
|
|
959
|
+
var _a;
|
|
960
|
+
const err = e;
|
|
961
|
+
const code = typeof (err == null ? void 0 : err.code) === "string" ? err.code : "";
|
|
962
|
+
const message = typeof (err == null ? void 0 : err.message) === "string" ? err.message : "";
|
|
963
|
+
if (code === "ERR_NETWORK" || code === "ECONNABORTED") return true;
|
|
964
|
+
if (message.toLowerCase().includes("network error")) return true;
|
|
965
|
+
if (message.toLowerCase().includes("timeout")) return true;
|
|
966
|
+
const status = typeof ((_a = err == null ? void 0 : err.response) == null ? void 0 : _a.status) === "number" ? err.response.status : void 0;
|
|
967
|
+
if (status && (status === 429 || status >= 500)) return true;
|
|
968
|
+
return false;
|
|
969
|
+
}
|
|
970
|
+
async function withRetry(fn, opts) {
|
|
971
|
+
let lastErr = null;
|
|
972
|
+
for (let attempt = 1; attempt <= opts.attempts; attempt += 1) {
|
|
973
|
+
try {
|
|
974
|
+
return await fn();
|
|
975
|
+
} catch (e) {
|
|
976
|
+
lastErr = e;
|
|
977
|
+
const retryable = isRetryableNetworkError(e);
|
|
978
|
+
if (!retryable || attempt >= opts.attempts) {
|
|
979
|
+
throw e;
|
|
980
|
+
}
|
|
981
|
+
const exp = Math.min(opts.maxDelayMs, opts.baseDelayMs * Math.pow(2, attempt - 1));
|
|
982
|
+
const jitter = Math.floor(Math.random() * 250);
|
|
983
|
+
await sleep(exp + jitter);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
throw lastErr;
|
|
987
|
+
}
|
|
955
988
|
function safeName(s) {
|
|
956
989
|
return s.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
957
990
|
}
|
|
@@ -1009,8 +1042,16 @@ async function getExistingNonEmptyFileUri(fileUri) {
|
|
|
1009
1042
|
async function downloadIfMissing(url, fileUri) {
|
|
1010
1043
|
const existing = await getExistingNonEmptyFileUri(fileUri);
|
|
1011
1044
|
if (existing) return existing;
|
|
1012
|
-
|
|
1013
|
-
|
|
1045
|
+
return await withRetry(
|
|
1046
|
+
async () => {
|
|
1047
|
+
await deleteFileIfExists(fileUri);
|
|
1048
|
+
const res = await FileSystem.downloadAsync(url, fileUri);
|
|
1049
|
+
const ok = await getExistingNonEmptyFileUri(res.uri);
|
|
1050
|
+
if (!ok) throw new Error("Downloaded bundle is empty.");
|
|
1051
|
+
return res.uri;
|
|
1052
|
+
},
|
|
1053
|
+
{ attempts: 3, baseDelayMs: 500, maxDelayMs: 4e3 }
|
|
1054
|
+
);
|
|
1014
1055
|
}
|
|
1015
1056
|
async function deleteFileIfExists(fileUri) {
|
|
1016
1057
|
try {
|
|
@@ -1024,11 +1065,15 @@ async function deleteFileIfExists(fileUri) {
|
|
|
1024
1065
|
async function safeReplaceFileFromUrl(url, targetUri, tmpKey) {
|
|
1025
1066
|
const tmpUri = toBundleFileUri(`tmp:${tmpKey}:${Date.now()}`);
|
|
1026
1067
|
try {
|
|
1027
|
-
await
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1068
|
+
await withRetry(
|
|
1069
|
+
async () => {
|
|
1070
|
+
await deleteFileIfExists(tmpUri);
|
|
1071
|
+
await FileSystem.downloadAsync(url, tmpUri);
|
|
1072
|
+
const tmpOk = await getExistingNonEmptyFileUri(tmpUri);
|
|
1073
|
+
if (!tmpOk) throw new Error("Downloaded bundle is empty.");
|
|
1074
|
+
},
|
|
1075
|
+
{ attempts: 3, baseDelayMs: 500, maxDelayMs: 4e3 }
|
|
1076
|
+
);
|
|
1032
1077
|
await deleteFileIfExists(targetUri);
|
|
1033
1078
|
await FileSystem.moveAsync({ from: tmpUri, to: targetUri });
|
|
1034
1079
|
const finalOk = await getExistingNonEmptyFileUri(targetUri);
|
|
@@ -1041,28 +1086,44 @@ async function safeReplaceFileFromUrl(url, targetUri, tmpKey) {
|
|
|
1041
1086
|
async function pollBundle(appId, bundleId, opts) {
|
|
1042
1087
|
const start = Date.now();
|
|
1043
1088
|
while (true) {
|
|
1044
|
-
|
|
1045
|
-
|
|
1089
|
+
try {
|
|
1090
|
+
const bundle = await bundlesRepository.getById(appId, bundleId);
|
|
1091
|
+
if (bundle.status === "succeeded" || bundle.status === "failed") return bundle;
|
|
1092
|
+
} catch (e) {
|
|
1093
|
+
if (!isRetryableNetworkError(e)) {
|
|
1094
|
+
throw e;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1046
1097
|
if (Date.now() - start > opts.timeoutMs) {
|
|
1047
1098
|
throw new Error("Bundle build timed out.");
|
|
1048
1099
|
}
|
|
1049
|
-
await
|
|
1100
|
+
await sleep(opts.intervalMs);
|
|
1050
1101
|
}
|
|
1051
1102
|
}
|
|
1052
1103
|
async function resolveBundlePath(src, platform, mode) {
|
|
1053
1104
|
const { appId, commitId } = src;
|
|
1054
1105
|
const dir = bundlesCacheDir();
|
|
1055
1106
|
await ensureDir(dir);
|
|
1056
|
-
const initiate = await
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1107
|
+
const initiate = await withRetry(
|
|
1108
|
+
async () => {
|
|
1109
|
+
return await bundlesRepository.initiate(appId, {
|
|
1110
|
+
platform,
|
|
1111
|
+
commitId: commitId ?? void 0,
|
|
1112
|
+
idempotencyKey: `${appId}:${commitId ?? "head"}:${platform}`
|
|
1113
|
+
});
|
|
1114
|
+
},
|
|
1115
|
+
{ attempts: 3, baseDelayMs: 500, maxDelayMs: 4e3 }
|
|
1116
|
+
);
|
|
1061
1117
|
const finalBundle = initiate.status === "succeeded" || initiate.status === "failed" ? initiate : await pollBundle(appId, initiate.id, { timeoutMs: 3 * 60 * 1e3, intervalMs: 1200 });
|
|
1062
1118
|
if (finalBundle.status === "failed") {
|
|
1063
1119
|
throw new Error("Bundle build failed.");
|
|
1064
1120
|
}
|
|
1065
|
-
const signed = await
|
|
1121
|
+
const signed = await withRetry(
|
|
1122
|
+
async () => {
|
|
1123
|
+
return await bundlesRepository.getSignedDownloadUrl(appId, finalBundle.id, { redirect: false });
|
|
1124
|
+
},
|
|
1125
|
+
{ attempts: 3, baseDelayMs: 500, maxDelayMs: 4e3 }
|
|
1126
|
+
);
|
|
1066
1127
|
const bundlePath = mode === "base" ? await safeReplaceFileFromUrl(
|
|
1067
1128
|
signed.url,
|
|
1068
1129
|
toBundleFileUri(baseBundleKey(appId, platform)),
|
|
@@ -1078,6 +1139,7 @@ function useBundleManager({
|
|
|
1078
1139
|
const [bundlePath, setBundlePath] = React5.useState(null);
|
|
1079
1140
|
const [renderToken, setRenderToken] = React5.useState(0);
|
|
1080
1141
|
const [loading, setLoading] = React5.useState(false);
|
|
1142
|
+
const [loadingMode, setLoadingMode] = React5.useState(null);
|
|
1081
1143
|
const [statusLabel, setStatusLabel] = React5.useState(null);
|
|
1082
1144
|
const [error, setError] = React5.useState(null);
|
|
1083
1145
|
const [isTesting, setIsTesting] = React5.useState(false);
|
|
@@ -1093,6 +1155,7 @@ function useBundleManager({
|
|
|
1093
1155
|
baseOpIdRef.current += 1;
|
|
1094
1156
|
if (activeLoadModeRef.current === "base") {
|
|
1095
1157
|
setLoading(false);
|
|
1158
|
+
setLoadingMode(null);
|
|
1096
1159
|
setStatusLabel(null);
|
|
1097
1160
|
activeLoadModeRef.current = null;
|
|
1098
1161
|
}
|
|
@@ -1157,6 +1220,7 @@ function useBundleManager({
|
|
|
1157
1220
|
const opId = mode === "base" ? ++baseOpIdRef.current : ++testOpIdRef.current;
|
|
1158
1221
|
activeLoadModeRef.current = mode;
|
|
1159
1222
|
setLoading(true);
|
|
1223
|
+
setLoadingMode(mode);
|
|
1160
1224
|
setError(null);
|
|
1161
1225
|
setStatusLabel(mode === "test" ? "Loading test bundle\u2026" : "Loading latest build\u2026");
|
|
1162
1226
|
if (mode === "base") {
|
|
@@ -1199,6 +1263,7 @@ function useBundleManager({
|
|
|
1199
1263
|
if (mode === "base" && opId !== baseOpIdRef.current) return;
|
|
1200
1264
|
if (mode === "test" && opId !== testOpIdRef.current) return;
|
|
1201
1265
|
setLoading(false);
|
|
1266
|
+
setLoadingMode(null);
|
|
1202
1267
|
if (activeLoadModeRef.current === mode) activeLoadModeRef.current = null;
|
|
1203
1268
|
}
|
|
1204
1269
|
}, [activateCachedBase, platform]);
|
|
@@ -1220,7 +1285,7 @@ function useBundleManager({
|
|
|
1220
1285
|
if (!canRequestLatest) return;
|
|
1221
1286
|
void loadBase();
|
|
1222
1287
|
}, [base.appId, base.commitId, platform, canRequestLatest, loadBase]);
|
|
1223
|
-
return { bundlePath, renderToken, loading, statusLabel, error, isTesting, loadBase, loadTest, restoreBase };
|
|
1288
|
+
return { bundlePath, renderToken, loading, loadingMode, statusLabel, error, isTesting, loadBase, loadTest, restoreBase };
|
|
1224
1289
|
}
|
|
1225
1290
|
|
|
1226
1291
|
// src/studio/hooks/useMergeRequests.ts
|
|
@@ -1479,6 +1544,8 @@ function useMergeRequests(params) {
|
|
|
1479
1544
|
|
|
1480
1545
|
// src/studio/hooks/useAttachmentUpload.ts
|
|
1481
1546
|
import * as React7 from "react";
|
|
1547
|
+
import { Platform } from "react-native";
|
|
1548
|
+
import * as FileSystem2 from "expo-file-system/legacy";
|
|
1482
1549
|
|
|
1483
1550
|
// src/data/attachment/remote.ts
|
|
1484
1551
|
var AttachmentRemoteDataSourceImpl = class extends BaseRemote {
|
|
@@ -1518,6 +1585,40 @@ var attachmentRepository = new AttachmentRepositoryImpl(
|
|
|
1518
1585
|
);
|
|
1519
1586
|
|
|
1520
1587
|
// src/studio/hooks/useAttachmentUpload.ts
|
|
1588
|
+
async function dataUrlToBlobAndroid(dataUrl) {
|
|
1589
|
+
const normalized = dataUrl.startsWith("data:") ? dataUrl : `data:image/png;base64,${dataUrl}`;
|
|
1590
|
+
const comma = normalized.indexOf(",");
|
|
1591
|
+
if (comma === -1) {
|
|
1592
|
+
throw new Error("Invalid data URL (missing comma separator)");
|
|
1593
|
+
}
|
|
1594
|
+
const header = normalized.slice(0, comma);
|
|
1595
|
+
const base64 = normalized.slice(comma + 1);
|
|
1596
|
+
const mimeMatch = header.match(/data:(.*?);base64/i);
|
|
1597
|
+
const mimeType = (mimeMatch == null ? void 0 : mimeMatch[1]) ?? "application/octet-stream";
|
|
1598
|
+
const cacheDir = FileSystem2.cacheDirectory;
|
|
1599
|
+
if (!cacheDir) {
|
|
1600
|
+
throw new Error("expo-file-system cacheDirectory is unavailable");
|
|
1601
|
+
}
|
|
1602
|
+
const fileUri = `${cacheDir}attachment-${Date.now()}-${Math.random().toString(16).slice(2)}.bin`;
|
|
1603
|
+
await FileSystem2.writeAsStringAsync(fileUri, base64, {
|
|
1604
|
+
encoding: FileSystem2.EncodingType.Base64
|
|
1605
|
+
});
|
|
1606
|
+
try {
|
|
1607
|
+
const resp = await fetch(fileUri);
|
|
1608
|
+
const blob = await resp.blob();
|
|
1609
|
+
return blob.type ? blob : new Blob([blob], { type: mimeType });
|
|
1610
|
+
} finally {
|
|
1611
|
+
void FileSystem2.deleteAsync(fileUri, { idempotent: true }).catch(() => {
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
function getMimeTypeFromDataUrl(dataUrl) {
|
|
1616
|
+
const normalized = dataUrl.startsWith("data:") ? dataUrl : `data:image/png;base64,${dataUrl}`;
|
|
1617
|
+
const comma = normalized.indexOf(",");
|
|
1618
|
+
const header = comma === -1 ? normalized : normalized.slice(0, comma);
|
|
1619
|
+
const mimeMatch = header.match(/data:(.*?);base64/i);
|
|
1620
|
+
return (mimeMatch == null ? void 0 : mimeMatch[1]) ?? "image/png";
|
|
1621
|
+
}
|
|
1521
1622
|
function useAttachmentUpload() {
|
|
1522
1623
|
const [uploading, setUploading] = React7.useState(false);
|
|
1523
1624
|
const [error, setError] = React7.useState(null);
|
|
@@ -1530,15 +1631,15 @@ function useAttachmentUpload() {
|
|
|
1530
1631
|
const blobs = await Promise.all(
|
|
1531
1632
|
dataUrls.map(async (dataUrl, idx) => {
|
|
1532
1633
|
const normalized = dataUrl.startsWith("data:") ? dataUrl : `data:image/png;base64,${dataUrl}`;
|
|
1533
|
-
const
|
|
1534
|
-
const
|
|
1535
|
-
return { blob, idx };
|
|
1634
|
+
const blob = Platform.OS === "android" ? await dataUrlToBlobAndroid(normalized) : await (await fetch(normalized)).blob();
|
|
1635
|
+
const mimeType = getMimeTypeFromDataUrl(normalized);
|
|
1636
|
+
return { blob, idx, mimeType };
|
|
1536
1637
|
})
|
|
1537
1638
|
);
|
|
1538
|
-
const files = blobs.map(({ blob }, idx) => ({
|
|
1639
|
+
const files = blobs.map(({ blob, mimeType }, idx) => ({
|
|
1539
1640
|
name: `attachment-${Date.now()}-${idx}.png`,
|
|
1540
1641
|
size: blob.size,
|
|
1541
|
-
mimeType
|
|
1642
|
+
mimeType
|
|
1542
1643
|
}));
|
|
1543
1644
|
const presign = await attachmentRepository.presign({ threadId, appId, files });
|
|
1544
1645
|
await Promise.all(presign.uploads.map((u, index) => attachmentRepository.upload(u, blobs[index].blob)));
|
|
@@ -1665,8 +1766,8 @@ function hasNoOutcomeAfterLastHuman(messages) {
|
|
|
1665
1766
|
import { View as View2 } from "react-native";
|
|
1666
1767
|
import { ComergeRuntimeRenderer } from "@comergehq/runtime";
|
|
1667
1768
|
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
1668
|
-
function RuntimeRenderer({ appKey, bundlePath, renderToken, style }) {
|
|
1669
|
-
if (!bundlePath) {
|
|
1769
|
+
function RuntimeRenderer({ appKey, bundlePath, forcePreparing, renderToken, style }) {
|
|
1770
|
+
if (!bundlePath || forcePreparing) {
|
|
1670
1771
|
return /* @__PURE__ */ jsx3(View2, { style: [{ flex: 1, justifyContent: "center", alignItems: "center", padding: 24 }, style], children: /* @__PURE__ */ jsx3(Text, { variant: "bodyMuted", children: "Preparing app\u2026" }) });
|
|
1671
1772
|
}
|
|
1672
1773
|
return /* @__PURE__ */ jsx3(View2, { style: [{ flex: 1 }, style], children: /* @__PURE__ */ jsx3(
|
|
@@ -1681,8 +1782,8 @@ function RuntimeRenderer({ appKey, bundlePath, renderToken, style }) {
|
|
|
1681
1782
|
}
|
|
1682
1783
|
|
|
1683
1784
|
// src/studio/ui/StudioOverlay.tsx
|
|
1684
|
-
import * as
|
|
1685
|
-
import { Keyboard as Keyboard5, Platform as
|
|
1785
|
+
import * as React39 from "react";
|
|
1786
|
+
import { Keyboard as Keyboard5, Platform as Platform9, View as View44, useWindowDimensions as useWindowDimensions4 } from "react-native";
|
|
1686
1787
|
|
|
1687
1788
|
// src/components/studio-sheet/StudioBottomSheet.tsx
|
|
1688
1789
|
import * as React9 from "react";
|
|
@@ -1691,7 +1792,7 @@ import BottomSheet from "@gorhom/bottom-sheet";
|
|
|
1691
1792
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
1692
1793
|
|
|
1693
1794
|
// src/components/studio-sheet/StudioSheetBackground.tsx
|
|
1694
|
-
import { Platform, View as View3 } from "react-native";
|
|
1795
|
+
import { Platform as Platform2, View as View3 } from "react-native";
|
|
1695
1796
|
import { LiquidGlassView, isLiquidGlassSupported } from "@callstack/liquid-glass";
|
|
1696
1797
|
import { Fragment as Fragment2, jsx as jsx4, jsxs } from "react/jsx-runtime";
|
|
1697
1798
|
function StudioSheetBackground({
|
|
@@ -1699,7 +1800,7 @@ function StudioSheetBackground({
|
|
|
1699
1800
|
renderBackground
|
|
1700
1801
|
}) {
|
|
1701
1802
|
const theme = useTheme();
|
|
1702
|
-
const radius =
|
|
1803
|
+
const radius = Platform2.OS === "ios" ? 39 : 16;
|
|
1703
1804
|
const fallbackBgColor = theme.scheme === "dark" ? "rgba(11, 8, 15, 0.85)" : "rgba(255, 255, 255, 0.85)";
|
|
1704
1805
|
const secondaryBgBaseColor = theme.scheme === "dark" ? "rgb(24, 24, 27)" : "rgb(173, 173, 173)";
|
|
1705
1806
|
const containerStyle = {
|
|
@@ -1745,7 +1846,7 @@ import { jsx as jsx5 } from "react/jsx-runtime";
|
|
|
1745
1846
|
function StudioBottomSheet({
|
|
1746
1847
|
open,
|
|
1747
1848
|
onOpenChange,
|
|
1748
|
-
snapPoints = ["
|
|
1849
|
+
snapPoints = ["100%"],
|
|
1749
1850
|
sheetRef,
|
|
1750
1851
|
background,
|
|
1751
1852
|
children,
|
|
@@ -1799,9 +1900,9 @@ function StudioBottomSheet({
|
|
|
1799
1900
|
ref: resolvedSheetRef,
|
|
1800
1901
|
index: open ? snapPoints.length - 1 : -1,
|
|
1801
1902
|
snapPoints,
|
|
1903
|
+
enableDynamicSizing: false,
|
|
1802
1904
|
enablePanDownToClose: true,
|
|
1803
|
-
|
|
1804
|
-
keyboardBlurBehavior: "restore",
|
|
1905
|
+
enableContentPanningGesture: false,
|
|
1805
1906
|
android_keyboardInputMode: "adjustResize",
|
|
1806
1907
|
backgroundComponent: (props) => /* @__PURE__ */ jsx5(StudioSheetBackground, { ...props, renderBackground: background == null ? void 0 : background.renderBackground }),
|
|
1807
1908
|
topInset: insets.top,
|
|
@@ -2779,7 +2880,7 @@ var styles3 = StyleSheet3.create({
|
|
|
2779
2880
|
|
|
2780
2881
|
// src/components/comments/AppCommentsSheet.tsx
|
|
2781
2882
|
import * as React21 from "react";
|
|
2782
|
-
import { ActivityIndicator as ActivityIndicator3, Keyboard as Keyboard3, Platform as
|
|
2883
|
+
import { ActivityIndicator as ActivityIndicator3, Keyboard as Keyboard3, Platform as Platform5, Pressable as Pressable5, View as View14 } from "react-native";
|
|
2783
2884
|
import {
|
|
2784
2885
|
BottomSheetBackdrop,
|
|
2785
2886
|
BottomSheetModal,
|
|
@@ -3384,11 +3485,11 @@ function useAppDetails(appId) {
|
|
|
3384
3485
|
|
|
3385
3486
|
// src/components/comments/useIosKeyboardSnapFix.ts
|
|
3386
3487
|
import * as React20 from "react";
|
|
3387
|
-
import { Keyboard as Keyboard2, Platform as
|
|
3488
|
+
import { Keyboard as Keyboard2, Platform as Platform4 } from "react-native";
|
|
3388
3489
|
function useIosKeyboardSnapFix(sheetRef, options) {
|
|
3389
3490
|
const [keyboardVisible, setKeyboardVisible] = React20.useState(false);
|
|
3390
3491
|
React20.useEffect(() => {
|
|
3391
|
-
if (
|
|
3492
|
+
if (Platform4.OS !== "ios") return;
|
|
3392
3493
|
const show = Keyboard2.addListener("keyboardWillShow", () => setKeyboardVisible(true));
|
|
3393
3494
|
const hide = Keyboard2.addListener("keyboardWillHide", () => {
|
|
3394
3495
|
var _a;
|
|
@@ -3466,8 +3567,8 @@ function AppCommentsSheet({ appId, onClose, onCountChange, onPlayApp }) {
|
|
|
3466
3567
|
onChange: handleChange,
|
|
3467
3568
|
backgroundStyle: {
|
|
3468
3569
|
backgroundColor: theme.scheme === "dark" ? "#0B080F" : "#FFFFFF",
|
|
3469
|
-
borderTopLeftRadius:
|
|
3470
|
-
borderTopRightRadius:
|
|
3570
|
+
borderTopLeftRadius: Platform5.OS === "ios" ? 39 : 16,
|
|
3571
|
+
borderTopRightRadius: Platform5.OS === "ios" ? 39 : 16
|
|
3471
3572
|
},
|
|
3472
3573
|
handleIndicatorStyle: { backgroundColor: theme.colors.handleIndicator },
|
|
3473
3574
|
keyboardBehavior: "interactive",
|
|
@@ -3574,7 +3675,7 @@ function AppCommentsSheet({ appId, onClose, onCountChange, onPlayApp }) {
|
|
|
3574
3675
|
bottom: 0,
|
|
3575
3676
|
paddingHorizontal: theme.spacing.lg,
|
|
3576
3677
|
paddingTop: theme.spacing.sm,
|
|
3577
|
-
paddingBottom:
|
|
3678
|
+
paddingBottom: Platform5.OS === "ios" ? keyboardVisible ? theme.spacing.lg : insets.bottom : insets.bottom + 10,
|
|
3578
3679
|
borderTopWidth: 1,
|
|
3579
3680
|
borderTopColor: withAlpha(theme.colors.border, 0.1),
|
|
3580
3681
|
backgroundColor: withAlpha(theme.colors.background, 0.8)
|
|
@@ -4473,7 +4574,7 @@ import { Animated as Animated7, Pressable as Pressable9, View as View28 } from "
|
|
|
4473
4574
|
import { Ban, Check as Check3, CheckCheck, ChevronDown as ChevronDown2 } from "lucide-react-native";
|
|
4474
4575
|
|
|
4475
4576
|
// src/components/primitives/MarkdownText.tsx
|
|
4476
|
-
import { Platform as
|
|
4577
|
+
import { Platform as Platform6, View as View27 } from "react-native";
|
|
4477
4578
|
import Markdown from "react-native-markdown-display";
|
|
4478
4579
|
import { jsx as jsx37 } from "react/jsx-runtime";
|
|
4479
4580
|
function MarkdownText({ markdown, variant = "chat", bodyColor, style }) {
|
|
@@ -4499,7 +4600,7 @@ function MarkdownText({ markdown, variant = "chat", bodyColor, style }) {
|
|
|
4499
4600
|
paddingHorizontal: variant === "mergeRequest" ? 6 : 4,
|
|
4500
4601
|
paddingVertical: variant === "mergeRequest" ? 2 : 0,
|
|
4501
4602
|
borderRadius: variant === "mergeRequest" ? 6 : 4,
|
|
4502
|
-
fontFamily:
|
|
4603
|
+
fontFamily: Platform6.OS === "ios" ? "Menlo" : "monospace",
|
|
4503
4604
|
fontSize: 13
|
|
4504
4605
|
},
|
|
4505
4606
|
code_block: {
|
|
@@ -5512,12 +5613,12 @@ import { ActivityIndicator as ActivityIndicator8, View as View41 } from "react-n
|
|
|
5512
5613
|
|
|
5513
5614
|
// src/components/chat/ChatPage.tsx
|
|
5514
5615
|
import * as React34 from "react";
|
|
5515
|
-
import { Keyboard as Keyboard4, Platform as
|
|
5616
|
+
import { Keyboard as Keyboard4, Platform as Platform8, View as View37 } from "react-native";
|
|
5516
5617
|
import { useSafeAreaInsets as useSafeAreaInsets4 } from "react-native-safe-area-context";
|
|
5517
5618
|
|
|
5518
5619
|
// src/components/chat/ChatMessageList.tsx
|
|
5519
5620
|
import * as React33 from "react";
|
|
5520
|
-
import {
|
|
5621
|
+
import { View as View36 } from "react-native";
|
|
5521
5622
|
import { BottomSheetFlatList } from "@gorhom/bottom-sheet";
|
|
5522
5623
|
|
|
5523
5624
|
// src/components/chat/ChatMessageBubble.tsx
|
|
@@ -5621,19 +5722,19 @@ var ChatMessageList = React33.forwardRef(
|
|
|
5621
5722
|
const nearBottomRef = React33.useRef(true);
|
|
5622
5723
|
const initialScrollDoneRef = React33.useRef(false);
|
|
5623
5724
|
const lastMessageIdRef = React33.useRef(null);
|
|
5725
|
+
const data = React33.useMemo(() => {
|
|
5726
|
+
return [...messages].reverse();
|
|
5727
|
+
}, [messages]);
|
|
5624
5728
|
const scrollToBottom = React33.useCallback((options) => {
|
|
5625
5729
|
var _a;
|
|
5626
5730
|
const animated = (options == null ? void 0 : options.animated) ?? true;
|
|
5627
|
-
(_a = listRef.current) == null ? void 0 : _a.
|
|
5731
|
+
(_a = listRef.current) == null ? void 0 : _a.scrollToOffset({ offset: 0, animated });
|
|
5628
5732
|
}, []);
|
|
5629
5733
|
React33.useImperativeHandle(ref, () => ({ scrollToBottom }), [scrollToBottom]);
|
|
5630
5734
|
const handleScroll = React33.useCallback(
|
|
5631
5735
|
(e) => {
|
|
5632
5736
|
const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;
|
|
5633
|
-
const distanceFromBottom = Math.max(
|
|
5634
|
-
contentSize.height - Math.max(bottomInset, 0) - (contentOffset.y + layoutMeasurement.height),
|
|
5635
|
-
0
|
|
5636
|
-
);
|
|
5737
|
+
const distanceFromBottom = Math.max(contentOffset.y - Math.max(bottomInset, 0), 0);
|
|
5637
5738
|
const isNear = distanceFromBottom <= nearBottomThreshold;
|
|
5638
5739
|
if (nearBottomRef.current !== isNear) {
|
|
5639
5740
|
nearBottomRef.current = isNear;
|
|
@@ -5642,15 +5743,6 @@ var ChatMessageList = React33.forwardRef(
|
|
|
5642
5743
|
},
|
|
5643
5744
|
[bottomInset, nearBottomThreshold, onNearBottomChange]
|
|
5644
5745
|
);
|
|
5645
|
-
React33.useEffect(() => {
|
|
5646
|
-
var _a;
|
|
5647
|
-
if (initialScrollDoneRef.current) return;
|
|
5648
|
-
if (messages.length === 0) return;
|
|
5649
|
-
initialScrollDoneRef.current = true;
|
|
5650
|
-
lastMessageIdRef.current = ((_a = messages[messages.length - 1]) == null ? void 0 : _a.id) ?? null;
|
|
5651
|
-
const id = requestAnimationFrame(() => scrollToBottom({ animated: false }));
|
|
5652
|
-
return () => cancelAnimationFrame(id);
|
|
5653
|
-
}, [messages, scrollToBottom]);
|
|
5654
5746
|
React33.useEffect(() => {
|
|
5655
5747
|
if (!initialScrollDoneRef.current) return;
|
|
5656
5748
|
const lastId = messages.length > 0 ? messages[messages.length - 1].id : null;
|
|
@@ -5668,33 +5760,35 @@ var ChatMessageList = React33.forwardRef(
|
|
|
5668
5760
|
}
|
|
5669
5761
|
return void 0;
|
|
5670
5762
|
}, [showTypingIndicator, scrollToBottom]);
|
|
5671
|
-
React33.useEffect(() => {
|
|
5672
|
-
if (!initialScrollDoneRef.current) return;
|
|
5673
|
-
if (!nearBottomRef.current) return;
|
|
5674
|
-
const id = requestAnimationFrame(() => scrollToBottom({ animated: false }));
|
|
5675
|
-
return () => cancelAnimationFrame(id);
|
|
5676
|
-
}, [bottomInset, scrollToBottom]);
|
|
5677
5763
|
return /* @__PURE__ */ jsx46(
|
|
5678
5764
|
BottomSheetFlatList,
|
|
5679
5765
|
{
|
|
5680
5766
|
ref: listRef,
|
|
5681
|
-
|
|
5767
|
+
inverted: true,
|
|
5768
|
+
data,
|
|
5682
5769
|
keyExtractor: (m) => m.id,
|
|
5683
|
-
keyboardDismissMode: Platform6.OS === "ios" ? "interactive" : "on-drag",
|
|
5684
5770
|
keyboardShouldPersistTaps: "handled",
|
|
5685
5771
|
onScroll: handleScroll,
|
|
5686
5772
|
scrollEventThrottle: 16,
|
|
5687
5773
|
showsVerticalScrollIndicator: false,
|
|
5774
|
+
onContentSizeChange: () => {
|
|
5775
|
+
if (initialScrollDoneRef.current) return;
|
|
5776
|
+
initialScrollDoneRef.current = true;
|
|
5777
|
+
lastMessageIdRef.current = messages.length > 0 ? messages[messages.length - 1].id : null;
|
|
5778
|
+
nearBottomRef.current = true;
|
|
5779
|
+
onNearBottomChange == null ? void 0 : onNearBottomChange(true);
|
|
5780
|
+
requestAnimationFrame(() => scrollToBottom({ animated: false }));
|
|
5781
|
+
},
|
|
5688
5782
|
contentContainerStyle: [
|
|
5689
5783
|
{
|
|
5690
5784
|
paddingHorizontal: theme.spacing.lg,
|
|
5691
|
-
|
|
5692
|
-
paddingBottom: theme.spacing.sm
|
|
5785
|
+
paddingVertical: theme.spacing.sm
|
|
5693
5786
|
},
|
|
5694
5787
|
contentStyle
|
|
5695
5788
|
],
|
|
5696
|
-
|
|
5697
|
-
|
|
5789
|
+
ItemSeparatorComponent: () => /* @__PURE__ */ jsx46(View36, { style: { height: theme.spacing.sm } }),
|
|
5790
|
+
renderItem: ({ item }) => /* @__PURE__ */ jsx46(ChatMessageBubble, { message: item, renderContent: renderMessageContent }),
|
|
5791
|
+
ListHeaderComponent: /* @__PURE__ */ jsxs28(View36, { children: [
|
|
5698
5792
|
showTypingIndicator ? /* @__PURE__ */ jsx46(View36, { style: { marginTop: theme.spacing.sm, alignSelf: "flex-start", paddingHorizontal: theme.spacing.lg }, children: /* @__PURE__ */ jsx46(TypingIndicator, {}) }) : null,
|
|
5699
5793
|
bottomInset > 0 ? /* @__PURE__ */ jsx46(View36, { style: { height: bottomInset } }) : null
|
|
5700
5794
|
] })
|
|
@@ -5715,6 +5809,7 @@ function ChatPage({
|
|
|
5715
5809
|
composer,
|
|
5716
5810
|
overlay,
|
|
5717
5811
|
style,
|
|
5812
|
+
composerHorizontalPadding,
|
|
5718
5813
|
onNearBottomChange,
|
|
5719
5814
|
listRef
|
|
5720
5815
|
}) {
|
|
@@ -5723,7 +5818,7 @@ function ChatPage({
|
|
|
5723
5818
|
const [composerHeight, setComposerHeight] = React34.useState(0);
|
|
5724
5819
|
const [keyboardVisible, setKeyboardVisible] = React34.useState(false);
|
|
5725
5820
|
React34.useEffect(() => {
|
|
5726
|
-
if (
|
|
5821
|
+
if (Platform8.OS !== "ios") return;
|
|
5727
5822
|
const show = Keyboard4.addListener("keyboardWillShow", () => setKeyboardVisible(true));
|
|
5728
5823
|
const hide = Keyboard4.addListener("keyboardWillHide", () => setKeyboardVisible(false));
|
|
5729
5824
|
return () => {
|
|
@@ -5731,7 +5826,7 @@ function ChatPage({
|
|
|
5731
5826
|
hide.remove();
|
|
5732
5827
|
};
|
|
5733
5828
|
}, []);
|
|
5734
|
-
const footerBottomPadding =
|
|
5829
|
+
const footerBottomPadding = Platform8.OS === "ios" ? keyboardVisible ? 0 : insets.bottom : insets.bottom + 10;
|
|
5735
5830
|
const overlayBottom = composerHeight + footerBottomPadding + theme.spacing.lg;
|
|
5736
5831
|
const bottomInset = composerHeight + footerBottomPadding + theme.spacing.xl;
|
|
5737
5832
|
const resolvedOverlay = React34.useMemo(() => {
|
|
@@ -5775,7 +5870,7 @@ function ChatPage({
|
|
|
5775
5870
|
left: 0,
|
|
5776
5871
|
right: 0,
|
|
5777
5872
|
bottom: 0,
|
|
5778
|
-
paddingHorizontal: theme.spacing.
|
|
5873
|
+
paddingHorizontal: composerHorizontalPadding ?? theme.spacing.md,
|
|
5779
5874
|
paddingTop: theme.spacing.sm,
|
|
5780
5875
|
paddingBottom: footerBottomPadding
|
|
5781
5876
|
},
|
|
@@ -6017,6 +6112,7 @@ function ChatPanel({
|
|
|
6017
6112
|
messages,
|
|
6018
6113
|
showTypingIndicator,
|
|
6019
6114
|
topBanner,
|
|
6115
|
+
composerHorizontalPadding: 0,
|
|
6020
6116
|
listRef,
|
|
6021
6117
|
onNearBottomChange: setNearBottom,
|
|
6022
6118
|
overlay: /* @__PURE__ */ jsx51(
|
|
@@ -6253,6 +6349,92 @@ function ConfirmMergeFlow({
|
|
|
6253
6349
|
);
|
|
6254
6350
|
}
|
|
6255
6351
|
|
|
6352
|
+
// src/studio/hooks/useOptimisticChatMessages.ts
|
|
6353
|
+
import * as React38 from "react";
|
|
6354
|
+
function makeOptimisticId() {
|
|
6355
|
+
return `optimistic:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 10)}`;
|
|
6356
|
+
}
|
|
6357
|
+
function toEpochMs(createdAt) {
|
|
6358
|
+
if (createdAt == null) return 0;
|
|
6359
|
+
if (typeof createdAt === "number") return createdAt;
|
|
6360
|
+
if (createdAt instanceof Date) return createdAt.getTime();
|
|
6361
|
+
const t = Date.parse(String(createdAt));
|
|
6362
|
+
return Number.isFinite(t) ? t : 0;
|
|
6363
|
+
}
|
|
6364
|
+
function isOptimisticResolvedByServer(chatMessages, o) {
|
|
6365
|
+
if (o.failed) return false;
|
|
6366
|
+
const normalize = (s) => s.trim();
|
|
6367
|
+
let startIndex = -1;
|
|
6368
|
+
if (o.baseServerLastId) {
|
|
6369
|
+
startIndex = chatMessages.findIndex((m) => m.id === o.baseServerLastId);
|
|
6370
|
+
}
|
|
6371
|
+
const candidates = startIndex >= 0 ? chatMessages.slice(startIndex + 1) : chatMessages;
|
|
6372
|
+
const target = normalize(o.content);
|
|
6373
|
+
for (const m of candidates) {
|
|
6374
|
+
if (m.author !== "human") continue;
|
|
6375
|
+
if (normalize(m.content) !== target) continue;
|
|
6376
|
+
const serverMs = toEpochMs(m.createdAt);
|
|
6377
|
+
const optimisticMs = Date.parse(o.createdAtIso);
|
|
6378
|
+
if (Number.isFinite(optimisticMs) && optimisticMs > 0 && serverMs > 0) {
|
|
6379
|
+
if (serverMs + 12e4 < optimisticMs) continue;
|
|
6380
|
+
}
|
|
6381
|
+
return true;
|
|
6382
|
+
}
|
|
6383
|
+
return false;
|
|
6384
|
+
}
|
|
6385
|
+
function useOptimisticChatMessages({
|
|
6386
|
+
threadId,
|
|
6387
|
+
shouldForkOnEdit,
|
|
6388
|
+
chatMessages,
|
|
6389
|
+
onSendChat
|
|
6390
|
+
}) {
|
|
6391
|
+
const [optimisticChat, setOptimisticChat] = React38.useState([]);
|
|
6392
|
+
React38.useEffect(() => {
|
|
6393
|
+
setOptimisticChat([]);
|
|
6394
|
+
}, [threadId]);
|
|
6395
|
+
const messages = React38.useMemo(() => {
|
|
6396
|
+
if (!optimisticChat || optimisticChat.length === 0) return chatMessages;
|
|
6397
|
+
const unresolved = optimisticChat.filter((o) => !isOptimisticResolvedByServer(chatMessages, o));
|
|
6398
|
+
if (unresolved.length === 0) return chatMessages;
|
|
6399
|
+
const optimisticAsChat = unresolved.map((o) => ({
|
|
6400
|
+
id: o.id,
|
|
6401
|
+
author: "human",
|
|
6402
|
+
content: o.content,
|
|
6403
|
+
createdAt: o.createdAtIso,
|
|
6404
|
+
kind: "optimistic",
|
|
6405
|
+
meta: o.failed ? { kind: "optimistic", event: "send.failed", status: "error" } : { kind: "optimistic", event: "send.pending", status: "info" }
|
|
6406
|
+
}));
|
|
6407
|
+
const merged = [...chatMessages, ...optimisticAsChat];
|
|
6408
|
+
merged.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
|
|
6409
|
+
return merged;
|
|
6410
|
+
}, [chatMessages, optimisticChat]);
|
|
6411
|
+
React38.useEffect(() => {
|
|
6412
|
+
if (optimisticChat.length === 0) return;
|
|
6413
|
+
setOptimisticChat((prev) => {
|
|
6414
|
+
if (prev.length === 0) return prev;
|
|
6415
|
+
const next = prev.filter((o) => !isOptimisticResolvedByServer(chatMessages, o) || o.failed);
|
|
6416
|
+
return next.length === prev.length ? prev : next;
|
|
6417
|
+
});
|
|
6418
|
+
}, [chatMessages, optimisticChat.length]);
|
|
6419
|
+
const onSend = React38.useCallback(
|
|
6420
|
+
async (text, attachments) => {
|
|
6421
|
+
if (shouldForkOnEdit) {
|
|
6422
|
+
await onSendChat(text, attachments);
|
|
6423
|
+
return;
|
|
6424
|
+
}
|
|
6425
|
+
const createdAtIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
6426
|
+
const baseServerLastId = chatMessages.length > 0 ? chatMessages[chatMessages.length - 1].id : null;
|
|
6427
|
+
const id = makeOptimisticId();
|
|
6428
|
+
setOptimisticChat((prev) => [...prev, { id, content: text, createdAtIso, baseServerLastId, failed: false }]);
|
|
6429
|
+
void Promise.resolve(onSendChat(text, attachments)).catch(() => {
|
|
6430
|
+
setOptimisticChat((prev) => prev.map((m) => m.id === id ? { ...m, failed: true } : m));
|
|
6431
|
+
});
|
|
6432
|
+
},
|
|
6433
|
+
[chatMessages, onSendChat, shouldForkOnEdit]
|
|
6434
|
+
);
|
|
6435
|
+
return { messages, onSend };
|
|
6436
|
+
}
|
|
6437
|
+
|
|
6256
6438
|
// src/studio/ui/StudioOverlay.tsx
|
|
6257
6439
|
import { Fragment as Fragment6, jsx as jsx55, jsxs as jsxs34 } from "react/jsx-runtime";
|
|
6258
6440
|
function StudioOverlay({
|
|
@@ -6285,31 +6467,38 @@ function StudioOverlay({
|
|
|
6285
6467
|
}) {
|
|
6286
6468
|
const theme = useTheme();
|
|
6287
6469
|
const { width } = useWindowDimensions4();
|
|
6288
|
-
const [sheetOpen, setSheetOpen] =
|
|
6289
|
-
const [activePage, setActivePage] =
|
|
6290
|
-
const [drawing, setDrawing] =
|
|
6291
|
-
const [chatAttachments, setChatAttachments] =
|
|
6292
|
-
const [commentsAppId, setCommentsAppId] =
|
|
6293
|
-
const [commentsCount, setCommentsCount] =
|
|
6294
|
-
const
|
|
6295
|
-
const
|
|
6470
|
+
const [sheetOpen, setSheetOpen] = React39.useState(false);
|
|
6471
|
+
const [activePage, setActivePage] = React39.useState("preview");
|
|
6472
|
+
const [drawing, setDrawing] = React39.useState(false);
|
|
6473
|
+
const [chatAttachments, setChatAttachments] = React39.useState([]);
|
|
6474
|
+
const [commentsAppId, setCommentsAppId] = React39.useState(null);
|
|
6475
|
+
const [commentsCount, setCommentsCount] = React39.useState(null);
|
|
6476
|
+
const threadId = (app == null ? void 0 : app.threadId) ?? null;
|
|
6477
|
+
const optimistic = useOptimisticChatMessages({
|
|
6478
|
+
threadId,
|
|
6479
|
+
shouldForkOnEdit,
|
|
6480
|
+
chatMessages,
|
|
6481
|
+
onSendChat
|
|
6482
|
+
});
|
|
6483
|
+
const [confirmMrId, setConfirmMrId] = React39.useState(null);
|
|
6484
|
+
const confirmMr = React39.useMemo(
|
|
6296
6485
|
() => confirmMrId ? incomingMergeRequests.find((m) => m.id === confirmMrId) ?? null : null,
|
|
6297
6486
|
[confirmMrId, incomingMergeRequests]
|
|
6298
6487
|
);
|
|
6299
|
-
const handleSheetOpenChange =
|
|
6488
|
+
const handleSheetOpenChange = React39.useCallback((open) => {
|
|
6300
6489
|
setSheetOpen(open);
|
|
6301
6490
|
if (!open) Keyboard5.dismiss();
|
|
6302
6491
|
}, []);
|
|
6303
|
-
const closeSheet =
|
|
6492
|
+
const closeSheet = React39.useCallback(() => {
|
|
6304
6493
|
handleSheetOpenChange(false);
|
|
6305
6494
|
}, [handleSheetOpenChange]);
|
|
6306
|
-
const openSheet =
|
|
6307
|
-
const goToChat =
|
|
6495
|
+
const openSheet = React39.useCallback(() => setSheetOpen(true), []);
|
|
6496
|
+
const goToChat = React39.useCallback(() => {
|
|
6308
6497
|
setActivePage("chat");
|
|
6309
6498
|
openSheet();
|
|
6310
6499
|
}, [openSheet]);
|
|
6311
|
-
const backToPreview =
|
|
6312
|
-
if (
|
|
6500
|
+
const backToPreview = React39.useCallback(() => {
|
|
6501
|
+
if (Platform9.OS !== "ios") {
|
|
6313
6502
|
Keyboard5.dismiss();
|
|
6314
6503
|
setActivePage("preview");
|
|
6315
6504
|
return;
|
|
@@ -6326,11 +6515,11 @@ function StudioOverlay({
|
|
|
6326
6515
|
const t = setTimeout(finalize, 350);
|
|
6327
6516
|
Keyboard5.dismiss();
|
|
6328
6517
|
}, []);
|
|
6329
|
-
const startDraw =
|
|
6518
|
+
const startDraw = React39.useCallback(() => {
|
|
6330
6519
|
setDrawing(true);
|
|
6331
6520
|
closeSheet();
|
|
6332
6521
|
}, [closeSheet]);
|
|
6333
|
-
const handleDrawCapture =
|
|
6522
|
+
const handleDrawCapture = React39.useCallback(
|
|
6334
6523
|
(dataUrl) => {
|
|
6335
6524
|
setChatAttachments((prev) => [...prev, dataUrl]);
|
|
6336
6525
|
setDrawing(false);
|
|
@@ -6339,7 +6528,7 @@ function StudioOverlay({
|
|
|
6339
6528
|
},
|
|
6340
6529
|
[openSheet]
|
|
6341
6530
|
);
|
|
6342
|
-
const toggleSheet =
|
|
6531
|
+
const toggleSheet = React39.useCallback(async () => {
|
|
6343
6532
|
if (!sheetOpen) {
|
|
6344
6533
|
const shouldExitTest = Boolean(testingMrId) || isTesting;
|
|
6345
6534
|
if (shouldExitTest) {
|
|
@@ -6351,7 +6540,7 @@ function StudioOverlay({
|
|
|
6351
6540
|
closeSheet();
|
|
6352
6541
|
}
|
|
6353
6542
|
}, [closeSheet, isTesting, onRestoreBase, sheetOpen, testingMrId]);
|
|
6354
|
-
const handleTestMr =
|
|
6543
|
+
const handleTestMr = React39.useCallback(
|
|
6355
6544
|
async (mr) => {
|
|
6356
6545
|
if (!onTestMr) return;
|
|
6357
6546
|
await onTestMr(mr);
|
|
@@ -6395,7 +6584,7 @@ function StudioOverlay({
|
|
|
6395
6584
|
chat: /* @__PURE__ */ jsx55(
|
|
6396
6585
|
ChatPanel,
|
|
6397
6586
|
{
|
|
6398
|
-
messages:
|
|
6587
|
+
messages: optimistic.messages,
|
|
6399
6588
|
showTypingIndicator: chatShowTypingIndicator,
|
|
6400
6589
|
loading: chatLoading,
|
|
6401
6590
|
sendDisabled: chatSendDisabled,
|
|
@@ -6410,7 +6599,7 @@ function StudioOverlay({
|
|
|
6410
6599
|
onClose: closeSheet,
|
|
6411
6600
|
onNavigateHome,
|
|
6412
6601
|
onStartDraw: startDraw,
|
|
6413
|
-
onSend:
|
|
6602
|
+
onSend: optimistic.onSend
|
|
6414
6603
|
}
|
|
6415
6604
|
)
|
|
6416
6605
|
}
|
|
@@ -6469,16 +6658,16 @@ function ComergeStudio({
|
|
|
6469
6658
|
onNavigateHome,
|
|
6470
6659
|
style
|
|
6471
6660
|
}) {
|
|
6472
|
-
const [activeAppId, setActiveAppId] =
|
|
6473
|
-
const [runtimeAppId, setRuntimeAppId] =
|
|
6474
|
-
const [pendingRuntimeTargetAppId, setPendingRuntimeTargetAppId] =
|
|
6475
|
-
const platform =
|
|
6476
|
-
|
|
6661
|
+
const [activeAppId, setActiveAppId] = React40.useState(appId);
|
|
6662
|
+
const [runtimeAppId, setRuntimeAppId] = React40.useState(appId);
|
|
6663
|
+
const [pendingRuntimeTargetAppId, setPendingRuntimeTargetAppId] = React40.useState(null);
|
|
6664
|
+
const platform = React40.useMemo(() => RNPlatform.OS === "ios" ? "ios" : "android", []);
|
|
6665
|
+
React40.useEffect(() => {
|
|
6477
6666
|
setActiveAppId(appId);
|
|
6478
6667
|
setRuntimeAppId(appId);
|
|
6479
6668
|
setPendingRuntimeTargetAppId(null);
|
|
6480
6669
|
}, [appId]);
|
|
6481
|
-
const captureTargetRef =
|
|
6670
|
+
const captureTargetRef = React40.useRef(null);
|
|
6482
6671
|
return /* @__PURE__ */ jsx56(StudioBootstrap, { apiKey, children: ({ userId }) => /* @__PURE__ */ jsx56(BottomSheetModalProvider, { children: /* @__PURE__ */ jsx56(
|
|
6483
6672
|
ComergeStudioInner,
|
|
6484
6673
|
{
|
|
@@ -6514,11 +6703,11 @@ function ComergeStudioInner({
|
|
|
6514
6703
|
const { app, loading: appLoading } = useApp(activeAppId);
|
|
6515
6704
|
const { app: runtimeAppFromHook } = useApp(runtimeAppId, { enabled: runtimeAppId !== activeAppId });
|
|
6516
6705
|
const runtimeApp = runtimeAppId === activeAppId ? app : runtimeAppFromHook;
|
|
6517
|
-
const sawEditingOnPendingTargetRef =
|
|
6518
|
-
|
|
6706
|
+
const sawEditingOnPendingTargetRef = React40.useRef(false);
|
|
6707
|
+
React40.useEffect(() => {
|
|
6519
6708
|
sawEditingOnPendingTargetRef.current = false;
|
|
6520
6709
|
}, [pendingRuntimeTargetAppId]);
|
|
6521
|
-
|
|
6710
|
+
React40.useEffect(() => {
|
|
6522
6711
|
if (!pendingRuntimeTargetAppId) return;
|
|
6523
6712
|
if (activeAppId !== pendingRuntimeTargetAppId) return;
|
|
6524
6713
|
if ((app == null ? void 0 : app.status) === "editing") {
|
|
@@ -6535,13 +6724,38 @@ function ComergeStudioInner({
|
|
|
6535
6724
|
platform,
|
|
6536
6725
|
canRequestLatest: (runtimeApp == null ? void 0 : runtimeApp.status) === "ready"
|
|
6537
6726
|
});
|
|
6727
|
+
const sawEditingOnActiveAppRef = React40.useRef(false);
|
|
6728
|
+
const [showPostEditPreparing, setShowPostEditPreparing] = React40.useState(false);
|
|
6729
|
+
React40.useEffect(() => {
|
|
6730
|
+
sawEditingOnActiveAppRef.current = false;
|
|
6731
|
+
setShowPostEditPreparing(false);
|
|
6732
|
+
}, [activeAppId]);
|
|
6733
|
+
React40.useEffect(() => {
|
|
6734
|
+
if (!(app == null ? void 0 : app.id)) return;
|
|
6735
|
+
if (app.status === "editing") {
|
|
6736
|
+
sawEditingOnActiveAppRef.current = true;
|
|
6737
|
+
setShowPostEditPreparing(false);
|
|
6738
|
+
return;
|
|
6739
|
+
}
|
|
6740
|
+
if (app.status === "ready" && sawEditingOnActiveAppRef.current) {
|
|
6741
|
+
setShowPostEditPreparing(true);
|
|
6742
|
+
sawEditingOnActiveAppRef.current = false;
|
|
6743
|
+
}
|
|
6744
|
+
}, [app == null ? void 0 : app.id, app == null ? void 0 : app.status]);
|
|
6745
|
+
React40.useEffect(() => {
|
|
6746
|
+
if (!showPostEditPreparing) return;
|
|
6747
|
+
const stillProcessingBaseBundle = bundle.loading && bundle.loadingMode === "base" && !bundle.isTesting;
|
|
6748
|
+
if (!stillProcessingBaseBundle) {
|
|
6749
|
+
setShowPostEditPreparing(false);
|
|
6750
|
+
}
|
|
6751
|
+
}, [showPostEditPreparing, bundle.loading, bundle.loadingMode, bundle.isTesting]);
|
|
6538
6752
|
const threadId = (app == null ? void 0 : app.threadId) ?? "";
|
|
6539
6753
|
const thread = useThreadMessages(threadId);
|
|
6540
6754
|
const mergeRequests = useMergeRequests({ appId: activeAppId });
|
|
6541
|
-
const hasOpenOutgoingMr =
|
|
6755
|
+
const hasOpenOutgoingMr = React40.useMemo(() => {
|
|
6542
6756
|
return mergeRequests.lists.outgoing.some((mr) => mr.status === "open");
|
|
6543
6757
|
}, [mergeRequests.lists.outgoing]);
|
|
6544
|
-
const incomingReviewMrs =
|
|
6758
|
+
const incomingReviewMrs = React40.useMemo(() => {
|
|
6545
6759
|
if (!userId) return mergeRequests.lists.incoming;
|
|
6546
6760
|
return mergeRequests.lists.incoming.filter((mr) => mr.createdBy !== userId);
|
|
6547
6761
|
}, [mergeRequests.lists.incoming, userId]);
|
|
@@ -6563,9 +6777,9 @@ function ComergeStudioInner({
|
|
|
6563
6777
|
uploadAttachments: uploader.uploadBase64Images
|
|
6564
6778
|
});
|
|
6565
6779
|
const chatSendDisabled = hasNoOutcomeAfterLastHuman(thread.raw);
|
|
6566
|
-
const [processingMrId, setProcessingMrId] =
|
|
6567
|
-
const [testingMrId, setTestingMrId] =
|
|
6568
|
-
const chatShowTypingIndicator =
|
|
6780
|
+
const [processingMrId, setProcessingMrId] = React40.useState(null);
|
|
6781
|
+
const [testingMrId, setTestingMrId] = React40.useState(null);
|
|
6782
|
+
const chatShowTypingIndicator = React40.useMemo(() => {
|
|
6569
6783
|
var _a;
|
|
6570
6784
|
if (!thread.raw || thread.raw.length === 0) return false;
|
|
6571
6785
|
const last = thread.raw[thread.raw.length - 1];
|
|
@@ -6573,7 +6787,15 @@ function ComergeStudioInner({
|
|
|
6573
6787
|
return payloadType !== "outcome";
|
|
6574
6788
|
}, [thread.raw]);
|
|
6575
6789
|
return /* @__PURE__ */ jsx56(View45, { style: [{ flex: 1 }, style], children: /* @__PURE__ */ jsxs35(View45, { ref: captureTargetRef, style: { flex: 1 }, collapsable: false, children: [
|
|
6576
|
-
/* @__PURE__ */ jsx56(
|
|
6790
|
+
/* @__PURE__ */ jsx56(
|
|
6791
|
+
RuntimeRenderer,
|
|
6792
|
+
{
|
|
6793
|
+
appKey,
|
|
6794
|
+
bundlePath: bundle.bundlePath,
|
|
6795
|
+
forcePreparing: showPostEditPreparing,
|
|
6796
|
+
renderToken: bundle.renderToken
|
|
6797
|
+
}
|
|
6798
|
+
),
|
|
6577
6799
|
/* @__PURE__ */ jsx56(
|
|
6578
6800
|
StudioOverlay,
|
|
6579
6801
|
{
|