@catdaemon/pi-cmux 0.1.1 → 0.1.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/README.md +43 -2
- package/package.json +1 -1
- package/src/cmuxIntegration.ts +60 -22
- package/src/tests/turnTracker.test.ts +27 -0
package/README.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pi-cmux
|
|
2
2
|
|
|
3
3
|
cmux status and notification integration for Pi.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
When Pi runs inside cmux, it is useful for the surrounding workspace to know whether the agent is busy, idle, or waiting for attention. Without an integration, the user has to keep checking the Pi surface manually to see whether a turn has finished or whether an approval prompt needs feedback.
|
|
8
|
+
|
|
9
|
+
`pi-cmux` bridges Pi and cmux with lightweight status updates and notifications. It keeps the cmux surface informed while avoiding noise from subagent sessions by default.
|
|
10
|
+
|
|
11
|
+
This is especially useful for:
|
|
12
|
+
|
|
13
|
+
- Seeing at a glance whether Pi is currently working
|
|
14
|
+
- Getting notified when an agent turn finishes
|
|
15
|
+
- Surfacing approval prompts from guard extensions
|
|
16
|
+
- Sharing cmux notification helpers with other Pi extensions
|
|
17
|
+
|
|
18
|
+
## What it does
|
|
19
|
+
|
|
20
|
+
This Pi package adds cmux integration to Pi:
|
|
21
|
+
|
|
22
|
+
- **Agent status updates**: sets a cmux status while the main agent is running and returns it to idle when the turn ends.
|
|
23
|
+
- **Done notifications**: sends a cmux notification when a main agent turn completes.
|
|
24
|
+
- **Feedback notifications**: exports helpers that other extensions can use to notify cmux when user feedback is needed.
|
|
25
|
+
- **Subagent filtering**: suppresses subagent turn noise by default.
|
|
26
|
+
- **Runtime status command**: `/cmux-status` shows whether cmux environment variables are detected and how the integration is configured.
|
|
27
|
+
- **Environment configuration**: supports `PI_CMUX_STATUS_KEY`, `PI_CMUX_NOTIFY_DONE`, `PI_CMUX_STATUS_PREVIEW`, and `PI_CMUX_INCLUDE_SUBAGENTS`.
|
|
28
|
+
|
|
29
|
+
## Install from npm
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pi install npm:@catdaemon/pi-cmux
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then reload Pi:
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
/reload
|
|
39
|
+
/cmux-status
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Runtime dependencies
|
|
43
|
+
|
|
44
|
+
This package expects the `cmux` CLI to be available when Pi is running inside cmux. Outside cmux, it safely no-ops.
|
|
45
|
+
|
|
46
|
+
Other Pi extensions can import its notification helpers when they need to signal cmux feedback or completion events.
|
package/package.json
CHANGED
package/src/cmuxIntegration.ts
CHANGED
|
@@ -13,13 +13,59 @@ import {
|
|
|
13
13
|
const DEFAULT_STATUS_KEY = 'pi-agent'
|
|
14
14
|
const SUBAGENT_SESSION_DIR = join(getAgentDir(), 'subagents', 'sessions')
|
|
15
15
|
|
|
16
|
-
type CmuxIntegrationConfig = {
|
|
16
|
+
export type CmuxIntegrationConfig = {
|
|
17
17
|
statusKey: string
|
|
18
18
|
notifyDone: boolean
|
|
19
19
|
includePromptPreviewInStatus: boolean
|
|
20
20
|
includeSubagents: boolean
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
type ActiveTurn = {
|
|
24
|
+
prompt: string
|
|
25
|
+
startedAt: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type FinishedTurn = ActiveTurn & {
|
|
29
|
+
remainingActiveTurns: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type CmuxTurnTracker = {
|
|
33
|
+
start(input: { key: string; prompt: string; ignored: boolean; now: number }): void
|
|
34
|
+
finish(key: string): FinishedTurn | undefined
|
|
35
|
+
activeCount(): number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createTurnTracker(): CmuxTurnTracker {
|
|
39
|
+
const activeTurns = new Map<string, ActiveTurn>()
|
|
40
|
+
const ignoredTurns = new Set<string>()
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
start(input) {
|
|
44
|
+
activeTurns.delete(input.key)
|
|
45
|
+
ignoredTurns.delete(input.key)
|
|
46
|
+
if (input.ignored) {
|
|
47
|
+
ignoredTurns.add(input.key)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
activeTurns.set(input.key, { prompt: input.prompt, startedAt: input.now })
|
|
51
|
+
},
|
|
52
|
+
finish(key) {
|
|
53
|
+
if (ignoredTurns.delete(key)) return undefined
|
|
54
|
+
const turn = activeTurns.get(key)
|
|
55
|
+
activeTurns.delete(key)
|
|
56
|
+
if (!turn) return undefined
|
|
57
|
+
return { ...turn, remainingActiveTurns: activeTurns.size }
|
|
58
|
+
},
|
|
59
|
+
activeCount() {
|
|
60
|
+
return activeTurns.size
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createCmuxTurnTrackerForTest(): CmuxTurnTracker {
|
|
66
|
+
return createTurnTracker()
|
|
67
|
+
}
|
|
68
|
+
|
|
23
69
|
function readConfig(env: NodeJS.ProcessEnv = process.env): CmuxIntegrationConfig {
|
|
24
70
|
return {
|
|
25
71
|
statusKey: env.PI_CMUX_STATUS_KEY || DEFAULT_STATUS_KEY,
|
|
@@ -53,21 +99,20 @@ function sessionKey(ctx: ExtensionContext) {
|
|
|
53
99
|
}
|
|
54
100
|
|
|
55
101
|
export default function cmuxIntegration(pi: ExtensionAPI) {
|
|
56
|
-
const
|
|
57
|
-
const activeStarts = new Map<string, number>()
|
|
102
|
+
const turnTracker = createTurnTracker()
|
|
58
103
|
const config = readConfig()
|
|
59
104
|
|
|
60
105
|
pi.on('before_agent_start', async (event, ctx) => {
|
|
61
106
|
const key = sessionKey(ctx)
|
|
62
|
-
if (!isCmuxEnvironment()
|
|
63
|
-
|
|
64
|
-
activeStarts.delete(key)
|
|
107
|
+
if (!isCmuxEnvironment()) {
|
|
108
|
+
turnTracker.finish(key)
|
|
65
109
|
return undefined
|
|
66
110
|
}
|
|
67
111
|
|
|
112
|
+
const ignored = shouldIgnoreTurn(ctx, event.prompt, config)
|
|
68
113
|
const preview = truncatePreview(event.prompt)
|
|
69
|
-
|
|
70
|
-
|
|
114
|
+
turnTracker.start({ key, prompt: preview, ignored, now: Date.now() })
|
|
115
|
+
if (ignored) return undefined
|
|
71
116
|
const status = config.includePromptPreviewInStatus ? `Running: ${truncatePreview(event.prompt, 48)}` : 'Running'
|
|
72
117
|
void setCmuxStatus(pi, config.statusKey, status, { icon: 'sparkles', color: '#3b82f6', signal: ctx.signal })
|
|
73
118
|
return undefined
|
|
@@ -75,23 +120,16 @@ export default function cmuxIntegration(pi: ExtensionAPI) {
|
|
|
75
120
|
|
|
76
121
|
pi.on('agent_end', async (_event, ctx) => {
|
|
77
122
|
const key = sessionKey(ctx)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
activeStarts.delete(key)
|
|
81
|
-
return
|
|
82
|
-
}
|
|
123
|
+
const finished = turnTracker.finish(key)
|
|
124
|
+
if (!isCmuxEnvironment() || !finished) return
|
|
83
125
|
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const elapsed = startedAt ? Math.max(1, Math.round((Date.now() - startedAt) / 1000)) : undefined
|
|
90
|
-
const idleText = elapsed ? `Idle (${elapsed}s)` : 'Idle'
|
|
91
|
-
void setCmuxStatus(pi, config.statusKey, idleText, { icon: 'check', color: '#34c759', signal: ctx.signal })
|
|
126
|
+
const elapsed = Math.max(1, Math.round((Date.now() - finished.startedAt) / 1000))
|
|
127
|
+
if (finished.remainingActiveTurns === 0) {
|
|
128
|
+
void setCmuxStatus(pi, config.statusKey, `Idle (${elapsed}s)`, { icon: 'check', color: '#34c759', signal: ctx.signal })
|
|
129
|
+
}
|
|
92
130
|
|
|
93
131
|
if (config.notifyDone) {
|
|
94
|
-
const body = prompt ? `Finished: ${prompt}` : 'Agent finished and is ready for your next message.'
|
|
132
|
+
const body = finished.prompt ? `Finished: ${finished.prompt}` : 'Agent finished and is ready for your next message.'
|
|
95
133
|
void notifyCmuxDone(pi, elapsed ? `${body}\nElapsed: ${elapsed}s` : body, {
|
|
96
134
|
title: 'Pi',
|
|
97
135
|
subtitle: 'Done',
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it } from 'node:test'
|
|
3
|
+
import { createCmuxTurnTrackerForTest } from '../cmuxIntegration.ts'
|
|
4
|
+
|
|
5
|
+
describe('cmux turn tracker', () => {
|
|
6
|
+
it('does not finish ignored subagent turns', () => {
|
|
7
|
+
const tracker = createCmuxTurnTrackerForTest()
|
|
8
|
+
tracker.start({ key: 'subagent', prompt: 'subagent work', ignored: true, now: 1000 })
|
|
9
|
+
|
|
10
|
+
assert.equal(tracker.activeCount(), 0)
|
|
11
|
+
assert.equal(tracker.finish('subagent'), undefined)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('keeps workspace running until all tracked turns finish', () => {
|
|
15
|
+
const tracker = createCmuxTurnTrackerForTest()
|
|
16
|
+
tracker.start({ key: 'main', prompt: 'main work', ignored: false, now: 1000 })
|
|
17
|
+
tracker.start({ key: 'worker', prompt: 'worker work', ignored: false, now: 2000 })
|
|
18
|
+
|
|
19
|
+
const first = tracker.finish('worker')
|
|
20
|
+
assert.equal(first?.remainingActiveTurns, 1)
|
|
21
|
+
assert.equal(tracker.activeCount(), 1)
|
|
22
|
+
|
|
23
|
+
const second = tracker.finish('main')
|
|
24
|
+
assert.equal(second?.remainingActiveTurns, 0)
|
|
25
|
+
assert.equal(tracker.activeCount(), 0)
|
|
26
|
+
})
|
|
27
|
+
})
|