atom-lite 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +1 -1
  3. package/dist/agent/loop.d.ts +9 -0
  4. package/dist/agent/loop.js +233 -47
  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/turn-recorder.d.ts +33 -0
  10. package/dist/agent/turn-recorder.js +74 -0
  11. package/dist/agent/turn-recorder.js.map +1 -0
  12. package/dist/agent/types.d.ts +4 -0
  13. package/dist/cli/app.js +373 -33
  14. package/dist/cli/app.js.map +1 -1
  15. package/dist/cli/commands.d.ts +3 -0
  16. package/dist/cli/commands.js +20 -5
  17. package/dist/cli/commands.js.map +1 -1
  18. package/dist/cli/main.js +2 -2
  19. package/dist/cli/main.js.map +1 -1
  20. package/dist/cli/mouse.js +4 -1
  21. package/dist/cli/mouse.js.map +1 -1
  22. package/dist/cli/stream-text.js +1 -0
  23. package/dist/cli/stream-text.js.map +1 -1
  24. package/dist/cli/theme.d.ts +1 -1
  25. package/dist/cli/theme.js +2 -0
  26. package/dist/cli/theme.js.map +1 -1
  27. package/dist/cli/transcript.d.ts +15 -1
  28. package/dist/cli/transcript.js +40 -1
  29. package/dist/cli/transcript.js.map +1 -1
  30. package/dist/cli/trust-prompt.d.ts +24 -0
  31. package/dist/cli/trust-prompt.js +89 -0
  32. package/dist/cli/trust-prompt.js.map +1 -0
  33. package/dist/config/defaults.d.ts +2 -0
  34. package/dist/config/defaults.js +2 -0
  35. package/dist/config/defaults.js.map +1 -1
  36. package/dist/config/load.js +4 -0
  37. package/dist/config/load.js.map +1 -1
  38. package/dist/config/paths.d.ts +2 -0
  39. package/dist/config/paths.js +12 -1
  40. package/dist/config/paths.js.map +1 -1
  41. package/dist/config/schema.d.ts +4 -0
  42. package/dist/config/schema.js +2 -0
  43. package/dist/config/schema.js.map +1 -1
  44. package/dist/context/manager.d.ts +17 -3
  45. package/dist/context/manager.js +129 -41
  46. package/dist/context/manager.js.map +1 -1
  47. package/dist/extensions/api.d.ts +51 -1
  48. package/dist/extensions/api.js +124 -6
  49. package/dist/extensions/api.js.map +1 -1
  50. package/dist/extensions/discovery.d.ts +16 -0
  51. package/dist/extensions/discovery.js +191 -108
  52. package/dist/extensions/discovery.js.map +1 -1
  53. package/dist/extensions/loader.d.ts +42 -1
  54. package/dist/extensions/loader.js +272 -32
  55. package/dist/extensions/loader.js.map +1 -1
  56. package/dist/extensions/session-manager.d.ts +44 -0
  57. package/dist/extensions/session-manager.js +137 -0
  58. package/dist/extensions/session-manager.js.map +1 -0
  59. package/dist/extensions/tool-adapter.d.ts +12 -0
  60. package/dist/extensions/tool-adapter.js +157 -0
  61. package/dist/extensions/tool-adapter.js.map +1 -0
  62. package/dist/extensions/trust.d.ts +2 -0
  63. package/dist/extensions/trust.js +32 -17
  64. package/dist/extensions/trust.js.map +1 -1
  65. package/dist/extensions/types.d.ts +45 -3
  66. package/dist/extensions/ui.d.ts +21 -0
  67. package/dist/extensions/ui.js +75 -0
  68. package/dist/extensions/ui.js.map +1 -0
  69. package/dist/index.d.ts +1 -0
  70. package/dist/index.js +1 -0
  71. package/dist/index.js.map +1 -1
  72. package/dist/mcp/stdio-transport.js +3 -20
  73. package/dist/mcp/stdio-transport.js.map +1 -1
  74. package/dist/runtime/scheduler.d.ts +4 -3
  75. package/dist/runtime/scheduler.js +7 -105
  76. package/dist/runtime/scheduler.js.map +1 -1
  77. package/dist/runtime/tool-executor.d.ts +18 -0
  78. package/dist/runtime/tool-executor.js +126 -0
  79. package/dist/runtime/tool-executor.js.map +1 -0
  80. package/dist/security/process.d.ts +10 -0
  81. package/dist/security/process.js +36 -0
  82. package/dist/security/process.js.map +1 -1
  83. package/dist/sessions/store.js +2 -3
  84. package/dist/sessions/store.js.map +1 -1
  85. package/dist/tools/bash.js +6 -3
  86. package/dist/tools/bash.js.map +1 -1
  87. package/dist/tools/types.d.ts +6 -0
  88. package/package.json +3 -1
package/dist/cli/app.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { loadConfig } from "../config/load.js";
2
+ import { defaultAgentDir, userConfigDir } from "../config/paths.js";
2
3
  import { saveProviderSelection, saveTheme, } from "../config/write.js";
3
4
  import { resolveApiKey, saveApiKey } from "../config/credentials.js";
4
5
  import { join } from "node:path";
@@ -20,6 +21,9 @@ import { Transcript } from "./transcript.js";
20
21
  import { AgentRuntime } from "../agent/loop.js";
21
22
  import { JsonlSessionStore } from "../sessions/store.js";
22
23
  import { ExtensionLoader } from "../extensions/loader.js";
24
+ import { collectExtensionTools } from "../extensions/tool-adapter.js";
25
+ import { denyProject, trustProject } from "../extensions/trust.js";
26
+ import { ProjectTrustPrompt } from "./trust-prompt.js";
23
27
  import { formatSessionList, formatSessionSearch, searchSessions, sessionTitleFromEvents, } from "../sessions/search.js";
24
28
  import { providerSwitchNotice, sessionHistoryLines, sessionSelectionFromEvents, } from "../sessions/history.js";
25
29
  import { blockAtLine, createMouseCapture, handleMouseEvent, parseSgrMouse, shouldEnableMouse, } from "./mouse.js";
@@ -28,7 +32,7 @@ import { McpManager, formatMcpStatus, formatMcpDoctor, createMcpTools } from "..
28
32
  import { createProvider } from "../providers/factory.js";
29
33
  import { availableProviderNames, getPreset, listModelsForProvider, materializeProviderConfig, probeApiKey, providerLabel, PROVIDER_PRESETS, } from "../providers/catalog.js";
30
34
  import { ContextManager } from "../context/manager.js";
31
- import { ProcessTerminal, isKeyRelease, isKeyRepeat, Key, matchesKey, } from "@earendil-works/pi-tui";
35
+ import { ProcessTerminal, isKeyRelease, isKeyRepeat, Key, matchesKey, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
32
36
  export async function run(args) {
33
37
  let providerOverride;
34
38
  let modelOverride;
@@ -41,9 +45,15 @@ export async function run(args) {
41
45
  let listSessions = false;
42
46
  let searchSessionsQuery;
43
47
  const positional = [];
48
+ const extensionPaths = [];
44
49
  for (let i = 0; i < args.length; i++) {
45
50
  const a = args[i];
46
- if (a === "--provider")
51
+ if (a === "-e" || a === "--extension") {
52
+ const path = args[++i];
53
+ if (path)
54
+ extensionPaths.push(path);
55
+ }
56
+ else if (a === "--provider")
47
57
  providerOverride = args[++i];
48
58
  else if (a === "--model")
49
59
  modelOverride = args[++i];
@@ -68,7 +78,7 @@ export async function run(args) {
68
78
  else if (a === "--no-tools")
69
79
  positional.push("__notools__");
70
80
  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");
81
+ 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
82
  return;
73
83
  }
74
84
  else if (a.startsWith("--")) {
@@ -145,20 +155,101 @@ export async function run(args) {
145
155
  }
146
156
  },
147
157
  });
158
+ const extensionUICallbacks = {};
159
+ const extensionProviderNames = new Set();
160
+ function resolveExtensionApiKey(value) {
161
+ if (typeof value !== "string" || !value)
162
+ return undefined;
163
+ if (value.startsWith("!")) {
164
+ try {
165
+ const { execFileSync } = require("node:child_process");
166
+ const parts = value.slice(1).trim().split(/\s+/);
167
+ return execFileSync(parts[0], parts.slice(1), { encoding: "utf8" }).trim();
168
+ }
169
+ catch {
170
+ return undefined;
171
+ }
172
+ }
173
+ if (value.startsWith("$"))
174
+ return process.env[value.slice(1)] ?? value;
175
+ if (value.startsWith("${") && value.endsWith("}"))
176
+ return process.env[value.slice(2, -1)] ?? value;
177
+ return value;
178
+ }
148
179
  const extensionLoader = new ExtensionLoader({
149
- userDir: join(process.env.HOME ?? "", ".config", "atom-lite", "extensions"),
180
+ cliPaths: extensionPaths,
181
+ cwd: process.cwd(),
182
+ agentDir: defaultAgentDir(),
183
+ userDir: join(userConfigDir(), "extensions"),
150
184
  projectDir: process.cwd(),
151
- trustedProjects: config.extensions?.trustedProjects,
152
185
  });
153
- void extensionLoader.loadAll().catch(() => {
186
+ const pendingTrust = extensionLoader
187
+ .discover()
188
+ .filter((entry) => entry.source === "project" &&
189
+ !entry.trusted &&
190
+ entry.skipReason === "project is not trusted");
191
+ if (!interactive && pendingTrust.length > 0) {
192
+ process.stderr.write(`atom-lite: skipping ${pendingTrust.length} untrusted project extension(s); run interactively to review\n`);
193
+ }
194
+ await extensionLoader.loadAll().catch(() => {
154
195
  // Extension failures are reported per extension.
155
196
  });
197
+ // Remove any previously bridged extension providers before re-bridging.
198
+ for (const name of extensionProviderNames) {
199
+ delete config.providers[name];
200
+ }
201
+ extensionProviderNames.clear();
202
+ // Bridge Pi-style extension providers into Atom-lite's config so they
203
+ // appear in /provider, /model, and the command palette.
204
+ for (const loaded of extensionLoader.list()) {
205
+ for (const [name, provider] of (loaded.registries?.providers ?? new Map())) {
206
+ const cfg = provider?.config ?? {};
207
+ const firstModel = Array.isArray(cfg.models) && cfg.models.length > 0
208
+ ? (cfg.models[0].id ?? cfg.models[0].name ?? "default")
209
+ : "default";
210
+ const apiKey = resolveExtensionApiKey(cfg.apiKey);
211
+ const headers = { ...(cfg.headers ?? {}) };
212
+ if (cfg.authHeader && apiKey)
213
+ headers.Authorization = `Bearer ${apiKey}`;
214
+ config.providers[name] = {
215
+ type: "openai-compatible",
216
+ baseUrl: cfg.baseUrl,
217
+ model: firstModel,
218
+ apiKey: apiKey ?? null,
219
+ apiKeyEnv: null,
220
+ headers,
221
+ contextWindow: cfg.models?.[0]?.contextWindow,
222
+ };
223
+ extensionProviderNames.add(name);
224
+ }
225
+ }
226
+ const extensionTools = collectExtensionTools(extensionLoader);
227
+ const allTools = [...builtInTools, ...extensionTools];
228
+ const extensionDispatch = async (event, payload) => {
229
+ const results = [];
230
+ for (const loaded of extensionLoader.list()) {
231
+ const handlers = loaded.registries.handlers.get(event);
232
+ if (!handlers)
233
+ continue;
234
+ for (const handler of handlers) {
235
+ try {
236
+ results.push(await handler(payload, loaded.context ?? {}));
237
+ }
238
+ catch (error) {
239
+ // eslint-disable-next-line no-console
240
+ console.error(`[extension:${loaded.id}] handler error: ${error instanceof Error ? error.message : String(error)}`);
241
+ }
242
+ }
243
+ }
244
+ return results;
245
+ };
156
246
  const contextManager = new ContextManager({
157
247
  systemPrompt: "You are a coding assistant.",
158
248
  maxInputTokens: config.context.maxInputTokens ?? null,
159
249
  reserveOutputTokens: config.context.reserveOutputTokens,
160
250
  compactAtPercent: config.context.compactAtPercent,
161
251
  maxProjectInstructionBytes: config.context.maxProjectInstructionBytes,
252
+ compactEveryNTurns: config.context.compactEveryNTurns ?? 0,
162
253
  provider,
163
254
  model: modelOverride ?? "default",
164
255
  userInstructionsPath: undefined,
@@ -178,13 +269,14 @@ export async function run(args) {
178
269
  provider,
179
270
  model: modelOverride,
180
271
  workspace: process.cwd(),
181
- tools: builtInTools,
272
+ tools: allTools,
182
273
  approvals,
183
274
  contextManager,
275
+ extensionDispatch,
184
276
  bashConcurrency: config.runtime.bashConcurrency,
185
277
  });
186
278
  mcpManager.onToolsChanged = () => {
187
- runtime.setTools([...builtInTools, ...createMcpTools(mcpManager)]);
279
+ runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
188
280
  };
189
281
  if (oneShot || !interactive) {
190
282
  // non-interactive / one-shot path
@@ -194,7 +286,7 @@ export async function run(args) {
194
286
  try {
195
287
  if (useTools) {
196
288
  await mcpManager.syncTools();
197
- runtime.setTools([...builtInTools, ...createMcpTools(mcpManager)]);
289
+ runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
198
290
  }
199
291
  const result = await runtime.runAgentTurn(oneShot || " ", session, controller.signal);
200
292
  if (jsonMode) {
@@ -218,14 +310,14 @@ export async function run(args) {
218
310
  return;
219
311
  }
220
312
  // ---- 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);
313
+ const { TuiAltScreen, Text, Input, Editor, Container, Spacer, ScrollView, VStack, Loader, Markdown, Box } = await import("@earendil-works/pi-tui");
224
314
  const mouseEnabled = shouldEnableMouse({
225
315
  interactive,
226
316
  configured: config.ui.mouse,
227
317
  stdoutIsTTY: process.stdout.isTTY === true,
228
318
  });
319
+ const terminal = new ProcessTerminal();
320
+ const tui = new TuiAltScreen(terminal, false, undefined, { mouse: mouseEnabled });
229
321
  const mouseCapture = createMouseCapture((data) => terminal.write(data));
230
322
  let effortLevel = "medium";
231
323
  runtime.setEffort(effortLevel);
@@ -308,14 +400,16 @@ export async function run(args) {
308
400
  input.setAutocompleteProvider(new CombinedAutocompleteProvider(commandRegistry.autocomplete(), process.cwd()));
309
401
  }
310
402
  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);
403
+ const root = new VStack([]);
404
+ root.addChild(output, { grow: 1 });
405
+ root.addChild(new Spacer(1));
406
+ root.addChild(input);
407
+ root.addChild(statusBar);
315
408
  // folder:branch info below the status line, dimmed
316
- tui.addChild(header);
409
+ root.addChild(header);
317
410
  const dropdownContainer = new Container();
318
- tui.addChild(dropdownContainer);
411
+ root.addChild(dropdownContainer);
412
+ tui.setLayoutRoot(root);
319
413
  tui.setFocus(input);
320
414
  if (!noColor) {
321
415
  try {
@@ -331,6 +425,27 @@ export async function run(args) {
331
425
  // COLORFGBG/dark fallback.
332
426
  }
333
427
  }
428
+ class ThinkingBlock {
429
+ buffer = "";
430
+ render(width) {
431
+ const lines = [theme.tokens.dim("💭 thinking")];
432
+ const sep = theme.tokens.dim("│ ");
433
+ const contentWidth = Math.max(1, width - 2);
434
+ const logicalLines = this.buffer.length > 0 ? this.buffer.split("\\n") : [""];
435
+ for (const logical of logicalLines) {
436
+ const wrapped = wrapTextWithAnsi(logical, contentWidth);
437
+ if (wrapped.length === 0) {
438
+ lines.push(sep);
439
+ }
440
+ else {
441
+ for (const part of wrapped)
442
+ lines.push(sep + theme.tokens.dim(part));
443
+ }
444
+ }
445
+ return lines;
446
+ }
447
+ invalidate() { }
448
+ }
334
449
  let thinkingText = null;
335
450
  let thinkingBuffer = "";
336
451
  let showThinking = config.ui.showThinking;
@@ -348,29 +463,82 @@ export async function run(args) {
348
463
  closeActiveOverlay = undefined;
349
464
  close?.();
350
465
  }
351
- function appendLine(text) {
466
+ function appendLine(text, style = theme.tokens.text) {
352
467
  if (thinkingText) {
353
468
  thinkingText = null;
354
469
  thinkingBuffer = "";
355
470
  }
356
471
  streamText.finalize();
357
- transcript.addLine(text);
472
+ transcript.add(new Text(style(text)));
358
473
  tui.requestRender();
359
474
  }
360
475
  function appendDelta(delta) {
361
476
  streamText.append(delta);
362
477
  tui.requestRender();
363
478
  }
479
+ /** Review project-local extensions once per project decision. */
480
+ function maybeReviewProjectTrust() {
481
+ if (uiModalOpen)
482
+ return;
483
+ const pending = extensionLoader
484
+ .discover()
485
+ .filter((entry) => entry.source === "project" &&
486
+ !entry.trusted &&
487
+ entry.skipReason === "project is not trusted");
488
+ if (pending.length === 0)
489
+ return;
490
+ uiModalOpen = true;
491
+ closeOverlay();
492
+ dropdownContainer.clear();
493
+ const prompt = new ProjectTrustPrompt(pending.map((entry) => ({
494
+ id: entry.id,
495
+ entryPath: entry.entryPath,
496
+ version: entry.manifest?.version,
497
+ capabilities: entry.manifest?.capabilities ?? [],
498
+ })), process.cwd(), (decision) => {
499
+ uiModalOpen = false;
500
+ dropdownContainer.clear();
501
+ tui.setFocus(input);
502
+ if (decision === "approve") {
503
+ trustProject(process.cwd());
504
+ appendLine("approved project extensions");
505
+ }
506
+ else {
507
+ denyProject(process.cwd());
508
+ appendLine("denied project extensions");
509
+ }
510
+ void extensionLoader
511
+ .loadAll()
512
+ .then(() => {
513
+ runtime.setTools([
514
+ ...builtInTools,
515
+ ...collectExtensionTools(extensionLoader),
516
+ ...createMcpTools(mcpManager),
517
+ ]);
518
+ appendLine(`extensions: ${extensionLoader.list().length} processed (see /extensions)`);
519
+ tui.requestRender();
520
+ })
521
+ .catch((error) => {
522
+ appendLine(`extensions: reload failed: ${error instanceof Error ? error.message : String(error)}`);
523
+ });
524
+ });
525
+ dropdownContainer.addChild(prompt);
526
+ tui.setFocus(prompt);
527
+ tui.requestRender();
528
+ }
364
529
  function thinkingBody() {
365
530
  return thinkingBuffer
366
531
  .split("\n")
367
- .map((line) => theme.tokens.dim(`| ${line}`))
532
+ .map((line) => theme.tokens.dim(`│ ${line}`))
368
533
  .join("\n");
369
534
  }
370
535
  function finalizeText() {
371
536
  streamText.finalize();
372
537
  }
373
538
  let activeTurnController = null;
539
+ const messageQueue = [];
540
+ const steeringQueue = [];
541
+ runtime.setSteeringQueue(steeringQueue);
374
542
  /** Run one interactive turn while keeping the status bar's activity fresh. */
375
543
  async function runTurn(prompt) {
376
544
  activity.start("formulating");
@@ -379,7 +547,7 @@ export async function run(args) {
379
547
  try {
380
548
  if (useTools) {
381
549
  await mcpManager.syncTools();
382
- runtime.setTools([...builtInTools, ...createMcpTools(mcpManager)]);
550
+ runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
383
551
  }
384
552
  await runtime.runAgentTurn(prompt, session, controller.signal);
385
553
  }
@@ -394,6 +562,11 @@ export async function run(args) {
394
562
  finally {
395
563
  activeTurnController = null;
396
564
  activity.stop();
565
+ if (messageQueue.length > 0) {
566
+ const next = messageQueue.shift();
567
+ appendLine(`> ${next}`, theme.tokens.user);
568
+ void runTurn(next);
569
+ }
397
570
  }
398
571
  }
399
572
  function updateStatus() {
@@ -467,6 +640,17 @@ export async function run(args) {
467
640
  refreshContextWindow();
468
641
  const MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
469
642
  const modelCache = new Map();
643
+ for (const loaded of extensionLoader.list()) {
644
+ for (const [name, provider] of (loaded.registries?.providers ?? new Map())) {
645
+ const cfg = provider?.config ?? {};
646
+ const models = Array.isArray(cfg.models)
647
+ ? cfg.models.map((m) => m.id ?? m.name).filter(Boolean)
648
+ : [];
649
+ if (models.length > 0) {
650
+ modelCache.set(name, { models, at: Date.now() });
651
+ }
652
+ }
653
+ }
470
654
  function profileConfig(profile) {
471
655
  return config.providers[profile];
472
656
  }
@@ -927,6 +1111,7 @@ export async function run(args) {
927
1111
  updateStatus,
928
1112
  runDoctor,
929
1113
  runMcp,
1114
+ runCompact,
930
1115
  runExport,
931
1116
  openModelPicker,
932
1117
  openProviderPicker,
@@ -935,10 +1120,22 @@ export async function run(args) {
935
1120
  openSessionsPicker,
936
1121
  searchSessions: searchSessionsInStore,
937
1122
  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.",
1123
+ listExtensions: () => {
1124
+ const report = extensionLoader.report();
1125
+ if (report.length === 0)
1126
+ return "No extensions discovered.";
1127
+ return report
1128
+ .map((ext) => {
1129
+ const origin = ext.origin ? `${ext.source}:${ext.origin}` : ext.source;
1130
+ const version = ext.version ? `v${ext.version}` : "-";
1131
+ const counts = `${ext.toolCount} tools / ${ext.commandCount} commands`;
1132
+ const error = ext.error ? `\t${ext.error}` : "";
1133
+ return `${ext.id}\t${origin}\t${version}\t${ext.status}\t${counts}\t${ext.entryPath}${error}`;
1134
+ })
1135
+ .join("\n");
1136
+ },
1137
+ extensionDoctor: () => extensionLoader.doctor(),
1138
+ reloadExtensions,
942
1139
  switchModel: switchModelTarget,
943
1140
  startProviderSetup,
944
1141
  setEffort: setEffortTarget,
@@ -1164,6 +1361,108 @@ export async function run(args) {
1164
1361
  }
1165
1362
  tui.requestRender();
1166
1363
  }
1364
+ async function reloadExtensions() {
1365
+ const previous = extensionLoader.list();
1366
+ // Emit session_shutdown and extension_unload before unloading.
1367
+ for (const loaded of previous) {
1368
+ const handlers = loaded.registries?.handlers?.get?.("session_shutdown");
1369
+ if (handlers) {
1370
+ for (const handler of handlers) {
1371
+ try {
1372
+ await handler({ type: "session_shutdown", reason: "reload" }, (loaded.context ?? {}));
1373
+ }
1374
+ catch (error) {
1375
+ appendLine(`extension shutdown error: ${error instanceof Error ? error.message : String(error)}`);
1376
+ }
1377
+ }
1378
+ }
1379
+ }
1380
+ await extensionLoader.unloadAll();
1381
+ try {
1382
+ await extensionLoader.loadAll();
1383
+ }
1384
+ catch (error) {
1385
+ appendLine(`extensions: reload failed: ${error instanceof Error ? error.message : String(error)}`);
1386
+ tui.requestRender();
1387
+ return;
1388
+ }
1389
+ // Remove previously bridged providers, then re-bridge.
1390
+ for (const name of extensionProviderNames) {
1391
+ delete config.providers[name];
1392
+ modelCache.delete(name);
1393
+ }
1394
+ extensionProviderNames.clear();
1395
+ for (const loaded of extensionLoader.list()) {
1396
+ for (const [name, provider] of (loaded.registries?.providers ?? new Map())) {
1397
+ const cfg = provider?.config ?? {};
1398
+ const firstModel = Array.isArray(cfg.models) && cfg.models.length > 0
1399
+ ? (cfg.models[0].id ?? cfg.models[0].name ?? "default")
1400
+ : "default";
1401
+ const apiKey = resolveExtensionApiKey(cfg.apiKey);
1402
+ const headers = { ...(cfg.headers ?? {}) };
1403
+ if (cfg.authHeader && apiKey)
1404
+ headers.Authorization = `Bearer ${apiKey}`;
1405
+ config.providers[name] = {
1406
+ type: "openai-compatible",
1407
+ baseUrl: cfg.baseUrl,
1408
+ model: firstModel,
1409
+ apiKey: apiKey ?? null,
1410
+ apiKeyEnv: null,
1411
+ headers,
1412
+ contextWindow: cfg.models?.[0]?.contextWindow,
1413
+ };
1414
+ extensionProviderNames.add(name);
1415
+ const models = Array.isArray(cfg.models)
1416
+ ? cfg.models.map((m) => m.id ?? m.name).filter(Boolean)
1417
+ : [];
1418
+ if (models.length > 0)
1419
+ modelCache.set(name, { models, at: Date.now() });
1420
+ }
1421
+ }
1422
+ // Re-apply extension tools.
1423
+ try {
1424
+ await mcpManager.syncTools();
1425
+ }
1426
+ catch {
1427
+ // ignore
1428
+ }
1429
+ runtime.setTools([
1430
+ ...builtInTools,
1431
+ ...collectExtensionTools(extensionLoader),
1432
+ ...createMcpTools(mcpManager),
1433
+ ]);
1434
+ // Emit session_start with reason reload.
1435
+ for (const loaded of extensionLoader.list()) {
1436
+ const handlers = loaded.registries?.handlers?.get?.("session_start");
1437
+ if (handlers) {
1438
+ for (const handler of handlers) {
1439
+ try {
1440
+ await handler({ type: "session_start", reason: "reload" }, (loaded.context ?? {}));
1441
+ }
1442
+ catch (error) {
1443
+ appendLine(`extension start error: ${error instanceof Error ? error.message : String(error)}`);
1444
+ }
1445
+ }
1446
+ }
1447
+ }
1448
+ appendLine(`extensions: ${extensionLoader.list().length} reloaded (see /extensions)`);
1449
+ tui.requestRender();
1450
+ }
1451
+ // Wire interactive extension UI callbacks now that the TUI exists.
1452
+ extensionUICallbacks.notify = (message, type) => {
1453
+ const style = type === "error"
1454
+ ? theme.tokens.error
1455
+ : type === "warning"
1456
+ ? theme.tokens.warning
1457
+ : theme.tokens.accent;
1458
+ appendLine(`${style(`[ext]`)} ${message}`);
1459
+ };
1460
+ extensionUICallbacks.requestRender = () => tui.requestRender();
1461
+ extensionUICallbacks.getToolsExpanded = () => expandTools;
1462
+ extensionUICallbacks.setToolsExpanded = (expanded) => {
1463
+ if (expandTools !== expanded)
1464
+ toggleTools();
1465
+ };
1167
1466
  function toggleToolBoxAtScreenRow(screenY) {
1168
1467
  const width = terminal.columns;
1169
1468
  const outputHeight = Math.max(1, output.viewportHeight || output.render(width).length);
@@ -1189,6 +1488,9 @@ export async function run(args) {
1189
1488
  const event = parseSgrMouse(data);
1190
1489
  if (!event)
1191
1490
  return undefined;
1491
+ // Let the alt-screen TUI own wheel scrolling; only intercept clicks.
1492
+ if (event.type === "wheel")
1493
+ return undefined;
1192
1494
  if (!mouseEnabled)
1193
1495
  return { consume: true };
1194
1496
  handleMouseEvent(event, {
@@ -1227,18 +1529,19 @@ export async function run(args) {
1227
1529
  if (!showThinking)
1228
1530
  return;
1229
1531
  if (!thinkingText) {
1230
- thinkingText = new Text(theme.tokens.dim("Thinking..."));
1532
+ thinkingText = new ThinkingBlock();
1231
1533
  transcript.add(thinkingText);
1232
1534
  thinkingBuffer = "";
1233
1535
  }
1234
1536
  thinkingBuffer += e.text;
1235
- thinkingText.setText(`${theme.tokens.dim("Thinking...")}\n${thinkingBody()}`);
1537
+ thinkingText.buffer = thinkingBuffer;
1538
+ transcript.invalidate();
1236
1539
  tui.requestRender();
1237
1540
  }
1238
1541
  else if (e.type === "assistant_delta") {
1239
1542
  activity.setPhase("working");
1240
1543
  if (thinkingText) {
1241
- thinkingText.setText(`${theme.tokens.dim("Thinking...")}\n${thinkingBody()}\n${theme.tokens.dim("| end")}`);
1544
+ transcript.invalidate();
1242
1545
  thinkingText = null;
1243
1546
  thinkingBuffer = "";
1244
1547
  }
@@ -1247,7 +1550,7 @@ export async function run(args) {
1247
1550
  else if (e.type === "assistant_text") {
1248
1551
  activity.setPhase("working");
1249
1552
  if (thinkingText) {
1250
- thinkingText.setText(`${theme.tokens.dim("Thinking...")}\n${thinkingBody()}\n${theme.tokens.dim("| end")}`);
1553
+ transcript.invalidate();
1251
1554
  thinkingText = null;
1252
1555
  thinkingBuffer = "";
1253
1556
  }
@@ -1330,7 +1633,7 @@ export async function run(args) {
1330
1633
  }
1331
1634
  if (useTools) {
1332
1635
  await mcpManager.syncTools();
1333
- runtime.setTools([...builtInTools, ...createMcpTools(mcpManager)]);
1636
+ runtime.setTools([...builtInTools, ...extensionTools, ...createMcpTools(mcpManager)]);
1334
1637
  }
1335
1638
  appendLine(formatMcpStatus(mcpManager.status()));
1336
1639
  return;
@@ -1483,6 +1786,19 @@ export async function run(args) {
1483
1786
  writeFileSync(outPath, lines.join("\\n"), "utf8");
1484
1787
  appendLine(`exported session to ${outPath}`);
1485
1788
  }
1789
+ async function runCompact(args) {
1790
+ const focus = (args ?? "").trim();
1791
+ if (session.messages.length <= 6) {
1792
+ appendLine("nothing to compact");
1793
+ return;
1794
+ }
1795
+ const summary = await runtime.compactSession(session, focus || undefined);
1796
+ if (!summary) {
1797
+ appendLine("nothing to compact");
1798
+ return;
1799
+ }
1800
+ appendLine(`compacted: ${summary.slice(0, 200)}`);
1801
+ }
1486
1802
  input.onSubmit = async (value) => {
1487
1803
  const trimmed = value.trim();
1488
1804
  input.setText("");
@@ -1491,6 +1807,24 @@ export async function run(args) {
1491
1807
  return;
1492
1808
  }
1493
1809
  refreshCommandRegistry();
1810
+ if (trimmed === "/steer" || trimmed.startsWith("/steer ")) {
1811
+ const text = trimmed.slice("/steer".length).trim();
1812
+ if (!text) {
1813
+ appendLine("usage: /steer <message>");
1814
+ tui.requestRender();
1815
+ return;
1816
+ }
1817
+ if (activeTurnController) {
1818
+ steeringQueue.push(text);
1819
+ appendLine(`steering: ${text}`, theme.tokens.dim);
1820
+ }
1821
+ else {
1822
+ appendLine(`> ${text}`, theme.tokens.user);
1823
+ await runTurn(text);
1824
+ }
1825
+ tui.requestRender();
1826
+ return;
1827
+ }
1494
1828
  if (trimmed.startsWith("/")) {
1495
1829
  const result = await commandRegistry.dispatch(trimmed, commandContext);
1496
1830
  if (result.exit)
@@ -1506,7 +1840,13 @@ export async function run(args) {
1506
1840
  await commandRegistry.dispatch(`/${bare} ${trimmed.slice(bare.length)}`, commandContext);
1507
1841
  return;
1508
1842
  }
1509
- appendLine(`> ${trimmed}`);
1843
+ if (activeTurnController) {
1844
+ messageQueue.push(trimmed);
1845
+ appendLine(`queued: ${trimmed}`, theme.tokens.dim);
1846
+ tui.requestRender();
1847
+ return;
1848
+ }
1849
+ appendLine(`> ${trimmed}`, theme.tokens.user);
1510
1850
  await runTurn(trimmed);
1511
1851
  };
1512
1852
  approvals.handler = interactive
@@ -1537,8 +1877,8 @@ export async function run(args) {
1537
1877
  });
1538
1878
  });
1539
1879
  tui.start();
1880
+ maybeReviewProjectTrust();
1540
1881
  if (mouseEnabled) {
1541
- mouseCapture.enable();
1542
1882
  process.once("exit", () => mouseCapture.disable());
1543
1883
  if (process.platform !== "win32") {
1544
1884
  process.on("SIGTSTP", () => {