@pi-unipi/notify 2.6.1 → 2.9.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.
- package/events.ts +0 -10
- package/package.json +8 -6
- package/settings.ts +0 -19
- package/tui/gotify-setup.ts +108 -143
- package/tui/ntfy-setup.ts +137 -172
- package/tui/recap-model-selector.ts +36 -71
- package/tui/settings-overlay.ts +50 -86
- package/tui/telegram-setup.ts +48 -79
- package/types.ts +0 -2
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { Component } from "@earendil-works/pi-tui";
|
|
10
|
-
import { matchesKey
|
|
10
|
+
import { matchesKey } from "@earendil-works/pi-tui";
|
|
11
11
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import { readModelCache, type CachedModel, boxInnerWidth } from "@pi-unipi/core";
|
|
12
|
+
import { readModelCache, type CachedModel, boxInnerWidth, OverlayTheme } from "@pi-unipi/core";
|
|
13
13
|
import { loadConfig, saveConfig } from "../settings.js";
|
|
14
14
|
|
|
15
15
|
const DEFAULT_MODEL = "openrouter/openai/gpt-oss-20b";
|
|
@@ -27,7 +27,7 @@ export class RecapModelSelectorOverlay implements Component {
|
|
|
27
27
|
private error: string | null = null;
|
|
28
28
|
onClose?: () => void;
|
|
29
29
|
requestRender?: () => void;
|
|
30
|
-
private
|
|
30
|
+
private overlay = new OverlayTheme();
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* @param models Optional model list, preferably collected from Pi's live
|
|
@@ -51,7 +51,7 @@ export class RecapModelSelectorOverlay implements Component {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
setTheme(theme: Theme): void {
|
|
54
|
-
this.theme
|
|
54
|
+
this.overlay.setTheme(theme);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
invalidate(): void {}
|
|
@@ -169,84 +169,49 @@ export class RecapModelSelectorOverlay implements Component {
|
|
|
169
169
|
setTimeout(() => this.onClose?.(), 500);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
// ─── Theme helpers ───────────────────────────────────────────────────
|
|
173
|
-
|
|
174
|
-
private fg(color: string, text: string): string {
|
|
175
|
-
if (this.theme) return this.theme.fg(color as any, text);
|
|
176
|
-
const c: Record<string, string> = {
|
|
177
|
-
accent: "\x1b[36m",
|
|
178
|
-
success: "\x1b[32m",
|
|
179
|
-
warning: "\x1b[33m",
|
|
180
|
-
error: "\x1b[31m",
|
|
181
|
-
dim: "\x1b[2m",
|
|
182
|
-
borderMuted: "\x1b[90m",
|
|
183
|
-
};
|
|
184
|
-
return `${c[color] ?? ""}${text}\x1b[0m`;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
private bold(text: string): string {
|
|
188
|
-
return this.theme ? this.theme.bold(text) : `\x1b[1m${text}\x1b[0m`;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
private frameLine(content: string, innerWidth: number): string {
|
|
192
|
-
const truncated = truncateToWidth(content, innerWidth, "");
|
|
193
|
-
const padding = Math.max(0, innerWidth - visibleWidth(truncated));
|
|
194
|
-
return `${this.fg("borderMuted", "│")}${truncated}${" ".repeat(padding)}${this.fg("borderMuted", "│")}`;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
private ruleLine(innerWidth: number): string {
|
|
198
|
-
return this.fg("borderMuted", `├${"─".repeat(innerWidth)}┤`);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
private borderLine(innerWidth: number, edge: "top" | "bottom"): string {
|
|
202
|
-
const left = edge === "top" ? "┌" : "└";
|
|
203
|
-
const right = edge === "top" ? "┐" : "┘";
|
|
204
|
-
return this.fg("borderMuted", `${left}${"─".repeat(innerWidth)}${right}`);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
172
|
render(width: number): string[] {
|
|
208
173
|
const innerWidth = boxInnerWidth(width);
|
|
209
174
|
const lines: string[] = [];
|
|
210
175
|
|
|
211
|
-
lines.push(this.borderLine(innerWidth, "top"));
|
|
176
|
+
lines.push(this.overlay.borderLine(innerWidth, "top"));
|
|
212
177
|
lines.push(
|
|
213
|
-
this.frameLine(
|
|
214
|
-
this.fg("accent", this.bold("🤖 Recap Model Selector")),
|
|
178
|
+
this.overlay.frameLine(
|
|
179
|
+
this.overlay.fg("accent", this.overlay.bold("🤖 Recap Model Selector")),
|
|
215
180
|
innerWidth
|
|
216
181
|
)
|
|
217
182
|
);
|
|
218
183
|
lines.push(
|
|
219
|
-
this.frameLine(
|
|
220
|
-
this.fg("dim", "Select model for notification recaps"),
|
|
184
|
+
this.overlay.frameLine(
|
|
185
|
+
this.overlay.fg("dim", "Select model for notification recaps"),
|
|
221
186
|
innerWidth
|
|
222
187
|
)
|
|
223
188
|
);
|
|
224
|
-
lines.push(this.ruleLine(innerWidth));
|
|
189
|
+
lines.push(this.overlay.ruleLine(innerWidth));
|
|
225
190
|
|
|
226
191
|
// Filter bar
|
|
227
192
|
if (this.filterMode) {
|
|
228
193
|
lines.push(
|
|
229
|
-
this.frameLine(
|
|
230
|
-
` ${this.fg("accent", "Filter:")} ${this.filter}${this.fg("accent", "█")}`,
|
|
194
|
+
this.overlay.frameLine(
|
|
195
|
+
` ${this.overlay.fg("accent", "Filter:")} ${this.filter}${this.overlay.fg("accent", "█")}`,
|
|
231
196
|
innerWidth
|
|
232
197
|
)
|
|
233
198
|
);
|
|
234
199
|
} else if (this.filter) {
|
|
235
200
|
lines.push(
|
|
236
|
-
this.frameLine(
|
|
237
|
-
` ${this.fg("dim", "Filter:")} ${this.filter} ${this.fg("dim", "(press / to edit)")}`,
|
|
201
|
+
this.overlay.frameLine(
|
|
202
|
+
` ${this.overlay.fg("dim", "Filter:")} ${this.filter} ${this.overlay.fg("dim", "(press / to edit)")}`,
|
|
238
203
|
innerWidth
|
|
239
204
|
)
|
|
240
205
|
);
|
|
241
206
|
} else {
|
|
242
207
|
lines.push(
|
|
243
|
-
this.frameLine(
|
|
244
|
-
` ${this.fg("dim", `/${this.models.length} models · press / to filter`)}`,
|
|
208
|
+
this.overlay.frameLine(
|
|
209
|
+
` ${this.overlay.fg("dim", `/${this.models.length} models · press / to filter`)}`,
|
|
245
210
|
innerWidth
|
|
246
211
|
)
|
|
247
212
|
);
|
|
248
213
|
}
|
|
249
|
-
lines.push(this.ruleLine(innerWidth));
|
|
214
|
+
lines.push(this.overlay.ruleLine(innerWidth));
|
|
250
215
|
|
|
251
216
|
// Model list
|
|
252
217
|
const terminalRows = process.stdout.rows ?? 30;
|
|
@@ -268,8 +233,8 @@ export class RecapModelSelectorOverlay implements Component {
|
|
|
268
233
|
? "No models — check ~/.pi/agent/models.json or API keys, then reopen"
|
|
269
234
|
: "No models found";
|
|
270
235
|
lines.push(
|
|
271
|
-
this.frameLine(
|
|
272
|
-
` ${this.fg("dim", emptyMsg)}`,
|
|
236
|
+
this.overlay.frameLine(
|
|
237
|
+
` ${this.overlay.fg("dim", emptyMsg)}`,
|
|
273
238
|
innerWidth
|
|
274
239
|
)
|
|
275
240
|
);
|
|
@@ -277,20 +242,20 @@ export class RecapModelSelectorOverlay implements Component {
|
|
|
277
242
|
for (let i = startIdx; i < endIdx; i++) {
|
|
278
243
|
const m = this.filteredModels[i];
|
|
279
244
|
const isSelected = i === this.selectedIndex;
|
|
280
|
-
const marker = isSelected ? this.fg("accent", "▸") : " ";
|
|
245
|
+
const marker = isSelected ? this.overlay.fg("accent", "▸") : " ";
|
|
281
246
|
const label = m.name || m.id;
|
|
282
247
|
const fullRef = `${m.provider}/${m.id}`;
|
|
283
248
|
const isDefault = fullRef === DEFAULT_MODEL;
|
|
284
249
|
const defaultTag = isDefault
|
|
285
|
-
? ` ${this.fg("warning", "(default)")}`
|
|
250
|
+
? ` ${this.overlay.fg("warning", "(default)")}`
|
|
286
251
|
: "";
|
|
287
252
|
|
|
288
|
-
const providerTag = this.fg("dim", `[${m.provider}]`);
|
|
253
|
+
const providerTag = this.overlay.fg("dim", `[${m.provider}]`);
|
|
289
254
|
const display = isSelected
|
|
290
|
-
? `${providerTag} ${this.bold(label)}${defaultTag}`
|
|
291
|
-
: `${providerTag} ${this.fg("dim", label)}${defaultTag}`;
|
|
255
|
+
? `${providerTag} ${this.overlay.bold(label)}${defaultTag}`
|
|
256
|
+
: `${providerTag} ${this.overlay.fg("dim", label)}${defaultTag}`;
|
|
292
257
|
|
|
293
|
-
lines.push(this.frameLine(` ${marker} ${display}`, innerWidth));
|
|
258
|
+
lines.push(this.overlay.frameLine(` ${marker} ${display}`, innerWidth));
|
|
294
259
|
}
|
|
295
260
|
}
|
|
296
261
|
|
|
@@ -300,8 +265,8 @@ export class RecapModelSelectorOverlay implements Component {
|
|
|
300
265
|
((this.selectedIndex + 1) / this.filteredModels.length) * 100
|
|
301
266
|
);
|
|
302
267
|
lines.push(
|
|
303
|
-
this.frameLine(
|
|
304
|
-
this.fg("dim", ` ${pct}% (${this.selectedIndex + 1}/${this.filteredModels.length})`),
|
|
268
|
+
this.overlay.frameLine(
|
|
269
|
+
this.overlay.fg("dim", ` ${pct}% (${this.selectedIndex + 1}/${this.filteredModels.length})`),
|
|
305
270
|
innerWidth
|
|
306
271
|
)
|
|
307
272
|
);
|
|
@@ -309,33 +274,33 @@ export class RecapModelSelectorOverlay implements Component {
|
|
|
309
274
|
|
|
310
275
|
// Status messages
|
|
311
276
|
if (this.error) {
|
|
312
|
-
lines.push(this.ruleLine(innerWidth));
|
|
277
|
+
lines.push(this.overlay.ruleLine(innerWidth));
|
|
313
278
|
lines.push(
|
|
314
|
-
this.frameLine(` ${this.fg("error", `⚠ ${this.error}`)}`, innerWidth)
|
|
279
|
+
this.overlay.frameLine(` ${this.overlay.fg("error", `⚠ ${this.error}`)}`, innerWidth)
|
|
315
280
|
);
|
|
316
281
|
}
|
|
317
282
|
if (this.saved) {
|
|
318
|
-
lines.push(this.ruleLine(innerWidth));
|
|
283
|
+
lines.push(this.overlay.ruleLine(innerWidth));
|
|
319
284
|
lines.push(
|
|
320
|
-
this.frameLine(
|
|
321
|
-
` ${this.fg("success", "✓ Model saved")}`,
|
|
285
|
+
this.overlay.frameLine(
|
|
286
|
+
` ${this.overlay.fg("success", "✓ Model saved")}`,
|
|
322
287
|
innerWidth
|
|
323
288
|
)
|
|
324
289
|
);
|
|
325
290
|
}
|
|
326
291
|
|
|
327
292
|
// Footer
|
|
328
|
-
lines.push(this.ruleLine(innerWidth));
|
|
293
|
+
lines.push(this.overlay.ruleLine(innerWidth));
|
|
329
294
|
lines.push(
|
|
330
|
-
this.frameLine(
|
|
331
|
-
this.fg(
|
|
295
|
+
this.overlay.frameLine(
|
|
296
|
+
this.overlay.fg(
|
|
332
297
|
"dim",
|
|
333
298
|
"↑↓ navigate · / filter · Enter select · Esc cancel"
|
|
334
299
|
),
|
|
335
300
|
innerWidth
|
|
336
301
|
)
|
|
337
302
|
);
|
|
338
|
-
lines.push(this.borderLine(innerWidth, "bottom"));
|
|
303
|
+
lines.push(this.overlay.borderLine(innerWidth, "bottom"));
|
|
339
304
|
|
|
340
305
|
return lines;
|
|
341
306
|
}
|
package/tui/settings-overlay.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Component } from "@earendil-works/pi-tui";
|
|
9
|
-
import { matchesKey
|
|
9
|
+
import { matchesKey } from "@earendil-works/pi-tui";
|
|
10
10
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import {
|
|
12
12
|
loadConfig,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from "../settings.js";
|
|
16
16
|
import { loadNtfyConfig, saveNtfyConfig, getNtfyConfigScope } from "../ntfy-config.js";
|
|
17
17
|
import type { NotifyConfig, NtfyConfig } from "../types.js";
|
|
18
|
-
import { boxInnerWidth } from "@pi-unipi/core";
|
|
18
|
+
import { OverlayTheme, boxInnerWidth } from "@pi-unipi/core";
|
|
19
19
|
|
|
20
20
|
/** Section types */
|
|
21
21
|
type Section = "platforms" | "events" | "recap";
|
|
@@ -35,7 +35,7 @@ export class NotifySettingsOverlay implements Component {
|
|
|
35
35
|
requestRender?: () => void;
|
|
36
36
|
/** Called when user presses M in recap section to open model selector */
|
|
37
37
|
onOpenModelSelector?: () => void;
|
|
38
|
-
private
|
|
38
|
+
private overlay = new OverlayTheme();
|
|
39
39
|
|
|
40
40
|
constructor() {
|
|
41
41
|
this.config = loadConfig();
|
|
@@ -45,7 +45,7 @@ export class NotifySettingsOverlay implements Component {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
setTheme(theme: Theme): void {
|
|
48
|
-
this.theme
|
|
48
|
+
this.overlay.setTheme(theme);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
invalidate(): void {}
|
|
@@ -151,66 +151,30 @@ export class NotifySettingsOverlay implements Component {
|
|
|
151
151
|
setTimeout(() => this.onClose?.(), 500);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
// ─── Theme helpers ───────────────────────────────────────────────────
|
|
155
|
-
|
|
156
|
-
private fg(color: string, text: string): string {
|
|
157
|
-
if (this.theme) return this.theme.fg(color as any, text);
|
|
158
|
-
const c: Record<string, string> = {
|
|
159
|
-
accent: "\x1b[36m", success: "\x1b[32m", warning: "\x1b[33m",
|
|
160
|
-
error: "\x1b[31m", dim: "\x1b[2m", borderMuted: "\x1b[90m",
|
|
161
|
-
};
|
|
162
|
-
return `${c[color] ?? ""}${text}\x1b[0m`;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
private bold(text: string): string {
|
|
166
|
-
return this.theme ? this.theme.bold(text) : `\x1b[1m${text}\x1b[0m`;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
private frameLine(content: string, innerWidth: number): string {
|
|
170
|
-
const truncated = truncateToWidth(content, innerWidth, "");
|
|
171
|
-
const padding = Math.max(0, innerWidth - visibleWidth(truncated));
|
|
172
|
-
return `${this.fg("borderMuted", "│")}${truncated}${" ".repeat(padding)}${this.fg("borderMuted", "│")}`;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
private ruleLine(innerWidth: number): string {
|
|
176
|
-
return this.fg("borderMuted", `├${"─".repeat(innerWidth)}┤`);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
private borderLine(innerWidth: number, edge: "top" | "bottom"): string {
|
|
180
|
-
const left = edge === "top" ? "┌" : "└";
|
|
181
|
-
const right = edge === "top" ? "┐" : "┘";
|
|
182
|
-
return this.fg("borderMuted", `${left}${"─".repeat(innerWidth)}${right}`);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
private getDialogHeight(): number {
|
|
186
|
-
const terminalRows = process.stdout.rows ?? 30;
|
|
187
|
-
return Math.max(14, Math.min(24, Math.floor(terminalRows * 0.65)));
|
|
188
|
-
}
|
|
189
|
-
|
|
190
154
|
render(width: number): string[] {
|
|
191
155
|
const innerWidth = boxInnerWidth(width);
|
|
192
156
|
const lines: string[] = [];
|
|
193
157
|
|
|
194
|
-
lines.push(this.borderLine(innerWidth, "top"));
|
|
195
|
-
lines.push(this.frameLine(this.fg("accent", this.bold("🔔 Notify Settings")), innerWidth));
|
|
196
|
-
lines.push(this.frameLine(this.fg("dim", "Configure notification platforms and events"), innerWidth));
|
|
197
|
-
lines.push(this.ruleLine(innerWidth));
|
|
158
|
+
lines.push(this.overlay.borderLine(innerWidth, "top"));
|
|
159
|
+
lines.push(this.overlay.frameLine(this.overlay.fg("accent", this.overlay.bold("🔔 Notify Settings")), innerWidth));
|
|
160
|
+
lines.push(this.overlay.frameLine(this.overlay.fg("dim", "Configure notification platforms and events"), innerWidth));
|
|
161
|
+
lines.push(this.overlay.ruleLine(innerWidth));
|
|
198
162
|
|
|
199
163
|
// Section tabs
|
|
200
164
|
const platformTab =
|
|
201
165
|
this.section === "platforms"
|
|
202
|
-
? this.fg("accent", this.bold("[Platforms]"))
|
|
203
|
-
: this.fg("dim", "Platforms");
|
|
166
|
+
? this.overlay.fg("accent", this.overlay.bold("[Platforms]"))
|
|
167
|
+
: this.overlay.fg("dim", "Platforms");
|
|
204
168
|
const eventsTab =
|
|
205
169
|
this.section === "events"
|
|
206
|
-
? this.fg("accent", this.bold("[Events]"))
|
|
207
|
-
: this.fg("dim", "Events");
|
|
170
|
+
? this.overlay.fg("accent", this.overlay.bold("[Events]"))
|
|
171
|
+
: this.overlay.fg("dim", "Events");
|
|
208
172
|
const recapTab =
|
|
209
173
|
this.section === "recap"
|
|
210
|
-
? this.fg("accent", this.bold("[Recap]"))
|
|
211
|
-
: this.fg("dim", "Recap");
|
|
212
|
-
lines.push(this.frameLine(` ${platformTab} ${eventsTab} ${recapTab}`, innerWidth));
|
|
213
|
-
lines.push(this.ruleLine(innerWidth));
|
|
174
|
+
? this.overlay.fg("accent", this.overlay.bold("[Recap]"))
|
|
175
|
+
: this.overlay.fg("dim", "Recap");
|
|
176
|
+
lines.push(this.overlay.frameLine(` ${platformTab} ${eventsTab} ${recapTab}`, innerWidth));
|
|
177
|
+
lines.push(this.overlay.ruleLine(innerWidth));
|
|
214
178
|
|
|
215
179
|
if (this.section === "platforms") {
|
|
216
180
|
this.renderPlatforms(lines, innerWidth);
|
|
@@ -222,21 +186,21 @@ export class NotifySettingsOverlay implements Component {
|
|
|
222
186
|
|
|
223
187
|
// Status messages
|
|
224
188
|
if (this.error) {
|
|
225
|
-
lines.push(this.ruleLine(innerWidth));
|
|
226
|
-
lines.push(this.frameLine(` ${this.fg("error", `⚠ ${this.error}`)}`, innerWidth));
|
|
189
|
+
lines.push(this.overlay.ruleLine(innerWidth));
|
|
190
|
+
lines.push(this.overlay.frameLine(` ${this.overlay.fg("error", `⚠ ${this.error}`)}`, innerWidth));
|
|
227
191
|
}
|
|
228
192
|
if (this.saved) {
|
|
229
|
-
lines.push(this.ruleLine(innerWidth));
|
|
230
|
-
lines.push(this.frameLine(` ${this.fg("success", "✓ Settings saved")}`, innerWidth));
|
|
193
|
+
lines.push(this.overlay.ruleLine(innerWidth));
|
|
194
|
+
lines.push(this.overlay.frameLine(` ${this.overlay.fg("success", "✓ Settings saved")}`, innerWidth));
|
|
231
195
|
}
|
|
232
196
|
|
|
233
197
|
// Footer
|
|
234
|
-
lines.push(this.ruleLine(innerWidth));
|
|
198
|
+
lines.push(this.overlay.ruleLine(innerWidth));
|
|
235
199
|
const footerHint = this.section === "recap"
|
|
236
200
|
? "↑↓ navigate · Space toggle · M change model · Tab switch · Enter save · Esc cancel"
|
|
237
201
|
: "↑↓ navigate · Space toggle · Tab switch · Enter save · Esc cancel";
|
|
238
|
-
lines.push(this.frameLine(this.fg("dim", footerHint), innerWidth));
|
|
239
|
-
lines.push(this.borderLine(innerWidth, "bottom"));
|
|
202
|
+
lines.push(this.overlay.frameLine(this.overlay.fg("dim", footerHint), innerWidth));
|
|
203
|
+
lines.push(this.overlay.borderLine(innerWidth, "bottom"));
|
|
240
204
|
|
|
241
205
|
return lines;
|
|
242
206
|
}
|
|
@@ -278,16 +242,16 @@ export class NotifySettingsOverlay implements Component {
|
|
|
278
242
|
for (let i = 0; i < platforms.length; i++) {
|
|
279
243
|
const p = platforms[i];
|
|
280
244
|
const isSelected = i === this.selectedIndex;
|
|
281
|
-
const toggleOn = this.fg("success", "●");
|
|
282
|
-
const toggleOff = this.fg("dim", "○");
|
|
245
|
+
const toggleOn = this.overlay.fg("success", "●");
|
|
246
|
+
const toggleOff = this.overlay.fg("dim", "○");
|
|
283
247
|
// ntfy enabled state comes from resolved ntfy.json, not config.json
|
|
284
248
|
const isEnabled = p.key === "ntfy" ? this.ntfyConfig.enabled : this.config[p.key].enabled;
|
|
285
249
|
const toggle = isEnabled ? toggleOn : toggleOff;
|
|
286
|
-
const label = isSelected ? this.bold(p.label) : this.fg("dim", p.label);
|
|
250
|
+
const label = isSelected ? this.overlay.bold(p.label) : this.overlay.fg("dim", p.label);
|
|
287
251
|
|
|
288
252
|
lines.push(
|
|
289
|
-
this.frameLine(
|
|
290
|
-
`${isSelected ? this.fg("accent", "▸") : " "} ${toggle} ${label} ${this.fg("dim", p.detail)}`,
|
|
253
|
+
this.overlay.frameLine(
|
|
254
|
+
`${isSelected ? this.overlay.fg("accent", "▸") : " "} ${toggle} ${label} ${this.overlay.fg("dim", p.detail)}`,
|
|
291
255
|
innerWidth
|
|
292
256
|
)
|
|
293
257
|
);
|
|
@@ -298,17 +262,17 @@ export class NotifySettingsOverlay implements Component {
|
|
|
298
262
|
const i = platforms.length;
|
|
299
263
|
const isSelected = i === this.selectedIndex;
|
|
300
264
|
const isEnabled = this.config.native.suppressWhenFocused === true;
|
|
301
|
-
const toggleOn = this.fg("success", "●");
|
|
302
|
-
const toggleOff = this.fg("dim", "○");
|
|
265
|
+
const toggleOn = this.overlay.fg("success", "●");
|
|
266
|
+
const toggleOff = this.overlay.fg("dim", "○");
|
|
303
267
|
const toggle = isEnabled ? toggleOn : toggleOff;
|
|
304
268
|
const label = isSelected
|
|
305
|
-
? this.bold("Suppress when focused")
|
|
306
|
-
: this.fg("dim", "Suppress when focused");
|
|
307
|
-
const detail = this.fg("dim", isEnabled ? "Windows only — terminal in foreground → skip" : "Windows only");
|
|
269
|
+
? this.overlay.bold("Suppress when focused")
|
|
270
|
+
: this.overlay.fg("dim", "Suppress when focused");
|
|
271
|
+
const detail = this.overlay.fg("dim", isEnabled ? "Windows only — terminal in foreground → skip" : "Windows only");
|
|
308
272
|
|
|
309
273
|
lines.push(
|
|
310
|
-
this.frameLine(
|
|
311
|
-
`${isSelected ? this.fg("accent", "▸") : " "} ${toggle} ${label} ${detail}`,
|
|
274
|
+
this.overlay.frameLine(
|
|
275
|
+
`${isSelected ? this.overlay.fg("accent", "▸") : " "} ${toggle} ${label} ${detail}`,
|
|
312
276
|
innerWidth
|
|
313
277
|
)
|
|
314
278
|
);
|
|
@@ -321,14 +285,14 @@ export class NotifySettingsOverlay implements Component {
|
|
|
321
285
|
for (let i = 0; i < events.length; i++) {
|
|
322
286
|
const [key, cfg] = events[i];
|
|
323
287
|
const isSelected = i === this.selectedIndex;
|
|
324
|
-
const toggleOn = this.fg("success", "●");
|
|
325
|
-
const toggleOff = this.fg("dim", "○");
|
|
288
|
+
const toggleOn = this.overlay.fg("success", "●");
|
|
289
|
+
const toggleOff = this.overlay.fg("dim", "○");
|
|
326
290
|
const toggle = cfg.enabled ? toggleOn : toggleOff;
|
|
327
|
-
const label = isSelected ? this.bold(key) : this.fg("dim", key);
|
|
291
|
+
const label = isSelected ? this.overlay.bold(key) : this.overlay.fg("dim", key);
|
|
328
292
|
|
|
329
293
|
lines.push(
|
|
330
|
-
this.frameLine(
|
|
331
|
-
`${isSelected ? this.fg("accent", "▸") : " "} ${toggle} ${label}`,
|
|
294
|
+
this.overlay.frameLine(
|
|
295
|
+
`${isSelected ? this.overlay.fg("accent", "▸") : " "} ${toggle} ${label}`,
|
|
332
296
|
innerWidth
|
|
333
297
|
)
|
|
334
298
|
);
|
|
@@ -338,27 +302,27 @@ export class NotifySettingsOverlay implements Component {
|
|
|
338
302
|
private renderRecap(lines: string[], innerWidth: number): void {
|
|
339
303
|
// Toggle
|
|
340
304
|
const isSelected = this.selectedIndex === 0;
|
|
341
|
-
const toggleOn = this.fg("success", "●");
|
|
342
|
-
const toggleOff = this.fg("dim", "○");
|
|
305
|
+
const toggleOn = this.overlay.fg("success", "●");
|
|
306
|
+
const toggleOff = this.overlay.fg("dim", "○");
|
|
343
307
|
const toggle = this.config.recap.enabled ? toggleOn : toggleOff;
|
|
344
308
|
const label = isSelected
|
|
345
|
-
? this.bold("Enable Recap")
|
|
346
|
-
: this.fg("dim", "Enable Recap");
|
|
309
|
+
? this.overlay.bold("Enable Recap")
|
|
310
|
+
: this.overlay.fg("dim", "Enable Recap");
|
|
347
311
|
|
|
348
312
|
lines.push(
|
|
349
|
-
this.frameLine(
|
|
350
|
-
`${isSelected ? this.fg("accent", "▸") : " "} ${toggle} ${label}`,
|
|
313
|
+
this.overlay.frameLine(
|
|
314
|
+
`${isSelected ? this.overlay.fg("accent", "▸") : " "} ${toggle} ${label}`,
|
|
351
315
|
innerWidth
|
|
352
316
|
)
|
|
353
317
|
);
|
|
354
318
|
|
|
355
319
|
// Current model display
|
|
356
320
|
const modelRef = this.config.recap.model;
|
|
357
|
-
const modelLabel = this.fg("dim", ` Model: ${modelRef}`);
|
|
358
|
-
lines.push(this.frameLine(modelLabel, innerWidth));
|
|
321
|
+
const modelLabel = this.overlay.fg("dim", ` Model: ${modelRef}`);
|
|
322
|
+
lines.push(this.overlay.frameLine(modelLabel, innerWidth));
|
|
359
323
|
lines.push(
|
|
360
|
-
this.frameLine(
|
|
361
|
-
this.fg("dim", " Press M to change model"),
|
|
324
|
+
this.overlay.frameLine(
|
|
325
|
+
this.overlay.fg("dim", " Press M to change model"),
|
|
362
326
|
innerWidth
|
|
363
327
|
)
|
|
364
328
|
);
|
package/tui/telegram-setup.ts
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Component } from "@earendil-works/pi-tui";
|
|
9
|
-
import { matchesKey
|
|
9
|
+
import { matchesKey } from "@earendil-works/pi-tui";
|
|
10
10
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { pollForChatId } from "../platforms/telegram.js";
|
|
12
12
|
import { updateConfig } from "../settings.js";
|
|
13
|
-
import { boxInnerWidth } from "@pi-unipi/core";
|
|
13
|
+
import { OverlayTheme, boxInnerWidth } from "@pi-unipi/core";
|
|
14
14
|
|
|
15
15
|
type SetupPhase = "instructions" | "token" | "polling" | "success" | "error" | "timeout";
|
|
16
16
|
|
|
@@ -35,10 +35,10 @@ export class TelegramSetupOverlay implements Component {
|
|
|
35
35
|
private pasteBuffer = "";
|
|
36
36
|
onClose?: () => void;
|
|
37
37
|
requestRender?: () => void;
|
|
38
|
-
private
|
|
38
|
+
private overlay = new OverlayTheme();
|
|
39
39
|
|
|
40
40
|
setTheme(theme: Theme): void {
|
|
41
|
-
this.theme
|
|
41
|
+
this.overlay.setTheme(theme);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
invalidate(): void {}
|
|
@@ -193,110 +193,79 @@ export class TelegramSetupOverlay implements Component {
|
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
// ─── Theme helpers ───────────────────────────────────────────────────
|
|
197
|
-
|
|
198
|
-
private fg(color: string, text: string): string {
|
|
199
|
-
if (this.theme) return this.theme.fg(color as any, text);
|
|
200
|
-
const c: Record<string, string> = {
|
|
201
|
-
accent: "\x1b[36m", success: "\x1b[32m", warning: "\x1b[33m",
|
|
202
|
-
error: "\x1b[31m", dim: "\x1b[2m", borderMuted: "\x1b[90m",
|
|
203
|
-
};
|
|
204
|
-
return `${c[color] ?? ""}${text}\x1b[0m`;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
private bold(text: string): string {
|
|
208
|
-
return this.theme ? this.theme.bold(text) : `\x1b[1m${text}\x1b[0m`;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
private frameLine(content: string, innerWidth: number): string {
|
|
212
|
-
const truncated = truncateToWidth(content, innerWidth, "");
|
|
213
|
-
const padding = Math.max(0, innerWidth - visibleWidth(truncated));
|
|
214
|
-
return `${this.fg("borderMuted", "│")}${truncated}${" ".repeat(padding)}${this.fg("borderMuted", "│")}`;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
private ruleLine(innerWidth: number): string {
|
|
218
|
-
return this.fg("borderMuted", `├${"─".repeat(innerWidth)}┤`);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
private borderLine(innerWidth: number, edge: "top" | "bottom"): string {
|
|
222
|
-
const left = edge === "top" ? "┌" : "└";
|
|
223
|
-
const right = edge === "top" ? "┐" : "┘";
|
|
224
|
-
return this.fg("borderMuted", `${left}${"─".repeat(innerWidth)}${right}`);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
196
|
render(width: number): string[] {
|
|
228
197
|
const innerWidth = boxInnerWidth(width);
|
|
229
198
|
const lines: string[] = [];
|
|
230
199
|
|
|
231
|
-
lines.push(this.borderLine(innerWidth, "top"));
|
|
232
|
-
lines.push(this.frameLine(this.fg("accent", this.bold("🤖 Telegram Bot Setup")), innerWidth));
|
|
233
|
-
lines.push(this.ruleLine(innerWidth));
|
|
200
|
+
lines.push(this.overlay.borderLine(innerWidth, "top"));
|
|
201
|
+
lines.push(this.overlay.frameLine(this.overlay.fg("accent", this.overlay.bold("🤖 Telegram Bot Setup")), innerWidth));
|
|
202
|
+
lines.push(this.overlay.ruleLine(innerWidth));
|
|
234
203
|
|
|
235
204
|
switch (this.phase) {
|
|
236
205
|
case "instructions":
|
|
237
|
-
lines.push(this.frameLine(this.fg("dim", "Set up Telegram notifications in 3 steps:"), innerWidth));
|
|
238
|
-
lines.push(this.frameLine("", innerWidth));
|
|
239
|
-
lines.push(this.frameLine(` ${this.bold("1.")} Open Telegram and message ${this.fg("accent", "@BotFather")}`, innerWidth));
|
|
240
|
-
lines.push(this.frameLine(` Send /newbot and follow the prompts to create a bot`, innerWidth));
|
|
241
|
-
lines.push(this.frameLine("", innerWidth));
|
|
242
|
-
lines.push(this.frameLine(` ${this.bold("2.")} Copy the bot token from BotFather`, innerWidth));
|
|
243
|
-
lines.push(this.frameLine("", innerWidth));
|
|
244
|
-
lines.push(this.frameLine(` ${this.bold("3.")} Send any message to your new bot`, innerWidth));
|
|
245
|
-
lines.push(this.frameLine(` (We'll detect your chat ID automatically)`, innerWidth));
|
|
246
|
-
lines.push(this.ruleLine(innerWidth));
|
|
247
|
-
lines.push(this.frameLine(this.fg("dim", "Press Enter to continue, Esc to cancel"), innerWidth));
|
|
206
|
+
lines.push(this.overlay.frameLine(this.overlay.fg("dim", "Set up Telegram notifications in 3 steps:"), innerWidth));
|
|
207
|
+
lines.push(this.overlay.frameLine("", innerWidth));
|
|
208
|
+
lines.push(this.overlay.frameLine(` ${this.overlay.bold("1.")} Open Telegram and message ${this.overlay.fg("accent", "@BotFather")}`, innerWidth));
|
|
209
|
+
lines.push(this.overlay.frameLine(` Send /newbot and follow the prompts to create a bot`, innerWidth));
|
|
210
|
+
lines.push(this.overlay.frameLine("", innerWidth));
|
|
211
|
+
lines.push(this.overlay.frameLine(` ${this.overlay.bold("2.")} Copy the bot token from BotFather`, innerWidth));
|
|
212
|
+
lines.push(this.overlay.frameLine("", innerWidth));
|
|
213
|
+
lines.push(this.overlay.frameLine(` ${this.overlay.bold("3.")} Send any message to your new bot`, innerWidth));
|
|
214
|
+
lines.push(this.overlay.frameLine(` (We'll detect your chat ID automatically)`, innerWidth));
|
|
215
|
+
lines.push(this.overlay.ruleLine(innerWidth));
|
|
216
|
+
lines.push(this.overlay.frameLine(this.overlay.fg("dim", "Press Enter to continue, Esc to cancel"), innerWidth));
|
|
248
217
|
break;
|
|
249
218
|
|
|
250
219
|
case "token":
|
|
251
|
-
lines.push(this.frameLine(this.fg("dim", "Paste your bot token from BotFather:"), innerWidth));
|
|
252
|
-
lines.push(this.frameLine("", innerWidth));
|
|
253
|
-
const display = this.fg("accent", this.bold(this.botToken || " "));
|
|
254
|
-
lines.push(this.frameLine(` ${display}${this.fg("dim", "█")}`, innerWidth));
|
|
255
|
-
lines.push(this.frameLine("", innerWidth));
|
|
256
|
-
lines.push(this.frameLine(this.fg("dim", "Enter to start polling · Esc to cancel"), innerWidth));
|
|
220
|
+
lines.push(this.overlay.frameLine(this.overlay.fg("dim", "Paste your bot token from BotFather:"), innerWidth));
|
|
221
|
+
lines.push(this.overlay.frameLine("", innerWidth));
|
|
222
|
+
const display = this.overlay.fg("accent", this.overlay.bold(this.botToken || " "));
|
|
223
|
+
lines.push(this.overlay.frameLine(` ${display}${this.overlay.fg("dim", "█")}`, innerWidth));
|
|
224
|
+
lines.push(this.overlay.frameLine("", innerWidth));
|
|
225
|
+
lines.push(this.overlay.frameLine(this.overlay.fg("dim", "Enter to start polling · Esc to cancel"), innerWidth));
|
|
257
226
|
break;
|
|
258
227
|
|
|
259
228
|
case "polling": {
|
|
260
229
|
const frame = SPINNER_FRAMES[this.spinnerFrame] || "⠋";
|
|
261
230
|
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
|
|
262
231
|
const remaining = Math.max(0, 300 - elapsed);
|
|
263
|
-
lines.push(this.frameLine(` ${this.fg("accent", frame)} ${this.bold("Waiting for first message...")}`, innerWidth));
|
|
264
|
-
lines.push(this.frameLine("", innerWidth));
|
|
265
|
-
lines.push(this.frameLine(` ${this.fg("dim", "Send any message to your bot in Telegram")}`, innerWidth));
|
|
266
|
-
lines.push(this.frameLine(` ${this.fg("dim", `Timeout: ${Math.floor(remaining / 60)}:${String(remaining % 60).padStart(2, "0")}`)}`, innerWidth));
|
|
267
|
-
lines.push(this.ruleLine(innerWidth));
|
|
268
|
-
lines.push(this.frameLine(this.fg("dim", "Esc to cancel"), innerWidth));
|
|
232
|
+
lines.push(this.overlay.frameLine(` ${this.overlay.fg("accent", frame)} ${this.overlay.bold("Waiting for first message...")}`, innerWidth));
|
|
233
|
+
lines.push(this.overlay.frameLine("", innerWidth));
|
|
234
|
+
lines.push(this.overlay.frameLine(` ${this.overlay.fg("dim", "Send any message to your bot in Telegram")}`, innerWidth));
|
|
235
|
+
lines.push(this.overlay.frameLine(` ${this.overlay.fg("dim", `Timeout: ${Math.floor(remaining / 60)}:${String(remaining % 60).padStart(2, "0")}`)}`, innerWidth));
|
|
236
|
+
lines.push(this.overlay.ruleLine(innerWidth));
|
|
237
|
+
lines.push(this.overlay.frameLine(this.overlay.fg("dim", "Esc to cancel"), innerWidth));
|
|
269
238
|
break;
|
|
270
239
|
}
|
|
271
240
|
|
|
272
241
|
case "success":
|
|
273
|
-
lines.push(this.frameLine(` ${this.fg("success", "✓ Telegram bot configured!")}`, innerWidth));
|
|
274
|
-
lines.push(this.frameLine("", innerWidth));
|
|
275
|
-
lines.push(this.frameLine(` ${this.fg("dim", `Chat ID: ${this.chatId}`)}`, innerWidth));
|
|
276
|
-
lines.push(this.frameLine(` ${this.fg("dim", "Notifications will be sent to this chat")}`, innerWidth));
|
|
277
|
-
lines.push(this.ruleLine(innerWidth));
|
|
278
|
-
lines.push(this.frameLine(this.fg("dim", "Press Enter to close"), innerWidth));
|
|
242
|
+
lines.push(this.overlay.frameLine(` ${this.overlay.fg("success", "✓ Telegram bot configured!")}`, innerWidth));
|
|
243
|
+
lines.push(this.overlay.frameLine("", innerWidth));
|
|
244
|
+
lines.push(this.overlay.frameLine(` ${this.overlay.fg("dim", `Chat ID: ${this.chatId}`)}`, innerWidth));
|
|
245
|
+
lines.push(this.overlay.frameLine(` ${this.overlay.fg("dim", "Notifications will be sent to this chat")}`, innerWidth));
|
|
246
|
+
lines.push(this.overlay.ruleLine(innerWidth));
|
|
247
|
+
lines.push(this.overlay.frameLine(this.overlay.fg("dim", "Press Enter to close"), innerWidth));
|
|
279
248
|
break;
|
|
280
249
|
|
|
281
250
|
case "error":
|
|
282
|
-
lines.push(this.frameLine(` ${this.fg("error", "✗ Setup failed")}`, innerWidth));
|
|
283
|
-
lines.push(this.frameLine("", innerWidth));
|
|
284
|
-
lines.push(this.frameLine(` ${this.fg("dim", this.error || "Unknown error")}`, innerWidth));
|
|
285
|
-
lines.push(this.ruleLine(innerWidth));
|
|
286
|
-
lines.push(this.frameLine(this.fg("dim", "Press Enter to close"), innerWidth));
|
|
251
|
+
lines.push(this.overlay.frameLine(` ${this.overlay.fg("error", "✗ Setup failed")}`, innerWidth));
|
|
252
|
+
lines.push(this.overlay.frameLine("", innerWidth));
|
|
253
|
+
lines.push(this.overlay.frameLine(` ${this.overlay.fg("dim", this.error || "Unknown error")}`, innerWidth));
|
|
254
|
+
lines.push(this.overlay.ruleLine(innerWidth));
|
|
255
|
+
lines.push(this.overlay.frameLine(this.overlay.fg("dim", "Press Enter to close"), innerWidth));
|
|
287
256
|
break;
|
|
288
257
|
|
|
289
258
|
case "timeout":
|
|
290
|
-
lines.push(this.frameLine(` ${this.fg("warning", "⏰ Timed out after 5 minutes")}`, innerWidth));
|
|
291
|
-
lines.push(this.frameLine("", innerWidth));
|
|
292
|
-
lines.push(this.frameLine(` ${this.fg("dim", "Make sure you sent a message to your bot in Telegram")}`, innerWidth));
|
|
293
|
-
lines.push(this.frameLine(` ${this.fg("dim", "You can try again with /unipi:notify-set-tg")}`, innerWidth));
|
|
294
|
-
lines.push(this.ruleLine(innerWidth));
|
|
295
|
-
lines.push(this.frameLine(this.fg("dim", "Press Enter to close"), innerWidth));
|
|
259
|
+
lines.push(this.overlay.frameLine(` ${this.overlay.fg("warning", "⏰ Timed out after 5 minutes")}`, innerWidth));
|
|
260
|
+
lines.push(this.overlay.frameLine("", innerWidth));
|
|
261
|
+
lines.push(this.overlay.frameLine(` ${this.overlay.fg("dim", "Make sure you sent a message to your bot in Telegram")}`, innerWidth));
|
|
262
|
+
lines.push(this.overlay.frameLine(` ${this.overlay.fg("dim", "You can try again with /unipi:notify-set-tg")}`, innerWidth));
|
|
263
|
+
lines.push(this.overlay.ruleLine(innerWidth));
|
|
264
|
+
lines.push(this.overlay.frameLine(this.overlay.fg("dim", "Press Enter to close"), innerWidth));
|
|
296
265
|
break;
|
|
297
266
|
}
|
|
298
267
|
|
|
299
|
-
lines.push(this.borderLine(innerWidth, "bottom"));
|
|
268
|
+
lines.push(this.overlay.borderLine(innerWidth, "bottom"));
|
|
300
269
|
return lines;
|
|
301
270
|
}
|
|
302
271
|
}
|