feat(coding-agent): add automatic theme mode (#5874)

This commit is contained in:
Armin Ronacher
2026-06-18 17:13:47 +02:00
committed by GitHub
parent 908be616f2
commit d0b46764b1
16 changed files with 620 additions and 80 deletions
@@ -128,22 +128,19 @@ import { UserMessageComponent } from "./components/user-message.ts";
import { UserMessageSelectorComponent } from "./components/user-message-selector.ts";
import { getModelSearchText } from "./model-search.ts";
import {
detectTerminalBackgroundTheme,
getAvailableThemes,
getAvailableThemesWithPaths,
getEditorTheme,
getMarkdownTheme,
getThemeByName,
initTheme,
onThemeChange,
setRegisteredThemes,
setTheme,
setThemeInstance,
stopThemeWatcher,
Theme,
type ThemeColor,
theme,
} from "./theme/theme.ts";
import { InteractiveThemeController } from "./theme/theme-controller.ts";
/** Interface for components that can be expanded/collapsed */
interface Expandable {
@@ -374,6 +371,7 @@ export class InteractiveMode {
private options: InteractiveModeOptions;
private autoTrustOnReloadCwd: string | undefined;
private themeController: InteractiveThemeController;
// Convenience accessors
private get session(): AgentSession {
@@ -428,26 +426,12 @@ export class InteractiveMode {
// Register themes from resource loader and initialize
setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
initTheme(this.settingsManager.getTheme(), true);
}
private async detectThemeIfUnset(): Promise<void> {
if (this.settingsManager.getTheme()) {
return;
}
const detection = await detectTerminalBackgroundTheme({ ui: this.ui, timeoutMs: 100 });
const result = setTheme(detection.theme, true);
if (!result.success) {
return;
}
if (detection.confidence === "high") {
this.settingsManager.setTheme(detection.theme);
await this.settingsManager.flush();
}
this.updateEditorBorderColor();
this.ui.requestRender();
this.themeController = new InteractiveThemeController(
this.ui,
this.settingsManager,
(message) => this.showError(message),
() => this.updateEditorBorderColor(),
);
}
private getAutocompleteSourceTag(sourceInfo?: SourceInfo): string | undefined {
@@ -672,7 +656,7 @@ export class InteractiveMode {
this.ui.start();
this.isInitialized = true;
await this.detectThemeIfUnset();
await this.themeController.applyFromSettings();
// Add header with keybindings from config (unless silenced)
if (this.options.verbose || !this.settingsManager.getQuietStartup()) {
@@ -2080,16 +2064,13 @@ export class InteractiveMode {
getTheme: (name) => getThemeByName(name),
setTheme: (themeOrName) => {
if (themeOrName instanceof Theme) {
setThemeInstance(themeOrName);
this.ui.requestRender();
return { success: true };
return this.themeController.setThemeInstance(themeOrName);
}
const result = setTheme(themeOrName, true);
const result = this.themeController.setThemeName(themeOrName);
if (result.success) {
if (this.settingsManager.getTheme() !== themeOrName) {
this.settingsManager.setTheme(themeOrName);
}
this.ui.requestRender();
}
return result;
},
@@ -3378,6 +3359,7 @@ export class InteractiveMode {
// which the stdout/stderr error handler turns into emergencyTerminalExit;
// the render loop is already idle, so this cannot hot-spin (see #4144).
await this.runtimeHost.dispose();
this.themeController.disableAutoSync();
await this.ui.terminal.drainInput(1000);
this.stop();
process.exit(0);
@@ -3388,6 +3370,7 @@ export class InteractiveMode {
// the final frame while the process is exiting.
// Drain any in-flight Kitty key release events before stopping.
// This prevents escape sequences from leaking to the parent shell over slow SSH.
this.themeController.disableAutoSync();
await this.ui.terminal.drainInput(1000);
this.stop();
@@ -3991,7 +3974,8 @@ export class InteractiveMode {
httpIdleTimeoutMs: this.settingsManager.getHttpIdleTimeoutMs(),
thinkingLevel: this.session.thinkingLevel,
availableThinkingLevels: this.session.getAvailableThinkingLevels(),
currentTheme: this.settingsManager.getTheme() || "dark",
currentTheme: this.settingsManager.getThemeSetting() || "dark",
terminalTheme: this.themeController.getTerminalTheme(),
availableThemes: getAvailableThemes(),
hideThinkingBlock: this.hideThinkingBlock,
collapseChangelog: this.settingsManager.getCollapseChangelog(),
@@ -4058,21 +4042,11 @@ export class InteractiveMode {
this.footer.invalidate();
this.updateEditorBorderColor();
},
onThemeChange: (themeName) => {
const result = setTheme(themeName, true);
this.settingsManager.setTheme(themeName);
this.ui.invalidate();
if (!result.success) {
this.showError(`Failed to load theme "${themeName}": ${result.error}\nFell back to dark theme.`);
}
},
onThemePreview: (themeName) => {
const result = setTheme(themeName, true);
if (result.success) {
this.ui.invalidate();
this.ui.requestRender();
}
onThemeChange: (themeSetting) => {
this.settingsManager.setTheme(themeSetting);
void this.themeController.applyFromSettings();
},
onThemePreview: (themeName) => this.themeController.preview(themeName),
onHideThinkingBlockChange: (hidden) => {
this.hideThinkingBlock = hidden;
this.settingsManager.setHideThinkingBlock(hidden);
@@ -5109,11 +5083,7 @@ export class InteractiveMode {
}
setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
const themeName = this.settingsManager.getTheme();
const themeResult = themeName ? setTheme(themeName, true) : { success: true };
if (!themeResult.success) {
this.showError(`Failed to load theme "${themeName}": ${themeResult.error}\nFell back to dark theme.`);
}
await this.themeController.applyFromSettings();
const editorPaddingX = this.settingsManager.getEditorPaddingX();
const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
this.defaultEditor.setPaddingX(editorPaddingX);
@@ -5754,6 +5724,7 @@ export class InteractiveMode {
this.loadingAnimation.stop();
this.loadingAnimation = undefined;
}
this.themeController.disableAutoSync();
this.clearExtensionTerminalInputListeners();
this.footer.dispose();
this.footerDataProvider.dispose();