@agimon-ai/po-notification 0.0.0-alpha.1 → 0.0.0-stage
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 +2 -95
- package/package.json +3 -102
- package/LICENSE +0 -21
- package/dist/apiContracts.cjs +0 -17
- package/dist/apiContracts.cjs.map +0 -1
- package/dist/apiContracts.d.mts +0 -53
- package/dist/apiContracts.d.mts.map +0 -1
- package/dist/apiContracts.mjs +0 -13
- package/dist/apiContracts.mjs.map +0 -1
- package/dist/extensions/server.cjs +0 -39
- package/dist/extensions/server.cjs.map +0 -1
- package/dist/extensions/server.d.mts +0 -5
- package/dist/extensions/server.d.mts.map +0 -1
- package/dist/extensions/server.mjs +0 -34
- package/dist/extensions/server.mjs.map +0 -1
- package/dist/index.cjs +0 -307
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.mts +0 -116
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs +0 -272
- package/dist/index.mjs.map +0 -1
- package/llms.txt +0 -7
package/README.md
CHANGED
|
@@ -1,96 +1,3 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Temporary Holding Version
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[Po](https://www.npmjs.com/package/@agimon-ai/po) sessions.
|
|
5
|
-
|
|
6
|
-
Long agent runs are worth walking away from. This package owns the shared `doom/notification` service,
|
|
7
|
-
routes each request for the active session, and announces when the agent needs attention.
|
|
8
|
-
|
|
9
|
-
## What it does
|
|
10
|
-
|
|
11
|
-
| Surface | Behavior |
|
|
12
|
-
| ---------------- | -------------------------------------------------------------------------------------------- |
|
|
13
|
-
| Shell tab title | `π - <session or first prompt> - <repository>`, with a braille spinner while the agent works |
|
|
14
|
-
| Notification API | caller-authored notices through the shared `doom/notification` Cordis service |
|
|
15
|
-
| Mid-run | one notification for each agent-initiated dialog and each `ask_user_question` prompt |
|
|
16
|
-
| At rest | one notification when a run settles with nothing queued behind it |
|
|
17
|
-
|
|
18
|
-
In an interactive terminal session, requests use `cmux notify` first because it routes them back to
|
|
19
|
-
the session window. On macOS, a missing `cmux` falls back to `osascript`. In RPC mode, requests become
|
|
20
|
-
versioned `doom-notification` session entries for a live client such as `po-web`. An RPC append
|
|
21
|
-
failure stays silent and never falls back to a host notifier. Hosts without a supported notifier also
|
|
22
|
-
stay silent.
|
|
23
|
-
|
|
24
|
-
## Service usage
|
|
25
|
-
|
|
26
|
-
Callers should discover the optional service through Cordis and request delivery without depending on
|
|
27
|
-
a particular host:
|
|
28
|
-
|
|
29
|
-
```ts
|
|
30
|
-
import { DOOM_NOTIFICATION_SERVICE, readDoomNotificationService } from '@agimon-ai/po-core/notification';
|
|
31
|
-
|
|
32
|
-
ctx.inject([DOOM_NOTIFICATION_SERVICE], (notificationContext) => {
|
|
33
|
-
void readDoomNotificationService(notificationContext)?.request({
|
|
34
|
-
body: 'Deployment needs approval',
|
|
35
|
-
level: 'warning',
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
`body` is required. `title`, `subtitle`, and `level` are optional, and `level` accepts `info`,
|
|
41
|
-
`warning`, or `error`. The router defaults the title to `Pi`, the subtitle to the session name or
|
|
42
|
-
working-directory basename, and the level to `info`. Invalid requests, missing active sessions,
|
|
43
|
-
unavailable providers, and delivery failures are silent so notifications cannot fail an agent turn.
|
|
44
|
-
|
|
45
|
-
The package also wraps Pi's broad `ui.notify(message, level)` API while active, so existing extensions
|
|
46
|
-
use the same route without changing their calls. If this package is muted or the process is a detached
|
|
47
|
-
subagent child, it does not install that wrapper and Pi's original `ui.notify` behavior remains.
|
|
48
|
-
|
|
49
|
-
## Browser delivery
|
|
50
|
-
|
|
51
|
-
`po-web` shows full notification text only for entries received by a currently open page. Every connected
|
|
52
|
-
page receives live notification entries for every attached session, not only the focused session. Permission
|
|
53
|
-
is requested only from an explicit settings action.
|
|
54
|
-
|
|
55
|
-
A paired installed PWA may also opt into live Web Push while closed. That payload is fixed generic copy with
|
|
56
|
-
`TTL: 0`; it contains no session, prompt, response, or file data. The host keeps subscriptions only in memory,
|
|
57
|
-
so there is no durable database, outbox, replay, or delivery of historical entries. The open app re-registers
|
|
58
|
-
a browser-held subscription after a host restart. Device revocation or expiry, remote shutdown, browser
|
|
59
|
-
disable, and expired provider endpoints remove the live subscription.
|
|
60
|
-
|
|
61
|
-
## The shell title
|
|
62
|
-
|
|
63
|
-
The title is animated from a worker thread, so a busy agent turn never stalls the spinner. The
|
|
64
|
-
worker is unreferenced, so a pending frame cannot hold the process open. If worker threads are
|
|
65
|
-
unavailable, or the worker fails or exits, the last command replays on the main thread.
|
|
66
|
-
|
|
67
|
-
Titles are only written to an attached terminal: `rpc` sessions and headless ones get none, so
|
|
68
|
-
escape sequences never end up in machine-read output.
|
|
69
|
-
|
|
70
|
-
## Quiet by design
|
|
71
|
-
|
|
72
|
-
- A detached subagent (`PI_SUBAGENT_CHILD`) does not register this package's service or wrappers. It
|
|
73
|
-
shares its parent's terminal and desktop, and a second voice reporting the same run is noise.
|
|
74
|
-
- Dialogs only notify while the agent holds the turn. A dialog the user opened is already in front
|
|
75
|
-
of them.
|
|
76
|
-
- An `ask_user_question` prompt notifies from its own event, and the dialog it then opens stays
|
|
77
|
-
silent, so one question is announced once.
|
|
78
|
-
- A settled run with pending follow-up messages notifies nothing: the agent is about to keep going.
|
|
79
|
-
|
|
80
|
-
## Installation
|
|
81
|
-
|
|
82
|
-
Po depends on this package and activates it as fixed host core, so a Po install already has
|
|
83
|
-
it. It is not selectable from `.po/modes.yaml`.
|
|
84
|
-
|
|
85
|
-
The Pi and server loaders use `/extensions/pi` and `/extensions/server`, built directly from
|
|
86
|
-
`src/extensions`. The root export contains reusable notification helpers and types.
|
|
87
|
-
Controllers own delivery and UI integration; policy and text services live in named service folders.
|
|
88
|
-
|
|
89
|
-
The Pi declaration installs its router and optional session binding before handling events.
|
|
90
|
-
`onStop` restores the original UI methods and shell title before service registrations are removed.
|
|
91
|
-
`onDispose` provides the same cleanup after partial startup. Detached children return before
|
|
92
|
-
acquiring a Cordis connection, leaving notification ownership with the parent.
|
|
93
|
-
|
|
94
|
-
## License
|
|
95
|
-
|
|
96
|
-
MIT
|
|
3
|
+
This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
|
package/package.json
CHANGED
|
@@ -1,105 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agimon-ai/po-notification",
|
|
3
|
-
"version": "0.0.0-
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"ai",
|
|
7
|
-
"coding-agent",
|
|
8
|
-
"desktop-notifications",
|
|
9
|
-
"developer-tools",
|
|
10
|
-
"notifications",
|
|
11
|
-
"pi-coding-agent",
|
|
12
|
-
"pi-extension",
|
|
13
|
-
"pi-package",
|
|
14
|
-
"po",
|
|
15
|
-
"shell-title",
|
|
16
|
-
"terminal-title"
|
|
17
|
-
],
|
|
18
|
-
"homepage": "https://agimon.ai",
|
|
19
|
-
"bugs": {
|
|
20
|
-
"url": "https://github.com/AgiFlow/po/issues"
|
|
21
|
-
},
|
|
22
|
-
"license": "MIT",
|
|
23
|
-
"author": "Vuong Ngo",
|
|
24
|
-
"repository": {
|
|
25
|
-
"type": "git",
|
|
26
|
-
"url": "git+https://github.com/AgiFlow/po.git",
|
|
27
|
-
"directory": "packages/foundations/po-notification"
|
|
28
|
-
},
|
|
29
|
-
"files": [
|
|
30
|
-
"dist",
|
|
31
|
-
"llms.txt",
|
|
32
|
-
"README.md",
|
|
33
|
-
"LICENSE",
|
|
34
|
-
"package.json"
|
|
35
|
-
],
|
|
36
|
-
"type": "module",
|
|
37
|
-
"main": "./dist/index.cjs",
|
|
38
|
-
"module": "./dist/index.mjs",
|
|
39
|
-
"types": "./dist/index.d.mts",
|
|
40
|
-
"jsnext:main": "./dist/index.mjs",
|
|
41
|
-
"exports": {
|
|
42
|
-
".": {
|
|
43
|
-
"types": "./dist/index.d.mts",
|
|
44
|
-
"import": "./dist/index.mjs",
|
|
45
|
-
"require": "./dist/index.cjs"
|
|
46
|
-
},
|
|
47
|
-
"./apiContracts": {
|
|
48
|
-
"types": "./dist/apiContracts.d.mts",
|
|
49
|
-
"import": "./dist/apiContracts.mjs",
|
|
50
|
-
"require": "./dist/apiContracts.cjs"
|
|
51
|
-
},
|
|
52
|
-
"./extensions/server": {
|
|
53
|
-
"types": "./dist/extensions/server.d.mts",
|
|
54
|
-
"import": "./dist/extensions/server.mjs",
|
|
55
|
-
"require": "./dist/extensions/server.cjs"
|
|
56
|
-
},
|
|
57
|
-
"./package.json": "./package.json"
|
|
58
|
-
},
|
|
59
|
-
"publishConfig": {
|
|
60
|
-
"access": "public"
|
|
61
|
-
},
|
|
62
|
-
"dependencies": {
|
|
63
|
-
"@agimon-ai/po-core": "0.0.0-alpha.1",
|
|
64
|
-
"@deepseek-ai/cordis": "4.0.4"
|
|
65
|
-
},
|
|
66
|
-
"devDependencies": {
|
|
67
|
-
"@agimon-ai/po-build": "0.0.0-alpha.1",
|
|
68
|
-
"@agimon-ai/vibe-lint-plugin-doom-extension": "0.0.0-alpha.1",
|
|
69
|
-
"@earendil-works/pi-coding-agent": "1.0.2",
|
|
70
|
-
"@types/node": "26.6.4",
|
|
71
|
-
"@vitest/coverage-v8": "5.0.3",
|
|
72
|
-
"tsdown": "0.23.0",
|
|
73
|
-
"typescript": "7.0.2",
|
|
74
|
-
"vitest": "5.0.3"
|
|
75
|
-
},
|
|
76
|
-
"peerDependencies": {
|
|
77
|
-
"@earendil-works/pi-coding-agent": "1.0.2"
|
|
78
|
-
},
|
|
79
|
-
"peerDependenciesMeta": {
|
|
80
|
-
"@earendil-works/pi-coding-agent": {
|
|
81
|
-
"optional": true
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
"engines": {
|
|
85
|
-
"node": ">=24"
|
|
86
|
-
},
|
|
87
|
-
"poServer": {
|
|
88
|
-
"entry": "./generated/server.ts",
|
|
89
|
-
"dist": "./dist/extensions/server.mjs",
|
|
90
|
-
"scopes": [
|
|
91
|
-
"session"
|
|
92
|
-
],
|
|
93
|
-
"contracts": {
|
|
94
|
-
"entry": "./src/exports/apiContracts.ts",
|
|
95
|
-
"dist": "./dist/apiContracts.mjs"
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
"scripts": {
|
|
99
|
-
"build": "tsdown",
|
|
100
|
-
"test": "vitest --run",
|
|
101
|
-
"typecheck": "tsc --noEmit",
|
|
102
|
-
"lint": "oxlint . && oxfmt . --check",
|
|
103
|
-
"fixcode": "oxlint . --fix && oxfmt ."
|
|
104
|
-
}
|
|
3
|
+
"version": "0.0.0-stage",
|
|
4
|
+
"stub": true,
|
|
5
|
+
"description": "Temporary package placeholder for staged publishing"
|
|
105
6
|
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Vuong Ngo
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
package/dist/apiContracts.cjs
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
Object.defineProperties(exports, {
|
|
2
|
-
__esModule: { value: true },
|
|
3
|
-
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
-
});
|
|
5
|
-
//#region src/schemas/apiContracts.ts
|
|
6
|
-
/** This facet contributes agent tools, commands or lifecycle only, with no separate HTTP or socket surface. */
|
|
7
|
-
const apiContracts = (0, require("@agimon-ai/po-core/apiContracts").defineApiContract)({
|
|
8
|
-
version: 1,
|
|
9
|
-
http: [],
|
|
10
|
-
sockets: [],
|
|
11
|
-
dynamic: []
|
|
12
|
-
});
|
|
13
|
-
//#endregion
|
|
14
|
-
exports.apiContracts = apiContracts;
|
|
15
|
-
exports.default = apiContracts;
|
|
16
|
-
|
|
17
|
-
//# sourceMappingURL=apiContracts.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"apiContracts.cjs","names":["defineApiContract"],"sources":["../src/schemas/apiContracts.ts"],"sourcesContent":["import { defineApiContract } from '@agimon-ai/po-core/apiContracts';\n\n/** This facet contributes agent tools, commands or lifecycle only, with no separate HTTP or socket surface. */\nexport const apiContracts = defineApiContract({ version: 1, http: [], sockets: [], dynamic: [] });\nexport default apiContracts;\n"],"mappings":";;;;;;AAGA,MAAa,gBAAA,4CAAeA,CAAAA,CAAAA,kBAAAA,CAAkB;CAAE,SAAS;CAAG,MAAM,CAAC;CAAG,SAAS,CAAC;CAAG,SAAS,CAAC;AAAE,CAAC"}
|
package/dist/apiContracts.d.mts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
//#region src/schemas/apiContracts.d.ts
|
|
2
|
-
/** This facet contributes agent tools, commands or lifecycle only, with no separate HTTP or socket surface. */
|
|
3
|
-
export declare const apiContracts: {
|
|
4
|
-
version: 1;
|
|
5
|
-
protocols?: Record<string, string> | undefined;
|
|
6
|
-
schemas?: Record<string, boolean | object> | undefined;
|
|
7
|
-
http: {
|
|
8
|
-
id: string;
|
|
9
|
-
scope: "global" | "session" | "workspace";
|
|
10
|
-
basePath?: string | undefined;
|
|
11
|
-
path: string;
|
|
12
|
-
method: "DELETE" | "GET" | "HEAD" | "OPTIONS" | "PATCH" | "POST" | "PUT";
|
|
13
|
-
description: string;
|
|
14
|
-
availability?: string | undefined;
|
|
15
|
-
authentication: "device" | "none" | "owner";
|
|
16
|
-
parameters?: {
|
|
17
|
-
name: string;
|
|
18
|
-
in: "header" | "path" | "query";
|
|
19
|
-
required: boolean;
|
|
20
|
-
schema: boolean | object;
|
|
21
|
-
}[] | undefined;
|
|
22
|
-
body?: {
|
|
23
|
-
contentType: string;
|
|
24
|
-
contentTypes?: string[] | undefined;
|
|
25
|
-
required: boolean;
|
|
26
|
-
schema: boolean | object;
|
|
27
|
-
} | undefined;
|
|
28
|
-
responses: Record<string, {
|
|
29
|
-
description: string;
|
|
30
|
-
contentType?: string | undefined;
|
|
31
|
-
schema?: boolean | object | undefined;
|
|
32
|
-
events?: Record<string, boolean | object> | undefined;
|
|
33
|
-
}>;
|
|
34
|
-
}[];
|
|
35
|
-
sockets: {
|
|
36
|
-
id: string;
|
|
37
|
-
scope: "global" | "session" | "workspace";
|
|
38
|
-
description: string;
|
|
39
|
-
availability?: string | undefined;
|
|
40
|
-
service: string;
|
|
41
|
-
member: string;
|
|
42
|
-
direction: "client-to-server" | "server-to-client";
|
|
43
|
-
kind: "channel" | "method" | "state";
|
|
44
|
-
input: boolean | object;
|
|
45
|
-
output?: boolean | object | undefined;
|
|
46
|
-
errors?: boolean | object | undefined;
|
|
47
|
-
}[];
|
|
48
|
-
dynamic: string[];
|
|
49
|
-
gaps?: string[] | undefined;
|
|
50
|
-
};
|
|
51
|
-
//#endregion
|
|
52
|
-
export { apiContracts as default };
|
|
53
|
-
//# sourceMappingURL=apiContracts.d.mts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"apiContracts.d.mts","names":[],"sources":["../src/schemas/apiContracts.ts"],"mappings":";;qBAGa"}
|
package/dist/apiContracts.mjs
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { defineApiContract } from "@agimon-ai/po-core/apiContracts";
|
|
2
|
-
//#region src/schemas/apiContracts.ts
|
|
3
|
-
/** This facet contributes agent tools, commands or lifecycle only, with no separate HTTP or socket surface. */
|
|
4
|
-
const apiContracts = defineApiContract({
|
|
5
|
-
version: 1,
|
|
6
|
-
http: [],
|
|
7
|
-
sockets: [],
|
|
8
|
-
dynamic: []
|
|
9
|
-
});
|
|
10
|
-
//#endregion
|
|
11
|
-
export { apiContracts, apiContracts as default };
|
|
12
|
-
|
|
13
|
-
//# sourceMappingURL=apiContracts.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"apiContracts.mjs","names":[],"sources":["../src/schemas/apiContracts.ts"],"sourcesContent":["import { defineApiContract } from '@agimon-ai/po-core/apiContracts';\n\n/** This facet contributes agent tools, commands or lifecycle only, with no separate HTTP or socket surface. */\nexport const apiContracts = defineApiContract({ version: 1, http: [], sockets: [], dynamic: [] });\nexport default apiContracts;\n"],"mappings":";;;AAGA,MAAa,eAAe,kBAAkB;CAAE,SAAS;CAAG,MAAM,CAAC;CAAG,SAAS,CAAC;CAAG,SAAS,CAAC;AAAE,CAAC"}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
Object.defineProperties(exports, {
|
|
2
|
-
__esModule: { value: true },
|
|
3
|
-
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
-
});
|
|
5
|
-
let _agimon_ai_po_core_serverFacet = require("@agimon-ai/po-core/serverFacet");
|
|
6
|
-
let _agimon_ai_po_core_extensionFile = require("@agimon-ai/po-core/extensionFile");
|
|
7
|
-
//#region src/extensions/workspaces/sessions/(backend)/_lib/notificationCommand.ts
|
|
8
|
-
const notificationCommand = (0, _agimon_ai_po_core_serverFacet.defineCommand)({
|
|
9
|
-
name: "notify",
|
|
10
|
-
description: "Send a notification through the active headless client.",
|
|
11
|
-
async execute(args, execution) {
|
|
12
|
-
const body = args.trim();
|
|
13
|
-
if (body) await execution.notify({
|
|
14
|
-
body,
|
|
15
|
-
level: "info"
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
});
|
|
19
|
-
//#endregion
|
|
20
|
-
//#region src/extensions/workspaces/sessions/(backend)/command/notify.server.ts
|
|
21
|
-
var notify_server_default = (0, _agimon_ai_po_core_extensionFile.defineServerCommand)(notificationCommand);
|
|
22
|
-
//#endregion
|
|
23
|
-
//#region generated/server.ts
|
|
24
|
-
const at = (value, context) => typeof value === "function" ? value(context) : value;
|
|
25
|
-
const via = (identity, value) => value === void 0 ? void 0 : {
|
|
26
|
-
...identity,
|
|
27
|
-
...value
|
|
28
|
-
};
|
|
29
|
-
const facet = (0, _agimon_ai_po_core_serverFacet.defineServerPlugin)({
|
|
30
|
-
name: "@agimon-ai/po-notification",
|
|
31
|
-
session: (context) => ({ get commands() {
|
|
32
|
-
return [via({ name: "notify" }, at(notify_server_default, context))];
|
|
33
|
-
} })
|
|
34
|
-
});
|
|
35
|
-
//#endregion
|
|
36
|
-
exports.default = facet;
|
|
37
|
-
exports.facet = facet;
|
|
38
|
-
|
|
39
|
-
//# sourceMappingURL=server.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"server.cjs","names":["defineCommand","defineServerCommand","defineServerPlugin","commandNotify"],"sources":["../../src/extensions/workspaces/sessions/(backend)/_lib/notificationCommand.ts","../../src/extensions/workspaces/sessions/(backend)/command/notify.server.ts","../../generated/server.ts"],"sourcesContent":["import { defineCommand } from '@agimon-ai/po-core/serverFacet';\n\nexport const notificationCommand = defineCommand({\n name: 'notify',\n description: 'Send a notification through the active headless client.',\n async execute(args, execution) {\n const body = args.trim();\n if (body) await execution.notify({ body, level: 'info' });\n },\n});\n","import { defineServerCommand } from '@agimon-ai/po-core/extensionFile';\n\nimport { notificationCommand } from '../_lib/notificationCommand';\n\nexport default defineServerCommand(notificationCommand);\n","// Generated by @agimon-ai/po-build. Do not edit by hand.\nimport { defineServerPlugin, type DoomServerSessionPlugin } from '@agimon-ai/po-core/serverFacet';\n\nimport commandNotify from '../src/extensions/workspaces/sessions/(backend)/command/notify.server';\n\ntype Factory<T, C> = (context: C) => T;\nconst at = <T, C>(value: T | Factory<T, C>, context: C): T =>\n typeof value === 'function' ? (value as Factory<T, C>)(context) : value;\nconst via = <T>(identity: Record<string, unknown>, value: unknown): T =>\n (value === undefined ? undefined : { ...identity, ...(value as object) }) as T;\n\nexport const facet = defineServerPlugin({\n name: '@agimon-ai/po-notification',\n session: (context) => ({\n get commands(): DoomServerSessionPlugin['commands'] { return [via({ name: 'notify' }, at(commandNotify, context))]; },\n }),\n});\n\nexport default facet;\n"],"mappings":";;;;;;;AAEA,MAAa,uBAAA,GAAsBA,+BAAAA,cAAAA,CAAc;CAC/C,MAAM;CACN,aAAa;CACb,MAAM,QAAQ,MAAM,WAAW;EAC7B,MAAM,OAAO,KAAK,KAAK;EACvB,IAAI,MAAM,MAAM,UAAU,OAAO;GAAE;GAAM,OAAO;EAAO,CAAC;CAC1D;AACF,CAAC;;;ACLD,IAAA,yBAAA,GAAeC,iCAAAA,oBAAAA,CAAoB,mBAAmB;;;ACEtD,MAAM,MAAY,OAA0B,YAC1C,OAAO,UAAU,aAAc,MAAwB,OAAO,IAAI;AACpE,MAAM,OAAU,UAAmC,UAChD,UAAU,KAAA,IAAY,KAAA,IAAY;CAAE,GAAG;CAAU,GAAI;AAAiB;AAEzE,MAAa,SAAA,GAAQC,+BAAAA,mBAAAA,CAAmB;CACtC,MAAM;CACN,UAAU,aAAa,EACrB,IAAI,WAAgD;EAAE,OAAO,CAAC,IAAI,EAAE,MAAM,SAAS,GAAG,GAAGC,uBAAe,OAAO,CAAC,CAAC;CAAG,EACtH;AACF,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.mts","names":[],"sources":["../../generated/server.ts"],"mappings":";qBAWa,gDAAK"}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { defineCommand, defineServerPlugin } from "@agimon-ai/po-core/serverFacet";
|
|
2
|
-
import { defineServerCommand } from "@agimon-ai/po-core/extensionFile";
|
|
3
|
-
//#region src/extensions/workspaces/sessions/(backend)/_lib/notificationCommand.ts
|
|
4
|
-
const notificationCommand = defineCommand({
|
|
5
|
-
name: "notify",
|
|
6
|
-
description: "Send a notification through the active headless client.",
|
|
7
|
-
async execute(args, execution) {
|
|
8
|
-
const body = args.trim();
|
|
9
|
-
if (body) await execution.notify({
|
|
10
|
-
body,
|
|
11
|
-
level: "info"
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
});
|
|
15
|
-
//#endregion
|
|
16
|
-
//#region src/extensions/workspaces/sessions/(backend)/command/notify.server.ts
|
|
17
|
-
var notify_server_default = defineServerCommand(notificationCommand);
|
|
18
|
-
//#endregion
|
|
19
|
-
//#region generated/server.ts
|
|
20
|
-
const at = (value, context) => typeof value === "function" ? value(context) : value;
|
|
21
|
-
const via = (identity, value) => value === void 0 ? void 0 : {
|
|
22
|
-
...identity,
|
|
23
|
-
...value
|
|
24
|
-
};
|
|
25
|
-
const facet = defineServerPlugin({
|
|
26
|
-
name: "@agimon-ai/po-notification",
|
|
27
|
-
session: (context) => ({ get commands() {
|
|
28
|
-
return [via({ name: "notify" }, at(notify_server_default, context))];
|
|
29
|
-
} })
|
|
30
|
-
});
|
|
31
|
-
//#endregion
|
|
32
|
-
export { facet as default, facet };
|
|
33
|
-
|
|
34
|
-
//# sourceMappingURL=server.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"server.mjs","names":["commandNotify"],"sources":["../../src/extensions/workspaces/sessions/(backend)/_lib/notificationCommand.ts","../../src/extensions/workspaces/sessions/(backend)/command/notify.server.ts","../../generated/server.ts"],"sourcesContent":["import { defineCommand } from '@agimon-ai/po-core/serverFacet';\n\nexport const notificationCommand = defineCommand({\n name: 'notify',\n description: 'Send a notification through the active headless client.',\n async execute(args, execution) {\n const body = args.trim();\n if (body) await execution.notify({ body, level: 'info' });\n },\n});\n","import { defineServerCommand } from '@agimon-ai/po-core/extensionFile';\n\nimport { notificationCommand } from '../_lib/notificationCommand';\n\nexport default defineServerCommand(notificationCommand);\n","// Generated by @agimon-ai/po-build. Do not edit by hand.\nimport { defineServerPlugin, type DoomServerSessionPlugin } from '@agimon-ai/po-core/serverFacet';\n\nimport commandNotify from '../src/extensions/workspaces/sessions/(backend)/command/notify.server';\n\ntype Factory<T, C> = (context: C) => T;\nconst at = <T, C>(value: T | Factory<T, C>, context: C): T =>\n typeof value === 'function' ? (value as Factory<T, C>)(context) : value;\nconst via = <T>(identity: Record<string, unknown>, value: unknown): T =>\n (value === undefined ? undefined : { ...identity, ...(value as object) }) as T;\n\nexport const facet = defineServerPlugin({\n name: '@agimon-ai/po-notification',\n session: (context) => ({\n get commands(): DoomServerSessionPlugin['commands'] { return [via({ name: 'notify' }, at(commandNotify, context))]; },\n }),\n});\n\nexport default facet;\n"],"mappings":";;;AAEA,MAAa,sBAAsB,cAAc;CAC/C,MAAM;CACN,aAAa;CACb,MAAM,QAAQ,MAAM,WAAW;EAC7B,MAAM,OAAO,KAAK,KAAK;EACvB,IAAI,MAAM,MAAM,UAAU,OAAO;GAAE;GAAM,OAAO;EAAO,CAAC;CAC1D;AACF,CAAC;;;ACLD,IAAA,wBAAe,oBAAoB,mBAAmB;;;ACEtD,MAAM,MAAY,OAA0B,YAC1C,OAAO,UAAU,aAAc,MAAwB,OAAO,IAAI;AACpE,MAAM,OAAU,UAAmC,UAChD,UAAU,KAAA,IAAY,KAAA,IAAY;CAAE,GAAG;CAAU,GAAI;AAAiB;AAEzE,MAAa,QAAQ,mBAAmB;CACtC,MAAM;CACN,UAAU,aAAa,EACrB,IAAI,WAAgD;EAAE,OAAO,CAAC,IAAI,EAAE,MAAM,SAAS,GAAG,GAAGA,uBAAe,OAAO,CAAC,CAAC;CAAG,EACtH;AACF,CAAC"}
|
package/dist/index.cjs
DELETED
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
//#region \0rolldown/runtime.js
|
|
3
|
-
var __create = Object.create;
|
|
4
|
-
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
-
var __copyProps = (to, from, except, desc) => {
|
|
10
|
-
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
-
key = keys[i];
|
|
12
|
-
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
-
get: ((k) => from[k]).bind(null, key),
|
|
14
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
return to;
|
|
18
|
-
};
|
|
19
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
20
|
-
value: mod,
|
|
21
|
-
enumerable: true
|
|
22
|
-
}) : target, mod));
|
|
23
|
-
//#endregion
|
|
24
|
-
let node_worker_threads = require("node:worker_threads");
|
|
25
|
-
let node_path = require("node:path");
|
|
26
|
-
node_path = __toESM(node_path, 1);
|
|
27
|
-
//#region src/services/shellTitleController/index.ts
|
|
28
|
-
const BRAILLE_FRAMES = [
|
|
29
|
-
"⠋",
|
|
30
|
-
"⠙",
|
|
31
|
-
"⠹",
|
|
32
|
-
"⠸",
|
|
33
|
-
"⠼",
|
|
34
|
-
"⠴",
|
|
35
|
-
"⠦",
|
|
36
|
-
"⠧",
|
|
37
|
-
"⠇",
|
|
38
|
-
"⠏"
|
|
39
|
-
];
|
|
40
|
-
const TITLE_FRAME_INTERVAL_MS = 80;
|
|
41
|
-
/**
|
|
42
|
-
* The animator runs off the main thread so a busy agent turn cannot stall the
|
|
43
|
-
* spinner. It is an inline eval source rather than a shipped file because the
|
|
44
|
-
* package is bundled per entry, and a separate worker module would have to be
|
|
45
|
-
* located on disk at runtime.
|
|
46
|
-
*/
|
|
47
|
-
const TITLE_WORKER_SOURCE = `
|
|
48
|
-
const { writeSync } = require('node:fs');
|
|
49
|
-
const { parentPort, workerData } = require('node:worker_threads');
|
|
50
|
-
let frameIndex = 0;
|
|
51
|
-
let timer;
|
|
52
|
-
const writeTitle = (title) => writeSync(1, '\\u001b]0;' + title + '\\u0007');
|
|
53
|
-
const stop = () => {
|
|
54
|
-
if (timer) clearInterval(timer);
|
|
55
|
-
timer = undefined;
|
|
56
|
-
frameIndex = 0;
|
|
57
|
-
};
|
|
58
|
-
parentPort.on('message', (command) => {
|
|
59
|
-
stop();
|
|
60
|
-
if (command.action === 'start') {
|
|
61
|
-
const render = () => {
|
|
62
|
-
writeTitle(workerData.frames[frameIndex % workerData.frames.length] + ' ' + command.title);
|
|
63
|
-
frameIndex += 1;
|
|
64
|
-
};
|
|
65
|
-
render();
|
|
66
|
-
timer = setInterval(render, workerData.intervalMs);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
writeTitle(command.title);
|
|
70
|
-
if (command.action === 'dispose') parentPort.close();
|
|
71
|
-
});
|
|
72
|
-
`;
|
|
73
|
-
/** Animates on the main thread, for hosts where a worker could not be started. */
|
|
74
|
-
function createMainThreadTitleController() {
|
|
75
|
-
let frameIndex = 0;
|
|
76
|
-
let timer;
|
|
77
|
-
const stopAnimation = () => {
|
|
78
|
-
if (timer) clearInterval(timer);
|
|
79
|
-
timer = void 0;
|
|
80
|
-
frameIndex = 0;
|
|
81
|
-
};
|
|
82
|
-
return {
|
|
83
|
-
set(title, write) {
|
|
84
|
-
stopAnimation();
|
|
85
|
-
write(title);
|
|
86
|
-
},
|
|
87
|
-
start(title, write) {
|
|
88
|
-
stopAnimation();
|
|
89
|
-
const render = () => {
|
|
90
|
-
write(`${BRAILLE_FRAMES[frameIndex % BRAILLE_FRAMES.length]} ${title}`);
|
|
91
|
-
frameIndex += 1;
|
|
92
|
-
};
|
|
93
|
-
render();
|
|
94
|
-
timer = setInterval(render, TITLE_FRAME_INTERVAL_MS);
|
|
95
|
-
},
|
|
96
|
-
stop(title, write) {
|
|
97
|
-
stopAnimation();
|
|
98
|
-
write(title);
|
|
99
|
-
},
|
|
100
|
-
dispose(title, write) {
|
|
101
|
-
stopAnimation();
|
|
102
|
-
write(title);
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Animates from a worker thread, replaying the last command on the main thread
|
|
108
|
-
* if the worker never started, failed, or exited under us. The worker is
|
|
109
|
-
* unreferenced so a pending frame never holds the process open.
|
|
110
|
-
*/
|
|
111
|
-
function createWorkerTitleController() {
|
|
112
|
-
const fallback = createMainThreadTitleController();
|
|
113
|
-
let lastCommand;
|
|
114
|
-
let lastWrite;
|
|
115
|
-
let worker;
|
|
116
|
-
const fallBack = () => {
|
|
117
|
-
worker = void 0;
|
|
118
|
-
if (!lastCommand || !lastWrite) return;
|
|
119
|
-
fallback[lastCommand.action](lastCommand.title, lastWrite);
|
|
120
|
-
};
|
|
121
|
-
try {
|
|
122
|
-
const titleWorker = new node_worker_threads.Worker(TITLE_WORKER_SOURCE, {
|
|
123
|
-
eval: true,
|
|
124
|
-
workerData: {
|
|
125
|
-
frames: BRAILLE_FRAMES,
|
|
126
|
-
intervalMs: TITLE_FRAME_INTERVAL_MS
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
titleWorker.unref();
|
|
130
|
-
titleWorker.once("error", fallBack);
|
|
131
|
-
titleWorker.once("exit", (code) => {
|
|
132
|
-
if (code !== 0 && worker === titleWorker) fallBack();
|
|
133
|
-
});
|
|
134
|
-
worker = titleWorker;
|
|
135
|
-
} catch {
|
|
136
|
-
worker = void 0;
|
|
137
|
-
}
|
|
138
|
-
const dispatch = (command, write) => {
|
|
139
|
-
lastCommand = command;
|
|
140
|
-
lastWrite = write;
|
|
141
|
-
if (!worker) {
|
|
142
|
-
fallback[command.action](command.title, write);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
try {
|
|
146
|
-
worker.postMessage(command);
|
|
147
|
-
} catch {
|
|
148
|
-
fallBack();
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
return {
|
|
152
|
-
set: (title, write) => dispatch({
|
|
153
|
-
action: "set",
|
|
154
|
-
title
|
|
155
|
-
}, write),
|
|
156
|
-
start: (title, write) => dispatch({
|
|
157
|
-
action: "start",
|
|
158
|
-
title
|
|
159
|
-
}, write),
|
|
160
|
-
stop: (title, write) => dispatch({
|
|
161
|
-
action: "stop",
|
|
162
|
-
title
|
|
163
|
-
}, write),
|
|
164
|
-
dispose: (title, write) => dispatch({
|
|
165
|
-
action: "dispose",
|
|
166
|
-
title
|
|
167
|
-
}, write)
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
//#endregion
|
|
171
|
-
//#region src/services/notificationText/index.ts
|
|
172
|
-
const ATTENTION_SUBTITLE = "Approval or feedback required";
|
|
173
|
-
const ATTENTION_TITLE = "Pi needs your input";
|
|
174
|
-
const ELLIPSIS = "…";
|
|
175
|
-
const MAX_BODY_LENGTH = 240;
|
|
176
|
-
const MAX_PROMPT_TITLE_LENGTH = 36;
|
|
177
|
-
const SETTLED_BODY = "The agent finished its work and is waiting for you.";
|
|
178
|
-
const SETTLED_TITLE = "Pi finished";
|
|
179
|
-
const SHELL_TITLE_SEPARATOR = " - ";
|
|
180
|
-
const SHELL_TITLE_PREFIX = "π";
|
|
181
|
-
function collapseWhitespace(value) {
|
|
182
|
-
return value.replace(/\s+/g, " ").trim();
|
|
183
|
-
}
|
|
184
|
-
function truncate(value, maxLength) {
|
|
185
|
-
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}${ELLIPSIS}` : value;
|
|
186
|
-
}
|
|
187
|
-
/** The first user prompt, shortened to fit a shell tab. A blank prompt names nothing. */
|
|
188
|
-
function promptTitle(prompt) {
|
|
189
|
-
const normalized = collapseWhitespace(prompt);
|
|
190
|
-
return normalized ? truncate(normalized, MAX_PROMPT_TITLE_LENGTH) : void 0;
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* The idle shell-tab title: `π - <label> - <repository>`.
|
|
194
|
-
*
|
|
195
|
-
* The session name wins over the first prompt because the user chose it. With
|
|
196
|
-
* neither, the repository alone still tells two tabs apart.
|
|
197
|
-
*/
|
|
198
|
-
function shellTabTitle({ cwd, sessionName, prompt }) {
|
|
199
|
-
const repository = node_path.default.basename(cwd);
|
|
200
|
-
const label = sessionName ?? prompt;
|
|
201
|
-
return label ? `${SHELL_TITLE_PREFIX}${SHELL_TITLE_SEPARATOR}${label}${SHELL_TITLE_SEPARATOR}${repository}` : `${SHELL_TITLE_PREFIX}${SHELL_TITLE_SEPARATOR}${repository}`;
|
|
202
|
-
}
|
|
203
|
-
/** Notification bodies are one line in a system panel, so they collapse and truncate. */
|
|
204
|
-
function notificationBody(body) {
|
|
205
|
-
return truncate(collapseWhitespace(body), MAX_BODY_LENGTH);
|
|
206
|
-
}
|
|
207
|
-
/** Sent while the agent holds the turn and something is blocking on the user. */
|
|
208
|
-
function attentionNotification(body) {
|
|
209
|
-
return {
|
|
210
|
-
title: ATTENTION_TITLE,
|
|
211
|
-
subtitle: ATTENTION_SUBTITLE,
|
|
212
|
-
body
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
/** Sent once a run has stopped, subtitled with whichever name the user recognises. */
|
|
216
|
-
function settledNotification({ cwd, sessionName }) {
|
|
217
|
-
return {
|
|
218
|
-
title: SETTLED_TITLE,
|
|
219
|
-
subtitle: sessionName ?? node_path.default.basename(cwd),
|
|
220
|
-
body: SETTLED_BODY
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
//#endregion
|
|
224
|
-
//#region src/services/systemNotification/index.ts
|
|
225
|
-
const CMUX_COMMAND = "cmux";
|
|
226
|
-
const COMMAND_TIMEOUT_MS = 3e3;
|
|
227
|
-
const DARWIN_PLATFORM = "darwin";
|
|
228
|
-
const OSASCRIPT_COMMAND = "osascript";
|
|
229
|
-
const OSASCRIPT_EXPRESSION_FLAG = "-e";
|
|
230
|
-
async function execute(pi, command, args) {
|
|
231
|
-
try {
|
|
232
|
-
return (await pi.exec(command, args, { timeout: COMMAND_TIMEOUT_MS })).code === 0;
|
|
233
|
-
} catch {
|
|
234
|
-
return false;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Delivers one desktop notification on a best-effort basis.
|
|
239
|
-
*
|
|
240
|
-
* cmux is tried first because it routes the notification to the window the
|
|
241
|
-
* session actually lives in. Only macOS has a second option worth attempting,
|
|
242
|
-
* and a host with neither stays silent rather than failing the turn.
|
|
243
|
-
*/
|
|
244
|
-
async function sendSystemNotification(pi, notification, platform = process.platform) {
|
|
245
|
-
const body = notificationBody(notification.body);
|
|
246
|
-
if (await execute(pi, CMUX_COMMAND, [
|
|
247
|
-
"notify",
|
|
248
|
-
"--title",
|
|
249
|
-
notification.title,
|
|
250
|
-
"--subtitle",
|
|
251
|
-
notification.subtitle,
|
|
252
|
-
"--body",
|
|
253
|
-
body
|
|
254
|
-
]) || platform !== DARWIN_PLATFORM) return;
|
|
255
|
-
await execute(pi, OSASCRIPT_COMMAND, [
|
|
256
|
-
OSASCRIPT_EXPRESSION_FLAG,
|
|
257
|
-
"on run argv",
|
|
258
|
-
OSASCRIPT_EXPRESSION_FLAG,
|
|
259
|
-
"display notification (item 3 of argv) with title (item 1 of argv) subtitle (item 2 of argv)",
|
|
260
|
-
OSASCRIPT_EXPRESSION_FLAG,
|
|
261
|
-
"end run",
|
|
262
|
-
notification.title,
|
|
263
|
-
notification.subtitle,
|
|
264
|
-
body
|
|
265
|
-
]);
|
|
266
|
-
}
|
|
267
|
-
//#endregion
|
|
268
|
-
//#region src/services/notificationPolicy/index.ts
|
|
269
|
-
const DEFAULT_ASK_USER_BODY = "The agent is waiting for your feedback.";
|
|
270
|
-
const RPC_MODE = "rpc";
|
|
271
|
-
/**
|
|
272
|
-
* A dialog is worth interrupting the user for only while the agent holds the turn.
|
|
273
|
-
*
|
|
274
|
-
* Outside a run the user opened the dialog themselves and is already looking at
|
|
275
|
-
* it. An ask-user prompt is announced by its own event, so the dialog it then
|
|
276
|
-
* opens must stay silent or the same question notifies twice.
|
|
277
|
-
*/
|
|
278
|
-
function warrantsAttentionNotification({ agentRunning, askUserBlocked }) {
|
|
279
|
-
return agentRunning && !askUserBlocked;
|
|
280
|
-
}
|
|
281
|
-
/** A queued follow-up means the agent keeps working, so the run has not stopped for the user. */
|
|
282
|
-
function warrantsSettledNotification(hasPendingMessages) {
|
|
283
|
-
return !hasPendingMessages;
|
|
284
|
-
}
|
|
285
|
-
/** Terminal title escapes only mean something on an attached terminal; RPC output stays clean. */
|
|
286
|
-
function supportsShellTitle({ hasUI, mode }) {
|
|
287
|
-
return hasUI && mode !== RPC_MODE;
|
|
288
|
-
}
|
|
289
|
-
/** The question an ask-user prompt is blocking on, or a stand-in when it asked none. */
|
|
290
|
-
function askUserPromptBody(questions) {
|
|
291
|
-
return questions[0]?.question ?? DEFAULT_ASK_USER_BODY;
|
|
292
|
-
}
|
|
293
|
-
//#endregion
|
|
294
|
-
exports.askUserPromptBody = askUserPromptBody;
|
|
295
|
-
exports.attentionNotification = attentionNotification;
|
|
296
|
-
exports.createMainThreadTitleController = createMainThreadTitleController;
|
|
297
|
-
exports.createWorkerTitleController = createWorkerTitleController;
|
|
298
|
-
exports.notificationBody = notificationBody;
|
|
299
|
-
exports.promptTitle = promptTitle;
|
|
300
|
-
exports.sendSystemNotification = sendSystemNotification;
|
|
301
|
-
exports.settledNotification = settledNotification;
|
|
302
|
-
exports.shellTabTitle = shellTabTitle;
|
|
303
|
-
exports.supportsShellTitle = supportsShellTitle;
|
|
304
|
-
exports.warrantsAttentionNotification = warrantsAttentionNotification;
|
|
305
|
-
exports.warrantsSettledNotification = warrantsSettledNotification;
|
|
306
|
-
|
|
307
|
-
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["Worker","path"],"sources":["../src/services/shellTitleController/index.ts","../src/services/notificationText/index.ts","../src/services/systemNotification/index.ts","../src/services/notificationPolicy/index.ts"],"sourcesContent":["import { Worker } from 'node:worker_threads';\n\nimport type { ShellTitleCommand, ShellTitleController, WriteTitle } from '../../types/notifications';\n\nconst BRAILLE_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];\nconst TITLE_FRAME_INTERVAL_MS = 80;\n/**\n * The animator runs off the main thread so a busy agent turn cannot stall the\n * spinner. It is an inline eval source rather than a shipped file because the\n * package is bundled per entry, and a separate worker module would have to be\n * located on disk at runtime.\n */\nconst TITLE_WORKER_SOURCE = `\nconst { writeSync } = require('node:fs');\nconst { parentPort, workerData } = require('node:worker_threads');\nlet frameIndex = 0;\nlet timer;\nconst writeTitle = (title) => writeSync(1, '\\\\u001b]0;' + title + '\\\\u0007');\nconst stop = () => {\n if (timer) clearInterval(timer);\n timer = undefined;\n frameIndex = 0;\n};\nparentPort.on('message', (command) => {\n stop();\n if (command.action === 'start') {\n const render = () => {\n writeTitle(workerData.frames[frameIndex % workerData.frames.length] + ' ' + command.title);\n frameIndex += 1;\n };\n render();\n timer = setInterval(render, workerData.intervalMs);\n return;\n }\n writeTitle(command.title);\n if (command.action === 'dispose') parentPort.close();\n});\n`;\n\n/** Animates on the main thread, for hosts where a worker could not be started. */\nexport function createMainThreadTitleController(): ShellTitleController {\n let frameIndex = 0;\n let timer: ReturnType<typeof setInterval> | undefined;\n const stopAnimation = () => {\n if (timer) clearInterval(timer);\n timer = undefined;\n frameIndex = 0;\n };\n\n return {\n set(title, write) {\n stopAnimation();\n write(title);\n },\n start(title, write) {\n stopAnimation();\n const render = () => {\n write(`${BRAILLE_FRAMES[frameIndex % BRAILLE_FRAMES.length]} ${title}`);\n frameIndex += 1;\n };\n render();\n timer = setInterval(render, TITLE_FRAME_INTERVAL_MS);\n },\n stop(title, write) {\n stopAnimation();\n write(title);\n },\n dispose(title, write) {\n stopAnimation();\n write(title);\n },\n };\n}\n\n/**\n * Animates from a worker thread, replaying the last command on the main thread\n * if the worker never started, failed, or exited under us. The worker is\n * unreferenced so a pending frame never holds the process open.\n */\nexport function createWorkerTitleController(): ShellTitleController {\n const fallback = createMainThreadTitleController();\n let lastCommand: ShellTitleCommand | undefined;\n let lastWrite: WriteTitle | undefined;\n let worker: Worker | undefined;\n\n const fallBack = () => {\n worker = undefined;\n if (!lastCommand || !lastWrite) return;\n fallback[lastCommand.action](lastCommand.title, lastWrite);\n };\n\n try {\n const titleWorker = new Worker(TITLE_WORKER_SOURCE, {\n eval: true,\n workerData: { frames: BRAILLE_FRAMES, intervalMs: TITLE_FRAME_INTERVAL_MS },\n });\n titleWorker.unref();\n titleWorker.once('error', fallBack);\n titleWorker.once('exit', (code) => {\n if (code !== 0 && worker === titleWorker) fallBack();\n });\n worker = titleWorker;\n } catch {\n // Workers are unavailable on this host, which the main-thread animator covers.\n worker = undefined;\n }\n\n const dispatch = (command: ShellTitleCommand, write: WriteTitle) => {\n lastCommand = command;\n lastWrite = write;\n if (!worker) {\n fallback[command.action](command.title, write);\n return;\n }\n try {\n worker.postMessage(command);\n } catch {\n // The worker closed between the last frame and this command.\n fallBack();\n }\n };\n\n return {\n set: (title, write) => dispatch({ action: 'set', title }, write),\n start: (title, write) => dispatch({ action: 'start', title }, write),\n stop: (title, write) => dispatch({ action: 'stop', title }, write),\n dispose: (title, write) => dispatch({ action: 'dispose', title }, write),\n };\n}\n","import path from 'node:path';\n\nimport type { DesktopNotification } from '../../types/notifications';\n\nconst ATTENTION_SUBTITLE = 'Approval or feedback required';\nconst ATTENTION_TITLE = 'Pi needs your input';\nconst ELLIPSIS = '…';\nconst MAX_BODY_LENGTH = 240;\nconst MAX_PROMPT_TITLE_LENGTH = 36;\nconst SETTLED_BODY = 'The agent finished its work and is waiting for you.';\nconst SETTLED_TITLE = 'Pi finished';\nconst SHELL_TITLE_SEPARATOR = ' - ';\nconst SHELL_TITLE_PREFIX = 'π';\n\nfunction collapseWhitespace(value: string): string {\n return value.replace(/\\s+/g, ' ').trim();\n}\n\nfunction truncate(value: string, maxLength: number): string {\n return value.length > maxLength ? `${value.slice(0, maxLength - 1)}${ELLIPSIS}` : value;\n}\n\n/** The first user prompt, shortened to fit a shell tab. A blank prompt names nothing. */\nexport function promptTitle(prompt: string): string | undefined {\n const normalized = collapseWhitespace(prompt);\n return normalized ? truncate(normalized, MAX_PROMPT_TITLE_LENGTH) : undefined;\n}\n\nexport interface ShellTabTitleInput {\n cwd: string;\n sessionName?: string;\n prompt?: string;\n}\n\n/**\n * The idle shell-tab title: `π - <label> - <repository>`.\n *\n * The session name wins over the first prompt because the user chose it. With\n * neither, the repository alone still tells two tabs apart.\n */\nexport function shellTabTitle({ cwd, sessionName, prompt }: ShellTabTitleInput): string {\n const repository = path.basename(cwd);\n const label = sessionName ?? prompt;\n return label\n ? `${SHELL_TITLE_PREFIX}${SHELL_TITLE_SEPARATOR}${label}${SHELL_TITLE_SEPARATOR}${repository}`\n : `${SHELL_TITLE_PREFIX}${SHELL_TITLE_SEPARATOR}${repository}`;\n}\n\n/** Notification bodies are one line in a system panel, so they collapse and truncate. */\nexport function notificationBody(body: string): string {\n return truncate(collapseWhitespace(body), MAX_BODY_LENGTH);\n}\n\n/** Sent while the agent holds the turn and something is blocking on the user. */\nexport function attentionNotification(body: string): DesktopNotification {\n return { title: ATTENTION_TITLE, subtitle: ATTENTION_SUBTITLE, body };\n}\n\n/** Sent once a run has stopped, subtitled with whichever name the user recognises. */\nexport function settledNotification({ cwd, sessionName }: { cwd: string; sessionName?: string }): DesktopNotification {\n return { title: SETTLED_TITLE, subtitle: sessionName ?? path.basename(cwd), body: SETTLED_BODY };\n}\n","import type { DesktopNotification, NotificationExecutor } from '../../types/notifications';\nimport { notificationBody } from '../notificationText';\n\nconst CMUX_COMMAND = 'cmux';\nconst COMMAND_TIMEOUT_MS = 3_000;\nconst DARWIN_PLATFORM = 'darwin';\nconst OSASCRIPT_COMMAND = 'osascript';\nconst OSASCRIPT_EXPRESSION_FLAG = '-e';\n\nasync function execute(pi: NotificationExecutor, command: string, args: string[]): Promise<boolean> {\n try {\n const result = await pi.exec(command, args, { timeout: COMMAND_TIMEOUT_MS });\n return result.code === 0;\n } catch {\n // A notifier that is not installed is the ordinary case, not a fault: report\n // \"not delivered\" and let the caller decide whether another one exists.\n return false;\n }\n}\n\n/**\n * Delivers one desktop notification on a best-effort basis.\n *\n * cmux is tried first because it routes the notification to the window the\n * session actually lives in. Only macOS has a second option worth attempting,\n * and a host with neither stays silent rather than failing the turn.\n */\nexport async function sendSystemNotification(\n pi: NotificationExecutor,\n notification: DesktopNotification,\n platform: string = process.platform,\n): Promise<void> {\n const body = notificationBody(notification.body);\n const sentToCmux = await execute(pi, CMUX_COMMAND, [\n 'notify',\n '--title',\n notification.title,\n '--subtitle',\n notification.subtitle,\n '--body',\n body,\n ]);\n if (sentToCmux || platform !== DARWIN_PLATFORM) return;\n\n await execute(pi, OSASCRIPT_COMMAND, [\n OSASCRIPT_EXPRESSION_FLAG,\n 'on run argv',\n OSASCRIPT_EXPRESSION_FLAG,\n 'display notification (item 3 of argv) with title (item 1 of argv) subtitle (item 2 of argv)',\n OSASCRIPT_EXPRESSION_FLAG,\n 'end run',\n notification.title,\n notification.subtitle,\n body,\n ]);\n}\n","const DEFAULT_ASK_USER_BODY = 'The agent is waiting for your feedback.';\nconst RPC_MODE = 'rpc';\n\nexport interface AttentionState {\n agentRunning: boolean;\n askUserBlocked: boolean;\n}\n\nexport interface ShellSurface {\n hasUI: boolean;\n mode?: string;\n}\n\n/**\n * A dialog is worth interrupting the user for only while the agent holds the turn.\n *\n * Outside a run the user opened the dialog themselves and is already looking at\n * it. An ask-user prompt is announced by its own event, so the dialog it then\n * opens must stay silent or the same question notifies twice.\n */\nexport function warrantsAttentionNotification({ agentRunning, askUserBlocked }: AttentionState): boolean {\n return agentRunning && !askUserBlocked;\n}\n\n/** A queued follow-up means the agent keeps working, so the run has not stopped for the user. */\nexport function warrantsSettledNotification(hasPendingMessages: boolean): boolean {\n return !hasPendingMessages;\n}\n\n/** Terminal title escapes only mean something on an attached terminal; RPC output stays clean. */\nexport function supportsShellTitle({ hasUI, mode }: ShellSurface): boolean {\n return hasUI && mode !== RPC_MODE;\n}\n\n/** The question an ask-user prompt is blocking on, or a stand-in when it asked none. */\nexport function askUserPromptBody(questions: readonly { question: string }[]): string {\n return questions[0]?.question ?? DEFAULT_ASK_USER_BODY;\n}\n"],"mappings":"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"}
|
package/dist/index.d.mts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
//#region src/types/notifications.d.ts
|
|
2
|
-
interface NotificationExtensionOptions {
|
|
3
|
-
/** Replaces the worker-thread animator, which a host without worker threads cannot start. */
|
|
4
|
-
titleController?: ShellTitleController;
|
|
5
|
-
environment?: NodeJS.ProcessEnv;
|
|
6
|
-
}
|
|
7
|
-
interface NotificationExecutor {
|
|
8
|
-
exec(command: string, args: string[], options: {
|
|
9
|
-
timeout: number;
|
|
10
|
-
}): Promise<{
|
|
11
|
-
code: number | null;
|
|
12
|
-
}>;
|
|
13
|
-
}
|
|
14
|
-
/** Writes a shell-tab title to whatever terminal surface the host owns. */
|
|
15
|
-
type WriteTitle = (title: string) => void;
|
|
16
|
-
/** The four things a title controller can be asked to do, and its method names. */
|
|
17
|
-
type ShellTitleAction = 'dispose' | 'set' | 'start' | 'stop';
|
|
18
|
-
/** One instruction for a title controller, and the message shape its worker consumes. */
|
|
19
|
-
interface ShellTitleCommand {
|
|
20
|
-
action: ShellTitleAction;
|
|
21
|
-
title: string;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Drives the shell tab title across a run.
|
|
25
|
-
*
|
|
26
|
-
* `start` animates a spinner in front of the title until `stop` restores the
|
|
27
|
-
* idle one; `set` writes a title without animating; `dispose` writes the final
|
|
28
|
-
* title and releases whatever machinery the implementation owns. Every method
|
|
29
|
-
* takes the writer so the controller never has to hold a session reference.
|
|
30
|
-
*/
|
|
31
|
-
interface ShellTitleController {
|
|
32
|
-
set(title: string, write: WriteTitle): void;
|
|
33
|
-
start(title: string, write: WriteTitle): void;
|
|
34
|
-
stop(title: string, write: WriteTitle): void;
|
|
35
|
-
dispose(title: string, write: WriteTitle): void;
|
|
36
|
-
}
|
|
37
|
-
/** One OS-level desktop notification. */
|
|
38
|
-
interface DesktopNotification {
|
|
39
|
-
title: string;
|
|
40
|
-
subtitle: string;
|
|
41
|
-
body: string;
|
|
42
|
-
}
|
|
43
|
-
//#endregion
|
|
44
|
-
//#region src/services/shellTitleController/index.d.ts
|
|
45
|
-
/** Animates on the main thread, for hosts where a worker could not be started. */
|
|
46
|
-
export declare function createMainThreadTitleController(): ShellTitleController;
|
|
47
|
-
/**
|
|
48
|
-
* Animates from a worker thread, replaying the last command on the main thread
|
|
49
|
-
* if the worker never started, failed, or exited under us. The worker is
|
|
50
|
-
* unreferenced so a pending frame never holds the process open.
|
|
51
|
-
*/
|
|
52
|
-
export declare function createWorkerTitleController(): ShellTitleController;
|
|
53
|
-
//#endregion
|
|
54
|
-
//#region src/services/systemNotification/index.d.ts
|
|
55
|
-
/**
|
|
56
|
-
* Delivers one desktop notification on a best-effort basis.
|
|
57
|
-
*
|
|
58
|
-
* cmux is tried first because it routes the notification to the window the
|
|
59
|
-
* session actually lives in. Only macOS has a second option worth attempting,
|
|
60
|
-
* and a host with neither stays silent rather than failing the turn.
|
|
61
|
-
*/
|
|
62
|
-
export declare function sendSystemNotification(pi: NotificationExecutor, notification: DesktopNotification, platform?: string): Promise<void>;
|
|
63
|
-
//#endregion
|
|
64
|
-
//#region src/services/notificationPolicy/index.d.ts
|
|
65
|
-
interface AttentionState {
|
|
66
|
-
agentRunning: boolean;
|
|
67
|
-
askUserBlocked: boolean;
|
|
68
|
-
}
|
|
69
|
-
interface ShellSurface {
|
|
70
|
-
hasUI: boolean;
|
|
71
|
-
mode?: string;
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* A dialog is worth interrupting the user for only while the agent holds the turn.
|
|
75
|
-
*
|
|
76
|
-
* Outside a run the user opened the dialog themselves and is already looking at
|
|
77
|
-
* it. An ask-user prompt is announced by its own event, so the dialog it then
|
|
78
|
-
* opens must stay silent or the same question notifies twice.
|
|
79
|
-
*/
|
|
80
|
-
export declare function warrantsAttentionNotification({ agentRunning, askUserBlocked }: AttentionState): boolean;
|
|
81
|
-
/** A queued follow-up means the agent keeps working, so the run has not stopped for the user. */
|
|
82
|
-
export declare function warrantsSettledNotification(hasPendingMessages: boolean): boolean;
|
|
83
|
-
/** Terminal title escapes only mean something on an attached terminal; RPC output stays clean. */
|
|
84
|
-
export declare function supportsShellTitle({ hasUI, mode }: ShellSurface): boolean;
|
|
85
|
-
/** The question an ask-user prompt is blocking on, or a stand-in when it asked none. */
|
|
86
|
-
export declare function askUserPromptBody(questions: readonly {
|
|
87
|
-
question: string;
|
|
88
|
-
}[]): string;
|
|
89
|
-
//#endregion
|
|
90
|
-
//#region src/services/notificationText/index.d.ts
|
|
91
|
-
/** The first user prompt, shortened to fit a shell tab. A blank prompt names nothing. */
|
|
92
|
-
export declare function promptTitle(prompt: string): string | undefined;
|
|
93
|
-
interface ShellTabTitleInput {
|
|
94
|
-
cwd: string;
|
|
95
|
-
sessionName?: string;
|
|
96
|
-
prompt?: string;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* The idle shell-tab title: `π - <label> - <repository>`.
|
|
100
|
-
*
|
|
101
|
-
* The session name wins over the first prompt because the user chose it. With
|
|
102
|
-
* neither, the repository alone still tells two tabs apart.
|
|
103
|
-
*/
|
|
104
|
-
export declare function shellTabTitle({ cwd, sessionName, prompt }: ShellTabTitleInput): string;
|
|
105
|
-
/** Notification bodies are one line in a system panel, so they collapse and truncate. */
|
|
106
|
-
export declare function notificationBody(body: string): string;
|
|
107
|
-
/** Sent while the agent holds the turn and something is blocking on the user. */
|
|
108
|
-
export declare function attentionNotification(body: string): DesktopNotification;
|
|
109
|
-
/** Sent once a run has stopped, subtitled with whichever name the user recognises. */
|
|
110
|
-
export declare function settledNotification({ cwd, sessionName }: {
|
|
111
|
-
cwd: string;
|
|
112
|
-
sessionName?: string;
|
|
113
|
-
}): DesktopNotification;
|
|
114
|
-
//#endregion
|
|
115
|
-
export type { AttentionState, DesktopNotification, NotificationExtensionOptions, ShellSurface, ShellTabTitleInput, ShellTitleAction, ShellTitleCommand, ShellTitleController, WriteTitle };
|
|
116
|
-
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types/notifications.ts","../src/services/shellTitleController/index.ts","../src/services/systemNotification/index.ts","../src/services/notificationPolicy/index.ts","../src/services/notificationText/index.ts"],"mappings":";UAAiB;;EAEf,kBAAkB;EAClB,cAAc,OAAO;;UAGN;EACf,KAAK,iBAAiB,gBAAgB;IAAW;MAAoB;IAAU;;;;KAIrE,cAAc;;KAGd;;UAGK;EACf,QAAQ;EACR;;;;;;;;;;UAWe;EACf,IAAI,eAAe,OAAO;EAC1B,MAAM,eAAe,OAAO;EAC5B,KAAK,eAAe,OAAO;EAC3B,QAAQ,eAAe,OAAO;;;UAIf;EACf;EACA;EACA;;;;;wBCDc,mCAAmC;;;;;;wBAuCnC,+BAA+B;;;;;;;;;;wBCpDzB,uBACpB,IAAI,sBACJ,cAAc,qBACd,oBACC;;;UC5Bc;EACf;EACA;;UAGe;EACf;EACA;;;;;;;;;wBAUc,gCAAgC,cAAc,kBAAkB;;wBAKhE,4BAA4B;;wBAK5B,qBAAqB,OAAO,QAAQ;;wBAKpC,kBAAkB;EAAsB;;;;;wBCZxC,YAAY;UAKX;EACf;EACA;EACA;;;;;;;;wBASc,gBAAgB,KAAK,aAAa,UAAU;;wBAS5C,iBAAiB;;wBAKjB,sBAAsB,eAAe;;wBAKrC,sBAAsB,KAAK;EAAiB;EAAa;IAAyB"}
|
package/dist/index.mjs
DELETED
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
import { Worker } from "node:worker_threads";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
//#region src/services/shellTitleController/index.ts
|
|
4
|
-
const BRAILLE_FRAMES = [
|
|
5
|
-
"⠋",
|
|
6
|
-
"⠙",
|
|
7
|
-
"⠹",
|
|
8
|
-
"⠸",
|
|
9
|
-
"⠼",
|
|
10
|
-
"⠴",
|
|
11
|
-
"⠦",
|
|
12
|
-
"⠧",
|
|
13
|
-
"⠇",
|
|
14
|
-
"⠏"
|
|
15
|
-
];
|
|
16
|
-
const TITLE_FRAME_INTERVAL_MS = 80;
|
|
17
|
-
/**
|
|
18
|
-
* The animator runs off the main thread so a busy agent turn cannot stall the
|
|
19
|
-
* spinner. It is an inline eval source rather than a shipped file because the
|
|
20
|
-
* package is bundled per entry, and a separate worker module would have to be
|
|
21
|
-
* located on disk at runtime.
|
|
22
|
-
*/
|
|
23
|
-
const TITLE_WORKER_SOURCE = `
|
|
24
|
-
const { writeSync } = require('node:fs');
|
|
25
|
-
const { parentPort, workerData } = require('node:worker_threads');
|
|
26
|
-
let frameIndex = 0;
|
|
27
|
-
let timer;
|
|
28
|
-
const writeTitle = (title) => writeSync(1, '\\u001b]0;' + title + '\\u0007');
|
|
29
|
-
const stop = () => {
|
|
30
|
-
if (timer) clearInterval(timer);
|
|
31
|
-
timer = undefined;
|
|
32
|
-
frameIndex = 0;
|
|
33
|
-
};
|
|
34
|
-
parentPort.on('message', (command) => {
|
|
35
|
-
stop();
|
|
36
|
-
if (command.action === 'start') {
|
|
37
|
-
const render = () => {
|
|
38
|
-
writeTitle(workerData.frames[frameIndex % workerData.frames.length] + ' ' + command.title);
|
|
39
|
-
frameIndex += 1;
|
|
40
|
-
};
|
|
41
|
-
render();
|
|
42
|
-
timer = setInterval(render, workerData.intervalMs);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
writeTitle(command.title);
|
|
46
|
-
if (command.action === 'dispose') parentPort.close();
|
|
47
|
-
});
|
|
48
|
-
`;
|
|
49
|
-
/** Animates on the main thread, for hosts where a worker could not be started. */
|
|
50
|
-
function createMainThreadTitleController() {
|
|
51
|
-
let frameIndex = 0;
|
|
52
|
-
let timer;
|
|
53
|
-
const stopAnimation = () => {
|
|
54
|
-
if (timer) clearInterval(timer);
|
|
55
|
-
timer = void 0;
|
|
56
|
-
frameIndex = 0;
|
|
57
|
-
};
|
|
58
|
-
return {
|
|
59
|
-
set(title, write) {
|
|
60
|
-
stopAnimation();
|
|
61
|
-
write(title);
|
|
62
|
-
},
|
|
63
|
-
start(title, write) {
|
|
64
|
-
stopAnimation();
|
|
65
|
-
const render = () => {
|
|
66
|
-
write(`${BRAILLE_FRAMES[frameIndex % BRAILLE_FRAMES.length]} ${title}`);
|
|
67
|
-
frameIndex += 1;
|
|
68
|
-
};
|
|
69
|
-
render();
|
|
70
|
-
timer = setInterval(render, TITLE_FRAME_INTERVAL_MS);
|
|
71
|
-
},
|
|
72
|
-
stop(title, write) {
|
|
73
|
-
stopAnimation();
|
|
74
|
-
write(title);
|
|
75
|
-
},
|
|
76
|
-
dispose(title, write) {
|
|
77
|
-
stopAnimation();
|
|
78
|
-
write(title);
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Animates from a worker thread, replaying the last command on the main thread
|
|
84
|
-
* if the worker never started, failed, or exited under us. The worker is
|
|
85
|
-
* unreferenced so a pending frame never holds the process open.
|
|
86
|
-
*/
|
|
87
|
-
function createWorkerTitleController() {
|
|
88
|
-
const fallback = createMainThreadTitleController();
|
|
89
|
-
let lastCommand;
|
|
90
|
-
let lastWrite;
|
|
91
|
-
let worker;
|
|
92
|
-
const fallBack = () => {
|
|
93
|
-
worker = void 0;
|
|
94
|
-
if (!lastCommand || !lastWrite) return;
|
|
95
|
-
fallback[lastCommand.action](lastCommand.title, lastWrite);
|
|
96
|
-
};
|
|
97
|
-
try {
|
|
98
|
-
const titleWorker = new Worker(TITLE_WORKER_SOURCE, {
|
|
99
|
-
eval: true,
|
|
100
|
-
workerData: {
|
|
101
|
-
frames: BRAILLE_FRAMES,
|
|
102
|
-
intervalMs: TITLE_FRAME_INTERVAL_MS
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
titleWorker.unref();
|
|
106
|
-
titleWorker.once("error", fallBack);
|
|
107
|
-
titleWorker.once("exit", (code) => {
|
|
108
|
-
if (code !== 0 && worker === titleWorker) fallBack();
|
|
109
|
-
});
|
|
110
|
-
worker = titleWorker;
|
|
111
|
-
} catch {
|
|
112
|
-
worker = void 0;
|
|
113
|
-
}
|
|
114
|
-
const dispatch = (command, write) => {
|
|
115
|
-
lastCommand = command;
|
|
116
|
-
lastWrite = write;
|
|
117
|
-
if (!worker) {
|
|
118
|
-
fallback[command.action](command.title, write);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
try {
|
|
122
|
-
worker.postMessage(command);
|
|
123
|
-
} catch {
|
|
124
|
-
fallBack();
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
return {
|
|
128
|
-
set: (title, write) => dispatch({
|
|
129
|
-
action: "set",
|
|
130
|
-
title
|
|
131
|
-
}, write),
|
|
132
|
-
start: (title, write) => dispatch({
|
|
133
|
-
action: "start",
|
|
134
|
-
title
|
|
135
|
-
}, write),
|
|
136
|
-
stop: (title, write) => dispatch({
|
|
137
|
-
action: "stop",
|
|
138
|
-
title
|
|
139
|
-
}, write),
|
|
140
|
-
dispose: (title, write) => dispatch({
|
|
141
|
-
action: "dispose",
|
|
142
|
-
title
|
|
143
|
-
}, write)
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
//#endregion
|
|
147
|
-
//#region src/services/notificationText/index.ts
|
|
148
|
-
const ATTENTION_SUBTITLE = "Approval or feedback required";
|
|
149
|
-
const ATTENTION_TITLE = "Pi needs your input";
|
|
150
|
-
const ELLIPSIS = "…";
|
|
151
|
-
const MAX_BODY_LENGTH = 240;
|
|
152
|
-
const MAX_PROMPT_TITLE_LENGTH = 36;
|
|
153
|
-
const SETTLED_BODY = "The agent finished its work and is waiting for you.";
|
|
154
|
-
const SETTLED_TITLE = "Pi finished";
|
|
155
|
-
const SHELL_TITLE_SEPARATOR = " - ";
|
|
156
|
-
const SHELL_TITLE_PREFIX = "π";
|
|
157
|
-
function collapseWhitespace(value) {
|
|
158
|
-
return value.replace(/\s+/g, " ").trim();
|
|
159
|
-
}
|
|
160
|
-
function truncate(value, maxLength) {
|
|
161
|
-
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}${ELLIPSIS}` : value;
|
|
162
|
-
}
|
|
163
|
-
/** The first user prompt, shortened to fit a shell tab. A blank prompt names nothing. */
|
|
164
|
-
function promptTitle(prompt) {
|
|
165
|
-
const normalized = collapseWhitespace(prompt);
|
|
166
|
-
return normalized ? truncate(normalized, MAX_PROMPT_TITLE_LENGTH) : void 0;
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* The idle shell-tab title: `π - <label> - <repository>`.
|
|
170
|
-
*
|
|
171
|
-
* The session name wins over the first prompt because the user chose it. With
|
|
172
|
-
* neither, the repository alone still tells two tabs apart.
|
|
173
|
-
*/
|
|
174
|
-
function shellTabTitle({ cwd, sessionName, prompt }) {
|
|
175
|
-
const repository = path.basename(cwd);
|
|
176
|
-
const label = sessionName ?? prompt;
|
|
177
|
-
return label ? `${SHELL_TITLE_PREFIX}${SHELL_TITLE_SEPARATOR}${label}${SHELL_TITLE_SEPARATOR}${repository}` : `${SHELL_TITLE_PREFIX}${SHELL_TITLE_SEPARATOR}${repository}`;
|
|
178
|
-
}
|
|
179
|
-
/** Notification bodies are one line in a system panel, so they collapse and truncate. */
|
|
180
|
-
function notificationBody(body) {
|
|
181
|
-
return truncate(collapseWhitespace(body), MAX_BODY_LENGTH);
|
|
182
|
-
}
|
|
183
|
-
/** Sent while the agent holds the turn and something is blocking on the user. */
|
|
184
|
-
function attentionNotification(body) {
|
|
185
|
-
return {
|
|
186
|
-
title: ATTENTION_TITLE,
|
|
187
|
-
subtitle: ATTENTION_SUBTITLE,
|
|
188
|
-
body
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
/** Sent once a run has stopped, subtitled with whichever name the user recognises. */
|
|
192
|
-
function settledNotification({ cwd, sessionName }) {
|
|
193
|
-
return {
|
|
194
|
-
title: SETTLED_TITLE,
|
|
195
|
-
subtitle: sessionName ?? path.basename(cwd),
|
|
196
|
-
body: SETTLED_BODY
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
//#endregion
|
|
200
|
-
//#region src/services/systemNotification/index.ts
|
|
201
|
-
const CMUX_COMMAND = "cmux";
|
|
202
|
-
const COMMAND_TIMEOUT_MS = 3e3;
|
|
203
|
-
const DARWIN_PLATFORM = "darwin";
|
|
204
|
-
const OSASCRIPT_COMMAND = "osascript";
|
|
205
|
-
const OSASCRIPT_EXPRESSION_FLAG = "-e";
|
|
206
|
-
async function execute(pi, command, args) {
|
|
207
|
-
try {
|
|
208
|
-
return (await pi.exec(command, args, { timeout: COMMAND_TIMEOUT_MS })).code === 0;
|
|
209
|
-
} catch {
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* Delivers one desktop notification on a best-effort basis.
|
|
215
|
-
*
|
|
216
|
-
* cmux is tried first because it routes the notification to the window the
|
|
217
|
-
* session actually lives in. Only macOS has a second option worth attempting,
|
|
218
|
-
* and a host with neither stays silent rather than failing the turn.
|
|
219
|
-
*/
|
|
220
|
-
async function sendSystemNotification(pi, notification, platform = process.platform) {
|
|
221
|
-
const body = notificationBody(notification.body);
|
|
222
|
-
if (await execute(pi, CMUX_COMMAND, [
|
|
223
|
-
"notify",
|
|
224
|
-
"--title",
|
|
225
|
-
notification.title,
|
|
226
|
-
"--subtitle",
|
|
227
|
-
notification.subtitle,
|
|
228
|
-
"--body",
|
|
229
|
-
body
|
|
230
|
-
]) || platform !== DARWIN_PLATFORM) return;
|
|
231
|
-
await execute(pi, OSASCRIPT_COMMAND, [
|
|
232
|
-
OSASCRIPT_EXPRESSION_FLAG,
|
|
233
|
-
"on run argv",
|
|
234
|
-
OSASCRIPT_EXPRESSION_FLAG,
|
|
235
|
-
"display notification (item 3 of argv) with title (item 1 of argv) subtitle (item 2 of argv)",
|
|
236
|
-
OSASCRIPT_EXPRESSION_FLAG,
|
|
237
|
-
"end run",
|
|
238
|
-
notification.title,
|
|
239
|
-
notification.subtitle,
|
|
240
|
-
body
|
|
241
|
-
]);
|
|
242
|
-
}
|
|
243
|
-
//#endregion
|
|
244
|
-
//#region src/services/notificationPolicy/index.ts
|
|
245
|
-
const DEFAULT_ASK_USER_BODY = "The agent is waiting for your feedback.";
|
|
246
|
-
const RPC_MODE = "rpc";
|
|
247
|
-
/**
|
|
248
|
-
* A dialog is worth interrupting the user for only while the agent holds the turn.
|
|
249
|
-
*
|
|
250
|
-
* Outside a run the user opened the dialog themselves and is already looking at
|
|
251
|
-
* it. An ask-user prompt is announced by its own event, so the dialog it then
|
|
252
|
-
* opens must stay silent or the same question notifies twice.
|
|
253
|
-
*/
|
|
254
|
-
function warrantsAttentionNotification({ agentRunning, askUserBlocked }) {
|
|
255
|
-
return agentRunning && !askUserBlocked;
|
|
256
|
-
}
|
|
257
|
-
/** A queued follow-up means the agent keeps working, so the run has not stopped for the user. */
|
|
258
|
-
function warrantsSettledNotification(hasPendingMessages) {
|
|
259
|
-
return !hasPendingMessages;
|
|
260
|
-
}
|
|
261
|
-
/** Terminal title escapes only mean something on an attached terminal; RPC output stays clean. */
|
|
262
|
-
function supportsShellTitle({ hasUI, mode }) {
|
|
263
|
-
return hasUI && mode !== RPC_MODE;
|
|
264
|
-
}
|
|
265
|
-
/** The question an ask-user prompt is blocking on, or a stand-in when it asked none. */
|
|
266
|
-
function askUserPromptBody(questions) {
|
|
267
|
-
return questions[0]?.question ?? DEFAULT_ASK_USER_BODY;
|
|
268
|
-
}
|
|
269
|
-
//#endregion
|
|
270
|
-
export { askUserPromptBody, attentionNotification, createMainThreadTitleController, createWorkerTitleController, notificationBody, promptTitle, sendSystemNotification, settledNotification, shellTabTitle, supportsShellTitle, warrantsAttentionNotification, warrantsSettledNotification };
|
|
271
|
-
|
|
272
|
-
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/services/shellTitleController/index.ts","../src/services/notificationText/index.ts","../src/services/systemNotification/index.ts","../src/services/notificationPolicy/index.ts"],"sourcesContent":["import { Worker } from 'node:worker_threads';\n\nimport type { ShellTitleCommand, ShellTitleController, WriteTitle } from '../../types/notifications';\n\nconst BRAILLE_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];\nconst TITLE_FRAME_INTERVAL_MS = 80;\n/**\n * The animator runs off the main thread so a busy agent turn cannot stall the\n * spinner. It is an inline eval source rather than a shipped file because the\n * package is bundled per entry, and a separate worker module would have to be\n * located on disk at runtime.\n */\nconst TITLE_WORKER_SOURCE = `\nconst { writeSync } = require('node:fs');\nconst { parentPort, workerData } = require('node:worker_threads');\nlet frameIndex = 0;\nlet timer;\nconst writeTitle = (title) => writeSync(1, '\\\\u001b]0;' + title + '\\\\u0007');\nconst stop = () => {\n if (timer) clearInterval(timer);\n timer = undefined;\n frameIndex = 0;\n};\nparentPort.on('message', (command) => {\n stop();\n if (command.action === 'start') {\n const render = () => {\n writeTitle(workerData.frames[frameIndex % workerData.frames.length] + ' ' + command.title);\n frameIndex += 1;\n };\n render();\n timer = setInterval(render, workerData.intervalMs);\n return;\n }\n writeTitle(command.title);\n if (command.action === 'dispose') parentPort.close();\n});\n`;\n\n/** Animates on the main thread, for hosts where a worker could not be started. */\nexport function createMainThreadTitleController(): ShellTitleController {\n let frameIndex = 0;\n let timer: ReturnType<typeof setInterval> | undefined;\n const stopAnimation = () => {\n if (timer) clearInterval(timer);\n timer = undefined;\n frameIndex = 0;\n };\n\n return {\n set(title, write) {\n stopAnimation();\n write(title);\n },\n start(title, write) {\n stopAnimation();\n const render = () => {\n write(`${BRAILLE_FRAMES[frameIndex % BRAILLE_FRAMES.length]} ${title}`);\n frameIndex += 1;\n };\n render();\n timer = setInterval(render, TITLE_FRAME_INTERVAL_MS);\n },\n stop(title, write) {\n stopAnimation();\n write(title);\n },\n dispose(title, write) {\n stopAnimation();\n write(title);\n },\n };\n}\n\n/**\n * Animates from a worker thread, replaying the last command on the main thread\n * if the worker never started, failed, or exited under us. The worker is\n * unreferenced so a pending frame never holds the process open.\n */\nexport function createWorkerTitleController(): ShellTitleController {\n const fallback = createMainThreadTitleController();\n let lastCommand: ShellTitleCommand | undefined;\n let lastWrite: WriteTitle | undefined;\n let worker: Worker | undefined;\n\n const fallBack = () => {\n worker = undefined;\n if (!lastCommand || !lastWrite) return;\n fallback[lastCommand.action](lastCommand.title, lastWrite);\n };\n\n try {\n const titleWorker = new Worker(TITLE_WORKER_SOURCE, {\n eval: true,\n workerData: { frames: BRAILLE_FRAMES, intervalMs: TITLE_FRAME_INTERVAL_MS },\n });\n titleWorker.unref();\n titleWorker.once('error', fallBack);\n titleWorker.once('exit', (code) => {\n if (code !== 0 && worker === titleWorker) fallBack();\n });\n worker = titleWorker;\n } catch {\n // Workers are unavailable on this host, which the main-thread animator covers.\n worker = undefined;\n }\n\n const dispatch = (command: ShellTitleCommand, write: WriteTitle) => {\n lastCommand = command;\n lastWrite = write;\n if (!worker) {\n fallback[command.action](command.title, write);\n return;\n }\n try {\n worker.postMessage(command);\n } catch {\n // The worker closed between the last frame and this command.\n fallBack();\n }\n };\n\n return {\n set: (title, write) => dispatch({ action: 'set', title }, write),\n start: (title, write) => dispatch({ action: 'start', title }, write),\n stop: (title, write) => dispatch({ action: 'stop', title }, write),\n dispose: (title, write) => dispatch({ action: 'dispose', title }, write),\n };\n}\n","import path from 'node:path';\n\nimport type { DesktopNotification } from '../../types/notifications';\n\nconst ATTENTION_SUBTITLE = 'Approval or feedback required';\nconst ATTENTION_TITLE = 'Pi needs your input';\nconst ELLIPSIS = '…';\nconst MAX_BODY_LENGTH = 240;\nconst MAX_PROMPT_TITLE_LENGTH = 36;\nconst SETTLED_BODY = 'The agent finished its work and is waiting for you.';\nconst SETTLED_TITLE = 'Pi finished';\nconst SHELL_TITLE_SEPARATOR = ' - ';\nconst SHELL_TITLE_PREFIX = 'π';\n\nfunction collapseWhitespace(value: string): string {\n return value.replace(/\\s+/g, ' ').trim();\n}\n\nfunction truncate(value: string, maxLength: number): string {\n return value.length > maxLength ? `${value.slice(0, maxLength - 1)}${ELLIPSIS}` : value;\n}\n\n/** The first user prompt, shortened to fit a shell tab. A blank prompt names nothing. */\nexport function promptTitle(prompt: string): string | undefined {\n const normalized = collapseWhitespace(prompt);\n return normalized ? truncate(normalized, MAX_PROMPT_TITLE_LENGTH) : undefined;\n}\n\nexport interface ShellTabTitleInput {\n cwd: string;\n sessionName?: string;\n prompt?: string;\n}\n\n/**\n * The idle shell-tab title: `π - <label> - <repository>`.\n *\n * The session name wins over the first prompt because the user chose it. With\n * neither, the repository alone still tells two tabs apart.\n */\nexport function shellTabTitle({ cwd, sessionName, prompt }: ShellTabTitleInput): string {\n const repository = path.basename(cwd);\n const label = sessionName ?? prompt;\n return label\n ? `${SHELL_TITLE_PREFIX}${SHELL_TITLE_SEPARATOR}${label}${SHELL_TITLE_SEPARATOR}${repository}`\n : `${SHELL_TITLE_PREFIX}${SHELL_TITLE_SEPARATOR}${repository}`;\n}\n\n/** Notification bodies are one line in a system panel, so they collapse and truncate. */\nexport function notificationBody(body: string): string {\n return truncate(collapseWhitespace(body), MAX_BODY_LENGTH);\n}\n\n/** Sent while the agent holds the turn and something is blocking on the user. */\nexport function attentionNotification(body: string): DesktopNotification {\n return { title: ATTENTION_TITLE, subtitle: ATTENTION_SUBTITLE, body };\n}\n\n/** Sent once a run has stopped, subtitled with whichever name the user recognises. */\nexport function settledNotification({ cwd, sessionName }: { cwd: string; sessionName?: string }): DesktopNotification {\n return { title: SETTLED_TITLE, subtitle: sessionName ?? path.basename(cwd), body: SETTLED_BODY };\n}\n","import type { DesktopNotification, NotificationExecutor } from '../../types/notifications';\nimport { notificationBody } from '../notificationText';\n\nconst CMUX_COMMAND = 'cmux';\nconst COMMAND_TIMEOUT_MS = 3_000;\nconst DARWIN_PLATFORM = 'darwin';\nconst OSASCRIPT_COMMAND = 'osascript';\nconst OSASCRIPT_EXPRESSION_FLAG = '-e';\n\nasync function execute(pi: NotificationExecutor, command: string, args: string[]): Promise<boolean> {\n try {\n const result = await pi.exec(command, args, { timeout: COMMAND_TIMEOUT_MS });\n return result.code === 0;\n } catch {\n // A notifier that is not installed is the ordinary case, not a fault: report\n // \"not delivered\" and let the caller decide whether another one exists.\n return false;\n }\n}\n\n/**\n * Delivers one desktop notification on a best-effort basis.\n *\n * cmux is tried first because it routes the notification to the window the\n * session actually lives in. Only macOS has a second option worth attempting,\n * and a host with neither stays silent rather than failing the turn.\n */\nexport async function sendSystemNotification(\n pi: NotificationExecutor,\n notification: DesktopNotification,\n platform: string = process.platform,\n): Promise<void> {\n const body = notificationBody(notification.body);\n const sentToCmux = await execute(pi, CMUX_COMMAND, [\n 'notify',\n '--title',\n notification.title,\n '--subtitle',\n notification.subtitle,\n '--body',\n body,\n ]);\n if (sentToCmux || platform !== DARWIN_PLATFORM) return;\n\n await execute(pi, OSASCRIPT_COMMAND, [\n OSASCRIPT_EXPRESSION_FLAG,\n 'on run argv',\n OSASCRIPT_EXPRESSION_FLAG,\n 'display notification (item 3 of argv) with title (item 1 of argv) subtitle (item 2 of argv)',\n OSASCRIPT_EXPRESSION_FLAG,\n 'end run',\n notification.title,\n notification.subtitle,\n body,\n ]);\n}\n","const DEFAULT_ASK_USER_BODY = 'The agent is waiting for your feedback.';\nconst RPC_MODE = 'rpc';\n\nexport interface AttentionState {\n agentRunning: boolean;\n askUserBlocked: boolean;\n}\n\nexport interface ShellSurface {\n hasUI: boolean;\n mode?: string;\n}\n\n/**\n * A dialog is worth interrupting the user for only while the agent holds the turn.\n *\n * Outside a run the user opened the dialog themselves and is already looking at\n * it. An ask-user prompt is announced by its own event, so the dialog it then\n * opens must stay silent or the same question notifies twice.\n */\nexport function warrantsAttentionNotification({ agentRunning, askUserBlocked }: AttentionState): boolean {\n return agentRunning && !askUserBlocked;\n}\n\n/** A queued follow-up means the agent keeps working, so the run has not stopped for the user. */\nexport function warrantsSettledNotification(hasPendingMessages: boolean): boolean {\n return !hasPendingMessages;\n}\n\n/** Terminal title escapes only mean something on an attached terminal; RPC output stays clean. */\nexport function supportsShellTitle({ hasUI, mode }: ShellSurface): boolean {\n return hasUI && mode !== RPC_MODE;\n}\n\n/** The question an ask-user prompt is blocking on, or a stand-in when it asked none. */\nexport function askUserPromptBody(questions: readonly { question: string }[]): string {\n return questions[0]?.question ?? DEFAULT_ASK_USER_BODY;\n}\n"],"mappings":"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"}
|
package/llms.txt
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
# Doom Pi Notification
|
|
2
|
-
|
|
3
|
-
Shared notification routing, browser and system delivery, and an animated shell-tab title for Doom Pi sessions.
|
|
4
|
-
|
|
5
|
-
## Resources
|
|
6
|
-
|
|
7
|
-
- [Package README](./README.md): public `doom/notification` service usage, request defaults, RPC and interactive delivery, live-page browser permission behavior, shell-title animation, and silent fallback rules.
|