@agimon-ai/doompi-notification 0.0.1-alpha.2
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/LICENSE +21 -0
- package/README.md +47 -0
- package/dist/_virtual/_rolldown/runtime.cjs +1 -0
- package/dist/adapters/pi/extension.cjs +2 -0
- package/dist/adapters/pi/extension.cjs.map +1 -0
- package/dist/adapters/pi/extension.d.cts +19 -0
- package/dist/adapters/pi/extension.d.cts.map +1 -0
- package/dist/adapters/pi/extension.d.mts +19 -0
- package/dist/adapters/pi/extension.d.mts.map +1 -0
- package/dist/adapters/pi/extension.mjs +2 -0
- package/dist/adapters/pi/extension.mjs.map +1 -0
- package/dist/adapters/shellTitleController.cjs +27 -0
- package/dist/adapters/shellTitleController.cjs.map +1 -0
- package/dist/adapters/shellTitleController.d.cts +13 -0
- package/dist/adapters/shellTitleController.d.cts.map +1 -0
- package/dist/adapters/shellTitleController.d.mts +13 -0
- package/dist/adapters/shellTitleController.d.mts.map +1 -0
- package/dist/adapters/shellTitleController.mjs +27 -0
- package/dist/adapters/shellTitleController.mjs.map +1 -0
- package/dist/adapters/systemNotification.cjs +2 -0
- package/dist/adapters/systemNotification.cjs.map +1 -0
- package/dist/adapters/systemNotification.d.cts +14 -0
- package/dist/adapters/systemNotification.d.cts.map +1 -0
- package/dist/adapters/systemNotification.d.mts +14 -0
- package/dist/adapters/systemNotification.d.mts.map +1 -0
- package/dist/adapters/systemNotification.mjs +2 -0
- package/dist/adapters/systemNotification.mjs.map +1 -0
- package/dist/extensions/pi.cjs +1 -0
- package/dist/extensions/pi.d.cts +2 -0
- package/dist/extensions/pi.d.mts +2 -0
- package/dist/extensions/pi.mjs +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +7 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +1 -0
- package/dist/services/notificationPolicy.cjs +2 -0
- package/dist/services/notificationPolicy.cjs.map +1 -0
- package/dist/services/notificationPolicy.d.cts +28 -0
- package/dist/services/notificationPolicy.d.cts.map +1 -0
- package/dist/services/notificationPolicy.d.mts +28 -0
- package/dist/services/notificationPolicy.d.mts.map +1 -0
- package/dist/services/notificationPolicy.mjs +2 -0
- package/dist/services/notificationPolicy.mjs.map +1 -0
- package/dist/services/notificationText.cjs +2 -0
- package/dist/services/notificationText.cjs.map +1 -0
- package/dist/services/notificationText.d.cts +28 -0
- package/dist/services/notificationText.d.cts.map +1 -0
- package/dist/services/notificationText.d.mts +28 -0
- package/dist/services/notificationText.d.mts.map +1 -0
- package/dist/services/notificationText.mjs +2 -0
- package/dist/services/notificationText.mjs.map +1 -0
- package/dist/types/notifications.d.cts +33 -0
- package/dist/types/notifications.d.cts.map +1 -0
- package/dist/types/notifications.d.mts +33 -0
- package/dist/types/notifications.d.mts.map +1 -0
- package/llms.txt +7 -0
- package/package.json +84 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vuong Ngo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# @agimon-ai/doompi-notification
|
|
2
|
+
|
|
3
|
+
Desktop notifications and an animated shell-tab title for
|
|
4
|
+
[DoomPi](https://www.npmjs.com/package/@agimon-ai/doompi) sessions.
|
|
5
|
+
|
|
6
|
+
Long agent runs are worth walking away from. This package makes that safe: the terminal tab says
|
|
7
|
+
what the session is doing at a glance, and the desktop says when the agent has stopped and why.
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
| Surface | Behaviour |
|
|
12
|
+
| ---------------- | ----------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| Shell tab title | `π - <session or first prompt> - <repository>`, with a braille spinner while the agent works |
|
|
14
|
+
| Desktop, mid-run | one notification per dialog the agent opens on its own initiative, and per `ask_user_question` prompt |
|
|
15
|
+
| Desktop, at rest | one notification when a run settles with nothing queued behind it |
|
|
16
|
+
|
|
17
|
+
Notifications go to `cmux notify` first, because it routes them back to the window the session
|
|
18
|
+
lives in. On macOS a missing `cmux` falls back to `osascript`. Any other host stays silent rather
|
|
19
|
+
than failing the turn.
|
|
20
|
+
|
|
21
|
+
## The shell title
|
|
22
|
+
|
|
23
|
+
The title is animated from a worker thread, so a busy agent turn never stalls the spinner. The
|
|
24
|
+
worker is unreferenced, so a pending frame cannot hold the process open. If worker threads are
|
|
25
|
+
unavailable, or the worker fails or exits, the last command replays on the main thread.
|
|
26
|
+
|
|
27
|
+
Titles are only written to an attached terminal: `rpc` sessions and headless ones get none, so
|
|
28
|
+
escape sequences never end up in machine-read output.
|
|
29
|
+
|
|
30
|
+
## Quiet by design
|
|
31
|
+
|
|
32
|
+
- A detached subagent (`PI_SUBAGENT_CHILD`) registers nothing. It shares its parent's terminal and
|
|
33
|
+
desktop, and a second voice reporting the same run is noise.
|
|
34
|
+
- Dialogs only notify while the agent holds the turn. A dialog the user opened is already in front
|
|
35
|
+
of them.
|
|
36
|
+
- An `ask_user_question` prompt notifies from its own event, and the dialog it then opens stays
|
|
37
|
+
silent, so one question is announced once.
|
|
38
|
+
- A settled run with pending follow-up messages notifies nothing: the agent is about to keep going.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
DoomPi depends on this package and activates it as fixed host core, so a DoomPi install already has
|
|
43
|
+
it. It is not selectable from `.doom/modes.yaml`.
|
|
44
|
+
|
|
45
|
+
## License
|
|
46
|
+
|
|
47
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,s)=>(s=n==null?{}:e(i(n)),o(r||!n||!n.__esModule||!a.call(n,`default`)?t(s,`default`,{value:n,enumerable:!0}):s,n));exports.__toESM=s;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../../services/notificationPolicy.cjs"),t=require("../../services/notificationText.cjs"),n=require("../shellTitleController.cjs"),r=require("../systemNotification.cjs");let i=require("@agimon-ai/doompi-extension-contracts"),a=require("@agimon-ai/doompi-extension-contracts/child-process"),o=require("@deepseek-ai/cordis");function s(e,t,n){let r=e.confirm.bind(e);e.confirm=(e,i,a)=>(t()&&n(`${e}: ${i}`),r(e,i,a));let i=e.select.bind(e);e.select=(e,r,a)=>(t()&&n(e),i(e,r,a));let a=e.input.bind(e);e.input=(e,r,i)=>(t()&&n(e),a(e,r,i));let o=e.editor.bind(e);e.editor=(e,r)=>(t()&&n(e),o(e,r))}async function c(c,l={}){if((l.environment??process.env)[a.SUBAGENT_CHILD_ENV])return;let u=new o.Context,d,f=()=>(d??=u.fiber.dispose(),d);try{u.effect(function*(){let a=l.titleController??n.createWorkerTitleController(),o=new WeakSet,u=!0,d=!1,f=!1,p,m,h=e=>t.shellTabTitle({cwd:e,sessionName:c.getSessionName(),prompt:p}),g=t=>{if(!(!u||!e.supportsShellTitle(t)))return m={cwd:t.cwd,write:e=>t.ui.setTitle(e)},m},_=e=>{u&&r.sendSystemNotification(c,t.attentionNotification(e))},v=(0,i.subscribeToAskUserEvents)(c.events,{onPrompt(t){_(e.askUserPromptBody(t.questions))},onBlocked(e){f=e.active}});yield()=>v.dispose(),yield()=>{m&&a.dispose(h(m.cwd),m.write),m=void 0},yield()=>{u=!1,d=!1,f=!1},c.on(`input`,(e,n)=>{if(!u||p||e.source===`extension`)return;p=t.promptTitle(e.text);let r=p?g(n):void 0;r&&a.set(h(r.cwd),r.write)}),c.on(`session_start`,(t,n)=>{!u||o.has(n.ui)||(o.add(n.ui),s(n.ui,()=>u&&e.warrantsAttentionNotification({agentRunning:d,askUserBlocked:f}),_))}),c.on(`agent_start`,(e,t)=>{if(!u)return;d=!0;let n=g(t);n&&a.start(h(n.cwd),n.write)}),c.on(`agent_settled`,async(n,i)=>{if(!u)return;d=!1,f=!1;let o=g(i);o&&a.stop(h(o.cwd),o.write),e.warrantsSettledNotification(i.hasPendingMessages())&&await r.sendSystemNotification(c,t.settledNotification({cwd:i.cwd,sessionName:c.getSessionName()}))})},`@agimon-ai/doompi-notification`)}catch(e){throw await f(),e}c.on(`session_shutdown`,f)}exports.default=c,exports.notificationExtension=c;
|
|
2
|
+
//# sourceMappingURL=extension.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension.cjs","names":["SUBAGENT_CHILD_ENV","Context","createWorkerTitleController","shellTabTitle","supportsShellTitle","sendSystemNotification","attentionNotification","subscribeToAskUserEvents","askUserPromptBody","promptTitle","warrantsAttentionNotification","warrantsSettledNotification","settledNotification"],"sources":["../../../src/adapters/pi/extension.ts"],"sourcesContent":["import { subscribeToAskUserEvents } from '@agimon-ai/doompi-extension-contracts';\nimport { SUBAGENT_CHILD_ENV } from '@agimon-ai/doompi-extension-contracts/child-process';\nimport { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionAPI, ExtensionContext, ExtensionUIContext } from '@earendil-works/pi-coding-agent';\nimport {\n askUserPromptBody,\n supportsShellTitle,\n warrantsAttentionNotification,\n warrantsSettledNotification,\n} from '../../services/notificationPolicy.ts';\nimport {\n attentionNotification,\n promptTitle,\n settledNotification,\n shellTabTitle,\n} from '../../services/notificationText.ts';\nimport type { ShellTitleController, WriteTitle } from '../../types/notifications.ts';\nimport { createWorkerTitleController } from '../shellTitleController.ts';\nimport { sendSystemNotification } from '../systemNotification.ts';\n\nconst AGENT_SETTLED_EVENT = 'agent_settled';\nconst AGENT_START_EVENT = 'agent_start';\nconst EXTENSION_INPUT_SOURCE = 'extension';\nconst INPUT_EVENT = 'input';\nconst PACKAGE_SOURCE = '@agimon-ai/doompi-notification';\nconst SESSION_SHUTDOWN_EVENT = 'session_shutdown';\nconst SESSION_START_EVENT = 'session_start';\n\nexport interface NotificationExtensionOptions {\n /** Replaces the worker-thread animator, which a host without worker threads cannot start. */\n titleController?: ShellTitleController;\n environment?: NodeJS.ProcessEnv;\n}\n\n/** Where the tab title is written, remembered so shutdown can restore it without an event. */\ninterface TitleSurface {\n cwd: string;\n write: WriteTitle;\n}\n\n/**\n * Announces every dialog the agent opens on its own initiative.\n *\n * Pi has no event for \"a dialog is showing\", so the only way to catch one is to\n * wrap the UI context the session hands out. Each wrapper delegates to the\n * original, so nothing about the dialog itself changes.\n */\nfunction wrapAttentionDialogs(\n ui: ExtensionUIContext,\n shouldNotify: () => boolean,\n notify: (body: string) => void,\n): void {\n const originalConfirm = ui.confirm.bind(ui);\n ui.confirm = (title, message, options) => {\n if (shouldNotify()) notify(`${title}: ${message}`);\n return originalConfirm(title, message, options);\n };\n\n const originalSelect = ui.select.bind(ui);\n ui.select = (title, options, dialogOptions) => {\n if (shouldNotify()) notify(title);\n return originalSelect(title, options, dialogOptions);\n };\n\n const originalInput = ui.input.bind(ui);\n ui.input = (title, placeholder, options) => {\n if (shouldNotify()) notify(title);\n return originalInput(title, placeholder, options);\n };\n\n const originalEditor = ui.editor.bind(ui);\n ui.editor = (title, prefill) => {\n if (shouldNotify()) notify(title);\n return originalEditor(title, prefill);\n };\n}\n\n/**\n * The package's single standard Pi factory, and the owner of the cordis Context.\n *\n * Pi can reload an extension in-process, so every registration is yielded to the\n * fiber and one dispose on session shutdown unwinds them in reverse: the handlers\n * go quiet, the tab title is restored, then the event subscriptions are dropped.\n */\nexport async function notificationExtension(\n pi: ExtensionAPI,\n options: NotificationExtensionOptions = {},\n): Promise<void> {\n // A detached subagent shares its parent's terminal and desktop, so anything it\n // announced would be a second voice reporting on the same run.\n if ((options.environment ?? process.env)[SUBAGENT_CHILD_ENV]) return;\n\n const cordis = new Context();\n let disposal: Promise<void> | undefined;\n const dispose = (): Promise<void> => {\n disposal ??= cordis.fiber.dispose();\n return disposal;\n };\n\n try {\n cordis.effect(function* () {\n const titles = options.titleController ?? createWorkerTitleController();\n const wrappedUis = new WeakSet<ExtensionUIContext>();\n let active = true;\n let agentRunning = false;\n let askUserBlocked = false;\n let firstPrompt: string | undefined;\n let surface: TitleSurface | undefined;\n\n const idleTitle = (cwd: string): string =>\n shellTabTitle({ cwd, sessionName: pi.getSessionName(), prompt: firstPrompt });\n const titleSurface = (context: ExtensionContext): TitleSurface | undefined => {\n if (!active || !supportsShellTitle(context)) return undefined;\n surface = { cwd: context.cwd, write: (title) => context.ui.setTitle(title) };\n return surface;\n };\n const notifyAttention = (body: string): void => {\n if (active) void sendSystemNotification(pi, attentionNotification(body));\n };\n\n const askUserSubscriptions = subscribeToAskUserEvents(pi.events, {\n onPrompt(prompt) {\n notifyAttention(askUserPromptBody(prompt.questions));\n },\n onBlocked(payload) {\n askUserBlocked = payload.active;\n },\n });\n yield () => askUserSubscriptions.dispose();\n\n // The fiber owns the animator, so the tab lands on its idle title however\n // the session ends — including a reload that fires no settle event.\n yield () => {\n if (surface) titles.dispose(idleTitle(surface.cwd), surface.write);\n surface = undefined;\n };\n yield () => {\n active = false;\n agentRunning = false;\n askUserBlocked = false;\n };\n\n pi.on(INPUT_EVENT, (event, context) => {\n if (!active || firstPrompt || event.source === EXTENSION_INPUT_SOURCE) return;\n\n firstPrompt = promptTitle(event.text);\n const target = firstPrompt ? titleSurface(context) : undefined;\n if (target) titles.set(idleTitle(target.cwd), target.write);\n });\n\n pi.on(SESSION_START_EVENT, (_event, context) => {\n if (!active || wrappedUis.has(context.ui)) return;\n wrappedUis.add(context.ui);\n wrapAttentionDialogs(\n context.ui,\n () => active && warrantsAttentionNotification({ agentRunning, askUserBlocked }),\n notifyAttention,\n );\n });\n\n pi.on(AGENT_START_EVENT, (_event, context) => {\n if (!active) return;\n agentRunning = true;\n const target = titleSurface(context);\n if (target) titles.start(idleTitle(target.cwd), target.write);\n });\n\n pi.on(AGENT_SETTLED_EVENT, async (_event, context) => {\n if (!active) return;\n agentRunning = false;\n askUserBlocked = false;\n const target = titleSurface(context);\n if (target) titles.stop(idleTitle(target.cwd), target.write);\n if (!warrantsSettledNotification(context.hasPendingMessages())) return;\n\n await sendSystemNotification(pi, settledNotification({ cwd: context.cwd, sessionName: pi.getSessionName() }));\n });\n }, PACKAGE_SOURCE);\n } catch (error) {\n await dispose();\n throw error;\n }\n\n pi.on(SESSION_SHUTDOWN_EVENT, dispose);\n}\n\nexport default notificationExtension;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ShellTitleController } from "../../types/notifications.cjs";
|
|
2
|
+
import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
//#region src/adapters/pi/extension.d.ts
|
|
4
|
+
interface NotificationExtensionOptions {
|
|
5
|
+
/** Replaces the worker-thread animator, which a host without worker threads cannot start. */
|
|
6
|
+
titleController?: ShellTitleController;
|
|
7
|
+
environment?: NodeJS.ProcessEnv;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The package's single standard Pi factory, and the owner of the cordis Context.
|
|
11
|
+
*
|
|
12
|
+
* Pi can reload an extension in-process, so every registration is yielded to the
|
|
13
|
+
* fiber and one dispose on session shutdown unwinds them in reverse: the handlers
|
|
14
|
+
* go quiet, the tab title is restored, then the event subscriptions are dropped.
|
|
15
|
+
*/
|
|
16
|
+
declare function notificationExtension(pi: ExtensionAPI, options?: NotificationExtensionOptions): Promise<void>;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { NotificationExtensionOptions, notificationExtension as default, notificationExtension };
|
|
19
|
+
//# sourceMappingURL=extension.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension.d.cts","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"mappings":";;;UA4BiB;;EAEf,kBAAkB;EAClB,cAAc,OAAO;;;;;;;;;iBAqDD,sBACpB,IAAI,cACJ,UAAS,+BACR"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ShellTitleController } from "../../types/notifications.mjs";
|
|
2
|
+
import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
//#region src/adapters/pi/extension.d.ts
|
|
4
|
+
interface NotificationExtensionOptions {
|
|
5
|
+
/** Replaces the worker-thread animator, which a host without worker threads cannot start. */
|
|
6
|
+
titleController?: ShellTitleController;
|
|
7
|
+
environment?: NodeJS.ProcessEnv;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The package's single standard Pi factory, and the owner of the cordis Context.
|
|
11
|
+
*
|
|
12
|
+
* Pi can reload an extension in-process, so every registration is yielded to the
|
|
13
|
+
* fiber and one dispose on session shutdown unwinds them in reverse: the handlers
|
|
14
|
+
* go quiet, the tab title is restored, then the event subscriptions are dropped.
|
|
15
|
+
*/
|
|
16
|
+
declare function notificationExtension(pi: ExtensionAPI, options?: NotificationExtensionOptions): Promise<void>;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { NotificationExtensionOptions, notificationExtension as default, notificationExtension };
|
|
19
|
+
//# sourceMappingURL=extension.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension.d.mts","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"mappings":";;;UA4BiB;;EAEf,kBAAkB;EAClB,cAAc,OAAO;;;;;;;;;iBAqDD,sBACpB,IAAI,cACJ,UAAS,+BACR"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{askUserPromptBody as e,supportsShellTitle as t,warrantsAttentionNotification as n,warrantsSettledNotification as r}from"../../services/notificationPolicy.mjs";import{attentionNotification as i,promptTitle as a,settledNotification as o,shellTabTitle as s}from"../../services/notificationText.mjs";import{createWorkerTitleController as c}from"../shellTitleController.mjs";import{sendSystemNotification as l}from"../systemNotification.mjs";import{subscribeToAskUserEvents as u}from"@agimon-ai/doompi-extension-contracts";import{SUBAGENT_CHILD_ENV as d}from"@agimon-ai/doompi-extension-contracts/child-process";import{Context as f}from"@deepseek-ai/cordis";function p(e,t,n){let r=e.confirm.bind(e);e.confirm=(e,i,a)=>(t()&&n(`${e}: ${i}`),r(e,i,a));let i=e.select.bind(e);e.select=(e,r,a)=>(t()&&n(e),i(e,r,a));let a=e.input.bind(e);e.input=(e,r,i)=>(t()&&n(e),a(e,r,i));let o=e.editor.bind(e);e.editor=(e,r)=>(t()&&n(e),o(e,r))}async function m(m,h={}){if((h.environment??process.env)[d])return;let g=new f,_,v=()=>(_??=g.fiber.dispose(),_);try{g.effect(function*(){let d=h.titleController??c(),f=new WeakSet,g=!0,_=!1,v=!1,y,b,x=e=>s({cwd:e,sessionName:m.getSessionName(),prompt:y}),S=e=>{if(!(!g||!t(e)))return b={cwd:e.cwd,write:t=>e.ui.setTitle(t)},b},C=e=>{g&&l(m,i(e))},w=u(m.events,{onPrompt(t){C(e(t.questions))},onBlocked(e){v=e.active}});yield()=>w.dispose(),yield()=>{b&&d.dispose(x(b.cwd),b.write),b=void 0},yield()=>{g=!1,_=!1,v=!1},m.on(`input`,(e,t)=>{if(!g||y||e.source===`extension`)return;y=a(e.text);let n=y?S(t):void 0;n&&d.set(x(n.cwd),n.write)}),m.on(`session_start`,(e,t)=>{!g||f.has(t.ui)||(f.add(t.ui),p(t.ui,()=>g&&n({agentRunning:_,askUserBlocked:v}),C))}),m.on(`agent_start`,(e,t)=>{if(!g)return;_=!0;let n=S(t);n&&d.start(x(n.cwd),n.write)}),m.on(`agent_settled`,async(e,t)=>{if(!g)return;_=!1,v=!1;let n=S(t);n&&d.stop(x(n.cwd),n.write),r(t.hasPendingMessages())&&await l(m,o({cwd:t.cwd,sessionName:m.getSessionName()}))})},`@agimon-ai/doompi-notification`)}catch(e){throw await v(),e}m.on(`session_shutdown`,v)}export{m as default,m as notificationExtension};
|
|
2
|
+
//# sourceMappingURL=extension.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension.mjs","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"sourcesContent":["import { subscribeToAskUserEvents } from '@agimon-ai/doompi-extension-contracts';\nimport { SUBAGENT_CHILD_ENV } from '@agimon-ai/doompi-extension-contracts/child-process';\nimport { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionAPI, ExtensionContext, ExtensionUIContext } from '@earendil-works/pi-coding-agent';\nimport {\n askUserPromptBody,\n supportsShellTitle,\n warrantsAttentionNotification,\n warrantsSettledNotification,\n} from '../../services/notificationPolicy.ts';\nimport {\n attentionNotification,\n promptTitle,\n settledNotification,\n shellTabTitle,\n} from '../../services/notificationText.ts';\nimport type { ShellTitleController, WriteTitle } from '../../types/notifications.ts';\nimport { createWorkerTitleController } from '../shellTitleController.ts';\nimport { sendSystemNotification } from '../systemNotification.ts';\n\nconst AGENT_SETTLED_EVENT = 'agent_settled';\nconst AGENT_START_EVENT = 'agent_start';\nconst EXTENSION_INPUT_SOURCE = 'extension';\nconst INPUT_EVENT = 'input';\nconst PACKAGE_SOURCE = '@agimon-ai/doompi-notification';\nconst SESSION_SHUTDOWN_EVENT = 'session_shutdown';\nconst SESSION_START_EVENT = 'session_start';\n\nexport interface NotificationExtensionOptions {\n /** Replaces the worker-thread animator, which a host without worker threads cannot start. */\n titleController?: ShellTitleController;\n environment?: NodeJS.ProcessEnv;\n}\n\n/** Where the tab title is written, remembered so shutdown can restore it without an event. */\ninterface TitleSurface {\n cwd: string;\n write: WriteTitle;\n}\n\n/**\n * Announces every dialog the agent opens on its own initiative.\n *\n * Pi has no event for \"a dialog is showing\", so the only way to catch one is to\n * wrap the UI context the session hands out. Each wrapper delegates to the\n * original, so nothing about the dialog itself changes.\n */\nfunction wrapAttentionDialogs(\n ui: ExtensionUIContext,\n shouldNotify: () => boolean,\n notify: (body: string) => void,\n): void {\n const originalConfirm = ui.confirm.bind(ui);\n ui.confirm = (title, message, options) => {\n if (shouldNotify()) notify(`${title}: ${message}`);\n return originalConfirm(title, message, options);\n };\n\n const originalSelect = ui.select.bind(ui);\n ui.select = (title, options, dialogOptions) => {\n if (shouldNotify()) notify(title);\n return originalSelect(title, options, dialogOptions);\n };\n\n const originalInput = ui.input.bind(ui);\n ui.input = (title, placeholder, options) => {\n if (shouldNotify()) notify(title);\n return originalInput(title, placeholder, options);\n };\n\n const originalEditor = ui.editor.bind(ui);\n ui.editor = (title, prefill) => {\n if (shouldNotify()) notify(title);\n return originalEditor(title, prefill);\n };\n}\n\n/**\n * The package's single standard Pi factory, and the owner of the cordis Context.\n *\n * Pi can reload an extension in-process, so every registration is yielded to the\n * fiber and one dispose on session shutdown unwinds them in reverse: the handlers\n * go quiet, the tab title is restored, then the event subscriptions are dropped.\n */\nexport async function notificationExtension(\n pi: ExtensionAPI,\n options: NotificationExtensionOptions = {},\n): Promise<void> {\n // A detached subagent shares its parent's terminal and desktop, so anything it\n // announced would be a second voice reporting on the same run.\n if ((options.environment ?? process.env)[SUBAGENT_CHILD_ENV]) return;\n\n const cordis = new Context();\n let disposal: Promise<void> | undefined;\n const dispose = (): Promise<void> => {\n disposal ??= cordis.fiber.dispose();\n return disposal;\n };\n\n try {\n cordis.effect(function* () {\n const titles = options.titleController ?? createWorkerTitleController();\n const wrappedUis = new WeakSet<ExtensionUIContext>();\n let active = true;\n let agentRunning = false;\n let askUserBlocked = false;\n let firstPrompt: string | undefined;\n let surface: TitleSurface | undefined;\n\n const idleTitle = (cwd: string): string =>\n shellTabTitle({ cwd, sessionName: pi.getSessionName(), prompt: firstPrompt });\n const titleSurface = (context: ExtensionContext): TitleSurface | undefined => {\n if (!active || !supportsShellTitle(context)) return undefined;\n surface = { cwd: context.cwd, write: (title) => context.ui.setTitle(title) };\n return surface;\n };\n const notifyAttention = (body: string): void => {\n if (active) void sendSystemNotification(pi, attentionNotification(body));\n };\n\n const askUserSubscriptions = subscribeToAskUserEvents(pi.events, {\n onPrompt(prompt) {\n notifyAttention(askUserPromptBody(prompt.questions));\n },\n onBlocked(payload) {\n askUserBlocked = payload.active;\n },\n });\n yield () => askUserSubscriptions.dispose();\n\n // The fiber owns the animator, so the tab lands on its idle title however\n // the session ends — including a reload that fires no settle event.\n yield () => {\n if (surface) titles.dispose(idleTitle(surface.cwd), surface.write);\n surface = undefined;\n };\n yield () => {\n active = false;\n agentRunning = false;\n askUserBlocked = false;\n };\n\n pi.on(INPUT_EVENT, (event, context) => {\n if (!active || firstPrompt || event.source === EXTENSION_INPUT_SOURCE) return;\n\n firstPrompt = promptTitle(event.text);\n const target = firstPrompt ? titleSurface(context) : undefined;\n if (target) titles.set(idleTitle(target.cwd), target.write);\n });\n\n pi.on(SESSION_START_EVENT, (_event, context) => {\n if (!active || wrappedUis.has(context.ui)) return;\n wrappedUis.add(context.ui);\n wrapAttentionDialogs(\n context.ui,\n () => active && warrantsAttentionNotification({ agentRunning, askUserBlocked }),\n notifyAttention,\n );\n });\n\n pi.on(AGENT_START_EVENT, (_event, context) => {\n if (!active) return;\n agentRunning = true;\n const target = titleSurface(context);\n if (target) titles.start(idleTitle(target.cwd), target.write);\n });\n\n pi.on(AGENT_SETTLED_EVENT, async (_event, context) => {\n if (!active) return;\n agentRunning = false;\n askUserBlocked = false;\n const target = titleSurface(context);\n if (target) titles.stop(idleTitle(target.cwd), target.write);\n if (!warrantsSettledNotification(context.hasPendingMessages())) return;\n\n await sendSystemNotification(pi, settledNotification({ cwd: context.cwd, sessionName: pi.getSessionName() }));\n });\n }, PACKAGE_SOURCE);\n } catch (error) {\n await dispose();\n throw error;\n }\n\n pi.on(SESSION_SHUTDOWN_EVENT, dispose);\n}\n\nexport default notificationExtension;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
let e=require("node:worker_threads");const t=[`⠋`,`⠙`,`⠹`,`⠸`,`⠼`,`⠴`,`⠦`,`⠧`,`⠇`,`⠏`];function n(){let e=0,n,r=()=>{n&&clearInterval(n),n=void 0,e=0};return{set(e,t){r(),t(e)},start(i,a){r();let o=()=>{a(`${t[e%t.length]} ${i}`),e+=1};o(),n=setInterval(o,80)},stop(e,t){r(),t(e)},dispose(e,t){r(),t(e)}}}function r(){let r=n(),i,a,o,s=()=>{o=void 0,!(!i||!a)&&r[i.action](i.title,a)};try{let n=new e.Worker(`
|
|
2
|
+
const { writeSync } = require('node:fs');
|
|
3
|
+
const { parentPort, workerData } = require('node:worker_threads');
|
|
4
|
+
let frameIndex = 0;
|
|
5
|
+
let timer;
|
|
6
|
+
const writeTitle = (title) => writeSync(1, '\\u001b]0;' + title + '\\u0007');
|
|
7
|
+
const stop = () => {
|
|
8
|
+
if (timer) clearInterval(timer);
|
|
9
|
+
timer = undefined;
|
|
10
|
+
frameIndex = 0;
|
|
11
|
+
};
|
|
12
|
+
parentPort.on('message', (command) => {
|
|
13
|
+
stop();
|
|
14
|
+
if (command.action === 'start') {
|
|
15
|
+
const render = () => {
|
|
16
|
+
writeTitle(workerData.frames[frameIndex % workerData.frames.length] + ' ' + command.title);
|
|
17
|
+
frameIndex += 1;
|
|
18
|
+
};
|
|
19
|
+
render();
|
|
20
|
+
timer = setInterval(render, workerData.intervalMs);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
writeTitle(command.title);
|
|
24
|
+
if (command.action === 'dispose') parentPort.close();
|
|
25
|
+
});
|
|
26
|
+
`,{eval:!0,workerData:{frames:t,intervalMs:80}});n.unref(),n.once(`error`,s),n.once(`exit`,e=>{e!==0&&o===n&&s()}),o=n}catch{o=void 0}let c=(e,t)=>{if(i=e,a=t,!o){r[e.action](e.title,t);return}try{o.postMessage(e)}catch{s()}};return{set:(e,t)=>c({action:`set`,title:e},t),start:(e,t)=>c({action:`start`,title:e},t),stop:(e,t)=>c({action:`stop`,title:e},t),dispose:(e,t)=>c({action:`dispose`,title:e},t)}}exports.createMainThreadTitleController=n,exports.createWorkerTitleController=r;
|
|
27
|
+
//# sourceMappingURL=shellTitleController.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shellTitleController.cjs","names":["Worker"],"sources":["../../src/adapters/shellTitleController.ts"],"sourcesContent":["import { Worker } from 'node:worker_threads';\nimport type { ShellTitleCommand, ShellTitleController, WriteTitle } from '../types/notifications.ts';\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"],"mappings":"qCAGA,MAAM,EAAiB,CAAC,IAAK,IAAK,IAAK,IAAK,IAAK,IAAK,IAAK,IAAK,IAAK,GAAG,EAoCxE,SAAgB,GAAwD,CACtE,IAAI,EAAa,EACb,EACE,MAAsB,CACtB,GAAO,cAAc,CAAK,EAC9B,EAAQ,IAAA,GACR,EAAa,CACf,EAEA,MAAO,CACL,IAAI,EAAO,EAAO,CAChB,EAAc,EACd,EAAM,CAAK,CACb,EACA,MAAM,EAAO,EAAO,CAClB,EAAc,EACd,IAAM,MAAe,CACnB,EAAM,GAAG,EAAe,EAAa,EAAe,QAAQ,GAAG,GAAO,EACtE,GAAc,CAChB,EACA,EAAO,EACP,EAAQ,YAAY,EAAQ,EAAuB,CACrD,EACA,KAAK,EAAO,EAAO,CACjB,EAAc,EACd,EAAM,CAAK,CACb,EACA,QAAQ,EAAO,EAAO,CACpB,EAAc,EACd,EAAM,CAAK,CACb,CACF,CACF,CAOA,SAAgB,GAAoD,CAClE,IAAM,EAAW,EAAgC,EAC7C,EACA,EACA,EAEE,MAAiB,CACrB,EAAS,IAAA,GACL,GAAC,GAAe,CAAC,IACrB,EAAS,EAAY,OAAO,CAAC,EAAY,MAAO,CAAS,CAC3D,EAEA,GAAI,CACF,IAAM,EAAc,IAAIA,EAAAA,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;EAAqB,CAClD,KAAM,GACN,WAAY,CAAE,OAAQ,EAAgB,WAAY,EAAwB,CAC5E,CAAC,EACD,EAAY,MAAM,EAClB,EAAY,KAAK,QAAS,CAAQ,EAClC,EAAY,KAAK,OAAS,GAAS,CAC7B,IAAS,GAAK,IAAW,GAAa,EAAS,CACrD,CAAC,EACD,EAAS,CACX,MAAQ,CAEN,EAAS,IAAA,EACX,CAEA,IAAM,GAAY,EAA4B,IAAsB,CAGlE,GAFA,EAAc,EACd,EAAY,EACR,CAAC,EAAQ,CACX,EAAS,EAAQ,OAAO,CAAC,EAAQ,MAAO,CAAK,EAC7C,MACF,CACA,GAAI,CACF,EAAO,YAAY,CAAO,CAC5B,MAAQ,CAEN,EAAS,CACX,CACF,EAEA,MAAO,CACL,KAAM,EAAO,IAAU,EAAS,CAAE,OAAQ,MAAO,OAAM,EAAG,CAAK,EAC/D,OAAQ,EAAO,IAAU,EAAS,CAAE,OAAQ,QAAS,OAAM,EAAG,CAAK,EACnE,MAAO,EAAO,IAAU,EAAS,CAAE,OAAQ,OAAQ,OAAM,EAAG,CAAK,EACjE,SAAU,EAAO,IAAU,EAAS,CAAE,OAAQ,UAAW,OAAM,EAAG,CAAK,CACzE,CACF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ShellTitleController } from "../types/notifications.cjs";
|
|
2
|
+
//#region src/adapters/shellTitleController.d.ts
|
|
3
|
+
/** Animates on the main thread, for hosts where a worker could not be started. */
|
|
4
|
+
declare function createMainThreadTitleController(): ShellTitleController;
|
|
5
|
+
/**
|
|
6
|
+
* Animates from a worker thread, replaying the last command on the main thread
|
|
7
|
+
* if the worker never started, failed, or exited under us. The worker is
|
|
8
|
+
* unreferenced so a pending frame never holds the process open.
|
|
9
|
+
*/
|
|
10
|
+
declare function createWorkerTitleController(): ShellTitleController;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { createMainThreadTitleController, createWorkerTitleController };
|
|
13
|
+
//# sourceMappingURL=shellTitleController.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shellTitleController.d.cts","names":[],"sources":["../../src/adapters/shellTitleController.ts"],"mappings":";;;iBAuCgB,mCAAmC;;;;;;iBAuCnC,+BAA+B"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ShellTitleController } from "../types/notifications.mjs";
|
|
2
|
+
//#region src/adapters/shellTitleController.d.ts
|
|
3
|
+
/** Animates on the main thread, for hosts where a worker could not be started. */
|
|
4
|
+
declare function createMainThreadTitleController(): ShellTitleController;
|
|
5
|
+
/**
|
|
6
|
+
* Animates from a worker thread, replaying the last command on the main thread
|
|
7
|
+
* if the worker never started, failed, or exited under us. The worker is
|
|
8
|
+
* unreferenced so a pending frame never holds the process open.
|
|
9
|
+
*/
|
|
10
|
+
declare function createWorkerTitleController(): ShellTitleController;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { createMainThreadTitleController, createWorkerTitleController };
|
|
13
|
+
//# sourceMappingURL=shellTitleController.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shellTitleController.d.mts","names":[],"sources":["../../src/adapters/shellTitleController.ts"],"mappings":";;;iBAuCgB,mCAAmC;;;;;;iBAuCnC,+BAA+B"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import{Worker as e}from"node:worker_threads";const t=[`⠋`,`⠙`,`⠹`,`⠸`,`⠼`,`⠴`,`⠦`,`⠧`,`⠇`,`⠏`];function n(){let e=0,n,r=()=>{n&&clearInterval(n),n=void 0,e=0};return{set(e,t){r(),t(e)},start(i,a){r();let o=()=>{a(`${t[e%t.length]} ${i}`),e+=1};o(),n=setInterval(o,80)},stop(e,t){r(),t(e)},dispose(e,t){r(),t(e)}}}function r(){let r=n(),i,a,o,s=()=>{o=void 0,!(!i||!a)&&r[i.action](i.title,a)};try{let n=new e(`
|
|
2
|
+
const { writeSync } = require('node:fs');
|
|
3
|
+
const { parentPort, workerData } = require('node:worker_threads');
|
|
4
|
+
let frameIndex = 0;
|
|
5
|
+
let timer;
|
|
6
|
+
const writeTitle = (title) => writeSync(1, '\\u001b]0;' + title + '\\u0007');
|
|
7
|
+
const stop = () => {
|
|
8
|
+
if (timer) clearInterval(timer);
|
|
9
|
+
timer = undefined;
|
|
10
|
+
frameIndex = 0;
|
|
11
|
+
};
|
|
12
|
+
parentPort.on('message', (command) => {
|
|
13
|
+
stop();
|
|
14
|
+
if (command.action === 'start') {
|
|
15
|
+
const render = () => {
|
|
16
|
+
writeTitle(workerData.frames[frameIndex % workerData.frames.length] + ' ' + command.title);
|
|
17
|
+
frameIndex += 1;
|
|
18
|
+
};
|
|
19
|
+
render();
|
|
20
|
+
timer = setInterval(render, workerData.intervalMs);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
writeTitle(command.title);
|
|
24
|
+
if (command.action === 'dispose') parentPort.close();
|
|
25
|
+
});
|
|
26
|
+
`,{eval:!0,workerData:{frames:t,intervalMs:80}});n.unref(),n.once(`error`,s),n.once(`exit`,e=>{e!==0&&o===n&&s()}),o=n}catch{o=void 0}let c=(e,t)=>{if(i=e,a=t,!o){r[e.action](e.title,t);return}try{o.postMessage(e)}catch{s()}};return{set:(e,t)=>c({action:`set`,title:e},t),start:(e,t)=>c({action:`start`,title:e},t),stop:(e,t)=>c({action:`stop`,title:e},t),dispose:(e,t)=>c({action:`dispose`,title:e},t)}}export{n as createMainThreadTitleController,r as createWorkerTitleController};
|
|
27
|
+
//# sourceMappingURL=shellTitleController.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shellTitleController.mjs","names":[],"sources":["../../src/adapters/shellTitleController.ts"],"sourcesContent":["import { Worker } from 'node:worker_threads';\nimport type { ShellTitleCommand, ShellTitleController, WriteTitle } from '../types/notifications.ts';\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"],"mappings":"6CAGA,MAAM,EAAiB,CAAC,IAAK,IAAK,IAAK,IAAK,IAAK,IAAK,IAAK,IAAK,IAAK,GAAG,EAoCxE,SAAgB,GAAwD,CACtE,IAAI,EAAa,EACb,EACE,MAAsB,CACtB,GAAO,cAAc,CAAK,EAC9B,EAAQ,IAAA,GACR,EAAa,CACf,EAEA,MAAO,CACL,IAAI,EAAO,EAAO,CAChB,EAAc,EACd,EAAM,CAAK,CACb,EACA,MAAM,EAAO,EAAO,CAClB,EAAc,EACd,IAAM,MAAe,CACnB,EAAM,GAAG,EAAe,EAAa,EAAe,QAAQ,GAAG,GAAO,EACtE,GAAc,CAChB,EACA,EAAO,EACP,EAAQ,YAAY,EAAQ,EAAuB,CACrD,EACA,KAAK,EAAO,EAAO,CACjB,EAAc,EACd,EAAM,CAAK,CACb,EACA,QAAQ,EAAO,EAAO,CACpB,EAAc,EACd,EAAM,CAAK,CACb,CACF,CACF,CAOA,SAAgB,GAAoD,CAClE,IAAM,EAAW,EAAgC,EAC7C,EACA,EACA,EAEE,MAAiB,CACrB,EAAS,IAAA,GACL,GAAC,GAAe,CAAC,IACrB,EAAS,EAAY,OAAO,CAAC,EAAY,MAAO,CAAS,CAC3D,EAEA,GAAI,CACF,IAAM,EAAc,IAAI,EAAO;;;;;;;;;;;;;;;;;;;;;;;;;EAAqB,CAClD,KAAM,GACN,WAAY,CAAE,OAAQ,EAAgB,WAAY,EAAwB,CAC5E,CAAC,EACD,EAAY,MAAM,EAClB,EAAY,KAAK,QAAS,CAAQ,EAClC,EAAY,KAAK,OAAS,GAAS,CAC7B,IAAS,GAAK,IAAW,GAAa,EAAS,CACrD,CAAC,EACD,EAAS,CACX,MAAQ,CAEN,EAAS,IAAA,EACX,CAEA,IAAM,GAAY,EAA4B,IAAsB,CAGlE,GAFA,EAAc,EACd,EAAY,EACR,CAAC,EAAQ,CACX,EAAS,EAAQ,OAAO,CAAC,EAAQ,MAAO,CAAK,EAC7C,MACF,CACA,GAAI,CACF,EAAO,YAAY,CAAO,CAC5B,MAAQ,CAEN,EAAS,CACX,CACF,EAEA,MAAO,CACL,KAAM,EAAO,IAAU,EAAS,CAAE,OAAQ,MAAO,OAAM,EAAG,CAAK,EAC/D,OAAQ,EAAO,IAAU,EAAS,CAAE,OAAQ,QAAS,OAAM,EAAG,CAAK,EACnE,MAAO,EAAO,IAAU,EAAS,CAAE,OAAQ,OAAQ,OAAM,EAAG,CAAK,EACjE,SAAU,EAAO,IAAU,EAAS,CAAE,OAAQ,UAAW,OAAM,EAAG,CAAK,CACzE,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../services/notificationText.cjs");async function t(e,t,n){try{return(await e.exec(t,n,{timeout:3e3})).code===0}catch{return!1}}async function n(n,r,i=process.platform){let a=e.notificationBody(r.body);await t(n,`cmux`,[`notify`,`--title`,r.title,`--subtitle`,r.subtitle,`--body`,a])||i!==`darwin`||await t(n,`osascript`,[`-e`,`on run argv`,`-e`,`display notification (item 3 of argv) with title (item 1 of argv) subtitle (item 2 of argv)`,`-e`,`end run`,r.title,r.subtitle,a])}exports.sendSystemNotification=n;
|
|
2
|
+
//# sourceMappingURL=systemNotification.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"systemNotification.cjs","names":["notificationBody"],"sources":["../../src/adapters/systemNotification.ts"],"sourcesContent":["import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { notificationBody } from '../services/notificationText.ts';\nimport type { DesktopNotification } from '../types/notifications.ts';\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: ExtensionAPI, 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: ExtensionAPI,\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"],"mappings":"oDAUA,eAAe,EAAQ,EAAkB,EAAiB,EAAkC,CAC1F,GAAI,CAEF,OAAO,MADc,EAAG,KAAK,EAAS,EAAM,CAAE,QAAS,GAAmB,CAAC,EAAA,CAC7D,OAAS,CACzB,MAAQ,CAGN,MAAO,EACT,CACF,CASA,eAAsB,EACpB,EACA,EACA,EAAmB,QAAQ,SACZ,CACf,IAAM,EAAOA,EAAAA,iBAAiB,EAAa,IAAI,EAU3C,MATqB,EAAQ,EAAI,OAAc,CACjD,SACA,UACA,EAAa,MACb,aACA,EAAa,SACb,SACA,CACF,CAAC,GACiB,IAAa,UAE/B,MAAM,EAAQ,EAAI,YAAmB,CACnC,KACA,cACA,KACA,8FACA,KACA,UACA,EAAa,MACb,EAAa,SACb,CACF,CAAC,CACH"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { DesktopNotification } from "../types/notifications.cjs";
|
|
2
|
+
import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
//#region src/adapters/systemNotification.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Delivers one desktop notification on a best-effort basis.
|
|
6
|
+
*
|
|
7
|
+
* cmux is tried first because it routes the notification to the window the
|
|
8
|
+
* session actually lives in. Only macOS has a second option worth attempting,
|
|
9
|
+
* and a host with neither stays silent rather than failing the turn.
|
|
10
|
+
*/
|
|
11
|
+
declare function sendSystemNotification(pi: ExtensionAPI, notification: DesktopNotification, platform?: string): Promise<void>;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { sendSystemNotification };
|
|
14
|
+
//# sourceMappingURL=systemNotification.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"systemNotification.d.cts","names":[],"sources":["../../src/adapters/systemNotification.ts"],"mappings":";;;;;;;;;;iBA4BsB,uBACpB,IAAI,cACJ,cAAc,qBACd,oBACC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { DesktopNotification } from "../types/notifications.mjs";
|
|
2
|
+
import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
//#region src/adapters/systemNotification.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Delivers one desktop notification on a best-effort basis.
|
|
6
|
+
*
|
|
7
|
+
* cmux is tried first because it routes the notification to the window the
|
|
8
|
+
* session actually lives in. Only macOS has a second option worth attempting,
|
|
9
|
+
* and a host with neither stays silent rather than failing the turn.
|
|
10
|
+
*/
|
|
11
|
+
declare function sendSystemNotification(pi: ExtensionAPI, notification: DesktopNotification, platform?: string): Promise<void>;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { sendSystemNotification };
|
|
14
|
+
//# sourceMappingURL=systemNotification.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"systemNotification.d.mts","names":[],"sources":["../../src/adapters/systemNotification.ts"],"mappings":";;;;;;;;;;iBA4BsB,uBACpB,IAAI,cACJ,cAAc,qBACd,oBACC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{notificationBody as e}from"../services/notificationText.mjs";async function t(e,t,n){try{return(await e.exec(t,n,{timeout:3e3})).code===0}catch{return!1}}async function n(n,r,i=process.platform){let a=e(r.body);await t(n,`cmux`,[`notify`,`--title`,r.title,`--subtitle`,r.subtitle,`--body`,a])||i!==`darwin`||await t(n,`osascript`,[`-e`,`on run argv`,`-e`,`display notification (item 3 of argv) with title (item 1 of argv) subtitle (item 2 of argv)`,`-e`,`end run`,r.title,r.subtitle,a])}export{n as sendSystemNotification};
|
|
2
|
+
//# sourceMappingURL=systemNotification.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"systemNotification.mjs","names":[],"sources":["../../src/adapters/systemNotification.ts"],"sourcesContent":["import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { notificationBody } from '../services/notificationText.ts';\nimport type { DesktopNotification } from '../types/notifications.ts';\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: ExtensionAPI, 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: ExtensionAPI,\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"],"mappings":"oEAUA,eAAe,EAAQ,EAAkB,EAAiB,EAAkC,CAC1F,GAAI,CAEF,OAAO,MADc,EAAG,KAAK,EAAS,EAAM,CAAE,QAAS,GAAmB,CAAC,EAAA,CAC7D,OAAS,CACzB,MAAQ,CAGN,MAAO,EACT,CACF,CASA,eAAsB,EACpB,EACA,EACA,EAAmB,QAAQ,SACZ,CACf,IAAM,EAAO,EAAiB,EAAa,IAAI,EAU3C,MATqB,EAAQ,EAAI,OAAc,CACjD,SACA,UACA,EAAa,MACb,aACA,EAAa,SACb,SACA,CACF,CAAC,GACiB,IAAa,UAE/B,MAAM,EAAQ,EAAI,YAAmB,CACnC,KACA,cACA,KACA,8FACA,KACA,UACA,EAAa,MACb,EAAa,SACb,CACF,CAAC,CACH"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});const e=require("../adapters/pi/extension.cjs");exports.default=e.default,exports.notificationExtension=e.default;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"../adapters/pi/extension.mjs";export{e as default,e as notificationExtension};
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./services/notificationPolicy.cjs"),t=require("./services/notificationText.cjs"),n=require("./adapters/shellTitleController.cjs"),r=require("./adapters/systemNotification.cjs"),i=require("./adapters/pi/extension.cjs");exports.askUserPromptBody=e.askUserPromptBody,exports.attentionNotification=t.attentionNotification,exports.createMainThreadTitleController=n.createMainThreadTitleController,exports.createWorkerTitleController=n.createWorkerTitleController,exports.notificationBody=t.notificationBody,exports.notificationExtension=i.default,exports.promptTitle=t.promptTitle,exports.sendSystemNotification=r.sendSystemNotification,exports.settledNotification=t.settledNotification,exports.shellTabTitle=t.shellTabTitle,exports.supportsShellTitle=e.supportsShellTitle,exports.warrantsAttentionNotification=e.warrantsAttentionNotification,exports.warrantsSettledNotification=e.warrantsSettledNotification;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { DesktopNotification, ShellTitleAction, ShellTitleCommand, ShellTitleController, WriteTitle } from "./types/notifications.cjs";
|
|
2
|
+
import notificationExtension, { NotificationExtensionOptions } from "./adapters/pi/extension.cjs";
|
|
3
|
+
import { createMainThreadTitleController, createWorkerTitleController } from "./adapters/shellTitleController.cjs";
|
|
4
|
+
import { sendSystemNotification } from "./adapters/systemNotification.cjs";
|
|
5
|
+
import { AttentionState, ShellSurface, askUserPromptBody, supportsShellTitle, warrantsAttentionNotification, warrantsSettledNotification } from "./services/notificationPolicy.cjs";
|
|
6
|
+
import { ShellTabTitleInput, attentionNotification, notificationBody, promptTitle, settledNotification, shellTabTitle } from "./services/notificationText.cjs";
|
|
7
|
+
export { type AttentionState, type DesktopNotification, type NotificationExtensionOptions, type ShellSurface, type ShellTabTitleInput, type ShellTitleAction, type ShellTitleCommand, type ShellTitleController, type WriteTitle, askUserPromptBody, attentionNotification, createMainThreadTitleController, createWorkerTitleController, notificationBody, notificationExtension, promptTitle, sendSystemNotification, settledNotification, shellTabTitle, supportsShellTitle, warrantsAttentionNotification, warrantsSettledNotification };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { DesktopNotification, ShellTitleAction, ShellTitleCommand, ShellTitleController, WriteTitle } from "./types/notifications.mjs";
|
|
2
|
+
import notificationExtension, { NotificationExtensionOptions } from "./adapters/pi/extension.mjs";
|
|
3
|
+
import { createMainThreadTitleController, createWorkerTitleController } from "./adapters/shellTitleController.mjs";
|
|
4
|
+
import { sendSystemNotification } from "./adapters/systemNotification.mjs";
|
|
5
|
+
import { AttentionState, ShellSurface, askUserPromptBody, supportsShellTitle, warrantsAttentionNotification, warrantsSettledNotification } from "./services/notificationPolicy.mjs";
|
|
6
|
+
import { ShellTabTitleInput, attentionNotification, notificationBody, promptTitle, settledNotification, shellTabTitle } from "./services/notificationText.mjs";
|
|
7
|
+
export { type AttentionState, type DesktopNotification, type NotificationExtensionOptions, type ShellSurface, type ShellTabTitleInput, type ShellTitleAction, type ShellTitleCommand, type ShellTitleController, type WriteTitle, askUserPromptBody, attentionNotification, createMainThreadTitleController, createWorkerTitleController, notificationBody, notificationExtension, promptTitle, sendSystemNotification, settledNotification, shellTabTitle, supportsShellTitle, warrantsAttentionNotification, warrantsSettledNotification };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{askUserPromptBody as e,supportsShellTitle as t,warrantsAttentionNotification as n,warrantsSettledNotification as r}from"./services/notificationPolicy.mjs";import{attentionNotification as i,notificationBody as a,promptTitle as o,settledNotification as s,shellTabTitle as c}from"./services/notificationText.mjs";import{createMainThreadTitleController as l,createWorkerTitleController as u}from"./adapters/shellTitleController.mjs";import{sendSystemNotification as d}from"./adapters/systemNotification.mjs";import f from"./adapters/pi/extension.mjs";export{e as askUserPromptBody,i as attentionNotification,l as createMainThreadTitleController,u as createWorkerTitleController,a as notificationBody,f as notificationExtension,o as promptTitle,d as sendSystemNotification,s as settledNotification,c as shellTabTitle,t as supportsShellTitle,n as warrantsAttentionNotification,r as warrantsSettledNotification};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e({agentRunning:e,askUserBlocked:t}){return e&&!t}function t(e){return!e}function n({hasUI:e,mode:t}){return e&&t!==`rpc`}function r(e){return e[0]?.question??`The agent is waiting for your feedback.`}exports.askUserPromptBody=r,exports.supportsShellTitle=n,exports.warrantsAttentionNotification=e,exports.warrantsSettledNotification=t;
|
|
2
|
+
//# sourceMappingURL=notificationPolicy.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notificationPolicy.cjs","names":[],"sources":["../../src/services/notificationPolicy.ts"],"sourcesContent":["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":"AAoBA,SAAgB,EAA8B,CAAE,eAAc,kBAA2C,CACvG,OAAO,GAAgB,CAAC,CAC1B,CAGA,SAAgB,EAA4B,EAAsC,CAChF,MAAO,CAAC,CACV,CAGA,SAAgB,EAAmB,CAAE,QAAO,QAA+B,CACzE,OAAO,GAAS,IAAS,KAC3B,CAGA,SAAgB,EAAkB,EAAoD,CACpF,OAAO,EAAU,EAAE,EAAE,UAAY,yCACnC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/services/notificationPolicy.d.ts
|
|
2
|
+
interface AttentionState {
|
|
3
|
+
agentRunning: boolean;
|
|
4
|
+
askUserBlocked: boolean;
|
|
5
|
+
}
|
|
6
|
+
interface ShellSurface {
|
|
7
|
+
hasUI: boolean;
|
|
8
|
+
mode?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A dialog is worth interrupting the user for only while the agent holds the turn.
|
|
12
|
+
*
|
|
13
|
+
* Outside a run the user opened the dialog themselves and is already looking at
|
|
14
|
+
* it. An ask-user prompt is announced by its own event, so the dialog it then
|
|
15
|
+
* opens must stay silent or the same question notifies twice.
|
|
16
|
+
*/
|
|
17
|
+
declare function warrantsAttentionNotification({ agentRunning, askUserBlocked }: AttentionState): boolean;
|
|
18
|
+
/** A queued follow-up means the agent keeps working, so the run has not stopped for the user. */
|
|
19
|
+
declare function warrantsSettledNotification(hasPendingMessages: boolean): boolean;
|
|
20
|
+
/** Terminal title escapes only mean something on an attached terminal; RPC output stays clean. */
|
|
21
|
+
declare function supportsShellTitle({ hasUI, mode }: ShellSurface): boolean;
|
|
22
|
+
/** The question an ask-user prompt is blocking on, or a stand-in when it asked none. */
|
|
23
|
+
declare function askUserPromptBody(questions: readonly {
|
|
24
|
+
question: string;
|
|
25
|
+
}[]): string;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { AttentionState, ShellSurface, askUserPromptBody, supportsShellTitle, warrantsAttentionNotification, warrantsSettledNotification };
|
|
28
|
+
//# sourceMappingURL=notificationPolicy.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notificationPolicy.d.cts","names":[],"sources":["../../src/services/notificationPolicy.ts"],"mappings":";UAGiB;EACf;EACA;;UAGe;EACf;EACA;;;;;;;;;iBAUc,gCAAgC,cAAc,kBAAkB;;iBAKhE,4BAA4B;;iBAK5B,qBAAqB,OAAO,QAAQ;;iBAKpC,kBAAkB;EAAsB"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/services/notificationPolicy.d.ts
|
|
2
|
+
interface AttentionState {
|
|
3
|
+
agentRunning: boolean;
|
|
4
|
+
askUserBlocked: boolean;
|
|
5
|
+
}
|
|
6
|
+
interface ShellSurface {
|
|
7
|
+
hasUI: boolean;
|
|
8
|
+
mode?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A dialog is worth interrupting the user for only while the agent holds the turn.
|
|
12
|
+
*
|
|
13
|
+
* Outside a run the user opened the dialog themselves and is already looking at
|
|
14
|
+
* it. An ask-user prompt is announced by its own event, so the dialog it then
|
|
15
|
+
* opens must stay silent or the same question notifies twice.
|
|
16
|
+
*/
|
|
17
|
+
declare function warrantsAttentionNotification({ agentRunning, askUserBlocked }: AttentionState): boolean;
|
|
18
|
+
/** A queued follow-up means the agent keeps working, so the run has not stopped for the user. */
|
|
19
|
+
declare function warrantsSettledNotification(hasPendingMessages: boolean): boolean;
|
|
20
|
+
/** Terminal title escapes only mean something on an attached terminal; RPC output stays clean. */
|
|
21
|
+
declare function supportsShellTitle({ hasUI, mode }: ShellSurface): boolean;
|
|
22
|
+
/** The question an ask-user prompt is blocking on, or a stand-in when it asked none. */
|
|
23
|
+
declare function askUserPromptBody(questions: readonly {
|
|
24
|
+
question: string;
|
|
25
|
+
}[]): string;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { AttentionState, ShellSurface, askUserPromptBody, supportsShellTitle, warrantsAttentionNotification, warrantsSettledNotification };
|
|
28
|
+
//# sourceMappingURL=notificationPolicy.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notificationPolicy.d.mts","names":[],"sources":["../../src/services/notificationPolicy.ts"],"mappings":";UAGiB;EACf;EACA;;UAGe;EACf;EACA;;;;;;;;;iBAUc,gCAAgC,cAAc,kBAAkB;;iBAKhE,4BAA4B;;iBAK5B,qBAAqB,OAAO,QAAQ;;iBAKpC,kBAAkB;EAAsB"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e({agentRunning:e,askUserBlocked:t}){return e&&!t}function t(e){return!e}function n({hasUI:e,mode:t}){return e&&t!==`rpc`}function r(e){return e[0]?.question??`The agent is waiting for your feedback.`}export{r as askUserPromptBody,n as supportsShellTitle,e as warrantsAttentionNotification,t as warrantsSettledNotification};
|
|
2
|
+
//# sourceMappingURL=notificationPolicy.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notificationPolicy.mjs","names":[],"sources":["../../src/services/notificationPolicy.ts"],"sourcesContent":["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":"AAoBA,SAAgB,EAA8B,CAAE,eAAc,kBAA2C,CACvG,OAAO,GAAgB,CAAC,CAC1B,CAGA,SAAgB,EAA4B,EAAsC,CAChF,MAAO,CAAC,CACV,CAGA,SAAgB,EAAmB,CAAE,QAAO,QAA+B,CACzE,OAAO,GAAS,IAAS,KAC3B,CAGA,SAAgB,EAAkB,EAAoD,CACpF,OAAO,EAAU,EAAE,EAAE,UAAY,yCACnC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../_virtual/_rolldown/runtime.cjs");let t=require("node:path");t=e.__toESM(t,1);function n(e){return e.replace(/\s+/g,` `).trim()}function r(e,t){return e.length>t?`${e.slice(0,t-1)}…`:e}function i(e){let t=n(e);return t?r(t,36):void 0}function a({cwd:e,sessionName:n,prompt:r}){let i=t.default.basename(e),a=n??r;return a?`π - ${a} - ${i}`:`π - ${i}`}function o(e){return r(n(e),240)}function s(e){return{title:`Pi needs your input`,subtitle:`Approval or feedback required`,body:e}}function c({cwd:e,sessionName:n}){return{title:`Pi finished`,subtitle:n??t.default.basename(e),body:`The agent finished its work and is waiting for you.`}}exports.attentionNotification=s,exports.notificationBody=o,exports.promptTitle=i,exports.settledNotification=c,exports.shellTabTitle=a;
|
|
2
|
+
//# sourceMappingURL=notificationText.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notificationText.cjs","names":["path"],"sources":["../../src/services/notificationText.ts"],"sourcesContent":["import path from 'node:path';\nimport type { DesktopNotification } from '../types/notifications.ts';\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"],"mappings":"iGAaA,SAAS,EAAmB,EAAuB,CACjD,OAAO,EAAM,QAAQ,OAAQ,GAAG,CAAC,CAAC,KAAK,CACzC,CAEA,SAAS,EAAS,EAAe,EAA2B,CAC1D,OAAO,EAAM,OAAS,EAAY,GAAG,EAAM,MAAM,EAAG,EAAY,CAAC,KAAiB,CACpF,CAGA,SAAgB,EAAY,EAAoC,CAC9D,IAAM,EAAa,EAAmB,CAAM,EAC5C,OAAO,EAAa,EAAS,EAAY,EAAuB,EAAI,IAAA,EACtE,CAcA,SAAgB,EAAc,CAAE,MAAK,cAAa,UAAsC,CACtF,IAAM,EAAaA,EAAAA,QAAK,SAAS,CAAG,EAC9B,EAAQ,GAAe,EAC7B,OAAO,EACH,OAAgD,OAAgC,IAChF,OAAgD,GACtD,CAGA,SAAgB,EAAiB,EAAsB,CACrD,OAAO,EAAS,EAAmB,CAAI,EAAG,GAAe,CAC3D,CAGA,SAAgB,EAAsB,EAAmC,CACvE,MAAO,CAAE,MAAO,sBAAiB,SAAU,gCAAoB,MAAK,CACtE,CAGA,SAAgB,EAAoB,CAAE,MAAK,eAA2E,CACpH,MAAO,CAAE,MAAO,cAAe,SAAU,GAAeA,EAAAA,QAAK,SAAS,CAAG,EAAG,KAAM,qDAAa,CACjG"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { DesktopNotification } from "../types/notifications.cjs";
|
|
2
|
+
//#region src/services/notificationText.d.ts
|
|
3
|
+
/** The first user prompt, shortened to fit a shell tab. A blank prompt names nothing. */
|
|
4
|
+
declare function promptTitle(prompt: string): string | undefined;
|
|
5
|
+
interface ShellTabTitleInput {
|
|
6
|
+
cwd: string;
|
|
7
|
+
sessionName?: string;
|
|
8
|
+
prompt?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The idle shell-tab title: `π - <label> - <repository>`.
|
|
12
|
+
*
|
|
13
|
+
* The session name wins over the first prompt because the user chose it. With
|
|
14
|
+
* neither, the repository alone still tells two tabs apart.
|
|
15
|
+
*/
|
|
16
|
+
declare function shellTabTitle({ cwd, sessionName, prompt }: ShellTabTitleInput): string;
|
|
17
|
+
/** Notification bodies are one line in a system panel, so they collapse and truncate. */
|
|
18
|
+
declare function notificationBody(body: string): string;
|
|
19
|
+
/** Sent while the agent holds the turn and something is blocking on the user. */
|
|
20
|
+
declare function attentionNotification(body: string): DesktopNotification;
|
|
21
|
+
/** Sent once a run has stopped, subtitled with whichever name the user recognises. */
|
|
22
|
+
declare function settledNotification({ cwd, sessionName }: {
|
|
23
|
+
cwd: string;
|
|
24
|
+
sessionName?: string;
|
|
25
|
+
}): DesktopNotification;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { ShellTabTitleInput, attentionNotification, notificationBody, promptTitle, settledNotification, shellTabTitle };
|
|
28
|
+
//# sourceMappingURL=notificationText.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notificationText.d.cts","names":[],"sources":["../../src/services/notificationText.ts"],"mappings":";;;iBAsBgB,YAAY;UAKX;EACf;EACA;EACA;;;;;;;;iBASc,gBAAgB,KAAK,aAAa,UAAU;;iBAS5C,iBAAiB;;iBAKjB,sBAAsB,eAAe;;iBAKrC,sBAAsB,KAAK;EAAiB;EAAa;IAAyB"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { DesktopNotification } from "../types/notifications.mjs";
|
|
2
|
+
//#region src/services/notificationText.d.ts
|
|
3
|
+
/** The first user prompt, shortened to fit a shell tab. A blank prompt names nothing. */
|
|
4
|
+
declare function promptTitle(prompt: string): string | undefined;
|
|
5
|
+
interface ShellTabTitleInput {
|
|
6
|
+
cwd: string;
|
|
7
|
+
sessionName?: string;
|
|
8
|
+
prompt?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The idle shell-tab title: `π - <label> - <repository>`.
|
|
12
|
+
*
|
|
13
|
+
* The session name wins over the first prompt because the user chose it. With
|
|
14
|
+
* neither, the repository alone still tells two tabs apart.
|
|
15
|
+
*/
|
|
16
|
+
declare function shellTabTitle({ cwd, sessionName, prompt }: ShellTabTitleInput): string;
|
|
17
|
+
/** Notification bodies are one line in a system panel, so they collapse and truncate. */
|
|
18
|
+
declare function notificationBody(body: string): string;
|
|
19
|
+
/** Sent while the agent holds the turn and something is blocking on the user. */
|
|
20
|
+
declare function attentionNotification(body: string): DesktopNotification;
|
|
21
|
+
/** Sent once a run has stopped, subtitled with whichever name the user recognises. */
|
|
22
|
+
declare function settledNotification({ cwd, sessionName }: {
|
|
23
|
+
cwd: string;
|
|
24
|
+
sessionName?: string;
|
|
25
|
+
}): DesktopNotification;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { ShellTabTitleInput, attentionNotification, notificationBody, promptTitle, settledNotification, shellTabTitle };
|
|
28
|
+
//# sourceMappingURL=notificationText.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notificationText.d.mts","names":[],"sources":["../../src/services/notificationText.ts"],"mappings":";;;iBAsBgB,YAAY;UAKX;EACf;EACA;EACA;;;;;;;;iBASc,gBAAgB,KAAK,aAAa,UAAU;;iBAS5C,iBAAiB;;iBAKjB,sBAAsB,eAAe;;iBAKrC,sBAAsB,KAAK;EAAiB;EAAa;IAAyB"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import e from"node:path";function t(e){return e.replace(/\s+/g,` `).trim()}function n(e,t){return e.length>t?`${e.slice(0,t-1)}…`:e}function r(e){let r=t(e);return r?n(r,36):void 0}function i({cwd:t,sessionName:n,prompt:r}){let i=e.basename(t),a=n??r;return a?`π - ${a} - ${i}`:`π - ${i}`}function a(e){return n(t(e),240)}function o(e){return{title:`Pi needs your input`,subtitle:`Approval or feedback required`,body:e}}function s({cwd:t,sessionName:n}){return{title:`Pi finished`,subtitle:n??e.basename(t),body:`The agent finished its work and is waiting for you.`}}export{o as attentionNotification,a as notificationBody,r as promptTitle,s as settledNotification,i as shellTabTitle};
|
|
2
|
+
//# sourceMappingURL=notificationText.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notificationText.mjs","names":[],"sources":["../../src/services/notificationText.ts"],"sourcesContent":["import path from 'node:path';\nimport type { DesktopNotification } from '../types/notifications.ts';\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"],"mappings":"yBAaA,SAAS,EAAmB,EAAuB,CACjD,OAAO,EAAM,QAAQ,OAAQ,GAAG,CAAC,CAAC,KAAK,CACzC,CAEA,SAAS,EAAS,EAAe,EAA2B,CAC1D,OAAO,EAAM,OAAS,EAAY,GAAG,EAAM,MAAM,EAAG,EAAY,CAAC,KAAiB,CACpF,CAGA,SAAgB,EAAY,EAAoC,CAC9D,IAAM,EAAa,EAAmB,CAAM,EAC5C,OAAO,EAAa,EAAS,EAAY,EAAuB,EAAI,IAAA,EACtE,CAcA,SAAgB,EAAc,CAAE,MAAK,cAAa,UAAsC,CACtF,IAAM,EAAa,EAAK,SAAS,CAAG,EAC9B,EAAQ,GAAe,EAC7B,OAAO,EACH,OAAgD,OAAgC,IAChF,OAAgD,GACtD,CAGA,SAAgB,EAAiB,EAAsB,CACrD,OAAO,EAAS,EAAmB,CAAI,EAAG,GAAe,CAC3D,CAGA,SAAgB,EAAsB,EAAmC,CACvE,MAAO,CAAE,MAAO,sBAAiB,SAAU,gCAAoB,MAAK,CACtE,CAGA,SAAgB,EAAoB,CAAE,MAAK,eAA2E,CACpH,MAAO,CAAE,MAAO,cAAe,SAAU,GAAe,EAAK,SAAS,CAAG,EAAG,KAAM,qDAAa,CACjG"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/types/notifications.d.ts
|
|
2
|
+
/** Writes a shell-tab title to whatever terminal surface the host owns. */
|
|
3
|
+
type WriteTitle = (title: string) => void;
|
|
4
|
+
/** The four things a title controller can be asked to do, and its method names. */
|
|
5
|
+
type ShellTitleAction = 'dispose' | 'set' | 'start' | 'stop';
|
|
6
|
+
/** One instruction for a title controller, and the message shape its worker consumes. */
|
|
7
|
+
interface ShellTitleCommand {
|
|
8
|
+
action: ShellTitleAction;
|
|
9
|
+
title: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Drives the shell tab title across a run.
|
|
13
|
+
*
|
|
14
|
+
* `start` animates a spinner in front of the title until `stop` restores the
|
|
15
|
+
* idle one; `set` writes a title without animating; `dispose` writes the final
|
|
16
|
+
* title and releases whatever machinery the implementation owns. Every method
|
|
17
|
+
* takes the writer so the controller never has to hold a session reference.
|
|
18
|
+
*/
|
|
19
|
+
interface ShellTitleController {
|
|
20
|
+
set(title: string, write: WriteTitle): void;
|
|
21
|
+
start(title: string, write: WriteTitle): void;
|
|
22
|
+
stop(title: string, write: WriteTitle): void;
|
|
23
|
+
dispose(title: string, write: WriteTitle): void;
|
|
24
|
+
}
|
|
25
|
+
/** One OS-level desktop notification. */
|
|
26
|
+
interface DesktopNotification {
|
|
27
|
+
title: string;
|
|
28
|
+
subtitle: string;
|
|
29
|
+
body: string;
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { DesktopNotification, ShellTitleAction, ShellTitleCommand, ShellTitleController, WriteTitle };
|
|
33
|
+
//# sourceMappingURL=notifications.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notifications.d.cts","names":[],"sources":["../../src/types/notifications.ts"],"mappings":";;KACY,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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/types/notifications.d.ts
|
|
2
|
+
/** Writes a shell-tab title to whatever terminal surface the host owns. */
|
|
3
|
+
type WriteTitle = (title: string) => void;
|
|
4
|
+
/** The four things a title controller can be asked to do, and its method names. */
|
|
5
|
+
type ShellTitleAction = 'dispose' | 'set' | 'start' | 'stop';
|
|
6
|
+
/** One instruction for a title controller, and the message shape its worker consumes. */
|
|
7
|
+
interface ShellTitleCommand {
|
|
8
|
+
action: ShellTitleAction;
|
|
9
|
+
title: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Drives the shell tab title across a run.
|
|
13
|
+
*
|
|
14
|
+
* `start` animates a spinner in front of the title until `stop` restores the
|
|
15
|
+
* idle one; `set` writes a title without animating; `dispose` writes the final
|
|
16
|
+
* title and releases whatever machinery the implementation owns. Every method
|
|
17
|
+
* takes the writer so the controller never has to hold a session reference.
|
|
18
|
+
*/
|
|
19
|
+
interface ShellTitleController {
|
|
20
|
+
set(title: string, write: WriteTitle): void;
|
|
21
|
+
start(title: string, write: WriteTitle): void;
|
|
22
|
+
stop(title: string, write: WriteTitle): void;
|
|
23
|
+
dispose(title: string, write: WriteTitle): void;
|
|
24
|
+
}
|
|
25
|
+
/** One OS-level desktop notification. */
|
|
26
|
+
interface DesktopNotification {
|
|
27
|
+
title: string;
|
|
28
|
+
subtitle: string;
|
|
29
|
+
body: string;
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { DesktopNotification, ShellTitleAction, ShellTitleCommand, ShellTitleController, WriteTitle };
|
|
33
|
+
//# sourceMappingURL=notifications.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notifications.d.mts","names":[],"sources":["../../src/types/notifications.ts"],"mappings":";;KACY,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"}
|
package/llms.txt
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agimon-ai/doompi-notification",
|
|
3
|
+
"version": "0.0.1-alpha.2",
|
|
4
|
+
"description": "Desktop notifications and an animated shell-tab title for DoomPi sessions.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"coding-agent",
|
|
8
|
+
"desktop-notifications",
|
|
9
|
+
"developer-tools",
|
|
10
|
+
"doompi",
|
|
11
|
+
"notifications",
|
|
12
|
+
"pi-coding-agent",
|
|
13
|
+
"pi-extension",
|
|
14
|
+
"pi-package",
|
|
15
|
+
"shell-title",
|
|
16
|
+
"terminal-title"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://agimon.ai",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "Vuong Ngo",
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"llms.txt",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"package.json"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"main": "./dist/index.cjs",
|
|
30
|
+
"types": "./dist/index.d.mts",
|
|
31
|
+
"jsnext:main": "./dist/index.mjs",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.mts",
|
|
35
|
+
"import": "./dist/index.mjs",
|
|
36
|
+
"require": "./dist/index.cjs"
|
|
37
|
+
},
|
|
38
|
+
"./extensions/pi": {
|
|
39
|
+
"types": "./dist/extensions/pi.d.mts",
|
|
40
|
+
"import": "./dist/extensions/pi.mjs",
|
|
41
|
+
"require": "./dist/extensions/pi.cjs"
|
|
42
|
+
},
|
|
43
|
+
"./package.json": "./package.json"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@deepseek-ai/cordis": "4.0.1",
|
|
50
|
+
"@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.24"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@earendil-works/pi-coding-agent": "0.84.2",
|
|
54
|
+
"@types/node": "26.1.1",
|
|
55
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
56
|
+
"tsdown": "0.22.14",
|
|
57
|
+
"typescript": "6.0.3",
|
|
58
|
+
"vitest": "4.1.10",
|
|
59
|
+
"@agimon-ai/vibe-lint-plugin-doom-extension": "0.0.1-alpha.24"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"@earendil-works/pi-coding-agent": "0.84.2"
|
|
63
|
+
},
|
|
64
|
+
"peerDependenciesMeta": {
|
|
65
|
+
"@earendil-works/pi-coding-agent": {
|
|
66
|
+
"optional": true
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"engines": {
|
|
70
|
+
"node": ">=22.19.0"
|
|
71
|
+
},
|
|
72
|
+
"pi": {
|
|
73
|
+
"extensions": [
|
|
74
|
+
"./dist/extensions/pi.mjs"
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
"scripts": {
|
|
78
|
+
"build": "tsdown",
|
|
79
|
+
"test": "vitest --run",
|
|
80
|
+
"typecheck": "tsc --noEmit",
|
|
81
|
+
"lint": "oxlint . && oxfmt . --check",
|
|
82
|
+
"fixcode": "oxlint . --fix && oxfmt ."
|
|
83
|
+
}
|
|
84
|
+
}
|