@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.mjs CHANGED
@@ -2869,6 +2869,13 @@ var AttachmentRemoteDataSourceImpl = class extends BaseRemote {
2869
2869
  );
2870
2870
  return data;
2871
2871
  }
2872
+ async stagePresign(payload) {
2873
+ const { data } = await api.post(
2874
+ "/v1/attachments/stage/presign",
2875
+ payload
2876
+ );
2877
+ return data;
2878
+ }
2872
2879
  };
2873
2880
  var attachmentRemoteDataSource = new AttachmentRemoteDataSourceImpl();
2874
2881
 
@@ -2882,6 +2889,10 @@ var AttachmentRepositoryImpl = class extends BaseRepository {
2882
2889
  const res = await this.remote.presign(payload);
2883
2890
  return this.unwrapOrThrow(res);
2884
2891
  }
2892
+ async stagePresign(payload) {
2893
+ const res = await this.remote.stagePresign(payload);
2894
+ return this.unwrapOrThrow(res);
2895
+ }
2885
2896
  async upload(upload, file) {
2886
2897
  const resp = await fetch(upload.uploadUrl, {
2887
2898
  method: "PUT",
@@ -2892,6 +2903,16 @@ var AttachmentRepositoryImpl = class extends BaseRepository {
2892
2903
  throw new Error(`upload failed: ${resp.status}`);
2893
2904
  }
2894
2905
  }
2906
+ async uploadStaged(upload, file) {
2907
+ const resp = await fetch(upload.uploadUrl, {
2908
+ method: "PUT",
2909
+ headers: upload.headers,
2910
+ body: file
2911
+ });
2912
+ if (!resp.ok) {
2913
+ throw new Error(`staged upload failed: ${resp.status}`);
2914
+ }
2915
+ }
2895
2916
  };
2896
2917
  var attachmentRepository = new AttachmentRepositoryImpl(
2897
2918
  attachmentRemoteDataSource
@@ -2965,7 +2986,36 @@ function useAttachmentUpload() {
2965
2986
  setUploading(false);
2966
2987
  }
2967
2988
  }, []);
2968
- return { uploadBase64Images, uploading, error };
2989
+ const stageBase64Images = React7.useCallback(async ({ dataUrls }) => {
2990
+ if (!dataUrls || dataUrls.length === 0) return [];
2991
+ setUploading(true);
2992
+ setError(null);
2993
+ try {
2994
+ const blobs = await Promise.all(
2995
+ dataUrls.map(async (dataUrl) => {
2996
+ const normalized = dataUrl.startsWith("data:") ? dataUrl : `data:image/png;base64,${dataUrl}`;
2997
+ const blob = Platform2.OS === "android" ? await dataUrlToBlobAndroid(normalized) : await (await fetch(normalized)).blob();
2998
+ const mimeType = getMimeTypeFromDataUrl(normalized);
2999
+ return { blob, mimeType };
3000
+ })
3001
+ );
3002
+ const files = blobs.map(({ blob, mimeType }, idx) => ({
3003
+ name: `attachment-${Date.now()}-${idx}.png`,
3004
+ size: blob.size,
3005
+ mimeType
3006
+ }));
3007
+ const presign = await attachmentRepository.stagePresign({ files });
3008
+ await Promise.all(presign.uploads.map((u, index) => attachmentRepository.uploadStaged(u, blobs[index].blob)));
3009
+ return presign.uploads.map((u) => u.attachmentToken);
3010
+ } catch (e) {
3011
+ const err = e instanceof Error ? e : new Error(String(e));
3012
+ setError(err);
3013
+ throw err;
3014
+ } finally {
3015
+ setUploading(false);
3016
+ }
3017
+ }, []);
3018
+ return { uploadBase64Images, stageBase64Images, uploading, error };
2969
3019
  }
2970
3020
 
2971
3021
  // src/studio/hooks/useStudioActions.ts
@@ -2981,6 +3031,10 @@ var AgentRemoteDataSourceImpl = class extends BaseRemote {
2981
3031
  const { data } = await api.post("/v1/agent/editApp", payload);
2982
3032
  return data;
2983
3033
  }
3034
+ async forkEditStart(payload) {
3035
+ const { data } = await api.post("/v1/agent/forkEditStart", payload);
3036
+ return data;
3037
+ }
2984
3038
  };
2985
3039
  var agentRemoteDataSource = new AgentRemoteDataSourceImpl();
2986
3040
 
@@ -2998,6 +3052,10 @@ var AgentRepositoryImpl = class extends BaseRepository {
2998
3052
  const res = await this.remote.editApp(payload);
2999
3053
  return this.unwrapOrThrow(res);
3000
3054
  }
3055
+ async forkEditStart(payload) {
3056
+ const res = await this.remote.forkEditStart(payload);
3057
+ return this.unwrapOrThrow(res);
3058
+ }
3001
3059
  };
3002
3060
  var agentRepository = new AgentRepositoryImpl(agentRemoteDataSource);
3003
3061
 
@@ -3048,7 +3106,8 @@ function useStudioActions({
3048
3106
  onEditStart,
3049
3107
  onEditQueued,
3050
3108
  onEditFinished,
3051
- uploadAttachments
3109
+ uploadAttachments,
3110
+ stageAttachments
3052
3111
  }) {
3053
3112
  const [forking, setForking] = React8.useState(false);
3054
3113
  const [sending, setSending] = React8.useState(false);
@@ -3068,16 +3127,46 @@ function useStudioActions({
3068
3127
  const sourceAppId = app.id;
3069
3128
  if (shouldForkOnEdit) {
3070
3129
  setForking(true);
3071
- const forked = await appsRepository.fork(app.id, {});
3072
- targetApp = forked;
3130
+ let attachmentTokens;
3131
+ if (attachments && attachments.length > 0 && stageAttachments) {
3132
+ attachmentTokens = await stageAttachments({ dataUrls: attachments });
3133
+ }
3134
+ const idempotencyKey2 = generateIdempotencyKey();
3135
+ const startResult = await withRetry2(
3136
+ async () => await agentRepository.forkEditStart({
3137
+ source_app_id: sourceAppId,
3138
+ prompt,
3139
+ attachmentTokens,
3140
+ idempotencyKey: idempotencyKey2
3141
+ }),
3142
+ { attempts: 3, baseDelayMs: 500, maxDelayMs: 4e3 }
3143
+ );
3144
+ targetApp = {
3145
+ ...app,
3146
+ id: startResult.targetAppId,
3147
+ threadId: startResult.targetThreadId
3148
+ };
3073
3149
  await trackRemixApp({
3074
- appId: forked.id,
3150
+ appId: startResult.targetAppId,
3075
3151
  sourceAppId,
3076
- threadId: forked.threadId ?? void 0,
3152
+ threadId: startResult.targetThreadId ?? void 0,
3077
3153
  success: true
3078
3154
  });
3079
3155
  forkSucceeded = true;
3080
- onForkedApp == null ? void 0 : onForkedApp(forked.id, { keepRenderingAppId: sourceAppId });
3156
+ onForkedApp == null ? void 0 : onForkedApp(startResult.targetAppId, { keepRenderingAppId: sourceAppId });
3157
+ onEditStart == null ? void 0 : onEditStart();
3158
+ onEditQueued == null ? void 0 : onEditQueued({
3159
+ queueItemId: startResult.queueItemId ?? null,
3160
+ queuePosition: startResult.queuePosition ?? null
3161
+ });
3162
+ await trackEditApp({
3163
+ appId: startResult.targetAppId,
3164
+ threadId: startResult.targetThreadId,
3165
+ promptLength: prompt.trim().length,
3166
+ success: true
3167
+ });
3168
+ setForking(false);
3169
+ return;
3081
3170
  }
3082
3171
  setForking(false);
3083
3172
  const threadId = targetApp.threadId;
@@ -3138,7 +3227,18 @@ function useStudioActions({
3138
3227
  onEditFinished == null ? void 0 : onEditFinished();
3139
3228
  }
3140
3229
  },
3141
- [app, onEditFinished, onEditQueued, onEditStart, onForkedApp, sending, shouldForkOnEdit, uploadAttachments, userId]
3230
+ [
3231
+ app,
3232
+ onEditFinished,
3233
+ onEditQueued,
3234
+ onEditStart,
3235
+ onForkedApp,
3236
+ sending,
3237
+ shouldForkOnEdit,
3238
+ stageAttachments,
3239
+ uploadAttachments,
3240
+ userId
3241
+ ]
3142
3242
  );
3143
3243
  return { isOwner, shouldForkOnEdit, forking, sending, error, sendEdit };
3144
3244
  }
@@ -5899,9 +5999,9 @@ function PreviewRelatedAppsSection({
5899
5999
  const renderRelatedCard = React27.useCallback(
5900
6000
  (item, options) => {
5901
6001
  const isCurrent = item.app.id === currentAppId;
5902
- const isReady = item.app.status === "ready";
6002
+ const isArchived = item.app.status === "archived";
5903
6003
  const isSwitching = switchingRelatedAppId === item.app.id;
5904
- const disabled = isCurrent || !isReady || Boolean(switchingRelatedAppId);
6004
+ const disabled = isCurrent || isArchived || Boolean(switchingRelatedAppId);
5905
6005
  return /* @__PURE__ */ jsx36(
5906
6006
  Pressable9,
5907
6007
  {
@@ -5941,7 +6041,7 @@ function PreviewRelatedAppsSection({
5941
6041
  )
5942
6042
  ] }),
5943
6043
  /* @__PURE__ */ jsxs19(View24, { style: { alignItems: "flex-end", gap: 6 }, children: [
5944
- /* @__PURE__ */ jsx36(View24, { style: { minHeight: 20, justifyContent: "center" }, children: !isReady ? /* @__PURE__ */ jsx36(PreviewStatusBadge, { status: item.app.status }) : null }),
6044
+ /* @__PURE__ */ jsx36(View24, { style: { minHeight: 20, justifyContent: "center" }, children: item.app.status !== "ready" ? /* @__PURE__ */ jsx36(PreviewStatusBadge, { status: item.app.status }) : null }),
5945
6045
  isSwitching ? /* @__PURE__ */ jsx36(ActivityIndicator4, { size: "small", color: theme.colors.primary }) : null
5946
6046
  ] })
5947
6047
  ] })
@@ -8431,7 +8531,7 @@ function ChatPanel({
8431
8531
  showTypingIndicator,
8432
8532
  loading,
8433
8533
  sendDisabled,
8434
- forking = false,
8534
+ forking: _forking = false,
8435
8535
  sending,
8436
8536
  shouldForkOnEdit,
8437
8537
  attachments = [],
@@ -8490,7 +8590,7 @@ function ChatPanel({
8490
8590
  style: { marginBottom: 12 }
8491
8591
  }
8492
8592
  ) : null;
8493
- const showMessagesLoading = Boolean(loading) && messages.length === 0 || forking;
8593
+ const showMessagesLoading = Boolean(loading) && messages.length === 0;
8494
8594
  if (showMessagesLoading) {
8495
8595
  return /* @__PURE__ */ jsxs37(View46, { style: { flex: 1 }, children: [
8496
8596
  /* @__PURE__ */ jsx59(View46, { children: header }),
@@ -8498,14 +8598,14 @@ function ChatPanel({
8498
8598
  /* @__PURE__ */ jsxs37(View46, { style: { flex: 1, alignItems: "center", justifyContent: "center", paddingHorizontal: 24, paddingVertical: 12 }, children: [
8499
8599
  /* @__PURE__ */ jsx59(ActivityIndicator10, {}),
8500
8600
  /* @__PURE__ */ jsx59(View46, { style: { height: 12 } }),
8501
- /* @__PURE__ */ jsx59(Text, { variant: "bodyMuted", children: forking ? "Creating your copy\u2026" : "Loading messages\u2026" })
8601
+ /* @__PURE__ */ jsx59(Text, { variant: "bodyMuted", children: "Loading messages\u2026" })
8502
8602
  ] })
8503
8603
  ] });
8504
8604
  }
8505
8605
  const bundleProgress = (progress == null ? void 0 : progress.bundle) ?? null;
8506
8606
  const queueTop = progress || queueItems.length > 0 ? /* @__PURE__ */ jsxs37(View46, { style: { gap: theme.spacing.sm }, children: [
8507
8607
  progress ? bundleProgress ? /* @__PURE__ */ jsx59(BundleProgressCard, { progress: bundleProgress }) : /* @__PURE__ */ jsx59(AgentProgressCard, { progress }) : null,
8508
- queueItems.length > 0 ? /* @__PURE__ */ jsx59(ChatQueue, { items: queueItems, onRemove: onRemoveQueueItem }) : null
8608
+ !progress && queueItems.length > 0 ? /* @__PURE__ */ jsx59(ChatQueue, { items: queueItems, onRemove: onRemoveQueueItem }) : null
8509
8609
  ] }) : null;
8510
8610
  return /* @__PURE__ */ jsx59(
8511
8611
  ChatPage,
@@ -8532,8 +8632,8 @@ function ChatPanel({
8532
8632
  composer: {
8533
8633
  // Keep the input editable even when sending is disallowed (e.g. agent still working),
8534
8634
  // otherwise iOS will drop focus/keyboard and BottomSheet can get "stuck" with a keyboard-sized gap.
8535
- disabled: Boolean(loading) || Boolean(forking),
8536
- sendDisabled: Boolean(sendDisabled) || Boolean(loading) || Boolean(forking),
8635
+ disabled: Boolean(loading),
8636
+ sendDisabled: Boolean(sendDisabled) || Boolean(loading),
8537
8637
  sending: Boolean(sending),
8538
8638
  onSend: handleSend,
8539
8639
  attachments,
@@ -8749,7 +8849,7 @@ function isOptimisticResolvedByServer(chatMessages, o) {
8749
8849
  }
8750
8850
  function useOptimisticChatMessages({
8751
8851
  threadId,
8752
- shouldForkOnEdit,
8852
+ shouldForkOnEdit: _shouldForkOnEdit,
8753
8853
  disableOptimistic = false,
8754
8854
  chatMessages,
8755
8855
  onSendChat
@@ -8784,7 +8884,7 @@ function useOptimisticChatMessages({
8784
8884
  }, [chatMessages, optimisticChat.length]);
8785
8885
  const onSend = React45.useCallback(
8786
8886
  async (text, attachments) => {
8787
- if (shouldForkOnEdit || disableOptimistic) {
8887
+ if (disableOptimistic) {
8788
8888
  await onSendChat(text, attachments);
8789
8889
  return;
8790
8890
  }
@@ -8808,11 +8908,11 @@ function useOptimisticChatMessages({
8808
8908
  setOptimisticChat((prev) => prev.map((m) => m.id === id ? { ...m, failed: true } : m));
8809
8909
  });
8810
8910
  },
8811
- [chatMessages, disableOptimistic, onSendChat, shouldForkOnEdit]
8911
+ [chatMessages, disableOptimistic, onSendChat]
8812
8912
  );
8813
8913
  const onRetry = React45.useCallback(
8814
8914
  async (messageId) => {
8815
- if (shouldForkOnEdit || disableOptimistic) return;
8915
+ if (disableOptimistic) return;
8816
8916
  const target = optimisticChat.find((m) => m.id === messageId);
8817
8917
  if (!target || target.retrying) return;
8818
8918
  const baseServerLastId = chatMessages.length > 0 ? chatMessages[chatMessages.length - 1].id : null;
@@ -8832,7 +8932,7 @@ function useOptimisticChatMessages({
8832
8932
  );
8833
8933
  }
8834
8934
  },
8835
- [chatMessages, disableOptimistic, onSendChat, optimisticChat, shouldForkOnEdit]
8935
+ [chatMessages, disableOptimistic, onSendChat, optimisticChat]
8836
8936
  );
8837
8937
  const isRetrying = React45.useCallback(
8838
8938
  (messageId) => {
@@ -9988,6 +10088,7 @@ function ComergeStudioInner({
9988
10088
  }
9989
10089
  },
9990
10090
  uploadAttachments: uploader.uploadBase64Images,
10091
+ stageAttachments: uploader.stageBase64Images,
9991
10092
  onEditStart: () => {
9992
10093
  if (editQueue.items.length === 0) {
9993
10094
  setSuppressQueueUntilResponse(true);
@@ -10013,7 +10114,26 @@ function ComergeStudioInner({
10013
10114
  const [upstreamSyncStatus, setUpstreamSyncStatus] = React51.useState(null);
10014
10115
  const isMrTestBuildInProgress = bundle.loading && bundle.loadingMode === "test";
10015
10116
  const isBaseBundleDownloading = bundle.loading && bundle.loadingMode === "base" && !bundle.isTesting;
10016
- const runtimePreparingText = bundle.bundleStatus === "pending" ? "Bundling app\u2026 this may take a few minutes" : "Preparing app\u2026";
10117
+ const runtimePreparingText = React51.useMemo(() => {
10118
+ const status = app == null ? void 0 : app.status;
10119
+ if (status === "ready" && bundle.bundleStatus === "pending") {
10120
+ return "Bundling app\u2026 this may take a few minutes";
10121
+ }
10122
+ switch (status) {
10123
+ case "creating":
10124
+ return "Creating your app\u2026 this may take a moment";
10125
+ case "forking":
10126
+ return "Forking your app\u2026";
10127
+ case "editing":
10128
+ return "Applying your latest changes\u2026";
10129
+ case "merging":
10130
+ return "Merging app updates\u2026";
10131
+ case "error":
10132
+ return "This app hit an error while preparing.";
10133
+ default:
10134
+ return "Preparing app\u2026";
10135
+ }
10136
+ }, [app == null ? void 0 : app.status, bundle.bundleStatus]);
10017
10137
  const chatShowTypingIndicator = React51.useMemo(() => {
10018
10138
  var _a2;
10019
10139
  if (agentProgress.hasLiveProgress) return false;
@@ -10080,9 +10200,9 @@ function ComergeStudioInner({
10080
10200
  setSwitchingRelatedAppId(targetAppId);
10081
10201
  try {
10082
10202
  const targetApp = await appsRepository.getById(targetAppId);
10083
- if (targetApp.status !== "ready") {
10084
- const reason = `target_not_ready:${targetApp.status}`;
10085
- log.warn("[related-apps] switch blocked: target app not ready", {
10203
+ if (targetApp.status === "archived") {
10204
+ const reason = "target_archived";
10205
+ log.warn("[related-apps] switch blocked: target app archived", {
10086
10206
  fromAppId: activeAppId,
10087
10207
  toAppId: targetAppId,
10088
10208
  status: targetApp.status