@pi-unipi/unipi 2.1.2 → 2.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +22 -22
- package/packages/ask-user/package.json +3 -3
- package/packages/autocomplete/package.json +1 -1
- package/packages/btw/package.json +2 -2
- package/packages/cocoindex/package.json +2 -2
- package/packages/compactor/package.json +4 -4
- package/packages/core/package.json +1 -1
- package/packages/footer/package.json +3 -3
- package/packages/info-screen/package.json +2 -2
- package/packages/input-shortcuts/package.json +3 -3
- package/packages/kanboard/package.json +2 -2
- package/packages/mcp/package.json +2 -2
- package/packages/memory/package.json +3 -3
- package/packages/milestone/package.json +2 -2
- package/packages/notify/README.md +2 -1
- package/packages/notify/events.ts +119 -81
- package/packages/notify/package.json +2 -2
- package/packages/notify/settings.ts +1 -0
- package/packages/notify/skills/configure-notify/SKILL.md +3 -1
- package/packages/notify/src/__tests__/event-bus.test.ts +3 -2
- package/packages/ralph/package.json +3 -3
- package/packages/subagents/package.json +5 -5
- package/packages/updater/package.json +3 -3
- package/packages/utility/package.json +2 -2
- package/packages/web-api/package.json +2 -2
- package/packages/workflow/package.json +2 -2
- package/packages/info-screen/tui/info-overlay.ts +0 -504
- package/packages/kanboard/tui/kanboard-overlay.ts +0 -300
- package/packages/memory/tui/settings-tui.ts +0 -281
- package/packages/notify/platforms/focus-win.ts +0 -123
- package/packages/notify/platforms/focus.ts +0 -33
- package/packages/notify/platforms/gotify.ts +0 -36
- package/packages/notify/platforms/native.ts +0 -79
- package/packages/notify/platforms/node-notifier.d.ts +0 -20
- package/packages/notify/platforms/ntfy.ts +0 -48
- package/packages/notify/platforms/telegram.ts +0 -77
- package/packages/notify/tui/gotify-setup.ts +0 -528
- package/packages/notify/tui/ntfy-setup.ts +0 -652
- package/packages/notify/tui/recap-model-selector.ts +0 -301
- package/packages/notify/tui/settings-overlay.ts +0 -352
- package/packages/notify/tui/telegram-setup.ts +0 -301
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @pi-unipi/notify — Telegram Setup TUI Component
|
|
3
|
-
*
|
|
4
|
-
* Interactive overlay for setting up Telegram bot notifications.
|
|
5
|
-
* Guides user through BotFather flow and auto-detects chat ID.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { Component } from "@earendil-works/pi-tui";
|
|
9
|
-
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
10
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import { pollForChatId } from "../platforms/telegram.js";
|
|
12
|
-
import { updateConfig } from "../settings.js";
|
|
13
|
-
|
|
14
|
-
type SetupPhase = "instructions" | "token" | "polling" | "success" | "error" | "timeout";
|
|
15
|
-
|
|
16
|
-
/** Spinner frames */
|
|
17
|
-
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Telegram setup overlay component.
|
|
21
|
-
*/
|
|
22
|
-
export class TelegramSetupOverlay implements Component {
|
|
23
|
-
private phase: SetupPhase = "instructions";
|
|
24
|
-
private botToken = "";
|
|
25
|
-
private chatId: string | null = null;
|
|
26
|
-
private error: string | null = null;
|
|
27
|
-
private spinnerFrame = 0;
|
|
28
|
-
private spinnerTimer: ReturnType<typeof setInterval> | null = null;
|
|
29
|
-
private pollAbort: AbortController | null = null;
|
|
30
|
-
private pollTimer: ReturnType<typeof setTimeout> | null = null;
|
|
31
|
-
private startTime = Date.now();
|
|
32
|
-
private readonly TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
33
|
-
private isInPaste = false;
|
|
34
|
-
private pasteBuffer = "";
|
|
35
|
-
onClose?: () => void;
|
|
36
|
-
requestRender?: () => void;
|
|
37
|
-
private theme: Theme | null = null;
|
|
38
|
-
|
|
39
|
-
setTheme(theme: Theme): void {
|
|
40
|
-
this.theme = theme;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
invalidate(): void {}
|
|
44
|
-
|
|
45
|
-
handleInput(data: string): void {
|
|
46
|
-
switch (this.phase) {
|
|
47
|
-
case "instructions":
|
|
48
|
-
if (data === "\r" || data === " ") {
|
|
49
|
-
this.phase = "token";
|
|
50
|
-
} else if (data === "\x1b") {
|
|
51
|
-
this.cleanup();
|
|
52
|
-
this.onClose?.();
|
|
53
|
-
}
|
|
54
|
-
break;
|
|
55
|
-
case "token":
|
|
56
|
-
// Handle bracketed paste mode
|
|
57
|
-
if (this.isInPaste) {
|
|
58
|
-
this.pasteBuffer += data;
|
|
59
|
-
const endIndex = this.pasteBuffer.indexOf("\x1b[201~");
|
|
60
|
-
if (endIndex !== -1) {
|
|
61
|
-
// Extract pasted content and process it
|
|
62
|
-
const pasteContent = this.pasteBuffer.substring(0, endIndex);
|
|
63
|
-
this.processTokenInput(pasteContent);
|
|
64
|
-
this.isInPaste = false;
|
|
65
|
-
this.pasteBuffer = "";
|
|
66
|
-
}
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
// Detect start of bracketed paste
|
|
70
|
-
if (data.includes("\x1b[200~")) {
|
|
71
|
-
this.isInPaste = true;
|
|
72
|
-
this.pasteBuffer = data.replace("\x1b[200~", "");
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
if (data === "\r" && this.botToken.length > 0) {
|
|
76
|
-
this.startPolling();
|
|
77
|
-
} else if (data === "\x1b") {
|
|
78
|
-
this.cleanup();
|
|
79
|
-
this.onClose?.();
|
|
80
|
-
} else if (data === "\x7f" || data === "\b") {
|
|
81
|
-
this.botToken = this.botToken.slice(0, -1);
|
|
82
|
-
} else {
|
|
83
|
-
this.processTokenInput(data);
|
|
84
|
-
}
|
|
85
|
-
break;
|
|
86
|
-
case "polling":
|
|
87
|
-
if (data === "\x1b") {
|
|
88
|
-
this.cleanup();
|
|
89
|
-
this.onClose?.();
|
|
90
|
-
}
|
|
91
|
-
break;
|
|
92
|
-
case "success":
|
|
93
|
-
case "error":
|
|
94
|
-
case "timeout":
|
|
95
|
-
if (data === "\r" || data === " " || data === "\x1b") {
|
|
96
|
-
this.cleanup();
|
|
97
|
-
this.onClose?.();
|
|
98
|
-
}
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
private processTokenInput(data: string): void {
|
|
104
|
-
// Ignore escape sequences (arrow keys, function keys, etc.)
|
|
105
|
-
if (data.startsWith("\x1b[")) return;
|
|
106
|
-
// Filter to valid bot token characters only
|
|
107
|
-
const cleaned = data.replace(/[^0-9:A-Za-z_-]/g, "");
|
|
108
|
-
if (cleaned.length > 0) {
|
|
109
|
-
this.botToken += cleaned;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
private startPolling(): void {
|
|
114
|
-
this.phase = "polling";
|
|
115
|
-
this.startTime = Date.now();
|
|
116
|
-
this.pollAbort = new AbortController();
|
|
117
|
-
|
|
118
|
-
// Start spinner animation
|
|
119
|
-
this.spinnerTimer = setInterval(() => {
|
|
120
|
-
this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
|
|
121
|
-
this.requestRender?.();
|
|
122
|
-
}, 80);
|
|
123
|
-
|
|
124
|
-
// Start polling
|
|
125
|
-
this.doPoll();
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
private async doPoll(): Promise<void> {
|
|
129
|
-
if (this.phase !== "polling" || !this.pollAbort) return;
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
const chatId = await pollForChatId(
|
|
133
|
-
this.botToken,
|
|
134
|
-
this.pollAbort.signal
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
if (chatId) {
|
|
138
|
-
this.chatId = chatId;
|
|
139
|
-
this.phase = "success";
|
|
140
|
-
this.saveConfig();
|
|
141
|
-
this.cleanup();
|
|
142
|
-
this.requestRender?.();
|
|
143
|
-
// Auto-close after brief delay to show success
|
|
144
|
-
setTimeout(() => this.onClose?.(), 1000);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Check timeout
|
|
149
|
-
if (Date.now() - this.startTime > this.TIMEOUT_MS) {
|
|
150
|
-
this.phase = "timeout";
|
|
151
|
-
this.error = "Timed out after 5 minutes";
|
|
152
|
-
this.cleanup();
|
|
153
|
-
this.requestRender?.();
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Schedule next poll
|
|
158
|
-
this.pollTimer = setTimeout(() => this.doPoll(), 2000);
|
|
159
|
-
} catch (err) {
|
|
160
|
-
if (this.pollAbort?.signal.aborted) return;
|
|
161
|
-
this.phase = "error";
|
|
162
|
-
this.error = err instanceof Error ? err.message : String(err);
|
|
163
|
-
this.cleanup();
|
|
164
|
-
this.requestRender?.();
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
private saveConfig(): void {
|
|
169
|
-
if (this.botToken && this.chatId) {
|
|
170
|
-
updateConfig({
|
|
171
|
-
telegram: {
|
|
172
|
-
enabled: true,
|
|
173
|
-
botToken: this.botToken,
|
|
174
|
-
chatId: this.chatId,
|
|
175
|
-
},
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
private cleanup(): void {
|
|
181
|
-
if (this.spinnerTimer) {
|
|
182
|
-
clearInterval(this.spinnerTimer);
|
|
183
|
-
this.spinnerTimer = null;
|
|
184
|
-
}
|
|
185
|
-
if (this.pollTimer) {
|
|
186
|
-
clearTimeout(this.pollTimer);
|
|
187
|
-
this.pollTimer = null;
|
|
188
|
-
}
|
|
189
|
-
if (this.pollAbort) {
|
|
190
|
-
this.pollAbort.abort();
|
|
191
|
-
this.pollAbort = null;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ─── Theme helpers ───────────────────────────────────────────────────
|
|
196
|
-
|
|
197
|
-
private fg(color: string, text: string): string {
|
|
198
|
-
if (this.theme) return this.theme.fg(color as any, text);
|
|
199
|
-
const c: Record<string, string> = {
|
|
200
|
-
accent: "\x1b[36m", success: "\x1b[32m", warning: "\x1b[33m",
|
|
201
|
-
error: "\x1b[31m", dim: "\x1b[2m", borderMuted: "\x1b[90m",
|
|
202
|
-
};
|
|
203
|
-
return `${c[color] ?? ""}${text}\x1b[0m`;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
private bold(text: string): string {
|
|
207
|
-
return this.theme ? this.theme.bold(text) : `\x1b[1m${text}\x1b[0m`;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
private frameLine(content: string, innerWidth: number): string {
|
|
211
|
-
const truncated = truncateToWidth(content, innerWidth, "");
|
|
212
|
-
const padding = Math.max(0, innerWidth - visibleWidth(truncated));
|
|
213
|
-
return `${this.fg("borderMuted", "│")}${truncated}${" ".repeat(padding)}${this.fg("borderMuted", "│")}`;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
private ruleLine(innerWidth: number): string {
|
|
217
|
-
return this.fg("borderMuted", `├${"─".repeat(innerWidth)}┤`);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
private borderLine(innerWidth: number, edge: "top" | "bottom"): string {
|
|
221
|
-
const left = edge === "top" ? "┌" : "└";
|
|
222
|
-
const right = edge === "top" ? "┐" : "┘";
|
|
223
|
-
return this.fg("borderMuted", `${left}${"─".repeat(innerWidth)}${right}`);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
render(width: number): string[] {
|
|
227
|
-
const innerWidth = Math.max(22, width - 2);
|
|
228
|
-
const lines: string[] = [];
|
|
229
|
-
|
|
230
|
-
lines.push(this.borderLine(innerWidth, "top"));
|
|
231
|
-
lines.push(this.frameLine(this.fg("accent", this.bold("🤖 Telegram Bot Setup")), innerWidth));
|
|
232
|
-
lines.push(this.ruleLine(innerWidth));
|
|
233
|
-
|
|
234
|
-
switch (this.phase) {
|
|
235
|
-
case "instructions":
|
|
236
|
-
lines.push(this.frameLine(this.fg("dim", "Set up Telegram notifications in 3 steps:"), innerWidth));
|
|
237
|
-
lines.push(this.frameLine("", innerWidth));
|
|
238
|
-
lines.push(this.frameLine(` ${this.bold("1.")} Open Telegram and message ${this.fg("accent", "@BotFather")}`, innerWidth));
|
|
239
|
-
lines.push(this.frameLine(` Send /newbot and follow the prompts to create a bot`, innerWidth));
|
|
240
|
-
lines.push(this.frameLine("", innerWidth));
|
|
241
|
-
lines.push(this.frameLine(` ${this.bold("2.")} Copy the bot token from BotFather`, innerWidth));
|
|
242
|
-
lines.push(this.frameLine("", innerWidth));
|
|
243
|
-
lines.push(this.frameLine(` ${this.bold("3.")} Send any message to your new bot`, innerWidth));
|
|
244
|
-
lines.push(this.frameLine(` (We'll detect your chat ID automatically)`, innerWidth));
|
|
245
|
-
lines.push(this.ruleLine(innerWidth));
|
|
246
|
-
lines.push(this.frameLine(this.fg("dim", "Press Enter to continue, Esc to cancel"), innerWidth));
|
|
247
|
-
break;
|
|
248
|
-
|
|
249
|
-
case "token":
|
|
250
|
-
lines.push(this.frameLine(this.fg("dim", "Paste your bot token from BotFather:"), innerWidth));
|
|
251
|
-
lines.push(this.frameLine("", innerWidth));
|
|
252
|
-
const display = this.fg("accent", this.bold(this.botToken || " "));
|
|
253
|
-
lines.push(this.frameLine(` ${display}${this.fg("dim", "█")}`, innerWidth));
|
|
254
|
-
lines.push(this.frameLine("", innerWidth));
|
|
255
|
-
lines.push(this.frameLine(this.fg("dim", "Enter to start polling · Esc to cancel"), innerWidth));
|
|
256
|
-
break;
|
|
257
|
-
|
|
258
|
-
case "polling": {
|
|
259
|
-
const frame = SPINNER_FRAMES[this.spinnerFrame] || "⠋";
|
|
260
|
-
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
|
|
261
|
-
const remaining = Math.max(0, 300 - elapsed);
|
|
262
|
-
lines.push(this.frameLine(` ${this.fg("accent", frame)} ${this.bold("Waiting for first message...")}`, innerWidth));
|
|
263
|
-
lines.push(this.frameLine("", innerWidth));
|
|
264
|
-
lines.push(this.frameLine(` ${this.fg("dim", "Send any message to your bot in Telegram")}`, innerWidth));
|
|
265
|
-
lines.push(this.frameLine(` ${this.fg("dim", `Timeout: ${Math.floor(remaining / 60)}:${String(remaining % 60).padStart(2, "0")}`)}`, innerWidth));
|
|
266
|
-
lines.push(this.ruleLine(innerWidth));
|
|
267
|
-
lines.push(this.frameLine(this.fg("dim", "Esc to cancel"), innerWidth));
|
|
268
|
-
break;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
case "success":
|
|
272
|
-
lines.push(this.frameLine(` ${this.fg("success", "✓ Telegram bot configured!")}`, innerWidth));
|
|
273
|
-
lines.push(this.frameLine("", innerWidth));
|
|
274
|
-
lines.push(this.frameLine(` ${this.fg("dim", `Chat ID: ${this.chatId}`)}`, innerWidth));
|
|
275
|
-
lines.push(this.frameLine(` ${this.fg("dim", "Notifications will be sent to this chat")}`, innerWidth));
|
|
276
|
-
lines.push(this.ruleLine(innerWidth));
|
|
277
|
-
lines.push(this.frameLine(this.fg("dim", "Press Enter to close"), innerWidth));
|
|
278
|
-
break;
|
|
279
|
-
|
|
280
|
-
case "error":
|
|
281
|
-
lines.push(this.frameLine(` ${this.fg("error", "✗ Setup failed")}`, innerWidth));
|
|
282
|
-
lines.push(this.frameLine("", innerWidth));
|
|
283
|
-
lines.push(this.frameLine(` ${this.fg("dim", this.error || "Unknown error")}`, innerWidth));
|
|
284
|
-
lines.push(this.ruleLine(innerWidth));
|
|
285
|
-
lines.push(this.frameLine(this.fg("dim", "Press Enter to close"), innerWidth));
|
|
286
|
-
break;
|
|
287
|
-
|
|
288
|
-
case "timeout":
|
|
289
|
-
lines.push(this.frameLine(` ${this.fg("warning", "⏰ Timed out after 5 minutes")}`, innerWidth));
|
|
290
|
-
lines.push(this.frameLine("", innerWidth));
|
|
291
|
-
lines.push(this.frameLine(` ${this.fg("dim", "Make sure you sent a message to your bot in Telegram")}`, innerWidth));
|
|
292
|
-
lines.push(this.frameLine(` ${this.fg("dim", "You can try again with /unipi:notify-set-tg")}`, innerWidth));
|
|
293
|
-
lines.push(this.ruleLine(innerWidth));
|
|
294
|
-
lines.push(this.frameLine(this.fg("dim", "Press Enter to close"), innerWidth));
|
|
295
|
-
break;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
lines.push(this.borderLine(innerWidth, "bottom"));
|
|
299
|
-
return lines;
|
|
300
|
-
}
|
|
301
|
-
}
|