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/model-precedence.ts
CHANGED
|
@@ -18,6 +18,11 @@ export interface SubagentsConfig {
|
|
|
18
18
|
default: string | null;
|
|
19
19
|
forceBackground: boolean;
|
|
20
20
|
graceTurns?: number;
|
|
21
|
+
showCost?: boolean;
|
|
22
|
+
widgetMaxLines?: number;
|
|
23
|
+
widgetMaxLinesCompact?: number;
|
|
24
|
+
widgetCompact?: boolean;
|
|
25
|
+
widgetShortcut?: boolean;
|
|
21
26
|
[agentType: string]: string | null | undefined | boolean | number;
|
|
22
27
|
};
|
|
23
28
|
concurrency: {
|
package/src/output-file.ts
CHANGED
|
@@ -10,12 +10,7 @@ import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import type { AgentSession, AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { formatTokens } from "./usage.js";
|
|
13
|
-
|
|
14
|
-
/** Max length for a truncated command in tool arg summaries. */
|
|
15
|
-
const MAX_COMMAND_DISPLAY_LENGTH = 100;
|
|
16
|
-
|
|
17
|
-
/** Max length for a truncated string value in default tool arg summaries. */
|
|
18
|
-
const MAX_DEFAULT_STRING_DISPLAY_LENGTH = 200;
|
|
13
|
+
import { summarizeToolArgs } from "./format.js";
|
|
19
14
|
|
|
20
15
|
/** Max content length for full tool result display — longer results get a summary line. */
|
|
21
16
|
const MAX_TOOL_RESULT_DISPLAY_LENGTH = 500;
|
|
@@ -68,68 +63,6 @@ function splitAndPrefix(text: string, role: string): string {
|
|
|
68
63
|
.join("");
|
|
69
64
|
}
|
|
70
65
|
|
|
71
|
-
/**
|
|
72
|
-
* Summarize tool arguments for log-friendly display.
|
|
73
|
-
*
|
|
74
|
-
* Heavy tools (read, write, edit, bash, grep, rg) get compact summaries.
|
|
75
|
-
* Other tools fall back to the default JSON formatting.
|
|
76
|
-
*/
|
|
77
|
-
export function summarizeToolArgs(name: string, rawArgs: Record<string, unknown> | undefined): string {
|
|
78
|
-
if (!rawArgs || typeof rawArgs !== "object" || Object.keys(rawArgs).length === 0) return "";
|
|
79
|
-
|
|
80
|
-
switch (name) {
|
|
81
|
-
case "read": {
|
|
82
|
-
// read("/path/to/file") — just the path
|
|
83
|
-
const path = typeof rawArgs.path === "string" ? rawArgs.path : "";
|
|
84
|
-
return `(${JSON.stringify(path)})`;
|
|
85
|
-
}
|
|
86
|
-
case "write": {
|
|
87
|
-
// write("/path/to/file", <N> chars) — path + content size
|
|
88
|
-
const path = typeof rawArgs.file_path === "string" ? rawArgs.file_path : "";
|
|
89
|
-
const content = rawArgs.content;
|
|
90
|
-
const size = typeof content === "string" ? content.length : 0;
|
|
91
|
-
return `(${JSON.stringify(path)}, ${size} chars)`;
|
|
92
|
-
}
|
|
93
|
-
case "edit": {
|
|
94
|
-
// edit("/path/to/file", <N> edits) — path + edit count
|
|
95
|
-
const path = typeof rawArgs.path === "string" ? rawArgs.path : "";
|
|
96
|
-
const edits = rawArgs.edits;
|
|
97
|
-
const editCount = Array.isArray(edits) ? edits.length : 0;
|
|
98
|
-
return `(${JSON.stringify(path)}, ${editCount} edits)`;
|
|
99
|
-
}
|
|
100
|
-
case "bash": {
|
|
101
|
-
// bash("command") — just the command, strip heredoc, truncate long
|
|
102
|
-
const cmd = typeof rawArgs.command === "string" ? rawArgs.command : "";
|
|
103
|
-
// Strip heredoc: truncate at << followed by delimiter
|
|
104
|
-
const heredocIdx = cmd.search(/<<\s*['"]?\w+['"]?/);
|
|
105
|
-
const cleanCmd = heredocIdx >= 0 ? cmd.slice(0, heredocIdx).trim() : cmd.trim();
|
|
106
|
-
// Truncate long commands
|
|
107
|
-
const display = cleanCmd.length > MAX_COMMAND_DISPLAY_LENGTH
|
|
108
|
-
? cleanCmd.slice(0, MAX_COMMAND_DISPLAY_LENGTH) + "…" : cleanCmd;
|
|
109
|
-
return `(${JSON.stringify(display)})`;
|
|
110
|
-
}
|
|
111
|
-
case "grep":
|
|
112
|
-
case "rg": {
|
|
113
|
-
// grep("pattern", "/path") — pattern + path
|
|
114
|
-
const pattern = typeof rawArgs.pattern === "string" ? rawArgs.pattern : "";
|
|
115
|
-
const path = typeof rawArgs.path === "string" ? rawArgs.path : "";
|
|
116
|
-
return `(${JSON.stringify(pattern)}, ${JSON.stringify(path)})`;
|
|
117
|
-
}
|
|
118
|
-
default: {
|
|
119
|
-
// Default behavior for other tools: single-arg shorthand or JSON dump
|
|
120
|
-
const keys = Object.keys(rawArgs);
|
|
121
|
-
if (keys.length === 1) {
|
|
122
|
-
const val = rawArgs[keys[0]];
|
|
123
|
-
const display = typeof val === "string" && val.length > MAX_DEFAULT_STRING_DISPLAY_LENGTH
|
|
124
|
-
? JSON.stringify(val.slice(0, MAX_DEFAULT_STRING_DISPLAY_LENGTH) + "...")
|
|
125
|
-
: JSON.stringify(val);
|
|
126
|
-
return `(${display})`;
|
|
127
|
-
}
|
|
128
|
-
return ` ${JSON.stringify(rawArgs)}`;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
66
|
/** Format a toolUse/toolCall content item as a single log line. */
|
|
134
67
|
function formatToolItem(item: Record<string, unknown>): string {
|
|
135
68
|
const name = (item.name ?? item.toolName ?? "unknown") as string;
|
package/src/renderer.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* renderer.ts — Rendering helpers for the Agent tool and subagent-result messages.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from index.ts to separate display concerns from extension wiring.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
8
|
+
import type { Theme } from "./types.js";
|
|
9
|
+
import { buildStatsParts, formatMs, getDisplayName } from "./format.js";
|
|
10
|
+
import { __config } from "./state.js";
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Stats rendering helpers
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
/** Format agent display name with optional model: "Agent (mimo-v2.5-pro)" or "Agent". */
|
|
17
|
+
export function agentNameLabel(d: Record<string, unknown>, theme: Theme): string {
|
|
18
|
+
const typeName = getDisplayName((d.type as string) || "");
|
|
19
|
+
const modelName = d.modelName as string | undefined;
|
|
20
|
+
return modelName ? `${theme.bold(typeName)} (${modelName})` : theme.bold(typeName);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Build the stats line for an agent result card. */
|
|
24
|
+
export function buildStatsLine(d: Record<string, unknown>, theme: Theme): string {
|
|
25
|
+
const showCost = __config.agent.showCost === true;
|
|
26
|
+
const parts = buildStatsParts({
|
|
27
|
+
toolUses: (d.toolUses as number) ?? 0,
|
|
28
|
+
turnCount: d.turnCount as number | undefined,
|
|
29
|
+
maxTurns: d.maxTurns as number | undefined,
|
|
30
|
+
tokens: (d.tokens as number) ?? 0,
|
|
31
|
+
contextPercent: d.contextPercent as number | null,
|
|
32
|
+
compactions: (d.compactions as number) ?? 0,
|
|
33
|
+
cost: showCost ? (d.cost as number | undefined) : undefined,
|
|
34
|
+
}, theme);
|
|
35
|
+
parts.push(formatMs(d.durationMs as number));
|
|
36
|
+
return parts.join("·");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Agent tool renderers
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
/** Render the Agent tool call line (e.g., "▸ Agent (model)"). */
|
|
44
|
+
export function renderAgentToolCall(
|
|
45
|
+
args: Record<string, unknown>,
|
|
46
|
+
theme: Theme,
|
|
47
|
+
): Text {
|
|
48
|
+
const typeName = getDisplayName((args.agent as string) || "");
|
|
49
|
+
const label = typeName || "Agent";
|
|
50
|
+
let text = `▸ ${theme.fg("accent", theme.bold(label))}`;
|
|
51
|
+
|
|
52
|
+
const modelOverride = args._modelOverride as string | undefined;
|
|
53
|
+
if (modelOverride) {
|
|
54
|
+
text += ` (${modelOverride})`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return new Text(text, 0, 0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Render the Agent tool result — compact or expanded. */
|
|
61
|
+
export function renderAgentToolResult(
|
|
62
|
+
result: { content: Array<{ type: string; text?: string }>; details?: Record<string, unknown>; isError?: boolean },
|
|
63
|
+
options: { expanded?: boolean },
|
|
64
|
+
theme: Theme,
|
|
65
|
+
): Text {
|
|
66
|
+
const { expanded } = options;
|
|
67
|
+
const text = result.content[0]?.type === "text" ? result.content[0].text ?? "" : "";
|
|
68
|
+
const d = result.details;
|
|
69
|
+
const icon = result.isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
70
|
+
const desc = (d?.description as string) || "";
|
|
71
|
+
|
|
72
|
+
if (d && d.turnCount != null) {
|
|
73
|
+
const namePart = agentNameLabel(d, theme);
|
|
74
|
+
const statsLine = buildStatsLine(d, theme);
|
|
75
|
+
let lines = `${icon} ${namePart}·${statsLine}\n ${theme.fg("text", desc)}`;
|
|
76
|
+
if (expanded && text) {
|
|
77
|
+
lines += "\n" + text.split("\n").map(l => ` ${l}`).join("\n");
|
|
78
|
+
}
|
|
79
|
+
return new Text(lines, 0, 0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Minimal card — background spawns (no stats) use space placeholder
|
|
83
|
+
const isBackground = text.includes("running in background") || text.includes("queued");
|
|
84
|
+
const prefix = isBackground ? " " : `${icon} `;
|
|
85
|
+
if (desc) {
|
|
86
|
+
return new Text(`${prefix}${theme.fg("text", desc)}`, 0, 0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return new Text(`${prefix}${theme.fg("dim", text)}`, 0, 0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// Message renderer — subagent-result (background agent completion)
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
/** Render a subagent-result message injected after background agent completion. */
|
|
97
|
+
export function renderSubagentResult(
|
|
98
|
+
message: { content?: string; details?: Record<string, unknown> },
|
|
99
|
+
options: { expanded?: boolean },
|
|
100
|
+
theme: Theme,
|
|
101
|
+
): Container {
|
|
102
|
+
const { expanded } = options;
|
|
103
|
+
const d = message.details;
|
|
104
|
+
const text = (message.content as string)?.trim() || "";
|
|
105
|
+
|
|
106
|
+
const inner = new Container();
|
|
107
|
+
inner.addChild(new Text(theme.fg("customMessageLabel", "Subagent Result"), 0, 0));
|
|
108
|
+
inner.addChild(new Spacer(1));
|
|
109
|
+
|
|
110
|
+
if (d && d.turnCount != null) {
|
|
111
|
+
const isError = d.status === "error" || d.status === "aborted" || d.status === "stopped";
|
|
112
|
+
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
113
|
+
|
|
114
|
+
const namePart = agentNameLabel(d, theme);
|
|
115
|
+
const statsLine = buildStatsLine(d, theme);
|
|
116
|
+
let headerLine = `${icon} ${namePart}·${statsLine}\n ${theme.fg("text", (d.description as string) || "")}`;
|
|
117
|
+
if (d.outputFile as string) {
|
|
118
|
+
headerLine += `\n ${theme.fg("dim", `tail -f ${d.outputFile}`)}`;
|
|
119
|
+
}
|
|
120
|
+
if (d.worktreePath as string) {
|
|
121
|
+
headerLine += `\n ${theme.fg("dim", `worktree: ${d.worktreePath}`)}`;
|
|
122
|
+
}
|
|
123
|
+
inner.addChild(new Text(headerLine, 0, 0));
|
|
124
|
+
|
|
125
|
+
if (expanded && text) {
|
|
126
|
+
inner.addChild(new Spacer(1));
|
|
127
|
+
inner.addChild(new Text(text.split("\n").map(l => ` ${l}`).join("\n"), 0, 0));
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
inner.addChild(new Text(buildFallbackResultLine(d, text, theme), 0, 0));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t));
|
|
134
|
+
box.addChild(inner);
|
|
135
|
+
|
|
136
|
+
const outer = new Container();
|
|
137
|
+
outer.addChild(new Spacer(1));
|
|
138
|
+
outer.addChild(box);
|
|
139
|
+
outer.addChild(new Spacer(1));
|
|
140
|
+
return outer;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Build a fallback result line for subagent-result messages without stats. */
|
|
144
|
+
function buildFallbackResultLine(
|
|
145
|
+
d: Record<string, unknown> | undefined,
|
|
146
|
+
text: string,
|
|
147
|
+
theme: Theme,
|
|
148
|
+
): string {
|
|
149
|
+
const icon = theme.fg("success", "✓");
|
|
150
|
+
let line = icon;
|
|
151
|
+
if (d?.type) {
|
|
152
|
+
line += ` ${agentNameLabel(d, theme)}`;
|
|
153
|
+
}
|
|
154
|
+
const desc = (d?.description as string) || "";
|
|
155
|
+
if (desc) line += `\n ${theme.fg("text", desc)}`;
|
|
156
|
+
if (d?.outputFile) {
|
|
157
|
+
line += `\n ${theme.fg("dim", `tail -f ${d.outputFile}`)}`;
|
|
158
|
+
}
|
|
159
|
+
if (d?.worktreePath) {
|
|
160
|
+
line += `\n ${theme.fg("dim", `worktree: ${d.worktreePath}`)}`;
|
|
161
|
+
}
|
|
162
|
+
return line;
|
|
163
|
+
}
|
package/src/result-viewer.ts
CHANGED
|
@@ -17,7 +17,8 @@ import {
|
|
|
17
17
|
} from "@earendil-works/pi-tui";
|
|
18
18
|
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
19
19
|
import { type LifetimeUsage, formatTokens } from "./usage.js";
|
|
20
|
-
import {
|
|
20
|
+
import type { Theme } from "./ui/agent-widget.js";
|
|
21
|
+
import { formatMs } from "./format.js";
|
|
21
22
|
|
|
22
23
|
/* ------------------------------------------------------------------ */
|
|
23
24
|
/* Types */
|
package/src/state.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* state.ts — Shared module state. Extracted from index.ts to break circular deps.
|
|
3
|
+
*
|
|
4
|
+
* manager and widget use holders because they're reassigned after import and the
|
|
5
|
+
* PI runtime doesn't propagate ESM live binding reassignments.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionContext, ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import type { SessionModelOverrides, SubagentsConfig } from "./model-precedence.js";
|
|
10
|
+
import { DEFAULT_CONFIG } from "./config-io.js";
|
|
11
|
+
import { AgentManager } from "./agent-manager.js";
|
|
12
|
+
import { AgentWidget, type AgentActivity } from "./ui/agent-widget.js";
|
|
13
|
+
|
|
14
|
+
export let sessionOverrides: SessionModelOverrides = { default: null };
|
|
15
|
+
export let __config: SubagentsConfig = { ...DEFAULT_CONFIG, agent: { ...DEFAULT_CONFIG.agent }, concurrency: { ...DEFAULT_CONFIG.concurrency } };
|
|
16
|
+
export const agentActivity = new Map<string, AgentActivity>();
|
|
17
|
+
export let piInstance: ExtensionAPI;
|
|
18
|
+
/** Stored ExtensionContext from session_start — used by menu spawn flow. */
|
|
19
|
+
export let sessionCtx: ExtensionContext;
|
|
20
|
+
|
|
21
|
+
// Holder objects — PI runtime doesn't propagate ESM live binding reassignments
|
|
22
|
+
const managerHolder: { current?: AgentManager } = {};
|
|
23
|
+
const widgetHolder: { current?: AgentWidget } = {};
|
|
24
|
+
|
|
25
|
+
export function setConfig(config: SubagentsConfig): void { __config = config; }
|
|
26
|
+
export function resetSessionOverrides(): void { sessionOverrides = { default: null }; }
|
|
27
|
+
export function setManager(m: AgentManager): void { managerHolder.current = m; }
|
|
28
|
+
export function clearManager(): void { managerHolder.current = undefined; }
|
|
29
|
+
export function setWidget(w: AgentWidget | undefined): void { widgetHolder.current = w; }
|
|
30
|
+
export function setPiInstance(pi: ExtensionAPI): void { piInstance = pi; }
|
|
31
|
+
export function setSessionCtx(ctx: ExtensionContext): void { sessionCtx = ctx; }
|
|
32
|
+
export function getManager(): AgentManager { return managerHolder.current!; }
|
|
33
|
+
export function getWidget(): AgentWidget | undefined { return widgetHolder.current; }
|
|
34
|
+
|
|
35
|
+
// State mutation helpers
|
|
36
|
+
|
|
37
|
+
/** Update the cost display toggle in config and sync to widget. */
|
|
38
|
+
export function setShowCostEnabled(enabled: boolean): void {
|
|
39
|
+
__config.agent.showCost = enabled;
|
|
40
|
+
getWidget()?.setShowCost(enabled);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Sync widget display settings from config to the widget instance. */
|
|
44
|
+
export function syncWidgetSettings(): void {
|
|
45
|
+
const w = getWidget();
|
|
46
|
+
if (!w) return;
|
|
47
|
+
w.setForceCompact(__config.agent.widgetCompact === true);
|
|
48
|
+
w.setWidgetShortcut(__config.agent.widgetShortcut === true);
|
|
49
|
+
w.setMaxLines(__config.agent.widgetMaxLines ?? 12);
|
|
50
|
+
w.setMaxLinesCompact(
|
|
51
|
+
__config.agent.widgetMaxLinesCompact ?? Math.floor((__config.agent.widgetMaxLines ?? 12) / 2),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Track previous tool expansion state to detect ctrl+o toggle. */
|
|
56
|
+
let lastToolsExpanded: boolean | undefined;
|
|
57
|
+
|
|
58
|
+
/** Reset lastToolsExpanded (called at session_start). */
|
|
59
|
+
export function resetLastToolsExpanded(): void {
|
|
60
|
+
lastToolsExpanded = undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Sync compact mode with the tool expansion state (ctrl+o toggle).
|
|
64
|
+
* Only syncs when widgetShortcut is enabled in config (opt-in behavior).
|
|
65
|
+
* Only triggers on state change (not every tool_execution_start).
|
|
66
|
+
* When forceCompact (widgetCompact) is ON, ignores ctrl+o state changes.
|
|
67
|
+
*/
|
|
68
|
+
export function syncCompactFromToolsExpanded(expanded: boolean): void {
|
|
69
|
+
if (__config.agent.widgetShortcut !== true) {
|
|
70
|
+
lastToolsExpanded = expanded;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// When forceCompact is ON, ignore ctrl+o state changes
|
|
74
|
+
if (__config.agent.widgetCompact === true) {
|
|
75
|
+
lastToolsExpanded = expanded;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Tools expanded → widget full, tools collapsed → widget compact
|
|
79
|
+
if (lastToolsExpanded !== undefined && lastToolsExpanded !== expanded) {
|
|
80
|
+
getWidget()?.setCompactMode(!expanded);
|
|
81
|
+
}
|
|
82
|
+
lastToolsExpanded = expanded;
|
|
83
|
+
}
|