atom-lite 0.4.1 → 0.5.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 +11 -0
- package/README.md +1 -1
- package/dist/agent/loop.d.ts +9 -0
- package/dist/agent/loop.js +233 -47
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/model-turn.d.ts +37 -0
- package/dist/agent/model-turn.js +53 -0
- package/dist/agent/model-turn.js.map +1 -0
- package/dist/agent/turn-recorder.d.ts +33 -0
- package/dist/agent/turn-recorder.js +74 -0
- package/dist/agent/turn-recorder.js.map +1 -0
- package/dist/agent/types.d.ts +4 -0
- package/dist/cli/app.js +373 -33
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/commands.d.ts +3 -0
- package/dist/cli/commands.js +20 -5
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/main.js +2 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/mouse.js +4 -1
- package/dist/cli/mouse.js.map +1 -1
- package/dist/cli/stream-text.js +1 -0
- package/dist/cli/stream-text.js.map +1 -1
- package/dist/cli/theme.d.ts +1 -1
- package/dist/cli/theme.js +2 -0
- package/dist/cli/theme.js.map +1 -1
- package/dist/cli/transcript.d.ts +15 -1
- package/dist/cli/transcript.js +40 -1
- package/dist/cli/transcript.js.map +1 -1
- package/dist/cli/trust-prompt.d.ts +24 -0
- package/dist/cli/trust-prompt.js +89 -0
- package/dist/cli/trust-prompt.js.map +1 -0
- package/dist/config/defaults.d.ts +2 -0
- package/dist/config/defaults.js +2 -0
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/load.js +4 -0
- package/dist/config/load.js.map +1 -1
- package/dist/config/paths.d.ts +2 -0
- package/dist/config/paths.js +12 -1
- package/dist/config/paths.js.map +1 -1
- package/dist/config/schema.d.ts +4 -0
- package/dist/config/schema.js +2 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/context/manager.d.ts +17 -3
- package/dist/context/manager.js +129 -41
- package/dist/context/manager.js.map +1 -1
- package/dist/extensions/api.d.ts +51 -1
- package/dist/extensions/api.js +124 -6
- package/dist/extensions/api.js.map +1 -1
- package/dist/extensions/discovery.d.ts +16 -0
- package/dist/extensions/discovery.js +191 -108
- package/dist/extensions/discovery.js.map +1 -1
- package/dist/extensions/loader.d.ts +42 -1
- package/dist/extensions/loader.js +272 -32
- package/dist/extensions/loader.js.map +1 -1
- package/dist/extensions/session-manager.d.ts +44 -0
- package/dist/extensions/session-manager.js +137 -0
- package/dist/extensions/session-manager.js.map +1 -0
- package/dist/extensions/tool-adapter.d.ts +12 -0
- package/dist/extensions/tool-adapter.js +157 -0
- package/dist/extensions/tool-adapter.js.map +1 -0
- package/dist/extensions/trust.d.ts +2 -0
- package/dist/extensions/trust.js +32 -17
- package/dist/extensions/trust.js.map +1 -1
- package/dist/extensions/types.d.ts +45 -3
- package/dist/extensions/ui.d.ts +21 -0
- package/dist/extensions/ui.js +75 -0
- package/dist/extensions/ui.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/stdio-transport.js +3 -20
- package/dist/mcp/stdio-transport.js.map +1 -1
- package/dist/runtime/scheduler.d.ts +4 -3
- package/dist/runtime/scheduler.js +7 -105
- package/dist/runtime/scheduler.js.map +1 -1
- package/dist/runtime/tool-executor.d.ts +18 -0
- package/dist/runtime/tool-executor.js +126 -0
- package/dist/runtime/tool-executor.js.map +1 -0
- package/dist/security/process.d.ts +10 -0
- package/dist/security/process.js +36 -0
- package/dist/security/process.js.map +1 -1
- package/dist/sessions/store.js +2 -3
- package/dist/sessions/store.js.map +1 -1
- package/dist/tools/bash.js +6 -3
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/types.d.ts +6 -0
- package/package.json +3 -1
package/dist/cli/app.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadConfig } from "../config/load.js";
|
|
2
|
+
import { defaultAgentDir, userConfigDir } from "../config/paths.js";
|
|
2
3
|
import { saveProviderSelection, saveTheme, } from "../config/write.js";
|
|
3
4
|
import { resolveApiKey, saveApiKey } from "../config/credentials.js";
|
|
4
5
|
import { join } from "node:path";
|
|
@@ -20,6 +21,9 @@ import { Transcript } from "./transcript.js";
|
|
|
20
21
|
import { AgentRuntime } from "../agent/loop.js";
|
|
21
22
|
import { JsonlSessionStore } from "../sessions/store.js";
|
|
22
23
|
import { ExtensionLoader } from "../extensions/loader.js";
|
|
24
|
+
import { collectExtensionTools } from "../extensions/tool-adapter.js";
|
|
25
|
+
import { denyProject, trustProject } from "../extensions/trust.js";
|
|
26
|
+
import { ProjectTrustPrompt } from "./trust-prompt.js";
|
|
23
27
|
import { formatSessionList, formatSessionSearch, searchSessions, sessionTitleFromEvents, } from "../sessions/search.js";
|
|
24
28
|
import { providerSwitchNotice, sessionHistoryLines, sessionSelectionFromEvents, } from "../sessions/history.js";
|
|
25
29
|
import { blockAtLine, createMouseCapture, handleMouseEvent, parseSgrMouse, shouldEnableMouse, } from "./mouse.js";
|
|
@@ -28,7 +32,7 @@ import { McpManager, formatMcpStatus, formatMcpDoctor, createMcpTools } from "..
|
|
|
28
32
|
import { createProvider } from "../providers/factory.js";
|
|
29
33
|
import { availableProviderNames, getPreset, listModelsForProvider, materializeProviderConfig, probeApiKey, providerLabel, PROVIDER_PRESETS, } from "../providers/catalog.js";
|
|
30
34
|
import { ContextManager } from "../context/manager.js";
|
|
31
|
-
import { ProcessTerminal, isKeyRelease, isKeyRepeat, Key, matchesKey, } from "@earendil-works/pi-tui";
|
|
35
|
+
import { ProcessTerminal, isKeyRelease, isKeyRepeat, Key, matchesKey, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
|
|
32
36
|
export async function run(args) {
|
|
33
37
|
let providerOverride;
|
|
34
38
|
let modelOverride;
|
|
@@ -41,9 +45,15 @@ export async function run(args) {
|
|
|
41
45
|
let listSessions = false;
|
|
42
46
|
let searchSessionsQuery;
|
|
43
47
|
const positional = [];
|
|
48
|
+
const extensionPaths = [];
|
|
44
49
|
for (let i = 0; i < args.length; i++) {
|
|
45
50
|
const a = args[i];
|
|
46
|
-
if (a === "--
|
|
51
|
+
if (a === "-e" || a === "--extension") {
|
|
52
|
+
const path = args[++i];
|
|
53
|
+
if (path)
|
|
54
|
+
extensionPaths.push(path);
|
|
55
|
+
}
|
|
56
|
+
else if (a === "--provider")
|
|
47
57
|
providerOverride = args[++i];
|
|
48
58
|
else if (a === "--model")
|
|
49
59
|
modelOverride = args[++i];
|
|
@@ -68,7 +78,7 @@ export async function run(args) {
|
|
|
68
78
|
else if (a === "--no-tools")
|
|
69
79
|
positional.push("__notools__");
|
|
70
80
|
else if (a === "--help") {
|
|
71
|
-
process.stdout.write("atom-lite [task] [--provider NAME] [--model MODEL] [--continue] [--session ID] [--list-sessions] [--search-sessions QUERY] [--no-tools] [--json] [--debug] [--theme NAME] [--no-color] [--version] [--help]\n");
|
|
81
|
+
process.stdout.write("atom-lite [task] [--provider NAME] [--model MODEL] [--continue] [--session ID] [--list-sessions] [--search-sessions QUERY] [--no-tools] [--json] [--debug] [--theme NAME] [--no-color] [-e PATH] [--version] [--help]\n");
|
|
72
82
|
return;
|
|
73
83
|
}
|
|
74
84
|
else if (a.startsWith("--")) {
|
|
@@ -145,20 +155,101 @@ export async function run(args) {
|
|
|
145
155
|
}
|
|
146
156
|
},
|
|
147
157
|
});
|
|
158
|
+
const extensionUICallbacks = {};
|
|
159
|
+
const extensionProviderNames = new Set();
|
|
160
|
+
function resolveExtensionApiKey(value) {
|
|
161
|
+
if (typeof value !== "string" || !value)
|
|
162
|
+
return undefined;
|
|
163
|
+
if (value.startsWith("!")) {
|
|
164
|
+
try {
|
|
165
|
+
const { execFileSync } = require("node:child_process");
|
|
166
|
+
const parts = value.slice(1).trim().split(/\s+/);
|
|
167
|
+
return execFileSync(parts[0], parts.slice(1), { encoding: "utf8" }).trim();
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (value.startsWith("$"))
|
|
174
|
+
return process.env[value.slice(1)] ?? value;
|
|
175
|
+
if (value.startsWith("${") && value.endsWith("}"))
|
|
176
|
+
return process.env[value.slice(2, -1)] ?? value;
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
148
179
|
const extensionLoader = new ExtensionLoader({
|
|
149
|
-
|
|
180
|
+
cliPaths: extensionPaths,
|
|
181
|
+
cwd: process.cwd(),
|
|
182
|
+
agentDir: defaultAgentDir(),
|
|
183
|
+
userDir: join(userConfigDir(), "extensions"),
|
|
150
184
|
projectDir: process.cwd(),
|
|
151
|
-
trustedProjects: config.extensions?.trustedProjects,
|
|
152
185
|
});
|
|
153
|
-
|
|
186
|
+
const pendingTrust = extensionLoader
|
|
187
|
+
.discover()
|
|
188
|
+
.filter((entry) => entry.source === "project" &&
|
|
189
|
+
!entry.trusted &&
|
|
190
|
+
entry.skipReason === "project is not trusted");
|
|
191
|
+
if (!interactive && pendingTrust.length > 0) {
|
|
192
|
+
process.stderr.write(`atom-lite: skipping ${pendingTrust.length} untrusted project extension(s); run interactively to review\n`);
|
|
193
|
+
}
|
|
194
|
+
await extensionLoader.loadAll().catch(() => {
|
|
154
195
|
// Extension failures are reported per extension.
|
|
155
196
|
});
|
|
197
|
+
// Remove any previously bridged extension providers before re-bridging.
|
|
198
|
+
for (const name of extensionProviderNames) {
|
|
199
|
+
delete config.providers[name];
|
|
200
|
+
}
|
|
201
|
+
extensionProviderNames.clear();
|
|
202
|
+
// Bridge Pi-style extension providers into Atom-lite's config so they
|
|
203
|
+
// appear in /provider, /model, and the command palette.
|
|
204
|
+
for (const loaded of extensionLoader.list()) {
|
|
205
|
+
for (const [name, provider] of (loaded.registries?.providers ?? new Map())) {
|
|
206
|
+
const cfg = provider?.config ?? {};
|
|
207
|
+
const firstModel = Array.isArray(cfg.models) && cfg.models.length > 0
|
|
208
|
+
? (cfg.models[0].id ?? cfg.models[0].name ?? "default")
|
|
209
|
+
: "default";
|
|
210
|
+
const apiKey = resolveExtensionApiKey(cfg.apiKey);
|
|
211
|
+
const headers = { ...(cfg.headers ?? {}) };
|
|
212
|
+
if (cfg.authHeader && apiKey)
|
|
213
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
214
|
+
config.providers[name] = {
|
|
215
|
+
type: "openai-compatible",
|
|
216
|
+
baseUrl: cfg.baseUrl,
|
|
217
|
+
model: firstModel,
|
|
218
|
+
apiKey: apiKey ?? null,
|
|
219
|
+
apiKeyEnv: null,
|
|
220
|
+
headers,
|
|
221
|
+
contextWindow: cfg.models?.[0]?.contextWindow,
|
|
222
|
+
};
|
|
223
|
+
extensionProviderNames.add(name);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const extensionTools = collectExtensionTools(extensionLoader);
|
|
227
|
+
const allTools = [...builtInTools, ...extensionTools];
|
|
228
|
+
const extensionDispatch = async (event, payload) => {
|
|
229
|
+
const results = [];
|
|
230
|
+
for (const loaded of extensionLoader.list()) {
|
|
231
|
+
const handlers = loaded.registries.handlers.get(event);
|
|
232
|
+
if (!handlers)
|
|
233
|
+
continue;
|
|
234
|
+
for (const handler of handlers) {
|
|
235
|
+
try {
|
|
236
|
+
results.push(await handler(payload, loaded.context ?? {}));
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
// eslint-disable-next-line no-console
|
|
240
|
+
console.error(`[extension:${loaded.id}] handler error: ${error instanceof Error ? error.message : String(error)}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return results;
|
|
245
|
+
};
|
|
156
246
|
const contextManager = new ContextManager({
|
|
157
247
|
systemPrompt: "You are a coding assistant.",
|
|
158
248
|
maxInputTokens: config.context.maxInputTokens ?? null,
|
|
159
249
|
reserveOutputTokens: config.context.reserveOutputTokens,
|
|
160
250
|
compactAtPercent: config.context.compactAtPercent,
|
|
161
251
|
maxProjectInstructionBytes: config.context.maxProjectInstructionBytes,
|
|
252
|
+
compactEveryNTurns: config.context.compactEveryNTurns ?? 0,
|
|
162
253
|
provider,
|
|
163
254
|
model: modelOverride ?? "default",
|
|
164
255
|
userInstructionsPath: undefined,
|
|
@@ -178,13 +269,14 @@ export async function run(args) {
|
|
|
178
269
|
provider,
|
|
179
270
|
model: modelOverride,
|
|
180
271
|
workspace: process.cwd(),
|
|
181
|
-
tools:
|
|
272
|
+
tools: allTools,
|
|
182
273
|
approvals,
|
|
183
274
|
contextManager,
|
|
275
|
+
extensionDispatch,
|
|
184
276
|
bashConcurrency: config.runtime.bashConcurrency,
|
|
185
277
|
});
|
|
186
278
|
mcpManager.onToolsChanged = () => {
|
|
187
|
-
runtime.setTools([...builtInTools, ...createMcpTools(mcpManager)]);
|
|
279
|
+
runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
|
|
188
280
|
};
|
|
189
281
|
if (oneShot || !interactive) {
|
|
190
282
|
// non-interactive / one-shot path
|
|
@@ -194,7 +286,7 @@ export async function run(args) {
|
|
|
194
286
|
try {
|
|
195
287
|
if (useTools) {
|
|
196
288
|
await mcpManager.syncTools();
|
|
197
|
-
runtime.setTools([...builtInTools, ...createMcpTools(mcpManager)]);
|
|
289
|
+
runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
|
|
198
290
|
}
|
|
199
291
|
const result = await runtime.runAgentTurn(oneShot || " ", session, controller.signal);
|
|
200
292
|
if (jsonMode) {
|
|
@@ -218,14 +310,14 @@ export async function run(args) {
|
|
|
218
310
|
return;
|
|
219
311
|
}
|
|
220
312
|
// ---- Interactive TUI (first-class) ----
|
|
221
|
-
const {
|
|
222
|
-
const terminal = new ProcessTerminal();
|
|
223
|
-
const tui = new TuiMainScreen(terminal);
|
|
313
|
+
const { TuiAltScreen, Text, Input, Editor, Container, Spacer, ScrollView, VStack, Loader, Markdown, Box } = await import("@earendil-works/pi-tui");
|
|
224
314
|
const mouseEnabled = shouldEnableMouse({
|
|
225
315
|
interactive,
|
|
226
316
|
configured: config.ui.mouse,
|
|
227
317
|
stdoutIsTTY: process.stdout.isTTY === true,
|
|
228
318
|
});
|
|
319
|
+
const terminal = new ProcessTerminal();
|
|
320
|
+
const tui = new TuiAltScreen(terminal, false, undefined, { mouse: mouseEnabled });
|
|
229
321
|
const mouseCapture = createMouseCapture((data) => terminal.write(data));
|
|
230
322
|
let effortLevel = "medium";
|
|
231
323
|
runtime.setEffort(effortLevel);
|
|
@@ -308,14 +400,16 @@ export async function run(args) {
|
|
|
308
400
|
input.setAutocompleteProvider(new CombinedAutocompleteProvider(commandRegistry.autocomplete(), process.cwd()));
|
|
309
401
|
}
|
|
310
402
|
input.setAutocompleteProvider(new CombinedAutocompleteProvider(commandRegistry.autocomplete(), process.cwd()));
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
403
|
+
const root = new VStack([]);
|
|
404
|
+
root.addChild(output, { grow: 1 });
|
|
405
|
+
root.addChild(new Spacer(1));
|
|
406
|
+
root.addChild(input);
|
|
407
|
+
root.addChild(statusBar);
|
|
315
408
|
// folder:branch info below the status line, dimmed
|
|
316
|
-
|
|
409
|
+
root.addChild(header);
|
|
317
410
|
const dropdownContainer = new Container();
|
|
318
|
-
|
|
411
|
+
root.addChild(dropdownContainer);
|
|
412
|
+
tui.setLayoutRoot(root);
|
|
319
413
|
tui.setFocus(input);
|
|
320
414
|
if (!noColor) {
|
|
321
415
|
try {
|
|
@@ -331,6 +425,27 @@ export async function run(args) {
|
|
|
331
425
|
// COLORFGBG/dark fallback.
|
|
332
426
|
}
|
|
333
427
|
}
|
|
428
|
+
class ThinkingBlock {
|
|
429
|
+
buffer = "";
|
|
430
|
+
render(width) {
|
|
431
|
+
const lines = [theme.tokens.dim("💭 thinking")];
|
|
432
|
+
const sep = theme.tokens.dim("│ ");
|
|
433
|
+
const contentWidth = Math.max(1, width - 2);
|
|
434
|
+
const logicalLines = this.buffer.length > 0 ? this.buffer.split("\\n") : [""];
|
|
435
|
+
for (const logical of logicalLines) {
|
|
436
|
+
const wrapped = wrapTextWithAnsi(logical, contentWidth);
|
|
437
|
+
if (wrapped.length === 0) {
|
|
438
|
+
lines.push(sep);
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
for (const part of wrapped)
|
|
442
|
+
lines.push(sep + theme.tokens.dim(part));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return lines;
|
|
446
|
+
}
|
|
447
|
+
invalidate() { }
|
|
448
|
+
}
|
|
334
449
|
let thinkingText = null;
|
|
335
450
|
let thinkingBuffer = "";
|
|
336
451
|
let showThinking = config.ui.showThinking;
|
|
@@ -348,29 +463,82 @@ export async function run(args) {
|
|
|
348
463
|
closeActiveOverlay = undefined;
|
|
349
464
|
close?.();
|
|
350
465
|
}
|
|
351
|
-
function appendLine(text) {
|
|
466
|
+
function appendLine(text, style = theme.tokens.text) {
|
|
352
467
|
if (thinkingText) {
|
|
353
468
|
thinkingText = null;
|
|
354
469
|
thinkingBuffer = "";
|
|
355
470
|
}
|
|
356
471
|
streamText.finalize();
|
|
357
|
-
transcript.
|
|
472
|
+
transcript.add(new Text(style(text)));
|
|
358
473
|
tui.requestRender();
|
|
359
474
|
}
|
|
360
475
|
function appendDelta(delta) {
|
|
361
476
|
streamText.append(delta);
|
|
362
477
|
tui.requestRender();
|
|
363
478
|
}
|
|
479
|
+
/** Review project-local extensions once per project decision. */
|
|
480
|
+
function maybeReviewProjectTrust() {
|
|
481
|
+
if (uiModalOpen)
|
|
482
|
+
return;
|
|
483
|
+
const pending = extensionLoader
|
|
484
|
+
.discover()
|
|
485
|
+
.filter((entry) => entry.source === "project" &&
|
|
486
|
+
!entry.trusted &&
|
|
487
|
+
entry.skipReason === "project is not trusted");
|
|
488
|
+
if (pending.length === 0)
|
|
489
|
+
return;
|
|
490
|
+
uiModalOpen = true;
|
|
491
|
+
closeOverlay();
|
|
492
|
+
dropdownContainer.clear();
|
|
493
|
+
const prompt = new ProjectTrustPrompt(pending.map((entry) => ({
|
|
494
|
+
id: entry.id,
|
|
495
|
+
entryPath: entry.entryPath,
|
|
496
|
+
version: entry.manifest?.version,
|
|
497
|
+
capabilities: entry.manifest?.capabilities ?? [],
|
|
498
|
+
})), process.cwd(), (decision) => {
|
|
499
|
+
uiModalOpen = false;
|
|
500
|
+
dropdownContainer.clear();
|
|
501
|
+
tui.setFocus(input);
|
|
502
|
+
if (decision === "approve") {
|
|
503
|
+
trustProject(process.cwd());
|
|
504
|
+
appendLine("approved project extensions");
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
denyProject(process.cwd());
|
|
508
|
+
appendLine("denied project extensions");
|
|
509
|
+
}
|
|
510
|
+
void extensionLoader
|
|
511
|
+
.loadAll()
|
|
512
|
+
.then(() => {
|
|
513
|
+
runtime.setTools([
|
|
514
|
+
...builtInTools,
|
|
515
|
+
...collectExtensionTools(extensionLoader),
|
|
516
|
+
...createMcpTools(mcpManager),
|
|
517
|
+
]);
|
|
518
|
+
appendLine(`extensions: ${extensionLoader.list().length} processed (see /extensions)`);
|
|
519
|
+
tui.requestRender();
|
|
520
|
+
})
|
|
521
|
+
.catch((error) => {
|
|
522
|
+
appendLine(`extensions: reload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
dropdownContainer.addChild(prompt);
|
|
526
|
+
tui.setFocus(prompt);
|
|
527
|
+
tui.requestRender();
|
|
528
|
+
}
|
|
364
529
|
function thinkingBody() {
|
|
365
530
|
return thinkingBuffer
|
|
366
531
|
.split("\n")
|
|
367
|
-
.map((line) => theme.tokens.dim(
|
|
532
|
+
.map((line) => theme.tokens.dim(`│ ${line}`))
|
|
368
533
|
.join("\n");
|
|
369
534
|
}
|
|
370
535
|
function finalizeText() {
|
|
371
536
|
streamText.finalize();
|
|
372
537
|
}
|
|
373
538
|
let activeTurnController = null;
|
|
539
|
+
const messageQueue = [];
|
|
540
|
+
const steeringQueue = [];
|
|
541
|
+
runtime.setSteeringQueue(steeringQueue);
|
|
374
542
|
/** Run one interactive turn while keeping the status bar's activity fresh. */
|
|
375
543
|
async function runTurn(prompt) {
|
|
376
544
|
activity.start("formulating");
|
|
@@ -379,7 +547,7 @@ export async function run(args) {
|
|
|
379
547
|
try {
|
|
380
548
|
if (useTools) {
|
|
381
549
|
await mcpManager.syncTools();
|
|
382
|
-
runtime.setTools([...builtInTools, ...createMcpTools(mcpManager)]);
|
|
550
|
+
runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
|
|
383
551
|
}
|
|
384
552
|
await runtime.runAgentTurn(prompt, session, controller.signal);
|
|
385
553
|
}
|
|
@@ -394,6 +562,11 @@ export async function run(args) {
|
|
|
394
562
|
finally {
|
|
395
563
|
activeTurnController = null;
|
|
396
564
|
activity.stop();
|
|
565
|
+
if (messageQueue.length > 0) {
|
|
566
|
+
const next = messageQueue.shift();
|
|
567
|
+
appendLine(`> ${next}`, theme.tokens.user);
|
|
568
|
+
void runTurn(next);
|
|
569
|
+
}
|
|
397
570
|
}
|
|
398
571
|
}
|
|
399
572
|
function updateStatus() {
|
|
@@ -467,6 +640,17 @@ export async function run(args) {
|
|
|
467
640
|
refreshContextWindow();
|
|
468
641
|
const MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
469
642
|
const modelCache = new Map();
|
|
643
|
+
for (const loaded of extensionLoader.list()) {
|
|
644
|
+
for (const [name, provider] of (loaded.registries?.providers ?? new Map())) {
|
|
645
|
+
const cfg = provider?.config ?? {};
|
|
646
|
+
const models = Array.isArray(cfg.models)
|
|
647
|
+
? cfg.models.map((m) => m.id ?? m.name).filter(Boolean)
|
|
648
|
+
: [];
|
|
649
|
+
if (models.length > 0) {
|
|
650
|
+
modelCache.set(name, { models, at: Date.now() });
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
470
654
|
function profileConfig(profile) {
|
|
471
655
|
return config.providers[profile];
|
|
472
656
|
}
|
|
@@ -927,6 +1111,7 @@ export async function run(args) {
|
|
|
927
1111
|
updateStatus,
|
|
928
1112
|
runDoctor,
|
|
929
1113
|
runMcp,
|
|
1114
|
+
runCompact,
|
|
930
1115
|
runExport,
|
|
931
1116
|
openModelPicker,
|
|
932
1117
|
openProviderPicker,
|
|
@@ -935,10 +1120,22 @@ export async function run(args) {
|
|
|
935
1120
|
openSessionsPicker,
|
|
936
1121
|
searchSessions: searchSessionsInStore,
|
|
937
1122
|
renameSession,
|
|
938
|
-
listExtensions: () =>
|
|
939
|
-
.
|
|
940
|
-
|
|
941
|
-
|
|
1123
|
+
listExtensions: () => {
|
|
1124
|
+
const report = extensionLoader.report();
|
|
1125
|
+
if (report.length === 0)
|
|
1126
|
+
return "No extensions discovered.";
|
|
1127
|
+
return report
|
|
1128
|
+
.map((ext) => {
|
|
1129
|
+
const origin = ext.origin ? `${ext.source}:${ext.origin}` : ext.source;
|
|
1130
|
+
const version = ext.version ? `v${ext.version}` : "-";
|
|
1131
|
+
const counts = `${ext.toolCount} tools / ${ext.commandCount} commands`;
|
|
1132
|
+
const error = ext.error ? `\t${ext.error}` : "";
|
|
1133
|
+
return `${ext.id}\t${origin}\t${version}\t${ext.status}\t${counts}\t${ext.entryPath}${error}`;
|
|
1134
|
+
})
|
|
1135
|
+
.join("\n");
|
|
1136
|
+
},
|
|
1137
|
+
extensionDoctor: () => extensionLoader.doctor(),
|
|
1138
|
+
reloadExtensions,
|
|
942
1139
|
switchModel: switchModelTarget,
|
|
943
1140
|
startProviderSetup,
|
|
944
1141
|
setEffort: setEffortTarget,
|
|
@@ -1164,6 +1361,108 @@ export async function run(args) {
|
|
|
1164
1361
|
}
|
|
1165
1362
|
tui.requestRender();
|
|
1166
1363
|
}
|
|
1364
|
+
async function reloadExtensions() {
|
|
1365
|
+
const previous = extensionLoader.list();
|
|
1366
|
+
// Emit session_shutdown and extension_unload before unloading.
|
|
1367
|
+
for (const loaded of previous) {
|
|
1368
|
+
const handlers = loaded.registries?.handlers?.get?.("session_shutdown");
|
|
1369
|
+
if (handlers) {
|
|
1370
|
+
for (const handler of handlers) {
|
|
1371
|
+
try {
|
|
1372
|
+
await handler({ type: "session_shutdown", reason: "reload" }, (loaded.context ?? {}));
|
|
1373
|
+
}
|
|
1374
|
+
catch (error) {
|
|
1375
|
+
appendLine(`extension shutdown error: ${error instanceof Error ? error.message : String(error)}`);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
await extensionLoader.unloadAll();
|
|
1381
|
+
try {
|
|
1382
|
+
await extensionLoader.loadAll();
|
|
1383
|
+
}
|
|
1384
|
+
catch (error) {
|
|
1385
|
+
appendLine(`extensions: reload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1386
|
+
tui.requestRender();
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
// Remove previously bridged providers, then re-bridge.
|
|
1390
|
+
for (const name of extensionProviderNames) {
|
|
1391
|
+
delete config.providers[name];
|
|
1392
|
+
modelCache.delete(name);
|
|
1393
|
+
}
|
|
1394
|
+
extensionProviderNames.clear();
|
|
1395
|
+
for (const loaded of extensionLoader.list()) {
|
|
1396
|
+
for (const [name, provider] of (loaded.registries?.providers ?? new Map())) {
|
|
1397
|
+
const cfg = provider?.config ?? {};
|
|
1398
|
+
const firstModel = Array.isArray(cfg.models) && cfg.models.length > 0
|
|
1399
|
+
? (cfg.models[0].id ?? cfg.models[0].name ?? "default")
|
|
1400
|
+
: "default";
|
|
1401
|
+
const apiKey = resolveExtensionApiKey(cfg.apiKey);
|
|
1402
|
+
const headers = { ...(cfg.headers ?? {}) };
|
|
1403
|
+
if (cfg.authHeader && apiKey)
|
|
1404
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
1405
|
+
config.providers[name] = {
|
|
1406
|
+
type: "openai-compatible",
|
|
1407
|
+
baseUrl: cfg.baseUrl,
|
|
1408
|
+
model: firstModel,
|
|
1409
|
+
apiKey: apiKey ?? null,
|
|
1410
|
+
apiKeyEnv: null,
|
|
1411
|
+
headers,
|
|
1412
|
+
contextWindow: cfg.models?.[0]?.contextWindow,
|
|
1413
|
+
};
|
|
1414
|
+
extensionProviderNames.add(name);
|
|
1415
|
+
const models = Array.isArray(cfg.models)
|
|
1416
|
+
? cfg.models.map((m) => m.id ?? m.name).filter(Boolean)
|
|
1417
|
+
: [];
|
|
1418
|
+
if (models.length > 0)
|
|
1419
|
+
modelCache.set(name, { models, at: Date.now() });
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
// Re-apply extension tools.
|
|
1423
|
+
try {
|
|
1424
|
+
await mcpManager.syncTools();
|
|
1425
|
+
}
|
|
1426
|
+
catch {
|
|
1427
|
+
// ignore
|
|
1428
|
+
}
|
|
1429
|
+
runtime.setTools([
|
|
1430
|
+
...builtInTools,
|
|
1431
|
+
...collectExtensionTools(extensionLoader),
|
|
1432
|
+
...createMcpTools(mcpManager),
|
|
1433
|
+
]);
|
|
1434
|
+
// Emit session_start with reason reload.
|
|
1435
|
+
for (const loaded of extensionLoader.list()) {
|
|
1436
|
+
const handlers = loaded.registries?.handlers?.get?.("session_start");
|
|
1437
|
+
if (handlers) {
|
|
1438
|
+
for (const handler of handlers) {
|
|
1439
|
+
try {
|
|
1440
|
+
await handler({ type: "session_start", reason: "reload" }, (loaded.context ?? {}));
|
|
1441
|
+
}
|
|
1442
|
+
catch (error) {
|
|
1443
|
+
appendLine(`extension start error: ${error instanceof Error ? error.message : String(error)}`);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
appendLine(`extensions: ${extensionLoader.list().length} reloaded (see /extensions)`);
|
|
1449
|
+
tui.requestRender();
|
|
1450
|
+
}
|
|
1451
|
+
// Wire interactive extension UI callbacks now that the TUI exists.
|
|
1452
|
+
extensionUICallbacks.notify = (message, type) => {
|
|
1453
|
+
const style = type === "error"
|
|
1454
|
+
? theme.tokens.error
|
|
1455
|
+
: type === "warning"
|
|
1456
|
+
? theme.tokens.warning
|
|
1457
|
+
: theme.tokens.accent;
|
|
1458
|
+
appendLine(`${style(`[ext]`)} ${message}`);
|
|
1459
|
+
};
|
|
1460
|
+
extensionUICallbacks.requestRender = () => tui.requestRender();
|
|
1461
|
+
extensionUICallbacks.getToolsExpanded = () => expandTools;
|
|
1462
|
+
extensionUICallbacks.setToolsExpanded = (expanded) => {
|
|
1463
|
+
if (expandTools !== expanded)
|
|
1464
|
+
toggleTools();
|
|
1465
|
+
};
|
|
1167
1466
|
function toggleToolBoxAtScreenRow(screenY) {
|
|
1168
1467
|
const width = terminal.columns;
|
|
1169
1468
|
const outputHeight = Math.max(1, output.viewportHeight || output.render(width).length);
|
|
@@ -1189,6 +1488,9 @@ export async function run(args) {
|
|
|
1189
1488
|
const event = parseSgrMouse(data);
|
|
1190
1489
|
if (!event)
|
|
1191
1490
|
return undefined;
|
|
1491
|
+
// Let the alt-screen TUI own wheel scrolling; only intercept clicks.
|
|
1492
|
+
if (event.type === "wheel")
|
|
1493
|
+
return undefined;
|
|
1192
1494
|
if (!mouseEnabled)
|
|
1193
1495
|
return { consume: true };
|
|
1194
1496
|
handleMouseEvent(event, {
|
|
@@ -1227,18 +1529,19 @@ export async function run(args) {
|
|
|
1227
1529
|
if (!showThinking)
|
|
1228
1530
|
return;
|
|
1229
1531
|
if (!thinkingText) {
|
|
1230
|
-
thinkingText = new
|
|
1532
|
+
thinkingText = new ThinkingBlock();
|
|
1231
1533
|
transcript.add(thinkingText);
|
|
1232
1534
|
thinkingBuffer = "";
|
|
1233
1535
|
}
|
|
1234
1536
|
thinkingBuffer += e.text;
|
|
1235
|
-
thinkingText.
|
|
1537
|
+
thinkingText.buffer = thinkingBuffer;
|
|
1538
|
+
transcript.invalidate();
|
|
1236
1539
|
tui.requestRender();
|
|
1237
1540
|
}
|
|
1238
1541
|
else if (e.type === "assistant_delta") {
|
|
1239
1542
|
activity.setPhase("working");
|
|
1240
1543
|
if (thinkingText) {
|
|
1241
|
-
|
|
1544
|
+
transcript.invalidate();
|
|
1242
1545
|
thinkingText = null;
|
|
1243
1546
|
thinkingBuffer = "";
|
|
1244
1547
|
}
|
|
@@ -1247,7 +1550,7 @@ export async function run(args) {
|
|
|
1247
1550
|
else if (e.type === "assistant_text") {
|
|
1248
1551
|
activity.setPhase("working");
|
|
1249
1552
|
if (thinkingText) {
|
|
1250
|
-
|
|
1553
|
+
transcript.invalidate();
|
|
1251
1554
|
thinkingText = null;
|
|
1252
1555
|
thinkingBuffer = "";
|
|
1253
1556
|
}
|
|
@@ -1330,7 +1633,7 @@ export async function run(args) {
|
|
|
1330
1633
|
}
|
|
1331
1634
|
if (useTools) {
|
|
1332
1635
|
await mcpManager.syncTools();
|
|
1333
|
-
runtime.setTools([...builtInTools, ...createMcpTools(mcpManager)]);
|
|
1636
|
+
runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
|
|
1334
1637
|
}
|
|
1335
1638
|
appendLine(formatMcpStatus(mcpManager.status()));
|
|
1336
1639
|
return;
|
|
@@ -1483,6 +1786,19 @@ export async function run(args) {
|
|
|
1483
1786
|
writeFileSync(outPath, lines.join("\\n"), "utf8");
|
|
1484
1787
|
appendLine(`exported session to ${outPath}`);
|
|
1485
1788
|
}
|
|
1789
|
+
async function runCompact(args) {
|
|
1790
|
+
const focus = (args ?? "").trim();
|
|
1791
|
+
if (session.messages.length <= 6) {
|
|
1792
|
+
appendLine("nothing to compact");
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
const summary = await runtime.compactSession(session, focus || undefined);
|
|
1796
|
+
if (!summary) {
|
|
1797
|
+
appendLine("nothing to compact");
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
appendLine(`compacted: ${summary.slice(0, 200)}`);
|
|
1801
|
+
}
|
|
1486
1802
|
input.onSubmit = async (value) => {
|
|
1487
1803
|
const trimmed = value.trim();
|
|
1488
1804
|
input.setText("");
|
|
@@ -1491,6 +1807,24 @@ export async function run(args) {
|
|
|
1491
1807
|
return;
|
|
1492
1808
|
}
|
|
1493
1809
|
refreshCommandRegistry();
|
|
1810
|
+
if (trimmed === "/steer" || trimmed.startsWith("/steer ")) {
|
|
1811
|
+
const text = trimmed.slice("/steer".length).trim();
|
|
1812
|
+
if (!text) {
|
|
1813
|
+
appendLine("usage: /steer <message>");
|
|
1814
|
+
tui.requestRender();
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
if (activeTurnController) {
|
|
1818
|
+
steeringQueue.push(text);
|
|
1819
|
+
appendLine(`steering: ${text}`, theme.tokens.dim);
|
|
1820
|
+
}
|
|
1821
|
+
else {
|
|
1822
|
+
appendLine(`> ${text}`, theme.tokens.user);
|
|
1823
|
+
await runTurn(text);
|
|
1824
|
+
}
|
|
1825
|
+
tui.requestRender();
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1494
1828
|
if (trimmed.startsWith("/")) {
|
|
1495
1829
|
const result = await commandRegistry.dispatch(trimmed, commandContext);
|
|
1496
1830
|
if (result.exit)
|
|
@@ -1506,7 +1840,13 @@ export async function run(args) {
|
|
|
1506
1840
|
await commandRegistry.dispatch(`/${bare} ${trimmed.slice(bare.length)}`, commandContext);
|
|
1507
1841
|
return;
|
|
1508
1842
|
}
|
|
1509
|
-
|
|
1843
|
+
if (activeTurnController) {
|
|
1844
|
+
messageQueue.push(trimmed);
|
|
1845
|
+
appendLine(`queued: ${trimmed}`, theme.tokens.dim);
|
|
1846
|
+
tui.requestRender();
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
appendLine(`> ${trimmed}`, theme.tokens.user);
|
|
1510
1850
|
await runTurn(trimmed);
|
|
1511
1851
|
};
|
|
1512
1852
|
approvals.handler = interactive
|
|
@@ -1537,8 +1877,8 @@ export async function run(args) {
|
|
|
1537
1877
|
});
|
|
1538
1878
|
});
|
|
1539
1879
|
tui.start();
|
|
1880
|
+
maybeReviewProjectTrust();
|
|
1540
1881
|
if (mouseEnabled) {
|
|
1541
|
-
mouseCapture.enable();
|
|
1542
1882
|
process.once("exit", () => mouseCapture.disable());
|
|
1543
1883
|
if (process.platform !== "win32") {
|
|
1544
1884
|
process.on("SIGTSTP", () => {
|