@oh-my-pi/pi-coding-agent 15.10.12 → 15.11.1

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 (158) hide show
  1. package/CHANGELOG.md +90 -4
  2. package/dist/cli.js +869 -825
  3. package/dist/types/async/index.d.ts +0 -1
  4. package/dist/types/capability/mcp.d.ts +1 -0
  5. package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
  6. package/dist/types/config/keybindings.d.ts +6 -1
  7. package/dist/types/config/settings-schema.d.ts +66 -34
  8. package/dist/types/export/html/template.generated.d.ts +1 -1
  9. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  10. package/dist/types/extensibility/shared-events.d.ts +2 -2
  11. package/dist/types/internal-urls/history-protocol.d.ts +14 -0
  12. package/dist/types/internal-urls/index.d.ts +1 -0
  13. package/dist/types/internal-urls/types.d.ts +1 -1
  14. package/dist/types/irc/bus.d.ts +66 -0
  15. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  16. package/dist/types/mcp/oauth-flow.d.ts +6 -1
  17. package/dist/types/mcp/transports/stdio.d.ts +1 -0
  18. package/dist/types/mcp/types.d.ts +2 -0
  19. package/dist/types/modes/components/agent-hub.d.ts +30 -0
  20. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  21. package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
  22. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  23. package/dist/types/modes/components/mcp-add-wizard.d.ts +2 -1
  24. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  25. package/dist/types/modes/components/status-line/types.d.ts +3 -0
  26. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  27. package/dist/types/modes/components/transcript-container.d.ts +3 -2
  28. package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
  29. package/dist/types/modes/components/welcome.d.ts +3 -9
  30. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  31. package/dist/types/modes/controllers/tool-args-reveal.d.ts +43 -0
  32. package/dist/types/modes/interactive-mode.d.ts +3 -2
  33. package/dist/types/modes/theme/theme.d.ts +3 -1
  34. package/dist/types/modes/types.d.ts +3 -2
  35. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  36. package/dist/types/registry/agent-lifecycle.d.ts +51 -0
  37. package/dist/types/registry/agent-registry.d.ts +16 -5
  38. package/dist/types/session/agent-session.d.ts +35 -30
  39. package/dist/types/session/messages.d.ts +2 -4
  40. package/dist/types/session/session-history-format.d.ts +12 -0
  41. package/dist/types/session/session-manager.d.ts +21 -3
  42. package/dist/types/session/streaming-output.d.ts +23 -0
  43. package/dist/types/task/executor.d.ts +11 -2
  44. package/dist/types/task/index.d.ts +11 -4
  45. package/dist/types/task/output-manager.d.ts +0 -7
  46. package/dist/types/task/repair-args.d.ts +8 -7
  47. package/dist/types/task/types.d.ts +55 -51
  48. package/dist/types/tools/browser/tab-worker.d.ts +3 -1
  49. package/dist/types/tools/find.d.ts +0 -11
  50. package/dist/types/tools/grouped-file-output.d.ts +0 -49
  51. package/dist/types/tools/index.d.ts +1 -3
  52. package/dist/types/tools/irc.d.ts +76 -38
  53. package/dist/types/tools/job.d.ts +7 -1
  54. package/dist/types/tools/render-utils.d.ts +22 -0
  55. package/examples/extensions/with-deps/package.json +1 -0
  56. package/package.json +11 -10
  57. package/scripts/bundle-dist.ts +28 -19
  58. package/src/async/index.ts +0 -1
  59. package/src/capability/mcp.ts +1 -0
  60. package/src/cli/gallery-cli.ts +6 -5
  61. package/src/cli/gallery-fixtures/agentic.ts +230 -115
  62. package/src/cli/gallery-fixtures/types.ts +5 -0
  63. package/src/cli.ts +20 -6
  64. package/src/commit/agentic/tools/analyze-file.ts +38 -19
  65. package/src/config/keybindings.ts +6 -1
  66. package/src/config/mcp-schema.json +4 -0
  67. package/src/config/settings-schema.ts +68 -41
  68. package/src/config/settings.ts +7 -0
  69. package/src/edit/renderer.ts +96 -46
  70. package/src/eval/__tests__/agent-bridge.test.ts +5 -3
  71. package/src/eval/agent-bridge.ts +3 -16
  72. package/src/eval/js/shared/prelude.txt +1 -1
  73. package/src/eval/py/prelude.py +5 -6
  74. package/src/export/html/template.generated.ts +1 -1
  75. package/src/export/html/template.js +44 -14
  76. package/src/extensibility/custom-tools/types.ts +2 -2
  77. package/src/extensibility/shared-events.ts +2 -2
  78. package/src/internal-urls/docs-index.generated.ts +9 -9
  79. package/src/internal-urls/history-protocol.ts +113 -0
  80. package/src/internal-urls/index.ts +1 -0
  81. package/src/internal-urls/router.ts +3 -1
  82. package/src/internal-urls/types.ts +1 -1
  83. package/src/irc/bus.ts +292 -0
  84. package/src/main.ts +8 -60
  85. package/src/mcp/manager.ts +3 -0
  86. package/src/mcp/oauth-discovery.ts +27 -2
  87. package/src/mcp/oauth-flow.ts +47 -1
  88. package/src/mcp/transports/stdio.ts +3 -0
  89. package/src/mcp/types.ts +2 -0
  90. package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
  91. package/src/modes/components/assistant-message.ts +15 -0
  92. package/src/modes/components/btw-panel.ts +5 -1
  93. package/src/modes/components/compaction-summary-message.ts +68 -32
  94. package/src/modes/components/custom-editor.ts +10 -0
  95. package/src/modes/components/mcp-add-wizard.ts +13 -0
  96. package/src/modes/components/settings-selector.ts +2 -0
  97. package/src/modes/components/status-line/component.ts +22 -12
  98. package/src/modes/components/status-line/types.ts +3 -0
  99. package/src/modes/components/tool-execution.ts +31 -1
  100. package/src/modes/components/transcript-container.ts +99 -18
  101. package/src/modes/components/tree-selector.ts +6 -1
  102. package/src/modes/components/ttsr-notification.ts +72 -30
  103. package/src/modes/components/welcome.ts +9 -33
  104. package/src/modes/controllers/event-controller.ts +93 -4
  105. package/src/modes/controllers/extension-ui-controller.ts +8 -8
  106. package/src/modes/controllers/input-controller.ts +18 -2
  107. package/src/modes/controllers/mcp-command-controller.ts +34 -2
  108. package/src/modes/controllers/selector-controller.ts +25 -17
  109. package/src/modes/controllers/tool-args-reveal.ts +174 -0
  110. package/src/modes/interactive-mode.ts +17 -15
  111. package/src/modes/theme/theme.ts +24 -5
  112. package/src/modes/types.ts +3 -5
  113. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  114. package/src/modes/utils/ui-helpers.ts +51 -49
  115. package/src/prompts/system/irc-incoming.md +3 -4
  116. package/src/prompts/system/orchestrate-notice.md +2 -2
  117. package/src/prompts/system/subagent-system-prompt.md +0 -5
  118. package/src/prompts/system/system-prompt.md +1 -0
  119. package/src/prompts/system/workflow-notice.md +2 -2
  120. package/src/prompts/tools/eval.md +3 -3
  121. package/src/prompts/tools/irc.md +29 -19
  122. package/src/prompts/tools/read.md +2 -2
  123. package/src/prompts/tools/task-summary.md +5 -16
  124. package/src/prompts/tools/task.md +43 -29
  125. package/src/registry/agent-lifecycle.ts +218 -0
  126. package/src/registry/agent-registry.ts +16 -5
  127. package/src/sdk.ts +29 -9
  128. package/src/session/agent-session.ts +268 -241
  129. package/src/session/messages.ts +11 -78
  130. package/src/session/session-history-format.ts +246 -0
  131. package/src/session/session-manager.ts +59 -5
  132. package/src/session/streaming-output.ts +60 -0
  133. package/src/task/executor.ts +855 -466
  134. package/src/task/index.ts +723 -794
  135. package/src/task/output-manager.ts +0 -11
  136. package/src/task/render.ts +142 -66
  137. package/src/task/repair-args.ts +21 -9
  138. package/src/task/types.ts +73 -66
  139. package/src/tools/ask.ts +4 -2
  140. package/src/tools/bash.ts +15 -5
  141. package/src/tools/browser/tab-worker.ts +26 -7
  142. package/src/tools/browser.ts +28 -1
  143. package/src/tools/find.ts +2 -27
  144. package/src/tools/grouped-file-output.ts +1 -118
  145. package/src/tools/index.ts +4 -12
  146. package/src/tools/irc.ts +596 -171
  147. package/src/tools/job.ts +41 -7
  148. package/src/tools/read.ts +57 -1
  149. package/src/tools/render-utils.ts +56 -0
  150. package/src/tools/renderers.ts +2 -0
  151. package/src/tools/resolve.ts +4 -1
  152. package/src/tools/write.ts +65 -47
  153. package/src/web/search/providers/anthropic.ts +29 -4
  154. package/dist/types/async/support.d.ts +0 -2
  155. package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
  156. package/dist/types/task/simple-mode.d.ts +0 -8
  157. package/src/async/support.ts +0 -5
  158. package/src/task/simple-mode.ts +0 -27
package/src/tools/bash.ts CHANGED
@@ -17,7 +17,7 @@ import { truncateToVisualLines } from "../modes/components/visual-truncate";
17
17
  import { highlightCode, type Theme } from "../modes/theme/theme";
18
18
  import bashDescription from "../prompts/tools/bash.md" with { type: "text" };
19
19
  import type { ClientBridgeTerminalExitStatus, ClientBridgeTerminalOutput } from "../session/client-bridge";
20
- import { DEFAULT_MAX_BYTES, streamTailUpdates, TailBuffer } from "../session/streaming-output";
20
+ import { DEFAULT_MAX_BYTES, enforceInlineByteCap, streamTailUpdates, TailBuffer } from "../session/streaming-output";
21
21
  import { renderStatusLine } from "../tui";
22
22
  import { CachedOutputBlock, markFramedBlockComponent } from "../tui/output-block";
23
23
  import { getSixelLineMask } from "../utils/sixel";
@@ -429,7 +429,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
429
429
  }
430
430
  }
431
431
 
432
- #buildCompletedResult(
432
+ async #buildCompletedResult(
433
433
  result: BashResult | BashInteractiveResult,
434
434
  timeoutSec: number,
435
435
  options: {
@@ -438,7 +438,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
438
438
  terminalId?: string;
439
439
  wallTimeMs?: number;
440
440
  } = {},
441
- ): AgentToolResult<BashToolDetails> {
441
+ ): Promise<AgentToolResult<BashToolDetails>> {
442
442
  const exitCode = result.exitCode;
443
443
  const failedExit = exitCode !== undefined && exitCode !== 0;
444
444
 
@@ -472,7 +472,17 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
472
472
  if (failedExit) {
473
473
  details.exitCode = exitCode;
474
474
  }
475
- const resultBuilder = toolResult(details).text(outputText).truncationFromSummary(result, { direction: "tail" });
475
+ // Final defense at the tool-result boundary: no bash path (client bridge,
476
+ // head-retention spill, minimizer miss) may emit more than
477
+ // ~DEFAULT_MAX_BYTES inline. No-op for already-bounded output.
478
+ const cappedOutputText = await enforceInlineByteCap(outputText, {
479
+ label: "bash output",
480
+ saveArtifact: full => saveBashOriginalArtifact(this.session, full),
481
+ });
482
+
483
+ const resultBuilder = toolResult(details)
484
+ .text(cappedOutputText)
485
+ .truncationFromSummary(result, { direction: "tail" });
476
486
  if (failedExit) resultBuilder.error();
477
487
  return resultBuilder.done();
478
488
  }
@@ -560,7 +570,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
560
570
  onMinimizedSave: originalText => saveBashOriginalArtifact(this.session, originalText),
561
571
  });
562
572
  const wallTimeMs = performance.now() - wallTimeStart;
563
- const finalResult = this.#buildCompletedResult(result, options.timeoutSec, {
573
+ const finalResult = await this.#buildCompletedResult(result, options.timeoutSec, {
564
574
  requestedTimeoutSec: options.requestedTimeoutSec,
565
575
  notices: options.notices ?? [],
566
576
  wallTimeMs,
@@ -10,6 +10,7 @@ import type {
10
10
  ElementHandle,
11
11
  ElementScreenshotOptions,
12
12
  HTTPResponse,
13
+ ImageFormat,
13
14
  KeyInput,
14
15
  Page,
15
16
  SerializedAXNode,
@@ -436,6 +437,19 @@ export function describeScreenshot(opts?: ScreenshotOptions): string {
436
437
  return "tab.screenshot()";
437
438
  }
438
439
 
440
+ /** Map an explicit save path's extension to a puppeteer capture format (default png). */
441
+ export function imageFormatForPath(filePath: string): ImageFormat {
442
+ switch (path.extname(filePath).toLowerCase()) {
443
+ case ".webp":
444
+ return "webp";
445
+ case ".jpg":
446
+ case ".jpeg":
447
+ return "jpeg";
448
+ default:
449
+ return "png";
450
+ }
451
+ }
452
+
439
453
  /** Summarize still-running helpers (oldest first) so a cell timeout names what stalled. */
440
454
  export function describeInflight(inflight: Map<number, InflightOp>): string {
441
455
  const now = Date.now();
@@ -931,6 +945,12 @@ export class WorkerCore {
931
945
  ): Promise<ScreenshotResult> {
932
946
  const page = this.#requirePage();
933
947
  const fullPage = opts.selector ? false : (opts.fullPage ?? false);
948
+ // An explicit save path picks the full-res capture format: puppeteer encodes
949
+ // png/jpeg/webp natively, so `save: "shot.webp"` gets real WebP bytes instead
950
+ // of PNG bytes hiding behind a .webp name. Unknown/missing extensions stay PNG.
951
+ const explicitPath = opts.save ? resolveToCwd(opts.save, session.cwd) : undefined;
952
+ const captureType = explicitPath ? imageFormatForPath(explicitPath) : "png";
953
+ const captureMime = `image/${captureType}` as const;
934
954
  let buffer: Buffer;
935
955
  if (opts.selector) {
936
956
  const handle = (await untilAborted(signal, () =>
@@ -951,24 +971,23 @@ export class WorkerCore {
951
971
  ).catch(() => undefined);
952
972
  // scrollIntoView:false skips the same IntersectionObserver check inside screenshot();
953
973
  // captureBeyondViewport (puppeteer's default) still renders the clipped region.
954
- const shotOpts: ElementScreenshotOptions = { type: "png", scrollIntoView: false };
974
+ const shotOpts: ElementScreenshotOptions = { type: captureType, scrollIntoView: false };
955
975
  buffer = (await untilAborted(signal, () => handle.screenshot(shotOpts))) as Buffer;
956
976
  } finally {
957
977
  await handle.dispose().catch(() => undefined);
958
978
  }
959
979
  } else {
960
- buffer = (await untilAborted(signal, () => page.screenshot({ type: "png", fullPage }))) as Buffer;
980
+ buffer = (await untilAborted(signal, () => page.screenshot({ type: captureType, fullPage }))) as Buffer;
961
981
  }
962
982
  const resized = await resizeImage(
963
- { type: "image", data: buffer.toBase64(), mimeType: "image/png" },
983
+ { type: "image", data: buffer.toBase64(), mimeType: captureMime },
964
984
  { maxWidth: 1024, maxHeight: 1024, maxBytes: 150 * 1024, jpegQuality: 70 },
965
985
  );
966
- const explicitPath = opts.save ? resolveToCwd(opts.save, session.cwd) : undefined;
967
986
  const saveFullRes = !!(explicitPath || session.browserScreenshotDir);
968
987
  const savedBuffer = saveFullRes ? buffer : resized.buffer;
969
- const savedMimeType = saveFullRes ? "image/png" : resized.mimeType;
970
- // Auto-generated names must match the bytes we actually write: full-res is always
971
- // PNG, but the resized buffer is whichever of PNG/JPEG/WebP encoded smallest.
988
+ const savedMimeType = saveFullRes ? captureMime : resized.mimeType;
989
+ // Names must match the bytes we actually write: full-res follows the capture
990
+ // format, the resized buffer is whichever of PNG/JPEG/WebP encoded smallest.
972
991
  const ext = savedMimeType === "image/webp" ? "webp" : savedMimeType === "image/jpeg" ? "jpg" : "png";
973
992
  const dest =
974
993
  explicitPath ??
@@ -3,6 +3,7 @@ import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
3
3
  import * as z from "zod/v4";
4
4
  import browserDescription from "../prompts/tools/browser.md" with { type: "text" };
5
5
  import type { ToolSession } from "../sdk";
6
+ import { enforceInlineByteCap } from "../session/streaming-output";
6
7
  import { truncateForPrompt } from "./approval";
7
8
  import { acquireBrowser, type BrowserHandle, type BrowserKind, type BrowserKindTag } from "./browser/registry";
8
9
  import type { Observation, ScreenshotResult } from "./browser/tab-protocol";
@@ -271,11 +272,37 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
271
272
  .filter((c): c is { type: "text"; text: string } => c.type === "text")
272
273
  .map(c => c.text)
273
274
  .join("\n");
274
- details.result = textOnly;
275
+ // Final defense at the tool-result boundary: a single run can display
276
+ // tens of KB (large JSON returns, dumped observations). Cap the combined
277
+ // text inline; the full text stays recoverable via the artifact footer
278
+ // when allocation succeeds.
279
+ const cappedText = await enforceInlineByteCap(textOnly, {
280
+ label: "browser output",
281
+ saveArtifact: full => saveBrowserOutputArtifact(this.session, full),
282
+ });
283
+ details.result = cappedText;
284
+ if (cappedText !== textOnly) {
285
+ const nonText = content.filter(c => c.type !== "text");
286
+ return toolResult(details)
287
+ .content([...nonText, { type: "text", text: cappedText }])
288
+ .done();
289
+ }
275
290
  return toolResult(details).content(content).done();
276
291
  }
277
292
  }
278
293
 
294
+ /** Persist over-cap browser run output as a session artifact; mirrors the bash minimizer's save path. */
295
+ async function saveBrowserOutputArtifact(session: ToolSession, fullText: string): Promise<string | undefined> {
296
+ try {
297
+ const alloc = await session.allocateOutputArtifact?.("browser-original");
298
+ if (!alloc?.path || !alloc.id) return undefined;
299
+ await Bun.write(alloc.path, fullText);
300
+ return alloc.id;
301
+ } catch {
302
+ return undefined;
303
+ }
304
+ }
305
+
279
306
  function describeBrowser(handle: BrowserHandle): string {
280
307
  switch (handle.kind.kind) {
281
308
  case "headless":
package/src/tools/find.ts CHANGED
@@ -4,7 +4,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
4
4
  import * as natives from "@oh-my-pi/pi-natives";
5
5
  import type { Component } from "@oh-my-pi/pi-tui";
6
6
  import { Text } from "@oh-my-pi/pi-tui";
7
- import { isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
7
+ import { formatGroupedPaths, isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
8
8
  import * as z from "zod/v4";
9
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
10
10
  import { InternalUrlRouter } from "../internal-urls";
@@ -13,7 +13,6 @@ import findDescription from "../prompts/tools/find.md" with { type: "text" };
13
13
  import { type TruncationResult, truncateHead } from "../session/streaming-output";
14
14
  import { Ellipsis, fileHyperlink, renderFileList, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
15
15
  import type { ToolSession } from ".";
16
- import { buildPathTree, walkPathTree } from "./grouped-file-output";
17
16
  import { applyListLimit } from "./list-limit";
18
17
  import { formatFullOutputReference, type OutputMeta } from "./output-meta";
19
18
  import {
@@ -54,30 +53,6 @@ const DEFAULT_GLOB_TIMEOUT_MS = 5000;
54
53
  const MIN_GLOB_TIMEOUT_MS = 500;
55
54
  const MAX_GLOB_TIMEOUT_MS = 60_000;
56
55
 
57
- /**
58
- * Group find matches into a multi-level directory tree so the model doesn't pay
59
- * repeated tokens for shared path prefixes. Single-child directory chains fold
60
- * into one header (`# a/b/c/`), so a common prefix — including an absolute root
61
- * for out-of-cwd results — collapses to a single line. Each level adds one `#`;
62
- * files are listed bare under the deepest directory header that owns them.
63
- *
64
- * Order follows the input (mtime-desc for native glob): a directory appears when
65
- * its first member is emitted, and a node's own files precede its subdirectories.
66
- */
67
- export function formatFindGroupedOutput(paths: readonly string[]): string {
68
- if (paths.length === 0) return "";
69
- const tree = buildPathTree(paths.map(entry => ({ path: entry, isDir: entry.endsWith("/") })));
70
- const lines: string[] = [];
71
- for (const event of walkPathTree(tree)) {
72
- if (event.kind === "dir") {
73
- lines.push(`${"#".repeat(event.depth + 1)} ${event.name}/`);
74
- } else {
75
- lines.push(event.name);
76
- }
77
- }
78
- return lines.join("\n");
79
- }
80
-
81
56
  export interface FindToolDetails {
82
57
  truncation?: TruncationResult;
83
58
  resultLimitReached?: number;
@@ -270,7 +245,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
270
245
  const listLimit = applyListLimit(files, { limit: effectiveLimit });
271
246
  const limited = listLimit.items;
272
247
  const limitMeta = listLimit.meta;
273
- const baseOutput = formatFindGroupedOutput(limited);
248
+ const baseOutput = formatGroupedPaths(limited);
274
249
  const trailingNotes: string[] = [];
275
250
  if (notice) trailingNotes.push(notice);
276
251
  if (missingPathsNote) trailingNotes.push(missingPathsNote);
@@ -1,123 +1,6 @@
1
1
  import path from "node:path";
2
2
 
3
- const URL_LIKE_PATH_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
4
-
5
- function isUrlLikePath(filePath: string): boolean {
6
- return URL_LIKE_PATH_RE.test(filePath);
7
- }
8
-
9
- // =============================================================================
10
- // Multi-level path tree
11
- // =============================================================================
12
- //
13
- // File listings (grep / ast-grep / ast-edit / lsp diagnostics / find) used to
14
- // group by the *immediate* parent directory and print the full directory path in
15
- // every header. For results spread across a deep tree — or rooted outside cwd,
16
- // where paths stay absolute — that repeated the shared prefix on every line. The
17
- // tree below folds single-child directory chains (so the common prefix collapses
18
- // into one header) and nests the rest, charging the model one token per path
19
- // segment instead of one per file.
20
-
21
- interface PathTreeNode {
22
- /** Direct file leaves, in first-seen order. */
23
- files: Array<{ name: string; key: string }>;
24
- /** Dedup set for `files` (a glob can surface the same path twice on retry). */
25
- fileNames: Set<string>;
26
- /** Child directories, in first-seen order. */
27
- subdirs: Array<{ name: string; node: PathTreeNode }>;
28
- /** Dedup index for `subdirs`. */
29
- dirIndex: Map<string, PathTreeNode>;
30
- }
31
-
32
- export interface PathTreeInput {
33
- /** Path string; absolute, cwd-relative, or url-like. Backslashes are normalized. */
34
- path: string;
35
- /** Whether the leaf itself is a directory (trailing-slash match from find). */
36
- isDir: boolean;
37
- /** Opaque key carried onto file events for section lookup. Defaults to `path`. */
38
- key?: string;
39
- }
40
-
41
- /** One node emitted while walking the tree: a folded directory or a file leaf. */
42
- export interface GroupedTreeEvent {
43
- kind: "dir" | "file";
44
- /** 0-based nesting depth (root children are depth 0). */
45
- depth: number;
46
- /** Folded chain for dirs (e.g. `a/b/c`, no trailing slash); basename for files. */
47
- name: string;
48
- /** File key for `kind === "file"`; empty string for directories. */
49
- key: string;
50
- }
51
-
52
- function createNode(): PathTreeNode {
53
- return { files: [], fileNames: new Set(), subdirs: [], dirIndex: new Map() };
54
- }
55
-
56
- function addFile(node: PathTreeNode, name: string, key: string): void {
57
- if (node.fileNames.has(name)) return;
58
- node.fileNames.add(name);
59
- node.files.push({ name, key });
60
- }
61
-
62
- /**
63
- * Build a directory tree from a flat list of paths. URL-like entries are kept
64
- * whole as root-level file leaves (they have no meaningful directory structure).
65
- * Absolute paths carry a leading empty segment so they share a common `/` root
66
- * and fold like any other prefix.
67
- */
68
- export function buildPathTree(entries: Iterable<PathTreeInput>): PathTreeNode {
69
- const root = createNode();
70
- for (const { path: rawPath, isDir, key } of entries) {
71
- const normalized = rawPath.replace(/\\/g, "/");
72
- const fileKey = key ?? rawPath;
73
- if (isUrlLikePath(normalized)) {
74
- addFile(root, normalized, fileKey);
75
- continue;
76
- }
77
- const trimmed = normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
78
- if (trimmed.length === 0) continue;
79
- const segments = trimmed.split("/");
80
- const dirCount = isDir ? segments.length : segments.length - 1;
81
- let node = root;
82
- for (let i = 0; i < dirCount; i++) {
83
- const segment = segments[i]!;
84
- let child = node.dirIndex.get(segment);
85
- if (!child) {
86
- child = createNode();
87
- node.dirIndex.set(segment, child);
88
- node.subdirs.push({ name: segment, node: child });
89
- }
90
- node = child;
91
- }
92
- if (!isDir) {
93
- addFile(node, segments[segments.length - 1]!, fileKey);
94
- }
95
- }
96
- return root;
97
- }
98
-
99
- /**
100
- * Depth-first walk yielding directory and file events. Directories collapse their
101
- * single-child chains (`a` → `a/b` → `a/b/c`) so a shared prefix becomes one
102
- * header. Each node's direct files are emitted before its subdirectories, keeping
103
- * a file unambiguously attached to the header above it.
104
- */
105
- export function* walkPathTree(node: PathTreeNode, depth = 0): Generator<GroupedTreeEvent> {
106
- for (const file of node.files) {
107
- yield { kind: "file", depth, name: file.name, key: file.key };
108
- }
109
- for (const subdir of node.subdirs) {
110
- let dirNode = subdir.node;
111
- const parts = [subdir.name];
112
- while (dirNode.files.length === 0 && dirNode.subdirs.length === 1) {
113
- const only = dirNode.subdirs[0]!;
114
- parts.push(only.name);
115
- dirNode = only.node;
116
- }
117
- yield { kind: "dir", depth, name: parts.join("/"), key: "" };
118
- yield* walkPathTree(dirNode, depth + 1);
119
- }
120
- }
3
+ import { buildPathTree, isUrlLikePath, type PathTreeInput, walkPathTree } from "@oh-my-pi/pi-utils";
121
4
 
122
5
  // =============================================================================
123
6
  // Grouped file output (grep / ast-grep / ast-edit / lsp diagnostics)
@@ -18,7 +18,7 @@ import { LspTool } from "../lsp";
18
18
  import type { MCPManager } from "../mcp";
19
19
  import type { MnemopiSessionState } from "../mnemopi/state";
20
20
  import type { PlanModeState } from "../plan-mode/state";
21
- import { type AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
21
+ import type { AgentRegistry } from "../registry/agent-registry";
22
22
  import type { ArtifactManager } from "../session/artifacts";
23
23
  import type { ClientBridge } from "../session/client-bridge";
24
24
  import type { CustomMessage } from "../session/messages";
@@ -42,7 +42,7 @@ import { resolveEvalBackends } from "./eval-backends";
42
42
  import { FindTool } from "./find";
43
43
  import { GithubTool } from "./gh";
44
44
  import { InspectImageTool } from "./inspect-image";
45
- import { IrcTool } from "./irc";
45
+ import { IrcTool, isIrcEnabled } from "./irc";
46
46
  import { JobTool } from "./job";
47
47
  import { MemoryEditTool } from "./memory-edit";
48
48
  import { MemoryRecallTool } from "./memory-recall";
@@ -260,8 +260,6 @@ export interface ToolSession {
260
260
  recordEvalSubagentUsage?: (output: number) => void;
261
261
  /** Bridge to the connected client (e.g. ACP editor host). Tools should route fs/terminal/permission requests through this when available. */
262
262
  getClientBridge?: () => ClientBridge | undefined;
263
- /** Get compact conversation context for subagents (excludes tool results, system prompts) */
264
- getCompactContext?: () => string;
265
263
  /** Get cached todo phases for this session. */
266
264
  getTodoPhases?: () => TodoPhase[];
267
265
  /** Replace cached todo phases for this session. */
@@ -420,7 +418,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
420
418
  checkpoint: CheckpointTool.createIf,
421
419
  rewind: RewindTool.createIf,
422
420
  task: s => TaskTool.create(s),
423
- job: JobTool.createIf,
421
+ job: s => new JobTool(s),
424
422
  irc: IrcTool.createIf,
425
423
  todo: s => new TodoTool(s),
426
424
  web_search: s => new WebSearchTool(s),
@@ -539,13 +537,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
539
537
  if (name === "search_tool_bm25") return discoveryActive;
540
538
  if (name === "browser") return session.settings.get("browser.enabled");
541
539
  if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
542
- if (name === "irc") {
543
- if (!session.settings.get("irc.enabled")) return false;
544
- // Main agent only needs `irc` when subagents may run concurrently (async).
545
- // In sync mode main blocks on `task`, so peer messaging from main is dead weight.
546
- if (!session.settings.get("async.enabled") && session.getAgentId?.() === MAIN_AGENT_ID) return false;
547
- return true;
548
- }
540
+ if (name === "irc") return isIrcEnabled(session.settings, session.taskDepth ?? 0);
549
541
  if (name === "retain" || name === "recall" || name === "reflect") {
550
542
  return ["hindsight", "mnemopi"].includes(session.settings.get("memory.backend") ?? "");
551
543
  }