@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 CHANGED
@@ -1,96 +1,3 @@
1
- # @agimon-ai/po-notification
1
+ # Temporary Holding Version
2
2
 
3
- System and browser notifications, plus an animated shell-tab title, for
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-alpha.1",
4
- "description": "Desktop notifications and an animated shell-tab title for Po sessions.",
5
- "keywords": [
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.
@@ -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"}
@@ -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"}
@@ -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,5 +0,0 @@
1
- //#region generated/server.d.ts
2
- export declare const facet: import("@agimon-ai/po-core/serverFacet").DoomServerFacet;
3
- //#endregion
4
- export { facet as default };
5
- //# sourceMappingURL=server.d.mts.map
@@ -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
@@ -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
@@ -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
@@ -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.