feat(desktop): show open-with affordance on URLs rendered as inline code

Extend the injection effect in AssistantMessage to also append a ▾ trigger
after inline <code> elements whose text is a browser-localhost or remote URL,
so AI-written run URLs like \`http://localhost:9527/\` get the same open-with
menu that markdown <a> links already receive.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
程序员阿江(Relakkes) 2026-05-29 16:22:57 +08:00
parent 3f1617d139
commit 4f03b8f6e6
2 changed files with 62 additions and 5 deletions

View File

@ -130,3 +130,46 @@ describe('AssistantMessage open-with trigger injection', () => {
expect(screen.getByText('openWith.systemBrowser')).toBeInTheDocument()
})
})
describe('AssistantMessage open-with trigger injection — inline code URLs', () => {
it('injects a ▾ trigger next to an inline-code localhost URL', async () => {
render(
<AssistantMessage
sessionId="s1"
content={'运行地址 `http://localhost:9527/`'}
isStreaming={false}
/>,
)
// The trigger comes from the <code> path (no <a> in this content)
expect(screen.getByLabelText('打开方式')).toBeInTheDocument()
})
it('clicking the trigger next to inline-code URL opens menu with in-app-browser and system-browser items', async () => {
render(
<AssistantMessage
sessionId="s1"
content={'运行地址 `http://localhost:9527/`'}
isStreaming={false}
/>,
)
const trigger = screen.getByLabelText('打开方式')
fireEvent.click(trigger)
await waitFor(() => {
expect(screen.getByText('openWith.inAppBrowser')).toBeInTheDocument()
})
expect(screen.getByText('openWith.systemBrowser')).toBeInTheDocument()
})
it('does NOT inject a trigger for non-URL inline code', () => {
render(
<AssistantMessage
sessionId="s1"
content={'装一下 `npm install`'}
isStreaming={false}
/>,
)
expect(screen.queryByLabelText('打开方式')).toBeNull()
})
})

View File

@ -52,10 +52,8 @@ export const AssistantMessage = memo(function AssistantMessage({ content, isStre
useEffect(() => {
const root = contentRef.current
if (!root || !sessionId || isStreaming) return
root.querySelectorAll<HTMLAnchorElement>('a[href]').forEach((link) => {
const href = link.getAttribute('href') ?? ''
if (classifyPreviewLink(href).kind === 'ignored') return
if (link.nextElementSibling instanceof HTMLElement && link.nextElementSibling.classList.contains('md-open-with')) return
function makeTrigger(href: string): HTMLButtonElement {
const trigger = document.createElement('button')
trigger.type = 'button'
trigger.className = 'md-open-with'
@ -64,7 +62,23 @@ export const AssistantMessage = memo(function AssistantMessage({ content, isStre
trigger.tabIndex = -1
trigger.textContent = '▾'
trigger.style.cssText = 'margin-left:3px;padding:0 3px;border:none;background:transparent;color:var(--color-text-tertiary);cursor:pointer;font-size:11px;line-height:1;vertical-align:middle'
link.after(trigger)
return trigger
}
root.querySelectorAll<HTMLAnchorElement>('a[href]').forEach((link) => {
const href = link.getAttribute('href') ?? ''
if (classifyPreviewLink(href).kind === 'ignored') return
if (link.nextElementSibling instanceof HTMLElement && link.nextElementSibling.classList.contains('md-open-with')) return
link.after(makeTrigger(href))
})
root.querySelectorAll<HTMLElement>('code').forEach((code) => {
if (code.closest('pre')) return
const text = (code.textContent ?? '').trim()
const kind = classifyPreviewLink(text).kind
if (kind !== 'browser-localhost' && kind !== 'remote') return
if (code.nextElementSibling instanceof HTMLElement && code.nextElementSibling.classList.contains('md-open-with')) return
code.after(makeTrigger(text))
})
}, [content, isStreaming, sessionId])