@nghyane/arcane 0.1.10 → 0.1.12

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 (117) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/package.json +4 -4
  3. package/src/extensibility/plugins/installer.ts +6 -21
  4. package/src/extensibility/plugins/manager.ts +8 -29
  5. package/src/main.ts +3 -0
  6. package/src/modes/components/model-selector.ts +1 -1
  7. package/src/modes/components/status-line.ts +15 -40
  8. package/src/modes/components/user-message.ts +2 -0
  9. package/src/modes/components/welcome.ts +9 -10
  10. package/src/modes/controllers/event-controller.ts +51 -12
  11. package/src/modes/interactive-mode.ts +2 -0
  12. package/src/modes/theme/dark.json +56 -59
  13. package/src/modes/theme/defaults/dark-catppuccin.json +47 -56
  14. package/src/modes/theme/defaults/dark-dracula.json +24 -32
  15. package/src/modes/theme/defaults/dark-gruvbox.json +53 -74
  16. package/src/modes/theme/defaults/dark-solarized.json +33 -35
  17. package/src/modes/theme/defaults/dark-tokyo-night.json +57 -67
  18. package/src/modes/theme/defaults/index.ts +3 -179
  19. package/src/modes/theme/defaults/light-catppuccin.json +42 -50
  20. package/src/modes/theme/defaults/light-github.json +68 -94
  21. package/src/modes/theme/defaults/light-solarized.json +41 -49
  22. package/src/modes/theme/light.json +14 -12
  23. package/src/modes/theme/theme-schema.json +4 -0
  24. package/src/modes/theme/theme.ts +89 -6
  25. package/src/patch/index.ts +1 -1
  26. package/src/stt/downloader.ts +1 -4
  27. package/src/stt/setup.ts +2 -4
  28. package/src/tui/output-block.ts +2 -12
  29. package/src/utils/open.ts +2 -1
  30. package/src/modes/theme/defaults/alabaster.json +0 -93
  31. package/src/modes/theme/defaults/amethyst.json +0 -96
  32. package/src/modes/theme/defaults/anthracite.json +0 -93
  33. package/src/modes/theme/defaults/basalt.json +0 -91
  34. package/src/modes/theme/defaults/birch.json +0 -95
  35. package/src/modes/theme/defaults/dark-abyss.json +0 -91
  36. package/src/modes/theme/defaults/dark-arctic.json +0 -104
  37. package/src/modes/theme/defaults/dark-aurora.json +0 -95
  38. package/src/modes/theme/defaults/dark-cavern.json +0 -91
  39. package/src/modes/theme/defaults/dark-copper.json +0 -95
  40. package/src/modes/theme/defaults/dark-cosmos.json +0 -90
  41. package/src/modes/theme/defaults/dark-cyberpunk.json +0 -102
  42. package/src/modes/theme/defaults/dark-eclipse.json +0 -91
  43. package/src/modes/theme/defaults/dark-ember.json +0 -95
  44. package/src/modes/theme/defaults/dark-equinox.json +0 -90
  45. package/src/modes/theme/defaults/dark-forest.json +0 -96
  46. package/src/modes/theme/defaults/dark-github.json +0 -105
  47. package/src/modes/theme/defaults/dark-lavender.json +0 -95
  48. package/src/modes/theme/defaults/dark-lunar.json +0 -89
  49. package/src/modes/theme/defaults/dark-midnight.json +0 -95
  50. package/src/modes/theme/defaults/dark-monochrome.json +0 -94
  51. package/src/modes/theme/defaults/dark-monokai.json +0 -98
  52. package/src/modes/theme/defaults/dark-nebula.json +0 -90
  53. package/src/modes/theme/defaults/dark-nord.json +0 -97
  54. package/src/modes/theme/defaults/dark-ocean.json +0 -101
  55. package/src/modes/theme/defaults/dark-one.json +0 -100
  56. package/src/modes/theme/defaults/dark-rainforest.json +0 -91
  57. package/src/modes/theme/defaults/dark-reef.json +0 -91
  58. package/src/modes/theme/defaults/dark-retro.json +0 -92
  59. package/src/modes/theme/defaults/dark-rose-pine.json +0 -96
  60. package/src/modes/theme/defaults/dark-sakura.json +0 -95
  61. package/src/modes/theme/defaults/dark-slate.json +0 -95
  62. package/src/modes/theme/defaults/dark-solstice.json +0 -90
  63. package/src/modes/theme/defaults/dark-starfall.json +0 -91
  64. package/src/modes/theme/defaults/dark-sunset.json +0 -99
  65. package/src/modes/theme/defaults/dark-swamp.json +0 -90
  66. package/src/modes/theme/defaults/dark-synthwave.json +0 -103
  67. package/src/modes/theme/defaults/dark-taiga.json +0 -91
  68. package/src/modes/theme/defaults/dark-terminal.json +0 -95
  69. package/src/modes/theme/defaults/dark-tundra.json +0 -91
  70. package/src/modes/theme/defaults/dark-twilight.json +0 -91
  71. package/src/modes/theme/defaults/dark-volcanic.json +0 -91
  72. package/src/modes/theme/defaults/graphite.json +0 -92
  73. package/src/modes/theme/defaults/light-arctic.json +0 -107
  74. package/src/modes/theme/defaults/light-aurora-day.json +0 -91
  75. package/src/modes/theme/defaults/light-canyon.json +0 -91
  76. package/src/modes/theme/defaults/light-cirrus.json +0 -90
  77. package/src/modes/theme/defaults/light-coral.json +0 -95
  78. package/src/modes/theme/defaults/light-cyberpunk.json +0 -96
  79. package/src/modes/theme/defaults/light-dawn.json +0 -90
  80. package/src/modes/theme/defaults/light-dunes.json +0 -91
  81. package/src/modes/theme/defaults/light-eucalyptus.json +0 -95
  82. package/src/modes/theme/defaults/light-forest.json +0 -100
  83. package/src/modes/theme/defaults/light-frost.json +0 -95
  84. package/src/modes/theme/defaults/light-glacier.json +0 -91
  85. package/src/modes/theme/defaults/light-gruvbox.json +0 -108
  86. package/src/modes/theme/defaults/light-haze.json +0 -90
  87. package/src/modes/theme/defaults/light-honeycomb.json +0 -95
  88. package/src/modes/theme/defaults/light-lagoon.json +0 -91
  89. package/src/modes/theme/defaults/light-lavender.json +0 -95
  90. package/src/modes/theme/defaults/light-meadow.json +0 -91
  91. package/src/modes/theme/defaults/light-mint.json +0 -95
  92. package/src/modes/theme/defaults/light-monochrome.json +0 -101
  93. package/src/modes/theme/defaults/light-ocean.json +0 -99
  94. package/src/modes/theme/defaults/light-one.json +0 -99
  95. package/src/modes/theme/defaults/light-opal.json +0 -91
  96. package/src/modes/theme/defaults/light-orchard.json +0 -91
  97. package/src/modes/theme/defaults/light-paper.json +0 -95
  98. package/src/modes/theme/defaults/light-prism.json +0 -90
  99. package/src/modes/theme/defaults/light-retro.json +0 -98
  100. package/src/modes/theme/defaults/light-sand.json +0 -95
  101. package/src/modes/theme/defaults/light-savanna.json +0 -91
  102. package/src/modes/theme/defaults/light-soleil.json +0 -90
  103. package/src/modes/theme/defaults/light-sunset.json +0 -99
  104. package/src/modes/theme/defaults/light-synthwave.json +0 -98
  105. package/src/modes/theme/defaults/light-tokyo-night.json +0 -111
  106. package/src/modes/theme/defaults/light-wetland.json +0 -91
  107. package/src/modes/theme/defaults/light-zenith.json +0 -89
  108. package/src/modes/theme/defaults/limestone.json +0 -94
  109. package/src/modes/theme/defaults/mahogany.json +0 -97
  110. package/src/modes/theme/defaults/marble.json +0 -93
  111. package/src/modes/theme/defaults/obsidian.json +0 -91
  112. package/src/modes/theme/defaults/onyx.json +0 -91
  113. package/src/modes/theme/defaults/pearl.json +0 -93
  114. package/src/modes/theme/defaults/porcelain.json +0 -91
  115. package/src/modes/theme/defaults/quartz.json +0 -96
  116. package/src/modes/theme/defaults/sandstone.json +0 -95
  117. package/src/modes/theme/defaults/titanium.json +0 -90
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.1.12] - 2026-02-24
6
+
7
+ ### Fixed
8
+
9
+ - Preserve single blank line content in edit tool — `hashlineParseContent` no longer strips the only line when it is empty
10
+
11
+ ### Changed
12
+
13
+ - Stream codemode intent immediately during LLM generation instead of waiting for execution start
14
+ - Hide loader spinner when codemode group is active to avoid duplicate status indicators
15
+
5
16
  ## [0.1.8] - 2026-02-22
6
17
 
7
18
  ### Changed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@nghyane/arcane",
4
- "version": "0.1.10",
4
+ "version": "0.1.12",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/nghyane/arcane",
7
7
  "author": "Can Bölük",
@@ -45,11 +45,11 @@
45
45
  "dependencies": {
46
46
  "@mozilla/readability": "0.6.0",
47
47
  "@nghyane/arcane-stats": "^0.1.8",
48
- "@nghyane/arcane-agent": "^0.1.8",
49
- "@nghyane/arcane-codemode": "^0.1.9",
48
+ "@nghyane/arcane-agent": "^0.1.10",
49
+ "@nghyane/arcane-codemode": "^0.1.11",
50
50
  "@nghyane/arcane-ai": "^0.1.8",
51
51
  "@nghyane/arcane-natives": "^0.1.7",
52
- "@nghyane/arcane-tui": "^0.1.8",
52
+ "@nghyane/arcane-tui": "^0.1.9",
53
53
  "@nghyane/arcane-utils": "^0.1.6",
54
54
  "@sinclair/typebox": "^0.34.48",
55
55
  "@xterm/headless": "^6.0.0",
@@ -2,6 +2,7 @@ import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { isEnoent } from "@nghyane/arcane-utils";
4
4
  import { getAgentDir, getProjectDir } from "@nghyane/arcane-utils/dirs";
5
+ import { $ } from "bun";
5
6
  import type { InstalledPlugin } from "./types";
6
7
 
7
8
  const PLUGINS_DIR = path.join(getAgentDir(), "plugins");
@@ -45,17 +46,9 @@ export async function installPlugin(packageName: string): Promise<InstalledPlugi
45
46
  }
46
47
 
47
48
  // Run npm install in plugins directory
48
- const proc = Bun.spawn(["bun", "install", packageName], {
49
- cwd: PLUGINS_DIR,
50
- stdin: "ignore",
51
- stdout: "pipe",
52
- stderr: "pipe",
53
- windowsHide: true,
54
- });
55
-
56
- const exitCode = await proc.exited;
57
- if (exitCode !== 0) {
58
- const stderr = await new Response(proc.stderr).text();
49
+ const result = await $`bun install ${packageName}`.cwd(PLUGINS_DIR).quiet().nothrow();
50
+ if (result.exitCode !== 0) {
51
+ const stderr = result.stderr.toString().trim();
59
52
  throw new Error(`Failed to install ${packageName}: ${stderr}`);
60
53
  }
61
54
 
@@ -87,16 +80,8 @@ export async function uninstallPlugin(name: string): Promise<void> {
87
80
 
88
81
  await ensurePluginsDir();
89
82
 
90
- const proc = Bun.spawn(["bun", "uninstall", name], {
91
- cwd: PLUGINS_DIR,
92
- stdin: "ignore",
93
- stdout: "pipe",
94
- stderr: "pipe",
95
- windowsHide: true,
96
- });
97
-
98
- const exitCode = await proc.exited;
99
- if (exitCode !== 0) {
83
+ const result = await $`bun uninstall ${name}`.cwd(PLUGINS_DIR).quiet().nothrow();
84
+ if (result.exitCode !== 0) {
100
85
  throw new Error(`Failed to uninstall ${name}`);
101
86
  }
102
87
  }
@@ -9,6 +9,7 @@ import {
9
9
  getProjectDir,
10
10
  getProjectPluginOverridesPath,
11
11
  } from "@nghyane/arcane-utils/dirs";
12
+ import { $ } from "bun";
12
13
  import { extractPackageName, parsePluginSpec } from "./parser";
13
14
  import type {
14
15
  DoctorCheck,
@@ -155,17 +156,9 @@ export class PluginManager {
155
156
  }
156
157
 
157
158
  // Run npm install
158
- const proc = Bun.spawn(["bun", "install", spec.packageName], {
159
- cwd: getPluginsDir(),
160
- stdin: "ignore",
161
- stdout: "pipe",
162
- stderr: "pipe",
163
- windowsHide: true,
164
- });
165
-
166
- const exitCode = await proc.exited;
167
- if (exitCode !== 0) {
168
- const stderr = await new Response(proc.stderr).text();
159
+ const result = await $`bun install ${spec.packageName}`.cwd(getPluginsDir()).quiet().nothrow();
160
+ if (result.exitCode !== 0) {
161
+ const stderr = result.stderr.toString().trim();
169
162
  throw new Error(`npm install failed: ${stderr}`);
170
163
  }
171
164
 
@@ -236,16 +229,8 @@ export class PluginManager {
236
229
  validatePackageName(name);
237
230
  await this.#ensurePackageJson();
238
231
 
239
- const proc = Bun.spawn(["bun", "uninstall", name], {
240
- cwd: getPluginsDir(),
241
- stdin: "ignore",
242
- stdout: "pipe",
243
- stderr: "pipe",
244
- windowsHide: true,
245
- });
246
-
247
- const exitCode = await proc.exited;
248
- if (exitCode !== 0) {
232
+ const result = await $`bun uninstall ${name}`.cwd(getPluginsDir()).quiet().nothrow();
233
+ if (result.exitCode !== 0) {
249
234
  throw new Error(`npm uninstall failed for ${name}`);
250
235
  }
251
236
 
@@ -619,14 +604,8 @@ export class PluginManager {
619
604
 
620
605
  async #fixMissingPlugin(): Promise<boolean> {
621
606
  try {
622
- const proc = Bun.spawn(["bun", "install"], {
623
- cwd: getPluginsDir(),
624
- stdin: "ignore",
625
- stdout: "pipe",
626
- stderr: "pipe",
627
- windowsHide: true,
628
- });
629
- return (await proc.exited) === 0;
607
+ const result = await $`bun install`.cwd(getPluginsDir()).quiet().nothrow();
608
+ return result.exitCode === 0;
630
609
  } catch {
631
610
  return false;
632
611
  }
package/src/main.ts CHANGED
@@ -11,6 +11,7 @@ import * as os from "node:os";
11
11
  import * as path from "node:path";
12
12
  import { createInterface } from "node:readline/promises";
13
13
  import { type ImageContent, supportsXhigh } from "@nghyane/arcane-ai";
14
+ import { queryTerminalBackground } from "@nghyane/arcane-tui";
14
15
  import { $env, postmortem } from "@nghyane/arcane-utils";
15
16
  import { getProjectDir, setProjectDir } from "@nghyane/arcane-utils/dirs";
16
17
  import chalk from "chalk";
@@ -591,12 +592,14 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
591
592
  });
592
593
  }
593
594
 
595
+ const terminalBg = isInteractive ? await queryTerminalBackground() : null;
594
596
  await initTheme(
595
597
  isInteractive,
596
598
  settings.get("symbolPreset"),
597
599
  settings.get("colorBlindMode"),
598
600
  settings.get("theme.dark"),
599
601
  settings.get("theme.light"),
602
+ terminalBg ?? undefined,
600
603
  );
601
604
  time("initTheme:final");
602
605
 
@@ -21,7 +21,7 @@ import { DynamicBorder } from "./dynamic-border";
21
21
  function makeInvertedBadge(label: string, color: ThemeColor): string {
22
22
  const fgAnsi = theme.getFgAnsi(color);
23
23
  const bgAnsi = fgAnsi.replace(/\x1b\[38;/g, "\x1b[48;");
24
- return `${bgAnsi}\x1b[30m ${label} \x1b[39m\x1b[49m`;
24
+ return `${bgAnsi}\x1b[30m ${label} \x1b[39m${theme.getAppBgAnsi()}`;
25
25
  }
26
26
 
27
27
  interface ModelItem {
@@ -309,78 +309,53 @@ export class StatusLineComponent implements Component {
309
309
  const ctx = this.#buildSegmentContext(width);
310
310
  const effectiveSettings = this.#resolveSettings();
311
311
  const separatorDef = getSeparator(effectiveSettings.separator ?? "powerline-thin", theme);
312
-
313
- const bgAnsi = theme.getBgAnsi("statusLineBg");
314
312
  const fgAnsi = theme.getFgAnsi("text");
315
313
  const sepAnsi = theme.getFgAnsi("statusLineSep");
314
+ const leftSepWidth = visibleWidth(separatorDef.left);
315
+ const rightSepWidth = visibleWidth(separatorDef.right);
316
316
 
317
- // Collect visible segment contents
318
317
  const leftParts: string[] = [];
319
- for (const segId of effectiveSettings.leftSegments) {
318
+ for (const segId of this.#resolveSettings().leftSegments) {
320
319
  const rendered = renderSegment(segId, ctx);
321
- if (rendered.visible && rendered.content) {
322
- leftParts.push(rendered.content);
323
- }
320
+ if (rendered.visible && rendered.content) leftParts.push(rendered.content);
324
321
  }
325
322
 
326
323
  const rightParts: string[] = [];
327
- for (const segId of effectiveSettings.rightSegments) {
324
+ for (const segId of this.#resolveSettings().rightSegments) {
328
325
  const rendered = renderSegment(segId, ctx);
329
- if (rendered.visible && rendered.content) {
330
- rightParts.push(rendered.content);
331
- }
326
+ if (rendered.visible && rendered.content) rightParts.push(rendered.content);
332
327
  }
333
328
 
334
329
  const topFillWidth = width > 0 ? Math.max(0, width - 4) : 0;
335
330
  const left = [...leftParts];
336
331
  const right = [...rightParts];
337
332
 
338
- const leftSepWidth = visibleWidth(separatorDef.left);
339
- const rightSepWidth = visibleWidth(separatorDef.right);
340
- const leftCapWidth = separatorDef.endCaps ? visibleWidth(separatorDef.endCaps.right) : 0;
341
- const rightCapWidth = separatorDef.endCaps ? visibleWidth(separatorDef.endCaps.left) : 0;
342
-
343
- const groupWidth = (parts: string[], capWidth: number, sepWidth: number): number => {
333
+ const groupWidth = (parts: string[], sepWidth: number): number => {
344
334
  if (parts.length === 0) return 0;
345
335
  const partsWidth = parts.reduce((sum, part) => sum + visibleWidth(part), 0);
346
336
  const sepTotal = Math.max(0, parts.length - 1) * (sepWidth + 2);
347
- return partsWidth + sepTotal + 2 + capWidth;
337
+ return partsWidth + sepTotal + 2;
348
338
  };
349
339
 
350
- let leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
351
- let rightWidth = groupWidth(right, rightCapWidth, rightSepWidth);
340
+ let leftWidth = groupWidth(left, leftSepWidth);
341
+ let rightWidth = groupWidth(right, rightSepWidth);
352
342
  const totalWidth = () => leftWidth + rightWidth + (left.length > 0 && right.length > 0 ? 1 : 0);
353
343
 
354
344
  if (topFillWidth > 0) {
355
345
  while (totalWidth() > topFillWidth && right.length > 0) {
356
346
  right.pop();
357
- rightWidth = groupWidth(right, rightCapWidth, rightSepWidth);
347
+ rightWidth = groupWidth(right, rightSepWidth);
358
348
  }
359
349
  while (totalWidth() > topFillWidth && left.length > 0) {
360
350
  left.pop();
361
- leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
351
+ leftWidth = groupWidth(left, leftSepWidth);
362
352
  }
363
353
  }
364
354
 
365
355
  const renderGroup = (parts: string[], direction: "left" | "right"): string => {
366
356
  if (parts.length === 0) return "";
367
357
  const sep = direction === "left" ? separatorDef.left : separatorDef.right;
368
- const cap = separatorDef.endCaps
369
- ? direction === "left"
370
- ? separatorDef.endCaps.right
371
- : separatorDef.endCaps.left
372
- : "";
373
- const capPrefix = separatorDef.endCaps?.useBgAsFg ? bgAnsi.replace("\x1b[48;", "\x1b[38;") : bgAnsi + sepAnsi;
374
- const capText = cap ? `${capPrefix}${cap}\x1b[0m` : "";
375
-
376
- let content = bgAnsi + fgAnsi;
377
- content += ` ${parts.join(` ${sepAnsi}${sep}${fgAnsi} `)} `;
378
- content += "\x1b[0m";
379
-
380
- if (capText) {
381
- return direction === "right" ? capText + content : content + capText;
382
- }
383
- return content;
358
+ return `${fgAnsi} ${parts.join(` ${sepAnsi}${sep}${fgAnsi} `)} \x1b[0m`;
384
359
  };
385
360
 
386
361
  const leftGroup = renderGroup(left, "left");
@@ -391,8 +366,8 @@ export class StatusLineComponent implements Component {
391
366
  return leftGroup + (leftGroup && rightGroup ? " " : "") + rightGroup;
392
367
  }
393
368
 
394
- leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
395
- rightWidth = groupWidth(right, rightCapWidth, rightSepWidth);
369
+ leftWidth = groupWidth(left, leftSepWidth);
370
+ rightWidth = groupWidth(right, rightSepWidth);
396
371
  const gapWidth = Math.max(1, topFillWidth - leftWidth - rightWidth);
397
372
  return leftGroup + padding(gapWidth) + rightGroup;
398
373
  }
@@ -8,6 +8,7 @@ export class UserMessageComponent extends Container {
8
8
  constructor(text: string, synthetic = false) {
9
9
  super();
10
10
  const bgColor = (value: string) => theme.bg("userMessageBg", value);
11
+ const leftBorder = theme.fg("accent", "▎");
11
12
  const color = synthetic
12
13
  ? (value: string) => theme.fg("dim", value)
13
14
  : (value: string) => theme.fg("userMessageText", value);
@@ -16,6 +17,7 @@ export class UserMessageComponent extends Container {
16
17
  new Markdown(text, 1, 1, getMarkdownTheme(), {
17
18
  bgColor,
18
19
  color,
20
+ leftBorder,
19
21
  }),
20
22
  );
21
23
  }
@@ -67,9 +67,9 @@ export class WelcomeComponent implements Component {
67
67
  const leftCol = showRightColumn ? dualLeftCol : boxWidth - 2;
68
68
  const rightCol = showRightColumn ? dualRightCol : 0;
69
69
 
70
- // Block-based OMP logo (gradient: magenta → cyan)
70
+ // Block-based ARC logo (gradient: blue → cyan → green / Nord Frost)
71
71
  // biome-ignore format: preserve ASCII art layout
72
- const piLogo = ["▀████████████▀", " ╘███ ███ ", " ███ ███ ", " ███ ███ ", " ▄███▄ ▄███▄ "];
72
+ const piLogo = ["╭━━━╮╭━━━╮╭━━━╮", "┃╭━╮┃┃╭━╮┃┃╭━━╯", "┃╰━╯┃┃╰━╯┃┃┃ ", "┃┃ ┃┃┃╭╮╭╯┃╰━━╮", "╰╯ ╰╯╰╯╰╯ ╰━━━╯"];
73
73
 
74
74
  // Apply gradient to logo
75
75
  const logoColored = piLogo.map(line => this.#gradientLine(line));
@@ -82,7 +82,7 @@ export class WelcomeComponent implements Component {
82
82
  ...logoColored.map(l => this.#centerText(l, leftCol)),
83
83
  "",
84
84
  this.#centerText(theme.fg("muted", this.modelName), leftCol),
85
- this.#centerText(theme.fg("borderMuted", this.providerName), leftCol),
85
+ this.#centerText(theme.fg("dim", this.providerName), leftCol),
86
86
  ];
87
87
 
88
88
  // Right column separator
@@ -190,15 +190,14 @@ export class WelcomeComponent implements Component {
190
190
  return padding(leftPad) + text + padding(rightPad);
191
191
  }
192
192
 
193
- /** Apply magenta→cyan gradient to a string */
193
+ /** Apply Nord Frost gradient (blue cyan green) to a string */
194
194
  #gradientLine(line: string): string {
195
195
  const colors = [
196
- "\x1b[38;5;199m", // bright magenta
197
- "\x1b[38;5;171m", // magenta-purple
198
- "\x1b[38;5;135m", // purple
199
- "\x1b[38;5;99m", // purple-blue
200
- "\x1b[38;5;75m", // cyan-blue
201
- "\x1b[38;5;51m", // bright cyan
196
+ "\x1b[38;2;136;192;208m", // #88c0d0 blue
197
+ "\x1b[38;2;141;200;200m", // blend
198
+ "\x1b[38;2;143;188;187m", // #8fbcbb cyan
199
+ "\x1b[38;2;153;189;170m", // blend
200
+ "\x1b[38;2;163;190;140m", // #a3be8c green
202
201
  ];
203
202
  const reset = "\x1b[0m";
204
203
 
@@ -42,6 +42,39 @@ export class EventController {
42
42
  this.ctx.setWorkingMessage(`${trimmed} (esc to interrupt)`);
43
43
  }
44
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
+
45
78
  subscribeToAgent(): void {
46
79
  this.ctx.unsubscribe = this.ctx.session.subscribe(async (event: AgentSessionEvent) => {
47
80
  await this.handleEvent(event);
@@ -132,8 +165,16 @@ export class EventController {
132
165
 
133
166
  for (const content of this.ctx.streamingMessage.content) {
134
167
  if (content.type !== "toolCall") continue;
135
- // Code Mode: suppress streaming render for "code" tool
136
- if (content.name === "code") 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
+ }
137
178
 
138
179
  if (!this.ctx.pendingTools.has(content.id)) {
139
180
  if (content.name === "read") {
@@ -169,9 +210,10 @@ export class EventController {
169
210
  }
170
211
  }
171
212
 
172
- // Update working message with intent from streamed tool arguments
213
+ // Update working message with intent skip for code tools that already have a visible group
173
214
  for (const content of this.ctx.streamingMessage.content) {
174
215
  if (content.type !== "toolCall") continue;
216
+ if (this.#codemodeGroups.has(content.id)) continue;
175
217
  const args = content.arguments;
176
218
  if (!args || typeof args !== "object" || !(INTENT_FIELD in args)) continue;
177
219
  this.#updateWorkingMessageFromIntent(args[INTENT_FIELD] as string | undefined);
@@ -218,19 +260,15 @@ export class EventController {
218
260
  break;
219
261
 
220
262
  case "tool_execution_start": {
221
- this.#updateWorkingMessageFromIntent(event.intent);
222
- // Code Mode: create a group component for the "code" tool
263
+ if (!this.#codemodeGroups.has(event.toolCallId)) this.#updateWorkingMessageFromIntent(event.intent);
223
264
  if (event.toolName === "code") {
224
- this.#resetReadGroup();
225
- const group = new CodeModeGroupComponent(this.ctx.ui);
226
- const intent = event.intent ?? (event.args as Record<string, unknown>)?.agent__intent;
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;
227
269
  if (typeof intent === "string" && intent.trim()) {
228
270
  group.setIntent(intent.trim());
229
271
  }
230
- group.setExpanded(this.ctx.toolOutputExpanded);
231
- this.ctx.chatContainer.addChild(group);
232
- this.#codemodeGroups.set(event.toolCallId, group);
233
- this.ctx.pendingTools.set(event.toolCallId, group);
234
272
  this.ctx.ui.requestRender();
235
273
  break;
236
274
  }
@@ -316,6 +354,7 @@ export class EventController {
316
354
  group.setDone();
317
355
  this.#codemodeGroups.delete(event.toolCallId);
318
356
  }
357
+ this.#restoreLoader();
319
358
  }
320
359
  // Update todo display when todo_write tool completes
321
360
  if (event.toolName === "todo_write" && !event.isError) {
@@ -175,6 +175,7 @@ export class InteractiveMode implements InteractiveModeContext {
175
175
 
176
176
  this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"));
177
177
  this.ui.setClearOnShrink(settings.get("clearOnShrink"));
178
+ this.ui.setAppBg(theme.getAppBgPackedRgb());
178
179
  setMermaidRenderCallback(() => this.ui.requestRender());
179
180
  this.chatContainer = new Container();
180
181
  this.pendingMessagesContainer = new Container();
@@ -344,6 +345,7 @@ export class InteractiveMode implements InteractiveModeContext {
344
345
  // Set up theme file watcher
345
346
  onThemeChange(() => {
346
347
  this.ui.invalidate();
348
+ this.ui.setAppBg(theme.getAppBgPackedRgb());
347
349
  this.updateEditorBorderColor();
348
350
  this.ui.requestRender();
349
351
  });
@@ -2,26 +2,22 @@
2
2
  "$schema": "https://raw.githubusercontent.com/nghyane/arcane/main/packages/coding-agent/theme-schema.json",
3
3
  "name": "dark",
4
4
  "vars": {
5
- "cyan": "#0088fa",
6
- "blue": "#178fb9",
7
- "green": "#89d281",
8
- "red": "#fc3a4b",
9
- "yellow": "#e4c00f",
10
- "gray": "#777d88",
11
- "dimGray": "#5f6673",
12
- "darkGray": "#3d424a",
13
- "accent": "#febc38",
14
- "selectedBg": "#31363f",
15
- "userMsgBg": "#221d1a",
16
- "toolPendingBg": "#1d2129",
17
- "toolSuccessBg": "#161a1f",
18
- "toolErrorBg": "#291d1d",
19
- "customMsgBg": "#2a2530"
5
+ "cyan": "#8fbcbb",
6
+ "blue": "#88c0d0",
7
+ "lightBlue": "#81a1c1",
8
+ "green": "#a3be8c",
9
+ "red": "#d06050",
10
+ "yellow": "#ebcb8b",
11
+ "gray": "#7b88a1",
12
+ "dimGray": "#4c566a",
13
+ "darkGray": "#434c5e",
14
+ "accent": "#d08770",
15
+ "darkGrayBg": "#434c5e"
20
16
  },
21
17
  "colors": {
22
18
  "accent": "accent",
23
19
  "border": "blue",
24
- "borderAccent": "cyan",
20
+ "borderAccent": "accent",
25
21
  "borderMuted": "darkGray",
26
22
  "success": "green",
27
23
  "error": "red",
@@ -30,22 +26,22 @@
30
26
  "dim": "dimGray",
31
27
  "text": "",
32
28
  "thinkingText": "gray",
33
- "selectedBg": "selectedBg",
34
- "userMessageBg": "userMsgBg",
29
+ "selectedBg": "^2",
30
+ "userMessageBg": "^2",
35
31
  "userMessageText": "",
36
- "customMessageBg": "customMsgBg",
32
+ "customMessageBg": "^2",
37
33
  "customMessageText": "",
38
- "customMessageLabel": "#b281d6",
39
- "toolPendingBg": "toolPendingBg",
40
- "toolSuccessBg": "toolSuccessBg",
41
- "toolErrorBg": "toolErrorBg",
34
+ "customMessageLabel": "accent",
35
+ "toolPendingBg": "^3",
36
+ "toolSuccessBg": "^2",
37
+ "toolErrorBg": "^3",
42
38
  "toolTitle": "",
43
39
  "toolOutput": "gray",
44
- "mdHeading": "#febc38",
45
- "mdLink": "#0088fa",
40
+ "mdHeading": "accent",
41
+ "mdLink": "blue",
46
42
  "mdLinkUrl": "dimGray",
47
- "mdCode": "#e5c1ff",
48
- "mdCodeBlock": "#9CDCFE",
43
+ "mdCode": "yellow",
44
+ "mdCodeBlock": "green",
49
45
  "mdCodeBlockBorder": "darkGray",
50
46
  "mdQuote": "gray",
51
47
  "mdQuoteBorder": "darkGray",
@@ -54,42 +50,43 @@
54
50
  "toolDiffAdded": "green",
55
51
  "toolDiffRemoved": "red",
56
52
  "toolDiffContext": "gray",
57
- "link": "#0088fa",
58
- "syntaxComment": "#6A9955",
59
- "syntaxKeyword": "#569CD6",
60
- "syntaxFunction": "#DCDCAA",
61
- "syntaxVariable": "#9CDCFE",
62
- "syntaxString": "#CE9178",
63
- "syntaxNumber": "#B5CEA8",
64
- "syntaxType": "#4EC9B0",
65
- "syntaxOperator": "#D4D4D4",
66
- "syntaxPunctuation": "#D4D4D4",
53
+ "link": "blue",
54
+ "syntaxComment": "gray",
55
+ "syntaxKeyword": "lightBlue",
56
+ "syntaxFunction": "blue",
57
+ "syntaxVariable": "cyan",
58
+ "syntaxString": "green",
59
+ "syntaxNumber": "#b48ead",
60
+ "syntaxType": "yellow",
61
+ "syntaxOperator": "gray",
62
+ "syntaxPunctuation": "gray",
67
63
  "thinkingOff": "darkGray",
68
64
  "thinkingMinimal": "dimGray",
69
- "thinkingLow": "#178fb9",
70
- "thinkingMedium": "#0088fa",
71
- "thinkingHigh": "#b281d6",
72
- "thinkingXhigh": "#e5c1ff",
73
- "bashMode": "cyan",
74
- "statusLineBg": "#121212",
75
- "statusLineSep": 244,
76
- "statusLineModel": "#d787af",
77
- "statusLinePath": "#00afaf",
78
- "statusLineGitClean": "#5faf5f",
79
- "statusLineGitDirty": "#d7af5f",
80
- "statusLineContext": "#8787af",
81
- "statusLineSpend": "#5fafaf",
82
- "statusLineStaged": 70,
83
- "statusLineDirty": 178,
84
- "statusLineUntracked": 39,
85
- "statusLineOutput": 205,
86
- "statusLineCost": 205,
65
+ "thinkingLow": "blue",
66
+ "thinkingMedium": "cyan",
67
+ "thinkingHigh": "accent",
68
+ "thinkingXhigh": "#c87558",
69
+ "bashMode": "accent",
70
+ "statusLineBg": "^-2",
71
+ "statusLineSep": "dimGray",
72
+ "statusLineModel": "accent",
73
+ "statusLinePath": "blue",
74
+ "statusLineGitClean": "green",
75
+ "statusLineGitDirty": "yellow",
76
+ "statusLineContext": "gray",
77
+ "statusLineSpend": "cyan",
78
+ "statusLineStaged": 179,
79
+ "statusLineDirty": 173,
80
+ "statusLineUntracked": 180,
81
+ "statusLineOutput": 180,
82
+ "statusLineCost": 173,
87
83
  "statusLineSubagents": "accent",
88
- "pythonMode": "yellow"
84
+ "pythonMode": "yellow",
85
+ "appBg": "^0"
89
86
  },
90
87
  "export": {
91
- "pageBg": "#18181e",
92
- "cardBg": "#1e1e24",
93
- "infoBg": "#3c3728"
88
+ "pageBg": "#242933",
89
+ "cardBg": "#323846",
90
+ "infoBg": "#2d333b"
94
91
  }
95
92
  }