@codex-infinity/pi-infinity 0.52.4 → 0.60.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 (258) hide show
  1. package/CHANGELOG.md +386 -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 +28 -6
  23. package/dist/core/agent-session.d.ts.map +1 -1
  24. package/dist/core/agent-session.js +289 -69
  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/export-html/index.d.ts +5 -2
  45. package/dist/core/export-html/index.d.ts.map +1 -1
  46. package/dist/core/export-html/index.js +4 -3
  47. package/dist/core/export-html/index.js.map +1 -1
  48. package/dist/core/export-html/template.js +11 -14
  49. package/dist/core/export-html/tool-renderer.d.ts +5 -2
  50. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  51. package/dist/core/export-html/tool-renderer.js +17 -4
  52. package/dist/core/export-html/tool-renderer.js.map +1 -1
  53. package/dist/core/extensions/index.d.ts +2 -2
  54. package/dist/core/extensions/index.d.ts.map +1 -1
  55. package/dist/core/extensions/index.js +1 -1
  56. package/dist/core/extensions/index.js.map +1 -1
  57. package/dist/core/extensions/loader.d.ts.map +1 -1
  58. package/dist/core/extensions/loader.js +37 -11
  59. package/dist/core/extensions/loader.js.map +1 -1
  60. package/dist/core/extensions/runner.d.ts +8 -4
  61. package/dist/core/extensions/runner.d.ts.map +1 -1
  62. package/dist/core/extensions/runner.js +77 -8
  63. package/dist/core/extensions/runner.js.map +1 -1
  64. package/dist/core/extensions/types.d.ts +56 -4
  65. package/dist/core/extensions/types.d.ts.map +1 -1
  66. package/dist/core/extensions/types.js.map +1 -1
  67. package/dist/core/extensions/wrapper.d.ts +4 -11
  68. package/dist/core/extensions/wrapper.d.ts.map +1 -1
  69. package/dist/core/extensions/wrapper.js +4 -78
  70. package/dist/core/extensions/wrapper.js.map +1 -1
  71. package/dist/core/footer-data-provider.d.ts +6 -1
  72. package/dist/core/footer-data-provider.d.ts.map +1 -1
  73. package/dist/core/footer-data-provider.js +83 -37
  74. package/dist/core/footer-data-provider.js.map +1 -1
  75. package/dist/core/index.d.ts +1 -1
  76. package/dist/core/index.d.ts.map +1 -1
  77. package/dist/core/index.js +1 -1
  78. package/dist/core/index.js.map +1 -1
  79. package/dist/core/keybindings.d.ts +3 -0
  80. package/dist/core/keybindings.d.ts.map +1 -1
  81. package/dist/core/keybindings.js +22 -12
  82. package/dist/core/keybindings.js.map +1 -1
  83. package/dist/core/model-registry.d.ts +11 -0
  84. package/dist/core/model-registry.d.ts.map +1 -1
  85. package/dist/core/model-registry.js +56 -16
  86. package/dist/core/model-registry.js.map +1 -1
  87. package/dist/core/model-resolver.d.ts +6 -0
  88. package/dist/core/model-resolver.d.ts.map +1 -1
  89. package/dist/core/model-resolver.js +122 -39
  90. package/dist/core/model-resolver.js.map +1 -1
  91. package/dist/core/package-manager.d.ts +19 -1
  92. package/dist/core/package-manager.d.ts.map +1 -1
  93. package/dist/core/package-manager.js +290 -57
  94. package/dist/core/package-manager.js.map +1 -1
  95. package/dist/core/resolve-config-value.d.ts.map +1 -1
  96. package/dist/core/resolve-config-value.js +43 -8
  97. package/dist/core/resolve-config-value.js.map +1 -1
  98. package/dist/core/resource-loader.d.ts.map +1 -1
  99. package/dist/core/resource-loader.js +4 -7
  100. package/dist/core/resource-loader.js.map +1 -1
  101. package/dist/core/sdk.d.ts +1 -1
  102. package/dist/core/sdk.d.ts.map +1 -1
  103. package/dist/core/sdk.js +7 -0
  104. package/dist/core/sdk.js.map +1 -1
  105. package/dist/core/session-manager.d.ts +1 -0
  106. package/dist/core/session-manager.d.ts.map +1 -1
  107. package/dist/core/session-manager.js +21 -15
  108. package/dist/core/session-manager.js.map +1 -1
  109. package/dist/core/settings-manager.d.ts +10 -0
  110. package/dist/core/settings-manager.d.ts.map +1 -1
  111. package/dist/core/settings-manager.js +59 -5
  112. package/dist/core/settings-manager.js.map +1 -1
  113. package/dist/core/skills.d.ts +3 -2
  114. package/dist/core/skills.d.ts.map +1 -1
  115. package/dist/core/skills.js +29 -8
  116. package/dist/core/skills.js.map +1 -1
  117. package/dist/core/slash-commands.d.ts.map +1 -1
  118. package/dist/core/slash-commands.js +1 -1
  119. package/dist/core/slash-commands.js.map +1 -1
  120. package/dist/core/system-prompt.d.ts +4 -0
  121. package/dist/core/system-prompt.d.ts.map +1 -1
  122. package/dist/core/system-prompt.js +43 -29
  123. package/dist/core/system-prompt.js.map +1 -1
  124. package/dist/core/tools/bash.d.ts +8 -0
  125. package/dist/core/tools/bash.d.ts.map +1 -1
  126. package/dist/core/tools/bash.js +75 -69
  127. package/dist/core/tools/bash.js.map +1 -1
  128. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  129. package/dist/core/tools/edit-diff.js +1 -0
  130. package/dist/core/tools/edit-diff.js.map +1 -1
  131. package/dist/core/tools/find.d.ts.map +1 -1
  132. package/dist/core/tools/find.js +6 -3
  133. package/dist/core/tools/find.js.map +1 -1
  134. package/dist/core/tools/index.d.ts +1 -1
  135. package/dist/core/tools/index.d.ts.map +1 -1
  136. package/dist/core/tools/index.js +1 -1
  137. package/dist/core/tools/index.js.map +1 -1
  138. package/dist/index.d.ts +3 -3
  139. package/dist/index.d.ts.map +1 -1
  140. package/dist/index.js +2 -2
  141. package/dist/index.js.map +1 -1
  142. package/dist/main.d.ts.map +1 -1
  143. package/dist/main.js +116 -36
  144. package/dist/main.js.map +1 -1
  145. package/dist/modes/interactive/components/extension-editor.d.ts +5 -2
  146. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  147. package/dist/modes/interactive/components/extension-editor.js +9 -0
  148. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  149. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  150. package/dist/modes/interactive/components/footer.js +8 -23
  151. package/dist/modes/interactive/components/footer.js.map +1 -1
  152. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  153. package/dist/modes/interactive/components/login-dialog.js +1 -1
  154. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  155. package/dist/modes/interactive/components/model-selector.d.ts +1 -1
  156. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  157. package/dist/modes/interactive/components/model-selector.js +1 -1
  158. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  159. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  160. package/dist/modes/interactive/components/oauth-selector.js +1 -1
  161. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  162. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  163. package/dist/modes/interactive/components/session-selector.js +1 -1
  164. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  165. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  166. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  167. package/dist/modes/interactive/components/settings-selector.js +15 -1
  168. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  169. package/dist/modes/interactive/components/show-images-selector.d.ts.map +1 -1
  170. package/dist/modes/interactive/components/show-images-selector.js +5 -1
  171. package/dist/modes/interactive/components/show-images-selector.js.map +1 -1
  172. package/dist/modes/interactive/components/theme-selector.d.ts.map +1 -1
  173. package/dist/modes/interactive/components/theme-selector.js +5 -1
  174. package/dist/modes/interactive/components/theme-selector.js.map +1 -1
  175. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  176. package/dist/modes/interactive/components/thinking-selector.js +5 -1
  177. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  178. package/dist/modes/interactive/components/tool-execution.d.ts +7 -0
  179. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  180. package/dist/modes/interactive/components/tool-execution.js +158 -7
  181. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  182. package/dist/modes/interactive/components/tree-selector.d.ts +21 -2
  183. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  184. package/dist/modes/interactive/components/tree-selector.js +127 -10
  185. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  186. package/dist/modes/interactive/components/user-message.d.ts +1 -0
  187. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  188. package/dist/modes/interactive/components/user-message.js +12 -0
  189. package/dist/modes/interactive/components/user-message.js.map +1 -1
  190. package/dist/modes/interactive/interactive-mode.d.ts +4 -1
  191. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  192. package/dist/modes/interactive/interactive-mode.js +160 -66
  193. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  194. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  195. package/dist/modes/interactive/theme/theme.js +5 -0
  196. package/dist/modes/interactive/theme/theme.js.map +1 -1
  197. package/dist/modes/rpc/jsonl.d.ts +17 -0
  198. package/dist/modes/rpc/jsonl.d.ts.map +1 -0
  199. package/dist/modes/rpc/jsonl.js +49 -0
  200. package/dist/modes/rpc/jsonl.js.map +1 -0
  201. package/dist/modes/rpc/rpc-client.d.ts +1 -1
  202. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  203. package/dist/modes/rpc/rpc-client.js +7 -11
  204. package/dist/modes/rpc/rpc-client.js.map +1 -1
  205. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  206. package/dist/modes/rpc/rpc-mode.js +9 -11
  207. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  208. package/dist/utils/clipboard-image.d.ts.map +1 -1
  209. package/dist/utils/clipboard-image.js +94 -11
  210. package/dist/utils/clipboard-image.js.map +1 -1
  211. package/dist/utils/clipboard.d.ts.map +1 -1
  212. package/dist/utils/clipboard.js +16 -15
  213. package/dist/utils/clipboard.js.map +1 -1
  214. package/dist/utils/exif-orientation.d.ts +5 -0
  215. package/dist/utils/exif-orientation.d.ts.map +1 -0
  216. package/dist/utils/exif-orientation.js +158 -0
  217. package/dist/utils/exif-orientation.js.map +1 -0
  218. package/dist/utils/image-convert.d.ts.map +1 -1
  219. package/dist/utils/image-convert.js +5 -1
  220. package/dist/utils/image-convert.js.map +1 -1
  221. package/dist/utils/image-resize.d.ts.map +1 -1
  222. package/dist/utils/image-resize.js +6 -1
  223. package/dist/utils/image-resize.js.map +1 -1
  224. package/dist/utils/tools-manager.d.ts.map +1 -1
  225. package/dist/utils/tools-manager.js +66 -21
  226. package/dist/utils/tools-manager.js.map +1 -1
  227. package/docs/compaction.md +2 -0
  228. package/docs/custom-provider.md +57 -9
  229. package/docs/extensions.md +125 -12
  230. package/docs/keybindings.md +11 -1
  231. package/docs/models.md +44 -2
  232. package/docs/packages.md +9 -0
  233. package/docs/providers.md +10 -1
  234. package/docs/rpc.md +44 -7
  235. package/docs/sdk.md +2 -2
  236. package/docs/settings.md +11 -0
  237. package/docs/terminal-setup.md +39 -3
  238. package/docs/tmux.md +61 -0
  239. package/docs/tree.md +9 -0
  240. package/examples/extensions/README.md +2 -0
  241. package/examples/extensions/antigravity-image-gen.ts +8 -5
  242. package/examples/extensions/built-in-tool-renderer.ts +246 -0
  243. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  244. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  245. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  246. package/examples/extensions/custom-provider-gitlab-duo/test.ts +2 -2
  247. package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
  248. package/examples/extensions/dynamic-tools.ts +74 -0
  249. package/examples/extensions/overlay-qa-tests.ts +468 -1
  250. package/examples/extensions/preset.ts +2 -3
  251. package/examples/extensions/provider-payload.ts +14 -0
  252. package/examples/extensions/sandbox/index.ts +2 -3
  253. package/examples/extensions/subagent/agents.ts +2 -3
  254. package/examples/extensions/tool-override.ts +2 -3
  255. package/examples/extensions/with-deps/index.ts +1 -5
  256. package/examples/extensions/with-deps/package-lock.json +2 -2
  257. package/examples/extensions/with-deps/package.json +1 -1
  258. 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);
@@ -1720,7 +1803,6 @@ export class InteractiveMode {
1720
1803
  for (const content of this.streamingMessage.content) {
1721
1804
  if (content.type === "toolCall") {
1722
1805
  if (!this.pendingTools.has(content.id)) {
1723
- this.chatContainer.addChild(new Text("", 0, 0));
1724
1806
  const component = new ToolExecutionComponent(content.name, content.arguments, {
1725
1807
  showImages: this.settingsManager.getShowImages(),
1726
1808
  }, this.getRegisteredToolDefinition(content.name), this.ui);
@@ -2153,8 +2235,13 @@ export class InteractiveMode {
2153
2235
  await this.shutdown();
2154
2236
  }
2155
2237
  handleCtrlZ() {
2238
+ // Ignore SIGINT while suspended so Ctrl+C in the terminal does not
2239
+ // kill the backgrounded process. The handler is removed on resume.
2240
+ const ignoreSigint = () => { };
2241
+ process.on("SIGINT", ignoreSigint);
2156
2242
  // Set up handler to restore TUI when resumed
2157
2243
  process.once("SIGCONT", () => {
2244
+ process.removeListener("SIGINT", ignoreSigint);
2158
2245
  this.ui.start();
2159
2246
  this.ui.requestRender(true);
2160
2247
  });
@@ -2286,6 +2373,7 @@ export class InteractiveMode {
2286
2373
  // Spawn editor synchronously with inherited stdio for interactive editing
2287
2374
  const result = spawnSync(editor, [...editorArgs, tmpFile], {
2288
2375
  stdio: "inherit",
2376
+ shell: process.platform === "win32",
2289
2377
  });
2290
2378
  // On successful exit (status 0), replace editor content
2291
2379
  if (result.status === 0) {
@@ -2336,6 +2424,16 @@ export class InteractiveMode {
2336
2424
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2337
2425
  this.ui.requestRender();
2338
2426
  }
2427
+ showPackageUpdateNotification(packages) {
2428
+ const action = theme.fg("accent", `${APP_NAME} update`);
2429
+ const updateInstruction = theme.fg("muted", "Package updates are available. Run ") + action;
2430
+ const packageLines = packages.map((pkg) => `- ${pkg}`).join("\n");
2431
+ this.chatContainer.addChild(new Spacer(1));
2432
+ this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2433
+ this.chatContainer.addChild(new Text(`${theme.bold(theme.fg("warning", "Package Updates Available"))}\n${updateInstruction}\n${theme.fg("muted", "Packages:")}\n${packageLines}`, 1, 0));
2434
+ this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2435
+ this.ui.requestRender();
2436
+ }
2339
2437
  /**
2340
2438
  * Get all queued messages (read-only).
2341
2439
  * Combines session queue and compaction queue.
@@ -2539,6 +2637,7 @@ export class InteractiveMode {
2539
2637
  hideThinkingBlock: this.hideThinkingBlock,
2540
2638
  collapseChangelog: this.settingsManager.getCollapseChangelog(),
2541
2639
  doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
2640
+ treeFilterMode: this.settingsManager.getTreeFilterMode(),
2542
2641
  showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
2543
2642
  editorPaddingX: this.settingsManager.getEditorPaddingX(),
2544
2643
  autocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),
@@ -2617,6 +2716,9 @@ export class InteractiveMode {
2617
2716
  onDoubleEscapeActionChange: (action) => {
2618
2717
  this.settingsManager.setDoubleEscapeAction(action);
2619
2718
  },
2719
+ onTreeFilterModeChange: (mode) => {
2720
+ this.settingsManager.setTreeFilterMode(mode);
2721
+ },
2620
2722
  onShowHardwareCursorChange: (enabled) => {
2621
2723
  this.settingsManager.setShowHardwareCursor(enabled);
2622
2724
  this.ui.setShowHardwareCursor(enabled);
@@ -2669,28 +2771,8 @@ export class InteractiveMode {
2669
2771
  this.showModelSelector(searchTerm);
2670
2772
  }
2671
2773
  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
2774
  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;
2775
+ return findExactModelReferenceMatch(searchTerm, models);
2694
2776
  }
2695
2777
  async getModelCandidates() {
2696
2778
  if (this.session.scopedModels.length > 0) {
@@ -2770,12 +2852,10 @@ export class InteractiveMode {
2770
2852
  // Helper to update session's scoped models (session-only, no persist)
2771
2853
  const updateSessionModels = async (enabledIds) => {
2772
2854
  if (enabledIds.size > 0 && enabledIds.size < allModels.length) {
2773
- // Use current session thinking level, not settings default
2774
- const currentThinkingLevel = this.session.thinkingLevel;
2775
2855
  const newScopedModels = await resolveModelScope(Array.from(enabledIds), this.session.modelRegistry);
2776
2856
  this.session.setScopedModels(newScopedModels.map((sm) => ({
2777
2857
  model: sm.model,
2778
- thinkingLevel: sm.thinkingLevel ?? currentThinkingLevel,
2858
+ thinkingLevel: sm.thinkingLevel,
2779
2859
  })));
2780
2860
  }
2781
2861
  else {
@@ -2872,6 +2952,7 @@ export class InteractiveMode {
2872
2952
  showTreeSelector(initialSelectedId) {
2873
2953
  const tree = this.sessionManager.getTree();
2874
2954
  const realLeafId = this.sessionManager.getLeafId();
2955
+ const initialFilterMode = this.settingsManager.getTreeFilterMode();
2875
2956
  if (tree.length === 0) {
2876
2957
  this.showStatus("No entries in session");
2877
2958
  return;
@@ -2889,27 +2970,30 @@ export class InteractiveMode {
2889
2970
  // Loop until user makes a complete choice or cancels to tree
2890
2971
  let wantsSummary = false;
2891
2972
  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;
2973
+ // Check if we should skip the prompt (user preference to always default to no summary)
2974
+ if (!this.settingsManager.getBranchSummarySkipPrompt()) {
2975
+ while (true) {
2976
+ const summaryChoice = await this.showExtensionSelector("Summarize branch?", [
2977
+ "No summary",
2978
+ "Summarize",
2979
+ "Summarize with custom prompt",
2980
+ ]);
2981
+ if (summaryChoice === undefined) {
2982
+ // User pressed escape - re-show tree selector with same selection
2983
+ this.showTreeSelector(entryId);
2984
+ return;
2909
2985
  }
2986
+ wantsSummary = summaryChoice !== "No summary";
2987
+ if (summaryChoice === "Summarize with custom prompt") {
2988
+ customInstructions = await this.showExtensionEditor("Custom summarization instructions");
2989
+ if (customInstructions === undefined) {
2990
+ // User cancelled - loop back to summary selector
2991
+ continue;
2992
+ }
2993
+ }
2994
+ // User made a complete choice
2995
+ break;
2910
2996
  }
2911
- // User made a complete choice
2912
- break;
2913
2997
  }
2914
2998
  // Set up escape handler and loader if summarizing
2915
2999
  let summaryLoader;
@@ -2962,7 +3046,7 @@ export class InteractiveMode {
2962
3046
  }, (entryId, label) => {
2963
3047
  this.sessionManager.appendLabelChange(entryId, label);
2964
3048
  this.ui.requestRender();
2965
- }, initialSelectedId);
3049
+ }, initialSelectedId, initialFilterMode);
2966
3050
  return { component: selector, focus: selector };
2967
3051
  });
2968
3052
  }
@@ -3027,7 +3111,9 @@ export class InteractiveMode {
3027
3111
  }
3028
3112
  else {
3029
3113
  // Logout flow
3030
- const providerInfo = getOAuthProviders().find((p) => p.id === providerId);
3114
+ const providerInfo = this.session.modelRegistry.authStorage
3115
+ .getOAuthProviders()
3116
+ .find((p) => p.id === providerId);
3031
3117
  const providerName = providerInfo?.name || providerId;
3032
3118
  try {
3033
3119
  this.session.modelRegistry.authStorage.logout(providerId);
@@ -3047,7 +3133,7 @@ export class InteractiveMode {
3047
3133
  });
3048
3134
  }
3049
3135
  async showLoginDialog(providerId) {
3050
- const providerInfo = getOAuthProviders().find((p) => p.id === providerId);
3136
+ const providerInfo = this.session.modelRegistry.authStorage.getOAuthProviders().find((p) => p.id === providerId);
3051
3137
  const providerName = providerInfo?.name || providerId;
3052
3138
  // Providers that use callback servers (can paste redirect URL)
3053
3139
  const usesCallbackServer = providerInfo?.usesCallbackServer ?? false;
@@ -3137,7 +3223,7 @@ export class InteractiveMode {
3137
3223
  return;
3138
3224
  }
3139
3225
  this.resetExtensionUI();
3140
- const loader = new BorderedLoader(this.ui, theme, "Reloading extensions, skills, prompts, themes...", {
3226
+ const loader = new BorderedLoader(this.ui, theme, "Reloading keybindings, extensions, skills, prompts, themes...", {
3141
3227
  cancellable: false,
3142
3228
  });
3143
3229
  const previousEditor = this.editor;
@@ -3154,6 +3240,7 @@ export class InteractiveMode {
3154
3240
  };
3155
3241
  try {
3156
3242
  await this.session.reload();
3243
+ this.keybindings.reload();
3157
3244
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
3158
3245
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
3159
3246
  const themeName = this.settingsManager.getTheme();
@@ -3187,7 +3274,7 @@ export class InteractiveMode {
3187
3274
  if (modelsJsonError) {
3188
3275
  this.showError(`models.json error: ${modelsJsonError}`);
3189
3276
  }
3190
- this.showStatus("Reloaded extensions, skills, prompts, themes");
3277
+ this.showStatus("Reloaded keybindings, extensions, skills, prompts, themes");
3191
3278
  }
3192
3279
  catch (error) {
3193
3280
  dismissLoader(previousEditor);
@@ -3402,6 +3489,10 @@ export class InteractiveMode {
3402
3489
  }
3403
3490
  handleHotkeysCommand() {
3404
3491
  // Navigation keybindings
3492
+ const cursorUp = this.getEditorKeyDisplay("cursorUp");
3493
+ const cursorDown = this.getEditorKeyDisplay("cursorDown");
3494
+ const cursorLeft = this.getEditorKeyDisplay("cursorLeft");
3495
+ const cursorRight = this.getEditorKeyDisplay("cursorRight");
3405
3496
  const cursorWordLeft = this.getEditorKeyDisplay("cursorWordLeft");
3406
3497
  const cursorWordRight = this.getEditorKeyDisplay("cursorWordRight");
3407
3498
  const cursorLineStart = this.getEditorKeyDisplay("cursorLineStart");
@@ -3432,13 +3523,15 @@ export class InteractiveMode {
3432
3523
  const expandTools = this.getAppKeyDisplay("expandTools");
3433
3524
  const toggleThinking = this.getAppKeyDisplay("toggleThinking");
3434
3525
  const externalEditor = this.getAppKeyDisplay("externalEditor");
3526
+ const cycleModelBackward = this.getAppKeyDisplay("cycleModelBackward");
3435
3527
  const followUp = this.getAppKeyDisplay("followUp");
3436
3528
  const dequeue = this.getAppKeyDisplay("dequeue");
3529
+ const pasteImage = this.getAppKeyDisplay("pasteImage");
3437
3530
  let hotkeys = `
3438
3531
  **Navigation**
3439
3532
  | Key | Action |
3440
3533
  |-----|--------|
3441
- | \`Arrow keys\` | Move cursor / browse history (Up when empty) |
3534
+ | \`${cursorUp}\` / \`${cursorDown}\` / \`${cursorLeft}\` / \`${cursorRight}\` | Move cursor / browse history (Up when empty) |
3442
3535
  | \`${cursorWordLeft}\` / \`${cursorWordRight}\` | Move by word |
3443
3536
  | \`${cursorLineStart}\` | Start of line |
3444
3537
  | \`${cursorLineEnd}\` | End of line |
@@ -3468,14 +3561,14 @@ export class InteractiveMode {
3468
3561
  | \`${exit}\` | Exit (when editor is empty) |
3469
3562
  | \`${suspend}\` | Suspend to background |
3470
3563
  | \`${cycleThinkingLevel}\` | Cycle thinking level |
3471
- | \`${cycleModelForward}\` | Cycle models |
3564
+ | \`${cycleModelForward}\` / \`${cycleModelBackward}\` | Cycle models |
3472
3565
  | \`${selectModel}\` | Open model selector |
3473
3566
  | \`${expandTools}\` | Toggle tool output expansion |
3474
3567
  | \`${toggleThinking}\` | Toggle thinking block visibility |
3475
3568
  | \`${externalEditor}\` | Edit message in external editor |
3476
3569
  | \`${followUp}\` | Queue follow-up message |
3477
3570
  | \`${dequeue}\` | Restore queued messages |
3478
- | \`Ctrl+V\` | Paste image from clipboard |
3571
+ | \`${pasteImage}\` | Paste image from clipboard |
3479
3572
  | \`/\` | Slash commands |
3480
3573
  | \`!\` | Run bash command |
3481
3574
  | \`!!\` | Run bash command (excluded from context) |
@@ -3515,6 +3608,7 @@ export class InteractiveMode {
3515
3608
  // New session via session (emits extension session events)
3516
3609
  await this.session.newSession();
3517
3610
  // Clear UI state
3611
+ this.headerContainer.clear();
3518
3612
  this.chatContainer.clear();
3519
3613
  this.pendingMessagesContainer.clear();
3520
3614
  this.compactionQueuedMessages = [];