@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
package/src/tools/read.ts CHANGED
@@ -8,11 +8,8 @@ import { Text } from "@nghyane/arcane-tui";
8
8
  import { ptree, untilAborted } from "@nghyane/arcane-utils";
9
9
  import { getRemoteDir } from "@nghyane/arcane-utils/dirs";
10
10
  import { type Static, Type } from "@sinclair/typebox";
11
- import { renderPromptTemplate } from "../config/prompt-templates";
12
11
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
13
- import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
14
12
  import { computeLineHash } from "../patch/hashline";
15
- import readDescription from "../prompts/tools/read.md" with { type: "text" };
16
13
  import type { ToolSession } from "../sdk";
17
14
  import {
18
15
  DEFAULT_MAX_BYTES,
@@ -22,18 +19,17 @@ import {
22
19
  truncateHead,
23
20
  truncateStringToBytesFromStart,
24
21
  } from "../session/streaming-output";
25
- import { renderCodeCell, renderStatusLine } from "../tui";
26
- import { CachedOutputBlock } from "../tui/output-block";
22
+ import { getLanguageFromPath, type Theme } from "../theme/theme";
23
+ import { renderStatusLine } from "../tui";
24
+ import { formatAge, formatCount, formatErrorMessage, shortenPath } from "../ui/render-utils";
27
25
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
28
26
  import { formatDimensionNote, resizeImage } from "../utils/image-resize";
29
27
  import { detectSupportedImageMimeTypeFromFile } from "../utils/mime";
30
28
  import { ensureTool } from "../utils/tools-manager";
31
29
  import { applyListLimit } from "./list-limit";
32
- import type { OutputMeta } from "./output-meta";
30
+ import { type OutputMeta, toolResult } from "./output-meta";
33
31
  import { resolveReadPath, resolveToCwd } from "./path-utils";
34
- import { formatAge, shortenPath, wrapBrackets } from "./render-utils";
35
32
  import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
36
- import { toolResult } from "./tool-result";
37
33
 
38
34
  // Document types convertible via markitdown
39
35
  const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".rtf", ".epub"]);
@@ -512,9 +508,9 @@ async function convertWithMarkitdown(
512
508
  }
513
509
 
514
510
  const readSchema = Type.Object({
515
- path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
516
- offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
517
- limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
511
+ path: Type.String({ description: "File path (relative or absolute)" }),
512
+ offset: Type.Optional(Type.Number({ description: "Start reading from this line number (1-indexed)" })),
513
+ limit: Type.Optional(Type.Number({ description: "Max number of lines to read" })),
518
514
  });
519
515
 
520
516
  export type ReadToolInput = Static<typeof readSchema>;
@@ -534,23 +530,17 @@ type ReadParams = ReadToolInput;
534
530
  * Reads files with support for images, documents (via markitdown), and text.
535
531
  * Directories return a formatted listing with modification times.
536
532
  */
537
- export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
533
+ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails, Theme> {
538
534
  readonly name = "read";
539
535
  readonly label = "Read";
540
- readonly description: string;
536
+ description = "Read file contents, list directories, or view images";
541
537
  readonly parameters = readSchema;
542
538
  readonly nonAbortable = true;
543
539
 
544
540
  readonly #autoResizeImages: boolean;
545
541
 
546
542
  constructor(private readonly session: ToolSession) {
547
- const displayMode = resolveFileDisplayMode(session);
548
543
  this.#autoResizeImages = session.settings.get("images.autoResize");
549
- this.description = renderPromptTemplate(readDescription, {
550
- DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES),
551
- IS_HASHLINE_MODE: displayMode.hashLines,
552
- IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
553
- });
554
544
  }
555
545
 
556
546
  async execute(
@@ -1063,20 +1053,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1063
1053
 
1064
1054
  return resultBuilder.done();
1065
1055
  }
1066
- }
1067
-
1068
- // =============================================================================
1069
- // TUI Renderer
1070
- // =============================================================================
1071
-
1072
- interface ReadRenderArgs {
1073
- path?: string;
1074
- file_path?: string;
1075
- offset?: number;
1076
- limit?: number;
1077
- }
1078
1056
 
1079
- export const readToolRenderer = {
1080
1057
  renderCall(args: ReadRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
1081
1058
  const rawPath = args.file_path || args.path || "";
1082
1059
  const filePath = shortenPath(rawPath);
@@ -1092,102 +1069,42 @@ export const readToolRenderer = {
1092
1069
 
1093
1070
  const text = renderStatusLine({ icon: "pending", title: "Read", description: pathDisplay }, uiTheme);
1094
1071
  return new Text(text, 0, 0);
1095
- },
1072
+ }
1096
1073
 
1097
1074
  renderResult(
1098
- result: { content: Array<{ type: string; text?: string }>; details?: ReadToolDetails },
1075
+ result: { content: Array<{ type: string; text?: string }>; details?: ReadToolDetails; isError?: boolean },
1099
1076
  _options: RenderResultOptions,
1100
1077
  uiTheme: Theme,
1101
1078
  args?: ReadRenderArgs,
1102
1079
  ): Component {
1103
- const details = result.details;
1104
- const contentText = result.content?.find(c => c.type === "text")?.text ?? "";
1105
- const imageContent = result.content?.find(c => c.type === "image");
1106
1080
  const rawPath = args?.file_path || args?.path || "";
1107
1081
  const filePath = shortenPath(rawPath);
1108
- const lang = getLanguageFromPath(rawPath);
1109
1082
 
1110
- const warningLines: string[] = [];
1111
- const truncation = details?.meta?.truncation;
1112
- const fallback = details?.truncation;
1113
- if (details?.resolvedPath) {
1114
- warningLines.push(uiTheme.fg("dim", wrapBrackets(`Resolved path: ${details.resolvedPath}`, uiTheme)));
1115
- }
1116
- if (truncation) {
1117
- let warning: string;
1118
- if (fallback?.firstLineExceedsLimit) {
1119
- warning = `First line exceeds ${formatBytes(fallback.maxBytes ?? DEFAULT_MAX_BYTES)} limit`;
1120
- } else if (truncation.truncatedBy === "lines") {
1121
- warning = `Truncated: ${truncation.outputLines} of ${truncation.totalLines} lines (${DEFAULT_MAX_LINES} line limit)`;
1122
- } else {
1123
- const maxBytes = fallback?.maxBytes ?? DEFAULT_MAX_BYTES;
1124
- warning = `Truncated: ${truncation.outputLines} lines (${formatBytes(maxBytes)} limit)`;
1125
- }
1126
- if (truncation.artifactId) {
1127
- warning += `. Full output: artifact://${truncation.artifactId}`;
1128
- }
1129
- warningLines.push(uiTheme.fg("warning", wrapBrackets(warning, uiTheme)));
1083
+ if (result.isError) {
1084
+ const errorText = result.content?.find(c => c.type === "text")?.text || "Unknown error";
1085
+ return new Text(formatErrorMessage(errorText, uiTheme), 0, 0);
1130
1086
  }
1131
1087
 
1132
- if (imageContent) {
1133
- const header = renderStatusLine(
1134
- { icon: "success", title: "Read", description: filePath || rawPath || "image" },
1135
- uiTheme,
1136
- );
1137
- const detailLines = contentText ? contentText.split("\n").map(line => uiTheme.fg("toolOutput", line)) : [];
1138
- const lines = [...detailLines, ...warningLines];
1139
- const outputBlock = new CachedOutputBlock();
1140
- return {
1141
- render: (width: number) =>
1142
- outputBlock.render(
1143
- {
1144
- header,
1145
- state: "success",
1146
- sections: [
1147
- {
1148
- label: uiTheme.fg("toolTitle", "Details"),
1149
- lines: lines.length > 0 ? lines : [uiTheme.fg("dim", "(image)")],
1150
- },
1151
- ],
1152
- width,
1153
- },
1154
- uiTheme,
1155
- ),
1156
- invalidate: () => outputBlock.invalidate(),
1157
- };
1158
- }
1088
+ const details = result.details;
1089
+ const meta: string[] = [];
1090
+ const contentText = result.content?.find(c => c.type === "text")?.text ?? "";
1091
+ const lineCount = contentText ? contentText.split("\n").length : 0;
1092
+ if (lineCount > 0) meta.push(formatCount("line", lineCount));
1093
+ const lang = getLanguageFromPath(rawPath);
1094
+ if (lang) meta.push(lang);
1095
+ if (details?.meta?.truncation) meta.push(uiTheme.fg("warning", "truncated"));
1159
1096
 
1160
- let title = filePath ? `Read ${filePath}` : "Read";
1161
- if (args?.offset !== undefined || args?.limit !== undefined) {
1162
- const startLine = args.offset ?? 1;
1163
- const endLine = args.limit !== undefined ? startLine + args.limit - 1 : "";
1164
- title += `:${startLine}${endLine ? `-${endLine}` : ""}`;
1165
- }
1166
- let cachedWidth: number | undefined;
1167
- let cachedLines: string[] | undefined;
1168
- return {
1169
- render: (width: number) => {
1170
- if (cachedLines && cachedWidth === width) return cachedLines;
1171
- cachedLines = renderCodeCell(
1172
- {
1173
- code: contentText,
1174
- language: lang,
1175
- title,
1176
- status: "complete",
1177
- output: warningLines.length > 0 ? warningLines.join("\n") : undefined,
1178
- expanded: true,
1179
- width,
1180
- },
1181
- uiTheme,
1182
- );
1183
- cachedWidth = width;
1184
- return cachedLines;
1185
- },
1186
- invalidate: () => {
1187
- cachedWidth = undefined;
1188
- cachedLines = undefined;
1189
- },
1190
- };
1191
- },
1192
- mergeCallAndResult: true,
1193
- };
1097
+ const text = renderStatusLine(
1098
+ { icon: "success", title: "Read", description: filePath || rawPath || "file", meta },
1099
+ uiTheme,
1100
+ );
1101
+ return new Text(text, 0, 0);
1102
+ }
1103
+ }
1104
+
1105
+ interface ReadRenderArgs {
1106
+ path?: string;
1107
+ file_path?: string;
1108
+ offset?: number;
1109
+ limit?: number;
1110
+ }
@@ -1,23 +1,10 @@
1
1
  import { Type } from "@sinclair/typebox";
2
- import reviewerDescription from "../prompts/tools/reviewer.md" with { type: "text" };
3
- import { createSubagentTool } from "./subagent-tool";
2
+ import type { SubagentConfig } from "./subagent-tool";
4
3
 
5
4
  const schema = Type.Object({
6
- diff_description: Type.String({
7
- description:
8
- 'A description or command identifying the diff to review. Examples: "uncommitted changes", "last commit", "PR #42", "changes against main branch".',
9
- }),
10
- files: Type.Optional(
11
- Type.Array(Type.String(), {
12
- description: "Specific file paths to focus the review on. If omitted, all changed files are reviewed.",
13
- }),
14
- ),
15
- instructions: Type.Optional(
16
- Type.String({
17
- description:
18
- 'Additional guidance for the reviewer. Examples: "Focus on error handling", "Check for race conditions".',
19
- }),
20
- ),
5
+ diff_description: Type.String({ description: "Description of the diff or change to review" }),
6
+ files: Type.Optional(Type.Array(Type.String(), { description: "Specific files to focus the review on" })),
7
+ instructions: Type.Optional(Type.String({ description: "Additional review instructions" })),
21
8
  });
22
9
 
23
10
  function buildTask(p: Record<string, unknown>): string {
@@ -28,14 +15,25 @@ function buildTask(p: Record<string, unknown>): string {
28
15
  return parts.join("\n");
29
16
  }
30
17
 
31
- export const ReviewerTool = createSubagentTool({
18
+ export const reviewerConfig: SubagentConfig<typeof schema.properties> = {
32
19
  name: "code_review",
33
20
  label: "Code Review",
34
21
  agent: "reviewer",
35
22
  schema,
36
- descriptionTemplate: reviewerDescription,
37
23
  progressText: "Reviewing code...",
38
24
  tmpPrefix: "arc-review-",
39
25
  buildTask,
40
- buildDescription: p => `Review: ${(p.diff_description as string).slice(0, 60)}`,
41
- });
26
+ buildDescription: p => String(p.diff_description ?? "").slice(0, 80),
27
+ buildContextLine: p => {
28
+ const parts: string[] = [];
29
+ const files = p.files as string[] | undefined;
30
+ if (files?.length) parts.push(`${files.length} file${files.length > 1 ? "s" : ""}`);
31
+ if (p.instructions) parts.push(String(p.instructions).slice(0, 50));
32
+ return parts.length > 0 ? parts.join(" · ") : null;
33
+ },
34
+ toolDescription: [
35
+ "Code review specialist — spawns reviewer agent on a diff.",
36
+ 'Pass diff_description (e.g. "uncommitted changes", "last commit"), optionally files and instructions.',
37
+ ].join(" "),
38
+ passContext: false,
39
+ };
@@ -0,0 +1,128 @@
1
+ import type {
2
+ AgentTool,
3
+ AgentToolContext,
4
+ AgentToolResult,
5
+ AgentToolUpdateCallback,
6
+ RenderResultOptions,
7
+ } from "@nghyane/arcane-agent";
8
+ import type { Component } from "@nghyane/arcane-tui";
9
+ import { Text } from "@nghyane/arcane-tui";
10
+ import { Type } from "@sinclair/typebox";
11
+ import type { Theme } from "../theme/theme";
12
+ import { renderStatusLine } from "../tui";
13
+ import { formatCount, formatErrorMessage, truncateToWidth } from "../ui/render-utils";
14
+ import { GrepAppProvider, type SearchCodeSource } from "../web/search/providers/grep";
15
+ import { renderSearchCall, type SearchRenderDetails } from "../web/search/render";
16
+
17
+ const grepProvider = new GrepAppProvider();
18
+
19
+ const searchCodeSchema = Type.Object({
20
+ query: Type.String({ description: "Search query or code pattern" }),
21
+ regexp: Type.Optional(Type.Boolean({ description: "Treat query as regex" })),
22
+ language: Type.Optional(Type.String({ description: "Filter by programming language" })),
23
+ repo: Type.Optional(Type.String({ description: "Filter by repository (owner/repo)" })),
24
+ limit: Type.Optional(Type.Number({ description: "Max number of results" })),
25
+ });
26
+
27
+ interface SearchCodeToolParams {
28
+ query: string;
29
+ regexp?: boolean;
30
+ language?: string;
31
+ repo?: string;
32
+ limit?: number;
33
+ }
34
+
35
+ export class SearchCodeTool implements AgentTool<typeof searchCodeSchema, SearchRenderDetails, Theme> {
36
+ readonly name = "search_code";
37
+ readonly label = "Code Search";
38
+ readonly description = "Search source code across public GitHub repositories via grep.app";
39
+ readonly parameters = searchCodeSchema;
40
+ readonly renderCall = renderSearchCall;
41
+
42
+ renderResult(
43
+ result: { content: Array<{ type: string; text?: string }>; details?: SearchRenderDetails; isError?: boolean },
44
+ _options: RenderResultOptions,
45
+ theme: Theme,
46
+ args?: SearchCodeToolParams,
47
+ ): Component {
48
+ if (result.isError || result.details?.error) {
49
+ const errorText =
50
+ result.details?.error || result.content?.find(c => c.type === "text")?.text || "Unknown error";
51
+ return new Text(formatErrorMessage(errorText, theme), 0, 0);
52
+ }
53
+ const query = args?.query ? truncateToWidth(args.query, 60) : "code";
54
+ const sourceCount = result.details?.response?.sources?.length ?? 0;
55
+ return new Text(
56
+ renderStatusLine(
57
+ {
58
+ icon: sourceCount > 0 ? "success" : "warning",
59
+ title: "Code Search",
60
+ description: `"${query}"`,
61
+ meta: [formatCount("result", sourceCount)],
62
+ },
63
+ theme,
64
+ ),
65
+ 0,
66
+ 0,
67
+ );
68
+ }
69
+
70
+ async execute(
71
+ _toolCallId: string,
72
+ params: SearchCodeToolParams,
73
+ _signal?: AbortSignal,
74
+ _onUpdate?: AgentToolUpdateCallback<SearchRenderDetails>,
75
+ _context?: AgentToolContext,
76
+ ): Promise<AgentToolResult<SearchRenderDetails>> {
77
+ try {
78
+ const response = await grepProvider.search({
79
+ query: params.query,
80
+ regexp: params.regexp,
81
+ language: params.language,
82
+ repo: params.repo,
83
+ limit: params.limit,
84
+ });
85
+
86
+ const parts: string[] = [];
87
+ if (response.sources.length === 0) {
88
+ parts.push("No code results found.");
89
+ } else {
90
+ const header = response.total
91
+ ? `Found ${response.sources.length} of ${response.total} total matches across public GitHub repos.`
92
+ : `Found ${response.sources.length} code result(s) across public GitHub repos.`;
93
+ parts.push(header);
94
+
95
+ if (response.topLanguages && response.topLanguages.length > 0) {
96
+ parts.push(`Top languages: ${response.topLanguages.map(l => `${l.val} (${l.count})`).join(", ")}`);
97
+ }
98
+ if (response.topRepos && response.topRepos.length > 0) {
99
+ parts.push(`Top repos: ${response.topRepos.map(r => `${r.val} (${r.count})`).join(", ")}`);
100
+ }
101
+ parts.push("");
102
+
103
+ for (const source of response.sources as SearchCodeSource[]) {
104
+ const matchInfo = source.matchCount ? ` (${source.matchCount} matches)` : "";
105
+ parts.push(`### ${source.title}${matchInfo}`);
106
+ parts.push(source.url);
107
+ if (source.snippet) {
108
+ const lineInfo = source.lineNumbers?.[0] ? `L${source.lineNumbers[0]}` : "";
109
+ parts.push(`\`\`\`${lineInfo}\n${source.snippet}\n\`\`\``);
110
+ }
111
+ parts.push("");
112
+ }
113
+ }
114
+
115
+ return {
116
+ content: [{ type: "text" as const, text: parts.join("\n") }],
117
+ details: { response, error: undefined },
118
+ };
119
+ } catch (error) {
120
+ const message = `Code search failed: ${error instanceof Error ? error.message : String(error)}`;
121
+
122
+ return {
123
+ content: [{ type: "text" as const, text: message }],
124
+ details: { response: { provider: "grep" as const, sources: [] }, error: message },
125
+ };
126
+ }
127
+ }
128
+ }
package/src/tools/ssh.ts CHANGED
@@ -4,29 +4,25 @@ import { Text } from "@nghyane/arcane-tui";
4
4
  import { type Static, Type } from "@sinclair/typebox";
5
5
  import type { SSHHost } from "../capability/ssh";
6
6
  import { sshCapability } from "../capability/ssh";
7
- import { renderPromptTemplate } from "../config/prompt-templates";
8
7
  import { loadCapability } from "../discovery";
9
8
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
10
- import type { Theme } from "../modes/theme/theme";
11
- import sshDescriptionBase from "../prompts/tools/ssh.md" with { type: "text" };
12
9
  import { DEFAULT_MAX_BYTES } from "../session/streaming-output";
13
10
  import type { SSHHostInfo } from "../ssh/connection-manager";
14
11
  import { ensureHostInfo, getHostInfoForHost } from "../ssh/connection-manager";
15
12
  import { executeSSH } from "../ssh/ssh-executor";
13
+ import type { Theme } from "../theme/theme";
16
14
  import { renderStatusLine } from "../tui";
17
- import { CachedOutputBlock } from "../tui/output-block";
15
+ import { formatClickHint, replaceTabs } from "../ui/render-utils";
18
16
  import type { ToolSession } from ".";
19
- import type { OutputMeta } from "./output-meta";
17
+ import { type OutputMeta, toolResult } from "./output-meta";
20
18
  import { allocateOutputArtifact, createTailBuffer } from "./output-utils";
21
- import { formatBytes, wrapBrackets } from "./render-utils";
22
19
  import { ToolError } from "./tool-errors";
23
- import { toolResult } from "./tool-result";
24
20
 
25
21
  const sshSchema = Type.Object({
26
- host: Type.String({ description: "Host name from managed SSH config or discovered ssh.json files" }),
27
- command: Type.String({ description: "Command to execute on the remote host" }),
28
- cwd: Type.Optional(Type.String({ description: "Remote working directory (optional)" })),
29
- timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (default: 60)" })),
22
+ host: Type.String({ description: "SSH host alias (from ~/.ssh/config)" }),
23
+ command: Type.String({ description: "Shell command to execute on the remote host" }),
24
+ cwd: Type.Optional(Type.String({ description: "Working directory on the remote host" })),
25
+ timeout: Type.Optional(Type.Number({ description: "Timeout in milliseconds" })),
30
26
  });
31
27
 
32
28
  export interface SSHToolDetails {
@@ -60,12 +56,9 @@ async function formatHostEntry(host: SSHHost): Promise<string> {
60
56
  }
61
57
 
62
58
  async function formatDescription(hosts: SSHHost[]): Promise<string> {
63
- const baseDescription = renderPromptTemplate(sshDescriptionBase);
64
- if (hosts.length === 0) {
65
- return baseDescription;
66
- }
59
+ if (hosts.length === 0) return "";
67
60
  const hostList = (await Promise.all(hosts.map(formatHostEntry))).join("\n");
68
- return `${baseDescription}\n\nAvailable hosts:\n${hostList}`;
61
+ return `Available hosts:\n${hostList}`;
69
62
  }
70
63
 
71
64
  function quoteRemotePath(value: string): string {
@@ -119,7 +112,7 @@ async function loadHosts(session: ToolSession): Promise<{
119
112
 
120
113
  type SshToolParams = Static<typeof sshSchema>;
121
114
 
122
- export class SshTool implements AgentTool<typeof sshSchema, SSHToolDetails> {
115
+ export class SshTool implements AgentTool<typeof sshSchema, SSHToolDetails, Theme> {
123
116
  readonly name = "ssh";
124
117
  readonly label = "SSH";
125
118
  readonly parameters = sshSchema;
@@ -193,6 +186,62 @@ export class SshTool implements AgentTool<typeof sshSchema, SSHToolDetails> {
193
186
 
194
187
  return resultBuilder.done();
195
188
  }
189
+
190
+ renderCall(args: SshRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
191
+ const host = args.host || "…";
192
+ const command = args.command || "…";
193
+ const text = renderStatusLine({ icon: "pending", title: "SSH", description: `[${host}] $ ${command}` }, uiTheme);
194
+ return new Text(text, 0, 0);
195
+ }
196
+
197
+ renderResult(
198
+ result: { content: Array<{ type: string; text?: string }>; details?: SSHToolDetails },
199
+ options: RenderResultOptions & { renderContext?: SshRenderContext },
200
+ uiTheme: Theme,
201
+ args?: SshRenderArgs,
202
+ ): Component {
203
+ const host = args?.host || "…";
204
+ const command = args?.command || "…";
205
+ const cmdText = `[${host}] $ ${command}`;
206
+ const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
207
+ const output = textContent.trimEnd();
208
+ const outputLines = output ? output.split("\n") : [];
209
+ const total = outputLines.length;
210
+ const truncation = result.details?.meta?.truncation;
211
+ const isError = false;
212
+
213
+ const meta: string[] = [];
214
+ if (total > 0) meta.push(`${total} lines`);
215
+
216
+ const header = renderStatusLine(
217
+ { icon: isError ? "error" : "success", title: "SSH", description: cmdText, meta },
218
+ uiTheme,
219
+ );
220
+
221
+ const TAIL = 4;
222
+ const expanded = options.expanded;
223
+ const showAll = isError || expanded;
224
+ const displayLines = showAll ? outputLines : outputLines.slice(-TAIL);
225
+ const skipped = total - displayLines.length;
226
+
227
+ const bodyLines: string[] = [];
228
+ if (skipped > 0) {
229
+ bodyLines.push(uiTheme.fg("dim", `… (${skipped} earlier lines)`));
230
+ }
231
+ const hasTruncation = Boolean(truncation);
232
+ for (let i = 0; i < displayLines.length; i++) {
233
+ bodyLines.push(uiTheme.fg("toolOutput", replaceTabs(displayLines[i])));
234
+ }
235
+ if (hasTruncation) {
236
+ bodyLines.push(uiTheme.fg("warning", "output truncated"));
237
+ }
238
+ if (!showAll && skipped > 0) {
239
+ bodyLines.push(formatClickHint(uiTheme));
240
+ }
241
+
242
+ const lines = bodyLines.length > 0 ? [header, ...bodyLines] : [header];
243
+ return new Text(lines.join("\n"), 0, 0);
244
+ }
196
245
  }
197
246
 
198
247
  export async function loadSshTool(session: ToolSession): Promise<SshTool | null> {
@@ -209,118 +258,10 @@ export async function loadSshTool(session: ToolSession): Promise<SshTool | null>
209
258
  return new SshTool(session, hostNames, hostsByName, description);
210
259
  }
211
260
 
212
- // =============================================================================
213
- // TUI Renderer
214
- // =============================================================================
215
-
216
261
  interface SshRenderArgs {
217
262
  host?: string;
218
263
  command?: string;
219
264
  timeout?: number;
220
265
  }
221
266
 
222
- interface SshRenderContext {
223
- /** Visual lines for truncated output (pre-computed by tool-execution) */
224
- visualLines?: string[];
225
- /** Number of lines skipped */
226
- skippedCount?: number;
227
- /** Total visual lines */
228
- totalVisualLines?: number;
229
- }
230
-
231
- export const sshToolRenderer = {
232
- renderCall(args: SshRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
233
- const host = args.host || "…";
234
- const command = args.command || "…";
235
- const text = renderStatusLine({ icon: "pending", title: "SSH", description: `[${host}] $ ${command}` }, uiTheme);
236
- return new Text(text, 0, 0);
237
- },
238
-
239
- renderResult(
240
- result: {
241
- content: Array<{ type: string; text?: string }>;
242
- details?: SSHToolDetails;
243
- },
244
- options: RenderResultOptions & { renderContext?: SshRenderContext },
245
- uiTheme: Theme,
246
- args?: SshRenderArgs,
247
- ): Component {
248
- const details = result.details;
249
- const host = args?.host || "…";
250
- const command = args?.command || "…";
251
- const header = renderStatusLine(
252
- { icon: "success", title: "SSH", description: `[${host}] $ ${command}` },
253
- uiTheme,
254
- );
255
- const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
256
- const truncation = details?.meta?.truncation;
257
- const outputBlock = new CachedOutputBlock();
258
-
259
- return {
260
- render: (width: number): string[] => {
261
- // REACTIVE: read mutable options at render time
262
- const { expanded, renderContext } = options;
263
- const output = textContent.trimEnd();
264
- const outputLines: string[] = [];
265
-
266
- if (output) {
267
- if (expanded) {
268
- outputLines.push(...output.split("\n").map(line => uiTheme.fg("toolOutput", line)));
269
- } else if (renderContext?.visualLines) {
270
- const { visualLines, skippedCount = 0, totalVisualLines = visualLines.length } = renderContext;
271
- if (skippedCount > 0) {
272
- outputLines.push(
273
- uiTheme.fg(
274
- "dim",
275
- `… (${skippedCount} earlier lines, showing ${visualLines.length} of ${totalVisualLines}) (ctrl+o to expand)`,
276
- ),
277
- );
278
- }
279
- const styledVisual = visualLines.map(line =>
280
- line.includes("\x1b[") ? line : uiTheme.fg("toolOutput", line),
281
- );
282
- outputLines.push(...styledVisual);
283
- } else {
284
- const outputLinesRaw = output.split("\n");
285
- const maxLines = 5;
286
- const displayLines = outputLinesRaw.slice(0, maxLines);
287
- const remaining = outputLinesRaw.length - maxLines;
288
- outputLines.push(...displayLines.map(line => uiTheme.fg("toolOutput", line)));
289
- if (remaining > 0) {
290
- outputLines.push(uiTheme.fg("dim", `… (${remaining} more lines) (ctrl+o to expand)`));
291
- }
292
- }
293
- }
294
-
295
- if (truncation) {
296
- const warnings: string[] = [];
297
- if (truncation.artifactId) {
298
- warnings.push(`Full output: artifact://${truncation.artifactId}`);
299
- }
300
- if (truncation.truncatedBy === "lines") {
301
- warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
302
- } else {
303
- warnings.push(
304
- `Truncated: ${truncation.outputLines} lines shown (${formatBytes(truncation.outputBytes)} limit)`,
305
- );
306
- }
307
- outputLines.push(uiTheme.fg("warning", wrapBrackets(warnings.join(". "), uiTheme)));
308
- }
309
-
310
- return outputBlock.render(
311
- {
312
- header,
313
- state: "success",
314
- sections: [{ label: uiTheme.fg("toolTitle", "Output"), lines: outputLines }],
315
- width,
316
- },
317
- uiTheme,
318
- );
319
- },
320
- invalidate: () => {
321
- outputBlock.invalidate();
322
- },
323
- };
324
- },
325
- mergeCallAndResult: true,
326
- };
267
+ interface SshRenderContext {}