feat(chat): render assistant markdown + collapse tool-call runs + copy-raw (mobile) #13
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#13
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Follow-up to the embedded Chat (issue #7, ADR-0016). Three UI enhancements to
web/src/routes/RunChat.tsx, all mobile-first:<p class="chat-msg-text">{m().text}</p>,white-space: pre-wrap) —**bold**, headings, code fences, lists, tables render as literal characters. Parse and render markdown.Design was fully grilled with the operator on 2026-07-07; the decisions below are settled — do not re-litigate them. This extends the existing chat surface; the message schema and provider seam are untouched (this is pure frontend + CSS).
Decisions (settled)
A. Markdown rendering
innerHTML/dangerouslySetInnerHTML), so the renderer stays XSS-safe by construction and needs no sanitizer dependency. This is deliberate and matches the repo's twice-documented "no markdown engine — no heavy deps" ethos (web/src/routes/IssueDetail.tsx:2). Deps staysolid-js+ router only.#–######), bold, italic, inline code, fenced code blocks, bullet + numbered lists, nested lists, tables, blockquotes, links, horizontal rules. Any unsupported/malformed syntax degrades to visible plain text — never crash, never drop characters.textandthinkingmessage (any role — assistant, user, and thinking alike). Toolinput/outputstay plain<pre>(they are literal command I/O — parsing them would corrupt JSON/diffs).[text](url)and barehttp(s)://…URLs are clickable (autolink bare URLs). Scheme allowlisthttp/https/mailto— anything else (javascript:,data:,file:, relative paths) renders as plain text (closes the href-injection channel, which escaped-text-node safety does not cover). External links:target="_blank" rel="noopener noreferrer". No syntax highlighting (needs a heavy dep + grammar) — plain monospace only.overflow-x: autoscroll container (faithful shape, swipe sideways — no stacked-card reflow); everything elseoverflow-wrap: anywhere. The message column must never force horizontal page scroll — only individual wide elements scroll internally (same as today's tool<pre>blocks). Readable base font/line-height; comfortable link tap targets.**,[text](with no closing paren, half-streamed table. All must degrade to visible plain text, not throw and not eat content.B. Tool-call grouping
kind:'tool'messages. The server already emits a flat message list; this is a pure display concern — no backend/schema/API change.text,dialog, andlifecyclemessages.thinkingfolds into the run (does not break it) — Claude interleavesthinking → tool → thinking → tool, and thinking is hidden-by-default noise, so folding it makes a run read as one "the bot did a bunch of work" block. Threshold = 2+: a single lone tool call renders exactly as today (one chip).<details>→ ordered list of individual chips → each chip expands to input/output (today'schat-toolbehavior, unchanged). Interleaved thinking messages still render in order inside the expanded group.5 tool calls · 1 failed); a hidden failure is an unacceptable UX trap. No tool-name preview (keep the summary short for mobile).4 tool calls · running…, count ticks up as tools arrive). Watching live detail is one tap. (Chosen over "stay expanded until sealed" to avoid the scroll-anchoring jank of a run shrinking mid-scroll.)seq, held in a signal that survives SSE refetches — mandatory. The group is a derived structure recomputed on everyrun.messages.changedrefetch; with native<details>state alone, an expanded live group would slam shut on the next SSE tick, defeating decision 11.seqis the immutable cursor, so it is the stable key.C. Copy-raw (claude.ai style)
textmessage; copies the message's raw markdown (m().textis retained — we render a parsed view but never discard the source). Same icon→check feedback. (User replies are plain and already selectable; thinking is hidden noise — assistant-only for v1.)navigator.clipboard.writeText(localhost/https is a secure context in the embedded server).Implementation notes (seams, not prescriptions)
web/src/lib/markdown.ts+markdown.test.ts. Signature roughlyparse(src) → node treeconsumed by the view (or emits Solid nodes directly). Match the repo's pure-helper-with-test convention (web/src/lib/chatStream.ts,web/src/lib/conversation.ts). Tests must include the adversarial/partial-input cases from decision 6.web/src/lib/toolGroups.ts+ test (or fold intochatStream.ts):groupMessages(messages) → renderable items. Tests cover threshold 2, thinking-folds-in, error rollup, count-tools-only, sealed vs. live trailing run.web/src/routes/RunChat.tsx—MessageViewrender branches (~lines 369–411): swap thekind:'text'branch to use the markdown renderer; add the grouped-tool render path; add copy affordances. Keep the view thin — logic lives in the lib helpers. The open-state signal (decision 12) lives inRunChatView.web/src/base.css— chat styles live at ~1322–1559 (.chat-msg-textat 1415–1419,.monoat 91). Add markdown element styles (headings, lists, tables, code, blockquote, hr), the two scroll containers, the code-block header bar, and the copy buttons here — plain global CSS, CSS custom properties (var(--accent)etc.), no CSS modules/Tailwind.provider.Message/ToolInfo, the API, SSE events, migrations, and the transcript fold are all untouched.web/src/api.tstypes (ChatMessage,ToolInfo, ~411–445) are unchanged.Deliverables
web/src/lib/markdown.ts+markdown.test.ts(hand-rolled, zero-dep, adversarial/partial-input tests).web/src/lib/toolGroups.ts(or folded intochatStream.ts) + tests for the grouping rules.RunChat.tsx: markdown render branch, grouped-tool disclosure, copy-raw on code blocks + assistant messages, controlled open-state signal.base.css: markdown element styles, table/code scroll containers, code-block header bar, copy buttons — mobile-first, dark viaprefers-color-scheme(keep v0 design language: max-width 760px, 44px touch targets).RunChat.test.tsx: coverage for markdown rendering, tool grouping (incl. error rollup + live summary), and copy actions.docs/adr/<next-free-number>-*.md(or an addendum to ADR-0016) recording the deliberate hand-rolled-no-dependency markdown renderer decision and the tool-grouping model, in the house style. NOTE: take the next ADR number free onmainat implementation time — do not hardcode.solid-js+@solidjs/router).Out of scope (explicitly deferred)
Syntax highlighting in code blocks · bare-URL autolink for non-http schemes · stacked-card table reflow on mobile · copy-raw on user/thinking messages · message-level actions beyond copy (retry/edit/feedback) · rendering markdown in tool input/output · any backend, schema, API, or SSE change.