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

@ -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;
}