@nghyane/arcane 0.1.13 → 0.1.15

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 (303) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/package.json +21 -70
  3. package/scripts/format-prompts.ts +1 -3
  4. package/src/cli/args.ts +2 -7
  5. package/src/cli/config-cli.ts +1 -1
  6. package/src/cli/plugin-cli.ts +1 -1
  7. package/src/cli/setup-cli.ts +1 -1
  8. package/src/cli/update-cli.ts +1 -1
  9. package/src/cli/web-search-cli.ts +1 -1
  10. package/src/cli.ts +0 -1
  11. package/src/commands/config.ts +1 -1
  12. package/src/commands/grep.ts +1 -1
  13. package/src/commands/jupyter.ts +1 -1
  14. package/src/commands/plugin.ts +1 -1
  15. package/src/commands/setup.ts +1 -1
  16. package/src/commands/shell.ts +1 -1
  17. package/src/commands/ssh.ts +1 -1
  18. package/src/commands/stats.ts +1 -1
  19. package/src/commands/update.ts +1 -1
  20. package/src/config/model-registry.ts +3 -4
  21. package/src/config/model-resolver.ts +36 -9
  22. package/src/config/prompt-templates.ts +1 -9
  23. package/src/config/settings-schema.ts +32 -88
  24. package/src/config/settings.ts +3 -4
  25. package/src/debug/index.ts +1 -1
  26. package/src/debug/log-formatting.ts +1 -1
  27. package/src/debug/log-viewer.ts +2 -2
  28. package/src/discovery/helpers.ts +13 -3
  29. package/src/exa/index.ts +1 -35
  30. package/src/exa/render.ts +30 -190
  31. package/src/export/html/index.ts +1 -1
  32. package/src/extensibility/custom-tools/loader.ts +1 -1
  33. package/src/extensibility/custom-tools/types.ts +5 -1
  34. package/src/extensibility/custom-tools/wrapper.ts +1 -1
  35. package/src/extensibility/extensions/runner.ts +1 -1
  36. package/src/extensibility/extensions/types.ts +1 -1
  37. package/src/extensibility/extensions/wrapper.ts +7 -15
  38. package/src/extensibility/hooks/runner.ts +1 -1
  39. package/src/extensibility/hooks/types.ts +1 -1
  40. package/src/extensibility/plugins/doctor.ts +1 -1
  41. package/src/index.ts +13 -13
  42. package/src/lsp/index.ts +77 -24
  43. package/src/lsp/render.ts +34 -583
  44. package/src/lsp/types.ts +3 -3
  45. package/src/lsp/utils.ts +1 -1
  46. package/src/main.ts +1 -1
  47. package/src/mcp/tool-bridge.ts +1 -24
  48. package/src/modes/components/assistant-message.ts +7 -7
  49. package/src/modes/components/bash-execution.ts +50 -112
  50. package/src/modes/components/bordered-loader.ts +1 -1
  51. package/src/modes/components/branch-summary-message.ts +16 -10
  52. package/src/modes/components/compaction-summary-message.ts +20 -12
  53. package/src/modes/components/context-group.ts +106 -0
  54. package/src/modes/components/custom-message.ts +4 -5
  55. package/src/modes/components/diff.ts +2 -2
  56. package/src/modes/components/dynamic-border.ts +1 -1
  57. package/src/modes/components/extensions/extension-dashboard.ts +1 -1
  58. package/src/modes/components/extensions/extension-list.ts +1 -1
  59. package/src/modes/components/extensions/inspector-panel.ts +1 -1
  60. package/src/modes/components/footer.ts +2 -2
  61. package/src/modes/components/history-search.ts +1 -1
  62. package/src/modes/components/hook-editor.ts +1 -1
  63. package/src/modes/components/hook-input.ts +1 -1
  64. package/src/modes/components/hook-message.ts +4 -5
  65. package/src/modes/components/hook-selector.ts +1 -1
  66. package/src/modes/components/index.ts +0 -2
  67. package/src/modes/components/keybinding-hints.ts +1 -1
  68. package/src/modes/components/login-dialog.ts +1 -1
  69. package/src/modes/components/mcp-add-wizard.ts +1 -1
  70. package/src/modes/components/model-selector.ts +1 -1
  71. package/src/modes/components/oauth-selector.ts +1 -1
  72. package/src/modes/components/plugin-settings.ts +1 -1
  73. package/src/modes/components/python-execution.ts +51 -91
  74. package/src/modes/components/queue-mode-selector.ts +1 -1
  75. package/src/modes/components/session-selector.ts +1 -1
  76. package/src/modes/components/settings-defs.ts +5 -10
  77. package/src/modes/components/settings-selector.ts +1 -1
  78. package/src/modes/components/show-images-selector.ts +1 -1
  79. package/src/modes/components/skill-message.ts +4 -4
  80. package/src/modes/components/status-line/segments.ts +2 -2
  81. package/src/modes/components/status-line/separators.ts +1 -1
  82. package/src/modes/components/status-line-segment-editor.ts +1 -1
  83. package/src/modes/components/status-line.ts +1 -1
  84. package/src/modes/components/theme-selector.ts +1 -1
  85. package/src/modes/components/thinking-selector.ts +1 -1
  86. package/src/modes/components/todo-display.ts +2 -4
  87. package/src/modes/components/todo-reminder.ts +4 -4
  88. package/src/modes/components/tool-execution.ts +118 -440
  89. package/src/modes/components/tool-image-display.ts +107 -0
  90. package/src/modes/components/tree-selector.ts +2 -2
  91. package/src/modes/components/ttsr-notification.ts +4 -17
  92. package/src/modes/components/user-message-selector.ts +1 -1
  93. package/src/modes/components/user-message.ts +9 -10
  94. package/src/modes/components/welcome.ts +1 -1
  95. package/src/modes/controllers/command-controller.ts +1 -1
  96. package/src/modes/controllers/event-controller.ts +58 -187
  97. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  98. package/src/modes/controllers/input-controller.ts +3 -1
  99. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  100. package/src/modes/controllers/selector-controller.ts +3 -26
  101. package/src/modes/controllers/ssh-command-controller.ts +1 -1
  102. package/src/modes/interactive-mode.ts +3 -7
  103. package/src/modes/print-mode.ts +5 -5
  104. package/src/modes/rpc/rpc-mode.ts +1 -1
  105. package/src/modes/types.ts +1 -2
  106. package/src/modes/utils/ui-helpers.ts +34 -32
  107. package/src/patch/edit-tool.ts +742 -0
  108. package/src/patch/index.ts +32 -898
  109. package/src/patch/schemas.ts +208 -0
  110. package/src/patch/shared.ts +83 -151
  111. package/src/prompts/agents/explore.md +22 -37
  112. package/src/prompts/agents/init.md +1 -1
  113. package/src/prompts/agents/librarian.md +29 -20
  114. package/src/prompts/agents/oracle.md +9 -2
  115. package/src/prompts/agents/reviewer.md +14 -48
  116. package/src/prompts/agents/task.md +16 -8
  117. package/src/prompts/compaction/branch-summary.md +4 -1
  118. package/src/prompts/compaction/compaction-summary.md +4 -1
  119. package/src/prompts/system/subagent-system-prompt.md +1 -1
  120. package/src/prompts/system/system-prompt.md +162 -178
  121. package/src/prompts/system/verification-reminder.md +6 -0
  122. package/src/sdk.ts +0 -9
  123. package/src/session/agent-session.ts +244 -1459
  124. package/src/session/model-controller.ts +406 -0
  125. package/src/session/retry-utils.ts +71 -0
  126. package/src/session/session-manager.ts +22 -186
  127. package/src/session/session-types.ts +312 -0
  128. package/src/session/stats.ts +387 -0
  129. package/src/session/streaming-edit.ts +258 -0
  130. package/src/session/ttsr.ts +213 -0
  131. package/src/slash-commands/builtin-registry.ts +0 -8
  132. package/src/stt/recorder.ts +2 -2
  133. package/src/system-prompt.ts +1 -14
  134. package/src/task/agents.ts +7 -33
  135. package/src/task/executor.ts +50 -438
  136. package/src/task/index.ts +104 -71
  137. package/src/task/progress-tracker.ts +390 -0
  138. package/src/task/render.ts +371 -187
  139. package/src/task/subprocess-tool-registry.ts +1 -1
  140. package/src/task/types.ts +14 -47
  141. package/src/tools/ask.ts +31 -42
  142. package/src/tools/bash-interactive.ts +2 -2
  143. package/src/tools/bash-interceptor.ts +2 -2
  144. package/src/tools/bash-normalize.ts +1 -1
  145. package/src/tools/bash-skill-urls.ts +2 -2
  146. package/src/tools/bash.ts +87 -136
  147. package/src/tools/browser.ts +54 -84
  148. package/src/tools/create-tools.ts +186 -0
  149. package/src/tools/default-renderer.ts +104 -0
  150. package/src/tools/explore.ts +11 -10
  151. package/src/tools/fetch.ts +24 -114
  152. package/src/tools/find.ts +48 -132
  153. package/src/tools/gemini-image.ts +5 -15
  154. package/src/tools/github.ts +450 -0
  155. package/src/tools/grep.ts +43 -179
  156. package/src/tools/index.ts +35 -198
  157. package/src/tools/json-tree.ts +3 -3
  158. package/src/tools/librarian.ts +18 -18
  159. package/src/tools/list-limit.ts +2 -2
  160. package/src/tools/notebook.ts +35 -87
  161. package/src/tools/oracle.ts +25 -25
  162. package/src/tools/output-meta.ts +89 -4
  163. package/src/tools/output-utils.ts +2 -2
  164. package/src/tools/python.ts +86 -637
  165. package/src/tools/read.ts +36 -119
  166. package/src/tools/reviewer-tool.ts +19 -21
  167. package/src/tools/search-code.ts +128 -0
  168. package/src/tools/ssh.ts +67 -126
  169. package/src/tools/subagent-tool.ts +197 -123
  170. package/src/tools/todo-write.ts +15 -31
  171. package/src/tools/tool-errors.ts +0 -30
  172. package/src/tools/undo-edit.ts +30 -67
  173. package/src/tools/write.ts +78 -127
  174. package/src/tui/code-cell.ts +4 -4
  175. package/src/tui/file-list.ts +2 -2
  176. package/src/tui/output-block.ts +1 -1
  177. package/src/tui/status-line.ts +1 -1
  178. package/src/tui/tree-list.ts +2 -2
  179. package/src/tui/types.ts +1 -1
  180. package/src/tui/utils.ts +1 -1
  181. package/src/{tools → ui}/render-utils.ts +87 -126
  182. package/src/utils/external-editor.ts +4 -4
  183. package/src/utils/file-mentions.ts +1 -1
  184. package/src/utils/index.ts +30 -0
  185. package/src/utils/tools-manager.ts +9 -19
  186. package/src/web/github-client.ts +290 -0
  187. package/src/web/scrapers/github.ts +11 -62
  188. package/src/web/search/auth.ts +1 -3
  189. package/src/web/search/index.ts +82 -46
  190. package/src/web/search/provider.ts +11 -16
  191. package/src/web/search/providers/grep.ts +160 -0
  192. package/src/web/search/render.ts +48 -235
  193. package/src/web/search/types.ts +1 -1
  194. package/src/commands/commit.ts +0 -36
  195. package/src/commit/agentic/agent.ts +0 -311
  196. package/src/commit/agentic/fallback.ts +0 -96
  197. package/src/commit/agentic/index.ts +0 -359
  198. package/src/commit/agentic/prompts/analyze-file.md +0 -22
  199. package/src/commit/agentic/prompts/session-user.md +0 -25
  200. package/src/commit/agentic/prompts/split-confirm.md +0 -1
  201. package/src/commit/agentic/prompts/system.md +0 -38
  202. package/src/commit/agentic/state.ts +0 -69
  203. package/src/commit/agentic/tools/analyze-file.ts +0 -118
  204. package/src/commit/agentic/tools/git-file-diff.ts +0 -194
  205. package/src/commit/agentic/tools/git-hunk.ts +0 -50
  206. package/src/commit/agentic/tools/git-overview.ts +0 -84
  207. package/src/commit/agentic/tools/index.ts +0 -56
  208. package/src/commit/agentic/tools/propose-changelog.ts +0 -128
  209. package/src/commit/agentic/tools/propose-commit.ts +0 -154
  210. package/src/commit/agentic/tools/recent-commits.ts +0 -81
  211. package/src/commit/agentic/tools/split-commit.ts +0 -280
  212. package/src/commit/agentic/topo-sort.ts +0 -44
  213. package/src/commit/agentic/trivial.ts +0 -51
  214. package/src/commit/agentic/validation.ts +0 -200
  215. package/src/commit/analysis/conventional.ts +0 -165
  216. package/src/commit/analysis/index.ts +0 -4
  217. package/src/commit/analysis/scope.ts +0 -242
  218. package/src/commit/analysis/summary.ts +0 -112
  219. package/src/commit/analysis/validation.ts +0 -66
  220. package/src/commit/changelog/detect.ts +0 -37
  221. package/src/commit/changelog/generate.ts +0 -110
  222. package/src/commit/changelog/index.ts +0 -234
  223. package/src/commit/changelog/parse.ts +0 -44
  224. package/src/commit/cli.ts +0 -93
  225. package/src/commit/git/diff.ts +0 -148
  226. package/src/commit/git/errors.ts +0 -9
  227. package/src/commit/git/index.ts +0 -211
  228. package/src/commit/git/operations.ts +0 -54
  229. package/src/commit/index.ts +0 -5
  230. package/src/commit/map-reduce/index.ts +0 -64
  231. package/src/commit/map-reduce/map-phase.ts +0 -178
  232. package/src/commit/map-reduce/reduce-phase.ts +0 -145
  233. package/src/commit/map-reduce/utils.ts +0 -9
  234. package/src/commit/message.ts +0 -11
  235. package/src/commit/model-selection.ts +0 -69
  236. package/src/commit/pipeline.ts +0 -243
  237. package/src/commit/prompts/analysis-system.md +0 -148
  238. package/src/commit/prompts/analysis-user.md +0 -38
  239. package/src/commit/prompts/changelog-system.md +0 -50
  240. package/src/commit/prompts/changelog-user.md +0 -18
  241. package/src/commit/prompts/file-observer-system.md +0 -24
  242. package/src/commit/prompts/file-observer-user.md +0 -8
  243. package/src/commit/prompts/reduce-system.md +0 -50
  244. package/src/commit/prompts/reduce-user.md +0 -17
  245. package/src/commit/prompts/summary-retry.md +0 -3
  246. package/src/commit/prompts/summary-system.md +0 -38
  247. package/src/commit/prompts/summary-user.md +0 -13
  248. package/src/commit/prompts/types-description.md +0 -2
  249. package/src/commit/types.ts +0 -109
  250. package/src/commit/utils/exclusions.ts +0 -42
  251. package/src/mcp/render.ts +0 -123
  252. package/src/modes/components/agent-dashboard.ts +0 -1130
  253. package/src/modes/components/codemode-group.ts +0 -369
  254. package/src/modes/components/read-tool-group.ts +0 -119
  255. package/src/modes/components/visual-truncate.ts +0 -63
  256. package/src/prompts/system/subagent-user-prompt.md +0 -8
  257. package/src/prompts/tools/ask.md +0 -44
  258. package/src/prompts/tools/bash.md +0 -24
  259. package/src/prompts/tools/browser.md +0 -33
  260. package/src/prompts/tools/calculator.md +0 -12
  261. package/src/prompts/tools/explore.md +0 -29
  262. package/src/prompts/tools/fetch.md +0 -16
  263. package/src/prompts/tools/find.md +0 -18
  264. package/src/prompts/tools/gemini-image.md +0 -23
  265. package/src/prompts/tools/grep.md +0 -28
  266. package/src/prompts/tools/hashline.md +0 -232
  267. package/src/prompts/tools/librarian.md +0 -24
  268. package/src/prompts/tools/lsp.md +0 -28
  269. package/src/prompts/tools/oracle.md +0 -26
  270. package/src/prompts/tools/patch.md +0 -74
  271. package/src/prompts/tools/python.md +0 -66
  272. package/src/prompts/tools/read.md +0 -36
  273. package/src/prompts/tools/replace.md +0 -38
  274. package/src/prompts/tools/reviewer.md +0 -41
  275. package/src/prompts/tools/ssh.md +0 -51
  276. package/src/prompts/tools/task-summary.md +0 -28
  277. package/src/prompts/tools/task.md +0 -146
  278. package/src/prompts/tools/todo-write.md +0 -65
  279. package/src/prompts/tools/undo-edit.md +0 -7
  280. package/src/prompts/tools/web-search.md +0 -19
  281. package/src/prompts/tools/write.md +0 -18
  282. package/src/task/batch.ts +0 -102
  283. package/src/task/discovery.ts +0 -126
  284. package/src/task/parallel.ts +0 -84
  285. package/src/task/template.ts +0 -32
  286. package/src/tools/calculator.ts +0 -537
  287. package/src/tools/jtd-to-typescript.ts +0 -198
  288. package/src/tools/renderers.ts +0 -60
  289. package/src/tools/tool-result.ts +0 -86
  290. /package/src/{modes/theme → theme}/dark.json +0 -0
  291. /package/src/{modes/theme → theme}/defaults/dark-catppuccin.json +0 -0
  292. /package/src/{modes/theme → theme}/defaults/dark-dracula.json +0 -0
  293. /package/src/{modes/theme → theme}/defaults/dark-gruvbox.json +0 -0
  294. /package/src/{modes/theme → theme}/defaults/dark-solarized.json +0 -0
  295. /package/src/{modes/theme → theme}/defaults/dark-tokyo-night.json +0 -0
  296. /package/src/{modes/theme → theme}/defaults/index.ts +0 -0
  297. /package/src/{modes/theme → theme}/defaults/light-catppuccin.json +0 -0
  298. /package/src/{modes/theme → theme}/defaults/light-github.json +0 -0
  299. /package/src/{modes/theme → theme}/defaults/light-solarized.json +0 -0
  300. /package/src/{modes/theme → theme}/light.json +0 -0
  301. /package/src/{modes/theme → theme}/mermaid-cache.ts +0 -0
  302. /package/src/{modes/theme → theme}/theme-schema.json +0 -0
  303. /package/src/{modes/theme → theme}/theme.ts +0 -0
@@ -0,0 +1,208 @@
1
+ import { StringEnum } from "@nghyane/arcane-ai";
2
+ import { type Static, Type } from "@sinclair/typebox";
3
+
4
+ export const replaceEditSchema = Type.Object({
5
+ path: Type.String({ description: "File path (relative or absolute)" }),
6
+ old_text: Type.String({
7
+ description: "Text to find (fuzzy whitespace matching enabled)",
8
+ }),
9
+ new_text: Type.String({ description: "Replacement text" }),
10
+ all: Type.Optional(
11
+ Type.Boolean({
12
+ description: "Replace all occurrences (default: unique match required)",
13
+ }),
14
+ ),
15
+ });
16
+
17
+ export const patchEditSchema = Type.Object({
18
+ path: Type.String({ description: "File path" }),
19
+ op: Type.Optional(
20
+ StringEnum(["create", "delete", "update"], {
21
+ description: "Operation (default: update)",
22
+ }),
23
+ ),
24
+ rename: Type.Optional(Type.String({ description: "New path for move" })),
25
+ diff: Type.Optional(
26
+ Type.String({
27
+ description: "Diff hunks (update) or full content (create)",
28
+ }),
29
+ ),
30
+ });
31
+
32
+ export type ReplaceParams = Static<typeof replaceEditSchema>;
33
+ export type PatchParams = Static<typeof patchEditSchema>;
34
+
35
+ /** Pattern matching hashline display format: `LINE#ID:CONTENT` */
36
+ const HASHLINE_PREFIX_RE = /^\s*(?:>>>|>>)?\s*\d+#[0-9a-zA-Z]{1,16}:/;
37
+
38
+ /** Pattern matching a unified-diff `+` prefix (but not `++`) */
39
+ const DIFF_PLUS_RE = /^[+-](?![+-])/;
40
+
41
+ /**
42
+ * Strip hashline display prefixes and diff `+` markers from replacement lines.
43
+ *
44
+ * Models frequently copy the `LINE#ID ` prefix from read output into their
45
+ * replacement content, or include unified-diff `+` prefixes. Both corrupt the
46
+ * output file. This strips them heuristically before application.
47
+ */
48
+ export function stripNewLinePrefixes(lines: string[]): string[] {
49
+ // Detect whether the *majority* of non-empty lines carry a prefix —
50
+ // if only one line out of many has a match it's likely real content.
51
+ let hashPrefixCount = 0;
52
+ let diffPlusCount = 0;
53
+ let nonEmpty = 0;
54
+ for (const l of lines) {
55
+ if (l.length === 0) continue;
56
+ nonEmpty++;
57
+ if (HASHLINE_PREFIX_RE.test(l)) hashPrefixCount++;
58
+ if (DIFF_PLUS_RE.test(l)) diffPlusCount++;
59
+ }
60
+ if (nonEmpty === 0) return lines;
61
+
62
+ const stripHash = hashPrefixCount > 0 && hashPrefixCount >= nonEmpty * 0.5;
63
+ const stripPlus = !stripHash && nonEmpty >= 2 && diffPlusCount > 0 && diffPlusCount >= nonEmpty * 0.5;
64
+
65
+ if (!stripHash && !stripPlus) return lines;
66
+
67
+ return lines.map(l => {
68
+ if (stripHash) return l.replace(HASHLINE_PREFIX_RE, "");
69
+ if (stripPlus) return l.replace(DIFF_PLUS_RE, "");
70
+ return l;
71
+ });
72
+ }
73
+
74
+ const hashlineReplaceContentFormat = (kind: string) =>
75
+ Type.Union([
76
+ Type.Null(),
77
+ Type.Array(Type.String(), { description: `${kind} lines` }),
78
+ Type.String({ description: `${kind} line` }),
79
+ ]);
80
+
81
+ const hashlineInsertContentFormat = (kind: string) =>
82
+ Type.Union([
83
+ Type.Array(Type.String(), { description: `${kind} lines`, minItems: 1 }),
84
+ Type.String({ description: `${kind} line`, minLength: 1 }),
85
+ ]);
86
+
87
+ const hashlineTagFormat = (what: string) =>
88
+ Type.String({
89
+ description: `Tag identifying the ${what} — format "N#XX" (e.g. "5#PM"), copied verbatim from read output`,
90
+ });
91
+
92
+ export function hashlineParseContent(edit: string | string[] | null): string[] {
93
+ if (edit === null) return [];
94
+ if (Array.isArray(edit)) return edit;
95
+ const lines = stripNewLinePrefixes(edit.split("\n"));
96
+ if (lines.length === 0) return [];
97
+ if (lines.length > 1 && lines[lines.length - 1].trim() === "") return lines.slice(0, -1);
98
+ return lines;
99
+ }
100
+
101
+ export function hashlineParseContentString(edit: string | string[] | null): string {
102
+ if (edit === null) return "";
103
+ if (Array.isArray(edit)) return edit.join("\n");
104
+ return edit;
105
+ }
106
+
107
+ const hashlineTargetEditSchema = Type.Object(
108
+ {
109
+ op: Type.Literal("set"),
110
+ tag: hashlineTagFormat("line being replaced"),
111
+ content: hashlineReplaceContentFormat("Replacement"),
112
+ },
113
+ { additionalProperties: false },
114
+ );
115
+
116
+ const hashlineAppendEditSchema = Type.Object(
117
+ {
118
+ op: Type.Literal("append"),
119
+ after: Type.Optional(hashlineTagFormat("line after which to append")),
120
+ content: hashlineInsertContentFormat("Appended"),
121
+ },
122
+ { additionalProperties: false },
123
+ );
124
+
125
+ const hashlinePrependEditSchema = Type.Object(
126
+ {
127
+ op: Type.Literal("prepend"),
128
+ before: Type.Optional(hashlineTagFormat("line before which to prepend")),
129
+ content: hashlineInsertContentFormat("Prepended"),
130
+ },
131
+ { additionalProperties: false },
132
+ );
133
+
134
+ const hashlineRangeEditSchema = Type.Object(
135
+ {
136
+ op: Type.Literal("replace"),
137
+ first: hashlineTagFormat("first line"),
138
+ last: hashlineTagFormat("last line"),
139
+ content: hashlineReplaceContentFormat("Replacement"),
140
+ },
141
+ { additionalProperties: false },
142
+ );
143
+
144
+ const hashlineInsertEditSchema = Type.Object(
145
+ {
146
+ op: Type.Literal("insert"),
147
+ before: Type.Optional(hashlineTagFormat("line before which to insert")),
148
+ after: Type.Optional(hashlineTagFormat("line after which to insert")),
149
+ content: hashlineInsertContentFormat("Inserted"),
150
+ },
151
+ { additionalProperties: false },
152
+ );
153
+
154
+ const hashlineReplaceTextEditSchema = Type.Object(
155
+ {
156
+ op: Type.Literal("replaceText"),
157
+ old_text: Type.String({ description: "Text to find", minLength: 1 }),
158
+ new_text: hashlineReplaceContentFormat("Replacement"),
159
+ all: Type.Optional(Type.Boolean({ description: "Replace all occurrences" })),
160
+ },
161
+ { additionalProperties: false },
162
+ );
163
+
164
+ const HL_REPLACE_ENABLED = Bun.env.ARCANE_HL_REPLACETXT === "1";
165
+
166
+ export const hashlineEditSpecSchema = Type.Union([
167
+ hashlineTargetEditSchema,
168
+ hashlineRangeEditSchema,
169
+ hashlineAppendEditSchema,
170
+ hashlinePrependEditSchema,
171
+ hashlineInsertEditSchema,
172
+ ...(HL_REPLACE_ENABLED ? [hashlineReplaceTextEditSchema] : []),
173
+ ]);
174
+
175
+ export const hashlineEditSchema = Type.Object(
176
+ {
177
+ path: Type.String({ description: "File path (relative or absolute)" }),
178
+ edits: Type.Array(hashlineEditSpecSchema, {
179
+ description: "Changes to apply to the file at `path`",
180
+ minItems: 0,
181
+ }),
182
+ delete: Type.Optional(Type.Boolean({ description: "Delete the file when true" })),
183
+ rename: Type.Optional(Type.String({ description: "New path if moving" })),
184
+ },
185
+ { additionalProperties: false },
186
+ );
187
+
188
+ export type HashlineToolEdit = Static<typeof hashlineEditSpecSchema>;
189
+ export type HashlineParams = Static<typeof hashlineEditSchema>;
190
+
191
+ export type TInput = typeof replaceEditSchema | typeof patchEditSchema | typeof hashlineEditSchema;
192
+
193
+ export type EditMode = "replace" | "patch" | "hashline";
194
+
195
+ export const DEFAULT_EDIT_MODE: EditMode = "patch";
196
+
197
+ export function normalizeEditMode(mode?: string | null): EditMode | null {
198
+ switch (mode) {
199
+ case "replace":
200
+ return "replace";
201
+ case "patch":
202
+ return "patch";
203
+ case "hashline":
204
+ return "hashline";
205
+ default:
206
+ return null;
207
+ }
208
+ }
@@ -7,19 +7,20 @@ import { Text } from "@nghyane/arcane-tui";
7
7
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
8
8
  import type { FileDiagnosticsResult } from "../lsp";
9
9
  import { renderDiff as renderDiffColored } from "../modes/components/diff";
10
- import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
10
+ import { getLanguageFromPath, type Theme } from "../theme/theme";
11
11
  import type { OutputMeta } from "../tools/output-meta";
12
+ import { renderStatusLine } from "../tui";
12
13
  import {
13
- formatExpandHint,
14
+ formatClickHint,
15
+ formatDiagnostics,
14
16
  formatStatusIcon,
15
17
  getDiffStats,
16
18
  PREVIEW_LIMITS,
17
19
  replaceTabs,
18
20
  shortenPath,
19
- ToolUIKit,
20
- truncateDiffByHunk,
21
- } from "../tools/render-utils";
22
- import { Ellipsis, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
21
+ TRUNCATE_LENGTHS,
22
+ truncateToWidth,
23
+ } from "../ui/render-utils";
23
24
  import type { DiffError, DiffResult, Operation } from "./types";
24
25
 
25
26
  // ═══════════════════════════════════════════════════════════════════════════
@@ -100,11 +101,6 @@ export interface EditRenderContext {
100
101
 
101
102
  const EDIT_STREAMING_PREVIEW_LINES = 12;
102
103
 
103
- function countLines(text: string): number {
104
- if (!text) return 0;
105
- return text.split("\n").length;
106
- }
107
-
108
104
  function formatStreamingDiff(diff: string, rawPath: string, uiTheme: Theme, label = "streaming"): string {
109
105
  if (!diff) return "";
110
106
  const lines = diff.split("\n");
@@ -120,7 +116,7 @@ function formatStreamingDiff(diff: string, rawPath: string, uiTheme: Theme, labe
120
116
  return text;
121
117
  }
122
118
 
123
- function formatStreamingHashlineEdits(edits: unknown[], uiTheme: Theme, ui: ToolUIKit): string {
119
+ function formatStreamingHashlineEdits(edits: unknown[], uiTheme: Theme): string {
124
120
  const MAX_EDITS = 4;
125
121
  const MAX_DST_LINES = 8;
126
122
  let text = "\n\n";
@@ -132,17 +128,17 @@ function formatStreamingHashlineEdits(edits: unknown[], uiTheme: Theme, ui: Tool
132
128
  shownEdits++;
133
129
  if (shownEdits > MAX_EDITS) break;
134
130
  const formatted = formatHashlineEdit(edit);
135
- text += uiTheme.fg("toolOutput", ui.truncate(replaceTabs(formatted.srcLabel), 120));
131
+ text += uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(formatted.srcLabel), TRUNCATE_LENGTHS.LONG));
136
132
  text += "\n";
137
133
  if (formatted.dst === "") {
138
- text += uiTheme.fg("dim", ui.truncate(" (delete)", 120));
134
+ text += uiTheme.fg("dim", truncateToWidth(" (delete)", TRUNCATE_LENGTHS.LONG));
139
135
  text += "\n";
140
136
  continue;
141
137
  }
142
138
  for (const dstLine of formatted.dst.split("\n")) {
143
139
  shownDstLines++;
144
140
  if (shownDstLines > MAX_DST_LINES) break;
145
- text += uiTheme.fg("toolOutput", ui.truncate(replaceTabs(`+ ${dstLine}`), 120));
141
+ text += uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(`+ ${dstLine}`), TRUNCATE_LENGTHS.LONG));
146
142
  text += "\n";
147
143
  }
148
144
  if (shownDstLines > MAX_DST_LINES) break;
@@ -208,53 +204,13 @@ function formatStreamingHashlineEdits(edits: unknown[], uiTheme: Theme, ui: Tool
208
204
  };
209
205
  }
210
206
  }
211
- function formatMetadataLine(lineCount: number | null, language: string | undefined, uiTheme: Theme): string {
212
- const icon = uiTheme.getLangIcon(language);
213
- if (lineCount !== null) {
214
- return uiTheme.fg("dim", `${icon} ${lineCount} lines`);
215
- }
216
- return uiTheme.fg("dim", `${icon}`);
217
- }
218
-
219
- function renderDiffSection(
220
- diff: string,
221
- rawPath: string,
222
- expanded: boolean,
223
- uiTheme: Theme,
224
- ui: ToolUIKit,
225
- renderDiffFn: (t: string, o?: { filePath?: string }) => string,
226
- ): string {
227
- let text = "";
228
- const diffStats = getDiffStats(diff);
229
- text += `\n${uiTheme.fg("dim", uiTheme.format.bracketLeft)}${ui.formatDiffStats(
230
- diffStats.added,
231
- diffStats.removed,
232
- diffStats.hunks,
233
- )}${uiTheme.fg("dim", uiTheme.format.bracketRight)}`;
234
-
235
- const {
236
- text: truncatedDiff,
237
- hiddenHunks,
238
- hiddenLines,
239
- } = expanded
240
- ? { text: diff, hiddenHunks: 0, hiddenLines: 0 }
241
- : truncateDiffByHunk(diff, PREVIEW_LIMITS.DIFF_COLLAPSED_HUNKS, PREVIEW_LIMITS.DIFF_COLLAPSED_LINES);
242
-
243
- text += `\n\n${renderDiffFn(truncatedDiff, { filePath: rawPath })}`;
244
- if (!expanded && (hiddenHunks > 0 || hiddenLines > 0)) {
245
- const remainder: string[] = [];
246
- if (hiddenHunks > 0) remainder.push(`${hiddenHunks} more hunks`);
247
- if (hiddenLines > 0) remainder.push(`${hiddenLines} more lines`);
248
- text += uiTheme.fg("toolOutput", `\n… (${remainder.join(", ")}) ${formatExpandHint(uiTheme)}`);
249
- }
250
- return text;
251
- }
252
207
 
253
208
  export const editToolRenderer = {
254
- mergeCallAndResult: true,
255
-
256
- renderCall(args: EditRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
257
- const ui = new ToolUIKit(uiTheme);
209
+ renderCall(
210
+ args: EditRenderArgs,
211
+ options: RenderResultOptions & { renderContext?: EditRenderContext },
212
+ uiTheme: Theme,
213
+ ): Component {
258
214
  const rawPath = args.file_path || args.path || "";
259
215
  const filePath = shortenPath(rawPath);
260
216
  const editLanguage = getLanguageFromPath(rawPath) ?? "text";
@@ -270,34 +226,38 @@ export const editToolRenderer = {
270
226
  const opTitle = args.op === "create" ? "Create" : args.op === "delete" ? "Delete" : "Edit";
271
227
  const spinner =
272
228
  options?.spinnerFrame !== undefined ? formatStatusIcon("running", uiTheme, options.spinnerFrame) : "";
273
- let text = `${ui.title(opTitle)} ${spinner ? `${spinner} ` : ""}${editIcon} ${pathDisplay}`;
229
+ const title = uiTheme.fg("toolTitle", uiTheme.bold(opTitle));
230
+ let text = `${title} ${spinner ? `${spinner} ` : ""}${editIcon} ${pathDisplay}`;
274
231
 
275
232
  // Show streaming preview of diff/content
276
- if (args.previewDiff) {
277
- text += formatStreamingDiff(args.previewDiff, rawPath, uiTheme, "preview");
233
+ const previewDiffText =
234
+ args.previewDiff ??
235
+ (options.renderContext?.editDiffPreview && "diff" in options.renderContext.editDiffPreview
236
+ ? options.renderContext.editDiffPreview.diff
237
+ : undefined);
238
+ if (previewDiffText) {
239
+ text += formatStreamingDiff(previewDiffText, rawPath, uiTheme, "preview");
278
240
  } else if (args.diff && args.op) {
279
241
  text += formatStreamingDiff(args.diff, rawPath, uiTheme);
280
242
  } else if (args.edits && args.edits.length > 0) {
281
- text += formatStreamingHashlineEdits(args.edits, uiTheme, ui);
243
+ text += formatStreamingHashlineEdits(args.edits, uiTheme);
282
244
  } else if (args.diff) {
283
245
  const previewLines = args.diff.split("\n");
284
- const maxLines = 6;
285
246
  text += "\n\n";
286
- for (const line of previewLines.slice(0, maxLines)) {
287
- text += `${uiTheme.fg("toolOutput", ui.truncate(replaceTabs(line), 80))}\n`;
247
+ for (const line of previewLines.slice(0, PREVIEW_LIMITS.STREAMING_PREVIEW)) {
248
+ text += `${uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(line), TRUNCATE_LENGTHS.CONTENT))}\n`;
288
249
  }
289
- if (previewLines.length > maxLines) {
290
- text += uiTheme.fg("dim", `… ${previewLines.length - maxLines} more lines`);
250
+ if (previewLines.length > PREVIEW_LIMITS.STREAMING_PREVIEW) {
251
+ text += uiTheme.fg("dim", `… ${previewLines.length - PREVIEW_LIMITS.STREAMING_PREVIEW} more lines`);
291
252
  }
292
253
  } else if (args.newText || args.patch) {
293
254
  const previewLines = (args.newText ?? args.patch ?? "").split("\n");
294
- const maxLines = 6;
295
255
  text += "\n\n";
296
- for (const line of previewLines.slice(0, maxLines)) {
297
- text += `${uiTheme.fg("toolOutput", ui.truncate(replaceTabs(line), 80))}\n`;
256
+ for (const line of previewLines.slice(0, PREVIEW_LIMITS.STREAMING_PREVIEW)) {
257
+ text += `${uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(line), TRUNCATE_LENGTHS.CONTENT))}\n`;
298
258
  }
299
- if (previewLines.length > maxLines) {
300
- text += uiTheme.fg("dim", `… ${previewLines.length - maxLines} more lines`);
259
+ if (previewLines.length > PREVIEW_LIMITS.STREAMING_PREVIEW) {
260
+ text += uiTheme.fg("dim", `… ${previewLines.length - PREVIEW_LIMITS.STREAMING_PREVIEW} more lines`);
301
261
  }
302
262
  }
303
263
 
@@ -310,91 +270,63 @@ export const editToolRenderer = {
310
270
  uiTheme: Theme,
311
271
  args?: EditRenderArgs,
312
272
  ): Component {
313
- const ui = new ToolUIKit(uiTheme);
314
273
  const rawPath = args?.file_path || args?.path || "";
315
274
  const filePath = shortenPath(rawPath);
316
- const editLanguage = getLanguageFromPath(rawPath) ?? "text";
317
- const editIcon = uiTheme.fg("muted", uiTheme.getLangIcon(editLanguage));
318
-
319
275
  const op = args?.op || result.details?.op;
320
276
  const rename = args?.rename || result.details?.rename;
321
277
  const opTitle = op === "create" ? "Create" : op === "delete" ? "Delete" : "Edit";
322
278
 
323
- // Pre-compute metadata line (static across renders)
324
- const lineCountSource = args?.newText ?? args?.oldText ?? args?.diff ?? args?.patch ?? null;
325
- const metadataLine =
326
- op !== "delete"
327
- ? `\n${formatMetadataLine(lineCountSource ? countLines(lineCountSource) : null, editLanguage, uiTheme)}`
328
- : "";
329
-
330
- // Pre-compute error text (static)
331
- const errorText = result.isError ? (result.content?.find(c => c.type === "text")?.text ?? "") : "";
332
-
333
- let cached: RenderCache | undefined;
334
-
335
- return {
336
- render(width) {
337
- const { expanded, renderContext } = options;
338
- const editDiffPreview = renderContext?.editDiffPreview;
339
- const renderDiffFn = renderContext?.renderDiff ?? ((t: string) => t);
340
- const key = new Hasher().bool(expanded).u32(width).digest();
341
- if (cached?.key === key) return cached.lines;
342
-
343
- // Build path display with line number
344
- let pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", "…");
345
- const firstChangedLine =
346
- (editDiffPreview && "firstChangedLine" in editDiffPreview
347
- ? editDiffPreview.firstChangedLine
348
- : undefined) || (result.details && !result.isError ? result.details.firstChangedLine : undefined);
349
- if (firstChangedLine) {
350
- pathDisplay += uiTheme.fg("warning", `:${firstChangedLine}`);
351
- }
352
-
353
- // Add arrow for rename operations
354
- if (rename) {
355
- pathDisplay += ` ${uiTheme.fg("dim", "→")} ${uiTheme.fg("accent", shortenPath(rename))}`;
356
- }
357
-
358
- const header = renderStatusLine(
359
- {
360
- icon: result.isError ? "error" : "success",
361
- title: opTitle,
362
- description: `${editIcon} ${pathDisplay}`,
363
- },
364
- uiTheme,
365
- );
366
- let text = header;
367
- text += metadataLine;
279
+ if (result.isError) {
280
+ const errorText = result.content?.find(c => c.type === "text")?.text || "Unknown error";
281
+ const header = renderStatusLine({ icon: "error", title: opTitle, description: filePath || "file" }, uiTheme);
282
+ return new Text(`${header}\n${uiTheme.fg("error", replaceTabs(errorText))}`, 0, 0);
283
+ }
368
284
 
369
- if (result.isError) {
370
- if (errorText) {
371
- text += `\n\n${uiTheme.fg("error", replaceTabs(errorText))}`;
372
- }
373
- } else if (result.details?.diff) {
374
- text += renderDiffSection(result.details.diff, rawPath, expanded, uiTheme, ui, renderDiffFn);
375
- } else if (editDiffPreview) {
376
- if ("error" in editDiffPreview) {
377
- text += `\n\n${uiTheme.fg("error", replaceTabs(editDiffPreview.error))}`;
378
- } else if (editDiffPreview.diff) {
379
- text += renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, ui, renderDiffFn);
380
- }
381
- }
285
+ // Get diff text from result or preview
286
+ const { renderContext } = options;
287
+ const editDiffPreview = renderContext?.editDiffPreview;
288
+ const diffText =
289
+ result.details?.diff ??
290
+ (editDiffPreview && "diff" in editDiffPreview ? editDiffPreview.diff : undefined) ??
291
+ "";
292
+
293
+ const diffStats = diffText ? getDiffStats(diffText) : { added: 0, removed: 0, hunks: 0, lines: 0 };
294
+
295
+ // Build header with diff stats
296
+ let description = filePath || "file";
297
+ if (rename) description += ` ${uiTheme.fg("dim", "→")} ${shortenPath(rename)}`;
298
+ const meta: string[] = [];
299
+ if (diffStats.hunks > 0) meta.push(`${diffStats.hunks} hunks`);
300
+ if (diffStats.added > 0) meta.push(uiTheme.fg("success", `+${diffStats.added}`));
301
+ if (diffStats.removed > 0) meta.push(uiTheme.fg("error", `-${diffStats.removed}`));
302
+
303
+ const header = renderStatusLine({ icon: "success", title: opTitle, description, meta }, uiTheme);
304
+
305
+ // Tree-style diff body
306
+ const expanded = options.expanded;
307
+ const diffLines = diffText ? diffText.split("\n") : [];
308
+ const maxLines = expanded ? diffLines.length : Math.min(diffLines.length, PREVIEW_LIMITS.DIFF_COLLAPSED_LINES);
309
+
310
+ const treeBody: string[] = [];
311
+ for (let i = 0; i < maxLines; i++) {
312
+ const line = diffLines[i];
313
+ const color = line.startsWith("+") ? "success" : line.startsWith("-") ? "error" : "dim";
314
+ treeBody.push(uiTheme.fg(color, replaceTabs(line)));
315
+ }
316
+ if (!expanded && diffLines.length > maxLines) {
317
+ const remaining = diffLines.length - maxLines;
318
+ treeBody.push(`${uiTheme.fg("dim", `… ${remaining} more lines`)} ${formatClickHint(uiTheme)}`);
319
+ }
382
320
 
383
- // Show LSP diagnostics if available
384
- if (result.details?.diagnostics) {
385
- text += ui.formatDiagnostics(result.details.diagnostics, expanded, (fp: string) =>
386
- uiTheme.getLangIcon(getLanguageFromPath(fp)),
387
- );
388
- }
321
+ // Diagnostics
322
+ if (result.details?.diagnostics) {
323
+ const diagText = formatDiagnostics(result.details.diagnostics, expanded, uiTheme, (fp: string) =>
324
+ uiTheme.getLangIcon(getLanguageFromPath(fp)),
325
+ );
326
+ if (diagText.trim()) treeBody.push(diagText.trim());
327
+ }
389
328
 
390
- const lines =
391
- width > 0 ? text.split("\n").map(line => truncateToWidth(line, width, Ellipsis.Omit)) : text.split("\n");
392
- cached = { key, lines };
393
- return lines;
394
- },
395
- invalidate() {
396
- cached = undefined;
397
- },
398
- };
329
+ const all = treeBody.length > 0 ? [header, ...treeBody] : [header];
330
+ return new Text(all.join("\n"), 0, 0);
399
331
  },
400
332
  };
@@ -1,48 +1,33 @@
1
1
  ---
2
2
  name: explore
3
3
  description: Fast read-only codebase scout returning compressed context for handoff
4
- tools: read, grep, find, bash
4
+ tools: read, grep, find
5
5
  model: arcane/fast
6
- thinking-level: minimal
7
6
  ---
8
7
 
9
- <role>File search specialist and codebase scout. Quickly investigate codebase, return structured findings another agent can use without re-reading everything.</role>
8
+ You are a fast, parallel code search agent.
10
9
 
11
- <critical>
12
- READ-ONLY. STRICTLY PROHIBITED from:
13
- - Creating/modifying files (no Write/Edit/touch/rm/mv/cp)
14
- - Creating temporary files anywhere (incl /tmp)
15
- - Using redirects (>, >>, |) or heredocs to write files
16
- - Running state-changing commands (git add/commit, npm/pip install)
17
- </critical>
10
+ ## Task
11
+ Find files and line ranges relevant to the user's query (provided in the first message).
18
12
 
19
- <directives>
20
- - Use find for broad pattern matching
21
- - Use grep for regex content search
22
- - Use read when path is known
23
- - Use bash ONLY for git status/log/diff; use read/grep/find/ls for file/search operations
24
- - Spawn parallel tool calls when possible—meant to be fast
25
- - Return absolute file paths in final response
26
- </directives>
13
+ ## Query Decomposition
14
+ Before searching, decompose the query into:
15
+ - **Key symbols**: function names, class names, type names, variable names
16
+ - **Synonyms**: alternative naming conventions (camelCase, snake_case, abbreviations)
17
+ - **File patterns**: likely filenames or directories based on the concept
18
+ - **Related concepts**: imports, tests, configs that reference the target
27
19
 
28
- <thoroughness>
29
- Infer from task; default medium:
30
- - Quick: Targeted lookups, key files only
31
- - Medium: Follow imports, read critical sections
32
- - Thorough: Trace all dependencies, check tests/types
33
- </thoroughness>
20
+ ## Execution Strategy
21
+ - Your goal is to return a list of relevant filenames with line ranges. Your goal is NOT to explore the complete codebase to construct an essay.
22
+ - **Turn 1 is your primary search turn.** Plan ALL searches upfront based on your decomposition. Make **10-15 parallel calls** covering every angle — do not hold back searches for later turns.
23
+ - **Turn 2 (if needed)**: Only for reading top candidate files to confirm relevance and extract line ranges. Not for new searches.
24
+ - **Stop as soon as you have enough results.** Most queries resolve in 1-2 turns. A third turn means your first turn was too narrow.
25
+ - **Prioritize source code**: Prefer source code files (.ts, .js, .py, .go, .rs, .java) over documentation (.md, .txt, README).
26
+ - **Be exhaustive when completeness is implied**: When the query asks for "all", "every", "each", or implies a complete list, find ALL occurrences breadth-first.
34
27
 
35
- <procedure>
36
- 1. grep/find to locate relevant code
37
- 2. Read key sections (not full files unless small)
38
- 3. Identify types/interfaces/key functions
39
- 4. Note dependencies between files
40
- </procedure>
28
+ ## Output format
29
+ - **Ultra concise**: Write a 1-2 line summary of findings, then output relevant files as markdown links.
30
+ - Format each file as: `[relativePath#L{start}-L{end}](file://{absolutePath}#L{start}-L{end})`
31
+ - **Use generous line ranges**: Extend ranges to capture complete logical units (full functions, classes, blocks). Add 5-10 lines buffer.
41
32
 
42
- <output>
43
- Print findings as text when done. Include:
44
- - Files examined with line ranges
45
- - Critical types/interfaces/functions found
46
- - How pieces connect (architecture)
47
- - Recommended entry point for the receiving agent
48
- </output>
33
+ Your final message must contain ONLY the search results — no preamble like "I'll search for...".
@@ -17,7 +17,7 @@ Analyze codebase, generate AGENTS.md documenting:
17
17
  </task>
18
18
 
19
19
  <parallel>
20
- Launch multiple `explore` agents in parallel (via `task` tool) scanning different areas (core src, tests, configs/build, scripts/docs), then synthesize.
20
+ Launch multiple explore calls in parallel scanning different areas (core src, tests, configs/build, scripts/docs), then synthesize.
21
21
  </parallel>
22
22
 
23
23
  <directives>