@mariozechner/pi-coding-agent 0.59.0 → 0.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +15 -2
  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 +7 -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 +1 -0
  12. package/dist/cli/args.d.ts.map +1 -1
  13. package/dist/cli/args.js +4 -0
  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/session-picker.d.ts.map +1 -1
  20. package/dist/cli/session-picker.js +2 -1
  21. package/dist/cli/session-picker.js.map +1 -1
  22. package/dist/cli.d.ts.map +1 -1
  23. package/dist/cli.js +1 -3
  24. package/dist/cli.js.map +1 -1
  25. package/dist/core/agent-session.d.ts +18 -2
  26. package/dist/core/agent-session.d.ts.map +1 -1
  27. package/dist/core/agent-session.js +83 -8
  28. package/dist/core/agent-session.js.map +1 -1
  29. package/dist/core/bash-executor.d.ts +6 -7
  30. package/dist/core/bash-executor.d.ts.map +1 -1
  31. package/dist/core/bash-executor.js +8 -107
  32. package/dist/core/bash-executor.js.map +1 -1
  33. package/dist/core/exec.d.ts.map +1 -1
  34. package/dist/core/exec.js +7 -3
  35. package/dist/core/exec.js.map +1 -1
  36. package/dist/core/export-html/template.css +43 -13
  37. package/dist/core/export-html/template.html +1 -0
  38. package/dist/core/export-html/template.js +107 -0
  39. package/dist/core/extensions/index.d.ts +1 -1
  40. package/dist/core/extensions/index.d.ts.map +1 -1
  41. package/dist/core/extensions/index.js.map +1 -1
  42. package/dist/core/extensions/loader.d.ts.map +1 -1
  43. package/dist/core/extensions/loader.js +4 -4
  44. package/dist/core/extensions/loader.js.map +1 -1
  45. package/dist/core/extensions/runner.d.ts +6 -3
  46. package/dist/core/extensions/runner.d.ts.map +1 -1
  47. package/dist/core/extensions/runner.js +62 -33
  48. package/dist/core/extensions/runner.js.map +1 -1
  49. package/dist/core/extensions/types.d.ts +4 -3
  50. package/dist/core/extensions/types.d.ts.map +1 -1
  51. package/dist/core/extensions/types.js.map +1 -1
  52. package/dist/core/footer-data-provider.d.ts +9 -2
  53. package/dist/core/footer-data-provider.d.ts.map +1 -1
  54. package/dist/core/footer-data-provider.js +85 -13
  55. package/dist/core/footer-data-provider.js.map +1 -1
  56. package/dist/core/keybindings.d.ts +270 -50
  57. package/dist/core/keybindings.d.ts.map +1 -1
  58. package/dist/core/keybindings.js +222 -134
  59. package/dist/core/keybindings.js.map +1 -1
  60. package/dist/core/model-registry.d.ts +1 -0
  61. package/dist/core/model-registry.d.ts.map +1 -1
  62. package/dist/core/model-registry.js +23 -14
  63. package/dist/core/model-registry.js.map +1 -1
  64. package/dist/core/package-manager.d.ts +15 -1
  65. package/dist/core/package-manager.d.ts.map +1 -1
  66. package/dist/core/package-manager.js +194 -15
  67. package/dist/core/package-manager.js.map +1 -1
  68. package/dist/core/sdk.d.ts +2 -2
  69. package/dist/core/sdk.d.ts.map +1 -1
  70. package/dist/core/sdk.js +2 -2
  71. package/dist/core/sdk.js.map +1 -1
  72. package/dist/core/slash-commands.d.ts.map +1 -1
  73. package/dist/core/slash-commands.js +3 -2
  74. package/dist/core/slash-commands.js.map +1 -1
  75. package/dist/core/tools/bash.d.ts +8 -0
  76. package/dist/core/tools/bash.d.ts.map +1 -1
  77. package/dist/core/tools/bash.js +77 -69
  78. package/dist/core/tools/bash.js.map +1 -1
  79. package/dist/core/tools/edit.d.ts.map +1 -1
  80. package/dist/core/tools/edit.js +3 -2
  81. package/dist/core/tools/edit.js.map +1 -1
  82. package/dist/core/tools/file-mutation-queue.d.ts +6 -0
  83. package/dist/core/tools/file-mutation-queue.d.ts.map +1 -0
  84. package/dist/core/tools/file-mutation-queue.js +37 -0
  85. package/dist/core/tools/file-mutation-queue.js.map +1 -0
  86. package/dist/core/tools/index.d.ts +2 -1
  87. package/dist/core/tools/index.d.ts.map +1 -1
  88. package/dist/core/tools/index.js +2 -1
  89. package/dist/core/tools/index.js.map +1 -1
  90. package/dist/core/tools/write.d.ts.map +1 -1
  91. package/dist/core/tools/write.js +6 -3
  92. package/dist/core/tools/write.js.map +1 -1
  93. package/dist/index.d.ts +3 -3
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +2 -2
  96. package/dist/index.js.map +1 -1
  97. package/dist/main.d.ts.map +1 -1
  98. package/dist/main.js +60 -24
  99. package/dist/main.js.map +1 -1
  100. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  101. package/dist/modes/interactive/components/bash-execution.js +4 -4
  102. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  103. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
  104. package/dist/modes/interactive/components/bordered-loader.js +1 -1
  105. package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
  106. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  107. package/dist/modes/interactive/components/branch-summary-message.js +2 -2
  108. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  109. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  110. package/dist/modes/interactive/components/compaction-summary-message.js +2 -2
  111. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  112. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  113. package/dist/modes/interactive/components/config-selector.js +8 -8
  114. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  115. package/dist/modes/interactive/components/custom-editor.d.ts +3 -3
  116. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  117. package/dist/modes/interactive/components/custom-editor.js +6 -6
  118. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  119. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  120. package/dist/modes/interactive/components/extension-editor.js +9 -9
  121. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  122. package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  123. package/dist/modes/interactive/components/extension-input.js +5 -5
  124. package/dist/modes/interactive/components/extension-input.js.map +1 -1
  125. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  126. package/dist/modes/interactive/components/extension-selector.js +8 -8
  127. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  128. package/dist/modes/interactive/components/index.d.ts +1 -1
  129. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  130. package/dist/modes/interactive/components/index.js +1 -1
  131. package/dist/modes/interactive/components/index.js.map +1 -1
  132. package/dist/modes/interactive/components/keybinding-hints.d.ts +3 -36
  133. package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
  134. package/dist/modes/interactive/components/keybinding-hints.js +5 -44
  135. package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
  136. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  137. package/dist/modes/interactive/components/login-dialog.js +6 -6
  138. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  139. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  140. package/dist/modes/interactive/components/model-selector.js +12 -8
  141. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  142. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  143. package/dist/modes/interactive/components/oauth-selector.js +6 -6
  144. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  145. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  146. package/dist/modes/interactive/components/scoped-models-selector.js +4 -4
  147. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  148. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  149. package/dist/modes/interactive/components/session-selector.js +32 -35
  150. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  151. package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  152. package/dist/modes/interactive/components/skill-invocation-message.js +2 -2
  153. package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  154. package/dist/modes/interactive/components/tool-execution.d.ts +7 -0
  155. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  156. package/dist/modes/interactive/components/tool-execution.js +51 -6
  157. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  158. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  159. package/dist/modes/interactive/components/tree-selector.js +15 -15
  160. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  161. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  162. package/dist/modes/interactive/components/user-message-selector.js +6 -6
  163. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  164. package/dist/modes/interactive/interactive-mode.d.ts +3 -0
  165. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  166. package/dist/modes/interactive/interactive-mode.js +184 -87
  167. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  168. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  169. package/dist/modes/interactive/theme/theme.js +49 -37
  170. package/dist/modes/interactive/theme/theme.js.map +1 -1
  171. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  172. package/dist/modes/rpc/rpc-mode.js +4 -1
  173. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  174. package/dist/utils/child-process.d.ts +11 -0
  175. package/dist/utils/child-process.d.ts.map +1 -0
  176. package/dist/utils/child-process.js +78 -0
  177. package/dist/utils/child-process.js.map +1 -0
  178. package/dist/utils/clipboard-native.d.ts +1 -0
  179. package/dist/utils/clipboard-native.d.ts.map +1 -1
  180. package/dist/utils/clipboard-native.js.map +1 -1
  181. package/dist/utils/clipboard.d.ts +1 -1
  182. package/dist/utils/clipboard.d.ts.map +1 -1
  183. package/dist/utils/clipboard.js +11 -1
  184. package/dist/utils/clipboard.js.map +1 -1
  185. package/docs/extensions.md +59 -8
  186. package/docs/keybindings.md +103 -112
  187. package/docs/providers.md +7 -0
  188. package/docs/rpc.md +4 -4
  189. package/docs/sdk.md +2 -2
  190. package/examples/extensions/antigravity-image-gen.ts +5 -3
  191. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  192. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  193. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  194. package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
  195. package/examples/extensions/subagent/index.ts +7 -5
  196. package/examples/extensions/tool-override.ts +9 -7
  197. package/examples/extensions/truncated-tool.ts +6 -3
  198. package/examples/extensions/with-deps/package-lock.json +2 -2
  199. package/examples/extensions/with-deps/package.json +1 -1
  200. package/package.json +5 -5
@@ -6,14 +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 { CombinedAutocompleteProvider, Container, fuzzyFilter, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
9
+ import { CombinedAutocompleteProvider, Container, fuzzyFilter, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
10
10
  import { spawn, spawnSync } from "child_process";
11
- import { APP_NAME, getAuthPath, getDebugLogPath, getShareViewerUrl, getUpdateInstruction, VERSION, } from "../../config.js";
11
+ import { APP_NAME, getAgentDir, getAuthPath, getDebugLogPath, getShareViewerUrl, getUpdateInstruction, VERSION, } from "../../config.js";
12
12
  import { parseSkillBlock } from "../../core/agent-session.js";
13
13
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
14
14
  import { KeybindingsManager } from "../../core/keybindings.js";
15
15
  import { createCompactionSummaryMessage } from "../../core/messages.js";
16
16
  import { findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
17
+ import { DefaultPackageManager } from "../../core/package-manager.js";
17
18
  import { SessionManager } from "../../core/session-manager.js";
18
19
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
19
20
  import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
@@ -34,7 +35,7 @@ import { ExtensionEditorComponent } from "./components/extension-editor.js";
34
35
  import { ExtensionInputComponent } from "./components/extension-input.js";
35
36
  import { ExtensionSelectorComponent } from "./components/extension-selector.js";
36
37
  import { FooterComponent } from "./components/footer.js";
37
- import { appKey, appKeyHint, editorKey, keyHint, rawKeyHint } from "./components/keybinding-hints.js";
38
+ import { keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
38
39
  import { LoginDialogComponent } from "./components/login-dialog.js";
39
40
  import { ModelSelectorComponent } from "./components/model-selector.js";
40
41
  import { OAuthSelectorComponent } from "./components/oauth-selector.js";
@@ -64,6 +65,7 @@ export class InteractiveMode {
64
65
  editorContainer;
65
66
  footer;
66
67
  footerDataProvider;
68
+ // Stored so the same manager can be injected into custom editors, selectors, and extension UI.
67
69
  keybindings;
68
70
  version;
69
71
  isInitialized = false;
@@ -147,6 +149,7 @@ export class InteractiveMode {
147
149
  this.widgetContainerAbove = new Container();
148
150
  this.widgetContainerBelow = new Container();
149
151
  this.keybindings = KeybindingsManager.create();
152
+ setKeybindings(this.keybindings);
150
153
  const editorPaddingX = this.settingsManager.getEditorPaddingX();
151
154
  const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
152
155
  this.defaultEditor = new CustomEditor(this.ui, getEditorTheme(), this.keybindings, {
@@ -241,27 +244,26 @@ export class InteractiveMode {
241
244
  if (this.options.verbose || !this.settingsManager.getQuietStartup()) {
242
245
  const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
243
246
  // Build startup instructions using keybinding hint helpers
244
- const kb = this.keybindings;
245
- const hint = (action, desc) => appKeyHint(kb, action, desc);
247
+ const hint = (keybinding, description) => keyHint(keybinding, description);
246
248
  const instructions = [
247
- hint("interrupt", "to interrupt"),
248
- hint("clear", "to clear"),
249
- rawKeyHint(`${appKey(kb, "clear")} twice`, "to exit"),
250
- hint("exit", "to exit (empty)"),
251
- hint("suspend", "to suspend"),
252
- keyHint("deleteToLineEnd", "to delete to end"),
253
- hint("cycleThinkingLevel", "to cycle thinking level"),
254
- rawKeyHint(`${appKey(kb, "cycleModelForward")}/${appKey(kb, "cycleModelBackward")}`, "to cycle models"),
255
- hint("selectModel", "to select model"),
256
- hint("expandTools", "to expand tools"),
257
- hint("toggleThinking", "to expand thinking"),
258
- hint("externalEditor", "for external editor"),
249
+ hint("app.interrupt", "to interrupt"),
250
+ hint("app.clear", "to clear"),
251
+ rawKeyHint(`${keyText("app.clear")} twice`, "to exit"),
252
+ hint("app.exit", "to exit (empty)"),
253
+ hint("app.suspend", "to suspend"),
254
+ keyHint("tui.editor.deleteToLineEnd", "to delete to end"),
255
+ hint("app.thinking.cycle", "to cycle thinking level"),
256
+ rawKeyHint(`${keyText("app.model.cycleForward")}/${keyText("app.model.cycleBackward")}`, "to cycle models"),
257
+ hint("app.model.select", "to select model"),
258
+ hint("app.tools.expand", "to expand tools"),
259
+ hint("app.thinking.toggle", "to expand thinking"),
260
+ hint("app.editor.external", "for external editor"),
259
261
  rawKeyHint("/", "for commands"),
260
262
  rawKeyHint("!", "to run bash"),
261
263
  rawKeyHint("!!", "to run bash (no context)"),
262
- hint("followUp", "to queue follow-up"),
263
- hint("dequeue", "to edit all queued messages"),
264
- hint("pasteImage", "to paste image"),
264
+ hint("app.message.followUp", "to queue follow-up"),
265
+ hint("app.message.dequeue", "to edit all queued messages"),
266
+ hint("app.clipboard.pasteImage", "to paste image"),
265
267
  rawKeyHint("drop files", "to attach"),
266
268
  ].join("\n");
267
269
  this.builtInHeader = new Text(`${logo}\n${instructions}`, 1, 0);
@@ -360,6 +362,12 @@ export class InteractiveMode {
360
362
  this.showNewVersionNotification(newVersion);
361
363
  }
362
364
  });
365
+ // Start package update check asynchronously
366
+ this.checkForPackageUpdates().then((updates) => {
367
+ if (updates.length > 0) {
368
+ this.showPackageUpdateNotification(updates);
369
+ }
370
+ });
363
371
  // Check tmux keyboard setup asynchronously
364
372
  this.checkTmuxKeyboardSetup().then((warning) => {
365
373
  if (warning) {
@@ -434,6 +442,23 @@ export class InteractiveMode {
434
442
  return undefined;
435
443
  }
436
444
  }
445
+ async checkForPackageUpdates() {
446
+ if (process.env.PI_OFFLINE) {
447
+ return [];
448
+ }
449
+ try {
450
+ const packageManager = new DefaultPackageManager({
451
+ cwd: process.cwd(),
452
+ agentDir: getAgentDir(),
453
+ settingsManager: this.settingsManager,
454
+ });
455
+ const updates = await packageManager.checkForAvailableUpdates();
456
+ return updates.map((update) => update.displayName);
457
+ }
458
+ catch {
459
+ return [];
460
+ }
461
+ }
437
462
  async checkTmuxKeyboardSetup() {
438
463
  if (!process.env.TMUX)
439
464
  return undefined;
@@ -464,6 +489,9 @@ export class InteractiveMode {
464
489
  runTmuxShow("extended-keys"),
465
490
  runTmuxShow("extended-keys-format"),
466
491
  ]);
492
+ // If we couldn't query tmux (timeout, sandbox, etc.), don't warn
493
+ if (extendedKeys === undefined)
494
+ return undefined;
467
495
  if (extendedKeys !== "on" && extendedKeys !== "always") {
468
496
  return "tmux extended-keys is off. Modified Enter keys may not work. Add `set -g extended-keys on` to ~/.tmux.conf and restart tmux.";
469
497
  }
@@ -1022,7 +1050,7 @@ export class InteractiveMode {
1022
1050
  this.defaultEditor.onExtensionShortcut = undefined;
1023
1051
  this.updateTerminalTitle();
1024
1052
  if (this.loadingAnimation) {
1025
- this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${appKey(this.keybindings, "interrupt")} to interrupt)`);
1053
+ this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
1026
1054
  }
1027
1055
  }
1028
1056
  // Maximum total widget lines to prevent viewport overflow
@@ -1145,7 +1173,7 @@ export class InteractiveMode {
1145
1173
  this.loadingAnimation.setMessage(message);
1146
1174
  }
1147
1175
  else {
1148
- this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${appKey(this.keybindings, "interrupt")} to interrupt)`);
1176
+ this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
1149
1177
  }
1150
1178
  }
1151
1179
  else {
@@ -1503,24 +1531,24 @@ export class InteractiveMode {
1503
1531
  }
1504
1532
  };
1505
1533
  // Register app action handlers
1506
- this.defaultEditor.onAction("clear", () => this.handleCtrlC());
1534
+ this.defaultEditor.onAction("app.clear", () => this.handleCtrlC());
1507
1535
  this.defaultEditor.onCtrlD = () => this.handleCtrlD();
1508
- this.defaultEditor.onAction("suspend", () => this.handleCtrlZ());
1509
- this.defaultEditor.onAction("cycleThinkingLevel", () => this.cycleThinkingLevel());
1510
- this.defaultEditor.onAction("cycleModelForward", () => this.cycleModel("forward"));
1511
- this.defaultEditor.onAction("cycleModelBackward", () => this.cycleModel("backward"));
1536
+ this.defaultEditor.onAction("app.suspend", () => this.handleCtrlZ());
1537
+ this.defaultEditor.onAction("app.thinking.cycle", () => this.cycleThinkingLevel());
1538
+ this.defaultEditor.onAction("app.model.cycleForward", () => this.cycleModel("forward"));
1539
+ this.defaultEditor.onAction("app.model.cycleBackward", () => this.cycleModel("backward"));
1512
1540
  // Global debug handler on TUI (works regardless of focus)
1513
1541
  this.ui.onDebug = () => this.handleDebugCommand();
1514
- this.defaultEditor.onAction("selectModel", () => this.showModelSelector());
1515
- this.defaultEditor.onAction("expandTools", () => this.toggleToolOutputExpansion());
1516
- this.defaultEditor.onAction("toggleThinking", () => this.toggleThinkingBlockVisibility());
1517
- this.defaultEditor.onAction("externalEditor", () => this.openExternalEditor());
1518
- this.defaultEditor.onAction("followUp", () => this.handleFollowUp());
1519
- this.defaultEditor.onAction("dequeue", () => this.handleDequeue());
1520
- this.defaultEditor.onAction("newSession", () => this.handleClearCommand());
1521
- this.defaultEditor.onAction("tree", () => this.showTreeSelector());
1522
- this.defaultEditor.onAction("fork", () => this.showUserMessageSelector());
1523
- this.defaultEditor.onAction("resume", () => this.showSessionSelector());
1542
+ this.defaultEditor.onAction("app.model.select", () => this.showModelSelector());
1543
+ this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
1544
+ this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
1545
+ this.defaultEditor.onAction("app.editor.external", () => this.openExternalEditor());
1546
+ this.defaultEditor.onAction("app.message.followUp", () => this.handleFollowUp());
1547
+ this.defaultEditor.onAction("app.message.dequeue", () => this.handleDequeue());
1548
+ this.defaultEditor.onAction("app.session.new", () => this.handleClearCommand());
1549
+ this.defaultEditor.onAction("app.session.tree", () => this.showTreeSelector());
1550
+ this.defaultEditor.onAction("app.session.fork", () => this.showUserMessageSelector());
1551
+ this.defaultEditor.onAction("app.session.resume", () => this.showSessionSelector());
1524
1552
  this.defaultEditor.onChange = (text) => {
1525
1553
  const wasBashMode = this.isBashMode;
1526
1554
  this.isBashMode = text.trimStart().startsWith("!");
@@ -1580,13 +1608,18 @@ export class InteractiveMode {
1580
1608
  this.editor.setText("");
1581
1609
  return;
1582
1610
  }
1611
+ if (text.startsWith("/import")) {
1612
+ await this.handleImportCommand(text);
1613
+ this.editor.setText("");
1614
+ return;
1615
+ }
1583
1616
  if (text === "/share") {
1584
1617
  await this.handleShareCommand();
1585
1618
  this.editor.setText("");
1586
1619
  return;
1587
1620
  }
1588
1621
  if (text === "/copy") {
1589
- this.handleCopyCommand();
1622
+ await this.handleCopyCommand();
1590
1623
  this.editor.setText("");
1591
1624
  return;
1592
1625
  }
@@ -1834,15 +1867,17 @@ export class InteractiveMode {
1834
1867
  this.ui.requestRender();
1835
1868
  break;
1836
1869
  case "tool_execution_start": {
1837
- if (!this.pendingTools.has(event.toolCallId)) {
1838
- const component = new ToolExecutionComponent(event.toolName, event.args, {
1870
+ let component = this.pendingTools.get(event.toolCallId);
1871
+ if (!component) {
1872
+ component = new ToolExecutionComponent(event.toolName, event.args, {
1839
1873
  showImages: this.settingsManager.getShowImages(),
1840
1874
  }, this.getRegisteredToolDefinition(event.toolName), this.ui);
1841
1875
  component.setExpanded(this.toolOutputExpanded);
1842
1876
  this.chatContainer.addChild(component);
1843
1877
  this.pendingTools.set(event.toolCallId, component);
1844
- this.ui.requestRender();
1845
1878
  }
1879
+ component.markExecutionStarted();
1880
+ this.ui.requestRender();
1846
1881
  break;
1847
1882
  }
1848
1883
  case "tool_execution_update": {
@@ -1887,7 +1922,7 @@ export class InteractiveMode {
1887
1922
  // Show compacting indicator with reason
1888
1923
  this.statusContainer.clear();
1889
1924
  const reasonText = event.reason === "overflow" ? "Context overflow detected, " : "";
1890
- this.autoCompactionLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `${reasonText}Auto-compacting... (${appKey(this.keybindings, "interrupt")} to cancel)`);
1925
+ this.autoCompactionLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `${reasonText}Auto-compacting... (${keyText("app.interrupt")} to cancel)`);
1891
1926
  this.statusContainer.addChild(this.autoCompactionLoader);
1892
1927
  this.ui.requestRender();
1893
1928
  break;
@@ -1939,7 +1974,7 @@ export class InteractiveMode {
1939
1974
  // Show retry indicator
1940
1975
  this.statusContainer.clear();
1941
1976
  const delaySeconds = Math.round(event.delayMs / 1000);
1942
- this.retryLoader = new Loader(this.ui, (spinner) => theme.fg("warning", spinner), (text) => theme.fg("muted", text), `Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s... (${appKey(this.keybindings, "interrupt")} to cancel)`);
1977
+ this.retryLoader = new Loader(this.ui, (spinner) => theme.fg("warning", spinner), (text) => theme.fg("muted", text), `Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s... (${keyText("app.interrupt")} to cancel)`);
1943
1978
  this.statusContainer.addChild(this.retryLoader);
1944
1979
  this.ui.requestRender();
1945
1980
  break;
@@ -2397,6 +2432,16 @@ export class InteractiveMode {
2397
2432
  this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2398
2433
  this.ui.requestRender();
2399
2434
  }
2435
+ showPackageUpdateNotification(packages) {
2436
+ const action = theme.fg("accent", `${APP_NAME} update`);
2437
+ const updateInstruction = theme.fg("muted", "Package updates are available. Run ") + action;
2438
+ const packageLines = packages.map((pkg) => `- ${pkg}`).join("\n");
2439
+ this.chatContainer.addChild(new Spacer(1));
2440
+ this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2441
+ this.chatContainer.addChild(new Text(`${theme.bold(theme.fg("warning", "Package Updates Available"))}\n${updateInstruction}\n${theme.fg("muted", "Packages:")}\n${packageLines}`, 1, 0));
2442
+ this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
2443
+ this.ui.requestRender();
2444
+ }
2400
2445
  /**
2401
2446
  * Get all queued messages (read-only).
2402
2447
  * Combines session queue and compaction queue.
@@ -2444,7 +2489,7 @@ export class InteractiveMode {
2444
2489
  const text = theme.fg("dim", `Follow-up: ${message}`);
2445
2490
  this.pendingMessagesContainer.addChild(new TruncatedText(text, 1, 0));
2446
2491
  }
2447
- const dequeueHint = this.getAppKeyDisplay("dequeue");
2492
+ const dequeueHint = this.getAppKeyDisplay("app.message.dequeue");
2448
2493
  const hintText = theme.fg("dim", `↳ ${dequeueHint} to edit all queued messages`);
2449
2494
  this.pendingMessagesContainer.addChild(new TruncatedText(hintText, 1, 0));
2450
2495
  }
@@ -2966,7 +3011,7 @@ export class InteractiveMode {
2966
3011
  this.session.abortBranchSummary();
2967
3012
  };
2968
3013
  this.chatContainer.addChild(new Spacer(1));
2969
- summaryLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `Summarizing branch... (${appKey(this.keybindings, "interrupt")} to cancel)`);
3014
+ summaryLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `Summarizing branch... (${keyText("app.interrupt")} to cancel)`);
2970
3015
  this.statusContainer.addChild(summaryLoader);
2971
3016
  this.ui.requestRender();
2972
3017
  }
@@ -3186,7 +3231,7 @@ export class InteractiveMode {
3186
3231
  return;
3187
3232
  }
3188
3233
  this.resetExtensionUI();
3189
- const loader = new BorderedLoader(this.ui, theme, "Reloading extensions, skills, prompts, themes...", {
3234
+ const loader = new BorderedLoader(this.ui, theme, "Reloading keybindings, extensions, skills, prompts, themes...", {
3190
3235
  cancellable: false,
3191
3236
  });
3192
3237
  const previousEditor = this.editor;
@@ -3203,6 +3248,7 @@ export class InteractiveMode {
3203
3248
  };
3204
3249
  try {
3205
3250
  await this.session.reload();
3251
+ this.keybindings.reload();
3206
3252
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
3207
3253
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
3208
3254
  const themeName = this.settingsManager.getTheme();
@@ -3236,7 +3282,7 @@ export class InteractiveMode {
3236
3282
  if (modelsJsonError) {
3237
3283
  this.showError(`models.json error: ${modelsJsonError}`);
3238
3284
  }
3239
- this.showStatus("Reloaded extensions, skills, prompts, themes");
3285
+ this.showStatus("Reloaded keybindings, extensions, skills, prompts, themes");
3240
3286
  }
3241
3287
  catch (error) {
3242
3288
  dismissLoader(previousEditor);
@@ -3247,13 +3293,58 @@ export class InteractiveMode {
3247
3293
  const parts = text.split(/\s+/);
3248
3294
  const outputPath = parts.length > 1 ? parts[1] : undefined;
3249
3295
  try {
3250
- const filePath = await this.session.exportToHtml(outputPath);
3251
- this.showStatus(`Session exported to: ${filePath}`);
3296
+ if (outputPath?.endsWith(".jsonl")) {
3297
+ const filePath = this.session.exportToJsonl(outputPath);
3298
+ this.showStatus(`Session exported to: ${filePath}`);
3299
+ }
3300
+ else {
3301
+ const filePath = await this.session.exportToHtml(outputPath);
3302
+ this.showStatus(`Session exported to: ${filePath}`);
3303
+ }
3252
3304
  }
3253
3305
  catch (error) {
3254
3306
  this.showError(`Failed to export session: ${error instanceof Error ? error.message : "Unknown error"}`);
3255
3307
  }
3256
3308
  }
3309
+ async handleImportCommand(text) {
3310
+ const parts = text.split(/\s+/);
3311
+ if (parts.length < 2 || !parts[1]) {
3312
+ this.showError("Usage: /import <path.jsonl>");
3313
+ return;
3314
+ }
3315
+ const inputPath = parts[1];
3316
+ const confirmed = await this.showExtensionConfirm("Import session", `Replace current session with ${inputPath}?`);
3317
+ if (!confirmed) {
3318
+ this.showStatus("Import cancelled");
3319
+ return;
3320
+ }
3321
+ try {
3322
+ // Stop loading animation
3323
+ if (this.loadingAnimation) {
3324
+ this.loadingAnimation.stop();
3325
+ this.loadingAnimation = undefined;
3326
+ }
3327
+ this.statusContainer.clear();
3328
+ // Clear UI state
3329
+ this.pendingMessagesContainer.clear();
3330
+ this.compactionQueuedMessages = [];
3331
+ this.streamingComponent = undefined;
3332
+ this.streamingMessage = undefined;
3333
+ this.pendingTools.clear();
3334
+ const success = await this.session.importFromJsonl(inputPath);
3335
+ if (!success) {
3336
+ this.showWarning("Import cancelled");
3337
+ return;
3338
+ }
3339
+ // Clear and re-render the chat
3340
+ this.chatContainer.clear();
3341
+ this.renderInitialMessages();
3342
+ this.showStatus(`Session imported from: ${inputPath}`);
3343
+ }
3344
+ catch (error) {
3345
+ this.showError(`Failed to import session: ${error instanceof Error ? error.message : "Unknown error"}`);
3346
+ }
3347
+ }
3257
3348
  async handleShareCommand() {
3258
3349
  // Check if gh is available and logged in
3259
3350
  try {
@@ -3341,14 +3432,14 @@ export class InteractiveMode {
3341
3432
  }
3342
3433
  }
3343
3434
  }
3344
- handleCopyCommand() {
3435
+ async handleCopyCommand() {
3345
3436
  const text = this.session.getLastAssistantText();
3346
3437
  if (!text) {
3347
3438
  this.showError("No agent messages to copy yet.");
3348
3439
  return;
3349
3440
  }
3350
3441
  try {
3351
- copyToClipboard(text);
3442
+ await copyToClipboard(text);
3352
3443
  this.showStatus("Copied last agent message to clipboard");
3353
3444
  }
3354
3445
  catch (error) {
@@ -3441,53 +3532,59 @@ export class InteractiveMode {
3441
3532
  * Get capitalized display string for an app keybinding action.
3442
3533
  */
3443
3534
  getAppKeyDisplay(action) {
3444
- return this.capitalizeKey(appKey(this.keybindings, action));
3535
+ return this.capitalizeKey(keyText(action));
3445
3536
  }
3446
3537
  /**
3447
3538
  * Get capitalized display string for an editor keybinding action.
3448
3539
  */
3449
3540
  getEditorKeyDisplay(action) {
3450
- return this.capitalizeKey(editorKey(action));
3541
+ return this.capitalizeKey(keyText(action));
3451
3542
  }
3452
3543
  handleHotkeysCommand() {
3453
3544
  // Navigation keybindings
3454
- const cursorWordLeft = this.getEditorKeyDisplay("cursorWordLeft");
3455
- const cursorWordRight = this.getEditorKeyDisplay("cursorWordRight");
3456
- const cursorLineStart = this.getEditorKeyDisplay("cursorLineStart");
3457
- const cursorLineEnd = this.getEditorKeyDisplay("cursorLineEnd");
3458
- const jumpForward = this.getEditorKeyDisplay("jumpForward");
3459
- const jumpBackward = this.getEditorKeyDisplay("jumpBackward");
3460
- const pageUp = this.getEditorKeyDisplay("pageUp");
3461
- const pageDown = this.getEditorKeyDisplay("pageDown");
3545
+ const cursorUp = this.getEditorKeyDisplay("tui.editor.cursorUp");
3546
+ const cursorDown = this.getEditorKeyDisplay("tui.editor.cursorDown");
3547
+ const cursorLeft = this.getEditorKeyDisplay("tui.editor.cursorLeft");
3548
+ const cursorRight = this.getEditorKeyDisplay("tui.editor.cursorRight");
3549
+ const cursorWordLeft = this.getEditorKeyDisplay("tui.editor.cursorWordLeft");
3550
+ const cursorWordRight = this.getEditorKeyDisplay("tui.editor.cursorWordRight");
3551
+ const cursorLineStart = this.getEditorKeyDisplay("tui.editor.cursorLineStart");
3552
+ const cursorLineEnd = this.getEditorKeyDisplay("tui.editor.cursorLineEnd");
3553
+ const jumpForward = this.getEditorKeyDisplay("tui.editor.jumpForward");
3554
+ const jumpBackward = this.getEditorKeyDisplay("tui.editor.jumpBackward");
3555
+ const pageUp = this.getEditorKeyDisplay("tui.editor.pageUp");
3556
+ const pageDown = this.getEditorKeyDisplay("tui.editor.pageDown");
3462
3557
  // Editing keybindings
3463
- const submit = this.getEditorKeyDisplay("submit");
3464
- const newLine = this.getEditorKeyDisplay("newLine");
3465
- const deleteWordBackward = this.getEditorKeyDisplay("deleteWordBackward");
3466
- const deleteWordForward = this.getEditorKeyDisplay("deleteWordForward");
3467
- const deleteToLineStart = this.getEditorKeyDisplay("deleteToLineStart");
3468
- const deleteToLineEnd = this.getEditorKeyDisplay("deleteToLineEnd");
3469
- const yank = this.getEditorKeyDisplay("yank");
3470
- const yankPop = this.getEditorKeyDisplay("yankPop");
3471
- const undo = this.getEditorKeyDisplay("undo");
3472
- const tab = this.getEditorKeyDisplay("tab");
3558
+ const submit = this.getEditorKeyDisplay("tui.input.submit");
3559
+ const newLine = this.getEditorKeyDisplay("tui.input.newLine");
3560
+ const deleteWordBackward = this.getEditorKeyDisplay("tui.editor.deleteWordBackward");
3561
+ const deleteWordForward = this.getEditorKeyDisplay("tui.editor.deleteWordForward");
3562
+ const deleteToLineStart = this.getEditorKeyDisplay("tui.editor.deleteToLineStart");
3563
+ const deleteToLineEnd = this.getEditorKeyDisplay("tui.editor.deleteToLineEnd");
3564
+ const yank = this.getEditorKeyDisplay("tui.editor.yank");
3565
+ const yankPop = this.getEditorKeyDisplay("tui.editor.yankPop");
3566
+ const undo = this.getEditorKeyDisplay("tui.editor.undo");
3567
+ const tab = this.getEditorKeyDisplay("tui.input.tab");
3473
3568
  // App keybindings
3474
- const interrupt = this.getAppKeyDisplay("interrupt");
3475
- const clear = this.getAppKeyDisplay("clear");
3476
- const exit = this.getAppKeyDisplay("exit");
3477
- const suspend = this.getAppKeyDisplay("suspend");
3478
- const cycleThinkingLevel = this.getAppKeyDisplay("cycleThinkingLevel");
3479
- const cycleModelForward = this.getAppKeyDisplay("cycleModelForward");
3480
- const selectModel = this.getAppKeyDisplay("selectModel");
3481
- const expandTools = this.getAppKeyDisplay("expandTools");
3482
- const toggleThinking = this.getAppKeyDisplay("toggleThinking");
3483
- const externalEditor = this.getAppKeyDisplay("externalEditor");
3484
- const followUp = this.getAppKeyDisplay("followUp");
3485
- const dequeue = this.getAppKeyDisplay("dequeue");
3569
+ const interrupt = this.getAppKeyDisplay("app.interrupt");
3570
+ const clear = this.getAppKeyDisplay("app.clear");
3571
+ const exit = this.getAppKeyDisplay("app.exit");
3572
+ const suspend = this.getAppKeyDisplay("app.suspend");
3573
+ const cycleThinkingLevel = this.getAppKeyDisplay("app.thinking.cycle");
3574
+ const cycleModelForward = this.getAppKeyDisplay("app.model.cycleForward");
3575
+ const selectModel = this.getAppKeyDisplay("app.model.select");
3576
+ const expandTools = this.getAppKeyDisplay("app.tools.expand");
3577
+ const toggleThinking = this.getAppKeyDisplay("app.thinking.toggle");
3578
+ const externalEditor = this.getAppKeyDisplay("app.editor.external");
3579
+ const cycleModelBackward = this.getAppKeyDisplay("app.model.cycleBackward");
3580
+ const followUp = this.getAppKeyDisplay("app.message.followUp");
3581
+ const dequeue = this.getAppKeyDisplay("app.message.dequeue");
3582
+ const pasteImage = this.getAppKeyDisplay("app.clipboard.pasteImage");
3486
3583
  let hotkeys = `
3487
3584
  **Navigation**
3488
3585
  | Key | Action |
3489
3586
  |-----|--------|
3490
- | \`Arrow keys\` | Move cursor / browse history (Up when empty) |
3587
+ | \`${cursorUp}\` / \`${cursorDown}\` / \`${cursorLeft}\` / \`${cursorRight}\` | Move cursor / browse history (Up when empty) |
3491
3588
  | \`${cursorWordLeft}\` / \`${cursorWordRight}\` | Move by word |
3492
3589
  | \`${cursorLineStart}\` | Start of line |
3493
3590
  | \`${cursorLineEnd}\` | End of line |
@@ -3517,14 +3614,14 @@ export class InteractiveMode {
3517
3614
  | \`${exit}\` | Exit (when editor is empty) |
3518
3615
  | \`${suspend}\` | Suspend to background |
3519
3616
  | \`${cycleThinkingLevel}\` | Cycle thinking level |
3520
- | \`${cycleModelForward}\` | Cycle models |
3617
+ | \`${cycleModelForward}\` / \`${cycleModelBackward}\` | Cycle models |
3521
3618
  | \`${selectModel}\` | Open model selector |
3522
3619
  | \`${expandTools}\` | Toggle tool output expansion |
3523
3620
  | \`${toggleThinking}\` | Toggle thinking block visibility |
3524
3621
  | \`${externalEditor}\` | Edit message in external editor |
3525
3622
  | \`${followUp}\` | Queue follow-up message |
3526
3623
  | \`${dequeue}\` | Restore queued messages |
3527
- | \`Ctrl+V\` | Paste image from clipboard |
3624
+ | \`${pasteImage}\` | Paste image from clipboard |
3528
3625
  | \`/\` | Slash commands |
3529
3626
  | \`!\` | Run bash command |
3530
3627
  | \`!!\` | Run bash command (excluded from context) |
@@ -3707,7 +3804,7 @@ export class InteractiveMode {
3707
3804
  };
3708
3805
  // Show compacting status
3709
3806
  this.chatContainer.addChild(new Spacer(1));
3710
- const cancelHint = `(${appKey(this.keybindings, "interrupt")} to cancel)`;
3807
+ const cancelHint = `(${keyText("app.interrupt")} to cancel)`;
3711
3808
  const label = isAuto ? `Auto-compacting context... ${cancelHint}` : `Compacting context... ${cancelHint}`;
3712
3809
  const compactingLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), label);
3713
3810
  this.statusContainer.addChild(compactingLoader);