diff --git a/webclient/src/components/artifacts/ArtifactsPanel.vue b/webclient/src/components/artifacts/ArtifactsPanel.vue index a62a0a0..2e926be 100644 --- a/webclient/src/components/artifacts/ArtifactsPanel.vue +++ b/webclient/src/components/artifacts/ArtifactsPanel.vue @@ -367,14 +367,15 @@ const seen = new Set() const out = [] + // Pass 1: Markdown links have priority — they carry a title / caption. + // We scan all messages first so a bare URL from a newer message never + // shadows a markdown link from an older one. for (let i = chat.messages.length - 1; i >= 0; i--) { const m = chat.messages[i] if (m.role !== 'assistant') continue const text = m.text || '' let match - // Markdown links — fresh regex per message so lastIndex doesn't leak across - // messages or across computed re-evaluations during streaming. const mdLinkRe = /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g while ((match = mdLinkRe.exec(text)) !== null) { let url = match[2].trim() @@ -388,17 +389,21 @@ domain: domainOf(url), }) } + } - // Bare URLs — fresh regex per message for the same reason. - // Trailing punctuation is stripped both from the extracted URL - // and from the dedup key so "https://a/b)" and "https://a/b" - // are treated as the same link. + // Pass 2: Bare URLs — only URLs we have not already seen as markdown links. + for (let i = chat.messages.length - 1; i >= 0; i--) { + const m = chat.messages[i] + if (m.role !== 'assistant') continue + const text = m.text || '' + let match + const bareUrlRe = /https?:\/\/[^\s<>"{}|\\^`[\]]+/g while ((match = bareUrlRe.exec(text)) !== null) { let url = match[0].trim() const norm = normalizeUrlForDedup(url) if (seen.has(norm)) continue - // Rough guard: skip if this bare URL is the href part of a markdown link + // Skip bare URLs that are actually the href part of a markdown link const before = text.slice(Math.max(0, match.index - 1), match.index) if (before === '(' && text[match.index + match[0].length] === ')') { const openBracket = text.lastIndexOf('[', match.index - 2) @@ -415,6 +420,7 @@ }) } } + return out })