@oh-my-pi/pi-coding-agent 12.18.3 → 12.19.2

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 (233) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/package.json +35 -27
  3. package/src/async/index.ts +1 -0
  4. package/src/async/job-manager.ts +341 -0
  5. package/src/cli/file-processor.ts +3 -3
  6. package/src/cli/list-models.ts +3 -17
  7. package/src/cli/stats-cli.ts +3 -22
  8. package/src/cli/web-search-cli.ts +8 -16
  9. package/src/commit/agentic/agent.ts +6 -9
  10. package/src/commit/agentic/index.ts +44 -50
  11. package/src/commit/agentic/state.ts +0 -9
  12. package/src/commit/agentic/tools/propose-commit.ts +1 -30
  13. package/src/commit/agentic/tools/schemas.ts +31 -0
  14. package/src/commit/agentic/tools/split-commit.ts +1 -30
  15. package/src/commit/agentic/validation.ts +1 -18
  16. package/src/commit/analysis/conventional.ts +3 -50
  17. package/src/commit/analysis/summary.ts +2 -13
  18. package/src/commit/changelog/detect.ts +4 -1
  19. package/src/commit/changelog/generate.ts +2 -25
  20. package/src/commit/changelog/index.ts +1 -2
  21. package/src/commit/cli.ts +4 -12
  22. package/src/commit/map-reduce/reduce-phase.ts +2 -43
  23. package/src/commit/pipeline.ts +7 -15
  24. package/src/commit/utils.ts +44 -0
  25. package/src/config/prompt-templates.ts +1 -81
  26. package/src/config/settings-schema.ts +20 -1
  27. package/src/config.ts +2 -3
  28. package/src/debug/index.ts +1 -6
  29. package/src/debug/system-info.ts +2 -6
  30. package/src/discovery/builtin.ts +5 -9
  31. package/src/discovery/helpers.ts +0 -26
  32. package/src/discovery/ssh.ts +1 -8
  33. package/src/exa/company.ts +8 -39
  34. package/src/exa/factory.ts +64 -0
  35. package/src/exa/index.ts +0 -16
  36. package/src/exa/linkedin.ts +8 -39
  37. package/src/exa/mcp-client.ts +0 -64
  38. package/src/exa/researcher.ts +17 -59
  39. package/src/exa/search.ts +30 -154
  40. package/src/extensibility/custom-tools/loader.ts +3 -41
  41. package/src/extensibility/extensions/loader.ts +2 -9
  42. package/src/extensibility/hooks/loader.ts +3 -20
  43. package/src/extensibility/hooks/runner.ts +3 -19
  44. package/src/extensibility/plugins/installer.ts +2 -1
  45. package/src/extensibility/plugins/loader.ts +29 -117
  46. package/src/extensibility/skills.ts +2 -89
  47. package/src/extensibility/slash-commands.ts +1 -63
  48. package/src/extensibility/utils.ts +38 -0
  49. package/src/index.ts +9 -25
  50. package/src/internal-urls/index.ts +1 -0
  51. package/src/internal-urls/jobs-protocol.ts +118 -0
  52. package/src/ipy/kernel.ts +2 -0
  53. package/src/lsp/config.ts +1 -5
  54. package/src/lsp/lspmux.ts +0 -17
  55. package/src/lsp/utils.ts +2 -24
  56. package/src/main.ts +16 -24
  57. package/src/mcp/client.ts +1 -46
  58. package/src/mcp/render.ts +8 -1
  59. package/src/mcp/tool-cache.ts +1 -5
  60. package/src/mcp/transports/http.ts +2 -7
  61. package/src/mcp/transports/stdio.ts +2 -7
  62. package/src/modes/components/bash-execution.ts +2 -16
  63. package/src/modes/components/extensions/inspector-panel.ts +8 -18
  64. package/src/modes/components/footer.ts +10 -50
  65. package/src/modes/components/model-selector.ts +2 -21
  66. package/src/modes/components/python-execution.ts +2 -16
  67. package/src/modes/components/settings-selector.ts +1 -10
  68. package/src/modes/components/status-line/segments.ts +8 -25
  69. package/src/modes/components/status-line.ts +14 -31
  70. package/src/modes/components/tool-execution.ts +8 -2
  71. package/src/modes/controllers/command-controller.ts +71 -30
  72. package/src/modes/controllers/event-controller.ts +34 -4
  73. package/src/modes/controllers/mcp-command-controller.ts +3 -34
  74. package/src/modes/controllers/selector-controller.ts +2 -2
  75. package/src/modes/controllers/ssh-command-controller.ts +3 -34
  76. package/src/modes/interactive-mode.ts +6 -2
  77. package/src/modes/rpc/rpc-client.ts +1 -5
  78. package/src/modes/shared.ts +73 -0
  79. package/src/modes/types.ts +1 -0
  80. package/src/modes/utils/ui-helpers.ts +26 -2
  81. package/src/patch/hashline.ts +6 -286
  82. package/src/patch/index.ts +6 -57
  83. package/src/patch/normalize.ts +22 -65
  84. package/src/patch/shared.ts +16 -16
  85. package/src/prompts/system/custom-system-prompt.md +0 -10
  86. package/src/prompts/system/system-prompt.md +69 -89
  87. package/src/prompts/tools/async-result.md +5 -0
  88. package/src/prompts/tools/bash.md +5 -0
  89. package/src/prompts/tools/cancel-job.md +7 -0
  90. package/src/prompts/tools/hashline.md +0 -16
  91. package/src/prompts/tools/poll-jobs.md +7 -0
  92. package/src/prompts/tools/task.md +4 -0
  93. package/src/sdk.ts +70 -6
  94. package/src/session/agent-session.ts +43 -6
  95. package/src/session/agent-storage.ts +69 -278
  96. package/src/session/auth-storage.ts +14 -1430
  97. package/src/session/session-manager.ts +69 -5
  98. package/src/session/session-storage.ts +1 -5
  99. package/src/session/streaming-output.ts +637 -76
  100. package/src/slash-commands/builtin-registry.ts +8 -0
  101. package/src/ssh/connection-manager.ts +4 -12
  102. package/src/ssh/sshfs-mount.ts +3 -7
  103. package/src/ssh/utils.ts +8 -0
  104. package/src/system-prompt.ts +24 -90
  105. package/src/task/executor.ts +11 -1
  106. package/src/task/index.ts +258 -13
  107. package/src/task/parallel.ts +32 -0
  108. package/src/task/render.ts +15 -7
  109. package/src/task/types.ts +5 -0
  110. package/src/tools/ask.ts +4 -7
  111. package/src/tools/bash-interactive.ts +4 -5
  112. package/src/tools/bash.ts +125 -41
  113. package/src/tools/cancel-job.ts +93 -0
  114. package/src/tools/fetch.ts +7 -27
  115. package/src/tools/find.ts +3 -3
  116. package/src/tools/gemini-image.ts +15 -14
  117. package/src/tools/grep.ts +3 -3
  118. package/src/tools/index.ts +13 -29
  119. package/src/tools/json-tree.ts +12 -1
  120. package/src/tools/jtd-to-json-schema.ts +10 -74
  121. package/src/tools/jtd-to-typescript.ts +10 -72
  122. package/src/tools/jtd-utils.ts +102 -0
  123. package/src/tools/notebook.ts +4 -9
  124. package/src/tools/output-meta.ts +52 -26
  125. package/src/tools/path-utils.ts +13 -7
  126. package/src/tools/poll-jobs.ts +178 -0
  127. package/src/tools/python.ts +32 -35
  128. package/src/tools/read.ts +61 -82
  129. package/src/tools/render-utils.ts +8 -159
  130. package/src/tools/ssh.ts +7 -20
  131. package/src/tools/submit-result.ts +1 -1
  132. package/src/tools/tool-errors.ts +0 -30
  133. package/src/tools/tool-result.ts +1 -2
  134. package/src/tools/write.ts +8 -10
  135. package/src/tui/code-cell.ts +8 -3
  136. package/src/tui/status-line.ts +4 -4
  137. package/src/tui/types.ts +0 -1
  138. package/src/tui/utils.ts +1 -14
  139. package/src/utils/command-args.ts +76 -0
  140. package/src/utils/file-mentions.ts +15 -19
  141. package/src/utils/frontmatter.ts +5 -10
  142. package/src/utils/shell-snapshot.ts +0 -11
  143. package/src/utils/title-generator.ts +0 -12
  144. package/src/web/scrapers/artifacthub.ts +7 -16
  145. package/src/web/scrapers/arxiv.ts +3 -8
  146. package/src/web/scrapers/aur.ts +8 -22
  147. package/src/web/scrapers/biorxiv.ts +5 -14
  148. package/src/web/scrapers/bluesky.ts +13 -36
  149. package/src/web/scrapers/brew.ts +5 -10
  150. package/src/web/scrapers/cheatsh.ts +2 -12
  151. package/src/web/scrapers/chocolatey.ts +63 -26
  152. package/src/web/scrapers/choosealicense.ts +3 -18
  153. package/src/web/scrapers/cisa-kev.ts +4 -18
  154. package/src/web/scrapers/clojars.ts +6 -33
  155. package/src/web/scrapers/coingecko.ts +25 -33
  156. package/src/web/scrapers/crates-io.ts +7 -26
  157. package/src/web/scrapers/crossref.ts +4 -18
  158. package/src/web/scrapers/devto.ts +11 -41
  159. package/src/web/scrapers/discogs.ts +7 -10
  160. package/src/web/scrapers/discourse.ts +6 -31
  161. package/src/web/scrapers/dockerhub.ts +12 -35
  162. package/src/web/scrapers/fdroid.ts +8 -33
  163. package/src/web/scrapers/firefox-addons.ts +10 -34
  164. package/src/web/scrapers/flathub.ts +7 -24
  165. package/src/web/scrapers/github-gist.ts +2 -12
  166. package/src/web/scrapers/github.ts +9 -47
  167. package/src/web/scrapers/gitlab.ts +130 -185
  168. package/src/web/scrapers/go-pkg.ts +12 -22
  169. package/src/web/scrapers/hackage.ts +88 -43
  170. package/src/web/scrapers/hackernews.ts +25 -45
  171. package/src/web/scrapers/hex.ts +19 -36
  172. package/src/web/scrapers/huggingface.ts +26 -91
  173. package/src/web/scrapers/iacr.ts +3 -8
  174. package/src/web/scrapers/jetbrains-marketplace.ts +9 -20
  175. package/src/web/scrapers/lemmy.ts +5 -23
  176. package/src/web/scrapers/lobsters.ts +16 -28
  177. package/src/web/scrapers/mastodon.ts +24 -43
  178. package/src/web/scrapers/maven.ts +6 -21
  179. package/src/web/scrapers/mdn.ts +7 -11
  180. package/src/web/scrapers/metacpan.ts +9 -41
  181. package/src/web/scrapers/musicbrainz.ts +4 -28
  182. package/src/web/scrapers/npm.ts +8 -25
  183. package/src/web/scrapers/nuget.ts +14 -37
  184. package/src/web/scrapers/nvd.ts +6 -28
  185. package/src/web/scrapers/ollama.ts +7 -34
  186. package/src/web/scrapers/open-vsx.ts +5 -19
  187. package/src/web/scrapers/opencorporates.ts +30 -14
  188. package/src/web/scrapers/openlibrary.ts +49 -33
  189. package/src/web/scrapers/orcid.ts +4 -18
  190. package/src/web/scrapers/osv.ts +7 -24
  191. package/src/web/scrapers/packagist.ts +9 -24
  192. package/src/web/scrapers/pub-dev.ts +7 -50
  193. package/src/web/scrapers/pubmed.ts +54 -21
  194. package/src/web/scrapers/pypi.ts +8 -26
  195. package/src/web/scrapers/rawg.ts +11 -19
  196. package/src/web/scrapers/readthedocs.ts +4 -9
  197. package/src/web/scrapers/reddit.ts +5 -15
  198. package/src/web/scrapers/repology.ts +8 -20
  199. package/src/web/scrapers/rfc.ts +5 -14
  200. package/src/web/scrapers/rubygems.ts +6 -21
  201. package/src/web/scrapers/searchcode.ts +8 -36
  202. package/src/web/scrapers/sec-edgar.ts +4 -18
  203. package/src/web/scrapers/semantic-scholar.ts +15 -35
  204. package/src/web/scrapers/snapcraft.ts +5 -19
  205. package/src/web/scrapers/sourcegraph.ts +5 -43
  206. package/src/web/scrapers/spdx.ts +4 -18
  207. package/src/web/scrapers/spotify.ts +4 -23
  208. package/src/web/scrapers/stackoverflow.ts +8 -13
  209. package/src/web/scrapers/terraform.ts +9 -37
  210. package/src/web/scrapers/tldr.ts +3 -7
  211. package/src/web/scrapers/twitter.ts +3 -7
  212. package/src/web/scrapers/types.ts +105 -27
  213. package/src/web/scrapers/utils.ts +97 -103
  214. package/src/web/scrapers/vimeo.ts +7 -27
  215. package/src/web/scrapers/vscode-marketplace.ts +8 -17
  216. package/src/web/scrapers/w3c.ts +6 -14
  217. package/src/web/scrapers/wikidata.ts +5 -19
  218. package/src/web/scrapers/wikipedia.ts +2 -12
  219. package/src/web/scrapers/youtube.ts +5 -34
  220. package/src/web/search/index.ts +0 -9
  221. package/src/web/search/providers/anthropic.ts +3 -2
  222. package/src/web/search/providers/brave.ts +3 -18
  223. package/src/web/search/providers/exa.ts +1 -12
  224. package/src/web/search/providers/kimi.ts +5 -44
  225. package/src/web/search/providers/perplexity.ts +1 -12
  226. package/src/web/search/providers/synthetic.ts +3 -26
  227. package/src/web/search/providers/utils.ts +36 -0
  228. package/src/web/search/providers/zai.ts +9 -50
  229. package/src/web/search/types.ts +0 -28
  230. package/src/web/search/utils.ts +17 -0
  231. package/src/tools/output-utils.ts +0 -63
  232. package/src/tools/truncate.ts +0 -385
  233. package/src/web/search/auth.ts +0 -178
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import * as os from "node:os";
8
8
  import { type Ellipsis, truncateToWidth } from "@oh-my-pi/pi-tui";
9
+ import { pluralize } from "@oh-my-pi/pi-utils";
9
10
  import type { Theme } from "../modes/theme/theme";
10
11
 
11
12
  export { Ellipsis, replaceTabs, truncateToWidth } from "@oh-my-pi/pi-tui";
@@ -81,60 +82,7 @@ export function getDomain(url: string): string {
81
82
  // Formatting Utilities
82
83
  // =============================================================================
83
84
 
84
- /**
85
- * Format byte count for display (e.g., "1.5KB", "2.3MB").
86
- */
87
- export function formatBytes(bytes: number): string {
88
- if (bytes < 1024) return `${bytes}B`;
89
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
90
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
91
- }
92
-
93
- /**
94
- * Format token count for display (e.g., "1.5k", "25k").
95
- */
96
- export function formatTokens(tokens: number): string {
97
- if (tokens >= 1000) {
98
- return `${(tokens / 1000).toFixed(1)}k`;
99
- }
100
- return String(tokens);
101
- }
102
-
103
- /**
104
- * Format duration for display (e.g., "500ms", "2.5s", "1.2m").
105
- */
106
- export function formatDuration(ms: number): string {
107
- if (ms < 1000) return `${ms}ms`;
108
- if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
109
- return `${(ms / 60000).toFixed(1)}m`;
110
- }
111
-
112
- /**
113
- * Format count with pluralized label (e.g., "3 files", "1 error").
114
- */
115
- export function formatCount(label: string, count: number): string {
116
- const safeCount = Number.isFinite(count) ? count : 0;
117
- return `${safeCount} ${pluralize(label, safeCount)}`;
118
- }
119
-
120
- /**
121
- * Format age from seconds to human-readable string.
122
- */
123
- export function formatAge(ageSeconds: number | null | undefined): string {
124
- if (!ageSeconds) return "";
125
- const mins = Math.floor(ageSeconds / 60);
126
- const hours = Math.floor(mins / 60);
127
- const days = Math.floor(hours / 24);
128
- const weeks = Math.floor(days / 7);
129
- const months = Math.floor(days / 30);
130
-
131
- if (months > 0) return `${months}mo ago`;
132
- if (weeks > 0) return `${weeks}w ago`;
133
- if (days > 0) return `${days}d ago`;
134
- if (hours > 0) return `${hours}h ago`;
135
- if (mins > 0) return `${mins}m ago`;
136
- return "just now";
137
- }
85
+ export { formatAge, formatBytes, formatCount, formatDuration, pluralize } from "@oh-my-pi/pi-utils";
138
86
 
139
87
  // =============================================================================
140
88
  // Theme Helper Utilities
@@ -199,14 +147,6 @@ export function formatMeta(meta: string[], theme: Theme): string {
199
147
  return meta.length > 0 ? ` ${theme.fg("muted", meta.join(theme.sep.dot))}` : "";
200
148
  }
201
149
 
202
- export function formatScope(scopePath: string | undefined, theme: Theme): string {
203
- return scopePath ? ` ${theme.fg("muted", `in ${scopePath}`)}` : "";
204
- }
205
-
206
- export function formatTruncationSuffix(truncated: boolean, theme: Theme): string {
207
- return truncated ? theme.fg("warning", " (truncated)") : "";
208
- }
209
-
210
150
  export function formatErrorMessage(message: string | undefined, theme: Theme): string {
211
151
  const clean = (message ?? "").replace(/^Error:\s*/, "").trim();
212
152
  return `${theme.styledSymbol("status.error", "error")} ${theme.fg("error", `Error: ${clean || "Unknown error"}`)}`;
@@ -217,7 +157,7 @@ export function formatEmptyMessage(message: string, theme: Theme): string {
217
157
  }
218
158
 
219
159
  // =============================================================================
220
- // Tool UI Kit
160
+ // Tool UI Helpers
221
161
  // =============================================================================
222
162
 
223
163
  export type ToolUIStatus = "success" | "error" | "warning" | "info" | "pending" | "running" | "aborted";
@@ -227,99 +167,15 @@ export interface ToolUITitleOptions {
227
167
  bold?: boolean;
228
168
  }
229
169
 
170
+ export function formatTitle(label: string, theme: Theme, options?: ToolUITitleOptions): string {
171
+ const content = options?.bold === false ? label : theme.bold(label);
172
+ return theme.fg("toolTitle", content);
173
+ }
174
+
230
175
  // =============================================================================
231
176
  // Diagnostic Formatting
232
177
  // =============================================================================
233
178
 
234
- export class ToolUIKit {
235
- constructor(public theme: Theme) {}
236
-
237
- title(label: string, options?: ToolUITitleOptions): string {
238
- const content = options?.bold === false ? label : this.theme.bold(label);
239
- return this.theme.fg("toolTitle", content);
240
- }
241
-
242
- meta(meta: string[]): string {
243
- return formatMeta(meta, this.theme);
244
- }
245
-
246
- count(label: string, count: number): string {
247
- return formatCount(label, count);
248
- }
249
-
250
- moreItems(remaining: number, itemType: string): string {
251
- return formatMoreItems(remaining, itemType);
252
- }
253
-
254
- expandHint(expanded: boolean, hasMore: boolean): string {
255
- return formatExpandHint(this.theme, expanded, hasMore);
256
- }
257
-
258
- scope(scopePath?: string): string {
259
- return formatScope(scopePath, this.theme);
260
- }
261
-
262
- truncationSuffix(truncated: boolean): string {
263
- return formatTruncationSuffix(truncated, this.theme);
264
- }
265
-
266
- errorMessage(message: string | undefined): string {
267
- return formatErrorMessage(message, this.theme);
268
- }
269
-
270
- emptyMessage(message: string): string {
271
- return formatEmptyMessage(message, this.theme);
272
- }
273
-
274
- badge(label: string, color: ToolUIColor): string {
275
- return formatBadge(label, color, this.theme);
276
- }
277
-
278
- statusIcon(status: ToolUIStatus, spinnerFrame?: number): string {
279
- return formatStatusIcon(status, this.theme, spinnerFrame);
280
- }
281
-
282
- wrapBrackets(text: string): string {
283
- return wrapBrackets(text, this.theme);
284
- }
285
-
286
- truncate(text: string, maxLen: number): string {
287
- return truncateToWidth(text, maxLen);
288
- }
289
-
290
- previewLines(text: string, maxLines: number, maxLineLen: number): string[] {
291
- return getPreviewLines(text, maxLines, maxLineLen);
292
- }
293
-
294
- formatBytes(bytes: number): string {
295
- return formatBytes(bytes);
296
- }
297
-
298
- formatTokens(tokens: number): string {
299
- return formatTokens(tokens);
300
- }
301
-
302
- formatDuration(ms: number): string {
303
- return formatDuration(ms);
304
- }
305
-
306
- formatAge(ageSeconds: number | null | undefined): string {
307
- return formatAge(ageSeconds);
308
- }
309
-
310
- formatDiagnostics(
311
- diag: { errored: boolean; summary: string; messages: string[] },
312
- expanded: boolean,
313
- getLangIcon: (filePath: string) => string,
314
- ): string {
315
- return formatDiagnostics(diag, expanded, this.theme, getLangIcon);
316
- }
317
-
318
- formatDiffStats(added: number, removed: number, hunks: number): string {
319
- return formatDiffStats(added, removed, hunks, this.theme);
320
- }
321
- }
322
-
323
179
  interface ParsedDiagnostic {
324
180
  filePath: string;
325
181
  line: number;
@@ -671,10 +527,3 @@ export function shortenPath(filePath: string, homeDir?: string): string {
671
527
  export function wrapBrackets(text: string, theme: Theme): string {
672
528
  return `${theme.format.bracketLeft}${text}${theme.format.bracketRight}`;
673
529
  }
674
-
675
- function pluralize(label: string, count: number): string {
676
- if (count === 1) return label;
677
- if (/(?:ch|sh|s|x|z)$/i.test(label)) return `${label}es`;
678
- if (/[^aeiou]y$/i.test(label)) return `${label.slice(0, -1)}ies`;
679
- return `${label}s`;
680
- }
package/src/tools/ssh.ts CHANGED
@@ -9,18 +9,16 @@ import { loadCapability } from "../discovery";
9
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
10
10
  import type { Theme } from "../modes/theme/theme";
11
11
  import sshDescriptionBase from "../prompts/tools/ssh.md" with { type: "text" };
12
+ import { DEFAULT_MAX_BYTES, TailBuffer } from "../session/streaming-output";
12
13
  import type { SSHHostInfo } from "../ssh/connection-manager";
13
14
  import { ensureHostInfo, getHostInfoForHost } from "../ssh/connection-manager";
14
15
  import { executeSSH } from "../ssh/ssh-executor";
15
16
  import { renderStatusLine } from "../tui";
16
17
  import { CachedOutputBlock } from "../tui/output-block";
17
18
  import type { ToolSession } from ".";
18
- import type { OutputMeta } from "./output-meta";
19
- import { allocateOutputArtifact, createTailBuffer } from "./output-utils";
20
- import { formatBytes, wrapBrackets } from "./render-utils";
19
+ import { formatStyledTruncationWarning, type OutputMeta } from "./output-meta";
21
20
  import { ToolError } from "./tool-errors";
22
21
  import { toolResult } from "./tool-result";
23
- import { DEFAULT_MAX_BYTES } from "./truncate";
24
22
 
25
23
  const sshSchema = Type.Object({
26
24
  host: Type.String({ description: "Host name from managed SSH config or discovered ssh.json files" }),
@@ -159,8 +157,8 @@ export class SshTool implements AgentTool<typeof sshSchema, SSHToolDetails> {
159
157
  const timeoutSec = Math.max(1, Math.min(3600, rawTimeout));
160
158
  const timeoutMs = timeoutSec * 1000;
161
159
 
162
- const tailBuffer = createTailBuffer(DEFAULT_MAX_BYTES);
163
- const { artifactPath, artifactId } = await allocateOutputArtifact(this.session, "ssh");
160
+ const tailBuffer = new TailBuffer(DEFAULT_MAX_BYTES);
161
+ const { path: artifactPath, id: artifactId } = (await this.session.allocateOutputArtifact?.("ssh")) ?? {};
164
162
 
165
163
  const result = await executeSSH(hostConfig, remoteCommand, {
166
164
  timeout: timeoutMs,
@@ -253,7 +251,6 @@ export const sshToolRenderer = {
253
251
  uiTheme,
254
252
  );
255
253
  const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
256
- const truncation = details?.meta?.truncation;
257
254
  const outputBlock = new CachedOutputBlock();
258
255
 
259
256
  return {
@@ -292,19 +289,9 @@ export const sshToolRenderer = {
292
289
  }
293
290
  }
294
291
 
295
- if (truncation) {
296
- const warnings: string[] = [];
297
- if (truncation.artifactId) {
298
- warnings.push(`Full output: artifact://${truncation.artifactId}`);
299
- }
300
- if (truncation.truncatedBy === "lines") {
301
- warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
302
- } else {
303
- warnings.push(
304
- `Truncated: ${truncation.outputLines} lines shown (${formatBytes(truncation.outputBytes)} limit)`,
305
- );
306
- }
307
- outputLines.push(uiTheme.fg("warning", wrapBrackets(warnings.join(". "), uiTheme)));
292
+ if (details?.meta?.truncation) {
293
+ const warning = formatStyledTruncationWarning(details.meta, uiTheme);
294
+ if (warning) outputLines.push(warning);
308
295
  }
309
296
 
310
297
  return outputBlock.render(
@@ -89,7 +89,7 @@ export class SubmitResultTool implements AgentTool<TObject, SubmitResultDetails>
89
89
  ...(normalizedSchema as object),
90
90
  description: `Structured output matching the schema:\n${schemaHint}`,
91
91
  })
92
- : Type.Any({ description: "Structured JSON output (no schema specified)" });
92
+ : Type.Object({}, { additionalProperties: true, description: "Structured JSON output (no schema specified)" });
93
93
 
94
94
  this.parameters = Type.Object({
95
95
  data: Type.Optional(dataSchema),
@@ -24,36 +24,6 @@ export class ToolError extends Error {
24
24
  }
25
25
  }
26
26
 
27
- /**
28
- * Error entry for MultiError.
29
- */
30
- export interface ErrorEntry {
31
- message: string;
32
- context?: string;
33
- }
34
-
35
- /**
36
- * Error with multiple entries (e.g., multiple validation failures, batch errors).
37
- */
38
- export class MultiError extends ToolError {
39
- constructor(readonly errors: ErrorEntry[]) {
40
- super(errors.map(e => e.message).join("; "));
41
- this.name = "MultiError";
42
- }
43
-
44
- render(): string {
45
- if (this.errors.length === 1) {
46
- const e = this.errors[0];
47
- return e.context ? `${e.context}: ${e.message}` : e.message;
48
- }
49
- return this.errors.map(e => (e.context ? `${e.context}: ${e.message}` : e.message)).join("\n");
50
- }
51
-
52
- static from(errors: Array<string | ErrorEntry>): MultiError {
53
- return new MultiError(errors.map(e => (typeof e === "string" ? { message: e } : e)));
54
- }
55
- }
56
-
57
27
  /**
58
28
  * Error thrown when a tool operation is aborted (e.g., via AbortSignal).
59
29
  */
@@ -1,9 +1,8 @@
1
1
  import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
2
2
  import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
3
- import type { OutputSummary } from "../session/streaming-output";
3
+ import type { OutputSummary, TruncationResult } from "../session/streaming-output";
4
4
  import type { OutputMeta, TruncationOptions, TruncationSummaryOptions, TruncationTextOptions } from "./output-meta";
5
5
  import { outputMeta } from "./output-meta";
6
- import type { TruncationResult } from "./truncate";
7
6
 
8
7
  type ToolContent = Array<TextContent | ImageContent>;
9
8
 
@@ -24,9 +24,9 @@ import {
24
24
  formatExpandHint,
25
25
  formatMoreItems,
26
26
  formatStatusIcon,
27
+ formatTitle,
27
28
  replaceTabs,
28
29
  shortenPath,
29
- ToolUIKit,
30
30
  } from "./render-utils";
31
31
 
32
32
  const writeSchema = Type.Object({
@@ -151,7 +151,7 @@ function formatMetadataLine(lineCount: number | null, language: string | undefin
151
151
  return uiTheme.fg("dim", `${icon}`);
152
152
  }
153
153
 
154
- function formatStreamingContent(content: string, uiTheme: Theme, ui: ToolUIKit): string {
154
+ function formatStreamingContent(content: string, uiTheme: Theme): string {
155
155
  if (!content) return "";
156
156
  const lines = content.split("\n");
157
157
  const displayLines = lines.slice(-WRITE_STREAMING_PREVIEW_LINES);
@@ -162,13 +162,13 @@ function formatStreamingContent(content: string, uiTheme: Theme, ui: ToolUIKit):
162
162
  text += uiTheme.fg("dim", `… (${hidden} earlier lines)\n`);
163
163
  }
164
164
  for (const line of displayLines) {
165
- text += `${uiTheme.fg("toolOutput", ui.truncate(replaceTabs(line), 80))}\n`;
165
+ text += `${uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(line), 80))}\n`;
166
166
  }
167
167
  text += uiTheme.fg("dim", `… (streaming)`);
168
168
  return text;
169
169
  }
170
170
 
171
- function renderContentPreview(content: string, expanded: boolean, uiTheme: Theme, ui: ToolUIKit): string {
171
+ function renderContentPreview(content: string, expanded: boolean, uiTheme: Theme): string {
172
172
  if (!content) return "";
173
173
  const lines = content.split("\n");
174
174
  const maxLines = expanded ? lines.length : Math.min(lines.length, WRITE_PREVIEW_LINES);
@@ -177,7 +177,7 @@ function renderContentPreview(content: string, expanded: boolean, uiTheme: Theme
177
177
 
178
178
  let text = "\n\n";
179
179
  for (const line of displayLines) {
180
- text += `${uiTheme.fg("toolOutput", ui.truncate(replaceTabs(line), 80))}\n`;
180
+ text += `${uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(line), 80))}\n`;
181
181
  }
182
182
  if (!expanded && hidden > 0) {
183
183
  const hint = formatExpandHint(uiTheme, expanded, hidden > 0);
@@ -189,7 +189,6 @@ function renderContentPreview(content: string, expanded: boolean, uiTheme: Theme
189
189
 
190
190
  export const writeToolRenderer = {
191
191
  renderCall(args: WriteRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
192
- const ui = new ToolUIKit(uiTheme);
193
192
  const rawPath = args.file_path || args.path || "";
194
193
  const filePath = shortenPath(rawPath);
195
194
  const lang = getLanguageFromPath(rawPath) ?? "text";
@@ -198,14 +197,14 @@ export const writeToolRenderer = {
198
197
  const spinner =
199
198
  options?.spinnerFrame !== undefined ? formatStatusIcon("running", uiTheme, options.spinnerFrame) : "";
200
199
 
201
- let text = `${ui.title("Write")} ${spinner ? `${spinner} ` : ""}${langIcon} ${pathDisplay}`;
200
+ let text = `${formatTitle("Write", uiTheme)} ${spinner ? `${spinner} ` : ""}${langIcon} ${pathDisplay}`;
202
201
 
203
202
  if (!args.content) {
204
203
  return new Text(text, 0, 0);
205
204
  }
206
205
 
207
206
  // Show streaming preview of content (tail)
208
- text += formatStreamingContent(args.content, uiTheme, ui);
207
+ text += formatStreamingContent(args.content, uiTheme);
209
208
 
210
209
  return new Text(text, 0, 0);
211
210
  },
@@ -216,7 +215,6 @@ export const writeToolRenderer = {
216
215
  uiTheme: Theme,
217
216
  args?: WriteRenderArgs,
218
217
  ): Component {
219
- const ui = new ToolUIKit(uiTheme);
220
218
  const rawPath = args?.file_path || args?.path || "";
221
219
  const filePath = shortenPath(rawPath);
222
220
  const fileContent = args?.content || "";
@@ -247,7 +245,7 @@ export const writeToolRenderer = {
247
245
 
248
246
  let text = header;
249
247
  text += `\n${metadataLine}`;
250
- text += renderContentPreview(fileContent, expanded, uiTheme, ui);
248
+ text += renderContentPreview(fileContent, expanded, uiTheme);
251
249
 
252
250
  if (diagnostics) {
253
251
  const diagText = formatDiagnostics(diagnostics, expanded, uiTheme, fp =>
@@ -2,10 +2,15 @@
2
2
  * Render a code cell with optional output section.
3
3
  */
4
4
  import { highlightCode, type Theme } from "../modes/theme/theme";
5
- import { formatDuration, formatExpandHint, formatMoreItems, replaceTabs } from "../tools/render-utils";
5
+ import {
6
+ formatDuration,
7
+ formatExpandHint,
8
+ formatMoreItems,
9
+ formatStatusIcon,
10
+ replaceTabs,
11
+ } from "../tools/render-utils";
6
12
  import { renderOutputBlock } from "./output-block";
7
13
  import type { State } from "./types";
8
- import { getStateIcon } from "./utils";
9
14
 
10
15
  export interface CodeCellOptions {
11
16
  code: string;
@@ -35,7 +40,7 @@ function formatHeader(options: CodeCellOptions, theme: Theme): { title: string;
35
40
  const { index, total, title, status, spinnerFrame, duration } = options;
36
41
  const parts: string[] = [];
37
42
  if (status) {
38
- const icon = getStateIcon(
43
+ const icon = formatStatusIcon(
39
44
  status === "complete"
40
45
  ? "success"
41
46
  : status === "error"
@@ -2,11 +2,11 @@
2
2
  * Standardized status header rendering for tool output.
3
3
  */
4
4
  import type { Theme, ThemeColor } from "../modes/theme/theme";
5
- import type { IconType } from "./types";
6
- import { getStateIcon } from "./utils";
5
+ import type { ToolUIStatus } from "../tools/render-utils";
6
+ import { formatStatusIcon } from "../tools/render-utils";
7
7
 
8
8
  export interface StatusLineOptions {
9
- icon?: IconType;
9
+ icon?: ToolUIStatus;
10
10
  spinnerFrame?: number;
11
11
  title: string;
12
12
  titleColor?: ThemeColor;
@@ -16,7 +16,7 @@ export interface StatusLineOptions {
16
16
  }
17
17
 
18
18
  export function renderStatusLine(options: StatusLineOptions, theme: Theme): string {
19
- const icon = options.icon ? getStateIcon(options.icon, theme, options.spinnerFrame) : "";
19
+ const icon = options.icon ? formatStatusIcon(options.icon, theme, options.spinnerFrame) : "";
20
20
  const titleColor = options.titleColor ?? "accent";
21
21
  const title = theme.fg(titleColor, options.title);
22
22
  let line = icon ? `${icon} ${title}` : title;
package/src/tui/types.ts CHANGED
@@ -4,7 +4,6 @@
4
4
  import type { Theme } from "../modes/theme/theme";
5
5
 
6
6
  export type State = "pending" | "running" | "success" | "error" | "warning";
7
- export type IconType = "success" | "error" | "running" | "pending" | "warning" | "info";
8
7
 
9
8
  export interface TreeContext {
10
9
  index: number;
package/src/tui/utils.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { padding, visibleWidth } from "@oh-my-pi/pi-tui";
5
5
  import type { Theme, ThemeBg } from "../modes/theme/theme";
6
- import type { IconType, State } from "./types";
6
+ import type { State } from "./types";
7
7
 
8
8
  export { Ellipsis, truncateToWidth } from "@oh-my-pi/pi-tui";
9
9
 
@@ -101,16 +101,3 @@ export function getStateBgColor(state: State): ThemeBg {
101
101
  if (state === "error") return "toolErrorBg";
102
102
  return "toolPendingBg";
103
103
  }
104
-
105
- export function getStateIcon(icon: IconType, theme: Theme, spinnerFrame?: number): string {
106
- if (icon === "success") return theme.styledSymbol("status.success", "success");
107
- if (icon === "error") return theme.styledSymbol("status.error", "error");
108
- if (icon === "warning") return theme.styledSymbol("status.warning", "warning");
109
- if (icon === "info") return theme.styledSymbol("status.info", "accent");
110
- if (icon === "pending") return theme.styledSymbol("status.pending", "accent");
111
- if (spinnerFrame !== undefined) {
112
- const frames = theme.spinnerFrames;
113
- return frames[spinnerFrame % frames.length];
114
- }
115
- return theme.styledSymbol("status.running", "accent");
116
- }
@@ -0,0 +1,76 @@
1
+ export function parseCommandArgs(argsString: string): string[] {
2
+ const args: string[] = [];
3
+ let current = "";
4
+ let inQuote: string | null = null;
5
+
6
+ for (let i = 0; i < argsString.length; i++) {
7
+ const char = argsString[i];
8
+
9
+ if (inQuote) {
10
+ if (char === inQuote) {
11
+ inQuote = null;
12
+ } else {
13
+ current += char;
14
+ }
15
+ } else if (char === '"' || char === "'") {
16
+ inQuote = char;
17
+ } else if (char === " " || char === "\t") {
18
+ if (current) {
19
+ args.push(current);
20
+ current = "";
21
+ }
22
+ } else {
23
+ current += char;
24
+ }
25
+ }
26
+
27
+ if (current) {
28
+ args.push(current);
29
+ }
30
+
31
+ return args;
32
+ }
33
+
34
+ /**
35
+ * Substitute argument placeholders in template content
36
+ * Supports $1, $2, ... for positional args, $@ and $ARGUMENTS for all args
37
+ *
38
+ * Note: Replacement happens on the template string only. Argument values
39
+ * containing patterns like $1, $@, or $ARGUMENTS are NOT recursively substituted.
40
+ */
41
+ export function substituteArgs(content: string, args: string[]): string {
42
+ let result = content;
43
+
44
+ // Replace $1, $2, etc. with positional args FIRST (before wildcards)
45
+ // This prevents wildcard replacement values containing $<digit> patterns from being re-substituted
46
+ result = result.replace(/\$(\d+)/g, (_, num) => {
47
+ const index = parseInt(num, 10) - 1;
48
+ return args[index] ?? "";
49
+ });
50
+
51
+ result = result.replace(/\$@\[(\d+)(?::(\d*)?)?\]/g, (_, startRaw: string, lengthRaw?: string) => {
52
+ const start = Number.parseInt(startRaw, 10);
53
+ if (!Number.isFinite(start) || start < 1) return "";
54
+ const startIndex = start - 1;
55
+ if (startIndex >= args.length) return "";
56
+
57
+ if (lengthRaw === undefined || lengthRaw === "") {
58
+ return args.slice(startIndex).join(" ");
59
+ }
60
+
61
+ const length = Number.parseInt(lengthRaw, 10);
62
+ if (!Number.isFinite(length) || length <= 0) return "";
63
+ return args.slice(startIndex, startIndex + length).join(" ");
64
+ });
65
+
66
+ // Pre-compute all args joined (optimization)
67
+ const allArgs = args.join(" ");
68
+
69
+ // Replace $ARGUMENTS with all args joined (new syntax, aligns with Claude, Codex, OpenCode)
70
+ result = result.replaceAll("$ARGUMENTS", allArgs);
71
+
72
+ // Replace $@ with all args joined (existing syntax)
73
+ result = result.replaceAll("$@", allArgs);
74
+
75
+ return result;
76
+ }
@@ -11,9 +11,14 @@ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
11
11
  import { glob } from "@oh-my-pi/pi-natives";
12
12
  import { formatHashLines } from "../patch/hashline";
13
13
  import type { FileMentionMessage } from "../session/messages";
14
+ import {
15
+ DEFAULT_MAX_BYTES,
16
+ formatHeadTruncationNotice,
17
+ truncateHead,
18
+ truncateHeadBytes,
19
+ } from "../session/streaming-output";
14
20
  import { resolveReadPath } from "../tools/path-utils";
15
- import { formatAge } from "../tools/render-utils";
16
- import { DEFAULT_MAX_BYTES, formatSize, truncateHead, truncateStringToBytesFromStart } from "../tools/truncate";
21
+ import { formatAge, formatBytes } from "../tools/render-utils";
17
22
  import { fuzzyMatch } from "./fuzzy";
18
23
  import { formatDimensionNote, resizeImage } from "./image-resize";
19
24
  import { detectSupportedImageMimeTypeFromFile } from "./mime";
@@ -162,15 +167,15 @@ function buildTextOutput(textContent: string): { output: string; lineCount: numb
162
167
  if (truncation.firstLineExceedsLimit) {
163
168
  const firstLine = allLines[0] ?? "";
164
169
  const firstLineBytes = Buffer.byteLength(firstLine, "utf-8");
165
- const snippet = truncateStringToBytesFromStart(firstLine, DEFAULT_MAX_BYTES);
170
+ const snippet = truncateHeadBytes(firstLine, DEFAULT_MAX_BYTES);
166
171
  let outputText = snippet.text;
167
172
 
168
173
  if (outputText.length > 0) {
169
- outputText += `\n\n[Line 1 is ${formatSize(firstLineBytes)}, exceeds ${formatSize(
174
+ outputText += `\n\n[Line 1 is ${formatBytes(firstLineBytes)}, exceeds ${formatBytes(
170
175
  DEFAULT_MAX_BYTES,
171
- )} limit. Showing first ${formatSize(snippet.bytes)} of the line.]`;
176
+ )} limit. Showing first ${formatBytes(snippet.bytes)} of the line.]`;
172
177
  } else {
173
- outputText = `[Line 1 is ${formatSize(firstLineBytes)}, exceeds ${formatSize(
178
+ outputText = `[Line 1 is ${formatBytes(firstLineBytes)}, exceeds ${formatBytes(
174
179
  DEFAULT_MAX_BYTES,
175
180
  )} limit. Unable to display a valid UTF-8 snippet.]`;
176
181
  }
@@ -181,16 +186,7 @@ function buildTextOutput(textContent: string): { output: string; lineCount: numb
181
186
  let outputText = truncation.content;
182
187
 
183
188
  if (truncation.truncated) {
184
- const endLineDisplay = truncation.outputLines;
185
- const nextOffset = endLineDisplay + 1;
186
-
187
- if (truncation.truncatedBy === "lines") {
188
- outputText += `\n\n[Showing lines 1-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;
189
- } else {
190
- outputText += `\n\n[Showing lines 1-${endLineDisplay} of ${totalFileLines} (${formatSize(
191
- DEFAULT_MAX_BYTES,
192
- )} limit). Use offset=${nextOffset} to continue]`;
193
- }
189
+ outputText += formatHeadTruncationNotice(truncation, { startLine: 1, totalFileLines });
194
190
  }
195
191
 
196
192
  return { output: outputText, lineCount: totalFileLines };
@@ -247,7 +243,7 @@ async function buildDirectoryListing(absolutePath: string): Promise<{ output: st
247
243
  notices.push(`${DEFAULT_DIR_LIMIT} entries limit reached. Use limit=${DEFAULT_DIR_LIMIT * 2} for more`);
248
244
  }
249
245
  if (truncation.truncated) {
250
- notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
246
+ notices.push(`${formatBytes(DEFAULT_MAX_BYTES)} limit reached`);
251
247
  }
252
248
  if (notices.length > 0) {
253
249
  output += `\n\n[${notices.join(". ")}]`;
@@ -313,7 +309,7 @@ export async function generateFileMentionMessages(
313
309
  if (stat.size > MAX_AUTO_READ_IMAGE_BYTES) {
314
310
  files.push({
315
311
  path: resolvedPath,
316
- content: `(skipped auto-read: too large, ${formatSize(stat.size)})`,
312
+ content: `(skipped auto-read: too large, ${formatBytes(stat.size)})`,
317
313
  byteSize: stat.size,
318
314
  skippedReason: "tooLarge",
319
315
  });
@@ -349,7 +345,7 @@ export async function generateFileMentionMessages(
349
345
  if (stat.size > MAX_AUTO_READ_TEXT_BYTES) {
350
346
  files.push({
351
347
  path: resolvedPath,
352
- content: `(skipped auto-read: too large, ${formatSize(stat.size)})`,
348
+ content: `(skipped auto-read: too large, ${formatBytes(stat.size)})`,
353
349
  byteSize: stat.size,
354
350
  skippedReason: "tooLarge",
355
351
  });