@kolisachint/hoocode-agent 0.5.58 → 0.5.59

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.
@@ -11,6 +11,7 @@ import { isLongRetryDelayError } from "@kolisachint/hoocode-ai";
11
11
  import { Box, CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TUI, } from "@kolisachint/hoocode-tui";
12
12
  import { spawnSync } from "child_process";
13
13
  import { APP_NAME, APP_TITLE, VERSION } from "../../config.js";
14
+ import { TASK_TOOL_NAME } from "../../core/agent-frontmatter.js";
14
15
  import { setTerminalOwnedByTui } from "../../core/agent-log.js";
15
16
  import { loadAgentRegistry } from "../../core/agent-registry.js";
16
17
  import { parseSkillBlock } from "../../core/agent-session.js";
@@ -377,7 +378,7 @@ export class InteractiveMode {
377
378
  await this.rebindCurrentSession();
378
379
  });
379
380
  this.version = VERSION;
380
- this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
381
+ this.ui = new TUI(options.terminal ?? new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
381
382
  this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
382
383
  this.headerContainer = new Container();
383
384
  this.chatContainer = new Container();
@@ -790,7 +791,7 @@ export class InteractiveMode {
790
791
  setTerminalOwnedByTui(true);
791
792
  this.isInitialized = true;
792
793
  // Initialize extensions first so resources are shown before messages
793
- await this.rebindCurrentSession();
794
+ await this.rebindCurrentSession({ renderResources: true });
794
795
  // Render initial messages AFTER showing loaded resources
795
796
  this.renderInitialMessages();
796
797
  // Set up theme file watcher
@@ -1080,26 +1081,28 @@ export class InteractiveMode {
1080
1081
  this.showExtensionError(error.extensionPath, error.error, error.stack);
1081
1082
  },
1082
1083
  });
1083
- setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
1084
1084
  this.setupAutocompleteProvider();
1085
- const extensionRunner = this.session.extensionRunner;
1086
- this.setupExtensionShortcuts(extensionRunner);
1087
- // The startup path draws the listing here, because it renders messages
1088
- // without clearing. A *rebind* (/new, /resume, /fork) clears the transcript
1089
- // straight afterwards, so `renderCurrentSessionState` draws it again — this
1090
- // call is the one startup keeps.
1091
- this.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });
1092
- this.showStartupNoticesIfNeeded();
1085
+ this.setupExtensionShortcuts(this.session.extensionRunner);
1093
1086
  }
1087
+ /**
1088
+ * Push the current settings and session onto the chrome: keybindings, footer,
1089
+ * editor, cursor.
1090
+ *
1091
+ * Startup, a session swap (/new, /resume, /fork, /cd) and /reload all rebuild
1092
+ * the same things from disk, so they must all repaint the same way. They used
1093
+ * to do it with two hand-kept copies of this list, and the /reload copy was
1094
+ * missing pieces — a `compaction.enabled` edit left the footer still
1095
+ * promising auto-compaction. One list, called from both paths; the theme is
1096
+ * the second half, in `applySessionTheme`.
1097
+ */
1094
1098
  applyRuntimeSettings() {
1099
+ // Keybindings first: what follows is drawn with the hints they name.
1100
+ this.keybindings.reload();
1095
1101
  this.footer.setSession(this.session);
1096
1102
  this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
1097
1103
  this.footer.setToolOutputView(this.toolOutputView);
1098
- // The startup banner names the cwd, which `/cd` changes under it.
1099
- if (this.builtInHeader instanceof ExpandableText) {
1100
- this.builtInHeader.refresh();
1101
- }
1102
1104
  this.footerDataProvider.setCwd(this.sessionManager.getCwd());
1105
+ this.footerDataProvider.setSubagentEnabled(this.session.getActiveToolNames().includes(TASK_TOOL_NAME));
1103
1106
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
1104
1107
  this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
1105
1108
  this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
@@ -1115,15 +1118,63 @@ export class InteractiveMode {
1115
1118
  this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
1116
1119
  }
1117
1120
  }
1118
- async rebindCurrentSession() {
1121
+ /**
1122
+ * Load the theme the settings name, and rebuild what holds colour baked in.
1123
+ *
1124
+ * Runs after extensions are bound (and after `session.reload()`), because a
1125
+ * `resources_discover` handler can contribute the very theme directory the
1126
+ * name resolves in — resolving it earlier would fall back to dark for anyone
1127
+ * whose theme ships with an extension.
1128
+ *
1129
+ * The banner is rebuilt rather than invalidated: a `Text` holds its string
1130
+ * with the escapes already in it, so invalidating only drops the line cache
1131
+ * and the old theme's colours come straight back. It also names the working
1132
+ * directory, which `/cd` moves.
1133
+ */
1134
+ applySessionTheme() {
1135
+ setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
1136
+ const themeName = this.settingsManager.getTheme();
1137
+ if (themeName) {
1138
+ const themeResult = setTheme(themeName, true);
1139
+ if (!themeResult.success) {
1140
+ this.showError(`Failed to load theme "${themeName}": ${themeResult.error}\nFell back to dark theme.`);
1141
+ }
1142
+ }
1143
+ const activeHeader = this.chrome.customHeader ?? this.builtInHeader;
1144
+ if (isExpandable(activeHeader)) {
1145
+ activeHeader.setExpanded(this.getStartupExpansionState());
1146
+ }
1147
+ }
1148
+ /**
1149
+ * The half of the chrome refresh that has to wait for extensions to bind:
1150
+ * they can swap the editor out from under us, and the provider count is only
1151
+ * final once their `registerProvider` calls have run.
1152
+ */
1153
+ async finishRuntimeSettings() {
1154
+ await this.modelController.updateAvailableProviderCount();
1155
+ this.updateEditorBorderColor();
1156
+ this.refreshSessionIdentity();
1157
+ }
1158
+ /**
1159
+ * Rebind the mode to whatever session the runtime now holds.
1160
+ *
1161
+ * `renderResources` belongs to startup alone: it renders into a transcript it
1162
+ * never clears, so the listing has to be drawn here. Every other caller
1163
+ * clears the transcript straight afterwards and draws the listing itself
1164
+ * through `renderCurrentSessionState`.
1165
+ */
1166
+ async rebindCurrentSession(options) {
1119
1167
  this.unsubscribe?.();
1120
1168
  this.unsubscribe = undefined;
1121
1169
  this.applyRuntimeSettings();
1122
1170
  await this.bindCurrentSessionExtensions();
1171
+ this.applySessionTheme();
1172
+ if (options?.renderResources) {
1173
+ this.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });
1174
+ this.showStartupNoticesIfNeeded();
1175
+ }
1123
1176
  this.subscribeToAgent();
1124
- await this.modelController.updateAvailableProviderCount();
1125
- this.updateEditorBorderColor();
1126
- this.refreshSessionIdentity();
1177
+ await this.finishRuntimeSettings();
1127
1178
  }
1128
1179
  async handleFatalRuntimeError(prefix, error) {
1129
1180
  const message = error instanceof Error ? error.message : String(error);
@@ -1133,26 +1184,35 @@ export class InteractiveMode {
1133
1184
  process.exit(1);
1134
1185
  }
1135
1186
  /**
1136
- * Reset the transcript to whatever the (possibly just-swapped) session holds.
1137
- *
1138
- * Every caller /new, /resume, /fork reaches here *after* the runtime has
1139
- * rebound extensions, and rebinding is what renders the loaded-resource
1140
- * listing. Clearing the chat therefore wiped the listing a moment after it was
1141
- * drawn, which is why /reload showed skills, agents and plugins and starting a
1142
- * new session showed nothing. Re-rendering it here is what makes the surface
1143
- * common: one call site, so a session change of any kind reports the same
1144
- * capabilities /reload does.
1187
+ * Drop every view-layer reference into the transcript that is about to be
1188
+ * rebuilt. Shared by the session swaps and by /reload: a component that
1189
+ * survives the clear is detached, so anything still pointing at it (the open
1190
+ * chain the next tool call would join, the block the unfold key peels back)
1191
+ * would be writing to a frame nobody renders.
1145
1192
  */
1146
- renderCurrentSessionState() {
1193
+ resetTranscriptView() {
1147
1194
  this.chatContainer.clear();
1148
1195
  this.openChain = undefined;
1149
1196
  this.latestToolBlock = undefined;
1150
1197
  this.latestChain = undefined;
1151
- this.pendingMessagesContainer.clear();
1152
- this.messageQueue.resetCompactionQueue();
1153
1198
  this.streamingComponent = undefined;
1154
1199
  this.streamingMessage = undefined;
1155
1200
  this.pendingTools.clear();
1201
+ }
1202
+ /**
1203
+ * Reset the transcript to whatever the just-swapped session holds.
1204
+ *
1205
+ * Every caller — /new, /resume, /fork, /cd, /import — reaches here after the
1206
+ * runtime has rebound extensions. Drawing the listing here rather than in the
1207
+ * rebind is what makes the surface common: one call site, so a session change
1208
+ * of any kind reports the same capabilities /reload does, and neither path
1209
+ * draws the listing twice.
1210
+ */
1211
+ renderCurrentSessionState() {
1212
+ this.resetTranscriptView();
1213
+ // Queues belong to the session that is going away, not to the new one.
1214
+ this.pendingMessagesContainer.clear();
1215
+ this.messageQueue.resetCompactionQueue();
1156
1216
  this.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });
1157
1217
  this.renderInitialMessages();
1158
1218
  }
@@ -3689,40 +3749,25 @@ export class InteractiveMode {
3689
3749
  };
3690
3750
  try {
3691
3751
  await this.session.reload();
3692
- this.keybindings.reload();
3693
- const activeHeader = this.chrome.customHeader ?? this.builtInHeader;
3694
- if (isExpandable(activeHeader)) {
3695
- activeHeader.setExpanded(this.toolOutputExpanded);
3696
- }
3697
- setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
3698
- this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
3699
- const themeName = this.settingsManager.getTheme();
3700
- const themeResult = themeName ? setTheme(themeName, true) : { success: true };
3701
- if (!themeResult.success) {
3702
- this.showError(`Failed to load theme "${themeName}": ${themeResult.error}\nFell back to dark theme.`);
3703
- }
3704
- const editorBorder = this.settingsManager.getEditorBorder();
3705
- const editorPaddingX = this.settingsManager.getEditorPaddingX();
3706
- const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
3707
- this.defaultEditor.setBorder(editorBorder);
3708
- this.defaultEditor.setPaddingX(editorPaddingX);
3709
- this.defaultEditor.setAutocompleteMaxVisible(autocompleteMaxVisible);
3710
- if (this.editor !== this.defaultEditor) {
3711
- this.editor.setBorder?.(editorBorder);
3712
- this.editor.setPaddingX?.(editorPaddingX);
3713
- this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
3714
- }
3715
- this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
3716
- this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
3752
+ // The same chrome refresh a session swap runs, in the same order, so the
3753
+ // two paths cannot report different capabilities or settings.
3754
+ this.applyRuntimeSettings();
3755
+ this.applySessionTheme();
3717
3756
  this.setupAutocompleteProvider();
3718
- const runner = this.session.extensionRunner;
3719
- this.setupExtensionShortcuts(runner);
3720
- this.rebuildChatFromMessages();
3757
+ this.setupExtensionShortcuts(this.session.extensionRunner);
3758
+ // The session itself is not replaced, so the transcript is replayed
3759
+ // rather than rebuilt from a new one — but the view state pointing into
3760
+ // it is just as stale, and gets the same reset.
3761
+ this.resetTranscriptView();
3762
+ this.renderSessionContext(this.sessionManager.buildSessionContext());
3721
3763
  dismissReloadBox(this.editor);
3764
+ // Below the replayed transcript, not above it: /reload is asked for to
3765
+ // see what just loaded, and that answer belongs where you are looking.
3722
3766
  this.showLoadedResources({
3723
3767
  force: false,
3724
3768
  showDiagnosticsWhenQuiet: true,
3725
3769
  });
3770
+ await this.finishRuntimeSettings();
3726
3771
  const modelsJsonError = this.session.modelRegistry.getError();
3727
3772
  if (modelsJsonError) {
3728
3773
  this.showError(`models.json error: ${modelsJsonError}`);