From d99a6489dd0b3ff642b19b31cc5b5b8a429fc4d8 Mon Sep 17 00:00:00 2001 From: roboomp Date: Fri, 19 Jun 2026 18:14:40 +0000 Subject: [PATCH] fix(tui): guarded hyperlinks before settings init Return plain text from hyperlink helpers until Settings.init() has completed so image paste placeholders cannot crash early editor renders. Fixes #3064 --- packages/coding-agent/CHANGELOG.md | 4 ++++ packages/coding-agent/src/tui/hyperlink.ts | 9 ++++++--- packages/coding-agent/test/tui/hyperlink.test.ts | 11 +++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index caafcff95..e0b943f8c 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed image paste placeholders falling through to terminal hyperlink settings before `Settings.init()`, so early editor rendering falls back to plain text instead of crashing. ([#3064](https://github.com/can1357/oh-my-pi/issues/3064)) + ## [16.1.3] - 2026-06-19 ### Changed diff --git a/packages/coding-agent/src/tui/hyperlink.ts b/packages/coding-agent/src/tui/hyperlink.ts index d5c161469..fabcac16e 100644 --- a/packages/coding-agent/src/tui/hyperlink.ts +++ b/packages/coding-agent/src/tui/hyperlink.ts @@ -7,7 +7,7 @@ */ import * as url from "node:url"; import { TERMINAL } from "@oh-my-pi/pi-tui"; -import { settings } from "../config/settings"; +import { isSettingsInitialized, settings } from "../config/settings"; import { LocalProtocolHandler, memoryRootsFromRegistry, @@ -45,8 +45,10 @@ function buildFileUri(filePath: string, opts?: { line?: number; col?: number }): * - `"off"`: never * - `"auto"`: when `process.stdout.isTTY`, `NO_COLOR` is unset, and the detected terminal reports hyperlink support * - `"always"`: unconditionally (useful for viewers that support OSC 8 without advertising it) + * Before settings initialization, returns false so early render paths stay plain text. */ export function isHyperlinkEnabled(): boolean { + if (!isSettingsInitialized()) return false; const mode = settings.get("tui.hyperlinks"); if (mode === "off") return false; if (mode === "always") return true; @@ -104,10 +106,11 @@ export function urlHyperlink(url: string, displayText: string): string { * Wrap `displayText` in an OSC 8 hyperlink pointing at an HTTP(S) URL, * bypassing terminal capability auto-detection. Used for auth prompts where * an inert "click" label blocks login on terminals whose capabilities are - * not advertised. Still returns plain text when the user has explicitly - * opted out via `tui.hyperlinks=off`. + * not advertised. Still returns plain text before settings initialization or + * when the user has explicitly opted out via `tui.hyperlinks=off`. */ export function urlHyperlinkAlways(url: string, displayText: string): string { + if (!isSettingsInitialized()) return displayText; if (settings.get("tui.hyperlinks") === "off") return displayText; const normalized = url.match(/^www\./i) ? `https://${url}` : url; try { diff --git a/packages/coding-agent/test/tui/hyperlink.test.ts b/packages/coding-agent/test/tui/hyperlink.test.ts index aba751235..b4d21b612 100644 --- a/packages/coding-agent/test/tui/hyperlink.test.ts +++ b/packages/coding-agent/test/tui/hyperlink.test.ts @@ -60,6 +60,17 @@ afterEach(() => { }); describe("isHyperlinkEnabled", () => { + it("falls back to plain text before Settings.init", async () => { + resetSettingsForTest(); + try { + expect(isHyperlinkEnabled()).toBe(false); + expect(fileHyperlink(path.resolve("/Users/foo/bar.ts"), "bar.ts")).toBe("bar.ts"); + expect(urlHyperlinkAlways("https://example.com/path", "example")).toBe("example"); + } finally { + await Settings.init({ inMemory: true }); + } + }); + it('returns false when mode is "off"', () => { setHyperlinkMode("off"); expect(isHyperlinkEnabled()).toBe(false);