From aa6bdd778a561b8641a5451093135445e4c855e1 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 22 Jun 2026 17:18:43 +0200 Subject: [PATCH] fix(coding-agent): load resume startup themes --- packages/coding-agent/CHANGELOG.md | 4 + .../coding-agent/src/cli/session-picker.ts | 9 +- packages/coding-agent/src/cli/startup-ui.ts | 90 +++++++++++++++---- .../coding-agent/src/core/settings-manager.ts | 4 +- packages/coding-agent/src/main.ts | 2 +- .../interactive/theme/theme-controller.ts | 13 +-- .../src/modes/interactive/theme/theme.ts | 23 +++++ 7 files changed, 112 insertions(+), 33 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 6795f662..e2650200 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -12,6 +12,10 @@ - Added an experimental first-time setup flow behind `PI_EXPERIMENTAL=1` that asks for a dark/light theme choice (preselecting the detected appearance) and opt-in analytics data sharing on first launch with the default agent directory; opting in stores a `trackingId` in `settings.json`. +### Fixed + +- Fixed `pi --resume` to load user package themes and resolve automatic light/dark theme settings. + ## [0.79.10] - 2026-06-22 ### New Features diff --git a/packages/coding-agent/src/cli/session-picker.ts b/packages/coding-agent/src/cli/session-picker.ts index 42dcb808..793f1d0a 100644 --- a/packages/coding-agent/src/cli/session-picker.ts +++ b/packages/coding-agent/src/cli/session-picker.ts @@ -2,10 +2,12 @@ * TUI session selector for --resume flag */ -import { ProcessTerminal, setKeybindings, TUI } from "@earendil-works/pi-tui"; +import { setKeybindings } from "@earendil-works/pi-tui"; import { KeybindingsManager } from "../core/keybindings.ts"; import type { SessionInfo, SessionListProgress } from "../core/session-manager.ts"; +import type { SettingsManager } from "../core/settings-manager.ts"; import { SessionSelectorComponent } from "../modes/interactive/components/session-selector.ts"; +import { createStartupTui, startStartupTui } from "./startup-ui.ts"; type SessionsLoader = (onProgress?: SessionListProgress) => Promise; @@ -13,9 +15,10 @@ type SessionsLoader = (onProgress?: SessionListProgress) => Promise { + const ui = await createStartupTui(settingsManager); return new Promise((resolve) => { - const ui = new TUI(new ProcessTerminal()); const keybindings = KeybindingsManager.create(); setKeybindings(keybindings); let resolved = false; @@ -47,6 +50,6 @@ export async function selectSession( ui.addChild(selector); ui.setFocus(selector.getSessionList()); - ui.start(); + startStartupTui(ui, settingsManager); }); } diff --git a/packages/coding-agent/src/cli/startup-ui.ts b/packages/coding-agent/src/cli/startup-ui.ts index 93841304..73c0271c 100644 --- a/packages/coding-agent/src/cli/startup-ui.ts +++ b/packages/coding-agent/src/cli/startup-ui.ts @@ -1,16 +1,27 @@ import { ProcessTerminal, setKeybindings, TUI } from "@earendil-works/pi-tui"; import { existsSync } from "fs"; -import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, getSettingsPath, PACKAGE_NAME } from "../config.ts"; +import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, getAgentDir, getSettingsPath, PACKAGE_NAME } from "../config.ts"; import { areExperimentalFeaturesEnabled } from "../core/experimental.ts"; import { KeybindingsManager } from "../core/keybindings.ts"; -import type { SettingsManager } from "../core/settings-manager.ts"; +import { DefaultPackageManager, type ResolvedResource } from "../core/package-manager.ts"; +import { SettingsManager } from "../core/settings-manager.ts"; import { ExtensionInputComponent } from "../modes/interactive/components/extension-input.ts"; import { ExtensionSelectorComponent } from "../modes/interactive/components/extension-selector.ts"; import { FirstTimeSetupComponent, type FirstTimeSetupResult, } from "../modes/interactive/components/first-time-setup.ts"; -import { detectTerminalBackgroundTheme, initTheme, setTheme } from "../modes/interactive/theme/theme.ts"; +import { + detectTerminalBackgroundFromEnv, + detectTerminalThemeForAuto, + initTheme, + loadThemeFromPath, + parseAutoThemeSetting, + resolveThemeSetting, + setRegisteredThemes, + setTheme, + type Theme, +} from "../modes/interactive/theme/theme.ts"; const OFFICIAL_PACKAGE_NAME = "@earendil-works/pi-coding-agent"; const OFFICIAL_APP_NAME = "pi"; @@ -30,14 +41,64 @@ function isOfficialDistribution({ packageName, appName, configDirName }: Distrib ); } -function createStartupTui(settingsManager: SettingsManager): TUI { - initTheme(settingsManager.getTheme()); +function loadThemes(resources: ResolvedResource[]): Theme[] { + const themes: Theme[] = []; + const seen = new Set(); + for (const resource of resources) { + if (!resource.enabled) continue; + try { + const loadedTheme = loadThemeFromPath(resource.path); + if (loadedTheme.name) { + if (seen.has(loadedTheme.name)) continue; + seen.add(loadedTheme.name); + } + themes.push(loadedTheme); + } catch { + // Startup prompts should not fail because a theme is broken. The normal + // resource loader reports theme diagnostics later in startup. + } + } + return themes; +} + +async function loadStartupThemes(settingsManager: SettingsManager): Promise { + const globalSettingsManager = SettingsManager.inMemory(settingsManager.getGlobalSettings(), { + projectTrusted: false, + }); + const packageManager = new DefaultPackageManager({ + cwd: process.cwd(), + agentDir: getAgentDir(), + settingsManager: globalSettingsManager, + }); + const resolvedPaths = await packageManager.resolve(async () => "skip"); + return loadThemes(resolvedPaths.themes); +} + +export async function createStartupTui(settingsManager: SettingsManager): Promise { + setRegisteredThemes(await loadStartupThemes(settingsManager)); + const terminalTheme = detectTerminalBackgroundFromEnv().theme; + initTheme(resolveThemeSetting(settingsManager.getThemeSetting(), terminalTheme) ?? terminalTheme); setKeybindings(KeybindingsManager.create()); const ui = new TUI(new ProcessTerminal(), settingsManager.getShowHardwareCursor()); ui.setClearOnShrink(settingsManager.getClearOnShrink()); return ui; } +export function startStartupTui(ui: TUI, settingsManager: SettingsManager): void { + ui.start(); + void applyDetectedStartupTheme(ui, settingsManager); +} + +async function applyDetectedStartupTheme(ui: TUI, settingsManager: SettingsManager): Promise { + const themeSetting = settingsManager.getThemeSetting(); + if (themeSetting && !parseAutoThemeSetting(themeSetting)) return; + + const terminalTheme = await detectTerminalThemeForAuto({ ui, timeoutMs: 100 }); + setTheme(resolveThemeSetting(themeSetting, terminalTheme) ?? terminalTheme); + ui.invalidate(); + ui.requestRender(); +} + async function clearStartupTui(ui: TUI): Promise { ui.clear(); ui.requestRender(); @@ -75,9 +136,8 @@ export async function showStartupSelector( title: string, options: Array<{ label: string; value: T }>, ): Promise { + const ui = await createStartupTui(settingsManager); return new Promise((resolve) => { - const ui = createStartupTui(settingsManager); - let settled = false; const finish = async (result: T | undefined) => { if (settled) { @@ -98,15 +158,14 @@ export async function showStartupSelector( ); ui.addChild(selector); ui.setFocus(selector); - ui.start(); + startStartupTui(ui, settingsManager); }); } /** Show the first-time setup dialog and persist the result */ export async function showFirstTimeSetup(settingsManager: SettingsManager): Promise { + const ui = await createStartupTui(settingsManager); return new Promise((resolve) => { - const ui = createStartupTui(settingsManager); - let settled = false; const finish = async (result: FirstTimeSetupResult | undefined) => { if (settled) { @@ -125,10 +184,10 @@ export async function showFirstTimeSetup(settingsManager: SettingsManager): Prom const showSetup = async () => { ui.start(); - const detection = await detectTerminalBackgroundTheme({ ui, timeoutMs: 100 }); - setTheme(detection.theme); + const detectedTheme = await detectTerminalThemeForAuto({ ui, timeoutMs: 100 }); + setTheme(detectedTheme); const component = new FirstTimeSetupComponent({ - detectedTheme: detection.theme, + detectedTheme, onThemePreview: (themeName) => { setTheme(themeName); ui.requestRender(); @@ -150,9 +209,8 @@ export async function showStartupInput( title: string, placeholder?: string, ): Promise { + const ui = await createStartupTui(settingsManager); return new Promise((resolve) => { - const ui = createStartupTui(settingsManager); - let settled = false; const finish = async (result: string | undefined) => { if (settled) { @@ -176,6 +234,6 @@ export async function showStartupInput( ); ui.addChild(input); ui.setFocus(input); - ui.start(); + startStartupTui(ui, settingsManager); }); } diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index a90916a4..99ae71a4 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -334,11 +334,11 @@ export class SettingsManager { } /** Create an in-memory SettingsManager (no file I/O) */ - static inMemory(settings: Partial = {}): SettingsManager { + static inMemory(settings: Partial = {}, options: SettingsManagerCreateOptions = {}): SettingsManager { const storage = new InMemorySettingsStorage(); const initialSettings = SettingsManager.migrateSettings(structuredClone(settings) as Record); storage.withLock("global", () => JSON.stringify(initialSettings, null, 2)); - return SettingsManager.fromStorage(storage); + return SettingsManager.fromStorage(storage, options); } private static loadFromStorage(storage: SettingsStorage, scope: SettingsScope, projectTrusted = true): Settings { diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index f66040bb..568d96d1 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -308,11 +308,11 @@ async function createSessionManager( } if (parsed.resume) { - initTheme(settingsManager.getTheme(), true); try { const selectedPath = await selectSession( (onProgress) => SessionManager.list(cwd, sessionDir, onProgress), (onProgress) => SessionManager.listAll(sessionDir, onProgress), + settingsManager, ); if (!selectedPath) { console.log(chalk.dim("No session selected")); diff --git a/packages/coding-agent/src/modes/interactive/theme/theme-controller.ts b/packages/coding-agent/src/modes/interactive/theme/theme-controller.ts index 9fe9e8cc..43ad620d 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme-controller.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme-controller.ts @@ -3,6 +3,7 @@ import type { SettingsManager } from "../../../core/settings-manager.ts"; import { detectTerminalBackgroundFromEnv, detectTerminalBackgroundTheme, + detectTerminalThemeForAuto, initTheme, parseAutoThemeSetting, resolveThemeSetting, @@ -37,7 +38,7 @@ export class InteractiveThemeController { const themeSetting = this.settingsManager.getThemeSetting(); const autoTheme = parseAutoThemeSetting(themeSetting); if (autoTheme) { - this.terminalTheme = await this.detectTerminalThemeForAuto(); + this.terminalTheme = await detectTerminalThemeForAuto({ ui: this.ui, timeoutMs: 100 }); this.setAutoSync(true); this.applyThemeName(this.terminalTheme === "light" ? autoTheme.lightTheme : autoTheme.darkTheme, true); return; @@ -109,16 +110,6 @@ export class InteractiveThemeController { this.ui.setTerminalColorSchemeNotifications(enabled); } - private async detectTerminalThemeForAuto(): Promise { - try { - const colorScheme = await this.ui.queryTerminalColorScheme({ timeoutMs: 100 }); - if (colorScheme) return colorScheme; - } catch { - // Fall back to OSC 11 / COLORFGBG detection when color-scheme DSR is unsupported. - } - return (await detectTerminalBackgroundTheme({ ui: this.ui, timeoutMs: 100 })).theme; - } - private applyTerminalTheme(terminalTheme: TerminalTheme): void { if (!this.autoSyncEnabled) return; this.terminalTheme = terminalTheme; diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index 58e5aac3..676bc529 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -680,11 +680,20 @@ export interface TerminalBackgroundThemeDetector { queryTerminalBackgroundColor({ timeoutMs }: { timeoutMs: number }): Promise; } +export interface TerminalAutoThemeDetector extends TerminalBackgroundThemeDetector { + queryTerminalColorScheme?({ timeoutMs }: { timeoutMs: number }): Promise; +} + export interface TerminalBackgroundThemeDetectionOptions extends TerminalThemeDetectionOptions { ui: TerminalBackgroundThemeDetector; timeoutMs: number; } +export interface TerminalAutoThemeDetectionOptions extends TerminalThemeDetectionOptions { + ui: TerminalAutoThemeDetector; + timeoutMs: number; +} + function getColorFgBgBackgroundIndex(colorfgbg: string): number | undefined { const parts = colorfgbg.split(";"); for (let i = parts.length - 1; i >= 0; i--) { @@ -755,6 +764,20 @@ export async function detectTerminalBackgroundTheme({ return detectTerminalBackgroundFromEnv({ env }); } +export async function detectTerminalThemeForAuto({ + ui, + timeoutMs, + env, +}: TerminalAutoThemeDetectionOptions): Promise { + try { + const colorScheme = await ui.queryTerminalColorScheme?.({ timeoutMs }); + if (colorScheme) return colorScheme; + } catch { + // Fall back to OSC 11 / COLORFGBG detection when color-scheme DSR is unsupported. + } + return (await detectTerminalBackgroundTheme({ ui, timeoutMs, env })).theme; +} + export function getDefaultTheme(): string { return detectTerminalBackgroundFromEnv().theme; }