novaprime 1.4.0 → 1.4.1

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/prompt.js +124 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "novaprime",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
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
@@ -1,7 +1,8 @@
1
1
  'use strict';
2
2
  const readline = require('readline');
3
3
 
4
- // Use Node's readline it correctly handles long lines, wrapping, editing and paste.
4
+ // Use Node's readline for single-line prompts (login key, y/N) it handles
5
+ // editing and history. The multi-line chat box uses a paste-aware reader below.
5
6
  let rl = null;
6
7
  function getRl() {
7
8
  if (!rl) {
@@ -22,19 +23,131 @@ function confirm(message) {
22
23
  return ask(message + ' (y/N) ').then((a) => /^y(es)?$/i.test((a || '').trim()));
23
24
  }
24
25
 
25
- // Chat input: top border above (safe), readline handles the input line,
26
- // and the bottom border is drawn AFTER submit so the message shows as a complete box.
26
+ function close() { if (rl) { rl.close(); rl = null; } }
27
+
28
+ // ---- bracketed-paste-aware chat input ----------------------------------
29
+ // Terminals that support bracketed paste wrap pasted text in these markers.
30
+ // We capture everything between them (newlines included) WITHOUT submitting,
31
+ // so pasting a big multi-line prompt no longer auto-sends on the first line.
32
+ const PASTE_START = '\x1b[200~';
33
+ const PASTE_END = '\x1b[201~';
34
+
35
+ function normalize(t) { return t.replace(/\r\n?/g, '\n'); }
36
+
37
+ // length of the longest suffix of s that is a prefix of marker (handles a
38
+ // marker that got split across two stdin chunks)
39
+ function partialTail(s, marker) {
40
+ const max = Math.min(s.length, marker.length - 1);
41
+ for (let n = max; n > 0; n--) {
42
+ if (marker.startsWith(s.slice(s.length - n))) return n;
43
+ }
44
+ return 0;
45
+ }
46
+
47
+ // Handle a run of plain (non-paste) bytes: typing, Enter, backspace, Ctrl+C/D,
48
+ // and swallow escape sequences (arrow keys, etc.) so they don't corrupt input.
49
+ function handlePlain(state, s, sink) {
50
+ for (let i = 0; i < s.length && !state.finished; i++) {
51
+ const ch = s[i];
52
+ if (ch === '\x03') { sink.cancel(); return; } // Ctrl+C
53
+ if (ch === '\x04') { if (!state.buf) { sink.cancel(); return; } continue; } // Ctrl+D on empty
54
+ if (ch === '\r' || ch === '\n') { sink.submit(state.buf); return; } // Enter -> send
55
+ if (ch === '\x7f' || ch === '\b') { // backspace
56
+ if (state.buf.length) {
57
+ const last = state.buf[state.buf.length - 1];
58
+ state.buf = state.buf.slice(0, -1);
59
+ if (last !== '\n') sink.out('\b \b');
60
+ }
61
+ continue;
62
+ }
63
+ if (ch === '\x1b') { // skip CSI/SS3 escape (arrows, fn keys)
64
+ if (s[i + 1] === '[' || s[i + 1] === 'O') {
65
+ i += 2;
66
+ while (i < s.length && !/[A-Za-z~]/.test(s[i])) i++;
67
+ }
68
+ continue;
69
+ }
70
+ if (ch === '\t') { state.buf += ' '; sink.out(' '); continue; } // tab -> 2 spaces
71
+ if (ch >= ' ') { state.buf += ch; sink.out(ch); } // printable
72
+ }
73
+ }
74
+
75
+ // Pure state machine — testable. state = { buf, pasting, pending, finished }.
76
+ // sink = { out(str), submit(value), cancel() }.
77
+ function processChunk(state, chunk, sink) {
78
+ let s = state.pending + chunk;
79
+ state.pending = '';
80
+ while (s.length && !state.finished) {
81
+ if (state.pasting) {
82
+ const end = s.indexOf(PASTE_END);
83
+ if (end === -1) {
84
+ const hold = partialTail(s, PASTE_END);
85
+ const seg = normalize(s.slice(0, s.length - hold));
86
+ if (seg) { state.buf += seg; sink.out(seg); }
87
+ state.pending = s.slice(s.length - hold);
88
+ return;
89
+ }
90
+ const seg = normalize(s.slice(0, end));
91
+ if (seg) { state.buf += seg; sink.out(seg); }
92
+ state.pasting = false;
93
+ s = s.slice(end + PASTE_END.length);
94
+ continue;
95
+ }
96
+ const start = s.indexOf(PASTE_START);
97
+ if (start === -1) {
98
+ const hold = partialTail(s, PASTE_START);
99
+ const plain = s.slice(0, s.length - hold);
100
+ handlePlain(state, plain, sink);
101
+ if (!state.finished) state.pending = s.slice(s.length - hold);
102
+ return;
103
+ }
104
+ handlePlain(state, s.slice(0, start), sink);
105
+ if (state.finished) return;
106
+ state.pasting = true;
107
+ s = s.slice(start + PASTE_START.length);
108
+ }
109
+ }
110
+
111
+ function ttyRaw() { return process.stdin.isTTY && typeof process.stdin.setRawMode === 'function'; }
112
+
113
+ // Chat input box. Top border above; readline-free raw reader captures multi-line
114
+ // pastes; bottom border drawn after submit so the message shows as a complete box.
27
115
  function boxInput(top, bottom, prompt) {
116
+ // Fallback for piped / non-TTY input (e.g. `echo ... | novaprime`): plain readline.
117
+ if (!ttyRaw()) {
118
+ return new Promise((resolve) => {
119
+ process.stdout.write(top + '\n');
120
+ const r = getRl();
121
+ r.question(prompt, (answer) => { process.stdout.write(bottom + '\n'); resolve(answer); });
122
+ });
123
+ }
124
+ if (rl) { rl.close(); rl = null; } // release stdin from readline
125
+
28
126
  return new Promise((resolve) => {
127
+ const stdin = process.stdin;
128
+ const state = { buf: '', pasting: false, pending: '', finished: false };
129
+
130
+ const onData = (chunk) => processChunk(state, chunk, sink);
131
+ const cleanup = () => {
132
+ try { process.stdout.write('\x1b[?2004l'); } catch (_) {}
133
+ try { stdin.setRawMode(false); } catch (_) {}
134
+ stdin.pause();
135
+ stdin.removeListener('data', onData);
136
+ };
137
+ const sink = {
138
+ out: (str) => process.stdout.write(str),
139
+ submit: (val) => { state.finished = true; cleanup(); process.stdout.write('\n' + bottom + '\n'); resolve(val); },
140
+ cancel: () => { state.finished = true; cleanup(); process.stdout.write('\n' + bottom + '\n'); resolve(null); },
141
+ };
142
+
29
143
  process.stdout.write(top + '\n');
30
- const r = getRl();
31
- r.question(prompt, (answer) => {
32
- process.stdout.write(bottom + '\n');
33
- resolve(answer);
34
- });
144
+ process.stdout.write(prompt);
145
+ stdin.setRawMode(true);
146
+ stdin.resume();
147
+ stdin.setEncoding('utf8');
148
+ process.stdout.write('\x1b[?2004h'); // enable bracketed paste
149
+ stdin.on('data', onData);
35
150
  });
36
151
  }
37
152
 
38
- function close() { if (rl) { rl.close(); rl = null; } }
39
-
40
- module.exports = { ask, confirm, boxInput, close };
153
+ module.exports = { ask, confirm, boxInput, close, processChunk };