fix(coding-agent): load resume startup themes

This commit is contained in:
Armin Ronacher
2026-06-22 17:18:43 +02:00
parent 02540acd17
commit aa6bdd778a
7 changed files with 112 additions and 33 deletions
+4
View File
@@ -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`. - 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 ## [0.79.10] - 2026-06-22
### New Features ### New Features
@@ -2,10 +2,12 @@
* TUI session selector for --resume flag * 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 { KeybindingsManager } from "../core/keybindings.ts";
import type { SessionInfo, SessionListProgress } from "../core/session-manager.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 { SessionSelectorComponent } from "../modes/interactive/components/session-selector.ts";
import { createStartupTui, startStartupTui } from "./startup-ui.ts";
type SessionsLoader = (onProgress?: SessionListProgress) => Promise<SessionInfo[]>; type SessionsLoader = (onProgress?: SessionListProgress) => Promise<SessionInfo[]>;
@@ -13,9 +15,10 @@ type SessionsLoader = (onProgress?: SessionListProgress) => Promise<SessionInfo[
export async function selectSession( export async function selectSession(
currentSessionsLoader: SessionsLoader, currentSessionsLoader: SessionsLoader,
allSessionsLoader: SessionsLoader, allSessionsLoader: SessionsLoader,
settingsManager: SettingsManager,
): Promise<string | null> { ): Promise<string | null> {
const ui = await createStartupTui(settingsManager);
return new Promise((resolve) => { return new Promise((resolve) => {
const ui = new TUI(new ProcessTerminal());
const keybindings = KeybindingsManager.create(); const keybindings = KeybindingsManager.create();
setKeybindings(keybindings); setKeybindings(keybindings);
let resolved = false; let resolved = false;
@@ -47,6 +50,6 @@ export async function selectSession(
ui.addChild(selector); ui.addChild(selector);
ui.setFocus(selector.getSessionList()); ui.setFocus(selector.getSessionList());
ui.start(); startStartupTui(ui, settingsManager);
}); });
} }
+74 -16
View File
@@ -1,16 +1,27 @@
import { ProcessTerminal, setKeybindings, TUI } from "@earendil-works/pi-tui"; import { ProcessTerminal, setKeybindings, TUI } from "@earendil-works/pi-tui";
import { existsSync } from "fs"; 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 { areExperimentalFeaturesEnabled } from "../core/experimental.ts";
import { KeybindingsManager } from "../core/keybindings.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 { ExtensionInputComponent } from "../modes/interactive/components/extension-input.ts";
import { ExtensionSelectorComponent } from "../modes/interactive/components/extension-selector.ts"; import { ExtensionSelectorComponent } from "../modes/interactive/components/extension-selector.ts";
import { import {
FirstTimeSetupComponent, FirstTimeSetupComponent,
type FirstTimeSetupResult, type FirstTimeSetupResult,
} from "../modes/interactive/components/first-time-setup.ts"; } 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_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
const OFFICIAL_APP_NAME = "pi"; const OFFICIAL_APP_NAME = "pi";
@@ -30,14 +41,64 @@ function isOfficialDistribution({ packageName, appName, configDirName }: Distrib
); );
} }
function createStartupTui(settingsManager: SettingsManager): TUI { function loadThemes(resources: ResolvedResource[]): Theme[] {
initTheme(settingsManager.getTheme()); const themes: Theme[] = [];
const seen = new Set<string>();
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<Theme[]> {
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<TUI> {
setRegisteredThemes(await loadStartupThemes(settingsManager));
const terminalTheme = detectTerminalBackgroundFromEnv().theme;
initTheme(resolveThemeSetting(settingsManager.getThemeSetting(), terminalTheme) ?? terminalTheme);
setKeybindings(KeybindingsManager.create()); setKeybindings(KeybindingsManager.create());
const ui = new TUI(new ProcessTerminal(), settingsManager.getShowHardwareCursor()); const ui = new TUI(new ProcessTerminal(), settingsManager.getShowHardwareCursor());
ui.setClearOnShrink(settingsManager.getClearOnShrink()); ui.setClearOnShrink(settingsManager.getClearOnShrink());
return ui; return ui;
} }
export function startStartupTui(ui: TUI, settingsManager: SettingsManager): void {
ui.start();
void applyDetectedStartupTheme(ui, settingsManager);
}
async function applyDetectedStartupTheme(ui: TUI, settingsManager: SettingsManager): Promise<void> {
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<void> { async function clearStartupTui(ui: TUI): Promise<void> {
ui.clear(); ui.clear();
ui.requestRender(); ui.requestRender();
@@ -75,9 +136,8 @@ export async function showStartupSelector<T>(
title: string, title: string,
options: Array<{ label: string; value: T }>, options: Array<{ label: string; value: T }>,
): Promise<T | undefined> { ): Promise<T | undefined> {
const ui = await createStartupTui(settingsManager);
return new Promise((resolve) => { return new Promise((resolve) => {
const ui = createStartupTui(settingsManager);
let settled = false; let settled = false;
const finish = async (result: T | undefined) => { const finish = async (result: T | undefined) => {
if (settled) { if (settled) {
@@ -98,15 +158,14 @@ export async function showStartupSelector<T>(
); );
ui.addChild(selector); ui.addChild(selector);
ui.setFocus(selector); ui.setFocus(selector);
ui.start(); startStartupTui(ui, settingsManager);
}); });
} }
/** Show the first-time setup dialog and persist the result */ /** Show the first-time setup dialog and persist the result */
export async function showFirstTimeSetup(settingsManager: SettingsManager): Promise<void> { export async function showFirstTimeSetup(settingsManager: SettingsManager): Promise<void> {
const ui = await createStartupTui(settingsManager);
return new Promise((resolve) => { return new Promise((resolve) => {
const ui = createStartupTui(settingsManager);
let settled = false; let settled = false;
const finish = async (result: FirstTimeSetupResult | undefined) => { const finish = async (result: FirstTimeSetupResult | undefined) => {
if (settled) { if (settled) {
@@ -125,10 +184,10 @@ export async function showFirstTimeSetup(settingsManager: SettingsManager): Prom
const showSetup = async () => { const showSetup = async () => {
ui.start(); ui.start();
const detection = await detectTerminalBackgroundTheme({ ui, timeoutMs: 100 }); const detectedTheme = await detectTerminalThemeForAuto({ ui, timeoutMs: 100 });
setTheme(detection.theme); setTheme(detectedTheme);
const component = new FirstTimeSetupComponent({ const component = new FirstTimeSetupComponent({
detectedTheme: detection.theme, detectedTheme,
onThemePreview: (themeName) => { onThemePreview: (themeName) => {
setTheme(themeName); setTheme(themeName);
ui.requestRender(); ui.requestRender();
@@ -150,9 +209,8 @@ export async function showStartupInput(
title: string, title: string,
placeholder?: string, placeholder?: string,
): Promise<string | undefined> { ): Promise<string | undefined> {
const ui = await createStartupTui(settingsManager);
return new Promise((resolve) => { return new Promise((resolve) => {
const ui = createStartupTui(settingsManager);
let settled = false; let settled = false;
const finish = async (result: string | undefined) => { const finish = async (result: string | undefined) => {
if (settled) { if (settled) {
@@ -176,6 +234,6 @@ export async function showStartupInput(
); );
ui.addChild(input); ui.addChild(input);
ui.setFocus(input); ui.setFocus(input);
ui.start(); startStartupTui(ui, settingsManager);
}); });
} }
@@ -334,11 +334,11 @@ export class SettingsManager {
} }
/** Create an in-memory SettingsManager (no file I/O) */ /** Create an in-memory SettingsManager (no file I/O) */
static inMemory(settings: Partial<Settings> = {}): SettingsManager { static inMemory(settings: Partial<Settings> = {}, options: SettingsManagerCreateOptions = {}): SettingsManager {
const storage = new InMemorySettingsStorage(); const storage = new InMemorySettingsStorage();
const initialSettings = SettingsManager.migrateSettings(structuredClone(settings) as Record<string, unknown>); const initialSettings = SettingsManager.migrateSettings(structuredClone(settings) as Record<string, unknown>);
storage.withLock("global", () => JSON.stringify(initialSettings, null, 2)); 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 { private static loadFromStorage(storage: SettingsStorage, scope: SettingsScope, projectTrusted = true): Settings {
+1 -1
View File
@@ -308,11 +308,11 @@ async function createSessionManager(
} }
if (parsed.resume) { if (parsed.resume) {
initTheme(settingsManager.getTheme(), true);
try { try {
const selectedPath = await selectSession( const selectedPath = await selectSession(
(onProgress) => SessionManager.list(cwd, sessionDir, onProgress), (onProgress) => SessionManager.list(cwd, sessionDir, onProgress),
(onProgress) => SessionManager.listAll(sessionDir, onProgress), (onProgress) => SessionManager.listAll(sessionDir, onProgress),
settingsManager,
); );
if (!selectedPath) { if (!selectedPath) {
console.log(chalk.dim("No session selected")); console.log(chalk.dim("No session selected"));
@@ -3,6 +3,7 @@ import type { SettingsManager } from "../../../core/settings-manager.ts";
import { import {
detectTerminalBackgroundFromEnv, detectTerminalBackgroundFromEnv,
detectTerminalBackgroundTheme, detectTerminalBackgroundTheme,
detectTerminalThemeForAuto,
initTheme, initTheme,
parseAutoThemeSetting, parseAutoThemeSetting,
resolveThemeSetting, resolveThemeSetting,
@@ -37,7 +38,7 @@ export class InteractiveThemeController {
const themeSetting = this.settingsManager.getThemeSetting(); const themeSetting = this.settingsManager.getThemeSetting();
const autoTheme = parseAutoThemeSetting(themeSetting); const autoTheme = parseAutoThemeSetting(themeSetting);
if (autoTheme) { if (autoTheme) {
this.terminalTheme = await this.detectTerminalThemeForAuto(); this.terminalTheme = await detectTerminalThemeForAuto({ ui: this.ui, timeoutMs: 100 });
this.setAutoSync(true); this.setAutoSync(true);
this.applyThemeName(this.terminalTheme === "light" ? autoTheme.lightTheme : autoTheme.darkTheme, true); this.applyThemeName(this.terminalTheme === "light" ? autoTheme.lightTheme : autoTheme.darkTheme, true);
return; return;
@@ -109,16 +110,6 @@ export class InteractiveThemeController {
this.ui.setTerminalColorSchemeNotifications(enabled); this.ui.setTerminalColorSchemeNotifications(enabled);
} }
private async detectTerminalThemeForAuto(): Promise<TerminalTheme> {
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 { private applyTerminalTheme(terminalTheme: TerminalTheme): void {
if (!this.autoSyncEnabled) return; if (!this.autoSyncEnabled) return;
this.terminalTheme = terminalTheme; this.terminalTheme = terminalTheme;
@@ -680,11 +680,20 @@ export interface TerminalBackgroundThemeDetector {
queryTerminalBackgroundColor({ timeoutMs }: { timeoutMs: number }): Promise<RgbColor | undefined>; queryTerminalBackgroundColor({ timeoutMs }: { timeoutMs: number }): Promise<RgbColor | undefined>;
} }
export interface TerminalAutoThemeDetector extends TerminalBackgroundThemeDetector {
queryTerminalColorScheme?({ timeoutMs }: { timeoutMs: number }): Promise<TerminalTheme | undefined>;
}
export interface TerminalBackgroundThemeDetectionOptions extends TerminalThemeDetectionOptions { export interface TerminalBackgroundThemeDetectionOptions extends TerminalThemeDetectionOptions {
ui: TerminalBackgroundThemeDetector; ui: TerminalBackgroundThemeDetector;
timeoutMs: number; timeoutMs: number;
} }
export interface TerminalAutoThemeDetectionOptions extends TerminalThemeDetectionOptions {
ui: TerminalAutoThemeDetector;
timeoutMs: number;
}
function getColorFgBgBackgroundIndex(colorfgbg: string): number | undefined { function getColorFgBgBackgroundIndex(colorfgbg: string): number | undefined {
const parts = colorfgbg.split(";"); const parts = colorfgbg.split(";");
for (let i = parts.length - 1; i >= 0; i--) { for (let i = parts.length - 1; i >= 0; i--) {
@@ -755,6 +764,20 @@ export async function detectTerminalBackgroundTheme({
return detectTerminalBackgroundFromEnv({ env }); return detectTerminalBackgroundFromEnv({ env });
} }
export async function detectTerminalThemeForAuto({
ui,
timeoutMs,
env,
}: TerminalAutoThemeDetectionOptions): Promise<TerminalTheme> {
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 { export function getDefaultTheme(): string {
return detectTerminalBackgroundFromEnv().theme; return detectTerminalBackgroundFromEnv().theme;
} }