@oh-my-pi/pi-coding-agent 6.8.5 → 6.9.69

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.
Files changed (155) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/package.json +6 -6
  3. package/src/cli/stats-cli.ts +191 -0
  4. package/src/core/agent-session.ts +103 -1
  5. package/src/core/extensions/index.ts +2 -0
  6. package/src/core/extensions/runner.ts +31 -0
  7. package/src/core/extensions/types.ts +24 -0
  8. package/src/core/messages.ts +48 -0
  9. package/src/core/sdk.ts +0 -2
  10. package/src/core/session-manager.ts +10 -1
  11. package/src/core/settings-manager.ts +0 -105
  12. package/src/core/tools/bash.ts +5 -7
  13. package/src/core/tools/index.ts +1 -5
  14. package/src/core/tools/patch/applicator.ts +115 -17
  15. package/src/core/tools/patch/index.ts +1 -1
  16. package/src/core/tools/patch/normalize.ts +185 -10
  17. package/src/core/tools/python.ts +444 -86
  18. package/src/core/tools/task/executor.ts +2 -6
  19. package/src/core/tools/task/index.ts +30 -12
  20. package/src/core/tools/task/render.ts +163 -30
  21. package/src/core/tools/task/template.ts +37 -0
  22. package/src/core/tools/task/types.ts +6 -2
  23. package/src/core/tools/task/worker.ts +1 -1
  24. package/src/index.ts +2 -2
  25. package/src/main.ts +12 -0
  26. package/src/modes/interactive/components/python-execution.ts +180 -0
  27. package/src/modes/interactive/components/settings-defs.ts +0 -70
  28. package/src/modes/interactive/components/settings-selector.ts +0 -1
  29. package/src/modes/interactive/components/welcome.ts +1 -0
  30. package/src/modes/interactive/controllers/command-controller.ts +46 -0
  31. package/src/modes/interactive/controllers/event-controller.ts +0 -11
  32. package/src/modes/interactive/controllers/input-controller.ts +28 -1
  33. package/src/modes/interactive/controllers/selector-controller.ts +0 -9
  34. package/src/modes/interactive/interactive-mode.ts +10 -58
  35. package/src/modes/interactive/theme/dark.json +2 -9
  36. package/src/modes/interactive/theme/defaults/alabaster.json +2 -8
  37. package/src/modes/interactive/theme/defaults/amethyst.json +2 -9
  38. package/src/modes/interactive/theme/defaults/anthracite.json +2 -9
  39. package/src/modes/interactive/theme/defaults/basalt.json +89 -88
  40. package/src/modes/interactive/theme/defaults/birch.json +2 -8
  41. package/src/modes/interactive/theme/defaults/dark-abyss.json +2 -8
  42. package/src/modes/interactive/theme/defaults/dark-arctic.json +2 -9
  43. package/src/modes/interactive/theme/defaults/dark-aurora.json +3 -2
  44. package/src/modes/interactive/theme/defaults/dark-catppuccin.json +2 -1
  45. package/src/modes/interactive/theme/defaults/dark-cavern.json +2 -8
  46. package/src/modes/interactive/theme/defaults/dark-copper.json +3 -2
  47. package/src/modes/interactive/theme/defaults/dark-cosmos.json +2 -8
  48. package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +2 -9
  49. package/src/modes/interactive/theme/defaults/dark-dracula.json +2 -9
  50. package/src/modes/interactive/theme/defaults/dark-eclipse.json +2 -8
  51. package/src/modes/interactive/theme/defaults/dark-ember.json +3 -2
  52. package/src/modes/interactive/theme/defaults/dark-equinox.json +2 -8
  53. package/src/modes/interactive/theme/defaults/dark-forest.json +2 -9
  54. package/src/modes/interactive/theme/defaults/dark-github.json +2 -9
  55. package/src/modes/interactive/theme/defaults/dark-gruvbox.json +2 -9
  56. package/src/modes/interactive/theme/defaults/dark-lavender.json +3 -2
  57. package/src/modes/interactive/theme/defaults/dark-lunar.json +2 -8
  58. package/src/modes/interactive/theme/defaults/dark-midnight.json +3 -2
  59. package/src/modes/interactive/theme/defaults/dark-monochrome.json +2 -9
  60. package/src/modes/interactive/theme/defaults/dark-monokai.json +2 -9
  61. package/src/modes/interactive/theme/defaults/dark-nebula.json +2 -8
  62. package/src/modes/interactive/theme/defaults/dark-nord.json +2 -9
  63. package/src/modes/interactive/theme/defaults/dark-ocean.json +2 -9
  64. package/src/modes/interactive/theme/defaults/dark-one.json +2 -9
  65. package/src/modes/interactive/theme/defaults/dark-rainforest.json +2 -8
  66. package/src/modes/interactive/theme/defaults/dark-reef.json +2 -8
  67. package/src/modes/interactive/theme/defaults/dark-retro.json +2 -9
  68. package/src/modes/interactive/theme/defaults/dark-rose-pine.json +2 -1
  69. package/src/modes/interactive/theme/defaults/dark-sakura.json +3 -2
  70. package/src/modes/interactive/theme/defaults/dark-slate.json +3 -2
  71. package/src/modes/interactive/theme/defaults/dark-solarized.json +2 -1
  72. package/src/modes/interactive/theme/defaults/dark-solstice.json +2 -8
  73. package/src/modes/interactive/theme/defaults/dark-starfall.json +2 -8
  74. package/src/modes/interactive/theme/defaults/dark-sunset.json +2 -9
  75. package/src/modes/interactive/theme/defaults/dark-swamp.json +2 -8
  76. package/src/modes/interactive/theme/defaults/dark-synthwave.json +2 -1
  77. package/src/modes/interactive/theme/defaults/dark-taiga.json +2 -8
  78. package/src/modes/interactive/theme/defaults/dark-terminal.json +3 -2
  79. package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +2 -9
  80. package/src/modes/interactive/theme/defaults/dark-tundra.json +2 -8
  81. package/src/modes/interactive/theme/defaults/dark-twilight.json +2 -8
  82. package/src/modes/interactive/theme/defaults/dark-volcanic.json +2 -8
  83. package/src/modes/interactive/theme/defaults/graphite.json +2 -9
  84. package/src/modes/interactive/theme/defaults/light-arctic.json +2 -1
  85. package/src/modes/interactive/theme/defaults/light-aurora-day.json +2 -8
  86. package/src/modes/interactive/theme/defaults/light-canyon.json +2 -8
  87. package/src/modes/interactive/theme/defaults/light-catppuccin.json +2 -1
  88. package/src/modes/interactive/theme/defaults/light-cirrus.json +2 -8
  89. package/src/modes/interactive/theme/defaults/light-coral.json +3 -2
  90. package/src/modes/interactive/theme/defaults/light-cyberpunk.json +2 -9
  91. package/src/modes/interactive/theme/defaults/light-dawn.json +2 -8
  92. package/src/modes/interactive/theme/defaults/light-dunes.json +2 -8
  93. package/src/modes/interactive/theme/defaults/light-eucalyptus.json +3 -2
  94. package/src/modes/interactive/theme/defaults/light-forest.json +2 -9
  95. package/src/modes/interactive/theme/defaults/light-frost.json +3 -2
  96. package/src/modes/interactive/theme/defaults/light-github.json +2 -1
  97. package/src/modes/interactive/theme/defaults/light-glacier.json +2 -8
  98. package/src/modes/interactive/theme/defaults/light-gruvbox.json +2 -9
  99. package/src/modes/interactive/theme/defaults/light-haze.json +2 -8
  100. package/src/modes/interactive/theme/defaults/light-honeycomb.json +3 -2
  101. package/src/modes/interactive/theme/defaults/light-lagoon.json +2 -8
  102. package/src/modes/interactive/theme/defaults/light-lavender.json +3 -2
  103. package/src/modes/interactive/theme/defaults/light-meadow.json +2 -8
  104. package/src/modes/interactive/theme/defaults/light-mint.json +3 -2
  105. package/src/modes/interactive/theme/defaults/light-monochrome.json +2 -1
  106. package/src/modes/interactive/theme/defaults/light-ocean.json +2 -9
  107. package/src/modes/interactive/theme/defaults/light-one.json +2 -8
  108. package/src/modes/interactive/theme/defaults/light-opal.json +2 -8
  109. package/src/modes/interactive/theme/defaults/light-orchard.json +2 -8
  110. package/src/modes/interactive/theme/defaults/light-paper.json +3 -2
  111. package/src/modes/interactive/theme/defaults/light-prism.json +2 -8
  112. package/src/modes/interactive/theme/defaults/light-retro.json +2 -9
  113. package/src/modes/interactive/theme/defaults/light-sand.json +3 -2
  114. package/src/modes/interactive/theme/defaults/light-savanna.json +2 -8
  115. package/src/modes/interactive/theme/defaults/light-solarized.json +2 -1
  116. package/src/modes/interactive/theme/defaults/light-soleil.json +2 -8
  117. package/src/modes/interactive/theme/defaults/light-sunset.json +2 -9
  118. package/src/modes/interactive/theme/defaults/light-synthwave.json +2 -9
  119. package/src/modes/interactive/theme/defaults/light-tokyo-night.json +2 -9
  120. package/src/modes/interactive/theme/defaults/light-wetland.json +2 -8
  121. package/src/modes/interactive/theme/defaults/light-zenith.json +2 -8
  122. package/src/modes/interactive/theme/defaults/limestone.json +2 -8
  123. package/src/modes/interactive/theme/defaults/mahogany.json +2 -9
  124. package/src/modes/interactive/theme/defaults/marble.json +2 -8
  125. package/src/modes/interactive/theme/defaults/obsidian.json +89 -88
  126. package/src/modes/interactive/theme/defaults/onyx.json +89 -88
  127. package/src/modes/interactive/theme/defaults/pearl.json +2 -8
  128. package/src/modes/interactive/theme/defaults/porcelain.json +89 -88
  129. package/src/modes/interactive/theme/defaults/quartz.json +2 -8
  130. package/src/modes/interactive/theme/defaults/sandstone.json +2 -8
  131. package/src/modes/interactive/theme/defaults/titanium.json +88 -87
  132. package/src/modes/interactive/theme/light.json +2 -8
  133. package/src/modes/interactive/theme/theme-schema.json +5 -0
  134. package/src/modes/interactive/theme/theme.ts +7 -0
  135. package/src/modes/interactive/types.ts +5 -15
  136. package/src/modes/interactive/utils/ui-helpers.ts +20 -0
  137. package/src/prompts/system/system-prompt.md +8 -0
  138. package/src/prompts/tools/python.md +40 -2
  139. package/src/prompts/tools/task.md +8 -13
  140. package/src/core/custom-commands/bundled/wt/index.ts +0 -435
  141. package/src/core/tools/git.ts +0 -213
  142. package/src/core/voice-controller.ts +0 -135
  143. package/src/core/voice-supervisor.ts +0 -976
  144. package/src/core/voice.ts +0 -314
  145. package/src/lib/worktree/collapse.ts +0 -180
  146. package/src/lib/worktree/constants.ts +0 -14
  147. package/src/lib/worktree/errors.ts +0 -23
  148. package/src/lib/worktree/git.ts +0 -60
  149. package/src/lib/worktree/index.ts +0 -15
  150. package/src/lib/worktree/operations.ts +0 -216
  151. package/src/lib/worktree/session.ts +0 -114
  152. package/src/lib/worktree/stats.ts +0 -67
  153. package/src/modes/interactive/utils/voice-manager.ts +0 -96
  154. package/src/prompts/tools/git.md +0 -9
  155. package/src/prompts/voice-summary.md +0 -12
@@ -28,12 +28,14 @@ import { discoverAgents, getAgent } from "./discovery";
28
28
  import { runSubprocess } from "./executor";
29
29
  import { mapWithConcurrencyLimit } from "./parallel";
30
30
  import { renderCall, renderResult } from "./render";
31
+ import { renderTemplate, validateTaskTemplate } from "./template";
31
32
  import {
32
33
  type AgentProgress,
33
34
  MAX_AGENTS_IN_DESCRIPTION,
34
35
  MAX_CONCURRENCY,
35
36
  MAX_PARALLEL_TASKS,
36
37
  type SingleResult,
38
+ type TaskParams,
37
39
  type TaskToolDetails,
38
40
  taskSchema,
39
41
  } from "./types";
@@ -112,14 +114,6 @@ async function buildDescription(cwd: string): Promise<string> {
112
114
  // Tool Class
113
115
  // ═══════════════════════════════════════════════════════════════════════════
114
116
 
115
- type TaskParams = {
116
- agent: string;
117
- context?: string;
118
- model?: string;
119
- output?: unknown;
120
- tasks: Array<{ id: string; task: string; description: string }>;
121
- };
122
-
123
117
  /**
124
118
  * Task tool - Delegate tasks to specialized agents.
125
119
  *
@@ -201,7 +195,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
201
195
  content: [
202
196
  {
203
197
  type: "text",
204
- text: `No tasks provided. Use: { agent, context, tasks: [{id, task, description}, ...] }`,
198
+ text: `No tasks provided. Use: { agent, context, tasks: [{id, description, vars}, ...] }`,
205
199
  },
206
200
  ],
207
201
  details: {
@@ -277,6 +271,18 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
277
271
  };
278
272
  }
279
273
 
274
+ const templateError = validateTaskTemplate(context, tasks);
275
+ if (templateError) {
276
+ return {
277
+ content: [{ type: "text", text: templateError }],
278
+ details: {
279
+ projectAgentsDir,
280
+ results: [],
281
+ totalDurationMs: 0,
282
+ },
283
+ };
284
+ }
285
+
280
286
  // Derive artifacts directory
281
287
  const sessionFile = this.session.getSessionFile();
282
288
  const artifactsDir = sessionFile ? sessionFile.slice(0, -6) : null;
@@ -341,10 +347,12 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
341
347
  }
342
348
 
343
349
  // Build full prompts with context prepended
350
+ const contextTemplate = context ?? "";
344
351
  const tasksWithContext = tasks.map((t) => ({
345
- task: context ? `${context}\n\n${t.task}` : t.task,
352
+ task: renderTemplate(contextTemplate, t.vars),
346
353
  description: t.description,
347
354
  taskId: t.id,
355
+ vars: t.vars,
348
356
  }));
349
357
 
350
358
  // Initialize progress for all tasks
@@ -357,6 +365,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
357
365
  agentSource: agent.source,
358
366
  status: "pending",
359
367
  task: t.task,
368
+ vars: t.vars,
360
369
  recentTools: [],
361
370
  recentOutput: [],
362
371
  toolCount: 0,
@@ -391,7 +400,10 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
391
400
  signal,
392
401
  eventBus: undefined,
393
402
  onProgress: (progress) => {
394
- progressMap.set(index, structuredClone(progress));
403
+ progressMap.set(index, {
404
+ ...structuredClone(progress),
405
+ vars: tasksWithContext[index]?.vars,
406
+ });
395
407
  emitProgress();
396
408
  },
397
409
  authStorage: this.session.authStorage,
@@ -405,7 +417,12 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
405
417
 
406
418
  // Fill in skipped tasks (undefined entries from abort) with placeholder results
407
419
  const results: SingleResult[] = partialResults.map((result, index) => {
408
- if (result !== undefined) return result;
420
+ if (result !== undefined) {
421
+ return {
422
+ ...result,
423
+ vars: tasksWithContext[index]?.vars,
424
+ };
425
+ }
409
426
  const task = tasksWithContext[index];
410
427
  return {
411
428
  index,
@@ -413,6 +430,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
413
430
  agent: agentName,
414
431
  agentSource: agent.source,
415
432
  task: task.task,
433
+ vars: task.vars,
416
434
  description: task.description,
417
435
  exitCode: 1,
418
436
  output: "",
@@ -67,9 +67,12 @@ function formatFindingSummary(findings: ReportFindingDetails[], theme: Theme): s
67
67
  return `${theme.fg("dim", "Findings:")} ${parts.join(theme.sep.dot)}`;
68
68
  }
69
69
 
70
- function formatJsonScalar(value: unknown): string {
70
+ function formatJsonScalar(value: unknown, theme: Theme): string {
71
71
  if (value === null) return "null";
72
- if (typeof value === "string") return `"${value}"`;
72
+ if (typeof value === "string") {
73
+ const trimmed = truncate(value, 70, theme.format.ellipsis);
74
+ return `"${trimmed}"`;
75
+ }
73
76
  if (typeof value === "number" || typeof value === "boolean") return String(value);
74
77
  return "";
75
78
  }
@@ -108,7 +111,7 @@ function renderJsonTreeLines(
108
111
 
109
112
  const connector = isLast ? theme.tree.last : theme.tree.branch;
110
113
  const prefix = `${buildTreePrefix(ancestors, theme)}${theme.fg("dim", connector)} `;
111
- const scalar = formatJsonScalar(val);
114
+ const scalar = formatJsonScalar(val, theme);
112
115
 
113
116
  if (scalar) {
114
117
  const label = key ? theme.fg("muted", key) : theme.fg("muted", "value");
@@ -186,7 +189,33 @@ function renderJsonTreeLines(
186
189
  pushLine(`${prefix}${iconScalar} ${label}: ${theme.fg("dim", String(val))}`);
187
190
  };
188
191
 
189
- renderNode(value, undefined, [], true, 0);
192
+ const renderRoot = (val: unknown) => {
193
+ if (Array.isArray(val)) {
194
+ for (let i = 0; i < val.length; i++) {
195
+ renderNode(val[i], `[${i}]`, [], i === val.length - 1, 1);
196
+ if (lines.length >= maxLines) {
197
+ truncated = true;
198
+ return;
199
+ }
200
+ }
201
+ return;
202
+ }
203
+ if (val && typeof val === "object") {
204
+ const entries = Object.entries(val as Record<string, unknown>);
205
+ for (let i = 0; i < entries.length; i++) {
206
+ const [childKey, child] = entries[i];
207
+ renderNode(child, childKey, [], i === entries.length - 1, 1);
208
+ if (lines.length >= maxLines) {
209
+ truncated = true;
210
+ return;
211
+ }
212
+ }
213
+ return;
214
+ }
215
+ renderNode(val, undefined, [], true, 0);
216
+ };
217
+
218
+ renderRoot(value);
190
219
 
191
220
  return { lines, truncated };
192
221
  }
@@ -203,11 +232,18 @@ function renderOutputSection(
203
232
  const trimmedOutput = output.trim();
204
233
  if (!trimmedOutput) return lines;
205
234
 
206
- lines.push(`${continuePrefix}${theme.fg("dim", "Output")}`);
207
-
208
235
  if (trimmedOutput.startsWith("{") || trimmedOutput.startsWith("[")) {
209
236
  try {
210
237
  const parsed = JSON.parse(trimmedOutput);
238
+
239
+ // Collapsed: inline format like Vars
240
+ if (!expanded) {
241
+ lines.push(`${continuePrefix}${theme.fg("dim", formatOutputInline(parsed, theme))}`);
242
+ return lines;
243
+ }
244
+
245
+ // Expanded: tree format
246
+ lines.push(`${continuePrefix}${theme.fg("dim", "Output")}`);
211
247
  const tree = renderJsonTreeLines(parsed, theme, expanded ? 6 : 2, expanded ? 24 : 6);
212
248
  if (tree.lines.length > 0) {
213
249
  for (const line of tree.lines) {
@@ -223,6 +259,8 @@ function renderOutputSection(
223
259
  }
224
260
  }
225
261
 
262
+ lines.push(`${continuePrefix}${theme.fg("dim", "Output")}`);
263
+
226
264
  const outputLines = output.split("\n").filter((line) => line.trim());
227
265
  const previewCount = expanded ? maxExpanded : maxCollapsed;
228
266
  for (const line of outputLines.slice(0, previewCount)) {
@@ -238,6 +276,92 @@ function renderOutputSection(
238
276
  return lines;
239
277
  }
240
278
 
279
+ function formatVarsInline(vars: Record<string, string>, theme: Theme): string {
280
+ const entries = Object.entries(vars);
281
+ if (entries.length === 0) return "Vars: none";
282
+ const pairs = entries.map(([key, value]) => `${key}=${truncate(value, 24, theme.format.ellipsis)}`);
283
+ return `Vars: ${pairs.join(", ")}`;
284
+ }
285
+
286
+ function formatScalarInline(value: unknown, maxLen: number, theme: Theme): string {
287
+ if (value === null) return "null";
288
+ if (value === undefined) return "undefined";
289
+ if (typeof value === "boolean") return String(value);
290
+ if (typeof value === "number") return String(value);
291
+ if (typeof value === "string") return `"${truncate(value, maxLen, theme.format.ellipsis)}"`;
292
+ if (Array.isArray(value)) return `[${value.length} items]`;
293
+ if (typeof value === "object") {
294
+ const keys = Object.keys(value);
295
+ return `{${keys.length} keys}`;
296
+ }
297
+ return String(value);
298
+ }
299
+
300
+ function formatOutputInline(data: unknown, theme: Theme, maxWidth = 80): string {
301
+ if (data === null || data === undefined) return "Output: none";
302
+
303
+ // For scalars, show directly
304
+ if (typeof data !== "object") {
305
+ return `Output: ${formatScalarInline(data, 60, theme)}`;
306
+ }
307
+
308
+ // For arrays, show count and first element preview
309
+ if (Array.isArray(data)) {
310
+ if (data.length === 0) return "Output: []";
311
+ const preview = formatScalarInline(data[0], 40, theme);
312
+ return `Output: [${data.length} items] ${preview}${data.length > 1 ? theme.format.ellipsis : ""}`;
313
+ }
314
+
315
+ // For objects, show key=value pairs inline
316
+ const entries = Object.entries(data as Record<string, unknown>);
317
+ if (entries.length === 0) return "Output: {}";
318
+
319
+ const pairs: string[] = [];
320
+ let totalLen = "Output: ".length;
321
+
322
+ for (const [key, value] of entries) {
323
+ const valueStr = formatScalarInline(value, 24, theme);
324
+ const pairStr = `${key}=${valueStr}`;
325
+ const addLen = pairs.length > 0 ? pairStr.length + 2 : pairStr.length; // +2 for ", "
326
+
327
+ if (totalLen + addLen > maxWidth && pairs.length > 0) {
328
+ pairs.push(theme.format.ellipsis);
329
+ break;
330
+ }
331
+
332
+ pairs.push(pairStr);
333
+ totalLen += addLen;
334
+ }
335
+
336
+ return `Output: ${pairs.join(", ")}`;
337
+ }
338
+
339
+ function renderVarsSection(
340
+ vars: Record<string, string> | undefined,
341
+ continuePrefix: string,
342
+ expanded: boolean,
343
+ theme: Theme,
344
+ ): string[] {
345
+ if (!vars || Object.keys(vars).length === 0) return [];
346
+ const lines: string[] = [];
347
+
348
+ if (!expanded) {
349
+ lines.push(`${continuePrefix}${theme.fg("dim", formatVarsInline(vars, theme))}`);
350
+ return lines;
351
+ }
352
+
353
+ lines.push(`${continuePrefix}${theme.fg("dim", "Vars")}`);
354
+ const tree = renderJsonTreeLines(vars, theme, 4, 16);
355
+ for (const line of tree.lines) {
356
+ lines.push(`${continuePrefix} ${line}`);
357
+ }
358
+ if (tree.truncated) {
359
+ lines.push(`${continuePrefix} ${theme.fg("dim", theme.format.ellipsis)}`);
360
+ }
361
+
362
+ return lines;
363
+ }
364
+
241
365
  /**
242
366
  * Render the tool call arguments.
243
367
  */
@@ -247,24 +371,29 @@ export function renderCall(args: TaskParams, theme: Theme): Component {
247
371
  theme.fg("dim", `${theme.format.bracketLeft}${args.agent}${theme.format.bracketRight}`),
248
372
  );
249
373
 
250
- if (args.tasks.length === 1) {
251
- // Single task - show description preview
252
- const task = args.tasks[0];
253
- const summary = task.description.trim() || task.task;
254
- const taskPreview = truncate(summary, 50, theme.format.ellipsis);
255
- return new Text(`${label} ${agentTag} ${theme.fg("muted", taskPreview)}`, 0, 0);
256
- }
257
-
258
- // Multiple tasks - show count and descriptions
259
- const descriptions = args.tasks.map((t) => t.description.trim()).join(", ");
260
- return new Text(
261
- `${label} ${agentTag} ${args.tasks.length} agents: ${theme.fg(
262
- "muted",
263
- truncate(descriptions, 50, theme.format.ellipsis),
264
- )}`,
265
- 0,
266
- 0,
267
- );
374
+ const lines: string[] = [];
375
+ lines.push(`${label} ${agentTag}`);
376
+
377
+ const contextTemplate = args.context ?? "";
378
+ const context = contextTemplate.trim();
379
+ const hasContext = context.length > 0;
380
+ const branch = theme.fg("dim", theme.tree.branch);
381
+ const last = theme.fg("dim", theme.tree.last);
382
+ const vertical = theme.fg("dim", theme.tree.vertical);
383
+
384
+ if (hasContext) {
385
+ lines.push(` ${branch} ${theme.fg("dim", "Context")}`);
386
+ for (const line of context.split("\n")) {
387
+ const content = line ? theme.fg("muted", line) : "";
388
+ lines.push(` ${vertical} ${content}`);
389
+ }
390
+ lines.push(` ${last} ${theme.fg("dim", "Tasks")}: ${theme.fg("muted", `${args.tasks.length} agents`)}`);
391
+ return new Text(lines.join("\n"), 0, 0);
392
+ }
393
+
394
+ lines.push(`${theme.fg("dim", "Tasks")}: ${theme.fg("muted", `${args.tasks.length} agents`)}`);
395
+
396
+ return new Text(lines.join("\n"), 0, 0);
268
397
  }
269
398
 
270
399
  /**
@@ -278,8 +407,8 @@ function renderAgentProgress(
278
407
  spinnerFrame?: number,
279
408
  ): string[] {
280
409
  const lines: string[] = [];
281
- const prefix = isLast ? theme.tree.last : theme.tree.branch;
282
- const continuePrefix = isLast ? " " : `${theme.tree.vertical} `;
410
+ const prefix = isLast ? theme.fg("dim", theme.tree.last) : theme.fg("dim", theme.tree.branch);
411
+ const continuePrefix = isLast ? " " : `${theme.fg("dim", theme.tree.vertical)} `;
283
412
 
284
413
  const icon = getStatusIcon(progress.status, theme, spinnerFrame);
285
414
  const iconColor =
@@ -316,6 +445,8 @@ function renderAgentProgress(
316
445
 
317
446
  lines.push(statusLine);
318
447
 
448
+ lines.push(...renderVarsSection(progress.vars, continuePrefix, expanded, theme));
449
+
319
450
  // Current tool (if running) or most recent completed tool
320
451
  if (progress.status === "running") {
321
452
  if (progress.currentTool) {
@@ -498,8 +629,8 @@ function renderFindings(
498
629
  */
499
630
  function renderAgentResult(result: SingleResult, isLast: boolean, expanded: boolean, theme: Theme): string[] {
500
631
  const lines: string[] = [];
501
- const prefix = isLast ? theme.tree.last : theme.tree.branch;
502
- const continuePrefix = isLast ? " " : `${theme.tree.vertical} `;
632
+ const prefix = isLast ? theme.fg("dim", theme.tree.last) : theme.fg("dim", theme.tree.branch);
633
+ const continuePrefix = isLast ? " " : `${theme.fg("dim", theme.tree.vertical)} `;
503
634
 
504
635
  const aborted = result.aborted ?? false;
505
636
  const success = !aborted && result.exitCode === 0;
@@ -525,6 +656,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
525
656
  }
526
657
 
527
658
  lines.push(statusLine);
659
+ lines.push(...renderVarsSection(result.vars, continuePrefix, expanded, theme));
528
660
 
529
661
  // Check for review result (complete with review schema + report_finding)
530
662
  const completeData = result.extractedToolData?.complete as Array<{ data: unknown }> | undefined;
@@ -634,7 +766,7 @@ export function renderResult(
634
766
  const abortedCount = details.results.filter((r) => r.aborted).length;
635
767
  const successCount = details.results.filter((r) => !r.aborted && r.exitCode === 0).length;
636
768
  const failCount = details.results.length - successCount - abortedCount;
637
- let summary = `\n${theme.fg("dim", "Total:")} `;
769
+ let summary = `${theme.fg("dim", "Total:")} `;
638
770
  if (abortedCount > 0) {
639
771
  summary += theme.fg("error", `${abortedCount} aborted`);
640
772
  if (successCount > 0 || failCount > 0) summary += theme.sep.dot;
@@ -656,7 +788,8 @@ export function renderResult(
656
788
  return new Text(theme.fg("dim", "No results"), 0, 0);
657
789
  }
658
790
 
659
- return new Text(lines.join("\n"), 0, 0);
791
+ const indented = lines.map((line) => (line.trim() ? ` ${line}` : ""));
792
+ return new Text(indented.join("\n"), 0, 0);
660
793
  }
661
794
 
662
795
  export const taskToolRenderer = {
@@ -0,0 +1,37 @@
1
+ export function renderTemplate(template: string, vars: Record<string, string>): string {
2
+ return template.replace(/\{\{(\w+)\}\}/g, (_match, key: string) => vars[key] ?? `{{${key}}}`);
3
+ }
4
+
5
+ export function extractPlaceholders(template: string): string[] {
6
+ return [...template.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1]);
7
+ }
8
+
9
+ export function validateTaskTemplate(
10
+ context: string | undefined,
11
+ tasks: Array<{ id: string; vars: Record<string, string> }>,
12
+ ): string | null {
13
+ const template = context ?? "";
14
+ const placeholders = extractPlaceholders(template);
15
+
16
+ if (tasks.length > 1 && placeholders.length === 0) {
17
+ return "Multi-task invocations require {{placeholders}} in context";
18
+ }
19
+
20
+ if (placeholders.length > 0) {
21
+ for (const task of tasks) {
22
+ const missing = placeholders.filter((placeholder) => !(placeholder in task.vars));
23
+ if (missing.length > 0) {
24
+ return `Task "${task.id}" missing vars: ${missing.join(", ")}`;
25
+ }
26
+ }
27
+ }
28
+
29
+ if (tasks.length > 1 && placeholders.length > 0) {
30
+ const withoutPlaceholders = template.replace(/\{\{\w+\}\}/g, "").trim();
31
+ if (withoutPlaceholders.length < 50) {
32
+ return "Context must contain instructions (50+ chars) around {{placeholders}}";
33
+ }
34
+ }
35
+
36
+ return null;
37
+ }
@@ -46,8 +46,10 @@ export const taskItemSchema = Type.Object({
46
46
  description: "Short task identifier for display (max 32 chars, CamelCase, e.g. 'SessionStore', 'WebFetchFix')",
47
47
  maxLength: 32,
48
48
  }),
49
- task: Type.String({ description: "Task description for the agent" }),
50
49
  description: Type.String({ description: "Short description for UI display" }),
50
+ vars: Type.Record(Type.String(), Type.String(), {
51
+ description: "Template variables to fill {{placeholders}} in context",
52
+ }),
51
53
  });
52
54
 
53
55
  export type TaskItem = Static<typeof taskItemSchema>;
@@ -55,7 +57,7 @@ export type TaskItem = Static<typeof taskItemSchema>;
55
57
  /** Task tool parameters */
56
58
  export const taskSchema = Type.Object({
57
59
  agent: Type.String({ description: "Agent type to use for all tasks" }),
58
- context: Type.String({ description: "Shared context prepended to all task prompts" }),
60
+ context: Type.String({ description: "Template with {{placeholders}} filled by task vars" }),
59
61
  model: Type.Optional(
60
62
  Type.String({
61
63
  description: "Model override for all tasks (fuzzy matching, e.g. 'sonnet', 'opus')",
@@ -120,6 +122,7 @@ export interface AgentProgress {
120
122
  agentSource: AgentSource;
121
123
  status: "pending" | "running" | "completed" | "failed" | "aborted";
122
124
  task: string;
125
+ vars?: Record<string, string>;
123
126
  description?: string;
124
127
  currentTool?: string;
125
128
  currentToolArgs?: string;
@@ -141,6 +144,7 @@ export interface SingleResult {
141
144
  agent: string;
142
145
  agentSource: AgentSource;
143
146
  task: string;
147
+ vars?: Record<string, string>;
144
148
  description?: string;
145
149
  exitCode: number;
146
150
  output: string;
@@ -363,7 +363,7 @@ function createMCPProxyTool(metadata: MCPToolMetadata): CustomTool<TSchema> {
363
363
  }
364
364
 
365
365
  function getPythonCallTimeoutMs(params: PythonToolParams): number | undefined {
366
- const timeout = params.timeout;
366
+ const timeout = params.timeoutMs;
367
367
  if (typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0) {
368
368
  return Math.max(1000, Math.round(timeout * 1000) + 1000);
369
369
  }
package/src/index.ts CHANGED
@@ -95,6 +95,8 @@ export type {
95
95
  TurnStartEvent,
96
96
  UserBashEvent,
97
97
  UserBashEventResult,
98
+ UserPythonEvent,
99
+ UserPythonEventResult,
98
100
  } from "./core/extensions/index";
99
101
  // Extension types and utilities
100
102
  export {
@@ -201,8 +203,6 @@ export {
201
203
  type FindToolDetails,
202
204
  type FindToolOptions,
203
205
  formatSize,
204
- GitTool,
205
- type GitToolDetails,
206
206
  type GrepOperations,
207
207
  type GrepToolDetails,
208
208
  type GrepToolOptions,
package/src/main.ts CHANGED
@@ -18,6 +18,7 @@ import { listModels } from "./cli/list-models";
18
18
  import { parsePluginArgs, printPluginHelp, runPluginCommand } from "./cli/plugin-cli";
19
19
  import { selectSession } from "./cli/session-picker";
20
20
  import { parseSetupArgs, printSetupHelp, runSetupCommand } from "./cli/setup-cli";
21
+ import { parseStatsArgs, printStatsHelp, runStatsCommand } from "./cli/stats-cli";
21
22
  import { parseUpdateArgs, printUpdateHelp, runUpdateCommand } from "./cli/update-cli";
22
23
  import { findConfigFile, getModelsPath, VERSION } from "./config";
23
24
  import type { AgentSession } from "./core/agent-session";
@@ -520,6 +521,17 @@ export async function main(args: string[]) {
520
521
  return;
521
522
  }
522
523
 
524
+ // Handle stats subcommand
525
+ const statsCmd = parseStatsArgs(args);
526
+ if (statsCmd) {
527
+ if (args.includes("--help") || args.includes("-h")) {
528
+ printStatsHelp();
529
+ return;
530
+ }
531
+ await runStatsCommand(statsCmd);
532
+ return;
533
+ }
534
+
523
535
  const parsed = parseArgs(args);
524
536
  time("parseArgs");
525
537
  await maybeAutoChdir(parsed);