pi-subagent-in-memory 0.1.2 → 0.1.4
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 +13 -30
- package/extensions/index.ts +267 -65
- package/extensions/tui-draw.ts +36 -8
- package/media/parallel-subagents.png +0 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -32,11 +32,20 @@ If `provider` and `model` are omitted, the subagent inherits the main agent's mo
|
|
|
32
32
|
|
|
33
33
|
Each running subagent is displayed as a colored card widget above the editor:
|
|
34
34
|
|
|
35
|
-
- Cards show **title**, **model**, **
|
|
35
|
+
- Cards show **title**, **model**, **prompt preview**, **elapsed time**, and **status indicator** (⏳ started, ⚡ working…, ✅ finished, ❌ error)
|
|
36
|
+
- The prompt passed to the subagent is displayed as card content, so you can see at a glance what each subagent is doing
|
|
36
37
|
- Cards auto-layout into a responsive grid (1–3 columns based on `columnWidthPercent`)
|
|
37
|
-
-
|
|
38
|
+
- Subagent number badge (`#1`, `#2`, …) shown on the top-right corner of each card
|
|
38
39
|
- Six rotating color themes for visual distinction between cards
|
|
39
40
|
|
|
41
|
+
### 🔍 Subagent Detail Overlay (`Ctrl+N`)
|
|
42
|
+
|
|
43
|
+
Press **Ctrl+1** through **Ctrl+9** to open a detail popup for the corresponding subagent:
|
|
44
|
+
|
|
45
|
+
- **Prompt** — Full prompt text with word wrapping (up to 5 lines)
|
|
46
|
+
- **Messages** — Live-updating stream of the subagent's activity (text output, tool calls, status changes), always showing the latest 5 lines
|
|
47
|
+
- Press the same **Ctrl+N** shortcut or **Escape** to close the overlay
|
|
48
|
+
|
|
40
49
|
### 📝 JSONL Session Logging
|
|
41
50
|
|
|
42
51
|
Every subagent session is logged to disk for debugging and auditing:
|
|
@@ -68,40 +77,14 @@ Type `/in-memory-clear-widgets` in the pi prompt to clear all subagent card widg
|
|
|
68
77
|
|
|
69
78
|
## Install
|
|
70
79
|
|
|
71
|
-
### From local path (recommended for development)
|
|
72
|
-
|
|
73
|
-
```bash
|
|
74
|
-
pi install /path/to/pi-subagent-in-memory
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### From a git repository
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
pi install git:github.com/your-org/pi-subagent-in-memory
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### Project-local install (shared with team via `.pi/settings.json`)
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
pi install -l /path/to/pi-subagent-in-memory
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
### One-time use without installing
|
|
90
|
-
|
|
91
80
|
```bash
|
|
92
|
-
pi
|
|
81
|
+
pi install npm:pi-subagent-in-memory
|
|
93
82
|
```
|
|
94
83
|
|
|
95
84
|
## Remove
|
|
96
85
|
|
|
97
86
|
```bash
|
|
98
|
-
pi remove pi-subagent-in-memory
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
For project-local installs:
|
|
102
|
-
|
|
103
|
-
```bash
|
|
104
|
-
pi remove -l pi-subagent-in-memory
|
|
87
|
+
pi remove npm:pi-subagent-in-memory
|
|
105
88
|
```
|
|
106
89
|
|
|
107
90
|
## Verify Installation
|
package/extensions/index.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* - Nested subagent support (subagents can spawn subagents)
|
|
16
16
|
* - /in-memory-clear-widgets slash command to remove widget cards
|
|
17
17
|
* - Multi-provider support (Anthropic, OpenAI, Google, etc.)
|
|
18
|
+
* - Ctrl+Alt+<N> to inspect subagent prompt & live messages
|
|
18
19
|
*
|
|
19
20
|
* Results are written to ./.pi/subagent-in-memory/<mainSessionId>/subagent_<N>/result.md
|
|
20
21
|
* (or error.md on failure) so the calling agent gets a short pointer instead of
|
|
@@ -38,7 +39,8 @@ import type { AgentToolResult, AgentToolUpdateCallback } from "@mariozechner/pi-
|
|
|
38
39
|
import { resolveModel } from "./model.ts";
|
|
39
40
|
import { renderCard, type CardTheme } from "./tui-draw.ts";
|
|
40
41
|
|
|
41
|
-
import { visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
|
|
42
|
+
import { visibleWidth, truncateToWidth, wrapTextWithAnsi, matchesKey, Key } from "@mariozechner/pi-tui";
|
|
43
|
+
import type { Focusable } from "@mariozechner/pi-tui";
|
|
42
44
|
import { mkdirSync, writeFileSync, appendFileSync } from "node:fs";
|
|
43
45
|
import { join } from "node:path";
|
|
44
46
|
import { randomUUID } from "node:crypto";
|
|
@@ -50,11 +52,13 @@ function jsonlAppend(filePath: string, data: Record<string, any>) {
|
|
|
50
52
|
|
|
51
53
|
// ── Subagent card state ─────────────────────────────────────────
|
|
52
54
|
interface SubagentCard {
|
|
55
|
+
num: number;
|
|
53
56
|
sessionId: string;
|
|
54
57
|
title: string;
|
|
55
58
|
modelLabel: string;
|
|
56
59
|
status: "created" | "running" | "completed" | "error";
|
|
57
|
-
|
|
60
|
+
prompt: string;
|
|
61
|
+
messages: string;
|
|
58
62
|
columnWidthPercent: number;
|
|
59
63
|
startedAt: number;
|
|
60
64
|
endedAt?: number;
|
|
@@ -81,76 +85,232 @@ const subagents: SubagentCard[] = [];
|
|
|
81
85
|
let currentCtx: { ui: any } | null = null;
|
|
82
86
|
let mainSessionId = "unknown";
|
|
83
87
|
let subagentCount = 0;
|
|
88
|
+
let flashTimer: ReturnType<typeof setInterval> | null = null;
|
|
89
|
+
|
|
90
|
+
// Track open detail overlay so we can trigger re-renders
|
|
91
|
+
let activeDetailTui: any = null;
|
|
84
92
|
|
|
85
93
|
function updateSubagentWidget() {
|
|
86
94
|
if (!currentCtx) return;
|
|
87
95
|
const ctx = currentCtx;
|
|
88
96
|
|
|
97
|
+
// Start flash timer for "working..." dot animation if not already running
|
|
98
|
+
if (!flashTimer) {
|
|
99
|
+
flashTimer = setInterval(() => {
|
|
100
|
+
const hasActive = subagents.some((sa) => sa.status === "running" || sa.status === "created");
|
|
101
|
+
if (currentCtx && (hasActive || activeDetailTui)) {
|
|
102
|
+
currentCtx.ui.setWidget(
|
|
103
|
+
"in-memory-subagent-cards",
|
|
104
|
+
buildWidgetFactory(),
|
|
105
|
+
{ placement: "aboveEditor" }
|
|
106
|
+
);
|
|
107
|
+
// Also re-render the detail overlay if open
|
|
108
|
+
if (activeDetailTui) {
|
|
109
|
+
try { activeDetailTui.requestRender(); } catch {}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}, 500);
|
|
113
|
+
}
|
|
114
|
+
|
|
89
115
|
ctx.ui.setWidget(
|
|
90
116
|
"in-memory-subagent-cards",
|
|
91
|
-
(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// Derive cols from columnWidthPercent (all cards share the same value).
|
|
96
|
-
const pct = subagents[subagents.length - 1].columnWidthPercent;
|
|
97
|
-
const cols = Math.min(3, Math.max(1, Math.round(100 / pct)));
|
|
98
|
-
const gap = 1;
|
|
99
|
-
const colWidth = Math.floor((width - gap * (cols - 1)) / cols);
|
|
100
|
-
const maxContentLines = 4;
|
|
101
|
-
const lines: string[] = [""];
|
|
102
|
-
|
|
103
|
-
for (let i = 0; i < subagents.length; i += cols) {
|
|
104
|
-
const rowCards = subagents.slice(i, i + cols).map((sa, idx) => {
|
|
105
|
-
const cardTheme = CARD_THEMES[(i + idx) % CARD_THEMES.length];
|
|
106
|
-
|
|
107
|
-
const titleText = `${sa.title} [${sa.modelLabel}]`;
|
|
108
|
-
const innerW = colWidth - 4;
|
|
109
|
-
|
|
110
|
-
const allText = sa.textPreview || "…";
|
|
111
|
-
const contentLines = allText.split("\n");
|
|
112
|
-
const trimmedLines = contentLines.map((l) =>
|
|
113
|
-
visibleWidth(l) > innerW ? "…" + truncateToWidth(l, innerW - 1) : l
|
|
114
|
-
);
|
|
115
|
-
const visible = trimmedLines.slice(-maxContentLines);
|
|
116
|
-
const content = (contentLines.length > maxContentLines ? "…\n" : "") + visible.join("\n");
|
|
117
|
-
|
|
118
|
-
const statusIcon = sa.status === "completed" ? "✓" : sa.status === "error" ? "✗" : "●";
|
|
119
|
-
const footer = `${statusIcon} ${formatElapsed(sa.startedAt, sa.endedAt)}`;
|
|
120
|
-
|
|
121
|
-
return renderCard({
|
|
122
|
-
title: titleText,
|
|
123
|
-
content,
|
|
124
|
-
footer,
|
|
125
|
-
colWidth,
|
|
126
|
-
theme,
|
|
127
|
-
cardTheme,
|
|
128
|
-
});
|
|
129
|
-
});
|
|
117
|
+
buildWidgetFactory(),
|
|
118
|
+
{ placement: "aboveEditor" }
|
|
119
|
+
);
|
|
120
|
+
}
|
|
130
121
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
122
|
+
function buildWidgetFactory() {
|
|
123
|
+
return (_tui: any, theme: any) => ({
|
|
124
|
+
render(width: number): string[] {
|
|
125
|
+
if (subagents.length === 0) return [];
|
|
126
|
+
|
|
127
|
+
// Derive cols from columnWidthPercent (all cards share the same value).
|
|
128
|
+
const pct = subagents[subagents.length - 1].columnWidthPercent;
|
|
129
|
+
const cols = Math.min(3, Math.max(1, Math.round(100 / pct)));
|
|
130
|
+
const gap = 1;
|
|
131
|
+
const colWidth = Math.floor((width - gap * (cols - 1)) / cols);
|
|
132
|
+
const maxContentLines = 4;
|
|
133
|
+
const lines: string[] = [""];
|
|
134
|
+
|
|
135
|
+
for (let i = 0; i < subagents.length; i += cols) {
|
|
136
|
+
const rowCards = subagents.slice(i, i + cols).map((sa, idx) => {
|
|
137
|
+
const cardTheme = CARD_THEMES[(i + idx) % CARD_THEMES.length];
|
|
138
|
+
|
|
139
|
+
const titleText = `${sa.title} [${sa.modelLabel}]`;
|
|
140
|
+
const innerW = colWidth - 4;
|
|
141
|
+
|
|
142
|
+
// Show the prompt as content
|
|
143
|
+
const allText = sa.prompt || "…";
|
|
144
|
+
const contentLines = allText.split("\n");
|
|
145
|
+
const trimmedLines = contentLines.map((l) =>
|
|
146
|
+
visibleWidth(l) > innerW ? truncateToWidth(l, innerW - 1) + "…" : l
|
|
147
|
+
);
|
|
148
|
+
const visible = trimmedLines.slice(0, maxContentLines);
|
|
149
|
+
const content = visible.join("\n") + (contentLines.length > maxContentLines ? "\n…" : "");
|
|
150
|
+
|
|
151
|
+
// Status indicator with animated dots for "working"
|
|
152
|
+
// All status strings are padded to the same visible width so the
|
|
153
|
+
// elapsed-time counter stays in a fixed column.
|
|
154
|
+
let statusRaw: string;
|
|
155
|
+
if (sa.status === "created") {
|
|
156
|
+
statusRaw = "⏳ started";
|
|
157
|
+
} else if (sa.status === "running") {
|
|
158
|
+
const dotPhase = Math.floor(Date.now() / 2000) % 3;
|
|
159
|
+
statusRaw = "⚡ working" + ".".repeat(dotPhase + 1);
|
|
160
|
+
} else if (sa.status === "completed") {
|
|
161
|
+
statusRaw = "✅ finished";
|
|
162
|
+
} else {
|
|
163
|
+
statusRaw = "❌ error";
|
|
134
164
|
}
|
|
165
|
+
// Pad to fixed visible width (longest is "⚡ working..." or "✅ finished")
|
|
166
|
+
const STATUS_WIDTH = 14;
|
|
167
|
+
const visPad = Math.max(0, STATUS_WIDTH - visibleWidth(statusRaw));
|
|
168
|
+
const elapsed = formatElapsed(sa.startedAt, sa.endedAt);
|
|
169
|
+
const footer = `${statusRaw}${" ".repeat(visPad)} ${elapsed}`;
|
|
170
|
+
|
|
171
|
+
return renderCard({
|
|
172
|
+
title: titleText,
|
|
173
|
+
badge: `#${sa.num}`,
|
|
174
|
+
content,
|
|
175
|
+
footer,
|
|
176
|
+
footerRight: `Ctrl+${sa.num}`,
|
|
177
|
+
colWidth,
|
|
178
|
+
theme,
|
|
179
|
+
cardTheme,
|
|
180
|
+
});
|
|
181
|
+
});
|
|
135
182
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
}
|
|
183
|
+
// Pad incomplete rows
|
|
184
|
+
while (rowCards.length < cols) {
|
|
185
|
+
rowCards.push(Array(rowCards[0].length).fill(" ".repeat(colWidth)));
|
|
186
|
+
}
|
|
142
187
|
|
|
143
|
-
|
|
144
|
-
|
|
188
|
+
const cardHeight = Math.max(...rowCards.map((c) => c.length));
|
|
189
|
+
for (const card of rowCards) {
|
|
190
|
+
while (card.length < cardHeight) {
|
|
191
|
+
card.push(" ".repeat(colWidth));
|
|
145
192
|
}
|
|
146
193
|
}
|
|
147
194
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
195
|
+
for (let row = 0; row < cardHeight; row++) {
|
|
196
|
+
lines.push(rowCards.map((card) => card[row]).join(" ".repeat(gap)));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return lines;
|
|
201
|
+
},
|
|
202
|
+
invalidate() {},
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Detail overlay component ────────────────────────────────────
|
|
207
|
+
class SubagentDetailOverlay implements Focusable {
|
|
208
|
+
focused = false;
|
|
209
|
+
|
|
210
|
+
constructor(
|
|
211
|
+
private card: SubagentCard,
|
|
212
|
+
private cardNum: number,
|
|
213
|
+
private theme: any,
|
|
214
|
+
private done: (result: void) => void,
|
|
215
|
+
) {}
|
|
216
|
+
|
|
217
|
+
handleInput(data: string): void {
|
|
218
|
+
// Close on Escape, Enter, or the same Ctrl+N that opened it
|
|
219
|
+
if (
|
|
220
|
+
matchesKey(data, "escape") ||
|
|
221
|
+
matchesKey(data, "return") ||
|
|
222
|
+
matchesKey(data, Key.ctrl(`${this.cardNum}` as any))
|
|
223
|
+
) {
|
|
224
|
+
this.done();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
render(width: number): string[] {
|
|
230
|
+
const th = this.theme;
|
|
231
|
+
const sa = this.card;
|
|
232
|
+
const innerW = width - 2; // borders are 2 chars total
|
|
233
|
+
|
|
234
|
+
const pad = (s: string, len: number) => {
|
|
235
|
+
const vis = visibleWidth(s);
|
|
236
|
+
return s + " ".repeat(Math.max(0, len - vis));
|
|
237
|
+
};
|
|
238
|
+
const row = (content: string) =>
|
|
239
|
+
th.fg("border", "│") + pad(content, innerW) + th.fg("border", "│");
|
|
240
|
+
const divider = () =>
|
|
241
|
+
th.fg("border", "├" + "─".repeat(innerW) + "┤");
|
|
242
|
+
|
|
243
|
+
const lines: string[] = [];
|
|
244
|
+
|
|
245
|
+
// Top border
|
|
246
|
+
lines.push(th.fg("border", "╭" + "─".repeat(innerW) + "╮"));
|
|
247
|
+
|
|
248
|
+
// Header
|
|
249
|
+
const statusIcon = sa.status === "created" ? "⏳"
|
|
250
|
+
: sa.status === "running" ? "⚡"
|
|
251
|
+
: sa.status === "completed" ? "✅"
|
|
252
|
+
: "❌";
|
|
253
|
+
const headerText = ` ${statusIcon} Subagent #${sa.num}: ${sa.title} [${sa.modelLabel}]`;
|
|
254
|
+
lines.push(row(th.fg("accent", th.bold(truncateToWidth(headerText, innerW)))));
|
|
255
|
+
lines.push(row(th.fg("dim", ` ${formatElapsed(sa.startedAt, sa.endedAt)} elapsed`)));
|
|
256
|
+
|
|
257
|
+
// Prompt section — word-wrap to show at least 3 lines, max 5
|
|
258
|
+
lines.push(divider());
|
|
259
|
+
lines.push(row(th.fg("accent", " PROMPT")));
|
|
260
|
+
const promptWrapWidth = innerW - 2; // 1 char padding each side
|
|
261
|
+
const promptLines = wrapTextWithAnsi(sa.prompt, promptWrapWidth);
|
|
262
|
+
const PROMPT_MIN = 3;
|
|
263
|
+
const PROMPT_MAX = 5;
|
|
264
|
+
const promptDisplay = promptLines.slice(0, PROMPT_MAX);
|
|
265
|
+
for (const pl of promptDisplay) {
|
|
266
|
+
lines.push(row(" " + th.fg("text", truncateToWidth(pl, innerW - 1))));
|
|
267
|
+
}
|
|
268
|
+
// Pad to minimum rows so prompt section is always visible
|
|
269
|
+
for (let r = promptDisplay.length; r < PROMPT_MIN; r++) {
|
|
270
|
+
lines.push(row(""));
|
|
271
|
+
}
|
|
272
|
+
if (promptLines.length > PROMPT_MAX) {
|
|
273
|
+
lines.push(row(th.fg("dim", ` … (${promptLines.length - PROMPT_MAX} more lines)`)));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Messages section — always show latest 5 lines
|
|
277
|
+
lines.push(divider());
|
|
278
|
+
lines.push(row(th.fg("accent", " MESSAGES")));
|
|
279
|
+
|
|
280
|
+
const MSG_VISIBLE = 5;
|
|
281
|
+
const msgText = sa.messages || "(no messages yet)";
|
|
282
|
+
const allMsgLines = msgText.split("\n");
|
|
283
|
+
// Always auto-scroll to the latest lines
|
|
284
|
+
const msgStart = Math.max(0, allMsgLines.length - MSG_VISIBLE);
|
|
285
|
+
const visibleMsgLines = allMsgLines.slice(msgStart);
|
|
286
|
+
for (const ml of visibleMsgLines) {
|
|
287
|
+
lines.push(row(" " + th.fg("muted", truncateToWidth(ml, innerW - 1))));
|
|
288
|
+
}
|
|
289
|
+
// Pad to minimum rows
|
|
290
|
+
for (let r = visibleMsgLines.length; r < MSG_VISIBLE; r++) {
|
|
291
|
+
lines.push(row(""));
|
|
292
|
+
}
|
|
293
|
+
if (allMsgLines.length > MSG_VISIBLE) {
|
|
294
|
+
lines.push(row(th.fg("dim", ` … ${allMsgLines.length - MSG_VISIBLE} earlier lines hidden`)));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Bottom border with right-aligned hint
|
|
298
|
+
const hint = ` Esc / Ctrl+${sa.num} `;
|
|
299
|
+
const hintLen = hint.length;
|
|
300
|
+
const dashBefore = Math.max(0, innerW - hintLen);
|
|
301
|
+
lines.push(
|
|
302
|
+
th.fg("border", "╰" + "─".repeat(dashBefore)) +
|
|
303
|
+
th.fg("dim", hint) +
|
|
304
|
+
th.fg("border", "╯")
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
return lines;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
invalidate(): void {}
|
|
311
|
+
dispose(): void {
|
|
312
|
+
activeDetailTui = null;
|
|
313
|
+
}
|
|
154
314
|
}
|
|
155
315
|
|
|
156
316
|
// ── Parameter schema ────────────────────────────────────────────
|
|
@@ -189,6 +349,11 @@ const SubagentParams = Type.Object({
|
|
|
189
349
|
|
|
190
350
|
type SubagentParamsType = Static<typeof SubagentParams>;
|
|
191
351
|
|
|
352
|
+
// ── Helper: append to card messages ─────────────────────────────
|
|
353
|
+
function appendMessage(card: SubagentCard, msg: string) {
|
|
354
|
+
card.messages += (card.messages ? "\n" : "") + msg;
|
|
355
|
+
}
|
|
356
|
+
|
|
192
357
|
// ── Core execution logic ────────────────────────────────────────
|
|
193
358
|
async function executeSubagent(
|
|
194
359
|
toolCallId: string,
|
|
@@ -267,11 +432,13 @@ async function executeSubagent(
|
|
|
267
432
|
|
|
268
433
|
// Track card
|
|
269
434
|
const card: SubagentCard = {
|
|
435
|
+
num: subagentNum,
|
|
270
436
|
sessionId: session.sessionId,
|
|
271
437
|
title: params.title ?? params.task.slice(0, 30),
|
|
272
438
|
modelLabel: modelId,
|
|
273
439
|
status: "created",
|
|
274
|
-
|
|
440
|
+
prompt: params.task,
|
|
441
|
+
messages: "",
|
|
275
442
|
columnWidthPercent: params.columnWidthPercent ?? 50,
|
|
276
443
|
startedAt: Date.now(),
|
|
277
444
|
};
|
|
@@ -296,6 +463,7 @@ async function executeSubagent(
|
|
|
296
463
|
session.abort();
|
|
297
464
|
card.status = "error";
|
|
298
465
|
card.endedAt = Date.now();
|
|
466
|
+
appendMessage(card, "[aborted]");
|
|
299
467
|
updateSubagentWidget();
|
|
300
468
|
};
|
|
301
469
|
|
|
@@ -318,6 +486,7 @@ async function executeSubagent(
|
|
|
318
486
|
switch (event.type) {
|
|
319
487
|
case "agent_start":
|
|
320
488
|
card.status = "running";
|
|
489
|
+
appendMessage(card, "[agent started]");
|
|
321
490
|
updateSubagentWidget();
|
|
322
491
|
jsonlAppend(jsonlPath, baseLog);
|
|
323
492
|
lastEventId = eventId;
|
|
@@ -332,8 +501,8 @@ async function executeSubagent(
|
|
|
332
501
|
if (ame.type === "text_delta") {
|
|
333
502
|
textDeltaBuffer += ame.delta;
|
|
334
503
|
finalText += ame.delta;
|
|
335
|
-
|
|
336
|
-
|
|
504
|
+
// Append delta text to messages for live view
|
|
505
|
+
card.messages += ame.delta;
|
|
337
506
|
onUpdate?.({
|
|
338
507
|
content: [{ type: "text", text: finalText }],
|
|
339
508
|
details: {
|
|
@@ -369,8 +538,7 @@ async function executeSubagent(
|
|
|
369
538
|
}
|
|
370
539
|
|
|
371
540
|
case "tool_execution_start":
|
|
372
|
-
card
|
|
373
|
-
updateSubagentWidget();
|
|
541
|
+
appendMessage(card, `\n[🔧 ${event.toolName} ⏳]`);
|
|
374
542
|
jsonlAppend(jsonlPath, { ...baseLog, toolName: event.toolName, args: event.args });
|
|
375
543
|
lastEventId = eventId;
|
|
376
544
|
onUpdate?.({
|
|
@@ -385,8 +553,7 @@ async function executeSubagent(
|
|
|
385
553
|
break;
|
|
386
554
|
|
|
387
555
|
case "tool_execution_end":
|
|
388
|
-
card
|
|
389
|
-
updateSubagentWidget();
|
|
556
|
+
appendMessage(card, `[🔧 ${event.toolName} ${event.isError ? "❌" : "✅"}]`);
|
|
390
557
|
jsonlAppend(jsonlPath, { ...baseLog, toolName: event.toolName, isError: event.isError, result: event.result });
|
|
391
558
|
lastEventId = eventId;
|
|
392
559
|
onUpdate?.({
|
|
@@ -406,6 +573,7 @@ async function executeSubagent(
|
|
|
406
573
|
case "agent_end":
|
|
407
574
|
card.status = "completed";
|
|
408
575
|
card.endedAt = Date.now();
|
|
576
|
+
appendMessage(card, "\n[agent completed]");
|
|
409
577
|
updateSubagentWidget();
|
|
410
578
|
jsonlAppend(jsonlPath, { ...baseLog, finalTextLength: finalText.length });
|
|
411
579
|
resolve(finalText || "Subagent completed with no text output.");
|
|
@@ -444,6 +612,7 @@ async function executeSubagent(
|
|
|
444
612
|
session.prompt(params.task).catch((err) => {
|
|
445
613
|
card.status = "error";
|
|
446
614
|
card.endedAt = Date.now();
|
|
615
|
+
appendMessage(card, `[error: ${err?.message ?? String(err)}]`);
|
|
447
616
|
updateSubagentWidget();
|
|
448
617
|
reject(err);
|
|
449
618
|
});
|
|
@@ -519,11 +688,44 @@ export default function (pi: ExtensionAPI) {
|
|
|
519
688
|
description: "Clear all in-memory subagent card widgets",
|
|
520
689
|
handler: async (_args, ctx) => {
|
|
521
690
|
subagents.length = 0;
|
|
691
|
+
if (flashTimer) { clearInterval(flashTimer); flashTimer = null; }
|
|
522
692
|
ctx.ui.setWidget("in-memory-subagent-cards", undefined);
|
|
523
693
|
ctx.ui.notify("In-memory subagent widgets cleared", "info");
|
|
524
694
|
},
|
|
525
695
|
});
|
|
526
696
|
|
|
697
|
+
// Register Ctrl+1 through Ctrl+9 to open subagent detail overlay
|
|
698
|
+
for (let n = 1; n <= 9; n++) {
|
|
699
|
+
pi.registerShortcut(Key.ctrl(`${n}` as any), {
|
|
700
|
+
description: `Inspect subagent #${n}`,
|
|
701
|
+
handler: async (ctx) => {
|
|
702
|
+
const card = subagents.find((sa) => sa.num === n);
|
|
703
|
+
if (!card) {
|
|
704
|
+
ctx.ui.notify(`No subagent #${n}`, "warning");
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
await ctx.ui.custom<void>(
|
|
709
|
+
(tui: any, theme: any, _keybindings: any, done: (result: void) => void) => {
|
|
710
|
+
activeDetailTui = tui;
|
|
711
|
+
return new SubagentDetailOverlay(card, n, theme, done);
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
overlay: true,
|
|
715
|
+
overlayOptions: {
|
|
716
|
+
anchor: "center",
|
|
717
|
+
width: "80%",
|
|
718
|
+
maxHeight: "80%",
|
|
719
|
+
minWidth: 60,
|
|
720
|
+
},
|
|
721
|
+
}
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
activeDetailTui = null;
|
|
725
|
+
},
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
527
729
|
pi.registerTool<typeof SubagentParams>({
|
|
528
730
|
name: "subagent_create",
|
|
529
731
|
label: "Subagent",
|
package/extensions/tui-draw.ts
CHANGED
|
@@ -14,8 +14,10 @@ export interface CardTheme {
|
|
|
14
14
|
|
|
15
15
|
export interface RenderCardOptions {
|
|
16
16
|
title: string;
|
|
17
|
+
badge?: string;
|
|
17
18
|
content?: string;
|
|
18
19
|
footer?: string;
|
|
20
|
+
footerRight?: string;
|
|
19
21
|
colWidth: number;
|
|
20
22
|
theme: {
|
|
21
23
|
fg: (style: string, text: string) => string;
|
|
@@ -31,7 +33,7 @@ export interface RenderCardOptions {
|
|
|
31
33
|
* Each line is exactly `colWidth` visible characters wide (including borders).
|
|
32
34
|
*/
|
|
33
35
|
export function renderCard(opts: RenderCardOptions): string[] {
|
|
34
|
-
const { title, content, footer, colWidth, theme, cardTheme } = opts;
|
|
36
|
+
const { title, badge, content, footer, colWidth, theme, cardTheme } = opts;
|
|
35
37
|
const w = colWidth - 2; // inner width (minus left+right border)
|
|
36
38
|
const { bg, br } = cardTheme;
|
|
37
39
|
|
|
@@ -43,15 +45,28 @@ export function renderCard(opts: RenderCardOptions): string[] {
|
|
|
43
45
|
return bord("│") + bg + text + bg + pad + BG_RESET + bord("│");
|
|
44
46
|
};
|
|
45
47
|
|
|
48
|
+
/** Like borderLine but places a right-aligned badge before the right border */
|
|
49
|
+
const borderLineWithBadge = (text: string, badgeText: string) => {
|
|
50
|
+
const styledBadge = theme.fg("accent", theme.bold(badgeText));
|
|
51
|
+
const badgeVisLen = visibleWidth(badgeText);
|
|
52
|
+
const textVisLen = visibleWidth(text);
|
|
53
|
+
const gap = Math.max(1, w - textVisLen - badgeVisLen);
|
|
54
|
+
return bord("│") + bg + text + bg + " ".repeat(gap) + styledBadge + BG_RESET + bord("│");
|
|
55
|
+
};
|
|
56
|
+
|
|
46
57
|
const top = "┌" + "─".repeat(w) + "┐";
|
|
47
58
|
const bot = "└" + "─".repeat(w) + "┘";
|
|
48
59
|
|
|
49
60
|
const lines: string[] = [bord(top)];
|
|
50
61
|
|
|
51
|
-
// Title —
|
|
52
|
-
const truncTitle = truncateToWidth(title, w - 1);
|
|
62
|
+
// Title line — with optional badge on the right
|
|
63
|
+
const truncTitle = truncateToWidth(title, badge ? w - visibleWidth(badge) - 2 : w - 1);
|
|
53
64
|
const styledTitle = theme.fg("accent", theme.bold(truncTitle));
|
|
54
|
-
|
|
65
|
+
if (badge) {
|
|
66
|
+
lines.push(borderLineWithBadge(" " + styledTitle, badge));
|
|
67
|
+
} else {
|
|
68
|
+
lines.push(borderLine(" " + styledTitle));
|
|
69
|
+
}
|
|
55
70
|
|
|
56
71
|
// Content (defaults to "ready" muted) — supports multi-line
|
|
57
72
|
const contentText = content ?? "ready";
|
|
@@ -61,11 +76,24 @@ export function renderCard(opts: RenderCardOptions): string[] {
|
|
|
61
76
|
lines.push(borderLine(" " + styledContent));
|
|
62
77
|
}
|
|
63
78
|
|
|
64
|
-
// Optional footer —
|
|
79
|
+
// Optional footer — with optional right-aligned text
|
|
65
80
|
if (footer !== undefined) {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
const { footerRight } = opts;
|
|
82
|
+
if (footerRight) {
|
|
83
|
+
const rightVis = visibleWidth(footerRight);
|
|
84
|
+
const maxLeft = w - rightVis - 2; // 1 pad each side
|
|
85
|
+
const truncFooter = truncateToWidth(footer, maxLeft);
|
|
86
|
+
const styledLeft = theme.fg("muted", truncFooter);
|
|
87
|
+
const styledRight = theme.fg("dim", footerRight);
|
|
88
|
+
const leftVis = visibleWidth(truncFooter);
|
|
89
|
+
const gap = Math.max(1, w - 1 - leftVis - rightVis);
|
|
90
|
+
const combined = " " + styledLeft + " ".repeat(gap) + styledRight;
|
|
91
|
+
lines.push(borderLine(combined));
|
|
92
|
+
} else {
|
|
93
|
+
const truncFooter = truncateToWidth(footer, w - 1);
|
|
94
|
+
const styledFooter = theme.fg("muted", truncFooter);
|
|
95
|
+
lines.push(borderLine(" " + styledFooter));
|
|
96
|
+
}
|
|
69
97
|
}
|
|
70
98
|
|
|
71
99
|
lines.push(bord(bot));
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-subagent-in-memory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "In-process subagent tool for pi with live TUI card widgets, JSONL session logging, and zero system-prompt overhead.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
|
-
"url": "git
|
|
7
|
+
"url": "git+https://github.com/ross-jill-ws/pi-subagent-in-memory.git"
|
|
8
8
|
},
|
|
9
9
|
"author": "Ross Z",
|
|
10
10
|
"license": "MIT",
|