@semalt-ai/code 1.5.0 → 1.6.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.
Files changed (2) hide show
  1. package/lib/ui.js +237 -91
  2. package/package.json +1 -1
package/lib/ui.js CHANGED
@@ -111,133 +111,279 @@ function readInteractiveInput(promptText, options = {}) {
111
111
  let done = false;
112
112
  let historyIndex = Array.isArray(history) ? history.length : -1;
113
113
  let historyActive = false;
114
+ let pasting = false;
115
+ let lastLineCount = 1;
116
+ let lastCursorRow = 0;
117
+ let lastWasCR = false;
114
118
 
115
- readline.emitKeypressEvents(process.stdin);
116
119
  process.stdin.setRawMode(true);
117
120
  process.stdin.resume();
121
+ process.stdout.write('\x1b[?2004h'); // enable bracketed paste mode
122
+
123
+ const promptWidth = stripAnsi(promptText).length;
118
124
 
119
125
  const render = () => {
126
+ const lines = buffer.split('\n');
127
+ const newLineCount = lines.length;
128
+
129
+ if (lastLineCount > 1) {
130
+ readline.moveCursor(process.stdout, 0, -(lastLineCount - 1));
131
+ }
132
+
120
133
  readline.cursorTo(process.stdout, 0);
121
134
  readline.clearLine(process.stdout, 0);
122
- process.stdout.write(`${promptText}${buffer}`);
123
- const promptWidth = stripAnsi(promptText).length;
124
- readline.cursorTo(process.stdout, promptWidth + cursor);
135
+ process.stdout.write(`${promptText}${lines[0]}`);
136
+
137
+ for (let i = 1; i < newLineCount; i++) {
138
+ process.stdout.write('\n');
139
+ readline.cursorTo(process.stdout, 0);
140
+ readline.clearLine(process.stdout, 0);
141
+ process.stdout.write(lines[i]);
142
+ }
143
+
144
+ // Clear any extra lines from a previous longer render
145
+ for (let i = newLineCount; i < lastLineCount; i++) {
146
+ process.stdout.write('\n');
147
+ readline.cursorTo(process.stdout, 0);
148
+ readline.clearLine(process.stdout, 0);
149
+ }
150
+
151
+ lastLineCount = newLineCount;
152
+
153
+ const beforeCursor = Array.from(buffer).slice(0, cursor).join('');
154
+ const cursorLines = beforeCursor.split('\n');
155
+ lastCursorRow = cursorLines.length - 1;
156
+ const cursorCol = (lastCursorRow === 0 ? promptWidth : 0) + cursorLines[lastCursorRow].length;
157
+
158
+ const rowsFromBottom = newLineCount - 1 - lastCursorRow;
159
+ if (rowsFromBottom > 0) {
160
+ readline.moveCursor(process.stdout, 0, -rowsFromBottom);
161
+ }
162
+ readline.cursorTo(process.stdout, cursorCol);
125
163
  };
126
164
 
127
165
  const finish = (result, addNewline = true) => {
128
166
  if (done) return;
129
167
  done = true;
168
+ process.stdout.write('\x1b[?2004l'); // disable bracketed paste mode
130
169
  process.stdin.setRawMode(wasRaw);
131
- process.stdin.removeListener('keypress', onKeypress);
132
- if (addNewline) process.stdout.write('\n');
170
+ process.stdin.removeListener('data', onData);
171
+ if (addNewline) {
172
+ const rowsToBottom = lastLineCount - 1 - lastCursorRow;
173
+ if (rowsToBottom > 0) {
174
+ readline.moveCursor(process.stdout, 0, rowsToBottom);
175
+ }
176
+ process.stdout.write('\n');
177
+ }
133
178
  resolve(result);
134
179
  };
135
180
 
136
- const onKeypress = (str, key = {}) => {
137
- if (key.ctrl && key.name === 'c') {
138
- if (buffer) {
139
- buffer = '';
140
- cursor = 0;
141
- render();
142
- return;
181
+ // Parse raw stdin data directly so we can intercept bracketed paste markers
182
+ // (\x1b[200~ ... \x1b[201~) before any higher-level key parser sees them.
183
+ const onData = (chunk) => {
184
+ if (done) return;
185
+ const data = chunk.toString('utf8');
186
+ let i = 0;
187
+
188
+ while (i < data.length) {
189
+ // --- Bracketed paste markers ---
190
+ if (data.startsWith('\x1b[200~', i)) {
191
+ pasting = true;
192
+ lastWasCR = false;
193
+ i += 6;
194
+ continue;
195
+ }
196
+ if (data.startsWith('\x1b[201~', i)) {
197
+ pasting = false;
198
+ lastWasCR = false;
199
+ i += 6;
200
+ continue;
143
201
  }
144
- finish({ type: 'sigint' }, false);
145
- return;
146
- }
147
202
 
148
- if (key.ctrl && key.name === 'd') {
149
- if (!buffer) finish({ type: 'eof' }, false);
150
- return;
151
- }
203
+ const ch = data[i];
204
+
205
+ // --- Escape sequences ---
206
+ if (ch === '\x1b') {
207
+ lastWasCR = false;
208
+ const next = data[i + 1];
209
+
210
+ // CSI sequences: \x1b[...
211
+ if (next === '[') {
212
+ const seq3 = data.slice(i, i + 3);
213
+ const seq4 = data.slice(i, i + 4);
214
+
215
+ if (seq3 === '\x1b[A') { // up
216
+ if (Array.isArray(history) && history.length) {
217
+ if (historyActive || buffer.length === 0) {
218
+ historyActive = true;
219
+ historyIndex = Math.max(0, historyIndex - 1);
220
+ buffer = historyIndex >= history.length ? '' : history[historyIndex];
221
+ cursor = Array.from(buffer).length;
222
+ render();
223
+ }
224
+ }
225
+ i += 3; continue;
226
+ }
227
+ if (seq3 === '\x1b[B') { // down
228
+ if (Array.isArray(history) && history.length) {
229
+ if (historyActive || buffer.length === 0) {
230
+ historyActive = true;
231
+ historyIndex = Math.min(history.length, historyIndex + 1);
232
+ buffer = historyIndex >= history.length ? '' : history[historyIndex];
233
+ cursor = Array.from(buffer).length;
234
+ render();
235
+ }
236
+ }
237
+ i += 3; continue;
238
+ }
239
+ if (seq3 === '\x1b[C') { // right
240
+ if (allowCursorNavigation && cursor < Array.from(buffer).length) { cursor++; render(); }
241
+ i += 3; continue;
242
+ }
243
+ if (seq3 === '\x1b[D') { // left
244
+ if (allowCursorNavigation && cursor > 0) { cursor--; render(); }
245
+ i += 3; continue;
246
+ }
247
+ if (seq3 === '\x1b[H' || seq4 === '\x1b[1~') { // home
248
+ if (allowCursorNavigation) { cursor = 0; render(); }
249
+ i += (seq3 === '\x1b[H' ? 3 : 4); continue;
250
+ }
251
+ if (seq3 === '\x1b[F' || seq4 === '\x1b[4~') { // end
252
+ if (allowCursorNavigation) { cursor = Array.from(buffer).length; render(); }
253
+ i += (seq3 === '\x1b[F' ? 3 : 4); continue;
254
+ }
255
+ if (seq4 === '\x1b[3~') { // delete
256
+ if (cursor < Array.from(buffer).length) {
257
+ buffer = removeCharAt(buffer, cursor);
258
+ historyActive = false;
259
+ render();
260
+ }
261
+ i += 4; continue;
262
+ }
263
+ // Skip unknown CSI sequence (read until terminating byte)
264
+ let j = i + 2;
265
+ while (j < data.length && (data.charCodeAt(j) < 0x40 || data.charCodeAt(j) > 0x7e)) j++;
266
+ i = j + 1;
267
+ continue;
268
+ }
269
+
270
+ // SS3 sequences: \x1bO... (application cursor keys)
271
+ if (next === 'O') {
272
+ const c = data[i + 2];
273
+ if (c === 'A') { // up
274
+ if (Array.isArray(history) && history.length && (historyActive || buffer.length === 0)) {
275
+ historyActive = true;
276
+ historyIndex = Math.max(0, historyIndex - 1);
277
+ buffer = historyIndex >= history.length ? '' : history[historyIndex];
278
+ cursor = Array.from(buffer).length;
279
+ render();
280
+ }
281
+ i += 3; continue;
282
+ }
283
+ if (c === 'B') { // down
284
+ if (Array.isArray(history) && history.length && (historyActive || buffer.length === 0)) {
285
+ historyActive = true;
286
+ historyIndex = Math.min(history.length, historyIndex + 1);
287
+ buffer = historyIndex >= history.length ? '' : history[historyIndex];
288
+ cursor = Array.from(buffer).length;
289
+ render();
290
+ }
291
+ i += 3; continue;
292
+ }
293
+ if (c === 'C' && allowCursorNavigation) { if (cursor < Array.from(buffer).length) { cursor++; render(); } i += 3; continue; }
294
+ if (c === 'D' && allowCursorNavigation) { if (cursor > 0) { cursor--; render(); } i += 3; continue; }
295
+ if (c === 'H' && allowCursorNavigation) { cursor = 0; render(); i += 3; continue; }
296
+ if (c === 'F' && allowCursorNavigation) { cursor = Array.from(buffer).length; render(); i += 3; continue; }
297
+ i += 3; continue;
298
+ }
299
+
300
+ // Unknown escape - skip
301
+ i += 2;
302
+ continue;
303
+ }
152
304
 
153
- if (key.name === 'return' || key.name === 'enter') {
154
- finish({ type: 'submit', value: trim ? buffer.trim() : buffer });
155
- return;
156
- }
305
+ // --- Ctrl+C ---
306
+ if (ch === '\x03') {
307
+ lastWasCR = false;
308
+ if (buffer) { buffer = ''; cursor = 0; pasting = false; render(); }
309
+ else { finish({ type: 'sigint' }, false); return; }
310
+ i++; continue;
311
+ }
157
312
 
158
- if (key.name === 'backspace' || key.name === 'delete') {
159
- if (key.name === 'backspace' && cursor > 0) {
160
- buffer = removeCharAt(buffer, cursor - 1);
161
- cursor--;
162
- historyActive = false;
163
- render();
164
- } else if (key.name === 'delete' && cursor < Array.from(buffer).length) {
165
- buffer = removeCharAt(buffer, cursor);
166
- historyActive = false;
167
- render();
313
+ // --- Ctrl+D ---
314
+ if (ch === '\x04') {
315
+ lastWasCR = false;
316
+ if (!buffer) { finish({ type: 'eof' }, false); return; }
317
+ i++; continue;
168
318
  }
169
- return;
170
- }
171
319
 
172
- if (allowCursorNavigation && key.name === 'left') {
173
- if (cursor > 0) {
174
- cursor--;
175
- render();
320
+ // --- Enter / newline ---
321
+ if (ch === '\r' || ch === '\n') {
322
+ // Collapse CRLF into a single newline
323
+ if (ch === '\n' && lastWasCR) { lastWasCR = false; i++; continue; }
324
+ lastWasCR = (ch === '\r');
325
+
326
+ if (pasting) {
327
+ buffer = insertCharAt(buffer, cursor, '\n');
328
+ cursor++;
329
+ historyActive = false;
330
+ render();
331
+ } else {
332
+ finish({ type: 'submit', value: trim ? buffer.trim() : buffer });
333
+ return;
334
+ }
335
+ i++; continue;
336
+ }
337
+ lastWasCR = false;
338
+
339
+ // --- Backspace ---
340
+ if (ch === '\x7f' || ch === '\x08') {
341
+ if (cursor > 0) {
342
+ buffer = removeCharAt(buffer, cursor - 1);
343
+ cursor--;
344
+ historyActive = false;
345
+ render();
346
+ }
347
+ i++; continue;
176
348
  }
177
- return;
178
- }
179
349
 
180
- if (allowCursorNavigation && key.name === 'right') {
181
- if (cursor < Array.from(buffer).length) {
182
- cursor++;
183
- render();
350
+ // --- Tab ---
351
+ if (ch === '\t') {
352
+ if (pasting) {
353
+ buffer = insertCharAt(buffer, cursor, '\t');
354
+ cursor++;
355
+ render();
356
+ }
357
+ i++; continue;
184
358
  }
185
- return;
186
- }
187
359
 
188
- if (allowCursorNavigation && key.name === 'home') {
189
- cursor = 0;
190
- render();
191
- return;
192
- }
360
+ // --- Other control characters: skip ---
361
+ if (ch.charCodeAt(0) < 0x20) { i++; continue; }
193
362
 
194
- if (allowCursorNavigation && key.name === 'end') {
195
- cursor = Array.from(buffer).length;
196
- render();
197
- return;
198
- }
363
+ // --- Printable character (handles multi-byte Unicode via code points) ---
364
+ const cp = data.codePointAt(i);
365
+ const char = String.fromCodePoint(cp);
199
366
 
200
- if (Array.isArray(history) && (key.name === 'up' || key.name === 'down')) {
201
- const canEnterHistory = buffer.length === 0;
202
- if (!history.length) return;
203
- if (!historyActive && !canEnterHistory) return;
367
+ if (allowed && !allowed.includes(char)) { i += char.length; continue; }
204
368
 
205
- historyActive = true;
206
- if (key.name === 'up') {
207
- historyIndex = Math.max(0, historyIndex - 1);
208
- } else {
209
- historyIndex = Math.min(history.length, historyIndex + 1);
369
+ if (immediate) {
370
+ buffer = char;
371
+ cursor = 1;
372
+ historyActive = false;
373
+ render();
374
+ finish({ type: 'submit', value: char });
375
+ return;
210
376
  }
211
377
 
212
- buffer = historyIndex >= history.length ? '' : history[historyIndex];
213
- cursor = Array.from(buffer).length;
214
- render();
215
- return;
216
- }
217
-
218
- if (key.name && ['up', 'down', 'left', 'right', 'home', 'end', 'pageup', 'pagedown', 'escape', 'tab'].includes(key.name)) {
219
- return;
220
- }
221
-
222
- if (!isPrintableKey(str, key)) return;
223
- if (allowed && !allowed.includes(str)) return;
224
-
225
- if (immediate) {
226
- buffer = str;
227
- cursor = Array.from(buffer).length;
378
+ buffer = insertCharAt(buffer, cursor, char);
379
+ cursor++;
228
380
  historyActive = false;
229
381
  render();
230
- finish({ type: 'submit', value: str });
231
- return;
382
+ i += char.length;
232
383
  }
233
-
234
- buffer = insertCharAt(buffer, cursor, str);
235
- cursor++;
236
- historyActive = false;
237
- render();
238
384
  };
239
385
 
240
- process.stdin.on('keypress', onKeypress);
386
+ process.stdin.on('data', onData);
241
387
  render();
242
388
  });
243
389
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@semalt-ai/code",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Self-hosted AI Coding Assistant CLI",
5
5
  "main": "index.js",
6
6
  "bin": {