Surface every outstanding suggestion as a card in the right-hand
whitespace, vertically aligned to the text it flags — so the writer sees
the whole queue at once instead of hovering each highlight. Cards stack
with collision avoidance, link both ways with their highlight (hover/click
↔ soft text wash, driven through the decoration plugin so it survives
edit repaints), and carry the same Accept / Dismiss / Ask Petal actions.
The rail is a progressive enhancement: it mounts only when there's room
beside the editor, otherwise the existing inline hover card is unchanged.
Stacked cards that reach the bottom-right corner tuck behind the
companion mascot (z-order).
When a card is expanded, the Ask Petal bubble now opens with the
Simplified-Chinese translation of the explanation (the English stays in
the card body) instead of repeating the same text twice — a new
POST /api/suggestions/{id}/translate one-shot LLM endpoint, loaded
lazily on open with an English fallback.
Verified live against the local LLM via the uitest harness: rail
stacking, hover↔text wash, expand/Ask Petal, accept-from-rail, narrow
fallback, and the Mandarin bubble.
Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
Conversational follow-up on a suggestion, streamed token-by-token.
Backend (interface-only; handlers never touch a concrete LLM client):
- internal/llm/chat.go: StreamAskPetal with conversational sampling
(max_tokens 512, temp 0.7, rep 1.15, top_p 0.92, stop "\n\n\n"),
reusing AskPetalSystemPrompt + TrimHistory.
- internal/suggestions/chat.go: POST /api/suggestions/:id/chat. One
user-scoped join loads the suggestion + parent content_text;
surroundingParagraph extracts the \n\n-bounded paragraph at from_pos
(whole-doc fallback when unlocated) and injects it server-side.
Streams event: token / event: done SSE frames with JSON-encoded data
so token newlines can't break framing; real http.Flusher per chunk.
LLM-unreachable -> 502 before SSE headers; unknown suggestion -> 404.
Frontend:
- streamSuggestionChat: fetch + ReadableStream SSE parser (not
EventSource, needs POST), abortable.
- AskPetal.tsx: whole conversation in component state (no persistence,
cleared on close), Petal's first bubble pre-seeded with the
explanation, rose/lavender bubbles, CJK font stack on the bubbles
only (Note #17), streaming caret.
- SuggestionCard "Ask Petal" pill pins the card open while chatting
(hover-close suppressed, click-away closes) and widens it to 340px.
Tests: chat_test.go covers streamed-text concat + done event,
server-side context injection on the system message, sampling params,
404, and surroundingParagraph. go build/vet/test clean, tsc clean,
vite build OK. Live SSE smoke-tested against a fake streaming vLLM:
tokens flushed individually through the chi middleware stack, done
terminator, 502 on LLM-down, 404 on unknown suggestion.
Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd