Problem
chatbot.js calls escapeHtml at three places and formatMarkdown at one. Neither is defined anywhere in the file:
$ grep -nE "^\s*(function|const|let|var)\s+(escapeHtml|formatMarkdown)\b" frontend/docs_scripts/chatbot.js
(no output)
$ grep -n "escapeHtml(\|formatMarkdown(" frontend/docs_scripts/chatbot.js
934: const formattedText = formatMarkdown(currentMessageContent.trim());
1257: <span class="flo-tool-status-text">${escapeHtml(text)}</span>
1882: <span class="citation-pill-title">${escapeHtml(info.title)}</span>
1883: <span class="citation-pill-sub">${escapeHtml(info.domain)}</span>
Both were real functions until #234 (b24399e), which hoisted them out of the DOMContentLoaded closure and renamed them to escapeMarkdownHtml and formatChatMarkdown, alongside the new tests/test_widget_markdown.py. #234 converted every call site with them: at b24399e the file contains no reference to either old name. All four call sites above arrive at 9a1e5ab (#237).
What breaks
The tool-status pill, on every tool-using turn. setToolStatus (:1257) builds its markup with escapeHtml, so the "Searching Kubeflow documentation..." indicator #237 added never mounts, whether or not the turn returns citations.
Citations, and the saved chat. In the SSE handler the final event does:
if (currentMessageDiv && pendingCitations.length > 0) {
renderCitationsOnDiv(currentMessageDiv, pendingCitations); // ReferenceError: escapeHtml
}
if (currentMessageContent.trim()) {
messagesHistory.push({ role: 'assistant', content: ... }); // never reached
}
...
autoSaveCurrentChat(); // never reached
renderCitationsOnDiv builds each pill with escapeHtml, so it throws on the first citation. The block sits inside try { ... } catch (parseError) { // Ignore partial / heartbeat lines }, which swallows the ReferenceError with no console output. The Sources accordion never mounts, and because the throw lands before messagesHistory.push and autoSaveCurrentChat(), the reply stays on screen but is never written to the saved chat, so it is gone on reload or chat switch. The agent's own context is server-side via contextId and is unaffected.
Stop generation. stopGeneration renders the partial answer with formatMarkdown (:934) before adding the interrupted badge and pushing to history, so the badge never appears, the partial is never saved, and currentMessageDiv / currentMessageContent are left uncleared. This path is not silent: stopGeneration is async and the click handler at :1500 drops the promise, so it surfaces as an unhandled rejection.
Why CI does not see it
tests/test_widget_markdown.py loads the file by splitting on document.addEventListener('DOMContentLoaded' and evaluating only the prelude. All four call sites are inside that closure, so nothing in the repo evaluates them. node --check passes, since an undefined reference is a runtime error rather than a syntax error.
Suggested fix
Point the call sites at the helpers that exist, and add a check over the whole file that every bare name( call resolves to something the file declares or a known global.
Related: #242 resolves the escapeHtml half by adding an alias, as part of the XSS work for #175. It does not cover formatMarkdown.
P.S. I'll shortly open a PR addressing this.
Problem
chatbot.jscallsescapeHtmlat three places andformatMarkdownat one. Neither is defined anywhere in the file:Both were real functions until #234 (
b24399e), which hoisted them out of theDOMContentLoadedclosure and renamed them toescapeMarkdownHtmlandformatChatMarkdown, alongside the newtests/test_widget_markdown.py. #234 converted every call site with them: atb24399ethe file contains no reference to either old name. All four call sites above arrive at9a1e5ab(#237).What breaks
The tool-status pill, on every tool-using turn.
setToolStatus(:1257) builds its markup withescapeHtml, so the "Searching Kubeflow documentation..." indicator #237 added never mounts, whether or not the turn returns citations.Citations, and the saved chat. In the SSE handler the final event does:
renderCitationsOnDivbuilds each pill withescapeHtml, so it throws on the first citation. The block sits insidetry { ... } catch (parseError) { // Ignore partial / heartbeat lines }, which swallows the ReferenceError with no console output. The Sources accordion never mounts, and because the throw lands beforemessagesHistory.pushandautoSaveCurrentChat(), the reply stays on screen but is never written to the saved chat, so it is gone on reload or chat switch. The agent's own context is server-side viacontextIdand is unaffected.Stop generation.
stopGenerationrenders the partial answer withformatMarkdown(:934) before adding the interrupted badge and pushing to history, so the badge never appears, the partial is never saved, andcurrentMessageDiv/currentMessageContentare left uncleared. This path is not silent:stopGenerationisasyncand the click handler at:1500drops the promise, so it surfaces as an unhandled rejection.Why CI does not see it
tests/test_widget_markdown.pyloads the file by splitting ondocument.addEventListener('DOMContentLoaded'and evaluating only the prelude. All four call sites are inside that closure, so nothing in the repo evaluates them.node --checkpasses, since an undefined reference is a runtime error rather than a syntax error.Suggested fix
Point the call sites at the helpers that exist, and add a check over the whole file that every bare
name(call resolves to something the file declares or a known global.Related: #242 resolves the
escapeHtmlhalf by adding an alias, as part of the XSS work for #175. It does not coverformatMarkdown.P.S. I'll shortly open a PR addressing this.