@oh-my-pi/pi-coding-agent 14.9.9 → 15.0.1

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 (230) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/examples/extensions/plan-mode.ts +0 -1
  3. package/package.json +9 -9
  4. package/scripts/build-binary.ts +5 -0
  5. package/scripts/format-prompts.ts +1 -1
  6. package/src/autoresearch/helpers.ts +17 -0
  7. package/src/autoresearch/tools/log-experiment.ts +9 -17
  8. package/src/autoresearch/tools/run-experiment.ts +2 -17
  9. package/src/capability/skill.ts +7 -0
  10. package/src/cli/args.ts +2 -2
  11. package/src/cli/list-models.ts +1 -1
  12. package/src/cli/shell-cli.ts +3 -13
  13. package/src/cli/update-cli.ts +1 -1
  14. package/src/cli.ts +11 -29
  15. package/src/commands/acp.ts +24 -0
  16. package/src/commands/launch.ts +6 -4
  17. package/src/commit/agentic/prompts/system.md +1 -1
  18. package/src/commit/agentic/tools/propose-changelog.ts +8 -1
  19. package/src/commit/analysis/conventional.ts +8 -66
  20. package/src/commit/map-reduce/reduce-phase.ts +6 -65
  21. package/src/commit/pipeline.ts +2 -2
  22. package/src/commit/shared-llm.ts +89 -0
  23. package/src/config/config-file.ts +210 -0
  24. package/src/config/model-equivalence.ts +8 -11
  25. package/src/config/model-registry.ts +13 -2
  26. package/src/config/model-resolver.ts +31 -4
  27. package/src/config/settings-schema.ts +102 -1
  28. package/src/config/settings.ts +1 -1
  29. package/src/config.ts +3 -219
  30. package/src/edit/index.ts +22 -1
  31. package/src/edit/modes/patch.ts +10 -0
  32. package/src/edit/modes/replace.ts +3 -0
  33. package/src/edit/renderer.ts +17 -1
  34. package/src/eval/js/context-manager.ts +1 -1
  35. package/src/eval/js/executor.ts +3 -0
  36. package/src/eval/js/shared/rewrite-imports.ts +122 -50
  37. package/src/eval/js/shared/runtime.ts +31 -4
  38. package/src/eval/js/tool-bridge.ts +43 -21
  39. package/src/eval/py/executor.ts +5 -0
  40. package/src/exa/factory.ts +2 -2
  41. package/src/exa/mcp-client.ts +74 -1
  42. package/src/exec/bash-executor.ts +5 -1
  43. package/src/export/html/template.generated.ts +1 -1
  44. package/src/export/html/template.js +0 -11
  45. package/src/extensibility/extensions/runner.ts +55 -2
  46. package/src/extensibility/extensions/types.ts +98 -221
  47. package/src/extensibility/hooks/types.ts +89 -314
  48. package/src/extensibility/shared-events.ts +343 -0
  49. package/src/extensibility/skills.ts +42 -1
  50. package/src/goals/index.ts +3 -0
  51. package/src/goals/runtime.ts +500 -0
  52. package/src/goals/state.ts +37 -0
  53. package/src/goals/tools/goal-tool.ts +237 -0
  54. package/src/hashline/anchors.ts +2 -2
  55. package/src/hindsight/mental-models.ts +1 -1
  56. package/src/internal-urls/agent-protocol.ts +1 -20
  57. package/src/internal-urls/artifact-protocol.ts +1 -19
  58. package/src/internal-urls/docs-index.generated.ts +9 -10
  59. package/src/internal-urls/index.ts +1 -0
  60. package/src/internal-urls/issue-pr-protocol.ts +577 -0
  61. package/src/internal-urls/registry-helpers.ts +25 -0
  62. package/src/internal-urls/router.ts +6 -3
  63. package/src/internal-urls/types.ts +22 -1
  64. package/src/main.ts +24 -11
  65. package/src/mcp/oauth-flow.ts +20 -0
  66. package/src/modes/acp/acp-agent.ts +412 -71
  67. package/src/modes/acp/acp-client-bridge.ts +152 -0
  68. package/src/modes/acp/acp-event-mapper.ts +180 -15
  69. package/src/modes/acp/terminal-auth.ts +37 -0
  70. package/src/modes/components/assistant-message.ts +14 -8
  71. package/src/modes/components/bash-execution.ts +24 -63
  72. package/src/modes/components/custom-message.ts +14 -40
  73. package/src/modes/components/eval-execution.ts +27 -57
  74. package/src/modes/components/execution-shared.ts +102 -0
  75. package/src/modes/components/hook-message.ts +17 -49
  76. package/src/modes/components/mcp-add-wizard.ts +26 -5
  77. package/src/modes/components/message-frame.ts +88 -0
  78. package/src/modes/components/model-selector.ts +1 -1
  79. package/src/modes/components/read-tool-group.ts +29 -1
  80. package/src/modes/components/session-observer-overlay.ts +6 -2
  81. package/src/modes/components/session-selector.ts +1 -1
  82. package/src/modes/components/status-line/segments.ts +55 -4
  83. package/src/modes/components/status-line/types.ts +4 -0
  84. package/src/modes/components/status-line.ts +28 -10
  85. package/src/modes/components/tool-execution.ts +7 -8
  86. package/src/modes/controllers/command-controller-shared.ts +108 -0
  87. package/src/modes/controllers/command-controller.ts +27 -10
  88. package/src/modes/controllers/event-controller.ts +60 -18
  89. package/src/modes/controllers/extension-ui-controller.ts +8 -2
  90. package/src/modes/controllers/input-controller.ts +85 -39
  91. package/src/modes/controllers/mcp-command-controller.ts +56 -61
  92. package/src/modes/controllers/ssh-command-controller.ts +18 -57
  93. package/src/modes/interactive-mode.ts +675 -39
  94. package/src/modes/print-mode.ts +16 -86
  95. package/src/modes/rpc/rpc-mode.ts +30 -88
  96. package/src/modes/runtime-init.ts +115 -0
  97. package/src/modes/theme/defaults/dark-poimandres.json +2 -0
  98. package/src/modes/theme/defaults/light-poimandres.json +2 -0
  99. package/src/modes/theme/theme.ts +18 -6
  100. package/src/modes/types.ts +20 -5
  101. package/src/modes/utils/context-usage.ts +13 -13
  102. package/src/modes/utils/ui-helpers.ts +25 -6
  103. package/src/plan-mode/approved-plan.ts +35 -1
  104. package/src/prompts/agents/designer.md +5 -5
  105. package/src/prompts/agents/explore.md +7 -7
  106. package/src/prompts/agents/init.md +9 -9
  107. package/src/prompts/agents/librarian.md +14 -14
  108. package/src/prompts/agents/plan.md +4 -4
  109. package/src/prompts/agents/reviewer.md +5 -5
  110. package/src/prompts/agents/task.md +10 -10
  111. package/src/prompts/commands/orchestrate.md +2 -2
  112. package/src/prompts/compaction/branch-summary.md +3 -3
  113. package/src/prompts/compaction/compaction-short-summary.md +7 -7
  114. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  115. package/src/prompts/compaction/compaction-summary.md +5 -5
  116. package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
  117. package/src/prompts/compaction/compaction-update-summary.md +11 -11
  118. package/src/prompts/goals/goal-budget-limit.md +16 -0
  119. package/src/prompts/goals/goal-continuation.md +28 -0
  120. package/src/prompts/goals/goal-mode-active.md +23 -0
  121. package/src/prompts/memories/consolidation.md +2 -2
  122. package/src/prompts/memories/read-path.md +1 -1
  123. package/src/prompts/memories/stage_one_input.md +1 -1
  124. package/src/prompts/memories/stage_one_system.md +5 -5
  125. package/src/prompts/review-request.md +4 -4
  126. package/src/prompts/system/agent-creation-architect.md +17 -17
  127. package/src/prompts/system/agent-creation-user.md +2 -2
  128. package/src/prompts/system/commit-message-system.md +2 -2
  129. package/src/prompts/system/custom-system-prompt.md +2 -2
  130. package/src/prompts/system/eager-todo.md +6 -6
  131. package/src/prompts/system/handoff-document.md +1 -1
  132. package/src/prompts/system/plan-mode-active.md +25 -24
  133. package/src/prompts/system/plan-mode-approved.md +4 -4
  134. package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
  135. package/src/prompts/system/plan-mode-reference.md +2 -2
  136. package/src/prompts/system/plan-mode-subagent.md +8 -8
  137. package/src/prompts/system/plan-mode-tool-decision-reminder.md +3 -3
  138. package/src/prompts/system/project-prompt.md +4 -4
  139. package/src/prompts/system/subagent-system-prompt.md +7 -7
  140. package/src/prompts/system/subagent-yield-reminder.md +4 -4
  141. package/src/prompts/system/system-prompt.md +72 -71
  142. package/src/prompts/system/ttsr-interrupt.md +1 -1
  143. package/src/prompts/tools/apply-patch.md +1 -1
  144. package/src/prompts/tools/ast-edit.md +3 -3
  145. package/src/prompts/tools/ast-grep.md +3 -3
  146. package/src/prompts/tools/bash.md +6 -0
  147. package/src/prompts/tools/browser.md +3 -3
  148. package/src/prompts/tools/checkpoint.md +3 -3
  149. package/src/prompts/tools/find.md +3 -3
  150. package/src/prompts/tools/github.md +2 -5
  151. package/src/prompts/tools/goal.md +13 -0
  152. package/src/prompts/tools/hashline.md +104 -116
  153. package/src/prompts/tools/image-gen.md +3 -3
  154. package/src/prompts/tools/irc.md +1 -1
  155. package/src/prompts/tools/lsp.md +2 -2
  156. package/src/prompts/tools/patch.md +6 -6
  157. package/src/prompts/tools/read.md +8 -7
  158. package/src/prompts/tools/replace.md +5 -5
  159. package/src/prompts/tools/resolve.md +6 -5
  160. package/src/prompts/tools/retain.md +1 -1
  161. package/src/prompts/tools/rewind.md +2 -2
  162. package/src/prompts/tools/search.md +2 -2
  163. package/src/prompts/tools/ssh.md +2 -2
  164. package/src/prompts/tools/task.md +12 -6
  165. package/src/prompts/tools/web-search.md +2 -2
  166. package/src/prompts/tools/write.md +3 -3
  167. package/src/sdk.ts +81 -17
  168. package/src/session/agent-session.ts +656 -125
  169. package/src/session/blob-store.ts +36 -3
  170. package/src/session/client-bridge.ts +81 -0
  171. package/src/session/compaction/errors.ts +31 -0
  172. package/src/session/compaction/index.ts +1 -0
  173. package/src/session/messages.ts +67 -2
  174. package/src/session/session-manager.ts +131 -12
  175. package/src/session/session-storage.ts +33 -15
  176. package/src/session/streaming-output.ts +309 -13
  177. package/src/slash-commands/acp-builtins.ts +46 -0
  178. package/src/slash-commands/builtin-registry.ts +717 -116
  179. package/src/slash-commands/helpers/context-report.ts +39 -0
  180. package/src/slash-commands/helpers/format.ts +23 -0
  181. package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
  182. package/src/slash-commands/helpers/mcp.ts +532 -0
  183. package/src/slash-commands/helpers/parse.ts +85 -0
  184. package/src/slash-commands/helpers/ssh.ts +193 -0
  185. package/src/slash-commands/helpers/todo.ts +279 -0
  186. package/src/slash-commands/helpers/usage-report.ts +91 -0
  187. package/src/slash-commands/types.ts +126 -0
  188. package/src/ssh/ssh-executor.ts +5 -0
  189. package/src/system-prompt.ts +4 -2
  190. package/src/task/executor.ts +27 -10
  191. package/src/task/index.ts +20 -1
  192. package/src/task/render.ts +27 -18
  193. package/src/task/types.ts +4 -0
  194. package/src/tools/ast-edit.ts +21 -120
  195. package/src/tools/ast-grep.ts +21 -119
  196. package/src/tools/bash-interactive.ts +9 -1
  197. package/src/tools/bash.ts +203 -6
  198. package/src/tools/browser/attach.ts +3 -3
  199. package/src/tools/browser/launch.ts +81 -18
  200. package/src/tools/browser/registry.ts +1 -5
  201. package/src/tools/browser/tab-supervisor.ts +51 -14
  202. package/src/tools/conflict-detect.ts +21 -10
  203. package/src/tools/eval.ts +3 -1
  204. package/src/tools/fetch.ts +15 -4
  205. package/src/tools/find.ts +39 -39
  206. package/src/tools/gh-renderer.ts +0 -12
  207. package/src/tools/gh.ts +689 -182
  208. package/src/tools/github-cache.ts +548 -0
  209. package/src/tools/index.ts +25 -11
  210. package/src/tools/inspect-image.ts +3 -10
  211. package/src/tools/output-meta.ts +176 -37
  212. package/src/tools/path-utils.ts +125 -2
  213. package/src/tools/read.ts +605 -239
  214. package/src/tools/render-utils.ts +92 -0
  215. package/src/tools/renderers.ts +2 -0
  216. package/src/tools/resolve.ts +72 -44
  217. package/src/tools/search.ts +120 -186
  218. package/src/tools/write.ts +67 -10
  219. package/src/tui/code-cell.ts +70 -2
  220. package/src/utils/file-mentions.ts +1 -1
  221. package/src/utils/image-loading.ts +7 -3
  222. package/src/utils/image-resize.ts +32 -43
  223. package/src/vim/parser.ts +0 -17
  224. package/src/vim/render.ts +1 -1
  225. package/src/vim/types.ts +1 -1
  226. package/src/web/search/providers/gemini.ts +35 -95
  227. package/src/prompts/tools/exit-plan-mode.md +0 -6
  228. package/src/tools/exit-plan-mode.ts +0 -97
  229. package/src/utils/fuzzy.ts +0 -108
  230. package/src/utils/image-convert.ts +0 -27
@@ -460,6 +460,46 @@ export const SETTINGS_SCHEMA = {
460
460
  ],
461
461
  },
462
462
  },
463
+ "tools.artifactHeadBytes": {
464
+ type: "number",
465
+ default: 20,
466
+ ui: {
467
+ tab: "tools",
468
+ label: "Artifact head size (KB)",
469
+ description:
470
+ "Amount of head content kept inline alongside the tail when output spills to artifact (middle elision). 0 disables — keep tail only.",
471
+ options: [
472
+ { value: "0", label: "0 KB", description: "Disabled; tail-only truncation" },
473
+ { value: "1", label: "1 KB", description: "~250 tokens" },
474
+ { value: "2.5", label: "2.5 KB", description: "~625 tokens" },
475
+ { value: "5", label: "5 KB", description: "~1.25K tokens" },
476
+ { value: "10", label: "10 KB", description: "~2.5K tokens" },
477
+ { value: "20", label: "20 KB", description: "Default; ~5K tokens" },
478
+ { value: "50", label: "50 KB", description: "~12.5K tokens" },
479
+ { value: "100", label: "100 KB", description: "~25K tokens" },
480
+ { value: "200", label: "200 KB", description: "~50K tokens" },
481
+ ],
482
+ },
483
+ },
484
+ "tools.outputMaxColumns": {
485
+ type: "number",
486
+ default: 768,
487
+ ui: {
488
+ tab: "tools",
489
+ label: "Output column cap",
490
+ description:
491
+ "Per-line byte cap for streaming tool outputs (bash, ssh, python, js eval) and `read`. Lines wider than this are ellipsis-truncated; remaining bytes up to the next newline are dropped. 0 disables.",
492
+ options: [
493
+ { value: "0", label: "Off", description: "No per-line cap" },
494
+ { value: "256", label: "256", description: "Tight" },
495
+ { value: "512", label: "512" },
496
+ { value: "768", label: "768", description: "Default" },
497
+ { value: "1024", label: "1024" },
498
+ { value: "2048", label: "2048" },
499
+ { value: "4096", label: "4096", description: "Loose" },
500
+ ],
501
+ },
502
+ },
463
503
  "tools.artifactTailLines": {
464
504
  type: "number",
465
505
  default: 500,
@@ -1498,7 +1538,7 @@ export const SETTINGS_SCHEMA = {
1498
1538
 
1499
1539
  "read.defaultLimit": {
1500
1540
  type: "number",
1501
- default: 500,
1541
+ default: 300,
1502
1542
  ui: {
1503
1543
  tab: "editing",
1504
1544
  label: "Default Read Limit",
@@ -1864,6 +1904,37 @@ export const SETTINGS_SCHEMA = {
1864
1904
  },
1865
1905
  },
1866
1906
 
1907
+ "github.cache.enabled": {
1908
+ type: "boolean",
1909
+ default: true,
1910
+ ui: {
1911
+ tab: "tools",
1912
+ label: "GitHub view cache",
1913
+ description: "Cache rendered issue/PR view output in ~/.omp/cache/github-cache.db so repeated reads are free",
1914
+ },
1915
+ },
1916
+
1917
+ "github.cache.softTtlSec": {
1918
+ type: "number",
1919
+ default: 300,
1920
+ ui: {
1921
+ tab: "tools",
1922
+ label: "GitHub cache soft TTL (seconds)",
1923
+ description: "Within this window, cached issue/PR view rows are returned directly. Default 5 minutes.",
1924
+ },
1925
+ },
1926
+
1927
+ "github.cache.hardTtlSec": {
1928
+ type: "number",
1929
+ default: 604800,
1930
+ ui: {
1931
+ tab: "tools",
1932
+ label: "GitHub cache hard TTL (seconds)",
1933
+ description:
1934
+ "Past soft TTL but within hard TTL, the tool returns the cached row and refreshes it in the background. Past hard TTL, the row is dropped. Default 7 days.",
1935
+ },
1936
+ },
1937
+
1867
1938
  "web_search.enabled": {
1868
1939
  type: "boolean",
1869
1940
  default: true,
@@ -2064,6 +2135,36 @@ export const SETTINGS_SCHEMA = {
2064
2135
  },
2065
2136
  },
2066
2137
 
2138
+ "goal.enabled": {
2139
+ type: "boolean",
2140
+ default: true,
2141
+ ui: {
2142
+ tab: "tasks",
2143
+ label: "Goal Mode",
2144
+ description: "Enable per-session goal mode and the hidden goal tool",
2145
+ },
2146
+ },
2147
+
2148
+ "goal.statusInFooter": {
2149
+ type: "boolean",
2150
+ default: true,
2151
+ ui: {
2152
+ tab: "tasks",
2153
+ label: "Goal Status In Footer",
2154
+ description: "Show token budget alongside the goal indicator in the status line",
2155
+ },
2156
+ },
2157
+
2158
+ "goal.continuationModes": {
2159
+ type: "array",
2160
+ default: ["interactive"],
2161
+ ui: {
2162
+ tab: "tasks",
2163
+ label: "Goal Continuation Modes",
2164
+ description: "Run modes where active goals may auto-continue between turns",
2165
+ },
2166
+ },
2167
+
2067
2168
  // Delegation
2068
2169
  "task.isolation.mode": {
2069
2170
  type: "enum",
@@ -850,7 +850,7 @@ export function isSettingsInitialized(): boolean {
850
850
  * Reset the global singleton for testing.
851
851
  * @internal
852
852
  */
853
- export function _resetSettingsForTest(): void {
853
+ export function resetSettingsForTest(): void {
854
854
  globalInstance = null;
855
855
  globalInstancePromise = null;
856
856
  }
package/src/config.ts CHANGED
@@ -1,20 +1,11 @@
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 {
5
- CONFIG_DIR_NAME,
6
- getAgentDir,
7
- getConfigAgentDirName,
8
- getProjectDir,
9
- isEnoent,
10
- logger,
11
- } from "@oh-my-pi/pi-utils";
12
- import type { TSchema } from "@sinclair/typebox";
13
- import { Value } from "@sinclair/typebox/value";
14
- import type { ErrorObject } from "ajv";
15
- import { JSONC, YAML } from "bun";
4
+ import { CONFIG_DIR_NAME, getConfigAgentDirName, getProjectDir } from "@oh-my-pi/pi-utils";
16
5
  import { expandTilde } from "./tools/path-utils";
17
6
 
7
+ export * from "./config/config-file";
8
+
18
9
  const priorityList = [
19
10
  { dir: CONFIG_DIR_NAME, globalAgentDir: getConfigAgentDirName },
20
11
  { dir: ".claude" },
@@ -53,213 +44,6 @@ export function getChangelogPath(): string {
53
44
  return path.resolve(path.join(getPackageDir(), "CHANGELOG.md"));
54
45
  }
55
46
 
56
- // =============================================================================
57
- // User Config Paths (~/.omp/agent/*)
58
- // =============================================================================
59
-
60
- function migrateJsonToYml(jsonPath: string, ymlPath: string) {
61
- try {
62
- if (fs.existsSync(ymlPath)) return;
63
- if (!fs.existsSync(jsonPath)) return;
64
-
65
- const content = fs.readFileSync(jsonPath, "utf-8");
66
- const parsed = JSON.parse(content);
67
- if (!parsed) {
68
- logger.warn("migrateJsonToYml: invalid json structure", { path: jsonPath });
69
- return;
70
- }
71
- fs.writeFileSync(ymlPath, YAML.stringify(parsed, null, 2));
72
- } catch (error) {
73
- logger.warn("migrateJsonToYml: migration failed", { error: String(error) });
74
- }
75
- }
76
-
77
- export interface IConfigFile<T> {
78
- readonly id: string;
79
- readonly schema: TSchema;
80
- path?(): string;
81
- load(): T | null;
82
- invalidate?(): void;
83
- }
84
-
85
- export class ConfigError extends Error {
86
- readonly #message: string;
87
- constructor(
88
- public readonly id: string,
89
- public readonly schemaErrors: ErrorObject[] | null | undefined,
90
- public readonly other?: { err: unknown; stage: string },
91
- ) {
92
- let messages: string[] | undefined;
93
- let cause: any | undefined;
94
- let klass: string;
95
-
96
- if (schemaErrors) {
97
- klass = "Schema";
98
- messages = schemaErrors.map(e => `${e.instancePath || "root"}: ${e.message}`);
99
- } else if (other) {
100
- klass = other.stage;
101
- if (other.err instanceof Error) {
102
- messages = [other.err.message];
103
- cause = other.err;
104
- } else {
105
- messages = [String(other.err)];
106
- }
107
- } else {
108
- klass = "Unknown";
109
- }
110
-
111
- const title = `Failed to load config file ${id}, ${klass} error:`;
112
- let message: string;
113
- switch (messages?.length ?? 0) {
114
- case 0:
115
- message = title.slice(0, -1);
116
- break;
117
- case 1:
118
- message = `${title} ${messages![0]}`;
119
- break;
120
- default:
121
- message = `${title}\n${messages!.map(m => ` - ${m}`).join("\n")}`;
122
- break;
123
- }
124
-
125
- super(message, { cause });
126
- this.name = "LoadError";
127
- this.#message = message;
128
- }
129
-
130
- get message(): string {
131
- return this.#message;
132
- }
133
-
134
- toString(): string {
135
- return this.message;
136
- }
137
- }
138
-
139
- export type LoadStatus = "ok" | "error" | "not-found";
140
-
141
- export type LoadResult<T> =
142
- | { value?: null; error: ConfigError; status: "error" }
143
- | { value: T; error?: undefined; status: "ok" }
144
- | { value?: null; error?: unknown; status: "not-found" };
145
-
146
- export class ConfigFile<T> implements IConfigFile<T> {
147
- readonly #basePath: string;
148
- #cache?: LoadResult<T>;
149
- #auxValidate?: (value: T) => void;
150
-
151
- constructor(
152
- readonly id: string,
153
- readonly schema: TSchema,
154
- configPath: string = path.join(getAgentDir(), `${id}.yml`),
155
- ) {
156
- this.#basePath = configPath;
157
- if (configPath.endsWith(".yml")) {
158
- const jsonPath = `${configPath.slice(0, -4)}.json`;
159
- migrateJsonToYml(jsonPath, configPath);
160
- } else if (configPath.endsWith(".yaml")) {
161
- const jsonPath = `${configPath.slice(0, -5)}.json`;
162
- migrateJsonToYml(jsonPath, configPath);
163
- } else if (configPath.endsWith(".json") || configPath.endsWith(".jsonc")) {
164
- // JSON configs are still supported without migration.
165
- } else {
166
- throw new Error(`Invalid config file path: ${configPath}`);
167
- }
168
- }
169
-
170
- relocate(path?: string): ConfigFile<T> {
171
- if (!path || path === this.#basePath) return this;
172
- const result = new ConfigFile<T>(this.id, this.schema, path);
173
- result.#auxValidate = this.#auxValidate;
174
- return result;
175
- }
176
-
177
- getMtimeMs(): number | null {
178
- try {
179
- return fs.statSync(this.path()).mtimeMs;
180
- } catch (err) {
181
- if (isEnoent(err)) return null;
182
- throw err;
183
- }
184
- }
185
-
186
- withValidation(name: string, validate: (value: T) => void): this {
187
- const prev = this.#auxValidate;
188
- this.#auxValidate = (value: T) => {
189
- prev?.(value);
190
- try {
191
- validate(value);
192
- } catch (error) {
193
- throw new ConfigError(this.id, undefined, { err: error, stage: `Validate(${name})` });
194
- }
195
- };
196
- return this;
197
- }
198
-
199
- createDefault() {
200
- return Value.Default(this.schema, [], undefined) as T;
201
- }
202
-
203
- #storeCache(result: LoadResult<T>): LoadResult<T> {
204
- this.#cache = result;
205
- return result;
206
- }
207
-
208
- tryLoad(): LoadResult<T> {
209
- if (this.#cache) return this.#cache;
210
-
211
- try {
212
- const content = fs.readFileSync(this.path(), "utf-8").trim();
213
-
214
- let parsed: unknown;
215
- if (this.#basePath.endsWith(".json") || this.#basePath.endsWith(".jsonc")) {
216
- parsed = JSONC.parse(content);
217
- } else if (this.#basePath.endsWith(".yml") || this.#basePath.endsWith(".yaml")) {
218
- parsed = YAML.parse(content);
219
- } else {
220
- throw new Error(`Invalid config file path: ${this.#basePath}`);
221
- }
222
-
223
- if (!Value.Check(this.schema, parsed)) {
224
- const schemaErrors: ErrorObject[] = [];
225
- for (const err of Value.Errors(this.schema, parsed)) {
226
- schemaErrors.push({ instancePath: err.path, message: err.message } as ErrorObject);
227
- if (schemaErrors.length >= 50) break;
228
- }
229
- const error = new ConfigError(this.id, schemaErrors);
230
- logger.warn("Failed to parse config file", { path: this.path(), error });
231
- return this.#storeCache({ error, status: "error" });
232
- }
233
- return this.#storeCache({ value: parsed as T, status: "ok" });
234
- } catch (error) {
235
- if (isEnoent(error)) {
236
- return this.#storeCache({ status: "not-found" });
237
- }
238
- logger.warn("Failed to parse config file", { path: this.path(), error });
239
- return this.#storeCache({
240
- error: new ConfigError(this.id, undefined, { err: error, stage: "Unexpected" }),
241
- status: "error",
242
- });
243
- }
244
- }
245
-
246
- load(): T | null {
247
- return this.tryLoad().value ?? null;
248
- }
249
-
250
- loadOrDefault(): T {
251
- return this.tryLoad().value ?? this.createDefault();
252
- }
253
-
254
- path(): string {
255
- return this.#basePath;
256
- }
257
-
258
- invalidate() {
259
- this.#cache = undefined;
260
- }
261
- }
262
-
263
47
  // =============================================================================
264
48
  // Multi-Config Directory Helpers
265
49
  // =============================================================================
package/src/edit/index.ts CHANGED
@@ -145,13 +145,15 @@ async function executeApplyPatchPerFile(
145
145
  const result = await run(batchRequest);
146
146
  const details = result.details;
147
147
  perFileResults.push({
148
- path,
148
+ path: details?.path ?? path,
149
149
  diff: details?.diff ?? "",
150
150
  firstChangedLine: details?.firstChangedLine,
151
151
  diagnostics: details?.diagnostics,
152
152
  op: details?.op,
153
153
  move: details?.move,
154
154
  meta: details?.meta,
155
+ oldText: details?.oldText,
156
+ newText: details?.newText,
155
157
  });
156
158
  const text = result.content?.find(c => c.type === "text")?.text ?? "";
157
159
  if (text) contentTexts.push(text);
@@ -205,6 +207,11 @@ async function executeSinglePathEntries(
205
207
  const diffTexts: string[] = [];
206
208
  let firstChangedLine: number | undefined;
207
209
  let errorCount = 0;
210
+ let metadataPath: string | undefined;
211
+ let hasFirstOldText = false;
212
+ let firstOldText: string | undefined;
213
+ let hasLastNewText = false;
214
+ let lastNewText: string | undefined;
208
215
 
209
216
  for (let i = 0; i < runs.length; i++) {
210
217
  const isLast = i === runs.length - 1;
@@ -217,6 +224,17 @@ async function executeSinglePathEntries(
217
224
  const details = result.details;
218
225
  if (details?.diff) diffTexts.push(details.diff);
219
226
  firstChangedLine ??= details?.firstChangedLine;
227
+ if (details?.path) {
228
+ metadataPath ??= details.path;
229
+ }
230
+ if (details && "oldText" in details && !hasFirstOldText) {
231
+ firstOldText = details.oldText;
232
+ hasFirstOldText = true;
233
+ }
234
+ if (details && "newText" in details) {
235
+ lastNewText = details.newText;
236
+ hasLastNewText = true;
237
+ }
220
238
  const text = result.content?.find(c => c.type === "text")?.text ?? "";
221
239
  if (text) contentTexts.push(text);
222
240
  } catch (err) {
@@ -242,6 +260,9 @@ async function executeSinglePathEntries(
242
260
  details: {
243
261
  diff: diffTexts.join("\n"),
244
262
  firstChangedLine,
263
+ path: metadataPath ?? path,
264
+ ...(hasFirstOldText ? { oldText: firstOldText } : {}),
265
+ ...(hasLastNewText ? { newText: lastNewText } : {}),
245
266
  },
246
267
  // Any per-entry failure marks the aggregate result as an error so the
247
268
  // renderer takes the error branch instead of falling through to the
@@ -1772,15 +1772,25 @@ export async function executePatchSingle(
1772
1772
  .diagnostics(mergedDiagnostics?.summary ?? "", mergedDiagnostics?.messages ?? [])
1773
1773
  .get();
1774
1774
 
1775
+ const oldText = result.change.type !== "create" ? result.change.oldContent : undefined;
1776
+ const newText = result.change.type !== "delete" ? result.change.newContent : undefined;
1777
+
1775
1778
  return {
1776
1779
  content: [{ type: "text", text: resultText }],
1777
1780
  details: {
1778
1781
  diff: diffResult.diff,
1782
+ // When the patch moves the file, anchor the diff to the destination
1783
+ // path. ACP `ToolCallContent.diff.path` comes from this field, and
1784
+ // clients use it to open or focus the file post-change; pointing at
1785
+ // the (now-deleted) source navigates to nothing.
1786
+ path: result.change.newPath ?? resolvedPath,
1779
1787
  firstChangedLine: diffResult.firstChangedLine,
1780
1788
  diagnostics: mergedDiagnostics,
1781
1789
  op,
1782
1790
  move: effectiveRename,
1783
1791
  meta,
1792
+ oldText,
1793
+ newText,
1784
1794
  },
1785
1795
  };
1786
1796
  }
@@ -1094,9 +1094,12 @@ export async function executeReplaceSingle(
1094
1094
  content: [{ type: "text", text: resultText }],
1095
1095
  details: {
1096
1096
  diff: diffResult.diff,
1097
+ path: absolutePath,
1097
1098
  firstChangedLine: diffResult.firstChangedLine,
1098
1099
  diagnostics,
1099
1100
  meta,
1101
+ oldText: rawContent,
1102
+ newText: finalContent,
1100
1103
  },
1101
1104
  };
1102
1105
  }
@@ -55,6 +55,10 @@ export interface EditToolPerFileResult {
55
55
  * Set when the underlying error carries a `displayMessage` (e.g. {@link HashlineMismatchError}). */
56
56
  displayErrorText?: string;
57
57
  meta?: OutputMeta;
58
+ /** Source-of-truth content before the edit; `undefined` for create operations. */
59
+ oldText?: string;
60
+ /** Source-of-truth content after the edit; `undefined` for delete operations. */
61
+ newText?: string;
58
62
  }
59
63
 
60
64
  export interface EditToolDetails {
@@ -72,6 +76,12 @@ export interface EditToolDetails {
72
76
  meta?: OutputMeta;
73
77
  /** Per-file results (multi-file edits) */
74
78
  perFileResults?: EditToolPerFileResult[];
79
+ /** Absolute file path for single-file edit results. Required by ACP diff metadata consumers. */
80
+ path?: string;
81
+ /** Source-of-truth content before the edit; `undefined` for create operations. */
82
+ oldText?: string;
83
+ /** Source-of-truth content after the edit; `undefined` for delete operations. */
84
+ newText?: string;
75
85
  }
76
86
 
77
87
  // ═══════════════════════════════════════════════════════════════════════════
@@ -330,7 +340,13 @@ function normalizeHashlineInputPreviewPath(rawPath: string): string {
330
340
 
331
341
  function parseHashlineInputPreviewHeader(line: string): string | null {
332
342
  if (!line.startsWith(HL_INPUT_HEADER_PREFIX)) return null;
333
- const body = line.slice(HL_INPUT_HEADER_PREFIX.length).trim();
343
+ // The real parser (`parseHashlineHeaderLine` in `hashline/input.ts`) strips
344
+ // every leading "@" before resolving the path so canonical "@@ PATH" headers
345
+ // (and stray "@ PATH" / "@@@ PATH" runs) all route to the same file. Mirror
346
+ // that here so the renderer doesn't surface a literal "@ " in the title.
347
+ let prefixEnd = 0;
348
+ while (prefixEnd < line.length && line[prefixEnd] === HL_INPUT_HEADER_PREFIX) prefixEnd++;
349
+ const body = line.slice(prefixEnd).trim();
334
350
  const previewPath = normalizeHashlineInputPreviewPath(body);
335
351
  return previewPath.length > 0 ? previewPath : null;
336
352
  }
@@ -17,7 +17,7 @@ import type {
17
17
  WorkerOutbound,
18
18
  } from "./worker-protocol";
19
19
 
20
- export { rewriteStaticImports } from "./shared/rewrite-imports";
20
+ export { rewriteImports, wrapCode } from "./shared/rewrite-imports";
21
21
  export type { JsDisplayOutput } from "./worker-protocol";
22
22
 
23
23
  export interface VmRunState {
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_MAX_BYTES, OutputSink } from "../../session/streaming-output";
2
2
  import type { ToolSession } from "../../tools";
3
+ import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../../tools/output-meta";
3
4
  import { executeInVmContext, type JsDisplayOutput } from "./context-manager";
4
5
 
5
6
  export interface JsExecutorOptions {
@@ -49,6 +50,8 @@ export async function executeJs(code: string, options: JsExecutorOptions): Promi
49
50
  artifactPath: options.artifactPath,
50
51
  artifactId: options.artifactId,
51
52
  spillThreshold: DEFAULT_MAX_BYTES,
53
+ headBytes: resolveOutputSinkHeadBytes(options.session.settings),
54
+ maxColumns: resolveOutputMaxColumns(options.session.settings),
52
55
  onChunk: chunk => options.onChunk?.(chunk),
53
56
  });
54
57
  const timeoutMs = getExecutionTimeoutMs(options);