@gajae-code/coding-agent 0.12.7 → 0.12.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/dist/types/cli/notify-cli.d.ts +1 -0
- package/dist/types/commands/notify.d.ts +4 -0
- package/dist/types/commands/sdk.d.ts +23 -0
- package/dist/types/config/file-lock.d.ts +3 -0
- package/dist/types/config/model-discovery-manager.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/resolve-config-value.d.ts +2 -1
- package/dist/types/config/settings-schema.d.ts +17 -1
- package/dist/types/discovery/agents-md.d.ts +11 -1
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +20 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/sdk/broker/broker.d.ts +5 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
- package/dist/types/sdk/bus/config.d.ts +75 -113
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
- package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
- package/dist/types/sdk/bus/index.d.ts +1 -1
- package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
- package/dist/types/sdk/bus/notification-service.d.ts +54 -4
- package/dist/types/sdk/bus/session-control.d.ts +8 -3
- package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
- package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
- package/dist/types/sdk/client/discovery.d.ts +2 -1
- package/dist/types/sdk/host/query/handlers.d.ts +2 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/providers.d.ts +22 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +19 -1
- package/dist/types/session/internal/native-publish-outcome.d.ts +3 -2
- package/dist/types/session/internal/session-open-errors.d.ts +5 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/session/session-storage.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +12 -1
- package/dist/types/tools/composer-bash-policy.d.ts +9 -1
- package/package.json +10 -8
- package/scripts/build-sdk-package-smoke.ts +51 -0
- package/scripts/capture-notifications-settings-showcase.ts +1 -1
- package/scripts/verify-gjc-sdk-canonicalization.ts +14 -1
- package/src/cli/notify-cli.ts +215 -99
- package/src/commands/notify.ts +19 -8
- package/src/commands/sdk.ts +54 -0
- package/src/config/file-lock.ts +6 -0
- package/src/config/model-discovery-manager.ts +50 -8
- package/src/config/model-profiles.ts +8 -0
- package/src/config/model-registry.ts +866 -76
- package/src/config/resolve-config-value.ts +10 -8
- package/src/config/settings-schema.ts +25 -15
- package/src/config/settings.ts +15 -15
- package/src/discovery/agents-md.ts +76 -19
- package/src/gjc-runtime/psmux-detect.ts +12 -16
- package/src/gjc-runtime/session-state-sidecar.ts +12 -2
- package/src/gjc-runtime/tmux-sessions.ts +8 -1
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/main.ts +43 -7
- package/src/modes/DESIGN.md +11 -11
- package/src/modes/components/extensions/inspector-panel.ts +9 -5
- package/src/modes/components/hook-selector.ts +155 -44
- package/src/modes/components/model-selector.ts +242 -49
- package/src/modes/components/notifications-settings-editor.ts +631 -85
- package/src/modes/controllers/extension-ui-controller.ts +114 -15
- package/src/modes/controllers/selector-controller.ts +525 -93
- package/src/modes/print-mode.ts +3 -0
- package/src/modes/types.ts +6 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/sdk/broker/broker.ts +54 -10
- package/src/sdk/broker/lifecycle-ledger.ts +34 -1
- package/src/sdk/broker/lifecycle.ts +47 -50
- package/src/sdk/bus/chat-daemon-cli.ts +12 -5
- package/src/sdk/bus/chat-daemon-control.ts +35 -6
- package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
- package/src/sdk/bus/config.ts +534 -286
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/discord-live-provider.ts +57 -2
- package/src/sdk/bus/discord-provider.ts +21 -0
- package/src/sdk/bus/index.ts +97 -35
- package/src/sdk/bus/notification-orchestration.ts +371 -68
- package/src/sdk/bus/notification-service.ts +571 -137
- package/src/sdk/bus/session-control.ts +119 -44
- package/src/sdk/bus/slack-live-provider.ts +78 -2
- package/src/sdk/bus/slack-provider.ts +24 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
- package/src/sdk/bus/telegram-daemon-control.ts +3 -3
- package/src/sdk/bus/telegram-daemon.ts +23 -15
- package/src/sdk/client/discovery.ts +10 -4
- package/src/sdk/host/query/handlers.ts +20 -1
- package/src/sdk/index.ts +1 -0
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/sdk/protocol/operation-registry.ts +5 -2
- package/src/sdk/providers.ts +51 -0
- package/src/sdk/session-directory.ts +1 -0
- package/src/sdk/session.ts +34 -14
- package/src/session/agent-session.ts +109 -37
- package/src/session/internal/managed-session-scope.ts +981 -450
- package/src/session/internal/managed-session-storage.ts +418 -47
- package/src/session/internal/native-publish-outcome.ts +15 -3
- package/src/session/internal/session-open-errors.ts +11 -0
- package/src/session/session-manager.ts +27 -3
- package/src/session/session-storage.ts +72 -5
- package/src/slash-commands/builtin-registry.ts +117 -15
- package/src/system-prompt.ts +28 -15
- package/src/task/index.ts +9 -3
- package/src/tools/ask.ts +1 -1
- package/src/tools/bash.ts +4 -2
- package/src/tools/composer-bash-policy.ts +21 -5
- package/src/tools/telegram-send.ts +4 -2
- package/src/utils/command-args.ts +19 -30
- package/src/utils/lang-from-path.ts +2 -4
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
wrapTextWithAnsi,
|
|
10
10
|
} from "@gajae-code/tui";
|
|
11
11
|
import type { CasReceipt } from "../../config/atomic-yaml-patch";
|
|
12
|
+
import type { NotificationProvider, ProviderSecretDisposition } from "../../sdk/bus/config";
|
|
12
13
|
import type {
|
|
13
14
|
BlockedTelegramRestoreResult,
|
|
14
15
|
ProposedTelegramIdentity,
|
|
@@ -69,6 +70,43 @@ export interface PreparedTelegramConfiguration {
|
|
|
69
70
|
streamingEnabled: boolean;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
export interface ProviderSecretActionInput {
|
|
74
|
+
action: ProviderSecretDisposition;
|
|
75
|
+
value?: SecretValue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface NotificationsProviderSetupInput {
|
|
79
|
+
provider: "discord" | "slack";
|
|
80
|
+
botToken: ProviderSecretActionInput;
|
|
81
|
+
appToken?: ProviderSecretActionInput;
|
|
82
|
+
applicationId?: string;
|
|
83
|
+
guildId?: string;
|
|
84
|
+
parentChannelId?: string;
|
|
85
|
+
workspaceId?: string;
|
|
86
|
+
channelId?: string;
|
|
87
|
+
authorizedUserId?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface PreparedNotificationProviderConfiguration {
|
|
91
|
+
provider: "discord" | "slack";
|
|
92
|
+
botTokenDisposition: ProviderSecretDisposition;
|
|
93
|
+
botTokenMask: string;
|
|
94
|
+
appTokenDisposition?: ProviderSecretDisposition;
|
|
95
|
+
appTokenMask?: string;
|
|
96
|
+
applicationId?: string;
|
|
97
|
+
guildId?: string;
|
|
98
|
+
parentChannelId?: string;
|
|
99
|
+
workspaceId?: string;
|
|
100
|
+
channelId?: string;
|
|
101
|
+
authorizedUserId?: string;
|
|
102
|
+
applicationIdDisplay?: string;
|
|
103
|
+
guildIdDisplay?: string;
|
|
104
|
+
parentChannelIdDisplay?: string;
|
|
105
|
+
workspaceIdDisplay?: string;
|
|
106
|
+
channelIdDisplay?: string;
|
|
107
|
+
authorizedUserIdDisplay?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
72
110
|
export interface NotificationsPreflightResult {
|
|
73
111
|
status: "ready" | "aborted" | "cancelled" | "error";
|
|
74
112
|
identity: ProposedTelegramIdentity;
|
|
@@ -80,6 +118,7 @@ export interface NotificationsPreflightResult {
|
|
|
80
118
|
/** A successful configuration save always carries an opaque CAS receipt. */
|
|
81
119
|
export type NotificationsConfigureCommitResult =
|
|
82
120
|
| { status: "saved"; receipt: CasReceipt; message: string }
|
|
121
|
+
| { status: "activation_failed" | "observer_failed"; receipt: CasReceipt; message: string }
|
|
83
122
|
| {
|
|
84
123
|
status: "blocked_identity";
|
|
85
124
|
receipt: CasReceipt;
|
|
@@ -91,10 +130,12 @@ export type NotificationsConfigureCommitResult =
|
|
|
91
130
|
export interface NotificationsMutationResult {
|
|
92
131
|
message: string;
|
|
93
132
|
receipt?: CasReceipt;
|
|
133
|
+
outcome?: "success" | "degraded" | "failed";
|
|
94
134
|
}
|
|
95
135
|
|
|
96
136
|
export type NotificationsSaveInactiveResult =
|
|
97
137
|
| { status: "saved_inactive"; receipt: CasReceipt; message: string }
|
|
138
|
+
| { status: "observer_failed"; receipt: CasReceipt; message: string }
|
|
98
139
|
| { status: "unavailable"; guidance: string };
|
|
99
140
|
|
|
100
141
|
/**
|
|
@@ -106,9 +147,13 @@ export type NotificationsSaveInactiveResult =
|
|
|
106
147
|
export interface NotificationsEditorOperations {
|
|
107
148
|
/** Combines a buildNotificationStatusReport-shaped status snapshot with the current session-control query. */
|
|
108
149
|
loadState(): Promise<NotificationsEditorState>;
|
|
109
|
-
/** Performs offline
|
|
110
|
-
refreshHealth(input: {
|
|
111
|
-
|
|
150
|
+
/** Performs offline or selected REST-only health refresh. */
|
|
151
|
+
refreshHealth(input: {
|
|
152
|
+
probe: boolean;
|
|
153
|
+
provider?: NotificationProvider;
|
|
154
|
+
signal?: AbortSignal;
|
|
155
|
+
}): Promise<NotificationHealthReport>;
|
|
156
|
+
sendTest(provider?: NotificationProvider): Promise<NotificationTestResult>;
|
|
112
157
|
recover(): Promise<NotificationRecoveryReport>;
|
|
113
158
|
reconnect(): Promise<TelegramDaemonReconnectOutcome>;
|
|
114
159
|
/** Validates/discovers a Telegram destination and returns a secret-safe prepared draft. */
|
|
@@ -122,6 +167,13 @@ export interface NotificationsEditorOperations {
|
|
|
122
167
|
saveInactive(draft: PreparedTelegramConfiguration): Promise<NotificationsSaveInactiveResult>;
|
|
123
168
|
/** Clears adapter-local ephemeral credential material for a prepared setup draft. Idempotent. */
|
|
124
169
|
discardConfigureDraft(draft: PreparedTelegramConfiguration): void;
|
|
170
|
+
prepareProviderConfiguration(
|
|
171
|
+
input: NotificationsProviderSetupInput,
|
|
172
|
+
): Promise<PreparedNotificationProviderConfiguration>;
|
|
173
|
+
commitProviderConfiguration(draft: PreparedNotificationProviderConfiguration): Promise<NotificationsMutationResult>;
|
|
174
|
+
discardProviderConfiguration(draft: PreparedNotificationProviderConfiguration): void;
|
|
175
|
+
setProviderDesired(provider: NotificationProvider, enabled: boolean): Promise<NotificationsMutationResult>;
|
|
176
|
+
removeProvider(provider: NotificationProvider): Promise<NotificationsMutationResult>;
|
|
125
177
|
/** Enables globally without requesting or exposing a credential. */
|
|
126
178
|
enableGlobally(): Promise<NotificationsMutationResult>;
|
|
127
179
|
disableGlobally(): Promise<NotificationsMutationResult>;
|
|
@@ -138,16 +190,49 @@ export interface NotificationsSettingsEditorCallbacks {
|
|
|
138
190
|
onCancel?: () => void;
|
|
139
191
|
}
|
|
140
192
|
|
|
141
|
-
type EditorMode =
|
|
193
|
+
export type EditorMode =
|
|
142
194
|
| "home"
|
|
143
195
|
| "provider-selection"
|
|
196
|
+
| "provider-actions"
|
|
197
|
+
| "provider-form"
|
|
198
|
+
| "provider-review"
|
|
144
199
|
| "chat-entry"
|
|
145
200
|
| "token-entry"
|
|
146
201
|
| "pairing"
|
|
147
202
|
| "review"
|
|
148
203
|
| "preferences"
|
|
149
204
|
| "confirmation";
|
|
150
|
-
type ConfirmationAction = "disable" | "remove" | "blocked_identity";
|
|
205
|
+
type ConfirmationAction = "disable" | "remove" | "remove-provider" | "blocked_identity";
|
|
206
|
+
|
|
207
|
+
type ProviderActionId =
|
|
208
|
+
| "provider-configure"
|
|
209
|
+
| "provider-toggle"
|
|
210
|
+
| "provider-probe"
|
|
211
|
+
| "provider-test"
|
|
212
|
+
| "provider-remove";
|
|
213
|
+
|
|
214
|
+
type ProviderFormStep =
|
|
215
|
+
| "bot-action"
|
|
216
|
+
| "bot-secret"
|
|
217
|
+
| "app-action"
|
|
218
|
+
| "app-secret"
|
|
219
|
+
| "application-id"
|
|
220
|
+
| "guild-id"
|
|
221
|
+
| "parent-channel-id"
|
|
222
|
+
| "workspace-id"
|
|
223
|
+
| "channel-id"
|
|
224
|
+
| "authorized-user-id";
|
|
225
|
+
|
|
226
|
+
interface ProviderFormDraft {
|
|
227
|
+
botToken?: ProviderSecretActionInput;
|
|
228
|
+
appToken?: ProviderSecretActionInput;
|
|
229
|
+
applicationId?: string;
|
|
230
|
+
guildId?: string;
|
|
231
|
+
parentChannelId?: string;
|
|
232
|
+
workspaceId?: string;
|
|
233
|
+
channelId?: string;
|
|
234
|
+
authorizedUserId?: string;
|
|
235
|
+
}
|
|
151
236
|
|
|
152
237
|
type HomeActionId =
|
|
153
238
|
| "configure"
|
|
@@ -165,7 +250,7 @@ type HomeActionId =
|
|
|
165
250
|
| "sound";
|
|
166
251
|
|
|
167
252
|
interface Action {
|
|
168
|
-
id: HomeActionId |
|
|
253
|
+
id: HomeActionId | ProviderActionId | NotificationProvider | "save" | "save-inactive" | "cancel" | "confirm";
|
|
169
254
|
label: string;
|
|
170
255
|
description: string;
|
|
171
256
|
}
|
|
@@ -180,16 +265,46 @@ function emptyState(): NotificationsEditorState {
|
|
|
180
265
|
redact: false,
|
|
181
266
|
verbosity: "lean",
|
|
182
267
|
globallyConfigured: false,
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
268
|
+
anyProviderComplete: false,
|
|
269
|
+
anyProviderEffective: false,
|
|
270
|
+
telegram: {
|
|
271
|
+
botTokenMasked: "(not set)",
|
|
272
|
+
channel: undefined,
|
|
273
|
+
configured: false,
|
|
274
|
+
quarantined: false,
|
|
275
|
+
desiredEnabled: false,
|
|
276
|
+
desiredSource: "legacy",
|
|
277
|
+
effectiveEnabled: false,
|
|
278
|
+
issues: [],
|
|
279
|
+
tokenFingerprint: undefined,
|
|
280
|
+
},
|
|
281
|
+
discord: {
|
|
282
|
+
botTokenMasked: "(not set)",
|
|
283
|
+
channel: undefined,
|
|
284
|
+
configured: false,
|
|
285
|
+
quarantined: false,
|
|
286
|
+
desiredEnabled: false,
|
|
287
|
+
desiredSource: "legacy",
|
|
288
|
+
effectiveEnabled: false,
|
|
289
|
+
issues: [],
|
|
290
|
+
},
|
|
291
|
+
slack: {
|
|
292
|
+
botTokenMasked: "(not set)",
|
|
293
|
+
channel: undefined,
|
|
294
|
+
configured: false,
|
|
295
|
+
quarantined: false,
|
|
296
|
+
desiredEnabled: false,
|
|
297
|
+
desiredSource: "legacy",
|
|
298
|
+
effectiveEnabled: false,
|
|
299
|
+
issues: [],
|
|
300
|
+
},
|
|
186
301
|
},
|
|
187
302
|
session: {
|
|
188
303
|
eligible: false,
|
|
189
304
|
locallyEnabled: false,
|
|
190
|
-
|
|
305
|
+
genericSessionEnabled: false,
|
|
306
|
+
genericEligibilitySource: "none",
|
|
191
307
|
running: false,
|
|
192
|
-
environment: "default",
|
|
193
308
|
},
|
|
194
309
|
preferences: {
|
|
195
310
|
redact: false,
|
|
@@ -208,6 +323,12 @@ function statusLabel(level: "ok" | "warn" | "error"): "OK" | "WARNING" | "ERROR"
|
|
|
208
323
|
return level === "ok" ? "OK" : level === "warn" ? "WARNING" : "ERROR";
|
|
209
324
|
}
|
|
210
325
|
|
|
326
|
+
function mutationStatus(result: NotificationsMutationResult): "OK" | "WARNING" | "ERROR" {
|
|
327
|
+
if (result.outcome === "degraded") return "WARNING";
|
|
328
|
+
if (result.outcome === "failed") return "ERROR";
|
|
329
|
+
return result.receipt || result.outcome === "success" ? "OK" : "ERROR";
|
|
330
|
+
}
|
|
331
|
+
|
|
211
332
|
function safeDetail(value: string | undefined, fallback: string): string {
|
|
212
333
|
const detail = (value ?? fallback)
|
|
213
334
|
.replace(TOKEN_PATTERN, "<redacted>")
|
|
@@ -251,6 +372,10 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
251
372
|
#status = "INFO — Loading notification status…";
|
|
252
373
|
#lastTest: NotificationTestResult | undefined;
|
|
253
374
|
#prepared: PreparedTelegramConfiguration | undefined;
|
|
375
|
+
#selectedProvider: NotificationProvider = "telegram";
|
|
376
|
+
#providerFormStep: ProviderFormStep = "bot-action";
|
|
377
|
+
#providerFormDraft: ProviderFormDraft = {};
|
|
378
|
+
#providerPrepared: PreparedNotificationProviderConfiguration | undefined;
|
|
254
379
|
#preflightIdentity: ProposedTelegramIdentity["status"] | undefined;
|
|
255
380
|
#preferencesDraft: NotificationsEditorPreferences | undefined;
|
|
256
381
|
#confirmation: ConfirmationAction | undefined;
|
|
@@ -258,6 +383,8 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
258
383
|
#pairingPhase: "discovery" | "validation" = "discovery";
|
|
259
384
|
#chatInput = new Input();
|
|
260
385
|
#tokenInput = new SecretInput();
|
|
386
|
+
#providerTextInput = new Input();
|
|
387
|
+
#providerSecretInput = new SecretInput();
|
|
261
388
|
#abortController = new AbortController();
|
|
262
389
|
#cancellableWork: Promise<void> | undefined;
|
|
263
390
|
#guarded = false;
|
|
@@ -275,6 +402,10 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
275
402
|
this.#chatInput.onEscape = () => this.#cancelCurrentMode();
|
|
276
403
|
this.#tokenInput.onSubmit = token => this.#startPreflight(token);
|
|
277
404
|
this.#tokenInput.onEscape = () => this.#cancelCurrentMode();
|
|
405
|
+
this.#providerTextInput.onSubmit = value => this.#advanceProviderText(value);
|
|
406
|
+
this.#providerTextInput.onEscape = () => this.#cancelCurrentMode();
|
|
407
|
+
this.#providerSecretInput.onSubmit = value => this.#advanceProviderSecret(value);
|
|
408
|
+
this.#providerSecretInput.onEscape = () => this.#cancelCurrentMode();
|
|
278
409
|
void this.#loadState();
|
|
279
410
|
}
|
|
280
411
|
|
|
@@ -291,17 +422,23 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
291
422
|
invalidate(): void {
|
|
292
423
|
this.#chatInput.invalidate();
|
|
293
424
|
this.#tokenInput.invalidate();
|
|
425
|
+
this.#providerTextInput.invalidate();
|
|
426
|
+
this.#providerSecretInput.invalidate();
|
|
294
427
|
}
|
|
295
428
|
|
|
296
429
|
dispose(): void {
|
|
297
|
-
if (this.#disposed
|
|
430
|
+
if (this.#disposed) return;
|
|
298
431
|
this.#disposed = true;
|
|
299
432
|
this.#abortController.abort();
|
|
300
433
|
this.#clearDrafts(true);
|
|
434
|
+
this.#clearProviderDraft(true);
|
|
301
435
|
}
|
|
302
436
|
|
|
303
437
|
render(width: number): string[] {
|
|
304
438
|
if (this.#mode === "provider-selection") return this.#renderProviderSelection(width);
|
|
439
|
+
if (this.#mode === "provider-actions") return this.#renderProviderActions(width);
|
|
440
|
+
if (this.#mode === "provider-form") return this.#renderProviderForm(width);
|
|
441
|
+
if (this.#mode === "provider-review") return this.#renderProviderReview(width);
|
|
305
442
|
if (this.#mode === "chat-entry") return this.#renderChatEntry(width);
|
|
306
443
|
if (this.#mode === "token-entry") return this.#renderTokenEntry(width);
|
|
307
444
|
if (this.#mode === "pairing") return this.#renderPairing(width);
|
|
@@ -336,6 +473,11 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
336
473
|
this.#tokenInput.handleInput(data);
|
|
337
474
|
return;
|
|
338
475
|
}
|
|
476
|
+
if (this.#mode === "provider-form") {
|
|
477
|
+
if (this.#providerStepUsesSecret()) this.#providerSecretInput.handleInput(data);
|
|
478
|
+
else this.#providerTextInput.handleInput(data);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
339
481
|
if (this.#mode === "pairing") {
|
|
340
482
|
if (this.#matchesCancel(data))
|
|
341
483
|
this.#cancelCancellableWork("Pairing cancelled; no notification configuration was changed.");
|
|
@@ -345,6 +487,14 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
345
487
|
this.#handleListInput(data, this.#providerActions());
|
|
346
488
|
return;
|
|
347
489
|
}
|
|
490
|
+
if (this.#mode === "provider-actions") {
|
|
491
|
+
this.#handleListInput(data, this.#selectedProviderActions());
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (this.#mode === "provider-review") {
|
|
495
|
+
this.#handleListInput(data, this.#providerReviewActions());
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
348
498
|
if (this.#mode === "review") {
|
|
349
499
|
this.#handleListInput(data, this.#reviewActions());
|
|
350
500
|
return;
|
|
@@ -461,14 +611,36 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
461
611
|
this.#status = "INFO — Select a notification provider to begin setup.";
|
|
462
612
|
return;
|
|
463
613
|
case "telegram":
|
|
464
|
-
|
|
614
|
+
case "discord":
|
|
615
|
+
case "slack":
|
|
616
|
+
this.#selectedProvider = id;
|
|
617
|
+
this.#mode = "provider-actions";
|
|
465
618
|
this.#selectedIndex = 0;
|
|
466
|
-
this.#
|
|
467
|
-
|
|
468
|
-
|
|
619
|
+
this.#status = `INFO — ${id} provider actions.`;
|
|
620
|
+
return;
|
|
621
|
+
case "provider-configure":
|
|
622
|
+
if (this.#selectedProvider === "telegram") {
|
|
623
|
+
this.#mode = "chat-entry";
|
|
624
|
+
this.#selectedIndex = 0;
|
|
625
|
+
this.#chatInput.setValue("");
|
|
626
|
+
this.#tokenInput.clear();
|
|
627
|
+
this.#status = "INFO — Enter a private chat ID, or leave it blank for guided pairing discovery.";
|
|
628
|
+
} else {
|
|
629
|
+
this.#startProviderForm();
|
|
630
|
+
}
|
|
469
631
|
return;
|
|
470
|
-
case "
|
|
471
|
-
this.#
|
|
632
|
+
case "provider-toggle":
|
|
633
|
+
this.#setSelectedProviderDesired();
|
|
634
|
+
return;
|
|
635
|
+
case "provider-probe":
|
|
636
|
+
this.#refreshHealth(true, this.#selectedProvider);
|
|
637
|
+
return;
|
|
638
|
+
case "provider-test":
|
|
639
|
+
this.#sendTest(this.#selectedProvider);
|
|
640
|
+
return;
|
|
641
|
+
case "provider-remove":
|
|
642
|
+
if (this.#selectedProvider === "telegram") this.#openConfirmation("remove");
|
|
643
|
+
else this.#openConfirmation("remove-provider");
|
|
472
644
|
return;
|
|
473
645
|
case "enable":
|
|
474
646
|
this.#enableGlobally();
|
|
@@ -504,7 +676,8 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
504
676
|
this.#status = "INFO — Edit the draft, then explicitly save it atomically.";
|
|
505
677
|
return;
|
|
506
678
|
case "save":
|
|
507
|
-
this.#
|
|
679
|
+
if (this.#mode === "provider-review") this.#commitProviderConfiguration();
|
|
680
|
+
else this.#commitConfiguration();
|
|
508
681
|
return;
|
|
509
682
|
case "save-inactive":
|
|
510
683
|
this.#saveInactive();
|
|
@@ -527,6 +700,7 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
527
700
|
return;
|
|
528
701
|
}
|
|
529
702
|
this.#clearDrafts();
|
|
703
|
+
this.#clearProviderDraft();
|
|
530
704
|
this.#mode = "home";
|
|
531
705
|
this.#selectedIndex = 0;
|
|
532
706
|
this.#status = "INFO — Setup cancelled; saved notification configuration is unchanged.";
|
|
@@ -543,6 +717,163 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
543
717
|
if (disposeSecret) this.#tokenInput.dispose();
|
|
544
718
|
}
|
|
545
719
|
|
|
720
|
+
#clearProviderDraft(disposeSecret = false): void {
|
|
721
|
+
if (this.#providerPrepared) this.operations.discardProviderConfiguration(this.#providerPrepared);
|
|
722
|
+
this.#providerPrepared = undefined;
|
|
723
|
+
this.#providerFormDraft.botToken?.value?.consume();
|
|
724
|
+
this.#providerFormDraft.appToken?.value?.consume();
|
|
725
|
+
this.#providerFormDraft = {};
|
|
726
|
+
this.#providerFormStep = "bot-action";
|
|
727
|
+
this.#providerTextInput.setValue("");
|
|
728
|
+
this.#providerSecretInput.clear();
|
|
729
|
+
if (disposeSecret) this.#providerSecretInput.dispose();
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
#startProviderForm(): void {
|
|
733
|
+
this.#clearProviderDraft();
|
|
734
|
+
this.#mode = "provider-form";
|
|
735
|
+
this.#providerFormStep = "bot-action";
|
|
736
|
+
this.#providerTextInput.setValue("");
|
|
737
|
+
this.#status = `INFO — Choose ${this.#selectedProvider} bot token action: keep, replace, or remove.`;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
#providerStepUsesSecret(): boolean {
|
|
741
|
+
return this.#providerFormStep === "bot-secret" || this.#providerFormStep === "app-secret";
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
#normalizedSecretAction(value: string): ProviderSecretDisposition | undefined {
|
|
745
|
+
const action = value.trim().toLowerCase();
|
|
746
|
+
return action === "keep" || action === "replace" || action === "remove" ? action : undefined;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
#advanceProviderText(value: string): void {
|
|
750
|
+
const trimmed = value.trim();
|
|
751
|
+
this.#providerTextInput.setValue("");
|
|
752
|
+
switch (this.#providerFormStep) {
|
|
753
|
+
case "bot-action": {
|
|
754
|
+
const action = this.#normalizedSecretAction(trimmed);
|
|
755
|
+
if (!action) {
|
|
756
|
+
this.#status = "ERROR — Enter keep, replace, or remove for the bot token.";
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
this.#providerFormDraft.botToken = { action };
|
|
760
|
+
this.#providerFormStep =
|
|
761
|
+
action === "replace"
|
|
762
|
+
? "bot-secret"
|
|
763
|
+
: this.#selectedProvider === "slack"
|
|
764
|
+
? "app-action"
|
|
765
|
+
: "application-id";
|
|
766
|
+
this.#status =
|
|
767
|
+
action === "replace"
|
|
768
|
+
? "INFO — Enter the replacement bot token; only bullets are rendered."
|
|
769
|
+
: this.#selectedProvider === "slack"
|
|
770
|
+
? "INFO — Choose Slack app token action: keep, replace, or remove."
|
|
771
|
+
: "INFO — Enter the Discord application ID; blank keeps the stored value.";
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
case "app-action": {
|
|
775
|
+
const action = this.#normalizedSecretAction(trimmed);
|
|
776
|
+
if (!action) {
|
|
777
|
+
this.#status = "ERROR — Enter keep, replace, or remove for the Slack app token.";
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
this.#providerFormDraft.appToken = { action };
|
|
781
|
+
this.#providerFormStep = action === "replace" ? "app-secret" : "workspace-id";
|
|
782
|
+
this.#status =
|
|
783
|
+
action === "replace"
|
|
784
|
+
? "INFO — Enter the replacement Slack app token; only bullets are rendered."
|
|
785
|
+
: "INFO — Enter the Slack workspace ID; blank keeps the stored value.";
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
case "application-id":
|
|
789
|
+
this.#providerFormDraft.applicationId = trimmed || undefined;
|
|
790
|
+
this.#providerFormStep = "guild-id";
|
|
791
|
+
this.#status = "INFO — Enter the Discord guild ID; blank keeps the stored value.";
|
|
792
|
+
return;
|
|
793
|
+
case "guild-id":
|
|
794
|
+
this.#providerFormDraft.guildId = trimmed || undefined;
|
|
795
|
+
this.#providerFormStep = "parent-channel-id";
|
|
796
|
+
this.#status = "INFO — Enter the Discord parent channel ID; blank keeps the stored value.";
|
|
797
|
+
return;
|
|
798
|
+
case "parent-channel-id":
|
|
799
|
+
this.#providerFormDraft.parentChannelId = trimmed || undefined;
|
|
800
|
+
this.#prepareProviderDraft();
|
|
801
|
+
return;
|
|
802
|
+
case "workspace-id":
|
|
803
|
+
this.#providerFormDraft.workspaceId = trimmed || undefined;
|
|
804
|
+
this.#providerFormStep = "channel-id";
|
|
805
|
+
this.#status = "INFO — Enter the Slack channel ID; blank keeps the stored value.";
|
|
806
|
+
return;
|
|
807
|
+
case "channel-id":
|
|
808
|
+
this.#providerFormDraft.channelId = trimmed || undefined;
|
|
809
|
+
this.#providerFormStep = "authorized-user-id";
|
|
810
|
+
this.#status = "INFO — Enter the optional authorized user ID; blank keeps the stored value.";
|
|
811
|
+
return;
|
|
812
|
+
case "authorized-user-id":
|
|
813
|
+
this.#providerFormDraft.authorizedUserId = trimmed || undefined;
|
|
814
|
+
this.#prepareProviderDraft();
|
|
815
|
+
return;
|
|
816
|
+
case "bot-secret":
|
|
817
|
+
case "app-secret":
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
#advanceProviderSecret(value: SecretValue): void {
|
|
823
|
+
this.#providerSecretInput.clear();
|
|
824
|
+
if (this.#providerFormStep === "bot-secret") {
|
|
825
|
+
this.#providerFormDraft.botToken = { action: "replace", value };
|
|
826
|
+
this.#providerFormStep = this.#selectedProvider === "slack" ? "app-action" : "application-id";
|
|
827
|
+
this.#status =
|
|
828
|
+
this.#selectedProvider === "slack"
|
|
829
|
+
? "INFO — Choose Slack app token action: keep, replace, or remove."
|
|
830
|
+
: "INFO — Enter the Discord application ID; blank keeps the stored value.";
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
if (this.#providerFormStep === "app-secret") {
|
|
834
|
+
this.#providerFormDraft.appToken = { action: "replace", value };
|
|
835
|
+
this.#providerFormStep = "workspace-id";
|
|
836
|
+
this.#status = "INFO — Enter the Slack workspace ID; blank keeps the stored value.";
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
#prepareProviderDraft(): void {
|
|
841
|
+
const botToken = this.#providerFormDraft.botToken;
|
|
842
|
+
if (!botToken || (this.#selectedProvider === "slack" && !this.#providerFormDraft.appToken)) {
|
|
843
|
+
this.#status = "ERROR — Provider secret actions are incomplete.";
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
const input: NotificationsProviderSetupInput =
|
|
847
|
+
this.#selectedProvider === "discord"
|
|
848
|
+
? {
|
|
849
|
+
provider: "discord",
|
|
850
|
+
botToken,
|
|
851
|
+
applicationId: this.#providerFormDraft.applicationId,
|
|
852
|
+
guildId: this.#providerFormDraft.guildId,
|
|
853
|
+
parentChannelId: this.#providerFormDraft.parentChannelId,
|
|
854
|
+
}
|
|
855
|
+
: {
|
|
856
|
+
provider: "slack",
|
|
857
|
+
botToken,
|
|
858
|
+
appToken: this.#providerFormDraft.appToken,
|
|
859
|
+
workspaceId: this.#providerFormDraft.workspaceId,
|
|
860
|
+
channelId: this.#providerFormDraft.channelId,
|
|
861
|
+
authorizedUserId: this.#providerFormDraft.authorizedUserId,
|
|
862
|
+
};
|
|
863
|
+
this.#runGuarded(
|
|
864
|
+
`Preparing ${this.#selectedProvider} configuration.`,
|
|
865
|
+
() => this.operations.prepareProviderConfiguration(input),
|
|
866
|
+
draft => {
|
|
867
|
+
this.#providerFormDraft = {};
|
|
868
|
+
this.#providerPrepared = draft;
|
|
869
|
+
this.#mode = "provider-review";
|
|
870
|
+
this.#selectedIndex = 0;
|
|
871
|
+
this.#status = `INFO — Review secret-safe ${draft.provider} configuration before saving.`;
|
|
872
|
+
},
|
|
873
|
+
draft => this.operations.discardProviderConfiguration(draft),
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
|
|
546
877
|
#startPreflight(token: SecretValue): void {
|
|
547
878
|
const chatId = this.#chatInput.getValue().trim() || undefined;
|
|
548
879
|
const preferences = this.#state.preferences;
|
|
@@ -621,11 +952,11 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
621
952
|
this.#cancellableWork = pending;
|
|
622
953
|
}
|
|
623
954
|
|
|
624
|
-
#refreshHealth(probe: boolean): void {
|
|
955
|
+
#refreshHealth(probe: boolean, provider?: NotificationProvider): void {
|
|
625
956
|
if (probe) {
|
|
626
957
|
this.#runGuarded(
|
|
627
958
|
"Health probe in progress. It cannot be cancelled once started.",
|
|
628
|
-
() => this.operations.refreshHealth({ probe: true }),
|
|
959
|
+
() => this.operations.refreshHealth({ probe: true, provider }),
|
|
629
960
|
health => {
|
|
630
961
|
this.#state = { ...this.#state, health };
|
|
631
962
|
this.#status = `${statusLabel(health.overall)} — ${this.#healthSummary(health)}`;
|
|
@@ -635,7 +966,7 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
635
966
|
}
|
|
636
967
|
this.#status = "PENDING — Refreshing notification health.";
|
|
637
968
|
this.#runCancellable(
|
|
638
|
-
signal => this.operations.refreshHealth({ probe: false, signal }),
|
|
969
|
+
signal => this.operations.refreshHealth({ probe: false, provider, signal }),
|
|
639
970
|
health => {
|
|
640
971
|
this.#state = { ...this.#state, health };
|
|
641
972
|
this.#status = `${statusLabel(health.overall)} — ${this.#healthSummary(health)}`;
|
|
@@ -643,14 +974,22 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
643
974
|
);
|
|
644
975
|
}
|
|
645
976
|
|
|
646
|
-
#runGuarded<T>(
|
|
977
|
+
#runGuarded<T>(
|
|
978
|
+
pending: string,
|
|
979
|
+
work: () => Promise<T>,
|
|
980
|
+
complete: (result: T) => Promise<void> | void,
|
|
981
|
+
disposeResult?: (result: T) => Promise<void> | void,
|
|
982
|
+
): void {
|
|
647
983
|
if (this.#guarded || this.#disposed) return;
|
|
648
984
|
this.#guarded = true;
|
|
649
985
|
this.#status = `PENDING — ${pending} Navigation is locked; it may already have started or delivered.`;
|
|
650
986
|
void (async () => {
|
|
651
987
|
try {
|
|
652
988
|
const result = await work();
|
|
653
|
-
if (this.#disposed)
|
|
989
|
+
if (this.#disposed) {
|
|
990
|
+
await disposeResult?.(result);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
654
993
|
await complete(result);
|
|
655
994
|
} catch {
|
|
656
995
|
if (!this.#disposed) {
|
|
@@ -663,20 +1002,32 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
663
1002
|
}
|
|
664
1003
|
|
|
665
1004
|
async #afterDurableMutation(): Promise<boolean> {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
1005
|
+
try {
|
|
1006
|
+
await this.operations.reconcileCurrentSession();
|
|
1007
|
+
const health = await this.operations.refreshHealth({ probe: false });
|
|
1008
|
+
if (this.#disposed) return false;
|
|
1009
|
+
this.#state = { ...this.#state, health };
|
|
1010
|
+
await this.#loadState();
|
|
1011
|
+
return !this.#disposed;
|
|
1012
|
+
} catch {
|
|
1013
|
+
if (!this.#disposed) {
|
|
1014
|
+
this.#status = "WARNING — Configuration was saved, but post-commit reconciliation or refresh failed.";
|
|
1015
|
+
}
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
672
1018
|
}
|
|
673
1019
|
|
|
674
1020
|
async #refreshAfterOperation(): Promise<boolean> {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
1021
|
+
try {
|
|
1022
|
+
const health = await this.operations.refreshHealth({ probe: false });
|
|
1023
|
+
if (this.#disposed) return false;
|
|
1024
|
+
this.#state = { ...this.#state, health };
|
|
1025
|
+
await this.#loadState();
|
|
1026
|
+
return !this.#disposed;
|
|
1027
|
+
} catch {
|
|
1028
|
+
if (!this.#disposed) this.#status = "WARNING — The operation completed, but state refresh failed.";
|
|
1029
|
+
return false;
|
|
1030
|
+
}
|
|
680
1031
|
}
|
|
681
1032
|
|
|
682
1033
|
#enableGlobally(): void {
|
|
@@ -686,7 +1037,56 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
686
1037
|
async result => {
|
|
687
1038
|
if (!(await this.#afterDurableMutation())) return;
|
|
688
1039
|
|
|
689
|
-
this.#status =
|
|
1040
|
+
this.#status = `${mutationStatus(result)} — ${safeDetail(result.message, "Global notifications enabled using stored credentials.")}`;
|
|
1041
|
+
},
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
#setSelectedProviderDesired(): void {
|
|
1046
|
+
const view = this.#state.status[this.#selectedProvider];
|
|
1047
|
+
const enabled = !view.desiredEnabled;
|
|
1048
|
+
this.#runGuarded(
|
|
1049
|
+
`${enabled ? "Enabling" : "Disabling"} ${this.#selectedProvider} desired intent.`,
|
|
1050
|
+
() => this.operations.setProviderDesired(this.#selectedProvider, enabled),
|
|
1051
|
+
async result => {
|
|
1052
|
+
if (!(await this.#afterDurableMutation())) return;
|
|
1053
|
+
this.#mode = "provider-actions";
|
|
1054
|
+
this.#selectedIndex = 0;
|
|
1055
|
+
this.#status = `${mutationStatus(result)} — ${safeDetail(result.message, "Provider intent updated.")}`;
|
|
1056
|
+
},
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
#commitProviderConfiguration(): void {
|
|
1061
|
+
const draft = this.#providerPrepared;
|
|
1062
|
+
if (!draft) {
|
|
1063
|
+
this.#mode = "provider-actions";
|
|
1064
|
+
this.#status = "ERROR — The provider draft expired; re-enter secret actions.";
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
this.#runGuarded(
|
|
1068
|
+
`Saving ${draft.provider} configuration.`,
|
|
1069
|
+
() => this.operations.commitProviderConfiguration(draft),
|
|
1070
|
+
async result => {
|
|
1071
|
+
this.#providerPrepared = undefined;
|
|
1072
|
+
if (!(await this.#afterDurableMutation())) return;
|
|
1073
|
+
this.#mode = "provider-actions";
|
|
1074
|
+
this.#selectedIndex = 0;
|
|
1075
|
+
this.#status = `${mutationStatus(result)} — ${safeDetail(result.message, "Provider configuration updated.")}`;
|
|
1076
|
+
},
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
#removeSelectedProvider(): void {
|
|
1081
|
+
const provider = this.#selectedProvider;
|
|
1082
|
+
this.#runGuarded(
|
|
1083
|
+
`Removing ${provider} configuration.`,
|
|
1084
|
+
() => this.operations.removeProvider(provider),
|
|
1085
|
+
async result => {
|
|
1086
|
+
if (!(await this.#afterDurableMutation())) return;
|
|
1087
|
+
this.#mode = "home";
|
|
1088
|
+
this.#selectedIndex = 0;
|
|
1089
|
+
this.#status = `${mutationStatus(result)} — ${safeDetail(result.message, "Provider removal completed.")}`;
|
|
690
1090
|
},
|
|
691
1091
|
);
|
|
692
1092
|
}
|
|
@@ -702,7 +1102,7 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
702
1102
|
if (!(await this.#afterDurableMutation())) return;
|
|
703
1103
|
this.#mode = "home";
|
|
704
1104
|
this.#selectedIndex = 0;
|
|
705
|
-
this.#status =
|
|
1105
|
+
this.#status = `${mutationStatus(result)} — ${safeDetail(result.message, "Notifications disabled globally.")}`;
|
|
706
1106
|
},
|
|
707
1107
|
);
|
|
708
1108
|
return;
|
|
@@ -715,15 +1115,14 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
715
1115
|
if (!(await this.#afterDurableMutation())) return;
|
|
716
1116
|
this.#mode = "home";
|
|
717
1117
|
this.#selectedIndex = 0;
|
|
718
|
-
this.#status =
|
|
1118
|
+
this.#status = `${mutationStatus(result)} — ${safeDetail(
|
|
719
1119
|
result.message,
|
|
720
|
-
|
|
721
|
-
? "Telegram removed and global notifications disabled."
|
|
722
|
-
: "Telegram removed; other adapters remain enabled.",
|
|
1120
|
+
"Telegram removed without changing the global master or sibling providers.",
|
|
723
1121
|
)}`;
|
|
724
1122
|
},
|
|
725
1123
|
);
|
|
726
1124
|
}
|
|
1125
|
+
if (action === "remove-provider") this.#removeSelectedProvider();
|
|
727
1126
|
}
|
|
728
1127
|
|
|
729
1128
|
#restoreBlockedConfiguration(): void {
|
|
@@ -782,8 +1181,10 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
782
1181
|
this.#selectedIndex = 1;
|
|
783
1182
|
this.#status =
|
|
784
1183
|
action === "disable"
|
|
785
|
-
? "WARNING — Disable globally stops
|
|
786
|
-
:
|
|
1184
|
+
? "WARNING — Disable globally preserves provider configuration and desired intent but stops active runtimes. Confirm explicitly."
|
|
1185
|
+
: action === "remove-provider"
|
|
1186
|
+
? `WARNING — Remove only ${this.#selectedProvider} configuration and set its desired intent off. Siblings remain unchanged.`
|
|
1187
|
+
: "WARNING — Remove only Telegram credentials. Discord and Slack remain unchanged.";
|
|
787
1188
|
}
|
|
788
1189
|
|
|
789
1190
|
#setSessionLocal(enabled: boolean): void {
|
|
@@ -804,15 +1205,15 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
804
1205
|
);
|
|
805
1206
|
}
|
|
806
1207
|
|
|
807
|
-
#sendTest(): void {
|
|
1208
|
+
#sendTest(provider?: NotificationProvider): void {
|
|
808
1209
|
this.#runGuarded(
|
|
809
1210
|
"Sending a notification test.",
|
|
810
|
-
() => this.operations.sendTest(),
|
|
1211
|
+
() => this.operations.sendTest(provider),
|
|
811
1212
|
async result => {
|
|
812
1213
|
this.#lastTest = result;
|
|
813
1214
|
if (!(await this.#refreshAfterOperation())) return;
|
|
814
1215
|
|
|
815
|
-
this.#status = `${result.ok ? "OK" : "ERROR"} — Test ${result.ok ? "delivered" : "failed"}: ${safeDetail(
|
|
1216
|
+
this.#status = `${result.ok ? "OK" : result.uncertain ? "WARNING" : "ERROR"} — Test ${result.ok ? "delivered" : result.uncertain ? "outcome uncertain" : "failed"}: ${safeDetail(
|
|
816
1217
|
result.detail,
|
|
817
1218
|
"No delivery detail returned.",
|
|
818
1219
|
)}`;
|
|
@@ -864,7 +1265,6 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
864
1265
|
this.#clearDrafts();
|
|
865
1266
|
if (result.status === "blocked_identity") {
|
|
866
1267
|
this.#blockedCommit = result;
|
|
867
|
-
if (!(await this.#refreshAfterOperation())) return;
|
|
868
1268
|
this.#confirmation = "blocked_identity";
|
|
869
1269
|
this.#mode = "confirmation";
|
|
870
1270
|
this.#selectedIndex = 0;
|
|
@@ -872,6 +1272,14 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
872
1272
|
result.message,
|
|
873
1273
|
"Configuration saved but activation blocked by a foreign daemon.",
|
|
874
1274
|
)}`;
|
|
1275
|
+
await this.#refreshAfterOperation();
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
if (result.status === "activation_failed" || result.status === "observer_failed") {
|
|
1279
|
+
if (!(await this.#afterDurableMutation())) return;
|
|
1280
|
+
this.#mode = "home";
|
|
1281
|
+
this.#selectedIndex = 0;
|
|
1282
|
+
this.#status = `WARNING — ${safeDetail(result.message, "Telegram configuration was saved with a post-commit failure.")}`;
|
|
875
1283
|
return;
|
|
876
1284
|
}
|
|
877
1285
|
if (!(await this.#afterDurableMutation())) return;
|
|
@@ -899,7 +1307,7 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
899
1307
|
this.#clearDrafts();
|
|
900
1308
|
this.#mode = "home";
|
|
901
1309
|
this.#selectedIndex = 0;
|
|
902
|
-
this.#status =
|
|
1310
|
+
this.#status = `${result.status === "observer_failed" ? "WARNING" : "OK"} — ${safeDetail(result.message, "Telegram configuration saved inactive; foreign daemon untouched.")}`;
|
|
903
1311
|
},
|
|
904
1312
|
);
|
|
905
1313
|
}
|
|
@@ -958,20 +1366,62 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
958
1366
|
{
|
|
959
1367
|
id: "telegram",
|
|
960
1368
|
label: "Telegram",
|
|
961
|
-
description: "Configure
|
|
1369
|
+
description: "Configure pairing, desired intent, health, tests, and runtime state.",
|
|
962
1370
|
},
|
|
963
1371
|
{
|
|
964
|
-
id: "
|
|
965
|
-
label: "Discord
|
|
966
|
-
description:
|
|
967
|
-
"Discord credentials are configured by the Discord provider integration, not this Telegram setup flow.",
|
|
1372
|
+
id: "discord",
|
|
1373
|
+
label: "Discord",
|
|
1374
|
+
description: "Configure the bot token and Discord routing IDs in this Settings surface.",
|
|
968
1375
|
},
|
|
969
1376
|
{
|
|
970
|
-
id: "
|
|
971
|
-
label: "Slack
|
|
1377
|
+
id: "slack",
|
|
1378
|
+
label: "Slack",
|
|
1379
|
+
description: "Configure bot/app tokens and Slack routing IDs in this Settings surface.",
|
|
1380
|
+
},
|
|
1381
|
+
];
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
#selectedProviderActions(): readonly Action[] {
|
|
1385
|
+
const view = this.#state.status[this.#selectedProvider];
|
|
1386
|
+
return [
|
|
1387
|
+
{
|
|
1388
|
+
id: "provider-configure",
|
|
1389
|
+
label: view.quarantined
|
|
1390
|
+
? `Repair ${this.#selectedProvider}`
|
|
1391
|
+
: view.configured
|
|
1392
|
+
? `Edit ${this.#selectedProvider}`
|
|
1393
|
+
: `Configure ${this.#selectedProvider}`,
|
|
1394
|
+
description: "Use explicit keep, replace, or remove secret actions and selected-provider CAS persistence.",
|
|
1395
|
+
},
|
|
1396
|
+
{
|
|
1397
|
+
id: "provider-toggle",
|
|
1398
|
+
label: `${view.desiredEnabled ? "Disable" : "Enable"} ${this.#selectedProvider} desired intent`,
|
|
1399
|
+
description: "Changes only this provider's durable desired intent; the global master stays separate.",
|
|
1400
|
+
},
|
|
1401
|
+
{
|
|
1402
|
+
id: "provider-probe",
|
|
1403
|
+
label: `Probe ${this.#selectedProvider}`,
|
|
1404
|
+
description: "Run the selected provider's REST-only diagnostic without opening a socket transport.",
|
|
1405
|
+
},
|
|
1406
|
+
{
|
|
1407
|
+
id: "provider-test",
|
|
1408
|
+
label: `Test ${this.#selectedProvider}`,
|
|
1409
|
+
description: "Send one selected-provider test and report delivered, failed, or uncertain truthfully.",
|
|
1410
|
+
},
|
|
1411
|
+
{
|
|
1412
|
+
id: "provider-remove",
|
|
1413
|
+
label: `Remove ${this.#selectedProvider}`,
|
|
972
1414
|
description:
|
|
973
|
-
"
|
|
1415
|
+
"Remove only this provider after explicit confirmation; siblings and the global master remain unchanged.",
|
|
974
1416
|
},
|
|
1417
|
+
{ id: "cancel", label: "Back to Notifications", description: "Return without changing settings." },
|
|
1418
|
+
];
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
#providerReviewActions(): readonly Action[] {
|
|
1422
|
+
return [
|
|
1423
|
+
{ id: "save", label: "Save and activate", description: "Commit one selected-provider batch, then activate." },
|
|
1424
|
+
{ id: "cancel", label: "Discard draft", description: "Clear all transient secret capabilities." },
|
|
975
1425
|
];
|
|
976
1426
|
}
|
|
977
1427
|
|
|
@@ -979,9 +1429,8 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
979
1429
|
return [
|
|
980
1430
|
{
|
|
981
1431
|
id: "configure",
|
|
982
|
-
label:
|
|
983
|
-
description:
|
|
984
|
-
"Enter a masked Telegram credential and optionally a private-chat destination; guided discovery is available.",
|
|
1432
|
+
label: "Configure notification providers",
|
|
1433
|
+
description: "Set up or repair Telegram, Discord, and Slack.",
|
|
985
1434
|
},
|
|
986
1435
|
{
|
|
987
1436
|
id: "enable",
|
|
@@ -1008,12 +1457,12 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
1008
1457
|
{
|
|
1009
1458
|
id: "probe",
|
|
1010
1459
|
label: "Probe health",
|
|
1011
|
-
description: "
|
|
1460
|
+
description: "Infer exactly one effective provider or require an explicit provider selection.",
|
|
1012
1461
|
},
|
|
1013
1462
|
{
|
|
1014
1463
|
id: "test",
|
|
1015
1464
|
label: "Send test notification",
|
|
1016
|
-
description: "
|
|
1465
|
+
description: "Infer exactly one effective provider or require an explicit provider selection.",
|
|
1017
1466
|
},
|
|
1018
1467
|
{
|
|
1019
1468
|
id: "recover",
|
|
@@ -1119,7 +1568,7 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
1119
1568
|
const lines = [theme.bold(theme.fg("accent", "Choose a notification provider"))];
|
|
1120
1569
|
this.#appendWrapped(
|
|
1121
1570
|
lines,
|
|
1122
|
-
"Select
|
|
1571
|
+
"Select Telegram, Discord, or Slack for configuration, desired intent, health, test, and removal actions.",
|
|
1123
1572
|
width,
|
|
1124
1573
|
"muted",
|
|
1125
1574
|
);
|
|
@@ -1128,6 +1577,95 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
1128
1577
|
return lines;
|
|
1129
1578
|
}
|
|
1130
1579
|
|
|
1580
|
+
#renderProviderActions(width: number): string[] {
|
|
1581
|
+
const view = this.#state.status[this.#selectedProvider];
|
|
1582
|
+
const lines = [theme.bold(theme.fg("accent", `${this.#selectedProvider} notification settings`))];
|
|
1583
|
+
this.#appendWrapped(
|
|
1584
|
+
lines,
|
|
1585
|
+
`configured: ${view.configured ? "yes" : "no"} · repair: ${view.quarantined ? "required" : "no"} · desired: ${view.desiredEnabled ? "on" : "off"} · effective: ${view.effectiveEnabled ? "yes" : "no"} · destination: ${view.channel ?? "(unset)"}`,
|
|
1586
|
+
width,
|
|
1587
|
+
"muted",
|
|
1588
|
+
);
|
|
1589
|
+
if (view.issues.length > 0) {
|
|
1590
|
+
this.#appendWrapped(
|
|
1591
|
+
lines,
|
|
1592
|
+
`issues: ${view.issues.map(issue => `${issue.path}:${issue.code}`).join(", ")}`,
|
|
1593
|
+
width,
|
|
1594
|
+
"muted",
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
lines.push("");
|
|
1598
|
+
this.#renderActionList(lines, width, this.#selectedProviderActions());
|
|
1599
|
+
return lines;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
#providerFormPrompt(): string {
|
|
1603
|
+
switch (this.#providerFormStep) {
|
|
1604
|
+
case "bot-action":
|
|
1605
|
+
return "Bot token action (keep | replace | remove)";
|
|
1606
|
+
case "bot-secret":
|
|
1607
|
+
return "Replacement bot token";
|
|
1608
|
+
case "app-action":
|
|
1609
|
+
return "Slack app token action (keep | replace | remove)";
|
|
1610
|
+
case "app-secret":
|
|
1611
|
+
return "Replacement Slack app token";
|
|
1612
|
+
case "application-id":
|
|
1613
|
+
return "Discord application ID (blank keeps stored)";
|
|
1614
|
+
case "guild-id":
|
|
1615
|
+
return "Discord guild ID (blank keeps stored)";
|
|
1616
|
+
case "parent-channel-id":
|
|
1617
|
+
return "Discord parent channel ID (blank keeps stored)";
|
|
1618
|
+
case "workspace-id":
|
|
1619
|
+
return "Slack workspace ID (blank keeps stored)";
|
|
1620
|
+
case "channel-id":
|
|
1621
|
+
return "Slack channel ID (blank keeps stored)";
|
|
1622
|
+
case "authorized-user-id":
|
|
1623
|
+
return "Slack authorized user ID (optional; blank keeps stored)";
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
#renderProviderForm(width: number): string[] {
|
|
1628
|
+
const lines = [theme.bold(theme.fg("accent", `${this.#selectedProvider} configuration`))];
|
|
1629
|
+
this.#appendWrapped(lines, this.#providerFormPrompt(), width, "muted");
|
|
1630
|
+
lines.push("");
|
|
1631
|
+
if (this.#providerStepUsesSecret()) {
|
|
1632
|
+
this.#providerSecretInput.focused = this.focused;
|
|
1633
|
+
for (const line of this.#providerSecretInput.render(width)) lines.push(line);
|
|
1634
|
+
} else {
|
|
1635
|
+
this.#providerTextInput.focused = this.focused;
|
|
1636
|
+
for (const line of this.#providerTextInput.render(width)) lines.push(line);
|
|
1637
|
+
}
|
|
1638
|
+
lines.push("");
|
|
1639
|
+
this.#appendStatus(lines, width);
|
|
1640
|
+
lines.push(theme.fg("dim", " Enter to continue · Esc to discard all transient provider secrets"));
|
|
1641
|
+
return lines;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
#renderProviderReview(width: number): string[] {
|
|
1645
|
+
const draft = this.#providerPrepared;
|
|
1646
|
+
const lines = [
|
|
1647
|
+
theme.bold(theme.fg("accent", `Review ${draft?.provider ?? this.#selectedProvider} configuration`)),
|
|
1648
|
+
];
|
|
1649
|
+
if (draft) {
|
|
1650
|
+
lines.push(
|
|
1651
|
+
this.#truncate(
|
|
1652
|
+
` bot token: ${draft.botTokenDisposition} ${maskedToken(draft.botTokenMask, "(unset)")}${draft.appTokenDisposition ? ` · app token: ${draft.appTokenDisposition} ${maskedToken(draft.appTokenMask, "(unset)")}` : ""}`,
|
|
1653
|
+
width,
|
|
1654
|
+
),
|
|
1655
|
+
);
|
|
1656
|
+
const routing =
|
|
1657
|
+
draft.provider === "discord"
|
|
1658
|
+
? `application ${draft.applicationIdDisplay ?? "(unset)"} · guild ${draft.guildIdDisplay ?? "(unset)"} · parent ${draft.parentChannelIdDisplay ?? "(unset)"}`
|
|
1659
|
+
: `workspace ${draft.workspaceIdDisplay ?? "(unset)"} · channel ${draft.channelIdDisplay ?? "(unset)"} · authorized user ${draft.authorizedUserIdDisplay ?? "(unset)"}`;
|
|
1660
|
+
lines.push(this.#truncate(` ${routing}`, width));
|
|
1661
|
+
}
|
|
1662
|
+
lines.push("");
|
|
1663
|
+
this.#appendStatus(lines, width);
|
|
1664
|
+
lines.push("");
|
|
1665
|
+
this.#renderActionList(lines, width, this.#providerReviewActions());
|
|
1666
|
+
return lines;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1131
1669
|
#renderChatEntry(width: number): string[] {
|
|
1132
1670
|
const lines = [theme.bold(theme.fg("accent", "Telegram setup — private chat ID (optional)"))];
|
|
1133
1671
|
this.#appendWrapped(
|
|
@@ -1222,14 +1760,15 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
1222
1760
|
#renderConfirmation(width: number): string[] {
|
|
1223
1761
|
const blocked = this.#confirmation === "blocked_identity";
|
|
1224
1762
|
const removing = this.#confirmation === "remove";
|
|
1763
|
+
const removingProvider = this.#confirmation === "remove-provider";
|
|
1225
1764
|
const lines = [
|
|
1226
1765
|
theme.bold(
|
|
1227
1766
|
theme.fg(
|
|
1228
1767
|
"accent",
|
|
1229
1768
|
blocked
|
|
1230
1769
|
? "Telegram activation blocked by foreign daemon"
|
|
1231
|
-
: removing
|
|
1232
|
-
?
|
|
1770
|
+
: removing || removingProvider
|
|
1771
|
+
? `Remove ${removingProvider ? this.#selectedProvider : "Telegram"} configuration?`
|
|
1233
1772
|
: "Disable notifications globally?",
|
|
1234
1773
|
),
|
|
1235
1774
|
),
|
|
@@ -1238,9 +1777,9 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
1238
1777
|
lines,
|
|
1239
1778
|
blocked
|
|
1240
1779
|
? "Configuration saved but activation blocked by a foreign daemon. Restore the CAS-protected previous configuration, or keep the saved configuration inactive."
|
|
1241
|
-
: removing
|
|
1242
|
-
?
|
|
1243
|
-
: "This disables
|
|
1780
|
+
: removing || removingProvider
|
|
1781
|
+
? `This removes only ${removingProvider ? this.#selectedProvider : "Telegram"} configuration and sets that provider's desired intent off. Siblings and the global master remain unchanged.`
|
|
1782
|
+
: "This disables the global master while preserving every provider's configuration and desired intent.",
|
|
1244
1783
|
width,
|
|
1245
1784
|
"muted",
|
|
1246
1785
|
);
|
|
@@ -1274,30 +1813,36 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
1274
1813
|
const lines = [theme.bold(theme.fg("accent", "Notifications"))];
|
|
1275
1814
|
lines.push(
|
|
1276
1815
|
this.#truncate(
|
|
1277
|
-
` Global: ${status.enabled ? "enabled" : "disabled"} ·
|
|
1278
|
-
status.telegram.configured ? "configured" : "not configured"
|
|
1279
|
-
}`,
|
|
1280
|
-
width,
|
|
1281
|
-
),
|
|
1282
|
-
);
|
|
1283
|
-
lines.push(
|
|
1284
|
-
this.#truncate(
|
|
1285
|
-
` Session: ${session.effectiveEnabled ? "ACTIVE" : "inactive"} · local: ${session.locallyEnabled ? "on" : "off"} · runtime: ${
|
|
1286
|
-
session.running ? "running" : "stopped"
|
|
1287
|
-
} · environment: ${session.environment}`,
|
|
1816
|
+
` Global master: ${status.enabled ? "enabled" : "disabled"} · any provider effective: ${status.anyProviderEffective ? "yes" : "no"}`,
|
|
1288
1817
|
width,
|
|
1289
1818
|
),
|
|
1290
1819
|
);
|
|
1291
|
-
|
|
1820
|
+
for (const provider of ["telegram", "discord", "slack"] as const) {
|
|
1821
|
+
const view = status[provider];
|
|
1822
|
+
const checkedHealth = health?.provider === provider ? statusLabel(health.overall) : "not checked";
|
|
1823
|
+
const test =
|
|
1824
|
+
this.#lastTest?.adapter === provider
|
|
1825
|
+
? this.#lastTest.ok
|
|
1826
|
+
? "OK"
|
|
1827
|
+
: this.#lastTest.uncertain
|
|
1828
|
+
? "UNCERTAIN"
|
|
1829
|
+
: "ERROR"
|
|
1830
|
+
: "not run";
|
|
1292
1831
|
lines.push(
|
|
1293
1832
|
this.#truncate(
|
|
1294
|
-
`
|
|
1295
|
-
status.telegram.channel ?? "(not set)"
|
|
1296
|
-
}`,
|
|
1833
|
+
` ${provider}: ${view.quarantined ? "NEEDS REPAIR" : view.configured ? "configured" : "not configured"} · desired ${view.desiredEnabled ? "on" : "off"} (${view.desiredSource}) · effective ${view.effectiveEnabled ? "yes" : "no"} · runtime ${view.runtime ?? "inactive"} · health ${checkedHealth} · test ${test} · destination ${view.channel ?? "(unset)"}`,
|
|
1297
1834
|
width,
|
|
1298
1835
|
),
|
|
1299
1836
|
);
|
|
1300
1837
|
}
|
|
1838
|
+
lines.push(
|
|
1839
|
+
this.#truncate(
|
|
1840
|
+
` Session: ${session.genericSessionEnabled ? "ACTIVE" : "inactive"} · local: ${session.locallyEnabled ? "on" : "off"} · runtime: ${
|
|
1841
|
+
session.running ? "running" : "stopped"
|
|
1842
|
+
} · admission: ${session.genericEligibilitySource}`,
|
|
1843
|
+
width,
|
|
1844
|
+
),
|
|
1845
|
+
);
|
|
1301
1846
|
if (health && width >= 120) {
|
|
1302
1847
|
lines.push(
|
|
1303
1848
|
this.#truncate(
|
|
@@ -1327,7 +1872,7 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
1327
1872
|
if (this.#lastTest && width >= 120) {
|
|
1328
1873
|
lines.push(
|
|
1329
1874
|
this.#truncate(
|
|
1330
|
-
` Last in-editor test: ${this.#lastTest.ok ? "OK" : "ERROR"} — ${safeDetail(this.#lastTest.detail, "No detail returned.")}`,
|
|
1875
|
+
` Last in-editor test: ${this.#lastTest.ok ? "OK" : this.#lastTest.uncertain ? "UNCERTAIN" : "ERROR"} — ${safeDetail(this.#lastTest.detail, "No detail returned.")}`,
|
|
1331
1876
|
width,
|
|
1332
1877
|
),
|
|
1333
1878
|
);
|
|
@@ -1338,11 +1883,12 @@ export class NotificationsSettingsEditorComponent implements Component, Focusabl
|
|
|
1338
1883
|
#renderActionList(lines: string[], width: number, actions: readonly Action[]): void {
|
|
1339
1884
|
if (actions.length === 0) return;
|
|
1340
1885
|
this.#selectedIndex = Math.max(0, Math.min(this.#selectedIndex, actions.length - 1));
|
|
1886
|
+
const visibleActions = width < 100 ? 2 : VISIBLE_ACTIONS;
|
|
1341
1887
|
const start = Math.max(
|
|
1342
1888
|
0,
|
|
1343
|
-
Math.min(this.#selectedIndex - Math.floor(
|
|
1889
|
+
Math.min(this.#selectedIndex - Math.floor(visibleActions / 2), actions.length - visibleActions),
|
|
1344
1890
|
);
|
|
1345
|
-
const end = Math.min(actions.length, start +
|
|
1891
|
+
const end = Math.min(actions.length, start + visibleActions);
|
|
1346
1892
|
for (let index = start; index < end; index += 1) {
|
|
1347
1893
|
const action = actions[index];
|
|
1348
1894
|
if (!action) continue;
|