From 746ec9eb01193d3c792921330cf83a252645ee09 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sat, 3 Jan 2026 04:14:35 +0100 Subject: [PATCH] Add shell commands without context contribution (!! prefix) Use !!command to execute bash commands that are shown in the TUI and saved to session history but excluded from LLM context, compaction summaries, and branch summaries. - Add excludeFromContext field to BashExecutionMessage - Filter excluded messages in convertToLlm() - Parse !! prefix in interactive mode - Use dim border color for excluded commands fixes #414 --- packages/coding-agent/CHANGELOG.md | 4 +++ .../coding-agent/src/core/agent-session.ts | 8 ++++- packages/coding-agent/src/core/messages.ts | 6 ++++ .../interactive/components/bash-execution.ts | 10 ++++--- .../src/modes/interactive/interactive-mode.ts | 29 +++++++++++-------- 5 files changed, 40 insertions(+), 17 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 4e63445d1..82b3f673f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Shell commands without context contribution: use `!!command` to execute a bash command that is shown in the TUI and saved to session history but excluded from LLM context. Useful for running commands you don't want the AI to see. ([#414](https://github.com/badlogic/pi-mono/issues/414)) + ## [0.32.0] - 2026-01-03 ### Breaking Changes diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index bf9984ec9..e8b699186 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -1395,8 +1395,13 @@ export class AgentSession { * Adds result to agent context and session. * @param command The bash command to execute * @param onChunk Optional streaming callback for output + * @param options.excludeFromContext If true, command output won't be sent to LLM (!! prefix) */ - async executeBash(command: string, onChunk?: (chunk: string) => void): Promise { + async executeBash( + command: string, + onChunk?: (chunk: string) => void, + options?: { excludeFromContext?: boolean }, + ): Promise { this._bashAbortController = new AbortController(); try { @@ -1415,6 +1420,7 @@ export class AgentSession { truncated: result.truncated, fullOutputPath: result.fullOutputPath, timestamp: Date.now(), + excludeFromContext: options?.excludeFromContext, }; // If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering diff --git a/packages/coding-agent/src/core/messages.ts b/packages/coding-agent/src/core/messages.ts index 2726903d5..87c37fac0 100644 --- a/packages/coding-agent/src/core/messages.ts +++ b/packages/coding-agent/src/core/messages.ts @@ -35,6 +35,8 @@ export interface BashExecutionMessage { truncated: boolean; fullOutputPath?: string; timestamp: number; + /** If true, this message is excluded from LLM context (!! prefix) */ + excludeFromContext?: boolean; } /** @@ -148,6 +150,10 @@ export function convertToLlm(messages: AgentMessage[]): Message[] { .map((m): Message | undefined => { switch (m.role) { case "bashExecution": + // Skip messages excluded from context (!! prefix) + if (m.excludeFromContext) { + return undefined; + } return { role: "user", content: [{ type: "text", text: bashExecutionToText(m) }], diff --git a/packages/coding-agent/src/modes/interactive/components/bash-execution.ts b/packages/coding-agent/src/modes/interactive/components/bash-execution.ts index 2e914fa9d..8f98b45eb 100644 --- a/packages/coding-agent/src/modes/interactive/components/bash-execution.ts +++ b/packages/coding-agent/src/modes/interactive/components/bash-execution.ts @@ -29,12 +29,14 @@ export class BashExecutionComponent extends Container { private contentContainer: Container; private ui: TUI; - constructor(command: string, ui: TUI) { + constructor(command: string, ui: TUI, excludeFromContext = false) { super(); this.command = command; this.ui = ui; - const borderColor = (str: string) => theme.fg("bashMode", str); + // Use dim border for excluded-from-context commands (!! prefix) + const colorKey = excludeFromContext ? "dim" : "bashMode"; + const borderColor = (str: string) => theme.fg(colorKey, str); // Add spacer this.addChild(new Spacer(1)); @@ -47,13 +49,13 @@ export class BashExecutionComponent extends Container { this.addChild(this.contentContainer); // Command header - const header = new Text(theme.fg("bashMode", theme.bold(`$ ${command}`)), 1, 0); + const header = new Text(theme.fg(colorKey, theme.bold(`$ ${command}`)), 1, 0); this.contentContainer.addChild(header); // Loader this.loader = new Loader( ui, - (spinner) => theme.fg("bashMode", spinner), + (spinner) => theme.fg(colorKey, spinner), (text) => theme.fg("muted", text), "Running... (esc to cancel)", ); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 871b21f7d..238aaa5c4 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -892,9 +892,10 @@ export class InteractiveMode { return; } - // Handle bash command + // Handle bash command (! for normal, !! for excluded from context) if (text.startsWith("!")) { - const command = text.slice(1).trim(); + const isExcluded = text.startsWith("!!"); + const command = isExcluded ? text.slice(2).trim() : text.slice(1).trim(); if (command) { if (this.session.isBashRunning) { this.showWarning("A bash command is already running. Press Esc to cancel it first."); @@ -902,7 +903,7 @@ export class InteractiveMode { return; } this.editor.addToHistory(text); - await this.handleBashCommand(command); + await this.handleBashCommand(command, isExcluded); this.isBashMode = false; this.updateEditorBorderColor(); return; @@ -1250,7 +1251,7 @@ export class InteractiveMode { private addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): void { switch (message.role) { case "bashExecution": { - const component = new BashExecutionComponent(message.command, this.ui); + const component = new BashExecutionComponent(message.command, this.ui, message.excludeFromContext); if (message.output) { component.appendOutput(message.output); } @@ -2362,9 +2363,9 @@ export class InteractiveMode { this.ui.requestRender(); } - private async handleBashCommand(command: string): Promise { + private async handleBashCommand(command: string, excludeFromContext = false): Promise { const isDeferred = this.session.isStreaming; - this.bashComponent = new BashExecutionComponent(command, this.ui); + this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext); if (isDeferred) { // Show in pending area when agent is streaming @@ -2377,12 +2378,16 @@ export class InteractiveMode { this.ui.requestRender(); try { - const result = await this.session.executeBash(command, (chunk) => { - if (this.bashComponent) { - this.bashComponent.appendOutput(chunk); - this.ui.requestRender(); - } - }); + const result = await this.session.executeBash( + command, + (chunk) => { + if (this.bashComponent) { + this.bashComponent.appendOutput(chunk); + this.ui.requestRender(); + } + }, + { excludeFromContext }, + ); if (this.bashComponent) { this.bashComponent.setComplete(