@selesai/code 0.13.26 → 0.13.27

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 (119) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/dist/cli/args.js +1 -0
  3. package/dist/cli/session-picker.d.ts +1 -1
  4. package/dist/config.js +1 -1
  5. package/dist/core/agent-session-auto-handoff.test.js +2 -1
  6. package/dist/core/agent-session-length-continuation.test.js +2 -1
  7. package/dist/core/agent-session.d.ts +58 -7
  8. package/dist/core/agent-session.js +334 -167
  9. package/dist/core/bug-report-upload.d.ts +11 -0
  10. package/dist/core/bug-report-upload.js +21 -0
  11. package/dist/core/bug-report.d.ts +175 -0
  12. package/dist/core/bug-report.js +291 -0
  13. package/dist/core/cache-stats.js +12 -1
  14. package/dist/core/cache-warmer.d.ts +101 -0
  15. package/dist/core/cache-warmer.js +341 -0
  16. package/dist/core/compaction/branch-summarization.js +2 -2
  17. package/dist/core/compaction/compaction.d.ts +6 -5
  18. package/dist/core/compaction/compaction.js +49 -42
  19. package/dist/core/crash-log.d.ts +21 -0
  20. package/dist/core/crash-log.js +70 -0
  21. package/dist/core/experimental.d.ts +0 -4
  22. package/dist/core/experimental.js +0 -4
  23. package/dist/core/extensions/index.d.ts +1 -1
  24. package/dist/core/extensions/jiti-loader.d.ts +1 -0
  25. package/dist/core/extensions/jiti-loader.js +3 -0
  26. package/dist/core/extensions/jiti-static-loader.d.ts +1 -0
  27. package/dist/core/extensions/jiti-static-loader.js +3 -0
  28. package/dist/core/extensions/loader.js +39 -49
  29. package/dist/core/extensions/runner.d.ts +10 -7
  30. package/dist/core/extensions/runner.js +85 -67
  31. package/dist/core/extensions/types.d.ts +66 -52
  32. package/dist/core/extensions/virtual-modules.d.ts +2 -0
  33. package/dist/core/extensions/virtual-modules.js +39 -0
  34. package/dist/core/extensions/wrapper.js +1 -20
  35. package/dist/core/index.d.ts +2 -1
  36. package/dist/core/keybindings.d.ts +1 -1
  37. package/dist/core/keybindings.js +1 -1
  38. package/dist/core/messages.js +1 -0
  39. package/dist/core/model-config.d.ts +101 -20
  40. package/dist/core/model-config.js +25 -18
  41. package/dist/core/model-resolver.js +2 -1
  42. package/dist/core/model-runtime.js +5 -3
  43. package/dist/core/provider-composer.d.ts +7 -2
  44. package/dist/core/provider-composer.js +10 -5
  45. package/dist/core/radius.d.ts +3 -0
  46. package/dist/core/radius.js +6 -0
  47. package/dist/core/sdk.js +60 -38
  48. package/dist/core/session-export.d.ts +6 -2
  49. package/dist/core/session-export.js +14 -13
  50. package/dist/core/session-format.test.d.ts +1 -0
  51. package/dist/core/session-format.test.js +96 -0
  52. package/dist/core/session-manager.d.ts +29 -6
  53. package/dist/core/session-manager.js +159 -96
  54. package/dist/core/settings-manager.d.ts +22 -5
  55. package/dist/core/settings-manager.js +56 -23
  56. package/dist/core/slash-commands.js +1 -0
  57. package/dist/core/system-prompt.d.ts +49 -4
  58. package/dist/core/system-prompt.js +155 -102
  59. package/dist/core/system-prompt.test.js +61 -23
  60. package/dist/core/tools/bash.d.ts +2 -1
  61. package/dist/core/tools/bash.js +10 -4
  62. package/dist/core/tools/edit.js +1 -2
  63. package/dist/core/tools/read.js +1 -2
  64. package/dist/core/tools/renderers/bash.js +9 -1
  65. package/dist/core/tools/write.js +1 -2
  66. package/dist/core/usage-totals.d.ts +1 -1
  67. package/dist/core/usage-totals.js +5 -1
  68. package/dist/extensions/pi-web-agent/src/backends/factory.ts +14 -6
  69. package/dist/extensions/pi-web-agent/src/extension.ts +3 -1
  70. package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +7 -3
  71. package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +3 -0
  72. package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +2 -0
  73. package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +4 -2
  74. package/dist/extensions/pi-web-agent/src/search/tokenin.ts +4 -1
  75. package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +2 -1
  76. package/dist/extensions/pi-web-agent/src/types.ts +4 -0
  77. package/dist/index.d.ts +3 -2
  78. package/dist/main.js +13 -9
  79. package/dist/migrations.js +2 -2
  80. package/dist/modes/interactive/bug-report.d.ts +15 -0
  81. package/dist/modes/interactive/bug-report.js +205 -0
  82. package/dist/modes/interactive/chat-viewport.js +1 -1
  83. package/dist/modes/interactive/components/branch-summary-message.js +12 -5
  84. package/dist/modes/interactive/components/compaction-summary-message.js +12 -5
  85. package/dist/modes/interactive/components/custom-editor.d.ts +3 -3
  86. package/dist/modes/interactive/components/extension-editor.d.ts +4 -1
  87. package/dist/modes/interactive/components/extension-editor.js +7 -2
  88. package/dist/modes/interactive/components/extension-input.d.ts +2 -0
  89. package/dist/modes/interactive/components/extension-input.js +6 -0
  90. package/dist/modes/interactive/components/extension-selector.d.ts +1 -0
  91. package/dist/modes/interactive/components/extension-selector.js +4 -0
  92. package/dist/modes/interactive/components/footer.js +4 -1
  93. package/dist/modes/interactive/components/session-selector.d.ts +5 -5
  94. package/dist/modes/interactive/components/session-selector.js +74 -48
  95. package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
  96. package/dist/modes/interactive/components/settings-selector.js +11 -0
  97. package/dist/modes/interactive/components/skill-invocation-message.js +11 -4
  98. package/dist/modes/interactive/components/status-indicator.d.ts +2 -2
  99. package/dist/modes/interactive/components/status-indicator.js +7 -7
  100. package/dist/modes/interactive/components/tool-execution.js +18 -9
  101. package/dist/modes/interactive/components/tree-selector.js +2 -0
  102. package/dist/modes/interactive/interactive-mode.d.ts +15 -2
  103. package/dist/modes/interactive/interactive-mode.js +257 -81
  104. package/dist/modes/interactive/session-share.d.ts +2 -0
  105. package/dist/modes/interactive/session-share.js +8 -4
  106. package/dist/modes/interactive/tui-renderer.js +2 -2
  107. package/dist/modes/rpc/rpc-mode.js +2 -2
  108. package/dist/utils/clipboard-command.d.ts +6 -0
  109. package/dist/utils/clipboard-command.js +44 -0
  110. package/dist/utils/clipboard-image.js +55 -89
  111. package/dist/utils/clipboard.d.ts +1 -1
  112. package/dist/utils/clipboard.js +118 -108
  113. package/dist/utils/wsl.d.ts +2 -0
  114. package/dist/utils/wsl.js +14 -0
  115. package/dist/utils/zip.d.ts +6 -0
  116. package/dist/utils/zip.js +59 -0
  117. package/package.json +9 -9
  118. package/dist/utils/clipboard-native.d.ts +0 -10
  119. package/dist/utils/clipboard-native.js +0 -19
@@ -5,6 +5,7 @@
5
5
  import * as fs from "node:fs";
6
6
  import * as os from "node:os";
7
7
  import * as path from "node:path";
8
+ import { isRetryableAssistantError, } from "@earendil-works/pi-ai/compat";
8
9
  import * as TuiLayouts from "@earendil-works/pi-tui";
9
10
  import { CombinedAutocompleteProvider, Container, fuzzyFilter, Markdown, matchesKey, Spacer, setCapabilityOverrides, setKeybindings, Text, TruncatedText, TuiAltScreen, TuiMainScreen, visibleWidth, } from "@earendil-works/pi-tui";
10
11
  import chalk from "chalk";
@@ -13,6 +14,9 @@ import { APP_NAME, APP_TITLE, CONFIG_DIR_NAME, getAgentDir, getAuthPath, getBund
13
14
  import { parseSkillBlocks } from "../../core/agent-session.js";
14
15
  import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
15
16
  import { CACHE_TTL_MS, collectCacheMisses, computeCacheWaste, detectCacheMiss, } from "../../core/cache-stats.js";
17
+ import { formatCacheWarmingStatus, formatCacheWarmingUsage } from "../../core/cache-warmer.js";
18
+ import { recordCrash, takeUnnotifiedCrash } from "../../core/crash-log.js";
19
+ import { DEFAULT_THINKING_LEVEL, THINKING_LEVEL_OPTIONS } from "../../core/defaults.js";
16
20
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
17
21
  import { configureHttpDispatcher, formatHttpIdleTimeoutMs } from "../../core/http-dispatcher.js";
18
22
  import { KeybindingsManager } from "../../core/keybindings.js";
@@ -20,14 +24,13 @@ import { createCompactionSummaryMessage } from "../../core/messages.js";
20
24
  import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope, resolveModelScopeWithDiagnostics, } from "../../core/model-resolver.js";
21
25
  import { DefaultPackageManager } from "../../core/package-manager.js";
22
26
  import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
23
- import { SessionManager, sessionEntryToContextMessages } from "../../core/session-manager.js";
27
+ import { SessionManager, sessionEntryToContextMessages, } from "../../core/session-manager.js";
24
28
  import { buildGitCommandPrompt, GIT_COMMAND_MODES } from "../../core/git-command.js";
25
29
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
26
30
  import { isInstallTelemetryEnabled } from "../../core/telemetry.js";
27
31
  import { withBuiltInRenderers } from "../../core/tools/renderers/index.js";
28
32
  import { hasTrustRequiringProjectResources, ProjectTrustStore } from "../../core/trust-manager.js";
29
33
  import { formatCost, getUsageCostBreakdown } from "../../core/usage-totals.js";
30
- import { DEFAULT_THINKING_LEVEL } from "../../core/defaults.js";
31
34
  import { getChangelogPath, getNewEntries, normalizeChangelogLinks, parseChangelog } from "../../utils/changelog.js";
32
35
  import { copyToClipboard, readClipboardText } from "../../utils/clipboard.js";
33
36
  import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
@@ -38,6 +41,7 @@ import { getPiUserAgent } from "../../utils/pi-user-agent.js";
38
41
  import { killTrackedDetachedChildren } from "../../utils/shell.js";
39
42
  import { ensureTool } from "../../utils/tools-manager.js";
40
43
  import { checkForNewPiVersion } from "../../utils/version-check.js";
44
+ import { reportBug } from "./bug-report.js";
41
45
  import { createChatViewport } from "./chat-viewport.js";
42
46
  import { ArminComponent } from "./components/armin.js";
43
47
  import { AssistantMessageComponent } from "./components/assistant-message.js";
@@ -104,6 +108,9 @@ function isCustomSessionEntry(item) {
104
108
  function isCompactionCostNotice(item) {
105
109
  return "type" in item && item.type === "compaction_cost";
106
110
  }
111
+ function isUsageSessionEntry(item) {
112
+ return "type" in item && item.type === "usage";
113
+ }
107
114
  const DEAD_TERMINAL_ERROR_CODES = new Set(["EIO", "EPIPE", "ENOTCONN"]);
108
115
  function isDeadTerminalError(error) {
109
116
  if (!error || typeof error !== "object" || !("code" in error)) {
@@ -166,6 +173,12 @@ export function formatResumeCommand(sessionManager) {
166
173
  function hasDefaultModelProvider(providerId) {
167
174
  return providerId in defaultModelPerProvider;
168
175
  }
176
+ /** Fork-specific post-login guidance for the bundled llama.cpp runtime. */
177
+ function llamaCppPostLoginGuidance(actionLabel, loadedModelCount) {
178
+ return loadedModelCount === 0
179
+ ? `${actionLabel}. No llama.cpp models are loaded. Use /llama to load a model, then /model to select it.`
180
+ : `${actionLabel}. Use /model to select a loaded llama.cpp model, or /llama to manage models.`;
181
+ }
169
182
  const AUTH_TYPE_ORDER = { oauth: 0, api_key: 1 };
170
183
  function createFuzzyAutocompleteItems(items, prefix, getSearchText, toAutocompleteItem) {
171
184
  const filtered = fuzzyFilter(items, prefix, getSearchText);
@@ -276,6 +289,8 @@ export class InteractiveMode {
276
289
  compactionQueuedMessages = [];
277
290
  // Shutdown state
278
291
  shutdownRequested = false;
292
+ /** The `/bug` hint is shown at most once per session so error output stays readable. */
293
+ bugReportHintShown = false;
279
294
  // Extension UI state
280
295
  extensionSelector = undefined;
281
296
  extensionInput = undefined;
@@ -334,6 +349,7 @@ export class InteractiveMode {
334
349
  tuiMode,
335
350
  showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
336
351
  logDirectory: getAgentDir(),
352
+ terminal: options.terminal,
337
353
  onRightClickPaste: this.onRightClickPaste,
338
354
  fullscreenCopyOnSelect: this.settingsManager.getFullscreenCopyOnSelect(),
339
355
  });
@@ -775,6 +791,11 @@ export class InteractiveMode {
775
791
  if (modelFallbackMessage) {
776
792
  this.showWarning(modelFallbackMessage);
777
793
  }
794
+ const crash = takeUnnotifiedCrash();
795
+ if (crash) {
796
+ const when = new Date(crash.timestamp).toLocaleString();
797
+ this.showWarning(`${APP_NAME} crashed on ${when} (${crash.message}). Run /bug to report it; the crash details are attached automatically.`);
798
+ }
778
799
  void this.maybeWarnAboutAnthropicSubscriptionAuth();
779
800
  // Process initial messages
780
801
  if (initialMessage) {
@@ -1431,10 +1452,45 @@ export class InteractiveMode {
1431
1452
  async handleFatalRuntimeError(prefix, error) {
1432
1453
  const message = error instanceof Error ? error.message : String(error);
1433
1454
  this.showError(`${prefix}: ${message}`);
1455
+ if (this.recordCrash("fatal_error", error)) {
1456
+ this.chatContainer.addChild(new Text(theme.fg("muted", this.crashReportInstructions()), this.outputPad, 0));
1457
+ }
1434
1458
  stopThemeWatcher();
1435
1459
  this.stop("transcript");
1436
1460
  process.exit(1);
1437
1461
  }
1462
+ /** Persist a crash so the next start can point the user at `/bug`. Returns false when nothing was written. */
1463
+ recordCrash(kind, error) {
1464
+ try {
1465
+ return (recordCrash({
1466
+ kind,
1467
+ error,
1468
+ sessionFile: this.session.sessionFile,
1469
+ cwd: this.session.sessionManager.getCwd(),
1470
+ }) !== undefined);
1471
+ }
1472
+ catch {
1473
+ return false;
1474
+ }
1475
+ }
1476
+ crashReportInstructions() {
1477
+ const resume = this.session.sessionFile ? `run \`${APP_NAME} -r\` to resume the session, then` : "start pi and";
1478
+ return `To report this crash: ${resume} run /bug. The crash details are attached automatically.`;
1479
+ }
1480
+ suggestBugReport() {
1481
+ if (this.bugReportHintShown)
1482
+ return;
1483
+ this.bugReportHintShown = true;
1484
+ this.chatContainer.addChild(new Text(theme.fg("muted", `If this looks like a ${APP_NAME} bug, /bug sends a report to the developers.`), this.outputPad, 0));
1485
+ this.ui.requestRender();
1486
+ }
1487
+ maybeSuggestBugReport(message) {
1488
+ if (message.stopReason !== "error" || isRetryableAssistantError(message))
1489
+ return;
1490
+ if (/\b(?:abort(?:ed)?|cancel(?:l?ed)?)\b/i.test(message.errorMessage ?? ""))
1491
+ return;
1492
+ this.suggestBugReport();
1493
+ }
1438
1494
  renderCurrentSessionState() {
1439
1495
  this.loadedResourcesContainer.clear();
1440
1496
  this.chatContainer.clear();
@@ -1533,7 +1589,7 @@ export class InteractiveMode {
1533
1589
  this.activeWorkingIndicatorEmbedded = false;
1534
1590
  this.statusContainer.clear();
1535
1591
  this.setEditorWorkingStatusIndicator(undefined);
1536
- if (indicator instanceof WorkingStatusIndicator && this.setEditorWorkingStatusIndicator(indicator)) {
1592
+ if (this.setEditorWorkingStatusIndicator(indicator)) {
1537
1593
  this.activeWorkingIndicatorEmbedded = true;
1538
1594
  return;
1539
1595
  }
@@ -1544,7 +1600,7 @@ export class InteractiveMode {
1544
1600
  return;
1545
1601
  }
1546
1602
  const clearedIndicator = this.activeStatusIndicator;
1547
- const clearedIndicatorWasEmbedded = clearedIndicator?.kind === "working" && this.activeWorkingIndicatorEmbedded;
1603
+ const clearedIndicatorWasEmbedded = this.activeWorkingIndicatorEmbedded;
1548
1604
  clearedIndicator?.dispose();
1549
1605
  this.activeStatusIndicator = undefined;
1550
1606
  this.activeWorkingIndicatorEmbedded = false;
@@ -2026,7 +2082,7 @@ export class InteractiveMode {
2026
2082
  this.editor = this.defaultEditor;
2027
2083
  }
2028
2084
  this.editorContainer.addChild(this.editor);
2029
- if (this.activeStatusIndicator instanceof WorkingStatusIndicator) {
2085
+ if (this.activeStatusIndicator) {
2030
2086
  this.statusContainer.clear();
2031
2087
  this.activeWorkingIndicatorEmbedded = this.setEditorWorkingStatusIndicator(this.activeStatusIndicator);
2032
2088
  if (!this.activeWorkingIndicatorEmbedded) {
@@ -2338,6 +2394,12 @@ export class InteractiveMode {
2338
2394
  this.editor.setText("");
2339
2395
  return;
2340
2396
  }
2397
+ if (text === "/bug" || text.startsWith("/bug ")) {
2398
+ const hint = text.slice("/bug".length).trim();
2399
+ this.editor.setText("");
2400
+ await this.handleBugCommand(hint ? hint : undefined);
2401
+ return;
2402
+ }
2341
2403
  if (text === "/copy") {
2342
2404
  await this.handleCopyCommand();
2343
2405
  this.editor.setText("");
@@ -2539,6 +2601,10 @@ export class InteractiveMode {
2539
2601
  this.addCustomEntryToChat(event.entry);
2540
2602
  this.ui.requestRender();
2541
2603
  }
2604
+ else if (event.entry.type === "usage" && event.entry.kind === "cache_warm") {
2605
+ this.addCacheWarmingUsage(event.entry);
2606
+ this.ui.requestRender();
2607
+ }
2542
2608
  break;
2543
2609
  case "session_info_changed":
2544
2610
  this.updateTerminalTitle();
@@ -2619,13 +2685,14 @@ export class InteractiveMode {
2619
2685
  });
2620
2686
  }
2621
2687
  this.pendingTools.clear();
2688
+ this.maybeSuggestBugReport(this.streamingMessage);
2622
2689
  }
2623
2690
  else {
2624
2691
  // Args are now complete - trigger diff computation for edit tools
2625
2692
  for (const [, component] of this.pendingTools.entries()) {
2626
2693
  component.setArgsComplete();
2627
2694
  }
2628
- this.maybeShowAssistantDiagnostics(this.streamingMessage);
2695
+ this.maybeShowThinkingDropNotice(this.streamingMessage);
2629
2696
  this.maybeShowCacheMissNotice(this.streamingMessage);
2630
2697
  }
2631
2698
  this.streamingComponent = undefined;
@@ -2892,6 +2959,8 @@ export class InteractiveMode {
2892
2959
  this.chatContainer.addChild(component);
2893
2960
  break;
2894
2961
  }
2962
+ case "system":
2963
+ break;
2895
2964
  case "user": {
2896
2965
  const textContent = this.getUserMessageText(message);
2897
2966
  if (textContent) {
@@ -2930,6 +2999,10 @@ export class InteractiveMode {
2930
2999
  // Tool results are rendered inline with tool calls, handled separately
2931
3000
  break;
2932
3001
  }
3002
+ case "system": {
3003
+ // System messages carry prompt/tool state, not transcript content.
3004
+ break;
3005
+ }
2933
3006
  default: {
2934
3007
  const _exhaustive = message;
2935
3008
  }
@@ -2938,8 +3011,8 @@ export class InteractiveMode {
2938
3011
  renderSessionItems(items, options = {}) {
2939
3012
  this.pendingTools.clear();
2940
3013
  const renderedPendingTools = new Map();
2941
- // Cache-miss notices are not persisted; re-derive them from the full entry
2942
- // list and re-inject them after the assistant messages that paid for them.
3014
+ // Cache misses are not persisted, unlike successful cache-warming usage.
3015
+ // Re-derive them and inject them after the assistant messages that paid for them.
2943
3016
  const cacheMisses = this.settingsManager.getShowCacheMissNotices()
2944
3017
  ? collectCacheMisses(this.sessionManager.getEntries(), this.session.modelRuntime)
2945
3018
  : new Map();
@@ -2952,6 +3025,10 @@ export class InteractiveMode {
2952
3025
  this.addCustomEntryToChat(item);
2953
3026
  continue;
2954
3027
  }
3028
+ if (isUsageSessionEntry(item)) {
3029
+ this.addCacheWarmingUsage(item);
3030
+ continue;
3031
+ }
2955
3032
  if (isCompactionCostNotice(item)) {
2956
3033
  this.addCompactionCostNotice(item);
2957
3034
  continue;
@@ -2989,7 +3066,6 @@ export class InteractiveMode {
2989
3066
  }
2990
3067
  }
2991
3068
  if (message.stopReason !== "aborted" && message.stopReason !== "error") {
2992
- this.maybeShowAssistantDiagnostics(message);
2993
3069
  const miss = cacheMisses.get(message);
2994
3070
  if (miss)
2995
3071
  this.addCacheMissNotice(miss);
@@ -3021,13 +3097,19 @@ export class InteractiveMode {
3021
3097
  */
3022
3098
  renderSessionEntries(entries, options = {}) {
3023
3099
  const items = entries.flatMap((entry) => {
3024
- if (entry.type === "custom") {
3100
+ if (entry.type === "custom" || (entry.type === "usage" && entry.kind === "cache_warm")) {
3025
3101
  return [entry];
3026
3102
  }
3027
3103
  return sessionEntryToContextMessages(entry);
3028
3104
  });
3029
3105
  this.renderSessionItems(items, options);
3030
3106
  }
3107
+ addCacheWarmingUsage(entry) {
3108
+ if (!this.settingsManager.getShowCacheMissNotices())
3109
+ return;
3110
+ this.chatContainer.addChild(new Spacer(1));
3111
+ this.chatContainer.addChild(new Text(theme.fg("dim", formatCacheWarmingUsage(entry)), 1, 0));
3112
+ }
3031
3113
  /**
3032
3114
  * Render billing usage for a compaction or branch summary. The notice is derived
3033
3115
  * from persisted summary usage and is not stored as a separate session entry.
@@ -3042,31 +3124,42 @@ export class InteractiveMode {
3042
3124
  this.chatContainer.addChild(new Spacer(1));
3043
3125
  this.chatContainer.addChild(new Text(theme.fg("warning", `${label}: ${formatTokens(tokens)} tokens billed${cost}`), 1, 0));
3044
3126
  }
3045
- maybeShowAssistantDiagnostics(message) {
3046
- if (!this.settingsManager.getShowCacheMissNotices())
3047
- return;
3127
+ static countDroppedThinkingBlocks(message) {
3128
+ let count = 0;
3048
3129
  for (const diagnostic of message.diagnostics ?? []) {
3049
3130
  if (diagnostic.type !== "anthropic_input_transformations")
3050
3131
  continue;
3051
3132
  const transformations = diagnostic.details?.transformations;
3052
3133
  if (!Array.isArray(transformations))
3053
3134
  continue;
3054
- const dropped = transformations.flatMap((transformation) => {
3055
- if (typeof transformation !== "object" || transformation === null)
3056
- return [];
3057
- const details = transformation;
3058
- if (details.type !== "thinking_dropped")
3059
- return [];
3060
- const reason = typeof details.reason === "string" ? details.reason : "unknown reason";
3061
- const location = typeof details.path === "string" ? ` at ${details.path}` : "";
3062
- return [`${reason}${location}`];
3063
- });
3064
- if (dropped.length === 0)
3065
- continue;
3066
- const noun = dropped.length === 1 ? "thinking block" : `${dropped.length} thinking blocks`;
3067
- this.chatContainer.addChild(new Spacer(1));
3068
- this.chatContainer.addChild(new Text(theme.fg("warning", `Anthropic dropped ${noun}: ${dropped.join("; ")}`), 1, 0));
3135
+ count += transformations.filter((transformation) => typeof transformation === "object" &&
3136
+ transformation !== null &&
3137
+ transformation.type === "thinking_dropped").length;
3069
3138
  }
3139
+ return count;
3140
+ }
3141
+ maybeShowThinkingDropNotice(message) {
3142
+ if (!this.settingsManager.getShowCacheMissNotices())
3143
+ return;
3144
+ const droppedCount = InteractiveMode.countDroppedThinkingBlocks(message);
3145
+ if (droppedCount === 0)
3146
+ return;
3147
+ let previousDroppedCount = 0;
3148
+ // message_end reaches the UI before the current message is persisted,
3149
+ // so the branch's last assistant message is the previous response.
3150
+ const branch = this.sessionManager.getBranch();
3151
+ for (let i = branch.length - 1; i >= 0; i--) {
3152
+ const entry = branch[i];
3153
+ if (entry.type === "message" && entry.message.role === "assistant") {
3154
+ previousDroppedCount = InteractiveMode.countDroppedThinkingBlocks(entry.message);
3155
+ break;
3156
+ }
3157
+ }
3158
+ if (droppedCount <= previousDroppedCount)
3159
+ return;
3160
+ const noun = droppedCount === 1 ? "thinking block" : "thinking blocks";
3161
+ this.chatContainer.addChild(new Spacer(1));
3162
+ this.chatContainer.addChild(new Text(theme.fg("warning", `Anthropic dropped ${droppedCount} ${noun} (details in session)`), 1, 0));
3070
3163
  }
3071
3164
  /**
3072
3165
  * Show a transcript notice when a completed assistant message paid for a
@@ -3238,6 +3331,9 @@ export class InteractiveMode {
3238
3331
  catch { }
3239
3332
  console.error(`${APP_NAME} exiting due to uncaughtException:`);
3240
3333
  console.error(error);
3334
+ if (this.recordCrash("uncaught_exception", error)) {
3335
+ console.error(`\n${this.crashReportInstructions()}`);
3336
+ }
3241
3337
  process.exit(1);
3242
3338
  }
3243
3339
  /**
@@ -3371,9 +3467,7 @@ export class InteractiveMode {
3371
3467
  const level = this.session.thinkingLevel || "off";
3372
3468
  this.editor.borderColor = theme.getThinkingBorderColor(level);
3373
3469
  }
3374
- if (this.activeStatusIndicator?.kind === "working") {
3375
- this.activeStatusIndicator.invalidate();
3376
- }
3470
+ this.activeStatusIndicator?.invalidate();
3377
3471
  this.ui.requestRender();
3378
3472
  }
3379
3473
  cycleThinkingLevel() {
@@ -3558,7 +3652,7 @@ export class InteractiveMode {
3558
3652
  if (allQueued.length === 0) {
3559
3653
  this.updatePendingMessagesDisplay();
3560
3654
  if (options?.abort) {
3561
- this.agent.abort();
3655
+ void this.session.abort();
3562
3656
  }
3563
3657
  return 0;
3564
3658
  }
@@ -3568,7 +3662,7 @@ export class InteractiveMode {
3568
3662
  this.editor.setText(combinedText);
3569
3663
  this.updatePendingMessagesDisplay();
3570
3664
  if (options?.abort) {
3571
- this.agent.abort();
3665
+ void this.session.abort();
3572
3666
  }
3573
3667
  return allQueued.length;
3574
3668
  }
@@ -3718,8 +3812,9 @@ export class InteractiveMode {
3718
3812
  followUpMode: this.session.followUpMode,
3719
3813
  transport: this.settingsManager.getTransport(),
3720
3814
  httpIdleTimeoutMs: this.settingsManager.getHttpIdleTimeoutMs(),
3721
- thinkingLevel: this.session.thinkingLevel,
3722
- availableThinkingLevels: this.session.getAvailableThinkingLevels(),
3815
+ cacheWarmingMode: this.settingsManager.getCacheWarmingMode(),
3816
+ thinkingLevel: this.settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL,
3817
+ availableThinkingLevels: [...THINKING_LEVEL_OPTIONS],
3723
3818
  modelThinkingLevels: this.settingsManager.getAllModelThinkingLevels(),
3724
3819
  currentTheme: this.themeController.getThemeSelection() || "dark",
3725
3820
  terminalTheme: this.themeController.getTerminalTheme(),
@@ -3800,6 +3895,10 @@ export class InteractiveMode {
3800
3895
  configureHttpDispatcher(timeoutMs);
3801
3896
  this.showStatus(`HTTP idle timeout: ${formatHttpIdleTimeoutMs(timeoutMs)}`);
3802
3897
  },
3898
+ onCacheWarmingModeChange: (mode) => {
3899
+ this.session.setCacheWarmingMode(mode);
3900
+ this.showStatus(`Cache warming: ${mode}`);
3901
+ },
3803
3902
  onModelThinkingLevelChange: (provider, modelId, level) => {
3804
3903
  this.settingsManager.setModelThinkingLevel(provider, modelId, level);
3805
3904
  const current = this.session.model;
@@ -4332,6 +4431,11 @@ export class InteractiveMode {
4332
4431
  this.restoreQueuedMessagesToEditor();
4333
4432
  await this.session.abort();
4334
4433
  }
4434
+ // Recheck after the dialogs and streaming abort, before replacing another operation's UI.
4435
+ if (this.session.isCompacting) {
4436
+ this.showError("Wait for the current compaction or tree navigation to finish before navigating the session tree.");
4437
+ return;
4438
+ }
4335
4439
  // Set up escape handler and status indicator if summarizing
4336
4440
  let showingSummaryIndicator = false;
4337
4441
  const originalOnEscape = this.defaultEditor.onEscape;
@@ -4402,9 +4506,9 @@ export class InteractiveMode {
4402
4506
  }
4403
4507
  showSessionSelector() {
4404
4508
  this.showSelector((done) => {
4405
- const selector = new SessionSelectorComponent((onProgress) => SessionManager.list(this.sessionManager.getCwd(), this.sessionManager.getSessionDir(), onProgress), (onProgress) => this.sessionManager.usesDefaultSessionDir()
4406
- ? SessionManager.listAll(onProgress)
4407
- : SessionManager.listAll(this.sessionManager.getSessionDir(), onProgress), async (sessionPath) => {
4509
+ const selector = new SessionSelectorComponent((onProgress, signal) => SessionManager.list(this.sessionManager.getCwd(), this.sessionManager.getSessionDir(), onProgress, signal), (onProgress, signal) => this.sessionManager.usesDefaultSessionDir()
4510
+ ? SessionManager.listAll(onProgress, signal)
4511
+ : SessionManager.listAll(this.sessionManager.getSessionDir(), onProgress, signal), async (sessionPath) => {
4408
4512
  done();
4409
4513
  await this.handleResumeSession(sessionPath);
4410
4514
  }, () => {
@@ -4655,54 +4759,99 @@ export class InteractiveMode {
4655
4759
  });
4656
4760
  }
4657
4761
  async completeProviderAuthentication(providerId, providerName, authType, previousModel) {
4658
- await this.session.modelRuntime.getAvailable();
4762
+ const session = this.session;
4659
4763
  const actionLabel = authType === "oauth" ? `Logged in to ${providerName}` : `Saved API key for ${providerName}`;
4660
- let selectedModel;
4661
- let selectionError;
4662
- if (isUnknownModel(previousModel)) {
4663
- const availableModels = await this.session.modelRuntime.getAvailable();
4664
- const providerModels = availableModels.filter((model) => model.provider === providerId);
4665
- if (!hasDefaultModelProvider(providerId)) {
4666
- selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
4764
+ // Dynamic catalogs may be empty until the first authenticated network refresh.
4765
+ const deferSelection = isUnknownModel(previousModel) &&
4766
+ hasDefaultModelProvider(providerId) &&
4767
+ !session.modelRuntime
4768
+ .getAvailableSnapshot()
4769
+ .some((model) => model.provider === providerId && model.id === defaultModelPerProvider[providerId]);
4770
+ const finishAuthentication = async () => {
4771
+ let selectedModel;
4772
+ let selectionError;
4773
+ if (isUnknownModel(previousModel)) {
4774
+ const availableModels = await this.session.modelRuntime.getAvailable();
4775
+ const providerModels = availableModels.filter((model) => model.provider === providerId);
4776
+ // Matches LLAMA_PROVIDER_ID from core/llama/provider.ts; kept inline to avoid
4777
+ // coupling interactive mode to the built-in extension.
4778
+ if (providerId === "llama.cpp") {
4779
+ selectionError = llamaCppPostLoginGuidance(actionLabel, providerModels.length);
4780
+ }
4781
+ else if (!hasDefaultModelProvider(providerId)) {
4782
+ selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
4783
+ }
4784
+ else if (providerModels.length === 0) {
4785
+ selectionError = `${actionLabel}, but no models are available for that provider. Use /model to select a model.`;
4786
+ }
4787
+ else {
4788
+ const defaultModelId = defaultModelPerProvider[providerId];
4789
+ // Radius catalogs vary by account; prefer balanced, then use catalog order.
4790
+ selectedModel =
4791
+ providerModels.find((model) => model.id === defaultModelId) ??
4792
+ (providerId === "radius" ? providerModels[0] : undefined);
4793
+ if (!selectedModel) {
4794
+ selectionError = `${actionLabel}, but its default model "${defaultModelId}" is not available. Use /model to select a model.`;
4795
+ }
4796
+ else {
4797
+ try {
4798
+ await this.session.setModel(selectedModel, { persist: true });
4799
+ }
4800
+ catch (error) {
4801
+ selectedModel = undefined;
4802
+ const errorMessage = error instanceof Error ? error.message : String(error);
4803
+ selectionError = `${actionLabel}, but selecting its default model failed: ${errorMessage}. Use /model to select a model.`;
4804
+ }
4805
+ }
4806
+ }
4667
4807
  }
4668
- else if (providerModels.length === 0) {
4669
- selectionError = `${actionLabel}, but no models are available for that provider. Use /model to select a model.`;
4808
+ await this.updateAvailableProviderCount();
4809
+ this.footer.invalidate();
4810
+ this.updateEditorBorderColor();
4811
+ if (selectedModel) {
4812
+ this.showStatus(`${actionLabel}. Selected ${selectedModel.id}. Credentials saved to ${getAuthPath()}`);
4813
+ void this.maybeWarnAboutAnthropicSubscriptionAuth(selectedModel);
4814
+ this.checkDaxnutsEasterEgg(selectedModel);
4670
4815
  }
4671
4816
  else {
4672
- const defaultModelId = defaultModelPerProvider[providerId];
4673
- selectedModel = providerModels.find((model) => model.id === defaultModelId);
4674
- if (!selectedModel) {
4675
- selectionError = `${actionLabel}, but its default model "${defaultModelId}" is not available. Use /model to select a model.`;
4817
+ this.showStatus(`${actionLabel}. Credentials saved to ${getAuthPath()}`);
4818
+ if (selectionError) {
4819
+ this.showError(selectionError);
4676
4820
  }
4677
4821
  else {
4678
- try {
4679
- await this.session.setModel(selectedModel);
4680
- }
4681
- catch (error) {
4682
- selectedModel = undefined;
4683
- const errorMessage = error instanceof Error ? error.message : String(error);
4684
- selectionError = `${actionLabel}, but selecting its default model failed: ${errorMessage}. Use /model to select a model.`;
4685
- }
4822
+ void this.maybeWarnAboutAnthropicSubscriptionAuth();
4686
4823
  }
4687
4824
  }
4688
- }
4689
- await this.updateAvailableProviderCount();
4690
- this.footer.invalidate();
4691
- this.updateEditorBorderColor();
4692
- if (selectedModel) {
4693
- this.showStatus(`${actionLabel}. Selected ${selectedModel.id}. Credentials saved to ${getAuthPath()}`);
4694
- void this.maybeWarnAboutAnthropicSubscriptionAuth(selectedModel);
4695
- this.checkDaxnutsEasterEgg(selectedModel);
4825
+ };
4826
+ if (deferSelection) {
4827
+ this.showStatus(`${actionLabel}. Credentials saved to ${getAuthPath()}. Refreshing model catalog…`);
4696
4828
  }
4697
4829
  else {
4698
- this.showStatus(`${actionLabel}. Credentials saved to ${getAuthPath()}`);
4699
- if (selectionError) {
4700
- this.showError(selectionError);
4830
+ await finishAuthentication();
4831
+ }
4832
+ const controller = new AbortController();
4833
+ const timeout = setTimeout(() => controller.abort(), 15_000);
4834
+ void session.modelRuntime
4835
+ .refresh({ providers: [providerId], signal: controller.signal })
4836
+ .then(async (result) => {
4837
+ if (result.aborted) {
4838
+ this.showWarning(`${actionLabel}, but its model catalog refresh timed out; using cached models.`);
4701
4839
  }
4702
- else {
4703
- void this.maybeWarnAboutAnthropicSubscriptionAuth();
4840
+ else if (result.errors.size > 0) {
4841
+ this.showWarning(`${actionLabel}, but its model catalog could not be refreshed; using cached models.`);
4704
4842
  }
4705
- }
4843
+ // Do not replace a model or session selected while the refresh was running.
4844
+ if (deferSelection && this.session === session && session.model === previousModel) {
4845
+ await finishAuthentication();
4846
+ }
4847
+ this.updateAvailableProviderCount();
4848
+ this.footer.invalidate();
4849
+ this.ui.requestRender();
4850
+ })
4851
+ .catch((error) => {
4852
+ this.showWarning(`${actionLabel}, but its model catalog could not be refreshed: ${error instanceof Error ? error.message : String(error)}`);
4853
+ })
4854
+ .finally(() => clearTimeout(timeout));
4706
4855
  }
4707
4856
  showAmbientAuthDialog(providerOption) {
4708
4857
  const restoreEditor = () => {
@@ -5150,6 +5299,17 @@ export class InteractiveMode {
5150
5299
  }
5151
5300
  }
5152
5301
  }
5302
+ async handleBugCommand(hint) {
5303
+ await reportBug({
5304
+ session: this.session,
5305
+ ui: this.ui,
5306
+ editorContainer: this.editorContainer,
5307
+ editor: this.editor,
5308
+ keybindings: this.keybindings,
5309
+ showStatus: (message) => this.showStatus(message),
5310
+ showError: (message) => this.showError(message),
5311
+ }, hint);
5312
+ }
5153
5313
  async handleCopyCommand(options = {}) {
5154
5314
  if (options.preferSelection &&
5155
5315
  this.ui instanceof TuiAltScreen &&
@@ -5235,6 +5395,15 @@ export class InteractiveMode {
5235
5395
  }
5236
5396
  info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`;
5237
5397
  info += `${theme.fg("dim", "Total:")} ${stats.tokens.total.toLocaleString()}\n`;
5398
+ const cacheWarmingStatus = this.session.cacheWarmingStatus;
5399
+ info += `\n${theme.bold("Cache Warming")}\n`;
5400
+ info += `${theme.fg("dim", "Mode:")} ${this.settingsManager.getCacheWarmingMode()}\n`;
5401
+ info += `${theme.fg("dim", "Status:")} ${cacheWarmingStatus ? formatCacheWarmingStatus(cacheWarmingStatus) : "Inactive (cache warming unavailable)"}\n`;
5402
+ const decision = cacheWarmingStatus?.decision;
5403
+ if (decision?.economicsAvailable) {
5404
+ info += `${theme.fg("dim", "Cache miss penalty:")} $${decision.missCost.toFixed(3)}\n`;
5405
+ info += `${theme.fg("dim", "Refresh cost:")} $${decision.warmCost.toFixed(3)}\n`;
5406
+ }
5238
5407
  if (stats.cost > 0 || cacheWaste.missedTokens > 0) {
5239
5408
  info += `\n${theme.bold("Cost")}\n`;
5240
5409
  info += `${theme.fg("dim", "Total:")} ${formatCost(stats.cost)}`;
@@ -5365,7 +5534,7 @@ export class InteractiveMode {
5365
5534
  | \`${expandTools}\` | Toggle tool output expansion |
5366
5535
  | \`${toggleThinking}\` | Toggle thinking block visibility |
5367
5536
  | \`${externalEditor}\` | Edit message in external editor |
5368
- | \`${copyMessage}\` | Copy last assistant message |
5537
+ | \`${copyMessage}\` | Copy selection or last assistant message |
5369
5538
  | \`${followUp}\` | Queue follow-up message |
5370
5539
  | \`${dequeue}\` | Restore queued messages |
5371
5540
  | \`${pasteImage}\` | Paste image or text from clipboard |
@@ -5461,12 +5630,19 @@ export class InteractiveMode {
5461
5630
  async handleBashCommand(command, excludeFromContext = false) {
5462
5631
  const extensionRunner = this.session.extensionRunner;
5463
5632
  // Emit user_bash event to let extensions intercept
5464
- const eventResult = await extensionRunner.emitUserBash({
5465
- type: "user_bash",
5466
- command,
5467
- excludeFromContext,
5468
- cwd: this.sessionManager.getCwd(),
5469
- });
5633
+ let eventResult;
5634
+ try {
5635
+ eventResult = await extensionRunner.emitUserBash({
5636
+ type: "user_bash",
5637
+ command,
5638
+ excludeFromContext,
5639
+ cwd: this.sessionManager.getCwd(),
5640
+ });
5641
+ }
5642
+ catch {
5643
+ // The extension runner already reported the error. Do not fall back to local execution.
5644
+ return;
5645
+ }
5470
5646
  // If extension returned a full result, use it directly
5471
5647
  if (eventResult?.result) {
5472
5648
  const result = eventResult.result;
@@ -8,6 +8,8 @@ interface SessionShareContext {
8
8
  showStatus: (message: string) => void;
9
9
  showError: (message: string) => void;
10
10
  }
11
+ /** Trailing `pi.share` entry carrying the system prompt and tool schemas for the session viewer. */
12
+ export declare function createShareTrailingEntries(session: AgentSession, parentId: string | null, timestamp: string): object[];
11
13
  /** Export the current branch with presentation metadata for Radius. */
12
14
  export declare function exportSessionForShare(filePath: string, session: AgentSession): void;
13
15
  /** Share the current session through Radius, falling back to a private gist. */
@@ -10,9 +10,9 @@ import { getShareViewerUrl } from "../../config.js";
10
10
  import { exportSessionToJsonl } from "../../core/session-export.js";
11
11
  import { BorderedLoader } from "./components/bordered-loader.js";
12
12
  import { theme } from "./theme/theme.js";
13
- /** Export the current branch with presentation metadata for Radius. */
14
- export function exportSessionForShare(filePath, session) {
15
- exportSessionToJsonl(session.sessionManager, filePath, (parentId, timestamp) => [
13
+ /** Trailing `pi.share` entry carrying the system prompt and tool schemas for the session viewer. */
14
+ export function createShareTrailingEntries(session, parentId, timestamp) {
15
+ return [
16
16
  {
17
17
  type: "custom",
18
18
  customType: "pi.share",
@@ -28,7 +28,11 @@ export function exportSessionForShare(filePath, session) {
28
28
  })),
29
29
  },
30
30
  },
31
- ]);
31
+ ];
32
+ }
33
+ /** Export the current branch with presentation metadata for Radius. */
34
+ export function exportSessionForShare(filePath, session) {
35
+ exportSessionToJsonl(session.sessionManager, filePath, (parentId, timestamp) => createShareTrailingEntries(session, parentId, timestamp));
32
36
  }
33
37
  /** Share the current session through Radius, falling back to a private gist. */
34
38
  export async function shareSession(context) {