@neetru/cli 2.19.1 → 2.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/dist/commands/code.d.ts +8 -2
- package/dist/commands/code.js +474 -191
- package/dist/commands/code.js.map +1 -1
- package/dist/commands/config.js +2 -0
- package/dist/commands/config.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/code-agent-files.d.ts +9 -1
- package/dist/lib/code-agent-files.js +37 -10
- package/dist/lib/code-agent-files.js.map +1 -1
- package/dist/lib/code-agent-gateway.js +2 -0
- package/dist/lib/code-agent-gateway.js.map +1 -1
- package/dist/lib/code-agent-git-tools.d.ts +56 -0
- package/dist/lib/code-agent-git-tools.js +212 -0
- package/dist/lib/code-agent-git-tools.js.map +1 -0
- package/dist/lib/code-agent-protocol.d.ts +8 -0
- package/dist/lib/code-agent-protocol.js.map +1 -1
- package/dist/lib/code-agent-session.d.ts +9 -1
- package/dist/lib/code-agent-session.js +58 -21
- package/dist/lib/code-agent-session.js.map +1 -1
- package/dist/lib/code-agent-settings.d.ts +24 -1
- package/dist/lib/code-agent-settings.js +53 -4
- package/dist/lib/code-agent-settings.js.map +1 -1
- package/dist/lib/code-agent-subagents.d.ts +105 -0
- package/dist/lib/code-agent-subagents.js +179 -0
- package/dist/lib/code-agent-subagents.js.map +1 -0
- package/dist/lib/code-agent-tools.js +16 -0
- package/dist/lib/code-agent-tools.js.map +1 -1
- package/dist/lib/code-agent-tui.js +44 -9
- package/dist/lib/code-agent-tui.js.map +1 -1
- package/dist/lib/code-agent-types.d.ts +51 -0
- package/dist/lib/code-agent-ui.js +15 -1
- package/dist/lib/code-agent-ui.js.map +1 -1
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.js +1 -0
- package/dist/lib/config.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/code.js
CHANGED
|
@@ -9,13 +9,17 @@ import { log } from '../utils/logger.js';
|
|
|
9
9
|
import { setSafeExitCode } from '../utils/safe-exit.js';
|
|
10
10
|
import { parseToolRequest, extractPatch } from '../lib/code-agent-protocol.js';
|
|
11
11
|
import { applyUnifiedPatch, getGitDiff, getProjectOverview, listFiles, readFileContext, renderColoredDiff, runCodeTool, truncateForGateway, } from '../lib/code-agent-files.js';
|
|
12
|
-
import { compactCodeHistory, compactHistoryToFit, MAX_HISTORY_TOTAL_CHARS, saveCodeSession, listCodeSessions, loadCodeSession, logAppliedPatch, CodeAgentStateMachine, } from '../lib/code-agent-session.js';
|
|
13
|
-
import { DEFAULT_CODE_MAX_TOKENS, DEFAULT_CODE_MAX_TURNS, DEFAULT_CODE_TEMPERATURE, parseBoundedNumber, resolveCodeModel, resolveCodeProvider, } from '../lib/code-agent-settings.js';
|
|
12
|
+
import { compactCodeHistory, compactHistoryToFit, MAX_HISTORY_TOTAL_CHARS, saveCodeSession, autoSaveCodeSession, listCodeSessions, loadCodeSession, logAppliedPatch, CodeAgentStateMachine, } from '../lib/code-agent-session.js';
|
|
13
|
+
import { DEFAULT_CODE_MAX_TOKENS, DEFAULT_CODE_MAX_TURNS, DEFAULT_CODE_TEMPERATURE, ULTRACODE_EFFORT, ULTRACODE_MAX_TOKENS, ULTRACODE_MAX_TURNS, parseBoundedNumber, resolveCodeEffort, resolveCodeModel, resolveCodeProvider, } from '../lib/code-agent-settings.js';
|
|
14
14
|
import { callGateway, streamGatewayChat, AI_GATEWAY_CHAT_PATH } from '../lib/code-agent-gateway.js';
|
|
15
|
+
import { CliApiError } from '../lib/api-client.js';
|
|
15
16
|
import { buildSystemPrompt } from '../lib/code-agent-tools.js';
|
|
16
|
-
import { printBox, printEvent, printAssistantText, printPatchPreview, patchSummaryRows, renderHeader, renderInteractiveHelp, renderSessionStatus, renderPromptLabel, printFileSample, printFilePicker, printSessionList, startTurnSpinner, summarizeToolResult,
|
|
17
|
+
import { printBox, printEvent, printAssistantText, printPatchPreview, patchSummaryRows, renderHeader, renderInteractiveHelp, renderSessionStatus, renderPromptLabel, printFileSample, printFilePicker, printSessionList, startTurnSpinner, summarizeToolResult, } from '../lib/code-agent-ui.js';
|
|
17
18
|
import { runInkSession } from '../lib/code-agent-tui.js';
|
|
19
|
+
import { configureSubagentRunner, startSubagent, stopSubagent, stopAllSubagents, listSubagents, getSubagentResult, getApplicablePatch, onSubagentSettled, } from '../lib/code-agent-subagents.js';
|
|
18
20
|
const SYSTEM_PROMPT = buildSystemPrompt();
|
|
21
|
+
/** Spinner inerte pro modo silencioso (subagentes) — nunca escreve no terminal. */
|
|
22
|
+
const NOOP_TURN_SPINNER = { update: () => { }, stop: () => { } };
|
|
19
23
|
function asOptionalString(value) {
|
|
20
24
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
21
25
|
}
|
|
@@ -38,23 +42,25 @@ export function resolveSettings(opts) {
|
|
|
38
42
|
envModel: process.env.NEETRU_CODE_MODEL,
|
|
39
43
|
configuredGatewayModel: config.get('gatewayModel'),
|
|
40
44
|
});
|
|
41
|
-
const provider = resolveCodeProvider(asOptionalString(opts.provider), config.get('gatewayProvider'));
|
|
45
|
+
const provider = resolveCodeProvider(asOptionalString(opts.provider), config.get('gatewayProvider'), model.model);
|
|
46
|
+
const ultracode = opts.ultracode === true;
|
|
47
|
+
const effort = ultracode
|
|
48
|
+
? ULTRACODE_EFFORT
|
|
49
|
+
: resolveCodeEffort(asOptionalString(opts.effort), config.get('gatewayEffort'));
|
|
42
50
|
return {
|
|
43
51
|
cwd,
|
|
44
52
|
files: normalizeFiles(opts.file),
|
|
45
53
|
model: model.model,
|
|
46
54
|
provider,
|
|
47
55
|
modelWarning: model.warning,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
maxTokens:
|
|
54
|
-
|
|
55
|
-
max: 32_768,
|
|
56
|
-
integer: true,
|
|
57
|
-
}),
|
|
56
|
+
// GAP-M1-18: `--ultracode` sobrepoe `--max-turns`/`--max-tokens`
|
|
57
|
+
// explicitos — e um preset "tudo pra cima", nao so um default alternativo.
|
|
58
|
+
maxTurns: ultracode
|
|
59
|
+
? ULTRACODE_MAX_TURNS
|
|
60
|
+
: parseBoundedNumber(opts.maxTurns, DEFAULT_CODE_MAX_TURNS, { min: 1, max: 30, integer: true }),
|
|
61
|
+
maxTokens: ultracode
|
|
62
|
+
? ULTRACODE_MAX_TOKENS
|
|
63
|
+
: parseBoundedNumber(opts.maxTokens, DEFAULT_CODE_MAX_TOKENS, { min: 256, max: 32_768, integer: true }),
|
|
58
64
|
temperature: parseBoundedNumber(opts.temperature, DEFAULT_CODE_TEMPERATURE, {
|
|
59
65
|
min: 0,
|
|
60
66
|
max: 2,
|
|
@@ -62,6 +68,8 @@ export function resolveSettings(opts) {
|
|
|
62
68
|
apply: opts.apply !== false && opts.plan !== true,
|
|
63
69
|
yes: opts.yes === true,
|
|
64
70
|
timeoutMs: 120_000,
|
|
71
|
+
effort,
|
|
72
|
+
ultracode,
|
|
65
73
|
};
|
|
66
74
|
}
|
|
67
75
|
async function runReviewCommand(settings, confirm, history) {
|
|
@@ -107,34 +115,38 @@ async function defaultConfirm(question) {
|
|
|
107
115
|
}
|
|
108
116
|
}
|
|
109
117
|
/**
|
|
110
|
-
* Chama o AI Gateway
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* fallback sem stream (antes o fallback ficava mudo do aviso ate a
|
|
114
|
-
* chegar ou o timeout estourar). So anima em TTY real
|
|
115
|
-
*
|
|
118
|
+
* Chama o AI Gateway atualizando o `spinner` compartilhado da tarefa inteira
|
|
119
|
+
* (nao cria um spinner proprio por chamada) — o indicador de progresso fica
|
|
120
|
+
* vivo e continuo entre turnos, so o `detail` muda. Cobre TANTO o streaming
|
|
121
|
+
* quanto o fallback sem stream (antes o fallback ficava mudo do aviso ate a
|
|
122
|
+
* resposta chegar ou o timeout estourar). So anima em TTY real (no-op em
|
|
123
|
+
* pipe/CI/teste, `startTurnSpinner` ja trata isso).
|
|
116
124
|
*
|
|
117
125
|
* Fallback: qualquer falha no streaming (rede, formato inesperado) refaz a
|
|
118
126
|
* chamada sem streaming — a corretude do turno nunca depende do streaming
|
|
119
|
-
* funcionar, so a experiencia visual.
|
|
127
|
+
* funcionar, so a experiencia visual. O aviso de fallback e um evento REAL
|
|
128
|
+
* (nao progresso normal), entao para o spinner pra nao interlear com o
|
|
129
|
+
* `printEvent` e abre um novo pra continuar depois.
|
|
120
130
|
*/
|
|
121
|
-
async function callGatewayWithProgress(settings, messages) {
|
|
122
|
-
const spinner = startTurnSpinner('AI Gateway');
|
|
131
|
+
async function callGatewayWithProgress(settings, messages, spinner, baseDetail, silent = false) {
|
|
123
132
|
try {
|
|
124
133
|
const content = await streamGatewayChat(settings, messages, (_delta, total) => {
|
|
125
|
-
spinner.update(`${total} char(s) recebido(s)`);
|
|
134
|
+
spinner.update(`${baseDetail} · ${total} char(s) recebido(s)`);
|
|
126
135
|
});
|
|
127
|
-
spinner
|
|
128
|
-
return content;
|
|
136
|
+
return { content, spinner };
|
|
129
137
|
}
|
|
130
138
|
catch (error) {
|
|
131
139
|
spinner.stop();
|
|
132
|
-
|
|
133
|
-
|
|
140
|
+
// silent (subagente): sem aviso de fallback e sem novo spinner — a saida
|
|
141
|
+
// visual pertence so a tarefa em primeiro plano.
|
|
142
|
+
if (!silent) {
|
|
143
|
+
printEvent('warn', 'Streaming falhou, tentando sem streaming', error instanceof Error ? error.message : String(error));
|
|
144
|
+
}
|
|
145
|
+
const fallbackSpinner = silent ? NOOP_TURN_SPINNER : startTurnSpinner('neetru code');
|
|
146
|
+
fallbackSpinner.update(`${baseDetail} · sem stream`);
|
|
134
147
|
try {
|
|
135
148
|
const content = await callGateway(settings, messages);
|
|
136
|
-
fallbackSpinner
|
|
137
|
-
return content;
|
|
149
|
+
return { content, spinner: fallbackSpinner };
|
|
138
150
|
}
|
|
139
151
|
catch (fallbackError) {
|
|
140
152
|
fallbackSpinner.stop();
|
|
@@ -142,6 +154,41 @@ async function callGatewayWithProgress(settings, messages) {
|
|
|
142
154
|
}
|
|
143
155
|
}
|
|
144
156
|
}
|
|
157
|
+
// GAP-M1-21 (2026-07-11): achado em uso real via Firestore — as 3 keys Groq
|
|
158
|
+
// do pool entraram em cooldown SIMULTANEO (429 do proprio Groq — rate-limit
|
|
159
|
+
// de conta, nao bug de roteamento) com `cooldownUntil` ~60-90s no futuro
|
|
160
|
+
// (backoff exponencial minimo de `reportKeyThrottle` no Core e 60s). Sem
|
|
161
|
+
// nenhuma key Groq elegivel, `selectKey` devolve pool vazio e o Core responde
|
|
162
|
+
// 503 `ai_unavailable` na hora — o usuario batia nisso e precisava mandar a
|
|
163
|
+
// mesma mensagem de novo manualmente. Retry curto e automatico SO pra esse
|
|
164
|
+
// erro especifico (nunca pra payload/auth/etc, que retry nao resolve).
|
|
165
|
+
const POOL_RETRY_DELAYS_MS = [20_000, 40_000];
|
|
166
|
+
export function isProviderPoolUnavailable(error) {
|
|
167
|
+
return error instanceof CliApiError && error.status === 503 && error.code === 'ai_unavailable';
|
|
168
|
+
}
|
|
169
|
+
function delay(ms, signal) {
|
|
170
|
+
return new Promise((resolve) => {
|
|
171
|
+
const timer = setTimeout(resolve, ms);
|
|
172
|
+
signal?.addEventListener('abort', () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
export async function callGatewayWithProgressRetrying(settings, messages, spinner, baseDetail, silent, signal) {
|
|
176
|
+
for (let attempt = 0;; attempt++) {
|
|
177
|
+
try {
|
|
178
|
+
return await callGatewayWithProgress(settings, messages, spinner, baseDetail, silent);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (!isProviderPoolUnavailable(error) || attempt >= POOL_RETRY_DELAYS_MS.length || signal?.aborted) {
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
const waitMs = POOL_RETRY_DELAYS_MS[attempt];
|
|
185
|
+
spinner.update(`${baseDetail} · pool de IA sem capacidade, tentando de novo em ${Math.round(waitMs / 1000)}s`);
|
|
186
|
+
await delay(waitMs, signal);
|
|
187
|
+
if (signal?.aborted)
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
145
192
|
/**
|
|
146
193
|
* `history` e mutado e reaproveitado entre chamadas: e o que da ao modo
|
|
147
194
|
* interativo memoria de conversa entre turnos do REPL (como Claude Code/Codex),
|
|
@@ -155,10 +202,16 @@ async function callGatewayWithProgress(settings, messages) {
|
|
|
155
202
|
* AI Gateway inteiro. Cada chamada de runAgentTask e um ciclo completo
|
|
156
203
|
* (comeca e termina em idle/applied/failed/aborted).
|
|
157
204
|
*/
|
|
158
|
-
export async function runAgentTask(task, settings, confirm, history = [], onState) {
|
|
205
|
+
export async function runAgentTask(task, settings, confirm, history = [], onState, options = {}) {
|
|
159
206
|
if (!getActiveTokenSync()) {
|
|
160
207
|
throw new Error('Sessao Neetru ausente ou expirada. Rode `neetru login`.');
|
|
161
208
|
}
|
|
209
|
+
const { silent = false, signal } = options;
|
|
210
|
+
// silent (subagente): nada vai pro terminal enquanto roda — o resultado é
|
|
211
|
+
// CAPTURADO no retorno e consultado depois. `emit` vira no-op e o spinner é
|
|
212
|
+
// inerte, senão a saída colidiria com o spinner da tarefa em primeiro plano.
|
|
213
|
+
const emit = silent ? (() => { }) : printEvent;
|
|
214
|
+
const makeSpinner = () => (silent ? NOOP_TURN_SPINNER : startTurnSpinner('neetru code'));
|
|
162
215
|
const machine = new CodeAgentStateMachine();
|
|
163
216
|
const goto = (next) => {
|
|
164
217
|
machine.transition(next);
|
|
@@ -172,8 +225,23 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
172
225
|
role: 'user',
|
|
173
226
|
content: isFirstTurn ? buildInitialPrompt(task, settings) : task,
|
|
174
227
|
});
|
|
228
|
+
// Um unico spinner vivo pra tarefa inteira (nao uma linha por evento) —
|
|
229
|
+
// achado de uso real: turno/tool-call/tool-result como linhas permanentes
|
|
230
|
+
// poluiam o historico. O detail acumula "turno X/Y" + estagio atual;
|
|
231
|
+
// avisos/erros REAIS (compactacao, fallback de streaming) continuam
|
|
232
|
+
// aparecendo como linha permanente via printEvent, pausando o spinner
|
|
233
|
+
// pra nao interlear as duas saidas.
|
|
234
|
+
let spinner = makeSpinner();
|
|
175
235
|
try {
|
|
176
236
|
for (let turn = 1; turn <= settings.maxTurns; turn++) {
|
|
237
|
+
// Cancelamento cooperativo: verificado na fronteira de cada turno. A
|
|
238
|
+
// chamada de rede em voo tem timeout proprio (code-agent-gateway.ts), que
|
|
239
|
+
// este modulo nao controla sem editar aquele arquivo — o corte efetivo
|
|
240
|
+
// acontece antes de iniciar o proximo turno.
|
|
241
|
+
if (signal?.aborted) {
|
|
242
|
+
spinner.stop();
|
|
243
|
+
return { kind: 'aborted' };
|
|
244
|
+
}
|
|
177
245
|
goto('thinking');
|
|
178
246
|
// GAP-M1-15: auto-compact por TAMANHO antes de cada chamada — sem isso
|
|
179
247
|
// uma sessao com varias tool results grandes (cada uma ate
|
|
@@ -183,7 +251,9 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
183
251
|
// antes disso se quiser.
|
|
184
252
|
const autoCompacted = compactHistoryToFit(messages, MAX_HISTORY_TOTAL_CHARS);
|
|
185
253
|
if (autoCompacted > 0) {
|
|
186
|
-
|
|
254
|
+
spinner.stop();
|
|
255
|
+
emit('warn', 'Historico compactado automaticamente', `${autoCompacted} mensagem(ns) antiga(s) removida(s)`);
|
|
256
|
+
spinner = makeSpinner();
|
|
187
257
|
}
|
|
188
258
|
// Rede de seguranca contra loop de tool-calls que nunca conclui (achado
|
|
189
259
|
// em producao: "oi" gastou os 8 turnos so chamando tools e falhou com
|
|
@@ -196,16 +266,19 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
196
266
|
'texto usando o que voce ja sabe — nao peca mais NEETRU_TOOL nem proponha patch.',
|
|
197
267
|
});
|
|
198
268
|
}
|
|
199
|
-
|
|
200
|
-
|
|
269
|
+
const baseDetail = `turno ${turn}/${settings.maxTurns}`;
|
|
270
|
+
spinner.update(baseDetail);
|
|
271
|
+
const gatewayResult = await callGatewayWithProgressRetrying(settings, messages, spinner, baseDetail, silent, signal);
|
|
272
|
+
spinner = gatewayResult.spinner;
|
|
273
|
+
const content = gatewayResult.content;
|
|
201
274
|
messages.push({ role: 'assistant', content });
|
|
202
275
|
const tool = parseToolRequest(content);
|
|
203
276
|
if (tool) {
|
|
204
277
|
goto('tool_requested');
|
|
205
|
-
|
|
278
|
+
spinner.update(`${baseDetail} · ${tool.tool}`);
|
|
206
279
|
goto('tool_running');
|
|
207
|
-
const result = runCodeTool(settings.cwd, tool);
|
|
208
|
-
|
|
280
|
+
const result = await runCodeTool(settings.cwd, tool, { settings, confirm });
|
|
281
|
+
spinner.update(`${baseDetail} · ${summarizeToolResult(result)}`);
|
|
209
282
|
messages.push({
|
|
210
283
|
role: 'user',
|
|
211
284
|
content: truncateForGateway(`Resultado da tool ${tool.tool}:\n\n${result}`),
|
|
@@ -214,48 +287,191 @@ export async function runAgentTask(task, settings, confirm, history = [], onStat
|
|
|
214
287
|
}
|
|
215
288
|
const patch = extractPatch(content);
|
|
216
289
|
if (patch) {
|
|
290
|
+
spinner.stop();
|
|
217
291
|
goto('patch_ready');
|
|
218
|
-
|
|
219
|
-
|
|
292
|
+
if (!silent) {
|
|
293
|
+
console.log();
|
|
294
|
+
printPatchPreview(patch);
|
|
295
|
+
}
|
|
220
296
|
if (!settings.apply) {
|
|
221
|
-
|
|
297
|
+
// Subagentes SEMPRE caem aqui (apply forçado a false): propõem o
|
|
298
|
+
// patch, nunca aplicam. Quem aplica é a sessão principal via /agents.
|
|
299
|
+
emit('warn', 'Patch nao aplicado', '--plan/--no-apply');
|
|
222
300
|
goto('aborted');
|
|
223
|
-
return;
|
|
301
|
+
return { kind: 'patch', patch };
|
|
224
302
|
}
|
|
225
303
|
const approved = settings.yes || (await (async () => {
|
|
226
304
|
goto('awaiting_confirm');
|
|
227
305
|
return confirm('\nAplicar este patch? [y/N] ');
|
|
228
306
|
})());
|
|
229
307
|
if (!approved) {
|
|
230
|
-
|
|
308
|
+
emit('warn', 'Patch nao aplicado');
|
|
231
309
|
goto('aborted');
|
|
232
|
-
return;
|
|
310
|
+
return { kind: 'aborted' };
|
|
233
311
|
}
|
|
234
312
|
applyUnifiedPatch(settings.cwd, patch);
|
|
235
313
|
logAppliedPatch(settings.cwd, patchSummaryRows(patch)[0] ?? 'arquivos: nao detectado');
|
|
236
|
-
|
|
314
|
+
emit('ok', 'Patch aplicado', 'git apply');
|
|
237
315
|
goto('applied');
|
|
238
|
-
return;
|
|
316
|
+
return { kind: 'applied', patch };
|
|
317
|
+
}
|
|
318
|
+
spinner.stop();
|
|
319
|
+
if (!silent) {
|
|
320
|
+
console.log();
|
|
321
|
+
printAssistantText(content);
|
|
239
322
|
}
|
|
240
|
-
console.log();
|
|
241
|
-
printAssistantText(content);
|
|
242
323
|
goto('idle');
|
|
243
|
-
return;
|
|
324
|
+
return { kind: 'text', text: content };
|
|
244
325
|
}
|
|
326
|
+
spinner.stop();
|
|
245
327
|
goto('failed');
|
|
246
328
|
throw new Error(`Limite de turnos atingido (${settings.maxTurns}) sem resposta final.`);
|
|
247
329
|
}
|
|
248
330
|
catch (error) {
|
|
331
|
+
spinner.stop();
|
|
249
332
|
if (machine.state !== 'failed' && machine.state !== 'aborted' && machine.state !== 'applied') {
|
|
250
333
|
goto('failed');
|
|
251
334
|
}
|
|
252
335
|
throw error;
|
|
253
336
|
}
|
|
254
337
|
}
|
|
338
|
+
// O REPL (runInteractive) chama startSubagent; o orquestrador chama de volta
|
|
339
|
+
// runAgentTask via este runner injetado. Injeção (não import) mantém o grafo de
|
|
340
|
+
// imports unidirecional: commands/code.ts -> code-agent-subagents.ts, sem ciclo.
|
|
341
|
+
configureSubagentRunner(runAgentTask);
|
|
255
342
|
function printFileSampleForSettings(settings) {
|
|
256
343
|
const all = listFiles(settings.cwd);
|
|
257
344
|
printFileSample(all.slice(0, 60), all.length);
|
|
258
345
|
}
|
|
346
|
+
function formatSubagentDuration(s) {
|
|
347
|
+
const end = s.finishedAt ?? Date.now();
|
|
348
|
+
const secs = Math.max(0, Math.round((end - s.startedAt) / 1000));
|
|
349
|
+
return `${secs}s`;
|
|
350
|
+
}
|
|
351
|
+
function formatSubagentRow(s) {
|
|
352
|
+
const patchTag = s.hasPatch ? ' (patch)' : '';
|
|
353
|
+
return `#${s.id} [${s.status}] ${formatSubagentDuration(s)}${patchTag} — ${s.task}`;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* `/agents ...` — controla os subagentes (fan-out de subtarefas). Todo o texto,
|
|
357
|
+
* nomenclatura e fluxo aqui é próprio; a inspiração é só o princípio geral de
|
|
358
|
+
* orquestração (disparar, listar, parar, coletar, aplicar sob confirmação).
|
|
359
|
+
*
|
|
360
|
+
* `apply` reaproveita EXATAMENTE o mesmo caminho de confirmação de um patch
|
|
361
|
+
* normal (preview + confirm + applyUnifiedPatch + logAppliedPatch) — um patch de
|
|
362
|
+
* subagente nunca é aplicado automaticamente.
|
|
363
|
+
*/
|
|
364
|
+
async function runAgentsCommand(line, settings, confirm) {
|
|
365
|
+
const afterCmd = line.trim().slice('/agents'.length).trim();
|
|
366
|
+
const spaceIdx = afterCmd.indexOf(' ');
|
|
367
|
+
const sub = (spaceIdx === -1 ? afterCmd : afterCmd.slice(0, spaceIdx)).toLowerCase();
|
|
368
|
+
const arg = spaceIdx === -1 ? '' : afterCmd.slice(spaceIdx + 1).trim();
|
|
369
|
+
const idArg = arg.replace(/^#/, '').trim();
|
|
370
|
+
switch (sub) {
|
|
371
|
+
case '':
|
|
372
|
+
case 'list': {
|
|
373
|
+
const all = listSubagents();
|
|
374
|
+
if (all.length === 0) {
|
|
375
|
+
printEvent('ok', 'Nenhum subagente', 'use /agents start <tarefa>');
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
printBox('subagentes', all.map(formatSubagentRow));
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
case 'start': {
|
|
382
|
+
if (!arg) {
|
|
383
|
+
printEvent('warn', 'Uso', '/agents start <tarefa>');
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
const id = startSubagent(arg, settings);
|
|
388
|
+
printEvent('ok', 'Subagente iniciado', `#${id}: ${arg}`);
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
printEvent('error', 'Nao foi possivel iniciar subagente', error instanceof Error ? error.message : String(error));
|
|
392
|
+
}
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
case 'stop': {
|
|
396
|
+
if (!idArg) {
|
|
397
|
+
printEvent('warn', 'Uso', '/agents stop <id>');
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const ok = stopSubagent(idArg);
|
|
401
|
+
printEvent(ok ? 'ok' : 'warn', ok ? 'Sinal de parada enviado' : 'Subagente nao esta em execucao', `#${idArg}`);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
case 'result': {
|
|
405
|
+
if (!idArg) {
|
|
406
|
+
printEvent('warn', 'Uso', '/agents result <id>');
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const record = getSubagentResult(idArg);
|
|
410
|
+
if (!record) {
|
|
411
|
+
printEvent('warn', 'Subagente nao encontrado', `#${idArg}`);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (record.status === 'running') {
|
|
415
|
+
printEvent('ok', 'Ainda em execucao', `#${idArg}`);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (record.status === 'stopped') {
|
|
419
|
+
printEvent('warn', 'Subagente parado', `#${idArg}`);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (record.error || record.status === 'failed') {
|
|
423
|
+
printEvent('error', 'Subagente falhou', record.error ?? `#${idArg}`);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const outcome = record.outcome;
|
|
427
|
+
if (outcome?.kind === 'patch' && outcome.patch) {
|
|
428
|
+
printEvent('ok', 'Patch proposto', `#${idArg} — /agents apply ${idArg} pra aplicar`);
|
|
429
|
+
printPatchPreview(outcome.patch);
|
|
430
|
+
}
|
|
431
|
+
else if (outcome?.kind === 'text') {
|
|
432
|
+
printAssistantText(outcome.text ?? '(sem texto)');
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
printEvent('warn', 'Sem resultado aplicavel', `#${idArg}`);
|
|
436
|
+
}
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
case 'apply': {
|
|
440
|
+
if (!idArg) {
|
|
441
|
+
printEvent('warn', 'Uso', '/agents apply <id>');
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const record = getSubagentResult(idArg);
|
|
445
|
+
if (!record) {
|
|
446
|
+
printEvent('warn', 'Subagente nao encontrado', `#${idArg}`);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const patch = getApplicablePatch(idArg);
|
|
450
|
+
if (!patch) {
|
|
451
|
+
printEvent('warn', 'Nada pra aplicar', `#${idArg} (${record.status}${record.outcome ? `, ${record.outcome.kind}` : ''})`);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
console.log();
|
|
455
|
+
printPatchPreview(patch);
|
|
456
|
+
const approved = settings.yes || (await confirm('\nAplicar patch do subagente? [y/N] '));
|
|
457
|
+
if (!approved) {
|
|
458
|
+
printEvent('warn', 'Patch nao aplicado');
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
try {
|
|
462
|
+
applyUnifiedPatch(settings.cwd, patch);
|
|
463
|
+
logAppliedPatch(settings.cwd, patchSummaryRows(patch)[0] ?? 'arquivos: nao detectado');
|
|
464
|
+
printEvent('ok', 'Patch aplicado', `git apply · subagente #${idArg}`);
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
printEvent('error', 'Falha ao aplicar patch', error instanceof Error ? error.message : String(error));
|
|
468
|
+
}
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
default:
|
|
472
|
+
printEvent('warn', 'Subcomando desconhecido', 'use: start | list | stop | result | apply');
|
|
473
|
+
}
|
|
474
|
+
}
|
|
259
475
|
async function runInteractive(settings) {
|
|
260
476
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
261
477
|
throw new Error('Modo interativo exige terminal TTY. Use: neetru code "tarefa".');
|
|
@@ -265,171 +481,238 @@ async function runInteractive(settings) {
|
|
|
265
481
|
const { resolveSessionEmail } = await import('../lib/auth.js');
|
|
266
482
|
const sessionEmail = await resolveSessionEmail().catch(() => null);
|
|
267
483
|
let history = [];
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
484
|
+
// Notificação de conclusão de subagente: UMA linha permanente quando termina
|
|
485
|
+
// (sucesso/falha/parado). Enquanto rodam, subagentes são silenciosos — o
|
|
486
|
+
// LineBuffer do Ink commita esta única linha acima do prompt/spinner sem
|
|
487
|
+
// interlear. Registrado 1x por sessão; cancelado ao sair.
|
|
488
|
+
const unsubscribeSettled = onSubagentSettled((s) => {
|
|
489
|
+
if (s.status === 'done') {
|
|
490
|
+
printEvent('ok', s.hasPatch ? 'Subagente concluiu (patch proposto)' : 'Subagente concluiu', s.hasPatch ? `#${s.id} — /agents apply ${s.id}` : `#${s.id} — /agents result ${s.id}`);
|
|
491
|
+
}
|
|
492
|
+
else if (s.status === 'stopped') {
|
|
493
|
+
printEvent('warn', 'Subagente parado', `#${s.id}`);
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
printEvent('error', 'Subagente falhou', `#${s.id} — /agents result ${s.id}`);
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
try {
|
|
500
|
+
await runInkSession({
|
|
501
|
+
initialPromptLabel: renderPromptLabel(settings),
|
|
502
|
+
statusLine: '/help comandos · /exit sai · Ctrl+C cancela',
|
|
503
|
+
onStart: () => {
|
|
504
|
+
log.banner(sessionEmail);
|
|
283
505
|
renderHeader(settings, 'interactive');
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
506
|
+
if (settings.modelWarning)
|
|
507
|
+
log.warn(settings.modelWarning);
|
|
508
|
+
},
|
|
509
|
+
onLine: async (line, ctx) => {
|
|
510
|
+
const lower = line.toLowerCase();
|
|
511
|
+
if (lower === '/exit' || lower === '/sair' || lower === 'exit' || lower === 'quit')
|
|
512
|
+
return 'exit';
|
|
513
|
+
if (lower === '/agents' || lower.startsWith('/agents ')) {
|
|
514
|
+
await runAgentsCommand(line, settings, async (question) => {
|
|
515
|
+
const answer = await ctx.askLine(question);
|
|
516
|
+
return /^(y|yes|s|sim)$/i.test(answer.trim());
|
|
517
|
+
});
|
|
296
518
|
return 'continue';
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
519
|
+
}
|
|
520
|
+
if (lower === '/clear') {
|
|
521
|
+
history = [];
|
|
522
|
+
renderHeader(settings, 'interactive');
|
|
523
|
+
printEvent('ok', 'Conversa limpa');
|
|
301
524
|
return 'continue';
|
|
302
525
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (Number.isInteger(idx) && idx >= 1 && idx <= shown.length) {
|
|
307
|
-
picked.push(shown[idx - 1]);
|
|
308
|
-
}
|
|
526
|
+
if (lower === '/files') {
|
|
527
|
+
printFileSampleForSettings(settings);
|
|
528
|
+
return 'continue';
|
|
309
529
|
}
|
|
310
|
-
if (
|
|
311
|
-
|
|
530
|
+
if (lower === '/pick') {
|
|
531
|
+
const all = listFiles(settings.cwd);
|
|
532
|
+
const shown = all.slice(0, 60);
|
|
533
|
+
printFilePicker(shown, all.length);
|
|
534
|
+
if (shown.length === 0)
|
|
535
|
+
return 'continue';
|
|
536
|
+
const answer = await ctx.askLine('Numero(s) separado(s) por virgula (Enter cancela): ');
|
|
537
|
+
const raw = answer.trim();
|
|
538
|
+
if (!raw) {
|
|
539
|
+
printEvent('warn', 'Cancelado');
|
|
540
|
+
return 'continue';
|
|
541
|
+
}
|
|
542
|
+
const picked = [];
|
|
543
|
+
for (const part of raw.split(',')) {
|
|
544
|
+
const idx = Number.parseInt(part.trim(), 10);
|
|
545
|
+
if (Number.isInteger(idx) && idx >= 1 && idx <= shown.length) {
|
|
546
|
+
picked.push(shown[idx - 1]);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (picked.length === 0) {
|
|
550
|
+
printEvent('warn', 'Nenhum arquivo valido selecionado');
|
|
551
|
+
return 'continue';
|
|
552
|
+
}
|
|
553
|
+
const blocks = picked.map((file) => readFileContext(settings.cwd, file));
|
|
554
|
+
history.push({
|
|
555
|
+
role: 'user',
|
|
556
|
+
content: truncateForGateway(`Arquivos anexados manualmente via /pick:\n\n${blocks.join('\n\n')}`),
|
|
557
|
+
});
|
|
558
|
+
printEvent('ok', 'Arquivo(s) anexado(s) ao contexto', picked.join(', '));
|
|
312
559
|
return 'continue';
|
|
313
560
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
content: truncateForGateway(`Arquivos anexados manualmente via /pick:\n\n${blocks.join('\n\n')}`),
|
|
318
|
-
});
|
|
319
|
-
printEvent('ok', 'Arquivo(s) anexado(s) ao contexto', picked.join(', '));
|
|
320
|
-
return 'continue';
|
|
321
|
-
}
|
|
322
|
-
if (lower === '/overview' || lower === '/visao') {
|
|
323
|
-
printBox('visao do projeto', getProjectOverview(settings.cwd).split(/\r?\n/));
|
|
324
|
-
return 'continue';
|
|
325
|
-
}
|
|
326
|
-
if (lower === '/diff') {
|
|
327
|
-
const diff = getGitDiff(settings.cwd);
|
|
328
|
-
if (diff === '(sem diff)') {
|
|
329
|
-
printEvent('ok', 'Sem diff local');
|
|
561
|
+
if (lower === '/overview' || lower === '/visao') {
|
|
562
|
+
printBox('visao do projeto', getProjectOverview(settings.cwd).split(/\r?\n/));
|
|
563
|
+
return 'continue';
|
|
330
564
|
}
|
|
331
|
-
|
|
332
|
-
|
|
565
|
+
if (lower === '/diff') {
|
|
566
|
+
const diff = getGitDiff(settings.cwd);
|
|
567
|
+
if (diff === '(sem diff)') {
|
|
568
|
+
printEvent('ok', 'Sem diff local');
|
|
569
|
+
}
|
|
570
|
+
else if (diff.startsWith('ERRO:')) {
|
|
571
|
+
printEvent('error', 'git diff', diff);
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
printBox('diff atual', ['git diff -- .']);
|
|
575
|
+
process.stdout.write(renderColoredDiff(diff));
|
|
576
|
+
if (!diff.endsWith('\n'))
|
|
577
|
+
process.stdout.write('\n');
|
|
578
|
+
}
|
|
579
|
+
return 'continue';
|
|
333
580
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
581
|
+
if (lower === '/review') {
|
|
582
|
+
await runReviewCommand(settings, async (question) => {
|
|
583
|
+
const answer = await ctx.askLine(question);
|
|
584
|
+
return /^(y|yes|s|sim)$/i.test(answer.trim());
|
|
585
|
+
}, history);
|
|
586
|
+
return 'continue';
|
|
339
587
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
await runReviewCommand(settings, async (question) => {
|
|
344
|
-
const answer = await ctx.askLine(question);
|
|
345
|
-
return /^(y|yes|s|sim)$/i.test(answer.trim());
|
|
346
|
-
}, history);
|
|
347
|
-
return 'continue';
|
|
348
|
-
}
|
|
349
|
-
if (lower === '/save') {
|
|
350
|
-
const saved = saveCodeSession(settings, history);
|
|
351
|
-
printEvent('ok', 'Sessao salva', saved);
|
|
352
|
-
return 'continue';
|
|
353
|
-
}
|
|
354
|
-
if (lower === '/resume') {
|
|
355
|
-
const sessions = listCodeSessions(settings.cwd);
|
|
356
|
-
printSessionList(sessions);
|
|
357
|
-
if (sessions.length === 0)
|
|
588
|
+
if (lower === '/save') {
|
|
589
|
+
const saved = saveCodeSession(settings, history);
|
|
590
|
+
printEvent('ok', 'Sessao salva', saved);
|
|
358
591
|
return 'continue';
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
592
|
+
}
|
|
593
|
+
if (lower === '/resume') {
|
|
594
|
+
const sessions = listCodeSessions(settings.cwd);
|
|
595
|
+
printSessionList(sessions);
|
|
596
|
+
if (sessions.length === 0)
|
|
597
|
+
return 'continue';
|
|
598
|
+
if (history.length > 0) {
|
|
599
|
+
const overwrite = await ctx.askLine('Conversa atual sera substituida. Continuar? [y/N] ');
|
|
600
|
+
if (!/^(y|yes|s|sim)$/i.test(overwrite.trim())) {
|
|
601
|
+
printEvent('warn', 'Cancelado');
|
|
602
|
+
return 'continue';
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
const answer = await ctx.askLine('Numero da sessao (Enter cancela): ');
|
|
606
|
+
const idx = Number.parseInt(answer.trim(), 10);
|
|
607
|
+
if (!Number.isInteger(idx) || idx < 1 || idx > sessions.length) {
|
|
362
608
|
printEvent('warn', 'Cancelado');
|
|
363
609
|
return 'continue';
|
|
364
610
|
}
|
|
611
|
+
const picked = sessions[idx - 1];
|
|
612
|
+
try {
|
|
613
|
+
history = loadCodeSession(settings.cwd, picked.file);
|
|
614
|
+
printEvent('ok', 'Sessao retomada', `${picked.file} (${history.length} mensagem(ns))`);
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
printEvent('error', 'Falha ao retomar sessao', error instanceof Error ? error.message : String(error));
|
|
618
|
+
}
|
|
619
|
+
return 'continue';
|
|
620
|
+
}
|
|
621
|
+
if (lower === '/compact') {
|
|
622
|
+
const removed = compactCodeHistory(history);
|
|
623
|
+
printEvent('ok', 'Historico compactado', `${removed} mensagem(ns) removida(s)`);
|
|
624
|
+
return 'continue';
|
|
625
|
+
}
|
|
626
|
+
if (lower === '/status' || lower === '/context') {
|
|
627
|
+
renderSessionStatus(settings, history);
|
|
628
|
+
return 'continue';
|
|
629
|
+
}
|
|
630
|
+
if (lower === '/model') {
|
|
631
|
+
printBox('modelo', [settings.model]);
|
|
632
|
+
return 'continue';
|
|
633
|
+
}
|
|
634
|
+
if (lower === '/effort' || lower.startsWith('/effort ')) {
|
|
635
|
+
const arg = line.trim().slice('/effort'.length).trim().toLowerCase();
|
|
636
|
+
if (!arg) {
|
|
637
|
+
printEvent('ok', 'Effort atual', settings.effort ?? 'default do provider');
|
|
638
|
+
return 'continue';
|
|
639
|
+
}
|
|
640
|
+
const next = resolveCodeEffort(arg);
|
|
641
|
+
if (!next) {
|
|
642
|
+
printEvent('warn', 'Nivel invalido', 'use: low | medium | high');
|
|
643
|
+
return 'continue';
|
|
644
|
+
}
|
|
645
|
+
settings.effort = next;
|
|
646
|
+
printEvent('ok', 'Effort atualizado', next);
|
|
647
|
+
return 'continue';
|
|
648
|
+
}
|
|
649
|
+
if (lower === '/ultracode') {
|
|
650
|
+
settings.ultracode = !settings.ultracode;
|
|
651
|
+
if (settings.ultracode) {
|
|
652
|
+
settings.maxTurns = ULTRACODE_MAX_TURNS;
|
|
653
|
+
settings.maxTokens = ULTRACODE_MAX_TOKENS;
|
|
654
|
+
settings.effort = ULTRACODE_EFFORT;
|
|
655
|
+
printEvent('ok', 'Ultracode ligado', `${ULTRACODE_MAX_TURNS} turnos · ${ULTRACODE_MAX_TOKENS} tokens · effort ${ULTRACODE_EFFORT}`);
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
settings.maxTurns = DEFAULT_CODE_MAX_TURNS;
|
|
659
|
+
settings.maxTokens = DEFAULT_CODE_MAX_TOKENS;
|
|
660
|
+
settings.effort = resolveCodeEffort(undefined, config.get('gatewayEffort'));
|
|
661
|
+
printEvent('ok', 'Ultracode desligado', `voltou pro default (${DEFAULT_CODE_MAX_TURNS} turnos)`);
|
|
662
|
+
}
|
|
663
|
+
return 'continue';
|
|
664
|
+
}
|
|
665
|
+
if (lower === '/plan') {
|
|
666
|
+
settings.apply = false;
|
|
667
|
+
printEvent('ok', 'Modo plano', 'patches nao serao aplicados');
|
|
668
|
+
return 'continue';
|
|
365
669
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
printEvent('
|
|
670
|
+
if (lower === '/apply') {
|
|
671
|
+
settings.apply = true;
|
|
672
|
+
settings.yes = false;
|
|
673
|
+
printEvent('ok', 'Modo aplicar', 'pedira confirmacao antes do git apply');
|
|
674
|
+
return 'continue';
|
|
675
|
+
}
|
|
676
|
+
if (lower === '/yes') {
|
|
677
|
+
settings.apply = true;
|
|
678
|
+
settings.yes = true;
|
|
679
|
+
printEvent('warn', 'Auto-aplicar ativo', 'patches validos serao aplicados sem pergunta');
|
|
680
|
+
return 'continue';
|
|
681
|
+
}
|
|
682
|
+
if (lower === '/no') {
|
|
683
|
+
settings.yes = false;
|
|
684
|
+
printEvent('ok', 'Confirmacao manual ativa');
|
|
685
|
+
return 'continue';
|
|
686
|
+
}
|
|
687
|
+
if (lower === '/help' || lower === '/ajuda') {
|
|
688
|
+
renderInteractiveHelp();
|
|
370
689
|
return 'continue';
|
|
371
690
|
}
|
|
372
|
-
const picked = sessions[idx - 1];
|
|
373
691
|
try {
|
|
374
|
-
|
|
375
|
-
|
|
692
|
+
await runAgentTask(line, settings, async (question) => {
|
|
693
|
+
const answer = await ctx.askLine(question);
|
|
694
|
+
return /^(y|yes|s|sim)$/i.test(answer.trim());
|
|
695
|
+
}, history);
|
|
376
696
|
}
|
|
377
697
|
catch (error) {
|
|
378
|
-
printEvent('error', 'Falha
|
|
698
|
+
printEvent('error', 'Falha', error instanceof Error ? error.message : String(error));
|
|
379
699
|
}
|
|
700
|
+
// GAP-M1-18: checkpoint automatico apos cada turno (sucesso OU falha —
|
|
701
|
+
// a conversa ate aqui e valida de qualquer jeito) pra sessao longa
|
|
702
|
+
// sobreviver a fechar o terminal sem lembrar de `/save`. Silencioso
|
|
703
|
+
// (sem printEvent) e best-effort — nunca deve virar ruido visual nem
|
|
704
|
+
// interromper a sessao.
|
|
705
|
+
autoSaveCodeSession(settings, history);
|
|
380
706
|
return 'continue';
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
if (lower === '/model') {
|
|
392
|
-
printBox('modelo', [settings.model]);
|
|
393
|
-
return 'continue';
|
|
394
|
-
}
|
|
395
|
-
if (lower === '/plan') {
|
|
396
|
-
settings.apply = false;
|
|
397
|
-
printEvent('ok', 'Modo plano', 'patches nao serao aplicados');
|
|
398
|
-
return 'continue';
|
|
399
|
-
}
|
|
400
|
-
if (lower === '/apply') {
|
|
401
|
-
settings.apply = true;
|
|
402
|
-
settings.yes = false;
|
|
403
|
-
printEvent('ok', 'Modo aplicar', 'pedira confirmacao antes do git apply');
|
|
404
|
-
return 'continue';
|
|
405
|
-
}
|
|
406
|
-
if (lower === '/yes') {
|
|
407
|
-
settings.apply = true;
|
|
408
|
-
settings.yes = true;
|
|
409
|
-
printEvent('warn', 'Auto-aplicar ativo', 'patches validos serao aplicados sem pergunta');
|
|
410
|
-
return 'continue';
|
|
411
|
-
}
|
|
412
|
-
if (lower === '/no') {
|
|
413
|
-
settings.yes = false;
|
|
414
|
-
printEvent('ok', 'Confirmacao manual ativa');
|
|
415
|
-
return 'continue';
|
|
416
|
-
}
|
|
417
|
-
if (lower === '/help' || lower === '/ajuda') {
|
|
418
|
-
renderInteractiveHelp();
|
|
419
|
-
return 'continue';
|
|
420
|
-
}
|
|
421
|
-
try {
|
|
422
|
-
await runAgentTask(line, settings, async (question) => {
|
|
423
|
-
const answer = await ctx.askLine(question);
|
|
424
|
-
return /^(y|yes|s|sim)$/i.test(answer.trim());
|
|
425
|
-
}, history);
|
|
426
|
-
}
|
|
427
|
-
catch (error) {
|
|
428
|
-
printEvent('error', 'Falha', error instanceof Error ? error.message : String(error));
|
|
429
|
-
}
|
|
430
|
-
return 'continue';
|
|
431
|
-
},
|
|
432
|
-
});
|
|
707
|
+
},
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
finally {
|
|
711
|
+
unsubscribeSettled();
|
|
712
|
+
// Ao sair da sessão, corta subagentes ainda em execução — sem isso ficariam
|
|
713
|
+
// órfãos consumindo o AI Gateway em background depois do REPL fechar.
|
|
714
|
+
stopAllSubagents();
|
|
715
|
+
}
|
|
433
716
|
}
|
|
434
717
|
function runDoctor(settings) {
|
|
435
718
|
const token = getActiveTokenSync();
|