novaprime 1.6.1 → 1.7.0
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 +7 -9
- package/package.json +1 -1
- package/src/prompt.js +61 -1
- package/src/screen.js +39 -0
- package/src/ui.js +9 -1
package/bin/novaprime.js
CHANGED
|
@@ -6,6 +6,7 @@ const { c } = ui;
|
|
|
6
6
|
const { ask, close, boxInput } = require('../src/prompt');
|
|
7
7
|
const { runTurn, fetchMe } = require('../src/agent');
|
|
8
8
|
const tools = require('../src/tools');
|
|
9
|
+
const screen = require('../src/screen');
|
|
9
10
|
const pkg = require('../package.json');
|
|
10
11
|
|
|
11
12
|
// Validate the key against the server before saving — a wrong key never logs in.
|
|
@@ -81,16 +82,12 @@ function showUsage(me) {
|
|
|
81
82
|
async function repl() {
|
|
82
83
|
const { cfg } = await ensureKey();
|
|
83
84
|
let me = await fetchMe(config.getServer(), cfg.key);
|
|
84
|
-
|
|
85
|
+
screen.enable(); // pinned-bottom chat layout: input stays at the bottom, chat scrolls above
|
|
85
86
|
ui.banner(meToBanner(cfg, me));
|
|
86
|
-
process.on('exit', () => { try { tools.stopBackground(); } catch (_) {} }); // stop bg dev servers on quit
|
|
87
|
-
process.on('SIGINT', () => { try { tools.stopBackground(); } catch (_) {} console.log(c.muted('\n bye')); process.exit(0); });
|
|
88
|
-
|
|
89
87
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
process.stdout.write('\n'.repeat(Math.max(0, rows - 26)));
|
|
88
|
+
const quit = () => { try { tools.stopBackground(); } catch (_) {} try { screen.disable(); } catch (_) {} };
|
|
89
|
+
process.on('exit', quit);
|
|
90
|
+
process.on('SIGINT', () => { quit(); console.log(c.muted('\n bye')); process.exit(0); });
|
|
94
91
|
|
|
95
92
|
let messages = [];
|
|
96
93
|
while (true) {
|
|
@@ -103,8 +100,9 @@ async function repl() {
|
|
|
103
100
|
if (input === '/help') { printHelp(); continue; }
|
|
104
101
|
if (input === '/key') { console.log('\n ' + c.muted('key ') + c.green(ui.maskKey(cfg.key)) + '\n'); continue; }
|
|
105
102
|
if (input === '/usage') { me = await fetchMe(config.getServer(), cfg.key); showUsage(me); continue; }
|
|
106
|
-
if (input === '/clear') {
|
|
103
|
+
if (input === '/clear') { screen.enable(); me = await fetchMe(config.getServer(), cfg.key); ui.banner(meToBanner(cfg, me)); continue; }
|
|
107
104
|
|
|
105
|
+
ui.userBubble(input); // show the sent message as a clean bubble (box already erased)
|
|
108
106
|
messages.push({ role: 'user', content: input });
|
|
109
107
|
try { await runTurn(config.getServer(), cfg.key, messages); }
|
|
110
108
|
catch (err) { ui.error(err.message); }
|
package/package.json
CHANGED
package/src/prompt.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
const readline = require('readline');
|
|
3
3
|
const { c } = require('./ui');
|
|
4
4
|
const mode = require('./mode');
|
|
5
|
+
const screen = require('./screen');
|
|
5
6
|
|
|
6
7
|
// Use Node's readline for single-line prompts (login key, y/N). The multi-line
|
|
7
8
|
// chat box below is a custom raw-mode reader (paste-aware + Shift+Tab + full box).
|
|
@@ -111,6 +112,7 @@ function boxInput(top, bottom, prompt) {
|
|
|
111
112
|
r.question(prompt, (answer) => { process.stdout.write(bottom + '\n'); resolve(answer); });
|
|
112
113
|
});
|
|
113
114
|
}
|
|
115
|
+
if (screen.isOn()) return boxInputPinned(top, bottom, prompt); // pinned-bottom chat layout
|
|
114
116
|
if (rl) { rl.close(); rl = null; }
|
|
115
117
|
|
|
116
118
|
return new Promise((resolve) => {
|
|
@@ -149,7 +151,9 @@ function boxInput(top, bottom, prompt) {
|
|
|
149
151
|
}
|
|
150
152
|
function finish(val) {
|
|
151
153
|
state.finished = true;
|
|
152
|
-
|
|
154
|
+
// erase the whole box (top border + dynamic region) — the REPL replaces it
|
|
155
|
+
// with a clean message bubble. prevUp+1 lines up = the top border line.
|
|
156
|
+
process.stdout.write('\x1b[' + (prevUp + 1) + 'A\r\x1b[0J');
|
|
153
157
|
cleanup();
|
|
154
158
|
resolve(val);
|
|
155
159
|
}
|
|
@@ -166,4 +170,60 @@ function boxInput(top, bottom, prompt) {
|
|
|
166
170
|
});
|
|
167
171
|
}
|
|
168
172
|
|
|
173
|
+
// Pinned-bottom version: the box is drawn in the bottom rows (growing upward for
|
|
174
|
+
// multi-line), the conversation scrolls in the region above. On submit the box is
|
|
175
|
+
// reset to empty and the cursor parks in the region so the bubble/response flow above.
|
|
176
|
+
function boxInputPinned(top, bottom, prompt) {
|
|
177
|
+
if (rl) { rl.close(); rl = null; }
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
const stdin = process.stdin;
|
|
180
|
+
const state = { buf: '', pasting: false, pending: '', finished: false };
|
|
181
|
+
const contPrefix = c.dim('│ ');
|
|
182
|
+
const statusLine = () => (mode.isAuto() ? [c.green(' ⚡ auto-accept ON') + c.dim(' · Shift+Tab to turn off')] : []);
|
|
183
|
+
|
|
184
|
+
function draw(inputLines) {
|
|
185
|
+
const status = statusLine();
|
|
186
|
+
const lines = [top, ...status, ...inputLines.map((ln, i) => (i === 0 ? prompt : contPrefix) + ln), bottom];
|
|
187
|
+
const boxH = lines.length;
|
|
188
|
+
const { rows } = screen.dims();
|
|
189
|
+
screen.setRegion(boxH);
|
|
190
|
+
const startRow = rows - boxH + 1;
|
|
191
|
+
let out = '';
|
|
192
|
+
for (let i = 0; i < lines.length; i++) out += '\x1b[' + (startRow + i) + ';1H\x1b[2K' + lines[i];
|
|
193
|
+
return { out, startRow, status, boxH };
|
|
194
|
+
}
|
|
195
|
+
function render() {
|
|
196
|
+
const inputLines = state.buf.split('\n');
|
|
197
|
+
const { out, startRow, status } = draw(inputLines);
|
|
198
|
+
const lastIdx = 1 + status.length + (inputLines.length - 1);
|
|
199
|
+
const prefVis = inputLines.length === 1 ? visLen(prompt) : visLen(contPrefix);
|
|
200
|
+
const col = prefVis + inputLines[inputLines.length - 1].length + 1;
|
|
201
|
+
process.stdout.write(out + '\x1b[' + (startRow + lastIdx) + ';' + col + 'H');
|
|
202
|
+
}
|
|
203
|
+
function cleanup() {
|
|
204
|
+
try { process.stdout.write('\x1b[?2004l'); } catch (_) {}
|
|
205
|
+
try { stdin.setRawMode(false); } catch (_) {}
|
|
206
|
+
stdin.pause();
|
|
207
|
+
stdin.removeListener('data', onData);
|
|
208
|
+
}
|
|
209
|
+
function finish(val) {
|
|
210
|
+
state.finished = true;
|
|
211
|
+
cleanup();
|
|
212
|
+
const { out, boxH } = draw(['']); // redraw an empty box (clears the typed text)
|
|
213
|
+
const { rows } = screen.dims();
|
|
214
|
+
process.stdout.write(out + '\x1b[' + (rows - boxH) + ';1H\n'); // park cursor in the region
|
|
215
|
+
resolve(val);
|
|
216
|
+
}
|
|
217
|
+
const sink = { submit: (v) => finish(v), cancel: () => finish(null), toggleMode: () => mode.toggle() };
|
|
218
|
+
const onData = (chunk) => { processChunk(state, chunk, sink); if (!state.finished) render(); };
|
|
219
|
+
|
|
220
|
+
process.stdout.write('\x1b[?2004h');
|
|
221
|
+
render();
|
|
222
|
+
stdin.setRawMode(true);
|
|
223
|
+
stdin.resume();
|
|
224
|
+
stdin.setEncoding('utf8');
|
|
225
|
+
stdin.on('data', onData);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
169
229
|
module.exports = { ask, confirm, boxInput, close, processChunk };
|
package/src/screen.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Pinned-bottom chat layout using a terminal scroll region (DECSTBM).
|
|
4
|
+
// The conversation lives in the scroll region (top area) and scrolls there,
|
|
5
|
+
// while the input box stays fixed in the bottom rows.
|
|
6
|
+
let on = false;
|
|
7
|
+
|
|
8
|
+
function dims() { return { rows: process.stdout.rows || 24, cols: process.stdout.columns || 80 }; }
|
|
9
|
+
|
|
10
|
+
function enable() {
|
|
11
|
+
if (!process.stdout.isTTY) return false;
|
|
12
|
+
on = true;
|
|
13
|
+
process.stdout.write('\x1b[2J\x1b[3J\x1b[H'); // clear screen + scrollback, cursor home
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function disable() {
|
|
18
|
+
if (!on) return;
|
|
19
|
+
on = false;
|
|
20
|
+
const { rows } = dims();
|
|
21
|
+
process.stdout.write('\x1b[r'); // reset scroll region to full screen
|
|
22
|
+
process.stdout.write('\x1b[' + rows + ';1H\n'); // park cursor at the bottom
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Scroll region = rows 1..(rows-boxH); the bottom boxH rows hold the input box.
|
|
26
|
+
function setRegion(boxH) {
|
|
27
|
+
const { rows } = dims();
|
|
28
|
+
const bottom = Math.max(1, rows - boxH);
|
|
29
|
+
process.stdout.write('\x1b[1;' + bottom + 'r');
|
|
30
|
+
return bottom;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Put the cursor at the last line of the conversation region (so prints scroll there).
|
|
34
|
+
function toRegionBottom(boxH) {
|
|
35
|
+
const { rows } = dims();
|
|
36
|
+
process.stdout.write('\x1b[' + Math.max(1, rows - boxH) + ';1H');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { enable, disable, isOn: () => on, dims, setRegion, toRegionBottom };
|
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 };
|