From bf345d9920977c20ae2a01fc5184c3cc6823c092 Mon Sep 17 00:00:00 2001 From: Paulo Camopy <29547465+camopy@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:37:05 -0300 Subject: [PATCH] fix(coding-agent): avoid placeholder crash before theme init --- packages/coding-agent/CHANGELOG.md | 4 +++ .../modes/components/custom-editor.test.ts | 27 +++++++++++++++++++ .../src/modes/components/custom-editor.ts | 6 ++--- .../coding-agent/src/modes/theme/theme.ts | 5 ++++ 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 325375f74..22e5fccd1 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed paste and image placeholders crashing when the editor renders before theme initialization. + ## [15.13.0] - 2026-06-14 ### Breaking Changes diff --git a/packages/coding-agent/src/modes/components/custom-editor.test.ts b/packages/coding-agent/src/modes/components/custom-editor.test.ts index 979d29dc0..71d50cfa0 100644 --- a/packages/coding-agent/src/modes/components/custom-editor.test.ts +++ b/packages/coding-agent/src/modes/components/custom-editor.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeAll, describe, expect, it, vi } from "bun:test"; +import { $ } from "bun"; import { getEditorTheme, initTheme } from "../theme/theme"; import { CustomEditor, SPACE_HOLD_RELEASE_MS, SPACE_HOLD_THRESHOLD } from "./custom-editor"; @@ -15,6 +16,32 @@ function holdSpace(editor: CustomEditor, count: number): void { for (let i = 0; i < count; i++) editor.handleInput(" "); } +async function decorateInFreshProcess(text: string): Promise { + const customEditorUrl = new URL("./custom-editor.ts", import.meta.url).href; + const script = ` +import { CustomEditor } from ${JSON.stringify(customEditorUrl)}; +const editor = new CustomEditor({}); +process.stdout.write(editor.decorateText(${JSON.stringify(text)})); +`; + const child = await $`bun -e ${script}`.quiet().nothrow(); + const stdout = child.stdout.toString(); + const stderr = child.stderr.toString(); + if (child.exitCode !== 0) throw new Error(stderr || stdout || `decorate subprocess exited with ${child.exitCode}`); + return stdout; +} + +describe("CustomEditor placeholder decoration", () => { + it("renders paste placeholders before theme initialization", async () => { + const output = await decorateInFreshProcess("[Paste #1, +30 lines]"); + expect(output).toBe("[Paste #1, +30 lines]"); + }); + + it("renders image placeholders before theme initialization", async () => { + const output = await decorateInFreshProcess("[Image #1]"); + expect(output).toBe("[Image #1]"); + }); +}); + describe("CustomEditor space-hold push-to-talk", () => { beforeAll(async () => { await initTheme(); diff --git a/packages/coding-agent/src/modes/components/custom-editor.ts b/packages/coding-agent/src/modes/components/custom-editor.ts index 9f365c807..6e74e9c9c 100644 --- a/packages/coding-agent/src/modes/components/custom-editor.ts +++ b/packages/coding-agent/src/modes/components/custom-editor.ts @@ -3,7 +3,7 @@ import type { AppKeybinding } from "../../config/keybindings"; import { isSettingsInitialized, settings } from "../../config/settings"; import { imageReferenceHyperlink, PLACEHOLDER_REGEX, renderPlaceholders } from "../image-references"; import { hasMagicKeyword, highlightMagicKeywords } from "../magic-keywords"; -import { theme } from "../theme/theme"; +import { fgOrPlain } from "../theme/theme"; type ConfigurableEditorAction = Extract< AppKeybinding, @@ -178,9 +178,9 @@ export class CustomEditor extends Editor { renderReference: (value, kind, index) => kind === "image" ? imageReferenceHyperlink(value, index, this.imageLinks, label => - theme.fg("accent", `\x1b[1m\x1b[4m${label}\x1b[24m\x1b[22m`), + fgOrPlain("accent", label, `\x1b[1m\x1b[4m${label}\x1b[24m\x1b[22m`), ) - : theme.fg("accent", `\x1b[1m${value}\x1b[22m`), + : fgOrPlain("accent", value, `\x1b[1m${value}\x1b[22m`), }); }; diff --git a/packages/coding-agent/src/modes/theme/theme.ts b/packages/coding-agent/src/modes/theme/theme.ts index 130f09b03..7ab65b264 100644 --- a/packages/coding-agent/src/modes/theme/theme.ts +++ b/packages/coding-agent/src/modes/theme/theme.ts @@ -2098,6 +2098,11 @@ var currentThemeName: string | undefined; export function getCurrentThemeName(): string | undefined { return currentThemeName; } + +/** Returns unstyled `text` before `initTheme()` assigns the global theme; use only for early-render paths. */ +export function fgOrPlain(color: ThemeColor, text: string, styledText: string = text): string { + return typeof theme === "undefined" ? text : theme.fg(color, styledText); +} var currentSymbolPresetOverride: SymbolPreset | undefined; var currentColorBlindMode: boolean = false; var themeWatcher: fs.FSWatcher | undefined;