@xynogen/pix-pretty 1.7.5 → 1.7.6
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 +1 -1
- package/src/gate-overlay.test.ts +53 -0
- package/src/gate-overlay.ts +37 -3
package/package.json
CHANGED
package/src/gate-overlay.test.ts
CHANGED
|
@@ -178,3 +178,56 @@ describe("showOverlay — sudo mode", () => {
|
|
|
178
178
|
expect(joined).toContain("●");
|
|
179
179
|
});
|
|
180
180
|
});
|
|
181
|
+
|
|
182
|
+
// ── Auto-deny timer (dead-man's switch) ───────────────────────────────────────
|
|
183
|
+
//
|
|
184
|
+
// Timer-aware mock: unlike makeUI, this keeps the promise pending and resolves
|
|
185
|
+
// only when `done` fires — so a real setInterval expiry can drive the result.
|
|
186
|
+
// `onReady` gets the live component to optionally feed input before expiry.
|
|
187
|
+
function makeTimerUI(onReady?: (comp: Wired) => void): OverlayUI {
|
|
188
|
+
return {
|
|
189
|
+
custom: <T>(
|
|
190
|
+
cb: (
|
|
191
|
+
tui: { requestRender(): void },
|
|
192
|
+
th: typeof theme,
|
|
193
|
+
kb: unknown,
|
|
194
|
+
done: (v: T) => void,
|
|
195
|
+
) => Wired,
|
|
196
|
+
): Promise<T | undefined> =>
|
|
197
|
+
new Promise((resolve) => {
|
|
198
|
+
const comp = cb({ requestRender: () => {} }, theme, undefined, (v) =>
|
|
199
|
+
resolve(v),
|
|
200
|
+
);
|
|
201
|
+
comp.render(80);
|
|
202
|
+
onReady?.(comp);
|
|
203
|
+
}),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
describe("showOverlay — auto-deny timer", () => {
|
|
208
|
+
test("expires to timeout when left untouched", async () => {
|
|
209
|
+
const result = await showOverlay(makeTimerUI(), {
|
|
210
|
+
mode: "confirm",
|
|
211
|
+
title: "T",
|
|
212
|
+
timeoutMs: 1000, // ceil → 1s, fires on first tick
|
|
213
|
+
});
|
|
214
|
+
expect(result.action).toBe("timeout");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("first keypress cancels the timer (no auto-deny)", async () => {
|
|
218
|
+
let live: Wired | undefined;
|
|
219
|
+
const pending = showOverlay(
|
|
220
|
+
makeTimerUI((comp) => {
|
|
221
|
+
live = comp;
|
|
222
|
+
comp.handleInput(DOWN); // any key — cancels the dead-man's switch
|
|
223
|
+
}),
|
|
224
|
+
{ mode: "confirm", title: "T", timeoutMs: 1000 },
|
|
225
|
+
);
|
|
226
|
+
// Wait well past the 1s window. A live timer would have resolved "timeout";
|
|
227
|
+
// since the keypress cancelled it, the promise is still pending here.
|
|
228
|
+
await new Promise((r) => setTimeout(r, 1300));
|
|
229
|
+
live?.handleInput(ENTER); // now deny explicitly
|
|
230
|
+
const result = await pending;
|
|
231
|
+
expect(result.action).toBe("denied");
|
|
232
|
+
});
|
|
233
|
+
});
|
package/src/gate-overlay.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { frameLines, modalWidth, selectListTheme } from "./modal-frame.js";
|
|
|
24
24
|
|
|
25
25
|
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
26
26
|
|
|
27
|
-
export type OverlayAction = "approved" | "denied" | "timeout";
|
|
27
|
+
export type OverlayAction = "approved" | "denied" | "timeout";
|
|
28
28
|
|
|
29
29
|
export interface OverlayResult {
|
|
30
30
|
action: OverlayAction;
|
|
@@ -45,7 +45,11 @@ interface BaseConfig {
|
|
|
45
45
|
title: string;
|
|
46
46
|
/** Optional body lines under the title. */
|
|
47
47
|
body?: string[];
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Auto-deny after this many ms of NO user input (dead-man's switch). The
|
|
50
|
+
* first keypress cancels the timer and the dialog then waits indefinitely.
|
|
51
|
+
* 0 or omitted = no timer (wait forever). Resolves with action "timeout".
|
|
52
|
+
*/
|
|
49
53
|
timeoutMs?: number;
|
|
50
54
|
/**
|
|
51
55
|
* Choices shown in the SelectList.
|
|
@@ -228,6 +232,18 @@ export function showOverlay(
|
|
|
228
232
|
let stage: Stage = "select";
|
|
229
233
|
let countdownLine: string | undefined;
|
|
230
234
|
|
|
235
|
+
// Dead-man's-switch timer: counts down only while untouched. The
|
|
236
|
+
// first keypress cancels it (user is present → let them decide). If
|
|
237
|
+
// it expires with no input, auto-deny so the agent isn't stuck.
|
|
238
|
+
const timeoutMs = config.timeoutMs ?? 0;
|
|
239
|
+
let remaining = Math.ceil(timeoutMs / 1000);
|
|
240
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
241
|
+
const cancelTimer = () => {
|
|
242
|
+
if (timer) clearInterval(timer);
|
|
243
|
+
timer = undefined;
|
|
244
|
+
countdownLine = undefined;
|
|
245
|
+
};
|
|
246
|
+
|
|
231
247
|
// ── components ──────────────────────────────────────────────────
|
|
232
248
|
const selectItems: SelectItem[] = choices.map((c) => ({
|
|
233
249
|
value: c.value,
|
|
@@ -243,7 +259,24 @@ export function showOverlay(
|
|
|
243
259
|
const maskedInput = new MaskedInput();
|
|
244
260
|
|
|
245
261
|
// ── finish ───────────────────────────────────────────────────────
|
|
246
|
-
const finish = (result: OverlayResult) =>
|
|
262
|
+
const finish = (result: OverlayResult) => {
|
|
263
|
+
cancelTimer();
|
|
264
|
+
done(result);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// Arm the dead-man's switch (only when a timeout was requested).
|
|
268
|
+
if (timeoutMs > 0) {
|
|
269
|
+
countdownLine = theme.fg("dim", `auto-deny in ${remaining}s`);
|
|
270
|
+
timer = setInterval(() => {
|
|
271
|
+
remaining -= 1;
|
|
272
|
+
if (remaining <= 0) {
|
|
273
|
+
finish({ action: "timeout" });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
countdownLine = theme.fg("dim", `auto-deny in ${remaining}s`);
|
|
277
|
+
tui.requestRender();
|
|
278
|
+
}, 1000);
|
|
279
|
+
}
|
|
247
280
|
|
|
248
281
|
// ── event wiring ─────────────────────────────────────────────────
|
|
249
282
|
selectList.onSelect = (item) => {
|
|
@@ -285,6 +318,7 @@ export function showOverlay(
|
|
|
285
318
|
},
|
|
286
319
|
invalidate: () => {},
|
|
287
320
|
handleInput: (data) => {
|
|
321
|
+
cancelTimer(); // user is present — stop the auto-deny countdown
|
|
288
322
|
if (stage === "select") selectList.handleInput(data);
|
|
289
323
|
else maskedInput.handleInput(data);
|
|
290
324
|
tui.requestRender();
|