apex-code 0.0.1-alpha.6 → 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 (263) hide show
  1. package/CHANGELOG.md +162 -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 +32 -2
  18. package/dist/core/agent-session.d.ts.map +1 -1
  19. package/dist/core/agent-session.js +37 -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/permissions/gate.d.ts +7 -0
  54. package/dist/core/permissions/gate.d.ts.map +1 -1
  55. package/dist/core/permissions/gate.js.map +1 -1
  56. package/dist/core/permissions/startup.d.ts +12 -0
  57. package/dist/core/permissions/startup.d.ts.map +1 -1
  58. package/dist/core/permissions/startup.js +12 -4
  59. package/dist/core/permissions/startup.js.map +1 -1
  60. package/dist/core/sandbox/child-entry.d.ts.map +1 -1
  61. package/dist/core/sandbox/child-entry.js +8 -1
  62. package/dist/core/sandbox/child-entry.js.map +1 -1
  63. package/dist/core/sandbox/cli-launch.d.ts +3 -0
  64. package/dist/core/sandbox/cli-launch.d.ts.map +1 -1
  65. package/dist/core/sandbox/cli-launch.js +25 -4
  66. package/dist/core/sandbox/cli-launch.js.map +1 -1
  67. package/dist/core/sandbox/cli-supervisor.d.ts.map +1 -1
  68. package/dist/core/sandbox/cli-supervisor.js +53 -13
  69. package/dist/core/sandbox/cli-supervisor.js.map +1 -1
  70. package/dist/core/sandbox/linux-backend.d.ts.map +1 -1
  71. package/dist/core/sandbox/linux-backend.js +15 -0
  72. package/dist/core/sandbox/linux-backend.js.map +1 -1
  73. package/dist/core/sandbox/macos-backend.d.ts.map +1 -1
  74. package/dist/core/sandbox/macos-backend.js +24 -1
  75. package/dist/core/sandbox/macos-backend.js.map +1 -1
  76. package/dist/core/sandbox/rpc/credential-client.d.ts +20 -0
  77. package/dist/core/sandbox/rpc/credential-client.d.ts.map +1 -0
  78. package/dist/core/sandbox/rpc/credential-client.js +170 -0
  79. package/dist/core/sandbox/rpc/credential-client.js.map +1 -0
  80. package/dist/core/sandbox/rpc/credential-proxy.d.ts +44 -0
  81. package/dist/core/sandbox/rpc/credential-proxy.d.ts.map +1 -0
  82. package/dist/core/sandbox/rpc/credential-proxy.js +381 -0
  83. package/dist/core/sandbox/rpc/credential-proxy.js.map +1 -0
  84. package/dist/core/sandbox/supervisor.d.ts +11 -0
  85. package/dist/core/sandbox/supervisor.d.ts.map +1 -1
  86. package/dist/core/sandbox/supervisor.js.map +1 -1
  87. package/dist/core/sandbox/terminal-size.d.ts +40 -0
  88. package/dist/core/sandbox/terminal-size.d.ts.map +1 -0
  89. package/dist/core/sandbox/terminal-size.js +105 -0
  90. package/dist/core/sandbox/terminal-size.js.map +1 -0
  91. package/dist/core/sdk.d.ts +6 -3
  92. package/dist/core/sdk.d.ts.map +1 -1
  93. package/dist/core/sdk.js +16 -4
  94. package/dist/core/sdk.js.map +1 -1
  95. package/dist/core/session-lease.d.ts +31 -0
  96. package/dist/core/session-lease.d.ts.map +1 -0
  97. package/dist/core/session-lease.js +138 -0
  98. package/dist/core/session-lease.js.map +1 -0
  99. package/dist/core/session-manager.d.ts +5 -0
  100. package/dist/core/session-manager.d.ts.map +1 -1
  101. package/dist/core/session-manager.js +3 -3
  102. package/dist/core/session-manager.js.map +1 -1
  103. package/dist/core/settings-manager.d.ts +26 -0
  104. package/dist/core/settings-manager.d.ts.map +1 -1
  105. package/dist/core/settings-manager.js +42 -3
  106. package/dist/core/settings-manager.js.map +1 -1
  107. package/dist/core/slash-commands.d.ts.map +1 -1
  108. package/dist/core/slash-commands.js +2 -0
  109. package/dist/core/slash-commands.js.map +1 -1
  110. package/dist/core/system-prompt.d.ts +2 -2
  111. package/dist/core/system-prompt.d.ts.map +1 -1
  112. package/dist/core/system-prompt.js +47 -62
  113. package/dist/core/system-prompt.js.map +1 -1
  114. package/dist/core/tools/bash.d.ts.map +1 -1
  115. package/dist/core/tools/bash.js +2 -0
  116. package/dist/core/tools/bash.js.map +1 -1
  117. package/dist/core/tools/delegate.d.ts.map +1 -1
  118. package/dist/core/tools/delegate.js +24 -0
  119. package/dist/core/tools/delegate.js.map +1 -1
  120. package/dist/core/tools/edit.d.ts.map +1 -1
  121. package/dist/core/tools/edit.js +2 -0
  122. package/dist/core/tools/edit.js.map +1 -1
  123. package/dist/core/tools/find.d.ts.map +1 -1
  124. package/dist/core/tools/find.js +1 -1
  125. package/dist/core/tools/find.js.map +1 -1
  126. package/dist/core/tools/grep.d.ts +2 -0
  127. package/dist/core/tools/grep.d.ts.map +1 -1
  128. package/dist/core/tools/grep.js +35 -10
  129. package/dist/core/tools/grep.js.map +1 -1
  130. package/dist/core/tools/ls.d.ts +2 -0
  131. package/dist/core/tools/ls.d.ts.map +1 -1
  132. package/dist/core/tools/ls.js +5 -1
  133. package/dist/core/tools/ls.js.map +1 -1
  134. package/dist/core/tools/read.d.ts.map +1 -1
  135. package/dist/core/tools/read.js +2 -0
  136. package/dist/core/tools/read.js.map +1 -1
  137. package/dist/core/tools/web-search-exa.d.ts +33 -0
  138. package/dist/core/tools/web-search-exa.d.ts.map +1 -0
  139. package/dist/core/tools/web-search-exa.js +128 -0
  140. package/dist/core/tools/web-search-exa.js.map +1 -0
  141. package/dist/core/tools/write.d.ts.map +1 -1
  142. package/dist/core/tools/write.js +2 -0
  143. package/dist/core/tools/write.js.map +1 -1
  144. package/dist/core/web-search-provider.d.ts +64 -0
  145. package/dist/core/web-search-provider.d.ts.map +1 -0
  146. package/dist/core/web-search-provider.js +149 -0
  147. package/dist/core/web-search-provider.js.map +1 -0
  148. package/dist/index.d.ts +1 -1
  149. package/dist/index.d.ts.map +1 -1
  150. package/dist/index.js.map +1 -1
  151. package/dist/main.d.ts +1 -0
  152. package/dist/main.d.ts.map +1 -1
  153. package/dist/main.js +21 -1
  154. package/dist/main.js.map +1 -1
  155. package/dist/modes/interactive/components/custom-editor.d.ts +23 -0
  156. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  157. package/dist/modes/interactive/components/custom-editor.js +105 -2
  158. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  159. package/dist/modes/interactive/components/dynamic-border.d.ts +7 -1
  160. package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  161. package/dist/modes/interactive/components/dynamic-border.js +11 -2
  162. package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  163. package/dist/modes/interactive/components/extension-selector.d.ts +4 -0
  164. package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  165. package/dist/modes/interactive/components/extension-selector.js +24 -6
  166. package/dist/modes/interactive/components/extension-selector.js.map +1 -1
  167. package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
  168. package/dist/modes/interactive/components/first-time-setup.js +6 -3
  169. package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
  170. package/dist/modes/interactive/components/first-use-hints.d.ts +8 -0
  171. package/dist/modes/interactive/components/first-use-hints.d.ts.map +1 -0
  172. package/dist/modes/interactive/components/first-use-hints.js +22 -0
  173. package/dist/modes/interactive/components/first-use-hints.js.map +1 -0
  174. package/dist/modes/interactive/components/footer.d.ts +15 -0
  175. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  176. package/dist/modes/interactive/components/footer.js +133 -1
  177. package/dist/modes/interactive/components/footer.js.map +1 -1
  178. package/dist/modes/interactive/components/index.d.ts +1 -0
  179. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  180. package/dist/modes/interactive/components/index.js +1 -0
  181. package/dist/modes/interactive/components/index.js.map +1 -1
  182. package/dist/modes/interactive/components/model-selector.d.ts +2 -0
  183. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  184. package/dist/modes/interactive/components/model-selector.js +20 -5
  185. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  186. package/dist/modes/interactive/components/settings-selector.d.ts +16 -1
  187. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  188. package/dist/modes/interactive/components/settings-selector.js +106 -0
  189. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  190. package/dist/modes/interactive/components/splash-header.d.ts +29 -4
  191. package/dist/modes/interactive/components/splash-header.d.ts.map +1 -1
  192. package/dist/modes/interactive/components/splash-header.js +58 -23
  193. package/dist/modes/interactive/components/splash-header.js.map +1 -1
  194. package/dist/modes/interactive/components/tool-execution.d.ts +14 -0
  195. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  196. package/dist/modes/interactive/components/tool-execution.js +113 -35
  197. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  198. package/dist/modes/interactive/components/tool-panel.d.ts +30 -0
  199. package/dist/modes/interactive/components/tool-panel.d.ts.map +1 -0
  200. package/dist/modes/interactive/components/tool-panel.js +130 -0
  201. package/dist/modes/interactive/components/tool-panel.js.map +1 -0
  202. package/dist/modes/interactive/interactive-mode.d.ts +48 -2
  203. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  204. package/dist/modes/interactive/interactive-mode.js +339 -68
  205. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  206. package/dist/modes/interactive/model-catalog-refresh.d.ts +7 -0
  207. package/dist/modes/interactive/model-catalog-refresh.d.ts.map +1 -0
  208. package/dist/modes/interactive/model-catalog-refresh.js +34 -0
  209. package/dist/modes/interactive/model-catalog-refresh.js.map +1 -0
  210. package/dist/modes/interactive/theme/apex.json +62 -63
  211. package/dist/modes/interactive/theme/dark.json +2 -0
  212. package/dist/modes/interactive/theme/light.json +2 -0
  213. package/dist/modes/interactive/theme/theme-controller.d.ts +10 -2
  214. package/dist/modes/interactive/theme/theme-controller.d.ts.map +1 -1
  215. package/dist/modes/interactive/theme/theme-controller.js +25 -11
  216. package/dist/modes/interactive/theme/theme-controller.js.map +1 -1
  217. package/dist/modes/interactive/theme/theme-schema.json +9 -1
  218. package/dist/modes/interactive/theme/theme.d.ts +14 -3
  219. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  220. package/dist/modes/interactive/theme/theme.js +55 -6
  221. package/dist/modes/interactive/theme/theme.js.map +1 -1
  222. package/dist/modes/json-event.d.ts +4 -1
  223. package/dist/modes/json-event.d.ts.map +1 -1
  224. package/dist/modes/json-event.js +5 -2
  225. package/dist/modes/json-event.js.map +1 -1
  226. package/dist/server/create-harness.d.ts.map +1 -1
  227. package/dist/server/create-harness.js +2 -0
  228. package/dist/server/create-harness.js.map +1 -1
  229. package/dist/themes/apex-logo.d.ts +36 -16
  230. package/dist/themes/apex-logo.d.ts.map +1 -1
  231. package/dist/themes/apex-logo.js +32 -28
  232. package/dist/themes/apex-logo.js.map +1 -1
  233. package/dist/utils/tools-manager.d.ts +10 -1
  234. package/dist/utils/tools-manager.d.ts.map +1 -1
  235. package/dist/utils/tools-manager.js +15 -20
  236. package/dist/utils/tools-manager.js.map +1 -1
  237. package/docs/compaction.md +3 -3
  238. package/docs/custom-provider.md +1 -1
  239. package/docs/environment-variables.md +1 -0
  240. package/docs/extensions.md +4 -0
  241. package/docs/json.md +6 -4
  242. package/docs/keybindings.md +8 -2
  243. package/docs/rpc.md +15 -4
  244. package/docs/sdk.md +9 -0
  245. package/docs/settings.md +63 -0
  246. package/docs/terminal-setup.md +23 -2
  247. package/docs/themes.md +24 -3
  248. package/docs/tui.md +2 -2
  249. package/docs/usage.md +3 -2
  250. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  251. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  252. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  253. package/examples/extensions/gondolin/package-lock.json +2 -2
  254. package/examples/extensions/gondolin/package.json +1 -1
  255. package/examples/extensions/sandbox/package-lock.json +2 -2
  256. package/examples/extensions/sandbox/package.json +1 -1
  257. package/examples/extensions/subagent/README.md +2 -0
  258. package/examples/extensions/subagent/agents.ts +40 -9
  259. package/examples/extensions/subagent/index.ts +21 -3
  260. package/examples/extensions/with-deps/package-lock.json +2 -2
  261. package/examples/extensions/with-deps/package.json +1 -1
  262. package/npm-shrinkwrap.json +27 -78
  263. 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";
@@ -62,7 +63,7 @@ import { ModelSelectorComponent } from "./components/model-selector.js";
62
63
  import { formatAuthSelectorProviderType, OAuthSelectorComponent, } from "./components/oauth-selector.js";
63
64
  import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js";
64
65
  import { SessionSelectorComponent } from "./components/session-selector.js";
65
- import { SettingsSelectorComponent } from "./components/settings-selector.js";
66
+ import { PERMISSION_MODE_OVERRIDE_HINTS, SettingsSelectorComponent } from "./components/settings-selector.js";
66
67
  import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.js";
67
68
  import { ApexSplashHeader } from "./components/splash-header.js";
68
69
  import { BranchSummaryStatusIndicator, CompactionStatusIndicator, IdleStatus, RetryStatusIndicator, WorkingStatusIndicator, } from "./components/status-indicator.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);
@@ -212,6 +226,8 @@ export function createInteractiveTuiReference(getTui) {
212
226
  }
213
227
  export class InteractiveMode {
214
228
  runtimeHost;
229
+ /** Settled once the most recent /settings permission-mode write has hit disk. */
230
+ pendingPermissionModeWrite;
215
231
  renderer;
216
232
  ui;
217
233
  mainScreenRenderState;
@@ -234,6 +250,14 @@ export class InteractiveMode {
234
250
  footer;
235
251
  footerContainer;
236
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;
237
261
  // Stored so the same manager can be injected into custom editors, selectors, and extension UI.
238
262
  keybindings;
239
263
  version;
@@ -256,6 +280,7 @@ export class InteractiveMode {
256
280
  // Status line tracking (for mutating immediately-sequential status updates)
257
281
  lastStatusSpacer = undefined;
258
282
  lastStatusText = undefined;
283
+ managedToolStatusStarted = false;
259
284
  // Streaming message tracking
260
285
  streamingComponent = undefined;
261
286
  streamingMessage = undefined;
@@ -277,6 +302,7 @@ export class InteractiveMode {
277
302
  signalCleanupHandlers = [];
278
303
  // Track if editor is in bash mode (text starts with !)
279
304
  isBashMode = false;
305
+ firstUseHints;
280
306
  // Track current bash execution component
281
307
  bashComponent = undefined;
282
308
  // Track pending bash components (shown in pending area, moved to chat on submit)
@@ -326,6 +352,14 @@ export class InteractiveMode {
326
352
  get settingsManager() {
327
353
  return this.session.settingsManager;
328
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
+ }
329
363
  constructor(runtimeHost, options = {}) {
330
364
  this.runtimeHost = runtimeHost;
331
365
  const tuiMode = options.tuiMode ?? this.settingsManager.getTuiMode();
@@ -336,6 +370,7 @@ export class InteractiveMode {
336
370
  });
337
371
  this.runtimeHost.setRebindSession(async () => {
338
372
  await this.rebindCurrentSession({ renderBeforeBind: true });
373
+ await this.themeController.applyFromSettings();
339
374
  });
340
375
  this.version = VERSION;
341
376
  this.renderer = createInteractiveTui({
@@ -364,11 +399,14 @@ export class InteractiveMode {
364
399
  this.defaultEditor = new CustomEditor(this.ui, getEditorTheme(), this.keybindings, {
365
400
  paddingX: editorPaddingX,
366
401
  autocompleteMaxVisible,
367
- promptPrefix: "> ",
402
+ promptPrefix: " ",
368
403
  promptColor: (text) => theme.fg("accent", text),
369
- placeholder: "Ask anything, / for commands, ! for bash",
404
+ placeholder: "Ask anything",
370
405
  placeholderColor: (text) => theme.fg("dim", text),
371
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(),
372
410
  });
373
411
  this.editor = this.defaultEditor;
374
412
  this.editorContainer = new Container();
@@ -383,7 +421,12 @@ export class InteractiveMode {
383
421
  this.outputPad = this.settingsManager.getOutputPad();
384
422
  // Register themes from resource loader and initialize
385
423
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
386
- 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
+ });
387
430
  }
388
431
  getAutocompleteSourceTag(sourceInfo) {
389
432
  if (!sourceInfo) {
@@ -546,14 +589,14 @@ export class InteractiveMode {
546
589
  tui.setLayoutRoot(this.fullscreenLayoutRoot);
547
590
  }
548
591
  }
549
- stopInteractiveTui() {
550
- if (this.renderer.mode === "fullscreen") {
592
+ stopInteractiveTui(fullscreenExitOutput) {
593
+ if (this.renderer.mode === "fullscreen" && fullscreenExitOutput === "transcript") {
551
594
  while (this.renderer.hasOverlayEntries)
552
595
  this.renderer.hideOverlay();
553
596
  this.switchTuiMode("regular", false, false);
554
597
  this.renderer.renderNow();
555
598
  }
556
- this.ui.stop();
599
+ this.ui.stop({ preserveScreen: this.renderer.mode === "fullscreen" });
557
600
  }
558
601
  switchTuiMode(mode, restoreProgress = true, startRenderer = true) {
559
602
  const previousUi = this.renderer;
@@ -610,10 +653,9 @@ export class InteractiveMode {
610
653
  this.registerSignalHandlers();
611
654
  // Load changelog (only show new entries, skip for resumed sessions)
612
655
  this.changelogMarkdown = this.getChangelogForDisplay();
613
- // Ensure fd and rg are available (downloads if missing, adds to PATH via getBinDir)
614
- // Both are needed: fd for autocomplete, rg for grep tool and bash commands
615
- const [fdPath] = await Promise.all([ensureTool("fd"), ensureTool("rg")]);
616
- 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();
617
659
  if (this.session.scopedModels.length > 0 && (this.options.verbose || !this.settingsManager.getQuietStartup())) {
618
660
  const modelList = this.session.scopedModels
619
661
  .map((sm) => {
@@ -657,9 +699,11 @@ export class InteractiveMode {
657
699
  this.widgetContainerBelow,
658
700
  this.footerContainer,
659
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);
660
706
  this.ui.setFocus(this.editor);
661
- this.setupKeyHandlers();
662
- this.setupEditorSubmitHandler();
663
707
  // Start the UI before initializing extensions so session_start handlers can use interactive dialogs
664
708
  this.ui.start();
665
709
  this.isInitialized = true;
@@ -696,18 +740,42 @@ export class InteractiveMode {
696
740
  this.builtInHeader = new ApexSplashHeader(this.version, () => this.session.state.model?.id, () => this.session.sessionManager.getCwd(), this.options.verbose ? expandedInstructions : undefined, {
697
741
  topPadding: true,
698
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(),
699
753
  getHint: () => "Apex Code can explain its own features and look up its docs.",
700
754
  });
701
755
  // Setup UI layout
756
+ this.startupInventoryVisible = true;
702
757
  this.headerContainer.addChild(this.builtInHeader);
703
758
  this.headerContainer.addChild(new Spacer(1));
704
759
  }
705
760
  else {
706
761
  // Minimal header when silenced
762
+ this.startupInventoryVisible = false;
707
763
  this.builtInHeader = new Text("", 0, 0);
708
764
  this.headerContainer.addChild(this.builtInHeader);
709
765
  }
710
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();
711
779
  // Initialize extensions first so resources are shown before messages
712
780
  await this.rebindCurrentSession();
713
781
  // Render initial messages AFTER showing loaded resources
@@ -747,8 +815,7 @@ export class InteractiveMode {
747
815
  if (!getApexEnvironment("APEX_CODE_OFFLINE")) {
748
816
  const controller = new AbortController();
749
817
  const timeout = setTimeout(() => controller.abort(), 15_000);
750
- void this.session.modelRuntime
751
- .refresh({ signal: controller.signal })
818
+ void refreshModelCatalogs(this.session.modelRuntime, controller.signal)
752
819
  .then(() => this.updateAvailableProviderCount())
753
820
  .catch(() => { })
754
821
  .finally(() => clearTimeout(timeout));
@@ -1203,7 +1270,7 @@ export class InteractiveMode {
1203
1270
  showLoadedResources(options) {
1204
1271
  // Resource rendering is idempotent; chat clears no longer clear this separate container.
1205
1272
  this.loadedResourcesContainer.clear();
1206
- const showListing = options?.force || this.options.verbose || !this.settingsManager.getQuietStartup();
1273
+ const showListing = options?.force || this.options.verbose;
1207
1274
  const showDiagnostics = showListing || options?.showDiagnosticsWhenQuiet === true;
1208
1275
  if (!showListing && !showDiagnostics) {
1209
1276
  return;
@@ -1320,43 +1387,105 @@ export class InteractiveMode {
1320
1387
  addLoadedSection("Themes", themeCompactList, themeList);
1321
1388
  }
1322
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.
1323
1393
  if (showDiagnostics) {
1324
- const skillDiagnostics = skillsResult.diagnostics;
1325
- if (skillDiagnostics.length > 0) {
1326
- const warningLines = this.formatDiagnostics(skillDiagnostics, sourceInfos);
1327
- this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0));
1328
- this.loadedResourcesContainer.addChild(new Spacer(1));
1329
- }
1330
- const promptDiagnostics = promptsResult.diagnostics;
1331
- if (promptDiagnostics.length > 0) {
1332
- const warningLines = this.formatDiagnostics(promptDiagnostics, sourceInfos);
1333
- this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, 0, 0));
1334
- this.loadedResourcesContainer.addChild(new Spacer(1));
1335
- }
1336
- const extensionDiagnostics = [];
1337
- const extensionErrors = this.session.resourceLoader.getExtensions().errors;
1338
- if (extensionErrors.length > 0) {
1339
- for (const error of extensionErrors) {
1340
- extensionDiagnostics.push({ type: "error", message: error.error, path: error.path });
1341
- }
1342
- }
1343
- const commandDiagnostics = this.session.extensionRunner.getCommandDiagnostics();
1344
- extensionDiagnostics.push(...commandDiagnostics);
1345
- extensionDiagnostics.push(...this.getBuiltInCommandConflictDiagnostics(this.session.extensionRunner));
1346
- const shortcutDiagnostics = this.session.extensionRunner.getShortcutDiagnostics();
1347
- extensionDiagnostics.push(...shortcutDiagnostics);
1348
- if (extensionDiagnostics.length > 0) {
1349
- const warningLines = this.formatDiagnostics(extensionDiagnostics, sourceInfos);
1350
- this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0));
1351
- this.loadedResourcesContainer.addChild(new Spacer(1));
1352
- }
1353
- const themeDiagnostics = themesResult.diagnostics;
1354
- if (themeDiagnostics.length > 0) {
1355
- const warningLines = this.formatDiagnostics(themeDiagnostics, sourceInfos);
1356
- 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));
1357
1404
  this.loadedResourcesContainer.addChild(new Spacer(1));
1358
1405
  }
1359
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);
1360
1489
  }
1361
1490
  /**
1362
1491
  * Initialize the extension system with TUI-based UI context.
@@ -1444,6 +1573,7 @@ export class InteractiveMode {
1444
1573
  this.applyFullscreenScrollbarSetting();
1445
1574
  this.footer.setSession(this.session);
1446
1575
  this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
1576
+ void this.refreshFooterPermissionMode();
1447
1577
  this.footerDataProvider.setCwd(this.sessionManager.getCwd());
1448
1578
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
1449
1579
  this.outputPad = this.settingsManager.getOutputPad();
@@ -1486,7 +1616,7 @@ export class InteractiveMode {
1486
1616
  const message = error instanceof Error ? error.message : String(error);
1487
1617
  this.showError(`${prefix}: ${message}`);
1488
1618
  stopThemeWatcher();
1489
- this.stop();
1619
+ this.stop("transcript");
1490
1620
  process.exit(1);
1491
1621
  }
1492
1622
  renderCurrentSessionState() {
@@ -1496,9 +1626,14 @@ export class InteractiveMode {
1496
1626
  this.compactionQueuedMessages = [];
1497
1627
  this.streamingComponent = undefined;
1498
1628
  this.streamingMessage = undefined;
1499
- this.pendingTools.clear();
1629
+ this.clearPendingTools();
1500
1630
  this.renderInitialMessages();
1501
1631
  }
1632
+ clearPendingTools() {
1633
+ for (const component of this.pendingTools.values())
1634
+ component.dispose();
1635
+ this.pendingTools.clear();
1636
+ }
1502
1637
  /**
1503
1638
  * Get a registered tool definition by name (for custom rendering).
1504
1639
  */
@@ -1881,7 +2016,7 @@ export class InteractiveMode {
1881
2016
  /**
1882
2017
  * Show a selector for extensions.
1883
2018
  */
1884
- showExtensionSelector(title, options, opts) {
2019
+ showExtensionSelector(title, options, opts, enableSearch = false) {
1885
2020
  return new Promise((resolve) => {
1886
2021
  if (opts?.signal?.aborted) {
1887
2022
  resolve(undefined);
@@ -1900,7 +2035,12 @@ export class InteractiveMode {
1900
2035
  opts?.signal?.removeEventListener("abort", onAbort);
1901
2036
  this.hideExtensionSelector();
1902
2037
  resolve(undefined);
1903
- }, { 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
+ });
1904
2044
  this.disposeActiveSelector();
1905
2045
  this.editorContainer.clear();
1906
2046
  this.editorContainer.addChild(this.extensionSelector);
@@ -1919,6 +2059,40 @@ export class InteractiveMode {
1919
2059
  this.ui.setFocus(this.editor);
1920
2060
  this.ui.requestRender();
1921
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
+ }
1922
2096
  /**
1923
2097
  * Show a confirmation dialog for extensions.
1924
2098
  */
@@ -2230,6 +2404,8 @@ export class InteractiveMode {
2230
2404
  const wasBashMode = this.isBashMode;
2231
2405
  this.isBashMode = text.trimStart().startsWith("!");
2232
2406
  if (wasBashMode !== this.isBashMode) {
2407
+ if (this.isBashMode)
2408
+ this.offerFirstUseHint("bash");
2233
2409
  this.updateEditorBorderColor();
2234
2410
  }
2235
2411
  };
@@ -2278,15 +2454,31 @@ export class InteractiveMode {
2278
2454
  // Silently ignore clipboard errors (may not have permission, etc.)
2279
2455
  }
2280
2456
  }
2457
+ handleStartupSubmit(text) {
2458
+ this.editor.setText(text);
2459
+ this.showStatus("Startup is still in progress");
2460
+ }
2281
2461
  setupEditorSubmitHandler() {
2282
2462
  this.defaultEditor.onSubmit = async (text) => {
2283
2463
  text = text.trim();
2284
2464
  if (!text)
2285
2465
  return;
2466
+ // A mode chosen in /settings is written asynchronously. Awaiting it here
2467
+ // keeps the store authoritative before any tool call can read it, so a
2468
+ // switch *out* of bypassPermissions can never be raced by the next turn.
2469
+ if (this.pendingPermissionModeWrite) {
2470
+ await this.pendingPermissionModeWrite;
2471
+ this.pendingPermissionModeWrite = undefined;
2472
+ }
2286
2473
  // Handle commands
2474
+ if (text === "/config") {
2475
+ this.editor.setText("");
2476
+ await this.showConfigurationIndex();
2477
+ return;
2478
+ }
2287
2479
  if (text === "/settings") {
2288
- this.showSettingsSelector();
2289
2480
  this.editor.setText("");
2481
+ await this.showSettingsSelector();
2290
2482
  return;
2291
2483
  }
2292
2484
  if (text === "/scoped-models") {
@@ -2335,6 +2527,11 @@ export class InteractiveMode {
2335
2527
  this.editor.setText("");
2336
2528
  return;
2337
2529
  }
2530
+ if (text === "/resources") {
2531
+ this.showLoadedResources({ force: true });
2532
+ this.editor.setText("");
2533
+ return;
2534
+ }
2338
2535
  if (text === "/hotkeys") {
2339
2536
  this.handleHotkeysCommand();
2340
2537
  this.editor.setText("");
@@ -2475,7 +2672,7 @@ export class InteractiveMode {
2475
2672
  this.footer.invalidate();
2476
2673
  switch (event.type) {
2477
2674
  case "agent_start":
2478
- this.pendingTools.clear();
2675
+ this.clearPendingTools();
2479
2676
  if (this.settingsManager.getShowTerminalProgress()) {
2480
2677
  this.ui.terminal.setProgress(true);
2481
2678
  }
@@ -2527,6 +2724,7 @@ export class InteractiveMode {
2527
2724
  this.streamingMessage = event.message;
2528
2725
  this.chatContainer.addChild(this.streamingComponent);
2529
2726
  this.streamingComponent.updateContent(this.streamingMessage, true);
2727
+ this.updateEditorBorderColor();
2530
2728
  this.ui.requestRender();
2531
2729
  }
2532
2730
  break;
@@ -2544,6 +2742,7 @@ export class InteractiveMode {
2544
2742
  component.setExpanded(this.toolOutputExpanded);
2545
2743
  this.chatContainer.addChild(component);
2546
2744
  this.pendingTools.set(content.id, component);
2745
+ this.offerFirstUseHint("tool-expand");
2547
2746
  }
2548
2747
  else {
2549
2748
  const component = this.pendingTools.get(content.id);
@@ -2581,7 +2780,7 @@ export class InteractiveMode {
2581
2780
  isError: true,
2582
2781
  });
2583
2782
  }
2584
- this.pendingTools.clear();
2783
+ this.clearPendingTools();
2585
2784
  }
2586
2785
  else {
2587
2786
  // Args are now complete - trigger diff computation for edit tools
@@ -2593,6 +2792,7 @@ export class InteractiveMode {
2593
2792
  this.streamingComponent = undefined;
2594
2793
  this.streamingMessage = undefined;
2595
2794
  this.footer.invalidate();
2795
+ this.updateEditorBorderColor();
2596
2796
  }
2597
2797
  this.ui.requestRender();
2598
2798
  break;
@@ -2641,7 +2841,7 @@ export class InteractiveMode {
2641
2841
  this.streamingComponent = undefined;
2642
2842
  this.streamingMessage = undefined;
2643
2843
  }
2644
- this.pendingTools.clear();
2844
+ this.clearPendingTools();
2645
2845
  this.ui.requestRender();
2646
2846
  break;
2647
2847
  case "agent_settled":
@@ -2753,6 +2953,19 @@ export class InteractiveMode {
2753
2953
  : message.content.filter((c) => c.type === "text");
2754
2954
  return textBlocks.map((c) => c.text).join("");
2755
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
+ }
2756
2969
  /**
2757
2970
  * Show a status message in the chat.
2758
2971
  *
@@ -2873,7 +3086,7 @@ export class InteractiveMode {
2873
3086
  }
2874
3087
  }
2875
3088
  renderSessionItems(items, options = {}) {
2876
- this.pendingTools.clear();
3089
+ this.clearPendingTools();
2877
3090
  const renderedPendingTools = new Map();
2878
3091
  // Cache-miss notices are not persisted; re-derive them from the full entry
2879
3092
  // list and re-inject them after the assistant messages that paid for them.
@@ -3231,6 +3444,7 @@ export class InteractiveMode {
3231
3444
  this.editor.addToHistory?.(text);
3232
3445
  this.editor.setText("");
3233
3446
  await this.session.prompt(text, { streamingBehavior: "followUp" });
3447
+ this.offerFirstUseHint("queue");
3234
3448
  this.updatePendingMessagesDisplay();
3235
3449
  this.ui.requestRender();
3236
3450
  }
@@ -3250,6 +3464,7 @@ export class InteractiveMode {
3250
3464
  }
3251
3465
  }
3252
3466
  updateEditorBorderColor() {
3467
+ this.defaultEditor.setModeLabel(this.isBashMode ? "bash" : this.session.isStreaming ? "busy" : undefined);
3253
3468
  if (this.isBashMode) {
3254
3469
  this.editor.borderColor = theme.getBashModeBorderColor();
3255
3470
  }
@@ -3310,6 +3525,7 @@ export class InteractiveMode {
3310
3525
  this.showStatus(`Tool output: ${expanded ? "expanded" : "collapsed"}`);
3311
3526
  }
3312
3527
  toggleThinkingBlockVisibility() {
3528
+ this.offerFirstUseHint("thinking");
3313
3529
  this.hideThinkingBlock = !this.hideThinkingBlock;
3314
3530
  this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);
3315
3531
  // Rebuild chat from session messages
@@ -3593,7 +3809,53 @@ export class InteractiveMode {
3593
3809
  this.ui.setFocus(created.focus);
3594
3810
  this.ui.requestRender();
3595
3811
  }
3596
- showSettingsSelector() {
3812
+ /**
3813
+ * Resyncs the footer from the store. Called on every session swap, not just at
3814
+ * startup: `applyRuntimeSettings` also repoints the cwd, and cwd selects which
3815
+ * project-scope permission file applies, so the mode genuinely can differ
3816
+ * between two sessions in one process. A footer left showing the old value
3817
+ * would under-report bypassPermissions, which is the one direction that matters.
3818
+ */
3819
+ async refreshFooterPermissionMode() {
3820
+ const resolution = await this.session.getPermissionMode();
3821
+ this.footer.setPermissionMode(resolution?.mode ?? "default");
3822
+ if (this.isInitialized) {
3823
+ this.ui.requestRender();
3824
+ }
3825
+ }
3826
+ /**
3827
+ * Writes the mode to user scope, then reports what is actually in force. The
3828
+ * write can be outranked by `--permission-mode` or a project file, and a
3829
+ * settings row that silently does nothing is worse than no row at all.
3830
+ */
3831
+ async applyPermissionMode(mode) {
3832
+ let resolution;
3833
+ try {
3834
+ const write = this.session.setPermissionMode(mode);
3835
+ this.pendingPermissionModeWrite = write.then(() => { }, () => { });
3836
+ resolution = await write;
3837
+ }
3838
+ catch (error) {
3839
+ // Never swallow this. A permission mode the user believes they set, and
3840
+ // did not, is the one outcome worse than the prompt they were escaping.
3841
+ const detail = error instanceof Error ? error.message : String(error);
3842
+ this.chatContainer.addChild(new Text(theme.fg("error", `Permission mode could not be saved: ${detail}`), 1, 0));
3843
+ this.ui.requestRender();
3844
+ return;
3845
+ }
3846
+ if (!resolution)
3847
+ return;
3848
+ const outranked = resolution.mode !== mode;
3849
+ const source = PERMISSION_MODE_OVERRIDE_HINTS[resolution.origin] ?? resolution.origin;
3850
+ const line = outranked
3851
+ ? `Permission mode saved as ${mode}, but ${source} sets ${resolution.mode} for this session.`
3852
+ : `Permission mode set to ${mode}.`;
3853
+ this.chatContainer.addChild(new Text(theme.fg(outranked ? "warning" : "muted", line), 1, 0));
3854
+ this.footer.setPermissionMode(resolution.mode);
3855
+ this.ui.requestRender();
3856
+ }
3857
+ async showSettingsSelector() {
3858
+ const permissionMode = await this.session.getPermissionMode();
3597
3859
  this.showSelector((done) => {
3598
3860
  let selector;
3599
3861
  selector = new SettingsSelectorComponent({
@@ -3609,7 +3871,7 @@ export class InteractiveMode {
3609
3871
  httpIdleTimeoutMs: this.settingsManager.getHttpIdleTimeoutMs(),
3610
3872
  thinkingLevel: this.session.thinkingLevel,
3611
3873
  availableThinkingLevels: this.session.getAvailableThinkingLevels(),
3612
- currentTheme: this.settingsManager.getThemeSetting() || "dark",
3874
+ currentTheme: this.themeController.getThemeSelection() || "dark",
3613
3875
  terminalTheme: this.themeController.getTerminalTheme(),
3614
3876
  availableThemes: getAvailableThemes(),
3615
3877
  hideThinkingBlock: this.hideThinkingBlock,
@@ -3621,6 +3883,7 @@ export class InteractiveMode {
3621
3883
  showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
3622
3884
  showCacheMissNotices: this.settingsManager.getShowCacheMissNotices(),
3623
3885
  defaultProjectTrust: this.settingsManager.getDefaultProjectTrust(),
3886
+ permissionMode,
3624
3887
  editorPaddingX: this.settingsManager.getEditorPaddingX(),
3625
3888
  outputPad: this.settingsManager.getOutputPad(),
3626
3889
  autocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),
@@ -3628,6 +3891,7 @@ export class InteractiveMode {
3628
3891
  clearOnShrink: this.settingsManager.getClearOnShrink(),
3629
3892
  showTerminalProgress: this.settingsManager.getShowTerminalProgress(),
3630
3893
  tuiMode: this.ui.mode,
3894
+ fullscreenExitOutput: this.settingsManager.getFullscreenExitOutput(),
3631
3895
  fullscreenScrollbar: this.settingsManager.getFullscreenScrollbar(),
3632
3896
  warnings: this.settingsManager.getWarnings(),
3633
3897
  }, {
@@ -3683,7 +3947,7 @@ export class InteractiveMode {
3683
3947
  },
3684
3948
  onThemeChange: (themeSetting) => {
3685
3949
  this.settingsManager.setTheme(themeSetting);
3686
- void this.themeController.applyFromSettings();
3950
+ void this.themeController.setThemeSetting(themeSetting);
3687
3951
  },
3688
3952
  onThemePreview: (themeName) => this.themeController.preview(themeName),
3689
3953
  onHideThinkingBlockChange: (hidden) => {
@@ -3718,6 +3982,9 @@ export class InteractiveMode {
3718
3982
  onDefaultProjectTrustChange: (defaultProjectTrust) => {
3719
3983
  this.settingsManager.setDefaultProjectTrust(defaultProjectTrust);
3720
3984
  },
3985
+ onPermissionModeChange: (mode) => {
3986
+ void this.applyPermissionMode(mode);
3987
+ },
3721
3988
  onDoubleEscapeActionChange: (action) => {
3722
3989
  this.settingsManager.setDoubleEscapeAction(action);
3723
3990
  },
@@ -3782,6 +4049,9 @@ export class InteractiveMode {
3782
4049
  this.statusContainer.clear();
3783
4050
  this.showStatus(`TUI mode: ${mode}`);
3784
4051
  },
4052
+ onFullscreenExitOutputChange: (output) => {
4053
+ this.settingsManager.setFullscreenExitOutput(output);
4054
+ },
3785
4055
  onFullscreenScrollbarChange: (mode) => {
3786
4056
  this.settingsManager.setFullscreenScrollbar(mode);
3787
4057
  this.applyFullscreenScrollbarSetting();
@@ -3834,7 +4104,7 @@ export class InteractiveMode {
3834
4104
  controller.abort();
3835
4105
  }, 15_000);
3836
4106
  try {
3837
- const result = await this.session.modelRuntime.refresh({ signal: controller.signal });
4107
+ const result = await refreshModelCatalogs(this.session.modelRuntime, controller.signal);
3838
4108
  if (result.aborted && timedOut) {
3839
4109
  this.showWarning("Model refresh timed out; searching cached models.");
3840
4110
  }
@@ -3922,7 +4192,7 @@ export class InteractiveMode {
3922
4192
  onSelect: (selection) => {
3923
4193
  trustStore.setMany(selection.updates);
3924
4194
  done();
3925
- 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.`);
3926
4196
  },
3927
4197
  onCancel: () => {
3928
4198
  done();
@@ -4022,8 +4292,7 @@ export class InteractiveMode {
4022
4292
  this.ui.requestRender();
4023
4293
  },
4024
4294
  });
4025
- void this.session.modelRuntime
4026
- .refresh({ signal: controller.signal })
4295
+ void refreshModelCatalogs(this.session.modelRuntime, controller.signal)
4027
4296
  .then((result) => {
4028
4297
  if (disposed)
4029
4298
  return;
@@ -4814,7 +5083,9 @@ export class InteractiveMode {
4814
5083
  this.showStatus(`Session exported to: ${filePath}`);
4815
5084
  }
4816
5085
  else {
4817
- const filePath = await this.session.exportToHtml(outputPath);
5086
+ const filePath = await this.session.exportToHtml(outputPath, {
5087
+ themeName: theme.name,
5088
+ });
4818
5089
  this.showStatus(`Session exported to: ${filePath}`);
4819
5090
  }
4820
5091
  }
@@ -5356,7 +5627,7 @@ export class InteractiveMode {
5356
5627
  // Ignore, will be emitted as an event
5357
5628
  }
5358
5629
  }
5359
- stop() {
5630
+ stop(fullscreenExitOutput = this.settingsManager.getFullscreenExitOutput()) {
5360
5631
  this.disposeActiveSelector();
5361
5632
  if (this.settingsManager.getShowTerminalProgress()) {
5362
5633
  this.ui.terminal.setProgress(false);
@@ -5370,7 +5641,7 @@ export class InteractiveMode {
5370
5641
  this.unsubscribe();
5371
5642
  }
5372
5643
  if (this.isInitialized) {
5373
- this.stopInteractiveTui();
5644
+ this.stopInteractiveTui(fullscreenExitOutput);
5374
5645
  this.isInitialized = false;
5375
5646
  }
5376
5647
  this.unregisterSignalHandlers();