@pi-unipi/subagents 0.1.13 → 0.2.3
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/package.json +1 -1
- package/src/__tests__/badge-generation.test.ts +244 -0
- package/src/agent-manager.ts +12 -1
- package/src/agent-runner.ts +23 -8
- package/src/conversation-viewer.ts +299 -0
- package/src/index.ts +432 -49
- package/src/types.ts +49 -0
- package/src/widget.ts +332 -72
- package/dist/agent-manager.d.ts +0 -72
- package/dist/agent-manager.d.ts.map +0 -1
- package/dist/agent-manager.js +0 -258
- package/dist/agent-manager.js.map +0 -1
- package/dist/agent-runner.d.ts +0 -50
- package/dist/agent-runner.d.ts.map +0 -1
- package/dist/agent-runner.js +0 -238
- package/dist/agent-runner.js.map +0 -1
- package/dist/config.d.ts +0 -24
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -132
- package/dist/config.js.map +0 -1
- package/dist/custom-agents.d.ts +0 -14
- package/dist/custom-agents.d.ts.map +0 -1
- package/dist/custom-agents.js +0 -106
- package/dist/custom-agents.js.map +0 -1
- package/dist/file-lock.d.ts +0 -42
- package/dist/file-lock.d.ts.map +0 -1
- package/dist/file-lock.js +0 -91
- package/dist/file-lock.js.map +0 -1
- package/dist/index.d.ts +0 -9
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -301
- package/dist/index.js.map +0 -1
- package/dist/model-resolver.d.ts +0 -19
- package/dist/model-resolver.d.ts.map +0 -1
- package/dist/model-resolver.js +0 -61
- package/dist/model-resolver.js.map +0 -1
- package/dist/prompts.d.ts +0 -13
- package/dist/prompts.d.ts.map +0 -1
- package/dist/prompts.js +0 -31
- package/dist/prompts.js.map +0 -1
- package/dist/types.d.ts +0 -79
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -6
- package/dist/types.js.map +0 -1
- package/dist/widget.d.ts +0 -26
- package/dist/widget.d.ts.map +0 -1
- package/dist/widget.js +0 -162
- package/dist/widget.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* @pi-unipi/subagents — Extension entry
|
|
3
3
|
*
|
|
4
4
|
* Tools: spawn_helper, get_helper_result
|
|
5
|
+
* Features: renderCall/renderResult, message renderer, conversation viewer
|
|
5
6
|
* ESC propagation: all children abort on parent ESC
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { defineTool, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
9
11
|
import { Type } from "@sinclair/typebox";
|
|
10
12
|
import { existsSync, readdirSync } from "node:fs";
|
|
11
13
|
import { join } from "node:path";
|
|
@@ -13,7 +15,8 @@ import { homedir } from "node:os";
|
|
|
13
15
|
import { emitEvent, MODULES, UNIPI_EVENTS } from "@pi-unipi/core";
|
|
14
16
|
import { AgentManager } from "./agent-manager.js";
|
|
15
17
|
import { initConfig } from "./config.js";
|
|
16
|
-
import { type AgentActivity, BUILTIN_TYPES } from "./types.js";
|
|
18
|
+
import { type AgentActivity, type NotificationDetails, BUILTIN_TYPES } from "./types.js";
|
|
19
|
+
import { ConversationViewer } from "./conversation-viewer.js";
|
|
17
20
|
import { AgentWidget } from "./widget.js";
|
|
18
21
|
|
|
19
22
|
/** Get info registry from global */
|
|
@@ -22,25 +25,105 @@ function getInfoRegistry() {
|
|
|
22
25
|
return g.__unipi_info_registry;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
// ---- Formatting helpers (shared between renderers and inline text) ----
|
|
29
|
+
|
|
30
|
+
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
31
|
+
|
|
32
|
+
/** Tool name → human-readable action. */
|
|
33
|
+
const TOOL_DISPLAY: Record<string, string> = {
|
|
34
|
+
read: "reading",
|
|
35
|
+
bash: "running command",
|
|
36
|
+
edit: "editing",
|
|
37
|
+
write: "writing",
|
|
38
|
+
grep: "searching",
|
|
39
|
+
find: "finding files",
|
|
40
|
+
ls: "listing",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function formatTokens(count: number): string {
|
|
44
|
+
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M token`;
|
|
45
|
+
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k token`;
|
|
46
|
+
return `${count} token`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatTurns(turn: number, max?: number | null): string {
|
|
50
|
+
return max != null ? `⟳${turn}≤${max}` : `⟳${turn}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function formatMs(ms: number): string {
|
|
54
|
+
if (ms >= 60_000) return `${(ms / 60_000).toFixed(1)}m`;
|
|
55
|
+
if (ms >= 1_000) return `${(ms / 1_000).toFixed(1)}s`;
|
|
56
|
+
return `${ms}ms`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Build activity description from active tools. */
|
|
60
|
+
function describeActivity(activeTools: Map<string, string>, responseText?: string): string {
|
|
61
|
+
if (activeTools.size > 0) {
|
|
62
|
+
const groups = new Map<string, number>();
|
|
63
|
+
for (const toolName of activeTools.values()) {
|
|
64
|
+
const action = TOOL_DISPLAY[toolName] ?? toolName;
|
|
65
|
+
groups.set(action, (groups.get(action) ?? 0) + 1);
|
|
66
|
+
}
|
|
67
|
+
const parts: string[] = [];
|
|
68
|
+
for (const [action, count] of groups) {
|
|
69
|
+
if (count > 1) {
|
|
70
|
+
parts.push(`${action} ${count} ${action === "searching" ? "patterns" : "files"}`);
|
|
71
|
+
} else {
|
|
72
|
+
parts.push(action);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return parts.join(", ") + "…";
|
|
76
|
+
}
|
|
77
|
+
if (responseText && responseText.trim().length > 0) {
|
|
78
|
+
const line = responseText.split("\n").find((l) => l.trim())?.trim() ?? "";
|
|
79
|
+
if (line.length > 60) return line.slice(0, 60) + "…";
|
|
80
|
+
if (line.length > 0) return line;
|
|
81
|
+
}
|
|
82
|
+
return "thinking…";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Format tokens safely from session. */
|
|
26
86
|
function safeFormatTokens(session: any): string {
|
|
27
87
|
if (!session) return "";
|
|
28
88
|
try {
|
|
29
89
|
const stats = session.getSessionStats();
|
|
30
90
|
const total = stats.tokens?.total ?? 0;
|
|
31
|
-
|
|
32
|
-
if (total >= 1_000) return `${(total / 1_000).toFixed(1)}k`;
|
|
33
|
-
return `${total}`;
|
|
91
|
+
return formatTokens(total);
|
|
34
92
|
} catch {
|
|
35
93
|
return "";
|
|
36
94
|
}
|
|
37
95
|
}
|
|
38
96
|
|
|
97
|
+
/** Get raw token count from session. */
|
|
98
|
+
function safeTokenCount(session: any): number {
|
|
99
|
+
if (!session) return 0;
|
|
100
|
+
try {
|
|
101
|
+
return session.getSessionStats().tokens?.total ?? 0;
|
|
102
|
+
} catch {
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
39
107
|
/** Build result text */
|
|
40
108
|
function textResult(msg: string, details?: any) {
|
|
41
109
|
return { content: [{ type: "text" as const, text: msg }], details };
|
|
42
110
|
}
|
|
43
111
|
|
|
112
|
+
/** Escape XML for structured notifications. */
|
|
113
|
+
function escapeXml(s: string): string {
|
|
114
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Human-readable status label. */
|
|
118
|
+
function getStatusLabel(status: string, error?: string): string {
|
|
119
|
+
switch (status) {
|
|
120
|
+
case "error": return `Error: ${error ?? "unknown"}`;
|
|
121
|
+
case "aborted": return "Aborted (max turns exceeded)";
|
|
122
|
+
case "stopped": return "Stopped";
|
|
123
|
+
default: return "Done";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
44
127
|
export default function (pi: ExtensionAPI) {
|
|
45
128
|
// Initialize config
|
|
46
129
|
const config = initConfig(process.cwd());
|
|
@@ -61,6 +144,51 @@ export default function (pi: ExtensionAPI) {
|
|
|
61
144
|
agentActivity.delete(record.id);
|
|
62
145
|
widget.markFinished(record.id);
|
|
63
146
|
widget.update();
|
|
147
|
+
|
|
148
|
+
// Build notification details
|
|
149
|
+
const details = buildNotificationDetails(record, agentActivity.get(record.id));
|
|
150
|
+
|
|
151
|
+
// Send styled notification via message renderer
|
|
152
|
+
const status = getStatusLabel(record.status, record.error);
|
|
153
|
+
const durationMs = record.completedAt ? record.completedAt - record.startedAt : 0;
|
|
154
|
+
const resultPreview = record.result
|
|
155
|
+
? record.result.length > 500
|
|
156
|
+
? record.result.slice(0, 500) + "…"
|
|
157
|
+
: record.result
|
|
158
|
+
: "No output.";
|
|
159
|
+
|
|
160
|
+
const notificationXml = [
|
|
161
|
+
`<task-notification>`,
|
|
162
|
+
`<task-id>${record.id}</task-id>`,
|
|
163
|
+
`<status>${escapeXml(status)}</status>`,
|
|
164
|
+
`<summary>Agent "${escapeXml(record.description)}" ${record.status}</summary>`,
|
|
165
|
+
`<result>${escapeXml(resultPreview)}</result>`,
|
|
166
|
+
`<usage><total_tokens>${details.totalTokens}</total_tokens><tool_uses>${record.toolUses}</tool_uses><duration_ms>${durationMs}</duration_ms></usage>`,
|
|
167
|
+
`</task-notification>`,
|
|
168
|
+
].join("\n");
|
|
169
|
+
|
|
170
|
+
if (!record.resultConsumed) {
|
|
171
|
+
pi.sendMessage<NotificationDetails>(
|
|
172
|
+
{
|
|
173
|
+
customType: "subagent-notification",
|
|
174
|
+
content: notificationXml,
|
|
175
|
+
display: true,
|
|
176
|
+
details,
|
|
177
|
+
},
|
|
178
|
+
{ deliverAs: "followUp", triggerTurn: true },
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Badge generation: extract name from agent result and set directly
|
|
183
|
+
if (record.description === "Generate session name" && record.result && record.status === "completed") {
|
|
184
|
+
const name = record.result.split("\n")[0]?.trim().slice(0, 50) ?? "";
|
|
185
|
+
if (name && !name.startsWith("Error") && !name.includes("error")) {
|
|
186
|
+
try {
|
|
187
|
+
pi.setSessionName(name);
|
|
188
|
+
} catch { /* best effort */ }
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
64
192
|
pi.events.emit("subagents:completed", {
|
|
65
193
|
id: record.id,
|
|
66
194
|
type: record.type,
|
|
@@ -80,6 +208,72 @@ export default function (pi: ExtensionAPI) {
|
|
|
80
208
|
},
|
|
81
209
|
);
|
|
82
210
|
|
|
211
|
+
// Build notification details for the message renderer
|
|
212
|
+
function buildNotificationDetails(record: any, activity?: AgentActivity): NotificationDetails {
|
|
213
|
+
return {
|
|
214
|
+
id: record.id,
|
|
215
|
+
description: record.description,
|
|
216
|
+
status: record.status,
|
|
217
|
+
toolUses: record.toolUses,
|
|
218
|
+
turnCount: activity?.turnCount ?? 0,
|
|
219
|
+
maxTurns: activity?.maxTurns,
|
|
220
|
+
totalTokens: safeTokenCount(record.session),
|
|
221
|
+
durationMs: record.completedAt ? record.completedAt - record.startedAt : 0,
|
|
222
|
+
error: record.error,
|
|
223
|
+
resultPreview: record.result
|
|
224
|
+
? record.result.length > 200
|
|
225
|
+
? record.result.slice(0, 200) + "…"
|
|
226
|
+
: record.result
|
|
227
|
+
: "No output.",
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---- Register custom notification renderer ----
|
|
232
|
+
pi.registerMessageRenderer<NotificationDetails>(
|
|
233
|
+
"subagent-notification",
|
|
234
|
+
(message, { expanded }, theme) => {
|
|
235
|
+
const d = message.details;
|
|
236
|
+
if (!d) return undefined;
|
|
237
|
+
|
|
238
|
+
function renderOne(d: NotificationDetails): string {
|
|
239
|
+
const isError = d.status === "error" || d.status === "stopped" || d.status === "aborted";
|
|
240
|
+
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
241
|
+
const statusText = isError
|
|
242
|
+
? d.status
|
|
243
|
+
: d.status === "steered"
|
|
244
|
+
? "completed (steered)"
|
|
245
|
+
: "completed";
|
|
246
|
+
|
|
247
|
+
// Line 1: icon + agent description + status
|
|
248
|
+
let line = `${icon} ${theme.bold(d.description)} ${theme.fg("dim", statusText)}`;
|
|
249
|
+
|
|
250
|
+
// Line 2: stats
|
|
251
|
+
const parts: string[] = [];
|
|
252
|
+
if (d.turnCount > 0) parts.push(formatTurns(d.turnCount, d.maxTurns));
|
|
253
|
+
if (d.toolUses > 0) parts.push(`${d.toolUses} tool use${d.toolUses === 1 ? "" : "s"}`);
|
|
254
|
+
if (d.totalTokens > 0) parts.push(formatTokens(d.totalTokens));
|
|
255
|
+
if (d.durationMs > 0) parts.push(formatMs(d.durationMs));
|
|
256
|
+
if (parts.length) {
|
|
257
|
+
line += "\n " + parts.map((p) => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Line 3: result preview (collapsed) or full (expanded)
|
|
261
|
+
if (expanded) {
|
|
262
|
+
const lines = d.resultPreview.split("\n").slice(0, 30);
|
|
263
|
+
for (const l of lines) line += "\n" + theme.fg("dim", ` ${l}`);
|
|
264
|
+
} else {
|
|
265
|
+
const preview = d.resultPreview.split("\n")[0]?.slice(0, 80) ?? "";
|
|
266
|
+
line += "\n " + theme.fg("dim", `⎿ ${preview}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return line;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const all = [d, ...(d.others ?? [])];
|
|
273
|
+
return new Text(all.map(renderOne).join("\n"), 0, 0);
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
|
|
83
277
|
// Create widget
|
|
84
278
|
const widget = new AgentWidget(manager, agentActivity);
|
|
85
279
|
|
|
@@ -104,7 +298,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
104
298
|
const types = config.types || {};
|
|
105
299
|
const builtinTypes = ["explore", "work"];
|
|
106
300
|
|
|
107
|
-
// Scan for custom agent types
|
|
108
301
|
const customTypes: string[] = [];
|
|
109
302
|
for (const dir of [globalAgentsDir, workspaceAgentsDir]) {
|
|
110
303
|
try {
|
|
@@ -119,14 +312,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
119
312
|
}
|
|
120
313
|
|
|
121
314
|
const allTypes = [...new Set([...builtinTypes, ...Object.keys(types), ...customTypes])];
|
|
122
|
-
const typeList = allTypes.map(t => {
|
|
315
|
+
const typeList = allTypes.map((t) => {
|
|
123
316
|
const isEnabled = types[t]?.enabled !== false;
|
|
124
317
|
const isBuiltin = builtinTypes.includes(t);
|
|
125
318
|
const scope = customTypes.includes(t) ? "project" : "global";
|
|
126
319
|
return `${t}(${scope})${isEnabled ? "" : " [disabled]"}`;
|
|
127
320
|
}).join(", ");
|
|
128
321
|
|
|
129
|
-
const activeAgents = manager.listAgents().filter(a => a.status === "running").length;
|
|
322
|
+
const activeAgents = manager.listAgents().filter((a) => a.status === "running").length;
|
|
130
323
|
|
|
131
324
|
return {
|
|
132
325
|
maxConcurrent: { value: String(manager.getMaxConcurrent()) },
|
|
@@ -141,42 +334,76 @@ export default function (pi: ExtensionAPI) {
|
|
|
141
334
|
});
|
|
142
335
|
}
|
|
143
336
|
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
const globalConfig = `${homeDir}/.unipi/config/subagents.json`;
|
|
147
|
-
const workspaceConfig = `${cwd}/.unipi/config/subagents.json`;
|
|
148
|
-
|
|
149
|
-
ctx.ui.notify(
|
|
150
|
-
`UniPi Subagents config:\n` +
|
|
151
|
-
`• Global: ${globalConfig}\n` +
|
|
152
|
-
`• Global agents: ${globalAgentsDir}\n` +
|
|
153
|
-
`• Workspace: ${workspaceConfig}\n` +
|
|
154
|
-
`• Workspace agents: ${workspaceAgentsDir}`,
|
|
155
|
-
"info",
|
|
156
|
-
);
|
|
337
|
+
// Store session context for badge generation
|
|
338
|
+
let sessionCtx: any = null;
|
|
157
339
|
|
|
340
|
+
// Session start: emit MODULE_READY + capture context
|
|
341
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
342
|
+
sessionCtx = ctx;
|
|
158
343
|
emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
|
|
159
344
|
name: MODULES.SUBAGENTS || "subagents",
|
|
160
|
-
version: "0.
|
|
345
|
+
version: "0.2.0",
|
|
161
346
|
commands: [],
|
|
162
347
|
tools: ["spawn_helper", "get_helper_result"],
|
|
163
348
|
});
|
|
164
349
|
});
|
|
165
350
|
|
|
351
|
+
// Listen for badge generation requests — spawn background agent
|
|
352
|
+
pi.events.on(UNIPI_EVENTS.BADGE_GENERATE_REQUEST, async (event: any) => {
|
|
353
|
+
if (!sessionCtx) return;
|
|
354
|
+
|
|
355
|
+
const summary = event?.conversationSummary ?? "";
|
|
356
|
+
const prompt = summary
|
|
357
|
+
? `Generate a concise session title (MAX 5 WORDS) for this conversation:\n\n"${summary}"\n\nReply with ONLY the title. No quotes, no explanation, no punctuation.`
|
|
358
|
+
: `Generate a concise session title (MAX 5 WORDS) for the current session. Reply with ONLY the title. No quotes, no explanation, no punctuation.`;
|
|
359
|
+
|
|
360
|
+
// Try with configured model, fallback to inherit
|
|
361
|
+
let modelInput: string | undefined = undefined;
|
|
362
|
+
try {
|
|
363
|
+
const fs = await import("node:fs");
|
|
364
|
+
const path = await import("node:path");
|
|
365
|
+
const configPath = path.resolve(process.cwd(), ".unipi/config/badge.json");
|
|
366
|
+
if (fs.existsSync(configPath)) {
|
|
367
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
368
|
+
if (typeof parsed.generationModel === "string" && parsed.generationModel !== "inherit") {
|
|
369
|
+
modelInput = parsed.generationModel;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
} catch { /* ignore — inherit parent model */ }
|
|
373
|
+
let resolvedModel: any = undefined;
|
|
374
|
+
|
|
375
|
+
// Check if model is available
|
|
376
|
+
if (modelInput && sessionCtx.modelRegistry) {
|
|
377
|
+
const { resolveModel } = await import("./model-resolver.js");
|
|
378
|
+
const result = resolveModel(modelInput, sessionCtx.modelRegistry);
|
|
379
|
+
if (typeof result !== "string") {
|
|
380
|
+
resolvedModel = result;
|
|
381
|
+
}
|
|
382
|
+
// If result is a string (error), resolvedModel stays undefined → inherit parent
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
manager.spawn(pi, sessionCtx, "explore", prompt, {
|
|
386
|
+
description: "Generate session name",
|
|
387
|
+
model: resolvedModel,
|
|
388
|
+
isBackground: true,
|
|
389
|
+
maxTurns: 3,
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
166
393
|
// ESC propagation: abort all agents on session shutdown
|
|
167
394
|
pi.on("session_shutdown", async () => {
|
|
168
395
|
manager.abortAll();
|
|
169
396
|
manager.dispose();
|
|
170
397
|
});
|
|
171
398
|
|
|
172
|
-
// Wire UI context for widget
|
|
399
|
+
// Wire UI context for widget + age finished agents on new turn
|
|
173
400
|
pi.on("tool_execution_start", async (_event, ctx) => {
|
|
174
401
|
widget.setUICtx(ctx.ui);
|
|
175
|
-
widget.
|
|
402
|
+
widget.onTurnStart();
|
|
176
403
|
});
|
|
177
404
|
|
|
178
405
|
// Create activity tracker
|
|
179
|
-
function createActivityTracker(maxTurns?: number) {
|
|
406
|
+
function createActivityTracker(maxTurns?: number, onStreamUpdate?: () => void) {
|
|
180
407
|
const state: AgentActivity = {
|
|
181
408
|
activeTools: new Map(),
|
|
182
409
|
toolUses: 0,
|
|
@@ -199,20 +426,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
199
426
|
}
|
|
200
427
|
state.toolUses++;
|
|
201
428
|
}
|
|
202
|
-
|
|
429
|
+
state.tokens = safeFormatTokens(state.session);
|
|
430
|
+
onStreamUpdate?.();
|
|
203
431
|
},
|
|
204
432
|
onTextDelta: (_delta: string, fullText: string) => {
|
|
205
433
|
state.responseText = fullText;
|
|
206
|
-
|
|
434
|
+
onStreamUpdate?.();
|
|
207
435
|
},
|
|
208
436
|
onTurnEnd: (turnCount: number) => {
|
|
209
437
|
state.turnCount = turnCount;
|
|
210
|
-
|
|
438
|
+
onStreamUpdate?.();
|
|
211
439
|
},
|
|
212
440
|
onSessionCreated: (session: any) => {
|
|
213
441
|
state.session = session;
|
|
214
|
-
state.tokens = safeFormatTokens(session);
|
|
215
|
-
widget.update();
|
|
216
442
|
},
|
|
217
443
|
};
|
|
218
444
|
|
|
@@ -273,6 +499,87 @@ Guidelines:
|
|
|
273
499
|
),
|
|
274
500
|
}),
|
|
275
501
|
|
|
502
|
+
// ---- Rich inline rendering ----
|
|
503
|
+
|
|
504
|
+
renderCall(args, theme) {
|
|
505
|
+
const displayName = args.type ? args.type : "Agent";
|
|
506
|
+
const desc = args.description ?? "";
|
|
507
|
+
return new Text(
|
|
508
|
+
"▸ " + theme.fg("toolTitle", theme.bold(displayName)) + (desc ? " " + theme.fg("muted", desc) : ""),
|
|
509
|
+
0,
|
|
510
|
+
0,
|
|
511
|
+
);
|
|
512
|
+
},
|
|
513
|
+
|
|
514
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
515
|
+
const details = result.details as any;
|
|
516
|
+
if (!details) {
|
|
517
|
+
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
518
|
+
return new Text(text, 0, 0);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Stats helper
|
|
522
|
+
const stats = (d: any) => {
|
|
523
|
+
const parts: string[] = [];
|
|
524
|
+
if (d.turnCount != null && d.turnCount > 0) parts.push(formatTurns(d.turnCount, d.maxTurns));
|
|
525
|
+
if (d.toolUses > 0) parts.push(`${d.toolUses} tool use${d.toolUses === 1 ? "" : "s"}`);
|
|
526
|
+
if (d.tokens) parts.push(d.tokens);
|
|
527
|
+
return parts.map((p) => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// Running
|
|
531
|
+
if (isPartial || details.status === "running") {
|
|
532
|
+
const frame = SPINNER[details.spinnerFrame ?? 0];
|
|
533
|
+
const s = stats(details);
|
|
534
|
+
let line = theme.fg("accent", frame) + (s ? " " + s : "");
|
|
535
|
+
line += "\n" + theme.fg("dim", ` ⎿ ${details.activity ?? "thinking…"}`);
|
|
536
|
+
return new Text(line, 0, 0);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Background launched
|
|
540
|
+
if (details.status === "background") {
|
|
541
|
+
return new Text(theme.fg("dim", ` ⎿ Running in background (ID: ${details.agentId})`), 0, 0);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Completed
|
|
545
|
+
if (details.status === "completed") {
|
|
546
|
+
const duration = formatMs(details.durationMs);
|
|
547
|
+
const s = stats(details);
|
|
548
|
+
let line = theme.fg("success", "✓") + (s ? " " + s : "");
|
|
549
|
+
line += " " + theme.fg("dim", "·") + " " + theme.fg("dim", duration);
|
|
550
|
+
|
|
551
|
+
if (expanded) {
|
|
552
|
+
const resultText = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
553
|
+
if (resultText) {
|
|
554
|
+
const rlines = resultText.split("\n").slice(0, 50);
|
|
555
|
+
for (const l of rlines) {
|
|
556
|
+
line += "\n" + theme.fg("dim", ` ${l}`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
} else {
|
|
560
|
+
line += "\n" + theme.fg("dim", " ⎿ Done");
|
|
561
|
+
}
|
|
562
|
+
return new Text(line, 0, 0);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Error / Aborted / Stopped
|
|
566
|
+
const isError = details.status === "error";
|
|
567
|
+
const isStopped = details.status === "stopped";
|
|
568
|
+
const s = stats(details);
|
|
569
|
+
let line = (isStopped ? theme.fg("dim", "■") : theme.fg("error", "✗")) + (s ? " " + s : "");
|
|
570
|
+
|
|
571
|
+
if (isError) {
|
|
572
|
+
line += "\n" + theme.fg("error", ` ⎿ Error: ${details.error ?? "unknown"}`);
|
|
573
|
+
} else if (isStopped) {
|
|
574
|
+
line += "\n" + theme.fg("dim", " ⎿ Stopped");
|
|
575
|
+
} else {
|
|
576
|
+
line += "\n" + theme.fg("warning", " ⎿ Aborted (max turns exceeded)");
|
|
577
|
+
}
|
|
578
|
+
return new Text(line, 0, 0);
|
|
579
|
+
},
|
|
580
|
+
|
|
581
|
+
// ---- Execute ----
|
|
582
|
+
|
|
276
583
|
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
277
584
|
widget.setUICtx(ctx.ui);
|
|
278
585
|
|
|
@@ -284,9 +591,17 @@ Guidelines:
|
|
|
284
591
|
const modelInput = params.model as string | undefined;
|
|
285
592
|
const thinkingLevel = params.thinking as any | undefined;
|
|
286
593
|
|
|
287
|
-
const { state: bgState, callbacks: bgCallbacks } = createActivityTracker(maxTurns);
|
|
288
|
-
|
|
289
594
|
if (runInBackground) {
|
|
595
|
+
const { state: bgState, callbacks: bgCallbacks } = createActivityTracker(maxTurns);
|
|
596
|
+
|
|
597
|
+
// Wrap onSessionCreated to sync tokens
|
|
598
|
+
const origOnSession = bgCallbacks.onSessionCreated;
|
|
599
|
+
bgCallbacks.onSessionCreated = (session: any) => {
|
|
600
|
+
origOnSession(session);
|
|
601
|
+
bgState.tokens = safeFormatTokens(session);
|
|
602
|
+
widget.update();
|
|
603
|
+
};
|
|
604
|
+
|
|
290
605
|
const id = manager.spawn(pi, ctx, type, prompt, {
|
|
291
606
|
description,
|
|
292
607
|
maxTurns,
|
|
@@ -316,27 +631,42 @@ Guidelines:
|
|
|
316
631
|
);
|
|
317
632
|
}
|
|
318
633
|
|
|
319
|
-
// Foreground execution
|
|
634
|
+
// Foreground execution — stream progress via onUpdate
|
|
320
635
|
let spinnerFrame = 0;
|
|
321
636
|
const startedAt = Date.now();
|
|
637
|
+
let fgId: string | undefined;
|
|
638
|
+
|
|
639
|
+
const { state: fgState, callbacks: fgCallbacks } = createActivityTracker(maxTurns);
|
|
322
640
|
|
|
323
641
|
const streamUpdate = () => {
|
|
324
642
|
onUpdate?.({
|
|
325
|
-
content: [{ type: "text", text: `${
|
|
643
|
+
content: [{ type: "text", text: `${fgState.toolUses} tool uses...` }],
|
|
326
644
|
details: {
|
|
327
645
|
status: "running",
|
|
328
|
-
toolUses:
|
|
329
|
-
tokens:
|
|
330
|
-
turnCount:
|
|
331
|
-
maxTurns:
|
|
646
|
+
toolUses: fgState.toolUses,
|
|
647
|
+
tokens: fgState.tokens,
|
|
648
|
+
turnCount: fgState.turnCount,
|
|
649
|
+
maxTurns: fgState.maxTurns,
|
|
332
650
|
durationMs: Date.now() - startedAt,
|
|
333
|
-
activity:
|
|
334
|
-
|
|
335
|
-
: "thinking…",
|
|
336
|
-
spinnerFrame: spinnerFrame % 10,
|
|
651
|
+
activity: describeActivity(fgState.activeTools, fgState.responseText),
|
|
652
|
+
spinnerFrame: spinnerFrame % SPINNER.length,
|
|
337
653
|
},
|
|
338
654
|
});
|
|
339
|
-
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
// Wire session to register in widget
|
|
658
|
+
const origOnSession = fgCallbacks.onSessionCreated;
|
|
659
|
+
fgCallbacks.onSessionCreated = (session: any) => {
|
|
660
|
+
origOnSession(session);
|
|
661
|
+
fgState.tokens = safeFormatTokens(session);
|
|
662
|
+
for (const a of manager.listAgents()) {
|
|
663
|
+
if (a.session === session) {
|
|
664
|
+
fgId = a.id;
|
|
665
|
+
agentActivity.set(a.id, fgState);
|
|
666
|
+
widget.ensureTimer();
|
|
667
|
+
break;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
340
670
|
};
|
|
341
671
|
|
|
342
672
|
const spinnerInterval = setInterval(() => {
|
|
@@ -344,7 +674,6 @@ Guidelines:
|
|
|
344
674
|
streamUpdate();
|
|
345
675
|
}, 80);
|
|
346
676
|
|
|
347
|
-
widget.ensureTimer();
|
|
348
677
|
streamUpdate();
|
|
349
678
|
|
|
350
679
|
const record = await manager.spawnAndWait(pi, ctx, type, prompt, {
|
|
@@ -353,21 +682,42 @@ Guidelines:
|
|
|
353
682
|
modelInput,
|
|
354
683
|
modelRegistry: ctx.modelRegistry,
|
|
355
684
|
thinkingLevel,
|
|
356
|
-
...
|
|
685
|
+
...fgCallbacks,
|
|
357
686
|
});
|
|
358
687
|
|
|
359
688
|
clearInterval(spinnerInterval);
|
|
360
689
|
|
|
361
|
-
|
|
690
|
+
// Clean up foreground agent from widget
|
|
691
|
+
if (fgId) {
|
|
692
|
+
agentActivity.delete(fgId);
|
|
693
|
+
widget.markFinished(fgId);
|
|
694
|
+
widget.update();
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const tokenText = safeFormatTokens(fgState.session);
|
|
362
698
|
const durationMs = (record.completedAt ?? Date.now()) - record.startedAt;
|
|
363
699
|
|
|
364
700
|
if (record.status === "error") {
|
|
365
|
-
return textResult(`Agent failed: ${record.error}
|
|
701
|
+
return textResult(`Agent failed: ${record.error}`, {
|
|
702
|
+
status: "error",
|
|
703
|
+
toolUses: record.toolUses,
|
|
704
|
+
tokens: tokenText,
|
|
705
|
+
durationMs,
|
|
706
|
+
error: record.error,
|
|
707
|
+
});
|
|
366
708
|
}
|
|
367
709
|
|
|
368
710
|
return textResult(
|
|
369
711
|
`Agent completed in ${(durationMs / 1000).toFixed(1)}s (${record.toolUses} tool uses${tokenText ? `, ${tokenText} tokens` : ""}).\n\n` +
|
|
370
712
|
(record.result?.trim() || "No output."),
|
|
713
|
+
{
|
|
714
|
+
status: "completed",
|
|
715
|
+
toolUses: record.toolUses,
|
|
716
|
+
tokens: tokenText,
|
|
717
|
+
durationMs,
|
|
718
|
+
turnCount: fgState.turnCount,
|
|
719
|
+
maxTurns: fgState.maxTurns,
|
|
720
|
+
},
|
|
371
721
|
);
|
|
372
722
|
},
|
|
373
723
|
}),
|
|
@@ -379,7 +729,7 @@ Guidelines:
|
|
|
379
729
|
defineTool({
|
|
380
730
|
name: "get_helper_result",
|
|
381
731
|
label: "Get Helper Result",
|
|
382
|
-
description: "Check status and retrieve results from a background agent.",
|
|
732
|
+
description: "Check status and retrieve results from a background agent. Use view: true to open a live conversation overlay.",
|
|
383
733
|
parameters: Type.Object({
|
|
384
734
|
agent_id: Type.String({
|
|
385
735
|
description: "The helper ID to check.",
|
|
@@ -389,13 +739,46 @@ Guidelines:
|
|
|
389
739
|
description: "Wait for completion. Default: false.",
|
|
390
740
|
}),
|
|
391
741
|
),
|
|
742
|
+
view: Type.Optional(
|
|
743
|
+
Type.Boolean({
|
|
744
|
+
description: "Open a live conversation viewer overlay. Default: false.",
|
|
745
|
+
}),
|
|
746
|
+
),
|
|
392
747
|
}),
|
|
393
|
-
execute: async (_toolCallId, params) => {
|
|
748
|
+
execute: async (_toolCallId, params, _signal, _onUpdate, ctx) => {
|
|
394
749
|
const record = manager.getRecord(params.agent_id as string);
|
|
395
750
|
if (!record) {
|
|
396
751
|
return textResult(`Helper not found: "${params.agent_id}". It may have been cleaned up.`);
|
|
397
752
|
}
|
|
398
753
|
|
|
754
|
+
// Open conversation viewer overlay if requested
|
|
755
|
+
if (params.view && record.session) {
|
|
756
|
+
const activity = agentActivity.get(record.id);
|
|
757
|
+
await ctx.ui.custom<undefined>(
|
|
758
|
+
(tui, theme, _keybindings, done) => {
|
|
759
|
+
return new ConversationViewer(
|
|
760
|
+
tui,
|
|
761
|
+
record.session!,
|
|
762
|
+
{
|
|
763
|
+
type: record.type,
|
|
764
|
+
description: record.description,
|
|
765
|
+
status: record.status,
|
|
766
|
+
toolUses: record.toolUses,
|
|
767
|
+
startedAt: record.startedAt,
|
|
768
|
+
completedAt: record.completedAt,
|
|
769
|
+
},
|
|
770
|
+
activity,
|
|
771
|
+
theme,
|
|
772
|
+
done,
|
|
773
|
+
);
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
overlay: true,
|
|
777
|
+
overlayOptions: { anchor: "center", width: "90%" },
|
|
778
|
+
},
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
|
|
399
782
|
if (params.wait && record.status === "running" && record.promise) {
|
|
400
783
|
record.resultConsumed = true;
|
|
401
784
|
await record.promise;
|