@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.
- package/CHANGELOG.md +18 -0
- package/dist/modes/interactive/interactive-mode.d.ts +46 -8
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +104 -59
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
*
|
|
1137
|
-
*
|
|
1138
|
-
*
|
|
1139
|
-
*
|
|
1140
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
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
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
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}`);
|