@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
@@ -4,6 +4,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
4
4
  import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
5
5
  import type { Component } from "@oh-my-pi/pi-tui";
6
6
  import { Text } from "@oh-my-pi/pi-tui";
7
+ import { ptree, untilAborted } from "@oh-my-pi/pi-utils";
7
8
  import { Type } from "@sinclair/typebox";
8
9
  import { CONFIG_DIR_NAME } from "../../config";
9
10
  import type { Theme } from "../../modes/interactive/theme/theme";
@@ -14,7 +15,7 @@ import { ensureTool } from "../../utils/tools-manager";
14
15
  import type { RenderResultOptions } from "../custom-tools/types";
15
16
  import { renderPromptTemplate } from "../prompt-templates";
16
17
  import type { ToolSession } from "../sdk";
17
- import { ScopeSignal, untilAborted } from "../utils";
18
+ import { runFd } from "./find";
18
19
  import { LsTool } from "./ls";
19
20
  import { resolveReadPath, resolveToCwd } from "./path-utils";
20
21
  import { shortenPath, wrapBrackets } from "./render-utils";
@@ -153,53 +154,6 @@ function similarityScore(a: string, b: string): number {
153
154
  return 1 - distance / maxLen;
154
155
  }
155
156
 
156
- async function captureCommandOutput(
157
- command: string,
158
- args: string[],
159
- signal?: AbortSignal,
160
- ): Promise<{ stdout: string; stderr: string; exitCode: number | null; aborted: boolean }> {
161
- const child = Bun.spawn([command, ...args], {
162
- stdin: "ignore",
163
- stdout: "pipe",
164
- stderr: "pipe",
165
- });
166
-
167
- using scope = new ScopeSignal(signal ? { signal } : undefined);
168
- scope.catch(() => {
169
- child.kill();
170
- });
171
-
172
- const stdoutReader = (child.stdout as ReadableStream<Uint8Array>).getReader();
173
- const stderrReader = (child.stderr as ReadableStream<Uint8Array>).getReader();
174
- const stdoutDecoder = new TextDecoder();
175
- const stderrDecoder = new TextDecoder();
176
- let stdout = "";
177
- let stderr = "";
178
-
179
- await Promise.all([
180
- (async () => {
181
- while (true) {
182
- const { done, value } = await stdoutReader.read();
183
- if (done) break;
184
- stdout += stdoutDecoder.decode(value, { stream: true });
185
- }
186
- stdout += stdoutDecoder.decode();
187
- })(),
188
- (async () => {
189
- while (true) {
190
- const { done, value } = await stderrReader.read();
191
- if (done) break;
192
- stderr += stderrDecoder.decode(value, { stream: true });
193
- }
194
- stderr += stderrDecoder.decode();
195
- })(),
196
- ]);
197
-
198
- const exitCode = await child.exited;
199
-
200
- return { stdout, stderr, exitCode, aborted: scope.aborted };
201
- }
202
-
203
157
  async function listCandidateFiles(
204
158
  searchRoot: string,
205
159
  signal?: AbortSignal,
@@ -238,10 +192,7 @@ async function listCandidateFiles(
238
192
  ".git",
239
193
  searchRoot,
240
194
  ];
241
- const { stdout, aborted } = await captureCommandOutput(fdPath, gitignoreArgs, signal);
242
- if (aborted) {
243
- throw new Error("Operation aborted");
244
- }
195
+ const { stdout } = await runFd(fdPath, gitignoreArgs, signal);
245
196
  const output = stdout.trim();
246
197
  if (output) {
247
198
  const nestedGitignores = output
@@ -269,17 +220,11 @@ async function listCandidateFiles(
269
220
 
270
221
  args.push(".", searchRoot);
271
222
 
272
- const { stdout, stderr, exitCode, aborted } = await captureCommandOutput(fdPath, args, signal);
273
-
274
- if (aborted) {
275
- throw new Error("Operation aborted");
276
- }
277
-
223
+ const { stdout, stderr, exitCode } = await runFd(fdPath, args, signal);
278
224
  const output = stdout.trim();
279
225
 
280
226
  if (exitCode !== 0 && !output) {
281
- const errorMsg = stderr.trim() || `fd exited with code ${exitCode ?? -1}`;
282
- return { files: [], truncated: false, error: errorMsg };
227
+ return { files: [], truncated: false, error: stderr.trim() || `fd exited with code ${exitCode ?? -1}` };
283
228
  }
284
229
 
285
230
  if (!output) {
@@ -400,17 +345,22 @@ async function convertWithMarkitdown(
400
345
  return { content: "", ok: false, error: "markitdown not found (uv/pip unavailable)" };
401
346
  }
402
347
 
403
- const { stdout, stderr, exitCode, aborted } = await captureCommandOutput(cmd, [filePath], signal);
404
-
405
- if (aborted) {
406
- throw new Error("Operation aborted");
348
+ const child = ptree.cspawn([cmd, filePath], { signal });
349
+ let stdout: string;
350
+ try {
351
+ stdout = await child.nothrow().text();
352
+ } catch (err) {
353
+ if (err instanceof ptree.Exception && err.aborted) {
354
+ throw new Error("Operation aborted");
355
+ }
356
+ throw err;
407
357
  }
408
358
 
409
- if (exitCode === 0 && stdout.length > 0) {
359
+ if (child.exitCode === 0 && stdout.length > 0) {
410
360
  return { content: stdout, ok: true };
411
361
  }
412
362
 
413
- return { content: "", ok: false, error: stderr.trim() || "Conversion failed" };
363
+ return { content: "", ok: false, error: child.peekStderr().trim() || "Conversion failed" };
414
364
  }
415
365
 
416
366
  const readSchema = Type.Object({
@@ -605,9 +555,20 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
605
555
  const startLine = offset ? Math.max(0, offset - 1) : 0;
606
556
  const startLineDisplay = startLine + 1; // For display (1-indexed)
607
557
 
608
- // Check if offset is out of bounds
558
+ // Check if offset is out of bounds - return graceful message instead of throwing
609
559
  if (startLine >= allLines.length) {
610
- throw new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);
560
+ const suggestion =
561
+ allLines.length === 0
562
+ ? "The file is empty."
563
+ : `Use offset=1 to read from the start, or offset=${allLines.length} to read the last line.`;
564
+ return {
565
+ content: [
566
+ {
567
+ type: "text",
568
+ text: `Offset ${offset} is beyond end of file (${allLines.length} lines total). ${suggestion}`,
569
+ },
570
+ ],
571
+ };
611
572
  }
612
573
 
613
574
  // If limit is specified by user, use it; otherwise we'll let truncateHead decide
@@ -324,11 +324,6 @@ export class ToolUIKit {
324
324
  }
325
325
  }
326
326
 
327
- /** @deprecated Use `new ToolUIKit(theme)` instead */
328
- export function createToolUIKit(theme: Theme): ToolUIKit {
329
- return new ToolUIKit(theme);
330
- }
331
-
332
327
  interface ParsedDiagnostic {
333
328
  filePath: string;
334
329
  line: number;
@@ -13,7 +13,7 @@ import type { SSHHostInfo } from "../ssh/connection-manager";
13
13
  import { ensureHostInfo, getHostInfoForHost } from "../ssh/connection-manager";
14
14
  import { executeSSH } from "../ssh/ssh-executor";
15
15
  import type { ToolSession } from "./index";
16
- import { createToolUIKit } from "./render-utils";
16
+ import { ToolUIKit } from "./render-utils";
17
17
  import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateTail } from "./truncate";
18
18
 
19
19
  const sshSchema = Type.Object({
@@ -245,7 +245,7 @@ interface SshRenderContext {
245
245
 
246
246
  export const sshToolRenderer = {
247
247
  renderCall(args: SshRenderArgs, uiTheme: Theme): Component {
248
- const ui = createToolUIKit(uiTheme);
248
+ const ui = new ToolUIKit(uiTheme);
249
249
  const host = args.host || uiTheme.format.ellipsis;
250
250
  const command = args.command || uiTheme.format.ellipsis;
251
251
  const text = ui.title(`[${host}] $ ${command}`);
@@ -260,7 +260,7 @@ export const sshToolRenderer = {
260
260
  options: RenderResultOptions & { renderContext?: SshRenderContext },
261
261
  uiTheme: Theme,
262
262
  ): Component {
263
- const ui = createToolUIKit(uiTheme);
263
+ const ui = new ToolUIKit(uiTheme);
264
264
  const { expanded, renderContext } = options;
265
265
  const details = result.details;
266
266
  const lines: string[] = [];
@@ -11,6 +11,7 @@
11
11
  * - "omp/slow" or "pi/slow" → configured slow model from settings
12
12
  */
13
13
 
14
+ import { $ } from "bun";
14
15
  import { type Settings as SettingsFile, settingsCapability } from "../../../capability/settings";
15
16
  import { loadCapability } from "../../../discovery";
16
17
  import type { Settings as SettingsData } from "../../settings-manager";
@@ -29,7 +30,7 @@ const CACHE_TTL_MS = 5 * 60 * 1000;
29
30
  * Returns models in "provider/modelId" format.
30
31
  * Caches the result for performance.
31
32
  */
32
- export function getAvailableModels(): string[] {
33
+ export async function getAvailableModels(): Promise<string[]> {
33
34
  const now = Date.now();
34
35
  if (cachedModels !== null && now < cacheExpiry) {
35
36
  return cachedModels;
@@ -37,20 +38,17 @@ export function getAvailableModels(): string[] {
37
38
 
38
39
  try {
39
40
  const ompCommand = resolveOmpCommand();
40
- const result = Bun.spawnSync([ompCommand.cmd, ...ompCommand.args, "--list-models"], {
41
- stdin: "ignore",
42
- stdout: "pipe",
43
- stderr: "pipe",
44
- });
41
+ const result = await $`${ompCommand.cmd} ${ompCommand.args} --list-models`.quiet().nothrow();
42
+ const stdout = result.stdout?.toString() ?? "";
45
43
 
46
- if (result.exitCode !== 0 || !result.stdout) {
44
+ if (result.exitCode !== 0 || !stdout.trim()) {
47
45
  cachedModels = [];
48
46
  cacheExpiry = now + CACHE_TTL_MS;
49
47
  return cachedModels;
50
48
  }
51
49
 
52
50
  // Parse output: skip header line, extract provider/model
53
- const lines = result.stdout.toString().trim().split("\n");
51
+ const lines = stdout.trim().split("\n");
54
52
  cachedModels = lines
55
53
  .slice(1) // Skip header
56
54
  .map((line) => {
@@ -151,7 +149,7 @@ export async function resolveModelPattern(
151
149
  return undefined;
152
150
  }
153
151
 
154
- const models = availableModels ?? getAvailableModels();
152
+ const models = availableModels ?? (await getAvailableModels());
155
153
  if (models.length === 0) {
156
154
  // Fallback: return pattern as-is if we can't get available models
157
155
  return pattern;
@@ -15,19 +15,18 @@
15
15
 
16
16
  import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
17
17
  import type { Api, Model } from "@oh-my-pi/pi-ai";
18
+ import { logger, untilAborted } from "@oh-my-pi/pi-utils";
18
19
  import type { TSchema } from "@sinclair/typebox";
19
20
  import lspDescription from "../../../prompts/tools/lsp.md" with { type: "text" };
20
21
  import type { AgentSessionEvent } from "../../agent-session";
21
22
  import { AuthStorage } from "../../auth-storage";
22
23
  import type { CustomTool } from "../../custom-tools/types";
23
- import { logger } from "../../logger";
24
24
  import { ModelRegistry } from "../../model-registry";
25
25
  import { parseModelPattern, parseModelString } from "../../model-resolver";
26
26
  import { renderPromptTemplate } from "../../prompt-templates";
27
27
  import { createAgentSession, discoverAuthStorage, discoverModels } from "../../sdk";
28
28
  import { SessionManager } from "../../session-manager";
29
29
  import { SettingsManager } from "../../settings-manager";
30
- import { untilAborted } from "../../utils";
31
30
  import { type LspToolDetails, lspSchema } from "../lsp/types";
32
31
  import { getPythonToolDescription, type PythonToolDetails, type PythonToolParams, pythonSchema } from "../python";
33
32
  import type {
@@ -94,54 +93,58 @@ function callMCPToolViaParent(
94
93
  signal?: AbortSignal,
95
94
  timeoutMs = MCP_CALL_TIMEOUT_MS,
96
95
  ): Promise<{ content: Array<{ type: string; text?: string; [key: string]: unknown }>; isError?: boolean }> {
97
- return new Promise((resolve, reject) => {
98
- const callId = generateMCPCallId();
99
- if (signal?.aborted) {
100
- reject(new Error("Aborted"));
101
- return;
102
- }
96
+ const { promise, resolve, reject } = Promise.withResolvers<{
97
+ content: Array<{ type: string; text?: string; [key: string]: unknown }>;
98
+ isError?: boolean;
99
+ }>();
100
+ const callId = generateMCPCallId();
101
+ if (signal?.aborted) {
102
+ reject(new Error("Aborted"));
103
+ return promise;
104
+ }
103
105
 
104
- const timeoutId = setTimeout(() => {
105
- pendingMCPCalls.delete(callId);
106
- reject(new Error(`MCP call timed out after ${timeoutMs}ms`));
107
- }, timeoutMs);
106
+ const timeoutId = setTimeout(() => {
107
+ pendingMCPCalls.delete(callId);
108
+ reject(new Error(`MCP call timed out after ${timeoutMs}ms`));
109
+ }, timeoutMs);
108
110
 
109
- const cleanup = () => {
110
- clearTimeout(timeoutId);
111
- pendingMCPCalls.delete(callId);
112
- };
113
-
114
- if (typeof signal?.addEventListener === "function") {
115
- signal.addEventListener(
116
- "abort",
117
- () => {
118
- cleanup();
119
- reject(new Error("Aborted"));
120
- },
121
- { once: true },
122
- );
123
- }
111
+ const cleanup = () => {
112
+ clearTimeout(timeoutId);
113
+ pendingMCPCalls.delete(callId);
114
+ };
124
115
 
125
- pendingMCPCalls.set(callId, {
126
- resolve: (result) => {
127
- cleanup();
128
- resolve(result ?? { content: [] });
129
- },
130
- reject: (error) => {
116
+ if (typeof signal?.addEventListener === "function") {
117
+ signal.addEventListener(
118
+ "abort",
119
+ () => {
131
120
  cleanup();
132
- reject(error);
121
+ reject(new Error("Aborted"));
133
122
  },
134
- timeoutId,
135
- });
123
+ { once: true },
124
+ );
125
+ }
136
126
 
137
- postMessageSafe({
138
- type: "mcp_tool_call",
139
- callId,
140
- toolName,
141
- params,
142
- timeoutMs,
143
- } as SubagentWorkerResponse);
127
+ pendingMCPCalls.set(callId, {
128
+ resolve: (result) => {
129
+ cleanup();
130
+ resolve(result ?? { content: [] });
131
+ },
132
+ reject: (error) => {
133
+ cleanup();
134
+ reject(error);
135
+ },
136
+ timeoutId,
144
137
  });
138
+
139
+ postMessageSafe({
140
+ type: "mcp_tool_call",
141
+ callId,
142
+ toolName,
143
+ params,
144
+ timeoutMs,
145
+ } as SubagentWorkerResponse);
146
+
147
+ return promise;
145
148
  }
146
149
 
147
150
  function callPythonToolViaParent(
@@ -149,64 +152,65 @@ function callPythonToolViaParent(
149
152
  signal?: AbortSignal,
150
153
  timeoutMs?: number,
151
154
  ): Promise<PythonToolCallResponse["result"]> {
152
- return new Promise((resolve, reject) => {
153
- const callId = generatePythonCallId();
154
- if (signal?.aborted) {
155
- reject(new Error("Aborted"));
156
- return;
157
- }
155
+ const { promise, resolve, reject } = Promise.withResolvers<PythonToolCallResponse["result"]>();
156
+ const callId = generatePythonCallId();
157
+ if (signal?.aborted) {
158
+ reject(new Error("Aborted"));
159
+ return promise;
160
+ }
158
161
 
159
- const sendCancel = (reason: string) => {
160
- postMessageSafe({ type: "python_tool_cancel", callId, reason } as SubagentWorkerResponse);
161
- };
162
-
163
- const timeoutId =
164
- typeof timeoutMs === "number" && Number.isFinite(timeoutMs)
165
- ? setTimeout(() => {
166
- pendingPythonCalls.delete(callId);
167
- sendCancel(`Python call timed out after ${timeoutMs}ms`);
168
- reject(new Error(`Python call timed out after ${timeoutMs}ms`));
169
- }, timeoutMs)
170
- : undefined;
171
-
172
- const cleanup = () => {
173
- if (timeoutId) {
174
- clearTimeout(timeoutId);
175
- }
176
- pendingPythonCalls.delete(callId);
177
- };
178
-
179
- if (typeof signal?.addEventListener === "function") {
180
- signal.addEventListener(
181
- "abort",
182
- () => {
183
- cleanup();
184
- sendCancel("Aborted");
185
- reject(new Error("Aborted"));
186
- },
187
- { once: true },
188
- );
162
+ const sendCancel = (reason: string) => {
163
+ postMessageSafe({ type: "python_tool_cancel", callId, reason } as SubagentWorkerResponse);
164
+ };
165
+
166
+ const timeoutId =
167
+ typeof timeoutMs === "number" && Number.isFinite(timeoutMs)
168
+ ? setTimeout(() => {
169
+ pendingPythonCalls.delete(callId);
170
+ sendCancel(`Python call timed out after ${timeoutMs}ms`);
171
+ reject(new Error(`Python call timed out after ${timeoutMs}ms`));
172
+ }, timeoutMs)
173
+ : undefined;
174
+
175
+ const cleanup = () => {
176
+ if (timeoutId) {
177
+ clearTimeout(timeoutId);
189
178
  }
179
+ pendingPythonCalls.delete(callId);
180
+ };
190
181
 
191
- pendingPythonCalls.set(callId, {
192
- resolve: (result) => {
193
- cleanup();
194
- resolve(result ?? { content: [] });
195
- },
196
- reject: (error) => {
182
+ if (typeof signal?.addEventListener === "function") {
183
+ signal.addEventListener(
184
+ "abort",
185
+ () => {
197
186
  cleanup();
198
- reject(error);
187
+ sendCancel("Aborted");
188
+ reject(new Error("Aborted"));
199
189
  },
200
- timeoutId,
201
- });
190
+ { once: true },
191
+ );
192
+ }
202
193
 
203
- postMessageSafe({
204
- type: "python_tool_call",
205
- callId,
206
- params,
207
- timeoutMs,
208
- } as SubagentWorkerResponse);
194
+ pendingPythonCalls.set(callId, {
195
+ resolve: (result) => {
196
+ cleanup();
197
+ resolve(result ?? { content: [] });
198
+ },
199
+ reject: (error) => {
200
+ cleanup();
201
+ reject(error);
202
+ },
203
+ timeoutId,
209
204
  });
205
+
206
+ postMessageSafe({
207
+ type: "python_tool_call",
208
+ callId,
209
+ params,
210
+ timeoutMs,
211
+ } as SubagentWorkerResponse);
212
+
213
+ return promise;
210
214
  }
211
215
 
212
216
  function callLspToolViaParent(
@@ -214,58 +218,59 @@ function callLspToolViaParent(
214
218
  signal?: AbortSignal,
215
219
  timeoutMs?: number,
216
220
  ): Promise<LspToolCallResponse["result"]> {
217
- return new Promise((resolve, reject) => {
218
- const callId = generateLspCallId();
219
- if (signal?.aborted) {
220
- reject(new Error("Aborted"));
221
- return;
222
- }
221
+ const { promise, resolve, reject } = Promise.withResolvers<LspToolCallResponse["result"]>();
222
+ const callId = generateLspCallId();
223
+ if (signal?.aborted) {
224
+ reject(new Error("Aborted"));
225
+ return promise;
226
+ }
223
227
 
224
- const timeoutId =
225
- typeof timeoutMs === "number" && Number.isFinite(timeoutMs)
226
- ? setTimeout(() => {
227
- pendingLspCalls.delete(callId);
228
- reject(new Error(`LSP call timed out after ${timeoutMs}ms`));
229
- }, timeoutMs)
230
- : undefined;
231
-
232
- const cleanup = () => {
233
- if (timeoutId) {
234
- clearTimeout(timeoutId);
235
- }
236
- pendingLspCalls.delete(callId);
237
- };
238
-
239
- if (typeof signal?.addEventListener === "function") {
240
- signal.addEventListener(
241
- "abort",
242
- () => {
243
- cleanup();
244
- reject(new Error("Aborted"));
245
- },
246
- { once: true },
247
- );
228
+ const timeoutId =
229
+ typeof timeoutMs === "number" && Number.isFinite(timeoutMs)
230
+ ? setTimeout(() => {
231
+ pendingLspCalls.delete(callId);
232
+ reject(new Error(`LSP call timed out after ${timeoutMs}ms`));
233
+ }, timeoutMs)
234
+ : undefined;
235
+
236
+ const cleanup = () => {
237
+ if (timeoutId) {
238
+ clearTimeout(timeoutId);
248
239
  }
240
+ pendingLspCalls.delete(callId);
241
+ };
249
242
 
250
- pendingLspCalls.set(callId, {
251
- resolve: (result) => {
252
- cleanup();
253
- resolve(result ?? { content: [] });
254
- },
255
- reject: (error) => {
243
+ if (typeof signal?.addEventListener === "function") {
244
+ signal.addEventListener(
245
+ "abort",
246
+ () => {
256
247
  cleanup();
257
- reject(error);
248
+ reject(new Error("Aborted"));
258
249
  },
259
- timeoutId,
260
- });
250
+ { once: true },
251
+ );
252
+ }
261
253
 
262
- postMessageSafe({
263
- type: "lsp_tool_call",
264
- callId,
265
- params,
266
- timeoutMs,
267
- } as SubagentWorkerResponse);
254
+ pendingLspCalls.set(callId, {
255
+ resolve: (result) => {
256
+ cleanup();
257
+ resolve(result ?? { content: [] });
258
+ },
259
+ reject: (error) => {
260
+ cleanup();
261
+ reject(error);
262
+ },
263
+ timeoutId,
268
264
  });
265
+
266
+ postMessageSafe({
267
+ type: "lsp_tool_call",
268
+ callId,
269
+ params,
270
+ timeoutMs,
271
+ } as SubagentWorkerResponse);
272
+
273
+ return promise;
269
274
  }
270
275
 
271
276
  function handleMCPToolResult(response: MCPToolCallResponse): void {
@@ -5,12 +5,12 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
5
5
  import { StringEnum } from "@oh-my-pi/pi-ai";
6
6
  import type { Component } from "@oh-my-pi/pi-tui";
7
7
  import { Text } from "@oh-my-pi/pi-tui";
8
+ import { logger } from "@oh-my-pi/pi-utils";
8
9
  import { Type } from "@sinclair/typebox";
9
10
  import chalk from "chalk";
10
11
  import type { Theme } from "../../modes/interactive/theme/theme";
11
12
  import todoWriteDescription from "../../prompts/tools/todo-write.md" with { type: "text" };
12
13
  import type { RenderResultOptions } from "../custom-tools/types";
13
- import { logger } from "../logger";
14
14
  import { renderPromptTemplate } from "../prompt-templates";
15
15
  import type { ToolSession } from "../sdk";
16
16
  import { ensureArtifactsDir, getArtifactsDir } from "./task/artifacts";
@@ -11,7 +11,7 @@
11
11
 
12
12
  export const DEFAULT_MAX_LINES = 2000;
13
13
  export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB
14
- export const GREP_MAX_LINE_LENGTH = 500; // Max chars per grep match line
14
+ export const DEFAULT_MAX_COLUMN = 1024; // Max chars per grep match line
15
15
 
16
16
  export interface TruncationResult {
17
17
  /** The truncated content */
@@ -282,7 +282,7 @@ export function truncateStringToBytesFromStart(str: string, maxBytes: number): {
282
282
  */
283
283
  export function truncateLine(
284
284
  line: string,
285
- maxChars: number = GREP_MAX_LINE_LENGTH,
285
+ maxChars: number = DEFAULT_MAX_COLUMN,
286
286
  ): { text: string; wasTruncated: boolean } {
287
287
  if (line.length <= maxChars) {
288
288
  return { text: line, wasTruncated: false };