agentlas 1.0.9 → 1.0.11

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.11 — 2026-07-27
4
+
5
+ - **The slash palette repaints in place instead of stacking copies of
6
+ itself.** Every keystroke left the previous frame on screen, so a few
7
+ arrow presses filled the terminal with duplicate palettes and pushed the
8
+ prompt out of view. Two causes, both measured on an emulated terminal:
9
+ the frame was drawn with the cursor saved and restored by absolute
10
+ position, and the frame was 18 lines tall with no regard for the window.
11
+ In a REPL the prompt sits at the bottom of the screen, so drawing below
12
+ it always scrolls — and after a scroll the saved absolute row points at
13
+ different content, so the next repaint erased the wrong region and left
14
+ the old frame behind. The palette now returns to the prompt with a
15
+ relative cursor move, which survives scrolling, and never draws a frame
16
+ taller than the window: the list shrinks around the highlighted entry,
17
+ and the selection detail folds away before the list does, so the
18
+ highlighted row and the controls hint survive at any height. Verified at
19
+ 14, 18, 24 and 50 rows — one palette on screen, prompt intact.
20
+ - Quitting no longer announces a wait for commands that finish in
21
+ milliseconds; the notice now appears only after 400ms.
22
+
23
+ ## 1.0.10 — 2026-07-27
24
+
25
+ Four defects in the REPL's slash surface, all found by sweeping for the shape
26
+ that produced 1.0.8: a v2 caller using a v1-era contract.
27
+
28
+ - **Quoted arguments survive.** Slash arguments were split on whitespace, so
29
+ the quotes the palette itself advertises (`/search "<what you need>"`) were
30
+ passed through as part of the query — `/search "hello world"` searched for
31
+ `"hello`. The quote-aware tokenizer the top-level CLI uses was exported but
32
+ had no call sites; the REPL now uses it.
33
+ - **Aliases work inside the REPL.** `agentlas hep-network …` was accepted
34
+ while `/hep-network …` answered "unknown", because alias resolution lived
35
+ only in the top-level dispatcher. Both surfaces now resolve the same names,
36
+ and a test pins every alias to a command that actually exists.
37
+ - **A command issued just before `/quit` is no longer discarded.** Slash
38
+ commands are async and were fire-and-forget, so closing the prompt resolved
39
+ immediately and the process exited mid-flight: `/search …` followed by
40
+ `/quit` printed nothing at all, while the same pair typed 25 seconds apart
41
+ worked. In-flight commands are now awaited — bounded at 30s, so quitting
42
+ can never hang — and the wait is announced rather than silent.
43
+ - **The first-run wizard's language applies to the whole session.** Choosing a
44
+ language wrote it to preferences and to `ui.lang`, but not to `ctx.lang`, so
45
+ the banner switched while `/help`, the palette, orchestrator notices and the
46
+ shortcut hints stayed in the OS-locale language until the next launch.
47
+
3
48
  ## 1.0.9 — 2026-07-27
4
49
 
5
50
  Three repairs of one mistake, found by a live run that a 4-agent task force
@@ -509,31 +509,56 @@ function renderSlashPalette(rows, selectedIndex, opts = {}) {
509
509
  );
510
510
  const descWidth = Math.max(0, lineWidth - commandWidth - 1);
511
511
  const selected = rows[Math.max(0, Math.min(selectedIndex, rows.length - 1))] || rows[0];
512
- const out = [
512
+ const head = [
513
513
  c.faint(truncateVisible(`${i18n.t(lang, "palette.title")} ${i18n.t(lang, "palette.search")}`, lineWidth)),
514
514
  c.faint("─".repeat(lineWidth)),
515
515
  ];
516
- rows.forEach((row, index) => {
517
- const command = padVisible(truncateVisible(row.command, commandWidth), commandWidth);
518
- const desc = truncateVisible(row.description, descWidth);
519
- const body = " " + c.blue(command) + c.text(desc);
520
- out.push(index === selectedIndex ? c.inverse(padVisible(body, lineWidth)) : body);
521
- });
522
- out.push(c.faint("─".repeat(lineWidth)));
516
+ /*
517
+ * 꼬리(구분선·선택 상세·조작 안내) 먼저 만든다 — 목록만 예산에 맞춰 줄이고
518
+ * 머리와 꼬리는 어떤 높이에서도 지키기 위해서다.
519
+ */
520
+ const tail = [c.faint("─".repeat(lineWidth))];
521
+ const tailStart = tail.length; // 이 뒤는 자리가 모자라면 접는다 (상세 → 예시 순으로 버림)
522
+ const out = head;
523
523
  if (selected) {
524
524
  const usage = truncateVisible(selected.usage || selected.command, lineWidth - 2);
525
525
  const detail = truncateVisible(selected.detail || selected.description || "", lineWidth - 2);
526
526
  const category = selected.category ? i18n.t(lang, "palette.category", selected.category) : "";
527
527
  const categoryRoom = Math.max(0, lineWidth - visibleWidthLite(usage) - 3);
528
528
  const categoryText = categoryRoom > 0 ? truncateVisible(category, categoryRoom) : "";
529
- out.push(" " + c.text(usage) + (categoryText ? c.dim(" " + categoryText) : ""));
530
- if (detail) out.push(" " + c.dim(detail));
529
+ tail.push(" " + c.text(usage) + (categoryText ? c.dim(" " + categoryText) : ""));
530
+ if (detail) tail.push(" " + c.dim(detail));
531
531
  if (selected.examples && selected.examples.length) {
532
- out.push(c.dim(truncateVisible(" " + i18n.t(lang, "palette.examples", selected.examples.slice(0, 2).join(" | ")), lineWidth)));
532
+ tail.push(c.dim(truncateVisible(" " + i18n.t(lang, "palette.examples", selected.examples.slice(0, 2).join(" | ")), lineWidth)));
533
533
  }
534
534
  }
535
- out.push(c.dim(truncateVisible(" " + i18n.t(lang, "palette.controls"), lineWidth)));
536
- return out.join("\n");
535
+ /*
536
+ * 화면 높이 예산. 터미널보다 긴 프레임을 쏟으면 그리는 도중 스크롤이 나고, 그 순간
537
+ * 오버레이가 제자리를 잃어 이전 프레임이 화면에 남는다(실측: 24행에서 18행짜리
538
+ * 프레임 → 블록이 겹겹이 쌓이고 프롬프트가 화면 밖으로 밀려남).
539
+ * 선택 항목이 잘려 나가지 않도록 강조 위치를 중심으로 창을 잡는다.
540
+ */
541
+ const controls = c.dim(truncateVisible(" " + i18n.t(lang, "palette.controls"), lineWidth));
542
+ const budget = Math.max(1, Math.floor(Number(opts.maxRows) || (rows.length + head.length + tail.length + 1)));
543
+ /*
544
+ * 자리가 모자라면 선택 상세부터 접는다. 목록 한 줄과 조작 안내는 마지막까지 지킨다 —
545
+ * 아무것도 못 고르는 팔레트나 나가는 법을 모르는 팔레트는 없느니만 못하다.
546
+ * 최소 프레임은 5줄(제목·구분선·목록 1줄·구분선·조작 안내)이며, 그보다 좁은 예산도 5줄이다.
547
+ */
548
+ while (tail.length > tailStart && budget - head.length - tail.length - 1 < 1) tail.pop();
549
+ const listBudget = Math.max(1, budget - head.length - tail.length - 1);
550
+ const start = Math.min(
551
+ Math.max(0, selectedIndex - listBudget + 1),
552
+ Math.max(0, rows.length - listBudget),
553
+ );
554
+ rows.slice(start, start + listBudget).forEach((row, offset) => {
555
+ const index = start + offset;
556
+ const command = padVisible(truncateVisible(row.command, commandWidth), commandWidth);
557
+ const desc = truncateVisible(row.description, descWidth);
558
+ const body = " " + c.blue(command) + c.text(desc);
559
+ out.push(index === selectedIndex ? c.inverse(padVisible(body, lineWidth)) : body);
560
+ });
561
+ return out.concat(tail, [controls]).join("\n");
537
562
  }
538
563
 
539
564
  function attachSlashPalette(rl, opts = {}) {
@@ -574,9 +599,25 @@ function attachSlashPalette(rl, opts = {}) {
574
599
  rl.write(null, { ctrl: true, name: "u" });
575
600
  rl.write(value);
576
601
  }
602
+ /*
603
+ * 커서 복원은 상대 이동으로만 한다.
604
+ *
605
+ * 예전 구현은 DECSC/DECRC(`\x1b7`/`\x1b8`)로 절대 위치를 저장·복원했다. 그런데 REPL은
606
+ * 프롬프트가 화면 맨 아래에 있는 게 보통이라, 그 아래로 프레임을 그리면 반드시 스크롤이
607
+ * 난다. 스크롤 뒤 저장된 절대 행은 다른 내용을 가리키므로 복원이 어긋나고, 다음 렌더의
608
+ * "커서 아래 전부 지우기"가 이전 프레임을 못 지운다 — 화면에 팔레트가 겹겹이 쌓였다.
609
+ * (pyte 에뮬레이션 실측: 24행에서 ↓ 3회 → 잔상 블록 + 프롬프트 유실.)
610
+ * `\x1b[nA` 같은 상대 이동은 내용과 함께 밀리므로 스크롤이 나도 어긋나지 않는다.
611
+ */
612
+ function promptColumn() {
613
+ const prompt = typeof rl.getPrompt === "function" ? rl.getPrompt() : "";
614
+ const typed = String(rl.line || "").slice(0, rl.cursor);
615
+ return visibleWidthLite(String(prompt)) + visibleWidthLite(typed) + 1;
616
+ }
577
617
  function clear() {
578
618
  if (!state.visible) return;
579
- stream.write("\x1b7\x1b[E\x1b[0J\x1b8");
619
+ // 그린 뒤에는 프롬프트 아래에 자리가 있으므로 커서 아래로 이동은 스크롤을 만들지 않는다.
620
+ stream.write(`\x1b[1B\r\x1b[0J\x1b[1A\x1b[${promptColumn()}G`);
580
621
  state.visible = false;
581
622
  }
582
623
  function render() {
@@ -596,10 +637,16 @@ function attachSlashPalette(rl, opts = {}) {
596
637
  if (state.selected < 0 || state.selected >= list.length) state.selected = 0;
597
638
  const body = renderSlashPalette(list, state.selected, {
598
639
  columns: stream.columns || process.stdout.columns || 88,
640
+ // 프롬프트 줄과 여유 한 줄을 남긴다 — 프레임이 화면을 다 먹으면 제자리 갱신이 불가능하다.
641
+ maxRows: Math.max(5, (stream.rows || process.stdout.rows || 24) - 2),
599
642
  colors,
600
643
  lang: opts.lang || (opts.ui && opts.ui.lang) || "en",
601
644
  });
602
- stream.write("\x1b7\x1b[E\x1b[0J" + body + "\x1b8");
645
+ if (!body) { clear(); return; }
646
+ const lines = body.split("\n");
647
+ // 첫 줄바꿈은 프롬프트 아래로 내려가며, 자리가 없으면 여기서 화면이 한 번 밀린다.
648
+ // 그린 만큼 그대로 되올라오므로 이후 갱신은 제자리에서 일어난다.
649
+ stream.write(`\r\n\x1b[0J${lines.join("\r\n")}\x1b[${lines.length}A\x1b[${promptColumn()}G`);
603
650
  state.visible = true;
604
651
  }
605
652
  /*
@@ -23,6 +23,7 @@ const { findAgent, listAgents } = require("../agents/registry.cjs");
23
23
  const { resolveRuntime, NoRuntimeError } = require("../runtimes/resolve.cjs");
24
24
  const permissions = require("../agentlas-permissions.cjs");
25
25
  const i18n = require("../agentlas-i18n.cjs");
26
+ const { tokenizeCommandLine } = require("../agentlas-input.cjs");
26
27
 
27
28
  const DEFAULT_AGENT_SLUG = "agentlas-orchestrator";
28
29
 
@@ -90,7 +91,8 @@ function pickDefaultAgent(db) {
90
91
  }
91
92
 
92
93
  async function startRepl(ctx, opts = {}) {
93
- const en = ctx.lang === "en";
94
+ // 마법사가 언어를 바꾸면 뒤의 문구도 따라가야 한다 — 아래 온보딩 블록에서 갱신한다.
95
+ let en = ctx.lang === "en";
94
96
  const ui = ctx.uiInstance;
95
97
  const db = ctx.db();
96
98
 
@@ -103,7 +105,17 @@ async function startRepl(ctx, opts = {}) {
103
105
  const { runWizard } = require("../commands/setup.cjs");
104
106
  const result = await runWizard(ctx, wizardRl);
105
107
  if (result) {
106
- if (result.lang) { ctx.prefs.language = result.lang; }
108
+ /*
109
+ * 고른 언어를 이번 세션에도 즉시 반영한다. 예전에는 prefs 에만 적어서,
110
+ * runOnboard 가 손댄 ui.lang 덕에 배너만 새 언어로 나오고 /help·팔레트·
111
+ * 오케스트레이터·단축키 안내는 재시작 전까지 OS 로케일 언어로 남았다.
112
+ */
113
+ if (result.lang) {
114
+ ctx.prefs.language = result.lang;
115
+ ctx.lang = result.lang;
116
+ ui.lang = result.lang;
117
+ en = ctx.lang === "en";
118
+ }
107
119
  if (result.permission) ctx.prefs.permission = result.permission;
108
120
  if (result.runtime) ctx.prefs.runtime = result.runtime;
109
121
  ctx.prefs.onboarded = !!result.onboarded;
@@ -186,6 +198,13 @@ async function startRepl(ctx, opts = {}) {
186
198
  getSessionKeys: () => orch.list().map((r) => r.key),
187
199
  getCwd: () => process.cwd(),
188
200
  });
201
+ // 진행 중인 비동기 슬래시 명령 — 종료가 이걸 잘라먹지 않도록 close 핸들러가 기다린다.
202
+ const pendingCommands = new Set();
203
+ const trackCommand = (promise) => {
204
+ pendingCommands.add(promise);
205
+ promise.then(() => pendingCommands.delete(promise), () => pendingCommands.delete(promise));
206
+ return promise;
207
+ };
189
208
  // Shift-Tab 이 온 턴에는 완성 후보를 비운다 — 아래 권한 순환 주석 참고.
190
209
  let swallowCompletion = false;
191
210
  const rl = readline.createInterface({
@@ -299,7 +318,7 @@ async function startRepl(ctx, opts = {}) {
299
318
 
300
319
  if (input.startsWith("/")) {
301
320
  try {
302
- const quit = handleSlash(ctx, input.slice(1), { orch, renderer, ensureMainSession, resolveRt, setPermission: (p) => { permission = p; }, getPermission: () => permission, setRuntime: (r) => { runtimeOverride = r; } });
321
+ const quit = handleSlash(ctx, input.slice(1), { orch, renderer, ensureMainSession, resolveRt, track: trackCommand, setPermission: (p) => { permission = p; }, getPermission: () => permission, setRuntime: (r) => { runtimeOverride = r; } });
303
322
  if (quit === "quit") { rl.close(); return; }
304
323
  } catch (e) {
305
324
  ui.error(String((e && e.message) || e));
@@ -341,9 +360,28 @@ async function startRepl(ctx, opts = {}) {
341
360
  process.stdin.removeListener("keypress", onShortcutKey);
342
361
  slashPalette.detach();
343
362
  renderer.detach();
344
- orch.shutdown();
345
- ui.ensureNl();
346
- resolve(0);
363
+ /*
364
+ * 슬래시 명령은 비동기다. 예전에는 close 가 곧바로 resolve 해서 프로세스가 끝나 버렸고,
365
+ * `/search …` 직후 `/quit` 을 치면 그 명령이 출력 한 줄 없이 사라졌다(실측: 같은 입력을
366
+ * 25초 벌려 치면 결과가 나온다). 진행 중인 명령을 먼저 기다린다.
367
+ *
368
+ * 다만 무한정 기다리지는 않는다 — 종료를 누른 사용자를 응답 없는 프로세스에 가둘 수 없다.
369
+ */
370
+ const finish = () => { orch.shutdown(); ui.ensureNl(); resolve(0); };
371
+ if (!pendingCommands.size) { finish(); return; }
372
+ let settled = false;
373
+ const once = () => { if (settled) return; settled = true; clearTimeout(notice); finish(); };
374
+ // 곧 끝나는 명령까지 매번 고지하면 종료 화면이 시끄러워진다 — 실제로 기다릴 때만 알린다.
375
+ const notice = setTimeout(() => {
376
+ ui.ensureNl();
377
+ ui.line(ui.c.dim(en
378
+ ? `finishing ${pendingCommands.size} command(s)…`
379
+ : `실행 중인 명령 ${pendingCommands.size}개를 마무리하는 중…`));
380
+ }, 400);
381
+ if (notice.unref) notice.unref();
382
+ const cap = setTimeout(once, 30_000);
383
+ if (cap.unref) cap.unref();
384
+ Promise.allSettled([...pendingCommands]).then(() => { clearTimeout(cap); once(); }, once);
347
385
  });
348
386
 
349
387
  prompt();
@@ -448,8 +486,19 @@ function handleSlash(ctx, cmdline, api) {
448
486
  const en = ctx.lang === "en";
449
487
  const ui = ctx.uiInstance;
450
488
  const { orch, renderer, ensureMainSession } = api;
451
- const [cmd, ...rest] = cmdline.split(/\s+/);
452
- const restStr = cmdline.slice(cmd.length).trim();
489
+ const commands = require("../commands/index.cjs");
490
+ /*
491
+ * 인자는 따옴표를 인식해 쪼갠다. 공백 분해는 따옴표를 인자 안에 그대로 남겨,
492
+ * 팔레트가 안내하는 그대로 `/search "무엇이 필요한지"` 를 치면 따옴표째 검색어가 됐다.
493
+ * 최상위 CLI 와 같은 토크나이저를 쓴다. restStr 은 원문 꼬리를 그대로 넘기는 자리
494
+ * (/spawn·/steer·/broadcast)라 계속 원문에서 자른다.
495
+ */
496
+ const rawCmd = cmdline.split(/\s+/)[0] || "";
497
+ const rest = tokenizeCommandLine(cmdline).slice(1);
498
+ const restStr = cmdline.slice(rawCmd.length).trim();
499
+ // 별칭도 최상위 CLI 와 동일하게 해석한다 — `agentlas hep-network` 는 되는데
500
+ // `/hep-network` 는 "알 수 없는 명령" 이던 비대칭을 없앤다.
501
+ const cmd = commands.resolveCommandName(rawCmd);
453
502
 
454
503
  switch (cmd) {
455
504
  case "quit": case "exit": return "quit";
@@ -564,11 +613,11 @@ function handleSlash(ctx, cmdline, api) {
564
613
  */
565
614
  // help/agents/list/chats/mcp/doctor 등은 위 케이스에서 이미 처리된다.
566
615
  const REPL_EXCLUDED = new Set(["chat", "open", "firm", "setup", "run"]);
567
- const commands = require("../commands/index.cjs");
568
616
  if (!REPL_EXCLUDED.has(cmd) && commands.COMMANDS[cmd]) {
569
617
  const result = commands.COMMANDS[cmd]().run(ctx, rest);
570
- if (result && typeof result.catch === "function") {
571
- result.catch((e) => ctx.err(String((e && e.message) || e)));
618
+ if (result && typeof result.then === "function") {
619
+ // 진행 중임을 REPL 알아야 종료가 이걸 잘라먹지 않는다 (close 핸들러 참고).
620
+ api.track(result.catch((e) => ctx.err(String((e && e.message) || e))));
572
621
  }
573
622
  return;
574
623
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentlas",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "Agentlas agent terminal — chat with your installed AI agents and teams from the terminal, Claude Code style. Standalone: no desktop app required.",
5
5
  "bin": {
6
6
  "agentlas": "bin/agentlas.cjs"