feat(coding-agent): add user_bash event and theme API extensions

- user_bash event for intercepting ! and !! commands (#528)
- Extensions can return { operations } or { result } to redirect/replace
- executeBashWithOperations() for custom BashOperations execution
- session.recordBashResult() for extensions handling bash themselves
- Theme API: getAllThemes(), getTheme(), setTheme() on ctx.ui
- mac-system-theme.ts example: sync with macOS dark/light mode
- Updated ssh.ts to use user_bash event
This commit is contained in:
Mario Zechner 2026-01-08 21:50:56 +01:00
parent 16e142ef7d
commit 121823c74d
14 changed files with 405 additions and 36 deletions

View file

@ -24,7 +24,7 @@ import type {
import type { AssistantMessage, ImageContent, Message, Model, TextContent } from "@mariozechner/pi-ai";
import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@mariozechner/pi-ai";
import { getAuthPath } from "../config.js";
import { type BashResult, executeBash as executeBashCommand } from "./bash-executor.js";
import { type BashResult, executeBash as executeBashCommand, executeBashWithOperations } from "./bash-executor.js";
import {
type CompactionResult,
calculateContextTokens,
@ -50,6 +50,7 @@ import type { ModelRegistry } from "./model-registry.js";
import { expandPromptTemplate, type PromptTemplate } from "./prompt-templates.js";
import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions, SessionManager } from "./session-manager.js";
import type { SettingsManager, SkillsSettings } from "./settings-manager.js";
import type { BashOperations } from "./tools/bash.js";
/** Session-specific events that extend the core AgentEvent */
export type AgentSessionEvent =
@ -1617,51 +1618,63 @@ export class AgentSession {
* @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)
* @param options.operations Custom BashOperations for remote execution
*/
async executeBash(
command: string,
onChunk?: (chunk: string) => void,
options?: { excludeFromContext?: boolean },
options?: { excludeFromContext?: boolean; operations?: BashOperations },
): Promise<BashResult> {
this._bashAbortController = new AbortController();
try {
const result = await executeBashCommand(command, {
onChunk,
signal: this._bashAbortController.signal,
});
// Create and save message
const bashMessage: BashExecutionMessage = {
role: "bashExecution",
command,
output: result.output,
exitCode: result.exitCode,
cancelled: result.cancelled,
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
if (this.isStreaming) {
// Queue for later - will be flushed on agent_end
this._pendingBashMessages.push(bashMessage);
} else {
// Add to agent state immediately
this.agent.appendMessage(bashMessage);
// Save to session
this.sessionManager.appendMessage(bashMessage);
}
const result = options?.operations
? await executeBashWithOperations(command, process.cwd(), options.operations, {
onChunk,
signal: this._bashAbortController.signal,
})
: await executeBashCommand(command, {
onChunk,
signal: this._bashAbortController.signal,
});
this.recordBashResult(command, result, options);
return result;
} finally {
this._bashAbortController = undefined;
}
}
/**
* Record a bash execution result in session history.
* Used by executeBash and by extensions that handle bash execution themselves.
*/
recordBashResult(command: string, result: BashResult, options?: { excludeFromContext?: boolean }): void {
const bashMessage: BashExecutionMessage = {
role: "bashExecution",
command,
output: result.output,
exitCode: result.exitCode,
cancelled: result.cancelled,
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
if (this.isStreaming) {
// Queue for later - will be flushed on agent_end
this._pendingBashMessages.push(bashMessage);
} else {
// Add to agent state immediately
this.agent.appendMessage(bashMessage);
// Save to session
this.sessionManager.appendMessage(bashMessage);
}
}
/**
* Cancel running bash command.
*/