atom-lite 0.6.2 → 0.7.0

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 (80) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +27 -16
  3. package/dist/agent/loop.d.ts +10 -0
  4. package/dist/agent/loop.js +114 -1
  5. package/dist/agent/loop.js.map +1 -1
  6. package/dist/agent/turn-recorder.d.ts +4 -0
  7. package/dist/agent/turn-recorder.js +3 -0
  8. package/dist/agent/turn-recorder.js.map +1 -1
  9. package/dist/agent/types.d.ts +22 -0
  10. package/dist/cli/app.js +267 -45
  11. package/dist/cli/app.js.map +1 -1
  12. package/dist/cli/commands.js +183 -2
  13. package/dist/cli/commands.js.map +1 -1
  14. package/dist/cli/main.js +2 -1
  15. package/dist/cli/main.js.map +1 -1
  16. package/dist/cli/tool-box.js +3 -2
  17. package/dist/cli/tool-box.js.map +1 -1
  18. package/dist/config/defaults.js +1 -1
  19. package/dist/config/load.js +2 -2
  20. package/dist/config/load.js.map +1 -1
  21. package/dist/config/schema.d.ts +4 -0
  22. package/dist/config/schema.js +5 -1
  23. package/dist/config/schema.js.map +1 -1
  24. package/dist/context/budget.js +19 -1
  25. package/dist/context/budget.js.map +1 -1
  26. package/dist/eval/runner.d.ts +51 -0
  27. package/dist/eval/runner.js +230 -0
  28. package/dist/eval/runner.js.map +1 -0
  29. package/dist/mcp/client.js +2 -1
  30. package/dist/mcp/client.js.map +1 -1
  31. package/dist/providers/context-window.d.ts +4 -0
  32. package/dist/providers/context-window.js +54 -0
  33. package/dist/providers/context-window.js.map +1 -1
  34. package/dist/providers/factory.js +3 -0
  35. package/dist/providers/factory.js.map +1 -1
  36. package/dist/providers/gemini.d.ts +2 -0
  37. package/dist/providers/gemini.js +18 -11
  38. package/dist/providers/gemini.js.map +1 -1
  39. package/dist/providers/ollama.d.ts +3 -0
  40. package/dist/providers/ollama.js +9 -0
  41. package/dist/providers/ollama.js.map +1 -1
  42. package/dist/providers/openai-compatible.d.ts +2 -0
  43. package/dist/providers/openai-compatible.js +24 -3
  44. package/dist/providers/openai-compatible.js.map +1 -1
  45. package/dist/runtime/background-tasks.d.ts +23 -0
  46. package/dist/runtime/background-tasks.js +141 -0
  47. package/dist/runtime/background-tasks.js.map +1 -0
  48. package/dist/runtime/checkpoint-store.d.ts +64 -0
  49. package/dist/runtime/checkpoint-store.js +419 -0
  50. package/dist/runtime/checkpoint-store.js.map +1 -0
  51. package/dist/sessions/events.js +4 -5
  52. package/dist/sessions/events.js.map +1 -1
  53. package/dist/sessions/store.js +0 -16
  54. package/dist/sessions/store.js.map +1 -1
  55. package/dist/tools/bash.js +45 -9
  56. package/dist/tools/bash.js.map +1 -1
  57. package/dist/tools/edit.js +28 -8
  58. package/dist/tools/edit.js.map +1 -1
  59. package/dist/tools/index.d.ts +1 -0
  60. package/dist/tools/index.js +1 -0
  61. package/dist/tools/index.js.map +1 -1
  62. package/dist/tools/read.js +70 -9
  63. package/dist/tools/read.js.map +1 -1
  64. package/dist/tools/task.d.ts +2 -0
  65. package/dist/tools/task.js +55 -0
  66. package/dist/tools/task.js.map +1 -0
  67. package/dist/tools/types.d.ts +8 -1
  68. package/dist/tools/write.js +29 -8
  69. package/dist/tools/write.js.map +1 -1
  70. package/dist/types.d.ts +11 -0
  71. package/dist/types.js.map +1 -1
  72. package/dist/version.d.ts +1 -0
  73. package/dist/version.js +14 -0
  74. package/dist/version.js.map +1 -0
  75. package/package.json +3 -2
  76. package/skills/code-review/SKILL.md +75 -32
  77. package/skills/code-review/agents/openai.yaml +3 -0
  78. package/skills/pr/CREDITS.md +3 -0
  79. package/skills/pr/SKILL.md +170 -0
  80. package/skills/pr/agents/openai.yaml +3 -0
package/dist/cli/app.js CHANGED
@@ -9,14 +9,15 @@ import { execSync, spawn } from "node:child_process";
9
9
  import { defaultShell, terminateProcessTree } from "../security/process.js";
10
10
  import { openSearchablePicker } from "./picker.js";
11
11
  import { openCommandPalette } from "./palette.js";
12
- import { bareCommandName, createCommandRegistry } from "./commands.js";
12
+ import { bareCommandName, createCommandRegistry, } from "./commands.js";
13
13
  import { resolveTheme, themeLabel, themeNames, markdownThemeFor, } from "./theme.js";
14
14
  import { SecretInput } from "./secret-input.js";
15
15
  import { StatusLine } from "./status-line.js";
16
16
  import { Activity } from "./activity.js";
17
17
  import { ToolBox } from "./tool-box.js";
18
+ import { ApprovalPrompt } from "./approval-prompt.js";
18
19
  import { Banner } from "./banner.js";
19
- import { formatContextUsage, knownContextWindow, } from "../providers/context-window.js";
20
+ import { formatContextUsage, knownContextWindow, knownVisionModel, } from "../providers/context-window.js";
20
21
  import { StreamText } from "./stream-text.js";
21
22
  import { Transcript } from "./transcript.js";
22
23
  import { AgentRuntime } from "../agent/loop.js";
@@ -26,10 +27,10 @@ import { collectExtensionTools } from "../extensions/tool-adapter.js";
26
27
  import { denyProject, trustProject } from "../extensions/trust.js";
27
28
  import { ProjectTrustPrompt } from "./trust-prompt.js";
28
29
  import { formatSessionList, formatSessionSearch, searchSessions, sessionTitleFromEvents, } from "../sessions/search.js";
29
- import { providerSwitchNotice, sessionHistoryLines, sessionSelectionFromEvents, } from "../sessions/history.js";
30
+ import { providerSwitchNotice, sessionSelectionFromEvents, } from "../sessions/history.js";
30
31
  import { blockAtLine, createMouseCapture, handleMouseEvent, parseSgrMouse, shouldEnableMouse, } from "./mouse.js";
31
- import { readTool, writeTool, editTool, bashTool, webfetchTool, websearchTool, grepTool, globTool } from "../tools/index.js";
32
- 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";
33
34
  import { createProvider } from "../providers/factory.js";
34
35
  import { availableProviderNames, getPreset, listModelsForProvider, materializeProviderConfig, probeApiKey, providerLabel, PROVIDER_PRESETS, } from "../providers/catalog.js";
35
36
  import { ContextManager } from "../context/manager.js";
@@ -90,7 +91,8 @@ export async function run(args) {
90
91
  }
91
92
  }
92
93
  if (showVersion) {
93
- process.stdout.write("0.3.1\n");
94
+ const { VERSION } = await import("../version.js");
95
+ process.stdout.write(VERSION + "\n");
94
96
  return;
95
97
  }
96
98
  if (listSessions || searchSessionsQuery !== undefined) {
@@ -125,12 +127,23 @@ export async function run(args) {
125
127
  modelByProvider.set(provider.id, modelOverride);
126
128
  const useTools = !positional.includes("__notools__");
127
129
  const builtInTools = useTools
128
- ? [readTool, writeTool, editTool, bashTool, webfetchTool, websearchTool, grepTool, globTool]
130
+ ? [
131
+ readTool,
132
+ writeTool,
133
+ editTool,
134
+ bashTool,
135
+ webfetchTool,
136
+ websearchTool,
137
+ grepTool,
138
+ globTool,
139
+ taskTool,
140
+ ]
129
141
  : [];
130
142
  const interactive = process.stdin.isTTY === true && !jsonMode;
131
143
  // one-shot task from positional or stdin
132
144
  let stdinTask = "";
133
- if (!process.stdin.isTTY && positional.filter((p) => !p.startsWith("__")).length === 0) {
145
+ if (!process.stdin.isTTY &&
146
+ positional.filter((p) => !p.startsWith("__")).length === 0) {
134
147
  const chunks = [];
135
148
  for await (const chunk of process.stdin)
136
149
  chunks.push(chunk);
@@ -140,9 +153,11 @@ export async function run(args) {
140
153
  const oneShot = (taskArgs.join(" ").trim() || stdinTask).trim();
141
154
  const store = new JsonlSessionStore();
142
155
  let session = sessionOverride
143
- ? store.resume(sessionOverride) ?? store.create(process.cwd(), provider.id)
156
+ ? (store.resume(sessionOverride) ??
157
+ store.create(process.cwd(), provider.id))
144
158
  : positional.includes("__continue__")
145
- ? store.resumeLatest(process.cwd()) ?? store.create(process.cwd(), provider.id)
159
+ ? (store.resumeLatest(process.cwd()) ??
160
+ store.create(process.cwd(), provider.id))
146
161
  : store.create(process.cwd(), provider.id);
147
162
  const mcpServers = (config.mcpServers ?? {});
148
163
  const mcpManager = new McpManager({
@@ -165,7 +180,9 @@ export async function run(args) {
165
180
  try {
166
181
  const { execFileSync } = require("node:child_process");
167
182
  const parts = value.slice(1).trim().split(/\s+/);
168
- return execFileSync(parts[0], parts.slice(1), { encoding: "utf8" }).trim();
183
+ return execFileSync(parts[0], parts.slice(1), {
184
+ encoding: "utf8",
185
+ }).trim();
169
186
  }
170
187
  catch {
171
188
  return undefined;
@@ -203,7 +220,7 @@ export async function run(args) {
203
220
  // Bridge Pi-style extension providers into Atom-lite's config so they
204
221
  // appear in /provider, /model, and the command palette.
205
222
  for (const loaded of extensionLoader.list()) {
206
- for (const [name, provider] of (loaded.registries?.providers ?? new Map())) {
223
+ for (const [name, provider] of loaded.registries?.providers ?? new Map()) {
207
224
  const cfg = provider?.config ?? {};
208
225
  const firstModel = Array.isArray(cfg.models) && cfg.models.length > 0
209
226
  ? (cfg.models[0].id ?? cfg.models[0].name ?? "default")
@@ -278,9 +295,14 @@ export async function run(args) {
278
295
  contextManager,
279
296
  extensionDispatch,
280
297
  bashConcurrency: config.runtime.bashConcurrency,
298
+ checkpointingEnabled: interactive || Boolean(config.runtime.checkpointing),
281
299
  });
282
300
  mcpManager.onToolsChanged = () => {
283
- runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
301
+ runtime.setTools([
302
+ ...builtInTools,
303
+ ...extensionTools,
304
+ ...createMcpTools(mcpManager),
305
+ ]);
284
306
  };
285
307
  if (oneShot || !interactive) {
286
308
  // non-interactive / one-shot path
@@ -290,7 +312,11 @@ export async function run(args) {
290
312
  try {
291
313
  if (useTools) {
292
314
  await mcpManager.syncTools();
293
- runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
315
+ runtime.setTools([
316
+ ...builtInTools,
317
+ ...extensionTools,
318
+ ...createMcpTools(mcpManager),
319
+ ]);
294
320
  }
295
321
  const result = await runtime.runAgentTurn(oneShot || " ", session, controller.signal);
296
322
  if (jsonMode) {
@@ -302,7 +328,9 @@ export async function run(args) {
302
328
  }
303
329
  catch (err) {
304
330
  if (jsonMode)
305
- 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");
306
334
  else
307
335
  process.stderr.write(`atom-lite: ${err instanceof Error ? err.message : String(err)}\n`);
308
336
  process.exit(1);
@@ -314,14 +342,16 @@ export async function run(args) {
314
342
  return;
315
343
  }
316
344
  // ---- Interactive TUI (first-class) ----
317
- 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, visibleWidth, } = await import("@earendil-works/pi-tui");
318
346
  const mouseEnabled = shouldEnableMouse({
319
347
  interactive,
320
348
  configured: config.ui.mouse,
321
349
  stdoutIsTTY: process.stdout.isTTY === true,
322
350
  });
323
351
  const terminal = new ProcessTerminal();
324
- const tui = new TuiAltScreen(terminal, false, undefined, { mouse: mouseEnabled });
352
+ const tui = new TuiAltScreen(terminal, false, undefined, {
353
+ mouse: mouseEnabled,
354
+ });
325
355
  const mouseCapture = createMouseCapture((data) => terminal.write(data));
326
356
  let effortLevel = "medium";
327
357
  runtime.setEffort(effortLevel);
@@ -349,9 +379,17 @@ export async function run(args) {
349
379
  displayPath = "~" + cwd.slice(home.length);
350
380
  let branch = "";
351
381
  try {
352
- 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();
353
387
  if (branch === "HEAD") {
354
- 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();
355
393
  }
356
394
  }
357
395
  catch {
@@ -401,7 +439,9 @@ export async function run(args) {
401
439
  const sig = skillCacheSignature(process.cwd());
402
440
  if (sig === registrySkillSignature)
403
441
  return;
404
- commandRegistry = createCommandRegistry({ skills: cachedSkillRegistry().list() });
442
+ commandRegistry = createCommandRegistry({
443
+ skills: cachedSkillRegistry().list(),
444
+ });
405
445
  registrySkillSignature = sig;
406
446
  input.setAutocompleteProvider(new CombinedAutocompleteProvider(commandRegistry.autocomplete(), process.cwd(), fdPath));
407
447
  }
@@ -433,7 +473,9 @@ export async function run(args) {
433
473
  input.autocompleteList) {
434
474
  const selected = input.autocompleteList.getSelectedItem?.();
435
475
  const name = selected?.value ?? selected?.name ?? selected?.label;
436
- const cmd = name ? commandRegistry.get(String(name).replace(/^\//, "")) : undefined;
476
+ const cmd = name
477
+ ? commandRegistry.get(String(name).replace(/^\//, ""))
478
+ : undefined;
437
479
  if (cmd?.bareInvokable) {
438
480
  originalHandleInput("\t");
439
481
  return;
@@ -445,18 +487,37 @@ export async function run(args) {
445
487
  }
446
488
  originalHandleInput(data);
447
489
  };
490
+ const jumpToBottomLabel = "↓ bottom";
491
+ const jumpToBottomView = {
492
+ render(width) {
493
+ const label = output.isFollowingEnd
494
+ ? theme.tokens.dim(jumpToBottomLabel)
495
+ : theme.tokens.accent(jumpToBottomLabel);
496
+ const pad = Math.max(0, width - visibleWidth(jumpToBottomLabel));
497
+ return [" ".repeat(pad) + label];
498
+ },
499
+ invalidate() { },
500
+ };
448
501
  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.
502
+ // Keep fixed chrome (spacer, input, jump-to-bottom, status, header, dropdown)
503
+ // from being collapsed when the transcript grows; only the output region may
504
+ // shrink.
451
505
  root.addChild(output, { grow: 1, shrink: 1, minSize: 0 });
452
506
  root.addChild(new Spacer(1), { grow: 0, shrink: 0, minSize: 1 });
453
507
  root.addChild(input, { grow: 0, shrink: 0 });
508
+ root.addChild(jumpToBottomView, { grow: 0, shrink: 0, minSize: 1 });
454
509
  root.addChild(statusBar, { grow: 0, shrink: 0 });
455
510
  // folder:branch info below the status line, dimmed
456
511
  root.addChild(header, { grow: 0, shrink: 0 });
457
512
  const dropdownContainer = new Container();
458
513
  root.addChild(dropdownContainer, { grow: 0, shrink: 0 });
459
514
  tui.setLayoutRoot(root);
515
+ function jumpToBottomRow() {
516
+ const width = terminal.columns;
517
+ const outputHeight = Math.max(1, output.viewportHeight || output.render(width).length);
518
+ const inputHeight = Math.max(1, input.render(width).length);
519
+ return outputHeight + 1 + inputHeight;
520
+ }
460
521
  tui.setFocus(input);
461
522
  if (!noColor) {
462
523
  try {
@@ -595,7 +656,11 @@ export async function run(args) {
595
656
  try {
596
657
  if (useTools) {
597
658
  await mcpManager.syncTools();
598
- runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
659
+ runtime.setTools([
660
+ ...builtInTools,
661
+ ...extensionTools,
662
+ ...createMcpTools(mcpManager),
663
+ ]);
599
664
  }
600
665
  const turnResult = await runtime.runAgentTurn(prompt, session, controller.signal);
601
666
  if (turnResult === "TOKEN_BUDGET_REACHED") {
@@ -746,11 +811,21 @@ export async function run(args) {
746
811
  function refreshContextWindow() {
747
812
  const profile = provider.id;
748
813
  const model = currentModelLabel();
749
- const configured = profileConfig(profile)?.contextWindow;
750
- const window = typeof configured === "number" && configured > 0
751
- ? configured
814
+ const pcfg = profileConfig(profile);
815
+ const configuredWindow = pcfg?.contextWindow;
816
+ const window = typeof configuredWindow === "number" && configuredWindow > 0
817
+ ? configuredWindow
752
818
  : knownContextWindow(model);
753
819
  runtime.setContextWindow(window);
820
+ // Config override for vision takes precedence over provider metadata.
821
+ if (typeof pcfg?.supportsVision === "boolean") {
822
+ runtime.setSupportsVision(pcfg.supportsVision);
823
+ }
824
+ else {
825
+ const guessed = knownVisionModel(model);
826
+ if (typeof guessed === "boolean")
827
+ runtime.setSupportsVision(guessed);
828
+ }
754
829
  updateStatus();
755
830
  const getModelInfo = provider.getModelInfo;
756
831
  if (typeof getModelInfo !== "function")
@@ -761,10 +836,16 @@ export async function run(args) {
761
836
  if (provider.id !== profile || currentModelLabel() !== model)
762
837
  return;
763
838
  const resolved = info?.contextWindow;
764
- if (typeof resolved === "number" && resolved > 0) {
839
+ if (typeof resolved === "number" &&
840
+ resolved > 0 &&
841
+ typeof configuredWindow !== "number") {
765
842
  runtime.setContextWindow(resolved);
766
843
  updateStatus();
767
844
  }
845
+ if (typeof pcfg?.supportsVision !== "boolean" &&
846
+ typeof info?.supportsVision === "boolean") {
847
+ runtime.setSupportsVision(info.supportsVision);
848
+ }
768
849
  })
769
850
  .catch(() => {
770
851
  // Keep the static/config fallback.
@@ -775,7 +856,7 @@ export async function run(args) {
775
856
  const MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
776
857
  const modelCache = new Map();
777
858
  for (const loaded of extensionLoader.list()) {
778
- for (const [name, provider] of (loaded.registries?.providers ?? new Map())) {
859
+ for (const [name, provider] of loaded.registries?.providers ?? new Map()) {
779
860
  const cfg = provider?.config ?? {};
780
861
  const models = Array.isArray(cfg.models)
781
862
  ? cfg.models.map((m) => m.id ?? m.name).filter(Boolean)
@@ -1011,7 +1092,11 @@ export async function run(args) {
1011
1092
  provider = createProvider(config, preset.id, chosen);
1012
1093
  runtime.switchModel(provider, chosen);
1013
1094
  if (typeof session.appendEvent === "function") {
1014
- session.appendEvent({ type: "provider_selected", provider: preset.id, model: chosen });
1095
+ session.appendEvent({
1096
+ type: "provider_selected",
1097
+ provider: preset.id,
1098
+ model: chosen,
1099
+ });
1015
1100
  }
1016
1101
  refreshContextWindow();
1017
1102
  appendLine(`provider: ${preset.label} (${preset.id}) · model: ${chosen}`);
@@ -1262,7 +1347,9 @@ export async function run(args) {
1262
1347
  return "No extensions discovered.";
1263
1348
  return report
1264
1349
  .map((ext) => {
1265
- const origin = ext.origin ? `${ext.source}:${ext.origin}` : ext.source;
1350
+ const origin = ext.origin
1351
+ ? `${ext.source}:${ext.origin}`
1352
+ : ext.source;
1266
1353
  const version = ext.version ? `v${ext.version}` : "-";
1267
1354
  const counts = `${ext.toolCount} tools / ${ext.commandCount} commands`;
1268
1355
  const error = ext.error ? `\t${ext.error}` : "";
@@ -1280,7 +1367,9 @@ export async function run(args) {
1280
1367
  return showThinking;
1281
1368
  },
1282
1369
  setTheme: setThemeTarget,
1283
- listSkills: () => cachedSkillRegistry().list().map((skill) => ({
1370
+ listSkills: () => cachedSkillRegistry()
1371
+ .list()
1372
+ .map((skill) => ({
1284
1373
  name: skill.name,
1285
1374
  description: skill.description,
1286
1375
  })),
@@ -1296,11 +1385,77 @@ export async function run(args) {
1296
1385
  function restoreTranscript(next) {
1297
1386
  streamText.finalize();
1298
1387
  transcript.reset();
1388
+ // Drop stale references so toggles only affect the resumed transcript.
1389
+ toolBoxes.length = 0;
1299
1390
  banner.setTheme(theme);
1300
1391
  transcript.add(banner);
1301
1392
  transcript.add(new Spacer(1));
1302
- for (const line of sessionHistoryLines(next.messages)) {
1303
- transcript.addLine(line);
1393
+ const messages = Array.isArray(next?.messages) ? next.messages : [];
1394
+ // Index tool results by their call id so each restored ToolBox can carry
1395
+ // its output and render as the same collapsible block used live.
1396
+ const resultsByCallId = new Map();
1397
+ for (const message of messages) {
1398
+ if (message?.role === "tool" && message.toolCallId) {
1399
+ resultsByCallId.set(String(message.toolCallId), message);
1400
+ }
1401
+ }
1402
+ const renderedToolCallIds = new Set();
1403
+ const textOf = (content) => {
1404
+ if (typeof content === "string")
1405
+ return content;
1406
+ if (Array.isArray(content)) {
1407
+ return content
1408
+ .map((part) => part?.type === "text" ? String(part.text ?? "") : "")
1409
+ .join("");
1410
+ }
1411
+ return "";
1412
+ };
1413
+ for (const message of messages) {
1414
+ if (!message || typeof message !== "object")
1415
+ continue;
1416
+ if (message.role === "system")
1417
+ continue;
1418
+ if (message.role === "user") {
1419
+ const text = textOf(message.content).trim();
1420
+ if (text) {
1421
+ transcript.add(new Text(theme.tokens.user(`> ${text}`)));
1422
+ }
1423
+ continue;
1424
+ }
1425
+ if (message.role === "assistant") {
1426
+ const text = textOf(message.content).trim();
1427
+ if (text) {
1428
+ transcript.add(new Markdown(text, 0, 0, markdownThemeFor(theme)));
1429
+ }
1430
+ for (const call of message.toolCalls ?? []) {
1431
+ const box = new ToolBox({
1432
+ id: String(call?.id ?? ""),
1433
+ name: String(call?.name ?? ""),
1434
+ args: call?.arguments ?? {},
1435
+ });
1436
+ box.setExpanded(expandTools);
1437
+ const result = resultsByCallId.get(String(call?.id ?? ""));
1438
+ if (result) {
1439
+ box.setResult(String(result.content ?? ""), {
1440
+ isError: result.isError === true,
1441
+ diff: Array.isArray(result.diff) ? result.diff : undefined,
1442
+ });
1443
+ renderedToolCallIds.add(String(call?.id ?? ""));
1444
+ }
1445
+ transcript.add(box);
1446
+ toolBoxes.push(box);
1447
+ }
1448
+ continue;
1449
+ }
1450
+ if (message.role === "tool") {
1451
+ if (renderedToolCallIds.has(String(message.toolCallId ?? "")))
1452
+ continue;
1453
+ const text = String(message.content ?? "").trim();
1454
+ if (text) {
1455
+ transcript.add(new Text(theme.tokens.text(`[${message.name}] ${text}`)));
1456
+ }
1457
+ continue;
1458
+ }
1304
1459
  }
1305
1460
  tui.requestRender();
1306
1461
  }
@@ -1347,9 +1502,7 @@ export async function run(args) {
1347
1502
  const summaries = scope === "all" ? store.list() : store.list(process.cwd());
1348
1503
  const toggle = {
1349
1504
  value: "__scope__",
1350
- label: scope === "all"
1351
- ? "Show this workspace only"
1352
- : "Show all workspaces",
1505
+ label: scope === "all" ? "Show this workspace only" : "Show all workspaces",
1353
1506
  description: `scope: ${scope === "all" ? "all workspaces" : process.cwd()}`,
1354
1507
  };
1355
1508
  return [
@@ -1373,7 +1526,9 @@ export async function run(args) {
1373
1526
  if (item.value === "__scope__") {
1374
1527
  scope = scope === "all" ? "workspace" : "all";
1375
1528
  picker.updateItems(buildItems());
1376
- picker.setStatus(scope === "all" ? "showing all workspaces" : "showing this workspace");
1529
+ picker.setStatus(scope === "all"
1530
+ ? "showing all workspaces"
1531
+ : "showing this workspace");
1377
1532
  return;
1378
1533
  }
1379
1534
  picker.close();
@@ -1530,7 +1685,8 @@ export async function run(args) {
1530
1685
  }
1531
1686
  extensionProviderNames.clear();
1532
1687
  for (const loaded of extensionLoader.list()) {
1533
- for (const [name, provider] of (loaded.registries?.providers ?? new Map())) {
1688
+ for (const [name, provider] of loaded.registries?.providers ??
1689
+ new Map()) {
1534
1690
  const cfg = provider?.config ?? {};
1535
1691
  const firstModel = Array.isArray(cfg.models) && cfg.models.length > 0
1536
1692
  ? (cfg.models[0].id ?? cfg.models[0].name ?? "default")
@@ -1631,6 +1787,13 @@ export async function run(args) {
1631
1787
  return undefined;
1632
1788
  if (!mouseEnabled)
1633
1789
  return { consume: true };
1790
+ if (event.type === "press" &&
1791
+ event.button === "left" &&
1792
+ event.y === jumpToBottomRow()) {
1793
+ output.scrollToEnd();
1794
+ tui.requestRender();
1795
+ return { consume: true };
1796
+ }
1634
1797
  handleMouseEvent(event, {
1635
1798
  isModalOpen: () => uiModalOpen,
1636
1799
  scrollBy: (lines) => output.scrollBy(lines),
@@ -1715,6 +1878,22 @@ export async function run(args) {
1715
1878
  else if (e.type === "context_usage") {
1716
1879
  updateStatus();
1717
1880
  }
1881
+ else if (e.type === "background_task_complete") {
1882
+ appendLine(`background task ${e.id}: ${e.status} (exit ${e.exitCode ?? "null"})`, theme.tokens.dim);
1883
+ tui.requestRender();
1884
+ }
1885
+ else if (e.type === "checkpoint_created") {
1886
+ appendLine(`checkpoint ${e.id} created`, theme.tokens.dim);
1887
+ tui.requestRender();
1888
+ }
1889
+ else if (e.type === "checkpoint_failed") {
1890
+ appendLine(`checkpoint failed: ${e.reason}`, theme.tokens.warning);
1891
+ tui.requestRender();
1892
+ }
1893
+ else if (e.type === "checkpoint_unavailable") {
1894
+ appendLine(`checkpoint unavailable: ${e.reason ?? "not a git repository"}`, theme.tokens.warning);
1895
+ tui.requestRender();
1896
+ }
1718
1897
  else if (e.type === "budget_warning") {
1719
1898
  appendLine(`budget warning (${e.scope}): ${e.used}/${e.limit} tokens`, theme.tokens.warning);
1720
1899
  tui.requestRender();
@@ -1722,11 +1901,20 @@ export async function run(args) {
1722
1901
  else if (e.type === "model_switched") {
1723
1902
  appendLine(`switched model to ${e.provider}/${e.model}`);
1724
1903
  if (typeof session.appendEvent === "function") {
1725
- session.appendEvent({ type: "model_switched", provider: e.provider, model: e.model });
1904
+ session.appendEvent({
1905
+ type: "model_switched",
1906
+ provider: e.provider,
1907
+ model: e.model,
1908
+ });
1726
1909
  }
1727
1910
  updateStatus();
1728
1911
  }
1729
1912
  else if (e.type === "tool_call") {
1913
+ if (thinkingText) {
1914
+ transcript.invalidate();
1915
+ thinkingText = null;
1916
+ thinkingBuffer = "";
1917
+ }
1730
1918
  finalizeText();
1731
1919
  activity.setPhase("working", e.name);
1732
1920
  const box = new ToolBox({ id: e.id, name: e.name, args: e.args });
@@ -1754,6 +1942,11 @@ export async function run(args) {
1754
1942
  }
1755
1943
  }
1756
1944
  else if (e.type === "final") {
1945
+ if (thinkingText) {
1946
+ transcript.invalidate();
1947
+ thinkingText = null;
1948
+ thinkingBuffer = "";
1949
+ }
1757
1950
  finalizeText();
1758
1951
  activity.stop();
1759
1952
  }
@@ -1768,7 +1961,11 @@ export async function run(args) {
1768
1961
  lines.push(` config: FAIL ${e instanceof Error ? e.message : String(e)}`);
1769
1962
  }
1770
1963
  try {
1771
- const r = await provider.generate({ model: modelOverride ?? "default", messages: [{ role: "user", content: [{ type: "text", text: "ping" }] }], tools: [] });
1964
+ const r = await provider.generate({
1965
+ model: modelOverride ?? "default",
1966
+ messages: [{ role: "user", content: [{ type: "text", text: "ping" }] }],
1967
+ tools: [],
1968
+ });
1772
1969
  lines.push(` provider generate: ok (${r.finishReason})`);
1773
1970
  }
1774
1971
  catch (e) {
@@ -1793,7 +1990,11 @@ export async function run(args) {
1793
1990
  }
1794
1991
  if (useTools) {
1795
1992
  await mcpManager.syncTools();
1796
- runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
1993
+ runtime.setTools([
1994
+ ...builtInTools,
1995
+ ...extensionTools,
1996
+ ...createMcpTools(mcpManager),
1997
+ ]);
1797
1998
  }
1798
1999
  appendLine(formatMcpStatus(mcpManager.status()));
1799
2000
  return;
@@ -1901,7 +2102,9 @@ export async function run(args) {
1901
2102
  for (const message of session.messages) {
1902
2103
  if (message.role === "user") {
1903
2104
  turn++;
1904
- const snippet = redactText(messageText(message)).split("\\n")[0].slice(0, 80);
2105
+ const snippet = redactText(messageText(message))
2106
+ .split("\\n")[0]
2107
+ .slice(0, 80);
1905
2108
  lines.push(`${turn}. ${snippet || "(empty)"}`);
1906
2109
  }
1907
2110
  }
@@ -2030,8 +2233,27 @@ export async function run(args) {
2030
2233
  appendLine(`> ${trimmed}`, theme.tokens.user);
2031
2234
  await runTurn(trimmed);
2032
2235
  };
2033
- // Approval prompts are superseded by plan/yolo modes.
2034
- approvals.handler = undefined;
2236
+ approvals.handler = interactive
2237
+ ? (req) => new Promise((resolve) => {
2238
+ uiModalOpen = true;
2239
+ closeOverlay();
2240
+ dropdownContainer.clear();
2241
+ activity.setPhase("waiting");
2242
+ const prompt = new ApprovalPrompt(req, (decision) => {
2243
+ uiModalOpen = false;
2244
+ dropdownContainer.clear();
2245
+ tui.setFocus(input);
2246
+ activity.setPhase("working");
2247
+ if (decision === "trust-session") {
2248
+ appendLine("trusted this session: outside-workspace operations will run without approval", theme.tokens.dim);
2249
+ }
2250
+ resolve(decision);
2251
+ });
2252
+ dropdownContainer.addChild(prompt);
2253
+ tui.setFocus(prompt);
2254
+ tui.requestRender();
2255
+ })
2256
+ : undefined;
2035
2257
  process.on("SIGTERM", () => {
2036
2258
  void mcpManager.close().finally(() => {
2037
2259
  tui.stop();