@earendil-works/pi-coding-agent 0.80.2 → 0.80.5

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 (211) hide show
  1. package/CHANGELOG.md +115 -1
  2. package/README.md +3 -2
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +2 -2
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/cli/config-selector.d.ts +4 -2
  7. package/dist/cli/config-selector.d.ts.map +1 -1
  8. package/dist/cli/config-selector.js +1 -1
  9. package/dist/cli/config-selector.js.map +1 -1
  10. package/dist/cli/file-processor.d.ts.map +1 -1
  11. package/dist/cli/file-processor.js +13 -25
  12. package/dist/cli/file-processor.js.map +1 -1
  13. package/dist/core/agent-session.d.ts +22 -5
  14. package/dist/core/agent-session.d.ts.map +1 -1
  15. package/dist/core/agent-session.js +133 -50
  16. package/dist/core/agent-session.js.map +1 -1
  17. package/dist/core/auth-storage.d.ts.map +1 -1
  18. package/dist/core/auth-storage.js +13 -5
  19. package/dist/core/auth-storage.js.map +1 -1
  20. package/dist/core/cache-stats.d.ts +49 -0
  21. package/dist/core/cache-stats.d.ts.map +1 -0
  22. package/dist/core/cache-stats.js +101 -0
  23. package/dist/core/cache-stats.js.map +1 -0
  24. package/dist/core/compaction/compaction.d.ts +1 -2
  25. package/dist/core/compaction/compaction.d.ts.map +1 -1
  26. package/dist/core/compaction/compaction.js +56 -81
  27. package/dist/core/compaction/compaction.js.map +1 -1
  28. package/dist/core/extensions/index.d.ts +1 -1
  29. package/dist/core/extensions/index.d.ts.map +1 -1
  30. package/dist/core/extensions/index.js.map +1 -1
  31. package/dist/core/extensions/loader.d.ts.map +1 -1
  32. package/dist/core/extensions/loader.js +10 -0
  33. package/dist/core/extensions/loader.js.map +1 -1
  34. package/dist/core/extensions/runner.d.ts +5 -3
  35. package/dist/core/extensions/runner.d.ts.map +1 -1
  36. package/dist/core/extensions/runner.js +38 -0
  37. package/dist/core/extensions/runner.js.map +1 -1
  38. package/dist/core/extensions/types.d.ts +39 -5
  39. package/dist/core/extensions/types.d.ts.map +1 -1
  40. package/dist/core/extensions/types.js.map +1 -1
  41. package/dist/core/http-dispatcher.d.ts.map +1 -1
  42. package/dist/core/http-dispatcher.js +28 -1
  43. package/dist/core/http-dispatcher.js.map +1 -1
  44. package/dist/core/index.d.ts +1 -1
  45. package/dist/core/index.d.ts.map +1 -1
  46. package/dist/core/index.js.map +1 -1
  47. package/dist/core/keybindings.d.ts.map +1 -1
  48. package/dist/core/keybindings.js +3 -6
  49. package/dist/core/keybindings.js.map +1 -1
  50. package/dist/core/model-registry.d.ts +3 -0
  51. package/dist/core/model-registry.d.ts.map +1 -1
  52. package/dist/core/model-registry.js +16 -2
  53. package/dist/core/model-registry.js.map +1 -1
  54. package/dist/core/model-resolver.d.ts +10 -0
  55. package/dist/core/model-resolver.d.ts.map +1 -1
  56. package/dist/core/model-resolver.js +15 -18
  57. package/dist/core/model-resolver.js.map +1 -1
  58. package/dist/core/package-manager.d.ts +4 -1
  59. package/dist/core/package-manager.d.ts.map +1 -1
  60. package/dist/core/package-manager.js +68 -26
  61. package/dist/core/package-manager.js.map +1 -1
  62. package/dist/core/provider-attribution.d.ts.map +1 -1
  63. package/dist/core/provider-attribution.js +0 -10
  64. package/dist/core/provider-attribution.js.map +1 -1
  65. package/dist/core/resource-loader.d.ts +2 -2
  66. package/dist/core/resource-loader.d.ts.map +1 -1
  67. package/dist/core/resource-loader.js +8 -7
  68. package/dist/core/resource-loader.js.map +1 -1
  69. package/dist/core/sdk.d.ts +1 -1
  70. package/dist/core/sdk.d.ts.map +1 -1
  71. package/dist/core/sdk.js +8 -1
  72. package/dist/core/sdk.js.map +1 -1
  73. package/dist/core/session-manager.d.ts +21 -2
  74. package/dist/core/session-manager.d.ts.map +1 -1
  75. package/dist/core/session-manager.js +104 -72
  76. package/dist/core/session-manager.js.map +1 -1
  77. package/dist/core/settings-manager.d.ts +10 -0
  78. package/dist/core/settings-manager.d.ts.map +1 -1
  79. package/dist/core/settings-manager.js +27 -0
  80. package/dist/core/settings-manager.js.map +1 -1
  81. package/dist/core/slash-commands.d.ts +1 -0
  82. package/dist/core/slash-commands.d.ts.map +1 -1
  83. package/dist/core/slash-commands.js +3 -3
  84. package/dist/core/slash-commands.js.map +1 -1
  85. package/dist/core/timings.d.ts +4 -2
  86. package/dist/core/timings.d.ts.map +1 -1
  87. package/dist/core/timings.js +24 -14
  88. package/dist/core/timings.js.map +1 -1
  89. package/dist/core/tools/bash.d.ts.map +1 -1
  90. package/dist/core/tools/bash.js +20 -5
  91. package/dist/core/tools/bash.js.map +1 -1
  92. package/dist/core/tools/edit.d.ts.map +1 -1
  93. package/dist/core/tools/edit.js +2 -2
  94. package/dist/core/tools/edit.js.map +1 -1
  95. package/dist/core/tools/read.d.ts.map +1 -1
  96. package/dist/core/tools/read.js +12 -25
  97. package/dist/core/tools/read.js.map +1 -1
  98. package/dist/index.d.ts +3 -2
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +2 -1
  101. package/dist/index.js.map +1 -1
  102. package/dist/main.d.ts +2 -2
  103. package/dist/main.d.ts.map +1 -1
  104. package/dist/main.js +17 -4
  105. package/dist/main.js.map +1 -1
  106. package/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  107. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  108. package/dist/modes/interactive/components/assistant-message.js +23 -15
  109. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  110. package/dist/modes/interactive/components/config-selector.d.ts +34 -3
  111. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  112. package/dist/modes/interactive/components/config-selector.js +280 -25
  113. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  114. package/dist/modes/interactive/components/custom-entry.d.ts +19 -0
  115. package/dist/modes/interactive/components/custom-entry.d.ts.map +1 -0
  116. package/dist/modes/interactive/components/custom-entry.js +52 -0
  117. package/dist/modes/interactive/components/custom-entry.js.map +1 -0
  118. package/dist/modes/interactive/components/extension-editor.d.ts +3 -1
  119. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  120. package/dist/modes/interactive/components/extension-editor.js +12 -3
  121. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  122. package/dist/modes/interactive/components/footer.d.ts +4 -0
  123. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  124. package/dist/modes/interactive/components/footer.js +1 -1
  125. package/dist/modes/interactive/components/footer.js.map +1 -1
  126. package/dist/modes/interactive/components/oauth-selector.d.ts +3 -1
  127. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  128. package/dist/modes/interactive/components/oauth-selector.js +17 -6
  129. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  130. package/dist/modes/interactive/components/settings-selector.d.ts +4 -0
  131. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  132. package/dist/modes/interactive/components/settings-selector.js +23 -1
  133. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  134. package/dist/modes/interactive/components/status-indicator.d.ts +28 -0
  135. package/dist/modes/interactive/components/status-indicator.d.ts.map +1 -0
  136. package/dist/modes/interactive/components/status-indicator.js +60 -0
  137. package/dist/modes/interactive/components/status-indicator.js.map +1 -0
  138. package/dist/modes/interactive/components/user-message.d.ts +6 -2
  139. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  140. package/dist/modes/interactive/components/user-message.js +19 -6
  141. package/dist/modes/interactive/components/user-message.js.map +1 -1
  142. package/dist/modes/interactive/interactive-mode.d.ts +21 -10
  143. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  144. package/dist/modes/interactive/interactive-mode.js +417 -196
  145. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  146. package/dist/modes/print-mode.d.ts.map +1 -1
  147. package/dist/modes/print-mode.js +1 -1
  148. package/dist/modes/print-mode.js.map +1 -1
  149. package/dist/modes/rpc/rpc-client.d.ts +21 -6
  150. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  151. package/dist/modes/rpc/rpc-client.js +17 -3
  152. package/dist/modes/rpc/rpc-client.js.map +1 -1
  153. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  154. package/dist/modes/rpc/rpc-mode.js +20 -1
  155. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  156. package/dist/modes/rpc/rpc-types.d.ts +26 -0
  157. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  158. package/dist/modes/rpc/rpc-types.js.map +1 -1
  159. package/dist/package-manager-cli.d.ts +2 -2
  160. package/dist/package-manager-cli.d.ts.map +1 -1
  161. package/dist/package-manager-cli.js +71 -17
  162. package/dist/package-manager-cli.js.map +1 -1
  163. package/dist/rpc-entry.d.ts +3 -0
  164. package/dist/rpc-entry.d.ts.map +1 -0
  165. package/dist/rpc-entry.js +10 -0
  166. package/dist/rpc-entry.js.map +1 -0
  167. package/dist/utils/clipboard-image.d.ts.map +1 -1
  168. package/dist/utils/clipboard-image.js +1 -1
  169. package/dist/utils/clipboard-image.js.map +1 -1
  170. package/dist/utils/image-convert.d.ts +1 -0
  171. package/dist/utils/image-convert.d.ts.map +1 -1
  172. package/dist/utils/image-convert.js +21 -15
  173. package/dist/utils/image-convert.js.map +1 -1
  174. package/dist/utils/image-process.d.ts +18 -0
  175. package/dist/utils/image-process.d.ts.map +1 -0
  176. package/dist/utils/image-process.js +83 -0
  177. package/dist/utils/image-process.js.map +1 -0
  178. package/dist/utils/mime.d.ts.map +1 -1
  179. package/dist/utils/mime.js +41 -0
  180. package/dist/utils/mime.js.map +1 -1
  181. package/docs/extensions.md +88 -17
  182. package/docs/keybindings.md +1 -1
  183. package/docs/models.md +4 -4
  184. package/docs/packages.md +2 -2
  185. package/docs/providers.md +2 -1
  186. package/docs/rpc.md +71 -3
  187. package/docs/sdk.md +50 -0
  188. package/docs/session-format.md +18 -8
  189. package/docs/settings.md +11 -0
  190. package/docs/usage.md +3 -3
  191. package/examples/extensions/README.md +1 -0
  192. package/examples/extensions/custom-compaction.ts +1 -0
  193. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  194. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  195. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  196. package/examples/extensions/entry-renderer.ts +41 -0
  197. package/examples/extensions/gondolin/package-lock.json +2 -2
  198. package/examples/extensions/gondolin/package.json +1 -1
  199. package/examples/extensions/handoff.ts +1 -1
  200. package/examples/extensions/qna.ts +1 -1
  201. package/examples/extensions/question.ts +1 -0
  202. package/examples/extensions/reload-runtime.ts +2 -2
  203. package/examples/extensions/sandbox/package-lock.json +2 -2
  204. package/examples/extensions/sandbox/package.json +1 -1
  205. package/examples/extensions/summarize.ts +1 -0
  206. package/examples/extensions/with-deps/package-lock.json +2 -2
  207. package/examples/extensions/with-deps/package.json +1 -1
  208. package/examples/sdk/12-full-control.ts +1 -1
  209. package/examples/sdk/README.md +1 -1
  210. package/npm-shrinkwrap.json +12 -12
  211. package/package.json +8 -5
@@ -7,12 +7,13 @@ import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
9
  import { getProviders, } from "@earendil-works/pi-ai/compat";
10
- import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@earendil-works/pi-tui";
10
+ import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@earendil-works/pi-tui";
11
11
  import chalk from "chalk";
12
12
  import { spawn, spawnSync } from "child_process";
13
13
  import { APP_NAME, APP_TITLE, CONFIG_DIR_NAME, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, VERSION, } from "../../config.js";
14
14
  import { parseSkillBlock } from "../../core/agent-session.js";
15
15
  import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
16
+ import { CACHE_TTL_MS, collectCacheMisses, computeCacheWaste, detectCacheMiss, } from "../../core/cache-stats.js";
16
17
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
17
18
  import { configureHttpDispatcher, formatHttpIdleTimeoutMs } from "../../core/http-dispatcher.js";
18
19
  import { KeybindingsManager } from "../../core/keybindings.js";
@@ -21,7 +22,7 @@ import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScop
21
22
  import { DefaultPackageManager } from "../../core/package-manager.js";
22
23
  import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.js";
23
24
  import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
24
- import { SessionManager } from "../../core/session-manager.js";
25
+ import { SessionManager, sessionEntryToContextMessages } from "../../core/session-manager.js";
25
26
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
26
27
  import { isInstallTelemetryEnabled } from "../../core/telemetry.js";
27
28
  import { hasTrustRequiringProjectResources, ProjectTrustStore } from "../../core/trust-manager.js";
@@ -40,8 +41,8 @@ import { BashExecutionComponent } from "./components/bash-execution.js";
40
41
  import { BorderedLoader } from "./components/bordered-loader.js";
41
42
  import { BranchSummaryMessageComponent } from "./components/branch-summary-message.js";
42
43
  import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message.js";
43
- import { CountdownTimer } from "./components/countdown-timer.js";
44
44
  import { CustomEditor } from "./components/custom-editor.js";
45
+ import { CustomEntryComponent } from "./components/custom-entry.js";
45
46
  import { CustomMessageComponent } from "./components/custom-message.js";
46
47
  import { DaxnutsComponent } from "./components/daxnuts.js";
47
48
  import { DynamicBorder } from "./components/dynamic-border.js";
@@ -49,15 +50,16 @@ import { EarendilAnnouncementComponent } from "./components/earendil-announcemen
49
50
  import { ExtensionEditorComponent } from "./components/extension-editor.js";
50
51
  import { ExtensionInputComponent } from "./components/extension-input.js";
51
52
  import { ExtensionSelectorComponent } from "./components/extension-selector.js";
52
- import { FooterComponent } from "./components/footer.js";
53
+ import { FooterComponent, formatTokens } from "./components/footer.js";
53
54
  import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
54
55
  import { LoginDialogComponent } from "./components/login-dialog.js";
55
56
  import { ModelSelectorComponent } from "./components/model-selector.js";
56
- import { OAuthSelectorComponent } from "./components/oauth-selector.js";
57
+ import { formatAuthSelectorProviderType, OAuthSelectorComponent, } from "./components/oauth-selector.js";
57
58
  import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js";
58
59
  import { SessionSelectorComponent } from "./components/session-selector.js";
59
60
  import { SettingsSelectorComponent } from "./components/settings-selector.js";
60
61
  import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.js";
62
+ import { BranchSummaryStatusIndicator, CompactionStatusIndicator, IdleStatus, RetryStatusIndicator, WorkingStatusIndicator, } from "./components/status-indicator.js";
61
63
  import { ToolExecutionComponent } from "./components/tool-execution.js";
62
64
  import { TreeSelectorComponent } from "./components/tree-selector.js";
63
65
  import { TrustSelectorComponent } from "./components/trust-selector.js";
@@ -81,6 +83,9 @@ class ExpandableText extends Text {
81
83
  this.setText(expanded ? this.getExpandedText() : this.getCollapsedText());
82
84
  }
83
85
  }
86
+ function isCustomSessionEntry(item) {
87
+ return "type" in item && item.type === "custom";
88
+ }
84
89
  const DEAD_TERMINAL_ERROR_CODES = new Set(["EIO", "EPIPE", "ENOTCONN"]);
85
90
  function isDeadTerminalError(error) {
86
91
  if (!error || typeof error !== "object" || !("code" in error)) {
@@ -131,9 +136,46 @@ export function isApiKeyLoginProvider(providerId, oauthProviderIds, builtInProvi
131
136
  }
132
137
  return !oauthProviderIds.has(providerId);
133
138
  }
139
+ const AUTH_TYPE_ORDER = { oauth: 0, api_key: 1 };
140
+ function createFuzzyAutocompleteItems(items, prefix, getSearchText, toAutocompleteItem) {
141
+ const filtered = fuzzyFilter(items, prefix, getSearchText);
142
+ if (filtered.length === 0)
143
+ return null;
144
+ return filtered.map(toAutocompleteItem);
145
+ }
146
+ function getLoginProviderCompletionOptions(providerOptions) {
147
+ const byId = new Map();
148
+ for (const provider of providerOptions) {
149
+ const existing = byId.get(provider.id);
150
+ if (existing) {
151
+ if (!existing.authTypes.includes(provider.authType)) {
152
+ existing.authTypes.push(provider.authType);
153
+ existing.authTypes.sort((a, b) => AUTH_TYPE_ORDER[a] - AUTH_TYPE_ORDER[b]);
154
+ }
155
+ continue;
156
+ }
157
+ byId.set(provider.id, {
158
+ id: provider.id,
159
+ name: provider.name,
160
+ authTypes: [provider.authType],
161
+ });
162
+ }
163
+ return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name));
164
+ }
165
+ function getLoginProviderSearchText(provider) {
166
+ const authTypes = provider.authTypes
167
+ .map((authType) => `${authType} ${formatAuthSelectorProviderType(authType)}`)
168
+ .join(" ");
169
+ return `${provider.id} ${provider.name} ${authTypes}`;
170
+ }
171
+ function formatLoginProviderCompletionDescription(provider) {
172
+ const authTypes = provider.authTypes.map(formatAuthSelectorProviderType).join("/");
173
+ return provider.name === provider.id ? authTypes : `${provider.name} · ${authTypes}`;
174
+ }
134
175
  export class InteractiveMode {
135
176
  runtimeHost;
136
177
  ui;
178
+ loadedResourcesContainer;
137
179
  chatContainer;
138
180
  pendingMessagesContainer;
139
181
  statusContainer;
@@ -152,7 +194,8 @@ export class InteractiveMode {
152
194
  isInitialized = false;
153
195
  onInputCallback;
154
196
  pendingUserInputs = [];
155
- loadingAnimation = undefined;
197
+ activeStatusIndicator = undefined;
198
+ idleStatus = new IdleStatus();
156
199
  workingMessage = undefined;
157
200
  workingVisible = true;
158
201
  workingIndicatorOptions = undefined;
@@ -176,6 +219,7 @@ export class InteractiveMode {
176
219
  toolOutputExpanded = false;
177
220
  // Thinking block visibility state
178
221
  hideThinkingBlock = false;
222
+ outputPad = 1;
179
223
  // Skill commands: command name -> skill file path
180
224
  skillCommands = new Map();
181
225
  // Agent subscription unsubscribe function
@@ -188,11 +232,8 @@ export class InteractiveMode {
188
232
  // Track pending bash components (shown in pending area, moved to chat on submit)
189
233
  pendingBashComponents = [];
190
234
  // Auto-compaction state
191
- autoCompactionLoader = undefined;
192
235
  autoCompactionEscapeHandler;
193
236
  // Auto-retry state
194
- retryLoader = undefined;
195
- retryCountdown = undefined;
196
237
  retryEscapeHandler;
197
238
  // Messages queued while compaction is running
198
239
  compactionQueuedMessages = [];
@@ -246,6 +287,7 @@ export class InteractiveMode {
246
287
  this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
247
288
  this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
248
289
  this.headerContainer = new Container();
290
+ this.loadedResourcesContainer = new Container();
249
291
  this.chatContainer = new Container();
250
292
  this.pendingMessagesContainer = new Container();
251
293
  this.statusContainer = new Container();
@@ -267,6 +309,7 @@ export class InteractiveMode {
267
309
  this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
268
310
  // Load hide thinking block setting
269
311
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
312
+ this.outputPad = this.settingsManager.getOutputPad();
270
313
  // Register themes from resource loader and initialize
271
314
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
272
315
  this.themeController = new InteractiveThemeController(this.ui, this.settingsManager, (message) => this.showError(message), () => this.updateEditorBorderColor());
@@ -315,6 +358,7 @@ export class InteractiveMode {
315
358
  const slashCommands = BUILTIN_SLASH_COMMANDS.map((command) => ({
316
359
  name: command.name,
317
360
  description: command.description,
361
+ ...(command.argumentHint && { argumentHint: command.argumentHint }),
318
362
  }));
319
363
  const modelCommand = slashCommands.find((command) => command.name === "model");
320
364
  if (modelCommand) {
@@ -332,17 +376,24 @@ export class InteractiveMode {
332
376
  name: m.name,
333
377
  label: `${m.provider}/${m.id}`,
334
378
  }));
335
- // Fuzzy filter by model ID + provider in either order.
336
- const filtered = fuzzyFilter(items, prefix, getModelSearchText);
337
- if (filtered.length === 0)
338
- return null;
339
- return filtered.map((item) => ({
379
+ return createFuzzyAutocompleteItems(items, prefix, getModelSearchText, (item) => ({
340
380
  value: item.label,
341
381
  label: item.id,
342
382
  description: item.provider,
343
383
  }));
344
384
  };
345
385
  }
386
+ const loginCommand = slashCommands.find((command) => command.name === "login");
387
+ if (loginCommand) {
388
+ loginCommand.getArgumentCompletions = (prefix) => {
389
+ const providers = getLoginProviderCompletionOptions(this.getLoginProviderOptions());
390
+ return createFuzzyAutocompleteItems(providers, prefix, getLoginProviderSearchText, (provider) => ({
391
+ value: provider.id,
392
+ label: provider.id,
393
+ description: formatLoginProviderCompletionDescription(provider),
394
+ }));
395
+ };
396
+ }
346
397
  // Convert prompt templates to SlashCommand format for autocomplete
347
398
  const templateCommands = this.session.promptTemplates.map((cmd) => ({
348
399
  name: cmd.name,
@@ -439,8 +490,10 @@ export class InteractiveMode {
439
490
  : "";
440
491
  console.log(theme.fg("dim", `Model scope: ${modelList}${cycleHint}`));
441
492
  }
442
- // Add header container as first child. Populate it after detectThemeIfUnset.
493
+ // Add header container as first child. Populate it after applying theme settings.
494
+ // Keep loaded resources before chat so restored session messages never precede them.
443
495
  this.ui.addChild(this.headerContainer);
496
+ this.ui.addChild(this.loadedResourcesContainer);
444
497
  this.ui.addChild(this.chatContainer);
445
498
  this.ui.addChild(this.pendingMessagesContainer);
446
499
  this.ui.addChild(this.statusContainer);
@@ -990,6 +1043,8 @@ export class InteractiveMode {
990
1043
  return lines.join("\n");
991
1044
  }
992
1045
  showLoadedResources(options) {
1046
+ // Resource rendering is idempotent; chat clears no longer clear this separate container.
1047
+ this.loadedResourcesContainer.clear();
993
1048
  const showListing = options?.force || this.options.verbose || !this.settingsManager.getQuietStartup();
994
1049
  const showDiagnostics = showListing || options?.showDiagnosticsWhenQuiet === true;
995
1050
  if (!showListing && !showDiagnostics) {
@@ -1005,8 +1060,8 @@ export class InteractiveMode {
1005
1060
  };
1006
1061
  const addLoadedSection = (name, collapsedBody, expandedBody = collapsedBody, color = "mdHeading") => {
1007
1062
  const section = new ExpandableText(() => `${sectionHeader(name, color)}\n${collapsedBody}`, () => `${sectionHeader(name, color)}\n${expandedBody}`, this.getStartupExpansionState(), 0, 0);
1008
- this.chatContainer.addChild(section);
1009
- this.chatContainer.addChild(new Spacer(1));
1063
+ this.loadedResourcesContainer.addChild(section);
1064
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1010
1065
  };
1011
1066
  const skillsResult = this.session.resourceLoader.getSkills();
1012
1067
  const promptsResult = this.session.resourceLoader.getPrompts();
@@ -1040,7 +1095,7 @@ export class InteractiveMode {
1040
1095
  if (showListing) {
1041
1096
  const contextFiles = this.session.resourceLoader.getAgentsFiles().agentsFiles;
1042
1097
  if (contextFiles.length > 0) {
1043
- this.chatContainer.addChild(new Spacer(1));
1098
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1044
1099
  const contextList = contextFiles
1045
1100
  .map((f) => theme.fg("dim", ` ${this.formatDisplayPath(f.path)}`))
1046
1101
  .join("\n");
@@ -1103,14 +1158,14 @@ export class InteractiveMode {
1103
1158
  const skillDiagnostics = skillsResult.diagnostics;
1104
1159
  if (skillDiagnostics.length > 0) {
1105
1160
  const warningLines = this.formatDiagnostics(skillDiagnostics, sourceInfos);
1106
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0));
1107
- this.chatContainer.addChild(new Spacer(1));
1161
+ this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0));
1162
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1108
1163
  }
1109
1164
  const promptDiagnostics = promptsResult.diagnostics;
1110
1165
  if (promptDiagnostics.length > 0) {
1111
1166
  const warningLines = this.formatDiagnostics(promptDiagnostics, sourceInfos);
1112
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, 0, 0));
1113
- this.chatContainer.addChild(new Spacer(1));
1167
+ this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, 0, 0));
1168
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1114
1169
  }
1115
1170
  const extensionDiagnostics = [];
1116
1171
  const extensionErrors = this.session.resourceLoader.getExtensions().errors;
@@ -1126,14 +1181,14 @@ export class InteractiveMode {
1126
1181
  extensionDiagnostics.push(...shortcutDiagnostics);
1127
1182
  if (extensionDiagnostics.length > 0) {
1128
1183
  const warningLines = this.formatDiagnostics(extensionDiagnostics, sourceInfos);
1129
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0));
1130
- this.chatContainer.addChild(new Spacer(1));
1184
+ this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0));
1185
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1131
1186
  }
1132
1187
  const themeDiagnostics = themesResult.diagnostics;
1133
1188
  if (themeDiagnostics.length > 0) {
1134
1189
  const warningLines = this.formatDiagnostics(themeDiagnostics, sourceInfos);
1135
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0));
1136
- this.chatContainer.addChild(new Spacer(1));
1190
+ this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0));
1191
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1137
1192
  }
1138
1193
  }
1139
1194
  }
@@ -1149,13 +1204,9 @@ export class InteractiveMode {
1149
1204
  this.restoreQueuedMessagesToEditor({ abort: true });
1150
1205
  },
1151
1206
  commandContextActions: {
1152
- waitForIdle: () => this.session.agent.waitForIdle(),
1207
+ waitForIdle: () => this.session.waitForIdle(),
1153
1208
  newSession: async (options) => {
1154
- if (this.loadingAnimation) {
1155
- this.loadingAnimation.stop();
1156
- this.loadingAnimation = undefined;
1157
- }
1158
- this.statusContainer.clear();
1209
+ this.clearStatusIndicator();
1159
1210
  try {
1160
1211
  return await this.runtimeHost.newSession(options);
1161
1212
  }
@@ -1204,7 +1255,7 @@ export class InteractiveMode {
1204
1255
  },
1205
1256
  shutdownHandler: () => {
1206
1257
  this.shutdownRequested = true;
1207
- if (!this.session.isStreaming) {
1258
+ if (this.session.isIdle) {
1208
1259
  void this.shutdown();
1209
1260
  }
1210
1261
  },
@@ -1225,8 +1276,13 @@ export class InteractiveMode {
1225
1276
  this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
1226
1277
  this.footerDataProvider.setCwd(this.sessionManager.getCwd());
1227
1278
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
1279
+ this.outputPad = this.settingsManager.getOutputPad();
1228
1280
  this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
1229
- this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
1281
+ const clearOnShrink = this.settingsManager.getClearOnShrink();
1282
+ this.ui.setClearOnShrink(clearOnShrink);
1283
+ if (!clearOnShrink && !this.activeStatusIndicator) {
1284
+ this.statusContainer.clear();
1285
+ }
1230
1286
  const editorPaddingX = this.settingsManager.getEditorPaddingX();
1231
1287
  const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
1232
1288
  this.defaultEditor.setPaddingX(editorPaddingX);
@@ -1261,6 +1317,7 @@ export class InteractiveMode {
1261
1317
  process.exit(1);
1262
1318
  }
1263
1319
  renderCurrentSessionState() {
1320
+ this.loadedResourcesContainer.clear();
1264
1321
  this.chatContainer.clear();
1265
1322
  this.pendingMessagesContainer.clear();
1266
1323
  this.compactionQueuedMessages = [];
@@ -1291,7 +1348,7 @@ export class InteractiveMode {
1291
1348
  sessionManager: this.sessionManager,
1292
1349
  modelRegistry: this.session.modelRegistry,
1293
1350
  model: this.session.model,
1294
- isIdle: () => !this.session.isStreaming,
1351
+ isIdle: () => this.session.isIdle,
1295
1352
  isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
1296
1353
  signal: this.session.agent.signal,
1297
1354
  abort: () => {
@@ -1338,36 +1395,41 @@ export class InteractiveMode {
1338
1395
  this.footerDataProvider.setExtensionStatus(key, text);
1339
1396
  this.ui.requestRender();
1340
1397
  }
1341
- getWorkingLoaderMessage() {
1342
- return this.workingMessage ?? this.defaultWorkingMessage;
1343
- }
1344
- createWorkingLoader() {
1345
- return new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), this.getWorkingLoaderMessage(), this.workingIndicatorOptions);
1398
+ showStatusIndicator(indicator) {
1399
+ this.activeStatusIndicator?.dispose();
1400
+ this.activeStatusIndicator = indicator;
1401
+ this.statusContainer.clear();
1402
+ this.statusContainer.addChild(indicator);
1346
1403
  }
1347
- stopWorkingLoader() {
1348
- if (this.loadingAnimation) {
1349
- this.loadingAnimation.stop();
1350
- this.loadingAnimation = undefined;
1404
+ clearStatusIndicator(kind) {
1405
+ if (kind && this.activeStatusIndicator?.kind !== kind) {
1406
+ return;
1351
1407
  }
1408
+ const hadActiveStatusIndicator = this.activeStatusIndicator !== undefined;
1409
+ this.activeStatusIndicator?.dispose();
1410
+ this.activeStatusIndicator = undefined;
1352
1411
  this.statusContainer.clear();
1412
+ if (hadActiveStatusIndicator && this.ui.getClearOnShrink()) {
1413
+ this.statusContainer.addChild(this.idleStatus);
1414
+ }
1353
1415
  }
1354
1416
  setWorkingVisible(visible) {
1355
1417
  this.workingVisible = visible;
1356
1418
  if (!visible) {
1357
- this.stopWorkingLoader();
1419
+ this.clearStatusIndicator("working");
1358
1420
  this.ui.requestRender();
1359
1421
  return;
1360
1422
  }
1361
- if (this.session.isStreaming && !this.loadingAnimation) {
1362
- this.statusContainer.clear();
1363
- this.loadingAnimation = this.createWorkingLoader();
1364
- this.statusContainer.addChild(this.loadingAnimation);
1423
+ if (this.session.isStreaming && this.activeStatusIndicator?.kind !== "working") {
1424
+ this.showStatusIndicator(new WorkingStatusIndicator(this.ui, this.workingMessage ?? this.defaultWorkingMessage, this.workingIndicatorOptions));
1365
1425
  }
1366
1426
  this.ui.requestRender();
1367
1427
  }
1368
1428
  setWorkingIndicator(options) {
1369
1429
  this.workingIndicatorOptions = options;
1370
- this.loadingAnimation?.setIndicator(options);
1430
+ if (this.activeStatusIndicator?.kind === "working") {
1431
+ this.activeStatusIndicator.setIndicator(options);
1432
+ }
1371
1433
  this.ui.requestRender();
1372
1434
  }
1373
1435
  setHiddenThinkingLabel(label) {
@@ -1455,8 +1517,8 @@ export class InteractiveMode {
1455
1517
  this.workingMessage = undefined;
1456
1518
  this.workingVisible = true;
1457
1519
  this.setWorkingIndicator();
1458
- if (this.loadingAnimation) {
1459
- this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
1520
+ if (this.activeStatusIndicator?.kind === "working") {
1521
+ this.activeStatusIndicator.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
1460
1522
  }
1461
1523
  this.setHiddenThinkingLabel();
1462
1524
  }
@@ -1596,8 +1658,8 @@ export class InteractiveMode {
1596
1658
  setStatus: (key, text) => this.setExtensionStatus(key, text),
1597
1659
  setWorkingMessage: (message) => {
1598
1660
  this.workingMessage = message;
1599
- if (this.loadingAnimation) {
1600
- this.loadingAnimation.setMessage(message ?? this.defaultWorkingMessage);
1661
+ if (this.activeStatusIndicator?.kind === "working") {
1662
+ this.activeStatusIndicator.setMessage(message ?? this.defaultWorkingMessage);
1601
1663
  }
1602
1664
  },
1603
1665
  setWorkingVisible: (visible) => this.setWorkingVisible(visible),
@@ -1741,7 +1803,7 @@ export class InteractiveMode {
1741
1803
  }, () => {
1742
1804
  this.hideExtensionEditor();
1743
1805
  resolve(undefined);
1744
- });
1806
+ }, undefined, this.settingsManager.getExternalEditorCommand());
1745
1807
  this.editorContainer.clear();
1746
1808
  this.editorContainer.addChild(this.extensionEditor);
1747
1809
  this.ui.setFocus(this.extensionEditor);
@@ -2092,9 +2154,10 @@ export class InteractiveMode {
2092
2154
  this.editor.setText("");
2093
2155
  return;
2094
2156
  }
2095
- if (text === "/login") {
2096
- this.showOAuthSelector("login");
2157
+ if (text === "/login" || text.startsWith("/login ")) {
2158
+ const providerRef = text.startsWith("/login ") ? text.slice(7).trim() : undefined;
2097
2159
  this.editor.setText("");
2160
+ await this.handleLoginCommand(providerRef);
2098
2161
  return;
2099
2162
  }
2100
2163
  if (text === "/logout") {
@@ -2216,18 +2279,11 @@ export class InteractiveMode {
2216
2279
  this.defaultEditor.onEscape = this.retryEscapeHandler;
2217
2280
  this.retryEscapeHandler = undefined;
2218
2281
  }
2219
- if (this.retryCountdown) {
2220
- this.retryCountdown.dispose();
2221
- this.retryCountdown = undefined;
2222
- }
2223
- if (this.retryLoader) {
2224
- this.retryLoader.stop();
2225
- this.retryLoader = undefined;
2226
- }
2227
- this.stopWorkingLoader();
2228
2282
  if (this.workingVisible) {
2229
- this.loadingAnimation = this.createWorkingLoader();
2230
- this.statusContainer.addChild(this.loadingAnimation);
2283
+ this.showStatusIndicator(new WorkingStatusIndicator(this.ui, this.workingMessage ?? this.defaultWorkingMessage, this.workingIndicatorOptions));
2284
+ }
2285
+ else {
2286
+ this.clearStatusIndicator();
2231
2287
  }
2232
2288
  this.ui.requestRender();
2233
2289
  break;
@@ -2235,6 +2291,12 @@ export class InteractiveMode {
2235
2291
  this.updatePendingMessagesDisplay();
2236
2292
  this.ui.requestRender();
2237
2293
  break;
2294
+ case "entry_appended":
2295
+ if (event.entry.type === "custom") {
2296
+ this.addCustomEntryToChat(event.entry);
2297
+ this.ui.requestRender();
2298
+ }
2299
+ break;
2238
2300
  case "session_info_changed":
2239
2301
  this.updateTerminalTitle();
2240
2302
  this.footer.invalidate();
@@ -2255,7 +2317,7 @@ export class InteractiveMode {
2255
2317
  this.ui.requestRender();
2256
2318
  }
2257
2319
  else if (event.message.role === "assistant") {
2258
- this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel);
2320
+ this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, this.outputPad);
2259
2321
  this.streamingMessage = event.message;
2260
2322
  this.chatContainer.addChild(this.streamingComponent);
2261
2323
  this.streamingComponent.updateContent(this.streamingMessage);
@@ -2320,6 +2382,7 @@ export class InteractiveMode {
2320
2382
  for (const [, component] of this.pendingTools.entries()) {
2321
2383
  component.setArgsComplete();
2322
2384
  }
2385
+ this.maybeShowCacheMissNotice(this.streamingMessage);
2323
2386
  }
2324
2387
  this.streamingComponent = undefined;
2325
2388
  this.streamingMessage = undefined;
@@ -2363,20 +2426,18 @@ export class InteractiveMode {
2363
2426
  if (this.settingsManager.getShowTerminalProgress()) {
2364
2427
  this.ui.terminal.setProgress(false);
2365
2428
  }
2366
- if (this.loadingAnimation) {
2367
- this.loadingAnimation.stop();
2368
- this.loadingAnimation = undefined;
2369
- this.statusContainer.clear();
2370
- }
2429
+ this.clearStatusIndicator("working");
2371
2430
  if (this.streamingComponent) {
2372
2431
  this.chatContainer.removeChild(this.streamingComponent);
2373
2432
  this.streamingComponent = undefined;
2374
2433
  this.streamingMessage = undefined;
2375
2434
  }
2376
2435
  this.pendingTools.clear();
2377
- await this.checkShutdownRequested();
2378
2436
  this.ui.requestRender();
2379
2437
  break;
2438
+ case "agent_settled":
2439
+ await this.checkShutdownRequested();
2440
+ break;
2380
2441
  case "compaction_start": {
2381
2442
  if (this.settingsManager.getShowTerminalProgress()) {
2382
2443
  this.ui.terminal.setProgress(true);
@@ -2386,13 +2447,7 @@ export class InteractiveMode {
2386
2447
  this.defaultEditor.onEscape = () => {
2387
2448
  this.session.abortCompaction();
2388
2449
  };
2389
- this.statusContainer.clear();
2390
- const cancelHint = `(${keyText("app.interrupt")} to cancel)`;
2391
- const label = event.reason === "manual"
2392
- ? `Compacting context... ${cancelHint}`
2393
- : `${event.reason === "overflow" ? "Context overflow detected, " : ""}Auto-compacting... ${cancelHint}`;
2394
- this.autoCompactionLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), label);
2395
- this.statusContainer.addChild(this.autoCompactionLoader);
2450
+ this.showStatusIndicator(new CompactionStatusIndicator(this.ui, event.reason));
2396
2451
  this.ui.requestRender();
2397
2452
  break;
2398
2453
  }
@@ -2404,11 +2459,7 @@ export class InteractiveMode {
2404
2459
  this.defaultEditor.onEscape = this.autoCompactionEscapeHandler;
2405
2460
  this.autoCompactionEscapeHandler = undefined;
2406
2461
  }
2407
- if (this.autoCompactionLoader) {
2408
- this.autoCompactionLoader.stop();
2409
- this.autoCompactionLoader = undefined;
2410
- this.statusContainer.clear();
2411
- }
2462
+ this.clearStatusIndicator("compaction");
2412
2463
  if (event.aborted) {
2413
2464
  if (event.reason === "manual") {
2414
2465
  this.showError("Compaction cancelled");
@@ -2442,17 +2493,7 @@ export class InteractiveMode {
2442
2493
  this.defaultEditor.onEscape = () => {
2443
2494
  this.session.abortRetry();
2444
2495
  };
2445
- // Show retry indicator
2446
- this.statusContainer.clear();
2447
- this.retryCountdown?.dispose();
2448
- const retryMessage = (seconds) => `Retrying (${event.attempt}/${event.maxAttempts}) in ${seconds}s... (${keyText("app.interrupt")} to cancel)`;
2449
- this.retryLoader = new Loader(this.ui, (spinner) => theme.fg("warning", spinner), (text) => theme.fg("muted", text), retryMessage(Math.ceil(event.delayMs / 1000)));
2450
- this.retryCountdown = new CountdownTimer(event.delayMs, this.ui, (seconds) => {
2451
- this.retryLoader?.setMessage(retryMessage(seconds));
2452
- }, () => {
2453
- this.retryCountdown = undefined;
2454
- });
2455
- this.statusContainer.addChild(this.retryLoader);
2496
+ this.showStatusIndicator(new RetryStatusIndicator(this.ui, event.attempt, event.maxAttempts, event.delayMs));
2456
2497
  this.ui.requestRender();
2457
2498
  break;
2458
2499
  }
@@ -2462,16 +2503,7 @@ export class InteractiveMode {
2462
2503
  this.defaultEditor.onEscape = this.retryEscapeHandler;
2463
2504
  this.retryEscapeHandler = undefined;
2464
2505
  }
2465
- if (this.retryCountdown) {
2466
- this.retryCountdown.dispose();
2467
- this.retryCountdown = undefined;
2468
- }
2469
- // Stop loader
2470
- if (this.retryLoader) {
2471
- this.retryLoader.stop();
2472
- this.retryLoader = undefined;
2473
- this.statusContainer.clear();
2474
- }
2506
+ this.clearStatusIndicator("retry");
2475
2507
  // Show error only on final failure (success shows normal response)
2476
2508
  if (!event.success) {
2477
2509
  this.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`);
@@ -2513,6 +2545,25 @@ export class InteractiveMode {
2513
2545
  this.lastStatusText = text;
2514
2546
  this.ui.requestRender();
2515
2547
  }
2548
+ addCustomEntryToChat(entry) {
2549
+ const renderer = this.session.extensionRunner.getEntryRenderer(entry.customType);
2550
+ if (!renderer) {
2551
+ return;
2552
+ }
2553
+ const component = new CustomEntryComponent(entry, renderer);
2554
+ component.setExpanded(this.toolOutputExpanded);
2555
+ if (!component.hasContent()) {
2556
+ return;
2557
+ }
2558
+ if (this.streamingComponent) {
2559
+ const streamingIndex = this.chatContainer.children.indexOf(this.streamingComponent);
2560
+ if (streamingIndex >= 0) {
2561
+ this.chatContainer.children.splice(streamingIndex, 0, component);
2562
+ return;
2563
+ }
2564
+ }
2565
+ this.chatContainer.addChild(component);
2566
+ }
2516
2567
  addMessageToChat(message, options) {
2517
2568
  switch (message.role) {
2518
2569
  case "bashExecution": {
@@ -2562,12 +2613,12 @@ export class InteractiveMode {
2562
2613
  // Render user message separately if present
2563
2614
  if (skillBlock.userMessage) {
2564
2615
  this.chatContainer.addChild(new Spacer(1));
2565
- const userComponent = new UserMessageComponent(skillBlock.userMessage, this.getMarkdownThemeWithSettings());
2616
+ const userComponent = new UserMessageComponent(skillBlock.userMessage, this.getMarkdownThemeWithSettings(), this.outputPad);
2566
2617
  this.chatContainer.addChild(userComponent);
2567
2618
  }
2568
2619
  }
2569
2620
  else {
2570
- const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings());
2621
+ const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings(), this.outputPad);
2571
2622
  this.chatContainer.addChild(userComponent);
2572
2623
  }
2573
2624
  if (options?.populateHistory) {
@@ -2577,7 +2628,7 @@ export class InteractiveMode {
2577
2628
  break;
2578
2629
  }
2579
2630
  case "assistant": {
2580
- const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel);
2631
+ const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, this.outputPad);
2581
2632
  this.chatContainer.addChild(assistantComponent);
2582
2633
  break;
2583
2634
  }
@@ -2590,20 +2641,24 @@ export class InteractiveMode {
2590
2641
  }
2591
2642
  }
2592
2643
  }
2593
- /**
2594
- * Render session context to chat. Used for initial load and rebuild after compaction.
2595
- * @param sessionContext Session context to render
2596
- * @param options.updateFooter Update footer state
2597
- * @param options.populateHistory Add user messages to editor history
2598
- */
2599
- renderSessionContext(sessionContext, options = {}) {
2644
+ renderSessionItems(items, options = {}) {
2600
2645
  this.pendingTools.clear();
2601
2646
  const renderedPendingTools = new Map();
2647
+ // Cache-miss notices are not persisted; re-derive them from the full entry
2648
+ // list and re-inject them after the assistant messages that paid for them.
2649
+ const cacheMisses = this.settingsManager.getShowCacheMissNotices()
2650
+ ? collectCacheMisses(this.sessionManager.getEntries(), this.session.modelRegistry)
2651
+ : new Map();
2602
2652
  if (options.updateFooter) {
2603
2653
  this.footer.invalidate();
2604
2654
  this.updateEditorBorderColor();
2605
2655
  }
2606
- for (const message of sessionContext.messages) {
2656
+ for (const item of items) {
2657
+ if (isCustomSessionEntry(item)) {
2658
+ this.addCustomEntryToChat(item);
2659
+ continue;
2660
+ }
2661
+ const message = item;
2607
2662
  // Assistant messages need special handling for tool calls
2608
2663
  if (message.role === "assistant") {
2609
2664
  this.addMessageToChat(message);
@@ -2635,6 +2690,11 @@ export class InteractiveMode {
2635
2690
  }
2636
2691
  }
2637
2692
  }
2693
+ if (message.stopReason !== "aborted" && message.stopReason !== "error") {
2694
+ const miss = cacheMisses.get(message);
2695
+ if (miss)
2696
+ this.addCacheMissNotice(miss);
2697
+ }
2638
2698
  }
2639
2699
  else if (message.role === "toolResult") {
2640
2700
  // Match tool results to pending tool components
@@ -2654,10 +2714,53 @@ export class InteractiveMode {
2654
2714
  }
2655
2715
  this.ui.requestRender();
2656
2716
  }
2717
+ /**
2718
+ * Render session entries to chat. Used for initial load and rebuild after compaction.
2719
+ * @param entries Compaction-aware session entries to render
2720
+ * @param options.updateFooter Update footer state
2721
+ * @param options.populateHistory Add user messages to editor history
2722
+ */
2723
+ renderSessionEntries(entries, options = {}) {
2724
+ const items = entries.flatMap((entry) => {
2725
+ if (entry.type === "custom") {
2726
+ return [entry];
2727
+ }
2728
+ return sessionEntryToContextMessages(entry);
2729
+ });
2730
+ this.renderSessionItems(items, options);
2731
+ }
2732
+ /**
2733
+ * Show a transcript notice when a completed assistant message paid for a
2734
+ * significant cache miss. Only states observable facts: the miss itself,
2735
+ * a model switch, or an idle gap past the cache TTL.
2736
+ */
2737
+ maybeShowCacheMissNotice(message) {
2738
+ if (!this.settingsManager.getShowCacheMissNotices())
2739
+ return;
2740
+ // Entries don't contain `message` yet: message_end fires before persistence.
2741
+ const miss = detectCacheMiss(this.sessionManager.getEntries(), message, this.session.modelRegistry);
2742
+ if (miss)
2743
+ this.addCacheMissNotice(miss);
2744
+ }
2745
+ addCacheMissNotice(miss) {
2746
+ if (miss.missedTokens < 20_000 && miss.missedCost < 0.1)
2747
+ return;
2748
+ const cost = miss.missedCost >= 0.01 ? ` (~$${miss.missedCost.toFixed(2)})` : "";
2749
+ const reBilled = `${formatTokens(miss.missedTokens)} tokens re-billed${cost}`;
2750
+ let label = "Cache miss";
2751
+ if (miss.modelChanged) {
2752
+ label = "Cache miss after model switch";
2753
+ }
2754
+ else if (miss.idleMs >= CACHE_TTL_MS) {
2755
+ label = `Cache miss after ${Math.round(miss.idleMs / 60_000)}m idle`;
2756
+ }
2757
+ const text = theme.fg("warning", `${label}: ${reBilled}`);
2758
+ this.chatContainer.addChild(new Spacer(1));
2759
+ this.chatContainer.addChild(new Text(text, 1, 0));
2760
+ }
2657
2761
  renderInitialMessages() {
2658
- // Get aligned messages and entries from session context
2659
- const context = this.sessionManager.buildSessionContext();
2660
- this.renderSessionContext(context, {
2762
+ const entries = this.sessionManager.buildContextEntries();
2763
+ this.renderSessionEntries(entries, {
2661
2764
  updateFooter: true,
2662
2765
  populateHistory: true,
2663
2766
  });
@@ -2693,8 +2796,7 @@ export class InteractiveMode {
2693
2796
  }
2694
2797
  rebuildChatFromMessages() {
2695
2798
  this.chatContainer.clear();
2696
- const context = this.sessionManager.buildSessionContext();
2697
- this.renderSessionContext(context);
2799
+ this.renderSessionEntries(this.sessionManager.buildContextEntries());
2698
2800
  }
2699
2801
  // =========================================================================
2700
2802
  // Key handlers
@@ -2965,9 +3067,11 @@ export class InteractiveMode {
2965
3067
  if (isExpandable(activeHeader)) {
2966
3068
  activeHeader.setExpanded(expanded);
2967
3069
  }
2968
- for (const child of this.chatContainer.children) {
2969
- if (isExpandable(child)) {
2970
- child.setExpanded(expanded);
3070
+ for (const container of [this.loadedResourcesContainer, this.chatContainer]) {
3071
+ for (const child of container.children) {
3072
+ if (isExpandable(child)) {
3073
+ child.setExpanded(expanded);
3074
+ }
2971
3075
  }
2972
3076
  }
2973
3077
  this.ui.requestRender();
@@ -2987,10 +3091,9 @@ export class InteractiveMode {
2987
3091
  this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`);
2988
3092
  }
2989
3093
  async openExternalEditor() {
2990
- // Determine editor (respect $VISUAL, then $EDITOR)
2991
- const editorCmd = process.env.VISUAL || process.env.EDITOR;
3094
+ const editorCmd = this.settingsManager.getExternalEditorCommand();
2992
3095
  if (!editorCmd) {
2993
- this.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
3096
+ this.showWarning("No editor configured. Set externalEditor in settings.json or $VISUAL/$EDITOR.");
2994
3097
  return;
2995
3098
  }
2996
3099
  const currentText = this.editor.getExpandedText?.() ?? this.editor.getText();
@@ -3292,8 +3395,10 @@ export class InteractiveMode {
3292
3395
  doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
3293
3396
  treeFilterMode: this.settingsManager.getTreeFilterMode(),
3294
3397
  showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
3398
+ showCacheMissNotices: this.settingsManager.getShowCacheMissNotices(),
3295
3399
  defaultProjectTrust: this.settingsManager.getDefaultProjectTrust(),
3296
3400
  editorPaddingX: this.settingsManager.getEditorPaddingX(),
3401
+ outputPad: this.settingsManager.getOutputPad(),
3297
3402
  autocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),
3298
3403
  quietStartup: this.settingsManager.getQuietStartup(),
3299
3404
  clearOnShrink: this.settingsManager.getClearOnShrink(),
@@ -3366,6 +3471,10 @@ export class InteractiveMode {
3366
3471
  this.chatContainer.clear();
3367
3472
  this.rebuildChatFromMessages();
3368
3473
  },
3474
+ onShowCacheMissNoticesChange: (shown) => {
3475
+ this.settingsManager.setShowCacheMissNotices(shown);
3476
+ this.rebuildChatFromMessages();
3477
+ },
3369
3478
  onCollapseChangelogChange: (collapsed) => {
3370
3479
  this.settingsManager.setCollapseChangelog(collapsed);
3371
3480
  },
@@ -3395,6 +3504,23 @@ export class InteractiveMode {
3395
3504
  this.editor.setPaddingX(padding);
3396
3505
  }
3397
3506
  },
3507
+ onOutputPadChange: (padding) => {
3508
+ this.settingsManager.setOutputPad(padding);
3509
+ this.outputPad = padding;
3510
+ if (this.streamingComponent || this.session.isStreaming) {
3511
+ for (const child of this.chatContainer.children) {
3512
+ if (child instanceof AssistantMessageComponent || child instanceof UserMessageComponent) {
3513
+ child.setOutputPad(padding);
3514
+ }
3515
+ }
3516
+ if (this.streamingComponent) {
3517
+ this.streamingComponent.setOutputPad(padding);
3518
+ }
3519
+ this.ui.requestRender();
3520
+ return;
3521
+ }
3522
+ this.rebuildChatFromMessages();
3523
+ },
3398
3524
  onAutocompleteMaxVisibleChange: (maxVisible) => {
3399
3525
  this.settingsManager.setAutocompleteMaxVisible(maxVisible);
3400
3526
  this.defaultEditor.setAutocompleteMaxVisible(maxVisible);
@@ -3405,6 +3531,9 @@ export class InteractiveMode {
3405
3531
  onClearOnShrinkChange: (enabled) => {
3406
3532
  this.settingsManager.setClearOnShrink(enabled);
3407
3533
  this.ui.setClearOnShrink(enabled);
3534
+ if (!enabled && !this.activeStatusIndicator) {
3535
+ this.statusContainer.clear();
3536
+ }
3408
3537
  },
3409
3538
  onShowTerminalProgressChange: (enabled) => {
3410
3539
  this.settingsManager.setShowTerminalProgress(enabled);
@@ -3635,19 +3764,17 @@ export class InteractiveMode {
3635
3764
  const initialSelectedId = userMessages[userMessages.length - 1]?.entryId;
3636
3765
  this.showSelector((done) => {
3637
3766
  const selector = new UserMessageSelectorComponent(userMessages.map((m) => ({ id: m.entryId, text: m.text })), async (entryId) => {
3767
+ done();
3638
3768
  try {
3639
3769
  const result = await this.runtimeHost.fork(entryId);
3640
3770
  if (result.cancelled) {
3641
- done();
3642
3771
  this.ui.requestRender();
3643
3772
  return;
3644
3773
  }
3645
3774
  this.editor.setText(result.selectedText ?? "");
3646
- done();
3647
3775
  this.showStatus("Forked to new session");
3648
3776
  }
3649
3777
  catch (error) {
3650
- done();
3651
3778
  this.showError(error instanceof Error ? error.message : String(error));
3652
3779
  }
3653
3780
  }, () => {
@@ -3722,16 +3849,16 @@ export class InteractiveMode {
3722
3849
  break;
3723
3850
  }
3724
3851
  }
3725
- // Set up escape handler and loader if summarizing
3726
- let summaryLoader;
3852
+ // Set up escape handler and status indicator if summarizing
3853
+ let showingSummaryIndicator = false;
3727
3854
  const originalOnEscape = this.defaultEditor.onEscape;
3728
3855
  if (wantsSummary) {
3729
3856
  this.defaultEditor.onEscape = () => {
3730
3857
  this.session.abortBranchSummary();
3731
3858
  };
3732
3859
  this.chatContainer.addChild(new Spacer(1));
3733
- summaryLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `Summarizing branch... (${keyText("app.interrupt")} to cancel)`);
3734
- this.statusContainer.addChild(summaryLoader);
3860
+ this.showStatusIndicator(new BranchSummaryStatusIndicator(this.ui));
3861
+ showingSummaryIndicator = true;
3735
3862
  this.ui.requestRender();
3736
3863
  }
3737
3864
  try {
@@ -3762,9 +3889,8 @@ export class InteractiveMode {
3762
3889
  this.showError(error instanceof Error ? error.message : String(error));
3763
3890
  }
3764
3891
  finally {
3765
- if (summaryLoader) {
3766
- summaryLoader.stop();
3767
- this.statusContainer.clear();
3892
+ if (showingSummaryIndicator) {
3893
+ this.clearStatusIndicator("branchSummary");
3768
3894
  }
3769
3895
  this.defaultEditor.onEscape = originalOnEscape;
3770
3896
  }
@@ -3805,11 +3931,7 @@ export class InteractiveMode {
3805
3931
  });
3806
3932
  }
3807
3933
  async handleResumeSession(sessionPath, options) {
3808
- if (this.loadingAnimation) {
3809
- this.loadingAnimation.stop();
3810
- this.loadingAnimation = undefined;
3811
- }
3812
- this.statusContainer.clear();
3934
+ this.clearStatusIndicator();
3813
3935
  try {
3814
3936
  const result = await this.runtimeHost.switchSession(sessionPath, {
3815
3937
  withSession: options?.withSession,
@@ -3881,13 +4003,82 @@ export class InteractiveMode {
3881
4003
  }
3882
4004
  return options.sort((a, b) => a.name.localeCompare(b.name));
3883
4005
  }
3884
- showLoginAuthTypeSelector() {
4006
+ findLoginProviderOptions(providerRef) {
4007
+ const normalizedProviderRef = providerRef.trim().toLowerCase();
4008
+ if (!normalizedProviderRef) {
4009
+ return [];
4010
+ }
4011
+ return this.getLoginProviderOptions().filter((provider) => provider.id.toLowerCase() === normalizedProviderRef ||
4012
+ provider.name.toLowerCase() === normalizedProviderRef);
4013
+ }
4014
+ async handleLoginCommand(providerRef) {
4015
+ if (!providerRef) {
4016
+ this.showLoginAuthTypeSelector();
4017
+ return;
4018
+ }
4019
+ const providerOptions = this.findLoginProviderOptions(providerRef);
4020
+ if (providerOptions.length === 1) {
4021
+ await this.startProviderLogin(providerOptions[0]);
4022
+ return;
4023
+ }
4024
+ if (providerOptions.length > 1) {
4025
+ const providerIds = new Set(providerOptions.map((provider) => provider.id));
4026
+ if (providerIds.size === 1) {
4027
+ this.showLoginAuthTypeSelector(providerOptions);
4028
+ return;
4029
+ }
4030
+ }
4031
+ this.showLoginProviderSelector(undefined, providerRef);
4032
+ }
4033
+ async startProviderLogin(providerOption) {
4034
+ if (providerOption.authType === "oauth") {
4035
+ await this.showLoginDialog(providerOption.id, providerOption.name);
4036
+ }
4037
+ else if (providerOption.id === BEDROCK_PROVIDER_ID) {
4038
+ this.showBedrockSetupDialog(providerOption.id, providerOption.name);
4039
+ }
4040
+ else {
4041
+ await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
4042
+ }
4043
+ }
4044
+ showLoginAuthTypeSelector(providerOptions) {
3885
4045
  const subscriptionLabel = "Use a subscription";
3886
4046
  const apiKeyLabel = "Use an API key";
4047
+ const availableAuthTypes = providerOptions
4048
+ ? new Set(providerOptions.map((provider) => provider.authType))
4049
+ : new Set(["oauth", "api_key"]);
4050
+ const options = [];
4051
+ if (availableAuthTypes.has("oauth")) {
4052
+ options.push(subscriptionLabel);
4053
+ }
4054
+ if (availableAuthTypes.has("api_key")) {
4055
+ options.push(apiKeyLabel);
4056
+ }
4057
+ if (options.length === 0) {
4058
+ this.showStatus("No login methods available.");
4059
+ return;
4060
+ }
4061
+ if (providerOptions && options.length === 1) {
4062
+ const providerOption = providerOptions[0];
4063
+ if (providerOption) {
4064
+ void this.startProviderLogin(providerOption);
4065
+ }
4066
+ return;
4067
+ }
4068
+ const title = providerOptions?.[0]
4069
+ ? `Select authentication method for ${providerOptions[0].name}:`
4070
+ : "Select authentication method:";
3887
4071
  this.showSelector((done) => {
3888
- const selector = new ExtensionSelectorComponent("Select authentication method:", [subscriptionLabel, apiKeyLabel], (option) => {
4072
+ const selector = new ExtensionSelectorComponent(title, options, (option) => {
3889
4073
  done();
3890
4074
  const authType = option === subscriptionLabel ? "oauth" : "api_key";
4075
+ if (providerOptions) {
4076
+ const providerOption = providerOptions.find((provider) => provider.authType === authType);
4077
+ if (providerOption) {
4078
+ void this.startProviderLogin(providerOption);
4079
+ }
4080
+ return;
4081
+ }
3891
4082
  this.showLoginProviderSelector(authType);
3892
4083
  }, () => {
3893
4084
  done();
@@ -3896,32 +4087,34 @@ export class InteractiveMode {
3896
4087
  return { component: selector, focus: selector };
3897
4088
  });
3898
4089
  }
3899
- showLoginProviderSelector(authType) {
4090
+ showLoginProviderSelector(authType, initialSearchInput) {
3900
4091
  const providerOptions = this.getLoginProviderOptions(authType);
3901
4092
  if (providerOptions.length === 0) {
3902
- this.showStatus(authType === "oauth" ? "No subscription providers available." : "No API key providers available.");
4093
+ const message = authType === "oauth"
4094
+ ? "No subscription providers available."
4095
+ : authType === "api_key"
4096
+ ? "No API key providers available."
4097
+ : "No login providers available.";
4098
+ this.showStatus(message);
3903
4099
  return;
3904
4100
  }
3905
4101
  this.showSelector((done) => {
3906
- const selector = new OAuthSelectorComponent("login", this.session.modelRegistry.authStorage, providerOptions, async (providerId) => {
4102
+ const selector = new OAuthSelectorComponent("login", this.session.modelRegistry.authStorage, providerOptions, async (providerId, selectedAuthType) => {
3907
4103
  done();
3908
- const providerOption = providerOptions.find((provider) => provider.id === providerId);
4104
+ const providerOption = providerOptions.find((provider) => provider.id === providerId && provider.authType === selectedAuthType);
3909
4105
  if (!providerOption) {
3910
4106
  return;
3911
4107
  }
3912
- if (providerOption.authType === "oauth") {
3913
- await this.showLoginDialog(providerOption.id, providerOption.name);
3914
- }
3915
- else if (providerOption.id === BEDROCK_PROVIDER_ID) {
3916
- this.showBedrockSetupDialog(providerOption.id, providerOption.name);
4108
+ await this.startProviderLogin(providerOption);
4109
+ }, () => {
4110
+ done();
4111
+ if (authType) {
4112
+ this.showLoginAuthTypeSelector();
3917
4113
  }
3918
4114
  else {
3919
- await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
4115
+ this.ui.requestRender();
3920
4116
  }
3921
- }, () => {
3922
- done();
3923
- this.showLoginAuthTypeSelector();
3924
- }, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId));
4117
+ }, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId), initialSearchInput);
3925
4118
  return { component: selector, focus: selector };
3926
4119
  });
3927
4120
  }
@@ -4180,7 +4373,7 @@ export class InteractiveMode {
4180
4373
  const borderColor = (s) => theme.fg("border", s);
4181
4374
  reloadBox.addChild(new DynamicBorder(borderColor));
4182
4375
  reloadBox.addChild(new Spacer(1));
4183
- reloadBox.addChild(new Text(theme.fg("muted", "Reloading keybindings, extensions, skills, prompts, themes..."), 1, 0));
4376
+ reloadBox.addChild(new Text(theme.fg("muted", "Reloading keybindings, extensions, skills, prompts, themes, and context files..."), 1, 0));
4184
4377
  reloadBox.addChild(new Spacer(1));
4185
4378
  reloadBox.addChild(new DynamicBorder(borderColor));
4186
4379
  const previousEditor = this.editor;
@@ -4202,6 +4395,7 @@ export class InteractiveMode {
4202
4395
  return;
4203
4396
  }
4204
4397
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
4398
+ this.outputPad = this.settingsManager.getOutputPad();
4205
4399
  this.rebuildChatFromMessages();
4206
4400
  chatRestoredBeforeSessionStart = true;
4207
4401
  };
@@ -4225,7 +4419,11 @@ export class InteractiveMode {
4225
4419
  this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
4226
4420
  }
4227
4421
  this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
4228
- this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
4422
+ const clearOnShrink = this.settingsManager.getClearOnShrink();
4423
+ this.ui.setClearOnShrink(clearOnShrink);
4424
+ if (!clearOnShrink && !this.activeStatusIndicator) {
4425
+ this.statusContainer.clear();
4426
+ }
4229
4427
  this.setupAutocompleteProvider();
4230
4428
  const runner = this.session.extensionRunner;
4231
4429
  this.setupExtensionShortcuts(runner);
@@ -4239,8 +4437,8 @@ export class InteractiveMode {
4239
4437
  this.showError(`models.json error: ${modelsJsonError}`);
4240
4438
  }
4241
4439
  this.showStatus(savedImplicitProjectTrust
4242
- ? "Reloaded keybindings, extensions, skills, prompts, themes; saved project trust"
4243
- : "Reloaded keybindings, extensions, skills, prompts, themes");
4440
+ ? "Reloaded keybindings, extensions, skills, prompts, themes, and context files; saved project trust"
4441
+ : "Reloaded keybindings, extensions, skills, prompts, themes, and context files");
4244
4442
  dismissReloadBox(this.editor);
4245
4443
  reloadBoxDismissed = true;
4246
4444
  }
@@ -4304,11 +4502,7 @@ export class InteractiveMode {
4304
4502
  return;
4305
4503
  }
4306
4504
  try {
4307
- if (this.loadingAnimation) {
4308
- this.loadingAnimation.stop();
4309
- this.loadingAnimation = undefined;
4310
- }
4311
- this.statusContainer.clear();
4505
+ this.clearStatusIndicator();
4312
4506
  const result = await this.runtimeHost.importFromJsonl(inputPath);
4313
4507
  if (result.cancelled) {
4314
4508
  this.showStatus("Import cancelled");
@@ -4465,6 +4659,26 @@ export class InteractiveMode {
4465
4659
  handleSessionCommand() {
4466
4660
  const stats = this.session.getSessionStats();
4467
4661
  const sessionName = this.sessionManager.getSessionName();
4662
+ const entries = this.sessionManager.getEntries();
4663
+ const cacheWaste = computeCacheWaste(entries, this.session.modelRegistry);
4664
+ // Cost/token totals per provider/model actually used (e.g. OpenRouter `auto`
4665
+ // resolves to a concrete responseModel), sorted by cost descending.
4666
+ const perModelMap = new Map();
4667
+ for (const entry of entries) {
4668
+ if (entry.type !== "message" || entry.message.role !== "assistant")
4669
+ continue;
4670
+ const message = entry.message;
4671
+ const usage = message.usage;
4672
+ const key = `${message.provider}/${message.responseModel ?? message.model}`;
4673
+ let bucket = perModelMap.get(key);
4674
+ if (!bucket) {
4675
+ bucket = { key, cost: 0, tokens: 0 };
4676
+ perModelMap.set(key, bucket);
4677
+ }
4678
+ bucket.cost += usage.cost.total;
4679
+ bucket.tokens += usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
4680
+ }
4681
+ const perModel = Array.from(perModelMap.values()).sort((a, b) => b.cost - a.cost);
4468
4682
  let info = `${theme.bold("Session Info")}\n\n`;
4469
4683
  if (sessionName) {
4470
4684
  info += `${theme.fg("dim", "Name:")} ${sessionName}\n`;
@@ -4472,24 +4686,42 @@ export class InteractiveMode {
4472
4686
  info += `${theme.fg("dim", "File:")} ${stats.sessionFile ?? "In-memory"}\n`;
4473
4687
  info += `${theme.fg("dim", "ID:")} ${stats.sessionId}\n\n`;
4474
4688
  info += `${theme.bold("Messages")}\n`;
4689
+ info += `${theme.fg("dim", "Total:")} ${stats.totalMessages}\n`;
4475
4690
  info += `${theme.fg("dim", "User:")} ${stats.userMessages}\n`;
4476
4691
  info += `${theme.fg("dim", "Assistant:")} ${stats.assistantMessages}\n`;
4477
- info += `${theme.fg("dim", "Tool Calls:")} ${stats.toolCalls}\n`;
4478
- info += `${theme.fg("dim", "Tool Results:")} ${stats.toolResults}\n`;
4479
- info += `${theme.fg("dim", "Total:")} ${stats.totalMessages}\n\n`;
4692
+ info += `${theme.fg("dim", "Tools:")} ${stats.toolCalls} calls, ${stats.toolResults} results\n\n`;
4480
4693
  info += `${theme.bold("Tokens")}\n`;
4481
- info += `${theme.fg("dim", "Input:")} ${stats.tokens.input.toLocaleString()}\n`;
4482
- info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`;
4483
- if (stats.tokens.cacheRead > 0) {
4484
- info += `${theme.fg("dim", "Cache Read:")} ${stats.tokens.cacheRead.toLocaleString()}\n`;
4485
- }
4486
- if (stats.tokens.cacheWrite > 0) {
4487
- info += `${theme.fg("dim", "Cache Write:")} ${stats.tokens.cacheWrite.toLocaleString()}\n`;
4694
+ // "Input" is the full prompt volume. With cache activity, split it into
4695
+ // cached (served from cache) vs uncached (everything else) - the only
4696
+ // provider-independent split. Cache writes, where reported, are a detail
4697
+ // of the uncached portion.
4698
+ const { input, cacheRead, cacheWrite } = stats.tokens;
4699
+ const promptTokens = input + cacheRead + cacheWrite;
4700
+ info += `${theme.fg("dim", "Input:")} ${promptTokens.toLocaleString()}\n`;
4701
+ if (promptTokens > 0 && (cacheRead > 0 || cacheWrite > 0)) {
4702
+ const hitRate = theme.fg("dim", `(${((cacheRead / promptTokens) * 100).toFixed(1)}%)`);
4703
+ info += ` ${theme.fg("dim", "Cached:")} ${cacheRead.toLocaleString()} ${hitRate}\n`;
4704
+ const written = cacheWrite > 0 ? ` ${theme.fg("dim", `(${cacheWrite.toLocaleString()} written to cache)`)}` : "";
4705
+ info += ` ${theme.fg("dim", "Uncached:")} ${(input + cacheWrite).toLocaleString()}${written}\n`;
4488
4706
  }
4707
+ info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`;
4489
4708
  info += `${theme.fg("dim", "Total:")} ${stats.tokens.total.toLocaleString()}\n`;
4490
- if (stats.cost > 0) {
4709
+ if (stats.cost > 0 || cacheWaste.missedTokens > 0) {
4491
4710
  info += `\n${theme.bold("Cost")}\n`;
4492
- info += `${theme.fg("dim", "Total:")} ${stats.cost.toFixed(4)}`;
4711
+ info += `${theme.fg("dim", "Total:")} $${stats.cost.toFixed(3)}`;
4712
+ if (perModel.length > 1) {
4713
+ for (const entry of perModel) {
4714
+ info += `\n ${theme.fg("dim", `${entry.key}:`)} $${entry.cost.toFixed(3)} ${theme.fg("dim", `(${formatTokens(entry.tokens)} tokens)`)}`;
4715
+ }
4716
+ }
4717
+ if (cacheWaste.missedTokens > 0) {
4718
+ const missLabel = cacheWaste.missCount === 1 ? "1 miss" : `${cacheWaste.missCount} misses`;
4719
+ const detail = `${cacheWaste.missedTokens.toLocaleString()} tokens, ${missLabel}`;
4720
+ info +=
4721
+ cacheWaste.missedCost >= 0.0001
4722
+ ? `\n${theme.fg("dim", "Cache Re-billed:")} $${cacheWaste.missedCost.toFixed(3)} ${theme.fg("dim", `(${detail})`)}`
4723
+ : `\n${theme.fg("dim", "Cache Re-billed:")} ${detail}`;
4724
+ }
4493
4725
  }
4494
4726
  this.chatContainer.addChild(new Spacer(1));
4495
4727
  this.chatContainer.addChild(new Text(info, 1, 0));
@@ -4634,11 +4866,7 @@ export class InteractiveMode {
4634
4866
  this.ui.requestRender();
4635
4867
  }
4636
4868
  async handleClearCommand() {
4637
- if (this.loadingAnimation) {
4638
- this.loadingAnimation.stop();
4639
- this.loadingAnimation = undefined;
4640
- }
4641
- this.statusContainer.clear();
4869
+ this.clearStatusIndicator();
4642
4870
  try {
4643
4871
  const result = await this.runtimeHost.newSession();
4644
4872
  if (result.cancelled) {
@@ -4765,11 +4993,7 @@ export class InteractiveMode {
4765
4993
  this.ui.requestRender();
4766
4994
  }
4767
4995
  async handleCompactCommand(customInstructions) {
4768
- if (this.loadingAnimation) {
4769
- this.loadingAnimation.stop();
4770
- this.loadingAnimation = undefined;
4771
- }
4772
- this.statusContainer.clear();
4996
+ this.clearStatusIndicator();
4773
4997
  try {
4774
4998
  await this.session.compact(customInstructions);
4775
4999
  }
@@ -4781,10 +5005,7 @@ export class InteractiveMode {
4781
5005
  if (this.settingsManager.getShowTerminalProgress()) {
4782
5006
  this.ui.terminal.setProgress(false);
4783
5007
  }
4784
- if (this.loadingAnimation) {
4785
- this.loadingAnimation.stop();
4786
- this.loadingAnimation = undefined;
4787
- }
5008
+ this.clearStatusIndicator();
4788
5009
  this.themeController.disableAutoSync();
4789
5010
  this.clearExtensionTerminalInputListeners();
4790
5011
  this.footer.dispose();