@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
@@ -1,12 +1,17 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { getProjectDir, getProjectPromptsDir, getPromptsDir, logger } from "@oh-my-pi/pi-utils";
4
- import Handlebars from "handlebars";
5
- import { computeLineHash } from "../patch/hashline";
3
+ import { type ChunkAnchorStyle, formatAnchor } from "@oh-my-pi/pi-natives";
4
+ import {
5
+ getProjectDir,
6
+ getProjectPromptsDir,
7
+ getPromptsDir,
8
+ logger,
9
+ parseFrontmatter,
10
+ prompt,
11
+ } from "@oh-my-pi/pi-utils";
12
+ import { computeLineHash } from "../edit/line-hash";
6
13
  import { jtdToTypeScript } from "../tools/jtd-to-typescript";
7
14
  import { parseCommandArgs, substituteArgs } from "../utils/command-args";
8
- import { parseFrontmatter } from "../utils/frontmatter";
9
- import { formatPromptContent } from "../utils/prompt-format";
10
15
 
11
16
  /**
12
17
  * Represents a prompt template loaded from a markdown file
@@ -18,215 +23,7 @@ export interface PromptTemplate {
18
23
  source: string; // e.g., "(user)", "(project)", "(project:frontend)"
19
24
  }
20
25
 
21
- export interface TemplateContext extends Record<string, unknown> {
22
- args?: string[];
23
- ARGUMENTS?: string;
24
- arguments?: string;
25
- }
26
-
27
- const handlebars = Handlebars.create();
28
-
29
- handlebars.registerHelper("arg", function (this: TemplateContext, index: number | string): string {
30
- const args = this.args ?? [];
31
- const parsedIndex = typeof index === "number" ? index : Number.parseInt(index, 10);
32
- if (!Number.isFinite(parsedIndex)) return "";
33
- const zeroBased = parsedIndex - 1;
34
- if (zeroBased < 0) return "";
35
- return args[zeroBased] ?? "";
36
- });
37
-
38
- /**
39
- * {{#list items prefix="- " suffix="" join="\n"}}{{this}}{{/list}}
40
- * Renders an array with customizable prefix, suffix, and join separator.
41
- * Note: Use \n in join for newlines (will be unescaped automatically).
42
- */
43
- handlebars.registerHelper(
44
- "list",
45
- function (this: unknown, context: unknown[], options: Handlebars.HelperOptions): string {
46
- if (!Array.isArray(context) || context.length === 0) return "";
47
- const prefix = (options.hash.prefix as string) ?? "";
48
- const suffix = (options.hash.suffix as string) ?? "";
49
- const rawSeparator = (options.hash.join as string) ?? "\n";
50
- const separator = rawSeparator.replace(/\\n/g, "\n").replace(/\\t/g, "\t");
51
- return context.map(item => `${prefix}${options.fn(item)}${suffix}`).join(separator);
52
- },
53
- );
54
-
55
- /**
56
- * {{join array ", "}}
57
- * Joins an array with a separator (default: ", ").
58
- */
59
- handlebars.registerHelper("join", (context: unknown[], separator?: unknown): string => {
60
- if (!Array.isArray(context)) return "";
61
- const sep = typeof separator === "string" ? separator : ", ";
62
- return context.join(sep);
63
- });
64
-
65
- /**
66
- * {{default value "fallback"}}
67
- * Returns the value if truthy, otherwise returns the fallback.
68
- */
69
- handlebars.registerHelper("default", (value: unknown, defaultValue: unknown): unknown => value || defaultValue);
70
-
71
- /**
72
- * {{pluralize count "item" "items"}}
73
- * Returns "1 item" or "5 items" based on count.
74
- */
75
- handlebars.registerHelper(
76
- "pluralize",
77
- (count: number, singular: string, plural: string): string => `${count} ${count === 1 ? singular : plural}`,
78
- );
79
-
80
- /**
81
- * {{#when value "==" compare}}...{{else}}...{{/when}}
82
- * Conditional block with comparison operators: ==, ===, !=, !==, >, <, >=, <=
83
- */
84
- handlebars.registerHelper(
85
- "when",
86
- function (this: unknown, lhs: unknown, operator: string, rhs: unknown, options: Handlebars.HelperOptions): string {
87
- const ops: Record<string, (a: unknown, b: unknown) => boolean> = {
88
- "==": (a, b) => a === b,
89
- "===": (a, b) => a === b,
90
- "!=": (a, b) => a !== b,
91
- "!==": (a, b) => a !== b,
92
- ">": (a, b) => (a as number) > (b as number),
93
- "<": (a, b) => (a as number) < (b as number),
94
- ">=": (a, b) => (a as number) >= (b as number),
95
- "<=": (a, b) => (a as number) <= (b as number),
96
- };
97
- const fn = ops[operator];
98
- if (!fn) return options.inverse(this);
99
- return fn(lhs, rhs) ? options.fn(this) : options.inverse(this);
100
- },
101
- );
102
-
103
- /**
104
- * {{#ifAny a b c}}...{{else}}...{{/ifAny}}
105
- * True if any argument is truthy.
106
- */
107
- handlebars.registerHelper("ifAny", function (this: unknown, ...args: unknown[]): string {
108
- const options = args.pop() as Handlebars.HelperOptions;
109
- return args.some(Boolean) ? options.fn(this) : options.inverse(this);
110
- });
111
-
112
- /**
113
- * {{#ifAll a b c}}...{{else}}...{{/ifAll}}
114
- * True if all arguments are truthy.
115
- */
116
- handlebars.registerHelper("ifAll", function (this: unknown, ...args: unknown[]): string {
117
- const options = args.pop() as Handlebars.HelperOptions;
118
- return args.every(Boolean) ? options.fn(this) : options.inverse(this);
119
- });
120
-
121
- /**
122
- * {{#table rows headers="Col1|Col2"}}{{col1}}|{{col2}}{{/table}}
123
- * Generates a markdown table from an array of objects.
124
- */
125
- handlebars.registerHelper(
126
- "table",
127
- function (this: unknown, context: unknown[], options: Handlebars.HelperOptions): string {
128
- if (!Array.isArray(context) || context.length === 0) return "";
129
- const headersStr = options.hash.headers as string | undefined;
130
- const headers = headersStr?.split("|") ?? [];
131
- const separator = headers.map(() => "---").join(" | ");
132
- const headerRow = headers.length > 0 ? `| ${headers.join(" | ")} |\n| ${separator} |\n` : "";
133
- const rows = context.map(item => `| ${options.fn(item).trim()} |`).join("\n");
134
- return headerRow + rows;
135
- },
136
- );
137
-
138
- /**
139
- * {{#codeblock lang="diff"}}...{{/codeblock}}
140
- * Wraps content in a fenced code block.
141
- */
142
- handlebars.registerHelper("codeblock", function (this: unknown, options: Handlebars.HelperOptions): string {
143
- const lang = (options.hash.lang as string) ?? "";
144
- const content = options.fn(this).trim();
145
- return `\`\`\`${lang}\n${content}\n\`\`\``;
146
- });
147
-
148
- /**
149
- * {{#xml "tag"}}content{{/xml}}
150
- * Wraps content in XML-style tags. Returns empty string if content is empty.
151
- */
152
- handlebars.registerHelper("xml", function (this: unknown, tag: string, options: Handlebars.HelperOptions): string {
153
- const content = options.fn(this).trim();
154
- if (!content) return "";
155
- return `<${tag}>\n${content}\n</${tag}>`;
156
- });
157
-
158
- /**
159
- * {{escapeXml value}}
160
- * Escapes XML special characters: & < > "
161
- */
162
- handlebars.registerHelper("escapeXml", (value: unknown): string => {
163
- if (value == null) return "";
164
- return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
165
- });
166
-
167
- /**
168
- * {{len array}}
169
- * Returns the length of an array or string.
170
- */
171
- handlebars.registerHelper("len", (value: unknown): number => {
172
- if (Array.isArray(value)) return value.length;
173
- if (typeof value === "string") return value.length;
174
- return 0;
175
- });
176
-
177
- /**
178
- * {{add a b}}
179
- * Adds two numbers.
180
- */
181
- handlebars.registerHelper("add", (a: number, b: number): number => (a ?? 0) + (b ?? 0));
182
-
183
- /**
184
- * {{sub a b}}
185
- * Subtracts b from a.
186
- */
187
- handlebars.registerHelper("sub", (a: number, b: number): number => (a ?? 0) - (b ?? 0));
188
-
189
- /**
190
- * {{#has collection item}}...{{else}}...{{/has}}
191
- * Checks if an array includes an item or if a Set/Map has a key.
192
- */
193
- handlebars.registerHelper(
194
- "has",
195
- function (this: unknown, collection: unknown, item: unknown, options: Handlebars.HelperOptions): string {
196
- let found = false;
197
- if (Array.isArray(collection)) {
198
- found = collection.includes(item);
199
- } else if (collection instanceof Set) {
200
- found = collection.has(item);
201
- } else if (collection instanceof Map) {
202
- found = collection.has(item);
203
- } else if (collection && typeof collection === "object") {
204
- if (typeof item === "string" || typeof item === "number" || typeof item === "symbol") {
205
- found = item in collection;
206
- }
207
- }
208
- return found ? options.fn(this) : options.inverse(this);
209
- },
210
- );
211
-
212
- /**
213
- * {{includes array item}}
214
- * Returns true if array includes item. For use in other helpers.
215
- */
216
- handlebars.registerHelper("includes", (collection: unknown, item: unknown): boolean => {
217
- if (Array.isArray(collection)) return collection.includes(item);
218
- if (collection instanceof Set) return collection.has(item);
219
- if (collection instanceof Map) return collection.has(item);
220
- return false;
221
- });
222
-
223
- /**
224
- * {{not value}}
225
- * Returns logical NOT of value. For use in subexpressions.
226
- */
227
- handlebars.registerHelper("not", (value: unknown): boolean => !value);
228
-
229
- handlebars.registerHelper("jtdToTypeScript", (schema: unknown): string => {
26
+ prompt.registerHelper("jtdToTypeScript", (schema: unknown): string => {
230
27
  try {
231
28
  return jtdToTypeScript(schema);
232
29
  } catch {
@@ -234,8 +31,6 @@ handlebars.registerHelper("jtdToTypeScript", (schema: unknown): string => {
234
31
  }
235
32
  });
236
33
 
237
- handlebars.registerHelper("jsonStringify", (value: unknown): string => JSON.stringify(value));
238
-
239
34
  /**
240
35
  * Renders a section separator:
241
36
  *
@@ -247,7 +42,7 @@ export function sectionSeparator(name: string): string {
247
42
  return `\n\n═══════════${name}═══════════\n`;
248
43
  }
249
44
 
250
- handlebars.registerHelper("SECTION_SEPERATOR", (name: unknown): string => sectionSeparator(String(name)));
45
+ prompt.registerHelper("SECTION_SEPERATOR", (name: unknown): string => sectionSeparator(String(name)));
251
46
 
252
47
  function formatHashlineRef(lineNum: unknown, content: unknown): { num: number; text: string; ref: string } {
253
48
  const num = typeof lineNum === "number" ? lineNum : Number.parseInt(String(lineNum), 10);
@@ -261,7 +56,7 @@ function formatHashlineRef(lineNum: unknown, content: unknown): { num: number; t
261
56
  * {{href lineNum "content"}} — compute a real hashline ref for prompt examples.
262
57
  * Returns `"lineNum#hash"` using the actual hash algorithm.
263
58
  */
264
- handlebars.registerHelper("href", (lineNum: unknown, content: unknown): string => {
59
+ prompt.registerHelper("href", (lineNum: unknown, content: unknown): string => {
265
60
  const { ref } = formatHashlineRef(lineNum, content);
266
61
  return JSON.stringify(ref);
267
62
  });
@@ -270,11 +65,40 @@ handlebars.registerHelper("href", (lineNum: unknown, content: unknown): string =
270
65
  * {{hline lineNum "content"}} — format a full read-style line with prefix.
271
66
  * Returns `"lineNum#hash:content"`.
272
67
  */
273
- handlebars.registerHelper("hline", (lineNum: unknown, content: unknown): string => {
68
+ prompt.registerHelper("hline", (lineNum: unknown, content: unknown): string => {
274
69
  const { ref, text } = formatHashlineRef(lineNum, content);
275
70
  return `${ref}:${text}`;
276
71
  });
277
72
 
73
+ /**
74
+ * {{anchor name checksum}} — render a branch anchor tag using the current anchor style.
75
+ * Style is resolved from the template context (`anchorStyle`) or defaults to "full".
76
+ */
77
+ prompt.registerHelper("anchor", function (this: prompt.TemplateContext, name: string, checksum: string): string {
78
+ const style = (this.anchorStyle as ChunkAnchorStyle) ?? "full";
79
+ return formatAnchor(name, checksum, style);
80
+ });
81
+
82
+ /**
83
+ * {{sel "parent_Name.child_Name"}} — render a chunk path for `sel` fields in examples.
84
+ * In `full` style the path is returned as-is (`class_Server.fn_start`).
85
+ * In `kind` style each segment is trimmed to its kind prefix (`class.fn`).
86
+ * In `bare` style the path is omitted (the model uses only `crc` to identify chunks).
87
+ */
88
+ prompt.registerHelper("sel", function (this: prompt.TemplateContext, chunkPath: string): string {
89
+ const style = (this.anchorStyle as ChunkAnchorStyle) ?? "full";
90
+ if (style === "full") return chunkPath;
91
+ if (style === "bare") return "";
92
+ // kind: trim each segment to its kind prefix (before the first `_`)
93
+ return chunkPath
94
+ .split(".")
95
+ .map(seg => {
96
+ const idx = seg.indexOf("_");
97
+ return idx === -1 ? seg : seg.slice(0, idx);
98
+ })
99
+ .join(".");
100
+ });
101
+
278
102
  const INLINE_ARG_SHELL_PATTERN = /\$(?:ARGUMENTS|@(?:\[\d+(?::\d*)?\])?|\d+)/;
279
103
  const INLINE_ARG_TEMPLATE_PATTERN = /\{\{[\s\S]*?(?:\b(?:arguments|ARGUMENTS|args)\b|\barg\s+[^}]+)[\s\S]*?\}\}/;
280
104
 
@@ -297,12 +121,6 @@ export function appendInlineArgsFallback(
297
121
  return `${rendered}\n\n${argsText}`;
298
122
  }
299
123
 
300
- export function renderPromptTemplate(template: string, context: TemplateContext = {}): string {
301
- const compiled = handlebars.compile(template, { noEscape: true, strict: false });
302
- const rendered = compiled(context ?? {});
303
- return formatPromptContent(rendered, { renderPhase: "post-render" });
304
- }
305
-
306
124
  /**
307
125
  * Recursively scan a directory for .md files (and symlinks to .md files) and load them as prompt templates
308
126
  */
@@ -429,7 +247,7 @@ export function expandPromptTemplate(text: string, templates: PromptTemplate[]):
429
247
  const argsText = args.join(" ");
430
248
  const usesInlineArgPlaceholders = templateUsesInlineArgPlaceholders(template.content);
431
249
  const substituted = substituteArgs(template.content, args);
432
- const rendered = renderPromptTemplate(substituted, { args, ARGUMENTS: argsText, arguments: argsText });
250
+ const rendered = prompt.render(substituted, { args, ARGUMENTS: argsText, arguments: argsText });
433
251
  return appendInlineArgsFallback(rendered, argsText, usesInlineArgPlaceholders);
434
252
  }
435
253
 
@@ -55,8 +55,10 @@ async function executeCommand(commandConfig: string): Promise<string | undefined
55
55
  async function runShellCommand(command: string, timeoutMs: number): Promise<string | undefined> {
56
56
  try {
57
57
  let output = "";
58
- const result = await executeShell({ command, timeoutMs }, chunk => {
59
- output += chunk;
58
+ const result = await executeShell({ command, timeoutMs }, (err, chunk) => {
59
+ if (!err) {
60
+ output += chunk;
61
+ }
60
62
  });
61
63
  if (result.timedOut || result.exitCode !== 0) {
62
64
  return undefined;
@@ -194,6 +194,15 @@ export const SETTINGS_SCHEMA = {
194
194
  // ────────────────────────────────────────────────────────────────────────
195
195
  lastChangelogVersion: { type: "string", default: undefined },
196
196
 
197
+ autoResume: {
198
+ type: "boolean",
199
+ default: false,
200
+ ui: {
201
+ tab: "interaction",
202
+ label: "Auto Resume",
203
+ description: "Automatically resume the most recent session in the current directory",
204
+ },
205
+ },
197
206
  shellPath: { type: "string", default: undefined },
198
207
 
199
208
  extensions: { type: "array", default: EMPTY_STRING_ARRAY },
@@ -797,6 +806,38 @@ export const SETTINGS_SCHEMA = {
797
806
 
798
807
  "compaction.remoteEndpoint": { type: "string", default: undefined },
799
808
 
809
+ // Idle compaction
810
+ "compaction.idleEnabled": {
811
+ type: "boolean",
812
+ default: false,
813
+ ui: {
814
+ tab: "context",
815
+ label: "Idle Compaction",
816
+ description: "Compact context while idle when token count exceeds threshold",
817
+ },
818
+ },
819
+
820
+ "compaction.idleThresholdTokens": {
821
+ type: "number",
822
+ default: 200000,
823
+ ui: {
824
+ tab: "context",
825
+ label: "Idle Compaction Threshold",
826
+ description: "Token count above which idle compaction triggers",
827
+ submenu: true,
828
+ },
829
+ },
830
+
831
+ "compaction.idleTimeoutSeconds": {
832
+ type: "number",
833
+ default: 300,
834
+ ui: {
835
+ tab: "context",
836
+ label: "Idle Compaction Delay",
837
+ description: "Seconds to wait while idle before compacting",
838
+ submenu: true,
839
+ },
840
+ },
800
841
  // Branch summaries
801
842
  "branchSummary.enabled": {
802
843
  type: "boolean",
@@ -908,12 +949,12 @@ export const SETTINGS_SCHEMA = {
908
949
  // Edit tool
909
950
  "edit.mode": {
910
951
  type: "enum",
911
- values: ["replace", "patch", "hashline"] as const,
952
+ values: ["replace", "patch", "hashline", "chunk"] as const,
912
953
  default: "hashline",
913
954
  ui: {
914
955
  tab: "editing",
915
956
  label: "Edit Mode",
916
- description: "Select the edit tool variant (replace, patch, or hashline)",
957
+ description: "Select the edit tool variant (replace, patch, hashline, or chunk)",
917
958
  },
918
959
  },
919
960
 
@@ -989,6 +1030,38 @@ export const SETTINGS_SCHEMA = {
989
1030
  },
990
1031
  },
991
1032
 
1033
+ "read.prosechunks": {
1034
+ type: "boolean",
1035
+ default: false,
1036
+ ui: {
1037
+ tab: "editing",
1038
+ label: "Prose Chunks",
1039
+ description: "Enable chunk rendering for prose files in chunk edit mode",
1040
+ },
1041
+ },
1042
+
1043
+ "read.explorechunks": {
1044
+ type: "boolean",
1045
+ default: false,
1046
+ ui: {
1047
+ tab: "editing",
1048
+ label: "Explore Chunks",
1049
+ description: "Show chunk tree without checksums for read-only agents like explore",
1050
+ },
1051
+ },
1052
+
1053
+ "read.anchorstyle": {
1054
+ type: "enum",
1055
+ values: ["full", "kind", "bare"],
1056
+ default: "full",
1057
+ ui: {
1058
+ tab: "editing",
1059
+ label: "Anchor Style",
1060
+ description: "Render chunk anchors with full names, kind prefixes, or checksum-only tags",
1061
+ submenu: true,
1062
+ },
1063
+ },
1064
+
992
1065
  // LSP
993
1066
  "lsp.enabled": {
994
1067
  type: "boolean",
@@ -1173,6 +1246,16 @@ export const SETTINGS_SCHEMA = {
1173
1246
  },
1174
1247
  },
1175
1248
 
1249
+ "debug.enabled": {
1250
+ type: "boolean",
1251
+ default: true,
1252
+ ui: {
1253
+ tab: "tools",
1254
+ label: "Debug",
1255
+ description: "Enable the debug tool for DAP-based debugging",
1256
+ },
1257
+ },
1258
+
1176
1259
  "calc.enabled": {
1177
1260
  type: "boolean",
1178
1261
  default: false,
@@ -1601,6 +1684,16 @@ export const SETTINGS_SCHEMA = {
1601
1684
 
1602
1685
  "commit.changelogMaxDiffChars": { type: "number", default: 120000 },
1603
1686
 
1687
+ "dev.autoqa": {
1688
+ type: "boolean",
1689
+ default: false,
1690
+ ui: {
1691
+ tab: "tools",
1692
+ label: "Auto QA",
1693
+ description: "Enable automated tool issue reporting (report_tool_issue) for all agents",
1694
+ },
1695
+ },
1696
+
1604
1697
  "thinkingBudgets.minimal": { type: "number", default: 1024 },
1605
1698
 
1606
1699
  "thinkingBudgets.low": { type: "number", default: 2048 },
@@ -1701,6 +1794,9 @@ export interface CompactionSettings {
1701
1794
  autoContinue: boolean;
1702
1795
  remoteEnabled: boolean;
1703
1796
  remoteEndpoint: string | undefined;
1797
+ idleEnabled: boolean;
1798
+ idleThresholdTokens: number;
1799
+ idleTimeoutSeconds: number;
1704
1800
  }
1705
1801
 
1706
1802
  export interface ContextPromotionSettings {
@@ -13,22 +13,15 @@
13
13
 
14
14
  import * as fs from "node:fs";
15
15
  import * as path from "node:path";
16
- import {
17
- getAgentDbPath,
18
- getAgentDir,
19
- getProjectDir,
20
- isEnoent,
21
- logger,
22
- procmgr,
23
- setDefaultTabWidth,
24
- } from "@oh-my-pi/pi-utils";
16
+ import { setDefaultTabWidth } from "@oh-my-pi/pi-natives";
17
+ import { getAgentDbPath, getAgentDir, getProjectDir, isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils";
25
18
  import { YAML } from "bun";
26
19
  import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings";
27
20
  import type { ModelRole } from "../config/model-registry";
28
21
  import { loadCapability } from "../discovery";
29
22
  import { isLightTheme, setAutoThemeMapping, setColorBlindMode, setSymbolPreset } from "../modes/theme/theme";
30
- import { type EditMode, normalizeEditMode } from "../patch";
31
23
  import { AgentStorage } from "../session/agent-storage";
24
+ import { type EditMode, normalizeEditMode } from "../utils/edit-mode";
32
25
  import { withFileLock } from "./file-lock";
33
26
  import {
34
27
  type BashInterceptorRule,
@@ -68,15 +61,6 @@ export interface SettingsOptions {
68
61
  // Path Utilities
69
62
  // ═══════════════════════════════════════════════════════════════════════════
70
63
 
71
- /**
72
- * Parse a dotted path into segments.
73
- * "compaction.enabled" → ["compaction", "enabled"]
74
- * "theme.dark" → ["theme", "dark"]
75
- */
76
- function parsePath(path: string): string[] {
77
- return path.split(".");
78
- }
79
-
80
64
  /**
81
65
  * Get a nested value from an object by path segments.
82
66
  */
@@ -144,7 +128,7 @@ export class Settings {
144
128
 
145
129
  if (options.overrides) {
146
130
  for (const [key, value] of Object.entries(options.overrides)) {
147
- setByPath(this.#overrides, parsePath(key), value);
131
+ setByPath(this.#overrides, key.split("."), value);
148
132
  }
149
133
  }
150
134
  }
@@ -207,7 +191,7 @@ export class Settings {
207
191
  * Returns the merged value from global + project + overrides, or the default.
208
192
  */
209
193
  get<P extends SettingPath>(path: P): SettingValue<P> {
210
- const segments = parsePath(path);
194
+ const segments = path.split(".");
211
195
  const value = getByPath(this.#merged, segments);
212
196
  if (value !== undefined) {
213
197
  return value as SettingValue<P>;
@@ -222,7 +206,7 @@ export class Settings {
222
206
  */
223
207
  set<P extends SettingPath>(path: P, value: SettingValue<P>): void {
224
208
  const prev = this.get(path);
225
- const segments = parsePath(path);
209
+ const segments = path.split(".");
226
210
  setByPath(this.#global, segments, value);
227
211
  this.#modified.add(path);
228
212
  this.#rebuildMerged();
@@ -239,7 +223,7 @@ export class Settings {
239
223
  * Apply runtime overrides (not persisted).
240
224
  */
241
225
  override<P extends SettingPath>(path: P, value: SettingValue<P>): void {
242
- const segments = parsePath(path);
226
+ const segments = path.split(".");
243
227
  setByPath(this.#overrides, segments, value);
244
228
  this.#rebuildMerged();
245
229
  }
@@ -248,7 +232,7 @@ export class Settings {
248
232
  * Clear a runtime override.
249
233
  */
250
234
  clearOverride(path: SettingPath): void {
251
- const segments = parsePath(path);
235
+ const segments = path.split(".");
252
236
  let current = this.#overrides;
253
237
  for (let i = 0; i < segments.length - 1; i++) {
254
238
  const segment = segments[i];
@@ -276,6 +260,21 @@ export class Settings {
276
260
  }
277
261
  }
278
262
 
263
+ async cloneForCwd(cwd: string): Promise<Settings> {
264
+ const cloned = new Settings({
265
+ cwd,
266
+ agentDir: this.#agentDir,
267
+ inMemory: !this.#persist,
268
+ });
269
+ cloned.#storage = this.#storage;
270
+ cloned.#global = structuredClone(this.#global);
271
+ cloned.#project = this.#persist ? await cloned.#loadProjectSettings() : structuredClone(this.#project);
272
+ cloned.#overrides = structuredClone(this.#overrides);
273
+ cloned.#rebuildMerged();
274
+ cloned.#fireAllHooks();
275
+ return cloned;
276
+ }
277
+
279
278
  // ─────────────────────────────────────────────────────────────────────────
280
279
  // Accessors
281
280
  // ─────────────────────────────────────────────────────────────────────────
@@ -320,7 +319,7 @@ export class Settings {
320
319
 
321
320
  /**
322
321
  * Get the edit variant for a specific model.
323
- * Returns "patch", "replace", "hashline", or null (use global default).
322
+ * Returns "patch", "replace", "hashline", "chunk", or null (use global default).
324
323
  */
325
324
  getEditVariantForModel(model: string | undefined): EditMode | null {
326
325
  if (!model) return null;
@@ -562,7 +561,7 @@ export class Settings {
562
561
 
563
562
  // Apply only our modified paths
564
563
  for (const modPath of modifiedPaths) {
565
- const segments = parsePath(modPath);
564
+ const segments = modPath.split(".");
566
565
  const value = getByPath(this.#global, segments);
567
566
  setByPath(current, segments, value);
568
567
  }