@gotgenes/pi-permission-system 26.2.0 → 26.2.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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [26.2.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v26.2.0...pi-permission-system-v26.2.1) (2026-08-17)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **pi-permission-system:** accept pasted text in the denial-reason field ([8b33c38](https://github.com/gotgenes/pi-packages/commit/8b33c38084689dbb356fa0a0b7069bc610140736)), closes [#760](https://github.com/gotgenes/pi-packages/issues/760)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
|
|
18
|
+
* **pi-permission-system:** document the delegated denial-reason editor ([e7329f5](https://github.com/gotgenes/pi-packages/commit/e7329f5e81fc66e5317e958c9928262aaf433173)), closes [#760](https://github.com/gotgenes/pi-packages/issues/760)
|
|
19
|
+
|
|
8
20
|
## [26.2.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v26.1.0...pi-permission-system-v26.2.0) (2026-08-17)
|
|
9
21
|
|
|
10
22
|
|
package/docs/configuration.md
CHANGED
|
@@ -135,6 +135,10 @@ It expands both the prompt itself — to the complete request, unbounded by `pro
|
|
|
135
135
|
It only toggles the display — it never resolves, commits, or arms the pending decision.
|
|
136
136
|
While you are typing a denial reason it is not intercepted, so a rebound printable key still reaches the reason editor.
|
|
137
137
|
|
|
138
|
+
The reason editor is Pi's own line editor, so it behaves like the chat input: pasting works, as do cursor movement, word and line deletion, the kill ring, and undo.
|
|
139
|
+
The reason is a single line — a pasted line break becomes a space, and a long reason scrolls sideways rather than growing the dialog.
|
|
140
|
+
`enter` submits it, and `esc` (or `Ctrl+C`) returns to the decision list without denying.
|
|
141
|
+
|
|
138
142
|
### What a prompt shows
|
|
139
143
|
|
|
140
144
|
The prompt renders one fact per line, with the requesting agent (and, for a forwarded subagent ask, its session), the tool, the gate surface, the matched rule, the decision-relevant value, and — for a wrapper such as `xargs` — the command that will actually run.
|
package/package.json
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bracketed-paste normalization for the inline permission dialog's reason field.
|
|
3
|
+
*
|
|
4
|
+
* A terminal in bracketed-paste mode wraps pasted text in these markers, and
|
|
5
|
+
* the TUI hands the wrapped chunk to the focused component in a single call.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const PASTE_START = "\u001b[200~";
|
|
9
|
+
const PASTE_END = "\u001b[201~";
|
|
10
|
+
const NEWLINE_RUN = /[\r\n]+/g;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Collapse newline runs inside a bracketed-paste chunk to single spaces.
|
|
14
|
+
*
|
|
15
|
+
* The framework line editor deletes newlines outright, which joins the words
|
|
16
|
+
* on either side of a line break; a reason pasted from a multi-line source
|
|
17
|
+
* should stay readable in the single-line field. The markers are preserved so
|
|
18
|
+
* the editor still recognizes the chunk as a paste, and anything that is not
|
|
19
|
+
* a complete paste chunk is returned unchanged.
|
|
20
|
+
*/
|
|
21
|
+
export function collapsePastedNewlines(data: string): string {
|
|
22
|
+
const start = data.indexOf(PASTE_START);
|
|
23
|
+
if (start === -1) {
|
|
24
|
+
return data;
|
|
25
|
+
}
|
|
26
|
+
const contentStart = start + PASTE_START.length;
|
|
27
|
+
const contentEnd = data.indexOf(PASTE_END, contentStart);
|
|
28
|
+
if (contentEnd === -1) {
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
31
|
+
const content = data
|
|
32
|
+
.slice(contentStart, contentEnd)
|
|
33
|
+
.replace(NEWLINE_RUN, " ");
|
|
34
|
+
return data.slice(0, contentStart) + content + data.slice(contentEnd);
|
|
35
|
+
}
|
|
@@ -3,7 +3,8 @@ import type {
|
|
|
3
3
|
ExtensionUIContext,
|
|
4
4
|
KeybindingsManager,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { type Component, matchesKey } from "@earendil-works/pi-tui";
|
|
6
|
+
import { type Component, Input, matchesKey } from "@earendil-works/pi-tui";
|
|
7
|
+
import { collapsePastedNewlines } from "#src/authority/bracketed-paste";
|
|
7
8
|
import type {
|
|
8
9
|
DecisionSource,
|
|
9
10
|
UserDecisionSurface,
|
|
@@ -188,7 +189,8 @@ function handleToolsExpandAction(
|
|
|
188
189
|
|
|
189
190
|
class PermissionPromptComponent implements Component {
|
|
190
191
|
private state: PromptViewState;
|
|
191
|
-
|
|
192
|
+
/** The denial-reason line editor, rebuilt each time the step is entered. */
|
|
193
|
+
private reason: Input;
|
|
192
194
|
/** Whether the operator asked to see the complete request (ADR 0011 §4). */
|
|
193
195
|
private expanded = false;
|
|
194
196
|
|
|
@@ -203,6 +205,28 @@ class PermissionPromptComponent implements Component {
|
|
|
203
205
|
private readonly done: (decision: UnattributedDecision) => void,
|
|
204
206
|
) {
|
|
205
207
|
this.state = initialPromptState(config);
|
|
208
|
+
this.reason = this.createReasonEditor();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* A fresh editor per visit to the reason step.
|
|
213
|
+
*
|
|
214
|
+
* The framework editor carries an undo stack and a kill ring, so reusing one
|
|
215
|
+
* instance would let a reason the operator backed out of be restored into a
|
|
216
|
+
* later ask.
|
|
217
|
+
*/
|
|
218
|
+
private createReasonEditor(): Input {
|
|
219
|
+
const editor = new Input();
|
|
220
|
+
// Emits pi-tui's zero-width cursor marker, which positions the hardware
|
|
221
|
+
// cursor for IME composition.
|
|
222
|
+
editor.focused = true;
|
|
223
|
+
editor.onSubmit = (draft) => {
|
|
224
|
+
this.apply({ type: "submitReason", draft });
|
|
225
|
+
};
|
|
226
|
+
editor.onEscape = () => {
|
|
227
|
+
this.apply({ type: "cancel" });
|
|
228
|
+
};
|
|
229
|
+
return editor;
|
|
206
230
|
}
|
|
207
231
|
|
|
208
232
|
invalidate(): void {
|
|
@@ -278,25 +302,18 @@ class PermissionPromptComponent implements Component {
|
|
|
278
302
|
}
|
|
279
303
|
}
|
|
280
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Hand the keystroke to the framework line editor.
|
|
307
|
+
*
|
|
308
|
+
* Delegating is what makes the field accept a paste: a paste arrives as one
|
|
309
|
+
* multi-character chunk wrapped in bracketed-paste markers, which the editor
|
|
310
|
+
* understands and a per-character reader cannot. Submit and cancel come back
|
|
311
|
+
* through the editor's callbacks, so the decision model still owns them.
|
|
312
|
+
*/
|
|
281
313
|
private handleReasonInput(data: string): void {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
if (matchesKey(data, "escape")) {
|
|
287
|
-
this.reasonBuffer = "";
|
|
288
|
-
this.apply({ type: "cancel" });
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
if (matchesKey(data, "backspace")) {
|
|
292
|
-
this.reasonBuffer = this.reasonBuffer.slice(0, -1);
|
|
293
|
-
this.requestRender();
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
if (isPrintable(data)) {
|
|
297
|
-
this.reasonBuffer += data;
|
|
298
|
-
this.requestRender();
|
|
299
|
-
}
|
|
314
|
+
this.reason.handleInput(collapsePastedNewlines(data));
|
|
315
|
+
// The editor mutates its own buffer silently; only the dialog can repaint.
|
|
316
|
+
this.requestRender();
|
|
300
317
|
}
|
|
301
318
|
|
|
302
319
|
private toEvent(data: string): PromptEvent | undefined {
|
|
@@ -328,7 +345,7 @@ class PermissionPromptComponent implements Component {
|
|
|
328
345
|
return;
|
|
329
346
|
}
|
|
330
347
|
if (outcome.state.step === "reason" && this.state.step !== "reason") {
|
|
331
|
-
this.
|
|
348
|
+
this.reason = this.createReasonEditor();
|
|
332
349
|
}
|
|
333
350
|
this.state = outcome.state;
|
|
334
351
|
this.requestRender();
|
|
@@ -354,7 +371,9 @@ class PermissionPromptComponent implements Component {
|
|
|
354
371
|
this.theme.fg("accent", this.title),
|
|
355
372
|
...this.renderAsk(width).lines,
|
|
356
373
|
"",
|
|
357
|
-
|
|
374
|
+
"Reason (required):",
|
|
375
|
+
// Exactly one row, whatever its length: the editor scrolls horizontally.
|
|
376
|
+
...this.reason.render(width),
|
|
358
377
|
];
|
|
359
378
|
if (this.state.reasonError) {
|
|
360
379
|
lines.push(this.theme.fg("error", this.state.reasonError));
|
|
@@ -388,11 +407,3 @@ class PermissionPromptComponent implements Component {
|
|
|
388
407
|
return lines;
|
|
389
408
|
}
|
|
390
409
|
}
|
|
391
|
-
|
|
392
|
-
function isPrintable(data: string): boolean {
|
|
393
|
-
if (data.length !== 1) {
|
|
394
|
-
return false;
|
|
395
|
-
}
|
|
396
|
-
const code = data.charCodeAt(0);
|
|
397
|
-
return code >= 0x20 && code !== 0x7f;
|
|
398
|
-
}
|
|
@@ -52,7 +52,6 @@ export interface PromptViewState {
|
|
|
52
52
|
armedKey?: PromptKey;
|
|
53
53
|
/** "Press y again to approve." while armed; empty otherwise. */
|
|
54
54
|
hint: string;
|
|
55
|
-
reasonDraft: string;
|
|
56
55
|
/** Set when an empty reason submit is rejected. */
|
|
57
56
|
reasonError?: string;
|
|
58
57
|
/** Scope step: false = subagent-only (default), true = whole serving session. */
|
|
@@ -80,7 +79,6 @@ export function initialPromptState(
|
|
|
80
79
|
highlightedKey: "y",
|
|
81
80
|
armedKey: undefined,
|
|
82
81
|
hint: "",
|
|
83
|
-
reasonDraft: "",
|
|
84
82
|
reasonError: undefined,
|
|
85
83
|
scopeServing: false,
|
|
86
84
|
};
|
|
@@ -169,7 +167,6 @@ function commit(
|
|
|
169
167
|
highlightedKey: "r",
|
|
170
168
|
armedKey: undefined,
|
|
171
169
|
hint: "",
|
|
172
|
-
reasonDraft: "",
|
|
173
170
|
reasonError: undefined,
|
|
174
171
|
});
|
|
175
172
|
case "s":
|
|
@@ -200,7 +197,6 @@ function reduceReasonStep(
|
|
|
200
197
|
step: "decision",
|
|
201
198
|
armedKey: undefined,
|
|
202
199
|
hint: "",
|
|
203
|
-
reasonDraft: "",
|
|
204
200
|
reasonError: undefined,
|
|
205
201
|
});
|
|
206
202
|
}
|
|
@@ -209,7 +205,6 @@ function reduceReasonStep(
|
|
|
209
205
|
if (reason === undefined) {
|
|
210
206
|
return render({
|
|
211
207
|
...state,
|
|
212
|
-
reasonDraft: event.draft,
|
|
213
208
|
reasonError: "A reason is required.",
|
|
214
209
|
});
|
|
215
210
|
}
|