Skip to content

bug(frontend): the chat widget calls two functions that no longer exist #247

Description

@Neilblaze

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions