@raidou/pi-notify 0.6.0 → 0.7.0
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 +10 -9
- package/package.json +4 -3
- package/src/config.ts +2 -15
- package/src/dashboard/session-store.test.ts +55 -15
- package/src/dashboard/session-store.ts +3 -0
- package/src/dashboard/state-store.ts +11 -3
- package/src/dashboard/ui/dashboard.test.ts +1 -0
- package/src/state-tracker.test.ts +84 -5
- package/src/state-tracker.ts +28 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A notification extension for the [pi](https://github.com/earendil-works/pi-coding-agent) coding agent.
|
|
4
4
|
|
|
5
|
-
`@raidou/pi-notify` fires a native desktop notification on idle,
|
|
5
|
+
`@raidou/pi-notify` fires a native desktop notification on idle, tool calls (optional, e.g. `Tool call: ask_user`), user-facing UI prompts (via pi's `ui_prompt_start`, e.g. permission approvals or `select`/`confirm`/`input` dialogs, requires pi >= 0.85.0), and custom pi events.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -25,12 +25,11 @@ All options live under the `piNotify` key in `~/.pi/agent/settings.json`. Everyt
|
|
|
25
25
|
{
|
|
26
26
|
"piNotify": {
|
|
27
27
|
"enabled": true, // master on/off switch (default: true)
|
|
28
|
-
"notifyTools": [
|
|
28
|
+
"notifyTools": [], // tools that trigger "Tool call" notifications (default: empty, i.e. no tool notifications)
|
|
29
29
|
"tmuxSymbol": "🔔", // symbol appended to tmux window title (empty string to disable)
|
|
30
30
|
"finished": true, // enable/disable "Idle" notification
|
|
31
31
|
"events": {
|
|
32
|
-
"
|
|
33
|
-
"my:custom:event": "Custom event triggered", // add your own custom events
|
|
32
|
+
"my:custom:event": "Custom event triggered", // custom event channel -> notification message
|
|
34
33
|
"other:event": false, // set to false to disable a specific event
|
|
35
34
|
},
|
|
36
35
|
"finishedThrottleSecs": 0, // 0 = always notify; >0 = skip finished toasts for runs shorter than N seconds
|
|
@@ -42,11 +41,13 @@ All options live under the `piNotify` key in `~/.pi/agent/settings.json`. Everyt
|
|
|
42
41
|
|
|
43
42
|
## What triggers a notification
|
|
44
43
|
|
|
45
|
-
| Event | Source
|
|
46
|
-
| ----------------- |
|
|
47
|
-
| **Finished** | `agent_settled` (pi idle, no active jobs)
|
|
48
|
-
| **Tool calls** | `tool_call` on tools in `notifyTools`
|
|
49
|
-
| **
|
|
44
|
+
| Event | Source | Default body |
|
|
45
|
+
| ----------------- | --------------------------------------------------------------------- | --------------------------------- |
|
|
46
|
+
| **Finished** | `agent_settled` (pi idle, no active jobs) | `Idle` |
|
|
47
|
+
| **Tool calls** | `tool_call` on tools in `notifyTools` (default: none) | `Tool call: <toolName>` |
|
|
48
|
+
| **UI prompts** | `ui_prompt_start` (requires pi >= 0.85.0) | `Waiting: <kind>[ — <title>]` |
|
|
49
|
+
| **Custom events** | Custom pi event channels configured in `events` | Customizable |
|
|
50
|
+
| **External API** | `pi.events.emit('pi-notify:notify', 'message')` from other extensions | The emitted message |
|
|
50
51
|
|
|
51
52
|
### Job tracking for background tasks
|
|
52
53
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raidou/pi-notify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Desktop notification extension for the pi coding agent.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
@@ -34,16 +34,17 @@
|
|
|
34
34
|
"test:unit": "vitest run"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@earendil-works/pi-tui": "0.80.7",
|
|
38
37
|
"emittery": "^2.0.0",
|
|
39
38
|
"lodash-es": "^4.18.1",
|
|
40
39
|
"node-notifier": "^10.0.1",
|
|
41
40
|
"proper-lockfile": "^4.1.2"
|
|
42
41
|
},
|
|
43
42
|
"peerDependencies": {
|
|
44
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
43
|
+
"@earendil-works/pi-coding-agent": ">=0.85.0",
|
|
44
|
+
"@earendil-works/pi-tui": ">=0.85.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
+
"@earendil-works/pi-tui": "0.85.1",
|
|
47
48
|
"@raidou/eslint-config-base": "^4.4.3",
|
|
48
49
|
"@types/lodash-es": "^4.17.12",
|
|
49
50
|
"@types/node": "^22.0.0",
|
package/src/config.ts
CHANGED
|
@@ -28,20 +28,8 @@ export interface ResolvedNotifyConfig {
|
|
|
28
28
|
readonly tmuxSymbol: string
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/**
|
|
32
|
-
* Default tool names that trigger notifications.
|
|
33
|
-
*
|
|
34
|
-
* Note: These are built-in pi tool names. If pi renames these tools, the default should be updated.
|
|
35
|
-
* Source: @earendil-works/pi-coding-agent
|
|
36
|
-
*/
|
|
37
|
-
const DEFAULT_NOTIFY_TOOLS = ['ask_user', 'ask_user_question'] as const
|
|
38
|
-
|
|
39
31
|
const DEFAULT_TMUX_SYMBOL = '🔔'
|
|
40
32
|
|
|
41
|
-
const DEFAULT_EVENTS: NotifyEventsConfig = {
|
|
42
|
-
'permissions:ui_prompt': 'Permission prompt',
|
|
43
|
-
}
|
|
44
|
-
|
|
45
33
|
const SETTINGS_PATH = join(getAgentDir(), 'settings.json')
|
|
46
34
|
|
|
47
35
|
function readRawConfig(): NotifyConfig {
|
|
@@ -59,11 +47,10 @@ function readRawConfig(): NotifyConfig {
|
|
|
59
47
|
|
|
60
48
|
export function loadConfig(): ResolvedNotifyConfig {
|
|
61
49
|
const cfg = readRawConfig()
|
|
62
|
-
const events = cfg.events ?? DEFAULT_EVENTS
|
|
63
50
|
return {
|
|
64
51
|
enabled: cfg.enabled ?? true,
|
|
65
|
-
notifyTools: new Set(cfg.notifyTools ??
|
|
66
|
-
events,
|
|
52
|
+
notifyTools: new Set(cfg.notifyTools ?? []),
|
|
53
|
+
events: cfg.events ?? {},
|
|
67
54
|
finished: cfg.finished ?? true,
|
|
68
55
|
onlyNotifyWhenUnfocused: cfg.onlyNotifyWhenUnfocused ?? true,
|
|
69
56
|
unfocusedActivityThresholdMs: Math.max(
|
|
@@ -171,10 +171,11 @@ describe('SessionStore', () => {
|
|
|
171
171
|
)
|
|
172
172
|
store.register(vi.fn())
|
|
173
173
|
|
|
174
|
-
expect(emitSpy).toHaveBeenCalledTimes(
|
|
174
|
+
expect(emitSpy).toHaveBeenCalledTimes(5)
|
|
175
175
|
expect(emitSpy).toHaveBeenCalledWith('running', expect.any(Function))
|
|
176
176
|
expect(emitSpy).toHaveBeenCalledWith('idle', expect.any(Function))
|
|
177
177
|
expect(emitSpy).toHaveBeenCalledWith('tool', expect.any(Function))
|
|
178
|
+
expect(emitSpy).toHaveBeenCalledWith('ui_prompt', expect.any(Function))
|
|
178
179
|
expect(emitSpy).toHaveBeenCalledWith('event', expect.any(Function))
|
|
179
180
|
expect(piOnSpy).toHaveBeenCalledWith('session_start', expect.any(Function))
|
|
180
181
|
})
|
|
@@ -347,7 +348,7 @@ describe('SessionStore', () => {
|
|
|
347
348
|
nowSpy.mockRestore()
|
|
348
349
|
})
|
|
349
350
|
|
|
350
|
-
it('
|
|
351
|
+
it('updates state immediately on event emission', async () => {
|
|
351
352
|
await updateState(() => ({ version: 2, sessions: {} }))
|
|
352
353
|
const pi = makeFakePi()
|
|
353
354
|
const jobTracker = {
|
|
@@ -359,8 +360,8 @@ describe('SessionStore', () => {
|
|
|
359
360
|
pi as unknown as ExtensionAPI,
|
|
360
361
|
jobTracker,
|
|
361
362
|
{
|
|
362
|
-
notifyTools: new Set(['
|
|
363
|
-
events: {},
|
|
363
|
+
notifyTools: new Set(['bash']),
|
|
364
|
+
events: { 'permissions:ui_prompt': 'msg' },
|
|
364
365
|
} as unknown as ResolvedNotifyConfig,
|
|
365
366
|
)
|
|
366
367
|
tracker.register(() => {})
|
|
@@ -374,20 +375,53 @@ describe('SessionStore', () => {
|
|
|
374
375
|
})
|
|
375
376
|
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
376
377
|
|
|
377
|
-
|
|
378
|
+
pi.emitEvent('permissions:ui_prompt', {})
|
|
379
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
378
380
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
381
|
+
const record = readState().sessions[String(process.pid)]
|
|
382
|
+
expect(record?.state).toBe('event:permissions:ui_prompt')
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it('updates state immediately on ui_prompt emission', async () => {
|
|
386
|
+
await updateState(() => ({ version: 2, sessions: {} }))
|
|
387
|
+
const pi = makeFakePi()
|
|
388
|
+
const jobTracker = {
|
|
389
|
+
hasActiveJobs: false,
|
|
390
|
+
onStart: () => () => {},
|
|
391
|
+
onEnd: () => () => {},
|
|
392
|
+
} as unknown as JobTracker
|
|
393
|
+
const tracker = new StateTracker(
|
|
394
|
+
pi as unknown as ExtensionAPI,
|
|
395
|
+
jobTracker,
|
|
396
|
+
{
|
|
397
|
+
notifyTools: new Set([]),
|
|
398
|
+
events: {},
|
|
399
|
+
} as unknown as ResolvedNotifyConfig,
|
|
400
|
+
)
|
|
401
|
+
tracker.register(() => {})
|
|
402
|
+
|
|
403
|
+
const store = new SessionStore(pi as unknown as ExtensionAPI, tracker)
|
|
404
|
+
store.register(() => {})
|
|
405
|
+
|
|
406
|
+
pi.emitSessionStart({
|
|
407
|
+
cwd: META.cwd,
|
|
408
|
+
sessionManager: { getSessionId: () => SESSION_ID },
|
|
383
409
|
})
|
|
384
410
|
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
385
411
|
|
|
412
|
+
pi.emit('ui_prompt_start', {
|
|
413
|
+
type: 'ui_prompt_start',
|
|
414
|
+
reason: 'ui_prompt',
|
|
415
|
+
kind: 'confirm',
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
419
|
+
|
|
386
420
|
const record = readState().sessions[String(process.pid)]
|
|
387
|
-
expect(record?.state).toBe('
|
|
421
|
+
expect(record?.state).toBe('ui_prompt:confirm')
|
|
388
422
|
})
|
|
389
423
|
|
|
390
|
-
it('
|
|
424
|
+
it('does not update state on tool emission outside notifyTools', async () => {
|
|
391
425
|
await updateState(() => ({ version: 2, sessions: {} }))
|
|
392
426
|
const pi = makeFakePi()
|
|
393
427
|
const jobTracker = {
|
|
@@ -399,8 +433,8 @@ describe('SessionStore', () => {
|
|
|
399
433
|
pi as unknown as ExtensionAPI,
|
|
400
434
|
jobTracker,
|
|
401
435
|
{
|
|
402
|
-
notifyTools: new Set(['
|
|
403
|
-
events: {
|
|
436
|
+
notifyTools: new Set(['read']),
|
|
437
|
+
events: {},
|
|
404
438
|
} as unknown as ResolvedNotifyConfig,
|
|
405
439
|
)
|
|
406
440
|
tracker.register(() => {})
|
|
@@ -414,11 +448,17 @@ describe('SessionStore', () => {
|
|
|
414
448
|
})
|
|
415
449
|
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
416
450
|
|
|
417
|
-
|
|
451
|
+
expect(readState().sessions[String(process.pid)]?.state).toBe('idle')
|
|
452
|
+
|
|
453
|
+
pi.emit('tool_call', {
|
|
454
|
+
type: 'tool_call',
|
|
455
|
+
toolCallId: 't1',
|
|
456
|
+
toolName: 'grep',
|
|
457
|
+
})
|
|
418
458
|
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
419
459
|
|
|
420
460
|
const record = readState().sessions[String(process.pid)]
|
|
421
|
-
expect(record?.state).toBe('
|
|
461
|
+
expect(record?.state).toBe('idle')
|
|
422
462
|
})
|
|
423
463
|
|
|
424
464
|
it('ignores tool and event emissions before session_start', async () => {
|
|
@@ -79,6 +79,9 @@ export class SessionStore extends Registrar {
|
|
|
79
79
|
this.stateTracker.events.on('tool', async ({ data }) => {
|
|
80
80
|
await this.saveSession(`tool_call:${data}`)
|
|
81
81
|
}),
|
|
82
|
+
this.stateTracker.events.on('ui_prompt', async ({ data }) => {
|
|
83
|
+
await this.saveSession(`ui_prompt:${data}`)
|
|
84
|
+
}),
|
|
82
85
|
this.stateTracker.events.on('event', async ({ data }) => {
|
|
83
86
|
await this.saveSession(`event:${data}`)
|
|
84
87
|
}),
|
|
@@ -54,7 +54,11 @@ export async function readSessions(): Promise<SessionRecord[]> {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export type SessionState =
|
|
57
|
-
|
|
57
|
+
| 'running'
|
|
58
|
+
| 'idle'
|
|
59
|
+
| `tool_call:${string}`
|
|
60
|
+
| `event:${string}`
|
|
61
|
+
| `ui_prompt:${string}`
|
|
58
62
|
|
|
59
63
|
export interface SessionRecord {
|
|
60
64
|
pid: number
|
|
@@ -112,8 +116,12 @@ function parseSessionRecord(value: unknown): SessionRecord | undefined {
|
|
|
112
116
|
|
|
113
117
|
function isActivityState(
|
|
114
118
|
value: string,
|
|
115
|
-
): value is `tool_call:${string}` | `event:${string}` {
|
|
116
|
-
return
|
|
119
|
+
): value is `tool_call:${string}` | `event:${string}` | `ui_prompt:${string}` {
|
|
120
|
+
return (
|
|
121
|
+
value.startsWith('tool_call:') ||
|
|
122
|
+
value.startsWith('event:') ||
|
|
123
|
+
value.startsWith('ui_prompt:')
|
|
124
|
+
)
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
function isSessionState(value: unknown): value is SessionState {
|
|
@@ -8,6 +8,7 @@ import { Dashboard } from './dashboard.js'
|
|
|
8
8
|
const theme = {
|
|
9
9
|
fg: (color: ThemeColor, text: string) => `\x1b[90m${text}\x1b[0m`,
|
|
10
10
|
bold: (text: string) => `\x1b[1m${text}\x1b[0m`,
|
|
11
|
+
underline: (text: string) => `\x1b[4m${text}\x1b[0m`,
|
|
11
12
|
} as unknown as Theme
|
|
12
13
|
|
|
13
14
|
const ANSI_RE =
|
|
@@ -89,7 +89,7 @@ const BASE_CONFIG: ResolvedNotifyConfig = {
|
|
|
89
89
|
enabled: true,
|
|
90
90
|
notifyTools: new Set(['bash', 'read']),
|
|
91
91
|
events: {
|
|
92
|
-
'
|
|
92
|
+
'my:custom:event': 'msg',
|
|
93
93
|
'disabled:channel': false,
|
|
94
94
|
},
|
|
95
95
|
finished: true,
|
|
@@ -171,6 +171,19 @@ describe('StateTracker', () => {
|
|
|
171
171
|
expect(states).toEqual(['running'])
|
|
172
172
|
})
|
|
173
173
|
|
|
174
|
+
it('re-emits running after a notified custom event resets the state', async () => {
|
|
175
|
+
const pi = makeFakePi()
|
|
176
|
+
const { states } = makeTracker(pi)
|
|
177
|
+
|
|
178
|
+
pi.emit('turn_start')
|
|
179
|
+
pi.emitEvent('my:custom:event', {})
|
|
180
|
+
pi.emit('turn_start')
|
|
181
|
+
|
|
182
|
+
await flush()
|
|
183
|
+
|
|
184
|
+
expect(states).toEqual(['running', 'running'])
|
|
185
|
+
})
|
|
186
|
+
|
|
174
187
|
it('re-emits running after a notified tool call resets the state', async () => {
|
|
175
188
|
const pi = makeFakePi()
|
|
176
189
|
const { states } = makeTracker(pi)
|
|
@@ -220,12 +233,12 @@ describe('StateTracker', () => {
|
|
|
220
233
|
events.push(event.data)
|
|
221
234
|
})
|
|
222
235
|
|
|
223
|
-
pi.emitEvent('
|
|
236
|
+
pi.emitEvent('my:custom:event', {})
|
|
224
237
|
pi.emitEvent('disabled:channel', {})
|
|
225
238
|
|
|
226
239
|
await flush()
|
|
227
240
|
|
|
228
|
-
expect(events).toEqual(['
|
|
241
|
+
expect(events).toEqual(['my:custom:event'])
|
|
229
242
|
})
|
|
230
243
|
|
|
231
244
|
it('unsubscribes from channel events on stop', async () => {
|
|
@@ -237,7 +250,7 @@ describe('StateTracker', () => {
|
|
|
237
250
|
})
|
|
238
251
|
|
|
239
252
|
tracker.stop()
|
|
240
|
-
pi.emitEvent('
|
|
253
|
+
pi.emitEvent('my:custom:event', {})
|
|
241
254
|
|
|
242
255
|
await flush()
|
|
243
256
|
|
|
@@ -262,7 +275,7 @@ describe('StateTracker', () => {
|
|
|
262
275
|
const pi = makeFakePi()
|
|
263
276
|
const { bodies } = makeTracker(pi)
|
|
264
277
|
|
|
265
|
-
pi.emitEvent('
|
|
278
|
+
pi.emitEvent('my:custom:event', {})
|
|
266
279
|
|
|
267
280
|
expect(bodies).toEqual(['msg'])
|
|
268
281
|
})
|
|
@@ -297,6 +310,56 @@ describe('StateTracker', () => {
|
|
|
297
310
|
expect(bodies).toEqual(['custom payload'])
|
|
298
311
|
})
|
|
299
312
|
|
|
313
|
+
it('notifies on ui_prompt_start with kind and title', () => {
|
|
314
|
+
const pi = makeFakePi()
|
|
315
|
+
const { bodies } = makeTracker(pi)
|
|
316
|
+
|
|
317
|
+
pi.emit('ui_prompt_start', {
|
|
318
|
+
type: 'ui_prompt_start',
|
|
319
|
+
reason: 'ui_prompt',
|
|
320
|
+
kind: 'confirm',
|
|
321
|
+
title: 'Apply changes?',
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
expect(bodies).toEqual(['Waiting: Confirm — Apply changes?'])
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('notifies on ui_prompt_start without a title', () => {
|
|
328
|
+
const pi = makeFakePi()
|
|
329
|
+
const { bodies } = makeTracker(pi)
|
|
330
|
+
|
|
331
|
+
pi.emit('ui_prompt_start', {
|
|
332
|
+
type: 'ui_prompt_start',
|
|
333
|
+
reason: 'ui_prompt',
|
|
334
|
+
kind: 'select',
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
expect(bodies).toEqual(['Waiting: Select'])
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('emits ui_prompt with the raw kind and resets running', async () => {
|
|
341
|
+
const pi = makeFakePi()
|
|
342
|
+
const { tracker, states } = makeTracker(pi)
|
|
343
|
+
const kinds: string[] = []
|
|
344
|
+
tracker.events.on('ui_prompt', (event) => {
|
|
345
|
+
kinds.push(event.data)
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
pi.emit('turn_start')
|
|
349
|
+
pi.emit('ui_prompt_start', {
|
|
350
|
+
type: 'ui_prompt_start',
|
|
351
|
+
reason: 'ui_prompt',
|
|
352
|
+
kind: 'confirm',
|
|
353
|
+
title: 'Apply changes?',
|
|
354
|
+
})
|
|
355
|
+
pi.emit('turn_start')
|
|
356
|
+
|
|
357
|
+
await flush()
|
|
358
|
+
|
|
359
|
+
expect(kinds).toEqual(['confirm'])
|
|
360
|
+
expect(states).toEqual(['running', 'running'])
|
|
361
|
+
})
|
|
362
|
+
|
|
300
363
|
it('notifies for tools in notifyTools', () => {
|
|
301
364
|
const pi = makeFakePi()
|
|
302
365
|
const { bodies } = makeTracker(pi)
|
|
@@ -323,6 +386,22 @@ describe('StateTracker', () => {
|
|
|
323
386
|
expect(bodies).toEqual([])
|
|
324
387
|
})
|
|
325
388
|
|
|
389
|
+
it('does not notify when notifyTools is empty', () => {
|
|
390
|
+
const pi = makeFakePi()
|
|
391
|
+
const { bodies } = makeTracker(pi, {
|
|
392
|
+
...BASE_CONFIG,
|
|
393
|
+
notifyTools: new Set([]),
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
pi.emit('tool_call', {
|
|
397
|
+
type: 'tool_call',
|
|
398
|
+
toolCallId: 't1',
|
|
399
|
+
toolName: 'bash',
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
expect(bodies).toEqual([])
|
|
403
|
+
})
|
|
404
|
+
|
|
326
405
|
it('notifies Idle on idle when there was activity', async () => {
|
|
327
406
|
const pi = makeFakePi()
|
|
328
407
|
const { bodies } = makeTracker(pi)
|
package/src/state-tracker.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
UIPromptKind,
|
|
4
|
+
} from '@earendil-works/pi-coding-agent'
|
|
2
5
|
import Emittery from 'emittery'
|
|
3
6
|
|
|
4
7
|
import type { ResolvedNotifyConfig } from './config.js'
|
|
@@ -10,12 +13,27 @@ export const PI_NOTIFY_EVENT = 'pi-notify:notify'
|
|
|
10
13
|
|
|
11
14
|
const IDLE_TIMEOUT_MS = 10000
|
|
12
15
|
|
|
16
|
+
const PROMPT_KIND_LABELS: Record<UIPromptKind, string> = {
|
|
17
|
+
select: 'Select',
|
|
18
|
+
confirm: 'Confirm',
|
|
19
|
+
input: 'Input',
|
|
20
|
+
editor: 'Editor',
|
|
21
|
+
custom: 'Prompt',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function promptMessage(kind: UIPromptKind, title?: string): string {
|
|
25
|
+
const label = PROMPT_KIND_LABELS[kind]
|
|
26
|
+
if (!title) return `Waiting: ${label}`
|
|
27
|
+
return `Waiting: ${label} — ${title}`
|
|
28
|
+
}
|
|
29
|
+
|
|
13
30
|
export class StateTracker extends Registrar {
|
|
14
31
|
readonly events = new Emittery<{
|
|
15
32
|
running: never
|
|
16
33
|
idle: never
|
|
17
34
|
tool: string
|
|
18
35
|
event: string
|
|
36
|
+
ui_prompt: string
|
|
19
37
|
}>()
|
|
20
38
|
|
|
21
39
|
private readonly jobTracker: JobTracker
|
|
@@ -80,6 +98,14 @@ export class StateTracker extends Registrar {
|
|
|
80
98
|
this.unsubscribes.push(customEventUnsub)
|
|
81
99
|
}
|
|
82
100
|
|
|
101
|
+
private setupUiPrompt() {
|
|
102
|
+
this.pi.on('ui_prompt_start', (event) => {
|
|
103
|
+
this.notify(promptMessage(event.kind, event.title))
|
|
104
|
+
this.running = false
|
|
105
|
+
void this.events.emit('ui_prompt', event.kind)
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
83
109
|
private setupToolCall() {
|
|
84
110
|
this.pi.on('tool_call', (event) => {
|
|
85
111
|
if (this.config.notifyTools.has(event.toolName)) {
|
|
@@ -95,6 +121,7 @@ export class StateTracker extends Registrar {
|
|
|
95
121
|
this.notify = notify
|
|
96
122
|
|
|
97
123
|
this.setupPiEvents()
|
|
124
|
+
this.setupUiPrompt()
|
|
98
125
|
this.setupToolCall()
|
|
99
126
|
|
|
100
127
|
this.pi.on('turn_start', () => {
|