@codex-infinity/pi-infinity 0.52.4 → 0.60.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/CHANGELOG.md +387 -0
  2. package/README.md +97 -66
  3. package/dist/bun/cli.d.ts +3 -0
  4. package/dist/bun/cli.d.ts.map +1 -0
  5. package/dist/bun/cli.js +6 -0
  6. package/dist/bun/cli.js.map +1 -0
  7. package/dist/bun/register-bedrock.d.ts +2 -0
  8. package/dist/bun/register-bedrock.d.ts.map +1 -0
  9. package/dist/bun/register-bedrock.js +4 -0
  10. package/dist/bun/register-bedrock.js.map +1 -0
  11. package/dist/cli/args.d.ts +2 -0
  12. package/dist/cli/args.d.ts.map +1 -1
  13. package/dist/cli/args.js +17 -6
  14. package/dist/cli/args.js.map +1 -1
  15. package/dist/cli/initial-message.d.ts +18 -0
  16. package/dist/cli/initial-message.d.ts.map +1 -0
  17. package/dist/cli/initial-message.js +22 -0
  18. package/dist/cli/initial-message.js.map +1 -0
  19. package/dist/cli.d.ts.map +1 -1
  20. package/dist/cli.js +2 -0
  21. package/dist/cli.js.map +1 -1
  22. package/dist/core/agent-session.d.ts +42 -6
  23. package/dist/core/agent-session.d.ts.map +1 -1
  24. package/dist/core/agent-session.js +346 -72
  25. package/dist/core/agent-session.js.map +1 -1
  26. package/dist/core/auth-storage.d.ts +1 -0
  27. package/dist/core/auth-storage.d.ts.map +1 -1
  28. package/dist/core/auth-storage.js +27 -2
  29. package/dist/core/auth-storage.js.map +1 -1
  30. package/dist/core/bash-executor.d.ts +6 -7
  31. package/dist/core/bash-executor.d.ts.map +1 -1
  32. package/dist/core/bash-executor.js +8 -107
  33. package/dist/core/bash-executor.js.map +1 -1
  34. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  35. package/dist/core/compaction/branch-summarization.js +1 -0
  36. package/dist/core/compaction/branch-summarization.js.map +1 -1
  37. package/dist/core/compaction/compaction.d.ts.map +1 -1
  38. package/dist/core/compaction/compaction.js +6 -1
  39. package/dist/core/compaction/compaction.js.map +1 -1
  40. package/dist/core/compaction/utils.d.ts +3 -0
  41. package/dist/core/compaction/utils.d.ts.map +1 -1
  42. package/dist/core/compaction/utils.js +16 -1
  43. package/dist/core/compaction/utils.js.map +1 -1
  44. package/dist/core/exec.d.ts.map +1 -1
  45. package/dist/core/exec.js +7 -3
  46. package/dist/core/exec.js.map +1 -1
  47. package/dist/core/export-html/index.d.ts +5 -2
  48. package/dist/core/export-html/index.d.ts.map +1 -1
  49. package/dist/core/export-html/index.js +4 -3
  50. package/dist/core/export-html/index.js.map +1 -1
  51. package/dist/core/export-html/template.js +11 -14
  52. package/dist/core/export-html/tool-renderer.d.ts +5 -2
  53. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  54. package/dist/core/export-html/tool-renderer.js +17 -4
  55. package/dist/core/export-html/tool-renderer.js.map +1 -1
  56. package/dist/core/extensions/index.d.ts +2 -2
  57. package/dist/core/extensions/index.d.ts.map +1 -1
  58. package/dist/core/extensions/index.js +1 -1
  59. package/dist/core/extensions/index.js.map +1 -1
  60. package/dist/core/extensions/loader.d.ts.map +1 -1
  61. package/dist/core/extensions/loader.js +37 -11
  62. package/dist/core/extensions/loader.js.map +1 -1
  63. package/dist/core/extensions/runner.d.ts +8 -4
  64. package/dist/core/extensions/runner.d.ts.map +1 -1
  65. package/dist/core/extensions/runner.js +77 -8
  66. package/dist/core/extensions/runner.js.map +1 -1
  67. package/dist/core/extensions/types.d.ts +56 -4
  68. package/dist/core/extensions/types.d.ts.map +1 -1
  69. package/dist/core/extensions/types.js.map +1 -1
  70. package/dist/core/extensions/wrapper.d.ts +4 -11
  71. package/dist/core/extensions/wrapper.d.ts.map +1 -1
  72. package/dist/core/extensions/wrapper.js +4 -78
  73. package/dist/core/extensions/wrapper.js.map +1 -1
  74. package/dist/core/footer-data-provider.d.ts +6 -1
  75. package/dist/core/footer-data-provider.d.ts.map +1 -1
  76. package/dist/core/footer-data-provider.js +83 -37
  77. package/dist/core/footer-data-provider.js.map +1 -1
  78. package/dist/core/index.d.ts +1 -1
  79. package/dist/core/index.d.ts.map +1 -1
  80. package/dist/core/index.js +1 -1
  81. package/dist/core/index.js.map +1 -1
  82. package/dist/core/keybindings.d.ts +3 -0
  83. package/dist/core/keybindings.d.ts.map +1 -1
  84. package/dist/core/keybindings.js +22 -12
  85. package/dist/core/keybindings.js.map +1 -1
  86. package/dist/core/model-registry.d.ts +11 -0
  87. package/dist/core/model-registry.d.ts.map +1 -1
  88. package/dist/core/model-registry.js +56 -16
  89. package/dist/core/model-registry.js.map +1 -1
  90. package/dist/core/model-resolver.d.ts +6 -0
  91. package/dist/core/model-resolver.d.ts.map +1 -1
  92. package/dist/core/model-resolver.js +122 -39
  93. package/dist/core/model-resolver.js.map +1 -1
  94. package/dist/core/package-manager.d.ts +19 -1
  95. package/dist/core/package-manager.d.ts.map +1 -1
  96. package/dist/core/package-manager.js +290 -57
  97. package/dist/core/package-manager.js.map +1 -1
  98. package/dist/core/resolve-config-value.d.ts.map +1 -1
  99. package/dist/core/resolve-config-value.js +43 -8
  100. package/dist/core/resolve-config-value.js.map +1 -1
  101. package/dist/core/resource-loader.d.ts.map +1 -1
  102. package/dist/core/resource-loader.js +4 -7
  103. package/dist/core/resource-loader.js.map +1 -1
  104. package/dist/core/sdk.d.ts +1 -1
  105. package/dist/core/sdk.d.ts.map +1 -1
  106. package/dist/core/sdk.js +7 -0
  107. package/dist/core/sdk.js.map +1 -1
  108. package/dist/core/session-manager.d.ts +1 -0
  109. package/dist/core/session-manager.d.ts.map +1 -1
  110. package/dist/core/session-manager.js +21 -15
  111. package/dist/core/session-manager.js.map +1 -1
  112. package/dist/core/settings-manager.d.ts +10 -0
  113. package/dist/core/settings-manager.d.ts.map +1 -1
  114. package/dist/core/settings-manager.js +59 -5
  115. package/dist/core/settings-manager.js.map +1 -1
  116. package/dist/core/skills.d.ts +3 -2
  117. package/dist/core/skills.d.ts.map +1 -1
  118. package/dist/core/skills.js +29 -8
  119. package/dist/core/skills.js.map +1 -1
  120. package/dist/core/slash-commands.d.ts.map +1 -1
  121. package/dist/core/slash-commands.js +3 -2
  122. package/dist/core/slash-commands.js.map +1 -1
  123. package/dist/core/system-prompt.d.ts +4 -0
  124. package/dist/core/system-prompt.d.ts.map +1 -1
  125. package/dist/core/system-prompt.js +43 -29
  126. package/dist/core/system-prompt.js.map +1 -1
  127. package/dist/core/tools/bash.d.ts +8 -0
  128. package/dist/core/tools/bash.d.ts.map +1 -1
  129. package/dist/core/tools/bash.js +77 -69
  130. package/dist/core/tools/bash.js.map +1 -1
  131. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  132. package/dist/core/tools/edit-diff.js +1 -0
  133. package/dist/core/tools/edit-diff.js.map +1 -1
  134. package/dist/core/tools/find.d.ts.map +1 -1
  135. package/dist/core/tools/find.js +6 -3
  136. package/dist/core/tools/find.js.map +1 -1
  137. package/dist/core/tools/index.d.ts +1 -1
  138. package/dist/core/tools/index.d.ts.map +1 -1
  139. package/dist/core/tools/index.js +1 -1
  140. package/dist/core/tools/index.js.map +1 -1
  141. package/dist/index.d.ts +3 -3
  142. package/dist/index.d.ts.map +1 -1
  143. package/dist/index.js +2 -2
  144. package/dist/index.js.map +1 -1
  145. package/dist/main.d.ts.map +1 -1
  146. package/dist/main.js +116 -36
  147. package/dist/main.js.map +1 -1
  148. package/dist/modes/interactive/components/extension-editor.d.ts +5 -2
  149. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  150. package/dist/modes/interactive/components/extension-editor.js +9 -0
  151. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  152. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  153. package/dist/modes/interactive/components/footer.js +8 -23
  154. package/dist/modes/interactive/components/footer.js.map +1 -1
  155. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  156. package/dist/modes/interactive/components/login-dialog.js +1 -1
  157. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  158. package/dist/modes/interactive/components/model-selector.d.ts +1 -1
  159. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  160. package/dist/modes/interactive/components/model-selector.js +1 -1
  161. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  162. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  163. package/dist/modes/interactive/components/oauth-selector.js +1 -1
  164. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  165. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  166. package/dist/modes/interactive/components/session-selector.js +1 -1
  167. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  168. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  169. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  170. package/dist/modes/interactive/components/settings-selector.js +15 -1
  171. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  172. package/dist/modes/interactive/components/show-images-selector.d.ts.map +1 -1
  173. package/dist/modes/interactive/components/show-images-selector.js +5 -1
  174. package/dist/modes/interactive/components/show-images-selector.js.map +1 -1
  175. package/dist/modes/interactive/components/theme-selector.d.ts.map +1 -1
  176. package/dist/modes/interactive/components/theme-selector.js +5 -1
  177. package/dist/modes/interactive/components/theme-selector.js.map +1 -1
  178. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  179. package/dist/modes/interactive/components/thinking-selector.js +5 -1
  180. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  181. package/dist/modes/interactive/components/tool-execution.d.ts +7 -0
  182. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  183. package/dist/modes/interactive/components/tool-execution.js +158 -7
  184. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  185. package/dist/modes/interactive/components/tree-selector.d.ts +21 -2
  186. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  187. package/dist/modes/interactive/components/tree-selector.js +127 -10
  188. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  189. package/dist/modes/interactive/components/user-message.d.ts +1 -0
  190. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  191. package/dist/modes/interactive/components/user-message.js +12 -0
  192. package/dist/modes/interactive/components/user-message.js.map +1 -1
  193. package/dist/modes/interactive/interactive-mode.d.ts +5 -1
  194. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  195. package/dist/modes/interactive/interactive-mode.js +215 -71
  196. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  197. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  198. package/dist/modes/interactive/theme/theme.js +5 -0
  199. package/dist/modes/interactive/theme/theme.js.map +1 -1
  200. package/dist/modes/rpc/jsonl.d.ts +17 -0
  201. package/dist/modes/rpc/jsonl.d.ts.map +1 -0
  202. package/dist/modes/rpc/jsonl.js +49 -0
  203. package/dist/modes/rpc/jsonl.js.map +1 -0
  204. package/dist/modes/rpc/rpc-client.d.ts +1 -1
  205. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  206. package/dist/modes/rpc/rpc-client.js +7 -11
  207. package/dist/modes/rpc/rpc-client.js.map +1 -1
  208. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  209. package/dist/modes/rpc/rpc-mode.js +9 -11
  210. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  211. package/dist/utils/child-process.d.ts +11 -0
  212. package/dist/utils/child-process.d.ts.map +1 -0
  213. package/dist/utils/child-process.js +78 -0
  214. package/dist/utils/child-process.js.map +1 -0
  215. package/dist/utils/clipboard-image.d.ts.map +1 -1
  216. package/dist/utils/clipboard-image.js +94 -11
  217. package/dist/utils/clipboard-image.js.map +1 -1
  218. package/dist/utils/clipboard-native.d.ts +1 -0
  219. package/dist/utils/clipboard-native.d.ts.map +1 -1
  220. package/dist/utils/clipboard-native.js.map +1 -1
  221. package/dist/utils/clipboard.d.ts +1 -1
  222. package/dist/utils/clipboard.d.ts.map +1 -1
  223. package/dist/utils/clipboard.js +27 -16
  224. package/dist/utils/clipboard.js.map +1 -1
  225. package/dist/utils/exif-orientation.d.ts +5 -0
  226. package/dist/utils/exif-orientation.d.ts.map +1 -0
  227. package/dist/utils/exif-orientation.js +158 -0
  228. package/dist/utils/exif-orientation.js.map +1 -0
  229. package/dist/utils/image-convert.d.ts.map +1 -1
  230. package/dist/utils/image-convert.js +5 -1
  231. package/dist/utils/image-convert.js.map +1 -1
  232. package/dist/utils/image-resize.d.ts.map +1 -1
  233. package/dist/utils/image-resize.js +6 -1
  234. package/dist/utils/image-resize.js.map +1 -1
  235. package/dist/utils/tools-manager.d.ts.map +1 -1
  236. package/dist/utils/tools-manager.js +66 -21
  237. package/dist/utils/tools-manager.js.map +1 -1
  238. package/docs/compaction.md +2 -0
  239. package/docs/custom-provider.md +57 -9
  240. package/docs/extensions.md +125 -12
  241. package/docs/keybindings.md +11 -1
  242. package/docs/models.md +44 -2
  243. package/docs/packages.md +9 -0
  244. package/docs/providers.md +10 -1
  245. package/docs/rpc.md +44 -7
  246. package/docs/sdk.md +2 -2
  247. package/docs/settings.md +11 -0
  248. package/docs/terminal-setup.md +39 -3
  249. package/docs/tmux.md +61 -0
  250. package/docs/tree.md +9 -0
  251. package/examples/extensions/README.md +2 -0
  252. package/examples/extensions/antigravity-image-gen.ts +8 -5
  253. package/examples/extensions/built-in-tool-renderer.ts +246 -0
  254. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  255. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  256. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  257. package/examples/extensions/custom-provider-gitlab-duo/test.ts +2 -2
  258. package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
  259. package/examples/extensions/dynamic-tools.ts +74 -0
  260. package/examples/extensions/overlay-qa-tests.ts +468 -1
  261. package/examples/extensions/preset.ts +2 -3
  262. package/examples/extensions/provider-payload.ts +14 -0
  263. package/examples/extensions/sandbox/index.ts +2 -3
  264. package/examples/extensions/subagent/agents.ts +2 -3
  265. package/examples/extensions/tool-override.ts +2 -3
  266. package/examples/extensions/with-deps/index.ts +1 -5
  267. package/examples/extensions/with-deps/package-lock.json +2 -2
  268. package/examples/extensions/with-deps/package.json +1 -1
  269. package/package.json +10 -7
@@ -6,15 +6,15 @@ import * as crypto from "node:crypto";
6
6
  import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
- import { getOAuthProviders, } from "@mariozechner/pi-ai";
10
9
  import { CombinedAutocompleteProvider, Container, fuzzyFilter, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
11
10
  import { spawn, spawnSync } from "child_process";
12
- import { APP_NAME, getAuthPath, getDebugLogPath, getShareViewerUrl, getUpdateInstruction, VERSION, } from "../../config.js";
11
+ import { APP_NAME, getAgentDir, getAuthPath, getDebugLogPath, getShareViewerUrl, getUpdateInstruction, VERSION, } from "../../config.js";
13
12
  import { parseSkillBlock } from "../../core/agent-session.js";
14
13
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
15
14
  import { KeybindingsManager } from "../../core/keybindings.js";
16
15
  import { createCompactionSummaryMessage } from "../../core/messages.js";
17
- import { resolveModelScope } from "../../core/model-resolver.js";
16
+ import { findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
17
+ import { DefaultPackageManager } from "../../core/package-manager.js";
18
18
  import { SessionManager } from "../../core/session-manager.js";
19
19
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
20
20
  import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
@@ -312,13 +312,13 @@ export class InteractiveMode {
312
312
  this.ui.setFocus(this.editor);
313
313
  this.setupKeyHandlers();
314
314
  this.setupEditorSubmitHandler();
315
+ // Start the UI before initializing extensions so session_start handlers can use interactive dialogs
316
+ this.ui.start();
317
+ this.isInitialized = true;
315
318
  // Initialize extensions first so resources are shown before messages
316
319
  await this.initExtensions();
317
320
  // Render initial messages AFTER showing loaded resources
318
321
  this.renderInitialMessages();
319
- // Start the UI
320
- this.ui.start();
321
- this.isInitialized = true;
322
322
  // Set terminal title
323
323
  this.updateTerminalTitle();
324
324
  // Subscribe to agent events
@@ -361,6 +361,18 @@ export class InteractiveMode {
361
361
  this.showNewVersionNotification(newVersion);
362
362
  }
363
363
  });
364
+ // Start package update check asynchronously
365
+ this.checkForPackageUpdates().then((updates) => {
366
+ if (updates.length > 0) {
367
+ this.showPackageUpdateNotification(updates);
368
+ }
369
+ });
370
+ // Check tmux keyboard setup asynchronously
371
+ this.checkTmuxKeyboardSetup().then((warning) => {
372
+ if (warning) {
373
+ this.showWarning(warning);
374
+ }
375
+ });
364
376
  // Show startup warnings
365
377
  const { migratedProviders, modelFallbackMessage, initialMessage, initialImages, initialMessages } = this.options;
366
378
  if (migratedProviders && migratedProviders.length > 0) {
@@ -410,10 +422,12 @@ export class InteractiveMode {
410
422
  * Check npm registry for a newer version.
411
423
  */
412
424
  async checkForNewVersion() {
413
- if (process.env.PI_SKIP_VERSION_CHECK)
425
+ if (process.env.PI_SKIP_VERSION_CHECK || process.env.PI_OFFLINE)
414
426
  return undefined;
415
427
  try {
416
- const response = await fetch("https://registry.npmjs.org/@mariozechner/pi-coding-agent/latest");
428
+ const response = await fetch("https://registry.npmjs.org/@mariozechner/pi-coding-agent/latest", {
429
+ signal: AbortSignal.timeout(10000),
430
+ });
417
431
  if (!response.ok)
418
432
  return undefined;
419
433
  const data = (await response.json());
@@ -427,6 +441,64 @@ export class InteractiveMode {
427
441
  return undefined;
428
442
  }
429
443
  }
444
+ async checkForPackageUpdates() {
445
+ if (process.env.PI_OFFLINE) {
446
+ return [];
447
+ }
448
+ try {
449
+ const packageManager = new DefaultPackageManager({
450
+ cwd: process.cwd(),
451
+ agentDir: getAgentDir(),
452
+ settingsManager: this.settingsManager,
453
+ });
454
+ const updates = await packageManager.checkForAvailableUpdates();
455
+ return updates.map((update) => update.displayName);
456
+ }
457
+ catch {
458
+ return [];
459
+ }
460
+ }
461
+ async checkTmuxKeyboardSetup() {
462
+ if (!process.env.TMUX)
463
+ return undefined;
464
+ const runTmuxShow = (option) => {
465
+ return new Promise((resolve) => {
466
+ const proc = spawn("tmux", ["show", "-gv", option], {
467
+ stdio: ["ignore", "pipe", "ignore"],
468
+ });
469
+ let stdout = "";
470
+ const timer = setTimeout(() => {
471
+ proc.kill();
472
+ resolve(undefined);
473
+ }, 2000);
474
+ proc.stdout?.on("data", (data) => {
475
+ stdout += data.toString();
476
+ });
477
+ proc.on("error", () => {
478
+ clearTimeout(timer);
479
+ resolve(undefined);
480
+ });
481
+ proc.on("close", (code) => {
482
+ clearTimeout(timer);
483
+ resolve(code === 0 ? stdout.trim() : undefined);
484
+ });
485
+ });
486
+ };
487
+ const [extendedKeys, extendedKeysFormat] = await Promise.all([
488
+ runTmuxShow("extended-keys"),
489
+ runTmuxShow("extended-keys-format"),
490
+ ]);
491
+ // If we couldn't query tmux (timeout, sandbox, etc.), don't warn
492
+ if (extendedKeys === undefined)
493
+ return undefined;
494
+ if (extendedKeys !== "on" && extendedKeys !== "always") {
495
+ return "tmux extended-keys is off. Modified Enter keys may not work. Add `set -g extended-keys on` to ~/.tmux.conf and restart tmux.";
496
+ }
497
+ if (extendedKeysFormat === "xterm") {
498
+ return "tmux extended-keys-format is xterm. Pi works best with csi-u. Add `set -g extended-keys-format csi-u` to ~/.tmux.conf and restart tmux.";
499
+ }
500
+ return undefined;
501
+ }
430
502
  /**
431
503
  * Get changelog entries to display on startup.
432
504
  * Only shows new entries since last seen version, skips for resumed sessions.
@@ -540,7 +612,7 @@ export class InteractiveMode {
540
612
  group.paths.push(p);
541
613
  }
542
614
  }
543
- return [groups.user, groups.project, groups.path].filter((group) => group.paths.length > 0 || group.packages.size > 0);
615
+ return [groups.project, groups.user, groups.path].filter((group) => group.paths.length > 0 || group.packages.size > 0);
544
616
  }
545
617
  formatScopeGroups(groups, options) {
546
618
  const lines = [];
@@ -1115,7 +1187,7 @@ export class InteractiveMode {
1115
1187
  custom: (factory, options) => this.showExtensionCustom(factory, options),
1116
1188
  pasteToEditor: (text) => this.editor.handleInput(`\x1b[200~${text}\x1b[201~`),
1117
1189
  setEditorText: (text) => this.editor.setText(text),
1118
- getEditorText: () => this.editor.getText(),
1190
+ getEditorText: () => this.editor.getExpandedText?.() ?? this.editor.getText(),
1119
1191
  editor: (title, prefill) => this.showExtensionEditor(title, prefill),
1120
1192
  setEditorComponent: (factory) => this.setCustomEditorComponent(factory),
1121
1193
  get theme() {
@@ -1131,6 +1203,9 @@ export class InteractiveMode {
1131
1203
  }
1132
1204
  const result = setTheme(themeOrName, true);
1133
1205
  if (result.success) {
1206
+ if (this.settingsManager.getTheme() !== themeOrName) {
1207
+ this.settingsManager.setTheme(themeOrName);
1208
+ }
1134
1209
  this.ui.requestRender();
1135
1210
  }
1136
1211
  return result;
@@ -1285,10 +1360,18 @@ export class InteractiveMode {
1285
1360
  // Use duck typing since instanceof fails across jiti module boundaries
1286
1361
  const customEditor = newEditor;
1287
1362
  if ("actionHandlers" in customEditor && customEditor.actionHandlers instanceof Map) {
1288
- customEditor.onEscape = () => this.defaultEditor.onEscape?.();
1289
- customEditor.onCtrlD = () => this.defaultEditor.onCtrlD?.();
1290
- customEditor.onPasteImage = () => this.defaultEditor.onPasteImage?.();
1291
- customEditor.onExtensionShortcut = (data) => this.defaultEditor.onExtensionShortcut?.(data);
1363
+ if (!customEditor.onEscape) {
1364
+ customEditor.onEscape = () => this.defaultEditor.onEscape?.();
1365
+ }
1366
+ if (!customEditor.onCtrlD) {
1367
+ customEditor.onCtrlD = () => this.defaultEditor.onCtrlD?.();
1368
+ }
1369
+ if (!customEditor.onPasteImage) {
1370
+ customEditor.onPasteImage = () => this.defaultEditor.onPasteImage?.();
1371
+ }
1372
+ if (!customEditor.onExtensionShortcut) {
1373
+ customEditor.onExtensionShortcut = (data) => this.defaultEditor.onExtensionShortcut?.(data);
1374
+ }
1292
1375
  // Copy action handlers (clear, suspend, model switching, etc.)
1293
1376
  for (const [action, handler] of this.defaultEditor.actionHandlers) {
1294
1377
  customEditor.actionHandlers.set(action, handler);
@@ -1524,13 +1607,18 @@ export class InteractiveMode {
1524
1607
  this.editor.setText("");
1525
1608
  return;
1526
1609
  }
1610
+ if (text.startsWith("/import")) {
1611
+ await this.handleImportCommand(text);
1612
+ this.editor.setText("");
1613
+ return;
1614
+ }
1527
1615
  if (text === "/share") {
1528
1616
  await this.handleShareCommand();
1529
1617
  this.editor.setText("");
1530
1618
  return;
1531
1619
  }
1532
1620
  if (text === "/copy") {
1533
- this.handleCopyCommand();
1621
+ await this.handleCopyCommand();
1534
1622
  this.editor.setText("");
1535
1623
  return;
1536
1624
  }
@@ -1720,7 +1808,6 @@ export class InteractiveMode {
1720
1808
  for (const content of this.streamingMessage.content) {
1721
1809
  if (content.type === "toolCall") {
1722
1810
  if (!this.pendingTools.has(content.id)) {
1723
- this.chatContainer.addChild(new Text("", 0, 0));
1724
1811
  const component = new ToolExecutionComponent(content.name, content.arguments, {
1725
1812
  showImages: this.settingsManager.getShowImages(),
1726
1813
  }, this.getRegisteredToolDefinition(content.name), this.ui);
@@ -2153,8 +2240,13 @@ export class InteractiveMode {
2153
2240
  await this.shutdown();
2154
2241
  }
2155
2242
  handleCtrlZ() {
2243
+ // Ignore SIGINT while suspended so Ctrl+C in the terminal does not
2244
+ // kill the backgrounded process. The handler is removed on resume.
2245
+ const ignoreSigint = () => { };
2246
+ process.on("SIGINT", ignoreSigint);
2156
2247
  // Set up handler to restore TUI when resumed
2157
2248
  process.once("SIGCONT", () => {
2249
+ process.removeListener("SIGINT", ignoreSigint);
2158
2250
  this.ui.start();
2159
2251
  this.ui.requestRender(true);
2160
2252
  });
@@ -2286,6 +2378,7 @@ export class InteractiveMode {
2286
2378
  // Spawn editor synchronously with inherited stdio for interactive editing
2287
2379
  const result = spawnSync(editor, [...editorArgs, tmpFile], {
2288
2380
  stdio: "inherit",
2381
+ shell: process.platform === "win32",
2289
2382
  });
2290
2383
  // On successful exit (status 0), replace editor content
2291
2384
  if (result.status === 0) {
@@ -2336,6 +2429,16 @@ export class InteractiveMode {
2336
2429
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2337
2430
  this.ui.requestRender();
2338
2431
  }
2432
+ showPackageUpdateNotification(packages) {
2433
+ const action = theme.fg("accent", `${APP_NAME} update`);
2434
+ const updateInstruction = theme.fg("muted", "Package updates are available. Run ") + action;
2435
+ const packageLines = packages.map((pkg) => `- ${pkg}`).join("\n");
2436
+ this.chatContainer.addChild(new Spacer(1));
2437
+ this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2438
+ this.chatContainer.addChild(new Text(`${theme.bold(theme.fg("warning", "Package Updates Available"))}\n${updateInstruction}\n${theme.fg("muted", "Packages:")}\n${packageLines}`, 1, 0));
2439
+ this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2440
+ this.ui.requestRender();
2441
+ }
2339
2442
  /**
2340
2443
  * Get all queued messages (read-only).
2341
2444
  * Combines session queue and compaction queue.
@@ -2539,6 +2642,7 @@ export class InteractiveMode {
2539
2642
  hideThinkingBlock: this.hideThinkingBlock,
2540
2643
  collapseChangelog: this.settingsManager.getCollapseChangelog(),
2541
2644
  doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
2645
+ treeFilterMode: this.settingsManager.getTreeFilterMode(),
2542
2646
  showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
2543
2647
  editorPaddingX: this.settingsManager.getEditorPaddingX(),
2544
2648
  autocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),
@@ -2617,6 +2721,9 @@ export class InteractiveMode {
2617
2721
  onDoubleEscapeActionChange: (action) => {
2618
2722
  this.settingsManager.setDoubleEscapeAction(action);
2619
2723
  },
2724
+ onTreeFilterModeChange: (mode) => {
2725
+ this.settingsManager.setTreeFilterMode(mode);
2726
+ },
2620
2727
  onShowHardwareCursorChange: (enabled) => {
2621
2728
  this.settingsManager.setShowHardwareCursor(enabled);
2622
2729
  this.ui.setShowHardwareCursor(enabled);
@@ -2669,28 +2776,8 @@ export class InteractiveMode {
2669
2776
  this.showModelSelector(searchTerm);
2670
2777
  }
2671
2778
  async findExactModelMatch(searchTerm) {
2672
- const term = searchTerm.trim();
2673
- if (!term)
2674
- return undefined;
2675
- let targetProvider;
2676
- let targetModelId = "";
2677
- if (term.includes("/")) {
2678
- const parts = term.split("/", 2);
2679
- targetProvider = parts[0]?.trim().toLowerCase();
2680
- targetModelId = parts[1]?.trim().toLowerCase() ?? "";
2681
- }
2682
- else {
2683
- targetModelId = term.toLowerCase();
2684
- }
2685
- if (!targetModelId)
2686
- return undefined;
2687
2779
  const models = await this.getModelCandidates();
2688
- const exactMatches = models.filter((item) => {
2689
- const idMatch = item.id.toLowerCase() === targetModelId;
2690
- const providerMatch = !targetProvider || item.provider.toLowerCase() === targetProvider;
2691
- return idMatch && providerMatch;
2692
- });
2693
- return exactMatches.length === 1 ? exactMatches[0] : undefined;
2780
+ return findExactModelReferenceMatch(searchTerm, models);
2694
2781
  }
2695
2782
  async getModelCandidates() {
2696
2783
  if (this.session.scopedModels.length > 0) {
@@ -2770,12 +2857,10 @@ export class InteractiveMode {
2770
2857
  // Helper to update session's scoped models (session-only, no persist)
2771
2858
  const updateSessionModels = async (enabledIds) => {
2772
2859
  if (enabledIds.size > 0 && enabledIds.size < allModels.length) {
2773
- // Use current session thinking level, not settings default
2774
- const currentThinkingLevel = this.session.thinkingLevel;
2775
2860
  const newScopedModels = await resolveModelScope(Array.from(enabledIds), this.session.modelRegistry);
2776
2861
  this.session.setScopedModels(newScopedModels.map((sm) => ({
2777
2862
  model: sm.model,
2778
- thinkingLevel: sm.thinkingLevel ?? currentThinkingLevel,
2863
+ thinkingLevel: sm.thinkingLevel,
2779
2864
  })));
2780
2865
  }
2781
2866
  else {
@@ -2872,6 +2957,7 @@ export class InteractiveMode {
2872
2957
  showTreeSelector(initialSelectedId) {
2873
2958
  const tree = this.sessionManager.getTree();
2874
2959
  const realLeafId = this.sessionManager.getLeafId();
2960
+ const initialFilterMode = this.settingsManager.getTreeFilterMode();
2875
2961
  if (tree.length === 0) {
2876
2962
  this.showStatus("No entries in session");
2877
2963
  return;
@@ -2889,27 +2975,30 @@ export class InteractiveMode {
2889
2975
  // Loop until user makes a complete choice or cancels to tree
2890
2976
  let wantsSummary = false;
2891
2977
  let customInstructions;
2892
- while (true) {
2893
- const summaryChoice = await this.showExtensionSelector("Summarize branch?", [
2894
- "No summary",
2895
- "Summarize",
2896
- "Summarize with custom prompt",
2897
- ]);
2898
- if (summaryChoice === undefined) {
2899
- // User pressed escape - re-show tree selector with same selection
2900
- this.showTreeSelector(entryId);
2901
- return;
2902
- }
2903
- wantsSummary = summaryChoice !== "No summary";
2904
- if (summaryChoice === "Summarize with custom prompt") {
2905
- customInstructions = await this.showExtensionEditor("Custom summarization instructions");
2906
- if (customInstructions === undefined) {
2907
- // User cancelled - loop back to summary selector
2908
- continue;
2978
+ // Check if we should skip the prompt (user preference to always default to no summary)
2979
+ if (!this.settingsManager.getBranchSummarySkipPrompt()) {
2980
+ while (true) {
2981
+ const summaryChoice = await this.showExtensionSelector("Summarize branch?", [
2982
+ "No summary",
2983
+ "Summarize",
2984
+ "Summarize with custom prompt",
2985
+ ]);
2986
+ if (summaryChoice === undefined) {
2987
+ // User pressed escape - re-show tree selector with same selection
2988
+ this.showTreeSelector(entryId);
2989
+ return;
2990
+ }
2991
+ wantsSummary = summaryChoice !== "No summary";
2992
+ if (summaryChoice === "Summarize with custom prompt") {
2993
+ customInstructions = await this.showExtensionEditor("Custom summarization instructions");
2994
+ if (customInstructions === undefined) {
2995
+ // User cancelled - loop back to summary selector
2996
+ continue;
2997
+ }
2909
2998
  }
2999
+ // User made a complete choice
3000
+ break;
2910
3001
  }
2911
- // User made a complete choice
2912
- break;
2913
3002
  }
2914
3003
  // Set up escape handler and loader if summarizing
2915
3004
  let summaryLoader;
@@ -2962,7 +3051,7 @@ export class InteractiveMode {
2962
3051
  }, (entryId, label) => {
2963
3052
  this.sessionManager.appendLabelChange(entryId, label);
2964
3053
  this.ui.requestRender();
2965
- }, initialSelectedId);
3054
+ }, initialSelectedId, initialFilterMode);
2966
3055
  return { component: selector, focus: selector };
2967
3056
  });
2968
3057
  }
@@ -3027,7 +3116,9 @@ export class InteractiveMode {
3027
3116
  }
3028
3117
  else {
3029
3118
  // Logout flow
3030
- const providerInfo = getOAuthProviders().find((p) => p.id === providerId);
3119
+ const providerInfo = this.session.modelRegistry.authStorage
3120
+ .getOAuthProviders()
3121
+ .find((p) => p.id === providerId);
3031
3122
  const providerName = providerInfo?.name || providerId;
3032
3123
  try {
3033
3124
  this.session.modelRegistry.authStorage.logout(providerId);
@@ -3047,7 +3138,7 @@ export class InteractiveMode {
3047
3138
  });
3048
3139
  }
3049
3140
  async showLoginDialog(providerId) {
3050
- const providerInfo = getOAuthProviders().find((p) => p.id === providerId);
3141
+ const providerInfo = this.session.modelRegistry.authStorage.getOAuthProviders().find((p) => p.id === providerId);
3051
3142
  const providerName = providerInfo?.name || providerId;
3052
3143
  // Providers that use callback servers (can paste redirect URL)
3053
3144
  const usesCallbackServer = providerInfo?.usesCallbackServer ?? false;
@@ -3137,7 +3228,7 @@ export class InteractiveMode {
3137
3228
  return;
3138
3229
  }
3139
3230
  this.resetExtensionUI();
3140
- const loader = new BorderedLoader(this.ui, theme, "Reloading extensions, skills, prompts, themes...", {
3231
+ const loader = new BorderedLoader(this.ui, theme, "Reloading keybindings, extensions, skills, prompts, themes...", {
3141
3232
  cancellable: false,
3142
3233
  });
3143
3234
  const previousEditor = this.editor;
@@ -3154,6 +3245,7 @@ export class InteractiveMode {
3154
3245
  };
3155
3246
  try {
3156
3247
  await this.session.reload();
3248
+ this.keybindings.reload();
3157
3249
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
3158
3250
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
3159
3251
  const themeName = this.settingsManager.getTheme();
@@ -3187,7 +3279,7 @@ export class InteractiveMode {
3187
3279
  if (modelsJsonError) {
3188
3280
  this.showError(`models.json error: ${modelsJsonError}`);
3189
3281
  }
3190
- this.showStatus("Reloaded extensions, skills, prompts, themes");
3282
+ this.showStatus("Reloaded keybindings, extensions, skills, prompts, themes");
3191
3283
  }
3192
3284
  catch (error) {
3193
3285
  dismissLoader(previousEditor);
@@ -3198,13 +3290,58 @@ export class InteractiveMode {
3198
3290
  const parts = text.split(/\s+/);
3199
3291
  const outputPath = parts.length > 1 ? parts[1] : undefined;
3200
3292
  try {
3201
- const filePath = await this.session.exportToHtml(outputPath);
3202
- this.showStatus(`Session exported to: ${filePath}`);
3293
+ if (outputPath?.endsWith(".jsonl")) {
3294
+ const filePath = this.session.exportToJsonl(outputPath);
3295
+ this.showStatus(`Session exported to: ${filePath}`);
3296
+ }
3297
+ else {
3298
+ const filePath = await this.session.exportToHtml(outputPath);
3299
+ this.showStatus(`Session exported to: ${filePath}`);
3300
+ }
3203
3301
  }
3204
3302
  catch (error) {
3205
3303
  this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
3206
3304
  }
3207
3305
  }
3306
+ async handleImportCommand(text) {
3307
+ const parts = text.split(/\s+/);
3308
+ if (parts.length < 2 || !parts[1]) {
3309
+ this.showError("Usage: /import <path.jsonl>");
3310
+ return;
3311
+ }
3312
+ const inputPath = parts[1];
3313
+ const confirmed = await this.showExtensionConfirm("Import session", `Replace current session with ${inputPath}?`);
3314
+ if (!confirmed) {
3315
+ this.showStatus("Import cancelled");
3316
+ return;
3317
+ }
3318
+ try {
3319
+ // Stop loading animation
3320
+ if (this.loadingAnimation) {
3321
+ this.loadingAnimation.stop();
3322
+ this.loadingAnimation = undefined;
3323
+ }
3324
+ this.statusContainer.clear();
3325
+ // Clear UI state
3326
+ this.pendingMessagesContainer.clear();
3327
+ this.compactionQueuedMessages = [];
3328
+ this.streamingComponent = undefined;
3329
+ this.streamingMessage = undefined;
3330
+ this.pendingTools.clear();
3331
+ const success = await this.session.importFromJsonl(inputPath);
3332
+ if (!success) {
3333
+ this.showWarning("Import cancelled");
3334
+ return;
3335
+ }
3336
+ // Clear and re-render the chat
3337
+ this.chatContainer.clear();
3338
+ this.renderInitialMessages();
3339
+ this.showStatus(`Session imported from: ${inputPath}`);
3340
+ }
3341
+ catch (error) {
3342
+ this.showError(`Failed to import session: ${error instanceof Error ? error.message : "Unknown error"}`);
3343
+ }
3344
+ }
3208
3345
  async handleShareCommand() {
3209
3346
  // Check if gh is available and logged in
3210
3347
  try {
@@ -3292,14 +3429,14 @@ export class InteractiveMode {
3292
3429
  }
3293
3430
  }
3294
3431
  }
3295
- handleCopyCommand() {
3432
+ async handleCopyCommand() {
3296
3433
  const text = this.session.getLastAssistantText();
3297
3434
  if (!text) {
3298
3435
  this.showError("No agent messages to copy yet.");
3299
3436
  return;
3300
3437
  }
3301
3438
  try {
3302
- copyToClipboard(text);
3439
+ await copyToClipboard(text);
3303
3440
  this.showStatus("Copied last agent message to clipboard");
3304
3441
  }
3305
3442
  catch (error) {
@@ -3402,6 +3539,10 @@ export class InteractiveMode {
3402
3539
  }
3403
3540
  handleHotkeysCommand() {
3404
3541
  // Navigation keybindings
3542
+ const cursorUp = this.getEditorKeyDisplay("cursorUp");
3543
+ const cursorDown = this.getEditorKeyDisplay("cursorDown");
3544
+ const cursorLeft = this.getEditorKeyDisplay("cursorLeft");
3545
+ const cursorRight = this.getEditorKeyDisplay("cursorRight");
3405
3546
  const cursorWordLeft = this.getEditorKeyDisplay("cursorWordLeft");
3406
3547
  const cursorWordRight = this.getEditorKeyDisplay("cursorWordRight");
3407
3548
  const cursorLineStart = this.getEditorKeyDisplay("cursorLineStart");
@@ -3432,13 +3573,15 @@ export class InteractiveMode {
3432
3573
  const expandTools = this.getAppKeyDisplay("expandTools");
3433
3574
  const toggleThinking = this.getAppKeyDisplay("toggleThinking");
3434
3575
  const externalEditor = this.getAppKeyDisplay("externalEditor");
3576
+ const cycleModelBackward = this.getAppKeyDisplay("cycleModelBackward");
3435
3577
  const followUp = this.getAppKeyDisplay("followUp");
3436
3578
  const dequeue = this.getAppKeyDisplay("dequeue");
3579
+ const pasteImage = this.getAppKeyDisplay("pasteImage");
3437
3580
  let hotkeys = `
3438
3581
  **Navigation**
3439
3582
  | Key | Action |
3440
3583
  |-----|--------|
3441
- | \`Arrow keys\` | Move cursor / browse history (Up when empty) |
3584
+ | \`${cursorUp}\` / \`${cursorDown}\` / \`${cursorLeft}\` / \`${cursorRight}\` | Move cursor / browse history (Up when empty) |
3442
3585
  | \`${cursorWordLeft}\` / \`${cursorWordRight}\` | Move by word |
3443
3586
  | \`${cursorLineStart}\` | Start of line |
3444
3587
  | \`${cursorLineEnd}\` | End of line |
@@ -3468,14 +3611,14 @@ export class InteractiveMode {
3468
3611
  | \`${exit}\` | Exit (when editor is empty) |
3469
3612
  | \`${suspend}\` | Suspend to background |
3470
3613
  | \`${cycleThinkingLevel}\` | Cycle thinking level |
3471
- | \`${cycleModelForward}\` | Cycle models |
3614
+ | \`${cycleModelForward}\` / \`${cycleModelBackward}\` | Cycle models |
3472
3615
  | \`${selectModel}\` | Open model selector |
3473
3616
  | \`${expandTools}\` | Toggle tool output expansion |
3474
3617
  | \`${toggleThinking}\` | Toggle thinking block visibility |
3475
3618
  | \`${externalEditor}\` | Edit message in external editor |
3476
3619
  | \`${followUp}\` | Queue follow-up message |
3477
3620
  | \`${dequeue}\` | Restore queued messages |
3478
- | \`Ctrl+V\` | Paste image from clipboard |
3621
+ | \`${pasteImage}\` | Paste image from clipboard |
3479
3622
  | \`/\` | Slash commands |
3480
3623
  | \`!\` | Run bash command |
3481
3624
  | \`!!\` | Run bash command (excluded from context) |
@@ -3515,6 +3658,7 @@ export class InteractiveMode {
3515
3658
  // New session via session (emits extension session events)
3516
3659
  await this.session.newSession();
3517
3660
  // Clear UI state
3661
+ this.headerContainer.clear();
3518
3662
  this.chatContainer.clear();
3519
3663
  this.pendingMessagesContainer.clear();
3520
3664
  this.compactionQueuedMessages = [];