@pi-unipi/unipi 2.1.3 → 2.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/README.md +6 -2
- package/package.json +23 -22
- package/packages/ask-user/ask-ui.ts +45 -31
- package/packages/ask-user/launcher-ui.ts +26 -22
- package/packages/ask-user/package.json +2 -2
- package/packages/ask-user/settings-tui.ts +22 -9
- package/packages/autocomplete/package.json +1 -1
- package/packages/autocomplete/src/constants.ts +5 -0
- package/packages/btw/package.json +2 -2
- package/packages/cocoindex/package.json +2 -2
- package/packages/compactor/package.json +3 -3
- package/packages/compactor/src/tui/settings-overlay.ts +2 -1
- package/packages/core/constants.ts +18 -0
- package/packages/core/index.ts +1 -0
- package/packages/core/package.json +1 -1
- package/packages/core/tui-width.ts +127 -0
- package/packages/footer/package.json +2 -2
- package/packages/footer/src/tui/settings-tui.ts +2 -1
- package/packages/image/README.md +82 -0
- package/packages/image/index.ts +1 -0
- package/packages/image/package.json +57 -0
- package/packages/image/skills/image/SKILL.md +73 -0
- package/packages/image/src/commands.ts +17 -0
- package/packages/image/src/generate.ts +201 -0
- package/packages/image/src/image-source.ts +204 -0
- package/packages/image/src/index.ts +99 -0
- package/packages/image/src/models.ts +290 -0
- package/packages/image/src/recognize.ts +223 -0
- package/packages/image/src/settings.ts +149 -0
- package/packages/image/src/tools.ts +296 -0
- package/packages/image/src/tui/model-selector.ts +279 -0
- package/packages/image/src/tui/settings-dialog.ts +236 -0
- package/packages/info-screen/package.json +2 -2
- package/packages/info-screen/tui/info-overlay.ts +505 -0
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/kanboard/package.json +2 -2
- package/packages/kanboard/tui/kanboard-overlay.ts +300 -0
- package/packages/mcp/package.json +2 -2
- package/packages/mcp/src/tui/add-overlay.ts +10 -7
- package/packages/mcp/src/tui/settings-overlay.ts +10 -7
- package/packages/memory/package.json +3 -3
- package/packages/memory/tui/settings-tui.ts +281 -0
- package/packages/milestone/package.json +2 -2
- package/packages/notify/README.md +2 -0
- package/packages/notify/events.ts +11 -0
- package/packages/notify/package.json +3 -11
- package/packages/notify/permission-prompt-message.ts +95 -0
- package/packages/notify/platforms/focus-win.ts +123 -0
- package/packages/notify/platforms/focus.ts +33 -0
- package/packages/notify/platforms/gotify.ts +36 -0
- package/packages/notify/platforms/native.ts +79 -0
- package/packages/notify/platforms/node-notifier.d.ts +20 -0
- package/packages/notify/platforms/ntfy.ts +48 -0
- package/packages/notify/platforms/telegram.ts +77 -0
- package/packages/notify/settings.ts +1 -0
- package/packages/notify/skills/configure-notify/SKILL.md +28 -1
- package/packages/notify/src/__tests__/permission-prompt-message.test.ts +172 -0
- package/packages/notify/tui/gotify-setup.ts +529 -0
- package/packages/notify/tui/ntfy-setup.ts +653 -0
- package/packages/notify/tui/recap-model-selector.ts +301 -0
- package/packages/notify/tui/settings-overlay.ts +353 -0
- package/packages/notify/tui/telegram-setup.ts +302 -0
- package/packages/ralph/package.json +3 -3
- package/packages/subagents/package.json +4 -4
- package/packages/subagents/src/__tests__/shutdown-stale-ctx.test.ts +185 -0
- package/packages/subagents/src/index.ts +52 -10
- package/packages/unipi/index.ts +2 -0
- package/packages/updater/package.json +2 -2
- package/packages/updater/src/tui/changelog-overlay.ts +2 -2
- package/packages/updater/src/tui/readme-overlay.ts +2 -1
- package/packages/updater/src/tui/update-overlay.ts +2 -1
- package/packages/utility/package.json +2 -2
- package/packages/utility/src/tui/badge-settings-tui.ts +2 -2
- package/packages/utility/src/tui/util-settings-tui.ts +2 -2
- package/packages/web-api/README.md +28 -11
- package/packages/web-api/package.json +8 -2
- package/packages/web-api/skills/web/SKILL.md +36 -9
- package/packages/web-api/src/index.ts +15 -0
- package/packages/web-api/src/providers/duckduckgo.ts +71 -27
- package/packages/web-api/src/providers/firecrawl.ts +1 -1
- package/packages/web-api/src/providers/jina-reader.ts +1 -1
- package/packages/web-api/src/providers/jina-search.ts +1 -1
- package/packages/web-api/src/providers/perplexity.ts +2 -2
- package/packages/web-api/src/providers/serpapi.ts +1 -1
- package/packages/web-api/src/providers/tavily.ts +1 -1
- package/packages/web-api/src/providers/wigolo-client.ts +196 -0
- package/packages/web-api/src/providers/wigolo.ts +117 -0
- package/packages/web-api/src/settings.ts +1 -0
- package/packages/web-api/src/tools.ts +93 -36
- package/packages/workflow/package.json +2 -2
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/notify — Native OS notification platform
|
|
3
|
+
*
|
|
4
|
+
* Wraps node-notifier for cross-platform desktop notifications.
|
|
5
|
+
* Windows: SnoreToast (no admin required)
|
|
6
|
+
* macOS: terminal-notifier
|
|
7
|
+
* Linux: notify-send / libnotify
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import notifier from "node-notifier";
|
|
11
|
+
import { isWindowFocused } from "./focus.js";
|
|
12
|
+
|
|
13
|
+
/** Options for native notification */
|
|
14
|
+
export interface NativeNotificationOptions {
|
|
15
|
+
/** Windows appID to show instead of "SnoreToast" */
|
|
16
|
+
windowsAppId?: string;
|
|
17
|
+
/**
|
|
18
|
+
* When true, suppresses the notification if the terminal window is
|
|
19
|
+
* the foreground (active) window. Only effective on platforms where
|
|
20
|
+
* `isWindowFocused` is implemented (currently Windows).
|
|
21
|
+
*/
|
|
22
|
+
suppressWhenFocused?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Thrown by sendNativeNotification when the notification was suppressed
|
|
27
|
+
* because suppressWhenFocused is set and the terminal window is focused.
|
|
28
|
+
*
|
|
29
|
+
* Callers should catch this and treat it as intentional suppression,
|
|
30
|
+
* NOT as a send failure.
|
|
31
|
+
*/
|
|
32
|
+
export class SuppressedError extends Error {
|
|
33
|
+
constructor() {
|
|
34
|
+
super("Notification suppressed: terminal window is focused");
|
|
35
|
+
this.name = "SuppressedError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Send a native OS notification.
|
|
41
|
+
*
|
|
42
|
+
* When `suppressWhenFocused` is true and `isWindowFocused()` returns true
|
|
43
|
+
* (i.e. the terminal is the foreground window), the notification is
|
|
44
|
+
* suppressed and the promise rejects with SuppressedError.
|
|
45
|
+
*
|
|
46
|
+
* Resolves when notification is shown, rejects with SuppressedError on
|
|
47
|
+
* suppression or with a standard Error on failure.
|
|
48
|
+
*/
|
|
49
|
+
export async function sendNativeNotification(
|
|
50
|
+
title: string,
|
|
51
|
+
message: string,
|
|
52
|
+
options?: NativeNotificationOptions
|
|
53
|
+
): Promise<void> {
|
|
54
|
+
// Suppress if the terminal window is currently focused
|
|
55
|
+
if (options?.suppressWhenFocused && await isWindowFocused()) {
|
|
56
|
+
throw new SuppressedError();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
notifier.notify(
|
|
61
|
+
{
|
|
62
|
+
title,
|
|
63
|
+
message,
|
|
64
|
+
appID: options?.windowsAppId,
|
|
65
|
+
},
|
|
66
|
+
(err: Error | null) => {
|
|
67
|
+
if (err) {
|
|
68
|
+
reject(
|
|
69
|
+
new Error(
|
|
70
|
+
`Native notification failed: ${err.message}`
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
} else {
|
|
74
|
+
resolve();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
declare module "node-notifier" {
|
|
2
|
+
interface Notification {
|
|
3
|
+
title?: string;
|
|
4
|
+
message?: string;
|
|
5
|
+
appID?: string;
|
|
6
|
+
sound?: boolean | string;
|
|
7
|
+
icon?: string;
|
|
8
|
+
wait?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface Notifier {
|
|
12
|
+
notify(
|
|
13
|
+
notification: Notification,
|
|
14
|
+
callback: (err: Error | null, data: any) => void
|
|
15
|
+
): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const notifier: Notifier;
|
|
19
|
+
export default notifier;
|
|
20
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/notify — ntfy notification platform
|
|
3
|
+
*
|
|
4
|
+
* Sends push notifications to an ntfy server via HTTP POST.
|
|
5
|
+
* ntfy is a simple HTTP-based pub-sub notification service.
|
|
6
|
+
* Supports self-hosted instances and ntfy.sh (public).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Send a notification to an ntfy server */
|
|
10
|
+
export async function sendNtfyNotification(
|
|
11
|
+
serverUrl: string,
|
|
12
|
+
topic: string,
|
|
13
|
+
title: string,
|
|
14
|
+
message: string,
|
|
15
|
+
priority: number = 3,
|
|
16
|
+
token?: string
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
// ntfy supports POSTing to the server root with a JSON body that carries
|
|
19
|
+
// topic/title/message/priority. JSON bodies are UTF-8 safe, unlike HTTP
|
|
20
|
+
// headers which must be ByteString (Latin-1) and reject characters like
|
|
21
|
+
// em dash (U+2014). See https://docs.ntfy.sh/publish/#publish-as-json
|
|
22
|
+
const url = serverUrl.replace(/\/$/, "");
|
|
23
|
+
const headers: Record<string, string> = {
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (token) {
|
|
28
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const body = JSON.stringify({
|
|
32
|
+
topic,
|
|
33
|
+
title,
|
|
34
|
+
message,
|
|
35
|
+
priority: Math.max(1, Math.min(5, priority)),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const response = await fetch(url, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers,
|
|
41
|
+
body,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
const resBody = await response.text().catch(() => "<no body>");
|
|
46
|
+
throw new Error(`ntfy API error ${response.status}: ${resBody}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/notify — Telegram notification platform
|
|
3
|
+
*
|
|
4
|
+
* Sends notifications via Telegram Bot API.
|
|
5
|
+
* Supports auto-detection of chat ID via polling getUpdates.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Send a notification via Telegram Bot API */
|
|
9
|
+
export async function sendTelegramNotification(
|
|
10
|
+
botToken: string,
|
|
11
|
+
chatId: string,
|
|
12
|
+
title: string,
|
|
13
|
+
message: string
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
const text = `*${escapeMarkdown(title)}*\n${escapeMarkdown(message)}`;
|
|
16
|
+
const url = `https://api.telegram.org/bot${botToken}/sendMessage`;
|
|
17
|
+
|
|
18
|
+
const response = await fetch(url, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: { "Content-Type": "application/json" },
|
|
21
|
+
body: JSON.stringify({
|
|
22
|
+
chat_id: chatId,
|
|
23
|
+
text,
|
|
24
|
+
parse_mode: "MarkdownV2",
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
const body = await response.text().catch(() => "<no body>");
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Telegram API error ${response.status}: ${body}`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Poll Telegram getUpdates to detect the chat ID from a user message.
|
|
38
|
+
* Returns the chat ID string, or null if no message found.
|
|
39
|
+
*/
|
|
40
|
+
export async function pollForChatId(
|
|
41
|
+
botToken: string,
|
|
42
|
+
signal?: AbortSignal
|
|
43
|
+
): Promise<string | null> {
|
|
44
|
+
const url = `https://api.telegram.org/bot${botToken}/getUpdates?allowed_updates=["message"]`;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetch(url, { signal });
|
|
48
|
+
const data = (await response.json()) as {
|
|
49
|
+
ok: boolean;
|
|
50
|
+
result: Array<{
|
|
51
|
+
message?: { chat?: { id?: number } };
|
|
52
|
+
callback_query?: { message?: { chat?: { id?: number } } };
|
|
53
|
+
}>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (data.ok && data.result.length > 0) {
|
|
57
|
+
const lastUpdate = data.result[data.result.length - 1];
|
|
58
|
+
const chatId =
|
|
59
|
+
lastUpdate.message?.chat?.id ||
|
|
60
|
+
lastUpdate.callback_query?.message?.chat?.id;
|
|
61
|
+
if (chatId) {
|
|
62
|
+
return String(chatId);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if (signal?.aborted) throw err;
|
|
67
|
+
// Network error — return null to allow retry
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Escape special MarkdownV2 characters */
|
|
75
|
+
function escapeMarkdown(text: string): string {
|
|
76
|
+
return text.replace(/[_*\[\]()~`>#+\-=|{}.!\\]/g, "\\$&");
|
|
77
|
+
}
|
|
@@ -28,6 +28,7 @@ export const DEFAULT_CONFIG: NotifyConfig = {
|
|
|
28
28
|
memory_consolidated: { enabled: false, platforms: [] },
|
|
29
29
|
session_shutdown: { enabled: false, platforms: [] },
|
|
30
30
|
ask_user_prompt: { enabled: false, platforms: [] },
|
|
31
|
+
permission_request: { enabled: false, platforms: [] },
|
|
31
32
|
},
|
|
32
33
|
native: {
|
|
33
34
|
enabled: true,
|
|
@@ -36,7 +36,9 @@ Help users configure the `@pi-unipi/notify` notification system.
|
|
|
36
36
|
"agent_end": { "enabled": false, "platforms": [] },
|
|
37
37
|
"agent_settled": { "enabled": false, "platforms": [] },
|
|
38
38
|
"memory_consolidated": { "enabled": false, "platforms": [] },
|
|
39
|
-
"session_shutdown": { "enabled": false, "platforms": [] }
|
|
39
|
+
"session_shutdown": { "enabled": false, "platforms": [] },
|
|
40
|
+
"ask_user_prompt": { "enabled": false, "platforms": [] },
|
|
41
|
+
"permission_request": { "enabled": false, "platforms": [] }
|
|
40
42
|
},
|
|
41
43
|
"native": {
|
|
42
44
|
"enabled": true,
|
|
@@ -155,9 +157,34 @@ ntfy uses dedicated `ntfy.json` files at both global and project scope, with ful
|
|
|
155
157
|
| `agent_settled` | Off | Agent fully settles after retries, compaction, and queued continuations |
|
|
156
158
|
| `memory_consolidated` | Off | Memory auto-saved |
|
|
157
159
|
| `session_shutdown` | Off | Session ends |
|
|
160
|
+
| `ask_user_prompt` | Off | Agent asked a question and is waiting for an answer |
|
|
161
|
+
| `permission_request` | Off | A permission prompt is about to be shown |
|
|
158
162
|
|
|
159
163
|
Each event can override `platforms` — empty array means use `defaultPlatforms`.
|
|
160
164
|
|
|
165
|
+
### `permission_request`
|
|
166
|
+
|
|
167
|
+
Fires on the `permissions:ui_prompt` broadcast from
|
|
168
|
+
[`@gotgenes/pi-permission-system`](https://www.npmjs.com/package/@gotgenes/pi-permission-system),
|
|
169
|
+
emitted immediately before the user-facing permission UI is invoked. Policy
|
|
170
|
+
auto-allow, policy deny, session approvals, and infrastructure auto-allowed
|
|
171
|
+
requests do **not** emit it, so this event does not produce notification spam.
|
|
172
|
+
Forwarded subagent prompts are handled too — the parent UI session emits the
|
|
173
|
+
event right before showing the forwarded dialog, and the notification is
|
|
174
|
+
suffixed with `(forwarded)`.
|
|
175
|
+
|
|
176
|
+
The notification is formatted defensively from the payload's `agentName`,
|
|
177
|
+
`surface`, `value` and `message` fields:
|
|
178
|
+
|
|
179
|
+
```text
|
|
180
|
+
Pi — Permission Request
|
|
181
|
+
Current agent requested bash 'npm test'. Allow this command?
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Enable it when you run Pi in a background pane and don't want to miss a
|
|
185
|
+
permission prompt. If the permission system is not installed the event simply
|
|
186
|
+
never fires.
|
|
187
|
+
|
|
161
188
|
## Agent workflow
|
|
162
189
|
|
|
163
190
|
### Reading current config
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for buildPermissionPromptMessage — the `permissions:ui_prompt`
|
|
3
|
+
* payload projection used by the `permission_request` notify event.
|
|
4
|
+
*
|
|
5
|
+
* The test fixtures use `satisfies PermissionPromptEventPayload` to
|
|
6
|
+
* enforce the payload contract at compile time.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it } from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
buildPermissionPromptMessage,
|
|
14
|
+
type PermissionPromptEventPayload,
|
|
15
|
+
} from "../../permission-prompt-message.ts";
|
|
16
|
+
|
|
17
|
+
describe("buildPermissionPromptMessage", () => {
|
|
18
|
+
it("formats a standard bash permission payload", () => {
|
|
19
|
+
const payload = {
|
|
20
|
+
requestId: "req-1",
|
|
21
|
+
source: "tool",
|
|
22
|
+
surface: "bash",
|
|
23
|
+
value: "npm test",
|
|
24
|
+
agentName: "Current agent",
|
|
25
|
+
message: "Allow this command?",
|
|
26
|
+
} satisfies PermissionPromptEventPayload;
|
|
27
|
+
|
|
28
|
+
assert.equal(
|
|
29
|
+
buildPermissionPromptMessage(payload),
|
|
30
|
+
"Current agent requested bash 'npm test'. Allow this command?",
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("falls back to \"Agent\" when agentName is absent", () => {
|
|
35
|
+
const payload = {
|
|
36
|
+
surface: "edit",
|
|
37
|
+
value: "src/index.ts",
|
|
38
|
+
} satisfies PermissionPromptEventPayload;
|
|
39
|
+
|
|
40
|
+
assert.equal(
|
|
41
|
+
buildPermissionPromptMessage(payload),
|
|
42
|
+
"Agent requested edit 'src/index.ts'.",
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("marks forwarded subagent prompts", () => {
|
|
47
|
+
const payload = {
|
|
48
|
+
surface: "mcp",
|
|
49
|
+
value: "github.create_issue",
|
|
50
|
+
agentName: "explore",
|
|
51
|
+
forwarding: { from: "subagent-1" },
|
|
52
|
+
} satisfies PermissionPromptEventPayload;
|
|
53
|
+
|
|
54
|
+
assert.equal(
|
|
55
|
+
buildPermissionPromptMessage(payload),
|
|
56
|
+
"explore requested mcp 'github.create_issue'. (forwarded)",
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("does not mark prompts when forwarding is explicitly falsy", () => {
|
|
61
|
+
for (const forwarding of [false, null, "", 0, {}, []]) {
|
|
62
|
+
const payload = {
|
|
63
|
+
surface: "read",
|
|
64
|
+
value: "/etc/hosts",
|
|
65
|
+
forwarding,
|
|
66
|
+
} satisfies PermissionPromptEventPayload;
|
|
67
|
+
|
|
68
|
+
const result = buildPermissionPromptMessage(payload);
|
|
69
|
+
assert.equal(
|
|
70
|
+
result.includes("(forwarded)"),
|
|
71
|
+
false,
|
|
72
|
+
`expected no forwarded marker for ${JSON.stringify(forwarding)}`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("uses surface alone when no value is provided", () => {
|
|
78
|
+
const payload = { surface: "bash" } satisfies PermissionPromptEventPayload;
|
|
79
|
+
|
|
80
|
+
assert.equal(
|
|
81
|
+
buildPermissionPromptMessage(payload),
|
|
82
|
+
"Agent requested bash access.",
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("uses value alone when no surface is provided", () => {
|
|
87
|
+
const payload = { value: "rm -rf build" } satisfies PermissionPromptEventPayload;
|
|
88
|
+
|
|
89
|
+
assert.equal(
|
|
90
|
+
buildPermissionPromptMessage(payload),
|
|
91
|
+
"Agent requested 'rm -rf build'.",
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("falls back to the message when surface and value are missing", () => {
|
|
96
|
+
const payload = {
|
|
97
|
+
requestId: "req-2",
|
|
98
|
+
message: "Allow access to the workspace?",
|
|
99
|
+
} satisfies PermissionPromptEventPayload;
|
|
100
|
+
|
|
101
|
+
assert.equal(
|
|
102
|
+
buildPermissionPromptMessage(payload),
|
|
103
|
+
"Allow access to the workspace?",
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("does not duplicate the message when it equals the request clause", () => {
|
|
108
|
+
const payload = {
|
|
109
|
+
surface: "bash",
|
|
110
|
+
value: "npm test",
|
|
111
|
+
agentName: "Current agent",
|
|
112
|
+
message: "Current agent requested bash 'npm test'.",
|
|
113
|
+
} satisfies PermissionPromptEventPayload;
|
|
114
|
+
|
|
115
|
+
assert.equal(
|
|
116
|
+
buildPermissionPromptMessage(payload),
|
|
117
|
+
"Current agent requested bash 'npm test'.",
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("returns a generic fallback for an empty payload", () => {
|
|
122
|
+
assert.equal(
|
|
123
|
+
buildPermissionPromptMessage({}),
|
|
124
|
+
"Pi is waiting for a permission decision.",
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("handles null, undefined and non-object payloads without throwing", () => {
|
|
129
|
+
for (const payload of [null, undefined, 42, "nope", true]) {
|
|
130
|
+
assert.equal(
|
|
131
|
+
buildPermissionPromptMessage(payload),
|
|
132
|
+
"Pi is waiting for a permission decision.",
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("treats empty and whitespace-only strings as absent", () => {
|
|
138
|
+
const payload = {
|
|
139
|
+
surface: " ",
|
|
140
|
+
value: "",
|
|
141
|
+
agentName: " ",
|
|
142
|
+
message: "\t\n",
|
|
143
|
+
} satisfies PermissionPromptEventPayload;
|
|
144
|
+
|
|
145
|
+
assert.equal(
|
|
146
|
+
buildPermissionPromptMessage(payload),
|
|
147
|
+
"Pi is waiting for a permission decision.",
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("ignores non-string surface/value/message fields", () => {
|
|
152
|
+
const payload = {
|
|
153
|
+
surface: 123,
|
|
154
|
+
value: { cmd: "ls" },
|
|
155
|
+
message: ["a"],
|
|
156
|
+
} as unknown as PermissionPromptEventPayload;
|
|
157
|
+
|
|
158
|
+
assert.equal(
|
|
159
|
+
buildPermissionPromptMessage(payload),
|
|
160
|
+
"Pi is waiting for a permission decision.",
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("still reports a forwarded prompt with no other detail", () => {
|
|
165
|
+
const payload = { forwarding: true } satisfies PermissionPromptEventPayload;
|
|
166
|
+
|
|
167
|
+
assert.equal(
|
|
168
|
+
buildPermissionPromptMessage(payload),
|
|
169
|
+
"Pi is waiting for a permission decision. (forwarded)",
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
});
|