@tintinweb/pi-subagents 0.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/CHANGELOG.md +81 -0
- package/LICENSE +21 -0
- package/README.md +223 -0
- package/package.json +46 -0
- package/src/agent-manager.ts +287 -0
- package/src/agent-runner.ts +341 -0
- package/src/agent-types.ts +137 -0
- package/src/context.ts +58 -0
- package/src/custom-agents.ts +94 -0
- package/src/env.ts +33 -0
- package/src/index.ts +855 -0
- package/src/prompts.ts +163 -0
- package/src/types.ts +84 -0
- package/src/ui/agent-widget.ts +326 -0
package/src/prompts.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompts.ts — System prompts per agent type.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { EnvInfo } from "./types.js";
|
|
6
|
+
|
|
7
|
+
// ---- Reusable prompt blocks ----
|
|
8
|
+
|
|
9
|
+
const READ_ONLY_PROHIBITION = `You are STRICTLY PROHIBITED from:
|
|
10
|
+
- Creating new files
|
|
11
|
+
- Modifying existing files
|
|
12
|
+
- Deleting files
|
|
13
|
+
- Moving or copying files
|
|
14
|
+
- Creating temporary files anywhere, including /tmp
|
|
15
|
+
- Using redirect operators (>, >>, |) or heredocs to write to files
|
|
16
|
+
- Running ANY commands that change system state`;
|
|
17
|
+
|
|
18
|
+
const READ_ONLY_TOOLS = `# Tool Usage
|
|
19
|
+
- Use the find tool for file pattern matching (NOT the bash find command)
|
|
20
|
+
- Use the grep tool for content search (NOT bash grep/rg command)
|
|
21
|
+
- Use the read tool for reading files (NOT bash cat/head/tail)
|
|
22
|
+
- Use Bash ONLY for read-only operations`;
|
|
23
|
+
|
|
24
|
+
const FULL_TOOL_USAGE = `# Tool Usage
|
|
25
|
+
- Use the read tool instead of cat/head/tail
|
|
26
|
+
- Use the edit tool instead of sed/awk
|
|
27
|
+
- Use the write tool instead of echo/heredoc
|
|
28
|
+
- Use the find tool instead of bash find/ls for file search
|
|
29
|
+
- Use the grep tool instead of bash grep/rg for content search
|
|
30
|
+
- Make independent tool calls in parallel`;
|
|
31
|
+
|
|
32
|
+
const GIT_SAFETY = `# Git Safety
|
|
33
|
+
- NEVER update git config
|
|
34
|
+
- NEVER run destructive git commands (push --force, reset --hard, checkout ., restore ., clean -f, branch -D) without explicit request
|
|
35
|
+
- NEVER skip hooks (--no-verify, --no-gpg-sign) unless explicitly asked
|
|
36
|
+
- NEVER force push to main/master — warn the user if they request it
|
|
37
|
+
- Always create NEW commits, never amend existing ones. When a pre-commit hook fails, the commit did NOT happen — so --amend would modify the PREVIOUS commit. Fix the issue, re-stage, and create a NEW commit
|
|
38
|
+
- Stage specific files by name, not git add -A or git add .
|
|
39
|
+
- NEVER commit changes unless the user explicitly asks
|
|
40
|
+
- NEVER push unless the user explicitly asks
|
|
41
|
+
- NEVER use git commands with the -i flag (like git rebase -i or git add -i) — they require interactive input
|
|
42
|
+
- Do not use --no-edit with git rebase commands
|
|
43
|
+
- Do not commit files that likely contain secrets (.env, credentials.json, etc); warn the user if they request it`;
|
|
44
|
+
|
|
45
|
+
const OUTPUT_RULES = `# Output
|
|
46
|
+
- Use absolute file paths
|
|
47
|
+
- Do not use emojis
|
|
48
|
+
- Be concise but complete`;
|
|
49
|
+
|
|
50
|
+
export function buildSystemPrompt(type: string, cwd: string, env: EnvInfo): string {
|
|
51
|
+
const commonHeader = `You are a pi coding agent sub-agent.
|
|
52
|
+
You have been invoked to handle a specific task autonomously.
|
|
53
|
+
|
|
54
|
+
# Environment
|
|
55
|
+
Working directory: ${cwd}
|
|
56
|
+
${env.isGitRepo ? `Git repository: yes\nBranch: ${env.branch}` : "Not a git repository"}
|
|
57
|
+
Platform: ${env.platform}`;
|
|
58
|
+
|
|
59
|
+
switch (type) {
|
|
60
|
+
case "Explore":
|
|
61
|
+
return `${commonHeader}
|
|
62
|
+
|
|
63
|
+
# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
|
|
64
|
+
You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
|
|
65
|
+
Your role is EXCLUSIVELY to search and analyze existing code. You do NOT have access to file editing tools.
|
|
66
|
+
|
|
67
|
+
${READ_ONLY_PROHIBITION}
|
|
68
|
+
|
|
69
|
+
Use Bash ONLY for read-only operations: ls, git status, git log, git diff, find, cat, head, tail.
|
|
70
|
+
|
|
71
|
+
${READ_ONLY_TOOLS}
|
|
72
|
+
- Make independent tool calls in parallel for efficiency
|
|
73
|
+
- Adapt search approach based on thoroughness level specified
|
|
74
|
+
|
|
75
|
+
# Output
|
|
76
|
+
- Use absolute file paths in all references
|
|
77
|
+
- Report findings as regular messages
|
|
78
|
+
- Do not use emojis
|
|
79
|
+
- Be thorough and precise`;
|
|
80
|
+
|
|
81
|
+
case "Plan":
|
|
82
|
+
return `${commonHeader}
|
|
83
|
+
|
|
84
|
+
# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
|
|
85
|
+
You are a software architect and planning specialist.
|
|
86
|
+
Your role is EXCLUSIVELY to explore the codebase and design implementation plans.
|
|
87
|
+
You do NOT have access to file editing tools — attempting to edit files will fail.
|
|
88
|
+
|
|
89
|
+
${READ_ONLY_PROHIBITION}
|
|
90
|
+
|
|
91
|
+
# Planning Process
|
|
92
|
+
1. Understand requirements
|
|
93
|
+
2. Explore thoroughly (read files, find patterns, understand architecture)
|
|
94
|
+
3. Design solution based on your assigned perspective
|
|
95
|
+
4. Detail the plan with step-by-step implementation strategy
|
|
96
|
+
|
|
97
|
+
# Requirements
|
|
98
|
+
- Consider trade-offs and architectural decisions
|
|
99
|
+
- Identify dependencies and sequencing
|
|
100
|
+
- Anticipate potential challenges
|
|
101
|
+
- Follow existing patterns where appropriate
|
|
102
|
+
|
|
103
|
+
${READ_ONLY_TOOLS}
|
|
104
|
+
|
|
105
|
+
# Output Format
|
|
106
|
+
- Use absolute file paths
|
|
107
|
+
- Do not use emojis
|
|
108
|
+
- End your response with:
|
|
109
|
+
|
|
110
|
+
### Critical Files for Implementation
|
|
111
|
+
List 3-5 files most critical for implementing this plan:
|
|
112
|
+
- /absolute/path/to/file.ts - [Brief reason]`;
|
|
113
|
+
|
|
114
|
+
case "general-purpose":
|
|
115
|
+
return `${commonHeader}
|
|
116
|
+
|
|
117
|
+
# Role
|
|
118
|
+
You are a general-purpose coding agent for complex, multi-step tasks.
|
|
119
|
+
You have full access to read, write, edit files, and execute commands.
|
|
120
|
+
Do what has been asked; nothing more, nothing less.
|
|
121
|
+
|
|
122
|
+
${FULL_TOOL_USAGE}
|
|
123
|
+
|
|
124
|
+
# File Operations
|
|
125
|
+
- NEVER create files unless absolutely necessary
|
|
126
|
+
- Prefer editing existing files over creating new ones
|
|
127
|
+
- NEVER create documentation files unless explicitly requested
|
|
128
|
+
|
|
129
|
+
${GIT_SAFETY}
|
|
130
|
+
|
|
131
|
+
${OUTPUT_RULES}`;
|
|
132
|
+
|
|
133
|
+
case "statusline-setup":
|
|
134
|
+
return `${commonHeader}
|
|
135
|
+
|
|
136
|
+
# Role
|
|
137
|
+
You configure settings. You can read and edit files only.
|
|
138
|
+
Focus on the specific configuration task requested.
|
|
139
|
+
Use absolute file paths.`;
|
|
140
|
+
|
|
141
|
+
case "claude-code-guide":
|
|
142
|
+
return `${commonHeader}
|
|
143
|
+
|
|
144
|
+
# Role
|
|
145
|
+
You help answer questions about the tool, its features, and capabilities.
|
|
146
|
+
Search documentation, read config files, and provide accurate answers.
|
|
147
|
+
You have read-only access to the codebase for reference.
|
|
148
|
+
Use absolute file paths.`;
|
|
149
|
+
|
|
150
|
+
default:
|
|
151
|
+
// Custom agents or unknown: general-purpose base without git safety / file ops
|
|
152
|
+
return `${commonHeader}
|
|
153
|
+
|
|
154
|
+
# Role
|
|
155
|
+
You are a general-purpose coding agent for complex, multi-step tasks.
|
|
156
|
+
You have full access to read, write, edit files, and execute commands.
|
|
157
|
+
Do what has been asked; nothing more, nothing less.
|
|
158
|
+
|
|
159
|
+
${FULL_TOOL_USAGE}
|
|
160
|
+
|
|
161
|
+
${OUTPUT_RULES}`;
|
|
162
|
+
}
|
|
163
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* types.ts — Type definitions for the subagent system.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
|
|
7
|
+
|
|
8
|
+
export type { ThinkingLevel };
|
|
9
|
+
|
|
10
|
+
/** Built-in agent types. Custom agents use arbitrary string names. */
|
|
11
|
+
export type BuiltinSubagentType = "general-purpose" | "Explore" | "Plan" | "statusline-setup" | "claude-code-guide";
|
|
12
|
+
|
|
13
|
+
/** Agent type: built-in or custom (any string). */
|
|
14
|
+
export type SubagentType = BuiltinSubagentType | (string & {});
|
|
15
|
+
|
|
16
|
+
/** Display name mapping for built-in types. */
|
|
17
|
+
export const DISPLAY_NAMES: Record<BuiltinSubagentType, string> = {
|
|
18
|
+
"general-purpose": "Agent",
|
|
19
|
+
"Explore": "Explore",
|
|
20
|
+
"Plan": "Plan",
|
|
21
|
+
"statusline-setup": "Config",
|
|
22
|
+
"claude-code-guide": "Guide",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const SUBAGENT_TYPES: BuiltinSubagentType[] = [
|
|
26
|
+
"general-purpose",
|
|
27
|
+
"Explore",
|
|
28
|
+
"Plan",
|
|
29
|
+
"statusline-setup",
|
|
30
|
+
"claude-code-guide",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export interface SubagentTypeConfig {
|
|
34
|
+
displayName: string;
|
|
35
|
+
description: string;
|
|
36
|
+
builtinToolNames: string[];
|
|
37
|
+
/** true = inherit all, string[] = only listed, false = none */
|
|
38
|
+
extensions: true | string[] | false;
|
|
39
|
+
/** true = inherit all, string[] = only listed, false = none */
|
|
40
|
+
skills: true | string[] | false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Configuration for a custom agent loaded from .pi/agents/<name>.md */
|
|
44
|
+
export interface CustomAgentConfig {
|
|
45
|
+
name: string;
|
|
46
|
+
description: string;
|
|
47
|
+
builtinToolNames: string[];
|
|
48
|
+
/** true = inherit all, string[] = only listed, false = none */
|
|
49
|
+
extensions: true | string[] | false;
|
|
50
|
+
/** true = inherit all, string[] = only listed, false = none */
|
|
51
|
+
skills: true | string[] | false;
|
|
52
|
+
model?: string;
|
|
53
|
+
thinking?: ThinkingLevel;
|
|
54
|
+
maxTurns?: number;
|
|
55
|
+
systemPrompt: string;
|
|
56
|
+
promptMode: "replace" | "append";
|
|
57
|
+
/** Default for spawn: fork parent conversation */
|
|
58
|
+
inheritContext: boolean;
|
|
59
|
+
/** Default for spawn: run in background */
|
|
60
|
+
runInBackground: boolean;
|
|
61
|
+
/** Default for spawn: no extension tools */
|
|
62
|
+
isolated: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface AgentRecord {
|
|
66
|
+
id: string;
|
|
67
|
+
type: SubagentType;
|
|
68
|
+
description: string;
|
|
69
|
+
status: "queued" | "running" | "completed" | "steered" | "aborted" | "stopped" | "error";
|
|
70
|
+
result?: string;
|
|
71
|
+
error?: string;
|
|
72
|
+
toolUses: number;
|
|
73
|
+
startedAt: number;
|
|
74
|
+
completedAt?: number;
|
|
75
|
+
session?: AgentSession;
|
|
76
|
+
abortController?: AbortController;
|
|
77
|
+
promise?: Promise<string>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface EnvInfo {
|
|
81
|
+
isGitRepo: boolean;
|
|
82
|
+
branch: string;
|
|
83
|
+
platform: string;
|
|
84
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-widget.ts — Persistent widget showing running/completed agents above the editor.
|
|
3
|
+
*
|
|
4
|
+
* Displays a tree of agents with animated spinners, live stats, and activity descriptions.
|
|
5
|
+
* Uses the callback form of setWidget for themed rendering.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AgentManager } from "../agent-manager.js";
|
|
9
|
+
import type { SubagentType } from "../types.js";
|
|
10
|
+
import { getConfig } from "../agent-types.js";
|
|
11
|
+
|
|
12
|
+
// ---- Constants ----
|
|
13
|
+
|
|
14
|
+
/** Braille spinner frames for animated running indicator. */
|
|
15
|
+
export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
16
|
+
|
|
17
|
+
/** Statuses that indicate an error/non-success outcome (used for linger behavior and icon rendering). */
|
|
18
|
+
export const ERROR_STATUSES = new Set(["error", "aborted", "steered", "stopped"]);
|
|
19
|
+
|
|
20
|
+
/** Tool name → human-readable action for activity descriptions. */
|
|
21
|
+
const TOOL_DISPLAY: Record<string, string> = {
|
|
22
|
+
read: "reading",
|
|
23
|
+
bash: "running command",
|
|
24
|
+
edit: "editing",
|
|
25
|
+
write: "writing",
|
|
26
|
+
grep: "searching",
|
|
27
|
+
find: "finding files",
|
|
28
|
+
ls: "listing",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ---- Types ----
|
|
32
|
+
|
|
33
|
+
export type Theme = {
|
|
34
|
+
fg(color: string, text: string): string;
|
|
35
|
+
bold(text: string): string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type UICtx = {
|
|
39
|
+
setStatus(key: string, text: string | undefined): void;
|
|
40
|
+
setWidget(
|
|
41
|
+
key: string,
|
|
42
|
+
content: undefined | ((tui: any, theme: Theme) => { render(): string[]; invalidate(): void }),
|
|
43
|
+
options?: { placement?: "aboveEditor" | "belowEditor" },
|
|
44
|
+
): void;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Per-agent live activity state. */
|
|
48
|
+
export interface AgentActivity {
|
|
49
|
+
activeTools: Map<string, string>;
|
|
50
|
+
toolUses: number;
|
|
51
|
+
tokens: string;
|
|
52
|
+
responseText: string;
|
|
53
|
+
session?: { getSessionStats(): { tokens: { total: number } } };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Metadata attached to Agent tool results for custom rendering. */
|
|
57
|
+
export interface AgentDetails {
|
|
58
|
+
displayName: string;
|
|
59
|
+
description: string;
|
|
60
|
+
subagentType: string;
|
|
61
|
+
toolUses: number;
|
|
62
|
+
tokens: string;
|
|
63
|
+
durationMs: number;
|
|
64
|
+
status: "queued" | "running" | "completed" | "steered" | "aborted" | "stopped" | "error" | "background";
|
|
65
|
+
/** Human-readable description of what the agent is currently doing. */
|
|
66
|
+
activity?: string;
|
|
67
|
+
/** Current spinner frame index (for animated running indicator). */
|
|
68
|
+
spinnerFrame?: number;
|
|
69
|
+
/** Short model name if different from parent (e.g. "haiku", "sonnet"). */
|
|
70
|
+
modelName?: string;
|
|
71
|
+
/** Notable config tags (e.g. ["thinking: high", "isolated"]). */
|
|
72
|
+
tags?: string[];
|
|
73
|
+
agentId?: string;
|
|
74
|
+
error?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---- Formatting helpers ----
|
|
78
|
+
|
|
79
|
+
/** Format a token count as "33.8k tokens" or "1.2M tokens". */
|
|
80
|
+
export function formatTokens(count: number): string {
|
|
81
|
+
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M tokens`;
|
|
82
|
+
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k tokens`;
|
|
83
|
+
return `${count} tokens`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Format milliseconds as human-readable duration. */
|
|
87
|
+
export function formatMs(ms: number): string {
|
|
88
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Format duration from start/completed timestamps. */
|
|
92
|
+
export function formatDuration(startedAt: number, completedAt?: number): string {
|
|
93
|
+
if (completedAt) return formatMs(completedAt - startedAt);
|
|
94
|
+
return `${formatMs(Date.now() - startedAt)} (running)`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Get display name for any agent type (built-in or custom). */
|
|
98
|
+
export function getDisplayName(type: SubagentType): string {
|
|
99
|
+
return getConfig(type).displayName;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Truncate text to a single line, max `len` chars. */
|
|
103
|
+
function truncateLine(text: string, len = 60): string {
|
|
104
|
+
const line = text.split("\n").find(l => l.trim())?.trim() ?? "";
|
|
105
|
+
if (line.length <= len) return line;
|
|
106
|
+
return line.slice(0, len) + "…";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Build a human-readable activity string from currently-running tools or response text. */
|
|
110
|
+
export function describeActivity(activeTools: Map<string, string>, responseText?: string): string {
|
|
111
|
+
if (activeTools.size > 0) {
|
|
112
|
+
const groups = new Map<string, number>();
|
|
113
|
+
for (const toolName of activeTools.values()) {
|
|
114
|
+
const action = TOOL_DISPLAY[toolName] ?? toolName;
|
|
115
|
+
groups.set(action, (groups.get(action) ?? 0) + 1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const parts: string[] = [];
|
|
119
|
+
for (const [action, count] of groups) {
|
|
120
|
+
if (count > 1) {
|
|
121
|
+
parts.push(`${action} ${count} ${action === "searching" ? "patterns" : "files"}`);
|
|
122
|
+
} else {
|
|
123
|
+
parts.push(action);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return parts.join(", ") + "…";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// No tools active — show truncated response text if available
|
|
130
|
+
if (responseText && responseText.trim().length > 0) {
|
|
131
|
+
return truncateLine(responseText);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return "thinking…";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---- Widget manager ----
|
|
138
|
+
|
|
139
|
+
export class AgentWidget {
|
|
140
|
+
private uiCtx: UICtx | undefined;
|
|
141
|
+
private widgetFrame = 0;
|
|
142
|
+
private widgetInterval: ReturnType<typeof setInterval> | undefined;
|
|
143
|
+
/** Tracks how many turns each finished agent has survived. Key: agent ID, Value: turns since finished. */
|
|
144
|
+
private finishedTurnAge = new Map<string, number>();
|
|
145
|
+
/** How many extra turns errors/aborted agents linger (completed agents clear after 1 turn). */
|
|
146
|
+
private static readonly ERROR_LINGER_TURNS = 2;
|
|
147
|
+
|
|
148
|
+
constructor(
|
|
149
|
+
private manager: AgentManager,
|
|
150
|
+
private agentActivity: Map<string, AgentActivity>,
|
|
151
|
+
) {}
|
|
152
|
+
|
|
153
|
+
/** Set the UI context (grabbed from first tool execution). */
|
|
154
|
+
setUICtx(ctx: UICtx) {
|
|
155
|
+
this.uiCtx = ctx;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Called on each new turn (tool_execution_start).
|
|
160
|
+
* Ages finished agents and clears those that have lingered long enough.
|
|
161
|
+
*/
|
|
162
|
+
onTurnStart() {
|
|
163
|
+
// Age all finished agents
|
|
164
|
+
for (const [id, age] of this.finishedTurnAge) {
|
|
165
|
+
this.finishedTurnAge.set(id, age + 1);
|
|
166
|
+
}
|
|
167
|
+
// Trigger a widget refresh (will filter out expired agents)
|
|
168
|
+
this.update();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Ensure the widget update timer is running. */
|
|
172
|
+
ensureTimer() {
|
|
173
|
+
if (!this.widgetInterval) {
|
|
174
|
+
this.widgetInterval = setInterval(() => this.update(), 80);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Check if a finished agent should still be shown in the widget. */
|
|
179
|
+
private shouldShowFinished(agentId: string, status: string): boolean {
|
|
180
|
+
const age = this.finishedTurnAge.get(agentId) ?? 0;
|
|
181
|
+
const maxAge = ERROR_STATUSES.has(status) ? AgentWidget.ERROR_LINGER_TURNS : 1;
|
|
182
|
+
return age < maxAge;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Record an agent as finished (call when agent completes). */
|
|
186
|
+
markFinished(agentId: string) {
|
|
187
|
+
if (!this.finishedTurnAge.has(agentId)) {
|
|
188
|
+
this.finishedTurnAge.set(agentId, 0);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Render a finished agent line. */
|
|
193
|
+
private renderFinishedLine(a: { type: SubagentType; status: string; description: string; toolUses: number; startedAt: number; completedAt?: number; error?: string }, theme: Theme): string {
|
|
194
|
+
const name = getDisplayName(a.type);
|
|
195
|
+
const duration = formatMs((a.completedAt ?? Date.now()) - a.startedAt);
|
|
196
|
+
|
|
197
|
+
let icon: string;
|
|
198
|
+
let statusText: string;
|
|
199
|
+
if (a.status === "completed") {
|
|
200
|
+
icon = theme.fg("success", "✓");
|
|
201
|
+
statusText = "";
|
|
202
|
+
} else if (a.status === "steered") {
|
|
203
|
+
icon = theme.fg("warning", "✓");
|
|
204
|
+
statusText = theme.fg("warning", " (turn limit)");
|
|
205
|
+
} else if (a.status === "stopped") {
|
|
206
|
+
icon = theme.fg("dim", "■");
|
|
207
|
+
statusText = theme.fg("dim", " stopped");
|
|
208
|
+
} else if (a.status === "error") {
|
|
209
|
+
icon = theme.fg("error", "✗");
|
|
210
|
+
const errMsg = a.error ? `: ${a.error.slice(0, 60)}` : "";
|
|
211
|
+
statusText = theme.fg("error", ` error${errMsg}`);
|
|
212
|
+
} else {
|
|
213
|
+
// aborted
|
|
214
|
+
icon = theme.fg("error", "✗");
|
|
215
|
+
statusText = theme.fg("warning", " aborted");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const parts: string[] = [];
|
|
219
|
+
if (a.toolUses > 0) parts.push(`${a.toolUses} tool use${a.toolUses === 1 ? "" : "s"}`);
|
|
220
|
+
parts.push(duration);
|
|
221
|
+
|
|
222
|
+
return `${icon} ${theme.fg("dim", name)} ${theme.fg("dim", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}${statusText}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Force an immediate widget update. */
|
|
226
|
+
update() {
|
|
227
|
+
if (!this.uiCtx) return;
|
|
228
|
+
const allAgents = this.manager.listAgents();
|
|
229
|
+
const running = allAgents.filter(a => a.status === "running");
|
|
230
|
+
const queued = allAgents.filter(a => a.status === "queued");
|
|
231
|
+
const finished = allAgents.filter(a =>
|
|
232
|
+
a.status !== "running" && a.status !== "queued" && a.completedAt
|
|
233
|
+
&& this.shouldShowFinished(a.id, a.status),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const hasActive = running.length > 0 || queued.length > 0;
|
|
237
|
+
const hasFinished = finished.length > 0;
|
|
238
|
+
|
|
239
|
+
// Nothing to show — clear widget
|
|
240
|
+
if (!hasActive && !hasFinished) {
|
|
241
|
+
this.uiCtx.setWidget("agents", undefined);
|
|
242
|
+
this.uiCtx.setStatus("subagents", undefined);
|
|
243
|
+
if (this.widgetInterval) { clearInterval(this.widgetInterval); this.widgetInterval = undefined; }
|
|
244
|
+
// Clean up stale entries
|
|
245
|
+
for (const [id] of this.finishedTurnAge) {
|
|
246
|
+
if (!allAgents.some(a => a.id === id)) this.finishedTurnAge.delete(id);
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Status bar
|
|
252
|
+
if (hasActive) {
|
|
253
|
+
const statusParts: string[] = [];
|
|
254
|
+
if (running.length > 0) statusParts.push(`${running.length} running`);
|
|
255
|
+
if (queued.length > 0) statusParts.push(`${queued.length} queued`);
|
|
256
|
+
const total = running.length + queued.length;
|
|
257
|
+
this.uiCtx.setStatus("subagents", `${statusParts.join(", ")} agent${total === 1 ? "" : "s"}`);
|
|
258
|
+
} else {
|
|
259
|
+
this.uiCtx.setStatus("subagents", undefined);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this.widgetFrame++;
|
|
263
|
+
const frame = SPINNER[this.widgetFrame % SPINNER.length];
|
|
264
|
+
|
|
265
|
+
this.uiCtx.setWidget("agents", (_tui, theme) => {
|
|
266
|
+
const headingColor = hasActive ? "accent" : "dim";
|
|
267
|
+
const headingIcon = hasActive ? "●" : "○";
|
|
268
|
+
const lines: string[] = [theme.fg(headingColor, headingIcon) + " " + theme.fg(headingColor, "Agents")];
|
|
269
|
+
|
|
270
|
+
// --- Finished agents (shown first, dimmed) ---
|
|
271
|
+
for (let i = 0; i < finished.length; i++) {
|
|
272
|
+
const a = finished[i];
|
|
273
|
+
const isLast = !hasActive && i === finished.length - 1;
|
|
274
|
+
const connector = isLast ? "└─" : "├─";
|
|
275
|
+
lines.push(theme.fg("dim", connector) + " " + this.renderFinishedLine(a, theme));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// --- Running agents ---
|
|
279
|
+
const isLastSection = queued.length === 0;
|
|
280
|
+
for (let i = 0; i < running.length; i++) {
|
|
281
|
+
const a = running[i];
|
|
282
|
+
const isLast = isLastSection && i === running.length - 1;
|
|
283
|
+
const connector = isLast ? "└─" : "├─";
|
|
284
|
+
const name = getDisplayName(a.type);
|
|
285
|
+
const elapsed = formatMs(Date.now() - a.startedAt);
|
|
286
|
+
|
|
287
|
+
const bg = this.agentActivity.get(a.id);
|
|
288
|
+
const toolUses = bg?.toolUses ?? a.toolUses;
|
|
289
|
+
let tokenText = "";
|
|
290
|
+
if (bg?.session) {
|
|
291
|
+
try { tokenText = formatTokens(bg.session.getSessionStats().tokens.total); } catch { /* */ }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const parts: string[] = [];
|
|
295
|
+
if (toolUses > 0) parts.push(`${toolUses} tool use${toolUses === 1 ? "" : "s"}`);
|
|
296
|
+
if (tokenText) parts.push(tokenText);
|
|
297
|
+
parts.push(elapsed);
|
|
298
|
+
const statsText = parts.join(" · ");
|
|
299
|
+
|
|
300
|
+
const activity = bg ? describeActivity(bg.activeTools, bg.responseText) : "thinking…";
|
|
301
|
+
|
|
302
|
+
lines.push(theme.fg("dim", connector) + ` ${theme.fg("accent", frame)} ${theme.bold(name)} ${theme.fg("muted", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", statsText)}`);
|
|
303
|
+
const indent = isLast ? " " : "│ ";
|
|
304
|
+
lines.push(theme.fg("dim", indent) + theme.fg("dim", ` ⎿ ${activity}`));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// --- Queued agents (collapsed) ---
|
|
308
|
+
if (queued.length > 0) {
|
|
309
|
+
lines.push(theme.fg("dim", "└─") + ` ${theme.fg("muted", "◦")} ${theme.fg("dim", `${queued.length} queued`)}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return { render: () => lines, invalidate: () => {} };
|
|
313
|
+
}, { placement: "aboveEditor" });
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
dispose() {
|
|
317
|
+
if (this.widgetInterval) {
|
|
318
|
+
clearInterval(this.widgetInterval);
|
|
319
|
+
this.widgetInterval = undefined;
|
|
320
|
+
}
|
|
321
|
+
if (this.uiCtx) {
|
|
322
|
+
this.uiCtx.setWidget("agents", undefined);
|
|
323
|
+
this.uiCtx.setStatus("subagents", undefined);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|