@pi-unipi/subagents 2.6.1 → 2.9.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 +89 -78
- package/agents/delegate.md +14 -0
- package/agents/oracle.md +78 -0
- package/agents/researcher.md +52 -0
- package/agents/reviewer.md +79 -0
- package/agents/scout.md +50 -0
- package/agents/worker.md +59 -0
- package/package.json +22 -20
- package/prompts/council.md +48 -0
- package/prompts/gather-context-and-clarify.md +13 -0
- package/prompts/parallel-cleanup.md +59 -0
- package/prompts/parallel-research.md +50 -0
- package/prompts/parallel-review.md +54 -0
- package/prompts/review-loop.md +43 -0
- package/skills/council-mode/SKILL.md +230 -0
- package/skills/subagents/SKILL.md +49 -0
- package/skills/subagents/references/constraints-and-recipes.md +259 -0
- package/skills/subagents/references/execution-controls.md +454 -0
- package/skills/subagents/references/management-authoring-rpc.md +161 -0
- package/skills/subagents/references/multi-lane-orchestration.md +39 -0
- package/skills/subagents/references/prompting-and-roles.md +267 -0
- package/src/acceptance.ts +331 -0
- package/src/agent-manager.ts +405 -0
- package/src/agent-memory.ts +300 -0
- package/src/agent-overrides.ts +183 -0
- package/src/agent-runner.ts +271 -0
- package/src/async-runner.ts +417 -0
- package/src/authority-policy.ts +47 -0
- package/src/budgets.ts +285 -0
- package/src/child-safety.ts +140 -0
- package/src/config.ts +226 -0
- package/src/conversation-viewer.ts +281 -0
- package/src/core-compat.ts +94 -0
- package/src/custom-agents.ts +323 -0
- package/src/file-system-retry.ts +50 -0
- package/src/fleet-data.ts +61 -0
- package/src/fleet-view.ts +316 -0
- package/src/foreground-detach.ts +59 -0
- package/src/fork-context.ts +285 -0
- package/src/global.d.ts +7 -0
- package/src/guide.ts +129 -0
- package/src/index.ts +1149 -0
- package/src/mission-state.ts +133 -0
- package/src/mission-store.ts +434 -0
- package/src/model-resolver.ts +79 -0
- package/src/output-limits.ts +142 -0
- package/src/parity-types.ts +315 -0
- package/src/pi-args.ts +161 -0
- package/src/pi-spawn.ts +147 -0
- package/src/result-files.ts +232 -0
- package/src/result-watcher.ts +221 -0
- package/src/retained-children.ts +176 -0
- package/src/run-fanout-budget.ts +356 -0
- package/src/scheduled-runs.ts +351 -0
- package/src/schemas.ts +263 -0
- package/src/slash-commands.ts +77 -0
- package/src/supervisor-channel.ts +249 -0
- package/src/tool-handler.ts +1144 -0
- package/src/types.ts +238 -0
- package/src/widget.ts +447 -0
- package/src/workflow-script.ts +737 -0
- package/src/workflow-worker.ts +384 -0
- package/src/worktree.ts +614 -0
- package/dist/agent-manager.d.ts +0 -81
- package/dist/agent-manager.d.ts.map +0 -1
- package/dist/agent-manager.js +0 -295
- package/dist/agent-manager.js.map +0 -1
- package/dist/agent-runner.d.ts +0 -51
- package/dist/agent-runner.d.ts.map +0 -1
- package/dist/agent-runner.js +0 -262
- 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/conversation-viewer.d.ts +0 -40
- package/dist/conversation-viewer.d.ts.map +0 -1
- package/dist/conversation-viewer.js +0 -276
- package/dist/conversation-viewer.js.map +0 -1
- package/dist/core-compat.d.ts +0 -21
- package/dist/core-compat.d.ts.map +0 -1
- package/dist/core-compat.js +0 -86
- package/dist/core-compat.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 -113
- 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 -10
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -758
- 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/types.d.ts +0 -98
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -47
- package/dist/types.js.map +0 -1
- package/dist/widget.d.ts +0 -56
- package/dist/widget.d.ts.map +0 -1
- package/dist/widget.js +0 -396
- package/dist/widget.js.map +0 -1
package/src/types.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/subagents — Type definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
6
|
+
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
|
|
8
|
+
export type { ThinkingLevel };
|
|
9
|
+
|
|
10
|
+
/** Agent type name: built-in or user-defined. */
|
|
11
|
+
export type AgentType = string;
|
|
12
|
+
|
|
13
|
+
/** Built-in agent type names. */
|
|
14
|
+
export const BUILTIN_TYPES = ["explore", "work"] as const;
|
|
15
|
+
|
|
16
|
+
/** Read-only tool names for explore agents. */
|
|
17
|
+
const READ_ONLY_TOOLS = ["read", "bash", "grep", "find", "ls"];
|
|
18
|
+
|
|
19
|
+
/** All write-capable tool names. */
|
|
20
|
+
const ALL_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
21
|
+
|
|
22
|
+
/** Built-in agent configurations. */
|
|
23
|
+
export const BUILTIN_CONFIGS: Record<string, AgentConfig> = {
|
|
24
|
+
explore: {
|
|
25
|
+
name: "explore",
|
|
26
|
+
displayName: "Explore",
|
|
27
|
+
description: "Read-only exploration agent for parallel file reads and searches.",
|
|
28
|
+
builtinToolNames: READ_ONLY_TOOLS,
|
|
29
|
+
disallowedTools: ["edit", "write"],
|
|
30
|
+
extensions: false,
|
|
31
|
+
skills: false,
|
|
32
|
+
systemPrompt: "You are an explore agent. Read files, search code, and report findings. Do NOT modify any files.",
|
|
33
|
+
promptMode: "append",
|
|
34
|
+
source: "builtin",
|
|
35
|
+
},
|
|
36
|
+
work: {
|
|
37
|
+
name: "work",
|
|
38
|
+
displayName: "Worker",
|
|
39
|
+
description: "Write-capable worker agent with transparent file locking.",
|
|
40
|
+
builtinToolNames: ALL_TOOLS,
|
|
41
|
+
extensions: false,
|
|
42
|
+
skills: false,
|
|
43
|
+
systemPrompt: "You are a worker agent. Implement changes, write code, and complete tasks. Use the provided tools to make the requested modifications.",
|
|
44
|
+
promptMode: "append",
|
|
45
|
+
source: "builtin",
|
|
46
|
+
},
|
|
47
|
+
"name-gen": {
|
|
48
|
+
name: "name-gen",
|
|
49
|
+
displayName: "Name Generator",
|
|
50
|
+
description: "Minimal agent for generating session names from conversation context.",
|
|
51
|
+
builtinToolNames: [],
|
|
52
|
+
extensions: false,
|
|
53
|
+
skills: false,
|
|
54
|
+
systemPrompt: "You are a session name generator. Generate concise titles from conversation context. Reply with ONLY the title.",
|
|
55
|
+
promptMode: "replace",
|
|
56
|
+
source: "builtin",
|
|
57
|
+
},
|
|
58
|
+
} as const;
|
|
59
|
+
|
|
60
|
+
/** Memory scope for persistent agent memory. */
|
|
61
|
+
export type MemoryScope = "user" | "project" | "local";
|
|
62
|
+
|
|
63
|
+
/** Structured per-agent memory config (reference parity). */
|
|
64
|
+
export interface AgentMemoryConfig {
|
|
65
|
+
scope: "user" | "project";
|
|
66
|
+
path: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Unified agent configuration. */
|
|
70
|
+
export interface AgentConfig {
|
|
71
|
+
name: string;
|
|
72
|
+
displayName?: string;
|
|
73
|
+
description: string;
|
|
74
|
+
/** Alias names resolving to this agent (reference parity). */
|
|
75
|
+
aliases?: string[];
|
|
76
|
+
builtinToolNames?: string[];
|
|
77
|
+
disallowedTools?: string[];
|
|
78
|
+
extensions: true | string[] | false;
|
|
79
|
+
skills: true | string[] | false;
|
|
80
|
+
/** Extra skill directories to load for this agent. */
|
|
81
|
+
skillPath?: string[];
|
|
82
|
+
model?: string;
|
|
83
|
+
/** Ordered fallback models (reference parity; resolved Phase 2). */
|
|
84
|
+
fallbackModels?: string[];
|
|
85
|
+
thinking?: ThinkingLevel | string | false;
|
|
86
|
+
maxTurns?: number;
|
|
87
|
+
systemPrompt: string;
|
|
88
|
+
promptMode: "replace" | "append";
|
|
89
|
+
/** Reference alias for promptMode. */
|
|
90
|
+
systemPromptMode?: "replace" | "append";
|
|
91
|
+
/** Whether the child inherits project context (AGENTS.md etc.). Default: delegate only. */
|
|
92
|
+
inheritProjectContext?: boolean;
|
|
93
|
+
/** Whether the child inherits skills. Default: false. */
|
|
94
|
+
inheritSkills?: boolean;
|
|
95
|
+
/** Default context for launches that omit context. */
|
|
96
|
+
defaultContext?: "fresh" | "fork";
|
|
97
|
+
runInBackground?: boolean;
|
|
98
|
+
isolated?: boolean;
|
|
99
|
+
/** Agent-level default run deadline. */
|
|
100
|
+
timeoutMs?: number;
|
|
101
|
+
/** Agent-level default hard per-tool deadline. */
|
|
102
|
+
toolTimeoutMs?: number;
|
|
103
|
+
/** Nested-delegation cap for this agent's children. */
|
|
104
|
+
maxSubagentDepth?: number;
|
|
105
|
+
/** Output file for this agent's result. */
|
|
106
|
+
output?: string;
|
|
107
|
+
/** inline (default) or file-only result reference. */
|
|
108
|
+
outputMode?: "inline" | "file-only";
|
|
109
|
+
/** Files to read before running. */
|
|
110
|
+
defaultReads?: string[];
|
|
111
|
+
/** Maintain progress.md during runs. */
|
|
112
|
+
defaultProgress?: boolean;
|
|
113
|
+
memory?: MemoryScope | AgentMemoryConfig;
|
|
114
|
+
isDefault?: boolean;
|
|
115
|
+
enabled?: boolean;
|
|
116
|
+
source?: "builtin" | "project" | "global";
|
|
117
|
+
/** Unknown frontmatter fields preserved for later phases. */
|
|
118
|
+
extraFields?: Record<string, string>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Agent record — tracks a running agent. */
|
|
122
|
+
export interface AgentRecord {
|
|
123
|
+
id: string;
|
|
124
|
+
type: AgentType;
|
|
125
|
+
description: string;
|
|
126
|
+
status: "queued" | "running" | "completed" | "aborted" | "stopped" | "error";
|
|
127
|
+
result?: string;
|
|
128
|
+
/** Private artifact holding the complete result when model-visible output is bounded. */
|
|
129
|
+
resultArtifactPath?: string;
|
|
130
|
+
error?: string;
|
|
131
|
+
toolUses: number;
|
|
132
|
+
startedAt: number;
|
|
133
|
+
completedAt?: number;
|
|
134
|
+
session?: AgentSession;
|
|
135
|
+
abortController?: AbortController;
|
|
136
|
+
promise?: Promise<string>;
|
|
137
|
+
/** Set when result consumed via get_result — suppresses notification. */
|
|
138
|
+
resultConsumed?: boolean;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Extension config. */
|
|
142
|
+
export interface SubagentsConfig {
|
|
143
|
+
maxConcurrent: number;
|
|
144
|
+
enabled: boolean;
|
|
145
|
+
types: Record<string, { enabled?: boolean }>;
|
|
146
|
+
|
|
147
|
+
// ---- pi-subagents parity keys (optional; defaults follow the reference) ----
|
|
148
|
+
/** Background execution for launches that omit run_in_background. Default: true (their asyncByDefault). */
|
|
149
|
+
asyncByDefault?: boolean;
|
|
150
|
+
/** Context for launches that omit context. "fresh" | "fork". */
|
|
151
|
+
defaultSubagentContext?: "fresh" | "fork";
|
|
152
|
+
/** Force depth-0 runs into background mode and bypass launch UI. Default: false. */
|
|
153
|
+
forceTopLevelAsync?: boolean;
|
|
154
|
+
/** Global default runtime deadline (ms). Replaces the 30-minute foreground backstop. */
|
|
155
|
+
timeoutMs?: number;
|
|
156
|
+
/** Optional hard per-tool-call deadline (ms). Precedence: call > agent frontmatter > this > env. */
|
|
157
|
+
toolTimeoutMs?: number;
|
|
158
|
+
/** Cap on simultaneously running children within durable multi-child runs. Default: 20. */
|
|
159
|
+
globalConcurrencyLimit?: number;
|
|
160
|
+
/** Cumulative child launches per parent session. Unset/0 = unlimited. */
|
|
161
|
+
maxSubagentSpawnsPerSession?: number;
|
|
162
|
+
/** Cumulative logical-child cap for one top-level run tree. Default: 64. */
|
|
163
|
+
maxSubagentSpawnsPerRun?: number;
|
|
164
|
+
/** Concurrently active top-level async runs per parent session. Unset/0 = unlimited. */
|
|
165
|
+
maxActiveAsyncRunsPerSession?: number;
|
|
166
|
+
/** Nested delegation depth cap. Default: 2. */
|
|
167
|
+
maxSubagentDepth?: number;
|
|
168
|
+
/** Parallel fanout caps. maxTasks default 8; concurrency default 4. */
|
|
169
|
+
parallel?: { maxTasks?: number; concurrency?: number };
|
|
170
|
+
/** Default output limits for child results. Default: 200KB / 5000 lines. */
|
|
171
|
+
maxOutput?: { bytes?: number; lines?: number };
|
|
172
|
+
/** Session directory for child sessions. `~/` expanded. */
|
|
173
|
+
defaultSessionDir?: string;
|
|
174
|
+
/** Show the persistent fleet panel. Default: true. */
|
|
175
|
+
fleetView?: boolean;
|
|
176
|
+
/** Fleet panel placement. Default: "belowEditor". */
|
|
177
|
+
fleetViewPlacement?: "belowEditor" | "aboveEditor";
|
|
178
|
+
/** Inline chat rendering for spawn_helper results. "rich" | "summary". Default: rich. */
|
|
179
|
+
inlineToolDisplay?: "rich" | "summary";
|
|
180
|
+
/** Main chat renderer density controls. */
|
|
181
|
+
mainWindowRenderer?: { horizontalSpacing?: number; compactResultMaxLines?: number };
|
|
182
|
+
/** How slow async result scans are logged. "all" | "activity" | "off". Default: all. */
|
|
183
|
+
resultScanLogging?: "all" | "activity" | "off";
|
|
184
|
+
/** Shortcut detaching the active foreground single run without terminating it. */
|
|
185
|
+
foregroundDetachShortcut?: string;
|
|
186
|
+
/** Keep get_helper_result registered but make direct calls non-blocking. Default: true. */
|
|
187
|
+
waitTool?: boolean | { enabled?: boolean };
|
|
188
|
+
/** Base directory for worktree-isolated runs. Default: OS temp. */
|
|
189
|
+
worktreeBaseDir?: string;
|
|
190
|
+
/** Hook run once per created worktree. */
|
|
191
|
+
worktreeSetupHook?: string;
|
|
192
|
+
/** Timeout for the worktree setup hook in ms. Default: 30000. */
|
|
193
|
+
worktreeSetupHookTimeoutMs?: number;
|
|
194
|
+
/** Missions store config. */
|
|
195
|
+
missions?: {
|
|
196
|
+
enabled?: boolean;
|
|
197
|
+
directory?: string;
|
|
198
|
+
globalIndex?: boolean;
|
|
199
|
+
globalIndexDir?: string;
|
|
200
|
+
retainTerminal?: number;
|
|
201
|
+
};
|
|
202
|
+
/** Scheduled runs config. */
|
|
203
|
+
scheduledRuns?: { enabled?: boolean; maxPending?: number; storeRoot?: string };
|
|
204
|
+
/** Authority policy for operational actions. */
|
|
205
|
+
authorityPolicy?: {
|
|
206
|
+
discardWorktree?: "confirm" | "auto";
|
|
207
|
+
destructiveCleanup?: "confirm" | "auto";
|
|
208
|
+
spawnBudgetGrant?: "confirm" | "auto";
|
|
209
|
+
scheduleCreate?: "confirm" | "auto";
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Agent activity for widget display. */
|
|
214
|
+
export interface AgentActivity {
|
|
215
|
+
activeTools: Map<string, string>;
|
|
216
|
+
toolUses: number;
|
|
217
|
+
turnCount: number;
|
|
218
|
+
maxTurns?: number;
|
|
219
|
+
tokens: string;
|
|
220
|
+
responseText: string;
|
|
221
|
+
session?: AgentSession;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Details attached to custom notification messages for visual rendering. */
|
|
225
|
+
export interface NotificationDetails {
|
|
226
|
+
id: string;
|
|
227
|
+
description: string;
|
|
228
|
+
status: string;
|
|
229
|
+
toolUses: number;
|
|
230
|
+
turnCount: number;
|
|
231
|
+
maxTurns?: number;
|
|
232
|
+
totalTokens: number;
|
|
233
|
+
durationMs: number;
|
|
234
|
+
error?: string;
|
|
235
|
+
resultPreview: string;
|
|
236
|
+
/** Additional agents in a group notification. */
|
|
237
|
+
others?: NotificationDetails[];
|
|
238
|
+
}
|
package/src/widget.ts
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/subagents — Live widget
|
|
3
|
+
*
|
|
4
|
+
* Shows running/completed agents above the editor with:
|
|
5
|
+
* - Animated braille spinners
|
|
6
|
+
* - Finished agent lingering (1 turn success, 2 turns error)
|
|
7
|
+
* - Priority-based overflow (running > queued > finished)
|
|
8
|
+
* - Status bar integration
|
|
9
|
+
* - Activity description grouping
|
|
10
|
+
* - ANSI-aware truncation via pi-tui
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
14
|
+
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import type { AgentManager } from "./agent-manager.js";
|
|
16
|
+
import type { AgentActivity } from "./types.js";
|
|
17
|
+
|
|
18
|
+
// ---- Constants ----
|
|
19
|
+
|
|
20
|
+
/** Maximum lines the widget may render. */
|
|
21
|
+
const MAX_WIDGET_LINES = 12;
|
|
22
|
+
|
|
23
|
+
/** Braille spinner frames. */
|
|
24
|
+
export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
25
|
+
|
|
26
|
+
/** Statuses that indicate error/non-success (for linger behavior). */
|
|
27
|
+
const ERROR_STATUSES = new Set(["error", "aborted", "stopped"]);
|
|
28
|
+
|
|
29
|
+
/** Tool name → human-readable action for activity descriptions. */
|
|
30
|
+
export const TOOL_DISPLAY: Record<string, string> = {
|
|
31
|
+
read: "reading",
|
|
32
|
+
bash: "running command",
|
|
33
|
+
edit: "editing",
|
|
34
|
+
write: "writing",
|
|
35
|
+
grep: "searching",
|
|
36
|
+
find: "finding files",
|
|
37
|
+
ls: "listing",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ---- Formatting helpers ----
|
|
41
|
+
|
|
42
|
+
/** Format duration. */
|
|
43
|
+
export function formatMs(ms: number): string {
|
|
44
|
+
if (ms >= 60_000) return `${(ms / 60_000).toFixed(1)}m`;
|
|
45
|
+
if (ms >= 1_000) return `${(ms / 1_000).toFixed(1)}s`;
|
|
46
|
+
return `${ms}ms`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Format turns with optional max limit: "⟳5≤30" or "⟳5". */
|
|
50
|
+
export function formatTurns(turn: number, max?: number): string {
|
|
51
|
+
return max != null ? `⟳${turn}≤${max}` : `⟳${turn}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build a human-readable activity string from currently-running tools.
|
|
56
|
+
* Groups by tool type with counts: "reading 3 files, searching 2 patterns".
|
|
57
|
+
*/
|
|
58
|
+
export function describeActivity(activeTools: Map<string, string>, responseText?: string): string {
|
|
59
|
+
if (activeTools.size > 0) {
|
|
60
|
+
const groups = new Map<string, number>();
|
|
61
|
+
for (const toolName of activeTools.values()) {
|
|
62
|
+
const action = TOOL_DISPLAY[toolName] ?? toolName;
|
|
63
|
+
groups.set(action, (groups.get(action) ?? 0) + 1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const parts: string[] = [];
|
|
67
|
+
for (const [action, count] of groups) {
|
|
68
|
+
if (count > 1) {
|
|
69
|
+
parts.push(`${action} ${count} ${action === "searching" ? "patterns" : "files"}`);
|
|
70
|
+
} else {
|
|
71
|
+
parts.push(action);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return parts.join(", ") + "…";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (responseText && responseText.trim().length > 0) {
|
|
78
|
+
const lastLine = responseText.split("\n").find((l) => l.trim())?.trim() ?? "";
|
|
79
|
+
if (lastLine.length > 60) return lastLine.slice(0, 60) + "…";
|
|
80
|
+
if (lastLine.length > 0) return lastLine;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return "thinking…";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---- Widget ----
|
|
87
|
+
|
|
88
|
+
export class AgentWidget {
|
|
89
|
+
private spinnerFrame = 0;
|
|
90
|
+
private timer?: ReturnType<typeof setInterval>;
|
|
91
|
+
private uiCtx?: ExtensionUIContext;
|
|
92
|
+
private tui?: import("@earendil-works/pi-tui").TUI;
|
|
93
|
+
private widgetRegistered = false;
|
|
94
|
+
/** Last content key — skips requestRender when only spinner changed. */
|
|
95
|
+
private lastContentKey = "";
|
|
96
|
+
|
|
97
|
+
/** Tracks how many turns each finished agent has survived. */
|
|
98
|
+
private finishedTurnAge = new Map<string, number>();
|
|
99
|
+
/** How many extra turns error/aborted agents linger. */
|
|
100
|
+
private static readonly ERROR_LINGER_TURNS = 2;
|
|
101
|
+
/** Last status bar text for dedup. */
|
|
102
|
+
private lastStatusText: string | undefined;
|
|
103
|
+
|
|
104
|
+
constructor(
|
|
105
|
+
private manager: AgentManager,
|
|
106
|
+
private activity: Map<string, AgentActivity>,
|
|
107
|
+
) {}
|
|
108
|
+
|
|
109
|
+
setUICtx(ctx: ExtensionUIContext) {
|
|
110
|
+
if (ctx !== this.uiCtx) {
|
|
111
|
+
this.uiCtx = ctx;
|
|
112
|
+
this.widgetRegistered = false;
|
|
113
|
+
this.tui = undefined;
|
|
114
|
+
this.lastStatusText = undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Called on each new turn (tool_execution_start).
|
|
120
|
+
* Ages finished agents and clears those that have lingered long enough.
|
|
121
|
+
*/
|
|
122
|
+
onTurnStart() {
|
|
123
|
+
for (const [id, age] of this.finishedTurnAge) {
|
|
124
|
+
this.finishedTurnAge.set(id, age + 1);
|
|
125
|
+
}
|
|
126
|
+
this.update();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
ensureTimer() {
|
|
130
|
+
if (this.timer) return;
|
|
131
|
+
this.timer = setInterval(() => {
|
|
132
|
+
this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER.length;
|
|
133
|
+
if (this.lastContentKey) this.triggerRender();
|
|
134
|
+
}, 80);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Record an agent as finished (call when agent completes). */
|
|
138
|
+
markFinished(agentId: string) {
|
|
139
|
+
if (!this.finishedTurnAge.has(agentId)) {
|
|
140
|
+
this.finishedTurnAge.set(agentId, 0);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
update() {
|
|
145
|
+
this.triggerRender();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Check if a finished agent should still be shown. */
|
|
149
|
+
private shouldShowFinished(agentId: string, status: string): boolean {
|
|
150
|
+
const age = this.finishedTurnAge.get(agentId) ?? 0;
|
|
151
|
+
const maxAge = ERROR_STATUSES.has(status) ? AgentWidget.ERROR_LINGER_TURNS : 1;
|
|
152
|
+
return age < maxAge;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Build a content key capturing what's actually visible.
|
|
157
|
+
* Excludes spinner — spinner-only changes skip requestRender.
|
|
158
|
+
*/
|
|
159
|
+
private buildContentKey(): string {
|
|
160
|
+
const allAgents = this.manager.listAgents();
|
|
161
|
+
const parts: string[] = [];
|
|
162
|
+
for (const a of allAgents) {
|
|
163
|
+
if (a.status === "running" || a.status === "queued") {
|
|
164
|
+
const act = this.activity.get(a.id);
|
|
165
|
+
parts.push(
|
|
166
|
+
`${a.id}:${a.status}:${a.toolUses}:${act?.turnCount ?? 0}:${act?.tokens ?? ""}:${describeActivity(act?.activeTools ?? new Map(), act?.responseText ?? "")}`,
|
|
167
|
+
);
|
|
168
|
+
} else if (a.completedAt && this.shouldShowFinished(a.id, a.status)) {
|
|
169
|
+
parts.push(`${a.id}:${a.status}:${a.toolUses}:finished`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return parts.join("|");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private triggerRender() {
|
|
176
|
+
if (!this.uiCtx) return;
|
|
177
|
+
|
|
178
|
+
const contentKey = this.buildContentKey();
|
|
179
|
+
const hasContent = contentKey.length > 0;
|
|
180
|
+
|
|
181
|
+
// Nothing to show — clear widget
|
|
182
|
+
if (!hasContent) {
|
|
183
|
+
if (this.widgetRegistered) {
|
|
184
|
+
this.uiCtx.setWidget("unipi-agents", undefined);
|
|
185
|
+
this.widgetRegistered = false;
|
|
186
|
+
this.tui = undefined;
|
|
187
|
+
this.lastContentKey = "";
|
|
188
|
+
}
|
|
189
|
+
if (this.lastStatusText !== undefined) {
|
|
190
|
+
this.uiCtx.setStatus?.("subagents", undefined);
|
|
191
|
+
this.lastStatusText = undefined;
|
|
192
|
+
}
|
|
193
|
+
// Clean up stale finished entries
|
|
194
|
+
const allAgents = this.manager.listAgents();
|
|
195
|
+
for (const [id] of this.finishedTurnAge) {
|
|
196
|
+
if (!allAgents.some((a) => a.id === id)) this.finishedTurnAge.delete(id);
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Status bar
|
|
202
|
+
this.updateStatusBar();
|
|
203
|
+
|
|
204
|
+
// Register widget callback once
|
|
205
|
+
if (!this.widgetRegistered) {
|
|
206
|
+
this.uiCtx.setWidget(
|
|
207
|
+
"unipi-agents",
|
|
208
|
+
(tui: any, theme: any) => {
|
|
209
|
+
this.tui = tui;
|
|
210
|
+
return {
|
|
211
|
+
render: (width: number) => this.renderWidget(tui, theme, width),
|
|
212
|
+
invalidate: () => {
|
|
213
|
+
this.widgetRegistered = false;
|
|
214
|
+
this.tui = undefined;
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
},
|
|
218
|
+
{ placement: "aboveEditor" },
|
|
219
|
+
);
|
|
220
|
+
this.widgetRegistered = true;
|
|
221
|
+
this.lastContentKey = "";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Only request render when content actually changed
|
|
225
|
+
if (contentKey !== this.lastContentKey) {
|
|
226
|
+
this.lastContentKey = contentKey;
|
|
227
|
+
this.tui?.requestRender?.();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private updateStatusBar() {
|
|
232
|
+
if (!this.uiCtx?.setStatus) return;
|
|
233
|
+
const allAgents = this.manager.listAgents();
|
|
234
|
+
let runningCount = 0;
|
|
235
|
+
let queuedCount = 0;
|
|
236
|
+
for (const a of allAgents) {
|
|
237
|
+
if (a.status === "running") runningCount++;
|
|
238
|
+
else if (a.status === "queued") queuedCount++;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let newStatusText: string | undefined;
|
|
242
|
+
if (runningCount > 0 || queuedCount > 0) {
|
|
243
|
+
const parts: string[] = [];
|
|
244
|
+
if (runningCount > 0) parts.push(`${runningCount} running`);
|
|
245
|
+
if (queuedCount > 0) parts.push(`${queuedCount} queued`);
|
|
246
|
+
const total = runningCount + queuedCount;
|
|
247
|
+
newStatusText = `${parts.join(", ")} agent${total === 1 ? "" : "s"}`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (newStatusText !== this.lastStatusText) {
|
|
251
|
+
this.uiCtx.setStatus("subagents", newStatusText);
|
|
252
|
+
this.lastStatusText = newStatusText;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Render a finished agent line. */
|
|
257
|
+
private renderFinishedLine(
|
|
258
|
+
a: { id: string; type: string; status: string; description: string; toolUses: number; startedAt: number; completedAt?: number; error?: string },
|
|
259
|
+
theme: any,
|
|
260
|
+
w: number,
|
|
261
|
+
): string {
|
|
262
|
+
const duration = formatMs((a.completedAt ?? Date.now()) - a.startedAt);
|
|
263
|
+
|
|
264
|
+
let icon: string;
|
|
265
|
+
let statusText: string;
|
|
266
|
+
if (a.status === "completed") {
|
|
267
|
+
icon = theme.fg("success", "✓");
|
|
268
|
+
statusText = "";
|
|
269
|
+
} else if (a.status === "stopped") {
|
|
270
|
+
icon = theme.fg("dim", "■");
|
|
271
|
+
statusText = theme.fg("dim", " stopped");
|
|
272
|
+
} else if (a.status === "error") {
|
|
273
|
+
icon = theme.fg("error", "✗");
|
|
274
|
+
const errMsg = a.error ? `: ${a.error.slice(0, 40)}` : "";
|
|
275
|
+
statusText = theme.fg("error", ` error${errMsg}`);
|
|
276
|
+
} else {
|
|
277
|
+
// aborted
|
|
278
|
+
icon = theme.fg("error", "✗");
|
|
279
|
+
statusText = theme.fg("warning", " aborted");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const parts: string[] = [];
|
|
283
|
+
const act = this.activity.get(a.id);
|
|
284
|
+
if (act) parts.push(formatTurns(act.turnCount, act.maxTurns));
|
|
285
|
+
if (a.toolUses > 0) parts.push(`${a.toolUses} tool use${a.toolUses === 1 ? "" : "s"}`);
|
|
286
|
+
parts.push(duration);
|
|
287
|
+
|
|
288
|
+
const line =
|
|
289
|
+
theme.fg("dim", "├─") +
|
|
290
|
+
" " +
|
|
291
|
+
icon +
|
|
292
|
+
" " +
|
|
293
|
+
theme.fg("dim", a.type) +
|
|
294
|
+
" " +
|
|
295
|
+
theme.fg("dim", a.description) +
|
|
296
|
+
" " +
|
|
297
|
+
theme.fg("dim", "·") +
|
|
298
|
+
" " +
|
|
299
|
+
theme.fg("dim", parts.join(" · ")) +
|
|
300
|
+
statusText;
|
|
301
|
+
|
|
302
|
+
return truncateToWidth(line, w);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private renderWidget(tui: any, theme: any, width?: number): string[] {
|
|
306
|
+
const allAgents = this.manager.listAgents();
|
|
307
|
+
const running = allAgents.filter((a) => a.status === "running");
|
|
308
|
+
const queued = allAgents.filter((a) => a.status === "queued");
|
|
309
|
+
const finished = allAgents.filter(
|
|
310
|
+
(a) =>
|
|
311
|
+
a.status !== "running" &&
|
|
312
|
+
a.status !== "queued" &&
|
|
313
|
+
a.completedAt &&
|
|
314
|
+
this.shouldShowFinished(a.id, a.status),
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const hasActive = running.length > 0 || queued.length > 0;
|
|
318
|
+
const hasFinished = finished.length > 0;
|
|
319
|
+
if (!hasActive && !hasFinished) return [];
|
|
320
|
+
|
|
321
|
+
const w = width ?? tui.terminal?.columns ?? 80;
|
|
322
|
+
const frame = SPINNER[this.spinnerFrame % SPINNER.length];
|
|
323
|
+
const headingColor = hasActive ? "accent" : "dim";
|
|
324
|
+
const headingIcon = hasActive ? "●" : "○";
|
|
325
|
+
|
|
326
|
+
// Build sections: finished (1 line each), running (2 lines each), queued (1 line)
|
|
327
|
+
const finishedLines: string[] = finished.map((a) => this.renderFinishedLine(a, theme, w));
|
|
328
|
+
|
|
329
|
+
const runningLines: string[][] = running.map((a) => {
|
|
330
|
+
const act = this.activity.get(a.id);
|
|
331
|
+
const toolCount = a.toolUses;
|
|
332
|
+
const tokens = act?.tokens ?? "";
|
|
333
|
+
const elapsed = formatMs(Date.now() - a.startedAt);
|
|
334
|
+
const activity = act ? describeActivity(act.activeTools, act.responseText) : "starting…";
|
|
335
|
+
|
|
336
|
+
const parts: string[] = [];
|
|
337
|
+
if (act?.turnCount) parts.push(formatTurns(act.turnCount, act.maxTurns));
|
|
338
|
+
if (toolCount > 0) parts.push(`${toolCount} tool use${toolCount === 1 ? "" : "s"}`);
|
|
339
|
+
if (tokens) parts.push(tokens);
|
|
340
|
+
parts.push(elapsed);
|
|
341
|
+
|
|
342
|
+
return [
|
|
343
|
+
truncateToWidth(
|
|
344
|
+
theme.fg("dim", "├─") +
|
|
345
|
+
` ${theme.fg("accent", frame)} ${theme.bold(a.type)} ${theme.fg("dim", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}`,
|
|
346
|
+
w,
|
|
347
|
+
),
|
|
348
|
+
truncateToWidth(theme.fg("dim", "│ ") + theme.fg("dim", ` ⎿ ${activity}`), w),
|
|
349
|
+
];
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const queuedLine =
|
|
353
|
+
queued.length > 0
|
|
354
|
+
? truncateToWidth(
|
|
355
|
+
theme.fg("dim", "├─") + ` ${theme.fg("muted", "◦")} ${theme.fg("dim", `${queued.length} queued`)}`,
|
|
356
|
+
w,
|
|
357
|
+
)
|
|
358
|
+
: undefined;
|
|
359
|
+
|
|
360
|
+
// Assemble with overflow cap
|
|
361
|
+
const maxBody = MAX_WIDGET_LINES - 1; // heading takes 1 line
|
|
362
|
+
const totalBody = finishedLines.length + runningLines.length * 2 + (queuedLine ? 1 : 0);
|
|
363
|
+
|
|
364
|
+
const lines: string[] = [truncateToWidth(theme.fg(headingColor, headingIcon) + " " + theme.fg(headingColor, "Agents"), w)];
|
|
365
|
+
|
|
366
|
+
if (totalBody <= maxBody) {
|
|
367
|
+
// Everything fits
|
|
368
|
+
lines.push(...finishedLines);
|
|
369
|
+
for (const pair of runningLines) lines.push(...pair);
|
|
370
|
+
if (queuedLine) lines.push(queuedLine);
|
|
371
|
+
|
|
372
|
+
// Fix last connector: ├─ → └─
|
|
373
|
+
if (lines.length > 1) {
|
|
374
|
+
const last = lines.length - 1;
|
|
375
|
+
lines[last] = lines[last].replace("├─", "└─");
|
|
376
|
+
if (runningLines.length > 0 && !queuedLine && last >= 2) {
|
|
377
|
+
lines[last - 1] = lines[last - 1].replace("├─", "└─");
|
|
378
|
+
lines[last] = lines[last].replace("│ ", " ");
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
// Overflow — prioritize: running > queued > finished
|
|
383
|
+
let budget = maxBody - 1; // reserve 1 for overflow indicator
|
|
384
|
+
let hiddenRunning = 0;
|
|
385
|
+
let hiddenFinished = 0;
|
|
386
|
+
|
|
387
|
+
// 1. Running agents (2 lines each)
|
|
388
|
+
for (const pair of runningLines) {
|
|
389
|
+
if (budget >= 2) {
|
|
390
|
+
lines.push(...pair);
|
|
391
|
+
budget -= 2;
|
|
392
|
+
} else {
|
|
393
|
+
hiddenRunning++;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// 2. Queued
|
|
398
|
+
if (queuedLine && budget >= 1) {
|
|
399
|
+
lines.push(queuedLine);
|
|
400
|
+
budget--;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// 3. Finished
|
|
404
|
+
for (const fl of finishedLines) {
|
|
405
|
+
if (budget >= 1) {
|
|
406
|
+
lines.push(fl);
|
|
407
|
+
budget--;
|
|
408
|
+
} else {
|
|
409
|
+
hiddenFinished++;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Overflow summary
|
|
414
|
+
const overflowParts: string[] = [];
|
|
415
|
+
if (hiddenRunning > 0) overflowParts.push(`${hiddenRunning} running`);
|
|
416
|
+
if (hiddenFinished > 0) overflowParts.push(`${hiddenFinished} finished`);
|
|
417
|
+
if (overflowParts.length > 0) {
|
|
418
|
+
lines.push(
|
|
419
|
+
truncateToWidth(
|
|
420
|
+
theme.fg("dim", "└─") + ` ${theme.fg("dim", `+${hiddenRunning + hiddenFinished} more (${overflowParts.join(", ")})`)}`,
|
|
421
|
+
w,
|
|
422
|
+
),
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return lines;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
dispose() {
|
|
431
|
+
if (this.timer) {
|
|
432
|
+
clearInterval(this.timer);
|
|
433
|
+
this.timer = undefined;
|
|
434
|
+
}
|
|
435
|
+
if (this.uiCtx) {
|
|
436
|
+
if (this.widgetRegistered) {
|
|
437
|
+
this.uiCtx.setWidget("unipi-agents", undefined);
|
|
438
|
+
}
|
|
439
|
+
if (this.lastStatusText !== undefined) {
|
|
440
|
+
this.uiCtx.setStatus?.("subagents", undefined);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
this.widgetRegistered = false;
|
|
444
|
+
this.tui = undefined;
|
|
445
|
+
this.lastStatusText = undefined;
|
|
446
|
+
}
|
|
447
|
+
}
|