agent-mp 0.5.23 → 0.5.25

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.
@@ -1,4 +1,3 @@
1
- import * as readline from 'readline';
2
1
  import { CliInfo } from '../types.js';
3
2
  import { Session } from '../utils/sessions.js';
4
3
  /**
@@ -17,7 +16,6 @@ export declare function initCoordinator(): Promise<{
17
16
  info: CliInfo;
18
17
  path: string;
19
18
  }>;
20
- rl: readline.Interface;
21
19
  } | null>;
22
20
  /** REPL mode — interactive loop */
23
21
  export declare function runRepl(resumeSession?: Session): Promise<void>;
@@ -795,60 +795,59 @@ export async function initCoordinator() {
795
795
  input: process.stdin,
796
796
  output: process.stdout,
797
797
  });
798
- // Step 1: Detect installed CLIs
799
- const installed = detectInstalledClis();
800
- if (installed.length === 0) {
801
- console.log(chalk.red(' No CLIs detected. Install at least one: qwen, claude, gemini, codex'));
802
- rl.close();
803
- return null;
804
- }
805
- // Step 2: Pick coordinator
806
- const auth = await loadAuth();
807
- const currentAuth = auth.activeProvider;
808
- let activeCli = installed.find((c) => c.name === currentAuth);
809
- // ── Fast-path: API key configured ────────────────────────────────────────
810
- const apiKeyCfg = await loadApiKeyConfig();
811
- if (apiKeyCfg?.api_key) {
812
- const cliCfg = await loadCliConfig();
813
- const model = cliCfg.coordinatorModel || apiKeyCfg.model;
814
- gCoordinatorCmd = `qwen-direct -m ${model}`;
815
- console.log(chalk.green(` ✓ Auth: ${apiKeyCfg.provider} / ${model}\n`));
816
- const syntheticCli = {
817
- name: apiKeyCfg.provider,
818
- info: { command: 'qwen-direct', modelFlag: '-m', promptFlag: '-p', description: 'API key' },
819
- path: 'qwen-direct',
820
- };
821
- return { coordinatorCmd: gCoordinatorCmd, activeCli: syntheticCli, installed, rl };
822
- }
823
- // ── No API key — prompt to configure one ─────────────────────────────────
824
- console.log(chalk.yellow('\n No auth configured.'));
825
- console.log(chalk.dim(' Configure an API key to continue.\n'));
826
- const doSetup = await new Promise((resolve) => {
827
- rl.question(' Set up API key now? (y/N): ', (a) => resolve(a.trim().toLowerCase() === 'y'));
828
- });
829
- if (doSetup) {
830
- const askFn = (q) => new Promise((resolve) => rl.question(q, resolve));
831
- const ok = await promptApiKeySetup(rl, askFn);
832
- if (!ok) {
833
- rl.close();
798
+ try {
799
+ // Step 1: Detect installed CLIs
800
+ const installed = detectInstalledClis();
801
+ if (installed.length === 0) {
802
+ console.log(chalk.red(' No CLIs detected. Install at least one: qwen, claude, gemini, codex'));
834
803
  return null;
835
804
  }
836
- const saved = await loadApiKeyConfig();
837
- if (!saved) {
838
- rl.close();
839
- return null;
805
+ // Step 2: Pick coordinator
806
+ const auth = await loadAuth();
807
+ const currentAuth = auth.activeProvider;
808
+ let activeCli = installed.find((c) => c.name === currentAuth);
809
+ // Fast-path: API key configured
810
+ const apiKeyCfg = await loadApiKeyConfig();
811
+ if (apiKeyCfg?.api_key) {
812
+ const cliCfg = await loadCliConfig();
813
+ const model = cliCfg.coordinatorModel || apiKeyCfg.model;
814
+ gCoordinatorCmd = `qwen-direct -m ${model}`;
815
+ console.log(chalk.green(` ✓ Auth: ${apiKeyCfg.provider} / ${model}\n`));
816
+ const syntheticCli = {
817
+ name: apiKeyCfg.provider,
818
+ info: { command: 'qwen-direct', modelFlag: '-m', promptFlag: '-p', description: 'API key' },
819
+ path: 'qwen-direct',
820
+ };
821
+ return { activeCli: syntheticCli, installed, coordinatorCmd: gCoordinatorCmd };
840
822
  }
841
- gCoordinatorCmd = `qwen-direct -m ${saved.model}`;
842
- const syntheticCli = {
843
- name: saved.provider,
844
- info: { command: 'qwen-direct', modelFlag: '-m', promptFlag: '-p', description: 'API key' },
845
- path: 'qwen-direct',
846
- };
847
- return { coordinatorCmd: gCoordinatorCmd, activeCli: syntheticCli, installed, rl };
823
+ // No API key — prompt to configure one
824
+ console.log(chalk.yellow('\n No auth configured.'));
825
+ console.log(chalk.dim(' Configure an API key to continue.\n'));
826
+ const doSetup = await new Promise((resolve) => {
827
+ rl.question(' Set up API key now? (y/N): ', (a) => resolve(a.trim().toLowerCase() === 'y'));
828
+ });
829
+ if (doSetup) {
830
+ const askFn = (q) => new Promise((resolve) => rl.question(q, resolve));
831
+ const ok = await promptApiKeySetup(rl, askFn);
832
+ if (!ok)
833
+ return null;
834
+ const saved = await loadApiKeyConfig();
835
+ if (!saved)
836
+ return null;
837
+ gCoordinatorCmd = `qwen-direct -m ${saved.model}`;
838
+ const syntheticCli = {
839
+ name: saved.provider,
840
+ info: { command: 'qwen-direct', modelFlag: '-m', promptFlag: '-p', description: 'API key' },
841
+ path: 'qwen-direct',
842
+ };
843
+ return { coordinatorCmd: gCoordinatorCmd, activeCli: syntheticCli, installed };
844
+ }
845
+ console.log(chalk.dim(' Run: agent-mp setup api-key'));
846
+ return null;
847
+ }
848
+ finally {
849
+ rl.close();
848
850
  }
849
- console.log(chalk.dim(' Run: agent-mp setup api-key'));
850
- rl.close();
851
- return null;
852
851
  }
853
852
  /** REPL mode — interactive loop */
854
853
  export async function runRepl(resumeSession) {
@@ -856,7 +855,6 @@ export async function runRepl(resumeSession) {
856
855
  if (!init)
857
856
  return;
858
857
  const { activeCli } = init;
859
- const rl = init.rl;
860
858
  const session = resumeSession ?? newSession(process.cwd());
861
859
  // Resolve actual project root (may be deeper than cwd)
862
860
  const projectDir = await resolveProjectDir(process.cwd());
@@ -879,10 +877,48 @@ export async function runRepl(resumeSession) {
879
877
  }
880
878
  }
881
879
  catch { }
880
+ // Fixed-bottom input — owns the last terminal rows
881
+ const fi = new FixedInput();
882
+ fi.setup();
883
+ // Helper: run a wizard command that needs readline (suspends fixed input)
884
+ const withRl = async (fn) => {
885
+ const resume = fi.suspend();
886
+ const tempRl = readline.createInterface({
887
+ input: process.stdin,
888
+ output: process.stdout,
889
+ });
890
+ try {
891
+ await fn(tempRl);
892
+ }
893
+ finally {
894
+ tempRl.close();
895
+ resume();
896
+ }
897
+ };
898
+ // Helper to create engine with readline (suspends fixed input)
899
+ const withEngine = async (fn) => {
900
+ const resume = fi.suspend();
901
+ const tempRl = readline.createInterface({
902
+ input: process.stdin,
903
+ output: process.stdout,
904
+ });
905
+ try {
906
+ const dir = await resolveProjectDir(process.cwd());
907
+ const config = await loadProjectConfig(dir);
908
+ const engine = new AgentEngine(config, dir, gCoordinatorCmd, tempRl, fi, handleCmd);
909
+ return await fn(engine);
910
+ }
911
+ finally {
912
+ tempRl.close();
913
+ resume();
914
+ }
915
+ };
882
916
  if (!hasRoles) {
883
917
  console.log(chalk.yellow('\n Todos los roles deben estar configurados antes de trabajar.'));
884
918
  console.log(chalk.yellow(' Configurando roles ahora...\n'));
885
- await cmdConfigMulti(rl);
919
+ await withRl(async (tempRl) => {
920
+ await cmdConfigMulti(tempRl);
921
+ });
886
922
  // Re-check
887
923
  try {
888
924
  const config = await loadProjectConfig(process.cwd());
@@ -900,7 +936,6 @@ export async function runRepl(resumeSession) {
900
936
  catch { }
901
937
  if (!hasRoles) {
902
938
  console.log(chalk.red('\n Error: roles no configurados. Saliendo.'));
903
- rl.close();
904
939
  return;
905
940
  }
906
941
  }
@@ -932,9 +967,6 @@ export async function runRepl(resumeSession) {
932
967
  }
933
968
  }
934
969
  catch { }
935
- // Fixed-bottom input — owns the last 3 terminal rows permanently
936
- const fi = new FixedInput();
937
- fi.setup();
938
970
  // If resuming, print past conversation history
939
971
  if (resumeSession && resumeSession.messages.length > 0) {
940
972
  const date = new Date(resumeSession.createdAt).toLocaleString();
@@ -954,16 +986,6 @@ export async function runRepl(resumeSession) {
954
986
  fi.println(chalk.dim(' ─── Continue below ───'));
955
987
  fi.println('');
956
988
  }
957
- // Helper: run a wizard command that needs readline (suspends fixed input)
958
- const withRl = async (fn) => {
959
- const resume = fi.suspend();
960
- try {
961
- await fn(rl);
962
- }
963
- finally {
964
- resume();
965
- }
966
- };
967
989
  // Command handler — used by the main loop AND by the engine during mid-conversation
968
990
  const handleCmd = async (trimmed) => {
969
991
  const parts = trimmed.slice(1).split(/\s+/);
@@ -991,58 +1013,53 @@ export async function runRepl(resumeSession) {
991
1013
  await cmdStatus(fi);
992
1014
  break;
993
1015
  case 'run': {
994
- const dir = await resolveProjectDir(process.cwd());
995
- const config = await loadProjectConfig(dir);
996
- if (args[0] === 'orch' || args[0] === 'orchestrator') {
997
- // Ensure documentation exists before orchestrator runs
998
- const contextDir = path.join(dir, '.agent', 'context');
999
- const archPath = path.join(contextDir, 'architecture.md');
1000
- if (!(await fileExists(archPath))) {
1001
- fi.println(chalk.yellow('\n ── Generando documentación del proyecto ──'));
1002
- fi.println(chalk.dim(' El orquestador necesita conocer la estructura del proyecto primero.\n'));
1003
- const prepEngine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1004
- await prepEngine.runExplorer('Document the complete project structure, services, dependencies, and entry points. Create/update .agent/context/architecture.md');
1005
- fi.println(chalk.green(' ✓ Documentación del proyecto generada.\n'));
1016
+ await withEngine(async (engine) => {
1017
+ const dir = await resolveProjectDir(process.cwd());
1018
+ const config = await loadProjectConfig(dir);
1019
+ if (args[0] === 'orch' || args[0] === 'orchestrator') {
1020
+ // Ensure documentation exists before orchestrator runs
1021
+ const contextDir = path.join(dir, '.agent', 'context');
1022
+ const archPath = path.join(contextDir, 'architecture.md');
1023
+ if (!(await fileExists(archPath))) {
1024
+ fi.println(chalk.yellow('\n ── Generando documentación del proyecto ──'));
1025
+ fi.println(chalk.dim(' El orquestador necesita conocer la estructura del proyecto primero.\n'));
1026
+ await engine.runExplorer('Document the complete project structure, services, dependencies, and entry points. Create/update .agent/context/architecture.md');
1027
+ fi.println(chalk.green(' ✓ Documentación del proyecto generada.\n'));
1028
+ }
1029
+ const task = args.slice(1).join(' ');
1030
+ const result = await engine.runOrchestrator(task);
1031
+ fi.println(chalk.green(` Task ID: ${result.taskId}`));
1006
1032
  }
1007
- const task = args.slice(1).join(' ');
1008
- const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1009
- const result = await engine.runOrchestrator(task);
1010
- fi.println(chalk.green(` Task ID: ${result.taskId}`));
1011
- }
1012
- else if (args[0] === 'impl' || args[0] === 'implementor') {
1013
- const taskId = args[1];
1014
- const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1015
- const taskDir = path.join(dir, '.agent', 'tasks', taskId);
1016
- const plan = await readJson(path.join(taskDir, 'plan.json'));
1017
- await engine.runImplementor(taskId, plan);
1018
- }
1019
- else if (args[0] === 'rev' || args[0] === 'reviewer') {
1020
- const taskId = args[1];
1021
- const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1022
- const taskDir = path.join(dir, '.agent', 'tasks', taskId);
1023
- const plan = await readJson(path.join(taskDir, 'plan.json'));
1024
- const progress = await readJson(path.join(taskDir, 'progress.json'));
1025
- await engine.runReviewer(taskId, plan, progress);
1026
- }
1027
- else if (args[0] === 'exp' || args[0] === 'explorer') {
1028
- const task = args.slice(1).join(' ');
1029
- const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1030
- await engine.runExplorer(task);
1031
- }
1032
- else {
1033
- const task = args.join(' ');
1034
- const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1035
- await engine.runFullCycle(task);
1036
- }
1033
+ else if (args[0] === 'impl' || args[0] === 'implementor') {
1034
+ const taskId = args[1];
1035
+ const taskDir = path.join(dir, '.agent', 'tasks', taskId);
1036
+ const plan = await readJson(path.join(taskDir, 'plan.json'));
1037
+ await engine.runImplementor(taskId, plan);
1038
+ }
1039
+ else if (args[0] === 'rev' || args[0] === 'reviewer') {
1040
+ const taskId = args[1];
1041
+ const taskDir = path.join(dir, '.agent', 'tasks', taskId);
1042
+ const plan = await readJson(path.join(taskDir, 'plan.json'));
1043
+ const progress = await readJson(path.join(taskDir, 'progress.json'));
1044
+ await engine.runReviewer(taskId, plan, progress);
1045
+ }
1046
+ else if (args[0] === 'exp' || args[0] === 'explorer') {
1047
+ const task = args.slice(1).join(' ');
1048
+ await engine.runExplorer(task);
1049
+ }
1050
+ else {
1051
+ const task = args.join(' ');
1052
+ await engine.runFullCycle(task);
1053
+ }
1054
+ });
1037
1055
  break;
1038
1056
  }
1039
1057
  case 'explorer':
1040
1058
  case 'exp': {
1041
- const dir = await resolveProjectDir(process.cwd());
1042
- const config = await loadProjectConfig(dir);
1043
- const task = args.join(' ');
1044
- const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1045
- await engine.runExplorer(task);
1059
+ await withEngine(async (engine) => {
1060
+ const task = args.join(' ');
1061
+ await engine.runExplorer(task);
1062
+ });
1046
1063
  break;
1047
1064
  }
1048
1065
  case 'models':
@@ -1062,7 +1079,6 @@ export async function runRepl(resumeSession) {
1062
1079
  await saveAuth(authStore);
1063
1080
  fi.println(chalk.dim(' Logged out. Credentials cleared.'));
1064
1081
  fi.teardown();
1065
- rl.close();
1066
1082
  throw new ExitError();
1067
1083
  }
1068
1084
  case 'auth-status':
@@ -1086,7 +1102,6 @@ export async function runRepl(resumeSession) {
1086
1102
  case 'quit':
1087
1103
  fi.println(chalk.dim(' Bye!'));
1088
1104
  fi.teardown();
1089
- rl.close();
1090
1105
  throw new ExitError();
1091
1106
  default:
1092
1107
  fi.println(chalk.red(` Unknown command: /${cmd}`));
@@ -1121,17 +1136,17 @@ export async function runRepl(resumeSession) {
1121
1136
  }
1122
1137
  else {
1123
1138
  // Before running the orchestrator, ensure project documentation exists
1124
- const contextDir = path.join(dir, '.agent', 'context');
1125
- const archPath = path.join(contextDir, 'architecture.md');
1126
- if (!(await fileExists(archPath))) {
1127
- fi.println(chalk.yellow('\n ── Generando documentación del proyecto ──'));
1128
- fi.println(chalk.dim(' El orquestador necesita conocer la estructura del proyecto primero.\n'));
1129
- const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1130
- await engine.runExplorer('Document the complete project structure, services, dependencies, and entry points. Create/update .agent/context/architecture.md');
1131
- fi.println(chalk.green(' ✓ Documentación del proyecto generada.\n'));
1132
- }
1133
- const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1134
- await engine.runFullCycle(trimmed);
1139
+ await withEngine(async (engine) => {
1140
+ const contextDir = path.join(dir, '.agent', 'context');
1141
+ const archPath = path.join(contextDir, 'architecture.md');
1142
+ if (!(await fileExists(archPath))) {
1143
+ fi.println(chalk.yellow('\n ── Generando documentación del proyecto ──'));
1144
+ fi.println(chalk.dim(' El orquestador necesita conocer la estructura del proyecto primero.\n'));
1145
+ await engine.runExplorer('Document the complete project structure, services, dependencies, and entry points. Create/update .agent/context/architecture.md');
1146
+ fi.println(chalk.green(' ✓ Documentación del proyecto generada.\n'));
1147
+ }
1148
+ await engine.runFullCycle(trimmed);
1149
+ });
1135
1150
  session.messages.push({ role: 'agent', content: `[task cycle completed for: ${trimmed}]`, ts: new Date().toISOString() });
1136
1151
  await saveSession(session);
1137
1152
  }
@@ -1247,7 +1262,7 @@ export async function runRole(role, arg, model) {
1247
1262
  if (!init)
1248
1263
  process.exit(1);
1249
1264
  const { coordinatorCmd } = init;
1250
- const rl = init.rl;
1265
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1251
1266
  const engine = new AgentEngine(config, dir, coordinatorCmd, rl);
1252
1267
  try {
1253
1268
  switch (role.toLowerCase()) {
@@ -1293,5 +1308,4 @@ export async function runRole(role, arg, model) {
1293
1308
  console.log(chalk.red(` Error: ${err.message}`));
1294
1309
  process.exit(1);
1295
1310
  }
1296
- rl.close();
1297
1311
  }
@@ -69,7 +69,7 @@ function rebuildCmd(cliName, model, flags) {
69
69
  }
70
70
  async function ask(prompt, rl, fi) {
71
71
  if (fi) {
72
- fi.println(chalk.cyan(` ${prompt}`));
72
+ fi.println(chalk.cyan(prompt));
73
73
  const answer = await fi.readLine();
74
74
  // Echo user response right-aligned (chat style, per line)
75
75
  const userLines = answer.trim().split('\n').filter(l => l.trim());
@@ -329,17 +329,25 @@ export class AgentEngine {
329
329
  if (!this.fi)
330
330
  return noop;
331
331
  const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
332
+ const dotFrames = ['· ', '·· ', '···'];
332
333
  let i = 0;
334
+ let ti = 0;
333
335
  const t0 = Date.now();
334
336
  const fi = this.fi;
337
+ let streaming = false;
335
338
  fi.startActivity(`${frames[0]} ${label} 0s`);
339
+ fi.setActivityLines([` ${dotFrames[0]} esperando respuesta...`]);
336
340
  const iv = setInterval(() => {
337
341
  const s = Math.floor((Date.now() - t0) / 1000);
342
+ ti++;
338
343
  fi.updateActivityHeader(`${frames[i++ % frames.length]} ${label} ${s}s`);
339
- }, 100);
344
+ if (!streaming) {
345
+ fi.setActivityLines([` ${dotFrames[ti % dotFrames.length]} esperando respuesta...`]);
346
+ }
347
+ }, 300);
340
348
  return {
341
349
  stop() { clearInterval(iv); fi.stopActivity(); },
342
- push(line) { fi.pushActivity(line); },
350
+ push(_chunk) { streaming = true; },
343
351
  };
344
352
  }
345
353
  /** Extract readable text lines from a qwen/CLI streaming chunk. */
@@ -409,7 +417,7 @@ INSTRUCCIONES:
409
417
  const model = this.coordinatorCmd.match(/(?:-m|--model)\s+(\S+)/)?.[1] || 'coder-model';
410
418
  const sp = this._startSpinner(`coordinador ${model}`);
411
419
  try {
412
- const result = await callQwenAPI(prompt, model, (c) => this._parseChunk(c).forEach(l => sp.push(l)));
420
+ const result = await callQwenAPI(prompt, model, (c) => sp.push(c));
413
421
  sp.stop();
414
422
  return result;
415
423
  }
@@ -632,21 +640,10 @@ INSTRUCCIONES:
632
640
  // Fall through: callQwenAPIFromCreds will use the API key config
633
641
  }
634
642
  const sp = this._startSpinner(`${cliName} ${model}`);
635
- let lineBuf = '';
636
- const onChunk = (delta) => {
637
- lineBuf += delta;
638
- const lines = lineBuf.split('\n');
639
- lineBuf = lines.pop() || '';
640
- for (const l of lines) {
641
- if (l.trim())
642
- sp.push(l.trim());
643
- }
644
- };
643
+ const onChunk = (delta) => sp.push(delta);
645
644
  try {
646
645
  log.info(`${cliName}: calling Qwen API with own credentials (${model})`);
647
646
  const result = await callQwenAPIFromCreds(rolePrompt, model, credsPath, onChunk);
648
- if (lineBuf.trim())
649
- sp.push(lineBuf.trim());
650
647
  sp.stop();
651
648
  return result;
652
649
  }
@@ -703,20 +700,9 @@ INSTRUCCIONES:
703
700
  const fb = this.config.fallback_global;
704
701
  log.warn(`Using global fallback: ${fb.cli} (${fb.model})`);
705
702
  const sp = this._startSpinner(`${fb.cli} ${fb.model} (fallback)`);
706
- let lineBuf = '';
707
- const onChunk = (delta) => {
708
- lineBuf += delta;
709
- const lines = lineBuf.split('\n');
710
- lineBuf = lines.pop() || '';
711
- for (const l of lines) {
712
- if (l.trim())
713
- sp.push(l.trim());
714
- }
715
- };
703
+ const onChunk = (delta) => sp.push(delta);
716
704
  try {
717
705
  const globalResult = await callQwenAPI(rolePrompt, fb.model, onChunk);
718
- if (lineBuf.trim())
719
- sp.push(lineBuf.trim());
720
706
  sp.stop();
721
707
  trackTokens(globalResult, fb.cli, fb.model);
722
708
  return globalResult;
@@ -1648,6 +1634,10 @@ REGLAS DE PATHS:
1648
1634
  content = content.replace(/^```markdown\s*/i, '').replace(/^```\s*$/gm, '').trim();
1649
1635
  if (!content)
1650
1636
  continue;
1637
+ // If the model emitted literal \n instead of real newlines (single-line output), unescape
1638
+ if (!content.includes('\n') && content.includes('\\n')) {
1639
+ content = content.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
1640
+ }
1651
1641
  // All explorer output goes under .agent/context/ — docs/ is manual-only
1652
1642
  const relPath = fileName.replace(/^\.agent\/context\//i, '').replace(/^\/+/, '');
1653
1643
  let targetPath = null;
@@ -1,41 +1,39 @@
1
+ /**
2
+ * Inline prompt, styled like Gemini/Qwen CLI.
3
+ * Draws at the current cursor line; when output arrives we erase the area
4
+ * (line-by-line, \x1b[2K) and redraw below. No absolute positioning.
5
+ */
1
6
  export declare class FixedInput {
2
- private buf;
3
7
  private history;
4
- private histIdx;
5
8
  private origLog;
6
- private _pasting;
7
- private _pasteAccum;
8
- private _drawPending;
9
9
  private _activityHeader;
10
10
  private _activityLines;
11
- private get rows();
11
+ private _inputBuffer;
12
+ private _cursorPos;
13
+ private _pasting;
14
+ private _pasteAccum;
15
+ private _resolveInput?;
16
+ private _inputActive;
17
+ private _areaRows;
18
+ private _cursorRow;
19
+ private _onResize?;
12
20
  get cols(): number;
13
- private get _reservedRows();
14
- private get scrollBottom();
15
- private _contentRows;
16
21
  setup(): void;
17
22
  teardown(): void;
18
- redrawBox(): void;
19
23
  suspend(): () => void;
20
- /** Enter activity mode: show the 5-line log box instead of the input box. */
21
24
  startActivity(header: string): void;
22
- /** Update the header line (spinner frame + elapsed time) without clearing lines. */
23
25
  updateActivityHeader(header: string): void;
24
- /**
25
- * Append a line to the activity log (keeps last ACTIVITY_LINES lines).
26
- * Strips ANSI codes and skips blank or pure-JSON lines.
27
- */
28
26
  pushActivity(rawLine: string): void;
29
- /** Leave activity mode and restore the normal input box. */
27
+ /** Replace all content lines at once (for streaming preview). */
28
+ setActivityLines(lines: string[]): void;
30
29
  stopActivity(): void;
31
- readLine(): Promise<string>;
32
30
  println(text: string): void;
33
31
  printSeparator(): void;
34
- private _scheduleDraw;
35
- private _setScrollRegion;
36
- private _clearReserved;
37
- private _drawBox;
38
- private _drawActivityBox;
39
- private _drawInputBox;
40
- private _wrapText;
32
+ redrawBox(): void;
33
+ readLine(): Promise<string>;
34
+ private _commitPaste;
35
+ /** Build a string that erases the currently-drawn area and leaves the cursor at col 0 of the top row. */
36
+ private _buildClear;
37
+ private _clearArea;
38
+ private _redraw;
41
39
  }
package/dist/ui/input.js CHANGED
@@ -1,347 +1,409 @@
1
1
  import chalk from 'chalk';
2
- // ─── Midas brand colors ──────────────────────────────────────────────────────
3
- const T = (s) => chalk.rgb(0, 185, 180)(s); // teal — borders
4
- const B = (s) => chalk.rgb(30, 110, 185)(s); // blue — prompt arrow
5
- const PREFIX = T('│') + B(' > ');
6
- const PREFIX_CONT = T('') + B(' '); // continuation lines
7
- const PREFIX_COLS = 4; // visual width of "│ > " and "│ "
8
- // Maximum content rows the input box can grow to (Shift+Enter / word-wrap).
9
- const MAX_CONTENT_ROWS = 4;
10
- const ACTIVITY_LINES = 5;
11
- // Reserved rows at the bottom:
12
- // Idle: 7 = 1 status row + up to 4 content + 2 borders
13
- // Active: 10 = 7 (activity box) + 3 (input box: 1 content + 2 borders)
14
- // The scroll region is updated whenever activity mode toggles.
15
- const IDLE_RESERVED = MAX_CONTENT_ROWS + 3; // 7
16
- const ACTIVE_RESERVED = ACTIVITY_LINES + 2 + 3; // 10 = activity(7) + input(3)
17
- // ─── FixedInput ──────────────────────────────────────────────────────────────
2
+ const DIM = chalk.dim;
3
+ const B = (s) => chalk.rgb(30, 110, 185)(s);
4
+ const PROMPT = B('> ');
5
+ const PROMPT_W = 2;
6
+ const INDENT = ' ';
7
+ const PLACEHOLDER = 'Type your message… (Shift+Enter / \\ para nueva línea)';
8
+ /**
9
+ * Inline prompt, styled like Gemini/Qwen CLI.
10
+ * Draws at the current cursor line; when output arrives we erase the area
11
+ * (line-by-line, \x1b[2K) and redraw below. No absolute positioning.
12
+ */
18
13
  export class FixedInput {
19
- buf = '';
20
14
  history = [];
21
- histIdx = -1;
22
15
  origLog;
23
- _pasting = false;
24
- _pasteAccum = '';
25
- _drawPending = false;
26
- // ── Activity box state (null = input mode, string = activity mode) ──────────
27
16
  _activityHeader = null;
28
17
  _activityLines = [];
29
- get rows() { return process.stdout.rows || 24; }
18
+ _inputBuffer = [];
19
+ _cursorPos = 0;
20
+ _pasting = false;
21
+ _pasteAccum = '';
22
+ _resolveInput;
23
+ _inputActive = false;
24
+ // Geometry after the last draw.
25
+ _areaRows = 0; // total rows the area occupies
26
+ _cursorRow = 0; // row (0-indexed from top of area) where terminal cursor sits
27
+ _onResize;
30
28
  get cols() { return process.stdout.columns || 80; }
31
- get _reservedRows() { return this._activityHeader !== null ? ACTIVE_RESERVED : IDLE_RESERVED; }
32
- get scrollBottom() { return this.rows - this._reservedRows; }
33
- _contentRows() {
34
- // During activity mode only 1 content row fits below the activity box
35
- if (this._activityHeader !== null)
36
- return 1;
37
- const w = this.cols - PREFIX_COLS - 2;
38
- if (w <= 0)
39
- return 1;
40
- if (!this.buf)
41
- return 1;
42
- let n = 0;
43
- for (const seg of this.buf.split('\n'))
44
- n += Math.max(1, Math.ceil((seg.length || 1) / w));
45
- return Math.min(n, MAX_CONTENT_ROWS);
46
- }
47
- // ── Lifecycle ──────────────────────────────────────────────────────────────
48
29
  setup() {
49
30
  this.origLog = console.log;
50
31
  console.log = (...args) => {
51
- const text = args.map(a => (typeof a === 'string' ? a : String(a))).join(' ');
32
+ const text = args.map(a => typeof a === 'string' ? a : String(a)).join(' ');
52
33
  this.println(text);
53
34
  };
54
- this._setScrollRegion();
55
- process.stdout.write(`\x1b[${this.scrollBottom};1H`);
56
- this._clearReserved();
57
- this._drawBox();
58
35
  process.stdout.write('\x1b[?2004h');
59
- process.stdout.on('resize', () => {
60
- this._setScrollRegion();
61
- this._clearReserved();
62
- this._drawBox();
63
- });
36
+ this._onResize = () => this._redraw();
37
+ process.stdout.on('resize', this._onResize);
64
38
  }
65
39
  teardown() {
66
- this._activityHeader = null;
67
- this._activityLines = [];
68
40
  console.log = this.origLog;
69
41
  process.stdout.write('\x1b[?2004l');
70
- process.stdout.write('\x1b[r');
71
- process.stdout.write('\x1b[?25h');
72
- process.stdout.write(`\x1b[${this.rows};1H\n`);
42
+ if (this._onResize)
43
+ process.stdout.off('resize', this._onResize);
73
44
  }
74
- redrawBox() { this._drawBox(); }
75
45
  suspend() {
46
+ this._clearArea();
76
47
  console.log = this.origLog;
77
48
  process.stdout.write('\x1b[?2004l');
78
- process.stdout.write('\x1b[r');
79
- this._clearReserved();
80
- process.stdout.write(`\x1b[${this.scrollBottom};1H`);
81
49
  return () => {
82
50
  console.log = (...args) => {
83
- const text = args.map(a => (typeof a === 'string' ? a : String(a))).join(' ');
51
+ const text = args.map(a => typeof a === 'string' ? a : String(a)).join(' ');
84
52
  this.println(text);
85
53
  };
86
- this._setScrollRegion();
87
- this._clearReserved();
88
- this._drawBox();
89
54
  process.stdout.write('\x1b[?2004h');
55
+ this._redraw();
90
56
  };
91
57
  }
92
- // ── Activity box API ───────────────────────────────────────────────────────
93
- /** Enter activity mode: show the 5-line log box instead of the input box. */
94
58
  startActivity(header) {
95
59
  this._activityHeader = header;
96
60
  this._activityLines = [];
97
- this._setScrollRegion();
98
- this._drawBox();
61
+ this._redraw();
99
62
  }
100
- /** Update the header line (spinner frame + elapsed time) without clearing lines. */
101
63
  updateActivityHeader(header) {
102
64
  this._activityHeader = header;
103
- this._drawBox();
65
+ this._redraw();
104
66
  }
105
- /**
106
- * Append a line to the activity log (keeps last ACTIVITY_LINES lines).
107
- * Strips ANSI codes and skips blank or pure-JSON lines.
108
- */
109
67
  pushActivity(rawLine) {
110
68
  if (this._activityHeader === null)
111
69
  return;
112
- // Strip ANSI escape sequences
113
- const clean = rawLine
114
- .replace(/\x1b\[[0-9;]*[A-Za-z]/g, '')
115
- .replace(/[^\x20-\x7e\u00a0-\uffff]/g, '')
116
- .trim();
70
+ const clean = rawLine.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '').trim();
117
71
  if (!clean)
118
72
  return;
119
73
  this._activityLines.push(clean);
120
- if (this._activityLines.length > ACTIVITY_LINES)
74
+ if (this._activityLines.length > 5)
121
75
  this._activityLines.shift();
122
- this._scheduleDraw();
76
+ this._redraw();
77
+ }
78
+ /** Replace all content lines at once (for streaming preview). */
79
+ setActivityLines(lines) {
80
+ if (this._activityHeader === null)
81
+ return;
82
+ this._activityLines = lines.map(l => l.slice(0, this.cols - 4));
83
+ this._redraw();
123
84
  }
124
- /** Leave activity mode and restore the normal input box. */
125
85
  stopActivity() {
126
- // Explicitly clear the full ACTIVE reserved zone before shrinking
127
- // the scroll region — otherwise activity box rows bleed into scroll history.
128
- const activeSB = this.rows - ACTIVE_RESERVED;
129
- for (let r = activeSB + 1; r <= this.rows; r++)
130
- process.stdout.write(`\x1b[${r};1H\x1b[2K`);
131
86
  this._activityHeader = null;
132
87
  this._activityLines = [];
133
- this._setScrollRegion();
134
- this._drawBox();
88
+ this._redraw();
89
+ }
90
+ println(text) {
91
+ this._clearArea();
92
+ process.stdout.write(text + '\n');
93
+ this._redraw();
94
+ }
95
+ printSeparator() {
96
+ this._clearArea();
97
+ process.stdout.write(DIM('─'.repeat(this.cols - 1)) + '\n');
98
+ this._redraw();
99
+ }
100
+ redrawBox() {
101
+ this._redraw();
135
102
  }
136
- // ── Input ──────────────────────────────────────────────────────────────────
137
103
  readLine() {
138
- this.buf = '';
139
- this.histIdx = -1;
140
- this._drawBox();
141
- return new Promise((resolve) => {
104
+ this._inputBuffer = [];
105
+ this._cursorPos = 0;
106
+ this._inputActive = true;
107
+ this._redraw();
108
+ if (process.stdin.isTTY)
142
109
  process.stdin.setRawMode(true);
143
- process.stdin.resume();
144
- const done = (line) => {
110
+ process.stdin.resume();
111
+ return new Promise((resolve) => {
112
+ this._resolveInput = resolve;
113
+ const finish = (value) => {
114
+ this._inputActive = false;
115
+ this._clearArea();
145
116
  process.stdin.removeListener('data', onData);
146
117
  if (process.stdin.isTTY)
147
118
  process.stdin.setRawMode(false);
148
- this.buf = '';
149
- this._drawBox();
150
- resolve(line);
119
+ process.stdin.pause();
120
+ const r = this._resolveInput;
121
+ this._resolveInput = undefined;
122
+ r?.(value);
151
123
  };
152
124
  const onData = (data) => {
153
- const hex = data.toString('hex');
154
125
  const key = data.toString();
155
- // ── Bracketed paste: start ────────────────────────────────────
156
- if (key.includes('\x1b[200~')) {
126
+ const hex = data.toString('hex');
127
+ // Bracketed paste start (may include end marker in same chunk).
128
+ if (!this._pasting && key.includes('\x1b[200~')) {
157
129
  this._pasting = true;
158
130
  this._pasteAccum = '';
159
- const after = key.slice(key.indexOf('\x1b[200~') + 6);
160
- if (after)
161
- this._pasteAccum += after;
131
+ const afterStart = key.slice(key.indexOf('\x1b[200~') + 6);
132
+ const endIdx = afterStart.indexOf('\x1b[201~');
133
+ if (endIdx !== -1) {
134
+ this._pasteAccum += afterStart.slice(0, endIdx);
135
+ this._commitPaste();
136
+ }
137
+ else {
138
+ this._pasteAccum += afterStart;
139
+ }
162
140
  return;
163
141
  }
164
- // ── Bracketed paste: accumulate ───────────────────────────────
165
142
  if (this._pasting) {
166
- if (key.includes('\x1b[201~')) {
167
- const before = key.slice(0, key.indexOf('\x1b[201~'));
168
- this._pasteAccum += before;
169
- this.buf += this._pasteAccum;
170
- this._pasting = false;
171
- this._pasteAccum = '';
172
- this._scheduleDraw();
143
+ const endIdx = key.indexOf('\x1b[201~');
144
+ if (endIdx !== -1) {
145
+ this._pasteAccum += key.slice(0, endIdx);
146
+ this._commitPaste();
173
147
  }
174
148
  else {
175
149
  this._pasteAccum += key;
176
150
  }
177
151
  return;
178
152
  }
179
- // ── Shift+Enter newline ────────────────────────────────────
180
- if (hex === '5c0d' ||
181
- key === '\x0a' ||
182
- hex === '1b5b31333b327e' ||
183
- hex === '1b5b31333b3275' ||
184
- hex === '1b4f4d') {
185
- this.buf += '\n';
186
- this._scheduleDraw();
187
- // ── Enter → submit ───────────────────────────────────────────
188
- }
189
- else if (key === '\r') {
190
- const line = this.buf;
153
+ // Enter submit
154
+ if (key === '\r') {
155
+ const line = this._inputBuffer.join('');
191
156
  if (line.trim()) {
192
157
  this.history.unshift(line);
193
158
  if (this.history.length > 200)
194
159
  this.history.pop();
195
160
  }
196
- done(line);
161
+ finish(line);
162
+ return;
163
+ }
164
+ // Ctrl+J (LF) — insert newline
165
+ if (key === '\n') {
166
+ this._inputBuffer.splice(this._cursorPos, 0, '\n');
167
+ this._cursorPos++;
168
+ this._redraw();
169
+ return;
170
+ }
171
+ // Backspace
172
+ if (key === '\x7f' || key === '\x08') {
173
+ if (this._cursorPos > 0) {
174
+ this._inputBuffer.splice(this._cursorPos - 1, 1);
175
+ this._cursorPos--;
176
+ this._redraw();
177
+ }
178
+ return;
197
179
  }
198
- else if (key === '\x7f' || key === '\x08') {
199
- if (this.buf.length > 0) {
200
- this.buf = this.buf.slice(0, -1);
201
- this._scheduleDraw();
180
+ // Delete
181
+ if (hex === '1b5b337e') {
182
+ if (this._cursorPos < this._inputBuffer.length) {
183
+ this._inputBuffer.splice(this._cursorPos, 1);
184
+ this._redraw();
202
185
  }
186
+ return;
203
187
  }
204
- else if (key === '\x03') {
188
+ // Ctrl+C
189
+ if (key === '\x03') {
190
+ this._clearArea();
191
+ if (process.stdin.isTTY)
192
+ process.stdin.setRawMode(false);
193
+ process.stdin.pause();
205
194
  this.teardown();
206
195
  process.exit(0);
207
196
  }
208
- else if (key === '\x04') {
209
- done('/exit');
197
+ // Ctrl+D
198
+ if (key === '\x04') {
199
+ finish('/exit');
200
+ return;
201
+ }
202
+ // Ctrl+U — clear line
203
+ if (key === '\x15') {
204
+ this._inputBuffer = [];
205
+ this._cursorPos = 0;
206
+ this._redraw();
207
+ return;
208
+ }
209
+ // Ctrl+W / Alt+Backspace — delete prev word
210
+ if (key === '\x17' || hex === '1b7f') {
211
+ const before = this._inputBuffer.slice(0, this._cursorPos).join('').trimEnd();
212
+ const lastSpace = before.lastIndexOf(' ');
213
+ const newPos = lastSpace === -1 ? 0 : lastSpace + 1;
214
+ this._inputBuffer = [
215
+ ...this._inputBuffer.slice(0, newPos),
216
+ ...this._inputBuffer.slice(this._cursorPos),
217
+ ];
218
+ this._cursorPos = newPos;
219
+ this._redraw();
220
+ return;
210
221
  }
211
- else if (key === '\x15') {
212
- this.buf = '';
213
- this._scheduleDraw();
222
+ // Ctrl+A / Home
223
+ if (key === '\x01' || hex === '1b5b48' || hex === '1b4f48') {
224
+ this._cursorPos = 0;
225
+ this._redraw();
226
+ return;
227
+ }
228
+ // Ctrl+E / End
229
+ if (key === '\x05' || hex === '1b5b46' || hex === '1b4f46') {
230
+ this._cursorPos = this._inputBuffer.length;
231
+ this._redraw();
232
+ return;
214
233
  }
215
- else if (hex === '1b5b41') { // Arrow
216
- if (this.histIdx + 1 < this.history.length) {
217
- this.histIdx++;
218
- this.buf = this.history[this.histIdx];
219
- this._scheduleDraw();
234
+ // Arrow Left
235
+ if (hex === '1b5b44') {
236
+ if (this._cursorPos > 0) {
237
+ this._cursorPos--;
238
+ this._redraw();
220
239
  }
240
+ return;
221
241
  }
222
- else if (hex === '1b5b42') { // Arrow
223
- if (this.histIdx > 0) {
224
- this.histIdx--;
225
- this.buf = this.history[this.histIdx];
242
+ // Arrow Right
243
+ if (hex === '1b5b43') {
244
+ if (this._cursorPos < this._inputBuffer.length) {
245
+ this._cursorPos++;
246
+ this._redraw();
226
247
  }
227
- else {
228
- this.histIdx = -1;
229
- this.buf = '';
248
+ return;
249
+ }
250
+ // Arrow Up — last history
251
+ if (hex === '1b5b41') {
252
+ if (this.history.length > 0) {
253
+ this._inputBuffer = this.history[0].split('');
254
+ this._cursorPos = this._inputBuffer.length;
255
+ this._redraw();
230
256
  }
231
- this._scheduleDraw();
257
+ return;
232
258
  }
233
- else if (key.length >= 1 && key.charCodeAt(0) >= 32 && !key.startsWith('\x1b')) {
234
- this.buf += key;
235
- this._scheduleDraw();
259
+ // Arrow Down clear
260
+ if (hex === '1b5b42') {
261
+ if (this._inputBuffer.length > 0) {
262
+ this._inputBuffer = [];
263
+ this._cursorPos = 0;
264
+ this._redraw();
265
+ }
266
+ return;
267
+ }
268
+ // Shift/Alt+Enter — insert newline (various terminals)
269
+ if (hex === '1b0d' || hex === '1b0a' ||
270
+ hex === '1b5b31333b327e' || hex === '1b5b31333b3275' ||
271
+ hex === '1b4f4d') {
272
+ this._inputBuffer.splice(this._cursorPos, 0, '\n');
273
+ this._cursorPos++;
274
+ this._redraw();
275
+ return;
276
+ }
277
+ // Regular typed characters.
278
+ // Shift+Enter fallback: gnome-terminal sends '\' + LF (or sometimes just '\').
279
+ // Collapse a '\' — with an optional trailing LF/CR — into a single newline.
280
+ // Literal '\' still works through paste (Ctrl+Shift+V) since that path is
281
+ // handled by the bracketed-paste branch.
282
+ if (key.length >= 1 && !key.startsWith('\x1b')) {
283
+ const raw = [...key];
284
+ const chars = [];
285
+ for (let i = 0; i < raw.length; i++) {
286
+ const c = raw[i];
287
+ if (c === '\\') {
288
+ chars.push('\n');
289
+ if (raw[i + 1] === '\n' || raw[i + 1] === '\r')
290
+ i++;
291
+ continue;
292
+ }
293
+ if (c === '\n' || c.charCodeAt(0) >= 32)
294
+ chars.push(c);
295
+ }
296
+ if (chars.length) {
297
+ this._inputBuffer.splice(this._cursorPos, 0, ...chars);
298
+ this._cursorPos += chars.length;
299
+ this._redraw();
300
+ }
236
301
  }
237
302
  };
238
303
  process.stdin.on('data', onData);
239
304
  });
240
305
  }
241
- // ── Output helpers ─────────────────────────────────────────────────────────
242
- println(text) {
243
- process.stdout.write(`\x1b[${this.scrollBottom};1H`);
244
- process.stdout.write(text + '\n');
245
- this._drawBox();
306
+ _commitPaste() {
307
+ const text = this._pasteAccum.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
308
+ const chars = [...text];
309
+ this._inputBuffer.splice(this._cursorPos, 0, ...chars);
310
+ this._cursorPos += chars.length;
311
+ this._pasting = false;
312
+ this._pasteAccum = '';
313
+ this._redraw();
246
314
  }
247
- printSeparator() {
248
- this.println(chalk.rgb(0, 120, 116)('─'.repeat(this.cols - 1)));
249
- }
250
- // ── Private drawing ────────────────────────────────────────────────────────
251
- _scheduleDraw() {
252
- if (this._drawPending)
253
- return;
254
- this._drawPending = true;
255
- setImmediate(() => { this._drawPending = false; this._drawBox(); });
256
- }
257
- _setScrollRegion() {
258
- const sb = this.scrollBottom;
259
- if (sb >= 1)
260
- process.stdout.write(`\x1b[1;${sb}r`);
261
- }
262
- _clearReserved() {
263
- for (let r = this.scrollBottom + 1; r <= this.rows; r++)
264
- process.stdout.write(`\x1b[${r};1H\x1b[2K`);
265
- }
266
- _drawBox() {
267
- process.stdout.write('\x1b[?25l');
268
- this._clearReserved();
269
- if (this._activityHeader !== null) {
270
- this._drawActivityBox();
315
+ /** Build a string that erases the currently-drawn area and leaves the cursor at col 0 of the top row. */
316
+ _buildClear() {
317
+ if (this._areaRows === 0)
318
+ return '';
319
+ let s = '\r';
320
+ // Move up to first row of area.
321
+ if (this._cursorRow > 0)
322
+ s += `\x1b[${this._cursorRow}A`;
323
+ // Clear each row (line-by-line most portable).
324
+ for (let i = 0; i < this._areaRows; i++) {
325
+ s += '\x1b[2K';
326
+ if (i < this._areaRows - 1)
327
+ s += '\x1b[1B';
271
328
  }
272
- this._drawInputBox();
273
- process.stdout.write('\x1b[?25h');
329
+ // Back to first row, col 0.
330
+ if (this._areaRows > 1)
331
+ s += `\x1b[${this._areaRows - 1}A`;
332
+ s += '\r';
333
+ return s;
274
334
  }
275
- // ── Activity box (shown while a subagent is running) ───────────────────────
276
- _drawActivityBox() {
277
- const cols = this.cols;
278
- const inner = cols - 4; // │ + space + content + space + │
279
- const topRow = this.scrollBottom + 1;
280
- const header = (this._activityHeader || '').slice(0, cols - 4);
281
- const dashFill = Math.max(0, cols - 3 - header.length);
282
- // Top border with header text
283
- process.stdout.write(`\x1b[${topRow};1H`);
284
- process.stdout.write(T('╭─') + chalk.bold.white(header) + T('─'.repeat(dashFill)) + T('╮'));
285
- // Content rows (last ACTIVITY_LINES lines, or blank)
286
- for (let i = 0; i < ACTIVITY_LINES; i++) {
287
- const row = topRow + 1 + i;
288
- const line = (this._activityLines[i] ?? '').slice(0, inner);
289
- const pad = inner - line.length;
290
- process.stdout.write(`\x1b[${row};1H`);
291
- process.stdout.write(T('│') + ' ' + chalk.rgb(180, 210, 210)(line) + ' '.repeat(pad) + ' ' + T('│'));
292
- }
293
- // Bottom border
294
- process.stdout.write(`\x1b[${topRow + ACTIVITY_LINES + 1};1H`);
295
- process.stdout.write(T('╰') + T('─'.repeat(cols - 2)) + T('╯'));
335
+ _clearArea() {
336
+ const s = this._buildClear();
337
+ if (s)
338
+ process.stdout.write(s);
339
+ this._areaRows = 0;
340
+ this._cursorRow = 0;
296
341
  }
297
- // ── Normal input box ───────────────────────────────────────────────────────
298
- _drawInputBox() {
342
+ _redraw() {
343
+ const clear = this._buildClear();
344
+ if (!this._inputActive && this._activityHeader === null) {
345
+ if (clear)
346
+ process.stdout.write(clear);
347
+ this._areaRows = 0;
348
+ this._cursorRow = 0;
349
+ return;
350
+ }
299
351
  const cols = this.cols;
300
- const cRows = this._contentRows();
301
- const cWidth = cols - PREFIX_COLS - 2;
302
- const topBorder = this.rows - cRows - 1;
303
- // ── Top border ───────────────────────────────────────────────
304
- process.stdout.write(`\x1b[${topBorder};1H`);
305
- process.stdout.write(T('╭') + T('─'.repeat(cols - 2)));
306
- process.stdout.write(`\x1b[${cols}G` + T('╮'));
307
- // ── Content rows ─────────────────────────────────────────────
308
- const wrapped = this._wrapText(this.buf, cWidth);
309
- const showStart = Math.max(0, wrapped.length - cRows);
310
- const visible = wrapped.slice(showStart);
311
- for (let i = 0; i < cRows; i++) {
312
- const row = topBorder + 1 + i;
313
- let line = visible[i] ?? '';
314
- if (i === 0 && showStart > 0)
315
- line = '… ' + line.slice(0, Math.max(0, cWidth - 2));
316
- else
317
- line = line.slice(0, cWidth);
318
- const pfx = (i === 0) ? PREFIX : PREFIX_CONT;
319
- process.stdout.write(`\x1b[${row};1H`);
320
- process.stdout.write(pfx + line);
321
- process.stdout.write(`\x1b[${cols}G` + T('│'));
352
+ let body = '';
353
+ let row = 0;
354
+ let cursorRow = 0;
355
+ let cursorCol = 0;
356
+ // Activity box
357
+ if (this._activityHeader !== null) {
358
+ const header = (this._activityHeader || '').slice(0, cols - 4);
359
+ const topPad = Math.max(0, cols - 3 - header.length);
360
+ body += DIM('┌') + chalk.bold.white(header) + DIM('─'.repeat(topPad) + '┐') + '\n';
361
+ row++;
362
+ for (let i = 0; i < 5; i++) {
363
+ const line = (this._activityLines[i] ?? '').slice(0, cols - 4);
364
+ const pad = Math.max(0, cols - 4 - line.length);
365
+ body += DIM('│') + ' ' + chalk.rgb(180, 210, 210)(line) + ' '.repeat(pad) + ' ' + DIM('│') + '\n';
366
+ row++;
367
+ }
368
+ body += DIM('└') + DIM('─'.repeat(cols - 2)) + DIM('┘') + '\n';
369
+ row++;
322
370
  }
323
- // ── Bottom border ────────────────────────────────────────────
324
- process.stdout.write(`\x1b[${this.rows};1H`);
325
- process.stdout.write(T('╰') + T('─'.repeat(cols - 2)));
326
- process.stdout.write(`\x1b[${cols}G` + T(''));
327
- // ── Position cursor ──────────────────────────────────────────
328
- const lastLine = visible[visible.length - 1] ?? '';
329
- const cursorRow = topBorder + cRows;
330
- const cursorCol = PREFIX_COLS + 1 + lastLine.length;
331
- process.stdout.write(`\x1b[${cursorRow};${cursorCol}H`);
332
- }
333
- _wrapText(text, maxWidth) {
334
- if (!text || maxWidth <= 0)
335
- return [''];
336
- const result = [];
337
- for (const seg of text.split('\n')) {
338
- if (seg.length === 0) {
339
- result.push('');
340
- continue;
371
+ let trailing = '';
372
+ let lastRow = row;
373
+ if (this._inputActive) {
374
+ const text = this._inputBuffer.join('');
375
+ const beforeCursor = this._inputBuffer.slice(0, this._cursorPos).join('');
376
+ const cursorLineIdx = beforeCursor.split('\n').length - 1;
377
+ const cursorColInLine = (beforeCursor.split('\n').pop() ?? '').length;
378
+ const logicalLines = text.length === 0 ? [''] : text.split('\n');
379
+ const displayLines = logicalLines.map((l, i) => (i === 0 ? PROMPT : INDENT) + (text === '' ? DIM(PLACEHOLDER) : l.slice(0, Math.max(0, cols - 3))));
380
+ for (let i = 0; i < displayLines.length; i++) {
381
+ const isLast = i === displayLines.length - 1;
382
+ body += displayLines[i] + (isLast ? '' : '\n');
383
+ if (i === cursorLineIdx) {
384
+ cursorRow = row;
385
+ cursorCol = PROMPT_W + Math.min(cursorColInLine, Math.max(0, cols - 3));
386
+ }
387
+ if (!isLast)
388
+ row++;
341
389
  }
342
- for (let i = 0; i < seg.length; i += maxWidth)
343
- result.push(seg.slice(i, i + maxWidth));
390
+ lastRow = row;
391
+ // After writing the body, terminal cursor is at end of last drawn line. Reposition to (cursorRow, cursorCol).
392
+ trailing = '\r';
393
+ const upBy = lastRow - cursorRow;
394
+ if (upBy > 0)
395
+ trailing += `\x1b[${upBy}A`;
396
+ if (cursorCol > 0)
397
+ trailing += `\x1b[${cursorCol}C`;
398
+ this._areaRows = lastRow + 1;
399
+ this._cursorRow = cursorRow;
400
+ }
401
+ else {
402
+ // Activity-only — leave cursor on the blank line just below the box.
403
+ this._areaRows = row;
404
+ this._cursorRow = row;
344
405
  }
345
- return result;
406
+ // Single atomic write: clear + body + reposition.
407
+ process.stdout.write(clear + body + trailing);
346
408
  }
347
409
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-mp",
3
- "version": "0.5.23",
3
+ "version": "0.5.25",
4
4
  "description": "Deterministic multi-agent CLI orchestrator — plan, code, review",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",