@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,80 @@
1
+ /**
2
+ * report_tool_issue — automated QA tool for tracking unexpected tool behavior.
3
+ *
4
+ * Enabled when PI_AUTO_QA=1 or the dev.autoqa setting is on.
5
+ * Always injected into every agent (including subagents) regardless of tool selection.
6
+ * Records grievances to a local SQLite database; never throws.
7
+ */
8
+ import { Database } from "bun:sqlite";
9
+ import path from "node:path";
10
+ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
11
+ import { $env, getAgentDir, logger, VERSION } from "@oh-my-pi/pi-utils";
12
+ import { Type } from "@sinclair/typebox";
13
+ import type { Settings } from "..";
14
+ import type { ToolSession } from "./index";
15
+
16
+ const ReportToolIssueParams = Type.Object({
17
+ tool: Type.String({ description: "Name of the tool that behaved unexpectedly" }),
18
+ report: Type.String({ description: "Description of what was unexpected about the tool's behavior" }),
19
+ });
20
+
21
+ export function isAutoQaEnabled(settings?: Settings): boolean {
22
+ return $env.PI_AUTO_QA === "1" || !!settings?.get("dev.autoqa");
23
+ }
24
+
25
+ export function getAutoQaDbPath(): string {
26
+ return path.join(getAgentDir(), "autoqa.db");
27
+ }
28
+
29
+ let cachedDb: Database | null = null;
30
+
31
+ function openDb(): Database | null {
32
+ if (cachedDb) return cachedDb;
33
+ try {
34
+ const db = new Database(getAutoQaDbPath());
35
+ db.run(`
36
+ PRAGMA journal_mode=WAL;
37
+ PRAGMA synchronous=NORMAL;
38
+ PRAGMA busy_timeout=5000;
39
+ CREATE TABLE IF NOT EXISTS grievances (
40
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
41
+ model TEXT NOT NULL,
42
+ version TEXT NOT NULL,
43
+ tool TEXT NOT NULL,
44
+ report TEXT NOT NULL
45
+ );
46
+ `);
47
+ cachedDb = db;
48
+ return db;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ export function createReportToolIssueTool(session: ToolSession): AgentTool {
55
+ const getModel = () => session.getActiveModelString?.() ?? "unknown";
56
+
57
+ return {
58
+ name: "report_tool_issue",
59
+ label: "Report Tool Issue",
60
+ description: "Report unexpected tool behavior for automated QA tracking.",
61
+ parameters: ReportToolIssueParams,
62
+ async execute(_toolCallId, rawParams) {
63
+ try {
64
+ const params = rawParams as { tool: string; report: string };
65
+ const db = openDb();
66
+ db?.prepare("INSERT INTO grievances (model, version, tool, report) VALUES (?, ?, ?, ?)").run(
67
+ getModel(),
68
+ VERSION,
69
+ params.tool,
70
+ params.report,
71
+ );
72
+ } catch (error) {
73
+ logger.error("Failed to record tool issue", { error });
74
+ }
75
+ return {
76
+ content: [{ type: "text", text: "Noted, thanks!" }],
77
+ };
78
+ },
79
+ };
80
+ }
@@ -1,9 +1,8 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Component } from "@oh-my-pi/pi-tui";
3
3
  import { Text } from "@oh-my-pi/pi-tui";
4
- import { untilAborted } from "@oh-my-pi/pi-utils";
4
+ import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
5
5
  import { type Static, Type } from "@sinclair/typebox";
6
- import { renderPromptTemplate } from "../config/prompt-templates";
7
6
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
8
7
  import type { Theme } from "../modes/theme/theme";
9
8
  import resolveDescription from "../prompts/tools/resolve.md" with { type: "text" };
@@ -11,7 +10,6 @@ import { Ellipsis, padToWidth, renderStatusLine, truncateToWidth } from "../tui"
11
10
  import type { ToolSession } from ".";
12
11
  import { replaceTabs } from "./render-utils";
13
12
  import { ToolError } from "./tool-errors";
14
- import { toolResult } from "./tool-result";
15
13
 
16
14
  const resolveSchema = Type.Object({
17
15
  action: Type.Union([Type.Literal("apply"), Type.Literal("discard")]),
@@ -33,6 +31,70 @@ function resolveReasonPreview(reason?: string): string | undefined {
33
31
  return truncateToWidth(trimmed, 72, Ellipsis.Omit);
34
32
  }
35
33
 
34
+ /**
35
+ * Queue a resolve-protocol handler on the tool-choice queue. Forces the next
36
+ * LLM call to invoke the hidden `resolve` tool, wraps the caller's apply/reject
37
+ * callbacks into an onInvoked closure that matches the resolve schema, and
38
+ * steers a preview reminder so the model understands why.
39
+ *
40
+ * This is the canonical entry point for any tool that wants preview/apply
41
+ * semantics. No session-level abstraction is needed: callers pass their
42
+ * apply/reject functions directly.
43
+ */
44
+ export function queueResolveHandler(
45
+ session: ToolSession,
46
+ options: {
47
+ label: string;
48
+ sourceToolName: string;
49
+ apply(reason: string): Promise<AgentToolResult<unknown>>;
50
+ reject?(reason: string): Promise<AgentToolResult<unknown> | undefined>;
51
+ },
52
+ ): void {
53
+ const queue = session.getToolChoiceQueue?.();
54
+ const forced = session.buildToolChoice?.("resolve");
55
+ if (!queue || !forced || typeof forced === "string") return;
56
+
57
+ const detailsFor = (params: ResolveParams): ResolveToolDetails => ({
58
+ action: params.action,
59
+ reason: params.reason,
60
+ sourceToolName: options.sourceToolName,
61
+ label: options.label,
62
+ });
63
+
64
+ queue.pushOnce(forced, {
65
+ label: `pending-action:${options.sourceToolName}`,
66
+ now: true,
67
+ onRejected: () => "requeue",
68
+ onInvoked: async (input: unknown) => {
69
+ const params = input as ResolveParams;
70
+ if (params.action === "apply") {
71
+ const result = await options.apply(params.reason);
72
+ return { ...result, details: detailsFor(params) };
73
+ }
74
+ if (params.action === "discard" && options.reject != null) {
75
+ const result = await options.reject(params.reason);
76
+ if (result != null) {
77
+ return { ...result, details: detailsFor(params) };
78
+ }
79
+ }
80
+ return {
81
+ content: [{ type: "text" as const, text: `Discarded: ${options.label}. Reason: ${params.reason}` }],
82
+ details: detailsFor(params),
83
+ };
84
+ },
85
+ });
86
+
87
+ session.steer?.({
88
+ customType: "resolve-reminder",
89
+ content: [
90
+ "<system-reminder>",
91
+ "This is a preview. Call the `resolve` tool to apply or discard these changes.",
92
+ "</system-reminder>",
93
+ ].join("\n"),
94
+ details: { toolName: options.sourceToolName },
95
+ });
96
+ }
97
+
36
98
  export class ResolveTool implements AgentTool<typeof resolveSchema, ResolveToolDetails> {
37
99
  readonly name = "resolve";
38
100
  readonly label = "Resolve";
@@ -42,7 +104,7 @@ export class ResolveTool implements AgentTool<typeof resolveSchema, ResolveToolD
42
104
  readonly strict = true;
43
105
 
44
106
  constructor(private readonly session: ToolSession) {
45
- this.description = renderPromptTemplate(resolveDescription);
107
+ this.description = prompt.render(resolveDescription);
46
108
  }
47
109
 
48
110
  async execute(
@@ -53,43 +115,12 @@ export class ResolveTool implements AgentTool<typeof resolveSchema, ResolveToolD
53
115
  _context?: AgentToolContext,
54
116
  ): Promise<AgentToolResult<ResolveToolDetails>> {
55
117
  return untilAborted(signal, async () => {
56
- const store = this.session.pendingActionStore;
57
- if (!store?.hasPending) {
118
+ const invoker = this.session.peekQueueInvoker?.();
119
+ if (!invoker) {
58
120
  throw new ToolError("No pending action to resolve. Nothing to apply or discard.");
59
121
  }
60
-
61
- const pendingAction = store.pop();
62
- if (!pendingAction) {
63
- throw new ToolError("No pending action to resolve. Nothing to apply or discard.");
64
- }
65
- const resolveDetails: ResolveToolDetails = {
66
- action: params.action,
67
- reason: params.reason,
68
- sourceToolName: pendingAction.sourceToolName,
69
- label: pendingAction.label,
70
- };
71
-
72
- if (params.action === "apply") {
73
- const applyResult = await pendingAction.apply(params.reason);
74
- const appliedText = applyResult.content
75
- .filter(part => part.type === "text")
76
- .map(part => part.text)
77
- .filter(text => text != null && text.length > 0)
78
- .join("\n");
79
- const baseResult = toolResult()
80
- .text(appliedText || `Applied: ${pendingAction.label}.`)
81
- .done();
82
- return { ...baseResult, details: resolveDetails };
83
- }
84
-
85
- if (params.action === "discard" && pendingAction.reject != null) {
86
- const discardResult = await pendingAction.reject(params.reason);
87
- if (discardResult != null) {
88
- return { ...discardResult, details: resolveDetails };
89
- }
90
- }
91
- const discardResult = toolResult().text(`Discarded: ${pendingAction.label}. Reason: ${params.reason}`).done();
92
- return { ...discardResult, details: resolveDetails };
122
+ const result = (await invoker(params)) as AgentToolResult<ResolveToolDetails>;
123
+ return result;
93
124
  });
94
125
  }
95
126
  }
@@ -1,7 +1,7 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
2
  import { type Component, Text } from "@oh-my-pi/pi-tui";
3
+ import { prompt } from "@oh-my-pi/pi-utils";
3
4
  import { type Static, Type } from "@sinclair/typebox";
4
- import { renderPromptTemplate } from "../config/prompt-templates";
5
5
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
6
6
  import {
7
7
  buildDiscoverableMCPSearchIndex,
@@ -105,7 +105,7 @@ function supportsMCPToolDiscoveryExecution(session: ToolSession): session is MCP
105
105
 
106
106
  export function renderSearchToolBm25Description(discoverableTools: DiscoverableMCPTool[] = []): string {
107
107
  const summary = summarizeDiscoverableMCPTools(discoverableTools);
108
- return renderPromptTemplate(searchToolBm25Description, {
108
+ return prompt.render(searchToolBm25Description, {
109
109
  discoverableMCPToolCount: summary.toolCount,
110
110
  discoverableMCPServerSummaries: summary.servers.map(formatDiscoverableMCPToolServerSummary),
111
111
  hasDiscoverableMCPServers: summary.servers.length > 0,
package/src/tools/ssh.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Component } from "@oh-my-pi/pi-tui";
3
3
  import { Text } from "@oh-my-pi/pi-tui";
4
+ import { prompt } from "@oh-my-pi/pi-utils";
4
5
  import { type Static, Type } from "@sinclair/typebox";
5
6
  import type { SSHHost } from "../capability/ssh";
6
7
  import { sshCapability } from "../capability/ssh";
7
- import { renderPromptTemplate } from "../config/prompt-templates";
8
8
  import { loadCapability } from "../discovery";
9
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
10
10
  import type { Theme } from "../modes/theme/theme";
@@ -59,7 +59,7 @@ async function formatHostEntry(host: SSHHost): Promise<string> {
59
59
  }
60
60
 
61
61
  async function formatDescription(hosts: SSHHost[]): Promise<string> {
62
- const baseDescription = renderPromptTemplate(sshDescriptionBase);
62
+ const baseDescription = prompt.render(sshDescriptionBase);
63
63
  if (hosts.length === 0) {
64
64
  return baseDescription;
65
65
  }
@@ -2,9 +2,9 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
2
2
  import { StringEnum } from "@oh-my-pi/pi-ai";
3
3
  import type { Component } from "@oh-my-pi/pi-tui";
4
4
  import { Text } from "@oh-my-pi/pi-tui";
5
+ import { prompt } from "@oh-my-pi/pi-utils";
5
6
  import { type Static, Type } from "@sinclair/typebox";
6
7
  import chalk from "chalk";
7
- import { renderPromptTemplate } from "../config/prompt-templates";
8
8
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
9
9
  import type { Theme } from "../modes/theme/theme";
10
10
  import todoWriteDescription from "../prompts/tools/todo-write.md" with { type: "text" };
@@ -347,7 +347,7 @@ export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWrit
347
347
  readonly strict = true;
348
348
 
349
349
  constructor(private readonly session: ToolSession) {
350
- this.description = renderPromptTemplate(todoWriteDescription);
350
+ this.description = prompt.render(todoWriteDescription);
351
351
  }
352
352
 
353
353
  async execute(
@@ -14,6 +14,7 @@ export const TOOL_TIMEOUTS = {
14
14
  ssh: { default: 60, min: 1, max: 3600 },
15
15
  fetch: { default: 20, min: 1, max: 45 },
16
16
  lsp: { default: 20, min: 5, max: 60 },
17
+ debug: { default: 30, min: 5, max: 300 },
17
18
  } as const satisfies Record<string, ToolTimeoutConfig>;
18
19
 
19
20
  export type ToolWithTimeout = keyof typeof TOOL_TIMEOUTS;
@@ -9,20 +9,19 @@ import type {
9
9
  } from "@oh-my-pi/pi-agent-core";
10
10
  import type { Component } from "@oh-my-pi/pi-tui";
11
11
  import { Text } from "@oh-my-pi/pi-tui";
12
- import { isEnoent, untilAborted } from "@oh-my-pi/pi-utils";
12
+ import { isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
13
13
  import { type Static, Type } from "@sinclair/typebox";
14
14
  import { unzipSync, zipSync } from "fflate";
15
- import { renderPromptTemplate } from "../config/prompt-templates";
15
+ import { stripHashlinePrefixes } from "../edit";
16
16
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
17
17
  import { createLspWritethrough, type FileDiagnosticsResult, type WritethroughCallback, writethroughNoop } from "../lsp";
18
18
  import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
19
- import { stripHashlinePrefixes } from "../patch";
20
19
  import writeDescription from "../prompts/tools/write.md" with { type: "text" };
21
20
  import type { ToolSession } from "../sdk";
22
21
  import { Ellipsis, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
23
22
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
24
23
  import { parseArchivePathCandidates } from "./archive-reader";
25
- import { checkAutoGeneratedFile } from "./auto-generated-guard";
24
+ import { assertEditableFile } from "./auto-generated-guard";
26
25
  import { invalidateFsScanAfterWrite } from "./fs-cache-invalidation";
27
26
  import { type OutputMeta, outputMeta } from "./output-meta";
28
27
  import { enforcePlanModeWrite, resolvePlanPath } from "./plan-mode-guard";
@@ -149,7 +148,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
149
148
  this.#writethrough = enableLsp
150
149
  ? createLspWritethrough(session.cwd, { enableFormat, enableDiagnostics })
151
150
  : writethroughNoop;
152
- this.description = renderPromptTemplate(writeDescription);
151
+ this.description = prompt.render(writeDescription);
153
152
  }
154
153
 
155
154
  async #resolveArchiveWritePath(writePath: string): Promise<ResolvedArchiveWritePath | null> {
@@ -298,7 +297,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
298
297
 
299
298
  // Check if file exists and is auto-generated before overwriting
300
299
  if (await fs.exists(absolutePath)) {
301
- await checkAutoGeneratedFile(absolutePath, path);
300
+ await assertEditableFile(absolutePath, path);
302
301
  }
303
302
 
304
303
  const diagnostics = await this.#writethrough(absolutePath, cleanContent, signal, undefined, batchRequest);
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Hierarchical tree list rendering helper.
3
3
  */
4
+
5
+ import { replaceTabs } from "@oh-my-pi/pi-tui";
4
6
  import type { Theme } from "../modes/theme/theme";
5
- import { formatMoreItems, replaceTabs } from "../tools/render-utils";
7
+ import { formatMoreItems } from "../tools/render-utils";
6
8
  import type { TreeContext } from "./types";
7
9
  import { getTreeBranch, getTreeContinuePrefix } from "./utils";
8
10
 
@@ -0,0 +1,80 @@
1
+ import { execSync } from "node:child_process";
2
+ import type { ClipboardImage } from "@oh-my-pi/pi-natives";
3
+ import * as native from "@oh-my-pi/pi-natives";
4
+
5
+ const hasDisplay = process.platform !== "linux" || Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
6
+
7
+ /**
8
+ * Copy text to the system clipboard.
9
+ *
10
+ * Emits OSC 52 first when running in a real terminal (works over SSH/mosh),
11
+ * then attempts native clipboard copy as best-effort for local sessions.
12
+ * On Termux, tries `termux-clipboard-set` before native.
13
+ *
14
+ * @param text - UTF-8 text to place on the clipboard.
15
+ */
16
+ export async function copyToClipboard(text: string): Promise<void> {
17
+ if (process.stdout.isTTY) {
18
+ const onError = (err: unknown) => {
19
+ process.stdout.off("error", onError);
20
+ // Prevent unhandled 'error' from crashing the process when stdout is a closed pipe.
21
+ if ((err as NodeJS.ErrnoException | null | undefined)?.code === "EPIPE") {
22
+ return;
23
+ }
24
+ };
25
+ try {
26
+ const encoded = Buffer.from(text).toString("base64");
27
+ const osc52 = `\x1b]52;c;${encoded}\x07`;
28
+ process.stdout.on("error", onError);
29
+ process.stdout.write(osc52, err => {
30
+ process.stdout.off("error", onError);
31
+ // If stdout is closed (e.g. piped to a process that exits early),
32
+ // ignore EPIPE and proceed with native clipboard best-effort.
33
+ if ((err as NodeJS.ErrnoException | null | undefined)?.code === "EPIPE") {
34
+ return;
35
+ }
36
+ });
37
+ } catch (err) {
38
+ process.stdout.off("error", onError);
39
+ if ((err as NodeJS.ErrnoException | null | undefined)?.code !== "EPIPE") {
40
+ // Ignore all write failures (OSC 52 is best-effort).
41
+ }
42
+ }
43
+ }
44
+
45
+ // Also try native tools (best effort for local sessions)
46
+ try {
47
+ if (process.env.TERMUX_VERSION) {
48
+ try {
49
+ execSync("termux-clipboard-set", { input: text, timeout: 5000 });
50
+ return;
51
+ } catch {
52
+ // Fall through to native
53
+ }
54
+ }
55
+
56
+ await native.copyToClipboard(text);
57
+ } catch {
58
+ // Ignore — clipboard copy is best-effort
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Read an image from the system clipboard.
64
+ *
65
+ * Returns null on Termux (no image clipboard support) or when no display
66
+ * server is available (headless/SSH without forwarding).
67
+ *
68
+ * @returns PNG payload or null when no image is available.
69
+ */
70
+ export async function readImageFromClipboard(): Promise<ClipboardImage | null> {
71
+ if (process.env.TERMUX_VERSION) {
72
+ return null;
73
+ }
74
+
75
+ if (!hasDisplay) {
76
+ return null;
77
+ }
78
+
79
+ return (await native.readImageFromClipboard()) ?? null;
80
+ }
@@ -5,16 +5,15 @@
5
5
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
6
6
  import type { Api, Model } from "@oh-my-pi/pi-ai";
7
7
  import { completeSimple } from "@oh-my-pi/pi-ai";
8
- import { logger } from "@oh-my-pi/pi-utils";
8
+ import { logger, prompt } from "@oh-my-pi/pi-utils";
9
9
  import type { ModelRegistry } from "../config/model-registry";
10
10
  import { resolveModelRoleValue } from "../config/model-resolver";
11
- import { renderPromptTemplate } from "../config/prompt-templates";
12
11
  import type { Settings } from "../config/settings";
13
12
  import MODEL_PRIO from "../priority.json" with { type: "json" };
14
13
  import commitSystemPrompt from "../prompts/system/commit-message-system.md" with { type: "text" };
15
14
  import { toReasoningEffort } from "../thinking";
16
15
 
17
- const COMMIT_SYSTEM_PROMPT = renderPromptTemplate(commitSystemPrompt);
16
+ const COMMIT_SYSTEM_PROMPT = prompt.render(commitSystemPrompt);
18
17
  const MAX_DIFF_CHARS = 4000;
19
18
 
20
19
  /** File patterns that should be excluded from commit message generation diffs. */
@@ -0,0 +1,49 @@
1
+ import { $env } from "@oh-my-pi/pi-utils";
2
+
3
+ export type EditMode = "replace" | "patch" | "hashline" | "chunk";
4
+
5
+ export const DEFAULT_EDIT_MODE: EditMode = "hashline";
6
+
7
+ const EDIT_MODE_IDS = {
8
+ chunk: "chunk",
9
+ hashline: "hashline",
10
+ patch: "patch",
11
+ replace: "replace",
12
+ } as const satisfies Record<string, EditMode>;
13
+
14
+ export const EDIT_MODES = Object.keys(EDIT_MODE_IDS) as EditMode[];
15
+
16
+ export function normalizeEditMode(mode?: string | null): EditMode | undefined {
17
+ if (!mode) return undefined;
18
+ return EDIT_MODE_IDS[mode as keyof typeof EDIT_MODE_IDS];
19
+ }
20
+
21
+ export interface EditModeSettingsLike {
22
+ get(key: "edit.mode"): unknown;
23
+ getEditVariantForModel?(model: string | undefined): EditMode | null;
24
+ }
25
+
26
+ export interface EditModeSessionLike {
27
+ settings: EditModeSettingsLike;
28
+ getActiveModelString?: () => string | undefined;
29
+ }
30
+
31
+ export function resolveEditMode(session: EditModeSessionLike): EditMode {
32
+ const activeModel = session.getActiveModelString?.();
33
+ const modelVariant = session.settings.getEditVariantForModel?.(activeModel);
34
+ if (modelVariant) return modelVariant;
35
+
36
+ const envMode = normalizeEditMode($env.PI_EDIT_VARIANT);
37
+ if (envMode) return envMode;
38
+
39
+ if ($env.PI_STRICT_EDIT_MODE === "1") {
40
+ if (activeModel?.includes("spark")) return "replace";
41
+ if (activeModel?.includes("nano")) return "replace";
42
+ if (activeModel?.includes("mini")) return "replace";
43
+ if (activeModel?.includes("haiku")) return "replace";
44
+ if (activeModel?.includes("flash")) return "replace";
45
+ }
46
+
47
+ const settingsMode = normalizeEditMode(String(session.settings.get("edit.mode") ?? ""));
48
+ return settingsMode ?? DEFAULT_EDIT_MODE;
49
+ }
@@ -17,6 +17,8 @@ export interface OpenInEditorOptions {
17
17
  extension?: string;
18
18
  /** Custom stdio configuration (default: all "inherit"). */
19
19
  stdio?: [number | "inherit", number | "inherit", number | "inherit"];
20
+ /** Keep the file's trailing newline instead of trimming it from the returned text. */
21
+ trimTrailingNewline?: boolean;
20
22
  }
21
23
 
22
24
  /**
@@ -40,13 +42,17 @@ export async function openInEditor(
40
42
  const stdio = options?.stdio ?? ["inherit", "inherit", "inherit"];
41
43
 
42
44
  const child = spawn(editor, [...editorArgs, tmpFile], { stdio, shell: process.platform === "win32" });
43
- const exitCode = await new Promise<number>((resolve, reject) => {
44
- child.once("exit", (code, signal) => resolve(code ?? (signal ? -1 : 0)));
45
- child.once("error", error => reject(error));
46
- });
45
+ const { promise, reject, resolve } = Promise.withResolvers<number>();
46
+ child.once("exit", (code, signal) => resolve(code ?? (signal ? -1 : 0)));
47
+ child.once("error", error => reject(error));
48
+ const exitCode = await promise;
47
49
 
48
50
  if (exitCode === 0) {
49
- return (await Bun.file(tmpFile).text()).replace(/\n$/, "");
51
+ const text = await Bun.file(tmpFile).text();
52
+ if (options?.trimTrailingNewline === false) {
53
+ return text;
54
+ }
55
+ return text.replace(/\n$/, "");
50
56
  }
51
57
  return null;
52
58
  } finally {
@@ -2,9 +2,12 @@
2
2
  * Resolve line-display mode for file-like outputs (read, grep, @file mentions).
3
3
  */
4
4
 
5
+ import { resolveEditMode } from "./edit-mode";
6
+
5
7
  export interface FileDisplayMode {
6
8
  lineNumbers: boolean;
7
9
  hashLines: boolean;
10
+ chunked: boolean;
8
11
  }
9
12
 
10
13
  /** Session-like object providing settings and tool availability for display mode resolution. */
@@ -24,13 +27,11 @@ export interface FileDisplayModeSession {
24
27
  export function resolveFileDisplayMode(session: FileDisplayModeSession): FileDisplayMode {
25
28
  const { settings } = session;
26
29
  const hasEditTool = session.hasEditTool ?? true;
27
- const hashLines =
28
- hasEditTool &&
29
- (settings.get("readHashLines") === true ||
30
- settings.get("edit.mode") === "hashline" ||
31
- Bun.env.PI_EDIT_VARIANT === "hashline");
30
+ const hashLines = hasEditTool && resolveEditMode(session) === "hashline" && settings.get("readHashLines") !== false;
31
+ const chunked = hasEditTool && resolveEditMode(session) === "chunk";
32
32
  return {
33
33
  hashLines,
34
34
  lineNumbers: hashLines || settings.get("readLineNumbers") === true,
35
+ chunked,
35
36
  };
36
37
  }
@@ -8,8 +8,10 @@
8
8
  import * as fs from "node:fs/promises";
9
9
  import path from "node:path";
10
10
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
11
+ import type { ImageContent } from "@oh-my-pi/pi-ai";
11
12
  import { glob } from "@oh-my-pi/pi-natives";
12
- import { formatHashLines } from "../patch/hashline";
13
+ import { formatAge, formatBytes, readImageMetadata } from "@oh-my-pi/pi-utils";
14
+ import { formatHashLines } from "../edit/line-hash";
13
15
  import type { FileMentionMessage } from "../session/messages";
14
16
  import {
15
17
  DEFAULT_MAX_BYTES,
@@ -18,10 +20,8 @@ import {
18
20
  truncateHeadBytes,
19
21
  } from "../session/streaming-output";
20
22
  import { resolveReadPath } from "../tools/path-utils";
21
- import { formatAge, formatBytes } from "../tools/render-utils";
22
23
  import { fuzzyMatch } from "./fuzzy";
23
24
  import { formatDimensionNote, resizeImage } from "./image-resize";
24
- import { detectSupportedImageMimeTypeFromFile } from "./mime";
25
25
 
26
26
  /** Regex to match @filepath patterns in text */
27
27
  const FILE_MENTION_REGEX = /@([^\s@]+)/g;
@@ -304,7 +304,8 @@ export async function generateFileMentionMessages(
304
304
  continue;
305
305
  }
306
306
 
307
- const mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);
307
+ const imageMetadata = await readImageMetadata(absolutePath);
308
+ const mimeType = imageMetadata?.mimeType;
308
309
  if (mimeType) {
309
310
  if (stat.size > MAX_AUTO_READ_IMAGE_BYTES) {
310
311
  files.push({
@@ -321,7 +322,7 @@ export async function generateFileMentionMessages(
321
322
  }
322
323
 
323
324
  const base64Content = buffer.toBase64();
324
- let image = { type: "image" as const, mimeType, data: base64Content };
325
+ let image: ImageContent = { type: "image", mimeType, data: base64Content };
325
326
  let dimensionNote: string | undefined;
326
327
 
327
328
  if (autoResizeImages) {
@@ -329,12 +330,12 @@ export async function generateFileMentionMessages(
329
330
  const resized = await resizeImage({ type: "image", data: base64Content, mimeType });
330
331
  dimensionNote = formatDimensionNote(resized);
331
332
  image = {
332
- type: "image" as const,
333
+ type: "image",
333
334
  mimeType: resized.mimeType,
334
335
  data: resized.data,
335
336
  };
336
337
  } catch {
337
- image = { type: "image" as const, mimeType, data: base64Content };
338
+ image = { type: "image", mimeType, data: base64Content };
338
339
  }
339
340
  }
340
341