novaprime 1.6.2 → 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 +6 -9
- package/package.json +1 -1
- package/src/prompt.js +58 -0
- package/src/screen.js +39 -0
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 - 23)));
|
|
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,7 +100,7 @@ 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
|
|
|
108
105
|
ui.userBubble(input); // show the sent message as a clean bubble (box already erased)
|
|
109
106
|
messages.push({ role: 'user', content: input });
|
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) => {
|
|
@@ -168,4 +170,60 @@ function boxInput(top, bottom, prompt) {
|
|
|
168
170
|
});
|
|
169
171
|
}
|
|
170
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
|
+
|
|
171
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 };
|