@oh-my-pi/pi-coding-agent 14.9.9 → 15.0.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.
Files changed (230) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/examples/extensions/plan-mode.ts +0 -1
  3. package/package.json +9 -9
  4. package/scripts/build-binary.ts +5 -0
  5. package/scripts/format-prompts.ts +1 -1
  6. package/src/autoresearch/helpers.ts +17 -0
  7. package/src/autoresearch/tools/log-experiment.ts +9 -17
  8. package/src/autoresearch/tools/run-experiment.ts +2 -17
  9. package/src/capability/skill.ts +7 -0
  10. package/src/cli/args.ts +2 -2
  11. package/src/cli/list-models.ts +1 -1
  12. package/src/cli/shell-cli.ts +3 -13
  13. package/src/cli/update-cli.ts +1 -1
  14. package/src/cli.ts +11 -29
  15. package/src/commands/acp.ts +24 -0
  16. package/src/commands/launch.ts +6 -4
  17. package/src/commit/agentic/prompts/system.md +1 -1
  18. package/src/commit/agentic/tools/propose-changelog.ts +8 -1
  19. package/src/commit/analysis/conventional.ts +8 -66
  20. package/src/commit/map-reduce/reduce-phase.ts +6 -65
  21. package/src/commit/pipeline.ts +2 -2
  22. package/src/commit/shared-llm.ts +89 -0
  23. package/src/config/config-file.ts +210 -0
  24. package/src/config/model-equivalence.ts +8 -11
  25. package/src/config/model-registry.ts +13 -2
  26. package/src/config/model-resolver.ts +31 -4
  27. package/src/config/settings-schema.ts +102 -1
  28. package/src/config/settings.ts +1 -1
  29. package/src/config.ts +3 -219
  30. package/src/edit/index.ts +22 -1
  31. package/src/edit/modes/patch.ts +10 -0
  32. package/src/edit/modes/replace.ts +3 -0
  33. package/src/edit/renderer.ts +17 -1
  34. package/src/eval/js/context-manager.ts +1 -1
  35. package/src/eval/js/executor.ts +3 -0
  36. package/src/eval/js/shared/rewrite-imports.ts +122 -50
  37. package/src/eval/js/shared/runtime.ts +31 -4
  38. package/src/eval/js/tool-bridge.ts +43 -21
  39. package/src/eval/py/executor.ts +5 -0
  40. package/src/exa/factory.ts +2 -2
  41. package/src/exa/mcp-client.ts +74 -1
  42. package/src/exec/bash-executor.ts +5 -1
  43. package/src/export/html/template.generated.ts +1 -1
  44. package/src/export/html/template.js +0 -11
  45. package/src/extensibility/extensions/runner.ts +55 -2
  46. package/src/extensibility/extensions/types.ts +98 -221
  47. package/src/extensibility/hooks/types.ts +89 -314
  48. package/src/extensibility/shared-events.ts +343 -0
  49. package/src/extensibility/skills.ts +42 -1
  50. package/src/goals/index.ts +3 -0
  51. package/src/goals/runtime.ts +500 -0
  52. package/src/goals/state.ts +37 -0
  53. package/src/goals/tools/goal-tool.ts +237 -0
  54. package/src/hashline/anchors.ts +2 -2
  55. package/src/hindsight/mental-models.ts +1 -1
  56. package/src/internal-urls/agent-protocol.ts +1 -20
  57. package/src/internal-urls/artifact-protocol.ts +1 -19
  58. package/src/internal-urls/docs-index.generated.ts +9 -10
  59. package/src/internal-urls/index.ts +1 -0
  60. package/src/internal-urls/issue-pr-protocol.ts +577 -0
  61. package/src/internal-urls/registry-helpers.ts +25 -0
  62. package/src/internal-urls/router.ts +6 -3
  63. package/src/internal-urls/types.ts +22 -1
  64. package/src/main.ts +24 -11
  65. package/src/mcp/oauth-flow.ts +20 -0
  66. package/src/modes/acp/acp-agent.ts +412 -71
  67. package/src/modes/acp/acp-client-bridge.ts +152 -0
  68. package/src/modes/acp/acp-event-mapper.ts +180 -15
  69. package/src/modes/acp/terminal-auth.ts +37 -0
  70. package/src/modes/components/assistant-message.ts +14 -8
  71. package/src/modes/components/bash-execution.ts +24 -63
  72. package/src/modes/components/custom-message.ts +14 -40
  73. package/src/modes/components/eval-execution.ts +27 -57
  74. package/src/modes/components/execution-shared.ts +102 -0
  75. package/src/modes/components/hook-message.ts +17 -49
  76. package/src/modes/components/mcp-add-wizard.ts +26 -5
  77. package/src/modes/components/message-frame.ts +88 -0
  78. package/src/modes/components/model-selector.ts +1 -1
  79. package/src/modes/components/read-tool-group.ts +29 -1
  80. package/src/modes/components/session-observer-overlay.ts +6 -2
  81. package/src/modes/components/session-selector.ts +1 -1
  82. package/src/modes/components/status-line/segments.ts +55 -4
  83. package/src/modes/components/status-line/types.ts +4 -0
  84. package/src/modes/components/status-line.ts +28 -10
  85. package/src/modes/components/tool-execution.ts +7 -8
  86. package/src/modes/controllers/command-controller-shared.ts +108 -0
  87. package/src/modes/controllers/command-controller.ts +27 -10
  88. package/src/modes/controllers/event-controller.ts +60 -18
  89. package/src/modes/controllers/extension-ui-controller.ts +8 -2
  90. package/src/modes/controllers/input-controller.ts +85 -39
  91. package/src/modes/controllers/mcp-command-controller.ts +56 -61
  92. package/src/modes/controllers/ssh-command-controller.ts +18 -57
  93. package/src/modes/interactive-mode.ts +675 -39
  94. package/src/modes/print-mode.ts +16 -86
  95. package/src/modes/rpc/rpc-mode.ts +30 -88
  96. package/src/modes/runtime-init.ts +115 -0
  97. package/src/modes/theme/defaults/dark-poimandres.json +2 -0
  98. package/src/modes/theme/defaults/light-poimandres.json +2 -0
  99. package/src/modes/theme/theme.ts +18 -6
  100. package/src/modes/types.ts +20 -5
  101. package/src/modes/utils/context-usage.ts +13 -13
  102. package/src/modes/utils/ui-helpers.ts +25 -6
  103. package/src/plan-mode/approved-plan.ts +35 -1
  104. package/src/prompts/agents/designer.md +5 -5
  105. package/src/prompts/agents/explore.md +7 -7
  106. package/src/prompts/agents/init.md +9 -9
  107. package/src/prompts/agents/librarian.md +14 -14
  108. package/src/prompts/agents/plan.md +4 -4
  109. package/src/prompts/agents/reviewer.md +5 -5
  110. package/src/prompts/agents/task.md +10 -10
  111. package/src/prompts/commands/orchestrate.md +2 -2
  112. package/src/prompts/compaction/branch-summary.md +3 -3
  113. package/src/prompts/compaction/compaction-short-summary.md +7 -7
  114. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  115. package/src/prompts/compaction/compaction-summary.md +5 -5
  116. package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
  117. package/src/prompts/compaction/compaction-update-summary.md +11 -11
  118. package/src/prompts/goals/goal-budget-limit.md +16 -0
  119. package/src/prompts/goals/goal-continuation.md +28 -0
  120. package/src/prompts/goals/goal-mode-active.md +23 -0
  121. package/src/prompts/memories/consolidation.md +2 -2
  122. package/src/prompts/memories/read-path.md +1 -1
  123. package/src/prompts/memories/stage_one_input.md +1 -1
  124. package/src/prompts/memories/stage_one_system.md +5 -5
  125. package/src/prompts/review-request.md +4 -4
  126. package/src/prompts/system/agent-creation-architect.md +17 -17
  127. package/src/prompts/system/agent-creation-user.md +2 -2
  128. package/src/prompts/system/commit-message-system.md +2 -2
  129. package/src/prompts/system/custom-system-prompt.md +2 -2
  130. package/src/prompts/system/eager-todo.md +6 -6
  131. package/src/prompts/system/handoff-document.md +1 -1
  132. package/src/prompts/system/plan-mode-active.md +25 -24
  133. package/src/prompts/system/plan-mode-approved.md +4 -4
  134. package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
  135. package/src/prompts/system/plan-mode-reference.md +2 -2
  136. package/src/prompts/system/plan-mode-subagent.md +8 -8
  137. package/src/prompts/system/plan-mode-tool-decision-reminder.md +3 -3
  138. package/src/prompts/system/project-prompt.md +4 -4
  139. package/src/prompts/system/subagent-system-prompt.md +7 -7
  140. package/src/prompts/system/subagent-yield-reminder.md +4 -4
  141. package/src/prompts/system/system-prompt.md +72 -71
  142. package/src/prompts/system/ttsr-interrupt.md +1 -1
  143. package/src/prompts/tools/apply-patch.md +1 -1
  144. package/src/prompts/tools/ast-edit.md +3 -3
  145. package/src/prompts/tools/ast-grep.md +3 -3
  146. package/src/prompts/tools/bash.md +6 -0
  147. package/src/prompts/tools/browser.md +3 -3
  148. package/src/prompts/tools/checkpoint.md +3 -3
  149. package/src/prompts/tools/find.md +3 -3
  150. package/src/prompts/tools/github.md +2 -5
  151. package/src/prompts/tools/goal.md +13 -0
  152. package/src/prompts/tools/hashline.md +104 -116
  153. package/src/prompts/tools/image-gen.md +3 -3
  154. package/src/prompts/tools/irc.md +1 -1
  155. package/src/prompts/tools/lsp.md +2 -2
  156. package/src/prompts/tools/patch.md +6 -6
  157. package/src/prompts/tools/read.md +8 -7
  158. package/src/prompts/tools/replace.md +5 -5
  159. package/src/prompts/tools/resolve.md +6 -5
  160. package/src/prompts/tools/retain.md +1 -1
  161. package/src/prompts/tools/rewind.md +2 -2
  162. package/src/prompts/tools/search.md +2 -2
  163. package/src/prompts/tools/ssh.md +2 -2
  164. package/src/prompts/tools/task.md +12 -6
  165. package/src/prompts/tools/web-search.md +2 -2
  166. package/src/prompts/tools/write.md +3 -3
  167. package/src/sdk.ts +81 -17
  168. package/src/session/agent-session.ts +656 -125
  169. package/src/session/blob-store.ts +36 -3
  170. package/src/session/client-bridge.ts +81 -0
  171. package/src/session/compaction/errors.ts +31 -0
  172. package/src/session/compaction/index.ts +1 -0
  173. package/src/session/messages.ts +67 -2
  174. package/src/session/session-manager.ts +131 -12
  175. package/src/session/session-storage.ts +33 -15
  176. package/src/session/streaming-output.ts +309 -13
  177. package/src/slash-commands/acp-builtins.ts +46 -0
  178. package/src/slash-commands/builtin-registry.ts +717 -116
  179. package/src/slash-commands/helpers/context-report.ts +39 -0
  180. package/src/slash-commands/helpers/format.ts +23 -0
  181. package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
  182. package/src/slash-commands/helpers/mcp.ts +532 -0
  183. package/src/slash-commands/helpers/parse.ts +85 -0
  184. package/src/slash-commands/helpers/ssh.ts +193 -0
  185. package/src/slash-commands/helpers/todo.ts +279 -0
  186. package/src/slash-commands/helpers/usage-report.ts +91 -0
  187. package/src/slash-commands/types.ts +126 -0
  188. package/src/ssh/ssh-executor.ts +5 -0
  189. package/src/system-prompt.ts +4 -2
  190. package/src/task/executor.ts +27 -10
  191. package/src/task/index.ts +20 -1
  192. package/src/task/render.ts +27 -18
  193. package/src/task/types.ts +4 -0
  194. package/src/tools/ast-edit.ts +21 -120
  195. package/src/tools/ast-grep.ts +21 -119
  196. package/src/tools/bash-interactive.ts +9 -1
  197. package/src/tools/bash.ts +203 -6
  198. package/src/tools/browser/attach.ts +3 -3
  199. package/src/tools/browser/launch.ts +81 -18
  200. package/src/tools/browser/registry.ts +1 -5
  201. package/src/tools/browser/tab-supervisor.ts +51 -14
  202. package/src/tools/conflict-detect.ts +21 -10
  203. package/src/tools/eval.ts +3 -1
  204. package/src/tools/fetch.ts +15 -4
  205. package/src/tools/find.ts +39 -39
  206. package/src/tools/gh-renderer.ts +0 -12
  207. package/src/tools/gh.ts +689 -182
  208. package/src/tools/github-cache.ts +548 -0
  209. package/src/tools/index.ts +25 -11
  210. package/src/tools/inspect-image.ts +3 -10
  211. package/src/tools/output-meta.ts +176 -37
  212. package/src/tools/path-utils.ts +125 -2
  213. package/src/tools/read.ts +605 -239
  214. package/src/tools/render-utils.ts +92 -0
  215. package/src/tools/renderers.ts +2 -0
  216. package/src/tools/resolve.ts +72 -44
  217. package/src/tools/search.ts +120 -186
  218. package/src/tools/write.ts +67 -10
  219. package/src/tui/code-cell.ts +70 -2
  220. package/src/utils/file-mentions.ts +1 -1
  221. package/src/utils/image-loading.ts +7 -3
  222. package/src/utils/image-resize.ts +32 -43
  223. package/src/vim/parser.ts +0 -17
  224. package/src/vim/render.ts +1 -1
  225. package/src/vim/types.ts +1 -1
  226. package/src/web/search/providers/gemini.ts +35 -95
  227. package/src/prompts/tools/exit-plan-mode.md +0 -6
  228. package/src/tools/exit-plan-mode.ts +0 -97
  229. package/src/utils/fuzzy.ts +0 -108
  230. package/src/utils/image-convert.ts +0 -27
@@ -0,0 +1,126 @@
1
+ import type { Settings } from "../config/settings";
2
+ import type { InteractiveModeContext } from "../modes/types";
3
+ import type { AgentSession } from "../session/agent-session";
4
+ import type { SessionManager } from "../session/session-manager";
5
+
6
+ /** Declarative subcommand definition for commands like /mcp. */
7
+ export interface SubcommandDef {
8
+ name: string;
9
+ description: string;
10
+ /** Usage hint shown as dim ghost text, e.g. "<name> [--scope project|user]". */
11
+ usage?: string;
12
+ }
13
+
14
+ /** Declarative builtin slash command metadata used by autocomplete and help UI. */
15
+ export interface BuiltinSlashCommand {
16
+ name: string;
17
+ description: string;
18
+ /** Subcommands for dropdown completion (e.g. /mcp add, /mcp list). */
19
+ subcommands?: SubcommandDef[];
20
+ /** Static inline hint when command takes a simple argument (no subcommands). */
21
+ inlineHint?: string;
22
+ }
23
+
24
+ /** Parsed slash-command text after stripping the leading "/". */
25
+ export interface ParsedSlashCommand {
26
+ name: string;
27
+ args: string;
28
+ text: string;
29
+ }
30
+
31
+ /**
32
+ * Result returned by a slash-command handler.
33
+ *
34
+ * - `void` / `undefined` — command was handled and consumed; no further input.
35
+ * - `{ consumed: true }` — explicit equivalent of the above (ACP shape).
36
+ * - `{ prompt: string }` — command handled, pass `prompt` through as the new
37
+ * user input (e.g. `/force <tool> <prompt>` keeps `<prompt>` as the message).
38
+ */
39
+ export type SlashCommandResult = undefined | { consumed: true } | { prompt: string };
40
+
41
+ /**
42
+ * Runtime visible to slash-command handlers that run in text/ACP mode.
43
+ *
44
+ * Both the TUI dispatcher (when invoking a `handle` via its adapter) and the
45
+ * ACP dispatcher pass this shape. Implementations MUST NOT depend on TUI-only
46
+ * state (editor, selectors, status line).
47
+ */
48
+ export interface SlashCommandRuntime {
49
+ session: AgentSession;
50
+ sessionManager: SessionManager;
51
+ settings: Settings;
52
+ cwd: string;
53
+ /** Emit text to the operator. TUI maps to `ctx.showStatus`, ACP to `sessionUpdate`. */
54
+ output: (text: string) => Promise<void> | void;
55
+ /** Re-advertise the available command list (no-op outside ACP). */
56
+ refreshCommands: () => Promise<void> | void;
57
+ /**
58
+ * Reload plugin state (caches, slash command registry, project registries)
59
+ * and re-emit available commands. Used by `/reload-plugins`, `/move`, and
60
+ * `/marketplace`/`/plugins` mutations so the session sees a consistent view
61
+ * after plugin or project-scope changes.
62
+ */
63
+ reloadPlugins: () => Promise<void>;
64
+ notifyTitleChanged?: () => Promise<void> | void;
65
+ notifyConfigChanged?: () => Promise<void> | void;
66
+ }
67
+
68
+ /**
69
+ * Runtime visible to TUI-only handlers (`handleTui`). Carries the interactive
70
+ * mode context plus the background-detach hook. Intentionally narrower than
71
+ * `SlashCommandRuntime` so existing callers can keep building it from just
72
+ * `{ ctx, handleBackgroundCommand }`; when the TUI dispatcher needs to invoke
73
+ * a `handle` (no `handleTui` override), it synthesizes a `SlashCommandRuntime`
74
+ * from `ctx`.
75
+ */
76
+ export interface TuiSlashCommandRuntime {
77
+ ctx: InteractiveModeContext;
78
+ handleBackgroundCommand: () => void;
79
+ }
80
+
81
+ /** Unified slash-command spec consumed by both TUI and ACP dispatchers. */
82
+ export interface SlashCommandSpec extends BuiltinSlashCommand {
83
+ aliases?: string[];
84
+ /** When false, the dispatcher refuses to handle invocations that include arguments. */
85
+ allowArgs?: boolean;
86
+ /**
87
+ * ACP-specific override for `description`. Used by `ACP_BUILTIN_SLASH_COMMANDS`
88
+ * when building `available_commands_update` payloads so the client receives
89
+ * mode-appropriate copy (e.g. `/dump` advertises "Return full transcript as
90
+ * plain text" in ACP rather than the TUI's clipboard-centric copy).
91
+ */
92
+ acpDescription?: string;
93
+ /**
94
+ * ACP-specific override for the advertised input hint. `subcommands`-only
95
+ * specs that historically advertised `<subcommand>` / `[on|off|status]` /
96
+ * `info|delete` to ACP clients carry the hint here so the unification does
97
+ * not silently drop it from `available_commands_update`.
98
+ */
99
+ acpInputHint?: string;
100
+ /**
101
+ * Text/ACP-mode handler. The same body is invoked from the ACP dispatcher
102
+ * and, via the TUI adapter, when no `handleTui` override is provided.
103
+ */
104
+ handle?: (
105
+ command: ParsedSlashCommand,
106
+ runtime: SlashCommandRuntime,
107
+ ) => Promise<SlashCommandResult> | SlashCommandResult;
108
+ /**
109
+ * TUI-only handler that supersedes `handle` when both are present. Use for
110
+ * selectors, wizards, dashboards, and anything else that requires
111
+ * `InteractiveModeContext`.
112
+ */
113
+ handleTui?: (
114
+ command: ParsedSlashCommand,
115
+ runtime: TuiSlashCommandRuntime,
116
+ ) => Promise<SlashCommandResult> | SlashCommandResult;
117
+ }
118
+
119
+ /**
120
+ * @deprecated Use `SlashCommandRuntime` directly. Retained as an alias so
121
+ * downstream code that imported the ACP-specific name keeps compiling.
122
+ */
123
+ export type AcpBuiltinCommandRuntime = SlashCommandRuntime;
124
+
125
+ /** Result returned by `executeAcpBuiltinSlashCommand`. */
126
+ export type AcpBuiltinSlashCommandResult = false | { consumed: true } | { prompt: string };
@@ -1,5 +1,7 @@
1
1
  import { logger, ptree } from "@oh-my-pi/pi-utils";
2
+ import { Settings } from "../config/settings";
2
3
  import { OutputSink } from "../session/streaming-output";
4
+ import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../tools/output-meta";
3
5
  import { buildRemoteCommand, ensureConnection, ensureHostInfo, type SSHConnectionTarget } from "./connection-manager";
4
6
  import { hasSshfs, mountRemote } from "./sshfs-mount";
5
7
 
@@ -83,10 +85,13 @@ export async function executeSSH(
83
85
  stderr: "full",
84
86
  });
85
87
 
88
+ const settings = await Settings.init();
86
89
  const sink = new OutputSink({
87
90
  onChunk: options?.onChunk,
88
91
  artifactPath: options?.artifactPath,
89
92
  artifactId: options?.artifactId,
93
+ headBytes: resolveOutputSinkHeadBytes(settings),
94
+ maxColumns: resolveOutputMaxColumns(settings),
90
95
  });
91
96
 
92
97
  const streams = [child.stdout.pipeTo(sink.createInput())];
@@ -530,9 +530,11 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
530
530
  description: tools?.get(name)?.description ?? "",
531
531
  }));
532
532
 
533
- // Filter skills to only include those with read tool.
533
+ // Filter skills for the rendered system prompt:
534
+ // - require the `read` tool so the model can actually fetch skill content;
535
+ // - drop skills with frontmatter `hide: true` (still loadable via skill:// and /skill:<name>).
534
536
  const hasRead = tools?.has("read");
535
- const filteredSkills = hasRead ? skills : [];
537
+ const filteredSkills = hasRead ? skills.filter(skill => skill.hide !== true) : [];
536
538
 
537
539
  const effectiveSystemPromptCustomization = dedupePromptSource(systemPromptCustomization, [
538
540
  resolvedCustomPrompt,
@@ -379,21 +379,29 @@ function firstNumberField(record: Record<string, unknown>, keys: string[]): numb
379
379
  }
380
380
 
381
381
  /**
382
- * Normalize usage objects from different event formats.
382
+ * Tokens for progress display: input + output + cacheWrite per turn.
383
+ *
384
+ * Deliberately excludes cacheRead. With prompt caching, cacheRead in each turn
385
+ * equals the full cached context (potentially hundreds of KB), so summing it
386
+ * across all turns produces a cumulative total that is N×context_size — far
387
+ * larger than the context window and misleading as a "work done" metric.
388
+ * cacheWrite is kept because each byte is written once, not repeated per turn.
389
+ * The cost segment handles billing; dedicated cache_read/cache_write segments
390
+ * handle cache-specific monitoring.
383
391
  */
384
392
  function getUsageTokens(usage: unknown): number {
385
393
  if (!usage || typeof usage !== "object") return 0;
386
394
  const record = usage as Record<string, unknown>;
387
395
 
388
- const totalTokens = firstNumberField(record, ["totalTokens", "total_tokens"]);
389
- if (totalTokens !== undefined && totalTokens > 0) return totalTokens;
390
-
391
396
  const input = firstNumberField(record, ["input", "input_tokens", "inputTokens"]) ?? 0;
392
397
  const output = firstNumberField(record, ["output", "output_tokens", "outputTokens"]) ?? 0;
393
- const cacheRead = firstNumberField(record, ["cacheRead", "cache_read", "cacheReadTokens"]) ?? 0;
394
398
  const cacheWrite = firstNumberField(record, ["cacheWrite", "cache_write", "cacheWriteTokens"]) ?? 0;
395
-
396
- return input + output + cacheRead + cacheWrite;
399
+ const computed = input + output + cacheWrite;
400
+ if (computed > 0) return computed;
401
+ // Fallback for providers that only surface a pre-summed total without individual
402
+ // field breakdown. This total includes cacheRead, but returning it is still better
403
+ // than silently showing 0 for those providers.
404
+ return firstNumberField(record, ["totalTokens", "total_tokens"]) ?? 0;
397
405
  }
398
406
 
399
407
  /**
@@ -497,6 +505,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
497
505
  recentOutput: [],
498
506
  toolCount: 0,
499
507
  tokens: 0,
508
+ cost: 0,
500
509
  durationMs: 0,
501
510
  modelOverride,
502
511
  };
@@ -892,6 +901,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
892
901
  accumulatedUsage.cost.cacheRead += getNumberField(costRecord, "cacheRead") ?? 0;
893
902
  accumulatedUsage.cost.cacheWrite += getNumberField(costRecord, "cacheWrite") ?? 0;
894
903
  accumulatedUsage.cost.total += getNumberField(costRecord, "total") ?? 0;
904
+ progress.cost = accumulatedUsage.cost.total;
895
905
  }
896
906
  }
897
907
  // Accumulate tokens for progress display
@@ -947,10 +957,17 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
947
957
 
948
958
  try {
949
959
  checkAbort();
950
- const authStorage = options.authStorage ?? (await discoverAuthStorage());
951
- checkAbort();
960
+ // Pin authStorage to modelRegistry.authStorage mirrors the createAgentSession invariant.
952
961
  const registryFromParent = options.modelRegistry !== undefined;
953
- const modelRegistry = options.modelRegistry ?? new ModelRegistry(authStorage);
962
+ const modelRegistry =
963
+ options.modelRegistry ?? new ModelRegistry(options.authStorage ?? (await discoverAuthStorage()));
964
+ const authStorage = modelRegistry.authStorage;
965
+ if (options.authStorage && options.authStorage !== authStorage) {
966
+ throw new Error(
967
+ "options.authStorage and options.modelRegistry.authStorage must be the same instance when both are provided",
968
+ );
969
+ }
970
+ checkAbort();
954
971
  if (!registryFromParent) {
955
972
  await modelRegistry.refresh();
956
973
  } else {
package/src/task/index.ts CHANGED
@@ -306,6 +306,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
306
306
  recentOutput: [],
307
307
  toolCount: 0,
308
308
  tokens: 0,
309
+ cost: 0,
309
310
  durationMs: 0,
310
311
  });
311
312
  }
@@ -390,6 +391,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
390
391
  : "failed";
391
392
  progress.durationMs = singleResult?.durationMs ?? Math.max(0, Date.now() - startedAt);
392
393
  progress.tokens = singleResult?.tokens ?? 0;
394
+ progress.cost = singleResult?.usage?.cost.total ?? 0;
393
395
  progress.extractedToolData = singleResult?.extractedToolData;
394
396
  }
395
397
  completedJobs += 1;
@@ -483,11 +485,27 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
483
485
  ? ` Failed to schedule ${failedSchedules.length} task${failedSchedules.length === 1 ? "" : "s"}.`
484
486
  : "";
485
487
 
488
+ const ircEnabled = this.session.settings.get("irc.enabled") === true;
489
+ const taskIdByItemId = new Map<string, string>();
490
+ for (let i = 0; i < taskItems.length; i++) {
491
+ taskIdByItemId.set(taskItems[i].id, uniqueIds[i]);
492
+ }
493
+ const startedListing = startedJobs
494
+ .map(({ taskId }) => {
495
+ const id = taskIdByItemId.get(taskId) ?? taskId;
496
+ const desc = progressByTaskId.get(taskId)?.description;
497
+ return desc ? `- \`${id}\` — ${desc}` : `- \`${id}\``;
498
+ })
499
+ .join("\n");
500
+ const coordinationHint = ircEnabled
501
+ ? ` DM these ids via \`irc\` to coordinate while they run; reach for \`job\` only to inspect (\`list\`), wait (\`poll\`), or cancel a stuck task.`
502
+ : ` Use \`job\` to inspect (\`list\`), wait (\`poll\`), or cancel a stuck task by id.`;
503
+
486
504
  return {
487
505
  content: [
488
506
  {
489
507
  type: "text",
490
- text: `Started ${startedJobs.length} background task job${startedJobs.length === 1 ? "" : "s"} using ${params.agent}.${scheduleFailureSummary} Results will be delivered when complete.`,
508
+ text: `Started ${startedJobs.length} background task job${startedJobs.length === 1 ? "" : "s"} using ${params.agent}.${scheduleFailureSummary} Results will be delivered when complete.\n${startedListing}\n${coordinationHint}`,
491
509
  },
492
510
  ],
493
511
  details: {
@@ -815,6 +833,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
815
833
  recentOutput: [],
816
834
  toolCount: 0,
817
835
  tokens: 0,
836
+ cost: 0,
818
837
  durationMs: 0,
819
838
  modelOverride,
820
839
  description: taskItem.description,
@@ -50,6 +50,24 @@ function getStatusIcon(status: AgentProgress["status"], theme: Theme, spinnerFra
50
50
  }
51
51
  }
52
52
 
53
+ /** Append tool-count, token, and cost stats to a status line string. */
54
+ function appendAgentStats(
55
+ line: string,
56
+ opts: { toolCount?: number; tokens: number; cost: number },
57
+ theme: Theme,
58
+ ): string {
59
+ if (opts.toolCount) {
60
+ line += `${theme.sep.dot}${theme.fg("dim", `${opts.toolCount} tools`)}`;
61
+ }
62
+ if (opts.tokens > 0) {
63
+ line += `${theme.sep.dot}${theme.fg("dim", `${formatNumber(opts.tokens)} tokens`)}`;
64
+ }
65
+ if (opts.cost > 0) {
66
+ line += `${theme.sep.dot}${theme.fg("statusLineCost", `$${opts.cost.toFixed(2)}`)}`;
67
+ }
68
+ return line;
69
+ }
70
+
53
71
  function formatFindingSummary(findings: ReportFindingDetails[], theme: Theme): string {
54
72
  if (findings.length === 0) return theme.fg("dim", "Findings: none");
55
73
 
@@ -526,19 +544,9 @@ function renderAgentProgress(
526
544
  const taskPreview = truncateToWidth(progress.assignment ?? progress.task, 40);
527
545
  statusLine += ` ${theme.fg("muted", taskPreview)}`;
528
546
  }
529
- if (progress.toolCount > 0) {
530
- statusLine += `${theme.sep.dot}${theme.fg("dim", `${progress.toolCount} tools`)}`;
531
- }
532
- if (progress.tokens > 0) {
533
- statusLine += `${theme.sep.dot}${theme.fg("dim", `${formatNumber(progress.tokens)} tokens`)}`;
534
- }
547
+ statusLine = appendAgentStats(statusLine, progress, theme);
535
548
  } else if (progress.status === "completed") {
536
- if (progress.toolCount > 0) {
537
- statusLine += `${theme.sep.dot}${theme.fg("dim", `${progress.toolCount} tools`)}`;
538
- }
539
- if (progress.tokens > 0) {
540
- statusLine += `${theme.sep.dot}${theme.fg("dim", `${formatNumber(progress.tokens)} tokens`)}`;
541
- }
549
+ statusLine = appendAgentStats(statusLine, progress, theme);
542
550
  }
543
551
 
544
552
  lines.push(statusLine);
@@ -661,8 +669,10 @@ function renderReviewResult(
661
669
  lines.push(`${continuePrefix} ${theme.fg("dim", replaceTabs(line))}`);
662
670
  }
663
671
  } else {
664
- // Preview: first sentence or ~100 chars
665
- const preview = truncateToWidth(`${summary.explanation.split(/[.!?]/)[0]}.`, 100);
672
+ // Preview: first sentence or ~100 chars (flatten tabs/newlines first)
673
+ const flat = replaceTabs(summary.explanation).replace(/[\r\n]+/g, " ");
674
+ const firstSentence = flat.split(/[.!?]/)[0].trim();
675
+ const preview = truncateToWidth(`${firstSentence}.`, 100);
666
676
  lines.push(`${continuePrefix}${theme.fg("dim", preview)}`);
667
677
  }
668
678
  }
@@ -701,7 +711,8 @@ function renderFindings(
701
711
  const findingContinue = isLastFinding ? " " : `${theme.tree.vertical} `;
702
712
 
703
713
  const { color } = getPriorityInfo(finding.priority);
704
- const titleText = finding.title?.replace(/^\[P\d\]\s*/, "") ?? "Untitled";
714
+ const rawTitle = finding.title?.replace(/^\[P\d\]\s*/, "") ?? "Untitled";
715
+ const titleText = replaceTabs(rawTitle).replace(/[\r\n]+/g, " ");
705
716
  const loc = `${path.basename(finding.file_path || "<unknown>")}:${finding.line_start}`;
706
717
 
707
718
  lines.push(
@@ -765,9 +776,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
765
776
  iconColor,
766
777
  theme,
767
778
  )}`;
768
- if (result.tokens > 0) {
769
- statusLine += `${theme.sep.dot}${theme.fg("dim", `${formatNumber(result.tokens)} tokens`)}`;
770
- }
779
+ statusLine = appendAgentStats(statusLine, { tokens: result.tokens, cost: result.usage?.cost.total ?? 0 }, theme);
771
780
  statusLine += `${theme.sep.dot}${theme.fg("dim", formatDuration(result.durationMs))}`;
772
781
 
773
782
  if (result.truncated) {
package/src/task/types.ts CHANGED
@@ -217,7 +217,10 @@ export interface AgentProgress {
217
217
  recentTools: Array<{ tool: string; args: string; endMs: number }>;
218
218
  recentOutput: string[];
219
219
  toolCount: number;
220
+ /** Cumulative input + output + cacheWrite tokens across all turns. Excludes cacheRead (re-reads cached context every turn, making cumulative sum misleading). */
220
221
  tokens: number;
222
+ /** Cumulative billing cost in USD, accumulated incrementally from message_end events. */
223
+ cost: number;
221
224
  durationMs: number;
222
225
  modelOverride?: string | string[];
223
226
  /** Data extracted by registered subprocess tool handlers (keyed by tool name) */
@@ -239,6 +242,7 @@ export interface SingleResult {
239
242
  stderr: string;
240
243
  truncated: boolean;
241
244
  durationMs: number;
245
+ /** Cumulative input + output + cacheWrite tokens across all turns. Excludes cacheRead (re-reads cached context every turn, making cumulative sum misleading). */
242
246
  tokens: number;
243
247
  modelOverride?: string | string[];
244
248
  error?: string;
@@ -7,33 +7,27 @@ import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
7
7
  import { type Static, Type } from "@sinclair/typebox";
8
8
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
9
9
  import { computeLineHash, HL_BODY_SEP } from "../hashline/hash";
10
- import { InternalUrlRouter } from "../internal-urls";
11
10
  import type { Theme } from "../modes/theme/theme";
12
11
  import astEditDescription from "../prompts/tools/ast-edit.md" with { type: "text" };
13
- import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
12
+ import { Ellipsis, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
14
13
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
15
14
  import type { ToolSession } from ".";
16
15
  import { createFileRecorder, formatResultPath } from "./file-recorder";
17
16
  import { formatGroupedFiles } from "./grouped-file-output";
18
17
  import type { OutputMeta } from "./output-meta";
18
+ import { resolveToolSearchScope } from "./path-utils";
19
19
  import {
20
- formatPathRelativeToCwd,
21
- hasGlobPathChars,
22
- normalizePathLikeInput,
23
- parseSearchPath,
24
- partitionExistingPaths,
25
- resolveExplicitSearchPaths,
26
- resolveToCwd,
27
- } from "./path-utils";
28
- import {
20
+ appendParseErrorsBulletList,
21
+ createCachedComponent,
29
22
  dedupeParseErrors,
30
23
  formatCodeFrameLine,
31
24
  formatCount,
32
25
  formatEmptyMessage,
33
26
  formatErrorMessage,
34
27
  formatParseErrors,
35
- PARSE_ERRORS_LIMIT,
28
+ formatParseErrorsCountLabel,
36
29
  PREVIEW_LIMITS,
30
+ splitGroupsByBlankLine,
37
31
  } from "./render-utils";
38
32
  import { queueResolveHandler } from "./resolve";
39
33
  import { ToolError } from "./tool-errors";
@@ -205,63 +199,12 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
205
199
  const normalizedRewrites = Object.fromEntries(ops);
206
200
  const maxFiles = $envpos("PI_MAX_AST_FILES", 1000);
207
201
 
208
- const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
209
- let searchPath: string;
210
- let scopePath: string;
211
- let globFilter: string | undefined;
212
- let multiTargets: Array<{ basePath: string; glob?: string }> | undefined;
213
- const rawPaths = params.paths.map(normalizePathLikeInput);
214
- if (rawPaths.some(rawPath => rawPath.length === 0)) {
215
- throw new ToolError("`paths` must contain non-empty paths or globs");
216
- }
217
- const internalRouter = InternalUrlRouter.instance();
218
- const resolvedPathInputs: string[] = [];
219
- for (const rawPath of rawPaths) {
220
- if (!internalRouter.canHandle(rawPath)) {
221
- resolvedPathInputs.push(rawPath);
222
- continue;
223
- }
224
- if (hasGlobPathChars(rawPath)) {
225
- throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
226
- }
227
- const resource = await internalRouter.resolve(rawPath);
228
- if (!resource.sourcePath) {
229
- throw new ToolError(`Cannot rewrite internal URL without backing file: ${rawPath}`);
230
- }
231
- resolvedPathInputs.push(resource.sourcePath);
232
- }
233
- let effectivePathInputs = resolvedPathInputs;
234
- if (resolvedPathInputs.length > 1) {
235
- const partition = await partitionExistingPaths(resolvedPathInputs, this.session.cwd, parseSearchPath);
236
- if (partition.valid.length === 0) {
237
- throw new ToolError(`Path not found: ${partition.missing.join(", ")}`);
238
- }
239
- effectivePathInputs = partition.valid;
240
- }
241
- if (effectivePathInputs.length === 1) {
242
- const parsedPath = parseSearchPath(effectivePathInputs[0] ?? ".");
243
- searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
244
- globFilter = parsedPath.glob;
245
- scopePath = formatScopePath(searchPath);
246
- } else {
247
- const multiSearchPath = await resolveExplicitSearchPaths(effectivePathInputs, this.session.cwd, globFilter);
248
- if (!multiSearchPath) {
249
- throw new ToolError("`paths` must contain at least one path or glob");
250
- }
251
- searchPath = multiSearchPath.basePath;
252
- globFilter = multiSearchPath.targets ? undefined : multiSearchPath.glob;
253
- multiTargets = multiSearchPath.targets;
254
- scopePath = multiSearchPath.scopePath;
255
- }
256
- const resolvedSearchPath = searchPath;
257
- scopePath = scopePath ?? formatScopePath(resolvedSearchPath);
258
- let isDirectory: boolean;
259
- try {
260
- const stat = await Bun.file(resolvedSearchPath).stat();
261
- isDirectory = stat.isDirectory();
262
- } catch {
263
- throw new ToolError(`Path not found: ${scopePath}`);
264
- }
202
+ const scope = await resolveToolSearchScope({
203
+ rawPaths: params.paths,
204
+ cwd: this.session.cwd,
205
+ internalUrlAction: "rewrite",
206
+ });
207
+ const { searchPath: resolvedSearchPath, scopePath, isDirectory, multiTargets, globFilter } = scope;
265
208
 
266
209
  const result = await runAstEditOnce(multiTargets, resolvedSearchPath, globFilter, {
267
210
  rewrites: normalizedRewrites,
@@ -502,15 +445,7 @@ export const astEditToolRenderer = {
502
445
  if (filesSearched > 0) meta.push(`searched ${filesSearched}`);
503
446
  const header = renderStatusLine({ icon: "warning", title: "AST Edit", description, meta }, uiTheme);
504
447
  const lines = [header, formatEmptyMessage("No replacements made", uiTheme)];
505
- if (details?.parseErrors?.length) {
506
- const capped = details.parseErrors.slice(0, PARSE_ERRORS_LIMIT);
507
- for (const err of capped) {
508
- lines.push(uiTheme.fg("warning", ` - ${err}`));
509
- }
510
- if (details.parseErrors.length > PARSE_ERRORS_LIMIT) {
511
- lines.push(uiTheme.fg("dim", ` … ${details.parseErrors.length - PARSE_ERRORS_LIMIT} more`));
512
- }
513
- }
448
+ appendParseErrorsBulletList(lines, details?.parseErrors, uiTheme);
514
449
  return new Text(lines.join("\n"), 0, 0);
515
450
  }
516
451
 
@@ -523,28 +458,7 @@ export const astEditToolRenderer = {
523
458
  const description = rewriteCount === 1 ? args?.ops?.[0]?.pat : undefined;
524
459
 
525
460
  const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text ?? "";
526
- const rawLines = textContent.split("\n");
527
- const hasSeparators = rawLines.some(line => line.trim().length === 0);
528
- const allGroups: string[][] = [];
529
- if (hasSeparators) {
530
- let current: string[] = [];
531
- for (const line of rawLines) {
532
- if (line.trim().length === 0) {
533
- if (current.length > 0) {
534
- allGroups.push(current);
535
- current = [];
536
- }
537
- continue;
538
- }
539
- current.push(line);
540
- }
541
- if (current.length > 0) allGroups.push(current);
542
- } else {
543
- const nonEmpty = rawLines.filter(line => line.trim().length > 0);
544
- if (nonEmpty.length > 0) {
545
- allGroups.push(nonEmpty);
546
- }
547
- }
461
+ const allGroups = splitGroupsByBlankLine(textContent.split("\n"));
548
462
  const changeGroups = allGroups.filter(
549
463
  group => !group[0]?.startsWith("Safety cap reached") && !group[0]?.startsWith("Parse issues:"),
550
464
  );
@@ -560,23 +474,15 @@ export const astEditToolRenderer = {
560
474
  extraLines.push(uiTheme.fg("warning", "limit reached; narrow path"));
561
475
  }
562
476
  if (details?.parseErrors?.length) {
563
- const total = details.parseErrors.length;
564
- const label =
565
- total > PARSE_ERRORS_LIMIT
566
- ? `${PARSE_ERRORS_LIMIT} / ${total} parse issues`
567
- : `${total} parse issue${total !== 1 ? "s" : ""}`;
568
- extraLines.push(uiTheme.fg("warning", label));
477
+ extraLines.push(uiTheme.fg("warning", formatParseErrorsCountLabel(details.parseErrors)));
569
478
  }
570
- let cached: RenderCache | undefined;
571
- return {
572
- render(width: number): string[] {
573
- const { expanded } = options;
574
- const key = new Hasher().bool(expanded).u32(width).digest();
575
- if (cached?.key === key) return cached.lines;
479
+ return createCachedComponent(
480
+ () => options.expanded,
481
+ width => {
576
482
  const changeLines = renderTreeList(
577
483
  {
578
484
  items: changeGroups,
579
- expanded,
485
+ expanded: options.expanded,
580
486
  maxCollapsed: changeGroups.length,
581
487
  maxCollapsedLines: COLLAPSED_CHANGE_LIMIT,
582
488
  itemType: "change",
@@ -591,14 +497,9 @@ export const astEditToolRenderer = {
591
497
  },
592
498
  uiTheme,
593
499
  );
594
- const rendered = [header, ...changeLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
595
- cached = { key, lines: rendered };
596
- return rendered;
500
+ return [header, ...changeLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
597
501
  },
598
- invalidate() {
599
- cached = undefined;
600
- },
601
- };
502
+ );
602
503
  },
603
504
  mergeCallAndResult: true,
604
505
  };