@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
@@ -1,25 +1,33 @@
1
1
  /**
2
- * Generic subagent tool factory.
2
+ * Subagent tool system.
3
3
  *
4
4
  * All standalone subagent tools (explore, oracle, librarian, code_review)
5
- * share identical execution logic. This factory eliminates the boilerplate.
5
+ * share identical execution logic. Each tool is a config object;
6
+ * SubagentTool is the single class that runs them.
7
+ *
8
+ * Rendering is assigned to the tool instance via createUnifiedSubagentRenderer.
6
9
  */
7
10
  import * as fs from "node:fs/promises";
8
11
  import * as os from "node:os";
9
12
  import * as path from "node:path";
10
13
  import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@nghyane/arcane-agent";
14
+ import type { Component } from "@nghyane/arcane-tui";
11
15
  import { Snowflake } from "@nghyane/arcane-utils";
12
16
  import type { TObject, TProperties } from "@sinclair/typebox";
13
17
  import type { ToolSession } from "..";
14
18
  import { isDefaultModelAlias } from "../config/model-resolver";
15
- import { renderPromptTemplate } from "../config/prompt-templates";
16
- import type { Theme } from "../modes/theme/theme";
19
+ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
17
20
  import { getBundledAgent } from "../task/agents";
18
21
  import { runAgent } from "../task/executor";
19
22
  import { AgentOutputManager } from "../task/output-manager";
20
- import type { AgentProgress, TaskToolDetails } from "../task/types";
21
-
22
- export interface SubagentToolConfig<T extends TProperties> {
23
+ import { extractAgentOutput, ProgressTracker } from "../task/progress-tracker";
24
+ import { createUnifiedSubagentRenderer } from "../task/render";
25
+ import type { TaskToolDetails } from "../task/types";
26
+ import { TASK_SUBAGENT_EVENT_CHANNEL } from "../task/types";
27
+ import type { Theme } from "../theme/theme";
28
+ import { EventBus } from "../utils/event-bus";
29
+
30
+ export interface SubagentConfig<T extends TProperties = TProperties> {
23
31
  /** Tool name exposed to the model */
24
32
  name: string;
25
33
  /** Display label in TUI */
@@ -28,142 +36,208 @@ export interface SubagentToolConfig<T extends TProperties> {
28
36
  agent: string;
29
37
  /** TypeBox schema for parameters */
30
38
  schema: TObject<T>;
31
- /** Raw .md template for tool description */
32
- descriptionTemplate: string;
33
39
  /** Progress message shown during execution */
34
40
  progressText: string;
35
41
  /** Temp directory prefix */
36
42
  tmpPrefix: string;
37
43
  /** Build the task string from parsed params */
38
44
  buildTask: (params: Record<string, unknown>) => string;
39
- /** Build the description for runAgent (shown in TUI) */
45
+ /** Build the short description shown in TUI header */
40
46
  buildDescription: (params: Record<string, unknown>) => string;
41
47
  /** Whether to pass compact conversation context to subagent (default: true) */
42
48
  passContext?: boolean;
49
+ /** One-line tool description for model context */
50
+ toolDescription?: string;
51
+ /** Build optional context line for TUI display (shown below header). Return null to hide. */
52
+ buildContextLine?: (params: Record<string, unknown>) => string | null;
43
53
  }
44
54
 
45
- export function createSubagentTool<T extends TProperties>(
46
- config: SubagentToolConfig<T>,
47
- ): new (
48
- session: ToolSession,
49
- ) => AgentTool<TObject<T>, TaskToolDetails, Theme> {
50
- const {
51
- name,
52
- label,
53
- agent: agentName,
54
- schema,
55
- descriptionTemplate,
56
- progressText,
57
- tmpPrefix,
58
- buildTask,
59
- buildDescription,
60
- passContext = true,
61
- } = config;
62
-
63
- const description = renderPromptTemplate(descriptionTemplate);
64
-
65
- return class SubagentTool implements AgentTool<TObject<T>, TaskToolDetails, Theme> {
66
- readonly name = name;
67
- readonly label = label;
68
- readonly parameters = schema;
69
- readonly description = description;
70
-
71
- constructor(private readonly session: ToolSession) {}
72
-
73
- async execute(
74
- _toolCallId: string,
75
- params: Record<string, unknown>,
76
- signal?: AbortSignal,
77
- onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
78
- ): Promise<AgentToolResult<TaskToolDetails>> {
79
- const startTime = Date.now();
80
- const agent = getBundledAgent(agentName);
81
- if (!agent) {
82
- return {
83
- content: [{ type: "text", text: `${label} agent not found.` }],
84
- details: { results: [], totalDurationMs: 0 },
85
- };
86
- }
87
-
88
- const effectiveAgentModel = isDefaultModelAlias(agent.model) ? undefined : agent.model;
89
- const modelOverride =
90
- effectiveAgentModel ?? this.session.getActiveModelString?.() ?? this.session.getModelString?.();
91
-
92
- const task = buildTask(params);
93
- const sessionFile = this.session.getSessionFile();
94
- const artifactsDir = sessionFile ? sessionFile.slice(0, -6) : null;
95
- const tempArtifactsDir = artifactsDir ? null : path.join(os.tmpdir(), `${tmpPrefix}${Snowflake.next()}`);
96
- const effectiveArtifactsDir = artifactsDir || tempArtifactsDir!;
97
-
98
- try {
99
- await fs.mkdir(effectiveArtifactsDir, { recursive: true });
100
-
101
- const outputManager =
102
- this.session.agentOutputManager ?? new AgentOutputManager(this.session.getArtifactsDir ?? (() => null));
103
- const [id] = await outputManager.allocateBatch([label]);
55
+ export class SubagentTool<T extends TProperties = TProperties>
56
+ implements AgentTool<TObject<T>, TaskToolDetails, Theme>
57
+ {
58
+ readonly name: string;
59
+ readonly label: string;
60
+ readonly parameters: TObject<T>;
61
+ description: string;
62
+ declare renderCall: (args: unknown, options: RenderResultOptions, theme: Theme) => Component;
63
+ declare renderResult: (
64
+ result: { content: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean },
65
+ options: RenderResultOptions,
66
+ theme: Theme,
67
+ args?: unknown,
68
+ ) => Component;
69
+
70
+ #config: SubagentConfig<T>;
71
+ #session: ToolSession;
72
+
73
+ constructor(session: ToolSession, config: SubagentConfig<T>) {
74
+ this.#config = config;
75
+ this.#session = session;
76
+ this.name = config.name;
77
+ this.label = config.label;
78
+ this.parameters = config.schema;
79
+ this.description = config.toolDescription ?? "";
80
+ const renderer = createUnifiedSubagentRenderer({
81
+ label: config.label,
82
+ getDescription: args => config.buildDescription(args),
83
+ getContextLine: config.buildContextLine ? args => config.buildContextLine!(args) : undefined,
84
+ });
85
+ this.renderCall = renderer.renderCall;
86
+ this.renderResult = renderer.renderResult;
87
+ }
88
+
89
+ async execute(
90
+ _toolCallId: string,
91
+ params: Record<string, unknown>,
92
+ signal?: AbortSignal,
93
+ onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
94
+ ): Promise<AgentToolResult<TaskToolDetails>> {
95
+ const {
96
+ label,
97
+ agent: agentName,
98
+ progressText,
99
+ tmpPrefix,
100
+ buildTask,
101
+ buildDescription,
102
+ passContext = true,
103
+ } = this.#config;
104
+ const session = this.#session;
105
+ const startTime = Date.now();
106
+
107
+ const agent = getBundledAgent(agentName);
108
+ if (!agent) {
109
+ return {
110
+ content: [{ type: "text", text: `${label} agent not found.` }],
111
+ details: { results: [], totalDurationMs: 0 },
112
+ };
113
+ }
104
114
 
105
- const emitProgress = (progress: AgentProgress) => {
115
+ const effectiveAgentModel = isDefaultModelAlias(agent.model) ? undefined : agent.model;
116
+ const modelOverride = effectiveAgentModel ?? session.getActiveModelString?.() ?? session.getModelString?.();
117
+
118
+ const task = buildTask(params);
119
+ const sessionFile = session.getSessionFile();
120
+ const artifactsDir = sessionFile ? sessionFile.slice(0, -6) : null;
121
+ const tempArtifactsDir = artifactsDir ? null : path.join(os.tmpdir(), `${tmpPrefix}${Snowflake.next()}`);
122
+ const effectiveArtifactsDir = artifactsDir || tempArtifactsDir!;
123
+
124
+ try {
125
+ await fs.mkdir(effectiveArtifactsDir, { recursive: true });
126
+
127
+ const outputManager =
128
+ session.agentOutputManager ?? new AgentOutputManager(session.getArtifactsDir ?? (() => null));
129
+ const [id] = await outputManager.allocateBatch([label]);
130
+
131
+ // Set up EventBus — all observation flows through here
132
+ const eventBus = new EventBus();
133
+
134
+ // Progress tracker subscribes to events
135
+ const tracker = new ProgressTracker({
136
+ index: 0,
137
+ id,
138
+ agent: agentName,
139
+ task,
140
+ description: buildDescription(params),
141
+ startTime,
142
+ onProgress: progress => {
106
143
  onUpdate?.({
107
144
  content: [{ type: "text", text: progressText }],
108
145
  details: { results: [], totalDurationMs: Date.now() - startTime, progress: [progress] },
109
146
  });
110
- };
111
-
112
- let contextFilePath: string | undefined;
113
- if (passContext) {
114
- const compactContext = this.session.subagentContext?.getCompactContext?.();
115
- if (compactContext) {
116
- contextFilePath = path.join(effectiveArtifactsDir, "context.md");
117
- await Bun.write(contextFilePath, compactContext);
118
- }
147
+ },
148
+ onTerminateRequest: () => eventBus.emit("executor:terminate", {}),
149
+ });
150
+ tracker.subscribe(eventBus);
151
+
152
+ // Capture output from agent_end event
153
+ let agentOutput = "";
154
+ const outputListener = eventBus.on(TASK_SUBAGENT_EVENT_CHANNEL, (data: unknown) => {
155
+ const payload = data as { event?: { type: string; messages?: unknown[] } };
156
+ if (payload.event?.type === "agent_end") {
157
+ agentOutput = extractAgentOutput(payload.event as Parameters<typeof extractAgentOutput>[0]);
158
+ }
159
+ });
160
+
161
+ let contextFilePath: string | undefined;
162
+ if (passContext) {
163
+ const compactContext = session.subagentContext?.getCompactContext?.();
164
+ if (compactContext) {
165
+ contextFilePath = path.join(effectiveArtifactsDir, "context.md");
166
+ await Bun.write(contextFilePath, compactContext);
119
167
  }
168
+ }
120
169
 
121
- const result = await runAgent({
122
- cwd: this.session.cwd,
123
- agent,
124
- task,
125
- description: buildDescription(params),
126
- index: 0,
127
- id,
128
- isSubagent: true,
129
- modelOverride,
130
- sessionFile,
131
- persistArtifacts: !!artifactsDir,
132
- artifactsDir: effectiveArtifactsDir,
133
- contextFile: contextFilePath,
134
- enableLsp: false,
135
- signal,
136
- onProgress: emitProgress,
137
- authStorage: this.session.subagentContext?.authStorage,
138
- modelRegistry: this.session.subagentContext?.modelRegistry,
139
- settings: this.session.settings,
140
- contextFiles: this.session.contextFiles,
141
- skills: this.session.skills,
142
- promptTemplates: this.session.promptTemplates,
143
- mcpManager: this.session.subagentContext?.mcpManager,
144
- });
145
-
146
- if (tempArtifactsDir) {
147
- await fs.rm(tempArtifactsDir, { recursive: true, force: true });
170
+ const result = await runAgent({
171
+ cwd: session.cwd,
172
+ agent,
173
+ task,
174
+ description: buildDescription(params),
175
+ index: 0,
176
+ id,
177
+ isSubagent: true,
178
+ modelOverride,
179
+ sessionFile,
180
+ persistArtifacts: !!artifactsDir,
181
+ artifactsDir: effectiveArtifactsDir,
182
+ contextFile: contextFilePath,
183
+ enableLsp: false,
184
+ signal,
185
+ eventBus,
186
+ authStorage: session.subagentContext?.authStorage,
187
+ modelRegistry: session.subagentContext?.modelRegistry,
188
+ settings: session.settings,
189
+ contextFiles: session.contextFiles,
190
+ skills: session.skills,
191
+ promptTemplates: session.promptTemplates,
192
+ mcpManager: session.subagentContext?.mcpManager,
193
+ });
194
+
195
+ // Finalize tracker
196
+ const wasAborted = result.aborted ?? false;
197
+ tracker.finalize(wasAborted ? "aborted" : result.exitCode === 0 ? "completed" : "failed");
198
+ tracker.dispose();
199
+ outputListener();
200
+
201
+ // Enrich result with tracker data
202
+ result.tokens = tracker.progress.tokens;
203
+ result.lastIntent = tracker.progress.lastIntent;
204
+ result.usage = tracker.usage;
205
+ result.toolHistory = tracker.progress.toolHistory.map(t => ({
206
+ tool: t.tool,
207
+ args: t.args,
208
+ status: t.status === "running" ? ("error" as const) : t.status,
209
+ }));
210
+
211
+ // Write output artifact for agent:// URL integration
212
+ if (artifactsDir && agentOutput) {
213
+ const outputFile = path.join(effectiveArtifactsDir, `${id}.md`);
214
+ try {
215
+ await Bun.write(outputFile, agentOutput);
216
+ } catch {
217
+ // Non-fatal
148
218
  }
219
+ }
149
220
 
150
- const totalDuration = Date.now() - startTime;
151
- const output = result.output.trim() || result.stderr.trim() || "(no output)";
152
-
153
- return {
154
- content: [{ type: "text", text: output }],
155
- details: {
156
- results: [result],
157
- totalDurationMs: totalDuration,
158
- usage: result.usage,
159
- },
160
- };
161
- } catch (err) {
162
- return {
163
- content: [{ type: "text", text: `${label} failed: ${err}` }],
164
- details: { results: [], totalDurationMs: Date.now() - startTime },
165
- };
221
+ if (tempArtifactsDir) {
222
+ await fs.rm(tempArtifactsDir, { recursive: true, force: true });
166
223
  }
224
+
225
+ const totalDuration = Date.now() - startTime;
226
+ const output = agentOutput.trim() || result.stderr.trim() || `${label} produced no output`;
227
+
228
+ return {
229
+ content: [{ type: "text", text: output }],
230
+ details: {
231
+ results: [result],
232
+ totalDurationMs: totalDuration,
233
+ usage: result.usage,
234
+ },
235
+ };
236
+ } catch (err) {
237
+ return {
238
+ content: [{ type: "text", text: `${label} failed: ${err}` }],
239
+ details: { results: [], totalDurationMs: Date.now() - startTime },
240
+ };
167
241
  }
168
- };
242
+ }
169
243
  }
@@ -6,22 +6,19 @@ import { Text } from "@nghyane/arcane-tui";
6
6
  import { logger, Snowflake } from "@nghyane/arcane-utils";
7
7
  import { type Static, Type } from "@sinclair/typebox";
8
8
  import chalk from "chalk";
9
- import { renderPromptTemplate } from "../config/prompt-templates";
10
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
11
- import type { Theme } from "../modes/theme/theme";
12
- import todoWriteDescription from "../prompts/tools/todo-write.md" with { type: "text" };
13
10
  import type { ToolSession } from "../sdk";
11
+ import type { Theme } from "../theme/theme";
14
12
  import { renderStatusLine, renderTreeList } from "../tui";
15
- import { PREVIEW_LIMITS } from "./render-utils";
13
+ import { PREVIEW_LIMITS } from "../ui/render-utils";
16
14
 
17
15
  const todoWriteSchema = Type.Object({
18
16
  todos: Type.Array(
19
17
  Type.Object({
20
- id: Type.Optional(Type.String({ description: "Stable todo id" })),
21
- content: Type.String({ description: "Task description (e.g., 'Run tests')" }),
22
- status: StringEnum(["pending", "in_progress", "completed"]),
18
+ id: Type.Optional(Type.String({ description: "Existing todo ID to update (omit for new)" })),
19
+ content: Type.String({ description: "Todo description" }),
20
+ status: StringEnum(["pending", "in_progress", "completed"], { description: "Todo status" }),
23
21
  }),
24
- { description: "The updated todo list" },
25
22
  ),
26
23
  });
27
24
 
@@ -79,10 +76,8 @@ function normalizeTodos(items: Array<{ id?: string; content?: string; status?: s
79
76
  }
80
77
 
81
78
  async function loadTodoFile(filePath: string): Promise<TodoFile | null> {
82
- const file = Bun.file(filePath);
83
- if (!(await file.exists())) return null;
84
79
  try {
85
- const text = await file.text();
80
+ const text = await Bun.file(filePath).text();
86
81
  const data = JSON.parse(text) as TodoFile;
87
82
  if (!data || !Array.isArray(data.todos)) return null;
88
83
  return data;
@@ -114,18 +109,18 @@ function formatTodoLine(item: TodoItem, uiTheme: Theme, prefix: string): string
114
109
 
115
110
  // =============================================================================
116
111
  // Tool Class
117
- // =============================================================================
112
+ interface TodoWriteRenderArgs {
113
+ todos?: Array<{ id?: string; content?: string; status?: string }>;
114
+ }
118
115
 
119
- export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWriteToolDetails> {
116
+ export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWriteToolDetails, Theme> {
120
117
  readonly name = "todo_write";
121
118
  readonly label = "Todo Write";
122
- readonly description: string;
119
+ description = "Update the task/todo list";
123
120
  readonly parameters = todoWriteSchema;
124
121
  readonly concurrency = "exclusive";
125
122
 
126
- constructor(private readonly session: ToolSession) {
127
- this.description = renderPromptTemplate(todoWriteDescription);
128
- }
123
+ constructor(private readonly session: ToolSession) {}
129
124
 
130
125
  async execute(
131
126
  _toolCallId: string,
@@ -166,23 +161,13 @@ export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWrit
166
161
  details: { todos: merged, updatedAt, storage: "session" },
167
162
  };
168
163
  }
169
- }
170
-
171
- // =============================================================================
172
- // TUI Renderer
173
- // =============================================================================
174
-
175
- interface TodoWriteRenderArgs {
176
- todos?: Array<{ id?: string; content?: string; status?: string }>;
177
- }
178
164
 
179
- export const todoWriteToolRenderer = {
180
165
  renderCall(args: TodoWriteRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
181
166
  const count = args.todos?.length ?? 0;
182
167
  const meta = count > 0 ? [`${count} items`] : ["empty"];
183
168
  const text = renderStatusLine({ icon: "pending", title: "Todo Write", meta }, uiTheme);
184
169
  return new Text(text, 0, 0);
185
- },
170
+ }
186
171
 
187
172
  renderResult(
188
173
  result: { content: Array<{ type: string; text?: string }>; details?: TodoWriteToolDetails },
@@ -213,6 +198,5 @@ export const todoWriteToolRenderer = {
213
198
  );
214
199
  const text = [header, ...treeLines].join("\n");
215
200
  return new Text(text, 0, 0);
216
- },
217
- mergeCallAndResult: true,
218
- };
201
+ }
202
+ }
@@ -24,36 +24,6 @@ export class ToolError extends Error {
24
24
  }
25
25
  }
26
26
 
27
- /**
28
- * Error entry for MultiError.
29
- */
30
- export interface ErrorEntry {
31
- message: string;
32
- context?: string;
33
- }
34
-
35
- /**
36
- * Error with multiple entries (e.g., multiple validation failures, batch errors).
37
- */
38
- export class MultiError extends ToolError {
39
- constructor(readonly errors: ErrorEntry[]) {
40
- super(errors.map(e => e.message).join("; "));
41
- this.name = "MultiError";
42
- }
43
-
44
- render(): string {
45
- if (this.errors.length === 1) {
46
- const e = this.errors[0];
47
- return e.context ? `${e.context}: ${e.message}` : e.message;
48
- }
49
- return this.errors.map(e => (e.context ? `${e.context}: ${e.message}` : e.message)).join("\n");
50
- }
51
-
52
- static from(errors: Array<string | ErrorEntry>): MultiError {
53
- return new MultiError(errors.map(e => (typeof e === "string" ? { message: e } : e)));
54
- }
55
- }
56
-
57
27
  /**
58
28
  * Error thrown when a tool operation is aborted (e.g., via AbortSignal).
59
29
  */
@@ -6,39 +6,39 @@ import type { Component } from "@nghyane/arcane-tui";
6
6
  import { Text } from "@nghyane/arcane-tui";
7
7
  import { isEnoent, untilAborted } from "@nghyane/arcane-utils";
8
8
  import { type Static, Type } from "@sinclair/typebox";
9
- import { renderPromptTemplate } from "../config/prompt-templates";
10
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
11
- import type { Theme } from "../modes/theme/theme";
12
10
  import { generateUnifiedDiffString } from "../patch/diff";
13
11
  import { normalizeToLF, stripBom } from "../patch/normalize";
14
- import undoEditDescription from "../prompts/tools/undo-edit.md" with { type: "text" };
15
- import { Ellipsis, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
12
+ import type { Theme } from "../theme/theme";
13
+ import { renderStatusLine } from "../tui";
14
+ import { formatErrorMessage, getDiffStats, shortenPath } from "../ui/render-utils";
16
15
  import type { ToolSession } from ".";
17
16
  import { invalidateFsScanAfterWrite } from "./fs-cache-invalidation";
18
17
  import { resolveToCwd } from "./path-utils";
19
- import { getDiffStats, replaceTabs, shortenPath, ToolUIKit } from "./render-utils";
20
18
  import { ToolError } from "./tool-errors";
21
19
  import { popUndo } from "./undo-history";
22
20
 
23
21
  const undoEditSchema = Type.Object({
24
- path: Type.String({ description: "Path to the file whose last edit should be undone (relative or absolute)" }),
22
+ path: Type.String({ description: "File path to revert" }),
25
23
  });
26
24
 
27
25
  export interface UndoEditToolDetails {
28
26
  diff: string;
29
27
  }
30
28
 
31
- export class UndoEditTool implements AgentTool<typeof undoEditSchema, UndoEditToolDetails> {
29
+ interface UndoEditRenderArgs {
30
+ path?: string;
31
+ }
32
+
33
+ export class UndoEditTool implements AgentTool<typeof undoEditSchema, UndoEditToolDetails, Theme> {
32
34
  readonly name = "undo_edit";
33
35
  readonly label = "Undo";
34
- readonly description: string;
36
+ description = "Undo the last edit to a file";
35
37
  readonly parameters = undoEditSchema;
36
38
  readonly nonAbortable = true;
37
39
  readonly concurrency = "exclusive";
38
40
 
39
- constructor(private readonly session: ToolSession) {
40
- this.description = renderPromptTemplate(undoEditDescription);
41
- }
41
+ constructor(private readonly session: ToolSession) {}
42
42
 
43
43
  async execute(
44
44
  _toolCallId: string,
@@ -74,72 +74,35 @@ export class UndoEditTool implements AgentTool<typeof undoEditSchema, UndoEditTo
74
74
  };
75
75
  });
76
76
  }
77
- }
78
-
79
- // =============================================================================
80
- // TUI Renderer
81
- // =============================================================================
82
-
83
- interface UndoEditRenderArgs {
84
- path?: string;
85
- }
86
-
87
- export const undoEditToolRenderer = {
88
- mergeCallAndResult: true,
89
77
 
90
78
  renderCall(args: UndoEditRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
91
79
  const filePath = shortenPath(args.path ?? "");
92
80
  const pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", "…");
93
81
  const text = renderStatusLine({ icon: "pending", title: "Undo", description: pathDisplay }, uiTheme);
94
82
  return new Text(text, 0, 0);
95
- },
83
+ }
96
84
 
97
85
  renderResult(
98
86
  result: { content: Array<{ type: string; text?: string }>; details?: UndoEditToolDetails; isError?: boolean },
99
- options: RenderResultOptions,
87
+ _options: RenderResultOptions,
100
88
  uiTheme: Theme,
101
89
  args?: UndoEditRenderArgs,
102
90
  ): Component {
103
- const ui = new ToolUIKit(uiTheme);
104
91
  const filePath = shortenPath(args?.path ?? "");
105
- const pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", "…");
106
- const errorText = result.isError ? (result.content?.find(c => c.type === "text")?.text ?? "") : "";
107
-
108
- let cached: RenderCache | undefined;
109
-
110
- return {
111
- render(width) {
112
- const { expanded } = options;
113
- const key = new Hasher().bool(expanded).u32(width).digest();
114
- if (cached?.key === key) return cached.lines;
115
-
116
- const header = renderStatusLine(
117
- { icon: result.isError ? "error" : "success", title: "Undo", description: pathDisplay },
118
- uiTheme,
119
- );
120
- let text = header;
121
-
122
- if (result.isError) {
123
- if (errorText) {
124
- text += `\n\n${uiTheme.fg("error", replaceTabs(errorText))}`;
125
- }
126
- } else if (result.details?.diff) {
127
- const diffStats = getDiffStats(result.details.diff);
128
- text += `\n${uiTheme.fg("dim", uiTheme.format.bracketLeft)}${ui.formatDiffStats(
129
- diffStats.added,
130
- diffStats.removed,
131
- diffStats.hunks,
132
- )}${uiTheme.fg("dim", uiTheme.format.bracketRight)}`;
133
- }
134
-
135
- const lines =
136
- width > 0 ? text.split("\n").map(line => truncateToWidth(line, width, Ellipsis.Omit)) : text.split("\n");
137
- cached = { key, lines };
138
- return lines;
139
- },
140
- invalidate() {
141
- cached = undefined;
142
- },
143
- };
144
- },
145
- };
92
+ if (result.isError) {
93
+ const errorText = result.content?.find(c => c.type === "text")?.text || "Unknown error";
94
+ return new Text(formatErrorMessage(errorText, uiTheme), 0, 0);
95
+ }
96
+ const meta: string[] = ["reverted"];
97
+ if (result.details?.diff) {
98
+ const diffStats = getDiffStats(result.details.diff);
99
+ if (diffStats.added > 0) meta.push(`+${diffStats.added}`);
100
+ if (diffStats.removed > 0) meta.push(`-${diffStats.removed}`);
101
+ }
102
+ return new Text(
103
+ renderStatusLine({ icon: "success", title: "Undo", description: filePath || "file", meta }, uiTheme),
104
+ 0,
105
+ 0,
106
+ );
107
+ }
108
+ }