@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.
@@ -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(private readonly pi: ExtensionAPI) {
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
- private readonly pi: ExtensionAPI,
22
- private readonly config: ResolvedNotifyConfig,
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
- register(): void {
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 type { NotifyAction } from './types.js'
4
+ import { Registrar } from './shared/registrar.js'
5
+ import type { NotifyAction } from './shared/types.js'
5
6
 
6
- export class ToolCallNotifier {
7
- constructor(
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
- register(notify: NotifyAction): void {
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