0agent 1.0.44 → 1.0.45

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/bin/chat.js +129 -93
  2. package/package.json +1 -1
package/bin/chat.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * /model to switch. /key to add provider keys. Never forgets previous keys.
8
8
  */
9
9
 
10
- import { createInterface, emitKeypressEvents, moveCursor, clearLine as rlClearLine } from 'node:readline';
10
+ import { createInterface, emitKeypressEvents } from 'node:readline';
11
11
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
12
12
  import { resolve } from 'node:path';
13
13
  import { homedir } from 'node:os';
@@ -836,61 +836,114 @@ async function handleCommand(input) {
836
836
  }
837
837
  }
838
838
 
839
- // ─── Live slash-command menu ──────────────────────────────────────────────────
840
- // Drawn below the prompt as the user types. Uses moveCursor to avoid cursor
841
- // save/restore conflicts with readline.
842
- let _menuLines = 0; // how many lines the current menu occupies below the cursor
839
+ // ─── Command palette ──────────────────────────────────────────────────────────
840
+ // Fully takes over stdin when open. No cursor tricks pauses readline,
841
+ // reads raw bytes directly, draws with \x1b[NA\x1b[0J (up + clear-to-end).
842
+ // Returns the selected command string, or null if cancelled.
843
843
 
844
- function _drawMenu(filter) {
845
- if (pendingResolve) { _clearMenu(); return; } // don't show while session running
844
+ let _paletteOpen = false;
846
845
 
847
- const items = filter === null ? [] :
848
- SLASH_COMMANDS.filter(c =>
849
- !filter || c.cmd.slice(1).toLowerCase().startsWith(filter.toLowerCase())
850
- ).slice(0, 10);
846
+ async function openPalette(initialFilter = '') {
847
+ if (_paletteOpen || pendingResolve) return null;
848
+ _paletteOpen = true;
851
849
 
852
- // If nothing changed (same count), skip redraw to avoid flicker
853
- if (items.length === 0) { _clearMenu(); return; }
850
+ // Pause readline so it stops consuming stdin events
851
+ rl.pause();
852
+ // Resume the raw stream so our data handler receives bytes
853
+ process.stdin.resume();
854
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
854
855
 
855
- const needed = items.length + 1; // +1 for blank line
856
+ let filter = initialFilter.toLowerCase();
857
+ let idx = 0;
858
+ let drawn = 0; // number of lines we've printed so far
856
859
 
857
- // Move down past existing menu lines (or 0), then clear downward
858
- const existingLines = _menuLines;
859
- if (existingLines > 0) {
860
- moveCursor(process.stdout, 0, existingLines);
861
- for (let i = 0; i < existingLines; i++) {
862
- rlClearLine(process.stdout, 0);
863
- if (i < existingLines - 1) moveCursor(process.stdout, 0, -1);
864
- }
865
- moveCursor(process.stdout, 0, -(existingLines - 1));
866
- }
860
+ const getItems = () => SLASH_COMMANDS.filter(c =>
861
+ !filter || c.cmd.slice(1).startsWith(filter)
862
+ );
867
863
 
868
- // Print blank separator + menu items, tracking column 0
869
- process.stdout.write('\n');
870
- for (const m of items) {
871
- process.stdout.write(
872
- ` ${fmt(C.cyan, m.cmd.padEnd(20))} ${fmt(C.dim, m.desc)}\x1b[K\n`
873
- );
874
- }
864
+ const paint = () => {
865
+ // Erase previous draw: move up `drawn` lines, clear everything below
866
+ if (drawn > 0) process.stdout.write(`\x1b[${drawn}A\x1b[0J`);
875
867
 
876
- // Move back up to the prompt line and restore cursor after the typed text
877
- moveCursor(process.stdout, 0, -(needed));
878
- // Jump to end of current line (readline already put cursor there)
879
- moveCursor(process.stdout, 999, 0);
868
+ const items = getItems();
869
+ const show = items.slice(0, 14);
870
+ if (idx >= items.length) idx = Math.max(0, items.length - 1);
880
871
 
881
- _menuLines = needed;
882
- }
872
+ const lines = [];
883
873
 
884
- function _clearMenu() {
885
- if (_menuLines === 0) return;
886
- const n = _menuLines;
887
- _menuLines = 0;
888
- moveCursor(process.stdout, 0, n);
889
- for (let i = 0; i < n; i++) {
890
- rlClearLine(process.stdout, 0);
891
- moveCursor(process.stdout, 0, -1);
892
- }
893
- moveCursor(process.stdout, 0, 1); // back to prompt line
874
+ // Top border
875
+ lines.push(` \x1b[2m${'─'.repeat(58)}\x1b[0m`);
876
+
877
+ if (show.length === 0) {
878
+ lines.push(` \x1b[2m no commands match "/${filter}"\x1b[0m`);
879
+ } else {
880
+ for (let i = 0; i < show.length; i++) {
881
+ const m = show[i];
882
+ const sel = i === idx;
883
+ if (sel) {
884
+ lines.push(
885
+ ` \x1b[36;1m›\x1b[0m \x1b[36;1m${m.cmd.padEnd(22)}\x1b[0m \x1b[0m${m.desc}\x1b[0m`
886
+ );
887
+ } else {
888
+ lines.push(
889
+ ` \x1b[36m${m.cmd.padEnd(22)}\x1b[0m \x1b[2m${m.desc}\x1b[0m`
890
+ );
891
+ }
892
+ }
893
+ }
894
+
895
+ if (items.length > 14) lines.push(` \x1b[2m …${items.length - 14} more\x1b[0m`);
896
+
897
+ // Bottom: search input line
898
+ lines.push(` \x1b[2m${'─'.repeat(58)}\x1b[0m`);
899
+ lines.push(` ${fmt(C.cyan, '/')}${filter}\x1b[K \x1b[2m↑↓ navigate · Enter select · Esc cancel\x1b[0m`);
900
+
901
+ const out = lines.join('\n') + '\n';
902
+ process.stdout.write(out);
903
+ drawn = lines.length;
904
+ };
905
+
906
+ paint();
907
+
908
+ return new Promise((resolve) => {
909
+ const onData = (chunk) => {
910
+ const s = chunk.toString();
911
+
912
+ if (s === '\r' || s === '\n') { // Enter — select
913
+ const sel = getItems()[idx]?.cmd ?? null;
914
+ finish(sel);
915
+ } else if (s === '\x1b' || s === '\x03') { // Esc / Ctrl-C — cancel
916
+ finish(null);
917
+ } else if (s === '\x1b[A') { // Up arrow
918
+ idx = Math.max(0, idx - 1);
919
+ paint();
920
+ } else if (s === '\x1b[B') { // Down arrow
921
+ idx = Math.min(getItems().length - 1, idx + 1);
922
+ paint();
923
+ } else if (s === '\x7f' || s === '\x08') { // Backspace
924
+ filter = filter.slice(0, -1);
925
+ idx = 0;
926
+ paint();
927
+ } else if (/^[a-z0-9\-_]$/i.test(s)) { // Printable letter/digit/hyphen
928
+ filter += s.toLowerCase();
929
+ idx = 0;
930
+ paint();
931
+ }
932
+ };
933
+
934
+ const finish = (selected) => {
935
+ process.stdin.removeListener('data', onData);
936
+ // Erase the palette
937
+ if (drawn > 0) process.stdout.write(`\x1b[${drawn}A\x1b[0J`);
938
+ drawn = 0;
939
+ _paletteOpen = false;
940
+ // Hand control back to readline
941
+ rl.resume();
942
+ resolve(selected);
943
+ };
944
+
945
+ process.stdin.on('data', onData);
946
+ });
894
947
  }
895
948
 
896
949
  // ─── Main REPL ────────────────────────────────────────────────────────────────
@@ -902,50 +955,31 @@ const rl = createInterface({
902
955
  completer: (line, callback) => {
903
956
  if (!line.startsWith('/')) return callback(null, [[], line]);
904
957
 
905
- const filter = line.slice(1).toLowerCase();
906
- const matches = SLASH_COMMANDS.filter(c =>
907
- !filter || c.cmd.slice(1).startsWith(filter)
908
- );
909
-
910
- if (matches.length === 0) return callback(null, [[], line]);
958
+ const filter = line.slice(1).toLowerCase();
959
+ const matches = SLASH_COMMANDS.filter(c => !filter || c.cmd.slice(1).startsWith(filter));
911
960
 
912
- // Single match — let readline silently auto-complete
913
- if (matches.length === 1) {
914
- _clearMenu();
915
- return callback(null, [[matches[0].cmd + ' '], line]);
916
- }
961
+ // Single exact match — let readline silently auto-complete
962
+ if (matches.length === 1) return callback(null, [[matches[0].cmd + ' '], line]);
917
963
 
918
- // Multiple matches print formatted menu, suppress readline's plain list
919
- _clearMenu();
920
- process.stdout.write('\n\n');
921
- for (const m of matches.slice(0, 12)) {
922
- process.stdout.write(` ${fmt(C.cyan, m.cmd.padEnd(22))} ${fmt(C.dim, m.desc)}\n`);
923
- }
924
- if (matches.length > 12) {
925
- process.stdout.write(` ${fmt(C.dim, `…and ${matches.length - 12} more`)}\n`);
926
- }
927
- process.stdout.write('\n');
928
- setImmediate(() => rl.prompt(true));
964
+ // Multiple (or bare /) open interactive palette
965
+ // Schedule after this tick so readline finishes its Tab handling first
966
+ setImmediate(async () => {
967
+ // Clear the typed fragment from the prompt line before opening palette
968
+ process.stdout.write('\r\x1b[2K');
969
+ const selected = await openPalette(filter);
970
+ if (selected) {
971
+ await executeInput(selected);
972
+ } else {
973
+ rl.prompt();
974
+ }
975
+ });
929
976
  return callback(null, [[], line]);
930
977
  },
931
978
  });
932
979
 
933
- // Live menu on keypress draws below the prompt as user types
980
+ // Trigger palette when user types exactly '/' and presses Tab or Enter isn't needed —
981
+ // the completer above handles Tab. For bare '/' + Enter, handled in rl.on('line') below.
934
982
  emitKeypressEvents(process.stdin, rl);
935
- process.stdin.on('keypress', (_char, key) => {
936
- if (key?.name === 'return' || key?.name === 'enter') {
937
- _clearMenu(); // clear before readline processes the line
938
- return;
939
- }
940
- setImmediate(() => {
941
- const line = rl.line ?? '';
942
- if (line.startsWith('/') && !pendingResolve) {
943
- _drawMenu(line.slice(1));
944
- } else {
945
- _clearMenu();
946
- }
947
- });
948
- });
949
983
 
950
984
  printHeader();
951
985
  printInsights();
@@ -1253,19 +1287,21 @@ async function drainQueue() {
1253
1287
  }
1254
1288
 
1255
1289
  rl.on('line', async (input) => {
1256
- _clearMenu(); // always clear menu when a line is submitted
1257
1290
  const line = input.trim();
1258
1291
  if (!line) { rl.prompt(); return; }
1259
1292
 
1260
- // Bare `/` → show full command palette
1261
- if (line === '/') {
1262
- console.log('');
1263
- for (const m of SLASH_COMMANDS) {
1264
- console.log(` ${fmt(C.cyan, m.cmd.padEnd(22))} ${fmt(C.dim, m.desc)}`);
1293
+ // Bare `/` or partial `/cmd` with no session running open interactive palette
1294
+ if (line.startsWith('/') && !pendingResolve) {
1295
+ const filter = line === '/' ? '' : line.slice(1);
1296
+ const exact = SLASH_COMMANDS.find(c => c.cmd === line);
1297
+ if (!exact) {
1298
+ // Clear the echoed input line before opening palette
1299
+ process.stdout.write('\x1b[1A\r\x1b[2K');
1300
+ const selected = await openPalette(filter);
1301
+ if (selected) await executeInput(selected);
1302
+ else rl.prompt();
1303
+ return;
1265
1304
  }
1266
- console.log('');
1267
- rl.prompt();
1268
- return;
1269
1305
  }
1270
1306
 
1271
1307
  // If a session is already running, queue the message.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0agent",
3
- "version": "1.0.44",
3
+ "version": "1.0.45",
4
4
  "description": "A persistent, learning AI agent that runs on your machine. An agent that learns.",
5
5
  "private": false,
6
6
  "license": "Apache-2.0",