@ryuu-reinzz/ryuu-cli 0.0.0-stage → 0.3.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 +560 -0
- package/LICENSE +21 -0
- package/README.md +1213 -2
- package/package.json +45 -4
- package/src/agent/agent.js +1194 -0
- package/src/agent/approvals.js +76 -0
- package/src/agent/context.js +106 -0
- package/src/agent/planner.js +237 -0
- package/src/agent/worklog.js +318 -0
- package/src/cli/app.js +463 -0
- package/src/cli/commands.js +1150 -0
- package/src/cli/input.js +287 -0
- package/src/cli/markdown.js +527 -0
- package/src/cli/prompts.js +255 -0
- package/src/cli/renderer.js +2155 -0
- package/src/cli/settings-editor.js +280 -0
- package/src/cli/settings-tui.js +669 -0
- package/src/cli/tui.js +1737 -0
- package/src/cli/width.js +48 -0
- package/src/config/load.js +132 -0
- package/src/config/scaffold.js +50 -0
- package/src/config/settings-registry.js +495 -0
- package/src/config/settings.js +304 -0
- package/src/config/validate.js +451 -0
- package/src/index.js +449 -0
- package/src/providers/catalog.js +282 -0
- package/src/providers/index.js +84 -0
- package/src/providers/nvidia.js +645 -0
- package/src/providers/registry.js +104 -0
- package/src/services/clipboard.js +38 -0
- package/src/services/diff.js +153 -0
- package/src/services/doctor.js +126 -0
- package/src/services/logger.js +190 -0
- package/src/services/preferences.js +249 -0
- package/src/services/processes.js +345 -0
- package/src/services/provider-settings.js +226 -0
- package/src/services/sessions.js +279 -0
- package/src/services/undo.js +133 -0
- package/src/services/ushell.js +146 -0
- package/src/services/websearch/manager.js +616 -0
- package/src/services/websearch/mcp.js +233 -0
- package/src/services/websearch/normalize.js +284 -0
- package/src/services/websearch/providers.js +178 -0
- package/src/services/websearch/serper.js +263 -0
- package/src/services/workspace.js +589 -0
- package/src/tools/filesystem.js +304 -0
- package/src/tools/git.js +114 -0
- package/src/tools/index.js +48 -0
- package/src/tools/project.js +160 -0
- package/src/tools/registry.js +118 -0
- package/src/tools/search.js +210 -0
- package/src/tools/shell.js +279 -0
- package/src/tools/url.js +542 -0
- package/src/tools/web.js +67 -0
- package/src/utils/errors.js +160 -0
- package/src/utils/network.js +129 -0
- package/src/utils/redaction.js +73 -0
- package/src/utils/validation.js +370 -0
- package/src/utils/version.js +24 -0
package/src/cli/app.js
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { Workspace } from '../services/workspace.js';
|
|
4
|
+
import { ProcessManager } from '../services/processes.js';
|
|
5
|
+
import { SessionStore, ProjectSummaryCache } from '../services/sessions.js';
|
|
6
|
+
import { UndoManager } from '../services/undo.js';
|
|
7
|
+
import { createLogger } from '../services/logger.js';
|
|
8
|
+
import { buildProjectSummary } from '../tools/project.js';
|
|
9
|
+
import { buildToolRegistry } from '../tools/index.js';
|
|
10
|
+
import { createActiveProvider, selectionFor } from '../providers/index.js';
|
|
11
|
+
import { buildProviders, parseExtraProviders } from '../providers/catalog.js';
|
|
12
|
+
import { buildRawConfig } from '../config/load.js';
|
|
13
|
+
import { readSettings } from '../config/settings.js';
|
|
14
|
+
import { Agent } from '../agent/agent.js';
|
|
15
|
+
import { ApprovalManager } from '../agent/approvals.js';
|
|
16
|
+
import { Renderer } from './renderer.js';
|
|
17
|
+
import { Tui } from './tui.js';
|
|
18
|
+
import { PromptSession } from './prompts.js';
|
|
19
|
+
import { createCommands, parseCommandLine, shellCommandFromLine } from './commands.js';
|
|
20
|
+
import { runDoctor } from '../services/doctor.js';
|
|
21
|
+
import { readProviderModel, stateDirFor, writeProviderPreference } from '../services/preferences.js';
|
|
22
|
+
import { RyuuError } from '../utils/errors.js';
|
|
23
|
+
import { readVersion } from '../utils/version.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read one task from the prompt. Enter sends the task immediately, which is
|
|
27
|
+
* what a terminal user expects; a trailing backslash keeps typing on the next
|
|
28
|
+
* line (`...\` then a line without `\` finishes). The old contract — type the
|
|
29
|
+
* task, then press Enter again on an empty line — made the CLI look frozen: it
|
|
30
|
+
* sat on a bare `>` prompt with no task uplink and no spinner.
|
|
31
|
+
*
|
|
32
|
+
* Exported for tests: `ask` is injected so no readline is involved.
|
|
33
|
+
*/
|
|
34
|
+
export async function collectTask({ firstLine, ask, onContinue = null }) {
|
|
35
|
+
const lines = [String(firstLine ?? '')];
|
|
36
|
+
while (lines[lines.length - 1].endsWith('\\')) {
|
|
37
|
+
lines[lines.length - 1] = lines[lines.length - 1].slice(0, -1).replace(/\s+$/, '');
|
|
38
|
+
if (onContinue) onContinue();
|
|
39
|
+
const line = await ask(' ↳ ');
|
|
40
|
+
if (line === null) return null; // Ctrl+C / EOF while composing
|
|
41
|
+
lines.push(line);
|
|
42
|
+
}
|
|
43
|
+
return lines.join('\n').trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Wire the UI, agent, tools, provider, processes and local state together.
|
|
48
|
+
* Everything is dependency-injectable so integration tests can use a fake
|
|
49
|
+
* provider without touching the network.
|
|
50
|
+
*/
|
|
51
|
+
export async function createApp({
|
|
52
|
+
config,
|
|
53
|
+
cliOptions = {},
|
|
54
|
+
stdout = process.stdout,
|
|
55
|
+
stderr = process.stderr,
|
|
56
|
+
input = process.stdin,
|
|
57
|
+
provider: injectedProvider = null,
|
|
58
|
+
registry: injectedRegistry = null,
|
|
59
|
+
renderer: injectedRenderer = null,
|
|
60
|
+
webManager: injectedWebManager = null,
|
|
61
|
+
version = readVersion(),
|
|
62
|
+
} = {}) {
|
|
63
|
+
const interactive = cliOptions.interactive ?? Boolean(input.isTTY && stdout.isTTY);
|
|
64
|
+
// --tui needs a real terminal; otherwise the line interface is used, which is
|
|
65
|
+
// the default and stays untouched.
|
|
66
|
+
const useTui =
|
|
67
|
+
Boolean(cliOptions.tui) && interactive && !cliOptions.plain && Boolean(input.isTTY && stdout.isTTY);
|
|
68
|
+
if (cliOptions.tui && !useTui) {
|
|
69
|
+
stderr?.write?.('[warn] --tui needs an interactive terminal (and cannot be combined with --plain); using the line interface.\n');
|
|
70
|
+
}
|
|
71
|
+
const tui = useTui ? new Tui({ input, output: stdout, version, color: cliOptions.color ?? null }) : null;
|
|
72
|
+
const renderer = injectedRenderer || tui || new Renderer({
|
|
73
|
+
stdout,
|
|
74
|
+
stderr,
|
|
75
|
+
plain: Boolean(cliOptions.plain),
|
|
76
|
+
color: cliOptions.color ?? null,
|
|
77
|
+
debug: Boolean(config.debug),
|
|
78
|
+
});
|
|
79
|
+
const providerSecrets = (config.providers || []).map((entry) => entry.apiKey);
|
|
80
|
+
const searchSecrets = [
|
|
81
|
+
config.webSearchApiKey,
|
|
82
|
+
config.youApiKey,
|
|
83
|
+
config.firecrawlApiKey,
|
|
84
|
+
config.tavilyApiKey,
|
|
85
|
+
].filter(Boolean);
|
|
86
|
+
const logger = createLogger(config, { stderr, secrets: [...providerSecrets, ...searchSecrets] });
|
|
87
|
+
if (config.webMcpDebug) logger.setLevel('debug');
|
|
88
|
+
const workspace = new Workspace(config.workspace, { logger });
|
|
89
|
+
const processes = new ProcessManager({
|
|
90
|
+
workspace,
|
|
91
|
+
logger,
|
|
92
|
+
logDir: config.logDir,
|
|
93
|
+
maxBufferBytes: Math.max(65536, config.maxToolOutputBytes),
|
|
94
|
+
defaultTimeoutMs: config.requestTimeoutMs,
|
|
95
|
+
});
|
|
96
|
+
const sessionStore = new SessionStore({
|
|
97
|
+
dir: config.historyDir,
|
|
98
|
+
secrets: [config.apiKey, ...providerSecrets, ...searchSecrets],
|
|
99
|
+
logger,
|
|
100
|
+
});
|
|
101
|
+
const undo = new UndoManager({ workspace, dir: config.undoDir, logger });
|
|
102
|
+
const registry = injectedRegistry || buildToolRegistry({
|
|
103
|
+
workspace,
|
|
104
|
+
config,
|
|
105
|
+
processes,
|
|
106
|
+
logger,
|
|
107
|
+
webManager: injectedWebManager,
|
|
108
|
+
onEvent: (event) => renderer.webSearchStatus?.(event),
|
|
109
|
+
});
|
|
110
|
+
const provider = injectedProvider || createActiveProvider(config, { logger });
|
|
111
|
+
if (!provider.config) provider.config = config;
|
|
112
|
+
provider.processes = processes;
|
|
113
|
+
const projectCache = new ProjectSummaryCache({ file: path.join(config.cacheDir, 'project-summary.json') });
|
|
114
|
+
|
|
115
|
+
let prompts = null;
|
|
116
|
+
let awaitingApproval = false;
|
|
117
|
+
let lastSummary = null;
|
|
118
|
+
let activeTask = null;
|
|
119
|
+
let stopping = false;
|
|
120
|
+
let lastSigintAt = 0;
|
|
121
|
+
let projectSummary = null;
|
|
122
|
+
let closed = false;
|
|
123
|
+
let approvals = null;
|
|
124
|
+
// Assigned below; declared here so the Ctrl+C handler can read it safely.
|
|
125
|
+
let state = null;
|
|
126
|
+
|
|
127
|
+
// Live stdout/stderr from a running command, streamed into the activity
|
|
128
|
+
// region while the tool is still running instead of after it exits.
|
|
129
|
+
processes.on('output', ({ stream, text, record }) => {
|
|
130
|
+
if (!activeTask || record?.status !== 'running') return;
|
|
131
|
+
if (typeof renderer.processOutput !== 'function') return;
|
|
132
|
+
renderer.processOutput({ id: record.id, stream, text });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const onSigint = () => {
|
|
136
|
+
if (state?.activeShell) {
|
|
137
|
+
state.activeShell.controller.abort();
|
|
138
|
+
renderer.warn('Cancelling shell command…');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (activeTask) {
|
|
142
|
+
activeTask.controller.abort();
|
|
143
|
+
renderer.warn('Cancelling task…');
|
|
144
|
+
if (awaitingApproval) prompts?.reset();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const now = Date.now();
|
|
148
|
+
if (now - lastSigintAt < 1500) {
|
|
149
|
+
stopping = true;
|
|
150
|
+
prompts?.close();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
lastSigintAt = now;
|
|
154
|
+
renderer.info('Press Ctrl+C again to exit, or type /exit.');
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (interactive) {
|
|
158
|
+
if (tui) {
|
|
159
|
+
// The TUI is both the renderer and the prompt: its composer lives in the
|
|
160
|
+
// same retained frame as the transcript.
|
|
161
|
+
tui.onSigint = onSigint;
|
|
162
|
+
prompts = tui;
|
|
163
|
+
} else {
|
|
164
|
+
prompts = new PromptSession({ input, output: stdout, onSigint, mouse: Boolean(renderer.ansi) });
|
|
165
|
+
// Clicks, panel keys and the panel's ability to sit above the prompt.
|
|
166
|
+
prompts.isOverlayOpen = () => renderer.isBlockViewOpen?.() ?? false;
|
|
167
|
+
prompts.onMouse = (event) => renderer.handleMouse?.(event);
|
|
168
|
+
prompts.onOverlayKey = (key) => renderer.handleBlockViewKey?.(key);
|
|
169
|
+
prompts.onBeforeSubmit = () => renderer.closeBlockView?.();
|
|
170
|
+
renderer.setPromptController?.({
|
|
171
|
+
metrics: () => prompts.metrics(),
|
|
172
|
+
repaint: () => prompts.repaint(),
|
|
173
|
+
resetCursor: () => prompts.resetCursor(),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const confirm = async (request) => {
|
|
179
|
+
if (!prompts) return false;
|
|
180
|
+
renderer.approvalRequest(request);
|
|
181
|
+
awaitingApproval = true;
|
|
182
|
+
try {
|
|
183
|
+
const answer = await prompts.confirm(' Approve? [y/N/a] ');
|
|
184
|
+
if (answer.all) {
|
|
185
|
+
approvals.autoApproveWrites = true;
|
|
186
|
+
renderer.info('Writes will be auto-approved for the rest of this session. Commands still require approval.');
|
|
187
|
+
}
|
|
188
|
+
return answer.value;
|
|
189
|
+
} finally {
|
|
190
|
+
awaitingApproval = false;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
approvals = new ApprovalManager({
|
|
195
|
+
policy: config.approvalPolicy,
|
|
196
|
+
logger,
|
|
197
|
+
interactive,
|
|
198
|
+
confirm: prompts ? confirm : null,
|
|
199
|
+
autoApproveWrites: Boolean(cliOptions.yes),
|
|
200
|
+
autoApproveAll: Boolean(cliOptions.autoApprove),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const agent = new Agent({
|
|
204
|
+
provider,
|
|
205
|
+
registry,
|
|
206
|
+
workspace,
|
|
207
|
+
config,
|
|
208
|
+
renderer,
|
|
209
|
+
logger,
|
|
210
|
+
approvals,
|
|
211
|
+
sessionStore,
|
|
212
|
+
undo,
|
|
213
|
+
processes,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
async function getProjectSummary({ force = false } = {}) {
|
|
217
|
+
if (projectSummary && !force) return projectSummary;
|
|
218
|
+
const cached = force ? null : projectCache.get(workspace.root);
|
|
219
|
+
if (cached?.summary) {
|
|
220
|
+
projectSummary = cached.summary;
|
|
221
|
+
return projectSummary;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
projectSummary = await buildProjectSummary({ workspace });
|
|
225
|
+
projectCache.set(workspace.root, projectSummary);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
logger.warn('project summary unavailable', { error: error.message });
|
|
228
|
+
projectSummary = null;
|
|
229
|
+
}
|
|
230
|
+
return projectSummary;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function runTask(task) {
|
|
234
|
+
if (activeTask) throw new RyuuError('A task is already running. Use /cancel before starting another.', { code: 'TASK_BUSY' });
|
|
235
|
+
const controller = new AbortController();
|
|
236
|
+
activeTask = { controller, task };
|
|
237
|
+
try {
|
|
238
|
+
agent.projectSummary = await getProjectSummary();
|
|
239
|
+
return await agent.run(task, { signal: controller.signal });
|
|
240
|
+
} finally {
|
|
241
|
+
activeTask = null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function confirmLocal(question) {
|
|
246
|
+
if (!prompts) return false;
|
|
247
|
+
const answer = await prompts.confirm(question);
|
|
248
|
+
return answer.value;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Limitations advertised by the active provider (never assumed present). */
|
|
252
|
+
function providerLimitations() {
|
|
253
|
+
const capabilities = provider.capabilities || config.capabilities || {};
|
|
254
|
+
const notes = [];
|
|
255
|
+
if (capabilities.tools === false) {
|
|
256
|
+
notes.push(
|
|
257
|
+
`${config.providerName} does not advertise tool calling: the agent can answer, but cannot read or change files through tools.`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
if (capabilities.streaming === false) {
|
|
261
|
+
notes.push(`${config.providerName} does not advertise streaming: responses arrive as one block.`);
|
|
262
|
+
}
|
|
263
|
+
return notes;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Re-read config.json and rebuild the provider descriptors.
|
|
268
|
+
*
|
|
269
|
+
* `/provider add|del` and `/addmodel|/delmodel` edit the file through
|
|
270
|
+
* updateSettings; this makes the change visible to the running session
|
|
271
|
+
* immediately, without restarting the CLI or dropping conversation memory.
|
|
272
|
+
*/
|
|
273
|
+
async function reloadProviders() {
|
|
274
|
+
const { data } = await readSettings();
|
|
275
|
+
const raw = buildRawConfig(data, { cwd: config.workspace });
|
|
276
|
+
let extras = [];
|
|
277
|
+
try {
|
|
278
|
+
extras = parseExtraProviders(raw.extraProviders, []);
|
|
279
|
+
} catch {
|
|
280
|
+
extras = [];
|
|
281
|
+
}
|
|
282
|
+
config.providers = buildProviders({ credentials: raw.providerCredentials, extras });
|
|
283
|
+
return config.providers;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Switch the active provider (used by /provider). The provider object is
|
|
288
|
+
* updated in place, so the agent keeps its conversation memory and worklog.
|
|
289
|
+
*/
|
|
290
|
+
async function switchProvider(providerId, { model = null } = {}) {
|
|
291
|
+
const stateDir = stateDirFor(config.workspace);
|
|
292
|
+
const remembered = model ? null : await readProviderModel(stateDir, providerId);
|
|
293
|
+
const selection = selectionFor(config, providerId, { model: model || remembered || null });
|
|
294
|
+
if (typeof provider.updateSelection === 'function') {
|
|
295
|
+
provider.updateSelection(selection);
|
|
296
|
+
} else if (provider.config) {
|
|
297
|
+
// Test doubles: keep their config view in sync with the effective one.
|
|
298
|
+
Object.assign(provider.config, selection);
|
|
299
|
+
}
|
|
300
|
+
Object.assign(config, {
|
|
301
|
+
providerId: selection.providerId,
|
|
302
|
+
provider: selection.providerId,
|
|
303
|
+
providerName: selection.providerName,
|
|
304
|
+
apiKeyPath: selection.apiKeyPath,
|
|
305
|
+
apiKey: selection.apiKey,
|
|
306
|
+
baseURL: selection.baseURL,
|
|
307
|
+
model: selection.model,
|
|
308
|
+
capabilities: selection.capabilities,
|
|
309
|
+
});
|
|
310
|
+
const persisted = await writeProviderPreference(stateDir, selection.providerId);
|
|
311
|
+
return { ...selection, persisted };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
state = {
|
|
315
|
+
config,
|
|
316
|
+
workspace,
|
|
317
|
+
registry,
|
|
318
|
+
processes,
|
|
319
|
+
sessionStore,
|
|
320
|
+
undo,
|
|
321
|
+
renderer,
|
|
322
|
+
provider,
|
|
323
|
+
approvals,
|
|
324
|
+
webSearch: registry.webSearchManager || injectedWebManager || null,
|
|
325
|
+
interactive,
|
|
326
|
+
getAgent: () => agent,
|
|
327
|
+
getLastSummary: () => lastSummary,
|
|
328
|
+
getActiveTask: () => activeTask,
|
|
329
|
+
switchProvider,
|
|
330
|
+
reloadProviders,
|
|
331
|
+
providerLimitations,
|
|
332
|
+
cancelActiveTask: () => {
|
|
333
|
+
if (!activeTask) return false;
|
|
334
|
+
activeTask.controller.abort();
|
|
335
|
+
return true;
|
|
336
|
+
},
|
|
337
|
+
confirmLocal,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const commands = createCommands(state);
|
|
341
|
+
|
|
342
|
+
// Keep the last summary available to /status.
|
|
343
|
+
const originalFinal = renderer.final?.bind(renderer);
|
|
344
|
+
if (typeof originalFinal === 'function') {
|
|
345
|
+
renderer.final = (summary) => {
|
|
346
|
+
lastSummary = summary;
|
|
347
|
+
originalFinal(summary);
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function banner() {
|
|
352
|
+
renderer.banner({
|
|
353
|
+
version,
|
|
354
|
+
model: config.model,
|
|
355
|
+
provider: config.providerName,
|
|
356
|
+
workspace: workspace.root,
|
|
357
|
+
node: process.version,
|
|
358
|
+
approvalPolicy: cliOptions.autoApprove
|
|
359
|
+
? `${config.approvalPolicy} + --auto-approve (all actions)`
|
|
360
|
+
: config.approvalPolicy,
|
|
361
|
+
logFile: logger.logFile,
|
|
362
|
+
});
|
|
363
|
+
for (const note of providerLimitations()) renderer.warn(note);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function readTask(firstLine) {
|
|
367
|
+
return collectTask({
|
|
368
|
+
firstLine,
|
|
369
|
+
ask: (question) => prompts.ask(question),
|
|
370
|
+
onContinue: () =>
|
|
371
|
+
renderer.muted(' … continuation line · finish the task on a line without \\ to send'),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function runInteractive() {
|
|
376
|
+
banner();
|
|
377
|
+
// The running task is not awaited here: the composer stays usable while the
|
|
378
|
+
// agent works, so slash commands can be sent at any time. A second prompt
|
|
379
|
+
// is refused instead of being queued behind the running task.
|
|
380
|
+
let taskPromise = null;
|
|
381
|
+
while (!stopping) {
|
|
382
|
+
const prefix = renderer.prompt();
|
|
383
|
+
const line = await prompts.ask(prefix);
|
|
384
|
+
if (line === null) break;
|
|
385
|
+
const trimmed = line.trim();
|
|
386
|
+
if (!trimmed) continue;
|
|
387
|
+
const parsed = parseCommandLine(trimmed) || shellCommandFromLine(trimmed);
|
|
388
|
+
try {
|
|
389
|
+
if (parsed) {
|
|
390
|
+
if (parsed.name === 'exit') break;
|
|
391
|
+
await commands.execute(parsed);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
if (activeTask) {
|
|
395
|
+
renderer.warn(
|
|
396
|
+
'A task is already running. Slash commands still work (e.g. /status, /model, /expand, /cancel); ' +
|
|
397
|
+
'send a new prompt once it finishes or after /cancel.',
|
|
398
|
+
);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const task = await readTask(trimmed);
|
|
402
|
+
if (task === null) break;
|
|
403
|
+
taskPromise = runTask(task)
|
|
404
|
+
.catch((error) => {
|
|
405
|
+
if (error?.code === 'TASK_BUSY') renderer.warn(error.message);
|
|
406
|
+
else renderer.error({ error });
|
|
407
|
+
})
|
|
408
|
+
.finally(() => {
|
|
409
|
+
taskPromise = null;
|
|
410
|
+
});
|
|
411
|
+
} catch (error) {
|
|
412
|
+
if (error.code === 'TASK_BUSY') renderer.warn(error.message);
|
|
413
|
+
else renderer.error({ error });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
// close() aborts anything still running, so wait for it only afterwards.
|
|
417
|
+
await close();
|
|
418
|
+
if (taskPromise) await taskPromise.catch(() => {});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function close() {
|
|
422
|
+
if (closed) return;
|
|
423
|
+
closed = true;
|
|
424
|
+
stopping = true;
|
|
425
|
+
renderer.closeBlockView?.();
|
|
426
|
+
if (state?.activeShell) state.activeShell.controller.abort();
|
|
427
|
+
if (activeTask) activeTask.controller.abort();
|
|
428
|
+
renderer.stopActivity();
|
|
429
|
+
prompts?.close();
|
|
430
|
+
const stopped = await processes.stopAll({ graceMs: 1500 }).catch(() => 0);
|
|
431
|
+
if (stopped > 0) logger.info('stopped processes on exit', { count: stopped });
|
|
432
|
+
await registry.webSearchManager?.close?.().catch(() => {});
|
|
433
|
+
logger.info('session closed');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
config,
|
|
438
|
+
workspace,
|
|
439
|
+
processes,
|
|
440
|
+
registry,
|
|
441
|
+
provider,
|
|
442
|
+
sessionStore,
|
|
443
|
+
undo,
|
|
444
|
+
approvals,
|
|
445
|
+
webSearch: registry.webSearchManager || injectedWebManager || null,
|
|
446
|
+
renderer,
|
|
447
|
+
logger,
|
|
448
|
+
commands,
|
|
449
|
+
agent,
|
|
450
|
+
interactive,
|
|
451
|
+
version,
|
|
452
|
+
banner,
|
|
453
|
+
runTask,
|
|
454
|
+
runInteractive,
|
|
455
|
+
runDoctor: (live = false) => runDoctor({ config, workspace, provider, live }),
|
|
456
|
+
getProjectSummary,
|
|
457
|
+
getLastSummary: () => lastSummary,
|
|
458
|
+
cancelActiveTask: state.cancelActiveTask,
|
|
459
|
+
setProvider: switchProvider,
|
|
460
|
+
providerLimitations,
|
|
461
|
+
close,
|
|
462
|
+
};
|
|
463
|
+
}
|