@codex-infinity/pi-infinity 0.60.2 → 0.61.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 (245) hide show
  1. package/CHANGELOG.md +45 -1
  2. package/dist/bun/cli.d.ts.map +1 -1
  3. package/dist/bun/cli.js +1 -0
  4. package/dist/bun/cli.js.map +1 -1
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/cli/config-selector.js.map +1 -1
  7. package/dist/cli/file-processor.js.map +1 -1
  8. package/dist/cli/initial-message.js.map +1 -1
  9. package/dist/cli/list-models.js.map +1 -1
  10. package/dist/cli/session-picker.d.ts.map +1 -1
  11. package/dist/cli/session-picker.js +2 -1
  12. package/dist/cli/session-picker.js.map +1 -1
  13. package/dist/cli.d.ts.map +1 -1
  14. package/dist/cli.js +1 -0
  15. package/dist/cli.js.map +1 -1
  16. package/dist/config.js.map +1 -1
  17. package/dist/core/agent-session.d.ts.map +1 -1
  18. package/dist/core/agent-session.js +50 -69
  19. package/dist/core/agent-session.js.map +1 -1
  20. package/dist/core/auth-storage.js +4 -8
  21. package/dist/core/auth-storage.js.map +1 -1
  22. package/dist/core/bash-executor.js.map +1 -1
  23. package/dist/core/compaction/branch-summarization.js.map +1 -1
  24. package/dist/core/compaction/compaction.js.map +1 -1
  25. package/dist/core/compaction/utils.js.map +1 -1
  26. package/dist/core/event-bus.js.map +1 -1
  27. package/dist/core/exec.js.map +1 -1
  28. package/dist/core/export-html/ansi-to-html.js.map +1 -1
  29. package/dist/core/export-html/index.js.map +1 -1
  30. package/dist/core/export-html/template.css +43 -13
  31. package/dist/core/export-html/template.html +1 -0
  32. package/dist/core/export-html/template.js +107 -0
  33. package/dist/core/export-html/tool-renderer.js.map +1 -1
  34. package/dist/core/extensions/index.d.ts +1 -1
  35. package/dist/core/extensions/index.d.ts.map +1 -1
  36. package/dist/core/extensions/index.js.map +1 -1
  37. package/dist/core/extensions/loader.d.ts.map +1 -1
  38. package/dist/core/extensions/loader.js +4 -4
  39. package/dist/core/extensions/loader.js.map +1 -1
  40. package/dist/core/extensions/runner.d.ts +1 -1
  41. package/dist/core/extensions/runner.d.ts.map +1 -1
  42. package/dist/core/extensions/runner.js +62 -56
  43. package/dist/core/extensions/runner.js.map +1 -1
  44. package/dist/core/extensions/types.d.ts +4 -3
  45. package/dist/core/extensions/types.d.ts.map +1 -1
  46. package/dist/core/extensions/types.js.map +1 -1
  47. package/dist/core/extensions/wrapper.js.map +1 -1
  48. package/dist/core/footer-data-provider.d.ts +9 -2
  49. package/dist/core/footer-data-provider.d.ts.map +1 -1
  50. package/dist/core/footer-data-provider.js +92 -20
  51. package/dist/core/footer-data-provider.js.map +1 -1
  52. package/dist/core/index.d.ts +1 -1
  53. package/dist/core/index.d.ts.map +1 -1
  54. package/dist/core/index.js.map +1 -1
  55. package/dist/core/keybindings.d.ts +268 -51
  56. package/dist/core/keybindings.d.ts.map +1 -1
  57. package/dist/core/keybindings.js +220 -143
  58. package/dist/core/keybindings.js.map +1 -1
  59. package/dist/core/messages.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 +29 -21
  63. package/dist/core/model-registry.js.map +1 -1
  64. package/dist/core/model-resolver.d.ts.map +1 -1
  65. package/dist/core/model-resolver.js +4 -4
  66. package/dist/core/model-resolver.js.map +1 -1
  67. package/dist/core/package-manager.js +0 -6
  68. package/dist/core/package-manager.js.map +1 -1
  69. package/dist/core/prompt-templates.js.map +1 -1
  70. package/dist/core/resolve-config-value.js.map +1 -1
  71. package/dist/core/resource-loader.js +0 -37
  72. package/dist/core/resource-loader.js.map +1 -1
  73. package/dist/core/sdk.d.ts +2 -2
  74. package/dist/core/sdk.d.ts.map +1 -1
  75. package/dist/core/sdk.js +4 -4
  76. package/dist/core/sdk.js.map +1 -1
  77. package/dist/core/session-manager.d.ts +5 -0
  78. package/dist/core/session-manager.d.ts.map +1 -1
  79. package/dist/core/session-manager.js +8 -12
  80. package/dist/core/session-manager.js.map +1 -1
  81. package/dist/core/settings-manager.js +7 -16
  82. package/dist/core/settings-manager.js.map +1 -1
  83. package/dist/core/skills.js.map +1 -1
  84. package/dist/core/system-prompt.js.map +1 -1
  85. package/dist/core/timings.js.map +1 -1
  86. package/dist/core/tools/bash.js.map +1 -1
  87. package/dist/core/tools/edit-diff.js.map +1 -1
  88. package/dist/core/tools/edit.d.ts.map +1 -1
  89. package/dist/core/tools/edit.js +3 -2
  90. package/dist/core/tools/edit.js.map +1 -1
  91. package/dist/core/tools/file-mutation-queue.d.ts +6 -0
  92. package/dist/core/tools/file-mutation-queue.d.ts.map +1 -0
  93. package/dist/core/tools/file-mutation-queue.js +37 -0
  94. package/dist/core/tools/file-mutation-queue.js.map +1 -0
  95. package/dist/core/tools/find.js.map +1 -1
  96. package/dist/core/tools/grep.js.map +1 -1
  97. package/dist/core/tools/index.d.ts +1 -0
  98. package/dist/core/tools/index.d.ts.map +1 -1
  99. package/dist/core/tools/index.js +1 -0
  100. package/dist/core/tools/index.js.map +1 -1
  101. package/dist/core/tools/ls.js.map +1 -1
  102. package/dist/core/tools/path-utils.js.map +1 -1
  103. package/dist/core/tools/read.js.map +1 -1
  104. package/dist/core/tools/truncate.js.map +1 -1
  105. package/dist/core/tools/write.d.ts.map +1 -1
  106. package/dist/core/tools/write.js +6 -3
  107. package/dist/core/tools/write.js.map +1 -1
  108. package/dist/index.d.ts +3 -3
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +2 -2
  111. package/dist/index.js.map +1 -1
  112. package/dist/main.d.ts.map +1 -1
  113. package/dist/main.js +10 -5
  114. package/dist/main.js.map +1 -1
  115. package/dist/migrations.js.map +1 -1
  116. package/dist/modes/interactive/components/armin.js +6 -10
  117. package/dist/modes/interactive/components/armin.js.map +1 -1
  118. package/dist/modes/interactive/components/assistant-message.js +0 -4
  119. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  120. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  121. package/dist/modes/interactive/components/bash-execution.js +8 -14
  122. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  123. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
  124. package/dist/modes/interactive/components/bordered-loader.js +1 -4
  125. package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
  126. package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
  127. package/dist/modes/interactive/components/branch-summary-message.js +3 -5
  128. package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
  129. package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
  130. package/dist/modes/interactive/components/compaction-summary-message.js +3 -5
  131. package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
  132. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  133. package/dist/modes/interactive/components/config-selector.js +14 -23
  134. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  135. package/dist/modes/interactive/components/countdown-timer.js +0 -5
  136. package/dist/modes/interactive/components/countdown-timer.js.map +1 -1
  137. package/dist/modes/interactive/components/custom-editor.d.ts +3 -3
  138. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  139. package/dist/modes/interactive/components/custom-editor.js +7 -14
  140. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  141. package/dist/modes/interactive/components/custom-message.js +1 -6
  142. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  143. package/dist/modes/interactive/components/daxnuts.js +6 -8
  144. package/dist/modes/interactive/components/daxnuts.js.map +1 -1
  145. package/dist/modes/interactive/components/diff.js.map +1 -1
  146. package/dist/modes/interactive/components/dynamic-border.js +0 -1
  147. package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  148. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  149. package/dist/modes/interactive/components/extension-editor.js +10 -15
  150. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  151. package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  152. package/dist/modes/interactive/components/extension-input.js +7 -13
  153. package/dist/modes/interactive/components/extension-input.js.map +1 -1
  154. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  155. package/dist/modes/interactive/components/extension-selector.js +9 -16
  156. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  157. package/dist/modes/interactive/components/footer.js +1 -3
  158. package/dist/modes/interactive/components/footer.js.map +1 -1
  159. package/dist/modes/interactive/components/index.d.ts +1 -1
  160. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  161. package/dist/modes/interactive/components/index.js +1 -1
  162. package/dist/modes/interactive/components/index.js.map +1 -1
  163. package/dist/modes/interactive/components/keybinding-hints.d.ts +3 -36
  164. package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
  165. package/dist/modes/interactive/components/keybinding-hints.js +5 -44
  166. package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
  167. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  168. package/dist/modes/interactive/components/login-dialog.js +9 -15
  169. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  170. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  171. package/dist/modes/interactive/components/model-selector.js +20 -28
  172. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  173. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  174. package/dist/modes/interactive/components/oauth-selector.js +8 -13
  175. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  176. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  177. package/dist/modes/interactive/components/scoped-models-selector.js +13 -17
  178. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  179. package/dist/modes/interactive/components/session-selector-search.js.map +1 -1
  180. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  181. package/dist/modes/interactive/components/session-selector.js +65 -93
  182. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  183. package/dist/modes/interactive/components/settings-selector.js +0 -2
  184. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  185. package/dist/modes/interactive/components/show-images-selector.js +0 -1
  186. package/dist/modes/interactive/components/show-images-selector.js.map +1 -1
  187. package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  188. package/dist/modes/interactive/components/skill-invocation-message.js +3 -5
  189. package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  190. package/dist/modes/interactive/components/theme-selector.js +0 -2
  191. package/dist/modes/interactive/components/theme-selector.js.map +1 -1
  192. package/dist/modes/interactive/components/thinking-selector.js +0 -1
  193. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  194. package/dist/modes/interactive/components/tool-execution.d.ts +7 -0
  195. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  196. package/dist/modes/interactive/components/tool-execution.js +57 -28
  197. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  198. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  199. package/dist/modes/interactive/components/tree-selector.js +32 -46
  200. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  201. package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
  202. package/dist/modes/interactive/components/user-message-selector.js +9 -12
  203. package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
  204. package/dist/modes/interactive/components/user-message.js.map +1 -1
  205. package/dist/modes/interactive/components/visual-truncate.js.map +1 -1
  206. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  207. package/dist/modes/interactive/interactive-mode.js +155 -165
  208. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  209. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  210. package/dist/modes/interactive/theme/theme.js +49 -42
  211. package/dist/modes/interactive/theme/theme.js.map +1 -1
  212. package/dist/modes/print-mode.js.map +1 -1
  213. package/dist/modes/rpc/jsonl.js.map +1 -1
  214. package/dist/modes/rpc/rpc-client.js +6 -7
  215. package/dist/modes/rpc/rpc-client.js.map +1 -1
  216. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  217. package/dist/modes/rpc/rpc-mode.js +4 -1
  218. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  219. package/dist/utils/changelog.js.map +1 -1
  220. package/dist/utils/child-process.js.map +1 -1
  221. package/dist/utils/clipboard-image.js.map +1 -1
  222. package/dist/utils/clipboard.js.map +1 -1
  223. package/dist/utils/exif-orientation.js.map +1 -1
  224. package/dist/utils/frontmatter.js.map +1 -1
  225. package/dist/utils/git.js.map +1 -1
  226. package/dist/utils/image-convert.js.map +1 -1
  227. package/dist/utils/image-resize.js.map +1 -1
  228. package/dist/utils/mime.js.map +1 -1
  229. package/dist/utils/photon.js.map +1 -1
  230. package/dist/utils/shell.js.map +1 -1
  231. package/dist/utils/sleep.js.map +1 -1
  232. package/dist/utils/tools-manager.js.map +1 -1
  233. package/docs/extensions.md +42 -5
  234. package/docs/keybindings.md +101 -112
  235. package/examples/extensions/antigravity-image-gen.ts +5 -3
  236. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  237. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  238. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  239. package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
  240. package/examples/extensions/subagent/index.ts +7 -5
  241. package/examples/extensions/tool-override.ts +9 -7
  242. package/examples/extensions/truncated-tool.ts +6 -3
  243. package/examples/extensions/with-deps/package-lock.json +2 -2
  244. package/examples/extensions/with-deps/package.json +1 -1
  245. package/package.json +4 -4
@@ -6,7 +6,7 @@ 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
11
  import { APP_NAME, getAgentDir, getAuthPath, getDebugLogPath, getShareViewerUrl, getUpdateInstruction, VERSION, } from "../../config.js";
12
12
  import { parseSkillBlock } from "../../core/agent-session.js";
@@ -35,7 +35,7 @@ import { ExtensionEditorComponent } from "./components/extension-editor.js";
35
35
  import { ExtensionInputComponent } from "./components/extension-input.js";
36
36
  import { ExtensionSelectorComponent } from "./components/extension-selector.js";
37
37
  import { FooterComponent } from "./components/footer.js";
38
- import { appKey, appKeyHint, editorKey, keyHint, rawKeyHint } from "./components/keybinding-hints.js";
38
+ import { keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
39
39
  import { LoginDialogComponent } from "./components/login-dialog.js";
40
40
  import { ModelSelectorComponent } from "./components/model-selector.js";
41
41
  import { OAuthSelectorComponent } from "./components/oauth-selector.js";
@@ -52,79 +52,6 @@ function isExpandable(obj) {
52
52
  return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
53
53
  }
54
54
  export class InteractiveMode {
55
- options;
56
- session;
57
- ui;
58
- chatContainer;
59
- pendingMessagesContainer;
60
- statusContainer;
61
- defaultEditor;
62
- editor;
63
- autocompleteProvider;
64
- fdPath;
65
- editorContainer;
66
- footer;
67
- footerDataProvider;
68
- keybindings;
69
- version;
70
- isInitialized = false;
71
- onInputCallback;
72
- loadingAnimation = undefined;
73
- pendingWorkingMessage = undefined;
74
- defaultWorkingMessage = "Working...";
75
- lastSigintTime = 0;
76
- lastEscapeTime = 0;
77
- changelogMarkdown = undefined;
78
- // Status line tracking (for mutating immediately-sequential status updates)
79
- lastStatusSpacer = undefined;
80
- lastStatusText = undefined;
81
- // Streaming message tracking
82
- streamingComponent = undefined;
83
- streamingMessage = undefined;
84
- // Tool execution tracking: toolCallId -> component
85
- pendingTools = new Map();
86
- // Tool output expansion state
87
- toolOutputExpanded = false;
88
- // Thinking block visibility state
89
- hideThinkingBlock = false;
90
- // Skill commands: command name -> skill file path
91
- skillCommands = new Map();
92
- // Agent subscription unsubscribe function
93
- unsubscribe;
94
- // Track if editor is in bash mode (text starts with !)
95
- isBashMode = false;
96
- // Track current bash execution component
97
- bashComponent = undefined;
98
- // Track pending bash components (shown in pending area, moved to chat on submit)
99
- pendingBashComponents = [];
100
- // Auto-compaction state
101
- autoCompactionLoader = undefined;
102
- autoCompactionEscapeHandler;
103
- // Auto-retry state
104
- retryLoader = undefined;
105
- retryEscapeHandler;
106
- // Messages queued while compaction is running
107
- compactionQueuedMessages = [];
108
- // Shutdown state
109
- shutdownRequested = false;
110
- // Extension UI state
111
- extensionSelector = undefined;
112
- extensionInput = undefined;
113
- extensionEditor = undefined;
114
- extensionTerminalInputUnsubscribers = new Set();
115
- // Extension widgets (components rendered above/below the editor)
116
- extensionWidgetsAbove = new Map();
117
- extensionWidgetsBelow = new Map();
118
- widgetContainerAbove;
119
- widgetContainerBelow;
120
- // Custom footer from extension (undefined = use built-in footer)
121
- customFooter = undefined;
122
- // Header container that holds the built-in or custom header
123
- headerContainer;
124
- // Built-in header (logo + keybinding hints + changelog)
125
- builtInHeader = undefined;
126
- // Custom header from extension (undefined = use built-in header)
127
- customHeader = undefined;
128
55
  // Convenience accessors
129
56
  get agent() {
130
57
  return this.session.agent;
@@ -137,6 +64,60 @@ export class InteractiveMode {
137
64
  }
138
65
  constructor(session, options = {}) {
139
66
  this.options = options;
67
+ this.isInitialized = false;
68
+ this.loadingAnimation = undefined;
69
+ this.pendingWorkingMessage = undefined;
70
+ this.defaultWorkingMessage = "Working...";
71
+ this.lastSigintTime = 0;
72
+ this.lastEscapeTime = 0;
73
+ this.changelogMarkdown = undefined;
74
+ // Status line tracking (for mutating immediately-sequential status updates)
75
+ this.lastStatusSpacer = undefined;
76
+ this.lastStatusText = undefined;
77
+ // Streaming message tracking
78
+ this.streamingComponent = undefined;
79
+ this.streamingMessage = undefined;
80
+ // Tool execution tracking: toolCallId -> component
81
+ this.pendingTools = new Map();
82
+ // Tool output expansion state
83
+ this.toolOutputExpanded = false;
84
+ // Thinking block visibility state
85
+ this.hideThinkingBlock = false;
86
+ // Skill commands: command name -> skill file path
87
+ this.skillCommands = new Map();
88
+ // Track if editor is in bash mode (text starts with !)
89
+ this.isBashMode = false;
90
+ // Track current bash execution component
91
+ this.bashComponent = undefined;
92
+ // Track pending bash components (shown in pending area, moved to chat on submit)
93
+ this.pendingBashComponents = [];
94
+ // Auto-compaction state
95
+ this.autoCompactionLoader = undefined;
96
+ // Auto-retry state
97
+ this.retryLoader = undefined;
98
+ // Messages queued while compaction is running
99
+ this.compactionQueuedMessages = [];
100
+ // Shutdown state
101
+ this.shutdownRequested = false;
102
+ // Extension UI state
103
+ this.extensionSelector = undefined;
104
+ this.extensionInput = undefined;
105
+ this.extensionEditor = undefined;
106
+ this.extensionTerminalInputUnsubscribers = new Set();
107
+ // Extension widgets (components rendered above/below the editor)
108
+ this.extensionWidgetsAbove = new Map();
109
+ this.extensionWidgetsBelow = new Map();
110
+ // Custom footer from extension (undefined = use built-in footer)
111
+ this.customFooter = undefined;
112
+ // Built-in header (logo + keybinding hints + changelog)
113
+ this.builtInHeader = undefined;
114
+ // Custom header from extension (undefined = use built-in header)
115
+ this.customHeader = undefined;
116
+ /**
117
+ * Gracefully shutdown the agent.
118
+ * Emits shutdown event to extensions, then exits.
119
+ */
120
+ this.isShuttingDown = false;
140
121
  this.session = session;
141
122
  this.version = VERSION;
142
123
  this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
@@ -148,6 +129,7 @@ export class InteractiveMode {
148
129
  this.widgetContainerAbove = new Container();
149
130
  this.widgetContainerBelow = new Container();
150
131
  this.keybindings = KeybindingsManager.create();
132
+ setKeybindings(this.keybindings);
151
133
  const editorPaddingX = this.settingsManager.getEditorPaddingX();
152
134
  const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
153
135
  this.defaultEditor = new CustomEditor(this.ui, getEditorTheme(), this.keybindings, {
@@ -242,27 +224,26 @@ export class InteractiveMode {
242
224
  if (this.options.verbose || !this.settingsManager.getQuietStartup()) {
243
225
  const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
244
226
  // Build startup instructions using keybinding hint helpers
245
- const kb = this.keybindings;
246
- const hint = (action, desc) => appKeyHint(kb, action, desc);
227
+ const hint = (keybinding, description) => keyHint(keybinding, description);
247
228
  const instructions = [
248
- hint("interrupt", "to interrupt"),
249
- hint("clear", "to clear"),
250
- rawKeyHint(`${appKey(kb, "clear")} twice`, "to exit"),
251
- hint("exit", "to exit (empty)"),
252
- hint("suspend", "to suspend"),
253
- keyHint("deleteToLineEnd", "to delete to end"),
254
- hint("cycleThinkingLevel", "to cycle thinking level"),
255
- rawKeyHint(`${appKey(kb, "cycleModelForward")}/${appKey(kb, "cycleModelBackward")}`, "to cycle models"),
256
- hint("selectModel", "to select model"),
257
- hint("expandTools", "to expand tools"),
258
- hint("toggleThinking", "to expand thinking"),
259
- hint("externalEditor", "for external editor"),
229
+ hint("app.interrupt", "to interrupt"),
230
+ hint("app.clear", "to clear"),
231
+ rawKeyHint(`${keyText("app.clear")} twice`, "to exit"),
232
+ hint("app.exit", "to exit (empty)"),
233
+ hint("app.suspend", "to suspend"),
234
+ keyHint("tui.editor.deleteToLineEnd", "to delete to end"),
235
+ hint("app.thinking.cycle", "to cycle thinking level"),
236
+ rawKeyHint(`${keyText("app.model.cycleForward")}/${keyText("app.model.cycleBackward")}`, "to cycle models"),
237
+ hint("app.model.select", "to select model"),
238
+ hint("app.tools.expand", "to expand tools"),
239
+ hint("app.thinking.toggle", "to expand thinking"),
240
+ hint("app.editor.external", "for external editor"),
260
241
  rawKeyHint("/", "for commands"),
261
242
  rawKeyHint("!", "to run bash"),
262
243
  rawKeyHint("!!", "to run bash (no context)"),
263
- hint("followUp", "to queue follow-up"),
264
- hint("dequeue", "to edit all queued messages"),
265
- hint("pasteImage", "to paste image"),
244
+ hint("app.message.followUp", "to queue follow-up"),
245
+ hint("app.message.dequeue", "to edit all queued messages"),
246
+ hint("app.clipboard.pasteImage", "to paste image"),
266
247
  rawKeyHint("drop files", "to attach"),
267
248
  ].join("\n");
268
249
  this.builtInHeader = new Text(`${logo}\n${instructions}`, 1, 0);
@@ -1049,11 +1030,11 @@ export class InteractiveMode {
1049
1030
  this.defaultEditor.onExtensionShortcut = undefined;
1050
1031
  this.updateTerminalTitle();
1051
1032
  if (this.loadingAnimation) {
1052
- this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${appKey(this.keybindings, "interrupt")} to interrupt)`);
1033
+ this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
1053
1034
  }
1054
1035
  }
1055
1036
  // Maximum total widget lines to prevent viewport overflow
1056
- static MAX_WIDGET_LINES = 10;
1037
+ static { this.MAX_WIDGET_LINES = 10; }
1057
1038
  /**
1058
1039
  * Render all extension widgets to the widget container.
1059
1040
  */
@@ -1172,7 +1153,7 @@ export class InteractiveMode {
1172
1153
  this.loadingAnimation.setMessage(message);
1173
1154
  }
1174
1155
  else {
1175
- this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${appKey(this.keybindings, "interrupt")} to interrupt)`);
1156
+ this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
1176
1157
  }
1177
1158
  }
1178
1159
  else {
@@ -1530,24 +1511,24 @@ export class InteractiveMode {
1530
1511
  }
1531
1512
  };
1532
1513
  // Register app action handlers
1533
- this.defaultEditor.onAction("clear", () => this.handleCtrlC());
1514
+ this.defaultEditor.onAction("app.clear", () => this.handleCtrlC());
1534
1515
  this.defaultEditor.onCtrlD = () => this.handleCtrlD();
1535
- this.defaultEditor.onAction("suspend", () => this.handleCtrlZ());
1536
- this.defaultEditor.onAction("cycleThinkingLevel", () => this.cycleThinkingLevel());
1537
- this.defaultEditor.onAction("cycleModelForward", () => this.cycleModel("forward"));
1538
- this.defaultEditor.onAction("cycleModelBackward", () => this.cycleModel("backward"));
1516
+ this.defaultEditor.onAction("app.suspend", () => this.handleCtrlZ());
1517
+ this.defaultEditor.onAction("app.thinking.cycle", () => this.cycleThinkingLevel());
1518
+ this.defaultEditor.onAction("app.model.cycleForward", () => this.cycleModel("forward"));
1519
+ this.defaultEditor.onAction("app.model.cycleBackward", () => this.cycleModel("backward"));
1539
1520
  // Global debug handler on TUI (works regardless of focus)
1540
1521
  this.ui.onDebug = () => this.handleDebugCommand();
1541
- this.defaultEditor.onAction("selectModel", () => this.showModelSelector());
1542
- this.defaultEditor.onAction("expandTools", () => this.toggleToolOutputExpansion());
1543
- this.defaultEditor.onAction("toggleThinking", () => this.toggleThinkingBlockVisibility());
1544
- this.defaultEditor.onAction("externalEditor", () => this.openExternalEditor());
1545
- this.defaultEditor.onAction("followUp", () => this.handleFollowUp());
1546
- this.defaultEditor.onAction("dequeue", () => this.handleDequeue());
1547
- this.defaultEditor.onAction("newSession", () => this.handleClearCommand());
1548
- this.defaultEditor.onAction("tree", () => this.showTreeSelector());
1549
- this.defaultEditor.onAction("fork", () => this.showUserMessageSelector());
1550
- this.defaultEditor.onAction("resume", () => this.showSessionSelector());
1522
+ this.defaultEditor.onAction("app.model.select", () => this.showModelSelector());
1523
+ this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
1524
+ this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
1525
+ this.defaultEditor.onAction("app.editor.external", () => this.openExternalEditor());
1526
+ this.defaultEditor.onAction("app.message.followUp", () => this.handleFollowUp());
1527
+ this.defaultEditor.onAction("app.message.dequeue", () => this.handleDequeue());
1528
+ this.defaultEditor.onAction("app.session.new", () => this.handleClearCommand());
1529
+ this.defaultEditor.onAction("app.session.tree", () => this.showTreeSelector());
1530
+ this.defaultEditor.onAction("app.session.fork", () => this.showUserMessageSelector());
1531
+ this.defaultEditor.onAction("app.session.resume", () => this.showSessionSelector());
1551
1532
  this.defaultEditor.onChange = (text) => {
1552
1533
  const wasBashMode = this.isBashMode;
1553
1534
  this.isBashMode = text.trimStart().startsWith("!");
@@ -1866,15 +1847,17 @@ export class InteractiveMode {
1866
1847
  this.ui.requestRender();
1867
1848
  break;
1868
1849
  case "tool_execution_start": {
1869
- if (!this.pendingTools.has(event.toolCallId)) {
1870
- const component = new ToolExecutionComponent(event.toolName, event.args, {
1850
+ let component = this.pendingTools.get(event.toolCallId);
1851
+ if (!component) {
1852
+ component = new ToolExecutionComponent(event.toolName, event.args, {
1871
1853
  showImages: this.settingsManager.getShowImages(),
1872
1854
  }, this.getRegisteredToolDefinition(event.toolName), this.ui);
1873
1855
  component.setExpanded(this.toolOutputExpanded);
1874
1856
  this.chatContainer.addChild(component);
1875
1857
  this.pendingTools.set(event.toolCallId, component);
1876
- this.ui.requestRender();
1877
1858
  }
1859
+ component.markExecutionStarted();
1860
+ this.ui.requestRender();
1878
1861
  break;
1879
1862
  }
1880
1863
  case "tool_execution_update": {
@@ -1919,7 +1902,7 @@ export class InteractiveMode {
1919
1902
  // Show compacting indicator with reason
1920
1903
  this.statusContainer.clear();
1921
1904
  const reasonText = event.reason === "overflow" ? "Context overflow detected, " : "";
1922
- 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)`);
1905
+ this.autoCompactionLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `${reasonText}Auto-compacting... (${keyText("app.interrupt")} to cancel)`);
1923
1906
  this.statusContainer.addChild(this.autoCompactionLoader);
1924
1907
  this.ui.requestRender();
1925
1908
  break;
@@ -1971,7 +1954,7 @@ export class InteractiveMode {
1971
1954
  // Show retry indicator
1972
1955
  this.statusContainer.clear();
1973
1956
  const delaySeconds = Math.round(event.delayMs / 1000);
1974
- 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)`);
1957
+ 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)`);
1975
1958
  this.statusContainer.addChild(this.retryLoader);
1976
1959
  this.ui.requestRender();
1977
1960
  break;
@@ -2206,11 +2189,6 @@ export class InteractiveMode {
2206
2189
  // Only called when editor is empty (enforced by CustomEditor)
2207
2190
  void this.shutdown();
2208
2191
  }
2209
- /**
2210
- * Gracefully shutdown the agent.
2211
- * Emits shutdown event to extensions, then exits.
2212
- */
2213
- isShuttingDown = false;
2214
2192
  async shutdown() {
2215
2193
  if (this.isShuttingDown)
2216
2194
  return;
@@ -2240,20 +2218,32 @@ export class InteractiveMode {
2240
2218
  await this.shutdown();
2241
2219
  }
2242
2220
  handleCtrlZ() {
2221
+ // Keep the event loop alive while suspended. Without this, stopping the TUI
2222
+ // can leave Node with no ref'ed handles, causing the process to exit on fg
2223
+ // before the SIGCONT handler gets a chance to restore the terminal.
2224
+ const suspendKeepAlive = setInterval(() => { }, 2 ** 30);
2243
2225
  // Ignore SIGINT while suspended so Ctrl+C in the terminal does not
2244
2226
  // kill the backgrounded process. The handler is removed on resume.
2245
2227
  const ignoreSigint = () => { };
2246
2228
  process.on("SIGINT", ignoreSigint);
2247
2229
  // Set up handler to restore TUI when resumed
2248
2230
  process.once("SIGCONT", () => {
2231
+ clearInterval(suspendKeepAlive);
2249
2232
  process.removeListener("SIGINT", ignoreSigint);
2250
2233
  this.ui.start();
2251
2234
  this.ui.requestRender(true);
2252
2235
  });
2253
- // Stop the TUI (restore terminal to normal mode)
2254
- this.ui.stop();
2255
- // Send SIGTSTP to process group (pid=0 means all processes in group)
2256
- process.kill(0, "SIGTSTP");
2236
+ try {
2237
+ // Stop the TUI (restore terminal to normal mode)
2238
+ this.ui.stop();
2239
+ // Send SIGTSTP to process group (pid=0 means all processes in group)
2240
+ process.kill(0, "SIGTSTP");
2241
+ }
2242
+ catch (error) {
2243
+ clearInterval(suspendKeepAlive);
2244
+ process.removeListener("SIGINT", ignoreSigint);
2245
+ throw error;
2246
+ }
2257
2247
  }
2258
2248
  async handleFollowUp() {
2259
2249
  const text = (this.editor.getExpandedText?.() ?? this.editor.getText()).trim();
@@ -2486,7 +2476,7 @@ export class InteractiveMode {
2486
2476
  const text = theme.fg("dim", `Follow-up: ${message}`);
2487
2477
  this.pendingMessagesContainer.addChild(new TruncatedText(text, 1, 0));
2488
2478
  }
2489
- const dequeueHint = this.getAppKeyDisplay("dequeue");
2479
+ const dequeueHint = this.getAppKeyDisplay("app.message.dequeue");
2490
2480
  const hintText = theme.fg("dim", `↳ ${dequeueHint} to edit all queued messages`);
2491
2481
  this.pendingMessagesContainer.addChild(new TruncatedText(hintText, 1, 0));
2492
2482
  }
@@ -3008,7 +2998,7 @@ export class InteractiveMode {
3008
2998
  this.session.abortBranchSummary();
3009
2999
  };
3010
3000
  this.chatContainer.addChild(new Spacer(1));
3011
- summaryLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `Summarizing branch... (${appKey(this.keybindings, "interrupt")} to cancel)`);
3001
+ summaryLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `Summarizing branch... (${keyText("app.interrupt")} to cancel)`);
3012
3002
  this.statusContainer.addChild(summaryLoader);
3013
3003
  this.ui.requestRender();
3014
3004
  }
@@ -3529,54 +3519,54 @@ export class InteractiveMode {
3529
3519
  * Get capitalized display string for an app keybinding action.
3530
3520
  */
3531
3521
  getAppKeyDisplay(action) {
3532
- return this.capitalizeKey(appKey(this.keybindings, action));
3522
+ return this.capitalizeKey(keyText(action));
3533
3523
  }
3534
3524
  /**
3535
3525
  * Get capitalized display string for an editor keybinding action.
3536
3526
  */
3537
3527
  getEditorKeyDisplay(action) {
3538
- return this.capitalizeKey(editorKey(action));
3528
+ return this.capitalizeKey(keyText(action));
3539
3529
  }
3540
3530
  handleHotkeysCommand() {
3541
3531
  // 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");
3546
- const cursorWordLeft = this.getEditorKeyDisplay("cursorWordLeft");
3547
- const cursorWordRight = this.getEditorKeyDisplay("cursorWordRight");
3548
- const cursorLineStart = this.getEditorKeyDisplay("cursorLineStart");
3549
- const cursorLineEnd = this.getEditorKeyDisplay("cursorLineEnd");
3550
- const jumpForward = this.getEditorKeyDisplay("jumpForward");
3551
- const jumpBackward = this.getEditorKeyDisplay("jumpBackward");
3552
- const pageUp = this.getEditorKeyDisplay("pageUp");
3553
- const pageDown = this.getEditorKeyDisplay("pageDown");
3532
+ const cursorUp = this.getEditorKeyDisplay("tui.editor.cursorUp");
3533
+ const cursorDown = this.getEditorKeyDisplay("tui.editor.cursorDown");
3534
+ const cursorLeft = this.getEditorKeyDisplay("tui.editor.cursorLeft");
3535
+ const cursorRight = this.getEditorKeyDisplay("tui.editor.cursorRight");
3536
+ const cursorWordLeft = this.getEditorKeyDisplay("tui.editor.cursorWordLeft");
3537
+ const cursorWordRight = this.getEditorKeyDisplay("tui.editor.cursorWordRight");
3538
+ const cursorLineStart = this.getEditorKeyDisplay("tui.editor.cursorLineStart");
3539
+ const cursorLineEnd = this.getEditorKeyDisplay("tui.editor.cursorLineEnd");
3540
+ const jumpForward = this.getEditorKeyDisplay("tui.editor.jumpForward");
3541
+ const jumpBackward = this.getEditorKeyDisplay("tui.editor.jumpBackward");
3542
+ const pageUp = this.getEditorKeyDisplay("tui.editor.pageUp");
3543
+ const pageDown = this.getEditorKeyDisplay("tui.editor.pageDown");
3554
3544
  // Editing keybindings
3555
- const submit = this.getEditorKeyDisplay("submit");
3556
- const newLine = this.getEditorKeyDisplay("newLine");
3557
- const deleteWordBackward = this.getEditorKeyDisplay("deleteWordBackward");
3558
- const deleteWordForward = this.getEditorKeyDisplay("deleteWordForward");
3559
- const deleteToLineStart = this.getEditorKeyDisplay("deleteToLineStart");
3560
- const deleteToLineEnd = this.getEditorKeyDisplay("deleteToLineEnd");
3561
- const yank = this.getEditorKeyDisplay("yank");
3562
- const yankPop = this.getEditorKeyDisplay("yankPop");
3563
- const undo = this.getEditorKeyDisplay("undo");
3564
- const tab = this.getEditorKeyDisplay("tab");
3545
+ const submit = this.getEditorKeyDisplay("tui.input.submit");
3546
+ const newLine = this.getEditorKeyDisplay("tui.input.newLine");
3547
+ const deleteWordBackward = this.getEditorKeyDisplay("tui.editor.deleteWordBackward");
3548
+ const deleteWordForward = this.getEditorKeyDisplay("tui.editor.deleteWordForward");
3549
+ const deleteToLineStart = this.getEditorKeyDisplay("tui.editor.deleteToLineStart");
3550
+ const deleteToLineEnd = this.getEditorKeyDisplay("tui.editor.deleteToLineEnd");
3551
+ const yank = this.getEditorKeyDisplay("tui.editor.yank");
3552
+ const yankPop = this.getEditorKeyDisplay("tui.editor.yankPop");
3553
+ const undo = this.getEditorKeyDisplay("tui.editor.undo");
3554
+ const tab = this.getEditorKeyDisplay("tui.input.tab");
3565
3555
  // App keybindings
3566
- const interrupt = this.getAppKeyDisplay("interrupt");
3567
- const clear = this.getAppKeyDisplay("clear");
3568
- const exit = this.getAppKeyDisplay("exit");
3569
- const suspend = this.getAppKeyDisplay("suspend");
3570
- const cycleThinkingLevel = this.getAppKeyDisplay("cycleThinkingLevel");
3571
- const cycleModelForward = this.getAppKeyDisplay("cycleModelForward");
3572
- const selectModel = this.getAppKeyDisplay("selectModel");
3573
- const expandTools = this.getAppKeyDisplay("expandTools");
3574
- const toggleThinking = this.getAppKeyDisplay("toggleThinking");
3575
- const externalEditor = this.getAppKeyDisplay("externalEditor");
3576
- const cycleModelBackward = this.getAppKeyDisplay("cycleModelBackward");
3577
- const followUp = this.getAppKeyDisplay("followUp");
3578
- const dequeue = this.getAppKeyDisplay("dequeue");
3579
- const pasteImage = this.getAppKeyDisplay("pasteImage");
3556
+ const interrupt = this.getAppKeyDisplay("app.interrupt");
3557
+ const clear = this.getAppKeyDisplay("app.clear");
3558
+ const exit = this.getAppKeyDisplay("app.exit");
3559
+ const suspend = this.getAppKeyDisplay("app.suspend");
3560
+ const cycleThinkingLevel = this.getAppKeyDisplay("app.thinking.cycle");
3561
+ const cycleModelForward = this.getAppKeyDisplay("app.model.cycleForward");
3562
+ const selectModel = this.getAppKeyDisplay("app.model.select");
3563
+ const expandTools = this.getAppKeyDisplay("app.tools.expand");
3564
+ const toggleThinking = this.getAppKeyDisplay("app.thinking.toggle");
3565
+ const externalEditor = this.getAppKeyDisplay("app.editor.external");
3566
+ const cycleModelBackward = this.getAppKeyDisplay("app.model.cycleBackward");
3567
+ const followUp = this.getAppKeyDisplay("app.message.followUp");
3568
+ const dequeue = this.getAppKeyDisplay("app.message.dequeue");
3569
+ const pasteImage = this.getAppKeyDisplay("app.clipboard.pasteImage");
3580
3570
  let hotkeys = `
3581
3571
  **Navigation**
3582
3572
  | Key | Action |
@@ -3801,7 +3791,7 @@ export class InteractiveMode {
3801
3791
  };
3802
3792
  // Show compacting status
3803
3793
  this.chatContainer.addChild(new Spacer(1));
3804
- const cancelHint = `(${appKey(this.keybindings, "interrupt")} to cancel)`;
3794
+ const cancelHint = `(${keyText("app.interrupt")} to cancel)`;
3805
3795
  const label = isAuto ? `Auto-compacting context... ${cancelHint}` : `Compacting context... ${cancelHint}`;
3806
3796
  const compactingLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), label);
3807
3797
  this.statusContainer.addChild(compactingLoader);