@tintinweb/pi-subagents 0.3.1 → 0.4.1

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/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