@agimon-ai/po-notification 0.0.0-alpha.2 → 0.0.0-alpha.20

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
@@ -3,7 +3,7 @@
3
3
  System and browser notifications, plus an animated shell-tab title, for
4
4
  [Po](https://www.npmjs.com/package/@agimon-ai/po) sessions.
5
5
 
6
- Long agent runs are worth walking away from. This package owns the shared `doom/notification` service,
6
+ Long agent runs are worth walking away from. This package owns the shared `po/notification` service,
7
7
  routes each request for the active session, and announces when the agent needs attention.
8
8
 
9
9
  ## What it does
@@ -11,13 +11,13 @@ routes each request for the active session, and announces when the agent needs a
11
11
  | Surface | Behavior |
12
12
  | ---------------- | -------------------------------------------------------------------------------------------- |
13
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 |
14
+ | Notification API | caller-authored notices through the shared `po/notification` Cordis service |
15
15
  | Mid-run | one notification for each agent-initiated dialog and each `ask_user_question` prompt |
16
16
  | At rest | one notification when a run settles with nothing queued behind it |
17
17
 
18
18
  In an interactive terminal session, requests use `cmux notify` first because it routes them back to
19
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
20
+ versioned `po-notification` session entries for a live client such as `po-web`. An RPC append
21
21
  failure stays silent and never falls back to a host notifier. Hosts without a supported notifier also
22
22
  stay silent.
23
23
 
@@ -27,10 +27,10 @@ Callers should discover the optional service through Cordis and request delivery
27
27
  a particular host:
28
28
 
29
29
  ```ts
30
- import { DOOM_NOTIFICATION_SERVICE, readDoomNotificationService } from '@agimon-ai/po-core/notification';
30
+ import { PO_NOTIFICATION_SERVICE, readPoNotificationService } from '@agimon-ai/po-core/notification';
31
31
 
32
- ctx.inject([DOOM_NOTIFICATION_SERVICE], (notificationContext) => {
33
- void readDoomNotificationService(notificationContext)?.request({
32
+ ctx.inject([PO_NOTIFICATION_SERVICE], (notificationContext) => {
33
+ void readPoNotificationService(notificationContext)?.request({
34
34
  body: 'Deployment needs approval',
35
35
  level: 'warning',
36
36
  });
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  //#region src/schemas/apiContracts.ts
6
- /** This facet contributes agent tools, commands or lifecycle only, with no separate HTTP or socket surface. */
6
+ /** This server plugin contributes agent tools, commands or lifecycle only, with no separate HTTP or socket surface. */
7
7
  const apiContracts = (0, require("@agimon-ai/po-core/apiContracts").defineApiContract)({
8
8
  version: 1,
9
9
  http: [],
@@ -1 +1 @@
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
+ {"version":3,"file":"apiContracts.cjs","names":["defineApiContract"],"sources":["../src/schemas/apiContracts.ts"],"sourcesContent":["import { defineApiContract } from '@agimon-ai/po-core/apiContracts';\n\n/** This server plugin 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,5 +1,5 @@
1
1
  //#region src/schemas/apiContracts.d.ts
2
- /** This facet contributes agent tools, commands or lifecycle only, with no separate HTTP or socket surface. */
2
+ /** This server plugin contributes agent tools, commands or lifecycle only, with no separate HTTP or socket surface. */
3
3
  export declare const apiContracts: {
4
4
  version: 1;
5
5
  protocols?: Record<string, string> | undefined;
@@ -1,6 +1,6 @@
1
1
  import { defineApiContract } from "@agimon-ai/po-core/apiContracts";
2
2
  //#region src/schemas/apiContracts.ts
3
- /** This facet contributes agent tools, commands or lifecycle only, with no separate HTTP or socket surface. */
3
+ /** This server plugin contributes agent tools, commands or lifecycle only, with no separate HTTP or socket surface. */
4
4
  const apiContracts = defineApiContract({
5
5
  version: 1,
6
6
  http: [],
@@ -1 +1 @@
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
+ {"version":3,"file":"apiContracts.mjs","names":[],"sources":["../src/schemas/apiContracts.ts"],"sourcesContent":["import { defineApiContract } from '@agimon-ai/po-core/apiContracts';\n\n/** This server plugin 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"}
@@ -2,10 +2,10 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- let _agimon_ai_po_core_serverFacet = require("@agimon-ai/po-core/serverFacet");
5
+ let _agimon_ai_po_core_serverPlugin = require("@agimon-ai/po-core/serverPlugin");
6
6
  let _agimon_ai_po_core_extensionFile = require("@agimon-ai/po-core/extensionFile");
7
7
  //#region src/extensions/workspaces/sessions/(backend)/_lib/notificationCommand.ts
8
- const notificationCommand = (0, _agimon_ai_po_core_serverFacet.defineCommand)({
8
+ const notificationCommand = (0, _agimon_ai_po_core_serverPlugin.defineCommand)({
9
9
  name: "notify",
10
10
  description: "Send a notification through the active headless client.",
11
11
  async execute(args, execution) {
@@ -26,14 +26,14 @@ const via = (identity, value) => value === void 0 ? void 0 : {
26
26
  ...identity,
27
27
  ...value
28
28
  };
29
- const facet = (0, _agimon_ai_po_core_serverFacet.defineServerPlugin)({
29
+ const serverPlugin = (0, _agimon_ai_po_core_serverPlugin.defineServerPlugin)({
30
30
  name: "@agimon-ai/po-notification",
31
31
  session: (context) => ({ get commands() {
32
32
  return [via({ name: "notify" }, at(notify_server_default, context))];
33
33
  } })
34
34
  });
35
35
  //#endregion
36
- exports.default = facet;
37
- exports.facet = facet;
36
+ exports.default = serverPlugin;
37
+ exports.serverPlugin = serverPlugin;
38
38
 
39
39
  //# sourceMappingURL=server.cjs.map
@@ -1 +1 @@
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
+ {"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/serverPlugin';\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 PoServerSessionPlugin } from '@agimon-ai/po-core/serverPlugin';\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 serverPlugin = defineServerPlugin({\n name: '@agimon-ai/po-notification',\n session: (context) => ({\n get commands(): PoServerSessionPlugin['commands'] { return [via({ name: 'notify' }, at(commandNotify, context))]; },\n }),\n});\n\nexport default serverPlugin;\n"],"mappings":";;;;;;;AAEA,MAAa,uBAAA,GAAsBA,gCAAAA,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,gBAAA,GAAeC,gCAAAA,mBAAAA,CAAmB;CAC7C,MAAM;CACN,UAAU,aAAa,EACrB,IAAI,WAA8C;EAAE,OAAO,CAAC,IAAI,EAAE,MAAM,SAAS,GAAG,GAAGC,uBAAe,OAAO,CAAC,CAAC;CAAG,EACpH;AACF,CAAC"}
@@ -1,5 +1,5 @@
1
1
  //#region generated/server.d.ts
2
- export declare const facet: import("@agimon-ai/po-core/serverFacet").DoomServerFacet;
2
+ export declare const serverPlugin: import("@agimon-ai/po-core/serverPlugin").PoServerPlugin;
3
3
  //#endregion
4
- export { facet as default };
4
+ export { serverPlugin as default };
5
5
  //# sourceMappingURL=server.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.mts","names":[],"sources":["../../generated/server.ts"],"mappings":";qBAWa,gDAAK"}
1
+ {"version":3,"file":"server.d.mts","names":[],"sources":["../../generated/server.ts"],"mappings":";qBAWa,wDAAY"}
@@ -1,4 +1,4 @@
1
- import { defineCommand, defineServerPlugin } from "@agimon-ai/po-core/serverFacet";
1
+ import { defineCommand, defineServerPlugin } from "@agimon-ai/po-core/serverPlugin";
2
2
  import { defineServerCommand } from "@agimon-ai/po-core/extensionFile";
3
3
  //#region src/extensions/workspaces/sessions/(backend)/_lib/notificationCommand.ts
4
4
  const notificationCommand = defineCommand({
@@ -22,13 +22,13 @@ const via = (identity, value) => value === void 0 ? void 0 : {
22
22
  ...identity,
23
23
  ...value
24
24
  };
25
- const facet = defineServerPlugin({
25
+ const serverPlugin = defineServerPlugin({
26
26
  name: "@agimon-ai/po-notification",
27
27
  session: (context) => ({ get commands() {
28
28
  return [via({ name: "notify" }, at(notify_server_default, context))];
29
29
  } })
30
30
  });
31
31
  //#endregion
32
- export { facet as default, facet };
32
+ export { serverPlugin as default, serverPlugin };
33
33
 
34
34
  //# sourceMappingURL=server.mjs.map
@@ -1 +1 @@
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"}
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/serverPlugin';\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 PoServerSessionPlugin } from '@agimon-ai/po-core/serverPlugin';\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 serverPlugin = defineServerPlugin({\n name: '@agimon-ai/po-notification',\n session: (context) => ({\n get commands(): PoServerSessionPlugin['commands'] { return [via({ name: 'notify' }, at(commandNotify, context))]; },\n }),\n});\n\nexport default serverPlugin;\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,eAAe,mBAAmB;CAC7C,MAAM;CACN,UAAU,aAAa,EACrB,IAAI,WAA8C;EAAE,OAAO,CAAC,IAAI,EAAE,MAAM,SAAS,GAAG,GAAGA,uBAAe,OAAO,CAAC,CAAC;CAAG,EACpH;AACF,CAAC"}
package/dist/index.cjs CHANGED
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  let node_worker_threads = require("node:worker_threads");
25
25
  let node_path = require("node:path");
26
26
  node_path = __toESM(node_path, 1);
27
- //#region src/services/shellTitleController/index.ts
27
+ //#region src/services/shellTitleController/mainThread.ts
28
28
  const BRAILLE_FRAMES = [
29
29
  "⠋",
30
30
  "⠙",
@@ -37,39 +37,6 @@ const BRAILLE_FRAMES = [
37
37
  "⠇",
38
38
  "⠏"
39
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
40
  /** Animates on the main thread, for hosts where a worker could not be started. */
74
41
  function createMainThreadTitleController() {
75
42
  let frameIndex = 0;
@@ -91,7 +58,7 @@ function createMainThreadTitleController() {
91
58
  frameIndex += 1;
92
59
  };
93
60
  render();
94
- timer = setInterval(render, TITLE_FRAME_INTERVAL_MS);
61
+ timer = setInterval(render, 80);
95
62
  },
96
63
  stop(title, write) {
97
64
  stopAnimation();
@@ -103,6 +70,40 @@ function createMainThreadTitleController() {
103
70
  }
104
71
  };
105
72
  }
73
+ //#endregion
74
+ //#region src/services/shellTitleController/index.ts
75
+ /**
76
+ * The animator runs off the main thread so a busy agent turn cannot stall the
77
+ * spinner. It is an inline eval source rather than a shipped file because the
78
+ * package is bundled per entry, and a separate worker module would have to be
79
+ * located on disk at runtime.
80
+ */
81
+ const TITLE_WORKER_SOURCE = `
82
+ const { writeSync } = require('node:fs');
83
+ const { parentPort, workerData } = require('node:worker_threads');
84
+ let frameIndex = 0;
85
+ let timer;
86
+ const writeTitle = (title) => writeSync(1, '\\u001b]0;' + title + '\\u0007');
87
+ const stop = () => {
88
+ if (timer) clearInterval(timer);
89
+ timer = undefined;
90
+ frameIndex = 0;
91
+ };
92
+ parentPort.on('message', (command) => {
93
+ stop();
94
+ if (command.action === 'start') {
95
+ const render = () => {
96
+ writeTitle(workerData.frames[frameIndex % workerData.frames.length] + ' ' + command.title);
97
+ frameIndex += 1;
98
+ };
99
+ render();
100
+ timer = setInterval(render, workerData.intervalMs);
101
+ return;
102
+ }
103
+ writeTitle(command.title);
104
+ if (command.action === 'dispose') parentPort.close();
105
+ });
106
+ `;
106
107
  /**
107
108
  * Animates from a worker thread, replaying the last command on the main thread
108
109
  * if the worker never started, failed, or exited under us. The worker is
@@ -123,7 +124,7 @@ function createWorkerTitleController() {
123
124
  eval: true,
124
125
  workerData: {
125
126
  frames: BRAILLE_FRAMES,
126
- intervalMs: TITLE_FRAME_INTERVAL_MS
127
+ intervalMs: 80
127
128
  }
128
129
  });
129
130
  titleWorker.unref();
@@ -1 +1 @@
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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"}
1
+ {"version":3,"file":"index.cjs","names":["Worker","path"],"sources":["../src/services/shellTitleController/mainThread.ts","../src/services/shellTitleController/index.ts","../src/services/notificationText/index.ts","../src/services/systemNotification/index.ts","../src/services/notificationPolicy/index.ts"],"sourcesContent":["import type { ShellTitleController } from '../../types/notifications';\n\nexport const BRAILLE_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];\nexport const TITLE_FRAME_INTERVAL_MS = 80;\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","import { Worker } from 'node:worker_threads';\n\nimport type { ShellTitleCommand, ShellTitleController, WriteTitle } from '../../types/notifications';\nimport { BRAILLE_FRAMES, TITLE_FRAME_INTERVAL_MS, createMainThreadTitleController } from './mainThread';\n\nexport { createMainThreadTitleController } from './mainThread';\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/**\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 CHANGED
@@ -41,9 +41,11 @@ interface DesktopNotification {
41
41
  body: string;
42
42
  }
43
43
  //#endregion
44
- //#region src/services/shellTitleController/index.d.ts
44
+ //#region src/services/shellTitleController/mainThread.d.ts
45
45
  /** Animates on the main thread, for hosts where a worker could not be started. */
46
46
  export declare function createMainThreadTitleController(): ShellTitleController;
47
+ //#endregion
48
+ //#region src/services/shellTitleController/index.d.ts
47
49
  /**
48
50
  * Animates from a worker thread, replaying the last command on the main thread
49
51
  * if the worker never started, failed, or exited under us. The worker is
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types/notifications.ts","../src/services/shellTitleController/mainThread.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;;;;;wBCnCc,mCAAmC;;;;;;;;wBCsCnC,+BAA+B;;;;;;;;;;wBCjBzB,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 CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Worker } from "node:worker_threads";
2
2
  import path from "node:path";
3
- //#region src/services/shellTitleController/index.ts
3
+ //#region src/services/shellTitleController/mainThread.ts
4
4
  const BRAILLE_FRAMES = [
5
5
  "⠋",
6
6
  "⠙",
@@ -13,39 +13,6 @@ const BRAILLE_FRAMES = [
13
13
  "⠇",
14
14
  "⠏"
15
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
16
  /** Animates on the main thread, for hosts where a worker could not be started. */
50
17
  function createMainThreadTitleController() {
51
18
  let frameIndex = 0;
@@ -67,7 +34,7 @@ function createMainThreadTitleController() {
67
34
  frameIndex += 1;
68
35
  };
69
36
  render();
70
- timer = setInterval(render, TITLE_FRAME_INTERVAL_MS);
37
+ timer = setInterval(render, 80);
71
38
  },
72
39
  stop(title, write) {
73
40
  stopAnimation();
@@ -79,6 +46,40 @@ function createMainThreadTitleController() {
79
46
  }
80
47
  };
81
48
  }
49
+ //#endregion
50
+ //#region src/services/shellTitleController/index.ts
51
+ /**
52
+ * The animator runs off the main thread so a busy agent turn cannot stall the
53
+ * spinner. It is an inline eval source rather than a shipped file because the
54
+ * package is bundled per entry, and a separate worker module would have to be
55
+ * located on disk at runtime.
56
+ */
57
+ const TITLE_WORKER_SOURCE = `
58
+ const { writeSync } = require('node:fs');
59
+ const { parentPort, workerData } = require('node:worker_threads');
60
+ let frameIndex = 0;
61
+ let timer;
62
+ const writeTitle = (title) => writeSync(1, '\\u001b]0;' + title + '\\u0007');
63
+ const stop = () => {
64
+ if (timer) clearInterval(timer);
65
+ timer = undefined;
66
+ frameIndex = 0;
67
+ };
68
+ parentPort.on('message', (command) => {
69
+ stop();
70
+ if (command.action === 'start') {
71
+ const render = () => {
72
+ writeTitle(workerData.frames[frameIndex % workerData.frames.length] + ' ' + command.title);
73
+ frameIndex += 1;
74
+ };
75
+ render();
76
+ timer = setInterval(render, workerData.intervalMs);
77
+ return;
78
+ }
79
+ writeTitle(command.title);
80
+ if (command.action === 'dispose') parentPort.close();
81
+ });
82
+ `;
82
83
  /**
83
84
  * Animates from a worker thread, replaying the last command on the main thread
84
85
  * if the worker never started, failed, or exited under us. The worker is
@@ -99,7 +100,7 @@ function createWorkerTitleController() {
99
100
  eval: true,
100
101
  workerData: {
101
102
  frames: BRAILLE_FRAMES,
102
- intervalMs: TITLE_FRAME_INTERVAL_MS
103
+ intervalMs: 80
103
104
  }
104
105
  });
105
106
  titleWorker.unref();
@@ -1 +1 @@
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":";;;AAIA,MAAM,iBAAiB;CAAC;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;CAAK;AAAG;AACxE,MAAM,0BAA0B;;;;;;;AAOhC,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4B5B,SAAgB,kCAAwD;CACtE,IAAI,aAAa;CACjB,IAAI;CACJ,MAAM,sBAAsB;EAC1B,IAAI,OAAO,cAAc,KAAK;EAC9B,QAAQ,KAAA;EACR,aAAa;CACf;CAEA,OAAO;EACL,IAAI,OAAO,OAAO;GAChB,cAAc;GACd,MAAM,KAAK;EACb;EACA,MAAM,OAAO,OAAO;GAClB,cAAc;GACd,MAAM,eAAe;IACnB,MAAM,GAAG,eAAe,aAAa,eAAe,QAAQ,GAAG,OAAO;IACtE,cAAc;GAChB;GACA,OAAO;GACP,QAAQ,YAAY,QAAQ,uBAAuB;EACrD;EACA,KAAK,OAAO,OAAO;GACjB,cAAc;GACd,MAAM,KAAK;EACb;EACA,QAAQ,OAAO,OAAO;GACpB,cAAc;GACd,MAAM,KAAK;EACb;CACF;AACF;;;;;;AAOA,SAAgB,8BAAoD;CAClE,MAAM,WAAW,gCAAgC;CACjD,IAAI;CACJ,IAAI;CACJ,IAAI;CAEJ,MAAM,iBAAiB;EACrB,SAAS,KAAA;EACT,IAAI,CAAC,eAAe,CAAC,WAAW;EAChC,SAAS,YAAY,OAAO,CAAC,YAAY,OAAO,SAAS;CAC3D;CAEA,IAAI;EACF,MAAM,cAAc,IAAI,OAAO,qBAAqB;GAClD,MAAM;GACN,YAAY;IAAE,QAAQ;IAAgB,YAAY;GAAwB;EAC5E,CAAC;EACD,YAAY,MAAM;EAClB,YAAY,KAAK,SAAS,QAAQ;EAClC,YAAY,KAAK,SAAS,SAAS;GACjC,IAAI,SAAS,KAAK,WAAW,aAAa,SAAS;EACrD,CAAC;EACD,SAAS;CACX,QAAQ;EAEN,SAAS,KAAA;CACX;CAEA,MAAM,YAAY,SAA4B,UAAsB;EAClE,cAAc;EACd,YAAY;EACZ,IAAI,CAAC,QAAQ;GACX,SAAS,QAAQ,OAAO,CAAC,QAAQ,OAAO,KAAK;GAC7C;EACF;EACA,IAAI;GACF,OAAO,YAAY,OAAO;EAC5B,QAAQ;GAEN,SAAS;EACX;CACF;CAEA,OAAO;EACL,MAAM,OAAO,UAAU,SAAS;GAAE,QAAQ;GAAO;EAAM,GAAG,KAAK;EAC/D,QAAQ,OAAO,UAAU,SAAS;GAAE,QAAQ;GAAS;EAAM,GAAG,KAAK;EACnE,OAAO,OAAO,UAAU,SAAS;GAAE,QAAQ;GAAQ;EAAM,GAAG,KAAK;EACjE,UAAU,OAAO,UAAU,SAAS;GAAE,QAAQ;GAAW;EAAM,GAAG,KAAK;CACzE;AACF;;;AC5HA,MAAM,qBAAqB;AAC3B,MAAM,kBAAkB;AACxB,MAAM,WAAW;AACjB,MAAM,kBAAkB;AACxB,MAAM,0BAA0B;AAChC,MAAM,eAAe;AACrB,MAAM,gBAAgB;AACtB,MAAM,wBAAwB;AAC9B,MAAM,qBAAqB;AAE3B,SAAS,mBAAmB,OAAuB;CACjD,OAAO,MAAM,QAAQ,QAAQ,GAAG,CAAC,CAAC,KAAK;AACzC;AAEA,SAAS,SAAS,OAAe,WAA2B;CAC1D,OAAO,MAAM,SAAS,YAAY,GAAG,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,aAAa;AACpF;;AAGA,SAAgB,YAAY,QAAoC;CAC9D,MAAM,aAAa,mBAAmB,MAAM;CAC5C,OAAO,aAAa,SAAS,YAAY,uBAAuB,IAAI,KAAA;AACtE;;;;;;;AAcA,SAAgB,cAAc,EAAE,KAAK,aAAa,UAAsC;CACtF,MAAM,aAAa,KAAK,SAAS,GAAG;CACpC,MAAM,QAAQ,eAAe;CAC7B,OAAO,QACH,GAAG,qBAAqB,wBAAwB,QAAQ,wBAAwB,eAChF,GAAG,qBAAqB,wBAAwB;AACtD;;AAGA,SAAgB,iBAAiB,MAAsB;CACrD,OAAO,SAAS,mBAAmB,IAAI,GAAG,eAAe;AAC3D;;AAGA,SAAgB,sBAAsB,MAAmC;CACvE,OAAO;EAAE,OAAO;EAAiB,UAAU;EAAoB;CAAK;AACtE;;AAGA,SAAgB,oBAAoB,EAAE,KAAK,eAA2E;CACpH,OAAO;EAAE,OAAO;EAAe,UAAU,eAAe,KAAK,SAAS,GAAG;EAAG,MAAM;CAAa;AACjG;;;AC1DA,MAAM,eAAe;AACrB,MAAM,qBAAqB;AAC3B,MAAM,kBAAkB;AACxB,MAAM,oBAAoB;AAC1B,MAAM,4BAA4B;AAElC,eAAe,QAAQ,IAA0B,SAAiB,MAAkC;CAClG,IAAI;EAEF,QAAO,MADc,GAAG,KAAK,SAAS,MAAM,EAAE,SAAS,mBAAmB,CAAC,EAAA,CAC7D,SAAS;CACzB,QAAQ;EAGN,OAAO;CACT;AACF;;;;;;;;AASA,eAAsB,uBACpB,IACA,cACA,WAAmB,QAAQ,UACZ;CACf,MAAM,OAAO,iBAAiB,aAAa,IAAI;CAU/C,IAAI,MATqB,QAAQ,IAAI,cAAc;EACjD;EACA;EACA,aAAa;EACb;EACA,aAAa;EACb;EACA;CACF,CAAC,KACiB,aAAa,iBAAiB;CAEhD,MAAM,QAAQ,IAAI,mBAAmB;EACnC;EACA;EACA;EACA;EACA;EACA;EACA,aAAa;EACb,aAAa;EACb;CACF,CAAC;AACH;;;ACvDA,MAAM,wBAAwB;AAC9B,MAAM,WAAW;;;;;;;;AAmBjB,SAAgB,8BAA8B,EAAE,cAAc,kBAA2C;CACvG,OAAO,gBAAgB,CAAC;AAC1B;;AAGA,SAAgB,4BAA4B,oBAAsC;CAChF,OAAO,CAAC;AACV;;AAGA,SAAgB,mBAAmB,EAAE,OAAO,QAA+B;CACzE,OAAO,SAAS,SAAS;AAC3B;;AAGA,SAAgB,kBAAkB,WAAoD;CACpF,OAAO,UAAU,EAAE,EAAE,YAAY;AACnC"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/services/shellTitleController/mainThread.ts","../src/services/shellTitleController/index.ts","../src/services/notificationText/index.ts","../src/services/systemNotification/index.ts","../src/services/notificationPolicy/index.ts"],"sourcesContent":["import type { ShellTitleController } from '../../types/notifications';\n\nexport const BRAILLE_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];\nexport const TITLE_FRAME_INTERVAL_MS = 80;\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","import { Worker } from 'node:worker_threads';\n\nimport type { ShellTitleCommand, ShellTitleController, WriteTitle } from '../../types/notifications';\nimport { BRAILLE_FRAMES, TITLE_FRAME_INTERVAL_MS, createMainThreadTitleController } from './mainThread';\n\nexport { createMainThreadTitleController } from './mainThread';\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/**\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 CHANGED
@@ -1,7 +1,7 @@
1
- # Doom Pi Notification
1
+ # Po Pi Notification
2
2
 
3
- Shared notification routing, browser and system delivery, and an animated shell-tab title for Doom Pi sessions.
3
+ Shared notification routing, browser and system delivery, and an animated shell-tab title for Po Pi sessions.
4
4
 
5
5
  ## Resources
6
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.
7
+ - [Package README](./README.md): public `po/notification` service usage, request defaults, RPC and interactive delivery, live-page browser permission behavior, shell-title animation, and silent fallback rules.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agimon-ai/po-notification",
3
- "version": "0.0.0-alpha.2",
3
+ "version": "0.0.0-alpha.20",
4
4
  "description": "Desktop notifications and an animated shell-tab title for Po sessions.",
5
5
  "keywords": [
6
6
  "ai",
@@ -60,21 +60,21 @@
60
60
  "access": "public"
61
61
  },
62
62
  "dependencies": {
63
- "@agimon-ai/po-core": "0.0.0-alpha.2",
64
- "@deepseek-ai/cordis": "4.0.4"
63
+ "@agimon-ai/po-core": "0.0.0-alpha.20",
64
+ "@deepseek-ai/cordis": "4.0.5"
65
65
  },
66
66
  "devDependencies": {
67
- "@agimon-ai/po-build": "0.0.0-alpha.2",
68
- "@agimon-ai/vibe-lint-plugin-doom-extension": "0.0.0-alpha.2",
69
- "@earendil-works/pi-coding-agent": "1.2.0",
70
- "@types/node": "26.6.4",
71
- "@vitest/coverage-v8": "5.0.3",
72
- "tsdown": "0.23.0",
67
+ "@agimon-ai/po-build": "0.0.0-alpha.20",
68
+ "@agimon-ai/vibe-lint-plugin-po-extension": "0.0.0-alpha.20",
69
+ "@earendil-works/pi-coding-agent": "1.3.0",
70
+ "@types/node": "26.6.5",
71
+ "@vitest/coverage-v8": "5.0.4",
72
+ "tsdown": "0.23.1",
73
73
  "typescript": "7.0.2",
74
- "vitest": "5.0.3"
74
+ "vitest": "5.0.4"
75
75
  },
76
76
  "peerDependencies": {
77
- "@earendil-works/pi-coding-agent": "1.2.0"
77
+ "@earendil-works/pi-coding-agent": "1.3.0"
78
78
  },
79
79
  "peerDependenciesMeta": {
80
80
  "@earendil-works/pi-coding-agent": {