@oh-my-pi/pi-coding-agent 15.5.1 → 15.5.3

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/bash.ts CHANGED
@@ -42,11 +42,11 @@ const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
42
42
  const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000;
43
43
 
44
44
  /**
45
- * Bash patterns that force an approval prompt even in yolo mode.
45
+ * Bash patterns flagged as safety critical for approval policy.
46
46
  *
47
- * Kept intentionally tight — the cost of a false positive is one extra prompt;
48
- * the cost of a false negative is data loss or a compromised host. New patterns
49
- * should target shapes that are virtually never legitimate in automation.
47
+ * Kept intentionally tight — the cost of a false negative is data loss or a compromised host,
48
+ * while false positives remain actionable through user policy control.
49
+ * New patterns should target shapes that are virtually never legitimate in automation.
50
50
  */
51
51
  export const CRITICAL_BASH_PATTERNS = [
52
52
  // Recursive destruction.
@@ -128,6 +128,7 @@ export interface BashToolDetails {
128
128
  meta?: OutputMeta;
129
129
  timeoutSeconds?: number;
130
130
  requestedTimeoutSeconds?: number;
131
+ wallTimeMs?: number;
131
132
  terminalId?: string;
132
133
  async?: {
133
134
  state: "running" | "completed" | "failed";
@@ -272,6 +273,34 @@ function formatTimeoutClampNotice(requestedTimeoutSec: number, effectiveTimeoutS
272
273
  : undefined;
273
274
  }
274
275
 
276
+ function formatWallTimeSeconds(wallTimeMs: number): string {
277
+ return (wallTimeMs / 1000).toFixed(2);
278
+ }
279
+
280
+ function formatWallTimeNotice(wallTimeMs: number): string {
281
+ return `Wall time: ${formatWallTimeSeconds(wallTimeMs)} seconds`;
282
+ }
283
+
284
+ /**
285
+ * Strip the trailing `Wall time: <secs> seconds` notice from text so the TUI
286
+ * can render the wall time via its styled `[Wall: …]` label without echoing
287
+ * the same value verbatim in the output pane.
288
+ */
289
+ function stripWallTimeNotice(text: string, wallTimeMs: number | undefined): string {
290
+ if (wallTimeMs === undefined) return text;
291
+ // Reconstruct the notice from the same value the result was tagged with so
292
+ // a literal sub-string match never strips a coincidental in-output token —
293
+ // only the exact line we appended in #buildCompletedResult.
294
+ const notice = formatWallTimeNotice(wallTimeMs);
295
+ const idx = text.lastIndexOf(notice);
296
+ if (idx === -1) return text;
297
+ let start = idx;
298
+ let end = idx + notice.length;
299
+ if (text[start - 1] === "\n") start -= 1;
300
+ if (text[end] === "\n") end += 1;
301
+ return (text.slice(0, start) + text.slice(end)).trimEnd();
302
+ }
303
+
275
304
  /**
276
305
  * Bash tool implementation.
277
306
  *
@@ -347,10 +376,23 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
347
376
  #buildCompletedResult(
348
377
  result: BashResult | BashInteractiveResult,
349
378
  timeoutSec: number,
350
- options: { requestedTimeoutSec?: number; notices?: readonly string[]; terminalId?: string } = {},
379
+ options: {
380
+ requestedTimeoutSec?: number;
381
+ notices?: readonly string[];
382
+ terminalId?: string;
383
+ wallTimeMs?: number;
384
+ } = {},
351
385
  ): AgentToolResult<BashToolDetails> {
352
386
  const outputLines = [this.#formatResultOutput(result)];
353
- const notices = options.notices?.filter(Boolean) ?? [];
387
+ const notices: string[] = [];
388
+ if (options.wallTimeMs !== undefined) {
389
+ notices.push(formatWallTimeNotice(options.wallTimeMs));
390
+ }
391
+ if (options.notices) {
392
+ for (const notice of options.notices) {
393
+ if (notice) notices.push(notice);
394
+ }
395
+ }
354
396
  if (notices.length > 0) outputLines.push("", ...notices);
355
397
  const outputText = outputLines.join("\n");
356
398
  const details: BashToolDetails = { timeoutSeconds: timeoutSec };
@@ -360,6 +402,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
360
402
  if (options.terminalId !== undefined) {
361
403
  details.terminalId = options.terminalId;
362
404
  }
405
+ if (options.wallTimeMs !== undefined) {
406
+ details.wallTimeMs = options.wallTimeMs;
407
+ }
363
408
  const resultBuilder = toolResult(details).text(outputText).truncationFromSummary(result, { direction: "tail" });
364
409
  this.#buildResultText(result, timeoutSec, outputText);
365
410
  return resultBuilder.done();
@@ -430,6 +475,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
430
475
  async ({ jobId, signal: runSignal, reportProgress }) => {
431
476
  const { path: artifactPath, id: artifactId } = (await this.session.allocateOutputArtifact?.("bash")) ?? {};
432
477
  const tailBuffer = new TailBuffer(DEFAULT_MAX_BYTES);
478
+ const wallTimeStart = performance.now();
433
479
  try {
434
480
  const result = await executeBash(options.command, {
435
481
  cwd: options.commandCwd,
@@ -446,9 +492,11 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
446
492
  },
447
493
  onMinimizedSave: originalText => saveBashOriginalArtifact(this.session, originalText),
448
494
  });
495
+ const wallTimeMs = performance.now() - wallTimeStart;
449
496
  const finalResult = this.#buildCompletedResult(result, options.timeoutSec, {
450
497
  requestedTimeoutSec: options.requestedTimeoutSec,
451
498
  notices: options.notices ?? [],
499
+ wallTimeMs,
452
500
  });
453
501
  const finalText = this.#extractTextResult(finalResult);
454
502
  latestText = finalText;
@@ -697,6 +745,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
697
745
  // Skip when pty=true (PTY needs the local terminal UI).
698
746
  const clientBridge = this.session.getClientBridge?.();
699
747
  if (clientBridge?.capabilities.terminal && clientBridge.createTerminal && !pty) {
748
+ const bridgeWallTimeStart = performance.now();
700
749
  const handle = await clientBridge.createTerminal({
701
750
  command,
702
751
  cwd: commandCwd,
@@ -792,6 +841,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
792
841
  requestedTimeoutSec,
793
842
  notices: pendingNotices,
794
843
  terminalId: handle.terminalId,
844
+ wallTimeMs: performance.now() - bridgeWallTimeStart,
795
845
  });
796
846
  }
797
847
 
@@ -852,6 +902,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
852
902
  requestedTimeoutSec,
853
903
  notices: bridgeNotices,
854
904
  terminalId: handle.terminalId,
905
+ wallTimeMs: performance.now() - bridgeWallTimeStart,
855
906
  });
856
907
  } finally {
857
908
  try {
@@ -869,6 +920,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
869
920
  const { path: artifactPath, id: artifactId } = (await this.session.allocateOutputArtifact?.("bash")) ?? {};
870
921
 
871
922
  const interactiveUi = canUseInteractiveBashPty(pty, ctx) ? ctx?.ui : undefined;
923
+ const wallTimeStart = performance.now();
872
924
  const result: BashResult | BashInteractiveResult = interactiveUi
873
925
  ? await runInteractiveBashPty(interactiveUi, {
874
926
  command,
@@ -890,6 +942,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
890
942
  onChunk: streamTailUpdates(tailBuffer, onUpdate),
891
943
  onMinimizedSave: originalText => saveBashOriginalArtifact(this.session, originalText),
892
944
  });
945
+ const wallTimeMs = performance.now() - wallTimeStart;
893
946
  if (result.cancelled) {
894
947
  if (signal?.aborted) {
895
948
  throw new ToolAbortError(normalizeResultOutput(result) || "Command aborted");
@@ -902,6 +955,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
902
955
  return this.#buildCompletedResult(result, timeoutSec, {
903
956
  requestedTimeoutSec,
904
957
  notices: pendingNotices,
958
+ wallTimeMs,
905
959
  });
906
960
  }
907
961
  }
@@ -1032,22 +1086,32 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1032
1086
  // Strip the LLM-facing notice appended by wrappedExecute so we don't
1033
1087
  // double-print it alongside the styled warning line below.
1034
1088
  const rawOutput = renderContext?.output ?? result.content?.find(c => c.type === "text")?.text ?? "";
1035
- const output = stripOutputNotice(rawOutput, details?.meta);
1089
+ const strippedOutput = stripOutputNotice(rawOutput, details?.meta);
1090
+ const output = stripWallTimeNotice(strippedOutput, details?.wallTimeMs);
1036
1091
  const displayOutput = output.trimEnd();
1037
1092
  const showingFullOutput = expanded && renderContext?.isFullOutput === true;
1038
1093
 
1039
1094
  // Build truncation warning
1040
1095
  const timeoutSeconds = details?.timeoutSeconds ?? renderContext?.timeout;
1041
1096
  const requestedTimeoutSeconds = details?.requestedTimeoutSeconds;
1042
- const timeoutLabel =
1043
- typeof timeoutSeconds === "number"
1044
- ? requestedTimeoutSeconds !== undefined && requestedTimeoutSeconds !== timeoutSeconds
1097
+ const wallTimeMs = details?.wallTimeMs;
1098
+ const statsParts: string[] = [];
1099
+ if (wallTimeMs !== undefined) {
1100
+ statsParts.push(`Wall: ${formatWallTimeSeconds(wallTimeMs)}s`);
1101
+ }
1102
+ if (typeof timeoutSeconds === "number") {
1103
+ statsParts.push(
1104
+ requestedTimeoutSeconds !== undefined && requestedTimeoutSeconds !== timeoutSeconds
1045
1105
  ? `Timeout: ${timeoutSeconds}s (requested ${requestedTimeoutSeconds}s clamped)`
1046
- : `Timeout: ${timeoutSeconds}s`
1047
- : undefined;
1106
+ : `Timeout: ${timeoutSeconds}s`,
1107
+ );
1108
+ }
1048
1109
  const timeoutLine =
1049
- timeoutLabel !== undefined
1050
- ? uiTheme.fg("dim", `${uiTheme.format.bracketLeft}${timeoutLabel}${uiTheme.format.bracketRight}`)
1110
+ statsParts.length > 0
1111
+ ? uiTheme.fg(
1112
+ "dim",
1113
+ `${uiTheme.format.bracketLeft}${statsParts.join(" | ")}${uiTheme.format.bracketRight}`,
1114
+ )
1051
1115
  : undefined;
1052
1116
  let warningLine: string | undefined;
1053
1117
  if (details?.meta?.truncation && !showingFullOutput) {