atom-lite 0.4.1 → 0.6.2
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 +35 -1
- package/README.md +41 -108
- package/dist/agent/loop.d.ts +15 -0
- package/dist/agent/loop.js +277 -57
- 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/system-prompt.d.ts +1 -0
- package/dist/agent/system-prompt.js +23 -0
- package/dist/agent/system-prompt.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 +15 -0
- package/dist/cli/app.js +570 -68
- 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/diff-view.d.ts +18 -0
- package/dist/cli/diff-view.js +66 -0
- package/dist/cli/diff-view.js.map +1 -0
- 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/panel.js +8 -8
- package/dist/cli/panel.js.map +1 -1
- package/dist/cli/status-line.d.ts +2 -0
- package/dist/cli/status-line.js +9 -1
- package/dist/cli/status-line.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/tool-box.d.ts +1 -1
- package/dist/cli/tool-box.js +44 -39
- package/dist/cli/tool-box.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 +6 -5
- package/dist/config/defaults.js +8 -3
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/load.js +33 -3
- 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 +14 -14
- package/dist/config/schema.js +6 -5
- 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/modes.d.ts +3 -0
- package/dist/runtime/modes.js +8 -0
- package/dist/runtime/modes.js.map +1 -0
- 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 +136 -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/skills/registry.js +8 -1
- package/dist/skills/registry.js.map +1 -1
- package/dist/tools/bash.js +7 -4
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/glob.d.ts +2 -0
- package/dist/tools/glob.js +97 -0
- package/dist/tools/glob.js.map +1 -0
- package/dist/tools/grep.d.ts +2 -0
- package/dist/tools/grep.js +290 -0
- package/dist/tools/grep.js.map +1 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/types.d.ts +8 -0
- package/dist/types.d.ts +0 -3
- package/dist/types.js +1 -4
- package/dist/types.js.map +1 -1
- package/package.json +5 -2
- package/skills/atom-lite-extensions/SKILL.md +240 -0
- package/skills/caveman/SKILL.md +89 -0
- package/skills/code-review/SKILL.md +46 -0
- package/skills/optimization/SKILL.md +51 -0
package/dist/cli/app.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { loadConfig } from "../config/load.js";
|
|
2
|
+
import { DEFAULT_SYSTEM_PROMPT } from "../agent/system-prompt.js";
|
|
3
|
+
import { defaultAgentDir, userConfigDir } from "../config/paths.js";
|
|
2
4
|
import { saveProviderSelection, saveTheme, } from "../config/write.js";
|
|
3
5
|
import { resolveApiKey, saveApiKey } from "../config/credentials.js";
|
|
4
6
|
import { join } from "node:path";
|
|
5
7
|
import { writeFileSync, existsSync } from "node:fs";
|
|
6
|
-
import { execSync } from "node:child_process";
|
|
8
|
+
import { execSync, spawn } from "node:child_process";
|
|
9
|
+
import { defaultShell, terminateProcessTree } from "../security/process.js";
|
|
7
10
|
import { openSearchablePicker } from "./picker.js";
|
|
8
11
|
import { openCommandPalette } from "./palette.js";
|
|
9
12
|
import { bareCommandName, createCommandRegistry } from "./commands.js";
|
|
@@ -12,7 +15,6 @@ import { SecretInput } from "./secret-input.js";
|
|
|
12
15
|
import { StatusLine } from "./status-line.js";
|
|
13
16
|
import { Activity } from "./activity.js";
|
|
14
17
|
import { ToolBox } from "./tool-box.js";
|
|
15
|
-
import { ApprovalPrompt } from "./approval-prompt.js";
|
|
16
18
|
import { Banner } from "./banner.js";
|
|
17
19
|
import { formatContextUsage, knownContextWindow, } from "../providers/context-window.js";
|
|
18
20
|
import { StreamText } from "./stream-text.js";
|
|
@@ -20,15 +22,18 @@ import { Transcript } from "./transcript.js";
|
|
|
20
22
|
import { AgentRuntime } from "../agent/loop.js";
|
|
21
23
|
import { JsonlSessionStore } from "../sessions/store.js";
|
|
22
24
|
import { ExtensionLoader } from "../extensions/loader.js";
|
|
25
|
+
import { collectExtensionTools } from "../extensions/tool-adapter.js";
|
|
26
|
+
import { denyProject, trustProject } from "../extensions/trust.js";
|
|
27
|
+
import { ProjectTrustPrompt } from "./trust-prompt.js";
|
|
23
28
|
import { formatSessionList, formatSessionSearch, searchSessions, sessionTitleFromEvents, } from "../sessions/search.js";
|
|
24
29
|
import { providerSwitchNotice, sessionHistoryLines, sessionSelectionFromEvents, } from "../sessions/history.js";
|
|
25
30
|
import { blockAtLine, createMouseCapture, handleMouseEvent, parseSgrMouse, shouldEnableMouse, } from "./mouse.js";
|
|
26
|
-
import { readTool, writeTool, editTool, bashTool, webfetchTool, websearchTool } from "../tools/index.js";
|
|
31
|
+
import { readTool, writeTool, editTool, bashTool, webfetchTool, websearchTool, grepTool, globTool } from "../tools/index.js";
|
|
27
32
|
import { McpManager, formatMcpStatus, formatMcpDoctor, createMcpTools } from "../mcp/index.js";
|
|
28
33
|
import { createProvider } from "../providers/factory.js";
|
|
29
34
|
import { availableProviderNames, getPreset, listModelsForProvider, materializeProviderConfig, probeApiKey, providerLabel, PROVIDER_PRESETS, } from "../providers/catalog.js";
|
|
30
35
|
import { ContextManager } from "../context/manager.js";
|
|
31
|
-
import { ProcessTerminal, isKeyRelease, isKeyRepeat, Key, matchesKey, } from "@earendil-works/pi-tui";
|
|
36
|
+
import { ProcessTerminal, isKeyRelease, isKeyRepeat, Key, matchesKey, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
|
|
32
37
|
export async function run(args) {
|
|
33
38
|
let providerOverride;
|
|
34
39
|
let modelOverride;
|
|
@@ -41,9 +46,15 @@ export async function run(args) {
|
|
|
41
46
|
let listSessions = false;
|
|
42
47
|
let searchSessionsQuery;
|
|
43
48
|
const positional = [];
|
|
49
|
+
const extensionPaths = [];
|
|
44
50
|
for (let i = 0; i < args.length; i++) {
|
|
45
51
|
const a = args[i];
|
|
46
|
-
if (a === "--
|
|
52
|
+
if (a === "-e" || a === "--extension") {
|
|
53
|
+
const path = args[++i];
|
|
54
|
+
if (path)
|
|
55
|
+
extensionPaths.push(path);
|
|
56
|
+
}
|
|
57
|
+
else if (a === "--provider")
|
|
47
58
|
providerOverride = args[++i];
|
|
48
59
|
else if (a === "--model")
|
|
49
60
|
modelOverride = args[++i];
|
|
@@ -68,7 +79,7 @@ export async function run(args) {
|
|
|
68
79
|
else if (a === "--no-tools")
|
|
69
80
|
positional.push("__notools__");
|
|
70
81
|
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");
|
|
82
|
+
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
83
|
return;
|
|
73
84
|
}
|
|
74
85
|
else if (a.startsWith("--")) {
|
|
@@ -114,7 +125,7 @@ export async function run(args) {
|
|
|
114
125
|
modelByProvider.set(provider.id, modelOverride);
|
|
115
126
|
const useTools = !positional.includes("__notools__");
|
|
116
127
|
const builtInTools = useTools
|
|
117
|
-
? [readTool, writeTool, editTool, bashTool, webfetchTool, websearchTool]
|
|
128
|
+
? [readTool, writeTool, editTool, bashTool, webfetchTool, websearchTool, grepTool, globTool]
|
|
118
129
|
: [];
|
|
119
130
|
const interactive = process.stdin.isTTY === true && !jsonMode;
|
|
120
131
|
// one-shot task from positional or stdin
|
|
@@ -145,27 +156,108 @@ export async function run(args) {
|
|
|
145
156
|
}
|
|
146
157
|
},
|
|
147
158
|
});
|
|
159
|
+
const extensionUICallbacks = {};
|
|
160
|
+
const extensionProviderNames = new Set();
|
|
161
|
+
function resolveExtensionApiKey(value) {
|
|
162
|
+
if (typeof value !== "string" || !value)
|
|
163
|
+
return undefined;
|
|
164
|
+
if (value.startsWith("!")) {
|
|
165
|
+
try {
|
|
166
|
+
const { execFileSync } = require("node:child_process");
|
|
167
|
+
const parts = value.slice(1).trim().split(/\s+/);
|
|
168
|
+
return execFileSync(parts[0], parts.slice(1), { encoding: "utf8" }).trim();
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (value.startsWith("$"))
|
|
175
|
+
return process.env[value.slice(1)] ?? value;
|
|
176
|
+
if (value.startsWith("${") && value.endsWith("}"))
|
|
177
|
+
return process.env[value.slice(2, -1)] ?? value;
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
148
180
|
const extensionLoader = new ExtensionLoader({
|
|
149
|
-
|
|
181
|
+
cliPaths: extensionPaths,
|
|
182
|
+
cwd: process.cwd(),
|
|
183
|
+
agentDir: defaultAgentDir(),
|
|
184
|
+
userDir: join(userConfigDir(), "extensions"),
|
|
150
185
|
projectDir: process.cwd(),
|
|
151
|
-
trustedProjects: config.extensions?.trustedProjects,
|
|
152
186
|
});
|
|
153
|
-
|
|
187
|
+
const pendingTrust = extensionLoader
|
|
188
|
+
.discover()
|
|
189
|
+
.filter((entry) => entry.source === "project" &&
|
|
190
|
+
!entry.trusted &&
|
|
191
|
+
entry.skipReason === "project is not trusted");
|
|
192
|
+
if (!interactive && pendingTrust.length > 0) {
|
|
193
|
+
process.stderr.write(`atom-lite: skipping ${pendingTrust.length} untrusted project extension(s); run interactively to review\n`);
|
|
194
|
+
}
|
|
195
|
+
await extensionLoader.loadAll().catch(() => {
|
|
154
196
|
// Extension failures are reported per extension.
|
|
155
197
|
});
|
|
198
|
+
// Remove any previously bridged extension providers before re-bridging.
|
|
199
|
+
for (const name of extensionProviderNames) {
|
|
200
|
+
delete config.providers[name];
|
|
201
|
+
}
|
|
202
|
+
extensionProviderNames.clear();
|
|
203
|
+
// Bridge Pi-style extension providers into Atom-lite's config so they
|
|
204
|
+
// appear in /provider, /model, and the command palette.
|
|
205
|
+
for (const loaded of extensionLoader.list()) {
|
|
206
|
+
for (const [name, provider] of (loaded.registries?.providers ?? new Map())) {
|
|
207
|
+
const cfg = provider?.config ?? {};
|
|
208
|
+
const firstModel = Array.isArray(cfg.models) && cfg.models.length > 0
|
|
209
|
+
? (cfg.models[0].id ?? cfg.models[0].name ?? "default")
|
|
210
|
+
: "default";
|
|
211
|
+
const apiKey = resolveExtensionApiKey(cfg.apiKey);
|
|
212
|
+
const headers = { ...(cfg.headers ?? {}) };
|
|
213
|
+
if (cfg.authHeader && apiKey)
|
|
214
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
215
|
+
config.providers[name] = {
|
|
216
|
+
type: "openai-compatible",
|
|
217
|
+
baseUrl: cfg.baseUrl,
|
|
218
|
+
model: firstModel,
|
|
219
|
+
apiKey: apiKey ?? null,
|
|
220
|
+
apiKeyEnv: null,
|
|
221
|
+
headers,
|
|
222
|
+
contextWindow: cfg.models?.[0]?.contextWindow,
|
|
223
|
+
};
|
|
224
|
+
extensionProviderNames.add(name);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const extensionTools = collectExtensionTools(extensionLoader);
|
|
228
|
+
const allTools = [...builtInTools, ...extensionTools];
|
|
229
|
+
const extensionDispatch = async (event, payload) => {
|
|
230
|
+
const results = [];
|
|
231
|
+
for (const loaded of extensionLoader.list()) {
|
|
232
|
+
const handlers = loaded.registries.handlers.get(event);
|
|
233
|
+
if (!handlers)
|
|
234
|
+
continue;
|
|
235
|
+
for (const handler of handlers) {
|
|
236
|
+
try {
|
|
237
|
+
results.push(await handler(payload, loaded.context ?? {}));
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
// eslint-disable-next-line no-console
|
|
241
|
+
console.error(`[extension:${loaded.id}] handler error: ${error instanceof Error ? error.message : String(error)}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return results;
|
|
246
|
+
};
|
|
156
247
|
const contextManager = new ContextManager({
|
|
157
|
-
systemPrompt:
|
|
248
|
+
systemPrompt: DEFAULT_SYSTEM_PROMPT,
|
|
158
249
|
maxInputTokens: config.context.maxInputTokens ?? null,
|
|
159
250
|
reserveOutputTokens: config.context.reserveOutputTokens,
|
|
160
251
|
compactAtPercent: config.context.compactAtPercent,
|
|
161
252
|
maxProjectInstructionBytes: config.context.maxProjectInstructionBytes,
|
|
253
|
+
compactEveryNTurns: config.context.compactEveryNTurns ?? 0,
|
|
162
254
|
provider,
|
|
163
255
|
model: modelOverride ?? "default",
|
|
164
256
|
userInstructionsPath: undefined,
|
|
165
257
|
});
|
|
166
258
|
let sessionTrusted = false;
|
|
167
259
|
const approvals = {
|
|
168
|
-
mode:
|
|
260
|
+
mode: "ask",
|
|
169
261
|
// Installed below once the interactive UI helpers exist.
|
|
170
262
|
handler: undefined,
|
|
171
263
|
isTrusted: () => sessionTrusted,
|
|
@@ -178,13 +270,17 @@ export async function run(args) {
|
|
|
178
270
|
provider,
|
|
179
271
|
model: modelOverride,
|
|
180
272
|
workspace: process.cwd(),
|
|
181
|
-
tools:
|
|
273
|
+
tools: allTools,
|
|
182
274
|
approvals,
|
|
275
|
+
mode: config.runtime.mode,
|
|
276
|
+
maxTokensPerSession: config.runtime.maxTokensPerSession,
|
|
277
|
+
maxTokensPerTurn: config.runtime.maxTokensPerTurn,
|
|
183
278
|
contextManager,
|
|
279
|
+
extensionDispatch,
|
|
184
280
|
bashConcurrency: config.runtime.bashConcurrency,
|
|
185
281
|
});
|
|
186
282
|
mcpManager.onToolsChanged = () => {
|
|
187
|
-
runtime.setTools([...builtInTools, ...createMcpTools(mcpManager)]);
|
|
283
|
+
runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
|
|
188
284
|
};
|
|
189
285
|
if (oneShot || !interactive) {
|
|
190
286
|
// non-interactive / one-shot path
|
|
@@ -194,7 +290,7 @@ export async function run(args) {
|
|
|
194
290
|
try {
|
|
195
291
|
if (useTools) {
|
|
196
292
|
await mcpManager.syncTools();
|
|
197
|
-
runtime.setTools([...builtInTools, ...createMcpTools(mcpManager)]);
|
|
293
|
+
runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
|
|
198
294
|
}
|
|
199
295
|
const result = await runtime.runAgentTurn(oneShot || " ", session, controller.signal);
|
|
200
296
|
if (jsonMode) {
|
|
@@ -218,14 +314,14 @@ export async function run(args) {
|
|
|
218
314
|
return;
|
|
219
315
|
}
|
|
220
316
|
// ---- Interactive TUI (first-class) ----
|
|
221
|
-
const {
|
|
222
|
-
const terminal = new ProcessTerminal();
|
|
223
|
-
const tui = new TuiMainScreen(terminal);
|
|
317
|
+
const { TuiAltScreen, Text, Input, Editor, Container, Spacer, ScrollView, VStack, Loader, Markdown, Box } = await import("@earendil-works/pi-tui");
|
|
224
318
|
const mouseEnabled = shouldEnableMouse({
|
|
225
319
|
interactive,
|
|
226
320
|
configured: config.ui.mouse,
|
|
227
321
|
stdoutIsTTY: process.stdout.isTTY === true,
|
|
228
322
|
});
|
|
323
|
+
const terminal = new ProcessTerminal();
|
|
324
|
+
const tui = new TuiAltScreen(terminal, false, undefined, { mouse: mouseEnabled });
|
|
229
325
|
const mouseCapture = createMouseCapture((data) => terminal.write(data));
|
|
230
326
|
let effortLevel = "medium";
|
|
231
327
|
runtime.setEffort(effortLevel);
|
|
@@ -267,10 +363,12 @@ export async function run(args) {
|
|
|
267
363
|
const header = new Text(theme.tokens.dim(headerLabel()), 1, 0);
|
|
268
364
|
const statusBar = new StatusLine(theme);
|
|
269
365
|
const activity = new Activity({ theme, onChange: () => updateStatus() });
|
|
366
|
+
let currentMode = config.runtime.mode;
|
|
270
367
|
statusBar.setParts({
|
|
271
368
|
left: `${provider.id}/${provider.model ?? "default"}`,
|
|
272
369
|
tokens: "tokens: ?",
|
|
273
370
|
effort: "effort: medium",
|
|
371
|
+
mode: currentMode,
|
|
274
372
|
activity: activity.text(),
|
|
275
373
|
});
|
|
276
374
|
const selectListTheme = {
|
|
@@ -305,17 +403,60 @@ export async function run(args) {
|
|
|
305
403
|
return;
|
|
306
404
|
commandRegistry = createCommandRegistry({ skills: cachedSkillRegistry().list() });
|
|
307
405
|
registrySkillSignature = sig;
|
|
308
|
-
input.setAutocompleteProvider(new CombinedAutocompleteProvider(commandRegistry.autocomplete(), process.cwd()));
|
|
406
|
+
input.setAutocompleteProvider(new CombinedAutocompleteProvider(commandRegistry.autocomplete(), process.cwd(), fdPath));
|
|
309
407
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
408
|
+
const fdPath = (() => {
|
|
409
|
+
for (const bin of ["fd", "fdfind"]) {
|
|
410
|
+
try {
|
|
411
|
+
const found = execSync(`command -v ${bin}`, {
|
|
412
|
+
encoding: "utf8",
|
|
413
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
414
|
+
}).trim();
|
|
415
|
+
if (found)
|
|
416
|
+
return found;
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
// ignore
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return null;
|
|
423
|
+
})();
|
|
424
|
+
input.setAutocompleteProvider(new CombinedAutocompleteProvider(commandRegistry.autocomplete(), process.cwd(), fdPath));
|
|
425
|
+
// Skills should autocomplete (fill the input) rather than execute immediately
|
|
426
|
+
// when selected from the autocomplete list; Enter on a typed command still
|
|
427
|
+
// runs them. Convert a confirming Enter into a Tab completion for skills.
|
|
428
|
+
const originalHandleInput = input.handleInput.bind(input);
|
|
429
|
+
input.handleInput = (data) => {
|
|
430
|
+
try {
|
|
431
|
+
if ((data === "\r" || data === "\n") &&
|
|
432
|
+
input.autocompleteState &&
|
|
433
|
+
input.autocompleteList) {
|
|
434
|
+
const selected = input.autocompleteList.getSelectedItem?.();
|
|
435
|
+
const name = selected?.value ?? selected?.name ?? selected?.label;
|
|
436
|
+
const cmd = name ? commandRegistry.get(String(name).replace(/^\//, "")) : undefined;
|
|
437
|
+
if (cmd?.bareInvokable) {
|
|
438
|
+
originalHandleInput("\t");
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
// Fall through to default handling on any introspection failure.
|
|
445
|
+
}
|
|
446
|
+
originalHandleInput(data);
|
|
447
|
+
};
|
|
448
|
+
const root = new VStack([]);
|
|
449
|
+
// Keep fixed chrome (spacer, input, status, header, dropdown) from being
|
|
450
|
+
// collapsed when the transcript grows; only the output region may shrink.
|
|
451
|
+
root.addChild(output, { grow: 1, shrink: 1, minSize: 0 });
|
|
452
|
+
root.addChild(new Spacer(1), { grow: 0, shrink: 0, minSize: 1 });
|
|
453
|
+
root.addChild(input, { grow: 0, shrink: 0 });
|
|
454
|
+
root.addChild(statusBar, { grow: 0, shrink: 0 });
|
|
315
455
|
// folder:branch info below the status line, dimmed
|
|
316
|
-
|
|
456
|
+
root.addChild(header, { grow: 0, shrink: 0 });
|
|
317
457
|
const dropdownContainer = new Container();
|
|
318
|
-
|
|
458
|
+
root.addChild(dropdownContainer, { grow: 0, shrink: 0 });
|
|
459
|
+
tui.setLayoutRoot(root);
|
|
319
460
|
tui.setFocus(input);
|
|
320
461
|
if (!noColor) {
|
|
321
462
|
try {
|
|
@@ -331,6 +472,27 @@ export async function run(args) {
|
|
|
331
472
|
// COLORFGBG/dark fallback.
|
|
332
473
|
}
|
|
333
474
|
}
|
|
475
|
+
class ThinkingBlock {
|
|
476
|
+
buffer = "";
|
|
477
|
+
render(width) {
|
|
478
|
+
const lines = [theme.tokens.dim("💭 thinking")];
|
|
479
|
+
const sep = theme.tokens.dim("│ ");
|
|
480
|
+
const contentWidth = Math.max(1, width - 2);
|
|
481
|
+
const logicalLines = this.buffer.length > 0 ? this.buffer.split("\\n") : [""];
|
|
482
|
+
for (const logical of logicalLines) {
|
|
483
|
+
const wrapped = wrapTextWithAnsi(logical, contentWidth);
|
|
484
|
+
if (wrapped.length === 0) {
|
|
485
|
+
lines.push(sep);
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
for (const part of wrapped)
|
|
489
|
+
lines.push(sep + theme.tokens.dim(part));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return lines;
|
|
493
|
+
}
|
|
494
|
+
invalidate() { }
|
|
495
|
+
}
|
|
334
496
|
let thinkingText = null;
|
|
335
497
|
let thinkingBuffer = "";
|
|
336
498
|
let showThinking = config.ui.showThinking;
|
|
@@ -348,29 +510,83 @@ export async function run(args) {
|
|
|
348
510
|
closeActiveOverlay = undefined;
|
|
349
511
|
close?.();
|
|
350
512
|
}
|
|
351
|
-
function appendLine(text) {
|
|
513
|
+
function appendLine(text, style = theme.tokens.text) {
|
|
352
514
|
if (thinkingText) {
|
|
353
515
|
thinkingText = null;
|
|
354
516
|
thinkingBuffer = "";
|
|
355
517
|
}
|
|
356
518
|
streamText.finalize();
|
|
357
|
-
transcript.
|
|
519
|
+
transcript.add(new Text(style(text)));
|
|
358
520
|
tui.requestRender();
|
|
359
521
|
}
|
|
360
522
|
function appendDelta(delta) {
|
|
361
523
|
streamText.append(delta);
|
|
362
524
|
tui.requestRender();
|
|
363
525
|
}
|
|
526
|
+
/** Review project-local extensions once per project decision. */
|
|
527
|
+
function maybeReviewProjectTrust() {
|
|
528
|
+
if (uiModalOpen)
|
|
529
|
+
return;
|
|
530
|
+
const pending = extensionLoader
|
|
531
|
+
.discover()
|
|
532
|
+
.filter((entry) => entry.source === "project" &&
|
|
533
|
+
!entry.trusted &&
|
|
534
|
+
entry.skipReason === "project is not trusted");
|
|
535
|
+
if (pending.length === 0)
|
|
536
|
+
return;
|
|
537
|
+
uiModalOpen = true;
|
|
538
|
+
closeOverlay();
|
|
539
|
+
dropdownContainer.clear();
|
|
540
|
+
const prompt = new ProjectTrustPrompt(pending.map((entry) => ({
|
|
541
|
+
id: entry.id,
|
|
542
|
+
entryPath: entry.entryPath,
|
|
543
|
+
version: entry.manifest?.version,
|
|
544
|
+
capabilities: entry.manifest?.capabilities ?? [],
|
|
545
|
+
})), process.cwd(), (decision) => {
|
|
546
|
+
uiModalOpen = false;
|
|
547
|
+
dropdownContainer.clear();
|
|
548
|
+
tui.setFocus(input);
|
|
549
|
+
if (decision === "approve") {
|
|
550
|
+
trustProject(process.cwd());
|
|
551
|
+
appendLine("approved project extensions");
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
denyProject(process.cwd());
|
|
555
|
+
appendLine("denied project extensions");
|
|
556
|
+
}
|
|
557
|
+
void extensionLoader
|
|
558
|
+
.loadAll()
|
|
559
|
+
.then(() => {
|
|
560
|
+
runtime.setTools([
|
|
561
|
+
...builtInTools,
|
|
562
|
+
...collectExtensionTools(extensionLoader),
|
|
563
|
+
...createMcpTools(mcpManager),
|
|
564
|
+
]);
|
|
565
|
+
appendLine(`extensions: ${extensionLoader.list().length} processed (see /extensions)`);
|
|
566
|
+
tui.requestRender();
|
|
567
|
+
})
|
|
568
|
+
.catch((error) => {
|
|
569
|
+
appendLine(`extensions: reload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
dropdownContainer.addChild(prompt);
|
|
573
|
+
tui.setFocus(prompt);
|
|
574
|
+
tui.requestRender();
|
|
575
|
+
}
|
|
364
576
|
function thinkingBody() {
|
|
365
577
|
return thinkingBuffer
|
|
366
578
|
.split("\n")
|
|
367
|
-
.map((line) => theme.tokens.dim(
|
|
579
|
+
.map((line) => theme.tokens.dim(`│ ${line}`))
|
|
368
580
|
.join("\n");
|
|
369
581
|
}
|
|
370
582
|
function finalizeText() {
|
|
371
583
|
streamText.finalize();
|
|
372
584
|
}
|
|
373
585
|
let activeTurnController = null;
|
|
586
|
+
let shellMode = false;
|
|
587
|
+
const messageQueue = [];
|
|
588
|
+
const steeringQueue = [];
|
|
589
|
+
runtime.setSteeringQueue(steeringQueue);
|
|
374
590
|
/** Run one interactive turn while keeping the status bar's activity fresh. */
|
|
375
591
|
async function runTurn(prompt) {
|
|
376
592
|
activity.start("formulating");
|
|
@@ -379,9 +595,13 @@ export async function run(args) {
|
|
|
379
595
|
try {
|
|
380
596
|
if (useTools) {
|
|
381
597
|
await mcpManager.syncTools();
|
|
382
|
-
runtime.setTools([...builtInTools, ...createMcpTools(mcpManager)]);
|
|
598
|
+
runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
|
|
599
|
+
}
|
|
600
|
+
const turnResult = await runtime.runAgentTurn(prompt, session, controller.signal);
|
|
601
|
+
if (turnResult === "TOKEN_BUDGET_REACHED") {
|
|
602
|
+
appendLine("token budget reached; stopping", theme.tokens.warning);
|
|
603
|
+
tui.requestRender();
|
|
383
604
|
}
|
|
384
|
-
await runtime.runAgentTurn(prompt, session, controller.signal);
|
|
385
605
|
}
|
|
386
606
|
catch (err) {
|
|
387
607
|
if (err instanceof Error && err.message === "USER_CANCELLED") {
|
|
@@ -394,6 +614,92 @@ export async function run(args) {
|
|
|
394
614
|
finally {
|
|
395
615
|
activeTurnController = null;
|
|
396
616
|
activity.stop();
|
|
617
|
+
if (messageQueue.length > 0) {
|
|
618
|
+
const next = messageQueue.shift();
|
|
619
|
+
appendLine(`> ${next}`, theme.tokens.user);
|
|
620
|
+
void runTurn(next);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
/** Run a shell command directly (shell mode / `!` prefix). */
|
|
625
|
+
async function runShellCommand(command) {
|
|
626
|
+
const shell = defaultShell();
|
|
627
|
+
const flag = process.platform === "win32" ? "/c" : "-c";
|
|
628
|
+
const start = Date.now();
|
|
629
|
+
const cap = 30000;
|
|
630
|
+
let stdout = "";
|
|
631
|
+
let stderr = "";
|
|
632
|
+
let truncated = false;
|
|
633
|
+
const append = (target, chunk) => {
|
|
634
|
+
const s = chunk.toString("utf8");
|
|
635
|
+
let accepted = s;
|
|
636
|
+
if (target === "out") {
|
|
637
|
+
if (stdout.length + s.length > cap) {
|
|
638
|
+
truncated = true;
|
|
639
|
+
accepted = s.slice(0, Math.max(0, cap - stdout.length));
|
|
640
|
+
stdout += accepted;
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
stdout += s;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
if (stderr.length + s.length > cap) {
|
|
648
|
+
truncated = true;
|
|
649
|
+
accepted = s.slice(0, Math.max(0, cap - stderr.length));
|
|
650
|
+
stderr += accepted;
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
stderr += s;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (accepted) {
|
|
657
|
+
tui.requestRender();
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
try {
|
|
661
|
+
const child = spawn(shell, [flag, command], {
|
|
662
|
+
cwd: process.cwd(),
|
|
663
|
+
env: process.env,
|
|
664
|
+
detached: process.platform !== "win32",
|
|
665
|
+
});
|
|
666
|
+
child.stdout?.on("data", (c) => append("out", c));
|
|
667
|
+
child.stderr?.on("data", (c) => append("err", c));
|
|
668
|
+
const exit = await new Promise((resolve) => {
|
|
669
|
+
const timer = setTimeout(() => {
|
|
670
|
+
terminateProcessTree(child, "SIGTERM");
|
|
671
|
+
setTimeout(() => terminateProcessTree(child, "SIGKILL"), 1000).unref();
|
|
672
|
+
resolve({ code: null, signal: "SIGTERM" });
|
|
673
|
+
}, 120000);
|
|
674
|
+
timer.unref();
|
|
675
|
+
child.on("exit", (code, signal) => {
|
|
676
|
+
clearTimeout(timer);
|
|
677
|
+
resolve({ code, signal });
|
|
678
|
+
});
|
|
679
|
+
child.on("error", (err) => {
|
|
680
|
+
clearTimeout(timer);
|
|
681
|
+
resolve({ code: null, signal: null });
|
|
682
|
+
stderr = `COMMAND_FAILED: ${err instanceof Error ? err.message : String(err)}`;
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
const parts = [];
|
|
686
|
+
parts.push(`exit_code: ${exit.code ?? "null"}`);
|
|
687
|
+
if (exit.signal)
|
|
688
|
+
parts.push(`signal: ${exit.signal}`);
|
|
689
|
+
if (truncated)
|
|
690
|
+
parts.push("OUTPUT_TRUNCATED");
|
|
691
|
+
parts.push(`duration_ms: ${Date.now() - start}`);
|
|
692
|
+
if (stdout)
|
|
693
|
+
parts.push(`stdout:\n${stdout}`);
|
|
694
|
+
if (stderr)
|
|
695
|
+
parts.push(`stderr:\n${stderr}`);
|
|
696
|
+
const content = parts.join("\n");
|
|
697
|
+
appendLine(content, exit.code === 0 && !exit.signal && !stderr
|
|
698
|
+
? theme.tokens.success
|
|
699
|
+
: theme.tokens.error);
|
|
700
|
+
}
|
|
701
|
+
catch (err) {
|
|
702
|
+
appendLine(`shell error: ${err instanceof Error ? err.message : String(err)}`, theme.tokens.error);
|
|
397
703
|
}
|
|
398
704
|
}
|
|
399
705
|
function updateStatus() {
|
|
@@ -402,6 +708,7 @@ export async function run(args) {
|
|
|
402
708
|
left: `${provider.id}/${currentModelLabel()}`,
|
|
403
709
|
tokens: `tokens: ${formatContextUsage(usage.used, usage.window)}`,
|
|
404
710
|
effort: `effort: ${effortLevel}`,
|
|
711
|
+
mode: currentMode,
|
|
405
712
|
activity: activity.text(),
|
|
406
713
|
});
|
|
407
714
|
tui.requestRender();
|
|
@@ -467,6 +774,17 @@ export async function run(args) {
|
|
|
467
774
|
refreshContextWindow();
|
|
468
775
|
const MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
469
776
|
const modelCache = new Map();
|
|
777
|
+
for (const loaded of extensionLoader.list()) {
|
|
778
|
+
for (const [name, provider] of (loaded.registries?.providers ?? new Map())) {
|
|
779
|
+
const cfg = provider?.config ?? {};
|
|
780
|
+
const models = Array.isArray(cfg.models)
|
|
781
|
+
? cfg.models.map((m) => m.id ?? m.name).filter(Boolean)
|
|
782
|
+
: [];
|
|
783
|
+
if (models.length > 0) {
|
|
784
|
+
modelCache.set(name, { models, at: Date.now() });
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
470
788
|
function profileConfig(profile) {
|
|
471
789
|
return config.providers[profile];
|
|
472
790
|
}
|
|
@@ -901,13 +1219,15 @@ export async function run(args) {
|
|
|
901
1219
|
appendLine(`skill not found: ${name}`);
|
|
902
1220
|
return false;
|
|
903
1221
|
}
|
|
1222
|
+
if (extra) {
|
|
1223
|
+
appendLine(`> ${extra}`, theme.tokens.user);
|
|
1224
|
+
}
|
|
904
1225
|
session.appendMessage({ role: "system", content });
|
|
905
1226
|
if (typeof session.appendEvent === "function") {
|
|
906
1227
|
session.appendEvent({ type: "skill_loaded", name });
|
|
907
1228
|
}
|
|
908
|
-
appendLine(`Loaded skill: ${name}
|
|
1229
|
+
appendLine(`Loaded skill: ${name}`, theme.tokens.dim);
|
|
909
1230
|
if (extra) {
|
|
910
|
-
appendLine(`> ${extra}`);
|
|
911
1231
|
await runTurn(extra);
|
|
912
1232
|
}
|
|
913
1233
|
return true;
|
|
@@ -927,6 +1247,7 @@ export async function run(args) {
|
|
|
927
1247
|
updateStatus,
|
|
928
1248
|
runDoctor,
|
|
929
1249
|
runMcp,
|
|
1250
|
+
runCompact,
|
|
930
1251
|
runExport,
|
|
931
1252
|
openModelPicker,
|
|
932
1253
|
openProviderPicker,
|
|
@@ -935,10 +1256,22 @@ export async function run(args) {
|
|
|
935
1256
|
openSessionsPicker,
|
|
936
1257
|
searchSessions: searchSessionsInStore,
|
|
937
1258
|
renameSession,
|
|
938
|
-
listExtensions: () =>
|
|
939
|
-
.
|
|
940
|
-
|
|
941
|
-
|
|
1259
|
+
listExtensions: () => {
|
|
1260
|
+
const report = extensionLoader.report();
|
|
1261
|
+
if (report.length === 0)
|
|
1262
|
+
return "No extensions discovered.";
|
|
1263
|
+
return report
|
|
1264
|
+
.map((ext) => {
|
|
1265
|
+
const origin = ext.origin ? `${ext.source}:${ext.origin}` : ext.source;
|
|
1266
|
+
const version = ext.version ? `v${ext.version}` : "-";
|
|
1267
|
+
const counts = `${ext.toolCount} tools / ${ext.commandCount} commands`;
|
|
1268
|
+
const error = ext.error ? `\t${ext.error}` : "";
|
|
1269
|
+
return `${ext.id}\t${origin}\t${version}\t${ext.status}\t${counts}\t${ext.entryPath}${error}`;
|
|
1270
|
+
})
|
|
1271
|
+
.join("\n");
|
|
1272
|
+
},
|
|
1273
|
+
extensionDoctor: () => extensionLoader.doctor(),
|
|
1274
|
+
reloadExtensions,
|
|
942
1275
|
switchModel: switchModelTarget,
|
|
943
1276
|
startProviderSetup,
|
|
944
1277
|
setEffort: setEffortTarget,
|
|
@@ -1162,8 +1495,111 @@ export async function run(args) {
|
|
|
1162
1495
|
for (const box of toolBoxes) {
|
|
1163
1496
|
box.setExpanded(expandTools);
|
|
1164
1497
|
}
|
|
1498
|
+
transcript.invalidate();
|
|
1499
|
+
tui.requestRender();
|
|
1500
|
+
}
|
|
1501
|
+
async function reloadExtensions() {
|
|
1502
|
+
const previous = extensionLoader.list();
|
|
1503
|
+
// Emit session_shutdown and extension_unload before unloading.
|
|
1504
|
+
for (const loaded of previous) {
|
|
1505
|
+
const handlers = loaded.registries?.handlers?.get?.("session_shutdown");
|
|
1506
|
+
if (handlers) {
|
|
1507
|
+
for (const handler of handlers) {
|
|
1508
|
+
try {
|
|
1509
|
+
await handler({ type: "session_shutdown", reason: "reload" }, (loaded.context ?? {}));
|
|
1510
|
+
}
|
|
1511
|
+
catch (error) {
|
|
1512
|
+
appendLine(`extension shutdown error: ${error instanceof Error ? error.message : String(error)}`);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
await extensionLoader.unloadAll();
|
|
1518
|
+
try {
|
|
1519
|
+
await extensionLoader.loadAll();
|
|
1520
|
+
}
|
|
1521
|
+
catch (error) {
|
|
1522
|
+
appendLine(`extensions: reload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1523
|
+
tui.requestRender();
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
// Remove previously bridged providers, then re-bridge.
|
|
1527
|
+
for (const name of extensionProviderNames) {
|
|
1528
|
+
delete config.providers[name];
|
|
1529
|
+
modelCache.delete(name);
|
|
1530
|
+
}
|
|
1531
|
+
extensionProviderNames.clear();
|
|
1532
|
+
for (const loaded of extensionLoader.list()) {
|
|
1533
|
+
for (const [name, provider] of (loaded.registries?.providers ?? new Map())) {
|
|
1534
|
+
const cfg = provider?.config ?? {};
|
|
1535
|
+
const firstModel = Array.isArray(cfg.models) && cfg.models.length > 0
|
|
1536
|
+
? (cfg.models[0].id ?? cfg.models[0].name ?? "default")
|
|
1537
|
+
: "default";
|
|
1538
|
+
const apiKey = resolveExtensionApiKey(cfg.apiKey);
|
|
1539
|
+
const headers = { ...(cfg.headers ?? {}) };
|
|
1540
|
+
if (cfg.authHeader && apiKey)
|
|
1541
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
1542
|
+
config.providers[name] = {
|
|
1543
|
+
type: "openai-compatible",
|
|
1544
|
+
baseUrl: cfg.baseUrl,
|
|
1545
|
+
model: firstModel,
|
|
1546
|
+
apiKey: apiKey ?? null,
|
|
1547
|
+
apiKeyEnv: null,
|
|
1548
|
+
headers,
|
|
1549
|
+
contextWindow: cfg.models?.[0]?.contextWindow,
|
|
1550
|
+
};
|
|
1551
|
+
extensionProviderNames.add(name);
|
|
1552
|
+
const models = Array.isArray(cfg.models)
|
|
1553
|
+
? cfg.models.map((m) => m.id ?? m.name).filter(Boolean)
|
|
1554
|
+
: [];
|
|
1555
|
+
if (models.length > 0)
|
|
1556
|
+
modelCache.set(name, { models, at: Date.now() });
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
// Re-apply extension tools.
|
|
1560
|
+
try {
|
|
1561
|
+
await mcpManager.syncTools();
|
|
1562
|
+
}
|
|
1563
|
+
catch {
|
|
1564
|
+
// ignore
|
|
1565
|
+
}
|
|
1566
|
+
runtime.setTools([
|
|
1567
|
+
...builtInTools,
|
|
1568
|
+
...collectExtensionTools(extensionLoader),
|
|
1569
|
+
...createMcpTools(mcpManager),
|
|
1570
|
+
]);
|
|
1571
|
+
// Emit session_start with reason reload.
|
|
1572
|
+
for (const loaded of extensionLoader.list()) {
|
|
1573
|
+
const handlers = loaded.registries?.handlers?.get?.("session_start");
|
|
1574
|
+
if (handlers) {
|
|
1575
|
+
for (const handler of handlers) {
|
|
1576
|
+
try {
|
|
1577
|
+
await handler({ type: "session_start", reason: "reload" }, (loaded.context ?? {}));
|
|
1578
|
+
}
|
|
1579
|
+
catch (error) {
|
|
1580
|
+
appendLine(`extension start error: ${error instanceof Error ? error.message : String(error)}`);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
appendLine(`extensions: ${extensionLoader.list().length} reloaded (see /extensions)`);
|
|
1165
1586
|
tui.requestRender();
|
|
1166
1587
|
}
|
|
1588
|
+
// Wire interactive extension UI callbacks now that the TUI exists.
|
|
1589
|
+
extensionUICallbacks.notify = (message, type) => {
|
|
1590
|
+
const style = type === "error"
|
|
1591
|
+
? theme.tokens.error
|
|
1592
|
+
: type === "warning"
|
|
1593
|
+
? theme.tokens.warning
|
|
1594
|
+
: theme.tokens.accent;
|
|
1595
|
+
appendLine(`${style(`[ext]`)} ${message}`);
|
|
1596
|
+
};
|
|
1597
|
+
extensionUICallbacks.requestRender = () => tui.requestRender();
|
|
1598
|
+
extensionUICallbacks.getToolsExpanded = () => expandTools;
|
|
1599
|
+
extensionUICallbacks.setToolsExpanded = (expanded) => {
|
|
1600
|
+
if (expandTools !== expanded)
|
|
1601
|
+
toggleTools();
|
|
1602
|
+
};
|
|
1167
1603
|
function toggleToolBoxAtScreenRow(screenY) {
|
|
1168
1604
|
const width = terminal.columns;
|
|
1169
1605
|
const outputHeight = Math.max(1, output.viewportHeight || output.render(width).length);
|
|
@@ -1183,12 +1619,16 @@ export async function run(args) {
|
|
|
1183
1619
|
if (!(child instanceof ToolBox))
|
|
1184
1620
|
return false;
|
|
1185
1621
|
child.toggleExpanded();
|
|
1622
|
+
transcript.invalidate();
|
|
1186
1623
|
return true;
|
|
1187
1624
|
}
|
|
1188
1625
|
tui.addInputListener((data) => {
|
|
1189
1626
|
const event = parseSgrMouse(data);
|
|
1190
1627
|
if (!event)
|
|
1191
1628
|
return undefined;
|
|
1629
|
+
// Let the alt-screen TUI own wheel scrolling; only intercept clicks.
|
|
1630
|
+
if (event.type === "wheel")
|
|
1631
|
+
return undefined;
|
|
1192
1632
|
if (!mouseEnabled)
|
|
1193
1633
|
return { consume: true };
|
|
1194
1634
|
handleMouseEvent(event, {
|
|
@@ -1214,11 +1654,28 @@ export async function run(args) {
|
|
|
1214
1654
|
toggleTools();
|
|
1215
1655
|
return { consume: true };
|
|
1216
1656
|
}
|
|
1217
|
-
//
|
|
1218
|
-
if (matchesKey(data, Key.
|
|
1219
|
-
|
|
1657
|
+
// Tab toggles plan/yolo mode.
|
|
1658
|
+
if (matchesKey(data, Key.tab) && !uiModalOpen) {
|
|
1659
|
+
currentMode = currentMode === "plan" ? "yolo" : "plan";
|
|
1660
|
+
runtime.setMode(currentMode);
|
|
1661
|
+
updateStatus();
|
|
1662
|
+
appendLine(`mode: ${currentMode}`, theme.tokens.dim);
|
|
1663
|
+
tui.requestRender();
|
|
1220
1664
|
return { consume: true };
|
|
1221
1665
|
}
|
|
1666
|
+
// Escape cancels the active turn, or exits shell mode.
|
|
1667
|
+
if (matchesKey(data, Key.escape) && !uiModalOpen) {
|
|
1668
|
+
if (activeTurnController) {
|
|
1669
|
+
activeTurnController.abort();
|
|
1670
|
+
return { consume: true };
|
|
1671
|
+
}
|
|
1672
|
+
if (shellMode) {
|
|
1673
|
+
shellMode = false;
|
|
1674
|
+
appendLine("shell mode: off", theme.tokens.dim);
|
|
1675
|
+
tui.requestRender();
|
|
1676
|
+
return { consume: true };
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1222
1679
|
return undefined;
|
|
1223
1680
|
});
|
|
1224
1681
|
runtime["onEvent"] = (e) => {
|
|
@@ -1227,18 +1684,19 @@ export async function run(args) {
|
|
|
1227
1684
|
if (!showThinking)
|
|
1228
1685
|
return;
|
|
1229
1686
|
if (!thinkingText) {
|
|
1230
|
-
thinkingText = new
|
|
1687
|
+
thinkingText = new ThinkingBlock();
|
|
1231
1688
|
transcript.add(thinkingText);
|
|
1232
1689
|
thinkingBuffer = "";
|
|
1233
1690
|
}
|
|
1234
1691
|
thinkingBuffer += e.text;
|
|
1235
|
-
thinkingText.
|
|
1692
|
+
thinkingText.buffer = thinkingBuffer;
|
|
1693
|
+
transcript.invalidate();
|
|
1236
1694
|
tui.requestRender();
|
|
1237
1695
|
}
|
|
1238
1696
|
else if (e.type === "assistant_delta") {
|
|
1239
1697
|
activity.setPhase("working");
|
|
1240
1698
|
if (thinkingText) {
|
|
1241
|
-
|
|
1699
|
+
transcript.invalidate();
|
|
1242
1700
|
thinkingText = null;
|
|
1243
1701
|
thinkingBuffer = "";
|
|
1244
1702
|
}
|
|
@@ -1247,7 +1705,7 @@ export async function run(args) {
|
|
|
1247
1705
|
else if (e.type === "assistant_text") {
|
|
1248
1706
|
activity.setPhase("working");
|
|
1249
1707
|
if (thinkingText) {
|
|
1250
|
-
|
|
1708
|
+
transcript.invalidate();
|
|
1251
1709
|
thinkingText = null;
|
|
1252
1710
|
thinkingBuffer = "";
|
|
1253
1711
|
}
|
|
@@ -1257,6 +1715,10 @@ export async function run(args) {
|
|
|
1257
1715
|
else if (e.type === "context_usage") {
|
|
1258
1716
|
updateStatus();
|
|
1259
1717
|
}
|
|
1718
|
+
else if (e.type === "budget_warning") {
|
|
1719
|
+
appendLine(`budget warning (${e.scope}): ${e.used}/${e.limit} tokens`, theme.tokens.warning);
|
|
1720
|
+
tui.requestRender();
|
|
1721
|
+
}
|
|
1260
1722
|
else if (e.type === "model_switched") {
|
|
1261
1723
|
appendLine(`switched model to ${e.provider}/${e.model}`);
|
|
1262
1724
|
if (typeof session.appendEvent === "function") {
|
|
@@ -1278,6 +1740,7 @@ export async function run(args) {
|
|
|
1278
1740
|
if (box) {
|
|
1279
1741
|
activity.setPhase("working", e.name);
|
|
1280
1742
|
box.appendLive(e.text);
|
|
1743
|
+
transcript.invalidate();
|
|
1281
1744
|
tui.requestRender();
|
|
1282
1745
|
}
|
|
1283
1746
|
}
|
|
@@ -1330,7 +1793,7 @@ export async function run(args) {
|
|
|
1330
1793
|
}
|
|
1331
1794
|
if (useTools) {
|
|
1332
1795
|
await mcpManager.syncTools();
|
|
1333
|
-
runtime.setTools([...builtInTools, ...createMcpTools(mcpManager)]);
|
|
1796
|
+
runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
|
|
1334
1797
|
}
|
|
1335
1798
|
appendLine(formatMcpStatus(mcpManager.status()));
|
|
1336
1799
|
return;
|
|
@@ -1483,6 +1946,19 @@ export async function run(args) {
|
|
|
1483
1946
|
writeFileSync(outPath, lines.join("\\n"), "utf8");
|
|
1484
1947
|
appendLine(`exported session to ${outPath}`);
|
|
1485
1948
|
}
|
|
1949
|
+
async function runCompact(args) {
|
|
1950
|
+
const focus = (args ?? "").trim();
|
|
1951
|
+
if (session.messages.length <= 6) {
|
|
1952
|
+
appendLine("nothing to compact");
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
const summary = await runtime.compactSession(session, focus || undefined);
|
|
1956
|
+
if (!summary) {
|
|
1957
|
+
appendLine("nothing to compact");
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
appendLine(`compacted: ${summary.slice(0, 200)}`);
|
|
1961
|
+
}
|
|
1486
1962
|
input.onSubmit = async (value) => {
|
|
1487
1963
|
const trimmed = value.trim();
|
|
1488
1964
|
input.setText("");
|
|
@@ -1490,7 +1966,46 @@ export async function run(args) {
|
|
|
1490
1966
|
tui.requestRender();
|
|
1491
1967
|
return;
|
|
1492
1968
|
}
|
|
1969
|
+
// Shell mode: every line is a shell command. `!` alone (or as first char)
|
|
1970
|
+
// enters shell mode; Escape exits it.
|
|
1971
|
+
if (shellMode) {
|
|
1972
|
+
const cmd = trimmed.startsWith("!") ? trimmed.slice(1).trim() : trimmed;
|
|
1973
|
+
appendLine(`$ ${cmd}`, theme.tokens.user);
|
|
1974
|
+
await runShellCommand(cmd);
|
|
1975
|
+
tui.requestRender();
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
// `!` as first character enters shell mode.
|
|
1979
|
+
if (trimmed === "!" || trimmed.startsWith("! ")) {
|
|
1980
|
+
shellMode = true;
|
|
1981
|
+
const rest = trimmed.slice(1).trim();
|
|
1982
|
+
appendLine("shell mode: on (Escape to exit)", theme.tokens.dim);
|
|
1983
|
+
if (rest) {
|
|
1984
|
+
appendLine(`$ ${rest}`, theme.tokens.user);
|
|
1985
|
+
await runShellCommand(rest);
|
|
1986
|
+
}
|
|
1987
|
+
tui.requestRender();
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1493
1990
|
refreshCommandRegistry();
|
|
1991
|
+
if (trimmed === "/steer" || trimmed.startsWith("/steer ")) {
|
|
1992
|
+
const text = trimmed.slice("/steer".length).trim();
|
|
1993
|
+
if (!text) {
|
|
1994
|
+
appendLine("usage: /steer <message>");
|
|
1995
|
+
tui.requestRender();
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
if (activeTurnController) {
|
|
1999
|
+
steeringQueue.push(text);
|
|
2000
|
+
appendLine(`steering: ${text}`, theme.tokens.dim);
|
|
2001
|
+
}
|
|
2002
|
+
else {
|
|
2003
|
+
appendLine(`> ${text}`, theme.tokens.user);
|
|
2004
|
+
await runTurn(text);
|
|
2005
|
+
}
|
|
2006
|
+
tui.requestRender();
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
1494
2009
|
if (trimmed.startsWith("/")) {
|
|
1495
2010
|
const result = await commandRegistry.dispatch(trimmed, commandContext);
|
|
1496
2011
|
if (result.exit)
|
|
@@ -1506,30 +2021,17 @@ export async function run(args) {
|
|
|
1506
2021
|
await commandRegistry.dispatch(`/${bare} ${trimmed.slice(bare.length)}`, commandContext);
|
|
1507
2022
|
return;
|
|
1508
2023
|
}
|
|
1509
|
-
|
|
2024
|
+
if (activeTurnController) {
|
|
2025
|
+
messageQueue.push(trimmed);
|
|
2026
|
+
appendLine(`queued: ${trimmed}`, theme.tokens.dim);
|
|
2027
|
+
tui.requestRender();
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
appendLine(`> ${trimmed}`, theme.tokens.user);
|
|
1510
2031
|
await runTurn(trimmed);
|
|
1511
2032
|
};
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
uiModalOpen = true;
|
|
1515
|
-
closeOverlay();
|
|
1516
|
-
dropdownContainer.clear();
|
|
1517
|
-
activity.setPhase("waiting");
|
|
1518
|
-
const prompt = new ApprovalPrompt(req, (decision) => {
|
|
1519
|
-
uiModalOpen = false;
|
|
1520
|
-
dropdownContainer.clear();
|
|
1521
|
-
tui.setFocus(input);
|
|
1522
|
-
activity.setPhase("working");
|
|
1523
|
-
if (decision === "trust-session") {
|
|
1524
|
-
appendLine("trusted this session: shell commands will run without approval");
|
|
1525
|
-
}
|
|
1526
|
-
resolve(decision);
|
|
1527
|
-
});
|
|
1528
|
-
dropdownContainer.addChild(prompt);
|
|
1529
|
-
tui.setFocus(prompt);
|
|
1530
|
-
tui.requestRender();
|
|
1531
|
-
})
|
|
1532
|
-
: undefined;
|
|
2033
|
+
// Approval prompts are superseded by plan/yolo modes.
|
|
2034
|
+
approvals.handler = undefined;
|
|
1533
2035
|
process.on("SIGTERM", () => {
|
|
1534
2036
|
void mcpManager.close().finally(() => {
|
|
1535
2037
|
tui.stop();
|
|
@@ -1537,8 +2039,8 @@ export async function run(args) {
|
|
|
1537
2039
|
});
|
|
1538
2040
|
});
|
|
1539
2041
|
tui.start();
|
|
2042
|
+
maybeReviewProjectTrust();
|
|
1540
2043
|
if (mouseEnabled) {
|
|
1541
|
-
mouseCapture.enable();
|
|
1542
2044
|
process.once("exit", () => mouseCapture.disable());
|
|
1543
2045
|
if (process.platform !== "win32") {
|
|
1544
2046
|
process.on("SIGTSTP", () => {
|