@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
@@ -78,7 +78,10 @@ export class FooterComponent {
78
78
  const usageTotals = createUsageTotals();
79
79
  let latestCacheHitRate;
80
80
  for (const entry of this.session.sessionManager.getEntries()) {
81
- if (entry.type === "message" && entry.message.role === "assistant") {
81
+ if (entry.type === "usage") {
82
+ addUsageToTotals(usageTotals, entry.usage);
83
+ }
84
+ else if (entry.type === "message" && entry.message.role === "assistant") {
82
85
  addUsageToTotals(usageTotals, entry.message.usage);
83
86
  const latestPromptTokens = entry.message.usage.input + entry.message.usage.cacheRead + entry.message.usage.cacheWrite;
84
87
  latestCacheHitRate =
@@ -10,6 +10,7 @@ declare class SessionList implements Component, Focusable {
10
10
  private allSessions;
11
11
  private filteredSessions;
12
12
  private selectedIndex;
13
+ private selectionTouched;
13
14
  private searchInput;
14
15
  private showCwd;
15
16
  private sortMode;
@@ -46,7 +47,7 @@ declare class SessionList implements Component, Focusable {
46
47
  private buildTreePrefix;
47
48
  handleInput(keyData: string): void;
48
49
  }
49
- type SessionsLoader = (onProgress?: SessionListProgress) => Promise<SessionInfo[]>;
50
+ type SessionsLoader = (onProgress?: SessionListProgress, signal?: AbortSignal) => Promise<SessionInfo[]>;
50
51
  /**
51
52
  * Component that renders a session selector
52
53
  */
@@ -65,9 +66,8 @@ export declare class SessionSelectorComponent extends Container implements Focus
65
66
  private allSessionsLoader;
66
67
  private requestRender;
67
68
  private renameSession?;
68
- private currentLoading;
69
- private allLoading;
70
- private allLoadSeq;
69
+ private currentLoad;
70
+ private allLoad;
71
71
  private mode;
72
72
  private renameInput;
73
73
  private renameTargetPath;
@@ -80,7 +80,7 @@ export declare class SessionSelectorComponent extends Container implements Focus
80
80
  showRenameHint?: boolean;
81
81
  keybindings?: KeybindingsManager;
82
82
  }, currentSessionFilePath?: string);
83
- private loadCurrentSessions;
83
+ private cancelLoads;
84
84
  private enterRenameMode;
85
85
  private exitRenameMode;
86
86
  private confirmRename;
@@ -233,6 +233,7 @@ class SessionList {
233
233
  allSessions = [];
234
234
  filteredSessions = [];
235
235
  selectedIndex = 0;
236
+ selectionTouched = false;
236
237
  searchInput;
237
238
  showCwd = false;
238
239
  sortMode = "threaded";
@@ -291,9 +292,18 @@ class SessionList {
291
292
  this.filterSessions(this.searchInput.getValue());
292
293
  }
293
294
  setSessions(sessions, showCwd) {
295
+ const selectedPath = this.selectionTouched ? this.getSelectedSessionPath() : undefined;
294
296
  this.allSessions = sessions;
295
297
  this.showCwd = showCwd;
296
298
  this.filterSessions(this.searchInput.getValue());
299
+ if (!this.selectionTouched) {
300
+ this.selectedIndex = 0;
301
+ }
302
+ else if (selectedPath) {
303
+ const selectedIndex = this.filteredSessions.findIndex((node) => node.session.path === selectedPath);
304
+ if (selectedIndex >= 0)
305
+ this.selectedIndex = selectedIndex;
306
+ }
297
307
  }
298
308
  filterSessions(query) {
299
309
  const trimmed = query.trim();
@@ -499,6 +509,7 @@ class SessionList {
499
509
  this.startDeleteConfirmationForSelectedSession();
500
510
  return;
501
511
  }
512
+ this.selectionTouched = true;
502
513
  // Up arrow
503
514
  if (kb.matches(keyData, "tui.select.up")) {
504
515
  this.selectedIndex = Math.max(0, this.selectedIndex - 1);
@@ -600,9 +611,8 @@ export class SessionSelectorComponent extends Container {
600
611
  allSessionsLoader;
601
612
  requestRender;
602
613
  renameSession;
603
- currentLoading = false;
604
- allLoading = false;
605
- allLoadSeq = 0;
614
+ currentLoad = null;
615
+ allLoad = null;
606
616
  mode = "list";
607
617
  renameInput = new Input();
608
618
  renameTargetPath = null;
@@ -653,14 +663,17 @@ export class SessionSelectorComponent extends Container {
653
663
  const clearStatusMessage = () => this.header.setStatusMessage(null);
654
664
  this.sessionList.onSelect = (sessionPath) => {
655
665
  clearStatusMessage();
666
+ this.cancelLoads();
656
667
  onSelect(sessionPath);
657
668
  };
658
669
  this.sessionList.onCancel = () => {
659
670
  clearStatusMessage();
671
+ this.cancelLoads();
660
672
  onCancel();
661
673
  };
662
674
  this.sessionList.onExit = () => {
663
675
  clearStatusMessage();
676
+ this.cancelLoads();
664
677
  onExit();
665
678
  };
666
679
  this.sessionList.onToggleScope = () => this.toggleScope();
@@ -669,9 +682,7 @@ export class SessionSelectorComponent extends Container {
669
682
  this.sessionList.onRenameSession = (sessionPath) => {
670
683
  if (!renameSession)
671
684
  return;
672
- if (this.scope === "current" && this.currentLoading)
673
- return;
674
- if (this.scope === "all" && this.allLoading)
685
+ if (this.scope === "current" ? this.currentLoad : this.allLoad)
675
686
  return;
676
687
  const sessions = this.scope === "all" ? (this.allSessions ?? []) : (this.currentSessions ?? []);
677
688
  const session = sessions.find((s) => s.path === sessionPath);
@@ -714,10 +725,19 @@ export class SessionSelectorComponent extends Container {
714
725
  this.requestRender();
715
726
  };
716
727
  // Start loading current sessions immediately
717
- this.loadCurrentSessions();
728
+ void this.loadScope("current");
718
729
  }
719
- loadCurrentSessions() {
720
- void this.loadScope("current", "initial");
730
+ cancelLoads() {
731
+ if (this.currentLoad) {
732
+ this.currentLoad.abort();
733
+ this.currentLoad = null;
734
+ this.currentSessions = null;
735
+ }
736
+ if (this.allLoad) {
737
+ this.allLoad.abort();
738
+ this.allLoad = null;
739
+ this.allSessions = null;
740
+ }
721
741
  }
722
742
  enterRenameMode(sessionPath, currentName) {
723
743
  this.mode = "rename";
@@ -762,64 +782,77 @@ export class SessionSelectorComponent extends Container {
762
782
  this.exitRenameMode();
763
783
  }
764
784
  }
765
- async loadScope(scope, reason) {
785
+ async loadScope(scope) {
786
+ if (scope === "current" ? this.currentLoad : this.allLoad)
787
+ return;
766
788
  const showCwd = scope === "all";
767
- // Mark loading
789
+ const controller = new AbortController();
768
790
  if (scope === "current") {
769
- this.currentLoading = true;
791
+ this.currentLoad = controller;
770
792
  }
771
793
  else {
772
- this.allLoading = true;
794
+ this.allLoad = controller;
773
795
  }
774
- const seq = scope === "all" ? ++this.allLoadSeq : undefined;
775
796
  this.header.setScope(scope);
776
797
  this.header.setLoading(true);
777
798
  this.requestRender();
778
- const onProgress = (loaded, total) => {
779
- if (scope !== this.scope)
799
+ const isActive = () => (scope === "current" ? this.currentLoad : this.allLoad) === controller;
800
+ const onProgress = (loaded, total, partialSessions) => {
801
+ if (!isActive())
780
802
  return;
781
- if (seq !== undefined && seq !== this.allLoadSeq)
803
+ if (partialSessions) {
804
+ const sessions = [...partialSessions];
805
+ if (scope === "current") {
806
+ this.currentSessions = sessions;
807
+ }
808
+ else {
809
+ this.allSessions = sessions;
810
+ }
811
+ if (scope === this.scope)
812
+ this.sessionList.setSessions(sessions, showCwd);
813
+ }
814
+ if (scope !== this.scope)
782
815
  return;
783
816
  this.header.setProgress(loaded, total);
784
817
  this.requestRender();
785
818
  };
786
819
  try {
787
820
  const sessions = await (scope === "current"
788
- ? this.currentSessionsLoader(onProgress)
789
- : this.allSessionsLoader(onProgress));
821
+ ? this.currentSessionsLoader(onProgress, controller.signal)
822
+ : this.allSessionsLoader(onProgress, controller.signal));
823
+ if (!isActive())
824
+ return;
790
825
  if (scope === "current") {
791
826
  this.currentSessions = sessions;
792
- this.currentLoading = false;
827
+ this.currentLoad = null;
793
828
  }
794
829
  else {
795
830
  this.allSessions = sessions;
796
- this.allLoading = false;
831
+ this.allLoad = null;
797
832
  }
798
833
  if (scope !== this.scope)
799
834
  return;
800
- if (seq !== undefined && seq !== this.allLoadSeq)
801
- return;
802
835
  this.header.setLoading(false);
803
836
  this.sessionList.setSessions(sessions, showCwd);
804
837
  this.requestRender();
805
838
  }
806
839
  catch (err) {
840
+ if (!isActive())
841
+ return;
807
842
  if (scope === "current") {
808
- this.currentLoading = false;
843
+ this.currentLoad = null;
844
+ this.currentSessions = null;
809
845
  }
810
846
  else {
811
- this.allLoading = false;
847
+ this.allLoad = null;
848
+ this.allSessions = null;
812
849
  }
813
850
  if (scope !== this.scope)
814
851
  return;
815
- if (seq !== undefined && seq !== this.allLoadSeq)
816
- return;
817
852
  const message = err instanceof Error ? err.message : String(err);
818
853
  this.header.setLoading(false);
819
854
  this.header.setStatusMessage({ type: "error", message: `Failed to load sessions: ${message}` }, 4000);
820
- if (reason === "initial") {
821
- this.sessionList.setSessions([], showCwd);
822
- }
855
+ this.sessionList.setSessions([], showCwd);
823
856
  this.requestRender();
824
857
  }
825
858
  }
@@ -837,28 +870,21 @@ export class SessionSelectorComponent extends Container {
837
870
  this.requestRender();
838
871
  }
839
872
  async refreshSessionsAfterMutation() {
840
- await this.loadScope(this.scope, "refresh");
873
+ this.cancelLoads();
874
+ this.currentSessions = null;
875
+ this.allSessions = null;
876
+ await this.loadScope(this.scope);
841
877
  }
842
878
  toggleScope() {
843
- if (this.scope === "current") {
844
- this.scope = "all";
845
- this.header.setScope(this.scope);
846
- if (this.allSessions !== null) {
847
- this.header.setLoading(false);
848
- this.sessionList.setSessions(this.allSessions, true);
849
- this.requestRender();
850
- return;
851
- }
852
- if (!this.allLoading) {
853
- void this.loadScope("all", "toggle");
854
- }
855
- return;
856
- }
857
- this.scope = "current";
879
+ this.scope = this.scope === "current" ? "all" : "current";
880
+ const sessions = this.scope === "current" ? this.currentSessions : this.allSessions;
881
+ const loading = (this.scope === "current" ? this.currentLoad : this.allLoad) !== null;
858
882
  this.header.setScope(this.scope);
859
- this.header.setLoading(this.currentLoading);
860
- this.sessionList.setSessions(this.currentSessions ?? [], false);
883
+ this.header.setLoading(loading);
884
+ this.sessionList.setSessions(sessions ?? [], this.scope === "all");
861
885
  this.requestRender();
886
+ if (sessions === null && !loading)
887
+ void this.loadScope(this.scope);
862
888
  }
863
889
  getSessionList() {
864
890
  return this.sessionList;
@@ -1,7 +1,7 @@
1
1
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
2
  import { type Model, type Transport } from "@earendil-works/pi-ai";
3
3
  import { Container, type ScrollViewScrollbar, SettingsList } from "@earendil-works/pi-tui";
4
- import type { DefaultProjectTrust, FullscreenExitOutput, TuiMode, WarningSettings } from "../../../core/settings-manager.ts";
4
+ import { type CacheWarmingMode, type DefaultProjectTrust, type FullscreenExitOutput, type TuiMode, type WarningSettings } from "../../../core/settings-manager.ts";
5
5
  import { type TerminalTheme } from "../theme/theme.ts";
6
6
  import type { ModelSelectorComponent } from "./model-selector.ts";
7
7
  export interface SkillToggleItem {
@@ -27,6 +27,7 @@ export interface SettingsConfig {
27
27
  followUpMode: "all" | "one-at-a-time";
28
28
  transport: Transport;
29
29
  httpIdleTimeoutMs: number;
30
+ cacheWarmingMode: CacheWarmingMode;
30
31
  thinkingLevel: ThinkingLevel;
31
32
  availableThinkingLevels: ThinkingLevel[];
32
33
  defaultModel: string;
@@ -71,6 +72,7 @@ export interface SettingsCallbacks {
71
72
  onFollowUpModeChange: (mode: "all" | "one-at-a-time") => void;
72
73
  onTransportChange: (transport: Transport) => void;
73
74
  onHttpIdleTimeoutMsChange: (timeoutMs: number) => void;
75
+ onCacheWarmingModeChange: (mode: CacheWarmingMode) => void;
74
76
  onModelThinkingLevelChange: (provider: string, modelId: string, level: ThinkingLevel) => void;
75
77
  onModelThinkingLevelRemove: (provider: string, modelId: string) => void;
76
78
  onThinkingLevelChange: (level: ThinkingLevel) => void;
@@ -1,6 +1,7 @@
1
1
  import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
2
2
  import { Container, getCapabilities, SelectList, SettingsList, Spacer, Text, } from "@earendil-works/pi-tui";
3
3
  import { formatHttpIdleTimeoutMs, HTTP_IDLE_TIMEOUT_CHOICES } from "../../../core/http-dispatcher.js";
4
+ import { CACHE_WARMING_MODES, } from "../../../core/settings-manager.js";
4
5
  import { getSelectListTheme, getSettingsListTheme, parseAutoThemeSetting, theme, } from "../theme/theme.js";
5
6
  import { DynamicBorder } from "./dynamic-border.js";
6
7
  import { keyDisplayText } from "./keybinding-hints.js";
@@ -390,6 +391,13 @@ export class SettingsSelectorComponent extends Container {
390
391
  currentValue: formatHttpIdleTimeoutMs(config.httpIdleTimeoutMs),
391
392
  values: HTTP_IDLE_TIMEOUT_CHOICES.map((choice) => choice.label),
392
393
  },
394
+ {
395
+ id: "cache-warming-mode",
396
+ label: "Cache warming",
397
+ description: "off; streaming while the agent runs; idle also between runs while continuation stays profitable",
398
+ currentValue: config.cacheWarmingMode,
399
+ values: [...CACHE_WARMING_MODES],
400
+ },
393
401
  {
394
402
  id: "hide-thinking",
395
403
  label: "Hide thinking",
@@ -757,6 +765,9 @@ export class SettingsSelectorComponent extends Container {
757
765
  }
758
766
  break;
759
767
  }
768
+ case "cache-warming-mode":
769
+ callbacks.onCacheWarmingModeChange(newValue);
770
+ break;
760
771
  case "hide-thinking":
761
772
  callbacks.onHideThinkingBlockChange(newValue === "true");
762
773
  break;
@@ -1,4 +1,4 @@
1
- import { Box, Markdown, Text } from "@earendil-works/pi-tui";
1
+ import { Box, Container, Markdown, MouseRegion, Text } from "@earendil-works/pi-tui";
2
2
  import { getMarkdownTheme, theme } from "../theme/theme.js";
3
3
  import { keyText } from "./keybinding-hints.js";
4
4
  /**
@@ -26,12 +26,13 @@ export class SkillInvocationMessageComponent extends Box {
26
26
  }
27
27
  updateDisplay() {
28
28
  this.clear();
29
+ const content = new Container();
29
30
  if (this.expanded) {
30
31
  // Expanded: label + skill name header + full content
31
32
  const label = theme.fg("customMessageLabel", `\x1b[1m[skill]\x1b[22m`);
32
- this.addChild(new Text(label, 0, 0));
33
+ content.addChild(new Text(label, 0, 0));
33
34
  const header = `**${this.skillBlock.name}**\n\n`;
34
- this.addChild(new Markdown(header + this.skillBlock.content, 0, 0, this.markdownTheme, {
35
+ content.addChild(new Markdown(header + this.skillBlock.content, 0, 0, this.markdownTheme, {
35
36
  color: (text) => theme.fg("customMessageText", text),
36
37
  }));
37
38
  }
@@ -40,7 +41,13 @@ export class SkillInvocationMessageComponent extends Box {
40
41
  const line = theme.fg("customMessageLabel", `\x1b[1m[skill]\x1b[22m `) +
41
42
  theme.fg("customMessageText", this.skillBlock.name) +
42
43
  theme.fg("dim", ` (${keyText("app.tools.expand")} to expand)`);
43
- this.addChild(new Text(line, 0, 0));
44
+ content.addChild(new Text(line, 0, 0));
44
45
  }
46
+ this.addChild(new MouseRegion(content, (event) => {
47
+ if (event.type !== "click" || event.button !== "left")
48
+ return undefined;
49
+ this.setExpanded(!this.expanded);
50
+ return { handled: true };
51
+ }));
45
52
  }
46
53
  }
@@ -4,12 +4,12 @@ export type StatusIndicatorKind = "working" | "retry" | "compaction" | "branchSu
4
4
  export declare class StatusIndicator extends Loader {
5
5
  readonly kind: StatusIndicatorKind;
6
6
  constructor(kind: StatusIndicatorKind, ui: TUI, spinnerColorFn: (str: string) => string, messageColorFn: (str: string) => string, message: string, indicator?: WorkingIndicatorOptions);
7
+ renderInBorder(width: number): string;
8
+ renderSpinnerInBorder(width: number): string;
7
9
  dispose(): void;
8
10
  }
9
11
  export declare class WorkingStatusIndicator extends StatusIndicator {
10
12
  constructor(ui: TUI, message: string, indicator?: WorkingIndicatorOptions, colorFn?: (text: string) => string);
11
- renderInBorder(width: number): string;
12
- renderSpinnerInBorder(width: number): string;
13
13
  }
14
14
  export declare class ImageCaptioningStatusIndicator extends StatusIndicator {
15
15
  constructor(ui: TUI);
@@ -8,6 +8,13 @@ export class StatusIndicator extends Loader {
8
8
  super(ui, spinnerColorFn, messageColorFn, message, indicator);
9
9
  this.kind = kind;
10
10
  }
11
+ renderInBorder(width) {
12
+ const line = super.render(width + 2)[1] ?? "";
13
+ return truncateToWidth(line.startsWith(" ") ? line.slice(1).trimEnd() : line.trimEnd(), width, "");
14
+ }
15
+ renderSpinnerInBorder(width) {
16
+ return truncateToWidth(this.getRenderedIndicator(), width, "");
17
+ }
11
18
  dispose() {
12
19
  this.stop();
13
20
  }
@@ -16,13 +23,6 @@ export class WorkingStatusIndicator extends StatusIndicator {
16
23
  constructor(ui, message, indicator, colorFn) {
17
24
  super("working", ui, colorFn ?? ((text) => theme.fg("accent", text)), colorFn ?? ((text) => theme.fg("muted", text)), message, indicator);
18
25
  }
19
- renderInBorder(width) {
20
- const line = super.render(width + 2)[1] ?? "";
21
- return truncateToWidth(line.startsWith(" ") ? line.slice(1).trimEnd() : line.trimEnd(), width, "");
22
- }
23
- renderSpinnerInBorder(width) {
24
- return truncateToWidth(this.getRenderedIndicator(), width, "");
25
- }
26
26
  }
27
27
  export class ImageCaptioningStatusIndicator extends StatusIndicator {
28
28
  constructor(ui) {
@@ -143,17 +143,25 @@ export class ToolExecutionComponent extends Container {
143
143
  const img = imageBlocks[i];
144
144
  if (!img.data || !img.mimeType)
145
145
  continue;
146
- if (img.mimeType === "image/png")
146
+ const sourceData = img.data;
147
+ const sourceMimeType = img.mimeType;
148
+ if (sourceMimeType === "image/png")
147
149
  continue;
148
- if (this.convertedImages.has(i))
150
+ const cached = this.convertedImages.get(i);
151
+ if (cached?.sourceData === sourceData && cached.sourceMimeType === sourceMimeType)
149
152
  continue;
150
153
  const index = i;
151
- convertToPng(img.data, img.mimeType).then((converted) => {
152
- if (converted) {
153
- this.convertedImages.set(index, converted);
154
- this.updateDisplay();
155
- this.ui.requestRender();
156
- }
154
+ convertToPng(sourceData, sourceMimeType).then((converted) => {
155
+ const currentImage = this.result?.content.filter((content) => content.type === "image")[index];
156
+ if (!converted || currentImage?.data !== sourceData || currentImage.mimeType !== sourceMimeType)
157
+ return;
158
+ this.convertedImages.set(index, {
159
+ sourceData,
160
+ sourceMimeType,
161
+ ...converted,
162
+ });
163
+ this.updateDisplay();
164
+ this.ui.requestRender();
157
165
  });
158
166
  }
159
167
  }
@@ -291,7 +299,8 @@ export class ToolExecutionComponent extends Container {
291
299
  for (let i = 0; i < imageBlocks.length; i++) {
292
300
  const img = imageBlocks[i];
293
301
  if (caps.images && this.showImages && img.data && img.mimeType) {
294
- const converted = this.convertedImages.get(i);
302
+ const cached = this.convertedImages.get(i);
303
+ const converted = cached?.sourceData === img.data && cached.sourceMimeType === img.mimeType ? cached : undefined;
295
304
  const imageData = converted?.data ?? img.data;
296
305
  const imageMimeType = converted?.mimeType ?? img.mimeType;
297
306
  if (caps.images === "kitty" && imageMimeType !== "image/png")
@@ -239,6 +239,8 @@ class TreeList {
239
239
  const searchTokens = this.searchQuery.toLowerCase().split(/\s+/).filter(Boolean);
240
240
  this.filteredNodes = this.flatNodes.filter((flatNode) => {
241
241
  const entry = flatNode.node.entry;
242
+ if (entry.type === "usage")
243
+ return false;
242
244
  const isCurrentLeaf = entry.id === this.currentLeafId;
243
245
  // Skip assistant messages with only tool calls (no text) unless error/aborted
244
246
  // Always show current leaf so active position is visible
@@ -2,7 +2,8 @@
2
2
  * Interactive mode for the coding agent.
3
3
  * Handles TUI rendering and user interaction, delegating business logic to AgentSession.
4
4
  */
5
- import type { ImageContent } from "@earendil-works/pi-ai/compat";
5
+ import { type ImageContent } from "@earendil-works/pi-ai/compat";
6
+ import { type Terminal } from "@earendil-works/pi-tui";
6
7
  import { type AgentSessionRuntime } from "../../core/agent-session-runtime.ts";
7
8
  import { SessionManager } from "../../core/session-manager.ts";
8
9
  import type { FullscreenExitOutput, TuiMode } from "../../core/settings-manager.ts";
@@ -35,6 +36,8 @@ export interface InteractiveModeOptions {
35
36
  tuiMode?: TuiMode;
36
37
  /** Initial interactive theme setting for this invocation. */
37
38
  initialThemeSetting?: string;
39
+ /** Terminal implementation. Defaults to the current process terminal. */
40
+ terminal?: Terminal;
38
41
  }
39
42
  export declare class InteractiveMode {
40
43
  private runtimeHost;
@@ -96,6 +99,8 @@ export declare class InteractiveMode {
96
99
  private retryEscapeHandler?;
97
100
  private compactionQueuedMessages;
98
101
  private shutdownRequested;
102
+ /** The `/bug` hint is shown at most once per session so error output stays readable. */
103
+ private bugReportHintShown;
99
104
  private extensionSelector;
100
105
  private extensionInput;
101
106
  private extensionEditor;
@@ -175,6 +180,11 @@ export declare class InteractiveMode {
175
180
  private applyRuntimeSettings;
176
181
  private rebindCurrentSession;
177
182
  private handleFatalRuntimeError;
183
+ /** Persist a crash so the next start can point the user at `/bug`. Returns false when nothing was written. */
184
+ private recordCrash;
185
+ private crashReportInstructions;
186
+ private suggestBugReport;
187
+ private maybeSuggestBugReport;
178
188
  private renderCurrentSessionState;
179
189
  /**
180
190
  * Get a registered tool definition by name (for custom rendering).
@@ -297,12 +307,14 @@ export declare class InteractiveMode {
297
307
  * @param options.populateHistory Add user messages to editor history
298
308
  */
299
309
  private renderSessionEntries;
310
+ private addCacheWarmingUsage;
300
311
  /**
301
312
  * Render billing usage for a compaction or branch summary. The notice is derived
302
313
  * from persisted summary usage and is not stored as a separate session entry.
303
314
  */
304
315
  private addCompactionCostNotice;
305
- private maybeShowAssistantDiagnostics;
316
+ private static countDroppedThinkingBlocks;
317
+ private maybeShowThinkingDropNotice;
306
318
  /**
307
319
  * Show a transcript notice when a completed assistant message paid for a
308
320
  * significant cache miss. Only states observable facts: the miss itself,
@@ -420,6 +432,7 @@ export declare class InteractiveMode {
420
432
  private getPathCommandArgument;
421
433
  private handleImportCommand;
422
434
  private handleShareCommand;
435
+ private handleBugCommand;
423
436
  private handleCopyCommand;
424
437
  private handleNameCommand;
425
438
  private handleSessionCommand;