@oh-my-pi/pi-coding-agent 15.0.0 → 15.0.2

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 (165) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/examples/extensions/plan-mode.ts +0 -1
  3. package/package.json +10 -10
  4. package/scripts/build-binary.ts +5 -0
  5. package/src/autoresearch/helpers.ts +17 -0
  6. package/src/autoresearch/tools/log-experiment.ts +9 -17
  7. package/src/autoresearch/tools/run-experiment.ts +2 -17
  8. package/src/capability/skill.ts +7 -0
  9. package/src/cli/list-models.ts +1 -1
  10. package/src/cli/shell-cli.ts +3 -13
  11. package/src/cli/update-cli.ts +1 -1
  12. package/src/cli.ts +10 -29
  13. package/src/commands/commit.ts +10 -0
  14. package/src/commit/agentic/tools/propose-changelog.ts +8 -1
  15. package/src/commit/analysis/conventional.ts +8 -66
  16. package/src/commit/map-reduce/reduce-phase.ts +6 -65
  17. package/src/commit/pipeline.ts +2 -2
  18. package/src/commit/shared-llm.ts +89 -0
  19. package/src/config/config-file.ts +210 -0
  20. package/src/config/model-equivalence.ts +8 -11
  21. package/src/config/model-registry.ts +44 -3
  22. package/src/config/model-resolver.ts +1 -4
  23. package/src/config/settings-schema.ts +82 -1
  24. package/src/config/settings.ts +1 -1
  25. package/src/config.ts +3 -219
  26. package/src/discovery/claude-plugins.ts +19 -7
  27. package/src/edit/renderer.ts +7 -1
  28. package/src/eval/js/executor.ts +3 -0
  29. package/src/eval/js/shared/rewrite-imports.ts +2 -2
  30. package/src/eval/py/executor.ts +5 -0
  31. package/src/eval/py/runner.py +42 -11
  32. package/src/eval/py/runtime.ts +1 -0
  33. package/src/exa/factory.ts +2 -2
  34. package/src/exa/mcp-client.ts +74 -1
  35. package/src/exec/bash-executor.ts +5 -1
  36. package/src/export/html/template.generated.ts +1 -1
  37. package/src/export/html/template.js +0 -11
  38. package/src/extensibility/extensions/get-commands-handler.ts +77 -0
  39. package/src/extensibility/extensions/runner.ts +1 -1
  40. package/src/extensibility/extensions/types.ts +89 -223
  41. package/src/extensibility/hooks/types.ts +89 -314
  42. package/src/extensibility/plugins/legacy-pi-compat.ts +48 -31
  43. package/src/extensibility/shared-events.ts +343 -0
  44. package/src/extensibility/skills.ts +9 -0
  45. package/src/goals/index.ts +3 -0
  46. package/src/goals/runtime.ts +500 -0
  47. package/src/goals/state.ts +37 -0
  48. package/src/goals/tools/goal-tool.ts +237 -0
  49. package/src/hashline/anchors.ts +2 -2
  50. package/src/hashline/input.ts +2 -1
  51. package/src/hashline/parser.ts +27 -3
  52. package/src/hindsight/mental-models.ts +1 -1
  53. package/src/internal-urls/agent-protocol.ts +1 -20
  54. package/src/internal-urls/artifact-protocol.ts +1 -19
  55. package/src/internal-urls/docs-index.generated.ts +11 -12
  56. package/src/internal-urls/registry-helpers.ts +25 -0
  57. package/src/internal-urls/router.ts +8 -0
  58. package/src/internal-urls/types.ts +21 -0
  59. package/src/lsp/config.ts +15 -6
  60. package/src/lsp/defaults.json +6 -2
  61. package/src/main.ts +11 -2
  62. package/src/mcp/oauth-flow.ts +20 -0
  63. package/src/modes/acp/acp-agent.ts +327 -95
  64. package/src/modes/components/assistant-message.ts +14 -8
  65. package/src/modes/components/bash-execution.ts +24 -63
  66. package/src/modes/components/custom-message.ts +14 -40
  67. package/src/modes/components/eval-execution.ts +27 -57
  68. package/src/modes/components/execution-shared.ts +102 -0
  69. package/src/modes/components/hook-message.ts +17 -49
  70. package/src/modes/components/mcp-add-wizard.ts +26 -5
  71. package/src/modes/components/message-frame.ts +88 -0
  72. package/src/modes/components/model-selector.ts +1 -1
  73. package/src/modes/components/session-observer-overlay.ts +6 -2
  74. package/src/modes/components/session-selector.ts +1 -1
  75. package/src/modes/components/status-line/segments.ts +93 -8
  76. package/src/modes/components/status-line/types.ts +4 -0
  77. package/src/modes/components/status-line.ts +28 -10
  78. package/src/modes/components/tool-execution.ts +7 -8
  79. package/src/modes/controllers/command-controller-shared.ts +108 -0
  80. package/src/modes/controllers/command-controller.ts +13 -4
  81. package/src/modes/controllers/event-controller.ts +36 -7
  82. package/src/modes/controllers/extension-ui-controller.ts +3 -2
  83. package/src/modes/controllers/input-controller.ts +13 -0
  84. package/src/modes/controllers/mcp-command-controller.ts +56 -61
  85. package/src/modes/controllers/ssh-command-controller.ts +18 -57
  86. package/src/modes/interactive-mode.ts +624 -52
  87. package/src/modes/print-mode.ts +16 -86
  88. package/src/modes/rpc/host-uris.ts +235 -0
  89. package/src/modes/rpc/rpc-mode.ts +41 -88
  90. package/src/modes/rpc/rpc-types.ts +57 -0
  91. package/src/modes/runtime-init.ts +116 -0
  92. package/src/modes/theme/defaults/dark-poimandres.json +3 -0
  93. package/src/modes/theme/defaults/light-poimandres.json +3 -0
  94. package/src/modes/theme/theme.ts +24 -6
  95. package/src/modes/types.ts +14 -3
  96. package/src/modes/utils/context-usage.ts +13 -13
  97. package/src/modes/utils/ui-helpers.ts +10 -3
  98. package/src/plan-mode/approved-plan.ts +35 -1
  99. package/src/prompts/goals/goal-budget-limit.md +16 -0
  100. package/src/prompts/goals/goal-continuation.md +28 -0
  101. package/src/prompts/goals/goal-mode-active.md +23 -0
  102. package/src/prompts/system/plan-mode-active.md +5 -5
  103. package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
  104. package/src/prompts/tools/bash.md +6 -0
  105. package/src/prompts/tools/github.md +4 -4
  106. package/src/prompts/tools/goal.md +13 -0
  107. package/src/prompts/tools/hashline.md +101 -117
  108. package/src/prompts/tools/read.md +55 -36
  109. package/src/prompts/tools/resolve.md +6 -5
  110. package/src/sdk.ts +12 -5
  111. package/src/session/agent-session.ts +428 -106
  112. package/src/session/blob-store.ts +36 -3
  113. package/src/session/messages.ts +67 -2
  114. package/src/session/session-manager.ts +131 -12
  115. package/src/session/session-storage.ts +33 -15
  116. package/src/session/streaming-output.ts +309 -13
  117. package/src/slash-commands/builtin-registry.ts +18 -0
  118. package/src/ssh/ssh-executor.ts +5 -0
  119. package/src/system-prompt.ts +4 -2
  120. package/src/task/discovery.ts +5 -2
  121. package/src/task/executor.ts +19 -8
  122. package/src/task/index.ts +3 -0
  123. package/src/task/render.ts +21 -15
  124. package/src/task/types.ts +4 -0
  125. package/src/tools/ast-edit.ts +21 -120
  126. package/src/tools/ast-grep.ts +21 -119
  127. package/src/tools/bash-command-fixup.ts +47 -0
  128. package/src/tools/bash-interactive.ts +9 -1
  129. package/src/tools/bash.ts +66 -19
  130. package/src/tools/browser/attach.ts +3 -3
  131. package/src/tools/browser/launch.ts +81 -18
  132. package/src/tools/browser/registry.ts +1 -5
  133. package/src/tools/browser/render.ts +2 -2
  134. package/src/tools/browser/tab-supervisor.ts +51 -14
  135. package/src/tools/conflict-detect.ts +15 -4
  136. package/src/tools/eval.ts +12 -2
  137. package/src/tools/find.ts +20 -38
  138. package/src/tools/gh.ts +44 -10
  139. package/src/tools/index.ts +22 -11
  140. package/src/tools/inspect-image.ts +3 -10
  141. package/src/tools/job.ts +16 -7
  142. package/src/tools/output-meta.ts +202 -37
  143. package/src/tools/path-utils.ts +125 -2
  144. package/src/tools/read.ts +548 -237
  145. package/src/tools/render-utils.ts +92 -0
  146. package/src/tools/renderers.ts +2 -0
  147. package/src/tools/resolve.ts +72 -44
  148. package/src/tools/search.ts +120 -186
  149. package/src/tools/ssh.ts +3 -2
  150. package/src/tools/write.ts +64 -9
  151. package/src/utils/file-mentions.ts +1 -1
  152. package/src/utils/image-loading.ts +7 -3
  153. package/src/utils/image-resize.ts +32 -43
  154. package/src/vim/parser.ts +0 -17
  155. package/src/vim/render.ts +1 -1
  156. package/src/vim/types.ts +1 -1
  157. package/src/web/search/providers/anthropic.ts +5 -0
  158. package/src/web/search/providers/exa.ts +3 -0
  159. package/src/web/search/providers/gemini.ts +40 -95
  160. package/src/web/search/providers/jina.ts +5 -2
  161. package/src/web/search/providers/zai.ts +5 -2
  162. package/src/prompts/tools/exit-plan-mode.md +0 -6
  163. package/src/tools/exit-plan-mode.ts +0 -97
  164. package/src/utils/fuzzy.ts +0 -108
  165. package/src/utils/image-convert.ts +0 -27
package/src/tools/find.ts CHANGED
@@ -12,15 +12,7 @@ import { InternalUrlRouter } from "../internal-urls";
12
12
  import type { Theme } from "../modes/theme/theme";
13
13
  import findDescription from "../prompts/tools/find.md" with { type: "text" };
14
14
  import { type TruncationResult, truncateHead } from "../session/streaming-output";
15
- import {
16
- Ellipsis,
17
- Hasher,
18
- type RenderCache,
19
- renderFileList,
20
- renderStatusLine,
21
- renderTreeList,
22
- truncateToWidth,
23
- } from "../tui";
15
+ import { Ellipsis, renderFileList, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
24
16
  import type { ToolSession } from ".";
25
17
  import { applyListLimit } from "./list-limit";
26
18
  import { formatFullOutputReference, type OutputMeta } from "./output-meta";
@@ -33,7 +25,13 @@ import {
33
25
  resolveExplicitFindPatterns,
34
26
  resolveToCwd,
35
27
  } from "./path-utils";
36
- import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
28
+ import {
29
+ createCachedComponent,
30
+ formatCount,
31
+ formatEmptyMessage,
32
+ formatErrorMessage,
33
+ PREVIEW_LIMITS,
34
+ } from "./render-utils";
37
35
  import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
38
36
  import { toolResult } from "./tool-result";
39
37
 
@@ -401,30 +399,22 @@ export const findToolRenderer = {
401
399
  },
402
400
  uiTheme,
403
401
  );
404
- let cached: RenderCache | undefined;
405
- return {
406
- render(width: number): string[] {
407
- const { expanded } = options;
408
- const key = new Hasher().bool(expanded).u32(width).digest();
409
- if (cached?.key === key) return cached.lines;
402
+ return createCachedComponent(
403
+ () => options.expanded,
404
+ width => {
410
405
  const listLines = renderTreeList(
411
406
  {
412
407
  items: lines,
413
- expanded,
408
+ expanded: options.expanded,
414
409
  maxCollapsed: COLLAPSED_LIST_LIMIT,
415
410
  itemType: "file",
416
411
  renderItem: line => uiTheme.fg("accent", line),
417
412
  },
418
413
  uiTheme,
419
414
  );
420
- const result = [header, ...listLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
421
- cached = { key, lines: result };
422
- return result;
415
+ return [header, ...listLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
423
416
  },
424
- invalidate() {
425
- cached = undefined;
426
- },
427
- };
417
+ );
428
418
  }
429
419
 
430
420
  const fileCount = details?.fileCount ?? 0;
@@ -467,28 +457,20 @@ export const findToolRenderer = {
467
457
  }
468
458
  if (missingNote) extraLines.push(missingNote);
469
459
 
470
- let cached: RenderCache | undefined;
471
- return {
472
- render(width: number): string[] {
473
- const { expanded } = options;
474
- const key = new Hasher().bool(expanded).u32(width).digest();
475
- if (cached?.key === key) return cached.lines;
460
+ return createCachedComponent(
461
+ () => options.expanded,
462
+ width => {
476
463
  const fileLines = renderFileList(
477
464
  {
478
465
  files: files.map(entry => ({ path: entry, isDirectory: entry.endsWith("/") })),
479
- expanded,
466
+ expanded: options.expanded,
480
467
  maxCollapsed: COLLAPSED_LIST_LIMIT,
481
468
  },
482
469
  uiTheme,
483
470
  );
484
- const result = [header, ...fileLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
485
- cached = { key, lines: result };
486
- return result;
487
- },
488
- invalidate() {
489
- cached = undefined;
471
+ return [header, ...fileLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
490
472
  },
491
- };
473
+ );
492
474
  },
493
475
  mergeCallAndResult: true,
494
476
  };
package/src/tools/gh.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
+ import { scheduler } from "node:timers/promises";
4
5
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
5
6
  import { StringEnum } from "@oh-my-pi/pi-ai";
6
- import { abortableSleep, getWorktreesDir, isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
7
+ import { getWorktreesDir, isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
7
8
  import { type Static, Type } from "@sinclair/typebox";
8
9
  import type { Settings } from "../config/settings";
9
10
  import githubDescription from "../prompts/tools/github.md" with { type: "text" };
@@ -1773,6 +1774,39 @@ export async function resolveDefaultRepoMemoized(cwd: string, signal?: AbortSign
1773
1774
  return untilAborted(signal, pending);
1774
1775
  }
1775
1776
 
1777
+ /**
1778
+ * Matches search-query qualifiers that already scope to a repository, org, or
1779
+ * user. When present, callers should avoid layering a default `repo:<current>`
1780
+ * on top — the user has already expressed an explicit scope.
1781
+ *
1782
+ * Only the leading `repo:`/`org:`/`user:`/`owner:` token is treated as a
1783
+ * scope marker; arbitrary substrings (e.g. inside quoted text) are ignored.
1784
+ */
1785
+ const REPO_SCOPE_QUALIFIER_PATTERN = /(?:^|\s)-?(?:repo|org|user|owner):\S/i;
1786
+
1787
+ /**
1788
+ * Resolve the effective `repo:` scope for a search op. Returns the explicit
1789
+ * `repo` when set, `undefined` when the query already carries a scoping
1790
+ * qualifier, and otherwise the current checkout's `owner/repo` via
1791
+ * `resolveDefaultRepoMemoized`. Resolution failures (no git/gh context, no
1792
+ * configured remote) silently fall back to `undefined` so the search proceeds
1793
+ * across all of GitHub instead of throwing.
1794
+ */
1795
+ async function resolveSearchRepoScope(
1796
+ cwd: string,
1797
+ repo: string | undefined,
1798
+ query: string | undefined,
1799
+ signal: AbortSignal | undefined,
1800
+ ): Promise<string | undefined> {
1801
+ if (repo) return repo;
1802
+ if (query && REPO_SCOPE_QUALIFIER_PATTERN.test(query)) return undefined;
1803
+ try {
1804
+ return await resolveDefaultRepoMemoized(cwd, signal);
1805
+ } catch {
1806
+ return undefined;
1807
+ }
1808
+ }
1809
+
1776
1810
  async function resolveGitHubBranchHead(
1777
1811
  cwd: string,
1778
1812
  repo: string,
@@ -3266,11 +3300,11 @@ async function executeSearchIssues(
3266
3300
  params: GithubInput,
3267
3301
  signal: AbortSignal | undefined,
3268
3302
  ): Promise<AgentToolResult<GhToolDetails>> {
3269
- const repo = normalizeOptionalString(params.repo);
3270
3303
  const limit = resolveSearchLimit(params.limit);
3271
3304
  const dateField = resolveSearchDateField("issues", params.dateField);
3272
3305
  const dateQualifier = buildSearchDateQualifier(dateField, params.since, params.until);
3273
3306
  const displayQuery = composeSearchQuery([params.query, dateQualifier]);
3307
+ const repo = await resolveSearchRepoScope(session.cwd, normalizeOptionalString(params.repo), displayQuery, signal);
3274
3308
  const apiQuery = composeSearchQuery([displayQuery, repo ? `repo:${repo}` : undefined, "is:issue"]);
3275
3309
  const args = buildGhApiSearchArgs("issues", apiQuery, limit);
3276
3310
 
@@ -3284,11 +3318,11 @@ async function executeSearchPrs(
3284
3318
  params: GithubInput,
3285
3319
  signal: AbortSignal | undefined,
3286
3320
  ): Promise<AgentToolResult<GhToolDetails>> {
3287
- const repo = normalizeOptionalString(params.repo);
3288
3321
  const limit = resolveSearchLimit(params.limit);
3289
3322
  const dateField = resolveSearchDateField("prs", params.dateField);
3290
3323
  const dateQualifier = buildSearchDateQualifier(dateField, params.since, params.until);
3291
3324
  const displayQuery = composeSearchQuery([params.query, dateQualifier]);
3325
+ const repo = await resolveSearchRepoScope(session.cwd, normalizeOptionalString(params.repo), displayQuery, signal);
3292
3326
  const apiQuery = composeSearchQuery([displayQuery, repo ? `repo:${repo}` : undefined, "is:pr"]);
3293
3327
  const args = buildGhApiSearchArgs("issues", apiQuery, limit);
3294
3328
 
@@ -3306,8 +3340,8 @@ async function executeSearchCode(
3306
3340
  if (params.since !== undefined || params.until !== undefined) {
3307
3341
  throw new ToolError("search_code does not support since/until; GitHub code search has no date qualifier.");
3308
3342
  }
3309
- const repo = normalizeOptionalString(params.repo);
3310
3343
  const limit = resolveSearchLimit(params.limit);
3344
+ const repo = await resolveSearchRepoScope(session.cwd, normalizeOptionalString(params.repo), query, signal);
3311
3345
  const apiQuery = composeSearchQuery([query, repo ? `repo:${repo}` : undefined]);
3312
3346
  const args = buildGhApiSearchArgs("code", apiQuery, limit, ["Accept: application/vnd.github.text-match+json"]);
3313
3347
 
@@ -3321,11 +3355,11 @@ async function executeSearchCommits(
3321
3355
  params: GithubInput,
3322
3356
  signal: AbortSignal | undefined,
3323
3357
  ): Promise<AgentToolResult<GhToolDetails>> {
3324
- const repo = normalizeOptionalString(params.repo);
3325
3358
  const limit = resolveSearchLimit(params.limit);
3326
3359
  const dateField = resolveSearchDateField("commits", params.dateField);
3327
3360
  const dateQualifier = buildSearchDateQualifier(dateField, params.since, params.until);
3328
3361
  const displayQuery = composeSearchQuery([params.query, dateQualifier]);
3362
+ const repo = await resolveSearchRepoScope(session.cwd, normalizeOptionalString(params.repo), displayQuery, signal);
3329
3363
  const apiQuery = composeSearchQuery([displayQuery, repo ? `repo:${repo}` : undefined]);
3330
3364
  const args = buildGhApiSearchArgs("commits", apiQuery, limit);
3331
3365
 
@@ -3400,7 +3434,7 @@ async function executeRunWatch(
3400
3434
  note,
3401
3435
  }),
3402
3436
  });
3403
- await abortableSleep(graceSeconds * 1000, signal);
3437
+ await scheduler.wait(graceSeconds * 1000, { signal });
3404
3438
  run = await fetchRunSnapshot(session.cwd, repo, runId, signal);
3405
3439
  }
3406
3440
 
@@ -3435,7 +3469,7 @@ async function executeRunWatch(
3435
3469
  return buildTextResult(formatRunWatchResult(repo, run, [], tail), run.url, finalDetails);
3436
3470
  }
3437
3471
 
3438
- await abortableSleep(intervalSeconds * 1000, signal);
3472
+ await scheduler.wait(intervalSeconds * 1000, { signal });
3439
3473
  }
3440
3474
  }
3441
3475
 
@@ -3477,7 +3511,7 @@ async function executeRunWatch(
3477
3511
  note,
3478
3512
  }),
3479
3513
  });
3480
- await abortableSleep(graceSeconds * 1000, signal);
3514
+ await scheduler.wait(graceSeconds * 1000, { signal });
3481
3515
  runs = await fetchRunsForCommit(session.cwd, repo, headSha, branch, signal);
3482
3516
  }
3483
3517
 
@@ -3533,11 +3567,11 @@ async function executeRunWatch(
3533
3567
  note,
3534
3568
  }),
3535
3569
  });
3536
- await abortableSleep(intervalSeconds * 1000, signal);
3570
+ await scheduler.wait(intervalSeconds * 1000, { signal });
3537
3571
  continue;
3538
3572
  }
3539
3573
 
3540
3574
  settledSuccessSignature = undefined;
3541
- await abortableSleep(intervalSeconds * 1000, signal);
3575
+ await scheduler.wait(intervalSeconds * 1000, { signal });
3542
3576
  }
3543
3577
  }
@@ -6,6 +6,8 @@ import type { Settings } from "../config/settings";
6
6
  import { EditTool } from "../edit";
7
7
  import { checkPythonKernelAvailability } from "../eval/py/kernel";
8
8
  import type { Skill } from "../extensibility/skills";
9
+ import type { GoalModeState, GoalRuntime } from "../goals";
10
+ import { GoalTool } from "../goals/tools/goal-tool";
9
11
  import type { HindsightSessionState } from "../hindsight/state";
10
12
  import { LspTool } from "../lsp";
11
13
  import type { PlanModeState } from "../plan-mode/state";
@@ -29,7 +31,6 @@ import { CalculatorTool } from "./calculator";
29
31
  import { type CheckpointState, CheckpointTool, RewindTool } from "./checkpoint";
30
32
  import { DebugTool } from "./debug";
31
33
  import { EvalTool } from "./eval";
32
- import { ExitPlanModeTool } from "./exit-plan-mode";
33
34
  import { FindTool } from "./find";
34
35
  import { GithubTool } from "./gh";
35
36
  import { HindsightRecallTool } from "./hindsight-recall";
@@ -57,6 +58,7 @@ import { YieldTool } from "./yield";
57
58
  export * from "../edit";
58
59
  export * from "../exa";
59
60
  export type * from "../exa/types";
61
+ export * from "../goals";
60
62
  export * from "../lsp";
61
63
  export * from "../session/streaming-output";
62
64
  export * from "../task";
@@ -70,7 +72,6 @@ export * from "./calculator";
70
72
  export * from "./checkpoint";
71
73
  export * from "./debug";
72
74
  export * from "./eval";
73
- export * from "./exit-plan-mode";
74
75
  export * from "./find";
75
76
  export * from "./gh";
76
77
  export * from "./hindsight-recall";
@@ -179,6 +180,10 @@ export interface ToolSession {
179
180
  settings: Settings;
180
181
  /** Plan mode state (if active) */
181
182
  getPlanModeState?: () => PlanModeState | undefined;
183
+ /** Goal mode state (if active or paused) */
184
+ getGoalModeState?: () => GoalModeState | undefined;
185
+ /** Goal runtime for the active agent session. */
186
+ getGoalRuntime?: () => GoalRuntime | undefined;
182
187
  /** Bridge to the connected client (e.g. ACP editor host). Tools should route fs/terminal/permission requests through this when available. */
183
188
  getClientBridge?: () => ClientBridge | undefined;
184
189
  /** Get compact conversation context for subagents (excludes tool results, system prompts) */
@@ -220,6 +225,12 @@ export interface ToolSession {
220
225
  steer?(message: { customType: string; content: string; details?: unknown }): void;
221
226
  /** Peek the currently in-flight tool-choice queue directive's invocation handler. Used by the `resolve` tool to dispatch to the pending action. */
222
227
  peekQueueInvoker?(): ((input: unknown) => Promise<unknown> | unknown) | undefined;
228
+ /** Peek the long-lived "standing" resolve handler registered by a mode (e.g. plan mode).
229
+ * Consulted by the `resolve` tool as a fallback when no queue invoker is in flight,
230
+ * letting modes accept `resolve` invocations without forcing the tool choice every turn. */
231
+ peekStandingResolveHandler?(): ((input: unknown) => Promise<unknown> | unknown) | undefined;
232
+ /** Register or clear the standing resolve handler. Passing `null` clears it. */
233
+ setStandingResolveHandler?(handler: ((input: unknown) => Promise<unknown> | unknown) | null): void;
223
234
  /** Get active checkpoint state if any. */
224
235
  getCheckpointState?: () => CheckpointState | undefined;
225
236
  /** Set or clear active checkpoint state. */
@@ -303,8 +314,8 @@ export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
303
314
  yield: s => new YieldTool(s),
304
315
  report_finding: () => reportFindingTool,
305
316
  report_tool_issue: s => createReportToolIssueTool(s),
306
- exit_plan_mode: s => new ExitPlanModeTool(s),
307
317
  resolve: s => new ResolveTool(s),
318
+ goal: s => new GoalTool(s),
308
319
  };
309
320
 
310
321
  export type ToolName = keyof typeof BUILTIN_TOOLS;
@@ -351,11 +362,12 @@ export function resolveEvalBackends(session: ToolSession): EvalBackendsAllowance
351
362
  export async function createTools(session: ToolSession, toolNames?: string[]): Promise<Tool[]> {
352
363
  const includeYield = session.requireYieldTool === true;
353
364
  const enableLsp = session.enableLsp ?? true;
354
- const requestedTools =
365
+ let requestedTools =
355
366
  toolNames && toolNames.length > 0 ? [...new Set(toolNames.map(name => name.toLowerCase()))] : undefined;
356
- const planEnabled = session.settings.get("plan.enabled");
357
- if (planEnabled && requestedTools && !requestedTools.includes("exit_plan_mode")) {
358
- requestedTools.push("exit_plan_mode");
367
+ const goalEnabled = session.settings.get("goal.enabled");
368
+ const goalModeActive = goalEnabled && session.getGoalModeState?.()?.enabled === true;
369
+ if (goalModeActive && requestedTools && !requestedTools.includes("goal")) {
370
+ requestedTools = [...requestedTools, "goal"];
359
371
  }
360
372
  const backends = resolveEvalBackends(session);
361
373
  const allowPython = backends.python;
@@ -428,7 +440,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
428
440
 
429
441
  const allTools: Record<string, ToolFactory> = { ...BUILTIN_TOOLS, ...HIDDEN_TOOLS };
430
442
  const isToolAllowed = (name: string) => {
431
- if (name === "exit_plan_mode") return planEnabled;
443
+ if (name === "goal") return goalEnabled && goalModeActive;
432
444
  if (name === "lsp") return enableLsp && session.settings.get("lsp.enabled");
433
445
  if (name === "bash") return true;
434
446
  if (name === "eval") return allowEval;
@@ -478,7 +490,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
478
490
  .filter(([name]) => isToolAllowed(name))
479
491
  .map(([name, factory]) => [name, factory] as const),
480
492
  ...(includeYield ? ([["yield", HIDDEN_TOOLS.yield]] as const) : []),
481
- ...(planEnabled ? ([["exit_plan_mode", HIDDEN_TOOLS.exit_plan_mode]] as const) : []),
493
+ ...(goalModeActive ? ([["goal", HIDDEN_TOOLS.goal]] as const) : []),
482
494
  ];
483
495
 
484
496
  const baseResults = await Promise.all(
@@ -488,8 +500,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
488
500
  }),
489
501
  );
490
502
  const tools = baseResults.filter((r): r is Tool => r !== null);
491
- const hasDeferrableTools = tools.some(tool => tool.deferrable === true);
492
- if (hasDeferrableTools && !tools.some(tool => tool.name === "resolve")) {
503
+ if (!tools.some(tool => tool.name === "resolve")) {
493
504
  const resolveTool = await logger.time("createTools:resolve", HIDDEN_TOOLS.resolve, session);
494
505
  if (resolveTool) {
495
506
  tools.push(wrapToolWithMetaNotice(resolveTool));
@@ -1,7 +1,8 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
- import { type Api, type AssistantMessage, completeSimple, type Model } from "@oh-my-pi/pi-ai";
2
+ import { type Api, completeSimple, type Model } from "@oh-my-pi/pi-ai";
3
3
  import { prompt } from "@oh-my-pi/pi-utils";
4
4
  import { type Static, Type } from "@sinclair/typebox";
5
+ import { extractTextContent } from "../commit/utils";
5
6
  import { expandRoleAlias, resolveModelFromString } from "../config/model-resolver";
6
7
  import inspectImageDescription from "../prompts/tools/inspect-image.md" with { type: "text" };
7
8
  import inspectImageSystemPromptTemplate from "../prompts/tools/inspect-image-system.md" with { type: "text" };
@@ -30,14 +31,6 @@ export interface InspectImageToolDetails {
30
31
  mimeType: string;
31
32
  }
32
33
 
33
- function extractResponseText(message: AssistantMessage): string {
34
- return message.content
35
- .filter(content => content.type === "text")
36
- .map(content => content.text)
37
- .join("")
38
- .trim();
39
- }
40
-
41
34
  export class InspectImageTool implements AgentTool<typeof inspectImageSchema, InspectImageToolDetails> {
42
35
  readonly name = "inspect_image";
43
36
  readonly label = "InspectImage";
@@ -151,7 +144,7 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
151
144
  throw new ToolError("inspect_image request aborted.");
152
145
  }
153
146
 
154
- const text = extractResponseText(response);
147
+ const text = extractTextContent(response);
155
148
  if (!text) {
156
149
  throw new ToolError("inspect_image model returned no text output.");
157
150
  }
package/src/tools/job.ts CHANGED
@@ -362,6 +362,8 @@ const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
362
362
  const LABEL_MAX_WIDTH = 60;
363
363
  const PREVIEW_LINES_COLLAPSED = 1;
364
364
  const PREVIEW_LINES_EXPANDED = 4;
365
+ const LABEL_LINES_COLLAPSED = 1;
366
+ const LABEL_LINES_EXPANDED = 3;
365
367
  const PREVIEW_LINE_WIDTH = 80;
366
368
 
367
369
  function statusToIcon(status: JobSnapshot["status"]): ToolUIStatus {
@@ -488,14 +490,21 @@ export const jobToolRenderer = {
488
490
  );
489
491
  const typeBadge = formatBadge(job.type, statusToColor(job.status), uiTheme);
490
492
  const idText = uiTheme.fg("muted", job.id);
491
- const label = truncateToWidth(
492
- replaceTabs(job.label || "(no label)"),
493
- LABEL_MAX_WIDTH,
494
- Ellipsis.Unicode,
495
- );
496
- const labelText = uiTheme.fg("toolOutput", label);
493
+ const rawLabelLines = (job.label || "(no label)").split(/\r?\n/);
494
+ const maxLabelLines = expanded ? LABEL_LINES_EXPANDED : LABEL_LINES_COLLAPSED;
495
+ const visibleLabelLines = rawLabelLines
496
+ .slice(0, maxLabelLines)
497
+ .map(l => truncateToWidth(replaceTabs(l), LABEL_MAX_WIDTH, Ellipsis.Unicode));
498
+ if (rawLabelLines.length > maxLabelLines && visibleLabelLines.length > 0) {
499
+ const last = visibleLabelLines[visibleLabelLines.length - 1]!;
500
+ visibleLabelLines[visibleLabelLines.length - 1] = `${last} …`;
501
+ }
497
502
  const durationText = uiTheme.fg("dim", formatDuration(job.durationMs));
498
- lines.push(`${icon} ${idText} ${typeBadge} ${labelText} ${durationText}`);
503
+ const headLabel = uiTheme.fg("toolOutput", visibleLabelLines[0] ?? "");
504
+ lines.push(`${icon} ${idText} ${typeBadge} ${headLabel} ${durationText}`);
505
+ for (let i = 1; i < visibleLabelLines.length; i++) {
506
+ lines.push(` ${uiTheme.fg("toolOutput", visibleLabelLines[i]!)}`);
507
+ }
499
508
 
500
509
  const preview = job.errorText?.trim() || job.resultText?.trim();
501
510
  if (preview) {