feat(tui): add hidden tool activity mode

This commit is contained in:
Daniel Anderson-Little
2026-08-02 22:59:48 -04:00
parent aa014717dc
commit a20690a40b
23 changed files with 443 additions and 2 deletions
@@ -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");
+1
View File
@@ -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);