@pi-unipi/unipi 2.1.3 → 2.2.0

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.
Files changed (90) hide show
  1. package/README.md +6 -2
  2. package/package.json +23 -22
  3. package/packages/ask-user/ask-ui.ts +45 -31
  4. package/packages/ask-user/launcher-ui.ts +26 -22
  5. package/packages/ask-user/package.json +2 -2
  6. package/packages/ask-user/settings-tui.ts +22 -9
  7. package/packages/autocomplete/package.json +1 -1
  8. package/packages/autocomplete/src/constants.ts +5 -0
  9. package/packages/btw/package.json +2 -2
  10. package/packages/cocoindex/package.json +2 -2
  11. package/packages/compactor/package.json +3 -3
  12. package/packages/compactor/src/tui/settings-overlay.ts +2 -1
  13. package/packages/core/constants.ts +18 -0
  14. package/packages/core/index.ts +1 -0
  15. package/packages/core/package.json +1 -1
  16. package/packages/core/tui-width.ts +127 -0
  17. package/packages/footer/package.json +2 -2
  18. package/packages/footer/src/tui/settings-tui.ts +2 -1
  19. package/packages/image/README.md +82 -0
  20. package/packages/image/index.ts +1 -0
  21. package/packages/image/package.json +57 -0
  22. package/packages/image/skills/image/SKILL.md +73 -0
  23. package/packages/image/src/commands.ts +17 -0
  24. package/packages/image/src/generate.ts +201 -0
  25. package/packages/image/src/image-source.ts +204 -0
  26. package/packages/image/src/index.ts +99 -0
  27. package/packages/image/src/models.ts +290 -0
  28. package/packages/image/src/recognize.ts +223 -0
  29. package/packages/image/src/settings.ts +149 -0
  30. package/packages/image/src/tools.ts +296 -0
  31. package/packages/image/src/tui/model-selector.ts +279 -0
  32. package/packages/image/src/tui/settings-dialog.ts +236 -0
  33. package/packages/info-screen/package.json +2 -2
  34. package/packages/info-screen/tui/info-overlay.ts +505 -0
  35. package/packages/input-shortcuts/package.json +2 -2
  36. package/packages/kanboard/package.json +2 -2
  37. package/packages/kanboard/tui/kanboard-overlay.ts +300 -0
  38. package/packages/mcp/package.json +2 -2
  39. package/packages/mcp/src/tui/add-overlay.ts +10 -7
  40. package/packages/mcp/src/tui/settings-overlay.ts +10 -7
  41. package/packages/memory/package.json +3 -3
  42. package/packages/memory/tui/settings-tui.ts +281 -0
  43. package/packages/milestone/package.json +2 -2
  44. package/packages/notify/README.md +2 -0
  45. package/packages/notify/events.ts +11 -0
  46. package/packages/notify/package.json +2 -2
  47. package/packages/notify/permission-prompt-message.ts +95 -0
  48. package/packages/notify/platforms/focus-win.ts +123 -0
  49. package/packages/notify/platforms/focus.ts +33 -0
  50. package/packages/notify/platforms/gotify.ts +36 -0
  51. package/packages/notify/platforms/native.ts +79 -0
  52. package/packages/notify/platforms/node-notifier.d.ts +20 -0
  53. package/packages/notify/platforms/ntfy.ts +48 -0
  54. package/packages/notify/platforms/telegram.ts +77 -0
  55. package/packages/notify/settings.ts +1 -0
  56. package/packages/notify/skills/configure-notify/SKILL.md +28 -1
  57. package/packages/notify/src/__tests__/permission-prompt-message.test.ts +172 -0
  58. package/packages/notify/tui/gotify-setup.ts +529 -0
  59. package/packages/notify/tui/ntfy-setup.ts +653 -0
  60. package/packages/notify/tui/recap-model-selector.ts +301 -0
  61. package/packages/notify/tui/settings-overlay.ts +353 -0
  62. package/packages/notify/tui/telegram-setup.ts +302 -0
  63. package/packages/ralph/package.json +3 -3
  64. package/packages/subagents/package.json +4 -4
  65. package/packages/subagents/src/__tests__/shutdown-stale-ctx.test.ts +185 -0
  66. package/packages/subagents/src/index.ts +52 -10
  67. package/packages/unipi/index.ts +2 -0
  68. package/packages/updater/package.json +2 -2
  69. package/packages/updater/src/tui/changelog-overlay.ts +2 -2
  70. package/packages/updater/src/tui/readme-overlay.ts +2 -1
  71. package/packages/updater/src/tui/update-overlay.ts +2 -1
  72. package/packages/utility/package.json +2 -2
  73. package/packages/utility/src/tui/badge-settings-tui.ts +2 -2
  74. package/packages/utility/src/tui/util-settings-tui.ts +2 -2
  75. package/packages/web-api/README.md +28 -11
  76. package/packages/web-api/package.json +8 -2
  77. package/packages/web-api/skills/web/SKILL.md +36 -9
  78. package/packages/web-api/src/index.ts +15 -0
  79. package/packages/web-api/src/providers/duckduckgo.ts +71 -27
  80. package/packages/web-api/src/providers/firecrawl.ts +1 -1
  81. package/packages/web-api/src/providers/jina-reader.ts +1 -1
  82. package/packages/web-api/src/providers/jina-search.ts +1 -1
  83. package/packages/web-api/src/providers/perplexity.ts +2 -2
  84. package/packages/web-api/src/providers/serpapi.ts +1 -1
  85. package/packages/web-api/src/providers/tavily.ts +1 -1
  86. package/packages/web-api/src/providers/wigolo-client.ts +196 -0
  87. package/packages/web-api/src/providers/wigolo.ts +117 -0
  88. package/packages/web-api/src/settings.ts +1 -0
  89. package/packages/web-api/src/tools.ts +93 -36
  90. package/packages/workflow/package.json +2 -2
@@ -0,0 +1,653 @@
1
+ /**
2
+ * @pi-unipi/notify — ntfy Setup TUI Component
3
+ *
4
+ * Interactive overlay for setting up ntfy push notifications.
5
+ * Guides user through server URL, topic, optional token, and priority.
6
+ * Tests connection before saving.
7
+ */
8
+
9
+ import type { Component } from "@earendil-works/pi-tui";
10
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
11
+ import type { Theme } from "@earendil-works/pi-coding-agent";
12
+ import { sendNtfyNotification } from "../platforms/ntfy.js";
13
+ import { loadNtfyConfig, saveNtfyConfig, getNtfyConfigScope } from "../ntfy-config.js";
14
+ import { boxInnerWidth } from "@pi-unipi/core";
15
+
16
+ type SetupPhase =
17
+ | "instructions"
18
+ | "scope"
19
+ | "server-url"
20
+ | "topic"
21
+ | "token"
22
+ | "priority"
23
+ | "testing"
24
+ | "success"
25
+ | "error"
26
+ | "test-failed";
27
+
28
+ /**
29
+ * ntfy setup overlay component.
30
+ */
31
+ export class NtfySetupOverlay implements Component {
32
+ private phase: SetupPhase = "instructions";
33
+ private scope: "global" | "project" = "global";
34
+ private scopeIndex = 0; // 0 = global, 1 = project
35
+ private serverUrl = "";
36
+ private topic = "";
37
+ private token = "";
38
+ private priority = "3";
39
+ private error: string | null = null;
40
+ private testError: string | null = null;
41
+ private isInPaste = false;
42
+ private pasteBuffer = "";
43
+ onClose?: () => void;
44
+ requestRender?: () => void;
45
+ private theme: Theme | null = null;
46
+
47
+ constructor() {
48
+ // Determine current scope and pre-fill from resolved config
49
+ const cwd = process.cwd();
50
+ const existingScope = getNtfyConfigScope(cwd);
51
+ if (existingScope !== "none") {
52
+ this.scope = existingScope;
53
+ this.scopeIndex = existingScope === "project" ? 1 : 0;
54
+ }
55
+ const config = loadNtfyConfig(cwd);
56
+ if (config.serverUrl) this.serverUrl = config.serverUrl;
57
+ if (config.topic) this.topic = config.topic;
58
+ if (config.token) this.token = config.token;
59
+ if (config.priority) this.priority = String(config.priority);
60
+ }
61
+
62
+ setTheme(theme: Theme): void {
63
+ this.theme = theme;
64
+ }
65
+
66
+ invalidate(): void {}
67
+
68
+ handleInput(data: string): void {
69
+ switch (this.phase) {
70
+ case "instructions":
71
+ if (data === "\r" || data === " ") {
72
+ this.phase = "scope";
73
+ } else if (data === "\x1b") {
74
+ this.onClose?.();
75
+ }
76
+ break;
77
+
78
+ case "scope":
79
+ if (data === "\x1b[A" || data === "k") {
80
+ // Up
81
+ this.scopeIndex = Math.max(0, this.scopeIndex - 1);
82
+ } else if (data === "\x1b[B" || data === "j") {
83
+ // Down
84
+ this.scopeIndex = Math.min(1, this.scopeIndex + 1);
85
+ } else if (data === "\r" || data === " ") {
86
+ this.scope = this.scopeIndex === 1 ? "project" : "global";
87
+ this.phase = this.serverUrl ? "topic" : "server-url";
88
+ } else if (data === "\x1b") {
89
+ this.onClose?.();
90
+ }
91
+ break;
92
+
93
+ case "server-url":
94
+ this.handleTextInput(data, "server-url", () => {
95
+ if (!this.serverUrl) {
96
+ this.serverUrl = "https://ntfy.sh";
97
+ }
98
+ this.phase = "topic";
99
+ });
100
+ break;
101
+
102
+ case "topic":
103
+ this.handleTextInput(data, "topic", () => {
104
+ this.phase = "token";
105
+ });
106
+ break;
107
+
108
+ case "token":
109
+ this.handleTextInput(data, "token", () => {
110
+ this.phase = "priority";
111
+ });
112
+ break;
113
+
114
+ case "priority":
115
+ if (data === "\r" && this.isValidPriority()) {
116
+ this.testConnection();
117
+ } else if (data === "\x1b") {
118
+ this.onClose?.();
119
+ } else if (data === "\x7f" || data === "\b") {
120
+ this.priority = this.priority.slice(0, -1);
121
+ } else {
122
+ const ch = data.replace(/[^\d]/g, "");
123
+ if (ch && this.priority.length < 1) {
124
+ this.priority += ch;
125
+ }
126
+ }
127
+ break;
128
+
129
+ case "testing":
130
+ if (data === "\x1b") {
131
+ this.onClose?.();
132
+ }
133
+ break;
134
+
135
+ case "success":
136
+ case "error":
137
+ case "test-failed":
138
+ if (data === "\r" || data === " " || data === "\x1b") {
139
+ this.onClose?.();
140
+ }
141
+ break;
142
+ }
143
+ }
144
+
145
+ private handleTextInput(
146
+ data: string,
147
+ target: "server-url" | "topic" | "token",
148
+ onEnter: () => void
149
+ ): void {
150
+ // Handle bracketed paste mode
151
+ if (this.isInPaste) {
152
+ this.pasteBuffer += data;
153
+ const endIndex = this.pasteBuffer.indexOf("\x1b[201~");
154
+ if (endIndex !== -1) {
155
+ const pasteContent = this.pasteBuffer.substring(0, endIndex).trim();
156
+ if (target === "server-url") {
157
+ this.serverUrl = pasteContent;
158
+ } else if (target === "topic") {
159
+ this.topic = pasteContent;
160
+ } else {
161
+ this.token = pasteContent;
162
+ }
163
+ this.isInPaste = false;
164
+ this.pasteBuffer = "";
165
+ }
166
+ return;
167
+ }
168
+ // Detect start of bracketed paste
169
+ if (data.includes("\x1b[200~")) {
170
+ this.isInPaste = true;
171
+ this.pasteBuffer = data.replace("\x1b[200~", "");
172
+ return;
173
+ }
174
+ if (data === "\r") {
175
+ onEnter();
176
+ } else if (data === "\x1b") {
177
+ // Escape during token phase — skip token (optional field)
178
+ if (target === "token") {
179
+ this.phase = "priority";
180
+ } else {
181
+ this.onClose?.();
182
+ }
183
+ } else if (data === "\x7f" || data === "\b") {
184
+ if (target === "server-url") {
185
+ this.serverUrl = this.serverUrl.slice(0, -1);
186
+ } else if (target === "topic") {
187
+ this.topic = this.topic.slice(0, -1);
188
+ } else {
189
+ this.token = this.token.slice(0, -1);
190
+ }
191
+ } else {
192
+ // Ignore escape sequences
193
+ if (data.startsWith("\x1b[")) return;
194
+ if (target === "server-url") {
195
+ this.serverUrl += data;
196
+ } else if (target === "topic") {
197
+ this.topic += data;
198
+ } else {
199
+ this.token += data;
200
+ }
201
+ }
202
+ }
203
+
204
+ private isValidPriority(): boolean {
205
+ const num = parseInt(this.priority, 10);
206
+ return !isNaN(num) && num >= 1 && num <= 5;
207
+ }
208
+
209
+ private async testConnection(): Promise<void> {
210
+ this.phase = "testing";
211
+ this.requestRender?.();
212
+
213
+ try {
214
+ await sendNtfyNotification(
215
+ this.serverUrl.replace(/\/$/, ""),
216
+ this.topic,
217
+ "Pi — Setup Test",
218
+ `ntfy configured successfully at ${new Date().toLocaleTimeString()}`,
219
+ parseInt(this.priority, 10) || 3,
220
+ this.token || undefined
221
+ );
222
+ this.saveConfig();
223
+ this.phase = "success";
224
+ this.requestRender?.();
225
+ setTimeout(() => this.onClose?.(), 1500);
226
+ } catch (err) {
227
+ this.testError = err instanceof Error ? err.message : String(err);
228
+ this.phase = "test-failed";
229
+ this.requestRender?.();
230
+ }
231
+ }
232
+
233
+ private saveConfig(): void {
234
+ const cwd = process.cwd();
235
+ saveNtfyConfig(this.scope, cwd, {
236
+ enabled: true,
237
+ serverUrl: this.serverUrl.replace(/\/$/, ""),
238
+ topic: this.topic,
239
+ token: this.token || undefined,
240
+ priority: parseInt(this.priority, 10) || 3,
241
+ });
242
+ }
243
+
244
+ // ─── Theme helpers ───────────────────────────────────────────────────
245
+
246
+ private fg(color: string, text: string): string {
247
+ if (this.theme) return this.theme.fg(color as any, text);
248
+ const c: Record<string, string> = {
249
+ accent: "\x1b[36m",
250
+ success: "\x1b[32m",
251
+ warning: "\x1b[33m",
252
+ error: "\x1b[31m",
253
+ dim: "\x1b[2m",
254
+ borderMuted: "\x1b[90m",
255
+ };
256
+ return `${c[color] ?? ""}${text}\x1b[0m`;
257
+ }
258
+
259
+ private bold(text: string): string {
260
+ return this.theme ? this.theme.bold(text) : `\x1b[1m${text}\x1b[0m`;
261
+ }
262
+
263
+ private frameLine(content: string, innerWidth: number): string {
264
+ const truncated = truncateToWidth(content, innerWidth, "");
265
+ const padding = Math.max(0, innerWidth - visibleWidth(truncated));
266
+ return `${this.fg("borderMuted", "│")}${truncated}${" ".repeat(padding)}${this.fg("borderMuted", "│")}`;
267
+ }
268
+
269
+ private ruleLine(innerWidth: number): string {
270
+ return this.fg("borderMuted", `├${"─".repeat(innerWidth)}┤`);
271
+ }
272
+
273
+ private borderLine(innerWidth: number, edge: "top" | "bottom"): string {
274
+ const left = edge === "top" ? "┌" : "└";
275
+ const right = edge === "top" ? "┐" : "┘";
276
+ return this.fg("borderMuted", `${left}${"─".repeat(innerWidth)}${right}`);
277
+ }
278
+
279
+ private maskToken(token: string): string {
280
+ if (token.length <= 8) return token;
281
+ return token.slice(0, 4) + "•".repeat(token.length - 8) + token.slice(-4);
282
+ }
283
+
284
+ render(width: number): string[] {
285
+ const innerWidth = boxInnerWidth(width);
286
+ const lines: string[] = [];
287
+
288
+ lines.push(this.borderLine(innerWidth, "top"));
289
+ lines.push(
290
+ this.frameLine(
291
+ this.fg("accent", this.bold("📢 ntfy Setup")),
292
+ innerWidth
293
+ )
294
+ );
295
+ lines.push(this.ruleLine(innerWidth));
296
+
297
+ switch (this.phase) {
298
+ case "instructions":
299
+ lines.push(
300
+ this.frameLine(
301
+ this.fg("dim", "Set up ntfy push notifications:"),
302
+ innerWidth
303
+ )
304
+ );
305
+ lines.push(this.frameLine("", innerWidth));
306
+ lines.push(
307
+ this.frameLine(
308
+ ` ${this.bold("1.")} ntfy is a simple HTTP-based notification service`,
309
+ innerWidth
310
+ )
311
+ );
312
+ lines.push(
313
+ this.frameLine(
314
+ ` ${this.fg("dim", "Public: https://ntfy.sh | Self-hosted: any ntfy server")}`,
315
+ innerWidth
316
+ )
317
+ );
318
+ lines.push(this.frameLine("", innerWidth));
319
+ lines.push(
320
+ this.frameLine(
321
+ ` ${this.bold("2.")} Choose a topic (acts as a channel name)`,
322
+ innerWidth
323
+ )
324
+ );
325
+ lines.push(
326
+ this.frameLine(
327
+ ` ${this.fg("dim", "Subscribe to the topic in the ntfy app or web UI")}`,
328
+ innerWidth
329
+ )
330
+ );
331
+ lines.push(this.frameLine("", innerWidth));
332
+ lines.push(
333
+ this.frameLine(
334
+ ` ${this.bold("3.")} Optionally set an access token for private servers`,
335
+ innerWidth
336
+ )
337
+ );
338
+ if (this.serverUrl) {
339
+ lines.push(
340
+ this.frameLine(
341
+ ` ${this.fg("success", "✓")} Server URL pre-filled from existing config`,
342
+ innerWidth
343
+ )
344
+ );
345
+ }
346
+ if (this.topic) {
347
+ lines.push(
348
+ this.frameLine(
349
+ ` ${this.fg("success", "✓")} Topic pre-filled from existing config`,
350
+ innerWidth
351
+ )
352
+ );
353
+ }
354
+ lines.push(this.ruleLine(innerWidth));
355
+ lines.push(
356
+ this.frameLine(
357
+ this.fg("dim", "Press Enter to continue, Esc to cancel"),
358
+ innerWidth
359
+ )
360
+ );
361
+ break;
362
+
363
+ case "scope": {
364
+ lines.push(
365
+ this.frameLine(
366
+ this.fg("dim", "Where should this config be saved?"),
367
+ innerWidth
368
+ )
369
+ );
370
+ lines.push(this.frameLine("", innerWidth));
371
+ const options = ["Global (all projects)", "Project (this project only)"];
372
+ for (let i = 0; i < options.length; i++) {
373
+ const isSelected = i === this.scopeIndex;
374
+ const label = isSelected ? this.bold(options[i]) : this.fg("dim", options[i]);
375
+ lines.push(
376
+ this.frameLine(
377
+ ` ${isSelected ? this.fg("accent", "▸") : " "} ${label}`,
378
+ innerWidth
379
+ )
380
+ );
381
+ }
382
+ lines.push(this.frameLine("", innerWidth));
383
+ lines.push(this.ruleLine(innerWidth));
384
+ lines.push(
385
+ this.frameLine(
386
+ this.fg("dim", "↑↓ select · Enter confirm · Esc cancel"),
387
+ innerWidth
388
+ )
389
+ );
390
+ break;
391
+ }
392
+
393
+ case "server-url":
394
+ lines.push(
395
+ this.frameLine(
396
+ this.fg("dim", "Enter ntfy server URL:"),
397
+ innerWidth
398
+ )
399
+ );
400
+ lines.push(this.frameLine("", innerWidth));
401
+ lines.push(
402
+ this.frameLine(
403
+ ` ${this.fg("accent", this.bold(this.serverUrl || " "))}${this.fg("dim", "█")}`,
404
+ innerWidth
405
+ )
406
+ );
407
+ lines.push(this.frameLine("", innerWidth));
408
+ lines.push(
409
+ this.frameLine(
410
+ this.fg("dim", "Default: https://ntfy.sh (public)"),
411
+ innerWidth
412
+ )
413
+ );
414
+ lines.push(
415
+ this.frameLine(
416
+ this.fg("dim", "Leave empty and press Enter for default"),
417
+ innerWidth
418
+ )
419
+ );
420
+ lines.push(this.ruleLine(innerWidth));
421
+ lines.push(
422
+ this.frameLine(
423
+ this.fg("dim", "Enter to continue · Esc to cancel"),
424
+ innerWidth
425
+ )
426
+ );
427
+ break;
428
+
429
+ case "topic":
430
+ lines.push(
431
+ this.frameLine(
432
+ this.fg("dim", "Enter topic name:"),
433
+ innerWidth
434
+ )
435
+ );
436
+ lines.push(this.frameLine("", innerWidth));
437
+ lines.push(
438
+ this.frameLine(
439
+ ` ${this.fg("accent", this.bold(this.topic || " "))}${this.fg("dim", "█")}`,
440
+ innerWidth
441
+ )
442
+ );
443
+ lines.push(this.frameLine("", innerWidth));
444
+ lines.push(
445
+ this.frameLine(
446
+ this.fg("dim", "e.g. my-pi-notifications, project-alerts"),
447
+ innerWidth
448
+ )
449
+ );
450
+ lines.push(
451
+ this.frameLine(
452
+ this.fg("dim", "Pick something unique if using public ntfy.sh"),
453
+ innerWidth
454
+ )
455
+ );
456
+ lines.push(this.ruleLine(innerWidth));
457
+ lines.push(
458
+ this.frameLine(
459
+ this.fg("dim", "Enter to continue · Esc to cancel"),
460
+ innerWidth
461
+ )
462
+ );
463
+ break;
464
+
465
+ case "token":
466
+ lines.push(
467
+ this.frameLine(
468
+ this.fg("dim", "Enter access token (optional):"),
469
+ innerWidth
470
+ )
471
+ );
472
+ lines.push(this.frameLine("", innerWidth));
473
+ const displayToken = this.token
474
+ ? this.fg("accent", this.bold(this.maskToken(this.token)))
475
+ : " ";
476
+ lines.push(
477
+ this.frameLine(
478
+ ` ${displayToken}${this.fg("dim", "█")}`,
479
+ innerWidth
480
+ )
481
+ );
482
+ lines.push(this.frameLine("", innerWidth));
483
+ lines.push(
484
+ this.frameLine(
485
+ this.fg("dim", "Only needed for private/authenticated ntfy servers"),
486
+ innerWidth
487
+ )
488
+ );
489
+ lines.push(
490
+ this.frameLine(
491
+ this.fg("dim", "Not needed for public ntfy.sh"),
492
+ innerWidth
493
+ )
494
+ );
495
+ lines.push(this.ruleLine(innerWidth));
496
+ lines.push(
497
+ this.frameLine(
498
+ this.fg("dim", "Enter to continue · Esc to skip"),
499
+ innerWidth
500
+ )
501
+ );
502
+ break;
503
+
504
+ case "priority":
505
+ lines.push(
506
+ this.frameLine(
507
+ this.fg("dim", "Set notification priority (1-5):"),
508
+ innerWidth
509
+ )
510
+ );
511
+ lines.push(this.frameLine("", innerWidth));
512
+ lines.push(
513
+ this.frameLine(
514
+ ` ${this.fg("accent", this.bold(this.priority || " "))}${this.fg("dim", "█")}`,
515
+ innerWidth
516
+ )
517
+ );
518
+ lines.push(this.frameLine("", innerWidth));
519
+ lines.push(
520
+ this.frameLine(
521
+ ` ${this.fg("dim", "1")} = min · ${this.fg("dim", "3")} = default · ${this.fg("dim", "5")} = max`,
522
+ innerWidth
523
+ )
524
+ );
525
+ lines.push(this.ruleLine(innerWidth));
526
+ lines.push(
527
+ this.frameLine(
528
+ this.fg("dim", "Enter to test connection · Esc to cancel"),
529
+ innerWidth
530
+ )
531
+ );
532
+ break;
533
+
534
+ case "testing":
535
+ lines.push(this.frameLine("", innerWidth));
536
+ lines.push(
537
+ this.frameLine(
538
+ ` ${this.fg("accent", "⠋")} ${this.bold("Testing connection...")}`,
539
+ innerWidth
540
+ )
541
+ );
542
+ lines.push(this.frameLine("", innerWidth));
543
+ lines.push(
544
+ this.frameLine(
545
+ ` ${this.fg("dim", `Sending test to ${this.serverUrl}/${this.topic}`)}`,
546
+ innerWidth
547
+ )
548
+ );
549
+ lines.push(this.ruleLine(innerWidth));
550
+ lines.push(
551
+ this.frameLine(
552
+ this.fg("dim", "Esc to cancel"),
553
+ innerWidth
554
+ )
555
+ );
556
+ break;
557
+
558
+ case "success":
559
+ lines.push(this.frameLine("", innerWidth));
560
+ lines.push(
561
+ this.frameLine(
562
+ ` ${this.fg("success", "✓ ntfy configured successfully!")}`,
563
+ innerWidth
564
+ )
565
+ );
566
+ lines.push(this.frameLine("", innerWidth));
567
+ lines.push(
568
+ this.frameLine(
569
+ ` ${this.fg("dim", `Server: ${this.serverUrl}`)}`,
570
+ innerWidth
571
+ )
572
+ );
573
+ lines.push(
574
+ this.frameLine(
575
+ ` ${this.fg("dim", `Topic: ${this.topic}`)}`,
576
+ innerWidth
577
+ )
578
+ );
579
+ lines.push(
580
+ this.frameLine(
581
+ ` ${this.fg("dim", `Priority: ${this.priority}`)}`,
582
+ innerWidth
583
+ )
584
+ );
585
+ lines.push(this.ruleLine(innerWidth));
586
+ lines.push(
587
+ this.frameLine(
588
+ this.fg("dim", "Closing..."),
589
+ innerWidth
590
+ )
591
+ );
592
+ break;
593
+
594
+ case "test-failed":
595
+ lines.push(this.frameLine("", innerWidth));
596
+ lines.push(
597
+ this.frameLine(
598
+ ` ${this.fg("error", "✗ Connection test failed")}`,
599
+ innerWidth
600
+ )
601
+ );
602
+ lines.push(this.frameLine("", innerWidth));
603
+ lines.push(
604
+ this.frameLine(
605
+ ` ${this.fg("dim", this.testError || "Unknown error")}`,
606
+ innerWidth
607
+ )
608
+ );
609
+ lines.push(this.frameLine("", innerWidth));
610
+ lines.push(
611
+ this.frameLine(
612
+ ` ${this.fg("dim", "Check your server URL and topic")}`,
613
+ innerWidth
614
+ )
615
+ );
616
+ lines.push(this.ruleLine(innerWidth));
617
+ lines.push(
618
+ this.frameLine(
619
+ this.fg("dim", "Press Enter to close"),
620
+ innerWidth
621
+ )
622
+ );
623
+ break;
624
+
625
+ case "error":
626
+ lines.push(this.frameLine("", innerWidth));
627
+ lines.push(
628
+ this.frameLine(
629
+ ` ${this.fg("error", "✗ Setup failed")}`,
630
+ innerWidth
631
+ )
632
+ );
633
+ lines.push(this.frameLine("", innerWidth));
634
+ lines.push(
635
+ this.frameLine(
636
+ ` ${this.fg("dim", this.error || "Unknown error")}`,
637
+ innerWidth
638
+ )
639
+ );
640
+ lines.push(this.ruleLine(innerWidth));
641
+ lines.push(
642
+ this.frameLine(
643
+ this.fg("dim", "Press Enter to close"),
644
+ innerWidth
645
+ )
646
+ );
647
+ break;
648
+ }
649
+
650
+ lines.push(this.borderLine(innerWidth, "bottom"));
651
+ return lines;
652
+ }
653
+ }