@oh-my-pi/pi-coding-agent 13.18.0 → 14.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 (235) hide show
  1. package/CHANGELOG.md +316 -1
  2. package/package.json +86 -24
  3. package/scripts/format-prompts.ts +2 -2
  4. package/src/autoresearch/apply-contract-to-state.ts +24 -0
  5. package/src/autoresearch/contract.ts +0 -44
  6. package/src/autoresearch/dashboard.ts +1 -2
  7. package/src/autoresearch/git.ts +116 -30
  8. package/src/autoresearch/helpers.ts +49 -0
  9. package/src/autoresearch/index.ts +28 -187
  10. package/src/autoresearch/prompt.md +26 -9
  11. package/src/autoresearch/state.ts +0 -6
  12. package/src/autoresearch/tools/init-experiment.ts +202 -117
  13. package/src/autoresearch/tools/log-experiment.ts +123 -178
  14. package/src/autoresearch/tools/run-experiment.ts +48 -10
  15. package/src/autoresearch/types.ts +2 -2
  16. package/src/capability/index.ts +4 -2
  17. package/src/cli/file-processor.ts +3 -3
  18. package/src/cli/grep-cli.ts +8 -8
  19. package/src/cli/grievances-cli.ts +78 -0
  20. package/src/cli/read-cli.ts +67 -0
  21. package/src/cli/setup-cli.ts +4 -4
  22. package/src/cli/update-cli.ts +3 -3
  23. package/src/cli.ts +2 -0
  24. package/src/commands/grep.ts +6 -1
  25. package/src/commands/grievances.ts +20 -0
  26. package/src/commands/read.ts +33 -0
  27. package/src/commit/agentic/agent.ts +5 -8
  28. package/src/commit/agentic/index.ts +22 -26
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/tools/git-file-diff.ts +3 -6
  31. package/src/commit/agentic/tools/git-hunk.ts +3 -3
  32. package/src/commit/agentic/tools/git-overview.ts +6 -9
  33. package/src/commit/agentic/tools/index.ts +6 -8
  34. package/src/commit/agentic/tools/propose-commit.ts +4 -7
  35. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  36. package/src/commit/agentic/tools/split-commit.ts +4 -4
  37. package/src/commit/agentic/validation.ts +1 -1
  38. package/src/commit/analysis/conventional.ts +4 -4
  39. package/src/commit/analysis/summary.ts +3 -3
  40. package/src/commit/changelog/generate.ts +4 -4
  41. package/src/commit/changelog/index.ts +5 -9
  42. package/src/commit/map-reduce/map-phase.ts +4 -4
  43. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  44. package/src/commit/pipeline.ts +13 -16
  45. package/src/config/keybindings.ts +7 -6
  46. package/src/config/prompt-templates.ts +44 -226
  47. package/src/config/resolve-config-value.ts +4 -2
  48. package/src/config/settings-schema.ts +98 -2
  49. package/src/config/settings.ts +25 -26
  50. package/src/dap/client.ts +674 -0
  51. package/src/dap/config.ts +150 -0
  52. package/src/dap/defaults.json +211 -0
  53. package/src/dap/index.ts +4 -0
  54. package/src/dap/session.ts +1255 -0
  55. package/src/dap/types.ts +600 -0
  56. package/src/debug/log-viewer.ts +3 -2
  57. package/src/discovery/builtin.ts +1 -2
  58. package/src/discovery/codex.ts +2 -2
  59. package/src/discovery/github.ts +2 -1
  60. package/src/discovery/helpers.ts +2 -2
  61. package/src/discovery/opencode.ts +2 -2
  62. package/src/edit/diff.ts +818 -0
  63. package/src/edit/index.ts +309 -0
  64. package/src/edit/line-hash.ts +67 -0
  65. package/src/edit/modes/chunk.ts +454 -0
  66. package/src/{patch → edit/modes}/hashline.ts +741 -361
  67. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  68. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  69. package/src/{patch → edit}/normalize.ts +97 -76
  70. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  71. package/src/exec/bash-executor.ts +4 -2
  72. package/src/exec/idle-timeout-watchdog.ts +126 -0
  73. package/src/exec/non-interactive-env.ts +5 -0
  74. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +6 -18
  75. package/src/extensibility/custom-commands/bundled/review/index.ts +45 -43
  76. package/src/extensibility/custom-commands/loader.ts +1 -2
  77. package/src/extensibility/custom-tools/loader.ts +34 -11
  78. package/src/extensibility/custom-tools/types.ts +1 -1
  79. package/src/extensibility/extensions/loader.ts +9 -4
  80. package/src/extensibility/extensions/runner.ts +24 -1
  81. package/src/extensibility/extensions/types.ts +4 -2
  82. package/src/extensibility/hooks/loader.ts +5 -6
  83. package/src/extensibility/hooks/types.ts +2 -2
  84. package/src/extensibility/plugins/doctor.ts +2 -1
  85. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
  86. package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
  87. package/src/extensibility/slash-commands.ts +3 -7
  88. package/src/index.ts +3 -1
  89. package/src/internal-urls/docs-index.generated.ts +11 -11
  90. package/src/ipy/executor.ts +58 -17
  91. package/src/ipy/gateway-coordinator.ts +6 -4
  92. package/src/ipy/kernel.ts +45 -22
  93. package/src/ipy/runtime.ts +2 -2
  94. package/src/lsp/client.ts +7 -4
  95. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  96. package/src/lsp/config.ts +2 -2
  97. package/src/lsp/defaults.json +688 -154
  98. package/src/lsp/index.ts +234 -45
  99. package/src/lsp/lspmux.ts +2 -2
  100. package/src/lsp/startup-events.ts +13 -0
  101. package/src/lsp/types.ts +12 -1
  102. package/src/lsp/utils.ts +8 -1
  103. package/src/main.ts +125 -47
  104. package/src/memories/index.ts +4 -5
  105. package/src/modes/acp/acp-agent.ts +563 -163
  106. package/src/modes/acp/acp-event-mapper.ts +9 -1
  107. package/src/modes/acp/acp-mode.ts +4 -2
  108. package/src/modes/components/agent-dashboard.ts +3 -4
  109. package/src/modes/components/diff.ts +6 -7
  110. package/src/modes/components/footer.ts +9 -29
  111. package/src/modes/components/hook-editor.ts +3 -3
  112. package/src/modes/components/hook-selector.ts +6 -1
  113. package/src/modes/components/read-tool-group.ts +6 -12
  114. package/src/modes/components/session-observer-overlay.ts +472 -0
  115. package/src/modes/components/settings-defs.ts +24 -0
  116. package/src/modes/components/status-line.ts +15 -61
  117. package/src/modes/components/tool-execution.ts +1 -1
  118. package/src/modes/components/welcome.ts +1 -1
  119. package/src/modes/controllers/btw-controller.ts +2 -2
  120. package/src/modes/controllers/command-controller.ts +4 -2
  121. package/src/modes/controllers/event-controller.ts +59 -2
  122. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  123. package/src/modes/controllers/input-controller.ts +15 -8
  124. package/src/modes/controllers/selector-controller.ts +26 -0
  125. package/src/modes/index.ts +20 -2
  126. package/src/modes/interactive-mode.ts +278 -69
  127. package/src/modes/rpc/host-tools.ts +186 -0
  128. package/src/modes/rpc/rpc-client.ts +178 -13
  129. package/src/modes/rpc/rpc-mode.ts +73 -3
  130. package/src/modes/rpc/rpc-types.ts +53 -1
  131. package/src/modes/session-observer-registry.ts +146 -0
  132. package/src/modes/shared.ts +0 -42
  133. package/src/modes/theme/theme.ts +80 -8
  134. package/src/modes/types.ts +4 -2
  135. package/src/modes/utils/keybinding-matchers.ts +9 -0
  136. package/src/prompts/system/custom-system-prompt.md +5 -0
  137. package/src/prompts/system/system-prompt.md +8 -1
  138. package/src/prompts/tools/chunk-edit.md +219 -0
  139. package/src/prompts/tools/debug.md +43 -0
  140. package/src/prompts/tools/grep.md +3 -0
  141. package/src/prompts/tools/lsp.md +5 -5
  142. package/src/prompts/tools/read-chunk.md +17 -0
  143. package/src/prompts/tools/read.md +19 -5
  144. package/src/sdk.ts +216 -165
  145. package/src/secrets/index.ts +1 -1
  146. package/src/secrets/obfuscator.ts +25 -17
  147. package/src/session/agent-session.ts +381 -286
  148. package/src/session/agent-storage.ts +12 -12
  149. package/src/session/compaction/branch-summarization.ts +3 -3
  150. package/src/session/compaction/compaction.ts +5 -6
  151. package/src/session/compaction/utils.ts +3 -3
  152. package/src/session/history-storage.ts +62 -19
  153. package/src/session/messages.ts +3 -3
  154. package/src/session/session-dump-format.ts +203 -0
  155. package/src/session/session-manager.ts +15 -5
  156. package/src/session/session-storage.ts +4 -2
  157. package/src/session/streaming-output.ts +1 -1
  158. package/src/session/tool-choice-queue.ts +213 -0
  159. package/src/slash-commands/builtin-registry.ts +56 -8
  160. package/src/ssh/connection-manager.ts +2 -2
  161. package/src/ssh/sshfs-mount.ts +5 -5
  162. package/src/stt/downloader.ts +4 -4
  163. package/src/stt/recorder.ts +4 -4
  164. package/src/stt/transcriber.ts +2 -2
  165. package/src/system-prompt.ts +25 -13
  166. package/src/task/agents.ts +5 -6
  167. package/src/task/commands.ts +2 -5
  168. package/src/task/executor.ts +32 -4
  169. package/src/task/index.ts +91 -82
  170. package/src/task/template.ts +2 -2
  171. package/src/task/types.ts +25 -0
  172. package/src/task/worktree.ts +131 -149
  173. package/src/tools/ask.ts +2 -3
  174. package/src/tools/ast-edit.ts +7 -7
  175. package/src/tools/ast-grep.ts +7 -7
  176. package/src/tools/auto-generated-guard.ts +36 -41
  177. package/src/tools/await-tool.ts +2 -2
  178. package/src/tools/bash.ts +5 -23
  179. package/src/tools/browser.ts +4 -5
  180. package/src/tools/calculator.ts +2 -3
  181. package/src/tools/cancel-job.ts +2 -2
  182. package/src/tools/checkpoint.ts +3 -3
  183. package/src/tools/debug.ts +1007 -0
  184. package/src/tools/exit-plan-mode.ts +3 -3
  185. package/src/tools/fetch.ts +67 -3
  186. package/src/tools/find.ts +4 -5
  187. package/src/tools/fs-cache-invalidation.ts +5 -0
  188. package/src/tools/gemini-image.ts +13 -5
  189. package/src/tools/gh.ts +130 -308
  190. package/src/tools/grep.ts +57 -9
  191. package/src/tools/index.ts +44 -22
  192. package/src/tools/inspect-image.ts +4 -4
  193. package/src/tools/output-meta.ts +1 -1
  194. package/src/tools/python.ts +19 -6
  195. package/src/tools/read.ts +211 -146
  196. package/src/tools/render-mermaid.ts +2 -3
  197. package/src/tools/render-utils.ts +20 -6
  198. package/src/tools/renderers.ts +3 -1
  199. package/src/tools/report-tool-issue.ts +80 -0
  200. package/src/tools/resolve.ts +70 -39
  201. package/src/tools/search-tool-bm25.ts +2 -2
  202. package/src/tools/ssh.ts +2 -2
  203. package/src/tools/todo-write.ts +2 -2
  204. package/src/tools/tool-timeouts.ts +1 -0
  205. package/src/tools/write.ts +5 -6
  206. package/src/tui/tree-list.ts +3 -1
  207. package/src/utils/clipboard.ts +80 -0
  208. package/src/utils/commit-message-generator.ts +2 -3
  209. package/src/utils/edit-mode.ts +49 -0
  210. package/src/utils/external-editor.ts +11 -5
  211. package/src/utils/file-display-mode.ts +6 -5
  212. package/src/utils/file-mentions.ts +8 -7
  213. package/src/utils/git.ts +1400 -0
  214. package/src/utils/image-loading.ts +98 -0
  215. package/src/utils/title-generator.ts +2 -3
  216. package/src/utils/tools-manager.ts +6 -6
  217. package/src/web/scrapers/choosealicense.ts +1 -1
  218. package/src/web/search/index.ts +3 -3
  219. package/src/web/search/render.ts +6 -4
  220. package/src/autoresearch/command-initialize.md +0 -34
  221. package/src/commit/git/errors.ts +0 -9
  222. package/src/commit/git/index.ts +0 -210
  223. package/src/commit/git/operations.ts +0 -54
  224. package/src/patch/diff.ts +0 -433
  225. package/src/patch/index.ts +0 -888
  226. package/src/patch/parser.ts +0 -532
  227. package/src/patch/types.ts +0 -292
  228. package/src/prompts/agents/oracle.md +0 -77
  229. package/src/tools/gh-cli.ts +0 -125
  230. package/src/tools/pending-action.ts +0 -49
  231. package/src/utils/child-process.ts +0 -88
  232. package/src/utils/frontmatter.ts +0 -117
  233. package/src/utils/image-input.ts +0 -274
  234. package/src/utils/mime.ts +0 -53
  235. package/src/utils/prompt-format.ts +0 -170
@@ -0,0 +1,126 @@
1
+ export type ExecutionAbortReason = "idle-timeout" | "signal";
2
+
3
+ export interface IdleTimeoutWatchdogOptions {
4
+ timeoutMs?: number;
5
+ signal?: AbortSignal;
6
+ hardTimeoutGraceMs: number;
7
+ onAbort?: (reason: ExecutionAbortReason) => void;
8
+ }
9
+
10
+ export class IdleTimeoutWatchdog {
11
+ #abortController = new AbortController();
12
+ #abortReason?: ExecutionAbortReason;
13
+ #hardTimeoutDeferred = Promise.withResolvers<"hard-timeout">();
14
+ #hardTimeoutGraceMs: number;
15
+ #hardTimeoutTimer?: NodeJS.Timeout;
16
+ #idleTimer?: NodeJS.Timeout;
17
+ #onAbort?: (reason: ExecutionAbortReason) => void;
18
+ #signal?: AbortSignal;
19
+ #signalAbortHandler?: () => void;
20
+ #timeoutMs?: number;
21
+
22
+ constructor(options: IdleTimeoutWatchdogOptions) {
23
+ this.#timeoutMs = options.timeoutMs;
24
+ this.#hardTimeoutGraceMs = options.hardTimeoutGraceMs;
25
+ this.#onAbort = options.onAbort;
26
+ this.#signal = options.signal;
27
+
28
+ if (this.#signal) {
29
+ if (this.#signal.aborted) {
30
+ this.#abort("signal");
31
+ return;
32
+ }
33
+
34
+ this.#signalAbortHandler = () => {
35
+ this.#abort("signal");
36
+ };
37
+ this.#signal.addEventListener("abort", this.#signalAbortHandler, { once: true });
38
+ }
39
+
40
+ this.touch();
41
+ }
42
+
43
+ get abortedBySignal(): boolean {
44
+ return this.#abortReason === "signal";
45
+ }
46
+
47
+ get hardTimeoutPromise(): Promise<"hard-timeout"> {
48
+ return this.#hardTimeoutDeferred.promise;
49
+ }
50
+
51
+ get signal(): AbortSignal {
52
+ return this.#abortController.signal;
53
+ }
54
+
55
+ get timedOut(): boolean {
56
+ return this.#abortReason === "idle-timeout";
57
+ }
58
+
59
+ touch(): void {
60
+ if (this.#abortReason || this.#timeoutMs === undefined || this.#timeoutMs <= 0) {
61
+ return;
62
+ }
63
+
64
+ if (this.#idleTimer) {
65
+ clearTimeout(this.#idleTimer);
66
+ }
67
+
68
+ this.#idleTimer = setTimeout(() => {
69
+ this.#abort("idle-timeout");
70
+ }, this.#timeoutMs);
71
+ }
72
+
73
+ dispose(): void {
74
+ if (this.#idleTimer) {
75
+ clearTimeout(this.#idleTimer);
76
+ this.#idleTimer = undefined;
77
+ }
78
+ if (this.#hardTimeoutTimer) {
79
+ clearTimeout(this.#hardTimeoutTimer);
80
+ this.#hardTimeoutTimer = undefined;
81
+ }
82
+ if (this.#signal && this.#signalAbortHandler) {
83
+ this.#signal.removeEventListener("abort", this.#signalAbortHandler);
84
+ this.#signalAbortHandler = undefined;
85
+ }
86
+ }
87
+
88
+ #abort(reason: ExecutionAbortReason): void {
89
+ if (this.#abortReason) {
90
+ return;
91
+ }
92
+
93
+ this.#abortReason = reason;
94
+
95
+ if (this.#idleTimer) {
96
+ clearTimeout(this.#idleTimer);
97
+ this.#idleTimer = undefined;
98
+ }
99
+
100
+ if (!this.#abortController.signal.aborted) {
101
+ this.#abortController.abort(reason);
102
+ }
103
+
104
+ this.#onAbort?.(reason);
105
+ this.#armHardTimeout();
106
+ }
107
+
108
+ #armHardTimeout(): void {
109
+ if (this.#hardTimeoutTimer || this.#hardTimeoutGraceMs <= 0) {
110
+ return;
111
+ }
112
+
113
+ this.#hardTimeoutTimer = setTimeout(() => {
114
+ this.#hardTimeoutDeferred.resolve("hard-timeout");
115
+ }, this.#hardTimeoutGraceMs);
116
+ }
117
+ }
118
+
119
+ export function formatIdleTimeoutMessage(timeoutMs?: number): string {
120
+ if (timeoutMs === undefined) {
121
+ return "Command timed out without output";
122
+ }
123
+
124
+ const seconds = Math.max(1, Math.round(timeoutMs / 1000));
125
+ return `Command timed out after ${seconds} seconds without output`;
126
+ }
@@ -13,6 +13,11 @@ export const NON_INTERACTIVE_ENV: Readonly<Record<string, string>> = {
13
13
  AWS_PAGER: "",
14
14
  HOMEBREW_PAGER: "cat",
15
15
  LESS: "FRX",
16
+ // Disable terminal features that can block the process.
17
+ TERM: "dumb",
18
+ GPG_TTY: "not a tty",
19
+ NO_COLOR: "1",
20
+ PYTHONUNBUFFERED: "1",
16
21
  // Disable editor and terminal credential prompts.
17
22
  GIT_EDITOR: "true",
18
23
  VISUAL: "true",
@@ -1,27 +1,15 @@
1
- import { renderPromptTemplate } from "../../../../config/prompt-templates";
1
+ import { prompt } from "@oh-my-pi/pi-utils";
2
2
  import type { CustomCommand, CustomCommandAPI } from "../../../../extensibility/custom-commands/types";
3
3
  import type { HookCommandContext } from "../../../../extensibility/hooks/types";
4
4
  import ciGreenRequestTemplate from "../../../../prompts/ci-green-request.md" with { type: "text" };
5
+ import * as git from "../../../../utils/git";
5
6
 
6
7
  async function getHeadTag(api: CustomCommandAPI): Promise<string | undefined> {
7
- const result = await api.exec("git", [
8
- "for-each-ref",
9
- "--points-at",
10
- "HEAD",
11
- "--sort=-version:refname",
12
- "--format=%(refname:strip=2)",
13
- "refs/tags",
14
- ]);
15
-
16
- if (result.code !== 0 || result.killed) {
8
+ try {
9
+ return (await git.ref.tags(api.cwd))[0];
10
+ } catch {
17
11
  return undefined;
18
12
  }
19
-
20
- const tag = result.stdout
21
- .split("\n")
22
- .map(line => line.trim())
23
- .find(Boolean);
24
- return tag || undefined;
25
13
  }
26
14
 
27
15
  export class GreenCommand implements CustomCommand {
@@ -32,6 +20,6 @@ export class GreenCommand implements CustomCommand {
32
20
 
33
21
  async execute(_args: string[], _ctx: HookCommandContext): Promise<string> {
34
22
  const headTag = await getHeadTag(this.api);
35
- return renderPromptTemplate(ciGreenRequestTemplate, { headTag });
23
+ return prompt.render(ciGreenRequestTemplate, { headTag });
36
24
  }
37
25
  }
@@ -11,10 +11,11 @@
11
11
  * rich context for the orchestrating agent to distribute work across
12
12
  * multiple reviewer agents based on diff weight and locality.
13
13
  */
14
- import { renderPromptTemplate } from "../../../../config/prompt-templates";
14
+ import { prompt } from "@oh-my-pi/pi-utils";
15
15
  import type { CustomCommand, CustomCommandAPI } from "../../../../extensibility/custom-commands/types";
16
16
  import type { HookCommandContext } from "../../../../extensibility/hooks/types";
17
17
  import reviewRequestTemplate from "../../../../prompts/review-request.md" with { type: "text" };
18
+ import * as git from "../../../../utils/git";
18
19
 
19
20
  // ─────────────────────────────────────────────────────────────────────────────
20
21
  // Types
@@ -208,7 +209,7 @@ function buildReviewPrompt(mode: string, stats: DiffStats, rawDiff: string): str
208
209
  hunksPreview: skipDiff ? getDiffPreview(f.hunks, linesPerFile) : "",
209
210
  }));
210
211
 
211
- return renderPromptTemplate(reviewRequestTemplate, {
212
+ return prompt.render(reviewRequestTemplate, {
212
213
  mode,
213
214
  files: filesWithExt,
214
215
  excluded: stats.excluded,
@@ -258,20 +259,20 @@ export class ReviewCommand implements CustomCommand {
258
259
  if (!baseBranch) return undefined;
259
260
 
260
261
  const currentBranch = await getCurrentBranch(this.api);
261
- const diffResult = await this.api.exec("git", ["diff", `${baseBranch}...${currentBranch}`], {
262
- timeout: 30000,
263
- });
264
- if (diffResult.code !== 0) {
265
- ctx.ui.notify(`Failed to get diff: ${diffResult.stderr}`, "error");
262
+ let diffText: string;
263
+ try {
264
+ diffText = await git.diff(this.api.cwd, { base: `${baseBranch}...${currentBranch}` });
265
+ } catch (err) {
266
+ ctx.ui.notify(`Failed to get diff: ${err instanceof Error ? err.message : String(err)}`, "error");
266
267
  return undefined;
267
268
  }
268
269
 
269
- if (!diffResult.stdout.trim()) {
270
+ if (!diffText.trim()) {
270
271
  ctx.ui.notify(`No changes between ${baseBranch} and ${currentBranch}`, "warning");
271
272
  return undefined;
272
273
  }
273
274
 
274
- const stats = parseDiff(diffResult.stdout);
275
+ const stats = parseDiff(diffText);
275
276
  if (stats.files.length === 0) {
276
277
  ctx.ui.notify("No reviewable files (all changes filtered out)", "warning");
277
278
  return undefined;
@@ -280,7 +281,7 @@ export class ReviewCommand implements CustomCommand {
280
281
  return buildReviewPrompt(
281
282
  `Reviewing changes between \`${baseBranch}\` and \`${currentBranch}\` (PR-style)`,
282
283
  stats,
283
- diffResult.stdout,
284
+ diffText,
284
285
  );
285
286
  }
286
287
 
@@ -292,12 +293,19 @@ export class ReviewCommand implements CustomCommand {
292
293
  return undefined;
293
294
  }
294
295
 
295
- const [unstagedResult, stagedResult] = await Promise.all([
296
- this.api.exec("git", ["diff"], { timeout: 30000 }),
297
- this.api.exec("git", ["diff", "--cached"], { timeout: 30000 }),
298
- ]);
296
+ let unstagedDiff: string;
297
+ let stagedDiff: string;
298
+ try {
299
+ [unstagedDiff, stagedDiff] = await Promise.all([
300
+ git.diff(this.api.cwd),
301
+ git.diff(this.api.cwd, { cached: true }),
302
+ ]);
303
+ } catch (err) {
304
+ ctx.ui.notify(`Failed to get diff: ${err instanceof Error ? err.message : String(err)}`, "error");
305
+ return undefined;
306
+ }
299
307
 
300
- const combinedDiff = [unstagedResult.stdout, stagedResult.stdout].filter(Boolean).join("\n");
308
+ const combinedDiff = [unstagedDiff, stagedDiff].filter(Boolean).join("\n");
301
309
 
302
310
  if (!combinedDiff.trim()) {
303
311
  ctx.ui.notify("No diff content found", "warning");
@@ -327,25 +335,26 @@ export class ReviewCommand implements CustomCommand {
327
335
  // Extract commit hash from selection (format: "abc1234 message")
328
336
  const hash = selected.split(" ")[0];
329
337
 
330
- // Get the commit diff (with timeout)
331
- const showResult = await this.api.exec("git", ["show", "--format=", hash], { timeout: 30000 });
332
- if (showResult.code !== 0) {
333
- ctx.ui.notify(`Failed to get commit: ${showResult.stderr}`, "error");
338
+ let diffText: string;
339
+ try {
340
+ diffText = await git.show(this.api.cwd, hash, { format: "" });
341
+ } catch (err) {
342
+ ctx.ui.notify(`Failed to get commit: ${err instanceof Error ? err.message : String(err)}`, "error");
334
343
  return undefined;
335
344
  }
336
345
 
337
- if (!showResult.stdout.trim()) {
346
+ if (!diffText.trim()) {
338
347
  ctx.ui.notify("Commit has no diff content", "warning");
339
348
  return undefined;
340
349
  }
341
350
 
342
- const stats = parseDiff(showResult.stdout);
351
+ const stats = parseDiff(diffText);
343
352
  if (stats.files.length === 0) {
344
353
  ctx.ui.notify("No reviewable files in commit (all changes filtered out)", "warning");
345
354
  return undefined;
346
355
  }
347
356
 
348
- return buildReviewPrompt(`Reviewing commit \`${hash}\``, stats, showResult.stdout);
357
+ return buildReviewPrompt(`Reviewing commit \`${hash}\``, stats, diffText);
349
358
  }
350
359
 
351
360
  case 4: {
@@ -354,16 +363,21 @@ export class ReviewCommand implements CustomCommand {
354
363
  if (!instructions?.trim()) return undefined;
355
364
 
356
365
  // For custom, we still try to get current diff for context
357
- const diffResult = await this.api.exec("git", ["diff", "HEAD"], { timeout: 30000 });
358
- const hasDiff = diffResult.code === 0 && diffResult.stdout.trim();
366
+ let diffText: string | undefined;
367
+ try {
368
+ diffText = await git.diff(this.api.cwd, { base: "HEAD" });
369
+ } catch {
370
+ diffText = undefined;
371
+ }
372
+ const reviewDiff = diffText?.trim();
359
373
 
360
- if (hasDiff) {
361
- const stats = parseDiff(diffResult.stdout);
374
+ if (reviewDiff) {
375
+ const stats = parseDiff(reviewDiff);
362
376
  // Even if all files filtered, include the custom instructions
363
377
  return `${buildReviewPrompt(
364
378
  `Custom review: ${instructions.split("\n")[0].slice(0, 60)}…`,
365
379
  stats,
366
- diffResult.stdout,
380
+ reviewDiff,
367
381
  )}\n\n### Additional Instructions\n\n${instructions}`;
368
382
  }
369
383
 
@@ -388,12 +402,7 @@ Use the Task tool with \`agent: "reviewer"\` to execute this review.`;
388
402
 
389
403
  async function getGitBranches(api: CustomCommandAPI): Promise<string[]> {
390
404
  try {
391
- const result = await api.exec("git", ["branch", "-a", "--format=%(refname:short)"]);
392
- if (result.code !== 0) return [];
393
- return result.stdout
394
- .split("\n")
395
- .map(b => b.trim())
396
- .filter(Boolean);
405
+ return await git.branch.list(api.cwd, { all: true });
397
406
  } catch {
398
407
  return [];
399
408
  }
@@ -401,8 +410,7 @@ async function getGitBranches(api: CustomCommandAPI): Promise<string[]> {
401
410
 
402
411
  async function getCurrentBranch(api: CustomCommandAPI): Promise<string> {
403
412
  try {
404
- const result = await api.exec("git", ["branch", "--show-current"]);
405
- return result.stdout.trim() || "HEAD";
413
+ return (await git.branch.current(api.cwd)) ?? "HEAD";
406
414
  } catch {
407
415
  return "HEAD";
408
416
  }
@@ -410,8 +418,7 @@ async function getCurrentBranch(api: CustomCommandAPI): Promise<string> {
410
418
 
411
419
  async function getGitStatus(api: CustomCommandAPI): Promise<string> {
412
420
  try {
413
- const result = await api.exec("git", ["status", "--porcelain"]);
414
- return result.stdout;
421
+ return await git.status(api.cwd);
415
422
  } catch {
416
423
  return "";
417
424
  }
@@ -419,12 +426,7 @@ async function getGitStatus(api: CustomCommandAPI): Promise<string> {
419
426
 
420
427
  async function getRecentCommits(api: CustomCommandAPI, count: number): Promise<string[]> {
421
428
  try {
422
- const result = await api.exec("git", ["log", `-${count}`, "--oneline", "--no-decorate"]);
423
- if (result.code !== 0) return [];
424
- return result.stdout
425
- .split("\n")
426
- .map(c => c.trim())
427
- .filter(Boolean);
429
+ return await git.log.onelines(api.cwd, count);
428
430
  } catch {
429
431
  return [];
430
432
  }
@@ -6,7 +6,6 @@
6
6
  */
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
- import * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
10
9
  import { getAgentDir, getProjectDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
11
10
  import * as typebox from "@sinclair/typebox";
12
11
  import { getConfigDirs } from "../../config";
@@ -184,7 +183,7 @@ export async function loadCustomCommands(options: LoadCustomCommandsOptions = {}
184
183
  exec: (command: string, args: string[], execOptions) =>
185
184
  execCommand(command, args, execOptions?.cwd ?? cwd, execOptions),
186
185
  typebox,
187
- pi: piCodingAgent,
186
+ pi: await import("@oh-my-pi/pi-coding-agent"),
188
187
  };
189
188
 
190
189
  // 1. Load bundled commands first (lowest priority - can be overridden)
@@ -5,7 +5,7 @@
5
5
  * to avoid import resolution issues with custom tools loaded from user directories.
6
6
  */
7
7
  import * as path from "node:path";
8
- import * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
8
+ import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
9
9
  import { logger } from "@oh-my-pi/pi-utils";
10
10
  import * as typebox from "@sinclair/typebox";
11
11
  import { toolCapability } from "../../capability/tool";
@@ -14,7 +14,6 @@ import type { ExecOptions } from "../../exec/exec";
14
14
  import { execCommand } from "../../exec/exec";
15
15
  import type { HookUIContext } from "../../extensibility/hooks/types";
16
16
  import { getAllPluginToolPaths } from "../../extensibility/plugins/loader";
17
- import type { PendingActionStore } from "../../tools/pending-action";
18
17
  import { createNoOpUIContext, resolvePath } from "../utils";
19
18
  import type { CustomToolAPI, CustomToolFactory, LoadedCustomTool, ToolLoadError } from "./types";
20
19
 
@@ -85,7 +84,17 @@ export class CustomToolLoader {
85
84
  #sharedApi: CustomToolAPI;
86
85
  #seenNames: Set<string>;
87
86
 
88
- constructor(cwd: string, builtInToolNames: string[], pendingActionStore?: PendingActionStore) {
87
+ constructor(
88
+ pi: typeof import("@oh-my-pi/pi-coding-agent"),
89
+ cwd: string,
90
+ builtInToolNames: string[],
91
+ pushPendingAction?: (action: {
92
+ label: string;
93
+ sourceToolName: string;
94
+ apply(reason: string): Promise<AgentToolResult<unknown>>;
95
+ reject?(reason: string): Promise<AgentToolResult<unknown> | undefined>;
96
+ }) => void,
97
+ ) {
89
98
  this.#sharedApi = {
90
99
  cwd,
91
100
  exec: (command: string, args: string[], options?: ExecOptions) =>
@@ -94,17 +103,16 @@ export class CustomToolLoader {
94
103
  hasUI: false,
95
104
  logger,
96
105
  typebox,
97
- pi: piCodingAgent,
106
+ pi,
98
107
  pushPendingAction: action => {
99
- if (!pendingActionStore) {
108
+ if (!pushPendingAction) {
100
109
  throw new Error("Pending action store unavailable for custom tools in this runtime.");
101
110
  }
102
- pendingActionStore.push({
111
+ pushPendingAction({
103
112
  label: action.label,
104
113
  sourceToolName: action.sourceToolName ?? "custom_tool",
105
114
  apply: action.apply,
106
115
  reject: action.reject,
107
- details: action.details,
108
116
  });
109
117
  },
110
118
  };
@@ -155,9 +163,19 @@ export async function loadCustomTools(
155
163
  pathsWithSources: ToolPathWithSource[],
156
164
  cwd: string,
157
165
  builtInToolNames: string[],
158
- pendingActionStore?: PendingActionStore,
166
+ pushPendingAction?: (action: {
167
+ label: string;
168
+ sourceToolName: string;
169
+ apply(reason: string): Promise<AgentToolResult<unknown>>;
170
+ reject?(reason: string): Promise<AgentToolResult<unknown> | undefined>;
171
+ }) => void,
159
172
  ) {
160
- const loader = new CustomToolLoader(cwd, builtInToolNames, pendingActionStore);
173
+ const loader = new CustomToolLoader(
174
+ await import("@oh-my-pi/pi-coding-agent"),
175
+ cwd,
176
+ builtInToolNames,
177
+ pushPendingAction,
178
+ );
161
179
  await loader.load(pathsWithSources);
162
180
  return {
163
181
  tools: loader.tools,
@@ -182,7 +200,12 @@ export async function discoverAndLoadCustomTools(
182
200
  configuredPaths: string[],
183
201
  cwd: string,
184
202
  builtInToolNames: string[],
185
- pendingActionStore?: PendingActionStore,
203
+ pushPendingAction?: (action: {
204
+ label: string;
205
+ sourceToolName: string;
206
+ apply(reason: string): Promise<AgentToolResult<unknown>>;
207
+ reject?(reason: string): Promise<AgentToolResult<unknown> | undefined>;
208
+ }) => void,
186
209
  ) {
187
210
  const allPathsWithSources: ToolPathWithSource[] = [];
188
211
  const seen = new Set<string>();
@@ -216,5 +239,5 @@ export async function discoverAndLoadCustomTools(
216
239
  addPath(resolvePath(configPath, cwd), { provider: "config", providerName: "Config", level: "project" });
217
240
  }
218
241
 
219
- return loadCustomTools(allPathsWithSources, cwd, builtInToolNames, pendingActionStore);
242
+ return loadCustomTools(allPathsWithSources, cwd, builtInToolNames, pushPendingAction);
220
243
  }
@@ -94,7 +94,7 @@ export type CustomToolSessionEvent =
94
94
  }
95
95
  | {
96
96
  reason: "auto_compaction_start";
97
- trigger: "threshold" | "overflow";
97
+ trigger: "threshold" | "overflow" | "idle";
98
98
  action: "context-full" | "handoff";
99
99
  }
100
100
  | {
@@ -6,7 +6,6 @@ import * as fs from "node:fs/promises";
6
6
  import * as path from "node:path";
7
7
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
8
  import type { ImageContent, Model, TextContent } from "@oh-my-pi/pi-ai";
9
- import * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
10
9
  import type { KeyId } from "@oh-my-pi/pi-tui";
11
10
  import { hasFsCode, isEacces, isEnoent, logger } from "@oh-my-pi/pi-utils";
12
11
  import type { TSchema } from "@sinclair/typebox";
@@ -102,7 +101,6 @@ export class ExtensionRuntime implements IExtensionRuntime {
102
101
  class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
103
102
  readonly logger = logger;
104
103
  readonly typebox = TypeBox;
105
- readonly pi = piCodingAgent;
106
104
  readonly flagValues = new Map<string, boolean | string>();
107
105
  readonly pendingProviderRegistrations: Array<{
108
106
  name: string;
@@ -111,6 +109,7 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
111
109
  }> = [];
112
110
 
113
111
  constructor(
112
+ public readonly pi: typeof import("@oh-my-pi/pi-coding-agent"),
114
113
  private readonly extension: Extension,
115
114
  private readonly runtime: IExtensionRuntime,
116
115
  private readonly cwd: string,
@@ -265,7 +264,13 @@ async function loadExtension(
265
264
  }
266
265
 
267
266
  const extension = createExtension(extensionPath, resolvedPath);
268
- const api = new ConcreteExtensionAPI(extension, runtime, cwd, eventBus);
267
+ const api = new ConcreteExtensionAPI(
268
+ await import("@oh-my-pi/pi-coding-agent"),
269
+ extension,
270
+ runtime,
271
+ cwd,
272
+ eventBus,
273
+ );
269
274
  await factory(api);
270
275
 
271
276
  return { extension, error: null };
@@ -286,7 +291,7 @@ export async function loadExtensionFromFactory(
286
291
  name = "<inline>",
287
292
  ): Promise<Extension> {
288
293
  const extension = createExtension(name, name);
289
- const api = new ConcreteExtensionAPI(extension, runtime, cwd, eventBus);
294
+ const api = new ConcreteExtensionAPI(await import("@oh-my-pi/pi-coding-agent"), extension, runtime, cwd, eventBus);
290
295
  await factory(api);
291
296
  return extension;
292
297
  }
@@ -262,6 +262,10 @@ export class ExtensionRunner {
262
262
  return allFlags;
263
263
  }
264
264
 
265
+ getFlagValues(): Map<string, boolean | string> {
266
+ return new Map(this.runtime.flagValues);
267
+ }
268
+
265
269
  setFlagValue(name: string, value: boolean | string): void {
266
270
  this.runtime.flagValues.set(name, value);
267
271
  }
@@ -695,7 +699,26 @@ export class ExtensionRunner {
695
699
 
696
700
  async emitContext(messages: AgentMessage[]): Promise<AgentMessage[]> {
697
701
  const ctx = this.createContext();
698
- let currentMessages = structuredClone(messages);
702
+
703
+ // Check if any extensions actually have context handlers before cloning
704
+ let hasContextHandlers = false;
705
+ for (const ext of this.extensions) {
706
+ if (ext.handlers.get("context")?.length) {
707
+ hasContextHandlers = true;
708
+ break;
709
+ }
710
+ }
711
+ if (!hasContextHandlers) return messages;
712
+
713
+ let currentMessages: AgentMessage[];
714
+ try {
715
+ currentMessages = structuredClone(messages);
716
+ } catch {
717
+ // Messages may contain non-cloneable objects (e.g. in ToolResultMessage.details
718
+ // or ProviderPayload). Fall back to a shallow array clone — extensions should
719
+ // return new message arrays rather than mutating in place.
720
+ currentMessages = [...messages];
721
+ }
699
722
 
700
723
  for (const ext of this.extensions) {
701
724
  const handlers = ext.handlers.get("context");
@@ -28,11 +28,11 @@ import type { Static, TSchema } from "@sinclair/typebox";
28
28
  import type { Rule } from "../../capability/rule";
29
29
  import type { KeybindingsManager } from "../../config/keybindings";
30
30
  import type { ModelRegistry } from "../../config/model-registry";
31
+ import type { EditToolDetails } from "../../edit";
31
32
  import type { BashResult } from "../../exec/bash-executor";
32
33
  import type { ExecOptions, ExecResult } from "../../exec/exec";
33
34
  import type { PythonResult } from "../../ipy/executor";
34
35
  import type { Theme } from "../../modes/theme/theme";
35
- import type { EditToolDetails } from "../../patch";
36
36
  import type { CompactionPreparation, CompactionResult } from "../../session/compaction";
37
37
  import type { CustomMessage } from "../../session/messages";
38
38
  import type {
@@ -81,6 +81,8 @@ export interface ExtensionUIDialogOptions {
81
81
  onLeft?: () => void;
82
82
  /** Invoked when user presses right arrow in select dialogs */
83
83
  onRight?: () => void;
84
+ /** Invoked when user presses the external editor shortcut in select dialogs */
85
+ onExternalEditor?: () => void;
84
86
  /** Optional footer hint text rendered by interactive selector */
85
87
  helpText?: string;
86
88
  }
@@ -566,7 +568,7 @@ export interface ToolExecutionEndEvent {
566
568
  /** Fired when auto-compaction starts */
567
569
  export interface AutoCompactionStartEvent {
568
570
  type: "auto_compaction_start";
569
- reason: "threshold" | "overflow";
571
+ reason: "threshold" | "overflow" | "idle";
570
572
  action: "context-full" | "handoff";
571
573
  }
572
574
 
@@ -2,7 +2,6 @@
2
2
  * Hook loader - loads TypeScript hook modules using native Bun import.
3
3
  */
4
4
  import * as path from "node:path";
5
- import * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
6
5
  import { logger } from "@oh-my-pi/pi-utils";
7
6
  import * as typebox from "@sinclair/typebox";
8
7
  import { hookCapability } from "../../capability/hook";
@@ -87,16 +86,16 @@ export interface LoadHooksResult {
87
86
  * Create a HookAPI instance that collects handlers, renderers, and commands.
88
87
  * Returns the API, maps, and functions to set handlers later.
89
88
  */
90
- function createHookAPI(
89
+ async function createHookAPI(
91
90
  handlers: Map<string, HandlerFn[]>,
92
91
  cwd: string,
93
- ): {
92
+ ): Promise<{
94
93
  api: HookAPI;
95
94
  messageRenderers: Map<string, HookMessageRenderer>;
96
95
  commands: Map<string, RegisteredCommand>;
97
96
  setSendMessageHandler: (handler: SendMessageHandler) => void;
98
97
  setAppendEntryHandler: (handler: AppendEntryHandler) => void;
99
- } {
98
+ }> {
100
99
  let sendMessageHandler: SendMessageHandler | null = null;
101
100
  let appendEntryHandler: AppendEntryHandler | null = null;
102
101
  const messageRenderers = new Map<string, HookMessageRenderer>();
@@ -137,7 +136,7 @@ function createHookAPI(
137
136
  },
138
137
  logger,
139
138
  typebox,
140
- pi: piCodingAgent,
139
+ pi: await import("@oh-my-pi/pi-coding-agent"),
141
140
  } as HookAPI;
142
141
 
143
142
  return {
@@ -170,7 +169,7 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo
170
169
 
171
170
  // Create handlers map and API
172
171
  const handlers = new Map<string, HandlerFn[]>();
173
- const { api, messageRenderers, commands, setSendMessageHandler, setAppendEntryHandler } = createHookAPI(
172
+ const { api, messageRenderers, commands, setSendMessageHandler, setAppendEntryHandler } = await createHookAPI(
174
173
  handlers,
175
174
  cwd,
176
175
  );