pi-subagent-windows 0.0.0-stage → 0.1.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/LICENSE +22 -0
- package/README.md +106 -2
- package/bin/subagent.js +16 -0
- package/index.ts +344 -0
- package/package.json +36 -4
- package/shared.ts +385 -0
- package/skills/subagent/SKILL.md +93 -0
- package/subagent.ts +378 -0
- package/supervisor.ts +155 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mario Zechner (upstream pi-subagent)
|
|
4
|
+
Copyright (c) 2026 Muntasim Ul Haque (Windows port)
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,107 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Pi Subagent for Windows (no tmux)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Hidden, persistent Pi subagents for independent reviews, investigations, and delegated implementation — **without tmux**.
|
|
4
|
+
|
|
5
|
+
This is a Windows port of [badlogic/pi-subagent](https://github.com/badlogic/pi-subagent). The agent-facing workflow is unchanged: the main Pi agent spawns named subagents with the `subagent` CLI, waits for their final reports, steers them when needed, and stops them when done. Everything happens in the background — there is no attach screen.
|
|
6
|
+
|
|
7
|
+
## How it works
|
|
8
|
+
|
|
9
|
+
Upstream runs each subagent inside a tmux session and lets a human attach to its live TUI. Windows has no tmux, so this port replaces the tmux layer with a hidden process host:
|
|
10
|
+
|
|
11
|
+
- Each subagent is a detached, hidden **supervisor** process that owns a `pi --mode rpc` child.
|
|
12
|
+
- The supervisor holds the child's stdin pipe, drains its protocol output, writes `supervisor.log`, and auto-cancels extension dialogs (there is no interactive screen to answer them).
|
|
13
|
+
- Runs are tracked by pid, stopped with `taskkill /T /F`, and survive parent `/reload`, session switches, and parent crashes.
|
|
14
|
+
- Subagents are **status-only** on Windows. Use `subagent status`, `subagent wait`, and the run's `session.jsonl` for inspection.
|
|
15
|
+
|
|
16
|
+
On non-Windows platforms the upstream tmux behavior is kept, so the fork stays mergeable with upstream.
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
19
|
+
|
|
20
|
+
- Pi
|
|
21
|
+
- Node.js 22.19 or newer
|
|
22
|
+
- Windows (no tmux required)
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
pi install git:github.com/muntasimulhaque/pi-subagent-windows
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or clone it manually into Pi's global extension directory:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
git clone https://github.com/muntasimulhaque/pi-subagent-windows.git ~/.pi/agent/extensions/pi-subagent-windows
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Run `/reload` in an existing Pi session (or start a new one). On Windows the extension writes `subagent.cmd` and an extensionless `subagent` sh shim into `~/.pi/agent/bin`, so the CLI is on `PATH` from both PowerShell/cmd and Git Bash.
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
Spawn with a descriptive name using the parent session's provider, model, and thinking level:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
subagent spawn --name review --prompt "Review the current diff independently"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Override the model configuration when needed:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
subagent spawn \
|
|
50
|
+
--provider commandcode \
|
|
51
|
+
--model deepseek/deepseek-v4.1-flash \
|
|
52
|
+
--thinking low \
|
|
53
|
+
--prompt "Find the relevant implementation"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Provide multiple prompt fragments and files:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
subagent spawn \
|
|
60
|
+
--file /tmp/spec.md \
|
|
61
|
+
--prompt "Implement this specification" \
|
|
62
|
+
--prompt "Run the targeted tests"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Restrict tools for a read-only investigation:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
subagent spawn --tools read,grep,find,ls --prompt "Investigate the failure"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Manage a run by its generated handle:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
subagent status a1b2c3
|
|
75
|
+
subagent rename a1b2c3 "error handling review"
|
|
76
|
+
subagent send a1b2c3 "Focus on error handling"
|
|
77
|
+
subagent send a1b2c3 --follow-up "Then summarize"
|
|
78
|
+
subagent wait a1b2c3
|
|
79
|
+
subagent stop a1b2c3
|
|
80
|
+
subagent list
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Run `/subagent` to list active subagents spawned by the current session. On Windows, selecting one shows its status and session file.
|
|
84
|
+
|
|
85
|
+
## Run files
|
|
86
|
+
|
|
87
|
+
Each run lives in `~/.pi/agent/subagents/<handle>/`:
|
|
88
|
+
|
|
89
|
+
| File | Purpose |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `metadata.json` | State, model, session pointers |
|
|
92
|
+
| `session.jsonl` | The child Pi session (full transcript) |
|
|
93
|
+
| `inbox/` | Queued messages (steering, follow-ups) |
|
|
94
|
+
| `supervisor.log` | Process host log, child stderr, auto-cancelled dialogs |
|
|
95
|
+
| `supervisor.pid` | Process tracking |
|
|
96
|
+
|
|
97
|
+
## Differences and limitations
|
|
98
|
+
|
|
99
|
+
- **No attach screen.** Subagents are observable through status, `wait`, and the session file, not a live TUI.
|
|
100
|
+
- **`--file` handling:** text file contents (up to 1 MB) are inlined into the first prompt; larger or binary files are referenced by path and can be read by the subagent's `read` tool.
|
|
101
|
+
- **Extension dialogs** (`select`, `confirm`, `input`, `editor`) are auto-cancelled inside hidden subagents, with a note in `supervisor.log`. Fire-and-forget notifications are ignored.
|
|
102
|
+
- **Suspension kills the current turn.** Resuming relaunches the run idle with its full history.
|
|
103
|
+
- Nested subagents remain disabled.
|
|
104
|
+
|
|
105
|
+
## Credits
|
|
106
|
+
|
|
107
|
+
Forked from [badlogic/pi-subagent](https://github.com/badlogic/pi-subagent) by Mario Zechner (MIT). See `LICENSE`.
|
package/bin/subagent.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* npm/npx bin entry. Node cannot link a `.ts` file as a global command, so this
|
|
4
|
+
* thin wrapper forwards to the real CLI, which runs under Node's type stripping.
|
|
5
|
+
*/
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
const entry = join(dirname(fileURLToPath(import.meta.url)), "..", "subagent.ts");
|
|
11
|
+
const result = spawnSync(process.execPath, [entry, ...process.argv.slice(2)], { stdio: "inherit" });
|
|
12
|
+
if (result.error) {
|
|
13
|
+
process.stderr.write(`${result.error.message}\n`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
process.exit(result.status ?? 1);
|
package/index.ts
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, chmodSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { DynamicBorder, type ExtensionAPI, type ExtensionContext, keyHint } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { Container, type SelectItem, SelectList, Text, type TUI } from "@earendil-works/pi-tui";
|
|
7
|
+
import {
|
|
8
|
+
effectiveRunState,
|
|
9
|
+
getAgentDir,
|
|
10
|
+
inboxDir,
|
|
11
|
+
isRunAlive,
|
|
12
|
+
killRun,
|
|
13
|
+
launchRun,
|
|
14
|
+
listRuns,
|
|
15
|
+
readMetadata,
|
|
16
|
+
removeRunDir,
|
|
17
|
+
runDisplayName,
|
|
18
|
+
type InboxMessage,
|
|
19
|
+
type RunMetadata,
|
|
20
|
+
updateMetadata,
|
|
21
|
+
waitForRunShutdown,
|
|
22
|
+
} from "./shared.ts";
|
|
23
|
+
|
|
24
|
+
const packageDir = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* On Windows, keep `subagent.cmd` (cmd/PowerShell) and an extensionless `subagent`
|
|
28
|
+
* sh script (Git Bash, which does not resolve bare names through PATHEXT) in the
|
|
29
|
+
* agent bin directory so the CLI is on PATH after install. Never clobbers a file
|
|
30
|
+
* this extension did not write.
|
|
31
|
+
*/
|
|
32
|
+
function ensureCliShim(): void {
|
|
33
|
+
if (process.platform !== "win32") return;
|
|
34
|
+
try {
|
|
35
|
+
const binDir = join(getAgentDir(), "bin");
|
|
36
|
+
const cliPath = join(packageDir, "subagent.ts");
|
|
37
|
+
const cliPathUnix = cliPath.replace(/\\/g, "/");
|
|
38
|
+
|
|
39
|
+
const cmdMarker = "rem pi-subagent-windows shim";
|
|
40
|
+
const cmdContent = `@ECHO off\r\n${cmdMarker}\r\nnode "${cliPath}" %*\r\n`;
|
|
41
|
+
|
|
42
|
+
const shMarker = "# pi-subagent-windows shim";
|
|
43
|
+
const shContent = `#!/bin/sh\n${shMarker}\nexec node "${cliPathUnix}" "$@"\n`;
|
|
44
|
+
|
|
45
|
+
mkdirSync(binDir, { recursive: true });
|
|
46
|
+
const writeShim = (path: string, marker: string, content: string, mode?: number): void => {
|
|
47
|
+
let shouldWrite = true;
|
|
48
|
+
if (existsSync(path)) {
|
|
49
|
+
const current = readFileSync(path, "utf8");
|
|
50
|
+
if (!current.includes(marker)) return; // foreign file: leave it alone
|
|
51
|
+
if (current === content) shouldWrite = false;
|
|
52
|
+
}
|
|
53
|
+
if (shouldWrite) writeFileSync(path, content, { encoding: "utf8" });
|
|
54
|
+
if (mode !== undefined) chmodSync(path, mode);
|
|
55
|
+
};
|
|
56
|
+
writeShim(join(binDir, "subagent.cmd"), cmdMarker, cmdContent);
|
|
57
|
+
writeShim(join(binDir, "subagent"), shMarker, shContent, 0o755);
|
|
58
|
+
} catch {
|
|
59
|
+
// The shim is a convenience; extension loading must never fail over it.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isInboxMessage(value: unknown): value is InboxMessage {
|
|
64
|
+
if (typeof value !== "object" || value === null) return false;
|
|
65
|
+
const message = value as Record<string, unknown>;
|
|
66
|
+
return typeof message.message === "string" && (message.delivery === "auto" || message.delivery === "followUp");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function displayState(metadata: RunMetadata): string {
|
|
70
|
+
return effectiveRunState(metadata).padEnd(8);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default function subagentExtension(pi: ExtensionAPI) {
|
|
74
|
+
const runDir = process.env.PI_SUBAGENT_RUN_DIR;
|
|
75
|
+
if (!runDir) {
|
|
76
|
+
pi.on("resources_discover", () => ({ skillPaths: [join(packageDir, "skills")] }));
|
|
77
|
+
ensureCliShim();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
pi.registerCommand("subagent", {
|
|
81
|
+
description: "Select a subagent spawned by this session",
|
|
82
|
+
handler: async (_args, ctx) => {
|
|
83
|
+
const runs = listRuns(ctx.sessionManager.getSessionId()).filter((run) => effectiveRunState(run) !== "exited");
|
|
84
|
+
if (runs.length === 0) {
|
|
85
|
+
ctx.ui.notify("No active subagents spawned by this session", "info");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const items: SelectItem[] = runs.map((run) => ({
|
|
90
|
+
value: run.handle,
|
|
91
|
+
label: `${runDisplayName(run)} ${displayState(run)} ${run.provider}/${run.model} ${run.thinking}`,
|
|
92
|
+
}));
|
|
93
|
+
let tui: TUI | undefined;
|
|
94
|
+
const selected = await ctx.ui.custom<string | undefined>((customTui, theme, _keybindings, done) => {
|
|
95
|
+
tui = customTui;
|
|
96
|
+
const list = new SelectList(items, Math.min(items.length, 10), {
|
|
97
|
+
selectedPrefix: (text) => theme.fg("accent", text),
|
|
98
|
+
selectedText: (text) => theme.fg("accent", text),
|
|
99
|
+
description: (text) => theme.fg("muted", text),
|
|
100
|
+
scrollInfo: (text) => theme.fg("dim", text),
|
|
101
|
+
noMatch: (text) => theme.fg("warning", text),
|
|
102
|
+
});
|
|
103
|
+
list.onSelect = (item) => done(item.value);
|
|
104
|
+
list.onCancel = () => done(undefined);
|
|
105
|
+
|
|
106
|
+
const container = new Container();
|
|
107
|
+
container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
|
|
108
|
+
container.addChild(
|
|
109
|
+
new Text(theme.fg("accent", theme.bold(process.platform === "win32" ? "Subagent status" : "Attach to subagent")), 1, 0),
|
|
110
|
+
);
|
|
111
|
+
container.addChild(list);
|
|
112
|
+
container.addChild(
|
|
113
|
+
new Text(
|
|
114
|
+
theme.fg(
|
|
115
|
+
"dim",
|
|
116
|
+
`${keyHint("tui.select.confirm", process.platform === "win32" ? "status" : "attach")} ${keyHint("tui.select.cancel", "cancel")}`,
|
|
117
|
+
),
|
|
118
|
+
1,
|
|
119
|
+
0,
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
render: (width) => container.render(width),
|
|
126
|
+
invalidate: () => container.invalidate(),
|
|
127
|
+
handleInput: (data) => {
|
|
128
|
+
list.handleInput(data);
|
|
129
|
+
customTui.requestRender();
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
if (!selected || !tui) return;
|
|
134
|
+
const run = runs.find((candidate) => candidate.handle === selected);
|
|
135
|
+
if (!run) return;
|
|
136
|
+
|
|
137
|
+
if (process.platform === "win32") {
|
|
138
|
+
ctx.ui.notify(
|
|
139
|
+
`${runDisplayName(run)}: ${effectiveRunState(run)} — session: ${run.sessionFile}`,
|
|
140
|
+
"info",
|
|
141
|
+
);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (process.env.TMUX) {
|
|
146
|
+
const exitCode = await new Promise<number | null>((resolveExit) => {
|
|
147
|
+
const child = spawn("tmux", ["switch-client", "-t", run.tmuxSession], { stdio: "inherit" });
|
|
148
|
+
child.on("error", () => resolveExit(null));
|
|
149
|
+
child.on("close", resolveExit);
|
|
150
|
+
});
|
|
151
|
+
if (exitCode !== 0) ctx.ui.notify(`Could not switch to ${run.handle}`, "error");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
tui.stop();
|
|
156
|
+
try {
|
|
157
|
+
const exitCode = await new Promise<number | null>((resolveExit) => {
|
|
158
|
+
const child = spawn("tmux", ["attach-session", "-t", run.tmuxSession], { stdio: "inherit" });
|
|
159
|
+
child.on("error", () => resolveExit(null));
|
|
160
|
+
child.on("close", resolveExit);
|
|
161
|
+
});
|
|
162
|
+
if (exitCode !== 0) process.stderr.write(`Could not attach to ${run.handle}\n`);
|
|
163
|
+
} finally {
|
|
164
|
+
tui.start();
|
|
165
|
+
tui.requestRender(true);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (!runDir) {
|
|
171
|
+
let widgetTimer: ReturnType<typeof setInterval> | undefined;
|
|
172
|
+
let widgetContext: ExtensionContext | undefined;
|
|
173
|
+
|
|
174
|
+
const refreshWidget = (): void => {
|
|
175
|
+
if (!widgetContext) return;
|
|
176
|
+
const activeRuns = listRuns(widgetContext.sessionManager.getSessionId())
|
|
177
|
+
.map((run) => ({ run, state: effectiveRunState(run) }))
|
|
178
|
+
.filter(({ state }) => state !== "exited");
|
|
179
|
+
if (activeRuns.length === 0) {
|
|
180
|
+
widgetContext.ui.setWidget("subagents", undefined);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const visible = activeRuns.slice(0, 5).map(({ run, state }) => {
|
|
185
|
+
const color =
|
|
186
|
+
state === "busy" ? "warning" : state === "idle" ? "success" : state === "error" ? "error" : "muted";
|
|
187
|
+
return widgetContext!.ui.theme.fg(color, `${run.name ?? run.handle}:${state}`);
|
|
188
|
+
});
|
|
189
|
+
if (activeRuns.length > visible.length) {
|
|
190
|
+
visible.push(widgetContext.ui.theme.fg("muted", `+${activeRuns.length - visible.length}`));
|
|
191
|
+
}
|
|
192
|
+
widgetContext.ui.setWidget(
|
|
193
|
+
"subagents",
|
|
194
|
+
[widgetContext.ui.theme.fg("dim", "subagents: ") + visible.join(widgetContext.ui.theme.fg("dim", " | "))],
|
|
195
|
+
{ placement: "belowEditor" },
|
|
196
|
+
);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
pi.on("session_start", (_event, ctx) => {
|
|
200
|
+
// Relaunch children that were suspended when this session was last quit or switched away from.
|
|
201
|
+
for (const run of listRuns(ctx.sessionManager.getSessionId())) {
|
|
202
|
+
if (!run.suspended || isRunAlive(run)) continue;
|
|
203
|
+
if (!existsSync(run.sessionFile)) {
|
|
204
|
+
removeRunDir(run.runDir);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const starting = updateMetadata(run.runDir, { state: "starting", error: undefined }) ?? run;
|
|
208
|
+
try {
|
|
209
|
+
launchRun(starting);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
212
|
+
updateMetadata(run.runDir, { state: "error", error: message });
|
|
213
|
+
if (ctx.hasUI) ctx.ui.notify(`Could not resume subagent ${runDisplayName(run)}: ${message}`, "error");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!ctx.hasUI) return;
|
|
218
|
+
widgetContext = ctx;
|
|
219
|
+
refreshWidget();
|
|
220
|
+
widgetTimer = setInterval(refreshWidget, 1000);
|
|
221
|
+
widgetTimer.unref();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
pi.on("session_shutdown", async (event, ctx) => {
|
|
225
|
+
if (widgetTimer) clearInterval(widgetTimer);
|
|
226
|
+
widgetTimer = undefined;
|
|
227
|
+
widgetContext = undefined;
|
|
228
|
+
ctx.ui.setWidget("subagents", undefined);
|
|
229
|
+
if (event.reason === "reload") return;
|
|
230
|
+
// Suspend running children: stop the process but keep transcript and metadata so resuming this
|
|
231
|
+
// session relaunches them. Children that already exited on their own are discarded.
|
|
232
|
+
for (const run of listRuns(ctx.sessionManager.getSessionId())) {
|
|
233
|
+
if (!isRunAlive(run)) {
|
|
234
|
+
if (!run.suspended) removeRunDir(run.runDir);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
updateMetadata(run.runDir, { suspended: true });
|
|
238
|
+
killRun(run);
|
|
239
|
+
await waitForRunShutdown(run.runDir);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let currentContext: ExtensionContext | undefined;
|
|
246
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
247
|
+
let processing = false;
|
|
248
|
+
let sessionName: string | undefined;
|
|
249
|
+
|
|
250
|
+
const syncSessionName = (): void => {
|
|
251
|
+
const metadata = readMetadata(runDir);
|
|
252
|
+
if (!metadata) return;
|
|
253
|
+
const next = `subagent ${metadata.name ?? metadata.handle}`;
|
|
254
|
+
if (next === sessionName) return;
|
|
255
|
+
pi.setSessionName(next);
|
|
256
|
+
sessionName = next;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const processInbox = async (): Promise<void> => {
|
|
260
|
+
if (processing || !currentContext) return;
|
|
261
|
+
syncSessionName();
|
|
262
|
+
const queueDir = inboxDir(runDir);
|
|
263
|
+
if (!existsSync(queueDir)) return;
|
|
264
|
+
processing = true;
|
|
265
|
+
try {
|
|
266
|
+
for (const name of readdirSync(queueDir)
|
|
267
|
+
.filter((entry) => entry.endsWith(".json"))
|
|
268
|
+
.sort()) {
|
|
269
|
+
const path = join(queueDir, name);
|
|
270
|
+
let payload: InboxMessage;
|
|
271
|
+
try {
|
|
272
|
+
const value: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
273
|
+
if (!isInboxMessage(value)) throw new Error("Invalid inbox message");
|
|
274
|
+
payload = value;
|
|
275
|
+
} catch (error) {
|
|
276
|
+
unlinkSync(path);
|
|
277
|
+
updateMetadata(runDir, {
|
|
278
|
+
state: "error",
|
|
279
|
+
error: error instanceof Error ? error.message : String(error),
|
|
280
|
+
});
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
updateMetadata(runDir, { state: "busy", error: undefined });
|
|
285
|
+
try {
|
|
286
|
+
if (currentContext.isIdle()) {
|
|
287
|
+
pi.sendUserMessage(payload.message);
|
|
288
|
+
} else {
|
|
289
|
+
pi.sendUserMessage(payload.message, {
|
|
290
|
+
deliverAs: payload.delivery === "followUp" ? "followUp" : "steer",
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
unlinkSync(path);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
updateMetadata(runDir, {
|
|
296
|
+
state: currentContext.isIdle() ? "idle" : "busy",
|
|
297
|
+
error: error instanceof Error ? error.message : String(error),
|
|
298
|
+
});
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
} finally {
|
|
303
|
+
processing = false;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
pi.on("session_start", (_event, ctx) => {
|
|
308
|
+
currentContext = ctx;
|
|
309
|
+
const metadata = readMetadata(runDir);
|
|
310
|
+
if (!metadata) return;
|
|
311
|
+
updateMetadata(runDir, {
|
|
312
|
+
childSessionId: ctx.sessionManager.getSessionId(),
|
|
313
|
+
sessionFile: ctx.sessionManager.getSessionFile() ?? metadata.sessionFile,
|
|
314
|
+
state: ctx.isIdle() ? "idle" : "busy",
|
|
315
|
+
suspended: undefined,
|
|
316
|
+
error: undefined,
|
|
317
|
+
});
|
|
318
|
+
syncSessionName();
|
|
319
|
+
if (!timer) {
|
|
320
|
+
timer = setInterval(() => void processInbox(), 250);
|
|
321
|
+
timer.unref();
|
|
322
|
+
}
|
|
323
|
+
void processInbox();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
327
|
+
currentContext = ctx;
|
|
328
|
+
updateMetadata(runDir, { state: "busy", hasStarted: true, error: undefined });
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
332
|
+
currentContext = ctx;
|
|
333
|
+
if (ctx.isIdle()) updateMetadata(runDir, { state: "idle" });
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
pi.on("session_shutdown", (event) => {
|
|
337
|
+
currentContext = undefined;
|
|
338
|
+
if (timer) {
|
|
339
|
+
clearInterval(timer);
|
|
340
|
+
timer = undefined;
|
|
341
|
+
}
|
|
342
|
+
if (event.reason === "quit") updateMetadata(runDir, { state: "exited" });
|
|
343
|
+
});
|
|
344
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-subagent-windows",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Windows port of pi-subagent: hidden, persistent Pi subagents without tmux",
|
|
5
|
+
"keywords": ["pi-package", "pi-extension", "subagent", "windows"],
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "Muntasim Ul Haque",
|
|
9
|
+
"homepage": "https://github.com/muntasimulhaque/pi-subagent-windows#readme",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/muntasimulhaque/pi-subagent-windows.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/muntasimulhaque/pi-subagent-windows/issues"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin",
|
|
19
|
+
"index.ts",
|
|
20
|
+
"shared.ts",
|
|
21
|
+
"subagent.ts",
|
|
22
|
+
"supervisor.ts",
|
|
23
|
+
"skills"
|
|
24
|
+
],
|
|
25
|
+
"bin": {
|
|
26
|
+
"subagent": "./bin/subagent.js"
|
|
27
|
+
},
|
|
28
|
+
"pi": {
|
|
29
|
+
"extensions": ["./index.ts"]
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=22.19.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
36
|
+
"@earendil-works/pi-tui": "*"
|
|
37
|
+
}
|
|
38
|
+
}
|