Thursday, 9 July 2026

Roject — Session Summary

RojoChatPanel — a streaming AI chat panel for the editor, with LangChain backend, markdown rendering, and tab container integration.

What we built

RojoChatPanel — Backend

A new /api/rojos/chat route streams AI responses from an OpenAI-compatible model (e.g. a local LLAMA). The backend is split into three files under server/rojos/:

  • RojosConfig.ts — hardcoded config wrapper (baseURL, model, apiKey). Designed to grow as more Rojo settings are added.
  • RojosAgent.ts — in-memory session store (Map of conversation ID → message history). Uses ChatOpenAI.stream() from @langchain/openai with messages converted to HumanMessage / AIMessage via @langchain/core.
  • server/routes/rojos.ts — Express router, POST /chat, requires auth. Iterates the agent stream with for await, writes newline-delimited JSON chunks ({ type:"CHAT", text }, { type:"THINKING" }, { type:"DONE" }) directly via res.write(), then calls res.end(). After streaming, calls updateAgentConversation so the assistant reply is appended to the session history.

The route is mounted at /api/rojos in server/index.ts, keeping the namespace open for future Rojo settings endpoints.

server/rojos/RojosConfig.ts server/rojos/RojosAgent.ts server/routes/rojos.ts @langchain/openai @langchain/core SSE / res.write

RojoChatPanel — Frontend Component

A new rojo-chat-panel custom element following the same pattern as html-editor-panel. The element has three zones:

  • Toolbar — robot emoji icon, conversation name label, two placeholder buttons (actions ★, settings ☰).
  • Chat history — scrolling div; user messages are right-aligned bubble, assistant messages are left-aligned and rendered with markdown-it (live as chunks arrive).
  • Input areacontenteditable div (3-line height), a placeholder + button, and a send button. Enter sends; Shift+Enter inserts a newline.

Each panel instance generates a fresh crypto.randomUUID() conversation ID on connect. Raw text is extracted from the contenteditable with an inlined recursive walker (same logic as DOMEditor.extractRawText). markdown-it is loaded as a UMD global from /vendor/markdown-it.min.js (built file copied from node_modules, no bundler required).

src/components/rojo-chat-panel/rojo-chat-panel.ts public/components/rojo-chat-panel/rojo-chat-panel.css public/vendor/markdown-it.min.js contenteditable ReadableStream

Tab Container Integration

Rojo Chat was added to the panelTypes array in TabContainer.openMenu(), so it appears under Add → Rojo Chat in every tab container's menu. Duplicate already worked for free — it calls the stored factory, creating a second panel with its own UUID and empty history.

RojoChatPanel also implements addContextMenuEntries (the EditorPanel interface), displaying a read-only label with the first 8 characters of the conversation ID.

src/components/tab-container/tab-container.ts addContextMenuEntries panelTypes

Key Decisions

Split LangChain packages, not the monolithic langchain

The example code used the monolithic langchain package with createAgent. We switched to @langchain/core + @langchain/openai and call model.stream() directly. With no tools in scope yet, this is equivalent and avoids a large dependency. Migrating to createReactAgent from @langchain/langgraph when tools are added will be a small, localised change in RojosAgent.ts.

RojosConfig.ts as a hardcoded TypeScript wrapper

A JSON config file in data/ would allow runtime editing but adds complexity (read-on-start, hot-reload). Since the model endpoint is not expected to change frequently, a TS constant is simpler and caught by the type checker. The file is the single place to change when adding providers.

markdown-it UMD copied to public/vendor/

The project has no bundler and all frontend modules are bare ES modules. A CDN script would require a network call. Copying the UMD build once into public/vendor/ keeps it self-hosted, available offline, and version-locked with the rest of node_modules.

In-memory conversation sessions, UUID per panel instance

Persisting conversations to data/ adds file I/O and a naming/indexing scheme. For a first iteration, in-memory is sufficient and keeps the surface area small. Each panel gets its own UUID on connectedCallback; named/saved conversations are deferred.

for await + res.write() instead of Readable.pipe()

The initial implementation used Readable.from(async function*(){}) piped to the response. This was simplified to a direct for await loop calling res.write() then res.end(), which is easier to read and avoids the intermediate Readable wrapper.

Structural Changes

package.json — added @langchain/core, @langchain/openai, markdown-it
server/rojos/RojosConfig.ts — new: model/provider config wrapper
server/rojos/RojosAgent.ts — new: in-memory session store, LangChain stream
server/routes/rojos.ts — new: POST /chat streaming endpoint
server/index.ts — added import rojosRouter, mounted at /api/rojos
src/components/rojo-chat-panel/rojo-chat-panel.ts — new: custom element
public/components/rojo-chat-panel/rojo-chat-panel.css — new: panel styles
public/vendor/markdown-it.min.js — new: UMD build (copied from node_modules)
public/editor.html — added rojo-chat-panel CSS, vendor script, module script
src/components/tab-container/tab-container.ts — added Rojo Chat to panelTypes