paperclip-github-plugin 0.2.1 → 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
@@ -67,6 +67,32 @@ function requiresPaperclipBoardAccess(value) {
67
67
  return health?.deploymentMode?.toLowerCase() === "authenticated";
68
68
  }
69
69
 
70
+ // src/ui/assignees.ts
71
+ function normalizeCompanyAssigneeOptionsResponse(response) {
72
+ if (!Array.isArray(response)) {
73
+ throw new Error("Unexpected company agents response: expected an array.");
74
+ }
75
+ return response.map((entry) => {
76
+ if (!entry || typeof entry !== "object") {
77
+ return null;
78
+ }
79
+ const record = entry;
80
+ const id = typeof record.id === "string" ? record.id.trim() : "";
81
+ const name = typeof record.name === "string" ? record.name.trim() : "";
82
+ const status = typeof record.status === "string" ? record.status.trim() : "";
83
+ const title = typeof record.title === "string" ? record.title.trim() : "";
84
+ if (!id || !name || status === "terminated") {
85
+ return null;
86
+ }
87
+ return {
88
+ id,
89
+ name,
90
+ ...title ? { title } : {},
91
+ ...status ? { status } : {}
92
+ };
93
+ }).filter((entry) => entry !== null).sort((left, right) => left.name.localeCompare(right.name));
94
+ }
95
+
70
96
  // src/ui/http.ts
71
97
  var JSON_CONTENT_TYPE_PATTERN = /\b(?:application\/json|[^;\s]+\/[^;\s]+\+json)\b/i;
72
98
  var HTML_LIKE_RESPONSE_PATTERN = /^\s*</;
@@ -217,6 +243,48 @@ async function fetchPaperclipHealth(origin) {
217
243
  }
218
244
  }
219
245
 
246
+ // src/ui/plugin-installation.ts
247
+ var SETTINGS_INDEX_HREF = "/instance/settings/plugins";
248
+ var GITHUB_SYNC_PLUGIN_KEY = "paperclip-github-plugin";
249
+ var GITHUB_SYNC_PLUGIN_DISPLAY_NAME = "GitHub Sync";
250
+ function getStringValue(record, key) {
251
+ if (!record) {
252
+ return null;
253
+ }
254
+ const value = record[key];
255
+ return typeof value === "string" && value.trim() ? value.trim() : null;
256
+ }
257
+ function resolveGitHubSyncPluginRecord(records) {
258
+ if (!Array.isArray(records)) {
259
+ return null;
260
+ }
261
+ for (const entry of records) {
262
+ if (!entry || typeof entry !== "object") {
263
+ continue;
264
+ }
265
+ const record = entry;
266
+ const manifest = record.manifest && typeof record.manifest === "object" ? record.manifest : null;
267
+ const key = getStringValue(record, "pluginKey") ?? getStringValue(record, "key") ?? getStringValue(record, "packageName") ?? getStringValue(record, "name") ?? getStringValue(manifest, "id");
268
+ const displayName = getStringValue(record, "displayName") ?? getStringValue(manifest, "displayName");
269
+ const id = getStringValue(record, "id") ?? getStringValue(record, "pluginId");
270
+ if (id && (key === GITHUB_SYNC_PLUGIN_KEY || displayName === GITHUB_SYNC_PLUGIN_DISPLAY_NAME)) {
271
+ return record;
272
+ }
273
+ }
274
+ return null;
275
+ }
276
+ function resolveInstalledGitHubSyncPluginId(records, preferredPluginId) {
277
+ if (typeof preferredPluginId === "string" && preferredPluginId.trim()) {
278
+ return preferredPluginId.trim();
279
+ }
280
+ const record = resolveGitHubSyncPluginRecord(records);
281
+ return getStringValue(record, "id") ?? getStringValue(record, "pluginId");
282
+ }
283
+ function resolvePluginSettingsHref(records) {
284
+ const pluginId = resolveInstalledGitHubSyncPluginId(records);
285
+ return pluginId ? `${SETTINGS_INDEX_HREF}/${pluginId}` : SETTINGS_INDEX_HREF;
286
+ }
287
+
220
288
  // src/ui/plugin-config.ts
221
289
  function normalizeOptionalString2(value) {
222
290
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
@@ -612,12 +680,18 @@ function getActiveRateLimitPause(syncState, referenceTimeMs = Date.now()) {
612
680
  ...syncState.errorDetails.rateLimitResource ? { resource: syncState.errorDetails.rateLimitResource } : {}
613
681
  };
614
682
  }
683
+ function isSyncCancellationRequested(syncState) {
684
+ return syncState.status === "running" && Boolean(syncState.cancelRequestedAt?.trim());
685
+ }
615
686
  function getSyncToastTitle(syncState) {
616
687
  if (getActiveRateLimitPause(syncState)) {
617
688
  return "GitHub sync is paused";
618
689
  }
690
+ if (syncState.status === "cancelled") {
691
+ return "GitHub sync was cancelled";
692
+ }
619
693
  if (syncState.status === "running") {
620
- return "GitHub sync is running";
694
+ return isSyncCancellationRequested(syncState) ? "GitHub sync is stopping" : "GitHub sync is running";
621
695
  }
622
696
  return syncState.status === "error" ? "GitHub sync needs attention" : "GitHub sync finished";
623
697
  }
@@ -626,7 +700,7 @@ function getSyncToastBody(syncState) {
626
700
  return syncState.message.trim();
627
701
  }
628
702
  if (syncState.status === "running") {
629
- 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.";
630
704
  }
631
705
  return "GitHub sync completed.";
632
706
  }
@@ -634,7 +708,7 @@ function getSyncToastTone(syncState) {
634
708
  if (getActiveRateLimitPause(syncState)) {
635
709
  return "info";
636
710
  }
637
- if (syncState.status === "running") {
711
+ if (syncState.status === "running" || syncState.status === "cancelled") {
638
712
  return "info";
639
713
  }
640
714
  return syncState.status === "error" ? "error" : "success";
@@ -765,6 +839,10 @@ var SHARED_PROGRESS_STYLES = `
765
839
  align-items: stretch;
766
840
  flex-direction: column;
767
841
  }
842
+
843
+ .ghsync-diagnostics__layout--split {
844
+ grid-template-columns: 1fr;
845
+ }
768
846
  }
769
847
  `;
770
848
  var PAGE_STYLES = `
@@ -992,6 +1070,79 @@ var PAGE_STYLES = `
992
1070
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
993
1071
  }
994
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
+
995
1146
  .ghsync-diagnostics__item,
996
1147
  .ghsync-diagnostics__block {
997
1148
  display: grid;
@@ -1965,6 +2116,72 @@ var WIDGET_STYLES = `
1965
2116
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
1966
2117
  }
1967
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
+
1968
2185
  .ghsync-diagnostics__item,
1969
2186
  .ghsync-diagnostics__block {
1970
2187
  display: grid;
@@ -2105,6 +2322,10 @@ var WIDGET_STYLES = `
2105
2322
  .ghsync-widget__link {
2106
2323
  flex: 1 1 auto;
2107
2324
  }
2325
+
2326
+ .ghsync-diagnostics__layout--split {
2327
+ grid-template-columns: 1fr;
2328
+ }
2108
2329
  }
2109
2330
 
2110
2331
  ${SHARED_PROGRESS_STYLES}
@@ -2681,24 +2902,39 @@ function getPaperclipApiBaseUrl() {
2681
2902
  return window.location.origin;
2682
2903
  }
2683
2904
  var syncedPaperclipApiBaseUrlsByPluginId = /* @__PURE__ */ new Map();
2905
+ var installedGitHubSyncPluginIdPromise = null;
2906
+ async function resolveCurrentPluginId(pluginId) {
2907
+ if (pluginId) {
2908
+ return pluginId;
2909
+ }
2910
+ if (!installedGitHubSyncPluginIdPromise) {
2911
+ installedGitHubSyncPluginIdPromise = fetchJson("/api/plugins").then((records) => resolveInstalledGitHubSyncPluginId(records)).catch(() => null);
2912
+ }
2913
+ const resolvedPluginId = await installedGitHubSyncPluginIdPromise;
2914
+ if (!resolvedPluginId) {
2915
+ installedGitHubSyncPluginIdPromise = null;
2916
+ }
2917
+ return resolvedPluginId;
2918
+ }
2684
2919
  async function syncTrustedPaperclipApiBaseUrl(pluginId) {
2685
2920
  const paperclipApiBaseUrl = getPaperclipApiBaseUrl();
2686
2921
  if (!paperclipApiBaseUrl) {
2687
2922
  return void 0;
2688
2923
  }
2689
- if (!pluginId) {
2924
+ const resolvedPluginId = await resolveCurrentPluginId(pluginId);
2925
+ if (!resolvedPluginId) {
2690
2926
  throw new Error(
2691
2927
  "Unable to sync the trusted Paperclip API origin because the plugin ID is missing. Reload the plugin and try again before saving or syncing."
2692
2928
  );
2693
2929
  }
2694
- const lastSyncedPaperclipApiBaseUrl = syncedPaperclipApiBaseUrlsByPluginId.get(pluginId);
2930
+ const lastSyncedPaperclipApiBaseUrl = syncedPaperclipApiBaseUrlsByPluginId.get(resolvedPluginId);
2695
2931
  if (lastSyncedPaperclipApiBaseUrl === paperclipApiBaseUrl) {
2696
2932
  return paperclipApiBaseUrl;
2697
2933
  }
2698
- await patchPluginConfig(pluginId, {
2934
+ await patchPluginConfig(resolvedPluginId, {
2699
2935
  paperclipApiBaseUrl
2700
2936
  });
2701
- syncedPaperclipApiBaseUrlsByPluginId.set(pluginId, paperclipApiBaseUrl);
2937
+ syncedPaperclipApiBaseUrlsByPluginId.set(resolvedPluginId, paperclipApiBaseUrl);
2702
2938
  return paperclipApiBaseUrl;
2703
2939
  }
2704
2940
  function formatDate(value, fallback = "Never") {
@@ -2804,6 +3040,11 @@ async function listCompanyProjects(companyId) {
2804
3040
  return id && name ? { id, name } : null;
2805
3041
  }).filter((entry) => entry !== null);
2806
3042
  }
3043
+ async function listCompanyAssigneeOptions(companyId) {
3044
+ return normalizeCompanyAssigneeOptionsResponse(
3045
+ await fetchJson(`/api/companies/${companyId}/agents`)
3046
+ );
3047
+ }
2807
3048
  async function listProjectWorkspaces(projectId) {
2808
3049
  const response = await fetchJson(`/api/projects/${projectId}/workspaces`);
2809
3050
  if (!Array.isArray(response)) {
@@ -2991,7 +3232,10 @@ function getSyncStatus(syncState, runningSync, syncUnlocked) {
2991
3232
  return { label: "Locked", tone: "neutral" };
2992
3233
  }
2993
3234
  if (runningSync || syncState.status === "running") {
2994
- return { label: "Running", tone: "info" };
3235
+ return {
3236
+ label: isSyncCancellationRequested(syncState) ? "Cancelling" : "Running",
3237
+ tone: "info"
3238
+ };
2995
3239
  }
2996
3240
  if (getActiveRateLimitPause(syncState)) {
2997
3241
  return { label: "Paused", tone: "warning" };
@@ -3002,6 +3246,9 @@ function getSyncStatus(syncState, runningSync, syncUnlocked) {
3002
3246
  if (syncState.status === "success") {
3003
3247
  return { label: "Ready", tone: "success" };
3004
3248
  }
3249
+ if (syncState.status === "cancelled") {
3250
+ return { label: "Cancelled", tone: "neutral" };
3251
+ }
3005
3252
  return { label: "Ready", tone: "info" };
3006
3253
  }
3007
3254
  function getToneClass(tone) {
@@ -3018,9 +3265,9 @@ function getToneClass(tone) {
3018
3265
  return "ghsync__badge--neutral";
3019
3266
  }
3020
3267
  }
3021
- var SETTINGS_INDEX_HREF = "/instance/settings/plugins";
3268
+ var SETTINGS_INDEX_HREF2 = "/instance/settings/plugins";
3022
3269
  var GITHUB_SYNC_SETTINGS_UPDATED_EVENT = "paperclip-github-plugin:settings-updated";
3023
- function getStringValue(record, key) {
3270
+ function getStringValue2(record, key) {
3024
3271
  const value = record[key];
3025
3272
  return typeof value === "string" && value.trim() ? value.trim() : null;
3026
3273
  }
@@ -3038,12 +3285,12 @@ function humanizeCompanyPrefix(value) {
3038
3285
  return trimmed.replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim().replace(/\b\w/g, (character) => character.toUpperCase());
3039
3286
  }
3040
3287
  function getCompanyLabelFromRecord(record) {
3041
- const explicitLabel = getStringValue(record, "displayName") ?? getStringValue(record, "name") ?? getStringValue(record, "title");
3288
+ const explicitLabel = getStringValue2(record, "displayName") ?? getStringValue2(record, "name") ?? getStringValue2(record, "title");
3042
3289
  if (explicitLabel && !isUuidLike(explicitLabel)) {
3043
3290
  return explicitLabel;
3044
3291
  }
3045
3292
  return humanizeCompanyPrefix(
3046
- getStringValue(record, "companyPrefix") ?? getStringValue(record, "prefix") ?? getStringValue(record, "slug")
3293
+ getStringValue2(record, "companyPrefix") ?? getStringValue2(record, "prefix") ?? getStringValue2(record, "slug")
3047
3294
  );
3048
3295
  }
3049
3296
  async function resolveCompanyScopeLabel(companyId, companyPrefix) {
@@ -3069,8 +3316,8 @@ async function resolveCompanyScopeLabel(companyId, companyPrefix) {
3069
3316
  return false;
3070
3317
  }
3071
3318
  const record = entry;
3072
- const entryId = getStringValue(record, "id");
3073
- const entryPrefix = getStringValue(record, "companyPrefix") ?? getStringValue(record, "prefix") ?? getStringValue(record, "slug");
3319
+ const entryId = getStringValue2(record, "id");
3320
+ const entryPrefix = getStringValue2(record, "companyPrefix") ?? getStringValue2(record, "prefix") ?? getStringValue2(record, "slug");
3074
3321
  return entryId === companyId || Boolean(companyPrefix && entryPrefix === companyPrefix);
3075
3322
  });
3076
3323
  if (matchingCompany && typeof matchingCompany === "object") {
@@ -3154,25 +3401,6 @@ function useResolvedIssueId(params) {
3154
3401
  loading: false
3155
3402
  };
3156
3403
  }
3157
- function resolvePluginSettingsHref(records) {
3158
- if (!Array.isArray(records)) {
3159
- return SETTINGS_INDEX_HREF;
3160
- }
3161
- for (const entry of records) {
3162
- if (!entry || typeof entry !== "object") {
3163
- continue;
3164
- }
3165
- const record = entry;
3166
- const manifest = record.manifest && typeof record.manifest === "object" ? record.manifest : null;
3167
- const id = getStringValue(record, "id") ?? getStringValue(record, "pluginId");
3168
- const key = getStringValue(record, "pluginKey") ?? getStringValue(record, "key") ?? getStringValue(record, "packageName") ?? getStringValue(record, "name") ?? (manifest ? getStringValue(manifest, "id") : null);
3169
- const displayName = getStringValue(record, "displayName") ?? (manifest ? getStringValue(manifest, "displayName") : null);
3170
- if (id && (key === "paperclip-github-plugin" || displayName === "GitHub Sync")) {
3171
- return `${SETTINGS_INDEX_HREF}/${id}`;
3172
- }
3173
- }
3174
- return SETTINGS_INDEX_HREF;
3175
- }
3176
3404
  function formatSyncProgressRepository(repositoryUrl) {
3177
3405
  if (!repositoryUrl?.trim()) {
3178
3406
  return null;
@@ -3291,10 +3519,10 @@ function getDashboardSummary(params) {
3291
3519
  if (params.runningSync || params.syncState.status === "running") {
3292
3520
  const progress = getRunningSyncProgressModel(params.syncState);
3293
3521
  return {
3294
- label: "Syncing",
3522
+ label: isSyncCancellationRequested(params.syncState) ? "Cancelling" : "Syncing",
3295
3523
  tone: "info",
3296
- title: progress?.title ?? "Sync in progress",
3297
- 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."
3298
3526
  };
3299
3527
  }
3300
3528
  if (activeRateLimitPause) {
@@ -3313,6 +3541,14 @@ function getDashboardSummary(params) {
3313
3541
  body: params.syncState.message ?? "Open settings to review the latest GitHub sync issue."
3314
3542
  };
3315
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
+ }
3316
3552
  if (params.syncState.checkedAt) {
3317
3553
  return {
3318
3554
  label: "Ready",
@@ -3422,16 +3658,28 @@ function formatSyncFailureRepository(repositoryUrl) {
3422
3658
  }
3423
3659
  return repositoryUrl.trim();
3424
3660
  }
3425
- 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
+ }
3426
3665
  if (syncState.status !== "error") {
3427
- return null;
3666
+ return [];
3428
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) {
3429
3677
  const rows = [];
3430
- const repositoryLabel = formatSyncFailureRepository(syncState.errorDetails?.repositoryUrl);
3431
- const phaseLabel = formatSyncFailurePhase(syncState.errorDetails?.phase);
3432
- const issueNumber = syncState.errorDetails?.githubIssueNumber;
3433
- const rateLimitResetAt = syncState.errorDetails?.rateLimitResetAt;
3434
- 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);
3435
3683
  if (repositoryLabel) {
3436
3684
  rows.push({
3437
3685
  label: "Repository",
@@ -3462,12 +3710,19 @@ function getSyncDiagnostics(syncState) {
3462
3710
  value: formatDate(rateLimitResetAt, rateLimitResetAt)
3463
3711
  });
3464
3712
  }
3465
- const rawMessage = syncState.errorDetails?.rawMessage && syncState.errorDetails.rawMessage !== syncState.message ? syncState.errorDetails.rawMessage : void 0;
3466
- const suggestedAction = syncState.errorDetails?.suggestedAction;
3467
- 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) {
3468
3722
  return null;
3469
3723
  }
3470
3724
  return {
3725
+ message: entry.message,
3471
3726
  rows,
3472
3727
  ...rawMessage ? { rawMessage } : {},
3473
3728
  ...suggestedAction ? { suggestedAction } : {}
@@ -3521,31 +3776,76 @@ function SyncProgressPanel(props) {
3521
3776
  );
3522
3777
  }
3523
3778
  function SyncDiagnosticsPanel(props) {
3524
- 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;
3525
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]);
3526
3790
  if (!diagnostics && !requestError) {
3527
3791
  return null;
3528
3792
  }
3529
3793
  return /* @__PURE__ */ jsxs("section", { className: `ghsync-diagnostics${props.compact ? " ghsync-diagnostics--compact" : ""}`, children: [
3530
3794
  /* @__PURE__ */ jsxs("div", { className: "ghsync-diagnostics__header", children: [
3531
3795
  /* @__PURE__ */ jsx("strong", { children: diagnostics ? "Troubleshooting details" : "Sync request failed" }),
3532
- /* @__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." })
3533
3797
  ] }),
3534
3798
  requestError ? /* @__PURE__ */ jsxs("div", { className: "ghsync-diagnostics__block", children: [
3535
3799
  /* @__PURE__ */ jsx("span", { className: "ghsync-diagnostics__label", children: "Request error" }),
3536
3800
  /* @__PURE__ */ jsx("div", { className: "ghsync-diagnostics__value ghsync-diagnostics__value--code", children: requestError })
3537
3801
  ] }) : null,
3538
- diagnostics?.rows.length ? /* @__PURE__ */ jsx("div", { className: "ghsync-diagnostics__grid", children: diagnostics.rows.map((row) => /* @__PURE__ */ jsxs("div", { className: "ghsync-diagnostics__item", children: [
3539
- /* @__PURE__ */ jsx("span", { className: "ghsync-diagnostics__label", children: row.label }),
3540
- /* @__PURE__ */ jsx("strong", { className: "ghsync-diagnostics__value", children: row.value })
3541
- ] }, row.label)) }) : null,
3542
- diagnostics?.rawMessage ? /* @__PURE__ */ jsxs("div", { className: "ghsync-diagnostics__block", children: [
3543
- /* @__PURE__ */ jsx("span", { className: "ghsync-diagnostics__label", children: "Raw error" }),
3544
- /* @__PURE__ */ jsx("div", { className: "ghsync-diagnostics__value ghsync-diagnostics__value--code", children: diagnostics.rawMessage })
3545
- ] }) : null,
3546
- diagnostics?.suggestedAction ? /* @__PURE__ */ jsxs("div", { className: "ghsync-diagnostics__block", children: [
3547
- /* @__PURE__ */ jsx("span", { className: "ghsync-diagnostics__label", children: "Next step" }),
3548
- /* @__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
+ ] })
3549
3849
  ] }) : null
3550
3850
  ] });
3551
3851
  }
@@ -3561,11 +3861,13 @@ function GitHubSyncSettingsPage() {
3561
3861
  const updateBoardAccess = usePluginAction("settings.updateBoardAccess");
3562
3862
  const validateToken = usePluginAction("settings.validateToken");
3563
3863
  const runSyncNow = usePluginAction("sync.runNow");
3864
+ const cancelSync = usePluginAction("sync.cancel");
3564
3865
  const [form, setForm] = useState(EMPTY_SETTINGS);
3565
3866
  const [submittingToken, setSubmittingToken] = useState(false);
3566
3867
  const [connectingBoardAccess, setConnectingBoardAccess] = useState(false);
3567
3868
  const [submittingSetup, setSubmittingSetup] = useState(false);
3568
3869
  const [runningSync, setRunningSync] = useState(false);
3870
+ const [cancellingSync, setCancellingSync] = useState(false);
3569
3871
  const [manualSyncRequestError, setManualSyncRequestError] = useState(null);
3570
3872
  const [scheduleFrequencyDraft, setScheduleFrequencyDraft] = useState(String(DEFAULT_SCHEDULE_FREQUENCY_MINUTES));
3571
3873
  const [ignoredAuthorsDraft, setIgnoredAuthorsDraft] = useState(DEFAULT_ADVANCED_SETTINGS.ignoredIssueAuthorUsernames.join(", "));
@@ -3580,10 +3882,12 @@ function GitHubSyncSettingsPage() {
3580
3882
  const [existingProjectCandidates, setExistingProjectCandidates] = useState([]);
3581
3883
  const [existingProjectCandidatesLoading, setExistingProjectCandidatesLoading] = useState(false);
3582
3884
  const [existingProjectCandidatesError, setExistingProjectCandidatesError] = useState(null);
3885
+ const [browserAvailableAssignees, setBrowserAvailableAssignees] = useState([]);
3583
3886
  const themeMode = useResolvedThemeMode();
3584
3887
  const boardAccessRequirement = usePaperclipBoardAccessRequirement();
3585
3888
  const armSyncCompletionToast = useSyncCompletionToast(form.syncState, toast);
3586
3889
  const boardAccessConfigSyncAttemptRef = useRef(null);
3890
+ const assigneeFallbackAttemptRef = useRef(null);
3587
3891
  const currentSettings = settings.data ?? cachedSettings;
3588
3892
  const showInitialLoadingState = settings.loading && !settings.data && !cachedSettings;
3589
3893
  useEffect(() => {
@@ -3659,10 +3963,47 @@ function GitHubSyncSettingsPage() {
3659
3963
  cancelled = true;
3660
3964
  };
3661
3965
  }, [hostContext.companyId, settings.data?.updatedAt, tokenStatusOverride]);
3966
+ useEffect(() => {
3967
+ const companyId = hostContext.companyId;
3968
+ const workerAvailableAssignees = currentSettings?.availableAssignees ?? [];
3969
+ const snapshotKey = `${companyId ?? "none"}:${currentSettings?.updatedAt ?? "none"}`;
3970
+ if (!companyId) {
3971
+ assigneeFallbackAttemptRef.current = null;
3972
+ setBrowserAvailableAssignees([]);
3973
+ return;
3974
+ }
3975
+ if (workerAvailableAssignees.length > 0) {
3976
+ assigneeFallbackAttemptRef.current = snapshotKey;
3977
+ setBrowserAvailableAssignees([]);
3978
+ return;
3979
+ }
3980
+ if (assigneeFallbackAttemptRef.current === snapshotKey) {
3981
+ return;
3982
+ }
3983
+ assigneeFallbackAttemptRef.current = snapshotKey;
3984
+ let cancelled = false;
3985
+ void (async () => {
3986
+ try {
3987
+ const assignees = await listCompanyAssigneeOptions(companyId);
3988
+ if (cancelled) {
3989
+ return;
3990
+ }
3991
+ setBrowserAvailableAssignees(assignees);
3992
+ } catch {
3993
+ if (cancelled) {
3994
+ return;
3995
+ }
3996
+ setBrowserAvailableAssignees([]);
3997
+ }
3998
+ })();
3999
+ return () => {
4000
+ cancelled = true;
4001
+ };
4002
+ }, [currentSettings?.availableAssignees?.length, currentSettings?.updatedAt, hostContext.companyId]);
3662
4003
  useEffect(() => {
3663
4004
  const companyId = hostContext.companyId;
3664
4005
  const secretRef = settings.data?.paperclipBoardAccessNeedsConfigSync ? settings.data.paperclipBoardAccessConfigSyncRef : void 0;
3665
- if (!companyId || !pluginIdFromLocation || !secretRef) {
4006
+ if (!companyId || !secretRef) {
3666
4007
  return;
3667
4008
  }
3668
4009
  const attemptKey = `${companyId}:${secretRef}`;
@@ -3673,7 +4014,11 @@ function GitHubSyncSettingsPage() {
3673
4014
  let cancelled = false;
3674
4015
  void (async () => {
3675
4016
  try {
3676
- await patchPluginConfig(pluginIdFromLocation, {
4017
+ const pluginId = await resolveCurrentPluginId(pluginIdFromLocation);
4018
+ if (!pluginId) {
4019
+ throw new Error("Plugin id is required to finish syncing Paperclip board access into plugin config.");
4020
+ }
4021
+ await patchPluginConfig(pluginId, {
3677
4022
  paperclipBoardApiTokenRefs: {
3678
4023
  [companyId]: secretRef
3679
4024
  }
@@ -3760,7 +4105,7 @@ function GitHubSyncSettingsPage() {
3760
4105
  const boardAccessSectionDescription = "";
3761
4106
  const repositoriesUnlocked = tokenStatus === "valid";
3762
4107
  const availableAssignees = getAvailableAssigneeOptions(
3763
- currentSettings?.availableAssignees ?? form.availableAssignees,
4108
+ (currentSettings?.availableAssignees?.length ? currentSettings.availableAssignees : null) ?? (form.availableAssignees?.length ? form.availableAssignees : null) ?? browserAvailableAssignees,
3764
4109
  form.advancedSettings.defaultAssigneeAgentId
3765
4110
  );
3766
4111
  const savedMappingsSource = currentSettings ? currentSettings.mappings ?? [] : form.mappings;
@@ -3785,6 +4130,10 @@ function GitHubSyncSettingsPage() {
3785
4130
  hasMappings: savedMappingCount > 0,
3786
4131
  hasBoardAccess: boardAccessReady
3787
4132
  });
4133
+ const syncPersistedRunning = displaySyncState.status === "running";
4134
+ const syncStartPending = runningSync && !syncPersistedRunning;
4135
+ const syncInFlight = syncStartPending || syncPersistedRunning;
4136
+ const cancellationRequested = syncPersistedRunning && (cancellingSync || isSyncCancellationRequested(displaySyncState));
3788
4137
  const mappingsDirty = JSON.stringify(draftMappings) !== JSON.stringify(savedMappings);
3789
4138
  const advancedSettingsDirty = JSON.stringify(draftAdvancedSettings) !== JSON.stringify(savedAdvancedSettings);
3790
4139
  const scheduleFrequencyError = getScheduleFrequencyError(scheduleFrequencyDraft);
@@ -3792,7 +4141,6 @@ function GitHubSyncSettingsPage() {
3792
4141
  const savedScheduleFrequencyMinutes = normalizeScheduleFrequencyMinutes(currentSettings?.scheduleFrequencyMinutes);
3793
4142
  const scheduleDirty = scheduleFrequencyError === null && scheduleFrequencyMinutes !== savedScheduleFrequencyMinutes;
3794
4143
  const mappings = form.mappings.length > 0 ? form.mappings : [createEmptyMapping(0)];
3795
- const syncInFlight = runningSync || displaySyncState.status === "running";
3796
4144
  const settingsMutationsLocked = syncInFlight;
3797
4145
  const settingsMutationsLockReason = settingsMutationsLocked ? "Settings are temporarily locked while a sync is running to avoid overwriting local edits." : null;
3798
4146
  const syncStatus = getSyncStatus(displaySyncState, runningSync, syncUnlocked);
@@ -3814,10 +4162,11 @@ function GitHubSyncSettingsPage() {
3814
4162
  savedMappingCount
3815
4163
  });
3816
4164
  const syncSectionDescription = "";
3817
- 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);
3818
4166
  const manualSyncScopeSummary = hasCompanyContext ? `Manual sync: ${currentCompanyName}` : "Manual sync: all companies";
3819
4167
  const syncSummarySecondaryText = syncProgress ? [
3820
4168
  manualSyncScopeSummary,
4169
+ cancellationRequested ? "Stopping after the current step" : null,
3821
4170
  syncProgress.issueProgressLabel,
3822
4171
  syncProgress.currentIssueLabel ?? syncProgress.repositoryPosition,
3823
4172
  `Auto-sync: ${scheduleDescription}`
@@ -3946,13 +4295,14 @@ function GitHubSyncSettingsPage() {
3946
4295
  if (!companyId) {
3947
4296
  throw new Error("Company context is required to save the GitHub token.");
3948
4297
  }
3949
- if (!pluginIdFromLocation) {
4298
+ const pluginId = await resolveCurrentPluginId(pluginIdFromLocation);
4299
+ if (!pluginId) {
3950
4300
  throw new Error("Plugin id is required to save the GitHub token.");
3951
4301
  }
3952
4302
  const trimmedToken = tokenDraft.trim();
3953
4303
  const secretName = `github_sync_${companyId.replace(/[^a-z0-9]+/gi, "_").toLowerCase()}`;
3954
4304
  const secret = await resolveOrCreateCompanySecret(companyId, secretName, trimmedToken);
3955
- await patchPluginConfig(pluginIdFromLocation, {
4305
+ await patchPluginConfig(pluginId, {
3956
4306
  githubTokenRef: secret.id
3957
4307
  });
3958
4308
  await saveRegistration({
@@ -3997,7 +4347,8 @@ function GitHubSyncSettingsPage() {
3997
4347
  if (!companyId) {
3998
4348
  throw new Error("Company context is required to connect Paperclip board access.");
3999
4349
  }
4000
- if (!pluginIdFromLocation) {
4350
+ const pluginId = await resolveCurrentPluginId(pluginIdFromLocation);
4351
+ if (!pluginId) {
4001
4352
  throw new Error("Plugin id is required to connect Paperclip board access.");
4002
4353
  }
4003
4354
  if (typeof window !== "undefined") {
@@ -4020,7 +4371,7 @@ function GitHubSyncSettingsPage() {
4020
4371
  const identity = await fetchBoardAccessIdentity(boardApiToken);
4021
4372
  const secretName = `paperclip_board_api_${companyId.replace(/[^a-z0-9]+/gi, "_").toLowerCase()}`;
4022
4373
  const secret = await resolveOrCreateCompanySecret(companyId, secretName, boardApiToken);
4023
- await patchPluginConfig(pluginIdFromLocation, {
4374
+ await patchPluginConfig(pluginId, {
4024
4375
  paperclipBoardApiTokenRefs: {
4025
4376
  [companyId]: secret.id
4026
4377
  }
@@ -4184,6 +4535,46 @@ function GitHubSyncSettingsPage() {
4184
4535
  setRunningSync(false);
4185
4536
  }
4186
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
+ }
4187
4578
  return /* @__PURE__ */ jsxs("div", { className: "ghsync", style: themeVars, children: [
4188
4579
  /* @__PURE__ */ jsx("style", { children: PAGE_STYLES }),
4189
4580
  /* @__PURE__ */ jsxs("section", { className: "ghsync__header", children: [
@@ -4638,10 +5029,10 @@ function GitHubSyncSettingsPage() {
4638
5029
  "button",
4639
5030
  {
4640
5031
  type: "button",
4641
- className: getPluginActionClassName({ variant: "primary" }),
4642
- onClick: handleRunSyncNow,
4643
- disabled: syncInFlight || showInitialLoadingState,
4644
- 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
4645
5036
  }
4646
5037
  )
4647
5038
  ] })
@@ -4727,9 +5118,11 @@ function GitHubSyncDashboardWidget() {
4727
5118
  hostContext.companyId ? { companyId: hostContext.companyId } : {}
4728
5119
  );
4729
5120
  const runSyncNow = usePluginAction("sync.runNow");
5121
+ const cancelSync = usePluginAction("sync.cancel");
4730
5122
  const [runningSync, setRunningSync] = useState(false);
5123
+ const [cancellingSync, setCancellingSync] = useState(false);
4731
5124
  const [manualSyncRequestError, setManualSyncRequestError] = useState(null);
4732
- const [settingsHref, setSettingsHref] = useState(SETTINGS_INDEX_HREF);
5125
+ const [settingsHref, setSettingsHref] = useState(SETTINGS_INDEX_HREF2);
4733
5126
  const [cachedSettings, setCachedSettings] = useState(null);
4734
5127
  const themeMode = useResolvedThemeMode();
4735
5128
  const boardAccessRequirement = usePaperclipBoardAccessRequirement();
@@ -4758,7 +5151,10 @@ function GitHubSyncDashboardWidget() {
4758
5151
  hasBoardAccess: boardAccessReady
4759
5152
  });
4760
5153
  const syncUnlocked = syncSetupIssue === null;
4761
- 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));
4762
5158
  const scheduleFrequencyMinutes = normalizeScheduleFrequencyMinutes(current.scheduleFrequencyMinutes);
4763
5159
  const scheduleDescription = formatScheduleFrequency(scheduleFrequencyMinutes);
4764
5160
  const summary = getDashboardSummary({
@@ -4792,7 +5188,7 @@ function GitHubSyncDashboardWidget() {
4792
5188
  }
4793
5189
  } catch {
4794
5190
  if (!cancelled) {
4795
- setSettingsHref(SETTINGS_INDEX_HREF);
5191
+ setSettingsHref(SETTINGS_INDEX_HREF2);
4796
5192
  }
4797
5193
  }
4798
5194
  }
@@ -4858,6 +5254,34 @@ function GitHubSyncDashboardWidget() {
4858
5254
  setRunningSync(false);
4859
5255
  }
4860
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
+ }
4861
5285
  return /* @__PURE__ */ jsxs("section", { className: "ghsync-widget", style: themeVars, children: [
4862
5286
  /* @__PURE__ */ jsx("style", { children: WIDGET_STYLES }),
4863
5287
  /* @__PURE__ */ jsxs("div", { className: "ghsync-widget__card", children: [
@@ -4940,10 +5364,10 @@ function GitHubSyncDashboardWidget() {
4940
5364
  "button",
4941
5365
  {
4942
5366
  type: "button",
4943
- className: getPluginActionClassName({ variant: "primary" }),
4944
- onClick: handleRunSync,
4945
- disabled: syncInFlight || showInitialLoadingState,
4946
- 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"
4947
5371
  }
4948
5372
  ) : null
4949
5373
  ] }) })
@@ -4971,6 +5395,7 @@ function GitHubMarkIcon(props) {
4971
5395
  function GitHubSyncToolbarButtonSurface(props) {
4972
5396
  const toast = usePluginToast();
4973
5397
  const runSyncNow = usePluginAction("sync.runNow");
5398
+ const cancelSync = usePluginAction("sync.cancel");
4974
5399
  const pluginIdFromLocation = getPluginIdFromLocation();
4975
5400
  const surfaceRef = useRef(null);
4976
5401
  const resolvedIssue = useResolvedIssueId({
@@ -4990,6 +5415,7 @@ function GitHubSyncToolbarButtonSurface(props) {
4990
5415
  props.companyId ? { companyId: props.companyId } : {}
4991
5416
  );
4992
5417
  const [runningSync, setRunningSync] = useState(false);
5418
+ const [cancellingSync, setCancellingSync] = useState(false);
4993
5419
  const themeMode = useResolvedThemeMode();
4994
5420
  const boardAccessRequirement = usePaperclipBoardAccessRequirement();
4995
5421
  const theme = themeMode === "light" ? LIGHT_PALETTE : DARK_PALETTE;
@@ -5009,7 +5435,10 @@ function GitHubSyncToolbarButtonSurface(props) {
5009
5435
  const effectiveCanRun = state.canRun && !boardAccessSetupIssue;
5010
5436
  const effectiveMessage = boardAccessSetupIssue ? getSyncSetupMessage(boardAccessSetupIssue, hasCompanyContext) : state.message;
5011
5437
  const effectiveLabel = boardAccessSetupIssue ? "Board access required" : state.label;
5012
- 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));
5013
5442
  const armSyncCompletionToast = useSyncCompletionToast(state.syncState, toast);
5014
5443
  useEffect(() => {
5015
5444
  if (state.syncState.status !== "running") {
@@ -5114,6 +5543,31 @@ function GitHubSyncToolbarButtonSurface(props) {
5114
5543
  setRunningSync(false);
5115
5544
  }
5116
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
+ }
5117
5571
  return /* @__PURE__ */ jsxs(
5118
5572
  "div",
5119
5573
  {
@@ -5131,11 +5585,11 @@ function GitHubSyncToolbarButtonSurface(props) {
5131
5585
  "data-variant": "outline",
5132
5586
  "data-size": "sm",
5133
5587
  className: props.entityType ? HOST_ENTITY_BUTTON_CLASSNAME : HOST_GLOBAL_BUTTON_CLASSNAME,
5134
- disabled: !effectiveCanRun || syncInFlight || toolbarState.loading,
5135
- onClick: handleRunSync,
5588
+ disabled: toolbarState.loading || syncStartPending || (syncPersistedRunning ? cancellationRequested : !effectiveCanRun),
5589
+ onClick: syncPersistedRunning ? handleCancelSync : handleRunSync,
5136
5590
  children: [
5137
5591
  /* @__PURE__ */ jsx(GitHubMarkIcon, { className: "mr-1.5 h-3.5 w-3.5" }),
5138
- /* @__PURE__ */ jsx("span", { children: syncInFlight ? "Syncing\u2026" : effectiveLabel })
5592
+ /* @__PURE__ */ jsx("span", { children: syncPersistedRunning ? cancellationRequested ? "Cancelling\u2026" : "Cancel sync" : syncStartPending ? "Syncing\u2026" : effectiveLabel })
5139
5593
  ]
5140
5594
  }
5141
5595
  )