@code-yeongyu/senpi 2026.7.9 → 2026.7.10

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 (207) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/README.md +1 -1
  3. package/dist/cli/args.js +3 -3
  4. package/dist/cli/args.js.map +1 -1
  5. package/dist/cli/config-selector.d.ts +4 -2
  6. package/dist/cli/config-selector.d.ts.map +1 -1
  7. package/dist/cli/config-selector.js +1 -1
  8. package/dist/cli/config-selector.js.map +1 -1
  9. package/dist/core/agent-session.d.ts +18 -3
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +153 -36
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/cache-stats.d.ts +49 -0
  14. package/dist/core/cache-stats.d.ts.map +1 -0
  15. package/dist/core/cache-stats.js +101 -0
  16. package/dist/core/cache-stats.js.map +1 -0
  17. package/dist/core/compaction/compaction.d.ts +1 -2
  18. package/dist/core/compaction/compaction.d.ts.map +1 -1
  19. package/dist/core/compaction/compaction.js +54 -79
  20. package/dist/core/compaction/compaction.js.map +1 -1
  21. package/dist/core/extensions/builtin/goal/continuation.js +1 -1
  22. package/dist/core/extensions/builtin/goal/continuation.js.map +1 -1
  23. package/dist/core/extensions/builtin/goal/index.d.ts.map +1 -1
  24. package/dist/core/extensions/builtin/goal/index.js +1 -0
  25. package/dist/core/extensions/builtin/goal/index.js.map +1 -1
  26. package/dist/core/extensions/builtin/import-repro.d.ts.map +1 -1
  27. package/dist/core/extensions/builtin/import-repro.js +100 -6
  28. package/dist/core/extensions/builtin/import-repro.js.map +1 -1
  29. package/dist/core/extensions/builtin/mcp/connection.d.ts +1 -0
  30. package/dist/core/extensions/builtin/mcp/connection.d.ts.map +1 -1
  31. package/dist/core/extensions/builtin/mcp/connection.js +15 -0
  32. package/dist/core/extensions/builtin/mcp/connection.js.map +1 -1
  33. package/dist/core/extensions/builtin/mcp/service.d.ts.map +1 -1
  34. package/dist/core/extensions/builtin/mcp/service.js +3 -1
  35. package/dist/core/extensions/builtin/mcp/service.js.map +1 -1
  36. package/dist/core/extensions/index.d.ts +1 -1
  37. package/dist/core/extensions/index.d.ts.map +1 -1
  38. package/dist/core/extensions/index.js.map +1 -1
  39. package/dist/core/extensions/runner.d.ts +4 -3
  40. package/dist/core/extensions/runner.d.ts.map +1 -1
  41. package/dist/core/extensions/runner.js +30 -24
  42. package/dist/core/extensions/runner.js.map +1 -1
  43. package/dist/core/extensions/types.d.ts +23 -3
  44. package/dist/core/extensions/types.d.ts.map +1 -1
  45. package/dist/core/extensions/types.js.map +1 -1
  46. package/dist/core/model-registry.d.ts +3 -0
  47. package/dist/core/model-registry.d.ts.map +1 -1
  48. package/dist/core/model-registry.js +20 -3
  49. package/dist/core/model-registry.js.map +1 -1
  50. package/dist/core/package-manager.d.ts +4 -1
  51. package/dist/core/package-manager.d.ts.map +1 -1
  52. package/dist/core/package-manager.js +68 -26
  53. package/dist/core/package-manager.js.map +1 -1
  54. package/dist/core/resource-loader.d.ts +2 -2
  55. package/dist/core/resource-loader.d.ts.map +1 -1
  56. package/dist/core/resource-loader.js +6 -7
  57. package/dist/core/resource-loader.js.map +1 -1
  58. package/dist/core/sdk.d.ts +1 -1
  59. package/dist/core/sdk.d.ts.map +1 -1
  60. package/dist/core/sdk.js +9 -2
  61. package/dist/core/sdk.js.map +1 -1
  62. package/dist/core/session-manager.d.ts.map +1 -1
  63. package/dist/core/session-manager.js +11 -2
  64. package/dist/core/session-manager.js.map +1 -1
  65. package/dist/core/settings-manager.d.ts +5 -0
  66. package/dist/core/settings-manager.d.ts.map +1 -1
  67. package/dist/core/settings-manager.js +8 -0
  68. package/dist/core/settings-manager.js.map +1 -1
  69. package/dist/core/slash-commands.d.ts +1 -0
  70. package/dist/core/slash-commands.d.ts.map +1 -1
  71. package/dist/core/slash-commands.js +3 -3
  72. package/dist/core/slash-commands.js.map +1 -1
  73. package/dist/index.d.ts +1 -1
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js.map +1 -1
  76. package/dist/main.d.ts +2 -2
  77. package/dist/main.d.ts.map +1 -1
  78. package/dist/main.js +1 -0
  79. package/dist/main.js.map +1 -1
  80. package/dist/modes/interactive/components/config-selector.d.ts +34 -3
  81. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  82. package/dist/modes/interactive/components/config-selector.js +272 -24
  83. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  84. package/dist/modes/interactive/components/footer.d.ts +4 -0
  85. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  86. package/dist/modes/interactive/components/footer.js +5 -2
  87. package/dist/modes/interactive/components/footer.js.map +1 -1
  88. package/dist/modes/interactive/components/oauth-selector.d.ts +3 -1
  89. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  90. package/dist/modes/interactive/components/oauth-selector.js +16 -6
  91. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  92. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  93. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  94. package/dist/modes/interactive/components/settings-selector.js +10 -0
  95. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  96. package/dist/modes/interactive/interactive-mode.d.ts +10 -0
  97. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  98. package/dist/modes/interactive/interactive-mode.js +261 -37
  99. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  100. package/dist/modes/print-mode.js +1 -1
  101. package/dist/modes/print-mode.js.map +1 -1
  102. package/dist/modes/rpc/rpc-client.d.ts +6 -6
  103. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  104. package/dist/modes/rpc/rpc-client.js +3 -3
  105. package/dist/modes/rpc/rpc-client.js.map +1 -1
  106. package/dist/package-manager-cli.d.ts +2 -2
  107. package/dist/package-manager-cli.d.ts.map +1 -1
  108. package/dist/package-manager-cli.js +64 -17
  109. package/dist/package-manager-cli.js.map +1 -1
  110. package/dist/utils/clipboard-image.js +1 -1
  111. package/dist/utils/clipboard-image.js.map +1 -1
  112. package/docs/extensions.md +33 -9
  113. package/docs/models.md +4 -4
  114. package/docs/packages.md +7 -10
  115. package/docs/rpc.md +13 -3
  116. package/docs/sdk.md +22 -0
  117. package/docs/settings.md +1 -0
  118. package/docs/usage.md +1 -1
  119. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  120. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  121. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  122. package/examples/extensions/gondolin/package-lock.json +2 -2
  123. package/examples/extensions/gondolin/package.json +1 -1
  124. package/examples/extensions/reload-runtime.ts +2 -2
  125. package/examples/extensions/sandbox/package-lock.json +2 -2
  126. package/examples/extensions/sandbox/package.json +1 -1
  127. package/examples/extensions/with-deps/package-lock.json +2 -2
  128. package/examples/extensions/with-deps/package.json +1 -1
  129. package/examples/sdk/README.md +1 -1
  130. package/node_modules/@code-yeongyu/senpi-codemode/package.json +3 -3
  131. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  132. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js +37 -2
  133. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js.map +1 -1
  134. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/jsonl-repo.d.ts.map +1 -1
  135. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/jsonl-repo.js +2 -0
  136. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/jsonl-repo.js.map +1 -1
  137. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/jsonl-storage.d.ts +1 -0
  138. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/jsonl-storage.d.ts.map +1 -1
  139. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/jsonl-storage.js +7 -0
  140. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/jsonl-storage.js.map +1 -1
  141. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/session.d.ts +18 -4
  142. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/session.d.ts.map +1 -1
  143. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/session.js +72 -37
  144. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/session.js.map +1 -1
  145. package/node_modules/@earendil-works/pi-agent-core/dist/harness/types.d.ts +2 -0
  146. package/node_modules/@earendil-works/pi-agent-core/dist/harness/types.d.ts.map +1 -1
  147. package/node_modules/@earendil-works/pi-agent-core/dist/harness/types.js.map +1 -1
  148. package/node_modules/@earendil-works/pi-agent-core/dist/index.d.ts +2 -0
  149. package/node_modules/@earendil-works/pi-agent-core/dist/index.d.ts.map +1 -1
  150. package/node_modules/@earendil-works/pi-agent-core/dist/index.js +2 -0
  151. package/node_modules/@earendil-works/pi-agent-core/dist/index.js.map +1 -1
  152. package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
  153. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.d.ts.map +1 -1
  154. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js +2 -1
  155. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js.map +1 -1
  156. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.js +1 -1
  157. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.js.map +1 -1
  158. package/node_modules/@earendil-works/pi-ai/dist/api/transform-messages.d.ts.map +1 -1
  159. package/node_modules/@earendil-works/pi-ai/dist/api/transform-messages.js +4 -1
  160. package/node_modules/@earendil-works/pi-ai/dist/api/transform-messages.js.map +1 -1
  161. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts +446 -195
  162. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts.map +1 -1
  163. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.models.d.ts +84 -0
  164. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.models.d.ts.map +1 -1
  165. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.models.js +72 -0
  166. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.models.js.map +1 -1
  167. package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.js +4 -4
  168. package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.js.map +1 -1
  169. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex.models.d.ts +63 -0
  170. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex.models.d.ts.map +1 -1
  171. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex.models.js +54 -0
  172. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex.models.js.map +1 -1
  173. package/node_modules/@earendil-works/pi-ai/dist/providers/openai.models.d.ts +84 -0
  174. package/node_modules/@earendil-works/pi-ai/dist/providers/openai.models.d.ts.map +1 -1
  175. package/node_modules/@earendil-works/pi-ai/dist/providers/openai.models.js +72 -0
  176. package/node_modules/@earendil-works/pi-ai/dist/providers/openai.models.js.map +1 -1
  177. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.d.ts +138 -63
  178. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.d.ts.map +1 -1
  179. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js +136 -76
  180. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js.map +1 -1
  181. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.d.ts +77 -0
  182. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.d.ts.map +1 -1
  183. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.js +72 -1
  184. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.js.map +1 -1
  185. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-ams.models.d.ts +0 -44
  186. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-ams.models.d.ts.map +1 -1
  187. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-ams.models.js +9 -45
  188. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-ams.models.js.map +1 -1
  189. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-cn.models.d.ts +0 -44
  190. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-cn.models.d.ts.map +1 -1
  191. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-cn.models.js +9 -45
  192. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-cn.models.js.map +1 -1
  193. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-sgp.models.d.ts +0 -44
  194. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-sgp.models.d.ts.map +1 -1
  195. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-sgp.models.js +9 -45
  196. package/node_modules/@earendil-works/pi-ai/dist/providers/xiaomi-token-plan-sgp.models.js.map +1 -1
  197. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.d.ts.map +1 -1
  198. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js +3 -0
  199. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js.map +1 -1
  200. package/node_modules/@earendil-works/pi-ai/package.json +1 -1
  201. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  202. package/node_modules/@earendil-works/pi-tui/dist/components/editor.d.ts.map +1 -1
  203. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js +22 -1
  204. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js.map +1 -1
  205. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  206. package/package.json +7 -8
  207. package/npm-shrinkwrap.json +0 -3479
@@ -13,6 +13,7 @@ import { APP_NAME, APP_TITLE, CONFIG_DIR_NAME, expandTildePath, getAuthPath, get
13
13
  import { parseSkillBlock } from "../../core/agent-session.js";
14
14
  import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
15
15
  import { isApiKeyLoginProvider } from "../../core/auth-providers.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 { appendHiddenTuiStdout } from "../../core/hidden-stdout-log.js";
18
19
  import { configureHttpDispatcher, formatHttpIdleTimeoutMs } from "../../core/http-dispatcher.js";
@@ -49,12 +50,12 @@ import { ExtensionEditorComponent } from "./components/extension-editor.js";
49
50
  import { ExtensionInputComponent } from "./components/extension-input.js";
50
51
  import { ExtensionSelectorComponent } from "./components/extension-selector.js";
51
52
  import { FavoriteModelsSelectorComponent } from "./components/favorite-models-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 { getModelFullId } from "./components/model-favorites.js";
56
57
  import { ModelSelectorComponent } from "./components/model-selector.js";
57
- import { OAuthSelectorComponent } from "./components/oauth-selector.js";
58
+ import { formatAuthSelectorProviderType, OAuthSelectorComponent, } from "./components/oauth-selector.js";
58
59
  import { 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";
@@ -66,7 +67,6 @@ import { UserMessageComponent } from "./components/user-message.js";
66
67
  import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
67
68
  import { restoreInteractiveStderr, takeOverInteractiveStderr } from "./interactive-stderr-guard.js";
68
69
  import { getModelSearchText } from "./model-search.js";
69
- import { formatSessionInfo } from "./session-info-format.js";
70
70
  import { resolveStartupToolPaths } from "./startup-tools.js";
71
71
  import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, onThemeChange, setRegisteredThemes, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
72
72
  import { InteractiveThemeController } from "./theme/theme-controller.js";
@@ -179,6 +179,42 @@ const BEDROCK_PROVIDER_ID = "amazon-bedrock";
179
179
  // keep the existing public import (test/oauth-selector.test.ts) working, and used
180
180
  // locally by getLoginProviderOptions below.
181
181
  export { isApiKeyLoginProvider };
182
+ const AUTH_TYPE_ORDER = { oauth: 0, api_key: 1 };
183
+ function createFuzzyAutocompleteItems(items, prefix, getSearchText, toAutocompleteItem) {
184
+ const filtered = fuzzyFilter(items, prefix, getSearchText);
185
+ if (filtered.length === 0)
186
+ return null;
187
+ return filtered.map(toAutocompleteItem);
188
+ }
189
+ function getLoginProviderCompletionOptions(providerOptions) {
190
+ const byId = new Map();
191
+ for (const provider of providerOptions) {
192
+ const existing = byId.get(provider.id);
193
+ if (existing) {
194
+ if (!existing.authTypes.includes(provider.authType)) {
195
+ existing.authTypes.push(provider.authType);
196
+ existing.authTypes.sort((a, b) => AUTH_TYPE_ORDER[a] - AUTH_TYPE_ORDER[b]);
197
+ }
198
+ continue;
199
+ }
200
+ byId.set(provider.id, {
201
+ id: provider.id,
202
+ name: provider.name,
203
+ authTypes: [provider.authType],
204
+ });
205
+ }
206
+ return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name));
207
+ }
208
+ function getLoginProviderSearchText(provider) {
209
+ const authTypes = provider.authTypes
210
+ .map((authType) => `${authType} ${formatAuthSelectorProviderType(authType)}`)
211
+ .join(" ");
212
+ return `${provider.id} ${provider.name} ${authTypes}`;
213
+ }
214
+ function formatLoginProviderCompletionDescription(provider) {
215
+ const authTypes = provider.authTypes.map(formatAuthSelectorProviderType).join("/");
216
+ return provider.name === provider.id ? authTypes : `${provider.name} · ${authTypes}`;
217
+ }
182
218
  export class InteractiveMode {
183
219
  requestStreamingRender() {
184
220
  this.ui.requestRender();
@@ -350,6 +386,7 @@ export class InteractiveMode {
350
386
  const slashCommands = BUILTIN_SLASH_COMMANDS.map((command) => ({
351
387
  name: command.name,
352
388
  description: command.description,
389
+ ...(command.argumentHint && { argumentHint: command.argumentHint }),
353
390
  }));
354
391
  const modelCommand = slashCommands.find((command) => command.name === "model");
355
392
  if (modelCommand) {
@@ -367,17 +404,24 @@ export class InteractiveMode {
367
404
  name: m.name,
368
405
  label: `${m.provider}/${m.id}`,
369
406
  }));
370
- // Fuzzy filter by model ID + provider in either order.
371
- const filtered = fuzzyFilter(items, prefix, getModelSearchText);
372
- if (filtered.length === 0)
373
- return null;
374
- return filtered.map((item) => ({
407
+ return createFuzzyAutocompleteItems(items, prefix, getModelSearchText, (item) => ({
375
408
  value: item.label,
376
409
  label: item.id,
377
410
  description: item.provider,
378
411
  }));
379
412
  };
380
413
  }
414
+ const loginCommand = slashCommands.find((command) => command.name === "login");
415
+ if (loginCommand) {
416
+ loginCommand.getArgumentCompletions = (prefix) => {
417
+ const providers = getLoginProviderCompletionOptions(this.getLoginProviderOptions());
418
+ return createFuzzyAutocompleteItems(providers, prefix, getLoginProviderSearchText, (provider) => ({
419
+ value: provider.id,
420
+ label: provider.id,
421
+ description: formatLoginProviderCompletionDescription(provider),
422
+ }));
423
+ };
424
+ }
381
425
  // Convert prompt templates to SlashCommand format for autocomplete
382
426
  const templateCommands = this.session.promptTemplates.map((cmd) => ({
383
427
  name: cmd.name,
@@ -1205,7 +1249,7 @@ export class InteractiveMode {
1205
1249
  this.restoreQueuedMessagesToEditor({ abort: true });
1206
1250
  },
1207
1251
  commandContextActions: {
1208
- waitForIdle: () => this.session.agent.waitForIdle(),
1252
+ waitForIdle: () => this.session.waitForIdle(),
1209
1253
  newSession: async (options) => {
1210
1254
  this.clearStatusIndicator();
1211
1255
  try {
@@ -1256,7 +1300,7 @@ export class InteractiveMode {
1256
1300
  },
1257
1301
  shutdownHandler: () => {
1258
1302
  this.shutdownRequested = true;
1259
- if (!this.session.isStreaming) {
1303
+ if (this.session.isIdle) {
1260
1304
  void this.shutdown();
1261
1305
  }
1262
1306
  },
@@ -1351,7 +1395,7 @@ export class InteractiveMode {
1351
1395
  modelRegistry: this.session.modelRegistry,
1352
1396
  model: this.session.model,
1353
1397
  serviceTier: this.session.serviceTier,
1354
- isIdle: () => !this.session.isStreaming,
1398
+ isIdle: () => this.session.isIdle,
1355
1399
  isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
1356
1400
  signal: this.session.agent.signal,
1357
1401
  abort: () => this.session.abort(),
@@ -2347,9 +2391,10 @@ export class InteractiveMode {
2347
2391
  this.editor.setText("");
2348
2392
  return;
2349
2393
  }
2350
- if (text === "/login") {
2351
- this.showOAuthSelector("login");
2394
+ if (text === "/login" || text.startsWith("/login ")) {
2395
+ const providerRef = text.startsWith("/login ") ? text.slice(7).trim() : undefined;
2352
2396
  this.editor.setText("");
2397
+ await this.handleLoginCommand(providerRef);
2353
2398
  return;
2354
2399
  }
2355
2400
  if (text === "/logout") {
@@ -2582,6 +2627,7 @@ export class InteractiveMode {
2582
2627
  for (const [, component] of this.pendingTools.entries()) {
2583
2628
  component.setArgsComplete();
2584
2629
  }
2630
+ this.maybeShowCacheMissNotice(this.streamingMessage);
2585
2631
  }
2586
2632
  this.streamingComponent = undefined;
2587
2633
  this.streamingMessage = undefined;
@@ -2639,9 +2685,11 @@ export class InteractiveMode {
2639
2685
  this.streamingMessage = undefined;
2640
2686
  }
2641
2687
  this.clearPendingTools();
2642
- await this.checkShutdownRequested();
2643
2688
  this.ui.requestRender();
2644
2689
  break;
2690
+ case "agent_settled":
2691
+ await this.checkShutdownRequested();
2692
+ break;
2645
2693
  case "compaction_start": {
2646
2694
  if (this.settingsManager.getShowTerminalProgress()) {
2647
2695
  this.ui.terminal.setProgress(true);
@@ -2871,6 +2919,11 @@ export class InteractiveMode {
2871
2919
  renderSessionItems(items, options = {}) {
2872
2920
  this.clearPendingTools();
2873
2921
  const renderedPendingTools = new Map();
2922
+ // Cache-miss notices are not persisted; re-derive them from the full entry
2923
+ // list and re-inject them after the assistant messages that paid for them.
2924
+ const cacheMisses = this.settingsManager.getShowCacheMissNotices()
2925
+ ? collectCacheMisses(this.sessionManager.getEntries(), this.session.modelRegistry)
2926
+ : new Map();
2874
2927
  if (options.updateFooter) {
2875
2928
  this.footer.invalidate();
2876
2929
  this.updateEditorBorderColor();
@@ -2908,6 +2961,11 @@ export class InteractiveMode {
2908
2961
  }
2909
2962
  }
2910
2963
  }
2964
+ if (message.stopReason !== "aborted" && message.stopReason !== "error") {
2965
+ const miss = cacheMisses.get(message);
2966
+ if (miss)
2967
+ this.addCacheMissNotice(miss);
2968
+ }
2911
2969
  }
2912
2970
  else if (message.role === "toolResult") {
2913
2971
  // Match tool results to pending tool components
@@ -2942,6 +3000,35 @@ export class InteractiveMode {
2942
3000
  });
2943
3001
  this.renderSessionItems(items, options);
2944
3002
  }
3003
+ /**
3004
+ * Show a transcript notice when a completed assistant message paid for a
3005
+ * significant cache miss. Only states observable facts: the miss itself,
3006
+ * a model switch, or an idle gap past the cache TTL.
3007
+ */
3008
+ maybeShowCacheMissNotice(message) {
3009
+ if (!this.settingsManager.getShowCacheMissNotices())
3010
+ return;
3011
+ // Entries don't contain `message` yet: message_end fires before persistence.
3012
+ const miss = detectCacheMiss(this.sessionManager.getEntries(), message, this.session.modelRegistry);
3013
+ if (miss)
3014
+ this.addCacheMissNotice(miss);
3015
+ }
3016
+ addCacheMissNotice(miss) {
3017
+ if (miss.missedTokens < 20_000 && miss.missedCost < 0.1)
3018
+ return;
3019
+ const cost = miss.missedCost >= 0.01 ? ` (~$${miss.missedCost.toFixed(2)})` : "";
3020
+ const reBilled = `${formatTokens(miss.missedTokens)} tokens re-billed${cost}`;
3021
+ let label = "Cache miss";
3022
+ if (miss.modelChanged) {
3023
+ label = "Cache miss after model switch";
3024
+ }
3025
+ else if (miss.idleMs >= CACHE_TTL_MS) {
3026
+ label = `Cache miss after ${Math.round(miss.idleMs / 60_000)}m idle`;
3027
+ }
3028
+ const text = theme.fg("warning", `${label}: ${reBilled}`);
3029
+ this.chatContainer.addChild(new Spacer(1));
3030
+ this.chatContainer.addChild(new Text(text, 1, 0));
3031
+ }
2945
3032
  renderInitialMessages() {
2946
3033
  const entries = this.sessionManager.buildContextEntries();
2947
3034
  this.renderSessionEntries(entries, {
@@ -3596,6 +3683,7 @@ export class InteractiveMode {
3596
3683
  doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
3597
3684
  treeFilterMode: this.settingsManager.getTreeFilterMode(),
3598
3685
  showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
3686
+ showCacheMissNotices: this.settingsManager.getShowCacheMissNotices(),
3599
3687
  defaultProjectTrust: this.settingsManager.getDefaultProjectTrust(),
3600
3688
  editorPaddingX: this.settingsManager.getEditorPaddingX(),
3601
3689
  outputPad: this.settingsManager.getOutputPad(),
@@ -3672,6 +3760,10 @@ export class InteractiveMode {
3672
3760
  this.chatContainer.clear();
3673
3761
  this.rebuildChatFromMessages();
3674
3762
  },
3763
+ onShowCacheMissNoticesChange: (shown) => {
3764
+ this.settingsManager.setShowCacheMissNotices(shown);
3765
+ this.rebuildChatFromMessages();
3766
+ },
3675
3767
  onCollapseChangelogChange: (collapsed) => {
3676
3768
  this.settingsManager.setCollapseChangelog(collapsed);
3677
3769
  },
@@ -3975,19 +4067,17 @@ export class InteractiveMode {
3975
4067
  const initialSelectedId = userMessages[userMessages.length - 1]?.entryId;
3976
4068
  this.showSelector((done) => {
3977
4069
  const selector = new UserMessageSelectorComponent(userMessages.map((m) => ({ id: m.entryId, text: m.text })), async (entryId) => {
4070
+ done();
3978
4071
  try {
3979
4072
  const result = await this.runtimeHost.fork(entryId);
3980
4073
  if (result.cancelled) {
3981
- done();
3982
4074
  this.ui.requestRender();
3983
4075
  return;
3984
4076
  }
3985
4077
  this.editor.setText(result.selectedText ?? "");
3986
- done();
3987
4078
  this.showStatus("Forked to new session");
3988
4079
  }
3989
4080
  catch (error) {
3990
- done();
3991
4081
  this.showError(error instanceof Error ? error.message : String(error));
3992
4082
  }
3993
4083
  }, () => {
@@ -4216,13 +4306,82 @@ export class InteractiveMode {
4216
4306
  }
4217
4307
  return options.sort((a, b) => a.name.localeCompare(b.name));
4218
4308
  }
4219
- showLoginAuthTypeSelector() {
4309
+ findLoginProviderOptions(providerRef) {
4310
+ const normalizedProviderRef = providerRef.trim().toLowerCase();
4311
+ if (!normalizedProviderRef) {
4312
+ return [];
4313
+ }
4314
+ return this.getLoginProviderOptions().filter((provider) => provider.id.toLowerCase() === normalizedProviderRef ||
4315
+ provider.name.toLowerCase() === normalizedProviderRef);
4316
+ }
4317
+ async handleLoginCommand(providerRef) {
4318
+ if (!providerRef) {
4319
+ this.showLoginAuthTypeSelector();
4320
+ return;
4321
+ }
4322
+ const providerOptions = this.findLoginProviderOptions(providerRef);
4323
+ if (providerOptions.length === 1) {
4324
+ await this.startProviderLogin(providerOptions[0]);
4325
+ return;
4326
+ }
4327
+ if (providerOptions.length > 1) {
4328
+ const providerIds = new Set(providerOptions.map((provider) => provider.id));
4329
+ if (providerIds.size === 1) {
4330
+ this.showLoginAuthTypeSelector(providerOptions);
4331
+ return;
4332
+ }
4333
+ }
4334
+ this.showLoginProviderSelector(undefined, providerRef);
4335
+ }
4336
+ async startProviderLogin(providerOption) {
4337
+ if (providerOption.authType === "oauth") {
4338
+ await this.showLoginDialog(providerOption.id, providerOption.name);
4339
+ }
4340
+ else if (providerOption.id === BEDROCK_PROVIDER_ID) {
4341
+ this.showBedrockSetupDialog(providerOption.id, providerOption.name);
4342
+ }
4343
+ else {
4344
+ await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
4345
+ }
4346
+ }
4347
+ showLoginAuthTypeSelector(providerOptions) {
4220
4348
  const subscriptionLabel = "Use a subscription";
4221
4349
  const apiKeyLabel = "Use an API key";
4350
+ const availableAuthTypes = providerOptions
4351
+ ? new Set(providerOptions.map((provider) => provider.authType))
4352
+ : new Set(["oauth", "api_key"]);
4353
+ const options = [];
4354
+ if (availableAuthTypes.has("oauth")) {
4355
+ options.push(subscriptionLabel);
4356
+ }
4357
+ if (availableAuthTypes.has("api_key")) {
4358
+ options.push(apiKeyLabel);
4359
+ }
4360
+ if (options.length === 0) {
4361
+ this.showStatus("No login methods available.");
4362
+ return;
4363
+ }
4364
+ if (providerOptions && options.length === 1) {
4365
+ const providerOption = providerOptions[0];
4366
+ if (providerOption) {
4367
+ void this.startProviderLogin(providerOption);
4368
+ }
4369
+ return;
4370
+ }
4371
+ const title = providerOptions?.[0]
4372
+ ? `Select authentication method for ${providerOptions[0].name}:`
4373
+ : "Select authentication method:";
4222
4374
  this.showSelector((done) => {
4223
- const selector = new ExtensionSelectorComponent("Select authentication method:", [subscriptionLabel, apiKeyLabel], (option) => {
4375
+ const selector = new ExtensionSelectorComponent(title, options, (option) => {
4224
4376
  done();
4225
4377
  const authType = option === subscriptionLabel ? "oauth" : "api_key";
4378
+ if (providerOptions) {
4379
+ const providerOption = providerOptions.find((provider) => provider.authType === authType);
4380
+ if (providerOption) {
4381
+ void this.startProviderLogin(providerOption);
4382
+ }
4383
+ return;
4384
+ }
4226
4385
  this.showLoginProviderSelector(authType);
4227
4386
  }, () => {
4228
4387
  done();
@@ -4231,32 +4390,34 @@ export class InteractiveMode {
4231
4390
  return { component: selector, focus: selector };
4232
4391
  });
4233
4392
  }
4234
- showLoginProviderSelector(authType) {
4393
+ showLoginProviderSelector(authType, initialSearchInput) {
4235
4394
  const providerOptions = this.getLoginProviderOptions(authType);
4236
4395
  if (providerOptions.length === 0) {
4237
- this.showStatus(authType === "oauth" ? "No subscription providers available." : "No API key providers available.");
4396
+ const message = authType === "oauth"
4397
+ ? "No subscription providers available."
4398
+ : authType === "api_key"
4399
+ ? "No API key providers available."
4400
+ : "No login providers available.";
4401
+ this.showStatus(message);
4238
4402
  return;
4239
4403
  }
4240
4404
  this.showSelector((done) => {
4241
- const selector = new OAuthSelectorComponent("login", this.session.modelRegistry.authStorage, providerOptions, async (providerId) => {
4405
+ const selector = new OAuthSelectorComponent("login", this.session.modelRegistry.authStorage, providerOptions, async (providerId, selectedAuthType) => {
4242
4406
  done();
4243
- const providerOption = providerOptions.find((provider) => provider.id === providerId);
4407
+ const providerOption = providerOptions.find((provider) => provider.id === providerId && provider.authType === selectedAuthType);
4244
4408
  if (!providerOption) {
4245
4409
  return;
4246
4410
  }
4247
- if (providerOption.authType === "oauth") {
4248
- await this.showLoginDialog(providerOption.id, providerOption.name);
4249
- }
4250
- else if (providerOption.id === BEDROCK_PROVIDER_ID) {
4251
- this.showBedrockSetupDialog(providerOption.id, providerOption.name);
4411
+ await this.startProviderLogin(providerOption);
4412
+ }, () => {
4413
+ done();
4414
+ if (authType) {
4415
+ this.showLoginAuthTypeSelector();
4252
4416
  }
4253
4417
  else {
4254
- await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
4418
+ this.ui.requestRender();
4255
4419
  }
4256
- }, () => {
4257
- done();
4258
- this.showLoginAuthTypeSelector();
4259
- }, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId));
4420
+ }, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId), initialSearchInput);
4260
4421
  return { component: selector, focus: selector };
4261
4422
  });
4262
4423
  }
@@ -4517,7 +4678,7 @@ export class InteractiveMode {
4517
4678
  const borderColor = (s) => theme.fg("border", s);
4518
4679
  reloadBox.addChild(new DynamicBorder(borderColor));
4519
4680
  reloadBox.addChild(new Spacer(1));
4520
- reloadBox.addChild(new Text(theme.fg("muted", "Reloading keybindings, extensions, skills, prompts, themes..."), 1, 0));
4681
+ reloadBox.addChild(new Text(theme.fg("muted", "Reloading keybindings, extensions, skills, prompts, themes, and context files..."), 1, 0));
4521
4682
  reloadBox.addChild(new Spacer(1));
4522
4683
  reloadBox.addChild(new DynamicBorder(borderColor));
4523
4684
  const previousEditor = this.editor;
@@ -4581,8 +4742,8 @@ export class InteractiveMode {
4581
4742
  this.showError(`models.json error: ${modelsJsonError}`);
4582
4743
  }
4583
4744
  this.showStatus(savedImplicitProjectTrust
4584
- ? "Reloaded keybindings, extensions, skills, prompts, themes; saved project trust"
4585
- : "Reloaded keybindings, extensions, skills, prompts, themes");
4745
+ ? "Reloaded keybindings, extensions, skills, prompts, themes, and context files; saved project trust"
4746
+ : "Reloaded keybindings, extensions, skills, prompts, themes, and context files");
4586
4747
  dismissReloadBox(this.editor);
4587
4748
  reloadBoxDismissed = true;
4588
4749
  }
@@ -4803,7 +4964,70 @@ export class InteractiveMode {
4803
4964
  handleSessionCommand() {
4804
4965
  const stats = this.session.getSessionStats();
4805
4966
  const sessionName = this.sessionManager.getSessionName();
4806
- const info = formatSessionInfo(stats, sessionName);
4967
+ const entries = this.sessionManager.getEntries();
4968
+ const cacheWaste = computeCacheWaste(entries, this.session.modelRegistry);
4969
+ // Cost/token totals per provider/model actually used (e.g. OpenRouter `auto`
4970
+ // resolves to a concrete responseModel), sorted by cost descending.
4971
+ const perModelMap = new Map();
4972
+ for (const entry of entries) {
4973
+ if (entry.type !== "message" || entry.message.role !== "assistant")
4974
+ continue;
4975
+ const message = entry.message;
4976
+ const usage = message.usage;
4977
+ const key = `${message.provider}/${message.responseModel ?? message.model}`;
4978
+ let bucket = perModelMap.get(key);
4979
+ if (!bucket) {
4980
+ bucket = { key, cost: 0, tokens: 0 };
4981
+ perModelMap.set(key, bucket);
4982
+ }
4983
+ bucket.cost += usage.cost.total;
4984
+ bucket.tokens += usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
4985
+ }
4986
+ const perModel = Array.from(perModelMap.values()).sort((a, b) => b.cost - a.cost);
4987
+ let info = `${theme.bold("Session Info")}\n\n`;
4988
+ if (sessionName) {
4989
+ info += `${theme.fg("dim", "Name:")} ${sessionName}\n`;
4990
+ }
4991
+ info += `${theme.fg("dim", "File:")} ${stats.sessionFile ?? "In-memory"}\n`;
4992
+ info += `${theme.fg("dim", "ID:")} ${stats.sessionId}\n\n`;
4993
+ info += `${theme.bold("Messages")}\n`;
4994
+ info += `${theme.fg("dim", "Total:")} ${stats.totalMessages}\n`;
4995
+ info += `${theme.fg("dim", "User:")} ${stats.userMessages}\n`;
4996
+ info += `${theme.fg("dim", "Assistant:")} ${stats.assistantMessages}\n`;
4997
+ info += `${theme.fg("dim", "Tools:")} ${stats.toolCalls} calls, ${stats.toolResults} results\n\n`;
4998
+ info += `${theme.bold("Tokens")}\n`;
4999
+ // "Input" is the full prompt volume. With cache activity, split it into
5000
+ // cached (served from cache) vs uncached (everything else) - the only
5001
+ // provider-independent split. Cache writes, where reported, are a detail
5002
+ // of the uncached portion.
5003
+ const { input, cacheRead, cacheWrite } = stats.tokens;
5004
+ const promptTokens = input + cacheRead + cacheWrite;
5005
+ info += `${theme.fg("dim", "Input:")} ${promptTokens.toLocaleString()}\n`;
5006
+ if (promptTokens > 0 && (cacheRead > 0 || cacheWrite > 0)) {
5007
+ const hitRate = theme.fg("dim", `(${((cacheRead / promptTokens) * 100).toFixed(1)}%)`);
5008
+ info += ` ${theme.fg("dim", "Cached:")} ${cacheRead.toLocaleString()} ${hitRate}\n`;
5009
+ const written = cacheWrite > 0 ? ` ${theme.fg("dim", `(${cacheWrite.toLocaleString()} written to cache)`)}` : "";
5010
+ info += ` ${theme.fg("dim", "Uncached:")} ${(input + cacheWrite).toLocaleString()}${written}\n`;
5011
+ }
5012
+ info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`;
5013
+ info += `${theme.fg("dim", "Total:")} ${stats.tokens.total.toLocaleString()}\n`;
5014
+ if (stats.cost > 0 || cacheWaste.missedTokens > 0) {
5015
+ info += `\n${theme.bold("Cost")}\n`;
5016
+ info += `${theme.fg("dim", "Total:")} $${stats.cost.toFixed(3)}`;
5017
+ if (perModel.length > 1) {
5018
+ for (const entry of perModel) {
5019
+ info += `\n ${theme.fg("dim", `${entry.key}:`)} $${entry.cost.toFixed(3)} ${theme.fg("dim", `(${formatTokens(entry.tokens)} tokens)`)}`;
5020
+ }
5021
+ }
5022
+ if (cacheWaste.missedTokens > 0) {
5023
+ const missLabel = cacheWaste.missCount === 1 ? "1 miss" : `${cacheWaste.missCount} misses`;
5024
+ const detail = `${cacheWaste.missedTokens.toLocaleString()} tokens, ${missLabel}`;
5025
+ info +=
5026
+ cacheWaste.missedCost >= 0.0001
5027
+ ? `\n${theme.fg("dim", "Cache Re-billed:")} $${cacheWaste.missedCost.toFixed(3)} ${theme.fg("dim", `(${detail})`)}`
5028
+ : `\n${theme.fg("dim", "Cache Re-billed:")} ${detail}`;
5029
+ }
5030
+ }
4807
5031
  this.chatContainer.addChild(new Spacer(1));
4808
5032
  this.chatContainer.addChild(new Text(info, 1, 0));
4809
5033
  this.ui.requestRender();