@hncode/hncode 0.4.2 → 0.5.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 (65) hide show
  1. package/README.md +1 -15
  2. package/README_CN.md +1 -15
  3. package/bin/hncode +22 -9
  4. package/package.json +3 -2
  5. package/src/agent.js +149 -35
  6. package/src/bootstrap.js +56 -0
  7. package/src/btw-panel.js +224 -0
  8. package/src/cache.js +3 -2
  9. package/src/cell-buffer.js +173 -0
  10. package/src/ci.js +4 -1
  11. package/src/clipboard.js +202 -43
  12. package/src/colors.js +478 -242
  13. package/src/config.js +409 -40
  14. package/src/context-bar.js +237 -0
  15. package/src/doctor.js +518 -0
  16. package/src/experiments.js +122 -0
  17. package/src/file-history.js +112 -0
  18. package/src/file-tree.js +255 -0
  19. package/src/flush-tick.js +56 -0
  20. package/src/fork.js +44 -0
  21. package/src/fx.js +148 -0
  22. package/src/index.js +123 -11
  23. package/src/keybindings.js +229 -0
  24. package/src/llm.js +355 -34
  25. package/src/lsp.js +401 -0
  26. package/src/memory-profile.js +247 -0
  27. package/src/notebook.js +232 -0
  28. package/src/output-styles.js +183 -0
  29. package/src/patch.js +248 -0
  30. package/src/permissions.js +130 -1
  31. package/src/plan.js +3 -2
  32. package/src/plugin.js +611 -58
  33. package/src/prompt-presets.js +1 -1
  34. package/src/registry-browser.js +31 -13
  35. package/src/remote.js +20 -2
  36. package/src/rollout.js +151 -0
  37. package/src/row-cache.js +148 -0
  38. package/src/schedule.js +294 -0
  39. package/src/session-approvals.js +86 -0
  40. package/src/session-hub.js +1 -0
  41. package/src/session.js +178 -8
  42. package/src/settings-schema.js +301 -0
  43. package/src/shell-safety.js +473 -0
  44. package/src/side-thread.js +240 -0
  45. package/src/skills.js +27 -0
  46. package/src/stream-buffer.js +350 -0
  47. package/src/subagent-control.js +160 -0
  48. package/src/teleport.js +260 -0
  49. package/src/term-caps.js +411 -0
  50. package/src/term.js +283 -8
  51. package/src/tool-result.js +264 -236
  52. package/src/tools/agent-control.js +98 -0
  53. package/src/tools/agent-swarm.js +11 -2
  54. package/src/tools/agent.js +35 -1
  55. package/src/tools/diagnostics.js +175 -0
  56. package/src/tools/edit.js +25 -10
  57. package/src/tools/grep.js +62 -20
  58. package/src/tools/ignore.js +38 -14
  59. package/src/tools/index.js +10 -0
  60. package/src/tools/notebook.js +113 -0
  61. package/src/tools/read-media-file.js +10 -3
  62. package/src/tools/shell.js +34 -6
  63. package/src/tui.js +5637 -746
  64. package/src/web-public/app.js +7 -0
  65. package/src/web.js +3 -1
package/src/colors.js CHANGED
@@ -1,242 +1,478 @@
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
- }
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-colour xterm index.
14
+ //
15
+ // Two different palettes live in xterm-256 and they must not be confused:
16
+ // * the 6x6x6 COLOUR CUBE (16..231): each channel has 6 levels — 0, then
17
+ // 95,135,175,215,255 — so the step is 40 above a floor of 35, not 51;
18
+ // * the GREY RAMP (232..255): 24 levels from rgb(8) to rgb(238), steps of 10.
19
+ //
20
+ // The grey case used to be `232 + Math.round(v / 51)`, which collapses a whole range of
21
+ // greys onto a few ramp entries: rgb(46,46,46) — the panel surface — quantised to 233,
22
+ // i.e. rgb(18,18,18), three steps darker than asked for. The panel then read as a
23
+ // near-black hole instead of a raised surface. Quantising against the ramp's own step
24
+ // fixes that.
25
+ function ansi256(r, g, b) {
26
+ const cube = (v) => (v < 48 ? 0 : v < 115 ? 1 : Math.min(5, Math.round((v - 35) / 40)));
27
+ // A triple that is (nearly) neutral belongs on the grey ramp, which has finer steps
28
+ // than the cube's 6 levels: rgb(46,46,46) is not representable in the cube at all.
29
+ const max = Math.max(r, g, b);
30
+ const min = Math.min(r, g, b);
31
+ if (max - min <= 8) {
32
+ const v = (r + g + b) / 3;
33
+ const n = Math.max(0, Math.min(23, Math.round((v - 8) / 10)));
34
+ return 232 + n;
35
+ }
36
+ return 16 + 36 * cube(r) + 6 * cube(g) + cube(b);
37
+ }
38
+
39
+ // Brand accent: cyan-blue.
40
+ // A BACKGROUND escape at the terminal's own colour depth: the exact triple under
41
+ // truecolor, the nearest palette index otherwise.
42
+ //
43
+ // Exported because a caller computing a NEW colour at paint time — the backdrop dimming
44
+ // does, when it pulls a fill toward the page — has to state its result in the same notation
45
+ // the untouched fills use. Emitting `48;2;…` unconditionally loses the fill entirely on a
46
+ // 256-colour terminal, which silently ignores what it does not implement.
47
+ export const bgRgb = brgb;
48
+
49
+ export const TEAL = rgb(0, 184, 219);
50
+ export const CYAN = rgb(0, 215, 255);
51
+ export const BLUE = rgb(66, 135, 245);
52
+ export const FG = TEAL; // primary fg
53
+ export const BG = rgb(0, 18, 26);
54
+ export const BORDER = rgb(70, 130, 180);
55
+
56
+ // Named accent colours per theme. `auto` keeps the brand cyan-blue and lets the
57
+ // terminal's own palette show through for everything else; `dark`/`light` pick
58
+ // accent triples that stay readable on that background.
59
+ const THEMES = {
60
+ // `auto` is deliberately NOT a table here: it is a CHOICE, resolved by `setTheme` into
61
+ // `light` or `dark` from the terminal's background. A table named `auto` shadows that,
62
+ // and did — it was a fixed dark palette with brand-cyan text, while its own label claimed
63
+ // it followed the terminal.
64
+ dark: {
65
+ fg: [255, 255, 255], bg: [0, 18, 26], // White foreground for normal text
66
+ fg: [255, 255, 255], bg: [0, 18, 26], // White foreground for normal text
67
+ teal: [0, 184, 219], cyan: [0, 215, 255], blue: [66, 135, 245], border: [70, 130, 180],
68
+ // The semantic colours. They were missing, and `setTheme` guards each assignment, so
69
+ // they kept the previous theme's values — a theme that only looks right after a
70
+ // particular one was applied.
71
+ red: [230, 80, 80], green: [120, 200, 120], yellow: [230, 200, 100], orange: [240, 160, 80],
72
+ white: [255, 255, 255], gray: [130, 140, 150],
73
+ hover: [190, 245, 255], selBg: [0, 70, 84],
74
+ surface: [46, 46, 46],
75
+ },
76
+ light: {
77
+ fg: [26, 26, 26], bg: [255, 255, 255],
78
+ teal: [0, 102, 128], cyan: [0, 122, 153], blue: [10, 77, 158], border: [138, 155, 176],
79
+ hover: [0, 78, 102], selBg: [200, 230, 239],
80
+ red: [179, 38, 30], green: [30, 122, 60], yellow: [160, 120, 0], orange: [178, 90, 0],
81
+ white: [26, 26, 26], gray: [85, 85, 85],
82
+ // A LIGHT surface is a grey that is darker than the white page but far lighter than
83
+ // the dark themes' — a panel has to read as raised in whichever direction the theme
84
+ // goes, and reusing #2e2e2e on white would be a black box.
85
+ surface: [232, 232, 232],
86
+ },
87
+ // The four below are the same SHAPE as `dark` with the accent hue moved, so every
88
+ // derived colour in `setTheme` (the scrollbar shades, hover, and the light/dark branch
89
+ // test) keeps working with no special case. Each keeps ONE accent family varying only in
90
+ // lightness, because the context bar's five fills read as a gradient and a second hue in
91
+ // that ramp makes the bar look broken.
92
+ nord: {
93
+ fg: [216, 222, 233], bg: [46, 52, 64],
94
+ teal: [136, 192, 208], cyan: [143, 188, 187], blue: [129, 161, 193], border: [94, 129, 172],
95
+ hover: [236, 239, 244], selBg: [59, 66, 82],
96
+ red: [191, 97, 106], green: [163, 190, 140], yellow: [235, 203, 139], orange: [208, 135, 112],
97
+ white: [236, 239, 244], gray: [129, 135, 148],
98
+ surface: [59, 66, 82],
99
+ },
100
+ dracula: {
101
+ fg: [248, 248, 242], bg: [40, 42, 54],
102
+ teal: [139, 233, 253], cyan: [139, 233, 253], blue: [98, 114, 164], border: [98, 114, 164],
103
+ hover: [255, 121, 198], selBg: [68, 71, 90],
104
+ red: [255, 85, 85], green: [80, 250, 123], yellow: [241, 250, 140], orange: [255, 184, 108],
105
+ white: [248, 248, 242], gray: [98, 114, 164],
106
+ surface: [68, 71, 90],
107
+ },
108
+ gruvbox: {
109
+ fg: [235, 219, 178], bg: [40, 40, 40],
110
+ teal: [142, 192, 124], cyan: [131, 165, 152], blue: [69, 133, 136], border: [146, 131, 116],
111
+ hover: [250, 189, 47], selBg: [60, 56, 54],
112
+ red: [251, 73, 52], green: [184, 187, 38], yellow: [250, 189, 47], orange: [254, 128, 25],
113
+ white: [235, 219, 178], gray: [146, 131, 116],
114
+ surface: [60, 56, 54],
115
+ },
116
+ mono: {
117
+ // No hue at all: every accent is a grey, so the UI is readable on a monochrome
118
+ // terminal or for anyone who needs colour off without losing the structure that
119
+ // colour was carrying. The lightness steps are what keep the context bar legible.
120
+ fg: [230, 230, 230], bg: [18, 18, 18],
121
+ teal: [190, 190, 190], cyan: [210, 210, 210], blue: [150, 150, 150], border: [110, 110, 110],
122
+ hover: [255, 255, 255], selBg: [60, 60, 60],
123
+ red: [200, 90, 90], green: [140, 190, 140], yellow: [200, 190, 120], orange: [210, 160, 110],
124
+ white: [240, 240, 240], gray: [130, 130, 130],
125
+ surface: [42, 42, 42],
126
+ },
127
+ catppuccin: {
128
+ // Mocha: a warm-tinted dark with pastel accents. Every accent is desaturated toward
129
+ // the background so long replies do not glare, which is the whole point of the palette.
130
+ fg: [205, 214, 244], bg: [30, 30, 46],
131
+ teal: [137, 180, 250], cyan: [137, 220, 235], blue: [137, 180, 250], border: [108, 112, 134],
132
+ hover: [203, 166, 247], selBg: [49, 50, 68],
133
+ red: [243, 139, 168], green: [166, 227, 161], yellow: [249, 226, 175], orange: [250, 179, 135],
134
+ white: [205, 214, 244], gray: [127, 132, 156],
135
+ surface: [49, 50, 68],
136
+ },
137
+ tokyo: {
138
+ // Night: a blue-black with high-contrast accents. Distinct from nord, which is greyed
139
+ // out; this keeps the blue in the background and lets the cyan carry the structure.
140
+ fg: [192, 202, 245], bg: [26, 27, 38],
141
+ teal: [125, 207, 255], cyan: [125, 207, 255], blue: [122, 162, 247], border: [86, 95, 137],
142
+ hover: [187, 154, 247], selBg: [41, 46, 66],
143
+ red: [247, 118, 142], green: [158, 206, 106], yellow: [224, 175, 104], orange: [255, 158, 100],
144
+ white: [192, 202, 245], gray: [86, 95, 137],
145
+ surface: [41, 46, 66],
146
+ },
147
+ solar: {
148
+ // Solarized LIGHT. Included because `light` is a plain neutral and solarized's even
149
+ // hue ramp is the best-tested light palette in the terminal world. The dark variant is
150
+ // `solar-dark`, so both exist; the light one is the one worth having.
151
+ fg: [101, 123, 131], bg: [253, 246, 227],
152
+ teal: [38, 139, 210], cyan: [38, 139, 210], blue: [108, 113, 196], border: [147, 161, 161],
153
+ hover: [211, 54, 130], selBg: [238, 232, 213],
154
+ red: [220, 50, 47], green: [133, 153, 0], yellow: [181, 137, 0], orange: [203, 75, 22],
155
+ white: [253, 246, 227], gray: [147, 161, 161],
156
+ surface: [238, 232, 213],
157
+ },
158
+ rose: {
159
+ // Pine: a muted mauve dark, between dracula's saturation and nord's restraint.
160
+ fg: [224, 222, 244], bg: [25, 23, 36],
161
+ teal: [196, 167, 231], cyan: [137, 180, 250], blue: [137, 180, 250], border: [110, 106, 134],
162
+ hover: [196, 167, 231], selBg: [31, 29, 46],
163
+ red: [215, 130, 126], green: [156, 207, 216], yellow: [234, 157, 173], orange: [235, 188, 186],
164
+ white: [224, 222, 244], gray: [110, 106, 134],
165
+ surface: [31, 29, 46],
166
+ },
167
+ };
168
+ /** Theme name -> one-line description, for the /theme picker. */
169
+ export const THEME_LABELS = {
170
+ auto: 'follow the terminal: light theme on a light background, dark otherwise',
171
+ dark: 'dark teal background, white text',
172
+ light: 'light background, darkened accents',
173
+ nord: 'cool blue-grey',
174
+ dracula: 'high-contrast purple and pink',
175
+ gruvbox: 'warm retro earth tones',
176
+ catppuccin: 'soft pastel, warm-tinted dark',
177
+ tokyo: 'blue-black, high-contrast accents',
178
+ solar: 'even hue ramp, light background',
179
+ rose: 'muted mauve dark',
180
+ mono: 'greyscale, no hue',
181
+ };
182
+
183
+ /** `auto` is a name you can set without being a palette, so it is listed separately from
184
+ * the tables — see `setTheme` for how it resolves. */
185
+ export const AUTO_THEME = 'auto';
186
+
187
+ /** Whether a theme name is one we know. */
188
+ export function hasTheme(name) {
189
+ return name === AUTO_THEME || Object.prototype.hasOwnProperty.call(THEMES, name);
190
+ }
191
+
192
+ /** The names a user may choose, `auto` first because it is the default. */
193
+ export const THEME_NAMES = [AUTO_THEME, ...Object.keys(THEMES)];
194
+
195
+ /** True when a theme paints on a light background. Resolves `auto` first, so a caller
196
+ * asking about the CURRENT theme gets the answer for what is actually on screen. */
197
+ export function isLightTheme(name) {
198
+ const resolved = name === AUTO_THEME ? (autoIsLight ? 'light' : 'dark') : name;
199
+ const t = THEMES[resolved];
200
+ return !!(t && Array.isArray(t.bg) && t.bg[0] > 128);
201
+ }
202
+
203
+ export const C = {
204
+ reset: '\x1b[0m',
205
+ bold: '\x1b[1m',
206
+ dim: '\x1b[2m',
207
+ // Italic (SGR 3). Used for the BODY of a reasoning block — the prose reads as
208
+ // an aside rather than as the answer. Must be closed with SGR 23 (`endItalic`),
209
+ // NOT with `reset`, because resetting also drops the surrounding dim/gray that
210
+ // the same row depends on. Terminals without italic fall back to normal text,
211
+ // so this is a decoration, never load-bearing.
212
+ italic: '\x1b[3m',
213
+ endItalic: '\x1b[23m',
214
+ muted: '\x1b[90m',
215
+ teal: TEAL,
216
+ cyan: CYAN,
217
+ blue: BLUE,
218
+ fg: FG,
219
+ bg: BG,
220
+ bgTeal: brgb(0, 184, 219),
221
+ bgDim: brgb(12, 24, 30),
222
+ // Fills for the segmented context bar: five blues in one family, deepening towards
223
+ // black, plus a neutral free segment. Background colours, because the bar is read by
224
+ // FILL rather than by any text — a segment is usually only a few columns wide, so a
225
+ // letter inside it would be unreadable noise.
226
+ //
227
+ // The steps are deliberately narrow. Widening them makes the bar easier to tell apart
228
+ // at a glance but harder to keep a dark terminal readable, and the darkest member has
229
+ // to stay distinguishable from the page background it sits on.
230
+ ctxSystem: brgb(35, 48, 95), // deepest navy
231
+ ctxPrompt: brgb(43, 61, 120), // navy
232
+ ctxAssistant: brgb(52, 74, 146), // indigo
233
+ ctxThinking: brgb(77, 107, 254), // brand blue, the brightest used segment
234
+ ctxTools: brgb(90, 124, 255), // light blue
235
+ ctxFree: brgb(46, 46, 46), // neutral grey: "nothing here yet"
236
+ // The SURFACE colour for a floating panel (picker menus, dialogs). A NEUTRAL GREY,
237
+ // deliberately separate from the theme's `bg`:
238
+ //
239
+ // * the theme background is `[0, 18, 26]` — a dark TEAL. A raised surface should be
240
+ // neutral in every theme; only the ACCENTS carry the brand hue. Reusing `bg` for
241
+ // the panel made every popup read blue, which is not what a panel is.
242
+ // * `applyTheme` used to overwrite this with `t.bg`, so the value here only applied
243
+ // before a theme was set. It is no longer touched by the theme.
244
+ //
245
+ // xterm-256's grey ramp runs 232..255 from rgb(8) to rgb(238) in steps of 10, so 236
246
+ // is rgb(48,48,48) = #303030 — clearly lifted off a near-black terminal without
247
+ // washing the text out. Truecolor gets `#2e2e2e`, the same shade written exactly.
248
+ bgPanel: TRUECOLOR ? '\x1b[48;2;46;46;46m' : '\x1b[48;5;236m',
249
+ border: BORDER,
250
+ red: '\x1b[91m',
251
+ green: '\x1b[92m',
252
+ yellow: '\x1b[93m',
253
+ magenta: '\x1b[95m',
254
+ gray: '\x1b[90m',
255
+ white: '\x1b[97m',
256
+ orange: TRUECOLOR ? rgb(255, 140, 0) : '\x1b[38;5;208m',
257
+ // Cyan pushed slightly toward green — used for the Swarm mode badge so it reads
258
+ // as its own mode next to Plan (pure cyan) and Focus (blue) while staying in the
259
+ // theme's blue-green family. 38;5;43 is rgb(0,215,175): red at zero and green
260
+ // only 40 above blue, i.e. still clearly cyan with a green cast. 38;5;85 was the
261
+ // first attempt and read as plain GREEN — its red channel is lifted to 95 and
262
+ // green pins at 255, which is a spring green, not a green-tinted cyan.
263
+ spring: TRUECOLOR ? rgb(0, 215, 175) : '\x1b[38;5;43m',
264
+ // Git branch in the status line: yellow pulled about half way toward white, so
265
+ // it stays clearly "yellow" (not cream/ivory) while reading brighter than the
266
+ // plain 93 yellow on a dark background. rgb(255,219,110) keeps red at max,
267
+ // green near it, and blue around 43% — that ratio is what keeps the hue
268
+ // yellow rather than dropping into orange.
269
+ branch: TRUECOLOR ? rgb(255, 219, 110) : '\x1b[38;5;222m',
270
+ // Shell-mode (`!`) accent — violet, matching kimi-code's shellMode token so the
271
+ // bash-mode editor border and the `!` read as "shell", distinct from the cyan
272
+ // prompt and the yellow git badge. #BD93F9 dark / #7C3AED light.
273
+ shellMode: TRUECOLOR ? rgb(189, 147, 249) : '\x1b[38;5;141m',
274
+ bgRed: '\x1b[41m',
275
+ bgGreen: '\x1b[42m',
276
+ // Selection highlight (mouse text selection) and the scrollbar gutter.
277
+ // Selection uses a dark teal background so white text stays readable.
278
+ selBg: TRUECOLOR ? brgb(0, 70, 84) : '\x1b[48;5;23m', // Original dark cyan background
279
+ // Reset ONLY the background colour, keeping the current foreground. Used to end
280
+ // a selection highlight so text after it keeps its own colour (a bare ESC[0m
281
+ // would also reset the foreground, turning a cyan/white run grey).
282
+ bgReset: '\x1b[49m',
283
+ // Scrollbar, styled after OpenTUI (what cline uses): a solid block thumb on a
284
+ // near-black track. Only the FOREGROUND colour is stateful — the track is always the
285
+ // background — because that is how OpenTUI's SliderRenderable paints a cell:
286
+ // foreground = thumb, background = track. A `█` covers both columns with the
287
+ // foreground, while `▀`/`▄` expose one half to the track, so the two colours must
288
+ // stay DISTINCT; giving a half-cell the thumb colour as its background is what made
289
+ // it read as a second square, and it also survived the modal backdrop's faint
290
+ // attribute, which fades the foreground only.
291
+ // track #252527 thumb #9a9ea3 (OpenTUI's defaults, inlined)
292
+ // hover #d2d6da active #ffffff (brighter thumb, not a new hue)
293
+ scrollTrackBg: TRUECOLOR ? brgb(0x25, 0x25, 0x27) : '\x1b[48;5;235m',
294
+ scrollTrackFg: TRUECOLOR ? rgb(0x25, 0x25, 0x27) : '\x1b[38;5;235m',
295
+ scrollThumbBg: TRUECOLOR ? brgb(0x9a, 0x9e, 0xa3) : '\x1b[48;5;246m',
296
+ scrollThumbFg: TRUECOLOR ? rgb(0x9a, 0x9e, 0xa3) : '\x1b[38;5;246m',
297
+ scrollThumbHoverBg: TRUECOLOR ? brgb(0xd2, 0xd6, 0xda) : '\x1b[48;5;252m',
298
+ scrollThumbHoverFg: TRUECOLOR ? rgb(0xd2, 0xd6, 0xda) : '\x1b[38;5;252m',
299
+ scrollThumbActiveBg: TRUECOLOR ? brgb(0xff, 0xff, 0xff) : '\x1b[48;5;231m',
300
+ scrollThumbActiveFg: TRUECOLOR ? rgb(0xff, 0xff, 0xff) : '\x1b[38;5;231m',
301
+ // FOREGROUND aliases kept for the todo/queue resize rule, which draws a `─` line
302
+ // and therefore needs a foreground, not the block's background pair.
303
+ scrollThumbActive: TRUECOLOR ? rgb(0xff, 0xff, 0xff) : '\x1b[38;5;231m',
304
+ scrollThumbHover: TRUECOLOR ? rgb(0xd2, 0xd6, 0xda) : '\x1b[38;5;252m',
305
+ // Legacy foreground forms, still referenced by the gutter-hover path.
306
+ scrollTrack: '\x1b[90m',
307
+ scrollThumb: TRUECOLOR ? rgb(0, 184, 219) : '\x1b[38;5;37m',
308
+ // Hover highlight for clickable rows (menu items, picker options, form
309
+ // fields, composer rows). An explicit bright foreground + BOLD rather than a
310
+ // bare BOLD: BOLD only brightens the 16-colour palette (it does nothing to a
311
+ // true-color row), which is why hover used to be invisible on most rows.
312
+ // tintRange() re-applies this so the row's own SGR codes cannot cancel it.
313
+ hover: '\x1b[1m' + rgb(190, 245, 255),
314
+ };
315
+
316
+ export function style(text, ...codes) {
317
+ return codes.join('') + text + '\x1b[0m';
318
+ }
319
+
320
+ // Linearly interpolate between two RGB triples and return an ANSI colour string.
321
+ // Used for the pulsing Working indicator (orange pulses to a brighter/darker
322
+ // shade over a 2s cycle).
323
+ export function lerpColor(r1, g1, b1, r2, g2, b2, t) {
324
+ const ti = Math.max(0, Math.min(1, t));
325
+ const r = Math.round(r1 + (r2 - r1) * ti);
326
+ const g = Math.round(g1 + (g2 - g1) * ti);
327
+ const b = Math.round(b1 + (b2 - b1) * ti);
328
+ return rgb(r, g, b);
329
+ }
330
+
331
+ // Which concrete theme `auto` resolves to right now. `setTheme` keeps it so a repaint or a
332
+ // `/theme auto` after a terminal probe lands on the same answer.
333
+ let autoIsLight = false;
334
+
335
+ // The theme `setTheme` last applied, resolved (so `auto` reports the concrete name it
336
+ // became). Read by the render cache: a cached row holds finished escapes, so a palette
337
+ // change has to invalidate them or the old colours survive the switch.
338
+ let appliedTheme = '';
339
+
340
+ /** The concrete theme in force, or `''` before the first `setTheme`. */
341
+ export function currentTheme() { return appliedTheme; }
342
+
343
+ /**
344
+ * Tell `setTheme` whether the terminal has a light background.
345
+ *
346
+ * The probe lives in term-caps.js and this module is a leaf that imports nothing, so the
347
+ * answer has to be handed in. Called once after the probe and again if the terminal is
348
+ * resized on a different display.
349
+ */
350
+ export function setLightBackground(light) {
351
+ autoIsLight = !!light;
352
+ }
353
+
354
+ // Apply a named theme IN PLACE. `C` is a module-level object and ESM imports are
355
+ // live bindings, so mutating its properties here changes the colours everywhere
356
+ // without re-importing. Returns true when the name was recognised.
357
+ //
358
+ // `auto` is a CHOICE, not a palette: it resolves to `light` on a light terminal and to
359
+ // `dark` otherwise, and then applies that theme's colours. It used to be a fixed table —
360
+ // a dark background with brand-cyan text — which is not what "auto" means and not what its
361
+ // own label claimed ("on the terminal's own palette"). The user asking for auto is asking
362
+ // not to have to think about it.
363
+ export function setTheme(name) {
364
+ const resolved = name === 'auto' ? (autoIsLight ? 'light' : 'dark') : name;
365
+ const t = THEMES[resolved];
366
+ if (!t) return false;
367
+ appliedTheme = resolved;
368
+
369
+
370
+
371
+ // Update foreground colors
372
+ C.teal = rgb(...t.teal);
373
+ C.fg = rgb(...t.fg);
374
+ C.cyan = rgb(...t.cyan);
375
+ C.blue = rgb(...t.blue);
376
+ C.border = rgb(...t.border);
377
+ // Update background colors
378
+ C.bg = brgb(...t.bg);
379
+ C.bgTeal = brgb(...t.teal);
380
+ // The panel surface is NEUTRAL GREY and comes from the theme's own `surface` triple,
381
+ // NOT from `bg`. It used to be set to `t.bg` (a dark teal), which made every popup
382
+ // carry a blue cast; a raised surface must be neutral, with only the accents following
383
+ // the brand hue. A light theme gets a light grey so the panel still reads as raised.
384
+ if (t.surface) C.bgPanel = brgb(...t.surface);
385
+ C.selBg = brgb(...t.selBg);
386
+
387
+ // Scrollbar colours. The thumb is a BLOCK, so its colour goes on BOTH the
388
+ // foreground and the background (a half-cell `▀`/`▄` shows the foreground, a full
389
+ // `█` shows the background — they must match). Hover / drag are brighter shades
390
+ // of the same hue, applied to the same pair, so the whole thumb changes together.
391
+ const active = [
392
+ Math.min(255, Math.round(t.teal[0] * 0.6 + 255 * 0.4)),
393
+ Math.min(255, Math.round(t.teal[1] * 0.6 + 255 * 0.4)),
394
+ Math.min(255, Math.round(t.teal[2] * 0.6 + 255 * 0.4)),
395
+ ];
396
+ const hover = t.hover || [190, 245, 255];
397
+ C.scrollTrackFg = rgb(0x25, 0x25, 0x27);
398
+ C.scrollTrackBg = brgb(0x25, 0x25, 0x27);
399
+ C.scrollThumbFg = rgb(...t.teal);
400
+ C.scrollThumbBg = brgb(...t.teal);
401
+ C.scrollThumbHoverFg = rgb(...hover);
402
+ C.scrollThumbHoverBg = brgb(...hover);
403
+ C.scrollThumbActiveFg = rgb(...active);
404
+ C.scrollThumbActiveBg = brgb(...active);
405
+ // Foreground aliases for the todo/queue resize rule.
406
+ C.scrollTrack = '\x1b[90m';
407
+ C.scrollThumb = rgb(...t.teal);
408
+ C.scrollThumbHover = rgb(...hover);
409
+ C.scrollThumbActive = rgb(...active);
410
+
411
+ // Hover stays visible on this theme's background (bright on dark, dark on light).
412
+ C.hover = '\x1b[1m' + rgb(...(t.hover || [190, 245, 255]));
413
+
414
+ // Semantic colors — swap to deep versions on light backgrounds.
415
+ if (t.white) C.white = rgb(...t.white);
416
+ if (t.gray) C.gray = rgb(...t.gray);
417
+ if (t.red) C.red = rgb(...t.red);
418
+ if (t.green) C.green = rgb(...t.green);
419
+ if (t.yellow) C.yellow = rgb(...t.yellow);
420
+ if (t.orange) C.orange = rgb(...t.orange);
421
+ // The git branch colour is derived per theme: the pale yellow that reads well
422
+ // on the dark background is nearly invisible on white, so on a light theme it
423
+ // is darkened (keeping the same hue) instead of being left as-is.
424
+ if (t.bg && t.bg[0] > 128) {
425
+ C.branch = rgb(146, 106, 0); // dark amber for light backgrounds
426
+ } else {
427
+ C.branch = TRUECOLOR ? rgb(255, 219, 110) : '\x1b[38;5;222m';
428
+ }
429
+
430
+ return true;
431
+ }
432
+
433
+ export function clearScreen() {
434
+ return '\x1b[3J\x1b[H\x1b[2J';
435
+ }
436
+
437
+ // Clear screen and set background color based on current theme
438
+ export function clearAndSetBg() {
439
+ // Reset all attributes first
440
+ const reset = '\x1b[0m';
441
+ // Clear screen
442
+ const clear = '\x1b[3J\x1b[H\x1b[2J';
443
+ // Set background color using current C.bg value
444
+ const bg = C.bg || '\x1b[48;2;0;18;26m'; // Default dark background
445
+ return reset + clear + bg;
446
+ }
447
+ export function eraseLine() {
448
+ return '\x1b[2K';
449
+ }
450
+ export function eraseScreen() {
451
+ return '\x1b[2J';
452
+ }
453
+ export function moveTo(r, c) {
454
+ return `\x1b[${r + 1};${c + 1}H`;
455
+ }
456
+ export function cursorUp(n) { return `\x1b[${n}A`; }
457
+ export function cursorDown(n) { return `\x1b[${n}B`; }
458
+ export function cursorLeft(n) { return `\x1b[${n}D`; }
459
+ export function cursorForward(n) { return `\x1b[${n}C`; }
460
+ export function hideCursor() { return '\x1b[?25l'; }
461
+ export function showCursor() { return '\x1b[?25h'; }
462
+ // Cursor shape: terminals accept DECSCUSR — 2 = block, 4 = underline, 6 = bar(I-beam);
463
+ // 1/3/5 = blinking variants. Force a solid block cursor so positioning is always exact
464
+ // and unaffected by I-beam alignment.
465
+ export function blockCursor() { return '\x1b[2 q'; }
466
+ export function underlineCursor() { return '\x1b[4 q'; }
467
+ export function barCursor() { return '\x1b[6 q'; }
468
+ export function alternateScreen(on) { return on ? '\x1b[?1049h' : '\x1b[?1049l'; }
469
+ export function setScrollRegion(top, bottom) {
470
+ if (process.platform === 'win32') return ''; // win TTY scroll regions are unreliable
471
+ return `\x1b[${top + 1};${bottom + 1}r`;
472
+ }
473
+ export function resetScrollRegion() {
474
+ return '\x1b[r';
475
+ }
476
+ export function setBracketed(on) {
477
+ return `\x1b[?2004h${on ? '' : ''}`; // placeholder; real impl toggles bracketed paste
478
+ }