Skip to content

Commit 7f62107

Browse files
authored
feat: add timed confirmation dialog with auto-resolve (#7)
* feat: add timed confirmation dialog with auto-resolve * docs: update changelog
1 parent c41f053 commit 7f62107

5 files changed

Lines changed: 468 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ All notable changes to agent-stuff are documented here.
1010

1111

1212

13-
## chore/eslint-typescript-config
13+
14+
15+
## feat/timed-confirm-dialog
16+
17+
Introduces a reusable timed confirmation dialog component (#7) that displays a bordered prompt with an auto-resolving countdown timer, allowing users to confirm immediately via Enter or cancel with Escape. The dialog has been integrated into the PR merge workflow, replacing the standard confirmation prompt to streamline the merge process with a 5-second auto-confirm default. Includes comprehensive test coverage (336 lines) validating timer behavior, keyboard input handling, and configuration options, ensuring robust interaction across various scenarios.
18+
19+
## [1.0.0](https://github.com/kostyay/agent-stuff/pull/6) - 2026-03-02
1420

1521
Added ESLint configuration with TypeScript support (#6) to enforce syntax and error detection across the codebase. The setup leverages typescript-eslint with recommended rules while disabling noisy stylistic checks to align with the project's existing conventions. ESLint dependencies (eslint ^10.0.2 and typescript-eslint ^8.56.1) have been added to the dev stack. TypeScript files in pi-extensions were linted and adjusted to comply with the new configuration, focusing on catching genuine bugs rather than enforcing code style preferences.
1622

lib/timed-confirm.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/**
2+
* Timed Confirm — a reusable confirmation dialog with a countdown timer.
3+
*
4+
* Shows a bordered confirmation prompt that auto-confirms after a
5+
* configurable number of seconds. The user can press Enter to confirm
6+
* immediately or Escape to cancel.
7+
*/
8+
9+
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
10+
import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
11+
import { Container, Key, Text, matchesKey } from "@mariozechner/pi-tui";
12+
13+
// ---------------------------------------------------------------------------
14+
// Options
15+
// ---------------------------------------------------------------------------
16+
17+
/** Configuration for the timed confirmation dialog. */
18+
export interface TimedConfirmOptions {
19+
/** Dialog title. */
20+
title: string;
21+
/** Descriptive message shown below the title. */
22+
message: string;
23+
/** Countdown duration in seconds. Defaults to 5. */
24+
seconds?: number;
25+
/** Value returned when the timer expires. Defaults to `true`. */
26+
defaultValue?: boolean;
27+
}
28+
29+
// ---------------------------------------------------------------------------
30+
// Public API
31+
// ---------------------------------------------------------------------------
32+
33+
/** Default countdown duration in seconds. */
34+
const DEFAULT_SECONDS = 5;
35+
36+
/**
37+
* Show a confirmation dialog with a countdown timer.
38+
*
39+
* Auto-resolves with `defaultValue` (default `true`) when the timer expires.
40+
* The user can press Enter to confirm or Escape to cancel at any time.
41+
*
42+
* @example
43+
* ```ts
44+
* const ok = await timedConfirm(ctx, {
45+
* title: "Merge PR",
46+
* message: `Merge PR #${pr.number} into main?`,
47+
* seconds: 5,
48+
* });
49+
* ```
50+
*/
51+
export async function timedConfirm(
52+
ctx: ExtensionCommandContext,
53+
options: TimedConfirmOptions,
54+
): Promise<boolean> {
55+
const { title, message, seconds = DEFAULT_SECONDS, defaultValue = true } = options;
56+
57+
return ctx.ui.custom<boolean>((tui, theme, _kb, done) => {
58+
let remaining = seconds;
59+
let resolved = false;
60+
61+
const finish = (value: boolean) => {
62+
if (resolved) return;
63+
resolved = true;
64+
clearInterval(timer);
65+
done(value);
66+
};
67+
68+
const timer = setInterval(() => {
69+
remaining--;
70+
if (remaining <= 0) {
71+
finish(defaultValue);
72+
} else {
73+
tui.requestRender();
74+
}
75+
}, 1000);
76+
77+
const container = new Container();
78+
const borderTop = new DynamicBorder((s: string) => theme.fg("accent", s));
79+
const titleText = new Text("", 1, 0);
80+
const messageText = new Text("", 1, 0);
81+
const helpText = new Text("", 1, 0);
82+
const borderBottom = new DynamicBorder((s: string) => theme.fg("accent", s));
83+
84+
container.addChild(borderTop);
85+
container.addChild(titleText);
86+
container.addChild(messageText);
87+
container.addChild(helpText);
88+
container.addChild(borderBottom);
89+
90+
const defaultLabel = defaultValue ? "confirm" : "cancel";
91+
92+
const updateTexts = () => {
93+
titleText.setText(theme.fg("accent", theme.bold(title)));
94+
messageText.setText(message);
95+
helpText.setText(
96+
theme.fg("dim", `Auto-${defaultLabel} in ${remaining}s - enter confirm - esc cancel`),
97+
);
98+
};
99+
updateTexts();
100+
101+
return {
102+
render: (w: number) => {
103+
updateTexts();
104+
return container.render(w);
105+
},
106+
invalidate: () => container.invalidate(),
107+
handleInput: (data: string) => {
108+
if (matchesKey(data, Key.enter)) {
109+
finish(true);
110+
} else if (matchesKey(data, Key.escape)) {
111+
finish(false);
112+
}
113+
},
114+
};
115+
});
116+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"readme": "README.md",
1010
"type": "module",
1111
"scripts": {
12-
"lint": "eslint pi-extensions/ tests/",
12+
"lint": "eslint --fix pi-extensions/ tests/",
1313
"test": "npm run lint && node --experimental-strip-types --test tests/*.test.ts"
1414
},
1515
"keywords": [

pi-extensions/commit.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-cod
1818
import { readFileSync, writeFileSync } from "node:fs";
1919
import { resolve } from "node:path";
2020

21+
import { timedConfirm } from "../lib/timed-confirm.ts";
2122
import {
2223
type ChangelogContext,
2324
buildChangelogPrompt,
@@ -858,6 +859,8 @@ async function performRebase(
858859
return false;
859860
}
860861

862+
// ---------------------------------------------------------------------------
863+
// Extension entry point
861864
// ---------------------------------------------------------------------------
862865
// Extension entry point
863866
// ---------------------------------------------------------------------------
@@ -1011,11 +1014,11 @@ export default function commitExtension(pi: ExtensionAPI) {
10111014
if (!(await performRebase(pi, ctx, defaultBranch))) return;
10121015
}
10131016

1014-
// Step 6: Confirm with user
1015-
const confirmed = await ctx.ui.confirm(
1016-
"Merge PR",
1017-
`Merge PR #${pr.number} into ${defaultBranch}?`,
1018-
);
1017+
// Step 6: Confirm with user (auto-confirms after 5s)
1018+
const confirmed = await timedConfirm(ctx, {
1019+
title: "Merge PR",
1020+
message: `Merge PR #${pr.number} into ${defaultBranch}?`,
1021+
});
10191022
if (!confirmed) {
10201023
ctx.ui.notify("Merge cancelled", "info");
10211024
return;

0 commit comments

Comments
 (0)