@kolisachint/hoocode-agent 0.5.31 → 0.5.32
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 +131 -0
- package/dist/core/agent-session-runtime.d.ts +28 -0
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +61 -2
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/keybindings.d.ts +102 -30
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +123 -29
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/settings-defaults.d.ts +1 -1
- package/dist/core/settings-defaults.d.ts.map +1 -1
- package/dist/core/settings-defaults.js +1 -1
- package/dist/core/settings-defaults.js.map +1 -1
- package/dist/core/settings-manager.d.ts +10 -3
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +19 -8
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/settings-types.d.ts +1 -1
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +5 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/tool-output-view.d.ts +33 -0
- package/dist/core/tool-output-view.d.ts.map +1 -0
- package/dist/core/tool-output-view.js +31 -0
- package/dist/core/tool-output-view.js.map +1 -0
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +4 -0
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/modes/interactive/command-executor.d.ts +20 -0
- package/dist/modes/interactive/command-executor.d.ts.map +1 -1
- package/dist/modes/interactive/command-executor.js +138 -9
- package/dist/modes/interactive/command-executor.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +4 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +31 -2
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts +2 -2
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +2 -2
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +2 -2
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +3 -2
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +13 -12
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-chain-summary.d.ts +65 -0
- package/dist/modes/interactive/components/tool-chain-summary.d.ts.map +1 -0
- package/dist/modes/interactive/components/tool-chain-summary.js +216 -0
- package/dist/modes/interactive/components/tool-chain-summary.js.map +1 -0
- package/dist/modes/interactive/components/tool-chain.d.ts +61 -0
- package/dist/modes/interactive/components/tool-chain.d.ts.map +1 -0
- package/dist/modes/interactive/components/tool-chain.js +156 -0
- package/dist/modes/interactive/components/tool-chain.js.map +1 -0
- package/dist/modes/interactive/components/tool-execution.d.ts +55 -9
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +173 -33
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tool-signal.d.ts +50 -0
- package/dist/modes/interactive/components/tool-signal.d.ts.map +1 -0
- package/dist/modes/interactive/components/tool-signal.js +151 -0
- package/dist/modes/interactive/components/tool-signal.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +50 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +239 -29
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/resource-display.d.ts +9 -0
- package/dist/modes/interactive/resource-display.d.ts.map +1 -1
- package/dist/modes/interactive/resource-display.js +13 -0
- package/dist/modes/interactive/resource-display.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
|
@@ -29,6 +29,7 @@ import { SessionManager } from "../../core/session-manager.js";
|
|
|
29
29
|
import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
|
|
30
30
|
import { startupProgress } from "../../core/startup-progress.js";
|
|
31
31
|
import { taskStore } from "../../core/task-store.js";
|
|
32
|
+
import { cycleToolOutputView } from "../../core/tool-output-view.js";
|
|
32
33
|
import { settleDanglingMainTasks } from "../../core/tools/todo.js";
|
|
33
34
|
import { buildCompactWordmark } from "../../core/wordmark.js";
|
|
34
35
|
import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
|
|
@@ -53,6 +54,7 @@ import { SessionSelectorComponent } from "./components/session-selector.js";
|
|
|
53
54
|
import { SettingsSelectorComponent } from "./components/settings-selector.js";
|
|
54
55
|
import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.js";
|
|
55
56
|
import { TaskPanelComponent } from "./components/task-panel.js";
|
|
57
|
+
import { ToolChainComponent } from "./components/tool-chain.js";
|
|
56
58
|
import { ToolExecutionComponent } from "./components/tool-execution.js";
|
|
57
59
|
import { TreeSelectorComponent } from "./components/tree-selector.js";
|
|
58
60
|
import { UserMessageComponent } from "./components/user-message.js";
|
|
@@ -197,8 +199,13 @@ export class InteractiveMode {
|
|
|
197
199
|
pendingTools = new Map();
|
|
198
200
|
// Tool output expansion state
|
|
199
201
|
toolOutputExpanded = false;
|
|
200
|
-
// Persisted
|
|
201
|
-
|
|
202
|
+
// Persisted view dial (radar / glance / full). See core/tool-output-view.ts.
|
|
203
|
+
toolOutputView = "glance";
|
|
204
|
+
// The chain currently collecting tool calls, if the agent is mid-run.
|
|
205
|
+
openChain;
|
|
206
|
+
// Whether the assistant message being streamed has said anything yet. The
|
|
207
|
+
// first words it speaks close the chain the previous message's calls built.
|
|
208
|
+
sawTextInCurrentMessage = false;
|
|
202
209
|
// Thinking block visibility state
|
|
203
210
|
hideThinkingBlock = false;
|
|
204
211
|
// Skill commands: command name -> skill file path
|
|
@@ -372,6 +379,7 @@ export class InteractiveMode {
|
|
|
372
379
|
this.footerDataProvider = new FooterDataProvider(this.sessionManager.getCwd());
|
|
373
380
|
this.footer = new FooterComponent(this.session, this.footerDataProvider);
|
|
374
381
|
this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
|
|
382
|
+
this.footer.setToolOutputView(this.toolOutputView);
|
|
375
383
|
this.footerDataProvider.setSubagentEnabled(this.session.getActiveToolNames().includes("Task"));
|
|
376
384
|
this.chrome = new ExtensionChrome({
|
|
377
385
|
ui: this.ui,
|
|
@@ -460,8 +468,8 @@ export class InteractiveMode {
|
|
|
460
468
|
this.taskPanel.onExitFocus = () => this.teamFocus.exitFocus();
|
|
461
469
|
// Load hide thinking block setting
|
|
462
470
|
this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
|
|
463
|
-
// Load tool-output
|
|
464
|
-
this.
|
|
471
|
+
// Load the tool-output view dial
|
|
472
|
+
this.toolOutputView = this.settingsManager.getToolOutputView();
|
|
465
473
|
// Completion chime: rings the terminal bell when a long turn finishes or the
|
|
466
474
|
// agent blocks awaiting input. Enable is read fresh so a live toggle applies.
|
|
467
475
|
this.chime = new CompletionChime({
|
|
@@ -543,6 +551,14 @@ export class InteractiveMode {
|
|
|
543
551
|
}));
|
|
544
552
|
};
|
|
545
553
|
}
|
|
554
|
+
const cdCommand = slashCommands.find((command) => command.name === "cd");
|
|
555
|
+
if (cdCommand) {
|
|
556
|
+
cdCommand.argumentHint = "<path>";
|
|
557
|
+
cdCommand.getArgumentCompletions = (prefix) => {
|
|
558
|
+
const completions = this.commandExecutor.getChangeDirectoryCompletions(prefix);
|
|
559
|
+
return completions.length > 0 ? completions : null;
|
|
560
|
+
};
|
|
561
|
+
}
|
|
546
562
|
// Convert prompt templates to SlashCommand format for autocomplete
|
|
547
563
|
const templateCommands = this.session.promptTemplates.map((cmd) => ({
|
|
548
564
|
name: cmd.name,
|
|
@@ -658,7 +674,9 @@ export class InteractiveMode {
|
|
|
658
674
|
// Add header with keybindings from config (unless silenced)
|
|
659
675
|
if (this.options.verbose || !this.settingsManager.getQuietStartup()) {
|
|
660
676
|
const termW = this.ui.terminal.columns;
|
|
661
|
-
|
|
677
|
+
// A function, not a value: the banner names the working directory, and
|
|
678
|
+
// `/cd` moves it. `refreshBuiltInHeader` re-reads this after a move.
|
|
679
|
+
const logo = () => termW >= 40
|
|
662
680
|
? buildCompactWordmark({
|
|
663
681
|
appName: APP_NAME,
|
|
664
682
|
version: this.version,
|
|
@@ -682,10 +700,16 @@ export class InteractiveMode {
|
|
|
682
700
|
hint("app.thinking.cycle", "to cycle thinking level"),
|
|
683
701
|
rawKeyHint(`${keyText("app.model.cycleForward")}/${keyText("app.model.cycleBackward")}`, "to cycle models"),
|
|
684
702
|
hint("app.model.select", "to select model"),
|
|
685
|
-
hint("app.tools.expand", "to expand
|
|
703
|
+
hint("app.tools.expand", "to expand all tool output"),
|
|
704
|
+
hint("app.tools.unfoldOne", "to open one chain or block (repeat to peel back)"),
|
|
705
|
+
hint("app.view.cycleForward", "to cycle tool output (radar/glance/full)"),
|
|
686
706
|
hint("app.thinking.toggle", "to expand thinking"),
|
|
687
707
|
hint("app.tasks.cycleView", "to cycle task panel view"),
|
|
688
708
|
...(this.teamFocus.connected ? [hint("app.team.focus", "to focus team roster")] : []),
|
|
709
|
+
hint("app.mode.cycle", "to cycle agent mode"),
|
|
710
|
+
hint("app.session.changeDirectory", "to change working directory"),
|
|
711
|
+
hint("app.settings.open", "for settings"),
|
|
712
|
+
hint("app.hotkeys.open", "for all shortcuts"),
|
|
689
713
|
hint("app.editor.external", "for external editor"),
|
|
690
714
|
rawKeyHint("/", "for commands"),
|
|
691
715
|
rawKeyHint("!", "to run bash"),
|
|
@@ -696,7 +720,7 @@ export class InteractiveMode {
|
|
|
696
720
|
rawKeyHint("drop files", "to attach"),
|
|
697
721
|
].join("\n");
|
|
698
722
|
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}.`);
|
|
699
|
-
this.builtInHeader = new ExpandableText(() => logo, () => `${logo}\n${expandedInstructions}\n\n${onboarding}`, this.getStartupExpansionState(), 1, 0);
|
|
723
|
+
this.builtInHeader = new ExpandableText(() => logo(), () => `${logo()}\n${expandedInstructions}\n\n${onboarding}`, this.getStartupExpansionState(), 1, 0);
|
|
700
724
|
// Setup UI layout
|
|
701
725
|
this.headerContainer.addChild(this.builtInHeader);
|
|
702
726
|
}
|
|
@@ -1002,6 +1026,11 @@ export class InteractiveMode {
|
|
|
1002
1026
|
applyRuntimeSettings() {
|
|
1003
1027
|
this.footer.setSession(this.session);
|
|
1004
1028
|
this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
|
|
1029
|
+
this.footer.setToolOutputView(this.toolOutputView);
|
|
1030
|
+
// The startup banner names the cwd, which `/cd` changes under it.
|
|
1031
|
+
if (this.builtInHeader instanceof ExpandableText) {
|
|
1032
|
+
this.builtInHeader.refresh();
|
|
1033
|
+
}
|
|
1005
1034
|
this.footerDataProvider.setCwd(this.sessionManager.getCwd());
|
|
1006
1035
|
this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
|
|
1007
1036
|
this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
|
|
@@ -1048,6 +1077,7 @@ export class InteractiveMode {
|
|
|
1048
1077
|
*/
|
|
1049
1078
|
renderCurrentSessionState() {
|
|
1050
1079
|
this.chatContainer.clear();
|
|
1080
|
+
this.openChain = undefined;
|
|
1051
1081
|
this.pendingMessagesContainer.clear();
|
|
1052
1082
|
this.messageQueue.resetCompactionQueue();
|
|
1053
1083
|
this.streamingComponent = undefined;
|
|
@@ -1064,12 +1094,7 @@ export class InteractiveMode {
|
|
|
1064
1094
|
* later full rebuild (theme toggle / reload) restores full fidelity.
|
|
1065
1095
|
*/
|
|
1066
1096
|
trimTranscriptMemory() {
|
|
1067
|
-
const freezable =
|
|
1068
|
-
for (const child of this.chatContainer.children) {
|
|
1069
|
-
if (child instanceof ToolExecutionComponent && child.isFreezable()) {
|
|
1070
|
-
freezable.push(child);
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1097
|
+
const freezable = this.transcriptToolBlocks().filter((block) => block.isFreezable());
|
|
1073
1098
|
const excess = freezable.length - LIVE_TOOL_WINDOW;
|
|
1074
1099
|
for (let i = 0; i < excess; i++) {
|
|
1075
1100
|
freezable[i].freeze();
|
|
@@ -1444,6 +1469,10 @@ export class InteractiveMode {
|
|
|
1444
1469
|
this.ui.onDebug = () => this.commandExecutor.handleDebug();
|
|
1445
1470
|
this.defaultEditor.onAction("app.model.select", () => this.modelController.showModelSelector());
|
|
1446
1471
|
this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
|
|
1472
|
+
this.defaultEditor.onAction("app.view.cycleForward", () => this.cycleToolOutputView("forward"));
|
|
1473
|
+
this.defaultEditor.onAction("app.view.cycleBackward", () => this.cycleToolOutputView("backward"));
|
|
1474
|
+
this.defaultEditor.onAction("app.tools.unfoldOne", () => this.stepToolOutput("unfold"));
|
|
1475
|
+
this.defaultEditor.onAction("app.tools.foldOne", () => this.stepToolOutput("fold"));
|
|
1447
1476
|
this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
|
|
1448
1477
|
this.defaultEditor.onAction("app.tasks.cycleView", () => {
|
|
1449
1478
|
this.taskPanel.cycleView();
|
|
@@ -1457,6 +1486,15 @@ export class InteractiveMode {
|
|
|
1457
1486
|
this.defaultEditor.onAction("app.session.tree", () => this.showTreeSelector());
|
|
1458
1487
|
this.defaultEditor.onAction("app.session.fork", () => this.showUserMessageSelector());
|
|
1459
1488
|
this.defaultEditor.onAction("app.session.resume", () => this.showSessionSelector());
|
|
1489
|
+
this.defaultEditor.onAction("app.session.changeDirectory", () => {
|
|
1490
|
+
// Prefill rather than act: a directory change needs a target, and the
|
|
1491
|
+
// editor's own path completion is already the best way to name one.
|
|
1492
|
+
this.editor.setText("/cd ");
|
|
1493
|
+
this.ui.requestRender();
|
|
1494
|
+
});
|
|
1495
|
+
this.defaultEditor.onAction("app.settings.open", () => this.showSettingsSelector());
|
|
1496
|
+
this.defaultEditor.onAction("app.hotkeys.open", () => this.commandExecutor.handleHotkeys());
|
|
1497
|
+
this.defaultEditor.onAction("app.mode.cycle", () => void this.cycleAgentMode());
|
|
1460
1498
|
this.defaultEditor.onChange = (text) => {
|
|
1461
1499
|
const wasBashMode = this.isBashMode;
|
|
1462
1500
|
this.isBashMode = text.trimStart().startsWith("!");
|
|
@@ -1645,6 +1683,13 @@ export class InteractiveMode {
|
|
|
1645
1683
|
clearEditor();
|
|
1646
1684
|
},
|
|
1647
1685
|
},
|
|
1686
|
+
"/cd": {
|
|
1687
|
+
withArgs: true,
|
|
1688
|
+
run: async (text) => {
|
|
1689
|
+
clearEditor();
|
|
1690
|
+
await this.commandExecutor.handleChangeDirectory(text);
|
|
1691
|
+
},
|
|
1692
|
+
},
|
|
1648
1693
|
"/quit": {
|
|
1649
1694
|
run: async () => {
|
|
1650
1695
|
clearEditor();
|
|
@@ -1743,6 +1788,10 @@ export class InteractiveMode {
|
|
|
1743
1788
|
if (this.chimePendingRetry || this.session.isStreaming || this.session.isCompacting) {
|
|
1744
1789
|
return;
|
|
1745
1790
|
}
|
|
1791
|
+
// The last chain of a turn has nothing after it to close it, so it
|
|
1792
|
+
// settles here. A turn that did not end cleanly leaves its chain in the
|
|
1793
|
+
// running rendering plus a marker rather than claiming an outcome.
|
|
1794
|
+
this.closeOpenChain(this.turnStopReason === "stop" ? "done" : "interrupted");
|
|
1746
1795
|
this.showTurnCost();
|
|
1747
1796
|
this.settleDanglingPlanItems();
|
|
1748
1797
|
this.chime?.onTurnComplete({ aborted: this.turnStopReason === "aborted" });
|
|
@@ -1894,6 +1943,7 @@ export class InteractiveMode {
|
|
|
1894
1943
|
else if (event.message.role === "assistant") {
|
|
1895
1944
|
this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel);
|
|
1896
1945
|
this.streamingMessage = event.message;
|
|
1946
|
+
this.sawTextInCurrentMessage = false;
|
|
1897
1947
|
this.chatContainer.addChild(this.streamingComponent);
|
|
1898
1948
|
this.streamingComponent.updateContent(this.streamingMessage);
|
|
1899
1949
|
this.ui.requestRender();
|
|
@@ -1903,16 +1953,25 @@ export class InteractiveMode {
|
|
|
1903
1953
|
if (this.streamingComponent && event.message.role === "assistant") {
|
|
1904
1954
|
this.streamingMessage = event.message;
|
|
1905
1955
|
this.scheduleStreamingRender();
|
|
1956
|
+
// The agent speaking ends the run its previous calls formed. Tool
|
|
1957
|
+
// calls later in this same message open a fresh chain.
|
|
1958
|
+
if (!this.sawTextInCurrentMessage) {
|
|
1959
|
+
const spoke = this.streamingMessage.content.some((content) => content.type === "text" && content.text.trim() !== "");
|
|
1960
|
+
if (spoke) {
|
|
1961
|
+
this.sawTextInCurrentMessage = true;
|
|
1962
|
+
this.closeOpenChain("done");
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1906
1965
|
for (const content of this.streamingMessage.content) {
|
|
1907
1966
|
if (content.type === "toolCall") {
|
|
1908
1967
|
if (!this.pendingTools.has(content.id)) {
|
|
1909
1968
|
const component = new ToolExecutionComponent(content.name, content.id, content.arguments, {
|
|
1910
1969
|
showImages: this.settingsManager.getShowImages(),
|
|
1911
1970
|
imageWidthCells: this.settingsManager.getImageWidthCells(),
|
|
1912
|
-
|
|
1971
|
+
view: this.toolOutputView,
|
|
1913
1972
|
}, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
|
|
1914
1973
|
component.setExpanded(this.toolOutputExpanded);
|
|
1915
|
-
this.
|
|
1974
|
+
this.attachToolBlock(component);
|
|
1916
1975
|
this.pendingTools.set(content.id, component);
|
|
1917
1976
|
}
|
|
1918
1977
|
else {
|
|
@@ -1976,10 +2035,10 @@ export class InteractiveMode {
|
|
|
1976
2035
|
component = new ToolExecutionComponent(event.toolName, event.toolCallId, event.args, {
|
|
1977
2036
|
showImages: this.settingsManager.getShowImages(),
|
|
1978
2037
|
imageWidthCells: this.settingsManager.getImageWidthCells(),
|
|
1979
|
-
|
|
2038
|
+
view: this.toolOutputView,
|
|
1980
2039
|
}, this.getRegisteredToolDefinition(event.toolName), this.ui, this.sessionManager.getCwd());
|
|
1981
2040
|
component.setExpanded(this.toolOutputExpanded);
|
|
1982
|
-
this.
|
|
2041
|
+
this.attachToolBlock(component);
|
|
1983
2042
|
this.pendingTools.set(event.toolCallId, component);
|
|
1984
2043
|
}
|
|
1985
2044
|
component.markExecutionStarted();
|
|
@@ -2256,9 +2315,18 @@ export class InteractiveMode {
|
|
|
2256
2315
|
this.footer.invalidate();
|
|
2257
2316
|
this.updateEditorBorderColor();
|
|
2258
2317
|
}
|
|
2318
|
+
let lastAssistantStopReason;
|
|
2259
2319
|
for (const message of sessionContext.messages) {
|
|
2260
2320
|
// Assistant messages need special handling for tool calls
|
|
2261
2321
|
if (message.role === "assistant") {
|
|
2322
|
+
// Same boundary the live path uses: the agent speaking ends the run
|
|
2323
|
+
// its previous calls formed. Rebuilding history has to reproduce it,
|
|
2324
|
+
// or a resumed session would show one chain where it lived through
|
|
2325
|
+
// several.
|
|
2326
|
+
const spoke = message.content.some((content) => content.type === "text" && content.text.trim() !== "");
|
|
2327
|
+
if (spoke)
|
|
2328
|
+
this.closeOpenChain("done");
|
|
2329
|
+
lastAssistantStopReason = message.stopReason;
|
|
2262
2330
|
this.addMessageToChat(message);
|
|
2263
2331
|
// Render tool call components
|
|
2264
2332
|
for (const content of message.content) {
|
|
@@ -2266,10 +2334,10 @@ export class InteractiveMode {
|
|
|
2266
2334
|
const component = new ToolExecutionComponent(content.name, content.id, content.arguments, {
|
|
2267
2335
|
showImages: this.settingsManager.getShowImages(),
|
|
2268
2336
|
imageWidthCells: this.settingsManager.getImageWidthCells(),
|
|
2269
|
-
|
|
2337
|
+
view: this.toolOutputView,
|
|
2270
2338
|
}, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
|
|
2271
2339
|
component.setExpanded(this.toolOutputExpanded);
|
|
2272
|
-
this.
|
|
2340
|
+
this.attachToolBlock(component);
|
|
2273
2341
|
if (message.stopReason === "aborted" || message.stopReason === "error") {
|
|
2274
2342
|
let errorMessage;
|
|
2275
2343
|
if (message.stopReason === "aborted") {
|
|
@@ -2303,6 +2371,13 @@ export class InteractiveMode {
|
|
|
2303
2371
|
this.addMessageToChat(message, options);
|
|
2304
2372
|
}
|
|
2305
2373
|
}
|
|
2374
|
+
// History has no live state: a chain left open at the end of the rebuild is
|
|
2375
|
+
// finished, and how the last assistant message ended says whether it
|
|
2376
|
+
// finished cleanly. Tool calls still awaiting results are the exception —
|
|
2377
|
+
// this is a resumed run in flight, so its chain stays open for them.
|
|
2378
|
+
if (renderedPendingTools.size === 0) {
|
|
2379
|
+
this.closeOpenChain(lastAssistantStopReason === "stop" ? "done" : "interrupted");
|
|
2380
|
+
}
|
|
2306
2381
|
for (const [toolCallId, component] of renderedPendingTools) {
|
|
2307
2382
|
this.pendingTools.set(toolCallId, component);
|
|
2308
2383
|
}
|
|
@@ -2529,9 +2604,124 @@ export class InteractiveMode {
|
|
|
2529
2604
|
this.showStatus(`Thinking level: ${newLevel}`);
|
|
2530
2605
|
}
|
|
2531
2606
|
}
|
|
2607
|
+
/**
|
|
2608
|
+
* Put a tool block into the transcript, extending the open chain or starting
|
|
2609
|
+
* one. Every path that renders a tool call comes through here — live
|
|
2610
|
+
* streaming, execution start, and rebuilding history — so a chain cannot be
|
|
2611
|
+
* assembled correctly in one place and forgotten in another.
|
|
2612
|
+
*/
|
|
2613
|
+
attachToolBlock(block) {
|
|
2614
|
+
if (!this.openChain || !this.openChain.isOpen) {
|
|
2615
|
+
this.openChain = new ToolChainComponent(this.toolOutputView);
|
|
2616
|
+
this.chatContainer.addChild(this.openChain);
|
|
2617
|
+
}
|
|
2618
|
+
this.openChain.add(block);
|
|
2619
|
+
}
|
|
2620
|
+
/**
|
|
2621
|
+
* Settle the open chain.
|
|
2622
|
+
*
|
|
2623
|
+
* Called when the agent speaks (the run it was doing is over, whatever comes
|
|
2624
|
+
* next is a new one) and when the turn settles. Closing on the agent's next
|
|
2625
|
+
* words rather than only at turn end is what keeps the flip cheap: the line
|
|
2626
|
+
* is still at the bottom of the screen, so the TUI rewrites it in place
|
|
2627
|
+
* instead of taking the full-redraw path that clears terminal scrollback.
|
|
2628
|
+
*/
|
|
2629
|
+
closeOpenChain(outcome) {
|
|
2630
|
+
if (!this.openChain)
|
|
2631
|
+
return;
|
|
2632
|
+
if (this.openChain.isEmpty) {
|
|
2633
|
+
this.chatContainer.removeChild(this.openChain);
|
|
2634
|
+
}
|
|
2635
|
+
else {
|
|
2636
|
+
this.openChain.close(outcome);
|
|
2637
|
+
}
|
|
2638
|
+
this.openChain = undefined;
|
|
2639
|
+
this.ui.requestRender();
|
|
2640
|
+
}
|
|
2641
|
+
/** Every tool block in the transcript, in order, across all chains. */
|
|
2642
|
+
transcriptToolBlocks() {
|
|
2643
|
+
const blocks = [];
|
|
2644
|
+
for (const child of this.chatContainer.children) {
|
|
2645
|
+
if (child instanceof ToolChainComponent)
|
|
2646
|
+
blocks.push(...child.toolBlocks);
|
|
2647
|
+
else if (child instanceof ToolExecutionComponent)
|
|
2648
|
+
blocks.push(child);
|
|
2649
|
+
}
|
|
2650
|
+
return blocks;
|
|
2651
|
+
}
|
|
2652
|
+
/** Every chain in the transcript, in order. */
|
|
2653
|
+
transcriptChains() {
|
|
2654
|
+
return this.chatContainer.children.filter((child) => child instanceof ToolChainComponent);
|
|
2655
|
+
}
|
|
2532
2656
|
toggleToolOutputExpansion() {
|
|
2533
2657
|
this.setToolsExpanded(!this.toolOutputExpanded);
|
|
2534
2658
|
}
|
|
2659
|
+
/**
|
|
2660
|
+
* Open or close one tool block instead of all of them.
|
|
2661
|
+
*
|
|
2662
|
+
* `ctrl+o` is all-or-nothing, which left the ▸ caret on every glance and
|
|
2663
|
+
* radar row advertising something no key could do. This is the missing half:
|
|
2664
|
+
* unfold walks back from the newest block to the first one still folded,
|
|
2665
|
+
* fold walks back to the most recently opened one.
|
|
2666
|
+
*
|
|
2667
|
+
* Working from the tail is not a shortcut, it is the only thing this view can
|
|
2668
|
+
* honestly offer. The transcript is bottom-anchored and has no app-level
|
|
2669
|
+
* scrolling — anything far enough up is in the terminal's own scrollback,
|
|
2670
|
+
* where this process cannot put a cursor or scroll to a selection. So the
|
|
2671
|
+
* key peels backwards through what is actually on screen, and each block it
|
|
2672
|
+
* opens is its own marker for where you have got to.
|
|
2673
|
+
*/
|
|
2674
|
+
stepToolOutput(direction) {
|
|
2675
|
+
const wantOpen = direction === "unfold";
|
|
2676
|
+
// Radar's unit is the chain: one press turns the newest summary line back
|
|
2677
|
+
// into the calls it stands for. Its per-call bodies are a glance/full
|
|
2678
|
+
// question, so the step stops there rather than cascading.
|
|
2679
|
+
if (this.toolOutputView === "radar") {
|
|
2680
|
+
const chains = this.transcriptChains();
|
|
2681
|
+
for (let i = chains.length - 1; i >= 0; i--) {
|
|
2682
|
+
if (chains[i].isEmpty || chains[i].isOpened === wantOpen)
|
|
2683
|
+
continue;
|
|
2684
|
+
chains[i].setOpened(wantOpen);
|
|
2685
|
+
this.ui.requestRender();
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
this.showStatus(wantOpen ? "No collapsed chains below this point" : "No open chains below this point");
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
2691
|
+
const blocks = this.transcriptToolBlocks();
|
|
2692
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
2693
|
+
const block = blocks[i];
|
|
2694
|
+
if (block.isRevealed() === wantOpen)
|
|
2695
|
+
continue;
|
|
2696
|
+
block.setExpanded(wantOpen);
|
|
2697
|
+
this.ui.requestRender();
|
|
2698
|
+
return;
|
|
2699
|
+
}
|
|
2700
|
+
this.showStatus(wantOpen ? "No folded tool output below this point" : "No unfolded tool output below this point");
|
|
2701
|
+
}
|
|
2702
|
+
/**
|
|
2703
|
+
* Move the view dial one stop and report where it landed.
|
|
2704
|
+
*
|
|
2705
|
+
* The dial is the persistent decision ("how much do I ever want to see"),
|
|
2706
|
+
* which is why it saves; `app.tools.expand` stays the momentary one ("open
|
|
2707
|
+
* what is in front of me"), which is why it does not.
|
|
2708
|
+
*/
|
|
2709
|
+
cycleToolOutputView(direction) {
|
|
2710
|
+
const next = cycleToolOutputView(this.toolOutputView, direction);
|
|
2711
|
+
this.applyToolOutputView(next);
|
|
2712
|
+
}
|
|
2713
|
+
applyToolOutputView(view) {
|
|
2714
|
+
this.toolOutputView = view;
|
|
2715
|
+
this.settingsManager.setToolOutputView(view);
|
|
2716
|
+
this.footer.setToolOutputView(view);
|
|
2717
|
+
for (const child of this.chatContainer.children) {
|
|
2718
|
+
if (child instanceof ToolChainComponent)
|
|
2719
|
+
child.setView(view);
|
|
2720
|
+
else if (child instanceof ToolExecutionComponent)
|
|
2721
|
+
child.setView(view);
|
|
2722
|
+
}
|
|
2723
|
+
this.ui.requestRender();
|
|
2724
|
+
}
|
|
2535
2725
|
setToolsExpanded(expanded) {
|
|
2536
2726
|
this.toolOutputExpanded = expanded;
|
|
2537
2727
|
const activeHeader = this.chrome.customHeader ?? this.builtInHeader;
|
|
@@ -2545,6 +2735,33 @@ export class InteractiveMode {
|
|
|
2545
2735
|
}
|
|
2546
2736
|
this.ui.requestRender();
|
|
2547
2737
|
}
|
|
2738
|
+
/**
|
|
2739
|
+
* Step the agent mode one along (ask → plan → build → debug → ask).
|
|
2740
|
+
*
|
|
2741
|
+
* Mode is the product's central guardrail and sits first in the footer, yet
|
|
2742
|
+
* it was reachable only by typing `/mode <name>`. The list comes from the
|
|
2743
|
+
* mode command's own argument completions rather than a copy kept here, so
|
|
2744
|
+
* a project that adds a mode gets it in the rotation for free; if the command
|
|
2745
|
+
* is missing (a `--light` run strips the extension), the key says so instead
|
|
2746
|
+
* of guessing.
|
|
2747
|
+
*/
|
|
2748
|
+
async cycleAgentMode() {
|
|
2749
|
+
const command = this.session.extensionRunner.getCommand("mode");
|
|
2750
|
+
if (!command) {
|
|
2751
|
+
this.showWarning("Modes are not available in this session");
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
const completions = (await command.getArgumentCompletions?.("")) ?? [];
|
|
2755
|
+
const modes = completions.map((item) => item.value);
|
|
2756
|
+
if (modes.length === 0) {
|
|
2757
|
+
this.showWarning("No modes are configured");
|
|
2758
|
+
return;
|
|
2759
|
+
}
|
|
2760
|
+
const current = this.footerDataProvider.getActiveMode();
|
|
2761
|
+
const index = modes.indexOf(current);
|
|
2762
|
+
const next = modes[(index + 1) % modes.length];
|
|
2763
|
+
await this.session.prompt(`/mode ${next}`);
|
|
2764
|
+
}
|
|
2548
2765
|
toggleThinkingBlockVisibility() {
|
|
2549
2766
|
this.hideThinkingBlock = !this.hideThinkingBlock;
|
|
2550
2767
|
this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);
|
|
@@ -2748,7 +2965,7 @@ export class InteractiveMode {
|
|
|
2748
2965
|
// shows as present without a restart.
|
|
2749
2966
|
externalTools: describeExternalTools(),
|
|
2750
2967
|
flags,
|
|
2751
|
-
|
|
2968
|
+
toolOutputView: this.toolOutputView,
|
|
2752
2969
|
toolOutputMaxBytes: this.settingsManager.getToolOutputMaxBytes(),
|
|
2753
2970
|
toolOutputMaxLines: this.settingsManager.getToolOutputMaxLines(),
|
|
2754
2971
|
contextGc: this.settingsManager.getContextGcEnabled(),
|
|
@@ -2848,15 +3065,8 @@ export class InteractiveMode {
|
|
|
2848
3065
|
break;
|
|
2849
3066
|
}
|
|
2850
3067
|
},
|
|
2851
|
-
|
|
2852
|
-
this.
|
|
2853
|
-
this.settingsManager.setToolOutputDisplay(level);
|
|
2854
|
-
for (const child of this.chatContainer.children) {
|
|
2855
|
-
if (child instanceof ToolExecutionComponent) {
|
|
2856
|
-
child.setDisplayLevel(level);
|
|
2857
|
-
}
|
|
2858
|
-
}
|
|
2859
|
-
this.ui.requestRender();
|
|
3068
|
+
onToolOutputViewChange: (view) => {
|
|
3069
|
+
this.applyToolOutputView(view);
|
|
2860
3070
|
},
|
|
2861
3071
|
onToolOutputMaxBytesChange: (bytes) => {
|
|
2862
3072
|
this.settingsManager.setToolOutputMaxBytes(bytes);
|