@pi-unipi/notify 2.1.2 → 2.2.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/README.md CHANGED
@@ -24,9 +24,12 @@ Notify subscribes to Pi lifecycle events and routes notifications based on your
24
24
  | `workflow_end` | On | Workflow command completes |
25
25
  | `ralph_loop_end` | On | Ralph loop completes |
26
26
  | `mcp_server_error` | On | MCP server error |
27
- | `agent_end` | Off | Agent finishes responding |
27
+ | `agent_end` | Off | Low-level agent run ends (may fire again on retries) |
28
+ | `agent_settled` | Off | Agent fully settles after retries, compaction, and queued continuations |
28
29
  | `memory_consolidated` | Off | Memory auto-saved |
29
30
  | `session_shutdown` | Off | Session ends |
31
+ | `ask_user_prompt` | Off | Agent asked a question and is waiting for an answer |
32
+ | `permission_request` | Off | A permission prompt is about to be shown (requires [`@gotgenes/pi-permission-system`](https://www.npmjs.com/package/@gotgenes/pi-permission-system)) |
30
33
 
31
34
  Notify registers with the info-screen dashboard, showing enabled platforms and last notification time. The footer subscribes to `NOTIFICATION_SENT` events to display notification stats.
32
35
 
package/events.ts CHANGED
@@ -14,6 +14,7 @@ import { sendGotifyNotification } from "./platforms/gotify.js";
14
14
  import { sendTelegramNotification } from "./platforms/telegram.js";
15
15
  import { sendNtfyNotification } from "./platforms/ntfy.js";
16
16
  import { buildAskUserPromptMessage } from "./ask-user-prompt-message.js";
17
+ import { buildPermissionPromptMessage } from "./permission-prompt-message.js";
17
18
  import { summarizeLastMessage } from "./summarize.js";
18
19
 
19
20
  // Event emitted by @juicesharp/rpiv-ask-user-question before showing its UI.
@@ -21,6 +22,13 @@ import { summarizeLastMessage } from "./summarize.js";
21
22
  // `./events` contract in npm.
22
23
  const ASK_USER_PROMPT_EVENT = "rpiv:ask-user:prompt" as const;
23
24
 
25
+ // Event emitted by @gotgenes/pi-permission-system immediately before the
26
+ // user-facing permission UI is invoked. Fires only for prompts a human must
27
+ // answer — policy auto-allow/deny and session approvals do not emit it.
28
+ // Kept as a local string (like the rpiv event above) because it belongs to a
29
+ // third-party package rather than the unipi event contract.
30
+ const PERMISSION_UI_PROMPT_EVENT = "permissions:ui_prompt" as const;
31
+
24
32
  /** Stored session context for modelRegistry access */
25
33
  let sessionCtx: ExtensionContext | null = null;
26
34
 
@@ -50,20 +58,22 @@ export const BUILTIN_EVENTS: Record<
50
58
  string,
51
59
  { hook: string; label: string }
52
60
  > = {
53
- agent_end: { hook: "agent_end", label: "Agent Complete" },
61
+ agent_end: { hook: "agent_end", label: "Agent Run Complete" },
62
+ agent_settled: { hook: "agent_settled", label: "Agent Complete" },
54
63
  workflow_end: { hook: UNIPI_EVENTS.WORKFLOW_END, label: "Workflow Done" },
55
64
  ralph_loop_end: { hook: UNIPI_EVENTS.RALPH_LOOP_END, label: "Ralph Complete" },
56
65
  mcp_server_error: { hook: UNIPI_EVENTS.MCP_SERVER_ERROR, label: "MCP Error" },
57
66
  memory_consolidated: { hook: UNIPI_EVENTS.MEMORY_CONSOLIDATED, label: "Memory Saved" },
58
67
  session_shutdown: { hook: "session_shutdown", label: "Session End" },
59
68
  ask_user_prompt: { hook: UNIPI_EVENTS.ASK_USER_PROMPT, label: "Question Asked" },
69
+ permission_request: { hook: PERMISSION_UI_PROMPT_EVENT, label: "Permission Request" },
60
70
  };
61
71
 
62
72
  /**
63
73
  * Pi lifecycle event types (dispatched by ExtensionRunner).
64
74
  * These must use pi.on() — not pi.events.on() — to receive events.
65
75
  */
66
- const LIFECYCLE_EVENTS = new Set(["agent_end", "session_shutdown"]);
76
+ const LIFECYCLE_EVENTS = new Set(["agent_end", "agent_settled", "session_shutdown"]);
67
77
 
68
78
  /**
69
79
  * Register event listeners for all enabled notification events.
@@ -77,9 +87,9 @@ export function registerEventListeners(
77
87
  // Remove all previously registered EventBus listeners to prevent accumulation
78
88
  // across reloads (EventBus persists but module instances are replaced).
79
89
  unregisterAll();
80
- // Register built-in events (except agent_end which has custom logic)
90
+ // Register built-in events (except agent lifecycle notifications which have custom logic)
81
91
  for (const [eventKey, def] of Object.entries(BUILTIN_EVENTS)) {
82
- if (eventKey === "agent_end") continue; // handled separately below
92
+ if (isAgentNotificationEvent(eventKey)) continue; // handled separately below
83
93
 
84
94
  const eventConfig = config.events[eventKey];
85
95
  if (!eventConfig?.enabled) continue;
@@ -95,8 +105,8 @@ export function registerEventListeners(
95
105
  );
96
106
  };
97
107
 
98
- // pi lifecycle events (agent_end, session_shutdown) are dispatched via
99
- // ExtensionRunner — must use pi.on(). These are stored in
108
+ // Pi lifecycle events are dispatched via ExtensionRunner — must use
109
+ // pi.on(). These are stored in
100
110
  // extension.handlers and automatically replaced on reload, so they
101
111
  // do NOT accumulate like EventBus listeners.
102
112
  if (LIFECYCLE_EVENTS.has(eventKey)) {
@@ -120,55 +130,8 @@ export function registerEventListeners(
120
130
  }));
121
131
  }
122
132
 
123
- // agent_end custom handler with session name and recap support
124
- const agentEndConfig = config.events["agent_end"];
125
- if (agentEndConfig?.enabled) {
126
- const handler = (payload: unknown) => {
127
- // Fire-and-forget: build message and dispatch in background,
128
- // don't block agent_end from completing
129
- const sessionName = pi.getSessionName?.();
130
- const title = `Pi — ${BUILTIN_EVENTS.agent_end.label}`;
131
-
132
- if (config.recap.enabled) {
133
- // Recap mode: summarize asynchronously, then dispatch
134
- const lastText = extractLastAssistantText(payload);
135
- if (lastText && sessionCtx?.modelRegistry) {
136
- const provider = extractProvider(config.recap.model);
137
- const modelId = extractModelId(config.recap.model);
138
- const model = sessionCtx.modelRegistry.find(provider, modelId);
139
- if (model) {
140
- sessionCtx.modelRegistry.getApiKeyAndHeaders(model)
141
- .then((apiKeyResult) => {
142
- const apiKey = apiKeyResult.ok ? (apiKeyResult as { apiKey?: string }).apiKey : undefined;
143
- if (apiKey) {
144
- return summarizeLastMessage(lastText, apiKey, model.baseUrl, model.api, modelId)
145
- .then((recap) => sessionName ? `${sessionName}: ${recap}` : recap);
146
- }
147
- return buildAgentEndMessage(sessionName);
148
- })
149
- .catch(() => buildAgentEndMessage(sessionName))
150
- .then((message) =>
151
- dispatchNotification(pi, title, message, agentEndConfig.platforms, "agent_end", config, cwd)
152
- )
153
- .catch(() => {
154
- // Silently ignore — background agent_end notification failure is non-blocking.
155
- });
156
- return;
157
- }
158
- }
159
- }
160
-
161
- // No recap or recap unavailable: dispatch immediately in background
162
- const message = buildAgentEndMessage(sessionName);
163
- dispatchNotification(pi, title, message, agentEndConfig.platforms, "agent_end", config, cwd).catch(
164
- () => {
165
- // Silently ignore — background agent_end notification failure is non-blocking.
166
- }
167
- );
168
- };
169
-
170
- (pi as any).on("agent_end", handler);
171
- }
133
+ registerAgentNotification(pi, "agent_end", config, cwd);
134
+ registerAgentNotification(pi, "agent_settled", config, cwd);
172
135
 
173
136
  // Listen for dynamic module events
174
137
  const moduleHandler = async (payload: unknown) => {
@@ -335,25 +298,109 @@ function buildEventMessage(eventKey: string, payload: unknown): string {
335
298
  case "mcp_server_error":
336
299
  return `Server "${String(p.name || "unknown")}" error: ${String(p.error || "unknown error")}`;
337
300
  case "agent_end":
338
- return "Agent finished responding";
301
+ return "Agent run finished responding";
302
+ case "agent_settled":
303
+ return "Agent is complete";
339
304
  case "memory_consolidated":
340
305
  return `Memory consolidated (${p.count || 0} items)`;
341
306
  case "session_shutdown":
342
307
  return "Session ending";
343
308
  case "ask_user_prompt":
344
309
  return buildAskUserPromptMessage(payload);
310
+ case "permission_request":
311
+ return buildPermissionPromptMessage(payload);
345
312
  default:
346
313
  return p.message ? String(p.message) : "Event occurred";
347
314
  }
348
315
  }
349
316
 
350
- /** Build agent_end message using session name */
351
- function buildAgentEndMessage(sessionName: string | undefined): string {
352
- if (sessionName) return `${sessionName} - Agent is complete`;
353
- return "Agent is complete";
317
+ /** Register an agent lifecycle notification with session name and recap support. */
318
+ function registerAgentNotification(
319
+ pi: ExtensionAPI,
320
+ eventKey: "agent_end" | "agent_settled",
321
+ config: NotifyConfig,
322
+ cwd: string
323
+ ): void {
324
+ const eventConfig = config.events[eventKey];
325
+ if (!eventConfig?.enabled) return;
326
+
327
+ const handler = (payload: unknown) => {
328
+ // Fire-and-forget: build message and dispatch in background,
329
+ // don't block agent lifecycle hooks from completing.
330
+ const sessionName = pi.getSessionName?.();
331
+ const title = `Pi — ${BUILTIN_EVENTS[eventKey].label}`;
332
+
333
+ if (config.recap.enabled) {
334
+ // Recap mode: summarize asynchronously, then dispatch.
335
+ // agent_settled does not currently include a messages payload, so fall
336
+ // back to the latest assistant message in the session.
337
+ const lastText = extractLastAssistantText(payload) ?? extractLastAssistantTextFromSession();
338
+ if (lastText && sessionCtx?.modelRegistry) {
339
+ const provider = extractProvider(config.recap.model);
340
+ const modelId = extractModelId(config.recap.model);
341
+ const model = sessionCtx.modelRegistry.find(provider, modelId);
342
+ if (model) {
343
+ sessionCtx.modelRegistry.getApiKeyAndHeaders(model)
344
+ .then((apiKeyResult) => {
345
+ const apiKey = apiKeyResult.ok ? (apiKeyResult as { apiKey?: string }).apiKey : undefined;
346
+ if (apiKey) {
347
+ return summarizeLastMessage(lastText, apiKey, model.baseUrl, model.api, modelId)
348
+ .then((recap) => sessionName ? `${sessionName}: ${recap}` : recap);
349
+ }
350
+ return buildAgentLifecycleMessage(eventKey, sessionName);
351
+ })
352
+ .catch(() => buildAgentLifecycleMessage(eventKey, sessionName))
353
+ .then((message) =>
354
+ dispatchNotification(pi, title, message, eventConfig.platforms, eventKey, config, cwd)
355
+ )
356
+ .catch(() => {
357
+ // Silently ignore — background agent notification failure is non-blocking.
358
+ });
359
+ return;
360
+ }
361
+ }
362
+ }
363
+
364
+ // No recap or recap unavailable: dispatch immediately in background.
365
+ const message = buildAgentLifecycleMessage(eventKey, sessionName);
366
+ dispatchNotification(pi, title, message, eventConfig.platforms, eventKey, config, cwd).catch(
367
+ () => {
368
+ // Silently ignore — background agent notification failure is non-blocking.
369
+ }
370
+ );
371
+ };
372
+
373
+ (pi as any).on(eventKey, handler);
374
+ }
375
+
376
+ /** Whether an event key is an agent lifecycle notification with custom handling. */
377
+ function isAgentNotificationEvent(eventKey: string): eventKey is "agent_end" | "agent_settled" {
378
+ return eventKey === "agent_end" || eventKey === "agent_settled";
379
+ }
380
+
381
+ /** Build agent lifecycle message using session name. */
382
+ function buildAgentLifecycleMessage(
383
+ eventKey: "agent_end" | "agent_settled",
384
+ sessionName: string | undefined
385
+ ): string {
386
+ const status = eventKey === "agent_end" ? "Agent run is complete" : "Agent is complete";
387
+ if (sessionName) return `${sessionName} - ${status}`;
388
+ return status;
389
+ }
390
+
391
+ /** Extract text from the latest assistant message in the current session. */
392
+ function extractLastAssistantTextFromSession(): string | null {
393
+ const entries = sessionCtx?.sessionManager.getEntries() ?? [];
394
+ for (let i = entries.length - 1; i >= 0; i--) {
395
+ const entry = entries[i];
396
+ if (entry?.type !== "message") continue;
397
+ const text = extractAssistantText(entry.message);
398
+ if (text) return text;
399
+ }
400
+ return null;
354
401
  }
355
402
 
356
- /** Extract text from the last assistant message in agent_end payload */
403
+ /** Extract text from the last assistant message in an agent lifecycle payload. */
357
404
  function extractLastAssistantText(payload: unknown): string | null {
358
405
  const p = payload as { messages?: Array<{ role?: string; content?: unknown }> };
359
406
  if (!p?.messages || !Array.isArray(p.messages)) return null;
@@ -363,21 +410,31 @@ function extractLastAssistantText(payload: unknown): string | null {
363
410
  const msg = p.messages[i];
364
411
  if (msg?.role !== "assistant") continue;
365
412
 
366
- const content = msg.content;
367
- if (typeof content === "string") return content;
368
- if (Array.isArray(content)) {
369
- // Extract text blocks from content array
370
- const textParts: string[] = [];
371
- for (const block of content) {
372
- if (typeof block === "object" && block !== null) {
373
- const b = block as { type?: string; text?: string };
374
- if (b.type === "text" && typeof b.text === "string") {
375
- textParts.push(b.text);
376
- }
413
+ const text = extractAssistantText(msg);
414
+ if (text) return text;
415
+ }
416
+
417
+ return null;
418
+ }
419
+
420
+ /** Extract text from an assistant message-like object. */
421
+ function extractAssistantText(message: { role?: string; content?: unknown }): string | null {
422
+ if (message.role !== "assistant") return null;
423
+
424
+ const content = message.content;
425
+ if (typeof content === "string") return content;
426
+ if (Array.isArray(content)) {
427
+ // Extract text blocks from content array.
428
+ const textParts: string[] = [];
429
+ for (const block of content) {
430
+ if (typeof block === "object" && block !== null) {
431
+ const b = block as { type?: string; text?: string };
432
+ if (b.type === "text" && typeof b.text === "string") {
433
+ textParts.push(b.text);
377
434
  }
378
435
  }
379
- if (textParts.length > 0) return textParts.join("\n");
380
436
  }
437
+ if (textParts.length > 0) return textParts.join("\n");
381
438
  }
382
439
 
383
440
  return null;
@@ -394,11 +451,3 @@ function extractModelId(modelRef: string): string {
394
451
  const slashIdx = modelRef.indexOf("/");
395
452
  return slashIdx > 0 ? modelRef.slice(slashIdx + 1) : modelRef;
396
453
  }
397
-
398
- /** Resolve API key for a provider from environment variables */
399
- function resolveApiKey(modelRef: string): string | undefined {
400
- const provider = extractProvider(modelRef);
401
- // Try standard env var patterns
402
- const envKey = `${provider.toUpperCase().replace(/-/g, "_")}_API_KEY`;
403
- return process.env[envKey];
404
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/notify",
3
- "version": "2.1.2",
3
+ "version": "2.2.0",
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",
@@ -20,7 +20,7 @@
20
20
  "notifications"
21
21
  ],
22
22
  "scripts": {
23
- "test": "tsc --noEmit && node --experimental-strip-types --test src/__tests__/*.test.ts",
23
+ "test": "tsc --noEmit && npx tsx --test src/__tests__/*.test.ts",
24
24
  "typecheck": "tsc --noEmit"
25
25
  },
26
26
  "files": [
@@ -42,7 +42,7 @@
42
42
  "access": "public"
43
43
  },
44
44
  "dependencies": {
45
- "@pi-unipi/core": "*",
45
+ "@pi-unipi/core": "2.2.0",
46
46
  "node-notifier": "^10.0.1"
47
47
  },
48
48
  "peerDependencies": {
package/settings.ts CHANGED
@@ -24,9 +24,11 @@ export const DEFAULT_CONFIG: NotifyConfig = {
24
24
  ralph_loop_end: { enabled: true, platforms: [] },
25
25
  mcp_server_error: { enabled: true, platforms: [] },
26
26
  agent_end: { enabled: false, platforms: [] },
27
+ agent_settled: { enabled: false, platforms: [] },
27
28
  memory_consolidated: { enabled: false, platforms: [] },
28
29
  session_shutdown: { enabled: false, platforms: [] },
29
30
  ask_user_prompt: { enabled: false, platforms: [] },
31
+ permission_request: { enabled: false, platforms: [] },
30
32
  },
31
33
  native: {
32
34
  enabled: true,
@@ -34,8 +34,11 @@ Help users configure the `@pi-unipi/notify` notification system.
34
34
  "ralph_loop_end": { "enabled": true, "platforms": [] },
35
35
  "mcp_server_error": { "enabled": true, "platforms": [] },
36
36
  "agent_end": { "enabled": false, "platforms": [] },
37
+ "agent_settled": { "enabled": false, "platforms": [] },
37
38
  "memory_consolidated": { "enabled": false, "platforms": [] },
38
- "session_shutdown": { "enabled": false, "platforms": [] }
39
+ "session_shutdown": { "enabled": false, "platforms": [] },
40
+ "ask_user_prompt": { "enabled": false, "platforms": [] },
41
+ "permission_request": { "enabled": false, "platforms": [] }
39
42
  },
40
43
  "native": {
41
44
  "enabled": true,
@@ -150,12 +153,38 @@ ntfy uses dedicated `ntfy.json` files at both global and project scope, with ful
150
153
  | `workflow_end` | On | Workflow command completes |
151
154
  | `ralph_loop_end` | On | Ralph loop completes |
152
155
  | `mcp_server_error` | On | MCP server error |
153
- | `agent_end` | Off | Agent finishes responding |
156
+ | `agent_end` | Off | Low-level agent run ends (may fire again on retries) |
157
+ | `agent_settled` | Off | Agent fully settles after retries, compaction, and queued continuations |
154
158
  | `memory_consolidated` | Off | Memory auto-saved |
155
159
  | `session_shutdown` | Off | Session ends |
160
+ | `ask_user_prompt` | Off | Agent asked a question and is waiting for an answer |
161
+ | `permission_request` | Off | A permission prompt is about to be shown |
156
162
 
157
163
  Each event can override `platforms` — empty array means use `defaultPlatforms`.
158
164
 
165
+ ### `permission_request`
166
+
167
+ Fires on the `permissions:ui_prompt` broadcast from
168
+ [`@gotgenes/pi-permission-system`](https://www.npmjs.com/package/@gotgenes/pi-permission-system),
169
+ emitted immediately before the user-facing permission UI is invoked. Policy
170
+ auto-allow, policy deny, session approvals, and infrastructure auto-allowed
171
+ requests do **not** emit it, so this event does not produce notification spam.
172
+ Forwarded subagent prompts are handled too — the parent UI session emits the
173
+ event right before showing the forwarded dialog, and the notification is
174
+ suffixed with `(forwarded)`.
175
+
176
+ The notification is formatted defensively from the payload's `agentName`,
177
+ `surface`, `value` and `message` fields:
178
+
179
+ ```text
180
+ Pi — Permission Request
181
+ Current agent requested bash 'npm test'. Allow this command?
182
+ ```
183
+
184
+ Enable it when you run Pi in a background pane and don't want to miss a
185
+ permission prompt. If the permission system is not installed the event simply
186
+ never fires.
187
+
159
188
  ## Agent workflow
160
189
 
161
190
  ### Reading current config
@@ -11,6 +11,7 @@ import { 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";
14
+ import { boxInnerWidth } from "@pi-unipi/core";
14
15
 
15
16
  type SetupPhase =
16
17
  | "instructions"
@@ -235,7 +236,7 @@ export class GotifySetupOverlay implements Component {
235
236
  }
236
237
 
237
238
  render(width: number): string[] {
238
- const innerWidth = Math.max(22, width - 2);
239
+ const innerWidth = boxInnerWidth(width);
239
240
  const lines: string[] = [];
240
241
 
241
242
  lines.push(this.borderLine(innerWidth, "top"));
package/tui/ntfy-setup.ts CHANGED
@@ -11,6 +11,7 @@ import { 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";
14
+ import { boxInnerWidth } from "@pi-unipi/core";
14
15
 
15
16
  type SetupPhase =
16
17
  | "instructions"
@@ -281,7 +282,7 @@ export class NtfySetupOverlay implements Component {
281
282
  }
282
283
 
283
284
  render(width: number): string[] {
284
- const innerWidth = Math.max(22, width - 2);
285
+ const innerWidth = boxInnerWidth(width);
285
286
  const lines: string[] = [];
286
287
 
287
288
  lines.push(this.borderLine(innerWidth, "top"));
@@ -8,7 +8,7 @@
8
8
  import type { Component } from "@earendil-works/pi-tui";
9
9
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
10
10
  import type { Theme } from "@earendil-works/pi-coding-agent";
11
- import { readModelCache, type CachedModel } from "@pi-unipi/core";
11
+ import { readModelCache, type CachedModel, boxInnerWidth } from "@pi-unipi/core";
12
12
  import { loadConfig, saveConfig } from "../settings.js";
13
13
 
14
14
  const DEFAULT_MODEL = "openrouter/openai/gpt-oss-20b";
@@ -170,7 +170,7 @@ export class RecapModelSelectorOverlay implements Component {
170
170
  }
171
171
 
172
172
  render(width: number): string[] {
173
- const innerWidth = Math.max(40, width - 2);
173
+ const innerWidth = boxInnerWidth(width);
174
174
  const lines: string[] = [];
175
175
 
176
176
  lines.push(this.borderLine(innerWidth, "top"));
@@ -15,6 +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
19
 
19
20
  /** Section types */
20
21
  type Section = "platforms" | "events" | "recap";
@@ -174,7 +175,7 @@ export class NotifySettingsOverlay implements Component {
174
175
  }
175
176
 
176
177
  render(width: number): string[] {
177
- const innerWidth = Math.max(22, width - 2);
178
+ const innerWidth = boxInnerWidth(width);
178
179
  const lines: string[] = [];
179
180
 
180
181
  lines.push(this.borderLine(innerWidth, "top"));
@@ -10,6 +10,7 @@ import { 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";
13
+ import { boxInnerWidth } from "@pi-unipi/core";
13
14
 
14
15
  type SetupPhase = "instructions" | "token" | "polling" | "success" | "error" | "timeout";
15
16
 
@@ -224,7 +225,7 @@ export class TelegramSetupOverlay implements Component {
224
225
  }
225
226
 
226
227
  render(width: number): string[] {
227
- const innerWidth = Math.max(22, width - 2);
228
+ const innerWidth = boxInnerWidth(width);
228
229
  const lines: string[] = [];
229
230
 
230
231
  lines.push(this.borderLine(innerWidth, "top"));