@averyyy/pi-coding-agent 0.80.3-piclient.8 → 0.80.6-piclient.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 (203) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +5 -3
  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 +21 -5
  11. package/dist/core/agent-session.d.ts.map +1 -1
  12. package/dist/core/agent-session.js +100 -30
  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/branch-summarization.d.ts +1 -1
  22. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  23. package/dist/core/compaction/branch-summarization.js.map +1 -1
  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 +6 -0
  33. package/dist/core/extensions/loader.js.map +1 -1
  34. package/dist/core/extensions/runner.d.ts +6 -3
  35. package/dist/core/extensions/runner.d.ts.map +1 -1
  36. package/dist/core/extensions/runner.js +43 -0
  37. package/dist/core/extensions/runner.js.map +1 -1
  38. package/dist/core/extensions/types.d.ts +33 -11
  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/extensions/wrapper.d.ts.map +1 -1
  42. package/dist/core/extensions/wrapper.js +22 -3
  43. package/dist/core/extensions/wrapper.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 +8 -3
  48. package/dist/core/keybindings.d.ts.map +1 -1
  49. package/dist/core/keybindings.js +10 -9
  50. package/dist/core/keybindings.js.map +1 -1
  51. package/dist/core/model-registry.d.ts +4 -6
  52. package/dist/core/model-registry.d.ts.map +1 -1
  53. package/dist/core/model-registry.js +39 -9
  54. package/dist/core/model-registry.js.map +1 -1
  55. package/dist/core/model-resolver.d.ts +10 -0
  56. package/dist/core/model-resolver.d.ts.map +1 -1
  57. package/dist/core/model-resolver.js +14 -17
  58. package/dist/core/model-resolver.js.map +1 -1
  59. package/dist/core/package-manager.d.ts +4 -1
  60. package/dist/core/package-manager.d.ts.map +1 -1
  61. package/dist/core/package-manager.js +75 -28
  62. package/dist/core/package-manager.js.map +1 -1
  63. package/dist/core/pi-client-runtime-update.d.ts +14 -0
  64. package/dist/core/pi-client-runtime-update.d.ts.map +1 -0
  65. package/dist/core/pi-client-runtime-update.js +27 -0
  66. package/dist/core/pi-client-runtime-update.js.map +1 -0
  67. package/dist/core/pi-server-client.d.ts.map +1 -1
  68. package/dist/core/pi-server-client.js +122 -5
  69. package/dist/core/pi-server-client.js.map +1 -1
  70. package/dist/core/provider-attribution.d.ts.map +1 -1
  71. package/dist/core/provider-attribution.js +0 -10
  72. package/dist/core/provider-attribution.js.map +1 -1
  73. package/dist/core/resource-loader.d.ts +2 -2
  74. package/dist/core/resource-loader.d.ts.map +1 -1
  75. package/dist/core/resource-loader.js +6 -7
  76. package/dist/core/resource-loader.js.map +1 -1
  77. package/dist/core/sdk.d.ts +1 -1
  78. package/dist/core/sdk.d.ts.map +1 -1
  79. package/dist/core/sdk.js +8 -1
  80. package/dist/core/sdk.js.map +1 -1
  81. package/dist/core/session-manager.d.ts +20 -1
  82. package/dist/core/session-manager.d.ts.map +1 -1
  83. package/dist/core/session-manager.js +97 -68
  84. package/dist/core/session-manager.js.map +1 -1
  85. package/dist/core/settings-manager.d.ts +9 -3
  86. package/dist/core/settings-manager.d.ts.map +1 -1
  87. package/dist/core/settings-manager.js +10 -1
  88. package/dist/core/settings-manager.js.map +1 -1
  89. package/dist/core/slash-commands.d.ts +1 -0
  90. package/dist/core/slash-commands.d.ts.map +1 -1
  91. package/dist/core/slash-commands.js +3 -3
  92. package/dist/core/slash-commands.js.map +1 -1
  93. package/dist/core/tools/edit.d.ts.map +1 -1
  94. package/dist/core/tools/edit.js +2 -2
  95. package/dist/core/tools/edit.js.map +1 -1
  96. package/dist/index.d.ts +3 -2
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +2 -1
  99. package/dist/index.js.map +1 -1
  100. package/dist/main.d.ts +2 -2
  101. package/dist/main.d.ts.map +1 -1
  102. package/dist/main.js +1 -0
  103. package/dist/main.js.map +1 -1
  104. package/dist/modes/interactive/components/config-selector.d.ts +34 -3
  105. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  106. package/dist/modes/interactive/components/config-selector.js +280 -25
  107. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  108. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  109. package/dist/modes/interactive/components/custom-editor.js +1 -1
  110. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  111. package/dist/modes/interactive/components/custom-entry.d.ts +19 -0
  112. package/dist/modes/interactive/components/custom-entry.d.ts.map +1 -0
  113. package/dist/modes/interactive/components/custom-entry.js +52 -0
  114. package/dist/modes/interactive/components/custom-entry.js.map +1 -0
  115. package/dist/modes/interactive/components/footer.d.ts +4 -0
  116. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  117. package/dist/modes/interactive/components/footer.js +1 -1
  118. package/dist/modes/interactive/components/footer.js.map +1 -1
  119. package/dist/modes/interactive/components/login-dialog.d.ts +4 -0
  120. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  121. package/dist/modes/interactive/components/login-dialog.js +9 -2
  122. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  123. package/dist/modes/interactive/components/oauth-selector.d.ts +3 -1
  124. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  125. package/dist/modes/interactive/components/oauth-selector.js +17 -6
  126. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  127. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  128. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  129. package/dist/modes/interactive/components/settings-selector.js +12 -1
  130. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  131. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  132. package/dist/modes/interactive/components/thinking-selector.js +2 -1
  133. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  134. package/dist/modes/interactive/components/tree-selector.d.ts +5 -0
  135. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  136. package/dist/modes/interactive/components/tree-selector.js +49 -12
  137. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  138. package/dist/modes/interactive/interactive-mode.d.ts +16 -5
  139. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  140. package/dist/modes/interactive/interactive-mode.js +368 -97
  141. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  142. package/dist/modes/interactive/theme/dark.json +1 -0
  143. package/dist/modes/interactive/theme/light.json +1 -0
  144. package/dist/modes/interactive/theme/theme-schema.json +6 -2
  145. package/dist/modes/interactive/theme/theme.d.ts +3 -2
  146. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  147. package/dist/modes/interactive/theme/theme.js +10 -3
  148. package/dist/modes/interactive/theme/theme.js.map +1 -1
  149. package/dist/modes/print-mode.d.ts.map +1 -1
  150. package/dist/modes/print-mode.js +1 -1
  151. package/dist/modes/print-mode.js.map +1 -1
  152. package/dist/modes/rpc/rpc-client.d.ts +6 -6
  153. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  154. package/dist/modes/rpc/rpc-client.js +3 -3
  155. package/dist/modes/rpc/rpc-client.js.map +1 -1
  156. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  157. package/dist/modes/rpc/rpc-mode.js +4 -1
  158. package/dist/modes/rpc/rpc-mode.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/utils/clipboard-image.d.ts.map +1 -1
  164. package/dist/utils/clipboard-image.js +1 -1
  165. package/dist/utils/clipboard-image.js.map +1 -1
  166. package/dist/utils/clipboard-native.d.ts +1 -0
  167. package/dist/utils/clipboard-native.d.ts.map +1 -1
  168. package/dist/utils/clipboard-native.js.map +1 -1
  169. package/dist/utils/clipboard.d.ts +2 -0
  170. package/dist/utils/clipboard.d.ts.map +1 -1
  171. package/dist/utils/clipboard.js +13 -0
  172. package/dist/utils/clipboard.js.map +1 -1
  173. package/docs/custom-provider.md +6 -3
  174. package/docs/extensions.md +213 -18
  175. package/docs/keybindings.md +1 -0
  176. package/docs/models.md +51 -8
  177. package/docs/packages.md +2 -2
  178. package/docs/providers.md +3 -0
  179. package/docs/quickstart.md +1 -1
  180. package/docs/rpc.md +15 -5
  181. package/docs/sdk.md +51 -1
  182. package/docs/session-format.md +18 -8
  183. package/docs/settings.md +3 -2
  184. package/docs/themes.md +5 -3
  185. package/docs/tui.md +1 -1
  186. package/docs/usage.md +3 -2
  187. package/examples/extensions/README.md +1 -0
  188. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  189. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  190. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  191. package/examples/extensions/entry-renderer.ts +41 -0
  192. package/examples/extensions/gondolin/package-lock.json +2 -2
  193. package/examples/extensions/gondolin/package.json +1 -1
  194. package/examples/extensions/preset.ts +2 -2
  195. package/examples/extensions/question.ts +1 -0
  196. package/examples/extensions/reload-runtime.ts +2 -2
  197. package/examples/extensions/sandbox/package-lock.json +2 -2
  198. package/examples/extensions/sandbox/package.json +1 -1
  199. package/examples/extensions/with-deps/package-lock.json +2 -2
  200. package/examples/extensions/with-deps/package.json +1 -1
  201. package/examples/sdk/12-full-control.ts +1 -1
  202. package/examples/sdk/README.md +1 -1
  203. package/package.json +8 -4
@@ -13,20 +13,22 @@ 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";
19
20
  import { createCompactionSummaryMessage } from "../../core/messages.js";
20
21
  import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
21
22
  import { DefaultPackageManager } from "../../core/package-manager.js";
23
+ import { getPiClientUpdateMarkerPath, PI_CLIENT_RUNTIME_RELOAD_EXIT_CODE, readPiClientUpdateMarker, writePiClientRuntimeReloadState, } from "../../core/pi-client-runtime-update.js";
22
24
  import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "../../core/provider-display-names.js";
23
25
  import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
24
- import { SessionManager } from "../../core/session-manager.js";
26
+ import { SessionManager, sessionEntryToContextMessages } from "../../core/session-manager.js";
25
27
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
26
28
  import { isInstallTelemetryEnabled } from "../../core/telemetry.js";
27
29
  import { hasTrustRequiringProjectResources, ProjectTrustStore } from "../../core/trust-manager.js";
28
30
  import { getChangelogPath, getNewEntries, normalizeChangelogLinks, parseChangelog } from "../../utils/changelog.js";
29
- import { copyToClipboard } from "../../utils/clipboard.js";
31
+ import { copyToClipboard, readClipboardText } from "../../utils/clipboard.js";
30
32
  import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
31
33
  import { parseGitUrl } from "../../utils/git.js";
32
34
  import { getCwdRelativePath } from "../../utils/paths.js";
@@ -41,6 +43,7 @@ import { BorderedLoader } from "./components/bordered-loader.js";
41
43
  import { BranchSummaryMessageComponent } from "./components/branch-summary-message.js";
42
44
  import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message.js";
43
45
  import { CustomEditor } from "./components/custom-editor.js";
46
+ import { CustomEntryComponent } from "./components/custom-entry.js";
44
47
  import { CustomMessageComponent } from "./components/custom-message.js";
45
48
  import { DaxnutsComponent } from "./components/daxnuts.js";
46
49
  import { DynamicBorder } from "./components/dynamic-border.js";
@@ -48,11 +51,11 @@ import { EarendilAnnouncementComponent } from "./components/earendil-announcemen
48
51
  import { ExtensionEditorComponent } from "./components/extension-editor.js";
49
52
  import { ExtensionInputComponent } from "./components/extension-input.js";
50
53
  import { ExtensionSelectorComponent } from "./components/extension-selector.js";
51
- import { FooterComponent } from "./components/footer.js";
54
+ import { FooterComponent, formatTokens } from "./components/footer.js";
52
55
  import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
53
56
  import { LoginDialogComponent } from "./components/login-dialog.js";
54
57
  import { ModelSelectorComponent } from "./components/model-selector.js";
55
- import { OAuthSelectorComponent } from "./components/oauth-selector.js";
58
+ import { formatAuthSelectorProviderType, OAuthSelectorComponent, } from "./components/oauth-selector.js";
56
59
  import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js";
57
60
  import { SessionSelectorComponent } from "./components/session-selector.js";
58
61
  import { SettingsSelectorComponent } from "./components/settings-selector.js";
@@ -81,6 +84,9 @@ class ExpandableText extends Text {
81
84
  this.setText(expanded ? this.getExpandedText() : this.getCollapsedText());
82
85
  }
83
86
  }
87
+ function isCustomSessionEntry(item) {
88
+ return "type" in item && item.type === "custom";
89
+ }
84
90
  const DEAD_TERMINAL_ERROR_CODES = new Set(["EIO", "EPIPE", "ENOTCONN"]);
85
91
  function isDeadTerminalError(error) {
86
92
  if (!error || typeof error !== "object" || !("code" in error)) {
@@ -120,7 +126,6 @@ export function formatResumeCommand(sessionManager) {
120
126
  function hasDefaultModelProvider(providerId) {
121
127
  return providerId in defaultModelPerProvider;
122
128
  }
123
- const BEDROCK_PROVIDER_ID = "amazon-bedrock";
124
129
  const BUILT_IN_MODEL_PROVIDERS = new Set(getProviders());
125
130
  export function isApiKeyLoginProvider(providerId, oauthProviderIds, builtInProviderIds = BUILT_IN_MODEL_PROVIDERS) {
126
131
  if (BUILT_IN_PROVIDER_DISPLAY_NAMES[providerId]) {
@@ -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,
@@ -483,7 +532,7 @@ export class InteractiveMode {
483
532
  rawKeyHint("!!", "to run bash (no context)"),
484
533
  hint("app.message.followUp", "to queue follow-up"),
485
534
  hint("app.message.dequeue", "to edit all queued messages"),
486
- hint("app.clipboard.pasteImage", "to paste image"),
535
+ hint("app.clipboard.pasteImage", "to paste image (with text fallback)"),
487
536
  rawKeyHint("drop files", "to attach"),
488
537
  ].join("\n");
489
538
  const compactInstructions = [
@@ -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: () => {
@@ -1985,6 +2034,7 @@ export class InteractiveMode {
1985
2034
  this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
1986
2035
  this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
1987
2036
  this.defaultEditor.onAction("app.editor.external", () => this.openExternalEditor());
2037
+ this.defaultEditor.onAction("app.message.copy", () => void this.handleCopyCommand());
1988
2038
  this.defaultEditor.onAction("app.message.followUp", () => this.handleFollowUp());
1989
2039
  this.defaultEditor.onAction("app.message.dequeue", () => this.handleDequeue());
1990
2040
  this.defaultEditor.onAction("app.session.new", () => this.handleClearCommand());
@@ -1998,26 +2048,30 @@ export class InteractiveMode {
1998
2048
  this.updateEditorBorderColor();
1999
2049
  }
2000
2050
  };
2001
- // Handle clipboard image paste (triggered on Ctrl+V)
2051
+ // Handle clipboard paste (triggered on Ctrl+V). Images are attached by path;
2052
+ // otherwise, paste plain text from the system clipboard.
2002
2053
  this.defaultEditor.onPasteImage = () => {
2003
- this.handleClipboardImagePaste();
2054
+ void this.handleClipboardPaste();
2004
2055
  };
2005
2056
  }
2006
- async handleClipboardImagePaste() {
2057
+ async handleClipboardPaste() {
2007
2058
  try {
2008
2059
  const image = await readClipboardImage();
2009
- if (!image) {
2060
+ if (image) {
2061
+ const tmpDir = os.tmpdir();
2062
+ const ext = extensionForImageMimeType(image.mimeType) ?? "png";
2063
+ const fileName = `pi-clipboard-${crypto.randomUUID()}.${ext}`;
2064
+ const filePath = path.join(tmpDir, fileName);
2065
+ fs.writeFileSync(filePath, Buffer.from(image.bytes));
2066
+ this.editor.insertTextAtCursor?.(filePath);
2067
+ this.ui.requestRender();
2010
2068
  return;
2011
2069
  }
2012
- // Write to temp file
2013
- const tmpDir = os.tmpdir();
2014
- const ext = extensionForImageMimeType(image.mimeType) ?? "png";
2015
- const fileName = `pi-clipboard-${crypto.randomUUID()}.${ext}`;
2016
- const filePath = path.join(tmpDir, fileName);
2017
- fs.writeFileSync(filePath, Buffer.from(image.bytes));
2018
- // Insert file path directly
2019
- this.editor.insertTextAtCursor?.(filePath);
2020
- this.ui.requestRender();
2070
+ const text = await readClipboardText();
2071
+ if (text) {
2072
+ this.editor.insertTextAtCursor?.(text);
2073
+ this.ui.requestRender();
2074
+ }
2021
2075
  }
2022
2076
  catch {
2023
2077
  // Silently ignore clipboard errors (may not have permission, etc.)
@@ -2105,9 +2159,10 @@ export class InteractiveMode {
2105
2159
  this.editor.setText("");
2106
2160
  return;
2107
2161
  }
2108
- if (text === "/login") {
2109
- this.showOAuthSelector("login");
2162
+ if (text === "/login" || text.startsWith("/login ")) {
2163
+ const providerRef = text.startsWith("/login ") ? text.slice(7).trim() : undefined;
2110
2164
  this.editor.setText("");
2165
+ await this.handleLoginCommand(providerRef);
2111
2166
  return;
2112
2167
  }
2113
2168
  if (text === "/logout") {
@@ -2131,6 +2186,12 @@ export class InteractiveMode {
2131
2186
  await this.handleReloadCommand();
2132
2187
  return;
2133
2188
  }
2189
+ const pendingPiClientUpdate = readPiClientUpdateMarker();
2190
+ if (pendingPiClientUpdate) {
2191
+ this.showWarning(`pi-client ${pendingPiClientUpdate.version} is installed. Run /reload to continue this session with the new version.`);
2192
+ this.editor.setText(text);
2193
+ return;
2194
+ }
2134
2195
  if (text === "/debug") {
2135
2196
  this.handleDebugCommand();
2136
2197
  this.editor.setText("");
@@ -2241,6 +2302,12 @@ export class InteractiveMode {
2241
2302
  this.updatePendingMessagesDisplay();
2242
2303
  this.ui.requestRender();
2243
2304
  break;
2305
+ case "entry_appended":
2306
+ if (event.entry.type === "custom") {
2307
+ this.addCustomEntryToChat(event.entry);
2308
+ this.ui.requestRender();
2309
+ }
2310
+ break;
2244
2311
  case "session_info_changed":
2245
2312
  this.updateTerminalTitle();
2246
2313
  this.footer.invalidate();
@@ -2326,6 +2393,7 @@ export class InteractiveMode {
2326
2393
  for (const [, component] of this.pendingTools.entries()) {
2327
2394
  component.setArgsComplete();
2328
2395
  }
2396
+ this.maybeShowCacheMissNotice(this.streamingMessage);
2329
2397
  }
2330
2398
  this.streamingComponent = undefined;
2331
2399
  this.streamingMessage = undefined;
@@ -2376,9 +2444,11 @@ export class InteractiveMode {
2376
2444
  this.streamingMessage = undefined;
2377
2445
  }
2378
2446
  this.pendingTools.clear();
2379
- await this.checkShutdownRequested();
2380
2447
  this.ui.requestRender();
2381
2448
  break;
2449
+ case "agent_settled":
2450
+ await this.checkShutdownRequested();
2451
+ break;
2382
2452
  case "compaction_start": {
2383
2453
  if (this.settingsManager.getShowTerminalProgress()) {
2384
2454
  this.ui.terminal.setProgress(true);
@@ -2486,6 +2556,25 @@ export class InteractiveMode {
2486
2556
  this.lastStatusText = text;
2487
2557
  this.ui.requestRender();
2488
2558
  }
2559
+ addCustomEntryToChat(entry) {
2560
+ const renderer = this.session.extensionRunner.getEntryRenderer(entry.customType);
2561
+ if (!renderer) {
2562
+ return;
2563
+ }
2564
+ const component = new CustomEntryComponent(entry, renderer);
2565
+ component.setExpanded(this.toolOutputExpanded);
2566
+ if (!component.hasContent()) {
2567
+ return;
2568
+ }
2569
+ if (this.streamingComponent) {
2570
+ const streamingIndex = this.chatContainer.children.indexOf(this.streamingComponent);
2571
+ if (streamingIndex >= 0) {
2572
+ this.chatContainer.children.splice(streamingIndex, 0, component);
2573
+ return;
2574
+ }
2575
+ }
2576
+ this.chatContainer.addChild(component);
2577
+ }
2489
2578
  addMessageToChat(message, options) {
2490
2579
  switch (message.role) {
2491
2580
  case "bashExecution": {
@@ -2563,20 +2652,24 @@ export class InteractiveMode {
2563
2652
  }
2564
2653
  }
2565
2654
  }
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 = {}) {
2655
+ renderSessionItems(items, options = {}) {
2573
2656
  this.pendingTools.clear();
2574
2657
  const renderedPendingTools = new Map();
2658
+ // Cache-miss notices are not persisted; re-derive them from the full entry
2659
+ // list and re-inject them after the assistant messages that paid for them.
2660
+ const cacheMisses = this.settingsManager.getShowCacheMissNotices()
2661
+ ? collectCacheMisses(this.sessionManager.getEntries(), this.session.modelRegistry)
2662
+ : new Map();
2575
2663
  if (options.updateFooter) {
2576
2664
  this.footer.invalidate();
2577
2665
  this.updateEditorBorderColor();
2578
2666
  }
2579
- for (const message of sessionContext.messages) {
2667
+ for (const item of items) {
2668
+ if (isCustomSessionEntry(item)) {
2669
+ this.addCustomEntryToChat(item);
2670
+ continue;
2671
+ }
2672
+ const message = item;
2580
2673
  // Assistant messages need special handling for tool calls
2581
2674
  if (message.role === "assistant") {
2582
2675
  this.addMessageToChat(message);
@@ -2608,6 +2701,11 @@ export class InteractiveMode {
2608
2701
  }
2609
2702
  }
2610
2703
  }
2704
+ if (message.stopReason !== "aborted" && message.stopReason !== "error") {
2705
+ const miss = cacheMisses.get(message);
2706
+ if (miss)
2707
+ this.addCacheMissNotice(miss);
2708
+ }
2611
2709
  }
2612
2710
  else if (message.role === "toolResult") {
2613
2711
  // Match tool results to pending tool components
@@ -2627,10 +2725,53 @@ export class InteractiveMode {
2627
2725
  }
2628
2726
  this.ui.requestRender();
2629
2727
  }
2728
+ /**
2729
+ * Render session entries to chat. Used for initial load and rebuild after compaction.
2730
+ * @param entries Compaction-aware session entries to render
2731
+ * @param options.updateFooter Update footer state
2732
+ * @param options.populateHistory Add user messages to editor history
2733
+ */
2734
+ renderSessionEntries(entries, options = {}) {
2735
+ const items = entries.flatMap((entry) => {
2736
+ if (entry.type === "custom") {
2737
+ return [entry];
2738
+ }
2739
+ return sessionEntryToContextMessages(entry);
2740
+ });
2741
+ this.renderSessionItems(items, options);
2742
+ }
2743
+ /**
2744
+ * Show a transcript notice when a completed assistant message paid for a
2745
+ * significant cache miss. Only states observable facts: the miss itself,
2746
+ * a model switch, or an idle gap past the cache TTL.
2747
+ */
2748
+ maybeShowCacheMissNotice(message) {
2749
+ if (!this.settingsManager.getShowCacheMissNotices())
2750
+ return;
2751
+ // Entries don't contain `message` yet: message_end fires before persistence.
2752
+ const miss = detectCacheMiss(this.sessionManager.getEntries(), message, this.session.modelRegistry);
2753
+ if (miss)
2754
+ this.addCacheMissNotice(miss);
2755
+ }
2756
+ addCacheMissNotice(miss) {
2757
+ if (miss.missedTokens < 20_000 && miss.missedCost < 0.1)
2758
+ return;
2759
+ const cost = miss.missedCost >= 0.01 ? ` (~$${miss.missedCost.toFixed(2)})` : "";
2760
+ const reBilled = `${formatTokens(miss.missedTokens)} tokens re-billed${cost}`;
2761
+ let label = "Cache miss";
2762
+ if (miss.modelChanged) {
2763
+ label = "Cache miss after model switch";
2764
+ }
2765
+ else if (miss.idleMs >= CACHE_TTL_MS) {
2766
+ label = `Cache miss after ${Math.round(miss.idleMs / 60_000)}m idle`;
2767
+ }
2768
+ const text = theme.fg("warning", `${label}: ${reBilled}`);
2769
+ this.chatContainer.addChild(new Spacer(1));
2770
+ this.chatContainer.addChild(new Text(text, 1, 0));
2771
+ }
2630
2772
  renderInitialMessages() {
2631
- // Get aligned messages and entries from session context
2632
- const context = this.sessionManager.buildSessionContext();
2633
- this.renderSessionContext(context, {
2773
+ const entries = this.sessionManager.buildContextEntries();
2774
+ this.renderSessionEntries(entries, {
2634
2775
  updateFooter: true,
2635
2776
  populateHistory: true,
2636
2777
  });
@@ -2666,8 +2807,7 @@ export class InteractiveMode {
2666
2807
  }
2667
2808
  rebuildChatFromMessages() {
2668
2809
  this.chatContainer.clear();
2669
- const context = this.sessionManager.buildSessionContext();
2670
- this.renderSessionContext(context);
2810
+ this.renderSessionEntries(this.sessionManager.buildContextEntries());
2671
2811
  }
2672
2812
  // =========================================================================
2673
2813
  // Key handlers
@@ -3266,6 +3406,7 @@ export class InteractiveMode {
3266
3406
  doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
3267
3407
  treeFilterMode: this.settingsManager.getTreeFilterMode(),
3268
3408
  showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
3409
+ showCacheMissNotices: this.settingsManager.getShowCacheMissNotices(),
3269
3410
  defaultProjectTrust: this.settingsManager.getDefaultProjectTrust(),
3270
3411
  editorPaddingX: this.settingsManager.getEditorPaddingX(),
3271
3412
  outputPad: this.settingsManager.getOutputPad(),
@@ -3341,6 +3482,10 @@ export class InteractiveMode {
3341
3482
  this.chatContainer.clear();
3342
3483
  this.rebuildChatFromMessages();
3343
3484
  },
3485
+ onShowCacheMissNoticesChange: (shown) => {
3486
+ this.settingsManager.setShowCacheMissNotices(shown);
3487
+ this.rebuildChatFromMessages();
3488
+ },
3344
3489
  onCollapseChangelogChange: (collapsed) => {
3345
3490
  this.settingsManager.setCollapseChangelog(collapsed);
3346
3491
  },
@@ -3630,19 +3775,17 @@ export class InteractiveMode {
3630
3775
  const initialSelectedId = userMessages[userMessages.length - 1]?.entryId;
3631
3776
  this.showSelector((done) => {
3632
3777
  const selector = new UserMessageSelectorComponent(userMessages.map((m) => ({ id: m.entryId, text: m.text })), async (entryId) => {
3778
+ done();
3633
3779
  try {
3634
3780
  const result = await this.runtimeHost.fork(entryId);
3635
3781
  if (result.cancelled) {
3636
- done();
3637
3782
  this.ui.requestRender();
3638
3783
  return;
3639
3784
  }
3640
3785
  this.editor.setText(result.selectedText ?? "");
3641
- done();
3642
3786
  this.showStatus("Forked to new session");
3643
3787
  }
3644
3788
  catch (error) {
3645
- done();
3646
3789
  this.showError(error instanceof Error ? error.message : String(error));
3647
3790
  }
3648
3791
  }, () => {
@@ -3769,6 +3912,19 @@ export class InteractiveMode {
3769
3912
  this.sessionManager.appendLabelChange(entryId, label);
3770
3913
  this.ui.requestRender();
3771
3914
  }, initialSelectedId, initialFilterMode);
3915
+ selector.onCopy = async (text) => {
3916
+ if (!text) {
3917
+ this.showError("Selected entry has no text to copy");
3918
+ return;
3919
+ }
3920
+ try {
3921
+ await copyToClipboard(text);
3922
+ this.showStatus("Copied selected message to clipboard");
3923
+ }
3924
+ catch (error) {
3925
+ this.showError(error instanceof Error ? error.message : String(error));
3926
+ }
3927
+ };
3772
3928
  return { component: selector, focus: selector };
3773
3929
  });
3774
3930
  }
@@ -3871,13 +4027,79 @@ export class InteractiveMode {
3871
4027
  }
3872
4028
  return options.sort((a, b) => a.name.localeCompare(b.name));
3873
4029
  }
3874
- showLoginAuthTypeSelector() {
4030
+ findLoginProviderOptions(providerRef) {
4031
+ const normalizedProviderRef = providerRef.trim().toLowerCase();
4032
+ if (!normalizedProviderRef) {
4033
+ return [];
4034
+ }
4035
+ return this.getLoginProviderOptions().filter((provider) => provider.id.toLowerCase() === normalizedProviderRef ||
4036
+ provider.name.toLowerCase() === normalizedProviderRef);
4037
+ }
4038
+ async handleLoginCommand(providerRef) {
4039
+ if (!providerRef) {
4040
+ this.showLoginAuthTypeSelector();
4041
+ return;
4042
+ }
4043
+ const providerOptions = this.findLoginProviderOptions(providerRef);
4044
+ if (providerOptions.length === 1) {
4045
+ await this.startProviderLogin(providerOptions[0]);
4046
+ return;
4047
+ }
4048
+ if (providerOptions.length > 1) {
4049
+ const providerIds = new Set(providerOptions.map((provider) => provider.id));
4050
+ if (providerIds.size === 1) {
4051
+ this.showLoginAuthTypeSelector(providerOptions);
4052
+ return;
4053
+ }
4054
+ }
4055
+ this.showLoginProviderSelector(undefined, providerRef);
4056
+ }
4057
+ async startProviderLogin(providerOption) {
4058
+ if (providerOption.authType === "oauth") {
4059
+ await this.showLoginDialog(providerOption.id, providerOption.name);
4060
+ }
4061
+ else {
4062
+ await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
4063
+ }
4064
+ }
4065
+ showLoginAuthTypeSelector(providerOptions) {
3875
4066
  const subscriptionLabel = "Use a subscription";
3876
4067
  const apiKeyLabel = "Use an API key";
4068
+ const availableAuthTypes = providerOptions
4069
+ ? new Set(providerOptions.map((provider) => provider.authType))
4070
+ : new Set(["oauth", "api_key"]);
4071
+ const options = [];
4072
+ if (availableAuthTypes.has("oauth")) {
4073
+ options.push(subscriptionLabel);
4074
+ }
4075
+ if (availableAuthTypes.has("api_key")) {
4076
+ options.push(apiKeyLabel);
4077
+ }
4078
+ if (options.length === 0) {
4079
+ this.showStatus("No login methods available.");
4080
+ return;
4081
+ }
4082
+ if (providerOptions && options.length === 1) {
4083
+ const providerOption = providerOptions[0];
4084
+ if (providerOption) {
4085
+ void this.startProviderLogin(providerOption);
4086
+ }
4087
+ return;
4088
+ }
4089
+ const title = providerOptions?.[0]
4090
+ ? `Select authentication method for ${providerOptions[0].name}:`
4091
+ : "Select authentication method:";
3877
4092
  this.showSelector((done) => {
3878
- const selector = new ExtensionSelectorComponent("Select authentication method:", [subscriptionLabel, apiKeyLabel], (option) => {
4093
+ const selector = new ExtensionSelectorComponent(title, options, (option) => {
3879
4094
  done();
3880
4095
  const authType = option === subscriptionLabel ? "oauth" : "api_key";
4096
+ if (providerOptions) {
4097
+ const providerOption = providerOptions.find((provider) => provider.authType === authType);
4098
+ if (providerOption) {
4099
+ void this.startProviderLogin(providerOption);
4100
+ }
4101
+ return;
4102
+ }
3881
4103
  this.showLoginProviderSelector(authType);
3882
4104
  }, () => {
3883
4105
  done();
@@ -3886,32 +4108,34 @@ export class InteractiveMode {
3886
4108
  return { component: selector, focus: selector };
3887
4109
  });
3888
4110
  }
3889
- showLoginProviderSelector(authType) {
4111
+ showLoginProviderSelector(authType, initialSearchInput) {
3890
4112
  const providerOptions = this.getLoginProviderOptions(authType);
3891
4113
  if (providerOptions.length === 0) {
3892
- this.showStatus(authType === "oauth" ? "No subscription providers available." : "No API key providers available.");
4114
+ const message = authType === "oauth"
4115
+ ? "No subscription providers available."
4116
+ : authType === "api_key"
4117
+ ? "No API key providers available."
4118
+ : "No login providers available.";
4119
+ this.showStatus(message);
3893
4120
  return;
3894
4121
  }
3895
4122
  this.showSelector((done) => {
3896
- const selector = new OAuthSelectorComponent("login", this.session.modelRegistry.authStorage, providerOptions, async (providerId) => {
4123
+ const selector = new OAuthSelectorComponent("login", this.session.modelRegistry.authStorage, providerOptions, async (providerId, selectedAuthType) => {
3897
4124
  done();
3898
- const providerOption = providerOptions.find((provider) => provider.id === providerId);
4125
+ const providerOption = providerOptions.find((provider) => provider.id === providerId && provider.authType === selectedAuthType);
3899
4126
  if (!providerOption) {
3900
4127
  return;
3901
4128
  }
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);
4129
+ await this.startProviderLogin(providerOption);
4130
+ }, () => {
4131
+ done();
4132
+ if (authType) {
4133
+ this.showLoginAuthTypeSelector();
3907
4134
  }
3908
4135
  else {
3909
- await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
4136
+ this.ui.requestRender();
3910
4137
  }
3911
- }, () => {
3912
- done();
3913
- this.showLoginAuthTypeSelector();
3914
- }, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId));
4138
+ }, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId), initialSearchInput);
3915
4139
  return { component: selector, focus: selector };
3916
4140
  });
3917
4141
  }
@@ -4001,30 +4225,18 @@ export class InteractiveMode {
4001
4225
  }
4002
4226
  }
4003
4227
  }
4004
- showBedrockSetupDialog(providerId, providerName) {
4005
- const restoreEditor = () => {
4006
- this.editorContainer.clear();
4007
- this.editorContainer.addChild(this.editor);
4008
- this.ui.setFocus(this.editor);
4009
- this.ui.requestRender();
4010
- };
4011
- const dialog = new LoginDialogComponent(this.ui, providerId, () => restoreEditor(), providerName, "Amazon Bedrock setup");
4012
- dialog.showInfo([
4013
- theme.fg("text", "Amazon Bedrock uses AWS credentials instead of a single API key."),
4014
- theme.fg("text", "Configure an AWS profile, IAM keys, bearer token, or role-based credentials."),
4015
- theme.fg("muted", "See:"),
4016
- theme.fg("accent", ` ${path.join(getDocsPath(), "providers.md")}`),
4017
- ]);
4018
- this.editorContainer.clear();
4019
- this.editorContainer.addChild(dialog);
4020
- this.ui.setFocus(dialog);
4021
- this.ui.requestRender();
4022
- }
4023
4228
  async showApiKeyLoginDialog(providerId, providerName) {
4024
4229
  const previousModel = this.session.model;
4025
4230
  const dialog = new LoginDialogComponent(this.ui, providerId, (_success, _message) => {
4026
4231
  // Completion handled below
4027
4232
  }, providerName);
4233
+ if (providerId === "amazon-bedrock") {
4234
+ dialog.showDetails([
4235
+ theme.fg("text", "You can also use an AWS profile, IAM keys, or role-based credentials."),
4236
+ theme.fg("muted", "See:"),
4237
+ theme.fg("accent", ` ${path.join(getDocsPath(), "providers.md")}`),
4238
+ ]);
4239
+ }
4028
4240
  this.editorContainer.clear();
4029
4241
  this.editorContainer.addChild(dialog);
4030
4242
  this.ui.setFocus(dialog);
@@ -4165,12 +4377,31 @@ export class InteractiveMode {
4165
4377
  this.showWarning("Wait for compaction to finish before reloading.");
4166
4378
  return;
4167
4379
  }
4380
+ const pendingPiClientUpdate = readPiClientUpdateMarker();
4381
+ if (pendingPiClientUpdate) {
4382
+ const reloadStatePath = process.env.PI_CLIENT_RELOAD_STATE_PATH;
4383
+ if (!reloadStatePath) {
4384
+ this.showWarning(`pi-client ${pendingPiClientUpdate.version} is installed. Restart pi-client to load the new runtime.`);
4385
+ return;
4386
+ }
4387
+ writePiClientRuntimeReloadState(reloadStatePath, {
4388
+ sessionId: this.sessionManager.getSessionId(),
4389
+ sessionDir: this.sessionManager.usesDefaultSessionDir() ? undefined : this.sessionManager.getSessionDir(),
4390
+ updateMarkerPath: getPiClientUpdateMarkerPath(),
4391
+ });
4392
+ this.isShuttingDown = true;
4393
+ this.themeController.disableAutoSync();
4394
+ await this.ui.terminal.drainInput(1000);
4395
+ this.stop();
4396
+ await this.runtimeHost.dispose();
4397
+ process.exit(PI_CLIENT_RUNTIME_RELOAD_EXIT_CODE);
4398
+ }
4168
4399
  this.resetExtensionUI();
4169
4400
  const reloadBox = new Container();
4170
4401
  const borderColor = (s) => theme.fg("border", s);
4171
4402
  reloadBox.addChild(new DynamicBorder(borderColor));
4172
4403
  reloadBox.addChild(new Spacer(1));
4173
- reloadBox.addChild(new Text(theme.fg("muted", "Reloading keybindings, extensions, skills, prompts, themes..."), 1, 0));
4404
+ reloadBox.addChild(new Text(theme.fg("muted", "Reloading keybindings, extensions, skills, prompts, themes, and context files..."), 1, 0));
4174
4405
  reloadBox.addChild(new Spacer(1));
4175
4406
  reloadBox.addChild(new DynamicBorder(borderColor));
4176
4407
  const previousEditor = this.editor;
@@ -4234,8 +4465,8 @@ export class InteractiveMode {
4234
4465
  this.showError(`models.json error: ${modelsJsonError}`);
4235
4466
  }
4236
4467
  this.showStatus(savedImplicitProjectTrust
4237
- ? "Reloaded keybindings, extensions, skills, prompts, themes; saved project trust"
4238
- : "Reloaded keybindings, extensions, skills, prompts, themes");
4468
+ ? "Reloaded keybindings, extensions, skills, prompts, themes, and context files; saved project trust"
4469
+ : "Reloaded keybindings, extensions, skills, prompts, themes, and context files");
4239
4470
  dismissReloadBox(this.editor);
4240
4471
  reloadBoxDismissed = true;
4241
4472
  }
@@ -4456,6 +4687,26 @@ export class InteractiveMode {
4456
4687
  handleSessionCommand() {
4457
4688
  const stats = this.session.getSessionStats();
4458
4689
  const sessionName = this.sessionManager.getSessionName();
4690
+ const entries = this.sessionManager.getEntries();
4691
+ const cacheWaste = computeCacheWaste(entries, this.session.modelRegistry);
4692
+ // Cost/token totals per provider/model actually used (e.g. OpenRouter `auto`
4693
+ // resolves to a concrete responseModel), sorted by cost descending.
4694
+ const perModelMap = new Map();
4695
+ for (const entry of entries) {
4696
+ if (entry.type !== "message" || entry.message.role !== "assistant")
4697
+ continue;
4698
+ const message = entry.message;
4699
+ const usage = message.usage;
4700
+ const key = `${message.provider}/${message.responseModel ?? message.model}`;
4701
+ let bucket = perModelMap.get(key);
4702
+ if (!bucket) {
4703
+ bucket = { key, cost: 0, tokens: 0 };
4704
+ perModelMap.set(key, bucket);
4705
+ }
4706
+ bucket.cost += usage.cost.total;
4707
+ bucket.tokens += usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
4708
+ }
4709
+ const perModel = Array.from(perModelMap.values()).sort((a, b) => b.cost - a.cost);
4459
4710
  let info = `${theme.bold("Session Info")}\n\n`;
4460
4711
  if (sessionName) {
4461
4712
  info += `${theme.fg("dim", "Name:")} ${sessionName}\n`;
@@ -4463,24 +4714,42 @@ export class InteractiveMode {
4463
4714
  info += `${theme.fg("dim", "File:")} ${stats.sessionFile ?? "In-memory"}\n`;
4464
4715
  info += `${theme.fg("dim", "ID:")} ${stats.sessionId}\n\n`;
4465
4716
  info += `${theme.bold("Messages")}\n`;
4717
+ info += `${theme.fg("dim", "Total:")} ${stats.totalMessages}\n`;
4466
4718
  info += `${theme.fg("dim", "User:")} ${stats.userMessages}\n`;
4467
4719
  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`;
4720
+ info += `${theme.fg("dim", "Tools:")} ${stats.toolCalls} calls, ${stats.toolResults} results\n\n`;
4471
4721
  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`;
4722
+ // "Input" is the full prompt volume. With cache activity, split it into
4723
+ // cached (served from cache) vs uncached (everything else) - the only
4724
+ // provider-independent split. Cache writes, where reported, are a detail
4725
+ // of the uncached portion.
4726
+ const { input, cacheRead, cacheWrite } = stats.tokens;
4727
+ const promptTokens = input + cacheRead + cacheWrite;
4728
+ info += `${theme.fg("dim", "Input:")} ${promptTokens.toLocaleString()}\n`;
4729
+ if (promptTokens > 0 && (cacheRead > 0 || cacheWrite > 0)) {
4730
+ const hitRate = theme.fg("dim", `(${((cacheRead / promptTokens) * 100).toFixed(1)}%)`);
4731
+ info += ` ${theme.fg("dim", "Cached:")} ${cacheRead.toLocaleString()} ${hitRate}\n`;
4732
+ const written = cacheWrite > 0 ? ` ${theme.fg("dim", `(${cacheWrite.toLocaleString()} written to cache)`)}` : "";
4733
+ info += ` ${theme.fg("dim", "Uncached:")} ${(input + cacheWrite).toLocaleString()}${written}\n`;
4479
4734
  }
4735
+ info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`;
4480
4736
  info += `${theme.fg("dim", "Total:")} ${stats.tokens.total.toLocaleString()}\n`;
4481
- if (stats.cost > 0) {
4737
+ if (stats.cost > 0 || cacheWaste.missedTokens > 0) {
4482
4738
  info += `\n${theme.bold("Cost")}\n`;
4483
- info += `${theme.fg("dim", "Total:")} ${stats.cost.toFixed(4)}`;
4739
+ info += `${theme.fg("dim", "Total:")} $${stats.cost.toFixed(3)}`;
4740
+ if (perModel.length > 1) {
4741
+ for (const entry of perModel) {
4742
+ info += `\n ${theme.fg("dim", `${entry.key}:`)} $${entry.cost.toFixed(3)} ${theme.fg("dim", `(${formatTokens(entry.tokens)} tokens)`)}`;
4743
+ }
4744
+ }
4745
+ if (cacheWaste.missedTokens > 0) {
4746
+ const missLabel = cacheWaste.missCount === 1 ? "1 miss" : `${cacheWaste.missCount} misses`;
4747
+ const detail = `${cacheWaste.missedTokens.toLocaleString()} tokens, ${missLabel}`;
4748
+ info +=
4749
+ cacheWaste.missedCost >= 0.0001
4750
+ ? `\n${theme.fg("dim", "Cache Re-billed:")} $${cacheWaste.missedCost.toFixed(3)} ${theme.fg("dim", `(${detail})`)}`
4751
+ : `\n${theme.fg("dim", "Cache Re-billed:")} ${detail}`;
4752
+ }
4484
4753
  }
4485
4754
  this.chatContainer.addChild(new Spacer(1));
4486
4755
  this.chatContainer.addChild(new Text(info, 1, 0));
@@ -4552,6 +4821,7 @@ export class InteractiveMode {
4552
4821
  const toggleThinking = this.getAppKeyDisplay("app.thinking.toggle");
4553
4822
  const externalEditor = this.getAppKeyDisplay("app.editor.external");
4554
4823
  const cycleModelBackward = this.getAppKeyDisplay("app.model.cycleBackward");
4824
+ const copyMessage = this.getAppKeyDisplay("app.message.copy");
4555
4825
  const followUp = this.getAppKeyDisplay("app.message.followUp");
4556
4826
  const dequeue = this.getAppKeyDisplay("app.message.dequeue");
4557
4827
  const pasteImage = this.getAppKeyDisplay("app.clipboard.pasteImage");
@@ -4594,9 +4864,10 @@ export class InteractiveMode {
4594
4864
  | \`${expandTools}\` | Toggle tool output expansion |
4595
4865
  | \`${toggleThinking}\` | Toggle thinking block visibility |
4596
4866
  | \`${externalEditor}\` | Edit message in external editor |
4867
+ | \`${copyMessage}\` | Copy last assistant message |
4597
4868
  | \`${followUp}\` | Queue follow-up message |
4598
4869
  | \`${dequeue}\` | Restore queued messages |
4599
- | \`${pasteImage}\` | Paste image from clipboard |
4870
+ | \`${pasteImage}\` | Paste image or text from clipboard |
4600
4871
  | \`/\` | Slash commands |
4601
4872
  | \`!\` | Run bash command |
4602
4873
  | \`!!\` | Run bash command (excluded from context) |