novaprime 1.6.0 → 1.6.2
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/bin/novaprime.js +3 -2
- package/package.json +1 -1
- package/src/prompt.js +60 -53
- package/src/ui.js +9 -1
package/bin/novaprime.js
CHANGED
|
@@ -88,9 +88,9 @@ async function repl() {
|
|
|
88
88
|
|
|
89
89
|
ui.hint(' Tip: press ' + c.indigo('Shift+Tab') + c.dim(' for auto-accept (no permission prompts), or answer ') + c.indigo('a') + c.dim(' at any prompt.'));
|
|
90
90
|
|
|
91
|
-
// push the first input box toward the lower part of the screen
|
|
91
|
+
// push the first input box toward the lower part of the screen (a bit lower than before)
|
|
92
92
|
const rows = process.stdout.rows || 24;
|
|
93
|
-
process.stdout.write('\n'.repeat(Math.max(0, rows -
|
|
93
|
+
process.stdout.write('\n'.repeat(Math.max(0, rows - 23)));
|
|
94
94
|
|
|
95
95
|
let messages = [];
|
|
96
96
|
while (true) {
|
|
@@ -105,6 +105,7 @@ async function repl() {
|
|
|
105
105
|
if (input === '/usage') { me = await fetchMe(config.getServer(), cfg.key); showUsage(me); continue; }
|
|
106
106
|
if (input === '/clear') { console.clear(); me = await fetchMe(config.getServer(), cfg.key); ui.banner(meToBanner(cfg, me)); continue; }
|
|
107
107
|
|
|
108
|
+
ui.userBubble(input); // show the sent message as a clean bubble (box already erased)
|
|
108
109
|
messages.push({ role: 'user', content: input });
|
|
109
110
|
try { await runTurn(config.getServer(), cfg.key, messages); }
|
|
110
111
|
catch (err) { ui.error(err.message); }
|
package/package.json
CHANGED
package/src/prompt.js
CHANGED
|
@@ -3,8 +3,8 @@ const readline = require('readline');
|
|
|
3
3
|
const { c } = require('./ui');
|
|
4
4
|
const mode = require('./mode');
|
|
5
5
|
|
|
6
|
-
// Use Node's readline for single-line prompts (login key, y/N)
|
|
7
|
-
//
|
|
6
|
+
// Use Node's readline for single-line prompts (login key, y/N). The multi-line
|
|
7
|
+
// chat box below is a custom raw-mode reader (paste-aware + Shift+Tab + full box).
|
|
8
8
|
let rl = null;
|
|
9
9
|
function getRl() {
|
|
10
10
|
if (!rl) {
|
|
@@ -28,16 +28,13 @@ function confirm(message) {
|
|
|
28
28
|
function close() { if (rl) { rl.close(); rl = null; } }
|
|
29
29
|
|
|
30
30
|
// ---- bracketed-paste-aware chat input ----------------------------------
|
|
31
|
-
// Terminals that support bracketed paste wrap pasted text in these markers.
|
|
32
|
-
// We capture everything between them (newlines included) WITHOUT submitting,
|
|
33
|
-
// so pasting a big multi-line prompt no longer auto-sends on the first line.
|
|
34
31
|
const PASTE_START = '\x1b[200~';
|
|
35
32
|
const PASTE_END = '\x1b[201~';
|
|
36
33
|
|
|
37
34
|
function normalize(t) { return t.replace(/\r\n?/g, '\n'); }
|
|
35
|
+
function visLen(s) { return String(s).replace(/\x1b\[[0-9;]*m/g, '').length; } // visible width, minus ANSI
|
|
38
36
|
|
|
39
|
-
//
|
|
40
|
-
// marker that got split across two stdin chunks)
|
|
37
|
+
// longest suffix of s that is a prefix of marker (marker split across chunks)
|
|
41
38
|
function partialTail(s, marker) {
|
|
42
39
|
const max = Math.min(s.length, marker.length - 1);
|
|
43
40
|
for (let n = max; n > 0; n--) {
|
|
@@ -46,37 +43,30 @@ function partialTail(s, marker) {
|
|
|
46
43
|
return 0;
|
|
47
44
|
}
|
|
48
45
|
|
|
49
|
-
//
|
|
50
|
-
//
|
|
46
|
+
// Plain (non-paste) bytes: typing, Enter, backspace, Ctrl+C/D, Shift+Tab, skip arrows.
|
|
47
|
+
// Only mutates state; the caller redraws the whole box afterward.
|
|
51
48
|
function handlePlain(state, s, sink) {
|
|
52
49
|
for (let i = 0; i < s.length && !state.finished; i++) {
|
|
53
50
|
const ch = s[i];
|
|
54
|
-
if (ch === '\x03') { sink.cancel(); return; }
|
|
51
|
+
if (ch === '\x03') { sink.cancel(); return; } // Ctrl+C
|
|
55
52
|
if (ch === '\x04') { if (!state.buf) { sink.cancel(); return; } continue; } // Ctrl+D on empty
|
|
56
|
-
if (ch === '\r' || ch === '\n') { sink.submit(state.buf); return; }
|
|
57
|
-
if (ch === '\x7f' || ch === '\b') {
|
|
58
|
-
if (state.buf.length) {
|
|
59
|
-
const last = state.buf[state.buf.length - 1];
|
|
60
|
-
state.buf = state.buf.slice(0, -1);
|
|
61
|
-
if (last !== '\n') sink.out('\b \b');
|
|
62
|
-
}
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
53
|
+
if (ch === '\r' || ch === '\n') { sink.submit(state.buf); return; } // Enter -> send
|
|
54
|
+
if (ch === '\x7f' || ch === '\b') { state.buf = state.buf.slice(0, -1); continue; } // backspace
|
|
65
55
|
if (ch === '\x1b') {
|
|
66
56
|
if (s.slice(i, i + 3) === '\x1b[Z') { if (sink.toggleMode) sink.toggleMode(); i += 2; continue; } // Shift+Tab
|
|
67
|
-
if (s[i + 1] === '[' || s[i + 1] === 'O') {
|
|
57
|
+
if (s[i + 1] === '[' || s[i + 1] === 'O') { // skip arrows / fn keys
|
|
68
58
|
i += 2;
|
|
69
59
|
while (i < s.length && !/[A-Za-z~]/.test(s[i])) i++;
|
|
70
60
|
}
|
|
71
61
|
continue;
|
|
72
62
|
}
|
|
73
|
-
if (ch === '\t') { state.buf += ' ';
|
|
74
|
-
if (ch >= ' ') { state.buf += ch;
|
|
63
|
+
if (ch === '\t') { state.buf += ' '; continue; } // tab -> 2 spaces
|
|
64
|
+
if (ch >= ' ') { state.buf += ch; } // printable
|
|
75
65
|
}
|
|
76
66
|
}
|
|
77
67
|
|
|
78
68
|
// Pure state machine — testable. state = { buf, pasting, pending, finished }.
|
|
79
|
-
// sink = {
|
|
69
|
+
// sink = { submit(value), cancel(), toggleMode() }.
|
|
80
70
|
function processChunk(state, chunk, sink) {
|
|
81
71
|
let s = state.pending + chunk;
|
|
82
72
|
state.pending = '';
|
|
@@ -85,13 +75,11 @@ function processChunk(state, chunk, sink) {
|
|
|
85
75
|
const end = s.indexOf(PASTE_END);
|
|
86
76
|
if (end === -1) {
|
|
87
77
|
const hold = partialTail(s, PASTE_END);
|
|
88
|
-
|
|
89
|
-
if (seg) { state.buf += seg; sink.out(seg); }
|
|
78
|
+
state.buf += normalize(s.slice(0, s.length - hold));
|
|
90
79
|
state.pending = s.slice(s.length - hold);
|
|
91
80
|
return;
|
|
92
81
|
}
|
|
93
|
-
|
|
94
|
-
if (seg) { state.buf += seg; sink.out(seg); }
|
|
82
|
+
state.buf += normalize(s.slice(0, end));
|
|
95
83
|
state.pasting = false;
|
|
96
84
|
s = s.slice(end + PASTE_END.length);
|
|
97
85
|
continue;
|
|
@@ -99,8 +87,7 @@ function processChunk(state, chunk, sink) {
|
|
|
99
87
|
const start = s.indexOf(PASTE_START);
|
|
100
88
|
if (start === -1) {
|
|
101
89
|
const hold = partialTail(s, PASTE_START);
|
|
102
|
-
|
|
103
|
-
handlePlain(state, plain, sink);
|
|
90
|
+
handlePlain(state, s.slice(0, s.length - hold), sink);
|
|
104
91
|
if (!state.finished) state.pending = s.slice(s.length - hold);
|
|
105
92
|
return;
|
|
106
93
|
}
|
|
@@ -111,52 +98,72 @@ function processChunk(state, chunk, sink) {
|
|
|
111
98
|
}
|
|
112
99
|
}
|
|
113
100
|
|
|
114
|
-
function ttyRaw() { return process.stdin.isTTY && typeof process.stdin.setRawMode === 'function'; }
|
|
101
|
+
function ttyRaw() { return process.stdout.isTTY && process.stdin.isTTY && typeof process.stdin.setRawMode === 'function'; }
|
|
115
102
|
|
|
116
|
-
// Chat input box
|
|
117
|
-
//
|
|
103
|
+
// Chat input box: a COMPLETE bordered box (top + input lines + bottom) that
|
|
104
|
+
// redraws on every change so the bottom stroke is always visible, multi-line
|
|
105
|
+
// pastes grow the box, and auto-accept status shows inside.
|
|
118
106
|
function boxInput(top, bottom, prompt) {
|
|
119
|
-
|
|
120
|
-
if (!ttyRaw()) {
|
|
107
|
+
if (!ttyRaw()) { // piped / non-TTY: fall back to readline
|
|
121
108
|
return new Promise((resolve) => {
|
|
122
109
|
process.stdout.write(top + '\n');
|
|
123
110
|
const r = getRl();
|
|
124
111
|
r.question(prompt, (answer) => { process.stdout.write(bottom + '\n'); resolve(answer); });
|
|
125
112
|
});
|
|
126
113
|
}
|
|
127
|
-
if (rl) { rl.close(); rl = null; }
|
|
114
|
+
if (rl) { rl.close(); rl = null; }
|
|
128
115
|
|
|
129
116
|
return new Promise((resolve) => {
|
|
130
117
|
const stdin = process.stdin;
|
|
131
118
|
const state = { buf: '', pasting: false, pending: '', finished: false };
|
|
119
|
+
const contPrefix = c.dim('│ ');
|
|
120
|
+
let firstRender = true;
|
|
121
|
+
let prevUp = 0; // lines from the input line up to the first dynamic line (for clearing)
|
|
122
|
+
|
|
123
|
+
function render() {
|
|
124
|
+
const statusLines = mode.isAuto()
|
|
125
|
+
? [c.green(' ⚡ auto-accept ON') + c.dim(' · Shift+Tab to turn off')]
|
|
126
|
+
: [];
|
|
127
|
+
const inputLines = state.buf.split('\n');
|
|
128
|
+
const dyn = statusLines
|
|
129
|
+
.concat(inputLines.map((ln, i) => (i === 0 ? prompt : contPrefix) + ln))
|
|
130
|
+
.concat([bottom]);
|
|
132
131
|
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
let out = '';
|
|
133
|
+
if (!firstRender) { if (prevUp > 0) out += '\x1b[' + prevUp + 'A'; out += '\r\x1b[0J'; }
|
|
134
|
+
out += dyn.join('\n');
|
|
135
|
+
out += '\x1b[1A\r'; // from end of bottom up to the last input line, col 0
|
|
136
|
+
const prefVis = inputLines.length === 1 ? visLen(prompt) : visLen(contPrefix);
|
|
137
|
+
const col = prefVis + inputLines[inputLines.length - 1].length;
|
|
138
|
+
if (col > 0) out += '\x1b[' + col + 'C';
|
|
139
|
+
process.stdout.write(out);
|
|
140
|
+
prevUp = statusLines.length + (inputLines.length - 1);
|
|
141
|
+
firstRender = false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function cleanup() {
|
|
135
145
|
try { process.stdout.write('\x1b[?2004l'); } catch (_) {}
|
|
136
146
|
try { stdin.setRawMode(false); } catch (_) {}
|
|
137
147
|
stdin.pause();
|
|
138
148
|
stdin.removeListener('data', onData);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
process.stdout.write(prompt + state.buf); // redraw input line with whatever was typed
|
|
151
|
-
},
|
|
152
|
-
};
|
|
149
|
+
}
|
|
150
|
+
function finish(val) {
|
|
151
|
+
state.finished = true;
|
|
152
|
+
// erase the whole box (top border + dynamic region) — the REPL replaces it
|
|
153
|
+
// with a clean message bubble. prevUp+1 lines up = the top border line.
|
|
154
|
+
process.stdout.write('\x1b[' + (prevUp + 1) + 'A\r\x1b[0J');
|
|
155
|
+
cleanup();
|
|
156
|
+
resolve(val);
|
|
157
|
+
}
|
|
158
|
+
const sink = { submit: (v) => finish(v), cancel: () => finish(null), toggleMode: () => mode.toggle() };
|
|
159
|
+
const onData = (chunk) => { processChunk(state, chunk, sink); if (!state.finished) render(); };
|
|
153
160
|
|
|
154
161
|
process.stdout.write(top + '\n');
|
|
155
|
-
|
|
162
|
+
render();
|
|
156
163
|
stdin.setRawMode(true);
|
|
157
164
|
stdin.resume();
|
|
158
165
|
stdin.setEncoding('utf8');
|
|
159
|
-
process.stdout.write('\x1b[?2004h'); //
|
|
166
|
+
process.stdout.write('\x1b[?2004h'); // bracketed paste on
|
|
160
167
|
stdin.on('data', onData);
|
|
161
168
|
});
|
|
162
169
|
}
|
package/src/ui.js
CHANGED
|
@@ -100,6 +100,14 @@ function inputBoxClose() { process.stdout.write('\n'); }
|
|
|
100
100
|
|
|
101
101
|
function aiLabel(model) { process.stdout.write('\n' + c.violet('● ') + c.violet.bold('Nova Prime') + (model ? c.dim(' · ' + model) : '') + '\n'); }
|
|
102
102
|
|
|
103
|
+
// User's sent message rendered as a clean chat bubble (background color, no input border).
|
|
104
|
+
function userBubble(text) {
|
|
105
|
+
const bub = chalk.bgHex('#2a3566').hex('#eef2ff');
|
|
106
|
+
const lines = String(text).split('\n');
|
|
107
|
+
process.stdout.write('\n ' + c.indigo.bold('You') + '\n');
|
|
108
|
+
for (const line of lines) console.log(' ' + bub(' ' + line + ' '));
|
|
109
|
+
}
|
|
110
|
+
|
|
103
111
|
// Live "model switch" report — shows what the free Flash brain decided for this request.
|
|
104
112
|
function routeReport(r) {
|
|
105
113
|
if (!r) return;
|
|
@@ -126,4 +134,4 @@ function error(msg) { console.log(c.red(' ✕ ') + c.red(msg)); }
|
|
|
126
134
|
function ok(msg) { console.log(c.green(' ✓ ') + c.body(msg)); }
|
|
127
135
|
function tool(name, detail) { console.log(c.dim(' · ' + name + (detail ? ' ' + detail : ''))); }
|
|
128
136
|
|
|
129
|
-
module.exports = { chalk, boxen, c, banner, maskKey, aiLabel, routeReport, inputTop, inputBottom, inputPrompt, inputBoxOpen, inputBoxClose, info, hint, warn, error, ok, tool };
|
|
137
|
+
module.exports = { chalk, boxen, c, banner, maskKey, aiLabel, userBubble, routeReport, inputTop, inputBottom, inputPrompt, inputBoxOpen, inputBoxClose, info, hint, warn, error, ok, tool };
|