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