fix(runner): assistant follow-ups — event rate limit, Markdown rendering, Ask AI tooltip (DEV-2047) - #120
Conversation
Reusing checkChatRateLimit for the event route made Apply/Undo increment the same per-IP counters as paid questions: applying edits silently spent a user's question quota, active users near the 8/min cap lost chat_edit_applied counts, and anyone could exhaust an IP's chat budget through the free route. Two buckets now, because the routes cost wildly different amounts — an answer is an LLM call, an event is one counter row. Chat stays 8/min and 120/day; events get 60/min and 600/day under their own keys. A regression I introduced while fixing the unbounded-dimensions finding, and it landed on the commit as it was being merged. Verified: 12 consecutive events all return 204 and a question afterwards still succeeds.
… (DEV-2047) Answers were rendered by a placeholder that understood fenced code blocks and paragraphs and nothing else, so every **bold**, `inline code` and bullet list came out as literal asterisks and backticks — the content was right and the presentation made it look broken. There is now a real renderer for the subset answers actually use: headings, ordered and unordered lists, bold, italic, inline code, links, fenced code. It builds React elements and never touches innerHTML, so model output cannot inject markup no matter what it contains, and links are restricted to http(s) — a Markdown library would have added a dependency and, in most cases, an HTML sink to sanitise. Anything unsupported falls through as text, which is the right failure: an unrendered character is a blemish, a broken renderer is a broken answer. The toolbar button also gets a tooltip. "Ask AI" is a label, not an invitation — nobody clicks a chat button to discover it can edit their code. The hint names the three things worth knowing: it is scoped to the open example, a change arrives as an edit you can apply, and answers are grounded in the docs. It also says up front that nothing is applied without you, because that is the question a user asks before letting an assistant near their code. It hides once the panel is open, where it would only repeat what is on screen.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 362d98b. Configure here.
| } | ||
| } | ||
|
|
||
| const INLINE = /(`[^`]+`)|(\*\*[^*]+\*\*)|(\*[^*\n]+\*)|(_[^_\n]+_)|(\[[^\]]+\]\([^)\s]+\))/g; |
There was a problem hiding this comment.
Underscore italics break snake_case
Medium Severity
The _italic_ alternative in INLINE matches any underscore pair, including spaces and mid-identifier characters, so phrases like first_name and last_name render as italicized spans instead of literal text. Chat answers and user turns both go through Markdown, so ordinary snake_case column names get mangled unless wrapped in backticks.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 362d98b. Configure here.


Three follow-ups to #119.
1.
/api/chat/eventgets its own rate-limit bucketA regression I introduced in #119 while fixing the unbounded-dimensions finding, and merged before the review comment landed. The event route reused
checkChatRateLimit, so Apply/Undo incremented the same per-IP counters as paid questions:chat_edit_appliedcounts — skewing the acceptance metric the panel exists to show;/api/chat/api/chat/eventVerified: 12 consecutive events all return 204 (the shared bucket refused after 8), and a question afterwards still succeeds.
2. Answers render as Markdown
The panel shipped with a placeholder renderer that understood fenced code and paragraphs and nothing else, so every
**bold**,inline codeand bullet list appeared as literal asterisks and backticks. The content was right; the presentation made it look broken.There is now a real renderer for the subset answers actually use — headings, ordered/unordered lists, bold, italic, inline code, links, fenced code. It builds React elements and never touches
innerHTML, so model output cannot inject markup, and links are restricted to http(s). A Markdown library would have meant a new dependency in the authoring bundle and, in most cases, an HTML sink to sanitise. Unsupported syntax falls through as text.3. The Ask AI button explains itself
"Ask AI" is a label, not an invitation — nobody clicks a chat button to find out it can edit their code. A hover/focus tooltip now names the three things worth knowing: scoped to the open example, changes arrive as an edit you can apply, answers are grounded in the docs with links. It also states up front that nothing is applied without you, which is the question people ask before letting an assistant near their code, and it hides once the panel is open.
Note
Medium Risk
Separate rate-limit buckets fix a real quota/abuse regression on a public API; the Markdown renderer touches untrusted model output but only allows http(s) links and avoids innerHTML.
Overview
Rate limiting:
POST /api/chat/eventno longer shares per-IP counters with paid/api/chat.checkChatRateLimitnow takes abucket(chatvsevent) with separate limits (8/120 vs 60/600 per min/day), so Apply/Undo analytics do not consume question quota and the free route cannot exhaust an IP’s LLM budget.Authoring UX: The toolbar Ask AI control is extracted into
AskAiButtonwith a hover/focus tooltip that explains example-scoped Q&A, doc grounding, and apply/undo. Chat replies swap the inlineMarkdownishhelper for a sharedMarkdowncomponent that renders headings, lists, bold/italic, fenced code, and http(s) links via React (noinnerHTML).Docs in
example-chat.mddocument the second bucket.Reviewed by Cursor Bugbot for commit 362d98b. Bugbot is set up for automated code reviews on this repo. Configure here.