@earendil-works/pi-coding-agent 0.80.3 → 0.80.6

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 (172) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +2 -2
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +4 -4
  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/core/agent-session.d.ts +20 -4
  11. package/dist/core/agent-session.d.ts.map +1 -1
  12. package/dist/core/agent-session.js +95 -22
  13. package/dist/core/agent-session.js.map +1 -1
  14. package/dist/core/auth-storage.d.ts.map +1 -1
  15. package/dist/core/auth-storage.js +13 -5
  16. package/dist/core/auth-storage.js.map +1 -1
  17. package/dist/core/cache-stats.d.ts +49 -0
  18. package/dist/core/cache-stats.d.ts.map +1 -0
  19. package/dist/core/cache-stats.js +101 -0
  20. package/dist/core/cache-stats.js.map +1 -0
  21. package/dist/core/compaction/compaction.d.ts +1 -2
  22. package/dist/core/compaction/compaction.d.ts.map +1 -1
  23. package/dist/core/compaction/compaction.js +56 -81
  24. package/dist/core/compaction/compaction.js.map +1 -1
  25. package/dist/core/extensions/index.d.ts +1 -1
  26. package/dist/core/extensions/index.d.ts.map +1 -1
  27. package/dist/core/extensions/index.js.map +1 -1
  28. package/dist/core/extensions/loader.d.ts.map +1 -1
  29. package/dist/core/extensions/loader.js +6 -0
  30. package/dist/core/extensions/loader.js.map +1 -1
  31. package/dist/core/extensions/runner.d.ts +5 -3
  32. package/dist/core/extensions/runner.d.ts.map +1 -1
  33. package/dist/core/extensions/runner.js +38 -0
  34. package/dist/core/extensions/runner.js.map +1 -1
  35. package/dist/core/extensions/types.d.ts +33 -11
  36. package/dist/core/extensions/types.d.ts.map +1 -1
  37. package/dist/core/extensions/types.js.map +1 -1
  38. package/dist/core/index.d.ts +1 -1
  39. package/dist/core/index.d.ts.map +1 -1
  40. package/dist/core/index.js.map +1 -1
  41. package/dist/core/keybindings.d.ts.map +1 -1
  42. package/dist/core/keybindings.js +3 -6
  43. package/dist/core/keybindings.js.map +1 -1
  44. package/dist/core/model-registry.d.ts +4 -6
  45. package/dist/core/model-registry.d.ts.map +1 -1
  46. package/dist/core/model-registry.js +34 -8
  47. package/dist/core/model-registry.js.map +1 -1
  48. package/dist/core/model-resolver.d.ts +10 -0
  49. package/dist/core/model-resolver.d.ts.map +1 -1
  50. package/dist/core/model-resolver.js +14 -17
  51. package/dist/core/model-resolver.js.map +1 -1
  52. package/dist/core/package-manager.d.ts +4 -1
  53. package/dist/core/package-manager.d.ts.map +1 -1
  54. package/dist/core/package-manager.js +68 -26
  55. package/dist/core/package-manager.js.map +1 -1
  56. package/dist/core/provider-attribution.d.ts.map +1 -1
  57. package/dist/core/provider-attribution.js +0 -10
  58. package/dist/core/provider-attribution.js.map +1 -1
  59. package/dist/core/resource-loader.d.ts +2 -2
  60. package/dist/core/resource-loader.d.ts.map +1 -1
  61. package/dist/core/resource-loader.js +6 -7
  62. package/dist/core/resource-loader.js.map +1 -1
  63. package/dist/core/sdk.d.ts +1 -1
  64. package/dist/core/sdk.d.ts.map +1 -1
  65. package/dist/core/sdk.js +8 -1
  66. package/dist/core/sdk.js.map +1 -1
  67. package/dist/core/session-manager.d.ts +20 -1
  68. package/dist/core/session-manager.d.ts.map +1 -1
  69. package/dist/core/session-manager.js +97 -68
  70. package/dist/core/session-manager.js.map +1 -1
  71. package/dist/core/settings-manager.d.ts +9 -3
  72. package/dist/core/settings-manager.d.ts.map +1 -1
  73. package/dist/core/settings-manager.js +10 -1
  74. package/dist/core/settings-manager.js.map +1 -1
  75. package/dist/core/slash-commands.d.ts +1 -0
  76. package/dist/core/slash-commands.d.ts.map +1 -1
  77. package/dist/core/slash-commands.js +3 -3
  78. package/dist/core/slash-commands.js.map +1 -1
  79. package/dist/core/tools/bash.d.ts.map +1 -1
  80. package/dist/core/tools/bash.js +20 -5
  81. package/dist/core/tools/bash.js.map +1 -1
  82. package/dist/core/tools/edit.d.ts.map +1 -1
  83. package/dist/core/tools/edit.js +2 -2
  84. package/dist/core/tools/edit.js.map +1 -1
  85. package/dist/index.d.ts +3 -2
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +2 -1
  88. package/dist/index.js.map +1 -1
  89. package/dist/main.d.ts +2 -2
  90. package/dist/main.d.ts.map +1 -1
  91. package/dist/main.js +1 -0
  92. package/dist/main.js.map +1 -1
  93. package/dist/modes/interactive/components/config-selector.d.ts +34 -3
  94. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  95. package/dist/modes/interactive/components/config-selector.js +280 -25
  96. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  97. package/dist/modes/interactive/components/custom-entry.d.ts +19 -0
  98. package/dist/modes/interactive/components/custom-entry.d.ts.map +1 -0
  99. package/dist/modes/interactive/components/custom-entry.js +52 -0
  100. package/dist/modes/interactive/components/custom-entry.js.map +1 -0
  101. package/dist/modes/interactive/components/footer.d.ts +4 -0
  102. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  103. package/dist/modes/interactive/components/footer.js +1 -1
  104. package/dist/modes/interactive/components/footer.js.map +1 -1
  105. package/dist/modes/interactive/components/oauth-selector.d.ts +3 -1
  106. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  107. package/dist/modes/interactive/components/oauth-selector.js +17 -6
  108. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  109. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  110. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  111. package/dist/modes/interactive/components/settings-selector.js +12 -1
  112. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  113. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  114. package/dist/modes/interactive/components/thinking-selector.js +2 -1
  115. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  116. package/dist/modes/interactive/interactive-mode.d.ts +15 -3
  117. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  118. package/dist/modes/interactive/interactive-mode.js +302 -61
  119. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  120. package/dist/modes/interactive/theme/dark.json +1 -0
  121. package/dist/modes/interactive/theme/light.json +1 -0
  122. package/dist/modes/interactive/theme/theme-schema.json +6 -2
  123. package/dist/modes/interactive/theme/theme.d.ts +3 -2
  124. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  125. package/dist/modes/interactive/theme/theme.js +10 -3
  126. package/dist/modes/interactive/theme/theme.js.map +1 -1
  127. package/dist/modes/print-mode.d.ts.map +1 -1
  128. package/dist/modes/print-mode.js +1 -1
  129. package/dist/modes/print-mode.js.map +1 -1
  130. package/dist/modes/rpc/rpc-client.d.ts +6 -6
  131. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  132. package/dist/modes/rpc/rpc-client.js +3 -3
  133. package/dist/modes/rpc/rpc-client.js.map +1 -1
  134. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  135. package/dist/modes/rpc/rpc-mode.js +4 -1
  136. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  137. package/dist/package-manager-cli.d.ts +2 -2
  138. package/dist/package-manager-cli.d.ts.map +1 -1
  139. package/dist/package-manager-cli.js +71 -17
  140. package/dist/package-manager-cli.js.map +1 -1
  141. package/dist/utils/clipboard-image.d.ts.map +1 -1
  142. package/dist/utils/clipboard-image.js +1 -1
  143. package/dist/utils/clipboard-image.js.map +1 -1
  144. package/docs/custom-provider.md +4 -3
  145. package/docs/extensions.md +75 -18
  146. package/docs/models.md +49 -8
  147. package/docs/packages.md +2 -2
  148. package/docs/rpc.md +15 -5
  149. package/docs/sdk.md +51 -1
  150. package/docs/session-format.md +18 -8
  151. package/docs/settings.md +3 -2
  152. package/docs/themes.md +5 -3
  153. package/docs/tui.md +1 -1
  154. package/docs/usage.md +2 -2
  155. package/examples/extensions/README.md +1 -0
  156. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  157. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  158. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  159. package/examples/extensions/entry-renderer.ts +41 -0
  160. package/examples/extensions/gondolin/package-lock.json +2 -2
  161. package/examples/extensions/gondolin/package.json +1 -1
  162. package/examples/extensions/preset.ts +2 -2
  163. package/examples/extensions/question.ts +1 -0
  164. package/examples/extensions/reload-runtime.ts +2 -2
  165. package/examples/extensions/sandbox/package-lock.json +2 -2
  166. package/examples/extensions/sandbox/package.json +1 -1
  167. package/examples/extensions/with-deps/package-lock.json +2 -2
  168. package/examples/extensions/with-deps/package.json +1 -1
  169. package/examples/sdk/12-full-control.ts +1 -1
  170. package/examples/sdk/README.md +1 -1
  171. package/npm-shrinkwrap.json +12 -12
  172. package/package.json +4 -4
@@ -13,6 +13,7 @@ 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";
@@ -41,6 +42,7 @@ 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
44
  import { CustomEditor } from "./components/custom-editor.js";
45
+ import { CustomEntryComponent } from "./components/custom-entry.js";
44
46
  import { CustomMessageComponent } from "./components/custom-message.js";
45
47
  import { DaxnutsComponent } from "./components/daxnuts.js";
46
48
  import { DynamicBorder } from "./components/dynamic-border.js";
@@ -48,11 +50,11 @@ import { EarendilAnnouncementComponent } from "./components/earendil-announcemen
48
50
  import { ExtensionEditorComponent } from "./components/extension-editor.js";
49
51
  import { ExtensionInputComponent } from "./components/extension-input.js";
50
52
  import { ExtensionSelectorComponent } from "./components/extension-selector.js";
51
- import { FooterComponent } from "./components/footer.js";
53
+ import { FooterComponent, formatTokens } from "./components/footer.js";
52
54
  import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
53
55
  import { LoginDialogComponent } from "./components/login-dialog.js";
54
56
  import { ModelSelectorComponent } from "./components/model-selector.js";
55
- import { OAuthSelectorComponent } from "./components/oauth-selector.js";
57
+ import { formatAuthSelectorProviderType, OAuthSelectorComponent, } from "./components/oauth-selector.js";
56
58
  import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js";
57
59
  import { SessionSelectorComponent } from "./components/session-selector.js";
58
60
  import { SettingsSelectorComponent } from "./components/settings-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,6 +136,42 @@ 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;
@@ -317,6 +358,7 @@ export class InteractiveMode {
317
358
  const slashCommands = BUILTIN_SLASH_COMMANDS.map((command) => ({
318
359
  name: command.name,
319
360
  description: command.description,
361
+ ...(command.argumentHint && { argumentHint: command.argumentHint }),
320
362
  }));
321
363
  const modelCommand = slashCommands.find((command) => command.name === "model");
322
364
  if (modelCommand) {
@@ -334,17 +376,24 @@ export class InteractiveMode {
334
376
  name: m.name,
335
377
  label: `${m.provider}/${m.id}`,
336
378
  }));
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) => ({
379
+ return createFuzzyAutocompleteItems(items, prefix, getModelSearchText, (item) => ({
342
380
  value: item.label,
343
381
  label: item.id,
344
382
  description: item.provider,
345
383
  }));
346
384
  };
347
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
+ }
348
397
  // Convert prompt templates to SlashCommand format for autocomplete
349
398
  const templateCommands = this.session.promptTemplates.map((cmd) => ({
350
399
  name: cmd.name,
@@ -1155,7 +1204,7 @@ export class InteractiveMode {
1155
1204
  this.restoreQueuedMessagesToEditor({ abort: true });
1156
1205
  },
1157
1206
  commandContextActions: {
1158
- waitForIdle: () => this.session.agent.waitForIdle(),
1207
+ waitForIdle: () => this.session.waitForIdle(),
1159
1208
  newSession: async (options) => {
1160
1209
  this.clearStatusIndicator();
1161
1210
  try {
@@ -1206,7 +1255,7 @@ export class InteractiveMode {
1206
1255
  },
1207
1256
  shutdownHandler: () => {
1208
1257
  this.shutdownRequested = true;
1209
- if (!this.session.isStreaming) {
1258
+ if (this.session.isIdle) {
1210
1259
  void this.shutdown();
1211
1260
  }
1212
1261
  },
@@ -1299,7 +1348,7 @@ export class InteractiveMode {
1299
1348
  sessionManager: this.sessionManager,
1300
1349
  modelRegistry: this.session.modelRegistry,
1301
1350
  model: this.session.model,
1302
- isIdle: () => !this.session.isStreaming,
1351
+ isIdle: () => this.session.isIdle,
1303
1352
  isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
1304
1353
  signal: this.session.agent.signal,
1305
1354
  abort: () => {
@@ -2105,9 +2154,10 @@ export class InteractiveMode {
2105
2154
  this.editor.setText("");
2106
2155
  return;
2107
2156
  }
2108
- if (text === "/login") {
2109
- this.showOAuthSelector("login");
2157
+ if (text === "/login" || text.startsWith("/login ")) {
2158
+ const providerRef = text.startsWith("/login ") ? text.slice(7).trim() : undefined;
2110
2159
  this.editor.setText("");
2160
+ await this.handleLoginCommand(providerRef);
2111
2161
  return;
2112
2162
  }
2113
2163
  if (text === "/logout") {
@@ -2241,6 +2291,12 @@ export class InteractiveMode {
2241
2291
  this.updatePendingMessagesDisplay();
2242
2292
  this.ui.requestRender();
2243
2293
  break;
2294
+ case "entry_appended":
2295
+ if (event.entry.type === "custom") {
2296
+ this.addCustomEntryToChat(event.entry);
2297
+ this.ui.requestRender();
2298
+ }
2299
+ break;
2244
2300
  case "session_info_changed":
2245
2301
  this.updateTerminalTitle();
2246
2302
  this.footer.invalidate();
@@ -2326,6 +2382,7 @@ export class InteractiveMode {
2326
2382
  for (const [, component] of this.pendingTools.entries()) {
2327
2383
  component.setArgsComplete();
2328
2384
  }
2385
+ this.maybeShowCacheMissNotice(this.streamingMessage);
2329
2386
  }
2330
2387
  this.streamingComponent = undefined;
2331
2388
  this.streamingMessage = undefined;
@@ -2376,9 +2433,11 @@ export class InteractiveMode {
2376
2433
  this.streamingMessage = undefined;
2377
2434
  }
2378
2435
  this.pendingTools.clear();
2379
- await this.checkShutdownRequested();
2380
2436
  this.ui.requestRender();
2381
2437
  break;
2438
+ case "agent_settled":
2439
+ await this.checkShutdownRequested();
2440
+ break;
2382
2441
  case "compaction_start": {
2383
2442
  if (this.settingsManager.getShowTerminalProgress()) {
2384
2443
  this.ui.terminal.setProgress(true);
@@ -2486,6 +2545,25 @@ export class InteractiveMode {
2486
2545
  this.lastStatusText = text;
2487
2546
  this.ui.requestRender();
2488
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
+ }
2489
2567
  addMessageToChat(message, options) {
2490
2568
  switch (message.role) {
2491
2569
  case "bashExecution": {
@@ -2563,20 +2641,24 @@ export class InteractiveMode {
2563
2641
  }
2564
2642
  }
2565
2643
  }
2566
- /**
2567
- * Render session context to chat. Used for initial load and rebuild after compaction.
2568
- * @param sessionContext Session context to render
2569
- * @param options.updateFooter Update footer state
2570
- * @param options.populateHistory Add user messages to editor history
2571
- */
2572
- renderSessionContext(sessionContext, options = {}) {
2644
+ renderSessionItems(items, options = {}) {
2573
2645
  this.pendingTools.clear();
2574
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();
2575
2652
  if (options.updateFooter) {
2576
2653
  this.footer.invalidate();
2577
2654
  this.updateEditorBorderColor();
2578
2655
  }
2579
- 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;
2580
2662
  // Assistant messages need special handling for tool calls
2581
2663
  if (message.role === "assistant") {
2582
2664
  this.addMessageToChat(message);
@@ -2608,6 +2690,11 @@ export class InteractiveMode {
2608
2690
  }
2609
2691
  }
2610
2692
  }
2693
+ if (message.stopReason !== "aborted" && message.stopReason !== "error") {
2694
+ const miss = cacheMisses.get(message);
2695
+ if (miss)
2696
+ this.addCacheMissNotice(miss);
2697
+ }
2611
2698
  }
2612
2699
  else if (message.role === "toolResult") {
2613
2700
  // Match tool results to pending tool components
@@ -2627,10 +2714,53 @@ export class InteractiveMode {
2627
2714
  }
2628
2715
  this.ui.requestRender();
2629
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
+ }
2630
2761
  renderInitialMessages() {
2631
- // Get aligned messages and entries from session context
2632
- const context = this.sessionManager.buildSessionContext();
2633
- this.renderSessionContext(context, {
2762
+ const entries = this.sessionManager.buildContextEntries();
2763
+ this.renderSessionEntries(entries, {
2634
2764
  updateFooter: true,
2635
2765
  populateHistory: true,
2636
2766
  });
@@ -2666,8 +2796,7 @@ export class InteractiveMode {
2666
2796
  }
2667
2797
  rebuildChatFromMessages() {
2668
2798
  this.chatContainer.clear();
2669
- const context = this.sessionManager.buildSessionContext();
2670
- this.renderSessionContext(context);
2799
+ this.renderSessionEntries(this.sessionManager.buildContextEntries());
2671
2800
  }
2672
2801
  // =========================================================================
2673
2802
  // Key handlers
@@ -3266,6 +3395,7 @@ export class InteractiveMode {
3266
3395
  doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
3267
3396
  treeFilterMode: this.settingsManager.getTreeFilterMode(),
3268
3397
  showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
3398
+ showCacheMissNotices: this.settingsManager.getShowCacheMissNotices(),
3269
3399
  defaultProjectTrust: this.settingsManager.getDefaultProjectTrust(),
3270
3400
  editorPaddingX: this.settingsManager.getEditorPaddingX(),
3271
3401
  outputPad: this.settingsManager.getOutputPad(),
@@ -3341,6 +3471,10 @@ export class InteractiveMode {
3341
3471
  this.chatContainer.clear();
3342
3472
  this.rebuildChatFromMessages();
3343
3473
  },
3474
+ onShowCacheMissNoticesChange: (shown) => {
3475
+ this.settingsManager.setShowCacheMissNotices(shown);
3476
+ this.rebuildChatFromMessages();
3477
+ },
3344
3478
  onCollapseChangelogChange: (collapsed) => {
3345
3479
  this.settingsManager.setCollapseChangelog(collapsed);
3346
3480
  },
@@ -3630,19 +3764,17 @@ export class InteractiveMode {
3630
3764
  const initialSelectedId = userMessages[userMessages.length - 1]?.entryId;
3631
3765
  this.showSelector((done) => {
3632
3766
  const selector = new UserMessageSelectorComponent(userMessages.map((m) => ({ id: m.entryId, text: m.text })), async (entryId) => {
3767
+ done();
3633
3768
  try {
3634
3769
  const result = await this.runtimeHost.fork(entryId);
3635
3770
  if (result.cancelled) {
3636
- done();
3637
3771
  this.ui.requestRender();
3638
3772
  return;
3639
3773
  }
3640
3774
  this.editor.setText(result.selectedText ?? "");
3641
- done();
3642
3775
  this.showStatus("Forked to new session");
3643
3776
  }
3644
3777
  catch (error) {
3645
- done();
3646
3778
  this.showError(error instanceof Error ? error.message : String(error));
3647
3779
  }
3648
3780
  }, () => {
@@ -3871,13 +4003,82 @@ export class InteractiveMode {
3871
4003
  }
3872
4004
  return options.sort((a, b) => a.name.localeCompare(b.name));
3873
4005
  }
3874
- 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) {
3875
4045
  const subscriptionLabel = "Use a subscription";
3876
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:";
3877
4071
  this.showSelector((done) => {
3878
- const selector = new ExtensionSelectorComponent("Select authentication method:", [subscriptionLabel, apiKeyLabel], (option) => {
4072
+ const selector = new ExtensionSelectorComponent(title, options, (option) => {
3879
4073
  done();
3880
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
+ }
3881
4082
  this.showLoginProviderSelector(authType);
3882
4083
  }, () => {
3883
4084
  done();
@@ -3886,32 +4087,34 @@ export class InteractiveMode {
3886
4087
  return { component: selector, focus: selector };
3887
4088
  });
3888
4089
  }
3889
- showLoginProviderSelector(authType) {
4090
+ showLoginProviderSelector(authType, initialSearchInput) {
3890
4091
  const providerOptions = this.getLoginProviderOptions(authType);
3891
4092
  if (providerOptions.length === 0) {
3892
- 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);
3893
4099
  return;
3894
4100
  }
3895
4101
  this.showSelector((done) => {
3896
- 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) => {
3897
4103
  done();
3898
- const providerOption = providerOptions.find((provider) => provider.id === providerId);
4104
+ const providerOption = providerOptions.find((provider) => provider.id === providerId && provider.authType === selectedAuthType);
3899
4105
  if (!providerOption) {
3900
4106
  return;
3901
4107
  }
3902
- if (providerOption.authType === "oauth") {
3903
- await this.showLoginDialog(providerOption.id, providerOption.name);
3904
- }
3905
- else if (providerOption.id === BEDROCK_PROVIDER_ID) {
3906
- this.showBedrockSetupDialog(providerOption.id, providerOption.name);
4108
+ await this.startProviderLogin(providerOption);
4109
+ }, () => {
4110
+ done();
4111
+ if (authType) {
4112
+ this.showLoginAuthTypeSelector();
3907
4113
  }
3908
4114
  else {
3909
- await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
4115
+ this.ui.requestRender();
3910
4116
  }
3911
- }, () => {
3912
- done();
3913
- this.showLoginAuthTypeSelector();
3914
- }, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId));
4117
+ }, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId), initialSearchInput);
3915
4118
  return { component: selector, focus: selector };
3916
4119
  });
3917
4120
  }
@@ -4170,7 +4373,7 @@ export class InteractiveMode {
4170
4373
  const borderColor = (s) => theme.fg("border", s);
4171
4374
  reloadBox.addChild(new DynamicBorder(borderColor));
4172
4375
  reloadBox.addChild(new Spacer(1));
4173
- 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));
4174
4377
  reloadBox.addChild(new Spacer(1));
4175
4378
  reloadBox.addChild(new DynamicBorder(borderColor));
4176
4379
  const previousEditor = this.editor;
@@ -4234,8 +4437,8 @@ export class InteractiveMode {
4234
4437
  this.showError(`models.json error: ${modelsJsonError}`);
4235
4438
  }
4236
4439
  this.showStatus(savedImplicitProjectTrust
4237
- ? "Reloaded keybindings, extensions, skills, prompts, themes; saved project trust"
4238
- : "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");
4239
4442
  dismissReloadBox(this.editor);
4240
4443
  reloadBoxDismissed = true;
4241
4444
  }
@@ -4456,6 +4659,26 @@ export class InteractiveMode {
4456
4659
  handleSessionCommand() {
4457
4660
  const stats = this.session.getSessionStats();
4458
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);
4459
4682
  let info = `${theme.bold("Session Info")}\n\n`;
4460
4683
  if (sessionName) {
4461
4684
  info += `${theme.fg("dim", "Name:")} ${sessionName}\n`;
@@ -4463,24 +4686,42 @@ export class InteractiveMode {
4463
4686
  info += `${theme.fg("dim", "File:")} ${stats.sessionFile ?? "In-memory"}\n`;
4464
4687
  info += `${theme.fg("dim", "ID:")} ${stats.sessionId}\n\n`;
4465
4688
  info += `${theme.bold("Messages")}\n`;
4689
+ info += `${theme.fg("dim", "Total:")} ${stats.totalMessages}\n`;
4466
4690
  info += `${theme.fg("dim", "User:")} ${stats.userMessages}\n`;
4467
4691
  info += `${theme.fg("dim", "Assistant:")} ${stats.assistantMessages}\n`;
4468
- info += `${theme.fg("dim", "Tool Calls:")} ${stats.toolCalls}\n`;
4469
- info += `${theme.fg("dim", "Tool Results:")} ${stats.toolResults}\n`;
4470
- info += `${theme.fg("dim", "Total:")} ${stats.totalMessages}\n\n`;
4692
+ info += `${theme.fg("dim", "Tools:")} ${stats.toolCalls} calls, ${stats.toolResults} results\n\n`;
4471
4693
  info += `${theme.bold("Tokens")}\n`;
4472
- info += `${theme.fg("dim", "Input:")} ${stats.tokens.input.toLocaleString()}\n`;
4473
- info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`;
4474
- if (stats.tokens.cacheRead > 0) {
4475
- info += `${theme.fg("dim", "Cache Read:")} ${stats.tokens.cacheRead.toLocaleString()}\n`;
4476
- }
4477
- if (stats.tokens.cacheWrite > 0) {
4478
- 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`;
4479
4706
  }
4707
+ info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`;
4480
4708
  info += `${theme.fg("dim", "Total:")} ${stats.tokens.total.toLocaleString()}\n`;
4481
- if (stats.cost > 0) {
4709
+ if (stats.cost > 0 || cacheWaste.missedTokens > 0) {
4482
4710
  info += `\n${theme.bold("Cost")}\n`;
4483
- 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
+ }
4484
4725
  }
4485
4726
  this.chatContainer.addChild(new Spacer(1));
4486
4727
  this.chatContainer.addChild(new Text(info, 1, 0));