@hncode/hncode 0.3.6 → 0.4.1

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 (148) hide show
  1. package/package.json +5 -3
  2. package/src/agent-task.js +154 -84
  3. package/src/agent.js +533 -145
  4. package/src/clipboard.js +5 -2
  5. package/src/colors.js +242 -238
  6. package/src/config.js +217 -17
  7. package/src/file-history.js +177 -0
  8. package/src/hooks.js +11 -1
  9. package/src/i18n.js +389 -0
  10. package/src/index.js +46 -3
  11. package/src/llm.js +139 -16
  12. package/src/permissions.js +120 -0
  13. package/src/session-hub.js +407 -0
  14. package/src/session.js +31 -0
  15. package/src/term.js +19 -3
  16. package/src/tool-result.js +236 -0
  17. package/src/tools/agent.js +11 -0
  18. package/src/tools/bash.js +84 -17
  19. package/src/tools/edit.js +192 -32
  20. package/src/tools/file-lines.js +5 -0
  21. package/src/tools/index.js +4 -0
  22. package/src/tools/memory.js +88 -0
  23. package/src/tools/read-media-file.js +85 -68
  24. package/src/tools/read.js +15 -1
  25. package/src/tools/utils.js +14 -0
  26. package/src/tools/write.js +35 -5
  27. package/src/tui.js +11665 -9179
  28. package/src/web-auth.js +98 -0
  29. package/src/web-daemon-cli.js +63 -0
  30. package/src/web-daemon-client.js +203 -0
  31. package/src/web-daemon.js +871 -0
  32. package/src/web-public/app.css +1509 -0
  33. package/src/web-public/app.js +2913 -0
  34. package/src/web-public/complete.js +154 -0
  35. package/src/web-public/fonts/JetBrainsMono-Bold.woff2 +0 -0
  36. package/src/web-public/fonts/JetBrainsMono-Regular.woff2 +0 -0
  37. package/src/web-public/fonts/noto-sans-sc-100-400-normal.woff2 +0 -0
  38. package/src/web-public/fonts/noto-sans-sc-101-400-normal.woff2 +0 -0
  39. package/src/web-public/fonts/noto-sans-sc-102-400-normal.woff2 +0 -0
  40. package/src/web-public/fonts/noto-sans-sc-103-400-normal.woff2 +0 -0
  41. package/src/web-public/fonts/noto-sans-sc-104-400-normal.woff2 +0 -0
  42. package/src/web-public/fonts/noto-sans-sc-105-400-normal.woff2 +0 -0
  43. package/src/web-public/fonts/noto-sans-sc-106-400-normal.woff2 +0 -0
  44. package/src/web-public/fonts/noto-sans-sc-107-400-normal.woff2 +0 -0
  45. package/src/web-public/fonts/noto-sans-sc-108-400-normal.woff2 +0 -0
  46. package/src/web-public/fonts/noto-sans-sc-109-400-normal.woff2 +0 -0
  47. package/src/web-public/fonts/noto-sans-sc-110-400-normal.woff2 +0 -0
  48. package/src/web-public/fonts/noto-sans-sc-111-400-normal.woff2 +0 -0
  49. package/src/web-public/fonts/noto-sans-sc-112-400-normal.woff2 +0 -0
  50. package/src/web-public/fonts/noto-sans-sc-113-400-normal.woff2 +0 -0
  51. package/src/web-public/fonts/noto-sans-sc-114-400-normal.woff2 +0 -0
  52. package/src/web-public/fonts/noto-sans-sc-115-400-normal.woff2 +0 -0
  53. package/src/web-public/fonts/noto-sans-sc-116-400-normal.woff2 +0 -0
  54. package/src/web-public/fonts/noto-sans-sc-117-400-normal.woff2 +0 -0
  55. package/src/web-public/fonts/noto-sans-sc-118-400-normal.woff2 +0 -0
  56. package/src/web-public/fonts/noto-sans-sc-119-400-normal.woff2 +0 -0
  57. package/src/web-public/fonts/noto-sans-sc-21-400-normal.woff2 +0 -0
  58. package/src/web-public/fonts/noto-sans-sc-22-400-normal.woff2 +0 -0
  59. package/src/web-public/fonts/noto-sans-sc-23-400-normal.woff2 +0 -0
  60. package/src/web-public/fonts/noto-sans-sc-24-400-normal.woff2 +0 -0
  61. package/src/web-public/fonts/noto-sans-sc-25-400-normal.woff2 +0 -0
  62. package/src/web-public/fonts/noto-sans-sc-26-400-normal.woff2 +0 -0
  63. package/src/web-public/fonts/noto-sans-sc-27-400-normal.woff2 +0 -0
  64. package/src/web-public/fonts/noto-sans-sc-28-400-normal.woff2 +0 -0
  65. package/src/web-public/fonts/noto-sans-sc-29-400-normal.woff2 +0 -0
  66. package/src/web-public/fonts/noto-sans-sc-30-400-normal.woff2 +0 -0
  67. package/src/web-public/fonts/noto-sans-sc-31-400-normal.woff2 +0 -0
  68. package/src/web-public/fonts/noto-sans-sc-32-400-normal.woff2 +0 -0
  69. package/src/web-public/fonts/noto-sans-sc-33-400-normal.woff2 +0 -0
  70. package/src/web-public/fonts/noto-sans-sc-34-400-normal.woff2 +0 -0
  71. package/src/web-public/fonts/noto-sans-sc-35-400-normal.woff2 +0 -0
  72. package/src/web-public/fonts/noto-sans-sc-36-400-normal.woff2 +0 -0
  73. package/src/web-public/fonts/noto-sans-sc-37-400-normal.woff2 +0 -0
  74. package/src/web-public/fonts/noto-sans-sc-38-400-normal.woff2 +0 -0
  75. package/src/web-public/fonts/noto-sans-sc-39-400-normal.woff2 +0 -0
  76. package/src/web-public/fonts/noto-sans-sc-4-400-normal.woff2 +0 -0
  77. package/src/web-public/fonts/noto-sans-sc-40-400-normal.woff2 +0 -0
  78. package/src/web-public/fonts/noto-sans-sc-41-400-normal.woff2 +0 -0
  79. package/src/web-public/fonts/noto-sans-sc-42-400-normal.woff2 +0 -0
  80. package/src/web-public/fonts/noto-sans-sc-43-400-normal.woff2 +0 -0
  81. package/src/web-public/fonts/noto-sans-sc-44-400-normal.woff2 +0 -0
  82. package/src/web-public/fonts/noto-sans-sc-45-400-normal.woff2 +0 -0
  83. package/src/web-public/fonts/noto-sans-sc-46-400-normal.woff2 +0 -0
  84. package/src/web-public/fonts/noto-sans-sc-47-400-normal.woff2 +0 -0
  85. package/src/web-public/fonts/noto-sans-sc-48-400-normal.woff2 +0 -0
  86. package/src/web-public/fonts/noto-sans-sc-49-400-normal.woff2 +0 -0
  87. package/src/web-public/fonts/noto-sans-sc-5-400-normal.woff2 +0 -0
  88. package/src/web-public/fonts/noto-sans-sc-50-400-normal.woff2 +0 -0
  89. package/src/web-public/fonts/noto-sans-sc-51-400-normal.woff2 +0 -0
  90. package/src/web-public/fonts/noto-sans-sc-52-400-normal.woff2 +0 -0
  91. package/src/web-public/fonts/noto-sans-sc-53-400-normal.woff2 +0 -0
  92. package/src/web-public/fonts/noto-sans-sc-54-400-normal.woff2 +0 -0
  93. package/src/web-public/fonts/noto-sans-sc-55-400-normal.woff2 +0 -0
  94. package/src/web-public/fonts/noto-sans-sc-56-400-normal.woff2 +0 -0
  95. package/src/web-public/fonts/noto-sans-sc-57-400-normal.woff2 +0 -0
  96. package/src/web-public/fonts/noto-sans-sc-58-400-normal.woff2 +0 -0
  97. package/src/web-public/fonts/noto-sans-sc-59-400-normal.woff2 +0 -0
  98. package/src/web-public/fonts/noto-sans-sc-6-400-normal.woff2 +0 -0
  99. package/src/web-public/fonts/noto-sans-sc-60-400-normal.woff2 +0 -0
  100. package/src/web-public/fonts/noto-sans-sc-61-400-normal.woff2 +0 -0
  101. package/src/web-public/fonts/noto-sans-sc-62-400-normal.woff2 +0 -0
  102. package/src/web-public/fonts/noto-sans-sc-63-400-normal.woff2 +0 -0
  103. package/src/web-public/fonts/noto-sans-sc-64-400-normal.woff2 +0 -0
  104. package/src/web-public/fonts/noto-sans-sc-65-400-normal.woff2 +0 -0
  105. package/src/web-public/fonts/noto-sans-sc-66-400-normal.woff2 +0 -0
  106. package/src/web-public/fonts/noto-sans-sc-67-400-normal.woff2 +0 -0
  107. package/src/web-public/fonts/noto-sans-sc-68-400-normal.woff2 +0 -0
  108. package/src/web-public/fonts/noto-sans-sc-69-400-normal.woff2 +0 -0
  109. package/src/web-public/fonts/noto-sans-sc-70-400-normal.woff2 +0 -0
  110. package/src/web-public/fonts/noto-sans-sc-71-400-normal.woff2 +0 -0
  111. package/src/web-public/fonts/noto-sans-sc-72-400-normal.woff2 +0 -0
  112. package/src/web-public/fonts/noto-sans-sc-73-400-normal.woff2 +0 -0
  113. package/src/web-public/fonts/noto-sans-sc-74-400-normal.woff2 +0 -0
  114. package/src/web-public/fonts/noto-sans-sc-75-400-normal.woff2 +0 -0
  115. package/src/web-public/fonts/noto-sans-sc-76-400-normal.woff2 +0 -0
  116. package/src/web-public/fonts/noto-sans-sc-77-400-normal.woff2 +0 -0
  117. package/src/web-public/fonts/noto-sans-sc-78-400-normal.woff2 +0 -0
  118. package/src/web-public/fonts/noto-sans-sc-79-400-normal.woff2 +0 -0
  119. package/src/web-public/fonts/noto-sans-sc-80-400-normal.woff2 +0 -0
  120. package/src/web-public/fonts/noto-sans-sc-81-400-normal.woff2 +0 -0
  121. package/src/web-public/fonts/noto-sans-sc-82-400-normal.woff2 +0 -0
  122. package/src/web-public/fonts/noto-sans-sc-83-400-normal.woff2 +0 -0
  123. package/src/web-public/fonts/noto-sans-sc-84-400-normal.woff2 +0 -0
  124. package/src/web-public/fonts/noto-sans-sc-85-400-normal.woff2 +0 -0
  125. package/src/web-public/fonts/noto-sans-sc-86-400-normal.woff2 +0 -0
  126. package/src/web-public/fonts/noto-sans-sc-87-400-normal.woff2 +0 -0
  127. package/src/web-public/fonts/noto-sans-sc-88-400-normal.woff2 +0 -0
  128. package/src/web-public/fonts/noto-sans-sc-89-400-normal.woff2 +0 -0
  129. package/src/web-public/fonts/noto-sans-sc-90-400-normal.woff2 +0 -0
  130. package/src/web-public/fonts/noto-sans-sc-91-400-normal.woff2 +0 -0
  131. package/src/web-public/fonts/noto-sans-sc-97-400-normal.woff2 +0 -0
  132. package/src/web-public/fonts/noto-sans-sc-98-400-normal.woff2 +0 -0
  133. package/src/web-public/fonts/noto-sans-sc-99-400-normal.woff2 +0 -0
  134. package/src/web-public/fonts/noto-sans-sc.css +775 -0
  135. package/src/web-public/fonts.css +31 -0
  136. package/src/web-public/home.css +222 -0
  137. package/src/web-public/home.html +33 -0
  138. package/src/web-public/home.js +160 -0
  139. package/src/web-public/i18n.js +983 -0
  140. package/src/web-public/index.html +196 -0
  141. package/src/web-public/login.css +123 -0
  142. package/src/web-public/login.html +25 -0
  143. package/src/web-public/login.js +46 -0
  144. package/src/web-public/settings.js +290 -0
  145. package/src/web-ws.js +226 -0
  146. package/src/web.js +380 -0
  147. /package/bin/{install.js → install.cjs} +0 -0
  148. /package/bin/{preinstall.js → preinstall.cjs} +0 -0
package/src/clipboard.js CHANGED
@@ -27,8 +27,11 @@ export function copyText(text) {
27
27
  if (!s) return false;
28
28
  if (win()) {
29
29
  // clip.exe takes UTF-16LE. (`cmd /c echo "…" | clip` mangled quotes, '%'
30
- // and newlines; plain UTF-8 input turned non-ASCII into mojibake.)
31
- cp.execSync('clip', { input: Buffer.from(s, 'utf16le'), windowsHide: true, timeout: 5000 });
30
+ // and newlines; plain UTF-8 input turned non-ASCII into mojibake.) Prefix the
31
+ // UTF-16 byte order mark (FF FE) so clip.exe is never left to guess the endian
32
+ // or treat input as ANSI — without it, non-ASCII on some Windows versions /
33
+ // code pages came out mojibake.
34
+ cp.execSync('clip', { input: Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(s, 'utf16le')]), windowsHide: true, timeout: 5000 });
32
35
  return true;
33
36
  }
34
37
  const cmd = process.platform === 'darwin' ? 'pbcopy' : 'xclip -selection clipboard';
package/src/colors.js CHANGED
@@ -1,238 +1,242 @@
1
- // Cyan-blue visual theme for hncode. Adapts to the terminal's color depth:
2
- // uses true-color when available, 256/16-color fallbacks otherwise.
3
-
4
- function colorDepth() {
5
- try { return process.stdout.getColorDepth?.() || 0; } catch { return 0; }
6
- }
7
- const DEPTH = colorDepth();
8
- export const TRUECOLOR = DEPTH >= 24 || process.env.COLORTERM === 'truecolor' || /^xterm.*-256color$|.*-truecolor$/.test(process.env.TERM || '');
9
-
10
- function rgb(r, g, b) { return TRUECOLOR ? `\x1b[38;2;${r};${g};${b}m` : `\x1b[38;5;${ansi256(r, g, b)}m`; }
11
- function brgb(r, g, b) { return TRUECOLOR ? `\x1b[48;2;${r};${g};${b}m` : `\x1b[48;5;${ansi256(r, g, b)}m`; }
12
-
13
- // Quantize a truecolor triple to the nearest 256-color xterm index.
14
- function ansi256(r, g, b) {
15
- const ri = Math.round(r / 51), gi = Math.round(g / 51), bi = Math.round(b / 51);
16
- if (ri === gi && gi === bi) {
17
- if (ri === 0) return 16;
18
- if (ri === 255 / 51) return 232 + ri;
19
- return 232 + ri; // grayscale ramp
20
- }
21
- return 16 + 36 * ri + 6 * gi + bi;
22
- }
23
-
24
- // Brand: cyan-blue (青蓝色).
25
- export const TEAL = rgb(0, 184, 219); // 青蓝
26
- export const CYAN = rgb(0, 215, 255);
27
- export const BLUE = rgb(66, 135, 245);
28
- export const FG = TEAL; // primary fg
29
- export const BG = rgb(0, 18, 26);
30
- export const BORDER = rgb(70, 130, 180);
31
-
32
- // Named accent colours per theme. `auto` keeps the brand cyan-blue and lets the
33
- // terminal's own palette show through for everything else; `dark`/`light` pick
34
- // accent triples that stay readable on that background.
35
- const THEMES = {
36
- auto: {
37
- fg: [0, 184, 219], bg: [0, 18, 26],
38
- teal: [0, 184, 219], cyan: [0, 215, 255], blue: [66, 135, 245], border: [70, 130, 180],
39
- hover: [190, 245, 255], selBg: [0, 70, 84]
40
- },
41
- dark: {
42
- fg: [255, 255, 255], bg: [0, 18, 26], // White foreground for normal text
43
- teal: [0, 184, 219], cyan: [0, 215, 255], blue: [66, 135, 245], border: [70, 130, 180],
44
- hover: [190, 245, 255], selBg: [0, 70, 84]
45
- },
46
- light: {
47
- fg: [26, 26, 26], bg: [255, 255, 255],
48
- teal: [0, 102, 128], cyan: [0, 122, 153], blue: [10, 77, 158], border: [138, 155, 176],
49
- hover: [0, 78, 102], selBg: [200, 230, 239],
50
- red: [179, 38, 30], green: [30, 122, 60], yellow: [160, 120, 0], orange: [178, 90, 0],
51
- white: [26, 26, 26], gray: [85, 85, 85],
52
- },
53
- };
54
- export const THEME_NAMES = Object.keys(THEMES);
55
-
56
- export const C = {
57
- reset: '\x1b[0m',
58
- bold: '\x1b[1m',
59
- dim: '\x1b[2m',
60
- // Italic (SGR 3). Used for the BODY of a reasoning block — the prose reads as
61
- // an aside rather than as the answer. Must be closed with SGR 23 (`endItalic`),
62
- // NOT with `reset`, because resetting also drops the surrounding dim/gray that
63
- // the same row depends on. Terminals without italic fall back to normal text,
64
- // so this is a decoration, never load-bearing.
65
- italic: '\x1b[3m',
66
- endItalic: '\x1b[23m',
67
- muted: '\x1b[90m',
68
- teal: TEAL,
69
- cyan: CYAN,
70
- blue: BLUE,
71
- fg: FG,
72
- bg: BG,
73
- bgTeal: brgb(0, 184, 219),
74
- bgDim: brgb(12, 24, 30),
75
- bgPanel: TRUECOLOR ? '\x1b[48;5;236m' : '\x1b[48;5;236m',
76
- border: BORDER,
77
- red: '\x1b[91m',
78
- green: '\x1b[92m',
79
- yellow: '\x1b[93m',
80
- magenta: '\x1b[95m',
81
- gray: '\x1b[90m',
82
- white: '\x1b[97m',
83
- orange: TRUECOLOR ? rgb(255, 140, 0) : '\x1b[38;5;208m',
84
- // Cyan pushed slightly toward green — used for the Swarm mode badge so it reads
85
- // as its own mode next to Plan (pure cyan) and Focus (blue) while staying in the
86
- // theme's blue-green family. 38;5;43 is rgb(0,215,175): red at zero and green
87
- // only 40 above blue, i.e. still clearly cyan with a green cast. 38;5;85 was the
88
- // first attempt and read as plain GREEN — its red channel is lifted to 95 and
89
- // green pins at 255, which is a spring green, not a green-tinted cyan.
90
- spring: TRUECOLOR ? rgb(0, 215, 175) : '\x1b[38;5;43m',
91
- // Git branch in the status line: yellow pulled about half way toward white, so
92
- // it stays clearly "yellow" (not cream/ivory) while reading brighter than the
93
- // plain 93 yellow on a dark background. rgb(255,219,110) keeps red at max,
94
- // green near it, and blue around 43% — that ratio is what keeps the hue
95
- // yellow rather than dropping into orange.
96
- branch: TRUECOLOR ? rgb(255, 219, 110) : '\x1b[38;5;222m',
97
- // Shell-mode (`!`) accent — violet, matching kimi-code's shellMode token so the
98
- // bash-mode editor border and the `!` read as "shell", distinct from the cyan
99
- // prompt and the yellow git badge. #BD93F9 dark / #7C3AED light.
100
- shellMode: TRUECOLOR ? rgb(189, 147, 249) : '\x1b[38;5;141m',
101
- bgRed: '\x1b[41m',
102
- bgGreen: '\x1b[42m',
103
- // Selection highlight (mouse text selection) and the scrollbar gutter.
104
- // Selection uses a dark teal background so white text stays readable.
105
- selBg: TRUECOLOR ? brgb(0, 70, 84) : '\x1b[48;5;23m', // Original dark cyan background
106
- scrollTrack: '\x1b[90m', // dim gutter
107
- scrollThumb: TRUECOLOR ? rgb(0, 184, 219) : '\x1b[38;5;37m', // brand cyan-blue
108
- // Scrollbar interaction states:
109
- // hover -> a touch whiter/brighter than the brand colour
110
- // active -> BOLD + the brand colour (pressed/dragging)
111
- // The pressed state used to be a DARKER colour, which on a dark background
112
- // read as "the thumb disappeared". Making it bold-and-bright means a press is
113
- // always unmistakably visible. Idle swaps back to scrollThumb automatically.
114
- scrollThumbHover: TRUECOLOR ? rgb(120, 226, 245) : '\x1b[38;5;123m',
115
- scrollThumbActive: '\x1b[1m' + (TRUECOLOR ? rgb(0, 220, 255) : '\x1b[38;5;51m'),
116
- // Hover highlight for clickable rows (menu items, picker options, form
117
- // fields, composer rows). An explicit bright foreground + BOLD rather than a
118
- // bare BOLD: BOLD only brightens the 16-colour palette (it does nothing to a
119
- // true-color row), which is why hover used to be invisible on most rows.
120
- // tintRange() re-applies this so the row's own SGR codes cannot cancel it.
121
- hover: '\x1b[1m' + rgb(190, 245, 255),
122
- };
123
-
124
- export function style(text, ...codes) {
125
- return codes.join('') + text + '\x1b[0m';
126
- }
127
-
128
- // Linearly interpolate between two RGB triples and return an ANSI colour string.
129
- // Used for the pulsing Working indicator (orange pulses to a brighter/darker
130
- // shade over a 2s cycle).
131
- export function lerpColor(r1, g1, b1, r2, g2, b2, t) {
132
- const ti = Math.max(0, Math.min(1, t));
133
- const r = Math.round(r1 + (r2 - r1) * ti);
134
- const g = Math.round(g1 + (g2 - g1) * ti);
135
- const b = Math.round(b1 + (b2 - b1) * ti);
136
- return rgb(r, g, b);
137
- }
138
-
139
- // Apply a named theme IN PLACE. `C` is a module-level object and ESM imports are
140
- // live bindings, so mutating its properties here changes the colours everywhere
141
- // without re-importing. Returns true when the name was recognised.
142
- export function setTheme(name) {
143
- const t = THEMES[name];
144
- if (!t) return false;
145
-
146
- // Update foreground colors
147
- C.teal = rgb(...t.teal);
148
- C.fg = rgb(...t.fg);
149
- C.cyan = rgb(...t.cyan);
150
- C.blue = rgb(...t.blue);
151
- C.border = rgb(...t.border);
152
-
153
- // Update background colors
154
- C.bg = brgb(...t.bg);
155
- C.bgTeal = brgb(...t.teal);
156
- C.bgPanel = brgb(...t.bg);
157
- C.selBg = brgb(...t.selBg);
158
-
159
- // Scrollbar colours. The active (pressed/dragging) thumb is BOLD + a brighter
160
- // shade of the theme colour. It used to be a DARKER shade, which on a dark
161
- // background read as "the thumb vanished" mid-drag.
162
- C.scrollTrack = '\x1b[90m'; // dim gutter
163
- C.scrollThumb = rgb(...t.teal);
164
- C.scrollThumbHover = rgb(...t.hover);
165
- C.scrollThumbActive = '\x1b[1m' + rgb(
166
- Math.min(255, Math.round(t.teal[0] * 0.6 + 255 * 0.4)),
167
- Math.min(255, Math.round(t.teal[1] * 0.6 + 255 * 0.4)),
168
- Math.min(255, Math.round(t.teal[2] * 0.6 + 255 * 0.4)),
169
- );
170
-
171
- // Hover stays visible on this theme's background (bright on dark, dark on light).
172
- C.hover = '\x1b[1m' + rgb(...(t.hover || [190, 245, 255]));
173
-
174
- // Semantic colors — swap to deep versions on light backgrounds.
175
- if (t.white) C.white = rgb(...t.white);
176
- if (t.gray) C.gray = rgb(...t.gray);
177
- if (t.red) C.red = rgb(...t.red);
178
- if (t.green) C.green = rgb(...t.green);
179
- if (t.yellow) C.yellow = rgb(...t.yellow);
180
- if (t.orange) C.orange = rgb(...t.orange);
181
- // The git branch colour is derived per theme: the pale yellow that reads well
182
- // on the dark background is nearly invisible on white, so on a light theme it
183
- // is darkened (keeping the same hue) instead of being left as-is.
184
- if (t.bg && t.bg[0] > 128) {
185
- C.branch = rgb(146, 106, 0); // dark amber for light backgrounds
186
- } else {
187
- C.branch = TRUECOLOR ? rgb(255, 219, 110) : '\x1b[38;5;222m';
188
- }
189
-
190
- return true;
191
- }
192
-
193
- export function clearScreen() {
194
- return '\x1b[3J\x1b[H\x1b[2J';
195
- }
196
-
197
- // Clear screen and set background color based on current theme
198
- export function clearAndSetBg() {
199
- // Reset all attributes first
200
- const reset = '\x1b[0m';
201
- // Clear screen
202
- const clear = '\x1b[3J\x1b[H\x1b[2J';
203
- // Set background color using current C.bg value
204
- const bg = C.bg || '\x1b[48;2;0;18;26m'; // Default dark background
205
- return reset + clear + bg;
206
- }
207
- export function eraseLine() {
208
- return '\x1b[2K';
209
- }
210
- export function eraseScreen() {
211
- return '\x1b[2J';
212
- }
213
- export function moveTo(r, c) {
214
- return `\x1b[${r + 1};${c + 1}H`;
215
- }
216
- export function cursorUp(n) { return `\x1b[${n}A`; }
217
- export function cursorDown(n) { return `\x1b[${n}B`; }
218
- export function cursorLeft(n) { return `\x1b[${n}D`; }
219
- export function cursorForward(n) { return `\x1b[${n}C`; }
220
- export function hideCursor() { return '\x1b[?25l'; }
221
- export function showCursor() { return '\x1b[?25h'; }
222
- // Cursor shape: terminals accept DECSCUSR — 2 = block, 4 = underline, 6 = bar(I-beam);
223
- // 1/3/5 = blinking variants. Force a solid block cursor so positioning is always exact
224
- // and unaffected by I-beam alignment.
225
- export function blockCursor() { return '\x1b[2 q'; }
226
- export function underlineCursor() { return '\x1b[4 q'; }
227
- export function barCursor() { return '\x1b[6 q'; }
228
- export function alternateScreen(on) { return on ? '\x1b[?1049h' : '\x1b[?1049l'; }
229
- export function setScrollRegion(top, bottom) {
230
- if (process.platform === 'win32') return ''; // win TTY scroll regions are unreliable
231
- return `\x1b[${top + 1};${bottom + 1}r`;
232
- }
233
- export function resetScrollRegion() {
234
- return '\x1b[r';
235
- }
236
- export function setBracketed(on) {
237
- return `\x1b[?2004h${on ? '' : ''}`; // placeholder; real impl toggles bracketed paste
238
- }
1
+ // Cyan-blue visual theme for hncode. Adapts to the terminal's color depth:
2
+ // uses true-color when available, 256/16-color fallbacks otherwise.
3
+
4
+ function colorDepth() {
5
+ try { return process.stdout.getColorDepth?.() || 0; } catch { return 0; }
6
+ }
7
+ const DEPTH = colorDepth();
8
+ export const TRUECOLOR = DEPTH >= 24 || process.env.COLORTERM === 'truecolor' || /^xterm.*-256color$|.*-truecolor$/.test(process.env.TERM || '');
9
+
10
+ function rgb(r, g, b) { return TRUECOLOR ? `\x1b[38;2;${r};${g};${b}m` : `\x1b[38;5;${ansi256(r, g, b)}m`; }
11
+ function brgb(r, g, b) { return TRUECOLOR ? `\x1b[48;2;${r};${g};${b}m` : `\x1b[48;5;${ansi256(r, g, b)}m`; }
12
+
13
+ // Quantize a truecolor triple to the nearest 256-color xterm index.
14
+ function ansi256(r, g, b) {
15
+ const ri = Math.round(r / 51), gi = Math.round(g / 51), bi = Math.round(b / 51);
16
+ if (ri === gi && gi === bi) {
17
+ if (ri === 0) return 16;
18
+ if (ri === 255 / 51) return 232 + ri;
19
+ return 232 + ri; // grayscale ramp
20
+ }
21
+ return 16 + 36 * ri + 6 * gi + bi;
22
+ }
23
+
24
+ // Brand accent: cyan-blue.
25
+ export const TEAL = rgb(0, 184, 219);
26
+ export const CYAN = rgb(0, 215, 255);
27
+ export const BLUE = rgb(66, 135, 245);
28
+ export const FG = TEAL; // primary fg
29
+ export const BG = rgb(0, 18, 26);
30
+ export const BORDER = rgb(70, 130, 180);
31
+
32
+ // Named accent colours per theme. `auto` keeps the brand cyan-blue and lets the
33
+ // terminal's own palette show through for everything else; `dark`/`light` pick
34
+ // accent triples that stay readable on that background.
35
+ const THEMES = {
36
+ auto: {
37
+ fg: [0, 184, 219], bg: [0, 18, 26],
38
+ teal: [0, 184, 219], cyan: [0, 215, 255], blue: [66, 135, 245], border: [70, 130, 180],
39
+ hover: [190, 245, 255], selBg: [0, 70, 84]
40
+ },
41
+ dark: {
42
+ fg: [255, 255, 255], bg: [0, 18, 26], // White foreground for normal text
43
+ teal: [0, 184, 219], cyan: [0, 215, 255], blue: [66, 135, 245], border: [70, 130, 180],
44
+ hover: [190, 245, 255], selBg: [0, 70, 84]
45
+ },
46
+ light: {
47
+ fg: [26, 26, 26], bg: [255, 255, 255],
48
+ teal: [0, 102, 128], cyan: [0, 122, 153], blue: [10, 77, 158], border: [138, 155, 176],
49
+ hover: [0, 78, 102], selBg: [200, 230, 239],
50
+ red: [179, 38, 30], green: [30, 122, 60], yellow: [160, 120, 0], orange: [178, 90, 0],
51
+ white: [26, 26, 26], gray: [85, 85, 85],
52
+ },
53
+ };
54
+ export const THEME_NAMES = Object.keys(THEMES);
55
+
56
+ export const C = {
57
+ reset: '\x1b[0m',
58
+ bold: '\x1b[1m',
59
+ dim: '\x1b[2m',
60
+ // Italic (SGR 3). Used for the BODY of a reasoning block — the prose reads as
61
+ // an aside rather than as the answer. Must be closed with SGR 23 (`endItalic`),
62
+ // NOT with `reset`, because resetting also drops the surrounding dim/gray that
63
+ // the same row depends on. Terminals without italic fall back to normal text,
64
+ // so this is a decoration, never load-bearing.
65
+ italic: '\x1b[3m',
66
+ endItalic: '\x1b[23m',
67
+ muted: '\x1b[90m',
68
+ teal: TEAL,
69
+ cyan: CYAN,
70
+ blue: BLUE,
71
+ fg: FG,
72
+ bg: BG,
73
+ bgTeal: brgb(0, 184, 219),
74
+ bgDim: brgb(12, 24, 30),
75
+ bgPanel: TRUECOLOR ? '\x1b[48;5;236m' : '\x1b[48;5;236m',
76
+ border: BORDER,
77
+ red: '\x1b[91m',
78
+ green: '\x1b[92m',
79
+ yellow: '\x1b[93m',
80
+ magenta: '\x1b[95m',
81
+ gray: '\x1b[90m',
82
+ white: '\x1b[97m',
83
+ orange: TRUECOLOR ? rgb(255, 140, 0) : '\x1b[38;5;208m',
84
+ // Cyan pushed slightly toward green — used for the Swarm mode badge so it reads
85
+ // as its own mode next to Plan (pure cyan) and Focus (blue) while staying in the
86
+ // theme's blue-green family. 38;5;43 is rgb(0,215,175): red at zero and green
87
+ // only 40 above blue, i.e. still clearly cyan with a green cast. 38;5;85 was the
88
+ // first attempt and read as plain GREEN — its red channel is lifted to 95 and
89
+ // green pins at 255, which is a spring green, not a green-tinted cyan.
90
+ spring: TRUECOLOR ? rgb(0, 215, 175) : '\x1b[38;5;43m',
91
+ // Git branch in the status line: yellow pulled about half way toward white, so
92
+ // it stays clearly "yellow" (not cream/ivory) while reading brighter than the
93
+ // plain 93 yellow on a dark background. rgb(255,219,110) keeps red at max,
94
+ // green near it, and blue around 43% — that ratio is what keeps the hue
95
+ // yellow rather than dropping into orange.
96
+ branch: TRUECOLOR ? rgb(255, 219, 110) : '\x1b[38;5;222m',
97
+ // Shell-mode (`!`) accent — violet, matching kimi-code's shellMode token so the
98
+ // bash-mode editor border and the `!` read as "shell", distinct from the cyan
99
+ // prompt and the yellow git badge. #BD93F9 dark / #7C3AED light.
100
+ shellMode: TRUECOLOR ? rgb(189, 147, 249) : '\x1b[38;5;141m',
101
+ bgRed: '\x1b[41m',
102
+ bgGreen: '\x1b[42m',
103
+ // Selection highlight (mouse text selection) and the scrollbar gutter.
104
+ // Selection uses a dark teal background so white text stays readable.
105
+ selBg: TRUECOLOR ? brgb(0, 70, 84) : '\x1b[48;5;23m', // Original dark cyan background
106
+ // Reset ONLY the background colour, keeping the current foreground. Used to end
107
+ // a selection highlight so text after it keeps its own colour (a bare ESC[0m
108
+ // would also reset the foreground, turning a cyan/white run grey).
109
+ bgReset: '\x1b[49m',
110
+ scrollTrack: '\x1b[90m', // dim gutter
111
+ scrollThumb: TRUECOLOR ? rgb(0, 184, 219) : '\x1b[38;5;37m', // brand cyan-blue
112
+ // Scrollbar interaction states:
113
+ // hover -> a touch whiter/brighter than the brand colour
114
+ // active -> BOLD + the brand colour (pressed/dragging)
115
+ // The pressed state used to be a DARKER colour, which on a dark background
116
+ // read as "the thumb disappeared". Making it bold-and-bright means a press is
117
+ // always unmistakably visible. Idle swaps back to scrollThumb automatically.
118
+ scrollThumbHover: TRUECOLOR ? rgb(120, 226, 245) : '\x1b[38;5;123m',
119
+ scrollThumbActive: '\x1b[1m' + (TRUECOLOR ? rgb(0, 220, 255) : '\x1b[38;5;51m'),
120
+ // Hover highlight for clickable rows (menu items, picker options, form
121
+ // fields, composer rows). An explicit bright foreground + BOLD rather than a
122
+ // bare BOLD: BOLD only brightens the 16-colour palette (it does nothing to a
123
+ // true-color row), which is why hover used to be invisible on most rows.
124
+ // tintRange() re-applies this so the row's own SGR codes cannot cancel it.
125
+ hover: '\x1b[1m' + rgb(190, 245, 255),
126
+ };
127
+
128
+ export function style(text, ...codes) {
129
+ return codes.join('') + text + '\x1b[0m';
130
+ }
131
+
132
+ // Linearly interpolate between two RGB triples and return an ANSI colour string.
133
+ // Used for the pulsing Working indicator (orange pulses to a brighter/darker
134
+ // shade over a 2s cycle).
135
+ export function lerpColor(r1, g1, b1, r2, g2, b2, t) {
136
+ const ti = Math.max(0, Math.min(1, t));
137
+ const r = Math.round(r1 + (r2 - r1) * ti);
138
+ const g = Math.round(g1 + (g2 - g1) * ti);
139
+ const b = Math.round(b1 + (b2 - b1) * ti);
140
+ return rgb(r, g, b);
141
+ }
142
+
143
+ // Apply a named theme IN PLACE. `C` is a module-level object and ESM imports are
144
+ // live bindings, so mutating its properties here changes the colours everywhere
145
+ // without re-importing. Returns true when the name was recognised.
146
+ export function setTheme(name) {
147
+ const t = THEMES[name];
148
+ if (!t) return false;
149
+
150
+ // Update foreground colors
151
+ C.teal = rgb(...t.teal);
152
+ C.fg = rgb(...t.fg);
153
+ C.cyan = rgb(...t.cyan);
154
+ C.blue = rgb(...t.blue);
155
+ C.border = rgb(...t.border);
156
+
157
+ // Update background colors
158
+ C.bg = brgb(...t.bg);
159
+ C.bgTeal = brgb(...t.teal);
160
+ C.bgPanel = brgb(...t.bg);
161
+ C.selBg = brgb(...t.selBg);
162
+
163
+ // Scrollbar colours. The active (pressed/dragging) thumb is BOLD + a brighter
164
+ // shade of the theme colour. It used to be a DARKER shade, which on a dark
165
+ // background read as "the thumb vanished" mid-drag.
166
+ C.scrollTrack = '\x1b[90m'; // dim gutter
167
+ C.scrollThumb = rgb(...t.teal);
168
+ C.scrollThumbHover = rgb(...t.hover);
169
+ C.scrollThumbActive = '\x1b[1m' + rgb(
170
+ Math.min(255, Math.round(t.teal[0] * 0.6 + 255 * 0.4)),
171
+ Math.min(255, Math.round(t.teal[1] * 0.6 + 255 * 0.4)),
172
+ Math.min(255, Math.round(t.teal[2] * 0.6 + 255 * 0.4)),
173
+ );
174
+
175
+ // Hover stays visible on this theme's background (bright on dark, dark on light).
176
+ C.hover = '\x1b[1m' + rgb(...(t.hover || [190, 245, 255]));
177
+
178
+ // Semantic colors — swap to deep versions on light backgrounds.
179
+ if (t.white) C.white = rgb(...t.white);
180
+ if (t.gray) C.gray = rgb(...t.gray);
181
+ if (t.red) C.red = rgb(...t.red);
182
+ if (t.green) C.green = rgb(...t.green);
183
+ if (t.yellow) C.yellow = rgb(...t.yellow);
184
+ if (t.orange) C.orange = rgb(...t.orange);
185
+ // The git branch colour is derived per theme: the pale yellow that reads well
186
+ // on the dark background is nearly invisible on white, so on a light theme it
187
+ // is darkened (keeping the same hue) instead of being left as-is.
188
+ if (t.bg && t.bg[0] > 128) {
189
+ C.branch = rgb(146, 106, 0); // dark amber for light backgrounds
190
+ } else {
191
+ C.branch = TRUECOLOR ? rgb(255, 219, 110) : '\x1b[38;5;222m';
192
+ }
193
+
194
+ return true;
195
+ }
196
+
197
+ export function clearScreen() {
198
+ return '\x1b[3J\x1b[H\x1b[2J';
199
+ }
200
+
201
+ // Clear screen and set background color based on current theme
202
+ export function clearAndSetBg() {
203
+ // Reset all attributes first
204
+ const reset = '\x1b[0m';
205
+ // Clear screen
206
+ const clear = '\x1b[3J\x1b[H\x1b[2J';
207
+ // Set background color using current C.bg value
208
+ const bg = C.bg || '\x1b[48;2;0;18;26m'; // Default dark background
209
+ return reset + clear + bg;
210
+ }
211
+ export function eraseLine() {
212
+ return '\x1b[2K';
213
+ }
214
+ export function eraseScreen() {
215
+ return '\x1b[2J';
216
+ }
217
+ export function moveTo(r, c) {
218
+ return `\x1b[${r + 1};${c + 1}H`;
219
+ }
220
+ export function cursorUp(n) { return `\x1b[${n}A`; }
221
+ export function cursorDown(n) { return `\x1b[${n}B`; }
222
+ export function cursorLeft(n) { return `\x1b[${n}D`; }
223
+ export function cursorForward(n) { return `\x1b[${n}C`; }
224
+ export function hideCursor() { return '\x1b[?25l'; }
225
+ export function showCursor() { return '\x1b[?25h'; }
226
+ // Cursor shape: terminals accept DECSCUSR — 2 = block, 4 = underline, 6 = bar(I-beam);
227
+ // 1/3/5 = blinking variants. Force a solid block cursor so positioning is always exact
228
+ // and unaffected by I-beam alignment.
229
+ export function blockCursor() { return '\x1b[2 q'; }
230
+ export function underlineCursor() { return '\x1b[4 q'; }
231
+ export function barCursor() { return '\x1b[6 q'; }
232
+ export function alternateScreen(on) { return on ? '\x1b[?1049h' : '\x1b[?1049l'; }
233
+ export function setScrollRegion(top, bottom) {
234
+ if (process.platform === 'win32') return ''; // win TTY scroll regions are unreliable
235
+ return `\x1b[${top + 1};${bottom + 1}r`;
236
+ }
237
+ export function resetScrollRegion() {
238
+ return '\x1b[r';
239
+ }
240
+ export function setBracketed(on) {
241
+ return `\x1b[?2004h${on ? '' : ''}`; // placeholder; real impl toggles bracketed paste
242
+ }