Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/TUI.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,15 @@ and unknown environments fall back to dark. The macOS read has a 500 ms process
timeout. Production does not query OSC 11 or switch themes after mount, so the
first frame cannot mix palettes across transcript, prompt, borders, or overlays.

Set `CORBITS_TRANSPARENT_BACKGROUND=1` (also `true`, `yes`, or `on`) to let the
host terminal show through the root, transcript, landing, welcome, and provider
canvases. This is an explicit OpenTUI canvas opt-in, not a guess based on
terminal name or truecolor reporting. Transparent canvas readability depends on
the detected or explicitly selected dark/light theme matching the host terminal
background. That theme owns every foreground color, while the prompt frame and
input, decision surfaces, and focused or selected rows retain its opaque ground,
so control focus and text contrast do not depend on the host background.

## The live task list panel

**Parked pending rebuild.** `formatChromeZones` (`src/tui/chrome-state.ts`)
Expand Down
5 changes: 5 additions & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,8 @@ export interface UnconfiguredConfig {
command: "tui" | "exec";
/** Exec-only chosen primary. Omitted on the unconfigured path too. */
director?: DirectorId;
/** Validated appearance preference needed before first-run surfaces mount. */
theme?: Settings["theme"];
// Path where the onboarding flow should write the new settings.
globalSettingsPath: string;
/** Original CLI path, present only when --config selected the write target. */
Expand Down Expand Up @@ -1089,6 +1091,9 @@ export async function loadConfig(
auto,
command,
...(director !== undefined ? { director } : {}),
...(settingsForResolution?.theme !== undefined
? { theme: settingsForResolution.theme }
: {}),
globalSettingsPath: effectiveSettingsPath,
...(configPath !== undefined ? { cliConfigPath: configPath } : {}),
programmaticSettingsPath: options.globalSettingsPath !== undefined,
Expand Down
9 changes: 9 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
import { runExec } from "./exec/runner.js";
import { runOnboarding } from "./tui/onboarding.js";
import { runTUI } from "./tui/runner/index.js";
import { applyStartupTheme } from "./tui/theme-startup.js";
import { configureTransparentBackground } from "./tui/theme.js";

export interface Runners {
runTUI: (config: import("./config/index.js").Config) => Promise<number>;
Expand Down Expand Up @@ -84,6 +86,13 @@ export async function mainWithRunners(
}

let exitCode: number;
// Welcome, setup, and the product host read `UI` at construction time.
if (config.command === "tui") {
configureTransparentBackground();
applyStartupTheme(
config.configured ? config.settings?.theme : config.theme,
);
}
if (!config.configured) {
if (config.command === "exec") {
// Exec needs a provider; onboarding is TUI-only. Fail closed with a
Expand Down
10 changes: 5 additions & 5 deletions src/tui/landing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,23 +317,23 @@ export function createLandingAbove(
flexDirection: "column",
justifyContent: "flex-end",
paddingLeft: LANDING_MARGIN,
backgroundColor: UI.ground,
backgroundColor: UI.canvasGround,
});
const hero = new BoxRenderable(ctx, {
id: "shell-landing-hero",
width: "100%",
height: MARK_LARGE.rows,
flexShrink: 0,
flexDirection: "row",
backgroundColor: UI.ground,
backgroundColor: UI.canvasGround,
});
const markColumn = new BoxRenderable(ctx, {
id: "shell-landing-mark",
width: MARK_LARGE.cols,
flexShrink: 0,
flexDirection: "column",
justifyContent: "flex-end",
backgroundColor: UI.ground,
backgroundColor: UI.canvasGround,
});
const markRows: TextRenderable[] = [];
for (let row = 0; row < MARK_LARGE.rows; row++) {
Expand Down Expand Up @@ -380,7 +380,7 @@ function createHintBlock(ctx: CliRenderer): BoxRenderable {
// empty beneath them, which reads as unfinished rather than composed.
justifyContent: "center",
paddingLeft: LANDING_HERO_GAP,
backgroundColor: UI.ground,
backgroundColor: UI.canvasGround,
});
LANDING_HINTS.forEach((hint, index) => {
const gap = " ".repeat(
Expand Down Expand Up @@ -452,7 +452,7 @@ export function createLandingBelow(
flexShrink: 0,
flexDirection: "column",
paddingLeft: LANDING_MARGIN,
backgroundColor: UI.ground,
backgroundColor: UI.canvasGround,
});
landingBelowRows(content).forEach((row, index) => {
box.add(
Expand Down
11 changes: 6 additions & 5 deletions src/tui/provider/surface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export function createSurface(
width: "100%",
height: "100%",
flexDirection: "column",
backgroundColor: UI.ground,
backgroundColor: UI.canvasGround,
paddingTop: 1,
paddingLeft: margin,
paddingRight: margin,
Expand Down Expand Up @@ -162,7 +162,7 @@ export function createSurface(
flexDirection: "column",
flexShrink: 0,
paddingTop: 1,
backgroundColor: UI.ground,
backgroundColor: UI.canvasGround,
});
const summarySlots = Array.from(
{ length: SUMMARY_SLOTS },
Expand All @@ -181,7 +181,7 @@ export function createSurface(
flexDirection: "column",
flexShrink: 0,
paddingTop: 1,
backgroundColor: UI.ground,
backgroundColor: UI.canvasGround,
});
const listSlots = Array.from(
{ length: PROVIDER_LIST_ROWS_MAX },
Expand Down Expand Up @@ -225,7 +225,7 @@ export function createSurface(
flexDirection: "column",
flexShrink: 0,
paddingTop: 1,
backgroundColor: UI.ground,
backgroundColor: UI.canvasGround,
visible: false,
});
const loginSlots = Array.from(
Expand Down Expand Up @@ -257,7 +257,7 @@ export function createSurface(
flexDirection: "column",
flexShrink: 0,
paddingTop: 1,
backgroundColor: UI.ground,
backgroundColor: UI.canvasGround,
visible: config.showTelemetryNotice,
});
const telemetrySlots = Array.from(
Expand Down Expand Up @@ -344,6 +344,7 @@ export function createSurface(
slot.visible = true;
slot.content = ` ${active ? ">" : " "} ${row.label}`;
slot.fg = active ? UI.text : UI.textDim;
slot.bg = active ? UI.ground : UI.canvasGround;
});
};

Expand Down
34 changes: 2 additions & 32 deletions src/tui/runner/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
*/

import { EventEmitter } from "node:events";
import { spawnSync } from "node:child_process";
import type { Config } from "../../config/index.js";
import { listFavoriteModels, listRecentModels } from "../../config/settings.js";
import { isCodexProviderName } from "../../config/codex-providers.js";
Expand Down Expand Up @@ -38,14 +37,7 @@ import { wireMcp } from "./mcp.js";
import { wirePostStartup } from "./wiring.js";
import { createRunnerState, liveAgent } from "./state.js";
import { applyCredentialRecoverySelection } from "./credential-recovery.js";
import { setTheme } from "../theme.js";
import {
detectOsAppearance,
resolveDetectedTheme,
resolveThemeSetting,
sniffSyncTheme,
syncEnvFromRecord,
} from "../theme-detect.js";
import { applyStartupTheme } from "../theme-startup.js";
import { getLogger } from "@intx/log";
import { LOG_NAMESPACE_ROOT } from "../../branding.js";

Expand All @@ -55,31 +47,9 @@ export function createTUIEventEmitter(): EventEmitter {

export { getTUIRunSummaryStatus } from "../../session/run-sink.js";

function detectStartupOsTheme() {
return detectOsAppearance(process.platform, (command, args) => {
try {
const out = spawnSync(command, [...args], {
encoding: "utf8",
timeout: 500,
});
if (out.error !== undefined) return undefined;
if (out.status !== 0) return null;
return typeof out.stdout === "string" ? out.stdout : undefined;
} catch {
return undefined;
}
});
}

export async function runTUI(initialConfig: Config): Promise<number> {
const tuiLogger = getLogger([LOG_NAMESPACE_ROOT, "tui"]);
const setting = resolveThemeSetting(initialConfig.settings?.theme);
const syncEnv = syncEnvFromRecord(process.env);
const os =
setting === "auto" && sniffSyncTheme(syncEnv) === null
? detectStartupOsTheme()
: null;
setTheme(resolveDetectedTheme({ setting, syncEnv, os }));
applyStartupTheme(initialConfig.settings?.theme);

const start = await prepareTUISession(initialConfig, liveTelemetry);
if (start === null) return 0;
Expand Down
1 change: 1 addition & 0 deletions src/tui/shell/chrome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ function syncPendingRows(shell: AppShell): void {
row.tag === null ? UI.textFaint : selected ? UI.text : UI.textDim,
)(fitted.text),
]),
...(selected ? { bg: UI.ground } : {}),
}),
);
}
Expand Down
22 changes: 11 additions & 11 deletions src/tui/shell/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export function createAppShell(
width: "100%",
height: "100%",
flexDirection: "column",
backgroundColor: UI.ground,
backgroundColor: UI.canvasGround,
paddingLeft: layout.sideMargin,
paddingRight: layout.sideMargin,
});
Expand All @@ -136,7 +136,7 @@ export function createAppShell(
width: "100%",
height: 1,
flexShrink: 0,
backgroundColor: UI.ground,
backgroundColor: UI.canvasGround,
});

// Same gutter, other end: keeps the prompt box off the terminal's last row.
Expand All @@ -145,7 +145,7 @@ export function createAppShell(
width: "100%",
height: 1,
flexShrink: 0,
backgroundColor: UI.ground,
backgroundColor: UI.canvasGround,
});

// Persistent chrome, not part of the landing composition (`landing.ts`
Expand All @@ -164,7 +164,7 @@ export function createAppShell(
flexShrink: 0,
flexDirection: "row",
justifyContent: "flex-end",
backgroundColor: UI.ground,
backgroundColor: UI.canvasGround,
visible: versionBadgeVisible(terminal.columns, terminal.rows),
});
const versionBadge = new TextRenderable(ctx, {
Expand All @@ -181,7 +181,7 @@ export function createAppShell(
height: 1,
flexShrink: 0,
flexDirection: "column",
backgroundColor: UI.ground,
backgroundColor: UI.canvasGround,
visible: false,
});

Expand All @@ -191,7 +191,7 @@ export function createAppShell(
height: 1,
flexShrink: 0,
flexDirection: "column",
backgroundColor: UI.ground,
backgroundColor: UI.canvasGround,
visible: false,
});

Expand All @@ -204,9 +204,9 @@ export function createAppShell(
stickyStart: "bottom",
scrollY: true,
focusable: true,
rootOptions: { backgroundColor: UI.ground },
contentOptions: { backgroundColor: UI.ground },
viewportOptions: { backgroundColor: UI.ground },
rootOptions: { backgroundColor: UI.canvasGround },
contentOptions: { backgroundColor: UI.canvasGround },
viewportOptions: { backgroundColor: UI.canvasGround },
});
// The transcript scrolls with the keyboard, and the bar spent a column on
// every row to say so. Position is legible from the content itself.
Expand All @@ -224,7 +224,7 @@ export function createAppShell(
width: "100%",
height: 0,
flexShrink: 0,
backgroundColor: UI.ground,
backgroundColor: UI.canvasGround,
});
transcript.add(transcriptSpacer);

Expand Down Expand Up @@ -260,7 +260,7 @@ export function createAppShell(
height: Math.max(1, layout.heights.pending),
flexShrink: 0,
flexDirection: "column",
backgroundColor: UI.ground,
backgroundColor: UI.canvasGround,
visible: layout.heights.pending > 0,
});

Expand Down
59 changes: 59 additions & 0 deletions src/tui/theme-startup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { spawnSync } from "node:child_process";

import {
detectOsAppearance,
resolveDetectedTheme,
resolveThemeSetting,
sniffSyncTheme,
syncEnvFromRecord,
type OsAppearanceRunner,
} from "./theme-detect.js";
import { setTheme, type ThemeName } from "./theme.js";

export interface StartupThemeOptions {
readonly env?: Record<string, string | undefined>;
readonly platform?: string;
readonly runOsCommand?: OsAppearanceRunner;
}

function runOsAppearanceCommand(
command: string,
args: readonly string[],
): string | null | undefined {
try {
const out = spawnSync(command, [...args], {
encoding: "utf8",
timeout: 500,
});
if (out.error !== undefined) return undefined;
if (out.status !== 0) return null;
return typeof out.stdout === "string" ? out.stdout : undefined;
} catch {
return undefined;
}
}

export function resolveStartupTheme(
rawSetting: unknown,
options: StartupThemeOptions = {},
): ThemeName {
const setting = resolveThemeSetting(rawSetting);
const syncEnv = syncEnvFromRecord(options.env ?? process.env);
const os =
setting === "auto" && sniffSyncTheme(syncEnv) === null
? detectOsAppearance(
options.platform ?? process.platform,
options.runOsCommand ?? runOsAppearanceCommand,
)
: null;
return resolveDetectedTheme({ setting, syncEnv, os });
}

export function applyStartupTheme(
rawSetting: unknown,
options: StartupThemeOptions = {},
): ThemeName {
const theme = resolveStartupTheme(rawSetting, options);
setTheme(theme);
return theme;
}
Loading
Loading