@bacnh85/pi-subagent 0.7.0 → 0.7.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/agent-format.md CHANGED
@@ -21,9 +21,21 @@ description: ... # Required. When to use this agent.
21
21
  tools: read, grep, ... # Optional. Comma-separated tool names. Defaults to all.
22
22
  model: provider/model # Optional. Defaults to parent's model.
23
23
  thinking: low # Optional: off|minimal|low|medium|high|xhigh|max.
24
+ sandbox: read-only # Optional: read-only | workspace-write. Auto-derives tool restrictions.
25
+ color: cyan # Optional: red|blue|green|yellow|purple|orange|pink|cyan.
24
26
  ---
25
27
  ```
26
28
 
29
+ ### `sandbox`
30
+
31
+ - `read-only`: Restricts tools to `read`, `grep`, `find`, `ls`. Overrides any `tools` field.
32
+ - `workspace-write` (default): Uses the agent's `tools` list or defaults to all tools.
33
+
34
+ ### `color`
35
+
36
+ Display color for the agent name in the TUI thread picker, viewer, and result summary.
37
+ Accepted values: `red`, `blue`, `green`, `yellow`, `purple`, `orange`, `pink`, `cyan`.
38
+
27
39
  Only `name` and `description` are required.
28
40
 
29
41
  ## Body
@@ -169,7 +169,7 @@ function loadAgentsFromDir(
169
169
  if (!validSandboxes.includes(frontmatter.sandbox)) {
170
170
  diagnostics.push({
171
171
  filePath,
172
- issue: `Invalid sandbox mode "${frontmatter.sandbox}". Valid values: ${validSandboxes.join(", ")}. Using default.`,
172
+ issue: `Invalid sandbox mode "${frontmatter.sandbox}". Valid values: ${validSandboxes.join(", ")}. Ignoring.`,
173
173
  severity: "warn",
174
174
  });
175
175
  }
@@ -25,11 +25,12 @@ import {
25
25
  getAgentDir,
26
26
  getMarkdownTheme,
27
27
  ModelRegistry,
28
+ type ThemeColor,
28
29
  } from "@earendil-works/pi-coding-agent";
29
30
  import { Container, Markdown, SelectList, Spacer, Text } from "@earendil-works/pi-tui";
30
31
  import { Type } from "typebox";
31
32
 
32
- import { type AgentConfig, type AgentScope, discoverAgents, formatAgentList, invalidateAgentCache } from "./agents.ts";
33
+ import { type AgentColor, type AgentConfig, type AgentScope, discoverAgents, formatAgentList, invalidateAgentCache } from "./agents.ts";
33
34
  import {
34
35
  type SubAgentResult,
35
36
  getFinalOutput,
@@ -44,6 +45,7 @@ import {
44
45
  validateAgentTools,
45
46
  truncateParallelOutput,
46
47
  validateExecutionRequest,
48
+ READ_ONLY_TOOLS,
47
49
  MAX_CONCURRENCY,
48
50
  MAX_PARALLEL_TASKS,
49
51
  MAX_CHAIN_LENGTH,
@@ -159,7 +161,7 @@ export default function (pi: ExtensionAPI) {
159
161
  // Inject available agent catalog into system prompt for semantic auto-delegation
160
162
  pi.on("before_agent_start", async (event) => {
161
163
  const ctx = currentCtx;
162
- const discovery = discoverAgents(event.cwd ?? ctx?.cwd ?? process.cwd(), "both", bundledAgentsDir);
164
+ const discovery = discoverAgents(ctx?.cwd ?? process.cwd(), "both", bundledAgentsDir);
163
165
  const catalog = discovery.agents
164
166
  .map((a) => {
165
167
  const modelInfo = a.model ? ` (model: ${a.model})` : " (inherits parent)";
@@ -192,7 +194,7 @@ export default function (pi: ExtensionAPI) {
192
194
  request.respond({ id: request.id, ok: false, error: `Unknown agent: ${request.agent}` });
193
195
  return;
194
196
  }
195
- const thread = threadStore.createThread({ agentName: agent.name, task: request.task, mode: "single", color: agent.color });
197
+ const thread = threadStore.createThread({ agentName: agent.name, task: request.task, mode: "single", color: agent.color ? AGENT_TO_THEME_COLOR[agent.color as AgentColor] : undefined });
196
198
  void runNamedAgent({
197
199
  agent: request.readOnly ? { ...agent, tools: ["read", "grep", "find", "ls"] } : agent,
198
200
  task: request.task,
@@ -289,6 +291,34 @@ export default function (pi: ExtensionAPI) {
289
291
  },
290
292
  });
291
293
 
294
+ /** Map AgentColor (from agent frontmatter) to ThemeColor (for pi TUI). */
295
+ const AGENT_TO_THEME_COLOR: Record<AgentColor, ThemeColor> = {
296
+ red: "error",
297
+ blue: "accent",
298
+ green: "success",
299
+ yellow: "warning",
300
+ purple: "syntaxType",
301
+ orange: "syntaxString",
302
+ pink: "customMessageLabel",
303
+ cyan: "syntaxVariable",
304
+ };
305
+
306
+ /** Resolve agent-defined color to a valid ThemeColor for thread creation. */
307
+ const agentToThemeColor = (agentName: string): ThemeColor | undefined => {
308
+ const ctx = currentCtx;
309
+ if (!ctx) return undefined;
310
+ const agent = discoverAgents(ctx.cwd, "both", bundledAgentsDir).agents.find(a => a.name === agentName);
311
+ return agent?.color ? AGENT_TO_THEME_COLOR[agent.color] : undefined;
312
+ };
313
+
314
+ /** Look up agent color by name for TUI rendering. */
315
+ const resolveAgentColor = (name: string): ThemeColor => {
316
+ const ctx = currentCtx;
317
+ if (!ctx) return "accent";
318
+ const found = discoverAgents(ctx.cwd, "both", bundledAgentsDir).agents.find(a => a.name === name);
319
+ return found?.color ? AGENT_TO_THEME_COLOR[found.color] : "accent";
320
+ };
321
+
292
322
  pi.registerTool({
293
323
  name: "subagent",
294
324
  label: "Subagent",
@@ -438,10 +468,16 @@ export default function (pi: ExtensionAPI) {
438
468
  }
439
469
 
440
470
  // Helper: validate and normalise tools for an agent.
441
- function resolveChildTools(agentTools: string[] | undefined, readOnly?: boolean): string[] {
471
+ function resolveChildTools(agentTools: string[] | undefined, sandbox?: string, readOnly?: boolean): string[] {
442
472
  const defaultTools = ["read", "bash", "edit", "write", "grep", "find", "ls"];
443
- const rawTools = agentTools ?? defaultTools;
444
- const result = validateAgentTools({ tools: rawTools, readOnly });
473
+ let rawTools = agentTools ?? defaultTools;
474
+ // sandbox overrides tools: silently strip mutation tools, not an error
475
+ if (sandbox === "read-only") {
476
+ rawTools = rawTools.filter(t => READ_ONLY_TOOLS.includes(t));
477
+ if (rawTools.length === 0) rawTools = [...READ_ONLY_TOOLS];
478
+ }
479
+ const effectiveReadOnly = readOnly || sandbox === "read-only";
480
+ const result = validateAgentTools({ tools: rawTools, readOnly: effectiveReadOnly });
445
481
  if (result.errors.length > 0) {
446
482
  throw new Error(`Tool validation errors: ${result.errors.join("; ")}`);
447
483
  }
@@ -505,7 +541,7 @@ export default function (pi: ExtensionAPI) {
505
541
  try {
506
542
  // Inject parent's API key so --api-key and other runtime overrides work
507
543
  await injectApiKey(resolved.model);
508
- tools = resolveChildTools(agent.tools, isReadOnly);
544
+ tools = resolveChildTools(agent.tools, agent.sandbox, isReadOnly);
509
545
  effectiveTimeoutMs = resolveChildTimeout(timeoutMs, params.timeout);
510
546
  safeCwd = resolveChildCwd(cwd);
511
547
  } catch (err: unknown) {
@@ -554,7 +590,7 @@ export default function (pi: ExtensionAPI) {
554
590
  task: taskWithContext,
555
591
  mode: "chain-step",
556
592
  toolCallId: _toolCallId,
557
- color: agents.find(a => a.name === step.agent)?.color,
593
+ color: agentToThemeColor(step.agent),
558
594
  });
559
595
  const result = await runOne(
560
596
  step.agent, taskWithContext, step.cwd,
@@ -646,7 +682,7 @@ export default function (pi: ExtensionAPI) {
646
682
  task: t.task,
647
683
  mode: "parallel-task",
648
684
  toolCallId: _toolCallId,
649
- color: agents.find(a => a.name === t.agent)?.color,
685
+ color: agentToThemeColor(t.agent),
650
686
  }),
651
687
  );
652
688
 
@@ -761,7 +797,7 @@ export default function (pi: ExtensionAPI) {
761
797
  task: params.task,
762
798
  mode: "single",
763
799
  toolCallId: _toolCallId,
764
- color: agents.find(a => a.name === params.agent)?.color,
800
+ color: agentToThemeColor(params.agent),
765
801
  });
766
802
  const result = await runOne(
767
803
  params.agent, params.task, params.cwd,
@@ -814,14 +850,6 @@ export default function (pi: ExtensionAPI) {
814
850
  // TUI rendering
815
851
  // ------------------------------------------------------------------
816
852
 
817
- /** Look up agent color by name for TUI rendering. */
818
- const resolveAgentColor = (name: string): string => {
819
- const ctx = currentCtx;
820
- if (!ctx) return "accent";
821
- const found = discoverAgents(ctx.cwd, "both", bundledAgentsDir).agents.find(a => a.name === name);
822
- return found?.color ?? "accent";
823
- };
824
-
825
853
  renderCall(args, theme, _context) {
826
854
  const scope: AgentScope = args.agentScope ?? "user";
827
855
  const fg = theme.fg.bind(theme);
@@ -890,7 +918,8 @@ export default function (pi: ExtensionAPI) {
890
918
 
891
919
  // --- Single ---
892
920
  if (details.mode === "single" && details.results.length === 1) {
893
- return renderSingleResult(details.results[0], expanded, theme);
921
+ const r = details.results[0];
922
+ return renderSingleResult(r, expanded, theme, resolveAgentColor(r.agent));
894
923
  }
895
924
 
896
925
  // --- Chain ---
@@ -188,6 +188,7 @@ export function renderSingleResult(
188
188
  result: SubAgentResult,
189
189
  expanded: boolean,
190
190
  theme: { fg: (c: any, t: string) => string; bold: (t: string) => string },
191
+ agentColor?: string,
191
192
  ): Container | Text {
192
193
  const isError = isFailedResult(result);
193
194
  const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
@@ -197,7 +198,7 @@ export function renderSingleResult(
197
198
  if (expanded) {
198
199
  const mdTheme = getMarkdownTheme();
199
200
  const container = new Container();
200
- let header = `${icon} ${theme.fg("toolTitle", theme.bold(result.agent))}`;
201
+ let header = `${icon} ${theme.fg(agentColor ?? "toolTitle", theme.bold(result.agent))}`;
201
202
  if (isError && result.stopReason) {
202
203
  const reasonColor = result.stopReason === "timeout" ? "warning" : "error";
203
204
  header += ` ${theme.fg(reasonColor, `[${result.stopReason}]`)}`;
@@ -239,7 +240,7 @@ export function renderSingleResult(
239
240
  }
240
241
 
241
242
  // Collapsed
242
- let text = `${icon} ${theme.fg("toolTitle", theme.bold(result.agent))}`;
243
+ let text = `${icon} ${theme.fg(agentColor ?? "toolTitle", theme.bold(result.agent))}`;
243
244
  if (isError && result.stopReason) {
244
245
  const reasonColor = result.stopReason === "timeout" ? "warning" : "error";
245
246
  text += ` ${theme.fg(reasonColor, `[${result.stopReason}]`)}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-subagent",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "In-process subagents for Pi with isolated SDK sessions, parallel and chained delegation, and inspectable threads.",
5
5
  "type": "module",
6
6
  "license": "MIT",