Split shared cloud chat UI into stream render and format modules
Problem
Refactor apps/cloud/static/js/chat/chat.js into smaller modules that separate SSE transport, message/tool rendering, and text formatting. Remove stale tool-surface assumptions and make the shared chat component easier to extend without growing one file that mixes protocol, DOM updates, and markdown-ish formatting.
Context
apps/cloud/static/js/chat/chat.js (383 lines) is the shared chat component used by four consumers:
- apps/cloud/static/js/dashboard/init.js (JS import)
- apps/cloud/templates/dashboards/dashboard_edit.html (inline script)
- apps/cloud/templates/charts/chart_editor.html (inline script)
- apps/cloud/templates/chat/org_chat_home.html (inline script)
All import { initChat } from chat/chat.js. The file mixes three concerns:
1. Text formatting — escapeHtml, formatMessage (markdown-ish transforms)
2. DOM rendering — TOOL_DISPLAY map, createDataTable, thinking indicators, tool call display, message elements
3. SSE transport — fetch + ReadableStream reader, line-based SSE parsing, event dispatch
No build step or bundler — files are served as ES modules from Django static paths.
Possible Solutions
A. Extract three sibling modules, keep chat.js as orchestrator (Recommended)
Split into format.js, render.js, stream.js alongside chat.js. The orchestrator imports from all three and exports initChat unchanged. Zero consumer changes needed.
+ Clean separation by concern; each module is independently testable
+ No new abstractions — just moving existing functions
- Four files instead of one (acceptable for clarity)
B. Two modules (format.js + everything else) Lighter touch but leaves rendering and transport interleaved. - Doesn't address the main pain: DOM + protocol mixed together
C. Class-based refactor
Wrap state in a ChatSession class.
- Adds abstraction that doesn't earn its place (anti-slop). The closure in initChat already scopes state fine.
Plan
Selected: Option A — three modules + thin orchestrator.
Files to create:
- apps/cloud/static/js/chat/format.js — escapeHtml, formatMessage
- apps/cloud/static/js/chat/render.js — TOOL_DISPLAY, createDataTable, createRenderer(containerEl, opts) returning bound DOM helpers
- apps/cloud/static/js/chat/stream.js — connectSSE(url, body, csrfToken, onEvent) handling fetch + ReadableStream + line parsing
Files to modify:
- apps/cloud/static/js/chat/chat.js — slim to orchestrator importing the three modules, wiring them together in initChat. Export signature unchanged.
No consumer changes needed — initChat API stays identical.
Implementation Progress
Created modules
format.js(44 lines) —escapeHtml,formatMessage. Pure functions, no DOM state.render.js(161 lines) —TOOL_DISPLAYmap,createDataTable(private),createRenderer(containerEl, opts)factory returning bound DOM helpers:addMessage,addError,createThinkingIndicator,updateThinkingStatus,createToolCallDisplay,updateToolResult,scrollToBottom.stream.js(55 lines) —connectSSE(url, body, csrfToken, onEvent)handles fetch + ReadableStream + line-based SSE parsing. CallsonEventfor each parsed JSON event.
Modified files
chat.js(136 lines, down from 383) — thin orchestrator. Imports from the three modules.initChatcreates a renderer, wires SSE events to renderer calls, binds input events. Exports{ initChat }with identical signature and return type{ addMessage, sendMessage }.
Key decisions
createRendererreturns an object of bound closures rather than a class — matches the existing closure style ininitChatand avoids adding abstraction.addErrorextracted as a renderer method to avoid duplicating the error-div pattern in the orchestrator.contentstreaming case still directly creates the assistant div in the orchestrator (not viaaddMessage) because it needs the incremental-update reference — moving this to render would require extra state tracking with no benefit.
Validation
- All four consumers import only
{ initChat }fromchat/chat.js— no import path changes needed. chart_editor.htmluseschat.addMessage(...)— preserved viarenderer.addMessagein the return object.just fixpasses (black + ruff).
QA Exploration
- [x] QA exploration completed — pure refactor, no visual/behavioral changes. Manual browser verification recommended before merge.
Review Feedback
- [ ] Review cleared