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 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, leaving a bottom margin
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 - 26)));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "novaprime",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
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
@@ -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) it handles
7
- // editing and history. The multi-line chat box uses a paste-aware reader below.
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
- // length of the longest suffix of s that is a prefix of marker (handles a
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
- // Handle a run of plain (non-paste) bytes: typing, Enter, backspace, Ctrl+C/D,
50
- // and swallow escape sequences (arrow keys, etc.) so they don't corrupt input.
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; } // Ctrl+C
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; } // Enter -> send
57
- if (ch === '\x7f' || ch === '\b') { // backspace
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') { // skip other CSI/SS3 escapes (arrows, fn keys)
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 += ' '; sink.out(' '); continue; } // tab -> 2 spaces
74
- if (ch >= ' ') { state.buf += ch; sink.out(ch); } // printable
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 = { out(str), submit(value), cancel() }.
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
- const seg = normalize(s.slice(0, s.length - hold));
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
- const seg = normalize(s.slice(0, end));
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
- const plain = s.slice(0, s.length - hold);
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. Top border above; readline-free raw reader captures multi-line
117
- // pastes; bottom border drawn after submit so the message shows as a complete box.
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
- // Fallback for piped / non-TTY input (e.g. `echo ... | novaprime`): plain readline.
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; } // release stdin from readline
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
- const onData = (chunk) => processChunk(state, chunk, sink);
134
- const cleanup = () => {
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
- const sink = {
141
- out: (str) => process.stdout.write(str),
142
- submit: (val) => { state.finished = true; cleanup(); process.stdout.write('\n' + bottom + '\n'); resolve(val); },
143
- cancel: () => { state.finished = true; cleanup(); process.stdout.write('\n' + bottom + '\n'); resolve(null); },
144
- toggleMode: () => {
145
- const on = mode.toggle();
146
- process.stdout.write('\r\x1b[2K'); // clear current input line
147
- process.stdout.write((on
148
- ? c.green(' ⚡ auto-accept: ON') + c.dim(' — running everything without asking (Shift+Tab to undo)')
149
- : c.dim(' ⚡ auto-accept: OFF will ask before changes')) + '\n');
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
- process.stdout.write(prompt);
162
+ render();
156
163
  stdin.setRawMode(true);
157
164
  stdin.resume();
158
165
  stdin.setEncoding('utf8');
159
- process.stdout.write('\x1b[?2004h'); // enable bracketed paste
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 };