@hncode/hncode 0.1.3 → 0.1.5
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.
- package/LICENSE +219 -219
- package/README.md +198 -197
- package/README_CN.md +187 -186
- package/bin/install.js +95 -95
- package/bin/preinstall.js +41 -41
- package/package.json +35 -35
- package/src/colors.js +53 -4
- package/src/tui.js +38 -9
package/package.json
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@hncode/hncode",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "hncode — A cyan-blue TUI AI coding agent with zero runtime dependencies.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "src/index.js",
|
|
7
|
-
"bin": {
|
|
8
|
-
"hncode": "bin/hncode"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"bin/",
|
|
12
|
-
"src/",
|
|
13
|
-
"LICENSE",
|
|
14
|
-
"README.md",
|
|
15
|
-
"README_CN.md",
|
|
16
|
-
"package.json"
|
|
17
|
-
],
|
|
18
|
-
"scripts": {
|
|
19
|
-
"preinstall": "node bin/preinstall.js",
|
|
20
|
-
"install": "node bin/install.js"
|
|
21
|
-
},
|
|
22
|
-
"engines": {
|
|
23
|
-
"node": ">=20"
|
|
24
|
-
},
|
|
25
|
-
"license": "GPL-3.0-only",
|
|
26
|
-
"author": "NiceHello666",
|
|
27
|
-
"keywords": [
|
|
28
|
-
"cli",
|
|
29
|
-
"agent",
|
|
30
|
-
"coding",
|
|
31
|
-
"ai",
|
|
32
|
-
"tui"
|
|
33
|
-
],
|
|
34
|
-
"dependencies": {}
|
|
35
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@hncode/hncode",
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "hncode — A cyan-blue TUI AI coding agent with zero runtime dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"hncode": "bin/hncode"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"src/",
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"README.md",
|
|
15
|
+
"README_CN.md",
|
|
16
|
+
"package.json"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"preinstall": "node bin/preinstall.js",
|
|
20
|
+
"install": "node bin/install.js"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"license": "GPL-3.0-only",
|
|
26
|
+
"author": "NiceHello666",
|
|
27
|
+
"keywords": [
|
|
28
|
+
"cli",
|
|
29
|
+
"agent",
|
|
30
|
+
"coding",
|
|
31
|
+
"ai",
|
|
32
|
+
"tui"
|
|
33
|
+
],
|
|
34
|
+
"dependencies": {}
|
|
35
|
+
}
|
package/src/colors.js
CHANGED
|
@@ -33,9 +33,23 @@ export const BORDER = rgb(70, 130, 180);
|
|
|
33
33
|
// terminal's own palette show through for everything else; `dark`/`light` pick
|
|
34
34
|
// accent triples that stay readable on that background.
|
|
35
35
|
const THEMES = {
|
|
36
|
-
auto: {
|
|
37
|
-
|
|
38
|
-
|
|
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: [0, 200, 230], bg: [0, 18, 26],
|
|
43
|
+
teal: [0, 200, 230], cyan: [90, 225, 255], blue: [90, 150, 255], border: [80, 140, 190],
|
|
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
|
+
},
|
|
39
53
|
};
|
|
40
54
|
export const THEME_NAMES = Object.keys(THEMES);
|
|
41
55
|
|
|
@@ -101,20 +115,55 @@ export function lerpColor(r1, g1, b1, r2, g2, b2, t) {
|
|
|
101
115
|
export function setTheme(name) {
|
|
102
116
|
const t = THEMES[name];
|
|
103
117
|
if (!t) return false;
|
|
118
|
+
|
|
119
|
+
// Update foreground colors
|
|
104
120
|
C.teal = rgb(...t.teal);
|
|
105
|
-
C.fg =
|
|
121
|
+
C.fg = rgb(...t.fg);
|
|
106
122
|
C.cyan = rgb(...t.cyan);
|
|
107
123
|
C.blue = rgb(...t.blue);
|
|
108
124
|
C.border = rgb(...t.border);
|
|
125
|
+
|
|
126
|
+
// Update background colors
|
|
127
|
+
C.bg = brgb(...t.bg);
|
|
109
128
|
C.bgTeal = brgb(...t.teal);
|
|
129
|
+
C.bgPanel = brgb(...t.bg);
|
|
130
|
+
C.selBg = brgb(...t.selBg);
|
|
131
|
+
|
|
132
|
+
// Scrollbar colors - adjust based on theme brightness
|
|
133
|
+
const isLight = t.bg[0] > 128; // Simple brightness check
|
|
134
|
+
C.scrollTrack = isLight ? '\x1b[90m' : '\x1b[90m'; // dim gutter (works for both)
|
|
135
|
+
C.scrollThumb = rgb(...t.teal);
|
|
136
|
+
C.scrollThumbHover = rgb(...t.hover);
|
|
137
|
+
C.scrollThumbActive = rgb(t.teal[0] * 0.7, t.teal[1] * 0.7, t.teal[2] * 0.7);
|
|
138
|
+
|
|
110
139
|
// Hover stays visible on this theme's background (bright on dark, dark on light).
|
|
111
140
|
C.hover = '\x1b[1m' + rgb(...(t.hover || [190, 245, 255]));
|
|
141
|
+
|
|
142
|
+
// Semantic colors — swap to deep versions on light backgrounds.
|
|
143
|
+
if (t.white) C.white = rgb(...t.white);
|
|
144
|
+
if (t.gray) C.gray = rgb(...t.gray);
|
|
145
|
+
if (t.red) C.red = rgb(...t.red);
|
|
146
|
+
if (t.green) C.green = rgb(...t.green);
|
|
147
|
+
if (t.yellow) C.yellow = rgb(...t.yellow);
|
|
148
|
+
if (t.orange) C.orange = rgb(...t.orange);
|
|
149
|
+
|
|
112
150
|
return true;
|
|
113
151
|
}
|
|
114
152
|
|
|
115
153
|
export function clearScreen() {
|
|
116
154
|
return '\x1b[3J\x1b[H\x1b[2J';
|
|
117
155
|
}
|
|
156
|
+
|
|
157
|
+
// Clear screen and set background color based on current theme
|
|
158
|
+
export function clearAndSetBg() {
|
|
159
|
+
// Reset all attributes first
|
|
160
|
+
const reset = '\x1b[0m';
|
|
161
|
+
// Clear screen
|
|
162
|
+
const clear = '\x1b[3J\x1b[H\x1b[2J';
|
|
163
|
+
// Set background color using current C.bg value
|
|
164
|
+
const bg = C.bg || '\x1b[48;2;0;18;26m'; // Default dark background
|
|
165
|
+
return reset + clear + bg;
|
|
166
|
+
}
|
|
118
167
|
export function eraseLine() {
|
|
119
168
|
return '\x1b[2K';
|
|
120
169
|
}
|
package/src/tui.js
CHANGED
|
@@ -18,7 +18,7 @@ import fs from 'node:fs';
|
|
|
18
18
|
import os from 'node:os';
|
|
19
19
|
import cp from 'node:child_process';
|
|
20
20
|
import {
|
|
21
|
-
C, blockCursor, showCursor, hideCursor, alternateScreen, clearScreen, setTheme, THEME_NAMES, lerpColor,
|
|
21
|
+
C, blockCursor, showCursor, hideCursor, alternateScreen, clearScreen, clearAndSetBg, setTheme, THEME_NAMES, lerpColor,
|
|
22
22
|
} from './colors.js';
|
|
23
23
|
import { copyText, readText, warmClipboard, readImage } from './clipboard.js';
|
|
24
24
|
import { visualWidth, estimateTokens, estimateMessagesTokens, expandTabs } from './term.js';
|
|
@@ -707,14 +707,15 @@ export function isFailureResult(text) {
|
|
|
707
707
|
|
|
708
708
|
function msgColorFor(role, text) {
|
|
709
709
|
if (role === 'system' && typeof text === 'string' && text.startsWith('[turn took')) return C.gray;
|
|
710
|
-
if (role === 'system') return C.
|
|
710
|
+
if (role === 'system') return C.teal;
|
|
711
711
|
if (role === 'user') return C.cyan;
|
|
712
712
|
if (role === 'warn') return C.orange; // unfinished-turn warning
|
|
713
713
|
if (role === 'queued') return C.gray; // pending, not yet sent
|
|
714
714
|
if (role === 'steer') return C.yellow; // injected into the running turn
|
|
715
|
-
if (role === 'tool') return C.
|
|
715
|
+
if (role === 'tool') return C.blue;
|
|
716
716
|
if (role === 'tool_result') return C.gray;
|
|
717
|
-
|
|
717
|
+
// Default foreground color based on theme
|
|
718
|
+
return C.fg;
|
|
718
719
|
}
|
|
719
720
|
|
|
720
721
|
// ---- tool call display: "Using Name (keyArg)" / "Used Name (keyArg) ----
|
|
@@ -2400,7 +2401,7 @@ export function makeState({ cfg, session, opts }) {
|
|
|
2400
2401
|
planPath: (session && session.planPath) || null,
|
|
2401
2402
|
focus: !!(session && session.focus),
|
|
2402
2403
|
effort: (session && session.effort) || cfg.effort || (cfg.reasoning ? 'on' : 'off'),
|
|
2403
|
-
|
|
2404
|
+
// Theme removed - forced dark only
|
|
2404
2405
|
addDirs: [],
|
|
2405
2406
|
tasks: {},
|
|
2406
2407
|
slMode: true, slModel: true, slEffort: true, slCwd: true, slTasks: true, slTips: true,
|
|
@@ -3161,6 +3162,18 @@ async function dispatch(cmdRaw, arg, state, cfg, session, h) {
|
|
|
3161
3162
|
: `theme = "${name}"\n` + txt;
|
|
3162
3163
|
fs.writeFileSync(f, next, 'utf8');
|
|
3163
3164
|
} catch { }
|
|
3165
|
+
|
|
3166
|
+
// Force a complete screen refresh with new theme colors
|
|
3167
|
+
// We need to access stdout from the host context
|
|
3168
|
+
if (host && typeof host.renderFrame === 'function') {
|
|
3169
|
+
// Trigger immediate re-render which will use new theme colors
|
|
3170
|
+
host.renderFrame();
|
|
3171
|
+
} else {
|
|
3172
|
+
// Fallback: just show message and let normal flow handle it
|
|
3173
|
+
app(`Theme set to "${name}".`);
|
|
3174
|
+
return true;
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3164
3177
|
app(`Theme set to "${name}".`);
|
|
3165
3178
|
return true;
|
|
3166
3179
|
};
|
|
@@ -4262,7 +4275,7 @@ export async function startTUI(opts) {
|
|
|
4262
4275
|
}
|
|
4263
4276
|
// Async so the notice can be painted BEFORE the clipboard read: a cold
|
|
4264
4277
|
// PowerShell took ~3s, which read as "the shortcut does nothing".
|
|
4265
|
-
async function pasteFromClipboard() {
|
|
4278
|
+
async function pasteFromClipboard(fastPathOnly = false) {
|
|
4266
4279
|
notice('Pasting…', 'info');
|
|
4267
4280
|
const imgBase64 = readImage();
|
|
4268
4281
|
if (imgBase64) {
|
|
@@ -4272,10 +4285,26 @@ export async function startTUI(opts) {
|
|
|
4272
4285
|
renderFrame();
|
|
4273
4286
|
return;
|
|
4274
4287
|
}
|
|
4275
|
-
|
|
4288
|
+
|
|
4289
|
+
let text = '';
|
|
4290
|
+
let via = 'none';
|
|
4291
|
+
|
|
4292
|
+
// Try fast path first (oneshot)
|
|
4293
|
+
try {
|
|
4294
|
+
({ text, via } = { text: readOnce(), via: 'oneshot' });
|
|
4295
|
+
} catch {}
|
|
4296
|
+
|
|
4297
|
+
// If fast path failed and we need to try slow path
|
|
4298
|
+
if (!text && !fastPathOnly) {
|
|
4299
|
+
const result = await readText();
|
|
4300
|
+
text = result.text;
|
|
4301
|
+
via = result.via;
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4276
4304
|
if (!text) { notice('Clipboard empty or unavailable', 'error'); renderFrame(); return; }
|
|
4277
4305
|
insertComposerPaste(text);
|
|
4278
|
-
if (via === '
|
|
4306
|
+
if (via === 'helper') notice(`Pasted via helper (${via})`, 'info');
|
|
4307
|
+
else if (via === 'oneshot') notice('Pasted instantly!', 'info');
|
|
4279
4308
|
renderFrame();
|
|
4280
4309
|
}
|
|
4281
4310
|
// Ctrl-S: inject the queued messages (plus the current draft) into the RUNNING
|
|
@@ -5406,7 +5435,7 @@ export async function startTUI(opts) {
|
|
|
5406
5435
|
session.planPath = state.planPath || null;
|
|
5407
5436
|
session.focus = !!state.focus;
|
|
5408
5437
|
session.effort = state.effort || '';
|
|
5409
|
-
|
|
5438
|
+
// Theme removed - forced dark only
|
|
5410
5439
|
session.steps = state.steps || 0;
|
|
5411
5440
|
session.rounds = state.rounds || 0;
|
|
5412
5441
|
if (state.lastTurnMs) session.lastTurnMs = state.lastTurnMs;
|