atom-lite 0.1.0 → 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 +57 -0
- package/README.md +58 -10
- package/dist/agent/loop.d.ts +11 -0
- package/dist/agent/loop.js +238 -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/activity.d.ts +45 -0
- package/dist/cli/activity.js +141 -0
- package/dist/cli/activity.js.map +1 -0
- package/dist/cli/app.js +1234 -378
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/approval-prompt.d.ts +24 -0
- package/dist/cli/approval-prompt.js +82 -0
- package/dist/cli/approval-prompt.js.map +1 -0
- package/dist/cli/banner.d.ts +5 -1
- package/dist/cli/banner.js +10 -4
- package/dist/cli/banner.js.map +1 -1
- package/dist/cli/commands.d.ts +92 -14
- package/dist/cli/commands.js +330 -52
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/diff.d.ts +1 -0
- package/dist/cli/diff.js +28 -0
- package/dist/cli/diff.js.map +1 -0
- package/dist/cli/input.d.ts +2 -0
- package/dist/cli/input.js +10 -0
- package/dist/cli/input.js.map +1 -0
- package/dist/cli/main.js +2 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/mouse.d.ts +54 -0
- package/dist/cli/mouse.js +86 -0
- package/dist/cli/mouse.js.map +1 -0
- package/dist/cli/palette.d.ts +41 -0
- package/dist/cli/palette.js +212 -0
- package/dist/cli/palette.js.map +1 -0
- package/dist/cli/panel.d.ts +29 -0
- package/dist/cli/panel.js +48 -0
- package/dist/cli/panel.js.map +1 -0
- package/dist/cli/picker.d.ts +8 -0
- package/dist/cli/picker.js +26 -28
- package/dist/cli/picker.js.map +1 -1
- package/dist/cli/status-line.d.ts +6 -0
- package/dist/cli/status-line.js +21 -7
- package/dist/cli/status-line.js.map +1 -1
- package/dist/cli/stream-text.d.ts +4 -8
- package/dist/cli/stream-text.js +26 -12
- package/dist/cli/stream-text.js.map +1 -1
- package/dist/cli/theme.d.ts +48 -0
- package/dist/cli/theme.js +160 -0
- package/dist/cli/theme.js.map +1 -0
- package/dist/cli/tool-box.d.ts +41 -0
- package/dist/cli/tool-box.js +177 -0
- package/dist/cli/tool-box.js.map +1 -0
- package/dist/cli/transcript.d.ts +24 -1
- package/dist/cli/transcript.js +65 -2
- 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 +12 -0
- package/dist/config/defaults.js +12 -0
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/load.d.ts +2 -0
- package/dist/config/load.js +36 -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 +53 -0
- package/dist/config/schema.js +33 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/config/write.d.ts +2 -0
- package/dist/config/write.js +9 -0
- package/dist/config/write.js.map +1 -1
- package/dist/context/builder.d.ts +7 -0
- package/dist/context/builder.js +11 -0
- package/dist/context/builder.js.map +1 -0
- 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 +101 -0
- package/dist/extensions/api.js +219 -0
- package/dist/extensions/api.js.map +1 -0
- package/dist/extensions/discovery.d.ts +42 -0
- package/dist/extensions/discovery.js +326 -0
- package/dist/extensions/discovery.js.map +1 -0
- package/dist/extensions/loader.d.ts +68 -0
- package/dist/extensions/loader.js +353 -0
- package/dist/extensions/loader.js.map +1 -0
- 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 +6 -0
- package/dist/extensions/trust.js +55 -0
- package/dist/extensions/trust.js.map +1 -0
- package/dist/extensions/types.d.ts +94 -0
- package/dist/extensions/types.js +2 -0
- package/dist/extensions/types.js.map +1 -0
- 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 +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/client.d.ts +73 -0
- package/dist/mcp/client.js +363 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/format.d.ts +3 -0
- package/dist/mcp/format.js +37 -0
- package/dist/mcp/format.js.map +1 -0
- package/dist/mcp/index.d.ts +6 -0
- package/dist/mcp/index.js +7 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/manager.d.ts +53 -0
- package/dist/mcp/manager.js +349 -0
- package/dist/mcp/manager.js.map +1 -0
- package/dist/mcp/stdio-transport.d.ts +26 -0
- package/dist/mcp/stdio-transport.js +174 -0
- package/dist/mcp/stdio-transport.js.map +1 -0
- package/dist/mcp/tools.d.ts +22 -0
- package/dist/mcp/tools.js +162 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/mcp/types.d.ts +92 -0
- package/dist/mcp/types.js +2 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/providers/factory.js +1 -0
- package/dist/providers/factory.js.map +1 -1
- package/dist/runtime/executor.d.ts +2 -0
- package/dist/runtime/executor.js +4 -0
- package/dist/runtime/executor.js.map +1 -0
- package/dist/runtime/scheduler.d.ts +4 -3
- package/dist/runtime/scheduler.js +7 -98
- 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/events.d.ts +9 -0
- package/dist/sessions/events.js +11 -6
- package/dist/sessions/events.js.map +1 -1
- package/dist/sessions/history.d.ts +18 -0
- package/dist/sessions/history.js +76 -0
- package/dist/sessions/history.js.map +1 -0
- package/dist/sessions/index.d.ts +2 -0
- package/dist/sessions/index.js +2 -0
- package/dist/sessions/index.js.map +1 -1
- package/dist/sessions/search.d.ts +16 -0
- package/dist/sessions/search.js +136 -0
- package/dist/sessions/search.js.map +1 -0
- package/dist/sessions/store.js +7 -4
- 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/edit.js +2 -1
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/types.d.ts +6 -0
- package/dist/types.d.ts +2 -1
- package/dist/types.js.map +1 -1
- package/package.json +3 -1
package/dist/cli/app.js
CHANGED
|
@@ -1,21 +1,38 @@
|
|
|
1
1
|
import { loadConfig } from "../config/load.js";
|
|
2
|
-
import {
|
|
2
|
+
import { defaultAgentDir, userConfigDir } from "../config/paths.js";
|
|
3
|
+
import { saveProviderSelection, saveTheme, } from "../config/write.js";
|
|
3
4
|
import { resolveApiKey, saveApiKey } from "../config/credentials.js";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { writeFileSync, existsSync } from "node:fs";
|
|
4
7
|
import { execSync } from "node:child_process";
|
|
5
8
|
import { openSearchablePicker } from "./picker.js";
|
|
9
|
+
import { openCommandPalette } from "./palette.js";
|
|
10
|
+
import { bareCommandName, createCommandRegistry } from "./commands.js";
|
|
11
|
+
import { resolveTheme, themeLabel, themeNames, markdownThemeFor, } from "./theme.js";
|
|
6
12
|
import { SecretInput } from "./secret-input.js";
|
|
7
13
|
import { StatusLine } from "./status-line.js";
|
|
14
|
+
import { Activity } from "./activity.js";
|
|
15
|
+
import { ToolBox } from "./tool-box.js";
|
|
16
|
+
import { ApprovalPrompt } from "./approval-prompt.js";
|
|
8
17
|
import { Banner } from "./banner.js";
|
|
9
18
|
import { formatContextUsage, knownContextWindow, } from "../providers/context-window.js";
|
|
10
19
|
import { StreamText } from "./stream-text.js";
|
|
11
20
|
import { Transcript } from "./transcript.js";
|
|
12
21
|
import { AgentRuntime } from "../agent/loop.js";
|
|
13
22
|
import { JsonlSessionStore } from "../sessions/store.js";
|
|
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";
|
|
27
|
+
import { formatSessionList, formatSessionSearch, searchSessions, sessionTitleFromEvents, } from "../sessions/search.js";
|
|
28
|
+
import { providerSwitchNotice, sessionHistoryLines, sessionSelectionFromEvents, } from "../sessions/history.js";
|
|
29
|
+
import { blockAtLine, createMouseCapture, handleMouseEvent, parseSgrMouse, shouldEnableMouse, } from "./mouse.js";
|
|
14
30
|
import { readTool, writeTool, editTool, bashTool, webfetchTool, websearchTool } from "../tools/index.js";
|
|
31
|
+
import { McpManager, formatMcpStatus, formatMcpDoctor, createMcpTools } from "../mcp/index.js";
|
|
15
32
|
import { createProvider } from "../providers/factory.js";
|
|
16
33
|
import { availableProviderNames, getPreset, listModelsForProvider, materializeProviderConfig, probeApiKey, providerLabel, PROVIDER_PRESETS, } from "../providers/catalog.js";
|
|
17
34
|
import { ContextManager } from "../context/manager.js";
|
|
18
|
-
import { ProcessTerminal, isKeyRelease, Key, matchesKey, } from "@earendil-works/pi-tui";
|
|
35
|
+
import { ProcessTerminal, isKeyRelease, isKeyRepeat, Key, matchesKey, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
|
|
19
36
|
export async function run(args) {
|
|
20
37
|
let providerOverride;
|
|
21
38
|
let modelOverride;
|
|
@@ -23,10 +40,20 @@ export async function run(args) {
|
|
|
23
40
|
let debug = false;
|
|
24
41
|
let showVersion = false;
|
|
25
42
|
let sessionOverride;
|
|
43
|
+
let themeOverride;
|
|
44
|
+
let noColor = false;
|
|
45
|
+
let listSessions = false;
|
|
46
|
+
let searchSessionsQuery;
|
|
26
47
|
const positional = [];
|
|
48
|
+
const extensionPaths = [];
|
|
27
49
|
for (let i = 0; i < args.length; i++) {
|
|
28
50
|
const a = args[i];
|
|
29
|
-
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")
|
|
30
57
|
providerOverride = args[++i];
|
|
31
58
|
else if (a === "--model")
|
|
32
59
|
modelOverride = args[++i];
|
|
@@ -40,10 +67,18 @@ export async function run(args) {
|
|
|
40
67
|
positional.push("__continue__");
|
|
41
68
|
else if (a === "--session")
|
|
42
69
|
sessionOverride = args[++i];
|
|
70
|
+
else if (a === "--list-sessions")
|
|
71
|
+
listSessions = true;
|
|
72
|
+
else if (a === "--search-sessions")
|
|
73
|
+
searchSessionsQuery = args[++i] ?? "";
|
|
74
|
+
else if (a === "--theme")
|
|
75
|
+
themeOverride = args[++i];
|
|
76
|
+
else if (a === "--no-color")
|
|
77
|
+
noColor = true;
|
|
43
78
|
else if (a === "--no-tools")
|
|
44
79
|
positional.push("__notools__");
|
|
45
80
|
else if (a === "--help") {
|
|
46
|
-
process.stdout.write("atom-lite [task] [--provider NAME] [--model MODEL] [--continue] [--session ID] [--no-tools] [--json] [--debug] [--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");
|
|
47
82
|
return;
|
|
48
83
|
}
|
|
49
84
|
else if (a.startsWith("--")) {
|
|
@@ -54,7 +89,24 @@ export async function run(args) {
|
|
|
54
89
|
}
|
|
55
90
|
}
|
|
56
91
|
if (showVersion) {
|
|
57
|
-
process.stdout.write("0.1
|
|
92
|
+
process.stdout.write("0.3.1\n");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (listSessions || searchSessionsQuery !== undefined) {
|
|
96
|
+
const store = new JsonlSessionStore();
|
|
97
|
+
if (listSessions) {
|
|
98
|
+
process.stdout.write(`${formatSessionList(store.list())}\n`);
|
|
99
|
+
}
|
|
100
|
+
if (searchSessionsQuery !== undefined) {
|
|
101
|
+
const query = searchSessionsQuery.trim();
|
|
102
|
+
if (!query) {
|
|
103
|
+
process.stderr.write("atom-lite: --search-sessions requires a query\n");
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const matches = searchSessions(store.list(), query);
|
|
108
|
+
process.stdout.write(`${formatSessionSearch(matches)}\n`);
|
|
109
|
+
}
|
|
58
110
|
return;
|
|
59
111
|
}
|
|
60
112
|
let config = loadConfig();
|
|
@@ -71,7 +123,9 @@ export async function run(args) {
|
|
|
71
123
|
if (modelOverride)
|
|
72
124
|
modelByProvider.set(provider.id, modelOverride);
|
|
73
125
|
const useTools = !positional.includes("__notools__");
|
|
74
|
-
const
|
|
126
|
+
const builtInTools = useTools
|
|
127
|
+
? [readTool, writeTool, editTool, bashTool, webfetchTool, websearchTool]
|
|
128
|
+
: [];
|
|
75
129
|
const interactive = process.stdin.isTTY === true && !jsonMode;
|
|
76
130
|
// one-shot task from positional or stdin
|
|
77
131
|
let stdinTask = "";
|
|
@@ -84,39 +138,127 @@ export async function run(args) {
|
|
|
84
138
|
const taskArgs = positional.filter((p) => !p.startsWith("__"));
|
|
85
139
|
const oneShot = (taskArgs.join(" ").trim() || stdinTask).trim();
|
|
86
140
|
const store = new JsonlSessionStore();
|
|
87
|
-
|
|
141
|
+
let session = sessionOverride
|
|
88
142
|
? store.resume(sessionOverride) ?? store.create(process.cwd(), provider.id)
|
|
89
143
|
: positional.includes("__continue__")
|
|
90
144
|
? store.resumeLatest(process.cwd()) ?? store.create(process.cwd(), provider.id)
|
|
91
145
|
: store.create(process.cwd(), provider.id);
|
|
146
|
+
const mcpServers = (config.mcpServers ?? {});
|
|
147
|
+
const mcpManager = new McpManager({
|
|
148
|
+
servers: mcpServers,
|
|
149
|
+
onEvent: (event) => {
|
|
150
|
+
try {
|
|
151
|
+
session?.appendEvent?.(event);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// Observability must not break the session.
|
|
155
|
+
}
|
|
156
|
+
},
|
|
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
|
+
}
|
|
179
|
+
const extensionLoader = new ExtensionLoader({
|
|
180
|
+
cliPaths: extensionPaths,
|
|
181
|
+
cwd: process.cwd(),
|
|
182
|
+
agentDir: defaultAgentDir(),
|
|
183
|
+
userDir: join(userConfigDir(), "extensions"),
|
|
184
|
+
projectDir: process.cwd(),
|
|
185
|
+
});
|
|
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(() => {
|
|
195
|
+
// Extension failures are reported per extension.
|
|
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
|
+
};
|
|
92
246
|
const contextManager = new ContextManager({
|
|
93
247
|
systemPrompt: "You are a coding assistant.",
|
|
94
248
|
maxInputTokens: config.context.maxInputTokens ?? null,
|
|
95
249
|
reserveOutputTokens: config.context.reserveOutputTokens,
|
|
96
250
|
compactAtPercent: config.context.compactAtPercent,
|
|
97
251
|
maxProjectInstructionBytes: config.context.maxProjectInstructionBytes,
|
|
252
|
+
compactEveryNTurns: config.context.compactEveryNTurns ?? 0,
|
|
98
253
|
provider,
|
|
99
254
|
model: modelOverride ?? "default",
|
|
100
255
|
userInstructionsPath: undefined,
|
|
101
256
|
});
|
|
102
257
|
let sessionTrusted = false;
|
|
103
|
-
let appendLineFn = () => { };
|
|
104
|
-
let pendingApproval = null;
|
|
105
258
|
const approvals = {
|
|
106
259
|
mode: config.runtime.approvalMode ?? "ask",
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
appendLineFn(`[approval] ${req.command}`);
|
|
110
|
-
appendLineFn(` cwd: ${req.cwd} timeout: ${req.timeoutMs}ms sandboxed: ${req.sandboxed}`);
|
|
111
|
-
appendLineFn(" type y (approve) / n (deny) / t (trust session)");
|
|
112
|
-
return new Promise((resolve) => {
|
|
113
|
-
pendingApproval = (d) => {
|
|
114
|
-
pendingApproval = null;
|
|
115
|
-
resolve(d);
|
|
116
|
-
};
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
: undefined,
|
|
260
|
+
// Installed below once the interactive UI helpers exist.
|
|
261
|
+
handler: undefined,
|
|
120
262
|
isTrusted: () => sessionTrusted,
|
|
121
263
|
trustSession: () => {
|
|
122
264
|
sessionTrusted = true;
|
|
@@ -127,17 +269,25 @@ export async function run(args) {
|
|
|
127
269
|
provider,
|
|
128
270
|
model: modelOverride,
|
|
129
271
|
workspace: process.cwd(),
|
|
130
|
-
tools,
|
|
272
|
+
tools: allTools,
|
|
131
273
|
approvals,
|
|
132
274
|
contextManager,
|
|
275
|
+
extensionDispatch,
|
|
133
276
|
bashConcurrency: config.runtime.bashConcurrency,
|
|
134
277
|
});
|
|
278
|
+
mcpManager.onToolsChanged = () => {
|
|
279
|
+
runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
|
|
280
|
+
};
|
|
135
281
|
if (oneShot || !interactive) {
|
|
136
282
|
// non-interactive / one-shot path
|
|
137
283
|
const controller = new AbortController();
|
|
138
284
|
const onSigint = () => controller.abort();
|
|
139
285
|
process.on("SIGINT", onSigint);
|
|
140
286
|
try {
|
|
287
|
+
if (useTools) {
|
|
288
|
+
await mcpManager.syncTools();
|
|
289
|
+
runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
|
|
290
|
+
}
|
|
141
291
|
const result = await runtime.runAgentTurn(oneShot || " ", session, controller.signal);
|
|
142
292
|
if (jsonMode) {
|
|
143
293
|
process.stdout.write(JSON.stringify({ result, sessionId: session.id }) + "\n");
|
|
@@ -154,22 +304,39 @@ export async function run(args) {
|
|
|
154
304
|
process.exit(1);
|
|
155
305
|
}
|
|
156
306
|
finally {
|
|
307
|
+
await mcpManager.close().catch(() => { });
|
|
157
308
|
process.off("SIGINT", onSigint);
|
|
158
309
|
}
|
|
159
310
|
return;
|
|
160
311
|
}
|
|
161
312
|
// ---- Interactive TUI (first-class) ----
|
|
162
|
-
const {
|
|
313
|
+
const { TuiAltScreen, Text, Input, Editor, Container, Spacer, ScrollView, VStack, Loader, Markdown, Box } = await import("@earendil-works/pi-tui");
|
|
314
|
+
const mouseEnabled = shouldEnableMouse({
|
|
315
|
+
interactive,
|
|
316
|
+
configured: config.ui.mouse,
|
|
317
|
+
stdoutIsTTY: process.stdout.isTTY === true,
|
|
318
|
+
});
|
|
163
319
|
const terminal = new ProcessTerminal();
|
|
164
|
-
const tui = new
|
|
320
|
+
const tui = new TuiAltScreen(terminal, false, undefined, { mouse: mouseEnabled });
|
|
321
|
+
const mouseCapture = createMouseCapture((data) => terminal.write(data));
|
|
165
322
|
let effortLevel = "medium";
|
|
166
323
|
runtime.setEffort(effortLevel);
|
|
167
|
-
|
|
168
|
-
|
|
324
|
+
let themeName = themeOverride ?? config.ui.theme;
|
|
325
|
+
if (noColor)
|
|
326
|
+
themeName = "ansi";
|
|
327
|
+
let terminalScheme;
|
|
328
|
+
let theme = resolveTheme(themeName, {
|
|
329
|
+
noColor,
|
|
330
|
+
env: process.env,
|
|
331
|
+
scheme: terminalScheme,
|
|
332
|
+
});
|
|
333
|
+
const transcript = new Transcript(theme);
|
|
334
|
+
const banner = new Banner(theme);
|
|
335
|
+
transcript.add(banner);
|
|
169
336
|
transcript.add(new Spacer(1));
|
|
170
337
|
const content = transcript.container;
|
|
171
338
|
const output = new ScrollView(content, { follow: "end" });
|
|
172
|
-
const streamText = new StreamText(transcript);
|
|
339
|
+
const streamText = new StreamText(transcript, 200, 200, markdownThemeFor(theme));
|
|
173
340
|
function headerLabel() {
|
|
174
341
|
const cwd = process.cwd();
|
|
175
342
|
const home = process.env.HOME ?? "";
|
|
@@ -188,23 +355,27 @@ export async function run(args) {
|
|
|
188
355
|
}
|
|
189
356
|
return branch ? `${displayPath}:${branch}` : displayPath;
|
|
190
357
|
}
|
|
191
|
-
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
192
358
|
// paddingY = 0 keeps the workspace path directly under the status line.
|
|
193
|
-
const header = new Text(dim(headerLabel()), 1, 0);
|
|
194
|
-
const statusBar = new StatusLine();
|
|
359
|
+
const header = new Text(theme.tokens.dim(headerLabel()), 1, 0);
|
|
360
|
+
const statusBar = new StatusLine(theme);
|
|
361
|
+
const activity = new Activity({ theme, onChange: () => updateStatus() });
|
|
195
362
|
statusBar.setParts({
|
|
196
|
-
left:
|
|
197
|
-
tokens:
|
|
198
|
-
effort:
|
|
363
|
+
left: `${provider.id}/${provider.model ?? "default"}`,
|
|
364
|
+
tokens: "tokens: ?",
|
|
365
|
+
effort: "effort: medium",
|
|
366
|
+
activity: activity.text(),
|
|
199
367
|
});
|
|
200
368
|
const selectListTheme = {
|
|
201
|
-
selectedPrefix: (s) => s,
|
|
202
|
-
selectedText: (s) =>
|
|
203
|
-
description: (s) => s,
|
|
204
|
-
scrollInfo: (s) => s,
|
|
205
|
-
noMatch: (s) => s,
|
|
369
|
+
selectedPrefix: (s) => theme.tokens.accent(s),
|
|
370
|
+
selectedText: (s) => theme.tokens.selection(s),
|
|
371
|
+
description: (s) => theme.tokens.dim(s),
|
|
372
|
+
scrollInfo: (s) => theme.tokens.dim(s),
|
|
373
|
+
noMatch: (s) => theme.tokens.dim(s),
|
|
374
|
+
};
|
|
375
|
+
const editorTheme = {
|
|
376
|
+
borderColor: (s) => theme.tokens.border(s),
|
|
377
|
+
selectList: selectListTheme,
|
|
206
378
|
};
|
|
207
|
-
const editorTheme = { borderColor: (s) => s, selectList: selectListTheme };
|
|
208
379
|
const input = new Editor(tui, editorTheme, {});
|
|
209
380
|
const { CombinedAutocompleteProvider } = await import("@earendil-works/pi-tui");
|
|
210
381
|
const { DefaultSkillRegistry, skillCacheSignature } = await import("../skills/registry.js");
|
|
@@ -218,80 +389,222 @@ export async function run(args) {
|
|
|
218
389
|
}
|
|
219
390
|
return skillReg;
|
|
220
391
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
input.setAutocompleteProvider(new CombinedAutocompleteProvider(slashCommands, process.cwd()));
|
|
238
|
-
tui.addChild(output);
|
|
239
|
-
tui.addChild(new Spacer());
|
|
240
|
-
tui.addChild(input);
|
|
241
|
-
tui.addChild(statusBar);
|
|
392
|
+
let commandRegistry = createCommandRegistry({ skills: skillReg.list() });
|
|
393
|
+
let registrySkillSignature = skillSignature;
|
|
394
|
+
function refreshCommandRegistry() {
|
|
395
|
+
const sig = skillCacheSignature(process.cwd());
|
|
396
|
+
if (sig === registrySkillSignature)
|
|
397
|
+
return;
|
|
398
|
+
commandRegistry = createCommandRegistry({ skills: cachedSkillRegistry().list() });
|
|
399
|
+
registrySkillSignature = sig;
|
|
400
|
+
input.setAutocompleteProvider(new CombinedAutocompleteProvider(commandRegistry.autocomplete(), process.cwd()));
|
|
401
|
+
}
|
|
402
|
+
input.setAutocompleteProvider(new CombinedAutocompleteProvider(commandRegistry.autocomplete(), process.cwd()));
|
|
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);
|
|
242
408
|
// folder:branch info below the status line, dimmed
|
|
243
|
-
|
|
409
|
+
root.addChild(header);
|
|
244
410
|
const dropdownContainer = new Container();
|
|
245
|
-
|
|
411
|
+
root.addChild(dropdownContainer);
|
|
412
|
+
tui.setLayoutRoot(root);
|
|
246
413
|
tui.setFocus(input);
|
|
414
|
+
if (!noColor) {
|
|
415
|
+
try {
|
|
416
|
+
tui.setTerminalColorSchemeNotifications(true);
|
|
417
|
+
tui.onTerminalColorSchemeChange((scheme) => {
|
|
418
|
+
terminalScheme = scheme;
|
|
419
|
+
if (themeName === "auto")
|
|
420
|
+
applyTheme("auto", false);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
// Terminals that do not support colour-scheme reports keep the
|
|
425
|
+
// COLORFGBG/dark fallback.
|
|
426
|
+
}
|
|
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
|
+
}
|
|
247
449
|
let thinkingText = null;
|
|
248
450
|
let thinkingBuffer = "";
|
|
249
|
-
let showThinking =
|
|
250
|
-
let loader = null;
|
|
451
|
+
let showThinking = config.ui.showThinking;
|
|
251
452
|
let expandTools = false;
|
|
252
453
|
let uiModalOpen = false;
|
|
454
|
+
let closeActiveOverlay;
|
|
253
455
|
const toolBoxes = [];
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
: undefined;
|
|
267
|
-
function appendLine(text) {
|
|
456
|
+
/**
|
|
457
|
+
* Close whichever modal overlay owns the keyboard. Callers that clear the
|
|
458
|
+
* dropdown container directly must use this first, or the overlay's input
|
|
459
|
+
* listener would keep consuming keys after it disappears from the screen.
|
|
460
|
+
*/
|
|
461
|
+
function closeOverlay() {
|
|
462
|
+
const close = closeActiveOverlay;
|
|
463
|
+
closeActiveOverlay = undefined;
|
|
464
|
+
close?.();
|
|
465
|
+
}
|
|
466
|
+
function appendLine(text, style = theme.tokens.text) {
|
|
268
467
|
if (thinkingText) {
|
|
269
468
|
thinkingText = null;
|
|
270
469
|
thinkingBuffer = "";
|
|
271
470
|
}
|
|
272
471
|
streamText.finalize();
|
|
273
|
-
transcript.add(new Text(text));
|
|
472
|
+
transcript.add(new Text(style(text)));
|
|
274
473
|
tui.requestRender();
|
|
275
474
|
}
|
|
276
475
|
function appendDelta(delta) {
|
|
277
476
|
streamText.append(delta);
|
|
278
477
|
tui.requestRender();
|
|
279
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
|
+
}
|
|
529
|
+
function thinkingBody() {
|
|
530
|
+
return thinkingBuffer
|
|
531
|
+
.split("\n")
|
|
532
|
+
.map((line) => theme.tokens.dim(`│ ${line}`))
|
|
533
|
+
.join("\n");
|
|
534
|
+
}
|
|
280
535
|
function finalizeText() {
|
|
281
536
|
streamText.finalize();
|
|
282
537
|
}
|
|
538
|
+
let activeTurnController = null;
|
|
539
|
+
const messageQueue = [];
|
|
540
|
+
const steeringQueue = [];
|
|
541
|
+
runtime.setSteeringQueue(steeringQueue);
|
|
542
|
+
/** Run one interactive turn while keeping the status bar's activity fresh. */
|
|
543
|
+
async function runTurn(prompt) {
|
|
544
|
+
activity.start("formulating");
|
|
545
|
+
const controller = new AbortController();
|
|
546
|
+
activeTurnController = controller;
|
|
547
|
+
try {
|
|
548
|
+
if (useTools) {
|
|
549
|
+
await mcpManager.syncTools();
|
|
550
|
+
runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
|
|
551
|
+
}
|
|
552
|
+
await runtime.runAgentTurn(prompt, session, controller.signal);
|
|
553
|
+
}
|
|
554
|
+
catch (err) {
|
|
555
|
+
if (err instanceof Error && err.message === "USER_CANCELLED") {
|
|
556
|
+
appendLine("cancelled");
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
appendLine(`atom-lite: ${err instanceof Error ? err.message : String(err)}`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
finally {
|
|
563
|
+
activeTurnController = null;
|
|
564
|
+
activity.stop();
|
|
565
|
+
if (messageQueue.length > 0) {
|
|
566
|
+
const next = messageQueue.shift();
|
|
567
|
+
appendLine(`> ${next}`, theme.tokens.user);
|
|
568
|
+
void runTurn(next);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
283
572
|
function updateStatus() {
|
|
284
573
|
const usage = runtime.getContextUsage();
|
|
285
574
|
statusBar.setParts({
|
|
286
|
-
left:
|
|
287
|
-
tokens:
|
|
288
|
-
effort:
|
|
575
|
+
left: `${provider.id}/${currentModelLabel()}`,
|
|
576
|
+
tokens: `tokens: ${formatContextUsage(usage.used, usage.window)}`,
|
|
577
|
+
effort: `effort: ${effortLevel}`,
|
|
578
|
+
activity: activity.text(),
|
|
289
579
|
});
|
|
290
580
|
tui.requestRender();
|
|
291
581
|
}
|
|
292
582
|
function currentModelLabel() {
|
|
293
583
|
return modelOverride ?? provider.model ?? "default";
|
|
294
584
|
}
|
|
585
|
+
function applyTheme(name, persist) {
|
|
586
|
+
themeName = name;
|
|
587
|
+
theme = resolveTheme(name, {
|
|
588
|
+
noColor,
|
|
589
|
+
env: process.env,
|
|
590
|
+
scheme: terminalScheme,
|
|
591
|
+
});
|
|
592
|
+
banner.setTheme(theme);
|
|
593
|
+
transcript.setTheme(theme);
|
|
594
|
+
statusBar.setTheme(theme);
|
|
595
|
+
activity.setTheme(theme);
|
|
596
|
+
header.setText(theme.tokens.dim(headerLabel()));
|
|
597
|
+
if (persist) {
|
|
598
|
+
try {
|
|
599
|
+
saveTheme(name);
|
|
600
|
+
}
|
|
601
|
+
catch (err) {
|
|
602
|
+
appendLine(`warning: could not persist theme: ${err instanceof Error ? err.message : String(err)}`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
updateStatus();
|
|
606
|
+
tui.requestRender();
|
|
607
|
+
}
|
|
295
608
|
/**
|
|
296
609
|
* Show the best-known window immediately, then refine it from live provider
|
|
297
610
|
* metadata without blocking the TUI.
|
|
@@ -327,6 +640,17 @@ export async function run(args) {
|
|
|
327
640
|
refreshContextWindow();
|
|
328
641
|
const MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
329
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
|
+
}
|
|
330
654
|
function profileConfig(profile) {
|
|
331
655
|
return config.providers[profile];
|
|
332
656
|
}
|
|
@@ -420,6 +744,13 @@ export async function run(args) {
|
|
|
420
744
|
});
|
|
421
745
|
}
|
|
422
746
|
}
|
|
747
|
+
/** Providers whose model list is missing or older than the cache TTL. */
|
|
748
|
+
function providersNeedingRefresh(names) {
|
|
749
|
+
return names.filter((name) => {
|
|
750
|
+
const cached = modelCache.get(name);
|
|
751
|
+
return !cached || Date.now() - cached.at >= MODEL_CACHE_TTL_MS;
|
|
752
|
+
});
|
|
753
|
+
}
|
|
423
754
|
function switchToModel(profile, model) {
|
|
424
755
|
const pcfg = ensureProfile(profile);
|
|
425
756
|
if (!pcfg) {
|
|
@@ -441,6 +772,35 @@ export async function run(args) {
|
|
|
441
772
|
refreshContextWindow();
|
|
442
773
|
appendLine(`switched to ${profile}/${model}`);
|
|
443
774
|
}
|
|
775
|
+
/** Interpret `/model` arguments: bare model, provider/model, or provider. */
|
|
776
|
+
function switchModelTarget(target) {
|
|
777
|
+
const rest = target.trim();
|
|
778
|
+
if (!rest) {
|
|
779
|
+
openModelPicker();
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const knownModels = modelsFor(provider.id);
|
|
783
|
+
if (knownModels.includes(rest)) {
|
|
784
|
+
switchToModel(provider.id, rest);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const slash = rest.indexOf("/");
|
|
788
|
+
if (slash > 0) {
|
|
789
|
+
const profile = rest.slice(0, slash);
|
|
790
|
+
const model = rest.slice(slash + 1);
|
|
791
|
+
if (profileConfig(profile) || getPreset(profile)) {
|
|
792
|
+
switchToModel(profile, model);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (profileConfig(rest) || getPreset(rest)) {
|
|
797
|
+
ensureProfile(rest);
|
|
798
|
+
const pcfg = profileConfig(rest);
|
|
799
|
+
switchToModel(rest, String(pcfg?.model ?? modelOverride ?? "default"));
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
switchToModel(provider.id, rest);
|
|
803
|
+
}
|
|
444
804
|
function openModelPicker() {
|
|
445
805
|
const names = availableProviderNames(config);
|
|
446
806
|
if (!names.includes(provider.id) && profileConfig(provider.id)) {
|
|
@@ -451,6 +811,7 @@ export async function run(args) {
|
|
|
451
811
|
tui,
|
|
452
812
|
container: dropdownContainer,
|
|
453
813
|
title: "Select model",
|
|
814
|
+
theme,
|
|
454
815
|
items: buildModelItems(),
|
|
455
816
|
maxVisible: 15,
|
|
456
817
|
onSelect: (item) => {
|
|
@@ -466,9 +827,11 @@ export async function run(args) {
|
|
|
466
827
|
},
|
|
467
828
|
restoreFocus: () => tui.setFocus(input),
|
|
468
829
|
});
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
830
|
+
closeActiveOverlay = picker.close;
|
|
831
|
+
const stale = providersNeedingRefresh(names);
|
|
832
|
+
if (stale.length) {
|
|
833
|
+
picker.setStatus(`loading live models from ${stale.length} provider${stale.length === 1 ? "" : "s"}…`);
|
|
834
|
+
refreshModelCache(stale, () => {
|
|
472
835
|
picker.setStatus("");
|
|
473
836
|
picker.updateItems(buildModelItems());
|
|
474
837
|
});
|
|
@@ -521,6 +884,7 @@ export async function run(args) {
|
|
|
521
884
|
appendLine("tip: /model searches every configured provider");
|
|
522
885
|
}
|
|
523
886
|
function closeApiKeyPrompt() {
|
|
887
|
+
closeOverlay();
|
|
524
888
|
dropdownContainer.clear();
|
|
525
889
|
uiModalOpen = false;
|
|
526
890
|
tui.setFocus(input);
|
|
@@ -528,13 +892,14 @@ export async function run(args) {
|
|
|
528
892
|
}
|
|
529
893
|
function promptForApiKey(preset, existingKey) {
|
|
530
894
|
uiModalOpen = true;
|
|
895
|
+
closeOverlay();
|
|
531
896
|
dropdownContainer.clear();
|
|
532
897
|
const wrapper = new Container();
|
|
533
|
-
wrapper.addChild(new Text(
|
|
898
|
+
wrapper.addChild(new Text(theme.tokens.accent(`${preset.label} API key`)));
|
|
534
899
|
if (preset.keyUrl)
|
|
535
|
-
wrapper.addChild(new Text(dim(`create one: ${preset.keyUrl}`)));
|
|
900
|
+
wrapper.addChild(new Text(theme.tokens.dim(`create one: ${preset.keyUrl}`)));
|
|
536
901
|
if (existingKey)
|
|
537
|
-
wrapper.addChild(new Text(dim("a key is already saved — press Enter to keep it")));
|
|
902
|
+
wrapper.addChild(new Text(theme.tokens.dim("a key is already saved — press Enter to keep it")));
|
|
538
903
|
const secret = new SecretInput("API key", existingKey ? "hidden (Enter keeps saved key)" : "paste key (hidden)");
|
|
539
904
|
const status = new Text("");
|
|
540
905
|
wrapper.addChild(secret);
|
|
@@ -564,18 +929,18 @@ export async function run(args) {
|
|
|
564
929
|
}
|
|
565
930
|
return;
|
|
566
931
|
}
|
|
567
|
-
status.setText(dim("an API key is required"));
|
|
932
|
+
status.setText(theme.tokens.dim("an API key is required"));
|
|
568
933
|
tui.requestRender();
|
|
569
934
|
return;
|
|
570
935
|
}
|
|
571
936
|
busy = true;
|
|
572
|
-
status.setText(dim("validating key…"));
|
|
937
|
+
status.setText(theme.tokens.dim("validating key…"));
|
|
573
938
|
tui.requestRender();
|
|
574
939
|
await new Promise((r) => setImmediate(r));
|
|
575
940
|
const probe = await probeApiKey(preset, key);
|
|
576
941
|
if (probe.invalid) {
|
|
577
942
|
busy = false;
|
|
578
|
-
status.setText(
|
|
943
|
+
status.setText(theme.tokens.error(`✗ key rejected by ${preset.label}`));
|
|
579
944
|
secret.reset();
|
|
580
945
|
tui.requestRender();
|
|
581
946
|
return;
|
|
@@ -585,7 +950,7 @@ export async function run(args) {
|
|
|
585
950
|
}
|
|
586
951
|
catch (err) {
|
|
587
952
|
busy = false;
|
|
588
|
-
status.setText(
|
|
953
|
+
status.setText(theme.tokens.error(`✗ could not save key: ${err instanceof Error ? err.message : String(err)}`));
|
|
589
954
|
tui.requestRender();
|
|
590
955
|
return;
|
|
591
956
|
}
|
|
@@ -621,10 +986,11 @@ export async function run(args) {
|
|
|
621
986
|
}
|
|
622
987
|
function openProviderPicker() {
|
|
623
988
|
uiModalOpen = true;
|
|
624
|
-
openSearchablePicker({
|
|
989
|
+
const picker = openSearchablePicker({
|
|
625
990
|
tui,
|
|
626
991
|
container: dropdownContainer,
|
|
627
992
|
title: "Select provider",
|
|
993
|
+
theme,
|
|
628
994
|
items: providerItems(),
|
|
629
995
|
maxVisible: 12,
|
|
630
996
|
onSelect: (item) => {
|
|
@@ -637,69 +1003,554 @@ export async function run(args) {
|
|
|
637
1003
|
},
|
|
638
1004
|
restoreFocus: () => tui.setFocus(input),
|
|
639
1005
|
});
|
|
1006
|
+
closeActiveOverlay = picker.close;
|
|
1007
|
+
}
|
|
1008
|
+
function openEffortPicker() {
|
|
1009
|
+
uiModalOpen = true;
|
|
1010
|
+
const items = [
|
|
1011
|
+
{ value: "low", label: "low", description: "minimal reasoning" },
|
|
1012
|
+
{ value: "medium", label: "medium", description: "balanced" },
|
|
1013
|
+
{ value: "high", label: "high", description: "deep reasoning" },
|
|
1014
|
+
];
|
|
1015
|
+
const picker = openSearchablePicker({
|
|
1016
|
+
tui,
|
|
1017
|
+
container: dropdownContainer,
|
|
1018
|
+
title: "Select effort",
|
|
1019
|
+
theme,
|
|
1020
|
+
items,
|
|
1021
|
+
maxVisible: 3,
|
|
1022
|
+
onSelect: (item) => {
|
|
1023
|
+
uiModalOpen = false;
|
|
1024
|
+
setEffortTarget(item.value);
|
|
1025
|
+
},
|
|
1026
|
+
onCancel: () => {
|
|
1027
|
+
uiModalOpen = false;
|
|
1028
|
+
appendLine("cancelled");
|
|
1029
|
+
},
|
|
1030
|
+
restoreFocus: () => tui.setFocus(input),
|
|
1031
|
+
});
|
|
1032
|
+
closeActiveOverlay = picker.close;
|
|
1033
|
+
picker.setStatus("applies to the next turn");
|
|
1034
|
+
}
|
|
1035
|
+
function setEffortTarget(level) {
|
|
1036
|
+
effortLevel = level;
|
|
1037
|
+
runtime.setEffort(level);
|
|
1038
|
+
updateStatus();
|
|
1039
|
+
appendLine(`effort set to ${effortLevel}`);
|
|
1040
|
+
}
|
|
1041
|
+
function themePickerItems() {
|
|
1042
|
+
return themeNames().map((name) => ({
|
|
1043
|
+
value: name,
|
|
1044
|
+
label: name,
|
|
1045
|
+
description: themeLabel(name),
|
|
1046
|
+
}));
|
|
1047
|
+
}
|
|
1048
|
+
function setThemeTarget(name) {
|
|
1049
|
+
if (!themeNames().includes(name)) {
|
|
1050
|
+
appendLine(`unknown theme: ${name} (available: ${themeNames().join(", ")})`);
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
applyTheme(name, true);
|
|
1054
|
+
appendLine(`theme: ${themeLabel(name)}`);
|
|
1055
|
+
}
|
|
1056
|
+
function openThemePicker() {
|
|
1057
|
+
uiModalOpen = true;
|
|
1058
|
+
const original = themeName;
|
|
1059
|
+
const picker = openSearchablePicker({
|
|
1060
|
+
tui,
|
|
1061
|
+
container: dropdownContainer,
|
|
1062
|
+
title: "Select theme",
|
|
1063
|
+
theme,
|
|
1064
|
+
items: themePickerItems(),
|
|
1065
|
+
maxVisible: 4,
|
|
1066
|
+
initialValue: themeName,
|
|
1067
|
+
onPreview: (item) => applyTheme(item.value, false),
|
|
1068
|
+
onSelect: (item) => {
|
|
1069
|
+
uiModalOpen = false;
|
|
1070
|
+
setThemeTarget(item.value);
|
|
1071
|
+
},
|
|
1072
|
+
onCancel: () => {
|
|
1073
|
+
uiModalOpen = false;
|
|
1074
|
+
applyTheme(original, false);
|
|
1075
|
+
appendLine("cancelled");
|
|
1076
|
+
},
|
|
1077
|
+
restoreFocus: () => tui.setFocus(input),
|
|
1078
|
+
});
|
|
1079
|
+
closeActiveOverlay = picker.close;
|
|
1080
|
+
}
|
|
1081
|
+
async function loadSkill(name, extra) {
|
|
1082
|
+
const registry = cachedSkillRegistry();
|
|
1083
|
+
const content = registry.load(name);
|
|
1084
|
+
if (!content) {
|
|
1085
|
+
appendLine(`skill not found: ${name}`);
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
session.appendMessage({ role: "system", content });
|
|
1089
|
+
if (typeof session.appendEvent === "function") {
|
|
1090
|
+
session.appendEvent({ type: "skill_loaded", name });
|
|
1091
|
+
}
|
|
1092
|
+
appendLine(`Loaded skill: ${name}`);
|
|
1093
|
+
if (extra) {
|
|
1094
|
+
appendLine(`> ${extra}`);
|
|
1095
|
+
await runTurn(extra);
|
|
1096
|
+
}
|
|
1097
|
+
return true;
|
|
1098
|
+
}
|
|
1099
|
+
const commandContext = {
|
|
1100
|
+
get session() {
|
|
1101
|
+
return session;
|
|
1102
|
+
},
|
|
1103
|
+
runtime,
|
|
1104
|
+
approvals,
|
|
1105
|
+
appendLine,
|
|
1106
|
+
clearContent: () => {
|
|
1107
|
+
session.messages.length = 0;
|
|
1108
|
+
content.clear();
|
|
1109
|
+
tui.requestRender();
|
|
1110
|
+
},
|
|
1111
|
+
updateStatus,
|
|
1112
|
+
runDoctor,
|
|
1113
|
+
runMcp,
|
|
1114
|
+
runCompact,
|
|
1115
|
+
runExport,
|
|
1116
|
+
openModelPicker,
|
|
1117
|
+
openProviderPicker,
|
|
1118
|
+
openEffortPicker,
|
|
1119
|
+
openThemePicker,
|
|
1120
|
+
openSessionsPicker,
|
|
1121
|
+
searchSessions: searchSessionsInStore,
|
|
1122
|
+
renameSession,
|
|
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,
|
|
1139
|
+
switchModel: switchModelTarget,
|
|
1140
|
+
startProviderSetup,
|
|
1141
|
+
setEffort: setEffortTarget,
|
|
1142
|
+
toggleThinking: () => {
|
|
1143
|
+
showThinking = !showThinking;
|
|
1144
|
+
return showThinking;
|
|
1145
|
+
},
|
|
1146
|
+
setTheme: setThemeTarget,
|
|
1147
|
+
listSkills: () => cachedSkillRegistry().list().map((skill) => ({
|
|
1148
|
+
name: skill.name,
|
|
1149
|
+
description: skill.description,
|
|
1150
|
+
})),
|
|
1151
|
+
loadSkill,
|
|
1152
|
+
runTurn,
|
|
1153
|
+
requestExit: () => {
|
|
1154
|
+
void mcpManager.close().finally(() => {
|
|
1155
|
+
tui.stop();
|
|
1156
|
+
process.exit(0);
|
|
1157
|
+
});
|
|
1158
|
+
},
|
|
1159
|
+
};
|
|
1160
|
+
function restoreTranscript(next) {
|
|
1161
|
+
streamText.finalize();
|
|
1162
|
+
transcript.reset();
|
|
1163
|
+
banner.setTheme(theme);
|
|
1164
|
+
transcript.add(banner);
|
|
1165
|
+
transcript.add(new Spacer(1));
|
|
1166
|
+
for (const line of sessionHistoryLines(next.messages)) {
|
|
1167
|
+
transcript.addLine(line);
|
|
1168
|
+
}
|
|
1169
|
+
tui.requestRender();
|
|
1170
|
+
}
|
|
1171
|
+
function resumeSession(id) {
|
|
1172
|
+
const next = store.resume(id);
|
|
1173
|
+
if (!next) {
|
|
1174
|
+
appendLine(`could not resume session: ${id}`);
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
const previous = session.id;
|
|
1178
|
+
session = next;
|
|
1179
|
+
session.appendEvent({ type: "session_resumed", from: previous });
|
|
1180
|
+
restoreTranscript(next);
|
|
1181
|
+
const title = sessionTitleFromEvents(next.events);
|
|
1182
|
+
appendLine(`resumed session ${session.id}: ${title} (${session.messages.length} messages)`);
|
|
1183
|
+
const notice = providerSwitchNotice({ provider: provider.id, model: currentModelLabel() }, sessionSelectionFromEvents(next.events));
|
|
1184
|
+
if (notice)
|
|
1185
|
+
appendLine(notice);
|
|
1186
|
+
updateStatus();
|
|
1187
|
+
tui.requestRender();
|
|
1188
|
+
}
|
|
1189
|
+
function renameSession(title) {
|
|
1190
|
+
const trimmed = title.trim();
|
|
1191
|
+
if (!trimmed) {
|
|
1192
|
+
appendLine("usage: /session rename <title>");
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
session.appendEvent({ type: "session_renamed", title: trimmed });
|
|
1196
|
+
appendLine(`session renamed: ${trimmed}`);
|
|
1197
|
+
tui.requestRender();
|
|
1198
|
+
}
|
|
1199
|
+
function searchSessionsInStore(query) {
|
|
1200
|
+
const matches = searchSessions(store.list(), query);
|
|
1201
|
+
if (matches.length === 0) {
|
|
1202
|
+
appendLine(`no sessions match “${query}”`);
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
appendLine(formatSessionSearch(matches));
|
|
1206
|
+
}
|
|
1207
|
+
function openSessionsPicker(initialScope) {
|
|
1208
|
+
uiModalOpen = true;
|
|
1209
|
+
let scope = initialScope;
|
|
1210
|
+
const buildItems = () => {
|
|
1211
|
+
const summaries = scope === "all" ? store.list() : store.list(process.cwd());
|
|
1212
|
+
const toggle = {
|
|
1213
|
+
value: "__scope__",
|
|
1214
|
+
label: scope === "all"
|
|
1215
|
+
? "Show this workspace only"
|
|
1216
|
+
: "Show all workspaces",
|
|
1217
|
+
description: `scope: ${scope === "all" ? "all workspaces" : process.cwd()}`,
|
|
1218
|
+
};
|
|
1219
|
+
return [
|
|
1220
|
+
toggle,
|
|
1221
|
+
...summaries.map((summary) => ({
|
|
1222
|
+
value: `session:${summary.id}`,
|
|
1223
|
+
label: summary.title ?? summary.id,
|
|
1224
|
+
description: `${summary.updatedAt ?? "unknown"} · ${summary.provider} · ${summary.messageCount ?? 0} messages · ${summary.workspace}`,
|
|
1225
|
+
})),
|
|
1226
|
+
];
|
|
1227
|
+
};
|
|
1228
|
+
let picker;
|
|
1229
|
+
picker = openSearchablePicker({
|
|
1230
|
+
tui,
|
|
1231
|
+
container: dropdownContainer,
|
|
1232
|
+
title: "Resume session",
|
|
1233
|
+
theme,
|
|
1234
|
+
items: buildItems(),
|
|
1235
|
+
closeOnSelect: false,
|
|
1236
|
+
onSelect: (item) => {
|
|
1237
|
+
if (item.value === "__scope__") {
|
|
1238
|
+
scope = scope === "all" ? "workspace" : "all";
|
|
1239
|
+
picker.updateItems(buildItems());
|
|
1240
|
+
picker.setStatus(scope === "all" ? "showing all workspaces" : "showing this workspace");
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
picker.close();
|
|
1244
|
+
uiModalOpen = false;
|
|
1245
|
+
resumeSession(item.value.slice("session:".length));
|
|
1246
|
+
},
|
|
1247
|
+
onCancel: () => {
|
|
1248
|
+
uiModalOpen = false;
|
|
1249
|
+
appendLine("cancelled");
|
|
1250
|
+
},
|
|
1251
|
+
restoreFocus: () => tui.setFocus(input),
|
|
1252
|
+
});
|
|
1253
|
+
closeActiveOverlay = picker.close;
|
|
1254
|
+
picker.setStatus(scope === "all" ? "showing all workspaces" : "showing this workspace");
|
|
1255
|
+
}
|
|
1256
|
+
function buildPaletteItems() {
|
|
1257
|
+
const items = [];
|
|
1258
|
+
for (const command of commandRegistry.list()) {
|
|
1259
|
+
const hint = command.argumentHint ? ` ${command.argumentHint}` : "";
|
|
1260
|
+
items.push({
|
|
1261
|
+
id: `command:${command.name}`,
|
|
1262
|
+
label: `/${command.name}${hint}`,
|
|
1263
|
+
description: command.description,
|
|
1264
|
+
category: command.category,
|
|
1265
|
+
keywords: command.aliases,
|
|
1266
|
+
action: async () => {
|
|
1267
|
+
await commandRegistry.dispatch(`/${command.name}`, commandContext);
|
|
1268
|
+
},
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
const profiles = new Set([
|
|
1272
|
+
...availableProviderNames(config),
|
|
1273
|
+
...Object.keys(config.providers),
|
|
1274
|
+
]);
|
|
1275
|
+
for (const profile of profiles) {
|
|
1276
|
+
const preset = getPreset(profile);
|
|
1277
|
+
items.push({
|
|
1278
|
+
id: `provider:${profile}`,
|
|
1279
|
+
label: providerLabel(profile),
|
|
1280
|
+
description: preset?.description ?? "configured provider",
|
|
1281
|
+
category: "Providers",
|
|
1282
|
+
action: () => {
|
|
1283
|
+
if (preset)
|
|
1284
|
+
startProviderSetup(profile);
|
|
1285
|
+
else
|
|
1286
|
+
switchModelTarget(`${profile}/${String(profileConfig(profile)?.model ?? "default")}`);
|
|
1287
|
+
},
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
for (const item of buildModelItems()) {
|
|
1291
|
+
const separator = item.value.indexOf("\u0000");
|
|
1292
|
+
const profile = item.value.slice(0, separator);
|
|
1293
|
+
const model = item.value.slice(separator + 1);
|
|
1294
|
+
items.push({
|
|
1295
|
+
id: `model:${item.value}`,
|
|
1296
|
+
label: item.label.replace(" (current)", ""),
|
|
1297
|
+
description: `${providerLabel(profile)}${item.label.includes("(current)") ? " · current" : ""}`,
|
|
1298
|
+
category: "Models",
|
|
1299
|
+
action: () => switchToModel(profile, model),
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
for (const name of themeNames()) {
|
|
1303
|
+
items.push({
|
|
1304
|
+
id: `theme:${name}`,
|
|
1305
|
+
label: name,
|
|
1306
|
+
description: themeLabel(name),
|
|
1307
|
+
category: "Themes",
|
|
1308
|
+
action: () => setThemeTarget(name),
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
for (const summary of store.list(process.cwd()).slice(0, 8)) {
|
|
1312
|
+
items.push({
|
|
1313
|
+
id: `session:${summary.id}`,
|
|
1314
|
+
label: summary.title ?? summary.id,
|
|
1315
|
+
description: `${summary.updatedAt ?? "unknown"} · ${summary.provider} · ${summary.messageCount ?? 0} messages`,
|
|
1316
|
+
category: "Sessions",
|
|
1317
|
+
keywords: [summary.provider, summary.workspace],
|
|
1318
|
+
action: () => resumeSession(summary.id),
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
return items;
|
|
1322
|
+
}
|
|
1323
|
+
function openPalette() {
|
|
1324
|
+
if (uiModalOpen)
|
|
1325
|
+
return;
|
|
1326
|
+
refreshCommandRegistry();
|
|
1327
|
+
uiModalOpen = true;
|
|
1328
|
+
const palette = openCommandPalette({
|
|
1329
|
+
tui,
|
|
1330
|
+
container: dropdownContainer,
|
|
1331
|
+
items: buildPaletteItems(),
|
|
1332
|
+
theme,
|
|
1333
|
+
title: "Command palette",
|
|
1334
|
+
onSelect: () => {
|
|
1335
|
+
uiModalOpen = false;
|
|
1336
|
+
},
|
|
1337
|
+
onCancel: () => {
|
|
1338
|
+
uiModalOpen = false;
|
|
1339
|
+
appendLine("cancelled");
|
|
1340
|
+
},
|
|
1341
|
+
onError: (err) => {
|
|
1342
|
+
uiModalOpen = false;
|
|
1343
|
+
appendLine(`palette: ${err instanceof Error ? err.message : String(err)}`);
|
|
1344
|
+
},
|
|
1345
|
+
restoreFocus: () => tui.setFocus(input),
|
|
1346
|
+
});
|
|
1347
|
+
closeActiveOverlay = palette.close;
|
|
1348
|
+
// The palette lists models too; refresh stale caches in the background and
|
|
1349
|
+
// update the open list when live discovery finishes.
|
|
1350
|
+
const stale = providersNeedingRefresh(availableProviderNames(config));
|
|
1351
|
+
if (stale.length) {
|
|
1352
|
+
refreshModelCache(stale, () => {
|
|
1353
|
+
palette.updateItems(buildPaletteItems());
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
640
1356
|
}
|
|
641
|
-
appendLineFn = appendLine;
|
|
642
1357
|
function toggleTools() {
|
|
643
1358
|
expandTools = !expandTools;
|
|
644
1359
|
for (const box of toolBoxes) {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
1360
|
+
box.setExpanded(expandTools);
|
|
1361
|
+
}
|
|
1362
|
+
tui.requestRender();
|
|
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
|
+
}
|
|
651
1377
|
}
|
|
652
1378
|
}
|
|
653
|
-
|
|
654
|
-
|
|
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
|
+
}
|
|
655
1446
|
}
|
|
656
1447
|
}
|
|
1448
|
+
appendLine(`extensions: ${extensionLoader.list().length} reloaded (see /extensions)`);
|
|
657
1449
|
tui.requestRender();
|
|
658
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
|
+
};
|
|
1466
|
+
function toggleToolBoxAtScreenRow(screenY) {
|
|
1467
|
+
const width = terminal.columns;
|
|
1468
|
+
const outputHeight = Math.max(1, output.viewportHeight || output.render(width).length);
|
|
1469
|
+
if (screenY < 0 || screenY >= outputHeight)
|
|
1470
|
+
return false;
|
|
1471
|
+
const children = content.children;
|
|
1472
|
+
const contentBlocks = children.map((child, index) => ({
|
|
1473
|
+
id: `content:${index}`,
|
|
1474
|
+
height: child.render(width).length,
|
|
1475
|
+
}));
|
|
1476
|
+
const total = contentBlocks.reduce((sum, block) => sum + block.height, 0);
|
|
1477
|
+
const viewportTop = Math.max(0, total - outputHeight);
|
|
1478
|
+
const inner = blockAtLine(contentBlocks, viewportTop + screenY);
|
|
1479
|
+
if (!inner)
|
|
1480
|
+
return false;
|
|
1481
|
+
const child = children[Number(inner.id.slice("content:".length))];
|
|
1482
|
+
if (!(child instanceof ToolBox))
|
|
1483
|
+
return false;
|
|
1484
|
+
child.toggleExpanded();
|
|
1485
|
+
return true;
|
|
1486
|
+
}
|
|
1487
|
+
tui.addInputListener((data) => {
|
|
1488
|
+
const event = parseSgrMouse(data);
|
|
1489
|
+
if (!event)
|
|
1490
|
+
return undefined;
|
|
1491
|
+
// Let the alt-screen TUI own wheel scrolling; only intercept clicks.
|
|
1492
|
+
if (event.type === "wheel")
|
|
1493
|
+
return undefined;
|
|
1494
|
+
if (!mouseEnabled)
|
|
1495
|
+
return { consume: true };
|
|
1496
|
+
handleMouseEvent(event, {
|
|
1497
|
+
isModalOpen: () => uiModalOpen,
|
|
1498
|
+
scrollBy: (lines) => output.scrollBy(lines),
|
|
1499
|
+
focusInput: () => tui.setFocus(input),
|
|
1500
|
+
toggleToolBox: (screenY) => toggleToolBoxAtScreenRow(screenY),
|
|
1501
|
+
requestRender: () => tui.requestRender(),
|
|
1502
|
+
});
|
|
1503
|
+
return { consume: true };
|
|
1504
|
+
});
|
|
659
1505
|
tui.addInputListener((data) => {
|
|
1506
|
+
// Releases and repeats must not toggle modal shortcuts twice.
|
|
1507
|
+
if (isKeyRelease(data) || isKeyRepeat(data))
|
|
1508
|
+
return undefined;
|
|
1509
|
+
// Ctrl+P opens the command palette unless a modal owns the keyboard.
|
|
1510
|
+
if (matchesKey(data, Key.ctrl("p")) && !uiModalOpen) {
|
|
1511
|
+
openPalette();
|
|
1512
|
+
return { consume: true };
|
|
1513
|
+
}
|
|
660
1514
|
// Ctrl+O toggles tool expansion (unless a modal is capturing input).
|
|
661
1515
|
if (matchesKey(data, Key.ctrl("o")) && !uiModalOpen) {
|
|
662
1516
|
toggleTools();
|
|
663
1517
|
return { consume: true };
|
|
664
1518
|
}
|
|
1519
|
+
// Escape cancels the active turn.
|
|
1520
|
+
if (matchesKey(data, Key.escape) && !uiModalOpen && activeTurnController) {
|
|
1521
|
+
activeTurnController.abort();
|
|
1522
|
+
return { consume: true };
|
|
1523
|
+
}
|
|
665
1524
|
return undefined;
|
|
666
1525
|
});
|
|
667
1526
|
runtime["onEvent"] = (e) => {
|
|
668
1527
|
if (e.type === "thinking_delta") {
|
|
1528
|
+
activity.setPhase("thinking");
|
|
669
1529
|
if (!showThinking)
|
|
670
1530
|
return;
|
|
671
1531
|
if (!thinkingText) {
|
|
672
|
-
thinkingText = new
|
|
1532
|
+
thinkingText = new ThinkingBlock();
|
|
673
1533
|
transcript.add(thinkingText);
|
|
674
1534
|
thinkingBuffer = "";
|
|
675
1535
|
}
|
|
676
1536
|
thinkingBuffer += e.text;
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
.map((l) => `\x1b[2m| ${l}\x1b[0m`)
|
|
680
|
-
.join("\n");
|
|
681
|
-
thinkingText.setText(`\x1b[2mThinking...\x1b[0m\n${prefixed}`);
|
|
1537
|
+
thinkingText.buffer = thinkingBuffer;
|
|
1538
|
+
transcript.invalidate();
|
|
682
1539
|
tui.requestRender();
|
|
683
1540
|
}
|
|
684
1541
|
else if (e.type === "assistant_delta") {
|
|
1542
|
+
activity.setPhase("working");
|
|
685
1543
|
if (thinkingText) {
|
|
686
|
-
|
|
687
|
-
.split("\n")
|
|
688
|
-
.map((l) => `\x1b[2m| ${l}\x1b[0m`)
|
|
689
|
-
.join("\n");
|
|
690
|
-
thinkingText.setText(`\x1b[2mThinking...\x1b[0m\n${prefixed}\n\x1b[2m| end\x1b[0m`);
|
|
1544
|
+
transcript.invalidate();
|
|
691
1545
|
thinkingText = null;
|
|
692
1546
|
thinkingBuffer = "";
|
|
693
1547
|
}
|
|
694
1548
|
appendDelta(e.text);
|
|
695
1549
|
}
|
|
696
1550
|
else if (e.type === "assistant_text") {
|
|
1551
|
+
activity.setPhase("working");
|
|
697
1552
|
if (thinkingText) {
|
|
698
|
-
|
|
699
|
-
.split("\n")
|
|
700
|
-
.map((l) => `\x1b[2m| ${l}\x1b[0m`)
|
|
701
|
-
.join("\n");
|
|
702
|
-
thinkingText.setText(`\x1b[2mThinking...\x1b[0m\n${prefixed}\n\x1b[2m| end\x1b[0m`);
|
|
1553
|
+
transcript.invalidate();
|
|
703
1554
|
thinkingText = null;
|
|
704
1555
|
thinkingBuffer = "";
|
|
705
1556
|
}
|
|
@@ -718,37 +1569,18 @@ export async function run(args) {
|
|
|
718
1569
|
}
|
|
719
1570
|
else if (e.type === "tool_call") {
|
|
720
1571
|
finalizeText();
|
|
721
|
-
|
|
722
|
-
const
|
|
723
|
-
box.
|
|
724
|
-
const output = new Container();
|
|
725
|
-
box.addChild(output);
|
|
1572
|
+
activity.setPhase("working", e.name);
|
|
1573
|
+
const box = new ToolBox({ id: e.id, name: e.name, args: e.args });
|
|
1574
|
+
box.setExpanded(expandTools);
|
|
726
1575
|
transcript.add(box);
|
|
727
|
-
toolBoxes.push(
|
|
728
|
-
id: e.id,
|
|
729
|
-
header,
|
|
730
|
-
output,
|
|
731
|
-
container: box,
|
|
732
|
-
lines: [],
|
|
733
|
-
toolName: e.name,
|
|
734
|
-
args: e.args,
|
|
735
|
-
});
|
|
1576
|
+
toolBoxes.push(box);
|
|
736
1577
|
tui.requestRender();
|
|
737
1578
|
}
|
|
738
1579
|
else if (e.type === "tool_output") {
|
|
739
1580
|
const box = toolBoxes.find((b) => b.id === e.toolCallId);
|
|
740
1581
|
if (box) {
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
box.output.addChild(box.live);
|
|
744
|
-
}
|
|
745
|
-
box.liveBuffer = (box.liveBuffer ?? "") + e.text;
|
|
746
|
-
// Keep the live view bounded; the completed result carries the full
|
|
747
|
-
// capped output, and the JSONL transcript keeps everything.
|
|
748
|
-
if (box.liveBuffer.length > 60_000) {
|
|
749
|
-
box.liveBuffer = box.liveBuffer.slice(-60_000);
|
|
750
|
-
}
|
|
751
|
-
box.live.setText(box.liveBuffer);
|
|
1582
|
+
activity.setPhase("working", e.name);
|
|
1583
|
+
box.appendLive(e.text);
|
|
752
1584
|
tui.requestRender();
|
|
753
1585
|
}
|
|
754
1586
|
}
|
|
@@ -756,44 +1588,14 @@ export async function run(args) {
|
|
|
756
1588
|
const last = toolBoxes.find((b) => b.id === e.toolCallId) ??
|
|
757
1589
|
toolBoxes[toolBoxes.length - 1];
|
|
758
1590
|
if (last) {
|
|
759
|
-
last.
|
|
760
|
-
|
|
761
|
-
last.output.clear();
|
|
762
|
-
if (e.diff) {
|
|
763
|
-
const diffLines = e.diff;
|
|
764
|
-
last.lines = diffLines;
|
|
765
|
-
if (expandTools) {
|
|
766
|
-
const colored = diffLines
|
|
767
|
-
.map((l) => l.startsWith("+")
|
|
768
|
-
? `\x1b[32m${l}\x1b[0m`
|
|
769
|
-
: l.startsWith("-")
|
|
770
|
-
? `\x1b[31m${l}\x1b[0m`
|
|
771
|
-
: l)
|
|
772
|
-
.join("\n");
|
|
773
|
-
if (colored)
|
|
774
|
-
last.output.addChild(new Text(colored));
|
|
775
|
-
}
|
|
776
|
-
else {
|
|
777
|
-
const added = diffLines.filter((l) => l.startsWith("+")).length;
|
|
778
|
-
const removed = diffLines.filter((l) => l.startsWith("-")).length;
|
|
779
|
-
last.output.addChild(new Text(` +${added} -${removed} (ctrl+o to expand)`));
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
else {
|
|
783
|
-
const lines = e.content.split("\n");
|
|
784
|
-
last.lines = lines;
|
|
785
|
-
if (expandTools) {
|
|
786
|
-
last.output.addChild(new Text(lines.join("\n")));
|
|
787
|
-
}
|
|
788
|
-
else {
|
|
789
|
-
last.output.addChild(new Text(` (${lines.length} lines, ctrl+o to expand)`));
|
|
790
|
-
}
|
|
791
|
-
}
|
|
1591
|
+
last.setResult(e.content, { isError: e.isError, diff: e.diff });
|
|
1592
|
+
activity.setPhase("working");
|
|
792
1593
|
tui.requestRender();
|
|
793
1594
|
}
|
|
794
1595
|
}
|
|
795
1596
|
else if (e.type === "final") {
|
|
796
1597
|
finalizeText();
|
|
1598
|
+
activity.stop();
|
|
797
1599
|
}
|
|
798
1600
|
};
|
|
799
1601
|
async function runDoctor() {
|
|
@@ -812,249 +1614,303 @@ export async function run(args) {
|
|
|
812
1614
|
catch (e) {
|
|
813
1615
|
lines.push(` provider generate: FAIL ${e instanceof Error ? e.message : String(e)}`);
|
|
814
1616
|
}
|
|
1617
|
+
lines.push(...formatMcpDoctor(mcpManager.status()));
|
|
815
1618
|
return lines.join("\n");
|
|
816
1619
|
}
|
|
817
|
-
|
|
818
|
-
const trimmed =
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
}
|
|
830
|
-
if (!trimmed) {
|
|
831
|
-
tui.requestRender();
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
834
|
-
const firstToken = trimmed.split(/\s+/)[0].replace(/^\//, "");
|
|
835
|
-
const _skillReg = cachedSkillRegistry();
|
|
836
|
-
const isSkill = _skillReg.list().some((s) => s.name === firstToken) || trimmed.startsWith("/skill ");
|
|
837
|
-
if (isSkill) {
|
|
838
|
-
const skillName = trimmed.startsWith("/skill ") ? trimmed.split(/\s+/)[1] : firstToken;
|
|
839
|
-
const content = _skillReg.load(skillName);
|
|
840
|
-
if (!content) {
|
|
841
|
-
appendLine(`skill not found: ${skillName}`);
|
|
1620
|
+
async function runMcp(args) {
|
|
1621
|
+
const trimmed = (args ?? "").trim();
|
|
1622
|
+
if (trimmed === "reconnect" || trimmed.startsWith("reconnect ")) {
|
|
1623
|
+
const server = trimmed.slice("reconnect".length).trim();
|
|
1624
|
+
try {
|
|
1625
|
+
if (server)
|
|
1626
|
+
await mcpManager.reconnect(server);
|
|
1627
|
+
else
|
|
1628
|
+
await mcpManager.reconnect();
|
|
1629
|
+
}
|
|
1630
|
+
catch (e) {
|
|
1631
|
+
appendLine(`mcp reconnect failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
842
1632
|
return;
|
|
843
1633
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1634
|
+
if (useTools) {
|
|
1635
|
+
await mcpManager.syncTools();
|
|
1636
|
+
runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
|
|
847
1637
|
}
|
|
848
|
-
appendLine(
|
|
849
|
-
const extra = trimmed.startsWith("/skill ")
|
|
850
|
-
? trimmed.split(/\s+/).slice(2).join(" ").trim()
|
|
851
|
-
: trimmed.slice(firstToken.length + 1).trim();
|
|
852
|
-
if (extra) {
|
|
853
|
-
appendLine(`> ${extra}`);
|
|
854
|
-
const controller = new AbortController();
|
|
855
|
-
try {
|
|
856
|
-
await runtime.runAgentTurn(extra, session, controller.signal);
|
|
857
|
-
}
|
|
858
|
-
catch (err) {
|
|
859
|
-
appendLine(`atom-lite: ${err instanceof Error ? err.message : String(err)}`);
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
if (trimmed === "/exit") {
|
|
865
|
-
tui.stop();
|
|
866
|
-
process.exit(0);
|
|
1638
|
+
appendLine(formatMcpStatus(mcpManager.status()));
|
|
867
1639
|
return;
|
|
868
1640
|
}
|
|
869
|
-
if (trimmed
|
|
870
|
-
appendLine(
|
|
1641
|
+
if (trimmed) {
|
|
1642
|
+
appendLine("usage: /mcp [reconnect [server]]");
|
|
871
1643
|
return;
|
|
872
1644
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1645
|
+
appendLine(formatMcpStatus(mcpManager.status()));
|
|
1646
|
+
}
|
|
1647
|
+
async function runExport(args) {
|
|
1648
|
+
const tokens = (args ?? "").trim().split(/\s+/).filter(Boolean);
|
|
1649
|
+
let format = "md";
|
|
1650
|
+
let force = false;
|
|
1651
|
+
let redact = true;
|
|
1652
|
+
const positional = [];
|
|
1653
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1654
|
+
const t = tokens[i];
|
|
1655
|
+
if (t === "--format") {
|
|
1656
|
+
format = tokens[++i] ?? format;
|
|
1657
|
+
}
|
|
1658
|
+
else if (t.startsWith("--format=")) {
|
|
1659
|
+
format = t.slice("--format=".length);
|
|
1660
|
+
}
|
|
1661
|
+
else if (t === "--force") {
|
|
1662
|
+
force = true;
|
|
1663
|
+
}
|
|
1664
|
+
else if (t === "--no-redact") {
|
|
1665
|
+
redact = false;
|
|
1666
|
+
}
|
|
1667
|
+
else {
|
|
1668
|
+
positional.push(t);
|
|
1669
|
+
}
|
|
880
1670
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
1671
|
+
const isJson = format === "json";
|
|
1672
|
+
const ext = isJson ? "json" : "md";
|
|
1673
|
+
const outPath = positional[0] || `${session.id}.${ext}`;
|
|
1674
|
+
if (existsSync(outPath) && !force) {
|
|
1675
|
+
appendLine(`export refused: ${outPath} already exists (use --force)`);
|
|
884
1676
|
return;
|
|
885
1677
|
}
|
|
886
|
-
if (
|
|
887
|
-
|
|
888
|
-
content.clear();
|
|
889
|
-
tui.requestRender();
|
|
890
|
-
appendLine("started new session");
|
|
891
|
-
return;
|
|
1678
|
+
if (!redact) {
|
|
1679
|
+
appendLine("warning: redaction disabled; secrets may be written to disk");
|
|
892
1680
|
}
|
|
893
|
-
|
|
894
|
-
|
|
1681
|
+
const secretPatterns = [
|
|
1682
|
+
/sk-[A-Za-z0-9_-]{8,}/g,
|
|
1683
|
+
/ghp_[A-Za-z0-9]{20,}/g,
|
|
1684
|
+
/gho_[A-Za-z0-9]{20,}/g,
|
|
1685
|
+
/github_pat_[A-Za-z0-9_]{20,}/g,
|
|
1686
|
+
/AKIA[0-9A-Z]{16}/g,
|
|
1687
|
+
/xox[baprs]-[A-Za-z0-9-]{10,}/g,
|
|
1688
|
+
/Bearer\s+[A-Za-z0-9._-]+/g,
|
|
1689
|
+
];
|
|
1690
|
+
const redactText = (value) => {
|
|
1691
|
+
if (!redact)
|
|
1692
|
+
return value;
|
|
1693
|
+
let out = value;
|
|
1694
|
+
for (const pattern of secretPatterns) {
|
|
1695
|
+
out = out.replace(pattern, "[REDACTED]");
|
|
1696
|
+
}
|
|
1697
|
+
return out;
|
|
1698
|
+
};
|
|
1699
|
+
const messageText = (message) => {
|
|
1700
|
+
const content = message?.content;
|
|
1701
|
+
if (Array.isArray(content)) {
|
|
1702
|
+
return content
|
|
1703
|
+
.map((p) => (p?.type === "text" ? p.text : ""))
|
|
1704
|
+
.join("");
|
|
1705
|
+
}
|
|
1706
|
+
return typeof content === "string" ? content : "";
|
|
1707
|
+
};
|
|
1708
|
+
if (isJson) {
|
|
1709
|
+
const events = typeof session.events !== "undefined"
|
|
1710
|
+
? session.events
|
|
1711
|
+
: [];
|
|
1712
|
+
const payload = {
|
|
1713
|
+
version: 1,
|
|
1714
|
+
session: {
|
|
1715
|
+
id: session.id,
|
|
1716
|
+
workspace: session.workspace,
|
|
1717
|
+
provider: session.provider,
|
|
1718
|
+
messageCount: session.messages.length,
|
|
1719
|
+
},
|
|
1720
|
+
messages: session.messages,
|
|
1721
|
+
events,
|
|
1722
|
+
};
|
|
1723
|
+
let json = JSON.stringify(payload, null, 2);
|
|
1724
|
+
if (redact)
|
|
1725
|
+
json = redactText(json);
|
|
1726
|
+
writeFileSync(outPath, json + "\n", "utf8");
|
|
1727
|
+
appendLine(`exported session to ${outPath}`);
|
|
895
1728
|
return;
|
|
896
1729
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1730
|
+
const lines = [];
|
|
1731
|
+
lines.push(`# Session ${session.id}`);
|
|
1732
|
+
lines.push("");
|
|
1733
|
+
lines.push(`- Workspace: ${session.workspace}`);
|
|
1734
|
+
lines.push(`- Provider: ${session.provider}`);
|
|
1735
|
+
lines.push(`- Messages: ${session.messages.length}`);
|
|
1736
|
+
lines.push(`- Exported: ${new Date().toISOString()}`);
|
|
1737
|
+
lines.push("");
|
|
1738
|
+
lines.push("## Table of contents");
|
|
1739
|
+
lines.push("");
|
|
1740
|
+
let turn = 0;
|
|
1741
|
+
for (const message of session.messages) {
|
|
1742
|
+
if (message.role === "user") {
|
|
1743
|
+
turn++;
|
|
1744
|
+
const snippet = redactText(messageText(message)).split("\\n")[0].slice(0, 80);
|
|
1745
|
+
lines.push(`${turn}. ${snippet || "(empty)"}`);
|
|
902
1746
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1747
|
+
}
|
|
1748
|
+
lines.push("");
|
|
1749
|
+
lines.push("## Transcript");
|
|
1750
|
+
lines.push("");
|
|
1751
|
+
for (const message of session.messages) {
|
|
1752
|
+
if (message.role === "user") {
|
|
1753
|
+
lines.push("### User");
|
|
1754
|
+
lines.push("");
|
|
1755
|
+
lines.push(redactText(messageText(message)));
|
|
1756
|
+
lines.push("");
|
|
907
1757
|
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
const
|
|
912
|
-
if (
|
|
913
|
-
|
|
914
|
-
|
|
1758
|
+
else if (message.role === "assistant") {
|
|
1759
|
+
lines.push("### Assistant");
|
|
1760
|
+
lines.push("");
|
|
1761
|
+
const text = redactText(messageText(message));
|
|
1762
|
+
if (text.trim())
|
|
1763
|
+
lines.push(text);
|
|
1764
|
+
if (Array.isArray(message.toolCalls) && message.toolCalls.length) {
|
|
1765
|
+
lines.push("");
|
|
1766
|
+
lines.push("Tool calls:");
|
|
1767
|
+
for (const call of message.toolCalls) {
|
|
1768
|
+
lines.push(`- \`${call.name}\` ${JSON.stringify(call.arguments ?? {})}`);
|
|
1769
|
+
}
|
|
915
1770
|
}
|
|
1771
|
+
lines.push("");
|
|
916
1772
|
}
|
|
917
|
-
if (
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1773
|
+
else if (message.role === "tool") {
|
|
1774
|
+
lines.push(`### Tool result: ${message.name}`);
|
|
1775
|
+
lines.push("");
|
|
1776
|
+
const content = redactText(String(message.content ?? ""));
|
|
1777
|
+
const truncated = content.length > 4000
|
|
1778
|
+
? content.slice(0, 4000) + "\\n… (truncated)"
|
|
1779
|
+
: content;
|
|
1780
|
+
lines.push("```");
|
|
1781
|
+
lines.push(truncated);
|
|
1782
|
+
lines.push("```");
|
|
1783
|
+
lines.push("");
|
|
922
1784
|
}
|
|
923
|
-
|
|
1785
|
+
}
|
|
1786
|
+
writeFileSync(outPath, lines.join("\\n"), "utf8");
|
|
1787
|
+
appendLine(`exported session to ${outPath}`);
|
|
1788
|
+
}
|
|
1789
|
+
async function runCompact(args) {
|
|
1790
|
+
const focus = (args ?? "").trim();
|
|
1791
|
+
if (session.messages.length <= 6) {
|
|
1792
|
+
appendLine("nothing to compact");
|
|
924
1793
|
return;
|
|
925
1794
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
startProviderSetup(rest);
|
|
930
|
-
else
|
|
931
|
-
openProviderPicker();
|
|
1795
|
+
const summary = await runtime.compactSession(session, focus || undefined);
|
|
1796
|
+
if (!summary) {
|
|
1797
|
+
appendLine("nothing to compact");
|
|
932
1798
|
return;
|
|
933
1799
|
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
appendLine(skills.map((sk) => `${sk.name} - ${sk.description}`).join("\n"));
|
|
1800
|
+
appendLine(`compacted: ${summary.slice(0, 200)}`);
|
|
1801
|
+
}
|
|
1802
|
+
input.onSubmit = async (value) => {
|
|
1803
|
+
const trimmed = value.trim();
|
|
1804
|
+
input.setText("");
|
|
1805
|
+
if (!trimmed) {
|
|
1806
|
+
tui.requestRender();
|
|
942
1807
|
return;
|
|
943
1808
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
const
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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();
|
|
950
1815
|
return;
|
|
951
1816
|
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1817
|
+
if (activeTurnController) {
|
|
1818
|
+
steeringQueue.push(text);
|
|
1819
|
+
appendLine(`steering: ${text}`, theme.tokens.dim);
|
|
955
1820
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
appendLine(`> ${extra}`);
|
|
960
|
-
const controller = new AbortController();
|
|
961
|
-
try {
|
|
962
|
-
await runtime.runAgentTurn(extra, session, controller.signal);
|
|
963
|
-
}
|
|
964
|
-
catch (err) {
|
|
965
|
-
appendLine(`atom-lite: ${err instanceof Error ? err.message : String(err)}`);
|
|
966
|
-
}
|
|
1821
|
+
else {
|
|
1822
|
+
appendLine(`> ${text}`, theme.tokens.user);
|
|
1823
|
+
await runTurn(text);
|
|
967
1824
|
}
|
|
1825
|
+
tui.requestRender();
|
|
968
1826
|
return;
|
|
969
1827
|
}
|
|
970
|
-
if (trimmed
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
return;
|
|
974
|
-
}
|
|
975
|
-
if (trimmed.startsWith("/effort")) {
|
|
976
|
-
const parts = trimmed.split(/\s+/);
|
|
977
|
-
if (parts.length >= 2) {
|
|
978
|
-
const lvl = parts[1];
|
|
979
|
-
if (lvl === "low" || lvl === "medium" || lvl === "high") {
|
|
980
|
-
effortLevel = lvl;
|
|
981
|
-
runtime.setEffort(lvl);
|
|
982
|
-
updateStatus();
|
|
983
|
-
appendLine(`effort set to ${effortLevel}`);
|
|
984
|
-
}
|
|
985
|
-
else {
|
|
986
|
-
appendLine("usage: /effort low|medium|high");
|
|
987
|
-
}
|
|
1828
|
+
if (trimmed.startsWith("/")) {
|
|
1829
|
+
const result = await commandRegistry.dispatch(trimmed, commandContext);
|
|
1830
|
+
if (result.exit)
|
|
988
1831
|
return;
|
|
1832
|
+
if (!result.handled) {
|
|
1833
|
+
appendLine(`unknown command: ${trimmed.split(/\s+/)[0]} (type /help for a list of commands)`);
|
|
989
1834
|
}
|
|
990
|
-
// Interactive dropdown
|
|
991
|
-
const items = [
|
|
992
|
-
{ value: "low", label: "low", description: "minimal reasoning" },
|
|
993
|
-
{ value: "medium", label: "medium", description: "balanced" },
|
|
994
|
-
{ value: "high", label: "high", description: "deep reasoning" },
|
|
995
|
-
];
|
|
996
|
-
const { SelectList } = await import("@earendil-works/pi-tui");
|
|
997
|
-
const selectListTheme = {
|
|
998
|
-
selectedPrefix: (s) => s,
|
|
999
|
-
selectedText: (s) => `\x1b[7m${s}\x1b[0m`,
|
|
1000
|
-
description: (s) => s,
|
|
1001
|
-
scrollInfo: (s) => s,
|
|
1002
|
-
noMatch: (s) => s,
|
|
1003
|
-
};
|
|
1004
|
-
const selectList = new SelectList(items, 3, selectListTheme);
|
|
1005
|
-
dropdownContainer.addChild(selectList);
|
|
1006
|
-
tui.requestRender();
|
|
1007
|
-
const removeListener = tui.addInputListener((data) => {
|
|
1008
|
-
// Skip kitty-protocol key-release events to avoid double moves.
|
|
1009
|
-
if (isKeyRelease(data))
|
|
1010
|
-
return { consume: true };
|
|
1011
|
-
selectList.handleInput(data);
|
|
1012
|
-
tui.requestRender();
|
|
1013
|
-
return { consume: true };
|
|
1014
|
-
});
|
|
1015
|
-
selectList.onSelect = (item) => {
|
|
1016
|
-
removeListener();
|
|
1017
|
-
const lvl = item.value;
|
|
1018
|
-
effortLevel = lvl;
|
|
1019
|
-
runtime.setEffort(lvl);
|
|
1020
|
-
dropdownContainer.clear();
|
|
1021
|
-
tui.setFocus(input);
|
|
1022
|
-
updateStatus();
|
|
1023
|
-
appendLine(`effort set to ${effortLevel}`);
|
|
1024
|
-
};
|
|
1025
|
-
selectList.onCancel = () => {
|
|
1026
|
-
removeListener();
|
|
1027
|
-
dropdownContainer.clear();
|
|
1028
|
-
tui.setFocus(input);
|
|
1029
|
-
appendLine("cancelled");
|
|
1030
|
-
};
|
|
1031
1835
|
return;
|
|
1032
1836
|
}
|
|
1033
|
-
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1036
|
-
await
|
|
1837
|
+
// Skills remain invokable by bare name, but still run through the registry.
|
|
1838
|
+
const bare = bareCommandName(commandRegistry, trimmed);
|
|
1839
|
+
if (bare) {
|
|
1840
|
+
await commandRegistry.dispatch(`/${bare} ${trimmed.slice(bare.length)}`, commandContext);
|
|
1841
|
+
return;
|
|
1037
1842
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1843
|
+
if (activeTurnController) {
|
|
1844
|
+
messageQueue.push(trimmed);
|
|
1845
|
+
appendLine(`queued: ${trimmed}`, theme.tokens.dim);
|
|
1846
|
+
tui.requestRender();
|
|
1847
|
+
return;
|
|
1040
1848
|
}
|
|
1849
|
+
appendLine(`> ${trimmed}`, theme.tokens.user);
|
|
1850
|
+
await runTurn(trimmed);
|
|
1041
1851
|
};
|
|
1042
|
-
// fix approvals handler to use appendLine (defined above via closure var)
|
|
1043
1852
|
approvals.handler = interactive
|
|
1044
|
-
?
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1853
|
+
? (req) => new Promise((resolve) => {
|
|
1854
|
+
uiModalOpen = true;
|
|
1855
|
+
closeOverlay();
|
|
1856
|
+
dropdownContainer.clear();
|
|
1857
|
+
activity.setPhase("waiting");
|
|
1858
|
+
const prompt = new ApprovalPrompt(req, (decision) => {
|
|
1859
|
+
uiModalOpen = false;
|
|
1860
|
+
dropdownContainer.clear();
|
|
1861
|
+
tui.setFocus(input);
|
|
1862
|
+
activity.setPhase("working");
|
|
1863
|
+
if (decision === "trust-session") {
|
|
1864
|
+
appendLine("trusted this session: shell commands will run without approval");
|
|
1865
|
+
}
|
|
1866
|
+
resolve(decision);
|
|
1055
1867
|
});
|
|
1056
|
-
|
|
1868
|
+
dropdownContainer.addChild(prompt);
|
|
1869
|
+
tui.setFocus(prompt);
|
|
1870
|
+
tui.requestRender();
|
|
1871
|
+
})
|
|
1057
1872
|
: undefined;
|
|
1873
|
+
process.on("SIGTERM", () => {
|
|
1874
|
+
void mcpManager.close().finally(() => {
|
|
1875
|
+
tui.stop();
|
|
1876
|
+
process.exit(0);
|
|
1877
|
+
});
|
|
1878
|
+
});
|
|
1058
1879
|
tui.start();
|
|
1880
|
+
maybeReviewProjectTrust();
|
|
1881
|
+
if (mouseEnabled) {
|
|
1882
|
+
process.once("exit", () => mouseCapture.disable());
|
|
1883
|
+
if (process.platform !== "win32") {
|
|
1884
|
+
process.on("SIGTSTP", () => {
|
|
1885
|
+
mouseCapture.disable();
|
|
1886
|
+
process.kill(process.pid, "SIGSTOP");
|
|
1887
|
+
});
|
|
1888
|
+
process.on("SIGCONT", () => {
|
|
1889
|
+
mouseCapture.enable();
|
|
1890
|
+
tui.requestRender();
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
// Some terminals answer OSC 11 but not the colour-scheme report. Query the
|
|
1895
|
+
// background once and derive light/dark from its luminance.
|
|
1896
|
+
if (!noColor && typeof tui.queryTerminalColors === "function") {
|
|
1897
|
+
void tui
|
|
1898
|
+
.queryTerminalColors({ timeoutMs: 250 })
|
|
1899
|
+
.then((colors) => {
|
|
1900
|
+
const background = colors.background;
|
|
1901
|
+
if (!background)
|
|
1902
|
+
return;
|
|
1903
|
+
const luminance = (0.2126 * background.r +
|
|
1904
|
+
0.7152 * background.g +
|
|
1905
|
+
0.0722 * background.b) /
|
|
1906
|
+
255;
|
|
1907
|
+
terminalScheme = luminance > 0.5 ? "light" : "dark";
|
|
1908
|
+
if (themeName === "auto")
|
|
1909
|
+
applyTheme("auto", false);
|
|
1910
|
+
})
|
|
1911
|
+
.catch(() => {
|
|
1912
|
+
// Keep the static fallback.
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1059
1915
|
}
|
|
1060
1916
|
//# sourceMappingURL=app.js.map
|