@pi-unipi/subagents 0.1.13 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/config.test.d.ts +11 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +196 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/esc-propagation.test.d.ts +10 -0
- package/dist/__tests__/esc-propagation.test.d.ts.map +1 -0
- package/dist/__tests__/esc-propagation.test.js +140 -0
- package/dist/__tests__/esc-propagation.test.js.map +1 -0
- package/dist/__tests__/file-lock.test.d.ts +12 -0
- package/dist/__tests__/file-lock.test.d.ts.map +1 -0
- package/dist/__tests__/file-lock.test.js +187 -0
- package/dist/__tests__/file-lock.test.js.map +1 -0
- package/dist/__tests__/workflow-integration.test.d.ts +12 -0
- package/dist/__tests__/workflow-integration.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-integration.test.js +261 -0
- package/dist/__tests__/workflow-integration.test.js.map +1 -0
- package/dist/agent-manager.d.ts +4 -1
- package/dist/agent-manager.d.ts.map +1 -1
- package/dist/agent-manager.js +10 -0
- package/dist/agent-manager.js.map +1 -1
- package/dist/agent-runner.d.ts +2 -1
- package/dist/agent-runner.d.ts.map +1 -1
- package/dist/agent-runner.js +23 -7
- package/dist/agent-runner.js.map +1 -1
- package/dist/conversation-viewer.d.ts +40 -0
- package/dist/conversation-viewer.d.ts.map +1 -0
- package/dist/conversation-viewer.js +276 -0
- package/dist/conversation-viewer.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +410 -58
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +30 -0
- package/dist/types.js.map +1 -1
- package/dist/widget.d.ts +32 -3
- package/dist/widget.d.ts.map +1 -1
- package/dist/widget.js +298 -56
- package/dist/widget.js.map +1 -1
- package/package.json +1 -1
- 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 +411 -49
- package/src/types.ts +49 -0
- package/src/widget.ts +332 -72
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,41 @@ 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
|
+
|
|
64
182
|
pi.events.emit("subagents:completed", {
|
|
65
183
|
id: record.id,
|
|
66
184
|
type: record.type,
|
|
@@ -80,6 +198,72 @@ export default function (pi: ExtensionAPI) {
|
|
|
80
198
|
},
|
|
81
199
|
);
|
|
82
200
|
|
|
201
|
+
// Build notification details for the message renderer
|
|
202
|
+
function buildNotificationDetails(record: any, activity?: AgentActivity): NotificationDetails {
|
|
203
|
+
return {
|
|
204
|
+
id: record.id,
|
|
205
|
+
description: record.description,
|
|
206
|
+
status: record.status,
|
|
207
|
+
toolUses: record.toolUses,
|
|
208
|
+
turnCount: activity?.turnCount ?? 0,
|
|
209
|
+
maxTurns: activity?.maxTurns,
|
|
210
|
+
totalTokens: safeTokenCount(record.session),
|
|
211
|
+
durationMs: record.completedAt ? record.completedAt - record.startedAt : 0,
|
|
212
|
+
error: record.error,
|
|
213
|
+
resultPreview: record.result
|
|
214
|
+
? record.result.length > 200
|
|
215
|
+
? record.result.slice(0, 200) + "…"
|
|
216
|
+
: record.result
|
|
217
|
+
: "No output.",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---- Register custom notification renderer ----
|
|
222
|
+
pi.registerMessageRenderer<NotificationDetails>(
|
|
223
|
+
"subagent-notification",
|
|
224
|
+
(message, { expanded }, theme) => {
|
|
225
|
+
const d = message.details;
|
|
226
|
+
if (!d) return undefined;
|
|
227
|
+
|
|
228
|
+
function renderOne(d: NotificationDetails): string {
|
|
229
|
+
const isError = d.status === "error" || d.status === "stopped" || d.status === "aborted";
|
|
230
|
+
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
231
|
+
const statusText = isError
|
|
232
|
+
? d.status
|
|
233
|
+
: d.status === "steered"
|
|
234
|
+
? "completed (steered)"
|
|
235
|
+
: "completed";
|
|
236
|
+
|
|
237
|
+
// Line 1: icon + agent description + status
|
|
238
|
+
let line = `${icon} ${theme.bold(d.description)} ${theme.fg("dim", statusText)}`;
|
|
239
|
+
|
|
240
|
+
// Line 2: stats
|
|
241
|
+
const parts: string[] = [];
|
|
242
|
+
if (d.turnCount > 0) parts.push(formatTurns(d.turnCount, d.maxTurns));
|
|
243
|
+
if (d.toolUses > 0) parts.push(`${d.toolUses} tool use${d.toolUses === 1 ? "" : "s"}`);
|
|
244
|
+
if (d.totalTokens > 0) parts.push(formatTokens(d.totalTokens));
|
|
245
|
+
if (d.durationMs > 0) parts.push(formatMs(d.durationMs));
|
|
246
|
+
if (parts.length) {
|
|
247
|
+
line += "\n " + parts.map((p) => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Line 3: result preview (collapsed) or full (expanded)
|
|
251
|
+
if (expanded) {
|
|
252
|
+
const lines = d.resultPreview.split("\n").slice(0, 30);
|
|
253
|
+
for (const l of lines) line += "\n" + theme.fg("dim", ` ${l}`);
|
|
254
|
+
} else {
|
|
255
|
+
const preview = d.resultPreview.split("\n")[0]?.slice(0, 80) ?? "";
|
|
256
|
+
line += "\n " + theme.fg("dim", `⎿ ${preview}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return line;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const all = [d, ...(d.others ?? [])];
|
|
263
|
+
return new Text(all.map(renderOne).join("\n"), 0, 0);
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
|
|
83
267
|
// Create widget
|
|
84
268
|
const widget = new AgentWidget(manager, agentActivity);
|
|
85
269
|
|
|
@@ -104,7 +288,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
104
288
|
const types = config.types || {};
|
|
105
289
|
const builtinTypes = ["explore", "work"];
|
|
106
290
|
|
|
107
|
-
// Scan for custom agent types
|
|
108
291
|
const customTypes: string[] = [];
|
|
109
292
|
for (const dir of [globalAgentsDir, workspaceAgentsDir]) {
|
|
110
293
|
try {
|
|
@@ -119,14 +302,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
119
302
|
}
|
|
120
303
|
|
|
121
304
|
const allTypes = [...new Set([...builtinTypes, ...Object.keys(types), ...customTypes])];
|
|
122
|
-
const typeList = allTypes.map(t => {
|
|
305
|
+
const typeList = allTypes.map((t) => {
|
|
123
306
|
const isEnabled = types[t]?.enabled !== false;
|
|
124
307
|
const isBuiltin = builtinTypes.includes(t);
|
|
125
308
|
const scope = customTypes.includes(t) ? "project" : "global";
|
|
126
309
|
return `${t}(${scope})${isEnabled ? "" : " [disabled]"}`;
|
|
127
310
|
}).join(", ");
|
|
128
311
|
|
|
129
|
-
const activeAgents = manager.listAgents().filter(a => a.status === "running").length;
|
|
312
|
+
const activeAgents = manager.listAgents().filter((a) => a.status === "running").length;
|
|
130
313
|
|
|
131
314
|
return {
|
|
132
315
|
maxConcurrent: { value: String(manager.getMaxConcurrent()) },
|
|
@@ -141,42 +324,65 @@ export default function (pi: ExtensionAPI) {
|
|
|
141
324
|
});
|
|
142
325
|
}
|
|
143
326
|
|
|
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
|
-
);
|
|
327
|
+
// Store session context for badge generation
|
|
328
|
+
let sessionCtx: any = null;
|
|
157
329
|
|
|
330
|
+
// Session start: emit MODULE_READY + capture context
|
|
331
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
332
|
+
sessionCtx = ctx;
|
|
158
333
|
emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
|
|
159
334
|
name: MODULES.SUBAGENTS || "subagents",
|
|
160
|
-
version: "0.
|
|
335
|
+
version: "0.2.0",
|
|
161
336
|
commands: [],
|
|
162
337
|
tools: ["spawn_helper", "get_helper_result"],
|
|
163
338
|
});
|
|
164
339
|
});
|
|
165
340
|
|
|
341
|
+
// Listen for badge generation requests — spawn background agent
|
|
342
|
+
pi.on(UNIPI_EVENTS.BADGE_GENERATE_REQUEST as any, async (event: any) => {
|
|
343
|
+
if (!sessionCtx) return;
|
|
344
|
+
|
|
345
|
+
const summary = event?.conversationSummary ?? "";
|
|
346
|
+
const prompt = summary
|
|
347
|
+
? `Generate a concise session title (MAX 5 WORDS) for this conversation:\n\n"${summary}"\n\nCall the set_session_name tool with the name. Do not explain.`
|
|
348
|
+
: `Generate a concise session title (MAX 5 WORDS) for the current session. Call the set_session_name tool. Do not explain.`;
|
|
349
|
+
|
|
350
|
+
// Try with openai/gpt-oss-20b, fallback to inherit
|
|
351
|
+
const modelInput = "openai/gpt-oss-20b";
|
|
352
|
+
let resolvedModel: any = undefined;
|
|
353
|
+
|
|
354
|
+
// Check if model is available
|
|
355
|
+
if (sessionCtx.modelRegistry) {
|
|
356
|
+
const { resolveModel } = await import("./model-resolver.js");
|
|
357
|
+
const result = resolveModel(modelInput, sessionCtx.modelRegistry);
|
|
358
|
+
if (typeof result !== "string") {
|
|
359
|
+
resolvedModel = result;
|
|
360
|
+
}
|
|
361
|
+
// If result is a string (error), resolvedModel stays undefined → inherit parent
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
manager.spawn(pi, sessionCtx, "explore", prompt, {
|
|
365
|
+
description: "Generate session name",
|
|
366
|
+
model: resolvedModel,
|
|
367
|
+
isBackground: true,
|
|
368
|
+
maxTurns: 3,
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
166
372
|
// ESC propagation: abort all agents on session shutdown
|
|
167
373
|
pi.on("session_shutdown", async () => {
|
|
168
374
|
manager.abortAll();
|
|
169
375
|
manager.dispose();
|
|
170
376
|
});
|
|
171
377
|
|
|
172
|
-
// Wire UI context for widget
|
|
378
|
+
// Wire UI context for widget + age finished agents on new turn
|
|
173
379
|
pi.on("tool_execution_start", async (_event, ctx) => {
|
|
174
380
|
widget.setUICtx(ctx.ui);
|
|
175
|
-
widget.
|
|
381
|
+
widget.onTurnStart();
|
|
176
382
|
});
|
|
177
383
|
|
|
178
384
|
// Create activity tracker
|
|
179
|
-
function createActivityTracker(maxTurns?: number) {
|
|
385
|
+
function createActivityTracker(maxTurns?: number, onStreamUpdate?: () => void) {
|
|
180
386
|
const state: AgentActivity = {
|
|
181
387
|
activeTools: new Map(),
|
|
182
388
|
toolUses: 0,
|
|
@@ -199,20 +405,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
199
405
|
}
|
|
200
406
|
state.toolUses++;
|
|
201
407
|
}
|
|
202
|
-
|
|
408
|
+
state.tokens = safeFormatTokens(state.session);
|
|
409
|
+
onStreamUpdate?.();
|
|
203
410
|
},
|
|
204
411
|
onTextDelta: (_delta: string, fullText: string) => {
|
|
205
412
|
state.responseText = fullText;
|
|
206
|
-
|
|
413
|
+
onStreamUpdate?.();
|
|
207
414
|
},
|
|
208
415
|
onTurnEnd: (turnCount: number) => {
|
|
209
416
|
state.turnCount = turnCount;
|
|
210
|
-
|
|
417
|
+
onStreamUpdate?.();
|
|
211
418
|
},
|
|
212
419
|
onSessionCreated: (session: any) => {
|
|
213
420
|
state.session = session;
|
|
214
|
-
state.tokens = safeFormatTokens(session);
|
|
215
|
-
widget.update();
|
|
216
421
|
},
|
|
217
422
|
};
|
|
218
423
|
|
|
@@ -273,6 +478,87 @@ Guidelines:
|
|
|
273
478
|
),
|
|
274
479
|
}),
|
|
275
480
|
|
|
481
|
+
// ---- Rich inline rendering ----
|
|
482
|
+
|
|
483
|
+
renderCall(args, theme) {
|
|
484
|
+
const displayName = args.type ? args.type : "Agent";
|
|
485
|
+
const desc = args.description ?? "";
|
|
486
|
+
return new Text(
|
|
487
|
+
"▸ " + theme.fg("toolTitle", theme.bold(displayName)) + (desc ? " " + theme.fg("muted", desc) : ""),
|
|
488
|
+
0,
|
|
489
|
+
0,
|
|
490
|
+
);
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
494
|
+
const details = result.details as any;
|
|
495
|
+
if (!details) {
|
|
496
|
+
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
497
|
+
return new Text(text, 0, 0);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Stats helper
|
|
501
|
+
const stats = (d: any) => {
|
|
502
|
+
const parts: string[] = [];
|
|
503
|
+
if (d.turnCount != null && d.turnCount > 0) parts.push(formatTurns(d.turnCount, d.maxTurns));
|
|
504
|
+
if (d.toolUses > 0) parts.push(`${d.toolUses} tool use${d.toolUses === 1 ? "" : "s"}`);
|
|
505
|
+
if (d.tokens) parts.push(d.tokens);
|
|
506
|
+
return parts.map((p) => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// Running
|
|
510
|
+
if (isPartial || details.status === "running") {
|
|
511
|
+
const frame = SPINNER[details.spinnerFrame ?? 0];
|
|
512
|
+
const s = stats(details);
|
|
513
|
+
let line = theme.fg("accent", frame) + (s ? " " + s : "");
|
|
514
|
+
line += "\n" + theme.fg("dim", ` ⎿ ${details.activity ?? "thinking…"}`);
|
|
515
|
+
return new Text(line, 0, 0);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Background launched
|
|
519
|
+
if (details.status === "background") {
|
|
520
|
+
return new Text(theme.fg("dim", ` ⎿ Running in background (ID: ${details.agentId})`), 0, 0);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Completed
|
|
524
|
+
if (details.status === "completed") {
|
|
525
|
+
const duration = formatMs(details.durationMs);
|
|
526
|
+
const s = stats(details);
|
|
527
|
+
let line = theme.fg("success", "✓") + (s ? " " + s : "");
|
|
528
|
+
line += " " + theme.fg("dim", "·") + " " + theme.fg("dim", duration);
|
|
529
|
+
|
|
530
|
+
if (expanded) {
|
|
531
|
+
const resultText = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
532
|
+
if (resultText) {
|
|
533
|
+
const rlines = resultText.split("\n").slice(0, 50);
|
|
534
|
+
for (const l of rlines) {
|
|
535
|
+
line += "\n" + theme.fg("dim", ` ${l}`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
} else {
|
|
539
|
+
line += "\n" + theme.fg("dim", " ⎿ Done");
|
|
540
|
+
}
|
|
541
|
+
return new Text(line, 0, 0);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Error / Aborted / Stopped
|
|
545
|
+
const isError = details.status === "error";
|
|
546
|
+
const isStopped = details.status === "stopped";
|
|
547
|
+
const s = stats(details);
|
|
548
|
+
let line = (isStopped ? theme.fg("dim", "■") : theme.fg("error", "✗")) + (s ? " " + s : "");
|
|
549
|
+
|
|
550
|
+
if (isError) {
|
|
551
|
+
line += "\n" + theme.fg("error", ` ⎿ Error: ${details.error ?? "unknown"}`);
|
|
552
|
+
} else if (isStopped) {
|
|
553
|
+
line += "\n" + theme.fg("dim", " ⎿ Stopped");
|
|
554
|
+
} else {
|
|
555
|
+
line += "\n" + theme.fg("warning", " ⎿ Aborted (max turns exceeded)");
|
|
556
|
+
}
|
|
557
|
+
return new Text(line, 0, 0);
|
|
558
|
+
},
|
|
559
|
+
|
|
560
|
+
// ---- Execute ----
|
|
561
|
+
|
|
276
562
|
execute: async (toolCallId, params, signal, onUpdate, ctx) => {
|
|
277
563
|
widget.setUICtx(ctx.ui);
|
|
278
564
|
|
|
@@ -284,9 +570,17 @@ Guidelines:
|
|
|
284
570
|
const modelInput = params.model as string | undefined;
|
|
285
571
|
const thinkingLevel = params.thinking as any | undefined;
|
|
286
572
|
|
|
287
|
-
const { state: bgState, callbacks: bgCallbacks } = createActivityTracker(maxTurns);
|
|
288
|
-
|
|
289
573
|
if (runInBackground) {
|
|
574
|
+
const { state: bgState, callbacks: bgCallbacks } = createActivityTracker(maxTurns);
|
|
575
|
+
|
|
576
|
+
// Wrap onSessionCreated to sync tokens
|
|
577
|
+
const origOnSession = bgCallbacks.onSessionCreated;
|
|
578
|
+
bgCallbacks.onSessionCreated = (session: any) => {
|
|
579
|
+
origOnSession(session);
|
|
580
|
+
bgState.tokens = safeFormatTokens(session);
|
|
581
|
+
widget.update();
|
|
582
|
+
};
|
|
583
|
+
|
|
290
584
|
const id = manager.spawn(pi, ctx, type, prompt, {
|
|
291
585
|
description,
|
|
292
586
|
maxTurns,
|
|
@@ -316,27 +610,42 @@ Guidelines:
|
|
|
316
610
|
);
|
|
317
611
|
}
|
|
318
612
|
|
|
319
|
-
// Foreground execution
|
|
613
|
+
// Foreground execution — stream progress via onUpdate
|
|
320
614
|
let spinnerFrame = 0;
|
|
321
615
|
const startedAt = Date.now();
|
|
616
|
+
let fgId: string | undefined;
|
|
617
|
+
|
|
618
|
+
const { state: fgState, callbacks: fgCallbacks } = createActivityTracker(maxTurns);
|
|
322
619
|
|
|
323
620
|
const streamUpdate = () => {
|
|
324
621
|
onUpdate?.({
|
|
325
|
-
content: [{ type: "text", text: `${
|
|
622
|
+
content: [{ type: "text", text: `${fgState.toolUses} tool uses...` }],
|
|
326
623
|
details: {
|
|
327
624
|
status: "running",
|
|
328
|
-
toolUses:
|
|
329
|
-
tokens:
|
|
330
|
-
turnCount:
|
|
331
|
-
maxTurns:
|
|
625
|
+
toolUses: fgState.toolUses,
|
|
626
|
+
tokens: fgState.tokens,
|
|
627
|
+
turnCount: fgState.turnCount,
|
|
628
|
+
maxTurns: fgState.maxTurns,
|
|
332
629
|
durationMs: Date.now() - startedAt,
|
|
333
|
-
activity:
|
|
334
|
-
|
|
335
|
-
: "thinking…",
|
|
336
|
-
spinnerFrame: spinnerFrame % 10,
|
|
630
|
+
activity: describeActivity(fgState.activeTools, fgState.responseText),
|
|
631
|
+
spinnerFrame: spinnerFrame % SPINNER.length,
|
|
337
632
|
},
|
|
338
633
|
});
|
|
339
|
-
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// Wire session to register in widget
|
|
637
|
+
const origOnSession = fgCallbacks.onSessionCreated;
|
|
638
|
+
fgCallbacks.onSessionCreated = (session: any) => {
|
|
639
|
+
origOnSession(session);
|
|
640
|
+
fgState.tokens = safeFormatTokens(session);
|
|
641
|
+
for (const a of manager.listAgents()) {
|
|
642
|
+
if (a.session === session) {
|
|
643
|
+
fgId = a.id;
|
|
644
|
+
agentActivity.set(a.id, fgState);
|
|
645
|
+
widget.ensureTimer();
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
340
649
|
};
|
|
341
650
|
|
|
342
651
|
const spinnerInterval = setInterval(() => {
|
|
@@ -344,7 +653,6 @@ Guidelines:
|
|
|
344
653
|
streamUpdate();
|
|
345
654
|
}, 80);
|
|
346
655
|
|
|
347
|
-
widget.ensureTimer();
|
|
348
656
|
streamUpdate();
|
|
349
657
|
|
|
350
658
|
const record = await manager.spawnAndWait(pi, ctx, type, prompt, {
|
|
@@ -353,21 +661,42 @@ Guidelines:
|
|
|
353
661
|
modelInput,
|
|
354
662
|
modelRegistry: ctx.modelRegistry,
|
|
355
663
|
thinkingLevel,
|
|
356
|
-
...
|
|
664
|
+
...fgCallbacks,
|
|
357
665
|
});
|
|
358
666
|
|
|
359
667
|
clearInterval(spinnerInterval);
|
|
360
668
|
|
|
361
|
-
|
|
669
|
+
// Clean up foreground agent from widget
|
|
670
|
+
if (fgId) {
|
|
671
|
+
agentActivity.delete(fgId);
|
|
672
|
+
widget.markFinished(fgId);
|
|
673
|
+
widget.update();
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const tokenText = safeFormatTokens(fgState.session);
|
|
362
677
|
const durationMs = (record.completedAt ?? Date.now()) - record.startedAt;
|
|
363
678
|
|
|
364
679
|
if (record.status === "error") {
|
|
365
|
-
return textResult(`Agent failed: ${record.error}
|
|
680
|
+
return textResult(`Agent failed: ${record.error}`, {
|
|
681
|
+
status: "error",
|
|
682
|
+
toolUses: record.toolUses,
|
|
683
|
+
tokens: tokenText,
|
|
684
|
+
durationMs,
|
|
685
|
+
error: record.error,
|
|
686
|
+
});
|
|
366
687
|
}
|
|
367
688
|
|
|
368
689
|
return textResult(
|
|
369
690
|
`Agent completed in ${(durationMs / 1000).toFixed(1)}s (${record.toolUses} tool uses${tokenText ? `, ${tokenText} tokens` : ""}).\n\n` +
|
|
370
691
|
(record.result?.trim() || "No output."),
|
|
692
|
+
{
|
|
693
|
+
status: "completed",
|
|
694
|
+
toolUses: record.toolUses,
|
|
695
|
+
tokens: tokenText,
|
|
696
|
+
durationMs,
|
|
697
|
+
turnCount: fgState.turnCount,
|
|
698
|
+
maxTurns: fgState.maxTurns,
|
|
699
|
+
},
|
|
371
700
|
);
|
|
372
701
|
},
|
|
373
702
|
}),
|
|
@@ -379,7 +708,7 @@ Guidelines:
|
|
|
379
708
|
defineTool({
|
|
380
709
|
name: "get_helper_result",
|
|
381
710
|
label: "Get Helper Result",
|
|
382
|
-
description: "Check status and retrieve results from a background agent.",
|
|
711
|
+
description: "Check status and retrieve results from a background agent. Use view: true to open a live conversation overlay.",
|
|
383
712
|
parameters: Type.Object({
|
|
384
713
|
agent_id: Type.String({
|
|
385
714
|
description: "The helper ID to check.",
|
|
@@ -389,13 +718,46 @@ Guidelines:
|
|
|
389
718
|
description: "Wait for completion. Default: false.",
|
|
390
719
|
}),
|
|
391
720
|
),
|
|
721
|
+
view: Type.Optional(
|
|
722
|
+
Type.Boolean({
|
|
723
|
+
description: "Open a live conversation viewer overlay. Default: false.",
|
|
724
|
+
}),
|
|
725
|
+
),
|
|
392
726
|
}),
|
|
393
|
-
execute: async (_toolCallId, params) => {
|
|
727
|
+
execute: async (_toolCallId, params, _signal, _onUpdate, ctx) => {
|
|
394
728
|
const record = manager.getRecord(params.agent_id as string);
|
|
395
729
|
if (!record) {
|
|
396
730
|
return textResult(`Helper not found: "${params.agent_id}". It may have been cleaned up.`);
|
|
397
731
|
}
|
|
398
732
|
|
|
733
|
+
// Open conversation viewer overlay if requested
|
|
734
|
+
if (params.view && record.session) {
|
|
735
|
+
const activity = agentActivity.get(record.id);
|
|
736
|
+
await ctx.ui.custom<undefined>(
|
|
737
|
+
(tui, theme, _keybindings, done) => {
|
|
738
|
+
return new ConversationViewer(
|
|
739
|
+
tui,
|
|
740
|
+
record.session!,
|
|
741
|
+
{
|
|
742
|
+
type: record.type,
|
|
743
|
+
description: record.description,
|
|
744
|
+
status: record.status,
|
|
745
|
+
toolUses: record.toolUses,
|
|
746
|
+
startedAt: record.startedAt,
|
|
747
|
+
completedAt: record.completedAt,
|
|
748
|
+
},
|
|
749
|
+
activity,
|
|
750
|
+
theme,
|
|
751
|
+
done,
|
|
752
|
+
);
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
overlay: true,
|
|
756
|
+
overlayOptions: { anchor: "center", width: "90%" },
|
|
757
|
+
},
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
399
761
|
if (params.wait && record.status === "running" && record.promise) {
|
|
400
762
|
record.resultConsumed = true;
|
|
401
763
|
await record.promise;
|