@pi-unipi/info-screen 2.14.0 → 2.14.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.
- package/index.ts +49 -10
- package/package.json +3 -3
- package/tui/info-overlay.ts +54 -22
package/index.ts
CHANGED
|
@@ -113,10 +113,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
113
113
|
*/
|
|
114
114
|
function showOverlay(ctx: ExtensionContext, autoCloseMs?: number): void {
|
|
115
115
|
let overlay: InfoOverlay;
|
|
116
|
+
// Splash mode (auto-close): the overlay must NEVER take keyboard focus —
|
|
117
|
+
// it lives exactly during the window where the user starts typing their
|
|
118
|
+
// first prompt. A capturing overlay here eats those keystrokes (worse: its
|
|
119
|
+
// vim-style keys "work", so command text vanishes into tab switches), and
|
|
120
|
+
// its first-keypress-cancels-auto-close rule strands it on screen forever
|
|
121
|
+
// as an unclosable, input-eating zombie. Non-capturing + stack-safe
|
|
122
|
+
// self-dismiss (see startBootTimer) makes it pure eye-candy.
|
|
123
|
+
const splashMode = autoCloseMs !== undefined && autoCloseMs > 0;
|
|
116
124
|
ctx.ui.custom<void>(
|
|
117
125
|
(tui, theme, _keybindings, done) => {
|
|
118
126
|
overlay = new InfoOverlay();
|
|
119
127
|
overlay.setTheme(theme);
|
|
128
|
+
overlay.interactive = !splashMode;
|
|
120
129
|
overlayVisible = true;
|
|
121
130
|
overlay.onClose = () => {
|
|
122
131
|
overlayVisible = false;
|
|
@@ -127,14 +136,37 @@ export default function (pi: ExtensionAPI) {
|
|
|
127
136
|
const component = {
|
|
128
137
|
render: (w: number) => overlay.render(w),
|
|
129
138
|
invalidate: () => overlay.invalidate(),
|
|
139
|
+
// In splash mode the overlay is non-capturing and never receives
|
|
140
|
+
// input; keep handleInput wired for interactive ("on") mode.
|
|
130
141
|
handleInput: (data: string) => {
|
|
131
142
|
overlay.handleInput(data);
|
|
132
143
|
tui.requestRender();
|
|
133
144
|
},
|
|
134
145
|
};
|
|
135
|
-
//
|
|
146
|
+
// Splash self-dismiss may only run while this overlay is the topmost
|
|
147
|
+
// VISIBLE entry of the TUI overlay stack. If the user opens anything
|
|
148
|
+
// during the splash window (a /unipi:… settings dialog, the updater's
|
|
149
|
+
// update prompt, …), dismissal must wait — removing a covered entry
|
|
150
|
+
// breaks the covering overlay (pi retargets focus and orphans its
|
|
151
|
+
// pending interaction, e.g. a hung ctx.ui.select promise). Re-check
|
|
152
|
+
// on every timer tick; once the stack clears we dismiss as usual.
|
|
153
|
+
const isTopmostVisible = (): boolean => {
|
|
154
|
+
try {
|
|
155
|
+
const stack = (tui as unknown as { overlayStack?: Array<{ component?: unknown; hidden?: boolean }> }).overlayStack;
|
|
156
|
+
if (!stack || stack.length === 0) return true;
|
|
157
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
158
|
+
const entry = stack[i];
|
|
159
|
+
if (entry?.hidden) continue;
|
|
160
|
+
return entry?.component === component;
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
} catch {
|
|
164
|
+
return true; // Stack unreadable — assume topmost (legacy behavior).
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
// Boot dashboard dismisses itself (splash mode).
|
|
136
168
|
if (autoCloseMs && autoCloseMs > 0) {
|
|
137
|
-
overlay.startBootTimer(autoCloseMs);
|
|
169
|
+
overlay.startBootTimer(autoCloseMs, isTopmostVisible);
|
|
138
170
|
}
|
|
139
171
|
return component;
|
|
140
172
|
},
|
|
@@ -145,17 +177,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
145
177
|
minWidth: 60,
|
|
146
178
|
anchor: "center" as const,
|
|
147
179
|
margin: 2,
|
|
180
|
+
nonCapturing: splashMode,
|
|
148
181
|
},
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
// until we are the focused (topmost) entry; the user can still press
|
|
156
|
-
// q/Esc to close once the covering overlay is gone.
|
|
182
|
+
// In splash mode the timer uses `selfHide` (handle.hide() splices this
|
|
183
|
+
// entry out by identity) guarded by the topmost-visible check above —
|
|
184
|
+
// never `done()`, which pops whatever is TOPMOST. `isTopmostOverlay`
|
|
185
|
+
// remains as the focused-done() fallback for interactive ("on") mode,
|
|
186
|
+
// where the user drives the dashboard directly: it is topmost while
|
|
187
|
+
// being driven, so the q/Esc → done() path is correct there.
|
|
157
188
|
onHandle: (handle) => {
|
|
158
189
|
overlay.isTopmostOverlay = () => handle.isFocused();
|
|
190
|
+
overlay.selfHide = () => {
|
|
191
|
+
overlayVisible = false;
|
|
192
|
+
if (typeof handle.hide === "function") {
|
|
193
|
+
handle.hide();
|
|
194
|
+
} else if (typeof (handle as { setHidden?: (v: boolean) => void }).setHidden === "function") {
|
|
195
|
+
(handle as { setHidden: (v: boolean) => void }).setHidden(true);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
159
198
|
},
|
|
160
199
|
}
|
|
161
200
|
);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/info-screen",
|
|
3
|
-
"version": "2.14.
|
|
4
|
-
"description": "Dashboard and module registry for Unipi
|
|
3
|
+
"version": "2.14.1",
|
|
4
|
+
"description": "Dashboard and module registry for Unipi \u2014 configurable info overlay with tabbed groups",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
7
7
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@pi-unipi/core": "2.14.
|
|
36
|
+
"@pi-unipi/core": "2.14.1"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-coding-agent": "^0.84.0",
|
package/tui/info-overlay.ts
CHANGED
|
@@ -69,6 +69,21 @@ export class InfoOverlay implements Component {
|
|
|
69
69
|
* `startBootTimer` for why.
|
|
70
70
|
*/
|
|
71
71
|
isTopmostOverlay?: () => boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Stack-safe self-removal via the overlay handle (`handle.hide()` splices
|
|
74
|
+
* this entry out of the TUI stack by identity, unlike `done()` which pops
|
|
75
|
+
* whatever is TOPMOST). Set from `onHandle` in index.ts. When available,
|
|
76
|
+
* the boot auto-close timer prefers this over `done()` so a splash that
|
|
77
|
+
* lingers while another overlay opens can never dismiss that overlay
|
|
78
|
+
* instead of itself.
|
|
79
|
+
*/
|
|
80
|
+
selfHide?: () => void;
|
|
81
|
+
/**
|
|
82
|
+
* False when running as a non-capturing boot splash (auto-close): the
|
|
83
|
+
* overlay never receives keyboard input, so interactive hints like
|
|
84
|
+
* "q/Esc close" would be misleading and are replaced accordingly.
|
|
85
|
+
*/
|
|
86
|
+
interactive = true;
|
|
72
87
|
|
|
73
88
|
private overlay = new OverlayTheme();
|
|
74
89
|
|
|
@@ -232,31 +247,46 @@ export class InfoOverlay implements Component {
|
|
|
232
247
|
}
|
|
233
248
|
|
|
234
249
|
/**
|
|
235
|
-
* Auto-close the overlay after `ms
|
|
250
|
+
* Auto-close the overlay after `ms`.
|
|
236
251
|
*
|
|
237
|
-
* Used when the overlay is shown
|
|
238
|
-
* it should get out of the way on its own rather than
|
|
252
|
+
* Used when the overlay is shown as a boot splash: the dashboard is
|
|
253
|
+
* informational, so it should get out of the way on its own rather than
|
|
254
|
+
* requiring a keypress.
|
|
239
255
|
*
|
|
240
|
-
*
|
|
241
|
-
* TUI stack
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
256
|
+
* Dismissal uses `selfHide` (handle.hide() — removes THIS entry from the
|
|
257
|
+
* TUI overlay stack by identity) and only fires while `isTopmostVisible`
|
|
258
|
+
* confirms nothing is stacked above: dismissing a covered overlay breaks
|
|
259
|
+
* the covering one (pi retargets focus and orphans its pending
|
|
260
|
+
* interaction, e.g. a ctx.ui.select promise that never resolves while its
|
|
261
|
+
* overlay vanishes). If covered, the timer re-arms and retries.
|
|
262
|
+
*
|
|
263
|
+
* If `selfHide` is unavailable (older host), falls back to the guarded
|
|
264
|
+
* `onClose` (`done()`) path, which requires focus (topmost) for the same
|
|
265
|
+
* reason.
|
|
248
266
|
*/
|
|
249
|
-
startBootTimer(ms: number): void {
|
|
267
|
+
startBootTimer(ms: number, isTopmostVisible?: () => boolean): void {
|
|
250
268
|
this.cancelBootTimer();
|
|
251
269
|
if (!Number.isFinite(ms) || ms <= 0) return;
|
|
252
270
|
const arm = (): void => {
|
|
253
271
|
this.bootTimer = setTimeout(() => {
|
|
254
272
|
this.bootTimer = null;
|
|
255
273
|
if (this._destroyed) return;
|
|
274
|
+
if (this.selfHide) {
|
|
275
|
+
if (isTopmostVisible && !isTopmostVisible()) {
|
|
276
|
+
// Something is stacked on top of us — dismissing now would break
|
|
277
|
+
// it (orphaned select promise, focus retarget). Retry shortly;
|
|
278
|
+
// once the stack clears we dismiss as usual.
|
|
279
|
+
arm();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
this.selfHide();
|
|
283
|
+
this.destroy();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
256
286
|
if (this.isTopmostOverlay && !this.isTopmostOverlay()) {
|
|
257
|
-
//
|
|
258
|
-
// instead of this dashboard. Retry shortly; once
|
|
259
|
-
//
|
|
287
|
+
// Fallback (no selfHide available): closing now would pop the
|
|
288
|
+
// covering overlay instead of this dashboard. Retry shortly; once
|
|
289
|
+
// we are topmost the close is safe.
|
|
260
290
|
arm();
|
|
261
291
|
return;
|
|
262
292
|
}
|
|
@@ -346,7 +376,7 @@ export class InfoOverlay implements Component {
|
|
|
346
376
|
lines.push(this.overlay.frameLine(this.overlay.fg("dim", "Modules will register groups on startup."), innerWidth));
|
|
347
377
|
for (let i = 0; i < 4; i++) lines.push(this.overlay.frameLine("", innerWidth));
|
|
348
378
|
lines.push(this.overlay.ruleLine(innerWidth));
|
|
349
|
-
lines.push(this.overlay.frameLine(this.overlay.fg("dim", "q/Esc close · r refresh"), innerWidth));
|
|
379
|
+
lines.push(this.overlay.frameLine(this.overlay.fg("dim", this.interactive ? "q/Esc close · r refresh" : "auto-dismissing…"), innerWidth));
|
|
350
380
|
lines.push(this.overlay.borderLine(innerWidth, "bottom"));
|
|
351
381
|
return lines;
|
|
352
382
|
}
|
|
@@ -529,12 +559,14 @@ export class InfoOverlay implements Component {
|
|
|
529
559
|
const lastUp = infoRegistry.getLastUpdated(group?.id ?? "");
|
|
530
560
|
const age = lastUp > 0 ? humanizeAge(Date.now() - lastUp) : "loading…";
|
|
531
561
|
|
|
532
|
-
const hints =
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
562
|
+
const hints = this.interactive
|
|
563
|
+
? [
|
|
564
|
+
`${this.overlay.fg("accent", "←/→")} tabs`,
|
|
565
|
+
`${this.overlay.fg("success", "↑/↓")} scroll`,
|
|
566
|
+
`${this.overlay.fg("warning", "r")} refresh`,
|
|
567
|
+
`${this.overlay.fg("error", "q/Esc")} close`,
|
|
568
|
+
]
|
|
569
|
+
: [`${this.overlay.fg("dim", "auto-dismissing…")}`];
|
|
538
570
|
|
|
539
571
|
const hintStr = hints.join(` ${this.overlay.fg("borderMuted", "•")} `);
|
|
540
572
|
|