paperclip-github-plugin 0.2.2 → 0.2.3

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/ui/index.js CHANGED
@@ -680,12 +680,18 @@ function getActiveRateLimitPause(syncState, referenceTimeMs = Date.now()) {
680
680
  ...syncState.errorDetails.rateLimitResource ? { resource: syncState.errorDetails.rateLimitResource } : {}
681
681
  };
682
682
  }
683
+ function isSyncCancellationRequested(syncState) {
684
+ return syncState.status === "running" && Boolean(syncState.cancelRequestedAt?.trim());
685
+ }
683
686
  function getSyncToastTitle(syncState) {
684
687
  if (getActiveRateLimitPause(syncState)) {
685
688
  return "GitHub sync is paused";
686
689
  }
690
+ if (syncState.status === "cancelled") {
691
+ return "GitHub sync was cancelled";
692
+ }
687
693
  if (syncState.status === "running") {
688
- return "GitHub sync is running";
694
+ return isSyncCancellationRequested(syncState) ? "GitHub sync is stopping" : "GitHub sync is running";
689
695
  }
690
696
  return syncState.status === "error" ? "GitHub sync needs attention" : "GitHub sync finished";
691
697
  }
@@ -694,7 +700,7 @@ function getSyncToastBody(syncState) {
694
700
  return syncState.message.trim();
695
701
  }
696
702
  if (syncState.status === "running") {
697
- return "GitHub sync is running in the background.";
703
+ return isSyncCancellationRequested(syncState) ? "Cancellation requested. GitHub sync will stop after the current step finishes." : "GitHub sync is running in the background.";
698
704
  }
699
705
  return "GitHub sync completed.";
700
706
  }
@@ -702,7 +708,7 @@ function getSyncToastTone(syncState) {
702
708
  if (getActiveRateLimitPause(syncState)) {
703
709
  return "info";
704
710
  }
705
- if (syncState.status === "running") {
711
+ if (syncState.status === "running" || syncState.status === "cancelled") {
706
712
  return "info";
707
713
  }
708
714
  return syncState.status === "error" ? "error" : "success";
@@ -833,6 +839,10 @@ var SHARED_PROGRESS_STYLES = `
833
839
  align-items: stretch;
834
840
  flex-direction: column;
835
841
  }
842
+
843
+ .ghsync-diagnostics__layout--split {
844
+ grid-template-columns: 1fr;
845
+ }
836
846
  }
837
847
  `;
838
848
  var PAGE_STYLES = `
@@ -1060,6 +1070,79 @@ var PAGE_STYLES = `
1060
1070
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
1061
1071
  }
1062
1072
 
1073
+ .ghsync-diagnostics__layout {
1074
+ display: grid;
1075
+ gap: 10px;
1076
+ }
1077
+
1078
+ .ghsync-diagnostics__layout--split {
1079
+ grid-template-columns: minmax(220px, 300px) minmax(0, 1fr);
1080
+ align-items: start;
1081
+ }
1082
+
1083
+ .ghsync-diagnostics__detail,
1084
+ .ghsync-diagnostics__failures {
1085
+ display: grid;
1086
+ gap: 10px;
1087
+ }
1088
+
1089
+ .ghsync-diagnostics__failures {
1090
+ max-height: 420px;
1091
+ overflow: auto;
1092
+ margin: 0;
1093
+ padding: 0;
1094
+ list-style: none;
1095
+ }
1096
+
1097
+ .ghsync-diagnostics__failure {
1098
+ display: grid;
1099
+ gap: 6px;
1100
+ width: 100%;
1101
+ padding: 12px;
1102
+ border-radius: 10px;
1103
+ border: 1px solid var(--ghsync-dangerBorder);
1104
+ background: var(--ghsync-surfaceAlt);
1105
+ text-align: left;
1106
+ transition: border-color 160ms ease, background 160ms ease, transform 160ms ease;
1107
+ }
1108
+
1109
+ .ghsync-diagnostics__failure-item {
1110
+ list-style: none;
1111
+ }
1112
+
1113
+ .ghsync-diagnostics__failure:hover {
1114
+ border-color: var(--ghsync-dangerText);
1115
+ transform: translateY(-1px);
1116
+ }
1117
+
1118
+ .ghsync-diagnostics__failure--active {
1119
+ border-color: var(--ghsync-dangerText);
1120
+ background: color-mix(in srgb, var(--ghsync-dangerBg) 35%, var(--ghsync-surfaceAlt));
1121
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--ghsync-dangerText) 22%, transparent);
1122
+ }
1123
+
1124
+ .ghsync-diagnostics__failure-title {
1125
+ color: var(--ghsync-title);
1126
+ font-size: 13px;
1127
+ line-height: 1.4;
1128
+ }
1129
+
1130
+ .ghsync-diagnostics__failure-meta {
1131
+ color: var(--ghsync-muted);
1132
+ font-size: 11px;
1133
+ line-height: 1.4;
1134
+ }
1135
+
1136
+ .ghsync-diagnostics__failure-preview {
1137
+ color: var(--ghsync-text);
1138
+ font-size: 12px;
1139
+ line-height: 1.5;
1140
+ display: -webkit-box;
1141
+ -webkit-box-orient: vertical;
1142
+ -webkit-line-clamp: 3;
1143
+ overflow: hidden;
1144
+ }
1145
+
1063
1146
  .ghsync-diagnostics__item,
1064
1147
  .ghsync-diagnostics__block {
1065
1148
  display: grid;
@@ -2033,6 +2116,72 @@ var WIDGET_STYLES = `
2033
2116
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
2034
2117
  }
2035
2118
 
2119
+ .ghsync-diagnostics__layout {
2120
+ display: grid;
2121
+ gap: 10px;
2122
+ }
2123
+
2124
+ .ghsync-diagnostics__layout--split {
2125
+ grid-template-columns: minmax(200px, 240px) minmax(0, 1fr);
2126
+ align-items: start;
2127
+ }
2128
+
2129
+ .ghsync-diagnostics__detail,
2130
+ .ghsync-diagnostics__failures {
2131
+ display: grid;
2132
+ gap: 10px;
2133
+ }
2134
+
2135
+ .ghsync-diagnostics__failures {
2136
+ max-height: 320px;
2137
+ overflow: auto;
2138
+ margin: 0;
2139
+ padding: 0;
2140
+ list-style: none;
2141
+ }
2142
+
2143
+ .ghsync-diagnostics__failure {
2144
+ display: grid;
2145
+ gap: 6px;
2146
+ width: 100%;
2147
+ padding: 12px;
2148
+ border-radius: 10px;
2149
+ border: 1px solid var(--ghsync-dangerBorder);
2150
+ background: var(--ghsync-surfaceAlt);
2151
+ text-align: left;
2152
+ }
2153
+
2154
+ .ghsync-diagnostics__failure-item {
2155
+ list-style: none;
2156
+ }
2157
+
2158
+ .ghsync-diagnostics__failure--active {
2159
+ border-color: var(--ghsync-dangerText);
2160
+ background: color-mix(in srgb, var(--ghsync-dangerBg) 35%, var(--ghsync-surfaceAlt));
2161
+ }
2162
+
2163
+ .ghsync-diagnostics__failure-title {
2164
+ color: var(--ghsync-title);
2165
+ font-size: 12px;
2166
+ line-height: 1.4;
2167
+ }
2168
+
2169
+ .ghsync-diagnostics__failure-meta {
2170
+ color: var(--ghsync-muted);
2171
+ font-size: 11px;
2172
+ line-height: 1.4;
2173
+ }
2174
+
2175
+ .ghsync-diagnostics__failure-preview {
2176
+ color: var(--ghsync-text);
2177
+ font-size: 12px;
2178
+ line-height: 1.5;
2179
+ display: -webkit-box;
2180
+ -webkit-box-orient: vertical;
2181
+ -webkit-line-clamp: 3;
2182
+ overflow: hidden;
2183
+ }
2184
+
2036
2185
  .ghsync-diagnostics__item,
2037
2186
  .ghsync-diagnostics__block {
2038
2187
  display: grid;
@@ -2173,6 +2322,10 @@ var WIDGET_STYLES = `
2173
2322
  .ghsync-widget__link {
2174
2323
  flex: 1 1 auto;
2175
2324
  }
2325
+
2326
+ .ghsync-diagnostics__layout--split {
2327
+ grid-template-columns: 1fr;
2328
+ }
2176
2329
  }
2177
2330
 
2178
2331
  ${SHARED_PROGRESS_STYLES}
@@ -3079,7 +3232,10 @@ function getSyncStatus(syncState, runningSync, syncUnlocked) {
3079
3232
  return { label: "Locked", tone: "neutral" };
3080
3233
  }
3081
3234
  if (runningSync || syncState.status === "running") {
3082
- return { label: "Running", tone: "info" };
3235
+ return {
3236
+ label: isSyncCancellationRequested(syncState) ? "Cancelling" : "Running",
3237
+ tone: "info"
3238
+ };
3083
3239
  }
3084
3240
  if (getActiveRateLimitPause(syncState)) {
3085
3241
  return { label: "Paused", tone: "warning" };
@@ -3090,6 +3246,9 @@ function getSyncStatus(syncState, runningSync, syncUnlocked) {
3090
3246
  if (syncState.status === "success") {
3091
3247
  return { label: "Ready", tone: "success" };
3092
3248
  }
3249
+ if (syncState.status === "cancelled") {
3250
+ return { label: "Cancelled", tone: "neutral" };
3251
+ }
3093
3252
  return { label: "Ready", tone: "info" };
3094
3253
  }
3095
3254
  function getToneClass(tone) {
@@ -3360,10 +3519,10 @@ function getDashboardSummary(params) {
3360
3519
  if (params.runningSync || params.syncState.status === "running") {
3361
3520
  const progress = getRunningSyncProgressModel(params.syncState);
3362
3521
  return {
3363
- label: "Syncing",
3522
+ label: isSyncCancellationRequested(params.syncState) ? "Cancelling" : "Syncing",
3364
3523
  tone: "info",
3365
- title: progress?.title ?? "Sync in progress",
3366
- body: progress?.description ?? "GitHub issues are being checked right now. This card refreshes automatically until the run finishes."
3524
+ title: isSyncCancellationRequested(params.syncState) ? "Stopping the current sync" : progress?.title ?? "Sync in progress",
3525
+ body: isSyncCancellationRequested(params.syncState) ? "GitHub Sync will stop after the current repository or issue step finishes." : progress?.description ?? "GitHub issues are being checked right now. This card refreshes automatically until the run finishes."
3367
3526
  };
3368
3527
  }
3369
3528
  if (activeRateLimitPause) {
@@ -3382,6 +3541,14 @@ function getDashboardSummary(params) {
3382
3541
  body: params.syncState.message ?? "Open settings to review the latest GitHub sync issue."
3383
3542
  };
3384
3543
  }
3544
+ if (params.syncState.status === "cancelled") {
3545
+ return {
3546
+ label: "Cancelled",
3547
+ tone: "neutral",
3548
+ title: "Sync cancelled",
3549
+ body: params.syncState.message ?? "The last GitHub sync was cancelled before it finished."
3550
+ };
3551
+ }
3385
3552
  if (params.syncState.checkedAt) {
3386
3553
  return {
3387
3554
  label: "Ready",
@@ -3491,16 +3658,28 @@ function formatSyncFailureRepository(repositoryUrl) {
3491
3658
  }
3492
3659
  return repositoryUrl.trim();
3493
3660
  }
3494
- function getSyncDiagnostics(syncState) {
3661
+ function getSyncFailureLogEntries(syncState) {
3662
+ if (syncState.recentFailures?.length) {
3663
+ return syncState.recentFailures.filter((entry) => typeof entry.message === "string" && entry.message.trim());
3664
+ }
3495
3665
  if (syncState.status !== "error") {
3496
- return null;
3666
+ return [];
3497
3667
  }
3668
+ return [
3669
+ {
3670
+ message: syncState.message?.trim() || "GitHub sync failed.",
3671
+ occurredAt: syncState.checkedAt,
3672
+ ...syncState.errorDetails ?? {}
3673
+ }
3674
+ ];
3675
+ }
3676
+ function getSyncDiagnostics(entry) {
3498
3677
  const rows = [];
3499
- const repositoryLabel = formatSyncFailureRepository(syncState.errorDetails?.repositoryUrl);
3500
- const phaseLabel = formatSyncFailurePhase(syncState.errorDetails?.phase);
3501
- const issueNumber = syncState.errorDetails?.githubIssueNumber;
3502
- const rateLimitResetAt = syncState.errorDetails?.rateLimitResetAt;
3503
- const rateLimitResourceLabel = getGitHubRateLimitResourceLabel(syncState.errorDetails?.rateLimitResource);
3678
+ const repositoryLabel = formatSyncFailureRepository(entry.repositoryUrl);
3679
+ const phaseLabel = formatSyncFailurePhase(entry.phase);
3680
+ const issueNumber = entry.githubIssueNumber;
3681
+ const rateLimitResetAt = entry.rateLimitResetAt;
3682
+ const rateLimitResourceLabel = getGitHubRateLimitResourceLabel(entry.rateLimitResource);
3504
3683
  if (repositoryLabel) {
3505
3684
  rows.push({
3506
3685
  label: "Repository",
@@ -3531,12 +3710,19 @@ function getSyncDiagnostics(syncState) {
3531
3710
  value: formatDate(rateLimitResetAt, rateLimitResetAt)
3532
3711
  });
3533
3712
  }
3534
- const rawMessage = syncState.errorDetails?.rawMessage && syncState.errorDetails.rawMessage !== syncState.message ? syncState.errorDetails.rawMessage : void 0;
3535
- const suggestedAction = syncState.errorDetails?.suggestedAction;
3536
- if (rows.length === 0 && !rawMessage && !suggestedAction) {
3713
+ if (entry.occurredAt) {
3714
+ rows.push({
3715
+ label: "Captured",
3716
+ value: formatDate(entry.occurredAt, entry.occurredAt)
3717
+ });
3718
+ }
3719
+ const rawMessage = entry.rawMessage && entry.rawMessage !== entry.message ? entry.rawMessage : void 0;
3720
+ const suggestedAction = entry.suggestedAction;
3721
+ if (!entry.message && rows.length === 0 && !rawMessage && !suggestedAction) {
3537
3722
  return null;
3538
3723
  }
3539
3724
  return {
3725
+ message: entry.message,
3540
3726
  rows,
3541
3727
  ...rawMessage ? { rawMessage } : {},
3542
3728
  ...suggestedAction ? { suggestedAction } : {}
@@ -3590,31 +3776,76 @@ function SyncProgressPanel(props) {
3590
3776
  );
3591
3777
  }
3592
3778
  function SyncDiagnosticsPanel(props) {
3593
- const diagnostics = getSyncDiagnostics(props.syncState);
3779
+ const failureEntries = getSyncFailureLogEntries(props.syncState);
3780
+ const latestFailureIndex = Math.max(failureEntries.length - 1, 0);
3781
+ const [selectedFailureIndex, setSelectedFailureIndex] = useState(latestFailureIndex);
3782
+ const selectedFailure = failureEntries[Math.min(selectedFailureIndex, latestFailureIndex)];
3783
+ const diagnostics = selectedFailure ? getSyncDiagnostics(selectedFailure) : null;
3594
3784
  const requestError = props.requestError?.trim() ? props.requestError.trim() : null;
3785
+ const canSelectFailures = !props.compact && failureEntries.length > 1;
3786
+ const savedFailureCount = props.syncState.erroredIssuesCount ?? failureEntries.length;
3787
+ useEffect(() => {
3788
+ setSelectedFailureIndex(latestFailureIndex);
3789
+ }, [latestFailureIndex, props.syncState.checkedAt, props.syncState.status]);
3595
3790
  if (!diagnostics && !requestError) {
3596
3791
  return null;
3597
3792
  }
3598
3793
  return /* @__PURE__ */ jsxs("section", { className: `ghsync-diagnostics${props.compact ? " ghsync-diagnostics--compact" : ""}`, children: [
3599
3794
  /* @__PURE__ */ jsxs("div", { className: "ghsync-diagnostics__header", children: [
3600
3795
  /* @__PURE__ */ jsx("strong", { children: diagnostics ? "Troubleshooting details" : "Sync request failed" }),
3601
- /* @__PURE__ */ jsx("span", { children: diagnostics ? "GitHub Sync saved this snapshot from the latest failed run." : "The sync request failed before the worker returned a saved result." })
3796
+ /* @__PURE__ */ jsx("span", { children: diagnostics ? canSelectFailures ? savedFailureCount > failureEntries.length ? `GitHub Sync saved the latest ${failureEntries.length} of ${savedFailureCount} failures from the latest run. Select one to inspect.` : `GitHub Sync saved ${failureEntries.length} failure${failureEntries.length === 1 ? "" : "s"} from the latest run. Select one to inspect.` : "GitHub Sync saved this snapshot from the latest failed run." : "The sync request failed before the worker returned a saved result." })
3602
3797
  ] }),
3603
3798
  requestError ? /* @__PURE__ */ jsxs("div", { className: "ghsync-diagnostics__block", children: [
3604
3799
  /* @__PURE__ */ jsx("span", { className: "ghsync-diagnostics__label", children: "Request error" }),
3605
3800
  /* @__PURE__ */ jsx("div", { className: "ghsync-diagnostics__value ghsync-diagnostics__value--code", children: requestError })
3606
3801
  ] }) : null,
3607
- diagnostics?.rows.length ? /* @__PURE__ */ jsx("div", { className: "ghsync-diagnostics__grid", children: diagnostics.rows.map((row) => /* @__PURE__ */ jsxs("div", { className: "ghsync-diagnostics__item", children: [
3608
- /* @__PURE__ */ jsx("span", { className: "ghsync-diagnostics__label", children: row.label }),
3609
- /* @__PURE__ */ jsx("strong", { className: "ghsync-diagnostics__value", children: row.value })
3610
- ] }, row.label)) }) : null,
3611
- diagnostics?.rawMessage ? /* @__PURE__ */ jsxs("div", { className: "ghsync-diagnostics__block", children: [
3612
- /* @__PURE__ */ jsx("span", { className: "ghsync-diagnostics__label", children: "Raw error" }),
3613
- /* @__PURE__ */ jsx("div", { className: "ghsync-diagnostics__value ghsync-diagnostics__value--code", children: diagnostics.rawMessage })
3614
- ] }) : null,
3615
- diagnostics?.suggestedAction ? /* @__PURE__ */ jsxs("div", { className: "ghsync-diagnostics__block", children: [
3616
- /* @__PURE__ */ jsx("span", { className: "ghsync-diagnostics__label", children: "Next step" }),
3617
- /* @__PURE__ */ jsx("div", { className: "ghsync-diagnostics__value", children: diagnostics.suggestedAction })
3802
+ diagnostics ? /* @__PURE__ */ jsxs("div", { className: `ghsync-diagnostics__layout${canSelectFailures ? " ghsync-diagnostics__layout--split" : ""}`, children: [
3803
+ canSelectFailures ? /* @__PURE__ */ jsx("ul", { className: "ghsync-diagnostics__failures", "aria-label": "Latest sync failures", children: failureEntries.map((failure, index) => {
3804
+ const repositoryLabel = formatSyncFailureRepository(failure.repositoryUrl);
3805
+ const issueLabel = failure.githubIssueNumber !== void 0 ? `Issue #${failure.githubIssueNumber}` : null;
3806
+ const phaseLabel = formatSyncFailurePhase(failure.phase);
3807
+ const title = [repositoryLabel, issueLabel].filter((value) => Boolean(value)).join(" \xB7 ");
3808
+ const meta = [phaseLabel, failure.occurredAt ? formatDate(failure.occurredAt, failure.occurredAt) : null].filter((value) => Boolean(value)).join(" \xB7 ");
3809
+ return /* @__PURE__ */ jsx(
3810
+ "li",
3811
+ {
3812
+ className: "ghsync-diagnostics__failure-item",
3813
+ children: /* @__PURE__ */ jsxs(
3814
+ "button",
3815
+ {
3816
+ type: "button",
3817
+ className: `ghsync-diagnostics__failure${index === selectedFailureIndex ? " ghsync-diagnostics__failure--active" : ""}`,
3818
+ "aria-pressed": index === selectedFailureIndex,
3819
+ onClick: () => setSelectedFailureIndex(index),
3820
+ children: [
3821
+ /* @__PURE__ */ jsx("strong", { className: "ghsync-diagnostics__failure-title", children: title || `Failure ${index + 1}` }),
3822
+ meta ? /* @__PURE__ */ jsx("span", { className: "ghsync-diagnostics__failure-meta", children: meta }) : null,
3823
+ /* @__PURE__ */ jsx("span", { className: "ghsync-diagnostics__failure-preview", children: failure.message })
3824
+ ]
3825
+ }
3826
+ )
3827
+ },
3828
+ `${failure.occurredAt ?? "unknown"}-${failure.githubIssueNumber ?? "no-issue"}-${index}`
3829
+ );
3830
+ }) }) : null,
3831
+ /* @__PURE__ */ jsxs("div", { className: "ghsync-diagnostics__detail", children: [
3832
+ /* @__PURE__ */ jsxs("div", { className: "ghsync-diagnostics__block", children: [
3833
+ /* @__PURE__ */ jsx("span", { className: "ghsync-diagnostics__label", children: "Summary" }),
3834
+ /* @__PURE__ */ jsx("div", { className: "ghsync-diagnostics__value", children: diagnostics.message })
3835
+ ] }),
3836
+ diagnostics.rows.length ? /* @__PURE__ */ jsx("div", { className: "ghsync-diagnostics__grid", children: diagnostics.rows.map((row) => /* @__PURE__ */ jsxs("div", { className: "ghsync-diagnostics__item", children: [
3837
+ /* @__PURE__ */ jsx("span", { className: "ghsync-diagnostics__label", children: row.label }),
3838
+ /* @__PURE__ */ jsx("strong", { className: "ghsync-diagnostics__value", children: row.value })
3839
+ ] }, row.label)) }) : null,
3840
+ diagnostics.rawMessage ? /* @__PURE__ */ jsxs("div", { className: "ghsync-diagnostics__block", children: [
3841
+ /* @__PURE__ */ jsx("span", { className: "ghsync-diagnostics__label", children: "Raw error" }),
3842
+ /* @__PURE__ */ jsx("div", { className: "ghsync-diagnostics__value ghsync-diagnostics__value--code", children: diagnostics.rawMessage })
3843
+ ] }) : null,
3844
+ diagnostics.suggestedAction ? /* @__PURE__ */ jsxs("div", { className: "ghsync-diagnostics__block", children: [
3845
+ /* @__PURE__ */ jsx("span", { className: "ghsync-diagnostics__label", children: "Next step" }),
3846
+ /* @__PURE__ */ jsx("div", { className: "ghsync-diagnostics__value", children: diagnostics.suggestedAction })
3847
+ ] }) : null
3848
+ ] })
3618
3849
  ] }) : null
3619
3850
  ] });
3620
3851
  }
@@ -3630,11 +3861,13 @@ function GitHubSyncSettingsPage() {
3630
3861
  const updateBoardAccess = usePluginAction("settings.updateBoardAccess");
3631
3862
  const validateToken = usePluginAction("settings.validateToken");
3632
3863
  const runSyncNow = usePluginAction("sync.runNow");
3864
+ const cancelSync = usePluginAction("sync.cancel");
3633
3865
  const [form, setForm] = useState(EMPTY_SETTINGS);
3634
3866
  const [submittingToken, setSubmittingToken] = useState(false);
3635
3867
  const [connectingBoardAccess, setConnectingBoardAccess] = useState(false);
3636
3868
  const [submittingSetup, setSubmittingSetup] = useState(false);
3637
3869
  const [runningSync, setRunningSync] = useState(false);
3870
+ const [cancellingSync, setCancellingSync] = useState(false);
3638
3871
  const [manualSyncRequestError, setManualSyncRequestError] = useState(null);
3639
3872
  const [scheduleFrequencyDraft, setScheduleFrequencyDraft] = useState(String(DEFAULT_SCHEDULE_FREQUENCY_MINUTES));
3640
3873
  const [ignoredAuthorsDraft, setIgnoredAuthorsDraft] = useState(DEFAULT_ADVANCED_SETTINGS.ignoredIssueAuthorUsernames.join(", "));
@@ -3897,6 +4130,10 @@ function GitHubSyncSettingsPage() {
3897
4130
  hasMappings: savedMappingCount > 0,
3898
4131
  hasBoardAccess: boardAccessReady
3899
4132
  });
4133
+ const syncPersistedRunning = displaySyncState.status === "running";
4134
+ const syncStartPending = runningSync && !syncPersistedRunning;
4135
+ const syncInFlight = syncStartPending || syncPersistedRunning;
4136
+ const cancellationRequested = syncPersistedRunning && (cancellingSync || isSyncCancellationRequested(displaySyncState));
3900
4137
  const mappingsDirty = JSON.stringify(draftMappings) !== JSON.stringify(savedMappings);
3901
4138
  const advancedSettingsDirty = JSON.stringify(draftAdvancedSettings) !== JSON.stringify(savedAdvancedSettings);
3902
4139
  const scheduleFrequencyError = getScheduleFrequencyError(scheduleFrequencyDraft);
@@ -3904,7 +4141,6 @@ function GitHubSyncSettingsPage() {
3904
4141
  const savedScheduleFrequencyMinutes = normalizeScheduleFrequencyMinutes(currentSettings?.scheduleFrequencyMinutes);
3905
4142
  const scheduleDirty = scheduleFrequencyError === null && scheduleFrequencyMinutes !== savedScheduleFrequencyMinutes;
3906
4143
  const mappings = form.mappings.length > 0 ? form.mappings : [createEmptyMapping(0)];
3907
- const syncInFlight = runningSync || displaySyncState.status === "running";
3908
4144
  const settingsMutationsLocked = syncInFlight;
3909
4145
  const settingsMutationsLockReason = settingsMutationsLocked ? "Settings are temporarily locked while a sync is running to avoid overwriting local edits." : null;
3910
4146
  const syncStatus = getSyncStatus(displaySyncState, runningSync, syncUnlocked);
@@ -3926,10 +4162,11 @@ function GitHubSyncSettingsPage() {
3926
4162
  savedMappingCount
3927
4163
  });
3928
4164
  const syncSectionDescription = "";
3929
- const syncSummaryPrimaryText = syncProgress?.title ?? displaySyncState.message ?? (syncUnlocked ? "Ready to sync." : syncSetupMessage);
4165
+ const syncSummaryPrimaryText = (cancellationRequested ? "Cancellation requested." : void 0) ?? syncProgress?.title ?? displaySyncState.message ?? (syncUnlocked ? "Ready to sync." : syncSetupMessage);
3930
4166
  const manualSyncScopeSummary = hasCompanyContext ? `Manual sync: ${currentCompanyName}` : "Manual sync: all companies";
3931
4167
  const syncSummarySecondaryText = syncProgress ? [
3932
4168
  manualSyncScopeSummary,
4169
+ cancellationRequested ? "Stopping after the current step" : null,
3933
4170
  syncProgress.issueProgressLabel,
3934
4171
  syncProgress.currentIssueLabel ?? syncProgress.repositoryPosition,
3935
4172
  `Auto-sync: ${scheduleDescription}`
@@ -4298,6 +4535,46 @@ function GitHubSyncSettingsPage() {
4298
4535
  setRunningSync(false);
4299
4536
  }
4300
4537
  }
4538
+ async function handleCancelSync() {
4539
+ if (!syncPersistedRunning) {
4540
+ return;
4541
+ }
4542
+ setCancellingSync(true);
4543
+ setManualSyncRequestError(null);
4544
+ try {
4545
+ const result = await cancelSync();
4546
+ setForm((current) => ({
4547
+ ...current,
4548
+ syncState: result.syncState
4549
+ }));
4550
+ toast({
4551
+ title: getSyncToastTitle(result.syncState),
4552
+ body: getSyncToastBody(result.syncState),
4553
+ tone: getSyncToastTone(result.syncState)
4554
+ });
4555
+ armSyncCompletionToast(result.syncState);
4556
+ try {
4557
+ await settings.refresh();
4558
+ } catch {
4559
+ return;
4560
+ }
4561
+ } catch (error) {
4562
+ const message = error instanceof Error ? error.message : "Unable to cancel sync.";
4563
+ setManualSyncRequestError(message);
4564
+ toast({
4565
+ title: "Unable to cancel GitHub sync",
4566
+ body: message,
4567
+ tone: "error"
4568
+ });
4569
+ try {
4570
+ await settings.refresh();
4571
+ } catch {
4572
+ return;
4573
+ }
4574
+ } finally {
4575
+ setCancellingSync(false);
4576
+ }
4577
+ }
4301
4578
  return /* @__PURE__ */ jsxs("div", { className: "ghsync", style: themeVars, children: [
4302
4579
  /* @__PURE__ */ jsx("style", { children: PAGE_STYLES }),
4303
4580
  /* @__PURE__ */ jsxs("section", { className: "ghsync__header", children: [
@@ -4752,10 +5029,10 @@ function GitHubSyncSettingsPage() {
4752
5029
  "button",
4753
5030
  {
4754
5031
  type: "button",
4755
- className: getPluginActionClassName({ variant: "primary" }),
4756
- onClick: handleRunSyncNow,
4757
- disabled: syncInFlight || showInitialLoadingState,
4758
- children: syncInFlight ? "Running\u2026" : manualSyncButtonLabel
5032
+ className: getPluginActionClassName({ variant: syncPersistedRunning ? "danger" : "primary" }),
5033
+ onClick: syncPersistedRunning ? handleCancelSync : handleRunSyncNow,
5034
+ disabled: showInitialLoadingState || syncStartPending || (syncPersistedRunning ? cancellationRequested : false),
5035
+ children: syncPersistedRunning ? cancellationRequested ? "Cancelling\u2026" : "Cancel sync" : syncStartPending ? "Running\u2026" : manualSyncButtonLabel
4759
5036
  }
4760
5037
  )
4761
5038
  ] })
@@ -4841,7 +5118,9 @@ function GitHubSyncDashboardWidget() {
4841
5118
  hostContext.companyId ? { companyId: hostContext.companyId } : {}
4842
5119
  );
4843
5120
  const runSyncNow = usePluginAction("sync.runNow");
5121
+ const cancelSync = usePluginAction("sync.cancel");
4844
5122
  const [runningSync, setRunningSync] = useState(false);
5123
+ const [cancellingSync, setCancellingSync] = useState(false);
4845
5124
  const [manualSyncRequestError, setManualSyncRequestError] = useState(null);
4846
5125
  const [settingsHref, setSettingsHref] = useState(SETTINGS_INDEX_HREF2);
4847
5126
  const [cachedSettings, setCachedSettings] = useState(null);
@@ -4872,7 +5151,10 @@ function GitHubSyncDashboardWidget() {
4872
5151
  hasBoardAccess: boardAccessReady
4873
5152
  });
4874
5153
  const syncUnlocked = syncSetupIssue === null;
4875
- const syncInFlight = runningSync || displaySyncState.status === "running";
5154
+ const syncPersistedRunning = displaySyncState.status === "running";
5155
+ const syncStartPending = runningSync && !syncPersistedRunning;
5156
+ const syncInFlight = syncStartPending || syncPersistedRunning;
5157
+ const cancellationRequested = syncPersistedRunning && (cancellingSync || isSyncCancellationRequested(displaySyncState));
4876
5158
  const scheduleFrequencyMinutes = normalizeScheduleFrequencyMinutes(current.scheduleFrequencyMinutes);
4877
5159
  const scheduleDescription = formatScheduleFrequency(scheduleFrequencyMinutes);
4878
5160
  const summary = getDashboardSummary({
@@ -4972,6 +5254,34 @@ function GitHubSyncDashboardWidget() {
4972
5254
  setRunningSync(false);
4973
5255
  }
4974
5256
  }
5257
+ async function handleCancelSync() {
5258
+ if (!syncPersistedRunning) {
5259
+ return;
5260
+ }
5261
+ setCancellingSync(true);
5262
+ setManualSyncRequestError(null);
5263
+ try {
5264
+ const result = await cancelSync();
5265
+ const nextSyncState = result.syncState ?? EMPTY_SETTINGS.syncState;
5266
+ toast({
5267
+ title: getSyncToastTitle(nextSyncState),
5268
+ body: getSyncToastBody(nextSyncState),
5269
+ tone: getSyncToastTone(nextSyncState)
5270
+ });
5271
+ armSyncCompletionToast(nextSyncState);
5272
+ await settings.refresh();
5273
+ } catch (error) {
5274
+ const message = error instanceof Error ? error.message : "Unable to cancel GitHub sync.";
5275
+ setManualSyncRequestError(message);
5276
+ toast({
5277
+ title: "Unable to cancel GitHub sync",
5278
+ body: message,
5279
+ tone: "error"
5280
+ });
5281
+ } finally {
5282
+ setCancellingSync(false);
5283
+ }
5284
+ }
4975
5285
  return /* @__PURE__ */ jsxs("section", { className: "ghsync-widget", style: themeVars, children: [
4976
5286
  /* @__PURE__ */ jsx("style", { children: WIDGET_STYLES }),
4977
5287
  /* @__PURE__ */ jsxs("div", { className: "ghsync-widget__card", children: [
@@ -5054,10 +5364,10 @@ function GitHubSyncDashboardWidget() {
5054
5364
  "button",
5055
5365
  {
5056
5366
  type: "button",
5057
- className: getPluginActionClassName({ variant: "primary" }),
5058
- onClick: handleRunSync,
5059
- disabled: syncInFlight || showInitialLoadingState,
5060
- children: syncInFlight ? "Running\u2026" : "Run sync now"
5367
+ className: getPluginActionClassName({ variant: syncPersistedRunning ? "danger" : "primary" }),
5368
+ onClick: syncPersistedRunning ? handleCancelSync : handleRunSync,
5369
+ disabled: showInitialLoadingState || syncStartPending || (syncPersistedRunning ? cancellationRequested : false),
5370
+ children: syncPersistedRunning ? cancellationRequested ? "Cancelling\u2026" : "Cancel sync" : syncStartPending ? "Running\u2026" : "Run sync now"
5061
5371
  }
5062
5372
  ) : null
5063
5373
  ] }) })
@@ -5085,6 +5395,7 @@ function GitHubMarkIcon(props) {
5085
5395
  function GitHubSyncToolbarButtonSurface(props) {
5086
5396
  const toast = usePluginToast();
5087
5397
  const runSyncNow = usePluginAction("sync.runNow");
5398
+ const cancelSync = usePluginAction("sync.cancel");
5088
5399
  const pluginIdFromLocation = getPluginIdFromLocation();
5089
5400
  const surfaceRef = useRef(null);
5090
5401
  const resolvedIssue = useResolvedIssueId({
@@ -5104,6 +5415,7 @@ function GitHubSyncToolbarButtonSurface(props) {
5104
5415
  props.companyId ? { companyId: props.companyId } : {}
5105
5416
  );
5106
5417
  const [runningSync, setRunningSync] = useState(false);
5418
+ const [cancellingSync, setCancellingSync] = useState(false);
5107
5419
  const themeMode = useResolvedThemeMode();
5108
5420
  const boardAccessRequirement = usePaperclipBoardAccessRequirement();
5109
5421
  const theme = themeMode === "light" ? LIGHT_PALETTE : DARK_PALETTE;
@@ -5123,7 +5435,10 @@ function GitHubSyncToolbarButtonSurface(props) {
5123
5435
  const effectiveCanRun = state.canRun && !boardAccessSetupIssue;
5124
5436
  const effectiveMessage = boardAccessSetupIssue ? getSyncSetupMessage(boardAccessSetupIssue, hasCompanyContext) : state.message;
5125
5437
  const effectiveLabel = boardAccessSetupIssue ? "Board access required" : state.label;
5126
- const syncInFlight = runningSync || state.syncState.status === "running";
5438
+ const syncPersistedRunning = state.syncState.status === "running";
5439
+ const syncStartPending = runningSync && !syncPersistedRunning;
5440
+ const syncInFlight = syncStartPending || syncPersistedRunning;
5441
+ const cancellationRequested = syncPersistedRunning && (cancellingSync || isSyncCancellationRequested(state.syncState));
5127
5442
  const armSyncCompletionToast = useSyncCompletionToast(state.syncState, toast);
5128
5443
  useEffect(() => {
5129
5444
  if (state.syncState.status !== "running") {
@@ -5228,6 +5543,31 @@ function GitHubSyncToolbarButtonSurface(props) {
5228
5543
  setRunningSync(false);
5229
5544
  }
5230
5545
  }
5546
+ async function handleCancelSync() {
5547
+ if (!syncPersistedRunning) {
5548
+ return;
5549
+ }
5550
+ try {
5551
+ setCancellingSync(true);
5552
+ const result = await cancelSync();
5553
+ const nextSyncState = result.syncState ?? EMPTY_SETTINGS.syncState;
5554
+ toast({
5555
+ title: getSyncToastTitle(nextSyncState),
5556
+ body: getSyncToastBody(nextSyncState),
5557
+ tone: getSyncToastTone(nextSyncState)
5558
+ });
5559
+ armSyncCompletionToast(nextSyncState);
5560
+ toolbarState.refresh();
5561
+ } catch (error) {
5562
+ toast({
5563
+ title: "Unable to cancel GitHub sync",
5564
+ body: error instanceof Error ? error.message : "Unable to cancel GitHub sync.",
5565
+ tone: "error"
5566
+ });
5567
+ } finally {
5568
+ setCancellingSync(false);
5569
+ }
5570
+ }
5231
5571
  return /* @__PURE__ */ jsxs(
5232
5572
  "div",
5233
5573
  {
@@ -5245,11 +5585,11 @@ function GitHubSyncToolbarButtonSurface(props) {
5245
5585
  "data-variant": "outline",
5246
5586
  "data-size": "sm",
5247
5587
  className: props.entityType ? HOST_ENTITY_BUTTON_CLASSNAME : HOST_GLOBAL_BUTTON_CLASSNAME,
5248
- disabled: !effectiveCanRun || syncInFlight || toolbarState.loading,
5249
- onClick: handleRunSync,
5588
+ disabled: toolbarState.loading || syncStartPending || (syncPersistedRunning ? cancellationRequested : !effectiveCanRun),
5589
+ onClick: syncPersistedRunning ? handleCancelSync : handleRunSync,
5250
5590
  children: [
5251
5591
  /* @__PURE__ */ jsx(GitHubMarkIcon, { className: "mr-1.5 h-3.5 w-3.5" }),
5252
- /* @__PURE__ */ jsx("span", { children: syncInFlight ? "Syncing\u2026" : effectiveLabel })
5592
+ /* @__PURE__ */ jsx("span", { children: syncPersistedRunning ? cancellationRequested ? "Cancelling\u2026" : "Cancel sync" : syncStartPending ? "Syncing\u2026" : effectiveLabel })
5253
5593
  ]
5254
5594
  }
5255
5595
  )