atom-lite 0.0.0-stage → 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 +72 -0
- package/LICENSE +21 -0
- package/README.md +313 -2
- package/dist/agent/loop.d.ts +32 -0
- package/dist/agent/loop.js +204 -0
- package/dist/agent/loop.js.map +1 -0
- package/dist/agent/types.d.ts +85 -0
- package/dist/agent/types.js +2 -0
- package/dist/agent/types.js.map +1 -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.d.ts +1 -0
- package/dist/cli/app.js +1576 -0
- package/dist/cli/app.js.map +1 -0
- 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/approvals.d.ts +1 -0
- package/dist/cli/approvals.js +2 -0
- package/dist/cli/approvals.js.map +1 -0
- package/dist/cli/banner.d.ts +12 -0
- package/dist/cli/banner.js +35 -0
- package/dist/cli/banner.js.map +1 -0
- package/dist/cli/commands.d.ts +94 -0
- package/dist/cli/commands.js +324 -0
- package/dist/cli/commands.js.map +1 -0
- 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.d.ts +2 -0
- package/dist/cli/main.js +22 -0
- package/dist/cli/main.js.map +1 -0
- 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 +34 -0
- package/dist/cli/picker.js +173 -0
- package/dist/cli/picker.js.map +1 -0
- package/dist/cli/renderer.d.ts +1 -0
- package/dist/cli/renderer.js +5 -0
- package/dist/cli/renderer.js.map +1 -0
- package/dist/cli/secret-input.d.ts +21 -0
- package/dist/cli/secret-input.js +101 -0
- package/dist/cli/secret-input.js.map +1 -0
- package/dist/cli/status-line.d.ts +24 -0
- package/dist/cli/status-line.js +49 -0
- package/dist/cli/status-line.js.map +1 -0
- package/dist/cli/stream-text.d.ts +18 -0
- package/dist/cli/stream-text.js +63 -0
- package/dist/cli/stream-text.js.map +1 -0
- 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 +26 -0
- package/dist/cli/transcript.js +64 -0
- package/dist/cli/transcript.js.map +1 -0
- package/dist/config/credentials.d.ts +26 -0
- package/dist/config/credentials.js +79 -0
- package/dist/config/credentials.js.map +1 -0
- package/dist/config/defaults.d.ts +34 -0
- package/dist/config/defaults.js +35 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/load.d.ts +9 -0
- package/dist/config/load.js +137 -0
- package/dist/config/load.js.map +1 -0
- package/dist/config/paths.d.ts +4 -0
- package/dist/config/paths.js +14 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/schema.d.ts +132 -0
- package/dist/config/schema.js +83 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/write.d.ts +20 -0
- package/dist/config/write.js +70 -0
- package/dist/config/write.js.map +1 -0
- package/dist/context/budget.d.ts +2 -0
- package/dist/context/budget.js +13 -0
- package/dist/context/budget.js.map +1 -0
- 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/compaction.d.ts +1 -0
- package/dist/context/compaction.js +5 -0
- package/dist/context/compaction.js.map +1 -0
- package/dist/context/instructions.d.ts +3 -0
- package/dist/context/instructions.js +30 -0
- package/dist/context/instructions.js.map +1 -0
- package/dist/context/manager.d.ts +29 -0
- package/dist/context/manager.js +122 -0
- package/dist/context/manager.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 +17 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- 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/catalog.d.ts +60 -0
- package/dist/providers/catalog.js +289 -0
- package/dist/providers/catalog.js.map +1 -0
- package/dist/providers/context-window.d.ts +11 -0
- package/dist/providers/context-window.js +87 -0
- package/dist/providers/context-window.js.map +1 -0
- package/dist/providers/factory.d.ts +2 -0
- package/dist/providers/factory.js +55 -0
- package/dist/providers/factory.js.map +1 -0
- package/dist/providers/fake.d.ts +14 -0
- package/dist/providers/fake.js +23 -0
- package/dist/providers/fake.js.map +1 -0
- package/dist/providers/gemini.d.ts +24 -0
- package/dist/providers/gemini.js +228 -0
- package/dist/providers/gemini.js.map +1 -0
- package/dist/providers/index.d.ts +5 -0
- package/dist/providers/index.js +4 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/ollama.d.ts +64 -0
- package/dist/providers/ollama.js +433 -0
- package/dist/providers/ollama.js.map +1 -0
- package/dist/providers/openai-compatible.d.ts +27 -0
- package/dist/providers/openai-compatible.js +282 -0
- package/dist/providers/openai-compatible.js.map +1 -0
- package/dist/providers/registry.d.ts +7 -0
- package/dist/providers/registry.js +13 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/tool-calls.d.ts +18 -0
- package/dist/providers/tool-calls.js +42 -0
- package/dist/providers/tool-calls.js.map +1 -0
- package/dist/providers/types.d.ts +16 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/runtime/approvals.d.ts +20 -0
- package/dist/runtime/approvals.js +20 -0
- package/dist/runtime/approvals.js.map +1 -0
- 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/locks.d.ts +13 -0
- package/dist/runtime/locks.js +180 -0
- package/dist/runtime/locks.js.map +1 -0
- package/dist/runtime/scheduler.d.ts +7 -0
- package/dist/runtime/scheduler.js +110 -0
- package/dist/runtime/scheduler.js.map +1 -0
- package/dist/security/paths.d.ts +1 -0
- package/dist/security/paths.js +35 -0
- package/dist/security/paths.js.map +1 -0
- package/dist/security/process.d.ts +1 -0
- package/dist/security/process.js +8 -0
- package/dist/security/process.js.map +1 -0
- package/dist/security/redaction.d.ts +1 -0
- package/dist/security/redaction.js +4 -0
- package/dist/security/redaction.js.map +1 -0
- package/dist/sessions/events.d.ts +30 -0
- package/dist/sessions/events.js +92 -0
- package/dist/sessions/events.js.map +1 -0
- 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 +5 -0
- package/dist/sessions/index.js +6 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/sessions/recovery.d.ts +6 -0
- package/dist/sessions/recovery.js +35 -0
- package/dist/sessions/recovery.js.map +1 -0
- 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.d.ts +91 -0
- package/dist/sessions/store.js +360 -0
- package/dist/sessions/store.js.map +1 -0
- package/dist/skills/registry.d.ts +23 -0
- package/dist/skills/registry.js +129 -0
- package/dist/skills/registry.js.map +1 -0
- package/dist/tools/bash.d.ts +2 -0
- package/dist/tools/bash.js +159 -0
- package/dist/tools/bash.js.map +1 -0
- package/dist/tools/diff.d.ts +7 -0
- package/dist/tools/diff.js +40 -0
- package/dist/tools/diff.js.map +1 -0
- package/dist/tools/edit.d.ts +2 -0
- package/dist/tools/edit.js +118 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/read.d.ts +2 -0
- package/dist/tools/read.js +138 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/registry.d.ts +7 -0
- package/dist/tools/registry.js +13 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/types.d.ts +27 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/webfetch.d.ts +2 -0
- package/dist/tools/webfetch.js +97 -0
- package/dist/tools/webfetch.js.map +1 -0
- package/dist/tools/websearch.d.ts +2 -0
- package/dist/tools/websearch.js +88 -0
- package/dist/tools/websearch.js.map +1 -0
- package/dist/tools/write.d.ts +2 -0
- package/dist/tools/write.js +91 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/types.d.ts +84 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/package.json +74 -4
package/dist/cli/app.js
ADDED
|
@@ -0,0 +1,1576 @@
|
|
|
1
|
+
import { loadConfig } from "../config/load.js";
|
|
2
|
+
import { saveProviderSelection, saveTheme, } from "../config/write.js";
|
|
3
|
+
import { resolveApiKey, saveApiKey } from "../config/credentials.js";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { writeFileSync, existsSync } from "node:fs";
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
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";
|
|
11
|
+
import { SecretInput } from "./secret-input.js";
|
|
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";
|
|
16
|
+
import { Banner } from "./banner.js";
|
|
17
|
+
import { formatContextUsage, knownContextWindow, } from "../providers/context-window.js";
|
|
18
|
+
import { StreamText } from "./stream-text.js";
|
|
19
|
+
import { Transcript } from "./transcript.js";
|
|
20
|
+
import { AgentRuntime } from "../agent/loop.js";
|
|
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";
|
|
26
|
+
import { readTool, writeTool, editTool, bashTool, webfetchTool, websearchTool } from "../tools/index.js";
|
|
27
|
+
import { McpManager, formatMcpStatus, formatMcpDoctor, createMcpTools } from "../mcp/index.js";
|
|
28
|
+
import { createProvider } from "../providers/factory.js";
|
|
29
|
+
import { availableProviderNames, getPreset, listModelsForProvider, materializeProviderConfig, probeApiKey, providerLabel, PROVIDER_PRESETS, } from "../providers/catalog.js";
|
|
30
|
+
import { ContextManager } from "../context/manager.js";
|
|
31
|
+
import { ProcessTerminal, isKeyRelease, isKeyRepeat, Key, matchesKey, } from "@earendil-works/pi-tui";
|
|
32
|
+
export async function run(args) {
|
|
33
|
+
let providerOverride;
|
|
34
|
+
let modelOverride;
|
|
35
|
+
let jsonMode = false;
|
|
36
|
+
let debug = false;
|
|
37
|
+
let showVersion = false;
|
|
38
|
+
let sessionOverride;
|
|
39
|
+
let themeOverride;
|
|
40
|
+
let noColor = false;
|
|
41
|
+
let listSessions = false;
|
|
42
|
+
let searchSessionsQuery;
|
|
43
|
+
const positional = [];
|
|
44
|
+
for (let i = 0; i < args.length; i++) {
|
|
45
|
+
const a = args[i];
|
|
46
|
+
if (a === "--provider")
|
|
47
|
+
providerOverride = args[++i];
|
|
48
|
+
else if (a === "--model")
|
|
49
|
+
modelOverride = args[++i];
|
|
50
|
+
else if (a === "--json")
|
|
51
|
+
jsonMode = true;
|
|
52
|
+
else if (a === "--debug")
|
|
53
|
+
debug = true;
|
|
54
|
+
else if (a === "--version")
|
|
55
|
+
showVersion = true;
|
|
56
|
+
else if (a === "--continue")
|
|
57
|
+
positional.push("__continue__");
|
|
58
|
+
else if (a === "--session")
|
|
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;
|
|
68
|
+
else if (a === "--no-tools")
|
|
69
|
+
positional.push("__notools__");
|
|
70
|
+
else if (a === "--help") {
|
|
71
|
+
process.stdout.write("atom-lite [task] [--provider NAME] [--model MODEL] [--continue] [--session ID] [--list-sessions] [--search-sessions QUERY] [--no-tools] [--json] [--debug] [--theme NAME] [--no-color] [--version] [--help]\n");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
else if (a.startsWith("--")) {
|
|
75
|
+
// ignore
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
positional.push(a);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (showVersion) {
|
|
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
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
let config = loadConfig();
|
|
103
|
+
const initialProfile = providerOverride ?? config.defaultProvider;
|
|
104
|
+
if (!config.providers[initialProfile]) {
|
|
105
|
+
const preset = getPreset(initialProfile);
|
|
106
|
+
if (preset) {
|
|
107
|
+
config.providers[initialProfile] =
|
|
108
|
+
materializeProviderConfig(preset);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
let provider = createProvider(config, providerOverride, modelOverride);
|
|
112
|
+
const modelByProvider = new Map();
|
|
113
|
+
if (modelOverride)
|
|
114
|
+
modelByProvider.set(provider.id, modelOverride);
|
|
115
|
+
const useTools = !positional.includes("__notools__");
|
|
116
|
+
const builtInTools = useTools
|
|
117
|
+
? [readTool, writeTool, editTool, bashTool, webfetchTool, websearchTool]
|
|
118
|
+
: [];
|
|
119
|
+
const interactive = process.stdin.isTTY === true && !jsonMode;
|
|
120
|
+
// one-shot task from positional or stdin
|
|
121
|
+
let stdinTask = "";
|
|
122
|
+
if (!process.stdin.isTTY && positional.filter((p) => !p.startsWith("__")).length === 0) {
|
|
123
|
+
const chunks = [];
|
|
124
|
+
for await (const chunk of process.stdin)
|
|
125
|
+
chunks.push(chunk);
|
|
126
|
+
stdinTask = Buffer.concat(chunks).toString("utf8").trim();
|
|
127
|
+
}
|
|
128
|
+
const taskArgs = positional.filter((p) => !p.startsWith("__"));
|
|
129
|
+
const oneShot = (taskArgs.join(" ").trim() || stdinTask).trim();
|
|
130
|
+
const store = new JsonlSessionStore();
|
|
131
|
+
let session = sessionOverride
|
|
132
|
+
? store.resume(sessionOverride) ?? store.create(process.cwd(), provider.id)
|
|
133
|
+
: positional.includes("__continue__")
|
|
134
|
+
? store.resumeLatest(process.cwd()) ?? store.create(process.cwd(), provider.id)
|
|
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
|
+
});
|
|
156
|
+
const contextManager = new ContextManager({
|
|
157
|
+
systemPrompt: "You are a coding assistant.",
|
|
158
|
+
maxInputTokens: config.context.maxInputTokens ?? null,
|
|
159
|
+
reserveOutputTokens: config.context.reserveOutputTokens,
|
|
160
|
+
compactAtPercent: config.context.compactAtPercent,
|
|
161
|
+
maxProjectInstructionBytes: config.context.maxProjectInstructionBytes,
|
|
162
|
+
provider,
|
|
163
|
+
model: modelOverride ?? "default",
|
|
164
|
+
userInstructionsPath: undefined,
|
|
165
|
+
});
|
|
166
|
+
let sessionTrusted = false;
|
|
167
|
+
const approvals = {
|
|
168
|
+
mode: config.runtime.approvalMode ?? "ask",
|
|
169
|
+
// Installed below once the interactive UI helpers exist.
|
|
170
|
+
handler: undefined,
|
|
171
|
+
isTrusted: () => sessionTrusted,
|
|
172
|
+
trustSession: () => {
|
|
173
|
+
sessionTrusted = true;
|
|
174
|
+
},
|
|
175
|
+
nonInteractive: !interactive,
|
|
176
|
+
};
|
|
177
|
+
const runtime = new AgentRuntime({
|
|
178
|
+
provider,
|
|
179
|
+
model: modelOverride,
|
|
180
|
+
workspace: process.cwd(),
|
|
181
|
+
tools: builtInTools,
|
|
182
|
+
approvals,
|
|
183
|
+
contextManager,
|
|
184
|
+
bashConcurrency: config.runtime.bashConcurrency,
|
|
185
|
+
});
|
|
186
|
+
mcpManager.onToolsChanged = () => {
|
|
187
|
+
runtime.setTools([...builtInTools, ...createMcpTools(mcpManager)]);
|
|
188
|
+
};
|
|
189
|
+
if (oneShot || !interactive) {
|
|
190
|
+
// non-interactive / one-shot path
|
|
191
|
+
const controller = new AbortController();
|
|
192
|
+
const onSigint = () => controller.abort();
|
|
193
|
+
process.on("SIGINT", onSigint);
|
|
194
|
+
try {
|
|
195
|
+
if (useTools) {
|
|
196
|
+
await mcpManager.syncTools();
|
|
197
|
+
runtime.setTools([...builtInTools, ...createMcpTools(mcpManager)]);
|
|
198
|
+
}
|
|
199
|
+
const result = await runtime.runAgentTurn(oneShot || " ", session, controller.signal);
|
|
200
|
+
if (jsonMode) {
|
|
201
|
+
process.stdout.write(JSON.stringify({ result, sessionId: session.id }) + "\n");
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
process.stdout.write(result + "\n");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
if (jsonMode)
|
|
209
|
+
process.stderr.write(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }) + "\n");
|
|
210
|
+
else
|
|
211
|
+
process.stderr.write(`atom-lite: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
await mcpManager.close().catch(() => { });
|
|
216
|
+
process.off("SIGINT", onSigint);
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// ---- Interactive TUI (first-class) ----
|
|
221
|
+
const { TuiMainScreen, Text, Input, Editor, Container, Spacer, ScrollView, Loader, Markdown, Box } = await import("@earendil-works/pi-tui");
|
|
222
|
+
const terminal = new ProcessTerminal();
|
|
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));
|
|
230
|
+
let effortLevel = "medium";
|
|
231
|
+
runtime.setEffort(effortLevel);
|
|
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);
|
|
244
|
+
transcript.add(new Spacer(1));
|
|
245
|
+
const content = transcript.container;
|
|
246
|
+
const output = new ScrollView(content, { follow: "end" });
|
|
247
|
+
const streamText = new StreamText(transcript, 200, 200, markdownThemeFor(theme));
|
|
248
|
+
function headerLabel() {
|
|
249
|
+
const cwd = process.cwd();
|
|
250
|
+
const home = process.env.HOME ?? "";
|
|
251
|
+
let displayPath = cwd;
|
|
252
|
+
if (home && cwd.startsWith(home))
|
|
253
|
+
displayPath = "~" + cwd.slice(home.length);
|
|
254
|
+
let branch = "";
|
|
255
|
+
try {
|
|
256
|
+
branch = execSync("git rev-parse --abbrev-ref HEAD", { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
257
|
+
if (branch === "HEAD") {
|
|
258
|
+
branch = execSync("git rev-parse --short HEAD", { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
branch = "";
|
|
263
|
+
}
|
|
264
|
+
return branch ? `${displayPath}:${branch}` : displayPath;
|
|
265
|
+
}
|
|
266
|
+
// paddingY = 0 keeps the workspace path directly under the status line.
|
|
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() });
|
|
270
|
+
statusBar.setParts({
|
|
271
|
+
left: `${provider.id}/${provider.model ?? "default"}`,
|
|
272
|
+
tokens: "tokens: ?",
|
|
273
|
+
effort: "effort: medium",
|
|
274
|
+
activity: activity.text(),
|
|
275
|
+
});
|
|
276
|
+
const selectListTheme = {
|
|
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,
|
|
286
|
+
};
|
|
287
|
+
const input = new Editor(tui, editorTheme, {});
|
|
288
|
+
const { CombinedAutocompleteProvider } = await import("@earendil-works/pi-tui");
|
|
289
|
+
const { DefaultSkillRegistry, skillCacheSignature } = await import("../skills/registry.js");
|
|
290
|
+
let skillSignature = skillCacheSignature(process.cwd());
|
|
291
|
+
let skillReg = new DefaultSkillRegistry(process.cwd());
|
|
292
|
+
function cachedSkillRegistry() {
|
|
293
|
+
const sig = skillCacheSignature(process.cwd());
|
|
294
|
+
if (sig !== skillSignature) {
|
|
295
|
+
skillReg = new DefaultSkillRegistry(process.cwd());
|
|
296
|
+
skillSignature = sig;
|
|
297
|
+
}
|
|
298
|
+
return skillReg;
|
|
299
|
+
}
|
|
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()));
|
|
309
|
+
}
|
|
310
|
+
input.setAutocompleteProvider(new CombinedAutocompleteProvider(commandRegistry.autocomplete(), process.cwd()));
|
|
311
|
+
tui.addChild(output);
|
|
312
|
+
tui.addChild(new Spacer());
|
|
313
|
+
tui.addChild(input);
|
|
314
|
+
tui.addChild(statusBar);
|
|
315
|
+
// folder:branch info below the status line, dimmed
|
|
316
|
+
tui.addChild(header);
|
|
317
|
+
const dropdownContainer = new Container();
|
|
318
|
+
tui.addChild(dropdownContainer);
|
|
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
|
+
}
|
|
334
|
+
let thinkingText = null;
|
|
335
|
+
let thinkingBuffer = "";
|
|
336
|
+
let showThinking = config.ui.showThinking;
|
|
337
|
+
let expandTools = false;
|
|
338
|
+
let uiModalOpen = false;
|
|
339
|
+
let closeActiveOverlay;
|
|
340
|
+
const toolBoxes = [];
|
|
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
|
+
}
|
|
351
|
+
function appendLine(text) {
|
|
352
|
+
if (thinkingText) {
|
|
353
|
+
thinkingText = null;
|
|
354
|
+
thinkingBuffer = "";
|
|
355
|
+
}
|
|
356
|
+
streamText.finalize();
|
|
357
|
+
transcript.addLine(text);
|
|
358
|
+
tui.requestRender();
|
|
359
|
+
}
|
|
360
|
+
function appendDelta(delta) {
|
|
361
|
+
streamText.append(delta);
|
|
362
|
+
tui.requestRender();
|
|
363
|
+
}
|
|
364
|
+
function thinkingBody() {
|
|
365
|
+
return thinkingBuffer
|
|
366
|
+
.split("\n")
|
|
367
|
+
.map((line) => theme.tokens.dim(`| ${line}`))
|
|
368
|
+
.join("\n");
|
|
369
|
+
}
|
|
370
|
+
function finalizeText() {
|
|
371
|
+
streamText.finalize();
|
|
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
|
+
}
|
|
399
|
+
function updateStatus() {
|
|
400
|
+
const usage = runtime.getContextUsage();
|
|
401
|
+
statusBar.setParts({
|
|
402
|
+
left: `${provider.id}/${currentModelLabel()}`,
|
|
403
|
+
tokens: `tokens: ${formatContextUsage(usage.used, usage.window)}`,
|
|
404
|
+
effort: `effort: ${effortLevel}`,
|
|
405
|
+
activity: activity.text(),
|
|
406
|
+
});
|
|
407
|
+
tui.requestRender();
|
|
408
|
+
}
|
|
409
|
+
function currentModelLabel() {
|
|
410
|
+
return modelOverride ?? provider.model ?? "default";
|
|
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
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Show the best-known window immediately, then refine it from live provider
|
|
437
|
+
* metadata without blocking the TUI.
|
|
438
|
+
*/
|
|
439
|
+
function refreshContextWindow() {
|
|
440
|
+
const profile = provider.id;
|
|
441
|
+
const model = currentModelLabel();
|
|
442
|
+
const configured = profileConfig(profile)?.contextWindow;
|
|
443
|
+
const window = typeof configured === "number" && configured > 0
|
|
444
|
+
? configured
|
|
445
|
+
: knownContextWindow(model);
|
|
446
|
+
runtime.setContextWindow(window);
|
|
447
|
+
updateStatus();
|
|
448
|
+
const getModelInfo = provider.getModelInfo;
|
|
449
|
+
if (typeof getModelInfo !== "function")
|
|
450
|
+
return;
|
|
451
|
+
void Promise.resolve()
|
|
452
|
+
.then(() => getModelInfo.call(provider))
|
|
453
|
+
.then((info) => {
|
|
454
|
+
if (provider.id !== profile || currentModelLabel() !== model)
|
|
455
|
+
return;
|
|
456
|
+
const resolved = info?.contextWindow;
|
|
457
|
+
if (typeof resolved === "number" && resolved > 0) {
|
|
458
|
+
runtime.setContextWindow(resolved);
|
|
459
|
+
updateStatus();
|
|
460
|
+
}
|
|
461
|
+
})
|
|
462
|
+
.catch(() => {
|
|
463
|
+
// Keep the static/config fallback.
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
// Resolve the initial model's window before the first response.
|
|
467
|
+
refreshContextWindow();
|
|
468
|
+
const MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
469
|
+
const modelCache = new Map();
|
|
470
|
+
function profileConfig(profile) {
|
|
471
|
+
return config.providers[profile];
|
|
472
|
+
}
|
|
473
|
+
/** Ensure a preset profile exists in the in-memory config. */
|
|
474
|
+
function ensureProfile(profile) {
|
|
475
|
+
let pcfg = profileConfig(profile);
|
|
476
|
+
if (pcfg)
|
|
477
|
+
return pcfg;
|
|
478
|
+
const preset = getPreset(profile);
|
|
479
|
+
if (!preset)
|
|
480
|
+
return undefined;
|
|
481
|
+
pcfg = materializeProviderConfig(preset);
|
|
482
|
+
config.providers[profile] = pcfg;
|
|
483
|
+
saveProviderSelection(profile, pcfg);
|
|
484
|
+
return pcfg;
|
|
485
|
+
}
|
|
486
|
+
function modelsFor(profile) {
|
|
487
|
+
const cached = modelCache.get(profile);
|
|
488
|
+
if (cached && Date.now() - cached.at < MODEL_CACHE_TTL_MS) {
|
|
489
|
+
return cached.models;
|
|
490
|
+
}
|
|
491
|
+
const pcfg = profileConfig(profile);
|
|
492
|
+
const preset = getPreset(profile);
|
|
493
|
+
const fallback = [
|
|
494
|
+
...(preset?.fallbackModels ?? []),
|
|
495
|
+
...(pcfg?.model ? [String(pcfg.model)] : []),
|
|
496
|
+
];
|
|
497
|
+
return [...new Set(fallback.filter(Boolean))];
|
|
498
|
+
}
|
|
499
|
+
function buildModelItems() {
|
|
500
|
+
const names = availableProviderNames(config);
|
|
501
|
+
if (!names.includes(provider.id) && profileConfig(provider.id)) {
|
|
502
|
+
names.unshift(provider.id);
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
const idx = names.indexOf(provider.id);
|
|
506
|
+
if (idx > 0) {
|
|
507
|
+
names.splice(idx, 1);
|
|
508
|
+
names.unshift(provider.id);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const items = [];
|
|
512
|
+
const seen = new Set();
|
|
513
|
+
for (const name of names) {
|
|
514
|
+
let pcfg = profileConfig(name);
|
|
515
|
+
if (!pcfg) {
|
|
516
|
+
const preset = getPreset(name);
|
|
517
|
+
if (!preset)
|
|
518
|
+
continue;
|
|
519
|
+
// Materialize env/credential-only presets in memory so they appear in
|
|
520
|
+
// the picker without being written to disk until the user picks one.
|
|
521
|
+
pcfg = materializeProviderConfig(preset);
|
|
522
|
+
config.providers[name] = pcfg;
|
|
523
|
+
}
|
|
524
|
+
for (const model of modelsFor(name)) {
|
|
525
|
+
const value = `${name}\u0000${model}`;
|
|
526
|
+
if (seen.has(value))
|
|
527
|
+
continue;
|
|
528
|
+
seen.add(value);
|
|
529
|
+
const isCurrent = name === provider.id && model === currentModelLabel();
|
|
530
|
+
items.push({
|
|
531
|
+
value,
|
|
532
|
+
label: `${model}${isCurrent ? " (current)" : ""}`,
|
|
533
|
+
description: providerLabel(name),
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return items;
|
|
538
|
+
}
|
|
539
|
+
function refreshModelCache(names, onUpdate) {
|
|
540
|
+
if (names.length === 0)
|
|
541
|
+
return;
|
|
542
|
+
let remaining = names.length;
|
|
543
|
+
for (const name of names) {
|
|
544
|
+
void listModelsForProvider(config, name, 8000).then((models) => {
|
|
545
|
+
if (models.length) {
|
|
546
|
+
const configured = profileConfig(name)?.model;
|
|
547
|
+
modelCache.set(name, {
|
|
548
|
+
models: [
|
|
549
|
+
...new Set([
|
|
550
|
+
...(configured ? [String(configured)] : []),
|
|
551
|
+
...models,
|
|
552
|
+
]),
|
|
553
|
+
],
|
|
554
|
+
at: Date.now(),
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
remaining--;
|
|
558
|
+
if (remaining === 0)
|
|
559
|
+
onUpdate();
|
|
560
|
+
});
|
|
561
|
+
}
|
|
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
|
+
}
|
|
570
|
+
function switchToModel(profile, model) {
|
|
571
|
+
const pcfg = ensureProfile(profile);
|
|
572
|
+
if (!pcfg) {
|
|
573
|
+
appendLine(`unknown provider: ${profile}`);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
pcfg.model = model;
|
|
577
|
+
config.defaultProvider = profile;
|
|
578
|
+
try {
|
|
579
|
+
saveProviderSelection(profile, { model }, { defaultProvider: profile });
|
|
580
|
+
}
|
|
581
|
+
catch (err) {
|
|
582
|
+
appendLine(`warning: could not persist model choice: ${err instanceof Error ? err.message : String(err)}`);
|
|
583
|
+
}
|
|
584
|
+
modelByProvider.set(profile, model);
|
|
585
|
+
modelOverride = model;
|
|
586
|
+
provider = createProvider(config, profile, model);
|
|
587
|
+
runtime.switchModel(provider, model);
|
|
588
|
+
refreshContextWindow();
|
|
589
|
+
appendLine(`switched to ${profile}/${model}`);
|
|
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
|
+
}
|
|
620
|
+
function openModelPicker() {
|
|
621
|
+
const names = availableProviderNames(config);
|
|
622
|
+
if (!names.includes(provider.id) && profileConfig(provider.id)) {
|
|
623
|
+
names.unshift(provider.id);
|
|
624
|
+
}
|
|
625
|
+
uiModalOpen = true;
|
|
626
|
+
const picker = openSearchablePicker({
|
|
627
|
+
tui,
|
|
628
|
+
container: dropdownContainer,
|
|
629
|
+
title: "Select model",
|
|
630
|
+
theme,
|
|
631
|
+
items: buildModelItems(),
|
|
632
|
+
maxVisible: 15,
|
|
633
|
+
onSelect: (item) => {
|
|
634
|
+
uiModalOpen = false;
|
|
635
|
+
const sep = item.value.indexOf("\u0000");
|
|
636
|
+
const profile = item.value.slice(0, sep);
|
|
637
|
+
const model = item.value.slice(sep + 1);
|
|
638
|
+
switchToModel(profile, model);
|
|
639
|
+
},
|
|
640
|
+
onCancel: () => {
|
|
641
|
+
uiModalOpen = false;
|
|
642
|
+
appendLine("cancelled");
|
|
643
|
+
},
|
|
644
|
+
restoreFocus: () => tui.setFocus(input),
|
|
645
|
+
});
|
|
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, () => {
|
|
651
|
+
picker.setStatus("");
|
|
652
|
+
picker.updateItems(buildModelItems());
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
function providerItems() {
|
|
657
|
+
return PROVIDER_PRESETS.map((preset) => {
|
|
658
|
+
const configured = Boolean(profileConfig(preset.id));
|
|
659
|
+
const key = preset.requiresKey
|
|
660
|
+
? resolveApiKey(preset.id, { apiKeyEnv: preset.keyEnv })
|
|
661
|
+
: null;
|
|
662
|
+
const status = !preset.requiresKey
|
|
663
|
+
? preset.local
|
|
664
|
+
? "local · no key"
|
|
665
|
+
: "no key"
|
|
666
|
+
: key
|
|
667
|
+
? "key saved"
|
|
668
|
+
: preset.keyEnv
|
|
669
|
+
? `needs key (${preset.keyEnv})`
|
|
670
|
+
: "needs key";
|
|
671
|
+
return {
|
|
672
|
+
value: preset.id,
|
|
673
|
+
label: `${configured ? "✓ " : " "}${preset.label}`,
|
|
674
|
+
description: `${preset.description} · ${status}`,
|
|
675
|
+
};
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
function activateProvider(preset, models) {
|
|
679
|
+
const existing = profileConfig(preset.id);
|
|
680
|
+
const pcfg = materializeProviderConfig(preset, existing);
|
|
681
|
+
config.providers[preset.id] = pcfg;
|
|
682
|
+
config.defaultProvider = preset.id;
|
|
683
|
+
saveProviderSelection(preset.id, pcfg, { defaultProvider: preset.id });
|
|
684
|
+
if (models?.length) {
|
|
685
|
+
modelCache.set(preset.id, {
|
|
686
|
+
models: [...new Set([String(pcfg.model), ...models])],
|
|
687
|
+
at: Date.now(),
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
const chosen = modelByProvider.get(preset.id) ?? String(pcfg.model);
|
|
691
|
+
pcfg.model = chosen;
|
|
692
|
+
modelOverride = chosen;
|
|
693
|
+
provider = createProvider(config, preset.id, chosen);
|
|
694
|
+
runtime.switchModel(provider, chosen);
|
|
695
|
+
if (typeof session.appendEvent === "function") {
|
|
696
|
+
session.appendEvent({ type: "provider_selected", provider: preset.id, model: chosen });
|
|
697
|
+
}
|
|
698
|
+
refreshContextWindow();
|
|
699
|
+
appendLine(`provider: ${preset.label} (${preset.id}) · model: ${chosen}`);
|
|
700
|
+
appendLine("tip: /model searches every configured provider");
|
|
701
|
+
}
|
|
702
|
+
function closeApiKeyPrompt() {
|
|
703
|
+
closeOverlay();
|
|
704
|
+
dropdownContainer.clear();
|
|
705
|
+
uiModalOpen = false;
|
|
706
|
+
tui.setFocus(input);
|
|
707
|
+
tui.requestRender();
|
|
708
|
+
}
|
|
709
|
+
function promptForApiKey(preset, existingKey) {
|
|
710
|
+
uiModalOpen = true;
|
|
711
|
+
closeOverlay();
|
|
712
|
+
dropdownContainer.clear();
|
|
713
|
+
const wrapper = new Container();
|
|
714
|
+
wrapper.addChild(new Text(theme.tokens.accent(`${preset.label} API key`)));
|
|
715
|
+
if (preset.keyUrl)
|
|
716
|
+
wrapper.addChild(new Text(theme.tokens.dim(`create one: ${preset.keyUrl}`)));
|
|
717
|
+
if (existingKey)
|
|
718
|
+
wrapper.addChild(new Text(theme.tokens.dim("a key is already saved — press Enter to keep it")));
|
|
719
|
+
const secret = new SecretInput("API key", existingKey ? "hidden (Enter keeps saved key)" : "paste key (hidden)");
|
|
720
|
+
const status = new Text("");
|
|
721
|
+
wrapper.addChild(secret);
|
|
722
|
+
wrapper.addChild(status);
|
|
723
|
+
dropdownContainer.addChild(wrapper);
|
|
724
|
+
tui.setFocus(secret);
|
|
725
|
+
tui.requestRender();
|
|
726
|
+
let busy = false;
|
|
727
|
+
secret.onCancel = () => {
|
|
728
|
+
if (busy)
|
|
729
|
+
return;
|
|
730
|
+
closeApiKeyPrompt();
|
|
731
|
+
appendLine(`cancelled ${preset.label} setup`);
|
|
732
|
+
};
|
|
733
|
+
secret.onSubmit = async (value) => {
|
|
734
|
+
if (busy)
|
|
735
|
+
return;
|
|
736
|
+
const key = value.trim();
|
|
737
|
+
if (!key) {
|
|
738
|
+
if (existingKey) {
|
|
739
|
+
closeApiKeyPrompt();
|
|
740
|
+
try {
|
|
741
|
+
activateProvider(preset);
|
|
742
|
+
}
|
|
743
|
+
catch (err) {
|
|
744
|
+
appendLine(`could not activate ${preset.label}: ${err instanceof Error ? err.message : String(err)}`);
|
|
745
|
+
}
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
status.setText(theme.tokens.dim("an API key is required"));
|
|
749
|
+
tui.requestRender();
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
busy = true;
|
|
753
|
+
status.setText(theme.tokens.dim("validating key…"));
|
|
754
|
+
tui.requestRender();
|
|
755
|
+
await new Promise((r) => setImmediate(r));
|
|
756
|
+
const probe = await probeApiKey(preset, key);
|
|
757
|
+
if (probe.invalid) {
|
|
758
|
+
busy = false;
|
|
759
|
+
status.setText(theme.tokens.error(`✗ key rejected by ${preset.label}`));
|
|
760
|
+
secret.reset();
|
|
761
|
+
tui.requestRender();
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
try {
|
|
765
|
+
saveApiKey(preset.id, key);
|
|
766
|
+
}
|
|
767
|
+
catch (err) {
|
|
768
|
+
busy = false;
|
|
769
|
+
status.setText(theme.tokens.error(`✗ could not save key: ${err instanceof Error ? err.message : String(err)}`));
|
|
770
|
+
tui.requestRender();
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
closeApiKeyPrompt();
|
|
774
|
+
if (!probe.ok) {
|
|
775
|
+
appendLine(`warning: could not verify ${preset.label} key (${probe.error ?? "network error"}); saved anyway`);
|
|
776
|
+
}
|
|
777
|
+
try {
|
|
778
|
+
activateProvider(preset, probe.models);
|
|
779
|
+
}
|
|
780
|
+
catch (err) {
|
|
781
|
+
appendLine(`could not activate ${preset.label}: ${err instanceof Error ? err.message : String(err)}`);
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
function startProviderSetup(profile) {
|
|
786
|
+
const preset = getPreset(profile);
|
|
787
|
+
if (!preset) {
|
|
788
|
+
appendLine(`unknown provider: ${profile}`);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (!preset.requiresKey) {
|
|
792
|
+
try {
|
|
793
|
+
activateProvider(preset);
|
|
794
|
+
}
|
|
795
|
+
catch (err) {
|
|
796
|
+
appendLine(`could not activate ${preset.label}: ${err instanceof Error ? err.message : String(err)}`);
|
|
797
|
+
}
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
const existingKey = resolveApiKey(preset.id, { apiKeyEnv: preset.keyEnv });
|
|
801
|
+
promptForApiKey(preset, existingKey);
|
|
802
|
+
}
|
|
803
|
+
function openProviderPicker() {
|
|
804
|
+
uiModalOpen = true;
|
|
805
|
+
const picker = openSearchablePicker({
|
|
806
|
+
tui,
|
|
807
|
+
container: dropdownContainer,
|
|
808
|
+
title: "Select provider",
|
|
809
|
+
theme,
|
|
810
|
+
items: providerItems(),
|
|
811
|
+
maxVisible: 12,
|
|
812
|
+
onSelect: (item) => {
|
|
813
|
+
uiModalOpen = false;
|
|
814
|
+
startProviderSetup(item.value);
|
|
815
|
+
},
|
|
816
|
+
onCancel: () => {
|
|
817
|
+
uiModalOpen = false;
|
|
818
|
+
appendLine("cancelled");
|
|
819
|
+
},
|
|
820
|
+
restoreFocus: () => tui.setFocus(input),
|
|
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
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
function toggleTools() {
|
|
1161
|
+
expandTools = !expandTools;
|
|
1162
|
+
for (const box of toolBoxes) {
|
|
1163
|
+
box.setExpanded(expandTools);
|
|
1164
|
+
}
|
|
1165
|
+
tui.requestRender();
|
|
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
|
+
});
|
|
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
|
+
}
|
|
1212
|
+
// Ctrl+O toggles tool expansion (unless a modal is capturing input).
|
|
1213
|
+
if (matchesKey(data, Key.ctrl("o")) && !uiModalOpen) {
|
|
1214
|
+
toggleTools();
|
|
1215
|
+
return { consume: true };
|
|
1216
|
+
}
|
|
1217
|
+
// Escape cancels the active turn.
|
|
1218
|
+
if (matchesKey(data, Key.escape) && !uiModalOpen && activeTurnController) {
|
|
1219
|
+
activeTurnController.abort();
|
|
1220
|
+
return { consume: true };
|
|
1221
|
+
}
|
|
1222
|
+
return undefined;
|
|
1223
|
+
});
|
|
1224
|
+
runtime["onEvent"] = (e) => {
|
|
1225
|
+
if (e.type === "thinking_delta") {
|
|
1226
|
+
activity.setPhase("thinking");
|
|
1227
|
+
if (!showThinking)
|
|
1228
|
+
return;
|
|
1229
|
+
if (!thinkingText) {
|
|
1230
|
+
thinkingText = new Text(theme.tokens.dim("Thinking..."));
|
|
1231
|
+
transcript.add(thinkingText);
|
|
1232
|
+
thinkingBuffer = "";
|
|
1233
|
+
}
|
|
1234
|
+
thinkingBuffer += e.text;
|
|
1235
|
+
thinkingText.setText(`${theme.tokens.dim("Thinking...")}\n${thinkingBody()}`);
|
|
1236
|
+
tui.requestRender();
|
|
1237
|
+
}
|
|
1238
|
+
else if (e.type === "assistant_delta") {
|
|
1239
|
+
activity.setPhase("working");
|
|
1240
|
+
if (thinkingText) {
|
|
1241
|
+
thinkingText.setText(`${theme.tokens.dim("Thinking...")}\n${thinkingBody()}\n${theme.tokens.dim("| end")}`);
|
|
1242
|
+
thinkingText = null;
|
|
1243
|
+
thinkingBuffer = "";
|
|
1244
|
+
}
|
|
1245
|
+
appendDelta(e.text);
|
|
1246
|
+
}
|
|
1247
|
+
else if (e.type === "assistant_text") {
|
|
1248
|
+
activity.setPhase("working");
|
|
1249
|
+
if (thinkingText) {
|
|
1250
|
+
thinkingText.setText(`${theme.tokens.dim("Thinking...")}\n${thinkingBody()}\n${theme.tokens.dim("| end")}`);
|
|
1251
|
+
thinkingText = null;
|
|
1252
|
+
thinkingBuffer = "";
|
|
1253
|
+
}
|
|
1254
|
+
appendDelta(e.text);
|
|
1255
|
+
finalizeText();
|
|
1256
|
+
}
|
|
1257
|
+
else if (e.type === "context_usage") {
|
|
1258
|
+
updateStatus();
|
|
1259
|
+
}
|
|
1260
|
+
else if (e.type === "model_switched") {
|
|
1261
|
+
appendLine(`switched model to ${e.provider}/${e.model}`);
|
|
1262
|
+
if (typeof session.appendEvent === "function") {
|
|
1263
|
+
session.appendEvent({ type: "model_switched", provider: e.provider, model: e.model });
|
|
1264
|
+
}
|
|
1265
|
+
updateStatus();
|
|
1266
|
+
}
|
|
1267
|
+
else if (e.type === "tool_call") {
|
|
1268
|
+
finalizeText();
|
|
1269
|
+
activity.setPhase("working", e.name);
|
|
1270
|
+
const box = new ToolBox({ id: e.id, name: e.name, args: e.args });
|
|
1271
|
+
box.setExpanded(expandTools);
|
|
1272
|
+
transcript.add(box);
|
|
1273
|
+
toolBoxes.push(box);
|
|
1274
|
+
tui.requestRender();
|
|
1275
|
+
}
|
|
1276
|
+
else if (e.type === "tool_output") {
|
|
1277
|
+
const box = toolBoxes.find((b) => b.id === e.toolCallId);
|
|
1278
|
+
if (box) {
|
|
1279
|
+
activity.setPhase("working", e.name);
|
|
1280
|
+
box.appendLive(e.text);
|
|
1281
|
+
tui.requestRender();
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
else if (e.type === "tool_result") {
|
|
1285
|
+
const last = toolBoxes.find((b) => b.id === e.toolCallId) ??
|
|
1286
|
+
toolBoxes[toolBoxes.length - 1];
|
|
1287
|
+
if (last) {
|
|
1288
|
+
last.setResult(e.content, { isError: e.isError, diff: e.diff });
|
|
1289
|
+
activity.setPhase("working");
|
|
1290
|
+
tui.requestRender();
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
else if (e.type === "final") {
|
|
1294
|
+
finalizeText();
|
|
1295
|
+
activity.stop();
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
async function runDoctor() {
|
|
1299
|
+
const lines = ["Doctor:"];
|
|
1300
|
+
try {
|
|
1301
|
+
loadConfig();
|
|
1302
|
+
lines.push(" config: ok");
|
|
1303
|
+
}
|
|
1304
|
+
catch (e) {
|
|
1305
|
+
lines.push(` config: FAIL ${e instanceof Error ? e.message : String(e)}`);
|
|
1306
|
+
}
|
|
1307
|
+
try {
|
|
1308
|
+
const r = await provider.generate({ model: modelOverride ?? "default", messages: [{ role: "user", content: [{ type: "text", text: "ping" }] }], tools: [] });
|
|
1309
|
+
lines.push(` provider generate: ok (${r.finishReason})`);
|
|
1310
|
+
}
|
|
1311
|
+
catch (e) {
|
|
1312
|
+
lines.push(` provider generate: FAIL ${e instanceof Error ? e.message : String(e)}`);
|
|
1313
|
+
}
|
|
1314
|
+
lines.push(...formatMcpDoctor(mcpManager.status()));
|
|
1315
|
+
return lines.join("\n");
|
|
1316
|
+
}
|
|
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();
|
|
1326
|
+
}
|
|
1327
|
+
catch (e) {
|
|
1328
|
+
appendLine(`mcp reconnect failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
if (useTools) {
|
|
1332
|
+
await mcpManager.syncTools();
|
|
1333
|
+
runtime.setTools([...builtInTools, ...createMcpTools(mcpManager)]);
|
|
1334
|
+
}
|
|
1335
|
+
appendLine(formatMcpStatus(mcpManager.status()));
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
if (trimmed) {
|
|
1339
|
+
appendLine("usage: /mcp [reconnect [server]]");
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
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;
|
|
1354
|
+
}
|
|
1355
|
+
else if (t.startsWith("--format=")) {
|
|
1356
|
+
format = t.slice("--format=".length);
|
|
1357
|
+
}
|
|
1358
|
+
else if (t === "--force") {
|
|
1359
|
+
force = true;
|
|
1360
|
+
}
|
|
1361
|
+
else if (t === "--no-redact") {
|
|
1362
|
+
redact = false;
|
|
1363
|
+
}
|
|
1364
|
+
else {
|
|
1365
|
+
positional.push(t);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
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)`);
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
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("");
|
|
1402
|
+
}
|
|
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}`);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
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)"}`);
|
|
1443
|
+
}
|
|
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("");
|
|
1454
|
+
}
|
|
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
|
+
}
|
|
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("");
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
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();
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
refreshCommandRegistry();
|
|
1494
|
+
if (trimmed.startsWith("/")) {
|
|
1495
|
+
const result = await commandRegistry.dispatch(trimmed, commandContext);
|
|
1496
|
+
if (result.exit)
|
|
1497
|
+
return;
|
|
1498
|
+
if (!result.handled) {
|
|
1499
|
+
appendLine(`unknown command: ${trimmed.split(/\s+/)[0]} (type /help for a list of commands)`);
|
|
1500
|
+
}
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
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;
|
|
1508
|
+
}
|
|
1509
|
+
appendLine(`> ${trimmed}`);
|
|
1510
|
+
await runTurn(trimmed);
|
|
1511
|
+
};
|
|
1512
|
+
approvals.handler = interactive
|
|
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);
|
|
1527
|
+
});
|
|
1528
|
+
dropdownContainer.addChild(prompt);
|
|
1529
|
+
tui.setFocus(prompt);
|
|
1530
|
+
tui.requestRender();
|
|
1531
|
+
})
|
|
1532
|
+
: undefined;
|
|
1533
|
+
process.on("SIGTERM", () => {
|
|
1534
|
+
void mcpManager.close().finally(() => {
|
|
1535
|
+
tui.stop();
|
|
1536
|
+
process.exit(0);
|
|
1537
|
+
});
|
|
1538
|
+
});
|
|
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
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
//# sourceMappingURL=app.js.map
|