atom-lite 0.5.1 → 0.6.3

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 (105) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/README.md +60 -116
  3. package/dist/agent/loop.d.ts +16 -0
  4. package/dist/agent/loop.js +158 -11
  5. package/dist/agent/loop.js.map +1 -1
  6. package/dist/agent/system-prompt.d.ts +1 -0
  7. package/dist/agent/system-prompt.js +23 -0
  8. package/dist/agent/system-prompt.js.map +1 -0
  9. package/dist/agent/turn-recorder.d.ts +4 -0
  10. package/dist/agent/turn-recorder.js +1 -0
  11. package/dist/agent/turn-recorder.js.map +1 -1
  12. package/dist/agent/types.d.ts +33 -0
  13. package/dist/cli/app.js +337 -55
  14. package/dist/cli/app.js.map +1 -1
  15. package/dist/cli/commands.js +180 -0
  16. package/dist/cli/commands.js.map +1 -1
  17. package/dist/cli/diff-view.d.ts +18 -0
  18. package/dist/cli/diff-view.js +66 -0
  19. package/dist/cli/diff-view.js.map +1 -0
  20. package/dist/cli/main.js +2 -1
  21. package/dist/cli/main.js.map +1 -1
  22. package/dist/cli/panel.js +8 -8
  23. package/dist/cli/panel.js.map +1 -1
  24. package/dist/cli/status-line.d.ts +2 -0
  25. package/dist/cli/status-line.js +9 -1
  26. package/dist/cli/status-line.js.map +1 -1
  27. package/dist/cli/tool-box.d.ts +1 -1
  28. package/dist/cli/tool-box.js +47 -41
  29. package/dist/cli/tool-box.js.map +1 -1
  30. package/dist/config/defaults.d.ts +4 -5
  31. package/dist/config/defaults.js +6 -3
  32. package/dist/config/defaults.js.map +1 -1
  33. package/dist/config/load.js +29 -3
  34. package/dist/config/load.js.map +1 -1
  35. package/dist/config/schema.d.ts +14 -14
  36. package/dist/config/schema.js +8 -5
  37. package/dist/config/schema.js.map +1 -1
  38. package/dist/context/budget.js +19 -1
  39. package/dist/context/budget.js.map +1 -1
  40. package/dist/context/manager.js +1 -1
  41. package/dist/context/manager.js.map +1 -1
  42. package/dist/eval/runner.d.ts +51 -0
  43. package/dist/eval/runner.js +230 -0
  44. package/dist/eval/runner.js.map +1 -0
  45. package/dist/mcp/client.js +2 -1
  46. package/dist/mcp/client.js.map +1 -1
  47. package/dist/providers/context-window.d.ts +4 -0
  48. package/dist/providers/context-window.js +54 -0
  49. package/dist/providers/context-window.js.map +1 -1
  50. package/dist/providers/factory.js +3 -0
  51. package/dist/providers/factory.js.map +1 -1
  52. package/dist/providers/gemini.d.ts +2 -0
  53. package/dist/providers/gemini.js +18 -11
  54. package/dist/providers/gemini.js.map +1 -1
  55. package/dist/providers/ollama.d.ts +3 -0
  56. package/dist/providers/ollama.js +9 -0
  57. package/dist/providers/ollama.js.map +1 -1
  58. package/dist/providers/openai-compatible.d.ts +2 -0
  59. package/dist/providers/openai-compatible.js +24 -3
  60. package/dist/providers/openai-compatible.js.map +1 -1
  61. package/dist/runtime/background-tasks.d.ts +23 -0
  62. package/dist/runtime/background-tasks.js +141 -0
  63. package/dist/runtime/background-tasks.js.map +1 -0
  64. package/dist/runtime/checkpoint-store.d.ts +64 -0
  65. package/dist/runtime/checkpoint-store.js +419 -0
  66. package/dist/runtime/checkpoint-store.js.map +1 -0
  67. package/dist/runtime/modes.d.ts +3 -0
  68. package/dist/runtime/modes.js +8 -0
  69. package/dist/runtime/modes.js.map +1 -0
  70. package/dist/runtime/tool-executor.js +10 -0
  71. package/dist/runtime/tool-executor.js.map +1 -1
  72. package/dist/skills/registry.js +8 -1
  73. package/dist/skills/registry.js.map +1 -1
  74. package/dist/tools/bash.js +46 -10
  75. package/dist/tools/bash.js.map +1 -1
  76. package/dist/tools/edit.js +28 -8
  77. package/dist/tools/edit.js.map +1 -1
  78. package/dist/tools/glob.d.ts +2 -0
  79. package/dist/tools/glob.js +97 -0
  80. package/dist/tools/glob.js.map +1 -0
  81. package/dist/tools/grep.d.ts +2 -0
  82. package/dist/tools/grep.js +290 -0
  83. package/dist/tools/grep.js.map +1 -0
  84. package/dist/tools/index.d.ts +3 -0
  85. package/dist/tools/index.js +3 -0
  86. package/dist/tools/index.js.map +1 -1
  87. package/dist/tools/read.js +70 -9
  88. package/dist/tools/read.js.map +1 -1
  89. package/dist/tools/task.d.ts +2 -0
  90. package/dist/tools/task.js +55 -0
  91. package/dist/tools/task.js.map +1 -0
  92. package/dist/tools/types.d.ts +10 -1
  93. package/dist/tools/write.js +29 -8
  94. package/dist/tools/write.js.map +1 -1
  95. package/dist/types.d.ts +9 -3
  96. package/dist/types.js +1 -4
  97. package/dist/types.js.map +1 -1
  98. package/dist/version.d.ts +1 -0
  99. package/dist/version.js +14 -0
  100. package/dist/version.js.map +1 -0
  101. package/package.json +5 -3
  102. package/skills/atom-lite-extensions/SKILL.md +240 -0
  103. package/skills/caveman/SKILL.md +89 -0
  104. package/skills/code-review/SKILL.md +46 -0
  105. package/skills/optimization/SKILL.md +51 -0
package/dist/cli/app.js CHANGED
@@ -1,13 +1,15 @@
1
1
  import { loadConfig } from "../config/load.js";
2
+ import { DEFAULT_SYSTEM_PROMPT } from "../agent/system-prompt.js";
2
3
  import { defaultAgentDir, userConfigDir } from "../config/paths.js";
3
4
  import { saveProviderSelection, saveTheme, } from "../config/write.js";
4
5
  import { resolveApiKey, saveApiKey } from "../config/credentials.js";
5
6
  import { join } from "node:path";
6
7
  import { writeFileSync, existsSync } from "node:fs";
7
- import { execSync } from "node:child_process";
8
+ import { execSync, spawn } from "node:child_process";
9
+ import { defaultShell, terminateProcessTree } from "../security/process.js";
8
10
  import { openSearchablePicker } from "./picker.js";
9
11
  import { openCommandPalette } from "./palette.js";
10
- import { bareCommandName, createCommandRegistry } from "./commands.js";
12
+ import { bareCommandName, createCommandRegistry, } from "./commands.js";
11
13
  import { resolveTheme, themeLabel, themeNames, markdownThemeFor, } from "./theme.js";
12
14
  import { SecretInput } from "./secret-input.js";
13
15
  import { StatusLine } from "./status-line.js";
@@ -15,7 +17,7 @@ import { Activity } from "./activity.js";
15
17
  import { ToolBox } from "./tool-box.js";
16
18
  import { ApprovalPrompt } from "./approval-prompt.js";
17
19
  import { Banner } from "./banner.js";
18
- import { formatContextUsage, knownContextWindow, } from "../providers/context-window.js";
20
+ import { formatContextUsage, knownContextWindow, knownVisionModel, } from "../providers/context-window.js";
19
21
  import { StreamText } from "./stream-text.js";
20
22
  import { Transcript } from "./transcript.js";
21
23
  import { AgentRuntime } from "../agent/loop.js";
@@ -27,8 +29,8 @@ import { ProjectTrustPrompt } from "./trust-prompt.js";
27
29
  import { formatSessionList, formatSessionSearch, searchSessions, sessionTitleFromEvents, } from "../sessions/search.js";
28
30
  import { providerSwitchNotice, sessionHistoryLines, sessionSelectionFromEvents, } from "../sessions/history.js";
29
31
  import { blockAtLine, createMouseCapture, handleMouseEvent, parseSgrMouse, shouldEnableMouse, } from "./mouse.js";
30
- import { readTool, writeTool, editTool, bashTool, webfetchTool, websearchTool } from "../tools/index.js";
31
- import { McpManager, formatMcpStatus, formatMcpDoctor, createMcpTools } from "../mcp/index.js";
32
+ import { readTool, writeTool, editTool, bashTool, webfetchTool, websearchTool, grepTool, globTool, taskTool, } from "../tools/index.js";
33
+ import { McpManager, formatMcpStatus, formatMcpDoctor, createMcpTools, } from "../mcp/index.js";
32
34
  import { createProvider } from "../providers/factory.js";
33
35
  import { availableProviderNames, getPreset, listModelsForProvider, materializeProviderConfig, probeApiKey, providerLabel, PROVIDER_PRESETS, } from "../providers/catalog.js";
34
36
  import { ContextManager } from "../context/manager.js";
@@ -89,7 +91,8 @@ export async function run(args) {
89
91
  }
90
92
  }
91
93
  if (showVersion) {
92
- process.stdout.write("0.3.1\n");
94
+ const { VERSION } = await import("../version.js");
95
+ process.stdout.write(VERSION + "\n");
93
96
  return;
94
97
  }
95
98
  if (listSessions || searchSessionsQuery !== undefined) {
@@ -124,12 +127,23 @@ export async function run(args) {
124
127
  modelByProvider.set(provider.id, modelOverride);
125
128
  const useTools = !positional.includes("__notools__");
126
129
  const builtInTools = useTools
127
- ? [readTool, writeTool, editTool, bashTool, webfetchTool, websearchTool]
130
+ ? [
131
+ readTool,
132
+ writeTool,
133
+ editTool,
134
+ bashTool,
135
+ webfetchTool,
136
+ websearchTool,
137
+ grepTool,
138
+ globTool,
139
+ taskTool,
140
+ ]
128
141
  : [];
129
142
  const interactive = process.stdin.isTTY === true && !jsonMode;
130
143
  // one-shot task from positional or stdin
131
144
  let stdinTask = "";
132
- if (!process.stdin.isTTY && positional.filter((p) => !p.startsWith("__")).length === 0) {
145
+ if (!process.stdin.isTTY &&
146
+ positional.filter((p) => !p.startsWith("__")).length === 0) {
133
147
  const chunks = [];
134
148
  for await (const chunk of process.stdin)
135
149
  chunks.push(chunk);
@@ -139,9 +153,11 @@ export async function run(args) {
139
153
  const oneShot = (taskArgs.join(" ").trim() || stdinTask).trim();
140
154
  const store = new JsonlSessionStore();
141
155
  let session = sessionOverride
142
- ? store.resume(sessionOverride) ?? store.create(process.cwd(), provider.id)
156
+ ? (store.resume(sessionOverride) ??
157
+ store.create(process.cwd(), provider.id))
143
158
  : positional.includes("__continue__")
144
- ? store.resumeLatest(process.cwd()) ?? store.create(process.cwd(), provider.id)
159
+ ? (store.resumeLatest(process.cwd()) ??
160
+ store.create(process.cwd(), provider.id))
145
161
  : store.create(process.cwd(), provider.id);
146
162
  const mcpServers = (config.mcpServers ?? {});
147
163
  const mcpManager = new McpManager({
@@ -164,7 +180,9 @@ export async function run(args) {
164
180
  try {
165
181
  const { execFileSync } = require("node:child_process");
166
182
  const parts = value.slice(1).trim().split(/\s+/);
167
- return execFileSync(parts[0], parts.slice(1), { encoding: "utf8" }).trim();
183
+ return execFileSync(parts[0], parts.slice(1), {
184
+ encoding: "utf8",
185
+ }).trim();
168
186
  }
169
187
  catch {
170
188
  return undefined;
@@ -202,7 +220,7 @@ export async function run(args) {
202
220
  // Bridge Pi-style extension providers into Atom-lite's config so they
203
221
  // appear in /provider, /model, and the command palette.
204
222
  for (const loaded of extensionLoader.list()) {
205
- for (const [name, provider] of (loaded.registries?.providers ?? new Map())) {
223
+ for (const [name, provider] of loaded.registries?.providers ?? new Map()) {
206
224
  const cfg = provider?.config ?? {};
207
225
  const firstModel = Array.isArray(cfg.models) && cfg.models.length > 0
208
226
  ? (cfg.models[0].id ?? cfg.models[0].name ?? "default")
@@ -244,7 +262,7 @@ export async function run(args) {
244
262
  return results;
245
263
  };
246
264
  const contextManager = new ContextManager({
247
- systemPrompt: "You are a coding assistant.",
265
+ systemPrompt: DEFAULT_SYSTEM_PROMPT,
248
266
  maxInputTokens: config.context.maxInputTokens ?? null,
249
267
  reserveOutputTokens: config.context.reserveOutputTokens,
250
268
  compactAtPercent: config.context.compactAtPercent,
@@ -256,7 +274,7 @@ export async function run(args) {
256
274
  });
257
275
  let sessionTrusted = false;
258
276
  const approvals = {
259
- mode: config.runtime.approvalMode ?? "ask",
277
+ mode: "ask",
260
278
  // Installed below once the interactive UI helpers exist.
261
279
  handler: undefined,
262
280
  isTrusted: () => sessionTrusted,
@@ -271,12 +289,20 @@ export async function run(args) {
271
289
  workspace: process.cwd(),
272
290
  tools: allTools,
273
291
  approvals,
292
+ mode: config.runtime.mode,
293
+ maxTokensPerSession: config.runtime.maxTokensPerSession,
294
+ maxTokensPerTurn: config.runtime.maxTokensPerTurn,
274
295
  contextManager,
275
296
  extensionDispatch,
276
297
  bashConcurrency: config.runtime.bashConcurrency,
298
+ checkpointingEnabled: interactive || Boolean(config.runtime.checkpointing),
277
299
  });
278
300
  mcpManager.onToolsChanged = () => {
279
- runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
301
+ runtime.setTools([
302
+ ...builtInTools,
303
+ ...extensionTools,
304
+ ...createMcpTools(mcpManager),
305
+ ]);
280
306
  };
281
307
  if (oneShot || !interactive) {
282
308
  // non-interactive / one-shot path
@@ -286,7 +312,11 @@ export async function run(args) {
286
312
  try {
287
313
  if (useTools) {
288
314
  await mcpManager.syncTools();
289
- runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
315
+ runtime.setTools([
316
+ ...builtInTools,
317
+ ...extensionTools,
318
+ ...createMcpTools(mcpManager),
319
+ ]);
290
320
  }
291
321
  const result = await runtime.runAgentTurn(oneShot || " ", session, controller.signal);
292
322
  if (jsonMode) {
@@ -298,7 +328,9 @@ export async function run(args) {
298
328
  }
299
329
  catch (err) {
300
330
  if (jsonMode)
301
- process.stderr.write(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }) + "\n");
331
+ process.stderr.write(JSON.stringify({
332
+ error: err instanceof Error ? err.message : String(err),
333
+ }) + "\n");
302
334
  else
303
335
  process.stderr.write(`atom-lite: ${err instanceof Error ? err.message : String(err)}\n`);
304
336
  process.exit(1);
@@ -310,14 +342,16 @@ export async function run(args) {
310
342
  return;
311
343
  }
312
344
  // ---- Interactive TUI (first-class) ----
313
- const { TuiAltScreen, Text, Input, Editor, Container, Spacer, ScrollView, VStack, Loader, Markdown, Box } = await import("@earendil-works/pi-tui");
345
+ const { TuiAltScreen, Text, Input, Editor, Container, Spacer, ScrollView, VStack, Loader, Markdown, Box, } = await import("@earendil-works/pi-tui");
314
346
  const mouseEnabled = shouldEnableMouse({
315
347
  interactive,
316
348
  configured: config.ui.mouse,
317
349
  stdoutIsTTY: process.stdout.isTTY === true,
318
350
  });
319
351
  const terminal = new ProcessTerminal();
320
- const tui = new TuiAltScreen(terminal, false, undefined, { mouse: mouseEnabled });
352
+ const tui = new TuiAltScreen(terminal, false, undefined, {
353
+ mouse: mouseEnabled,
354
+ });
321
355
  const mouseCapture = createMouseCapture((data) => terminal.write(data));
322
356
  let effortLevel = "medium";
323
357
  runtime.setEffort(effortLevel);
@@ -345,9 +379,17 @@ export async function run(args) {
345
379
  displayPath = "~" + cwd.slice(home.length);
346
380
  let branch = "";
347
381
  try {
348
- branch = execSync("git rev-parse --abbrev-ref HEAD", { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
382
+ branch = execSync("git rev-parse --abbrev-ref HEAD", {
383
+ cwd,
384
+ encoding: "utf8",
385
+ stdio: ["ignore", "pipe", "ignore"],
386
+ }).trim();
349
387
  if (branch === "HEAD") {
350
- branch = execSync("git rev-parse --short HEAD", { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
388
+ branch = execSync("git rev-parse --short HEAD", {
389
+ cwd,
390
+ encoding: "utf8",
391
+ stdio: ["ignore", "pipe", "ignore"],
392
+ }).trim();
351
393
  }
352
394
  }
353
395
  catch {
@@ -359,10 +401,12 @@ export async function run(args) {
359
401
  const header = new Text(theme.tokens.dim(headerLabel()), 1, 0);
360
402
  const statusBar = new StatusLine(theme);
361
403
  const activity = new Activity({ theme, onChange: () => updateStatus() });
404
+ let currentMode = config.runtime.mode;
362
405
  statusBar.setParts({
363
406
  left: `${provider.id}/${provider.model ?? "default"}`,
364
407
  tokens: "tokens: ?",
365
408
  effort: "effort: medium",
409
+ mode: currentMode,
366
410
  activity: activity.text(),
367
411
  });
368
412
  const selectListTheme = {
@@ -395,20 +439,65 @@ export async function run(args) {
395
439
  const sig = skillCacheSignature(process.cwd());
396
440
  if (sig === registrySkillSignature)
397
441
  return;
398
- commandRegistry = createCommandRegistry({ skills: cachedSkillRegistry().list() });
442
+ commandRegistry = createCommandRegistry({
443
+ skills: cachedSkillRegistry().list(),
444
+ });
399
445
  registrySkillSignature = sig;
400
- input.setAutocompleteProvider(new CombinedAutocompleteProvider(commandRegistry.autocomplete(), process.cwd()));
446
+ input.setAutocompleteProvider(new CombinedAutocompleteProvider(commandRegistry.autocomplete(), process.cwd(), fdPath));
401
447
  }
402
- input.setAutocompleteProvider(new CombinedAutocompleteProvider(commandRegistry.autocomplete(), process.cwd()));
448
+ const fdPath = (() => {
449
+ for (const bin of ["fd", "fdfind"]) {
450
+ try {
451
+ const found = execSync(`command -v ${bin}`, {
452
+ encoding: "utf8",
453
+ stdio: ["ignore", "pipe", "ignore"],
454
+ }).trim();
455
+ if (found)
456
+ return found;
457
+ }
458
+ catch {
459
+ // ignore
460
+ }
461
+ }
462
+ return null;
463
+ })();
464
+ input.setAutocompleteProvider(new CombinedAutocompleteProvider(commandRegistry.autocomplete(), process.cwd(), fdPath));
465
+ // Skills should autocomplete (fill the input) rather than execute immediately
466
+ // when selected from the autocomplete list; Enter on a typed command still
467
+ // runs them. Convert a confirming Enter into a Tab completion for skills.
468
+ const originalHandleInput = input.handleInput.bind(input);
469
+ input.handleInput = (data) => {
470
+ try {
471
+ if ((data === "\r" || data === "\n") &&
472
+ input.autocompleteState &&
473
+ input.autocompleteList) {
474
+ const selected = input.autocompleteList.getSelectedItem?.();
475
+ const name = selected?.value ?? selected?.name ?? selected?.label;
476
+ const cmd = name
477
+ ? commandRegistry.get(String(name).replace(/^\//, ""))
478
+ : undefined;
479
+ if (cmd?.bareInvokable) {
480
+ originalHandleInput("\t");
481
+ return;
482
+ }
483
+ }
484
+ }
485
+ catch {
486
+ // Fall through to default handling on any introspection failure.
487
+ }
488
+ originalHandleInput(data);
489
+ };
403
490
  const root = new VStack([]);
404
- root.addChild(output, { grow: 1 });
405
- root.addChild(new Spacer(1));
406
- root.addChild(input);
407
- root.addChild(statusBar);
491
+ // Keep fixed chrome (spacer, input, status, header, dropdown) from being
492
+ // collapsed when the transcript grows; only the output region may shrink.
493
+ root.addChild(output, { grow: 1, shrink: 1, minSize: 0 });
494
+ root.addChild(new Spacer(1), { grow: 0, shrink: 0, minSize: 1 });
495
+ root.addChild(input, { grow: 0, shrink: 0 });
496
+ root.addChild(statusBar, { grow: 0, shrink: 0 });
408
497
  // folder:branch info below the status line, dimmed
409
- root.addChild(header);
498
+ root.addChild(header, { grow: 0, shrink: 0 });
410
499
  const dropdownContainer = new Container();
411
- root.addChild(dropdownContainer);
500
+ root.addChild(dropdownContainer, { grow: 0, shrink: 0 });
412
501
  tui.setLayoutRoot(root);
413
502
  tui.setFocus(input);
414
503
  if (!noColor) {
@@ -536,6 +625,7 @@ export async function run(args) {
536
625
  streamText.finalize();
537
626
  }
538
627
  let activeTurnController = null;
628
+ let shellMode = false;
539
629
  const messageQueue = [];
540
630
  const steeringQueue = [];
541
631
  runtime.setSteeringQueue(steeringQueue);
@@ -547,9 +637,17 @@ export async function run(args) {
547
637
  try {
548
638
  if (useTools) {
549
639
  await mcpManager.syncTools();
550
- runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
640
+ runtime.setTools([
641
+ ...builtInTools,
642
+ ...extensionTools,
643
+ ...createMcpTools(mcpManager),
644
+ ]);
645
+ }
646
+ const turnResult = await runtime.runAgentTurn(prompt, session, controller.signal);
647
+ if (turnResult === "TOKEN_BUDGET_REACHED") {
648
+ appendLine("token budget reached; stopping", theme.tokens.warning);
649
+ tui.requestRender();
551
650
  }
552
- await runtime.runAgentTurn(prompt, session, controller.signal);
553
651
  }
554
652
  catch (err) {
555
653
  if (err instanceof Error && err.message === "USER_CANCELLED") {
@@ -569,12 +667,94 @@ export async function run(args) {
569
667
  }
570
668
  }
571
669
  }
670
+ /** Run a shell command directly (shell mode / `!` prefix). */
671
+ async function runShellCommand(command) {
672
+ const shell = defaultShell();
673
+ const flag = process.platform === "win32" ? "/c" : "-c";
674
+ const start = Date.now();
675
+ const cap = 30000;
676
+ let stdout = "";
677
+ let stderr = "";
678
+ let truncated = false;
679
+ const append = (target, chunk) => {
680
+ const s = chunk.toString("utf8");
681
+ let accepted = s;
682
+ if (target === "out") {
683
+ if (stdout.length + s.length > cap) {
684
+ truncated = true;
685
+ accepted = s.slice(0, Math.max(0, cap - stdout.length));
686
+ stdout += accepted;
687
+ }
688
+ else {
689
+ stdout += s;
690
+ }
691
+ }
692
+ else {
693
+ if (stderr.length + s.length > cap) {
694
+ truncated = true;
695
+ accepted = s.slice(0, Math.max(0, cap - stderr.length));
696
+ stderr += accepted;
697
+ }
698
+ else {
699
+ stderr += s;
700
+ }
701
+ }
702
+ if (accepted) {
703
+ tui.requestRender();
704
+ }
705
+ };
706
+ try {
707
+ const child = spawn(shell, [flag, command], {
708
+ cwd: process.cwd(),
709
+ env: process.env,
710
+ detached: process.platform !== "win32",
711
+ });
712
+ child.stdout?.on("data", (c) => append("out", c));
713
+ child.stderr?.on("data", (c) => append("err", c));
714
+ const exit = await new Promise((resolve) => {
715
+ const timer = setTimeout(() => {
716
+ terminateProcessTree(child, "SIGTERM");
717
+ setTimeout(() => terminateProcessTree(child, "SIGKILL"), 1000).unref();
718
+ resolve({ code: null, signal: "SIGTERM" });
719
+ }, 120000);
720
+ timer.unref();
721
+ child.on("exit", (code, signal) => {
722
+ clearTimeout(timer);
723
+ resolve({ code, signal });
724
+ });
725
+ child.on("error", (err) => {
726
+ clearTimeout(timer);
727
+ resolve({ code: null, signal: null });
728
+ stderr = `COMMAND_FAILED: ${err instanceof Error ? err.message : String(err)}`;
729
+ });
730
+ });
731
+ const parts = [];
732
+ parts.push(`exit_code: ${exit.code ?? "null"}`);
733
+ if (exit.signal)
734
+ parts.push(`signal: ${exit.signal}`);
735
+ if (truncated)
736
+ parts.push("OUTPUT_TRUNCATED");
737
+ parts.push(`duration_ms: ${Date.now() - start}`);
738
+ if (stdout)
739
+ parts.push(`stdout:\n${stdout}`);
740
+ if (stderr)
741
+ parts.push(`stderr:\n${stderr}`);
742
+ const content = parts.join("\n");
743
+ appendLine(content, exit.code === 0 && !exit.signal && !stderr
744
+ ? theme.tokens.success
745
+ : theme.tokens.error);
746
+ }
747
+ catch (err) {
748
+ appendLine(`shell error: ${err instanceof Error ? err.message : String(err)}`, theme.tokens.error);
749
+ }
750
+ }
572
751
  function updateStatus() {
573
752
  const usage = runtime.getContextUsage();
574
753
  statusBar.setParts({
575
754
  left: `${provider.id}/${currentModelLabel()}`,
576
755
  tokens: `tokens: ${formatContextUsage(usage.used, usage.window)}`,
577
756
  effort: `effort: ${effortLevel}`,
757
+ mode: currentMode,
578
758
  activity: activity.text(),
579
759
  });
580
760
  tui.requestRender();
@@ -612,11 +792,21 @@ export async function run(args) {
612
792
  function refreshContextWindow() {
613
793
  const profile = provider.id;
614
794
  const model = currentModelLabel();
615
- const configured = profileConfig(profile)?.contextWindow;
616
- const window = typeof configured === "number" && configured > 0
617
- ? configured
795
+ const pcfg = profileConfig(profile);
796
+ const configuredWindow = pcfg?.contextWindow;
797
+ const window = typeof configuredWindow === "number" && configuredWindow > 0
798
+ ? configuredWindow
618
799
  : knownContextWindow(model);
619
800
  runtime.setContextWindow(window);
801
+ // Config override for vision takes precedence over provider metadata.
802
+ if (typeof pcfg?.supportsVision === "boolean") {
803
+ runtime.setSupportsVision(pcfg.supportsVision);
804
+ }
805
+ else {
806
+ const guessed = knownVisionModel(model);
807
+ if (typeof guessed === "boolean")
808
+ runtime.setSupportsVision(guessed);
809
+ }
620
810
  updateStatus();
621
811
  const getModelInfo = provider.getModelInfo;
622
812
  if (typeof getModelInfo !== "function")
@@ -627,10 +817,16 @@ export async function run(args) {
627
817
  if (provider.id !== profile || currentModelLabel() !== model)
628
818
  return;
629
819
  const resolved = info?.contextWindow;
630
- if (typeof resolved === "number" && resolved > 0) {
820
+ if (typeof resolved === "number" &&
821
+ resolved > 0 &&
822
+ typeof configuredWindow !== "number") {
631
823
  runtime.setContextWindow(resolved);
632
824
  updateStatus();
633
825
  }
826
+ if (typeof pcfg?.supportsVision !== "boolean" &&
827
+ typeof info?.supportsVision === "boolean") {
828
+ runtime.setSupportsVision(info.supportsVision);
829
+ }
634
830
  })
635
831
  .catch(() => {
636
832
  // Keep the static/config fallback.
@@ -641,7 +837,7 @@ export async function run(args) {
641
837
  const MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
642
838
  const modelCache = new Map();
643
839
  for (const loaded of extensionLoader.list()) {
644
- for (const [name, provider] of (loaded.registries?.providers ?? new Map())) {
840
+ for (const [name, provider] of loaded.registries?.providers ?? new Map()) {
645
841
  const cfg = provider?.config ?? {};
646
842
  const models = Array.isArray(cfg.models)
647
843
  ? cfg.models.map((m) => m.id ?? m.name).filter(Boolean)
@@ -877,7 +1073,11 @@ export async function run(args) {
877
1073
  provider = createProvider(config, preset.id, chosen);
878
1074
  runtime.switchModel(provider, chosen);
879
1075
  if (typeof session.appendEvent === "function") {
880
- session.appendEvent({ type: "provider_selected", provider: preset.id, model: chosen });
1076
+ session.appendEvent({
1077
+ type: "provider_selected",
1078
+ provider: preset.id,
1079
+ model: chosen,
1080
+ });
881
1081
  }
882
1082
  refreshContextWindow();
883
1083
  appendLine(`provider: ${preset.label} (${preset.id}) · model: ${chosen}`);
@@ -1085,13 +1285,15 @@ export async function run(args) {
1085
1285
  appendLine(`skill not found: ${name}`);
1086
1286
  return false;
1087
1287
  }
1288
+ if (extra) {
1289
+ appendLine(`> ${extra}`, theme.tokens.user);
1290
+ }
1088
1291
  session.appendMessage({ role: "system", content });
1089
1292
  if (typeof session.appendEvent === "function") {
1090
1293
  session.appendEvent({ type: "skill_loaded", name });
1091
1294
  }
1092
- appendLine(`Loaded skill: ${name}`);
1295
+ appendLine(`Loaded skill: ${name}`, theme.tokens.dim);
1093
1296
  if (extra) {
1094
- appendLine(`> ${extra}`);
1095
1297
  await runTurn(extra);
1096
1298
  }
1097
1299
  return true;
@@ -1126,7 +1328,9 @@ export async function run(args) {
1126
1328
  return "No extensions discovered.";
1127
1329
  return report
1128
1330
  .map((ext) => {
1129
- const origin = ext.origin ? `${ext.source}:${ext.origin}` : ext.source;
1331
+ const origin = ext.origin
1332
+ ? `${ext.source}:${ext.origin}`
1333
+ : ext.source;
1130
1334
  const version = ext.version ? `v${ext.version}` : "-";
1131
1335
  const counts = `${ext.toolCount} tools / ${ext.commandCount} commands`;
1132
1336
  const error = ext.error ? `\t${ext.error}` : "";
@@ -1144,7 +1348,9 @@ export async function run(args) {
1144
1348
  return showThinking;
1145
1349
  },
1146
1350
  setTheme: setThemeTarget,
1147
- listSkills: () => cachedSkillRegistry().list().map((skill) => ({
1351
+ listSkills: () => cachedSkillRegistry()
1352
+ .list()
1353
+ .map((skill) => ({
1148
1354
  name: skill.name,
1149
1355
  description: skill.description,
1150
1356
  })),
@@ -1211,9 +1417,7 @@ export async function run(args) {
1211
1417
  const summaries = scope === "all" ? store.list() : store.list(process.cwd());
1212
1418
  const toggle = {
1213
1419
  value: "__scope__",
1214
- label: scope === "all"
1215
- ? "Show this workspace only"
1216
- : "Show all workspaces",
1420
+ label: scope === "all" ? "Show this workspace only" : "Show all workspaces",
1217
1421
  description: `scope: ${scope === "all" ? "all workspaces" : process.cwd()}`,
1218
1422
  };
1219
1423
  return [
@@ -1237,7 +1441,9 @@ export async function run(args) {
1237
1441
  if (item.value === "__scope__") {
1238
1442
  scope = scope === "all" ? "workspace" : "all";
1239
1443
  picker.updateItems(buildItems());
1240
- picker.setStatus(scope === "all" ? "showing all workspaces" : "showing this workspace");
1444
+ picker.setStatus(scope === "all"
1445
+ ? "showing all workspaces"
1446
+ : "showing this workspace");
1241
1447
  return;
1242
1448
  }
1243
1449
  picker.close();
@@ -1359,6 +1565,7 @@ export async function run(args) {
1359
1565
  for (const box of toolBoxes) {
1360
1566
  box.setExpanded(expandTools);
1361
1567
  }
1568
+ transcript.invalidate();
1362
1569
  tui.requestRender();
1363
1570
  }
1364
1571
  async function reloadExtensions() {
@@ -1393,7 +1600,8 @@ export async function run(args) {
1393
1600
  }
1394
1601
  extensionProviderNames.clear();
1395
1602
  for (const loaded of extensionLoader.list()) {
1396
- for (const [name, provider] of (loaded.registries?.providers ?? new Map())) {
1603
+ for (const [name, provider] of loaded.registries?.providers ??
1604
+ new Map()) {
1397
1605
  const cfg = provider?.config ?? {};
1398
1606
  const firstModel = Array.isArray(cfg.models) && cfg.models.length > 0
1399
1607
  ? (cfg.models[0].id ?? cfg.models[0].name ?? "default")
@@ -1482,6 +1690,7 @@ export async function run(args) {
1482
1690
  if (!(child instanceof ToolBox))
1483
1691
  return false;
1484
1692
  child.toggleExpanded();
1693
+ transcript.invalidate();
1485
1694
  return true;
1486
1695
  }
1487
1696
  tui.addInputListener((data) => {
@@ -1516,11 +1725,28 @@ export async function run(args) {
1516
1725
  toggleTools();
1517
1726
  return { consume: true };
1518
1727
  }
1519
- // Escape cancels the active turn.
1520
- if (matchesKey(data, Key.escape) && !uiModalOpen && activeTurnController) {
1521
- activeTurnController.abort();
1728
+ // Tab toggles plan/yolo mode.
1729
+ if (matchesKey(data, Key.tab) && !uiModalOpen) {
1730
+ currentMode = currentMode === "plan" ? "yolo" : "plan";
1731
+ runtime.setMode(currentMode);
1732
+ updateStatus();
1733
+ appendLine(`mode: ${currentMode}`, theme.tokens.dim);
1734
+ tui.requestRender();
1522
1735
  return { consume: true };
1523
1736
  }
1737
+ // Escape cancels the active turn, or exits shell mode.
1738
+ if (matchesKey(data, Key.escape) && !uiModalOpen) {
1739
+ if (activeTurnController) {
1740
+ activeTurnController.abort();
1741
+ return { consume: true };
1742
+ }
1743
+ if (shellMode) {
1744
+ shellMode = false;
1745
+ appendLine("shell mode: off", theme.tokens.dim);
1746
+ tui.requestRender();
1747
+ return { consume: true };
1748
+ }
1749
+ }
1524
1750
  return undefined;
1525
1751
  });
1526
1752
  runtime["onEvent"] = (e) => {
@@ -1560,10 +1786,34 @@ export async function run(args) {
1560
1786
  else if (e.type === "context_usage") {
1561
1787
  updateStatus();
1562
1788
  }
1789
+ else if (e.type === "background_task_complete") {
1790
+ appendLine(`background task ${e.id}: ${e.status} (exit ${e.exitCode ?? "null"})`, theme.tokens.dim);
1791
+ tui.requestRender();
1792
+ }
1793
+ else if (e.type === "checkpoint_created") {
1794
+ appendLine(`checkpoint ${e.id} created`, theme.tokens.dim);
1795
+ tui.requestRender();
1796
+ }
1797
+ else if (e.type === "checkpoint_failed") {
1798
+ appendLine(`checkpoint failed: ${e.reason}`, theme.tokens.warning);
1799
+ tui.requestRender();
1800
+ }
1801
+ else if (e.type === "checkpoint_unavailable") {
1802
+ appendLine(`checkpoint unavailable: ${e.reason ?? "not a git repository"}`, theme.tokens.warning);
1803
+ tui.requestRender();
1804
+ }
1805
+ else if (e.type === "budget_warning") {
1806
+ appendLine(`budget warning (${e.scope}): ${e.used}/${e.limit} tokens`, theme.tokens.warning);
1807
+ tui.requestRender();
1808
+ }
1563
1809
  else if (e.type === "model_switched") {
1564
1810
  appendLine(`switched model to ${e.provider}/${e.model}`);
1565
1811
  if (typeof session.appendEvent === "function") {
1566
- session.appendEvent({ type: "model_switched", provider: e.provider, model: e.model });
1812
+ session.appendEvent({
1813
+ type: "model_switched",
1814
+ provider: e.provider,
1815
+ model: e.model,
1816
+ });
1567
1817
  }
1568
1818
  updateStatus();
1569
1819
  }
@@ -1581,6 +1831,7 @@ export async function run(args) {
1581
1831
  if (box) {
1582
1832
  activity.setPhase("working", e.name);
1583
1833
  box.appendLive(e.text);
1834
+ transcript.invalidate();
1584
1835
  tui.requestRender();
1585
1836
  }
1586
1837
  }
@@ -1608,7 +1859,11 @@ export async function run(args) {
1608
1859
  lines.push(` config: FAIL ${e instanceof Error ? e.message : String(e)}`);
1609
1860
  }
1610
1861
  try {
1611
- const r = await provider.generate({ model: modelOverride ?? "default", messages: [{ role: "user", content: [{ type: "text", text: "ping" }] }], tools: [] });
1862
+ const r = await provider.generate({
1863
+ model: modelOverride ?? "default",
1864
+ messages: [{ role: "user", content: [{ type: "text", text: "ping" }] }],
1865
+ tools: [],
1866
+ });
1612
1867
  lines.push(` provider generate: ok (${r.finishReason})`);
1613
1868
  }
1614
1869
  catch (e) {
@@ -1633,7 +1888,11 @@ export async function run(args) {
1633
1888
  }
1634
1889
  if (useTools) {
1635
1890
  await mcpManager.syncTools();
1636
- runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
1891
+ runtime.setTools([
1892
+ ...builtInTools,
1893
+ ...extensionTools,
1894
+ ...createMcpTools(mcpManager),
1895
+ ]);
1637
1896
  }
1638
1897
  appendLine(formatMcpStatus(mcpManager.status()));
1639
1898
  return;
@@ -1741,7 +2000,9 @@ export async function run(args) {
1741
2000
  for (const message of session.messages) {
1742
2001
  if (message.role === "user") {
1743
2002
  turn++;
1744
- const snippet = redactText(messageText(message)).split("\\n")[0].slice(0, 80);
2003
+ const snippet = redactText(messageText(message))
2004
+ .split("\\n")[0]
2005
+ .slice(0, 80);
1745
2006
  lines.push(`${turn}. ${snippet || "(empty)"}`);
1746
2007
  }
1747
2008
  }
@@ -1806,6 +2067,27 @@ export async function run(args) {
1806
2067
  tui.requestRender();
1807
2068
  return;
1808
2069
  }
2070
+ // Shell mode: every line is a shell command. `!` alone (or as first char)
2071
+ // enters shell mode; Escape exits it.
2072
+ if (shellMode) {
2073
+ const cmd = trimmed.startsWith("!") ? trimmed.slice(1).trim() : trimmed;
2074
+ appendLine(`$ ${cmd}`, theme.tokens.user);
2075
+ await runShellCommand(cmd);
2076
+ tui.requestRender();
2077
+ return;
2078
+ }
2079
+ // `!` as first character enters shell mode.
2080
+ if (trimmed === "!" || trimmed.startsWith("! ")) {
2081
+ shellMode = true;
2082
+ const rest = trimmed.slice(1).trim();
2083
+ appendLine("shell mode: on (Escape to exit)", theme.tokens.dim);
2084
+ if (rest) {
2085
+ appendLine(`$ ${rest}`, theme.tokens.user);
2086
+ await runShellCommand(rest);
2087
+ }
2088
+ tui.requestRender();
2089
+ return;
2090
+ }
1809
2091
  refreshCommandRegistry();
1810
2092
  if (trimmed === "/steer" || trimmed.startsWith("/steer ")) {
1811
2093
  const text = trimmed.slice("/steer".length).trim();
@@ -1861,7 +2143,7 @@ export async function run(args) {
1861
2143
  tui.setFocus(input);
1862
2144
  activity.setPhase("working");
1863
2145
  if (decision === "trust-session") {
1864
- appendLine("trusted this session: shell commands will run without approval");
2146
+ appendLine("trusted this session: outside-workspace operations will run without approval", theme.tokens.dim);
1865
2147
  }
1866
2148
  resolve(decision);
1867
2149
  });