@oh-my-pi/pi-coding-agent 6.7.670 → 6.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/package.json +6 -7
  3. package/src/cli/session-picker.ts +27 -28
  4. package/src/cli/setup-cli.ts +7 -16
  5. package/src/cli/update-cli.ts +1 -1
  6. package/src/config.ts +1 -1
  7. package/src/core/agent-session.ts +202 -37
  8. package/src/core/agent-storage.ts +1 -1
  9. package/src/core/auth-storage.ts +15 -25
  10. package/src/core/bash-executor.ts +63 -105
  11. package/src/core/custom-commands/loader.ts +1 -1
  12. package/src/core/custom-tools/loader.ts +1 -1
  13. package/src/core/custom-tools/types.ts +1 -2
  14. package/src/core/exec.ts +16 -100
  15. package/src/core/extensions/index.ts +1 -7
  16. package/src/core/extensions/loader.ts +1 -1
  17. package/src/core/extensions/runner.ts +1 -1
  18. package/src/core/extensions/types.ts +2 -2
  19. package/src/core/extensions/wrapper.ts +15 -20
  20. package/src/core/frontmatter.ts +1 -1
  21. package/src/core/history-storage.ts +3 -6
  22. package/src/core/hooks/index.ts +2 -2
  23. package/src/core/hooks/loader.ts +1 -1
  24. package/src/core/hooks/tool-wrapper.ts +14 -26
  25. package/src/core/hooks/types.ts +1 -2
  26. package/src/core/keybindings.ts +1 -1
  27. package/src/core/mcp/client.ts +13 -13
  28. package/src/core/mcp/json-rpc.ts +1 -1
  29. package/src/core/mcp/loader.ts +1 -1
  30. package/src/core/mcp/manager.ts +2 -2
  31. package/src/core/mcp/tool-cache.ts +1 -1
  32. package/src/core/mcp/transports/http.ts +32 -70
  33. package/src/core/model-registry.ts +1 -1
  34. package/src/core/plugins/installer.ts +13 -11
  35. package/src/core/prompt-templates.ts +4 -9
  36. package/src/core/python-executor.ts +23 -18
  37. package/src/core/python-gateway-coordinator.ts +29 -28
  38. package/src/core/python-kernel.ts +230 -211
  39. package/src/core/sdk.ts +10 -13
  40. package/src/core/session-manager.ts +1 -1
  41. package/src/core/settings-manager.ts +22 -9
  42. package/src/core/skills.ts +1 -1
  43. package/src/core/ssh/connection-manager.ts +19 -33
  44. package/src/core/ssh/ssh-executor.ts +39 -35
  45. package/src/core/ssh/sshfs-mount.ts +14 -33
  46. package/src/core/storage-migration.ts +1 -1
  47. package/src/core/streaming-output.ts +183 -127
  48. package/src/core/system-prompt.ts +119 -79
  49. package/src/core/title-generator.ts +1 -1
  50. package/src/core/tools/ask.ts +2 -2
  51. package/src/core/tools/bash.ts +3 -3
  52. package/src/core/tools/calculator.ts +1 -1
  53. package/src/core/tools/exa/mcp-client.ts +1 -1
  54. package/src/core/tools/exa/render.ts +1 -1
  55. package/src/core/tools/find.ts +39 -71
  56. package/src/core/tools/gemini-image.ts +1 -1
  57. package/src/core/tools/grep.ts +88 -100
  58. package/src/core/tools/index.ts +1 -1
  59. package/src/core/tools/ls.ts +1 -1
  60. package/src/core/tools/lsp/client.ts +50 -50
  61. package/src/core/tools/lsp/clients/lsp-linter-client.ts +1 -1
  62. package/src/core/tools/lsp/config.ts +1 -1
  63. package/src/core/tools/lsp/index.ts +2 -4
  64. package/src/core/tools/lsp/lspmux.ts +1 -1
  65. package/src/core/tools/lsp/rust-analyzer.ts +2 -2
  66. package/src/core/tools/lsp/utils.ts +0 -14
  67. package/src/core/tools/notebook.ts +1 -1
  68. package/src/core/tools/patch/shared.ts +3 -4
  69. package/src/core/tools/python.ts +3 -3
  70. package/src/core/tools/read.ts +29 -68
  71. package/src/core/tools/render-utils.ts +0 -5
  72. package/src/core/tools/ssh.ts +3 -3
  73. package/src/core/tools/task/model-resolver.ts +7 -9
  74. package/src/core/tools/task/worker.ts +144 -139
  75. package/src/core/tools/todo-write.ts +1 -1
  76. package/src/core/tools/truncate.ts +2 -2
  77. package/src/core/tools/web-fetch.ts +13 -15
  78. package/src/core/tools/web-scrapers/types.ts +1 -3
  79. package/src/core/tools/web-scrapers/utils.ts +14 -13
  80. package/src/core/tools/web-scrapers/youtube.ts +39 -12
  81. package/src/core/tools/web-search/auth.ts +1 -1
  82. package/src/core/tools/write.ts +1 -1
  83. package/src/core/ttsr.ts +1 -1
  84. package/src/core/utils.ts +1 -187
  85. package/src/core/voice-controller.ts +1 -1
  86. package/src/core/voice-supervisor.ts +11 -38
  87. package/src/core/voice.ts +1 -8
  88. package/src/discovery/codex.ts +1 -1
  89. package/src/index.ts +4 -4
  90. package/src/main.ts +5 -10
  91. package/src/migrations.ts +1 -1
  92. package/src/modes/index.ts +7 -40
  93. package/src/modes/interactive/components/extensions/state-manager.ts +1 -1
  94. package/src/modes/interactive/components/hook-editor.ts +12 -9
  95. package/src/modes/interactive/components/login-dialog.ts +24 -11
  96. package/src/modes/interactive/components/settings-defs.ts +9 -0
  97. package/src/modes/interactive/components/status-line.ts +36 -35
  98. package/src/modes/interactive/components/todo-display.ts +1 -1
  99. package/src/modes/interactive/components/tool-execution.ts +1 -1
  100. package/src/modes/interactive/controllers/command-controller.ts +50 -84
  101. package/src/modes/interactive/controllers/extension-ui-controller.ts +76 -76
  102. package/src/modes/interactive/controllers/input-controller.ts +12 -11
  103. package/src/modes/interactive/interactive-mode.ts +10 -11
  104. package/src/modes/interactive/theme/theme.ts +1 -1
  105. package/src/modes/interactive/types.ts +1 -1
  106. package/src/modes/rpc/rpc-client.ts +91 -121
  107. package/src/modes/rpc/rpc-mode.ts +71 -79
  108. package/src/prompts/system/ttsr-interrupt.md +7 -0
  109. package/src/utils/clipboard.ts +57 -141
  110. package/src/utils/shell-snapshot.ts +12 -60
  111. package/src/utils/shell.ts +35 -56
  112. package/src/utils/tools-manager.ts +42 -71
  113. package/src/core/logger.ts +0 -111
  114. package/src/modes/cleanup.ts +0 -23
@@ -3,21 +3,22 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
3
3
  import { StringEnum } from "@oh-my-pi/pi-ai";
4
4
  import type { Component } from "@oh-my-pi/pi-tui";
5
5
  import { Text } from "@oh-my-pi/pi-tui";
6
+ import { ptree, readLines } from "@oh-my-pi/pi-utils";
6
7
  import { Type } from "@sinclair/typebox";
7
- import type { Subprocess } from "bun";
8
+ import { $ } from "bun";
8
9
  import { getLanguageFromPath, type Theme } from "../../modes/interactive/theme/theme";
9
10
  import grepDescription from "../../prompts/tools/grep.md" with { type: "text" };
10
11
  import { ensureTool } from "../../utils/tools-manager";
11
12
  import type { RenderResultOptions } from "../custom-tools/types";
12
13
  import { renderPromptTemplate } from "../prompt-templates";
13
- import { ScopeSignal, untilAborted } from "../utils";
14
+ import { untilAborted } from "../utils";
14
15
  import type { ToolSession } from "./index";
15
16
  import { resolveToCwd } from "./path-utils";
16
- import { createToolUIKit, PREVIEW_LIMITS } from "./render-utils";
17
+ import { PREVIEW_LIMITS, ToolUIKit } from "./render-utils";
17
18
  import {
18
19
  DEFAULT_MAX_BYTES,
20
+ DEFAULT_MAX_COLUMN,
19
21
  formatSize,
20
- GREP_MAX_LINE_LENGTH,
21
22
  type TruncationResult,
22
23
  truncateHead,
23
24
  truncateLine,
@@ -118,10 +119,38 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
118
119
  private readonly session: ToolSession;
119
120
  private readonly ops: GrepOperations;
120
121
 
122
+ private readonly rgPath: Promise<string | undefined>;
123
+
121
124
  constructor(session: ToolSession, options?: GrepToolOptions) {
122
125
  this.session = session;
123
126
  this.ops = options?.operations ?? defaultGrepOperations;
124
127
  this.description = renderPromptTemplate(grepDescription);
128
+ this.rgPath = ensureTool("rg", true);
129
+ }
130
+
131
+ /**
132
+ * Validates a pattern against ripgrep's regex engine.
133
+ * Uses a quick dry-run against /dev/null to check for parse errors.
134
+ */
135
+ private async validateRegexPattern(pattern: string): Promise<{ valid: boolean; error?: string }> {
136
+ const rgPath = await this.rgPath;
137
+ if (!rgPath) {
138
+ return { valid: true }; // Can't validate, assume valid
139
+ }
140
+
141
+ // Run ripgrep against /dev/null with the pattern - this validates regex syntax
142
+ // without searching any files
143
+ const result = await $`${rgPath} --no-config --quiet -- ${pattern} /dev/null`.quiet().nothrow();
144
+ const stderr = result.stderr?.toString() ?? "";
145
+ const exitCode = result.exitCode ?? 0;
146
+
147
+ // Exit code 1 = no matches (pattern is valid), 0 = matches found
148
+ // Exit code 2 = error (often regex parse error)
149
+ if (exitCode === 2 && stderr.includes("regex parse error")) {
150
+ return { valid: false, error: stderr.trim() };
151
+ }
152
+
153
+ return { valid: true };
125
154
  }
126
155
 
127
156
  public async execute(
@@ -148,7 +177,17 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
148
177
  } = params;
149
178
 
150
179
  return untilAborted(signal, async () => {
151
- const rgPath = await ensureTool("rg", true);
180
+ // Auto-detect invalid regex patterns and switch to literal mode
181
+ // This handles cases like "abort(" which would cause ripgrep regex parse errors
182
+ let useLiteral = literal ?? false;
183
+ if (!useLiteral) {
184
+ const validation = await this.validateRegexPattern(pattern);
185
+ if (!validation.valid) {
186
+ useLiteral = true;
187
+ }
188
+ }
189
+
190
+ const rgPath = await this.rgPath;
152
191
  if (!rgPath) {
153
192
  throw new Error("ripgrep (rg) is not available and could not be downloaded");
154
193
  }
@@ -221,7 +260,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
221
260
  args.push("--multiline");
222
261
  }
223
262
 
224
- if (literal) {
263
+ if (useLiteral) {
225
264
  args.push("--fixed-strings");
226
265
  }
227
266
 
@@ -235,17 +274,11 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
235
274
 
236
275
  args.push("--", pattern, searchPath);
237
276
 
238
- const child: Subprocess = Bun.spawn([rgPath, ...args], {
239
- stdin: "ignore",
240
- stdout: "pipe",
241
- stderr: "pipe",
242
- });
277
+ const child = ptree.cspawn([rgPath, ...args], { signal });
243
278
 
244
- let stderr = "";
245
279
  let matchCount = 0;
246
280
  let matchLimitReached = false;
247
281
  let linesTruncated = false;
248
- let aborted = false;
249
282
  let killedDueToLimit = false;
250
283
  const outputLines: string[] = [];
251
284
  const files = new Set<string>();
@@ -265,49 +298,18 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
265
298
  fileMatchCounts.set(relative, (fileMatchCounts.get(relative) ?? 0) + 1);
266
299
  };
267
300
 
268
- const stopChild = (dueToLimit: boolean = false) => {
269
- killedDueToLimit = dueToLimit;
270
- child.kill();
271
- };
272
-
273
- using signalScope = new ScopeSignal(signal ? { signal } : undefined);
274
- signalScope.catch(() => {
275
- aborted = true;
276
- stopChild();
277
- });
278
-
279
301
  // For simple output modes (files_with_matches, count), process text directly
280
302
  if (effectiveOutputMode === "files_with_matches" || effectiveOutputMode === "count") {
281
- const stdoutReader = (child.stdout as ReadableStream<Uint8Array>).getReader();
282
- const stderrReader = (child.stderr as ReadableStream<Uint8Array>).getReader();
283
- const decoder = new TextDecoder();
284
- let stdout = "";
285
-
286
- await Promise.all([
287
- (async () => {
288
- while (true) {
289
- const { done, value } = await stdoutReader.read();
290
- if (done) break;
291
- stdout += decoder.decode(value, { stream: true });
292
- }
293
- })(),
294
- (async () => {
295
- while (true) {
296
- const { done, value } = await stderrReader.read();
297
- if (done) break;
298
- stderr += decoder.decode(value, { stream: true });
299
- }
300
- })(),
301
- ]);
302
-
303
- const exitCode = await child.exited;
304
-
305
- if (aborted) {
306
- throw new Error("Operation aborted");
307
- }
303
+ const stdout = await child.text().catch((x) => {
304
+ if (x instanceof ptree.Exception && x.exitCode === 1) {
305
+ return "";
306
+ }
307
+ return Promise.reject(x);
308
+ });
308
309
 
310
+ const exitCode = child.exitCode ?? 0;
309
311
  if (exitCode !== 0 && exitCode !== 1) {
310
- const errorMsg = stderr.trim() || `ripgrep exited with code ${exitCode}`;
312
+ const errorMsg = child.peekStderr().trim() || `ripgrep exited with code ${exitCode}`;
311
313
  throw new Error(errorMsg);
312
314
  }
313
315
 
@@ -484,57 +486,43 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
484
486
 
485
487
  if (matchCount >= effectiveLimit) {
486
488
  matchLimitReached = true;
487
- stopChild(true);
489
+ killedDueToLimit = true;
490
+ child.kill("SIGKILL");
488
491
  }
489
492
  }
490
493
  };
491
494
 
492
- // Read streams using Bun's ReadableStream API
493
- const stdoutReader = (child.stdout as ReadableStream<Uint8Array>).getReader();
494
- const stderrReader = (child.stderr as ReadableStream<Uint8Array>).getReader();
495
- const decoder = new TextDecoder();
496
- let stdoutBuffer = "";
497
-
498
- await Promise.all([
499
- // Process stdout line by line
500
- (async () => {
501
- while (true) {
502
- const { done, value } = await stdoutReader.read();
503
- if (done) break;
504
-
505
- stdoutBuffer += decoder.decode(value, { stream: true });
506
- const lines = stdoutBuffer.split("\n");
507
- // Keep the last incomplete line in the buffer
508
- stdoutBuffer = lines.pop() ?? "";
509
-
510
- for (const line of lines) {
511
- await processLine(line);
512
- }
513
- }
514
- // Process any remaining content
515
- if (stdoutBuffer.trim()) {
516
- await processLine(stdoutBuffer);
517
- }
518
- })(),
519
- // Collect stderr
520
- (async () => {
521
- while (true) {
522
- const { done, value } = await stderrReader.read();
523
- if (done) break;
524
- stderr += decoder.decode(value, { stream: true });
525
- }
526
- })(),
527
- ]);
528
-
529
- const exitCode = await child.exited;
530
-
531
- if (aborted) {
532
- throw new Error("Operation aborted");
495
+ // Process stdout line by line
496
+ try {
497
+ for await (const line of readLines(child.stdout)) {
498
+ await processLine(line);
499
+ }
500
+ } catch (err) {
501
+ if (err instanceof ptree.Exception && err.aborted) {
502
+ throw new Error("Operation aborted");
503
+ }
504
+ // Stream may close early if we killed due to limit - that's ok
505
+ if (!killedDueToLimit) {
506
+ throw err;
507
+ }
533
508
  }
534
509
 
535
- if (!killedDueToLimit && exitCode !== 0 && exitCode !== 1) {
536
- const errorMsg = stderr.trim() || `ripgrep exited with code ${exitCode}`;
537
- throw new Error(errorMsg);
510
+ // Wait for process to exit
511
+ try {
512
+ await child.exited;
513
+ } catch (err) {
514
+ if (err instanceof ptree.Exception) {
515
+ if (err.aborted) {
516
+ throw new Error("Operation aborted");
517
+ }
518
+ // Non-zero exit is ok if we killed due to limit or exit code 1 (no matches)
519
+ if (!killedDueToLimit && err.exitCode !== 1) {
520
+ const errorMsg = child.peekStderr().trim() || `ripgrep exited with code ${err.exitCode}`;
521
+ throw new Error(errorMsg);
522
+ }
523
+ } else {
524
+ throw err;
525
+ }
538
526
  }
539
527
 
540
528
  if (matchCount === 0) {
@@ -596,7 +584,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
596
584
  }
597
585
 
598
586
  if (linesTruncated) {
599
- notices.push(`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`);
587
+ notices.push(`Some lines truncated to ${DEFAULT_MAX_COLUMN} chars. Use read tool to see full lines`);
600
588
  details.linesTruncated = true;
601
589
  }
602
590
 
@@ -636,7 +624,7 @@ const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
636
624
  export const grepToolRenderer = {
637
625
  inline: true,
638
626
  renderCall(args: GrepRenderArgs, uiTheme: Theme): Component {
639
- const ui = createToolUIKit(uiTheme);
627
+ const ui = new ToolUIKit(uiTheme);
640
628
  const label = ui.title("Grep");
641
629
  let text = `${uiTheme.format.bullet} ${label} ${uiTheme.fg("accent", args.pattern || "?")}`;
642
630
 
@@ -665,7 +653,7 @@ export const grepToolRenderer = {
665
653
  { expanded }: RenderResultOptions,
666
654
  uiTheme: Theme,
667
655
  ): Component {
668
- const ui = createToolUIKit(uiTheme);
656
+ const ui = new ToolUIKit(uiTheme);
669
657
  const details = result.details;
670
658
 
671
659
  if (result.isError || details?.error) {
@@ -62,8 +62,8 @@ export {
62
62
  export { WriteTool, type WriteToolDetails } from "./write";
63
63
 
64
64
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
65
+ import { logger } from "@oh-my-pi/pi-utils";
65
66
  import type { EventBus } from "../event-bus";
66
- import { logger } from "../logger";
67
67
  import { getPreludeDocs, warmPythonEnvironment } from "../python-executor";
68
68
  import { checkPythonKernelAvailability } from "../python-kernel";
69
69
  import type { BashInterceptorRule } from "../settings-manager";
@@ -1,10 +1,10 @@
1
1
  import nodePath from "node:path";
2
2
  import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
3
3
  import { type Component, Text } from "@oh-my-pi/pi-tui";
4
+ import { untilAborted } from "@oh-my-pi/pi-utils";
4
5
  import { Type } from "@sinclair/typebox";
5
6
  import { getLanguageFromPath, type Theme } from "../../modes/interactive/theme/theme";
6
7
  import type { RenderResultOptions } from "../custom-tools/types";
7
- import { untilAborted } from "../utils";
8
8
  import type { ToolSession } from "./index";
9
9
  import { resolveToCwd } from "./path-utils";
10
10
  import {
@@ -1,5 +1,5 @@
1
1
  import * as fs from "node:fs";
2
- import { logger } from "../../logger";
2
+ import { logger } from "@oh-my-pi/pi-utils";
3
3
  import { applyWorkspaceEdit } from "./edits";
4
4
  import { getLspmuxCommand, isLspmuxSupported } from "./lspmux";
5
5
  import type {
@@ -711,63 +711,63 @@ export async function sendRequest(
711
711
 
712
712
  client.lastActivity = Date.now();
713
713
 
714
- return new Promise((resolve, reject) => {
715
- let timeout: ReturnType<typeof setTimeout> | undefined;
716
- const cleanup = () => {
717
- if (signal) {
718
- signal.removeEventListener("abort", abortHandler);
719
- }
720
- };
721
- const abortHandler = () => {
722
- if (client.pendingRequests.has(id)) {
723
- client.pendingRequests.delete(id);
724
- }
725
- if (timeout) clearTimeout(timeout);
726
- cleanup();
727
- const reason = signal?.reason instanceof Error ? signal.reason : new Error("Operation aborted");
728
- reject(reason);
729
- };
730
-
731
- // Set timeout
732
- timeout = setTimeout(() => {
733
- if (client.pendingRequests.has(id)) {
734
- client.pendingRequests.delete(id);
735
- const err = new Error(`LSP request ${method} timed out`);
736
- cleanup();
737
- reject(err);
738
- }
739
- }, timeoutMs);
714
+ const { promise, resolve, reject } = Promise.withResolvers<unknown>();
715
+ let timeout: ReturnType<typeof setTimeout> | undefined;
716
+ const cleanup = () => {
740
717
  if (signal) {
741
- signal.addEventListener("abort", abortHandler, { once: true });
742
- if (signal.aborted) {
743
- abortHandler();
744
- return;
745
- }
718
+ signal.removeEventListener("abort", abortHandler);
719
+ }
720
+ };
721
+ const abortHandler = () => {
722
+ if (client.pendingRequests.has(id)) {
723
+ client.pendingRequests.delete(id);
746
724
  }
725
+ if (timeout) clearTimeout(timeout);
726
+ cleanup();
727
+ const reason = signal?.reason instanceof Error ? signal.reason : new Error("Operation aborted");
728
+ reject(reason);
729
+ };
747
730
 
748
- // Register pending request with timeout wrapper
749
- client.pendingRequests.set(id, {
750
- resolve: (result) => {
751
- if (timeout) clearTimeout(timeout);
752
- cleanup();
753
- resolve(result);
754
- },
755
- reject: (err) => {
756
- if (timeout) clearTimeout(timeout);
757
- cleanup();
758
- reject(err);
759
- },
760
- method,
761
- });
731
+ // Set timeout
732
+ timeout = setTimeout(() => {
733
+ if (client.pendingRequests.has(id)) {
734
+ client.pendingRequests.delete(id);
735
+ const err = new Error(`LSP request ${method} timed out`);
736
+ cleanup();
737
+ reject(err);
738
+ }
739
+ }, timeoutMs);
740
+ if (signal) {
741
+ signal.addEventListener("abort", abortHandler, { once: true });
742
+ if (signal.aborted) {
743
+ abortHandler();
744
+ return;
745
+ }
746
+ }
762
747
 
763
- // Write request
764
- writeMessage(client.process.stdin as import("bun").FileSink, request).catch((err) => {
748
+ // Register pending request with timeout wrapper
749
+ client.pendingRequests.set(id, {
750
+ resolve: (result) => {
751
+ if (timeout) clearTimeout(timeout);
752
+ cleanup();
753
+ resolve(result);
754
+ },
755
+ reject: (err) => {
765
756
  if (timeout) clearTimeout(timeout);
766
- client.pendingRequests.delete(id);
767
757
  cleanup();
768
758
  reject(err);
769
- });
759
+ },
760
+ method,
761
+ });
762
+
763
+ // Write request
764
+ writeMessage(client.process.stdin as import("bun").FileSink, request).catch((err) => {
765
+ if (timeout) clearTimeout(timeout);
766
+ client.pendingRequests.delete(id);
767
+ cleanup();
768
+ reject(err);
770
769
  });
770
+ return promise;
771
771
  }
772
772
 
773
773
  /**
@@ -84,7 +84,7 @@ export class LspLinterClient implements LinterClient {
84
84
  if (diagnostics !== undefined) {
85
85
  return diagnostics;
86
86
  }
87
- await new Promise((resolve) => setTimeout(resolve, 100));
87
+ await Bun.sleep(100);
88
88
  }
89
89
 
90
90
  return client.diagnostics.get(uri) ?? [];
@@ -1,9 +1,9 @@
1
1
  import { homedir } from "node:os";
2
2
  import { basename, extname, join } from "node:path";
3
+ import { logger } from "@oh-my-pi/pi-utils";
3
4
  import { YAML } from "bun";
4
5
  import { globSync } from "glob";
5
6
  import { getConfigDirPaths } from "../../../config";
6
- import { logger } from "../../logger";
7
7
  import { BiomeClient } from "./clients/biome-client";
8
8
  import DEFAULTS from "./defaults.json" with { type: "json" };
9
9
  import type { ServerConfig } from "./types";
@@ -2,12 +2,11 @@ import type { Dirent } from "node:fs";
2
2
  import { existsSync, statSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
5
+ import { logger, once, untilAborted } from "@oh-my-pi/pi-utils";
5
6
  import type { BunFile } from "bun";
6
7
  import { type Theme, theme } from "../../../modes/interactive/theme/theme";
7
8
  import lspDescription from "../../../prompts/tools/lsp.md" with { type: "text" };
8
- import { logger } from "../../logger";
9
9
  import { renderPromptTemplate } from "../../prompt-templates";
10
- import { once, untilAborted } from "../../utils";
11
10
  import type { ToolSession } from "../index";
12
11
  import { resolveToCwd } from "../path-utils";
13
12
  import {
@@ -57,7 +56,6 @@ import {
57
56
  formatLocation,
58
57
  formatSymbolInformation,
59
58
  formatWorkspaceEdit,
60
- sleep,
61
59
  symbolKindToIcon,
62
60
  uriToFile,
63
61
  } from "./utils";
@@ -340,7 +338,7 @@ async function waitForDiagnostics(
340
338
  const diagnostics = client.diagnostics.get(uri);
341
339
  const versionOk = minVersion === undefined || client.diagnosticsVersion > minVersion;
342
340
  if (diagnostics !== undefined && versionOk) return diagnostics;
343
- await sleep(100);
341
+ await Bun.sleep(100);
344
342
  }
345
343
  return client.diagnostics.get(uri) ?? [];
346
344
  }
@@ -1,7 +1,7 @@
1
1
  import { homedir, platform } from "node:os";
2
2
  import { join } from "node:path";
3
+ import { logger } from "@oh-my-pi/pi-utils";
3
4
  import { TOML } from "bun";
4
- import { logger } from "../../logger";
5
5
 
6
6
  /**
7
7
  * lspmux integration for LSP server multiplexing.
@@ -1,6 +1,6 @@
1
1
  import { sendNotification, sendRequest } from "./client";
2
2
  import type { Diagnostic, ExpandMacroResult, LspClient, RelatedTest, Runnable, WorkspaceEdit } from "./types";
3
- import { fileToUri, sleep } from "./utils";
3
+ import { fileToUri } from "./utils";
4
4
 
5
5
  /**
6
6
  * Run flycheck (cargo check) and collect diagnostics.
@@ -39,7 +39,7 @@ export async function flycheck(client: LspClient, file?: string): Promise<Diagno
39
39
  let stableIterations = 0;
40
40
 
41
41
  for (let i = 0; i < maxPollIterations; i++) {
42
- await sleep(pollIntervalMs);
42
+ await Bun.sleep(pollIntervalMs);
43
43
 
44
44
  const currentDiagnosticsVersion = client.diagnosticsVersion;
45
45
  const currentDiagnosticsCount = countDiagnostics(client.diagnostics);
@@ -507,20 +507,6 @@ export function extractHoverText(
507
507
  // General Utilities
508
508
  // =============================================================================
509
509
 
510
- /**
511
- * Sleep for the specified number of milliseconds.
512
- */
513
- export function sleep(ms: number): Promise<void> {
514
- return Bun.sleep(ms);
515
- }
516
-
517
- /**
518
- * Check if a command exists in PATH.
519
- */
520
- export async function commandExists(command: string): Promise<boolean> {
521
- return Bun.which(command) !== null;
522
- }
523
-
524
510
  /**
525
511
  * Truncate a string to a maximum length with ellipsis.
526
512
  */
@@ -2,11 +2,11 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
2
2
  import { StringEnum } from "@oh-my-pi/pi-ai";
3
3
  import type { Component } from "@oh-my-pi/pi-tui";
4
4
  import { Text } from "@oh-my-pi/pi-tui";
5
+ import { untilAborted } from "@oh-my-pi/pi-utils";
5
6
  import { type Static, Type } from "@sinclair/typebox";
6
7
  import type { Theme } from "../../modes/interactive/theme/theme";
7
8
  import type { RenderResultOptions } from "../custom-tools/types";
8
9
  import type { ToolSession } from "../sdk";
9
- import { untilAborted } from "../utils";
10
10
  import { resolveToCwd } from "./path-utils";
11
11
  import {
12
12
  formatCount,
@@ -10,12 +10,11 @@ import { getLanguageFromPath, type Theme } from "../../../modes/interactive/them
10
10
  import type { RenderResultOptions } from "../../custom-tools/types";
11
11
  import type { FileDiagnosticsResult } from "../lsp/index";
12
12
  import {
13
- createToolUIKit,
14
13
  formatExpandHint,
15
14
  formatStatusIcon,
16
15
  getDiffStats,
17
16
  shortenPath,
18
- type ToolUIKit,
17
+ ToolUIKit,
19
18
  truncateDiffByHunk,
20
19
  } from "../render-utils";
21
20
  import type { RenderCallOptions } from "../renderers";
@@ -157,7 +156,7 @@ export const editToolRenderer = {
157
156
  mergeCallAndResult: true,
158
157
 
159
158
  renderCall(args: EditRenderArgs, uiTheme: Theme, options?: RenderCallOptions): Component {
160
- const ui = createToolUIKit(uiTheme);
159
+ const ui = new ToolUIKit(uiTheme);
161
160
  const rawPath = args.file_path || args.path || "";
162
161
  const filePath = shortenPath(rawPath);
163
162
  const editLanguage = getLanguageFromPath(rawPath) ?? "text";
@@ -190,7 +189,7 @@ export const editToolRenderer = {
190
189
  uiTheme: Theme,
191
190
  args?: EditRenderArgs,
192
191
  ): Component {
193
- const ui = createToolUIKit(uiTheme);
192
+ const ui = new ToolUIKit(uiTheme);
194
193
  const { expanded, renderContext } = options;
195
194
  const rawPath = args?.file_path || args?.path || "";
196
195
  const filePath = shortenPath(rawPath);
@@ -13,7 +13,7 @@ import { executePython, getPreludeDocs, type PythonExecutorOptions } from "../py
13
13
  import type { PreludeHelper, PythonStatusEvent } from "../python-kernel";
14
14
  import type { ToolSession } from "./index";
15
15
  import { resolveToCwd } from "./path-utils";
16
- import { createToolUIKit, getTreeBranch, getTreeContinuePrefix, shortenPath, truncate } from "./render-utils";
16
+ import { getTreeBranch, getTreeContinuePrefix, shortenPath, ToolUIKit, truncate } from "./render-utils";
17
17
  import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateTail } from "./truncate";
18
18
 
19
19
  export const PYTHON_DEFAULT_PREVIEW_LINES = 10;
@@ -610,7 +610,7 @@ function renderStatusEvents(events: PythonStatusEvent[], theme: Theme, expanded:
610
610
 
611
611
  export const pythonToolRenderer = {
612
612
  renderCall(args: PythonRenderArgs, uiTheme: Theme): Component {
613
- const ui = createToolUIKit(uiTheme);
613
+ const ui = new ToolUIKit(uiTheme);
614
614
  const code = args.code || uiTheme.format.ellipsis;
615
615
  const prompt = uiTheme.fg("accent", ">>>");
616
616
  const cwd = process.cwd();
@@ -642,7 +642,7 @@ export const pythonToolRenderer = {
642
642
  options: RenderResultOptions & { renderContext?: PythonRenderContext },
643
643
  uiTheme: Theme,
644
644
  ): Component {
645
- const ui = createToolUIKit(uiTheme);
645
+ const ui = new ToolUIKit(uiTheme);
646
646
  const { renderContext } = options;
647
647
  const details = result.details;
648
648