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.
Files changed (134) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +58 -10
  3. package/dist/agent/loop.d.ts +2 -0
  4. package/dist/agent/loop.js +5 -0
  5. package/dist/agent/loop.js.map +1 -1
  6. package/dist/cli/activity.d.ts +45 -0
  7. package/dist/cli/activity.js +141 -0
  8. package/dist/cli/activity.js.map +1 -0
  9. package/dist/cli/app.js +893 -377
  10. package/dist/cli/app.js.map +1 -1
  11. package/dist/cli/approval-prompt.d.ts +24 -0
  12. package/dist/cli/approval-prompt.js +82 -0
  13. package/dist/cli/approval-prompt.js.map +1 -0
  14. package/dist/cli/banner.d.ts +5 -1
  15. package/dist/cli/banner.js +10 -4
  16. package/dist/cli/banner.js.map +1 -1
  17. package/dist/cli/commands.d.ts +89 -14
  18. package/dist/cli/commands.js +315 -52
  19. package/dist/cli/commands.js.map +1 -1
  20. package/dist/cli/diff.d.ts +1 -0
  21. package/dist/cli/diff.js +28 -0
  22. package/dist/cli/diff.js.map +1 -0
  23. package/dist/cli/input.d.ts +2 -0
  24. package/dist/cli/input.js +10 -0
  25. package/dist/cli/input.js.map +1 -0
  26. package/dist/cli/main.js +2 -2
  27. package/dist/cli/main.js.map +1 -1
  28. package/dist/cli/mouse.d.ts +54 -0
  29. package/dist/cli/mouse.js +83 -0
  30. package/dist/cli/mouse.js.map +1 -0
  31. package/dist/cli/palette.d.ts +41 -0
  32. package/dist/cli/palette.js +212 -0
  33. package/dist/cli/palette.js.map +1 -0
  34. package/dist/cli/panel.d.ts +29 -0
  35. package/dist/cli/panel.js +48 -0
  36. package/dist/cli/panel.js.map +1 -0
  37. package/dist/cli/picker.d.ts +8 -0
  38. package/dist/cli/picker.js +26 -28
  39. package/dist/cli/picker.js.map +1 -1
  40. package/dist/cli/status-line.d.ts +6 -0
  41. package/dist/cli/status-line.js +21 -7
  42. package/dist/cli/status-line.js.map +1 -1
  43. package/dist/cli/stream-text.d.ts +4 -8
  44. package/dist/cli/stream-text.js +25 -12
  45. package/dist/cli/stream-text.js.map +1 -1
  46. package/dist/cli/theme.d.ts +48 -0
  47. package/dist/cli/theme.js +158 -0
  48. package/dist/cli/theme.js.map +1 -0
  49. package/dist/cli/tool-box.d.ts +41 -0
  50. package/dist/cli/tool-box.js +177 -0
  51. package/dist/cli/tool-box.js.map +1 -0
  52. package/dist/cli/transcript.d.ts +9 -0
  53. package/dist/cli/transcript.js +25 -1
  54. package/dist/cli/transcript.js.map +1 -1
  55. package/dist/config/defaults.d.ts +10 -0
  56. package/dist/config/defaults.js +10 -0
  57. package/dist/config/defaults.js.map +1 -1
  58. package/dist/config/load.d.ts +2 -0
  59. package/dist/config/load.js +32 -0
  60. package/dist/config/load.js.map +1 -1
  61. package/dist/config/schema.d.ts +49 -0
  62. package/dist/config/schema.js +31 -0
  63. package/dist/config/schema.js.map +1 -1
  64. package/dist/config/write.d.ts +2 -0
  65. package/dist/config/write.js +9 -0
  66. package/dist/config/write.js.map +1 -1
  67. package/dist/context/builder.d.ts +7 -0
  68. package/dist/context/builder.js +11 -0
  69. package/dist/context/builder.js.map +1 -0
  70. package/dist/extensions/api.d.ts +51 -0
  71. package/dist/extensions/api.js +101 -0
  72. package/dist/extensions/api.js.map +1 -0
  73. package/dist/extensions/discovery.d.ts +26 -0
  74. package/dist/extensions/discovery.js +243 -0
  75. package/dist/extensions/discovery.js.map +1 -0
  76. package/dist/extensions/loader.d.ts +27 -0
  77. package/dist/extensions/loader.js +113 -0
  78. package/dist/extensions/loader.js.map +1 -0
  79. package/dist/extensions/trust.d.ts +4 -0
  80. package/dist/extensions/trust.js +40 -0
  81. package/dist/extensions/trust.js.map +1 -0
  82. package/dist/extensions/types.d.ts +52 -0
  83. package/dist/extensions/types.js +2 -0
  84. package/dist/extensions/types.js.map +1 -0
  85. package/dist/index.d.ts +1 -0
  86. package/dist/index.js +1 -0
  87. package/dist/index.js.map +1 -1
  88. package/dist/mcp/client.d.ts +73 -0
  89. package/dist/mcp/client.js +363 -0
  90. package/dist/mcp/client.js.map +1 -0
  91. package/dist/mcp/format.d.ts +3 -0
  92. package/dist/mcp/format.js +37 -0
  93. package/dist/mcp/format.js.map +1 -0
  94. package/dist/mcp/index.d.ts +6 -0
  95. package/dist/mcp/index.js +7 -0
  96. package/dist/mcp/index.js.map +1 -0
  97. package/dist/mcp/manager.d.ts +53 -0
  98. package/dist/mcp/manager.js +349 -0
  99. package/dist/mcp/manager.js.map +1 -0
  100. package/dist/mcp/stdio-transport.d.ts +26 -0
  101. package/dist/mcp/stdio-transport.js +191 -0
  102. package/dist/mcp/stdio-transport.js.map +1 -0
  103. package/dist/mcp/tools.d.ts +22 -0
  104. package/dist/mcp/tools.js +162 -0
  105. package/dist/mcp/tools.js.map +1 -0
  106. package/dist/mcp/types.d.ts +92 -0
  107. package/dist/mcp/types.js +2 -0
  108. package/dist/mcp/types.js.map +1 -0
  109. package/dist/providers/factory.js +1 -0
  110. package/dist/providers/factory.js.map +1 -1
  111. package/dist/runtime/executor.d.ts +2 -0
  112. package/dist/runtime/executor.js +4 -0
  113. package/dist/runtime/executor.js.map +1 -0
  114. package/dist/runtime/scheduler.js +7 -0
  115. package/dist/runtime/scheduler.js.map +1 -1
  116. package/dist/sessions/events.d.ts +9 -0
  117. package/dist/sessions/events.js +11 -6
  118. package/dist/sessions/events.js.map +1 -1
  119. package/dist/sessions/history.d.ts +18 -0
  120. package/dist/sessions/history.js +76 -0
  121. package/dist/sessions/history.js.map +1 -0
  122. package/dist/sessions/index.d.ts +2 -0
  123. package/dist/sessions/index.js +2 -0
  124. package/dist/sessions/index.js.map +1 -1
  125. package/dist/sessions/search.d.ts +16 -0
  126. package/dist/sessions/search.js +136 -0
  127. package/dist/sessions/search.js.map +1 -0
  128. package/dist/sessions/store.js +5 -1
  129. package/dist/sessions/store.js.map +1 -1
  130. package/dist/tools/edit.js +2 -1
  131. package/dist/tools/edit.js.map +1 -1
  132. package/dist/types.d.ts +2 -1
  133. package/dist/types.js.map +1 -1
  134. 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.0\n");
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 tools = useTools ? [readTool, writeTool, editTool, bashTool, webfetchTool, websearchTool] : [];
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
- const session = sessionOverride
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
- handler: interactive
108
- ? async (req) => {
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
- const transcript = new Transcript();
168
- transcript.add(new Banner());
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: dim(`${provider.id}/${provider.model ?? "default"}`),
197
- tokens: dim("tokens: ?"),
198
- effort: dim("effort: medium"),
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) => `\x1b[7m${s}\x1b[0m`,
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
- const slashCommands = [
222
- { name: "help", description: "show commands" },
223
- { name: "model", description: "search/switch model" },
224
- { name: "provider", description: "add/select provider" },
225
- { name: "session", description: "session info" },
226
- { name: "compact", description: "compact history" },
227
- { name: "clear", description: "clear session" },
228
- { name: "approval", description: "approval mode" },
229
- { name: "doctor", description: "diagnostics" },
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(slashCommands, process.cwd()));
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 = true;
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
- approvals.handler = interactive
255
- ? async (req) => {
256
- appendLine("box: approval");
257
- appendLine(req.command);
258
- appendLine("type y / n / t");
259
- return new Promise((resolve) => {
260
- pendingApproval = (d) => {
261
- pendingApproval = null;
262
- resolve(d);
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.add(new Text(text));
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: dim(`${provider.id}/${currentModelLabel()}`),
287
- tokens: dim(`tokens: ${formatContextUsage(usage.used, usage.window)}`),
288
- effort: dim(`effort: ${effortLevel}`),
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
- if (names.length) {
470
- picker.setStatus(`loading live models from ${names.length} provider${names.length === 1 ? "" : "s"}…`);
471
- refreshModelCache(names, () => {
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(`\x1b[36m${preset.label} API key\x1b[0m`));
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(`\x1b[31m✗ key rejected by ${preset.label}\x1b[0m`);
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(`\x1b[31m✗ could not save key: ${err instanceof Error ? err.message : String(err)}\x1b[0m`);
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
- if (box.live)
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("\x1b[2mThinking...\x1b[0m");
1230
+ thinkingText = new Text(theme.tokens.dim("Thinking..."));
673
1231
  transcript.add(thinkingText);
674
1232
  thinkingBuffer = "";
675
1233
  }
676
1234
  thinkingBuffer += e.text;
677
- const prefixed = thinkingBuffer
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
- const prefixed = thinkingBuffer
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
- const prefixed = thinkingBuffer
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
- const box = new Container();
722
- const header = new Text(`▸ ${e.name} ${JSON.stringify(e.args)}`);
723
- box.addChild(header);
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
- if (!box.live) {
742
- box.live = new Text("");
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.live = null;
760
- last.liveBuffer = "";
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
- input.onSubmit = async (value) => {
818
- const trimmed = value.trim();
819
- input.setText("");
820
- if (pendingApproval) {
821
- const a = trimmed.toLowerCase();
822
- if (a === "y" || a === "yes")
823
- pendingApproval("approve");
824
- else if (a === "t" || a === "trust")
825
- pendingApproval("trust-session");
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
- session.appendMessage({ role: "system", content });
845
- if (typeof session.appendEvent === "function") {
846
- session.appendEvent({ type: "skill_loaded", name: skillName });
1327
+ catch (e) {
1328
+ appendLine(`mcp reconnect failed: ${e instanceof Error ? e.message : String(e)}`);
1329
+ return;
847
1330
  }
848
- appendLine(`Loaded skill: ${skillName}`);
849
- const extra = trimmed.startsWith("/skill ")
850
- ? trimmed.split(/\s+/).slice(2).join(" ").trim()
851
- : trimmed.slice(firstToken.length + 1).trim();
852
- if (extra) {
853
- appendLine(`> ${extra}`);
854
- const controller = new AbortController();
855
- try {
856
- await runtime.runAgentTurn(extra, session, controller.signal);
857
- }
858
- catch (err) {
859
- appendLine(`atom-lite: ${err instanceof Error ? err.message : String(err)}`);
860
- }
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 === "/exit") {
865
- tui.stop();
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
- if (trimmed === "/model" || trimmed.startsWith("/model ")) {
898
- const rest = trimmed.slice("/model".length).trim();
899
- if (!rest) {
900
- openModelPicker();
901
- return;
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
- const knownModels = modelsFor(provider.id);
904
- if (knownModels.includes(rest)) {
905
- switchToModel(provider.id, rest);
906
- return;
1355
+ else if (t.startsWith("--format=")) {
1356
+ format = t.slice("--format=".length);
907
1357
  }
908
- const slash = rest.indexOf("/");
909
- if (slash > 0) {
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 (profileConfig(rest) || getPreset(rest)) {
918
- ensureProfile(rest);
919
- const pcfg = profileConfig(rest);
920
- switchToModel(rest, String(pcfg?.model ?? modelOverride ?? "default"));
921
- return;
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
- if (trimmed === "/provider" || trimmed.startsWith("/provider ")) {
927
- const rest = trimmed.slice("/provider".length).trim();
928
- if (rest)
929
- startProviderSetup(rest);
930
- else
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 (trimmed === "/skills") {
935
- const reg = new (await import("../skills/registry.js")).DefaultSkillRegistry(process.cwd());
936
- const skills = reg.list();
937
- if (skills.length === 0) {
938
- appendLine("no skills available");
939
- return;
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
- appendLine(skills.map((sk) => `${sk.name} - ${sk.description}`).join("\n"));
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
- if (trimmed.startsWith("/skill ")) {
945
- const name = trimmed.split(/\s+/)[1];
946
- const reg = new (await import("../skills/registry.js")).DefaultSkillRegistry(process.cwd());
947
- const content = reg.load(name);
948
- if (!content) {
949
- appendLine(`skill not found: ${name}`);
950
- return;
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
- session.appendMessage({ role: "system", content });
953
- if (typeof session.appendEvent === "function") {
954
- session.appendEvent({ type: "skill_loaded", name });
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
- appendLine(`Loaded skill: ${name}`);
957
- const extra = trimmed.split(/\s+/).slice(2).join(" ").trim();
958
- if (extra) {
959
- appendLine(`> ${extra}`);
960
- const controller = new AbortController();
961
- try {
962
- await runtime.runAgentTurn(extra, session, controller.signal);
963
- }
964
- catch (err) {
965
- appendLine(`atom-lite: ${err instanceof Error ? err.message : String(err)}`);
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
- if (trimmed === "/thinking") {
971
- showThinking = !showThinking;
972
- appendLine(`thinking display: ${showThinking ? "shown" : "hidden"}`);
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
- if (trimmed.startsWith("/effort")) {
976
- const parts = trimmed.split(/\s+/);
977
- if (parts.length >= 2) {
978
- const lvl = parts[1];
979
- if (lvl === "low" || lvl === "medium" || lvl === "high") {
980
- effortLevel = lvl;
981
- runtime.setEffort(lvl);
982
- updateStatus();
983
- appendLine(`effort set to ${effortLevel}`);
984
- }
985
- else {
986
- appendLine("usage: /effort low|medium|high");
987
- }
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
- appendLine(`> ${trimmed}`);
1034
- const controller = new AbortController();
1035
- try {
1036
- await runtime.runAgentTurn(trimmed, session, controller.signal);
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
- ? async (req) => {
1045
- appendLine("┌─ approval ─────────────────────────────");
1046
- appendLine(`│ ${req.command}`);
1047
- appendLine(`│ cwd: ${req.cwd} timeout: ${req.timeoutMs}ms sandboxed: ${req.sandboxed}`);
1048
- appendLine("│ [y]es / [n]o / [t]rust session");
1049
- appendLine("└────────────────────────────────────────");
1050
- return new Promise((resolve) => {
1051
- pendingApproval = (d) => {
1052
- pendingApproval = null;
1053
- resolve(d);
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