@ambitresearch/paperclip-agent-identities 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.
@@ -0,0 +1,3360 @@
1
+ // src/ui/index.tsx
2
+ import { usePluginData as usePluginData2 } from "@paperclipai/plugin-sdk/ui";
3
+
4
+ // src/ui/theme.ts
5
+ import { useEffect, useState } from "react";
6
+ var uiSurface = "var(--agent-identities-surface)";
7
+ var uiCanvas = "var(--agent-identities-canvas)";
8
+ var uiPanel = "var(--agent-identities-panel)";
9
+ var uiInput = "var(--agent-identities-input)";
10
+ var uiBorder = "var(--agent-identities-border)";
11
+ var uiBorderStrong = "var(--agent-identities-border-strong)";
12
+ var uiText = "var(--agent-identities-text)";
13
+ var uiMutedText = "var(--agent-identities-muted-text)";
14
+ var uiPrimary = "var(--agent-identities-primary)";
15
+ var uiPrimaryText = "var(--agent-identities-primary-text)";
16
+ var uiLink = "var(--agent-identities-link)";
17
+ var uiSuccess = "var(--agent-identities-success)";
18
+ var uiWarning = "var(--agent-identities-warning)";
19
+ var uiDanger = "var(--agent-identities-danger)";
20
+ var uiOverlay = "var(--agent-identities-overlay)";
21
+ var uiShadow = "var(--agent-identities-shadow)";
22
+ function createPaperclipThemeStyle(mode) {
23
+ return mode === "dark" ? darkThemeVars : lightThemeVars;
24
+ }
25
+ function usePaperclipThemeMode() {
26
+ const [mode, setMode] = useState(() => detectPaperclipThemeMode());
27
+ useEffect(() => {
28
+ const updateMode = () => setMode(detectPaperclipThemeMode());
29
+ const observedDocuments = [document, getSameOriginParentDocument()].filter((doc) => Boolean(doc));
30
+ const observers = observedDocuments.flatMap((doc) => {
31
+ const targets = [doc.documentElement, doc.body].filter((target) => Boolean(target));
32
+ return targets.map((target) => {
33
+ const observer = new MutationObserver(updateMode);
34
+ observer.observe(target, { attributes: true, attributeFilter: ["class", "data-theme", "style"] });
35
+ return observer;
36
+ });
37
+ });
38
+ const mediaQuery = window.matchMedia?.("(prefers-color-scheme: dark)");
39
+ mediaQuery?.addEventListener?.("change", updateMode);
40
+ updateMode();
41
+ return () => {
42
+ observers.forEach((observer) => observer.disconnect());
43
+ mediaQuery?.removeEventListener?.("change", updateMode);
44
+ };
45
+ }, []);
46
+ return mode;
47
+ }
48
+ function detectPaperclipThemeMode() {
49
+ if (typeof document === "undefined") return "light";
50
+ const documents = [document, getSameOriginParentDocument()].filter((doc) => Boolean(doc));
51
+ for (const doc of documents) {
52
+ const explicitMode = getExplicitThemeMode(doc.documentElement) ?? getExplicitThemeMode(doc.body);
53
+ if (explicitMode) return explicitMode;
54
+ }
55
+ if (window.matchMedia?.("(prefers-color-scheme: dark)").matches) return "dark";
56
+ return "light";
57
+ }
58
+ function getExplicitThemeMode(element) {
59
+ if (!element) return null;
60
+ const theme = element.dataset.theme?.toLowerCase();
61
+ if (theme === "dark") return "dark";
62
+ if (theme === "light") return "light";
63
+ if (element.classList.contains("dark") || element.classList.contains("dark-theme")) return "dark";
64
+ if (element.classList.contains("light") || element.classList.contains("light-theme")) return "light";
65
+ if (element.style.colorScheme === "dark") return "dark";
66
+ if (element.style.colorScheme === "light") return "light";
67
+ return null;
68
+ }
69
+ function getSameOriginParentDocument() {
70
+ if (typeof window === "undefined" || window.parent === window) return null;
71
+ try {
72
+ return window.parent.document;
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+ var lightThemeVars = {
78
+ colorScheme: "light",
79
+ "--agent-identities-surface": "oklch(100% 0 0)",
80
+ "--agent-identities-canvas": "oklch(100% 0 0)",
81
+ "--agent-identities-panel": "oklch(97% 0 0)",
82
+ "--agent-identities-muted-panel": "oklch(92.2% 0 0)",
83
+ "--agent-identities-input": "oklch(100% 0 0)",
84
+ "--agent-identities-border": "oklch(92.2% 0 0)",
85
+ "--agent-identities-border-strong": "oklch(70.8% 0 0)",
86
+ "--agent-identities-text": "oklch(14.5% 0 0)",
87
+ "--agent-identities-muted-text": "oklch(55.6% 0 0)",
88
+ "--agent-identities-primary": "oklch(20.5% 0 0)",
89
+ "--agent-identities-primary-text": "oklch(98.5% 0 0)",
90
+ "--agent-identities-link": "oklch(42% 0.16 264)",
91
+ "--agent-identities-success": "oklch(52.7% 0.154 150.069)",
92
+ "--agent-identities-warning": "oklch(55.5% 0.163 48.998)",
93
+ "--agent-identities-danger": "oklch(57.7% 0.245 27.325)",
94
+ "--agent-identities-overlay": "oklch(0% 0 0 / 42%)",
95
+ "--agent-identities-shadow": "oklch(0% 0 0 / 18%)"
96
+ };
97
+ var darkThemeVars = {
98
+ colorScheme: "dark",
99
+ "--agent-identities-surface": "oklch(20.5% 0 0)",
100
+ "--agent-identities-canvas": "oklch(20.5% 0 0)",
101
+ "--agent-identities-panel": "oklch(26.9% 0 0)",
102
+ "--agent-identities-muted-panel": "oklch(35% 0 0)",
103
+ "--agent-identities-input": "oklch(26.9% 0 0)",
104
+ "--agent-identities-border": "oklch(35% 0 0)",
105
+ "--agent-identities-border-strong": "oklch(43.9% 0 0)",
106
+ "--agent-identities-text": "oklch(98.5% 0 0)",
107
+ "--agent-identities-muted-text": "oklch(70.8% 0 0)",
108
+ "--agent-identities-primary": "oklch(98.5% 0 0)",
109
+ "--agent-identities-primary-text": "oklch(20.5% 0 0)",
110
+ "--agent-identities-link": "oklch(78% 0.12 264)",
111
+ "--agent-identities-success": "oklch(72.3% 0.219 149.579)",
112
+ "--agent-identities-warning": "oklch(85.2% 0.199 91.936)",
113
+ "--agent-identities-danger": "oklch(70.4% 0.191 22.216)",
114
+ "--agent-identities-overlay": "oklch(0% 0 0 / 58%)",
115
+ "--agent-identities-shadow": "oklch(0% 0 0 / 45%)"
116
+ };
117
+
118
+ // src/ui/SettingsPage.tsx
119
+ import { useEffect as useEffect4, useState as useState4 } from "react";
120
+ import { usePluginData, usePluginAction } from "@paperclipai/plugin-sdk/ui";
121
+
122
+ // src/shared/types.ts
123
+ var GITHUB_IDENTITY_PROVIDER_ID = "github";
124
+ var SLACK_IDENTITY_PROVIDER_ID = "slack";
125
+ var REBIND_LEGACY_SLACK_CREDENTIALS_ACTION = "rebind-legacy-slack-credentials";
126
+ var SUPPORTED_IDENTITY_PROVIDERS = [
127
+ {
128
+ id: "github",
129
+ name: "GitHub",
130
+ description: "GitHub App identity for repositories, pull requests, branch pushes, and commit attribution.",
131
+ status: "enabled"
132
+ },
133
+ {
134
+ id: "slack",
135
+ name: "Slack",
136
+ description: "Workspace identity for Slack messages and app-mediated actions.",
137
+ status: "coming-soon"
138
+ },
139
+ {
140
+ id: "mattermost",
141
+ name: "Mattermost",
142
+ description: "Team identity for Mattermost posts and channel operations.",
143
+ status: "coming-soon"
144
+ },
145
+ {
146
+ id: "entra",
147
+ name: "Microsoft Entra",
148
+ description: "Cloud directory identity for Microsoft Graph and Azure-backed workflows.",
149
+ status: "coming-soon"
150
+ },
151
+ {
152
+ id: "gcp",
153
+ name: "Google Cloud",
154
+ description: "Service account identity for Google Cloud APIs.",
155
+ status: "coming-soon"
156
+ },
157
+ {
158
+ id: "aws",
159
+ name: "AWS",
160
+ description: "IAM-backed identity for AWS APIs.",
161
+ status: "coming-soon"
162
+ }
163
+ ];
164
+ function isIdentityProviderId(value) {
165
+ return SUPPORTED_IDENTITY_PROVIDERS.some((provider) => provider.id === value);
166
+ }
167
+ var DEFAULT_BOT_IDENTITY_CONFIG = {
168
+ provider: GITHUB_IDENTITY_PROVIDER_ID,
169
+ id: "",
170
+ agentId: "",
171
+ label: "",
172
+ github: { username: "" }
173
+ };
174
+
175
+ // src/shared/slack-bot-whoami-tool.ts
176
+ var slackBotWhoamiToolName = "slack_bot_whoami";
177
+ var slackBotWhoamiToolMetadata = {
178
+ displayName: "Slack Identity Who Am I",
179
+ description: "Returns the calling agent's configured Slack identity metadata.",
180
+ parametersSchema: {
181
+ type: "object",
182
+ properties: {},
183
+ additionalProperties: false
184
+ }
185
+ };
186
+ var slackBotWhoamiManifestTool = {
187
+ name: slackBotWhoamiToolName,
188
+ ...slackBotWhoamiToolMetadata
189
+ };
190
+
191
+ // src/core/provider-settings-contract.ts
192
+ function buildProviderSettingsRegistry(adapters, defaultProviderId) {
193
+ const ordered = [...adapters];
194
+ const byId = new Map(ordered.map((adapter) => [adapter.providerId, adapter]));
195
+ const fallback = byId.get(defaultProviderId) ?? ordered[0];
196
+ if (!fallback) {
197
+ throw new Error("buildProviderSettingsRegistry: at least one adapter is required");
198
+ }
199
+ return {
200
+ get(providerId) {
201
+ return byId.get(providerId) ?? fallback;
202
+ },
203
+ all() {
204
+ return ordered;
205
+ }
206
+ };
207
+ }
208
+
209
+ // src/providers/github/settings-adapter.ts
210
+ var GITHUB_SETTINGS_PROVIDER_ID = "github";
211
+ var githubSettingsAdapter = {
212
+ providerId: GITHUB_SETTINGS_PROVIDER_ID,
213
+ formSteps: [
214
+ { id: "identity", label: "Identity" },
215
+ { id: "github", label: "GitHub App" },
216
+ { id: "commit", label: "Commit" }
217
+ ],
218
+ credentialStepId: "github",
219
+ savesViaSeparateAction: false,
220
+ hasProviderAccountFieldsInIdentityStep: true,
221
+ getValidation(config, hasDuplicate) {
222
+ const hasIdentity = Boolean(config.agentId.trim() && config.provider.trim() && config.label.trim() && config.githubUsername.trim()) && !hasDuplicate;
223
+ const hasGitHubAppCredential = Boolean(
224
+ config.githubAppId.trim() && config.githubInstallationId.trim() && (config.privateKeySecretId.trim() || config.privateKeyFile.trim())
225
+ );
226
+ const hasFallbackCredential2 = Boolean(config.fallbackTokenSecretId.trim() || config.tokenFile.trim());
227
+ const identityComplete = hasIdentity;
228
+ const credentialComplete = hasGitHubAppCredential || hasFallbackCredential2;
229
+ const identityMessage = hasDuplicate ? "This agent already has an identity for the selected provider. Edit the existing identity instead." : !hasIdentity ? "Choose an agent, provider, label, and provider username before continuing." : "Identity details are complete.";
230
+ const credentialMessage = credentialComplete ? "Credential source is complete." : "Add a complete GitHub App credential, or choose a fallback token source, before this identity can be saved.";
231
+ const saveMessage = !identityComplete ? identityMessage : !credentialComplete ? credentialMessage : "Required setup is complete. Review optional commit metadata, then save.";
232
+ return {
233
+ identityComplete,
234
+ credentialComplete,
235
+ isComplete: identityComplete && credentialComplete,
236
+ identityMessage,
237
+ credentialMessage,
238
+ saveMessage
239
+ };
240
+ }
241
+ };
242
+
243
+ // src/providers/slack/settings-adapter.ts
244
+ var SLACK_SETTINGS_PROVIDER_ID = "slack";
245
+ var slackSettingsAdapter = {
246
+ providerId: SLACK_SETTINGS_PROVIDER_ID,
247
+ formSteps: [
248
+ { id: "identity", label: "Identity" },
249
+ { id: "slack", label: "Slack App" }
250
+ ],
251
+ credentialStepId: "slack",
252
+ savesViaSeparateAction: true,
253
+ hasProviderAccountFieldsInIdentityStep: false,
254
+ getValidation(config, hasDuplicate, extra) {
255
+ const slackSaveResult = extra.slackSaveResult ?? null;
256
+ const slackSaveBusy = Boolean(extra.slackSaveBusy);
257
+ const hasIdentity = Boolean(config.agentId.trim() && config.provider.trim() && config.label.trim()) && !hasDuplicate;
258
+ const hasSlackInstallFields = Boolean(
259
+ config.slackTeamId.trim() && config.slackAppId.trim() && config.slackBotUserId.trim() && config.slackBotTokenSecretId.trim() && config.slackSigningSecretId.trim()
260
+ );
261
+ const slackSaveMatchesCurrentFields = Boolean(
262
+ slackSaveResult && slackSaveResult.teamId === config.slackTeamId.trim() && slackSaveResult.appId === config.slackAppId.trim() && slackSaveResult.botUserId === config.slackBotUserId.trim() && slackSaveResult.eventsRequestUrl === config.slackEventsRequestUrl.trim() && slackSaveResult.botTokenSecretId === config.slackBotTokenSecretId.trim() && slackSaveResult.signingSecretId === config.slackSigningSecretId.trim() && (slackSaveResult.defaultChannel ?? "") === config.slackDefaultChannel.trim()
263
+ );
264
+ const hasSlackInstall = hasSlackInstallFields && slackSaveMatchesCurrentFields && !slackSaveBusy;
265
+ const identityComplete = hasIdentity;
266
+ const credentialComplete = hasSlackInstall;
267
+ const identityMessage = hasDuplicate ? "This agent already has an identity for the selected provider. Edit the existing identity instead." : !hasIdentity ? "Choose an agent, provider, and label before continuing." : "Identity details are complete.";
268
+ const credentialMessage = credentialComplete ? "Slack install metadata is complete." : slackSaveBusy ? "Saving Slack install metadata..." : "Create the Slack App manifest, install it, and paste back the team/app/bot IDs plus bot token and signing secret references, then save install metadata before this identity can be saved.";
269
+ const saveMessage = !identityComplete ? identityMessage : !credentialComplete ? credentialMessage : "Required setup is complete.";
270
+ return {
271
+ identityComplete,
272
+ credentialComplete,
273
+ isComplete: identityComplete && credentialComplete,
274
+ identityMessage,
275
+ credentialMessage,
276
+ saveMessage
277
+ };
278
+ }
279
+ };
280
+
281
+ // src/providers/settings-index.ts
282
+ var ALL_SETTINGS_ADAPTERS = [githubSettingsAdapter, slackSettingsAdapter];
283
+
284
+ // src/core/provider-settings-ui-contract.ts
285
+ function buildProviderSettingsUIRegistry(adapters) {
286
+ const byId = new Map(adapters.map((adapter) => [adapter.providerId, adapter]));
287
+ return {
288
+ get(providerId) {
289
+ return byId.get(providerId);
290
+ }
291
+ };
292
+ }
293
+
294
+ // src/providers/slack/settings-adapter-ui.tsx
295
+ import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
296
+ import { jsx, jsxs } from "react/jsx-runtime";
297
+ var SLACK_MANIFEST_STATE_STORAGE_PREFIX = "paperclip-agent-identities:slack-app-manifest-state:";
298
+ function normalizeSlackAppIdInput(value) {
299
+ const trimmed = value.trim();
300
+ if (/^A[A-Z0-9]+$/i.test(trimmed)) {
301
+ return trimmed.toUpperCase();
302
+ }
303
+ try {
304
+ const url = new URL(trimmed);
305
+ const path = url.pathname.split("/").filter(Boolean);
306
+ const appId = path[0] === "apps" ? path[1] : void 0;
307
+ if (url.hostname === "api.slack.com" && appId && /^A[A-Z0-9]+$/i.test(appId)) {
308
+ return appId.toUpperCase();
309
+ }
310
+ } catch {
311
+ }
312
+ return value;
313
+ }
314
+ function actionErrorMessage(error, fallback) {
315
+ if (error instanceof Error && error.message.trim()) return error.message;
316
+ if (typeof error === "object" && error !== null) {
317
+ const message = error.message;
318
+ if (typeof message === "string" && message.trim()) return message;
319
+ }
320
+ return fallback;
321
+ }
322
+ function getSlackManifestStateStorageKey(companyId, agentId) {
323
+ return `${SLACK_MANIFEST_STATE_STORAGE_PREFIX}${companyId}:${agentId}`;
324
+ }
325
+ function writeSlackManifestFlowState(companyId, agentId, state) {
326
+ try {
327
+ window.sessionStorage.setItem(getSlackManifestStateStorageKey(companyId, agentId), state);
328
+ } catch {
329
+ }
330
+ }
331
+ function readSlackManifestFlowState(companyId, agentId) {
332
+ try {
333
+ return window.sessionStorage.getItem(getSlackManifestStateStorageKey(companyId, agentId));
334
+ } catch {
335
+ return null;
336
+ }
337
+ }
338
+ function deleteSlackManifestFlowState(companyId, agentId) {
339
+ try {
340
+ window.sessionStorage.removeItem(getSlackManifestStateStorageKey(companyId, agentId));
341
+ } catch {
342
+ }
343
+ }
344
+ async function copyTextToClipboard(value) {
345
+ try {
346
+ if (navigator.clipboard?.writeText) {
347
+ await navigator.clipboard.writeText(value);
348
+ return;
349
+ }
350
+ } catch {
351
+ }
352
+ const textarea = document.createElement("textarea");
353
+ textarea.value = value;
354
+ textarea.setAttribute("readonly", "");
355
+ textarea.style.position = "fixed";
356
+ textarea.style.opacity = "0";
357
+ document.body.appendChild(textarea);
358
+ textarea.select();
359
+ try {
360
+ const copied = document.execCommand("copy");
361
+ if (!copied) {
362
+ throw new Error("execCommand('copy') was rejected");
363
+ }
364
+ } finally {
365
+ textarea.remove();
366
+ }
367
+ }
368
+ function useSlackCredentialStep(input) {
369
+ const { config, hasPersistedIdentity, updateField, refresh, deleteConfig, patchFormState, createSlackAppManifest, getSlackAppManifestFlow, discoverSlackInstallMetadata, saveSlackInstallMetadata, rebindLegacySlackCredentials, slackBotWhoami, companyId } = input;
370
+ const [slackManifestFlow, setSlackManifestFlow] = useState2(null);
371
+ const [slackManifestBusy, setSlackManifestBusy] = useState2(false);
372
+ const [slackSaveBusy, setSlackSaveBusy] = useState2(false);
373
+ const [slackManifestError, setSlackManifestError] = useState2(null);
374
+ const [slackSaveResult, setSlackSaveResult] = useState2(null);
375
+ const [slackManifestCopied, setSlackManifestCopied] = useState2(false);
376
+ const [slackDiscoveryBusy, setSlackDiscoveryBusy] = useState2(false);
377
+ const [slackDiscoveryError, setSlackDiscoveryError] = useState2(null);
378
+ const [slackResumeStateInput, setSlackResumeStateInput] = useState2("");
379
+ const [slackResumeBusy, setSlackResumeBusy] = useState2(false);
380
+ const [slackResumeError, setSlackResumeError] = useState2(null);
381
+ const [slackStatus, setSlackStatus] = useState2(null);
382
+ const [slackStatusLoading, setSlackStatusLoading] = useState2(false);
383
+ const [slackStatusError, setSlackStatusError] = useState2(null);
384
+ const [legacySlackRebindBusy, setLegacySlackRebindBusy] = useState2(false);
385
+ const [legacySlackRebindError, setLegacySlackRebindError] = useState2(null);
386
+ const [legacySlackRebindResult, setLegacySlackRebindResult] = useState2(null);
387
+ const slackStatusGenerationRef = useRef(0);
388
+ const slackDiscoveryGenerationRef = useRef(0);
389
+ const [slackCleanupPendingAgentId, setSlackCleanupPendingAgentId] = useState2(null);
390
+ const [slackCleanupBusy, setSlackCleanupBusy] = useState2(false);
391
+ const [slackCleanupError, setSlackCleanupError] = useState2(null);
392
+ const slackCleanupPendingResultRef = useRef(null);
393
+ const slackSaveGenerationRef = useRef(0);
394
+ const slackManifestFlowGenerationRef = useRef(0);
395
+ function reset() {
396
+ setSlackManifestFlow(null);
397
+ setSlackManifestError(null);
398
+ setSlackSaveResult(null);
399
+ setSlackManifestCopied(false);
400
+ setSlackDiscoveryBusy(false);
401
+ setSlackDiscoveryError(null);
402
+ slackDiscoveryGenerationRef.current += 1;
403
+ setSlackResumeStateInput("");
404
+ setSlackResumeError(null);
405
+ slackManifestFlowGenerationRef.current += 1;
406
+ setSlackManifestBusy(false);
407
+ setSlackResumeBusy(false);
408
+ slackSaveGenerationRef.current += 1;
409
+ setSlackSaveBusy(false);
410
+ setSlackCleanupPendingAgentId(null);
411
+ setSlackCleanupError(null);
412
+ setSlackCleanupBusy(false);
413
+ slackCleanupPendingResultRef.current = null;
414
+ setSlackStatus(null);
415
+ setSlackStatusError(null);
416
+ setSlackStatusLoading(false);
417
+ slackStatusGenerationRef.current += 1;
418
+ setLegacySlackRebindBusy(false);
419
+ setLegacySlackRebindError(null);
420
+ setLegacySlackRebindResult(null);
421
+ }
422
+ const slackFieldsSignature = config ? `${config.slackTeamId}|${config.slackAppId}|${config.slackBotUserId}|${config.slackDefaultChannel}|${config.slackBotTokenSecretId}|${config.slackSigningSecretId}` : "";
423
+ const prevSlackFieldsSignatureRef = useRef(slackFieldsSignature);
424
+ useEffect2(() => {
425
+ if (prevSlackFieldsSignatureRef.current !== slackFieldsSignature) {
426
+ prevSlackFieldsSignatureRef.current = slackFieldsSignature;
427
+ setSlackSaveResult(null);
428
+ slackSaveGenerationRef.current += 1;
429
+ setSlackSaveBusy(false);
430
+ }
431
+ }, [slackFieldsSignature]);
432
+ const slackDiscoverySecretSignature = config?.slackBotTokenSecretId ?? "";
433
+ const prevSlackDiscoverySecretSignatureRef = useRef(slackDiscoverySecretSignature);
434
+ useEffect2(() => {
435
+ if (prevSlackDiscoverySecretSignatureRef.current === slackDiscoverySecretSignature) return;
436
+ prevSlackDiscoverySecretSignatureRef.current = slackDiscoverySecretSignature;
437
+ slackDiscoveryGenerationRef.current += 1;
438
+ setSlackDiscoveryBusy(false);
439
+ setSlackDiscoveryError(null);
440
+ }, [slackDiscoverySecretSignature]);
441
+ useEffect2(() => {
442
+ if (!config || config.provider !== SLACK_IDENTITY_PROVIDER_ID) return;
443
+ if (!hasPersistedIdentity || !config.agentId.trim()) {
444
+ slackStatusGenerationRef.current += 1;
445
+ setSlackStatus(null);
446
+ setSlackStatusError(null);
447
+ setSlackStatusLoading(false);
448
+ return;
449
+ }
450
+ void handleCheckSlackStatus();
451
+ }, [config?.agentId, config?.label, config?.provider, hasPersistedIdentity]);
452
+ useEffect2(() => {
453
+ if (!config || config.provider !== SLACK_IDENTITY_PROVIDER_ID) return;
454
+ if (slackManifestFlow) return;
455
+ const agentId = config.agentId.trim();
456
+ if (!agentId) return;
457
+ const storedState = readSlackManifestFlowState(companyId, agentId);
458
+ if (!storedState) return;
459
+ let cancelled = false;
460
+ const generation = slackManifestFlowGenerationRef.current;
461
+ void getSlackAppManifestFlow({ state: storedState }).then((result) => {
462
+ if (cancelled || slackManifestFlowGenerationRef.current !== generation) return;
463
+ const flow = result;
464
+ if (flow.agentId !== agentId) {
465
+ deleteSlackManifestFlowState(companyId, agentId);
466
+ return;
467
+ }
468
+ if (config.label.trim() && flow.label && flow.label !== config.label.trim()) {
469
+ deleteSlackManifestFlowState(companyId, agentId);
470
+ return;
471
+ }
472
+ if (config.slackEventsRequestUrl.trim() && flow.eventsRequestUrl !== config.slackEventsRequestUrl.trim()) {
473
+ deleteSlackManifestFlowState(companyId, agentId);
474
+ return;
475
+ }
476
+ if (!config.slackEventsRequestUrl.trim()) {
477
+ updateField("slackEventsRequestUrl", flow.eventsRequestUrl);
478
+ }
479
+ setSlackManifestFlow(flow);
480
+ setSlackManifestCopied(false);
481
+ }).catch((err) => {
482
+ if (cancelled) return;
483
+ const message = err instanceof Error ? err.message : String(err);
484
+ const isDefinitivelyGone = /unknown or expired|already been used|has expired/i.test(message);
485
+ if (isDefinitivelyGone) {
486
+ deleteSlackManifestFlowState(companyId, agentId);
487
+ } else {
488
+ setSlackManifestError(
489
+ `Could not restore the in-progress Slack App manifest flow (${message}). It has not been discarded - reload or try again.`
490
+ );
491
+ }
492
+ });
493
+ return () => {
494
+ cancelled = true;
495
+ };
496
+ }, [config, getSlackAppManifestFlow, slackManifestFlow, companyId]);
497
+ async function handleResumeSlackAppManifestFlow() {
498
+ if (!config) return;
499
+ const state = slackResumeStateInput.trim();
500
+ if (!state) return;
501
+ const generation = slackManifestFlowGenerationRef.current;
502
+ setSlackResumeBusy(true);
503
+ setSlackResumeError(null);
504
+ try {
505
+ const flow = await getSlackAppManifestFlow({ state });
506
+ if (slackManifestFlowGenerationRef.current !== generation) return;
507
+ if (flow.agentId !== config.agentId.trim()) {
508
+ throw new Error("This state token belongs to a different agent than the one currently selected.");
509
+ }
510
+ if (config.label.trim() && flow.label && flow.label !== config.label.trim()) {
511
+ throw new Error("This state token belongs to a different label than the one currently entered.");
512
+ }
513
+ if (config.slackEventsRequestUrl.trim() && flow.eventsRequestUrl !== config.slackEventsRequestUrl.trim()) {
514
+ throw new Error("This state token belongs to a different Events Request URL than the one currently entered.");
515
+ }
516
+ if (!config.slackEventsRequestUrl.trim()) {
517
+ updateField("slackEventsRequestUrl", flow.eventsRequestUrl);
518
+ }
519
+ setSlackManifestFlow(flow);
520
+ setSlackManifestCopied(false);
521
+ setSlackManifestError(null);
522
+ writeSlackManifestFlowState(companyId, config.agentId.trim(), flow.state);
523
+ } catch (err) {
524
+ if (slackManifestFlowGenerationRef.current !== generation) return;
525
+ setSlackResumeError(err instanceof Error ? err.message : "Could not restore the Slack App manifest flow for that state token.");
526
+ } finally {
527
+ if (slackManifestFlowGenerationRef.current === generation) {
528
+ setSlackResumeBusy(false);
529
+ }
530
+ }
531
+ }
532
+ async function handleCreateSlackAppManifest() {
533
+ if (!config) return;
534
+ if (config.provider !== SLACK_IDENTITY_PROVIDER_ID) {
535
+ setSlackManifestError("Slack App setup is only available for the Slack provider.");
536
+ return;
537
+ }
538
+ const generation = slackManifestFlowGenerationRef.current;
539
+ setSlackManifestBusy(true);
540
+ setSlackManifestError(null);
541
+ setSlackSaveResult(null);
542
+ try {
543
+ const result = await createSlackAppManifest({
544
+ agentId: config.agentId.trim(),
545
+ provider: config.provider,
546
+ label: config.label.trim(),
547
+ eventsRequestUrl: config.slackEventsRequestUrl.trim()
548
+ });
549
+ if (slackManifestFlowGenerationRef.current !== generation) return;
550
+ setSlackManifestFlow(result);
551
+ setSlackManifestCopied(false);
552
+ writeSlackManifestFlowState(companyId, config.agentId.trim(), result.state);
553
+ } catch (err) {
554
+ if (slackManifestFlowGenerationRef.current !== generation) return;
555
+ setSlackManifestError(err instanceof Error ? err.message : "Failed to create Slack App manifest");
556
+ } finally {
557
+ if (slackManifestFlowGenerationRef.current === generation) {
558
+ setSlackManifestBusy(false);
559
+ }
560
+ }
561
+ }
562
+ async function handleSaveSlackInstallMetadata() {
563
+ if (!config || !slackManifestFlow) return;
564
+ const generation = ++slackSaveGenerationRef.current;
565
+ setSlackSaveBusy(true);
566
+ setSlackManifestError(null);
567
+ try {
568
+ const targetAgentId = config.agentId.trim();
569
+ const previousAgentId = config.previousAgentId.trim();
570
+ const botTokenSecretId = config.slackBotTokenSecretId.trim();
571
+ const signingSecretId = config.slackSigningSecretId.trim();
572
+ const result = await saveSlackInstallMetadata({
573
+ state: slackManifestFlow.state,
574
+ agentId: targetAgentId,
575
+ teamId: config.slackTeamId.trim(),
576
+ appId: config.slackAppId.trim(),
577
+ botUserId: config.slackBotUserId.trim(),
578
+ botTokenSecretId,
579
+ signingSecretId,
580
+ ...config.slackDefaultChannel.trim() ? { defaultChannel: config.slackDefaultChannel.trim() } : {}
581
+ });
582
+ if (slackSaveGenerationRef.current !== generation) {
583
+ return;
584
+ }
585
+ if (previousAgentId && previousAgentId !== targetAgentId) {
586
+ try {
587
+ await deleteConfig({ agentId: previousAgentId, provider: SLACK_IDENTITY_PROVIDER_ID });
588
+ } catch (err) {
589
+ setSlackManifestError(
590
+ `Slack install metadata saved, but could not remove the previous identity for ${previousAgentId}: ${err instanceof Error ? err.message : "unknown error"}. Use "Retry cleanup" below.`
591
+ );
592
+ setSlackCleanupPendingAgentId(previousAgentId);
593
+ setSlackCleanupError(null);
594
+ slackCleanupPendingResultRef.current = result;
595
+ setSlackManifestFlow(null);
596
+ deleteSlackManifestFlowState(companyId, targetAgentId);
597
+ await refresh();
598
+ await handleCheckSlackStatus();
599
+ return;
600
+ }
601
+ }
602
+ setSlackSaveResult(result);
603
+ setSlackManifestFlow(null);
604
+ deleteSlackManifestFlowState(companyId, targetAgentId);
605
+ patchFormState((prev) => ({ ...prev, previousAgentId: result.agentId }));
606
+ await refresh();
607
+ if (hasPersistedIdentity) {
608
+ await handleCheckSlackStatus();
609
+ }
610
+ } catch (err) {
611
+ if (slackSaveGenerationRef.current !== generation) return;
612
+ setSlackManifestError(err instanceof Error ? err.message : "Failed to save Slack install metadata");
613
+ } finally {
614
+ if (slackSaveGenerationRef.current === generation) {
615
+ setSlackSaveBusy(false);
616
+ }
617
+ }
618
+ }
619
+ async function handleDiscoverSlackInstallMetadata() {
620
+ if (!config) return;
621
+ const botTokenSecretId = config.slackBotTokenSecretId.trim();
622
+ if (!botTokenSecretId) return;
623
+ const generation = ++slackDiscoveryGenerationRef.current;
624
+ setSlackDiscoveryBusy(true);
625
+ setSlackDiscoveryError(null);
626
+ try {
627
+ const result = await discoverSlackInstallMetadata({ botTokenSecretId });
628
+ if (slackDiscoveryGenerationRef.current !== generation) return;
629
+ const teamId = result.teamId?.trim();
630
+ const botUserId = result.botUserId?.trim();
631
+ const appId = result.appId?.trim();
632
+ if (!teamId || !botUserId || !appId) {
633
+ throw new Error("Slack did not return the workspace ID, App ID, and bot user ID for this token.");
634
+ }
635
+ updateField("slackTeamId", teamId);
636
+ updateField("slackAppId", appId);
637
+ updateField("slackBotUserId", botUserId);
638
+ } catch (err) {
639
+ if (slackDiscoveryGenerationRef.current !== generation) return;
640
+ setSlackDiscoveryError(actionErrorMessage(err, "Could not detect Slack installation metadata"));
641
+ } finally {
642
+ if (slackDiscoveryGenerationRef.current === generation) {
643
+ setSlackDiscoveryBusy(false);
644
+ }
645
+ }
646
+ }
647
+ async function handleRetrySlackCleanup() {
648
+ const pendingAgentId = slackCleanupPendingAgentId;
649
+ const pendingResult = slackCleanupPendingResultRef.current;
650
+ if (!pendingAgentId || !pendingResult) return;
651
+ const generation = slackSaveGenerationRef.current;
652
+ setSlackCleanupBusy(true);
653
+ setSlackCleanupError(null);
654
+ try {
655
+ await deleteConfig({ agentId: pendingAgentId, provider: SLACK_IDENTITY_PROVIDER_ID });
656
+ if (slackSaveGenerationRef.current !== generation) return;
657
+ setSlackCleanupPendingAgentId(null);
658
+ slackCleanupPendingResultRef.current = null;
659
+ setSlackManifestError(null);
660
+ setSlackSaveResult(pendingResult);
661
+ patchFormState((prev) => ({ ...prev, previousAgentId: pendingResult.agentId }));
662
+ await refresh();
663
+ } catch (err) {
664
+ if (slackSaveGenerationRef.current !== generation) return;
665
+ setSlackCleanupError(
666
+ err instanceof Error ? err.message : `Failed to remove the previous identity for ${pendingAgentId}`
667
+ );
668
+ } finally {
669
+ if (slackSaveGenerationRef.current === generation) {
670
+ setSlackCleanupBusy(false);
671
+ }
672
+ }
673
+ }
674
+ async function handleCheckSlackStatus() {
675
+ const agentId = config?.agentId?.trim();
676
+ if (!agentId) return;
677
+ const generation = ++slackStatusGenerationRef.current;
678
+ setSlackStatusLoading(true);
679
+ setSlackStatusError(null);
680
+ try {
681
+ const result = await slackBotWhoami({ agentId, companyId });
682
+ if (slackStatusGenerationRef.current !== generation) return;
683
+ const resultError = result?.error;
684
+ if (resultError) {
685
+ setSlackStatus(null);
686
+ setSlackStatusError(typeof resultError === "string" ? resultError : "Identity metadata unavailable");
687
+ return;
688
+ }
689
+ const data = result?.data ?? result;
690
+ setSlackStatus(data ?? null);
691
+ } catch (err) {
692
+ if (slackStatusGenerationRef.current !== generation) return;
693
+ setSlackStatus(null);
694
+ setSlackStatusError(err instanceof Error ? err.message : "Identity metadata unavailable");
695
+ } finally {
696
+ if (slackStatusGenerationRef.current === generation) {
697
+ setSlackStatusLoading(false);
698
+ }
699
+ }
700
+ }
701
+ async function handleReinstallSlackApp() {
702
+ if (!config) return;
703
+ const confirmed = window.confirm(
704
+ "Reinstall the Slack App for this identity? This creates a new Slack App manifest flow; you'll need to re-create/reinstall the app on Slack and paste back the resulting IDs and bot token secret."
705
+ );
706
+ if (!confirmed) return;
707
+ await handleCreateSlackAppManifest();
708
+ }
709
+ async function handleLegacySlackRebind() {
710
+ if (!config?.agentId.trim()) return;
711
+ const signingSecretId = config.slackSigningSecretId.trim();
712
+ if (config.slackLegacySigningSecretRequired === "true" && !signingSecretId) {
713
+ setLegacySlackRebindError(
714
+ "Select or enter the Paperclip company secret UUID containing the Slack signing secret before rebinding."
715
+ );
716
+ return;
717
+ }
718
+ setLegacySlackRebindBusy(true);
719
+ setLegacySlackRebindError(null);
720
+ try {
721
+ const result = await rebindLegacySlackCredentials({
722
+ agentId: config.agentId.trim(),
723
+ ...signingSecretId ? { signingSecretId } : {}
724
+ });
725
+ setLegacySlackRebindResult(result);
726
+ await refresh();
727
+ } catch (error) {
728
+ setLegacySlackRebindResult(null);
729
+ setLegacySlackRebindError(actionErrorMessage(error, "Could not rebind released Slack credentials"));
730
+ } finally {
731
+ setLegacySlackRebindBusy(false);
732
+ }
733
+ }
734
+ return {
735
+ validationExtra: { slackSaveResult, slackSaveBusy },
736
+ reset,
737
+ slackManifestFlow,
738
+ slackManifestBusy,
739
+ slackSaveBusy,
740
+ slackManifestError,
741
+ slackSaveResult,
742
+ slackManifestCopied,
743
+ slackDiscoveryBusy,
744
+ slackDiscoveryError,
745
+ setSlackManifestCopied,
746
+ setSlackManifestError,
747
+ slackResumeStateInput,
748
+ setSlackResumeStateInput,
749
+ slackResumeBusy,
750
+ slackResumeError,
751
+ handleResumeSlackAppManifestFlow,
752
+ handleCreateSlackAppManifest,
753
+ handleDiscoverSlackInstallMetadata,
754
+ handleSaveSlackInstallMetadata,
755
+ slackCleanupPendingAgentId,
756
+ slackCleanupBusy,
757
+ slackCleanupError,
758
+ handleRetrySlackCleanup,
759
+ slackStatus,
760
+ slackStatusLoading,
761
+ slackStatusError,
762
+ handleCheckSlackStatus,
763
+ hasPersistedIdentity,
764
+ handleReinstallSlackApp,
765
+ legacySlackRebindBusy,
766
+ legacySlackRebindError,
767
+ legacySlackRebindResult,
768
+ handleLegacySlackRebind,
769
+ updateField,
770
+ secretOptions: input.secretOptions,
771
+ secretsLoading: input.secretsLoading,
772
+ secretsError: input.secretsError,
773
+ companyId: input.companyId
774
+ };
775
+ }
776
+ function getSecretFieldHint(input) {
777
+ if (!input.companyId) {
778
+ return "No company context is available, so paste the Paperclip secret UUID manually.";
779
+ }
780
+ if (input.secretsLoading) {
781
+ return "Loading Paperclip secrets...";
782
+ }
783
+ if (input.secretsError) {
784
+ return `Could not load Paperclip secrets (${input.secretsError}); paste the secret UUID manually.`;
785
+ }
786
+ if (!input.hasSecretOptions) {
787
+ return "No Paperclip secrets were found; paste the bot token secret UUID manually.";
788
+ }
789
+ return "Saved as a Paperclip secret reference for the Slack bot token. There is no private-key or file fallback for Slack.";
790
+ }
791
+ function getSigningSecretFieldHint(input) {
792
+ if (!input.companyId) {
793
+ return "No company context is available, so paste the Paperclip secret UUID manually.";
794
+ }
795
+ if (input.secretsLoading) {
796
+ return "Loading Paperclip secrets...";
797
+ }
798
+ if (input.secretsError) {
799
+ return `Could not load Paperclip secrets (${input.secretsError}); paste the secret UUID manually.`;
800
+ }
801
+ if (!input.hasSecretOptions) {
802
+ return "No Paperclip secrets were found; paste the signing secret UUID manually.";
803
+ }
804
+ return "Saved as a Paperclip secret reference for Slack request signature verification.";
805
+ }
806
+ function formatSecretOption(secret) {
807
+ const label = secret.name || secret.key || secret.id;
808
+ const details = [secret.key && secret.key !== label ? secret.key : null, secret.status, secret.provider].filter(Boolean).join(" - ");
809
+ return details ? `${label} (${details})` : label;
810
+ }
811
+ function SlackCredentialStep(props) {
812
+ const { state, config } = props;
813
+ const {
814
+ slackManifestFlow,
815
+ slackManifestBusy,
816
+ slackSaveBusy,
817
+ slackManifestError,
818
+ slackSaveResult,
819
+ slackManifestCopied,
820
+ slackDiscoveryBusy,
821
+ slackDiscoveryError,
822
+ setSlackManifestCopied,
823
+ setSlackManifestError,
824
+ slackResumeStateInput,
825
+ setSlackResumeStateInput,
826
+ slackResumeBusy,
827
+ slackResumeError,
828
+ handleResumeSlackAppManifestFlow,
829
+ handleCreateSlackAppManifest,
830
+ handleDiscoverSlackInstallMetadata,
831
+ handleSaveSlackInstallMetadata,
832
+ slackCleanupPendingAgentId,
833
+ slackCleanupBusy,
834
+ slackCleanupError,
835
+ handleRetrySlackCleanup,
836
+ slackStatus,
837
+ slackStatusLoading,
838
+ slackStatusError,
839
+ handleReinstallSlackApp,
840
+ legacySlackRebindBusy,
841
+ legacySlackRebindError,
842
+ legacySlackRebindResult,
843
+ handleLegacySlackRebind,
844
+ hasPersistedIdentity,
845
+ validationExtra,
846
+ updateField,
847
+ secretOptions,
848
+ secretsLoading,
849
+ secretsError,
850
+ companyId
851
+ } = state;
852
+ const hasSecretOptions = secretOptions.length > 0;
853
+ const credentialComplete = Boolean(
854
+ slackSaveResult && slackSaveResult.teamId === config.slackTeamId.trim() && slackSaveResult.appId === config.slackAppId.trim() && slackSaveResult.botUserId === config.slackBotUserId.trim() && slackSaveResult.eventsRequestUrl === config.slackEventsRequestUrl.trim() && slackSaveResult.botTokenSecretId === config.slackBotTokenSecretId.trim() && slackSaveResult.signingSecretId === config.slackSigningSecretId.trim() && (slackSaveResult.defaultChannel ?? "") === config.slackDefaultChannel.trim() && !validationExtra.slackSaveBusy
855
+ );
856
+ const hasLegacyCredentialRecovery = Boolean(config.slackLegacyCredentialStatus);
857
+ return /* @__PURE__ */ jsxs("fieldset", { style: fieldsetStyle, children: [
858
+ /* @__PURE__ */ jsx("legend", { style: legendStyle, children: "Slack App setup" }),
859
+ hasPersistedIdentity && /* @__PURE__ */ jsx(
860
+ SlackStatusPanel,
861
+ {
862
+ agentId: config.agentId,
863
+ label: config.label,
864
+ loading: slackStatusLoading,
865
+ status: slackStatus,
866
+ error: slackStatusError
867
+ }
868
+ ),
869
+ hasPersistedIdentity && config.slackLegacyCredentialStatus && /* @__PURE__ */ jsx(
870
+ LegacySlackCredentialRebindPanel,
871
+ {
872
+ status: config.slackLegacyCredentialStatus,
873
+ signingSecretRequired: config.slackLegacySigningSecretRequired === "true",
874
+ signingSecretReady: Boolean(config.slackSigningSecretId.trim()),
875
+ busy: legacySlackRebindBusy,
876
+ error: legacySlackRebindError,
877
+ result: legacySlackRebindResult,
878
+ onRebind: handleLegacySlackRebind
879
+ }
880
+ ),
881
+ !credentialComplete && !hasLegacyCredentialRecovery && /* @__PURE__ */ jsx("div", { style: validationNoticeStyle, children: "Create the Slack App manifest, install it, and paste back the team/app/bot IDs plus bot token and signing secret references, then save install metadata before this identity can be saved. Do not verify the Request URL in Slack yet. Paperclip cannot answer Slack's verification challenge until the signing secret reference has been saved." }),
882
+ !hasLegacyCredentialRecovery && /* @__PURE__ */ jsxs("div", { style: inlineNoticeStyle, children: [
883
+ /* @__PURE__ */ jsx("strong", { children: "Create a Slack App from a manifest." }),
884
+ ' Slack does not support a prefilled deep link for manifests, so Paperclip generates the manifest JSON below for you to copy, then opens the plain Slack "create app" page where you paste it in via "From an app manifest".'
885
+ ] }),
886
+ !hasLegacyCredentialRecovery && /* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
887
+ /* @__PURE__ */ jsxs("span", { children: [
888
+ "Events Request URL ",
889
+ /* @__PURE__ */ jsx("span", { style: requiredStyle, children: "*" })
890
+ ] }),
891
+ /* @__PURE__ */ jsx(
892
+ "input",
893
+ {
894
+ type: "url",
895
+ value: config.slackEventsRequestUrl,
896
+ onChange: (e) => updateField("slackEventsRequestUrl", e.target.value),
897
+ placeholder: "https://your-public-tunnel.example/events",
898
+ style: inputStyle,
899
+ disabled: slackManifestBusy || slackSaveBusy || Boolean(slackManifestFlow)
900
+ }
901
+ ),
902
+ /* @__PURE__ */ jsx("span", { style: hintStyle, children: "Public HTTPS URL for the local Slack Events adapter. It must use the exact /events path." })
903
+ ] }),
904
+ !hasPersistedIdentity && /* @__PURE__ */ jsx("div", { style: formActionsStyle, children: /* @__PURE__ */ jsx(
905
+ "button",
906
+ {
907
+ type: "button",
908
+ onClick: () => void handleCreateSlackAppManifest(),
909
+ disabled: slackManifestBusy || slackSaveBusy || slackResumeBusy || !config.agentId || !config.label || !config.slackEventsRequestUrl.trim(),
910
+ style: secondaryButtonStyle,
911
+ children: slackManifestBusy ? "Working..." : "Create Slack App manifest"
912
+ }
913
+ ) }),
914
+ !hasLegacyCredentialRecovery && !slackManifestFlow && /* @__PURE__ */ jsxs("div", { style: formActionsStyle, children: [
915
+ /* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
916
+ /* @__PURE__ */ jsx("span", { children: "Resume an existing manifest flow" }),
917
+ /* @__PURE__ */ jsx(
918
+ "input",
919
+ {
920
+ type: "text",
921
+ value: slackResumeStateInput,
922
+ onChange: (e) => setSlackResumeStateInput(e.target.value),
923
+ placeholder: "Paste the pc_... state token from a previous session",
924
+ style: inputStyle
925
+ }
926
+ ),
927
+ /* @__PURE__ */ jsx("span", { style: hintStyle, children: "Reloading settings or closing this editor loses the in-progress manifest flow from local state. If you saved the state token from when the manifest was created, paste it here to resume within its 30-minute server-side window instead of creating a new Slack App." })
928
+ ] }),
929
+ /* @__PURE__ */ jsx(
930
+ "button",
931
+ {
932
+ type: "button",
933
+ onClick: () => void handleResumeSlackAppManifestFlow(),
934
+ disabled: slackResumeBusy || slackManifestBusy || slackSaveBusy || !slackResumeStateInput.trim(),
935
+ style: secondaryButtonStyle,
936
+ children: slackResumeBusy ? "Restoring..." : "Restore flow"
937
+ }
938
+ )
939
+ ] }),
940
+ slackResumeError && /* @__PURE__ */ jsx("span", { style: errorStyle, children: slackResumeError }),
941
+ slackManifestFlow && /* @__PURE__ */ jsxs("div", { style: manifestPanelStyle, children: [
942
+ /* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
943
+ /* @__PURE__ */ jsx("span", { children: "Manifest JSON" }),
944
+ /* @__PURE__ */ jsx(
945
+ "textarea",
946
+ {
947
+ readOnly: true,
948
+ value: slackManifestFlow.manifest,
949
+ style: { ...textareaStyle, minHeight: 320, fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace" }
950
+ }
951
+ )
952
+ ] }),
953
+ /* @__PURE__ */ jsxs("div", { style: formActionsStyle, children: [
954
+ /* @__PURE__ */ jsx(
955
+ "button",
956
+ {
957
+ type: "button",
958
+ onClick: () => void copyTextToClipboard(slackManifestFlow.manifest).then(() => {
959
+ setSlackManifestCopied(true);
960
+ setSlackManifestError(null);
961
+ }).catch(() => {
962
+ setSlackManifestCopied(false);
963
+ setSlackManifestError("Could not copy the manifest JSON to the clipboard. Select the text above and copy it manually.");
964
+ }),
965
+ style: secondaryButtonStyle,
966
+ children: slackManifestCopied ? "Copied!" : "Copy manifest JSON"
967
+ }
968
+ ),
969
+ /* @__PURE__ */ jsx("a", { href: slackManifestFlow.createAppUrl, target: "_blank", rel: "noreferrer", style: linkStyle, children: 'Open Slack "Create an app" page' })
970
+ ] }),
971
+ /* @__PURE__ */ jsx("span", { style: hintStyle, children: 'On the Slack page, choose "From an app manifest," select the workspace, then paste the copied JSON. After Slack creates and you install the app, come back and paste the resulting IDs below. Slack may show the Request URL as unverified. Leave it unverified until you save the signing secret reference in Paperclip.' }),
972
+ /* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
973
+ /* @__PURE__ */ jsx("span", { children: "Flow state token" }),
974
+ /* @__PURE__ */ jsx("input", { type: "text", readOnly: true, value: slackManifestFlow.state, style: inputStyle }),
975
+ /* @__PURE__ */ jsx("span", { style: hintStyle, children: 'Save this token if you might reload settings or close this editor before finishing setup. Paste it into "Resume an existing manifest flow" above to restore this in-progress flow within its 30-minute window.' })
976
+ ] })
977
+ ] }),
978
+ slackManifestError && /* @__PURE__ */ jsx("span", { style: errorStyle, children: slackManifestError }),
979
+ /* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
980
+ /* @__PURE__ */ jsxs("span", { children: [
981
+ "Team ID ",
982
+ /* @__PURE__ */ jsx("span", { style: requiredStyle, children: "*" })
983
+ ] }),
984
+ /* @__PURE__ */ jsx(
985
+ "input",
986
+ {
987
+ type: "text",
988
+ value: config.slackTeamId,
989
+ onChange: (e) => updateField("slackTeamId", e.target.value),
990
+ placeholder: "e.g. T0123456789",
991
+ style: inputStyle,
992
+ disabled: slackSaveBusy
993
+ }
994
+ )
995
+ ] }),
996
+ /* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
997
+ /* @__PURE__ */ jsxs("span", { children: [
998
+ "App ID ",
999
+ /* @__PURE__ */ jsx("span", { style: requiredStyle, children: "*" })
1000
+ ] }),
1001
+ /* @__PURE__ */ jsx(
1002
+ "input",
1003
+ {
1004
+ type: "text",
1005
+ value: config.slackAppId,
1006
+ onChange: (e) => updateField("slackAppId", normalizeSlackAppIdInput(e.target.value)),
1007
+ placeholder: "A0123456789 or https://api.slack.com/apps/A0123456789/general",
1008
+ style: inputStyle,
1009
+ disabled: slackSaveBusy
1010
+ }
1011
+ ),
1012
+ /* @__PURE__ */ jsx("span", { style: hintStyle, children: "Paste the App ID or the Slack app settings URL. Paperclip extracts the App ID from the URL." })
1013
+ ] }),
1014
+ /* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
1015
+ /* @__PURE__ */ jsxs("span", { children: [
1016
+ "Bot User ID ",
1017
+ /* @__PURE__ */ jsx("span", { style: requiredStyle, children: "*" })
1018
+ ] }),
1019
+ /* @__PURE__ */ jsx(
1020
+ "input",
1021
+ {
1022
+ type: "text",
1023
+ value: config.slackBotUserId,
1024
+ onChange: (e) => updateField("slackBotUserId", e.target.value),
1025
+ placeholder: "e.g. U0123456789",
1026
+ style: inputStyle,
1027
+ disabled: slackSaveBusy
1028
+ }
1029
+ )
1030
+ ] }),
1031
+ !hasLegacyCredentialRecovery && /* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
1032
+ /* @__PURE__ */ jsxs("span", { children: [
1033
+ "Bot token Paperclip secret UUID ",
1034
+ /* @__PURE__ */ jsx("span", { style: requiredStyle, children: "*" })
1035
+ ] }),
1036
+ hasSecretOptions ? /* @__PURE__ */ jsxs(
1037
+ "select",
1038
+ {
1039
+ value: config.slackBotTokenSecretId,
1040
+ onChange: (e) => updateField("slackBotTokenSecretId", e.target.value),
1041
+ style: inputStyle,
1042
+ disabled: slackSaveBusy || slackDiscoveryBusy,
1043
+ children: [
1044
+ /* @__PURE__ */ jsx("option", { value: "", children: "No bot token secret reference" }),
1045
+ config.slackBotTokenSecretId && !secretOptions.some((secret) => secret.id === config.slackBotTokenSecretId) && /* @__PURE__ */ jsxs("option", { value: config.slackBotTokenSecretId, children: [
1046
+ config.slackBotTokenSecretId,
1047
+ " (saved)"
1048
+ ] }),
1049
+ secretOptions.map((secret) => /* @__PURE__ */ jsx("option", { value: secret.id, children: formatSecretOption(secret) }, secret.id))
1050
+ ]
1051
+ }
1052
+ ) : /* @__PURE__ */ jsx(
1053
+ "input",
1054
+ {
1055
+ type: "text",
1056
+ value: config.slackBotTokenSecretId,
1057
+ onChange: (e) => updateField("slackBotTokenSecretId", e.target.value),
1058
+ placeholder: "Company secret UUID containing the Slack bot token",
1059
+ style: inputStyle,
1060
+ disabled: slackSaveBusy || slackDiscoveryBusy
1061
+ }
1062
+ ),
1063
+ /* @__PURE__ */ jsxs("span", { style: hintStyle, children: [
1064
+ getSecretFieldHint({ companyId, secretsLoading, secretsError, hasSecretOptions }),
1065
+ " The bot token itself is never stored in this config; only the secret reference is."
1066
+ ] })
1067
+ ] }),
1068
+ !hasLegacyCredentialRecovery && /* @__PURE__ */ jsxs("div", { style: formActionsStyle, children: [
1069
+ /* @__PURE__ */ jsx(
1070
+ "button",
1071
+ {
1072
+ type: "button",
1073
+ onClick: () => void handleDiscoverSlackInstallMetadata(),
1074
+ disabled: slackSaveBusy || slackDiscoveryBusy || !config.slackBotTokenSecretId.trim(),
1075
+ style: secondaryButtonStyle,
1076
+ children: slackDiscoveryBusy ? "Detecting..." : "Detect Slack installation IDs"
1077
+ }
1078
+ ),
1079
+ /* @__PURE__ */ jsx("span", { style: hintStyle, children: "Uses the selected bot token secret to fill Team ID, App ID, and Bot User ID." })
1080
+ ] }),
1081
+ !hasLegacyCredentialRecovery && slackDiscoveryError && /* @__PURE__ */ jsx("span", { style: errorStyle, children: slackDiscoveryError }),
1082
+ (!hasLegacyCredentialRecovery || config.slackLegacySigningSecretRequired === "true") && /* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
1083
+ /* @__PURE__ */ jsxs("span", { children: [
1084
+ "Signing secret Paperclip secret UUID ",
1085
+ /* @__PURE__ */ jsx("span", { style: requiredStyle, children: "*" })
1086
+ ] }),
1087
+ hasSecretOptions ? /* @__PURE__ */ jsxs(
1088
+ "select",
1089
+ {
1090
+ value: config.slackSigningSecretId,
1091
+ onChange: (e) => updateField("slackSigningSecretId", e.target.value),
1092
+ style: inputStyle,
1093
+ disabled: slackSaveBusy,
1094
+ children: [
1095
+ /* @__PURE__ */ jsx("option", { value: "", children: "No signing secret reference" }),
1096
+ config.slackSigningSecretId && !secretOptions.some((secret) => secret.id === config.slackSigningSecretId) && /* @__PURE__ */ jsxs("option", { value: config.slackSigningSecretId, children: [
1097
+ config.slackSigningSecretId,
1098
+ " (saved)"
1099
+ ] }),
1100
+ secretOptions.map((secret) => /* @__PURE__ */ jsx("option", { value: secret.id, children: formatSecretOption(secret) }, secret.id))
1101
+ ]
1102
+ }
1103
+ ) : /* @__PURE__ */ jsx(
1104
+ "input",
1105
+ {
1106
+ type: "text",
1107
+ value: config.slackSigningSecretId,
1108
+ onChange: (e) => updateField("slackSigningSecretId", e.target.value),
1109
+ placeholder: "Company secret UUID containing the Slack signing secret",
1110
+ style: inputStyle,
1111
+ disabled: slackSaveBusy
1112
+ }
1113
+ ),
1114
+ /* @__PURE__ */ jsxs("span", { style: hintStyle, children: [
1115
+ getSigningSecretFieldHint({ companyId, secretsLoading, secretsError, hasSecretOptions }),
1116
+ " The signing secret itself is never stored in this config; only the secret reference is."
1117
+ ] })
1118
+ ] }),
1119
+ /* @__PURE__ */ jsxs("label", { style: fieldStyle, children: [
1120
+ /* @__PURE__ */ jsx("span", { children: "Default channel" }),
1121
+ /* @__PURE__ */ jsx(
1122
+ "input",
1123
+ {
1124
+ type: "text",
1125
+ value: config.slackDefaultChannel,
1126
+ onChange: (e) => updateField("slackDefaultChannel", e.target.value),
1127
+ placeholder: "e.g. C0123456789",
1128
+ style: inputStyle,
1129
+ disabled: slackSaveBusy
1130
+ }
1131
+ ),
1132
+ /* @__PURE__ */ jsx("span", { style: hintStyle, children: "Optional. Must be a Slack conversation ID starting with C, D, or G." })
1133
+ ] }),
1134
+ !hasLegacyCredentialRecovery && /* @__PURE__ */ jsx("div", { style: formActionsStyle, children: /* @__PURE__ */ jsx(
1135
+ "button",
1136
+ {
1137
+ type: "button",
1138
+ onClick: () => void handleSaveSlackInstallMetadata(),
1139
+ disabled: slackManifestBusy || slackSaveBusy || Boolean(slackCleanupPendingAgentId) || !slackManifestFlow || !config.slackTeamId.trim() || !config.slackAppId.trim() || !config.slackBotUserId.trim() || !config.slackBotTokenSecretId.trim() || !config.slackSigningSecretId.trim(),
1140
+ style: secondaryButtonStyle,
1141
+ children: slackSaveBusy ? "Saving..." : "Save Slack install metadata"
1142
+ }
1143
+ ) }),
1144
+ !hasLegacyCredentialRecovery && !slackManifestFlow && !slackCleanupPendingAgentId && !slackSaveResult && /* @__PURE__ */ jsx("span", { style: hintStyle, children: "Create the manifest above first; saving install metadata requires an active manifest flow state." }),
1145
+ slackCleanupPendingAgentId && /* @__PURE__ */ jsxs("div", { style: validationNoticeStyle, children: [
1146
+ /* @__PURE__ */ jsxs("div", { children: [
1147
+ "Slack install metadata was saved, but Paperclip could not remove the previous identity for",
1148
+ " ",
1149
+ /* @__PURE__ */ jsx("strong", { children: slackCleanupPendingAgentId }),
1150
+ ". The new identity is safe; retry cleanup below rather than saving again (the manifest flow used for this save has already been consumed)."
1151
+ ] }),
1152
+ /* @__PURE__ */ jsx("div", { style: formActionsStyle, children: /* @__PURE__ */ jsx(
1153
+ "button",
1154
+ {
1155
+ type: "button",
1156
+ onClick: () => void handleRetrySlackCleanup(),
1157
+ disabled: slackCleanupBusy,
1158
+ style: secondaryButtonStyle,
1159
+ children: slackCleanupBusy ? "Retrying..." : "Retry cleanup"
1160
+ }
1161
+ ) }),
1162
+ slackCleanupError && /* @__PURE__ */ jsx("span", { style: errorStyle, children: slackCleanupError })
1163
+ ] }),
1164
+ slackSaveResult && /* @__PURE__ */ jsxs("div", { style: successStyle, children: [
1165
+ "Slack install metadata saved for team ",
1166
+ slackSaveResult.teamId,
1167
+ ". Return to Slack's App Manifest page now, verify the Request URL, and save the manifest changes. If verification failed before this save, retry it now."
1168
+ ] }),
1169
+ hasPersistedIdentity && !hasLegacyCredentialRecovery && /* @__PURE__ */ jsxs("details", { children: [
1170
+ /* @__PURE__ */ jsx("summary", { children: "Reinstall the Slack App for this identity" }),
1171
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: "0.75rem", marginTop: "0.75rem" }, children: [
1172
+ /* @__PURE__ */ jsxs("div", { style: inlineNoticeStyle, children: [
1173
+ /* @__PURE__ */ jsx("strong", { children: "Reinstall from a manifest." }),
1174
+ " This re-runs Slack App setup for this identity - creating a new manifest flow, requiring you to reinstall the app on Slack and paste back the resulting IDs and bot token secret. It does not remove the currently saved install metadata unless you save over it."
1175
+ ] }),
1176
+ /* @__PURE__ */ jsx("div", { style: formActionsStyle, children: /* @__PURE__ */ jsx(
1177
+ "button",
1178
+ {
1179
+ type: "button",
1180
+ onClick: () => void handleReinstallSlackApp(),
1181
+ disabled: slackManifestBusy || slackSaveBusy || slackResumeBusy || !config.slackEventsRequestUrl.trim(),
1182
+ style: secondaryButtonStyle,
1183
+ children: slackManifestBusy ? "Working..." : "Reinstall"
1184
+ }
1185
+ ) })
1186
+ ] })
1187
+ ] })
1188
+ ] });
1189
+ }
1190
+ function LegacySlackCredentialRebindPanel(props) {
1191
+ if (props.status === "conflict") {
1192
+ return /* @__PURE__ */ jsxs("div", { style: validationNoticeStyle, children: [
1193
+ /* @__PURE__ */ jsx("strong", { children: "Released Slack credentials need operator review." }),
1194
+ " The existing company Slack binding conflicts with the public identity or released sidecar references. Paperclip will not overwrite it. Remove or correct the conflicting host binding, then reopen this identity and rebind; reinstall is not required."
1195
+ ] });
1196
+ }
1197
+ const cleanupPending = props.status === "cleanup-pending" || props.result?.status === "cleanup-pending";
1198
+ return /* @__PURE__ */ jsxs("div", { style: cleanupPending ? inlineNoticeStyle : validationNoticeStyle, children: [
1199
+ /* @__PURE__ */ jsxs("div", { children: [
1200
+ /* @__PURE__ */ jsx("strong", { children: cleanupPending ? "Legacy Slack sidecar cleanup pending." : "Upgrade action required." }),
1201
+ " ",
1202
+ cleanupPending ? "The company host binding is working. Retry to remove only the released Slack sidecar entry." : "Slack v0.1.7/v0.1.8 saved credential UUID references in the local sidecar. Rebind copies those references into this company's authorized Slack config without reading either secret value.",
1203
+ !cleanupPending && props.signingSecretRequired ? " This released entry has no signing-secret reference; select or enter its Paperclip company secret UUID below first." : ""
1204
+ ] }),
1205
+ /* @__PURE__ */ jsx("div", { style: formActionsStyle, children: /* @__PURE__ */ jsx(
1206
+ "button",
1207
+ {
1208
+ type: "button",
1209
+ onClick: () => void props.onRebind(),
1210
+ disabled: props.busy || props.signingSecretRequired && !props.signingSecretReady,
1211
+ style: secondaryButtonStyle,
1212
+ children: props.busy ? "Working..." : cleanupPending ? "Retry legacy cleanup" : "Rebind released credentials"
1213
+ }
1214
+ ) }),
1215
+ props.result?.status === "rebound" && /* @__PURE__ */ jsx("span", { style: successStyle, children: "Released Slack credentials rebound successfully. Close this editor; reinstall is not required." }),
1216
+ props.error && /* @__PURE__ */ jsx("span", { style: errorStyle, children: props.error })
1217
+ ] });
1218
+ }
1219
+ function SlackStatusPanel(props) {
1220
+ const { agentId, label, loading, status, error } = props;
1221
+ if (loading) {
1222
+ return /* @__PURE__ */ jsxs("div", { style: inlineNoticeStyle, children: [
1223
+ "Loading configured Slack identity metadata for ",
1224
+ label || agentId,
1225
+ "..."
1226
+ ] });
1227
+ }
1228
+ if (error) {
1229
+ return /* @__PURE__ */ jsxs("div", { style: validationNoticeStyle, children: [
1230
+ "Slack identity metadata unavailable for agent ",
1231
+ /* @__PURE__ */ jsx("strong", { children: label || agentId }),
1232
+ " (",
1233
+ error,
1234
+ ")."
1235
+ ] });
1236
+ }
1237
+ if (!status) {
1238
+ return null;
1239
+ }
1240
+ return /* @__PURE__ */ jsxs("div", { style: inlineNoticeStyle, children: [
1241
+ /* @__PURE__ */ jsx("strong", { children: "Configured Slack identity." }),
1242
+ " ",
1243
+ "Saved metadata for agent ",
1244
+ /* @__PURE__ */ jsx("strong", { children: label || agentId }),
1245
+ ": workspace ",
1246
+ status.teamId ?? "unknown",
1247
+ ", app ",
1248
+ status.appId ?? "unknown",
1249
+ ", bot user ",
1250
+ status.botUserId ?? "unknown",
1251
+ status.hasDefaultChannel ? ", default channel configured" : ", no default channel configured",
1252
+ ".",
1253
+ " ",
1254
+ "This reflects saved install metadata only - it does not verify the bot token is still valid; a revoked or stale token is not detected here."
1255
+ ] });
1256
+ }
1257
+ var slackSettingsUIAdapter = {
1258
+ providerId: SLACK_IDENTITY_PROVIDER_ID,
1259
+ useCredentialStep: useSlackCredentialStep,
1260
+ CredentialStep: SlackCredentialStep,
1261
+ getRemovalConfirmation(entry) {
1262
+ return `Delete agent identity mapping for ${entry.label}? This clears saved Slack install metadata; your Slack app and bot token are not deleted, only unlinked from this agent. To reconnect later, create a new Slack mapping with "Add identity".`;
1263
+ }
1264
+ };
1265
+
1266
+ // src/providers/github/settings-adapter-ui.tsx
1267
+ import { useEffect as useEffect3, useState as useState3 } from "react";
1268
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1269
+ var MANIFEST_DRAFT_STORAGE_PREFIX = "paperclip-agent-identities:github-app-manifest-draft:";
1270
+ function getManifestDraftStorageKey(state) {
1271
+ return MANIFEST_DRAFT_STORAGE_PREFIX + state;
1272
+ }
1273
+ function writeManifestDraftForm(state, formState) {
1274
+ try {
1275
+ window.sessionStorage.setItem(getManifestDraftStorageKey(state), JSON.stringify(formState));
1276
+ } catch {
1277
+ }
1278
+ }
1279
+ function readManifestDraftForm(state) {
1280
+ try {
1281
+ const raw = window.sessionStorage.getItem(getManifestDraftStorageKey(state));
1282
+ if (!raw) return null;
1283
+ return normalizeManifestDraftForm(JSON.parse(raw));
1284
+ } catch {
1285
+ return null;
1286
+ }
1287
+ }
1288
+ function deleteManifestDraftForm(state) {
1289
+ try {
1290
+ window.sessionStorage.removeItem(getManifestDraftStorageKey(state));
1291
+ } catch {
1292
+ }
1293
+ }
1294
+ function isRecord(value) {
1295
+ return typeof value === "object" && value !== null;
1296
+ }
1297
+ function readString(value) {
1298
+ return typeof value === "string" ? value : "";
1299
+ }
1300
+ function normalizeManifestDraftForm(raw) {
1301
+ if (!isRecord(raw)) return null;
1302
+ return {
1303
+ agentId: readString(raw.agentId),
1304
+ provider: readString(raw.provider) || GITHUB_IDENTITY_PROVIDER_ID,
1305
+ label: readString(raw.label),
1306
+ githubUsername: readString(raw.githubUsername),
1307
+ commitName: readString(raw.commitName),
1308
+ commitEmail: readString(raw.commitEmail),
1309
+ githubAppId: readString(raw.githubAppId),
1310
+ githubInstallationId: readString(raw.githubInstallationId),
1311
+ privateKeySecretId: readString(raw.privateKeySecretId),
1312
+ privateKeyFile: readString(raw.privateKeyFile),
1313
+ fallbackTokenSecretId: readString(raw.fallbackTokenSecretId),
1314
+ tokenFile: readString(raw.tokenFile),
1315
+ previousAgentId: readString(raw.previousAgentId),
1316
+ previousGithubAppId: readString(raw.previousGithubAppId),
1317
+ previousGithubInstallationId: readString(raw.previousGithubInstallationId),
1318
+ previousPrivateKeySecretId: readString(raw.previousPrivateKeySecretId),
1319
+ previousPrivateKeyFile: readString(raw.previousPrivateKeyFile)
1320
+ };
1321
+ }
1322
+ function submitGitHubAppManifest(flow) {
1323
+ const form = document.createElement("form");
1324
+ form.method = "post";
1325
+ form.action = flow.postUrl;
1326
+ form.target = "_blank";
1327
+ form.setAttribute("rel", "noopener noreferrer");
1328
+ const manifest = document.createElement("input");
1329
+ manifest.type = "hidden";
1330
+ manifest.name = "manifest";
1331
+ manifest.value = flow.manifest;
1332
+ form.appendChild(manifest);
1333
+ document.body.appendChild(form);
1334
+ form.submit();
1335
+ form.remove();
1336
+ }
1337
+ function getManifestReturnUrl() {
1338
+ const url = new URL(window.location.href);
1339
+ url.searchParams.delete("code");
1340
+ url.searchParams.delete("installation_id");
1341
+ url.searchParams.delete("setup_action");
1342
+ url.searchParams.delete("state");
1343
+ url.searchParams.set("githubAppManifest", "1");
1344
+ return url.toString();
1345
+ }
1346
+ function getAgentDashboardUrl(agentId) {
1347
+ const url = new URL(window.location.href);
1348
+ const companySegment = url.pathname.split("/").filter(Boolean)[0];
1349
+ const agentSegment = encodeURIComponent(agentId);
1350
+ url.pathname = companySegment ? `/${encodeURIComponent(companySegment)}/agents/${agentSegment}/dashboard` : `/agents/${agentSegment}/dashboard`;
1351
+ url.search = "";
1352
+ url.hash = "";
1353
+ return url.toString();
1354
+ }
1355
+ function getManifestCallbackParams() {
1356
+ const url = new URL(window.location.href);
1357
+ const code = url.searchParams.get("code")?.trim();
1358
+ const installationId = url.searchParams.get("installation_id")?.trim();
1359
+ const state = url.searchParams.get("state")?.trim();
1360
+ if (!state?.startsWith("pc_") || !code && !installationId) return null;
1361
+ return { ...code ? { code } : {}, ...installationId ? { installationId } : {}, state };
1362
+ }
1363
+ function cleanManifestCallbackParams() {
1364
+ const url = new URL(window.location.href);
1365
+ url.searchParams.delete("code");
1366
+ url.searchParams.delete("installation_id");
1367
+ url.searchParams.delete("setup_action");
1368
+ url.searchParams.delete("state");
1369
+ url.searchParams.delete("githubAppManifest");
1370
+ window.history.replaceState(window.history.state, document.title, url.toString());
1371
+ }
1372
+ function extractManifestCode(value) {
1373
+ const trimmed = value.trim();
1374
+ if (!trimmed) return "";
1375
+ try {
1376
+ const parsed = new URL(trimmed);
1377
+ return parsed.searchParams.get("code")?.trim() || trimmed;
1378
+ } catch {
1379
+ return trimmed.replace(/^code=/i, "").trim();
1380
+ }
1381
+ }
1382
+ function useGitHubCredentialStep(input) {
1383
+ const {
1384
+ config,
1385
+ updateField,
1386
+ createGitHubAppManifest,
1387
+ getGitHubAppManifestFlow,
1388
+ convertGitHubAppManifest,
1389
+ identities,
1390
+ agentOptions,
1391
+ companyDisplayName,
1392
+ credentialSidecarPath,
1393
+ getAgentIdentityDefaults: getAgentIdentityDefaults2,
1394
+ toFormState: toFormState2,
1395
+ patchFormState
1396
+ } = input;
1397
+ const [manifestFlow, setManifestFlow] = useState3(null);
1398
+ const [manifestBusy, setManifestBusy] = useState3(false);
1399
+ const [manifestCode, setManifestCode] = useState3("");
1400
+ const [manifestError, setManifestError] = useState3(null);
1401
+ const [manifestResult, setManifestResult] = useState3(null);
1402
+ function reset() {
1403
+ setManifestFlow(null);
1404
+ setManifestCode("");
1405
+ setManifestError(null);
1406
+ setManifestResult(null);
1407
+ }
1408
+ useEffect3(() => {
1409
+ const callback = getManifestCallbackParams();
1410
+ if (!callback) return;
1411
+ let cancelled = false;
1412
+ setManifestBusy(true);
1413
+ setManifestError(null);
1414
+ setManifestResult(null);
1415
+ void getGitHubAppManifestFlow({ state: callback.state }).then((result) => {
1416
+ if (cancelled) return;
1417
+ const flow = result;
1418
+ const savedIdentity = identities.find((entry) => entry.agentId === flow.agentId && entry.provider === flow.provider);
1419
+ const selectedAgent = agentOptions.find((agent) => agent.id === flow.agentId);
1420
+ const defaults = selectedAgent ? getAgentIdentityDefaults2(selectedAgent, companyDisplayName, credentialSidecarPath) : null;
1421
+ const restoredForm = toFormState2(savedIdentity);
1422
+ const draftForm = readManifestDraftForm(callback.state);
1423
+ const conversion = flow.conversion;
1424
+ patchFormState(() => ({
1425
+ ...restoredForm,
1426
+ ...draftForm,
1427
+ agentId: flow.agentId,
1428
+ provider: flow.provider,
1429
+ label: draftForm?.label || restoredForm.label || flow.label,
1430
+ githubUsername: conversion?.githubUsername || draftForm?.githubUsername || restoredForm.githubUsername || defaults?.githubUsername || DEFAULT_BOT_IDENTITY_CONFIG.github.username,
1431
+ commitName: draftForm?.commitName || restoredForm.commitName || defaults?.commitName || "",
1432
+ commitEmail: draftForm?.commitEmail || restoredForm.commitEmail || defaults?.commitEmail || "",
1433
+ githubAppId: conversion?.appId || draftForm?.githubAppId || restoredForm.githubAppId,
1434
+ githubInstallationId: callback.installationId || restoredForm.githubInstallationId,
1435
+ privateKeyFile: conversion?.privateKeyFile || draftForm?.privateKeyFile || restoredForm.privateKeyFile || defaults?.privateKeyFile || ""
1436
+ }));
1437
+ setManifestFlow(flow);
1438
+ setManifestCode(callback.code ?? "");
1439
+ if (callback.installationId) {
1440
+ setManifestResult(conversion ?? null);
1441
+ deleteManifestDraftForm(callback.state);
1442
+ void getGitHubAppManifestFlow({ state: callback.state, consume: true }).catch(() => void 0);
1443
+ }
1444
+ cleanManifestCallbackParams();
1445
+ }).catch((err) => {
1446
+ if (!cancelled) {
1447
+ setManifestError(err instanceof Error ? err.message : "Could not restore GitHub App manifest flow");
1448
+ }
1449
+ }).finally(() => {
1450
+ if (!cancelled) {
1451
+ setManifestBusy(false);
1452
+ }
1453
+ });
1454
+ return () => {
1455
+ cancelled = true;
1456
+ };
1457
+ }, [agentOptions, companyDisplayName, credentialSidecarPath, getGitHubAppManifestFlow, identities]);
1458
+ async function handleCreateGitHubAppManifest() {
1459
+ if (!config) return;
1460
+ if (config.provider !== GITHUB_IDENTITY_PROVIDER_ID) {
1461
+ setManifestError("GitHub App setup is only available for the GitHub provider.");
1462
+ return;
1463
+ }
1464
+ setManifestBusy(true);
1465
+ setManifestError(null);
1466
+ setManifestResult(null);
1467
+ try {
1468
+ const result = await createGitHubAppManifest({
1469
+ agentId: config.agentId.trim(),
1470
+ provider: config.provider,
1471
+ label: config.label.trim(),
1472
+ homepageUrl: getAgentDashboardUrl(config.agentId.trim()),
1473
+ callbackUrl: getManifestReturnUrl()
1474
+ });
1475
+ setManifestFlow(result);
1476
+ writeManifestDraftForm(result.state, config);
1477
+ submitGitHubAppManifest(result);
1478
+ } catch (err) {
1479
+ setManifestError(err instanceof Error ? err.message : "Failed to create GitHub App manifest");
1480
+ } finally {
1481
+ setManifestBusy(false);
1482
+ }
1483
+ }
1484
+ async function handleConvertGitHubAppManifest() {
1485
+ if (!manifestFlow || !config) return;
1486
+ setManifestBusy(true);
1487
+ setManifestError(null);
1488
+ try {
1489
+ const result = await convertGitHubAppManifest({
1490
+ state: manifestFlow.state,
1491
+ code: manifestCode.trim()
1492
+ });
1493
+ const nextFormState = {
1494
+ ...config,
1495
+ githubAppId: result.appId,
1496
+ privateKeyFile: result.privateKeyFile,
1497
+ githubUsername: result.githubUsername
1498
+ };
1499
+ writeManifestDraftForm(manifestFlow.state, nextFormState);
1500
+ setManifestResult(result);
1501
+ updateField("githubAppId", result.appId);
1502
+ updateField("privateKeyFile", result.privateKeyFile);
1503
+ updateField("githubUsername", result.githubUsername);
1504
+ window.location.assign(result.installUrl);
1505
+ } catch (err) {
1506
+ setManifestError(err instanceof Error ? err.message : "Failed to convert GitHub App manifest");
1507
+ } finally {
1508
+ setManifestBusy(false);
1509
+ }
1510
+ }
1511
+ return {
1512
+ validationExtra: {},
1513
+ reset,
1514
+ manifestFlow,
1515
+ manifestBusy,
1516
+ manifestCode,
1517
+ setManifestCode,
1518
+ manifestError,
1519
+ manifestResult,
1520
+ handleCreateGitHubAppManifest,
1521
+ handleConvertGitHubAppManifest,
1522
+ updateField,
1523
+ secretOptions: input.secretOptions,
1524
+ secretsLoading: input.secretsLoading,
1525
+ secretsError: input.secretsError,
1526
+ companyId: input.companyId
1527
+ };
1528
+ }
1529
+ function GitHubAppManifestCreateIntro() {
1530
+ return /* @__PURE__ */ jsxs2("div", { style: inlineNoticeStyle, children: [
1531
+ /* @__PURE__ */ jsx2("strong", { children: "Create a GitHub App with a manifest." }),
1532
+ " This opens GitHub with the required app permissions prefilled. After GitHub creates the app, Paperclip saves the generated private key file, preloads the App ID, opens the install flow, and restores the form with the Installation ID when GitHub redirects back."
1533
+ ] });
1534
+ }
1535
+ function GitHubAppManifestActions(props) {
1536
+ return /* @__PURE__ */ jsxs2("div", { style: formActionsStyle, children: [
1537
+ /* @__PURE__ */ jsx2(
1538
+ "button",
1539
+ {
1540
+ type: "button",
1541
+ onClick: props.onCreate,
1542
+ disabled: props.manifestBusy || props.disabled,
1543
+ style: secondaryButtonStyle,
1544
+ children: props.manifestBusy ? "Working..." : props.buttonLabel
1545
+ }
1546
+ ),
1547
+ props.manifestFlow && /* @__PURE__ */ jsxs2("span", { style: hintStyle, children: [
1548
+ "Manifest ready for ",
1549
+ props.manifestFlow.appName,
1550
+ ". GitHub should be open in a new tab."
1551
+ ] })
1552
+ ] });
1553
+ }
1554
+ function GitHubCredentialStep(props) {
1555
+ const { state, config } = props;
1556
+ const { manifestFlow, manifestBusy, manifestCode, setManifestCode, manifestError, manifestResult, handleCreateGitHubAppManifest, handleConvertGitHubAppManifest } = state;
1557
+ const hasExistingGitHubAppCredential = Boolean(
1558
+ config.previousGithubAppId || config.previousGithubInstallationId || config.previousPrivateKeySecretId || config.previousPrivateKeyFile
1559
+ );
1560
+ const hasSecretOptions = state.secretOptions.length > 0;
1561
+ const hasSavedSecretOutsideOptions = Boolean(
1562
+ config.privateKeySecretId && !state.secretOptions.some((secret) => secret.id === config.privateKeySecretId)
1563
+ );
1564
+ const hasSavedFallbackSecretOutsideOptions = Boolean(
1565
+ config.fallbackTokenSecretId && !state.secretOptions.some((secret) => secret.id === config.fallbackTokenSecretId)
1566
+ );
1567
+ return /* @__PURE__ */ jsxs2("fieldset", { style: fieldsetStyle, children: [
1568
+ /* @__PURE__ */ jsx2("legend", { style: legendStyle, children: "GitHub App credential source" }),
1569
+ hasExistingGitHubAppCredential ? /* @__PURE__ */ jsxs2("div", { style: inlineNoticeStyle, children: [
1570
+ /* @__PURE__ */ jsx2("strong", { children: "GitHub App already configured." }),
1571
+ " Edit the fields below to update the saved App ID, Installation ID, or private key source. Creating another manifest is a replacement/rotation flow; it creates a new GitHub App and does not update the existing app in GitHub."
1572
+ ] }) : /* @__PURE__ */ jsx2(GitHubAppManifestCreateIntro, {}),
1573
+ hasExistingGitHubAppCredential ? /* @__PURE__ */ jsxs2("details", { children: [
1574
+ /* @__PURE__ */ jsx2("summary", { children: "Replace this GitHub App with a new manifest-created app" }),
1575
+ /* @__PURE__ */ jsxs2("div", { style: { display: "grid", gap: "0.75rem", marginTop: "0.75rem" }, children: [
1576
+ /* @__PURE__ */ jsx2(GitHubAppManifestCreateIntro, {}),
1577
+ /* @__PURE__ */ jsx2(
1578
+ GitHubAppManifestActions,
1579
+ {
1580
+ manifestBusy,
1581
+ disabled: !config.agentId || !config.label,
1582
+ manifestFlow,
1583
+ onCreate: () => void handleCreateGitHubAppManifest(),
1584
+ buttonLabel: "Create replacement GitHub App on GitHub"
1585
+ }
1586
+ )
1587
+ ] })
1588
+ ] }) : /* @__PURE__ */ jsx2(
1589
+ GitHubAppManifestActions,
1590
+ {
1591
+ manifestBusy,
1592
+ disabled: !config.agentId || !config.label,
1593
+ manifestFlow,
1594
+ onCreate: () => void handleCreateGitHubAppManifest(),
1595
+ buttonLabel: "Create GitHub App on GitHub"
1596
+ }
1597
+ ),
1598
+ manifestFlow && /* @__PURE__ */ jsxs2("div", { style: manifestPanelStyle, children: [
1599
+ /* @__PURE__ */ jsxs2("div", { style: fieldStyle, children: [
1600
+ /* @__PURE__ */ jsx2("span", { style: hintStyle, children: "If a popup was blocked, use this manual form:" }),
1601
+ /* @__PURE__ */ jsxs2("form", { action: manifestFlow.postUrl, method: "post", target: "_blank", children: [
1602
+ /* @__PURE__ */ jsx2("input", { type: "hidden", name: "manifest", value: manifestFlow.manifest }),
1603
+ /* @__PURE__ */ jsx2("button", { type: "submit", style: secondaryButtonStyle, children: "Open GitHub manifest form" })
1604
+ ] })
1605
+ ] }),
1606
+ /* @__PURE__ */ jsxs2("label", { style: fieldStyle, children: [
1607
+ /* @__PURE__ */ jsx2("span", { children: "Callback code" }),
1608
+ /* @__PURE__ */ jsx2(
1609
+ "input",
1610
+ {
1611
+ type: "text",
1612
+ value: manifestCode,
1613
+ onChange: (e) => setManifestCode(extractManifestCode(e.target.value)),
1614
+ placeholder: "Paste GitHub's callback URL or just the code=... value",
1615
+ style: inputStyle
1616
+ }
1617
+ )
1618
+ ] }),
1619
+ /* @__PURE__ */ jsxs2("div", { style: formActionsStyle, children: [
1620
+ /* @__PURE__ */ jsx2(
1621
+ "button",
1622
+ {
1623
+ type: "button",
1624
+ onClick: () => void handleConvertGitHubAppManifest(),
1625
+ disabled: manifestBusy || !manifestCode.trim(),
1626
+ style: secondaryButtonStyle,
1627
+ children: "Save generated private key and prefill fields"
1628
+ }
1629
+ ),
1630
+ manifestResult && /* @__PURE__ */ jsx2("a", { href: manifestResult.installUrl, target: "_blank", rel: "noreferrer", style: linkStyle, children: "Install GitHub App" })
1631
+ ] })
1632
+ ] }),
1633
+ manifestError && /* @__PURE__ */ jsx2("span", { style: errorStyle, children: manifestError }),
1634
+ manifestResult && config.githubInstallationId ? /* @__PURE__ */ jsxs2("span", { style: successStyle, children: [
1635
+ "GitHub App ",
1636
+ manifestResult.appName,
1637
+ " installed. Review the prefilled Installation ID, then save this identity."
1638
+ ] }) : manifestResult ? /* @__PURE__ */ jsxs2("span", { style: successStyle, children: [
1639
+ "GitHub App ",
1640
+ manifestResult.appName,
1641
+ " created. Install it on GitHub; Paperclip will prefill the Installation ID when GitHub redirects back."
1642
+ ] }) : null,
1643
+ hasExistingGitHubAppCredential && /* @__PURE__ */ jsxs2("div", { style: formActionsStyle, children: [
1644
+ /* @__PURE__ */ jsx2("a", { href: "https://github.com/settings/apps", target: "_blank", rel: "noreferrer", style: linkStyle, children: "Manage GitHub Apps" }),
1645
+ /* @__PURE__ */ jsx2("a", { href: "https://github.com/settings/installations", target: "_blank", rel: "noreferrer", style: linkStyle, children: "Manage GitHub App installations" })
1646
+ ] }),
1647
+ /* @__PURE__ */ jsxs2("label", { style: fieldStyle, children: [
1648
+ /* @__PURE__ */ jsx2("span", { children: "GitHub App ID" }),
1649
+ /* @__PURE__ */ jsx2(
1650
+ "input",
1651
+ {
1652
+ type: "text",
1653
+ value: config.githubAppId,
1654
+ onChange: (e) => state.updateField("githubAppId", e.target.value),
1655
+ placeholder: "GitHub App ID",
1656
+ style: inputStyle
1657
+ }
1658
+ )
1659
+ ] }),
1660
+ /* @__PURE__ */ jsxs2("label", { style: fieldStyle, children: [
1661
+ /* @__PURE__ */ jsx2("span", { children: "Installation ID" }),
1662
+ /* @__PURE__ */ jsx2(
1663
+ "input",
1664
+ {
1665
+ type: "text",
1666
+ value: config.githubInstallationId,
1667
+ onChange: (e) => state.updateField("githubInstallationId", e.target.value),
1668
+ placeholder: "GitHub App installation ID",
1669
+ style: inputStyle
1670
+ }
1671
+ )
1672
+ ] }),
1673
+ /* @__PURE__ */ jsxs2("label", { style: fieldStyle, children: [
1674
+ /* @__PURE__ */ jsx2("span", { children: "Private key Paperclip secret UUID" }),
1675
+ hasSecretOptions ? /* @__PURE__ */ jsxs2(
1676
+ "select",
1677
+ {
1678
+ value: config.privateKeySecretId,
1679
+ onChange: (e) => state.updateField("privateKeySecretId", e.target.value),
1680
+ style: inputStyle,
1681
+ children: [
1682
+ /* @__PURE__ */ jsx2("option", { value: "", children: "No private key secret reference" }),
1683
+ hasSavedSecretOutsideOptions && /* @__PURE__ */ jsxs2("option", { value: config.privateKeySecretId, children: [
1684
+ config.privateKeySecretId,
1685
+ " (saved)"
1686
+ ] }),
1687
+ state.secretOptions.map((secret) => /* @__PURE__ */ jsx2("option", { value: secret.id, children: formatSecretOption2(secret) }, secret.id))
1688
+ ]
1689
+ }
1690
+ ) : /* @__PURE__ */ jsx2(
1691
+ "input",
1692
+ {
1693
+ type: "text",
1694
+ value: config.privateKeySecretId,
1695
+ onChange: (e) => state.updateField("privateKeySecretId", e.target.value),
1696
+ placeholder: "Company secret UUID containing the GitHub App private key",
1697
+ style: inputStyle
1698
+ }
1699
+ ),
1700
+ /* @__PURE__ */ jsx2("span", { style: hintStyle, children: getSecretFieldHint2({ companyId: state.companyId, secretsLoading: state.secretsLoading, secretsError: state.secretsError, hasSecretOptions }) })
1701
+ ] }),
1702
+ /* @__PURE__ */ jsxs2("label", { style: fieldStyle, children: [
1703
+ /* @__PURE__ */ jsx2("span", { children: "Private key file fallback" }),
1704
+ /* @__PURE__ */ jsx2(
1705
+ "input",
1706
+ {
1707
+ type: "text",
1708
+ value: config.privateKeyFile,
1709
+ onChange: (e) => state.updateField("privateKeyFile", e.target.value),
1710
+ placeholder: "<runtime-home>/.paperclip/agent-identities/github-apps/<agent>/private-key.pem",
1711
+ style: inputStyle
1712
+ }
1713
+ ),
1714
+ /* @__PURE__ */ jsx2("span", { style: hintStyle, children: "Used by plugin tools while a secret UUID is not configured or cannot be resolved. The plugin mints short-lived installation tokens from this private key; it does not store generated tokens." })
1715
+ ] }),
1716
+ /* @__PURE__ */ jsxs2("details", { children: [
1717
+ /* @__PURE__ */ jsx2("summary", { children: "Fallback token source" }),
1718
+ /* @__PURE__ */ jsxs2("div", { style: { display: "grid", gap: "0.75rem", marginTop: "0.75rem" }, children: [
1719
+ /* @__PURE__ */ jsxs2("label", { style: fieldStyle, children: [
1720
+ /* @__PURE__ */ jsx2("span", { children: "Fallback token secret UUID" }),
1721
+ hasSecretOptions ? /* @__PURE__ */ jsxs2(
1722
+ "select",
1723
+ {
1724
+ value: config.fallbackTokenSecretId,
1725
+ onChange: (e) => state.updateField("fallbackTokenSecretId", e.target.value),
1726
+ style: inputStyle,
1727
+ children: [
1728
+ /* @__PURE__ */ jsx2("option", { value: "", children: "No fallback token secret reference" }),
1729
+ hasSavedFallbackSecretOutsideOptions && /* @__PURE__ */ jsxs2("option", { value: config.fallbackTokenSecretId, children: [
1730
+ config.fallbackTokenSecretId,
1731
+ " (saved)"
1732
+ ] }),
1733
+ state.secretOptions.map((secret) => /* @__PURE__ */ jsx2("option", { value: secret.id, children: formatSecretOption2(secret) }, secret.id))
1734
+ ]
1735
+ }
1736
+ ) : /* @__PURE__ */ jsx2(
1737
+ "input",
1738
+ {
1739
+ type: "text",
1740
+ value: config.fallbackTokenSecretId,
1741
+ onChange: (e) => state.updateField("fallbackTokenSecretId", e.target.value),
1742
+ placeholder: "Company secret UUID containing a GitHub token",
1743
+ style: inputStyle
1744
+ }
1745
+ ),
1746
+ /* @__PURE__ */ jsx2("span", { style: hintStyle, children: getFallbackTokenSecretFieldHint({ companyId: state.companyId, secretsLoading: state.secretsLoading, secretsError: state.secretsError, hasSecretOptions }) })
1747
+ ] }),
1748
+ /* @__PURE__ */ jsxs2("label", { style: fieldStyle, children: [
1749
+ /* @__PURE__ */ jsx2("span", { children: "Fallback token file" }),
1750
+ /* @__PURE__ */ jsx2(
1751
+ "input",
1752
+ {
1753
+ type: "text",
1754
+ value: config.tokenFile,
1755
+ onChange: (e) => state.updateField("tokenFile", e.target.value),
1756
+ placeholder: "<runtime-home>/.paperclip/agent-identities/tokens/<agent-id>.token",
1757
+ style: inputStyle
1758
+ }
1759
+ ),
1760
+ /* @__PURE__ */ jsx2("span", { style: hintStyle, children: "Fallback token files are available for dev and recovery flows. Prefer GitHub App credentials above." })
1761
+ ] })
1762
+ ] })
1763
+ ] })
1764
+ ] });
1765
+ }
1766
+ var githubSettingsUIAdapter = {
1767
+ providerId: GITHUB_IDENTITY_PROVIDER_ID,
1768
+ useCredentialStep: useGitHubCredentialStep,
1769
+ CredentialStep: GitHubCredentialStep,
1770
+ getRemovalConfirmation(entry) {
1771
+ return `Delete agent identity mapping for ${entry.label}? This clears the saved GitHub App binding for this agent; the GitHub App itself and its installation are not deleted, only unlinked from this agent. To reconnect later, create a new GitHub mapping with "Add identity".`;
1772
+ }
1773
+ };
1774
+
1775
+ // src/providers/settings-ui-index.ts
1776
+ var ALL_SETTINGS_UI_ADAPTERS = [githubSettingsUIAdapter, slackSettingsUIAdapter];
1777
+ var providerSettingsUIRegistry = buildProviderSettingsUIRegistry([...ALL_SETTINGS_UI_ADAPTERS]);
1778
+
1779
+ // src/ui/SettingsPage.tsx
1780
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1781
+ function SettingsPage(props) {
1782
+ const companyId = props.context.companyId ?? "";
1783
+ const themeMode = usePaperclipThemeMode();
1784
+ const { data, loading, error, refresh } = usePluginData("bot-identity-config", { companyId });
1785
+ const companyDisplayName = getCompanyDisplayName(data?.companyName, props.context.companyPrefix, companyId);
1786
+ const { data: agentsData, loading: agentsLoading, error: agentsError } = usePluginData("paperclip-agents", { companyId });
1787
+ const saveConfig = usePluginAction("save-bot-identity-config");
1788
+ const deleteConfig = usePluginAction("delete-bot-identity-config");
1789
+ const createGitHubAppManifest = usePluginAction("create-github-app-manifest");
1790
+ const getGitHubAppManifestFlow = usePluginAction("get-github-app-manifest-flow");
1791
+ const convertGitHubAppManifest = usePluginAction("convert-github-app-manifest");
1792
+ const createSlackAppManifest = usePluginAction("create-slack-app-manifest");
1793
+ const getSlackAppManifestFlow = usePluginAction("get-slack-app-manifest-flow");
1794
+ const discoverSlackInstallMetadata = usePluginAction("discover-slack-install-metadata");
1795
+ const saveSlackInstallMetadata = usePluginAction("save-slack-install-metadata");
1796
+ const rebindLegacySlackCredentials = usePluginAction(REBIND_LEGACY_SLACK_CREDENTIALS_ACTION);
1797
+ const slackBotWhoami = usePluginAction(slackBotWhoamiToolName);
1798
+ const [formState, setFormState] = useState4(null);
1799
+ const [saving, setSaving] = useState4(false);
1800
+ const [deletingAgentId, setDeletingAgentId] = useState4(null);
1801
+ const [saveError, setSaveError] = useState4(null);
1802
+ const [saveSuccess, setSaveSuccess] = useState4(false);
1803
+ const [secretOptions, setSecretOptions] = useState4([]);
1804
+ const [secretsLoading, setSecretsLoading] = useState4(false);
1805
+ const [secretsError, setSecretsError] = useState4(null);
1806
+ const [activeFormSection, setActiveFormSection] = useState4("identity");
1807
+ const identities = data?.identities ?? [];
1808
+ const agentOptions = agentsData?.agents ?? [];
1809
+ const summary = summarizeIdentitySettings(identities);
1810
+ useEffect4(() => {
1811
+ if (!companyId) {
1812
+ setSecretOptions([]);
1813
+ setSecretsError(null);
1814
+ setSecretsLoading(false);
1815
+ return;
1816
+ }
1817
+ let cancelled = false;
1818
+ setSecretsLoading(true);
1819
+ setSecretsError(null);
1820
+ void loadSecretOptions(companyId).then((secrets) => {
1821
+ if (!cancelled) {
1822
+ setSecretOptions(secrets);
1823
+ }
1824
+ }).catch((err) => {
1825
+ if (!cancelled) {
1826
+ setSecretOptions([]);
1827
+ setSecretsError(err instanceof Error ? err.message : "Could not load Paperclip secrets");
1828
+ }
1829
+ }).finally(() => {
1830
+ if (!cancelled) {
1831
+ setSecretsLoading(false);
1832
+ }
1833
+ });
1834
+ return () => {
1835
+ cancelled = true;
1836
+ };
1837
+ }, [companyId]);
1838
+ const hasAgentOptions = agentOptions.length > 0;
1839
+ const config = formState;
1840
+ const persistedAgentId = config?.previousAgentId || config?.agentId;
1841
+ const isEditingExistingIdentity = Boolean(
1842
+ config && identities.some(
1843
+ (entry) => entry.agentId === persistedAgentId && entry.provider === config.provider
1844
+ )
1845
+ );
1846
+ const slackUIState = providerSettingsUIRegistry.get(SLACK_IDENTITY_PROVIDER_ID).useCredentialStep({
1847
+ config,
1848
+ hasPersistedIdentity: isEditingExistingIdentity,
1849
+ updateField: (field, value) => updateField(field, value),
1850
+ refresh,
1851
+ deleteConfig,
1852
+ patchFormState: (patch) => setFormState((prev) => prev ? patch(prev) : prev),
1853
+ secretOptions,
1854
+ secretsLoading,
1855
+ secretsError,
1856
+ companyId,
1857
+ createSlackAppManifest,
1858
+ getSlackAppManifestFlow,
1859
+ discoverSlackInstallMetadata,
1860
+ saveSlackInstallMetadata,
1861
+ rebindLegacySlackCredentials,
1862
+ slackBotWhoami
1863
+ });
1864
+ const githubUIState = providerSettingsUIRegistry.get(GITHUB_IDENTITY_PROVIDER_ID).useCredentialStep({
1865
+ config,
1866
+ hasPersistedIdentity: isEditingExistingIdentity,
1867
+ updateField: (field, value) => updateField(field, value),
1868
+ refresh,
1869
+ deleteConfig,
1870
+ // Unlike Slack's patchFormState (which only ever patches an already-open
1871
+ // dialog), the GitHub OAuth callback effect can fire with the dialog
1872
+ // closed (formState === null) -- the operator left the page entirely to
1873
+ // authorize the GitHub App and is now landing back on a fresh page load.
1874
+ // Falling back to a no-op there (as a plain `prev ? patch(prev) : prev`
1875
+ // would) silently drops the restored manifest flow and the wizard never
1876
+ // reopens. Construct a fresh base via `toFormState()` -- the same
1877
+ // construction `startCreate` uses to open the dialog -- so the patch
1878
+ // always has a form to apply to and the wizard reopens with the restored
1879
+ // state.
1880
+ patchFormState: (patch) => setFormState((prev) => patch(prev ?? toFormState())),
1881
+ secretOptions,
1882
+ secretsLoading,
1883
+ secretsError,
1884
+ companyId,
1885
+ createGitHubAppManifest,
1886
+ getGitHubAppManifestFlow,
1887
+ convertGitHubAppManifest,
1888
+ identities,
1889
+ agentOptions,
1890
+ companyDisplayName,
1891
+ credentialSidecarPath: data?.credentialSidecarPath ?? "",
1892
+ getAgentIdentityDefaults,
1893
+ toFormState
1894
+ });
1895
+ const activeProviderId = config?.provider ?? GITHUB_IDENTITY_PROVIDER_ID;
1896
+ const activeProviderUIState = activeProviderId === SLACK_IDENTITY_PROVIDER_ID ? slackUIState : githubUIState;
1897
+ const activeProviderUIAdapter = providerSettingsUIRegistry.get(activeProviderId);
1898
+ const ActiveCredentialStep = activeProviderUIAdapter?.CredentialStep;
1899
+ const activeProviderCredentialStepId = providerSettingsRegistry.get(activeProviderId).credentialStepId;
1900
+ const hasSavedAgentOutsideOptions = Boolean(
1901
+ config?.agentId && !agentOptions.some((agent) => agent.id === config.agentId)
1902
+ );
1903
+ const duplicateIdentity = config ? identities.find((entry) => entry.agentId === config.agentId && entry.provider === config.provider && !(config.previousAgentId === entry.agentId && config.provider === entry.provider)) : void 0;
1904
+ const formValidation = config ? getIdentityFormValidation(config, Boolean(duplicateIdentity), slackUIState.slackSaveResult, slackUIState.slackSaveBusy) : null;
1905
+ const activeFormSteps = getFormSteps(config?.provider ?? GITHUB_IDENTITY_PROVIDER_ID);
1906
+ const activeFormStepIndex = getFormStepIndex(activeFormSection, config?.provider ?? GITHUB_IDENTITY_PROVIDER_ID);
1907
+ const isLastFormStep = activeFormStepIndex === activeFormSteps.length - 1;
1908
+ const canGoNext = formValidation ? canAdvanceFromStep(activeFormSection, formValidation) : false;
1909
+ const canSave = Boolean(formValidation?.isComplete && isLastFormStep && !saving);
1910
+ const slackSaveInFlight = activeProviderId === SLACK_IDENTITY_PROVIDER_ID && (slackUIState.slackSaveBusy || slackUIState.slackCleanupBusy || slackUIState.legacySlackRebindBusy);
1911
+ if (loading) return /* @__PURE__ */ jsx3("div", { children: "Loading settings..." });
1912
+ if (error) return /* @__PURE__ */ jsxs3("div", { children: [
1913
+ "Error loading settings: ",
1914
+ error.message
1915
+ ] });
1916
+ function startCreate() {
1917
+ setActiveFormSection("identity");
1918
+ setFormState(toFormState());
1919
+ setSaveError(null);
1920
+ setSaveSuccess(false);
1921
+ githubUIState.reset();
1922
+ slackUIState.reset();
1923
+ }
1924
+ function startEdit(entry) {
1925
+ setActiveFormSection("identity");
1926
+ setFormState(toFormState(entry));
1927
+ setSaveError(null);
1928
+ setSaveSuccess(false);
1929
+ githubUIState.reset();
1930
+ slackUIState.reset();
1931
+ }
1932
+ function updateField(field, value) {
1933
+ if (field === "agentId") {
1934
+ updateAgentSelection(value);
1935
+ } else {
1936
+ setFormState((prev) => ({
1937
+ ...prev ?? toFormState(),
1938
+ [field]: value
1939
+ }));
1940
+ }
1941
+ setSaveSuccess(false);
1942
+ setSaveError(null);
1943
+ if ((field === "agentId" || field === "label") && config?.[field] !== value) {
1944
+ githubUIState.reset();
1945
+ slackUIState.reset();
1946
+ }
1947
+ }
1948
+ function updateAgentSelection(agentId) {
1949
+ setFormState((prev) => {
1950
+ const base = prev ?? toFormState();
1951
+ const selectedAgent = agentOptions.find((agent) => agent.id === agentId);
1952
+ if (!selectedAgent || base.previousAgentId || base.agentId === agentId) {
1953
+ return { ...base, agentId };
1954
+ }
1955
+ const defaults = getAgentIdentityDefaults(
1956
+ selectedAgent,
1957
+ companyDisplayName,
1958
+ data?.credentialSidecarPath ?? ""
1959
+ );
1960
+ return {
1961
+ ...base,
1962
+ agentId,
1963
+ label: shouldPrefillIdentityField(base.label, DEFAULT_BOT_IDENTITY_CONFIG.label) ? defaults.label : base.label,
1964
+ githubUsername: shouldPrefillIdentityField(base.githubUsername, DEFAULT_BOT_IDENTITY_CONFIG.github.username) ? defaults.githubUsername : base.githubUsername,
1965
+ commitName: shouldPrefillIdentityField(base.commitName, "") ? defaults.commitName : base.commitName,
1966
+ commitEmail: shouldPrefillIdentityField(base.commitEmail, "") ? defaults.commitEmail : base.commitEmail,
1967
+ privateKeyFile: shouldPrefillIdentityField(base.privateKeyFile, "") ? defaults.privateKeyFile : base.privateKeyFile
1968
+ };
1969
+ });
1970
+ }
1971
+ async function handleSave() {
1972
+ if (!config) return;
1973
+ const validation = getIdentityFormValidation(config, Boolean(duplicateIdentity), slackUIState.slackSaveResult, slackUIState.slackSaveBusy);
1974
+ if (!validation.isComplete) {
1975
+ setSaveSuccess(false);
1976
+ setSaveError(validation.saveMessage);
1977
+ const adapter = providerSettingsRegistry.get(config.provider);
1978
+ setActiveFormSection(validation.identityComplete ? adapter.credentialStepId : "identity");
1979
+ return;
1980
+ }
1981
+ if (providerSettingsRegistry.get(config.provider).savesViaSeparateAction) {
1982
+ setSaveSuccess(true);
1983
+ setFormState(null);
1984
+ return;
1985
+ }
1986
+ setSaving(true);
1987
+ setSaveError(null);
1988
+ setSaveSuccess(false);
1989
+ try {
1990
+ if (!isIdentityProviderId(config.provider)) {
1991
+ throw new Error("Choose a supported identity provider before saving.");
1992
+ }
1993
+ const targetAgentId = config.agentId.trim();
1994
+ const previousAgentIds = config.previousAgentId && config.previousAgentId !== targetAgentId ? [config.previousAgentId] : [];
1995
+ const payload = {
1996
+ agentId: config.agentId.trim(),
1997
+ previousAgentId: config.previousAgentId.trim() || void 0,
1998
+ provider: "github",
1999
+ label: config.label.trim(),
2000
+ github: {
2001
+ username: config.githubUsername.trim(),
2002
+ ...config.commitName.trim() ? { commitName: config.commitName.trim() } : {},
2003
+ ...config.commitEmail.trim() ? { commitEmail: config.commitEmail.trim() } : {}
2004
+ },
2005
+ credential: {
2006
+ secretId: config.fallbackTokenSecretId.trim() || void 0,
2007
+ tokenFile: config.tokenFile.trim() || void 0,
2008
+ githubApp: {
2009
+ appId: config.githubAppId.trim() || void 0,
2010
+ installationId: config.githubInstallationId.trim() || void 0,
2011
+ privateKeySecretId: config.privateKeySecretId.trim() || void 0,
2012
+ privateKeyFile: config.privateKeyFile.trim() || void 0
2013
+ }
2014
+ }
2015
+ };
2016
+ const saved = await saveConfig(payload);
2017
+ await syncGitHubAppCredentialPropagationForAgents({
2018
+ selectedAgentIds: targetAgentId ? [targetAgentId] : [],
2019
+ previousAgentIds,
2020
+ githubAppId: config.githubAppId.trim() || void 0,
2021
+ githubInstallationId: config.githubInstallationId.trim() || void 0,
2022
+ privateKeySecretRef: config.privateKeySecretId.trim() || void 0,
2023
+ privateKeyFile: config.privateKeyFile.trim() || void 0,
2024
+ previousGithubAppId: config.previousGithubAppId || void 0,
2025
+ previousGithubInstallationId: config.previousGithubInstallationId || void 0,
2026
+ previousPrivateKeySecretRef: config.previousPrivateKeySecretId || void 0,
2027
+ previousPrivateKeyFile: config.previousPrivateKeyFile || void 0
2028
+ });
2029
+ setSaveSuccess(true);
2030
+ await refresh();
2031
+ const savedEntry = saved?.agentId ? saved : null;
2032
+ setFormState(savedEntry ? toFormState(savedEntry) : null);
2033
+ } catch (err) {
2034
+ setSaveError(err instanceof Error ? err.message : "Failed to save");
2035
+ } finally {
2036
+ setSaving(false);
2037
+ }
2038
+ }
2039
+ async function handleDelete(entry) {
2040
+ const confirmationMessage = providerSettingsUIRegistry.get(entry.provider)?.getRemovalConfirmation?.(entry) ?? `Delete agent identity mapping for ${entry.label}?`;
2041
+ const confirmed = window.confirm(confirmationMessage);
2042
+ if (!confirmed) return;
2043
+ setDeletingAgentId(entry.agentId);
2044
+ setSaveError(null);
2045
+ setSaveSuccess(false);
2046
+ try {
2047
+ await deleteConfig({ agentId: entry.agentId, provider: entry.provider });
2048
+ if (formState?.agentId === entry.agentId && formState.provider === entry.provider) {
2049
+ setFormState(null);
2050
+ }
2051
+ await refresh();
2052
+ if (hasGitHubAppPropagationValues(entry)) {
2053
+ try {
2054
+ await syncGitHubAppCredentialPropagationForAgents({
2055
+ selectedAgentIds: [],
2056
+ previousAgentIds: [entry.agentId],
2057
+ previousGithubAppId: entry.credential?.githubApp?.appId,
2058
+ previousGithubInstallationId: entry.credential?.githubApp?.installationId,
2059
+ previousPrivateKeySecretRef: entry.credential?.githubApp?.privateKeySecretId,
2060
+ previousPrivateKeyFile: entry.credential?.githubApp?.privateKeyFile
2061
+ });
2062
+ } catch (err) {
2063
+ setSaveError(`Deleted identity, but could not clean the agent environment: ${err instanceof Error ? err.message : "unknown error"}`);
2064
+ }
2065
+ }
2066
+ } catch (err) {
2067
+ setSaveError(err instanceof Error ? err.message : "Failed to delete");
2068
+ } finally {
2069
+ setDeletingAgentId(null);
2070
+ }
2071
+ }
2072
+ return /* @__PURE__ */ jsxs3(
2073
+ "div",
2074
+ {
2075
+ className: "agent-identities-settings",
2076
+ style: { ...createPaperclipThemeStyle(themeMode), ...pageStyle },
2077
+ "data-agent-identities-theme": themeMode,
2078
+ children: [
2079
+ /* @__PURE__ */ jsx3("style", { children: responsiveSettingsStyle }),
2080
+ /* @__PURE__ */ jsx3("div", { className: "agent-identities-header", style: headerStyle, children: /* @__PURE__ */ jsxs3("div", { children: [
2081
+ /* @__PURE__ */ jsx3("h2", { style: pageTitleStyle, children: "Agent Identities" }),
2082
+ /* @__PURE__ */ jsxs3("p", { style: descriptionStyle, children: [
2083
+ "Connect agents in ",
2084
+ companyDisplayName || "this company",
2085
+ " to GitHub and Slack identities."
2086
+ ] })
2087
+ ] }) }),
2088
+ /* @__PURE__ */ jsxs3("div", { className: "agent-identities-summary-grid", style: summaryGridStyle, children: [
2089
+ /* @__PURE__ */ jsx3(SummaryTile, { label: "Identities", value: summary.total }),
2090
+ /* @__PURE__ */ jsx3(SummaryTile, { label: "Ready", value: summary.ready, tone: "good" }),
2091
+ /* @__PURE__ */ jsx3(SummaryTile, { label: "Need setup", value: summary.needsSetup, tone: summary.needsSetup > 0 ? "warn" : "good" })
2092
+ ] }),
2093
+ data?.credentialSidecarError && /* @__PURE__ */ jsxs3("div", { style: credentialErrorStyle, children: [
2094
+ "Credential source unavailable: ",
2095
+ data.credentialSidecarError
2096
+ ] }),
2097
+ /* @__PURE__ */ jsxs3("section", { style: sectionStyle, children: [
2098
+ /* @__PURE__ */ jsxs3("div", { className: "agent-identities-section-header", style: sectionHeaderStyle, children: [
2099
+ /* @__PURE__ */ jsxs3("div", { children: [
2100
+ /* @__PURE__ */ jsx3("h3", { style: sectionTitleStyle, children: "Configured identities" }),
2101
+ /* @__PURE__ */ jsx3("p", { style: sectionDescriptionStyle, children: "Each row maps one Paperclip agent to a provider account and credential source." })
2102
+ ] }),
2103
+ /* @__PURE__ */ jsx3("button", { onClick: startCreate, style: secondaryButtonStyle, children: "Add identity" })
2104
+ ] }),
2105
+ saveError && !formState && /* @__PURE__ */ jsx3("div", { style: errorStyle, children: saveError }),
2106
+ saveSuccess && !formState && /* @__PURE__ */ jsx3("div", { style: successStyle, children: "Saved successfully." }),
2107
+ identities.length === 0 ? /* @__PURE__ */ jsxs3("div", { style: emptyStateStyle, children: [
2108
+ /* @__PURE__ */ jsx3("strong", { children: "No identities configured" }),
2109
+ /* @__PURE__ */ jsx3("span", { children: "Pick an agent, connect its first provider account, then save. Defaults are filled from the selected agent and company." }),
2110
+ /* @__PURE__ */ jsx3("button", { onClick: startCreate, style: primaryButtonStyle, children: "Create first identity" })
2111
+ ] }) : /* @__PURE__ */ jsxs3("div", { style: listStyle, children: [
2112
+ /* @__PURE__ */ jsxs3("div", { className: "agent-identities-list-header", style: listHeaderStyle, children: [
2113
+ /* @__PURE__ */ jsx3("span", { children: "Agent" }),
2114
+ /* @__PURE__ */ jsx3("span", { children: "Provider" }),
2115
+ /* @__PURE__ */ jsx3("span", { children: "Status" }),
2116
+ /* @__PURE__ */ jsx3("span", {})
2117
+ ] }),
2118
+ identities.map((entry) => /* @__PURE__ */ jsxs3("div", { className: "agent-identities-list-row", style: rowStyle, children: [
2119
+ /* @__PURE__ */ jsxs3("div", { style: { minWidth: 0 }, children: [
2120
+ /* @__PURE__ */ jsx3("div", { style: rowTitleStyle, children: entry.label }),
2121
+ /* @__PURE__ */ jsx3("div", { style: rowMetaStyle, children: formatAgentName(entry.agentId, agentOptions) })
2122
+ ] }),
2123
+ /* @__PURE__ */ jsx3("div", { style: rowMetaStyle, children: getProviderDisplayName(entry.provider, data?.providers) }),
2124
+ /* @__PURE__ */ jsx3("span", { style: statusBadgeStyle(getIdentityTone(entry)), children: formatCredentialStatus(entry.credentialStatus) }),
2125
+ /* @__PURE__ */ jsxs3("div", { className: "agent-identities-row-actions", style: rowActionsStyle, children: [
2126
+ /* @__PURE__ */ jsx3("button", { onClick: () => startEdit(entry), style: secondaryButtonStyle, children: "Edit" }),
2127
+ /* @__PURE__ */ jsx3(
2128
+ "button",
2129
+ {
2130
+ onClick: () => void handleDelete(entry),
2131
+ disabled: deletingAgentId === entry.agentId,
2132
+ style: dangerButtonStyle,
2133
+ children: deletingAgentId === entry.agentId ? "Deleting..." : "Delete"
2134
+ }
2135
+ )
2136
+ ] })
2137
+ ] }, entry.id))
2138
+ ] })
2139
+ ] }),
2140
+ config && /* @__PURE__ */ jsx3("div", { style: dialogBackdropStyle, role: "presentation", children: /* @__PURE__ */ jsxs3(
2141
+ "section",
2142
+ {
2143
+ className: "agent-identities-dialog",
2144
+ style: dialogStyle,
2145
+ role: "dialog",
2146
+ "aria-modal": "true",
2147
+ "aria-labelledby": "agent-identity-dialog-title",
2148
+ children: [
2149
+ /* @__PURE__ */ jsxs3("header", { className: "agent-identities-dialog-header", style: dialogHeaderStyle, children: [
2150
+ /* @__PURE__ */ jsxs3("div", { children: [
2151
+ /* @__PURE__ */ jsx3("h3", { id: "agent-identity-dialog-title", style: dialogTitleStyle, children: isEditingExistingIdentity ? "Edit agent identity" : "Add agent identity" }),
2152
+ /* @__PURE__ */ jsx3("p", { style: sectionDescriptionStyle, children: "Configure the agent and its provider-specific credentials." })
2153
+ ] }),
2154
+ /* @__PURE__ */ jsx3("button", { onClick: () => setFormState(null), disabled: saving || slackSaveInFlight, style: closeButtonStyle, "aria-label": "Close identity editor", children: "x" })
2155
+ ] }),
2156
+ /* @__PURE__ */ jsxs3("div", { style: dialogBodyStyle, children: [
2157
+ /* @__PURE__ */ jsx3("div", { className: "agent-identities-wizard-steps", "aria-label": "Identity setup progress", style: wizardStepListStyle, children: activeFormSteps.map((step, index) => /* @__PURE__ */ jsx3(
2158
+ WizardStepIndicator,
2159
+ {
2160
+ index: index + 1,
2161
+ label: step.label,
2162
+ active: step.id === activeFormSection,
2163
+ complete: formValidation ? isWizardStepComplete(step.id, formValidation) : false
2164
+ },
2165
+ step.id
2166
+ )) }),
2167
+ activeFormSection === "identity" && /* @__PURE__ */ jsxs3(Fragment, { children: [
2168
+ /* @__PURE__ */ jsxs3("fieldset", { style: fieldsetStyle, children: [
2169
+ /* @__PURE__ */ jsx3("legend", { style: legendStyle, children: "Identity" }),
2170
+ formValidation && !formValidation.identityComplete && /* @__PURE__ */ jsx3("div", { style: validationNoticeStyle, children: formValidation.identityMessage }),
2171
+ /* @__PURE__ */ jsxs3("label", { style: fieldStyle, children: [
2172
+ /* @__PURE__ */ jsxs3("span", { children: [
2173
+ "Agent ",
2174
+ /* @__PURE__ */ jsx3("span", { style: requiredStyle, children: "*" })
2175
+ ] }),
2176
+ hasAgentOptions ? /* @__PURE__ */ jsxs3(
2177
+ "select",
2178
+ {
2179
+ value: config.agentId,
2180
+ onChange: (e) => updateField("agentId", e.target.value),
2181
+ style: inputStyle,
2182
+ disabled: slackSaveInFlight,
2183
+ children: [
2184
+ /* @__PURE__ */ jsx3("option", { value: "", disabled: true, children: "Select a Paperclip agent" }),
2185
+ hasSavedAgentOutsideOptions && /* @__PURE__ */ jsxs3("option", { value: config.agentId, children: [
2186
+ config.agentId,
2187
+ " (saved)"
2188
+ ] }),
2189
+ agentOptions.map((agent) => /* @__PURE__ */ jsx3("option", { value: agent.id, children: formatAgentOption(agent) }, agent.id))
2190
+ ]
2191
+ }
2192
+ ) : /* @__PURE__ */ jsx3(
2193
+ "input",
2194
+ {
2195
+ type: "text",
2196
+ value: config.agentId,
2197
+ onChange: (e) => updateField("agentId", e.target.value),
2198
+ placeholder: "UUID of the Paperclip agent",
2199
+ style: inputStyle,
2200
+ disabled: slackSaveInFlight
2201
+ }
2202
+ ),
2203
+ /* @__PURE__ */ jsx3("span", { style: hintStyle, children: getAgentFieldHint({ companyId, agentsLoading, agentsError, hasAgentOptions }) })
2204
+ ] }),
2205
+ /* @__PURE__ */ jsxs3("label", { style: fieldStyle, children: [
2206
+ /* @__PURE__ */ jsxs3("span", { children: [
2207
+ "Provider ",
2208
+ /* @__PURE__ */ jsx3("span", { style: requiredStyle, children: "*" })
2209
+ ] }),
2210
+ /* @__PURE__ */ jsx3("select", { value: config.provider, onChange: (e) => updateField("provider", e.target.value), style: inputStyle, disabled: isEditingExistingIdentity || slackSaveInFlight, children: (data?.providers ?? []).map((provider) => /* @__PURE__ */ jsxs3(
2211
+ "option",
2212
+ {
2213
+ value: provider.id,
2214
+ disabled: provider.status !== "enabled" && provider.id !== SLACK_IDENTITY_PROVIDER_ID,
2215
+ children: [
2216
+ provider.name,
2217
+ provider.status === "coming-soon" ? provider.id === SLACK_IDENTITY_PROVIDER_ID ? "" : " (coming soon)" : ""
2218
+ ]
2219
+ },
2220
+ provider.id
2221
+ )) }),
2222
+ /* @__PURE__ */ jsx3("span", { style: hintStyle, children: "Each agent can have one identity per provider. GitHub and Slack are available, with additional providers coming soon." })
2223
+ ] }),
2224
+ duplicateIdentity && /* @__PURE__ */ jsxs3("div", { style: validationNoticeStyle, children: [
2225
+ "This agent already has a ",
2226
+ getProviderDisplayName(config.provider, data?.providers),
2227
+ " identity. Edit the existing row instead."
2228
+ ] }),
2229
+ /* @__PURE__ */ jsxs3("label", { style: fieldStyle, children: [
2230
+ /* @__PURE__ */ jsxs3("span", { children: [
2231
+ "Label ",
2232
+ /* @__PURE__ */ jsx3("span", { style: requiredStyle, children: "*" })
2233
+ ] }),
2234
+ /* @__PURE__ */ jsx3(
2235
+ "input",
2236
+ {
2237
+ type: "text",
2238
+ value: config.label,
2239
+ onChange: (e) => updateField("label", e.target.value),
2240
+ placeholder: "e.g. Cade Riven [Droidshop]",
2241
+ style: inputStyle,
2242
+ disabled: slackSaveInFlight
2243
+ }
2244
+ )
2245
+ ] })
2246
+ ] }),
2247
+ providerSettingsRegistry.get(config.provider).hasProviderAccountFieldsInIdentityStep && /* @__PURE__ */ jsxs3("fieldset", { style: fieldsetStyle, children: [
2248
+ /* @__PURE__ */ jsx3("legend", { style: legendStyle, children: "Provider account" }),
2249
+ /* @__PURE__ */ jsxs3("label", { style: fieldStyle, children: [
2250
+ /* @__PURE__ */ jsxs3("span", { children: [
2251
+ "GitHub Username ",
2252
+ /* @__PURE__ */ jsx3("span", { style: requiredStyle, children: "*" })
2253
+ ] }),
2254
+ /* @__PURE__ */ jsx3(
2255
+ "input",
2256
+ {
2257
+ type: "text",
2258
+ value: config.githubUsername,
2259
+ onChange: (e) => updateField("githubUsername", e.target.value),
2260
+ placeholder: "e.g. ouroboros-paperclip-agent[bot]",
2261
+ style: inputStyle
2262
+ }
2263
+ ),
2264
+ /* @__PURE__ */ jsx3("span", { style: hintStyle, children: "Public GitHub App login for this agent. GitHub appends [bot] to app account logins. Repository access is controlled by the GitHub App installation and provider permissions." })
2265
+ ] })
2266
+ ] })
2267
+ ] }),
2268
+ activeFormSection === activeProviderCredentialStepId && ActiveCredentialStep && /* @__PURE__ */ jsx3(
2269
+ ActiveCredentialStep,
2270
+ {
2271
+ state: activeProviderUIState,
2272
+ config
2273
+ }
2274
+ ),
2275
+ activeFormSection === "commit" && /* @__PURE__ */ jsxs3("fieldset", { style: fieldsetStyle, children: [
2276
+ /* @__PURE__ */ jsx3("legend", { style: legendStyle, children: "Commit identity (optional)" }),
2277
+ /* @__PURE__ */ jsx3("div", { style: inlineNoticeStyle, children: "These fields are optional. Save is available because the required identity and credential steps are complete." }),
2278
+ /* @__PURE__ */ jsxs3("label", { style: fieldStyle, children: [
2279
+ /* @__PURE__ */ jsx3("span", { children: "Commit Name" }),
2280
+ /* @__PURE__ */ jsx3(
2281
+ "input",
2282
+ {
2283
+ type: "text",
2284
+ value: config.commitName,
2285
+ onChange: (e) => updateField("commitName", e.target.value),
2286
+ placeholder: "e.g. Ouroboros Paperclip Agent",
2287
+ style: inputStyle
2288
+ }
2289
+ )
2290
+ ] }),
2291
+ /* @__PURE__ */ jsxs3("label", { style: fieldStyle, children: [
2292
+ /* @__PURE__ */ jsx3("span", { children: "Commit Email" }),
2293
+ /* @__PURE__ */ jsx3(
2294
+ "input",
2295
+ {
2296
+ type: "text",
2297
+ value: config.commitEmail,
2298
+ onChange: (e) => updateField("commitEmail", e.target.value),
2299
+ placeholder: "e.g. agent@example.com",
2300
+ style: inputStyle
2301
+ }
2302
+ )
2303
+ ] })
2304
+ ] })
2305
+ ] }),
2306
+ /* @__PURE__ */ jsxs3("footer", { className: "agent-identities-dialog-footer", style: dialogFooterStyle, children: [
2307
+ /* @__PURE__ */ jsxs3("div", { style: saveStatusStyle, children: [
2308
+ saveSuccess && /* @__PURE__ */ jsx3("span", { style: successStyle, children: "Saved successfully." }),
2309
+ saveError && /* @__PURE__ */ jsx3("span", { style: errorStyle, children: saveError }),
2310
+ !saveSuccess && !saveError && formValidation && !isLastFormStep && /* @__PURE__ */ jsx3("span", { style: hintStyle, children: formValidation.saveMessage }),
2311
+ !saveSuccess && !saveError && formValidation && isLastFormStep && !formValidation.isComplete && /* @__PURE__ */ jsx3("span", { style: hintStyle, children: formValidation.saveMessage })
2312
+ ] }),
2313
+ /* @__PURE__ */ jsx3(
2314
+ "button",
2315
+ {
2316
+ type: "button",
2317
+ onClick: () => setActiveFormSection(getPreviousFormStep(activeFormSection, config.provider)),
2318
+ disabled: saving || slackSaveInFlight || activeFormStepIndex === 0,
2319
+ style: buttonStyle(secondaryButtonStyle, saving || slackSaveInFlight || activeFormStepIndex === 0),
2320
+ children: "Previous"
2321
+ }
2322
+ ),
2323
+ !isLastFormStep ? /* @__PURE__ */ jsx3(
2324
+ "button",
2325
+ {
2326
+ type: "button",
2327
+ onClick: () => setActiveFormSection(getNextFormStep(activeFormSection, config.provider)),
2328
+ disabled: saving || slackSaveInFlight || !canGoNext,
2329
+ style: buttonStyle(primaryButtonStyle, saving || slackSaveInFlight || !canGoNext),
2330
+ children: "Next"
2331
+ }
2332
+ ) : /* @__PURE__ */ jsx3(
2333
+ "button",
2334
+ {
2335
+ type: "button",
2336
+ onClick: () => void handleSave(),
2337
+ disabled: !canSave,
2338
+ style: buttonStyle(primaryButtonStyle, !canSave),
2339
+ children: saving ? "Saving..." : "Save agent"
2340
+ }
2341
+ ),
2342
+ /* @__PURE__ */ jsx3("button", { type: "button", onClick: () => setFormState(null), disabled: saving || slackSaveInFlight, style: secondaryButtonStyle, children: "Cancel" })
2343
+ ] })
2344
+ ]
2345
+ }
2346
+ ) })
2347
+ ]
2348
+ }
2349
+ );
2350
+ }
2351
+ function SummaryTile(props) {
2352
+ return /* @__PURE__ */ jsxs3("div", { style: summaryTileStyle(props.tone ?? "neutral"), children: [
2353
+ /* @__PURE__ */ jsx3("span", { style: summaryValueStyle, children: props.value }),
2354
+ /* @__PURE__ */ jsx3("span", { style: summaryLabelStyle, children: props.label })
2355
+ ] });
2356
+ }
2357
+ function WizardStepIndicator(props) {
2358
+ return /* @__PURE__ */ jsxs3("div", { style: wizardStepStyle(props.active), children: [
2359
+ /* @__PURE__ */ jsx3("span", { style: wizardStepNumberStyle(props.active, props.complete), children: props.complete ? "\u2713" : props.index }),
2360
+ /* @__PURE__ */ jsx3("span", { children: props.label })
2361
+ ] });
2362
+ }
2363
+ function getAgentIdentityDefaults(agent, companyDisplayName, credentialSidecarPath) {
2364
+ const displayName = getAgentDisplayName(agent);
2365
+ const slug = slugifyGitHubAppName(displayName);
2366
+ return {
2367
+ label: formatIdentityLabel(displayName, companyDisplayName),
2368
+ githubUsername: `${slug}[bot]`,
2369
+ commitName: `${displayName} Paperclip Agent`,
2370
+ commitEmail: `${slug}[bot]@users.noreply.github.com`,
2371
+ privateKeyFile: getGitHubAppPrivateKeyFile(credentialSidecarPath, agent.id)
2372
+ };
2373
+ }
2374
+ function getGitHubAppPrivateKeyFile(credentialSidecarPath, agentId) {
2375
+ const lastSeparator = Math.max(
2376
+ credentialSidecarPath.lastIndexOf("/"),
2377
+ credentialSidecarPath.lastIndexOf("\\")
2378
+ );
2379
+ if (lastSeparator < 0) return "";
2380
+ const separator = credentialSidecarPath[lastSeparator];
2381
+ const directory = credentialSidecarPath.slice(0, lastSeparator);
2382
+ return `${directory}${separator}github-apps${separator}${agentId}${separator}private-key.pem`;
2383
+ }
2384
+ function shouldPrefillIdentityField(value, defaultValue) {
2385
+ return value.trim() === defaultValue.trim();
2386
+ }
2387
+ function formatIdentityLabel(agentName, companyDisplayName) {
2388
+ const companySuffix = companyDisplayName.trim();
2389
+ return companySuffix ? agentName + " [" + companySuffix + "]" : agentName;
2390
+ }
2391
+ function getAgentDisplayName(agent) {
2392
+ return (agent.name || agent.title || agent.role || agent.id).trim();
2393
+ }
2394
+ function getCompanyDisplayName(companyName, companyPrefix, companyId) {
2395
+ const resolvedName = (companyName ?? "").trim();
2396
+ if (resolvedName) return resolvedName;
2397
+ const displayName = titleCaseSlug(companyPrefix ?? "");
2398
+ return displayName || companyId.trim();
2399
+ }
2400
+ function titleCaseSlug(value) {
2401
+ return value.trim().replace(/[ _-]+/g, " ").replace(/\s+/g, " ").replace(/\b\w/g, (match) => match.toUpperCase());
2402
+ }
2403
+ function slugifyGitHubAppName(value) {
2404
+ return value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "paperclip-agent";
2405
+ }
2406
+ function toFormState(entry) {
2407
+ return {
2408
+ agentId: entry?.agentId ?? DEFAULT_BOT_IDENTITY_CONFIG.agentId,
2409
+ provider: entry?.provider ?? DEFAULT_BOT_IDENTITY_CONFIG.provider,
2410
+ label: entry?.label ?? DEFAULT_BOT_IDENTITY_CONFIG.label,
2411
+ githubUsername: entry?.provider === "github" ? entry.github.username : DEFAULT_BOT_IDENTITY_CONFIG.github.username,
2412
+ commitName: entry?.provider === "github" ? entry.github.commitName ?? "" : "",
2413
+ commitEmail: entry?.provider === "github" ? entry.github.commitEmail ?? "" : "",
2414
+ githubAppId: entry?.credential?.githubApp?.appId ?? "",
2415
+ githubInstallationId: entry?.credential?.githubApp?.installationId ?? "",
2416
+ privateKeySecretId: entry?.credential?.githubApp?.privateKeySecretId ?? "",
2417
+ privateKeyFile: entry?.credential?.githubApp?.privateKeyFile ?? "",
2418
+ fallbackTokenSecretId: entry?.credential?.secretId ?? "",
2419
+ tokenFile: entry?.credential?.tokenFile ?? "",
2420
+ previousAgentId: entry?.agentId ?? "",
2421
+ previousGithubAppId: entry?.credential?.githubApp?.appId ?? "",
2422
+ previousGithubInstallationId: entry?.credential?.githubApp?.installationId ?? "",
2423
+ previousPrivateKeySecretId: entry?.credential?.githubApp?.privateKeySecretId ?? "",
2424
+ previousPrivateKeyFile: entry?.credential?.githubApp?.privateKeyFile ?? "",
2425
+ slackTeamId: entry?.provider === "slack" ? entry.slack.teamId : "",
2426
+ slackAppId: entry?.provider === "slack" ? entry.slack.appId : "",
2427
+ slackBotUserId: entry?.provider === "slack" ? entry.slack.botUserId : "",
2428
+ slackDefaultChannel: entry?.provider === "slack" ? entry.slack.defaultChannel ?? "" : "",
2429
+ slackEventsRequestUrl: entry?.provider === "slack" ? entry.slackSetup?.eventsRequestUrl ?? "" : "",
2430
+ slackBotTokenSecretId: entry?.provider === "slack" ? entry.slackSetup?.botTokenSecretId ?? "" : "",
2431
+ slackSigningSecretId: entry?.provider === "slack" ? entry.slackSetup?.signingSecretId ?? "" : "",
2432
+ slackLegacyCredentialStatus: entry?.provider === "slack" ? entry.slackSetup?.legacyCredential?.status ?? "" : "",
2433
+ slackLegacySigningSecretRequired: entry?.provider === "slack" && entry.slackSetup?.legacyCredential?.signingSecretRequired ? "true" : ""
2434
+ };
2435
+ }
2436
+ var providerSettingsRegistry = buildProviderSettingsRegistry([...ALL_SETTINGS_ADAPTERS], GITHUB_IDENTITY_PROVIDER_ID);
2437
+ function getFormSteps(provider) {
2438
+ return providerSettingsRegistry.get(provider).formSteps;
2439
+ }
2440
+ function getIdentityFormValidation(config, hasDuplicate = false, slackSaveResult = null, slackSaveBusy = false) {
2441
+ const adapter = providerSettingsRegistry.get(config.provider);
2442
+ return adapter.getValidation(config, hasDuplicate, { slackSaveResult, slackSaveBusy });
2443
+ }
2444
+ function getFormStepIndex(step, provider) {
2445
+ return getFormSteps(provider).findIndex((entry) => entry.id === step);
2446
+ }
2447
+ function getNextFormStep(step, provider) {
2448
+ const steps = getFormSteps(provider);
2449
+ return steps[Math.min(getFormStepIndex(step, provider) + 1, steps.length - 1)]?.id ?? step;
2450
+ }
2451
+ function getPreviousFormStep(step, provider) {
2452
+ const steps = getFormSteps(provider);
2453
+ return steps[Math.max(getFormStepIndex(step, provider) - 1, 0)]?.id ?? step;
2454
+ }
2455
+ function canAdvanceFromStep(step, validation) {
2456
+ if (step === "identity") return validation.identityComplete;
2457
+ if (step === "github" || step === "slack") return validation.credentialComplete;
2458
+ return validation.isComplete;
2459
+ }
2460
+ function isWizardStepComplete(step, validation) {
2461
+ if (step === "identity") return validation.identityComplete;
2462
+ if (step === "github" || step === "slack") return validation.credentialComplete;
2463
+ return validation.isComplete;
2464
+ }
2465
+ function formatAgentOption(agent) {
2466
+ const details = [agent.role, agent.status].filter(Boolean).join(" - ");
2467
+ return details ? `${getAgentDisplayName(agent)} (${details})` : getAgentDisplayName(agent);
2468
+ }
2469
+ function formatAgentName(agentId, agents) {
2470
+ const agent = agents.find((entry) => entry.id === agentId);
2471
+ return agent ? getAgentDisplayName(agent) : agentId;
2472
+ }
2473
+ function getProviderDisplayName(providerId, providers) {
2474
+ return providers?.find((provider) => provider.id === providerId)?.name ?? providerId;
2475
+ }
2476
+ function formatSecretOption2(secret) {
2477
+ const label = secret.name || secret.key || secret.id;
2478
+ const details = [secret.key && secret.key !== label ? secret.key : null, secret.status, secret.provider].filter(Boolean).join(" - ");
2479
+ return details ? `${label} (${details})` : label;
2480
+ }
2481
+ function getSecretFieldHint2(input) {
2482
+ if (!input.companyId) {
2483
+ return "No company context is available, so paste the Paperclip secret UUID manually.";
2484
+ }
2485
+ if (input.secretsLoading) {
2486
+ return "Loading Paperclip secrets...";
2487
+ }
2488
+ if (input.secretsError) {
2489
+ return `Could not load Paperclip secrets (${input.secretsError}); paste the secret UUID manually.`;
2490
+ }
2491
+ if (!input.hasSecretOptions) {
2492
+ return "No Paperclip secrets were found; paste the private key secret UUID manually or use a private key file fallback.";
2493
+ }
2494
+ return "Saved as a Paperclip secret reference for the GitHub App private key and optionally propagated to agent environments.";
2495
+ }
2496
+ function getFallbackTokenSecretFieldHint(input) {
2497
+ if (!input.companyId) {
2498
+ return "No company context is available, so paste the fallback token secret UUID manually.";
2499
+ }
2500
+ if (input.secretsLoading) {
2501
+ return "Loading Paperclip secrets...";
2502
+ }
2503
+ if (input.secretsError) {
2504
+ return `Could not load Paperclip secrets (${input.secretsError}); paste the fallback token secret UUID manually.`;
2505
+ }
2506
+ if (!input.hasSecretOptions) {
2507
+ return "No Paperclip secrets were found; paste the fallback token secret UUID manually or use a fallback token file.";
2508
+ }
2509
+ return "Optional fallback token secret. Prefer GitHub App credentials above.";
2510
+ }
2511
+ async function loadSecretOptions(companyId) {
2512
+ const encodedCompanyId = encodeURIComponent(companyId);
2513
+ const paths = [
2514
+ `/api/companies/${encodedCompanyId}/secrets`,
2515
+ `/api/companies/${encodedCompanyId}/me/user-secrets`
2516
+ ];
2517
+ let lastError = null;
2518
+ for (const path of paths) {
2519
+ try {
2520
+ const response = await fetch(path, {
2521
+ credentials: "include",
2522
+ headers: { accept: "application/json" }
2523
+ });
2524
+ if (!response.ok) {
2525
+ lastError = new Error(`${response.status} ${response.statusText}`.trim());
2526
+ continue;
2527
+ }
2528
+ return normalizeSecretOptions(await response.json());
2529
+ } catch (err) {
2530
+ lastError = err instanceof Error ? err : new Error(String(err));
2531
+ }
2532
+ }
2533
+ throw lastError ?? new Error("Paperclip secrets API unavailable");
2534
+ }
2535
+ function normalizeSecretOptions(raw) {
2536
+ const candidates = Array.isArray(raw) ? raw : isRecord2(raw) && Array.isArray(raw.secrets) ? raw.secrets : isRecord2(raw) && Array.isArray(raw.userSecrets) ? raw.userSecrets : isRecord2(raw) && Array.isArray(raw.companySecrets) ? raw.companySecrets : isRecord2(raw) && Array.isArray(raw.items) ? raw.items : isRecord2(raw) && Array.isArray(raw.results) ? raw.results : isRecord2(raw) && Array.isArray(raw.data) ? raw.data : [];
2537
+ return candidates.map((entry) => normalizeSecretOption(entry)).filter((entry) => Boolean(entry)).filter((entry) => !entry.status || entry.status === "active").sort((left, right) => (left.name || left.key || left.id).localeCompare(right.name || right.key || right.id));
2538
+ }
2539
+ function normalizeSecretOption(raw) {
2540
+ const entry = isRecord2(raw) && isRecord2(raw.secret) ? raw.secret : raw;
2541
+ if (!isRecord2(entry)) return null;
2542
+ const id = readString2(entry.id) || readString2(entry.secretId);
2543
+ if (!id) return null;
2544
+ return {
2545
+ id,
2546
+ name: readString2(entry.name) || readString2(entry.label) || readString2(entry.key) || id,
2547
+ key: readString2(entry.key) || void 0,
2548
+ description: readString2(entry.description) || void 0,
2549
+ provider: readString2(entry.provider) || void 0,
2550
+ status: readString2(entry.status) || void 0
2551
+ };
2552
+ }
2553
+ function isRecord2(value) {
2554
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2555
+ }
2556
+ function readString2(value) {
2557
+ return typeof value === "string" ? value.trim() : "";
2558
+ }
2559
+ function formatCredentialStatus(status) {
2560
+ if (status === "configured") return "Configured";
2561
+ if (status === "rebind-required") return "Rebind required";
2562
+ if (status === "cleanup-pending") return "Cleanup pending";
2563
+ if (status === "conflict") return "Conflict";
2564
+ if (status === "sidecar-unavailable") return "Unavailable";
2565
+ return "Missing";
2566
+ }
2567
+ function getIdentityTone(entry) {
2568
+ if (entry.credentialStatus === "configured") return "good";
2569
+ if (entry.credentialStatus !== "missing") return "warn";
2570
+ return "neutral";
2571
+ }
2572
+ function summarizeIdentitySettings(identities) {
2573
+ const ready = identities.filter((identity) => identity.credentialStatus === "configured").length;
2574
+ return {
2575
+ total: identities.length,
2576
+ ready,
2577
+ needsSetup: identities.filter((identity) => identity.credentialStatus !== "configured").length
2578
+ };
2579
+ }
2580
+ function hasGitHubAppPropagationValues(entry) {
2581
+ const githubApp = entry.credential?.githubApp;
2582
+ return Boolean(githubApp?.appId && githubApp.installationId && (githubApp.privateKeySecretId || githubApp.privateKeyFile));
2583
+ }
2584
+ var GITHUB_APP_PROPAGATION_CONCURRENCY_LIMIT = 4;
2585
+ async function syncGitHubAppCredentialPropagationForAgents(input) {
2586
+ const selectedAgentIds = normalizeAgentIds(input.selectedAgentIds);
2587
+ const previousAgentIds = normalizeAgentIds(input.previousAgentIds);
2588
+ const selectedAgentIdSet = new Set(selectedAgentIds);
2589
+ const removeAgentIds = previousAgentIds.filter((agentId) => !selectedAgentIdSet.has(agentId));
2590
+ const operations = [];
2591
+ if (selectedAgentIds.length > 0) {
2592
+ const selectedConfig = {
2593
+ appId: input.githubAppId,
2594
+ installationId: input.githubInstallationId,
2595
+ privateKeySecretRef: input.privateKeySecretRef,
2596
+ privateKeyFile: input.privateKeyFile
2597
+ };
2598
+ if (hasCompleteGitHubAppValues(selectedConfig)) {
2599
+ const githubApp = buildGitHubAppPropagationConfig(selectedConfig);
2600
+ operations.push(...selectedAgentIds.map((agentId) => ({ agentId, mode: "ensure", githubApp })));
2601
+ }
2602
+ }
2603
+ if (removeAgentIds.length > 0) {
2604
+ const removeConfig = {
2605
+ appId: input.previousGithubAppId ?? input.githubAppId,
2606
+ installationId: input.previousGithubInstallationId ?? input.githubInstallationId,
2607
+ privateKeySecretRef: input.previousPrivateKeySecretRef ?? input.privateKeySecretRef,
2608
+ privateKeyFile: input.previousPrivateKeyFile ?? input.privateKeyFile
2609
+ };
2610
+ if (hasCompleteGitHubAppValues(removeConfig)) {
2611
+ const githubApp = buildGitHubAppPropagationConfig(removeConfig);
2612
+ operations.push(...removeAgentIds.map((agentId) => ({ agentId, mode: "remove", githubApp })));
2613
+ }
2614
+ }
2615
+ const failures = /* @__PURE__ */ new Set();
2616
+ await runWithConcurrencyLimit(operations, GITHUB_APP_PROPAGATION_CONCURRENCY_LIMIT, async (operation) => {
2617
+ try {
2618
+ await applyGitHubAppPropagationUpdate(operation);
2619
+ } catch {
2620
+ failures.add(operation.agentId);
2621
+ }
2622
+ });
2623
+ if (failures.size > 0) {
2624
+ throw new Error(`GitHub App credential propagation could not update these agents: ${[...failures].join(", ")}.`);
2625
+ }
2626
+ }
2627
+ function hasCompleteGitHubAppValues(input) {
2628
+ const appId = input.appId?.trim();
2629
+ const installationId = input.installationId?.trim();
2630
+ const privateKeySecretRef = input.privateKeySecretRef?.trim();
2631
+ const privateKeyFile = input.privateKeyFile?.trim();
2632
+ return Boolean(appId && installationId && (privateKeySecretRef || privateKeyFile));
2633
+ }
2634
+ function buildGitHubAppPropagationConfig(input) {
2635
+ const appId = input.appId?.trim();
2636
+ const installationId = input.installationId?.trim();
2637
+ const privateKeySecretRef = input.privateKeySecretRef?.trim();
2638
+ const privateKeyFile = input.privateKeyFile?.trim();
2639
+ if (!appId || !installationId || !privateKeySecretRef && !privateKeyFile) {
2640
+ throw new Error("GitHub App credential propagation requires app ID, installation ID, and a private key secret or file.");
2641
+ }
2642
+ return {
2643
+ appId,
2644
+ installationId,
2645
+ ...privateKeySecretRef ? { privateKeySecretRef } : {},
2646
+ ...privateKeyFile ? { privateKeyFile } : {}
2647
+ };
2648
+ }
2649
+ function normalizeAgentIds(value) {
2650
+ return value.map((entry) => entry.trim()).filter(Boolean).filter((entry, index, entries) => entries.indexOf(entry) === index);
2651
+ }
2652
+ async function runWithConcurrencyLimit(items, concurrencyLimit, worker) {
2653
+ if (items.length === 0) return;
2654
+ let nextIndex = 0;
2655
+ const runnerCount = Math.min(concurrencyLimit, items.length);
2656
+ await Promise.all(Array.from({ length: runnerCount }, async () => {
2657
+ while (nextIndex < items.length) {
2658
+ const currentIndex = nextIndex;
2659
+ nextIndex += 1;
2660
+ await worker(items[currentIndex]);
2661
+ }
2662
+ }));
2663
+ }
2664
+ async function applyGitHubAppPropagationUpdate(input) {
2665
+ const agent = await fetchJson(`/api/agents/${encodeURIComponent(input.agentId)}`);
2666
+ const nextAdapterConfig = getAgentPropagationPatch({
2667
+ adapterConfig: isRecord2(agent) ? agent.adapterConfig : void 0,
2668
+ githubApp: input.githubApp,
2669
+ mode: input.mode
2670
+ });
2671
+ if (!nextAdapterConfig) {
2672
+ return;
2673
+ }
2674
+ await fetchJson(`/api/agents/${encodeURIComponent(input.agentId)}`, {
2675
+ method: "PATCH",
2676
+ body: JSON.stringify({
2677
+ adapterConfig: nextAdapterConfig,
2678
+ replaceAdapterConfig: true
2679
+ })
2680
+ });
2681
+ }
2682
+ function getAgentPropagationPatch(input) {
2683
+ const adapterConfig = isRecord2(input.adapterConfig) ? { ...input.adapterConfig } : {};
2684
+ const currentEnv = isRecord2(adapterConfig.env) ? { ...adapterConfig.env } : {};
2685
+ if (input.mode === "ensure") {
2686
+ const nextEnv2 = {
2687
+ ...currentEnv,
2688
+ GITHUB_APP_ID: input.githubApp.appId,
2689
+ GITHUB_INSTALLATION_ID: input.githubApp.installationId
2690
+ };
2691
+ if (input.githubApp.privateKeySecretRef) {
2692
+ nextEnv2.GITHUB_APP_PRIVATE_KEY = {
2693
+ type: "secret_ref",
2694
+ secretId: input.githubApp.privateKeySecretRef,
2695
+ version: "latest"
2696
+ };
2697
+ delete nextEnv2.GITHUB_APP_PRIVATE_KEY_FILE;
2698
+ } else if (input.githubApp.privateKeyFile) {
2699
+ nextEnv2.GITHUB_APP_PRIVATE_KEY_FILE = input.githubApp.privateKeyFile;
2700
+ delete nextEnv2.GITHUB_APP_PRIVATE_KEY;
2701
+ }
2702
+ if (JSON.stringify(nextEnv2) === JSON.stringify(currentEnv)) {
2703
+ return null;
2704
+ }
2705
+ return { ...adapterConfig, env: nextEnv2 };
2706
+ }
2707
+ if (!isMatchingGitHubAppEnvBindings(currentEnv, input.githubApp)) {
2708
+ return null;
2709
+ }
2710
+ const nextEnv = { ...currentEnv };
2711
+ delete nextEnv.GITHUB_APP_ID;
2712
+ delete nextEnv.GITHUB_INSTALLATION_ID;
2713
+ delete nextEnv.GITHUB_APP_PRIVATE_KEY;
2714
+ delete nextEnv.GITHUB_APP_PRIVATE_KEY_FILE;
2715
+ const nextAdapterConfig = { ...adapterConfig };
2716
+ if (Object.keys(nextEnv).length > 0) {
2717
+ nextAdapterConfig.env = nextEnv;
2718
+ } else {
2719
+ delete nextAdapterConfig.env;
2720
+ }
2721
+ return nextAdapterConfig;
2722
+ }
2723
+ function isMatchingGitHubAppEnvBindings(env, githubApp) {
2724
+ if (env.GITHUB_APP_ID !== githubApp.appId || env.GITHUB_INSTALLATION_ID !== githubApp.installationId) {
2725
+ return false;
2726
+ }
2727
+ if (githubApp.privateKeySecretRef) {
2728
+ return isMatchingSecretRefEnvBinding(env.GITHUB_APP_PRIVATE_KEY, githubApp.privateKeySecretRef);
2729
+ }
2730
+ return Boolean(githubApp.privateKeyFile && env.GITHUB_APP_PRIVATE_KEY_FILE === githubApp.privateKeyFile);
2731
+ }
2732
+ function isMatchingSecretRefEnvBinding(value, secretId) {
2733
+ return isRecord2(value) && value.type === "secret_ref" && value.secretId === secretId;
2734
+ }
2735
+ async function fetchJson(path, init) {
2736
+ const response = await fetch(path, {
2737
+ ...init,
2738
+ credentials: "include",
2739
+ headers: {
2740
+ accept: "application/json",
2741
+ ...init?.body ? { "content-type": "application/json" } : {},
2742
+ ...init?.headers
2743
+ }
2744
+ });
2745
+ if (!response.ok) {
2746
+ throw new Error(`${response.status} ${response.statusText}`.trim());
2747
+ }
2748
+ return await response.json();
2749
+ }
2750
+ function getAgentFieldHint(input) {
2751
+ if (!input.companyId) {
2752
+ return "No company context is available, so paste the Paperclip agent UUID manually.";
2753
+ }
2754
+ if (input.agentsLoading) {
2755
+ return "Loading Paperclip agents...";
2756
+ }
2757
+ if (input.agentsError) {
2758
+ return "Could not load Paperclip agents; paste the agent UUID manually.";
2759
+ }
2760
+ if (!input.hasAgentOptions) {
2761
+ return "No Paperclip agents were found for this company; paste the agent UUID manually.";
2762
+ }
2763
+ return "The Paperclip agent that will use this identity.";
2764
+ }
2765
+ var responsiveSettingsStyle = `
2766
+ .agent-identities-settings,
2767
+ .agent-identities-settings * {
2768
+ box-sizing: border-box;
2769
+ }
2770
+
2771
+ .agent-identities-summary-grid {
2772
+ grid-template-columns: repeat(3, minmax(0, 1fr));
2773
+ }
2774
+
2775
+ .agent-identities-list-header,
2776
+ .agent-identities-list-row {
2777
+ grid-template-columns: minmax(0, 1fr) 8rem 8rem 13rem;
2778
+ }
2779
+
2780
+ .agent-identities-row-actions {
2781
+ justify-content: flex-end;
2782
+ }
2783
+
2784
+ .agent-identities-wizard-steps {
2785
+ grid-template-columns: repeat(3, minmax(0, 1fr));
2786
+ }
2787
+
2788
+ @container (max-width: 720px) {
2789
+ .agent-identities-list-header {
2790
+ display: none !important;
2791
+ }
2792
+
2793
+ .agent-identities-list-row {
2794
+ grid-template-columns: minmax(0, 1fr) auto;
2795
+ }
2796
+
2797
+ .agent-identities-list-row > :nth-child(1) {
2798
+ grid-column: 1;
2799
+ grid-row: 1;
2800
+ }
2801
+
2802
+ .agent-identities-list-row > :nth-child(2) {
2803
+ grid-column: 1 / -1;
2804
+ grid-row: 2;
2805
+ }
2806
+
2807
+ .agent-identities-list-row > :nth-child(3) {
2808
+ grid-column: 2;
2809
+ grid-row: 1;
2810
+ }
2811
+
2812
+ .agent-identities-list-row > :nth-child(4) {
2813
+ grid-column: 1 / -1;
2814
+ grid-row: 3;
2815
+ justify-content: flex-start;
2816
+ }
2817
+ }
2818
+
2819
+ @container (max-width: 520px) {
2820
+ .agent-identities-summary-grid,
2821
+ .agent-identities-wizard-steps {
2822
+ grid-template-columns: minmax(0, 1fr);
2823
+ }
2824
+ }
2825
+ `;
2826
+ var pageStyle = {
2827
+ width: "100%",
2828
+ minWidth: 0,
2829
+ maxWidth: 1160,
2830
+ display: "grid",
2831
+ gap: "1rem",
2832
+ color: uiText,
2833
+ containerType: "inline-size"
2834
+ };
2835
+ var pageTitleStyle = {
2836
+ margin: 0,
2837
+ fontSize: "1.125rem",
2838
+ lineHeight: 1.2,
2839
+ letterSpacing: "-0.02em"
2840
+ };
2841
+ var headerStyle = {
2842
+ display: "flex",
2843
+ justifyContent: "space-between",
2844
+ alignItems: "flex-start",
2845
+ gap: "1rem",
2846
+ flexWrap: "wrap"
2847
+ };
2848
+ var descriptionStyle = {
2849
+ margin: "0.35rem 0 0",
2850
+ color: uiMutedText,
2851
+ fontSize: "0.875rem"
2852
+ };
2853
+ var summaryGridStyle = {
2854
+ display: "grid",
2855
+ gap: "0.75rem"
2856
+ };
2857
+ function summaryTileStyle(tone) {
2858
+ return {
2859
+ display: "grid",
2860
+ gap: "0.25rem",
2861
+ padding: "0.85rem 1rem",
2862
+ border: `1px solid ${toneBorder(tone)}`,
2863
+ borderRadius: 12,
2864
+ backgroundColor: toneBackground(tone)
2865
+ };
2866
+ }
2867
+ var summaryValueStyle = {
2868
+ fontSize: "1.125rem",
2869
+ fontWeight: 700,
2870
+ lineHeight: 1
2871
+ };
2872
+ var summaryLabelStyle = {
2873
+ color: uiMutedText,
2874
+ fontSize: "0.8125rem",
2875
+ fontWeight: 600
2876
+ };
2877
+ var credentialErrorStyle = {
2878
+ padding: "0.75rem 0.9rem",
2879
+ backgroundColor: "color-mix(in srgb, var(--agent-identities-danger) 12%, transparent)",
2880
+ border: "1px solid color-mix(in srgb, var(--agent-identities-danger) 45%, transparent)",
2881
+ borderRadius: 10,
2882
+ color: uiDanger,
2883
+ fontSize: "0.875rem"
2884
+ };
2885
+ var sectionStyle = {
2886
+ minWidth: 0,
2887
+ border: `1px solid ${uiBorder}`,
2888
+ borderRadius: 14,
2889
+ padding: "1rem",
2890
+ display: "grid",
2891
+ gap: "0.9rem",
2892
+ backgroundColor: uiSurface
2893
+ };
2894
+ var sectionHeaderStyle = {
2895
+ display: "flex",
2896
+ alignItems: "flex-start",
2897
+ justifyContent: "space-between",
2898
+ gap: "1rem",
2899
+ flexWrap: "wrap"
2900
+ };
2901
+ var sectionTitleStyle = {
2902
+ margin: 0,
2903
+ fontSize: "0.875rem",
2904
+ lineHeight: 1.25
2905
+ };
2906
+ var sectionDescriptionStyle = {
2907
+ margin: "0.25rem 0 0",
2908
+ color: uiMutedText,
2909
+ fontSize: "0.875rem"
2910
+ };
2911
+ var listStyle = {
2912
+ display: "grid",
2913
+ gap: "0.35rem",
2914
+ minWidth: 0
2915
+ };
2916
+ var listHeaderStyle = {
2917
+ display: "grid",
2918
+ gap: "0.75rem",
2919
+ padding: "0 0.75rem 0.25rem",
2920
+ color: uiMutedText,
2921
+ fontSize: "0.8125rem",
2922
+ fontWeight: 650
2923
+ };
2924
+ var rowStyle = {
2925
+ display: "grid",
2926
+ gap: "0.75rem",
2927
+ alignItems: "center",
2928
+ border: `1px solid ${uiBorder}`,
2929
+ borderRadius: 10,
2930
+ padding: "0.75rem",
2931
+ backgroundColor: uiPanel
2932
+ };
2933
+ var rowTitleStyle = {
2934
+ fontWeight: 650,
2935
+ overflowWrap: "anywhere"
2936
+ };
2937
+ var rowMetaStyle = {
2938
+ color: uiMutedText,
2939
+ fontSize: "0.875rem",
2940
+ overflowWrap: "anywhere"
2941
+ };
2942
+ var rowActionsStyle = {
2943
+ display: "flex",
2944
+ gap: "0.5rem",
2945
+ flexWrap: "wrap"
2946
+ };
2947
+ var emptyStateStyle = {
2948
+ display: "grid",
2949
+ justifyItems: "start",
2950
+ gap: "0.5rem",
2951
+ padding: "1.25rem",
2952
+ border: `1px dashed ${uiBorderStrong}`,
2953
+ borderRadius: 12,
2954
+ color: uiMutedText,
2955
+ backgroundColor: uiPanel
2956
+ };
2957
+ var dialogBackdropStyle = {
2958
+ position: "fixed",
2959
+ inset: 0,
2960
+ zIndex: 1e3,
2961
+ display: "grid",
2962
+ placeItems: "start center",
2963
+ padding: "7vh 1rem 1rem",
2964
+ backgroundColor: uiOverlay,
2965
+ overflowY: "auto"
2966
+ };
2967
+ var dialogStyle = {
2968
+ width: "min(760px, 100%)",
2969
+ maxHeight: "86vh",
2970
+ display: "grid",
2971
+ gridTemplateRows: "auto minmax(0, 1fr) auto",
2972
+ border: `1px solid ${uiBorder}`,
2973
+ borderRadius: 16,
2974
+ backgroundColor: uiCanvas,
2975
+ color: uiText,
2976
+ boxShadow: `0 24px 80px ${uiShadow}`,
2977
+ overflow: "hidden"
2978
+ };
2979
+ var dialogHeaderStyle = {
2980
+ display: "flex",
2981
+ alignItems: "flex-start",
2982
+ justifyContent: "space-between",
2983
+ gap: "1rem",
2984
+ padding: "1.1rem 1.25rem 0.9rem",
2985
+ borderBottom: `1px solid ${uiBorder}`
2986
+ };
2987
+ var dialogTitleStyle = {
2988
+ margin: 0,
2989
+ fontSize: "1.125rem",
2990
+ lineHeight: 1.2,
2991
+ letterSpacing: "-0.015em"
2992
+ };
2993
+ var closeButtonStyle = {
2994
+ width: 32,
2995
+ height: 32,
2996
+ border: `1px solid ${uiBorder}`,
2997
+ borderRadius: 8,
2998
+ backgroundColor: "transparent",
2999
+ color: uiMutedText,
3000
+ cursor: "pointer",
3001
+ fontSize: "0.875rem"
3002
+ };
3003
+ var dialogBodyStyle = {
3004
+ display: "grid",
3005
+ gap: "0.85rem",
3006
+ padding: "1rem 1.25rem",
3007
+ overflowY: "auto"
3008
+ };
3009
+ var wizardStepListStyle = {
3010
+ display: "grid",
3011
+ gap: "0.5rem"
3012
+ };
3013
+ function wizardStepStyle(active) {
3014
+ return {
3015
+ display: "flex",
3016
+ alignItems: "center",
3017
+ gap: "0.5rem",
3018
+ minHeight: 36,
3019
+ padding: "0.45rem 0.6rem",
3020
+ border: `1px solid ${active ? uiBorderStrong : uiBorder}`,
3021
+ borderRadius: 8,
3022
+ backgroundColor: active ? uiSurface : "transparent",
3023
+ color: active ? uiText : uiMutedText,
3024
+ fontSize: "0.875rem",
3025
+ fontWeight: active ? 600 : 500
3026
+ };
3027
+ }
3028
+ function wizardStepNumberStyle(active, complete) {
3029
+ return {
3030
+ display: "inline-grid",
3031
+ placeItems: "center",
3032
+ width: 22,
3033
+ height: 22,
3034
+ borderRadius: 999,
3035
+ border: `1px solid ${complete ? uiSuccess : active ? uiBorderStrong : uiBorder}`,
3036
+ backgroundColor: complete ? "color-mix(in srgb, var(--agent-identities-success) 14%, transparent)" : uiPanel,
3037
+ color: complete ? uiSuccess : active ? uiText : uiMutedText,
3038
+ fontSize: "0.8125rem",
3039
+ fontWeight: 600
3040
+ };
3041
+ }
3042
+ var dialogFooterStyle = {
3043
+ display: "flex",
3044
+ justifyContent: "flex-end",
3045
+ alignItems: "center",
3046
+ gap: "0.75rem",
3047
+ flexWrap: "wrap",
3048
+ padding: "0.9rem 1.25rem",
3049
+ borderTop: `1px solid ${uiBorder}`,
3050
+ backgroundColor: uiPanel
3051
+ };
3052
+ var saveStatusStyle = {
3053
+ marginRight: "auto",
3054
+ fontSize: "0.875rem"
3055
+ };
3056
+ var validationNoticeStyle = {
3057
+ padding: "0.65rem 0.75rem",
3058
+ border: "1px solid color-mix(in srgb, var(--agent-identities-warning) 36%, transparent)",
3059
+ borderRadius: 8,
3060
+ backgroundColor: "color-mix(in srgb, var(--agent-identities-warning) 8%, transparent)",
3061
+ color: uiText,
3062
+ fontSize: "0.875rem"
3063
+ };
3064
+ var fieldsetStyle = {
3065
+ border: `1px solid ${uiBorder}`,
3066
+ borderRadius: 12,
3067
+ padding: "1rem",
3068
+ display: "grid",
3069
+ gap: "0.75rem"
3070
+ };
3071
+ var legendStyle = {
3072
+ padding: "0 0.25rem",
3073
+ color: uiText,
3074
+ fontSize: "0.875rem",
3075
+ fontWeight: 600
3076
+ };
3077
+ var fieldStyle = {
3078
+ display: "grid",
3079
+ gap: "0.35rem",
3080
+ fontSize: "0.875rem",
3081
+ fontWeight: 600
3082
+ };
3083
+ var inputStyle = {
3084
+ minHeight: 38,
3085
+ padding: "0.45rem 0.65rem",
3086
+ border: `1px solid ${uiBorderStrong}`,
3087
+ borderRadius: 8,
3088
+ fontSize: "0.875rem",
3089
+ backgroundColor: uiInput,
3090
+ color: uiText
3091
+ };
3092
+ var textareaStyle = {
3093
+ ...inputStyle,
3094
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace"
3095
+ };
3096
+ var manifestPanelStyle = {
3097
+ display: "grid",
3098
+ gap: "0.75rem",
3099
+ padding: "0.85rem",
3100
+ border: `1px dashed ${uiBorderStrong}`,
3101
+ borderRadius: 10,
3102
+ backgroundColor: uiPanel
3103
+ };
3104
+ var linkStyle = {
3105
+ color: uiLink,
3106
+ fontWeight: 600
3107
+ };
3108
+ var inlineNoticeStyle = {
3109
+ padding: "0.75rem 0.9rem",
3110
+ border: `1px solid ${uiBorder}`,
3111
+ borderRadius: 10,
3112
+ backgroundColor: uiPanel,
3113
+ color: uiMutedText,
3114
+ fontSize: "0.875rem"
3115
+ };
3116
+ var hintStyle = {
3117
+ fontSize: "0.8125rem",
3118
+ fontWeight: 400,
3119
+ color: uiMutedText
3120
+ };
3121
+ var formActionsStyle = {
3122
+ display: "flex",
3123
+ alignItems: "center",
3124
+ gap: "0.75rem",
3125
+ flexWrap: "wrap"
3126
+ };
3127
+ var primaryButtonStyle = {
3128
+ minHeight: 38,
3129
+ padding: "0.48rem 0.9rem",
3130
+ backgroundColor: uiPrimary,
3131
+ color: uiPrimaryText,
3132
+ border: `1px solid ${uiBorderStrong}`,
3133
+ borderRadius: 8,
3134
+ cursor: "pointer",
3135
+ fontWeight: 650
3136
+ };
3137
+ var secondaryButtonStyle = {
3138
+ minHeight: 34,
3139
+ padding: "0.4rem 0.75rem",
3140
+ backgroundColor: uiSurface,
3141
+ color: uiText,
3142
+ border: `1px solid ${uiBorderStrong}`,
3143
+ borderRadius: 8,
3144
+ cursor: "pointer",
3145
+ fontWeight: 600
3146
+ };
3147
+ var dangerButtonStyle = {
3148
+ ...secondaryButtonStyle,
3149
+ color: uiDanger
3150
+ };
3151
+ var requiredStyle = {
3152
+ color: uiDanger
3153
+ };
3154
+ var successStyle = {
3155
+ color: uiSuccess
3156
+ };
3157
+ var errorStyle = {
3158
+ color: uiDanger
3159
+ };
3160
+ function statusBadgeStyle(tone) {
3161
+ return {
3162
+ justifySelf: "start",
3163
+ border: `1px solid ${toneBorder(tone)}`,
3164
+ borderRadius: 999,
3165
+ padding: "0.2rem 0.55rem",
3166
+ color: toneText(tone),
3167
+ backgroundColor: toneBackground(tone),
3168
+ fontSize: "0.8125rem",
3169
+ fontWeight: 650,
3170
+ whiteSpace: "nowrap"
3171
+ };
3172
+ }
3173
+ function buttonStyle(base, disabled) {
3174
+ return disabled ? { ...base, opacity: 0.55, cursor: "not-allowed" } : base;
3175
+ }
3176
+ function toneText(tone) {
3177
+ if (tone === "good") return uiSuccess;
3178
+ if (tone === "warn") return uiWarning;
3179
+ return uiMutedText;
3180
+ }
3181
+ function toneBorder(tone) {
3182
+ if (tone === "good") return "color-mix(in srgb, var(--agent-identities-success) 42%, transparent)";
3183
+ if (tone === "warn") return "color-mix(in srgb, var(--agent-identities-warning) 48%, transparent)";
3184
+ return uiBorder;
3185
+ }
3186
+ function toneBackground(tone) {
3187
+ if (tone === "good") return "color-mix(in srgb, var(--agent-identities-success) 10%, transparent)";
3188
+ if (tone === "warn") return "color-mix(in srgb, var(--agent-identities-warning) 12%, transparent)";
3189
+ return uiPanel;
3190
+ }
3191
+
3192
+ // src/ui/index.tsx
3193
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
3194
+ function DashboardWidget({ context }) {
3195
+ const themeMode = usePaperclipThemeMode();
3196
+ const { data, loading, error } = usePluginData2("bot-identity-config", {
3197
+ companyId: context.companyId
3198
+ });
3199
+ if (loading) return /* @__PURE__ */ jsx4("div", { children: "Loading agent identities..." });
3200
+ if (error) return /* @__PURE__ */ jsxs4("div", { children: [
3201
+ "Agent identities unavailable: ",
3202
+ error.message
3203
+ ] });
3204
+ const identities = data?.identities ?? [];
3205
+ const summary = summarizeIdentities(identities);
3206
+ return /* @__PURE__ */ jsxs4("div", { style: { ...createPaperclipThemeStyle(themeMode), ...widgetStyle }, "data-agent-identities-theme": themeMode, children: [
3207
+ /* @__PURE__ */ jsxs4("div", { children: [
3208
+ /* @__PURE__ */ jsx4("strong", { children: "Agent Identities" }),
3209
+ /* @__PURE__ */ jsx4("div", { style: mutedStyle, children: "Identity provider coverage" })
3210
+ ] }),
3211
+ /* @__PURE__ */ jsxs4("div", { style: metricGridStyle, children: [
3212
+ /* @__PURE__ */ jsx4(Metric, { label: "Identities", value: summary.total, tone: "neutral" }),
3213
+ /* @__PURE__ */ jsx4(Metric, { label: "GitHub Apps", value: summary.githubApps, tone: summary.githubApps > 0 ? "good" : "neutral" }),
3214
+ /* @__PURE__ */ jsx4(Metric, { label: "Need setup", value: summary.needsSetup, tone: summary.needsSetup > 0 ? "warn" : "good" })
3215
+ ] }),
3216
+ data?.credentialSidecarError && /* @__PURE__ */ jsx4("div", { style: warningStyle, children: "GitHub credential sidecar unavailable. GitHub saves may not update private key references." }),
3217
+ identities.length === 0 ? /* @__PURE__ */ jsx4("div", { style: hintBoxStyle, children: "No agent identities configured yet. Open plugin settings to add a provider-backed agent identity." }) : /* @__PURE__ */ jsxs4("div", { style: identityListStyle, children: [
3218
+ identities.slice(0, 3).map((identity) => /* @__PURE__ */ jsxs4("div", { style: identityRowStyle, children: [
3219
+ /* @__PURE__ */ jsxs4("div", { style: { minWidth: 0 }, children: [
3220
+ /* @__PURE__ */ jsx4("div", { style: identityNameStyle, children: identity.label }),
3221
+ /* @__PURE__ */ jsx4("div", { style: mutedStyle, children: formatProviderIdentity(identity) })
3222
+ ] }),
3223
+ /* @__PURE__ */ jsx4("span", { style: badgeStyle(identityTone(identity)), children: formatIdentityCredential(identity) })
3224
+ ] }, identity.id)),
3225
+ identities.length > 3 && /* @__PURE__ */ jsxs4("div", { style: mutedStyle, children: [
3226
+ "+",
3227
+ identities.length - 3,
3228
+ " more configured"
3229
+ ] })
3230
+ ] })
3231
+ ] });
3232
+ }
3233
+ function summarizeIdentities(identities) {
3234
+ const githubApps = identities.filter((identity) => hasCompleteGitHubApp(identity)).length;
3235
+ return {
3236
+ total: identities.length,
3237
+ githubApps,
3238
+ needsSetup: identities.filter((identity) => identity.credentialStatus !== "configured").length
3239
+ };
3240
+ }
3241
+ function hasCompleteGitHubApp(identity) {
3242
+ const githubApp = identity.credential?.githubApp;
3243
+ return Boolean(githubApp?.appId && githubApp.installationId && (githubApp.privateKeySecretId || githubApp.privateKeyFile));
3244
+ }
3245
+ function hasFallbackCredential(identity) {
3246
+ return Boolean(identity.credential?.secretId || identity.credential?.tokenFile);
3247
+ }
3248
+ function formatProviderIdentity(identity) {
3249
+ if (identity.provider === "github") return `GitHub: ${identity.github.username}`;
3250
+ if (identity.provider === "slack") return `Slack: ${identity.slack.teamId}`;
3251
+ return identity.provider;
3252
+ }
3253
+ function formatIdentityCredential(identity) {
3254
+ if (hasCompleteGitHubApp(identity)) return "GitHub App";
3255
+ if (hasFallbackCredential(identity)) return "Fallback";
3256
+ if (identity.credentialStatus === "configured") return "Configured";
3257
+ if (identity.credentialStatus === "rebind-required") return "Rebind required";
3258
+ if (identity.credentialStatus === "cleanup-pending") return "Cleanup pending";
3259
+ if (identity.credentialStatus === "conflict") return "Conflict";
3260
+ if (identity.credentialStatus === "sidecar-unavailable") return "Unavailable";
3261
+ return "Missing";
3262
+ }
3263
+ function identityTone(identity) {
3264
+ if (hasCompleteGitHubApp(identity)) return "good";
3265
+ if (hasFallbackCredential(identity)) return "neutral";
3266
+ if (identity.credentialStatus === "configured") return "good";
3267
+ return "warn";
3268
+ }
3269
+ function Metric(props) {
3270
+ return /* @__PURE__ */ jsxs4("div", { style: metricStyle(props.tone), children: [
3271
+ /* @__PURE__ */ jsx4("div", { style: metricValueStyle, children: props.value }),
3272
+ /* @__PURE__ */ jsx4("div", { style: metricLabelStyle, children: props.label })
3273
+ ] });
3274
+ }
3275
+ var widgetStyle = {
3276
+ display: "grid",
3277
+ gap: "0.75rem"
3278
+ };
3279
+ var mutedStyle = {
3280
+ color: uiMutedText,
3281
+ fontSize: "0.85rem",
3282
+ overflowWrap: "anywhere"
3283
+ };
3284
+ var metricGridStyle = {
3285
+ display: "grid",
3286
+ gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
3287
+ gap: "0.5rem"
3288
+ };
3289
+ function metricStyle(tone) {
3290
+ return {
3291
+ border: `1px solid ${toneColor(tone)}`,
3292
+ borderRadius: 8,
3293
+ padding: "0.6rem",
3294
+ background: toneBackground2(tone)
3295
+ };
3296
+ }
3297
+ var metricValueStyle = {
3298
+ fontSize: "1.35rem",
3299
+ fontWeight: 700,
3300
+ lineHeight: 1
3301
+ };
3302
+ var metricLabelStyle = {
3303
+ color: uiMutedText,
3304
+ fontSize: "0.75rem",
3305
+ marginTop: "0.25rem"
3306
+ };
3307
+ var identityListStyle = {
3308
+ display: "grid",
3309
+ gap: "0.45rem"
3310
+ };
3311
+ var identityRowStyle = {
3312
+ display: "flex",
3313
+ justifyContent: "space-between",
3314
+ alignItems: "center",
3315
+ gap: "0.75rem"
3316
+ };
3317
+ var identityNameStyle = {
3318
+ fontWeight: 600,
3319
+ overflowWrap: "anywhere"
3320
+ };
3321
+ function badgeStyle(tone) {
3322
+ return {
3323
+ border: `1px solid ${toneColor(tone)}`,
3324
+ borderRadius: 999,
3325
+ padding: "0.15rem 0.5rem",
3326
+ color: toneColor(tone),
3327
+ fontSize: "0.75rem",
3328
+ whiteSpace: "nowrap"
3329
+ };
3330
+ }
3331
+ var hintBoxStyle = {
3332
+ border: `1px dashed ${uiBorderStrong}`,
3333
+ borderRadius: 8,
3334
+ padding: "0.65rem",
3335
+ color: uiMutedText,
3336
+ fontSize: "0.9rem"
3337
+ };
3338
+ var warningStyle = {
3339
+ border: `1px solid ${uiWarning}`,
3340
+ borderRadius: 8,
3341
+ padding: "0.65rem",
3342
+ color: uiWarning,
3343
+ background: "color-mix(in srgb, var(--agent-identities-warning) 12%, transparent)",
3344
+ fontSize: "0.9rem"
3345
+ };
3346
+ function toneColor(tone) {
3347
+ if (tone === "good") return uiSuccess;
3348
+ if (tone === "warn") return uiWarning;
3349
+ return uiMutedText;
3350
+ }
3351
+ function toneBackground2(tone) {
3352
+ if (tone === "good") return "color-mix(in srgb, var(--agent-identities-success) 10%, transparent)";
3353
+ if (tone === "warn") return "color-mix(in srgb, var(--agent-identities-warning) 12%, transparent)";
3354
+ return uiPanel;
3355
+ }
3356
+ export {
3357
+ DashboardWidget,
3358
+ SettingsPage
3359
+ };
3360
+ //# sourceMappingURL=index.js.map