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
+74 -1
View File
@@ -8,7 +8,13 @@ import * as path from "node:path";
import { performance } from "node:perf_hooks";
import { isKeyRelease, matchesKey } from "./keys.ts";
import type { Terminal } from "./terminal.ts";
import { isOsc11BackgroundColorResponse, parseOsc11BackgroundColor, type RgbColor } from "./terminal-colors.ts";
import {
isOsc11BackgroundColorResponse,
parseOsc11BackgroundColor,
parseTerminalColorSchemeReport,
type RgbColor,
type TerminalColorScheme,
} from "./terminal-colors.ts";
import { deleteKittyImage, getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.ts";
import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.ts";
@@ -311,6 +317,8 @@ export class TUI extends Container {
private stopped = false;
private pendingOsc11BackgroundReplies = 0;
private pendingOsc11BackgroundQueries: PendingOsc11BackgroundQuery[] = [];
private terminalColorSchemeListeners = new Set<(scheme: TerminalColorScheme) => void>();
private terminalColorSchemeNotificationsEnabled = false;
// Overlay stack for modal components rendered on top of base content
private focusOrderCounter = 0;
@@ -631,6 +639,9 @@ export class TUI extends Container {
() => this.requestRender(),
);
this.terminal.hideCursor();
if (this.terminalColorSchemeNotificationsEnabled) {
this.terminal.write("\x1b[?2031h");
}
this.queryCellSize();
this.requestRender();
}
@@ -646,6 +657,23 @@ export class TUI extends Container {
this.inputListeners.delete(listener);
}
onTerminalColorSchemeChange(listener: (scheme: TerminalColorScheme) => void): () => void {
this.terminalColorSchemeListeners.add(listener);
return () => {
this.terminalColorSchemeListeners.delete(listener);
};
}
setTerminalColorSchemeNotifications(enabled: boolean): void {
if (this.terminalColorSchemeNotificationsEnabled === enabled) {
return;
}
this.terminalColorSchemeNotificationsEnabled = enabled;
if (!this.stopped) {
this.terminal.write(enabled ? "\x1b[?2031h" : "\x1b[?2031l");
}
}
private queryCellSize(): void {
// Only query if terminal supports images (cell size is only used for image rendering)
if (!getCapabilities().images) {
@@ -662,6 +690,9 @@ export class TUI extends Container {
clearTimeout(this.renderTimer);
this.renderTimer = undefined;
}
if (this.terminalColorSchemeNotificationsEnabled) {
this.terminal.write("\x1b[?2031l");
}
// Move cursor to the end of the content to prevent overwriting/artifacts on exit
if (this.previousLines.length > 0) {
const targetRow = this.previousLines.length; // Line after the last content
@@ -731,6 +762,9 @@ export class TUI extends Container {
if (this.consumeOsc11BackgroundResponse(data)) {
return;
}
if (this.consumeTerminalColorSchemeReport(data)) {
return;
}
if (this.inputListeners.size > 0) {
let current = data;
@@ -824,6 +858,18 @@ export class TUI extends Container {
return true;
}
private consumeTerminalColorSchemeReport(data: string): boolean {
const scheme = parseTerminalColorSchemeReport(data);
if (!scheme) {
return false;
}
for (const listener of this.terminalColorSchemeListeners) {
listener(scheme);
}
return true;
}
private consumeCellSizeResponse(data: string): boolean {
// Response format: ESC [ 6 ; height ; width t
const match = data.match(/^\x1b\[6;(\d+);(\d+)t$/);
@@ -1638,4 +1684,31 @@ export class TUI extends Container {
this.terminal.write("\x1b]11;?\x07");
});
}
/**
* Query the terminal's color-scheme preference with DSR (`CSI ? 996 n`).
* Terminals that support the color palette notification protocol reply with
* `CSI ? 997 ; 1 n` for dark or `CSI ? 997 ; 2 n` for light.
*/
queryTerminalColorScheme({ timeoutMs }: { timeoutMs: number }): Promise<TerminalColorScheme | undefined> {
return new Promise((resolve) => {
let settled = false;
let timer: NodeJS.Timeout | undefined;
let unsubscribe: () => void = () => {};
const settle = (scheme: TerminalColorScheme | undefined) => {
if (settled) return;
settled = true;
if (timer) {
clearTimeout(timer);
timer = undefined;
}
unsubscribe();
resolve(scheme);
};
unsubscribe = this.onTerminalColorSchemeChange(settle);
timer = setTimeout(() => settle(undefined), timeoutMs);
this.terminal.write("\x1b[?996n");
});
}
}