@zeldrisho/pi-gate 0.7.2 → 0.7.3
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/CHANGELOG.md +8 -1
- package/README.md +1 -3
- package/package.json +1 -1
- package/src/approval-prompt.ts +46 -7
- package/src/index.ts +10 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.3] - 2026-10-28
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Show the approval timeout and configured confirmation keys, and allow horizontal scrolling of long command lines.
|
|
15
|
+
|
|
10
16
|
## [0.7.2] - 2026-10-25
|
|
11
17
|
|
|
12
18
|
### Changed
|
|
@@ -112,7 +118,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
112
118
|
- support `prompt`, `block`, and `allow` actions, with longest-pattern-wins resolution
|
|
113
119
|
- block rather than auto-approve in non-UI modes
|
|
114
120
|
|
|
115
|
-
[Unreleased]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.7.
|
|
121
|
+
[Unreleased]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.7.3...HEAD
|
|
122
|
+
[0.7.3]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.7.2...pi-gate-v0.7.3
|
|
116
123
|
[0.7.2]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.7.1...pi-gate-v0.7.2
|
|
117
124
|
[0.7.1]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.7.0...pi-gate-v0.7.1
|
|
118
125
|
[0.7.0]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.6.1...pi-gate-v0.7.0
|
package/README.md
CHANGED
|
@@ -46,9 +46,7 @@ pi-gate is a guard rail, not a sandbox. Complex shell syntax, pipelines, redirec
|
|
|
46
46
|
|
|
47
47
|
The built-in `bash` tool is gated, and `write`/`edit` calls targeting the gate config require approval. Prompt dialogs offer `Allow` and `Deny`; approvals are never remembered. To stop repeated prompts, add a specific `allow` rule to `gate.json`.
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
The matched command text is highlighted in the TUI approval dialog. Long commands are truncated in approval dialogs, but an approved command executes in full. Denials, policy blocks, dismissals, and timeouts stop the turn; the agent is told to ask before retrying a denial or proceeding past a block, while a timeout asks whether to retry approval. Other allowed calls in a parallel batch may still run. In non-interactive modes (`-p`, JSON), commands requiring approval are denied.
|
|
49
|
+
The approval dialog highlights the matched command text. Long commands are truncated in the dialog but execute in full if approved. Denials, blocks, dismissals, and timeouts stop the turn; other allowed calls in a parallel batch may still run. In non-interactive modes (`-p`, JSON), commands requiring approval are denied.
|
|
52
50
|
|
|
53
51
|
In Herdr-managed panes, install Herdr's Pi integration with `herdr integration install pi` to report pending approvals as `blocked`. Herdr is optional.
|
|
54
52
|
|
package/package.json
CHANGED
package/src/approval-prompt.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
import { keyHint, rawKeyHint } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import type { Component } from "@earendil-works/pi-tui";
|
|
2
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Key,
|
|
5
|
+
matchesKey,
|
|
6
|
+
sliceByColumn,
|
|
7
|
+
truncateToWidth,
|
|
8
|
+
visibleWidth,
|
|
9
|
+
wrapTextWithAnsi,
|
|
10
|
+
} from "@earendil-works/pi-tui";
|
|
3
11
|
|
|
4
12
|
const ACTIONS = ["Allow", "Deny"] as const;
|
|
5
13
|
|
|
@@ -31,8 +39,12 @@ export function createApprovalPrompt(
|
|
|
31
39
|
done: (result: Action | undefined) => void,
|
|
32
40
|
command: string,
|
|
33
41
|
rule: string,
|
|
42
|
+
deadline?: number,
|
|
34
43
|
): Component {
|
|
35
44
|
let selectedIndex = 0;
|
|
45
|
+
let horizontalOffset = 0;
|
|
46
|
+
let maxHorizontalOffset = 0;
|
|
47
|
+
let pageWidth = 1;
|
|
36
48
|
|
|
37
49
|
const invalidate = () => {};
|
|
38
50
|
|
|
@@ -55,11 +67,26 @@ export function createApprovalPrompt(
|
|
|
55
67
|
const component: Component = {
|
|
56
68
|
render(width: number): string[] {
|
|
57
69
|
const available = innerWidth(width);
|
|
70
|
+
|
|
71
|
+
const longestLine = Math.max(
|
|
72
|
+
0,
|
|
73
|
+
...command.split("\n").map((line) => visibleWidth(line.replace(/[»«]/gu, ""))),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
pageWidth = longestLine > available ? Math.max(1, available - 2) : available;
|
|
77
|
+
maxHorizontalOffset = longestLine > available ? longestLine - pageWidth : 0;
|
|
78
|
+
horizontalOffset = Math.min(horizontalOffset, maxHorizontalOffset);
|
|
58
79
|
const border = theme.style("─".repeat(available + 4), { fg: "border" });
|
|
59
80
|
|
|
60
81
|
const lines = [
|
|
61
82
|
border,
|
|
62
|
-
panelLine(
|
|
83
|
+
panelLine(
|
|
84
|
+
theme.bold(
|
|
85
|
+
`${deadline === undefined ? "" : `(${Math.max(0, Math.ceil((deadline - Date.now()) / 1_000))}s) `}pi-gate: allow this command?`,
|
|
86
|
+
),
|
|
87
|
+
width,
|
|
88
|
+
"accent",
|
|
89
|
+
),
|
|
63
90
|
panelLine("Review the requested command or file change:", width, "muted"),
|
|
64
91
|
panelLine("", width),
|
|
65
92
|
...command.split("\n").map((line) => {
|
|
@@ -86,10 +113,15 @@ export function createApprovalPrompt(
|
|
|
86
113
|
.join("");
|
|
87
114
|
|
|
88
115
|
const visible = line.replace(/[»«]/gu, "");
|
|
89
|
-
const content =
|
|
90
|
-
const
|
|
116
|
+
const content = sliceByColumn(rendered, horizontalOffset, pageWidth, true);
|
|
117
|
+
const prefix = horizontalOffset > 0 && visibleWidth(visible) > 0 ? "…" : "";
|
|
118
|
+
const suffix = visibleWidth(visible) > horizontalOffset + pageWidth ? "…" : "";
|
|
119
|
+
|
|
120
|
+
const padding = " ".repeat(
|
|
121
|
+
Math.max(0, contentWidth - visibleWidth(prefix + content + suffix)),
|
|
122
|
+
);
|
|
91
123
|
|
|
92
|
-
return ` ${
|
|
124
|
+
return ` ${theme.style(prefix, { fg: "toolOutput" })}${content}${theme.style(suffix + padding, { fg: "toolOutput" })} `;
|
|
93
125
|
}),
|
|
94
126
|
panelLine("", width),
|
|
95
127
|
panelLine(`Matched rule: ${rule}`, width, "muted"),
|
|
@@ -105,14 +137,21 @@ export function createApprovalPrompt(
|
|
|
105
137
|
});
|
|
106
138
|
}),
|
|
107
139
|
panelLine("", width),
|
|
108
|
-
|
|
140
|
+
...wrapTextWithAnsi(
|
|
141
|
+
`${rawKeyHint("↑↓", "navigate")} ${maxHorizontalOffset > 0 ? `${rawKeyHint("←→", "scroll command")} ` : ""}${keyHint("tui.select.confirm", "select")} ${keyHint("tui.select.cancel", "cancel")}`,
|
|
142
|
+
available,
|
|
143
|
+
).map((line) => ` ${line} `),
|
|
109
144
|
border,
|
|
110
145
|
];
|
|
111
146
|
|
|
112
147
|
return lines.map((line) => truncateToWidth(line, width));
|
|
113
148
|
},
|
|
114
149
|
handleInput(data: string): void {
|
|
115
|
-
if (
|
|
150
|
+
if (matchesKey(data, Key.left)) {
|
|
151
|
+
horizontalOffset = Math.max(0, horizontalOffset - pageWidth);
|
|
152
|
+
} else if (matchesKey(data, Key.right)) {
|
|
153
|
+
horizontalOffset = Math.min(maxHorizontalOffset, horizontalOffset + pageWidth);
|
|
154
|
+
} else if (keybindings.matches(data, "tui.select.up") || data === "k") {
|
|
116
155
|
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
117
156
|
} else if (keybindings.matches(data, "tui.select.down") || data === "j") {
|
|
118
157
|
selectedIndex = Math.min(ACTIONS.length - 1, selectedIndex + 1);
|
package/src/index.ts
CHANGED
|
@@ -66,6 +66,7 @@ async function selectApproval(
|
|
|
66
66
|
let uiDone: ((value: ApprovalAction | undefined) => void) | undefined;
|
|
67
67
|
let settled = false;
|
|
68
68
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
69
|
+
let countdown: ReturnType<typeof setInterval> | undefined;
|
|
69
70
|
let removeAbortListener: (() => void) | undefined;
|
|
70
71
|
|
|
71
72
|
return new Promise<ApprovalAction | undefined>((resolve, reject) => {
|
|
@@ -74,6 +75,8 @@ async function selectApproval(
|
|
|
74
75
|
settled = true;
|
|
75
76
|
|
|
76
77
|
if (timer !== undefined) clearTimeout(timer);
|
|
78
|
+
|
|
79
|
+
if (countdown !== undefined) clearInterval(countdown);
|
|
77
80
|
removeAbortListener?.();
|
|
78
81
|
resolve(value);
|
|
79
82
|
uiDone?.(value);
|
|
@@ -82,13 +85,15 @@ async function selectApproval(
|
|
|
82
85
|
const onAbort = () => finish(undefined);
|
|
83
86
|
|
|
84
87
|
try {
|
|
88
|
+
const deadline = Date.now() + timeoutMs;
|
|
85
89
|
timer = setTimeout(() => finish(undefined), timeoutMs);
|
|
86
90
|
|
|
87
91
|
const uiPromise = ctx.ui.custom<ApprovalAction | undefined>(
|
|
88
92
|
(tui, theme, keybindings, done) => {
|
|
89
93
|
uiDone = done;
|
|
94
|
+
countdown = setInterval(() => tui.requestRender(), 1_000);
|
|
90
95
|
|
|
91
|
-
return createApprovalPrompt(tui, theme, keybindings, done, command, rule);
|
|
96
|
+
return createApprovalPrompt(tui, theme, keybindings, done, command, rule, deadline);
|
|
92
97
|
},
|
|
93
98
|
);
|
|
94
99
|
|
|
@@ -97,6 +102,8 @@ async function selectApproval(
|
|
|
97
102
|
.catch(reject)
|
|
98
103
|
.finally(() => {
|
|
99
104
|
if (timer !== undefined) clearTimeout(timer);
|
|
105
|
+
|
|
106
|
+
if (countdown !== undefined) clearInterval(countdown);
|
|
100
107
|
removeAbortListener?.();
|
|
101
108
|
});
|
|
102
109
|
|
|
@@ -110,6 +117,8 @@ async function selectApproval(
|
|
|
110
117
|
settled = true;
|
|
111
118
|
|
|
112
119
|
if (timer !== undefined) clearTimeout(timer);
|
|
120
|
+
|
|
121
|
+
if (countdown !== undefined) clearInterval(countdown);
|
|
113
122
|
removeAbortListener?.();
|
|
114
123
|
reject(error);
|
|
115
124
|
}
|