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.
- package/dist/commands/repl.d.ts +0 -2
- package/dist/commands/repl.js +142 -128
- package/dist/core/engine.js +18 -28
- package/dist/ui/input.d.ts +23 -25
- package/dist/ui/input.js +310 -248
- package/package.json +1 -1
package/dist/commands/repl.d.ts
CHANGED
|
@@ -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>;
|
package/dist/commands/repl.js
CHANGED
|
@@ -795,60 +795,59 @@ export async function initCoordinator() {
|
|
|
795
795
|
input: process.stdin,
|
|
796
796
|
output: process.stdout,
|
|
797
797
|
});
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
};
|
|
847
|
-
|
|
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
|
|
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
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
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
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
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 =
|
|
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
|
}
|
package/dist/core/engine.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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(
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/ui/input.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
private
|
|
37
|
-
|
|
38
|
-
private
|
|
39
|
-
private
|
|
40
|
-
private
|
|
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
|
-
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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 =>
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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 =>
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 >
|
|
74
|
+
if (this._activityLines.length > 5)
|
|
121
75
|
this._activityLines.shift();
|
|
122
|
-
this.
|
|
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.
|
|
134
|
-
|
|
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.
|
|
139
|
-
this.
|
|
140
|
-
this.
|
|
141
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
149
|
-
this.
|
|
150
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
this._pasteAccum +=
|
|
169
|
-
this.
|
|
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
|
-
//
|
|
180
|
-
if (
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
this.
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
this.
|
|
219
|
-
this.
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
this.
|
|
242
|
+
// Arrow Right
|
|
243
|
+
if (hex === '1b5b43') {
|
|
244
|
+
if (this._cursorPos < this._inputBuffer.length) {
|
|
245
|
+
this._cursorPos++;
|
|
246
|
+
this._redraw();
|
|
226
247
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
257
|
+
return;
|
|
232
258
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
this.
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
this.
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
298
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
343
|
-
|
|
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
|
-
|
|
406
|
+
// Single atomic write: clear + body + reposition.
|
|
407
|
+
process.stdout.write(clear + body + trailing);
|
|
346
408
|
}
|
|
347
409
|
}
|