summaryrefslogtreecommitdiffhomepage
path: root/packages/ui/src
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-02-13 14:57:38 -0500
committerGitHub <[email protected]>2026-02-13 13:57:38 -0600
commitd30e91738570ee9ea06ca6f2d49bdae65b0ff3ec (patch)
treebb21f9e2ec870d36670fa8ea7ead2c31ca02faad /packages/ui/src
parent72c09e1dcceee8b38476b3541852436fa045b2be (diff)
downloadopencode-d30e91738570ee9ea06ca6f2d49bdae65b0ff3ec.tar.gz
opencode-d30e91738570ee9ea06ca6f2d49bdae65b0ff3ec.zip
fix(ui): support cmd-click links in inline code (#12552)
Diffstat (limited to 'packages/ui/src')
-rw-r--r--packages/ui/src/components/markdown.css5
-rw-r--r--packages/ui/src/components/markdown.tsx44
2 files changed, 49 insertions, 0 deletions
diff --git a/packages/ui/src/components/markdown.css b/packages/ui/src/components/markdown.css
index 68ae93bda..27c8f238d 100644
--- a/packages/ui/src/components/markdown.css
+++ b/packages/ui/src/components/markdown.css
@@ -209,3 +209,8 @@
display: block;
}
}
+
+[data-component="markdown"] a.external-link:hover > code {
+ text-decoration: underline;
+ text-underline-offset: 2px;
+}
diff --git a/packages/ui/src/components/markdown.tsx b/packages/ui/src/components/markdown.tsx
index 608db818f..4c3d56284 100644
--- a/packages/ui/src/components/markdown.tsx
+++ b/packages/ui/src/components/markdown.tsx
@@ -49,6 +49,19 @@ type CopyLabels = {
copied: string
}
+const urlPattern = /^https?:\/\/[^\s<>()`"']+$/
+
+function codeUrl(text: string) {
+ const href = text.trim().replace(/[),.;!?]+$/, "")
+ if (!urlPattern.test(href)) return
+ try {
+ const url = new URL(href)
+ return url.toString()
+ } catch {
+ return
+ }
+}
+
function createIcon(path: string, slot: string) {
const icon = document.createElement("div")
icon.setAttribute("data-component", "icon")
@@ -110,9 +123,39 @@ function setupCodeCopy(root: HTMLDivElement, labels: CopyLabels) {
wrapper.appendChild(createCopyButton(labels))
}
+ const markCodeLinks = () => {
+ const codeNodes = Array.from(root.querySelectorAll(":not(pre) > code"))
+ for (const code of codeNodes) {
+ const href = codeUrl(code.textContent ?? "")
+ const parentLink =
+ code.parentElement instanceof HTMLAnchorElement && code.parentElement.classList.contains("external-link")
+ ? code.parentElement
+ : null
+
+ if (!href) {
+ if (parentLink) parentLink.replaceWith(code)
+ continue
+ }
+
+ if (parentLink) {
+ parentLink.href = href
+ continue
+ }
+
+ const link = document.createElement("a")
+ link.href = href
+ link.className = "external-link"
+ link.target = "_blank"
+ link.rel = "noopener noreferrer"
+ code.parentNode?.replaceChild(link, code)
+ link.appendChild(code)
+ }
+ }
+
const handleClick = async (event: MouseEvent) => {
const target = event.target
if (!(target instanceof Element)) return
+
const button = target.closest('[data-slot="markdown-copy-button"]')
if (!(button instanceof HTMLButtonElement)) return
const code = button.closest('[data-component="markdown-code"]')?.querySelector("code")
@@ -132,6 +175,7 @@ function setupCodeCopy(root: HTMLDivElement, labels: CopyLabels) {
for (const block of blocks) {
ensureWrapper(block)
}
+ markCodeLinks()
const buttons = Array.from(root.querySelectorAll('[data-slot="markdown-copy-button"]'))
for (const button of buttons) {