@comergehq/studio 0.1.26 → 0.1.28

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 React47 from "react";
10
- import { Platform as RNPlatform, View as View46 } from "react-native";
9
+ import * as React49 from "react";
10
+ import { Platform as RNPlatform, View as View48 } from "react-native";
11
11
  import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
12
12
 
13
13
  // src/studio/bootstrap/StudioBootstrap.tsx
@@ -278,6 +278,109 @@ async function ensureAnonymousSession() {
278
278
  return { user: data.user, isNew: true };
279
279
  }
280
280
 
281
+ // src/studio/analytics/client.ts
282
+ import { Platform } from "react-native";
283
+ import { Mixpanel } from "mixpanel-react-native";
284
+
285
+ // src/core/logger.ts
286
+ import { logger, consoleTransport } from "react-native-logs";
287
+ var log = logger.createLogger(
288
+ {
289
+ levels: {
290
+ debug: 0,
291
+ info: 1,
292
+ warn: 2,
293
+ error: 3
294
+ },
295
+ severity: "debug",
296
+ transport: consoleTransport,
297
+ transportOptions: {
298
+ colors: {
299
+ info: "blueBright",
300
+ warn: "yellowBright",
301
+ error: "redBright"
302
+ }
303
+ },
304
+ async: true,
305
+ dateFormat: "time",
306
+ printLevel: true,
307
+ printDate: true,
308
+ fixedExtLvlLength: false,
309
+ enabled: true
310
+ }
311
+ );
312
+
313
+ // src/studio/analytics/client.ts
314
+ var studioMixpanel = null;
315
+ var studioAnalyticsEnabled = false;
316
+ var initPromise = null;
317
+ async function initStudioAnalytics(options) {
318
+ if (initPromise) return initPromise;
319
+ initPromise = (async () => {
320
+ if (!options.enabled) {
321
+ studioAnalyticsEnabled = false;
322
+ return;
323
+ }
324
+ const token = (options.token ?? "").trim();
325
+ if (!token) {
326
+ studioAnalyticsEnabled = false;
327
+ log.warn("[studio-analytics] disabled: missing Mixpanel token");
328
+ return;
329
+ }
330
+ try {
331
+ const trackAutomaticEvents = false;
332
+ const useNative = false;
333
+ const serverUrl = (options.serverUrl ?? "").trim() || "https://api.mixpanel.com";
334
+ const superProperties = {
335
+ runtime: "comerge-studio",
336
+ platform: Platform.OS
337
+ };
338
+ studioMixpanel = new Mixpanel(token, trackAutomaticEvents, useNative);
339
+ await studioMixpanel.init(false, superProperties, serverUrl);
340
+ studioMixpanel.setLoggingEnabled(Boolean(options.debug));
341
+ studioMixpanel.setFlushBatchSize(50);
342
+ studioAnalyticsEnabled = true;
343
+ } catch (error) {
344
+ studioMixpanel = null;
345
+ studioAnalyticsEnabled = false;
346
+ log.warn("[studio-analytics] init failed", error);
347
+ }
348
+ })();
349
+ return initPromise;
350
+ }
351
+ async function trackStudioEvent(eventName, properties) {
352
+ if (!studioAnalyticsEnabled || !studioMixpanel) return;
353
+ try {
354
+ await studioMixpanel.track(eventName, properties);
355
+ } catch (error) {
356
+ log.warn("[studio-analytics] track failed", { eventName, error });
357
+ }
358
+ }
359
+ async function flushStudioAnalytics() {
360
+ if (!studioAnalyticsEnabled || !studioMixpanel) return;
361
+ try {
362
+ await studioMixpanel.flush();
363
+ } catch (error) {
364
+ log.warn("[studio-analytics] flush failed", error);
365
+ }
366
+ }
367
+ async function identifyStudioUser(userId) {
368
+ if (!studioAnalyticsEnabled || !studioMixpanel || !userId) return;
369
+ try {
370
+ await studioMixpanel.identify(userId);
371
+ } catch (error) {
372
+ log.warn("[studio-analytics] identify failed", error);
373
+ }
374
+ }
375
+ async function resetStudioAnalytics() {
376
+ if (!studioAnalyticsEnabled || !studioMixpanel) return;
377
+ try {
378
+ await studioMixpanel.reset();
379
+ } catch (error) {
380
+ log.warn("[studio-analytics] reset failed", error);
381
+ }
382
+ }
383
+
281
384
  // src/studio/bootstrap/useStudioBootstrap.ts
282
385
  var SUPABASE_URL = "https://xtfxwbckjpfmqubnsusu.supabase.co";
283
386
  var SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh0Znh3YmNranBmbXF1Ym5zdXN1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjA2MDEyMzAsImV4cCI6MjA3NjE3NzIzMH0.dzWGAWrK4CvrmHVHzf8w7JlUZohdap0ZPnLZnABMV8s";
@@ -292,11 +395,23 @@ function useStudioBootstrap(options) {
292
395
  (async () => {
293
396
  try {
294
397
  setClientKey(options.clientKey);
295
- const requireAuth = isSupabaseClientInjected();
398
+ const hasInjectedSupabase = isSupabaseClientInjected();
399
+ const requireAuth = hasInjectedSupabase;
400
+ const analyticsEnabled = options.analyticsEnabled ?? hasInjectedSupabase;
401
+ await initStudioAnalytics({
402
+ enabled: analyticsEnabled,
403
+ token: process.env.EXPO_PUBLIC_MIXPANEL_TOKEN,
404
+ serverUrl: process.env.EXPO_PUBLIC_MIXPANEL_SERVER_URL,
405
+ debug: __DEV__
406
+ });
296
407
  if (!requireAuth) {
297
408
  setSupabaseConfig({ url: SUPABASE_URL, anonKey: SUPABASE_ANON_KEY });
409
+ await resetStudioAnalytics();
298
410
  }
299
411
  const { user } = requireAuth ? await ensureAuthenticatedSession() : await ensureAnonymousSession();
412
+ if (requireAuth) {
413
+ await identifyStudioUser(user.id);
414
+ }
300
415
  if (cancelled) return;
301
416
  setState({ ready: true, userId: user.id, error: null });
302
417
  } catch (e) {
@@ -308,14 +423,20 @@ function useStudioBootstrap(options) {
308
423
  return () => {
309
424
  cancelled = true;
310
425
  };
311
- }, [options.clientKey]);
426
+ }, [options.analyticsEnabled, options.clientKey]);
312
427
  return state;
313
428
  }
314
429
 
315
430
  // src/studio/bootstrap/StudioBootstrap.tsx
316
431
  import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
317
- function StudioBootstrap({ children, fallback, renderError, clientKey: clientKey2 }) {
318
- const { ready, error, userId } = useStudioBootstrap({ clientKey: clientKey2 });
432
+ function StudioBootstrap({
433
+ children,
434
+ fallback,
435
+ renderError,
436
+ clientKey: clientKey2,
437
+ analyticsEnabled
438
+ }) {
439
+ const { ready, error, userId } = useStudioBootstrap({ clientKey: clientKey2, analyticsEnabled });
319
440
  if (error) {
320
441
  return /* @__PURE__ */ jsx2(View, { style: { flex: 1, justifyContent: "center", alignItems: "center", padding: 24 }, children: renderError ? renderError(error) : /* @__PURE__ */ jsx2(Text, { variant: "bodyMuted", children: error.message }) });
321
442
  }
@@ -333,36 +454,6 @@ import * as React3 from "react";
333
454
 
334
455
  // src/core/services/http/index.ts
335
456
  import axios2 from "axios";
336
-
337
- // src/core/logger.ts
338
- import { logger, consoleTransport } from "react-native-logs";
339
- var log = logger.createLogger(
340
- {
341
- levels: {
342
- debug: 0,
343
- info: 1,
344
- warn: 2,
345
- error: 3
346
- },
347
- severity: "debug",
348
- transport: consoleTransport,
349
- transportOptions: {
350
- colors: {
351
- info: "blueBright",
352
- warn: "yellowBright",
353
- error: "redBright"
354
- }
355
- },
356
- async: true,
357
- dateFormat: "time",
358
- printLevel: true,
359
- printDate: true,
360
- fixedExtLvlLength: false,
361
- enabled: true
362
- }
363
- );
364
-
365
- // src/core/services/http/index.ts
366
457
  var RETRYABLE_MAX_ATTEMPTS = 3;
367
458
  var RETRYABLE_BASE_DELAY_MS = 500;
368
459
  var RETRYABLE_MAX_DELAY_MS = 4e3;
@@ -1177,6 +1268,154 @@ var BundlesRepositoryImpl = class extends BaseRepository {
1177
1268
  };
1178
1269
  var bundlesRepository = new BundlesRepositoryImpl(bundlesRemoteDataSource);
1179
1270
 
1271
+ // src/studio/analytics/events.ts
1272
+ var STUDIO_ANALYTICS_EVENT_VERSION = 1;
1273
+
1274
+ // src/studio/analytics/track.ts
1275
+ function baseProps() {
1276
+ return { event_version: STUDIO_ANALYTICS_EVENT_VERSION };
1277
+ }
1278
+ function normalizeError(error) {
1279
+ if (!error) return {};
1280
+ if (typeof error === "string") {
1281
+ return { error_code: error.slice(0, 120), error_domain: "string" };
1282
+ }
1283
+ if (error instanceof Error) {
1284
+ return {
1285
+ error_code: error.message.slice(0, 120),
1286
+ error_domain: error.name || "Error"
1287
+ };
1288
+ }
1289
+ if (typeof error === "object") {
1290
+ const candidate = error;
1291
+ return {
1292
+ error_code: String(candidate.code ?? candidate.message ?? "unknown_error").slice(0, 120),
1293
+ error_domain: candidate.name ?? "object"
1294
+ };
1295
+ }
1296
+ return { error_code: "unknown_error", error_domain: typeof error };
1297
+ }
1298
+ async function trackMutationEvent(name, payload) {
1299
+ await trackStudioEvent(name, payload);
1300
+ await flushStudioAnalytics();
1301
+ }
1302
+ var lastOpenCommentsKey = null;
1303
+ var lastOpenCommentsAt = 0;
1304
+ async function trackRemixApp(params) {
1305
+ const errorProps = params.success ? {} : normalizeError(params.error);
1306
+ await trackMutationEvent("remix_app", {
1307
+ app_id: params.appId,
1308
+ source_app_id: params.sourceAppId,
1309
+ thread_id: params.threadId,
1310
+ success: params.success,
1311
+ ...errorProps,
1312
+ ...baseProps()
1313
+ });
1314
+ }
1315
+ async function trackEditApp(params) {
1316
+ const errorProps = params.success ? {} : normalizeError(params.error);
1317
+ await trackMutationEvent("edit_app", {
1318
+ app_id: params.appId,
1319
+ thread_id: params.threadId,
1320
+ prompt_length: params.promptLength,
1321
+ success: params.success,
1322
+ ...errorProps,
1323
+ ...baseProps()
1324
+ });
1325
+ }
1326
+ async function trackShareApp(params) {
1327
+ const errorProps = params.success ? {} : normalizeError(params.error);
1328
+ await trackMutationEvent("share_app", {
1329
+ app_id: params.appId,
1330
+ success: params.success,
1331
+ ...errorProps,
1332
+ ...baseProps()
1333
+ });
1334
+ }
1335
+ async function trackOpenMergeRequest(params) {
1336
+ const errorProps = params.success ? {} : normalizeError(params.error);
1337
+ await trackMutationEvent("open_merge_request", {
1338
+ app_id: params.appId,
1339
+ merge_request_id: params.mergeRequestId,
1340
+ success: params.success,
1341
+ ...errorProps,
1342
+ ...baseProps()
1343
+ });
1344
+ }
1345
+ async function trackApproveMergeRequest(params) {
1346
+ const errorProps = params.success ? {} : normalizeError(params.error);
1347
+ await trackMutationEvent("approve_merge_request", {
1348
+ app_id: params.appId,
1349
+ merge_request_id: params.mergeRequestId,
1350
+ success: params.success,
1351
+ ...errorProps,
1352
+ ...baseProps()
1353
+ });
1354
+ }
1355
+ async function trackRejectMergeRequest(params) {
1356
+ const errorProps = params.success ? {} : normalizeError(params.error);
1357
+ await trackMutationEvent("reject_merge_request", {
1358
+ app_id: params.appId,
1359
+ merge_request_id: params.mergeRequestId,
1360
+ success: params.success,
1361
+ ...errorProps,
1362
+ ...baseProps()
1363
+ });
1364
+ }
1365
+ async function trackTestBundle(params) {
1366
+ const errorProps = params.success ? {} : normalizeError(params.error);
1367
+ await trackMutationEvent("test_bundle", {
1368
+ app_id: params.appId,
1369
+ commit_id: params.commitId,
1370
+ success: params.success,
1371
+ ...errorProps,
1372
+ ...baseProps()
1373
+ });
1374
+ }
1375
+ async function trackLikeApp(params) {
1376
+ const errorProps = params.success ? {} : normalizeError(params.error);
1377
+ await trackMutationEvent("like_app", {
1378
+ app_id: params.appId,
1379
+ source: params.source ?? "unknown",
1380
+ success: params.success,
1381
+ ...errorProps,
1382
+ ...baseProps()
1383
+ });
1384
+ }
1385
+ async function trackUnlikeApp(params) {
1386
+ const errorProps = params.success ? {} : normalizeError(params.error);
1387
+ await trackMutationEvent("unlike_app", {
1388
+ app_id: params.appId,
1389
+ source: params.source ?? "unknown",
1390
+ success: params.success,
1391
+ ...errorProps,
1392
+ ...baseProps()
1393
+ });
1394
+ }
1395
+ async function trackOpenComments(params) {
1396
+ const key = `${params.appId}:${params.source ?? "unknown"}`;
1397
+ const now = Date.now();
1398
+ if (lastOpenCommentsKey === key && now - lastOpenCommentsAt < 1e3) return;
1399
+ lastOpenCommentsKey = key;
1400
+ lastOpenCommentsAt = now;
1401
+ await trackStudioEvent("open_comments", {
1402
+ app_id: params.appId,
1403
+ source: params.source ?? "unknown",
1404
+ ...baseProps()
1405
+ });
1406
+ }
1407
+ async function trackSubmitComment(params) {
1408
+ const errorProps = params.success ? {} : normalizeError(params.error);
1409
+ await trackMutationEvent("submit_comment", {
1410
+ app_id: params.appId,
1411
+ comment_type: "general",
1412
+ comment_length: params.commentLength,
1413
+ success: params.success,
1414
+ ...errorProps,
1415
+ ...baseProps()
1416
+ });
1417
+ }
1418
+
1180
1419
  // src/studio/hooks/useBundleManager.ts
1181
1420
  function sleep2(ms) {
1182
1421
  return new Promise((r) => setTimeout(r, ms));
@@ -1493,7 +1732,7 @@ async function pollBundle(appId, bundleId, opts) {
1493
1732
  await sleep2(opts.intervalMs);
1494
1733
  }
1495
1734
  }
1496
- async function resolveBundlePath(src, platform, mode) {
1735
+ async function resolveBundlePath(src, platform, mode, onStatus) {
1497
1736
  const { appId, commitId } = src;
1498
1737
  const dir = bundlesCacheDir();
1499
1738
  await ensureDir(dir);
@@ -1507,7 +1746,9 @@ async function resolveBundlePath(src, platform, mode) {
1507
1746
  },
1508
1747
  { attempts: 3, baseDelayMs: 500, maxDelayMs: 4e3 }
1509
1748
  );
1749
+ onStatus == null ? void 0 : onStatus(initiate.status);
1510
1750
  const finalBundle = initiate.status === "succeeded" || initiate.status === "failed" ? initiate : await pollBundle(appId, initiate.id, { timeoutMs: 3 * 60 * 1e3, intervalMs: 1200 });
1751
+ onStatus == null ? void 0 : onStatus(finalBundle.status);
1511
1752
  if (finalBundle.status === "failed") {
1512
1753
  throw new Error("Bundle build failed.");
1513
1754
  }
@@ -1549,6 +1790,7 @@ function useBundleManager({
1549
1790
  const [renderToken, setRenderToken] = React5.useState(0);
1550
1791
  const [loading, setLoading] = React5.useState(false);
1551
1792
  const [loadingMode, setLoadingMode] = React5.useState(null);
1793
+ const [bundleStatus, setBundleStatus] = React5.useState(null);
1552
1794
  const [statusLabel, setStatusLabel] = React5.useState(null);
1553
1795
  const [error, setError] = React5.useState(null);
1554
1796
  const [isTesting, setIsTesting] = React5.useState(false);
@@ -1660,16 +1902,20 @@ function useBundleManager({
1660
1902
  activeLoadModeRef.current = mode;
1661
1903
  setLoading(true);
1662
1904
  setLoadingMode(mode);
1905
+ setBundleStatus(null);
1663
1906
  setError(null);
1664
1907
  setStatusLabel(mode === "test" ? "Loading test bundle\u2026" : "Loading latest build\u2026");
1665
1908
  if (mode === "base" && desiredModeRef.current === "base") {
1666
1909
  void activateCachedBase(src.appId);
1667
1910
  }
1668
1911
  try {
1669
- const { bundlePath: path, bundle } = await resolveBundlePath(src, platform, mode);
1912
+ const { bundlePath: path, bundle } = await resolveBundlePath(src, platform, mode, (status) => {
1913
+ setBundleStatus(status);
1914
+ });
1670
1915
  if (mode === "base" && opId !== baseOpIdRef.current) return;
1671
1916
  if (mode === "test" && opId !== testOpIdRef.current) return;
1672
1917
  if (desiredModeRef.current !== mode) return;
1918
+ setBundleStatus(bundle.status);
1673
1919
  setBundlePath(path);
1674
1920
  const fingerprint = bundle.checksumSha256 ?? `id:${bundle.id}`;
1675
1921
  const shouldSkipInitialBaseRemount = mode === "base" && initialHydratedBaseFromDiskRef.current && !hasCompletedFirstNetworkBaseLoadRef.current && Boolean(lastBaseFingerprintRef.current) && lastBaseFingerprintRef.current === fingerprint;
@@ -1705,6 +1951,7 @@ function useBundleManager({
1705
1951
  } catch (e) {
1706
1952
  if (mode === "base" && opId !== baseOpIdRef.current) return;
1707
1953
  if (mode === "test" && opId !== testOpIdRef.current) return;
1954
+ setBundleStatus("failed");
1708
1955
  const msg = e instanceof Error ? e.message : String(e);
1709
1956
  setError(msg);
1710
1957
  setStatusLabel(null);
@@ -1720,7 +1967,22 @@ function useBundleManager({
1720
1967
  await load(baseRef.current, "base");
1721
1968
  }, [load]);
1722
1969
  const loadTest = React5.useCallback(async (src) => {
1723
- await load(src, "test");
1970
+ try {
1971
+ await load(src, "test");
1972
+ await trackTestBundle({
1973
+ appId: src.appId,
1974
+ commitId: src.commitId ?? void 0,
1975
+ success: true
1976
+ });
1977
+ } catch (error2) {
1978
+ await trackTestBundle({
1979
+ appId: src.appId,
1980
+ commitId: src.commitId ?? void 0,
1981
+ success: false,
1982
+ error: error2
1983
+ });
1984
+ throw error2;
1985
+ }
1724
1986
  }, [load]);
1725
1987
  const restoreBase = React5.useCallback(async () => {
1726
1988
  const src = baseRef.current;
@@ -1736,7 +1998,19 @@ function useBundleManager({
1736
1998
  if (!canRequestLatest) return;
1737
1999
  void loadBase();
1738
2000
  }, [base.appId, base.commitId, platform, canRequestLatest, loadBase]);
1739
- return { bundlePath, renderToken, loading, loadingMode, statusLabel, error, isTesting, loadBase, loadTest, restoreBase };
2001
+ return {
2002
+ bundlePath,
2003
+ renderToken,
2004
+ loading,
2005
+ loadingMode,
2006
+ bundleStatus,
2007
+ statusLabel,
2008
+ error,
2009
+ isTesting,
2010
+ loadBase,
2011
+ loadTest,
2012
+ restoreBase
2013
+ };
1740
2014
  }
1741
2015
 
1742
2016
  // src/studio/hooks/useMergeRequests.ts
@@ -1946,24 +2220,68 @@ function useMergeRequests(params) {
1946
2220
  }, [appId]);
1947
2221
  React6.useEffect(() => {
1948
2222
  void refresh();
1949
- }, [refresh]);
2223
+ }, [appId, refresh]);
1950
2224
  const openMergeRequest = React6.useCallback(async (sourceAppId) => {
1951
- const mr = await mergeRequestsRepository.open({ sourceAppId });
1952
- await refresh();
1953
- return mr;
2225
+ try {
2226
+ const mr = await mergeRequestsRepository.open({ sourceAppId });
2227
+ await refresh();
2228
+ await trackOpenMergeRequest({
2229
+ appId,
2230
+ mergeRequestId: mr.id,
2231
+ success: true
2232
+ });
2233
+ return mr;
2234
+ } catch (error2) {
2235
+ await trackOpenMergeRequest({
2236
+ appId,
2237
+ success: false,
2238
+ error: error2
2239
+ });
2240
+ throw error2;
2241
+ }
1954
2242
  }, [refresh]);
1955
2243
  const approve = React6.useCallback(async (mrId) => {
1956
- const mr = await mergeRequestsRepository.update(mrId, { status: "approved" });
1957
- await refresh();
1958
- const merged = await pollUntilMerged(mrId);
1959
- await refresh();
1960
- return merged ?? mr;
1961
- }, [pollUntilMerged, refresh]);
2244
+ try {
2245
+ const mr = await mergeRequestsRepository.update(mrId, { status: "approved" });
2246
+ await refresh();
2247
+ const merged = await pollUntilMerged(mrId);
2248
+ await refresh();
2249
+ await trackApproveMergeRequest({
2250
+ appId,
2251
+ mergeRequestId: mrId,
2252
+ success: true
2253
+ });
2254
+ return merged ?? mr;
2255
+ } catch (error2) {
2256
+ await trackApproveMergeRequest({
2257
+ appId,
2258
+ mergeRequestId: mrId,
2259
+ success: false,
2260
+ error: error2
2261
+ });
2262
+ throw error2;
2263
+ }
2264
+ }, [appId, pollUntilMerged, refresh]);
1962
2265
  const reject = React6.useCallback(async (mrId) => {
1963
- const mr = await mergeRequestsRepository.update(mrId, { status: "rejected" });
1964
- await refresh();
1965
- return mr;
1966
- }, [refresh]);
2266
+ try {
2267
+ const mr = await mergeRequestsRepository.update(mrId, { status: "rejected" });
2268
+ await refresh();
2269
+ await trackRejectMergeRequest({
2270
+ appId,
2271
+ mergeRequestId: mrId,
2272
+ success: true
2273
+ });
2274
+ return mr;
2275
+ } catch (error2) {
2276
+ await trackRejectMergeRequest({
2277
+ appId,
2278
+ mergeRequestId: mrId,
2279
+ success: false,
2280
+ error: error2
2281
+ });
2282
+ throw error2;
2283
+ }
2284
+ }, [appId, refresh]);
1967
2285
  const toSummary = React6.useCallback((mr) => {
1968
2286
  const stats = creatorStatsById[mr.createdBy];
1969
2287
  return {
@@ -1999,7 +2317,7 @@ function useMergeRequests(params) {
1999
2317
 
2000
2318
  // src/studio/hooks/useAttachmentUpload.ts
2001
2319
  import * as React7 from "react";
2002
- import { Platform } from "react-native";
2320
+ import { Platform as Platform2 } from "react-native";
2003
2321
  import * as FileSystem2 from "expo-file-system/legacy";
2004
2322
 
2005
2323
  // src/data/attachment/remote.ts
@@ -2086,7 +2404,7 @@ function useAttachmentUpload() {
2086
2404
  const blobs = await Promise.all(
2087
2405
  dataUrls.map(async (dataUrl, idx) => {
2088
2406
  const normalized = dataUrl.startsWith("data:") ? dataUrl : `data:image/png;base64,${dataUrl}`;
2089
- const blob = Platform.OS === "android" ? await dataUrlToBlobAndroid(normalized) : await (await fetch(normalized)).blob();
2407
+ const blob = Platform2.OS === "android" ? await dataUrlToBlobAndroid(normalized) : await (await fetch(normalized)).blob();
2090
2408
  const mimeType = getMimeTypeFromDataUrl(normalized);
2091
2409
  return { blob, idx, mimeType };
2092
2410
  })
@@ -2204,13 +2522,21 @@ function useStudioActions({
2204
2522
  if (sending) return;
2205
2523
  setSending(true);
2206
2524
  setError(null);
2525
+ let forkSucceeded = false;
2207
2526
  try {
2208
2527
  let targetApp = app;
2528
+ const sourceAppId = app.id;
2209
2529
  if (shouldForkOnEdit) {
2210
2530
  setForking(true);
2211
- const sourceAppId = app.id;
2212
2531
  const forked = await appsRepository.fork(app.id, {});
2213
2532
  targetApp = forked;
2533
+ await trackRemixApp({
2534
+ appId: forked.id,
2535
+ sourceAppId,
2536
+ threadId: forked.threadId ?? void 0,
2537
+ success: true
2538
+ });
2539
+ forkSucceeded = true;
2214
2540
  onForkedApp == null ? void 0 : onForkedApp(forked.id, { keepRenderingAppId: sourceAppId });
2215
2541
  }
2216
2542
  setForking(false);
@@ -2238,9 +2564,33 @@ function useStudioActions({
2238
2564
  queueItemId: editResult.queueItemId ?? null,
2239
2565
  queuePosition: editResult.queuePosition ?? null
2240
2566
  });
2567
+ await trackEditApp({
2568
+ appId: targetApp.id,
2569
+ threadId,
2570
+ promptLength: prompt.trim().length,
2571
+ success: true
2572
+ });
2241
2573
  } catch (e) {
2242
2574
  const err = e instanceof Error ? e : new Error(String(e));
2243
2575
  setError(err);
2576
+ if (shouldForkOnEdit && !forkSucceeded && (app == null ? void 0 : app.id)) {
2577
+ await trackRemixApp({
2578
+ appId: app.id,
2579
+ sourceAppId: app.id,
2580
+ threadId: app.threadId ?? void 0,
2581
+ success: false,
2582
+ error: err
2583
+ });
2584
+ }
2585
+ if ((app == null ? void 0 : app.id) && app.threadId) {
2586
+ await trackEditApp({
2587
+ appId: app.id,
2588
+ threadId: app.threadId,
2589
+ promptLength: prompt.trim().length,
2590
+ success: false,
2591
+ error: err
2592
+ });
2593
+ }
2244
2594
  throw err;
2245
2595
  } finally {
2246
2596
  setForking(false);
@@ -2261,6 +2611,7 @@ import { jsx as jsx3 } from "react/jsx-runtime";
2261
2611
  function RuntimeRenderer({
2262
2612
  appKey,
2263
2613
  bundlePath,
2614
+ preparingText,
2264
2615
  forcePreparing,
2265
2616
  renderToken,
2266
2617
  style,
@@ -2276,7 +2627,7 @@ function RuntimeRenderer({
2276
2627
  if (!hasRenderedOnce && !forcePreparing && !allowInitialPreparing) {
2277
2628
  return /* @__PURE__ */ jsx3(View2, { style: [{ flex: 1 }, style] });
2278
2629
  }
2279
- return /* @__PURE__ */ jsx3(View2, { style: [{ flex: 1, justifyContent: "center", alignItems: "center", padding: 24 }, style], children: /* @__PURE__ */ jsx3(Text, { variant: "bodyMuted", children: "Preparing app\u2026" }) });
2630
+ return /* @__PURE__ */ jsx3(View2, { style: [{ flex: 1, justifyContent: "center", alignItems: "center", padding: 24 }, style], children: /* @__PURE__ */ jsx3(Text, { variant: "bodyMuted", children: preparingText ?? "Preparing app\u2026" }) });
2280
2631
  }
2281
2632
  return /* @__PURE__ */ jsx3(View2, { style: [{ flex: 1 }, style], children: /* @__PURE__ */ jsx3(
2282
2633
  ComergeRuntimeRenderer,
@@ -2290,8 +2641,8 @@ function RuntimeRenderer({
2290
2641
  }
2291
2642
 
2292
2643
  // src/studio/ui/StudioOverlay.tsx
2293
- import * as React44 from "react";
2294
- import { Keyboard as Keyboard5, Platform as Platform10, View as View45, useWindowDimensions as useWindowDimensions4 } from "react-native";
2644
+ import * as React45 from "react";
2645
+ import { Keyboard as Keyboard5, Platform as Platform11, View as View47, useWindowDimensions as useWindowDimensions4 } from "react-native";
2295
2646
 
2296
2647
  // src/components/studio-sheet/StudioBottomSheet.tsx
2297
2648
  import * as React12 from "react";
@@ -2302,7 +2653,7 @@ import {
2302
2653
  import { useSafeAreaInsets } from "react-native-safe-area-context";
2303
2654
 
2304
2655
  // src/components/studio-sheet/StudioSheetBackground.tsx
2305
- import { Platform as Platform3, View as View3 } from "react-native";
2656
+ import { Platform as Platform4, View as View3 } from "react-native";
2306
2657
  import { isLiquidGlassSupported } from "@callstack/liquid-glass";
2307
2658
 
2308
2659
  // src/components/utils/ResettableLiquidGlassView.tsx
@@ -2311,7 +2662,7 @@ import { LiquidGlassView } from "@callstack/liquid-glass";
2311
2662
 
2312
2663
  // src/components/utils/liquidGlassReset.tsx
2313
2664
  import * as React10 from "react";
2314
- import { AppState as AppState2, Platform as Platform2 } from "react-native";
2665
+ import { AppState as AppState2, Platform as Platform3 } from "react-native";
2315
2666
  import { jsx as jsx4 } from "react/jsx-runtime";
2316
2667
  var LiquidGlassResetContext = React10.createContext(0);
2317
2668
  function LiquidGlassResetProvider({
@@ -2320,7 +2671,7 @@ function LiquidGlassResetProvider({
2320
2671
  }) {
2321
2672
  const [token, setToken] = React10.useState(0);
2322
2673
  React10.useEffect(() => {
2323
- if (Platform2.OS !== "ios") return;
2674
+ if (Platform3.OS !== "ios") return;
2324
2675
  const onChange = (state) => {
2325
2676
  if (state === "active") setToken((t) => t + 1);
2326
2677
  };
@@ -2361,7 +2712,7 @@ function StudioSheetBackground({
2361
2712
  renderBackground
2362
2713
  }) {
2363
2714
  const theme = useTheme();
2364
- const radius = Platform3.OS === "ios" ? 39 : 16;
2715
+ const radius = Platform4.OS === "ios" ? 39 : 16;
2365
2716
  const fallbackBgColor = theme.scheme === "dark" ? "rgba(11, 8, 15, 0.85)" : "rgba(255, 255, 255, 0.85)";
2366
2717
  const secondaryBgBaseColor = theme.scheme === "dark" ? "rgb(24, 24, 27)" : "rgb(173, 173, 173)";
2367
2718
  const containerStyle = {
@@ -3421,7 +3772,7 @@ var styles3 = StyleSheet3.create({
3421
3772
 
3422
3773
  // src/components/comments/AppCommentsSheet.tsx
3423
3774
  import * as React24 from "react";
3424
- import { ActivityIndicator as ActivityIndicator3, Keyboard as Keyboard3, Platform as Platform5, Pressable as Pressable5, View as View14 } from "react-native";
3775
+ import { ActivityIndicator as ActivityIndicator3, Keyboard as Keyboard3, Platform as Platform6, Pressable as Pressable5, View as View14 } from "react-native";
3425
3776
  import {
3426
3777
  BottomSheetBackdrop,
3427
3778
  BottomSheetModal as BottomSheetModal2,
@@ -3971,7 +4322,18 @@ function useAppComments(appId) {
3971
4322
  try {
3972
4323
  const newComment = await appCommentsRepository.create(appId, { body: trimmed, commentType: "general" });
3973
4324
  setComments((prev) => sortByCreatedAtAsc([...prev, newComment]));
4325
+ await trackSubmitComment({
4326
+ appId,
4327
+ commentLength: trimmed.length,
4328
+ success: true
4329
+ });
3974
4330
  } catch (e) {
4331
+ await trackSubmitComment({
4332
+ appId,
4333
+ commentLength: trimmed.length,
4334
+ success: false,
4335
+ error: e
4336
+ });
3975
4337
  setError(e instanceof Error ? e : new Error(String(e)));
3976
4338
  throw e;
3977
4339
  } finally {
@@ -4014,11 +4376,11 @@ function useAppDetails(appId) {
4014
4376
 
4015
4377
  // src/components/comments/useIosKeyboardSnapFix.ts
4016
4378
  import * as React23 from "react";
4017
- import { Keyboard as Keyboard2, Platform as Platform4 } from "react-native";
4379
+ import { Keyboard as Keyboard2, Platform as Platform5 } from "react-native";
4018
4380
  function useIosKeyboardSnapFix(sheetRef, options) {
4019
4381
  const [keyboardVisible, setKeyboardVisible] = React23.useState(false);
4020
4382
  React23.useEffect(() => {
4021
- if (Platform4.OS !== "ios") return;
4383
+ if (Platform5.OS !== "ios") return;
4022
4384
  const show = Keyboard2.addListener("keyboardWillShow", () => setKeyboardVisible(true));
4023
4385
  const hide = Keyboard2.addListener("keyboardWillHide", () => {
4024
4386
  var _a;
@@ -4096,8 +4458,8 @@ function AppCommentsSheet({ appId, onClose, onCountChange, onPlayApp }) {
4096
4458
  onChange: handleChange,
4097
4459
  backgroundStyle: {
4098
4460
  backgroundColor: theme.scheme === "dark" ? "#0B080F" : "#FFFFFF",
4099
- borderTopLeftRadius: Platform5.OS === "ios" ? 39 : 16,
4100
- borderTopRightRadius: Platform5.OS === "ios" ? 39 : 16
4461
+ borderTopLeftRadius: Platform6.OS === "ios" ? 39 : 16,
4462
+ borderTopRightRadius: Platform6.OS === "ios" ? 39 : 16
4101
4463
  },
4102
4464
  handleIndicatorStyle: { backgroundColor: theme.colors.handleIndicator },
4103
4465
  keyboardBehavior: "interactive",
@@ -4203,7 +4565,7 @@ function AppCommentsSheet({ appId, onClose, onCountChange, onPlayApp }) {
4203
4565
  bottom: 0,
4204
4566
  paddingHorizontal: theme.spacing.lg,
4205
4567
  paddingTop: theme.spacing.sm,
4206
- paddingBottom: Platform5.OS === "ios" ? keyboardVisible ? theme.spacing.lg : insets.bottom : insets.bottom + 10,
4568
+ paddingBottom: Platform6.OS === "ios" ? keyboardVisible ? theme.spacing.lg : insets.bottom : insets.bottom + 10,
4207
4569
  borderTopWidth: 1,
4208
4570
  borderTopColor: withAlpha(theme.colors.border, 0.1),
4209
4571
  backgroundColor: withAlpha(theme.colors.background, 0.8)
@@ -4230,7 +4592,7 @@ function AppCommentsSheet({ appId, onClose, onCountChange, onPlayApp }) {
4230
4592
 
4231
4593
  // src/studio/ui/PreviewPanel.tsx
4232
4594
  import * as React35 from "react";
4233
- import { ActivityIndicator as ActivityIndicator7, Platform as Platform7, Share, View as View33 } from "react-native";
4595
+ import { ActivityIndicator as ActivityIndicator7, Platform as Platform8, Share, View as View33 } from "react-native";
4234
4596
 
4235
4597
  // src/components/preview/PreviewPage.tsx
4236
4598
  import { View as View15 } from "react-native";
@@ -5121,7 +5483,7 @@ import { Animated as Animated7, Pressable as Pressable10, View as View28 } from
5121
5483
  import { Ban, Check as Check3, CheckCheck, ChevronDown as ChevronDown2 } from "lucide-react-native";
5122
5484
 
5123
5485
  // src/components/primitives/MarkdownText.tsx
5124
- import { Dimensions as Dimensions2, Keyboard as Keyboard4, Modal, Platform as Platform6, Pressable as Pressable9, Text as Text3, View as View27 } from "react-native";
5486
+ import { Dimensions as Dimensions2, Keyboard as Keyboard4, Modal, Platform as Platform7, Pressable as Pressable9, Text as Text3, View as View27 } from "react-native";
5125
5487
  import Markdown from "react-native-markdown-display";
5126
5488
  import { useEffect as useEffect22, useRef as useRef15, useState as useState21 } from "react";
5127
5489
  import { jsx as jsx39, jsxs as jsxs22 } from "react/jsx-runtime";
@@ -5209,7 +5571,7 @@ function MarkdownText({ markdown, variant = "chat", bodyColor, style }) {
5209
5571
  paddingHorizontal: variant === "mergeRequest" ? 6 : 4,
5210
5572
  paddingVertical: variant === "mergeRequest" ? 2 : 0,
5211
5573
  borderRadius: variant === "mergeRequest" ? 6 : 4,
5212
- fontFamily: Platform6.OS === "ios" ? "Menlo" : "monospace",
5574
+ fontFamily: Platform7.OS === "ios" ? "Menlo" : "monospace",
5213
5575
  fontSize: 13
5214
5576
  },
5215
5577
  code_block: {
@@ -6051,7 +6413,8 @@ function useAppStats({
6051
6413
  initialComments = 0,
6052
6414
  initialForks = 0,
6053
6415
  initialIsLiked = false,
6054
- onOpenComments
6416
+ onOpenComments,
6417
+ interactionSource = "unknown"
6055
6418
  }) {
6056
6419
  const [likeCount, setLikeCount] = React33.useState(initialLikes);
6057
6420
  const [commentCount, setCommentCount] = React33.useState(initialComments);
@@ -6095,23 +6458,31 @@ function useAppStats({
6095
6458
  if (newIsLiked) {
6096
6459
  const res = await appLikesRepository.create(appId, {});
6097
6460
  if (typeof ((_a = res.stats) == null ? void 0 : _a.total) === "number") setLikeCount(Math.max(0, res.stats.total));
6461
+ await trackLikeApp({ appId, source: interactionSource, success: true });
6098
6462
  } else {
6099
6463
  const res = await appLikesRepository.removeMine(appId);
6100
6464
  if (typeof ((_b = res.stats) == null ? void 0 : _b.total) === "number") setLikeCount(Math.max(0, res.stats.total));
6465
+ await trackUnlikeApp({ appId, source: interactionSource, success: true });
6101
6466
  }
6102
6467
  } catch (e) {
6103
6468
  setIsLiked(!newIsLiked);
6104
6469
  setLikeCount((prev) => Math.max(0, prev + (newIsLiked ? -1 : 1)));
6470
+ if (newIsLiked) {
6471
+ await trackLikeApp({ appId, source: interactionSource, success: false, error: e });
6472
+ } else {
6473
+ await trackUnlikeApp({ appId, source: interactionSource, success: false, error: e });
6474
+ }
6105
6475
  }
6106
- }, [appId, isLiked, likeCount]);
6476
+ }, [appId, interactionSource, isLiked, likeCount]);
6107
6477
  const handleOpenComments = React33.useCallback(() => {
6108
6478
  if (!appId) return;
6109
6479
  try {
6110
6480
  void Haptics2.impactAsync(Haptics2.ImpactFeedbackStyle.Light);
6111
6481
  } catch {
6112
6482
  }
6483
+ void trackOpenComments({ appId, source: interactionSource });
6113
6484
  onOpenComments == null ? void 0 : onOpenComments();
6114
- }, [appId, onOpenComments]);
6485
+ }, [appId, interactionSource, onOpenComments]);
6115
6486
  return { likeCount, commentCount, forkCount, isLiked, setCommentCount, handleLike, handleOpenComments };
6116
6487
  }
6117
6488
 
@@ -6194,7 +6565,8 @@ function usePreviewPanelData(params) {
6194
6565
  initialForks: insights.forks,
6195
6566
  initialComments: commentCountOverride ?? insights.comments,
6196
6567
  initialIsLiked: Boolean(app == null ? void 0 : app.isLiked),
6197
- onOpenComments
6568
+ onOpenComments,
6569
+ interactionSource: "preview_panel"
6198
6570
  });
6199
6571
  const canSubmitMergeRequest = React34.useMemo(() => {
6200
6572
  if (!isOwner) return false;
@@ -6260,7 +6632,7 @@ ${shareUrl}` : `Check out this app on Remix
6260
6632
  ${shareUrl}`;
6261
6633
  try {
6262
6634
  const title = app.name ?? "Remix app";
6263
- const payload = Platform7.OS === "ios" ? {
6635
+ const payload = Platform8.OS === "ios" ? {
6264
6636
  title,
6265
6637
  message
6266
6638
  } : {
@@ -6269,8 +6641,17 @@ ${shareUrl}`;
6269
6641
  url: shareUrl
6270
6642
  };
6271
6643
  await Share.share(payload);
6644
+ await trackShareApp({
6645
+ appId: app.id,
6646
+ success: true
6647
+ });
6272
6648
  } catch (error) {
6273
6649
  log.warn("PreviewPanel share failed", error);
6650
+ await trackShareApp({
6651
+ appId: app.id,
6652
+ success: false,
6653
+ error
6654
+ });
6274
6655
  }
6275
6656
  }, [app]);
6276
6657
  const {
@@ -6364,20 +6745,21 @@ ${shareUrl}`;
6364
6745
  }
6365
6746
 
6366
6747
  // src/studio/ui/ChatPanel.tsx
6367
- import * as React41 from "react";
6368
- import { ActivityIndicator as ActivityIndicator9, View as View42 } from "react-native";
6748
+ import * as React42 from "react";
6749
+ import { ActivityIndicator as ActivityIndicator9, View as View44 } from "react-native";
6369
6750
 
6370
6751
  // src/components/chat/ChatPage.tsx
6371
- import * as React38 from "react";
6372
- import { Platform as Platform9, View as View37 } from "react-native";
6752
+ import * as React39 from "react";
6753
+ import { Platform as Platform10, View as View37 } from "react-native";
6373
6754
  import { useSafeAreaInsets as useSafeAreaInsets4 } from "react-native-safe-area-context";
6374
6755
 
6375
6756
  // src/components/chat/ChatMessageList.tsx
6376
- import * as React37 from "react";
6757
+ import * as React38 from "react";
6377
6758
  import { View as View36 } from "react-native";
6378
6759
  import { BottomSheetFlatList } from "@gorhom/bottom-sheet";
6379
6760
 
6380
6761
  // src/components/chat/ChatMessageBubble.tsx
6762
+ import * as React36 from "react";
6381
6763
  import { View as View34 } from "react-native";
6382
6764
  import { CheckCheck as CheckCheck2, GitMerge as GitMerge2, RotateCcw } from "lucide-react-native";
6383
6765
 
@@ -6448,7 +6830,19 @@ function Button({
6448
6830
 
6449
6831
  // src/components/chat/ChatMessageBubble.tsx
6450
6832
  import { jsx as jsx47, jsxs as jsxs28 } from "react/jsx-runtime";
6451
- function ChatMessageBubble({ message, renderContent, isLast, retrying, onRetry, style }) {
6833
+ function areMessageMetaEqual(a, b) {
6834
+ if (a === b) return true;
6835
+ if (!a || !b) return a === b;
6836
+ return a.kind === b.kind && a.event === b.event && a.status === b.status && a.mergeRequestId === b.mergeRequestId && a.sourceAppId === b.sourceAppId && a.targetAppId === b.targetAppId && a.appId === b.appId && a.threadId === b.threadId;
6837
+ }
6838
+ function ChatMessageBubbleInner({
6839
+ message,
6840
+ renderContent,
6841
+ isLast,
6842
+ retrying,
6843
+ onRetryMessage,
6844
+ style
6845
+ }) {
6452
6846
  var _a, _b;
6453
6847
  const theme = useTheme();
6454
6848
  const metaEvent = ((_a = message.meta) == null ? void 0 : _a.event) ?? null;
@@ -6463,8 +6857,11 @@ function ChatMessageBubble({ message, renderContent, isLast, retrying, onRetry,
6463
6857
  const bubbleVariant = isHuman ? "surface" : "surfaceRaised";
6464
6858
  const cornerStyle = isHuman ? { borderTopRightRadius: 0 } : { borderTopLeftRadius: 0 };
6465
6859
  const bodyColor = metaStatus === "success" ? theme.colors.success : metaStatus === "error" ? theme.colors.danger : void 0;
6466
- const showRetry = Boolean(onRetry) && isLast && metaStatus === "error" && message.author === "human";
6860
+ const showRetry = Boolean(onRetryMessage) && isLast && metaStatus === "error" && message.author === "human";
6467
6861
  const retryLabel = retrying ? "Retrying..." : "Retry";
6862
+ const handleRetryPress = React36.useCallback(() => {
6863
+ onRetryMessage == null ? void 0 : onRetryMessage(message.id);
6864
+ }, [message.id, onRetryMessage]);
6468
6865
  return /* @__PURE__ */ jsxs28(View34, { style: [align, style], children: [
6469
6866
  /* @__PURE__ */ jsx47(
6470
6867
  Surface,
@@ -6493,7 +6890,7 @@ function ChatMessageBubble({ message, renderContent, isLast, retrying, onRetry,
6493
6890
  {
6494
6891
  variant: "ghost",
6495
6892
  size: "sm",
6496
- onPress: onRetry,
6893
+ onPress: handleRetryPress,
6497
6894
  disabled: retrying,
6498
6895
  style: { borderColor: theme.colors.danger },
6499
6896
  accessibilityLabel: "Retry send",
@@ -6514,19 +6911,24 @@ function ChatMessageBubble({ message, renderContent, isLast, retrying, onRetry,
6514
6911
  ) }) : null
6515
6912
  ] });
6516
6913
  }
6914
+ function areEqual(prev, next) {
6915
+ return prev.message.id === next.message.id && prev.message.author === next.message.author && prev.message.content === next.message.content && prev.message.kind === next.message.kind && String(prev.message.createdAt) === String(next.message.createdAt) && areMessageMetaEqual(prev.message.meta, next.message.meta) && prev.renderContent === next.renderContent && prev.isLast === next.isLast && prev.retrying === next.retrying && prev.onRetryMessage === next.onRetryMessage && prev.style === next.style;
6916
+ }
6917
+ var ChatMessageBubble = React36.memo(ChatMessageBubbleInner, areEqual);
6918
+ ChatMessageBubble.displayName = "ChatMessageBubble";
6517
6919
 
6518
6920
  // src/components/chat/TypingIndicator.tsx
6519
- import * as React36 from "react";
6921
+ import * as React37 from "react";
6520
6922
  import { Animated as Animated10, View as View35 } from "react-native";
6521
6923
  import { jsx as jsx48 } from "react/jsx-runtime";
6522
6924
  function TypingIndicator({ style }) {
6523
6925
  const theme = useTheme();
6524
6926
  const dotColor = theme.colors.textSubtle;
6525
- const anims = React36.useMemo(
6927
+ const anims = React37.useMemo(
6526
6928
  () => [new Animated10.Value(0.3), new Animated10.Value(0.3), new Animated10.Value(0.3)],
6527
6929
  []
6528
6930
  );
6529
- React36.useEffect(() => {
6931
+ React37.useEffect(() => {
6530
6932
  const loops = [];
6531
6933
  anims.forEach((a, idx) => {
6532
6934
  const seq = Animated10.sequence([
@@ -6560,7 +6962,7 @@ function TypingIndicator({ style }) {
6560
6962
 
6561
6963
  // src/components/chat/ChatMessageList.tsx
6562
6964
  import { jsx as jsx49, jsxs as jsxs29 } from "react/jsx-runtime";
6563
- var ChatMessageList = React37.forwardRef(
6965
+ var ChatMessageList = React38.forwardRef(
6564
6966
  ({
6565
6967
  messages,
6566
6968
  showTypingIndicator = false,
@@ -6573,21 +6975,22 @@ var ChatMessageList = React37.forwardRef(
6573
6975
  nearBottomThreshold = 200
6574
6976
  }, ref) => {
6575
6977
  const theme = useTheme();
6576
- const listRef = React37.useRef(null);
6577
- const nearBottomRef = React37.useRef(true);
6578
- const initialScrollDoneRef = React37.useRef(false);
6579
- const lastMessageIdRef = React37.useRef(null);
6580
- const data = React37.useMemo(() => {
6978
+ const listRef = React38.useRef(null);
6979
+ const nearBottomRef = React38.useRef(true);
6980
+ const initialScrollDoneRef = React38.useRef(false);
6981
+ const lastMessageIdRef = React38.useRef(null);
6982
+ const data = React38.useMemo(() => {
6581
6983
  return [...messages].reverse();
6582
6984
  }, [messages]);
6583
6985
  const lastMessageId = messages.length > 0 ? messages[messages.length - 1].id : null;
6584
- const scrollToBottom = React37.useCallback((options) => {
6986
+ const keyExtractor = React38.useCallback((m) => m.id, []);
6987
+ const scrollToBottom = React38.useCallback((options) => {
6585
6988
  var _a;
6586
6989
  const animated = (options == null ? void 0 : options.animated) ?? true;
6587
6990
  (_a = listRef.current) == null ? void 0 : _a.scrollToOffset({ offset: 0, animated });
6588
6991
  }, []);
6589
- React37.useImperativeHandle(ref, () => ({ scrollToBottom }), [scrollToBottom]);
6590
- const handleScroll = React37.useCallback(
6992
+ React38.useImperativeHandle(ref, () => ({ scrollToBottom }), [scrollToBottom]);
6993
+ const handleScroll = React38.useCallback(
6591
6994
  (e) => {
6592
6995
  const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;
6593
6996
  const distanceFromBottom = Math.max(contentOffset.y - Math.max(bottomInset, 0), 0);
@@ -6599,7 +7002,7 @@ var ChatMessageList = React37.forwardRef(
6599
7002
  },
6600
7003
  [bottomInset, nearBottomThreshold, onNearBottomChange]
6601
7004
  );
6602
- React37.useEffect(() => {
7005
+ React38.useEffect(() => {
6603
7006
  if (!initialScrollDoneRef.current) return;
6604
7007
  const lastId = messages.length > 0 ? messages[messages.length - 1].id : null;
6605
7008
  const prevLastId = lastMessageIdRef.current;
@@ -6609,54 +7012,68 @@ var ChatMessageList = React37.forwardRef(
6609
7012
  const id = requestAnimationFrame(() => scrollToBottom({ animated: true }));
6610
7013
  return () => cancelAnimationFrame(id);
6611
7014
  }, [messages, scrollToBottom]);
6612
- React37.useEffect(() => {
7015
+ React38.useEffect(() => {
6613
7016
  if (showTypingIndicator && nearBottomRef.current) {
6614
7017
  const id = requestAnimationFrame(() => scrollToBottom({ animated: true }));
6615
7018
  return () => cancelAnimationFrame(id);
6616
7019
  }
6617
7020
  return void 0;
6618
7021
  }, [showTypingIndicator, scrollToBottom]);
7022
+ const handleContentSizeChange = React38.useCallback(() => {
7023
+ if (initialScrollDoneRef.current) return;
7024
+ initialScrollDoneRef.current = true;
7025
+ lastMessageIdRef.current = messages.length > 0 ? messages[messages.length - 1].id : null;
7026
+ nearBottomRef.current = true;
7027
+ onNearBottomChange == null ? void 0 : onNearBottomChange(true);
7028
+ requestAnimationFrame(() => scrollToBottom({ animated: false }));
7029
+ }, [messages, onNearBottomChange, scrollToBottom]);
7030
+ const contentContainerStyle = React38.useMemo(
7031
+ () => [
7032
+ {
7033
+ paddingHorizontal: theme.spacing.lg,
7034
+ paddingVertical: theme.spacing.sm
7035
+ },
7036
+ contentStyle
7037
+ ],
7038
+ [contentStyle, theme.spacing.lg, theme.spacing.sm]
7039
+ );
7040
+ const renderSeparator = React38.useCallback(() => /* @__PURE__ */ jsx49(View36, { style: { height: theme.spacing.sm } }), [theme.spacing.sm]);
7041
+ const listHeader = React38.useMemo(
7042
+ () => /* @__PURE__ */ jsxs29(View36, { children: [
7043
+ showTypingIndicator ? /* @__PURE__ */ jsx49(View36, { style: { marginTop: theme.spacing.sm, alignSelf: "flex-start", paddingHorizontal: theme.spacing.lg }, children: /* @__PURE__ */ jsx49(TypingIndicator, {}) }) : null,
7044
+ bottomInset > 0 ? /* @__PURE__ */ jsx49(View36, { style: { height: bottomInset } }) : null
7045
+ ] }),
7046
+ [bottomInset, showTypingIndicator, theme.spacing.lg, theme.spacing.sm]
7047
+ );
7048
+ const renderItem = React38.useCallback(
7049
+ ({ item }) => /* @__PURE__ */ jsx49(
7050
+ ChatMessageBubble,
7051
+ {
7052
+ message: item,
7053
+ renderContent: renderMessageContent,
7054
+ isLast: Boolean(lastMessageId && item.id === lastMessageId),
7055
+ retrying: (isRetryingMessage == null ? void 0 : isRetryingMessage(item.id)) ?? false,
7056
+ onRetryMessage
7057
+ }
7058
+ ),
7059
+ [isRetryingMessage, lastMessageId, onRetryMessage, renderMessageContent]
7060
+ );
6619
7061
  return /* @__PURE__ */ jsx49(
6620
7062
  BottomSheetFlatList,
6621
7063
  {
6622
7064
  ref: listRef,
6623
7065
  inverted: true,
6624
7066
  data,
6625
- keyExtractor: (m) => m.id,
7067
+ keyExtractor,
6626
7068
  keyboardShouldPersistTaps: "handled",
6627
7069
  onScroll: handleScroll,
6628
7070
  scrollEventThrottle: 16,
6629
7071
  showsVerticalScrollIndicator: false,
6630
- onContentSizeChange: () => {
6631
- if (initialScrollDoneRef.current) return;
6632
- initialScrollDoneRef.current = true;
6633
- lastMessageIdRef.current = messages.length > 0 ? messages[messages.length - 1].id : null;
6634
- nearBottomRef.current = true;
6635
- onNearBottomChange == null ? void 0 : onNearBottomChange(true);
6636
- requestAnimationFrame(() => scrollToBottom({ animated: false }));
6637
- },
6638
- contentContainerStyle: [
6639
- {
6640
- paddingHorizontal: theme.spacing.lg,
6641
- paddingVertical: theme.spacing.sm
6642
- },
6643
- contentStyle
6644
- ],
6645
- ItemSeparatorComponent: () => /* @__PURE__ */ jsx49(View36, { style: { height: theme.spacing.sm } }),
6646
- renderItem: ({ item }) => /* @__PURE__ */ jsx49(
6647
- ChatMessageBubble,
6648
- {
6649
- message: item,
6650
- renderContent: renderMessageContent,
6651
- isLast: Boolean(lastMessageId && item.id === lastMessageId),
6652
- retrying: (isRetryingMessage == null ? void 0 : isRetryingMessage(item.id)) ?? false,
6653
- onRetry: onRetryMessage ? () => onRetryMessage(item.id) : void 0
6654
- }
6655
- ),
6656
- ListHeaderComponent: /* @__PURE__ */ jsxs29(View36, { children: [
6657
- showTypingIndicator ? /* @__PURE__ */ jsx49(View36, { style: { marginTop: theme.spacing.sm, alignSelf: "flex-start", paddingHorizontal: theme.spacing.lg }, children: /* @__PURE__ */ jsx49(TypingIndicator, {}) }) : null,
6658
- bottomInset > 0 ? /* @__PURE__ */ jsx49(View36, { style: { height: bottomInset } }) : null
6659
- ] })
7072
+ onContentSizeChange: handleContentSizeChange,
7073
+ contentContainerStyle,
7074
+ ItemSeparatorComponent: renderSeparator,
7075
+ renderItem,
7076
+ ListHeaderComponent: listHeader
6660
7077
  }
6661
7078
  );
6662
7079
  }
@@ -6683,22 +7100,22 @@ function ChatPage({
6683
7100
  }) {
6684
7101
  const theme = useTheme();
6685
7102
  const insets = useSafeAreaInsets4();
6686
- const [composerHeight, setComposerHeight] = React38.useState(0);
6687
- const [composerTopHeight, setComposerTopHeight] = React38.useState(0);
6688
- const footerBottomPadding = Platform9.OS === "ios" ? insets.bottom - 24 : insets.bottom + 10;
7103
+ const [composerHeight, setComposerHeight] = React39.useState(0);
7104
+ const [composerTopHeight, setComposerTopHeight] = React39.useState(0);
7105
+ const footerBottomPadding = Platform10.OS === "ios" ? insets.bottom - 24 : insets.bottom + 10;
6689
7106
  const totalComposerHeight = composerHeight + composerTopHeight;
6690
7107
  const overlayBottom = totalComposerHeight + footerBottomPadding + theme.spacing.lg;
6691
7108
  const bottomInset = totalComposerHeight + footerBottomPadding + theme.spacing.xl;
6692
- const resolvedOverlay = React38.useMemo(() => {
7109
+ const resolvedOverlay = React39.useMemo(() => {
6693
7110
  var _a;
6694
7111
  if (!overlay) return null;
6695
- if (!React38.isValidElement(overlay)) return overlay;
7112
+ if (!React39.isValidElement(overlay)) return overlay;
6696
7113
  const prevStyle = (_a = overlay.props) == null ? void 0 : _a.style;
6697
- return React38.cloneElement(overlay, {
7114
+ return React39.cloneElement(overlay, {
6698
7115
  style: [prevStyle, { bottom: overlayBottom }]
6699
7116
  });
6700
7117
  }, [overlay, overlayBottom]);
6701
- React38.useEffect(() => {
7118
+ React39.useEffect(() => {
6702
7119
  if (composerTop) return;
6703
7120
  setComposerTopHeight(0);
6704
7121
  }, [composerTop]);
@@ -6765,15 +7182,15 @@ function ChatPage({
6765
7182
  }
6766
7183
 
6767
7184
  // src/components/chat/ScrollToBottomButton.tsx
6768
- import * as React39 from "react";
7185
+ import * as React40 from "react";
6769
7186
  import { Pressable as Pressable14, View as View38 } from "react-native";
6770
7187
  import Animated11, { Easing as Easing2, useAnimatedStyle as useAnimatedStyle2, useSharedValue as useSharedValue2, withTiming as withTiming2 } from "react-native-reanimated";
6771
7188
  import { jsx as jsx51 } from "react/jsx-runtime";
6772
7189
  function ScrollToBottomButton({ visible, onPress, children, style }) {
6773
7190
  const theme = useTheme();
6774
7191
  const progress = useSharedValue2(visible ? 1 : 0);
6775
- const [pressed, setPressed] = React39.useState(false);
6776
- React39.useEffect(() => {
7192
+ const [pressed, setPressed] = React40.useState(false);
7193
+ React40.useEffect(() => {
6777
7194
  progress.value = withTiming2(visible ? 1 : 0, { duration: 200, easing: Easing2.out(Easing2.ease) });
6778
7195
  }, [progress, visible]);
6779
7196
  const animStyle = useAnimatedStyle2(() => ({
@@ -6908,16 +7325,16 @@ function ForkNoticeBanner({ isOwner = true, title, description, style }) {
6908
7325
  }
6909
7326
 
6910
7327
  // src/components/chat/ChatQueue.tsx
6911
- import * as React40 from "react";
7328
+ import * as React41 from "react";
6912
7329
  import { ActivityIndicator as ActivityIndicator8, Pressable as Pressable15, View as View41 } from "react-native";
6913
7330
  import { jsx as jsx54, jsxs as jsxs32 } from "react/jsx-runtime";
6914
7331
  function ChatQueue({ items, onRemove }) {
6915
7332
  const theme = useTheme();
6916
- const [expanded, setExpanded] = React40.useState({});
6917
- const [canExpand, setCanExpand] = React40.useState({});
6918
- const [collapsedText, setCollapsedText] = React40.useState({});
6919
- const [removing, setRemoving] = React40.useState({});
6920
- const buildCollapsedText = React40.useCallback((lines) => {
7333
+ const [expanded, setExpanded] = React41.useState({});
7334
+ const [canExpand, setCanExpand] = React41.useState({});
7335
+ const [collapsedText, setCollapsedText] = React41.useState({});
7336
+ const [removing, setRemoving] = React41.useState({});
7337
+ const buildCollapsedText = React41.useCallback((lines) => {
6921
7338
  var _a, _b;
6922
7339
  const line1 = ((_a = lines[0]) == null ? void 0 : _a.text) ?? "";
6923
7340
  const line2 = ((_b = lines[1]) == null ? void 0 : _b.text) ?? "";
@@ -6933,7 +7350,7 @@ function ChatQueue({ items, onRemove }) {
6933
7350
  return `${line1}
6934
7351
  ${trimmedLine2}\u2026 `;
6935
7352
  }, []);
6936
- React40.useEffect(() => {
7353
+ React41.useEffect(() => {
6937
7354
  if (items.length === 0) return;
6938
7355
  const ids = new Set(items.map((item) => item.id));
6939
7356
  setExpanded((prev) => Object.fromEntries(Object.entries(prev).filter(([id]) => ids.has(id))));
@@ -7053,8 +7470,137 @@ ${trimmedLine2}\u2026 `;
7053
7470
  );
7054
7471
  }
7055
7472
 
7056
- // src/studio/ui/ChatPanel.tsx
7473
+ // src/components/chat/AgentProgressCard.tsx
7474
+ import { View as View42 } from "react-native";
7057
7475
  import { jsx as jsx55, jsxs as jsxs33 } from "react/jsx-runtime";
7476
+ function titleForPhase(phase) {
7477
+ if (phase === "planning") return "Planning";
7478
+ if (phase === "reasoning") return "Reasoning";
7479
+ if (phase === "analyzing") return "Analyzing";
7480
+ if (phase === "editing") return "Editing";
7481
+ if (phase === "executing") return "Executing";
7482
+ if (phase === "validating") return "Validating";
7483
+ if (phase === "finalizing") return "Finalizing";
7484
+ if (phase === "working") return "Working";
7485
+ return "Working";
7486
+ }
7487
+ function titleForStatus(status) {
7488
+ if (status === "succeeded") return "Completed";
7489
+ if (status === "failed") return "Failed";
7490
+ if (status === "cancelled") return "Cancelled";
7491
+ return "In Progress";
7492
+ }
7493
+ function AgentProgressCard({ progress }) {
7494
+ const theme = useTheme();
7495
+ const statusLabel = titleForStatus(progress.status);
7496
+ const phaseLabel = titleForPhase(progress.phase);
7497
+ const subtitle = progress.latestMessage || `Agent is ${phaseLabel.toLowerCase()}...`;
7498
+ const todo = progress.todoSummary;
7499
+ return /* @__PURE__ */ jsxs33(
7500
+ View42,
7501
+ {
7502
+ style: {
7503
+ borderWidth: 1,
7504
+ borderColor: theme.colors.border,
7505
+ borderRadius: theme.radii.lg,
7506
+ marginHorizontal: theme.spacing.md,
7507
+ padding: theme.spacing.md,
7508
+ backgroundColor: withAlpha(theme.colors.surface, theme.scheme === "dark" ? 0.84 : 0.94)
7509
+ },
7510
+ children: [
7511
+ /* @__PURE__ */ jsxs33(View42, { style: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", marginBottom: 4 }, children: [
7512
+ /* @__PURE__ */ jsx55(Text, { variant: "caption", children: statusLabel }),
7513
+ /* @__PURE__ */ jsx55(Text, { variant: "captionMuted", children: phaseLabel })
7514
+ ] }),
7515
+ /* @__PURE__ */ jsx55(Text, { variant: "bodyMuted", children: subtitle }),
7516
+ progress.changedFilesCount > 0 ? /* @__PURE__ */ jsxs33(Text, { variant: "captionMuted", style: { marginTop: 8 }, children: [
7517
+ "Updated files: ",
7518
+ progress.changedFilesCount
7519
+ ] }) : null,
7520
+ progress.recentFiles.length > 0 ? /* @__PURE__ */ jsx55(View42, { style: { marginTop: 6 }, children: progress.recentFiles.map((path) => /* @__PURE__ */ jsxs33(Text, { variant: "captionMuted", numberOfLines: 1, children: [
7521
+ "\u2022 ",
7522
+ path
7523
+ ] }, path)) }) : null,
7524
+ todo ? /* @__PURE__ */ jsxs33(Text, { variant: "captionMuted", style: { marginTop: 8 }, children: [
7525
+ "Todos: ",
7526
+ todo.completed,
7527
+ "/",
7528
+ todo.total,
7529
+ " complete",
7530
+ todo.currentTask ? ` \u2022 ${todo.currentTask}` : ""
7531
+ ] }) : null
7532
+ ]
7533
+ }
7534
+ );
7535
+ }
7536
+
7537
+ // src/components/chat/BundleProgressCard.tsx
7538
+ import { View as View43 } from "react-native";
7539
+ import { jsx as jsx56, jsxs as jsxs34 } from "react/jsx-runtime";
7540
+ function titleForStatus2(status) {
7541
+ if (status === "succeeded") return "Completed";
7542
+ if (status === "failed") return "Failed";
7543
+ return "In Progress";
7544
+ }
7545
+ function BundleProgressCard({ progress }) {
7546
+ const theme = useTheme();
7547
+ const statusLabel = titleForStatus2(progress.status);
7548
+ const percent = Math.round(Math.max(0, Math.min(1, progress.progressValue)) * 100);
7549
+ const fillColor = progress.status === "failed" ? theme.colors.danger : progress.status === "succeeded" ? theme.colors.success : theme.colors.warning;
7550
+ const detail = progress.errorMessage || progress.phaseLabel;
7551
+ return /* @__PURE__ */ jsxs34(
7552
+ View43,
7553
+ {
7554
+ accessible: true,
7555
+ accessibilityRole: "progressbar",
7556
+ accessibilityLabel: `Bundle progress ${statusLabel}`,
7557
+ accessibilityValue: { min: 0, max: 100, now: percent, text: `${percent}%` },
7558
+ style: {
7559
+ borderWidth: 1,
7560
+ borderColor: theme.colors.border,
7561
+ borderRadius: theme.radii.lg,
7562
+ marginHorizontal: theme.spacing.md,
7563
+ padding: theme.spacing.md,
7564
+ backgroundColor: withAlpha(theme.colors.surface, theme.scheme === "dark" ? 0.84 : 0.94)
7565
+ },
7566
+ children: [
7567
+ /* @__PURE__ */ jsxs34(View43, { style: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", marginBottom: 8 }, children: [
7568
+ /* @__PURE__ */ jsx56(Text, { variant: "caption", children: statusLabel }),
7569
+ /* @__PURE__ */ jsxs34(Text, { variant: "captionMuted", children: [
7570
+ percent,
7571
+ "%"
7572
+ ] })
7573
+ ] }),
7574
+ /* @__PURE__ */ jsx56(
7575
+ View43,
7576
+ {
7577
+ style: {
7578
+ width: "100%",
7579
+ height: 8,
7580
+ borderRadius: 999,
7581
+ backgroundColor: withAlpha(theme.colors.border, theme.scheme === "dark" ? 0.5 : 0.6),
7582
+ overflow: "hidden"
7583
+ },
7584
+ children: /* @__PURE__ */ jsx56(
7585
+ View43,
7586
+ {
7587
+ style: {
7588
+ width: `${percent}%`,
7589
+ height: "100%",
7590
+ backgroundColor: fillColor
7591
+ }
7592
+ }
7593
+ )
7594
+ }
7595
+ ),
7596
+ /* @__PURE__ */ jsx56(Text, { variant: "captionMuted", numberOfLines: 1, style: { marginTop: 8, minHeight: 16 }, children: detail })
7597
+ ]
7598
+ }
7599
+ );
7600
+ }
7601
+
7602
+ // src/studio/ui/ChatPanel.tsx
7603
+ import { jsx as jsx57, jsxs as jsxs35 } from "react/jsx-runtime";
7058
7604
  function ChatPanel({
7059
7605
  title = "Chat",
7060
7606
  messages,
@@ -7075,11 +7621,13 @@ function ChatPanel({
7075
7621
  onRetryMessage,
7076
7622
  isRetryingMessage,
7077
7623
  queueItems = [],
7078
- onRemoveQueueItem
7624
+ onRemoveQueueItem,
7625
+ progress = null
7079
7626
  }) {
7080
- const listRef = React41.useRef(null);
7081
- const [nearBottom, setNearBottom] = React41.useState(true);
7082
- const handleSend = React41.useCallback(
7627
+ const theme = useTheme();
7628
+ const listRef = React42.useRef(null);
7629
+ const [nearBottom, setNearBottom] = React42.useState(true);
7630
+ const handleSend = React42.useCallback(
7083
7631
  async (text, composerAttachments) => {
7084
7632
  const all = composerAttachments ?? attachments;
7085
7633
  await onSend(text, all.length > 0 ? all : void 0);
@@ -7093,25 +7641,25 @@ function ChatPanel({
7093
7641
  },
7094
7642
  [attachments, nearBottom, onClearAttachments, onSend]
7095
7643
  );
7096
- const handleScrollToBottom = React41.useCallback(() => {
7644
+ const handleScrollToBottom = React42.useCallback(() => {
7097
7645
  var _a;
7098
7646
  (_a = listRef.current) == null ? void 0 : _a.scrollToBottom({ animated: true });
7099
7647
  }, []);
7100
- const header = /* @__PURE__ */ jsx55(
7648
+ const header = /* @__PURE__ */ jsx57(
7101
7649
  ChatHeader,
7102
7650
  {
7103
- left: /* @__PURE__ */ jsxs33(View42, { style: { flexDirection: "row", alignItems: "center" }, children: [
7104
- /* @__PURE__ */ jsx55(StudioSheetHeaderIconButton, { onPress: onBack, accessibilityLabel: "Back", style: { marginRight: 8 }, children: /* @__PURE__ */ jsx55(IconBack, { size: 20, colorToken: "floatingContent" }) }),
7105
- onNavigateHome ? /* @__PURE__ */ jsx55(StudioSheetHeaderIconButton, { onPress: onNavigateHome, accessibilityLabel: "Home", children: /* @__PURE__ */ jsx55(IconHome, { size: 20, colorToken: "floatingContent" }) }) : null
7651
+ left: /* @__PURE__ */ jsxs35(View44, { style: { flexDirection: "row", alignItems: "center" }, children: [
7652
+ /* @__PURE__ */ jsx57(StudioSheetHeaderIconButton, { onPress: onBack, accessibilityLabel: "Back", style: { marginRight: 8 }, children: /* @__PURE__ */ jsx57(IconBack, { size: 20, colorToken: "floatingContent" }) }),
7653
+ onNavigateHome ? /* @__PURE__ */ jsx57(StudioSheetHeaderIconButton, { onPress: onNavigateHome, accessibilityLabel: "Home", children: /* @__PURE__ */ jsx57(IconHome, { size: 20, colorToken: "floatingContent" }) }) : null
7106
7654
  ] }),
7107
- right: /* @__PURE__ */ jsxs33(View42, { style: { flexDirection: "row", alignItems: "center" }, children: [
7108
- onStartDraw ? /* @__PURE__ */ jsx55(StudioSheetHeaderIconButton, { onPress: onStartDraw, accessibilityLabel: "Draw", intent: "danger", style: { marginRight: 8 }, children: /* @__PURE__ */ jsx55(IconDraw, { size: 20, colorToken: "onDanger" }) }) : null,
7109
- /* @__PURE__ */ jsx55(StudioSheetHeaderIconButton, { onPress: onClose, accessibilityLabel: "Close", children: /* @__PURE__ */ jsx55(IconClose, { size: 20, colorToken: "floatingContent" }) })
7655
+ right: /* @__PURE__ */ jsxs35(View44, { style: { flexDirection: "row", alignItems: "center" }, children: [
7656
+ onStartDraw ? /* @__PURE__ */ jsx57(StudioSheetHeaderIconButton, { onPress: onStartDraw, accessibilityLabel: "Draw", intent: "danger", style: { marginRight: 8 }, children: /* @__PURE__ */ jsx57(IconDraw, { size: 20, colorToken: "onDanger" }) }) : null,
7657
+ /* @__PURE__ */ jsx57(StudioSheetHeaderIconButton, { onPress: onClose, accessibilityLabel: "Close", children: /* @__PURE__ */ jsx57(IconClose, { size: 20, colorToken: "floatingContent" }) })
7110
7658
  ] }),
7111
7659
  center: null
7112
7660
  }
7113
7661
  );
7114
- const topBanner = shouldForkOnEdit ? /* @__PURE__ */ jsx55(
7662
+ const topBanner = shouldForkOnEdit ? /* @__PURE__ */ jsx57(
7115
7663
  ForkNoticeBanner,
7116
7664
  {
7117
7665
  isOwner: !shouldForkOnEdit,
@@ -7120,18 +7668,22 @@ function ChatPanel({
7120
7668
  ) : null;
7121
7669
  const showMessagesLoading = Boolean(loading) && messages.length === 0 || forking;
7122
7670
  if (showMessagesLoading) {
7123
- return /* @__PURE__ */ jsxs33(View42, { style: { flex: 1 }, children: [
7124
- /* @__PURE__ */ jsx55(View42, { children: header }),
7125
- topBanner ? /* @__PURE__ */ jsx55(View42, { style: { paddingHorizontal: 16, paddingTop: 8 }, children: topBanner }) : null,
7126
- /* @__PURE__ */ jsxs33(View42, { style: { flex: 1, alignItems: "center", justifyContent: "center", paddingHorizontal: 24, paddingVertical: 12 }, children: [
7127
- /* @__PURE__ */ jsx55(ActivityIndicator9, {}),
7128
- /* @__PURE__ */ jsx55(View42, { style: { height: 12 } }),
7129
- /* @__PURE__ */ jsx55(Text, { variant: "bodyMuted", children: forking ? "Creating your copy\u2026" : "Loading messages\u2026" })
7671
+ return /* @__PURE__ */ jsxs35(View44, { style: { flex: 1 }, children: [
7672
+ /* @__PURE__ */ jsx57(View44, { children: header }),
7673
+ topBanner ? /* @__PURE__ */ jsx57(View44, { style: { paddingHorizontal: 16, paddingTop: 8 }, children: topBanner }) : null,
7674
+ /* @__PURE__ */ jsxs35(View44, { style: { flex: 1, alignItems: "center", justifyContent: "center", paddingHorizontal: 24, paddingVertical: 12 }, children: [
7675
+ /* @__PURE__ */ jsx57(ActivityIndicator9, {}),
7676
+ /* @__PURE__ */ jsx57(View44, { style: { height: 12 } }),
7677
+ /* @__PURE__ */ jsx57(Text, { variant: "bodyMuted", children: forking ? "Creating your copy\u2026" : "Loading messages\u2026" })
7130
7678
  ] })
7131
7679
  ] });
7132
7680
  }
7133
- const queueTop = queueItems.length > 0 ? /* @__PURE__ */ jsx55(ChatQueue, { items: queueItems, onRemove: onRemoveQueueItem }) : null;
7134
- return /* @__PURE__ */ jsx55(
7681
+ const bundleProgress = (progress == null ? void 0 : progress.bundle) ?? null;
7682
+ const queueTop = progress || queueItems.length > 0 ? /* @__PURE__ */ jsxs35(View44, { style: { gap: theme.spacing.sm }, children: [
7683
+ progress ? bundleProgress ? /* @__PURE__ */ jsx57(BundleProgressCard, { progress: bundleProgress }) : /* @__PURE__ */ jsx57(AgentProgressCard, { progress }) : null,
7684
+ queueItems.length > 0 ? /* @__PURE__ */ jsx57(ChatQueue, { items: queueItems, onRemove: onRemoveQueueItem }) : null
7685
+ ] }) : null;
7686
+ return /* @__PURE__ */ jsx57(
7135
7687
  ChatPage,
7136
7688
  {
7137
7689
  header,
@@ -7144,13 +7696,13 @@ function ChatPanel({
7144
7696
  composerHorizontalPadding: 0,
7145
7697
  listRef,
7146
7698
  onNearBottomChange: setNearBottom,
7147
- overlay: /* @__PURE__ */ jsx55(
7699
+ overlay: /* @__PURE__ */ jsx57(
7148
7700
  ScrollToBottomButton,
7149
7701
  {
7150
7702
  visible: !nearBottom,
7151
7703
  onPress: handleScrollToBottom,
7152
7704
  style: { bottom: 80 },
7153
- children: /* @__PURE__ */ jsx55(IconArrowDown, { size: 20, colorToken: "floatingContent" })
7705
+ children: /* @__PURE__ */ jsx57(IconArrowDown, { size: 20, colorToken: "floatingContent" })
7154
7706
  }
7155
7707
  ),
7156
7708
  composer: {
@@ -7170,16 +7722,16 @@ function ChatPanel({
7170
7722
  }
7171
7723
 
7172
7724
  // src/components/dialogs/ConfirmMergeRequestDialog.tsx
7173
- import * as React42 from "react";
7174
- import { Pressable as Pressable17, View as View44 } from "react-native";
7725
+ import * as React43 from "react";
7726
+ import { Pressable as Pressable17, View as View46 } from "react-native";
7175
7727
 
7176
7728
  // src/components/primitives/Modal.tsx
7177
7729
  import {
7178
7730
  Modal as RNModal,
7179
7731
  Pressable as Pressable16,
7180
- View as View43
7732
+ View as View45
7181
7733
  } from "react-native";
7182
- import { jsx as jsx56, jsxs as jsxs34 } from "react/jsx-runtime";
7734
+ import { jsx as jsx58, jsxs as jsxs36 } from "react/jsx-runtime";
7183
7735
  function Modal2({
7184
7736
  visible,
7185
7737
  onRequestClose,
@@ -7188,15 +7740,15 @@ function Modal2({
7188
7740
  contentStyle
7189
7741
  }) {
7190
7742
  const theme = useTheme();
7191
- return /* @__PURE__ */ jsx56(
7743
+ return /* @__PURE__ */ jsx58(
7192
7744
  RNModal,
7193
7745
  {
7194
7746
  visible,
7195
7747
  transparent: true,
7196
7748
  animationType: "fade",
7197
7749
  onRequestClose,
7198
- children: /* @__PURE__ */ jsxs34(View43, { style: { flex: 1, backgroundColor: theme.colors.backdrop, justifyContent: "center", padding: theme.spacing.lg }, children: [
7199
- /* @__PURE__ */ jsx56(
7750
+ children: /* @__PURE__ */ jsxs36(View45, { style: { flex: 1, backgroundColor: theme.colors.backdrop, justifyContent: "center", padding: theme.spacing.lg }, children: [
7751
+ /* @__PURE__ */ jsx58(
7200
7752
  Pressable16,
7201
7753
  {
7202
7754
  accessibilityRole: "button",
@@ -7204,14 +7756,14 @@ function Modal2({
7204
7756
  style: { position: "absolute", inset: 0 }
7205
7757
  }
7206
7758
  ),
7207
- /* @__PURE__ */ jsx56(Card, { variant: "surfaceRaised", padded: true, style: [{ borderRadius: theme.radii.xl }, contentStyle], children })
7759
+ /* @__PURE__ */ jsx58(Card, { variant: "surfaceRaised", padded: true, style: [{ borderRadius: theme.radii.xl }, contentStyle], children })
7208
7760
  ] })
7209
7761
  }
7210
7762
  );
7211
7763
  }
7212
7764
 
7213
7765
  // src/components/dialogs/ConfirmMergeRequestDialog.tsx
7214
- import { jsx as jsx57, jsxs as jsxs35 } from "react/jsx-runtime";
7766
+ import { jsx as jsx59, jsxs as jsxs37 } from "react/jsx-runtime";
7215
7767
  function ConfirmMergeRequestDialog({
7216
7768
  visible,
7217
7769
  onOpenChange,
@@ -7222,14 +7774,14 @@ function ConfirmMergeRequestDialog({
7222
7774
  onTestFirst
7223
7775
  }) {
7224
7776
  const theme = useTheme();
7225
- const close = React42.useCallback(() => onOpenChange(false), [onOpenChange]);
7777
+ const close = React43.useCallback(() => onOpenChange(false), [onOpenChange]);
7226
7778
  const canConfirm = Boolean(mergeRequest) && !approveDisabled;
7227
- const handleConfirm = React42.useCallback(() => {
7779
+ const handleConfirm = React43.useCallback(() => {
7228
7780
  if (!mergeRequest) return;
7229
7781
  onOpenChange(false);
7230
7782
  void onConfirm();
7231
7783
  }, [mergeRequest, onConfirm, onOpenChange]);
7232
- const handleTestFirst = React42.useCallback(() => {
7784
+ const handleTestFirst = React43.useCallback(() => {
7233
7785
  if (!mergeRequest) return;
7234
7786
  onOpenChange(false);
7235
7787
  void onTestFirst(mergeRequest);
@@ -7241,7 +7793,7 @@ function ConfirmMergeRequestDialog({
7241
7793
  justifyContent: "center",
7242
7794
  alignSelf: "stretch"
7243
7795
  };
7244
- return /* @__PURE__ */ jsxs35(
7796
+ return /* @__PURE__ */ jsxs37(
7245
7797
  Modal2,
7246
7798
  {
7247
7799
  visible,
@@ -7252,7 +7804,7 @@ function ConfirmMergeRequestDialog({
7252
7804
  backgroundColor: theme.colors.background
7253
7805
  },
7254
7806
  children: [
7255
- /* @__PURE__ */ jsx57(View44, { children: /* @__PURE__ */ jsx57(
7807
+ /* @__PURE__ */ jsx59(View46, { children: /* @__PURE__ */ jsx59(
7256
7808
  Text,
7257
7809
  {
7258
7810
  style: {
@@ -7264,9 +7816,9 @@ function ConfirmMergeRequestDialog({
7264
7816
  children: "Are you sure you want to approve this merge request?"
7265
7817
  }
7266
7818
  ) }),
7267
- /* @__PURE__ */ jsxs35(View44, { style: { marginTop: 16 }, children: [
7268
- /* @__PURE__ */ jsx57(
7269
- View44,
7819
+ /* @__PURE__ */ jsxs37(View46, { style: { marginTop: 16 }, children: [
7820
+ /* @__PURE__ */ jsx59(
7821
+ View46,
7270
7822
  {
7271
7823
  style: [
7272
7824
  fullWidthButtonBase,
@@ -7275,7 +7827,7 @@ function ConfirmMergeRequestDialog({
7275
7827
  opacity: canConfirm ? 1 : 0.5
7276
7828
  }
7277
7829
  ],
7278
- children: /* @__PURE__ */ jsx57(
7830
+ children: /* @__PURE__ */ jsx59(
7279
7831
  Pressable17,
7280
7832
  {
7281
7833
  accessibilityRole: "button",
@@ -7283,14 +7835,14 @@ function ConfirmMergeRequestDialog({
7283
7835
  disabled: !canConfirm,
7284
7836
  onPress: handleConfirm,
7285
7837
  style: [fullWidthButtonBase, { flex: 1 }],
7286
- children: /* @__PURE__ */ jsx57(Text, { style: { textAlign: "center", color: theme.colors.onPrimary }, children: "Approve Merge" })
7838
+ children: /* @__PURE__ */ jsx59(Text, { style: { textAlign: "center", color: theme.colors.onPrimary }, children: "Approve Merge" })
7287
7839
  }
7288
7840
  )
7289
7841
  }
7290
7842
  ),
7291
- /* @__PURE__ */ jsx57(View44, { style: { height: 8 } }),
7292
- /* @__PURE__ */ jsx57(
7293
- View44,
7843
+ /* @__PURE__ */ jsx59(View46, { style: { height: 8 } }),
7844
+ /* @__PURE__ */ jsx59(
7845
+ View46,
7294
7846
  {
7295
7847
  style: [
7296
7848
  fullWidthButtonBase,
@@ -7301,7 +7853,7 @@ function ConfirmMergeRequestDialog({
7301
7853
  opacity: isBuilding || !mergeRequest ? 0.5 : 1
7302
7854
  }
7303
7855
  ],
7304
- children: /* @__PURE__ */ jsx57(
7856
+ children: /* @__PURE__ */ jsx59(
7305
7857
  Pressable17,
7306
7858
  {
7307
7859
  accessibilityRole: "button",
@@ -7309,14 +7861,14 @@ function ConfirmMergeRequestDialog({
7309
7861
  disabled: isBuilding || !mergeRequest,
7310
7862
  onPress: handleTestFirst,
7311
7863
  style: [fullWidthButtonBase, { flex: 1 }],
7312
- children: /* @__PURE__ */ jsx57(Text, { style: { textAlign: "center", color: theme.colors.text }, children: isBuilding ? "Preparing\u2026" : "Test edits first" })
7864
+ children: /* @__PURE__ */ jsx59(Text, { style: { textAlign: "center", color: theme.colors.text }, children: isBuilding ? "Preparing\u2026" : "Test edits first" })
7313
7865
  }
7314
7866
  )
7315
7867
  }
7316
7868
  ),
7317
- /* @__PURE__ */ jsx57(View44, { style: { height: 8 } }),
7318
- /* @__PURE__ */ jsx57(
7319
- View44,
7869
+ /* @__PURE__ */ jsx59(View46, { style: { height: 8 } }),
7870
+ /* @__PURE__ */ jsx59(
7871
+ View46,
7320
7872
  {
7321
7873
  style: [
7322
7874
  fullWidthButtonBase,
@@ -7326,14 +7878,14 @@ function ConfirmMergeRequestDialog({
7326
7878
  borderColor: theme.colors.border
7327
7879
  }
7328
7880
  ],
7329
- children: /* @__PURE__ */ jsx57(
7881
+ children: /* @__PURE__ */ jsx59(
7330
7882
  Pressable17,
7331
7883
  {
7332
7884
  accessibilityRole: "button",
7333
7885
  accessibilityLabel: "Cancel",
7334
7886
  onPress: close,
7335
7887
  style: [fullWidthButtonBase, { flex: 1 }],
7336
- children: /* @__PURE__ */ jsx57(Text, { style: { textAlign: "center", color: theme.colors.text }, children: "Cancel" })
7888
+ children: /* @__PURE__ */ jsx59(Text, { style: { textAlign: "center", color: theme.colors.text }, children: "Cancel" })
7337
7889
  }
7338
7890
  )
7339
7891
  }
@@ -7345,7 +7897,7 @@ function ConfirmMergeRequestDialog({
7345
7897
  }
7346
7898
 
7347
7899
  // src/studio/ui/ConfirmMergeFlow.tsx
7348
- import { jsx as jsx58 } from "react/jsx-runtime";
7900
+ import { jsx as jsx60 } from "react/jsx-runtime";
7349
7901
  function ConfirmMergeFlow({
7350
7902
  visible,
7351
7903
  onOpenChange,
@@ -7356,7 +7908,7 @@ function ConfirmMergeFlow({
7356
7908
  onConfirm,
7357
7909
  onTestFirst
7358
7910
  }) {
7359
- return /* @__PURE__ */ jsx58(
7911
+ return /* @__PURE__ */ jsx60(
7360
7912
  ConfirmMergeRequestDialog,
7361
7913
  {
7362
7914
  visible,
@@ -7378,7 +7930,7 @@ function ConfirmMergeFlow({
7378
7930
  }
7379
7931
 
7380
7932
  // src/studio/hooks/useOptimisticChatMessages.ts
7381
- import * as React43 from "react";
7933
+ import * as React44 from "react";
7382
7934
  function makeOptimisticId() {
7383
7935
  return `optimistic:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 10)}`;
7384
7936
  }
@@ -7417,11 +7969,11 @@ function useOptimisticChatMessages({
7417
7969
  chatMessages,
7418
7970
  onSendChat
7419
7971
  }) {
7420
- const [optimisticChat, setOptimisticChat] = React43.useState([]);
7421
- React43.useEffect(() => {
7972
+ const [optimisticChat, setOptimisticChat] = React44.useState([]);
7973
+ React44.useEffect(() => {
7422
7974
  setOptimisticChat([]);
7423
7975
  }, [threadId]);
7424
- const messages = React43.useMemo(() => {
7976
+ const messages = React44.useMemo(() => {
7425
7977
  if (!optimisticChat || optimisticChat.length === 0) return chatMessages;
7426
7978
  const unresolved = optimisticChat.filter((o) => !isOptimisticResolvedByServer(chatMessages, o));
7427
7979
  if (unresolved.length === 0) return chatMessages;
@@ -7437,7 +7989,7 @@ function useOptimisticChatMessages({
7437
7989
  merged.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
7438
7990
  return merged;
7439
7991
  }, [chatMessages, optimisticChat]);
7440
- React43.useEffect(() => {
7992
+ React44.useEffect(() => {
7441
7993
  if (optimisticChat.length === 0) return;
7442
7994
  setOptimisticChat((prev) => {
7443
7995
  if (prev.length === 0) return prev;
@@ -7445,7 +7997,7 @@ function useOptimisticChatMessages({
7445
7997
  return next.length === prev.length ? prev : next;
7446
7998
  });
7447
7999
  }, [chatMessages, optimisticChat.length]);
7448
- const onSend = React43.useCallback(
8000
+ const onSend = React44.useCallback(
7449
8001
  async (text, attachments) => {
7450
8002
  if (shouldForkOnEdit || disableOptimistic) {
7451
8003
  await onSendChat(text, attachments);
@@ -7473,7 +8025,7 @@ function useOptimisticChatMessages({
7473
8025
  },
7474
8026
  [chatMessages, disableOptimistic, onSendChat, shouldForkOnEdit]
7475
8027
  );
7476
- const onRetry = React43.useCallback(
8028
+ const onRetry = React44.useCallback(
7477
8029
  async (messageId) => {
7478
8030
  if (shouldForkOnEdit || disableOptimistic) return;
7479
8031
  const target = optimisticChat.find((m) => m.id === messageId);
@@ -7497,7 +8049,7 @@ function useOptimisticChatMessages({
7497
8049
  },
7498
8050
  [chatMessages, disableOptimistic, onSendChat, optimisticChat, shouldForkOnEdit]
7499
8051
  );
7500
- const isRetrying = React43.useCallback(
8052
+ const isRetrying = React44.useCallback(
7501
8053
  (messageId) => {
7502
8054
  return optimisticChat.some((m) => m.id === messageId && m.retrying);
7503
8055
  },
@@ -7511,7 +8063,7 @@ import {
7511
8063
  publishComergeStudioUIState,
7512
8064
  startStudioControlPolling
7513
8065
  } from "@comergehq/studio-control";
7514
- import { Fragment as Fragment6, jsx as jsx59, jsxs as jsxs36 } from "react/jsx-runtime";
8066
+ import { Fragment as Fragment6, jsx as jsx61, jsxs as jsxs38 } from "react/jsx-runtime";
7515
8067
  function StudioOverlay({
7516
8068
  captureTargetRef,
7517
8069
  app,
@@ -7544,19 +8096,20 @@ function StudioOverlay({
7544
8096
  onSendChat,
7545
8097
  chatQueueItems,
7546
8098
  onRemoveQueueItem,
8099
+ chatProgress,
7547
8100
  onNavigateHome,
7548
8101
  showBubble,
7549
8102
  studioControlOptions
7550
8103
  }) {
7551
8104
  const theme = useTheme();
7552
8105
  const { width } = useWindowDimensions4();
7553
- const [sheetOpen, setSheetOpen] = React44.useState(false);
7554
- const sheetOpenRef = React44.useRef(sheetOpen);
7555
- const [activePage, setActivePage] = React44.useState("preview");
7556
- const [drawing, setDrawing] = React44.useState(false);
7557
- const [chatAttachments, setChatAttachments] = React44.useState([]);
7558
- const [commentsAppId, setCommentsAppId] = React44.useState(null);
7559
- const [commentsCount, setCommentsCount] = React44.useState(null);
8106
+ const [sheetOpen, setSheetOpen] = React45.useState(false);
8107
+ const sheetOpenRef = React45.useRef(sheetOpen);
8108
+ const [activePage, setActivePage] = React45.useState("preview");
8109
+ const [drawing, setDrawing] = React45.useState(false);
8110
+ const [chatAttachments, setChatAttachments] = React45.useState([]);
8111
+ const [commentsAppId, setCommentsAppId] = React45.useState(null);
8112
+ const [commentsCount, setCommentsCount] = React45.useState(null);
7560
8113
  const threadId = (app == null ? void 0 : app.threadId) ?? null;
7561
8114
  const isForking = chatForking || (app == null ? void 0 : app.status) === "forking";
7562
8115
  const queueItemsForChat = isForking ? [] : chatQueueItems;
@@ -7568,25 +8121,25 @@ function StudioOverlay({
7568
8121
  chatMessages,
7569
8122
  onSendChat
7570
8123
  });
7571
- const [confirmMrId, setConfirmMrId] = React44.useState(null);
7572
- const confirmMr = React44.useMemo(
8124
+ const [confirmMrId, setConfirmMrId] = React45.useState(null);
8125
+ const confirmMr = React45.useMemo(
7573
8126
  () => confirmMrId ? incomingMergeRequests.find((m) => m.id === confirmMrId) ?? null : null,
7574
8127
  [confirmMrId, incomingMergeRequests]
7575
8128
  );
7576
- const handleSheetOpenChange = React44.useCallback((open) => {
8129
+ const handleSheetOpenChange = React45.useCallback((open) => {
7577
8130
  setSheetOpen(open);
7578
8131
  if (!open) Keyboard5.dismiss();
7579
8132
  }, []);
7580
- const closeSheet = React44.useCallback(() => {
8133
+ const closeSheet = React45.useCallback(() => {
7581
8134
  handleSheetOpenChange(false);
7582
8135
  }, [handleSheetOpenChange]);
7583
- const openSheet = React44.useCallback(() => setSheetOpen(true), []);
7584
- const goToChat = React44.useCallback(() => {
8136
+ const openSheet = React45.useCallback(() => setSheetOpen(true), []);
8137
+ const goToChat = React45.useCallback(() => {
7585
8138
  setActivePage("chat");
7586
8139
  openSheet();
7587
8140
  }, [openSheet]);
7588
- const backToPreview = React44.useCallback(() => {
7589
- if (Platform10.OS !== "ios") {
8141
+ const backToPreview = React45.useCallback(() => {
8142
+ if (Platform11.OS !== "ios") {
7590
8143
  Keyboard5.dismiss();
7591
8144
  setActivePage("preview");
7592
8145
  return;
@@ -7603,11 +8156,11 @@ function StudioOverlay({
7603
8156
  const t = setTimeout(finalize, 350);
7604
8157
  Keyboard5.dismiss();
7605
8158
  }, []);
7606
- const startDraw = React44.useCallback(() => {
8159
+ const startDraw = React45.useCallback(() => {
7607
8160
  setDrawing(true);
7608
8161
  closeSheet();
7609
8162
  }, [closeSheet]);
7610
- const handleDrawCapture = React44.useCallback(
8163
+ const handleDrawCapture = React45.useCallback(
7611
8164
  (dataUrl) => {
7612
8165
  setChatAttachments((prev) => [...prev, dataUrl]);
7613
8166
  setDrawing(false);
@@ -7616,7 +8169,7 @@ function StudioOverlay({
7616
8169
  },
7617
8170
  [openSheet]
7618
8171
  );
7619
- const toggleSheet = React44.useCallback(async () => {
8172
+ const toggleSheet = React45.useCallback(async () => {
7620
8173
  if (!sheetOpen) {
7621
8174
  const shouldExitTest = Boolean(testingMrId) || isTesting;
7622
8175
  if (shouldExitTest) {
@@ -7628,7 +8181,7 @@ function StudioOverlay({
7628
8181
  closeSheet();
7629
8182
  }
7630
8183
  }, [closeSheet, isTesting, onRestoreBase, sheetOpen, testingMrId]);
7631
- const handleTestMr = React44.useCallback(
8184
+ const handleTestMr = React45.useCallback(
7632
8185
  async (mr) => {
7633
8186
  if (!onTestMr) return;
7634
8187
  await onTestMr(mr);
@@ -7636,10 +8189,10 @@ function StudioOverlay({
7636
8189
  },
7637
8190
  [closeSheet, onTestMr]
7638
8191
  );
7639
- React44.useEffect(() => {
8192
+ React45.useEffect(() => {
7640
8193
  sheetOpenRef.current = sheetOpen;
7641
8194
  }, [sheetOpen]);
7642
- React44.useEffect(() => {
8195
+ React45.useEffect(() => {
7643
8196
  const poller = startStudioControlPolling((action) => {
7644
8197
  if (action === "show" && !sheetOpenRef.current) openSheet();
7645
8198
  if (action === "hide" && sheetOpenRef.current) closeSheet();
@@ -7647,17 +8200,17 @@ function StudioOverlay({
7647
8200
  }, studioControlOptions);
7648
8201
  return () => poller.stop();
7649
8202
  }, [closeSheet, openSheet, studioControlOptions, toggleSheet]);
7650
- React44.useEffect(() => {
8203
+ React45.useEffect(() => {
7651
8204
  void publishComergeStudioUIState(sheetOpen, studioControlOptions);
7652
8205
  }, [sheetOpen, studioControlOptions]);
7653
- return /* @__PURE__ */ jsxs36(Fragment6, { children: [
7654
- /* @__PURE__ */ jsx59(EdgeGlowFrame, { visible: isTesting, role: "accent", thickness: 40, intensity: 1 }),
7655
- /* @__PURE__ */ jsx59(StudioBottomSheet, { open: sheetOpen, onOpenChange: handleSheetOpenChange, children: /* @__PURE__ */ jsx59(
8206
+ return /* @__PURE__ */ jsxs38(Fragment6, { children: [
8207
+ /* @__PURE__ */ jsx61(EdgeGlowFrame, { visible: isTesting, role: "accent", thickness: 40, intensity: 1 }),
8208
+ /* @__PURE__ */ jsx61(StudioBottomSheet, { open: sheetOpen, onOpenChange: handleSheetOpenChange, children: /* @__PURE__ */ jsx61(
7656
8209
  StudioSheetPager,
7657
8210
  {
7658
8211
  activePage,
7659
8212
  width,
7660
- preview: /* @__PURE__ */ jsx59(
8213
+ preview: /* @__PURE__ */ jsx61(
7661
8214
  PreviewPanel,
7662
8215
  {
7663
8216
  app,
@@ -7686,7 +8239,7 @@ function StudioOverlay({
7686
8239
  commentCountOverride: commentsCount ?? void 0
7687
8240
  }
7688
8241
  ),
7689
- chat: /* @__PURE__ */ jsx59(
8242
+ chat: /* @__PURE__ */ jsx61(
7690
8243
  ChatPanel,
7691
8244
  {
7692
8245
  messages: optimistic.messages,
@@ -7707,12 +8260,13 @@ function StudioOverlay({
7707
8260
  onRetryMessage: optimistic.onRetry,
7708
8261
  isRetryingMessage: optimistic.isRetrying,
7709
8262
  queueItems: queueItemsForChat,
7710
- onRemoveQueueItem
8263
+ onRemoveQueueItem,
8264
+ progress: chatProgress
7711
8265
  }
7712
8266
  )
7713
8267
  }
7714
8268
  ) }),
7715
- showBubble && /* @__PURE__ */ jsx59(
8269
+ showBubble && /* @__PURE__ */ jsx61(
7716
8270
  Bubble,
7717
8271
  {
7718
8272
  visible: !sheetOpen && !drawing,
@@ -7721,10 +8275,10 @@ function StudioOverlay({
7721
8275
  onPress: toggleSheet,
7722
8276
  isLoading: (app == null ? void 0 : app.status) === "editing" || isBaseBundleDownloading,
7723
8277
  loadingBorderTone: isBaseBundleDownloading ? "warning" : "default",
7724
- children: /* @__PURE__ */ jsx59(View45, { style: { width: 28, height: 28, alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsx59(MergeIcon, { width: 24, height: 24, color: theme.colors.floatingContent }) })
8278
+ children: /* @__PURE__ */ jsx61(View47, { style: { width: 28, height: 28, alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsx61(MergeIcon, { width: 24, height: 24, color: theme.colors.floatingContent }) })
7725
8279
  }
7726
8280
  ),
7727
- /* @__PURE__ */ jsx59(
8281
+ /* @__PURE__ */ jsx61(
7728
8282
  DrawModeOverlay,
7729
8283
  {
7730
8284
  visible: drawing,
@@ -7733,7 +8287,7 @@ function StudioOverlay({
7733
8287
  onCapture: handleDrawCapture
7734
8288
  }
7735
8289
  ),
7736
- /* @__PURE__ */ jsx59(
8290
+ /* @__PURE__ */ jsx61(
7737
8291
  ConfirmMergeFlow,
7738
8292
  {
7739
8293
  visible: Boolean(confirmMr),
@@ -7747,7 +8301,7 @@ function StudioOverlay({
7747
8301
  onTestFirst: handleTestMr
7748
8302
  }
7749
8303
  ),
7750
- /* @__PURE__ */ jsx59(
8304
+ /* @__PURE__ */ jsx61(
7751
8305
  AppCommentsSheet,
7752
8306
  {
7753
8307
  appId: commentsAppId,
@@ -7760,7 +8314,7 @@ function StudioOverlay({
7760
8314
  }
7761
8315
 
7762
8316
  // src/studio/hooks/useEditQueue.ts
7763
- import * as React45 from "react";
8317
+ import * as React46 from "react";
7764
8318
 
7765
8319
  // src/data/apps/edit-queue/remote.ts
7766
8320
  var EditQueueRemoteDataSourceImpl = class extends BaseRemote {
@@ -7869,17 +8423,17 @@ var editQueueRepository = new EditQueueRepositoryImpl(
7869
8423
 
7870
8424
  // src/studio/hooks/useEditQueue.ts
7871
8425
  function useEditQueue(appId) {
7872
- const [items, setItems] = React45.useState([]);
7873
- const [loading, setLoading] = React45.useState(false);
7874
- const [error, setError] = React45.useState(null);
7875
- const activeRequestIdRef = React45.useRef(0);
8426
+ const [items, setItems] = React46.useState([]);
8427
+ const [loading, setLoading] = React46.useState(false);
8428
+ const [error, setError] = React46.useState(null);
8429
+ const activeRequestIdRef = React46.useRef(0);
7876
8430
  const foregroundSignal = useForegroundSignal(Boolean(appId));
7877
- const upsertSorted = React45.useCallback((prev, nextItem) => {
8431
+ const upsertSorted = React46.useCallback((prev, nextItem) => {
7878
8432
  const next = prev.some((x) => x.id === nextItem.id) ? prev.map((x) => x.id === nextItem.id ? nextItem : x) : [...prev, nextItem];
7879
8433
  next.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
7880
8434
  return next;
7881
8435
  }, []);
7882
- const refetch = React45.useCallback(async () => {
8436
+ const refetch = React46.useCallback(async () => {
7883
8437
  if (!appId) {
7884
8438
  setItems([]);
7885
8439
  return;
@@ -7899,10 +8453,10 @@ function useEditQueue(appId) {
7899
8453
  if (activeRequestIdRef.current === requestId) setLoading(false);
7900
8454
  }
7901
8455
  }, [appId]);
7902
- React45.useEffect(() => {
8456
+ React46.useEffect(() => {
7903
8457
  void refetch();
7904
8458
  }, [refetch]);
7905
- React45.useEffect(() => {
8459
+ React46.useEffect(() => {
7906
8460
  if (!appId) return;
7907
8461
  const unsubscribe = editQueueRepository.subscribeEditQueue(appId, {
7908
8462
  onInsert: (item) => setItems((prev) => upsertSorted(prev, item)),
@@ -7911,7 +8465,7 @@ function useEditQueue(appId) {
7911
8465
  });
7912
8466
  return unsubscribe;
7913
8467
  }, [appId, upsertSorted, foregroundSignal]);
7914
- React45.useEffect(() => {
8468
+ React46.useEffect(() => {
7915
8469
  if (!appId) return;
7916
8470
  if (foregroundSignal <= 0) return;
7917
8471
  void refetch();
@@ -7920,16 +8474,16 @@ function useEditQueue(appId) {
7920
8474
  }
7921
8475
 
7922
8476
  // src/studio/hooks/useEditQueueActions.ts
7923
- import * as React46 from "react";
8477
+ import * as React47 from "react";
7924
8478
  function useEditQueueActions(appId) {
7925
- const update = React46.useCallback(
8479
+ const update = React47.useCallback(
7926
8480
  async (queueItemId, payload) => {
7927
8481
  if (!appId) return;
7928
8482
  await editQueueRepository.update(appId, queueItemId, payload);
7929
8483
  },
7930
8484
  [appId]
7931
8485
  );
7932
- const cancel = React46.useCallback(
8486
+ const cancel = React47.useCallback(
7933
8487
  async (queueItemId) => {
7934
8488
  if (!appId) return;
7935
8489
  await editQueueRepository.cancel(appId, queueItemId);
@@ -7939,48 +8493,461 @@ function useEditQueueActions(appId) {
7939
8493
  return { update, cancel };
7940
8494
  }
7941
8495
 
8496
+ // src/studio/hooks/useAgentRunProgress.ts
8497
+ import * as React48 from "react";
8498
+
8499
+ // src/data/agent-progress/repository.ts
8500
+ function mapRun(row) {
8501
+ return {
8502
+ id: row.id,
8503
+ appId: row.app_id,
8504
+ threadId: row.thread_id,
8505
+ queueItemId: row.queue_item_id,
8506
+ status: row.status,
8507
+ currentPhase: row.current_phase,
8508
+ lastSeq: Number(row.last_seq || 0),
8509
+ summary: row.summary || {},
8510
+ errorCode: row.error_code,
8511
+ errorMessage: row.error_message,
8512
+ startedAt: row.started_at,
8513
+ finishedAt: row.finished_at,
8514
+ createdAt: row.created_at,
8515
+ updatedAt: row.updated_at
8516
+ };
8517
+ }
8518
+ function mapEvent(row) {
8519
+ return {
8520
+ id: row.id,
8521
+ runId: row.run_id,
8522
+ appId: row.app_id,
8523
+ threadId: row.thread_id,
8524
+ queueItemId: row.queue_item_id,
8525
+ seq: Number(row.seq || 0),
8526
+ eventType: row.event_type,
8527
+ phase: row.phase,
8528
+ toolName: row.tool_name,
8529
+ path: row.path,
8530
+ payload: row.payload || {},
8531
+ createdAt: row.created_at
8532
+ };
8533
+ }
8534
+ var AgentProgressRepositoryImpl = class {
8535
+ async getLatestRun(threadId) {
8536
+ if (!threadId) return null;
8537
+ const supabase = getSupabaseClient();
8538
+ const { data, error } = await supabase.from("agent_run").select("*").eq("thread_id", threadId).order("started_at", { ascending: false }).limit(1).maybeSingle();
8539
+ if (error) throw new Error(error.message || "Failed to fetch latest agent run");
8540
+ if (!data) return null;
8541
+ return mapRun(data);
8542
+ }
8543
+ async listEvents(runId, afterSeq) {
8544
+ if (!runId) return [];
8545
+ const supabase = getSupabaseClient();
8546
+ let query = supabase.from("agent_run_event").select("*").eq("run_id", runId).order("seq", { ascending: true });
8547
+ if (typeof afterSeq === "number" && Number.isFinite(afterSeq) && afterSeq > 0) {
8548
+ query = query.gt("seq", afterSeq);
8549
+ }
8550
+ const { data, error } = await query;
8551
+ if (error) throw new Error(error.message || "Failed to fetch agent run events");
8552
+ const rows = Array.isArray(data) ? data : [];
8553
+ return rows.map(mapEvent);
8554
+ }
8555
+ subscribeThreadRuns(threadId, handlers) {
8556
+ return subscribeManagedChannel(`agent-progress:runs:thread:${threadId}`, (channel) => {
8557
+ channel.on(
8558
+ "postgres_changes",
8559
+ { event: "INSERT", schema: "public", table: "agent_run", filter: `thread_id=eq.${threadId}` },
8560
+ (payload) => {
8561
+ var _a;
8562
+ const row = payload.new;
8563
+ (_a = handlers.onInsert) == null ? void 0 : _a.call(handlers, mapRun(row));
8564
+ }
8565
+ ).on(
8566
+ "postgres_changes",
8567
+ { event: "UPDATE", schema: "public", table: "agent_run", filter: `thread_id=eq.${threadId}` },
8568
+ (payload) => {
8569
+ var _a;
8570
+ const row = payload.new;
8571
+ (_a = handlers.onUpdate) == null ? void 0 : _a.call(handlers, mapRun(row));
8572
+ }
8573
+ );
8574
+ });
8575
+ }
8576
+ subscribeRunEvents(runId, handlers) {
8577
+ return subscribeManagedChannel(`agent-progress:events:run:${runId}`, (channel) => {
8578
+ channel.on(
8579
+ "postgres_changes",
8580
+ { event: "INSERT", schema: "public", table: "agent_run_event", filter: `run_id=eq.${runId}` },
8581
+ (payload) => {
8582
+ var _a;
8583
+ const row = payload.new;
8584
+ (_a = handlers.onInsert) == null ? void 0 : _a.call(handlers, mapEvent(row));
8585
+ }
8586
+ ).on(
8587
+ "postgres_changes",
8588
+ { event: "UPDATE", schema: "public", table: "agent_run_event", filter: `run_id=eq.${runId}` },
8589
+ (payload) => {
8590
+ var _a;
8591
+ const row = payload.new;
8592
+ (_a = handlers.onUpdate) == null ? void 0 : _a.call(handlers, mapEvent(row));
8593
+ }
8594
+ );
8595
+ });
8596
+ }
8597
+ };
8598
+ var agentProgressRepository = new AgentProgressRepositoryImpl();
8599
+
8600
+ // src/studio/hooks/useAgentRunProgress.ts
8601
+ function upsertBySeq(prev, next) {
8602
+ const map = /* @__PURE__ */ new Map();
8603
+ for (const item of prev) map.set(item.seq, item);
8604
+ map.set(next.seq, next);
8605
+ return Array.from(map.values()).sort((a, b) => a.seq - b.seq);
8606
+ }
8607
+ function mergeMany(prev, incoming) {
8608
+ if (incoming.length === 0) return prev;
8609
+ const map = /* @__PURE__ */ new Map();
8610
+ for (const item of prev) map.set(item.seq, item);
8611
+ for (const item of incoming) map.set(item.seq, item);
8612
+ return Array.from(map.values()).sort((a, b) => a.seq - b.seq);
8613
+ }
8614
+ function toMs(v) {
8615
+ if (!v) return 0;
8616
+ const n = Date.parse(v);
8617
+ return Number.isFinite(n) ? n : 0;
8618
+ }
8619
+ function shouldSwitchRun(current, candidate) {
8620
+ if (!current) return true;
8621
+ if (candidate.id === current.id) return true;
8622
+ return toMs(candidate.startedAt) >= toMs(current.startedAt);
8623
+ }
8624
+ function toInt(value) {
8625
+ const n = Number(value);
8626
+ return Number.isFinite(n) ? n : 0;
8627
+ }
8628
+ function toBundleStage(value) {
8629
+ if (value === "queued") return "queued";
8630
+ if (value === "building") return "building";
8631
+ if (value === "fixing") return "fixing";
8632
+ if (value === "retrying") return "retrying";
8633
+ if (value === "finalizing") return "finalizing";
8634
+ if (value === "ready") return "ready";
8635
+ if (value === "failed") return "failed";
8636
+ return null;
8637
+ }
8638
+ function toBundlePlatform(value) {
8639
+ if (value === "ios" || value === "android") return value;
8640
+ return "both";
8641
+ }
8642
+ function clamp01(value) {
8643
+ if (value <= 0) return 0;
8644
+ if (value >= 1) return 1;
8645
+ return value;
8646
+ }
8647
+ function defaultBundleLabel(stage) {
8648
+ if (stage === "queued") return "Queued for build";
8649
+ if (stage === "building") return "Building bundle";
8650
+ if (stage === "fixing") return "Applying auto-fix";
8651
+ if (stage === "retrying") return "Retrying bundle";
8652
+ if (stage === "finalizing") return "Finalizing artifacts";
8653
+ if (stage === "ready") return "Bundle ready";
8654
+ if (stage === "failed") return "Bundle failed";
8655
+ return "Building bundle";
8656
+ }
8657
+ function fallbackBundleProgress(stage, startedAtMs, nowMs) {
8658
+ if (stage === "ready") return 1;
8659
+ if (stage === "failed") return 0.96;
8660
+ const elapsed = Math.max(0, nowMs - startedAtMs);
8661
+ const expectedMs = 6e4;
8662
+ if (elapsed <= expectedMs) {
8663
+ const t = clamp01(elapsed / expectedMs);
8664
+ return 0.05 + 0.85 * (1 - Math.pow(1 - t, 2));
8665
+ }
8666
+ const over = elapsed - expectedMs;
8667
+ return Math.min(0.9 + 0.07 * (1 - Math.exp(-over / 25e3)), 0.97);
8668
+ }
8669
+ function deriveView(run, events, nowMs) {
8670
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
8671
+ const files = [];
8672
+ const fileSeen = /* @__PURE__ */ new Set();
8673
+ let todoSummary = null;
8674
+ let latestMessage = null;
8675
+ let phase = (run == null ? void 0 : run.currentPhase) ?? null;
8676
+ let bundleStage = null;
8677
+ let bundleLabel = null;
8678
+ let bundleError = null;
8679
+ let bundleProgressHint = null;
8680
+ let bundlePlatform = "both";
8681
+ let bundleStartedAtMs = null;
8682
+ let lastBundleSig = null;
8683
+ for (const ev of events) {
8684
+ if (ev.eventType === "phase_changed") {
8685
+ if (typeof ((_a = ev.payload) == null ? void 0 : _a.message) === "string") latestMessage = ev.payload.message;
8686
+ if (ev.phase) phase = ev.phase;
8687
+ }
8688
+ if (ev.eventType === "file_changed") {
8689
+ if (ev.path && !fileSeen.has(ev.path)) {
8690
+ fileSeen.add(ev.path);
8691
+ files.push(ev.path);
8692
+ }
8693
+ }
8694
+ if (ev.eventType === "todo_updated") {
8695
+ todoSummary = {
8696
+ total: toInt((_b = ev.payload) == null ? void 0 : _b.total),
8697
+ pending: toInt((_c = ev.payload) == null ? void 0 : _c.pending),
8698
+ inProgress: toInt((_d = ev.payload) == null ? void 0 : _d.inProgress),
8699
+ completed: toInt((_e = ev.payload) == null ? void 0 : _e.completed),
8700
+ currentTask: typeof ((_f = ev.payload) == null ? void 0 : _f.currentTask) === "string" ? ev.payload.currentTask : null
8701
+ };
8702
+ }
8703
+ const stageType = typeof ((_g = ev.payload) == null ? void 0 : _g.stage) === "string" ? ev.payload.stage : null;
8704
+ if (stageType !== "bundle") continue;
8705
+ const nextStage = toBundleStage((_h = ev.payload) == null ? void 0 : _h.bundlePhase);
8706
+ if (!nextStage) continue;
8707
+ const nextPlatform = toBundlePlatform((_i = ev.payload) == null ? void 0 : _i.platform);
8708
+ const message = typeof ((_j = ev.payload) == null ? void 0 : _j.message) === "string" ? ev.payload.message : null;
8709
+ const phaseLabel = message || (typeof ((_k = ev.payload) == null ? void 0 : _k.message) === "string" ? ev.payload.message : null);
8710
+ const hintRaw = Number((_l = ev.payload) == null ? void 0 : _l.progressHint);
8711
+ const progressHint = Number.isFinite(hintRaw) ? clamp01(hintRaw) : null;
8712
+ const errorText = typeof ((_m = ev.payload) == null ? void 0 : _m.error) === "string" ? ev.payload.error : null;
8713
+ const sig = `${ev.seq}:${nextStage}:${nextPlatform}:${progressHint ?? "none"}:${phaseLabel ?? "none"}:${errorText ?? "none"}`;
8714
+ if (sig === lastBundleSig) continue;
8715
+ lastBundleSig = sig;
8716
+ bundleStage = nextStage;
8717
+ bundlePlatform = nextPlatform;
8718
+ if (phaseLabel) bundleLabel = phaseLabel;
8719
+ if (progressHint != null) bundleProgressHint = progressHint;
8720
+ if (errorText) bundleError = errorText;
8721
+ const evMs = toMs(ev.createdAt);
8722
+ if (!bundleStartedAtMs && evMs > 0) bundleStartedAtMs = evMs;
8723
+ }
8724
+ if (!latestMessage) {
8725
+ if (phase === "planning") latestMessage = "Planning changes...";
8726
+ else if (phase === "analyzing") latestMessage = "Analyzing relevant files...";
8727
+ else if (phase === "editing") latestMessage = "Applying code updates...";
8728
+ else if (phase === "validating") latestMessage = "Validating updates...";
8729
+ else if (phase === "finalizing") latestMessage = "Finalizing response...";
8730
+ else if (phase) latestMessage = `Working (${phase})...`;
8731
+ }
8732
+ const runFinished = (run == null ? void 0 : run.status) === "succeeded" || (run == null ? void 0 : run.status) === "failed" || (run == null ? void 0 : run.status) === "cancelled";
8733
+ let bundle = null;
8734
+ if (bundleStage && !runFinished) {
8735
+ const baseTime = bundleStartedAtMs ?? toMs(run == null ? void 0 : run.startedAt) ?? nowMs;
8736
+ const fallback = fallbackBundleProgress(bundleStage, baseTime || nowMs, nowMs);
8737
+ const value = bundleProgressHint != null ? Math.max(fallback, bundleProgressHint) : fallback;
8738
+ const status = bundleStage === "failed" ? "failed" : bundleStage === "ready" ? "succeeded" : "loading";
8739
+ bundle = {
8740
+ active: status === "loading",
8741
+ status,
8742
+ phaseLabel: bundleLabel || defaultBundleLabel(bundleStage),
8743
+ progressValue: clamp01(value),
8744
+ errorMessage: bundleError,
8745
+ platform: bundlePlatform
8746
+ };
8747
+ }
8748
+ return {
8749
+ runId: (run == null ? void 0 : run.id) ?? null,
8750
+ status: (run == null ? void 0 : run.status) ?? null,
8751
+ phase,
8752
+ latestMessage,
8753
+ changedFilesCount: fileSeen.size,
8754
+ recentFiles: files.slice(-5),
8755
+ todoSummary,
8756
+ bundle,
8757
+ events
8758
+ };
8759
+ }
8760
+ function useAgentRunProgress(threadId, opts) {
8761
+ var _a;
8762
+ const enabled = Boolean((opts == null ? void 0 : opts.enabled) ?? true);
8763
+ const [run, setRun] = React48.useState(null);
8764
+ const [events, setEvents] = React48.useState([]);
8765
+ const [loading, setLoading] = React48.useState(false);
8766
+ const [error, setError] = React48.useState(null);
8767
+ const activeRequestIdRef = React48.useRef(0);
8768
+ const lastSeqRef = React48.useRef(0);
8769
+ const runRef = React48.useRef(null);
8770
+ const foregroundSignal = useForegroundSignal(Boolean(threadId) && enabled);
8771
+ const [bundleTick, setBundleTick] = React48.useState(0);
8772
+ React48.useEffect(() => {
8773
+ lastSeqRef.current = 0;
8774
+ runRef.current = null;
8775
+ }, [threadId]);
8776
+ React48.useEffect(() => {
8777
+ runRef.current = run;
8778
+ }, [run]);
8779
+ const refetch = React48.useCallback(async () => {
8780
+ if (!threadId || !enabled) {
8781
+ setRun(null);
8782
+ setEvents([]);
8783
+ setLoading(false);
8784
+ setError(null);
8785
+ return;
8786
+ }
8787
+ const requestId = ++activeRequestIdRef.current;
8788
+ setLoading(true);
8789
+ setError(null);
8790
+ try {
8791
+ const latestRun = await agentProgressRepository.getLatestRun(threadId);
8792
+ if (activeRequestIdRef.current !== requestId) return;
8793
+ if (!latestRun) {
8794
+ setRun(null);
8795
+ setEvents([]);
8796
+ lastSeqRef.current = 0;
8797
+ return;
8798
+ }
8799
+ const initialEvents = await agentProgressRepository.listEvents(latestRun.id);
8800
+ if (activeRequestIdRef.current !== requestId) return;
8801
+ const sorted = [...initialEvents].sort((a, b) => a.seq - b.seq);
8802
+ setRun(latestRun);
8803
+ setEvents(sorted);
8804
+ lastSeqRef.current = sorted.length > 0 ? sorted[sorted.length - 1].seq : 0;
8805
+ } catch (e) {
8806
+ if (activeRequestIdRef.current !== requestId) return;
8807
+ setError(e instanceof Error ? e : new Error(String(e)));
8808
+ setRun(null);
8809
+ setEvents([]);
8810
+ lastSeqRef.current = 0;
8811
+ } finally {
8812
+ if (activeRequestIdRef.current === requestId) setLoading(false);
8813
+ }
8814
+ }, [enabled, threadId]);
8815
+ React48.useEffect(() => {
8816
+ void refetch();
8817
+ }, [refetch]);
8818
+ React48.useEffect(() => {
8819
+ if (!threadId || !enabled) return;
8820
+ if (foregroundSignal <= 0) return;
8821
+ void refetch();
8822
+ }, [enabled, foregroundSignal, refetch, threadId]);
8823
+ React48.useEffect(() => {
8824
+ if (!threadId || !enabled) return;
8825
+ const unsubRuns = agentProgressRepository.subscribeThreadRuns(threadId, {
8826
+ onInsert: (nextRun) => {
8827
+ const currentRun = runRef.current;
8828
+ if (!shouldSwitchRun(currentRun, nextRun)) return;
8829
+ setRun(nextRun);
8830
+ runRef.current = nextRun;
8831
+ if (!currentRun || currentRun.id !== nextRun.id) {
8832
+ lastSeqRef.current = 0;
8833
+ setEvents([]);
8834
+ void agentProgressRepository.listEvents(nextRun.id).then((initial) => {
8835
+ var _a2;
8836
+ if (((_a2 = runRef.current) == null ? void 0 : _a2.id) !== nextRun.id) return;
8837
+ setEvents((prev) => mergeMany(prev, initial));
8838
+ const maxSeq = initial.length > 0 ? initial[initial.length - 1].seq : 0;
8839
+ if (maxSeq > lastSeqRef.current) lastSeqRef.current = maxSeq;
8840
+ }).catch(() => {
8841
+ });
8842
+ }
8843
+ },
8844
+ onUpdate: (nextRun) => {
8845
+ const currentRun = runRef.current;
8846
+ if (!shouldSwitchRun(currentRun, nextRun)) return;
8847
+ setRun(nextRun);
8848
+ runRef.current = nextRun;
8849
+ }
8850
+ });
8851
+ return unsubRuns;
8852
+ }, [enabled, threadId, foregroundSignal]);
8853
+ React48.useEffect(() => {
8854
+ if (!enabled || !(run == null ? void 0 : run.id)) return;
8855
+ const runId = run.id;
8856
+ const processIncoming = (incoming) => {
8857
+ var _a2;
8858
+ if (((_a2 = runRef.current) == null ? void 0 : _a2.id) !== runId) return;
8859
+ setEvents((prev) => upsertBySeq(prev, incoming));
8860
+ if (incoming.seq > lastSeqRef.current) {
8861
+ const expectedNext = lastSeqRef.current + 1;
8862
+ const seenSeq = incoming.seq;
8863
+ const currentLast = lastSeqRef.current;
8864
+ lastSeqRef.current = seenSeq;
8865
+ if (seenSeq > expectedNext) {
8866
+ void agentProgressRepository.listEvents(runId, currentLast).then((missing) => {
8867
+ var _a3;
8868
+ if (((_a3 = runRef.current) == null ? void 0 : _a3.id) !== runId) return;
8869
+ setEvents((prev) => mergeMany(prev, missing));
8870
+ if (missing.length > 0) {
8871
+ const maxSeq = missing[missing.length - 1].seq;
8872
+ if (maxSeq > lastSeqRef.current) lastSeqRef.current = maxSeq;
8873
+ }
8874
+ }).catch(() => {
8875
+ });
8876
+ }
8877
+ }
8878
+ };
8879
+ const unsubscribe = agentProgressRepository.subscribeRunEvents(runId, {
8880
+ onInsert: processIncoming,
8881
+ onUpdate: processIncoming
8882
+ });
8883
+ return unsubscribe;
8884
+ }, [enabled, run == null ? void 0 : run.id, foregroundSignal]);
8885
+ const view = React48.useMemo(() => deriveView(run, events, Date.now()), [bundleTick, events, run]);
8886
+ React48.useEffect(() => {
8887
+ var _a2;
8888
+ if (!((_a2 = view.bundle) == null ? void 0 : _a2.active)) return;
8889
+ const interval = setInterval(() => {
8890
+ setBundleTick((v) => v + 1);
8891
+ }, 300);
8892
+ return () => clearInterval(interval);
8893
+ }, [(_a = view.bundle) == null ? void 0 : _a.active]);
8894
+ const hasLiveProgress = Boolean(run) && (run == null ? void 0 : run.status) === "running";
8895
+ return { run, view, loading, error, hasLiveProgress, refetch };
8896
+ }
8897
+
7942
8898
  // src/studio/ComergeStudio.tsx
7943
- import { jsx as jsx60, jsxs as jsxs37 } from "react/jsx-runtime";
8899
+ import { jsx as jsx62, jsxs as jsxs39 } from "react/jsx-runtime";
7944
8900
  function ComergeStudio({
7945
8901
  appId,
7946
8902
  clientKey: clientKey2,
7947
8903
  appKey = "MicroMain",
8904
+ analyticsEnabled,
7948
8905
  onNavigateHome,
7949
8906
  style,
7950
8907
  showBubble = true,
8908
+ enableAgentProgress = true,
7951
8909
  studioControlOptions,
7952
8910
  embeddedBaseBundles
7953
8911
  }) {
7954
- const [activeAppId, setActiveAppId] = React47.useState(appId);
7955
- const [runtimeAppId, setRuntimeAppId] = React47.useState(appId);
7956
- const [pendingRuntimeTargetAppId, setPendingRuntimeTargetAppId] = React47.useState(null);
7957
- const platform = React47.useMemo(() => RNPlatform.OS === "ios" ? "ios" : "android", []);
7958
- React47.useEffect(() => {
8912
+ const [activeAppId, setActiveAppId] = React49.useState(appId);
8913
+ const [runtimeAppId, setRuntimeAppId] = React49.useState(appId);
8914
+ const [pendingRuntimeTargetAppId, setPendingRuntimeTargetAppId] = React49.useState(null);
8915
+ const platform = React49.useMemo(() => RNPlatform.OS === "ios" ? "ios" : "android", []);
8916
+ React49.useEffect(() => {
7959
8917
  setActiveAppId(appId);
7960
8918
  setRuntimeAppId(appId);
7961
8919
  setPendingRuntimeTargetAppId(null);
7962
8920
  }, [appId]);
7963
- const captureTargetRef = React47.useRef(null);
7964
- return /* @__PURE__ */ jsx60(StudioBootstrap, { clientKey: clientKey2, fallback: /* @__PURE__ */ jsx60(View46, { style: { flex: 1 } }), children: ({ userId }) => /* @__PURE__ */ jsx60(BottomSheetModalProvider, { children: /* @__PURE__ */ jsx60(LiquidGlassResetProvider, { resetTriggers: [appId, activeAppId, runtimeAppId], children: /* @__PURE__ */ jsx60(
7965
- ComergeStudioInner,
8921
+ const captureTargetRef = React49.useRef(null);
8922
+ return /* @__PURE__ */ jsx62(
8923
+ StudioBootstrap,
7966
8924
  {
7967
- userId,
7968
- activeAppId,
7969
- setActiveAppId,
7970
- runtimeAppId,
7971
- setRuntimeAppId,
7972
- pendingRuntimeTargetAppId,
7973
- setPendingRuntimeTargetAppId,
7974
- appKey,
7975
- platform,
7976
- onNavigateHome,
7977
- captureTargetRef,
7978
- style,
7979
- showBubble,
7980
- studioControlOptions,
7981
- embeddedBaseBundles
8925
+ clientKey: clientKey2,
8926
+ analyticsEnabled,
8927
+ fallback: /* @__PURE__ */ jsx62(View48, { style: { flex: 1 } }),
8928
+ children: ({ userId }) => /* @__PURE__ */ jsx62(BottomSheetModalProvider, { children: /* @__PURE__ */ jsx62(LiquidGlassResetProvider, { resetTriggers: [appId, activeAppId, runtimeAppId], children: /* @__PURE__ */ jsx62(
8929
+ ComergeStudioInner,
8930
+ {
8931
+ userId,
8932
+ activeAppId,
8933
+ setActiveAppId,
8934
+ runtimeAppId,
8935
+ setRuntimeAppId,
8936
+ pendingRuntimeTargetAppId,
8937
+ setPendingRuntimeTargetAppId,
8938
+ appKey,
8939
+ platform,
8940
+ onNavigateHome,
8941
+ captureTargetRef,
8942
+ style,
8943
+ showBubble,
8944
+ enableAgentProgress,
8945
+ studioControlOptions,
8946
+ embeddedBaseBundles
8947
+ }
8948
+ ) }) })
7982
8949
  }
7983
- ) }) }) });
8950
+ );
7984
8951
  }
7985
8952
  function ComergeStudioInner({
7986
8953
  userId,
@@ -7996,17 +8963,19 @@ function ComergeStudioInner({
7996
8963
  captureTargetRef,
7997
8964
  style,
7998
8965
  showBubble,
8966
+ enableAgentProgress,
7999
8967
  studioControlOptions,
8000
8968
  embeddedBaseBundles
8001
8969
  }) {
8970
+ var _a;
8002
8971
  const { app, loading: appLoading } = useApp(activeAppId);
8003
8972
  const { app: runtimeAppFromHook } = useApp(runtimeAppId, { enabled: runtimeAppId !== activeAppId });
8004
8973
  const runtimeApp = runtimeAppId === activeAppId ? app : runtimeAppFromHook;
8005
- const sawEditingOnPendingTargetRef = React47.useRef(false);
8006
- React47.useEffect(() => {
8974
+ const sawEditingOnPendingTargetRef = React49.useRef(false);
8975
+ React49.useEffect(() => {
8007
8976
  sawEditingOnPendingTargetRef.current = false;
8008
8977
  }, [pendingRuntimeTargetAppId]);
8009
- React47.useEffect(() => {
8978
+ React49.useEffect(() => {
8010
8979
  if (!pendingRuntimeTargetAppId) return;
8011
8980
  if (activeAppId !== pendingRuntimeTargetAppId) return;
8012
8981
  if ((app == null ? void 0 : app.status) === "editing") {
@@ -8024,13 +8993,13 @@ function ComergeStudioInner({
8024
8993
  canRequestLatest: (runtimeApp == null ? void 0 : runtimeApp.status) === "ready",
8025
8994
  embeddedBaseBundles
8026
8995
  });
8027
- const sawEditingOnActiveAppRef = React47.useRef(false);
8028
- const [showPostEditPreparing, setShowPostEditPreparing] = React47.useState(false);
8029
- React47.useEffect(() => {
8996
+ const sawEditingOnActiveAppRef = React49.useRef(false);
8997
+ const [showPostEditPreparing, setShowPostEditPreparing] = React49.useState(false);
8998
+ React49.useEffect(() => {
8030
8999
  sawEditingOnActiveAppRef.current = false;
8031
9000
  setShowPostEditPreparing(false);
8032
9001
  }, [activeAppId]);
8033
- React47.useEffect(() => {
9002
+ React49.useEffect(() => {
8034
9003
  if (!(app == null ? void 0 : app.id)) return;
8035
9004
  if (app.status === "editing") {
8036
9005
  sawEditingOnActiveAppRef.current = true;
@@ -8042,7 +9011,7 @@ function ComergeStudioInner({
8042
9011
  sawEditingOnActiveAppRef.current = false;
8043
9012
  }
8044
9013
  }, [app == null ? void 0 : app.id, app == null ? void 0 : app.status]);
8045
- React47.useEffect(() => {
9014
+ React49.useEffect(() => {
8046
9015
  if (!showPostEditPreparing) return;
8047
9016
  const stillProcessingBaseBundle = bundle.loading && bundle.loadingMode === "base" && !bundle.isTesting;
8048
9017
  if (!stillProcessingBaseBundle) {
@@ -8052,20 +9021,21 @@ function ComergeStudioInner({
8052
9021
  const threadId = (app == null ? void 0 : app.threadId) ?? "";
8053
9022
  const thread = useThreadMessages(threadId);
8054
9023
  const editQueue = useEditQueue(activeAppId);
9024
+ const agentProgress = useAgentRunProgress(threadId, { enabled: enableAgentProgress });
8055
9025
  const editQueueActions = useEditQueueActions(activeAppId);
8056
- const [lastEditQueueInfo, setLastEditQueueInfo] = React47.useState(null);
8057
- const lastEditQueueInfoRef = React47.useRef(null);
8058
- const [suppressQueueUntilResponse, setSuppressQueueUntilResponse] = React47.useState(false);
9026
+ const [lastEditQueueInfo, setLastEditQueueInfo] = React49.useState(null);
9027
+ const lastEditQueueInfoRef = React49.useRef(null);
9028
+ const [suppressQueueUntilResponse, setSuppressQueueUntilResponse] = React49.useState(false);
8059
9029
  const mergeRequests = useMergeRequests({ appId: activeAppId });
8060
- const hasOpenOutgoingMr = React47.useMemo(() => {
9030
+ const hasOpenOutgoingMr = React49.useMemo(() => {
8061
9031
  return mergeRequests.lists.outgoing.some((mr) => mr.status === "open");
8062
9032
  }, [mergeRequests.lists.outgoing]);
8063
- const incomingReviewMrs = React47.useMemo(() => {
9033
+ const incomingReviewMrs = React49.useMemo(() => {
8064
9034
  if (!userId) return mergeRequests.lists.incoming;
8065
9035
  return mergeRequests.lists.incoming.filter((mr) => mr.createdBy !== userId);
8066
9036
  }, [mergeRequests.lists.incoming, userId]);
8067
9037
  const uploader = useAttachmentUpload();
8068
- const updateLastEditQueueInfo = React47.useCallback(
9038
+ const updateLastEditQueueInfo = React49.useCallback(
8069
9039
  (info) => {
8070
9040
  lastEditQueueInfoRef.current = info;
8071
9041
  setLastEditQueueInfo(info);
@@ -8099,32 +9069,35 @@ function ComergeStudioInner({
8099
9069
  }
8100
9070
  },
8101
9071
  onEditFinished: () => {
8102
- var _a;
8103
- if (((_a = lastEditQueueInfoRef.current) == null ? void 0 : _a.queuePosition) !== 1) {
9072
+ var _a2;
9073
+ if (((_a2 = lastEditQueueInfoRef.current) == null ? void 0 : _a2.queuePosition) !== 1) {
8104
9074
  setSuppressQueueUntilResponse(false);
8105
9075
  }
8106
9076
  }
8107
9077
  });
8108
9078
  const chatSendDisabled = false;
8109
- const [processingMrId, setProcessingMrId] = React47.useState(null);
8110
- const [testingMrId, setTestingMrId] = React47.useState(null);
8111
- const [syncingUpstream, setSyncingUpstream] = React47.useState(false);
8112
- const [upstreamSyncStatus, setUpstreamSyncStatus] = React47.useState(null);
9079
+ const [processingMrId, setProcessingMrId] = React49.useState(null);
9080
+ const [testingMrId, setTestingMrId] = React49.useState(null);
9081
+ const [syncingUpstream, setSyncingUpstream] = React49.useState(false);
9082
+ const [upstreamSyncStatus, setUpstreamSyncStatus] = React49.useState(null);
8113
9083
  const isMrTestBuildInProgress = bundle.loading && bundle.loadingMode === "test";
8114
9084
  const isBaseBundleDownloading = bundle.loading && bundle.loadingMode === "base" && !bundle.isTesting;
8115
- const chatShowTypingIndicator = React47.useMemo(() => {
8116
- var _a;
9085
+ const runtimePreparingText = bundle.bundleStatus === "pending" ? "Bundling app\u2026 this may take a few minutes" : "Preparing app\u2026";
9086
+ const chatShowTypingIndicator = React49.useMemo(() => {
9087
+ var _a2;
9088
+ if (agentProgress.hasLiveProgress) return false;
8117
9089
  if (!thread.raw || thread.raw.length === 0) return false;
8118
9090
  const last = thread.raw[thread.raw.length - 1];
8119
- const payloadType = typeof ((_a = last.payload) == null ? void 0 : _a.type) === "string" ? String(last.payload.type) : void 0;
9091
+ const payloadType = typeof ((_a2 = last.payload) == null ? void 0 : _a2.type) === "string" ? String(last.payload.type) : void 0;
8120
9092
  return payloadType !== "outcome";
8121
- }, [thread.raw]);
8122
- React47.useEffect(() => {
9093
+ }, [agentProgress.hasLiveProgress, thread.raw]);
9094
+ const showChatProgress = agentProgress.hasLiveProgress || Boolean((_a = agentProgress.view.bundle) == null ? void 0 : _a.active);
9095
+ React49.useEffect(() => {
8123
9096
  updateLastEditQueueInfo(null);
8124
9097
  setSuppressQueueUntilResponse(false);
8125
9098
  setUpstreamSyncStatus(null);
8126
9099
  }, [activeAppId, updateLastEditQueueInfo]);
8127
- const handleSyncUpstream = React47.useCallback(async () => {
9100
+ const handleSyncUpstream = React49.useCallback(async () => {
8128
9101
  if (!(app == null ? void 0 : app.id)) {
8129
9102
  throw new Error("Missing app");
8130
9103
  }
@@ -8137,7 +9110,7 @@ function ComergeStudioInner({
8137
9110
  setSyncingUpstream(false);
8138
9111
  }
8139
9112
  }, [activeAppId, app == null ? void 0 : app.id]);
8140
- React47.useEffect(() => {
9113
+ React49.useEffect(() => {
8141
9114
  if (!(lastEditQueueInfo == null ? void 0 : lastEditQueueInfo.queueItemId)) return;
8142
9115
  const stillPresent = editQueue.items.some((item) => item.id === lastEditQueueInfo.queueItemId);
8143
9116
  if (!stillPresent) {
@@ -8145,31 +9118,32 @@ function ComergeStudioInner({
8145
9118
  setSuppressQueueUntilResponse(false);
8146
9119
  }
8147
9120
  }, [editQueue.items, lastEditQueueInfo == null ? void 0 : lastEditQueueInfo.queueItemId]);
8148
- const chatQueueItems = React47.useMemo(() => {
8149
- var _a;
9121
+ const chatQueueItems = React49.useMemo(() => {
9122
+ var _a2;
8150
9123
  if (suppressQueueUntilResponse && editQueue.items.length <= 1) {
8151
9124
  return [];
8152
9125
  }
8153
9126
  if (!lastEditQueueInfo || lastEditQueueInfo.queuePosition !== 1 || !lastEditQueueInfo.queueItemId) {
8154
9127
  return editQueue.items;
8155
9128
  }
8156
- if (editQueue.items.length === 1 && ((_a = editQueue.items[0]) == null ? void 0 : _a.id) === lastEditQueueInfo.queueItemId) {
9129
+ if (editQueue.items.length === 1 && ((_a2 = editQueue.items[0]) == null ? void 0 : _a2.id) === lastEditQueueInfo.queueItemId) {
8157
9130
  return [];
8158
9131
  }
8159
9132
  return editQueue.items;
8160
9133
  }, [editQueue.items, lastEditQueueInfo, suppressQueueUntilResponse]);
8161
- return /* @__PURE__ */ jsx60(View46, { style: [{ flex: 1 }, style], children: /* @__PURE__ */ jsxs37(View46, { ref: captureTargetRef, style: { flex: 1 }, collapsable: false, children: [
8162
- /* @__PURE__ */ jsx60(
9134
+ return /* @__PURE__ */ jsx62(View48, { style: [{ flex: 1 }, style], children: /* @__PURE__ */ jsxs39(View48, { ref: captureTargetRef, style: { flex: 1 }, collapsable: false, children: [
9135
+ /* @__PURE__ */ jsx62(
8163
9136
  RuntimeRenderer,
8164
9137
  {
8165
9138
  appKey,
8166
9139
  bundlePath: bundle.bundlePath,
9140
+ preparingText: runtimePreparingText,
8167
9141
  forcePreparing: showPostEditPreparing,
8168
9142
  renderToken: bundle.renderToken,
8169
9143
  allowInitialPreparing: !embeddedBaseBundles
8170
9144
  }
8171
9145
  ),
8172
- /* @__PURE__ */ jsx60(
9146
+ /* @__PURE__ */ jsx62(
8173
9147
  StudioOverlay,
8174
9148
  {
8175
9149
  captureTargetRef,
@@ -8228,6 +9202,7 @@ function ComergeStudioInner({
8228
9202
  onSendChat: (text, attachments) => actions.sendEdit({ prompt: text, attachments }),
8229
9203
  chatQueueItems,
8230
9204
  onRemoveQueueItem: (id) => editQueueActions.cancel(id),
9205
+ chatProgress: showChatProgress ? agentProgress.view : null,
8231
9206
  onNavigateHome,
8232
9207
  showBubble,
8233
9208
  studioControlOptions