@oh-my-pi/pi-coding-agent 13.10.0 → 13.11.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/package.json +7 -7
  3. package/src/commit/agentic/agent.ts +3 -1
  4. package/src/commit/agentic/index.ts +7 -1
  5. package/src/commit/analysis/conventional.ts +5 -1
  6. package/src/commit/analysis/summary.ts +5 -1
  7. package/src/commit/changelog/generate.ts +5 -1
  8. package/src/commit/changelog/index.ts +4 -0
  9. package/src/commit/map-reduce/index.ts +5 -0
  10. package/src/commit/map-reduce/map-phase.ts +17 -2
  11. package/src/commit/map-reduce/reduce-phase.ts +5 -1
  12. package/src/commit/model-selection.ts +38 -26
  13. package/src/commit/pipeline.ts +22 -11
  14. package/src/config/settings-schema.ts +20 -0
  15. package/src/config.ts +10 -3
  16. package/src/discovery/helpers.ts +7 -3
  17. package/src/internal-urls/docs-index.generated.ts +1 -1
  18. package/src/lsp/index.ts +4 -4
  19. package/src/lsp/utils.ts +81 -0
  20. package/src/main.ts +25 -14
  21. package/src/mcp/manager.ts +40 -2
  22. package/src/mcp/oauth-flow.ts +41 -0
  23. package/src/mcp/transports/http.ts +23 -0
  24. package/src/mcp/types.ts +6 -0
  25. package/src/modes/components/mcp-add-wizard.ts +12 -0
  26. package/src/modes/components/settings-defs.ts +2 -1
  27. package/src/modes/components/todo-reminder.ts +8 -1
  28. package/src/modes/controllers/command-controller.ts +75 -3
  29. package/src/modes/controllers/input-controller.ts +2 -3
  30. package/src/modes/controllers/mcp-command-controller.ts +9 -1
  31. package/src/modes/interactive-mode.ts +11 -7
  32. package/src/modes/theme/theme.ts +30 -27
  33. package/src/modes/types.ts +2 -1
  34. package/src/patch/hashline.ts +3 -6
  35. package/src/prompts/system/eager-todo.md +13 -0
  36. package/src/prompts/tools/ast-edit.md +1 -1
  37. package/src/prompts/tools/ast-grep.md +1 -1
  38. package/src/prompts/tools/find.md +1 -0
  39. package/src/prompts/tools/grep.md +1 -0
  40. package/src/prompts/tools/hashline.md +23 -111
  41. package/src/prompts/tools/todo-write.md +11 -1
  42. package/src/sdk.ts +1 -1
  43. package/src/session/agent-session.ts +85 -7
  44. package/src/session/session-manager.ts +5 -9
  45. package/src/slash-commands/builtin-registry.ts +10 -2
  46. package/src/task/executor.ts +9 -18
  47. package/src/task/index.ts +8 -4
  48. package/src/task/render.ts +5 -10
  49. package/src/task/template.ts +4 -1
  50. package/src/task/types.ts +2 -0
  51. package/src/tools/ast-edit.ts +26 -7
  52. package/src/tools/ast-grep.ts +26 -9
  53. package/src/tools/fetch.ts +36 -5
  54. package/src/tools/find.ts +13 -64
  55. package/src/tools/grep.ts +27 -10
  56. package/src/tools/json-tree.ts +1 -1
  57. package/src/tools/output-meta.ts +2 -1
  58. package/src/tools/path-utils.ts +348 -0
  59. package/src/tools/todo-write.ts +27 -4
  60. package/src/utils/commit-message-generator.ts +27 -22
  61. package/src/utils/image-input.ts +1 -1
  62. package/src/utils/image-resize.ts +4 -4
  63. package/src/utils/title-generator.ts +36 -23
  64. package/src/utils/tool-choice.ts +28 -0
  65. package/src/web/parallel.ts +346 -0
  66. package/src/web/scrapers/youtube.ts +29 -0
  67. package/src/web/search/provider.ts +4 -1
  68. package/src/web/search/providers/parallel.ts +63 -0
  69. package/src/web/search/types.ts +1 -0
@@ -89,6 +89,7 @@ import { getCurrentThemeName, theme } from "../modes/theme/theme";
89
89
  import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../patch";
90
90
  import type { PlanModeState } from "../plan-mode/state";
91
91
  import autoHandoffThresholdFocusPrompt from "../prompts/system/auto-handoff-threshold-focus.md" with { type: "text" };
92
+ import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
92
93
  import handoffDocumentPrompt from "../prompts/system/handoff-document.md" with { type: "text" };
93
94
  import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { type: "text" };
94
95
  import planModeReferencePrompt from "../prompts/system/plan-mode-reference.md" with { type: "text" };
@@ -106,6 +107,7 @@ import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from ".
106
107
  import { parseCommandArgs } from "../utils/command-args";
107
108
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
108
109
  import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
110
+ import { buildNamedToolChoice } from "../utils/tool-choice";
109
111
  import {
110
112
  type CompactionResult,
111
113
  calculateContextTokens,
@@ -350,6 +352,7 @@ export class AgentSession {
350
352
  // Todo completion reminder state
351
353
  #todoReminderCount = 0;
352
354
  #todoPhases: TodoPhase[] = [];
355
+ #nextToolChoiceOverride: ToolChoice | undefined = undefined;
353
356
 
354
357
  // Bash execution state
355
358
  #bashAbortController: AbortController | undefined = undefined;
@@ -457,6 +460,12 @@ export class AgentSession {
457
460
  return this.#modelRegistry;
458
461
  }
459
462
 
463
+ consumeNextToolChoiceOverride(): ToolChoice | undefined {
464
+ const toolChoice = this.#nextToolChoiceOverride;
465
+ this.#nextToolChoiceOverride = undefined;
466
+ return toolChoice;
467
+ }
468
+
460
469
  /** Provider-scoped mutable state store for transport/session caches. */
461
470
  get providerSessionState(): Map<string, ProviderSessionState> {
462
471
  return this.#providerSessionState;
@@ -791,7 +800,11 @@ export class AgentSession {
791
800
  const compactionTask = this.#checkCompaction(msg);
792
801
  this.#trackPostPromptTask(compactionTask);
793
802
  await compactionTask;
794
- // Check for incomplete todos (unless there was an error or abort)
803
+ // Check for incomplete todos only after a final assistant stop, not intermediate tool-use turns.
804
+ const hasToolCalls = msg.content.some(content => content.type === "toolCall");
805
+ if (hasToolCalls) {
806
+ return;
807
+ }
795
808
  if (msg.stopReason !== "error" && msg.stopReason !== "aborted") {
796
809
  if (this.#enforceRewindBeforeYield()) {
797
810
  return;
@@ -1349,6 +1362,7 @@ export class AgentSession {
1349
1362
  if (!this.#extensionRunner) return;
1350
1363
  if (event.type === "agent_start") {
1351
1364
  this.#turnIndex = 0;
1365
+ this.#nextToolChoiceOverride = undefined;
1352
1366
  await this.#extensionRunner.emit({ type: "agent_start" });
1353
1367
  } else if (event.type === "agent_end") {
1354
1368
  await this.#extensionRunner.emit({ type: "agent_end", messages: event.messages });
@@ -1945,6 +1959,8 @@ export class AgentSession {
1945
1959
  return;
1946
1960
  }
1947
1961
 
1962
+ const eagerTodoPrelude = !options?.synthetic ? this.#createEagerTodoPrelude() : undefined;
1963
+
1948
1964
  const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }];
1949
1965
  if (options?.images) {
1950
1966
  userContent.push(...options.images);
@@ -1954,7 +1970,20 @@ export class AgentSession {
1954
1970
  ? { role: "developer" as const, content: userContent, attribution: "agent" as const, timestamp: Date.now() }
1955
1971
  : { role: "user" as const, content: userContent, attribution: "user" as const, timestamp: Date.now() };
1956
1972
 
1957
- await this.#promptWithMessage(message, expandedText, options);
1973
+ if (eagerTodoPrelude) {
1974
+ this.#nextToolChoiceOverride = eagerTodoPrelude.toolChoice;
1975
+ }
1976
+
1977
+ try {
1978
+ await this.#promptWithMessage(message, expandedText, {
1979
+ ...options,
1980
+ prependMessages: eagerTodoPrelude ? [eagerTodoPrelude.message] : undefined,
1981
+ });
1982
+ } finally {
1983
+ if (eagerTodoPrelude) {
1984
+ this.#nextToolChoiceOverride = undefined;
1985
+ }
1986
+ }
1958
1987
  if (!options?.synthetic) {
1959
1988
  await this.#enforcePlanModeToolDecision();
1960
1989
  }
@@ -1997,6 +2026,7 @@ export class AgentSession {
1997
2026
  message: AgentMessage,
1998
2027
  expandedText: string,
1999
2028
  options?: Pick<PromptOptions, "toolChoice" | "images" | "skipCompactionCheck"> & {
2029
+ prependMessages?: AgentMessage[];
2000
2030
  skipPostPromptRecoveryWait?: boolean;
2001
2031
  },
2002
2032
  ): Promise<void> {
@@ -2034,7 +2064,7 @@ export class AgentSession {
2034
2064
  await this.#checkCompaction(lastAssistant, false);
2035
2065
  }
2036
2066
 
2037
- // Build messages array (custom messages if any, then user message)
2067
+ // Build messages array (session context, eager todo prelude, then active prompt message)
2038
2068
  const messages: AgentMessage[] = [];
2039
2069
  const planReferenceMessage = await this.#buildPlanReferenceMessage?.();
2040
2070
  if (planReferenceMessage) {
@@ -2044,6 +2074,9 @@ export class AgentSession {
2044
2074
  if (planModeMessage) {
2045
2075
  messages.push(planModeMessage);
2046
2076
  }
2077
+ if (options?.prependMessages) {
2078
+ messages.push(...options.prependMessages);
2079
+ }
2047
2080
 
2048
2081
  messages.push(message);
2049
2082
 
@@ -3481,6 +3514,51 @@ export class AgentSession {
3481
3514
  this.agent.setTools(previousTools);
3482
3515
  }
3483
3516
  }
3517
+
3518
+ #createEagerTodoPrelude(): { message: AgentMessage; toolChoice: ToolChoice } | undefined {
3519
+ const eagerTodosEnabled = this.settings.get("todo.eager");
3520
+ const todosEnabled = this.settings.get("todo.enabled");
3521
+ if (!eagerTodosEnabled || !todosEnabled) {
3522
+ return undefined;
3523
+ }
3524
+
3525
+ if (this.#planModeState?.enabled) {
3526
+ return undefined;
3527
+ }
3528
+ if (this.getTodoPhases().length > 0) {
3529
+ return undefined;
3530
+ }
3531
+
3532
+ if (!this.#toolRegistry.has("todo_write")) {
3533
+ logger.warn("Eager todo enforcement skipped because todo_write is unavailable", {
3534
+ activeToolNames: this.agent.state.tools.map(tool => tool.name),
3535
+ });
3536
+ return undefined;
3537
+ }
3538
+
3539
+ const todoWriteToolChoice = buildNamedToolChoice("todo_write", this.model);
3540
+ if (!todoWriteToolChoice) {
3541
+ logger.warn("Eager todo enforcement skipped because the current model does not support forcing todo_write", {
3542
+ modelApi: this.model?.api,
3543
+ modelId: this.model?.id,
3544
+ });
3545
+ return undefined;
3546
+ }
3547
+
3548
+ const eagerTodoReminder = renderPromptTemplate(eagerTodoPrompt);
3549
+
3550
+ return {
3551
+ message: {
3552
+ role: "custom",
3553
+ customType: "eager-todo-prelude",
3554
+ content: eagerTodoReminder,
3555
+ display: false,
3556
+ attribution: "agent",
3557
+ timestamp: Date.now(),
3558
+ },
3559
+ toolChoice: todoWriteToolChoice,
3560
+ };
3561
+ }
3484
3562
  /**
3485
3563
  * Check if agent stopped with incomplete todos and prompt to continue.
3486
3564
  */
@@ -5191,8 +5269,8 @@ export class AgentSession {
5191
5269
  }
5192
5270
 
5193
5271
  for (const msg of this.messages) {
5194
- if (msg.role === "user") {
5195
- lines.push("## User\n");
5272
+ if (msg.role === "user" || msg.role === "developer") {
5273
+ lines.push(msg.role === "developer" ? "## Developer\n" : "## User\n");
5196
5274
  if (typeof msg.content === "string") {
5197
5275
  lines.push(msg.content);
5198
5276
  } else {
@@ -5316,8 +5394,8 @@ export class AgentSession {
5316
5394
  lines.push("");
5317
5395
 
5318
5396
  for (const msg of this.messages) {
5319
- if (msg.role === "user") {
5320
- lines.push("## User");
5397
+ if (msg.role === "user" || msg.role === "developer") {
5398
+ lines.push(msg.role === "developer" ? "## Developer" : "## User");
5321
5399
  lines.push("");
5322
5400
  if (typeof msg.content === "string") {
5323
5401
  lines.push(msg.content);
@@ -629,15 +629,11 @@ function writeTerminalBreadcrumb(cwd: string, sessionFile: string): void {
629
629
  const terminalId = getTerminalId();
630
630
  if (!terminalId) return;
631
631
 
632
- try {
633
- const breadcrumbDir = path.join(getDefaultAgentDir(), TERMINAL_SESSIONS_DIR);
634
- const breadcrumbFile = path.join(breadcrumbDir, terminalId);
635
- const content = `${cwd}\n${sessionFile}\n`;
636
- // Bun.write auto-creates parent dirs
637
- void Bun.write(breadcrumbFile, content);
638
- } catch {
639
- // Best-effort — don't break session creation if breadcrumb fails
640
- }
632
+ const breadcrumbDir = path.join(getDefaultAgentDir(), TERMINAL_SESSIONS_DIR);
633
+ const breadcrumbFile = path.join(breadcrumbDir, terminalId);
634
+ const content = `${cwd}\n${sessionFile}\n`;
635
+ // Best-effort don't break session creation if breadcrumb fails
636
+ Bun.write(breadcrumbFile, content).catch(() => {});
641
637
  }
642
638
 
643
639
  /**
@@ -214,8 +214,16 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
214
214
  {
215
215
  name: "copy",
216
216
  description: "Copy last agent message to clipboard",
217
- handle: async (_command, runtime) => {
218
- await runtime.ctx.handleCopyCommand();
217
+ subcommands: [
218
+ { name: "last", description: "Copy full last agent message" },
219
+ { name: "code", description: "Copy last code block" },
220
+ { name: "all", description: "Copy all code blocks from last message" },
221
+ { name: "cmd", description: "Copy last bash/python command" },
222
+ ],
223
+ allowArgs: true,
224
+ handle: async (command, runtime) => {
225
+ const sub = command.args.trim().toLowerCase() || undefined;
226
+ await runtime.ctx.handleCopyCommand(sub);
219
227
  runtime.ctx.editor.setText("");
220
228
  },
221
229
  },
@@ -5,7 +5,6 @@
5
5
  */
6
6
  import path from "node:path";
7
7
  import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
- import type { Api, Model, ToolChoice } from "@oh-my-pi/pi-ai";
9
8
  import { logger, untilAborted } from "@oh-my-pi/pi-utils";
10
9
  import type { TSchema } from "@sinclair/typebox";
11
10
  import Ajv, { type ValidateFunction } from "ajv";
@@ -28,6 +27,7 @@ import { type ContextFileEntry, truncateTail } from "../tools";
28
27
  import { jtdToJsonSchema } from "../tools/jtd-to-json-schema";
29
28
  import { ToolAbortError } from "../tools/tool-errors";
30
29
  import type { EventBus } from "../utils/event-bus";
30
+ import { buildNamedToolChoice } from "../utils/tool-choice";
31
31
  import { subprocessToolRegistry } from "./subprocess-tool-registry";
32
32
  import {
33
33
  type AgentDefinition,
@@ -117,28 +117,13 @@ function getReportFindingKey(value: unknown): string | null {
117
117
  return `${filePath}:${lineStart}:${lineEnd}:${priority ?? ""}:${title}`;
118
118
  }
119
119
 
120
- function buildSubmitResultToolChoice(model?: Model<Api>): ToolChoice | undefined {
121
- if (!model) return undefined;
122
- if (
123
- model.api === "openai-codex-responses" ||
124
- model.api === "openai-responses" ||
125
- model.api === "openai-completions" ||
126
- model.api === "azure-openai-responses"
127
- ) {
128
- return { type: "function", name: "submit_result" };
129
- }
130
- if (model.api === "anthropic-messages" || model.api === "bedrock-converse-stream") {
131
- return { type: "tool", name: "submit_result" };
132
- }
133
- return undefined;
134
- }
135
-
136
120
  /** Options for subagent execution */
137
121
  export interface ExecutorOptions {
138
122
  cwd: string;
139
123
  worktree?: string;
140
124
  agent: AgentDefinition;
141
125
  task: string;
126
+ assignment?: string;
142
127
  description?: string;
143
128
  index: number;
144
129
  id: string;
@@ -458,6 +443,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
458
443
  cwd,
459
444
  agent,
460
445
  task,
446
+ assignment,
461
447
  index,
462
448
  id,
463
449
  worktree,
@@ -478,6 +464,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
478
464
  agentSource: agent.source,
479
465
  status: "running",
480
466
  task,
467
+ assignment,
481
468
  description: options.description,
482
469
  lastIntent: undefined,
483
470
  recentTools: [],
@@ -496,6 +483,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
496
483
  agent: agent.name,
497
484
  agentSource: agent.source,
498
485
  task,
486
+ assignment,
499
487
  description: options.description,
500
488
  exitCode: 1,
501
489
  output: "",
@@ -638,6 +626,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
638
626
  agent: agent.name,
639
627
  agentSource: agent.source,
640
628
  task,
629
+ assignment,
641
630
  progress: { ...progress },
642
631
  });
643
632
  }
@@ -727,6 +716,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
727
716
  agent: agent.name,
728
717
  agentSource: agent.source,
729
718
  task,
719
+ assignment,
730
720
  event,
731
721
  });
732
722
  }
@@ -1091,7 +1081,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1091
1081
  await session.prompt(task);
1092
1082
  await session.waitForIdle();
1093
1083
 
1094
- const reminderToolChoice = buildSubmitResultToolChoice(session.model);
1084
+ const reminderToolChoice = buildNamedToolChoice("submit_result", session.model);
1095
1085
 
1096
1086
  let retryCount = 0;
1097
1087
  while (!submitResultCalled && retryCount < MAX_SUBMIT_RESULT_RETRIES && !abortSignal.aborted) {
@@ -1247,6 +1237,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1247
1237
  agent: agent.name,
1248
1238
  agentSource: agent.source,
1249
1239
  task,
1240
+ assignment,
1250
1241
  description: options.description,
1251
1242
  lastIntent: progress.lastIntent,
1252
1243
  exitCode,
package/src/task/index.ts CHANGED
@@ -224,6 +224,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
224
224
  agentSource: fallbackAgentSource,
225
225
  status: "pending",
226
226
  task: renderedTask.task,
227
+ assignment: renderedTask.assignment,
227
228
  description: renderedTask.description,
228
229
  recentTools: [],
229
230
  recentOutput: [],
@@ -732,6 +733,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
732
733
  agentSource: agent.source,
733
734
  status: "pending",
734
735
  task: t.task,
736
+ assignment: t.assignment,
735
737
  recentTools: [],
736
738
  recentOutput: [],
737
739
  toolCount: 0,
@@ -749,6 +751,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
749
751
  cwd: this.session.cwd,
750
752
  agent,
751
753
  task: task.task,
754
+ assignment: task.assignment,
752
755
  description: task.description,
753
756
  index,
754
757
  id: task.id,
@@ -801,6 +804,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
801
804
  worktree: isolationDir,
802
805
  agent,
803
806
  task: task.task,
807
+ assignment: task.assignment,
804
808
  description: task.description,
805
809
  index,
806
810
  id: task.id,
@@ -834,11 +838,10 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
834
838
  const commitMsg =
835
839
  commitStyle === "ai" && this.session.modelRegistry
836
840
  ? async (diff: string) => {
837
- const smolModel = this.session.settings.getModelRole("smol");
838
841
  return generateCommitMessage(
839
842
  diff,
840
843
  this.session.modelRegistry!,
841
- smolModel,
844
+ this.session.settings,
842
845
  this.session.getSessionId?.() ?? undefined,
843
846
  );
844
847
  }
@@ -887,6 +890,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
887
890
  agent: agent.name,
888
891
  agentSource: agent.source,
889
892
  task: task.task,
893
+ assignment: task.assignment,
890
894
  description: task.description,
891
895
  exitCode: 1,
892
896
  output: "",
@@ -930,6 +934,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
930
934
  agent: agentName,
931
935
  agentSource: agent.source,
932
936
  task: task.task,
937
+ assignment: task.assignment,
933
938
  description: task.description,
934
939
  exitCode: 1,
935
940
  output: "",
@@ -1081,11 +1086,10 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
1081
1086
  const commitMsg =
1082
1087
  commitStyle === "ai" && this.session.modelRegistry
1083
1088
  ? async (diff: string) => {
1084
- const smolModel = this.session.settings.getModelRole("smol");
1085
1089
  return generateCommitMessage(
1086
1090
  diff,
1087
1091
  this.session.modelRegistry!,
1088
- smolModel,
1092
+ this.session.settings,
1089
1093
  this.session.getSessionId?.() ?? undefined,
1090
1094
  );
1091
1095
  }
@@ -374,16 +374,11 @@ function renderTaskSection(
374
374
  maxExpanded = 20,
375
375
  ): string[] {
376
376
  const lines: string[] = [];
377
- const trimmed = task.trimEnd();
377
+ const trimmed = task.trim();
378
378
  if (!expanded || !trimmed) return lines;
379
379
 
380
- // Strip the shared <context>...</context> block — it's the same
381
- // across all tasks and just adds noise when expanded.
382
- const stripped = trimmed.replace(/<context>[\s\S]*?<\/context>\s*/, "").trimStart();
383
- if (!stripped) return lines;
384
-
385
380
  lines.push(`${continuePrefix}${theme.fg("dim", "Task")}`);
386
- const taskLines = stripped.split("\n");
381
+ const taskLines = trimmed.split("\n");
387
382
  for (const line of taskLines.slice(0, maxExpanded)) {
388
383
  lines.push(`${continuePrefix} ${theme.fg("dim", truncateToWidth(replaceTabs(line), 70))}`);
389
384
  }
@@ -526,7 +521,7 @@ function renderAgentProgress(
526
521
 
527
522
  if (progress.status === "running") {
528
523
  if (!description) {
529
- const taskPreview = truncateToWidth(progress.task, 40);
524
+ const taskPreview = truncateToWidth(progress.assignment ?? progress.task, 40);
530
525
  statusLine += ` ${theme.fg("muted", taskPreview)}`;
531
526
  }
532
527
  if (progress.toolCount > 0) {
@@ -546,7 +541,7 @@ function renderAgentProgress(
546
541
 
547
542
  lines.push(statusLine);
548
543
 
549
- lines.push(...renderTaskSection(progress.task, continuePrefix, expanded, theme));
544
+ lines.push(...renderTaskSection(progress.assignment ?? progress.task, continuePrefix, expanded, theme));
550
545
 
551
546
  // Current tool (if running) or most recent completed tool
552
547
  if (progress.status === "running") {
@@ -781,7 +776,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
781
776
 
782
777
  lines.push(statusLine);
783
778
 
784
- lines.push(...renderTaskSection(result.task, continuePrefix, expanded, theme));
779
+ lines.push(...renderTaskSection(result.assignment ?? result.task, continuePrefix, expanded, theme));
785
780
 
786
781
  if (aborted && result.abortReason) {
787
782
  lines.push(
@@ -5,6 +5,8 @@ import type { TaskItem } from "./types";
5
5
  interface RenderResult {
6
6
  /** Full task text sent to the subagent */
7
7
  task: string;
8
+ /** Raw per-task assignment text, without prompt template boilerplate */
9
+ assignment: string;
8
10
  id: string;
9
11
  description: string;
10
12
  }
@@ -20,10 +22,11 @@ export function renderTemplate(context: string | undefined, task: TaskItem): Ren
20
22
  context = context?.trim();
21
23
 
22
24
  if (!context || !assignment) {
23
- return { task: assignment || context!, id, description };
25
+ return { task: assignment || context!, assignment: assignment || context!, id, description };
24
26
  }
25
27
  return {
26
28
  task: renderPromptTemplate(subagentUserPromptTemplate, { context, assignment }),
29
+ assignment,
27
30
  id,
28
31
  description,
29
32
  };
package/src/task/types.ts CHANGED
@@ -136,6 +136,7 @@ export interface AgentProgress {
136
136
  agentSource: AgentSource;
137
137
  status: "pending" | "running" | "completed" | "failed" | "aborted";
138
138
  task: string;
139
+ assignment?: string;
139
140
  description?: string;
140
141
  lastIntent?: string;
141
142
  currentTool?: string;
@@ -158,6 +159,7 @@ export interface SingleResult {
158
159
  agent: string;
159
160
  agentSource: AgentSource;
160
161
  task: string;
162
+ assignment?: string;
161
163
  description?: string;
162
164
  lastIntent?: string;
163
165
  exitCode: number;
@@ -14,7 +14,13 @@ import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, t
14
14
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
15
15
  import type { ToolSession } from ".";
16
16
  import type { OutputMeta } from "./output-meta";
17
- import { combineSearchGlobs, hasGlobPathChars, parseSearchPath, resolveToCwd } from "./path-utils";
17
+ import {
18
+ combineSearchGlobs,
19
+ hasGlobPathChars,
20
+ parseSearchPath,
21
+ resolveMultiSearchPath,
22
+ resolveToCwd,
23
+ } from "./path-utils";
18
24
  import {
19
25
  dedupeParseErrors,
20
26
  formatCount,
@@ -98,7 +104,12 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
98
104
  }
99
105
  const maxFiles = parseInt(process.env.PI_MAX_AST_FILES ?? "", 10) || 1000;
100
106
 
107
+ const formatScopePath = (targetPath: string): string => {
108
+ const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");
109
+ return relative.length === 0 ? "." : relative;
110
+ };
101
111
  let searchPath: string | undefined;
112
+ let scopePath: string | undefined;
102
113
  let globFilter = params.glob?.trim() || undefined;
103
114
  const rawPath = params.path?.trim();
104
115
  if (rawPath) {
@@ -112,21 +123,29 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
112
123
  throw new ToolError(`Cannot rewrite internal URL without backing file: ${rawPath}`);
113
124
  }
114
125
  searchPath = resource.sourcePath;
126
+ scopePath = formatScopePath(searchPath);
115
127
  } else {
116
- const parsedPath = parseSearchPath(rawPath);
117
- searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
118
- globFilter = combineSearchGlobs(parsedPath.glob, globFilter);
128
+ const multiSearchPath = await resolveMultiSearchPath(rawPath, this.session.cwd, globFilter);
129
+ if (multiSearchPath) {
130
+ searchPath = multiSearchPath.basePath;
131
+ globFilter = multiSearchPath.glob;
132
+ scopePath = multiSearchPath.scopePath;
133
+ } else {
134
+ const parsedPath = parseSearchPath(rawPath);
135
+ searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
136
+ globFilter = combineSearchGlobs(parsedPath.glob, globFilter);
137
+ scopePath = formatScopePath(searchPath);
138
+ }
119
139
  }
120
140
  }
121
-
122
141
  const resolvedSearchPath = searchPath ?? resolveToCwd(".", this.session.cwd);
123
- const scopePath = path.relative(this.session.cwd, resolvedSearchPath).replace(/\\/g, "/") || ".";
142
+ scopePath = scopePath ?? formatScopePath(resolvedSearchPath);
124
143
  let isDirectory: boolean;
125
144
  try {
126
145
  const stat = await Bun.file(resolvedSearchPath).stat();
127
146
  isDirectory = stat.isDirectory();
128
147
  } catch {
129
- throw new ToolError(`Path not found: ${resolvedSearchPath}`);
148
+ throw new ToolError(`Path not found: ${scopePath}`);
130
149
  }
131
150
 
132
151
  const result = await astEdit({
@@ -14,7 +14,13 @@ import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, t
14
14
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
15
15
  import type { ToolSession } from ".";
16
16
  import type { OutputMeta } from "./output-meta";
17
- import { combineSearchGlobs, hasGlobPathChars, parseSearchPath, resolveToCwd } from "./path-utils";
17
+ import {
18
+ combineSearchGlobs,
19
+ hasGlobPathChars,
20
+ parseSearchPath,
21
+ resolveMultiSearchPath,
22
+ resolveToCwd,
23
+ } from "./path-utils";
18
24
  import {
19
25
  dedupeParseErrors,
20
26
  formatCount,
@@ -86,7 +92,12 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
86
92
  throw new ToolError("Context must be a non-negative number");
87
93
  }
88
94
 
95
+ const formatScopePath = (targetPath: string): string => {
96
+ const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");
97
+ return relative.length === 0 ? "." : relative;
98
+ };
89
99
  let searchPath: string | undefined;
100
+ let scopePath: string | undefined;
90
101
  let globFilter = params.glob?.trim() || undefined;
91
102
  const rawPath = params.path?.trim();
92
103
  if (rawPath) {
@@ -100,24 +111,30 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
100
111
  throw new ToolError(`Cannot search internal URL without backing file: ${rawPath}`);
101
112
  }
102
113
  searchPath = resource.sourcePath;
114
+ scopePath = formatScopePath(searchPath);
103
115
  } else {
104
- const parsedPath = parseSearchPath(rawPath);
105
- searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
106
- globFilter = combineSearchGlobs(parsedPath.glob, globFilter);
116
+ const multiSearchPath = await resolveMultiSearchPath(rawPath, this.session.cwd, globFilter);
117
+ if (multiSearchPath) {
118
+ searchPath = multiSearchPath.basePath;
119
+ globFilter = multiSearchPath.glob;
120
+ scopePath = multiSearchPath.scopePath;
121
+ } else {
122
+ const parsedPath = parseSearchPath(rawPath);
123
+ searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
124
+ globFilter = combineSearchGlobs(parsedPath.glob, globFilter);
125
+ scopePath = formatScopePath(searchPath);
126
+ }
107
127
  }
108
128
  }
109
129
 
110
130
  const resolvedSearchPath = searchPath ?? resolveToCwd(".", this.session.cwd);
111
- const scopePath = (() => {
112
- const relative = path.relative(this.session.cwd, resolvedSearchPath).replace(/\\/g, "/");
113
- return relative.length === 0 ? "." : relative;
114
- })();
131
+ scopePath = scopePath ?? formatScopePath(resolvedSearchPath);
115
132
  let isDirectory: boolean;
116
133
  try {
117
134
  const stat = await Bun.file(resolvedSearchPath).stat();
118
135
  isDirectory = stat.isDirectory();
119
136
  } catch {
120
- throw new ToolError(`Path not found: ${resolvedSearchPath}`);
137
+ throw new ToolError(`Path not found: ${scopePath}`);
121
138
  }
122
139
 
123
140
  const result = await astGrep({