@comergehq/studio 0.1.8 → 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 +642 -427
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +363 -148
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/ChatMessageList.tsx +23 -35
- package/src/components/chat/ChatPage.tsx +26 -30
- package/src/components/studio-sheet/StudioBottomSheet.tsx +4 -19
- 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 +19 -6
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,17 +1782,17 @@ 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";
|
|
1689
|
-
import { AppState as AppState2, Keyboard,
|
|
1790
|
+
import { AppState as AppState2, Keyboard, View as View4 } from "react-native";
|
|
1690
1791
|
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,
|
|
@@ -1777,18 +1878,6 @@ function StudioBottomSheet({
|
|
|
1777
1878
|
});
|
|
1778
1879
|
return () => sub.remove();
|
|
1779
1880
|
}, [open, resolvedSheetRef]);
|
|
1780
|
-
React9.useEffect(() => {
|
|
1781
|
-
if (Platform2.OS !== "ios") return;
|
|
1782
|
-
const sub = Keyboard.addListener("keyboardDidHide", () => {
|
|
1783
|
-
const sheet = resolvedSheetRef.current;
|
|
1784
|
-
if (!sheet || !open) return;
|
|
1785
|
-
const targetIndex = snapPoints.length - 1;
|
|
1786
|
-
if (currentIndexRef.current === targetIndex) {
|
|
1787
|
-
setTimeout(() => sheet.snapToIndex(targetIndex), 10);
|
|
1788
|
-
}
|
|
1789
|
-
});
|
|
1790
|
-
return () => sub.remove();
|
|
1791
|
-
}, [open, resolvedSheetRef, snapPoints.length]);
|
|
1792
1881
|
React9.useEffect(() => {
|
|
1793
1882
|
const sheet = resolvedSheetRef.current;
|
|
1794
1883
|
if (!sheet) return;
|
|
@@ -1811,9 +1900,9 @@ function StudioBottomSheet({
|
|
|
1811
1900
|
ref: resolvedSheetRef,
|
|
1812
1901
|
index: open ? snapPoints.length - 1 : -1,
|
|
1813
1902
|
snapPoints,
|
|
1903
|
+
enableDynamicSizing: false,
|
|
1814
1904
|
enablePanDownToClose: true,
|
|
1815
|
-
|
|
1816
|
-
keyboardBlurBehavior: "restore",
|
|
1905
|
+
enableContentPanningGesture: false,
|
|
1817
1906
|
android_keyboardInputMode: "adjustResize",
|
|
1818
1907
|
backgroundComponent: (props) => /* @__PURE__ */ jsx5(StudioSheetBackground, { ...props, renderBackground: background == null ? void 0 : background.renderBackground }),
|
|
1819
1908
|
topInset: insets.top,
|
|
@@ -2791,7 +2880,7 @@ var styles3 = StyleSheet3.create({
|
|
|
2791
2880
|
|
|
2792
2881
|
// src/components/comments/AppCommentsSheet.tsx
|
|
2793
2882
|
import * as React21 from "react";
|
|
2794
|
-
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";
|
|
2795
2884
|
import {
|
|
2796
2885
|
BottomSheetBackdrop,
|
|
2797
2886
|
BottomSheetModal,
|
|
@@ -3396,11 +3485,11 @@ function useAppDetails(appId) {
|
|
|
3396
3485
|
|
|
3397
3486
|
// src/components/comments/useIosKeyboardSnapFix.ts
|
|
3398
3487
|
import * as React20 from "react";
|
|
3399
|
-
import { Keyboard as Keyboard2, Platform as
|
|
3488
|
+
import { Keyboard as Keyboard2, Platform as Platform4 } from "react-native";
|
|
3400
3489
|
function useIosKeyboardSnapFix(sheetRef, options) {
|
|
3401
3490
|
const [keyboardVisible, setKeyboardVisible] = React20.useState(false);
|
|
3402
3491
|
React20.useEffect(() => {
|
|
3403
|
-
if (
|
|
3492
|
+
if (Platform4.OS !== "ios") return;
|
|
3404
3493
|
const show = Keyboard2.addListener("keyboardWillShow", () => setKeyboardVisible(true));
|
|
3405
3494
|
const hide = Keyboard2.addListener("keyboardWillHide", () => {
|
|
3406
3495
|
var _a;
|
|
@@ -3478,8 +3567,8 @@ function AppCommentsSheet({ appId, onClose, onCountChange, onPlayApp }) {
|
|
|
3478
3567
|
onChange: handleChange,
|
|
3479
3568
|
backgroundStyle: {
|
|
3480
3569
|
backgroundColor: theme.scheme === "dark" ? "#0B080F" : "#FFFFFF",
|
|
3481
|
-
borderTopLeftRadius:
|
|
3482
|
-
borderTopRightRadius:
|
|
3570
|
+
borderTopLeftRadius: Platform5.OS === "ios" ? 39 : 16,
|
|
3571
|
+
borderTopRightRadius: Platform5.OS === "ios" ? 39 : 16
|
|
3483
3572
|
},
|
|
3484
3573
|
handleIndicatorStyle: { backgroundColor: theme.colors.handleIndicator },
|
|
3485
3574
|
keyboardBehavior: "interactive",
|
|
@@ -3586,7 +3675,7 @@ function AppCommentsSheet({ appId, onClose, onCountChange, onPlayApp }) {
|
|
|
3586
3675
|
bottom: 0,
|
|
3587
3676
|
paddingHorizontal: theme.spacing.lg,
|
|
3588
3677
|
paddingTop: theme.spacing.sm,
|
|
3589
|
-
paddingBottom:
|
|
3678
|
+
paddingBottom: Platform5.OS === "ios" ? keyboardVisible ? theme.spacing.lg : insets.bottom : insets.bottom + 10,
|
|
3590
3679
|
borderTopWidth: 1,
|
|
3591
3680
|
borderTopColor: withAlpha(theme.colors.border, 0.1),
|
|
3592
3681
|
backgroundColor: withAlpha(theme.colors.background, 0.8)
|
|
@@ -4485,7 +4574,7 @@ import { Animated as Animated7, Pressable as Pressable9, View as View28 } from "
|
|
|
4485
4574
|
import { Ban, Check as Check3, CheckCheck, ChevronDown as ChevronDown2 } from "lucide-react-native";
|
|
4486
4575
|
|
|
4487
4576
|
// src/components/primitives/MarkdownText.tsx
|
|
4488
|
-
import { Platform as
|
|
4577
|
+
import { Platform as Platform6, View as View27 } from "react-native";
|
|
4489
4578
|
import Markdown from "react-native-markdown-display";
|
|
4490
4579
|
import { jsx as jsx37 } from "react/jsx-runtime";
|
|
4491
4580
|
function MarkdownText({ markdown, variant = "chat", bodyColor, style }) {
|
|
@@ -4511,7 +4600,7 @@ function MarkdownText({ markdown, variant = "chat", bodyColor, style }) {
|
|
|
4511
4600
|
paddingHorizontal: variant === "mergeRequest" ? 6 : 4,
|
|
4512
4601
|
paddingVertical: variant === "mergeRequest" ? 2 : 0,
|
|
4513
4602
|
borderRadius: variant === "mergeRequest" ? 6 : 4,
|
|
4514
|
-
fontFamily:
|
|
4603
|
+
fontFamily: Platform6.OS === "ios" ? "Menlo" : "monospace",
|
|
4515
4604
|
fontSize: 13
|
|
4516
4605
|
},
|
|
4517
4606
|
code_block: {
|
|
@@ -5524,9 +5613,8 @@ import { ActivityIndicator as ActivityIndicator8, View as View41 } from "react-n
|
|
|
5524
5613
|
|
|
5525
5614
|
// src/components/chat/ChatPage.tsx
|
|
5526
5615
|
import * as React34 from "react";
|
|
5527
|
-
import { Keyboard as Keyboard4, Platform as
|
|
5616
|
+
import { Keyboard as Keyboard4, Platform as Platform8, View as View37 } from "react-native";
|
|
5528
5617
|
import { useSafeAreaInsets as useSafeAreaInsets4 } from "react-native-safe-area-context";
|
|
5529
|
-
import Animated11, { useAnimatedKeyboard, useAnimatedStyle as useAnimatedStyle2 } from "react-native-reanimated";
|
|
5530
5618
|
|
|
5531
5619
|
// src/components/chat/ChatMessageList.tsx
|
|
5532
5620
|
import * as React33 from "react";
|
|
@@ -5634,19 +5722,19 @@ var ChatMessageList = React33.forwardRef(
|
|
|
5634
5722
|
const nearBottomRef = React33.useRef(true);
|
|
5635
5723
|
const initialScrollDoneRef = React33.useRef(false);
|
|
5636
5724
|
const lastMessageIdRef = React33.useRef(null);
|
|
5725
|
+
const data = React33.useMemo(() => {
|
|
5726
|
+
return [...messages].reverse();
|
|
5727
|
+
}, [messages]);
|
|
5637
5728
|
const scrollToBottom = React33.useCallback((options) => {
|
|
5638
5729
|
var _a;
|
|
5639
5730
|
const animated = (options == null ? void 0 : options.animated) ?? true;
|
|
5640
|
-
(_a = listRef.current) == null ? void 0 : _a.
|
|
5731
|
+
(_a = listRef.current) == null ? void 0 : _a.scrollToOffset({ offset: 0, animated });
|
|
5641
5732
|
}, []);
|
|
5642
5733
|
React33.useImperativeHandle(ref, () => ({ scrollToBottom }), [scrollToBottom]);
|
|
5643
5734
|
const handleScroll = React33.useCallback(
|
|
5644
5735
|
(e) => {
|
|
5645
5736
|
const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;
|
|
5646
|
-
const distanceFromBottom = Math.max(
|
|
5647
|
-
contentSize.height - Math.max(bottomInset, 0) - (contentOffset.y + layoutMeasurement.height),
|
|
5648
|
-
0
|
|
5649
|
-
);
|
|
5737
|
+
const distanceFromBottom = Math.max(contentOffset.y - Math.max(bottomInset, 0), 0);
|
|
5650
5738
|
const isNear = distanceFromBottom <= nearBottomThreshold;
|
|
5651
5739
|
if (nearBottomRef.current !== isNear) {
|
|
5652
5740
|
nearBottomRef.current = isNear;
|
|
@@ -5655,15 +5743,6 @@ var ChatMessageList = React33.forwardRef(
|
|
|
5655
5743
|
},
|
|
5656
5744
|
[bottomInset, nearBottomThreshold, onNearBottomChange]
|
|
5657
5745
|
);
|
|
5658
|
-
React33.useEffect(() => {
|
|
5659
|
-
var _a;
|
|
5660
|
-
if (initialScrollDoneRef.current) return;
|
|
5661
|
-
if (messages.length === 0) return;
|
|
5662
|
-
initialScrollDoneRef.current = true;
|
|
5663
|
-
lastMessageIdRef.current = ((_a = messages[messages.length - 1]) == null ? void 0 : _a.id) ?? null;
|
|
5664
|
-
const id = requestAnimationFrame(() => scrollToBottom({ animated: false }));
|
|
5665
|
-
return () => cancelAnimationFrame(id);
|
|
5666
|
-
}, [messages, scrollToBottom]);
|
|
5667
5746
|
React33.useEffect(() => {
|
|
5668
5747
|
if (!initialScrollDoneRef.current) return;
|
|
5669
5748
|
const lastId = messages.length > 0 ? messages[messages.length - 1].id : null;
|
|
@@ -5681,31 +5760,35 @@ var ChatMessageList = React33.forwardRef(
|
|
|
5681
5760
|
}
|
|
5682
5761
|
return void 0;
|
|
5683
5762
|
}, [showTypingIndicator, scrollToBottom]);
|
|
5684
|
-
React33.useEffect(() => {
|
|
5685
|
-
if (!initialScrollDoneRef.current) return;
|
|
5686
|
-
if (!nearBottomRef.current) return;
|
|
5687
|
-
const id = requestAnimationFrame(() => scrollToBottom({ animated: false }));
|
|
5688
|
-
return () => cancelAnimationFrame(id);
|
|
5689
|
-
}, [bottomInset, scrollToBottom]);
|
|
5690
5763
|
return /* @__PURE__ */ jsx46(
|
|
5691
5764
|
BottomSheetFlatList,
|
|
5692
5765
|
{
|
|
5693
5766
|
ref: listRef,
|
|
5694
|
-
|
|
5767
|
+
inverted: true,
|
|
5768
|
+
data,
|
|
5695
5769
|
keyExtractor: (m) => m.id,
|
|
5770
|
+
keyboardShouldPersistTaps: "handled",
|
|
5696
5771
|
onScroll: handleScroll,
|
|
5697
5772
|
scrollEventThrottle: 16,
|
|
5698
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
|
+
},
|
|
5699
5782
|
contentContainerStyle: [
|
|
5700
5783
|
{
|
|
5701
5784
|
paddingHorizontal: theme.spacing.lg,
|
|
5702
|
-
|
|
5703
|
-
paddingBottom: theme.spacing.sm
|
|
5785
|
+
paddingVertical: theme.spacing.sm
|
|
5704
5786
|
},
|
|
5705
5787
|
contentStyle
|
|
5706
5788
|
],
|
|
5707
|
-
|
|
5708
|
-
|
|
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: [
|
|
5709
5792
|
showTypingIndicator ? /* @__PURE__ */ jsx46(View36, { style: { marginTop: theme.spacing.sm, alignSelf: "flex-start", paddingHorizontal: theme.spacing.lg }, children: /* @__PURE__ */ jsx46(TypingIndicator, {}) }) : null,
|
|
5710
5793
|
bottomInset > 0 ? /* @__PURE__ */ jsx46(View36, { style: { height: bottomInset } }) : null
|
|
5711
5794
|
] })
|
|
@@ -5726,6 +5809,7 @@ function ChatPage({
|
|
|
5726
5809
|
composer,
|
|
5727
5810
|
overlay,
|
|
5728
5811
|
style,
|
|
5812
|
+
composerHorizontalPadding,
|
|
5729
5813
|
onNearBottomChange,
|
|
5730
5814
|
listRef
|
|
5731
5815
|
}) {
|
|
@@ -5733,9 +5817,8 @@ function ChatPage({
|
|
|
5733
5817
|
const insets = useSafeAreaInsets4();
|
|
5734
5818
|
const [composerHeight, setComposerHeight] = React34.useState(0);
|
|
5735
5819
|
const [keyboardVisible, setKeyboardVisible] = React34.useState(false);
|
|
5736
|
-
const animatedKeyboard = useAnimatedKeyboard();
|
|
5737
5820
|
React34.useEffect(() => {
|
|
5738
|
-
if (
|
|
5821
|
+
if (Platform8.OS !== "ios") return;
|
|
5739
5822
|
const show = Keyboard4.addListener("keyboardWillShow", () => setKeyboardVisible(true));
|
|
5740
5823
|
const hide = Keyboard4.addListener("keyboardWillHide", () => setKeyboardVisible(false));
|
|
5741
5824
|
return () => {
|
|
@@ -5743,11 +5826,7 @@ function ChatPage({
|
|
|
5743
5826
|
hide.remove();
|
|
5744
5827
|
};
|
|
5745
5828
|
}, []);
|
|
5746
|
-
const footerBottomPadding =
|
|
5747
|
-
const footerAnimatedStyle = useAnimatedStyle2(() => {
|
|
5748
|
-
if (Platform6.OS !== "ios") return { paddingBottom: insets.bottom + 10 };
|
|
5749
|
-
return { paddingBottom: animatedKeyboard.height.value > 0 ? 0 : insets.bottom };
|
|
5750
|
-
});
|
|
5829
|
+
const footerBottomPadding = Platform8.OS === "ios" ? keyboardVisible ? 0 : insets.bottom : insets.bottom + 10;
|
|
5751
5830
|
const overlayBottom = composerHeight + footerBottomPadding + theme.spacing.lg;
|
|
5752
5831
|
const bottomInset = composerHeight + footerBottomPadding + theme.spacing.xl;
|
|
5753
5832
|
const resolvedOverlay = React34.useMemo(() => {
|
|
@@ -5763,32 +5842,38 @@ function ChatPage({
|
|
|
5763
5842
|
header ? /* @__PURE__ */ jsx47(View37, { children: header }) : null,
|
|
5764
5843
|
topBanner ? /* @__PURE__ */ jsx47(View37, { style: { paddingHorizontal: theme.spacing.lg, paddingTop: theme.spacing.sm }, children: topBanner }) : null,
|
|
5765
5844
|
/* @__PURE__ */ jsxs29(View37, { style: { flex: 1 }, children: [
|
|
5766
|
-
/* @__PURE__ */
|
|
5767
|
-
|
|
5845
|
+
/* @__PURE__ */ jsxs29(
|
|
5846
|
+
View37,
|
|
5768
5847
|
{
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5848
|
+
style: { flex: 1 },
|
|
5849
|
+
children: [
|
|
5850
|
+
/* @__PURE__ */ jsx47(
|
|
5851
|
+
ChatMessageList,
|
|
5852
|
+
{
|
|
5853
|
+
ref: listRef,
|
|
5854
|
+
messages,
|
|
5855
|
+
showTypingIndicator,
|
|
5856
|
+
renderMessageContent,
|
|
5857
|
+
onNearBottomChange,
|
|
5858
|
+
bottomInset
|
|
5859
|
+
}
|
|
5860
|
+
),
|
|
5861
|
+
resolvedOverlay
|
|
5862
|
+
]
|
|
5775
5863
|
}
|
|
5776
5864
|
),
|
|
5777
|
-
resolvedOverlay,
|
|
5778
5865
|
/* @__PURE__ */ jsx47(
|
|
5779
|
-
|
|
5866
|
+
View37,
|
|
5780
5867
|
{
|
|
5781
|
-
style:
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
footerAnimatedStyle
|
|
5791
|
-
],
|
|
5868
|
+
style: {
|
|
5869
|
+
position: "absolute",
|
|
5870
|
+
left: 0,
|
|
5871
|
+
right: 0,
|
|
5872
|
+
bottom: 0,
|
|
5873
|
+
paddingHorizontal: composerHorizontalPadding ?? theme.spacing.md,
|
|
5874
|
+
paddingTop: theme.spacing.sm,
|
|
5875
|
+
paddingBottom: footerBottomPadding
|
|
5876
|
+
},
|
|
5792
5877
|
children: /* @__PURE__ */ jsx47(
|
|
5793
5878
|
ChatComposer,
|
|
5794
5879
|
{
|
|
@@ -5806,7 +5891,7 @@ function ChatPage({
|
|
|
5806
5891
|
// src/components/chat/ScrollToBottomButton.tsx
|
|
5807
5892
|
import * as React35 from "react";
|
|
5808
5893
|
import { Pressable as Pressable12, View as View38 } from "react-native";
|
|
5809
|
-
import
|
|
5894
|
+
import Animated11, { Easing as Easing2, useAnimatedStyle as useAnimatedStyle2, useSharedValue as useSharedValue2, withTiming as withTiming2 } from "react-native-reanimated";
|
|
5810
5895
|
import { jsx as jsx48 } from "react/jsx-runtime";
|
|
5811
5896
|
function ScrollToBottomButton({ visible, onPress, children, style }) {
|
|
5812
5897
|
const theme = useTheme();
|
|
@@ -5815,14 +5900,14 @@ function ScrollToBottomButton({ visible, onPress, children, style }) {
|
|
|
5815
5900
|
React35.useEffect(() => {
|
|
5816
5901
|
progress.value = withTiming2(visible ? 1 : 0, { duration: 200, easing: Easing2.out(Easing2.ease) });
|
|
5817
5902
|
}, [progress, visible]);
|
|
5818
|
-
const animStyle =
|
|
5903
|
+
const animStyle = useAnimatedStyle2(() => ({
|
|
5819
5904
|
opacity: progress.value,
|
|
5820
5905
|
transform: [{ translateY: (1 - progress.value) * 20 }]
|
|
5821
5906
|
}));
|
|
5822
5907
|
const bg = theme.scheme === "dark" ? "rgba(39,39,42,0.9)" : "rgba(244,244,245,0.95)";
|
|
5823
5908
|
const border = theme.scheme === "dark" ? withAlpha("#FFFFFF", 0.12) : withAlpha("#000000", 0.08);
|
|
5824
5909
|
return /* @__PURE__ */ jsx48(
|
|
5825
|
-
|
|
5910
|
+
Animated11.View,
|
|
5826
5911
|
{
|
|
5827
5912
|
pointerEvents: visible ? "auto" : "none",
|
|
5828
5913
|
style: [
|
|
@@ -6027,6 +6112,7 @@ function ChatPanel({
|
|
|
6027
6112
|
messages,
|
|
6028
6113
|
showTypingIndicator,
|
|
6029
6114
|
topBanner,
|
|
6115
|
+
composerHorizontalPadding: 0,
|
|
6030
6116
|
listRef,
|
|
6031
6117
|
onNearBottomChange: setNearBottom,
|
|
6032
6118
|
overlay: /* @__PURE__ */ jsx51(
|
|
@@ -6263,6 +6349,92 @@ function ConfirmMergeFlow({
|
|
|
6263
6349
|
);
|
|
6264
6350
|
}
|
|
6265
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
|
+
|
|
6266
6438
|
// src/studio/ui/StudioOverlay.tsx
|
|
6267
6439
|
import { Fragment as Fragment6, jsx as jsx55, jsxs as jsxs34 } from "react/jsx-runtime";
|
|
6268
6440
|
function StudioOverlay({
|
|
@@ -6295,28 +6467,38 @@ function StudioOverlay({
|
|
|
6295
6467
|
}) {
|
|
6296
6468
|
const theme = useTheme();
|
|
6297
6469
|
const { width } = useWindowDimensions4();
|
|
6298
|
-
const [sheetOpen, setSheetOpen] =
|
|
6299
|
-
const [activePage, setActivePage] =
|
|
6300
|
-
const [drawing, setDrawing] =
|
|
6301
|
-
const [chatAttachments, setChatAttachments] =
|
|
6302
|
-
const [commentsAppId, setCommentsAppId] =
|
|
6303
|
-
const [commentsCount, setCommentsCount] =
|
|
6304
|
-
const
|
|
6305
|
-
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(
|
|
6306
6485
|
() => confirmMrId ? incomingMergeRequests.find((m) => m.id === confirmMrId) ?? null : null,
|
|
6307
6486
|
[confirmMrId, incomingMergeRequests]
|
|
6308
6487
|
);
|
|
6309
|
-
const
|
|
6310
|
-
setSheetOpen(
|
|
6311
|
-
Keyboard5.dismiss();
|
|
6488
|
+
const handleSheetOpenChange = React39.useCallback((open) => {
|
|
6489
|
+
setSheetOpen(open);
|
|
6490
|
+
if (!open) Keyboard5.dismiss();
|
|
6312
6491
|
}, []);
|
|
6313
|
-
const
|
|
6314
|
-
|
|
6492
|
+
const closeSheet = React39.useCallback(() => {
|
|
6493
|
+
handleSheetOpenChange(false);
|
|
6494
|
+
}, [handleSheetOpenChange]);
|
|
6495
|
+
const openSheet = React39.useCallback(() => setSheetOpen(true), []);
|
|
6496
|
+
const goToChat = React39.useCallback(() => {
|
|
6315
6497
|
setActivePage("chat");
|
|
6316
6498
|
openSheet();
|
|
6317
6499
|
}, [openSheet]);
|
|
6318
|
-
const backToPreview =
|
|
6319
|
-
if (
|
|
6500
|
+
const backToPreview = React39.useCallback(() => {
|
|
6501
|
+
if (Platform9.OS !== "ios") {
|
|
6320
6502
|
Keyboard5.dismiss();
|
|
6321
6503
|
setActivePage("preview");
|
|
6322
6504
|
return;
|
|
@@ -6333,11 +6515,11 @@ function StudioOverlay({
|
|
|
6333
6515
|
const t = setTimeout(finalize, 350);
|
|
6334
6516
|
Keyboard5.dismiss();
|
|
6335
6517
|
}, []);
|
|
6336
|
-
const startDraw =
|
|
6518
|
+
const startDraw = React39.useCallback(() => {
|
|
6337
6519
|
setDrawing(true);
|
|
6338
6520
|
closeSheet();
|
|
6339
6521
|
}, [closeSheet]);
|
|
6340
|
-
const handleDrawCapture =
|
|
6522
|
+
const handleDrawCapture = React39.useCallback(
|
|
6341
6523
|
(dataUrl) => {
|
|
6342
6524
|
setChatAttachments((prev) => [...prev, dataUrl]);
|
|
6343
6525
|
setDrawing(false);
|
|
@@ -6346,7 +6528,7 @@ function StudioOverlay({
|
|
|
6346
6528
|
},
|
|
6347
6529
|
[openSheet]
|
|
6348
6530
|
);
|
|
6349
|
-
const toggleSheet =
|
|
6531
|
+
const toggleSheet = React39.useCallback(async () => {
|
|
6350
6532
|
if (!sheetOpen) {
|
|
6351
6533
|
const shouldExitTest = Boolean(testingMrId) || isTesting;
|
|
6352
6534
|
if (shouldExitTest) {
|
|
@@ -6358,7 +6540,7 @@ function StudioOverlay({
|
|
|
6358
6540
|
closeSheet();
|
|
6359
6541
|
}
|
|
6360
6542
|
}, [closeSheet, isTesting, onRestoreBase, sheetOpen, testingMrId]);
|
|
6361
|
-
const handleTestMr =
|
|
6543
|
+
const handleTestMr = React39.useCallback(
|
|
6362
6544
|
async (mr) => {
|
|
6363
6545
|
if (!onTestMr) return;
|
|
6364
6546
|
await onTestMr(mr);
|
|
@@ -6368,7 +6550,7 @@ function StudioOverlay({
|
|
|
6368
6550
|
);
|
|
6369
6551
|
return /* @__PURE__ */ jsxs34(Fragment6, { children: [
|
|
6370
6552
|
/* @__PURE__ */ jsx55(EdgeGlowFrame, { visible: isTesting, role: "accent", thickness: 40, intensity: 1 }),
|
|
6371
|
-
/* @__PURE__ */ jsx55(StudioBottomSheet, { open: sheetOpen, onOpenChange:
|
|
6553
|
+
/* @__PURE__ */ jsx55(StudioBottomSheet, { open: sheetOpen, onOpenChange: handleSheetOpenChange, children: /* @__PURE__ */ jsx55(
|
|
6372
6554
|
StudioSheetPager,
|
|
6373
6555
|
{
|
|
6374
6556
|
activePage,
|
|
@@ -6402,7 +6584,7 @@ function StudioOverlay({
|
|
|
6402
6584
|
chat: /* @__PURE__ */ jsx55(
|
|
6403
6585
|
ChatPanel,
|
|
6404
6586
|
{
|
|
6405
|
-
messages:
|
|
6587
|
+
messages: optimistic.messages,
|
|
6406
6588
|
showTypingIndicator: chatShowTypingIndicator,
|
|
6407
6589
|
loading: chatLoading,
|
|
6408
6590
|
sendDisabled: chatSendDisabled,
|
|
@@ -6417,7 +6599,7 @@ function StudioOverlay({
|
|
|
6417
6599
|
onClose: closeSheet,
|
|
6418
6600
|
onNavigateHome,
|
|
6419
6601
|
onStartDraw: startDraw,
|
|
6420
|
-
onSend:
|
|
6602
|
+
onSend: optimistic.onSend
|
|
6421
6603
|
}
|
|
6422
6604
|
)
|
|
6423
6605
|
}
|
|
@@ -6476,16 +6658,16 @@ function ComergeStudio({
|
|
|
6476
6658
|
onNavigateHome,
|
|
6477
6659
|
style
|
|
6478
6660
|
}) {
|
|
6479
|
-
const [activeAppId, setActiveAppId] =
|
|
6480
|
-
const [runtimeAppId, setRuntimeAppId] =
|
|
6481
|
-
const [pendingRuntimeTargetAppId, setPendingRuntimeTargetAppId] =
|
|
6482
|
-
const platform =
|
|
6483
|
-
|
|
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(() => {
|
|
6484
6666
|
setActiveAppId(appId);
|
|
6485
6667
|
setRuntimeAppId(appId);
|
|
6486
6668
|
setPendingRuntimeTargetAppId(null);
|
|
6487
6669
|
}, [appId]);
|
|
6488
|
-
const captureTargetRef =
|
|
6670
|
+
const captureTargetRef = React40.useRef(null);
|
|
6489
6671
|
return /* @__PURE__ */ jsx56(StudioBootstrap, { apiKey, children: ({ userId }) => /* @__PURE__ */ jsx56(BottomSheetModalProvider, { children: /* @__PURE__ */ jsx56(
|
|
6490
6672
|
ComergeStudioInner,
|
|
6491
6673
|
{
|
|
@@ -6521,11 +6703,11 @@ function ComergeStudioInner({
|
|
|
6521
6703
|
const { app, loading: appLoading } = useApp(activeAppId);
|
|
6522
6704
|
const { app: runtimeAppFromHook } = useApp(runtimeAppId, { enabled: runtimeAppId !== activeAppId });
|
|
6523
6705
|
const runtimeApp = runtimeAppId === activeAppId ? app : runtimeAppFromHook;
|
|
6524
|
-
const sawEditingOnPendingTargetRef =
|
|
6525
|
-
|
|
6706
|
+
const sawEditingOnPendingTargetRef = React40.useRef(false);
|
|
6707
|
+
React40.useEffect(() => {
|
|
6526
6708
|
sawEditingOnPendingTargetRef.current = false;
|
|
6527
6709
|
}, [pendingRuntimeTargetAppId]);
|
|
6528
|
-
|
|
6710
|
+
React40.useEffect(() => {
|
|
6529
6711
|
if (!pendingRuntimeTargetAppId) return;
|
|
6530
6712
|
if (activeAppId !== pendingRuntimeTargetAppId) return;
|
|
6531
6713
|
if ((app == null ? void 0 : app.status) === "editing") {
|
|
@@ -6542,13 +6724,38 @@ function ComergeStudioInner({
|
|
|
6542
6724
|
platform,
|
|
6543
6725
|
canRequestLatest: (runtimeApp == null ? void 0 : runtimeApp.status) === "ready"
|
|
6544
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]);
|
|
6545
6752
|
const threadId = (app == null ? void 0 : app.threadId) ?? "";
|
|
6546
6753
|
const thread = useThreadMessages(threadId);
|
|
6547
6754
|
const mergeRequests = useMergeRequests({ appId: activeAppId });
|
|
6548
|
-
const hasOpenOutgoingMr =
|
|
6755
|
+
const hasOpenOutgoingMr = React40.useMemo(() => {
|
|
6549
6756
|
return mergeRequests.lists.outgoing.some((mr) => mr.status === "open");
|
|
6550
6757
|
}, [mergeRequests.lists.outgoing]);
|
|
6551
|
-
const incomingReviewMrs =
|
|
6758
|
+
const incomingReviewMrs = React40.useMemo(() => {
|
|
6552
6759
|
if (!userId) return mergeRequests.lists.incoming;
|
|
6553
6760
|
return mergeRequests.lists.incoming.filter((mr) => mr.createdBy !== userId);
|
|
6554
6761
|
}, [mergeRequests.lists.incoming, userId]);
|
|
@@ -6570,9 +6777,9 @@ function ComergeStudioInner({
|
|
|
6570
6777
|
uploadAttachments: uploader.uploadBase64Images
|
|
6571
6778
|
});
|
|
6572
6779
|
const chatSendDisabled = hasNoOutcomeAfterLastHuman(thread.raw);
|
|
6573
|
-
const [processingMrId, setProcessingMrId] =
|
|
6574
|
-
const [testingMrId, setTestingMrId] =
|
|
6575
|
-
const chatShowTypingIndicator =
|
|
6780
|
+
const [processingMrId, setProcessingMrId] = React40.useState(null);
|
|
6781
|
+
const [testingMrId, setTestingMrId] = React40.useState(null);
|
|
6782
|
+
const chatShowTypingIndicator = React40.useMemo(() => {
|
|
6576
6783
|
var _a;
|
|
6577
6784
|
if (!thread.raw || thread.raw.length === 0) return false;
|
|
6578
6785
|
const last = thread.raw[thread.raw.length - 1];
|
|
@@ -6580,7 +6787,15 @@ function ComergeStudioInner({
|
|
|
6580
6787
|
return payloadType !== "outcome";
|
|
6581
6788
|
}, [thread.raw]);
|
|
6582
6789
|
return /* @__PURE__ */ jsx56(View45, { style: [{ flex: 1 }, style], children: /* @__PURE__ */ jsxs35(View45, { ref: captureTargetRef, style: { flex: 1 }, collapsable: false, children: [
|
|
6583
|
-
/* @__PURE__ */ jsx56(
|
|
6790
|
+
/* @__PURE__ */ jsx56(
|
|
6791
|
+
RuntimeRenderer,
|
|
6792
|
+
{
|
|
6793
|
+
appKey,
|
|
6794
|
+
bundlePath: bundle.bundlePath,
|
|
6795
|
+
forcePreparing: showPostEditPreparing,
|
|
6796
|
+
renderToken: bundle.renderToken
|
|
6797
|
+
}
|
|
6798
|
+
),
|
|
6584
6799
|
/* @__PURE__ */ jsx56(
|
|
6585
6800
|
StudioOverlay,
|
|
6586
6801
|
{
|