Merge pull request #2581 from camopy/codex/avoid-placeholder-crash-before-theme-init

fix(coding-agent): avoid placeholder crash before theme init

Resolved CHANGELOG conflict: 15.13.0 was re-opened into [Unreleased] on
main, so the fix entry goes under the existing [Unreleased] Fixed section
rather than resurrecting the released 15.13.0 heading.
This commit is contained in:
can1357
2026-06-14 20:44:03 +02:00
4 changed files with 36 additions and 3 deletions
+1
View File
@@ -174,6 +174,7 @@
- Added env-driven OpenTelemetry trace export. When `OTEL_EXPORTER_OTLP_ENDPOINT` (or `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`) is set, `omp` registers a global OTLP/proto trace exporter and switches on the agent loop's telemetry, so the `invoke_agent` / `chat` / `execute_tool` spans actually reach a collector instead of a no-op tracer. Honors the standard `OTEL_*` env contract (endpoint, headers, `OTEL_SERVICE_NAME`, `OTEL_SDK_DISABLED` and `OTEL_TRACES_EXPORTER=none` parsed case-insensitively) and the `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` capture toggle; it is a no-op when no endpoint is configured. Only the `http/protobuf` transport is supported — a `grpc` or `http/json` `OTEL_EXPORTER_OTLP*_PROTOCOL` declines rather than misrouting spans. This makes the existing telemetry usable from headless hosts that run `omp` as a spawned child process, where an in-process `TracerProvider` registered by the parent can't reach the child. Uses the `@opentelemetry/exporter-trace-otlp-proto` 2.x line, which exports cleanly under Bun.
## Fixed
- Fixed paste and image placeholders crashing when the editor renders before theme initialization.
- Fixed the status line session name (and the editor border / status-line gap fill) being nearly illegible on light themes.
- Added `IndexedSessionStorage` and `SessionStorageBackend` exports to support shared metadata-indexed session backends
- Added the `tui.maxInlineImages` setting (default `8`) capping how many inline images render as live terminal graphics. Once a new image pushes the count past the cap, the oldest images are hidden via a full redraw — replaced by their `[Image: …]` text placeholder and purged from the terminal's graphics store — so long sessions with many screenshots/diagrams stop piling up images (and, on Kitty, stop leaving scrollback ghosts). Set to `0` to keep every image inline.
@@ -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<string> {
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();
@@ -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`),
});
};
@@ -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;