diff --git a/ui-tui/src/components/markdown.tsx b/ui-tui/src/components/markdown.tsx
index 9a75952f54..5882ab8c7e 100644
--- a/ui-tui/src/components/markdown.tsx
+++ b/ui-tui/src/components/markdown.tsx
@@ -3,6 +3,10 @@ import type { ReactNode } from 'react'
import type { Theme } from '../theme.js'
+/** OSC 8 hyperlink — wrap-ansi / Ink keep the link active across soft line wraps. */
+const osc8 = (url: string) => '\x1b]8;;' + url + '\x1b\\'
+const OSC8_END = '\x1b]8;;\x1b\\'
+
function MdInline({ t, text }: { t: Theme; text: string }) {
const parts: ReactNode[] = []
const re = /(\[(.+?)\]\((https?:\/\/[^\s)]+)\)|\*\*(.+?)\*\*|`([^`]+)`|\*(.+?)\*|(https?:\/\/[^\s]+))/g
@@ -18,8 +22,12 @@ function MdInline({ t, text }: { t: Theme; text: string }) {
if (m[2] && m[3]) {
parts.push(
-
- {m[2]}
+
+ {osc8(m[3])}
+
+ {m[2]}
+
+ {OSC8_END}
)
} else if (m[4]) {
@@ -41,9 +49,14 @@ function MdInline({ t, text }: { t: Theme; text: string }) {
)
} else if (m[7]) {
+ const u = m[7]
parts.push(
-
- {m[7]}
+
+ {osc8(u)}
+
+ {u}
+
+ {OSC8_END}
)
}
diff --git a/ui-tui/src/lib/text.ts b/ui-tui/src/lib/text.ts
index 264d741fde..44d6a31522 100644
--- a/ui-tui/src/lib/text.ts
+++ b/ui-tui/src/lib/text.ts
@@ -5,7 +5,7 @@ const ANSI_RE = /\x1b\[[0-9;]*m/g
export const stripAnsi = (s: string) => s.replace(ANSI_RE, '')
-export const hasAnsi = (s: string) => s.includes('\x1b[')
+export const hasAnsi = (s: string) => s.includes('\x1b[') || s.includes('\x1b]')
const renderEstimateLine = (line: string) => {
const trimmed = line.trim()