@pi-unipi/notify 2.6.0 → 2.6.2
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/README.md +1 -0
- package/commands.ts +61 -3
- package/events.ts +0 -10
- package/package.json +5 -3
- 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 +107 -101
- package/tui/settings-overlay.ts +94 -117
- package/tui/telegram-setup.ts +48 -79
- package/types.ts +0 -2
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
* @pi-unipi/notify — Recap Model Selector TUI
|
|
3
3
|
*
|
|
4
4
|
* Interactive overlay for selecting the recap summarization model.
|
|
5
|
-
* Uses
|
|
5
|
+
* Uses models injected from Pi's live model registry (preferred), falling back
|
|
6
|
+
* to the project-wide cached model list from ~/.unipi/config/models-cache.json.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import type { Component } from "@earendil-works/pi-tui";
|
|
9
|
-
import { matchesKey
|
|
10
|
+
import { matchesKey } from "@earendil-works/pi-tui";
|
|
10
11
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import { readModelCache, type CachedModel, boxInnerWidth } from "@pi-unipi/core";
|
|
12
|
+
import { readModelCache, type CachedModel, boxInnerWidth, OverlayTheme } from "@pi-unipi/core";
|
|
12
13
|
import { loadConfig, saveConfig } from "../settings.js";
|
|
13
14
|
|
|
14
15
|
const DEFAULT_MODEL = "openrouter/openai/gpt-oss-20b";
|
|
@@ -26,11 +27,18 @@ export class RecapModelSelectorOverlay implements Component {
|
|
|
26
27
|
private error: string | null = null;
|
|
27
28
|
onClose?: () => void;
|
|
28
29
|
requestRender?: () => void;
|
|
29
|
-
private
|
|
30
|
+
private overlay = new OverlayTheme();
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
/**
|
|
33
|
+
* @param models Optional model list, preferably collected from Pi's live
|
|
34
|
+
* model registry by the command handler. Falls back to the project-wide
|
|
35
|
+
* cache file (`~/.unipi/config/models-cache.json`), which may not exist
|
|
36
|
+
* (issue #27: selector showed "No models found" while Pi itself had
|
|
37
|
+
* models configured in `~/.pi/agent/models.json`).
|
|
38
|
+
*/
|
|
39
|
+
constructor(models?: CachedModel[]) {
|
|
40
|
+
// Prefer models injected from Pi's live model registry; fall back to cache.
|
|
41
|
+
this.models = models ?? readModelCache();
|
|
34
42
|
this.applyFilter();
|
|
35
43
|
|
|
36
44
|
// Pre-select current config model
|
|
@@ -43,29 +51,48 @@ export class RecapModelSelectorOverlay implements Component {
|
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
setTheme(theme: Theme): void {
|
|
46
|
-
this.theme
|
|
54
|
+
this.overlay.setTheme(theme);
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
invalidate(): void {}
|
|
50
58
|
|
|
51
59
|
handleInput(data: string): void {
|
|
60
|
+
// Ctrl+C must always close, even mid-filter — without this the overlay
|
|
61
|
+
// could trap the user on terminals with unexpected key encodings.
|
|
62
|
+
if (matchesKey(data, "ctrl+c")) {
|
|
63
|
+
this.onClose?.();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
52
67
|
// Filter mode: type to search
|
|
53
68
|
if (this.filterMode) {
|
|
54
|
-
if (data
|
|
69
|
+
if (matchesKey(data, "enter")) {
|
|
55
70
|
// Enter — exit filter mode
|
|
56
71
|
this.filterMode = false;
|
|
57
72
|
return;
|
|
58
73
|
}
|
|
59
74
|
if (matchesKey(data, "escape")) {
|
|
60
|
-
// Escape — clear filter and exit filter mode
|
|
75
|
+
// Escape — clear filter and exit filter mode (does NOT close overlay)
|
|
61
76
|
this.filter = "";
|
|
62
77
|
this.filterMode = false;
|
|
63
78
|
this.applyFilter();
|
|
64
79
|
this.selectedIndex = 0;
|
|
65
80
|
return;
|
|
66
81
|
}
|
|
67
|
-
|
|
68
|
-
|
|
82
|
+
// Let the list be navigated without leaving filter mode.
|
|
83
|
+
if (matchesKey(data, "up")) {
|
|
84
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (matchesKey(data, "down")) {
|
|
88
|
+
this.selectedIndex = Math.min(
|
|
89
|
+
Math.max(0, this.filteredModels.length - 1),
|
|
90
|
+
this.selectedIndex + 1
|
|
91
|
+
);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (matchesKey(data, "backspace") || data === "\x7f" || data === "\b") {
|
|
95
|
+
// Backspace (legacy or kitty "\x1b[127u")
|
|
69
96
|
this.filter = this.filter.slice(0, -1);
|
|
70
97
|
this.applyFilter();
|
|
71
98
|
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));
|
|
@@ -80,28 +107,36 @@ export class RecapModelSelectorOverlay implements Component {
|
|
|
80
107
|
return;
|
|
81
108
|
}
|
|
82
109
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
// Keys are matched via matchesKey, never raw byte comparison: under the
|
|
111
|
+
// kitty keyboard protocol / enhanced encodings (Ghostty, Herdr) Escape
|
|
112
|
+
// arrives as "\x1b[27u" and arrows as "\x1b[57419u"/"\x1b[57420u", so
|
|
113
|
+
// exact legacy comparisons silently fail there (issue #27).
|
|
114
|
+
if (matchesKey(data, "up") || data === "k") {
|
|
115
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (matchesKey(data, "down") || data === "j") {
|
|
119
|
+
this.selectedIndex = Math.min(
|
|
120
|
+
Math.max(0, this.filteredModels.length - 1),
|
|
121
|
+
this.selectedIndex + 1
|
|
122
|
+
);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (data === "/") {
|
|
126
|
+
// Start filter
|
|
127
|
+
this.filterMode = true;
|
|
128
|
+
this.filter = "";
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (matchesKey(data, "enter")) {
|
|
132
|
+
// Enter — select and save
|
|
133
|
+
this.selectModel();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (matchesKey(data, "escape")) {
|
|
137
|
+
// Escape — close
|
|
138
|
+
this.onClose?.();
|
|
139
|
+
return;
|
|
105
140
|
}
|
|
106
141
|
}
|
|
107
142
|
|
|
@@ -134,84 +169,49 @@ export class RecapModelSelectorOverlay implements Component {
|
|
|
134
169
|
setTimeout(() => this.onClose?.(), 500);
|
|
135
170
|
}
|
|
136
171
|
|
|
137
|
-
// ─── Theme helpers ───────────────────────────────────────────────────
|
|
138
|
-
|
|
139
|
-
private fg(color: string, text: string): string {
|
|
140
|
-
if (this.theme) return this.theme.fg(color as any, text);
|
|
141
|
-
const c: Record<string, string> = {
|
|
142
|
-
accent: "\x1b[36m",
|
|
143
|
-
success: "\x1b[32m",
|
|
144
|
-
warning: "\x1b[33m",
|
|
145
|
-
error: "\x1b[31m",
|
|
146
|
-
dim: "\x1b[2m",
|
|
147
|
-
borderMuted: "\x1b[90m",
|
|
148
|
-
};
|
|
149
|
-
return `${c[color] ?? ""}${text}\x1b[0m`;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
private bold(text: string): string {
|
|
153
|
-
return this.theme ? this.theme.bold(text) : `\x1b[1m${text}\x1b[0m`;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
private frameLine(content: string, innerWidth: number): string {
|
|
157
|
-
const truncated = truncateToWidth(content, innerWidth, "");
|
|
158
|
-
const padding = Math.max(0, innerWidth - visibleWidth(truncated));
|
|
159
|
-
return `${this.fg("borderMuted", "│")}${truncated}${" ".repeat(padding)}${this.fg("borderMuted", "│")}`;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
private ruleLine(innerWidth: number): string {
|
|
163
|
-
return this.fg("borderMuted", `├${"─".repeat(innerWidth)}┤`);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
private borderLine(innerWidth: number, edge: "top" | "bottom"): string {
|
|
167
|
-
const left = edge === "top" ? "┌" : "└";
|
|
168
|
-
const right = edge === "top" ? "┐" : "┘";
|
|
169
|
-
return this.fg("borderMuted", `${left}${"─".repeat(innerWidth)}${right}`);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
172
|
render(width: number): string[] {
|
|
173
173
|
const innerWidth = boxInnerWidth(width);
|
|
174
174
|
const lines: string[] = [];
|
|
175
175
|
|
|
176
|
-
lines.push(this.borderLine(innerWidth, "top"));
|
|
176
|
+
lines.push(this.overlay.borderLine(innerWidth, "top"));
|
|
177
177
|
lines.push(
|
|
178
|
-
this.frameLine(
|
|
179
|
-
this.fg("accent", this.bold("🤖 Recap Model Selector")),
|
|
178
|
+
this.overlay.frameLine(
|
|
179
|
+
this.overlay.fg("accent", this.overlay.bold("🤖 Recap Model Selector")),
|
|
180
180
|
innerWidth
|
|
181
181
|
)
|
|
182
182
|
);
|
|
183
183
|
lines.push(
|
|
184
|
-
this.frameLine(
|
|
185
|
-
this.fg("dim", "Select model for notification recaps"),
|
|
184
|
+
this.overlay.frameLine(
|
|
185
|
+
this.overlay.fg("dim", "Select model for notification recaps"),
|
|
186
186
|
innerWidth
|
|
187
187
|
)
|
|
188
188
|
);
|
|
189
|
-
lines.push(this.ruleLine(innerWidth));
|
|
189
|
+
lines.push(this.overlay.ruleLine(innerWidth));
|
|
190
190
|
|
|
191
191
|
// Filter bar
|
|
192
192
|
if (this.filterMode) {
|
|
193
193
|
lines.push(
|
|
194
|
-
this.frameLine(
|
|
195
|
-
` ${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", "█")}`,
|
|
196
196
|
innerWidth
|
|
197
197
|
)
|
|
198
198
|
);
|
|
199
199
|
} else if (this.filter) {
|
|
200
200
|
lines.push(
|
|
201
|
-
this.frameLine(
|
|
202
|
-
` ${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)")}`,
|
|
203
203
|
innerWidth
|
|
204
204
|
)
|
|
205
205
|
);
|
|
206
206
|
} else {
|
|
207
207
|
lines.push(
|
|
208
|
-
this.frameLine(
|
|
209
|
-
` ${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`)}`,
|
|
210
210
|
innerWidth
|
|
211
211
|
)
|
|
212
212
|
);
|
|
213
213
|
}
|
|
214
|
-
lines.push(this.ruleLine(innerWidth));
|
|
214
|
+
lines.push(this.overlay.ruleLine(innerWidth));
|
|
215
215
|
|
|
216
216
|
// Model list
|
|
217
217
|
const terminalRows = process.stdout.rows ?? 30;
|
|
@@ -226,9 +226,15 @@ export class RecapModelSelectorOverlay implements Component {
|
|
|
226
226
|
);
|
|
227
227
|
|
|
228
228
|
if (this.filteredModels.length === 0) {
|
|
229
|
+
const emptyMsg =
|
|
230
|
+
this.filter.length > 0
|
|
231
|
+
? `No models match "${this.filter}"`
|
|
232
|
+
: this.models.length === 0
|
|
233
|
+
? "No models — check ~/.pi/agent/models.json or API keys, then reopen"
|
|
234
|
+
: "No models found";
|
|
229
235
|
lines.push(
|
|
230
|
-
this.frameLine(
|
|
231
|
-
` ${this.fg("dim",
|
|
236
|
+
this.overlay.frameLine(
|
|
237
|
+
` ${this.overlay.fg("dim", emptyMsg)}`,
|
|
232
238
|
innerWidth
|
|
233
239
|
)
|
|
234
240
|
);
|
|
@@ -236,20 +242,20 @@ export class RecapModelSelectorOverlay implements Component {
|
|
|
236
242
|
for (let i = startIdx; i < endIdx; i++) {
|
|
237
243
|
const m = this.filteredModels[i];
|
|
238
244
|
const isSelected = i === this.selectedIndex;
|
|
239
|
-
const marker = isSelected ? this.fg("accent", "▸") : " ";
|
|
245
|
+
const marker = isSelected ? this.overlay.fg("accent", "▸") : " ";
|
|
240
246
|
const label = m.name || m.id;
|
|
241
247
|
const fullRef = `${m.provider}/${m.id}`;
|
|
242
248
|
const isDefault = fullRef === DEFAULT_MODEL;
|
|
243
249
|
const defaultTag = isDefault
|
|
244
|
-
? ` ${this.fg("warning", "(default)")}`
|
|
250
|
+
? ` ${this.overlay.fg("warning", "(default)")}`
|
|
245
251
|
: "";
|
|
246
252
|
|
|
247
|
-
const providerTag = this.fg("dim", `[${m.provider}]`);
|
|
253
|
+
const providerTag = this.overlay.fg("dim", `[${m.provider}]`);
|
|
248
254
|
const display = isSelected
|
|
249
|
-
? `${providerTag} ${this.bold(label)}${defaultTag}`
|
|
250
|
-
: `${providerTag} ${this.fg("dim", label)}${defaultTag}`;
|
|
255
|
+
? `${providerTag} ${this.overlay.bold(label)}${defaultTag}`
|
|
256
|
+
: `${providerTag} ${this.overlay.fg("dim", label)}${defaultTag}`;
|
|
251
257
|
|
|
252
|
-
lines.push(this.frameLine(` ${marker} ${display}`, innerWidth));
|
|
258
|
+
lines.push(this.overlay.frameLine(` ${marker} ${display}`, innerWidth));
|
|
253
259
|
}
|
|
254
260
|
}
|
|
255
261
|
|
|
@@ -259,8 +265,8 @@ export class RecapModelSelectorOverlay implements Component {
|
|
|
259
265
|
((this.selectedIndex + 1) / this.filteredModels.length) * 100
|
|
260
266
|
);
|
|
261
267
|
lines.push(
|
|
262
|
-
this.frameLine(
|
|
263
|
-
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})`),
|
|
264
270
|
innerWidth
|
|
265
271
|
)
|
|
266
272
|
);
|
|
@@ -268,33 +274,33 @@ export class RecapModelSelectorOverlay implements Component {
|
|
|
268
274
|
|
|
269
275
|
// Status messages
|
|
270
276
|
if (this.error) {
|
|
271
|
-
lines.push(this.ruleLine(innerWidth));
|
|
277
|
+
lines.push(this.overlay.ruleLine(innerWidth));
|
|
272
278
|
lines.push(
|
|
273
|
-
this.frameLine(` ${this.fg("error", `⚠ ${this.error}`)}`, innerWidth)
|
|
279
|
+
this.overlay.frameLine(` ${this.overlay.fg("error", `⚠ ${this.error}`)}`, innerWidth)
|
|
274
280
|
);
|
|
275
281
|
}
|
|
276
282
|
if (this.saved) {
|
|
277
|
-
lines.push(this.ruleLine(innerWidth));
|
|
283
|
+
lines.push(this.overlay.ruleLine(innerWidth));
|
|
278
284
|
lines.push(
|
|
279
|
-
this.frameLine(
|
|
280
|
-
` ${this.fg("success", "✓ Model saved")}`,
|
|
285
|
+
this.overlay.frameLine(
|
|
286
|
+
` ${this.overlay.fg("success", "✓ Model saved")}`,
|
|
281
287
|
innerWidth
|
|
282
288
|
)
|
|
283
289
|
);
|
|
284
290
|
}
|
|
285
291
|
|
|
286
292
|
// Footer
|
|
287
|
-
lines.push(this.ruleLine(innerWidth));
|
|
293
|
+
lines.push(this.overlay.ruleLine(innerWidth));
|
|
288
294
|
lines.push(
|
|
289
|
-
this.frameLine(
|
|
290
|
-
this.fg(
|
|
295
|
+
this.overlay.frameLine(
|
|
296
|
+
this.overlay.fg(
|
|
291
297
|
"dim",
|
|
292
298
|
"↑↓ navigate · / filter · Enter select · Esc cancel"
|
|
293
299
|
),
|
|
294
300
|
innerWidth
|
|
295
301
|
)
|
|
296
302
|
);
|
|
297
|
-
lines.push(this.borderLine(innerWidth, "bottom"));
|
|
303
|
+
lines.push(this.overlay.borderLine(innerWidth, "bottom"));
|
|
298
304
|
|
|
299
305
|
return lines;
|
|
300
306
|
}
|