@xynogen/pix-pretty 1.7.5 → 1.7.7

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": "@xynogen/pix-pretty",
3
- "version": "1.7.5",
3
+ "version": "1.7.7",
4
4
  "description": "Enhanced tool output rendering with syntax highlighting, file icons, tree views, FFF search, and paste chip formatting",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import { parseDiff } from "./diff.js";
4
+
5
+ const OLD = "line1\nline2\nline3";
6
+ const NEW = "line1\nCHANGED\nline3";
7
+
8
+ describe("parseDiff baseLine", () => {
9
+ it("is snippet-relative when baseLine omitted (default 0)", () => {
10
+ const { lines } = parseDiff(OLD, NEW);
11
+ const del = lines.find((l) => l.type === "del");
12
+ expect(del?.oldNum).toBe(2); // line2 is the 2nd line of the snippet
13
+ });
14
+
15
+ it("shifts gutter numbers to absolute when baseLine given", () => {
16
+ // Snippet begins at file line 84 → snippet line 2 becomes file line 85.
17
+ const { lines } = parseDiff(OLD, NEW, 3, 84);
18
+ const del = lines.find((l) => l.type === "del");
19
+ const add = lines.find((l) => l.type === "add");
20
+ expect(del?.oldNum).toBe(85);
21
+ expect(add?.newNum).toBe(85);
22
+ });
23
+ });
package/src/diff.ts CHANGED
@@ -22,6 +22,9 @@ export function parseDiff(
22
22
  oldContent: string,
23
23
  newContent: string,
24
24
  ctx = 3,
25
+ // 1-based file line where the snippet begins; shifts gutter numbers from
26
+ // snippet-relative to absolute. 0 = no shift (snippet-relative, the default).
27
+ baseLine = 0,
25
28
  ): ParsedDiff {
26
29
  const patch = Diff.structuredPatch("", "", oldContent, newContent, "", "", {
27
30
  context: ctx,
@@ -42,8 +45,9 @@ export function parseDiff(
42
45
  });
43
46
  }
44
47
  const h = patch.hunks[hi];
45
- let oL = h.oldStart;
46
- let nL = h.newStart;
48
+ const shift = baseLine > 0 ? baseLine - 1 : 0;
49
+ let oL = h.oldStart + shift;
50
+ let nL = h.newStart + shift;
47
51
  for (const raw of h.lines) {
48
52
  if (raw === "\") continue;
49
53
  const ch = raw[0];
@@ -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
+ });
@@ -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"; // ponytail: timeout kept for back-compat; never emitted now
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
- /** @deprecated No-op — timeout removed. Dialog waits indefinitely for user input. */
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) => done(result);
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();