@kolisachint/hoocode-agent 0.4.156 → 0.4.158

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 (75) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/dist/core/agent-session-stats.d.ts +17 -1
  3. package/dist/core/agent-session-stats.d.ts.map +1 -1
  4. package/dist/core/agent-session-stats.js +20 -0
  5. package/dist/core/agent-session-stats.js.map +1 -1
  6. package/dist/core/context-files.d.ts +14 -4
  7. package/dist/core/context-files.d.ts.map +1 -1
  8. package/dist/core/context-files.js +7 -4
  9. package/dist/core/context-files.js.map +1 -1
  10. package/dist/core/format-tokens.d.ts +9 -0
  11. package/dist/core/format-tokens.d.ts.map +1 -0
  12. package/dist/core/format-tokens.js +19 -0
  13. package/dist/core/format-tokens.js.map +1 -0
  14. package/dist/core/mcp-status.d.ts +32 -0
  15. package/dist/core/mcp-status.d.ts.map +1 -0
  16. package/dist/core/mcp-status.js +27 -0
  17. package/dist/core/mcp-status.js.map +1 -0
  18. package/dist/core/resource-loader.d.ts +6 -16
  19. package/dist/core/resource-loader.d.ts.map +1 -1
  20. package/dist/core/resource-loader.js.map +1 -1
  21. package/dist/core/settings-defaults.d.ts +1 -0
  22. package/dist/core/settings-defaults.d.ts.map +1 -1
  23. package/dist/core/settings-defaults.js +2 -1
  24. package/dist/core/settings-defaults.js.map +1 -1
  25. package/dist/core/settings-manager.d.ts +2 -0
  26. package/dist/core/settings-manager.d.ts.map +1 -1
  27. package/dist/core/settings-manager.js +8 -0
  28. package/dist/core/settings-manager.js.map +1 -1
  29. package/dist/core/settings-types.d.ts +1 -0
  30. package/dist/core/settings-types.d.ts.map +1 -1
  31. package/dist/core/settings-types.js.map +1 -1
  32. package/dist/core/tools/todo.d.ts +24 -0
  33. package/dist/core/tools/todo.d.ts.map +1 -1
  34. package/dist/core/tools/todo.js +50 -9
  35. package/dist/core/tools/todo.js.map +1 -1
  36. package/dist/core/wordmark.d.ts +2 -2
  37. package/dist/core/wordmark.d.ts.map +1 -1
  38. package/dist/core/wordmark.js +4 -4
  39. package/dist/core/wordmark.js.map +1 -1
  40. package/dist/extensions/core/hoo-core.d.ts +3 -0
  41. package/dist/extensions/core/hoo-core.d.ts.map +1 -1
  42. package/dist/extensions/core/hoo-core.js +3 -0
  43. package/dist/extensions/core/hoo-core.js.map +1 -1
  44. package/dist/extensions/core/mcp-loader.d.ts.map +1 -1
  45. package/dist/extensions/core/mcp-loader.js +27 -3
  46. package/dist/extensions/core/mcp-loader.js.map +1 -1
  47. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  48. package/dist/modes/interactive/components/footer.js +7 -26
  49. package/dist/modes/interactive/components/footer.js.map +1 -1
  50. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  51. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  52. package/dist/modes/interactive/components/settings-selector.js +15 -2
  53. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  54. package/dist/modes/interactive/components/task-panel.d.ts +20 -12
  55. package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
  56. package/dist/modes/interactive/components/task-panel.js +171 -233
  57. package/dist/modes/interactive/components/task-panel.js.map +1 -1
  58. package/dist/modes/interactive/interactive-mode.d.ts +56 -7
  59. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  60. package/dist/modes/interactive/interactive-mode.js +174 -20
  61. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  62. package/dist/modes/interactive/model-controller.d.ts +6 -0
  63. package/dist/modes/interactive/model-controller.d.ts.map +1 -1
  64. package/dist/modes/interactive/model-controller.js +33 -14
  65. package/dist/modes/interactive/model-controller.js.map +1 -1
  66. package/dist/modes/interactive/resource-display.d.ts +22 -0
  67. package/dist/modes/interactive/resource-display.d.ts.map +1 -1
  68. package/dist/modes/interactive/resource-display.js +73 -16
  69. package/dist/modes/interactive/resource-display.js.map +1 -1
  70. package/docs/settings.md +2 -1
  71. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  72. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  73. package/examples/extensions/sandbox/package.json +1 -1
  74. package/examples/extensions/with-deps/package.json +1 -1
  75. package/package.json +4 -4
@@ -10,14 +10,19 @@ import { createCompactionSummaryMessage } from "@kolisachint/hoocode-agent-core"
10
10
  import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TUI, } from "@kolisachint/hoocode-tui";
11
11
  import { spawnSync } from "child_process";
12
12
  import { APP_NAME, APP_TITLE, VERSION } from "../../config.js";
13
+ import { loadAgentRegistry } from "../../core/agent-registry.js";
13
14
  import { parseSkillBlock } from "../../core/agent-session.js";
15
+ import { sumAssistantUsage } from "../../core/agent-session-stats.js";
14
16
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
17
+ import { formatDurationSecs } from "../../core/format-duration.js";
18
+ import { formatTokens } from "../../core/format-tokens.js";
15
19
  import { KeybindingsManager } from "../../core/keybindings.js";
16
20
  import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
17
21
  import { SessionManager } from "../../core/session-manager.js";
18
22
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
19
23
  import { startupProgress } from "../../core/startup-progress.js";
20
24
  import { taskStore } from "../../core/task-store.js";
25
+ import { settleDanglingMainTasks } from "../../core/tools/todo.js";
21
26
  import { buildCompactWordmark } from "../../core/wordmark.js";
22
27
  import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
23
28
  import { parseGitUrl } from "../../utils/git.js";
@@ -25,6 +30,7 @@ import { killTrackedDetachedChildren } from "../../utils/shell.js";
25
30
  import { ensureTool } from "../../utils/tools-manager.js";
26
31
  import { checkForNewHooCodeVersion } from "../../utils/version-check.js";
27
32
  import { BashExecutionController } from "./bash-execution-controller.js";
33
+ import { SEGMENT_SEP } from "./brand.js";
28
34
  import { CommandExecutor } from "./command-executor.js";
29
35
  import { BELL, CompletionChime } from "./completion-chime.js";
30
36
  import { AssistantMessageComponent } from "./components/assistant-message.js";
@@ -50,7 +56,7 @@ import { ExtensionDialogs } from "./extension-dialogs.js";
50
56
  import { LoginController } from "./login-controller.js";
51
57
  import { MessageQueueController } from "./message-queue-controller.js";
52
58
  import { ModelController } from "./model-controller.js";
53
- import { ExpandableText, formatDisplayPath, isExpandable, showLoadedResources as renderLoadedResources, } from "./resource-display.js";
59
+ import { ExpandableText, formatDisplayPath, isExpandable, showLoadedResources as renderLoadedResources, willShowResourceListing, } from "./resource-display.js";
54
60
  import { checkForPackageUpdates, checkTmuxKeyboardSetup, getChangelogForDisplay } from "./startup-checks.js";
55
61
  import { TeamFocusController } from "./team-focus.js";
56
62
  import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, initTheme, onThemeChange, setRegisteredThemes, setTheme, setThemeInstance, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
@@ -151,6 +157,8 @@ export class InteractiveMode {
151
157
  lastEscapeTime = 0;
152
158
  changelogMarkdown = undefined;
153
159
  startupNoticesShown = false;
160
+ /** Billing caveat folded into the startup state line, once resolved. */
161
+ startupAuthNote = undefined;
154
162
  // Status line tracking (for mutating immediately-sequential status updates)
155
163
  lastStatusSpacer = undefined;
156
164
  lastStatusText = undefined;
@@ -199,9 +207,17 @@ export class InteractiveMode {
199
207
  // True between auto_retry_start and the next agent_start: the turn is not done,
200
208
  // it is about to re-run, so the deferred completion check must not fire the chime.
201
209
  chimePendingRetry = false;
202
- // Whether the current turn's final assistant message was user-aborted (Ctrl+C /
203
- // Esc). An aborted turn means the user is present, so no chime.
204
- chimeTurnAborted = false;
210
+ // Stop reason of the request's latest assistant message, reset when the user
211
+ // starts a new one. Read at settle time by two consumers that ask the same
212
+ // question — did this request end cleanly? An "aborted" turn means the user is
213
+ // present, so no chime; anything other than a clean "stop" means dangling plan
214
+ // items must not be claimed as completed.
215
+ turnStopReason = undefined;
216
+ // Cumulative usage + wall clock captured at the first agent_start of a request,
217
+ // diffed at agent_end to print that request's own cost into the transcript.
218
+ // Anchored on the FIRST start so a retried request reports one span, not one
219
+ // per attempt (same reason the chime anchors its duration there).
220
+ turnCostAnchor;
205
221
  // Shutdown state
206
222
  shutdownRequested = false;
207
223
  // Extension UI state
@@ -328,11 +344,12 @@ export class InteractiveMode {
328
344
  const editorPaddingX = this.settingsManager.getEditorPaddingX();
329
345
  const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
330
346
  this.defaultEditor = new CustomEditor(this.ui, getEditorTheme(), this.keybindings, {
347
+ border: this.settingsManager.getEditorBorder(),
331
348
  paddingX: editorPaddingX,
332
349
  autocompleteMaxVisible,
333
350
  });
334
351
  this.editor = this.defaultEditor;
335
- this.editor.promptPrefix = ">";
352
+ this.editor.promptPrefix = "";
336
353
  this.editorContainer = new Container();
337
354
  this.editorContainer.addChild(this.editor);
338
355
  this.footerDataProvider = new FooterDataProvider(this.sessionManager.getCwd());
@@ -819,12 +836,48 @@ export class InteractiveMode {
819
836
  getExtensionRunner: () => this.session.extensionRunner,
820
837
  getActiveMode: () => this.footerDataProvider.getActiveMode(),
821
838
  getSubagentEnabled: () => this.footerDataProvider.getSubagentEnabled(),
839
+ getAgentCount: () => this.getDispatchableAgentCount(),
840
+ getStateLine: () => this.buildSessionStateLine(),
822
841
  quietStartup: () => this.settingsManager.getQuietStartup(),
823
842
  verbose: this.options.verbose ?? false,
824
843
  isExpanded: () => this.getStartupExpansionState(),
825
844
  getBuiltInCommandConflictDiagnostics: (runner) => this.getBuiltInCommandConflictDiagnostics(runner),
826
845
  }, options);
827
846
  }
847
+ /**
848
+ * Agents the model can actually dispatch. Zero when the Task tool is off, so
849
+ * the summary never advertises a capability the session cannot use.
850
+ */
851
+ getDispatchableAgentCount() {
852
+ if (!this.footerDataProvider.getSubagentEnabled())
853
+ return 0;
854
+ try {
855
+ return loadAgentRegistry({ cwd: this.sessionManager.getCwd() }).list().length;
856
+ }
857
+ catch {
858
+ return 0;
859
+ }
860
+ }
861
+ /**
862
+ * One line of session state for the startup summary: the model, its thinking
863
+ * level, and any billing caveat. The footer carries the same facts for the
864
+ * rest of the session, so this replaces the separate status lines that used
865
+ * to be emitted for each of them.
866
+ */
867
+ buildSessionStateLine() {
868
+ const model = this.session.model;
869
+ if (!model)
870
+ return undefined;
871
+ const segments = [theme.fg("muted", model.id)];
872
+ const thinkingLevel = this.session.thinkingLevel;
873
+ if (model.reasoning && thinkingLevel && thinkingLevel !== "off") {
874
+ segments.push(theme.fg("dim", `thinking ${thinkingLevel}`));
875
+ }
876
+ if (this.startupAuthNote) {
877
+ segments.push(theme.fg("warning", this.startupAuthNote));
878
+ }
879
+ return segments.join(theme.fg("muted", ` ${SEGMENT_SEP} `));
880
+ }
828
881
  /**
829
882
  * Initialize the extension system with TUI-based UI context.
830
883
  */
@@ -906,6 +959,15 @@ export class InteractiveMode {
906
959
  this.setupAutocompleteProvider();
907
960
  const extensionRunner = this.session.extensionRunner;
908
961
  this.setupExtensionShortcuts(extensionRunner);
962
+ // Resolved before the summary renders so the billing caveat lands on the
963
+ // state line instead of arriving as a late, separate warning. Skipped on a
964
+ // quiet startup, where the full warning still fires from run().
965
+ if (willShowResourceListing({
966
+ verbose: this.options.verbose ?? false,
967
+ quietStartup: () => this.settingsManager.getQuietStartup(),
968
+ })) {
969
+ this.startupAuthNote = await this.modelController.getAnthropicSubscriptionNote();
970
+ }
909
971
  this.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });
910
972
  this.showStartupNoticesIfNeeded();
911
973
  }
@@ -916,11 +978,14 @@ export class InteractiveMode {
916
978
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
917
979
  this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
918
980
  this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
981
+ const editorBorder = this.settingsManager.getEditorBorder();
919
982
  const editorPaddingX = this.settingsManager.getEditorPaddingX();
920
983
  const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
984
+ this.defaultEditor.setBorder(editorBorder);
921
985
  this.defaultEditor.setPaddingX(editorPaddingX);
922
986
  this.defaultEditor.setAutocompleteMaxVisible(autocompleteMaxVisible);
923
987
  if (this.editor !== this.defaultEditor) {
988
+ this.editor.setBorder?.(editorBorder);
924
989
  this.editor.setPaddingX?.(editorPaddingX);
925
990
  this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
926
991
  }
@@ -1616,20 +1681,94 @@ export class InteractiveMode {
1616
1681
  });
1617
1682
  }
1618
1683
  /**
1619
- * Deferred completion chime. Called from the agent_end handler, it waits one
1620
- * tick so the streaming flag settles and any retry has had a chance to arm, then
1621
- * rings only when the session is genuinely idle (not streaming, not compacting)
1622
- * and no retry is pending. The chime itself enforces the enable/threshold/debounce
1623
- * rules; here we only decide whether the turn has truly ended.
1684
+ * Deferred end-of-request settle. Called from the agent_end handler, it waits
1685
+ * one tick so the streaming flag settles and any retry has had a chance to arm,
1686
+ * then acts only when the session is genuinely idle (not streaming, not
1687
+ * compacting) and no retry is pending so a retried or auto-continued request
1688
+ * neither rings a premature "it's done" nor prints a partial cost line.
1689
+ *
1690
+ * The completion chime, the transcript cost line, and settling dangling plan
1691
+ * items all hang off this single check because they answer the same question:
1692
+ * has the request actually ended?
1624
1693
  */
1625
- maybeChimeOnIdle() {
1694
+ settleRequestOnIdle() {
1626
1695
  setTimeout(() => {
1627
1696
  if (this.chimePendingRetry || this.session.isStreaming || this.session.isCompacting) {
1628
1697
  return;
1629
1698
  }
1630
- this.chime?.onTurnComplete({ aborted: this.chimeTurnAborted });
1699
+ this.showTurnCost();
1700
+ this.settleDanglingPlanItems();
1701
+ this.chime?.onTurnComplete({ aborted: this.turnStopReason === "aborted" });
1631
1702
  }, 0);
1632
1703
  }
1704
+ /**
1705
+ * Flip main-plan rows the model left at in_progress to a settled status now the
1706
+ * request is over, instead of letting them claim live work until the next user
1707
+ * message wipes the pane.
1708
+ *
1709
+ * The model marks its own TodoWrite items and routinely drops the final call
1710
+ * that completes the last one, so the row outlives the work. A clean "stop" is
1711
+ * taken as the model believing it was finished — it chose to stop talking, and
1712
+ * its closing message is the report — so those items settle to done. An abort,
1713
+ * error, or length cutoff says the opposite, so they settle to cancelled: an
1714
+ * honest "never finished" rather than a fabricated completion.
1715
+ *
1716
+ * Skipped while messages are queued: a follow-up/steer arriving during the run
1717
+ * means the request continues, and the plan with it.
1718
+ */
1719
+ settleDanglingPlanItems() {
1720
+ if (this.session.pendingMessageCount > 0)
1721
+ return;
1722
+ const settled = settleDanglingMainTasks(this.turnStopReason === "stop" ? "done" : "cancelled");
1723
+ if (settled > 0)
1724
+ this.ui.requestRender();
1725
+ }
1726
+ /**
1727
+ * Append this request's own token/time/cost to the transcript.
1728
+ *
1729
+ * This is the honest home for the number. The task panel is a live instrument —
1730
+ * present tense, wiped on the next user message — and the footer carries
1731
+ * cumulative session vitals, so after the fact neither can answer "what did that
1732
+ * request cost". Fired at agent_end rather than turn_end because one request is
1733
+ * commonly tens of turns; per-turn would wedge a number between every tool block.
1734
+ *
1735
+ * Delegated runs are reported separately rather than folded into the totals:
1736
+ * subagents bill against their own sessions, so their tokens never appear in this
1737
+ * session's entries and adding them to ↑/↓ would misreport what the parent spent.
1738
+ */
1739
+ showTurnCost() {
1740
+ const anchor = this.turnCostAnchor;
1741
+ this.turnCostAnchor = undefined;
1742
+ if (!anchor)
1743
+ return;
1744
+ const now = sumAssistantUsage(this.session.sessionManager.getEntries());
1745
+ const input = now.input - anchor.totals.input;
1746
+ const output = now.output - anchor.totals.output;
1747
+ const cost = now.cost - anchor.totals.cost;
1748
+ // Nothing was accounted (e.g. aborted before the first response landed): stay
1749
+ // silent rather than print a row of zeroes.
1750
+ if (input <= 0 && output <= 0)
1751
+ return;
1752
+ const segs = [
1753
+ theme.fg("dim", "↑") +
1754
+ theme.fg("muted", formatTokens(input)) +
1755
+ theme.fg("dim", " ↓") +
1756
+ theme.fg("muted", formatTokens(output)),
1757
+ theme.fg("muted", formatDurationSecs((Date.now() - anchor.at) / 1000)),
1758
+ ];
1759
+ if (cost > 0)
1760
+ segs.push(theme.fg("muted", `$${cost.toFixed(3)}`));
1761
+ const runs = taskStore.list().filter((task) => task.source === "subagent");
1762
+ if (runs.length > 0) {
1763
+ const delegatedTokens = runs.reduce((sum, r) => sum + (r.usage ? r.usage.input + r.usage.output : 0), 0);
1764
+ const label = runs.length === 1 ? (runs[0]?.subagentMode ?? "subagent") : "subagents";
1765
+ const text = `◇${runs.length} ${label}${delegatedTokens > 0 ? ` ${formatTokens(delegatedTokens)}` : ""}`;
1766
+ segs.push(theme.fg("dim", text));
1767
+ }
1768
+ this.chatContainer.addChild(new Spacer(1));
1769
+ this.chatContainer.addChild(new Text(segs.join(theme.fg("dim", " · ")), 1, 0));
1770
+ this.ui.requestRender();
1771
+ }
1633
1772
  async handleEvent(event) {
1634
1773
  if (!this.isInitialized) {
1635
1774
  await this.init();
@@ -1642,6 +1781,10 @@ export class InteractiveMode {
1642
1781
  // first start and clear the pending-retry gate now that it has resumed.
1643
1782
  this.chime?.onTurnStart();
1644
1783
  this.chimePendingRetry = false;
1784
+ this.turnCostAnchor ??= {
1785
+ totals: sumAssistantUsage(this.session.sessionManager.getEntries()),
1786
+ at: Date.now(),
1787
+ };
1645
1788
  if (this.settingsManager.getShowTerminalProgress()) {
1646
1789
  this.ui.terminal.setProgress(true);
1647
1790
  }
@@ -1696,7 +1839,7 @@ export class InteractiveMode {
1696
1839
  // turn, drop any lingering bars/notices (e.g. an unavailable-index line)
1697
1840
  // so they don't sit in the footer for the rest of the session.
1698
1841
  startupProgress.clear();
1699
- this.chimeTurnAborted = false;
1842
+ this.turnStopReason = undefined;
1700
1843
  this.addMessageToChat(event.message);
1701
1844
  this.messageQueue.updatePendingMessagesDisplay();
1702
1845
  this.ui.requestRender();
@@ -1740,9 +1883,9 @@ export class InteractiveMode {
1740
1883
  if (event.message.role === "user")
1741
1884
  break;
1742
1885
  if (event.message.role === "assistant") {
1743
- // Remember whether the turn's latest assistant message was user-aborted,
1744
- // so the completion chime stays silent when the user is clearly present.
1745
- this.chimeTurnAborted = event.message.stopReason === "aborted";
1886
+ // Remember how the turn's latest assistant message ended, so the deferred
1887
+ // settle can tell a clean finish from an abort/error.
1888
+ this.turnStopReason = event.message.stopReason;
1746
1889
  }
1747
1890
  if (this.streamingComponent && event.message.role === "assistant") {
1748
1891
  this.streamingMessage = event.message;
@@ -1832,10 +1975,10 @@ export class InteractiveMode {
1832
1975
  await this.checkShutdownRequested();
1833
1976
  // agent_end fires before the streaming flag clears and before the retry
1834
1977
  // decision is made (both happen after this listener returns), so defer a
1835
- // tick and re-check: only chime once the session is genuinely idle and no
1978
+ // tick and re-check: only settle once the session is genuinely idle and no
1836
1979
  // retry is pending — otherwise a retried or auto-continued turn would ring
1837
- // a premature "it's done".
1838
- this.maybeChimeOnIdle();
1980
+ // a premature "it's done" and print a partial cost line.
1981
+ this.settleRequestOnIdle();
1839
1982
  this.ui.requestRender();
1840
1983
  break;
1841
1984
  case "compaction_start": {
@@ -2323,7 +2466,7 @@ export class InteractiveMode {
2323
2466
  this.editor.promptColor = theme.getBashModeBorderColor();
2324
2467
  }
2325
2468
  else {
2326
- this.editor.promptPrefix = ">";
2469
+ this.editor.promptPrefix = "";
2327
2470
  this.editor.promptColor = (s) => s;
2328
2471
  }
2329
2472
  this.ui.requestRender();
@@ -2542,6 +2685,7 @@ export class InteractiveMode {
2542
2685
  doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
2543
2686
  treeFilterMode: this.settingsManager.getTreeFilterMode(),
2544
2687
  showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
2688
+ editorBorder: this.settingsManager.getEditorBorder(),
2545
2689
  editorPaddingX: this.settingsManager.getEditorPaddingX(),
2546
2690
  autocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),
2547
2691
  quietStartup: this.settingsManager.getQuietStartup(),
@@ -2711,6 +2855,13 @@ export class InteractiveMode {
2711
2855
  this.settingsManager.setShowHardwareCursor(enabled);
2712
2856
  this.ui.setShowHardwareCursor(enabled);
2713
2857
  },
2858
+ onEditorBorderChange: (border) => {
2859
+ this.settingsManager.setEditorBorder(border);
2860
+ this.defaultEditor.setBorder(border);
2861
+ if (this.editor !== this.defaultEditor && this.editor.setBorder !== undefined) {
2862
+ this.editor.setBorder(border);
2863
+ }
2864
+ },
2714
2865
  onEditorPaddingXChange: (padding) => {
2715
2866
  this.settingsManager.setEditorPaddingX(padding);
2716
2867
  this.defaultEditor.setPaddingX(padding);
@@ -2994,11 +3145,14 @@ export class InteractiveMode {
2994
3145
  if (!themeResult.success) {
2995
3146
  this.showError(`Failed to load theme "${themeName}": ${themeResult.error}\nFell back to dark theme.`);
2996
3147
  }
3148
+ const editorBorder = this.settingsManager.getEditorBorder();
2997
3149
  const editorPaddingX = this.settingsManager.getEditorPaddingX();
2998
3150
  const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
3151
+ this.defaultEditor.setBorder(editorBorder);
2999
3152
  this.defaultEditor.setPaddingX(editorPaddingX);
3000
3153
  this.defaultEditor.setAutocompleteMaxVisible(autocompleteMaxVisible);
3001
3154
  if (this.editor !== this.defaultEditor) {
3155
+ this.editor.setBorder?.(editorBorder);
3002
3156
  this.editor.setPaddingX?.(editorPaddingX);
3003
3157
  this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
3004
3158
  }