@tintinweb/pi-subagents 0.4.0 → 0.4.1
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/CHANGELOG.md +10 -0
- package/dist/agent-manager.d.ts +70 -0
- package/dist/agent-manager.js +236 -0
- package/dist/agent-runner.d.ts +60 -0
- package/dist/agent-runner.js +265 -0
- package/dist/agent-types.d.ts +41 -0
- package/dist/agent-types.js +130 -0
- package/dist/context.d.ts +12 -0
- package/dist/context.js +56 -0
- package/dist/custom-agents.d.ts +14 -0
- package/dist/custom-agents.js +100 -0
- package/dist/default-agents.d.ts +7 -0
- package/dist/default-agents.js +126 -0
- package/dist/env.d.ts +6 -0
- package/dist/env.js +28 -0
- package/dist/group-join.d.ts +32 -0
- package/dist/group-join.js +116 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1270 -0
- package/dist/model-resolver.d.ts +19 -0
- package/dist/model-resolver.js +62 -0
- package/dist/prompts.d.ts +14 -0
- package/dist/prompts.js +48 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.js +5 -0
- package/dist/ui/agent-widget.d.ts +101 -0
- package/dist/ui/agent-widget.js +333 -0
- package/dist/ui/conversation-viewer.d.ts +31 -0
- package/dist/ui/conversation-viewer.js +236 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +22 -0
- package/src/index.ts +15 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* conversation-viewer.ts — Live conversation overlay for viewing agent sessions.
|
|
3
|
+
*
|
|
4
|
+
* Displays a scrollable, live-updating view of an agent's conversation.
|
|
5
|
+
* Subscribes to session events for real-time streaming updates.
|
|
6
|
+
*/
|
|
7
|
+
import { type Component, type TUI } from "@mariozechner/pi-tui";
|
|
8
|
+
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import type { Theme } from "./agent-widget.js";
|
|
10
|
+
import { type AgentActivity } from "./agent-widget.js";
|
|
11
|
+
import type { AgentRecord } from "../types.js";
|
|
12
|
+
export declare class ConversationViewer implements Component {
|
|
13
|
+
private tui;
|
|
14
|
+
private session;
|
|
15
|
+
private record;
|
|
16
|
+
private activity;
|
|
17
|
+
private theme;
|
|
18
|
+
private done;
|
|
19
|
+
private scrollOffset;
|
|
20
|
+
private autoScroll;
|
|
21
|
+
private unsubscribe;
|
|
22
|
+
private lastInnerW;
|
|
23
|
+
private closed;
|
|
24
|
+
constructor(tui: TUI, session: AgentSession, record: AgentRecord, activity: AgentActivity | undefined, theme: Theme, done: (result: undefined) => void);
|
|
25
|
+
handleInput(data: string): void;
|
|
26
|
+
render(width: number): string[];
|
|
27
|
+
invalidate(): void;
|
|
28
|
+
dispose(): void;
|
|
29
|
+
private viewportHeight;
|
|
30
|
+
private buildContentLines;
|
|
31
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* conversation-viewer.ts — Live conversation overlay for viewing agent sessions.
|
|
3
|
+
*
|
|
4
|
+
* Displays a scrollable, live-updating view of an agent's conversation.
|
|
5
|
+
* Subscribes to session events for real-time streaming updates.
|
|
6
|
+
*/
|
|
7
|
+
import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
8
|
+
import { formatTokens, formatDuration, getDisplayName, getPromptModeLabel, describeActivity } from "./agent-widget.js";
|
|
9
|
+
import { extractText } from "../context.js";
|
|
10
|
+
/** Lines consumed by chrome: top border + header + header sep + footer sep + footer + bottom border. */
|
|
11
|
+
const CHROME_LINES = 6;
|
|
12
|
+
const MIN_VIEWPORT = 3;
|
|
13
|
+
export class ConversationViewer {
|
|
14
|
+
tui;
|
|
15
|
+
session;
|
|
16
|
+
record;
|
|
17
|
+
activity;
|
|
18
|
+
theme;
|
|
19
|
+
done;
|
|
20
|
+
scrollOffset = 0;
|
|
21
|
+
autoScroll = true;
|
|
22
|
+
unsubscribe;
|
|
23
|
+
lastInnerW = 0;
|
|
24
|
+
closed = false;
|
|
25
|
+
constructor(tui, session, record, activity, theme, done) {
|
|
26
|
+
this.tui = tui;
|
|
27
|
+
this.session = session;
|
|
28
|
+
this.record = record;
|
|
29
|
+
this.activity = activity;
|
|
30
|
+
this.theme = theme;
|
|
31
|
+
this.done = done;
|
|
32
|
+
this.unsubscribe = session.subscribe(() => {
|
|
33
|
+
if (this.closed)
|
|
34
|
+
return;
|
|
35
|
+
this.tui.requestRender();
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
handleInput(data) {
|
|
39
|
+
if (matchesKey(data, "escape") || matchesKey(data, "q")) {
|
|
40
|
+
this.closed = true;
|
|
41
|
+
this.done(undefined);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const totalLines = this.buildContentLines(this.lastInnerW).length;
|
|
45
|
+
const viewportHeight = this.viewportHeight();
|
|
46
|
+
const maxScroll = Math.max(0, totalLines - viewportHeight);
|
|
47
|
+
if (matchesKey(data, "up") || matchesKey(data, "k")) {
|
|
48
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - 1);
|
|
49
|
+
this.autoScroll = this.scrollOffset >= maxScroll;
|
|
50
|
+
}
|
|
51
|
+
else if (matchesKey(data, "down") || matchesKey(data, "j")) {
|
|
52
|
+
this.scrollOffset = Math.min(maxScroll, this.scrollOffset + 1);
|
|
53
|
+
this.autoScroll = this.scrollOffset >= maxScroll;
|
|
54
|
+
}
|
|
55
|
+
else if (matchesKey(data, "pageUp")) {
|
|
56
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - viewportHeight);
|
|
57
|
+
this.autoScroll = false;
|
|
58
|
+
}
|
|
59
|
+
else if (matchesKey(data, "pageDown")) {
|
|
60
|
+
this.scrollOffset = Math.min(maxScroll, this.scrollOffset + viewportHeight);
|
|
61
|
+
this.autoScroll = this.scrollOffset >= maxScroll;
|
|
62
|
+
}
|
|
63
|
+
else if (matchesKey(data, "home")) {
|
|
64
|
+
this.scrollOffset = 0;
|
|
65
|
+
this.autoScroll = false;
|
|
66
|
+
}
|
|
67
|
+
else if (matchesKey(data, "end")) {
|
|
68
|
+
this.scrollOffset = maxScroll;
|
|
69
|
+
this.autoScroll = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
render(width) {
|
|
73
|
+
if (width < 6)
|
|
74
|
+
return []; // too narrow for any meaningful rendering
|
|
75
|
+
const th = this.theme;
|
|
76
|
+
const innerW = width - 4; // border + padding
|
|
77
|
+
this.lastInnerW = innerW;
|
|
78
|
+
const lines = [];
|
|
79
|
+
const pad = (s, len) => {
|
|
80
|
+
const vis = visibleWidth(s);
|
|
81
|
+
return s + " ".repeat(Math.max(0, len - vis));
|
|
82
|
+
};
|
|
83
|
+
const row = (content) => th.fg("border", "│") + " " + truncateToWidth(pad(content, innerW), innerW) + " " + th.fg("border", "│");
|
|
84
|
+
const hrTop = th.fg("border", `╭${"─".repeat(width - 2)}╮`);
|
|
85
|
+
const hrBot = th.fg("border", `╰${"─".repeat(width - 2)}╯`);
|
|
86
|
+
const hrMid = row(th.fg("dim", "─".repeat(innerW)));
|
|
87
|
+
// Header
|
|
88
|
+
lines.push(hrTop);
|
|
89
|
+
const name = getDisplayName(this.record.type);
|
|
90
|
+
const modeLabel = getPromptModeLabel(this.record.type);
|
|
91
|
+
const modeTag = modeLabel ? ` ${th.fg("dim", `(${modeLabel})`)}` : "";
|
|
92
|
+
const statusIcon = this.record.status === "running"
|
|
93
|
+
? th.fg("accent", "●")
|
|
94
|
+
: this.record.status === "completed"
|
|
95
|
+
? th.fg("success", "✓")
|
|
96
|
+
: this.record.status === "error"
|
|
97
|
+
? th.fg("error", "✗")
|
|
98
|
+
: th.fg("dim", "○");
|
|
99
|
+
const duration = formatDuration(this.record.startedAt, this.record.completedAt);
|
|
100
|
+
const headerParts = [duration];
|
|
101
|
+
const toolUses = this.activity?.toolUses ?? this.record.toolUses;
|
|
102
|
+
if (toolUses > 0)
|
|
103
|
+
headerParts.unshift(`${toolUses} tool${toolUses === 1 ? "" : "s"}`);
|
|
104
|
+
if (this.activity?.session) {
|
|
105
|
+
try {
|
|
106
|
+
const tokens = this.activity.session.getSessionStats().tokens.total;
|
|
107
|
+
if (tokens > 0)
|
|
108
|
+
headerParts.push(formatTokens(tokens));
|
|
109
|
+
}
|
|
110
|
+
catch { /* */ }
|
|
111
|
+
}
|
|
112
|
+
lines.push(row(`${statusIcon} ${th.bold(name)}${modeTag} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${th.fg("dim", headerParts.join(" · "))}`));
|
|
113
|
+
lines.push(hrMid);
|
|
114
|
+
// Content area — rebuild every render (live data, no cache needed)
|
|
115
|
+
const contentLines = this.buildContentLines(innerW);
|
|
116
|
+
const viewportHeight = this.viewportHeight();
|
|
117
|
+
const maxScroll = Math.max(0, contentLines.length - viewportHeight);
|
|
118
|
+
if (this.autoScroll) {
|
|
119
|
+
this.scrollOffset = maxScroll;
|
|
120
|
+
}
|
|
121
|
+
const visibleStart = Math.min(this.scrollOffset, maxScroll);
|
|
122
|
+
const visible = contentLines.slice(visibleStart, visibleStart + viewportHeight);
|
|
123
|
+
for (let i = 0; i < viewportHeight; i++) {
|
|
124
|
+
lines.push(row(visible[i] ?? ""));
|
|
125
|
+
}
|
|
126
|
+
// Footer
|
|
127
|
+
lines.push(hrMid);
|
|
128
|
+
const scrollPct = contentLines.length <= viewportHeight
|
|
129
|
+
? "100%"
|
|
130
|
+
: `${Math.round(((visibleStart + viewportHeight) / contentLines.length) * 100)}%`;
|
|
131
|
+
const footerLeft = th.fg("dim", `${contentLines.length} lines · ${scrollPct}`);
|
|
132
|
+
const footerRight = th.fg("dim", "↑↓ scroll · PgUp/PgDn · Esc close");
|
|
133
|
+
const footerGap = Math.max(1, innerW - visibleWidth(footerLeft) - visibleWidth(footerRight));
|
|
134
|
+
lines.push(row(footerLeft + " ".repeat(footerGap) + footerRight));
|
|
135
|
+
lines.push(hrBot);
|
|
136
|
+
return lines;
|
|
137
|
+
}
|
|
138
|
+
invalidate() { }
|
|
139
|
+
dispose() {
|
|
140
|
+
this.closed = true;
|
|
141
|
+
if (this.unsubscribe) {
|
|
142
|
+
this.unsubscribe();
|
|
143
|
+
this.unsubscribe = undefined;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// ---- Private ----
|
|
147
|
+
viewportHeight() {
|
|
148
|
+
return Math.max(MIN_VIEWPORT, this.tui.terminal.rows - CHROME_LINES);
|
|
149
|
+
}
|
|
150
|
+
buildContentLines(width) {
|
|
151
|
+
if (width <= 0)
|
|
152
|
+
return [];
|
|
153
|
+
const th = this.theme;
|
|
154
|
+
const messages = this.session.messages;
|
|
155
|
+
const lines = [];
|
|
156
|
+
if (messages.length === 0) {
|
|
157
|
+
lines.push(th.fg("dim", "(waiting for first message...)"));
|
|
158
|
+
return lines;
|
|
159
|
+
}
|
|
160
|
+
let needsSeparator = false;
|
|
161
|
+
for (const msg of messages) {
|
|
162
|
+
if (msg.role === "user") {
|
|
163
|
+
const text = typeof msg.content === "string"
|
|
164
|
+
? msg.content
|
|
165
|
+
: extractText(msg.content);
|
|
166
|
+
if (!text.trim())
|
|
167
|
+
continue;
|
|
168
|
+
if (needsSeparator)
|
|
169
|
+
lines.push(th.fg("dim", "───"));
|
|
170
|
+
lines.push(th.fg("accent", "[User]"));
|
|
171
|
+
for (const line of wrapTextWithAnsi(text.trim(), width)) {
|
|
172
|
+
lines.push(line);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else if (msg.role === "assistant") {
|
|
176
|
+
const textParts = [];
|
|
177
|
+
const toolCalls = [];
|
|
178
|
+
for (const c of msg.content) {
|
|
179
|
+
if (c.type === "text" && c.text)
|
|
180
|
+
textParts.push(c.text);
|
|
181
|
+
else if (c.type === "toolCall") {
|
|
182
|
+
toolCalls.push(c.toolName ?? "unknown");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (needsSeparator)
|
|
186
|
+
lines.push(th.fg("dim", "───"));
|
|
187
|
+
lines.push(th.bold("[Assistant]"));
|
|
188
|
+
if (textParts.length > 0) {
|
|
189
|
+
for (const line of wrapTextWithAnsi(textParts.join("\n").trim(), width)) {
|
|
190
|
+
lines.push(line);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
for (const name of toolCalls) {
|
|
194
|
+
lines.push(truncateToWidth(th.fg("muted", ` [Tool: ${name}]`), width));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else if (msg.role === "toolResult") {
|
|
198
|
+
const text = extractText(msg.content);
|
|
199
|
+
const truncated = text.length > 500 ? text.slice(0, 500) + "... (truncated)" : text;
|
|
200
|
+
if (!truncated.trim())
|
|
201
|
+
continue;
|
|
202
|
+
if (needsSeparator)
|
|
203
|
+
lines.push(th.fg("dim", "───"));
|
|
204
|
+
lines.push(th.fg("dim", "[Result]"));
|
|
205
|
+
for (const line of wrapTextWithAnsi(truncated.trim(), width)) {
|
|
206
|
+
lines.push(th.fg("dim", line));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else if (msg.role === "bashExecution") {
|
|
210
|
+
const bash = msg;
|
|
211
|
+
if (needsSeparator)
|
|
212
|
+
lines.push(th.fg("dim", "───"));
|
|
213
|
+
lines.push(truncateToWidth(th.fg("muted", ` $ ${bash.command}`), width));
|
|
214
|
+
if (bash.output?.trim()) {
|
|
215
|
+
const out = bash.output.length > 500
|
|
216
|
+
? bash.output.slice(0, 500) + "... (truncated)"
|
|
217
|
+
: bash.output;
|
|
218
|
+
for (const line of wrapTextWithAnsi(out.trim(), width)) {
|
|
219
|
+
lines.push(th.fg("dim", line));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
needsSeparator = true;
|
|
227
|
+
}
|
|
228
|
+
// Streaming indicator for running agents
|
|
229
|
+
if (this.record.status === "running" && this.activity) {
|
|
230
|
+
const act = describeActivity(this.activity.activeTools, this.activity.responseText);
|
|
231
|
+
lines.push("");
|
|
232
|
+
lines.push(truncateToWidth(th.fg("accent", "▍ ") + th.fg("dim", act), width));
|
|
233
|
+
}
|
|
234
|
+
return lines;
|
|
235
|
+
}
|
|
236
|
+
}
|
package/package.json
CHANGED
package/src/agent-manager.ts
CHANGED
|
@@ -271,6 +271,28 @@ export class AgentManager {
|
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
/** Whether any agents are still running or queued. */
|
|
275
|
+
hasRunning(): boolean {
|
|
276
|
+
return [...this.agents.values()].some(
|
|
277
|
+
r => r.status === "running" || r.status === "queued",
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Wait for all running and queued agents to complete (including queued ones). */
|
|
282
|
+
async waitForAll(): Promise<void> {
|
|
283
|
+
// Loop because drainQueue respects the concurrency limit — as running
|
|
284
|
+
// agents finish they start queued ones, which need awaiting too.
|
|
285
|
+
while (true) {
|
|
286
|
+
this.drainQueue();
|
|
287
|
+
const pending = [...this.agents.values()]
|
|
288
|
+
.filter(r => r.status === "running" || r.status === "queued")
|
|
289
|
+
.map(r => r.promise)
|
|
290
|
+
.filter(Boolean);
|
|
291
|
+
if (pending.length === 0) break;
|
|
292
|
+
await Promise.allSettled(pending);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
274
296
|
dispose() {
|
|
275
297
|
clearInterval(this.cleanupInterval);
|
|
276
298
|
// Clear queue
|
package/src/index.ts
CHANGED
|
@@ -230,6 +230,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
230
230
|
widget.update();
|
|
231
231
|
});
|
|
232
232
|
|
|
233
|
+
// Expose manager via Symbol.for() global registry for cross-package access.
|
|
234
|
+
// Standard Node.js pattern for cross-package singletons (used by OpenTelemetry, etc.).
|
|
235
|
+
const MANAGER_KEY = Symbol.for("pi-subagents:manager");
|
|
236
|
+
(globalThis as any)[MANAGER_KEY] = {
|
|
237
|
+
waitForAll: () => manager.waitForAll(),
|
|
238
|
+
hasRunning: () => manager.hasRunning(),
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// Wait for all subagents on shutdown, then dispose the manager
|
|
242
|
+
pi.on("session_shutdown", async () => {
|
|
243
|
+
delete (globalThis as any)[MANAGER_KEY];
|
|
244
|
+
await manager.waitForAll();
|
|
245
|
+
manager.dispose();
|
|
246
|
+
});
|
|
247
|
+
|
|
233
248
|
// Live widget: show running agents above editor
|
|
234
249
|
const widget = new AgentWidget(manager, agentActivity);
|
|
235
250
|
|