atom-lite 0.4.1 → 0.6.2

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