@kolisachint/hoocode-agent 0.5.57 → 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 +181 -0
- package/dist/core/agent-session.d.ts +2 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +4 -3
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +4 -1
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/keybindings.d.ts +162 -98
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +222 -106
- package/dist/core/keybindings.js.map +1 -1
- package/dist/modes/interactive/command-executor.d.ts.map +1 -1
- package/dist/modes/interactive/command-executor.js +95 -62
- package/dist/modes/interactive/command-executor.js.map +1 -1
- package/dist/modes/interactive/components/ask-options.d.ts +20 -6
- package/dist/modes/interactive/components/ask-options.d.ts.map +1 -1
- package/dist/modes/interactive/components/ask-options.js +35 -13
- package/dist/modes/interactive/components/ask-options.js.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.d.ts +9 -0
- package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.js +12 -0
- package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
- package/dist/modes/interactive/components/task-panel.d.ts +2 -2
- package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/task-panel.js +24 -11
- package/dist/modes/interactive/components/task-panel.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-selector.js +29 -12
- package/dist/modes/interactive/components/tree-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +63 -8
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +189 -90
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/model-controller.d.ts +3 -0
- package/dist/modes/interactive/model-controller.d.ts.map +1 -1
- package/dist/modes/interactive/model-controller.js +1 -1
- package/dist/modes/interactive/model-controller.js.map +1 -1
- package/docs/keybindings.md +247 -80
- package/docs/terminal-setup.md +8 -3
- 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";
|
|
@@ -51,7 +52,7 @@ import { CustomEditor } from "./components/custom-editor.js";
|
|
|
51
52
|
import { CustomMessageComponent } from "./components/custom-message.js";
|
|
52
53
|
import { DynamicBorder } from "./components/dynamic-border.js";
|
|
53
54
|
import { FooterComponent } from "./components/footer.js";
|
|
54
|
-
import { keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
|
|
55
|
+
import { appKeyLabel, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
|
|
55
56
|
import { renderSessionChip } from "./components/session-chip.js";
|
|
56
57
|
import { SessionColorSelectorComponent } from "./components/session-color-selector.js";
|
|
57
58
|
import { SessionSelectorComponent } from "./components/session-selector.js";
|
|
@@ -185,6 +186,8 @@ export class InteractiveMode {
|
|
|
185
186
|
startupNoticesShown = false;
|
|
186
187
|
// Status line tracking (for mutating immediately-sequential status updates)
|
|
187
188
|
lastStatusSpacer = undefined;
|
|
189
|
+
/** Dial reverses already named this session; see showDialStep. */
|
|
190
|
+
dialReverseTaught = new Set();
|
|
188
191
|
lastStatusText = undefined;
|
|
189
192
|
// Streaming message tracking
|
|
190
193
|
streamingComponent = undefined;
|
|
@@ -375,7 +378,7 @@ export class InteractiveMode {
|
|
|
375
378
|
await this.rebindCurrentSession();
|
|
376
379
|
});
|
|
377
380
|
this.version = VERSION;
|
|
378
|
-
this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
|
|
381
|
+
this.ui = new TUI(options.terminal ?? new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
|
|
379
382
|
this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
|
|
380
383
|
this.headerContainer = new Container();
|
|
381
384
|
this.chatContainer = new Container();
|
|
@@ -461,6 +464,7 @@ export class InteractiveMode {
|
|
|
461
464
|
},
|
|
462
465
|
showSelector: (create) => this.showSelector(create),
|
|
463
466
|
showStatus: (message) => this.showStatus(message),
|
|
467
|
+
showDialStep: (backward, message) => this.showDialStep(backward, message),
|
|
464
468
|
showError: (message) => this.showError(message),
|
|
465
469
|
showWarning: (message) => this.showWarning(message),
|
|
466
470
|
showNotice: (title, body) => this.showNotice(title, body),
|
|
@@ -709,33 +713,53 @@ export class InteractiveMode {
|
|
|
709
713
|
: theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
|
|
710
714
|
// Build startup instructions using keybinding hint helpers
|
|
711
715
|
const hint = (keybinding, description) => keyHint(keybinding, description);
|
|
716
|
+
// A dial as one line in one shape: the key steps it forward, the same
|
|
717
|
+
// key with shift steps it back. The banner is where that rule is first
|
|
718
|
+
// read, and it is the rule covering the most-pressed keys in the app,
|
|
719
|
+
// so the dials are listed together rather than scattered by topic.
|
|
720
|
+
// appKeyLabel, not keyText: the thinking dial answers to shift+tab as well,
|
|
721
|
+
// and splicing that into the middle of `alt+t/shift+alt+t` would hide the
|
|
722
|
+
// one shape these six lines exist to show.
|
|
723
|
+
const dial = (forward, backward, subject) => rawKeyHint(`${appKeyLabel(forward)}/${appKeyLabel(backward)}`, `to step ${subject}`);
|
|
724
|
+
// Grouped the way the map itself is (see core/keybindings.ts): by what you
|
|
725
|
+
// are trying to do, not by what the widget is. A flat list of thirty
|
|
726
|
+
// hints is a wall nobody reads; five short groups with a heading each is
|
|
727
|
+
// something a person can skim once and place a key in later.
|
|
728
|
+
const group = (title) => theme.fg("dim", `\n${title}`);
|
|
712
729
|
const expandedInstructions = [
|
|
730
|
+
group("Compose — the message in your hands"),
|
|
731
|
+
hint("app.editor.external", "for external editor"),
|
|
732
|
+
hint("app.input.voiceTranscribe", "to speak instead of type"),
|
|
733
|
+
hint("app.clipboard.pasteImage", "to paste image"),
|
|
734
|
+
hint("app.message.followUp", "to queue follow-up"),
|
|
735
|
+
hint("app.message.dequeue", "to edit all queued messages"),
|
|
736
|
+
rawKeyHint("drop files", "to attach"),
|
|
737
|
+
rawKeyHint("/", "for commands"),
|
|
738
|
+
rawKeyHint("!", "to run bash"),
|
|
739
|
+
rawKeyHint("!!", "to run bash (no context)"),
|
|
740
|
+
group("Steer — what the agent is before it runs"),
|
|
741
|
+
dial("app.mode.cycleForward", "app.mode.cycleBackward", "agent mode (ask/plan/build/debug)"),
|
|
742
|
+
dial("app.model.cycleForward", "app.model.cycleBackward", "model — /model to pick one"),
|
|
743
|
+
dial("app.thinking.cycleForward", "app.thinking.cycleBackward", "thinking level"),
|
|
744
|
+
group("Read — what you see of what it did"),
|
|
745
|
+
dial("app.view.cycleForward", "app.view.cycleBackward", "tool output (radar/peek/full)"),
|
|
746
|
+
dial("app.tasks.cycleForward", "app.tasks.cycleBackward", "task panel view"),
|
|
747
|
+
hint("app.tools.expand", "to jump to full output and back"),
|
|
748
|
+
hint("app.thinking.toggle", "to show or hide thinking"),
|
|
749
|
+
...(this.teamFocus.connected ? [hint("app.team.focus", "to focus team roster")] : []),
|
|
750
|
+
group("Go — sessions and places"),
|
|
751
|
+
hint("app.session.resume", "to resume a session"),
|
|
752
|
+
hint("app.session.changeDirectory", "to change working directory"),
|
|
753
|
+
dial("app.session.color.cycleForward", "app.session.color.cycleBackward", "session colour"),
|
|
754
|
+
hint("app.settings.open", "for settings"),
|
|
755
|
+
hint("app.hotkeys.open", "for all shortcuts"),
|
|
756
|
+
group("Flow — getting out, getting back"),
|
|
713
757
|
hint("app.interrupt", "to interrupt"),
|
|
714
758
|
hint("app.clear", "to clear"),
|
|
715
759
|
rawKeyHint(`${keyText("app.clear")} twice`, "to exit"),
|
|
716
760
|
hint("app.exit", "to exit (empty)"),
|
|
717
761
|
hint("app.suspend", "to suspend"),
|
|
718
762
|
keyHint("tui.editor.deleteToLineEnd", "to delete to end"),
|
|
719
|
-
hint("app.thinking.cycle", "to cycle thinking level"),
|
|
720
|
-
rawKeyHint(`${keyText("app.model.cycleForward")}/${keyText("app.model.cycleBackward")}`, "to cycle models"),
|
|
721
|
-
hint("app.model.select", "to select model"),
|
|
722
|
-
hint("app.tools.expand", "to jump to full output and back"),
|
|
723
|
-
hint("app.view.cycleForward", "to cycle tool output (radar/peek/full)"),
|
|
724
|
-
hint("app.thinking.toggle", "to expand thinking"),
|
|
725
|
-
hint("app.tasks.cycleView", "to cycle task panel view"),
|
|
726
|
-
...(this.teamFocus.connected ? [hint("app.team.focus", "to focus team roster")] : []),
|
|
727
|
-
hint("app.mode.cycle", "to cycle agent mode"),
|
|
728
|
-
hint("app.session.changeDirectory", "to change working directory"),
|
|
729
|
-
hint("app.settings.open", "for settings"),
|
|
730
|
-
hint("app.hotkeys.open", "for all shortcuts"),
|
|
731
|
-
hint("app.editor.external", "for external editor"),
|
|
732
|
-
rawKeyHint("/", "for commands"),
|
|
733
|
-
rawKeyHint("!", "to run bash"),
|
|
734
|
-
rawKeyHint("!!", "to run bash (no context)"),
|
|
735
|
-
hint("app.message.followUp", "to queue follow-up"),
|
|
736
|
-
hint("app.message.dequeue", "to edit all queued messages"),
|
|
737
|
-
hint("app.clipboard.pasteImage", "to paste image"),
|
|
738
|
-
rawKeyHint("drop files", "to attach"),
|
|
739
763
|
].join("\n");
|
|
740
764
|
const onboarding = theme.fg("dim", `${APP_NAME} can explain its own features and look up its docs. Ask it how to use or extend ${APP_NAME}.`);
|
|
741
765
|
this.builtInHeader = new ExpandableText(() => logo(), () => `${logo()}\n${expandedInstructions}\n\n${onboarding}`, this.getStartupExpansionState(), 1, 0);
|
|
@@ -767,7 +791,7 @@ export class InteractiveMode {
|
|
|
767
791
|
setTerminalOwnedByTui(true);
|
|
768
792
|
this.isInitialized = true;
|
|
769
793
|
// Initialize extensions first so resources are shown before messages
|
|
770
|
-
await this.rebindCurrentSession();
|
|
794
|
+
await this.rebindCurrentSession({ renderResources: true });
|
|
771
795
|
// Render initial messages AFTER showing loaded resources
|
|
772
796
|
this.renderInitialMessages();
|
|
773
797
|
// Set up theme file watcher
|
|
@@ -1057,26 +1081,28 @@ export class InteractiveMode {
|
|
|
1057
1081
|
this.showExtensionError(error.extensionPath, error.error, error.stack);
|
|
1058
1082
|
},
|
|
1059
1083
|
});
|
|
1060
|
-
setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
|
|
1061
1084
|
this.setupAutocompleteProvider();
|
|
1062
|
-
|
|
1063
|
-
this.setupExtensionShortcuts(extensionRunner);
|
|
1064
|
-
// The startup path draws the listing here, because it renders messages
|
|
1065
|
-
// without clearing. A *rebind* (/new, /resume, /fork) clears the transcript
|
|
1066
|
-
// straight afterwards, so `renderCurrentSessionState` draws it again — this
|
|
1067
|
-
// call is the one startup keeps.
|
|
1068
|
-
this.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });
|
|
1069
|
-
this.showStartupNoticesIfNeeded();
|
|
1085
|
+
this.setupExtensionShortcuts(this.session.extensionRunner);
|
|
1070
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
|
+
*/
|
|
1071
1098
|
applyRuntimeSettings() {
|
|
1099
|
+
// Keybindings first: what follows is drawn with the hints they name.
|
|
1100
|
+
this.keybindings.reload();
|
|
1072
1101
|
this.footer.setSession(this.session);
|
|
1073
1102
|
this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
|
|
1074
1103
|
this.footer.setToolOutputView(this.toolOutputView);
|
|
1075
|
-
// The startup banner names the cwd, which `/cd` changes under it.
|
|
1076
|
-
if (this.builtInHeader instanceof ExpandableText) {
|
|
1077
|
-
this.builtInHeader.refresh();
|
|
1078
|
-
}
|
|
1079
1104
|
this.footerDataProvider.setCwd(this.sessionManager.getCwd());
|
|
1105
|
+
this.footerDataProvider.setSubagentEnabled(this.session.getActiveToolNames().includes(TASK_TOOL_NAME));
|
|
1080
1106
|
this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
|
|
1081
1107
|
this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
|
|
1082
1108
|
this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
|
|
@@ -1092,15 +1118,63 @@ export class InteractiveMode {
|
|
|
1092
1118
|
this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
|
|
1093
1119
|
}
|
|
1094
1120
|
}
|
|
1095
|
-
|
|
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) {
|
|
1096
1167
|
this.unsubscribe?.();
|
|
1097
1168
|
this.unsubscribe = undefined;
|
|
1098
1169
|
this.applyRuntimeSettings();
|
|
1099
1170
|
await this.bindCurrentSessionExtensions();
|
|
1171
|
+
this.applySessionTheme();
|
|
1172
|
+
if (options?.renderResources) {
|
|
1173
|
+
this.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });
|
|
1174
|
+
this.showStartupNoticesIfNeeded();
|
|
1175
|
+
}
|
|
1100
1176
|
this.subscribeToAgent();
|
|
1101
|
-
await this.
|
|
1102
|
-
this.updateEditorBorderColor();
|
|
1103
|
-
this.refreshSessionIdentity();
|
|
1177
|
+
await this.finishRuntimeSettings();
|
|
1104
1178
|
}
|
|
1105
1179
|
async handleFatalRuntimeError(prefix, error) {
|
|
1106
1180
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1110,26 +1184,35 @@ export class InteractiveMode {
|
|
|
1110
1184
|
process.exit(1);
|
|
1111
1185
|
}
|
|
1112
1186
|
/**
|
|
1113
|
-
*
|
|
1114
|
-
*
|
|
1115
|
-
*
|
|
1116
|
-
*
|
|
1117
|
-
*
|
|
1118
|
-
* drawn, which is why /reload showed skills, agents and plugins and starting a
|
|
1119
|
-
* new session showed nothing. Re-rendering it here is what makes the surface
|
|
1120
|
-
* common: one call site, so a session change of any kind reports the same
|
|
1121
|
-
* 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.
|
|
1122
1192
|
*/
|
|
1123
|
-
|
|
1193
|
+
resetTranscriptView() {
|
|
1124
1194
|
this.chatContainer.clear();
|
|
1125
1195
|
this.openChain = undefined;
|
|
1126
1196
|
this.latestToolBlock = undefined;
|
|
1127
1197
|
this.latestChain = undefined;
|
|
1128
|
-
this.pendingMessagesContainer.clear();
|
|
1129
|
-
this.messageQueue.resetCompactionQueue();
|
|
1130
1198
|
this.streamingComponent = undefined;
|
|
1131
1199
|
this.streamingMessage = undefined;
|
|
1132
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();
|
|
1133
1216
|
this.showLoadedResources({ force: false, showDiagnosticsWhenQuiet: true });
|
|
1134
1217
|
this.renderInitialMessages();
|
|
1135
1218
|
}
|
|
@@ -1510,7 +1593,8 @@ export class InteractiveMode {
|
|
|
1510
1593
|
this.defaultEditor.onAction("app.clear", () => this.handleCtrlC());
|
|
1511
1594
|
this.defaultEditor.onCtrlD = () => this.handleCtrlD();
|
|
1512
1595
|
this.defaultEditor.onAction("app.suspend", () => this.handleCtrlZ());
|
|
1513
|
-
this.defaultEditor.onAction("app.thinking.
|
|
1596
|
+
this.defaultEditor.onAction("app.thinking.cycleForward", () => this.cycleThinkingLevel("forward"));
|
|
1597
|
+
this.defaultEditor.onAction("app.thinking.cycleBackward", () => this.cycleThinkingLevel("backward"));
|
|
1514
1598
|
this.defaultEditor.onAction("app.model.cycleForward", () => this.modelController.cycleModel("forward"));
|
|
1515
1599
|
this.defaultEditor.onAction("app.model.cycleBackward", () => this.modelController.cycleModel("backward"));
|
|
1516
1600
|
// Global debug handler on TUI (works regardless of focus)
|
|
@@ -1520,8 +1604,11 @@ export class InteractiveMode {
|
|
|
1520
1604
|
this.defaultEditor.onAction("app.view.cycleForward", () => this.cycleToolOutputView("forward"));
|
|
1521
1605
|
this.defaultEditor.onAction("app.view.cycleBackward", () => this.cycleToolOutputView("backward"));
|
|
1522
1606
|
this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
|
|
1523
|
-
this.defaultEditor.onAction("app.tasks.
|
|
1524
|
-
this.taskPanel.cycleView();
|
|
1607
|
+
this.defaultEditor.onAction("app.tasks.cycleForward", () => {
|
|
1608
|
+
this.taskPanel.cycleView("forward");
|
|
1609
|
+
});
|
|
1610
|
+
this.defaultEditor.onAction("app.tasks.cycleBackward", () => {
|
|
1611
|
+
this.taskPanel.cycleView("backward");
|
|
1525
1612
|
});
|
|
1526
1613
|
this.defaultEditor.onAction("app.team.focus", () => this.teamFocus.enterFocus());
|
|
1527
1614
|
this.defaultEditor.onAction("app.editor.external", () => this.openExternalEditor());
|
|
@@ -1542,7 +1629,8 @@ export class InteractiveMode {
|
|
|
1542
1629
|
this.defaultEditor.onAction("app.session.color.cycleBackward", () => this.cycleSessionColor("backward"));
|
|
1543
1630
|
this.defaultEditor.onAction("app.settings.open", () => this.showSettingsSelector());
|
|
1544
1631
|
this.defaultEditor.onAction("app.hotkeys.open", () => this.commandExecutor.handleHotkeys());
|
|
1545
|
-
this.defaultEditor.onAction("app.mode.
|
|
1632
|
+
this.defaultEditor.onAction("app.mode.cycleForward", () => void this.cycleAgentMode("forward"));
|
|
1633
|
+
this.defaultEditor.onAction("app.mode.cycleBackward", () => void this.cycleAgentMode("backward"));
|
|
1546
1634
|
this.defaultEditor.onChange = (text) => {
|
|
1547
1635
|
const wasBashMode = this.isBashMode;
|
|
1548
1636
|
this.isBashMode = text.trimStart().startsWith("!");
|
|
@@ -2267,6 +2355,28 @@ export class InteractiveMode {
|
|
|
2267
2355
|
* If multiple status messages are emitted back-to-back (without anything else being added to the chat),
|
|
2268
2356
|
* we update the previous status line instead of appending new ones to avoid log spam.
|
|
2269
2357
|
*/
|
|
2358
|
+
/**
|
|
2359
|
+
* Report a dial step, and name the key that steps it back — once.
|
|
2360
|
+
*
|
|
2361
|
+
* The instant after someone moves a dial is the one moment they are primed to
|
|
2362
|
+
* learn its other half: they have just used one direction and can feel the
|
|
2363
|
+
* missing one. So the reverse rides along with the first step of each dial in
|
|
2364
|
+
* a session and never again. A hint that repeats forever stops being read,
|
|
2365
|
+
* and its cost falls entirely on the people who already know it.
|
|
2366
|
+
*
|
|
2367
|
+
* Only the three dials that announce their new value in the transcript come
|
|
2368
|
+
* through here. The other three announce themselves where they live — the
|
|
2369
|
+
* task panel prints its own key in its header, and the mode chip and view
|
|
2370
|
+
* glyph change in place in the footer.
|
|
2371
|
+
*/
|
|
2372
|
+
showDialStep(backward, message) {
|
|
2373
|
+
if (this.dialReverseTaught.has(backward)) {
|
|
2374
|
+
this.showStatus(message);
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
this.dialReverseTaught.add(backward);
|
|
2378
|
+
this.showStatus(theme.fg("dim", message) + theme.fg("halftone", ` ${keyText(backward)} steps back`));
|
|
2379
|
+
}
|
|
2270
2380
|
showStatus(message) {
|
|
2271
2381
|
const children = this.chatContainer.children;
|
|
2272
2382
|
const last = children.length > 0 ? children[children.length - 1] : undefined;
|
|
@@ -2650,15 +2760,15 @@ export class InteractiveMode {
|
|
|
2650
2760
|
}
|
|
2651
2761
|
this.ui.requestRender();
|
|
2652
2762
|
}
|
|
2653
|
-
cycleThinkingLevel() {
|
|
2654
|
-
const newLevel = this.session.cycleThinkingLevel();
|
|
2763
|
+
cycleThinkingLevel(direction) {
|
|
2764
|
+
const newLevel = this.session.cycleThinkingLevel(direction);
|
|
2655
2765
|
if (newLevel === undefined) {
|
|
2656
2766
|
this.showStatus("Current model does not support thinking");
|
|
2657
2767
|
}
|
|
2658
2768
|
else {
|
|
2659
2769
|
this.footer.invalidate();
|
|
2660
2770
|
this.updateEditorBorderColor();
|
|
2661
|
-
this.
|
|
2771
|
+
this.showDialStep("app.thinking.cycleBackward", `Thinking level: ${newLevel}`);
|
|
2662
2772
|
}
|
|
2663
2773
|
}
|
|
2664
2774
|
/**
|
|
@@ -2834,7 +2944,7 @@ export class InteractiveMode {
|
|
|
2834
2944
|
* is missing (a `--light` run strips the extension), the key says so instead
|
|
2835
2945
|
* of guessing.
|
|
2836
2946
|
*/
|
|
2837
|
-
async cycleAgentMode() {
|
|
2947
|
+
async cycleAgentMode(direction) {
|
|
2838
2948
|
const command = this.session.extensionRunner.getCommand("mode");
|
|
2839
2949
|
if (!command) {
|
|
2840
2950
|
this.showWarning("Modes are not available in this session");
|
|
@@ -2847,8 +2957,12 @@ export class InteractiveMode {
|
|
|
2847
2957
|
return;
|
|
2848
2958
|
}
|
|
2849
2959
|
const current = this.footerDataProvider.getActiveMode();
|
|
2960
|
+
// indexOf is -1 when the active mode is not in the list; stepping from
|
|
2961
|
+
// there lands on the first mode going forward and the last going back,
|
|
2962
|
+
// which is the useful answer either way.
|
|
2850
2963
|
const index = modes.indexOf(current);
|
|
2851
|
-
const
|
|
2964
|
+
const step = direction === "forward" ? 1 : -1;
|
|
2965
|
+
const next = modes[(index + step + modes.length) % modes.length];
|
|
2852
2966
|
await this.session.prompt(`/mode ${next}`);
|
|
2853
2967
|
}
|
|
2854
2968
|
/**
|
|
@@ -2865,7 +2979,7 @@ export class InteractiveMode {
|
|
|
2865
2979
|
const slot = cycleSessionColorSlot(this.sessionManager.getSessionColorSlot(), direction);
|
|
2866
2980
|
this.session.setSessionColor(slot);
|
|
2867
2981
|
const name = sessionColorName(slot);
|
|
2868
|
-
this.
|
|
2982
|
+
this.showDialStep("app.session.color.cycleBackward", `Session color: ${name ?? slot}`);
|
|
2869
2983
|
}
|
|
2870
2984
|
toggleThinkingBlockVisibility() {
|
|
2871
2985
|
this.hideThinkingBlock = !this.hideThinkingBlock;
|
|
@@ -3635,40 +3749,25 @@ export class InteractiveMode {
|
|
|
3635
3749
|
};
|
|
3636
3750
|
try {
|
|
3637
3751
|
await this.session.reload();
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
}
|
|
3643
|
-
setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
|
|
3644
|
-
this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
|
|
3645
|
-
const themeName = this.settingsManager.getTheme();
|
|
3646
|
-
const themeResult = themeName ? setTheme(themeName, true) : { success: true };
|
|
3647
|
-
if (!themeResult.success) {
|
|
3648
|
-
this.showError(`Failed to load theme "${themeName}": ${themeResult.error}\nFell back to dark theme.`);
|
|
3649
|
-
}
|
|
3650
|
-
const editorBorder = this.settingsManager.getEditorBorder();
|
|
3651
|
-
const editorPaddingX = this.settingsManager.getEditorPaddingX();
|
|
3652
|
-
const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
|
|
3653
|
-
this.defaultEditor.setBorder(editorBorder);
|
|
3654
|
-
this.defaultEditor.setPaddingX(editorPaddingX);
|
|
3655
|
-
this.defaultEditor.setAutocompleteMaxVisible(autocompleteMaxVisible);
|
|
3656
|
-
if (this.editor !== this.defaultEditor) {
|
|
3657
|
-
this.editor.setBorder?.(editorBorder);
|
|
3658
|
-
this.editor.setPaddingX?.(editorPaddingX);
|
|
3659
|
-
this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
|
|
3660
|
-
}
|
|
3661
|
-
this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
|
|
3662
|
-
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();
|
|
3663
3756
|
this.setupAutocompleteProvider();
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
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());
|
|
3667
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.
|
|
3668
3766
|
this.showLoadedResources({
|
|
3669
3767
|
force: false,
|
|
3670
3768
|
showDiagnosticsWhenQuiet: true,
|
|
3671
3769
|
});
|
|
3770
|
+
await this.finishRuntimeSettings();
|
|
3672
3771
|
const modelsJsonError = this.session.modelRegistry.getError();
|
|
3673
3772
|
if (modelsJsonError) {
|
|
3674
3773
|
this.showError(`models.json error: ${modelsJsonError}`);
|