From 6102dd2084276fd894600d9eabd84679da2ed1da Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 12 Jun 2026 17:56:43 +0200 Subject: [PATCH] fix(coding-agent): handle missing export themes closes #5596 --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/core/agent-session.ts | 5 +- .../5596-missing-theme-export.test.ts | 89 +++++++++++++++++++ 3 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 packages/coding-agent/test/suite/regressions/5596-missing-theme-export.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 4d3ee3c6e..eb2fa5a94 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Fixed `/share` and `/export` HTML exports to use the active fallback theme when the configured custom theme no longer exists ([#5596](https://github.com/earendil-works/pi/issues/5596)). - Fixed custom fallback model IDs with `:` suffixes to preserve the requested thinking level when the provider template model does not advertise reasoning ([#5552](https://github.com/earendil-works/pi/issues/5552)). ## [0.79.1] - 2026-06-09 diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 201b8084b..af9d32d7b 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -33,7 +33,7 @@ import { resetApiProviders, streamSimple, } from "@earendil-works/pi-ai"; -import { theme } from "../modes/interactive/theme/theme.ts"; +import { getThemeByName, theme } from "../modes/interactive/theme/theme.ts"; import { stripFrontmatter } from "../utils/frontmatter.ts"; import { resolvePath } from "../utils/paths.ts"; import { sleep } from "../utils/sleep.ts"; @@ -3017,7 +3017,8 @@ export class AgentSession { * @returns Path to exported file */ async exportToHtml(outputPath?: string): Promise { - const themeName = this.settingsManager.getTheme(); + const configuredThemeName = this.settingsManager.getTheme(); + const themeName = configuredThemeName && getThemeByName(configuredThemeName) ? configuredThemeName : undefined; // Create tool renderer if we have an extension runner (for custom tool HTML rendering) const toolRenderer: ToolHtmlRenderer = createToolHtmlRenderer({ diff --git a/packages/coding-agent/test/suite/regressions/5596-missing-theme-export.test.ts b/packages/coding-agent/test/suite/regressions/5596-missing-theme-export.test.ts new file mode 100644 index 000000000..58bc17f8b --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/5596-missing-theme-export.test.ts @@ -0,0 +1,89 @@ +import { existsSync, mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { Agent } from "@earendil-works/pi-agent-core"; +import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai"; +import { afterEach, describe, expect, it } from "vitest"; +import { AgentSession } from "../../../src/core/agent-session.ts"; +import { AuthStorage } from "../../../src/core/auth-storage.ts"; +import { convertToLlm } from "../../../src/core/messages.ts"; +import { ModelRegistry } from "../../../src/core/model-registry.ts"; +import { SessionManager } from "../../../src/core/session-manager.ts"; +import { SettingsManager } from "../../../src/core/settings-manager.ts"; +import { initTheme } from "../../../src/modes/interactive/theme/theme.ts"; +import { createTestResourceLoader } from "../../utilities.ts"; + +describe("regression #5596: missing configured theme export", () => { + const cleanups: Array<() => void> = []; + + afterEach(() => { + while (cleanups.length > 0) { + cleanups.pop()?.(); + } + initTheme("dark"); + }); + + it("exports with the active fallback theme when the configured theme is missing", async () => { + const tempDir = mkdtempSync(join(tmpdir(), "pi-5596-")); + const faux = registerFauxProvider({ + models: [{ id: "faux-1", reasoning: false }], + }); + faux.setResponses([fauxAssistantMessage("hello")]); + + const model = faux.getModel(); + const authStorage = AuthStorage.inMemory(); + authStorage.setRuntimeApiKey(model.provider, "faux-key"); + const modelRegistry = ModelRegistry.inMemory(authStorage); + modelRegistry.registerProvider(model.provider, { + baseUrl: model.baseUrl, + apiKey: "faux-key", + api: faux.api, + models: faux.models.map((registeredModel) => ({ + id: registeredModel.id, + name: registeredModel.name, + api: registeredModel.api, + reasoning: registeredModel.reasoning, + input: registeredModel.input, + cost: registeredModel.cost, + contextWindow: registeredModel.contextWindow, + maxTokens: registeredModel.maxTokens, + baseUrl: registeredModel.baseUrl, + })), + }); + + const settingsManager = SettingsManager.inMemory({ theme: "missing-theme" }); + const sessionManager = SessionManager.create(tempDir, join(tempDir, "sessions")); + const agent = new Agent({ + getApiKey: () => "faux-key", + initialState: { + model, + systemPrompt: "You are a test assistant.", + tools: [], + }, + convertToLlm, + }); + const session = new AgentSession({ + agent, + sessionManager, + settingsManager, + cwd: tempDir, + modelRegistry, + resourceLoader: createTestResourceLoader(), + }); + cleanups.push(() => { + session.dispose(); + faux.unregister(); + if (existsSync(tempDir)) { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + + await session.prompt("hi"); + initTheme(settingsManager.getTheme()); + + const outputPath = join(tempDir, "export.html"); + await expect(session.exportToHtml(outputPath)).resolves.toBe(outputPath); + expect(existsSync(outputPath)).toBe(true); + expect(settingsManager.getTheme()).toBe("missing-theme"); + }); +});