@hncode/hncode 0.1.6 → 0.1.8
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 +136 -37
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
|
@@ -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(/^\//, '');
|
|
@@ -2565,7 +2564,7 @@ async function dispatch(cmdRaw, arg, state, cfg, session, h) {
|
|
|
2565
2564
|
{ label: 'statusline', sub: 'configure status line items' },
|
|
2566
2565
|
{ label: 'add-dir', sub: 'add an additional workspace directory' },
|
|
2567
2566
|
],
|
|
2568
|
-
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; },
|
|
2569
2568
|
});
|
|
2570
2569
|
return;
|
|
2571
2570
|
|
|
@@ -2756,18 +2755,60 @@ async function dispatch(cmdRaw, arg, state, cfg, session, h) {
|
|
|
2756
2755
|
return true;
|
|
2757
2756
|
}
|
|
2758
2757
|
openForm({
|
|
2759
|
-
title: `
|
|
2758
|
+
title: `Edit ${pit.label}`,
|
|
2760
2759
|
fields: [
|
|
2761
|
-
{ key: '
|
|
2760
|
+
{ key: 'name', label: 'Provider Name', value: pit.name },
|
|
2762
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') },
|
|
2763
2764
|
],
|
|
2764
2765
|
type: null,
|
|
2765
|
-
hint: 'Tab next field · Enter save · Esc cancel
|
|
2766
|
-
onSubmit: (values) => {
|
|
2767
|
-
|
|
2768
|
-
const
|
|
2769
|
-
|
|
2770
|
-
|
|
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
|
+
}
|
|
2771
2812
|
},
|
|
2772
2813
|
});
|
|
2773
2814
|
return true;
|
|
@@ -2815,10 +2856,21 @@ async function dispatch(cmdRaw, arg, state, cfg, session, h) {
|
|
|
2815
2856
|
}
|
|
2816
2857
|
|
|
2817
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
|
+
|
|
2818
2865
|
state.chat = [];
|
|
2819
2866
|
Object.assign(session, { id: sess.newId(), title: '', messages: [], createdAt: Date.now(), rounds: 0, steps: 0, lastTurnMs: 0 });
|
|
2820
2867
|
state.objective = '';
|
|
2821
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');
|
|
2822
2874
|
// A fresh session has no history, so the context gauge must go back to 0
|
|
2823
2875
|
// (otherwise it kept showing the PREVIOUS session's usage until the next
|
|
2824
2876
|
// turn reported a new estimate). The window size is a property of the
|
|
@@ -2916,8 +2968,17 @@ async function dispatch(cmdRaw, arg, state, cfg, session, h) {
|
|
|
2916
2968
|
return;
|
|
2917
2969
|
}
|
|
2918
2970
|
case 'title': {
|
|
2919
|
-
if (raw) {
|
|
2920
|
-
|
|
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
|
+
}
|
|
2921
2982
|
return;
|
|
2922
2983
|
}
|
|
2923
2984
|
case 'compact': {
|
|
@@ -3430,10 +3491,8 @@ async function dispatch(cmdRaw, arg, state, cfg, session, h) {
|
|
|
3430
3491
|
return;
|
|
3431
3492
|
}
|
|
3432
3493
|
}
|
|
3433
|
-
// Unknown command:
|
|
3434
|
-
|
|
3435
|
-
const textToSend = '/' + raw;
|
|
3436
|
-
submit(textToSend);
|
|
3494
|
+
// Unknown command: show error message
|
|
3495
|
+
appErr(`Unknown command: /${cmdRaw.replace(/^\//, '')}`);
|
|
3437
3496
|
return;
|
|
3438
3497
|
}
|
|
3439
3498
|
}
|
|
@@ -3717,7 +3776,8 @@ export async function startTUI(opts) {
|
|
|
3717
3776
|
|
|
3718
3777
|
const state = makeState({ cfg, session, opts });
|
|
3719
3778
|
cfg.effort = state.reasoning ? state.effort : '';
|
|
3720
|
-
|
|
3779
|
+
// Force dark theme (theme removed from state)
|
|
3780
|
+
setTheme('dark');
|
|
3721
3781
|
|
|
3722
3782
|
// Calculate initial context usage from loaded session
|
|
3723
3783
|
if (session && session.messages && session.messages.length) {
|
|
@@ -3999,7 +4059,7 @@ export async function startTUI(opts) {
|
|
|
3999
4059
|
if (cmd) {
|
|
4000
4060
|
state.menuOpen = false; state.menuList = []; state.menuSel = 0; state.menuOffset = 0;
|
|
4001
4061
|
state.input = ''; state.caret = 0;
|
|
4002
|
-
dispatch(cmd.name, '', state, cfg, session, host);
|
|
4062
|
+
dispatch(cmd.name, '', state, cfg, session, host, submit, stdout);
|
|
4003
4063
|
if (state._quit) { quit(); return true; }
|
|
4004
4064
|
renderFrame();
|
|
4005
4065
|
}
|
|
@@ -4020,6 +4080,16 @@ export async function startTUI(opts) {
|
|
|
4020
4080
|
renderFrame();
|
|
4021
4081
|
return true;
|
|
4022
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
|
+
}
|
|
4023
4093
|
case 'pickerItem': {
|
|
4024
4094
|
if (!state.picker) return false;
|
|
4025
4095
|
state.picker.sel = hb.index;
|
|
@@ -4285,25 +4355,29 @@ export async function startTUI(opts) {
|
|
|
4285
4355
|
return;
|
|
4286
4356
|
}
|
|
4287
4357
|
|
|
4288
|
-
|
|
4289
|
-
let via = 'none';
|
|
4290
|
-
|
|
4291
|
-
// Try fast path first (oneshot)
|
|
4358
|
+
// Always try fast path first (readOnce is instant)
|
|
4292
4359
|
try {
|
|
4293
|
-
|
|
4360
|
+
const text = readOnce();
|
|
4361
|
+
if (text) {
|
|
4362
|
+
insertComposerPaste(text);
|
|
4363
|
+
notice('Pasted instantly!', 'info');
|
|
4364
|
+
renderFrame();
|
|
4365
|
+
return;
|
|
4366
|
+
}
|
|
4294
4367
|
} catch {}
|
|
4295
4368
|
|
|
4296
|
-
//
|
|
4297
|
-
if (
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
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;
|
|
4301
4374
|
}
|
|
4302
4375
|
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
if (
|
|
4306
|
-
|
|
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');
|
|
4307
4381
|
renderFrame();
|
|
4308
4382
|
}
|
|
4309
4383
|
// Ctrl-S: inject the queued messages (plus the current draft) into the RUNNING
|
|
@@ -5114,6 +5188,7 @@ export async function startTUI(opts) {
|
|
|
5114
5188
|
state.historyIdx = -1;
|
|
5115
5189
|
if (!text) { renderFrame(); return; }
|
|
5116
5190
|
if (!state.running) state.turnStart = Date.now();
|
|
5191
|
+
|
|
5117
5192
|
if (state.history[state.history.length - 1] !== text) state.history.push(text);
|
|
5118
5193
|
// Commands are handled immediately, never queued: queuing them delayed a
|
|
5119
5194
|
// /plan or /model until the running turn finished, which is not what
|
|
@@ -5122,7 +5197,7 @@ export async function startTUI(opts) {
|
|
|
5122
5197
|
const sp = text.indexOf(' ');
|
|
5123
5198
|
const c = sp === -1 ? text : text.slice(0, sp);
|
|
5124
5199
|
const arg = sp === -1 ? '' : text.slice(sp + 1).trim();
|
|
5125
|
-
dispatch(c, arg, state, cfg, session, host);
|
|
5200
|
+
dispatch(c, arg, state, cfg, session, host, submit, stdout);
|
|
5126
5201
|
if (state._quit) { quit(); return; }
|
|
5127
5202
|
renderFrame();
|
|
5128
5203
|
return;
|
|
@@ -5157,6 +5232,12 @@ export async function startTUI(opts) {
|
|
|
5157
5232
|
// model stops narrating what it is about to do and why.
|
|
5158
5233
|
const basePrompt = (cfg.systemPrompt && String(cfg.systemPrompt).trim()) || SYSTEM_PROMPT;
|
|
5159
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
|
+
|
|
5160
5241
|
if (cfg.calmMode) {
|
|
5161
5242
|
sysText += '\n\n' + CALM_MODE_INSTRUCTION;
|
|
5162
5243
|
}
|
|
@@ -5473,6 +5554,24 @@ export async function startTUI(opts) {
|
|
|
5473
5554
|
} else {
|
|
5474
5555
|
state.chat.push({ role: 'assistant', text: text || '' });
|
|
5475
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
|
+
|
|
5476
5575
|
anchorScroll();
|
|
5477
5576
|
}
|
|
5478
5577
|
|