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 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
- console.clear(); // hide login/clutter start clean with the header at the top
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
- // push the first input box toward the lower part of the screen, leaving a bottom margin
92
- const rows = process.stdout.rows || 24;
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') { console.clear(); me = await fetchMe(config.getServer(), cfg.key); ui.banner(meToBanner(cfg, me)); continue; }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "novaprime",
3
- "version": "1.6.1",
3
+ "version": "1.7.0",
4
4
  "description": "NovaPrime — an AI coding assistant in your terminal, powered by GLM.",
5
5
  "bin": {
6
6
  "novaprime": "bin/novaprime.js"
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
- process.stdout.write('\x1b[1B\r\n'); // move below the bottom border
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 };