@hncode/hncode 0.1.5 → 0.1.7
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/package.json +1 -1
- package/src/colors.js +2 -2
- package/src/tui.js +137 -39
package/package.json
CHANGED
package/src/colors.js
CHANGED
|
@@ -39,8 +39,8 @@ const THEMES = {
|
|
|
39
39
|
hover: [190, 245, 255], selBg: [0, 70, 84]
|
|
40
40
|
},
|
|
41
41
|
dark: {
|
|
42
|
-
fg: [
|
|
43
|
-
teal: [0,
|
|
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
44
|
hover: [190, 245, 255], selBg: [0, 70, 84]
|
|
45
45
|
},
|
|
46
46
|
light: {
|
package/src/tui.js
CHANGED
|
@@ -38,7 +38,7 @@ const VERSION = '0.1.0';
|
|
|
38
38
|
export const COMMANDS = [
|
|
39
39
|
{ name: 'yolo', aliases: ['yes'], desc: 'Ask When Needed mode: anything inside the workspace (edits, writes, commands) runs automatically; paths outside it, destructive commands, questions and plans still ask.', priority: 101 },
|
|
40
40
|
{ name: 'permission', desc: 'Select permission mode', priority: 100 },
|
|
41
|
-
{ name: 'settings', aliases: ['config'], desc: 'Open settings (model / permission /
|
|
41
|
+
{ name: 'settings', aliases: ['config'], desc: 'Open settings (model / permission / statusline)', priority: 100 },
|
|
42
42
|
{ name: 'plan', desc: 'Toggle plan mode', priority: 100, argumentHint: '[on|off|clear]' },
|
|
43
43
|
{ name: 'focus', desc: 'Toggle Focus mode (minimal tools first, full tools after)', priority: 98, argumentHint: '[on|off]' },
|
|
44
44
|
{ name: 'auto', desc: 'Never Ask mode: never interrupts you; everything runs and is decided automatically.', priority: 99 },
|
|
@@ -55,7 +55,7 @@ export const COMMANDS = [
|
|
|
55
55
|
{ name: 'init', desc: 'Analyze the codebase and generate AGENTS.md', priority: 70 },
|
|
56
56
|
{ name: 'fork', desc: 'Fork the current session into a copy without switching to it', priority: 80 },
|
|
57
57
|
{ name: 'undo', desc: 'Withdraw the last prompt from the transcript', priority: 80, argumentHint: '[count]' },
|
|
58
|
-
{ name: 'title', aliases: ['rename'], desc: 'Set or show session title', priority: 60, argumentHint: '<title>' },
|
|
58
|
+
{ name: 'title', aliases: ['rename'], desc: 'Set or show session title (also sets window title)', priority: 60, argumentHint: '<title>' },
|
|
59
59
|
{ name: 'status', desc: 'Show current session and runtime status', priority: 60 },
|
|
60
60
|
{ name: 'usage', desc: 'Show session tokens + context window', priority: 60 },
|
|
61
61
|
{ name: 'mcp', desc: 'Show MCP server status', priority: 60 },
|
|
@@ -1278,8 +1278,8 @@ function markdownLineToRows(line, width, fg, codeFg, isContinuation) {
|
|
|
1278
1278
|
for (const w of wrapWords(t, width)) out.push(col(inlineMarkdown(w, codeFg), fg));
|
|
1279
1279
|
return out;
|
|
1280
1280
|
}
|
|
1281
|
-
// Plain
|
|
1282
|
-
for (const w of wrapWords(t, width)) out.push(w);
|
|
1281
|
+
// Plain - apply default foreground color
|
|
1282
|
+
for (const w of wrapWords(t, width)) out.push(col(w, fg));
|
|
1283
1283
|
return out;
|
|
1284
1284
|
}
|
|
1285
1285
|
|
|
@@ -1714,7 +1714,6 @@ export function composeFrame(state, cols, rows) {
|
|
|
1714
1714
|
const hint = ed.hint || 'Ctrl+S save · Esc cancel · Enter newline';
|
|
1715
1715
|
lines.push(col(hint, C.gray));
|
|
1716
1716
|
lines.push('');
|
|
1717
|
-
const viewH = Math.max(1, bodyH - (ed.notice ? 6 : 5));
|
|
1718
1717
|
// Keep the caret row in view.
|
|
1719
1718
|
let top = Math.max(0, ed.top || 0);
|
|
1720
1719
|
if (ed.caretRow < top) top = ed.caretRow;
|
|
@@ -2498,7 +2497,7 @@ function ensureMenuVisible(state) {
|
|
|
2498
2497
|
}
|
|
2499
2498
|
|
|
2500
2499
|
// ---- command dispatch ----
|
|
2501
|
-
async function dispatch(cmdRaw, arg, state, cfg, session, h) {
|
|
2500
|
+
async function dispatch(cmdRaw, arg, state, cfg, session, h, submit, stdout, renderFrame) {
|
|
2502
2501
|
const { addChat, openPicker, openForm, notice, sendPrompt, quit, saveSession, openEditor } = h;
|
|
2503
2502
|
const entry = findCommand(cmdRaw);
|
|
2504
2503
|
const cmd = entry ? entry.name : String(cmdRaw || '').replace(/^\//, '');
|
|
@@ -2562,11 +2561,10 @@ async function dispatch(cmdRaw, arg, state, cfg, session, h) {
|
|
|
2562
2561
|
{ label: 'effort', sub: 'switch thinking effort' },
|
|
2563
2562
|
{ label: 'permission', sub: 'select permission mode' },
|
|
2564
2563
|
{ label: 'provider', sub: 'manage AI providers' },
|
|
2565
|
-
{ label: 'theme', sub: 'set the terminal UI theme' },
|
|
2566
2564
|
{ label: 'statusline', sub: 'configure status line items' },
|
|
2567
2565
|
{ label: 'add-dir', sub: 'add an additional workspace directory' },
|
|
2568
2566
|
],
|
|
2569
|
-
onPick: (it) => { dispatch(it.label, '', state, cfg, session, h); return true; },
|
|
2567
|
+
onPick: (it) => { dispatch(it.label, '', state, cfg, session, h, submit, stdout); return true; },
|
|
2570
2568
|
});
|
|
2571
2569
|
return;
|
|
2572
2570
|
|
|
@@ -2757,18 +2755,60 @@ async function dispatch(cmdRaw, arg, state, cfg, session, h) {
|
|
|
2757
2755
|
return true;
|
|
2758
2756
|
}
|
|
2759
2757
|
openForm({
|
|
2760
|
-
title: `
|
|
2758
|
+
title: `Edit ${pit.label}`,
|
|
2761
2759
|
fields: [
|
|
2762
|
-
{ key: '
|
|
2760
|
+
{ key: 'name', label: 'Provider Name', value: pit.name },
|
|
2763
2761
|
{ key: 'base_url', label: 'Base URL', value: pit.base_url },
|
|
2762
|
+
{ key: 'api_key', label: 'API Key', kind: 'mask', value: pit.api_key || '' },
|
|
2763
|
+
{ key: 'type', label: 'Type', options: ['OpenAI', 'Anthropic'], value: pit.type || (pit.protocol === 'anthropic' ? 'Anthropic' : 'OpenAI') },
|
|
2764
2764
|
],
|
|
2765
2765
|
type: null,
|
|
2766
|
-
hint: 'Tab next field · Enter save · Esc cancel
|
|
2767
|
-
onSubmit: (values) => {
|
|
2768
|
-
|
|
2769
|
-
const
|
|
2770
|
-
|
|
2771
|
-
|
|
2766
|
+
hint: 'Tab next field · Enter save · Esc cancel',
|
|
2767
|
+
onSubmit: async (values) => {
|
|
2768
|
+
if (!values.name) { appErr('Cancelled: provider name is required.'); return; }
|
|
2769
|
+
const protocol = values.type === 'Anthropic' ? 'anthropic' : 'openai';
|
|
2770
|
+
registerProvider(state, cfg, h, values.name, values.base_url, values.api_key, protocol, values.type);
|
|
2771
|
+
|
|
2772
|
+
// Auto-fetch models and configure context/thinking settings
|
|
2773
|
+
try {
|
|
2774
|
+
const models = await fetchModels({
|
|
2775
|
+
baseUrl: values.base_url,
|
|
2776
|
+
apiKey: values.api_key,
|
|
2777
|
+
protocol
|
|
2778
|
+
});
|
|
2779
|
+
|
|
2780
|
+
if (models.length > 0) {
|
|
2781
|
+
// Add each model with its configuration
|
|
2782
|
+
for (const m of models) {
|
|
2783
|
+
const key = modelKey(values.name, m.id);
|
|
2784
|
+
if (!cfg.raw.models[key]) {
|
|
2785
|
+
addModel(values.name, m.id, {
|
|
2786
|
+
display_name: m.display,
|
|
2787
|
+
contextLength: m.contextLength,
|
|
2788
|
+
maxTokens: m.maxTokens,
|
|
2789
|
+
});
|
|
2790
|
+
|
|
2791
|
+
// Configure model entry in raw config
|
|
2792
|
+
if (!cfg.raw.models) cfg.raw.models = {};
|
|
2793
|
+
cfg.raw.models[key] = {
|
|
2794
|
+
provider: values.name,
|
|
2795
|
+
model: m.id,
|
|
2796
|
+
display_name: m.display,
|
|
2797
|
+
context_length: m.contextLength,
|
|
2798
|
+
max_tokens: m.maxTokens,
|
|
2799
|
+
reasoning: m.reasoning,
|
|
2800
|
+
efforts: m.efforts,
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
app(`Added ${models.length} model(s) with auto-configured context windows and thinking capabilities.`);
|
|
2806
|
+
} else {
|
|
2807
|
+
app(`Provider added. No models found at this endpoint.`);
|
|
2808
|
+
}
|
|
2809
|
+
} catch (error) {
|
|
2810
|
+
app(`Provider added. Failed to fetch models: ${error.message}`);
|
|
2811
|
+
}
|
|
2772
2812
|
},
|
|
2773
2813
|
});
|
|
2774
2814
|
return true;
|
|
@@ -2816,10 +2856,21 @@ async function dispatch(cmdRaw, arg, state, cfg, session, h) {
|
|
|
2816
2856
|
}
|
|
2817
2857
|
|
|
2818
2858
|
case 'new':
|
|
2859
|
+
// Save current session's todos before switching
|
|
2860
|
+
if (session && Array.isArray(state.todos)) {
|
|
2861
|
+
session.todos = state.todos.slice();
|
|
2862
|
+
saveSession(session);
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2819
2865
|
state.chat = [];
|
|
2820
2866
|
Object.assign(session, { id: sess.newId(), title: '', messages: [], createdAt: Date.now(), rounds: 0, steps: 0, lastTurnMs: 0 });
|
|
2821
2867
|
state.objective = '';
|
|
2822
2868
|
state.rounds = 0; state.steps = 0; state.lastTurnMs = 0;
|
|
2869
|
+
// New session starts with empty TODO list
|
|
2870
|
+
state.todos = [];
|
|
2871
|
+
session.todos = [];
|
|
2872
|
+
// Set terminal window title to default (Untitled)
|
|
2873
|
+
stdout.write('\x1b]0;Untitled\x07');
|
|
2823
2874
|
// A fresh session has no history, so the context gauge must go back to 0
|
|
2824
2875
|
// (otherwise it kept showing the PREVIOUS session's usage until the next
|
|
2825
2876
|
// turn reported a new estimate). The window size is a property of the
|
|
@@ -2917,8 +2968,17 @@ async function dispatch(cmdRaw, arg, state, cfg, session, h) {
|
|
|
2917
2968
|
return;
|
|
2918
2969
|
}
|
|
2919
2970
|
case 'title': {
|
|
2920
|
-
if (raw) {
|
|
2921
|
-
|
|
2971
|
+
if (raw) {
|
|
2972
|
+
session.title = raw.slice(0, 200);
|
|
2973
|
+
saveSession(session);
|
|
2974
|
+
// Set terminal window title - use OSC sequence
|
|
2975
|
+
// Only show "Untitled" prefix if user hasn't set a custom title
|
|
2976
|
+
const displayTitle = session.title || 'Untitled';
|
|
2977
|
+
stdout.write(`\x1b]0;${displayTitle}\x07`);
|
|
2978
|
+
app(`Session title set to: "${displayTitle}"`);
|
|
2979
|
+
} else {
|
|
2980
|
+
app(`Session title: ${session.title || 'not set'}`);
|
|
2981
|
+
}
|
|
2922
2982
|
return;
|
|
2923
2983
|
}
|
|
2924
2984
|
case 'compact': {
|
|
@@ -3431,10 +3491,8 @@ async function dispatch(cmdRaw, arg, state, cfg, session, h) {
|
|
|
3431
3491
|
return;
|
|
3432
3492
|
}
|
|
3433
3493
|
}
|
|
3434
|
-
// Unknown command:
|
|
3435
|
-
|
|
3436
|
-
const textToSend = '/' + raw;
|
|
3437
|
-
submit(textToSend);
|
|
3494
|
+
// Unknown command: show error message
|
|
3495
|
+
appErr(`Unknown command: /${cmdRaw.replace(/^\//, '')}`);
|
|
3438
3496
|
return;
|
|
3439
3497
|
}
|
|
3440
3498
|
}
|
|
@@ -3718,7 +3776,8 @@ export async function startTUI(opts) {
|
|
|
3718
3776
|
|
|
3719
3777
|
const state = makeState({ cfg, session, opts });
|
|
3720
3778
|
cfg.effort = state.reasoning ? state.effort : '';
|
|
3721
|
-
|
|
3779
|
+
// Force dark theme (theme removed from state)
|
|
3780
|
+
setTheme('dark');
|
|
3722
3781
|
|
|
3723
3782
|
// Calculate initial context usage from loaded session
|
|
3724
3783
|
if (session && session.messages && session.messages.length) {
|
|
@@ -4000,7 +4059,7 @@ export async function startTUI(opts) {
|
|
|
4000
4059
|
if (cmd) {
|
|
4001
4060
|
state.menuOpen = false; state.menuList = []; state.menuSel = 0; state.menuOffset = 0;
|
|
4002
4061
|
state.input = ''; state.caret = 0;
|
|
4003
|
-
dispatch(cmd.name, '', state, cfg, session, host);
|
|
4062
|
+
dispatch(cmd.name, '', state, cfg, session, host, submit, stdout);
|
|
4004
4063
|
if (state._quit) { quit(); return true; }
|
|
4005
4064
|
renderFrame();
|
|
4006
4065
|
}
|
|
@@ -4021,6 +4080,16 @@ export async function startTUI(opts) {
|
|
|
4021
4080
|
renderFrame();
|
|
4022
4081
|
return true;
|
|
4023
4082
|
}
|
|
4083
|
+
case 'editor': {
|
|
4084
|
+
if (!state.editor) return false;
|
|
4085
|
+
const ed = state.editor;
|
|
4086
|
+
const ls = ed.text.split('\n');
|
|
4087
|
+
// Set caret to clicked position
|
|
4088
|
+
ed.caretRow = hb.row;
|
|
4089
|
+
ed.caretCol = Math.min((ls[ed.caretRow] || '').length, 1000); // Cap at reasonable length
|
|
4090
|
+
renderFrame();
|
|
4091
|
+
return true;
|
|
4092
|
+
}
|
|
4024
4093
|
case 'pickerItem': {
|
|
4025
4094
|
if (!state.picker) return false;
|
|
4026
4095
|
state.picker.sel = hb.index;
|
|
@@ -4286,25 +4355,29 @@ export async function startTUI(opts) {
|
|
|
4286
4355
|
return;
|
|
4287
4356
|
}
|
|
4288
4357
|
|
|
4289
|
-
|
|
4290
|
-
let via = 'none';
|
|
4291
|
-
|
|
4292
|
-
// Try fast path first (oneshot)
|
|
4358
|
+
// Always try fast path first (readOnce is instant)
|
|
4293
4359
|
try {
|
|
4294
|
-
|
|
4360
|
+
const text = readOnce();
|
|
4361
|
+
if (text) {
|
|
4362
|
+
insertComposerPaste(text);
|
|
4363
|
+
notice('Pasted instantly!', 'info');
|
|
4364
|
+
renderFrame();
|
|
4365
|
+
return;
|
|
4366
|
+
}
|
|
4295
4367
|
} catch {}
|
|
4296
4368
|
|
|
4297
|
-
//
|
|
4298
|
-
if (
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4369
|
+
// Fast path failed - only use slow path for Ctrl+Shift+V, not for right-click
|
|
4370
|
+
if (fastPathOnly) {
|
|
4371
|
+
notice('Clipboard empty or unavailable (fast path only)', 'error');
|
|
4372
|
+
renderFrame();
|
|
4373
|
+
return;
|
|
4302
4374
|
}
|
|
4303
4375
|
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
if (
|
|
4307
|
-
|
|
4376
|
+
// Slow path for Ctrl+Shift+V
|
|
4377
|
+
const result = await readText();
|
|
4378
|
+
if (!result.text) { notice('Clipboard empty or unavailable', 'error'); renderFrame(); return; }
|
|
4379
|
+
insertComposerPaste(result.text);
|
|
4380
|
+
notice(`Pasted via ${result.via}`, 'info');
|
|
4308
4381
|
renderFrame();
|
|
4309
4382
|
}
|
|
4310
4383
|
// Ctrl-S: inject the queued messages (plus the current draft) into the RUNNING
|
|
@@ -5115,6 +5188,7 @@ export async function startTUI(opts) {
|
|
|
5115
5188
|
state.historyIdx = -1;
|
|
5116
5189
|
if (!text) { renderFrame(); return; }
|
|
5117
5190
|
if (!state.running) state.turnStart = Date.now();
|
|
5191
|
+
|
|
5118
5192
|
if (state.history[state.history.length - 1] !== text) state.history.push(text);
|
|
5119
5193
|
// Commands are handled immediately, never queued: queuing them delayed a
|
|
5120
5194
|
// /plan or /model until the running turn finished, which is not what
|
|
@@ -5123,7 +5197,7 @@ export async function startTUI(opts) {
|
|
|
5123
5197
|
const sp = text.indexOf(' ');
|
|
5124
5198
|
const c = sp === -1 ? text : text.slice(0, sp);
|
|
5125
5199
|
const arg = sp === -1 ? '' : text.slice(sp + 1).trim();
|
|
5126
|
-
dispatch(c, arg, state, cfg, session, host);
|
|
5200
|
+
dispatch(c, arg, state, cfg, session, host, submit, stdout);
|
|
5127
5201
|
if (state._quit) { quit(); return; }
|
|
5128
5202
|
renderFrame();
|
|
5129
5203
|
return;
|
|
@@ -5158,6 +5232,12 @@ export async function startTUI(opts) {
|
|
|
5158
5232
|
// model stops narrating what it is about to do and why.
|
|
5159
5233
|
const basePrompt = (cfg.systemPrompt && String(cfg.systemPrompt).trim()) || SYSTEM_PROMPT;
|
|
5160
5234
|
let sysText = basePrompt;
|
|
5235
|
+
|
|
5236
|
+
// Auto-generate title on first turn (if not already set)
|
|
5237
|
+
if (!session.title && session.messages && session.messages.length === 0) {
|
|
5238
|
+
sysText += '\n\n[IMPORTANT: Please generate a concise, descriptive title for this conversation based on the user\'s request. Return ONLY the title text, nothing else. Example: Fix color rendering issue or Implement plugin system]';
|
|
5239
|
+
}
|
|
5240
|
+
|
|
5161
5241
|
if (cfg.calmMode) {
|
|
5162
5242
|
sysText += '\n\n' + CALM_MODE_INSTRUCTION;
|
|
5163
5243
|
}
|
|
@@ -5474,6 +5554,24 @@ export async function startTUI(opts) {
|
|
|
5474
5554
|
} else {
|
|
5475
5555
|
state.chat.push({ role: 'assistant', text: text || '' });
|
|
5476
5556
|
}
|
|
5557
|
+
|
|
5558
|
+
// Check if this is the first assistant response and we need to parse title
|
|
5559
|
+
if (!state.running && !session.title && state.chat.some(m => m.role === 'assistant')) {
|
|
5560
|
+
// Look for title in the assistant's response
|
|
5561
|
+
const assistantMsg = state.chat.find(m => m.role === 'assistant');
|
|
5562
|
+
if (assistantMsg && assistantMsg.text) {
|
|
5563
|
+
const text = assistantMsg.text.trim();
|
|
5564
|
+
// More lenient heuristic: short text (< 80 chars), no newlines, optional ending punctuation
|
|
5565
|
+
const cleanText = text.replace(/[.!?]$/, '').trim();
|
|
5566
|
+
if (cleanText.length < 80 && !text.includes('\n') && cleanText.length > 2) {
|
|
5567
|
+
session.title = cleanText;
|
|
5568
|
+
saveSession(session);
|
|
5569
|
+
stdout.write(`\x1b]0;${cleanText}\x07`);
|
|
5570
|
+
app(`Auto-generated title: "${cleanText}"`);
|
|
5571
|
+
}
|
|
5572
|
+
}
|
|
5573
|
+
}
|
|
5574
|
+
|
|
5477
5575
|
anchorScroll();
|
|
5478
5576
|
}
|
|
5479
5577
|
|