agent-mp 0.3.6 → 0.3.8
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/core/engine.d.ts +2 -0
- package/dist/core/engine.js +37 -3
- package/dist/ui/input.d.ts +10 -0
- package/dist/ui/input.js +66 -8
- package/package.json +1 -1
package/dist/core/engine.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export declare class AgentEngine {
|
|
|
16
16
|
private totalTokens;
|
|
17
17
|
private phaseTokens;
|
|
18
18
|
constructor(config: AgentConfig, projectDir: string, coordinatorCmd?: string, rl?: readline.Interface, fi?: FixedInput, slashHandler?: SlashHandler);
|
|
19
|
+
/** Start an animated spinner on the status row. Returns a stop function. */
|
|
20
|
+
private _startSpinner;
|
|
19
21
|
/**
|
|
20
22
|
* FASE 0 — Clarificacion con el programador.
|
|
21
23
|
* El coordinador (CLI activo, ej: Qwen) conversa con el usuario
|
package/dist/core/engine.js
CHANGED
|
@@ -219,6 +219,20 @@ export class AgentEngine {
|
|
|
219
219
|
this.fi = fi;
|
|
220
220
|
this.slashHandler = slashHandler;
|
|
221
221
|
}
|
|
222
|
+
/** Start an animated spinner on the status row. Returns a stop function. */
|
|
223
|
+
_startSpinner(label) {
|
|
224
|
+
if (!this.fi)
|
|
225
|
+
return () => { };
|
|
226
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
227
|
+
let i = 0;
|
|
228
|
+
const t0 = Date.now();
|
|
229
|
+
const fi = this.fi;
|
|
230
|
+
const iv = setInterval(() => {
|
|
231
|
+
const s = Math.floor((Date.now() - t0) / 1000);
|
|
232
|
+
fi.setStatus(` ${frames[i++ % frames.length]} ${label} ${s}s`);
|
|
233
|
+
}, 100);
|
|
234
|
+
return () => { clearInterval(iv); fi.setStatus(null); };
|
|
235
|
+
}
|
|
222
236
|
/**
|
|
223
237
|
* FASE 0 — Clarificacion con el programador.
|
|
224
238
|
* El coordinador (CLI activo, ej: Qwen) conversa con el usuario
|
|
@@ -253,11 +267,15 @@ INSTRUCCIONES:
|
|
|
253
267
|
if (this.coordinatorCmd.startsWith('qwen')) {
|
|
254
268
|
// Use Qwen API directly — avoids the qwen CLI's own OAuth flow
|
|
255
269
|
// which causes mid-session auth popups and breaks display.
|
|
270
|
+
const model = this.coordinatorCmd.match(/(?:-m|--model)\s+(\S+)/)?.[1] || 'coder-model';
|
|
271
|
+
const stopSpin = this._startSpinner(`coordinador ${model}`);
|
|
256
272
|
try {
|
|
257
|
-
const
|
|
258
|
-
|
|
273
|
+
const result = await callQwenAPI(prompt, model);
|
|
274
|
+
stopSpin();
|
|
275
|
+
return result;
|
|
259
276
|
}
|
|
260
277
|
catch (err) {
|
|
278
|
+
stopSpin();
|
|
261
279
|
if (err.message?.startsWith('QWEN_AUTH_EXPIRED')) {
|
|
262
280
|
console.log(chalk.red('\n ✗ Sesión Qwen expirada.'));
|
|
263
281
|
console.log(chalk.yellow(' Ejecutá: /login para re-autenticarte.\n'));
|
|
@@ -269,7 +287,9 @@ INSTRUCCIONES:
|
|
|
269
287
|
}
|
|
270
288
|
}
|
|
271
289
|
else {
|
|
290
|
+
const stopSpin = this._startSpinner(`coordinador`);
|
|
272
291
|
res = await runCli(this.coordinatorCmd, prompt, 600000, envOverride);
|
|
292
|
+
stopSpin();
|
|
273
293
|
}
|
|
274
294
|
// Extract readable text — search for JSON array even if there's prefix text
|
|
275
295
|
let responseText = res.output.trim();
|
|
@@ -366,15 +386,19 @@ INSTRUCCIONES:
|
|
|
366
386
|
const rolePrompt = this.buildRolePrompt(roleName, prompt);
|
|
367
387
|
/** Try a cmd, and if it fails, auto-detect flags from --help and retry */
|
|
368
388
|
const tryWithAutoRepair = async (cliName, model, currentCmd) => {
|
|
389
|
+
const stopSpin = this._startSpinner(`${cliName} ${model}`);
|
|
369
390
|
try {
|
|
370
391
|
const result = await runCli(currentCmd, rolePrompt);
|
|
371
392
|
if (result.exitCode !== 0) {
|
|
393
|
+
stopSpin();
|
|
372
394
|
const detail = result.output.trim().slice(0, 500);
|
|
373
395
|
throw new Error(`${cliName} exited with code ${result.exitCode}${detail ? `\n${detail}` : ''}`);
|
|
374
396
|
}
|
|
397
|
+
stopSpin();
|
|
375
398
|
return result.output;
|
|
376
399
|
}
|
|
377
400
|
catch (err) {
|
|
401
|
+
stopSpin();
|
|
378
402
|
log.warn(`${cliName} failed, detecting flags from --help: ${err.message}`);
|
|
379
403
|
const detected = detectCliFlags(cliName);
|
|
380
404
|
if (Object.keys(detected).length === 0) {
|
|
@@ -426,8 +450,10 @@ INSTRUCCIONES:
|
|
|
426
450
|
// Config file might not exist yet
|
|
427
451
|
}
|
|
428
452
|
// Retry with new command
|
|
453
|
+
const stopSpin2 = this._startSpinner(`${cliName} ${model} (retry)`);
|
|
429
454
|
try {
|
|
430
455
|
const result = await runCli(newCmd, rolePrompt);
|
|
456
|
+
stopSpin2();
|
|
431
457
|
if (result.exitCode !== 0) {
|
|
432
458
|
const detail = result.output.trim().slice(0, 500);
|
|
433
459
|
throw new Error(`${cliName} (repaired) exited with code ${result.exitCode}${detail ? `\n${detail}` : ''}`);
|
|
@@ -436,6 +462,7 @@ INSTRUCCIONES:
|
|
|
436
462
|
return result.output;
|
|
437
463
|
}
|
|
438
464
|
catch (retryErr) {
|
|
465
|
+
stopSpin2();
|
|
439
466
|
log.warn(`Repaired ${cliName} also failed: ${retryErr.message}`);
|
|
440
467
|
return null;
|
|
441
468
|
}
|
|
@@ -452,11 +479,15 @@ INSTRUCCIONES:
|
|
|
452
479
|
log.warn(`${cliName} has no credentials — run: ${cliName} --login`);
|
|
453
480
|
return null;
|
|
454
481
|
}
|
|
482
|
+
const stopSpin = this._startSpinner(`${cliName} ${model}`);
|
|
455
483
|
try {
|
|
456
484
|
log.info(`${cliName}: calling Qwen API with own credentials (${model})`);
|
|
457
|
-
|
|
485
|
+
const result = await callQwenAPIFromCreds(rolePrompt, model, credsPath);
|
|
486
|
+
stopSpin();
|
|
487
|
+
return result;
|
|
458
488
|
}
|
|
459
489
|
catch (err) {
|
|
490
|
+
stopSpin();
|
|
460
491
|
if (err.message?.startsWith('QWEN_AUTH_EXPIRED')) {
|
|
461
492
|
console.log(chalk.red(`\n ✗ Sesión expirada para ${cliName}.`));
|
|
462
493
|
console.log(chalk.yellow(` Ejecutá: ${cliName} --login\n`));
|
|
@@ -854,10 +885,13 @@ INSTRUCCIONES:
|
|
|
854
885
|
4. Crea/actualiza ${archPath} con tabla resumen y detalle por servicio.
|
|
855
886
|
5. Crea/actualiza ${contextDir}/<servicio>/architecture.md para cada servicio.`);
|
|
856
887
|
let result;
|
|
888
|
+
const stopSpin = this._startSpinner(`agent-explorer ${role.model}`);
|
|
857
889
|
try {
|
|
858
890
|
result = await callQwenAPI(prompt, role.model);
|
|
891
|
+
stopSpin();
|
|
859
892
|
}
|
|
860
893
|
catch (err) {
|
|
894
|
+
stopSpin();
|
|
861
895
|
if (err.message?.startsWith('QWEN_AUTH_EXPIRED')) {
|
|
862
896
|
console.log(chalk.red('\n ✗ Sesión Qwen expirada.'));
|
|
863
897
|
console.log(chalk.yellow(' Ejecutá: agent-mp --login (o agent-explorer --login)\n'));
|
package/dist/ui/input.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ export declare class FixedInput {
|
|
|
3
3
|
private history;
|
|
4
4
|
private histIdx;
|
|
5
5
|
private origLog;
|
|
6
|
+
private _pasting;
|
|
7
|
+
private _pasteAccum;
|
|
8
|
+
private _drawPending;
|
|
9
|
+
private _statusText;
|
|
6
10
|
private get rows();
|
|
7
11
|
get cols(): number;
|
|
8
12
|
private get scrollBottom();
|
|
@@ -10,10 +14,16 @@ export declare class FixedInput {
|
|
|
10
14
|
setup(): void;
|
|
11
15
|
teardown(): void;
|
|
12
16
|
redrawBox(): void;
|
|
17
|
+
/** Show or clear the status / spinner line above the input box. */
|
|
18
|
+
setStatus(text: string | null): void;
|
|
13
19
|
suspend(): () => void;
|
|
14
20
|
readLine(): Promise<string>;
|
|
15
21
|
println(text: string): void;
|
|
16
22
|
printSeparator(): void;
|
|
23
|
+
/** Repaint the status row (between scroll region and input box). */
|
|
24
|
+
private _drawStatusRow;
|
|
25
|
+
/** Debounced draw: coalesces rapid calls (e.g. during paste) into a single repaint. */
|
|
26
|
+
private _scheduleDraw;
|
|
17
27
|
/** Set DECSTBM once — only called in setup() and on resize, never during typing. */
|
|
18
28
|
private _setScrollRegion;
|
|
19
29
|
/** Blank every row in the reserved area. */
|
package/dist/ui/input.js
CHANGED
|
@@ -6,9 +6,9 @@ const PREFIX = T('│') + B(' > ');
|
|
|
6
6
|
const PREFIX_CONT = T('│') + B(' '); // continuation lines
|
|
7
7
|
const PREFIX_COLS = 4; // visual width of "│ > " and "│ "
|
|
8
8
|
// Maximum content rows the box can grow to (Shift+Enter / word-wrap).
|
|
9
|
-
// The reserved area at the bottom is MAX_CONTENT_ROWS + 2 (borders).
|
|
9
|
+
// The reserved area at the bottom is MAX_CONTENT_ROWS + 2 (borders) + 1 (status row).
|
|
10
10
|
const MAX_CONTENT_ROWS = 4;
|
|
11
|
-
const RESERVED_ROWS = MAX_CONTENT_ROWS +
|
|
11
|
+
const RESERVED_ROWS = MAX_CONTENT_ROWS + 3; // 7 = 4 content + 2 borders + 1 status
|
|
12
12
|
// ─── FixedInput ──────────────────────────────────────────────────────────────
|
|
13
13
|
// Keeps an input box pinned to the physical bottom of the terminal.
|
|
14
14
|
// The box starts as 3 rows (border + 1 content + border) and grows up to
|
|
@@ -20,6 +20,10 @@ export class FixedInput {
|
|
|
20
20
|
history = [];
|
|
21
21
|
histIdx = -1;
|
|
22
22
|
origLog;
|
|
23
|
+
_pasting = false; // true while inside bracketed paste sequence
|
|
24
|
+
_pasteAccum = ''; // accumulates paste content between \x1b[200~ and \x1b[201~
|
|
25
|
+
_drawPending = false; // debounce flag
|
|
26
|
+
_statusText = ''; // spinner / status line above the input box
|
|
23
27
|
get rows() { return process.stdout.rows || 24; }
|
|
24
28
|
get cols() { return process.stdout.columns || 80; }
|
|
25
29
|
// The scroll region always ends here — everything below is reserved for the box.
|
|
@@ -47,6 +51,7 @@ export class FixedInput {
|
|
|
47
51
|
process.stdout.write(`\x1b[${this.scrollBottom};1H`);
|
|
48
52
|
this._clearReserved();
|
|
49
53
|
this._drawBox();
|
|
54
|
+
process.stdout.write('\x1b[?2004h'); // enable bracketed paste mode
|
|
50
55
|
process.stdout.on('resize', () => {
|
|
51
56
|
this._setScrollRegion();
|
|
52
57
|
this._clearReserved();
|
|
@@ -55,13 +60,20 @@ export class FixedInput {
|
|
|
55
60
|
}
|
|
56
61
|
teardown() {
|
|
57
62
|
console.log = this.origLog;
|
|
63
|
+
process.stdout.write('\x1b[?2004l'); // disable bracketed paste mode
|
|
58
64
|
process.stdout.write('\x1b[r'); // reset scroll region
|
|
59
65
|
process.stdout.write('\x1b[?25h'); // show cursor
|
|
60
66
|
process.stdout.write(`\x1b[${this.rows};1H\n`);
|
|
61
67
|
}
|
|
62
68
|
redrawBox() { this._drawBox(); }
|
|
69
|
+
/** Show or clear the status / spinner line above the input box. */
|
|
70
|
+
setStatus(text) {
|
|
71
|
+
this._statusText = text || '';
|
|
72
|
+
this._drawStatusRow();
|
|
73
|
+
}
|
|
63
74
|
suspend() {
|
|
64
75
|
console.log = this.origLog;
|
|
76
|
+
process.stdout.write('\x1b[?2004l'); // disable bracketed paste while suspended
|
|
65
77
|
process.stdout.write('\x1b[r');
|
|
66
78
|
this._clearReserved();
|
|
67
79
|
process.stdout.write(`\x1b[${this.scrollBottom};1H`);
|
|
@@ -73,6 +85,7 @@ export class FixedInput {
|
|
|
73
85
|
this._setScrollRegion();
|
|
74
86
|
this._clearReserved();
|
|
75
87
|
this._drawBox();
|
|
88
|
+
process.stdout.write('\x1b[?2004h'); // re-enable bracketed paste mode
|
|
76
89
|
};
|
|
77
90
|
}
|
|
78
91
|
// ── Input ──────────────────────────────────────────────────────────────────
|
|
@@ -94,6 +107,32 @@ export class FixedInput {
|
|
|
94
107
|
const onData = (data) => {
|
|
95
108
|
const hex = data.toString('hex');
|
|
96
109
|
const key = data.toString();
|
|
110
|
+
// ── Bracketed paste: start ────────────────────────────────────
|
|
111
|
+
if (key.includes('\x1b[200~')) {
|
|
112
|
+
this._pasting = true;
|
|
113
|
+
this._pasteAccum = '';
|
|
114
|
+
// Strip the start marker and handle any content after it in the same chunk
|
|
115
|
+
const after = key.slice(key.indexOf('\x1b[200~') + 6);
|
|
116
|
+
if (after)
|
|
117
|
+
this._pasteAccum += after;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// ── Bracketed paste: accumulate ───────────────────────────────
|
|
121
|
+
if (this._pasting) {
|
|
122
|
+
if (key.includes('\x1b[201~')) {
|
|
123
|
+
// End marker — append everything before it, then commit
|
|
124
|
+
const before = key.slice(0, key.indexOf('\x1b[201~'));
|
|
125
|
+
this._pasteAccum += before;
|
|
126
|
+
this.buf += this._pasteAccum;
|
|
127
|
+
this._pasting = false;
|
|
128
|
+
this._pasteAccum = '';
|
|
129
|
+
this._scheduleDraw();
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
this._pasteAccum += key;
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
97
136
|
// ── Shift+Enter → insert newline into buffer ──────────────────
|
|
98
137
|
// Different terminals send different sequences:
|
|
99
138
|
if (hex === '5c0d' || // \\\r (GNOME Terminal, ThinkPad, many Linux)
|
|
@@ -103,7 +142,7 @@ export class FixedInput {
|
|
|
103
142
|
hex === '1b4f4d' // \x1bOM DECNKP
|
|
104
143
|
) {
|
|
105
144
|
this.buf += '\n';
|
|
106
|
-
this.
|
|
145
|
+
this._scheduleDraw();
|
|
107
146
|
// ── Enter → submit ────────────────────────────────────────────
|
|
108
147
|
}
|
|
109
148
|
else if (key === '\r') {
|
|
@@ -118,7 +157,7 @@ export class FixedInput {
|
|
|
118
157
|
else if (key === '\x7f' || key === '\x08') { // Backspace
|
|
119
158
|
if (this.buf.length > 0) {
|
|
120
159
|
this.buf = this.buf.slice(0, -1);
|
|
121
|
-
this.
|
|
160
|
+
this._scheduleDraw();
|
|
122
161
|
}
|
|
123
162
|
}
|
|
124
163
|
else if (key === '\x03') { // Ctrl+C
|
|
@@ -130,13 +169,13 @@ export class FixedInput {
|
|
|
130
169
|
}
|
|
131
170
|
else if (key === '\x15') { // Ctrl+U
|
|
132
171
|
this.buf = '';
|
|
133
|
-
this.
|
|
172
|
+
this._scheduleDraw();
|
|
134
173
|
}
|
|
135
174
|
else if (hex === '1b5b41') { // Arrow ↑
|
|
136
175
|
if (this.histIdx + 1 < this.history.length) {
|
|
137
176
|
this.histIdx++;
|
|
138
177
|
this.buf = this.history[this.histIdx];
|
|
139
|
-
this.
|
|
178
|
+
this._scheduleDraw();
|
|
140
179
|
}
|
|
141
180
|
}
|
|
142
181
|
else if (hex === '1b5b42') { // Arrow ↓
|
|
@@ -148,11 +187,11 @@ export class FixedInput {
|
|
|
148
187
|
this.histIdx = -1;
|
|
149
188
|
this.buf = '';
|
|
150
189
|
}
|
|
151
|
-
this.
|
|
190
|
+
this._scheduleDraw();
|
|
152
191
|
}
|
|
153
192
|
else if (key.length >= 1 && key.charCodeAt(0) >= 32 && !key.startsWith('\x1b')) {
|
|
154
193
|
this.buf += key;
|
|
155
|
-
this.
|
|
194
|
+
this._scheduleDraw();
|
|
156
195
|
}
|
|
157
196
|
};
|
|
158
197
|
process.stdin.on('data', onData);
|
|
@@ -168,6 +207,24 @@ export class FixedInput {
|
|
|
168
207
|
this.println(chalk.rgb(0, 120, 116)('─'.repeat(this.cols - 1)));
|
|
169
208
|
}
|
|
170
209
|
// ── Private drawing ────────────────────────────────────────────────────────
|
|
210
|
+
/** Repaint the status row (between scroll region and input box). */
|
|
211
|
+
_drawStatusRow() {
|
|
212
|
+
const row = this.scrollBottom + 1;
|
|
213
|
+
process.stdout.write(`\x1b[${row};1H\x1b[2K`);
|
|
214
|
+
if (this._statusText) {
|
|
215
|
+
process.stdout.write(chalk.dim(this._statusText));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/** Debounced draw: coalesces rapid calls (e.g. during paste) into a single repaint. */
|
|
219
|
+
_scheduleDraw() {
|
|
220
|
+
if (this._drawPending)
|
|
221
|
+
return;
|
|
222
|
+
this._drawPending = true;
|
|
223
|
+
setImmediate(() => {
|
|
224
|
+
this._drawPending = false;
|
|
225
|
+
this._drawBox();
|
|
226
|
+
});
|
|
227
|
+
}
|
|
171
228
|
/** Set DECSTBM once — only called in setup() and on resize, never during typing. */
|
|
172
229
|
_setScrollRegion() {
|
|
173
230
|
const sb = this.scrollBottom;
|
|
@@ -192,6 +249,7 @@ export class FixedInput {
|
|
|
192
249
|
process.stdout.write('\x1b[?25l');
|
|
193
250
|
// Clear entire reserved area (removes stale content from previous draws)
|
|
194
251
|
this._clearReserved();
|
|
252
|
+
this._drawStatusRow();
|
|
195
253
|
// ── Top border ───────────────────────────────────────────────
|
|
196
254
|
process.stdout.write(`\x1b[${topBorder};1H`);
|
|
197
255
|
process.stdout.write(T('╭') + T('─'.repeat(cols - 2)));
|