@tiens.nguyen/gonext-local-worker 1.0.210 → 1.0.212

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 +83 -21
  2. package/package.json +1 -1
package/gonext-repl.mjs CHANGED
@@ -175,20 +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 printCommands(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
- console.log(dim(" commands (Tab completes):"));
187
- for (const c of show.length ? show : COMMANDS) {
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) => {
188
193
  const label = c.usage ?? c.name;
189
194
  const alias = c.aliases?.length ? dim(` (${c.aliases.join(", ")})`) : "";
190
- console.log(` ${cyan(label)}${alias}${dim(" — " + c.desc)}`);
191
- }
195
+ const body = `${cyan(label)}${alias}${dim(" — " + c.desc)}`;
196
+ rows.push(i === sel ? `${green("❯")} ${bold(body)}` : ` ${body}`);
197
+ });
198
+ return rows;
199
+ }
200
+ // Permanent print (used by /help and a lone-"/" submit — scrolls with history).
201
+ function printCommands(prefix = "") {
202
+ for (const r of hintRows(filteredCommands(prefix), -1)) console.log(r);
192
203
  console.log("");
193
204
  }
194
205
 
@@ -221,6 +232,10 @@ const isNetworkError = (e) =>
221
232
  const _lineQueue = [];
222
233
  let _lineWaiter = null;
223
234
  let _stdinClosed = false;
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)
224
239
  rl.on("line", (l) => {
225
240
  // While a turn is running, IGNORE typed input entirely — no type-ahead, no queued
226
241
  // next question. Only Ctrl+C (handled via SIGINT) does anything mid-turn. Clear the
@@ -230,7 +245,18 @@ rl.on("line", (l) => {
230
245
  rl.cursor = 0;
231
246
  return;
232
247
  }
233
- const clean = stripAnsiEscapes(l);
248
+ // Enter left the cursor on the row where the live slash-hint started — erase it so the
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;
252
+ if (slashHintRows > 0) {
253
+ if (slashSel >= 0 && slashNames[slashSel]) submitted = slashNames[slashSel];
254
+ process.stdout.write("\x1b[J");
255
+ slashHintRows = 0;
256
+ slashSel = -1;
257
+ slashNames = [];
258
+ }
259
+ const clean = stripAnsiEscapes(submitted);
234
260
  if (_lineWaiter) {
235
261
  const w = _lineWaiter;
236
262
  _lineWaiter = null;
@@ -243,27 +269,63 @@ rl.on("line", (l) => {
243
269
  // turn: echo is already muted, but readline still recalls history into the invisible
244
270
  // buffer on ↑/↓ — reset the line state after every keypress so nothing accumulates or
245
271
  // moves. Ctrl+C is unaffected (readline emits its SIGINT before this handler runs).
246
- let slashHintShown = false; // the live command hint is currently displayed for a "/…" line
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").)
277
+ const inputCol = () => 4 + rl.cursor; // ">> " is 3 cols; content starts at col 4 (1-based)
278
+ function clearSlashHint() {
279
+ if (slashHintRows === 0) return;
280
+ process.stdout.write("\n\x1b[J" + `\x1b[1A\x1b[${inputCol()}G`);
281
+ slashHintRows = 0;
282
+ slashSel = -1;
283
+ slashNames = [];
284
+ }
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);
291
+ const body = rows.map((r) => "\x1b[K" + r).join("\n");
292
+ let out = "";
293
+ if (slashHintRows > 0) out += "\n\x1b[J\x1b[1A"; // wipe previous block, back to input row
294
+ out += "\n" + body; // draw below the input line
295
+ out += `\x1b[${rows.length}A\x1b[${inputCol()}G`; // back up to input line + column
296
+ process.stdout.write(out);
297
+ slashHintRows = rows.length;
298
+ }
247
299
  if (process.stdin.isTTY) {
248
- process.stdin.on("keypress", () => {
300
+ process.stdin.on("keypress", (_ch, key) => {
249
301
  if (following) {
250
302
  rl.line = "";
251
303
  rl.cursor = 0;
252
304
  rl.historyIndex = -1;
253
305
  return;
254
306
  }
255
- // Live command hint: the moment the line starts with "/", pop the command list ABOVE
256
- // the prompt (once) so the user sees /model etc. without typing the whole thing or
257
- // pressing Tab. Shown once per "/…" run; cleared/reset when the line stops being a
258
- // slash command (so a fresh "/" shows it again).
259
- const l = rl.line;
260
- if (l.startsWith("/") && !slashHintShown) {
261
- slashHintShown = true;
262
- process.stdout.write("\r\x1b[K"); // wipe the ">> /" input row
263
- printCommands(l); // list where the prompt was + below
264
- rl._refreshLine(); // redraw ">> /" (with cursor) beneath the list
265
- } else if (!l.startsWith("/")) {
266
- slashHintShown = false;
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();
267
329
  }
268
330
  });
269
331
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.210",
3
+ "version": "1.0.212",
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",