@raidou/pi-notify 0.3.1 → 0.4.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 +24 -38
- package/package.json +11 -6
- package/src/config.ts +2 -1
- package/src/dashboard/command.ts +9 -7
- package/src/dashboard/session-store.test.ts +290 -0
- package/src/dashboard/session-store.ts +83 -0
- package/src/dashboard/state-store.test.ts +231 -0
- package/src/dashboard/state-store.ts +64 -95
- package/src/dashboard/ui.ts +36 -41
- package/src/events.test.ts +115 -0
- package/src/events.ts +10 -25
- package/src/focus.ts +25 -25
- package/src/idle.ts +22 -37
- package/src/index.ts +19 -12
- package/src/jobs.ts +5 -22
- package/src/notify-test.ts +12 -8
- package/src/shared/registrar.test.ts +137 -0
- package/src/shared/registrar.ts +30 -0
- package/src/{types.ts → shared/types.ts} +4 -0
- package/src/state-tracker.test.ts +145 -0
- package/src/state-tracker.ts +76 -0
- package/src/states.ts +3 -1
- package/src/tmux-title.ts +12 -10
- package/src/tool.ts +10 -7
- package/src/dashboard/state-tracker.ts +0 -174
- /package/src/{utils.ts → shared/utils.ts} +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
|
|
2
|
+
import Emittery from 'emittery'
|
|
3
|
+
|
|
4
|
+
import type { JobTracker } from './jobs.js'
|
|
5
|
+
import { Registrar } from './shared/registrar.js'
|
|
6
|
+
|
|
7
|
+
const IDLE_TIMEOUT_MS = 10000
|
|
8
|
+
|
|
9
|
+
export class StateTracker extends Registrar {
|
|
10
|
+
readonly events = new Emittery<{
|
|
11
|
+
running: never
|
|
12
|
+
idle: never
|
|
13
|
+
}>()
|
|
14
|
+
|
|
15
|
+
private readonly jobTracker: JobTracker
|
|
16
|
+
private idleTimer: NodeJS.Timeout | null = null
|
|
17
|
+
private running = false
|
|
18
|
+
|
|
19
|
+
constructor(pi: ExtensionAPI, jobTracker: JobTracker) {
|
|
20
|
+
super(pi)
|
|
21
|
+
this.jobTracker = jobTracker
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private startIdleTimer(): void {
|
|
25
|
+
if (this.jobTracker.hasActiveJobs) return
|
|
26
|
+
this.clearIdleTimer()
|
|
27
|
+
this.idleTimer = setTimeout(() => {
|
|
28
|
+
this.idleTimer = null
|
|
29
|
+
this.running = false
|
|
30
|
+
void this.events.emit('idle')
|
|
31
|
+
}, IDLE_TIMEOUT_MS)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private clearIdleTimer(): void {
|
|
35
|
+
if (this.idleTimer) {
|
|
36
|
+
clearTimeout(this.idleTimer)
|
|
37
|
+
this.idleTimer = null
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private markRunning(): void {
|
|
42
|
+
if (this.running) return
|
|
43
|
+
this.running = true
|
|
44
|
+
void this.events.emit('running')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected override setup(): void {
|
|
48
|
+
this.pi.on('turn_start', () => {
|
|
49
|
+
this.markRunning()
|
|
50
|
+
this.clearIdleTimer()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
this.pi.on('message_start', () => {
|
|
54
|
+
this.clearIdleTimer()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
this.pi.on('agent_settled', () => {
|
|
58
|
+
this.startIdleTimer()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
this.pi.on('tool_call', () => {
|
|
62
|
+
this.clearIdleTimer()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
this.unsubscribes.push(
|
|
66
|
+
this.jobTracker.onEnd(() => {
|
|
67
|
+
this.startIdleTimer()
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
override stop(): void {
|
|
73
|
+
super.stop()
|
|
74
|
+
this.clearIdleTimer()
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/states.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
|
|
2
2
|
|
|
3
3
|
export class SessionState {
|
|
4
|
+
private readonly pi: ExtensionAPI
|
|
4
5
|
public hasUI = false
|
|
5
6
|
|
|
6
|
-
constructor(
|
|
7
|
+
constructor(pi: ExtensionAPI) {
|
|
8
|
+
this.pi = pi
|
|
7
9
|
this.register()
|
|
8
10
|
}
|
|
9
11
|
|
package/src/tmux-title.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { readlinkSync } from 'node:fs'
|
|
|
4
4
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
|
|
5
5
|
|
|
6
6
|
import type { ResolvedNotifyConfig } from './config.js'
|
|
7
|
+
import { Registrar } from './shared/registrar.js'
|
|
8
|
+
import type { NotifyAction } from './shared/types.js'
|
|
7
9
|
|
|
8
10
|
const WINDOW_ID_FORMAT = '#{window_id}'
|
|
9
11
|
const WINDOW_NAME_FORMAT = '#{window_name}'
|
|
@@ -11,22 +13,24 @@ const LIST_FORMAT = '#{pane_tty}\t#{window_id}'
|
|
|
11
13
|
const NEWLINE = '\n'
|
|
12
14
|
const TAB = '\t'
|
|
13
15
|
|
|
14
|
-
export class TmuxTitleTracker {
|
|
16
|
+
export class TmuxTitleTracker extends Registrar {
|
|
17
|
+
private readonly config: ResolvedNotifyConfig
|
|
15
18
|
private windowId: string | undefined
|
|
16
19
|
private originalTitle: string | undefined
|
|
17
20
|
private autoRename: boolean | undefined
|
|
18
21
|
private modified = false
|
|
19
22
|
|
|
20
|
-
constructor(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
constructor(pi: ExtensionAPI, config: ResolvedNotifyConfig) {
|
|
24
|
+
super(pi)
|
|
25
|
+
this.config = config
|
|
26
|
+
}
|
|
24
27
|
|
|
25
28
|
get enabled(): boolean {
|
|
26
29
|
return this.config.tmuxSymbol.length > 0
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
protected override setup(notify: NotifyAction): void {
|
|
33
|
+
void notify
|
|
30
34
|
this.pi.on('session_start', (_event, ctx) => {
|
|
31
35
|
if (ctx.mode !== 'tui' || !this.enabled) return
|
|
32
36
|
this.restore()
|
|
@@ -36,9 +40,6 @@ export class TmuxTitleTracker {
|
|
|
36
40
|
this.originalTitle = this.queryWindowName(id)
|
|
37
41
|
this.autoRename = this.queryAutomaticRename(id)
|
|
38
42
|
})
|
|
39
|
-
this.pi.on('session_shutdown', () => {
|
|
40
|
-
this.stop()
|
|
41
|
-
})
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
mark(): void {
|
|
@@ -72,7 +73,8 @@ export class TmuxTitleTracker {
|
|
|
72
73
|
this.modified = false
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
stop(): void {
|
|
76
|
+
override stop(): void {
|
|
77
|
+
super.stop()
|
|
76
78
|
this.restore()
|
|
77
79
|
this.windowId = undefined
|
|
78
80
|
this.originalTitle = undefined
|
package/src/tool.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
|
|
2
2
|
|
|
3
3
|
import type { ResolvedNotifyConfig } from './config.js'
|
|
4
|
-
import
|
|
4
|
+
import { Registrar } from './shared/registrar.js'
|
|
5
|
+
import type { NotifyAction } from './shared/types.js'
|
|
5
6
|
|
|
6
|
-
export class ToolCallNotifier {
|
|
7
|
-
|
|
8
|
-
private readonly pi: ExtensionAPI,
|
|
9
|
-
private readonly config: ResolvedNotifyConfig,
|
|
10
|
-
) {}
|
|
7
|
+
export class ToolCallNotifier extends Registrar {
|
|
8
|
+
private readonly config: ResolvedNotifyConfig
|
|
11
9
|
|
|
12
|
-
|
|
10
|
+
constructor(pi: ExtensionAPI, config: ResolvedNotifyConfig) {
|
|
11
|
+
super(pi)
|
|
12
|
+
this.config = config
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
protected override setup(notify: NotifyAction): void {
|
|
13
16
|
this.pi.on('tool_call', (event) => {
|
|
14
17
|
if (!this.config.notifyTools.has(event.toolName)) return
|
|
15
18
|
notify(`Tool call: ${event.toolName}`)
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import { basename } from 'node:path'
|
|
2
|
-
|
|
3
|
-
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
|
|
4
|
-
|
|
5
|
-
import { type SessionRecord, updateState } from './state-store.js'
|
|
6
|
-
|
|
7
|
-
const IDLE_TIMEOUT_MS = 10000
|
|
8
|
-
const HEARTBEAT_INTERVAL_MS = 5000
|
|
9
|
-
|
|
10
|
-
export class StateTracker {
|
|
11
|
-
private registered = false
|
|
12
|
-
private heartbeatTimer: NodeJS.Timeout | null = null
|
|
13
|
-
private idleTimer: NodeJS.Timeout | null = null
|
|
14
|
-
private sessionId: string | undefined = undefined
|
|
15
|
-
private cwd: string | undefined = undefined
|
|
16
|
-
private projectName: string | undefined = undefined
|
|
17
|
-
private model: string | undefined = undefined
|
|
18
|
-
|
|
19
|
-
private get cwdOrCwd(): string {
|
|
20
|
-
return this.cwd ?? process.cwd()
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
private get projectNameOrCwd(): string {
|
|
24
|
-
return this.projectName ?? basename(process.cwd())
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
constructor(private readonly pi: ExtensionAPI) {}
|
|
28
|
-
|
|
29
|
-
private updateSessionRecord(updates: Partial<SessionRecord>): void {
|
|
30
|
-
if (!this.sessionId) return
|
|
31
|
-
const now = Date.now()
|
|
32
|
-
const id = this.sessionId
|
|
33
|
-
|
|
34
|
-
void updateState((state) => {
|
|
35
|
-
const existing = state.sessions[id]
|
|
36
|
-
const stateChanged =
|
|
37
|
-
updates.state !== undefined && updates.state !== existing?.state
|
|
38
|
-
|
|
39
|
-
const record: SessionRecord = {
|
|
40
|
-
pid: process.pid,
|
|
41
|
-
cwd: this.cwdOrCwd,
|
|
42
|
-
projectName: this.projectNameOrCwd,
|
|
43
|
-
lastHeartbeatAt: now,
|
|
44
|
-
startedAt: existing?.startedAt ?? now,
|
|
45
|
-
state: updates.state ?? existing?.state ?? 'running',
|
|
46
|
-
stateChangedAt: stateChanged ? now : (existing?.stateChangedAt ?? now),
|
|
47
|
-
model: this.model ?? existing?.model,
|
|
48
|
-
lastEvent: updates.lastEvent ?? existing?.lastEvent,
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
...state,
|
|
53
|
-
sessions: { ...state.sessions, [id]: record },
|
|
54
|
-
}
|
|
55
|
-
})
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
private removeSession(): void {
|
|
59
|
-
if (!this.sessionId) return
|
|
60
|
-
const id = this.sessionId
|
|
61
|
-
|
|
62
|
-
void updateState((state) => {
|
|
63
|
-
const sessions: Record<string, SessionRecord | undefined> = {}
|
|
64
|
-
for (const [key, value] of Object.entries(state.sessions)) {
|
|
65
|
-
if (key !== id) sessions[key] = value
|
|
66
|
-
}
|
|
67
|
-
return { ...state, sessions }
|
|
68
|
-
})
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
private clearIdleTimer(): void {
|
|
72
|
-
if (this.idleTimer) {
|
|
73
|
-
clearTimeout(this.idleTimer)
|
|
74
|
-
this.idleTimer = null
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
private startIdleTimer(): void {
|
|
79
|
-
this.clearIdleTimer()
|
|
80
|
-
this.idleTimer = setTimeout(() => {
|
|
81
|
-
this.clearIdleTimer()
|
|
82
|
-
this.updateSessionRecord({
|
|
83
|
-
state: 'idle',
|
|
84
|
-
lastEvent: { type: 'idle', summary: 'idle', at: Date.now() },
|
|
85
|
-
})
|
|
86
|
-
}, IDLE_TIMEOUT_MS)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
register(): void {
|
|
90
|
-
if (this.registered) return
|
|
91
|
-
this.registered = true
|
|
92
|
-
|
|
93
|
-
this.pi.on('session_start', (_event, ctx) => {
|
|
94
|
-
this.sessionId = ctx.sessionManager.getSessionId()
|
|
95
|
-
this.cwd = ctx.cwd
|
|
96
|
-
this.projectName = basename(ctx.cwd)
|
|
97
|
-
this.model = ctx.model?.id
|
|
98
|
-
|
|
99
|
-
this.updateSessionRecord({
|
|
100
|
-
lastEvent: {
|
|
101
|
-
type: 'session_start',
|
|
102
|
-
summary: 'started',
|
|
103
|
-
at: Date.now(),
|
|
104
|
-
},
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
this.heartbeatTimer = setInterval(() => {
|
|
108
|
-
if (!this.sessionId) return
|
|
109
|
-
const id = this.sessionId
|
|
110
|
-
|
|
111
|
-
void updateState((state) => {
|
|
112
|
-
const existing = state.sessions[id]
|
|
113
|
-
if (!existing) return state
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
...state,
|
|
117
|
-
sessions: {
|
|
118
|
-
...state.sessions,
|
|
119
|
-
[id]: {
|
|
120
|
-
...existing,
|
|
121
|
-
lastHeartbeatAt: Date.now(),
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
}
|
|
125
|
-
})
|
|
126
|
-
}, HEARTBEAT_INTERVAL_MS)
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
this.pi.on('turn_start', () => {
|
|
130
|
-
this.clearIdleTimer()
|
|
131
|
-
this.updateSessionRecord({
|
|
132
|
-
state: 'running',
|
|
133
|
-
lastEvent: { type: 'turn_start', summary: 'running', at: Date.now() },
|
|
134
|
-
})
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
this.pi.on('agent_start', () => {
|
|
138
|
-
this.clearIdleTimer()
|
|
139
|
-
this.updateSessionRecord({
|
|
140
|
-
state: 'running',
|
|
141
|
-
lastEvent: { type: 'agent_start', summary: 'running', at: Date.now() },
|
|
142
|
-
})
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
this.pi.on('agent_settled', () => {
|
|
146
|
-
this.startIdleTimer()
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
this.pi.on('tool_call', (event) => {
|
|
150
|
-
this.updateSessionRecord({
|
|
151
|
-
state: 'running',
|
|
152
|
-
lastEvent: {
|
|
153
|
-
type: 'tool_call',
|
|
154
|
-
summary: `tool:${event.toolName}`,
|
|
155
|
-
at: Date.now(),
|
|
156
|
-
},
|
|
157
|
-
})
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
this.pi.on('session_shutdown', () => {
|
|
161
|
-
this.stop()
|
|
162
|
-
this.removeSession()
|
|
163
|
-
})
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
stop(): void {
|
|
167
|
-
this.clearIdleTimer()
|
|
168
|
-
if (this.heartbeatTimer) {
|
|
169
|
-
clearInterval(this.heartbeatTimer)
|
|
170
|
-
this.heartbeatTimer = null
|
|
171
|
-
}
|
|
172
|
-
this.registered = false
|
|
173
|
-
}
|
|
174
|
-
}
|
|
File without changes
|