@earendil-works/pi-coding-agent 0.80.2 → 0.80.3

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 (118) hide show
  1. package/CHANGELOG.md +48 -1
  2. package/README.md +2 -1
  3. package/dist/cli/file-processor.d.ts.map +1 -1
  4. package/dist/cli/file-processor.js +13 -25
  5. package/dist/cli/file-processor.js.map +1 -1
  6. package/dist/core/agent-session.d.ts +2 -1
  7. package/dist/core/agent-session.d.ts.map +1 -1
  8. package/dist/core/agent-session.js +38 -28
  9. package/dist/core/agent-session.js.map +1 -1
  10. package/dist/core/extensions/index.d.ts +1 -1
  11. package/dist/core/extensions/index.d.ts.map +1 -1
  12. package/dist/core/extensions/index.js.map +1 -1
  13. package/dist/core/extensions/loader.d.ts.map +1 -1
  14. package/dist/core/extensions/loader.js +4 -0
  15. package/dist/core/extensions/loader.js.map +1 -1
  16. package/dist/core/extensions/types.d.ts +8 -1
  17. package/dist/core/extensions/types.d.ts.map +1 -1
  18. package/dist/core/extensions/types.js.map +1 -1
  19. package/dist/core/http-dispatcher.d.ts.map +1 -1
  20. package/dist/core/http-dispatcher.js +28 -1
  21. package/dist/core/http-dispatcher.js.map +1 -1
  22. package/dist/core/model-resolver.d.ts.map +1 -1
  23. package/dist/core/model-resolver.js +1 -1
  24. package/dist/core/model-resolver.js.map +1 -1
  25. package/dist/core/resource-loader.d.ts.map +1 -1
  26. package/dist/core/resource-loader.js +2 -0
  27. package/dist/core/resource-loader.js.map +1 -1
  28. package/dist/core/session-manager.d.ts +1 -1
  29. package/dist/core/session-manager.d.ts.map +1 -1
  30. package/dist/core/session-manager.js +7 -4
  31. package/dist/core/session-manager.js.map +1 -1
  32. package/dist/core/settings-manager.d.ts +5 -0
  33. package/dist/core/settings-manager.d.ts.map +1 -1
  34. package/dist/core/settings-manager.js +19 -0
  35. package/dist/core/settings-manager.js.map +1 -1
  36. package/dist/core/timings.d.ts +4 -2
  37. package/dist/core/timings.d.ts.map +1 -1
  38. package/dist/core/timings.js +24 -14
  39. package/dist/core/timings.js.map +1 -1
  40. package/dist/core/tools/read.d.ts.map +1 -1
  41. package/dist/core/tools/read.js +12 -25
  42. package/dist/core/tools/read.js.map +1 -1
  43. package/dist/index.d.ts +2 -2
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js.map +1 -1
  46. package/dist/main.d.ts.map +1 -1
  47. package/dist/main.js +16 -4
  48. package/dist/main.js.map +1 -1
  49. package/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  50. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  51. package/dist/modes/interactive/components/assistant-message.js +23 -15
  52. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  53. package/dist/modes/interactive/components/extension-editor.d.ts +3 -1
  54. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  55. package/dist/modes/interactive/components/extension-editor.js +12 -3
  56. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  57. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  58. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  59. package/dist/modes/interactive/components/settings-selector.js +13 -1
  60. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  61. package/dist/modes/interactive/components/status-indicator.d.ts +28 -0
  62. package/dist/modes/interactive/components/status-indicator.d.ts.map +1 -0
  63. package/dist/modes/interactive/components/status-indicator.js +60 -0
  64. package/dist/modes/interactive/components/status-indicator.js.map +1 -0
  65. package/dist/modes/interactive/components/user-message.d.ts +6 -2
  66. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  67. package/dist/modes/interactive/components/user-message.js +19 -6
  68. package/dist/modes/interactive/components/user-message.js.map +1 -1
  69. package/dist/modes/interactive/interactive-mode.d.ts +6 -7
  70. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  71. package/dist/modes/interactive/interactive-mode.js +115 -135
  72. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  73. package/dist/modes/rpc/rpc-client.d.ts +15 -0
  74. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  75. package/dist/modes/rpc/rpc-client.js +14 -0
  76. package/dist/modes/rpc/rpc-client.js.map +1 -1
  77. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  78. package/dist/modes/rpc/rpc-mode.js +16 -0
  79. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  80. package/dist/modes/rpc/rpc-types.d.ts +26 -0
  81. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  82. package/dist/modes/rpc/rpc-types.js.map +1 -1
  83. package/dist/rpc-entry.d.ts +3 -0
  84. package/dist/rpc-entry.d.ts.map +1 -0
  85. package/dist/rpc-entry.js +10 -0
  86. package/dist/rpc-entry.js.map +1 -0
  87. package/dist/utils/image-convert.d.ts +1 -0
  88. package/dist/utils/image-convert.d.ts.map +1 -1
  89. package/dist/utils/image-convert.js +21 -15
  90. package/dist/utils/image-convert.js.map +1 -1
  91. package/dist/utils/image-process.d.ts +18 -0
  92. package/dist/utils/image-process.d.ts.map +1 -0
  93. package/dist/utils/image-process.js +83 -0
  94. package/dist/utils/image-process.js.map +1 -0
  95. package/dist/utils/mime.d.ts.map +1 -1
  96. package/dist/utils/mime.js +41 -0
  97. package/dist/utils/mime.js.map +1 -1
  98. package/docs/extensions.md +14 -0
  99. package/docs/keybindings.md +1 -1
  100. package/docs/providers.md +2 -1
  101. package/docs/rpc.md +58 -0
  102. package/docs/settings.md +10 -0
  103. package/docs/usage.md +2 -2
  104. package/examples/extensions/custom-compaction.ts +1 -0
  105. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  106. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  107. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  108. package/examples/extensions/gondolin/package-lock.json +2 -2
  109. package/examples/extensions/gondolin/package.json +1 -1
  110. package/examples/extensions/handoff.ts +1 -1
  111. package/examples/extensions/qna.ts +1 -1
  112. package/examples/extensions/sandbox/package-lock.json +2 -2
  113. package/examples/extensions/sandbox/package.json +1 -1
  114. package/examples/extensions/summarize.ts +1 -0
  115. package/examples/extensions/with-deps/package-lock.json +2 -2
  116. package/examples/extensions/with-deps/package.json +1 -1
  117. package/npm-shrinkwrap.json +12 -12
  118. package/package.json +8 -5
@@ -7,7 +7,7 @@ import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
9
  import { getProviders, } from "@earendil-works/pi-ai/compat";
10
- import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@earendil-works/pi-tui";
10
+ import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@earendil-works/pi-tui";
11
11
  import chalk from "chalk";
12
12
  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";
@@ -40,7 +40,6 @@ import { BashExecutionComponent } from "./components/bash-execution.js";
40
40
  import { BorderedLoader } from "./components/bordered-loader.js";
41
41
  import { BranchSummaryMessageComponent } from "./components/branch-summary-message.js";
42
42
  import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message.js";
43
- import { CountdownTimer } from "./components/countdown-timer.js";
44
43
  import { CustomEditor } from "./components/custom-editor.js";
45
44
  import { CustomMessageComponent } from "./components/custom-message.js";
46
45
  import { DaxnutsComponent } from "./components/daxnuts.js";
@@ -58,6 +57,7 @@ import { ScopedModelsSelectorComponent } from "./components/scoped-models-select
58
57
  import { SessionSelectorComponent } from "./components/session-selector.js";
59
58
  import { SettingsSelectorComponent } from "./components/settings-selector.js";
60
59
  import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.js";
60
+ import { BranchSummaryStatusIndicator, CompactionStatusIndicator, IdleStatus, RetryStatusIndicator, WorkingStatusIndicator, } from "./components/status-indicator.js";
61
61
  import { ToolExecutionComponent } from "./components/tool-execution.js";
62
62
  import { TreeSelectorComponent } from "./components/tree-selector.js";
63
63
  import { TrustSelectorComponent } from "./components/trust-selector.js";
@@ -134,6 +134,7 @@ export function isApiKeyLoginProvider(providerId, oauthProviderIds, builtInProvi
134
134
  export class InteractiveMode {
135
135
  runtimeHost;
136
136
  ui;
137
+ loadedResourcesContainer;
137
138
  chatContainer;
138
139
  pendingMessagesContainer;
139
140
  statusContainer;
@@ -152,7 +153,8 @@ export class InteractiveMode {
152
153
  isInitialized = false;
153
154
  onInputCallback;
154
155
  pendingUserInputs = [];
155
- loadingAnimation = undefined;
156
+ activeStatusIndicator = undefined;
157
+ idleStatus = new IdleStatus();
156
158
  workingMessage = undefined;
157
159
  workingVisible = true;
158
160
  workingIndicatorOptions = undefined;
@@ -176,6 +178,7 @@ export class InteractiveMode {
176
178
  toolOutputExpanded = false;
177
179
  // Thinking block visibility state
178
180
  hideThinkingBlock = false;
181
+ outputPad = 1;
179
182
  // Skill commands: command name -> skill file path
180
183
  skillCommands = new Map();
181
184
  // Agent subscription unsubscribe function
@@ -188,11 +191,8 @@ export class InteractiveMode {
188
191
  // Track pending bash components (shown in pending area, moved to chat on submit)
189
192
  pendingBashComponents = [];
190
193
  // Auto-compaction state
191
- autoCompactionLoader = undefined;
192
194
  autoCompactionEscapeHandler;
193
195
  // Auto-retry state
194
- retryLoader = undefined;
195
- retryCountdown = undefined;
196
196
  retryEscapeHandler;
197
197
  // Messages queued while compaction is running
198
198
  compactionQueuedMessages = [];
@@ -246,6 +246,7 @@ export class InteractiveMode {
246
246
  this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
247
247
  this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
248
248
  this.headerContainer = new Container();
249
+ this.loadedResourcesContainer = new Container();
249
250
  this.chatContainer = new Container();
250
251
  this.pendingMessagesContainer = new Container();
251
252
  this.statusContainer = new Container();
@@ -267,6 +268,7 @@ export class InteractiveMode {
267
268
  this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
268
269
  // Load hide thinking block setting
269
270
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
271
+ this.outputPad = this.settingsManager.getOutputPad();
270
272
  // Register themes from resource loader and initialize
271
273
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
272
274
  this.themeController = new InteractiveThemeController(this.ui, this.settingsManager, (message) => this.showError(message), () => this.updateEditorBorderColor());
@@ -439,8 +441,10 @@ export class InteractiveMode {
439
441
  : "";
440
442
  console.log(theme.fg("dim", `Model scope: ${modelList}${cycleHint}`));
441
443
  }
442
- // Add header container as first child. Populate it after detectThemeIfUnset.
444
+ // Add header container as first child. Populate it after applying theme settings.
445
+ // Keep loaded resources before chat so restored session messages never precede them.
443
446
  this.ui.addChild(this.headerContainer);
447
+ this.ui.addChild(this.loadedResourcesContainer);
444
448
  this.ui.addChild(this.chatContainer);
445
449
  this.ui.addChild(this.pendingMessagesContainer);
446
450
  this.ui.addChild(this.statusContainer);
@@ -990,6 +994,8 @@ export class InteractiveMode {
990
994
  return lines.join("\n");
991
995
  }
992
996
  showLoadedResources(options) {
997
+ // Resource rendering is idempotent; chat clears no longer clear this separate container.
998
+ this.loadedResourcesContainer.clear();
993
999
  const showListing = options?.force || this.options.verbose || !this.settingsManager.getQuietStartup();
994
1000
  const showDiagnostics = showListing || options?.showDiagnosticsWhenQuiet === true;
995
1001
  if (!showListing && !showDiagnostics) {
@@ -1005,8 +1011,8 @@ export class InteractiveMode {
1005
1011
  };
1006
1012
  const addLoadedSection = (name, collapsedBody, expandedBody = collapsedBody, color = "mdHeading") => {
1007
1013
  const section = new ExpandableText(() => `${sectionHeader(name, color)}\n${collapsedBody}`, () => `${sectionHeader(name, color)}\n${expandedBody}`, this.getStartupExpansionState(), 0, 0);
1008
- this.chatContainer.addChild(section);
1009
- this.chatContainer.addChild(new Spacer(1));
1014
+ this.loadedResourcesContainer.addChild(section);
1015
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1010
1016
  };
1011
1017
  const skillsResult = this.session.resourceLoader.getSkills();
1012
1018
  const promptsResult = this.session.resourceLoader.getPrompts();
@@ -1040,7 +1046,7 @@ export class InteractiveMode {
1040
1046
  if (showListing) {
1041
1047
  const contextFiles = this.session.resourceLoader.getAgentsFiles().agentsFiles;
1042
1048
  if (contextFiles.length > 0) {
1043
- this.chatContainer.addChild(new Spacer(1));
1049
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1044
1050
  const contextList = contextFiles
1045
1051
  .map((f) => theme.fg("dim", ` ${this.formatDisplayPath(f.path)}`))
1046
1052
  .join("\n");
@@ -1103,14 +1109,14 @@ export class InteractiveMode {
1103
1109
  const skillDiagnostics = skillsResult.diagnostics;
1104
1110
  if (skillDiagnostics.length > 0) {
1105
1111
  const warningLines = this.formatDiagnostics(skillDiagnostics, sourceInfos);
1106
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0));
1107
- this.chatContainer.addChild(new Spacer(1));
1112
+ this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0));
1113
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1108
1114
  }
1109
1115
  const promptDiagnostics = promptsResult.diagnostics;
1110
1116
  if (promptDiagnostics.length > 0) {
1111
1117
  const warningLines = this.formatDiagnostics(promptDiagnostics, sourceInfos);
1112
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, 0, 0));
1113
- this.chatContainer.addChild(new Spacer(1));
1118
+ this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, 0, 0));
1119
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1114
1120
  }
1115
1121
  const extensionDiagnostics = [];
1116
1122
  const extensionErrors = this.session.resourceLoader.getExtensions().errors;
@@ -1126,14 +1132,14 @@ export class InteractiveMode {
1126
1132
  extensionDiagnostics.push(...shortcutDiagnostics);
1127
1133
  if (extensionDiagnostics.length > 0) {
1128
1134
  const warningLines = this.formatDiagnostics(extensionDiagnostics, sourceInfos);
1129
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0));
1130
- this.chatContainer.addChild(new Spacer(1));
1135
+ this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0));
1136
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1131
1137
  }
1132
1138
  const themeDiagnostics = themesResult.diagnostics;
1133
1139
  if (themeDiagnostics.length > 0) {
1134
1140
  const warningLines = this.formatDiagnostics(themeDiagnostics, sourceInfos);
1135
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0));
1136
- this.chatContainer.addChild(new Spacer(1));
1141
+ this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0));
1142
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1137
1143
  }
1138
1144
  }
1139
1145
  }
@@ -1151,11 +1157,7 @@ export class InteractiveMode {
1151
1157
  commandContextActions: {
1152
1158
  waitForIdle: () => this.session.agent.waitForIdle(),
1153
1159
  newSession: async (options) => {
1154
- if (this.loadingAnimation) {
1155
- this.loadingAnimation.stop();
1156
- this.loadingAnimation = undefined;
1157
- }
1158
- this.statusContainer.clear();
1160
+ this.clearStatusIndicator();
1159
1161
  try {
1160
1162
  return await this.runtimeHost.newSession(options);
1161
1163
  }
@@ -1225,8 +1227,13 @@ export class InteractiveMode {
1225
1227
  this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
1226
1228
  this.footerDataProvider.setCwd(this.sessionManager.getCwd());
1227
1229
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
1230
+ this.outputPad = this.settingsManager.getOutputPad();
1228
1231
  this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
1229
- this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
1232
+ const clearOnShrink = this.settingsManager.getClearOnShrink();
1233
+ this.ui.setClearOnShrink(clearOnShrink);
1234
+ if (!clearOnShrink && !this.activeStatusIndicator) {
1235
+ this.statusContainer.clear();
1236
+ }
1230
1237
  const editorPaddingX = this.settingsManager.getEditorPaddingX();
1231
1238
  const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
1232
1239
  this.defaultEditor.setPaddingX(editorPaddingX);
@@ -1261,6 +1268,7 @@ export class InteractiveMode {
1261
1268
  process.exit(1);
1262
1269
  }
1263
1270
  renderCurrentSessionState() {
1271
+ this.loadedResourcesContainer.clear();
1264
1272
  this.chatContainer.clear();
1265
1273
  this.pendingMessagesContainer.clear();
1266
1274
  this.compactionQueuedMessages = [];
@@ -1338,36 +1346,41 @@ export class InteractiveMode {
1338
1346
  this.footerDataProvider.setExtensionStatus(key, text);
1339
1347
  this.ui.requestRender();
1340
1348
  }
1341
- getWorkingLoaderMessage() {
1342
- return this.workingMessage ?? this.defaultWorkingMessage;
1343
- }
1344
- createWorkingLoader() {
1345
- return new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), this.getWorkingLoaderMessage(), this.workingIndicatorOptions);
1349
+ showStatusIndicator(indicator) {
1350
+ this.activeStatusIndicator?.dispose();
1351
+ this.activeStatusIndicator = indicator;
1352
+ this.statusContainer.clear();
1353
+ this.statusContainer.addChild(indicator);
1346
1354
  }
1347
- stopWorkingLoader() {
1348
- if (this.loadingAnimation) {
1349
- this.loadingAnimation.stop();
1350
- this.loadingAnimation = undefined;
1355
+ clearStatusIndicator(kind) {
1356
+ if (kind && this.activeStatusIndicator?.kind !== kind) {
1357
+ return;
1351
1358
  }
1359
+ const hadActiveStatusIndicator = this.activeStatusIndicator !== undefined;
1360
+ this.activeStatusIndicator?.dispose();
1361
+ this.activeStatusIndicator = undefined;
1352
1362
  this.statusContainer.clear();
1363
+ if (hadActiveStatusIndicator && this.ui.getClearOnShrink()) {
1364
+ this.statusContainer.addChild(this.idleStatus);
1365
+ }
1353
1366
  }
1354
1367
  setWorkingVisible(visible) {
1355
1368
  this.workingVisible = visible;
1356
1369
  if (!visible) {
1357
- this.stopWorkingLoader();
1370
+ this.clearStatusIndicator("working");
1358
1371
  this.ui.requestRender();
1359
1372
  return;
1360
1373
  }
1361
- if (this.session.isStreaming && !this.loadingAnimation) {
1362
- this.statusContainer.clear();
1363
- this.loadingAnimation = this.createWorkingLoader();
1364
- this.statusContainer.addChild(this.loadingAnimation);
1374
+ if (this.session.isStreaming && this.activeStatusIndicator?.kind !== "working") {
1375
+ this.showStatusIndicator(new WorkingStatusIndicator(this.ui, this.workingMessage ?? this.defaultWorkingMessage, this.workingIndicatorOptions));
1365
1376
  }
1366
1377
  this.ui.requestRender();
1367
1378
  }
1368
1379
  setWorkingIndicator(options) {
1369
1380
  this.workingIndicatorOptions = options;
1370
- this.loadingAnimation?.setIndicator(options);
1381
+ if (this.activeStatusIndicator?.kind === "working") {
1382
+ this.activeStatusIndicator.setIndicator(options);
1383
+ }
1371
1384
  this.ui.requestRender();
1372
1385
  }
1373
1386
  setHiddenThinkingLabel(label) {
@@ -1455,8 +1468,8 @@ export class InteractiveMode {
1455
1468
  this.workingMessage = undefined;
1456
1469
  this.workingVisible = true;
1457
1470
  this.setWorkingIndicator();
1458
- if (this.loadingAnimation) {
1459
- this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
1471
+ if (this.activeStatusIndicator?.kind === "working") {
1472
+ this.activeStatusIndicator.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
1460
1473
  }
1461
1474
  this.setHiddenThinkingLabel();
1462
1475
  }
@@ -1596,8 +1609,8 @@ export class InteractiveMode {
1596
1609
  setStatus: (key, text) => this.setExtensionStatus(key, text),
1597
1610
  setWorkingMessage: (message) => {
1598
1611
  this.workingMessage = message;
1599
- if (this.loadingAnimation) {
1600
- this.loadingAnimation.setMessage(message ?? this.defaultWorkingMessage);
1612
+ if (this.activeStatusIndicator?.kind === "working") {
1613
+ this.activeStatusIndicator.setMessage(message ?? this.defaultWorkingMessage);
1601
1614
  }
1602
1615
  },
1603
1616
  setWorkingVisible: (visible) => this.setWorkingVisible(visible),
@@ -1741,7 +1754,7 @@ export class InteractiveMode {
1741
1754
  }, () => {
1742
1755
  this.hideExtensionEditor();
1743
1756
  resolve(undefined);
1744
- });
1757
+ }, undefined, this.settingsManager.getExternalEditorCommand());
1745
1758
  this.editorContainer.clear();
1746
1759
  this.editorContainer.addChild(this.extensionEditor);
1747
1760
  this.ui.setFocus(this.extensionEditor);
@@ -2216,18 +2229,11 @@ export class InteractiveMode {
2216
2229
  this.defaultEditor.onEscape = this.retryEscapeHandler;
2217
2230
  this.retryEscapeHandler = undefined;
2218
2231
  }
2219
- if (this.retryCountdown) {
2220
- this.retryCountdown.dispose();
2221
- this.retryCountdown = undefined;
2222
- }
2223
- if (this.retryLoader) {
2224
- this.retryLoader.stop();
2225
- this.retryLoader = undefined;
2226
- }
2227
- this.stopWorkingLoader();
2228
2232
  if (this.workingVisible) {
2229
- this.loadingAnimation = this.createWorkingLoader();
2230
- this.statusContainer.addChild(this.loadingAnimation);
2233
+ this.showStatusIndicator(new WorkingStatusIndicator(this.ui, this.workingMessage ?? this.defaultWorkingMessage, this.workingIndicatorOptions));
2234
+ }
2235
+ else {
2236
+ this.clearStatusIndicator();
2231
2237
  }
2232
2238
  this.ui.requestRender();
2233
2239
  break;
@@ -2255,7 +2261,7 @@ export class InteractiveMode {
2255
2261
  this.ui.requestRender();
2256
2262
  }
2257
2263
  else if (event.message.role === "assistant") {
2258
- this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel);
2264
+ this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, this.outputPad);
2259
2265
  this.streamingMessage = event.message;
2260
2266
  this.chatContainer.addChild(this.streamingComponent);
2261
2267
  this.streamingComponent.updateContent(this.streamingMessage);
@@ -2363,11 +2369,7 @@ export class InteractiveMode {
2363
2369
  if (this.settingsManager.getShowTerminalProgress()) {
2364
2370
  this.ui.terminal.setProgress(false);
2365
2371
  }
2366
- if (this.loadingAnimation) {
2367
- this.loadingAnimation.stop();
2368
- this.loadingAnimation = undefined;
2369
- this.statusContainer.clear();
2370
- }
2372
+ this.clearStatusIndicator("working");
2371
2373
  if (this.streamingComponent) {
2372
2374
  this.chatContainer.removeChild(this.streamingComponent);
2373
2375
  this.streamingComponent = undefined;
@@ -2386,13 +2388,7 @@ export class InteractiveMode {
2386
2388
  this.defaultEditor.onEscape = () => {
2387
2389
  this.session.abortCompaction();
2388
2390
  };
2389
- this.statusContainer.clear();
2390
- const cancelHint = `(${keyText("app.interrupt")} to cancel)`;
2391
- const label = event.reason === "manual"
2392
- ? `Compacting context... ${cancelHint}`
2393
- : `${event.reason === "overflow" ? "Context overflow detected, " : ""}Auto-compacting... ${cancelHint}`;
2394
- this.autoCompactionLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), label);
2395
- this.statusContainer.addChild(this.autoCompactionLoader);
2391
+ this.showStatusIndicator(new CompactionStatusIndicator(this.ui, event.reason));
2396
2392
  this.ui.requestRender();
2397
2393
  break;
2398
2394
  }
@@ -2404,11 +2400,7 @@ export class InteractiveMode {
2404
2400
  this.defaultEditor.onEscape = this.autoCompactionEscapeHandler;
2405
2401
  this.autoCompactionEscapeHandler = undefined;
2406
2402
  }
2407
- if (this.autoCompactionLoader) {
2408
- this.autoCompactionLoader.stop();
2409
- this.autoCompactionLoader = undefined;
2410
- this.statusContainer.clear();
2411
- }
2403
+ this.clearStatusIndicator("compaction");
2412
2404
  if (event.aborted) {
2413
2405
  if (event.reason === "manual") {
2414
2406
  this.showError("Compaction cancelled");
@@ -2442,17 +2434,7 @@ export class InteractiveMode {
2442
2434
  this.defaultEditor.onEscape = () => {
2443
2435
  this.session.abortRetry();
2444
2436
  };
2445
- // Show retry indicator
2446
- this.statusContainer.clear();
2447
- this.retryCountdown?.dispose();
2448
- const retryMessage = (seconds) => `Retrying (${event.attempt}/${event.maxAttempts}) in ${seconds}s... (${keyText("app.interrupt")} to cancel)`;
2449
- this.retryLoader = new Loader(this.ui, (spinner) => theme.fg("warning", spinner), (text) => theme.fg("muted", text), retryMessage(Math.ceil(event.delayMs / 1000)));
2450
- this.retryCountdown = new CountdownTimer(event.delayMs, this.ui, (seconds) => {
2451
- this.retryLoader?.setMessage(retryMessage(seconds));
2452
- }, () => {
2453
- this.retryCountdown = undefined;
2454
- });
2455
- this.statusContainer.addChild(this.retryLoader);
2437
+ this.showStatusIndicator(new RetryStatusIndicator(this.ui, event.attempt, event.maxAttempts, event.delayMs));
2456
2438
  this.ui.requestRender();
2457
2439
  break;
2458
2440
  }
@@ -2462,16 +2444,7 @@ export class InteractiveMode {
2462
2444
  this.defaultEditor.onEscape = this.retryEscapeHandler;
2463
2445
  this.retryEscapeHandler = undefined;
2464
2446
  }
2465
- if (this.retryCountdown) {
2466
- this.retryCountdown.dispose();
2467
- this.retryCountdown = undefined;
2468
- }
2469
- // Stop loader
2470
- if (this.retryLoader) {
2471
- this.retryLoader.stop();
2472
- this.retryLoader = undefined;
2473
- this.statusContainer.clear();
2474
- }
2447
+ this.clearStatusIndicator("retry");
2475
2448
  // Show error only on final failure (success shows normal response)
2476
2449
  if (!event.success) {
2477
2450
  this.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`);
@@ -2562,12 +2535,12 @@ export class InteractiveMode {
2562
2535
  // Render user message separately if present
2563
2536
  if (skillBlock.userMessage) {
2564
2537
  this.chatContainer.addChild(new Spacer(1));
2565
- const userComponent = new UserMessageComponent(skillBlock.userMessage, this.getMarkdownThemeWithSettings());
2538
+ const userComponent = new UserMessageComponent(skillBlock.userMessage, this.getMarkdownThemeWithSettings(), this.outputPad);
2566
2539
  this.chatContainer.addChild(userComponent);
2567
2540
  }
2568
2541
  }
2569
2542
  else {
2570
- const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings());
2543
+ const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings(), this.outputPad);
2571
2544
  this.chatContainer.addChild(userComponent);
2572
2545
  }
2573
2546
  if (options?.populateHistory) {
@@ -2577,7 +2550,7 @@ export class InteractiveMode {
2577
2550
  break;
2578
2551
  }
2579
2552
  case "assistant": {
2580
- const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel);
2553
+ const assistantComponent = new AssistantMessageComponent(message, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel, this.outputPad);
2581
2554
  this.chatContainer.addChild(assistantComponent);
2582
2555
  break;
2583
2556
  }
@@ -2965,9 +2938,11 @@ export class InteractiveMode {
2965
2938
  if (isExpandable(activeHeader)) {
2966
2939
  activeHeader.setExpanded(expanded);
2967
2940
  }
2968
- for (const child of this.chatContainer.children) {
2969
- if (isExpandable(child)) {
2970
- child.setExpanded(expanded);
2941
+ for (const container of [this.loadedResourcesContainer, this.chatContainer]) {
2942
+ for (const child of container.children) {
2943
+ if (isExpandable(child)) {
2944
+ child.setExpanded(expanded);
2945
+ }
2971
2946
  }
2972
2947
  }
2973
2948
  this.ui.requestRender();
@@ -2987,10 +2962,9 @@ export class InteractiveMode {
2987
2962
  this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`);
2988
2963
  }
2989
2964
  async openExternalEditor() {
2990
- // Determine editor (respect $VISUAL, then $EDITOR)
2991
- const editorCmd = process.env.VISUAL || process.env.EDITOR;
2965
+ const editorCmd = this.settingsManager.getExternalEditorCommand();
2992
2966
  if (!editorCmd) {
2993
- this.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
2967
+ this.showWarning("No editor configured. Set externalEditor in settings.json or $VISUAL/$EDITOR.");
2994
2968
  return;
2995
2969
  }
2996
2970
  const currentText = this.editor.getExpandedText?.() ?? this.editor.getText();
@@ -3294,6 +3268,7 @@ export class InteractiveMode {
3294
3268
  showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
3295
3269
  defaultProjectTrust: this.settingsManager.getDefaultProjectTrust(),
3296
3270
  editorPaddingX: this.settingsManager.getEditorPaddingX(),
3271
+ outputPad: this.settingsManager.getOutputPad(),
3297
3272
  autocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),
3298
3273
  quietStartup: this.settingsManager.getQuietStartup(),
3299
3274
  clearOnShrink: this.settingsManager.getClearOnShrink(),
@@ -3395,6 +3370,23 @@ export class InteractiveMode {
3395
3370
  this.editor.setPaddingX(padding);
3396
3371
  }
3397
3372
  },
3373
+ onOutputPadChange: (padding) => {
3374
+ this.settingsManager.setOutputPad(padding);
3375
+ this.outputPad = padding;
3376
+ if (this.streamingComponent || this.session.isStreaming) {
3377
+ for (const child of this.chatContainer.children) {
3378
+ if (child instanceof AssistantMessageComponent || child instanceof UserMessageComponent) {
3379
+ child.setOutputPad(padding);
3380
+ }
3381
+ }
3382
+ if (this.streamingComponent) {
3383
+ this.streamingComponent.setOutputPad(padding);
3384
+ }
3385
+ this.ui.requestRender();
3386
+ return;
3387
+ }
3388
+ this.rebuildChatFromMessages();
3389
+ },
3398
3390
  onAutocompleteMaxVisibleChange: (maxVisible) => {
3399
3391
  this.settingsManager.setAutocompleteMaxVisible(maxVisible);
3400
3392
  this.defaultEditor.setAutocompleteMaxVisible(maxVisible);
@@ -3405,6 +3397,9 @@ export class InteractiveMode {
3405
3397
  onClearOnShrinkChange: (enabled) => {
3406
3398
  this.settingsManager.setClearOnShrink(enabled);
3407
3399
  this.ui.setClearOnShrink(enabled);
3400
+ if (!enabled && !this.activeStatusIndicator) {
3401
+ this.statusContainer.clear();
3402
+ }
3408
3403
  },
3409
3404
  onShowTerminalProgressChange: (enabled) => {
3410
3405
  this.settingsManager.setShowTerminalProgress(enabled);
@@ -3722,16 +3717,16 @@ export class InteractiveMode {
3722
3717
  break;
3723
3718
  }
3724
3719
  }
3725
- // Set up escape handler and loader if summarizing
3726
- let summaryLoader;
3720
+ // Set up escape handler and status indicator if summarizing
3721
+ let showingSummaryIndicator = false;
3727
3722
  const originalOnEscape = this.defaultEditor.onEscape;
3728
3723
  if (wantsSummary) {
3729
3724
  this.defaultEditor.onEscape = () => {
3730
3725
  this.session.abortBranchSummary();
3731
3726
  };
3732
3727
  this.chatContainer.addChild(new Spacer(1));
3733
- summaryLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `Summarizing branch... (${keyText("app.interrupt")} to cancel)`);
3734
- this.statusContainer.addChild(summaryLoader);
3728
+ this.showStatusIndicator(new BranchSummaryStatusIndicator(this.ui));
3729
+ showingSummaryIndicator = true;
3735
3730
  this.ui.requestRender();
3736
3731
  }
3737
3732
  try {
@@ -3762,9 +3757,8 @@ export class InteractiveMode {
3762
3757
  this.showError(error instanceof Error ? error.message : String(error));
3763
3758
  }
3764
3759
  finally {
3765
- if (summaryLoader) {
3766
- summaryLoader.stop();
3767
- this.statusContainer.clear();
3760
+ if (showingSummaryIndicator) {
3761
+ this.clearStatusIndicator("branchSummary");
3768
3762
  }
3769
3763
  this.defaultEditor.onEscape = originalOnEscape;
3770
3764
  }
@@ -3805,11 +3799,7 @@ export class InteractiveMode {
3805
3799
  });
3806
3800
  }
3807
3801
  async handleResumeSession(sessionPath, options) {
3808
- if (this.loadingAnimation) {
3809
- this.loadingAnimation.stop();
3810
- this.loadingAnimation = undefined;
3811
- }
3812
- this.statusContainer.clear();
3802
+ this.clearStatusIndicator();
3813
3803
  try {
3814
3804
  const result = await this.runtimeHost.switchSession(sessionPath, {
3815
3805
  withSession: options?.withSession,
@@ -4202,6 +4192,7 @@ export class InteractiveMode {
4202
4192
  return;
4203
4193
  }
4204
4194
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
4195
+ this.outputPad = this.settingsManager.getOutputPad();
4205
4196
  this.rebuildChatFromMessages();
4206
4197
  chatRestoredBeforeSessionStart = true;
4207
4198
  };
@@ -4225,7 +4216,11 @@ export class InteractiveMode {
4225
4216
  this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
4226
4217
  }
4227
4218
  this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
4228
- this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
4219
+ const clearOnShrink = this.settingsManager.getClearOnShrink();
4220
+ this.ui.setClearOnShrink(clearOnShrink);
4221
+ if (!clearOnShrink && !this.activeStatusIndicator) {
4222
+ this.statusContainer.clear();
4223
+ }
4229
4224
  this.setupAutocompleteProvider();
4230
4225
  const runner = this.session.extensionRunner;
4231
4226
  this.setupExtensionShortcuts(runner);
@@ -4304,11 +4299,7 @@ export class InteractiveMode {
4304
4299
  return;
4305
4300
  }
4306
4301
  try {
4307
- if (this.loadingAnimation) {
4308
- this.loadingAnimation.stop();
4309
- this.loadingAnimation = undefined;
4310
- }
4311
- this.statusContainer.clear();
4302
+ this.clearStatusIndicator();
4312
4303
  const result = await this.runtimeHost.importFromJsonl(inputPath);
4313
4304
  if (result.cancelled) {
4314
4305
  this.showStatus("Import cancelled");
@@ -4634,11 +4625,7 @@ export class InteractiveMode {
4634
4625
  this.ui.requestRender();
4635
4626
  }
4636
4627
  async handleClearCommand() {
4637
- if (this.loadingAnimation) {
4638
- this.loadingAnimation.stop();
4639
- this.loadingAnimation = undefined;
4640
- }
4641
- this.statusContainer.clear();
4628
+ this.clearStatusIndicator();
4642
4629
  try {
4643
4630
  const result = await this.runtimeHost.newSession();
4644
4631
  if (result.cancelled) {
@@ -4765,11 +4752,7 @@ export class InteractiveMode {
4765
4752
  this.ui.requestRender();
4766
4753
  }
4767
4754
  async handleCompactCommand(customInstructions) {
4768
- if (this.loadingAnimation) {
4769
- this.loadingAnimation.stop();
4770
- this.loadingAnimation = undefined;
4771
- }
4772
- this.statusContainer.clear();
4755
+ this.clearStatusIndicator();
4773
4756
  try {
4774
4757
  await this.session.compact(customInstructions);
4775
4758
  }
@@ -4781,10 +4764,7 @@ export class InteractiveMode {
4781
4764
  if (this.settingsManager.getShowTerminalProgress()) {
4782
4765
  this.ui.terminal.setProgress(false);
4783
4766
  }
4784
- if (this.loadingAnimation) {
4785
- this.loadingAnimation.stop();
4786
- this.loadingAnimation = undefined;
4787
- }
4767
+ this.clearStatusIndicator();
4788
4768
  this.themeController.disableAutoSync();
4789
4769
  this.clearExtensionTerminalInputListeners();
4790
4770
  this.footer.dispose();