@tiens.nguyen/gonext-local-worker 1.0.211 → 1.0.213

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/gonext-repl.mjs +61 -22
  2. package/package.json +1 -1
package/gonext-repl.mjs CHANGED
@@ -175,26 +175,31 @@ function slashCompleter(line) {
175
175
  const hits = COMMAND_NAMES.filter((n) => n.startsWith(line));
176
176
  return [hits.length ? hits : COMMAND_NAMES, line];
177
177
  }
178
- function commandRows(prefix = "") {
178
+ // Commands whose name/alias matches the typed prefix (all when prefix is "" or "/").
179
+ function filteredCommands(prefix = "") {
179
180
  const p = (prefix ?? "").trim();
180
- const show = COMMANDS.filter(
181
+ const m = COMMANDS.filter(
181
182
  (c) =>
182
183
  !p ||
183
184
  p === "/" ||
184
185
  [c.name, ...(c.aliases ?? [])].some((n) => n.startsWith(p))
185
186
  );
186
- const list = show.length ? show : COMMANDS;
187
- const rows = [dim(" commands (Tab completes):")];
188
- for (const c of list) {
187
+ return m.length ? m : COMMANDS;
188
+ }
189
+ // Display rows for a filtered command set; row `sel` (if >=0) is highlighted (❯ + bold).
190
+ function hintRows(cmds, sel) {
191
+ const rows = [dim(" commands (↑/↓ select · Enter run · Tab complete):")];
192
+ cmds.forEach((c, i) => {
189
193
  const label = c.usage ?? c.name;
190
194
  const alias = c.aliases?.length ? dim(` (${c.aliases.join(", ")})`) : "";
191
- rows.push(` ${cyan(label)}${alias}${dim(" — " + c.desc)}`);
192
- }
195
+ const body = `${cyan(label)}${alias}${dim(" — " + c.desc)}`;
196
+ rows.push(i === sel ? `${green("❯")} ${bold(body)}` : ` ${body}`);
197
+ });
193
198
  return rows;
194
199
  }
195
200
  // Permanent print (used by /help and a lone-"/" submit — scrolls with history).
196
201
  function printCommands(prefix = "") {
197
- for (const r of commandRows(prefix)) console.log(r);
202
+ for (const r of hintRows(filteredCommands(prefix), -1)) console.log(r);
198
203
  console.log("");
199
204
  }
200
205
 
@@ -228,6 +233,9 @@ const _lineQueue = [];
228
233
  let _lineWaiter = null;
229
234
  let _stdinClosed = false;
230
235
  let slashHintRows = 0; // live slash-command hint rows drawn below the prompt (0 = none)
236
+ let slashSel = -1; // highlighted row in the current filtered list (-1 = none)
237
+ let slashNames = []; // command names in the current filtered list (for Enter → run)
238
+ let slashPrefix = ""; // the "/…" text the user typed (restored when ↑/↓ recall history)
231
239
  rl.on("line", (l) => {
232
240
  // While a turn is running, IGNORE typed input entirely — no type-ahead, no queued
233
241
  // next question. Only Ctrl+C (handled via SIGINT) does anything mid-turn. Clear the
@@ -238,12 +246,17 @@ rl.on("line", (l) => {
238
246
  return;
239
247
  }
240
248
  // Enter left the cursor on the row where the live slash-hint started — erase it so the
241
- // command's own output doesn't print over/under a stale hint block.
249
+ // command's own output doesn't print over/under a stale hint block. If a menu item was
250
+ // highlighted (↑/↓), RUN that command instead of the raw typed line.
251
+ let submitted = l;
242
252
  if (slashHintRows > 0) {
253
+ if (slashSel >= 0 && slashNames[slashSel]) submitted = slashNames[slashSel];
243
254
  process.stdout.write("\x1b[J");
244
255
  slashHintRows = 0;
256
+ slashSel = -1;
257
+ slashNames = [];
245
258
  }
246
- const clean = stripAnsiEscapes(l);
259
+ const clean = stripAnsiEscapes(submitted);
247
260
  if (_lineWaiter) {
248
261
  const w = _lineWaiter;
249
262
  _lineWaiter = null;
@@ -256,38 +269,64 @@ rl.on("line", (l) => {
256
269
  // turn: echo is already muted, but readline still recalls history into the invisible
257
270
  // buffer on ↑/↓ — reset the line state after every keypress so nothing accumulates or
258
271
  // moves. Ctrl+C is unaffected (readline emits its SIGINT before this handler runs).
259
- // Live command hint = an EPHEMERAL overlay drawn BELOW the prompt while the line starts
260
- // with "/", so it redraws as you type (narrowing) and DISAPPEARS when you clear the slash
261
- // (instead of stacking permanent copies in scrollback). Rendered with relative cursor
262
- // moves (down to draw, back up to the input line) so it survives terminal scrolling.
272
+ // Live command hint = an EPHEMERAL, SELECTABLE overlay drawn BELOW the prompt while the
273
+ // line starts with "/". It redraws as you type (narrowing), ↑/↓ move a highlight, Enter
274
+ // runs the highlighted command, and it DISAPPEARS when you clear the slash — no permanent
275
+ // scrollback copies. Relative cursor moves so it survives terminal scrolling.
276
+ // (slashSel / slashNames / slashPrefix are declared above, near rl.on("line").)
263
277
  const inputCol = () => 4 + rl.cursor; // ">> " is 3 cols; content starts at col 4 (1-based)
264
278
  function clearSlashHint() {
265
279
  if (slashHintRows === 0) return;
266
- // Cursor is on the input line. Drop into the hint region, erase to end of screen, return.
267
280
  process.stdout.write("\n\x1b[J" + `\x1b[1A\x1b[${inputCol()}G`);
268
281
  slashHintRows = 0;
282
+ slashSel = -1;
283
+ slashNames = [];
269
284
  }
270
- function drawSlashHint(line) {
271
- const rows = commandRows(line);
272
- // Repaint in place: erase the old block, draw the new one, move the cursor back up.
285
+ function drawSlashHint(prefix, sel) {
286
+ const cmds = filteredCommands(prefix);
287
+ slashNames = cmds.map((c) => c.name);
288
+ if (sel >= cmds.length) sel = cmds.length - 1;
289
+ slashSel = sel;
290
+ const rows = hintRows(cmds, sel);
273
291
  const body = rows.map((r) => "\x1b[K" + r).join("\n");
274
292
  let out = "";
275
293
  if (slashHintRows > 0) out += "\n\x1b[J\x1b[1A"; // wipe previous block, back to input row
276
294
  out += "\n" + body; // draw below the input line
277
- out += `\x1b[${rows.length}A\x1b[${inputCol()}G`; // back up to the input line + column
295
+ out += `\x1b[${rows.length}A\x1b[${inputCol()}G`; // back up to input line + column
278
296
  process.stdout.write(out);
279
297
  slashHintRows = rows.length;
280
298
  }
281
299
  if (process.stdin.isTTY) {
282
- process.stdin.on("keypress", () => {
300
+ process.stdin.on("keypress", (_ch, key) => {
283
301
  if (following) {
284
302
  rl.line = "";
285
303
  rl.cursor = 0;
286
304
  rl.historyIndex = -1;
287
305
  return;
288
306
  }
289
- if (rl.line.startsWith("/")) drawSlashHint(rl.line);
290
- else clearSlashHint();
307
+ // Enter is handled by rl.on("line") — don't redraw the hint on the way out.
308
+ if (key && (key.name === "return" || key.name === "enter")) return;
309
+ // ↑/↓ while the menu is up = move the highlight, NOT recall shell history. readline
310
+ // already ran history-nav (may have changed the line); restore the typed "/…" prefix
311
+ // and redraw with the new selection.
312
+ if (slashHintRows > 0 && key && (key.name === "up" || key.name === "down")) {
313
+ rl.line = slashPrefix;
314
+ rl.cursor = slashPrefix.length;
315
+ const n = slashNames.length;
316
+ if (n > 0) {
317
+ slashSel =
318
+ key.name === "down" ? (slashSel + 1 + n) % n : (slashSel - 1 + n) % n;
319
+ }
320
+ rl._refreshLine();
321
+ drawSlashHint(slashPrefix, slashSel);
322
+ return;
323
+ }
324
+ if (rl.line.startsWith("/")) {
325
+ slashPrefix = rl.line;
326
+ drawSlashHint(rl.line, -1); // typing resets the selection to none
327
+ } else {
328
+ clearSlashHint();
329
+ }
291
330
  });
292
331
  }
293
332
  rl.on("close", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.211",
3
+ "version": "1.0.213",
4
4
  "description": "Polls GoNext cloud API for async local LLM jobs and runs them against Ollama/OpenAI-compatible servers on this Mac",
5
5
  "type": "module",
6
6
  "license": "MIT",