@gotgenes/pi-permission-system 36.1.0 → 36.2.0
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 +11 -0
- package/docs/configuration.md +11 -1
- package/package.json +1 -1
- package/schemas/permissions.schema.json +1 -1
- package/src/authority/authorizer.ts +4 -0
- package/src/authority/local-user-authorizer.ts +15 -3
- package/src/authority/permission-prompt-component.ts +7 -2
- package/src/config/config-schema.ts +1 -1
- package/src/presentation/prompt-notification.ts +71 -10
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ 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
|
+
## [36.2.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v36.1.0...pi-permission-system-v36.2.0) (2026-09-30)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* name the session and the requested tool in prompt notifications ([#996](https://github.com/gotgenes/pi-packages/issues/996)) ([eb49c64](https://github.com/gotgenes/pi-packages/commit/eb49c6494d12749f71130d74b75c578530c89afa)), closes [#996](https://github.com/gotgenes/pi-packages/issues/996)
|
|
14
|
+
|
|
15
|
+
### Documentation
|
|
16
|
+
|
|
17
|
+
* describe the session and tool in prompt notifications ([#996](https://github.com/gotgenes/pi-packages/issues/996)) ([c5c74db](https://github.com/gotgenes/pi-packages/commit/c5c74db43235297a39bbe94303225ac50baf0ca4))
|
|
18
|
+
|
|
8
19
|
## [36.1.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v36.0.0...pi-permission-system-v36.1.0) (2026-09-30)
|
|
9
20
|
|
|
10
21
|
|
package/docs/configuration.md
CHANGED
|
@@ -208,7 +208,17 @@ The two OSC forms raise a desktop notification in terminals that implement them;
|
|
|
208
208
|
|
|
209
209
|
Channels are written in the order listed.
|
|
210
210
|
The list is empty by default, so nothing is emitted until you set it; an unknown channel name is a validation error like any other malformed field.
|
|
211
|
-
The notification
|
|
211
|
+
The notification names the session and what is being asked, so it tells you which pane to go to.
|
|
212
|
+
Its title is `pi — <session name>`, or `pi — <directory name>` for a session without a name.
|
|
213
|
+
Its body is the dialog title followed by the tool (or, for an ask that is not a tool call, the gate surface) and the requesting agent when there is one:
|
|
214
|
+
|
|
215
|
+
| Ask | `osc777` title | `osc777` body |
|
|
216
|
+
| ------------------------------------------- | -------------------- | ---------------------------------------------- |
|
|
217
|
+
| local `bash` ask in session `refactor-auth` | `pi — refactor-auth` | `Permission Required: bash` |
|
|
218
|
+
| `read` forwarded from subagent `scout` | `pi — refactor-auth` | `Permission Required (Subagent): read (scout)` |
|
|
219
|
+
|
|
220
|
+
OSC 9 has a single text field, so `osc9` writes the title ahead of the body: `pi — refactor-auth: Permission Required: bash`.
|
|
221
|
+
The notification carries labels only, never the command, path, MCP target, or skill being decided, so nothing the request holds lands in your notification history.
|
|
212
222
|
|
|
213
223
|
The signal fires once per prompt, when the prompt is actually shown.
|
|
214
224
|
A second ask that waits behind an open dialog signals when its own turn comes, and a forwarded subagent ask signals in the parent session that shows it.
|
package/package.json
CHANGED
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
},
|
|
71
71
|
"promptNotifications": {
|
|
72
72
|
"description": "Terminal notifications to emit when the inline permission dialog opens: bell (BEL), osc9 (OSC 9 notification), osc777 (OSC 777 notification). Omit or leave empty for none. TUI sessions only.",
|
|
73
|
-
"markdownDescription": "Terminal notifications to emit when the inline permission dialog opens, so a terminal or multiplexer can flag a session waiting on a decision.\n\n- `\"bell\"` writes a BEL — what tmux `monitor-bell` and most terminals react to.\n- `\"osc9\"` writes an OSC 9 desktop notification.\n- `\"osc777\"` writes an OSC 777 desktop notification.\n\nOmit or leave empty for none (the default). Applies to interactive **TUI** sessions only
|
|
73
|
+
"markdownDescription": "Terminal notifications to emit when the inline permission dialog opens, so a terminal or multiplexer can flag a session waiting on a decision.\n\n- `\"bell\"` writes a BEL — what tmux `monitor-bell` and most terminals react to.\n- `\"osc9\"` writes an OSC 9 desktop notification.\n- `\"osc777\"` writes an OSC 777 desktop notification.\n\nOmit or leave empty for none (the default). Applies to interactive **TUI** sessions only. The notification names the session and the requested tool and agent, never the command, path, or other value being decided.",
|
|
74
74
|
"default": [],
|
|
75
75
|
"type": "array",
|
|
76
76
|
"items": {
|
|
@@ -158,6 +158,10 @@ export function selectAuthorizer(
|
|
|
158
158
|
dialogs: deps.dialogs,
|
|
159
159
|
getPromptPreferences: deps.getPromptPreferences,
|
|
160
160
|
requestPermissionDecision: deps.requestPermissionDecision,
|
|
161
|
+
describeSession: () => ({
|
|
162
|
+
name: ctx.sessionManager.getSessionName(),
|
|
163
|
+
cwd: ctx.cwd,
|
|
164
|
+
}),
|
|
161
165
|
}),
|
|
162
166
|
adjudicatesLocally: true,
|
|
163
167
|
};
|
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
buildForwardedScopeLabels,
|
|
5
5
|
describeGrantTarget,
|
|
6
6
|
} from "#src/presentation/pattern-suggest";
|
|
7
|
+
import {
|
|
8
|
+
describePromptNotice,
|
|
9
|
+
type NotificationSession,
|
|
10
|
+
} from "#src/presentation/prompt-notification";
|
|
7
11
|
import {
|
|
8
12
|
emitUiPromptEvent,
|
|
9
13
|
type PermissionEventBus,
|
|
@@ -37,6 +41,8 @@ export interface LocalUserAuthorizerDeps {
|
|
|
37
41
|
getPromptPreferences: () => PromptPreferences;
|
|
38
42
|
/** Injected for testability; production callers pass the real function. */
|
|
39
43
|
requestPermissionDecision: typeof requestPermissionDecision;
|
|
44
|
+
/** Read when each prompt opens, so a session named mid-session is picked up. */
|
|
45
|
+
describeSession: () => NotificationSession;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
/**
|
|
@@ -77,15 +83,21 @@ export class LocalUserAuthorizer implements TerminalAuthorizer {
|
|
|
77
83
|
details: PromptPermissionDetails,
|
|
78
84
|
): Promise<PermissionPromptDecision> {
|
|
79
85
|
emitUiPromptEvent(this.deps.events, buildUiPrompt(details));
|
|
86
|
+
const title = details.forwarding
|
|
87
|
+
? "Permission Required (Subagent)"
|
|
88
|
+
: "Permission Required";
|
|
80
89
|
return this.deps.requestPermissionDecision(
|
|
81
90
|
{
|
|
82
91
|
mode: this.deps.mode,
|
|
83
92
|
ui: this.deps.ui,
|
|
84
93
|
...this.deps.getPromptPreferences(),
|
|
94
|
+
notice: describePromptNotice(
|
|
95
|
+
title,
|
|
96
|
+
details.payload.request,
|
|
97
|
+
this.deps.describeSession(),
|
|
98
|
+
),
|
|
85
99
|
},
|
|
86
|
-
|
|
87
|
-
? "Permission Required (Subagent)"
|
|
88
|
-
: "Permission Required",
|
|
100
|
+
title,
|
|
89
101
|
details.payload,
|
|
90
102
|
buildRequestOptions(details),
|
|
91
103
|
);
|
|
@@ -18,7 +18,10 @@ import {
|
|
|
18
18
|
renderPromptDialog,
|
|
19
19
|
} from "#src/presentation/dialog-renderer";
|
|
20
20
|
import { fitLinesToWidth } from "#src/presentation/line-fitting";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
type PromptNotice,
|
|
23
|
+
renderPromptNotification,
|
|
24
|
+
} from "#src/presentation/prompt-notification";
|
|
22
25
|
import type { PromptPayload } from "#src/presentation/prompt-payload";
|
|
23
26
|
import { collapsePastedNewlines } from "./bracketed-paste";
|
|
24
27
|
import type { DecisionSource, UserDecisionSurface } from "./decision-source";
|
|
@@ -60,6 +63,8 @@ type PromptKeybindings = Pick<KeybindingsManager, "matches">;
|
|
|
60
63
|
export interface PermissionPromptView extends PromptPreferences {
|
|
61
64
|
mode: ExtensionContext["mode"];
|
|
62
65
|
ui: PermissionPromptUi;
|
|
66
|
+
/** What the inline dialog's terminal notification says; the fallback ignores it. */
|
|
67
|
+
notice: PromptNotice;
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
/** Live prompt-behavior preferences read at prompt time (see `doublePressToConfirm`). */
|
|
@@ -160,7 +165,7 @@ export function presentInlinePermissionPrompt(
|
|
|
160
165
|
// human is being asked; a re-render does not come back through here.
|
|
161
166
|
const notification = renderPromptNotification(
|
|
162
167
|
view.promptNotifications,
|
|
163
|
-
|
|
168
|
+
view.notice,
|
|
164
169
|
);
|
|
165
170
|
if (notification) {
|
|
166
171
|
tui.terminal.write(notification);
|
|
@@ -378,7 +378,7 @@ export const unifiedConfigSchema = z
|
|
|
378
378
|
description:
|
|
379
379
|
"Terminal notifications to emit when the inline permission dialog opens: bell (BEL), osc9 (OSC 9 notification), osc777 (OSC 777 notification). Omit or leave empty for none. TUI sessions only.",
|
|
380
380
|
markdownDescription:
|
|
381
|
-
'Terminal notifications to emit when the inline permission dialog opens, so a terminal or multiplexer can flag a session waiting on a decision.\n\n- `"bell"` writes a BEL — what tmux `monitor-bell` and most terminals react to.\n- `"osc9"` writes an OSC 9 desktop notification.\n- `"osc777"` writes an OSC 777 desktop notification.\n\nOmit or leave empty for none (the default). Applies to interactive **TUI** sessions only
|
|
381
|
+
'Terminal notifications to emit when the inline permission dialog opens, so a terminal or multiplexer can flag a session waiting on a decision.\n\n- `"bell"` writes a BEL — what tmux `monitor-bell` and most terminals react to.\n- `"osc9"` writes an OSC 9 desktop notification.\n- `"osc777"` writes an OSC 777 desktop notification.\n\nOmit or leave empty for none (the default). Applies to interactive **TUI** sessions only. The notification names the session and the requested tool and agent, never the command, path, or other value being decided.',
|
|
382
382
|
default: [],
|
|
383
383
|
}),
|
|
384
384
|
forwardingTimeoutMs: z.number().int().min(1).optional().meta({
|
|
@@ -1,41 +1,102 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
1
2
|
import type { PromptNotificationChannel } from "#src/config/config-schema";
|
|
3
|
+
import type { PromptRequestFacts } from "./prompt-payload";
|
|
2
4
|
|
|
3
5
|
const BEL = "\x07";
|
|
4
6
|
const OSC = "\x1b]";
|
|
5
7
|
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
+
/** What a prompt notification says: OSC 777 carries both fields. */
|
|
9
|
+
export interface PromptNotice {
|
|
10
|
+
readonly title: string;
|
|
11
|
+
readonly body: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The session facts a notice names, read when the prompt opens. */
|
|
15
|
+
export interface NotificationSession {
|
|
16
|
+
readonly name: string | undefined;
|
|
17
|
+
readonly cwd: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The request facts a notice may name: labels only, never the value. */
|
|
21
|
+
export type NoticeRequestFacts = Pick<
|
|
22
|
+
PromptRequestFacts,
|
|
23
|
+
"toolName" | "surface" | "requester"
|
|
24
|
+
>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* What a prompt notification says: which session is waiting, and what is
|
|
28
|
+
* being asked and by whom.
|
|
29
|
+
*
|
|
30
|
+
* Labels only. The request's value (the command, path, MCP target, or skill)
|
|
31
|
+
* never appears: the OS notification history is a store this package does not
|
|
32
|
+
* control, outside the owner-only logs and their key-name masking.
|
|
33
|
+
*/
|
|
34
|
+
export function describePromptNotice(
|
|
35
|
+
dialogTitle: string,
|
|
36
|
+
request: NoticeRequestFacts,
|
|
37
|
+
session: NotificationSession,
|
|
38
|
+
): PromptNotice {
|
|
39
|
+
const label = sessionLabel(session);
|
|
40
|
+
const subject = askSubject(request);
|
|
41
|
+
return {
|
|
42
|
+
title: label ? `pi — ${label}` : "pi",
|
|
43
|
+
body: subject ? `${dialogTitle}: ${subject}` : dialogTitle,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The session's name, or the name of the directory it runs in, as Pi's own terminal title does. */
|
|
48
|
+
function sessionLabel(session: NotificationSession): string {
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- || intentional: an empty name is unnamed
|
|
50
|
+
return session.name || basename(session.cwd);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** What is asked (the tool, else the gate surface) and by which agent. */
|
|
54
|
+
function askSubject(request: NoticeRequestFacts): string {
|
|
55
|
+
const what = request.toolName ?? request.surface;
|
|
56
|
+
const who = request.requester.agentName;
|
|
57
|
+
if (what && who) return `${what} (${who})`;
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- || intentional: an empty label names nothing
|
|
59
|
+
return what || who || "";
|
|
60
|
+
}
|
|
8
61
|
|
|
9
62
|
/**
|
|
10
63
|
* The bytes that ask a terminal for attention as a permission dialog opens.
|
|
11
64
|
*
|
|
12
65
|
* One sequence per configured channel, in configured order; an empty list
|
|
13
|
-
* renders nothing.
|
|
66
|
+
* renders nothing. Every field is stripped of control characters, so it cannot
|
|
14
67
|
* end a sequence early or start another one.
|
|
15
68
|
*/
|
|
16
69
|
export function renderPromptNotification(
|
|
17
70
|
channels: readonly PromptNotificationChannel[],
|
|
18
|
-
|
|
71
|
+
notice: PromptNotice,
|
|
19
72
|
): string {
|
|
20
|
-
const
|
|
21
|
-
|
|
73
|
+
const clean: PromptNotice = {
|
|
74
|
+
title: withoutControlCharacters(notice.title),
|
|
75
|
+
body: withoutControlCharacters(notice.body),
|
|
76
|
+
};
|
|
77
|
+
return channels.map((channel) => renderChannel(channel, clean)).join("");
|
|
22
78
|
}
|
|
23
79
|
|
|
24
80
|
function renderChannel(
|
|
25
81
|
channel: PromptNotificationChannel,
|
|
26
|
-
|
|
82
|
+
notice: PromptNotice,
|
|
27
83
|
): string {
|
|
28
84
|
switch (channel) {
|
|
29
85
|
case "bell":
|
|
30
86
|
return BEL;
|
|
31
87
|
case "osc9":
|
|
32
|
-
|
|
88
|
+
// One field, so the title leads it: it is what names the session.
|
|
89
|
+
return `${OSC}9;${notice.title}: ${notice.body}${BEL}`;
|
|
33
90
|
case "osc777":
|
|
34
|
-
|
|
35
|
-
return `${OSC}777;notify;${OSC_777_TITLE};${text.replaceAll(";", ":")}${BEL}`;
|
|
91
|
+
return `${OSC}777;notify;${osc777Field(notice.title)};${osc777Field(notice.body)}${BEL}`;
|
|
36
92
|
}
|
|
37
93
|
}
|
|
38
94
|
|
|
95
|
+
/** `;` separates OSC 777's fields, so one inside a field would split it. */
|
|
96
|
+
function osc777Field(text: string): string {
|
|
97
|
+
return text.replaceAll(";", ":");
|
|
98
|
+
}
|
|
99
|
+
|
|
39
100
|
function withoutControlCharacters(value: string): string {
|
|
40
101
|
// Code points, not grapheme clusters: a control character is always one.
|
|
41
102
|
return Array.from(value)
|