@pi-unipi/unipi 2.2.3 → 2.2.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/unipi",
3
- "version": "2.2.3",
3
+ "version": "2.2.5",
4
4
  "description": "All-in-one extension suite for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -68,11 +68,11 @@
68
68
  "dependencies": {
69
69
  "@pi-unipi/ask-user": "2.2.0",
70
70
  "@pi-unipi/btw": "2.2.0",
71
- "@pi-unipi/compactor": "2.2.0",
72
- "@pi-unipi/notify": "2.2.1",
71
+ "@pi-unipi/compactor": "2.2.1",
72
+ "@pi-unipi/notify": "2.2.2",
73
73
  "@pi-unipi/command-enchantment": "2.2.0",
74
74
  "@pi-unipi/core": "2.2.0",
75
- "@pi-unipi/info-screen": "2.2.0",
75
+ "@pi-unipi/info-screen": "2.2.1",
76
76
  "@pi-unipi/mcp": "2.2.0",
77
77
  "@pi-unipi/memory": "2.2.0",
78
78
  "@pi-unipi/ralph": "2.2.0",
@@ -82,7 +82,7 @@
82
82
  "@pi-unipi/kanboard": "2.2.0",
83
83
  "@pi-unipi/web-api": "2.2.0",
84
84
  "@pi-unipi/workflow": "2.2.0",
85
- "@pi-unipi/footer": "2.2.0",
85
+ "@pi-unipi/footer": "2.2.1",
86
86
  "@pi-unipi/updater": "2.2.0",
87
87
  "@pi-unipi/input-shortcuts": "2.2.0",
88
88
  "@pi-unipi/cocoindex": "2.2.0"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/compactor",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Context engine for Pi — zero-LLM compaction, session continuity, sandbox execution, and tool display optimization",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@pi-unipi/core": "2.2.0",
38
- "@pi-unipi/info-screen": "2.2.0",
38
+ "@pi-unipi/info-screen": "2.2.1",
39
39
  "@earendil-works/pi-agent-core": "^0.80.0"
40
40
  },
41
41
  "peerDependencies": {
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { Component, TUI } from "@earendil-works/pi-tui";
11
- import { truncateToWidth, visibleWidth, SettingsList, type SettingItem, type SettingsListTheme } from "@earendil-works/pi-tui";
11
+ import { matchesKey, truncateToWidth, visibleWidth, SettingsList, type SettingItem, type SettingsListTheme } from "@earendil-works/pi-tui";
12
12
  import type { Theme, KeybindingsManager } from "@earendil-works/pi-coding-agent";
13
13
  import { loadConfig, saveConfig, projectConfigPath } from "../config/manager.js";
14
14
  import { applyPreset, detectPreset } from "../config/presets.js";
@@ -566,7 +566,7 @@ export class CompactorSettingsOverlay implements Component {
566
566
  }
567
567
 
568
568
  // Escape cancels (but SettingsList also handles it, calling onCancel)
569
- if (data === "\x1b") {
569
+ if (matchesKey(data, "escape")) {
570
570
  this.onClose?.();
571
571
  return;
572
572
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/footer",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Persistent status bar for Unipi — subscribes to UNIPI_EVENTS and renders key stats from all unipi packages",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
13
- import { SettingsList, type SettingItem, type SettingsListTheme } from "@earendil-works/pi-tui";
13
+ import { matchesKey, SettingsList, type SettingItem, type SettingsListTheme } from "@earendil-works/pi-tui";
14
14
  import { loadFooterSettings, saveFooterSettings } from "../config.js";
15
15
  import { PRESET_NAMES } from "../presets.js";
16
16
  import { setIconStyle } from "../rendering/icons.js";
@@ -151,7 +151,7 @@ class FooterSettingsOverlay {
151
151
  }
152
152
 
153
153
  // Escape — back from drill-down, or close at root
154
- if (data === "\x1b") {
154
+ if (matchesKey(data, "escape")) {
155
155
  if (this.section === "segments" && this.selectedGroupId) {
156
156
  this.backToGroups();
157
157
  } else {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/image",
3
- "version": "2.2.2",
3
+ "version": "2.2.4",
4
4
  "description": "Image generation and image recognition tools for the Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -100,6 +100,7 @@ async function callAnthropic(options: RecognizeOptions): Promise<string> {
100
100
  body: JSON.stringify({
101
101
  model: modelId,
102
102
  max_tokens: maxTokens,
103
+ stream: false,
103
104
  system: systemPrompt,
104
105
  messages: [
105
106
  {
@@ -123,11 +124,14 @@ async function callAnthropic(options: RecognizeOptions): Promise<string> {
123
124
 
124
125
  if (!response.ok) throw new Error(await describeHttpError(response));
125
126
 
126
- const data = (await response.json()) as {
127
- content?: Array<{ type?: string; text?: string }>;
128
- };
127
+ const data = await readJsonOrStream(response);
128
+
129
+ // A streamed response arrives as deltas rather than a `content` array.
130
+ const streamed = collectStreamedText(data);
131
+ if (streamed !== null) return streamed;
129
132
 
130
- return (data.content ?? [])
133
+ const typed = data as { content?: Array<{ type?: string; text?: string }> };
134
+ return (typed.content ?? [])
131
135
  .filter((block) => block.type === "text" && typeof block.text === "string")
132
136
  .map((block) => block.text as string)
133
137
  .join("\n")
@@ -137,6 +141,79 @@ async function callAnthropic(options: RecognizeOptions): Promise<string> {
137
141
  }
138
142
  }
139
143
 
144
+ /**
145
+ * Read a response body as JSON, tolerating a Server-Sent Events stream.
146
+ *
147
+ * Some OpenAI-compatible gateways (omniroute, for one) reply with
148
+ * `text/event-stream` even when streaming was never requested. Calling
149
+ * `response.json()` on that throws `Unexpected token 'd', "data: {"id"...`,
150
+ * which tells the user nothing. Parse the SSE frames instead and hand back a
151
+ * synthetic payload carrying the concatenated deltas.
152
+ */
153
+ async function readJsonOrStream(response: Response): Promise<unknown> {
154
+ const body = await response.text();
155
+ const trimmed = body.trimStart();
156
+
157
+ if (!trimmed.startsWith("data:")) {
158
+ try {
159
+ return JSON.parse(body) as unknown;
160
+ } catch {
161
+ throw new Error(
162
+ `The model returned a response that could not be parsed:\n${body.slice(0, 200)}`,
163
+ );
164
+ }
165
+ }
166
+
167
+ const parts: string[] = [];
168
+ for (const line of body.split(/\r?\n/)) {
169
+ if (!line.startsWith("data:")) continue;
170
+ const payload = line.slice(5).trim();
171
+ if (!payload || payload === "[DONE]") continue;
172
+
173
+ let frame: unknown;
174
+ try {
175
+ frame = JSON.parse(payload);
176
+ } catch {
177
+ continue; // Ignore a partial or malformed frame rather than failing.
178
+ }
179
+ parts.push(...extractDeltaText(frame));
180
+ }
181
+
182
+ return { __streamedText: parts.join("") };
183
+ }
184
+
185
+ /** Pull text out of one SSE frame, in both OpenAI and Anthropic shapes. */
186
+ function extractDeltaText(frame: unknown): string[] {
187
+ if (frame === null || typeof frame !== "object") return [];
188
+ const out: string[] = [];
189
+
190
+ // OpenAI: choices[].delta.content (or a non-streamed message.content)
191
+ const choices = (frame as { choices?: unknown }).choices;
192
+ if (Array.isArray(choices)) {
193
+ for (const choice of choices) {
194
+ if (choice === null || typeof choice !== "object") continue;
195
+ const delta = (choice as { delta?: { content?: unknown } }).delta;
196
+ if (typeof delta?.content === "string") out.push(delta.content);
197
+ const message = (choice as { message?: { content?: unknown } }).message;
198
+ if (typeof message?.content === "string") out.push(message.content);
199
+ }
200
+ }
201
+
202
+ // Anthropic: content_block_delta → delta.text
203
+ const delta = (frame as { delta?: { text?: unknown } }).delta;
204
+ if (typeof delta?.text === "string") out.push(delta.text);
205
+
206
+ return out;
207
+ }
208
+
209
+ /** Text collected from a streamed body, or null when it was ordinary JSON. */
210
+ function collectStreamedText(data: unknown): string | null {
211
+ if (data === null || typeof data !== "object") return null;
212
+ const streamed = (data as { __streamedText?: unknown }).__streamedText;
213
+ if (typeof streamed !== "string") return null;
214
+ return streamed.trim();
215
+ }
216
+
140
217
  /** OpenAI-compatible chat completions — image parts use a data: URL. */
141
218
  async function callOpenAICompatible(options: RecognizeOptions): Promise<string> {
142
219
  const {
@@ -159,6 +236,9 @@ async function callOpenAICompatible(options: RecognizeOptions): Promise<string>
159
236
  body: JSON.stringify({
160
237
  model: modelId,
161
238
  max_tokens: maxTokens,
239
+ // Ask for a single payload. Gateways may stream regardless, which
240
+ // readJsonOrStream handles.
241
+ stream: false,
162
242
  messages: [
163
243
  { role: "system", content: systemPrompt },
164
244
  {
@@ -181,11 +261,16 @@ async function callOpenAICompatible(options: RecognizeOptions): Promise<string>
181
261
 
182
262
  if (!response.ok) throw new Error(await describeHttpError(response));
183
263
 
184
- const data = (await response.json()) as {
264
+ const data = await readJsonOrStream(response);
265
+
266
+ const streamed = collectStreamedText(data);
267
+ if (streamed !== null) return streamed;
268
+
269
+ const typed = data as {
185
270
  choices?: Array<{ message?: { content?: string | Array<{ text?: string }> } }>;
186
271
  };
187
272
 
188
- const content = data.choices?.[0]?.message?.content;
273
+ const content = typed.choices?.[0]?.message?.content;
189
274
  if (typeof content === "string") return content.trim();
190
275
  if (Array.isArray(content)) {
191
276
  return content.map((part) => part?.text ?? "").join("").trim();
@@ -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 { boxInnerWidth, safeRepeat } from "@pi-unipi/core";
12
12
 
@@ -61,7 +61,7 @@ export class ImageModelSelectorOverlay implements Component {
61
61
  handleInput(data: string): void {
62
62
  // Ctrl+C must always escape, even mid-filter. Without this the overlay traps
63
63
  // the user with no way out.
64
- if (data === "\x03") {
64
+ if (matchesKey(data, "ctrl+c")) {
65
65
  this.onClose?.();
66
66
  return;
67
67
  }
@@ -76,49 +76,61 @@ export class ImageModelSelectorOverlay implements Component {
76
76
  return;
77
77
  }
78
78
 
79
- switch (data) {
80
- case "\x1b[A":
81
- case "k":
82
- this.selectedIndex = Math.max(0, this.selectedIndex - 1);
83
- break;
84
- case "\x1b[B":
85
- case "j":
86
- this.selectedIndex = Math.min(this.filtered.length - 1, this.selectedIndex + 1);
87
- break;
88
- case "/":
89
- this.filterMode = true;
90
- this.filter = "";
91
- break;
92
- case "c":
93
- case "C":
94
- // Escape hatch: generator detection is heuristic, so a provider may
95
- // expose a model the catalog cannot recognise. Let the user name it.
96
- this.customMode = true;
97
- this.custom = "";
98
- this.error = null;
99
- break;
100
- case "\r":
101
- this.commit();
102
- break;
103
- case "\x1b":
104
- this.onClose?.();
105
- break;
79
+ // Escape is checked via matchesKey, not `data === "\x1b"`: under the kitty
80
+ // keyboard protocol it arrives as "\x1b[27u" (and as "\x1b[27;1;27~" with
81
+ // modifyOtherKeys), so a bare comparison silently fails to close.
82
+ if (matchesKey(data, "escape")) {
83
+ this.onClose?.();
84
+ return;
85
+ }
86
+ if (matchesKey(data, "up") || data === "k") {
87
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
88
+ return;
89
+ }
90
+ if (matchesKey(data, "down") || data === "j") {
91
+ this.selectedIndex = Math.min(this.filtered.length - 1, this.selectedIndex + 1);
92
+ return;
93
+ }
94
+ if (matchesKey(data, "enter")) {
95
+ this.commit();
96
+ return;
97
+ }
98
+ if (data === "/") {
99
+ this.filterMode = true;
100
+ this.filter = "";
101
+ return;
102
+ }
103
+ if (data === "c" || data === "C") {
104
+ // Escape hatch: generator detection is heuristic, so a provider may
105
+ // expose a model the catalog cannot recognise. Let the user name it.
106
+ this.customMode = true;
107
+ this.custom = "";
108
+ this.error = null;
106
109
  }
107
110
  }
108
111
 
109
112
  private handleFilterInput(data: string): void {
110
- if (data === "\r") {
113
+ if (matchesKey(data, "enter")) {
111
114
  this.filterMode = false;
112
115
  return;
113
116
  }
114
- if (data === "\x1b") {
117
+ if (matchesKey(data, "escape")) {
115
118
  this.filter = "";
116
119
  this.filterMode = false;
117
120
  this.applyFilter();
118
121
  this.selectedIndex = 0;
119
122
  return;
120
123
  }
121
- if (data === "\x7f" || data === "\b") {
124
+ // Let the list be navigated without leaving the filter.
125
+ if (matchesKey(data, "up")) {
126
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
127
+ return;
128
+ }
129
+ if (matchesKey(data, "down")) {
130
+ this.selectedIndex = Math.min(this.filtered.length - 1, this.selectedIndex + 1);
131
+ return;
132
+ }
133
+ if (matchesKey(data, "backspace") || data === "\x7f" || data === "\b") {
122
134
  this.filter = this.filter.slice(0, -1);
123
135
  this.applyFilter();
124
136
  this.clampSelection();
@@ -133,17 +145,17 @@ export class ImageModelSelectorOverlay implements Component {
133
145
 
134
146
  /** Free-text "provider/model-id" entry. */
135
147
  private handleCustomInput(data: string): void {
136
- if (data === "\r") {
148
+ if (matchesKey(data, "enter")) {
137
149
  this.commitCustom();
138
150
  return;
139
151
  }
140
- if (data === "\x1b") {
152
+ if (matchesKey(data, "escape")) {
141
153
  this.customMode = false;
142
154
  this.custom = "";
143
155
  this.error = null;
144
156
  return;
145
157
  }
146
- if (data === "\x7f" || data === "\b") {
158
+ if (matchesKey(data, "backspace") || data === "\x7f" || data === "\b") {
147
159
  this.custom = this.custom.slice(0, -1);
148
160
  this.error = null;
149
161
  return;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/info-screen",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Dashboard and module registry for Unipi — configurable info overlay with tabbed groups",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { Component } from "@earendil-works/pi-tui";
11
- import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
11
+ import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
12
12
  import type { Theme } from "@earendil-works/pi-coding-agent";
13
13
  import { infoRegistry } from "../registry.js";
14
14
  import { getInfoSettings } from "../config.js";
@@ -200,7 +200,7 @@ export class InfoOverlay implements Component {
200
200
  } else if (data === "R") {
201
201
  // Refresh all
202
202
  this.refreshAll();
203
- } else if (data === "q" || data === "\x1b") {
203
+ } else if (data === "q" || matchesKey(data, "escape")) {
204
204
  this.destroy();
205
205
  this.onClose?.();
206
206
  }
@@ -44,7 +44,7 @@
44
44
  "sqlite-vec": "^0.1.9",
45
45
  "js-yaml": "^4.1.0",
46
46
  "@pi-unipi/core": "2.2.0",
47
- "@pi-unipi/info-screen": "2.2.0"
47
+ "@pi-unipi/info-screen": "2.2.1"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@earendil-works/pi-coding-agent": "^0.80.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/notify",
3
- "version": "2.2.1",
3
+ "version": "2.2.2",
4
4
  "description": "Cross-platform notification extension for Pi — native OS, Gotify, and Telegram notifications for agent lifecycle events",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { Component } from "@earendil-works/pi-tui";
10
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
10
+ import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
11
11
  import type { Theme } from "@earendil-works/pi-coding-agent";
12
12
  import { sendGotifyNotification } from "../platforms/gotify.js";
13
13
  import { updateConfig, loadConfig } from "../settings.js";
@@ -59,7 +59,7 @@ export class GotifySetupOverlay implements Component {
59
59
  case "instructions":
60
60
  if (data === "\r" || data === " ") {
61
61
  this.phase = this.serverUrl ? "app-token" : "server-url";
62
- } else if (data === "\x1b") {
62
+ } else if (matchesKey(data, "escape")) {
63
63
  this.onClose?.();
64
64
  }
65
65
  break;
@@ -79,7 +79,7 @@ export class GotifySetupOverlay implements Component {
79
79
  case "priority":
80
80
  if (data === "\r" && this.isValidPriority()) {
81
81
  this.testConnection();
82
- } else if (data === "\x1b") {
82
+ } else if (matchesKey(data, "escape")) {
83
83
  this.onClose?.();
84
84
  } else if (data === "\x7f" || data === "\b") {
85
85
  this.priority = this.priority.slice(0, -1);
@@ -92,7 +92,7 @@ export class GotifySetupOverlay implements Component {
92
92
  break;
93
93
 
94
94
  case "testing":
95
- if (data === "\x1b") {
95
+ if (matchesKey(data, "escape")) {
96
96
  this.onClose?.();
97
97
  }
98
98
  break;
@@ -100,7 +100,7 @@ export class GotifySetupOverlay implements Component {
100
100
  case "success":
101
101
  case "error":
102
102
  case "test-failed":
103
- if (data === "\r" || data === " " || data === "\x1b") {
103
+ if (data === "\r" || data === " " || matchesKey(data, "escape")) {
104
104
  this.onClose?.();
105
105
  }
106
106
  break;
@@ -137,7 +137,7 @@ export class GotifySetupOverlay implements Component {
137
137
  }
138
138
  if (data === "\r") {
139
139
  onEnter();
140
- } else if (data === "\x1b") {
140
+ } else if (matchesKey(data, "escape")) {
141
141
  this.onClose?.();
142
142
  } else if (data === "\x7f" || data === "\b") {
143
143
  if (target === "server-url") {
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { Component } from "@earendil-works/pi-tui";
10
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
10
+ import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
11
11
  import type { Theme } from "@earendil-works/pi-coding-agent";
12
12
  import { sendNtfyNotification } from "../platforms/ntfy.js";
13
13
  import { loadNtfyConfig, saveNtfyConfig, getNtfyConfigScope } from "../ntfy-config.js";
@@ -70,7 +70,7 @@ export class NtfySetupOverlay implements Component {
70
70
  case "instructions":
71
71
  if (data === "\r" || data === " ") {
72
72
  this.phase = "scope";
73
- } else if (data === "\x1b") {
73
+ } else if (matchesKey(data, "escape")) {
74
74
  this.onClose?.();
75
75
  }
76
76
  break;
@@ -85,7 +85,7 @@ export class NtfySetupOverlay implements Component {
85
85
  } else if (data === "\r" || data === " ") {
86
86
  this.scope = this.scopeIndex === 1 ? "project" : "global";
87
87
  this.phase = this.serverUrl ? "topic" : "server-url";
88
- } else if (data === "\x1b") {
88
+ } else if (matchesKey(data, "escape")) {
89
89
  this.onClose?.();
90
90
  }
91
91
  break;
@@ -114,7 +114,7 @@ export class NtfySetupOverlay implements Component {
114
114
  case "priority":
115
115
  if (data === "\r" && this.isValidPriority()) {
116
116
  this.testConnection();
117
- } else if (data === "\x1b") {
117
+ } else if (matchesKey(data, "escape")) {
118
118
  this.onClose?.();
119
119
  } else if (data === "\x7f" || data === "\b") {
120
120
  this.priority = this.priority.slice(0, -1);
@@ -127,7 +127,7 @@ export class NtfySetupOverlay implements Component {
127
127
  break;
128
128
 
129
129
  case "testing":
130
- if (data === "\x1b") {
130
+ if (matchesKey(data, "escape")) {
131
131
  this.onClose?.();
132
132
  }
133
133
  break;
@@ -135,7 +135,7 @@ export class NtfySetupOverlay implements Component {
135
135
  case "success":
136
136
  case "error":
137
137
  case "test-failed":
138
- if (data === "\r" || data === " " || data === "\x1b") {
138
+ if (data === "\r" || data === " " || matchesKey(data, "escape")) {
139
139
  this.onClose?.();
140
140
  }
141
141
  break;
@@ -173,7 +173,7 @@ export class NtfySetupOverlay implements Component {
173
173
  }
174
174
  if (data === "\r") {
175
175
  onEnter();
176
- } else if (data === "\x1b") {
176
+ } else if (matchesKey(data, "escape")) {
177
177
  // Escape during token phase — skip token (optional field)
178
178
  if (target === "token") {
179
179
  this.phase = "priority";
@@ -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 { readModelCache, type CachedModel, boxInnerWidth } from "@pi-unipi/core";
12
12
  import { loadConfig, saveConfig } from "../settings.js";
@@ -56,7 +56,7 @@ export class RecapModelSelectorOverlay implements Component {
56
56
  this.filterMode = false;
57
57
  return;
58
58
  }
59
- if (data === "\x1b") {
59
+ if (matchesKey(data, "escape")) {
60
60
  // Escape — clear filter and exit filter mode
61
61
  this.filter = "";
62
62
  this.filterMode = false;
@@ -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 { pollForChatId } from "../platforms/telegram.js";
12
12
  import { updateConfig } from "../settings.js";
@@ -48,7 +48,7 @@ export class TelegramSetupOverlay implements Component {
48
48
  case "instructions":
49
49
  if (data === "\r" || data === " ") {
50
50
  this.phase = "token";
51
- } else if (data === "\x1b") {
51
+ } else if (matchesKey(data, "escape")) {
52
52
  this.cleanup();
53
53
  this.onClose?.();
54
54
  }
@@ -75,7 +75,7 @@ export class TelegramSetupOverlay implements Component {
75
75
  }
76
76
  if (data === "\r" && this.botToken.length > 0) {
77
77
  this.startPolling();
78
- } else if (data === "\x1b") {
78
+ } else if (matchesKey(data, "escape")) {
79
79
  this.cleanup();
80
80
  this.onClose?.();
81
81
  } else if (data === "\x7f" || data === "\b") {
@@ -85,7 +85,7 @@ export class TelegramSetupOverlay implements Component {
85
85
  }
86
86
  break;
87
87
  case "polling":
88
- if (data === "\x1b") {
88
+ if (matchesKey(data, "escape")) {
89
89
  this.cleanup();
90
90
  this.onClose?.();
91
91
  }
@@ -93,7 +93,7 @@ export class TelegramSetupOverlay implements Component {
93
93
  case "success":
94
94
  case "error":
95
95
  case "timeout":
96
- if (data === "\r" || data === " " || data === "\x1b") {
96
+ if (data === "\r" || data === " " || matchesKey(data, "escape")) {
97
97
  this.cleanup();
98
98
  this.onClose?.();
99
99
  }
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@pi-unipi/core": "2.2.0",
31
- "@pi-unipi/info-screen": "2.2.0"
31
+ "@pi-unipi/info-screen": "2.2.1"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "@earendil-works/pi-ai": "^0.80.0",
@@ -19,7 +19,7 @@
19
19
  "dependencies": {
20
20
  "@pi-unipi/core": "2.2.0",
21
21
  "@pi-unipi/workflow": "2.2.0",
22
- "@pi-unipi/info-screen": "2.2.0",
22
+ "@pi-unipi/info-screen": "2.2.1",
23
23
  "@earendil-works/pi-agent-core": "^0.80.0"
24
24
  },
25
25
  "devDependencies": {