apex-code 0.0.1-alpha.7 → 0.0.1-alpha.8

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 (256) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +5 -0
  3. package/dist/cli/args.d.ts +3 -1
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +15 -0
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/concurrent-session.d.ts +3 -0
  8. package/dist/cli/concurrent-session.d.ts.map +1 -0
  9. package/dist/cli/concurrent-session.js +13 -0
  10. package/dist/cli/concurrent-session.js.map +1 -0
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +1 -2
  13. package/dist/cli.js.map +1 -1
  14. package/dist/core/agent-session-services.d.ts.map +1 -1
  15. package/dist/core/agent-session-services.js +20 -0
  16. package/dist/core/agent-session-services.js.map +1 -1
  17. package/dist/core/agent-session.d.ts +15 -2
  18. package/dist/core/agent-session.d.ts.map +1 -1
  19. package/dist/core/agent-session.js +9 -6
  20. package/dist/core/agent-session.js.map +1 -1
  21. package/dist/core/auth-storage.d.ts +4 -1
  22. package/dist/core/auth-storage.d.ts.map +1 -1
  23. package/dist/core/auth-storage.js +6 -2
  24. package/dist/core/auth-storage.js.map +1 -1
  25. package/dist/core/bash-executor.d.ts.map +1 -1
  26. package/dist/core/bash-executor.js +10 -9
  27. package/dist/core/bash-executor.js.map +1 -1
  28. package/dist/core/experimental.d.ts +4 -0
  29. package/dist/core/experimental.d.ts.map +1 -1
  30. package/dist/core/experimental.js +4 -0
  31. package/dist/core/experimental.js.map +1 -1
  32. package/dist/core/extensions/types.d.ts +4 -0
  33. package/dist/core/extensions/types.d.ts.map +1 -1
  34. package/dist/core/extensions/types.js.map +1 -1
  35. package/dist/core/http-dispatcher.d.ts +1 -2
  36. package/dist/core/http-dispatcher.d.ts.map +1 -1
  37. package/dist/core/http-dispatcher.js +2 -17
  38. package/dist/core/http-dispatcher.js.map +1 -1
  39. package/dist/core/http-idle-timeout.d.ts +12 -0
  40. package/dist/core/http-idle-timeout.d.ts.map +1 -0
  41. package/dist/core/http-idle-timeout.js +27 -0
  42. package/dist/core/http-idle-timeout.js.map +1 -0
  43. package/dist/core/keybindings.d.ts +24 -0
  44. package/dist/core/keybindings.d.ts.map +1 -1
  45. package/dist/core/lsp/client.d.ts +2 -0
  46. package/dist/core/lsp/client.d.ts.map +1 -1
  47. package/dist/core/lsp/client.js +27 -2
  48. package/dist/core/lsp/client.js.map +1 -1
  49. package/dist/core/model-config.d.ts +5 -0
  50. package/dist/core/model-config.d.ts.map +1 -1
  51. package/dist/core/model-config.js +1 -0
  52. package/dist/core/model-config.js.map +1 -1
  53. package/dist/core/sandbox/child-entry.d.ts.map +1 -1
  54. package/dist/core/sandbox/child-entry.js +8 -1
  55. package/dist/core/sandbox/child-entry.js.map +1 -1
  56. package/dist/core/sandbox/cli-launch.d.ts +3 -0
  57. package/dist/core/sandbox/cli-launch.d.ts.map +1 -1
  58. package/dist/core/sandbox/cli-launch.js +25 -4
  59. package/dist/core/sandbox/cli-launch.js.map +1 -1
  60. package/dist/core/sandbox/cli-supervisor.d.ts.map +1 -1
  61. package/dist/core/sandbox/cli-supervisor.js +53 -13
  62. package/dist/core/sandbox/cli-supervisor.js.map +1 -1
  63. package/dist/core/sandbox/linux-backend.d.ts.map +1 -1
  64. package/dist/core/sandbox/linux-backend.js +15 -0
  65. package/dist/core/sandbox/linux-backend.js.map +1 -1
  66. package/dist/core/sandbox/macos-backend.d.ts.map +1 -1
  67. package/dist/core/sandbox/macos-backend.js +24 -1
  68. package/dist/core/sandbox/macos-backend.js.map +1 -1
  69. package/dist/core/sandbox/rpc/credential-client.d.ts +20 -0
  70. package/dist/core/sandbox/rpc/credential-client.d.ts.map +1 -0
  71. package/dist/core/sandbox/rpc/credential-client.js +170 -0
  72. package/dist/core/sandbox/rpc/credential-client.js.map +1 -0
  73. package/dist/core/sandbox/rpc/credential-proxy.d.ts +44 -0
  74. package/dist/core/sandbox/rpc/credential-proxy.d.ts.map +1 -0
  75. package/dist/core/sandbox/rpc/credential-proxy.js +381 -0
  76. package/dist/core/sandbox/rpc/credential-proxy.js.map +1 -0
  77. package/dist/core/sandbox/supervisor.d.ts +11 -0
  78. package/dist/core/sandbox/supervisor.d.ts.map +1 -1
  79. package/dist/core/sandbox/supervisor.js.map +1 -1
  80. package/dist/core/sandbox/terminal-size.d.ts +40 -0
  81. package/dist/core/sandbox/terminal-size.d.ts.map +1 -0
  82. package/dist/core/sandbox/terminal-size.js +105 -0
  83. package/dist/core/sandbox/terminal-size.js.map +1 -0
  84. package/dist/core/sdk.d.ts +6 -3
  85. package/dist/core/sdk.d.ts.map +1 -1
  86. package/dist/core/sdk.js +16 -4
  87. package/dist/core/sdk.js.map +1 -1
  88. package/dist/core/session-lease.d.ts +31 -0
  89. package/dist/core/session-lease.d.ts.map +1 -0
  90. package/dist/core/session-lease.js +138 -0
  91. package/dist/core/session-lease.js.map +1 -0
  92. package/dist/core/session-manager.d.ts +5 -0
  93. package/dist/core/session-manager.d.ts.map +1 -1
  94. package/dist/core/session-manager.js +3 -3
  95. package/dist/core/session-manager.js.map +1 -1
  96. package/dist/core/settings-manager.d.ts +26 -0
  97. package/dist/core/settings-manager.d.ts.map +1 -1
  98. package/dist/core/settings-manager.js +42 -3
  99. package/dist/core/settings-manager.js.map +1 -1
  100. package/dist/core/slash-commands.d.ts.map +1 -1
  101. package/dist/core/slash-commands.js +2 -0
  102. package/dist/core/slash-commands.js.map +1 -1
  103. package/dist/core/system-prompt.d.ts +2 -2
  104. package/dist/core/system-prompt.d.ts.map +1 -1
  105. package/dist/core/system-prompt.js +47 -62
  106. package/dist/core/system-prompt.js.map +1 -1
  107. package/dist/core/tools/bash.d.ts.map +1 -1
  108. package/dist/core/tools/bash.js +2 -0
  109. package/dist/core/tools/bash.js.map +1 -1
  110. package/dist/core/tools/delegate.d.ts.map +1 -1
  111. package/dist/core/tools/delegate.js +24 -0
  112. package/dist/core/tools/delegate.js.map +1 -1
  113. package/dist/core/tools/edit.d.ts.map +1 -1
  114. package/dist/core/tools/edit.js +2 -0
  115. package/dist/core/tools/edit.js.map +1 -1
  116. package/dist/core/tools/find.d.ts.map +1 -1
  117. package/dist/core/tools/find.js +1 -1
  118. package/dist/core/tools/find.js.map +1 -1
  119. package/dist/core/tools/grep.d.ts +2 -0
  120. package/dist/core/tools/grep.d.ts.map +1 -1
  121. package/dist/core/tools/grep.js +35 -10
  122. package/dist/core/tools/grep.js.map +1 -1
  123. package/dist/core/tools/ls.d.ts +2 -0
  124. package/dist/core/tools/ls.d.ts.map +1 -1
  125. package/dist/core/tools/ls.js +5 -1
  126. package/dist/core/tools/ls.js.map +1 -1
  127. package/dist/core/tools/read.d.ts.map +1 -1
  128. package/dist/core/tools/read.js +2 -0
  129. package/dist/core/tools/read.js.map +1 -1
  130. package/dist/core/tools/web-search-exa.d.ts +33 -0
  131. package/dist/core/tools/web-search-exa.d.ts.map +1 -0
  132. package/dist/core/tools/web-search-exa.js +128 -0
  133. package/dist/core/tools/web-search-exa.js.map +1 -0
  134. package/dist/core/tools/write.d.ts.map +1 -1
  135. package/dist/core/tools/write.js +2 -0
  136. package/dist/core/tools/write.js.map +1 -1
  137. package/dist/core/web-search-provider.d.ts +64 -0
  138. package/dist/core/web-search-provider.d.ts.map +1 -0
  139. package/dist/core/web-search-provider.js +149 -0
  140. package/dist/core/web-search-provider.js.map +1 -0
  141. package/dist/index.d.ts +1 -1
  142. package/dist/index.d.ts.map +1 -1
  143. package/dist/index.js.map +1 -1
  144. package/dist/main.d.ts +1 -0
  145. package/dist/main.d.ts.map +1 -1
  146. package/dist/main.js +20 -1
  147. package/dist/main.js.map +1 -1
  148. package/dist/modes/interactive/components/custom-editor.d.ts +23 -0
  149. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  150. package/dist/modes/interactive/components/custom-editor.js +105 -2
  151. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  152. package/dist/modes/interactive/components/dynamic-border.d.ts +7 -1
  153. package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  154. package/dist/modes/interactive/components/dynamic-border.js +11 -2
  155. package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  156. package/dist/modes/interactive/components/extension-selector.d.ts +4 -0
  157. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  158. package/dist/modes/interactive/components/extension-selector.js +24 -6
  159. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  160. package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
  161. package/dist/modes/interactive/components/first-time-setup.js +6 -3
  162. package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
  163. package/dist/modes/interactive/components/first-use-hints.d.ts +8 -0
  164. package/dist/modes/interactive/components/first-use-hints.d.ts.map +1 -0
  165. package/dist/modes/interactive/components/first-use-hints.js +22 -0
  166. package/dist/modes/interactive/components/first-use-hints.js.map +1 -0
  167. package/dist/modes/interactive/components/footer.d.ts +12 -0
  168. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  169. package/dist/modes/interactive/components/footer.js +128 -9
  170. package/dist/modes/interactive/components/footer.js.map +1 -1
  171. package/dist/modes/interactive/components/index.d.ts +1 -0
  172. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  173. package/dist/modes/interactive/components/index.js +1 -0
  174. package/dist/modes/interactive/components/index.js.map +1 -1
  175. package/dist/modes/interactive/components/model-selector.d.ts +2 -0
  176. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  177. package/dist/modes/interactive/components/model-selector.js +20 -5
  178. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  179. package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
  180. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  181. package/dist/modes/interactive/components/settings-selector.js +10 -0
  182. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  183. package/dist/modes/interactive/components/splash-header.d.ts +29 -4
  184. package/dist/modes/interactive/components/splash-header.d.ts.map +1 -1
  185. package/dist/modes/interactive/components/splash-header.js +58 -23
  186. package/dist/modes/interactive/components/splash-header.js.map +1 -1
  187. package/dist/modes/interactive/components/tool-execution.d.ts +14 -0
  188. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  189. package/dist/modes/interactive/components/tool-execution.js +113 -35
  190. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  191. package/dist/modes/interactive/components/tool-panel.d.ts +30 -0
  192. package/dist/modes/interactive/components/tool-panel.d.ts.map +1 -0
  193. package/dist/modes/interactive/components/tool-panel.js +130 -0
  194. package/dist/modes/interactive/components/tool-panel.js.map +1 -0
  195. package/dist/modes/interactive/interactive-mode.d.ts +44 -9
  196. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  197. package/dist/modes/interactive/interactive-mode.js +282 -70
  198. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  199. package/dist/modes/interactive/model-catalog-refresh.d.ts +7 -0
  200. package/dist/modes/interactive/model-catalog-refresh.d.ts.map +1 -0
  201. package/dist/modes/interactive/model-catalog-refresh.js +34 -0
  202. package/dist/modes/interactive/model-catalog-refresh.js.map +1 -0
  203. package/dist/modes/interactive/theme/apex.json +62 -63
  204. package/dist/modes/interactive/theme/dark.json +2 -0
  205. package/dist/modes/interactive/theme/light.json +2 -0
  206. package/dist/modes/interactive/theme/theme-controller.d.ts +10 -2
  207. package/dist/modes/interactive/theme/theme-controller.d.ts.map +1 -1
  208. package/dist/modes/interactive/theme/theme-controller.js +25 -11
  209. package/dist/modes/interactive/theme/theme-controller.js.map +1 -1
  210. package/dist/modes/interactive/theme/theme-schema.json +9 -1
  211. package/dist/modes/interactive/theme/theme.d.ts +14 -3
  212. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  213. package/dist/modes/interactive/theme/theme.js +55 -6
  214. package/dist/modes/interactive/theme/theme.js.map +1 -1
  215. package/dist/modes/json-event.d.ts +4 -1
  216. package/dist/modes/json-event.d.ts.map +1 -1
  217. package/dist/modes/json-event.js +5 -2
  218. package/dist/modes/json-event.js.map +1 -1
  219. package/dist/server/create-harness.d.ts.map +1 -1
  220. package/dist/server/create-harness.js +2 -0
  221. package/dist/server/create-harness.js.map +1 -1
  222. package/dist/themes/apex-logo.d.ts +36 -16
  223. package/dist/themes/apex-logo.d.ts.map +1 -1
  224. package/dist/themes/apex-logo.js +32 -28
  225. package/dist/themes/apex-logo.js.map +1 -1
  226. package/dist/utils/tools-manager.d.ts +10 -1
  227. package/dist/utils/tools-manager.d.ts.map +1 -1
  228. package/dist/utils/tools-manager.js +15 -20
  229. package/dist/utils/tools-manager.js.map +1 -1
  230. package/docs/compaction.md +3 -3
  231. package/docs/custom-provider.md +1 -1
  232. package/docs/environment-variables.md +1 -0
  233. package/docs/extensions.md +4 -0
  234. package/docs/json.md +6 -4
  235. package/docs/keybindings.md +8 -2
  236. package/docs/rpc.md +15 -4
  237. package/docs/sdk.md +9 -0
  238. package/docs/settings.md +63 -0
  239. package/docs/terminal-setup.md +23 -2
  240. package/docs/themes.md +24 -3
  241. package/docs/tui.md +2 -2
  242. package/docs/usage.md +3 -2
  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/gondolin/package-lock.json +2 -2
  247. package/examples/extensions/gondolin/package.json +1 -1
  248. package/examples/extensions/sandbox/package-lock.json +2 -2
  249. package/examples/extensions/sandbox/package.json +1 -1
  250. package/examples/extensions/subagent/README.md +2 -0
  251. package/examples/extensions/subagent/agents.ts +40 -9
  252. package/examples/extensions/subagent/index.ts +21 -3
  253. package/examples/extensions/with-deps/package-lock.json +2 -2
  254. package/examples/extensions/with-deps/package.json +1 -1
  255. package/npm-shrinkwrap.json +27 -78
  256. package/package.json +6 -6
@@ -54,6 +54,7 @@ import { EarendilAnnouncementComponent } from "./components/earendil-announcemen
54
54
  import { ExtensionEditorComponent } from "./components/extension-editor.js";
55
55
  import { ExtensionInputComponent } from "./components/extension-input.js";
56
56
  import { ExtensionSelectorComponent } from "./components/extension-selector.js";
57
+ import { FirstUseHints } from "./components/first-use-hints.js";
57
58
  import { FooterComponent, formatTokens } from "./components/footer.js";
58
59
  import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
59
60
  import { LoginDialogComponent } from "./components/login-dialog.js";
@@ -72,6 +73,7 @@ import { TrustSelectorComponent } from "./components/trust-selector.js";
72
73
  import { UserMessageComponent } from "./components/user-message.js";
73
74
  import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
74
75
  import { editInExternalEditor } from "./external-editor.js";
76
+ import { refreshModelCatalogs } from "./model-catalog-refresh.js";
75
77
  import { getModelSearchText } from "./model-search.js";
76
78
  import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, onThemeChange, setRegisteredThemes, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
77
79
  import { InteractiveThemeController } from "./theme/theme-controller.js";
@@ -172,9 +174,21 @@ function formatLoginProviderCompletionDescription(provider) {
172
174
  export function createInteractiveTui(options) {
173
175
  const terminal = options.terminal ?? new ProcessTerminal();
174
176
  if (options.tuiMode === "fullscreen") {
177
+ const styleSearchMatch = (text) => theme.bg("searchMatchBg", theme.fg("searchMatchText", text));
175
178
  return new TuiAltScreen(terminal, options.showHardwareCursor, options.logDirectory, {
179
+ searchMatchStyle: (text) => theme.underline(styleSearchMatch(text)),
180
+ searchCurrentMatchStyle: (text) => theme.bold(theme.inverse(styleSearchMatch(text))),
176
181
  openUrl: openBrowser,
177
182
  onRightClickPaste: options.onRightClickPaste,
183
+ copySelection: async (text) => {
184
+ try {
185
+ await copyToClipboard(text);
186
+ return true;
187
+ }
188
+ catch {
189
+ return false;
190
+ }
191
+ },
178
192
  });
179
193
  }
180
194
  return new TuiMainScreen(terminal, options.showHardwareCursor, options.logDirectory);
@@ -236,6 +250,14 @@ export class InteractiveMode {
236
250
  footer;
237
251
  footerContainer;
238
252
  footerDataProvider;
253
+ /**
254
+ * Whether the startup screen is showing its counted inventory line.
255
+ *
256
+ * Warnings are only allowed to collapse into that count when the count is
257
+ * actually on screen. Under a quiet startup there is no splash header, so
258
+ * collapsing them would hide conflicts entirely rather than summarise them.
259
+ */
260
+ startupInventoryVisible = false;
239
261
  // Stored so the same manager can be injected into custom editors, selectors, and extension UI.
240
262
  keybindings;
241
263
  version;
@@ -258,6 +280,7 @@ export class InteractiveMode {
258
280
  // Status line tracking (for mutating immediately-sequential status updates)
259
281
  lastStatusSpacer = undefined;
260
282
  lastStatusText = undefined;
283
+ managedToolStatusStarted = false;
261
284
  // Streaming message tracking
262
285
  streamingComponent = undefined;
263
286
  streamingMessage = undefined;
@@ -279,6 +302,7 @@ export class InteractiveMode {
279
302
  signalCleanupHandlers = [];
280
303
  // Track if editor is in bash mode (text starts with !)
281
304
  isBashMode = false;
305
+ firstUseHints;
282
306
  // Track current bash execution component
283
307
  bashComponent = undefined;
284
308
  // Track pending bash components (shown in pending area, moved to chat on submit)
@@ -328,6 +352,14 @@ export class InteractiveMode {
328
352
  get settingsManager() {
329
353
  return this.session.settingsManager;
330
354
  }
355
+ offerFirstUseHint(id) {
356
+ this.firstUseHints ??= new FirstUseHints(this.settingsManager.getFirstUseHints());
357
+ const hint = this.firstUseHints.offer(id);
358
+ if (!hint)
359
+ return;
360
+ this.settingsManager.setFirstUseHints(this.firstUseHints.getSeen());
361
+ this.chatContainer.addChild(new Text(theme.fg("dim", hint), this.outputPad, 0));
362
+ }
331
363
  constructor(runtimeHost, options = {}) {
332
364
  this.runtimeHost = runtimeHost;
333
365
  const tuiMode = options.tuiMode ?? this.settingsManager.getTuiMode();
@@ -338,6 +370,7 @@ export class InteractiveMode {
338
370
  });
339
371
  this.runtimeHost.setRebindSession(async () => {
340
372
  await this.rebindCurrentSession({ renderBeforeBind: true });
373
+ await this.themeController.applyFromSettings();
341
374
  });
342
375
  this.version = VERSION;
343
376
  this.renderer = createInteractiveTui({
@@ -366,11 +399,14 @@ export class InteractiveMode {
366
399
  this.defaultEditor = new CustomEditor(this.ui, getEditorTheme(), this.keybindings, {
367
400
  paddingX: editorPaddingX,
368
401
  autocompleteMaxVisible,
369
- promptPrefix: "> ",
402
+ promptPrefix: " ",
370
403
  promptColor: (text) => theme.fg("accent", text),
371
- placeholder: "Ask anything, / for commands, ! for bash",
404
+ placeholder: "Ask anything",
372
405
  placeholderColor: (text) => theme.fg("dim", text),
373
406
  commandColor: (text) => theme.fg("accent", text),
407
+ surfaceColor: (text) => theme.bg("userMessageBg", text),
408
+ autocompleteRule: (width) => theme.fg("borderMuted", (this.settingsManager.getSymbolPreset() === "ascii" ? "-" : "┄").repeat(Math.max(1, width))),
409
+ autocompleteFooter: () => this.buildAutocompleteFooter(),
374
410
  });
375
411
  this.editor = this.defaultEditor;
376
412
  this.editorContainer = new Container();
@@ -385,7 +421,12 @@ export class InteractiveMode {
385
421
  this.outputPad = this.settingsManager.getOutputPad();
386
422
  // Register themes from resource loader and initialize
387
423
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
388
- this.themeController = new InteractiveThemeController(this.ui, this.settingsManager, (message) => this.showError(message), () => this.updateEditorBorderColor());
424
+ this.themeController = new InteractiveThemeController(this.ui, {
425
+ getSettingsManager: () => this.settingsManager,
426
+ showError: (message) => this.showError(message),
427
+ onChanged: () => this.updateEditorBorderColor(),
428
+ initialThemeSetting: options.initialThemeSetting,
429
+ });
389
430
  }
390
431
  getAutocompleteSourceTag(sourceInfo) {
391
432
  if (!sourceInfo) {
@@ -548,14 +589,14 @@ export class InteractiveMode {
548
589
  tui.setLayoutRoot(this.fullscreenLayoutRoot);
549
590
  }
550
591
  }
551
- stopInteractiveTui() {
552
- if (this.renderer.mode === "fullscreen") {
592
+ stopInteractiveTui(fullscreenExitOutput) {
593
+ if (this.renderer.mode === "fullscreen" && fullscreenExitOutput === "transcript") {
553
594
  while (this.renderer.hasOverlayEntries)
554
595
  this.renderer.hideOverlay();
555
596
  this.switchTuiMode("regular", false, false);
556
597
  this.renderer.renderNow();
557
598
  }
558
- this.ui.stop();
599
+ this.ui.stop({ preserveScreen: this.renderer.mode === "fullscreen" });
559
600
  }
560
601
  switchTuiMode(mode, restoreProgress = true, startRenderer = true) {
561
602
  const previousUi = this.renderer;
@@ -612,10 +653,9 @@ export class InteractiveMode {
612
653
  this.registerSignalHandlers();
613
654
  // Load changelog (only show new entries, skip for resumed sessions)
614
655
  this.changelogMarkdown = this.getChangelogForDisplay();
615
- // Ensure fd and rg are available (downloads if missing, adds to PATH via getBinDir)
616
- // Both are needed: fd for autocomplete, rg for grep tool and bash commands
617
- const [fdPath] = await Promise.all([ensureTool("fd"), ensureTool("rg")]);
618
- this.fdPath = fdPath;
656
+ // Resolve the effective mode before mounting or focusing components. Both
657
+ // operations can schedule a frame even before the renderer is started.
658
+ await this.refreshFooterPermissionMode();
619
659
  if (this.session.scopedModels.length > 0 && (this.options.verbose || !this.settingsManager.getQuietStartup())) {
620
660
  const modelList = this.session.scopedModels
621
661
  .map((sm) => {
@@ -659,9 +699,11 @@ export class InteractiveMode {
659
699
  this.widgetContainerBelow,
660
700
  this.footerContainer,
661
701
  ]);
702
+ // Accept text while startup completes, but only enable interrupt, exit, and submission feedback.
703
+ this.defaultEditor.onAction("app.clear", () => this.handleCtrlC());
704
+ this.defaultEditor.onCtrlD = () => this.handleCtrlD();
705
+ this.defaultEditor.onSubmit = (text) => this.handleStartupSubmit(text);
662
706
  this.ui.setFocus(this.editor);
663
- this.setupKeyHandlers();
664
- this.setupEditorSubmitHandler();
665
707
  // Start the UI before initializing extensions so session_start handlers can use interactive dialogs
666
708
  this.ui.start();
667
709
  this.isInitialized = true;
@@ -698,18 +740,42 @@ export class InteractiveMode {
698
740
  this.builtInHeader = new ApexSplashHeader(this.version, () => this.session.state.model?.id, () => this.session.sessionManager.getCwd(), this.options.verbose ? expandedInstructions : undefined, {
699
741
  topPadding: true,
700
742
  getSymbolPreset: () => this.settingsManager.getSymbolPreset(),
743
+ getExtraMetadata: () => {
744
+ // Which branch the session is rooted on differs between launches
745
+ // and is not derivable from the prompt, so it belongs here. The
746
+ // tray carries it too, but drops it first when space is tight.
747
+ const branch = this.footerDataProvider.getGitBranch();
748
+ return branch ? [{ label: "branch", value: branch }] : [];
749
+ },
750
+ getInventory: () => this.buildStartupInventory(),
751
+ inventoryHint: "/resources",
752
+ getShortcuts: () => this.buildStartupShortcuts(),
701
753
  getHint: () => "Apex Code can explain its own features and look up its docs.",
702
754
  });
703
755
  // Setup UI layout
756
+ this.startupInventoryVisible = true;
704
757
  this.headerContainer.addChild(this.builtInHeader);
705
758
  this.headerContainer.addChild(new Spacer(1));
706
759
  }
707
760
  else {
708
761
  // Minimal header when silenced
762
+ this.startupInventoryVisible = false;
709
763
  this.builtInHeader = new Text("", 0, 0);
710
764
  this.headerContainer.addChild(this.builtInHeader);
711
765
  }
712
766
  this.ui.requestRender();
767
+ // Ensure fd and rg are available after mounting the TUI (downloads if missing, adds to PATH via getBinDir)
768
+ // so slow downloads do not make startup appear frozen.
769
+ // Both are needed: fd for autocomplete, rg for grep tool and bash commands.
770
+ const [fdPath] = await Promise.all([
771
+ ensureTool("fd", (status) => this.showManagedToolStatus(status)),
772
+ ensureTool("rg", (status) => this.showManagedToolStatus(status)),
773
+ ]);
774
+ this.fdPath = fdPath;
775
+ // Enable the remaining input handlers only after managed-tool setup completes.
776
+ this.setupKeyHandlers();
777
+ this.setupEditorSubmitHandler();
778
+ this.ui.requestRender();
713
779
  // Initialize extensions first so resources are shown before messages
714
780
  await this.rebindCurrentSession();
715
781
  // Render initial messages AFTER showing loaded resources
@@ -749,8 +815,7 @@ export class InteractiveMode {
749
815
  if (!getApexEnvironment("APEX_CODE_OFFLINE")) {
750
816
  const controller = new AbortController();
751
817
  const timeout = setTimeout(() => controller.abort(), 15_000);
752
- void this.session.modelRuntime
753
- .refresh({ signal: controller.signal })
818
+ void refreshModelCatalogs(this.session.modelRuntime, controller.signal)
754
819
  .then(() => this.updateAvailableProviderCount())
755
820
  .catch(() => { })
756
821
  .finally(() => clearTimeout(timeout));
@@ -1205,7 +1270,7 @@ export class InteractiveMode {
1205
1270
  showLoadedResources(options) {
1206
1271
  // Resource rendering is idempotent; chat clears no longer clear this separate container.
1207
1272
  this.loadedResourcesContainer.clear();
1208
- const showListing = options?.force || this.options.verbose || !this.settingsManager.getQuietStartup();
1273
+ const showListing = options?.force || this.options.verbose;
1209
1274
  const showDiagnostics = showListing || options?.showDiagnosticsWhenQuiet === true;
1210
1275
  if (!showListing && !showDiagnostics) {
1211
1276
  return;
@@ -1322,43 +1387,105 @@ export class InteractiveMode {
1322
1387
  addLoadedSection("Themes", themeCompactList, themeList);
1323
1388
  }
1324
1389
  }
1390
+ // Warnings collapse into the counted line when that line is on screen; the
1391
+ // full trace lives behind /resources. Errors always surface inline,
1392
+ // because an error means a resource did not load at all.
1325
1393
  if (showDiagnostics) {
1326
- const skillDiagnostics = skillsResult.diagnostics;
1327
- if (skillDiagnostics.length > 0) {
1328
- const warningLines = this.formatDiagnostics(skillDiagnostics, sourceInfos);
1329
- this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0));
1330
- this.loadedResourcesContainer.addChild(new Spacer(1));
1331
- }
1332
- const promptDiagnostics = promptsResult.diagnostics;
1333
- if (promptDiagnostics.length > 0) {
1334
- const warningLines = this.formatDiagnostics(promptDiagnostics, sourceInfos);
1335
- this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, 0, 0));
1336
- this.loadedResourcesContainer.addChild(new Spacer(1));
1337
- }
1338
- const extensionDiagnostics = [];
1339
- const extensionErrors = this.session.resourceLoader.getExtensions().errors;
1340
- if (extensionErrors.length > 0) {
1341
- for (const error of extensionErrors) {
1342
- extensionDiagnostics.push({ type: "error", message: error.error, path: error.path });
1343
- }
1344
- }
1345
- const commandDiagnostics = this.session.extensionRunner.getCommandDiagnostics();
1346
- extensionDiagnostics.push(...commandDiagnostics);
1347
- extensionDiagnostics.push(...this.getBuiltInCommandConflictDiagnostics(this.session.extensionRunner));
1348
- const shortcutDiagnostics = this.session.extensionRunner.getShortcutDiagnostics();
1349
- extensionDiagnostics.push(...shortcutDiagnostics);
1350
- if (extensionDiagnostics.length > 0) {
1351
- const warningLines = this.formatDiagnostics(extensionDiagnostics, sourceInfos);
1352
- this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0));
1353
- this.loadedResourcesContainer.addChild(new Spacer(1));
1354
- }
1355
- const themeDiagnostics = themesResult.diagnostics;
1356
- if (themeDiagnostics.length > 0) {
1357
- const warningLines = this.formatDiagnostics(themeDiagnostics, sourceInfos);
1358
- this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0));
1394
+ const collapseWarnings = !showListing && this.startupInventoryVisible;
1395
+ for (const group of this.collectDiagnosticGroups()) {
1396
+ const visible = collapseWarnings
1397
+ ? group.diagnostics.filter((diagnostic) => diagnostic.type === "error")
1398
+ : group.diagnostics;
1399
+ if (visible.length === 0)
1400
+ continue;
1401
+ const severity = visible.some((diagnostic) => diagnostic.type === "error") ? "error" : "warning";
1402
+ const body = this.formatDiagnostics(visible, sourceInfos);
1403
+ this.loadedResourcesContainer.addChild(new Text(`${theme.fg(severity, `[${group.label}]`)}\n${body}`, 0, 0));
1359
1404
  this.loadedResourcesContainer.addChild(new Spacer(1));
1360
1405
  }
1361
1406
  }
1407
+ // Counts on the startup screen are read at render time, so they fill in
1408
+ // once resources finish loading without an explicit refresh here.
1409
+ this.builtInHeader?.invalidate?.();
1410
+ }
1411
+ /**
1412
+ * Every startup diagnostic, grouped by the resource kind that produced it.
1413
+ *
1414
+ * One list feeds both the counted line in the header and the detailed
1415
+ * listing, so the two can never disagree about how many issues exist.
1416
+ */
1417
+ collectDiagnosticGroups() {
1418
+ const extensionDiagnostics = [
1419
+ ...this.session.resourceLoader
1420
+ .getExtensions()
1421
+ .errors.map((error) => ({ type: "error", message: error.error, path: error.path })),
1422
+ ...this.session.extensionRunner.getCommandDiagnostics(),
1423
+ ...this.getBuiltInCommandConflictDiagnostics(this.session.extensionRunner),
1424
+ ...this.session.extensionRunner.getShortcutDiagnostics(),
1425
+ ];
1426
+ return [
1427
+ { label: "Skill conflicts", diagnostics: this.session.resourceLoader.getSkills().diagnostics },
1428
+ { label: "Prompt conflicts", diagnostics: this.session.resourceLoader.getPrompts().diagnostics },
1429
+ { label: "Extension issues", diagnostics: extensionDiagnostics },
1430
+ { label: "Theme conflicts", diagnostics: this.session.resourceLoader.getThemes().diagnostics },
1431
+ ].filter((group) => group.diagnostics.length > 0);
1432
+ }
1433
+ /**
1434
+ * The counted line in the ruled band under the mark.
1435
+ *
1436
+ * The startup screen used to print all 152 skill names across fifteen rows.
1437
+ * A count answers the only question that screen can usefully answer, which
1438
+ * is what got loaded; the names live behind /resources now.
1439
+ */
1440
+ /**
1441
+ * The sigil row under the hint: how to reach commands, bash, and files.
1442
+ *
1443
+ * These are the only three characters that change what the composer does, so
1444
+ * they take the accent and everything around them stays quiet.
1445
+ */
1446
+ /** Live keybindings under the autocomplete dropdown. */
1447
+ buildAutocompleteFooter() {
1448
+ const separator = theme.fg("borderMuted", this.settingsManager.getSymbolPreset() === "ascii" ? " - " : " · ");
1449
+ return [
1450
+ rawKeyHint(`${keyText("tui.select.up")}/${keyText("tui.select.down")}`, "move"),
1451
+ keyHint("tui.select.confirm", "select"),
1452
+ keyHint("tui.input.tab", "complete"),
1453
+ keyHint("tui.select.cancel", "dismiss"),
1454
+ ].join(separator);
1455
+ }
1456
+ buildStartupShortcuts() {
1457
+ const entry = (sigil, label) => `${theme.fg("accent", sigil)} ${theme.fg("muted", label)}`;
1458
+ return [
1459
+ entry("/", "commands"),
1460
+ entry("!", "bash"),
1461
+ entry("@", "files"),
1462
+ `${theme.fg("dim", keyText("app.interrupt"))} ${theme.fg("muted", "interrupt")}`,
1463
+ ].join(" ");
1464
+ }
1465
+ buildStartupInventory() {
1466
+ const counted = (total, singular) => `${total} ${singular}${total === 1 ? "" : "s"}`;
1467
+ const parts = [];
1468
+ const skills = this.session.resourceLoader.getSkills().skills.length;
1469
+ const extensions = this.session.resourceLoader.getExtensions().extensions.filter((e) => !e.hidden).length;
1470
+ const prompts = this.session.resourceLoader.getPrompts().prompts.length;
1471
+ if (skills > 0)
1472
+ parts.push(theme.fg("muted", counted(skills, "skill")));
1473
+ if (extensions > 0)
1474
+ parts.push(theme.fg("muted", counted(extensions, "extension")));
1475
+ if (prompts > 0)
1476
+ parts.push(theme.fg("muted", counted(prompts, "prompt")));
1477
+ const diagnostics = this.collectDiagnosticGroups().flatMap((group) => group.diagnostics);
1478
+ const errors = diagnostics.filter((diagnostic) => diagnostic.type === "error").length;
1479
+ const conflicts = diagnostics.length - errors;
1480
+ if (errors > 0)
1481
+ parts.push(theme.fg("error", counted(errors, "error")));
1482
+ if (conflicts > 0)
1483
+ parts.push(theme.fg("warning", counted(conflicts, "conflict")));
1484
+ if (parts.length === 0)
1485
+ return undefined;
1486
+ const ascii = this.settingsManager.getSymbolPreset() === "ascii";
1487
+ const separator = theme.fg("borderMuted", ascii ? " - " : " · ");
1488
+ return parts.join(separator);
1362
1489
  }
1363
1490
  /**
1364
1491
  * Initialize the extension system with TUI-based UI context.
@@ -1446,7 +1573,7 @@ export class InteractiveMode {
1446
1573
  this.applyFullscreenScrollbarSetting();
1447
1574
  this.footer.setSession(this.session);
1448
1575
  this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
1449
- this.refreshFooterPermissionMode();
1576
+ void this.refreshFooterPermissionMode();
1450
1577
  this.footerDataProvider.setCwd(this.sessionManager.getCwd());
1451
1578
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
1452
1579
  this.outputPad = this.settingsManager.getOutputPad();
@@ -1489,7 +1616,7 @@ export class InteractiveMode {
1489
1616
  const message = error instanceof Error ? error.message : String(error);
1490
1617
  this.showError(`${prefix}: ${message}`);
1491
1618
  stopThemeWatcher();
1492
- this.stop();
1619
+ this.stop("transcript");
1493
1620
  process.exit(1);
1494
1621
  }
1495
1622
  renderCurrentSessionState() {
@@ -1499,9 +1626,14 @@ export class InteractiveMode {
1499
1626
  this.compactionQueuedMessages = [];
1500
1627
  this.streamingComponent = undefined;
1501
1628
  this.streamingMessage = undefined;
1502
- this.pendingTools.clear();
1629
+ this.clearPendingTools();
1503
1630
  this.renderInitialMessages();
1504
1631
  }
1632
+ clearPendingTools() {
1633
+ for (const component of this.pendingTools.values())
1634
+ component.dispose();
1635
+ this.pendingTools.clear();
1636
+ }
1505
1637
  /**
1506
1638
  * Get a registered tool definition by name (for custom rendering).
1507
1639
  */
@@ -1884,7 +2016,7 @@ export class InteractiveMode {
1884
2016
  /**
1885
2017
  * Show a selector for extensions.
1886
2018
  */
1887
- showExtensionSelector(title, options, opts) {
2019
+ showExtensionSelector(title, options, opts, enableSearch = false) {
1888
2020
  return new Promise((resolve) => {
1889
2021
  if (opts?.signal?.aborted) {
1890
2022
  resolve(undefined);
@@ -1903,7 +2035,12 @@ export class InteractiveMode {
1903
2035
  opts?.signal?.removeEventListener("abort", onAbort);
1904
2036
  this.hideExtensionSelector();
1905
2037
  resolve(undefined);
1906
- }, { tui: this.ui, timeout: opts?.timeout, onToggleToolsExpanded: () => this.toggleToolOutputExpansion() });
2038
+ }, {
2039
+ tui: this.ui,
2040
+ timeout: opts?.timeout,
2041
+ onToggleToolsExpanded: () => this.toggleToolOutputExpansion(),
2042
+ enableSearch,
2043
+ });
1907
2044
  this.disposeActiveSelector();
1908
2045
  this.editorContainer.clear();
1909
2046
  this.editorContainer.addChild(this.extensionSelector);
@@ -1922,6 +2059,40 @@ export class InteractiveMode {
1922
2059
  this.ui.setFocus(this.editor);
1923
2060
  this.ui.requestRender();
1924
2061
  }
2062
+ async showConfigurationIndex() {
2063
+ const choice = await this.showExtensionSelector("Configuration", [
2064
+ "Settings",
2065
+ "Model",
2066
+ "Model cycling",
2067
+ "Provider login",
2068
+ "Provider logout",
2069
+ "Project trust",
2070
+ "Resources, extensions, and MCP adapters",
2071
+ ], undefined, true);
2072
+ switch (choice) {
2073
+ case "Settings":
2074
+ await this.showSettingsSelector();
2075
+ break;
2076
+ case "Model":
2077
+ this.showModelSelector();
2078
+ break;
2079
+ case "Model cycling":
2080
+ await this.showModelsSelector();
2081
+ break;
2082
+ case "Provider login":
2083
+ await this.handleLoginCommand();
2084
+ break;
2085
+ case "Provider logout":
2086
+ this.showOAuthSelector("logout");
2087
+ break;
2088
+ case "Project trust":
2089
+ this.showTrustSelector();
2090
+ break;
2091
+ case "Resources, extensions, and MCP adapters":
2092
+ this.showStatus("Run apex-code config to manage resources, extensions, and MCP adapters.");
2093
+ break;
2094
+ }
2095
+ }
1925
2096
  /**
1926
2097
  * Show a confirmation dialog for extensions.
1927
2098
  */
@@ -2233,6 +2404,8 @@ export class InteractiveMode {
2233
2404
  const wasBashMode = this.isBashMode;
2234
2405
  this.isBashMode = text.trimStart().startsWith("!");
2235
2406
  if (wasBashMode !== this.isBashMode) {
2407
+ if (this.isBashMode)
2408
+ this.offerFirstUseHint("bash");
2236
2409
  this.updateEditorBorderColor();
2237
2410
  }
2238
2411
  };
@@ -2281,6 +2454,10 @@ export class InteractiveMode {
2281
2454
  // Silently ignore clipboard errors (may not have permission, etc.)
2282
2455
  }
2283
2456
  }
2457
+ handleStartupSubmit(text) {
2458
+ this.editor.setText(text);
2459
+ this.showStatus("Startup is still in progress");
2460
+ }
2284
2461
  setupEditorSubmitHandler() {
2285
2462
  this.defaultEditor.onSubmit = async (text) => {
2286
2463
  text = text.trim();
@@ -2294,6 +2471,11 @@ export class InteractiveMode {
2294
2471
  this.pendingPermissionModeWrite = undefined;
2295
2472
  }
2296
2473
  // Handle commands
2474
+ if (text === "/config") {
2475
+ this.editor.setText("");
2476
+ await this.showConfigurationIndex();
2477
+ return;
2478
+ }
2297
2479
  if (text === "/settings") {
2298
2480
  this.editor.setText("");
2299
2481
  await this.showSettingsSelector();
@@ -2345,6 +2527,11 @@ export class InteractiveMode {
2345
2527
  this.editor.setText("");
2346
2528
  return;
2347
2529
  }
2530
+ if (text === "/resources") {
2531
+ this.showLoadedResources({ force: true });
2532
+ this.editor.setText("");
2533
+ return;
2534
+ }
2348
2535
  if (text === "/hotkeys") {
2349
2536
  this.handleHotkeysCommand();
2350
2537
  this.editor.setText("");
@@ -2485,7 +2672,7 @@ export class InteractiveMode {
2485
2672
  this.footer.invalidate();
2486
2673
  switch (event.type) {
2487
2674
  case "agent_start":
2488
- this.pendingTools.clear();
2675
+ this.clearPendingTools();
2489
2676
  if (this.settingsManager.getShowTerminalProgress()) {
2490
2677
  this.ui.terminal.setProgress(true);
2491
2678
  }
@@ -2537,6 +2724,7 @@ export class InteractiveMode {
2537
2724
  this.streamingMessage = event.message;
2538
2725
  this.chatContainer.addChild(this.streamingComponent);
2539
2726
  this.streamingComponent.updateContent(this.streamingMessage, true);
2727
+ this.updateEditorBorderColor();
2540
2728
  this.ui.requestRender();
2541
2729
  }
2542
2730
  break;
@@ -2554,6 +2742,7 @@ export class InteractiveMode {
2554
2742
  component.setExpanded(this.toolOutputExpanded);
2555
2743
  this.chatContainer.addChild(component);
2556
2744
  this.pendingTools.set(content.id, component);
2745
+ this.offerFirstUseHint("tool-expand");
2557
2746
  }
2558
2747
  else {
2559
2748
  const component = this.pendingTools.get(content.id);
@@ -2591,7 +2780,7 @@ export class InteractiveMode {
2591
2780
  isError: true,
2592
2781
  });
2593
2782
  }
2594
- this.pendingTools.clear();
2783
+ this.clearPendingTools();
2595
2784
  }
2596
2785
  else {
2597
2786
  // Args are now complete - trigger diff computation for edit tools
@@ -2603,6 +2792,7 @@ export class InteractiveMode {
2603
2792
  this.streamingComponent = undefined;
2604
2793
  this.streamingMessage = undefined;
2605
2794
  this.footer.invalidate();
2795
+ this.updateEditorBorderColor();
2606
2796
  }
2607
2797
  this.ui.requestRender();
2608
2798
  break;
@@ -2651,7 +2841,7 @@ export class InteractiveMode {
2651
2841
  this.streamingComponent = undefined;
2652
2842
  this.streamingMessage = undefined;
2653
2843
  }
2654
- this.pendingTools.clear();
2844
+ this.clearPendingTools();
2655
2845
  this.ui.requestRender();
2656
2846
  break;
2657
2847
  case "agent_settled":
@@ -2763,6 +2953,19 @@ export class InteractiveMode {
2763
2953
  : message.content.filter((c) => c.type === "text");
2764
2954
  return textBlocks.map((c) => c.text).join("");
2765
2955
  }
2956
+ /** Show a managed-tool status update in the chat. */
2957
+ showManagedToolStatus(status) {
2958
+ if (!this.managedToolStatusStarted) {
2959
+ this.chatContainer.addChild(new Spacer(1));
2960
+ this.managedToolStatusStarted = true;
2961
+ }
2962
+ const message = status.type === "warning" ? `Warning: ${status.message}` : status.message;
2963
+ const color = status.type === "warning" ? "warning" : "dim";
2964
+ this.chatContainer.addChild(new Text(theme.fg(color, message), 1, 0));
2965
+ this.lastStatusSpacer = undefined;
2966
+ this.lastStatusText = undefined;
2967
+ this.ui.requestRender();
2968
+ }
2766
2969
  /**
2767
2970
  * Show a status message in the chat.
2768
2971
  *
@@ -2883,7 +3086,7 @@ export class InteractiveMode {
2883
3086
  }
2884
3087
  }
2885
3088
  renderSessionItems(items, options = {}) {
2886
- this.pendingTools.clear();
3089
+ this.clearPendingTools();
2887
3090
  const renderedPendingTools = new Map();
2888
3091
  // Cache-miss notices are not persisted; re-derive them from the full entry
2889
3092
  // list and re-inject them after the assistant messages that paid for them.
@@ -3241,6 +3444,7 @@ export class InteractiveMode {
3241
3444
  this.editor.addToHistory?.(text);
3242
3445
  this.editor.setText("");
3243
3446
  await this.session.prompt(text, { streamingBehavior: "followUp" });
3447
+ this.offerFirstUseHint("queue");
3244
3448
  this.updatePendingMessagesDisplay();
3245
3449
  this.ui.requestRender();
3246
3450
  }
@@ -3260,6 +3464,7 @@ export class InteractiveMode {
3260
3464
  }
3261
3465
  }
3262
3466
  updateEditorBorderColor() {
3467
+ this.defaultEditor.setModeLabel(this.isBashMode ? "bash" : this.session.isStreaming ? "busy" : undefined);
3263
3468
  if (this.isBashMode) {
3264
3469
  this.editor.borderColor = theme.getBashModeBorderColor();
3265
3470
  }
@@ -3320,6 +3525,7 @@ export class InteractiveMode {
3320
3525
  this.showStatus(`Tool output: ${expanded ? "expanded" : "collapsed"}`);
3321
3526
  }
3322
3527
  toggleThinkingBlockVisibility() {
3528
+ this.offerFirstUseHint("thinking");
3323
3529
  this.hideThinkingBlock = !this.hideThinkingBlock;
3324
3530
  this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);
3325
3531
  // Rebuild chat from session messages
@@ -3610,11 +3816,12 @@ export class InteractiveMode {
3610
3816
  * between two sessions in one process. A footer left showing the old value
3611
3817
  * would under-report bypassPermissions, which is the one direction that matters.
3612
3818
  */
3613
- refreshFooterPermissionMode() {
3614
- void this.session.getPermissionMode().then((resolution) => {
3615
- this.footer.setPermissionMode(resolution?.mode ?? "default");
3819
+ async refreshFooterPermissionMode() {
3820
+ const resolution = await this.session.getPermissionMode();
3821
+ this.footer.setPermissionMode(resolution?.mode ?? "default");
3822
+ if (this.isInitialized) {
3616
3823
  this.ui.requestRender();
3617
- });
3824
+ }
3618
3825
  }
3619
3826
  /**
3620
3827
  * Writes the mode to user scope, then reports what is actually in force. The
@@ -3664,7 +3871,7 @@ export class InteractiveMode {
3664
3871
  httpIdleTimeoutMs: this.settingsManager.getHttpIdleTimeoutMs(),
3665
3872
  thinkingLevel: this.session.thinkingLevel,
3666
3873
  availableThinkingLevels: this.session.getAvailableThinkingLevels(),
3667
- currentTheme: this.settingsManager.getThemeSetting() || "dark",
3874
+ currentTheme: this.themeController.getThemeSelection() || "dark",
3668
3875
  terminalTheme: this.themeController.getTerminalTheme(),
3669
3876
  availableThemes: getAvailableThemes(),
3670
3877
  hideThinkingBlock: this.hideThinkingBlock,
@@ -3684,6 +3891,7 @@ export class InteractiveMode {
3684
3891
  clearOnShrink: this.settingsManager.getClearOnShrink(),
3685
3892
  showTerminalProgress: this.settingsManager.getShowTerminalProgress(),
3686
3893
  tuiMode: this.ui.mode,
3894
+ fullscreenExitOutput: this.settingsManager.getFullscreenExitOutput(),
3687
3895
  fullscreenScrollbar: this.settingsManager.getFullscreenScrollbar(),
3688
3896
  warnings: this.settingsManager.getWarnings(),
3689
3897
  }, {
@@ -3739,7 +3947,7 @@ export class InteractiveMode {
3739
3947
  },
3740
3948
  onThemeChange: (themeSetting) => {
3741
3949
  this.settingsManager.setTheme(themeSetting);
3742
- void this.themeController.applyFromSettings();
3950
+ void this.themeController.setThemeSetting(themeSetting);
3743
3951
  },
3744
3952
  onThemePreview: (themeName) => this.themeController.preview(themeName),
3745
3953
  onHideThinkingBlockChange: (hidden) => {
@@ -3841,6 +4049,9 @@ export class InteractiveMode {
3841
4049
  this.statusContainer.clear();
3842
4050
  this.showStatus(`TUI mode: ${mode}`);
3843
4051
  },
4052
+ onFullscreenExitOutputChange: (output) => {
4053
+ this.settingsManager.setFullscreenExitOutput(output);
4054
+ },
3844
4055
  onFullscreenScrollbarChange: (mode) => {
3845
4056
  this.settingsManager.setFullscreenScrollbar(mode);
3846
4057
  this.applyFullscreenScrollbarSetting();
@@ -3893,7 +4104,7 @@ export class InteractiveMode {
3893
4104
  controller.abort();
3894
4105
  }, 15_000);
3895
4106
  try {
3896
- const result = await this.session.modelRuntime.refresh({ signal: controller.signal });
4107
+ const result = await refreshModelCatalogs(this.session.modelRuntime, controller.signal);
3897
4108
  if (result.aborted && timedOut) {
3898
4109
  this.showWarning("Model refresh timed out; searching cached models.");
3899
4110
  }
@@ -3981,7 +4192,7 @@ export class InteractiveMode {
3981
4192
  onSelect: (selection) => {
3982
4193
  trustStore.setMany(selection.updates);
3983
4194
  done();
3984
- this.showStatus(`Saved trust decision: ${selection.trusted ? "trusted" : "untrusted"}. Restart pi for this to take effect.`);
4195
+ this.showStatus(`Saved trust decision: ${selection.trusted ? "trusted" : "untrusted"}. Restart ${APP_NAME} for this to take effect.`);
3985
4196
  },
3986
4197
  onCancel: () => {
3987
4198
  done();
@@ -4081,8 +4292,7 @@ export class InteractiveMode {
4081
4292
  this.ui.requestRender();
4082
4293
  },
4083
4294
  });
4084
- void this.session.modelRuntime
4085
- .refresh({ signal: controller.signal })
4295
+ void refreshModelCatalogs(this.session.modelRuntime, controller.signal)
4086
4296
  .then((result) => {
4087
4297
  if (disposed)
4088
4298
  return;
@@ -4873,7 +5083,9 @@ export class InteractiveMode {
4873
5083
  this.showStatus(`Session exported to: ${filePath}`);
4874
5084
  }
4875
5085
  else {
4876
- const filePath = await this.session.exportToHtml(outputPath);
5086
+ const filePath = await this.session.exportToHtml(outputPath, {
5087
+ themeName: theme.name,
5088
+ });
4877
5089
  this.showStatus(`Session exported to: ${filePath}`);
4878
5090
  }
4879
5091
  }
@@ -5415,7 +5627,7 @@ export class InteractiveMode {
5415
5627
  // Ignore, will be emitted as an event
5416
5628
  }
5417
5629
  }
5418
- stop() {
5630
+ stop(fullscreenExitOutput = this.settingsManager.getFullscreenExitOutput()) {
5419
5631
  this.disposeActiveSelector();
5420
5632
  if (this.settingsManager.getShowTerminalProgress()) {
5421
5633
  this.ui.terminal.setProgress(false);
@@ -5429,7 +5641,7 @@ export class InteractiveMode {
5429
5641
  this.unsubscribe();
5430
5642
  }
5431
5643
  if (this.isInitialized) {
5432
- this.stopInteractiveTui();
5644
+ this.stopInteractiveTui(fullscreenExitOutput);
5433
5645
  this.isInitialized = false;
5434
5646
  }
5435
5647
  this.unregisterSignalHandlers();