@comergehq/studio 0.1.13 → 0.1.16

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.mjs CHANGED
@@ -6,8 +6,8 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
6
6
  });
7
7
 
8
8
  // src/studio/ComergeStudio.tsx
9
- import * as React43 from "react";
10
- import { Platform as RNPlatform, View as View45 } from "react-native";
9
+ import * as React46 from "react";
10
+ import { Platform as RNPlatform, View as View46 } from "react-native";
11
11
  import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
12
12
 
13
13
  // src/studio/bootstrap/StudioBootstrap.tsx
@@ -816,6 +816,35 @@ function extractMeta(payload) {
816
816
  threadId: typeof obj.threadId === "string" ? obj.threadId : void 0
817
817
  };
818
818
  }
819
+ function getPayloadMeta(payload) {
820
+ const meta = payload == null ? void 0 : payload.meta;
821
+ if (!meta || typeof meta !== "object") return null;
822
+ return meta;
823
+ }
824
+ function isQueuedHiddenMessage(m) {
825
+ if (m.authorType !== "human") return false;
826
+ const meta = getPayloadMeta(m.payload);
827
+ return (meta == null ? void 0 : meta.visibility) === "queued";
828
+ }
829
+ function toEpochMs(value) {
830
+ if (value == null) return 0;
831
+ if (typeof value === "number") return value;
832
+ if (value instanceof Date) return value.getTime();
833
+ const parsed = Date.parse(String(value));
834
+ return Number.isFinite(parsed) ? parsed : 0;
835
+ }
836
+ function getEffectiveSortMs(m) {
837
+ const meta = getPayloadMeta(m.payload);
838
+ const runStartedAt = meta == null ? void 0 : meta.runStartedAt;
839
+ const runMs = toEpochMs(runStartedAt);
840
+ return runMs > 0 ? runMs : toEpochMs(m.createdAt);
841
+ }
842
+ function compareMessages(a, b) {
843
+ const aMs = getEffectiveSortMs(a);
844
+ const bMs = getEffectiveSortMs(b);
845
+ if (aMs !== bMs) return aMs - bMs;
846
+ return String(a.createdAt).localeCompare(String(b.createdAt));
847
+ }
819
848
  function mapMessageToChatMessage(m) {
820
849
  var _a, _b;
821
850
  const kind = typeof ((_a = m.payload) == null ? void 0 : _a.type) === "string" ? String(m.payload.type) : null;
@@ -835,8 +864,10 @@ function useThreadMessages(threadId) {
835
864
  const activeRequestIdRef = React4.useRef(0);
836
865
  const foregroundSignal = useForegroundSignal(Boolean(threadId));
837
866
  const upsertSorted = React4.useCallback((prev, m) => {
838
- const next = prev.some((x) => x.id === m.id) ? prev.map((x) => x.id === m.id ? m : x) : [...prev, m];
839
- next.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
867
+ const include = !isQueuedHiddenMessage(m);
868
+ const next = prev.filter((x) => x.id !== m.id);
869
+ if (include) next.push(m);
870
+ next.sort(compareMessages);
840
871
  return next;
841
872
  }, []);
842
873
  const refetch = React4.useCallback(async () => {
@@ -850,7 +881,7 @@ function useThreadMessages(threadId) {
850
881
  try {
851
882
  const list = await messagesRepository.list(threadId);
852
883
  if (activeRequestIdRef.current !== requestId) return;
853
- setRaw([...list].sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt))));
884
+ setRaw([...list].filter((m) => !isQueuedHiddenMessage(m)).sort(compareMessages));
854
885
  } catch (e) {
855
886
  if (activeRequestIdRef.current !== requestId) return;
856
887
  setError(e instanceof Error ? e : new Error(String(e)));
@@ -884,6 +915,7 @@ function useThreadMessages(threadId) {
884
915
  import * as React5 from "react";
885
916
  import * as FileSystem from "expo-file-system/legacy";
886
917
  import { Asset } from "expo-asset";
918
+ import { unzip } from "react-native-zip-archive";
887
919
 
888
920
  // src/data/apps/bundles/remote.ts
889
921
  var BundlesRemoteDataSourceImpl = class extends BaseRemote {
@@ -907,6 +939,13 @@ var BundlesRemoteDataSourceImpl = class extends BaseRemote {
907
939
  );
908
940
  return data;
909
941
  }
942
+ async getSignedAssetsDownloadUrl(appId, bundleId, options) {
943
+ const { data } = await api.get(
944
+ `/v1/apps/${encodeURIComponent(appId)}/bundles/${encodeURIComponent(bundleId)}/assets/download`,
945
+ { params: { redirect: (options == null ? void 0 : options.redirect) ?? false, kind: options == null ? void 0 : options.kind } }
946
+ );
947
+ return data;
948
+ }
910
949
  };
911
950
  var bundlesRemoteDataSource = new BundlesRemoteDataSourceImpl();
912
951
 
@@ -928,6 +967,10 @@ var BundlesRepositoryImpl = class extends BaseRepository {
928
967
  const res = await this.remote.getSignedDownloadUrl(appId, bundleId, options);
929
968
  return this.unwrapOrThrow(res);
930
969
  }
970
+ async getSignedAssetsDownloadUrl(appId, bundleId, options) {
971
+ const res = await this.remote.getSignedAssetsDownloadUrl(appId, bundleId, options);
972
+ return this.unwrapOrThrow(res);
973
+ }
931
974
  };
932
975
  var bundlesRepository = new BundlesRepositoryImpl(bundlesRemoteDataSource);
933
976
 
@@ -979,20 +1022,40 @@ async function ensureDir(path) {
979
1022
  if (info.exists) return;
980
1023
  await FileSystem.makeDirectoryAsync(path, { intermediates: true });
981
1024
  }
1025
+ async function ensureBundleDir(key) {
1026
+ await ensureDir(bundlesCacheDir());
1027
+ await ensureDir(bundleDir(key));
1028
+ }
982
1029
  function baseBundleKey(appId, platform) {
983
1030
  return `base:${appId}:${platform}`;
984
1031
  }
985
1032
  function testBundleKey(appId, commitId, platform, bundleId) {
986
1033
  return `test:${appId}:${commitId ?? "head"}:${platform}:${bundleId}`;
987
1034
  }
988
- function toBundleFileUri(key) {
1035
+ function legacyBundleFileUri(key) {
989
1036
  const dir = bundlesCacheDir();
990
1037
  return `${dir}${safeName(key)}.jsbundle`;
991
1038
  }
992
- function toBundleMetaFileUri(key) {
1039
+ function legacyBundleMetaFileUri(key) {
993
1040
  const dir = bundlesCacheDir();
994
1041
  return `${dir}${safeName(key)}.meta.json`;
995
1042
  }
1043
+ function bundleDir(key) {
1044
+ const dir = bundlesCacheDir();
1045
+ return `${dir}${safeName(key)}/`;
1046
+ }
1047
+ function toBundleFileUri(key, platform) {
1048
+ return `${bundleDir(key)}index.${platform}.jsbundle`;
1049
+ }
1050
+ function toBundleMetaFileUri(key) {
1051
+ return `${bundleDir(key)}bundle.meta.json`;
1052
+ }
1053
+ function toAssetsMetaFileUri(key) {
1054
+ return `${bundleDir(key)}assets.meta.json`;
1055
+ }
1056
+ function toAssetsDir(key) {
1057
+ return `${bundleDir(key)}assets/`;
1058
+ }
996
1059
  async function readJsonFile(fileUri) {
997
1060
  try {
998
1061
  const info = await FileSystem.getInfoAsync(fileUri);
@@ -1019,6 +1082,14 @@ async function getExistingNonEmptyFileUri(fileUri) {
1019
1082
  return null;
1020
1083
  }
1021
1084
  }
1085
+ async function getExistingBundleFileUri(key, platform) {
1086
+ const nextPath = toBundleFileUri(key, platform);
1087
+ const next = await getExistingNonEmptyFileUri(nextPath);
1088
+ if (next) return next;
1089
+ const legacyPath = legacyBundleFileUri(key);
1090
+ const legacy = await getExistingNonEmptyFileUri(legacyPath);
1091
+ return legacy;
1092
+ }
1022
1093
  async function downloadIfMissing(url, fileUri) {
1023
1094
  const existing = await getExistingNonEmptyFileUri(fileUri);
1024
1095
  if (existing) return existing;
@@ -1045,9 +1116,12 @@ async function deleteFileIfExists(fileUri) {
1045
1116
  async function hydrateBaseFromEmbeddedAsset(appId, platform, embedded) {
1046
1117
  if (!(embedded == null ? void 0 : embedded.module)) return null;
1047
1118
  const key = baseBundleKey(appId, platform);
1048
- const targetUri = toBundleFileUri(key);
1049
- const existing = await getExistingNonEmptyFileUri(targetUri);
1050
- if (existing) return { bundlePath: existing, meta: embedded.meta ?? null };
1119
+ const existing = await getExistingBundleFileUri(key, platform);
1120
+ if (existing) {
1121
+ return { bundlePath: existing, meta: embedded.meta ?? null };
1122
+ }
1123
+ await ensureBundleDir(key);
1124
+ const targetUri = toBundleFileUri(key, platform);
1051
1125
  const asset = Asset.fromModule(embedded.module);
1052
1126
  await asset.downloadAsync();
1053
1127
  const sourceUri = asset.localUri ?? asset.uri;
@@ -1060,8 +1134,50 @@ async function hydrateBaseFromEmbeddedAsset(appId, platform, embedded) {
1060
1134
  if (!finalUri) return null;
1061
1135
  return { bundlePath: finalUri, meta: embedded.meta ?? null };
1062
1136
  }
1063
- async function safeReplaceFileFromUrl(url, targetUri, tmpKey) {
1064
- const tmpUri = toBundleFileUri(`tmp:${tmpKey}:${Date.now()}`);
1137
+ async function hydrateAssetsFromEmbeddedAsset(appId, platform, key, embedded) {
1138
+ var _a;
1139
+ const moduleId = embedded == null ? void 0 : embedded.assetsModule;
1140
+ if (!moduleId) return false;
1141
+ const assetsMeta = (embedded == null ? void 0 : embedded.assetsMeta) ?? null;
1142
+ const assetsDir = toAssetsDir(key);
1143
+ const metaUri = toAssetsMetaFileUri(key);
1144
+ const existingMeta = await readJsonFile(metaUri);
1145
+ const assetsDirInfo = await FileSystem.getInfoAsync(assetsDir);
1146
+ const assetsDirExists = assetsDirInfo.exists && assetsDirInfo.isDirectory;
1147
+ const checksumMatches = Boolean(existingMeta == null ? void 0 : existingMeta.checksumSha256) && Boolean(assetsMeta == null ? void 0 : assetsMeta.checksumSha256) && (existingMeta == null ? void 0 : existingMeta.checksumSha256) === (assetsMeta == null ? void 0 : assetsMeta.checksumSha256);
1148
+ const embeddedMetaMatches = (_a = existingMeta == null ? void 0 : existingMeta.storageKey) == null ? void 0 : _a.startsWith("embedded:");
1149
+ if (assetsDirExists && checksumMatches && embeddedMetaMatches) {
1150
+ return true;
1151
+ }
1152
+ await ensureBundleDir(key);
1153
+ await ensureDir(assetsDir);
1154
+ const asset = Asset.fromModule(moduleId);
1155
+ await asset.downloadAsync();
1156
+ const sourceUri = asset.localUri ?? asset.uri;
1157
+ if (!sourceUri) return false;
1158
+ const info = await FileSystem.getInfoAsync(sourceUri);
1159
+ if (!info.exists) return false;
1160
+ const zipUri = `${bundleDir(key)}assets.zip`;
1161
+ await deleteFileIfExists(zipUri);
1162
+ await FileSystem.copyAsync({ from: sourceUri, to: zipUri });
1163
+ try {
1164
+ await FileSystem.deleteAsync(assetsDir, { idempotent: true }).catch(() => {
1165
+ });
1166
+ } catch {
1167
+ }
1168
+ await ensureDir(assetsDir);
1169
+ await unzipArchive(zipUri, assetsDir);
1170
+ await writeJsonFile(metaUri, {
1171
+ checksumSha256: (assetsMeta == null ? void 0 : assetsMeta.checksumSha256) ?? null,
1172
+ storageKey: `embedded:${(assetsMeta == null ? void 0 : assetsMeta.checksumSha256) ?? "unknown"}`,
1173
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1174
+ });
1175
+ return true;
1176
+ }
1177
+ async function safeReplaceFileFromUrl(url, targetUri, tmpKey, platform) {
1178
+ const tmpKeySafe = `tmp:${tmpKey}:${Date.now()}`;
1179
+ const tmpUri = toBundleFileUri(tmpKeySafe, platform);
1180
+ await ensureDir(bundleDir(tmpKeySafe));
1065
1181
  try {
1066
1182
  await withRetry(
1067
1183
  async () => {
@@ -1081,6 +1197,82 @@ async function safeReplaceFileFromUrl(url, targetUri, tmpKey) {
1081
1197
  await deleteFileIfExists(tmpUri);
1082
1198
  }
1083
1199
  }
1200
+ async function safeReplaceFileFromUrlToPath(url, targetUri, tmpKey) {
1201
+ const tmpDir = `${bundlesCacheDir()}tmp/`;
1202
+ await ensureDir(tmpDir);
1203
+ const tmpUri = `${tmpDir}${safeName(tmpKey)}.tmp`;
1204
+ try {
1205
+ await withRetry(
1206
+ async () => {
1207
+ await deleteFileIfExists(tmpUri);
1208
+ await FileSystem.downloadAsync(url, tmpUri);
1209
+ const tmpOk = await getExistingNonEmptyFileUri(tmpUri);
1210
+ if (!tmpOk) throw new Error("Downloaded file is empty.");
1211
+ },
1212
+ { attempts: 3, baseDelayMs: 500, maxDelayMs: 4e3 }
1213
+ );
1214
+ await deleteFileIfExists(targetUri);
1215
+ await FileSystem.moveAsync({ from: tmpUri, to: targetUri });
1216
+ const finalOk = await getExistingNonEmptyFileUri(targetUri);
1217
+ if (!finalOk) throw new Error("File replacement failed.");
1218
+ return targetUri;
1219
+ } finally {
1220
+ await deleteFileIfExists(tmpUri);
1221
+ }
1222
+ }
1223
+ function getMetroAssets(bundle) {
1224
+ const assets = bundle.assets ?? [];
1225
+ return assets.find((asset) => asset.kind === "metro-assets") ?? null;
1226
+ }
1227
+ async function ensureAssetsForBundle(appId, bundle, key, platform) {
1228
+ var _a;
1229
+ const asset = getMetroAssets(bundle);
1230
+ if (!(asset == null ? void 0 : asset.storageKey)) return;
1231
+ await ensureBundleDir(key);
1232
+ const assetsDir = toAssetsDir(key);
1233
+ await ensureDir(assetsDir);
1234
+ const metaUri = toAssetsMetaFileUri(key);
1235
+ const existingMeta = await readJsonFile(metaUri);
1236
+ const assetsDirInfo = await FileSystem.getInfoAsync(assetsDir);
1237
+ const assetsDirExists = assetsDirInfo.exists && assetsDirInfo.isDirectory;
1238
+ if ((existingMeta == null ? void 0 : existingMeta.checksumSha256) && asset.checksumSha256 && existingMeta.checksumSha256 === asset.checksumSha256 && (existingMeta.storageKey === asset.storageKey || ((_a = existingMeta.storageKey) == null ? void 0 : _a.startsWith("embedded:"))) && assetsDirExists) {
1239
+ return;
1240
+ }
1241
+ const signed = await withRetry(
1242
+ async () => {
1243
+ return await bundlesRepository.getSignedAssetsDownloadUrl(appId, bundle.id, {
1244
+ redirect: false,
1245
+ kind: asset.kind
1246
+ });
1247
+ },
1248
+ { attempts: 3, baseDelayMs: 500, maxDelayMs: 4e3 }
1249
+ );
1250
+ const zipUri = `${bundleDir(key)}assets.zip`;
1251
+ await safeReplaceFileFromUrlToPath(signed.url, zipUri, `${appId}:${bundle.id}:${platform}:${asset.kind}`);
1252
+ try {
1253
+ await FileSystem.deleteAsync(assetsDir, { idempotent: true }).catch(() => {
1254
+ });
1255
+ } catch {
1256
+ }
1257
+ await ensureDir(assetsDir);
1258
+ await unzipArchive(zipUri, assetsDir);
1259
+ await writeJsonFile(metaUri, {
1260
+ checksumSha256: asset.checksumSha256 ?? null,
1261
+ storageKey: asset.storageKey,
1262
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1263
+ });
1264
+ }
1265
+ async function unzipArchive(sourceUri, destDir) {
1266
+ try {
1267
+ await unzip(sourceUri, destDir);
1268
+ } catch (e) {
1269
+ throw new Error(
1270
+ `Failed to extract assets archive. Ensure 'react-native-zip-archive' is installed in the host app. ${String(
1271
+ (e == null ? void 0 : e.message) ?? e
1272
+ )}`
1273
+ );
1274
+ }
1275
+ }
1084
1276
  async function pollBundle(appId, bundleId, opts) {
1085
1277
  const start = Date.now();
1086
1278
  while (true) {
@@ -1116,18 +1308,33 @@ async function resolveBundlePath(src, platform, mode) {
1116
1308
  if (finalBundle.status === "failed") {
1117
1309
  throw new Error("Bundle build failed.");
1118
1310
  }
1311
+ let bundleWithAssets = finalBundle;
1312
+ if (finalBundle.status === "succeeded" && (!finalBundle.assets || finalBundle.assets.length === 0)) {
1313
+ try {
1314
+ bundleWithAssets = await bundlesRepository.getById(appId, finalBundle.id);
1315
+ } catch {
1316
+ bundleWithAssets = finalBundle;
1317
+ }
1318
+ }
1119
1319
  const signed = await withRetry(
1120
1320
  async () => {
1121
1321
  return await bundlesRepository.getSignedDownloadUrl(appId, finalBundle.id, { redirect: false });
1122
1322
  },
1123
1323
  { attempts: 3, baseDelayMs: 500, maxDelayMs: 4e3 }
1124
1324
  );
1325
+ const key = mode === "base" ? baseBundleKey(appId, platform) : testBundleKey(appId, commitId, platform, finalBundle.id);
1326
+ await ensureBundleDir(key);
1125
1327
  const bundlePath = mode === "base" ? await safeReplaceFileFromUrl(
1126
1328
  signed.url,
1127
- toBundleFileUri(baseBundleKey(appId, platform)),
1128
- `${appId}:${commitId ?? "head"}:${platform}:${finalBundle.id}`
1129
- ) : await downloadIfMissing(signed.url, toBundleFileUri(testBundleKey(appId, commitId, platform, finalBundle.id)));
1130
- return { bundlePath, label: "Ready", bundle: finalBundle };
1329
+ toBundleFileUri(key, platform),
1330
+ `${appId}:${commitId ?? "head"}:${platform}:${finalBundle.id}`,
1331
+ platform
1332
+ ) : await downloadIfMissing(signed.url, toBundleFileUri(key, platform));
1333
+ try {
1334
+ await ensureAssetsForBundle(appId, bundleWithAssets, key, platform);
1335
+ } catch {
1336
+ }
1337
+ return { bundlePath, label: "Ready", bundle: bundleWithAssets };
1131
1338
  }
1132
1339
  function useBundleManager({
1133
1340
  base,
@@ -1168,13 +1375,12 @@ function useBundleManager({
1168
1375
  const hasCompletedFirstNetworkBaseLoadRef = React5.useRef(false);
1169
1376
  const hydrateBaseFromDisk = React5.useCallback(
1170
1377
  async (appId, reason) => {
1171
- var _a;
1378
+ var _a, _b, _c;
1172
1379
  try {
1173
1380
  const dir = bundlesCacheDir();
1174
1381
  await ensureDir(dir);
1175
1382
  const key = baseBundleKey(appId, platform);
1176
- const uri = toBundleFileUri(key);
1177
- let existing = await getExistingNonEmptyFileUri(uri);
1383
+ let existing = await getExistingBundleFileUri(key, platform);
1178
1384
  let embeddedMeta = null;
1179
1385
  if (!existing) {
1180
1386
  const embedded = (_a = embeddedBaseBundlesRef.current) == null ? void 0 : _a[platform];
@@ -1183,14 +1389,25 @@ function useBundleManager({
1183
1389
  existing = hydrated.bundlePath;
1184
1390
  embeddedMeta = hydrated.meta ?? null;
1185
1391
  if (embeddedMeta) {
1392
+ await ensureBundleDir(key);
1186
1393
  await writeJsonFile(toBundleMetaFileUri(key), embeddedMeta);
1394
+ await writeJsonFile(legacyBundleMetaFileUri(key), embeddedMeta);
1187
1395
  }
1188
1396
  }
1189
1397
  }
1190
1398
  if (existing) {
1191
1399
  lastBaseBundlePathRef.current = existing;
1192
1400
  setBundlePath(existing);
1193
- const meta = embeddedMeta ?? await readJsonFile(toBundleMetaFileUri(key));
1401
+ const meta = embeddedMeta ?? await readJsonFile(toBundleMetaFileUri(key)) ?? await readJsonFile(legacyBundleMetaFileUri(key));
1402
+ const embedded = (_b = embeddedBaseBundlesRef.current) == null ? void 0 : _b[platform];
1403
+ const embeddedFingerprint = ((_c = embedded == null ? void 0 : embedded.meta) == null ? void 0 : _c.fingerprint) ?? null;
1404
+ const actualFingerprint = (meta == null ? void 0 : meta.fingerprint) ?? (embeddedMeta == null ? void 0 : embeddedMeta.fingerprint) ?? null;
1405
+ if ((embedded == null ? void 0 : embedded.assetsModule) && embeddedFingerprint && actualFingerprint === embeddedFingerprint) {
1406
+ try {
1407
+ await hydrateAssetsFromEmbeddedAsset(appId, platform, key, embedded);
1408
+ } catch {
1409
+ }
1410
+ }
1194
1411
  if (meta == null ? void 0 : meta.fingerprint) {
1195
1412
  lastBaseFingerprintRef.current = meta.fingerprint;
1196
1413
  }
@@ -1255,7 +1472,16 @@ function useBundleManager({
1255
1472
  lastBaseFingerprintRef.current = fingerprint;
1256
1473
  hasCompletedFirstNetworkBaseLoadRef.current = true;
1257
1474
  initialHydratedBaseFromDiskRef.current = false;
1258
- void writeJsonFile(toBundleMetaFileUri(baseBundleKey(src.appId, platform)), {
1475
+ const metaKey = baseBundleKey(src.appId, platform);
1476
+ await ensureBundleDir(metaKey);
1477
+ void writeJsonFile(toBundleMetaFileUri(metaKey), {
1478
+ fingerprint,
1479
+ bundleId: bundle.id,
1480
+ checksumSha256: bundle.checksumSha256 ?? null,
1481
+ size: bundle.size ?? null,
1482
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1483
+ });
1484
+ void writeJsonFile(legacyBundleMetaFileUri(metaKey), {
1259
1485
  fingerprint,
1260
1486
  bundleId: bundle.id,
1261
1487
  checksumSha256: bundle.checksumSha256 ?? null,
@@ -1707,6 +1933,9 @@ function useStudioActions({
1707
1933
  userId,
1708
1934
  app,
1709
1935
  onForkedApp,
1936
+ onEditStart,
1937
+ onEditQueued,
1938
+ onEditFinished,
1710
1939
  uploadAttachments
1711
1940
  }) {
1712
1941
  const [forking, setForking] = React8.useState(false);
@@ -1722,6 +1951,7 @@ function useStudioActions({
1722
1951
  setSending(true);
1723
1952
  setError(null);
1724
1953
  try {
1954
+ onEditStart == null ? void 0 : onEditStart();
1725
1955
  let targetApp = app;
1726
1956
  if (shouldForkOnEdit) {
1727
1957
  setForking(true);
@@ -1737,12 +1967,16 @@ function useStudioActions({
1737
1967
  if (attachments && attachments.length > 0 && uploadAttachments) {
1738
1968
  attachmentMetas = await uploadAttachments({ threadId, appId: targetApp.id, dataUrls: attachments });
1739
1969
  }
1740
- await agentRepository.editApp({
1970
+ const editResult = await agentRepository.editApp({
1741
1971
  prompt,
1742
1972
  thread_id: threadId,
1743
1973
  app_id: targetApp.id,
1744
1974
  attachments: attachmentMetas && attachmentMetas.length > 0 ? attachmentMetas : void 0
1745
1975
  });
1976
+ onEditQueued == null ? void 0 : onEditQueued({
1977
+ queueItemId: editResult.queueItemId ?? null,
1978
+ queuePosition: editResult.queuePosition ?? null
1979
+ });
1746
1980
  } catch (e) {
1747
1981
  const err = e instanceof Error ? e : new Error(String(e));
1748
1982
  setError(err);
@@ -1750,32 +1984,14 @@ function useStudioActions({
1750
1984
  } finally {
1751
1985
  setForking(false);
1752
1986
  setSending(false);
1987
+ onEditFinished == null ? void 0 : onEditFinished();
1753
1988
  }
1754
1989
  },
1755
- [app, onForkedApp, sending, shouldForkOnEdit, uploadAttachments, userId]
1990
+ [app, onEditFinished, onEditQueued, onEditStart, onForkedApp, sending, shouldForkOnEdit, uploadAttachments, userId]
1756
1991
  );
1757
1992
  return { isOwner, shouldForkOnEdit, forking, sending, error, sendEdit };
1758
1993
  }
1759
1994
 
1760
- // src/studio/lib/chat.ts
1761
- function hasNoOutcomeAfterLastHuman(messages) {
1762
- if (!messages || messages.length === 0) return false;
1763
- let lastHumanIndex = -1;
1764
- for (let i = messages.length - 1; i >= 0; i -= 1) {
1765
- if (messages[i].authorType === "human") {
1766
- lastHumanIndex = i;
1767
- break;
1768
- }
1769
- }
1770
- if (lastHumanIndex === -1) return false;
1771
- for (let i = lastHumanIndex + 1; i < messages.length; i += 1) {
1772
- const m = messages[i];
1773
- const payload = m.payload;
1774
- if (m.authorType === "ai" && (payload == null ? void 0 : payload.type) === "outcome") return false;
1775
- }
1776
- return true;
1777
- }
1778
-
1779
1995
  // src/studio/ui/RuntimeRenderer.tsx
1780
1996
  import * as React9 from "react";
1781
1997
  import { View as View2 } from "react-native";
@@ -1813,8 +2029,8 @@ function RuntimeRenderer({
1813
2029
  }
1814
2030
 
1815
2031
  // src/studio/ui/StudioOverlay.tsx
1816
- import * as React42 from "react";
1817
- import { Keyboard as Keyboard5, Platform as Platform10, View as View44, useWindowDimensions as useWindowDimensions4 } from "react-native";
2032
+ import * as React43 from "react";
2033
+ import { Keyboard as Keyboard5, Platform as Platform10, View as View45, useWindowDimensions as useWindowDimensions4 } from "react-native";
1818
2034
 
1819
2035
  // src/components/studio-sheet/StudioBottomSheet.tsx
1820
2036
  import * as React12 from "react";
@@ -5674,8 +5890,8 @@ function PreviewPanel({
5674
5890
  }
5675
5891
 
5676
5892
  // src/studio/ui/ChatPanel.tsx
5677
- import * as React39 from "react";
5678
- import { ActivityIndicator as ActivityIndicator8, View as View41 } from "react-native";
5893
+ import * as React40 from "react";
5894
+ import { ActivityIndicator as ActivityIndicator9, View as View42 } from "react-native";
5679
5895
 
5680
5896
  // src/components/chat/ChatPage.tsx
5681
5897
  import * as React37 from "react";
@@ -5872,6 +6088,7 @@ function ChatPage({
5872
6088
  showTypingIndicator,
5873
6089
  renderMessageContent,
5874
6090
  topBanner,
6091
+ composerTop,
5875
6092
  composer,
5876
6093
  overlay,
5877
6094
  style,
@@ -5882,6 +6099,7 @@ function ChatPage({
5882
6099
  const theme = useTheme();
5883
6100
  const insets = useSafeAreaInsets4();
5884
6101
  const [composerHeight, setComposerHeight] = React37.useState(0);
6102
+ const [composerTopHeight, setComposerTopHeight] = React37.useState(0);
5885
6103
  const [keyboardVisible, setKeyboardVisible] = React37.useState(false);
5886
6104
  React37.useEffect(() => {
5887
6105
  if (Platform9.OS !== "ios") return;
@@ -5893,8 +6111,9 @@ function ChatPage({
5893
6111
  };
5894
6112
  }, []);
5895
6113
  const footerBottomPadding = Platform9.OS === "ios" ? keyboardVisible ? 0 : insets.bottom : insets.bottom + 10;
5896
- const overlayBottom = composerHeight + footerBottomPadding + theme.spacing.lg;
5897
- const bottomInset = composerHeight + footerBottomPadding + theme.spacing.xl;
6114
+ const totalComposerHeight = composerHeight + composerTopHeight;
6115
+ const overlayBottom = totalComposerHeight + footerBottomPadding + theme.spacing.lg;
6116
+ const bottomInset = totalComposerHeight + footerBottomPadding + theme.spacing.xl;
5898
6117
  const resolvedOverlay = React37.useMemo(() => {
5899
6118
  var _a;
5900
6119
  if (!overlay) return null;
@@ -5904,6 +6123,10 @@ function ChatPage({
5904
6123
  style: [prevStyle, { bottom: overlayBottom }]
5905
6124
  });
5906
6125
  }, [overlay, overlayBottom]);
6126
+ React37.useEffect(() => {
6127
+ if (composerTop) return;
6128
+ setComposerTopHeight(0);
6129
+ }, [composerTop]);
5907
6130
  return /* @__PURE__ */ jsxs29(View37, { style: [{ flex: 1 }, style], children: [
5908
6131
  header ? /* @__PURE__ */ jsx49(View37, { children: header }) : null,
5909
6132
  topBanner ? /* @__PURE__ */ jsx49(View37, { style: { paddingHorizontal: theme.spacing.lg, paddingTop: theme.spacing.sm }, children: topBanner }) : null,
@@ -5928,7 +6151,7 @@ function ChatPage({
5928
6151
  ]
5929
6152
  }
5930
6153
  ),
5931
- /* @__PURE__ */ jsx49(
6154
+ /* @__PURE__ */ jsxs29(
5932
6155
  View37,
5933
6156
  {
5934
6157
  style: {
@@ -5940,14 +6163,24 @@ function ChatPage({
5940
6163
  paddingTop: theme.spacing.sm,
5941
6164
  paddingBottom: footerBottomPadding
5942
6165
  },
5943
- children: /* @__PURE__ */ jsx49(
5944
- ChatComposer,
5945
- {
5946
- ...composer,
5947
- attachments: composer.attachments ?? [],
5948
- onLayout: ({ height }) => setComposerHeight(height)
5949
- }
5950
- )
6166
+ children: [
6167
+ composerTop ? /* @__PURE__ */ jsx49(
6168
+ View37,
6169
+ {
6170
+ style: { marginBottom: theme.spacing.sm },
6171
+ onLayout: (e) => setComposerTopHeight(e.nativeEvent.layout.height),
6172
+ children: composerTop
6173
+ }
6174
+ ) : null,
6175
+ /* @__PURE__ */ jsx49(
6176
+ ChatComposer,
6177
+ {
6178
+ ...composer,
6179
+ attachments: composer.attachments ?? [],
6180
+ onLayout: ({ height }) => setComposerHeight(height)
6181
+ }
6182
+ )
6183
+ ]
5951
6184
  }
5952
6185
  )
5953
6186
  ] })
@@ -6097,8 +6330,154 @@ function ForkNoticeBanner({ isOwner = true, title, description, style }) {
6097
6330
  );
6098
6331
  }
6099
6332
 
6100
- // src/studio/ui/ChatPanel.tsx
6333
+ // src/components/chat/ChatQueue.tsx
6334
+ import * as React39 from "react";
6335
+ import { ActivityIndicator as ActivityIndicator8, Pressable as Pressable13, View as View41 } from "react-native";
6101
6336
  import { jsx as jsx53, jsxs as jsxs31 } from "react/jsx-runtime";
6337
+ function ChatQueue({ items, onRemove }) {
6338
+ const theme = useTheme();
6339
+ const [expanded, setExpanded] = React39.useState({});
6340
+ const [canExpand, setCanExpand] = React39.useState({});
6341
+ const [collapsedText, setCollapsedText] = React39.useState({});
6342
+ const [removing, setRemoving] = React39.useState({});
6343
+ const buildCollapsedText = React39.useCallback((lines) => {
6344
+ var _a, _b;
6345
+ const line1 = ((_a = lines[0]) == null ? void 0 : _a.text) ?? "";
6346
+ const line2 = ((_b = lines[1]) == null ? void 0 : _b.text) ?? "";
6347
+ const moreLabel = "more";
6348
+ const reserve = `\u2026 ${moreLabel}`.length;
6349
+ let trimmedLine2 = line2;
6350
+ if (trimmedLine2.length > reserve) {
6351
+ trimmedLine2 = trimmedLine2.slice(0, Math.max(0, trimmedLine2.length - reserve));
6352
+ } else {
6353
+ trimmedLine2 = "";
6354
+ }
6355
+ trimmedLine2 = trimmedLine2.replace(/\s+$/, "");
6356
+ return `${line1}
6357
+ ${trimmedLine2}\u2026 `;
6358
+ }, []);
6359
+ React39.useEffect(() => {
6360
+ if (items.length === 0) return;
6361
+ const ids = new Set(items.map((item) => item.id));
6362
+ setExpanded((prev) => Object.fromEntries(Object.entries(prev).filter(([id]) => ids.has(id))));
6363
+ setCanExpand((prev) => Object.fromEntries(Object.entries(prev).filter(([id]) => ids.has(id))));
6364
+ setCollapsedText((prev) => Object.fromEntries(Object.entries(prev).filter(([id]) => ids.has(id))));
6365
+ setRemoving((prev) => Object.fromEntries(Object.entries(prev).filter(([id]) => ids.has(id))));
6366
+ }, [items]);
6367
+ if (items.length === 0) return null;
6368
+ return /* @__PURE__ */ jsxs31(
6369
+ View41,
6370
+ {
6371
+ style: {
6372
+ borderWidth: 1,
6373
+ borderColor: theme.colors.border,
6374
+ borderRadius: theme.radii.lg,
6375
+ marginHorizontal: theme.spacing.md,
6376
+ padding: theme.spacing.md,
6377
+ backgroundColor: "transparent"
6378
+ },
6379
+ children: [
6380
+ /* @__PURE__ */ jsx53(Text, { variant: "caption", style: { marginBottom: theme.spacing.sm }, children: "Queue" }),
6381
+ /* @__PURE__ */ jsx53(View41, { style: { gap: theme.spacing.sm }, children: items.map((item) => {
6382
+ const isExpanded = Boolean(expanded[item.id]);
6383
+ const showToggle = Boolean(canExpand[item.id]);
6384
+ const prompt = item.prompt ?? "";
6385
+ const moreLabel = "more";
6386
+ const displayPrompt = !isExpanded && showToggle && collapsedText[item.id] ? collapsedText[item.id] : prompt;
6387
+ const isRemoving = Boolean(removing[item.id]);
6388
+ return /* @__PURE__ */ jsxs31(
6389
+ View41,
6390
+ {
6391
+ style: {
6392
+ flexDirection: "row",
6393
+ alignItems: "flex-start",
6394
+ gap: theme.spacing.sm,
6395
+ paddingHorizontal: theme.spacing.md,
6396
+ paddingVertical: theme.spacing.sm,
6397
+ borderRadius: theme.radii.md,
6398
+ backgroundColor: withAlpha(theme.colors.surface, theme.scheme === "dark" ? 0.8 : 0.9)
6399
+ },
6400
+ children: [
6401
+ /* @__PURE__ */ jsxs31(View41, { style: { flex: 1 }, children: [
6402
+ !canExpand[item.id] ? /* @__PURE__ */ jsx53(
6403
+ Text,
6404
+ {
6405
+ style: { position: "absolute", opacity: 0, zIndex: -1, width: "100%" },
6406
+ onTextLayout: (e) => {
6407
+ var _a;
6408
+ const lines = (_a = e.nativeEvent) == null ? void 0 : _a.lines;
6409
+ if (!lines) return;
6410
+ if (lines.length > 2) {
6411
+ setCanExpand((prev) => ({ ...prev, [item.id]: true }));
6412
+ setCollapsedText((prev) => ({
6413
+ ...prev,
6414
+ [item.id]: buildCollapsedText(lines)
6415
+ }));
6416
+ }
6417
+ },
6418
+ children: prompt
6419
+ }
6420
+ ) : null,
6421
+ /* @__PURE__ */ jsxs31(
6422
+ Text,
6423
+ {
6424
+ variant: "bodyMuted",
6425
+ numberOfLines: isExpanded ? void 0 : 2,
6426
+ children: [
6427
+ displayPrompt,
6428
+ !isExpanded && showToggle ? /* @__PURE__ */ jsx53(
6429
+ Text,
6430
+ {
6431
+ color: theme.colors.text,
6432
+ onPress: () => setExpanded((prev) => ({ ...prev, [item.id]: true })),
6433
+ suppressHighlighting: true,
6434
+ children: moreLabel
6435
+ }
6436
+ ) : null
6437
+ ]
6438
+ }
6439
+ ),
6440
+ showToggle && isExpanded ? /* @__PURE__ */ jsx53(
6441
+ Pressable13,
6442
+ {
6443
+ onPress: () => setExpanded((prev) => ({ ...prev, [item.id]: false })),
6444
+ hitSlop: 6,
6445
+ style: { alignSelf: "flex-start", marginTop: 4 },
6446
+ children: /* @__PURE__ */ jsx53(Text, { variant: "captionMuted", color: theme.colors.text, children: "less" })
6447
+ }
6448
+ ) : null
6449
+ ] }),
6450
+ /* @__PURE__ */ jsx53(
6451
+ Pressable13,
6452
+ {
6453
+ onPress: () => {
6454
+ if (!onRemove || isRemoving) return;
6455
+ setRemoving((prev) => ({ ...prev, [item.id]: true }));
6456
+ Promise.resolve(onRemove(item.id)).finally(() => {
6457
+ setRemoving((prev) => {
6458
+ if (!prev[item.id]) return prev;
6459
+ const { [item.id]: _removed, ...rest } = prev;
6460
+ return rest;
6461
+ });
6462
+ });
6463
+ },
6464
+ hitSlop: 8,
6465
+ style: { alignSelf: "center" },
6466
+ children: isRemoving ? /* @__PURE__ */ jsx53(ActivityIndicator8, { size: "small", color: theme.colors.text }) : /* @__PURE__ */ jsx53(IconClose, { size: 14, colorToken: "text" })
6467
+ }
6468
+ )
6469
+ ]
6470
+ },
6471
+ item.id
6472
+ );
6473
+ }) })
6474
+ ]
6475
+ }
6476
+ );
6477
+ }
6478
+
6479
+ // src/studio/ui/ChatPanel.tsx
6480
+ import { jsx as jsx54, jsxs as jsxs32 } from "react/jsx-runtime";
6102
6481
  function ChatPanel({
6103
6482
  title = "Chat",
6104
6483
  autoFocusComposer = false,
@@ -6116,11 +6495,13 @@ function ChatPanel({
6116
6495
  onClose,
6117
6496
  onNavigateHome,
6118
6497
  onStartDraw,
6119
- onSend
6498
+ onSend,
6499
+ queueItems = [],
6500
+ onRemoveQueueItem
6120
6501
  }) {
6121
- const listRef = React39.useRef(null);
6122
- const [nearBottom, setNearBottom] = React39.useState(true);
6123
- const handleSend = React39.useCallback(
6502
+ const listRef = React40.useRef(null);
6503
+ const [nearBottom, setNearBottom] = React40.useState(true);
6504
+ const handleSend = React40.useCallback(
6124
6505
  async (text, composerAttachments) => {
6125
6506
  const all = composerAttachments ?? attachments;
6126
6507
  await onSend(text, all.length > 0 ? all : void 0);
@@ -6134,25 +6515,25 @@ function ChatPanel({
6134
6515
  },
6135
6516
  [attachments, nearBottom, onClearAttachments, onSend]
6136
6517
  );
6137
- const handleScrollToBottom = React39.useCallback(() => {
6518
+ const handleScrollToBottom = React40.useCallback(() => {
6138
6519
  var _a;
6139
6520
  (_a = listRef.current) == null ? void 0 : _a.scrollToBottom({ animated: true });
6140
6521
  }, []);
6141
- const header = /* @__PURE__ */ jsx53(
6522
+ const header = /* @__PURE__ */ jsx54(
6142
6523
  ChatHeader,
6143
6524
  {
6144
- left: /* @__PURE__ */ jsxs31(View41, { style: { flexDirection: "row", alignItems: "center" }, children: [
6145
- /* @__PURE__ */ jsx53(StudioSheetHeaderIconButton, { onPress: onBack, accessibilityLabel: "Back", style: { marginRight: 8 }, children: /* @__PURE__ */ jsx53(IconBack, { size: 20, colorToken: "floatingContent" }) }),
6146
- onNavigateHome ? /* @__PURE__ */ jsx53(StudioSheetHeaderIconButton, { onPress: onNavigateHome, accessibilityLabel: "Home", children: /* @__PURE__ */ jsx53(IconHome, { size: 20, colorToken: "floatingContent" }) }) : null
6525
+ left: /* @__PURE__ */ jsxs32(View42, { style: { flexDirection: "row", alignItems: "center" }, children: [
6526
+ /* @__PURE__ */ jsx54(StudioSheetHeaderIconButton, { onPress: onBack, accessibilityLabel: "Back", style: { marginRight: 8 }, children: /* @__PURE__ */ jsx54(IconBack, { size: 20, colorToken: "floatingContent" }) }),
6527
+ onNavigateHome ? /* @__PURE__ */ jsx54(StudioSheetHeaderIconButton, { onPress: onNavigateHome, accessibilityLabel: "Home", children: /* @__PURE__ */ jsx54(IconHome, { size: 20, colorToken: "floatingContent" }) }) : null
6147
6528
  ] }),
6148
- right: /* @__PURE__ */ jsxs31(View41, { style: { flexDirection: "row", alignItems: "center" }, children: [
6149
- onStartDraw ? /* @__PURE__ */ jsx53(StudioSheetHeaderIconButton, { onPress: onStartDraw, accessibilityLabel: "Draw", intent: "danger", style: { marginRight: 8 }, children: /* @__PURE__ */ jsx53(IconDraw, { size: 20, colorToken: "onDanger" }) }) : null,
6150
- /* @__PURE__ */ jsx53(StudioSheetHeaderIconButton, { onPress: onClose, accessibilityLabel: "Close", children: /* @__PURE__ */ jsx53(IconClose, { size: 20, colorToken: "floatingContent" }) })
6529
+ right: /* @__PURE__ */ jsxs32(View42, { style: { flexDirection: "row", alignItems: "center" }, children: [
6530
+ onStartDraw ? /* @__PURE__ */ jsx54(StudioSheetHeaderIconButton, { onPress: onStartDraw, accessibilityLabel: "Draw", intent: "danger", style: { marginRight: 8 }, children: /* @__PURE__ */ jsx54(IconDraw, { size: 20, colorToken: "onDanger" }) }) : null,
6531
+ /* @__PURE__ */ jsx54(StudioSheetHeaderIconButton, { onPress: onClose, accessibilityLabel: "Close", children: /* @__PURE__ */ jsx54(IconClose, { size: 20, colorToken: "floatingContent" }) })
6151
6532
  ] }),
6152
6533
  center: null
6153
6534
  }
6154
6535
  );
6155
- const topBanner = shouldForkOnEdit ? /* @__PURE__ */ jsx53(
6536
+ const topBanner = shouldForkOnEdit ? /* @__PURE__ */ jsx54(
6156
6537
  ForkNoticeBanner,
6157
6538
  {
6158
6539
  isOwner: !shouldForkOnEdit,
@@ -6161,33 +6542,35 @@ function ChatPanel({
6161
6542
  ) : null;
6162
6543
  const showMessagesLoading = Boolean(loading) && messages.length === 0 || forking;
6163
6544
  if (showMessagesLoading) {
6164
- return /* @__PURE__ */ jsxs31(View41, { style: { flex: 1 }, children: [
6165
- /* @__PURE__ */ jsx53(View41, { children: header }),
6166
- topBanner ? /* @__PURE__ */ jsx53(View41, { style: { paddingHorizontal: 16, paddingTop: 8 }, children: topBanner }) : null,
6167
- /* @__PURE__ */ jsxs31(View41, { style: { flex: 1, alignItems: "center", justifyContent: "center", paddingHorizontal: 24, paddingVertical: 12 }, children: [
6168
- /* @__PURE__ */ jsx53(ActivityIndicator8, {}),
6169
- /* @__PURE__ */ jsx53(View41, { style: { height: 12 } }),
6170
- /* @__PURE__ */ jsx53(Text, { variant: "bodyMuted", children: forking ? "Creating your copy\u2026" : "Loading messages\u2026" })
6545
+ return /* @__PURE__ */ jsxs32(View42, { style: { flex: 1 }, children: [
6546
+ /* @__PURE__ */ jsx54(View42, { children: header }),
6547
+ topBanner ? /* @__PURE__ */ jsx54(View42, { style: { paddingHorizontal: 16, paddingTop: 8 }, children: topBanner }) : null,
6548
+ /* @__PURE__ */ jsxs32(View42, { style: { flex: 1, alignItems: "center", justifyContent: "center", paddingHorizontal: 24, paddingVertical: 12 }, children: [
6549
+ /* @__PURE__ */ jsx54(ActivityIndicator9, {}),
6550
+ /* @__PURE__ */ jsx54(View42, { style: { height: 12 } }),
6551
+ /* @__PURE__ */ jsx54(Text, { variant: "bodyMuted", children: forking ? "Creating your copy\u2026" : "Loading messages\u2026" })
6171
6552
  ] })
6172
6553
  ] });
6173
6554
  }
6174
- return /* @__PURE__ */ jsx53(
6555
+ const queueTop = queueItems.length > 0 ? /* @__PURE__ */ jsx54(ChatQueue, { items: queueItems, onRemove: onRemoveQueueItem }) : null;
6556
+ return /* @__PURE__ */ jsx54(
6175
6557
  ChatPage,
6176
6558
  {
6177
6559
  header,
6178
6560
  messages,
6179
6561
  showTypingIndicator,
6180
6562
  topBanner,
6563
+ composerTop: queueTop,
6181
6564
  composerHorizontalPadding: 0,
6182
6565
  listRef,
6183
6566
  onNearBottomChange: setNearBottom,
6184
- overlay: /* @__PURE__ */ jsx53(
6567
+ overlay: /* @__PURE__ */ jsx54(
6185
6568
  ScrollToBottomButton,
6186
6569
  {
6187
6570
  visible: !nearBottom,
6188
6571
  onPress: handleScrollToBottom,
6189
6572
  style: { bottom: 80 },
6190
- children: /* @__PURE__ */ jsx53(IconArrowDown, { size: 20, colorToken: "floatingContent" })
6573
+ children: /* @__PURE__ */ jsx54(IconArrowDown, { size: 20, colorToken: "floatingContent" })
6191
6574
  }
6192
6575
  ),
6193
6576
  composer: {
@@ -6208,16 +6591,16 @@ function ChatPanel({
6208
6591
  }
6209
6592
 
6210
6593
  // src/components/dialogs/ConfirmMergeRequestDialog.tsx
6211
- import * as React40 from "react";
6212
- import { Pressable as Pressable14, View as View43 } from "react-native";
6594
+ import * as React41 from "react";
6595
+ import { Pressable as Pressable15, View as View44 } from "react-native";
6213
6596
 
6214
6597
  // src/components/primitives/Modal.tsx
6215
6598
  import {
6216
6599
  Modal as RNModal,
6217
- Pressable as Pressable13,
6218
- View as View42
6600
+ Pressable as Pressable14,
6601
+ View as View43
6219
6602
  } from "react-native";
6220
- import { jsx as jsx54, jsxs as jsxs32 } from "react/jsx-runtime";
6603
+ import { jsx as jsx55, jsxs as jsxs33 } from "react/jsx-runtime";
6221
6604
  function Modal({
6222
6605
  visible,
6223
6606
  onRequestClose,
@@ -6226,30 +6609,30 @@ function Modal({
6226
6609
  contentStyle
6227
6610
  }) {
6228
6611
  const theme = useTheme();
6229
- return /* @__PURE__ */ jsx54(
6612
+ return /* @__PURE__ */ jsx55(
6230
6613
  RNModal,
6231
6614
  {
6232
6615
  visible,
6233
6616
  transparent: true,
6234
6617
  animationType: "fade",
6235
6618
  onRequestClose,
6236
- children: /* @__PURE__ */ jsxs32(View42, { style: { flex: 1, backgroundColor: theme.colors.backdrop, justifyContent: "center", padding: theme.spacing.lg }, children: [
6237
- /* @__PURE__ */ jsx54(
6238
- Pressable13,
6619
+ children: /* @__PURE__ */ jsxs33(View43, { style: { flex: 1, backgroundColor: theme.colors.backdrop, justifyContent: "center", padding: theme.spacing.lg }, children: [
6620
+ /* @__PURE__ */ jsx55(
6621
+ Pressable14,
6239
6622
  {
6240
6623
  accessibilityRole: "button",
6241
6624
  onPress: dismissOnBackdropPress ? onRequestClose : void 0,
6242
6625
  style: { position: "absolute", inset: 0 }
6243
6626
  }
6244
6627
  ),
6245
- /* @__PURE__ */ jsx54(Card, { variant: "surfaceRaised", padded: true, style: [{ borderRadius: theme.radii.xl }, contentStyle], children })
6628
+ /* @__PURE__ */ jsx55(Card, { variant: "surfaceRaised", padded: true, style: [{ borderRadius: theme.radii.xl }, contentStyle], children })
6246
6629
  ] })
6247
6630
  }
6248
6631
  );
6249
6632
  }
6250
6633
 
6251
6634
  // src/components/dialogs/ConfirmMergeRequestDialog.tsx
6252
- import { jsx as jsx55, jsxs as jsxs33 } from "react/jsx-runtime";
6635
+ import { jsx as jsx56, jsxs as jsxs34 } from "react/jsx-runtime";
6253
6636
  function ConfirmMergeRequestDialog({
6254
6637
  visible,
6255
6638
  onOpenChange,
@@ -6260,14 +6643,14 @@ function ConfirmMergeRequestDialog({
6260
6643
  onTestFirst
6261
6644
  }) {
6262
6645
  const theme = useTheme();
6263
- const close = React40.useCallback(() => onOpenChange(false), [onOpenChange]);
6646
+ const close = React41.useCallback(() => onOpenChange(false), [onOpenChange]);
6264
6647
  const canConfirm = Boolean(mergeRequest) && !approveDisabled;
6265
- const handleConfirm = React40.useCallback(() => {
6648
+ const handleConfirm = React41.useCallback(() => {
6266
6649
  if (!mergeRequest) return;
6267
6650
  onOpenChange(false);
6268
6651
  void onConfirm();
6269
6652
  }, [mergeRequest, onConfirm, onOpenChange]);
6270
- const handleTestFirst = React40.useCallback(() => {
6653
+ const handleTestFirst = React41.useCallback(() => {
6271
6654
  if (!mergeRequest) return;
6272
6655
  onOpenChange(false);
6273
6656
  void onTestFirst(mergeRequest);
@@ -6279,7 +6662,7 @@ function ConfirmMergeRequestDialog({
6279
6662
  justifyContent: "center",
6280
6663
  alignSelf: "stretch"
6281
6664
  };
6282
- return /* @__PURE__ */ jsxs33(
6665
+ return /* @__PURE__ */ jsxs34(
6283
6666
  Modal,
6284
6667
  {
6285
6668
  visible,
@@ -6290,7 +6673,7 @@ function ConfirmMergeRequestDialog({
6290
6673
  backgroundColor: theme.colors.background
6291
6674
  },
6292
6675
  children: [
6293
- /* @__PURE__ */ jsx55(View43, { children: /* @__PURE__ */ jsx55(
6676
+ /* @__PURE__ */ jsx56(View44, { children: /* @__PURE__ */ jsx56(
6294
6677
  Text,
6295
6678
  {
6296
6679
  style: {
@@ -6302,9 +6685,9 @@ function ConfirmMergeRequestDialog({
6302
6685
  children: "Are you sure you want to approve this merge request?"
6303
6686
  }
6304
6687
  ) }),
6305
- /* @__PURE__ */ jsxs33(View43, { style: { marginTop: 16 }, children: [
6306
- /* @__PURE__ */ jsx55(
6307
- View43,
6688
+ /* @__PURE__ */ jsxs34(View44, { style: { marginTop: 16 }, children: [
6689
+ /* @__PURE__ */ jsx56(
6690
+ View44,
6308
6691
  {
6309
6692
  style: [
6310
6693
  fullWidthButtonBase,
@@ -6313,22 +6696,22 @@ function ConfirmMergeRequestDialog({
6313
6696
  opacity: canConfirm ? 1 : 0.5
6314
6697
  }
6315
6698
  ],
6316
- children: /* @__PURE__ */ jsx55(
6317
- Pressable14,
6699
+ children: /* @__PURE__ */ jsx56(
6700
+ Pressable15,
6318
6701
  {
6319
6702
  accessibilityRole: "button",
6320
6703
  accessibilityLabel: "Approve Merge",
6321
6704
  disabled: !canConfirm,
6322
6705
  onPress: handleConfirm,
6323
6706
  style: [fullWidthButtonBase, { flex: 1 }],
6324
- children: /* @__PURE__ */ jsx55(Text, { style: { textAlign: "center", color: theme.colors.onPrimary }, children: "Approve Merge" })
6707
+ children: /* @__PURE__ */ jsx56(Text, { style: { textAlign: "center", color: theme.colors.onPrimary }, children: "Approve Merge" })
6325
6708
  }
6326
6709
  )
6327
6710
  }
6328
6711
  ),
6329
- /* @__PURE__ */ jsx55(View43, { style: { height: 8 } }),
6330
- /* @__PURE__ */ jsx55(
6331
- View43,
6712
+ /* @__PURE__ */ jsx56(View44, { style: { height: 8 } }),
6713
+ /* @__PURE__ */ jsx56(
6714
+ View44,
6332
6715
  {
6333
6716
  style: [
6334
6717
  fullWidthButtonBase,
@@ -6339,22 +6722,22 @@ function ConfirmMergeRequestDialog({
6339
6722
  opacity: isBuilding || !mergeRequest ? 0.5 : 1
6340
6723
  }
6341
6724
  ],
6342
- children: /* @__PURE__ */ jsx55(
6343
- Pressable14,
6725
+ children: /* @__PURE__ */ jsx56(
6726
+ Pressable15,
6344
6727
  {
6345
6728
  accessibilityRole: "button",
6346
6729
  accessibilityLabel: isBuilding ? "Preparing\u2026" : "Test edits first",
6347
6730
  disabled: isBuilding || !mergeRequest,
6348
6731
  onPress: handleTestFirst,
6349
6732
  style: [fullWidthButtonBase, { flex: 1 }],
6350
- children: /* @__PURE__ */ jsx55(Text, { style: { textAlign: "center", color: theme.colors.text }, children: isBuilding ? "Preparing\u2026" : "Test edits first" })
6733
+ children: /* @__PURE__ */ jsx56(Text, { style: { textAlign: "center", color: theme.colors.text }, children: isBuilding ? "Preparing\u2026" : "Test edits first" })
6351
6734
  }
6352
6735
  )
6353
6736
  }
6354
6737
  ),
6355
- /* @__PURE__ */ jsx55(View43, { style: { height: 8 } }),
6356
- /* @__PURE__ */ jsx55(
6357
- View43,
6738
+ /* @__PURE__ */ jsx56(View44, { style: { height: 8 } }),
6739
+ /* @__PURE__ */ jsx56(
6740
+ View44,
6358
6741
  {
6359
6742
  style: [
6360
6743
  fullWidthButtonBase,
@@ -6364,14 +6747,14 @@ function ConfirmMergeRequestDialog({
6364
6747
  borderColor: theme.colors.border
6365
6748
  }
6366
6749
  ],
6367
- children: /* @__PURE__ */ jsx55(
6368
- Pressable14,
6750
+ children: /* @__PURE__ */ jsx56(
6751
+ Pressable15,
6369
6752
  {
6370
6753
  accessibilityRole: "button",
6371
6754
  accessibilityLabel: "Cancel",
6372
6755
  onPress: close,
6373
6756
  style: [fullWidthButtonBase, { flex: 1 }],
6374
- children: /* @__PURE__ */ jsx55(Text, { style: { textAlign: "center", color: theme.colors.text }, children: "Cancel" })
6757
+ children: /* @__PURE__ */ jsx56(Text, { style: { textAlign: "center", color: theme.colors.text }, children: "Cancel" })
6375
6758
  }
6376
6759
  )
6377
6760
  }
@@ -6383,7 +6766,7 @@ function ConfirmMergeRequestDialog({
6383
6766
  }
6384
6767
 
6385
6768
  // src/studio/ui/ConfirmMergeFlow.tsx
6386
- import { jsx as jsx56 } from "react/jsx-runtime";
6769
+ import { jsx as jsx57 } from "react/jsx-runtime";
6387
6770
  function ConfirmMergeFlow({
6388
6771
  visible,
6389
6772
  onOpenChange,
@@ -6394,7 +6777,7 @@ function ConfirmMergeFlow({
6394
6777
  onConfirm,
6395
6778
  onTestFirst
6396
6779
  }) {
6397
- return /* @__PURE__ */ jsx56(
6780
+ return /* @__PURE__ */ jsx57(
6398
6781
  ConfirmMergeRequestDialog,
6399
6782
  {
6400
6783
  visible,
@@ -6416,11 +6799,11 @@ function ConfirmMergeFlow({
6416
6799
  }
6417
6800
 
6418
6801
  // src/studio/hooks/useOptimisticChatMessages.ts
6419
- import * as React41 from "react";
6802
+ import * as React42 from "react";
6420
6803
  function makeOptimisticId() {
6421
6804
  return `optimistic:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 10)}`;
6422
6805
  }
6423
- function toEpochMs(createdAt) {
6806
+ function toEpochMs2(createdAt) {
6424
6807
  if (createdAt == null) return 0;
6425
6808
  if (typeof createdAt === "number") return createdAt;
6426
6809
  if (createdAt instanceof Date) return createdAt.getTime();
@@ -6439,7 +6822,7 @@ function isOptimisticResolvedByServer(chatMessages, o) {
6439
6822
  for (const m of candidates) {
6440
6823
  if (m.author !== "human") continue;
6441
6824
  if (normalize(m.content) !== target) continue;
6442
- const serverMs = toEpochMs(m.createdAt);
6825
+ const serverMs = toEpochMs2(m.createdAt);
6443
6826
  const optimisticMs = Date.parse(o.createdAtIso);
6444
6827
  if (Number.isFinite(optimisticMs) && optimisticMs > 0 && serverMs > 0) {
6445
6828
  if (serverMs + 12e4 < optimisticMs) continue;
@@ -6451,14 +6834,15 @@ function isOptimisticResolvedByServer(chatMessages, o) {
6451
6834
  function useOptimisticChatMessages({
6452
6835
  threadId,
6453
6836
  shouldForkOnEdit,
6837
+ disableOptimistic = false,
6454
6838
  chatMessages,
6455
6839
  onSendChat
6456
6840
  }) {
6457
- const [optimisticChat, setOptimisticChat] = React41.useState([]);
6458
- React41.useEffect(() => {
6841
+ const [optimisticChat, setOptimisticChat] = React42.useState([]);
6842
+ React42.useEffect(() => {
6459
6843
  setOptimisticChat([]);
6460
6844
  }, [threadId]);
6461
- const messages = React41.useMemo(() => {
6845
+ const messages = React42.useMemo(() => {
6462
6846
  if (!optimisticChat || optimisticChat.length === 0) return chatMessages;
6463
6847
  const unresolved = optimisticChat.filter((o) => !isOptimisticResolvedByServer(chatMessages, o));
6464
6848
  if (unresolved.length === 0) return chatMessages;
@@ -6474,7 +6858,7 @@ function useOptimisticChatMessages({
6474
6858
  merged.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
6475
6859
  return merged;
6476
6860
  }, [chatMessages, optimisticChat]);
6477
- React41.useEffect(() => {
6861
+ React42.useEffect(() => {
6478
6862
  if (optimisticChat.length === 0) return;
6479
6863
  setOptimisticChat((prev) => {
6480
6864
  if (prev.length === 0) return prev;
@@ -6482,9 +6866,9 @@ function useOptimisticChatMessages({
6482
6866
  return next.length === prev.length ? prev : next;
6483
6867
  });
6484
6868
  }, [chatMessages, optimisticChat.length]);
6485
- const onSend = React41.useCallback(
6869
+ const onSend = React42.useCallback(
6486
6870
  async (text, attachments) => {
6487
- if (shouldForkOnEdit) {
6871
+ if (shouldForkOnEdit || disableOptimistic) {
6488
6872
  await onSendChat(text, attachments);
6489
6873
  return;
6490
6874
  }
@@ -6496,7 +6880,7 @@ function useOptimisticChatMessages({
6496
6880
  setOptimisticChat((prev) => prev.map((m) => m.id === id ? { ...m, failed: true } : m));
6497
6881
  });
6498
6882
  },
6499
- [chatMessages, onSendChat, shouldForkOnEdit]
6883
+ [chatMessages, disableOptimistic, onSendChat, shouldForkOnEdit]
6500
6884
  );
6501
6885
  return { messages, onSend };
6502
6886
  }
@@ -6506,7 +6890,7 @@ import {
6506
6890
  publishComergeStudioUIState,
6507
6891
  startStudioControlPolling
6508
6892
  } from "@comergehq/studio-control";
6509
- import { Fragment as Fragment6, jsx as jsx57, jsxs as jsxs34 } from "react/jsx-runtime";
6893
+ import { Fragment as Fragment6, jsx as jsx58, jsxs as jsxs35 } from "react/jsx-runtime";
6510
6894
  function StudioOverlay({
6511
6895
  captureTargetRef,
6512
6896
  app,
@@ -6533,44 +6917,48 @@ function StudioOverlay({
6533
6917
  chatSending,
6534
6918
  chatShowTypingIndicator,
6535
6919
  onSendChat,
6920
+ chatQueueItems,
6921
+ onRemoveQueueItem,
6536
6922
  onNavigateHome,
6537
6923
  showBubble,
6538
6924
  studioControlOptions
6539
6925
  }) {
6540
6926
  const theme = useTheme();
6541
6927
  const { width } = useWindowDimensions4();
6542
- const [sheetOpen, setSheetOpen] = React42.useState(false);
6543
- const sheetOpenRef = React42.useRef(sheetOpen);
6544
- const [activePage, setActivePage] = React42.useState("preview");
6545
- const [drawing, setDrawing] = React42.useState(false);
6546
- const [chatAttachments, setChatAttachments] = React42.useState([]);
6547
- const [commentsAppId, setCommentsAppId] = React42.useState(null);
6548
- const [commentsCount, setCommentsCount] = React42.useState(null);
6928
+ const [sheetOpen, setSheetOpen] = React43.useState(false);
6929
+ const sheetOpenRef = React43.useRef(sheetOpen);
6930
+ const [activePage, setActivePage] = React43.useState("preview");
6931
+ const [drawing, setDrawing] = React43.useState(false);
6932
+ const [chatAttachments, setChatAttachments] = React43.useState([]);
6933
+ const [commentsAppId, setCommentsAppId] = React43.useState(null);
6934
+ const [commentsCount, setCommentsCount] = React43.useState(null);
6549
6935
  const threadId = (app == null ? void 0 : app.threadId) ?? null;
6936
+ const disableOptimistic = Boolean(chatQueueItems && chatQueueItems.length > 0) || (app == null ? void 0 : app.status) === "editing";
6550
6937
  const optimistic = useOptimisticChatMessages({
6551
6938
  threadId,
6552
6939
  shouldForkOnEdit,
6940
+ disableOptimistic,
6553
6941
  chatMessages,
6554
6942
  onSendChat
6555
6943
  });
6556
- const [confirmMrId, setConfirmMrId] = React42.useState(null);
6557
- const confirmMr = React42.useMemo(
6944
+ const [confirmMrId, setConfirmMrId] = React43.useState(null);
6945
+ const confirmMr = React43.useMemo(
6558
6946
  () => confirmMrId ? incomingMergeRequests.find((m) => m.id === confirmMrId) ?? null : null,
6559
6947
  [confirmMrId, incomingMergeRequests]
6560
6948
  );
6561
- const handleSheetOpenChange = React42.useCallback((open) => {
6949
+ const handleSheetOpenChange = React43.useCallback((open) => {
6562
6950
  setSheetOpen(open);
6563
6951
  if (!open) Keyboard5.dismiss();
6564
6952
  }, []);
6565
- const closeSheet = React42.useCallback(() => {
6953
+ const closeSheet = React43.useCallback(() => {
6566
6954
  handleSheetOpenChange(false);
6567
6955
  }, [handleSheetOpenChange]);
6568
- const openSheet = React42.useCallback(() => setSheetOpen(true), []);
6569
- const goToChat = React42.useCallback(() => {
6956
+ const openSheet = React43.useCallback(() => setSheetOpen(true), []);
6957
+ const goToChat = React43.useCallback(() => {
6570
6958
  setActivePage("chat");
6571
6959
  openSheet();
6572
6960
  }, [openSheet]);
6573
- const backToPreview = React42.useCallback(() => {
6961
+ const backToPreview = React43.useCallback(() => {
6574
6962
  if (Platform10.OS !== "ios") {
6575
6963
  Keyboard5.dismiss();
6576
6964
  setActivePage("preview");
@@ -6588,11 +6976,11 @@ function StudioOverlay({
6588
6976
  const t = setTimeout(finalize, 350);
6589
6977
  Keyboard5.dismiss();
6590
6978
  }, []);
6591
- const startDraw = React42.useCallback(() => {
6979
+ const startDraw = React43.useCallback(() => {
6592
6980
  setDrawing(true);
6593
6981
  closeSheet();
6594
6982
  }, [closeSheet]);
6595
- const handleDrawCapture = React42.useCallback(
6983
+ const handleDrawCapture = React43.useCallback(
6596
6984
  (dataUrl) => {
6597
6985
  setChatAttachments((prev) => [...prev, dataUrl]);
6598
6986
  setDrawing(false);
@@ -6601,7 +6989,7 @@ function StudioOverlay({
6601
6989
  },
6602
6990
  [openSheet]
6603
6991
  );
6604
- const toggleSheet = React42.useCallback(async () => {
6992
+ const toggleSheet = React43.useCallback(async () => {
6605
6993
  if (!sheetOpen) {
6606
6994
  const shouldExitTest = Boolean(testingMrId) || isTesting;
6607
6995
  if (shouldExitTest) {
@@ -6613,7 +7001,7 @@ function StudioOverlay({
6613
7001
  closeSheet();
6614
7002
  }
6615
7003
  }, [closeSheet, isTesting, onRestoreBase, sheetOpen, testingMrId]);
6616
- const handleTestMr = React42.useCallback(
7004
+ const handleTestMr = React43.useCallback(
6617
7005
  async (mr) => {
6618
7006
  if (!onTestMr) return;
6619
7007
  await onTestMr(mr);
@@ -6621,10 +7009,10 @@ function StudioOverlay({
6621
7009
  },
6622
7010
  [closeSheet, onTestMr]
6623
7011
  );
6624
- React42.useEffect(() => {
7012
+ React43.useEffect(() => {
6625
7013
  sheetOpenRef.current = sheetOpen;
6626
7014
  }, [sheetOpen]);
6627
- React42.useEffect(() => {
7015
+ React43.useEffect(() => {
6628
7016
  const poller = startStudioControlPolling((action) => {
6629
7017
  if (action === "show" && !sheetOpenRef.current) openSheet();
6630
7018
  if (action === "hide" && sheetOpenRef.current) closeSheet();
@@ -6632,17 +7020,17 @@ function StudioOverlay({
6632
7020
  }, studioControlOptions);
6633
7021
  return () => poller.stop();
6634
7022
  }, [closeSheet, openSheet, studioControlOptions, toggleSheet]);
6635
- React42.useEffect(() => {
7023
+ React43.useEffect(() => {
6636
7024
  void publishComergeStudioUIState(sheetOpen, studioControlOptions);
6637
7025
  }, [sheetOpen, studioControlOptions]);
6638
- return /* @__PURE__ */ jsxs34(Fragment6, { children: [
6639
- /* @__PURE__ */ jsx57(EdgeGlowFrame, { visible: isTesting, role: "accent", thickness: 40, intensity: 1 }),
6640
- /* @__PURE__ */ jsx57(StudioBottomSheet, { open: sheetOpen, onOpenChange: handleSheetOpenChange, children: /* @__PURE__ */ jsx57(
7026
+ return /* @__PURE__ */ jsxs35(Fragment6, { children: [
7027
+ /* @__PURE__ */ jsx58(EdgeGlowFrame, { visible: isTesting, role: "accent", thickness: 40, intensity: 1 }),
7028
+ /* @__PURE__ */ jsx58(StudioBottomSheet, { open: sheetOpen, onOpenChange: handleSheetOpenChange, children: /* @__PURE__ */ jsx58(
6641
7029
  StudioSheetPager,
6642
7030
  {
6643
7031
  activePage,
6644
7032
  width,
6645
- preview: /* @__PURE__ */ jsx57(
7033
+ preview: /* @__PURE__ */ jsx58(
6646
7034
  PreviewPanel,
6647
7035
  {
6648
7036
  app,
@@ -6668,7 +7056,7 @@ function StudioOverlay({
6668
7056
  commentCountOverride: commentsCount ?? void 0
6669
7057
  }
6670
7058
  ),
6671
- chat: /* @__PURE__ */ jsx57(
7059
+ chat: /* @__PURE__ */ jsx58(
6672
7060
  ChatPanel,
6673
7061
  {
6674
7062
  messages: optimistic.messages,
@@ -6686,12 +7074,14 @@ function StudioOverlay({
6686
7074
  onClose: closeSheet,
6687
7075
  onNavigateHome,
6688
7076
  onStartDraw: startDraw,
6689
- onSend: optimistic.onSend
7077
+ onSend: optimistic.onSend,
7078
+ queueItems: chatQueueItems,
7079
+ onRemoveQueueItem
6690
7080
  }
6691
7081
  )
6692
7082
  }
6693
7083
  ) }),
6694
- showBubble && /* @__PURE__ */ jsx57(
7084
+ showBubble && /* @__PURE__ */ jsx58(
6695
7085
  Bubble,
6696
7086
  {
6697
7087
  visible: !sheetOpen && !drawing,
@@ -6699,10 +7089,10 @@ function StudioOverlay({
6699
7089
  badgeCount: incomingMergeRequests.length,
6700
7090
  onPress: toggleSheet,
6701
7091
  isLoading: (app == null ? void 0 : app.status) === "editing",
6702
- children: /* @__PURE__ */ jsx57(View44, { style: { width: 28, height: 28, alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsx57(MergeIcon, { width: 24, height: 24, color: theme.colors.floatingContent }) })
7092
+ children: /* @__PURE__ */ jsx58(View45, { style: { width: 28, height: 28, alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsx58(MergeIcon, { width: 24, height: 24, color: theme.colors.floatingContent }) })
6703
7093
  }
6704
7094
  ),
6705
- /* @__PURE__ */ jsx57(
7095
+ /* @__PURE__ */ jsx58(
6706
7096
  DrawModeOverlay,
6707
7097
  {
6708
7098
  visible: drawing,
@@ -6711,7 +7101,7 @@ function StudioOverlay({
6711
7101
  onCapture: handleDrawCapture
6712
7102
  }
6713
7103
  ),
6714
- /* @__PURE__ */ jsx57(
7104
+ /* @__PURE__ */ jsx58(
6715
7105
  ConfirmMergeFlow,
6716
7106
  {
6717
7107
  visible: Boolean(confirmMr),
@@ -6724,7 +7114,7 @@ function StudioOverlay({
6724
7114
  onTestFirst: handleTestMr
6725
7115
  }
6726
7116
  ),
6727
- /* @__PURE__ */ jsx57(
7117
+ /* @__PURE__ */ jsx58(
6728
7118
  AppCommentsSheet,
6729
7119
  {
6730
7120
  appId: commentsAppId,
@@ -6736,8 +7126,190 @@ function StudioOverlay({
6736
7126
  ] });
6737
7127
  }
6738
7128
 
7129
+ // src/studio/hooks/useEditQueue.ts
7130
+ import * as React44 from "react";
7131
+
7132
+ // src/data/apps/edit-queue/remote.ts
7133
+ var EditQueueRemoteDataSourceImpl = class extends BaseRemote {
7134
+ async list(appId) {
7135
+ const { data } = await api.get(
7136
+ `/v1/apps/${encodeURIComponent(appId)}/edit-queue`
7137
+ );
7138
+ return data;
7139
+ }
7140
+ async update(appId, queueItemId, payload) {
7141
+ const { data } = await api.patch(
7142
+ `/v1/apps/${encodeURIComponent(appId)}/edit-queue/${encodeURIComponent(queueItemId)}`,
7143
+ payload
7144
+ );
7145
+ return data;
7146
+ }
7147
+ async cancel(appId, queueItemId) {
7148
+ const { data } = await api.delete(
7149
+ `/v1/apps/${encodeURIComponent(appId)}/edit-queue/${encodeURIComponent(queueItemId)}`
7150
+ );
7151
+ return data;
7152
+ }
7153
+ };
7154
+ var editQueueRemoteDataSource = new EditQueueRemoteDataSourceImpl();
7155
+
7156
+ // src/data/apps/edit-queue/repository.ts
7157
+ var ACTIVE_STATUSES = ["pending"];
7158
+ function toString(value) {
7159
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
7160
+ }
7161
+ function toAttachments(value) {
7162
+ return Array.isArray(value) ? value : [];
7163
+ }
7164
+ function mapQueueItem(row) {
7165
+ const payload = row.payload ?? {};
7166
+ return {
7167
+ id: row.id,
7168
+ status: row.status,
7169
+ prompt: toString(payload.trimmedPrompt),
7170
+ messageId: toString(payload.messageId),
7171
+ attachments: toAttachments(payload.attachments),
7172
+ createdAt: row.created_at,
7173
+ updatedAt: row.updated_at,
7174
+ runAfter: row.run_after,
7175
+ priority: row.priority
7176
+ };
7177
+ }
7178
+ var EditQueueRepositoryImpl = class extends BaseRepository {
7179
+ constructor(remote) {
7180
+ super();
7181
+ this.remote = remote;
7182
+ }
7183
+ async list(appId) {
7184
+ const res = await this.remote.list(appId);
7185
+ const data = this.unwrapOrThrow(res);
7186
+ return data.items ?? [];
7187
+ }
7188
+ async update(appId, queueItemId, payload) {
7189
+ const res = await this.remote.update(appId, queueItemId, payload);
7190
+ return this.unwrapOrThrow(res);
7191
+ }
7192
+ async cancel(appId, queueItemId) {
7193
+ const res = await this.remote.cancel(appId, queueItemId);
7194
+ return this.unwrapOrThrow(res);
7195
+ }
7196
+ subscribeEditQueue(appId, handlers) {
7197
+ const supabase = getSupabaseClient();
7198
+ const channel = supabase.channel(`edit-queue:app:${appId}`).on(
7199
+ "postgres_changes",
7200
+ { event: "INSERT", schema: "public", table: "app_job_queue", filter: `app_id=eq.${appId}` },
7201
+ (payload) => {
7202
+ var _a;
7203
+ const row = payload.new;
7204
+ if (row.kind !== "edit") return;
7205
+ const item = mapQueueItem(row);
7206
+ if (!ACTIVE_STATUSES.includes(item.status)) return;
7207
+ (_a = handlers.onInsert) == null ? void 0 : _a.call(handlers, item);
7208
+ }
7209
+ ).on(
7210
+ "postgres_changes",
7211
+ { event: "UPDATE", schema: "public", table: "app_job_queue", filter: `app_id=eq.${appId}` },
7212
+ (payload) => {
7213
+ var _a, _b;
7214
+ const row = payload.new;
7215
+ if (row.kind !== "edit") return;
7216
+ const item = mapQueueItem(row);
7217
+ if (ACTIVE_STATUSES.includes(item.status)) (_a = handlers.onUpdate) == null ? void 0 : _a.call(handlers, item);
7218
+ else (_b = handlers.onDelete) == null ? void 0 : _b.call(handlers, item);
7219
+ }
7220
+ ).on(
7221
+ "postgres_changes",
7222
+ { event: "DELETE", schema: "public", table: "app_job_queue", filter: `app_id=eq.${appId}` },
7223
+ (payload) => {
7224
+ var _a;
7225
+ const row = payload.old;
7226
+ if (row.kind !== "edit") return;
7227
+ (_a = handlers.onDelete) == null ? void 0 : _a.call(handlers, mapQueueItem(row));
7228
+ }
7229
+ ).subscribe();
7230
+ return () => {
7231
+ supabase.removeChannel(channel);
7232
+ };
7233
+ }
7234
+ };
7235
+ var editQueueRepository = new EditQueueRepositoryImpl(
7236
+ editQueueRemoteDataSource
7237
+ );
7238
+
7239
+ // src/studio/hooks/useEditQueue.ts
7240
+ function useEditQueue(appId) {
7241
+ const [items, setItems] = React44.useState([]);
7242
+ const [loading, setLoading] = React44.useState(false);
7243
+ const [error, setError] = React44.useState(null);
7244
+ const activeRequestIdRef = React44.useRef(0);
7245
+ const foregroundSignal = useForegroundSignal(Boolean(appId));
7246
+ const upsertSorted = React44.useCallback((prev, nextItem) => {
7247
+ const next = prev.some((x) => x.id === nextItem.id) ? prev.map((x) => x.id === nextItem.id ? nextItem : x) : [...prev, nextItem];
7248
+ next.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
7249
+ return next;
7250
+ }, []);
7251
+ const refetch = React44.useCallback(async () => {
7252
+ if (!appId) {
7253
+ setItems([]);
7254
+ return;
7255
+ }
7256
+ const requestId = ++activeRequestIdRef.current;
7257
+ setLoading(true);
7258
+ setError(null);
7259
+ try {
7260
+ const list = await editQueueRepository.list(appId);
7261
+ if (activeRequestIdRef.current !== requestId) return;
7262
+ setItems([...list].sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt))));
7263
+ } catch (e) {
7264
+ if (activeRequestIdRef.current !== requestId) return;
7265
+ setError(e instanceof Error ? e : new Error(String(e)));
7266
+ setItems([]);
7267
+ } finally {
7268
+ if (activeRequestIdRef.current === requestId) setLoading(false);
7269
+ }
7270
+ }, [appId]);
7271
+ React44.useEffect(() => {
7272
+ void refetch();
7273
+ }, [refetch]);
7274
+ React44.useEffect(() => {
7275
+ if (!appId) return;
7276
+ const unsubscribe = editQueueRepository.subscribeEditQueue(appId, {
7277
+ onInsert: (item) => setItems((prev) => upsertSorted(prev, item)),
7278
+ onUpdate: (item) => setItems((prev) => upsertSorted(prev, item)),
7279
+ onDelete: (item) => setItems((prev) => prev.filter((x) => x.id !== item.id))
7280
+ });
7281
+ return unsubscribe;
7282
+ }, [appId, upsertSorted, foregroundSignal]);
7283
+ React44.useEffect(() => {
7284
+ if (!appId) return;
7285
+ if (foregroundSignal <= 0) return;
7286
+ void refetch();
7287
+ }, [appId, foregroundSignal, refetch]);
7288
+ return { items, loading, error, refetch };
7289
+ }
7290
+
7291
+ // src/studio/hooks/useEditQueueActions.ts
7292
+ import * as React45 from "react";
7293
+ function useEditQueueActions(appId) {
7294
+ const update = React45.useCallback(
7295
+ async (queueItemId, payload) => {
7296
+ if (!appId) return;
7297
+ await editQueueRepository.update(appId, queueItemId, payload);
7298
+ },
7299
+ [appId]
7300
+ );
7301
+ const cancel = React45.useCallback(
7302
+ async (queueItemId) => {
7303
+ if (!appId) return;
7304
+ await editQueueRepository.cancel(appId, queueItemId);
7305
+ },
7306
+ [appId]
7307
+ );
7308
+ return { update, cancel };
7309
+ }
7310
+
6739
7311
  // src/studio/ComergeStudio.tsx
6740
- import { jsx as jsx58, jsxs as jsxs35 } from "react/jsx-runtime";
7312
+ import { jsx as jsx59, jsxs as jsxs36 } from "react/jsx-runtime";
6741
7313
  function ComergeStudio({
6742
7314
  appId,
6743
7315
  clientKey: clientKey2,
@@ -6748,17 +7320,17 @@ function ComergeStudio({
6748
7320
  studioControlOptions,
6749
7321
  embeddedBaseBundles
6750
7322
  }) {
6751
- const [activeAppId, setActiveAppId] = React43.useState(appId);
6752
- const [runtimeAppId, setRuntimeAppId] = React43.useState(appId);
6753
- const [pendingRuntimeTargetAppId, setPendingRuntimeTargetAppId] = React43.useState(null);
6754
- const platform = React43.useMemo(() => RNPlatform.OS === "ios" ? "ios" : "android", []);
6755
- React43.useEffect(() => {
7323
+ const [activeAppId, setActiveAppId] = React46.useState(appId);
7324
+ const [runtimeAppId, setRuntimeAppId] = React46.useState(appId);
7325
+ const [pendingRuntimeTargetAppId, setPendingRuntimeTargetAppId] = React46.useState(null);
7326
+ const platform = React46.useMemo(() => RNPlatform.OS === "ios" ? "ios" : "android", []);
7327
+ React46.useEffect(() => {
6756
7328
  setActiveAppId(appId);
6757
7329
  setRuntimeAppId(appId);
6758
7330
  setPendingRuntimeTargetAppId(null);
6759
7331
  }, [appId]);
6760
- const captureTargetRef = React43.useRef(null);
6761
- return /* @__PURE__ */ jsx58(StudioBootstrap, { clientKey: clientKey2, fallback: /* @__PURE__ */ jsx58(View45, { style: { flex: 1 } }), children: ({ userId }) => /* @__PURE__ */ jsx58(BottomSheetModalProvider, { children: /* @__PURE__ */ jsx58(LiquidGlassResetProvider, { resetTriggers: [appId, activeAppId, runtimeAppId], children: /* @__PURE__ */ jsx58(
7332
+ const captureTargetRef = React46.useRef(null);
7333
+ return /* @__PURE__ */ jsx59(StudioBootstrap, { clientKey: clientKey2, fallback: /* @__PURE__ */ jsx59(View46, { style: { flex: 1 } }), children: ({ userId }) => /* @__PURE__ */ jsx59(BottomSheetModalProvider, { children: /* @__PURE__ */ jsx59(LiquidGlassResetProvider, { resetTriggers: [appId, activeAppId, runtimeAppId], children: /* @__PURE__ */ jsx59(
6762
7334
  ComergeStudioInner,
6763
7335
  {
6764
7336
  userId,
@@ -6799,11 +7371,11 @@ function ComergeStudioInner({
6799
7371
  const { app, loading: appLoading } = useApp(activeAppId);
6800
7372
  const { app: runtimeAppFromHook } = useApp(runtimeAppId, { enabled: runtimeAppId !== activeAppId });
6801
7373
  const runtimeApp = runtimeAppId === activeAppId ? app : runtimeAppFromHook;
6802
- const sawEditingOnPendingTargetRef = React43.useRef(false);
6803
- React43.useEffect(() => {
7374
+ const sawEditingOnPendingTargetRef = React46.useRef(false);
7375
+ React46.useEffect(() => {
6804
7376
  sawEditingOnPendingTargetRef.current = false;
6805
7377
  }, [pendingRuntimeTargetAppId]);
6806
- React43.useEffect(() => {
7378
+ React46.useEffect(() => {
6807
7379
  if (!pendingRuntimeTargetAppId) return;
6808
7380
  if (activeAppId !== pendingRuntimeTargetAppId) return;
6809
7381
  if ((app == null ? void 0 : app.status) === "editing") {
@@ -6821,13 +7393,13 @@ function ComergeStudioInner({
6821
7393
  canRequestLatest: (runtimeApp == null ? void 0 : runtimeApp.status) === "ready",
6822
7394
  embeddedBaseBundles
6823
7395
  });
6824
- const sawEditingOnActiveAppRef = React43.useRef(false);
6825
- const [showPostEditPreparing, setShowPostEditPreparing] = React43.useState(false);
6826
- React43.useEffect(() => {
7396
+ const sawEditingOnActiveAppRef = React46.useRef(false);
7397
+ const [showPostEditPreparing, setShowPostEditPreparing] = React46.useState(false);
7398
+ React46.useEffect(() => {
6827
7399
  sawEditingOnActiveAppRef.current = false;
6828
7400
  setShowPostEditPreparing(false);
6829
7401
  }, [activeAppId]);
6830
- React43.useEffect(() => {
7402
+ React46.useEffect(() => {
6831
7403
  if (!(app == null ? void 0 : app.id)) return;
6832
7404
  if (app.status === "editing") {
6833
7405
  sawEditingOnActiveAppRef.current = true;
@@ -6839,7 +7411,7 @@ function ComergeStudioInner({
6839
7411
  sawEditingOnActiveAppRef.current = false;
6840
7412
  }
6841
7413
  }, [app == null ? void 0 : app.id, app == null ? void 0 : app.status]);
6842
- React43.useEffect(() => {
7414
+ React46.useEffect(() => {
6843
7415
  if (!showPostEditPreparing) return;
6844
7416
  const stillProcessingBaseBundle = bundle.loading && bundle.loadingMode === "base" && !bundle.isTesting;
6845
7417
  if (!stillProcessingBaseBundle) {
@@ -6848,15 +7420,27 @@ function ComergeStudioInner({
6848
7420
  }, [showPostEditPreparing, bundle.loading, bundle.loadingMode, bundle.isTesting]);
6849
7421
  const threadId = (app == null ? void 0 : app.threadId) ?? "";
6850
7422
  const thread = useThreadMessages(threadId);
7423
+ const editQueue = useEditQueue(activeAppId);
7424
+ const editQueueActions = useEditQueueActions(activeAppId);
7425
+ const [lastEditQueueInfo, setLastEditQueueInfo] = React46.useState(null);
7426
+ const lastEditQueueInfoRef = React46.useRef(null);
7427
+ const [suppressQueueUntilResponse, setSuppressQueueUntilResponse] = React46.useState(false);
6851
7428
  const mergeRequests = useMergeRequests({ appId: activeAppId });
6852
- const hasOpenOutgoingMr = React43.useMemo(() => {
7429
+ const hasOpenOutgoingMr = React46.useMemo(() => {
6853
7430
  return mergeRequests.lists.outgoing.some((mr) => mr.status === "open");
6854
7431
  }, [mergeRequests.lists.outgoing]);
6855
- const incomingReviewMrs = React43.useMemo(() => {
7432
+ const incomingReviewMrs = React46.useMemo(() => {
6856
7433
  if (!userId) return mergeRequests.lists.incoming;
6857
7434
  return mergeRequests.lists.incoming.filter((mr) => mr.createdBy !== userId);
6858
7435
  }, [mergeRequests.lists.incoming, userId]);
6859
7436
  const uploader = useAttachmentUpload();
7437
+ const updateLastEditQueueInfo = React46.useCallback(
7438
+ (info) => {
7439
+ lastEditQueueInfoRef.current = info;
7440
+ setLastEditQueueInfo(info);
7441
+ },
7442
+ []
7443
+ );
6860
7444
  const actions = useStudioActions({
6861
7445
  userId,
6862
7446
  app,
@@ -6871,20 +7455,62 @@ function ComergeStudioInner({
6871
7455
  setPendingRuntimeTargetAppId(null);
6872
7456
  }
6873
7457
  },
6874
- uploadAttachments: uploader.uploadBase64Images
7458
+ uploadAttachments: uploader.uploadBase64Images,
7459
+ onEditStart: () => {
7460
+ if (editQueue.items.length === 0) {
7461
+ setSuppressQueueUntilResponse(true);
7462
+ }
7463
+ },
7464
+ onEditQueued: (info) => {
7465
+ updateLastEditQueueInfo(info);
7466
+ if (info.queuePosition !== 1) {
7467
+ setSuppressQueueUntilResponse(false);
7468
+ }
7469
+ },
7470
+ onEditFinished: () => {
7471
+ var _a;
7472
+ if (((_a = lastEditQueueInfoRef.current) == null ? void 0 : _a.queuePosition) !== 1) {
7473
+ setSuppressQueueUntilResponse(false);
7474
+ }
7475
+ }
6875
7476
  });
6876
- const chatSendDisabled = hasNoOutcomeAfterLastHuman(thread.raw);
6877
- const [processingMrId, setProcessingMrId] = React43.useState(null);
6878
- const [testingMrId, setTestingMrId] = React43.useState(null);
6879
- const chatShowTypingIndicator = React43.useMemo(() => {
7477
+ const chatSendDisabled = false;
7478
+ const [processingMrId, setProcessingMrId] = React46.useState(null);
7479
+ const [testingMrId, setTestingMrId] = React46.useState(null);
7480
+ const chatShowTypingIndicator = React46.useMemo(() => {
6880
7481
  var _a;
6881
7482
  if (!thread.raw || thread.raw.length === 0) return false;
6882
7483
  const last = thread.raw[thread.raw.length - 1];
6883
7484
  const payloadType = typeof ((_a = last.payload) == null ? void 0 : _a.type) === "string" ? String(last.payload.type) : void 0;
6884
7485
  return payloadType !== "outcome";
6885
7486
  }, [thread.raw]);
6886
- return /* @__PURE__ */ jsx58(View45, { style: [{ flex: 1 }, style], children: /* @__PURE__ */ jsxs35(View45, { ref: captureTargetRef, style: { flex: 1 }, collapsable: false, children: [
6887
- /* @__PURE__ */ jsx58(
7487
+ React46.useEffect(() => {
7488
+ updateLastEditQueueInfo(null);
7489
+ setSuppressQueueUntilResponse(false);
7490
+ }, [activeAppId, updateLastEditQueueInfo]);
7491
+ React46.useEffect(() => {
7492
+ if (!(lastEditQueueInfo == null ? void 0 : lastEditQueueInfo.queueItemId)) return;
7493
+ const stillPresent = editQueue.items.some((item) => item.id === lastEditQueueInfo.queueItemId);
7494
+ if (!stillPresent) {
7495
+ updateLastEditQueueInfo(null);
7496
+ setSuppressQueueUntilResponse(false);
7497
+ }
7498
+ }, [editQueue.items, lastEditQueueInfo == null ? void 0 : lastEditQueueInfo.queueItemId]);
7499
+ const chatQueueItems = React46.useMemo(() => {
7500
+ var _a;
7501
+ if (suppressQueueUntilResponse && editQueue.items.length <= 1) {
7502
+ return [];
7503
+ }
7504
+ if (!lastEditQueueInfo || lastEditQueueInfo.queuePosition !== 1 || !lastEditQueueInfo.queueItemId) {
7505
+ return editQueue.items;
7506
+ }
7507
+ if (editQueue.items.length === 1 && ((_a = editQueue.items[0]) == null ? void 0 : _a.id) === lastEditQueueInfo.queueItemId) {
7508
+ return [];
7509
+ }
7510
+ return editQueue.items;
7511
+ }, [editQueue.items, lastEditQueueInfo, suppressQueueUntilResponse]);
7512
+ return /* @__PURE__ */ jsx59(View46, { style: [{ flex: 1 }, style], children: /* @__PURE__ */ jsxs36(View46, { ref: captureTargetRef, style: { flex: 1 }, collapsable: false, children: [
7513
+ /* @__PURE__ */ jsx59(
6888
7514
  RuntimeRenderer,
6889
7515
  {
6890
7516
  appKey,
@@ -6894,7 +7520,7 @@ function ComergeStudioInner({
6894
7520
  allowInitialPreparing: !embeddedBaseBundles
6895
7521
  }
6896
7522
  ),
6897
- /* @__PURE__ */ jsx58(
7523
+ /* @__PURE__ */ jsx59(
6898
7524
  StudioOverlay,
6899
7525
  {
6900
7526
  captureTargetRef,
@@ -6946,6 +7572,8 @@ function ComergeStudioInner({
6946
7572
  chatSending: actions.sending,
6947
7573
  chatShowTypingIndicator,
6948
7574
  onSendChat: (text, attachments) => actions.sendEdit({ prompt: text, attachments }),
7575
+ chatQueueItems,
7576
+ onRemoveQueueItem: (id) => editQueueActions.cancel(id),
6949
7577
  onNavigateHome,
6950
7578
  showBubble,
6951
7579
  studioControlOptions