@tintinweb/pi-subagents 0.3.1 → 0.4.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 CHANGED
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.0] - 2026-03-11
9
+
10
+ ### Added
11
+ - **XML-delimited prompt sections** — append-mode agents now wrap inherited content in `<inherited_system_prompt>`, `<sub_agent_context>`, and `<agent_instructions>` XML tags, giving the model explicit structure to distinguish inherited rules from sub-agent-specific instructions. Replace mode is unchanged.
12
+ - **Token count in agent results** — foreground agent results, background completion notifications, and `get_subagent_result` now include the token count alongside tool uses and duration (e.g. `Agent completed in 4.2s (12 tool uses, 33.8k token)`).
13
+ - **Widget overflow cap** — the running agents widget now caps at 12 lines. When exceeded, running agents are prioritized over finished ones and an overflow summary line shows hidden counts (e.g. `+3 more (1 running, 2 finished)`).
14
+
15
+ ### Changed - **changing behavior**
16
+ - **General-purpose agent inherits parent prompt** — the default `general-purpose` agent now uses `promptMode: "append"` with an empty system prompt, making it a "parent twin" that inherits the full parent system prompt (including CLAUDE.md rules, project conventions, and safety guardrails). Previously it used a standalone prompt that duplicated a subset of the parent's rules. Explore and Plan are unchanged (standalone prompts). To customize: eject via `/agents` → select `general-purpose` → Eject, then edit the resulting `.md` file. Set `prompt_mode: replace` to go back to a standalone prompt, or keep `prompt_mode: append` and add extra instructions in the body.
17
+ - **Append-mode agents receive parent system prompt** — `buildAgentPrompt` now accepts the parent's system prompt and threads it into append-mode agents (env header + parent prompt + sub-agent context bridge + optional custom instructions). Replace-mode agents are unchanged.
18
+ - **Prompt pipeline simplified** — removed `systemPromptOverride`/`systemPromptAppend` from `SpawnOptions` and `RunOptions`. These were a separate code path where `index.ts` pre-resolved the prompt mode and passed raw strings into the runner, bypassing `buildAgentPrompt`. Now all prompt assembly flows through `buildAgentPrompt` using the agent's `promptMode` config — one code path, no special cases.
19
+
20
+ ### Removed
21
+ - Deprecated backwards-compat aliases: `registerCustomAgents`, `getCustomAgentConfig`, `getCustomAgentNames` (use `registerAgents`, `getAgentConfig`, `getUserAgentNames`).
22
+ - `resolveCustomPrompt()` helper in index.ts — no longer needed now that prompt routing is config-driven.
23
+
8
24
  ## [0.3.1] - 2026-03-09
9
25
 
10
26
  ### Added
@@ -139,7 +155,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
139
155
  ### Added
140
156
  - **Claude Code-style UI rendering** — `renderCall`/`renderResult`/`onUpdate` for live streaming progress
141
157
  - Live activity descriptions: "searching, reading 3 files…"
142
- - Token count display: "33.8k tokens"
158
+ - Token count display: "33.8k token"
143
159
  - Per-agent tool use counter
144
160
  - Expandable completed results (ctrl+o)
145
161
  - Distinct states: running, background, completed, error, aborted
@@ -172,6 +188,8 @@ Initial release.
172
188
  - **Thinking level** — per-agent extended thinking control
173
189
  - **`/agent` and `/agents` commands**
174
190
 
191
+ [0.4.0]: https://github.com/tintinweb/pi-subagents/compare/v0.3.1...v0.4.0
192
+ [0.3.1]: https://github.com/tintinweb/pi-subagents/compare/v0.3.0...v0.3.1
175
193
  [0.3.0]: https://github.com/tintinweb/pi-subagents/compare/v0.2.7...v0.3.0
176
194
  [0.2.7]: https://github.com/tintinweb/pi-subagents/compare/v0.2.6...v0.2.7
177
195
  [0.2.6]: https://github.com/tintinweb/pi-subagents/compare/v0.2.5...v0.2.6
package/README.md CHANGED
@@ -57,9 +57,9 @@ The extension renders a persistent widget above the editor showing all active ag
57
57
 
58
58
  ```
59
59
  ● Agents
60
- ├─ ⠹ Agent Refactor auth module · 5 tool uses · 33.8k tokens · 12.3s
60
+ ├─ ⠹ Agent Refactor auth module · 5 tool uses · 33.8k token · 12.3s
61
61
  │ ⎿ editing 2 files…
62
- ├─ ⠹ Explore Find auth files · 3 tool uses · 12.4k tokens · 4.1s
62
+ ├─ ⠹ Explore Find auth files · 3 tool uses · 12.4k token · 4.1s
63
63
  │ ⎿ searching…
64
64
  └─ 2 queued
65
65
  ```
@@ -68,24 +68,26 @@ Individual agent results render Claude Code-style in the conversation:
68
68
 
69
69
  | State | Example |
70
70
  |-------|---------|
71
- | **Running** | `⠹ 3 tool uses · 12.4k tokens` / `⎿ searching, reading 3 files…` |
72
- | **Completed** | `✓ 5 tool uses · 33.8k tokens · 12.3s` / `⎿ Done` |
73
- | **Wrapped up** | `✓ 50 tool uses · 89.1k tokens · 45.2s` / `⎿ Wrapped up (turn limit)` |
74
- | **Stopped** | `■ 3 tool uses · 12.4k tokens` / `⎿ Stopped` |
75
- | **Error** | `✗ 3 tool uses · 12.4k tokens` / `⎿ Error: timeout` |
76
- | **Aborted** | `✗ 55 tool uses · 102.3k tokens` / `⎿ Aborted (max turns exceeded)` |
71
+ | **Running** | `⠹ 3 tool uses · 12.4k token` / `⎿ searching, reading 3 files…` |
72
+ | **Completed** | `✓ 5 tool uses · 33.8k token · 12.3s` / `⎿ Done` |
73
+ | **Wrapped up** | `✓ 50 tool uses · 89.1k token · 45.2s` / `⎿ Wrapped up (turn limit)` |
74
+ | **Stopped** | `■ 3 tool uses · 12.4k token` / `⎿ Stopped` |
75
+ | **Error** | `✗ 3 tool uses · 12.4k token` / `⎿ Error: timeout` |
76
+ | **Aborted** | `✗ 55 tool uses · 102.3k token` / `⎿ Aborted (max turns exceeded)` |
77
77
 
78
78
  Completed results can be expanded (ctrl+o in pi) to show the full agent output inline.
79
79
 
80
80
  ## Default Agent Types
81
81
 
82
- | Type | Tools | Model | Description |
83
- |------|-------|-------|-------------|
84
- | `general-purpose` | all 7 | inherit | Full read/write access for complex multi-step tasks |
85
- | `Explore` | read, bash, grep, find, ls | haiku (falls back to inherit) | Fast codebase exploration (read-only) |
86
- | `Plan` | read, bash, grep, find, ls | inherit | Software architect for implementation planning (read-only) |
82
+ | Type | Tools | Model | Prompt Mode | Description |
83
+ |------|-------|-------|-------------|-------------|
84
+ | `general-purpose` | all 7 | inherit | `append` (parent twin) | Inherits the parent's full system prompt — same rules, CLAUDE.md, project conventions |
85
+ | `Explore` | read, bash, grep, find, ls | haiku (falls back to inherit) | `replace` (standalone) | Fast codebase exploration (read-only) |
86
+ | `Plan` | read, bash, grep, find, ls | inherit | `replace` (standalone) | Software architect for implementation planning (read-only) |
87
87
 
88
- Default agents can be **overridden** by creating a `.md` file with the same name (e.g. `.pi/agents/Explore.md`), or **disabled** per-project by creating a `.md` file with `enabled: false` frontmatter.
88
+ The `general-purpose` agent is a **parent twin** it receives the parent's entire system prompt plus a sub-agent context bridge, so it follows the same rules the parent does. Explore and Plan use standalone prompts tailored to their read-only roles.
89
+
90
+ Default agents can be **ejected** (`/agents` → select agent → Eject) to export them as `.md` files for customization, **overridden** by creating a `.md` file with the same name (e.g. `.pi/agents/general-purpose.md`), or **disabled** per-project with `enabled: false` frontmatter.
89
91
 
90
92
  ## Custom Agents
91
93
 
@@ -140,7 +142,7 @@ All fields are optional — sensible defaults for everything.
140
142
  | `model` | inherit parent | Model — `provider/modelId` or fuzzy name (`"haiku"`, `"sonnet"`) |
141
143
  | `thinking` | inherit | off, minimal, low, medium, high, xhigh |
142
144
  | `max_turns` | 50 | Max agentic turns before graceful shutdown |
143
- | `prompt_mode` | `replace` | `replace`: body is the full system prompt. `append`: body appended to default prompt |
145
+ | `prompt_mode` | `replace` | `replace`: body is the full system prompt. `append`: body appended to parent's prompt (agent acts as a "parent twin" with optional extra instructions) |
144
146
  | `inherit_context` | `false` | Fork parent conversation into agent |
145
147
  | `run_in_background` | `false` | Run in background by default |
146
148
  | `isolated` | `false` | No extension/MCP tools, only built-in |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tintinweb/pi-subagents",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "A pi extension extension that brings smart Claude Code-style autonomous sub-agents to pi.",
5
5
  "author": "tintinweb",
6
6
  "license": "MIT",
@@ -33,8 +33,6 @@ interface SpawnOptions {
33
33
  isolated?: boolean;
34
34
  inheritContext?: boolean;
35
35
  thinkingLevel?: ThinkingLevel;
36
- systemPromptOverride?: string;
37
- systemPromptAppend?: string;
38
36
  isBackground?: boolean;
39
37
  /** Called on tool start/end with activity info (for streaming progress to UI). */
40
38
  onToolActivity?: (activity: ToolActivity) => void;
@@ -122,8 +120,6 @@ export class AgentManager {
122
120
  isolated: options.isolated,
123
121
  inheritContext: options.inheritContext,
124
122
  thinkingLevel: options.thinkingLevel,
125
- systemPromptOverride: options.systemPromptOverride,
126
- systemPromptAppend: options.systemPromptAppend,
127
123
  signal: record.abortController!.signal,
128
124
  onToolActivity: (activity) => {
129
125
  if (activity.type === "end") record.toolUses++;
@@ -84,10 +84,6 @@ export interface RunOptions {
84
84
  isolated?: boolean;
85
85
  inheritContext?: boolean;
86
86
  thinkingLevel?: ThinkingLevel;
87
- /** Override system prompt entirely (for custom agents with promptMode: "replace"). */
88
- systemPromptOverride?: string;
89
- /** Append to default system prompt (for custom agents with promptMode: "append"). */
90
- systemPromptAppend?: string;
91
87
  /** Called on tool start/end with activity info. */
92
88
  onToolActivity?: (activity: ToolActivity) => void;
93
89
  /** Called on streaming text deltas from the assistant response. */
@@ -142,57 +138,27 @@ export async function runAgent(
142
138
  const agentConfig = getAgentConfig(type);
143
139
  const env = await detectEnv(options.pi, ctx.cwd);
144
140
 
145
- // Build system prompt: custom override > custom append > config-driven
141
+ // Get parent system prompt for append-mode agents
142
+ const parentSystemPrompt = ctx.getSystemPrompt();
143
+
144
+ // Build system prompt from agent config
146
145
  let systemPrompt: string;
147
- if (options.systemPromptOverride) {
148
- systemPrompt = options.systemPromptOverride;
149
- } else if (options.systemPromptAppend) {
150
- // Build a default prompt and append to it
151
- const defaultConfig = agentConfig ?? {
152
- name: type,
153
- description: "",
154
- builtinToolNames: [],
155
- extensions: true,
156
- skills: true,
157
- systemPrompt: "",
158
- promptMode: "replace" as const,
159
- inheritContext: false,
160
- runInBackground: false,
161
- isolated: false,
162
- };
163
- systemPrompt = buildAgentPrompt(defaultConfig, ctx.cwd, env) + "\n\n" + options.systemPromptAppend;
164
- } else if (agentConfig) {
165
- systemPrompt = buildAgentPrompt(agentConfig, ctx.cwd, env);
146
+ if (agentConfig) {
147
+ systemPrompt = buildAgentPrompt(agentConfig, ctx.cwd, env, parentSystemPrompt);
166
148
  } else {
167
- // Unknown type use a minimal general-purpose prompt
149
+ // Unknown type fallback: general-purpose (defensive — unreachable in practice
150
+ // since index.ts resolves unknown types to "general-purpose" before calling runAgent)
168
151
  systemPrompt = buildAgentPrompt({
169
152
  name: type,
170
153
  description: "General-purpose agent",
171
- builtinToolNames: [],
154
+ systemPrompt: "",
155
+ promptMode: "append",
172
156
  extensions: true,
173
157
  skills: true,
174
- systemPrompt: `# Role
175
- You are a general-purpose coding agent for complex, multi-step tasks.
176
- You have full access to read, write, edit files, and execute commands.
177
- Do what has been asked; nothing more, nothing less.
178
-
179
- # Tool Usage
180
- - Use the read tool instead of cat/head/tail
181
- - Use the edit tool instead of sed/awk
182
- - Use the write tool instead of echo/heredoc
183
- - Use the find tool instead of bash find/ls for file search
184
- - Use the grep tool instead of bash grep/rg for content search
185
- - Make independent tool calls in parallel
186
-
187
- # Output
188
- - Use absolute file paths
189
- - Do not use emojis
190
- - Be concise but complete`,
191
- promptMode: "replace",
192
158
  inheritContext: false,
193
159
  runInBackground: false,
194
160
  isolated: false,
195
- }, ctx.cwd, env);
161
+ }, ctx.cwd, env, parentSystemPrompt);
196
162
  }
197
163
 
198
164
  const tools = getToolsForType(type, ctx.cwd);
@@ -125,6 +125,7 @@ export function getConfig(type: string): {
125
125
  builtinToolNames: string[];
126
126
  extensions: true | string[] | false;
127
127
  skills: true | string[] | false;
128
+ promptMode: "replace" | "append";
128
129
  } {
129
130
  const key = resolveKey(type);
130
131
  const config = key ? agents.get(key) : undefined;
@@ -135,6 +136,7 @@ export function getConfig(type: string): {
135
136
  builtinToolNames: config.builtinToolNames ?? BUILTIN_TOOL_NAMES,
136
137
  extensions: config.extensions,
137
138
  skills: config.skills,
139
+ promptMode: config.promptMode,
138
140
  };
139
141
  }
140
142
 
@@ -147,6 +149,7 @@ export function getConfig(type: string): {
147
149
  builtinToolNames: gp.builtinToolNames ?? BUILTIN_TOOL_NAMES,
148
150
  extensions: gp.extensions,
149
151
  skills: gp.skills,
152
+ promptMode: gp.promptMode,
150
153
  };
151
154
  }
152
155
 
@@ -157,21 +160,7 @@ export function getConfig(type: string): {
157
160
  builtinToolNames: BUILTIN_TOOL_NAMES,
158
161
  extensions: true,
159
162
  skills: true,
163
+ promptMode: "append",
160
164
  };
161
165
  }
162
166
 
163
- // ---- Backwards-compatible aliases ----
164
-
165
- /** @deprecated Use registerAgents instead */
166
- export const registerCustomAgents = registerAgents;
167
-
168
- /** @deprecated Use getAgentConfig instead */
169
- export function getCustomAgentConfig(name: string): AgentConfig | undefined {
170
- const key = resolveKey(name);
171
- return key ? agents.get(key) : undefined;
172
- }
173
-
174
- /** @deprecated Use getUserAgentNames instead */
175
- export function getCustomAgentNames(): string[] {
176
- return getUserAgentNames();
177
- }
@@ -18,42 +18,8 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
18
18
  // builtinToolNames omitted — means "all available tools" (resolved at lookup time)
19
19
  extensions: true,
20
20
  skills: true,
21
- systemPrompt: `# Role
22
- You are a general-purpose coding agent for complex, multi-step tasks.
23
- You have full access to read, write, edit files, and execute commands.
24
- Do what has been asked; nothing more, nothing less.
25
-
26
- # Tool Usage
27
- - Use the read tool instead of cat/head/tail
28
- - Use the edit tool instead of sed/awk
29
- - Use the write tool instead of echo/heredoc
30
- - Use the find tool instead of bash find/ls for file search
31
- - Use the grep tool instead of bash grep/rg for content search
32
- - Make independent tool calls in parallel
33
-
34
- # File Operations
35
- - NEVER create files unless absolutely necessary
36
- - Prefer editing existing files over creating new ones
37
- - NEVER create documentation files unless explicitly requested
38
-
39
- # Git Safety
40
- - NEVER update git config
41
- - NEVER run destructive git commands (push --force, reset --hard, checkout ., restore ., clean -f, branch -D) without explicit request
42
- - NEVER skip hooks (--no-verify, --no-gpg-sign) unless explicitly asked
43
- - NEVER force push to main/master — warn the user if they request it
44
- - 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
45
- - Stage specific files by name, not git add -A or git add .
46
- - NEVER commit changes unless the user explicitly asks
47
- - NEVER push unless the user explicitly asks
48
- - NEVER use git commands with the -i flag (like git rebase -i or git add -i) — they require interactive input
49
- - Do not use --no-edit with git rebase commands
50
- - Do not commit files that likely contain secrets (.env, credentials.json, etc); warn the user if they request it
51
-
52
- # Output
53
- - Use absolute file paths
54
- - Do not use emojis
55
- - Be concise but complete`,
56
- promptMode: "replace",
21
+ systemPrompt: "",
22
+ promptMode: "append",
57
23
  inheritContext: false,
58
24
  runInBackground: false,
59
25
  isolated: false,
package/src/index.ts CHANGED
@@ -30,6 +30,7 @@ import {
30
30
  formatMs,
31
31
  formatDuration,
32
32
  getDisplayName,
33
+ getPromptModeLabel,
33
34
  describeActivity,
34
35
  type AgentDetails,
35
36
  type AgentActivity,
@@ -120,20 +121,6 @@ function buildDetails(
120
121
  };
121
122
  }
122
123
 
123
- /** Resolve system prompt overrides from an agent config. */
124
- function resolveCustomPrompt(config: AgentConfig | undefined): {
125
- systemPromptOverride?: string;
126
- systemPromptAppend?: string;
127
- } {
128
- if (!config?.systemPrompt) return {};
129
- // Default agents use their systemPrompt via buildAgentPrompt in agent-runner,
130
- // not via override/append. Only non-default agents use this path.
131
- if (config.isDefault) return {};
132
- if (config.promptMode === "append") return { systemPromptAppend: config.systemPrompt };
133
- return { systemPromptOverride: config.systemPrompt };
134
- }
135
-
136
-
137
124
  export default function (pi: ExtensionAPI) {
138
125
  /** Reload agents from .pi/agents/*.md and merge with defaults (called on init and each Agent invocation). */
139
126
  const reloadCustomAgents = () => {
@@ -161,9 +148,11 @@ export default function (pi: ExtensionAPI) {
161
148
  agentActivity.delete(record.id);
162
149
  widget.markFinished(record.id);
163
150
 
151
+ const tokens = safeFormatTokens(record.session);
152
+ const toolStats = tokens ? `Tool uses: ${record.toolUses} | ${tokens}` : `Tool uses: ${record.toolUses}`;
164
153
  pi.sendUserMessage(
165
154
  `Background agent completed: ${displayName} (${record.description})\n` +
166
- `Agent ID: ${record.id} | Status: ${status} | Tool uses: ${record.toolUses} | Duration: ${duration}\n\n` +
155
+ `Agent ID: ${record.id} | Status: ${status} | ${toolStats} | Duration: ${duration}\n\n` +
167
156
  resultPreview,
168
157
  { deliverAs: "followUp" },
169
158
  );
@@ -180,7 +169,9 @@ export default function (pi: ExtensionAPI) {
180
169
  ? record.result.slice(0, 300) + "\n...(truncated)"
181
170
  : record.result
182
171
  : "No output.";
183
- return `- ${displayName} (${record.description})\n ID: ${record.id} | Status: ${status} | Tools: ${record.toolUses} | Duration: ${duration}\n ${resultPreview}`;
172
+ const tokens = safeFormatTokens(record.session);
173
+ const toolStats = tokens ? `Tools: ${record.toolUses} | ${tokens}` : `Tools: ${record.toolUses}`;
174
+ return `- ${displayName} (${record.description})\n ID: ${record.id} | Status: ${status} | ${toolStats} | Duration: ${duration}\n ${resultPreview}`;
184
175
  }
185
176
 
186
177
  // ---- Group join manager ----
@@ -539,8 +530,6 @@ Guidelines:
539
530
  const runInBackground = params.run_in_background ?? customConfig?.runInBackground ?? false;
540
531
  const isolated = params.isolated ?? customConfig?.isolated ?? false;
541
532
 
542
- const { systemPromptOverride, systemPromptAppend } = resolveCustomPrompt(customConfig);
543
-
544
533
  // Build display tags for non-default config
545
534
  const parentModelId = ctx.model?.id;
546
535
  const effectiveModelId = model?.id;
@@ -548,6 +537,8 @@ Guidelines:
548
537
  ? (model?.name ?? effectiveModelId).replace(/^Claude\s+/i, "").toLowerCase()
549
538
  : undefined;
550
539
  const agentTags: string[] = [];
540
+ const modeLabel = getPromptModeLabel(subagentType);
541
+ if (modeLabel) agentTags.push(modeLabel);
551
542
  if (thinking) agentTags.push(`thinking: ${thinking}`);
552
543
  if (isolated) agentTags.push("isolated");
553
544
  // Shared base fields for all AgentDetails in this call
@@ -589,8 +580,6 @@ Guidelines:
589
580
  isolated,
590
581
  inheritContext,
591
582
  thinkingLevel: thinking,
592
- systemPromptOverride,
593
- systemPromptAppend,
594
583
  isBackground: true,
595
584
  ...bgCallbacks,
596
585
  });
@@ -680,8 +669,6 @@ Guidelines:
680
669
  isolated,
681
670
  inheritContext,
682
671
  thinkingLevel: thinking,
683
- systemPromptOverride,
684
- systemPromptAppend,
685
672
  ...fgCallbacks,
686
673
  });
687
674
 
@@ -707,8 +694,10 @@ Guidelines:
707
694
  }
708
695
 
709
696
  const durationMs = (record.completedAt ?? Date.now()) - record.startedAt;
697
+ const statsParts = [`${record.toolUses} tool uses`];
698
+ if (tokenText) statsParts.push(tokenText);
710
699
  return textResult(
711
- `${fallbackNote}Agent completed in ${formatMs(durationMs)} (${record.toolUses} tool uses)${getStatusNote(record.status)}.\n\n` +
700
+ `${fallbackNote}Agent completed in ${formatMs(durationMs)} (${statsParts.join(", ")})${getStatusNote(record.status)}.\n\n` +
712
701
  (record.result ?? "No output."),
713
702
  details,
714
703
  );
@@ -750,10 +739,12 @@ Guidelines:
750
739
 
751
740
  const displayName = getDisplayName(record.type);
752
741
  const duration = formatDuration(record.startedAt, record.completedAt);
742
+ const tokens = safeFormatTokens(record.session);
743
+ const toolStats = tokens ? `Tool uses: ${record.toolUses} | ${tokens}` : `Tool uses: ${record.toolUses}`;
753
744
 
754
745
  let output =
755
746
  `Agent: ${record.id}\n` +
756
- `Type: ${displayName} | Status: ${record.status} | Tool uses: ${record.toolUses} | Duration: ${duration}\n` +
747
+ `Type: ${displayName} | Status: ${record.status} | ${toolStats} | Duration: ${duration}\n` +
757
748
  `Description: ${record.description}\n\n`;
758
749
 
759
750
  if (record.status === "running") {
package/src/prompts.ts CHANGED
@@ -7,42 +7,57 @@ import type { AgentConfig, EnvInfo } from "./types.js";
7
7
  /**
8
8
  * Build the system prompt for an agent from its config.
9
9
  *
10
- * - "replace" mode: common header + config.systemPrompt
11
- * - "append" mode: common header + generic base + config.systemPrompt
10
+ * - "replace" mode: env header + config.systemPrompt (full control, no parent identity)
11
+ * - "append" mode: env header + parent system prompt + sub-agent context + config.systemPrompt
12
+ * - "append" with empty systemPrompt: pure parent clone
13
+ *
14
+ * @param parentSystemPrompt The parent agent's effective system prompt (for append mode).
12
15
  */
13
- export function buildAgentPrompt(config: AgentConfig, cwd: string, env: EnvInfo): string {
14
- const commonHeader = `You are a pi coding agent sub-agent.
15
- You have been invoked to handle a specific task autonomously.
16
-
17
- # Environment
16
+ export function buildAgentPrompt(
17
+ config: AgentConfig,
18
+ cwd: string,
19
+ env: EnvInfo,
20
+ parentSystemPrompt?: string,
21
+ ): string {
22
+ const envBlock = `# Environment
18
23
  Working directory: ${cwd}
19
24
  ${env.isGitRepo ? `Git repository: yes\nBranch: ${env.branch}` : "Not a git repository"}
20
25
  Platform: ${env.platform}`;
21
26
 
22
27
  if (config.promptMode === "append") {
23
- const genericBase = `
28
+ const identity = parentSystemPrompt || genericBase;
24
29
 
25
- # Role
26
- You are a general-purpose coding agent for complex, multi-step tasks.
27
- You have full access to read, write, edit files, and execute commands.
28
- Do what has been asked; nothing more, nothing less.
29
-
30
- # Tool Usage
30
+ const bridge = `<sub_agent_context>
31
+ You are operating as a sub-agent invoked to handle a specific task.
31
32
  - Use the read tool instead of cat/head/tail
32
33
  - Use the edit tool instead of sed/awk
33
34
  - Use the write tool instead of echo/heredoc
34
35
  - Use the find tool instead of bash find/ls for file search
35
36
  - Use the grep tool instead of bash grep/rg for content search
36
37
  - Make independent tool calls in parallel
37
-
38
- # Output
39
38
  - Use absolute file paths
40
39
  - Do not use emojis
41
- - Be concise but complete`;
40
+ - Be concise but complete
41
+ </sub_agent_context>`;
42
42
 
43
- return commonHeader + genericBase + "\n\n" + config.systemPrompt;
43
+ const customSection = config.systemPrompt?.trim()
44
+ ? `\n\n<agent_instructions>\n${config.systemPrompt}\n</agent_instructions>`
45
+ : "";
46
+
47
+ return envBlock + "\n\n<inherited_system_prompt>\n" + identity + "\n</inherited_system_prompt>\n\n" + bridge + customSection;
44
48
  }
45
49
 
46
- // "replace" mode — header + the config's full system prompt
47
- return commonHeader + "\n\n" + config.systemPrompt;
50
+ // "replace" mode — env header + the config's full system prompt
51
+ const replaceHeader = `You are a pi coding agent sub-agent.
52
+ You have been invoked to handle a specific task autonomously.
53
+
54
+ ${envBlock}`;
55
+
56
+ return replaceHeader + "\n\n" + config.systemPrompt;
48
57
  }
58
+
59
+ /** Fallback base prompt when parent system prompt is unavailable in append mode. */
60
+ const genericBase = `# Role
61
+ You are a general-purpose coding agent for complex, multi-step tasks.
62
+ You have full access to read, write, edit files, and execute commands.
63
+ Do what has been asked; nothing more, nothing less.`;
@@ -12,6 +12,9 @@ import { getConfig } from "../agent-types.js";
12
12
 
13
13
  // ---- Constants ----
14
14
 
15
+ /** Maximum number of rendered lines before overflow collapse kicks in. */
16
+ const MAX_WIDGET_LINES = 12;
17
+
15
18
  /** Braille spinner frames for animated running indicator. */
16
19
  export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
17
20
 
@@ -77,11 +80,11 @@ export interface AgentDetails {
77
80
 
78
81
  // ---- Formatting helpers ----
79
82
 
80
- /** Format a token count as "33.8k tokens" or "1.2M tokens". */
83
+ /** Format a token count compactly: "33.8k token", "1.2M token". */
81
84
  export function formatTokens(count: number): string {
82
- if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M tokens`;
83
- if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k tokens`;
84
- return `${count} tokens`;
85
+ if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M token`;
86
+ if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k token`;
87
+ return `${count} token`;
85
88
  }
86
89
 
87
90
  /** Format milliseconds as human-readable duration. */
@@ -100,6 +103,12 @@ export function getDisplayName(type: SubagentType): string {
100
103
  return getConfig(type).displayName;
101
104
  }
102
105
 
106
+ /** Short label for prompt mode: "twin" for append, nothing for replace (the default). */
107
+ export function getPromptModeLabel(type: SubagentType): string | undefined {
108
+ const config = getConfig(type);
109
+ return config.promptMode === "append" ? "twin" : undefined;
110
+ }
111
+
103
112
  /** Truncate text to a single line, max `len` chars. */
104
113
  function truncateLine(text: string, len = 60): string {
105
114
  const line = text.split("\n").find(l => l.trim())?.trim() ?? "";
@@ -193,6 +202,7 @@ export class AgentWidget {
193
202
  /** Render a finished agent line. */
194
203
  private renderFinishedLine(a: { type: SubagentType; status: string; description: string; toolUses: number; startedAt: number; completedAt?: number; error?: string }, theme: Theme): string {
195
204
  const name = getDisplayName(a.type);
205
+ const modeLabel = getPromptModeLabel(a.type);
196
206
  const duration = formatMs((a.completedAt ?? Date.now()) - a.startedAt);
197
207
 
198
208
  let icon: string;
@@ -220,7 +230,8 @@ export class AgentWidget {
220
230
  if (a.toolUses > 0) parts.push(`${a.toolUses} tool use${a.toolUses === 1 ? "" : "s"}`);
221
231
  parts.push(duration);
222
232
 
223
- return `${icon} ${theme.fg("dim", name)} ${theme.fg("dim", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}${statusText}`;
233
+ const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
234
+ return `${icon} ${theme.fg("dim", name)}${modeTag} ${theme.fg("dim", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}${statusText}`;
224
235
  }
225
236
 
226
237
  /** Force an immediate widget update. */
@@ -268,23 +279,20 @@ export class AgentWidget {
268
279
  const truncate = (line: string) => truncateToWidth(line, w);
269
280
  const headingColor = hasActive ? "accent" : "dim";
270
281
  const headingIcon = hasActive ? "●" : "○";
271
- const lines: string[] = [truncate(theme.fg(headingColor, headingIcon) + " " + theme.fg(headingColor, "Agents"))];
272
282
 
273
- // --- Finished agents (shown first, dimmed) ---
274
- for (let i = 0; i < finished.length; i++) {
275
- const a = finished[i];
276
- const isLast = !hasActive && i === finished.length - 1;
277
- const connector = isLast ? "└─" : "├─";
278
- lines.push(truncate(theme.fg("dim", connector) + " " + this.renderFinishedLine(a, theme)));
283
+ // Build sections separately for overflow-aware assembly.
284
+ // Each running agent = 2 lines (header + activity), finished = 1 line, queued = 1 line.
285
+
286
+ const finishedLines: string[] = [];
287
+ for (const a of finished) {
288
+ finishedLines.push(truncate(theme.fg("dim", "├─") + " " + this.renderFinishedLine(a, theme)));
279
289
  }
280
290
 
281
- // --- Running agents ---
282
- const isLastSection = queued.length === 0;
283
- for (let i = 0; i < running.length; i++) {
284
- const a = running[i];
285
- const isLast = isLastSection && i === running.length - 1;
286
- const connector = isLast ? "└─" : "├─";
291
+ const runningLines: string[][] = []; // each entry is [header, activity]
292
+ for (const a of running) {
287
293
  const name = getDisplayName(a.type);
294
+ const modeLabel = getPromptModeLabel(a.type);
295
+ const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
288
296
  const elapsed = formatMs(Date.now() - a.startedAt);
289
297
 
290
298
  const bg = this.agentActivity.get(a.id);
@@ -302,14 +310,82 @@ export class AgentWidget {
302
310
 
303
311
  const activity = bg ? describeActivity(bg.activeTools, bg.responseText) : "thinking…";
304
312
 
305
- lines.push(truncate(theme.fg("dim", connector) + ` ${theme.fg("accent", frame)} ${theme.bold(name)} ${theme.fg("muted", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", statsText)}`));
306
- const indent = isLast ? " " : "";
307
- lines.push(truncate(theme.fg("dim", indent) + theme.fg("dim", ` ⎿ ${activity}`)));
313
+ runningLines.push([
314
+ truncate(theme.fg("dim", "├─") + ` ${theme.fg("accent", frame)} ${theme.bold(name)}${modeTag} ${theme.fg("muted", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", statsText)}`),
315
+ truncate(theme.fg("dim", "│ ") + theme.fg("dim", ` ⎿ ${activity}`)),
316
+ ]);
308
317
  }
309
318
 
310
- // --- Queued agents (collapsed) ---
311
- if (queued.length > 0) {
312
- lines.push(truncate(theme.fg("dim", "└─") + ` ${theme.fg("muted", "◦")} ${theme.fg("dim", `${queued.length} queued`)}`));
319
+ const queuedLine = queued.length > 0
320
+ ? truncate(theme.fg("dim", "├─") + ` ${theme.fg("muted", "◦")} ${theme.fg("dim", `${queued.length} queued`)}`)
321
+ : undefined;
322
+
323
+ // Assemble with overflow cap (heading + overflow indicator = 2 reserved lines).
324
+ const maxBody = MAX_WIDGET_LINES - 1; // heading takes 1 line
325
+ const totalBody = finishedLines.length + runningLines.length * 2 + (queuedLine ? 1 : 0);
326
+
327
+ const lines: string[] = [truncate(theme.fg(headingColor, headingIcon) + " " + theme.fg(headingColor, "Agents"))];
328
+
329
+ if (totalBody <= maxBody) {
330
+ // Everything fits — add all lines and fix up connectors for the last item.
331
+ lines.push(...finishedLines);
332
+ for (const pair of runningLines) lines.push(...pair);
333
+ if (queuedLine) lines.push(queuedLine);
334
+
335
+ // Fix last connector: swap ├─ → └─ and │ → space for activity lines.
336
+ if (lines.length > 1) {
337
+ const last = lines.length - 1;
338
+ lines[last] = lines[last].replace("├─", "└─");
339
+ // If last item is a running agent activity line, fix indent of that line
340
+ // and fix the header line above it.
341
+ if (runningLines.length > 0 && !queuedLine) {
342
+ // The last two lines are the last running agent's header + activity.
343
+ if (last >= 2) {
344
+ lines[last - 1] = lines[last - 1].replace("├─", "└─");
345
+ lines[last] = lines[last].replace("│ ", " ");
346
+ }
347
+ }
348
+ }
349
+ } else {
350
+ // Overflow — prioritize: running > queued > finished.
351
+ // Reserve 1 line for overflow indicator.
352
+ let budget = maxBody - 1;
353
+ let hiddenRunning = 0;
354
+ let hiddenFinished = 0;
355
+
356
+ // 1. Running agents (2 lines each)
357
+ for (const pair of runningLines) {
358
+ if (budget >= 2) {
359
+ lines.push(...pair);
360
+ budget -= 2;
361
+ } else {
362
+ hiddenRunning++;
363
+ }
364
+ }
365
+
366
+ // 2. Queued line
367
+ if (queuedLine && budget >= 1) {
368
+ lines.push(queuedLine);
369
+ budget--;
370
+ }
371
+
372
+ // 3. Finished agents
373
+ for (const fl of finishedLines) {
374
+ if (budget >= 1) {
375
+ lines.push(fl);
376
+ budget--;
377
+ } else {
378
+ hiddenFinished++;
379
+ }
380
+ }
381
+
382
+ // Overflow summary
383
+ const overflowParts: string[] = [];
384
+ if (hiddenRunning > 0) overflowParts.push(`${hiddenRunning} running`);
385
+ if (hiddenFinished > 0) overflowParts.push(`${hiddenFinished} finished`);
386
+ const overflowText = overflowParts.join(", ");
387
+ lines.push(truncate(theme.fg("dim", "└─") + ` ${theme.fg("dim", `+${hiddenRunning + hiddenFinished} more (${overflowText})`)}`)
388
+ );
313
389
  }
314
390
 
315
391
  return { render: () => lines, invalidate: () => {} };
@@ -8,7 +8,7 @@
8
8
  import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component, type TUI } from "@mariozechner/pi-tui";
9
9
  import type { AgentSession } from "@mariozechner/pi-coding-agent";
10
10
  import type { Theme } from "./agent-widget.js";
11
- import { formatTokens, formatDuration, getDisplayName, describeActivity, type AgentActivity } from "./agent-widget.js";
11
+ import { formatTokens, formatDuration, getDisplayName, getPromptModeLabel, describeActivity, type AgentActivity } from "./agent-widget.js";
12
12
  import type { AgentRecord } from "../types.js";
13
13
  import { extractText } from "../context.js";
14
14
 
@@ -89,6 +89,8 @@ export class ConversationViewer implements Component {
89
89
  // Header
90
90
  lines.push(hrTop);
91
91
  const name = getDisplayName(this.record.type);
92
+ const modeLabel = getPromptModeLabel(this.record.type);
93
+ const modeTag = modeLabel ? ` ${th.fg("dim", `(${modeLabel})`)}` : "";
92
94
  const statusIcon = this.record.status === "running"
93
95
  ? th.fg("accent", "●")
94
96
  : this.record.status === "completed"
@@ -109,7 +111,7 @@ export class ConversationViewer implements Component {
109
111
  }
110
112
 
111
113
  lines.push(row(
112
- `${statusIcon} ${th.bold(name)} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${th.fg("dim", headerParts.join(" · "))}`,
114
+ `${statusIcon} ${th.bold(name)}${modeTag} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${th.fg("dim", headerParts.join(" · "))}`,
113
115
  ));
114
116
  lines.push(hrMid);
115
117