@pi-unipi/unipi 2.6.0 → 2.6.1

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.
@@ -2,7 +2,8 @@
2
2
  * @pi-unipi/notify — Recap Model Selector TUI
3
3
  *
4
4
  * Interactive overlay for selecting the recap summarization model.
5
- * Uses the project-wide cached model list from ~/.unipi/config/models-cache.json.
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";
@@ -28,9 +29,16 @@ export class RecapModelSelectorOverlay implements Component {
28
29
  requestRender?: () => void;
29
30
  private theme: Theme | null = null;
30
31
 
31
- constructor() {
32
- // Load all cached models from project-wide cache
33
- this.models = readModelCache();
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
@@ -49,23 +57,42 @@ export class RecapModelSelectorOverlay implements Component {
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 === "\r") {
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
- if (data === "\x7f" || data === "\b") {
68
- // Backspace
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
- switch (data) {
84
- case "\x1b[A": // Up
85
- case "k":
86
- this.selectedIndex = Math.max(0, this.selectedIndex - 1);
87
- break;
88
- case "\x1b[B": // Down
89
- case "j":
90
- this.selectedIndex = Math.min(
91
- this.filteredModels.length - 1,
92
- this.selectedIndex + 1
93
- );
94
- break;
95
- case "/": // Start filter
96
- this.filterMode = true;
97
- this.filter = "";
98
- break;
99
- case "\r": // Enter — select and save
100
- this.selectModel();
101
- break;
102
- case "\x1b": // Escape — close
103
- this.onClose?.();
104
- break;
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
 
@@ -226,9 +261,15 @@ export class RecapModelSelectorOverlay implements Component {
226
261
  );
227
262
 
228
263
  if (this.filteredModels.length === 0) {
264
+ const emptyMsg =
265
+ this.filter.length > 0
266
+ ? `No models match "${this.filter}"`
267
+ : this.models.length === 0
268
+ ? "No models — check ~/.pi/agent/models.json or API keys, then reopen"
269
+ : "No models found";
229
270
  lines.push(
230
271
  this.frameLine(
231
- ` ${this.fg("dim", "No models found")}`,
272
+ ` ${this.fg("dim", emptyMsg)}`,
232
273
  innerWidth
233
274
  )
234
275
  );
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { Component } from "@earendil-works/pi-tui";
9
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
9
+ import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
10
10
  import type { Theme } from "@earendil-works/pi-coding-agent";
11
11
  import {
12
12
  loadConfig,
@@ -51,37 +51,50 @@ export class NotifySettingsOverlay implements Component {
51
51
  invalidate(): void {}
52
52
 
53
53
  handleInput(data: string): void {
54
- switch (data) {
55
- case "\x1b[A": // Up
56
- case "k":
57
- this.selectedIndex = Math.max(0, this.selectedIndex - 1);
58
- break;
59
- case "\x1b[B": // Down
60
- case "j":
61
- this.selectedIndex = Math.min(this.maxItems - 1, this.selectedIndex + 1);
62
- break;
63
- case " ": // Space - toggle
64
- this.toggleCurrent();
65
- break;
66
- case "\t": // Tab - switch section
67
- {
68
- const sections: Section[] = ["platforms", "events", "recap"];
69
- const idx = sections.indexOf(this.section);
70
- this.section = sections[(idx + 1) % sections.length];
71
- this.selectedIndex = 0;
72
- }
73
- break;
74
- case "m": // M - open model selector (only in recap section)
75
- if (this.section === "recap") {
76
- this.onOpenModelSelector?.();
77
- }
78
- break;
79
- case "\r": // Enter - save
80
- this.save();
81
- break;
82
- case "\x1b": // Escape - close
83
- this.onClose?.();
84
- break;
54
+ // Ctrl+C always closes — escape hatch for terminals with key encodings
55
+ // this overlay does not understand (issue #27).
56
+ if (matchesKey(data, "ctrl+c")) {
57
+ this.onClose?.();
58
+ return;
59
+ }
60
+ // Navigation keys are matched via matchesKey, never raw byte comparison:
61
+ // under the kitty keyboard protocol / enhanced encodings (Ghostty, Herdr)
62
+ // Escape arrives as "\x1b[27u" (or "\x1b[27;1;27~" with modifyOtherKeys)
63
+ // and arrows as "\x1b[57419u"/"\x1b[57420u" — exact legacy comparisons
64
+ // like data === "\x1b[A" silently fail there.
65
+ if (matchesKey(data, "up") || data === "k") {
66
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
67
+ return;
68
+ }
69
+ if (matchesKey(data, "down") || data === "j") {
70
+ this.selectedIndex = Math.min(this.maxItems - 1, this.selectedIndex + 1);
71
+ return;
72
+ }
73
+ if (matchesKey(data, "space")) {
74
+ this.toggleCurrent();
75
+ return;
76
+ }
77
+ if (matchesKey(data, "tab")) {
78
+ const sections: Section[] = ["platforms", "events", "recap"];
79
+ const idx = sections.indexOf(this.section);
80
+ this.section = sections[(idx + 1) % sections.length];
81
+ this.selectedIndex = 0;
82
+ return;
83
+ }
84
+ if (data === "m" || data === "M") {
85
+ // Open model selector (only in recap section)
86
+ if (this.section === "recap") {
87
+ this.onOpenModelSelector?.();
88
+ }
89
+ return;
90
+ }
91
+ if (matchesKey(data, "enter")) {
92
+ this.save();
93
+ return;
94
+ }
95
+ if (matchesKey(data, "escape")) {
96
+ this.onClose?.();
97
+ return;
85
98
  }
86
99
  }
87
100
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/ralph",
3
- "version": "2.6.0",
3
+ "version": "2.6.1",
4
4
  "description": "Long-running iterative development loops for Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -30,8 +30,8 @@
30
30
  "test": "npx tsx --test reminder.test.ts"
31
31
  },
32
32
  "dependencies": {
33
- "@pi-unipi/core": "2.6.0",
34
- "@pi-unipi/info-screen": "2.6.0"
33
+ "@pi-unipi/core": "2.6.1",
34
+ "@pi-unipi/info-screen": "2.6.1"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "@earendil-works/pi-ai": "^0.80.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/subagents",
3
- "version": "2.6.0",
3
+ "version": "2.6.1",
4
4
  "description": "Subagents for UniPi — parallel execution, file locking, workflow integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",