atom-lite 0.1.0 → 0.4.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 +46 -0
- package/README.md +58 -10
- package/dist/agent/loop.d.ts +2 -0
- package/dist/agent/loop.js +5 -0
- package/dist/agent/loop.js.map +1 -1
- 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 +893 -377
- 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 +89 -14
- package/dist/cli/commands.js +315 -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 +83 -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 +25 -12
- package/dist/cli/stream-text.js.map +1 -1
- package/dist/cli/theme.d.ts +48 -0
- package/dist/cli/theme.js +158 -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 +9 -0
- package/dist/cli/transcript.js +25 -1
- package/dist/cli/transcript.js.map +1 -1
- package/dist/config/defaults.d.ts +10 -0
- package/dist/config/defaults.js +10 -0
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/load.d.ts +2 -0
- package/dist/config/load.js +32 -0
- package/dist/config/load.js.map +1 -1
- package/dist/config/schema.d.ts +49 -0
- package/dist/config/schema.js +31 -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/extensions/api.d.ts +51 -0
- package/dist/extensions/api.js +101 -0
- package/dist/extensions/api.js.map +1 -0
- package/dist/extensions/discovery.d.ts +26 -0
- package/dist/extensions/discovery.js +243 -0
- package/dist/extensions/discovery.js.map +1 -0
- package/dist/extensions/loader.d.ts +27 -0
- package/dist/extensions/loader.js +113 -0
- package/dist/extensions/loader.js.map +1 -0
- package/dist/extensions/trust.d.ts +4 -0
- package/dist/extensions/trust.js +40 -0
- package/dist/extensions/trust.js.map +1 -0
- package/dist/extensions/types.d.ts +52 -0
- package/dist/extensions/types.js +2 -0
- package/dist/extensions/types.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/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 +191 -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.js +7 -0
- package/dist/runtime/scheduler.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 +5 -1
- package/dist/sessions/store.js.map +1 -1
- package/dist/tools/edit.js +2 -1
- package/dist/tools/edit.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/app.js
CHANGED
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
import { loadConfig } from "../config/load.js";
|
|
2
|
-
import { saveProviderSelection } from "../config/write.js";
|
|
2
|
+
import { saveProviderSelection, saveTheme, } from "../config/write.js";
|
|
3
3
|
import { resolveApiKey, saveApiKey } from "../config/credentials.js";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { writeFileSync, existsSync } from "node:fs";
|
|
4
6
|
import { execSync } from "node:child_process";
|
|
5
7
|
import { openSearchablePicker } from "./picker.js";
|
|
8
|
+
import { openCommandPalette } from "./palette.js";
|
|
9
|
+
import { bareCommandName, createCommandRegistry } from "./commands.js";
|
|
10
|
+
import { resolveTheme, themeLabel, themeNames, markdownThemeFor, } from "./theme.js";
|
|
6
11
|
import { SecretInput } from "./secret-input.js";
|
|
7
12
|
import { StatusLine } from "./status-line.js";
|
|
13
|
+
import { Activity } from "./activity.js";
|
|
14
|
+
import { ToolBox } from "./tool-box.js";
|
|
15
|
+
import { ApprovalPrompt } from "./approval-prompt.js";
|
|
8
16
|
import { Banner } from "./banner.js";
|
|
9
17
|
import { formatContextUsage, knownContextWindow, } from "../providers/context-window.js";
|
|
10
18
|
import { StreamText } from "./stream-text.js";
|
|
11
19
|
import { Transcript } from "./transcript.js";
|
|
12
20
|
import { AgentRuntime } from "../agent/loop.js";
|
|
13
21
|
import { JsonlSessionStore } from "../sessions/store.js";
|
|
22
|
+
import { ExtensionLoader } from "../extensions/loader.js";
|
|
23
|
+
import { formatSessionList, formatSessionSearch, searchSessions, sessionTitleFromEvents, } from "../sessions/search.js";
|
|
24
|
+
import { providerSwitchNotice, sessionHistoryLines, sessionSelectionFromEvents, } from "../sessions/history.js";
|
|
25
|
+
import { blockAtLine, createMouseCapture, handleMouseEvent, parseSgrMouse, shouldEnableMouse, } from "./mouse.js";
|
|
14
26
|
import { readTool, writeTool, editTool, bashTool, webfetchTool, websearchTool } from "../tools/index.js";
|
|
27
|
+
import { McpManager, formatMcpStatus, formatMcpDoctor, createMcpTools } from "../mcp/index.js";
|
|
15
28
|
import { createProvider } from "../providers/factory.js";
|
|
16
29
|
import { availableProviderNames, getPreset, listModelsForProvider, materializeProviderConfig, probeApiKey, providerLabel, PROVIDER_PRESETS, } from "../providers/catalog.js";
|
|
17
30
|
import { ContextManager } from "../context/manager.js";
|
|
18
|
-
import { ProcessTerminal, isKeyRelease, Key, matchesKey, } from "@earendil-works/pi-tui";
|
|
31
|
+
import { ProcessTerminal, isKeyRelease, isKeyRepeat, Key, matchesKey, } from "@earendil-works/pi-tui";
|
|
19
32
|
export async function run(args) {
|
|
20
33
|
let providerOverride;
|
|
21
34
|
let modelOverride;
|
|
@@ -23,6 +36,10 @@ export async function run(args) {
|
|
|
23
36
|
let debug = false;
|
|
24
37
|
let showVersion = false;
|
|
25
38
|
let sessionOverride;
|
|
39
|
+
let themeOverride;
|
|
40
|
+
let noColor = false;
|
|
41
|
+
let listSessions = false;
|
|
42
|
+
let searchSessionsQuery;
|
|
26
43
|
const positional = [];
|
|
27
44
|
for (let i = 0; i < args.length; i++) {
|
|
28
45
|
const a = args[i];
|
|
@@ -40,10 +57,18 @@ export async function run(args) {
|
|
|
40
57
|
positional.push("__continue__");
|
|
41
58
|
else if (a === "--session")
|
|
42
59
|
sessionOverride = args[++i];
|
|
60
|
+
else if (a === "--list-sessions")
|
|
61
|
+
listSessions = true;
|
|
62
|
+
else if (a === "--search-sessions")
|
|
63
|
+
searchSessionsQuery = args[++i] ?? "";
|
|
64
|
+
else if (a === "--theme")
|
|
65
|
+
themeOverride = args[++i];
|
|
66
|
+
else if (a === "--no-color")
|
|
67
|
+
noColor = true;
|
|
43
68
|
else if (a === "--no-tools")
|
|
44
69
|
positional.push("__notools__");
|
|
45
70
|
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");
|
|
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");
|
|
47
72
|
return;
|
|
48
73
|
}
|
|
49
74
|
else if (a.startsWith("--")) {
|
|
@@ -54,7 +79,24 @@ export async function run(args) {
|
|
|
54
79
|
}
|
|
55
80
|
}
|
|
56
81
|
if (showVersion) {
|
|
57
|
-
process.stdout.write("0.1
|
|
82
|
+
process.stdout.write("0.3.1\n");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (listSessions || searchSessionsQuery !== undefined) {
|
|
86
|
+
const store = new JsonlSessionStore();
|
|
87
|
+
if (listSessions) {
|
|
88
|
+
process.stdout.write(`${formatSessionList(store.list())}\n`);
|
|
89
|
+
}
|
|
90
|
+
if (searchSessionsQuery !== undefined) {
|
|
91
|
+
const query = searchSessionsQuery.trim();
|
|
92
|
+
if (!query) {
|
|
93
|
+
process.stderr.write("atom-lite: --search-sessions requires a query\n");
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const matches = searchSessions(store.list(), query);
|
|
98
|
+
process.stdout.write(`${formatSessionSearch(matches)}\n`);
|
|
99
|
+
}
|
|
58
100
|
return;
|
|
59
101
|
}
|
|
60
102
|
let config = loadConfig();
|
|
@@ -71,7 +113,9 @@ export async function run(args) {
|
|
|
71
113
|
if (modelOverride)
|
|
72
114
|
modelByProvider.set(provider.id, modelOverride);
|
|
73
115
|
const useTools = !positional.includes("__notools__");
|
|
74
|
-
const
|
|
116
|
+
const builtInTools = useTools
|
|
117
|
+
? [readTool, writeTool, editTool, bashTool, webfetchTool, websearchTool]
|
|
118
|
+
: [];
|
|
75
119
|
const interactive = process.stdin.isTTY === true && !jsonMode;
|
|
76
120
|
// one-shot task from positional or stdin
|
|
77
121
|
let stdinTask = "";
|
|
@@ -84,11 +128,31 @@ export async function run(args) {
|
|
|
84
128
|
const taskArgs = positional.filter((p) => !p.startsWith("__"));
|
|
85
129
|
const oneShot = (taskArgs.join(" ").trim() || stdinTask).trim();
|
|
86
130
|
const store = new JsonlSessionStore();
|
|
87
|
-
|
|
131
|
+
let session = sessionOverride
|
|
88
132
|
? store.resume(sessionOverride) ?? store.create(process.cwd(), provider.id)
|
|
89
133
|
: positional.includes("__continue__")
|
|
90
134
|
? store.resumeLatest(process.cwd()) ?? store.create(process.cwd(), provider.id)
|
|
91
135
|
: store.create(process.cwd(), provider.id);
|
|
136
|
+
const mcpServers = (config.mcpServers ?? {});
|
|
137
|
+
const mcpManager = new McpManager({
|
|
138
|
+
servers: mcpServers,
|
|
139
|
+
onEvent: (event) => {
|
|
140
|
+
try {
|
|
141
|
+
session?.appendEvent?.(event);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Observability must not break the session.
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
const extensionLoader = new ExtensionLoader({
|
|
149
|
+
userDir: join(process.env.HOME ?? "", ".config", "atom-lite", "extensions"),
|
|
150
|
+
projectDir: process.cwd(),
|
|
151
|
+
trustedProjects: config.extensions?.trustedProjects,
|
|
152
|
+
});
|
|
153
|
+
void extensionLoader.loadAll().catch(() => {
|
|
154
|
+
// Extension failures are reported per extension.
|
|
155
|
+
});
|
|
92
156
|
const contextManager = new ContextManager({
|
|
93
157
|
systemPrompt: "You are a coding assistant.",
|
|
94
158
|
maxInputTokens: config.context.maxInputTokens ?? null,
|
|
@@ -100,23 +164,10 @@ export async function run(args) {
|
|
|
100
164
|
userInstructionsPath: undefined,
|
|
101
165
|
});
|
|
102
166
|
let sessionTrusted = false;
|
|
103
|
-
let appendLineFn = () => { };
|
|
104
|
-
let pendingApproval = null;
|
|
105
167
|
const approvals = {
|
|
106
168
|
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,
|
|
169
|
+
// Installed below once the interactive UI helpers exist.
|
|
170
|
+
handler: undefined,
|
|
120
171
|
isTrusted: () => sessionTrusted,
|
|
121
172
|
trustSession: () => {
|
|
122
173
|
sessionTrusted = true;
|
|
@@ -127,17 +178,24 @@ export async function run(args) {
|
|
|
127
178
|
provider,
|
|
128
179
|
model: modelOverride,
|
|
129
180
|
workspace: process.cwd(),
|
|
130
|
-
tools,
|
|
181
|
+
tools: builtInTools,
|
|
131
182
|
approvals,
|
|
132
183
|
contextManager,
|
|
133
184
|
bashConcurrency: config.runtime.bashConcurrency,
|
|
134
185
|
});
|
|
186
|
+
mcpManager.onToolsChanged = () => {
|
|
187
|
+
runtime.setTools([...builtInTools, ...createMcpTools(mcpManager)]);
|
|
188
|
+
};
|
|
135
189
|
if (oneShot || !interactive) {
|
|
136
190
|
// non-interactive / one-shot path
|
|
137
191
|
const controller = new AbortController();
|
|
138
192
|
const onSigint = () => controller.abort();
|
|
139
193
|
process.on("SIGINT", onSigint);
|
|
140
194
|
try {
|
|
195
|
+
if (useTools) {
|
|
196
|
+
await mcpManager.syncTools();
|
|
197
|
+
runtime.setTools([...builtInTools, ...createMcpTools(mcpManager)]);
|
|
198
|
+
}
|
|
141
199
|
const result = await runtime.runAgentTurn(oneShot || " ", session, controller.signal);
|
|
142
200
|
if (jsonMode) {
|
|
143
201
|
process.stdout.write(JSON.stringify({ result, sessionId: session.id }) + "\n");
|
|
@@ -154,6 +212,7 @@ export async function run(args) {
|
|
|
154
212
|
process.exit(1);
|
|
155
213
|
}
|
|
156
214
|
finally {
|
|
215
|
+
await mcpManager.close().catch(() => { });
|
|
157
216
|
process.off("SIGINT", onSigint);
|
|
158
217
|
}
|
|
159
218
|
return;
|
|
@@ -162,14 +221,30 @@ export async function run(args) {
|
|
|
162
221
|
const { TuiMainScreen, Text, Input, Editor, Container, Spacer, ScrollView, Loader, Markdown, Box } = await import("@earendil-works/pi-tui");
|
|
163
222
|
const terminal = new ProcessTerminal();
|
|
164
223
|
const tui = new TuiMainScreen(terminal);
|
|
224
|
+
const mouseEnabled = shouldEnableMouse({
|
|
225
|
+
interactive,
|
|
226
|
+
configured: config.ui.mouse,
|
|
227
|
+
stdoutIsTTY: process.stdout.isTTY === true,
|
|
228
|
+
});
|
|
229
|
+
const mouseCapture = createMouseCapture((data) => terminal.write(data));
|
|
165
230
|
let effortLevel = "medium";
|
|
166
231
|
runtime.setEffort(effortLevel);
|
|
167
|
-
|
|
168
|
-
|
|
232
|
+
let themeName = themeOverride ?? config.ui.theme;
|
|
233
|
+
if (noColor)
|
|
234
|
+
themeName = "ansi";
|
|
235
|
+
let terminalScheme;
|
|
236
|
+
let theme = resolveTheme(themeName, {
|
|
237
|
+
noColor,
|
|
238
|
+
env: process.env,
|
|
239
|
+
scheme: terminalScheme,
|
|
240
|
+
});
|
|
241
|
+
const transcript = new Transcript(theme);
|
|
242
|
+
const banner = new Banner(theme);
|
|
243
|
+
transcript.add(banner);
|
|
169
244
|
transcript.add(new Spacer(1));
|
|
170
245
|
const content = transcript.container;
|
|
171
246
|
const output = new ScrollView(content, { follow: "end" });
|
|
172
|
-
const streamText = new StreamText(transcript);
|
|
247
|
+
const streamText = new StreamText(transcript, 200, 200, markdownThemeFor(theme));
|
|
173
248
|
function headerLabel() {
|
|
174
249
|
const cwd = process.cwd();
|
|
175
250
|
const home = process.env.HOME ?? "";
|
|
@@ -188,23 +263,27 @@ export async function run(args) {
|
|
|
188
263
|
}
|
|
189
264
|
return branch ? `${displayPath}:${branch}` : displayPath;
|
|
190
265
|
}
|
|
191
|
-
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
192
266
|
// 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();
|
|
267
|
+
const header = new Text(theme.tokens.dim(headerLabel()), 1, 0);
|
|
268
|
+
const statusBar = new StatusLine(theme);
|
|
269
|
+
const activity = new Activity({ theme, onChange: () => updateStatus() });
|
|
195
270
|
statusBar.setParts({
|
|
196
|
-
left:
|
|
197
|
-
tokens:
|
|
198
|
-
effort:
|
|
271
|
+
left: `${provider.id}/${provider.model ?? "default"}`,
|
|
272
|
+
tokens: "tokens: ?",
|
|
273
|
+
effort: "effort: medium",
|
|
274
|
+
activity: activity.text(),
|
|
199
275
|
});
|
|
200
276
|
const selectListTheme = {
|
|
201
|
-
selectedPrefix: (s) => s,
|
|
202
|
-
selectedText: (s) =>
|
|
203
|
-
description: (s) => s,
|
|
204
|
-
scrollInfo: (s) => s,
|
|
205
|
-
noMatch: (s) => s,
|
|
277
|
+
selectedPrefix: (s) => theme.tokens.accent(s),
|
|
278
|
+
selectedText: (s) => theme.tokens.selection(s),
|
|
279
|
+
description: (s) => theme.tokens.dim(s),
|
|
280
|
+
scrollInfo: (s) => theme.tokens.dim(s),
|
|
281
|
+
noMatch: (s) => theme.tokens.dim(s),
|
|
282
|
+
};
|
|
283
|
+
const editorTheme = {
|
|
284
|
+
borderColor: (s) => theme.tokens.border(s),
|
|
285
|
+
selectList: selectListTheme,
|
|
206
286
|
};
|
|
207
|
-
const editorTheme = { borderColor: (s) => s, selectList: selectListTheme };
|
|
208
287
|
const input = new Editor(tui, editorTheme, {});
|
|
209
288
|
const { CombinedAutocompleteProvider } = await import("@earendil-works/pi-tui");
|
|
210
289
|
const { DefaultSkillRegistry, skillCacheSignature } = await import("../skills/registry.js");
|
|
@@ -218,23 +297,17 @@ export async function run(args) {
|
|
|
218
297
|
}
|
|
219
298
|
return skillReg;
|
|
220
299
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
{ name: "effort", description: "set effort level" },
|
|
231
|
-
{ name: "skills", description: "list/select skills" },
|
|
232
|
-
{ name: "exit", description: "exit" },
|
|
233
|
-
];
|
|
234
|
-
for (const s of skillReg.list()) {
|
|
235
|
-
slashCommands.push({ name: s.name, description: s.description });
|
|
300
|
+
let commandRegistry = createCommandRegistry({ skills: skillReg.list() });
|
|
301
|
+
let registrySkillSignature = skillSignature;
|
|
302
|
+
function refreshCommandRegistry() {
|
|
303
|
+
const sig = skillCacheSignature(process.cwd());
|
|
304
|
+
if (sig === registrySkillSignature)
|
|
305
|
+
return;
|
|
306
|
+
commandRegistry = createCommandRegistry({ skills: cachedSkillRegistry().list() });
|
|
307
|
+
registrySkillSignature = sig;
|
|
308
|
+
input.setAutocompleteProvider(new CombinedAutocompleteProvider(commandRegistry.autocomplete(), process.cwd()));
|
|
236
309
|
}
|
|
237
|
-
input.setAutocompleteProvider(new CombinedAutocompleteProvider(
|
|
310
|
+
input.setAutocompleteProvider(new CombinedAutocompleteProvider(commandRegistry.autocomplete(), process.cwd()));
|
|
238
311
|
tui.addChild(output);
|
|
239
312
|
tui.addChild(new Spacer());
|
|
240
313
|
tui.addChild(input);
|
|
@@ -244,54 +317,121 @@ export async function run(args) {
|
|
|
244
317
|
const dropdownContainer = new Container();
|
|
245
318
|
tui.addChild(dropdownContainer);
|
|
246
319
|
tui.setFocus(input);
|
|
320
|
+
if (!noColor) {
|
|
321
|
+
try {
|
|
322
|
+
tui.setTerminalColorSchemeNotifications(true);
|
|
323
|
+
tui.onTerminalColorSchemeChange((scheme) => {
|
|
324
|
+
terminalScheme = scheme;
|
|
325
|
+
if (themeName === "auto")
|
|
326
|
+
applyTheme("auto", false);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
// Terminals that do not support colour-scheme reports keep the
|
|
331
|
+
// COLORFGBG/dark fallback.
|
|
332
|
+
}
|
|
333
|
+
}
|
|
247
334
|
let thinkingText = null;
|
|
248
335
|
let thinkingBuffer = "";
|
|
249
|
-
let showThinking =
|
|
250
|
-
let loader = null;
|
|
336
|
+
let showThinking = config.ui.showThinking;
|
|
251
337
|
let expandTools = false;
|
|
252
338
|
let uiModalOpen = false;
|
|
339
|
+
let closeActiveOverlay;
|
|
253
340
|
const toolBoxes = [];
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
: undefined;
|
|
341
|
+
/**
|
|
342
|
+
* Close whichever modal overlay owns the keyboard. Callers that clear the
|
|
343
|
+
* dropdown container directly must use this first, or the overlay's input
|
|
344
|
+
* listener would keep consuming keys after it disappears from the screen.
|
|
345
|
+
*/
|
|
346
|
+
function closeOverlay() {
|
|
347
|
+
const close = closeActiveOverlay;
|
|
348
|
+
closeActiveOverlay = undefined;
|
|
349
|
+
close?.();
|
|
350
|
+
}
|
|
267
351
|
function appendLine(text) {
|
|
268
352
|
if (thinkingText) {
|
|
269
353
|
thinkingText = null;
|
|
270
354
|
thinkingBuffer = "";
|
|
271
355
|
}
|
|
272
356
|
streamText.finalize();
|
|
273
|
-
transcript.
|
|
357
|
+
transcript.addLine(text);
|
|
274
358
|
tui.requestRender();
|
|
275
359
|
}
|
|
276
360
|
function appendDelta(delta) {
|
|
277
361
|
streamText.append(delta);
|
|
278
362
|
tui.requestRender();
|
|
279
363
|
}
|
|
364
|
+
function thinkingBody() {
|
|
365
|
+
return thinkingBuffer
|
|
366
|
+
.split("\n")
|
|
367
|
+
.map((line) => theme.tokens.dim(`| ${line}`))
|
|
368
|
+
.join("\n");
|
|
369
|
+
}
|
|
280
370
|
function finalizeText() {
|
|
281
371
|
streamText.finalize();
|
|
282
372
|
}
|
|
373
|
+
let activeTurnController = null;
|
|
374
|
+
/** Run one interactive turn while keeping the status bar's activity fresh. */
|
|
375
|
+
async function runTurn(prompt) {
|
|
376
|
+
activity.start("formulating");
|
|
377
|
+
const controller = new AbortController();
|
|
378
|
+
activeTurnController = controller;
|
|
379
|
+
try {
|
|
380
|
+
if (useTools) {
|
|
381
|
+
await mcpManager.syncTools();
|
|
382
|
+
runtime.setTools([...builtInTools, ...createMcpTools(mcpManager)]);
|
|
383
|
+
}
|
|
384
|
+
await runtime.runAgentTurn(prompt, session, controller.signal);
|
|
385
|
+
}
|
|
386
|
+
catch (err) {
|
|
387
|
+
if (err instanceof Error && err.message === "USER_CANCELLED") {
|
|
388
|
+
appendLine("cancelled");
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
appendLine(`atom-lite: ${err instanceof Error ? err.message : String(err)}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
finally {
|
|
395
|
+
activeTurnController = null;
|
|
396
|
+
activity.stop();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
283
399
|
function updateStatus() {
|
|
284
400
|
const usage = runtime.getContextUsage();
|
|
285
401
|
statusBar.setParts({
|
|
286
|
-
left:
|
|
287
|
-
tokens:
|
|
288
|
-
effort:
|
|
402
|
+
left: `${provider.id}/${currentModelLabel()}`,
|
|
403
|
+
tokens: `tokens: ${formatContextUsage(usage.used, usage.window)}`,
|
|
404
|
+
effort: `effort: ${effortLevel}`,
|
|
405
|
+
activity: activity.text(),
|
|
289
406
|
});
|
|
290
407
|
tui.requestRender();
|
|
291
408
|
}
|
|
292
409
|
function currentModelLabel() {
|
|
293
410
|
return modelOverride ?? provider.model ?? "default";
|
|
294
411
|
}
|
|
412
|
+
function applyTheme(name, persist) {
|
|
413
|
+
themeName = name;
|
|
414
|
+
theme = resolveTheme(name, {
|
|
415
|
+
noColor,
|
|
416
|
+
env: process.env,
|
|
417
|
+
scheme: terminalScheme,
|
|
418
|
+
});
|
|
419
|
+
banner.setTheme(theme);
|
|
420
|
+
transcript.setTheme(theme);
|
|
421
|
+
statusBar.setTheme(theme);
|
|
422
|
+
activity.setTheme(theme);
|
|
423
|
+
header.setText(theme.tokens.dim(headerLabel()));
|
|
424
|
+
if (persist) {
|
|
425
|
+
try {
|
|
426
|
+
saveTheme(name);
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
appendLine(`warning: could not persist theme: ${err instanceof Error ? err.message : String(err)}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
updateStatus();
|
|
433
|
+
tui.requestRender();
|
|
434
|
+
}
|
|
295
435
|
/**
|
|
296
436
|
* Show the best-known window immediately, then refine it from live provider
|
|
297
437
|
* metadata without blocking the TUI.
|
|
@@ -420,6 +560,13 @@ export async function run(args) {
|
|
|
420
560
|
});
|
|
421
561
|
}
|
|
422
562
|
}
|
|
563
|
+
/** Providers whose model list is missing or older than the cache TTL. */
|
|
564
|
+
function providersNeedingRefresh(names) {
|
|
565
|
+
return names.filter((name) => {
|
|
566
|
+
const cached = modelCache.get(name);
|
|
567
|
+
return !cached || Date.now() - cached.at >= MODEL_CACHE_TTL_MS;
|
|
568
|
+
});
|
|
569
|
+
}
|
|
423
570
|
function switchToModel(profile, model) {
|
|
424
571
|
const pcfg = ensureProfile(profile);
|
|
425
572
|
if (!pcfg) {
|
|
@@ -441,6 +588,35 @@ export async function run(args) {
|
|
|
441
588
|
refreshContextWindow();
|
|
442
589
|
appendLine(`switched to ${profile}/${model}`);
|
|
443
590
|
}
|
|
591
|
+
/** Interpret `/model` arguments: bare model, provider/model, or provider. */
|
|
592
|
+
function switchModelTarget(target) {
|
|
593
|
+
const rest = target.trim();
|
|
594
|
+
if (!rest) {
|
|
595
|
+
openModelPicker();
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const knownModels = modelsFor(provider.id);
|
|
599
|
+
if (knownModels.includes(rest)) {
|
|
600
|
+
switchToModel(provider.id, rest);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const slash = rest.indexOf("/");
|
|
604
|
+
if (slash > 0) {
|
|
605
|
+
const profile = rest.slice(0, slash);
|
|
606
|
+
const model = rest.slice(slash + 1);
|
|
607
|
+
if (profileConfig(profile) || getPreset(profile)) {
|
|
608
|
+
switchToModel(profile, model);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (profileConfig(rest) || getPreset(rest)) {
|
|
613
|
+
ensureProfile(rest);
|
|
614
|
+
const pcfg = profileConfig(rest);
|
|
615
|
+
switchToModel(rest, String(pcfg?.model ?? modelOverride ?? "default"));
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
switchToModel(provider.id, rest);
|
|
619
|
+
}
|
|
444
620
|
function openModelPicker() {
|
|
445
621
|
const names = availableProviderNames(config);
|
|
446
622
|
if (!names.includes(provider.id) && profileConfig(provider.id)) {
|
|
@@ -451,6 +627,7 @@ export async function run(args) {
|
|
|
451
627
|
tui,
|
|
452
628
|
container: dropdownContainer,
|
|
453
629
|
title: "Select model",
|
|
630
|
+
theme,
|
|
454
631
|
items: buildModelItems(),
|
|
455
632
|
maxVisible: 15,
|
|
456
633
|
onSelect: (item) => {
|
|
@@ -466,9 +643,11 @@ export async function run(args) {
|
|
|
466
643
|
},
|
|
467
644
|
restoreFocus: () => tui.setFocus(input),
|
|
468
645
|
});
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
646
|
+
closeActiveOverlay = picker.close;
|
|
647
|
+
const stale = providersNeedingRefresh(names);
|
|
648
|
+
if (stale.length) {
|
|
649
|
+
picker.setStatus(`loading live models from ${stale.length} provider${stale.length === 1 ? "" : "s"}…`);
|
|
650
|
+
refreshModelCache(stale, () => {
|
|
472
651
|
picker.setStatus("");
|
|
473
652
|
picker.updateItems(buildModelItems());
|
|
474
653
|
});
|
|
@@ -521,6 +700,7 @@ export async function run(args) {
|
|
|
521
700
|
appendLine("tip: /model searches every configured provider");
|
|
522
701
|
}
|
|
523
702
|
function closeApiKeyPrompt() {
|
|
703
|
+
closeOverlay();
|
|
524
704
|
dropdownContainer.clear();
|
|
525
705
|
uiModalOpen = false;
|
|
526
706
|
tui.setFocus(input);
|
|
@@ -528,13 +708,14 @@ export async function run(args) {
|
|
|
528
708
|
}
|
|
529
709
|
function promptForApiKey(preset, existingKey) {
|
|
530
710
|
uiModalOpen = true;
|
|
711
|
+
closeOverlay();
|
|
531
712
|
dropdownContainer.clear();
|
|
532
713
|
const wrapper = new Container();
|
|
533
|
-
wrapper.addChild(new Text(
|
|
714
|
+
wrapper.addChild(new Text(theme.tokens.accent(`${preset.label} API key`)));
|
|
534
715
|
if (preset.keyUrl)
|
|
535
|
-
wrapper.addChild(new Text(dim(`create one: ${preset.keyUrl}`)));
|
|
716
|
+
wrapper.addChild(new Text(theme.tokens.dim(`create one: ${preset.keyUrl}`)));
|
|
536
717
|
if (existingKey)
|
|
537
|
-
wrapper.addChild(new Text(dim("a key is already saved — press Enter to keep it")));
|
|
718
|
+
wrapper.addChild(new Text(theme.tokens.dim("a key is already saved — press Enter to keep it")));
|
|
538
719
|
const secret = new SecretInput("API key", existingKey ? "hidden (Enter keeps saved key)" : "paste key (hidden)");
|
|
539
720
|
const status = new Text("");
|
|
540
721
|
wrapper.addChild(secret);
|
|
@@ -564,18 +745,18 @@ export async function run(args) {
|
|
|
564
745
|
}
|
|
565
746
|
return;
|
|
566
747
|
}
|
|
567
|
-
status.setText(dim("an API key is required"));
|
|
748
|
+
status.setText(theme.tokens.dim("an API key is required"));
|
|
568
749
|
tui.requestRender();
|
|
569
750
|
return;
|
|
570
751
|
}
|
|
571
752
|
busy = true;
|
|
572
|
-
status.setText(dim("validating key…"));
|
|
753
|
+
status.setText(theme.tokens.dim("validating key…"));
|
|
573
754
|
tui.requestRender();
|
|
574
755
|
await new Promise((r) => setImmediate(r));
|
|
575
756
|
const probe = await probeApiKey(preset, key);
|
|
576
757
|
if (probe.invalid) {
|
|
577
758
|
busy = false;
|
|
578
|
-
status.setText(
|
|
759
|
+
status.setText(theme.tokens.error(`✗ key rejected by ${preset.label}`));
|
|
579
760
|
secret.reset();
|
|
580
761
|
tui.requestRender();
|
|
581
762
|
return;
|
|
@@ -585,7 +766,7 @@ export async function run(args) {
|
|
|
585
766
|
}
|
|
586
767
|
catch (err) {
|
|
587
768
|
busy = false;
|
|
588
|
-
status.setText(
|
|
769
|
+
status.setText(theme.tokens.error(`✗ could not save key: ${err instanceof Error ? err.message : String(err)}`));
|
|
589
770
|
tui.requestRender();
|
|
590
771
|
return;
|
|
591
772
|
}
|
|
@@ -621,10 +802,11 @@ export async function run(args) {
|
|
|
621
802
|
}
|
|
622
803
|
function openProviderPicker() {
|
|
623
804
|
uiModalOpen = true;
|
|
624
|
-
openSearchablePicker({
|
|
805
|
+
const picker = openSearchablePicker({
|
|
625
806
|
tui,
|
|
626
807
|
container: dropdownContainer,
|
|
627
808
|
title: "Select provider",
|
|
809
|
+
theme,
|
|
628
810
|
items: providerItems(),
|
|
629
811
|
maxVisible: 12,
|
|
630
812
|
onSelect: (item) => {
|
|
@@ -637,69 +819,435 @@ export async function run(args) {
|
|
|
637
819
|
},
|
|
638
820
|
restoreFocus: () => tui.setFocus(input),
|
|
639
821
|
});
|
|
822
|
+
closeActiveOverlay = picker.close;
|
|
823
|
+
}
|
|
824
|
+
function openEffortPicker() {
|
|
825
|
+
uiModalOpen = true;
|
|
826
|
+
const items = [
|
|
827
|
+
{ value: "low", label: "low", description: "minimal reasoning" },
|
|
828
|
+
{ value: "medium", label: "medium", description: "balanced" },
|
|
829
|
+
{ value: "high", label: "high", description: "deep reasoning" },
|
|
830
|
+
];
|
|
831
|
+
const picker = openSearchablePicker({
|
|
832
|
+
tui,
|
|
833
|
+
container: dropdownContainer,
|
|
834
|
+
title: "Select effort",
|
|
835
|
+
theme,
|
|
836
|
+
items,
|
|
837
|
+
maxVisible: 3,
|
|
838
|
+
onSelect: (item) => {
|
|
839
|
+
uiModalOpen = false;
|
|
840
|
+
setEffortTarget(item.value);
|
|
841
|
+
},
|
|
842
|
+
onCancel: () => {
|
|
843
|
+
uiModalOpen = false;
|
|
844
|
+
appendLine("cancelled");
|
|
845
|
+
},
|
|
846
|
+
restoreFocus: () => tui.setFocus(input),
|
|
847
|
+
});
|
|
848
|
+
closeActiveOverlay = picker.close;
|
|
849
|
+
picker.setStatus("applies to the next turn");
|
|
850
|
+
}
|
|
851
|
+
function setEffortTarget(level) {
|
|
852
|
+
effortLevel = level;
|
|
853
|
+
runtime.setEffort(level);
|
|
854
|
+
updateStatus();
|
|
855
|
+
appendLine(`effort set to ${effortLevel}`);
|
|
856
|
+
}
|
|
857
|
+
function themePickerItems() {
|
|
858
|
+
return themeNames().map((name) => ({
|
|
859
|
+
value: name,
|
|
860
|
+
label: name,
|
|
861
|
+
description: themeLabel(name),
|
|
862
|
+
}));
|
|
863
|
+
}
|
|
864
|
+
function setThemeTarget(name) {
|
|
865
|
+
if (!themeNames().includes(name)) {
|
|
866
|
+
appendLine(`unknown theme: ${name} (available: ${themeNames().join(", ")})`);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
applyTheme(name, true);
|
|
870
|
+
appendLine(`theme: ${themeLabel(name)}`);
|
|
871
|
+
}
|
|
872
|
+
function openThemePicker() {
|
|
873
|
+
uiModalOpen = true;
|
|
874
|
+
const original = themeName;
|
|
875
|
+
const picker = openSearchablePicker({
|
|
876
|
+
tui,
|
|
877
|
+
container: dropdownContainer,
|
|
878
|
+
title: "Select theme",
|
|
879
|
+
theme,
|
|
880
|
+
items: themePickerItems(),
|
|
881
|
+
maxVisible: 4,
|
|
882
|
+
initialValue: themeName,
|
|
883
|
+
onPreview: (item) => applyTheme(item.value, false),
|
|
884
|
+
onSelect: (item) => {
|
|
885
|
+
uiModalOpen = false;
|
|
886
|
+
setThemeTarget(item.value);
|
|
887
|
+
},
|
|
888
|
+
onCancel: () => {
|
|
889
|
+
uiModalOpen = false;
|
|
890
|
+
applyTheme(original, false);
|
|
891
|
+
appendLine("cancelled");
|
|
892
|
+
},
|
|
893
|
+
restoreFocus: () => tui.setFocus(input),
|
|
894
|
+
});
|
|
895
|
+
closeActiveOverlay = picker.close;
|
|
896
|
+
}
|
|
897
|
+
async function loadSkill(name, extra) {
|
|
898
|
+
const registry = cachedSkillRegistry();
|
|
899
|
+
const content = registry.load(name);
|
|
900
|
+
if (!content) {
|
|
901
|
+
appendLine(`skill not found: ${name}`);
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
session.appendMessage({ role: "system", content });
|
|
905
|
+
if (typeof session.appendEvent === "function") {
|
|
906
|
+
session.appendEvent({ type: "skill_loaded", name });
|
|
907
|
+
}
|
|
908
|
+
appendLine(`Loaded skill: ${name}`);
|
|
909
|
+
if (extra) {
|
|
910
|
+
appendLine(`> ${extra}`);
|
|
911
|
+
await runTurn(extra);
|
|
912
|
+
}
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
915
|
+
const commandContext = {
|
|
916
|
+
get session() {
|
|
917
|
+
return session;
|
|
918
|
+
},
|
|
919
|
+
runtime,
|
|
920
|
+
approvals,
|
|
921
|
+
appendLine,
|
|
922
|
+
clearContent: () => {
|
|
923
|
+
session.messages.length = 0;
|
|
924
|
+
content.clear();
|
|
925
|
+
tui.requestRender();
|
|
926
|
+
},
|
|
927
|
+
updateStatus,
|
|
928
|
+
runDoctor,
|
|
929
|
+
runMcp,
|
|
930
|
+
runExport,
|
|
931
|
+
openModelPicker,
|
|
932
|
+
openProviderPicker,
|
|
933
|
+
openEffortPicker,
|
|
934
|
+
openThemePicker,
|
|
935
|
+
openSessionsPicker,
|
|
936
|
+
searchSessions: searchSessionsInStore,
|
|
937
|
+
renameSession,
|
|
938
|
+
listExtensions: () => extensionLoader
|
|
939
|
+
.list()
|
|
940
|
+
.map((ext) => `${ext.id}\t${ext.source}\t${ext.status}${ext.error ? `\t${ext.error}` : ""}`)
|
|
941
|
+
.join("\n") || "No extensions loaded.",
|
|
942
|
+
switchModel: switchModelTarget,
|
|
943
|
+
startProviderSetup,
|
|
944
|
+
setEffort: setEffortTarget,
|
|
945
|
+
toggleThinking: () => {
|
|
946
|
+
showThinking = !showThinking;
|
|
947
|
+
return showThinking;
|
|
948
|
+
},
|
|
949
|
+
setTheme: setThemeTarget,
|
|
950
|
+
listSkills: () => cachedSkillRegistry().list().map((skill) => ({
|
|
951
|
+
name: skill.name,
|
|
952
|
+
description: skill.description,
|
|
953
|
+
})),
|
|
954
|
+
loadSkill,
|
|
955
|
+
runTurn,
|
|
956
|
+
requestExit: () => {
|
|
957
|
+
void mcpManager.close().finally(() => {
|
|
958
|
+
tui.stop();
|
|
959
|
+
process.exit(0);
|
|
960
|
+
});
|
|
961
|
+
},
|
|
962
|
+
};
|
|
963
|
+
function restoreTranscript(next) {
|
|
964
|
+
streamText.finalize();
|
|
965
|
+
transcript.reset();
|
|
966
|
+
banner.setTheme(theme);
|
|
967
|
+
transcript.add(banner);
|
|
968
|
+
transcript.add(new Spacer(1));
|
|
969
|
+
for (const line of sessionHistoryLines(next.messages)) {
|
|
970
|
+
transcript.addLine(line);
|
|
971
|
+
}
|
|
972
|
+
tui.requestRender();
|
|
973
|
+
}
|
|
974
|
+
function resumeSession(id) {
|
|
975
|
+
const next = store.resume(id);
|
|
976
|
+
if (!next) {
|
|
977
|
+
appendLine(`could not resume session: ${id}`);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
const previous = session.id;
|
|
981
|
+
session = next;
|
|
982
|
+
session.appendEvent({ type: "session_resumed", from: previous });
|
|
983
|
+
restoreTranscript(next);
|
|
984
|
+
const title = sessionTitleFromEvents(next.events);
|
|
985
|
+
appendLine(`resumed session ${session.id}: ${title} (${session.messages.length} messages)`);
|
|
986
|
+
const notice = providerSwitchNotice({ provider: provider.id, model: currentModelLabel() }, sessionSelectionFromEvents(next.events));
|
|
987
|
+
if (notice)
|
|
988
|
+
appendLine(notice);
|
|
989
|
+
updateStatus();
|
|
990
|
+
tui.requestRender();
|
|
991
|
+
}
|
|
992
|
+
function renameSession(title) {
|
|
993
|
+
const trimmed = title.trim();
|
|
994
|
+
if (!trimmed) {
|
|
995
|
+
appendLine("usage: /session rename <title>");
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
session.appendEvent({ type: "session_renamed", title: trimmed });
|
|
999
|
+
appendLine(`session renamed: ${trimmed}`);
|
|
1000
|
+
tui.requestRender();
|
|
1001
|
+
}
|
|
1002
|
+
function searchSessionsInStore(query) {
|
|
1003
|
+
const matches = searchSessions(store.list(), query);
|
|
1004
|
+
if (matches.length === 0) {
|
|
1005
|
+
appendLine(`no sessions match “${query}”`);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
appendLine(formatSessionSearch(matches));
|
|
1009
|
+
}
|
|
1010
|
+
function openSessionsPicker(initialScope) {
|
|
1011
|
+
uiModalOpen = true;
|
|
1012
|
+
let scope = initialScope;
|
|
1013
|
+
const buildItems = () => {
|
|
1014
|
+
const summaries = scope === "all" ? store.list() : store.list(process.cwd());
|
|
1015
|
+
const toggle = {
|
|
1016
|
+
value: "__scope__",
|
|
1017
|
+
label: scope === "all"
|
|
1018
|
+
? "Show this workspace only"
|
|
1019
|
+
: "Show all workspaces",
|
|
1020
|
+
description: `scope: ${scope === "all" ? "all workspaces" : process.cwd()}`,
|
|
1021
|
+
};
|
|
1022
|
+
return [
|
|
1023
|
+
toggle,
|
|
1024
|
+
...summaries.map((summary) => ({
|
|
1025
|
+
value: `session:${summary.id}`,
|
|
1026
|
+
label: summary.title ?? summary.id,
|
|
1027
|
+
description: `${summary.updatedAt ?? "unknown"} · ${summary.provider} · ${summary.messageCount ?? 0} messages · ${summary.workspace}`,
|
|
1028
|
+
})),
|
|
1029
|
+
];
|
|
1030
|
+
};
|
|
1031
|
+
let picker;
|
|
1032
|
+
picker = openSearchablePicker({
|
|
1033
|
+
tui,
|
|
1034
|
+
container: dropdownContainer,
|
|
1035
|
+
title: "Resume session",
|
|
1036
|
+
theme,
|
|
1037
|
+
items: buildItems(),
|
|
1038
|
+
closeOnSelect: false,
|
|
1039
|
+
onSelect: (item) => {
|
|
1040
|
+
if (item.value === "__scope__") {
|
|
1041
|
+
scope = scope === "all" ? "workspace" : "all";
|
|
1042
|
+
picker.updateItems(buildItems());
|
|
1043
|
+
picker.setStatus(scope === "all" ? "showing all workspaces" : "showing this workspace");
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
picker.close();
|
|
1047
|
+
uiModalOpen = false;
|
|
1048
|
+
resumeSession(item.value.slice("session:".length));
|
|
1049
|
+
},
|
|
1050
|
+
onCancel: () => {
|
|
1051
|
+
uiModalOpen = false;
|
|
1052
|
+
appendLine("cancelled");
|
|
1053
|
+
},
|
|
1054
|
+
restoreFocus: () => tui.setFocus(input),
|
|
1055
|
+
});
|
|
1056
|
+
closeActiveOverlay = picker.close;
|
|
1057
|
+
picker.setStatus(scope === "all" ? "showing all workspaces" : "showing this workspace");
|
|
1058
|
+
}
|
|
1059
|
+
function buildPaletteItems() {
|
|
1060
|
+
const items = [];
|
|
1061
|
+
for (const command of commandRegistry.list()) {
|
|
1062
|
+
const hint = command.argumentHint ? ` ${command.argumentHint}` : "";
|
|
1063
|
+
items.push({
|
|
1064
|
+
id: `command:${command.name}`,
|
|
1065
|
+
label: `/${command.name}${hint}`,
|
|
1066
|
+
description: command.description,
|
|
1067
|
+
category: command.category,
|
|
1068
|
+
keywords: command.aliases,
|
|
1069
|
+
action: async () => {
|
|
1070
|
+
await commandRegistry.dispatch(`/${command.name}`, commandContext);
|
|
1071
|
+
},
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
const profiles = new Set([
|
|
1075
|
+
...availableProviderNames(config),
|
|
1076
|
+
...Object.keys(config.providers),
|
|
1077
|
+
]);
|
|
1078
|
+
for (const profile of profiles) {
|
|
1079
|
+
const preset = getPreset(profile);
|
|
1080
|
+
items.push({
|
|
1081
|
+
id: `provider:${profile}`,
|
|
1082
|
+
label: providerLabel(profile),
|
|
1083
|
+
description: preset?.description ?? "configured provider",
|
|
1084
|
+
category: "Providers",
|
|
1085
|
+
action: () => {
|
|
1086
|
+
if (preset)
|
|
1087
|
+
startProviderSetup(profile);
|
|
1088
|
+
else
|
|
1089
|
+
switchModelTarget(`${profile}/${String(profileConfig(profile)?.model ?? "default")}`);
|
|
1090
|
+
},
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
for (const item of buildModelItems()) {
|
|
1094
|
+
const separator = item.value.indexOf("\u0000");
|
|
1095
|
+
const profile = item.value.slice(0, separator);
|
|
1096
|
+
const model = item.value.slice(separator + 1);
|
|
1097
|
+
items.push({
|
|
1098
|
+
id: `model:${item.value}`,
|
|
1099
|
+
label: item.label.replace(" (current)", ""),
|
|
1100
|
+
description: `${providerLabel(profile)}${item.label.includes("(current)") ? " · current" : ""}`,
|
|
1101
|
+
category: "Models",
|
|
1102
|
+
action: () => switchToModel(profile, model),
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
for (const name of themeNames()) {
|
|
1106
|
+
items.push({
|
|
1107
|
+
id: `theme:${name}`,
|
|
1108
|
+
label: name,
|
|
1109
|
+
description: themeLabel(name),
|
|
1110
|
+
category: "Themes",
|
|
1111
|
+
action: () => setThemeTarget(name),
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
for (const summary of store.list(process.cwd()).slice(0, 8)) {
|
|
1115
|
+
items.push({
|
|
1116
|
+
id: `session:${summary.id}`,
|
|
1117
|
+
label: summary.title ?? summary.id,
|
|
1118
|
+
description: `${summary.updatedAt ?? "unknown"} · ${summary.provider} · ${summary.messageCount ?? 0} messages`,
|
|
1119
|
+
category: "Sessions",
|
|
1120
|
+
keywords: [summary.provider, summary.workspace],
|
|
1121
|
+
action: () => resumeSession(summary.id),
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
return items;
|
|
1125
|
+
}
|
|
1126
|
+
function openPalette() {
|
|
1127
|
+
if (uiModalOpen)
|
|
1128
|
+
return;
|
|
1129
|
+
refreshCommandRegistry();
|
|
1130
|
+
uiModalOpen = true;
|
|
1131
|
+
const palette = openCommandPalette({
|
|
1132
|
+
tui,
|
|
1133
|
+
container: dropdownContainer,
|
|
1134
|
+
items: buildPaletteItems(),
|
|
1135
|
+
theme,
|
|
1136
|
+
title: "Command palette",
|
|
1137
|
+
onSelect: () => {
|
|
1138
|
+
uiModalOpen = false;
|
|
1139
|
+
},
|
|
1140
|
+
onCancel: () => {
|
|
1141
|
+
uiModalOpen = false;
|
|
1142
|
+
appendLine("cancelled");
|
|
1143
|
+
},
|
|
1144
|
+
onError: (err) => {
|
|
1145
|
+
uiModalOpen = false;
|
|
1146
|
+
appendLine(`palette: ${err instanceof Error ? err.message : String(err)}`);
|
|
1147
|
+
},
|
|
1148
|
+
restoreFocus: () => tui.setFocus(input),
|
|
1149
|
+
});
|
|
1150
|
+
closeActiveOverlay = palette.close;
|
|
1151
|
+
// The palette lists models too; refresh stale caches in the background and
|
|
1152
|
+
// update the open list when live discovery finishes.
|
|
1153
|
+
const stale = providersNeedingRefresh(availableProviderNames(config));
|
|
1154
|
+
if (stale.length) {
|
|
1155
|
+
refreshModelCache(stale, () => {
|
|
1156
|
+
palette.updateItems(buildPaletteItems());
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
640
1159
|
}
|
|
641
|
-
appendLineFn = appendLine;
|
|
642
1160
|
function toggleTools() {
|
|
643
1161
|
expandTools = !expandTools;
|
|
644
1162
|
for (const box of toolBoxes) {
|
|
645
|
-
|
|
646
|
-
continue; // a running command keeps its live view
|
|
647
|
-
box.output.clear();
|
|
648
|
-
if (expandTools) {
|
|
649
|
-
if (box.lines.length > 0) {
|
|
650
|
-
box.output.addChild(new Text(box.lines.join("\n")));
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
else {
|
|
654
|
-
box.output.addChild(new Text(` (${box.lines.length} lines, ctrl+o to expand)`));
|
|
655
|
-
}
|
|
1163
|
+
box.setExpanded(expandTools);
|
|
656
1164
|
}
|
|
657
1165
|
tui.requestRender();
|
|
658
1166
|
}
|
|
1167
|
+
function toggleToolBoxAtScreenRow(screenY) {
|
|
1168
|
+
const width = terminal.columns;
|
|
1169
|
+
const outputHeight = Math.max(1, output.viewportHeight || output.render(width).length);
|
|
1170
|
+
if (screenY < 0 || screenY >= outputHeight)
|
|
1171
|
+
return false;
|
|
1172
|
+
const children = content.children;
|
|
1173
|
+
const contentBlocks = children.map((child, index) => ({
|
|
1174
|
+
id: `content:${index}`,
|
|
1175
|
+
height: child.render(width).length,
|
|
1176
|
+
}));
|
|
1177
|
+
const total = contentBlocks.reduce((sum, block) => sum + block.height, 0);
|
|
1178
|
+
const viewportTop = Math.max(0, total - outputHeight);
|
|
1179
|
+
const inner = blockAtLine(contentBlocks, viewportTop + screenY);
|
|
1180
|
+
if (!inner)
|
|
1181
|
+
return false;
|
|
1182
|
+
const child = children[Number(inner.id.slice("content:".length))];
|
|
1183
|
+
if (!(child instanceof ToolBox))
|
|
1184
|
+
return false;
|
|
1185
|
+
child.toggleExpanded();
|
|
1186
|
+
return true;
|
|
1187
|
+
}
|
|
1188
|
+
tui.addInputListener((data) => {
|
|
1189
|
+
const event = parseSgrMouse(data);
|
|
1190
|
+
if (!event)
|
|
1191
|
+
return undefined;
|
|
1192
|
+
if (!mouseEnabled)
|
|
1193
|
+
return { consume: true };
|
|
1194
|
+
handleMouseEvent(event, {
|
|
1195
|
+
isModalOpen: () => uiModalOpen,
|
|
1196
|
+
scrollBy: (lines) => output.scrollBy(lines),
|
|
1197
|
+
focusInput: () => tui.setFocus(input),
|
|
1198
|
+
toggleToolBox: (screenY) => toggleToolBoxAtScreenRow(screenY),
|
|
1199
|
+
requestRender: () => tui.requestRender(),
|
|
1200
|
+
});
|
|
1201
|
+
return { consume: true };
|
|
1202
|
+
});
|
|
659
1203
|
tui.addInputListener((data) => {
|
|
1204
|
+
// Releases and repeats must not toggle modal shortcuts twice.
|
|
1205
|
+
if (isKeyRelease(data) || isKeyRepeat(data))
|
|
1206
|
+
return undefined;
|
|
1207
|
+
// Ctrl+P opens the command palette unless a modal owns the keyboard.
|
|
1208
|
+
if (matchesKey(data, Key.ctrl("p")) && !uiModalOpen) {
|
|
1209
|
+
openPalette();
|
|
1210
|
+
return { consume: true };
|
|
1211
|
+
}
|
|
660
1212
|
// Ctrl+O toggles tool expansion (unless a modal is capturing input).
|
|
661
1213
|
if (matchesKey(data, Key.ctrl("o")) && !uiModalOpen) {
|
|
662
1214
|
toggleTools();
|
|
663
1215
|
return { consume: true };
|
|
664
1216
|
}
|
|
1217
|
+
// Escape cancels the active turn.
|
|
1218
|
+
if (matchesKey(data, Key.escape) && !uiModalOpen && activeTurnController) {
|
|
1219
|
+
activeTurnController.abort();
|
|
1220
|
+
return { consume: true };
|
|
1221
|
+
}
|
|
665
1222
|
return undefined;
|
|
666
1223
|
});
|
|
667
1224
|
runtime["onEvent"] = (e) => {
|
|
668
1225
|
if (e.type === "thinking_delta") {
|
|
1226
|
+
activity.setPhase("thinking");
|
|
669
1227
|
if (!showThinking)
|
|
670
1228
|
return;
|
|
671
1229
|
if (!thinkingText) {
|
|
672
|
-
thinkingText = new Text("
|
|
1230
|
+
thinkingText = new Text(theme.tokens.dim("Thinking..."));
|
|
673
1231
|
transcript.add(thinkingText);
|
|
674
1232
|
thinkingBuffer = "";
|
|
675
1233
|
}
|
|
676
1234
|
thinkingBuffer += e.text;
|
|
677
|
-
|
|
678
|
-
.split("\n")
|
|
679
|
-
.map((l) => `\x1b[2m| ${l}\x1b[0m`)
|
|
680
|
-
.join("\n");
|
|
681
|
-
thinkingText.setText(`\x1b[2mThinking...\x1b[0m\n${prefixed}`);
|
|
1235
|
+
thinkingText.setText(`${theme.tokens.dim("Thinking...")}\n${thinkingBody()}`);
|
|
682
1236
|
tui.requestRender();
|
|
683
1237
|
}
|
|
684
1238
|
else if (e.type === "assistant_delta") {
|
|
1239
|
+
activity.setPhase("working");
|
|
685
1240
|
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`);
|
|
1241
|
+
thinkingText.setText(`${theme.tokens.dim("Thinking...")}\n${thinkingBody()}\n${theme.tokens.dim("| end")}`);
|
|
691
1242
|
thinkingText = null;
|
|
692
1243
|
thinkingBuffer = "";
|
|
693
1244
|
}
|
|
694
1245
|
appendDelta(e.text);
|
|
695
1246
|
}
|
|
696
1247
|
else if (e.type === "assistant_text") {
|
|
1248
|
+
activity.setPhase("working");
|
|
697
1249
|
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`);
|
|
1250
|
+
thinkingText.setText(`${theme.tokens.dim("Thinking...")}\n${thinkingBody()}\n${theme.tokens.dim("| end")}`);
|
|
703
1251
|
thinkingText = null;
|
|
704
1252
|
thinkingBuffer = "";
|
|
705
1253
|
}
|
|
@@ -718,37 +1266,18 @@ export async function run(args) {
|
|
|
718
1266
|
}
|
|
719
1267
|
else if (e.type === "tool_call") {
|
|
720
1268
|
finalizeText();
|
|
721
|
-
|
|
722
|
-
const
|
|
723
|
-
box.
|
|
724
|
-
const output = new Container();
|
|
725
|
-
box.addChild(output);
|
|
1269
|
+
activity.setPhase("working", e.name);
|
|
1270
|
+
const box = new ToolBox({ id: e.id, name: e.name, args: e.args });
|
|
1271
|
+
box.setExpanded(expandTools);
|
|
726
1272
|
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
|
-
});
|
|
1273
|
+
toolBoxes.push(box);
|
|
736
1274
|
tui.requestRender();
|
|
737
1275
|
}
|
|
738
1276
|
else if (e.type === "tool_output") {
|
|
739
1277
|
const box = toolBoxes.find((b) => b.id === e.toolCallId);
|
|
740
1278
|
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);
|
|
1279
|
+
activity.setPhase("working", e.name);
|
|
1280
|
+
box.appendLive(e.text);
|
|
752
1281
|
tui.requestRender();
|
|
753
1282
|
}
|
|
754
1283
|
}
|
|
@@ -756,44 +1285,14 @@ export async function run(args) {
|
|
|
756
1285
|
const last = toolBoxes.find((b) => b.id === e.toolCallId) ??
|
|
757
1286
|
toolBoxes[toolBoxes.length - 1];
|
|
758
1287
|
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
|
-
}
|
|
1288
|
+
last.setResult(e.content, { isError: e.isError, diff: e.diff });
|
|
1289
|
+
activity.setPhase("working");
|
|
792
1290
|
tui.requestRender();
|
|
793
1291
|
}
|
|
794
1292
|
}
|
|
795
1293
|
else if (e.type === "final") {
|
|
796
1294
|
finalizeText();
|
|
1295
|
+
activity.stop();
|
|
797
1296
|
}
|
|
798
1297
|
};
|
|
799
1298
|
async function runDoctor() {
|
|
@@ -812,249 +1311,266 @@ export async function run(args) {
|
|
|
812
1311
|
catch (e) {
|
|
813
1312
|
lines.push(` provider generate: FAIL ${e instanceof Error ? e.message : String(e)}`);
|
|
814
1313
|
}
|
|
1314
|
+
lines.push(...formatMcpDoctor(mcpManager.status()));
|
|
815
1315
|
return lines.join("\n");
|
|
816
1316
|
}
|
|
817
|
-
|
|
818
|
-
const trimmed =
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
else
|
|
827
|
-
pendingApproval("deny");
|
|
828
|
-
return;
|
|
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}`);
|
|
842
|
-
return;
|
|
1317
|
+
async function runMcp(args) {
|
|
1318
|
+
const trimmed = (args ?? "").trim();
|
|
1319
|
+
if (trimmed === "reconnect" || trimmed.startsWith("reconnect ")) {
|
|
1320
|
+
const server = trimmed.slice("reconnect".length).trim();
|
|
1321
|
+
try {
|
|
1322
|
+
if (server)
|
|
1323
|
+
await mcpManager.reconnect(server);
|
|
1324
|
+
else
|
|
1325
|
+
await mcpManager.reconnect();
|
|
843
1326
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1327
|
+
catch (e) {
|
|
1328
|
+
appendLine(`mcp reconnect failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1329
|
+
return;
|
|
847
1330
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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
|
-
}
|
|
1331
|
+
if (useTools) {
|
|
1332
|
+
await mcpManager.syncTools();
|
|
1333
|
+
runtime.setTools([...builtInTools, ...createMcpTools(mcpManager)]);
|
|
861
1334
|
}
|
|
1335
|
+
appendLine(formatMcpStatus(mcpManager.status()));
|
|
862
1336
|
return;
|
|
863
1337
|
}
|
|
864
|
-
if (trimmed
|
|
865
|
-
|
|
866
|
-
process.exit(0);
|
|
867
|
-
return;
|
|
868
|
-
}
|
|
869
|
-
if (trimmed === "/help") {
|
|
870
|
-
appendLine(["/help", "/model", "/provider", "/session", "/compact", "/clear", "/approval", "/doctor", "/effort", "/thinking", "/exit"].join("\n"));
|
|
871
|
-
return;
|
|
872
|
-
}
|
|
873
|
-
if (trimmed === "/session") {
|
|
874
|
-
appendLine(`session: ${session.id}\nworkspace: ${session.workspace}\nprovider: ${session.provider}\nmessages: ${session.messages.length}`);
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
if (trimmed === "/approval") {
|
|
878
|
-
appendLine(`approval mode: ${approvals.mode}\ntrusted this session: ${approvals.isTrusted()}`);
|
|
879
|
-
return;
|
|
880
|
-
}
|
|
881
|
-
if (trimmed === "/doctor") {
|
|
882
|
-
const out = await runDoctor();
|
|
883
|
-
appendLine(out);
|
|
884
|
-
return;
|
|
885
|
-
}
|
|
886
|
-
if (trimmed === "/clear") {
|
|
887
|
-
session.messages.length = 0;
|
|
888
|
-
content.clear();
|
|
889
|
-
tui.requestRender();
|
|
890
|
-
appendLine("started new session");
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
|
-
if (trimmed === "/compact") {
|
|
894
|
-
appendLine("compaction is automatic");
|
|
1338
|
+
if (trimmed) {
|
|
1339
|
+
appendLine("usage: /mcp [reconnect [server]]");
|
|
895
1340
|
return;
|
|
896
1341
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1342
|
+
appendLine(formatMcpStatus(mcpManager.status()));
|
|
1343
|
+
}
|
|
1344
|
+
async function runExport(args) {
|
|
1345
|
+
const tokens = (args ?? "").trim().split(/\s+/).filter(Boolean);
|
|
1346
|
+
let format = "md";
|
|
1347
|
+
let force = false;
|
|
1348
|
+
let redact = true;
|
|
1349
|
+
const positional = [];
|
|
1350
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1351
|
+
const t = tokens[i];
|
|
1352
|
+
if (t === "--format") {
|
|
1353
|
+
format = tokens[++i] ?? format;
|
|
902
1354
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
switchToModel(provider.id, rest);
|
|
906
|
-
return;
|
|
1355
|
+
else if (t.startsWith("--format=")) {
|
|
1356
|
+
format = t.slice("--format=".length);
|
|
907
1357
|
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
const profile = rest.slice(0, slash);
|
|
911
|
-
const model = rest.slice(slash + 1);
|
|
912
|
-
if (profileConfig(profile) || getPreset(profile)) {
|
|
913
|
-
switchToModel(profile, model);
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
1358
|
+
else if (t === "--force") {
|
|
1359
|
+
force = true;
|
|
916
1360
|
}
|
|
917
|
-
if (
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1361
|
+
else if (t === "--no-redact") {
|
|
1362
|
+
redact = false;
|
|
1363
|
+
}
|
|
1364
|
+
else {
|
|
1365
|
+
positional.push(t);
|
|
922
1366
|
}
|
|
923
|
-
switchToModel(provider.id, rest);
|
|
924
|
-
return;
|
|
925
1367
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
openProviderPicker();
|
|
1368
|
+
const isJson = format === "json";
|
|
1369
|
+
const ext = isJson ? "json" : "md";
|
|
1370
|
+
const outPath = positional[0] || `${session.id}.${ext}`;
|
|
1371
|
+
if (existsSync(outPath) && !force) {
|
|
1372
|
+
appendLine(`export refused: ${outPath} already exists (use --force)`);
|
|
932
1373
|
return;
|
|
933
1374
|
}
|
|
934
|
-
if (
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1375
|
+
if (!redact) {
|
|
1376
|
+
appendLine("warning: redaction disabled; secrets may be written to disk");
|
|
1377
|
+
}
|
|
1378
|
+
const secretPatterns = [
|
|
1379
|
+
/sk-[A-Za-z0-9_-]{8,}/g,
|
|
1380
|
+
/ghp_[A-Za-z0-9]{20,}/g,
|
|
1381
|
+
/gho_[A-Za-z0-9]{20,}/g,
|
|
1382
|
+
/github_pat_[A-Za-z0-9_]{20,}/g,
|
|
1383
|
+
/AKIA[0-9A-Z]{16}/g,
|
|
1384
|
+
/xox[baprs]-[A-Za-z0-9-]{10,}/g,
|
|
1385
|
+
/Bearer\s+[A-Za-z0-9._-]+/g,
|
|
1386
|
+
];
|
|
1387
|
+
const redactText = (value) => {
|
|
1388
|
+
if (!redact)
|
|
1389
|
+
return value;
|
|
1390
|
+
let out = value;
|
|
1391
|
+
for (const pattern of secretPatterns) {
|
|
1392
|
+
out = out.replace(pattern, "[REDACTED]");
|
|
1393
|
+
}
|
|
1394
|
+
return out;
|
|
1395
|
+
};
|
|
1396
|
+
const messageText = (message) => {
|
|
1397
|
+
const content = message?.content;
|
|
1398
|
+
if (Array.isArray(content)) {
|
|
1399
|
+
return content
|
|
1400
|
+
.map((p) => (p?.type === "text" ? p.text : ""))
|
|
1401
|
+
.join("");
|
|
940
1402
|
}
|
|
941
|
-
|
|
1403
|
+
return typeof content === "string" ? content : "";
|
|
1404
|
+
};
|
|
1405
|
+
if (isJson) {
|
|
1406
|
+
const events = typeof session.events !== "undefined"
|
|
1407
|
+
? session.events
|
|
1408
|
+
: [];
|
|
1409
|
+
const payload = {
|
|
1410
|
+
version: 1,
|
|
1411
|
+
session: {
|
|
1412
|
+
id: session.id,
|
|
1413
|
+
workspace: session.workspace,
|
|
1414
|
+
provider: session.provider,
|
|
1415
|
+
messageCount: session.messages.length,
|
|
1416
|
+
},
|
|
1417
|
+
messages: session.messages,
|
|
1418
|
+
events,
|
|
1419
|
+
};
|
|
1420
|
+
let json = JSON.stringify(payload, null, 2);
|
|
1421
|
+
if (redact)
|
|
1422
|
+
json = redactText(json);
|
|
1423
|
+
writeFileSync(outPath, json + "\n", "utf8");
|
|
1424
|
+
appendLine(`exported session to ${outPath}`);
|
|
942
1425
|
return;
|
|
943
1426
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1427
|
+
const lines = [];
|
|
1428
|
+
lines.push(`# Session ${session.id}`);
|
|
1429
|
+
lines.push("");
|
|
1430
|
+
lines.push(`- Workspace: ${session.workspace}`);
|
|
1431
|
+
lines.push(`- Provider: ${session.provider}`);
|
|
1432
|
+
lines.push(`- Messages: ${session.messages.length}`);
|
|
1433
|
+
lines.push(`- Exported: ${new Date().toISOString()}`);
|
|
1434
|
+
lines.push("");
|
|
1435
|
+
lines.push("## Table of contents");
|
|
1436
|
+
lines.push("");
|
|
1437
|
+
let turn = 0;
|
|
1438
|
+
for (const message of session.messages) {
|
|
1439
|
+
if (message.role === "user") {
|
|
1440
|
+
turn++;
|
|
1441
|
+
const snippet = redactText(messageText(message)).split("\\n")[0].slice(0, 80);
|
|
1442
|
+
lines.push(`${turn}. ${snippet || "(empty)"}`);
|
|
951
1443
|
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1444
|
+
}
|
|
1445
|
+
lines.push("");
|
|
1446
|
+
lines.push("## Transcript");
|
|
1447
|
+
lines.push("");
|
|
1448
|
+
for (const message of session.messages) {
|
|
1449
|
+
if (message.role === "user") {
|
|
1450
|
+
lines.push("### User");
|
|
1451
|
+
lines.push("");
|
|
1452
|
+
lines.push(redactText(messageText(message)));
|
|
1453
|
+
lines.push("");
|
|
955
1454
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1455
|
+
else if (message.role === "assistant") {
|
|
1456
|
+
lines.push("### Assistant");
|
|
1457
|
+
lines.push("");
|
|
1458
|
+
const text = redactText(messageText(message));
|
|
1459
|
+
if (text.trim())
|
|
1460
|
+
lines.push(text);
|
|
1461
|
+
if (Array.isArray(message.toolCalls) && message.toolCalls.length) {
|
|
1462
|
+
lines.push("");
|
|
1463
|
+
lines.push("Tool calls:");
|
|
1464
|
+
for (const call of message.toolCalls) {
|
|
1465
|
+
lines.push(`- \`${call.name}\` ${JSON.stringify(call.arguments ?? {})}`);
|
|
1466
|
+
}
|
|
966
1467
|
}
|
|
1468
|
+
lines.push("");
|
|
1469
|
+
}
|
|
1470
|
+
else if (message.role === "tool") {
|
|
1471
|
+
lines.push(`### Tool result: ${message.name}`);
|
|
1472
|
+
lines.push("");
|
|
1473
|
+
const content = redactText(String(message.content ?? ""));
|
|
1474
|
+
const truncated = content.length > 4000
|
|
1475
|
+
? content.slice(0, 4000) + "\\n… (truncated)"
|
|
1476
|
+
: content;
|
|
1477
|
+
lines.push("```");
|
|
1478
|
+
lines.push(truncated);
|
|
1479
|
+
lines.push("```");
|
|
1480
|
+
lines.push("");
|
|
967
1481
|
}
|
|
968
|
-
return;
|
|
969
1482
|
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1483
|
+
writeFileSync(outPath, lines.join("\\n"), "utf8");
|
|
1484
|
+
appendLine(`exported session to ${outPath}`);
|
|
1485
|
+
}
|
|
1486
|
+
input.onSubmit = async (value) => {
|
|
1487
|
+
const trimmed = value.trim();
|
|
1488
|
+
input.setText("");
|
|
1489
|
+
if (!trimmed) {
|
|
1490
|
+
tui.requestRender();
|
|
973
1491
|
return;
|
|
974
1492
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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
|
-
}
|
|
1493
|
+
refreshCommandRegistry();
|
|
1494
|
+
if (trimmed.startsWith("/")) {
|
|
1495
|
+
const result = await commandRegistry.dispatch(trimmed, commandContext);
|
|
1496
|
+
if (result.exit)
|
|
988
1497
|
return;
|
|
1498
|
+
if (!result.handled) {
|
|
1499
|
+
appendLine(`unknown command: ${trimmed.split(/\s+/)[0]} (type /help for a list of commands)`);
|
|
989
1500
|
}
|
|
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
1501
|
return;
|
|
1032
1502
|
}
|
|
1033
|
-
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1036
|
-
await
|
|
1037
|
-
|
|
1038
|
-
catch (err) {
|
|
1039
|
-
appendLine(`atom-lite: ${err instanceof Error ? err.message : String(err)}`);
|
|
1503
|
+
// Skills remain invokable by bare name, but still run through the registry.
|
|
1504
|
+
const bare = bareCommandName(commandRegistry, trimmed);
|
|
1505
|
+
if (bare) {
|
|
1506
|
+
await commandRegistry.dispatch(`/${bare} ${trimmed.slice(bare.length)}`, commandContext);
|
|
1507
|
+
return;
|
|
1040
1508
|
}
|
|
1509
|
+
appendLine(`> ${trimmed}`);
|
|
1510
|
+
await runTurn(trimmed);
|
|
1041
1511
|
};
|
|
1042
|
-
// fix approvals handler to use appendLine (defined above via closure var)
|
|
1043
1512
|
approvals.handler = interactive
|
|
1044
|
-
?
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1513
|
+
? (req) => new Promise((resolve) => {
|
|
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);
|
|
1055
1527
|
});
|
|
1056
|
-
|
|
1528
|
+
dropdownContainer.addChild(prompt);
|
|
1529
|
+
tui.setFocus(prompt);
|
|
1530
|
+
tui.requestRender();
|
|
1531
|
+
})
|
|
1057
1532
|
: undefined;
|
|
1533
|
+
process.on("SIGTERM", () => {
|
|
1534
|
+
void mcpManager.close().finally(() => {
|
|
1535
|
+
tui.stop();
|
|
1536
|
+
process.exit(0);
|
|
1537
|
+
});
|
|
1538
|
+
});
|
|
1058
1539
|
tui.start();
|
|
1540
|
+
if (mouseEnabled) {
|
|
1541
|
+
mouseCapture.enable();
|
|
1542
|
+
process.once("exit", () => mouseCapture.disable());
|
|
1543
|
+
if (process.platform !== "win32") {
|
|
1544
|
+
process.on("SIGTSTP", () => {
|
|
1545
|
+
mouseCapture.disable();
|
|
1546
|
+
process.kill(process.pid, "SIGSTOP");
|
|
1547
|
+
});
|
|
1548
|
+
process.on("SIGCONT", () => {
|
|
1549
|
+
mouseCapture.enable();
|
|
1550
|
+
tui.requestRender();
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
// Some terminals answer OSC 11 but not the colour-scheme report. Query the
|
|
1555
|
+
// background once and derive light/dark from its luminance.
|
|
1556
|
+
if (!noColor && typeof tui.queryTerminalColors === "function") {
|
|
1557
|
+
void tui
|
|
1558
|
+
.queryTerminalColors({ timeoutMs: 250 })
|
|
1559
|
+
.then((colors) => {
|
|
1560
|
+
const background = colors.background;
|
|
1561
|
+
if (!background)
|
|
1562
|
+
return;
|
|
1563
|
+
const luminance = (0.2126 * background.r +
|
|
1564
|
+
0.7152 * background.g +
|
|
1565
|
+
0.0722 * background.b) /
|
|
1566
|
+
255;
|
|
1567
|
+
terminalScheme = luminance > 0.5 ? "light" : "dark";
|
|
1568
|
+
if (themeName === "auto")
|
|
1569
|
+
applyTheme("auto", false);
|
|
1570
|
+
})
|
|
1571
|
+
.catch(() => {
|
|
1572
|
+
// Keep the static fallback.
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1059
1575
|
}
|
|
1060
1576
|
//# sourceMappingURL=app.js.map
|