@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
package/src/lsp/utils.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export { truncate } from "@oh-my-pi/pi-utils";
2
+
1
3
  import path from "node:path";
2
4
  import { type Theme, theme } from "../modes/theme/theme";
3
5
  import type {
@@ -523,27 +525,3 @@ export function extractHoverText(
523
525
 
524
526
  // =============================================================================
525
527
  // General Utilities
526
- // =============================================================================
527
-
528
- /**
529
- * Truncate a string to a maximum length with ellipsis.
530
- */
531
- export function truncate(str: string, maxLength: number): string {
532
- if (str.length <= maxLength) return str;
533
- return `${str.slice(0, maxLength - 1)}…`;
534
- }
535
-
536
- /**
537
- * Group items by a key function.
538
- */
539
- export function groupBy<T, K extends string | number>(items: T[], keyFn: (item: T) => K): Record<K, T[]> {
540
- const result = {} as Record<K, T[]>;
541
- for (const item of items) {
542
- const key = keyFn(item);
543
- if (!result[key]) {
544
- result[key] = [];
545
- }
546
- result[key].push(item);
547
- }
548
- return result;
549
- }
package/src/main.ts CHANGED
@@ -51,14 +51,6 @@ async function checkForNewVersion(currentVersion: string): Promise<string | unde
51
51
  }
52
52
  }
53
53
 
54
- const writeStdout = (message: string): void => {
55
- process.stdout.write(`${message}\n`);
56
- };
57
-
58
- const writeStderr = (message: string): void => {
59
- process.stderr.write(`${message}\n`);
60
- };
61
-
62
54
  async function readPipedInput(): Promise<string | undefined> {
63
55
  if (process.stdin.isTTY !== false) return undefined;
64
56
  try {
@@ -381,7 +373,7 @@ async function buildSessionOptions(
381
373
  preferences: modelMatchPreferences,
382
374
  });
383
375
  if (resolved.warning) {
384
- writeStderr(chalk.yellow(`Warning: ${resolved.warning}`));
376
+ process.stderr.write(`${chalk.yellow(`Warning: ${resolved.warning}`)}\n`);
385
377
  }
386
378
  if (resolved.error) {
387
379
  if (!parsed.provider && !parsed.model.includes(":")) {
@@ -389,7 +381,7 @@ async function buildSessionOptions(
389
381
  // (extensions may register additional providers/models via registerProvider)
390
382
  options.modelPattern = parsed.model;
391
383
  } else {
392
- writeStderr(chalk.red(resolved.error));
384
+ process.stderr.write(`${chalk.red(resolved.error)}\n`);
393
385
  process.exit(1);
394
386
  }
395
387
  } else if (resolved.model) {
@@ -514,7 +506,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
514
506
  });
515
507
 
516
508
  if (parsedArgs.version) {
517
- writeStdout(VERSION);
509
+ process.stdout.write(`${VERSION}\n`);
518
510
  process.exit(0);
519
511
  }
520
512
 
@@ -531,15 +523,15 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
531
523
  result = await exportFromFile(parsedArgs.export, outputPath);
532
524
  } catch (error: unknown) {
533
525
  const message = error instanceof Error ? error.message : "Failed to export session";
534
- writeStderr(chalk.red(`Error: ${message}`));
526
+ process.stderr.write(`${chalk.red(`Error: ${message}`)}\n`);
535
527
  process.exit(1);
536
528
  }
537
- writeStdout(`Exported to: ${result}`);
529
+ process.stdout.write(`Exported to: ${result}\n`);
538
530
  process.exit(0);
539
531
  }
540
532
 
541
533
  if (parsedArgs.mode === "rpc" && parsedArgs.fileArgs.length > 0) {
542
- writeStderr(chalk.red("Error: @file arguments are not supported in RPC mode"));
534
+ process.stderr.write(`${chalk.red("Error: @file arguments are not supported in RPC mode")}\n`);
543
535
  process.exit(1);
544
536
  }
545
537
 
@@ -614,12 +606,12 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
614
606
  SessionManager.list(cwd, parsedArgs.sessionDir),
615
607
  );
616
608
  if (sessions.length === 0) {
617
- writeStdout(chalk.dim("No sessions found"));
609
+ process.stdout.write(`${chalk.dim("No sessions found")}\n`);
618
610
  return;
619
611
  }
620
612
  const selectedPath = await logger.timeAsync("selectSession", () => selectSession(sessions));
621
613
  if (!selectedPath) {
622
- writeStdout(chalk.dim("No session selected"));
614
+ process.stdout.write(`${chalk.dim("No session selected")}\n`);
623
615
  return;
624
616
  }
625
617
  sessionManager = await SessionManager.open(selectedPath);
@@ -635,8 +627,8 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
635
627
  // Handle CLI --api-key as runtime override (not persisted)
636
628
  if (parsedArgs.apiKey) {
637
629
  if (!sessionOptions.model && !sessionOptions.modelPattern) {
638
- writeStderr(
639
- chalk.red("--api-key requires a model to be specified via --model, --provider/--model, or --models"),
630
+ process.stderr.write(
631
+ `${chalk.red("--api-key requires a model to be specified via --model, --provider/--model, or --models")}\n`,
640
632
  );
641
633
  process.exit(1);
642
634
  }
@@ -689,13 +681,13 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
689
681
 
690
682
  if (!isInteractive && !session.model) {
691
683
  if (modelFallbackMessage) {
692
- writeStderr(chalk.red(modelFallbackMessage));
684
+ process.stderr.write(`${chalk.red(modelFallbackMessage)}\n`);
693
685
  } else {
694
- writeStderr(chalk.red("No models available."));
686
+ process.stderr.write(`${chalk.red("No models available.")}\n`);
695
687
  }
696
- writeStderr(chalk.yellow("\nSet an API key environment variable:"));
697
- writeStderr(" ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, etc.");
698
- writeStderr(chalk.yellow(`\nOr create ${ModelsConfigFile.path()}`));
688
+ process.stderr.write(`${chalk.yellow("\nSet an API key environment variable:")}\n`);
689
+ process.stderr.write(" ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, etc.\n");
690
+ process.stderr.write(`${chalk.yellow(`\nOr create ${ModelsConfigFile.path()}`)}\n`);
699
691
  process.exit(1);
700
692
  }
701
693
 
@@ -728,7 +720,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
728
720
  return `${scopedModel.model.id}${thinkingStr}`;
729
721
  })
730
722
  .join(", ");
731
- writeStdout(chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Ctrl+P to cycle)")}`));
723
+ process.stdout.write(`${chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Ctrl+P to cycle)")}`)}\n`);
732
724
  }
733
725
 
734
726
  if ($env.PI_TIMING === "1") {
package/src/mcp/client.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Handles connection initialization, tool listing, and tool calling.
5
5
  */
6
+ import { withTimeout } from "@oh-my-pi/pi-utils";
6
7
  import { createHttpTransport } from "./transports/http";
7
8
  import { createStdioTransport } from "./transports/stdio";
8
9
  import type {
@@ -34,52 +35,6 @@ const CLIENT_INFO = {
34
35
  version: "1.0.0",
35
36
  };
36
37
 
37
- /** Wrap a promise with a timeout and optional abort signal */
38
- function withTimeout<T>(promise: Promise<T>, ms: number, message: string, signal?: AbortSignal): Promise<T> {
39
- if (signal?.aborted) {
40
- const reason = signal.reason instanceof Error ? signal.reason : new Error("Aborted");
41
- return Promise.reject(reason);
42
- }
43
-
44
- const { promise: wrapped, resolve, reject } = Promise.withResolvers<T>();
45
- let settled = false;
46
- const timeoutId = setTimeout(() => {
47
- if (settled) return;
48
- settled = true;
49
- reject(new Error(message));
50
- }, ms);
51
-
52
- const onAbort = () => {
53
- if (settled) return;
54
- settled = true;
55
- clearTimeout(timeoutId);
56
- reject(signal?.reason instanceof Error ? signal.reason : new Error("Aborted"));
57
- };
58
-
59
- if (signal) {
60
- signal.addEventListener("abort", onAbort, { once: true });
61
- }
62
-
63
- promise.then(
64
- value => {
65
- if (settled) return;
66
- settled = true;
67
- clearTimeout(timeoutId);
68
- if (signal) signal.removeEventListener("abort", onAbort);
69
- resolve(value);
70
- },
71
- error => {
72
- if (settled) return;
73
- settled = true;
74
- clearTimeout(timeoutId);
75
- if (signal) signal.removeEventListener("abort", onAbort);
76
- reject(error);
77
- },
78
- );
79
-
80
- return wrapped;
81
- }
82
-
83
38
  /**
84
39
  * Create a transport for the given server config.
85
40
  */
package/src/mcp/render.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  JSON_TREE_SCALAR_LEN_COLLAPSED,
18
18
  JSON_TREE_SCALAR_LEN_EXPANDED,
19
19
  renderJsonTreeLines,
20
+ stripInternalArgs,
20
21
  } from "../tools/json-tree";
21
22
  import { formatExpandHint, truncateToWidth } from "../tools/render-utils";
22
23
  import { renderStatusLine } from "../tui";
@@ -57,7 +58,13 @@ export function renderMCPResult(
57
58
  lines.push(`${theme.fg("dim", "Args")}`);
58
59
  const maxDepth = JSON_TREE_MAX_DEPTH_EXPANDED;
59
60
  const maxLines = JSON_TREE_MAX_LINES_EXPANDED;
60
- const tree = renderJsonTreeLines(args, theme, maxDepth, maxLines, JSON_TREE_SCALAR_LEN_EXPANDED);
61
+ const tree = renderJsonTreeLines(
62
+ stripInternalArgs(args),
63
+ theme,
64
+ maxDepth,
65
+ maxLines,
66
+ JSON_TREE_SCALAR_LEN_EXPANDED,
67
+ );
61
68
  for (const line of tree.lines) {
62
69
  lines.push(line);
63
70
  }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Stores tool definitions per server in agent.db for fast startup.
5
5
  */
6
- import { logger } from "@oh-my-pi/pi-utils";
6
+ import { isRecord, logger } from "@oh-my-pi/pi-utils";
7
7
  import type { AgentStorage } from "../session/agent-storage";
8
8
  import type { MCPServerConfig, MCPToolDefinition } from "./types";
9
9
 
@@ -17,10 +17,6 @@ type MCPToolCachePayload = {
17
17
  tools: MCPToolDefinition[];
18
18
  };
19
19
 
20
- function isRecord(value: unknown): value is Record<string, unknown> {
21
- return !!value && typeof value === "object" && !Array.isArray(value);
22
- }
23
-
24
20
  function stableClone(value: unknown): unknown {
25
21
  if (Array.isArray(value)) {
26
22
  return value.map(item => stableClone(item));
@@ -4,7 +4,7 @@
4
4
  * Implements JSON-RPC 2.0 over HTTP POST with optional SSE streaming.
5
5
  * Based on MCP spec 2025-03-26.
6
6
  */
7
- import { readSseJson } from "@oh-my-pi/pi-utils";
7
+ import { readSseJson, Snowflake } from "@oh-my-pi/pi-utils";
8
8
  import type {
9
9
  JsonRpcMessage,
10
10
  JsonRpcResponse,
@@ -14,11 +14,6 @@ import type {
14
14
  MCPTransport,
15
15
  } from "../../mcp/types";
16
16
 
17
- /** Generate unique request ID */
18
- function generateId(): string {
19
- return Math.random().toString(36).slice(2) + Date.now().toString(36);
20
- }
21
-
22
17
  /**
23
18
  * HTTP transport for MCP servers.
24
19
  * Uses POST for requests, supports SSE responses.
@@ -112,7 +107,7 @@ export class HttpTransport implements MCPTransport {
112
107
  throw new Error("Transport not connected");
113
108
  }
114
109
 
115
- const id = generateId();
110
+ const id = Snowflake.next();
116
111
  const body = {
117
112
  jsonrpc: "2.0" as const,
118
113
  id,
@@ -5,16 +5,11 @@
5
5
  * Messages are newline-delimited JSON.
6
6
  */
7
7
 
8
- import { readJsonl } from "@oh-my-pi/pi-utils";
8
+ import { readJsonl, Snowflake } from "@oh-my-pi/pi-utils";
9
9
  import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
10
10
  import { type Subprocess, spawn } from "bun";
11
11
  import type { JsonRpcResponse, MCPRequestOptions, MCPStdioServerConfig, MCPTransport } from "../../mcp/types";
12
12
 
13
- /** Generate unique request ID */
14
- function generateId(): string {
15
- return Math.random().toString(36).slice(2) + Date.now().toString(36);
16
- }
17
-
18
13
  /**
19
14
  * Stdio transport for MCP servers.
20
15
  * Spawns a subprocess and communicates via stdin/stdout.
@@ -156,7 +151,7 @@ export class StdioTransport implements MCPTransport {
156
151
  throw new Error("Transport not connected");
157
152
  }
158
153
 
159
- const id = generateId();
154
+ const id = Snowflake.next();
160
155
  const request = {
161
156
  jsonrpc: "2.0" as const,
162
157
  id,
@@ -5,8 +5,7 @@
5
5
  import { sanitizeText } from "@oh-my-pi/pi-natives";
6
6
  import { Container, Loader, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
7
7
  import { getSymbolTheme, theme } from "../../modes/theme/theme";
8
- import type { TruncationMeta } from "../../tools/output-meta";
9
- import { formatSize } from "../../tools/truncate";
8
+ import { formatTruncationMetaNotice, type TruncationMeta } from "../../tools/output-meta";
10
9
  import { DynamicBorder } from "./dynamic-border";
11
10
  import { truncateToVisualLines } from "./visual-truncate";
12
11
 
@@ -167,20 +166,7 @@ export class BashExecutionComponent extends Container {
167
166
  }
168
167
 
169
168
  if (this.#truncation) {
170
- const warnings: string[] = [];
171
- if (this.#truncation.artifactId) {
172
- warnings.push(`Full output: artifact://${this.#truncation.artifactId}`);
173
- }
174
- if (this.#truncation.truncatedBy === "lines") {
175
- warnings.push(
176
- `Truncated: showing ${this.#truncation.outputLines} of ${this.#truncation.totalLines} lines`,
177
- );
178
- } else {
179
- warnings.push(
180
- `Truncated: ${this.#truncation.outputLines} lines shown (${formatSize(this.#truncation.outputBytes)} limit)`,
181
- );
182
- }
183
- statusParts.push(theme.fg("warning", warnings.join(". ")));
169
+ statusParts.push(theme.fg("warning", formatTruncationMetaNotice(this.#truncation)));
184
170
  }
185
171
 
186
172
  if (statusParts.length > 0) {
@@ -6,6 +6,7 @@
6
6
  import * as os from "node:os";
7
7
  import { type Component, truncateToWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
8
8
  import { theme } from "../../../modes/theme/theme";
9
+ import { shortenPath } from "../../../tools/render-utils";
9
10
  import type { Extension, ExtensionState } from "./types";
10
11
 
11
12
  export class InspectorPanel implements Component {
@@ -46,7 +47,13 @@ export class InspectorPanel implements Component {
46
47
  lines.push(theme.fg("muted", "Origin:"));
47
48
  const levelLabel = ext.source.level === "user" ? "User" : ext.source.level === "project" ? "Project" : "Native";
48
49
  lines.push(` ${theme.italic(`via ${ext.source.providerName} (${levelLabel})`)}`);
49
- lines.push(` ${theme.fg("dim", this.#shortenPath(ext.path))}`);
50
+ const shortened = shortenPath(ext.path, os.homedir());
51
+ // If path is very long, show just the last parts
52
+ const displayPath =
53
+ shortened.length > 40 && shortened.split("/").length > 3
54
+ ? `.../${shortened.split("/").slice(-3).join("/")}`
55
+ : shortened;
56
+ lines.push(` ${theme.fg("dim", displayPath)}`);
50
57
  lines.push("");
51
58
 
52
59
  // Status badge
@@ -301,21 +308,4 @@ export class InspectorPanel implements Component {
301
308
  return theme.fg("warning", `${theme.status.shadowed} Shadowed${shadowedBy ? ` by ${shadowedBy}` : ""}`);
302
309
  }
303
310
  }
304
-
305
- #shortenPath(path: string): string {
306
- const home = os.homedir();
307
- if (home && path.startsWith(home)) {
308
- return `~${path.slice(home.length)}`;
309
- }
310
-
311
- // If path is very long, show just the last parts
312
- if (path.length > 40) {
313
- const parts = path.split("/");
314
- if (parts.length > 3) {
315
- return `.../${parts.slice(-3).join("/")}`;
316
- }
317
- }
318
-
319
- return path;
320
- }
321
311
  }
@@ -1,42 +1,11 @@
1
1
  import * as fs from "node:fs";
2
- import * as path from "node:path";
3
2
  import { type Component, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
4
- import { isEnoent } from "@oh-my-pi/pi-utils";
3
+ import { formatNumber } from "@oh-my-pi/pi-utils";
5
4
  import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
6
5
  import { theme } from "../../modes/theme/theme";
7
6
  import type { AgentSession } from "../../session/agent-session";
8
7
  import { shortenPath } from "../../tools/render-utils";
9
-
10
- /**
11
- * Sanitize text for display in a single-line status.
12
- * Removes newlines, tabs, carriage returns, and other control characters.
13
- */
14
- function sanitizeStatusText(text: string): string {
15
- // Replace newlines, tabs, carriage returns with space, then collapse multiple spaces
16
- return text
17
- .replace(/[\r\n\t]/g, " ")
18
- .replace(/ +/g, " ")
19
- .trim();
20
- }
21
-
22
- /** Find the git root by walking up from cwd. Returns path and content of .git/HEAD if found. */
23
- async function findGitHeadPath(): Promise<{ path: string; content: string } | null> {
24
- let dir = getProjectDir();
25
- while (true) {
26
- const gitHeadPath = path.join(dir, ".git", "HEAD");
27
- try {
28
- const content = await Bun.file(gitHeadPath).text();
29
- return { path: gitHeadPath, content };
30
- } catch (err) {
31
- if (!isEnoent(err)) throw err;
32
- }
33
- const parent = path.dirname(dir);
34
- if (parent === dir) {
35
- return null;
36
- }
37
- dir = parent;
38
- }
39
- }
8
+ import { findGitHeadPathAsync, sanitizeStatusText } from "../shared";
40
9
 
41
10
  /**
42
11
  * Footer component that shows pwd, token stats, and context usage
@@ -85,7 +54,7 @@ export class FooterComponent implements Component {
85
54
  this.#gitWatcher = null;
86
55
  }
87
56
 
88
- findGitHeadPath().then(result => {
57
+ findGitHeadPathAsync().then(result => {
89
58
  if (!result) {
90
59
  return;
91
60
  }
@@ -130,7 +99,7 @@ export class FooterComponent implements Component {
130
99
 
131
100
  // Note: fire-and-forget async call - will return undefined on first call
132
101
  // This is acceptable since it's a cached value that will update on next render
133
- findGitHeadPath().then(result => {
102
+ findGitHeadPathAsync().then(result => {
134
103
  if (!result) {
135
104
  this.#cachedBranch = null;
136
105
  if (this.#onBranchChange) {
@@ -181,15 +150,6 @@ export class FooterComponent implements Component {
181
150
  const contextPercentValue = contextUsage?.percent ?? 0;
182
151
  const contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
183
152
 
184
- // Format token counts (similar to web-ui)
185
- const formatTokens = (count: number): string => {
186
- if (count < 1000) return count.toString();
187
- if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
188
- if (count < 1000000) return `${Math.round(count / 1000)}k`;
189
- if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;
190
- return `${Math.round(count / 1000000)}M`;
191
- };
192
-
193
153
  // Replace home directory with ~
194
154
  let pwd = shortenPath(getProjectDir());
195
155
 
@@ -213,10 +173,10 @@ export class FooterComponent implements Component {
213
173
 
214
174
  // Build stats line
215
175
  const statsParts = [];
216
- if (totalInput) statsParts.push(`↑${formatTokens(totalInput)}`);
217
- if (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);
218
- if (totalCacheRead) statsParts.push(`R${formatTokens(totalCacheRead)}`);
219
- if (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);
176
+ if (totalInput) statsParts.push(`↑${formatNumber(totalInput)}`);
177
+ if (totalOutput) statsParts.push(`↓${formatNumber(totalOutput)}`);
178
+ if (totalCacheRead) statsParts.push(`R${formatNumber(totalCacheRead)}`);
179
+ if (totalCacheWrite) statsParts.push(`W${formatNumber(totalCacheWrite)}`);
220
180
 
221
181
  // Show cost with "(sub)" indicator if using OAuth subscription
222
182
  const usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;
@@ -230,8 +190,8 @@ export class FooterComponent implements Component {
230
190
  const autoIndicator = this.#autoCompactEnabled ? " (auto)" : "";
231
191
  const contextPercentDisplay =
232
192
  contextPercent === "?"
233
- ? `?/${formatTokens(contextWindow)}${autoIndicator}`
234
- : `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;
193
+ ? `?/${formatNumber(contextWindow)}${autoIndicator}`
194
+ : `${contextPercent}%/${formatNumber(contextWindow)}${autoIndicator}`;
235
195
  if (contextPercentValue > 90) {
236
196
  contextPercentStr = theme.fg("error", contextPercentDisplay);
237
197
  } else if (contextPercentValue > 70) {
@@ -1,21 +1,11 @@
1
1
  import { type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
2
- import {
3
- Container,
4
- Input,
5
- matchesKey,
6
- Spacer,
7
- type Tab,
8
- TabBar,
9
- type TabBarTheme,
10
- Text,
11
- type TUI,
12
- visibleWidth,
13
- } from "@oh-my-pi/pi-tui";
2
+ import { Container, Input, matchesKey, Spacer, type Tab, TabBar, Text, type TUI, visibleWidth } from "@oh-my-pi/pi-tui";
14
3
  import { MODEL_ROLE_IDS, MODEL_ROLES, type ModelRegistry, type ModelRole } from "../../config/model-registry";
15
4
  import { parseModelString } from "../../config/model-resolver";
16
5
  import type { Settings } from "../../config/settings";
17
6
  import { type ThemeColor, theme } from "../../modes/theme/theme";
18
7
  import { fuzzyFilter } from "../../utils/fuzzy";
8
+ import { getTabBarTheme } from "../shared";
19
9
  import { DynamicBorder } from "./dynamic-border";
20
10
 
21
11
  function makeInvertedBadge(label: string, color: ThemeColor): string {
@@ -44,15 +34,6 @@ const MENU_ACTIONS: MenuAction[] = MODEL_ROLE_IDS.map(role => ({ label: `Set as
44
34
 
45
35
  const ALL_TAB = "ALL";
46
36
 
47
- function getTabBarTheme(): TabBarTheme {
48
- return {
49
- label: (text: string) => theme.bold(theme.fg("accent", text)),
50
- activeTab: (text: string) => theme.bold(theme.bg("selectedBg", theme.fg("text", text))),
51
- inactiveTab: (text: string) => theme.fg("muted", text),
52
- hint: (text: string) => theme.fg("dim", text),
53
- };
54
- }
55
-
56
37
  /**
57
38
  * Component that renders a model selector with provider tabs and context menu.
58
39
  * - Tab/Arrow Left/Right: Switch between provider tabs
@@ -6,8 +6,7 @@
6
6
  import { sanitizeText } from "@oh-my-pi/pi-natives";
7
7
  import { Container, Loader, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
8
8
  import { getSymbolTheme, highlightCode, theme } from "../../modes/theme/theme";
9
- import type { TruncationMeta } from "../../tools/output-meta";
10
- import { formatSize } from "../../tools/truncate";
9
+ import { formatTruncationMetaNotice, type TruncationMeta } from "../../tools/output-meta";
11
10
  import { DynamicBorder } from "./dynamic-border";
12
11
  import { truncateToVisualLines } from "./visual-truncate";
13
12
 
@@ -151,20 +150,7 @@ export class PythonExecutionComponent extends Container {
151
150
  }
152
151
 
153
152
  if (this.#truncation) {
154
- const warnings: string[] = [];
155
- if (this.#truncation.artifactId) {
156
- warnings.push(`Full output: artifact://${this.#truncation.artifactId}`);
157
- }
158
- if (this.#truncation.truncatedBy === "lines") {
159
- warnings.push(
160
- `Truncated: showing ${this.#truncation.outputLines} of ${this.#truncation.totalLines} lines`,
161
- );
162
- } else {
163
- warnings.push(
164
- `Truncated: ${this.#truncation.outputLines} lines shown (${formatSize(this.#truncation.outputBytes)} limit)`,
165
- );
166
- }
167
- statusParts.push(theme.fg("warning", warnings.join(". ")));
153
+ statusParts.push(theme.fg("warning", formatTruncationMetaNotice(this.#truncation)));
168
154
  }
169
155
 
170
156
  if (statusParts.length > 0) {
@@ -9,7 +9,6 @@ import {
9
9
  Spacer,
10
10
  type Tab,
11
11
  TabBar,
12
- type TabBarTheme,
13
12
  Text,
14
13
  } from "@oh-my-pi/pi-tui";
15
14
  import { type SettingPath, settings } from "../../config/settings";
@@ -21,20 +20,12 @@ import type {
21
20
  } from "../../config/settings-schema";
22
21
  import { SETTING_TABS, TAB_METADATA } from "../../config/settings-schema";
23
22
  import { getCurrentThemeName, getSelectListTheme, getSettingsListTheme, theme } from "../../modes/theme/theme";
23
+ import { getTabBarTheme } from "../shared";
24
24
  import { DynamicBorder } from "./dynamic-border";
25
25
  import { PluginSettingsComponent } from "./plugin-settings";
26
26
  import { getSettingsForTab, type SettingDef } from "./settings-defs";
27
27
  import { getPreset } from "./status-line/presets";
28
28
 
29
- function getTabBarTheme(): TabBarTheme {
30
- return {
31
- label: text => theme.bold(theme.fg("accent", text)),
32
- activeTab: text => theme.bold(theme.bg("selectedBg", theme.fg("text", text))),
33
- inactiveTab: text => theme.fg("muted", text),
34
- hint: text => theme.fg("dim", text),
35
- };
36
- }
37
-
38
29
  /**
39
30
  * A submenu component for selecting from a list of options.
40
31
  */