@comergehq/studio 0.1.20 → 0.1.22

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
@@ -549,6 +549,12 @@ var AppsRemoteDataSourceImpl = class extends BaseRemote {
549
549
  );
550
550
  return data;
551
551
  }
552
+ async syncUpstream(appId) {
553
+ const { data } = await api.post(
554
+ `/v1/apps/${encodeURIComponent(appId)}/sync-upstream`
555
+ );
556
+ return data;
557
+ }
552
558
  };
553
559
  var appsRemoteDataSource = new AppsRemoteDataSourceImpl();
554
560
 
@@ -561,6 +567,95 @@ var BaseRepository = class {
561
567
  }
562
568
  };
563
569
 
570
+ // src/core/services/supabase/realtimeManager.ts
571
+ var INITIAL_BACKOFF_MS = 1e3;
572
+ var MAX_BACKOFF_MS = 3e4;
573
+ var realtimeLog = log.extend("realtime");
574
+ var entries = /* @__PURE__ */ new Map();
575
+ var subscriberIdCounter = 0;
576
+ function clearTimer(entry) {
577
+ if (!entry.timer) return;
578
+ clearTimeout(entry.timer);
579
+ entry.timer = null;
580
+ }
581
+ function buildChannel(entry) {
582
+ const supabase = getSupabaseClient();
583
+ const channel = supabase.channel(entry.key);
584
+ entry.subscribers.forEach((configure) => {
585
+ configure(channel);
586
+ });
587
+ return channel;
588
+ }
589
+ function scheduleResubscribe(entry, reason) {
590
+ if (entry.timer) return;
591
+ const delay = entry.backoffMs;
592
+ entry.backoffMs = Math.min(entry.backoffMs * 2, MAX_BACKOFF_MS);
593
+ realtimeLog.warn(`[realtime] channel ${entry.key} ${reason}; resubscribe in ${delay}ms`);
594
+ entry.timer = setTimeout(() => {
595
+ entry.timer = null;
596
+ if (!entries.has(entry.key)) return;
597
+ if (entry.subscribers.size === 0) return;
598
+ subscribeChannel(entry);
599
+ }, delay);
600
+ }
601
+ function handleStatus(entry, status) {
602
+ if (status === "SUBSCRIBED") {
603
+ entry.backoffMs = INITIAL_BACKOFF_MS;
604
+ clearTimer(entry);
605
+ return;
606
+ }
607
+ if (status === "CLOSED" || status === "TIMED_OUT" || status === "CHANNEL_ERROR") {
608
+ scheduleResubscribe(entry, status);
609
+ }
610
+ }
611
+ function subscribeChannel(entry) {
612
+ try {
613
+ const supabase = getSupabaseClient();
614
+ if (entry.channel) supabase.removeChannel(entry.channel);
615
+ const channel = buildChannel(entry);
616
+ entry.channel = channel;
617
+ channel.subscribe((status) => handleStatus(entry, status));
618
+ } catch (error) {
619
+ realtimeLog.warn("[realtime] subscribe failed", error);
620
+ scheduleResubscribe(entry, "SUBSCRIBE_FAILED");
621
+ }
622
+ }
623
+ function subscribeManagedChannel(key, configure) {
624
+ let entry = entries.get(key);
625
+ if (!entry) {
626
+ entry = {
627
+ key,
628
+ channel: null,
629
+ subscribers: /* @__PURE__ */ new Map(),
630
+ backoffMs: INITIAL_BACKOFF_MS,
631
+ timer: null
632
+ };
633
+ entries.set(key, entry);
634
+ }
635
+ const subscriberId = ++subscriberIdCounter;
636
+ entry.subscribers.set(subscriberId, configure);
637
+ if (!entry.channel) {
638
+ subscribeChannel(entry);
639
+ } else {
640
+ configure(entry.channel);
641
+ }
642
+ return () => {
643
+ const current = entries.get(key);
644
+ if (!current) return;
645
+ current.subscribers.delete(subscriberId);
646
+ if (current.subscribers.size === 0) {
647
+ clearTimer(current);
648
+ try {
649
+ if (current.channel) getSupabaseClient().removeChannel(current.channel);
650
+ } finally {
651
+ entries.delete(key);
652
+ }
653
+ return;
654
+ }
655
+ subscribeChannel(current);
656
+ };
657
+ }
658
+
564
659
  // src/data/apps/repository.ts
565
660
  function mapDbAppRow(row) {
566
661
  return {
@@ -627,6 +722,10 @@ var AppsRepositoryImpl = class extends BaseRepository {
627
722
  const res = await this.remote.importFromGithub(payload);
628
723
  return this.unwrapOrThrow(res);
629
724
  }
725
+ async syncUpstream(appId) {
726
+ const res = await this.remote.syncUpstream(appId);
727
+ return this.unwrapOrThrow(res);
728
+ }
630
729
  subscribeCreatedApps(userId, handlers) {
631
730
  if (!userId) return () => {
632
731
  };
@@ -638,35 +737,33 @@ var AppsRepositoryImpl = class extends BaseRepository {
638
737
  return this.subscribeToAppChannel(`apps:id:${appId}`, `id=eq.${appId}`, handlers);
639
738
  }
640
739
  subscribeToAppChannel(channelKey, filter, handlers) {
641
- const supabase = getSupabaseClient();
642
- const channel = supabase.channel(channelKey).on(
643
- "postgres_changes",
644
- { event: "INSERT", schema: "public", table: "app", filter },
645
- (payload) => {
646
- var _a;
647
- console.log("[subscribeToAppChannel] onInsert", payload);
648
- (_a = handlers.onInsert) == null ? void 0 : _a.call(handlers, mapDbAppRow(payload.new));
649
- }
650
- ).on(
651
- "postgres_changes",
652
- { event: "UPDATE", schema: "public", table: "app", filter },
653
- (payload) => {
654
- var _a;
655
- console.log("[subscribeToAppChannel] onUpdate", payload);
656
- (_a = handlers.onUpdate) == null ? void 0 : _a.call(handlers, mapDbAppRow(payload.new));
657
- }
658
- ).on(
659
- "postgres_changes",
660
- { event: "DELETE", schema: "public", table: "app", filter },
661
- (payload) => {
662
- var _a;
663
- console.log("[subscribeToAppChannel] onDelete", payload);
664
- (_a = handlers.onDelete) == null ? void 0 : _a.call(handlers, mapDbAppRow(payload.old));
665
- }
666
- ).subscribe();
667
- return () => {
668
- supabase.removeChannel(channel);
669
- };
740
+ return subscribeManagedChannel(channelKey, (channel) => {
741
+ channel.on(
742
+ "postgres_changes",
743
+ { event: "INSERT", schema: "public", table: "app", filter },
744
+ (payload) => {
745
+ var _a;
746
+ console.log("[subscribeToAppChannel] onInsert", payload);
747
+ (_a = handlers.onInsert) == null ? void 0 : _a.call(handlers, mapDbAppRow(payload.new));
748
+ }
749
+ ).on(
750
+ "postgres_changes",
751
+ { event: "UPDATE", schema: "public", table: "app", filter },
752
+ (payload) => {
753
+ var _a;
754
+ console.log("[subscribeToAppChannel] onUpdate", payload);
755
+ (_a = handlers.onUpdate) == null ? void 0 : _a.call(handlers, mapDbAppRow(payload.new));
756
+ }
757
+ ).on(
758
+ "postgres_changes",
759
+ { event: "DELETE", schema: "public", table: "app", filter },
760
+ (payload) => {
761
+ var _a;
762
+ console.log("[subscribeToAppChannel] onDelete", payload);
763
+ (_a = handlers.onDelete) == null ? void 0 : _a.call(handlers, mapDbAppRow(payload.old));
764
+ }
765
+ );
766
+ });
670
767
  }
671
768
  };
672
769
  var appsRepository = new AppsRepositoryImpl(appsRemoteDataSource);
@@ -797,35 +894,33 @@ var MessagesRepositoryImpl = class extends BaseRepository {
797
894
  return this.unwrapOrThrow(res);
798
895
  }
799
896
  subscribeThread(threadId, handlers) {
800
- const supabase = getSupabaseClient();
801
- const channel = supabase.channel(`messages:thread:${threadId}`).on(
802
- "postgres_changes",
803
- { event: "INSERT", schema: "public", table: "message", filter: `thread_id=eq.${threadId}` },
804
- (payload) => {
805
- var _a;
806
- const row = payload.new;
807
- (_a = handlers.onInsert) == null ? void 0 : _a.call(handlers, mapDbRowToMessage(row));
808
- }
809
- ).on(
810
- "postgres_changes",
811
- { event: "UPDATE", schema: "public", table: "message", filter: `thread_id=eq.${threadId}` },
812
- (payload) => {
813
- var _a;
814
- const row = payload.new;
815
- (_a = handlers.onUpdate) == null ? void 0 : _a.call(handlers, mapDbRowToMessage(row));
816
- }
817
- ).on(
818
- "postgres_changes",
819
- { event: "DELETE", schema: "public", table: "message", filter: `thread_id=eq.${threadId}` },
820
- (payload) => {
821
- var _a;
822
- const row = payload.old;
823
- (_a = handlers.onDelete) == null ? void 0 : _a.call(handlers, mapDbRowToMessage(row));
824
- }
825
- ).subscribe();
826
- return () => {
827
- supabase.removeChannel(channel);
828
- };
897
+ return subscribeManagedChannel(`messages:thread:${threadId}`, (channel) => {
898
+ channel.on(
899
+ "postgres_changes",
900
+ { event: "INSERT", schema: "public", table: "message", filter: `thread_id=eq.${threadId}` },
901
+ (payload) => {
902
+ var _a;
903
+ const row = payload.new;
904
+ (_a = handlers.onInsert) == null ? void 0 : _a.call(handlers, mapDbRowToMessage(row));
905
+ }
906
+ ).on(
907
+ "postgres_changes",
908
+ { event: "UPDATE", schema: "public", table: "message", filter: `thread_id=eq.${threadId}` },
909
+ (payload) => {
910
+ var _a;
911
+ const row = payload.new;
912
+ (_a = handlers.onUpdate) == null ? void 0 : _a.call(handlers, mapDbRowToMessage(row));
913
+ }
914
+ ).on(
915
+ "postgres_changes",
916
+ { event: "DELETE", schema: "public", table: "message", filter: `thread_id=eq.${threadId}` },
917
+ (payload) => {
918
+ var _a;
919
+ const row = payload.old;
920
+ (_a = handlers.onDelete) == null ? void 0 : _a.call(handlers, mapDbRowToMessage(row));
921
+ }
922
+ );
923
+ });
829
924
  }
830
925
  };
831
926
  var messagesRepository = new MessagesRepositoryImpl(messagesRemoteDataSource);
@@ -5390,6 +5485,9 @@ function ReviewMergeRequestCarousel({
5390
5485
  var import_jsx_runtime44 = require("react/jsx-runtime");
5391
5486
  function PreviewCollaborateSection({
5392
5487
  canSubmitMergeRequest,
5488
+ canSyncUpstream,
5489
+ syncingUpstream,
5490
+ upstreamSyncStatus,
5393
5491
  incomingMergeRequests,
5394
5492
  outgoingMergeRequests,
5395
5493
  creatorStatsById,
@@ -5398,15 +5496,18 @@ function PreviewCollaborateSection({
5398
5496
  testingMrId,
5399
5497
  toMergeRequestSummary,
5400
5498
  onSubmitMergeRequest,
5499
+ onSyncUpstream,
5401
5500
  onRequestApprove,
5402
5501
  onReject,
5403
5502
  onTestMr
5404
5503
  }) {
5405
5504
  const theme = useTheme();
5406
5505
  const [submittingMr, setSubmittingMr] = React32.useState(false);
5407
- const hasSection = canSubmitMergeRequest || incomingMergeRequests.length > 0 || outgoingMergeRequests.length > 0;
5506
+ const [syncingLocal, setSyncingLocal] = React32.useState(false);
5507
+ const hasSection = canSubmitMergeRequest || canSyncUpstream || incomingMergeRequests.length > 0 || outgoingMergeRequests.length > 0;
5408
5508
  if (!hasSection) return null;
5409
- const showActionsSubtitle = canSubmitMergeRequest && onSubmitMergeRequest || onTestMr && incomingMergeRequests.length > 0;
5509
+ const isSyncing = Boolean(syncingUpstream || syncingLocal);
5510
+ const showActionsSubtitle = canSubmitMergeRequest && onSubmitMergeRequest || canSyncUpstream && onSyncUpstream || onTestMr && incomingMergeRequests.length > 0;
5410
5511
  return /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)(import_jsx_runtime44.Fragment, { children: [
5411
5512
  /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(SectionTitle, { marginTop: theme.spacing.xl, children: "Collaborate" }),
5412
5513
  showActionsSubtitle ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
@@ -5475,6 +5576,64 @@ function PreviewCollaborateSection({
5475
5576
  right: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_lucide_react_native9.Send, { size: 16, color: "#03DAC6" })
5476
5577
  }
5477
5578
  ) : null,
5579
+ canSyncUpstream && onSyncUpstream ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
5580
+ PressableCardRow,
5581
+ {
5582
+ accessibilityLabel: "Sync from original",
5583
+ disabled: isSyncing,
5584
+ onPress: () => {
5585
+ import_react_native42.Alert.alert(
5586
+ "Sync from Original",
5587
+ "This will pull the latest upstream changes into your remix.",
5588
+ [
5589
+ { text: "Cancel", style: "cancel" },
5590
+ {
5591
+ text: "Sync",
5592
+ style: "destructive",
5593
+ onPress: () => {
5594
+ setSyncingLocal(true);
5595
+ Promise.resolve(onSyncUpstream()).then((result) => {
5596
+ if ((result == null ? void 0 : result.status) === "up-to-date") {
5597
+ import_react_native42.Alert.alert("Up to date", "Your remix already includes the latest upstream changes.");
5598
+ } else {
5599
+ import_react_native42.Alert.alert("Sync started", "Upstream changes are being merged into your remix.");
5600
+ }
5601
+ }).catch(() => {
5602
+ import_react_native42.Alert.alert("Sync failed", "We could not start the sync. Please try again.");
5603
+ }).finally(() => setSyncingLocal(false));
5604
+ }
5605
+ }
5606
+ ]
5607
+ );
5608
+ },
5609
+ style: {
5610
+ padding: theme.spacing.lg,
5611
+ borderRadius: theme.radii.lg,
5612
+ backgroundColor: withAlpha(theme.colors.surfaceRaised, 0.5),
5613
+ borderWidth: 1,
5614
+ borderColor: withAlpha(theme.colors.primary, 0.25),
5615
+ marginBottom: theme.spacing.sm
5616
+ },
5617
+ left: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
5618
+ import_react_native42.View,
5619
+ {
5620
+ style: {
5621
+ width: 40,
5622
+ height: 40,
5623
+ borderRadius: 999,
5624
+ alignItems: "center",
5625
+ justifyContent: "center",
5626
+ backgroundColor: withAlpha(theme.colors.primary, 0.12),
5627
+ marginRight: theme.spacing.lg
5628
+ },
5629
+ children: isSyncing ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_react_native42.ActivityIndicator, { color: theme.colors.primary, size: "small" }) : /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_lucide_react_native9.RefreshCw, { size: 18, color: theme.colors.primary })
5630
+ }
5631
+ ),
5632
+ title: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(Text, { style: { color: theme.colors.text, fontSize: 16, lineHeight: 20, fontWeight: theme.typography.fontWeight.semibold }, children: "Sync from Original" }),
5633
+ subtitle: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(Text, { style: { color: theme.colors.textMuted, fontSize: 12, lineHeight: 16, marginTop: 2 }, children: isSyncing ? "Syncing upstream changes..." : upstreamSyncStatus === "up-to-date" ? "You are already up to date with the original app" : "Pull the latest upstream changes into this remix" }),
5634
+ right: /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(import_lucide_react_native9.RefreshCw, { size: 16, color: theme.colors.primary })
5635
+ }
5636
+ ) : null,
5478
5637
  onTestMr && incomingMergeRequests.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime44.jsx)(
5479
5638
  ReviewMergeRequestCarousel,
5480
5639
  {
@@ -5782,6 +5941,12 @@ function usePreviewPanelData(params) {
5782
5941
  if (app.headCommitId && app.forkedFromCommitId && app.headCommitId !== app.forkedFromCommitId) return true;
5783
5942
  return false;
5784
5943
  }, [app, isOwner, outgoingMergeRequests]);
5944
+ const canSyncUpstream = React34.useMemo(() => {
5945
+ if (!isOwner) return false;
5946
+ if (!app) return false;
5947
+ if (!app.forkedFromAppId) return false;
5948
+ return app.status === "ready";
5949
+ }, [app, isOwner]);
5785
5950
  const showProcessing = app ? app.status !== "ready" : false;
5786
5951
  return {
5787
5952
  imageUrl,
@@ -5791,7 +5956,8 @@ function usePreviewPanelData(params) {
5791
5956
  insights,
5792
5957
  stats,
5793
5958
  showProcessing,
5794
- canSubmitMergeRequest
5959
+ canSubmitMergeRequest,
5960
+ canSyncUpstream
5795
5961
  };
5796
5962
  }
5797
5963
 
@@ -5814,6 +5980,9 @@ function PreviewPanel({
5814
5980
  onGoToChat,
5815
5981
  onStartDraw,
5816
5982
  onSubmitMergeRequest,
5983
+ onSyncUpstream,
5984
+ syncingUpstream,
5985
+ upstreamSyncStatus,
5817
5986
  onRequestApprove,
5818
5987
  onReject,
5819
5988
  onTestMr,
@@ -5841,7 +6010,17 @@ ${shareUrl}`;
5841
6010
  log.warn("PreviewPanel share failed", error);
5842
6011
  }
5843
6012
  }, [app]);
5844
- const { imageUrl, imageLoaded, setImageLoaded, creator, insights, stats, showProcessing, canSubmitMergeRequest } = usePreviewPanelData({
6013
+ const {
6014
+ imageUrl,
6015
+ imageLoaded,
6016
+ setImageLoaded,
6017
+ creator,
6018
+ insights,
6019
+ stats,
6020
+ showProcessing,
6021
+ canSubmitMergeRequest,
6022
+ canSyncUpstream
6023
+ } = usePreviewPanelData({
5845
6024
  app,
5846
6025
  isOwner,
5847
6026
  outgoingMergeRequests,
@@ -5901,6 +6080,9 @@ ${shareUrl}`;
5901
6080
  PreviewCollaborateSection,
5902
6081
  {
5903
6082
  canSubmitMergeRequest,
6083
+ canSyncUpstream,
6084
+ syncingUpstream,
6085
+ upstreamSyncStatus,
5904
6086
  incomingMergeRequests,
5905
6087
  outgoingMergeRequests,
5906
6088
  creatorStatsById,
@@ -5909,6 +6091,7 @@ ${shareUrl}`;
5909
6091
  testingMrId,
5910
6092
  toMergeRequestSummary,
5911
6093
  onSubmitMergeRequest,
6094
+ onSyncUpstream,
5912
6095
  onRequestApprove,
5913
6096
  onReject,
5914
6097
  onTestMr
@@ -5943,6 +6126,8 @@ function ChatMessageBubble({ message, renderContent, style }) {
5943
6126
  const isMergeApproved = metaEvent === "merge_request.approved";
5944
6127
  const isMergeRejected = metaEvent === "merge_request.rejected";
5945
6128
  const isMergeCompleted = metaEvent === "merge.completed";
6129
+ const isSyncStarted = metaEvent === "sync.started";
6130
+ const isSyncCompleted = metaEvent === "sync.completed";
5946
6131
  const isHuman = message.author === "human" || isMergeApproved || isMergeRejected;
5947
6132
  const align = { alignSelf: isHuman ? "flex-end" : "flex-start" };
5948
6133
  const bubbleVariant = isHuman ? "surface" : "surfaceRaised";
@@ -5964,8 +6149,8 @@ function ChatMessageBubble({ message, renderContent, style }) {
5964
6149
  cornerStyle
5965
6150
  ],
5966
6151
  children: /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(import_react_native44.View, { style: { flexDirection: "row", alignItems: "center" }, children: [
5967
- isMergeCompleted ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_lucide_react_native10.CheckCheck, { size: 16, color: theme.colors.success, style: { marginRight: theme.spacing.sm } }) : null,
5968
- isMergeApproved ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_lucide_react_native10.GitMerge, { size: 16, color: theme.colors.text, style: { marginRight: theme.spacing.sm } }) : null,
6152
+ isMergeCompleted || isSyncCompleted ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_lucide_react_native10.CheckCheck, { size: 16, color: theme.colors.success, style: { marginRight: theme.spacing.sm } }) : null,
6153
+ isMergeApproved || isSyncStarted ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_lucide_react_native10.GitMerge, { size: 16, color: theme.colors.text, style: { marginRight: theme.spacing.sm } }) : null,
5969
6154
  /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(import_react_native44.View, { style: { flexShrink: 1, minWidth: 0 }, children: renderContent ? renderContent(message) : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(MarkdownText, { markdown: message.content, variant: "chat", bodyColor }) })
5970
6155
  ] })
5971
6156
  }
@@ -6928,6 +7113,9 @@ function StudioOverlay({
6928
7113
  testingMrId,
6929
7114
  toMergeRequestSummary,
6930
7115
  onSubmitMergeRequest,
7116
+ onSyncUpstream,
7117
+ syncingUpstream,
7118
+ upstreamSyncStatus,
6931
7119
  onApprove,
6932
7120
  onReject,
6933
7121
  onTestMr,
@@ -7072,6 +7260,9 @@ function StudioOverlay({
7072
7260
  onGoToChat: goToChat,
7073
7261
  onStartDraw: isOwner ? startDraw : void 0,
7074
7262
  onSubmitMergeRequest,
7263
+ onSyncUpstream,
7264
+ syncingUpstream,
7265
+ upstreamSyncStatus,
7075
7266
  onRequestApprove: (mr) => setConfirmMrId(mr.id),
7076
7267
  onReject,
7077
7268
  onTestMr: handleTestMr,
@@ -7217,42 +7408,40 @@ var EditQueueRepositoryImpl = class extends BaseRepository {
7217
7408
  return this.unwrapOrThrow(res);
7218
7409
  }
7219
7410
  subscribeEditQueue(appId, handlers) {
7220
- const supabase = getSupabaseClient();
7221
- const channel = supabase.channel(`edit-queue:app:${appId}`).on(
7222
- "postgres_changes",
7223
- { event: "INSERT", schema: "public", table: "app_job_queue", filter: `app_id=eq.${appId}` },
7224
- (payload) => {
7225
- var _a;
7226
- const row = payload.new;
7227
- if (row.kind !== "edit") return;
7228
- const item = mapQueueItem(row);
7229
- if (!ACTIVE_STATUSES.includes(item.status)) return;
7230
- (_a = handlers.onInsert) == null ? void 0 : _a.call(handlers, item);
7231
- }
7232
- ).on(
7233
- "postgres_changes",
7234
- { event: "UPDATE", schema: "public", table: "app_job_queue", filter: `app_id=eq.${appId}` },
7235
- (payload) => {
7236
- var _a, _b;
7237
- const row = payload.new;
7238
- if (row.kind !== "edit") return;
7239
- const item = mapQueueItem(row);
7240
- if (ACTIVE_STATUSES.includes(item.status)) (_a = handlers.onUpdate) == null ? void 0 : _a.call(handlers, item);
7241
- else (_b = handlers.onDelete) == null ? void 0 : _b.call(handlers, item);
7242
- }
7243
- ).on(
7244
- "postgres_changes",
7245
- { event: "DELETE", schema: "public", table: "app_job_queue", filter: `app_id=eq.${appId}` },
7246
- (payload) => {
7247
- var _a;
7248
- const row = payload.old;
7249
- if (row.kind !== "edit") return;
7250
- (_a = handlers.onDelete) == null ? void 0 : _a.call(handlers, mapQueueItem(row));
7251
- }
7252
- ).subscribe();
7253
- return () => {
7254
- supabase.removeChannel(channel);
7255
- };
7411
+ return subscribeManagedChannel(`edit-queue:app:${appId}`, (channel) => {
7412
+ channel.on(
7413
+ "postgres_changes",
7414
+ { event: "INSERT", schema: "public", table: "app_job_queue", filter: `app_id=eq.${appId}` },
7415
+ (payload) => {
7416
+ var _a;
7417
+ const row = payload.new;
7418
+ if (row.kind !== "edit") return;
7419
+ const item = mapQueueItem(row);
7420
+ if (!ACTIVE_STATUSES.includes(item.status)) return;
7421
+ (_a = handlers.onInsert) == null ? void 0 : _a.call(handlers, item);
7422
+ }
7423
+ ).on(
7424
+ "postgres_changes",
7425
+ { event: "UPDATE", schema: "public", table: "app_job_queue", filter: `app_id=eq.${appId}` },
7426
+ (payload) => {
7427
+ var _a, _b;
7428
+ const row = payload.new;
7429
+ if (row.kind !== "edit") return;
7430
+ const item = mapQueueItem(row);
7431
+ if (ACTIVE_STATUSES.includes(item.status)) (_a = handlers.onUpdate) == null ? void 0 : _a.call(handlers, item);
7432
+ else (_b = handlers.onDelete) == null ? void 0 : _b.call(handlers, item);
7433
+ }
7434
+ ).on(
7435
+ "postgres_changes",
7436
+ { event: "DELETE", schema: "public", table: "app_job_queue", filter: `app_id=eq.${appId}` },
7437
+ (payload) => {
7438
+ var _a;
7439
+ const row = payload.old;
7440
+ if (row.kind !== "edit") return;
7441
+ (_a = handlers.onDelete) == null ? void 0 : _a.call(handlers, mapQueueItem(row));
7442
+ }
7443
+ );
7444
+ });
7256
7445
  }
7257
7446
  };
7258
7447
  var editQueueRepository = new EditQueueRepositoryImpl(
@@ -7500,6 +7689,8 @@ function ComergeStudioInner({
7500
7689
  const chatSendDisabled = false;
7501
7690
  const [processingMrId, setProcessingMrId] = React47.useState(null);
7502
7691
  const [testingMrId, setTestingMrId] = React47.useState(null);
7692
+ const [syncingUpstream, setSyncingUpstream] = React47.useState(false);
7693
+ const [upstreamSyncStatus, setUpstreamSyncStatus] = React47.useState(null);
7503
7694
  const chatShowTypingIndicator = React47.useMemo(() => {
7504
7695
  var _a;
7505
7696
  if (!thread.raw || thread.raw.length === 0) return false;
@@ -7510,7 +7701,21 @@ function ComergeStudioInner({
7510
7701
  React47.useEffect(() => {
7511
7702
  updateLastEditQueueInfo(null);
7512
7703
  setSuppressQueueUntilResponse(false);
7704
+ setUpstreamSyncStatus(null);
7513
7705
  }, [activeAppId, updateLastEditQueueInfo]);
7706
+ const handleSyncUpstream = React47.useCallback(async () => {
7707
+ if (!(app == null ? void 0 : app.id)) {
7708
+ throw new Error("Missing app");
7709
+ }
7710
+ setSyncingUpstream(true);
7711
+ try {
7712
+ const result = await appsRepository.syncUpstream(activeAppId);
7713
+ setUpstreamSyncStatus(result.status);
7714
+ return result;
7715
+ } finally {
7716
+ setSyncingUpstream(false);
7717
+ }
7718
+ }, [activeAppId, app == null ? void 0 : app.id]);
7514
7719
  React47.useEffect(() => {
7515
7720
  if (!(lastEditQueueInfo == null ? void 0 : lastEditQueueInfo.queueItemId)) return;
7516
7721
  const stillPresent = editQueue.items.some((item) => item.id === lastEditQueueInfo.queueItemId);
@@ -7566,6 +7771,9 @@ function ComergeStudioInner({
7566
7771
  onSubmitMergeRequest: (app == null ? void 0 : app.forkedFromAppId) && actions.isOwner && !hasOpenOutgoingMr ? async () => {
7567
7772
  await mergeRequests.actions.openMergeRequest(activeAppId);
7568
7773
  } : void 0,
7774
+ onSyncUpstream: actions.isOwner && (app == null ? void 0 : app.forkedFromAppId) ? handleSyncUpstream : void 0,
7775
+ syncingUpstream,
7776
+ upstreamSyncStatus,
7569
7777
  onApprove: async (mr) => {
7570
7778
  if (processingMrId) return;
7571
7779
  setProcessingMrId(mr.id);