feat(tui): add hidden tool activity mode
This commit is contained in:
@@ -29,6 +29,7 @@ interface AppKeybindings {
|
||||
"app.model.select": true;
|
||||
"app.model.selectTemporary": true;
|
||||
"app.tools.expand": true;
|
||||
"app.tools.toggleVisibility": true;
|
||||
"app.editor.external": true;
|
||||
"app.message.followUp": true;
|
||||
"app.retry": true;
|
||||
@@ -124,6 +125,10 @@ export const KEYBINDINGS = {
|
||||
defaultKeys: "ctrl+o",
|
||||
description: "Expand tools",
|
||||
},
|
||||
"app.tools.toggleVisibility": {
|
||||
defaultKeys: "ctrl+shift+o",
|
||||
description: "Show or hide tool activity",
|
||||
},
|
||||
"app.editor.external": {
|
||||
defaultKeys: "ctrl+g",
|
||||
description: "Open external editor",
|
||||
|
||||
@@ -1018,6 +1018,17 @@ export const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
|
||||
"display.hideToolActivity": {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
ui: {
|
||||
tab: "appearance",
|
||||
group: "Display",
|
||||
label: "Hide Tool Activity",
|
||||
description: "Hide model-initiated tool calls and results from the transcript",
|
||||
},
|
||||
},
|
||||
|
||||
"display.showTokenUsage": {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
|
||||
@@ -185,6 +185,7 @@ export class AssistantMessageComponent extends Container {
|
||||
#toolImagesByCallId = new Map<string, ImageContent[]>();
|
||||
#convertedKittyImages = new Map<string, ImageContent>();
|
||||
#showImages = true;
|
||||
#showToolResultImages = true;
|
||||
#kittyConversionsInFlight = new Set<string>();
|
||||
#transcriptBlockFinalized: boolean;
|
||||
/**
|
||||
@@ -578,6 +579,15 @@ export class AssistantMessageComponent extends Container {
|
||||
}
|
||||
}
|
||||
|
||||
/** Toggle only images produced by tool results; assistant-native images remain governed by setImagesVisible. */
|
||||
setToolResultImagesVisible(visible: boolean): void {
|
||||
if (this.#showToolResultImages === visible) return;
|
||||
this.#showToolResultImages = visible;
|
||||
if (this.#lastMessage) {
|
||||
this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
|
||||
}
|
||||
}
|
||||
|
||||
setToolResultImages(toolCallId: string, images: ImageContent[]): void {
|
||||
if (!toolCallId) return;
|
||||
const validImages = images.filter(img => img.type === "image" && img.data && img.mimeType);
|
||||
@@ -649,6 +659,7 @@ export class AssistantMessageComponent extends Container {
|
||||
}
|
||||
|
||||
#renderToolImages(): void {
|
||||
if (!this.#showToolResultImages) return;
|
||||
const entries = Array.from(this.#toolImagesByCallId.entries()).flatMap(([toolCallId, images]) =>
|
||||
images.map((image, index) => ({ image, key: `${toolCallId}:${index}` })),
|
||||
);
|
||||
|
||||
@@ -190,6 +190,7 @@ export class ChatTranscriptBuilder {
|
||||
this.#readGroup = new ReadToolGroupComponent({
|
||||
showContentPreview: settings.get("read.toolResultPreview"),
|
||||
});
|
||||
this.#readGroup.setToolActivityVisible(!settings.get("display.hideToolActivity"));
|
||||
this.#trackExpandable(this.#readGroup);
|
||||
this.container.addChild(this.#readGroup);
|
||||
}
|
||||
@@ -321,6 +322,7 @@ export class ChatTranscriptBuilder {
|
||||
proseOnlyThinking,
|
||||
);
|
||||
assistantComponent.setImagesVisible(settings.get("terminal.showImages"));
|
||||
assistantComponent.setToolResultImagesVisible(!settings.get("display.hideToolActivity"));
|
||||
this.#trackExpandable(assistantComponent);
|
||||
this.container.addChild(assistantComponent);
|
||||
|
||||
@@ -353,6 +355,7 @@ export class ChatTranscriptBuilder {
|
||||
proseOnlyThinking,
|
||||
);
|
||||
component.setImagesVisible(settings.get("terminal.showImages"));
|
||||
component.setToolResultImagesVisible(!settings.get("display.hideToolActivity"));
|
||||
this.#trackExpandable(component);
|
||||
this.container.addChild(component);
|
||||
};
|
||||
@@ -401,6 +404,7 @@ export class ChatTranscriptBuilder {
|
||||
this.deps.cwd,
|
||||
content.id,
|
||||
);
|
||||
component.setToolActivityVisible(!settings.get("display.hideToolActivity"));
|
||||
this.#trackExpandable(component);
|
||||
this.container.addChild(component);
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ type ConfigurableEditorAction = Extract<
|
||||
| "app.model.select"
|
||||
| "app.model.selectTemporary"
|
||||
| "app.tools.expand"
|
||||
| "app.tools.toggleVisibility"
|
||||
| "app.thinking.toggle"
|
||||
| "app.editor.external"
|
||||
| "app.history.search"
|
||||
@@ -53,6 +54,7 @@ const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
|
||||
"app.model.select": ["alt+m"],
|
||||
"app.model.selectTemporary": ["alt+p"],
|
||||
"app.tools.expand": ["ctrl+o"],
|
||||
"app.tools.toggleVisibility": ["ctrl+shift+o"],
|
||||
"app.thinking.toggle": ["ctrl+t"],
|
||||
"app.editor.external": ["ctrl+g"],
|
||||
"app.history.search": ["ctrl+r"],
|
||||
@@ -550,6 +552,7 @@ export class CustomEditor extends Editor {
|
||||
onCycleModelBackward?: () => void;
|
||||
onSelectModel?: () => void;
|
||||
onExpandTools?: () => void;
|
||||
onToggleToolActivity?: () => void;
|
||||
onToggleThinking?: () => void;
|
||||
onExternalEditor?: () => void;
|
||||
onHistorySearch?: () => void;
|
||||
@@ -910,6 +913,12 @@ export class CustomEditor extends Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept configured tool activity visibility toggle
|
||||
if (this.#matchesAction(canonical, "app.tools.toggleVisibility") && this.onToggleToolActivity) {
|
||||
this.onToggleToolActivity();
|
||||
return;
|
||||
}
|
||||
|
||||
// Intercept configured backward model cycling (check before forward cycling)
|
||||
if (this.#matchesAction(canonical, "app.model.cycleBackward") && this.onCycleModelBackward) {
|
||||
this.onCycleModelBackward();
|
||||
|
||||
@@ -332,6 +332,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
||||
#usageBatchByToolCallId = new Map<string, string>();
|
||||
#text: Text;
|
||||
#expanded = false;
|
||||
#toolActivityVisible = true;
|
||||
#showContentPreview: boolean;
|
||||
// A read group accretes entries across multiple assistant completions for as
|
||||
// long as the run of reads is uninterrupted. While it is the active group it
|
||||
@@ -353,6 +354,10 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
||||
this.#updateDisplay();
|
||||
}
|
||||
|
||||
override render(width: number): readonly string[] {
|
||||
if (!this.#toolActivityVisible) return [];
|
||||
return super.render(width);
|
||||
}
|
||||
isTranscriptBlockFinalized(): boolean {
|
||||
if (this.#sealed) return true;
|
||||
if (!this.#finalized) return false;
|
||||
@@ -496,6 +501,11 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
||||
this.#updateDisplay();
|
||||
}
|
||||
|
||||
setToolActivityVisible(visible: boolean): void {
|
||||
this.#toolActivityVisible = visible;
|
||||
super.invalidate();
|
||||
}
|
||||
|
||||
getComponent(): Component {
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -246,6 +246,7 @@ export interface ToolExecutionHandle extends Component {
|
||||
): void;
|
||||
setArgsComplete(toolCallId?: string): void;
|
||||
setExpanded(expanded: boolean): void;
|
||||
setToolActivityVisible(visible: boolean): void;
|
||||
/** Freeze the block as final history: stop spinners and let it commit to scrollback. */
|
||||
seal(): void;
|
||||
}
|
||||
@@ -287,6 +288,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
||||
#toolLabel: string;
|
||||
#args: any;
|
||||
#expanded = false;
|
||||
#toolActivityVisible = true;
|
||||
#showImages: boolean;
|
||||
#editFuzzyThreshold: number | undefined;
|
||||
#editAllowFuzzy: boolean | undefined;
|
||||
@@ -901,6 +903,11 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
||||
this.#updateDisplay();
|
||||
}
|
||||
|
||||
setToolActivityVisible(visible: boolean): void {
|
||||
this.#toolActivityVisible = visible;
|
||||
super.invalidate();
|
||||
}
|
||||
|
||||
setShowImages(show: boolean): void {
|
||||
this.#showImages = show;
|
||||
this.#updateDisplay();
|
||||
@@ -960,6 +967,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
||||
}
|
||||
|
||||
override render(width: number): readonly string[] {
|
||||
if (!this.#toolActivityVisible) return [];
|
||||
const lines = super.render(width);
|
||||
// Update the paint-tracking flags after `super.render(width)` — the
|
||||
// override runs on every compose the parent Container performs, so a
|
||||
|
||||
@@ -276,6 +276,7 @@ export class EventController {
|
||||
showContentPreview: this.ctx.settings.get("read.toolResultPreview"),
|
||||
});
|
||||
group.setExpanded(this.ctx.toolOutputExpanded);
|
||||
group.setToolActivityVisible(!this.ctx.hideToolActivity);
|
||||
this.ctx.chatContainer.addChild(group);
|
||||
this.#lastReadGroup = group;
|
||||
}
|
||||
@@ -927,6 +928,7 @@ export class EventController {
|
||||
content.id,
|
||||
);
|
||||
component.setExpanded(this.ctx.toolOutputExpanded);
|
||||
component.setToolActivityVisible(!this.ctx.hideToolActivity);
|
||||
this.ctx.chatContainer.addChild(component);
|
||||
this.ctx.pendingTools.set(content.id, component);
|
||||
this.#toolTimelineComponents.set(content.id, component);
|
||||
@@ -1176,6 +1178,7 @@ export class EventController {
|
||||
event.toolCallId,
|
||||
);
|
||||
component.setExpanded(this.ctx.toolOutputExpanded);
|
||||
component.setToolActivityVisible(!this.ctx.hideToolActivity);
|
||||
this.ctx.chatContainer.addChild(component);
|
||||
this.ctx.pendingTools.set(event.toolCallId, component);
|
||||
this.#toolTimelineComponents.set(event.toolCallId, component);
|
||||
|
||||
@@ -8,8 +8,10 @@ import { isSettingsInitialized, settings } from "../../config/settings";
|
||||
import { resolveLocalRoot } from "../../internal-urls";
|
||||
import { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
||||
import { extractImagePathFromText } from "../../modes/components/custom-editor";
|
||||
import { ReadToolGroupComponent } from "../../modes/components/read-tool-group";
|
||||
import { renderSegmentTrack } from "../../modes/components/segment-track";
|
||||
import { TinyTitleDownloadProgressComponent } from "../../modes/components/tiny-title-download-progress";
|
||||
import { ToolExecutionComponent } from "../../modes/components/tool-execution";
|
||||
import { expandEmoticons } from "../../modes/emoji-autocomplete";
|
||||
import { materializeImageReferenceLinks, shiftImageMarkers } from "../../modes/image-references";
|
||||
import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
|
||||
@@ -455,6 +457,11 @@ export class InputController {
|
||||
this.ctx.editor.onCopyPrompt = () => this.handleCopyPrompt();
|
||||
this.ctx.editor.setActionKeys("app.tools.expand", this.ctx.keybindings.getKeys("app.tools.expand"));
|
||||
this.ctx.editor.onExpandTools = () => this.toggleToolOutputExpansion();
|
||||
this.ctx.editor.setActionKeys(
|
||||
"app.tools.toggleVisibility",
|
||||
this.ctx.keybindings.getKeys("app.tools.toggleVisibility"),
|
||||
);
|
||||
this.ctx.editor.onToggleToolActivity = () => this.toggleToolActivityVisibility();
|
||||
this.ctx.editor.setActionKeys("app.message.dequeue", this.ctx.keybindings.getKeys("app.message.dequeue"));
|
||||
this.ctx.editor.onDequeue = () => this.handleDequeue();
|
||||
this.ctx.editor.setActionKeys("app.retry", this.ctx.keybindings.getKeys("app.retry"));
|
||||
@@ -1845,9 +1852,36 @@ export class InputController {
|
||||
}
|
||||
|
||||
toggleToolOutputExpansion(): void {
|
||||
if (this.ctx.hideToolActivity) {
|
||||
const visibilityKey = this.ctx.keybindings.getDisplayString("app.tools.toggleVisibility");
|
||||
const visibilityHint = visibilityKey ? `${visibilityKey} or /settings` : "/settings";
|
||||
this.ctx.showStatus(`Tool activity is hidden — show it with ${visibilityHint} before expanding`);
|
||||
return;
|
||||
}
|
||||
this.setToolsExpanded(!this.ctx.toolOutputExpanded);
|
||||
}
|
||||
|
||||
toggleToolActivityVisibility(): void {
|
||||
this.ctx.hideToolActivity = !this.ctx.hideToolActivity;
|
||||
this.ctx.settings.set("display.hideToolActivity", this.ctx.hideToolActivity);
|
||||
|
||||
if (!this.ctx.hideToolActivity) {
|
||||
this.ctx.toolOutputExpanded = false;
|
||||
}
|
||||
|
||||
for (const child of this.ctx.chatContainer.children) {
|
||||
if (child instanceof ToolExecutionComponent || child instanceof ReadToolGroupComponent) {
|
||||
if (!this.ctx.hideToolActivity) child.setExpanded(false);
|
||||
child.setToolActivityVisible(!this.ctx.hideToolActivity);
|
||||
} else if (child instanceof AssistantMessageComponent) {
|
||||
child.setToolResultImagesVisible(!this.ctx.hideToolActivity);
|
||||
}
|
||||
}
|
||||
|
||||
this.ctx.ui.resetDisplay();
|
||||
this.ctx.showStatus(`Tool activity: ${this.ctx.hideToolActivity ? "hidden" : "visible"}`);
|
||||
}
|
||||
|
||||
setToolsExpanded(expanded: boolean): void {
|
||||
this.ctx.toolOutputExpanded = expanded;
|
||||
for (const child of this.ctx.chatContainer.children) {
|
||||
|
||||
@@ -92,6 +92,7 @@ import { ModelHubComponent, type ModelRoleSelectionScope } from "../components/m
|
||||
import { ModelPickerComponent } from "../components/model-picker";
|
||||
import { OAuthSelectorComponent } from "../components/oauth-selector";
|
||||
import { PluginSelectorComponent } from "../components/plugin-selector";
|
||||
import { ReadToolGroupComponent } from "../components/read-tool-group";
|
||||
import { ResetUsageSelectorComponent } from "../components/reset-usage-selector";
|
||||
import { renderSegmentTrack } from "../components/segment-track";
|
||||
import { SessionAccountSelectorComponent } from "../components/session-account-selector";
|
||||
@@ -474,6 +475,21 @@ export class SelectorController {
|
||||
break;
|
||||
|
||||
// Settings with UI side effects
|
||||
case "display.hideToolActivity": {
|
||||
const hidden = value as boolean;
|
||||
this.ctx.hideToolActivity = hidden;
|
||||
if (!hidden) this.ctx.toolOutputExpanded = false;
|
||||
for (const child of this.ctx.chatContainer.children) {
|
||||
if (child instanceof ToolExecutionComponent || child instanceof ReadToolGroupComponent) {
|
||||
if (!hidden) child.setExpanded(false);
|
||||
child.setToolActivityVisible(!hidden);
|
||||
} else if (child instanceof AssistantMessageComponent) {
|
||||
child.setToolResultImagesVisible(!hidden);
|
||||
}
|
||||
}
|
||||
this.ctx.ui.resetDisplay();
|
||||
break;
|
||||
}
|
||||
case "terminal.showImages":
|
||||
case "showImages": {
|
||||
const visible = value as boolean;
|
||||
|
||||
@@ -460,6 +460,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
initialChatRendered = false;
|
||||
isBashMode = false;
|
||||
toolOutputExpanded = false;
|
||||
hideToolActivity = false;
|
||||
todoExpanded = false;
|
||||
planModeEnabled = false;
|
||||
planModePaused = false;
|
||||
@@ -778,6 +779,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
// hot path where the render never gets to paint the result.
|
||||
this.editor.setTopBorderProvider(availableWidth => this.statusLine.getTopBorder(availableWidth));
|
||||
|
||||
this.hideToolActivity = settings.get("display.hideToolActivity");
|
||||
this.hideThinkingBlock = settings.get("hideThinkingBlock");
|
||||
this.proseOnlyThinking = settings.get("proseOnlyThinking");
|
||||
|
||||
|
||||
@@ -162,6 +162,7 @@ export interface InteractiveModeContext {
|
||||
initialChatRendered: boolean;
|
||||
isBashMode: boolean;
|
||||
toolOutputExpanded: boolean;
|
||||
hideToolActivity: boolean;
|
||||
todoExpanded: boolean;
|
||||
planModeEnabled: boolean;
|
||||
vibeModeEnabled: boolean;
|
||||
|
||||
@@ -46,6 +46,7 @@ export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string
|
||||
`| \`${appKey(bindings, "app.plan.toggle")}\` | Toggle plan mode |`,
|
||||
`| \`${appKey(bindings, "app.history.search")}\` | Search prompt history |`,
|
||||
`| \`${appKey(bindings, "app.tools.expand")}\` | Toggle tool output expansion |`,
|
||||
`| \`${appKey(bindings, "app.tools.toggleVisibility")}\` | Toggle tool activity visibility |`,
|
||||
`| \`${appKey(bindings, "app.thinking.toggle")}\` | Toggle thinking block visibility |`,
|
||||
`| \`${appKey(bindings, "app.editor.external")}\` | Edit message in external editor |`,
|
||||
`| \`${appKey(bindings, "app.retry")}\` | Retry last failed assistant turn |`,
|
||||
|
||||
@@ -25,6 +25,7 @@ export function createAssistantMessageComponent(
|
||||
ctx.proseOnlyThinking,
|
||||
);
|
||||
component.setImagesVisible(ctx.settings.get("terminal.showImages"));
|
||||
component.setToolResultImagesVisible(!ctx.hideToolActivity);
|
||||
component.setExpanded(ctx.toolOutputExpanded);
|
||||
return component;
|
||||
}
|
||||
|
||||
@@ -444,6 +444,7 @@ export class UiHelpers {
|
||||
showContentPreview: this.ctx.settings.get("read.toolResultPreview"),
|
||||
});
|
||||
readGroup.setExpanded(this.ctx.toolOutputExpanded);
|
||||
readGroup.setToolActivityVisible(!this.ctx.hideToolActivity);
|
||||
this.ctx.chatContainer.addChild(readGroup);
|
||||
}
|
||||
readGroup.updateArgs(content.arguments, content.id);
|
||||
@@ -458,6 +459,7 @@ export class UiHelpers {
|
||||
showContentPreview: this.ctx.settings.get("read.toolResultPreview"),
|
||||
});
|
||||
readGroup.setExpanded(this.ctx.toolOutputExpanded);
|
||||
readGroup.setToolActivityVisible(!this.ctx.hideToolActivity);
|
||||
this.ctx.chatContainer.addChild(readGroup);
|
||||
}
|
||||
readGroup.updateArgs(content.arguments, content.id);
|
||||
@@ -510,6 +512,7 @@ export class UiHelpers {
|
||||
content.id,
|
||||
);
|
||||
component.setExpanded(this.ctx.toolOutputExpanded);
|
||||
component.setToolActivityVisible(!this.ctx.hideToolActivity);
|
||||
this.ctx.chatContainer.addChild(component);
|
||||
|
||||
if (hasErrorStop && errorMessage) {
|
||||
@@ -578,6 +581,7 @@ export class UiHelpers {
|
||||
showContentPreview: this.ctx.settings.get("read.toolResultPreview"),
|
||||
});
|
||||
readGroup.setExpanded(this.ctx.toolOutputExpanded);
|
||||
readGroup.setToolActivityVisible(!this.ctx.hideToolActivity);
|
||||
this.ctx.chatContainer.addChild(readGroup);
|
||||
}
|
||||
const args = readToolCallArgs.get(message.toolCallId);
|
||||
|
||||
Reference in New Issue
Block a user