@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,107 @@
1
+ import {
2
+ type Component,
3
+ getImageDimensions,
4
+ Image,
5
+ ImageProtocol,
6
+ imageFallback,
7
+ Spacer,
8
+ TERMINAL,
9
+ } from "@nghyane/arcane-tui";
10
+ import { convertToPng } from "../../utils/image-convert";
11
+
12
+ type ImageBlock = { data?: string; mimeType?: string };
13
+
14
+ /**
15
+ * Manages image rendering for tool results.
16
+ * Handles Kitty PNG conversion, spacers, and fallback text.
17
+ */
18
+ export class ToolImageDisplay {
19
+ #images: Image[] = [];
20
+ #spacers: Spacer[] = [];
21
+ #convertedImages = new Map<number, { data: string; mimeType: string }>();
22
+ #parent: { addChild(c: Component): void; removeChild(c: Component): void };
23
+ #onUpdate: () => void;
24
+
25
+ constructor(parent: { addChild(c: Component): void; removeChild(c: Component): void }, onUpdate: () => void) {
26
+ this.#parent = parent;
27
+ this.#onUpdate = onUpdate;
28
+ }
29
+
30
+ /**
31
+ * Trigger async PNG conversion for Kitty protocol.
32
+ * Call when result is first received or updated.
33
+ */
34
+ convertForKitty(imageBlocks: ImageBlock[]): void {
35
+ if (TERMINAL.imageProtocol !== ImageProtocol.Kitty) return;
36
+
37
+ for (let i = 0; i < imageBlocks.length; i++) {
38
+ const img = imageBlocks[i];
39
+ if (!img.data || !img.mimeType) continue;
40
+ if (img.mimeType === "image/png") continue;
41
+ if (this.#convertedImages.has(i)) continue;
42
+
43
+ const index = i;
44
+ convertToPng(img.data, img.mimeType)
45
+ .then(converted => {
46
+ if (converted) {
47
+ this.#convertedImages.set(index, converted);
48
+ this.#onUpdate();
49
+ }
50
+ })
51
+ .catch(() => {});
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Update displayed images. Removes old components, adds new ones.
57
+ */
58
+ update(imageBlocks: ImageBlock[], showImages: boolean, themeFg: (s: string) => string): void {
59
+ // Remove old
60
+ for (const img of this.#images) this.#parent.removeChild(img);
61
+ for (const spacer of this.#spacers) this.#parent.removeChild(spacer);
62
+ this.#images = [];
63
+ this.#spacers = [];
64
+
65
+ for (let i = 0; i < imageBlocks.length; i++) {
66
+ const img = imageBlocks[i];
67
+ if (!TERMINAL.imageProtocol || !showImages || !img.data || !img.mimeType) continue;
68
+
69
+ const converted = this.#convertedImages.get(i);
70
+ const imageData = converted?.data ?? img.data;
71
+ const imageMimeType = converted?.mimeType ?? img.mimeType;
72
+
73
+ // Kitty requires PNG — skip unconverted non-PNG
74
+ if (TERMINAL.imageProtocol === ImageProtocol.Kitty && imageMimeType !== "image/png") {
75
+ continue;
76
+ }
77
+
78
+ const spacer = new Spacer(1);
79
+ this.#parent.addChild(spacer);
80
+ this.#spacers.push(spacer);
81
+
82
+ const imageComponent = new Image(
83
+ imageData,
84
+ imageMimeType,
85
+ {
86
+ fallbackColor: (s: string) => themeFg(s),
87
+ },
88
+ { maxWidthCells: 60 },
89
+ );
90
+ this.#images.push(imageComponent);
91
+ this.#parent.addChild(imageComponent);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Get text fallback for images when terminal doesn't support image display.
97
+ */
98
+ static fallbackText(imageBlocks: ImageBlock[]): string {
99
+ return imageBlocks
100
+ .filter((img): img is ImageBlock & { mimeType: string } => !!img.mimeType)
101
+ .map(img => {
102
+ const dims = img.data ? (getImageDimensions(img.data, img.mimeType) ?? undefined) : undefined;
103
+ return imageFallback(img.mimeType, dims);
104
+ })
105
+ .join("\n");
106
+ }
107
+ }
@@ -8,9 +8,9 @@ import {
8
8
  TruncatedText,
9
9
  truncateToWidth,
10
10
  } from "@nghyane/arcane-tui";
11
- import { theme } from "../../modes/theme/theme";
12
11
  import type { SessionTreeNode } from "../../session/session-manager";
13
- import { shortenPath } from "../../tools/render-utils";
12
+ import { theme } from "../../theme/theme";
13
+ import { shortenPath } from "../../ui/render-utils";
14
14
  import { DynamicBorder } from "./dynamic-border";
15
15
 
16
16
  /** Gutter info: position (displayIndent where connector was) and whether to show │ */
@@ -1,13 +1,13 @@
1
- import { Box, Container, Spacer, Text } from "@nghyane/arcane-tui";
1
+ import { Container, LeftBorderBox, Spacer, Text } from "@nghyane/arcane-tui";
2
2
  import type { Rule } from "../../capability/rule";
3
- import { theme } from "../../modes/theme/theme";
3
+ import { theme } from "../../theme/theme";
4
4
 
5
5
  /**
6
6
  * Component that renders a TTSR (Time Traveling Stream Rules) notification.
7
7
  * Shows when a rule violation is detected and the stream is being rewound.
8
8
  */
9
9
  export class TtsrNotificationComponent extends Container {
10
- #box: Box;
10
+ #box: LeftBorderBox;
11
11
  #expanded = false;
12
12
 
13
13
  constructor(private readonly rules: Rule[]) {
@@ -15,8 +15,7 @@ export class TtsrNotificationComponent extends Container {
15
15
 
16
16
  this.addChild(new Spacer(1));
17
17
 
18
- // Use inverse warning color for yellow background effect
19
- this.#box = new Box(1, 1, t => theme.inverse(theme.fg("warning", t)));
18
+ this.#box = new LeftBorderBox(1, 1, s => theme.fg("warning", s));
20
19
  this.addChild(this.#box);
21
20
 
22
21
  this.#rebuild();
@@ -61,20 +60,8 @@ export class TtsrNotificationComponent extends Container {
61
60
  }
62
61
  }
63
62
 
64
- // Use italic for subtle distinction (fg colors conflict with inverse)
65
63
  this.#box.addChild(new Text(theme.italic(displayText), 0, 0));
66
64
  }
67
65
  }
68
-
69
- // Show expand hint if collapsed and there's more content
70
- if (!this.#expanded) {
71
- const hasMoreContent = this.rules.some(r => {
72
- const desc = r.description || r.content;
73
- return desc && desc.split("\n").length > 2;
74
- });
75
- if (hasMoreContent) {
76
- this.#box.addChild(new Text(theme.italic(" (ctrl+o to expand)"), 0, 0));
77
- }
78
- }
79
66
  }
80
67
  }
@@ -1,5 +1,5 @@
1
1
  import { type Component, Container, matchesKey, Spacer, Text, truncateToWidth } from "@nghyane/arcane-tui";
2
- import { theme } from "../../modes/theme/theme";
2
+ import { theme } from "../../theme/theme";
3
3
  import { DynamicBorder } from "./dynamic-border";
4
4
 
5
5
  interface UserMessageItem {
@@ -1,5 +1,5 @@
1
- import { Container, Markdown, Spacer } from "@nghyane/arcane-tui";
2
- import { getMarkdownTheme, theme } from "../../modes/theme/theme";
1
+ import { Container, LeftBorderBox, Markdown, Spacer } from "@nghyane/arcane-tui";
2
+ import { getMarkdownTheme, theme } from "../../theme/theme";
3
3
 
4
4
  /**
5
5
  * Component that renders a user message
@@ -7,18 +7,17 @@ import { getMarkdownTheme, theme } from "../../modes/theme/theme";
7
7
  export class UserMessageComponent extends Container {
8
8
  constructor(text: string, synthetic = false) {
9
9
  super();
10
- const bgColor = (value: string) => theme.bg("userMessageBg", value);
11
- const leftBorder = theme.fg("accent", "▎");
12
10
  const color = synthetic
13
11
  ? (value: string) => theme.fg("dim", value)
14
12
  : (value: string) => theme.fg("userMessageText", value);
15
13
  this.addChild(new Spacer(1));
16
- this.addChild(
17
- new Markdown(text, 1, 1, getMarkdownTheme(), {
18
- bgColor,
19
- color,
20
- leftBorder,
21
- }),
14
+ const borderBox = new LeftBorderBox(
15
+ 2,
16
+ 1,
17
+ s => theme.fg("accent", s),
18
+ t => theme.bg("userMessageBg", t),
22
19
  );
20
+ borderBox.addChild(new Markdown(text, 0, 0, getMarkdownTheme(), { color }));
21
+ this.addChild(borderBox);
23
22
  }
24
23
  }
@@ -1,6 +1,6 @@
1
1
  import { type Component, padding, truncateToWidth, visibleWidth } from "@nghyane/arcane-tui";
2
2
  import { APP_NAME } from "@nghyane/arcane-utils/dirs";
3
- import { theme } from "../../modes/theme/theme";
3
+ import { theme } from "../../theme/theme";
4
4
 
5
5
  export interface RecentSession {
6
6
  name: string;
@@ -22,10 +22,10 @@ import { BashExecutionComponent } from "../../modes/components/bash-execution";
22
22
  import { BorderedLoader } from "../../modes/components/bordered-loader";
23
23
  import { DynamicBorder } from "../../modes/components/dynamic-border";
24
24
  import { PythonExecutionComponent } from "../../modes/components/python-execution";
25
- import { getMarkdownTheme, getSymbolTheme, theme } from "../../modes/theme/theme";
26
25
  import type { InteractiveModeContext } from "../../modes/types";
27
26
  import type { AuthStorage } from "../../session/auth-storage";
28
27
  import { createCompactionSummaryMessage } from "../../session/messages";
28
+ import { getMarkdownTheme, getSymbolTheme, theme } from "../../theme/theme";
29
29
  import { outputMeta } from "../../tools/output-meta";
30
30
  import { resolveToCwd } from "../../tools/path-utils";
31
31
  import { getChangelogPath, parseChangelog } from "../../utils/changelog";
@@ -1,80 +1,24 @@
1
- import { INTENT_FIELD } from "@nghyane/arcane-agent";
2
- import { Loader, TERMINAL, Text } from "@nghyane/arcane-tui";
1
+ import { type AgentTool, toolDetails } from "@nghyane/arcane-agent";
2
+ import { Loader, TERMINAL } from "@nghyane/arcane-tui";
3
3
  import { settings } from "../../config/settings";
4
4
  import { AssistantMessageComponent } from "../../modes/components/assistant-message";
5
- import { CodeModeGroupComponent } from "../../modes/components/codemode-group";
6
- import { ReadToolGroupComponent } from "../../modes/components/read-tool-group";
5
+ import { ContextGroupComponent } from "../../modes/components/context-group";
7
6
  import { TodoReminderComponent } from "../../modes/components/todo-reminder";
8
7
  import { ToolExecutionComponent } from "../../modes/components/tool-execution";
9
8
  import { TtsrNotificationComponent } from "../../modes/components/ttsr-notification";
10
- import { getSymbolTheme, theme } from "../../modes/theme/theme";
11
- import type { InteractiveModeContext, TodoItem } from "../../modes/types";
9
+ import type { InteractiveModeContext } from "../../modes/types";
12
10
  import type { AgentSessionEvent } from "../../session/agent-session";
11
+ import { getSymbolTheme, theme } from "../../theme/theme";
12
+ import { getToolTier, isContextTool } from "../../ui/render-utils";
13
13
 
14
14
  export class EventController {
15
- #lastReadGroup: ReadToolGroupComponent | undefined = undefined;
16
- #codemodeGroups = new Map<string, CodeModeGroupComponent>();
17
15
  #lastThinkingCount = 0;
18
16
  #renderedCustomMessages = new Set<string>();
19
- #lastIntent: string | undefined = undefined;
17
+ #currentContextGroup?: ContextGroupComponent;
18
+ #toolGroups = new Map<string, ContextGroupComponent>();
20
19
 
21
20
  constructor(private ctx: InteractiveModeContext) {}
22
21
 
23
- #resetReadGroup(): void {
24
- this.#lastReadGroup = undefined;
25
- }
26
-
27
- #getReadGroup(): ReadToolGroupComponent {
28
- if (!this.#lastReadGroup) {
29
- this.ctx.chatContainer.addChild(new Text("", 0, 0));
30
- const group = new ReadToolGroupComponent();
31
- group.setExpanded(this.ctx.toolOutputExpanded);
32
- this.ctx.chatContainer.addChild(group);
33
- this.#lastReadGroup = group;
34
- }
35
- return this.#lastReadGroup;
36
- }
37
-
38
- #updateWorkingMessageFromIntent(intent: string | undefined): void {
39
- const trimmed = intent?.trim();
40
- if (!trimmed || trimmed === this.#lastIntent) return;
41
- this.#lastIntent = trimmed;
42
- this.ctx.setWorkingMessage(`${trimmed} (esc to interrupt)`);
43
- }
44
-
45
- #ensureCodemodeGroup(id: string): CodeModeGroupComponent {
46
- let group = this.#codemodeGroups.get(id);
47
- if (!group) {
48
- this.#resetReadGroup();
49
- group = new CodeModeGroupComponent(this.ctx.ui);
50
- group.setExpanded(this.ctx.toolOutputExpanded);
51
- this.ctx.chatContainer.addChild(group);
52
- this.#codemodeGroups.set(id, group);
53
- this.ctx.pendingTools.set(id, group);
54
- this.#hideLoader();
55
- }
56
- return group;
57
- }
58
-
59
- #hideLoader(): void {
60
- if (!this.ctx.loadingAnimation) return;
61
- this.ctx.loadingAnimation.stop();
62
- this.ctx.statusContainer.clear();
63
- this.ctx.loadingAnimation = undefined;
64
- }
65
-
66
- #restoreLoader(): void {
67
- if (this.ctx.loadingAnimation || this.#codemodeGroups.size > 0) return;
68
- this.ctx.loadingAnimation = new Loader(
69
- this.ctx.ui,
70
- spinner => theme.fg("accent", spinner),
71
- text => theme.fg("muted", text),
72
- `Working\u2026 (esc to interrupt)`,
73
- getSymbolTheme().spinnerFrames,
74
- );
75
- this.ctx.statusContainer.addChild(this.ctx.loadingAnimation);
76
- }
77
-
78
22
  subscribeToAgent(): void {
79
23
  this.ctx.unsubscribe = this.ctx.session.subscribe(async (event: AgentSessionEvent) => {
80
24
  await this.handleEvent(event);
@@ -91,7 +35,6 @@ export class EventController {
91
35
 
92
36
  switch (event.type) {
93
37
  case "agent_start":
94
- this.#lastIntent = undefined;
95
38
  if (this.ctx.retryEscapeHandler) {
96
39
  this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
97
40
  this.ctx.retryEscapeHandler = undefined;
@@ -124,11 +67,11 @@ export class EventController {
124
67
  break;
125
68
  }
126
69
  this.#renderedCustomMessages.add(signature);
127
- this.#resetReadGroup();
70
+ this.#finalizeContextGroup();
128
71
  this.ctx.addMessageToChat(event.message);
129
72
  this.ctx.ui.requestRender();
130
73
  } else if (event.message.role === "user") {
131
- this.#resetReadGroup();
74
+ this.#finalizeContextGroup();
132
75
  this.ctx.addMessageToChat(event.message);
133
76
  if (!event.message.synthetic) {
134
77
  this.ctx.editor.setText("");
@@ -136,12 +79,12 @@ export class EventController {
136
79
  }
137
80
  this.ctx.ui.requestRender();
138
81
  } else if (event.message.role === "fileMention") {
139
- this.#resetReadGroup();
82
+ this.#finalizeContextGroup();
140
83
  this.ctx.addMessageToChat(event.message);
141
84
  this.ctx.ui.requestRender();
142
85
  } else if (event.message.role === "assistant") {
143
86
  this.#lastThinkingCount = 0;
144
- this.#resetReadGroup();
87
+ this.#finalizeContextGroup();
145
88
  this.ctx.streamingComponent = new AssistantMessageComponent(undefined, this.ctx.hideThinkingBlock);
146
89
  this.ctx.streamingMessage = event.message;
147
90
  this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
@@ -159,49 +102,15 @@ export class EventController {
159
102
  content => content.type === "thinking" && content.thinking.trim(),
160
103
  ).length;
161
104
  if (thinkingCount > this.#lastThinkingCount) {
162
- this.#resetReadGroup();
163
105
  this.#lastThinkingCount = thinkingCount;
164
106
  }
165
107
 
166
108
  for (const content of this.ctx.streamingMessage.content) {
167
109
  if (content.type !== "toolCall") continue;
168
- // Code Mode: create group component early during streaming for intent display
169
- if (content.name === "code") {
170
- const group = this.#ensureCodemodeGroup(content.id);
171
- const args = content.arguments;
172
- if (args && typeof args === "object" && INTENT_FIELD in args) {
173
- const intent = (args[INTENT_FIELD] as string | undefined)?.trim();
174
- if (intent) group.setIntent(intent);
175
- }
176
- continue;
177
- }
178
110
 
179
111
  if (!this.ctx.pendingTools.has(content.id)) {
180
- if (content.name === "read") {
181
- const group = this.#getReadGroup();
182
- group.updateArgs(content.arguments, content.id);
183
- this.ctx.pendingTools.set(content.id, group);
184
- continue;
185
- }
186
-
187
- this.#resetReadGroup();
188
- this.ctx.chatContainer.addChild(new Text("", 0, 0));
189
112
  const tool = this.ctx.session.getToolByName(content.name);
190
- const component = new ToolExecutionComponent(
191
- content.name,
192
- content.arguments,
193
- {
194
- showImages: settings.get("terminal.showImages"),
195
- editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
196
- editAllowFuzzy: settings.get("edit.fuzzyMatch"),
197
- },
198
- tool,
199
- this.ctx.ui,
200
- this.ctx.sessionManager.getCwd(),
201
- );
202
- component.setExpanded(this.ctx.toolOutputExpanded);
203
- this.ctx.chatContainer.addChild(component);
204
- this.ctx.pendingTools.set(content.id, component);
113
+ this.#appendTool(content.id, content.name, content.arguments, tool);
205
114
  } else {
206
115
  const component = this.ctx.pendingTools.get(content.id);
207
116
  if (component) {
@@ -210,15 +119,6 @@ export class EventController {
210
119
  }
211
120
  }
212
121
 
213
- // Update working message with intent — skip for code tools that already have a visible group
214
- for (const content of this.ctx.streamingMessage.content) {
215
- if (content.type !== "toolCall") continue;
216
- if (this.#codemodeGroups.has(content.id)) continue;
217
- const args = content.arguments;
218
- if (!args || typeof args !== "object" || !(INTENT_FIELD in args)) continue;
219
- this.#updateWorkingMessageFromIntent(args[INTENT_FIELD] as string | undefined);
220
- }
221
-
222
122
  this.ctx.ui.requestRender();
223
123
  }
224
124
  break;
@@ -260,68 +160,11 @@ export class EventController {
260
160
  break;
261
161
 
262
162
  case "tool_execution_start": {
263
- if (!this.#codemodeGroups.has(event.toolCallId)) this.#updateWorkingMessageFromIntent(event.intent);
264
- if (event.toolName === "code") {
265
- const group = this.#ensureCodemodeGroup(event.toolCallId);
266
- const intent = (event.intent ?? (event.args as Record<string, unknown>)?.agent__intent) as
267
- | string
268
- | undefined;
269
- if (typeof intent === "string" && intent.trim()) {
270
- group.setIntent(intent.trim());
271
- }
272
- this.ctx.ui.requestRender();
273
- break;
274
- }
275
- // Route sub-tools into their parent codemode group
276
- if (event.parentToolCallId) {
277
- const parentGroup = this.#codemodeGroups.get(event.parentToolCallId);
278
- if (parentGroup) {
279
- const tool = event.tool ?? this.ctx.session.getToolByName(event.toolName);
280
- const handle = parentGroup.addSubTool(
281
- event.toolCallId,
282
- event.toolName,
283
- event.args,
284
- tool,
285
- {
286
- showImages: settings.get("terminal.showImages"),
287
- editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
288
- editAllowFuzzy: settings.get("edit.fuzzyMatch"),
289
- },
290
- this.ctx.ui,
291
- this.ctx.sessionManager.getCwd(),
292
- );
293
- this.ctx.pendingTools.set(event.toolCallId, handle);
294
- this.ctx.ui.requestRender();
295
- break;
296
- }
297
- }
163
+ if (event.intent) this.ctx.setWorkingMessage(`${event.intent} (esc to interrupt)`);
298
164
 
299
165
  if (!this.ctx.pendingTools.has(event.toolCallId)) {
300
- if (event.toolName === "read") {
301
- const group = this.#getReadGroup();
302
- group.updateArgs(event.args, event.toolCallId);
303
- this.ctx.pendingTools.set(event.toolCallId, group);
304
- this.ctx.ui.requestRender();
305
- break;
306
- }
307
-
308
- this.#resetReadGroup();
309
166
  const tool = event.tool ?? this.ctx.session.getToolByName(event.toolName);
310
- const component = new ToolExecutionComponent(
311
- event.toolName,
312
- event.args,
313
- {
314
- showImages: settings.get("terminal.showImages"),
315
- editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
316
- editAllowFuzzy: settings.get("edit.fuzzyMatch"),
317
- },
318
- tool,
319
- this.ctx.ui,
320
- this.ctx.sessionManager.getCwd(),
321
- );
322
- component.setExpanded(this.ctx.toolOutputExpanded);
323
- this.ctx.chatContainer.addChild(component);
324
- this.ctx.pendingTools.set(event.toolCallId, component);
167
+ this.#appendTool(event.toolCallId, event.toolName, event.args, tool);
325
168
  this.ctx.ui.requestRender();
326
169
  }
327
170
  break;
@@ -341,30 +184,23 @@ export class EventController {
341
184
  if (component) {
342
185
  component.updateResult({ ...event.result, isError: event.isError }, false, event.toolCallId);
343
186
  this.ctx.pendingTools.delete(event.toolCallId);
344
- this.ctx.ui.requestRender();
345
- }
346
- // Code Mode: finalize the group when the "code" tool ends
347
- if (event.toolName === "code") {
348
- const group = this.#codemodeGroups.get(event.toolCallId);
187
+ // Mark context group completion
188
+ const group = this.#toolGroups.get(event.toolCallId);
349
189
  if (group) {
350
- const details = event.result.details as { logs?: string[] } | undefined;
351
- if (details?.logs) {
352
- group.setLogs(details.logs);
353
- }
354
- group.setDone();
355
- this.#codemodeGroups.delete(event.toolCallId);
190
+ group.markDone();
191
+ this.#toolGroups.delete(event.toolCallId);
356
192
  }
357
- this.#restoreLoader();
193
+ this.ctx.ui.requestRender();
358
194
  }
359
195
  // Update todo display when todo_write tool completes
360
196
  if (event.toolName === "todo_write" && !event.isError) {
361
- const details = event.result.details as { todos?: TodoItem[] } | undefined;
197
+ const details = toolDetails("todo_write", (event.result.details ?? {}) as Record<string, unknown>);
362
198
  if (details?.todos) {
363
199
  this.ctx.setTodos(details.todos);
364
200
  }
365
201
  } else if (event.toolName === "todo_write" && event.isError) {
366
202
  const textContent = event.result.content.find(
367
- (content: { type: string; text?: string }) => content.type === "text",
203
+ (content): content is { type: "text"; text: string } => content.type === "text",
368
204
  )?.text;
369
205
  this.ctx.showWarning(
370
206
  `Todo update failed${textContent ? `: ${textContent}` : ". Progress may be stale until todo_write succeeds."}`,
@@ -374,6 +210,7 @@ export class EventController {
374
210
  }
375
211
 
376
212
  case "agent_end":
213
+ this.#finalizeContextGroup();
377
214
  if (this.ctx.loadingAnimation) {
378
215
  this.ctx.loadingAnimation.stop();
379
216
  this.ctx.loadingAnimation = undefined;
@@ -385,7 +222,7 @@ export class EventController {
385
222
  this.ctx.streamingMessage = undefined;
386
223
  }
387
224
  this.ctx.pendingTools.clear();
388
- this.#codemodeGroups.clear();
225
+ this.#toolGroups.clear();
389
226
  this.ctx.ui.requestRender();
390
227
  this.sendCompletionNotification();
391
228
  break;
@@ -480,6 +317,7 @@ export class EventController {
480
317
  }
481
318
 
482
319
  case "ttsr_triggered": {
320
+ this.#finalizeContextGroup();
483
321
  const component = new TtsrNotificationComponent(event.rules);
484
322
  component.setExpanded(this.ctx.toolOutputExpanded);
485
323
  this.ctx.chatContainer.addChild(component);
@@ -488,6 +326,7 @@ export class EventController {
488
326
  }
489
327
 
490
328
  case "todo_reminder": {
329
+ this.#finalizeContextGroup();
491
330
  const component = new TodoReminderComponent(event.todos, event.attempt, event.maxAttempts);
492
331
  this.ctx.chatContainer.addChild(component);
493
332
  this.ctx.ui.requestRender();
@@ -496,6 +335,38 @@ export class EventController {
496
335
  }
497
336
  }
498
337
 
338
+ #appendTool(toolCallId: string, toolName: string, args: unknown, tool: AgentTool | undefined): void {
339
+ const tier = getToolTier(toolName);
340
+ const component = new ToolExecutionComponent(
341
+ toolName,
342
+ args,
343
+ { showImages: settings.get("terminal.showImages"), tier },
344
+ tool,
345
+ this.ctx.ui,
346
+ this.ctx.sessionManager.getCwd(),
347
+ );
348
+ component.setExpanded(this.ctx.toolOutputExpanded);
349
+
350
+ if (isContextTool(toolName)) {
351
+ if (!this.#currentContextGroup) {
352
+ this.#currentContextGroup = new ContextGroupComponent();
353
+ this.#currentContextGroup.setExpanded(this.ctx.toolOutputExpanded);
354
+ this.ctx.chatContainer.addChild(this.#currentContextGroup);
355
+ }
356
+ this.#currentContextGroup.addTool(toolName, component);
357
+ this.#toolGroups.set(toolCallId, this.#currentContextGroup);
358
+ } else {
359
+ this.#finalizeContextGroup();
360
+ this.ctx.chatContainer.addChild(component);
361
+ }
362
+
363
+ this.ctx.pendingTools.set(toolCallId, component);
364
+ }
365
+
366
+ #finalizeContextGroup(): void {
367
+ this.#currentContextGroup = undefined;
368
+ }
369
+
499
370
  sendCompletionNotification(): void {
500
371
  if (this.ctx.isBackgrounded === false) return;
501
372
  const notify = settings.get("completion.notify");
@@ -14,8 +14,8 @@ import type {
14
14
  import { HookEditorComponent } from "../../modes/components/hook-editor";
15
15
  import { HookInputComponent } from "../../modes/components/hook-input";
16
16
  import { HookSelectorComponent } from "../../modes/components/hook-selector";
17
- import { getAvailableThemesWithPaths, getThemeByName, setTheme, type Theme, theme } from "../../modes/theme/theme";
18
17
  import type { InteractiveModeContext } from "../../modes/types";
18
+ import { getAvailableThemesWithPaths, getThemeByName, setTheme, type Theme, theme } from "../../theme/theme";
19
19
  import { setTerminalTitle } from "../../utils/title-generator";
20
20
 
21
21
  export class ExtensionUiController {
@@ -3,11 +3,11 @@ import type { AgentMessage } from "@nghyane/arcane-agent";
3
3
  import { copyToClipboard, readImageFromClipboard, sanitizeText } from "@nghyane/arcane-natives";
4
4
  import { $env } from "@nghyane/arcane-utils";
5
5
  import { settings } from "../../config/settings";
6
- import { theme } from "../../modes/theme/theme";
7
6
  import type { InteractiveModeContext } from "../../modes/types";
8
7
  import type { AgentSessionEvent } from "../../session/agent-session";
9
8
  import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
10
9
  import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
10
+ import { theme } from "../../theme/theme";
11
11
  import { getEditorCommand, openInEditor } from "../../utils/external-editor";
12
12
  import { resizeImage } from "../../utils/image-resize";
13
13
  import { generateSessionTitle, setTerminalTitle } from "../../utils/title-generator";
@@ -52,6 +52,8 @@ export class InputController {
52
52
  this.ctx.editor.setText("");
53
53
  this.ctx.isPythonMode = false;
54
54
  this.ctx.updateEditorBorderColor();
55
+ } else if (this.ctx.session.isStreaming) {
56
+ this.ctx.agent.abort();
55
57
  } else if (!this.ctx.editor.getText().trim()) {
56
58
  // Double-escape with empty editor triggers /tree, /branch, or nothing based on setting
57
59
  const action = settings.get("doubleEscapeAction");
@@ -19,10 +19,10 @@ import {
19
19
  import { MCPOAuthFlow } from "../../mcp/oauth-flow";
20
20
  import type { MCPServerConfig, MCPServerConnection } from "../../mcp/types";
21
21
  import type { OAuthCredential } from "../../session/auth-storage";
22
+ import { theme } from "../../theme/theme";
22
23
  import { openPath } from "../../utils/open";
23
24
  import { DynamicBorder } from "../components/dynamic-border";
24
25
  import { MCPAddWizard } from "../components/mcp-add-wizard";
25
- import { theme } from "../theme/theme";
26
26
  import type { InteractiveModeContext } from "../types";
27
27
 
28
28
  function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {