@selesai/code 0.3.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/dist/cli/args.d.ts.map +1 -1
  2. package/dist/cli/args.js +4 -4
  3. package/dist/cli/args.js.map +1 -1
  4. package/dist/cli/config-selector.d.ts +4 -2
  5. package/dist/cli/config-selector.d.ts.map +1 -1
  6. package/dist/cli/config-selector.js +1 -1
  7. package/dist/cli/config-selector.js.map +1 -1
  8. package/dist/cli/file-processor.d.ts.map +1 -1
  9. package/dist/cli/file-processor.js +13 -25
  10. package/dist/cli/file-processor.js.map +1 -1
  11. package/dist/core/agent-session.d.ts +22 -5
  12. package/dist/core/agent-session.d.ts.map +1 -1
  13. package/dist/core/agent-session.js +130 -49
  14. package/dist/core/agent-session.js.map +1 -1
  15. package/dist/core/auth-storage.d.ts.map +1 -1
  16. package/dist/core/auth-storage.js +13 -5
  17. package/dist/core/auth-storage.js.map +1 -1
  18. package/dist/core/cache-stats.d.ts +49 -0
  19. package/dist/core/cache-stats.d.ts.map +1 -0
  20. package/dist/core/cache-stats.js +101 -0
  21. package/dist/core/cache-stats.js.map +1 -0
  22. package/dist/core/compaction/compaction.d.ts +1 -2
  23. package/dist/core/compaction/compaction.d.ts.map +1 -1
  24. package/dist/core/compaction/compaction.js +56 -81
  25. package/dist/core/compaction/compaction.js.map +1 -1
  26. package/dist/core/extensions/index.d.ts +1 -1
  27. package/dist/core/extensions/index.d.ts.map +1 -1
  28. package/dist/core/extensions/index.js.map +1 -1
  29. package/dist/core/extensions/loader.d.ts.map +1 -1
  30. package/dist/core/extensions/loader.js +10 -0
  31. package/dist/core/extensions/loader.js.map +1 -1
  32. package/dist/core/extensions/runner.d.ts +5 -3
  33. package/dist/core/extensions/runner.d.ts.map +1 -1
  34. package/dist/core/extensions/runner.js +38 -0
  35. package/dist/core/extensions/runner.js.map +1 -1
  36. package/dist/core/extensions/types.d.ts +41 -12
  37. package/dist/core/extensions/types.d.ts.map +1 -1
  38. package/dist/core/extensions/types.js.map +1 -1
  39. package/dist/core/http-dispatcher.d.ts.map +1 -1
  40. package/dist/core/http-dispatcher.js +28 -1
  41. package/dist/core/http-dispatcher.js.map +1 -1
  42. package/dist/core/index.d.ts +1 -1
  43. package/dist/core/index.d.ts.map +1 -1
  44. package/dist/core/index.js.map +1 -1
  45. package/dist/core/keybindings.d.ts.map +1 -1
  46. package/dist/core/keybindings.js +3 -6
  47. package/dist/core/keybindings.js.map +1 -1
  48. package/dist/core/model-registry.d.ts +4 -6
  49. package/dist/core/model-registry.d.ts.map +1 -1
  50. package/dist/core/model-registry.js +34 -8
  51. package/dist/core/model-registry.js.map +1 -1
  52. package/dist/core/model-resolver.d.ts +10 -0
  53. package/dist/core/model-resolver.d.ts.map +1 -1
  54. package/dist/core/model-resolver.js +15 -18
  55. package/dist/core/model-resolver.js.map +1 -1
  56. package/dist/core/package-manager.d.ts +4 -1
  57. package/dist/core/package-manager.d.ts.map +1 -1
  58. package/dist/core/package-manager.js +68 -26
  59. package/dist/core/package-manager.js.map +1 -1
  60. package/dist/core/provider-attribution.d.ts.map +1 -1
  61. package/dist/core/provider-attribution.js +0 -10
  62. package/dist/core/provider-attribution.js.map +1 -1
  63. package/dist/core/resource-loader.d.ts +2 -2
  64. package/dist/core/resource-loader.d.ts.map +1 -1
  65. package/dist/core/resource-loader.js +8 -7
  66. package/dist/core/resource-loader.js.map +1 -1
  67. package/dist/core/sdk.d.ts +1 -1
  68. package/dist/core/sdk.d.ts.map +1 -1
  69. package/dist/core/sdk.js +8 -1
  70. package/dist/core/sdk.js.map +1 -1
  71. package/dist/core/session-manager.d.ts +21 -2
  72. package/dist/core/session-manager.d.ts.map +1 -1
  73. package/dist/core/session-manager.js +104 -72
  74. package/dist/core/session-manager.js.map +1 -1
  75. package/dist/core/settings-manager.d.ts +14 -3
  76. package/dist/core/settings-manager.d.ts.map +1 -1
  77. package/dist/core/settings-manager.js +29 -1
  78. package/dist/core/settings-manager.js.map +1 -1
  79. package/dist/core/slash-commands.d.ts +1 -0
  80. package/dist/core/slash-commands.d.ts.map +1 -1
  81. package/dist/core/slash-commands.js +3 -3
  82. package/dist/core/slash-commands.js.map +1 -1
  83. package/dist/core/timings.d.ts +4 -2
  84. package/dist/core/timings.d.ts.map +1 -1
  85. package/dist/core/timings.js +24 -14
  86. package/dist/core/timings.js.map +1 -1
  87. package/dist/core/tools/bash.d.ts.map +1 -1
  88. package/dist/core/tools/bash.js +20 -5
  89. package/dist/core/tools/bash.js.map +1 -1
  90. package/dist/core/tools/edit.d.ts.map +1 -1
  91. package/dist/core/tools/edit.js +2 -2
  92. package/dist/core/tools/edit.js.map +1 -1
  93. package/dist/core/tools/read.d.ts.map +1 -1
  94. package/dist/core/tools/read.js +12 -25
  95. package/dist/core/tools/read.js.map +1 -1
  96. package/dist/defaults/models.json +2 -3
  97. package/dist/extensions/caveman/index.js +16 -1
  98. package/dist/extensions/caveman/test/extension.test.js +7 -4
  99. package/dist/extensions/caveman/test/helpers.test.js +12 -1
  100. package/dist/extensions/context-compaction-reminder.test.ts +82 -0
  101. package/dist/extensions/context-compaction-reminder.ts +28 -0
  102. package/dist/extensions/handoff-new.test.ts +2 -13
  103. package/dist/extensions/handoff-new.ts +16 -25
  104. package/dist/extensions/package.json +1 -0
  105. package/dist/extensions/pi-subagents/agents/architect.md +25 -173
  106. package/dist/extensions/pi-subagents/agents/builder.md +16 -105
  107. package/dist/extensions/pi-subagents/agents/commentator.md +19 -116
  108. package/dist/extensions/pi-subagents/agents/explorer.md +13 -33
  109. package/dist/extensions/pi-subagents/agents/recapper.md +19 -11
  110. package/dist/extensions/pi-subagents/agents/researcher.md +10 -33
  111. package/dist/extensions/question/constants.ts +0 -7
  112. package/dist/extensions/question/index.ts +11 -108
  113. package/dist/extensions/question/schemas.ts +1 -26
  114. package/dist/extensions/question/shortcuts.ts +5 -14
  115. package/dist/extensions/question/tests/question-list.test.ts +8 -0
  116. package/dist/extensions/question/tests/ui-protocol.test.ts +1 -7
  117. package/dist/extensions/question/tui-adapter.ts +3 -20
  118. package/dist/extensions/question/types.ts +0 -6
  119. package/dist/extensions/question/ui-protocol.ts +2 -17
  120. package/dist/extensions/workflow/adapter.ts +384 -124
  121. package/dist/extensions/workflow/extension.ts +2 -1
  122. package/dist/extensions/workflow/modes/prototype.ts +6 -6
  123. package/dist/extensions/workflow/modes/quick.ts +6 -6
  124. package/dist/extensions/workflow/modes/task.ts +79 -0
  125. package/dist/extensions/workflow/run-state.ts +124 -0
  126. package/dist/extensions/workflow/state-machine.ts +4 -8
  127. package/dist/index.d.ts +3 -2
  128. package/dist/index.d.ts.map +1 -1
  129. package/dist/index.js +2 -1
  130. package/dist/index.js.map +1 -1
  131. package/dist/main.d.ts +2 -2
  132. package/dist/main.d.ts.map +1 -1
  133. package/dist/main.js +17 -4
  134. package/dist/main.js.map +1 -1
  135. package/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  136. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  137. package/dist/modes/interactive/components/assistant-message.js +23 -15
  138. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  139. package/dist/modes/interactive/components/config-selector.d.ts +34 -3
  140. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  141. package/dist/modes/interactive/components/config-selector.js +280 -25
  142. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  143. package/dist/modes/interactive/components/custom-entry.d.ts +19 -0
  144. package/dist/modes/interactive/components/custom-entry.d.ts.map +1 -0
  145. package/dist/modes/interactive/components/custom-entry.js +52 -0
  146. package/dist/modes/interactive/components/custom-entry.js.map +1 -0
  147. package/dist/modes/interactive/components/extension-editor.d.ts +3 -1
  148. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  149. package/dist/modes/interactive/components/extension-editor.js +12 -3
  150. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  151. package/dist/modes/interactive/components/footer.d.ts +4 -0
  152. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  153. package/dist/modes/interactive/components/footer.js +1 -1
  154. package/dist/modes/interactive/components/footer.js.map +1 -1
  155. package/dist/modes/interactive/components/oauth-selector.d.ts +3 -1
  156. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  157. package/dist/modes/interactive/components/oauth-selector.js +17 -6
  158. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  159. package/dist/modes/interactive/components/settings-selector.d.ts +4 -0
  160. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  161. package/dist/modes/interactive/components/settings-selector.js +25 -2
  162. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  163. package/dist/modes/interactive/components/status-indicator.d.ts +28 -0
  164. package/dist/modes/interactive/components/status-indicator.d.ts.map +1 -0
  165. package/dist/modes/interactive/components/status-indicator.js +60 -0
  166. package/dist/modes/interactive/components/status-indicator.js.map +1 -0
  167. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  168. package/dist/modes/interactive/components/thinking-selector.js +2 -1
  169. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  170. package/dist/modes/interactive/components/user-message.d.ts +6 -2
  171. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  172. package/dist/modes/interactive/components/user-message.js +19 -6
  173. package/dist/modes/interactive/components/user-message.js.map +1 -1
  174. package/dist/modes/interactive/interactive-mode.d.ts +21 -10
  175. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  176. package/dist/modes/interactive/interactive-mode.js +417 -196
  177. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  178. package/dist/modes/interactive/theme/dark.json +1 -0
  179. package/dist/modes/interactive/theme/light.json +1 -0
  180. package/dist/modes/interactive/theme/theme-schema.json +6 -2
  181. package/dist/modes/interactive/theme/theme.d.ts +3 -2
  182. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  183. package/dist/modes/interactive/theme/theme.js +10 -3
  184. package/dist/modes/interactive/theme/theme.js.map +1 -1
  185. package/dist/modes/print-mode.d.ts.map +1 -1
  186. package/dist/modes/print-mode.js +1 -1
  187. package/dist/modes/print-mode.js.map +1 -1
  188. package/dist/modes/rpc/rpc-client.d.ts +21 -6
  189. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  190. package/dist/modes/rpc/rpc-client.js +17 -3
  191. package/dist/modes/rpc/rpc-client.js.map +1 -1
  192. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  193. package/dist/modes/rpc/rpc-mode.js +20 -1
  194. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  195. package/dist/modes/rpc/rpc-types.d.ts +26 -0
  196. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  197. package/dist/modes/rpc/rpc-types.js.map +1 -1
  198. package/dist/package-manager-cli.d.ts +2 -2
  199. package/dist/package-manager-cli.d.ts.map +1 -1
  200. package/dist/package-manager-cli.js +71 -17
  201. package/dist/package-manager-cli.js.map +1 -1
  202. package/dist/rpc-entry.d.ts +3 -0
  203. package/dist/rpc-entry.d.ts.map +1 -0
  204. package/dist/rpc-entry.js +10 -0
  205. package/dist/rpc-entry.js.map +1 -0
  206. package/dist/skills/workflow-creation/SKILL.md +72 -0
  207. package/dist/utils/clipboard-image.d.ts.map +1 -1
  208. package/dist/utils/clipboard-image.js +1 -1
  209. package/dist/utils/clipboard-image.js.map +1 -1
  210. package/dist/utils/image-convert.d.ts +1 -0
  211. package/dist/utils/image-convert.d.ts.map +1 -1
  212. package/dist/utils/image-convert.js +21 -15
  213. package/dist/utils/image-convert.js.map +1 -1
  214. package/dist/utils/image-process.d.ts +18 -0
  215. package/dist/utils/image-process.d.ts.map +1 -0
  216. package/dist/utils/image-process.js +83 -0
  217. package/dist/utils/image-process.js.map +1 -0
  218. package/dist/utils/mime.d.ts.map +1 -1
  219. package/dist/utils/mime.js +41 -0
  220. package/dist/utils/mime.js.map +1 -1
  221. package/docs/workflows.md +40 -6
  222. package/package.json +8 -5
@@ -6,12 +6,13 @@ import * as fs from "node:fs";
6
6
  import * as os from "node:os";
7
7
  import * as path from "node:path";
8
8
  import { getProviders, } from "@earendil-works/pi-ai/compat";
9
- import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@earendil-works/pi-tui";
9
+ import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@earendil-works/pi-tui";
10
10
  import chalk from "chalk";
11
11
  import { spawn, spawnSync } from "child_process";
12
12
  import { APP_NAME, APP_TITLE, CONFIG_DIR_NAME, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, VERSION, } from "../../config.js";
13
13
  import { parseSkillBlock } from "../../core/agent-session.js";
14
14
  import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
15
+ import { CACHE_TTL_MS, collectCacheMisses, computeCacheWaste, detectCacheMiss, } from "../../core/cache-stats.js";
15
16
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
16
17
  import { configureHttpDispatcher, formatHttpIdleTimeoutMs } from "../../core/http-dispatcher.js";
17
18
  import { KeybindingsManager } from "../../core/keybindings.js";
@@ -20,7 +21,7 @@ import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScop
20
21
  import { DefaultPackageManager } from "../../core/package-manager.js";
21
22
  import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.js";
22
23
  import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
23
- import { SessionManager } from "../../core/session-manager.js";
24
+ import { SessionManager, sessionEntryToContextMessages } from "../../core/session-manager.js";
24
25
  import { buildGitCommandPrompt, GIT_COMMAND_MODES } from "../../core/git-command.js";
25
26
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
26
27
  import { isInstallTelemetryEnabled } from "../../core/telemetry.js";
@@ -41,8 +42,8 @@ import { BashExecutionComponent } from "./components/bash-execution.js";
41
42
  import { BorderedLoader } from "./components/bordered-loader.js";
42
43
  import { BranchSummaryMessageComponent } from "./components/branch-summary-message.js";
43
44
  import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message.js";
44
- import { CountdownTimer } from "./components/countdown-timer.js";
45
45
  import { CustomEditor } from "./components/custom-editor.js";
46
+ import { CustomEntryComponent } from "./components/custom-entry.js";
46
47
  import { CustomMessageComponent } from "./components/custom-message.js";
47
48
  import { DaxnutsComponent } from "./components/daxnuts.js";
48
49
  import { DynamicBorder } from "./components/dynamic-border.js";
@@ -50,15 +51,16 @@ import { EarendilAnnouncementComponent } from "./components/earendil-announcemen
50
51
  import { ExtensionEditorComponent } from "./components/extension-editor.js";
51
52
  import { ExtensionInputComponent } from "./components/extension-input.js";
52
53
  import { ExtensionSelectorComponent } from "./components/extension-selector.js";
53
- import { FooterComponent } from "./components/footer.js";
54
+ import { FooterComponent, formatTokens } from "./components/footer.js";
54
55
  import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
55
56
  import { LoginDialogComponent } from "./components/login-dialog.js";
56
57
  import { ModelSelectorComponent } from "./components/model-selector.js";
57
- import { OAuthSelectorComponent } from "./components/oauth-selector.js";
58
+ import { formatAuthSelectorProviderType, OAuthSelectorComponent, } from "./components/oauth-selector.js";
58
59
  import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js";
59
60
  import { SessionSelectorComponent } from "./components/session-selector.js";
60
61
  import { SettingsSelectorComponent } from "./components/settings-selector.js";
61
62
  import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.js";
63
+ import { BranchSummaryStatusIndicator, CompactionStatusIndicator, IdleStatus, RetryStatusIndicator, WorkingStatusIndicator, } from "./components/status-indicator.js";
62
64
  import { ToolExecutionComponent } from "./components/tool-execution.js";
63
65
  import { TreeSelectorComponent } from "./components/tree-selector.js";
64
66
  import { TrustSelectorComponent } from "./components/trust-selector.js";
@@ -82,6 +84,9 @@ class ExpandableText extends Text {
82
84
  this.setText(expanded ? this.getExpandedText() : this.getCollapsedText());
83
85
  }
84
86
  }
87
+ function isCustomSessionEntry(item) {
88
+ return "type" in item && item.type === "custom";
89
+ }
85
90
  const DEAD_TERMINAL_ERROR_CODES = new Set(["EIO", "EPIPE", "ENOTCONN"]);
86
91
  function isDeadTerminalError(error) {
87
92
  if (!error || typeof error !== "object" || !("code" in error)) {
@@ -132,9 +137,46 @@ export function isApiKeyLoginProvider(providerId, oauthProviderIds, builtInProvi
132
137
  }
133
138
  return !oauthProviderIds.has(providerId);
134
139
  }
140
+ const AUTH_TYPE_ORDER = { oauth: 0, api_key: 1 };
141
+ function createFuzzyAutocompleteItems(items, prefix, getSearchText, toAutocompleteItem) {
142
+ const filtered = fuzzyFilter(items, prefix, getSearchText);
143
+ if (filtered.length === 0)
144
+ return null;
145
+ return filtered.map(toAutocompleteItem);
146
+ }
147
+ function getLoginProviderCompletionOptions(providerOptions) {
148
+ const byId = new Map();
149
+ for (const provider of providerOptions) {
150
+ const existing = byId.get(provider.id);
151
+ if (existing) {
152
+ if (!existing.authTypes.includes(provider.authType)) {
153
+ existing.authTypes.push(provider.authType);
154
+ existing.authTypes.sort((a, b) => AUTH_TYPE_ORDER[a] - AUTH_TYPE_ORDER[b]);
155
+ }
156
+ continue;
157
+ }
158
+ byId.set(provider.id, {
159
+ id: provider.id,
160
+ name: provider.name,
161
+ authTypes: [provider.authType],
162
+ });
163
+ }
164
+ return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name));
165
+ }
166
+ function getLoginProviderSearchText(provider) {
167
+ const authTypes = provider.authTypes
168
+ .map((authType) => `${authType} ${formatAuthSelectorProviderType(authType)}`)
169
+ .join(" ");
170
+ return `${provider.id} ${provider.name} ${authTypes}`;
171
+ }
172
+ function formatLoginProviderCompletionDescription(provider) {
173
+ const authTypes = provider.authTypes.map(formatAuthSelectorProviderType).join("/");
174
+ return provider.name === provider.id ? authTypes : `${provider.name} · ${authTypes}`;
175
+ }
135
176
  export class InteractiveMode {
136
177
  runtimeHost;
137
178
  ui;
179
+ loadedResourcesContainer;
138
180
  chatContainer;
139
181
  pendingMessagesContainer;
140
182
  statusContainer;
@@ -154,7 +196,8 @@ export class InteractiveMode {
154
196
  onInputCallback;
155
197
  pendingUserInputs = [];
156
198
  pendingInputImages = [];
157
- loadingAnimation = undefined;
199
+ activeStatusIndicator = undefined;
200
+ idleStatus = new IdleStatus();
158
201
  workingMessage = undefined;
159
202
  workingVisible = true;
160
203
  workingIndicatorOptions = undefined;
@@ -178,6 +221,7 @@ export class InteractiveMode {
178
221
  toolOutputExpanded = false;
179
222
  // Thinking block visibility state
180
223
  hideThinkingBlock = false;
224
+ outputPad = 1;
181
225
  // Skill commands: command name -> skill file path
182
226
  skillCommands = new Map();
183
227
  // Agent subscription unsubscribe function
@@ -190,11 +234,8 @@ export class InteractiveMode {
190
234
  // Track pending bash components (shown in pending area, moved to chat on submit)
191
235
  pendingBashComponents = [];
192
236
  // Auto-compaction state
193
- autoCompactionLoader = undefined;
194
237
  autoCompactionEscapeHandler;
195
238
  // Auto-retry state
196
- retryLoader = undefined;
197
- retryCountdown = undefined;
198
239
  retryEscapeHandler;
199
240
  // Messages queued while compaction is running
200
241
  compactionQueuedMessages = [];
@@ -248,6 +289,7 @@ export class InteractiveMode {
248
289
  this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
249
290
  this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
250
291
  this.headerContainer = new Container();
292
+ this.loadedResourcesContainer = new Container();
251
293
  this.chatContainer = new Container();
252
294
  this.pendingMessagesContainer = new Container();
253
295
  this.statusContainer = new Container();
@@ -269,6 +311,7 @@ export class InteractiveMode {
269
311
  this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
270
312
  // Load hide thinking block setting
271
313
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
314
+ this.outputPad = this.settingsManager.getOutputPad();
272
315
  // Register themes from resource loader and initialize
273
316
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
274
317
  this.themeController = new InteractiveThemeController(this.ui, this.settingsManager, (message) => this.showError(message), () => this.updateEditorBorderColor());
@@ -317,6 +360,7 @@ export class InteractiveMode {
317
360
  const slashCommands = BUILTIN_SLASH_COMMANDS.map((command) => ({
318
361
  name: command.name,
319
362
  description: command.description,
363
+ ...(command.argumentHint && { argumentHint: command.argumentHint }),
320
364
  }));
321
365
  const modelCommand = slashCommands.find((command) => command.name === "model");
322
366
  if (modelCommand) {
@@ -334,11 +378,7 @@ export class InteractiveMode {
334
378
  name: m.name,
335
379
  label: `${m.provider}/${m.id}`,
336
380
  }));
337
- // Fuzzy filter by model ID + provider in either order.
338
- const filtered = fuzzyFilter(items, prefix, getModelSearchText);
339
- if (filtered.length === 0)
340
- return null;
341
- return filtered.map((item) => ({
381
+ return createFuzzyAutocompleteItems(items, prefix, getModelSearchText, (item) => ({
342
382
  value: item.label,
343
383
  label: item.id,
344
384
  description: item.provider,
@@ -355,6 +395,17 @@ export class InteractiveMode {
355
395
  : null;
356
396
  };
357
397
  }
398
+ const loginCommand = slashCommands.find((command) => command.name === "login");
399
+ if (loginCommand) {
400
+ loginCommand.getArgumentCompletions = (prefix) => {
401
+ const providers = getLoginProviderCompletionOptions(this.getLoginProviderOptions());
402
+ return createFuzzyAutocompleteItems(providers, prefix, getLoginProviderSearchText, (provider) => ({
403
+ value: provider.id,
404
+ label: provider.id,
405
+ description: formatLoginProviderCompletionDescription(provider),
406
+ }));
407
+ };
408
+ }
358
409
  // Convert prompt templates to SlashCommand format for autocomplete
359
410
  const templateCommands = this.session.promptTemplates.map((cmd) => ({
360
411
  name: cmd.name,
@@ -451,8 +502,10 @@ export class InteractiveMode {
451
502
  : "";
452
503
  console.log(theme.fg("dim", `Model scope: ${modelList}${cycleHint}`));
453
504
  }
454
- // Add header container as first child. Populate it after detectThemeIfUnset.
505
+ // Add header container as first child. Populate it after applying theme settings.
506
+ // Keep loaded resources before chat so restored session messages never precede them.
455
507
  this.ui.addChild(this.headerContainer);
508
+ this.ui.addChild(this.loadedResourcesContainer);
456
509
  this.ui.addChild(this.chatContainer);
457
510
  this.ui.addChild(this.pendingMessagesContainer);
458
511
  this.ui.addChild(this.statusContainer);
@@ -1002,6 +1055,8 @@ export class InteractiveMode {
1002
1055
  return lines.join("\n");
1003
1056
  }
1004
1057
  showLoadedResources(options) {
1058
+ // Resource rendering is idempotent; chat clears no longer clear this separate container.
1059
+ this.loadedResourcesContainer.clear();
1005
1060
  const showListing = options?.force || this.options.verbose || !this.settingsManager.getQuietStartup();
1006
1061
  const showDiagnostics = showListing || options?.showDiagnosticsWhenQuiet === true;
1007
1062
  if (!showListing && !showDiagnostics) {
@@ -1017,8 +1072,8 @@ export class InteractiveMode {
1017
1072
  };
1018
1073
  const addLoadedSection = (name, collapsedBody, expandedBody = collapsedBody, color = "mdHeading") => {
1019
1074
  const section = new ExpandableText(() => `${sectionHeader(name, color)}\n${collapsedBody}`, () => `${sectionHeader(name, color)}\n${expandedBody}`, this.getStartupExpansionState(), 0, 0);
1020
- this.chatContainer.addChild(section);
1021
- this.chatContainer.addChild(new Spacer(1));
1075
+ this.loadedResourcesContainer.addChild(section);
1076
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1022
1077
  };
1023
1078
  const skillsResult = this.session.resourceLoader.getSkills();
1024
1079
  const promptsResult = this.session.resourceLoader.getPrompts();
@@ -1052,7 +1107,7 @@ export class InteractiveMode {
1052
1107
  if (showListing) {
1053
1108
  const contextFiles = this.session.resourceLoader.getAgentsFiles().agentsFiles;
1054
1109
  if (contextFiles.length > 0) {
1055
- this.chatContainer.addChild(new Spacer(1));
1110
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1056
1111
  const contextList = contextFiles
1057
1112
  .map((f) => theme.fg("dim", ` ${this.formatDisplayPath(f.path)}`))
1058
1113
  .join("\n");
@@ -1115,14 +1170,14 @@ export class InteractiveMode {
1115
1170
  const skillDiagnostics = skillsResult.diagnostics;
1116
1171
  if (skillDiagnostics.length > 0) {
1117
1172
  const warningLines = this.formatDiagnostics(skillDiagnostics, sourceInfos);
1118
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0));
1119
- this.chatContainer.addChild(new Spacer(1));
1173
+ this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0));
1174
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1120
1175
  }
1121
1176
  const promptDiagnostics = promptsResult.diagnostics;
1122
1177
  if (promptDiagnostics.length > 0) {
1123
1178
  const warningLines = this.formatDiagnostics(promptDiagnostics, sourceInfos);
1124
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, 0, 0));
1125
- this.chatContainer.addChild(new Spacer(1));
1179
+ this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, 0, 0));
1180
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1126
1181
  }
1127
1182
  const extensionDiagnostics = [];
1128
1183
  const extensionErrors = this.session.resourceLoader.getExtensions().errors;
@@ -1138,14 +1193,14 @@ export class InteractiveMode {
1138
1193
  extensionDiagnostics.push(...shortcutDiagnostics);
1139
1194
  if (extensionDiagnostics.length > 0) {
1140
1195
  const warningLines = this.formatDiagnostics(extensionDiagnostics, sourceInfos);
1141
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0));
1142
- this.chatContainer.addChild(new Spacer(1));
1196
+ this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0));
1197
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1143
1198
  }
1144
1199
  const themeDiagnostics = themesResult.diagnostics;
1145
1200
  if (themeDiagnostics.length > 0) {
1146
1201
  const warningLines = this.formatDiagnostics(themeDiagnostics, sourceInfos);
1147
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0));
1148
- this.chatContainer.addChild(new Spacer(1));
1202
+ this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0));
1203
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1149
1204
  }
1150
1205
  }
1151
1206
  }
@@ -1161,13 +1216,9 @@ export class InteractiveMode {
1161
1216
  this.restoreQueuedMessagesToEditor({ abort: true });
1162
1217
  },
1163
1218
  commandContextActions: {
1164
- waitForIdle: () => this.session.agent.waitForIdle(),
1219
+ waitForIdle: () => this.session.waitForIdle(),
1165
1220
  newSession: async (options) => {
1166
- if (this.loadingAnimation) {
1167
- this.loadingAnimation.stop();
1168
- this.loadingAnimation = undefined;
1169
- }
1170
- this.statusContainer.clear();
1221
+ this.clearStatusIndicator();
1171
1222
  try {
1172
1223
  return await this.runtimeHost.newSession(options);
1173
1224
  }
@@ -1216,7 +1267,7 @@ export class InteractiveMode {
1216
1267
  },
1217
1268
  shutdownHandler: () => {
1218
1269
  this.shutdownRequested = true;
1219
- if (!this.session.isStreaming) {
1270
+ if (this.session.isIdle) {
1220
1271
  void this.shutdown();
1221
1272
  }
1222
1273
  },
@@ -1237,8 +1288,13 @@ export class InteractiveMode {
1237
1288
  this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
1238
1289
  this.footerDataProvider.setCwd(this.sessionManager.getCwd());
1239
1290
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
1291
+ this.outputPad = this.settingsManager.getOutputPad();
1240
1292
  this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
1241
- this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
1293
+ const clearOnShrink = this.settingsManager.getClearOnShrink();
1294
+ this.ui.setClearOnShrink(clearOnShrink);
1295
+ if (!clearOnShrink && !this.activeStatusIndicator) {
1296
+ this.statusContainer.clear();
1297
+ }
1242
1298
  const editorPaddingX = this.settingsManager.getEditorPaddingX();
1243
1299
  const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
1244
1300
  this.defaultEditor.setPaddingX(editorPaddingX);
@@ -1273,6 +1329,7 @@ export class InteractiveMode {
1273
1329
  process.exit(1);
1274
1330
  }
1275
1331
  renderCurrentSessionState() {
1332
+ this.loadedResourcesContainer.clear();
1276
1333
  this.chatContainer.clear();
1277
1334
  this.pendingMessagesContainer.clear();
1278
1335
  this.compactionQueuedMessages = [];
@@ -1303,7 +1360,7 @@ export class InteractiveMode {
1303
1360
  sessionManager: this.sessionManager,
1304
1361
  modelRegistry: this.session.modelRegistry,
1305
1362
  model: this.session.model,
1306
- isIdle: () => !this.session.isStreaming,
1363
+ isIdle: () => this.session.isIdle,
1307
1364
  isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
1308
1365
  signal: this.session.agent.signal,
1309
1366
  abort: () => {
@@ -1350,36 +1407,41 @@ export class InteractiveMode {
1350
1407
  this.footerDataProvider.setExtensionStatus(key, text);
1351
1408
  this.ui.requestRender();
1352
1409
  }
1353
- getWorkingLoaderMessage() {
1354
- return this.workingMessage ?? this.defaultWorkingMessage;
1355
- }
1356
- createWorkingLoader() {
1357
- return new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), this.getWorkingLoaderMessage(), this.workingIndicatorOptions);
1410
+ showStatusIndicator(indicator) {
1411
+ this.activeStatusIndicator?.dispose();
1412
+ this.activeStatusIndicator = indicator;
1413
+ this.statusContainer.clear();
1414
+ this.statusContainer.addChild(indicator);
1358
1415
  }
1359
- stopWorkingLoader() {
1360
- if (this.loadingAnimation) {
1361
- this.loadingAnimation.stop();
1362
- this.loadingAnimation = undefined;
1416
+ clearStatusIndicator(kind) {
1417
+ if (kind && this.activeStatusIndicator?.kind !== kind) {
1418
+ return;
1363
1419
  }
1420
+ const hadActiveStatusIndicator = this.activeStatusIndicator !== undefined;
1421
+ this.activeStatusIndicator?.dispose();
1422
+ this.activeStatusIndicator = undefined;
1364
1423
  this.statusContainer.clear();
1424
+ if (hadActiveStatusIndicator && this.ui.getClearOnShrink()) {
1425
+ this.statusContainer.addChild(this.idleStatus);
1426
+ }
1365
1427
  }
1366
1428
  setWorkingVisible(visible) {
1367
1429
  this.workingVisible = visible;
1368
1430
  if (!visible) {
1369
- this.stopWorkingLoader();
1431
+ this.clearStatusIndicator("working");
1370
1432
  this.ui.requestRender();
1371
1433
  return;
1372
1434
  }
1373
- if (this.session.isStreaming && !this.loadingAnimation) {
1374
- this.statusContainer.clear();
1375
- this.loadingAnimation = this.createWorkingLoader();
1376
- this.statusContainer.addChild(this.loadingAnimation);
1435
+ if (this.session.isStreaming && this.activeStatusIndicator?.kind !== "working") {
1436
+ this.showStatusIndicator(new WorkingStatusIndicator(this.ui, this.workingMessage ?? this.defaultWorkingMessage, this.workingIndicatorOptions));
1377
1437
  }
1378
1438
  this.ui.requestRender();
1379
1439
  }
1380
1440
  setWorkingIndicator(options) {
1381
1441
  this.workingIndicatorOptions = options;
1382
- this.loadingAnimation?.setIndicator(options);
1442
+ if (this.activeStatusIndicator?.kind === "working") {
1443
+ this.activeStatusIndicator.setIndicator(options);
1444
+ }
1383
1445
  this.ui.requestRender();
1384
1446
  }
1385
1447
  setHiddenThinkingLabel(label) {
@@ -1467,8 +1529,8 @@ export class InteractiveMode {
1467
1529
  this.workingMessage = undefined;
1468
1530
  this.workingVisible = true;
1469
1531
  this.setWorkingIndicator();
1470
- if (this.loadingAnimation) {
1471
- this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
1532
+ if (this.activeStatusIndicator?.kind === "working") {
1533
+ this.activeStatusIndicator.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
1472
1534
  }
1473
1535
  this.setHiddenThinkingLabel();
1474
1536
  }
@@ -1608,8 +1670,8 @@ export class InteractiveMode {
1608
1670
  setStatus: (key, text) => this.setExtensionStatus(key, text),
1609
1671
  setWorkingMessage: (message) => {
1610
1672
  this.workingMessage = message;
1611
- if (this.loadingAnimation) {
1612
- this.loadingAnimation.setMessage(message ?? this.defaultWorkingMessage);
1673
+ if (this.activeStatusIndicator?.kind === "working") {
1674
+ this.activeStatusIndicator.setMessage(message ?? this.defaultWorkingMessage);
1613
1675
  }
1614
1676
  },
1615
1677
  setWorkingVisible: (visible) => this.setWorkingVisible(visible),
@@ -1753,7 +1815,7 @@ export class InteractiveMode {
1753
1815
  }, () => {
1754
1816
  this.hideExtensionEditor();
1755
1817
  resolve(undefined);
1756
- });
1818
+ }, undefined, this.settingsManager.getExternalEditorCommand());
1757
1819
  this.editorContainer.clear();
1758
1820
  this.editorContainer.addChild(this.extensionEditor);
1759
1821
  this.ui.setFocus(this.extensionEditor);
@@ -2163,9 +2225,10 @@ export class InteractiveMode {
2163
2225
  this.editor.setText("");
2164
2226
  return;
2165
2227
  }
2166
- if (text === "/login") {
2167
- this.showOAuthSelector("login");
2228
+ if (text === "/login" || text.startsWith("/login ")) {
2229
+ const providerRef = text.startsWith("/login ") ? text.slice(7).trim() : undefined;
2168
2230
  this.editor.setText("");
2231
+ await this.handleLoginCommand(providerRef);
2169
2232
  return;
2170
2233
  }
2171
2234
  if (text === "/logout") {
@@ -2288,18 +2351,11 @@ export class InteractiveMode {
2288
2351
  this.defaultEditor.onEscape = this.retryEscapeHandler;
2289
2352
  this.retryEscapeHandler = undefined;
2290
2353
  }
2291
- if (this.retryCountdown) {
2292
- this.retryCountdown.dispose();
2293
- this.retryCountdown = undefined;
2294
- }
2295
- if (this.retryLoader) {
2296
- this.retryLoader.stop();
2297
- this.retryLoader = undefined;
2298
- }
2299
- this.stopWorkingLoader();
2300
2354
  if (this.workingVisible) {
2301
- this.loadingAnimation = this.createWorkingLoader();
2302
- this.statusContainer.addChild(this.loadingAnimation);
2355
+ this.showStatusIndicator(new WorkingStatusIndicator(this.ui, this.workingMessage ?? this.defaultWorkingMessage, this.workingIndicatorOptions));
2356
+ }
2357
+ else {
2358
+ this.clearStatusIndicator();
2303
2359
  }
2304
2360
  this.ui.requestRender();
2305
2361
  break;
@@ -2307,6 +2363,12 @@ export class InteractiveMode {
2307
2363
  this.updatePendingMessagesDisplay();
2308
2364
  this.ui.requestRender();
2309
2365
  break;
2366
+ case "entry_appended":
2367
+ if (event.entry.type === "custom") {
2368
+ this.addCustomEntryToChat(event.entry);
2369
+ this.ui.requestRender();
2370
+ }
2371
+ break;
2310
2372
  case "session_info_changed":
2311
2373
  this.updateTerminalTitle();
2312
2374
  this.footer.invalidate();
@@ -2327,7 +2389,7 @@ export class InteractiveMode {
2327
2389
  this.ui.requestRender();
2328
2390
  }
2329
2391
  else if (event.message.role === "assistant") {
2330
- this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel);
2392
+ this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, this.outputPad);
2331
2393
  this.streamingMessage = event.message;
2332
2394
  this.chatContainer.addChild(this.streamingComponent);
2333
2395
  this.streamingComponent.updateContent(this.streamingMessage);
@@ -2392,6 +2454,7 @@ export class InteractiveMode {
2392
2454
  for (const [, component] of this.pendingTools.entries()) {
2393
2455
  component.setArgsComplete();
2394
2456
  }
2457
+ this.maybeShowCacheMissNotice(this.streamingMessage);
2395
2458
  }
2396
2459
  this.streamingComponent = undefined;
2397
2460
  this.streamingMessage = undefined;
@@ -2435,20 +2498,18 @@ export class InteractiveMode {
2435
2498
  if (this.settingsManager.getShowTerminalProgress()) {
2436
2499
  this.ui.terminal.setProgress(false);
2437
2500
  }
2438
- if (this.loadingAnimation) {
2439
- this.loadingAnimation.stop();
2440
- this.loadingAnimation = undefined;
2441
- this.statusContainer.clear();
2442
- }
2501
+ this.clearStatusIndicator("working");
2443
2502
  if (this.streamingComponent) {
2444
2503
  this.chatContainer.removeChild(this.streamingComponent);
2445
2504
  this.streamingComponent = undefined;
2446
2505
  this.streamingMessage = undefined;
2447
2506
  }
2448
2507
  this.pendingTools.clear();
2449
- await this.checkShutdownRequested();
2450
2508
  this.ui.requestRender();
2451
2509
  break;
2510
+ case "agent_settled":
2511
+ await this.checkShutdownRequested();
2512
+ break;
2452
2513
  case "compaction_start": {
2453
2514
  if (this.settingsManager.getShowTerminalProgress()) {
2454
2515
  this.ui.terminal.setProgress(true);
@@ -2458,13 +2519,7 @@ export class InteractiveMode {
2458
2519
  this.defaultEditor.onEscape = () => {
2459
2520
  this.session.abortCompaction();
2460
2521
  };
2461
- this.statusContainer.clear();
2462
- const cancelHint = `(${keyText("app.interrupt")} to cancel)`;
2463
- const label = event.reason === "manual"
2464
- ? `Compacting context... ${cancelHint}`
2465
- : `${event.reason === "overflow" ? "Context overflow detected, " : ""}Auto-compacting... ${cancelHint}`;
2466
- this.autoCompactionLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), label);
2467
- this.statusContainer.addChild(this.autoCompactionLoader);
2522
+ this.showStatusIndicator(new CompactionStatusIndicator(this.ui, event.reason));
2468
2523
  this.ui.requestRender();
2469
2524
  break;
2470
2525
  }
@@ -2476,11 +2531,7 @@ export class InteractiveMode {
2476
2531
  this.defaultEditor.onEscape = this.autoCompactionEscapeHandler;
2477
2532
  this.autoCompactionEscapeHandler = undefined;
2478
2533
  }
2479
- if (this.autoCompactionLoader) {
2480
- this.autoCompactionLoader.stop();
2481
- this.autoCompactionLoader = undefined;
2482
- this.statusContainer.clear();
2483
- }
2534
+ this.clearStatusIndicator("compaction");
2484
2535
  if (event.aborted) {
2485
2536
  if (event.reason === "manual") {
2486
2537
  this.showError("Compaction cancelled");
@@ -2514,17 +2565,7 @@ export class InteractiveMode {
2514
2565
  this.defaultEditor.onEscape = () => {
2515
2566
  this.session.abortRetry();
2516
2567
  };
2517
- // Show retry indicator
2518
- this.statusContainer.clear();
2519
- this.retryCountdown?.dispose();
2520
- const retryMessage = (seconds) => `Retrying (${event.attempt}/${event.maxAttempts}) in ${seconds}s... (${keyText("app.interrupt")} to cancel)`;
2521
- this.retryLoader = new Loader(this.ui, (spinner) => theme.fg("warning", spinner), (text) => theme.fg("muted", text), retryMessage(Math.ceil(event.delayMs / 1000)));
2522
- this.retryCountdown = new CountdownTimer(event.delayMs, this.ui, (seconds) => {
2523
- this.retryLoader?.setMessage(retryMessage(seconds));
2524
- }, () => {
2525
- this.retryCountdown = undefined;
2526
- });
2527
- this.statusContainer.addChild(this.retryLoader);
2568
+ this.showStatusIndicator(new RetryStatusIndicator(this.ui, event.attempt, event.maxAttempts, event.delayMs));
2528
2569
  this.ui.requestRender();
2529
2570
  break;
2530
2571
  }
@@ -2534,16 +2575,7 @@ export class InteractiveMode {
2534
2575
  this.defaultEditor.onEscape = this.retryEscapeHandler;
2535
2576
  this.retryEscapeHandler = undefined;
2536
2577
  }
2537
- if (this.retryCountdown) {
2538
- this.retryCountdown.dispose();
2539
- this.retryCountdown = undefined;
2540
- }
2541
- // Stop loader
2542
- if (this.retryLoader) {
2543
- this.retryLoader.stop();
2544
- this.retryLoader = undefined;
2545
- this.statusContainer.clear();
2546
- }
2578
+ this.clearStatusIndicator("retry");
2547
2579
  // Show error only on final failure (success shows normal response)
2548
2580
  if (!event.success) {
2549
2581
  this.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`);
@@ -2585,6 +2617,25 @@ export class InteractiveMode {
2585
2617
  this.lastStatusText = text;
2586
2618
  this.ui.requestRender();
2587
2619
  }
2620
+ addCustomEntryToChat(entry) {
2621
+ const renderer = this.session.extensionRunner.getEntryRenderer(entry.customType);
2622
+ if (!renderer) {
2623
+ return;
2624
+ }
2625
+ const component = new CustomEntryComponent(entry, renderer);
2626
+ component.setExpanded(this.toolOutputExpanded);
2627
+ if (!component.hasContent()) {
2628
+ return;
2629
+ }
2630
+ if (this.streamingComponent) {
2631
+ const streamingIndex = this.chatContainer.children.indexOf(this.streamingComponent);
2632
+ if (streamingIndex >= 0) {
2633
+ this.chatContainer.children.splice(streamingIndex, 0, component);
2634
+ return;
2635
+ }
2636
+ }
2637
+ this.chatContainer.addChild(component);
2638
+ }
2588
2639
  addMessageToChat(message, options) {
2589
2640
  switch (message.role) {
2590
2641
  case "bashExecution": {
@@ -2634,12 +2685,12 @@ export class InteractiveMode {
2634
2685
  // Render user message separately if present
2635
2686
  if (skillBlock.userMessage) {
2636
2687
  this.chatContainer.addChild(new Spacer(1));
2637
- const userComponent = new UserMessageComponent(skillBlock.userMessage, this.getMarkdownThemeWithSettings());
2688
+ const userComponent = new UserMessageComponent(skillBlock.userMessage, this.getMarkdownThemeWithSettings(), this.outputPad);
2638
2689
  this.chatContainer.addChild(userComponent);
2639
2690
  }
2640
2691
  }
2641
2692
  else {
2642
- const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings());
2693
+ const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings(), this.outputPad);
2643
2694
  this.chatContainer.addChild(userComponent);
2644
2695
  }
2645
2696
  if (options?.populateHistory) {
@@ -2649,7 +2700,7 @@ export class InteractiveMode {
2649
2700
  break;
2650
2701
  }
2651
2702
  case "assistant": {
2652
- const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel);
2703
+ const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, this.outputPad);
2653
2704
  this.chatContainer.addChild(assistantComponent);
2654
2705
  break;
2655
2706
  }
@@ -2662,20 +2713,24 @@ export class InteractiveMode {
2662
2713
  }
2663
2714
  }
2664
2715
  }
2665
- /**
2666
- * Render session context to chat. Used for initial load and rebuild after compaction.
2667
- * @param sessionContext Session context to render
2668
- * @param options.updateFooter Update footer state
2669
- * @param options.populateHistory Add user messages to editor history
2670
- */
2671
- renderSessionContext(sessionContext, options = {}) {
2716
+ renderSessionItems(items, options = {}) {
2672
2717
  this.pendingTools.clear();
2673
2718
  const renderedPendingTools = new Map();
2719
+ // Cache-miss notices are not persisted; re-derive them from the full entry
2720
+ // list and re-inject them after the assistant messages that paid for them.
2721
+ const cacheMisses = this.settingsManager.getShowCacheMissNotices()
2722
+ ? collectCacheMisses(this.sessionManager.getEntries(), this.session.modelRegistry)
2723
+ : new Map();
2674
2724
  if (options.updateFooter) {
2675
2725
  this.footer.invalidate();
2676
2726
  this.updateEditorBorderColor();
2677
2727
  }
2678
- for (const message of sessionContext.messages) {
2728
+ for (const item of items) {
2729
+ if (isCustomSessionEntry(item)) {
2730
+ this.addCustomEntryToChat(item);
2731
+ continue;
2732
+ }
2733
+ const message = item;
2679
2734
  // Assistant messages need special handling for tool calls
2680
2735
  if (message.role === "assistant") {
2681
2736
  this.addMessageToChat(message);
@@ -2707,6 +2762,11 @@ export class InteractiveMode {
2707
2762
  }
2708
2763
  }
2709
2764
  }
2765
+ if (message.stopReason !== "aborted" && message.stopReason !== "error") {
2766
+ const miss = cacheMisses.get(message);
2767
+ if (miss)
2768
+ this.addCacheMissNotice(miss);
2769
+ }
2710
2770
  }
2711
2771
  else if (message.role === "toolResult") {
2712
2772
  // Match tool results to pending tool components
@@ -2726,10 +2786,53 @@ export class InteractiveMode {
2726
2786
  }
2727
2787
  this.ui.requestRender();
2728
2788
  }
2789
+ /**
2790
+ * Render session entries to chat. Used for initial load and rebuild after compaction.
2791
+ * @param entries Compaction-aware session entries to render
2792
+ * @param options.updateFooter Update footer state
2793
+ * @param options.populateHistory Add user messages to editor history
2794
+ */
2795
+ renderSessionEntries(entries, options = {}) {
2796
+ const items = entries.flatMap((entry) => {
2797
+ if (entry.type === "custom") {
2798
+ return [entry];
2799
+ }
2800
+ return sessionEntryToContextMessages(entry);
2801
+ });
2802
+ this.renderSessionItems(items, options);
2803
+ }
2804
+ /**
2805
+ * Show a transcript notice when a completed assistant message paid for a
2806
+ * significant cache miss. Only states observable facts: the miss itself,
2807
+ * a model switch, or an idle gap past the cache TTL.
2808
+ */
2809
+ maybeShowCacheMissNotice(message) {
2810
+ if (!this.settingsManager.getShowCacheMissNotices())
2811
+ return;
2812
+ // Entries don't contain `message` yet: message_end fires before persistence.
2813
+ const miss = detectCacheMiss(this.sessionManager.getEntries(), message, this.session.modelRegistry);
2814
+ if (miss)
2815
+ this.addCacheMissNotice(miss);
2816
+ }
2817
+ addCacheMissNotice(miss) {
2818
+ if (miss.missedTokens < 20_000 && miss.missedCost < 0.1)
2819
+ return;
2820
+ const cost = miss.missedCost >= 0.01 ? ` (~$${miss.missedCost.toFixed(2)})` : "";
2821
+ const reBilled = `${formatTokens(miss.missedTokens)} tokens re-billed${cost}`;
2822
+ let label = "Cache miss";
2823
+ if (miss.modelChanged) {
2824
+ label = "Cache miss after model switch";
2825
+ }
2826
+ else if (miss.idleMs >= CACHE_TTL_MS) {
2827
+ label = `Cache miss after ${Math.round(miss.idleMs / 60_000)}m idle`;
2828
+ }
2829
+ const text = theme.fg("warning", `${label}: ${reBilled}`);
2830
+ this.chatContainer.addChild(new Spacer(1));
2831
+ this.chatContainer.addChild(new Text(text, 1, 0));
2832
+ }
2729
2833
  renderInitialMessages() {
2730
- // Get aligned messages and entries from session context
2731
- const context = this.sessionManager.buildSessionContext();
2732
- this.renderSessionContext(context, {
2834
+ const entries = this.sessionManager.buildContextEntries();
2835
+ this.renderSessionEntries(entries, {
2733
2836
  updateFooter: true,
2734
2837
  populateHistory: true,
2735
2838
  });
@@ -2765,8 +2868,7 @@ export class InteractiveMode {
2765
2868
  }
2766
2869
  rebuildChatFromMessages() {
2767
2870
  this.chatContainer.clear();
2768
- const context = this.sessionManager.buildSessionContext();
2769
- this.renderSessionContext(context);
2871
+ this.renderSessionEntries(this.sessionManager.buildContextEntries());
2770
2872
  }
2771
2873
  // =========================================================================
2772
2874
  // Key handlers
@@ -3039,9 +3141,11 @@ export class InteractiveMode {
3039
3141
  if (isExpandable(activeHeader)) {
3040
3142
  activeHeader.setExpanded(expanded);
3041
3143
  }
3042
- for (const child of this.chatContainer.children) {
3043
- if (isExpandable(child)) {
3044
- child.setExpanded(expanded);
3144
+ for (const container of [this.loadedResourcesContainer, this.chatContainer]) {
3145
+ for (const child of container.children) {
3146
+ if (isExpandable(child)) {
3147
+ child.setExpanded(expanded);
3148
+ }
3045
3149
  }
3046
3150
  }
3047
3151
  this.ui.requestRender();
@@ -3061,10 +3165,9 @@ export class InteractiveMode {
3061
3165
  this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`);
3062
3166
  }
3063
3167
  async openExternalEditor() {
3064
- // Determine editor (respect $VISUAL, then $EDITOR)
3065
- const editorCmd = process.env.VISUAL || process.env.EDITOR;
3168
+ const editorCmd = this.settingsManager.getExternalEditorCommand();
3066
3169
  if (!editorCmd) {
3067
- this.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
3170
+ this.showWarning("No editor configured. Set externalEditor in settings.json or $VISUAL/$EDITOR.");
3068
3171
  return;
3069
3172
  }
3070
3173
  const currentText = this.editor.getExpandedText?.() ?? this.editor.getText();
@@ -3367,8 +3470,10 @@ export class InteractiveMode {
3367
3470
  doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
3368
3471
  treeFilterMode: this.settingsManager.getTreeFilterMode(),
3369
3472
  showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
3473
+ showCacheMissNotices: this.settingsManager.getShowCacheMissNotices(),
3370
3474
  defaultProjectTrust: this.settingsManager.getDefaultProjectTrust(),
3371
3475
  editorPaddingX: this.settingsManager.getEditorPaddingX(),
3476
+ outputPad: this.settingsManager.getOutputPad(),
3372
3477
  autocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),
3373
3478
  quietStartup: this.settingsManager.getQuietStartup(),
3374
3479
  clearOnShrink: this.settingsManager.getClearOnShrink(),
@@ -3441,6 +3546,10 @@ export class InteractiveMode {
3441
3546
  this.chatContainer.clear();
3442
3547
  this.rebuildChatFromMessages();
3443
3548
  },
3549
+ onShowCacheMissNoticesChange: (shown) => {
3550
+ this.settingsManager.setShowCacheMissNotices(shown);
3551
+ this.rebuildChatFromMessages();
3552
+ },
3444
3553
  onCollapseChangelogChange: (collapsed) => {
3445
3554
  this.settingsManager.setCollapseChangelog(collapsed);
3446
3555
  },
@@ -3470,6 +3579,23 @@ export class InteractiveMode {
3470
3579
  this.editor.setPaddingX(padding);
3471
3580
  }
3472
3581
  },
3582
+ onOutputPadChange: (padding) => {
3583
+ this.settingsManager.setOutputPad(padding);
3584
+ this.outputPad = padding;
3585
+ if (this.streamingComponent || this.session.isStreaming) {
3586
+ for (const child of this.chatContainer.children) {
3587
+ if (child instanceof AssistantMessageComponent || child instanceof UserMessageComponent) {
3588
+ child.setOutputPad(padding);
3589
+ }
3590
+ }
3591
+ if (this.streamingComponent) {
3592
+ this.streamingComponent.setOutputPad(padding);
3593
+ }
3594
+ this.ui.requestRender();
3595
+ return;
3596
+ }
3597
+ this.rebuildChatFromMessages();
3598
+ },
3473
3599
  onAutocompleteMaxVisibleChange: (maxVisible) => {
3474
3600
  this.settingsManager.setAutocompleteMaxVisible(maxVisible);
3475
3601
  this.defaultEditor.setAutocompleteMaxVisible(maxVisible);
@@ -3480,6 +3606,9 @@ export class InteractiveMode {
3480
3606
  onClearOnShrinkChange: (enabled) => {
3481
3607
  this.settingsManager.setClearOnShrink(enabled);
3482
3608
  this.ui.setClearOnShrink(enabled);
3609
+ if (!enabled && !this.activeStatusIndicator) {
3610
+ this.statusContainer.clear();
3611
+ }
3483
3612
  },
3484
3613
  onShowTerminalProgressChange: (enabled) => {
3485
3614
  this.settingsManager.setShowTerminalProgress(enabled);
@@ -3710,19 +3839,17 @@ export class InteractiveMode {
3710
3839
  const initialSelectedId = userMessages[userMessages.length - 1]?.entryId;
3711
3840
  this.showSelector((done) => {
3712
3841
  const selector = new UserMessageSelectorComponent(userMessages.map((m) => ({ id: m.entryId, text: m.text })), async (entryId) => {
3842
+ done();
3713
3843
  try {
3714
3844
  const result = await this.runtimeHost.fork(entryId);
3715
3845
  if (result.cancelled) {
3716
- done();
3717
3846
  this.ui.requestRender();
3718
3847
  return;
3719
3848
  }
3720
3849
  this.editor.setText(result.selectedText ?? "");
3721
- done();
3722
3850
  this.showStatus("Forked to new session");
3723
3851
  }
3724
3852
  catch (error) {
3725
- done();
3726
3853
  this.showError(error instanceof Error ? error.message : String(error));
3727
3854
  }
3728
3855
  }, () => {
@@ -3797,16 +3924,16 @@ export class InteractiveMode {
3797
3924
  break;
3798
3925
  }
3799
3926
  }
3800
- // Set up escape handler and loader if summarizing
3801
- let summaryLoader;
3927
+ // Set up escape handler and status indicator if summarizing
3928
+ let showingSummaryIndicator = false;
3802
3929
  const originalOnEscape = this.defaultEditor.onEscape;
3803
3930
  if (wantsSummary) {
3804
3931
  this.defaultEditor.onEscape = () => {
3805
3932
  this.session.abortBranchSummary();
3806
3933
  };
3807
3934
  this.chatContainer.addChild(new Spacer(1));
3808
- summaryLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `Summarizing branch... (${keyText("app.interrupt")} to cancel)`);
3809
- this.statusContainer.addChild(summaryLoader);
3935
+ this.showStatusIndicator(new BranchSummaryStatusIndicator(this.ui));
3936
+ showingSummaryIndicator = true;
3810
3937
  this.ui.requestRender();
3811
3938
  }
3812
3939
  try {
@@ -3837,9 +3964,8 @@ export class InteractiveMode {
3837
3964
  this.showError(error instanceof Error ? error.message : String(error));
3838
3965
  }
3839
3966
  finally {
3840
- if (summaryLoader) {
3841
- summaryLoader.stop();
3842
- this.statusContainer.clear();
3967
+ if (showingSummaryIndicator) {
3968
+ this.clearStatusIndicator("branchSummary");
3843
3969
  }
3844
3970
  this.defaultEditor.onEscape = originalOnEscape;
3845
3971
  }
@@ -3880,11 +4006,7 @@ export class InteractiveMode {
3880
4006
  });
3881
4007
  }
3882
4008
  async handleResumeSession(sessionPath, options) {
3883
- if (this.loadingAnimation) {
3884
- this.loadingAnimation.stop();
3885
- this.loadingAnimation = undefined;
3886
- }
3887
- this.statusContainer.clear();
4009
+ this.clearStatusIndicator();
3888
4010
  try {
3889
4011
  const result = await this.runtimeHost.switchSession(sessionPath, {
3890
4012
  withSession: options?.withSession,
@@ -3956,13 +4078,82 @@ export class InteractiveMode {
3956
4078
  }
3957
4079
  return options.sort((a, b) => a.name.localeCompare(b.name));
3958
4080
  }
3959
- showLoginAuthTypeSelector() {
4081
+ findLoginProviderOptions(providerRef) {
4082
+ const normalizedProviderRef = providerRef.trim().toLowerCase();
4083
+ if (!normalizedProviderRef) {
4084
+ return [];
4085
+ }
4086
+ return this.getLoginProviderOptions().filter((provider) => provider.id.toLowerCase() === normalizedProviderRef ||
4087
+ provider.name.toLowerCase() === normalizedProviderRef);
4088
+ }
4089
+ async handleLoginCommand(providerRef) {
4090
+ if (!providerRef) {
4091
+ this.showLoginAuthTypeSelector();
4092
+ return;
4093
+ }
4094
+ const providerOptions = this.findLoginProviderOptions(providerRef);
4095
+ if (providerOptions.length === 1) {
4096
+ await this.startProviderLogin(providerOptions[0]);
4097
+ return;
4098
+ }
4099
+ if (providerOptions.length > 1) {
4100
+ const providerIds = new Set(providerOptions.map((provider) => provider.id));
4101
+ if (providerIds.size === 1) {
4102
+ this.showLoginAuthTypeSelector(providerOptions);
4103
+ return;
4104
+ }
4105
+ }
4106
+ this.showLoginProviderSelector(undefined, providerRef);
4107
+ }
4108
+ async startProviderLogin(providerOption) {
4109
+ if (providerOption.authType === "oauth") {
4110
+ await this.showLoginDialog(providerOption.id, providerOption.name);
4111
+ }
4112
+ else if (providerOption.id === BEDROCK_PROVIDER_ID) {
4113
+ this.showBedrockSetupDialog(providerOption.id, providerOption.name);
4114
+ }
4115
+ else {
4116
+ await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
4117
+ }
4118
+ }
4119
+ showLoginAuthTypeSelector(providerOptions) {
3960
4120
  const subscriptionLabel = "Use a subscription";
3961
4121
  const apiKeyLabel = "Use an API key";
4122
+ const availableAuthTypes = providerOptions
4123
+ ? new Set(providerOptions.map((provider) => provider.authType))
4124
+ : new Set(["oauth", "api_key"]);
4125
+ const options = [];
4126
+ if (availableAuthTypes.has("oauth")) {
4127
+ options.push(subscriptionLabel);
4128
+ }
4129
+ if (availableAuthTypes.has("api_key")) {
4130
+ options.push(apiKeyLabel);
4131
+ }
4132
+ if (options.length === 0) {
4133
+ this.showStatus("No login methods available.");
4134
+ return;
4135
+ }
4136
+ if (providerOptions && options.length === 1) {
4137
+ const providerOption = providerOptions[0];
4138
+ if (providerOption) {
4139
+ void this.startProviderLogin(providerOption);
4140
+ }
4141
+ return;
4142
+ }
4143
+ const title = providerOptions?.[0]
4144
+ ? `Select authentication method for ${providerOptions[0].name}:`
4145
+ : "Select authentication method:";
3962
4146
  this.showSelector((done) => {
3963
- const selector = new ExtensionSelectorComponent("Select authentication method:", [subscriptionLabel, apiKeyLabel], (option) => {
4147
+ const selector = new ExtensionSelectorComponent(title, options, (option) => {
3964
4148
  done();
3965
4149
  const authType = option === subscriptionLabel ? "oauth" : "api_key";
4150
+ if (providerOptions) {
4151
+ const providerOption = providerOptions.find((provider) => provider.authType === authType);
4152
+ if (providerOption) {
4153
+ void this.startProviderLogin(providerOption);
4154
+ }
4155
+ return;
4156
+ }
3966
4157
  this.showLoginProviderSelector(authType);
3967
4158
  }, () => {
3968
4159
  done();
@@ -3971,32 +4162,34 @@ export class InteractiveMode {
3971
4162
  return { component: selector, focus: selector };
3972
4163
  });
3973
4164
  }
3974
- showLoginProviderSelector(authType) {
4165
+ showLoginProviderSelector(authType, initialSearchInput) {
3975
4166
  const providerOptions = this.getLoginProviderOptions(authType);
3976
4167
  if (providerOptions.length === 0) {
3977
- this.showStatus(authType === "oauth" ? "No subscription providers available." : "No API key providers available.");
4168
+ const message = authType === "oauth"
4169
+ ? "No subscription providers available."
4170
+ : authType === "api_key"
4171
+ ? "No API key providers available."
4172
+ : "No login providers available.";
4173
+ this.showStatus(message);
3978
4174
  return;
3979
4175
  }
3980
4176
  this.showSelector((done) => {
3981
- const selector = new OAuthSelectorComponent("login", this.session.modelRegistry.authStorage, providerOptions, async (providerId) => {
4177
+ const selector = new OAuthSelectorComponent("login", this.session.modelRegistry.authStorage, providerOptions, async (providerId, selectedAuthType) => {
3982
4178
  done();
3983
- const providerOption = providerOptions.find((provider) => provider.id === providerId);
4179
+ const providerOption = providerOptions.find((provider) => provider.id === providerId && provider.authType === selectedAuthType);
3984
4180
  if (!providerOption) {
3985
4181
  return;
3986
4182
  }
3987
- if (providerOption.authType === "oauth") {
3988
- await this.showLoginDialog(providerOption.id, providerOption.name);
3989
- }
3990
- else if (providerOption.id === BEDROCK_PROVIDER_ID) {
3991
- this.showBedrockSetupDialog(providerOption.id, providerOption.name);
4183
+ await this.startProviderLogin(providerOption);
4184
+ }, () => {
4185
+ done();
4186
+ if (authType) {
4187
+ this.showLoginAuthTypeSelector();
3992
4188
  }
3993
4189
  else {
3994
- await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
4190
+ this.ui.requestRender();
3995
4191
  }
3996
- }, () => {
3997
- done();
3998
- this.showLoginAuthTypeSelector();
3999
- }, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId));
4192
+ }, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId), initialSearchInput);
4000
4193
  return { component: selector, focus: selector };
4001
4194
  });
4002
4195
  }
@@ -4255,7 +4448,7 @@ export class InteractiveMode {
4255
4448
  const borderColor = (s) => theme.fg("border", s);
4256
4449
  reloadBox.addChild(new DynamicBorder(borderColor));
4257
4450
  reloadBox.addChild(new Spacer(1));
4258
- reloadBox.addChild(new Text(theme.fg("muted", "Reloading keybindings, extensions, skills, prompts, themes..."), 1, 0));
4451
+ reloadBox.addChild(new Text(theme.fg("muted", "Reloading keybindings, extensions, skills, prompts, themes, and context files..."), 1, 0));
4259
4452
  reloadBox.addChild(new Spacer(1));
4260
4453
  reloadBox.addChild(new DynamicBorder(borderColor));
4261
4454
  const previousEditor = this.editor;
@@ -4277,6 +4470,7 @@ export class InteractiveMode {
4277
4470
  return;
4278
4471
  }
4279
4472
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
4473
+ this.outputPad = this.settingsManager.getOutputPad();
4280
4474
  this.rebuildChatFromMessages();
4281
4475
  chatRestoredBeforeSessionStart = true;
4282
4476
  };
@@ -4300,7 +4494,11 @@ export class InteractiveMode {
4300
4494
  this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
4301
4495
  }
4302
4496
  this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
4303
- this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
4497
+ const clearOnShrink = this.settingsManager.getClearOnShrink();
4498
+ this.ui.setClearOnShrink(clearOnShrink);
4499
+ if (!clearOnShrink && !this.activeStatusIndicator) {
4500
+ this.statusContainer.clear();
4501
+ }
4304
4502
  this.setupAutocompleteProvider();
4305
4503
  const runner = this.session.extensionRunner;
4306
4504
  this.setupExtensionShortcuts(runner);
@@ -4314,8 +4512,8 @@ export class InteractiveMode {
4314
4512
  this.showError(`models.json error: ${modelsJsonError}`);
4315
4513
  }
4316
4514
  this.showStatus(savedImplicitProjectTrust
4317
- ? "Reloaded keybindings, extensions, skills, prompts, themes; saved project trust"
4318
- : "Reloaded keybindings, extensions, skills, prompts, themes");
4515
+ ? "Reloaded keybindings, extensions, skills, prompts, themes, and context files; saved project trust"
4516
+ : "Reloaded keybindings, extensions, skills, prompts, themes, and context files");
4319
4517
  dismissReloadBox(this.editor);
4320
4518
  reloadBoxDismissed = true;
4321
4519
  }
@@ -4379,11 +4577,7 @@ export class InteractiveMode {
4379
4577
  return;
4380
4578
  }
4381
4579
  try {
4382
- if (this.loadingAnimation) {
4383
- this.loadingAnimation.stop();
4384
- this.loadingAnimation = undefined;
4385
- }
4386
- this.statusContainer.clear();
4580
+ this.clearStatusIndicator();
4387
4581
  const result = await this.runtimeHost.importFromJsonl(inputPath);
4388
4582
  if (result.cancelled) {
4389
4583
  this.showStatus("Import cancelled");
@@ -4540,6 +4734,26 @@ export class InteractiveMode {
4540
4734
  handleSessionCommand() {
4541
4735
  const stats = this.session.getSessionStats();
4542
4736
  const sessionName = this.sessionManager.getSessionName();
4737
+ const entries = this.sessionManager.getEntries();
4738
+ const cacheWaste = computeCacheWaste(entries, this.session.modelRegistry);
4739
+ // Cost/token totals per provider/model actually used (e.g. OpenRouter `auto`
4740
+ // resolves to a concrete responseModel), sorted by cost descending.
4741
+ const perModelMap = new Map();
4742
+ for (const entry of entries) {
4743
+ if (entry.type !== "message" || entry.message.role !== "assistant")
4744
+ continue;
4745
+ const message = entry.message;
4746
+ const usage = message.usage;
4747
+ const key = `${message.provider}/${message.responseModel ?? message.model}`;
4748
+ let bucket = perModelMap.get(key);
4749
+ if (!bucket) {
4750
+ bucket = { key, cost: 0, tokens: 0 };
4751
+ perModelMap.set(key, bucket);
4752
+ }
4753
+ bucket.cost += usage.cost.total;
4754
+ bucket.tokens += usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
4755
+ }
4756
+ const perModel = Array.from(perModelMap.values()).sort((a, b) => b.cost - a.cost);
4543
4757
  let info = `${theme.bold("Session Info")}\n\n`;
4544
4758
  if (sessionName) {
4545
4759
  info += `${theme.fg("dim", "Name:")} ${sessionName}\n`;
@@ -4547,24 +4761,42 @@ export class InteractiveMode {
4547
4761
  info += `${theme.fg("dim", "File:")} ${stats.sessionFile ?? "In-memory"}\n`;
4548
4762
  info += `${theme.fg("dim", "ID:")} ${stats.sessionId}\n\n`;
4549
4763
  info += `${theme.bold("Messages")}\n`;
4764
+ info += `${theme.fg("dim", "Total:")} ${stats.totalMessages}\n`;
4550
4765
  info += `${theme.fg("dim", "User:")} ${stats.userMessages}\n`;
4551
4766
  info += `${theme.fg("dim", "Assistant:")} ${stats.assistantMessages}\n`;
4552
- info += `${theme.fg("dim", "Tool Calls:")} ${stats.toolCalls}\n`;
4553
- info += `${theme.fg("dim", "Tool Results:")} ${stats.toolResults}\n`;
4554
- info += `${theme.fg("dim", "Total:")} ${stats.totalMessages}\n\n`;
4767
+ info += `${theme.fg("dim", "Tools:")} ${stats.toolCalls} calls, ${stats.toolResults} results\n\n`;
4555
4768
  info += `${theme.bold("Tokens")}\n`;
4556
- info += `${theme.fg("dim", "Input:")} ${stats.tokens.input.toLocaleString()}\n`;
4557
- info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`;
4558
- if (stats.tokens.cacheRead > 0) {
4559
- info += `${theme.fg("dim", "Cache Read:")} ${stats.tokens.cacheRead.toLocaleString()}\n`;
4560
- }
4561
- if (stats.tokens.cacheWrite > 0) {
4562
- info += `${theme.fg("dim", "Cache Write:")} ${stats.tokens.cacheWrite.toLocaleString()}\n`;
4769
+ // "Input" is the full prompt volume. With cache activity, split it into
4770
+ // cached (served from cache) vs uncached (everything else) - the only
4771
+ // provider-independent split. Cache writes, where reported, are a detail
4772
+ // of the uncached portion.
4773
+ const { input, cacheRead, cacheWrite } = stats.tokens;
4774
+ const promptTokens = input + cacheRead + cacheWrite;
4775
+ info += `${theme.fg("dim", "Input:")} ${promptTokens.toLocaleString()}\n`;
4776
+ if (promptTokens > 0 && (cacheRead > 0 || cacheWrite > 0)) {
4777
+ const hitRate = theme.fg("dim", `(${((cacheRead / promptTokens) * 100).toFixed(1)}%)`);
4778
+ info += ` ${theme.fg("dim", "Cached:")} ${cacheRead.toLocaleString()} ${hitRate}\n`;
4779
+ const written = cacheWrite > 0 ? ` ${theme.fg("dim", `(${cacheWrite.toLocaleString()} written to cache)`)}` : "";
4780
+ info += ` ${theme.fg("dim", "Uncached:")} ${(input + cacheWrite).toLocaleString()}${written}\n`;
4563
4781
  }
4782
+ info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`;
4564
4783
  info += `${theme.fg("dim", "Total:")} ${stats.tokens.total.toLocaleString()}\n`;
4565
- if (stats.cost > 0) {
4784
+ if (stats.cost > 0 || cacheWaste.missedTokens > 0) {
4566
4785
  info += `\n${theme.bold("Cost")}\n`;
4567
- info += `${theme.fg("dim", "Total:")} ${stats.cost.toFixed(4)}`;
4786
+ info += `${theme.fg("dim", "Total:")} $${stats.cost.toFixed(3)}`;
4787
+ if (perModel.length > 1) {
4788
+ for (const entry of perModel) {
4789
+ info += `\n ${theme.fg("dim", `${entry.key}:`)} $${entry.cost.toFixed(3)} ${theme.fg("dim", `(${formatTokens(entry.tokens)} tokens)`)}`;
4790
+ }
4791
+ }
4792
+ if (cacheWaste.missedTokens > 0) {
4793
+ const missLabel = cacheWaste.missCount === 1 ? "1 miss" : `${cacheWaste.missCount} misses`;
4794
+ const detail = `${cacheWaste.missedTokens.toLocaleString()} tokens, ${missLabel}`;
4795
+ info +=
4796
+ cacheWaste.missedCost >= 0.0001
4797
+ ? `\n${theme.fg("dim", "Cache Re-billed:")} $${cacheWaste.missedCost.toFixed(3)} ${theme.fg("dim", `(${detail})`)}`
4798
+ : `\n${theme.fg("dim", "Cache Re-billed:")} ${detail}`;
4799
+ }
4568
4800
  }
4569
4801
  this.chatContainer.addChild(new Spacer(1));
4570
4802
  this.chatContainer.addChild(new Text(info, 1, 0));
@@ -4709,11 +4941,7 @@ export class InteractiveMode {
4709
4941
  this.ui.requestRender();
4710
4942
  }
4711
4943
  async handleClearCommand() {
4712
- if (this.loadingAnimation) {
4713
- this.loadingAnimation.stop();
4714
- this.loadingAnimation = undefined;
4715
- }
4716
- this.statusContainer.clear();
4944
+ this.clearStatusIndicator();
4717
4945
  try {
4718
4946
  const result = await this.runtimeHost.newSession();
4719
4947
  if (result.cancelled) {
@@ -4840,11 +5068,7 @@ export class InteractiveMode {
4840
5068
  this.ui.requestRender();
4841
5069
  }
4842
5070
  async handleCompactCommand(customInstructions) {
4843
- if (this.loadingAnimation) {
4844
- this.loadingAnimation.stop();
4845
- this.loadingAnimation = undefined;
4846
- }
4847
- this.statusContainer.clear();
5071
+ this.clearStatusIndicator();
4848
5072
  try {
4849
5073
  await this.session.compact(customInstructions);
4850
5074
  }
@@ -4856,10 +5080,7 @@ export class InteractiveMode {
4856
5080
  if (this.settingsManager.getShowTerminalProgress()) {
4857
5081
  this.ui.terminal.setProgress(false);
4858
5082
  }
4859
- if (this.loadingAnimation) {
4860
- this.loadingAnimation.stop();
4861
- this.loadingAnimation = undefined;
4862
- }
5083
+ this.clearStatusIndicator();
4863
5084
  this.themeController.disableAutoSync();
4864
5085
  this.clearExtensionTerminalInputListeners();
4865
5086
  this.footer.dispose();