pi-subagents-lite 1.0.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -5
- package/package.json +1 -1
- package/src/agent-manager.ts +88 -62
- package/src/agent-runner.ts +194 -167
- package/src/agent-types.ts +21 -1
- package/src/config-io.ts +9 -1
- package/src/config-mutator.ts +183 -0
- package/src/context.ts +1 -1
- package/src/format.ts +173 -0
- package/src/index.ts +127 -177
- package/src/menus.ts +586 -137
- package/src/model-precedence.ts +5 -0
- package/src/output-file.ts +1 -68
- package/src/renderer.ts +163 -0
- package/src/result-viewer.ts +2 -1
- package/src/state.ts +83 -0
- package/src/tool-execution.ts +179 -56
- package/src/types.ts +104 -31
- package/src/ui/agent-widget.ts +159 -146
- package/src/usage.ts +5 -0
- package/src/worktree-validator.ts +199 -0
- package/src/stop-agent-tool.ts +0 -77
package/src/format.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* format.ts — Consolidated display formatting helpers.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for all display-formatting functions used across
|
|
5
|
+
* the UI layer. Previously scattered across agent-widget.ts, output-file.ts,
|
|
6
|
+
* and agent-types.ts by historical accident.
|
|
7
|
+
*
|
|
8
|
+
* Pure functions — no module-level state, no side effects.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { getConfig } from "./agent-types.js";
|
|
12
|
+
import type { SubagentType, Theme } from "./types.js";
|
|
13
|
+
import { formatTokens, formatCost } from "./usage.js";
|
|
14
|
+
|
|
15
|
+
/** Max length for a truncated command in tool arg summaries. */
|
|
16
|
+
const MAX_COMMAND_DISPLAY_LENGTH = 100;
|
|
17
|
+
|
|
18
|
+
/** Max length for a truncated string value in default tool arg summaries. */
|
|
19
|
+
const MAX_DEFAULT_STRING_DISPLAY_LENGTH = 200;
|
|
20
|
+
|
|
21
|
+
// ---- Internal helpers (used by buildStatsParts) ----
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Token count with optional context-fill % and compaction-count annotations.
|
|
25
|
+
* Thresholds for percent: <70% dim, 70–85% warning, ≥85% error.
|
|
26
|
+
* Compaction count rendered as `↻ N` in dim.
|
|
27
|
+
*
|
|
28
|
+
* "12.3k" — no annotations
|
|
29
|
+
* "12.3k(45%)" — percent only
|
|
30
|
+
* "12.3k(↻ 2)" — compactions only (e.g. right after compact)
|
|
31
|
+
* "12.3k(45%·↻ 2)" — both
|
|
32
|
+
*/
|
|
33
|
+
function formatSessionTokens(
|
|
34
|
+
tokens: number,
|
|
35
|
+
percent: number | null,
|
|
36
|
+
theme: Theme,
|
|
37
|
+
compactions = 0,
|
|
38
|
+
): string {
|
|
39
|
+
const tokenStr = formatTokens(tokens);
|
|
40
|
+
const annot: string[] = [];
|
|
41
|
+
if (percent !== null) {
|
|
42
|
+
const color = percent >= 85 ? "error" : percent >= 70 ? "warning" : "dim";
|
|
43
|
+
annot.push(theme.fg(color, `${Math.round(percent)}%`));
|
|
44
|
+
}
|
|
45
|
+
if (compactions > 0) {
|
|
46
|
+
annot.push(theme.fg("dim", `↻ ${compactions}`));
|
|
47
|
+
}
|
|
48
|
+
if (annot.length === 0) return tokenStr;
|
|
49
|
+
// Include closing paren in the last annotation's color span to prevent
|
|
50
|
+
// ANSI reset from leaving `)` in default color when wrapped in outer dim.
|
|
51
|
+
const lastIdx = annot.length - 1;
|
|
52
|
+
annot[lastIdx] += ")";
|
|
53
|
+
return `${tokenStr}(${annot.join("·")}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Format turn count with optional max limit: "5≤30⟳" or "5⟳". */
|
|
57
|
+
function formatTurns(turnCount: number, maxTurns?: number | null): string {
|
|
58
|
+
return maxTurns != null ? `${turnCount}≤${maxTurns}⟳ ` : `${turnCount}⟳ `;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---- Exported formatting functions ----
|
|
62
|
+
|
|
63
|
+
/** Format milliseconds as a compact human-readable duration: "1h 1m 1s", "5m 37s", "10s", "<1s". */
|
|
64
|
+
export function formatMs(ms: number): string {
|
|
65
|
+
if (!Number.isFinite(ms) || ms < 1000) return "<1s";
|
|
66
|
+
|
|
67
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
68
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
69
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
70
|
+
const seconds = totalSeconds % 60;
|
|
71
|
+
|
|
72
|
+
const parts: string[] = [];
|
|
73
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
74
|
+
if (minutes > 0) parts.push(`${minutes}m`);
|
|
75
|
+
if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`);
|
|
76
|
+
|
|
77
|
+
return parts.join(" ");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build common stats parts: toolUses · turns · tokens with context % · cost.
|
|
82
|
+
* Shared by AgentWidget and index.ts for consistent stats display.
|
|
83
|
+
*/
|
|
84
|
+
export function buildStatsParts(
|
|
85
|
+
args: {
|
|
86
|
+
toolUses: number;
|
|
87
|
+
turnCount?: number;
|
|
88
|
+
maxTurns?: number;
|
|
89
|
+
tokens: number;
|
|
90
|
+
contextPercent: number | null;
|
|
91
|
+
compactions: number;
|
|
92
|
+
cost?: number;
|
|
93
|
+
},
|
|
94
|
+
theme: Theme,
|
|
95
|
+
): string[] {
|
|
96
|
+
const parts: string[] = [];
|
|
97
|
+
if (args.toolUses > 0) parts.push(`${args.toolUses}🛠 `);
|
|
98
|
+
if (args.turnCount != null) parts.push(formatTurns(args.turnCount, args.maxTurns));
|
|
99
|
+
if (args.tokens > 0) {
|
|
100
|
+
parts.push(formatSessionTokens(
|
|
101
|
+
args.tokens, args.contextPercent, theme, args.compactions,
|
|
102
|
+
));
|
|
103
|
+
}
|
|
104
|
+
if (args.cost != null && args.cost > 0) parts.push(formatCost(args.cost));
|
|
105
|
+
return parts;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Get display name for any agent type (built-in or custom). */
|
|
109
|
+
export function getDisplayName(type: SubagentType): string {
|
|
110
|
+
return getConfig(type).displayName;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Summarize tool arguments for log-friendly display.
|
|
115
|
+
*
|
|
116
|
+
* Heavy tools (read, write, edit, bash, grep, rg) get compact summaries.
|
|
117
|
+
* Other tools fall back to the default JSON formatting.
|
|
118
|
+
*/
|
|
119
|
+
export function summarizeToolArgs(name: string, rawArgs: Record<string, unknown> | undefined): string {
|
|
120
|
+
if (!rawArgs || typeof rawArgs !== "object" || Object.keys(rawArgs).length === 0) return "";
|
|
121
|
+
|
|
122
|
+
switch (name) {
|
|
123
|
+
case "read": {
|
|
124
|
+
// read("/path/to/file") — just the path
|
|
125
|
+
const path = typeof rawArgs.path === "string" ? rawArgs.path : "";
|
|
126
|
+
return `(${JSON.stringify(path)})`;
|
|
127
|
+
}
|
|
128
|
+
case "write": {
|
|
129
|
+
// write("/path/to/file", <N> chars) — path + content size
|
|
130
|
+
const path = typeof rawArgs.file_path === "string" ? rawArgs.file_path : "";
|
|
131
|
+
const content = rawArgs.content;
|
|
132
|
+
const size = typeof content === "string" ? content.length : 0;
|
|
133
|
+
return `(${JSON.stringify(path)}, ${size} chars)`;
|
|
134
|
+
}
|
|
135
|
+
case "edit": {
|
|
136
|
+
// edit("/path/to/file", <N> edits) — path + edit count
|
|
137
|
+
const path = typeof rawArgs.path === "string" ? rawArgs.path : "";
|
|
138
|
+
const edits = rawArgs.edits;
|
|
139
|
+
const editCount = Array.isArray(edits) ? edits.length : 0;
|
|
140
|
+
return `(${JSON.stringify(path)}, ${editCount} edits)`;
|
|
141
|
+
}
|
|
142
|
+
case "bash": {
|
|
143
|
+
// bash("command") — just the command, strip heredoc, truncate long
|
|
144
|
+
const cmd = typeof rawArgs.command === "string" ? rawArgs.command : "";
|
|
145
|
+
// Strip heredoc: truncate at << followed by delimiter
|
|
146
|
+
const heredocIdx = cmd.search(/<<\s*['"]?\w+['"]?/);
|
|
147
|
+
const cleanCmd = heredocIdx >= 0 ? cmd.slice(0, heredocIdx).trim() : cmd.trim();
|
|
148
|
+
// Truncate long commands
|
|
149
|
+
const display = cleanCmd.length > MAX_COMMAND_DISPLAY_LENGTH
|
|
150
|
+
? cleanCmd.slice(0, MAX_COMMAND_DISPLAY_LENGTH) + "…" : cleanCmd;
|
|
151
|
+
return `(${JSON.stringify(display)})`;
|
|
152
|
+
}
|
|
153
|
+
case "grep":
|
|
154
|
+
case "rg": {
|
|
155
|
+
// grep("pattern", "/path") — pattern + path
|
|
156
|
+
const pattern = typeof rawArgs.pattern === "string" ? rawArgs.pattern : "";
|
|
157
|
+
const path = typeof rawArgs.path === "string" ? rawArgs.path : "";
|
|
158
|
+
return `(${JSON.stringify(pattern)}, ${JSON.stringify(path)})`;
|
|
159
|
+
}
|
|
160
|
+
default: {
|
|
161
|
+
// Default behavior for other tools: single-arg shorthand or JSON dump
|
|
162
|
+
const keys = Object.keys(rawArgs);
|
|
163
|
+
if (keys.length === 1) {
|
|
164
|
+
const val = rawArgs[keys[0]];
|
|
165
|
+
const display = typeof val === "string" && val.length > MAX_DEFAULT_STRING_DISPLAY_LENGTH
|
|
166
|
+
? JSON.stringify(val.slice(0, MAX_DEFAULT_STRING_DISPLAY_LENGTH) + "...")
|
|
167
|
+
: JSON.stringify(val);
|
|
168
|
+
return `(${display})`;
|
|
169
|
+
}
|
|
170
|
+
return ` ${JSON.stringify(rawArgs)}`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
* - session_shutdown: Abort all, dispose manager
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
27
26
|
import { Type } from "@sinclair/typebox";
|
|
28
27
|
import * as path from "node:path";
|
|
29
28
|
import type {
|
|
@@ -31,38 +30,44 @@ import type {
|
|
|
31
30
|
ExtensionCommandContext,
|
|
32
31
|
ExtensionContext,
|
|
33
32
|
} from "@earendil-works/pi-coding-agent";
|
|
34
|
-
import type { SessionModelOverrides, SubagentsConfig } from "./model-precedence.js";
|
|
35
33
|
import { DEFAULT_AGENTS } from "./default-agents.js";
|
|
36
34
|
import { registerAgents, getAvailableTypes, setAgentScanDirs } from "./agent-types.js";
|
|
37
35
|
import { scanAgentFilesInDir, mergeAgents } from "./agent-discovery.js";
|
|
38
36
|
import { AgentManager } from "./agent-manager.js";
|
|
39
|
-
import { AgentWidget,
|
|
37
|
+
import { AgentWidget, type UICtx } from "./ui/agent-widget.js";
|
|
40
38
|
import { showAgentsMainMenu } from "./menus.js";
|
|
41
|
-
import { loadConfig
|
|
42
|
-
import { executeAgentTool, toolCallListener, backgroundAgentIds, scheduleNudge } from "./tool-execution.js";
|
|
43
|
-
import {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
39
|
+
import { loadConfig } from "./config-io.js";
|
|
40
|
+
import { executeAgentTool, executeStopAgentTool, toolCallListener, backgroundAgentIds, scheduleNudge } from "./tool-execution.js";
|
|
41
|
+
import { renderAgentToolCall, renderAgentToolResult, renderSubagentResult } from "./renderer.js";
|
|
42
|
+
import {
|
|
43
|
+
__config,
|
|
44
|
+
sessionOverrides,
|
|
45
|
+
agentActivity,
|
|
46
|
+
piInstance,
|
|
47
|
+
setConfig,
|
|
48
|
+
setManager,
|
|
49
|
+
clearManager,
|
|
50
|
+
setWidget,
|
|
51
|
+
setPiInstance,
|
|
52
|
+
setSessionCtx,
|
|
53
|
+
resetSessionOverrides,
|
|
54
|
+
resetLastToolsExpanded,
|
|
55
|
+
syncWidgetSettings,
|
|
56
|
+
syncCompactFromToolsExpanded,
|
|
57
|
+
getManager,
|
|
58
|
+
getWidget,
|
|
59
|
+
} from "./state.js";
|
|
60
|
+
|
|
61
|
+
// Re-exports for backward compatibility
|
|
62
|
+
export {
|
|
63
|
+
__config,
|
|
64
|
+
sessionOverrides,
|
|
65
|
+
agentActivity,
|
|
66
|
+
piInstance,
|
|
67
|
+
setShowCostEnabled,
|
|
68
|
+
syncWidgetSettings,
|
|
69
|
+
syncCompactFromToolsExpanded,
|
|
70
|
+
} from "./state.js";
|
|
66
71
|
|
|
67
72
|
|
|
68
73
|
|
|
@@ -75,30 +80,37 @@ export let piInstance: ExtensionAPI;
|
|
|
75
80
|
* Idempotent — safe to call on every session_start.
|
|
76
81
|
*/
|
|
77
82
|
function ensureManagerAndWidget(): void {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
manager
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
backgroundAgentIds.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// renderFinishedLine reads activity for turn count, tokens, etc.
|
|
90
|
-
widget?.markFinished(record.id);
|
|
91
|
-
widget?.update();
|
|
83
|
+
const currentManager = getManager();
|
|
84
|
+
const currentWidget = getWidget();
|
|
85
|
+
// Create manager if missing
|
|
86
|
+
if (!currentManager) {
|
|
87
|
+
const newManager = new AgentManager(
|
|
88
|
+
(record) => {
|
|
89
|
+
// Only nudge for background (async) agents — sync agents already returned via tool result
|
|
90
|
+
if (backgroundAgentIds.has(record.id)) {
|
|
91
|
+
scheduleNudge(record.id);
|
|
92
|
+
backgroundAgentIds.delete(record.id);
|
|
93
|
+
}
|
|
92
94
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
// Mark finished and update widget BEFORE deleting activity —
|
|
96
|
+
// renderFinishedLine reads activity for turn count, tokens, etc.
|
|
97
|
+
getWidget()?.markFinished(record.id);
|
|
98
|
+
getWidget()?.update();
|
|
99
|
+
|
|
100
|
+
// Remove from live activity tracking
|
|
101
|
+
agentActivity.delete(record.id);
|
|
102
|
+
},
|
|
103
|
+
__config.concurrency,
|
|
104
|
+
);
|
|
105
|
+
setManager(newManager);
|
|
106
|
+
}
|
|
98
107
|
|
|
99
|
-
// Create
|
|
100
|
-
if (!
|
|
101
|
-
|
|
108
|
+
// Create widget if missing (uses existing or newly created manager)
|
|
109
|
+
if (!currentWidget) {
|
|
110
|
+
const newWidget = new AgentWidget(getManager(), agentActivity);
|
|
111
|
+
newWidget.setShowCost(__config.agent.showCost === true);
|
|
112
|
+
setWidget(newWidget);
|
|
113
|
+
syncWidgetSettings();
|
|
102
114
|
}
|
|
103
115
|
}
|
|
104
116
|
|
|
@@ -127,35 +139,12 @@ async function scanAndRegisterAgents(ctx: ExtensionContext): Promise<void> {
|
|
|
127
139
|
}
|
|
128
140
|
|
|
129
141
|
async function loadConfigAndRegisterAgents(ctx: ExtensionContext): Promise<void> {
|
|
130
|
-
|
|
142
|
+
setConfig(loadConfig());
|
|
131
143
|
ensureManagerAndWidget();
|
|
132
144
|
await scanAndRegisterAgents(ctx);
|
|
133
145
|
}
|
|
134
146
|
|
|
135
|
-
// ============================================================================
|
|
136
|
-
// UI helpers — stats card rendering (shared by renderResult and message renderer)
|
|
137
|
-
// ============================================================================
|
|
138
|
-
|
|
139
|
-
/** Format agent display name with optional model: "Agent (mimo-v2.5-pro)" or "Agent". */
|
|
140
|
-
function agentNameLabel(d: Record<string, unknown>, theme: Theme): string {
|
|
141
|
-
const typeName = getDisplayName((d.type as string) || "");
|
|
142
|
-
const modelName = d.modelName as string | undefined;
|
|
143
|
-
return modelName ? `${theme.bold(typeName)} (${modelName})` : theme.bold(typeName);
|
|
144
|
-
}
|
|
145
147
|
|
|
146
|
-
/** Build the stats line for an agent result card. Used by both renderers. */
|
|
147
|
-
function buildStatsLine(d: Record<string, unknown>, theme: Theme): string {
|
|
148
|
-
const parts = buildStatsParts({
|
|
149
|
-
toolUses: (d.toolUses as number) ?? 0,
|
|
150
|
-
turnCount: d.turnCount as number | undefined,
|
|
151
|
-
maxTurns: d.maxTurns as number | undefined,
|
|
152
|
-
tokens: (d.tokens as number) ?? 0,
|
|
153
|
-
contextPercent: d.contextPercent as number | null,
|
|
154
|
-
compactions: (d.compactions as number) ?? 0,
|
|
155
|
-
}, theme);
|
|
156
|
-
parts.push(formatMs(d.durationMs as number));
|
|
157
|
-
return parts.join("·");
|
|
158
|
-
}
|
|
159
148
|
|
|
160
149
|
// ============================================================================
|
|
161
150
|
// Agent tool registration helper — dynamic enum for agent types
|
|
@@ -182,55 +171,17 @@ function registerAgentTool(pi: ExtensionAPI): void {
|
|
|
182
171
|
description: Type.String(),
|
|
183
172
|
agent: agentParam,
|
|
184
173
|
run_in_background: Type.Optional(Type.Boolean()),
|
|
185
|
-
|
|
174
|
+
worktree_path: Type.Optional(Type.String()),
|
|
186
175
|
}),
|
|
187
176
|
execute: executeAgentTool,
|
|
188
177
|
|
|
189
|
-
renderCall(args, theme)
|
|
190
|
-
const typeName = getDisplayName((args.agent as string) || "");
|
|
191
|
-
const label = typeName || "Agent";
|
|
192
|
-
let text = `▸ ${theme.fg("accent", theme.bold(label))}`;
|
|
193
|
-
|
|
194
|
-
// Show model in parens when it differs from the parent model
|
|
195
|
-
// _modelOverride is injected by toolCallListener when the resolved
|
|
196
|
-
// model differs from the session's parent model
|
|
197
|
-
const a = args as Record<string, unknown>;
|
|
198
|
-
const modelOverride = a._modelOverride as string | undefined;
|
|
199
|
-
if (modelOverride) {
|
|
200
|
-
text += ` (${modelOverride})`;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return new Text(text, 0, 0);
|
|
204
|
-
},
|
|
205
|
-
|
|
206
|
-
renderResult(result, options, theme) {
|
|
207
|
-
const { expanded } = options as { expanded?: boolean };
|
|
208
|
-
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
209
|
-
const d = result.details as Record<string, unknown> | undefined;
|
|
210
|
-
const isError = !!(result as any).isError;
|
|
211
|
-
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
212
|
-
const desc = (d?.description as string) || "";
|
|
213
|
-
|
|
214
|
-
if (d && d.turnCount != null) {
|
|
215
|
-
const namePart = agentNameLabel(d, theme);
|
|
216
|
-
const statsLine = buildStatsLine(d, theme);
|
|
217
|
-
let lines = `${icon} ${namePart}·${statsLine}\n ${theme.fg("text", desc)}`;
|
|
218
|
-
if (expanded && text) {
|
|
219
|
-
lines += "\n" + text.split("\n").map(l => ` ${l}`).join("\n");
|
|
220
|
-
}
|
|
221
|
-
return new Text(lines, 0, 0);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Minimal card — type name already shown by renderCall
|
|
225
|
-
// For background spawns (no stats), use space placeholder — agent isn't done yet
|
|
226
|
-
const isBackground = text.includes("running in background") || text.includes("queued");
|
|
227
|
-
const prefix = isBackground ? " " : `${icon} `;
|
|
228
|
-
if (desc) {
|
|
229
|
-
return new Text(`${prefix}${theme.fg("text", desc)}`, 0, 0);
|
|
230
|
-
}
|
|
178
|
+
renderCall: (args, theme) => renderAgentToolCall(args as Record<string, unknown>, theme),
|
|
231
179
|
|
|
232
|
-
|
|
233
|
-
|
|
180
|
+
renderResult: (result, options, theme) => renderAgentToolResult(
|
|
181
|
+
result as { content: Array<{ type: string; text?: string }>; details?: Record<string, unknown>; isError?: boolean },
|
|
182
|
+
options as { expanded?: boolean },
|
|
183
|
+
theme,
|
|
184
|
+
),
|
|
234
185
|
});
|
|
235
186
|
}
|
|
236
187
|
|
|
@@ -240,7 +191,7 @@ function registerAgentTool(pi: ExtensionAPI): void {
|
|
|
240
191
|
|
|
241
192
|
export default function (pi: ExtensionAPI) {
|
|
242
193
|
// Store pi for execute callbacks
|
|
243
|
-
|
|
194
|
+
setPiInstance(pi);
|
|
244
195
|
|
|
245
196
|
// ========================================================================
|
|
246
197
|
// Tool registration (stealth schemas — at init time)
|
|
@@ -261,55 +212,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
261
212
|
});
|
|
262
213
|
|
|
263
214
|
// Message renderer — subagent-result (background agent completion)
|
|
264
|
-
pi.registerMessageRenderer("subagent-result", (message, options, theme) =>
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
inner.addChild(new Spacer(1));
|
|
272
|
-
|
|
273
|
-
if (d && d.turnCount != null) {
|
|
274
|
-
const isError = d.status === "error" || d.status === "aborted" || d.status === "stopped";
|
|
275
|
-
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
276
|
-
const desc = (d.description as string) || "";
|
|
277
|
-
|
|
278
|
-
const namePart = agentNameLabel(d, theme);
|
|
279
|
-
const statsLine = buildStatsLine(d, theme);
|
|
280
|
-
let headerLine = `${icon} ${namePart}·${statsLine}\n ${theme.fg("text", desc)}`;
|
|
281
|
-
if ((d.outputFile as string)) {
|
|
282
|
-
headerLine += `\n ${theme.fg("dim", `tail -f ${d.outputFile}`)}`;
|
|
283
|
-
}
|
|
284
|
-
inner.addChild(new Text(headerLine, 0, 0));
|
|
285
|
-
|
|
286
|
-
if (expanded && text) {
|
|
287
|
-
inner.addChild(new Spacer(1));
|
|
288
|
-
const resultLines = text.split("\n").map(l => ` ${l}`).join("\n");
|
|
289
|
-
inner.addChild(new Text(resultLines, 0, 0));
|
|
290
|
-
}
|
|
291
|
-
} else {
|
|
292
|
-
const desc = (d?.description as string) || "";
|
|
293
|
-
let line = `${theme.fg("success", "✓")}`;
|
|
294
|
-
if (d?.type) {
|
|
295
|
-
line += ` ${agentNameLabel(d, theme)}`;
|
|
296
|
-
}
|
|
297
|
-
if (desc) line += `\n ${theme.fg("text", desc)}`;
|
|
298
|
-
if (d?.outputFile) {
|
|
299
|
-
line += `\n ${theme.fg("dim", `tail -f ${d.outputFile}`)}`;
|
|
300
|
-
}
|
|
301
|
-
inner.addChild(new Text(line, 0, 0));
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t));
|
|
305
|
-
box.addChild(inner);
|
|
306
|
-
|
|
307
|
-
const outer = new Container();
|
|
308
|
-
outer.addChild(new Spacer(1));
|
|
309
|
-
outer.addChild(box);
|
|
310
|
-
outer.addChild(new Spacer(1));
|
|
311
|
-
return outer;
|
|
312
|
-
});
|
|
215
|
+
pi.registerMessageRenderer("subagent-result", (message, options, theme) =>
|
|
216
|
+
renderSubagentResult(
|
|
217
|
+
message as { content?: string; details?: Record<string, unknown> },
|
|
218
|
+
options as { expanded?: boolean },
|
|
219
|
+
theme,
|
|
220
|
+
),
|
|
221
|
+
);
|
|
313
222
|
|
|
314
223
|
// Command registration
|
|
315
224
|
pi.registerCommand("agents", {
|
|
@@ -324,25 +233,66 @@ export default function (pi: ExtensionAPI) {
|
|
|
324
233
|
pi.on("tool_call", toolCallListener);
|
|
325
234
|
|
|
326
235
|
pi.on("tool_execution_start", async (_event, ctx) => {
|
|
327
|
-
|
|
328
|
-
|
|
236
|
+
// Set UI context on first tool execution
|
|
237
|
+
if (!getWidget()) {
|
|
238
|
+
ensureManagerAndWidget();
|
|
239
|
+
}
|
|
240
|
+
getWidget()?.setUICtx(ctx.ui as unknown as UICtx);
|
|
241
|
+
getWidget()?.onTurnStart();
|
|
329
242
|
});
|
|
330
243
|
|
|
244
|
+
|
|
245
|
+
|
|
331
246
|
// session_start — load config, scan agents, register into registry,
|
|
332
247
|
// then re-register Agent tool with dynamic agent type enum
|
|
248
|
+
// Listen for ctrl+o keypress to sync compact mode (push-based, no polling)
|
|
249
|
+
let unregisterTerminalInput: (() => void) | undefined;
|
|
250
|
+
|
|
333
251
|
pi.on("session_start", async (_event: unknown, ctx: ExtensionContext) => {
|
|
334
|
-
|
|
252
|
+
setSessionCtx(ctx);
|
|
253
|
+
resetSessionOverrides();
|
|
335
254
|
agentActivity.clear();
|
|
255
|
+
resetLastToolsExpanded();
|
|
336
256
|
await loadConfigAndRegisterAgents(ctx);
|
|
337
257
|
// Re-register with updated agent type list (now includes user/project agents)
|
|
338
258
|
registerAgentTool(pi);
|
|
259
|
+
// Register ctrl+o listener
|
|
260
|
+
if (ctx.hasUI && !unregisterTerminalInput) {
|
|
261
|
+
unregisterTerminalInput = ctx.ui.onTerminalInput((data: string) => {
|
|
262
|
+
// ctrl+o = 0x0F (15) — toggles tool expansion
|
|
263
|
+
if (data === "\u000f") {
|
|
264
|
+
// Read state after a tick to let the built-in handler process it first
|
|
265
|
+
setTimeout(() => {
|
|
266
|
+
const ui = ctx.ui as unknown as { getToolsExpanded?: () => boolean };
|
|
267
|
+
const expanded = ui.getToolsExpanded?.();
|
|
268
|
+
if (expanded !== undefined) {
|
|
269
|
+
getWidget()?.notifyToolsExpansionChanged(expanded);
|
|
270
|
+
}
|
|
271
|
+
}, 0);
|
|
272
|
+
}
|
|
273
|
+
return undefined; // Don't consume the input
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
// Sync compact mode with initial tool expansion state
|
|
277
|
+
syncCompactFromToolsExpanded(false);
|
|
339
278
|
});
|
|
340
279
|
|
|
341
|
-
pi.on("session_shutdown", async (_event: unknown) => {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (
|
|
345
|
-
|
|
280
|
+
pi.on("session_shutdown", async (_event: unknown, ctx: ExtensionContext) => {
|
|
281
|
+
// Warn if agents were killed
|
|
282
|
+
const currentManager = getManager();
|
|
283
|
+
if (currentManager) {
|
|
284
|
+
const records = currentManager.listAgents();
|
|
285
|
+
const active = records.filter(r => r.lifecycle.status === "running" || r.lifecycle.status === "queued");
|
|
286
|
+
if (active.length > 0 && ctx.hasUI) {
|
|
287
|
+
ctx.ui.notify(`${active.length} agent(s) killed by reload`, "warning");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
getWidget()?.dispose();
|
|
291
|
+
setWidget(undefined);
|
|
292
|
+
const mgr = getManager();
|
|
293
|
+
if (mgr) {
|
|
294
|
+
await mgr.dispose();
|
|
295
|
+
clearManager();
|
|
346
296
|
}
|
|
347
297
|
});
|
|
348
298
|
}
|