@oh-my-pi/pi-coding-agent 13.19.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 (202) hide show
  1. package/CHANGELOG.md +266 -1
  2. package/package.json +86 -20
  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 +91 -0
  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 +83 -125
  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 -5
  28. package/src/commit/agentic/index.ts +3 -4
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/validation.ts +1 -1
  31. package/src/commit/analysis/conventional.ts +4 -4
  32. package/src/commit/analysis/summary.ts +3 -3
  33. package/src/commit/changelog/generate.ts +4 -4
  34. package/src/commit/map-reduce/map-phase.ts +4 -4
  35. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  36. package/src/commit/pipeline.ts +3 -4
  37. package/src/config/prompt-templates.ts +44 -226
  38. package/src/config/resolve-config-value.ts +4 -2
  39. package/src/config/settings-schema.ts +54 -2
  40. package/src/config/settings.ts +25 -26
  41. package/src/dap/client.ts +674 -0
  42. package/src/dap/config.ts +150 -0
  43. package/src/dap/defaults.json +211 -0
  44. package/src/dap/index.ts +4 -0
  45. package/src/dap/session.ts +1255 -0
  46. package/src/dap/types.ts +600 -0
  47. package/src/debug/log-viewer.ts +3 -2
  48. package/src/discovery/builtin.ts +1 -2
  49. package/src/discovery/codex.ts +2 -2
  50. package/src/discovery/github.ts +2 -1
  51. package/src/discovery/helpers.ts +2 -2
  52. package/src/discovery/opencode.ts +2 -2
  53. package/src/edit/diff.ts +818 -0
  54. package/src/edit/index.ts +309 -0
  55. package/src/edit/line-hash.ts +67 -0
  56. package/src/edit/modes/chunk.ts +454 -0
  57. package/src/{patch → edit/modes}/hashline.ts +741 -361
  58. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  59. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  60. package/src/{patch → edit}/normalize.ts +97 -76
  61. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  62. package/src/exec/bash-executor.ts +4 -2
  63. package/src/exec/idle-timeout-watchdog.ts +126 -0
  64. package/src/exec/non-interactive-env.ts +5 -0
  65. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +2 -2
  66. package/src/extensibility/custom-commands/bundled/review/index.ts +2 -2
  67. package/src/extensibility/custom-commands/loader.ts +1 -2
  68. package/src/extensibility/custom-tools/loader.ts +34 -11
  69. package/src/extensibility/extensions/loader.ts +9 -4
  70. package/src/extensibility/extensions/runner.ts +24 -1
  71. package/src/extensibility/extensions/types.ts +1 -1
  72. package/src/extensibility/hooks/loader.ts +5 -6
  73. package/src/extensibility/hooks/types.ts +1 -1
  74. package/src/extensibility/plugins/doctor.ts +2 -1
  75. package/src/extensibility/slash-commands.ts +3 -7
  76. package/src/index.ts +2 -1
  77. package/src/internal-urls/docs-index.generated.ts +11 -11
  78. package/src/ipy/executor.ts +58 -17
  79. package/src/ipy/gateway-coordinator.ts +6 -4
  80. package/src/ipy/kernel.ts +45 -22
  81. package/src/ipy/runtime.ts +2 -2
  82. package/src/lsp/client.ts +7 -4
  83. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  84. package/src/lsp/config.ts +2 -2
  85. package/src/lsp/defaults.json +688 -154
  86. package/src/lsp/index.ts +234 -45
  87. package/src/lsp/lspmux.ts +2 -2
  88. package/src/lsp/startup-events.ts +13 -0
  89. package/src/lsp/types.ts +12 -1
  90. package/src/lsp/utils.ts +8 -1
  91. package/src/main.ts +102 -46
  92. package/src/memories/index.ts +4 -5
  93. package/src/modes/acp/acp-agent.ts +563 -163
  94. package/src/modes/acp/acp-event-mapper.ts +9 -1
  95. package/src/modes/acp/acp-mode.ts +4 -2
  96. package/src/modes/components/agent-dashboard.ts +3 -4
  97. package/src/modes/components/diff.ts +6 -7
  98. package/src/modes/components/read-tool-group.ts +6 -12
  99. package/src/modes/components/settings-defs.ts +5 -0
  100. package/src/modes/components/tool-execution.ts +1 -1
  101. package/src/modes/components/welcome.ts +1 -1
  102. package/src/modes/controllers/btw-controller.ts +2 -2
  103. package/src/modes/controllers/command-controller.ts +3 -2
  104. package/src/modes/controllers/input-controller.ts +12 -8
  105. package/src/modes/index.ts +20 -2
  106. package/src/modes/interactive-mode.ts +94 -37
  107. package/src/modes/rpc/host-tools.ts +186 -0
  108. package/src/modes/rpc/rpc-client.ts +178 -13
  109. package/src/modes/rpc/rpc-mode.ts +73 -3
  110. package/src/modes/rpc/rpc-types.ts +53 -1
  111. package/src/modes/theme/theme.ts +80 -8
  112. package/src/modes/types.ts +2 -2
  113. package/src/prompts/system/system-prompt.md +2 -1
  114. package/src/prompts/tools/chunk-edit.md +219 -0
  115. package/src/prompts/tools/debug.md +43 -0
  116. package/src/prompts/tools/grep.md +3 -0
  117. package/src/prompts/tools/lsp.md +5 -5
  118. package/src/prompts/tools/read-chunk.md +17 -0
  119. package/src/prompts/tools/read.md +19 -5
  120. package/src/sdk.ts +190 -154
  121. package/src/secrets/obfuscator.ts +1 -1
  122. package/src/session/agent-session.ts +306 -256
  123. package/src/session/agent-storage.ts +12 -12
  124. package/src/session/compaction/branch-summarization.ts +3 -3
  125. package/src/session/compaction/compaction.ts +5 -6
  126. package/src/session/compaction/utils.ts +3 -3
  127. package/src/session/history-storage.ts +62 -19
  128. package/src/session/messages.ts +3 -3
  129. package/src/session/session-dump-format.ts +203 -0
  130. package/src/session/session-storage.ts +4 -2
  131. package/src/session/streaming-output.ts +1 -1
  132. package/src/session/tool-choice-queue.ts +213 -0
  133. package/src/slash-commands/builtin-registry.ts +56 -8
  134. package/src/ssh/connection-manager.ts +2 -2
  135. package/src/ssh/sshfs-mount.ts +5 -5
  136. package/src/stt/downloader.ts +4 -4
  137. package/src/stt/recorder.ts +4 -4
  138. package/src/stt/transcriber.ts +2 -2
  139. package/src/system-prompt.ts +21 -13
  140. package/src/task/agents.ts +5 -6
  141. package/src/task/commands.ts +2 -5
  142. package/src/task/executor.ts +4 -4
  143. package/src/task/index.ts +3 -4
  144. package/src/task/template.ts +2 -2
  145. package/src/task/worktree.ts +4 -4
  146. package/src/tools/ask.ts +2 -3
  147. package/src/tools/ast-edit.ts +7 -7
  148. package/src/tools/ast-grep.ts +7 -7
  149. package/src/tools/auto-generated-guard.ts +36 -41
  150. package/src/tools/await-tool.ts +2 -2
  151. package/src/tools/bash.ts +5 -23
  152. package/src/tools/browser.ts +4 -5
  153. package/src/tools/calculator.ts +2 -3
  154. package/src/tools/cancel-job.ts +2 -2
  155. package/src/tools/checkpoint.ts +3 -3
  156. package/src/tools/debug.ts +1007 -0
  157. package/src/tools/exit-plan-mode.ts +2 -3
  158. package/src/tools/fetch.ts +67 -3
  159. package/src/tools/find.ts +4 -5
  160. package/src/tools/fs-cache-invalidation.ts +5 -0
  161. package/src/tools/gemini-image.ts +13 -5
  162. package/src/tools/gh.ts +10 -11
  163. package/src/tools/grep.ts +57 -9
  164. package/src/tools/index.ts +44 -22
  165. package/src/tools/inspect-image.ts +4 -4
  166. package/src/tools/output-meta.ts +1 -1
  167. package/src/tools/python.ts +19 -6
  168. package/src/tools/read.ts +198 -67
  169. package/src/tools/render-mermaid.ts +2 -3
  170. package/src/tools/render-utils.ts +20 -6
  171. package/src/tools/renderers.ts +3 -1
  172. package/src/tools/report-tool-issue.ts +80 -0
  173. package/src/tools/resolve.ts +70 -39
  174. package/src/tools/search-tool-bm25.ts +2 -2
  175. package/src/tools/ssh.ts +2 -2
  176. package/src/tools/todo-write.ts +2 -2
  177. package/src/tools/tool-timeouts.ts +1 -0
  178. package/src/tools/write.ts +5 -6
  179. package/src/tui/tree-list.ts +3 -1
  180. package/src/utils/clipboard.ts +80 -0
  181. package/src/utils/commit-message-generator.ts +2 -3
  182. package/src/utils/edit-mode.ts +49 -0
  183. package/src/utils/file-display-mode.ts +6 -5
  184. package/src/utils/file-mentions.ts +8 -7
  185. package/src/utils/git.ts +4 -4
  186. package/src/utils/image-loading.ts +98 -0
  187. package/src/utils/title-generator.ts +2 -3
  188. package/src/utils/tools-manager.ts +6 -6
  189. package/src/web/scrapers/choosealicense.ts +1 -1
  190. package/src/web/search/index.ts +3 -3
  191. package/src/autoresearch/command-initialize.md +0 -34
  192. package/src/patch/diff.ts +0 -433
  193. package/src/patch/index.ts +0 -888
  194. package/src/patch/parser.ts +0 -532
  195. package/src/patch/types.ts +0 -292
  196. package/src/prompts/agents/oracle.md +0 -77
  197. package/src/tools/pending-action.ts +0 -49
  198. package/src/utils/child-process.ts +0 -88
  199. package/src/utils/frontmatter.ts +0 -117
  200. package/src/utils/image-input.ts +0 -274
  201. package/src/utils/mime.ts +0 -53
  202. 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
+ }
@@ -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
 
package/src/utils/git.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import { isEnoent, Snowflake } from "@oh-my-pi/pi-utils";
4
+ import { $which, isEnoent, Snowflake } from "@oh-my-pi/pi-utils";
5
5
  import {
6
6
  parseDiffHunks as parseCommitDiffHunks,
7
7
  parseFileDiffs,
@@ -162,7 +162,7 @@ function normalizeStdin(input: CommandOptions["stdin"]): "ignore" | Uint8Array {
162
162
  }
163
163
 
164
164
  function ensureAvailable(): void {
165
- if (!Bun.which("git")) {
165
+ if (!$which("git")) {
166
166
  throw new Error("git is not installed.");
167
167
  }
168
168
  }
@@ -1334,13 +1334,13 @@ function formatGhFailure(args: readonly string[], stdout: string, stderr: string
1334
1334
  export const github = {
1335
1335
  /** Check if `gh` CLI is installed. */
1336
1336
  available(): boolean {
1337
- return Boolean(Bun.which("gh"));
1337
+ return Boolean($which("gh"));
1338
1338
  },
1339
1339
 
1340
1340
  /** Run a raw `gh` CLI command. Does not throw on non-zero exit. */
1341
1341
  async run(cwd: string, args: string[], signal?: AbortSignal, options?: GhCommandOptions): Promise<GhCommandResult> {
1342
1342
  throwIfAborted(signal);
1343
- if (!Bun.which("gh")) {
1343
+ if (!$which("gh")) {
1344
1344
  throw new ToolError("GitHub CLI (gh) is not installed. Install it from https://cli.github.com/.");
1345
1345
  }
1346
1346
  try {