@oh-my-pi/pi-coding-agent 8.12.2 → 8.12.4

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.
package/src/tools/ask.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  * - Users will always be able to select "Other" to provide custom text input
13
13
  * - Use multi: true to allow multiple answers to be selected for a question
14
14
  * - Use recommended: <index> to mark the default option; "(Recommended)" suffix is added automatically
15
- * - Questions time out after 30 seconds and auto-select the recommended option
15
+ * - Questions may time out and auto-select the recommended option (configurable, disabled in plan mode)
16
16
  */
17
17
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
18
18
  import type { Component } from "@oh-my-pi/pi-tui";
@@ -23,6 +23,7 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
23
23
  import { type Theme, theme } from "../modes/theme/theme";
24
24
  import askDescription from "../prompts/tools/ask.md" with { type: "text" };
25
25
  import { renderStatusLine } from "../tui";
26
+ import { detectNotificationProtocol, isNotificationSuppressed, sendNotification } from "../utils/terminal-notify";
26
27
  import type { ToolSession } from ".";
27
28
  import { ToolUIKit } from "./render-utils";
28
29
 
@@ -77,7 +78,8 @@ export interface AskToolDetails {
77
78
 
78
79
  const OTHER_OPTION = "Other (type your own)";
79
80
  const RECOMMENDED_SUFFIX = " (Recommended)";
80
- const ASK_TIMEOUT_MS = 30000;
81
+ /** Default timeout in milliseconds (used when settings unavailable) */
82
+ const DEFAULT_ASK_TIMEOUT_MS = 30000;
81
83
 
82
84
  function getDoneOptionLabel(): string {
83
85
  return `${theme.status.success} Done selecting`;
@@ -119,13 +121,20 @@ interface UIContext {
119
121
  input(prompt: string): Promise<string | undefined>;
120
122
  }
121
123
 
124
+ interface AskQuestionOptions {
125
+ /** Timeout in milliseconds, null/undefined to disable */
126
+ timeout?: number | null;
127
+ }
128
+
122
129
  async function askSingleQuestion(
123
130
  ui: UIContext,
124
131
  question: string,
125
132
  optionLabels: string[],
126
133
  multi: boolean,
127
134
  recommended?: number,
135
+ options?: AskQuestionOptions,
128
136
  ): Promise<SelectionResult> {
137
+ const timeout = options?.timeout ?? undefined;
129
138
  const doneLabel = getDoneOptionLabel();
130
139
  let selectedOptions: string[] = [];
131
140
  let customInput: string | undefined;
@@ -152,11 +161,11 @@ async function askSingleQuestion(
152
161
  const selectionStart = Date.now();
153
162
  const choice = await ui.select(`${prefix}${question}`, opts, {
154
163
  initialIndex: cursorIndex,
155
- timeout: ASK_TIMEOUT_MS,
164
+ timeout: timeout ?? undefined,
156
165
  outline: true,
157
166
  });
158
167
  const elapsed = Date.now() - selectionStart;
159
- const timedOut = elapsed >= ASK_TIMEOUT_MS;
168
+ const timedOut = timeout != null && elapsed >= timeout;
160
169
 
161
170
  if (choice === undefined || choice === doneLabel) break;
162
171
 
@@ -198,7 +207,7 @@ async function askSingleQuestion(
198
207
  } else {
199
208
  const displayLabels = addRecommendedSuffix(optionLabels, recommended);
200
209
  const choice = await ui.select(question, [...displayLabels, OTHER_OPTION], {
201
- timeout: ASK_TIMEOUT_MS,
210
+ timeout: timeout ?? undefined,
202
211
  initialIndex: recommended,
203
212
  outline: true,
204
213
  });
@@ -254,8 +263,10 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
254
263
  public readonly label = "Ask";
255
264
  public readonly description: string;
256
265
  public readonly parameters = askSchema;
266
+ private readonly session: ToolSession;
257
267
 
258
- constructor(_session: ToolSession) {
268
+ constructor(session: ToolSession) {
269
+ this.session = session;
259
270
  this.description = renderPromptTemplate(askDescription);
260
271
  }
261
272
 
@@ -263,6 +274,17 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
263
274
  return session.hasUI ? new AskTool(session) : null;
264
275
  }
265
276
 
277
+ /** Send terminal notification when ask tool is waiting for input */
278
+ private sendAskNotification(): void {
279
+ if (isNotificationSuppressed()) return;
280
+
281
+ const method = this.session.settingsManager?.getAskNotification() ?? "auto";
282
+ if (method === "off") return;
283
+
284
+ const protocol = method === "auto" ? detectNotificationProtocol() : method;
285
+ sendNotification(protocol, "Waiting for input");
286
+ }
287
+
266
288
  public async execute(
267
289
  _toolCallId: string,
268
290
  params: AskParams,
@@ -280,6 +302,14 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
280
302
 
281
303
  const { ui } = context;
282
304
 
305
+ // Determine timeout based on settings and plan mode
306
+ const planModeEnabled = this.session.getPlanModeState?.()?.enabled ?? false;
307
+ const settingsTimeout = this.session.settingsManager?.getAskTimeout() ?? DEFAULT_ASK_TIMEOUT_MS;
308
+ const timeout = planModeEnabled ? null : settingsTimeout;
309
+
310
+ // Send notification if waiting and not suppressed
311
+ this.sendAskNotification();
312
+
283
313
  // Multi-part questions mode
284
314
  if (params.questions && params.questions.length > 0) {
285
315
  const results: QuestionResult[] = [];
@@ -292,6 +322,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
292
322
  optionLabels,
293
323
  q.multi ?? false,
294
324
  q.recommended,
325
+ { timeout },
295
326
  );
296
327
 
297
328
  results.push({
@@ -330,6 +361,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
330
361
  optionLabels,
331
362
  multi,
332
363
  params.recommended,
364
+ { timeout },
333
365
  );
334
366
 
335
367
  const details: AskToolDetails = {
@@ -75,65 +75,6 @@ const CONVERTIBLE_EXTENSIONS = new Set([
75
75
  // Utilities
76
76
  // =============================================================================
77
77
 
78
- /**
79
- * Execute a command and return stdout
80
- */
81
-
82
- type WritableLike = {
83
- write: (chunk: string | Uint8Array) => unknown;
84
- flush?: () => unknown;
85
- end?: () => unknown;
86
- };
87
-
88
- const textEncoder = new TextEncoder();
89
-
90
- async function writeStdin(handle: unknown, input: string | Buffer): Promise<void> {
91
- if (!handle || typeof handle === "number") return;
92
- if (typeof (handle as WritableStream<Uint8Array>).getWriter === "function") {
93
- const writer = (handle as WritableStream<Uint8Array>).getWriter();
94
- try {
95
- const chunk = typeof input === "string" ? textEncoder.encode(input) : new Uint8Array(input);
96
- await writer.write(chunk);
97
- } finally {
98
- await writer.close();
99
- }
100
- return;
101
- }
102
-
103
- const sink = handle as WritableLike;
104
- sink.write(input);
105
- if (sink.flush) sink.flush();
106
- if (sink.end) sink.end();
107
- }
108
-
109
- async function exec(
110
- cmd: string,
111
- args: string[],
112
- options?: { timeout?: number; input?: string | Buffer },
113
- ): Promise<{ stdout: string; stderr: string; ok: boolean }> {
114
- const proc = ptree.cspawn([cmd, ...args], {
115
- stdin: options?.input ? "pipe" : null,
116
- timeout: options?.timeout ? options.timeout * 1000 : undefined,
117
- });
118
-
119
- if (options?.input) {
120
- await writeStdin(proc.stdin, options.input);
121
- }
122
-
123
- const [stdout, stderr] = await Promise.all([proc.stdout.text(), proc.stderr.text()]);
124
- try {
125
- await proc.exited;
126
- } catch {
127
- // Handle non-zero exit or timeout
128
- }
129
-
130
- return {
131
- stdout,
132
- stderr,
133
- ok: proc.exitCode === 0,
134
- };
135
- }
136
-
137
78
  /**
138
79
  * Check if a command exists (cross-platform)
139
80
  */
@@ -456,13 +397,20 @@ async function renderHtmlToText(
456
397
 
457
398
  try {
458
399
  await Bun.write(tmpFile, html);
400
+ const execOptions = {
401
+ mode: "group" as const,
402
+ timeout: timeout * 1000,
403
+ allowNonZero: true,
404
+ allowAbort: true,
405
+ stderr: "full" as const,
406
+ };
459
407
 
460
408
  // Try lynx first (can't auto-install, system package)
461
409
  const lynx = hasCommand("lynx");
462
410
  if (lynx) {
463
411
  const normalizedPath = tmpFile.replace(/\\/g, "/");
464
412
  const fileUrl = normalizedPath.startsWith("/") ? `file://${normalizedPath}` : `file:///${normalizedPath}`;
465
- const result = await exec("lynx", ["-dump", "-nolist", "-width", "120", fileUrl], { timeout });
413
+ const result = await ptree.exec(["lynx", "-dump", "-nolist", "-width", "120", fileUrl], execOptions);
466
414
  if (result.ok) {
467
415
  return { content: result.stdout, ok: true, method: "lynx" };
468
416
  }
@@ -471,7 +419,7 @@ async function renderHtmlToText(
471
419
  // Fall back to html2text (auto-install via uv/pip)
472
420
  const html2text = await ensureTool("html2text", true);
473
421
  if (html2text) {
474
- const result = await exec(html2text, [tmpFile], { timeout });
422
+ const result = await ptree.exec([html2text, tmpFile], execOptions);
475
423
  if (result.ok) {
476
424
  return { content: result.stdout, ok: true, method: "html2text" };
477
425
  }
package/src/tools/find.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
4
+ import { find as wasmFind } from "@oh-my-pi/pi-natives";
4
5
  import type { Component } from "@oh-my-pi/pi-tui";
5
6
  import { Text } from "@oh-my-pi/pi-tui";
6
- import { globPaths, isEnoent, untilAborted } from "@oh-my-pi/pi-utils";
7
+ import { isEnoent, untilAborted } from "@oh-my-pi/pi-utils";
7
8
  import type { Static } from "@sinclair/typebox";
8
9
  import { Type } from "@sinclair/typebox";
9
10
  import { renderPromptTemplate } from "../config/prompt-templates";
@@ -169,22 +170,26 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
169
170
  }
170
171
 
171
172
  let lines: string[];
173
+ const timeoutSignal = AbortSignal.timeout(GLOB_TIMEOUT_MS);
174
+ const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
172
175
  try {
173
- lines = await globPaths(globPattern, {
174
- cwd: searchPath,
175
- gitignore: true,
176
- dot: includeHidden,
177
- signal,
178
- timeoutMs: GLOB_TIMEOUT_MS,
179
- });
176
+ const result = await untilAborted(combinedSignal, () =>
177
+ wasmFind({
178
+ pattern: globPattern,
179
+ path: searchPath,
180
+ fileType: "file",
181
+ hidden: includeHidden,
182
+ }),
183
+ );
184
+ lines = result.matches.map(match => match.path);
180
185
  } catch (error) {
181
186
  if (error instanceof Error && error.name === "AbortError") {
187
+ if (timeoutSignal.aborted && !signal?.aborted) {
188
+ const timeoutSeconds = Math.max(1, Math.round(GLOB_TIMEOUT_MS / 1000));
189
+ throw new ToolError(`find timed out after ${timeoutSeconds}s`);
190
+ }
182
191
  throw new ToolAbortError();
183
192
  }
184
- if (error instanceof Error && error.name === "TimeoutError") {
185
- const timeoutSeconds = Math.max(1, Math.round(GLOB_TIMEOUT_MS / 1000));
186
- throw new ToolError(`glob timed out after ${timeoutSeconds}s`);
187
- }
188
193
  throw error;
189
194
  }
190
195
 
@@ -197,13 +202,13 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
197
202
 
198
203
  for (const rawLine of lines) {
199
204
  throwIfAborted(signal);
200
- const line = rawLine.replace(/\r$/, "").trim();
205
+ const line = rawLine;
201
206
  if (!line) {
202
207
  continue;
203
208
  }
204
209
 
205
210
  const hadTrailingSlash = line.endsWith("/") || line.endsWith("\\");
206
- let relativePath = line.replace(/\\/g, "/");
211
+ let relativePath = line;
207
212
 
208
213
  let mtimeMs = 0;
209
214
  let isDirectory = false;