@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 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
 
@@ -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 text is the dialog title (`Permission Required`, or `Permission Required (Subagent)` for a forwarded ask), never the command or path being decided, so nothing from the request lands in your notification history.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "36.1.0",
3
+ "version": "36.2.0",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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; the notification text is the dialog title, never the request itself.",
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
- details.forwarding
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 { renderPromptNotification } from "#src/presentation/prompt-notification";
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
- title,
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; the notification text is the dialog title, never the request itself.',
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
- /** The OSC 777 title; the body carries the dialog title. */
7
- const OSC_777_TITLE = "pi";
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. The message is stripped of control characters, so it cannot
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
- message: string,
71
+ notice: PromptNotice,
19
72
  ): string {
20
- const text = withoutControlCharacters(message);
21
- return channels.map((channel) => renderChannel(channel, text)).join("");
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
- text: string,
82
+ notice: PromptNotice,
27
83
  ): string {
28
84
  switch (channel) {
29
85
  case "bell":
30
86
  return BEL;
31
87
  case "osc9":
32
- return `${OSC}9;${text}${BEL}`;
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
- // `;` separates OSC 777's fields, so one in the body would split it.
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)