@comergehq/studio 0.1.31 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2899,6 +2899,13 @@ var AttachmentRemoteDataSourceImpl = class extends BaseRemote {
2899
2899
  );
2900
2900
  return data;
2901
2901
  }
2902
+ async stagePresign(payload) {
2903
+ const { data } = await api.post(
2904
+ "/v1/attachments/stage/presign",
2905
+ payload
2906
+ );
2907
+ return data;
2908
+ }
2902
2909
  };
2903
2910
  var attachmentRemoteDataSource = new AttachmentRemoteDataSourceImpl();
2904
2911
 
@@ -2912,6 +2919,10 @@ var AttachmentRepositoryImpl = class extends BaseRepository {
2912
2919
  const res = await this.remote.presign(payload);
2913
2920
  return this.unwrapOrThrow(res);
2914
2921
  }
2922
+ async stagePresign(payload) {
2923
+ const res = await this.remote.stagePresign(payload);
2924
+ return this.unwrapOrThrow(res);
2925
+ }
2915
2926
  async upload(upload, file) {
2916
2927
  const resp = await fetch(upload.uploadUrl, {
2917
2928
  method: "PUT",
@@ -2922,6 +2933,16 @@ var AttachmentRepositoryImpl = class extends BaseRepository {
2922
2933
  throw new Error(`upload failed: ${resp.status}`);
2923
2934
  }
2924
2935
  }
2936
+ async uploadStaged(upload, file) {
2937
+ const resp = await fetch(upload.uploadUrl, {
2938
+ method: "PUT",
2939
+ headers: upload.headers,
2940
+ body: file
2941
+ });
2942
+ if (!resp.ok) {
2943
+ throw new Error(`staged upload failed: ${resp.status}`);
2944
+ }
2945
+ }
2925
2946
  };
2926
2947
  var attachmentRepository = new AttachmentRepositoryImpl(
2927
2948
  attachmentRemoteDataSource
@@ -2995,7 +3016,36 @@ function useAttachmentUpload() {
2995
3016
  setUploading(false);
2996
3017
  }
2997
3018
  }, []);
2998
- return { uploadBase64Images, uploading, error };
3019
+ const stageBase64Images = React7.useCallback(async ({ dataUrls }) => {
3020
+ if (!dataUrls || dataUrls.length === 0) return [];
3021
+ setUploading(true);
3022
+ setError(null);
3023
+ try {
3024
+ const blobs = await Promise.all(
3025
+ dataUrls.map(async (dataUrl) => {
3026
+ const normalized = dataUrl.startsWith("data:") ? dataUrl : `data:image/png;base64,${dataUrl}`;
3027
+ const blob = import_react_native6.Platform.OS === "android" ? await dataUrlToBlobAndroid(normalized) : await (await fetch(normalized)).blob();
3028
+ const mimeType = getMimeTypeFromDataUrl(normalized);
3029
+ return { blob, mimeType };
3030
+ })
3031
+ );
3032
+ const files = blobs.map(({ blob, mimeType }, idx) => ({
3033
+ name: `attachment-${Date.now()}-${idx}.png`,
3034
+ size: blob.size,
3035
+ mimeType
3036
+ }));
3037
+ const presign = await attachmentRepository.stagePresign({ files });
3038
+ await Promise.all(presign.uploads.map((u, index) => attachmentRepository.uploadStaged(u, blobs[index].blob)));
3039
+ return presign.uploads.map((u) => u.attachmentToken);
3040
+ } catch (e) {
3041
+ const err = e instanceof Error ? e : new Error(String(e));
3042
+ setError(err);
3043
+ throw err;
3044
+ } finally {
3045
+ setUploading(false);
3046
+ }
3047
+ }, []);
3048
+ return { uploadBase64Images, stageBase64Images, uploading, error };
2999
3049
  }
3000
3050
 
3001
3051
  // src/studio/hooks/useStudioActions.ts
@@ -3011,6 +3061,10 @@ var AgentRemoteDataSourceImpl = class extends BaseRemote {
3011
3061
  const { data } = await api.post("/v1/agent/editApp", payload);
3012
3062
  return data;
3013
3063
  }
3064
+ async forkEditStart(payload) {
3065
+ const { data } = await api.post("/v1/agent/forkEditStart", payload);
3066
+ return data;
3067
+ }
3014
3068
  };
3015
3069
  var agentRemoteDataSource = new AgentRemoteDataSourceImpl();
3016
3070
 
@@ -3028,6 +3082,10 @@ var AgentRepositoryImpl = class extends BaseRepository {
3028
3082
  const res = await this.remote.editApp(payload);
3029
3083
  return this.unwrapOrThrow(res);
3030
3084
  }
3085
+ async forkEditStart(payload) {
3086
+ const res = await this.remote.forkEditStart(payload);
3087
+ return this.unwrapOrThrow(res);
3088
+ }
3031
3089
  };
3032
3090
  var agentRepository = new AgentRepositoryImpl(agentRemoteDataSource);
3033
3091
 
@@ -3078,7 +3136,8 @@ function useStudioActions({
3078
3136
  onEditStart,
3079
3137
  onEditQueued,
3080
3138
  onEditFinished,
3081
- uploadAttachments
3139
+ uploadAttachments,
3140
+ stageAttachments
3082
3141
  }) {
3083
3142
  const [forking, setForking] = React8.useState(false);
3084
3143
  const [sending, setSending] = React8.useState(false);
@@ -3098,16 +3157,46 @@ function useStudioActions({
3098
3157
  const sourceAppId = app.id;
3099
3158
  if (shouldForkOnEdit) {
3100
3159
  setForking(true);
3101
- const forked = await appsRepository.fork(app.id, {});
3102
- targetApp = forked;
3160
+ let attachmentTokens;
3161
+ if (attachments && attachments.length > 0 && stageAttachments) {
3162
+ attachmentTokens = await stageAttachments({ dataUrls: attachments });
3163
+ }
3164
+ const idempotencyKey2 = generateIdempotencyKey();
3165
+ const startResult = await withRetry2(
3166
+ async () => await agentRepository.forkEditStart({
3167
+ source_app_id: sourceAppId,
3168
+ prompt,
3169
+ attachmentTokens,
3170
+ idempotencyKey: idempotencyKey2
3171
+ }),
3172
+ { attempts: 3, baseDelayMs: 500, maxDelayMs: 4e3 }
3173
+ );
3174
+ targetApp = {
3175
+ ...app,
3176
+ id: startResult.targetAppId,
3177
+ threadId: startResult.targetThreadId
3178
+ };
3103
3179
  await trackRemixApp({
3104
- appId: forked.id,
3180
+ appId: startResult.targetAppId,
3105
3181
  sourceAppId,
3106
- threadId: forked.threadId ?? void 0,
3182
+ threadId: startResult.targetThreadId ?? void 0,
3107
3183
  success: true
3108
3184
  });
3109
3185
  forkSucceeded = true;
3110
- onForkedApp == null ? void 0 : onForkedApp(forked.id, { keepRenderingAppId: sourceAppId });
3186
+ onForkedApp == null ? void 0 : onForkedApp(startResult.targetAppId, { keepRenderingAppId: sourceAppId });
3187
+ onEditStart == null ? void 0 : onEditStart();
3188
+ onEditQueued == null ? void 0 : onEditQueued({
3189
+ queueItemId: startResult.queueItemId ?? null,
3190
+ queuePosition: startResult.queuePosition ?? null
3191
+ });
3192
+ await trackEditApp({
3193
+ appId: startResult.targetAppId,
3194
+ threadId: startResult.targetThreadId,
3195
+ promptLength: prompt.trim().length,
3196
+ success: true
3197
+ });
3198
+ setForking(false);
3199
+ return;
3111
3200
  }
3112
3201
  setForking(false);
3113
3202
  const threadId = targetApp.threadId;
@@ -3168,7 +3257,18 @@ function useStudioActions({
3168
3257
  onEditFinished == null ? void 0 : onEditFinished();
3169
3258
  }
3170
3259
  },
3171
- [app, onEditFinished, onEditQueued, onEditStart, onForkedApp, sending, shouldForkOnEdit, uploadAttachments, userId]
3260
+ [
3261
+ app,
3262
+ onEditFinished,
3263
+ onEditQueued,
3264
+ onEditStart,
3265
+ onForkedApp,
3266
+ sending,
3267
+ shouldForkOnEdit,
3268
+ stageAttachments,
3269
+ uploadAttachments,
3270
+ userId
3271
+ ]
3172
3272
  );
3173
3273
  return { isOwner, shouldForkOnEdit, forking, sending, error, sendEdit };
3174
3274
  }
@@ -5866,9 +5966,9 @@ function PreviewRelatedAppsSection({
5866
5966
  const renderRelatedCard = React27.useCallback(
5867
5967
  (item, options) => {
5868
5968
  const isCurrent = item.app.id === currentAppId;
5869
- const isReady = item.app.status === "ready";
5969
+ const isArchived = item.app.status === "archived";
5870
5970
  const isSwitching = switchingRelatedAppId === item.app.id;
5871
- const disabled = isCurrent || !isReady || Boolean(switchingRelatedAppId);
5971
+ const disabled = isCurrent || isArchived || Boolean(switchingRelatedAppId);
5872
5972
  return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
5873
5973
  import_react_native35.Pressable,
5874
5974
  {
@@ -5908,7 +6008,7 @@ function PreviewRelatedAppsSection({
5908
6008
  )
5909
6009
  ] }),
5910
6010
  /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(import_react_native35.View, { style: { alignItems: "flex-end", gap: 6 }, children: [
5911
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_react_native35.View, { style: { minHeight: 20, justifyContent: "center" }, children: !isReady ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(PreviewStatusBadge, { status: item.app.status }) : null }),
6011
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_react_native35.View, { style: { minHeight: 20, justifyContent: "center" }, children: item.app.status !== "ready" ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(PreviewStatusBadge, { status: item.app.status }) : null }),
5912
6012
  isSwitching ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(import_react_native35.ActivityIndicator, { size: "small", color: theme.colors.primary }) : null
5913
6013
  ] })
5914
6014
  ] })
@@ -8396,7 +8496,7 @@ function ChatPanel({
8396
8496
  showTypingIndicator,
8397
8497
  loading,
8398
8498
  sendDisabled,
8399
- forking = false,
8499
+ forking: _forking = false,
8400
8500
  sending,
8401
8501
  shouldForkOnEdit,
8402
8502
  attachments = [],
@@ -8455,7 +8555,7 @@ function ChatPanel({
8455
8555
  style: { marginBottom: 12 }
8456
8556
  }
8457
8557
  ) : null;
8458
- const showMessagesLoading = Boolean(loading) && messages.length === 0 || forking;
8558
+ const showMessagesLoading = Boolean(loading) && messages.length === 0;
8459
8559
  if (showMessagesLoading) {
8460
8560
  return /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(import_react_native58.View, { style: { flex: 1 }, children: [
8461
8561
  /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(import_react_native58.View, { children: header }),
@@ -8463,14 +8563,14 @@ function ChatPanel({
8463
8563
  /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(import_react_native58.View, { style: { flex: 1, alignItems: "center", justifyContent: "center", paddingHorizontal: 24, paddingVertical: 12 }, children: [
8464
8564
  /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(import_react_native58.ActivityIndicator, {}),
8465
8565
  /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(import_react_native58.View, { style: { height: 12 } }),
8466
- /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(Text, { variant: "bodyMuted", children: forking ? "Creating your copy\u2026" : "Loading messages\u2026" })
8566
+ /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(Text, { variant: "bodyMuted", children: "Loading messages\u2026" })
8467
8567
  ] })
8468
8568
  ] });
8469
8569
  }
8470
8570
  const bundleProgress = (progress == null ? void 0 : progress.bundle) ?? null;
8471
8571
  const queueTop = progress || queueItems.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime59.jsxs)(import_react_native58.View, { style: { gap: theme.spacing.sm }, children: [
8472
8572
  progress ? bundleProgress ? /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(BundleProgressCard, { progress: bundleProgress }) : /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(AgentProgressCard, { progress }) : null,
8473
- queueItems.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(ChatQueue, { items: queueItems, onRemove: onRemoveQueueItem }) : null
8573
+ !progress && queueItems.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(ChatQueue, { items: queueItems, onRemove: onRemoveQueueItem }) : null
8474
8574
  ] }) : null;
8475
8575
  return /* @__PURE__ */ (0, import_jsx_runtime59.jsx)(
8476
8576
  ChatPage,
@@ -8497,8 +8597,8 @@ function ChatPanel({
8497
8597
  composer: {
8498
8598
  // Keep the input editable even when sending is disallowed (e.g. agent still working),
8499
8599
  // otherwise iOS will drop focus/keyboard and BottomSheet can get "stuck" with a keyboard-sized gap.
8500
- disabled: Boolean(loading) || Boolean(forking),
8501
- sendDisabled: Boolean(sendDisabled) || Boolean(loading) || Boolean(forking),
8600
+ disabled: Boolean(loading),
8601
+ sendDisabled: Boolean(sendDisabled) || Boolean(loading),
8502
8602
  sending: Boolean(sending),
8503
8603
  onSend: handleSend,
8504
8604
  attachments,
@@ -8714,7 +8814,7 @@ function isOptimisticResolvedByServer(chatMessages, o) {
8714
8814
  }
8715
8815
  function useOptimisticChatMessages({
8716
8816
  threadId,
8717
- shouldForkOnEdit,
8817
+ shouldForkOnEdit: _shouldForkOnEdit,
8718
8818
  disableOptimistic = false,
8719
8819
  chatMessages,
8720
8820
  onSendChat
@@ -8749,7 +8849,7 @@ function useOptimisticChatMessages({
8749
8849
  }, [chatMessages, optimisticChat.length]);
8750
8850
  const onSend = React45.useCallback(
8751
8851
  async (text, attachments) => {
8752
- if (shouldForkOnEdit || disableOptimistic) {
8852
+ if (disableOptimistic) {
8753
8853
  await onSendChat(text, attachments);
8754
8854
  return;
8755
8855
  }
@@ -8773,11 +8873,11 @@ function useOptimisticChatMessages({
8773
8873
  setOptimisticChat((prev) => prev.map((m) => m.id === id ? { ...m, failed: true } : m));
8774
8874
  });
8775
8875
  },
8776
- [chatMessages, disableOptimistic, onSendChat, shouldForkOnEdit]
8876
+ [chatMessages, disableOptimistic, onSendChat]
8777
8877
  );
8778
8878
  const onRetry = React45.useCallback(
8779
8879
  async (messageId) => {
8780
- if (shouldForkOnEdit || disableOptimistic) return;
8880
+ if (disableOptimistic) return;
8781
8881
  const target = optimisticChat.find((m) => m.id === messageId);
8782
8882
  if (!target || target.retrying) return;
8783
8883
  const baseServerLastId = chatMessages.length > 0 ? chatMessages[chatMessages.length - 1].id : null;
@@ -8797,7 +8897,7 @@ function useOptimisticChatMessages({
8797
8897
  );
8798
8898
  }
8799
8899
  },
8800
- [chatMessages, disableOptimistic, onSendChat, optimisticChat, shouldForkOnEdit]
8900
+ [chatMessages, disableOptimistic, onSendChat, optimisticChat]
8801
8901
  );
8802
8902
  const isRetrying = React45.useCallback(
8803
8903
  (messageId) => {
@@ -9950,6 +10050,7 @@ function ComergeStudioInner({
9950
10050
  }
9951
10051
  },
9952
10052
  uploadAttachments: uploader.uploadBase64Images,
10053
+ stageAttachments: uploader.stageBase64Images,
9953
10054
  onEditStart: () => {
9954
10055
  if (editQueue.items.length === 0) {
9955
10056
  setSuppressQueueUntilResponse(true);
@@ -9975,7 +10076,26 @@ function ComergeStudioInner({
9975
10076
  const [upstreamSyncStatus, setUpstreamSyncStatus] = React51.useState(null);
9976
10077
  const isMrTestBuildInProgress = bundle.loading && bundle.loadingMode === "test";
9977
10078
  const isBaseBundleDownloading = bundle.loading && bundle.loadingMode === "base" && !bundle.isTesting;
9978
- const runtimePreparingText = bundle.bundleStatus === "pending" ? "Bundling app\u2026 this may take a few minutes" : "Preparing app\u2026";
10079
+ const runtimePreparingText = React51.useMemo(() => {
10080
+ const status = app == null ? void 0 : app.status;
10081
+ if (status === "ready" && bundle.bundleStatus === "pending") {
10082
+ return "Bundling app\u2026 this may take a few minutes";
10083
+ }
10084
+ switch (status) {
10085
+ case "creating":
10086
+ return "Creating your app\u2026 this may take a moment";
10087
+ case "forking":
10088
+ return "Forking your app\u2026";
10089
+ case "editing":
10090
+ return "Applying your latest changes\u2026";
10091
+ case "merging":
10092
+ return "Merging app updates\u2026";
10093
+ case "error":
10094
+ return "This app hit an error while preparing.";
10095
+ default:
10096
+ return "Preparing app\u2026";
10097
+ }
10098
+ }, [app == null ? void 0 : app.status, bundle.bundleStatus]);
9979
10099
  const chatShowTypingIndicator = React51.useMemo(() => {
9980
10100
  var _a2;
9981
10101
  if (agentProgress.hasLiveProgress) return false;
@@ -10042,9 +10162,9 @@ function ComergeStudioInner({
10042
10162
  setSwitchingRelatedAppId(targetAppId);
10043
10163
  try {
10044
10164
  const targetApp = await appsRepository.getById(targetAppId);
10045
- if (targetApp.status !== "ready") {
10046
- const reason = `target_not_ready:${targetApp.status}`;
10047
- log.warn("[related-apps] switch blocked: target app not ready", {
10165
+ if (targetApp.status === "archived") {
10166
+ const reason = "target_archived";
10167
+ log.warn("[related-apps] switch blocked: target app archived", {
10048
10168
  fromAppId: activeAppId,
10049
10169
  toAppId: targetAppId,
10050
10170
  status: targetApp.status