@averyyy/pi-coding-agent 0.80.3-piclient.9 → 0.80.6-piclient.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/CHANGELOG.md +86 -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/pi-client-runtime-update.d.ts +14 -0
  57. package/dist/core/pi-client-runtime-update.d.ts.map +1 -0
  58. package/dist/core/pi-client-runtime-update.js +27 -0
  59. package/dist/core/pi-client-runtime-update.js.map +1 -0
  60. package/dist/core/pi-server-client.d.ts.map +1 -1
  61. package/dist/core/pi-server-client.js +122 -5
  62. package/dist/core/pi-server-client.js.map +1 -1
  63. package/dist/core/provider-attribution.d.ts.map +1 -1
  64. package/dist/core/provider-attribution.js +0 -10
  65. package/dist/core/provider-attribution.js.map +1 -1
  66. package/dist/core/resource-loader.d.ts +2 -2
  67. package/dist/core/resource-loader.d.ts.map +1 -1
  68. package/dist/core/resource-loader.js +6 -7
  69. package/dist/core/resource-loader.js.map +1 -1
  70. package/dist/core/sdk.d.ts +1 -1
  71. package/dist/core/sdk.d.ts.map +1 -1
  72. package/dist/core/sdk.js +8 -1
  73. package/dist/core/sdk.js.map +1 -1
  74. package/dist/core/session-manager.d.ts +20 -1
  75. package/dist/core/session-manager.d.ts.map +1 -1
  76. package/dist/core/session-manager.js +97 -68
  77. package/dist/core/session-manager.js.map +1 -1
  78. package/dist/core/settings-manager.d.ts +9 -3
  79. package/dist/core/settings-manager.d.ts.map +1 -1
  80. package/dist/core/settings-manager.js +10 -1
  81. package/dist/core/settings-manager.js.map +1 -1
  82. package/dist/core/slash-commands.d.ts +1 -0
  83. package/dist/core/slash-commands.d.ts.map +1 -1
  84. package/dist/core/slash-commands.js +3 -3
  85. package/dist/core/slash-commands.js.map +1 -1
  86. package/dist/core/tools/edit.d.ts.map +1 -1
  87. package/dist/core/tools/edit.js +2 -2
  88. package/dist/core/tools/edit.js.map +1 -1
  89. package/dist/index.d.ts +3 -2
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +2 -1
  92. package/dist/index.js.map +1 -1
  93. package/dist/main.d.ts +2 -2
  94. package/dist/main.d.ts.map +1 -1
  95. package/dist/main.js +1 -0
  96. package/dist/main.js.map +1 -1
  97. package/dist/modes/interactive/components/config-selector.d.ts +34 -3
  98. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  99. package/dist/modes/interactive/components/config-selector.js +280 -25
  100. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  101. package/dist/modes/interactive/components/custom-entry.d.ts +19 -0
  102. package/dist/modes/interactive/components/custom-entry.d.ts.map +1 -0
  103. package/dist/modes/interactive/components/custom-entry.js +52 -0
  104. package/dist/modes/interactive/components/custom-entry.js.map +1 -0
  105. package/dist/modes/interactive/components/footer.d.ts +4 -0
  106. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  107. package/dist/modes/interactive/components/footer.js +1 -1
  108. package/dist/modes/interactive/components/footer.js.map +1 -1
  109. package/dist/modes/interactive/components/oauth-selector.d.ts +3 -1
  110. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  111. package/dist/modes/interactive/components/oauth-selector.js +17 -6
  112. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  113. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  114. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  115. package/dist/modes/interactive/components/settings-selector.js +12 -1
  116. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  117. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  118. package/dist/modes/interactive/components/thinking-selector.js +2 -1
  119. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  120. package/dist/modes/interactive/interactive-mode.d.ts +15 -3
  121. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  122. package/dist/modes/interactive/interactive-mode.js +328 -61
  123. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  124. package/dist/modes/interactive/theme/dark.json +1 -0
  125. package/dist/modes/interactive/theme/light.json +1 -0
  126. package/dist/modes/interactive/theme/theme-schema.json +6 -2
  127. package/dist/modes/interactive/theme/theme.d.ts +3 -2
  128. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  129. package/dist/modes/interactive/theme/theme.js +10 -3
  130. package/dist/modes/interactive/theme/theme.js.map +1 -1
  131. package/dist/modes/print-mode.d.ts.map +1 -1
  132. package/dist/modes/print-mode.js +1 -1
  133. package/dist/modes/print-mode.js.map +1 -1
  134. package/dist/modes/rpc/rpc-client.d.ts +6 -6
  135. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  136. package/dist/modes/rpc/rpc-client.js +3 -3
  137. package/dist/modes/rpc/rpc-client.js.map +1 -1
  138. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  139. package/dist/modes/rpc/rpc-mode.js +4 -1
  140. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  141. package/dist/package-manager-cli.d.ts +2 -2
  142. package/dist/package-manager-cli.d.ts.map +1 -1
  143. package/dist/package-manager-cli.js +71 -17
  144. package/dist/package-manager-cli.js.map +1 -1
  145. package/dist/utils/clipboard-image.d.ts.map +1 -1
  146. package/dist/utils/clipboard-image.js +1 -1
  147. package/dist/utils/clipboard-image.js.map +1 -1
  148. package/docs/custom-provider.md +4 -3
  149. package/docs/extensions.md +75 -18
  150. package/docs/models.md +49 -8
  151. package/docs/packages.md +2 -2
  152. package/docs/rpc.md +15 -5
  153. package/docs/sdk.md +51 -1
  154. package/docs/session-format.md +18 -8
  155. package/docs/settings.md +3 -2
  156. package/docs/themes.md +5 -3
  157. package/docs/tui.md +1 -1
  158. package/docs/usage.md +2 -2
  159. package/examples/extensions/README.md +1 -0
  160. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  161. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  162. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  163. package/examples/extensions/entry-renderer.ts +41 -0
  164. package/examples/extensions/gondolin/package-lock.json +2 -2
  165. package/examples/extensions/gondolin/package.json +1 -1
  166. package/examples/extensions/preset.ts +2 -2
  167. package/examples/extensions/question.ts +1 -0
  168. package/examples/extensions/reload-runtime.ts +2 -2
  169. package/examples/extensions/sandbox/package-lock.json +2 -2
  170. package/examples/extensions/sandbox/package.json +1 -1
  171. package/examples/extensions/with-deps/package-lock.json +2 -2
  172. package/examples/extensions/with-deps/package.json +1 -1
  173. package/examples/sdk/12-full-control.ts +1 -1
  174. package/examples/sdk/README.md +1 -1
  175. package/package.json +4 -4
@@ -13,15 +13,17 @@ 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";
@@ -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)) {
@@ -131,6 +137,42 @@ export function isApiKeyLoginProvider(providerId, oauthProviderIds, builtInProvi
131
137
  }
132
138
  return !oauthProviderIds.has(providerId);
133
139
  }
140
+ const AUTH_TYPE_ORDER = { oauth: 0, api_key: 1 };
141
+ function createFuzzyAutocompleteItems(items, prefix, getSearchText, toAutocompleteItem) {
142
+ const filtered = fuzzyFilter(items, prefix, getSearchText);
143
+ if (filtered.length === 0)
144
+ return null;
145
+ return filtered.map(toAutocompleteItem);
146
+ }
147
+ function getLoginProviderCompletionOptions(providerOptions) {
148
+ const byId = new Map();
149
+ for (const provider of providerOptions) {
150
+ const existing = byId.get(provider.id);
151
+ if (existing) {
152
+ if (!existing.authTypes.includes(provider.authType)) {
153
+ existing.authTypes.push(provider.authType);
154
+ existing.authTypes.sort((a, b) => AUTH_TYPE_ORDER[a] - AUTH_TYPE_ORDER[b]);
155
+ }
156
+ continue;
157
+ }
158
+ byId.set(provider.id, {
159
+ id: provider.id,
160
+ name: provider.name,
161
+ authTypes: [provider.authType],
162
+ });
163
+ }
164
+ return Array.from(byId.values()).sort((a, b) => a.name.localeCompare(b.name));
165
+ }
166
+ function getLoginProviderSearchText(provider) {
167
+ const authTypes = provider.authTypes
168
+ .map((authType) => `${authType} ${formatAuthSelectorProviderType(authType)}`)
169
+ .join(" ");
170
+ return `${provider.id} ${provider.name} ${authTypes}`;
171
+ }
172
+ function formatLoginProviderCompletionDescription(provider) {
173
+ const authTypes = provider.authTypes.map(formatAuthSelectorProviderType).join("/");
174
+ return provider.name === provider.id ? authTypes : `${provider.name} · ${authTypes}`;
175
+ }
134
176
  export class InteractiveMode {
135
177
  runtimeHost;
136
178
  ui;
@@ -317,6 +359,7 @@ export class InteractiveMode {
317
359
  const slashCommands = BUILTIN_SLASH_COMMANDS.map((command) => ({
318
360
  name: command.name,
319
361
  description: command.description,
362
+ ...(command.argumentHint && { argumentHint: command.argumentHint }),
320
363
  }));
321
364
  const modelCommand = slashCommands.find((command) => command.name === "model");
322
365
  if (modelCommand) {
@@ -334,17 +377,24 @@ export class InteractiveMode {
334
377
  name: m.name,
335
378
  label: `${m.provider}/${m.id}`,
336
379
  }));
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) => ({
380
+ return createFuzzyAutocompleteItems(items, prefix, getModelSearchText, (item) => ({
342
381
  value: item.label,
343
382
  label: item.id,
344
383
  description: item.provider,
345
384
  }));
346
385
  };
347
386
  }
387
+ const loginCommand = slashCommands.find((command) => command.name === "login");
388
+ if (loginCommand) {
389
+ loginCommand.getArgumentCompletions = (prefix) => {
390
+ const providers = getLoginProviderCompletionOptions(this.getLoginProviderOptions());
391
+ return createFuzzyAutocompleteItems(providers, prefix, getLoginProviderSearchText, (provider) => ({
392
+ value: provider.id,
393
+ label: provider.id,
394
+ description: formatLoginProviderCompletionDescription(provider),
395
+ }));
396
+ };
397
+ }
348
398
  // Convert prompt templates to SlashCommand format for autocomplete
349
399
  const templateCommands = this.session.promptTemplates.map((cmd) => ({
350
400
  name: cmd.name,
@@ -1155,7 +1205,7 @@ export class InteractiveMode {
1155
1205
  this.restoreQueuedMessagesToEditor({ abort: true });
1156
1206
  },
1157
1207
  commandContextActions: {
1158
- waitForIdle: () => this.session.agent.waitForIdle(),
1208
+ waitForIdle: () => this.session.waitForIdle(),
1159
1209
  newSession: async (options) => {
1160
1210
  this.clearStatusIndicator();
1161
1211
  try {
@@ -1206,7 +1256,7 @@ export class InteractiveMode {
1206
1256
  },
1207
1257
  shutdownHandler: () => {
1208
1258
  this.shutdownRequested = true;
1209
- if (!this.session.isStreaming) {
1259
+ if (this.session.isIdle) {
1210
1260
  void this.shutdown();
1211
1261
  }
1212
1262
  },
@@ -1299,7 +1349,7 @@ export class InteractiveMode {
1299
1349
  sessionManager: this.sessionManager,
1300
1350
  modelRegistry: this.session.modelRegistry,
1301
1351
  model: this.session.model,
1302
- isIdle: () => !this.session.isStreaming,
1352
+ isIdle: () => this.session.isIdle,
1303
1353
  isProjectTrusted: () => this.settingsManager.isProjectTrusted(),
1304
1354
  signal: this.session.agent.signal,
1305
1355
  abort: () => {
@@ -2105,9 +2155,10 @@ export class InteractiveMode {
2105
2155
  this.editor.setText("");
2106
2156
  return;
2107
2157
  }
2108
- if (text === "/login") {
2109
- this.showOAuthSelector("login");
2158
+ if (text === "/login" || text.startsWith("/login ")) {
2159
+ const providerRef = text.startsWith("/login ") ? text.slice(7).trim() : undefined;
2110
2160
  this.editor.setText("");
2161
+ await this.handleLoginCommand(providerRef);
2111
2162
  return;
2112
2163
  }
2113
2164
  if (text === "/logout") {
@@ -2131,6 +2182,12 @@ export class InteractiveMode {
2131
2182
  await this.handleReloadCommand();
2132
2183
  return;
2133
2184
  }
2185
+ const pendingPiClientUpdate = readPiClientUpdateMarker();
2186
+ if (pendingPiClientUpdate) {
2187
+ this.showWarning(`pi-client ${pendingPiClientUpdate.version} is installed. Run /reload to continue this session with the new version.`);
2188
+ this.editor.setText(text);
2189
+ return;
2190
+ }
2134
2191
  if (text === "/debug") {
2135
2192
  this.handleDebugCommand();
2136
2193
  this.editor.setText("");
@@ -2241,6 +2298,12 @@ export class InteractiveMode {
2241
2298
  this.updatePendingMessagesDisplay();
2242
2299
  this.ui.requestRender();
2243
2300
  break;
2301
+ case "entry_appended":
2302
+ if (event.entry.type === "custom") {
2303
+ this.addCustomEntryToChat(event.entry);
2304
+ this.ui.requestRender();
2305
+ }
2306
+ break;
2244
2307
  case "session_info_changed":
2245
2308
  this.updateTerminalTitle();
2246
2309
  this.footer.invalidate();
@@ -2326,6 +2389,7 @@ export class InteractiveMode {
2326
2389
  for (const [, component] of this.pendingTools.entries()) {
2327
2390
  component.setArgsComplete();
2328
2391
  }
2392
+ this.maybeShowCacheMissNotice(this.streamingMessage);
2329
2393
  }
2330
2394
  this.streamingComponent = undefined;
2331
2395
  this.streamingMessage = undefined;
@@ -2376,9 +2440,11 @@ export class InteractiveMode {
2376
2440
  this.streamingMessage = undefined;
2377
2441
  }
2378
2442
  this.pendingTools.clear();
2379
- await this.checkShutdownRequested();
2380
2443
  this.ui.requestRender();
2381
2444
  break;
2445
+ case "agent_settled":
2446
+ await this.checkShutdownRequested();
2447
+ break;
2382
2448
  case "compaction_start": {
2383
2449
  if (this.settingsManager.getShowTerminalProgress()) {
2384
2450
  this.ui.terminal.setProgress(true);
@@ -2486,6 +2552,25 @@ export class InteractiveMode {
2486
2552
  this.lastStatusText = text;
2487
2553
  this.ui.requestRender();
2488
2554
  }
2555
+ addCustomEntryToChat(entry) {
2556
+ const renderer = this.session.extensionRunner.getEntryRenderer(entry.customType);
2557
+ if (!renderer) {
2558
+ return;
2559
+ }
2560
+ const component = new CustomEntryComponent(entry, renderer);
2561
+ component.setExpanded(this.toolOutputExpanded);
2562
+ if (!component.hasContent()) {
2563
+ return;
2564
+ }
2565
+ if (this.streamingComponent) {
2566
+ const streamingIndex = this.chatContainer.children.indexOf(this.streamingComponent);
2567
+ if (streamingIndex >= 0) {
2568
+ this.chatContainer.children.splice(streamingIndex, 0, component);
2569
+ return;
2570
+ }
2571
+ }
2572
+ this.chatContainer.addChild(component);
2573
+ }
2489
2574
  addMessageToChat(message, options) {
2490
2575
  switch (message.role) {
2491
2576
  case "bashExecution": {
@@ -2563,20 +2648,24 @@ export class InteractiveMode {
2563
2648
  }
2564
2649
  }
2565
2650
  }
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 = {}) {
2651
+ renderSessionItems(items, options = {}) {
2573
2652
  this.pendingTools.clear();
2574
2653
  const renderedPendingTools = new Map();
2654
+ // Cache-miss notices are not persisted; re-derive them from the full entry
2655
+ // list and re-inject them after the assistant messages that paid for them.
2656
+ const cacheMisses = this.settingsManager.getShowCacheMissNotices()
2657
+ ? collectCacheMisses(this.sessionManager.getEntries(), this.session.modelRegistry)
2658
+ : new Map();
2575
2659
  if (options.updateFooter) {
2576
2660
  this.footer.invalidate();
2577
2661
  this.updateEditorBorderColor();
2578
2662
  }
2579
- for (const message of sessionContext.messages) {
2663
+ for (const item of items) {
2664
+ if (isCustomSessionEntry(item)) {
2665
+ this.addCustomEntryToChat(item);
2666
+ continue;
2667
+ }
2668
+ const message = item;
2580
2669
  // Assistant messages need special handling for tool calls
2581
2670
  if (message.role === "assistant") {
2582
2671
  this.addMessageToChat(message);
@@ -2608,6 +2697,11 @@ export class InteractiveMode {
2608
2697
  }
2609
2698
  }
2610
2699
  }
2700
+ if (message.stopReason !== "aborted" && message.stopReason !== "error") {
2701
+ const miss = cacheMisses.get(message);
2702
+ if (miss)
2703
+ this.addCacheMissNotice(miss);
2704
+ }
2611
2705
  }
2612
2706
  else if (message.role === "toolResult") {
2613
2707
  // Match tool results to pending tool components
@@ -2627,10 +2721,53 @@ export class InteractiveMode {
2627
2721
  }
2628
2722
  this.ui.requestRender();
2629
2723
  }
2724
+ /**
2725
+ * Render session entries to chat. Used for initial load and rebuild after compaction.
2726
+ * @param entries Compaction-aware session entries to render
2727
+ * @param options.updateFooter Update footer state
2728
+ * @param options.populateHistory Add user messages to editor history
2729
+ */
2730
+ renderSessionEntries(entries, options = {}) {
2731
+ const items = entries.flatMap((entry) => {
2732
+ if (entry.type === "custom") {
2733
+ return [entry];
2734
+ }
2735
+ return sessionEntryToContextMessages(entry);
2736
+ });
2737
+ this.renderSessionItems(items, options);
2738
+ }
2739
+ /**
2740
+ * Show a transcript notice when a completed assistant message paid for a
2741
+ * significant cache miss. Only states observable facts: the miss itself,
2742
+ * a model switch, or an idle gap past the cache TTL.
2743
+ */
2744
+ maybeShowCacheMissNotice(message) {
2745
+ if (!this.settingsManager.getShowCacheMissNotices())
2746
+ return;
2747
+ // Entries don't contain `message` yet: message_end fires before persistence.
2748
+ const miss = detectCacheMiss(this.sessionManager.getEntries(), message, this.session.modelRegistry);
2749
+ if (miss)
2750
+ this.addCacheMissNotice(miss);
2751
+ }
2752
+ addCacheMissNotice(miss) {
2753
+ if (miss.missedTokens < 20_000 && miss.missedCost < 0.1)
2754
+ return;
2755
+ const cost = miss.missedCost >= 0.01 ? ` (~$${miss.missedCost.toFixed(2)})` : "";
2756
+ const reBilled = `${formatTokens(miss.missedTokens)} tokens re-billed${cost}`;
2757
+ let label = "Cache miss";
2758
+ if (miss.modelChanged) {
2759
+ label = "Cache miss after model switch";
2760
+ }
2761
+ else if (miss.idleMs >= CACHE_TTL_MS) {
2762
+ label = `Cache miss after ${Math.round(miss.idleMs / 60_000)}m idle`;
2763
+ }
2764
+ const text = theme.fg("warning", `${label}: ${reBilled}`);
2765
+ this.chatContainer.addChild(new Spacer(1));
2766
+ this.chatContainer.addChild(new Text(text, 1, 0));
2767
+ }
2630
2768
  renderInitialMessages() {
2631
- // Get aligned messages and entries from session context
2632
- const context = this.sessionManager.buildSessionContext();
2633
- this.renderSessionContext(context, {
2769
+ const entries = this.sessionManager.buildContextEntries();
2770
+ this.renderSessionEntries(entries, {
2634
2771
  updateFooter: true,
2635
2772
  populateHistory: true,
2636
2773
  });
@@ -2666,8 +2803,7 @@ export class InteractiveMode {
2666
2803
  }
2667
2804
  rebuildChatFromMessages() {
2668
2805
  this.chatContainer.clear();
2669
- const context = this.sessionManager.buildSessionContext();
2670
- this.renderSessionContext(context);
2806
+ this.renderSessionEntries(this.sessionManager.buildContextEntries());
2671
2807
  }
2672
2808
  // =========================================================================
2673
2809
  // Key handlers
@@ -3266,6 +3402,7 @@ export class InteractiveMode {
3266
3402
  doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
3267
3403
  treeFilterMode: this.settingsManager.getTreeFilterMode(),
3268
3404
  showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
3405
+ showCacheMissNotices: this.settingsManager.getShowCacheMissNotices(),
3269
3406
  defaultProjectTrust: this.settingsManager.getDefaultProjectTrust(),
3270
3407
  editorPaddingX: this.settingsManager.getEditorPaddingX(),
3271
3408
  outputPad: this.settingsManager.getOutputPad(),
@@ -3341,6 +3478,10 @@ export class InteractiveMode {
3341
3478
  this.chatContainer.clear();
3342
3479
  this.rebuildChatFromMessages();
3343
3480
  },
3481
+ onShowCacheMissNoticesChange: (shown) => {
3482
+ this.settingsManager.setShowCacheMissNotices(shown);
3483
+ this.rebuildChatFromMessages();
3484
+ },
3344
3485
  onCollapseChangelogChange: (collapsed) => {
3345
3486
  this.settingsManager.setCollapseChangelog(collapsed);
3346
3487
  },
@@ -3630,19 +3771,17 @@ export class InteractiveMode {
3630
3771
  const initialSelectedId = userMessages[userMessages.length - 1]?.entryId;
3631
3772
  this.showSelector((done) => {
3632
3773
  const selector = new UserMessageSelectorComponent(userMessages.map((m) => ({ id: m.entryId, text: m.text })), async (entryId) => {
3774
+ done();
3633
3775
  try {
3634
3776
  const result = await this.runtimeHost.fork(entryId);
3635
3777
  if (result.cancelled) {
3636
- done();
3637
3778
  this.ui.requestRender();
3638
3779
  return;
3639
3780
  }
3640
3781
  this.editor.setText(result.selectedText ?? "");
3641
- done();
3642
3782
  this.showStatus("Forked to new session");
3643
3783
  }
3644
3784
  catch (error) {
3645
- done();
3646
3785
  this.showError(error instanceof Error ? error.message : String(error));
3647
3786
  }
3648
3787
  }, () => {
@@ -3871,13 +4010,82 @@ export class InteractiveMode {
3871
4010
  }
3872
4011
  return options.sort((a, b) => a.name.localeCompare(b.name));
3873
4012
  }
3874
- showLoginAuthTypeSelector() {
4013
+ findLoginProviderOptions(providerRef) {
4014
+ const normalizedProviderRef = providerRef.trim().toLowerCase();
4015
+ if (!normalizedProviderRef) {
4016
+ return [];
4017
+ }
4018
+ return this.getLoginProviderOptions().filter((provider) => provider.id.toLowerCase() === normalizedProviderRef ||
4019
+ provider.name.toLowerCase() === normalizedProviderRef);
4020
+ }
4021
+ async handleLoginCommand(providerRef) {
4022
+ if (!providerRef) {
4023
+ this.showLoginAuthTypeSelector();
4024
+ return;
4025
+ }
4026
+ const providerOptions = this.findLoginProviderOptions(providerRef);
4027
+ if (providerOptions.length === 1) {
4028
+ await this.startProviderLogin(providerOptions[0]);
4029
+ return;
4030
+ }
4031
+ if (providerOptions.length > 1) {
4032
+ const providerIds = new Set(providerOptions.map((provider) => provider.id));
4033
+ if (providerIds.size === 1) {
4034
+ this.showLoginAuthTypeSelector(providerOptions);
4035
+ return;
4036
+ }
4037
+ }
4038
+ this.showLoginProviderSelector(undefined, providerRef);
4039
+ }
4040
+ async startProviderLogin(providerOption) {
4041
+ if (providerOption.authType === "oauth") {
4042
+ await this.showLoginDialog(providerOption.id, providerOption.name);
4043
+ }
4044
+ else if (providerOption.id === BEDROCK_PROVIDER_ID) {
4045
+ this.showBedrockSetupDialog(providerOption.id, providerOption.name);
4046
+ }
4047
+ else {
4048
+ await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
4049
+ }
4050
+ }
4051
+ showLoginAuthTypeSelector(providerOptions) {
3875
4052
  const subscriptionLabel = "Use a subscription";
3876
4053
  const apiKeyLabel = "Use an API key";
4054
+ const availableAuthTypes = providerOptions
4055
+ ? new Set(providerOptions.map((provider) => provider.authType))
4056
+ : new Set(["oauth", "api_key"]);
4057
+ const options = [];
4058
+ if (availableAuthTypes.has("oauth")) {
4059
+ options.push(subscriptionLabel);
4060
+ }
4061
+ if (availableAuthTypes.has("api_key")) {
4062
+ options.push(apiKeyLabel);
4063
+ }
4064
+ if (options.length === 0) {
4065
+ this.showStatus("No login methods available.");
4066
+ return;
4067
+ }
4068
+ if (providerOptions && options.length === 1) {
4069
+ const providerOption = providerOptions[0];
4070
+ if (providerOption) {
4071
+ void this.startProviderLogin(providerOption);
4072
+ }
4073
+ return;
4074
+ }
4075
+ const title = providerOptions?.[0]
4076
+ ? `Select authentication method for ${providerOptions[0].name}:`
4077
+ : "Select authentication method:";
3877
4078
  this.showSelector((done) => {
3878
- const selector = new ExtensionSelectorComponent("Select authentication method:", [subscriptionLabel, apiKeyLabel], (option) => {
4079
+ const selector = new ExtensionSelectorComponent(title, options, (option) => {
3879
4080
  done();
3880
4081
  const authType = option === subscriptionLabel ? "oauth" : "api_key";
4082
+ if (providerOptions) {
4083
+ const providerOption = providerOptions.find((provider) => provider.authType === authType);
4084
+ if (providerOption) {
4085
+ void this.startProviderLogin(providerOption);
4086
+ }
4087
+ return;
4088
+ }
3881
4089
  this.showLoginProviderSelector(authType);
3882
4090
  }, () => {
3883
4091
  done();
@@ -3886,32 +4094,34 @@ export class InteractiveMode {
3886
4094
  return { component: selector, focus: selector };
3887
4095
  });
3888
4096
  }
3889
- showLoginProviderSelector(authType) {
4097
+ showLoginProviderSelector(authType, initialSearchInput) {
3890
4098
  const providerOptions = this.getLoginProviderOptions(authType);
3891
4099
  if (providerOptions.length === 0) {
3892
- this.showStatus(authType === "oauth" ? "No subscription providers available." : "No API key providers available.");
4100
+ const message = authType === "oauth"
4101
+ ? "No subscription providers available."
4102
+ : authType === "api_key"
4103
+ ? "No API key providers available."
4104
+ : "No login providers available.";
4105
+ this.showStatus(message);
3893
4106
  return;
3894
4107
  }
3895
4108
  this.showSelector((done) => {
3896
- const selector = new OAuthSelectorComponent("login", this.session.modelRegistry.authStorage, providerOptions, async (providerId) => {
4109
+ const selector = new OAuthSelectorComponent("login", this.session.modelRegistry.authStorage, providerOptions, async (providerId, selectedAuthType) => {
3897
4110
  done();
3898
- const providerOption = providerOptions.find((provider) => provider.id === providerId);
4111
+ const providerOption = providerOptions.find((provider) => provider.id === providerId && provider.authType === selectedAuthType);
3899
4112
  if (!providerOption) {
3900
4113
  return;
3901
4114
  }
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);
4115
+ await this.startProviderLogin(providerOption);
4116
+ }, () => {
4117
+ done();
4118
+ if (authType) {
4119
+ this.showLoginAuthTypeSelector();
3907
4120
  }
3908
4121
  else {
3909
- await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
4122
+ this.ui.requestRender();
3910
4123
  }
3911
- }, () => {
3912
- done();
3913
- this.showLoginAuthTypeSelector();
3914
- }, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId));
4124
+ }, (providerId) => this.session.modelRegistry.getProviderAuthStatus(providerId), initialSearchInput);
3915
4125
  return { component: selector, focus: selector };
3916
4126
  });
3917
4127
  }
@@ -4165,12 +4375,31 @@ export class InteractiveMode {
4165
4375
  this.showWarning("Wait for compaction to finish before reloading.");
4166
4376
  return;
4167
4377
  }
4378
+ const pendingPiClientUpdate = readPiClientUpdateMarker();
4379
+ if (pendingPiClientUpdate) {
4380
+ const reloadStatePath = process.env.PI_CLIENT_RELOAD_STATE_PATH;
4381
+ if (!reloadStatePath) {
4382
+ this.showWarning(`pi-client ${pendingPiClientUpdate.version} is installed. Restart pi-client to load the new runtime.`);
4383
+ return;
4384
+ }
4385
+ writePiClientRuntimeReloadState(reloadStatePath, {
4386
+ sessionId: this.sessionManager.getSessionId(),
4387
+ sessionDir: this.sessionManager.usesDefaultSessionDir() ? undefined : this.sessionManager.getSessionDir(),
4388
+ updateMarkerPath: getPiClientUpdateMarkerPath(),
4389
+ });
4390
+ this.isShuttingDown = true;
4391
+ this.themeController.disableAutoSync();
4392
+ await this.ui.terminal.drainInput(1000);
4393
+ this.stop();
4394
+ await this.runtimeHost.dispose();
4395
+ process.exit(PI_CLIENT_RUNTIME_RELOAD_EXIT_CODE);
4396
+ }
4168
4397
  this.resetExtensionUI();
4169
4398
  const reloadBox = new Container();
4170
4399
  const borderColor = (s) => theme.fg("border", s);
4171
4400
  reloadBox.addChild(new DynamicBorder(borderColor));
4172
4401
  reloadBox.addChild(new Spacer(1));
4173
- reloadBox.addChild(new Text(theme.fg("muted", "Reloading keybindings, extensions, skills, prompts, themes..."), 1, 0));
4402
+ reloadBox.addChild(new Text(theme.fg("muted", "Reloading keybindings, extensions, skills, prompts, themes, and context files..."), 1, 0));
4174
4403
  reloadBox.addChild(new Spacer(1));
4175
4404
  reloadBox.addChild(new DynamicBorder(borderColor));
4176
4405
  const previousEditor = this.editor;
@@ -4234,8 +4463,8 @@ export class InteractiveMode {
4234
4463
  this.showError(`models.json error: ${modelsJsonError}`);
4235
4464
  }
4236
4465
  this.showStatus(savedImplicitProjectTrust
4237
- ? "Reloaded keybindings, extensions, skills, prompts, themes; saved project trust"
4238
- : "Reloaded keybindings, extensions, skills, prompts, themes");
4466
+ ? "Reloaded keybindings, extensions, skills, prompts, themes, and context files; saved project trust"
4467
+ : "Reloaded keybindings, extensions, skills, prompts, themes, and context files");
4239
4468
  dismissReloadBox(this.editor);
4240
4469
  reloadBoxDismissed = true;
4241
4470
  }
@@ -4456,6 +4685,26 @@ export class InteractiveMode {
4456
4685
  handleSessionCommand() {
4457
4686
  const stats = this.session.getSessionStats();
4458
4687
  const sessionName = this.sessionManager.getSessionName();
4688
+ const entries = this.sessionManager.getEntries();
4689
+ const cacheWaste = computeCacheWaste(entries, this.session.modelRegistry);
4690
+ // Cost/token totals per provider/model actually used (e.g. OpenRouter `auto`
4691
+ // resolves to a concrete responseModel), sorted by cost descending.
4692
+ const perModelMap = new Map();
4693
+ for (const entry of entries) {
4694
+ if (entry.type !== "message" || entry.message.role !== "assistant")
4695
+ continue;
4696
+ const message = entry.message;
4697
+ const usage = message.usage;
4698
+ const key = `${message.provider}/${message.responseModel ?? message.model}`;
4699
+ let bucket = perModelMap.get(key);
4700
+ if (!bucket) {
4701
+ bucket = { key, cost: 0, tokens: 0 };
4702
+ perModelMap.set(key, bucket);
4703
+ }
4704
+ bucket.cost += usage.cost.total;
4705
+ bucket.tokens += usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
4706
+ }
4707
+ const perModel = Array.from(perModelMap.values()).sort((a, b) => b.cost - a.cost);
4459
4708
  let info = `${theme.bold("Session Info")}\n\n`;
4460
4709
  if (sessionName) {
4461
4710
  info += `${theme.fg("dim", "Name:")} ${sessionName}\n`;
@@ -4463,24 +4712,42 @@ export class InteractiveMode {
4463
4712
  info += `${theme.fg("dim", "File:")} ${stats.sessionFile ?? "In-memory"}\n`;
4464
4713
  info += `${theme.fg("dim", "ID:")} ${stats.sessionId}\n\n`;
4465
4714
  info += `${theme.bold("Messages")}\n`;
4715
+ info += `${theme.fg("dim", "Total:")} ${stats.totalMessages}\n`;
4466
4716
  info += `${theme.fg("dim", "User:")} ${stats.userMessages}\n`;
4467
4717
  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`;
4718
+ info += `${theme.fg("dim", "Tools:")} ${stats.toolCalls} calls, ${stats.toolResults} results\n\n`;
4471
4719
  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`;
4720
+ // "Input" is the full prompt volume. With cache activity, split it into
4721
+ // cached (served from cache) vs uncached (everything else) - the only
4722
+ // provider-independent split. Cache writes, where reported, are a detail
4723
+ // of the uncached portion.
4724
+ const { input, cacheRead, cacheWrite } = stats.tokens;
4725
+ const promptTokens = input + cacheRead + cacheWrite;
4726
+ info += `${theme.fg("dim", "Input:")} ${promptTokens.toLocaleString()}\n`;
4727
+ if (promptTokens > 0 && (cacheRead > 0 || cacheWrite > 0)) {
4728
+ const hitRate = theme.fg("dim", `(${((cacheRead / promptTokens) * 100).toFixed(1)}%)`);
4729
+ info += ` ${theme.fg("dim", "Cached:")} ${cacheRead.toLocaleString()} ${hitRate}\n`;
4730
+ const written = cacheWrite > 0 ? ` ${theme.fg("dim", `(${cacheWrite.toLocaleString()} written to cache)`)}` : "";
4731
+ info += ` ${theme.fg("dim", "Uncached:")} ${(input + cacheWrite).toLocaleString()}${written}\n`;
4479
4732
  }
4733
+ info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`;
4480
4734
  info += `${theme.fg("dim", "Total:")} ${stats.tokens.total.toLocaleString()}\n`;
4481
- if (stats.cost > 0) {
4735
+ if (stats.cost > 0 || cacheWaste.missedTokens > 0) {
4482
4736
  info += `\n${theme.bold("Cost")}\n`;
4483
- info += `${theme.fg("dim", "Total:")} ${stats.cost.toFixed(4)}`;
4737
+ info += `${theme.fg("dim", "Total:")} $${stats.cost.toFixed(3)}`;
4738
+ if (perModel.length > 1) {
4739
+ for (const entry of perModel) {
4740
+ info += `\n ${theme.fg("dim", `${entry.key}:`)} $${entry.cost.toFixed(3)} ${theme.fg("dim", `(${formatTokens(entry.tokens)} tokens)`)}`;
4741
+ }
4742
+ }
4743
+ if (cacheWaste.missedTokens > 0) {
4744
+ const missLabel = cacheWaste.missCount === 1 ? "1 miss" : `${cacheWaste.missCount} misses`;
4745
+ const detail = `${cacheWaste.missedTokens.toLocaleString()} tokens, ${missLabel}`;
4746
+ info +=
4747
+ cacheWaste.missedCost >= 0.0001
4748
+ ? `\n${theme.fg("dim", "Cache Re-billed:")} $${cacheWaste.missedCost.toFixed(3)} ${theme.fg("dim", `(${detail})`)}`
4749
+ : `\n${theme.fg("dim", "Cache Re-billed:")} ${detail}`;
4750
+ }
4484
4751
  }
4485
4752
  this.chatContainer.addChild(new Spacer(1));
4486
4753
  this.chatContainer.addChild(new Text(info, 1, 0));