mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 21:03:42 +00:00
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:
parent
16e142ef7d
commit
121823c74d
14 changed files with 405 additions and 36 deletions
|
|
@ -75,12 +75,15 @@ import { UserMessageComponent } from "./components/user-message.js";
|
|||
import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
|
||||
import {
|
||||
getAvailableThemes,
|
||||
getAvailableThemesWithPaths,
|
||||
getEditorTheme,
|
||||
getMarkdownTheme,
|
||||
getThemeByName,
|
||||
initTheme,
|
||||
onThemeChange,
|
||||
setTheme,
|
||||
type Theme,
|
||||
setThemeInstance,
|
||||
Theme,
|
||||
theme,
|
||||
} from "./theme/theme.js";
|
||||
|
||||
|
|
@ -937,6 +940,20 @@ export class InteractiveMode {
|
|||
get theme() {
|
||||
return theme;
|
||||
},
|
||||
getAllThemes: () => getAvailableThemesWithPaths(),
|
||||
getTheme: (name) => getThemeByName(name),
|
||||
setTheme: (themeOrName) => {
|
||||
if (themeOrName instanceof Theme) {
|
||||
setThemeInstance(themeOrName);
|
||||
this.ui.requestRender();
|
||||
return { success: true };
|
||||
}
|
||||
const result = setTheme(themeOrName, true);
|
||||
if (result.success) {
|
||||
this.ui.requestRender();
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -3140,6 +3157,50 @@ export class InteractiveMode {
|
|||
}
|
||||
|
||||
private async handleBashCommand(command: string, excludeFromContext = false): Promise<void> {
|
||||
const extensionRunner = this.session.extensionRunner;
|
||||
|
||||
// Emit user_bash event to let extensions intercept
|
||||
const eventResult = extensionRunner
|
||||
? await extensionRunner.emitUserBash({
|
||||
type: "user_bash",
|
||||
command,
|
||||
excludeFromContext,
|
||||
cwd: process.cwd(),
|
||||
})
|
||||
: undefined;
|
||||
|
||||
// If extension returned a full result, use it directly
|
||||
if (eventResult?.result) {
|
||||
const result = eventResult.result;
|
||||
|
||||
// Create UI component for display
|
||||
this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
|
||||
if (this.session.isStreaming) {
|
||||
this.pendingMessagesContainer.addChild(this.bashComponent);
|
||||
this.pendingBashComponents.push(this.bashComponent);
|
||||
} else {
|
||||
this.chatContainer.addChild(this.bashComponent);
|
||||
}
|
||||
|
||||
// Show output and complete
|
||||
if (result.output) {
|
||||
this.bashComponent.appendOutput(result.output);
|
||||
}
|
||||
this.bashComponent.setComplete(
|
||||
result.exitCode,
|
||||
result.cancelled,
|
||||
result.truncated ? ({ truncated: true, content: result.output } as TruncationResult) : undefined,
|
||||
result.fullOutputPath,
|
||||
);
|
||||
|
||||
// Record the result in session
|
||||
this.session.recordBashResult(command, result, { excludeFromContext });
|
||||
this.bashComponent = undefined;
|
||||
this.ui.requestRender();
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal execution path (possibly with custom operations)
|
||||
const isDeferred = this.session.isStreaming;
|
||||
this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
|
||||
|
||||
|
|
@ -3162,7 +3223,7 @@ export class InteractiveMode {
|
|||
this.ui.requestRender();
|
||||
}
|
||||
},
|
||||
{ excludeFromContext },
|
||||
{ excludeFromContext, operations: eventResult?.operations },
|
||||
);
|
||||
|
||||
if (this.bashComponent) {
|
||||
|
|
|
|||
|
|
@ -456,6 +456,36 @@ export function getAvailableThemes(): string[] {
|
|||
return Array.from(themes).sort();
|
||||
}
|
||||
|
||||
export interface ThemeInfo {
|
||||
name: string;
|
||||
path: string | undefined;
|
||||
}
|
||||
|
||||
export function getAvailableThemesWithPaths(): ThemeInfo[] {
|
||||
const themesDir = getThemesDir();
|
||||
const customThemesDir = getCustomThemesDir();
|
||||
const result: ThemeInfo[] = [];
|
||||
|
||||
// Built-in themes
|
||||
for (const name of Object.keys(getBuiltinThemes())) {
|
||||
result.push({ name, path: path.join(themesDir, `${name}.json`) });
|
||||
}
|
||||
|
||||
// Custom themes
|
||||
if (fs.existsSync(customThemesDir)) {
|
||||
for (const file of fs.readdirSync(customThemesDir)) {
|
||||
if (file.endsWith(".json")) {
|
||||
const name = file.slice(0, -5);
|
||||
if (!result.some((t) => t.name === name)) {
|
||||
result.push({ name, path: path.join(customThemesDir, file) });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
function loadThemeJson(name: string): ThemeJson {
|
||||
const builtinThemes = getBuiltinThemes();
|
||||
if (name in builtinThemes) {
|
||||
|
|
@ -532,6 +562,14 @@ function loadTheme(name: string, mode?: ColorMode): Theme {
|
|||
return createTheme(themeJson, mode);
|
||||
}
|
||||
|
||||
export function getThemeByName(name: string): Theme | undefined {
|
||||
try {
|
||||
return loadTheme(name);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function detectTerminalBackground(): "dark" | "light" {
|
||||
const colorfgbg = process.env.COLORFGBG || "";
|
||||
if (colorfgbg) {
|
||||
|
|
@ -596,6 +634,12 @@ export function setTheme(name: string, enableWatcher: boolean = false): { succes
|
|||
}
|
||||
}
|
||||
|
||||
export function setThemeInstance(themeInstance: Theme): void {
|
||||
theme = themeInstance;
|
||||
currentThemeName = "<in-memory>";
|
||||
stopThemeWatcher(); // Can't watch a direct instance
|
||||
}
|
||||
|
||||
export function onThemeChange(callback: () => void): void {
|
||||
onThemeChangeCallback = callback;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue