The assistant answers in Markdown — tables, lists, bold, links — but the
chat bubble showed the raw source (whitespace-pre-wrap), so a table came
through as a wall of pipes.
- New MarkdownMessage: react-markdown + remark-gfm (for GFM tables). It does
NOT enable raw HTML (no rehype-raw), so a model reply can't inject markup;
every element the assistant actually uses is Tailwind-styled for a chat
bubble, and wide content (tables, code) scrolls in its own box so the
bubble can't blow out.
- Only ASSISTANT bubbles render as Markdown; the user's own text stays
literal (their `*` shouldn't become a bullet) and the assistant bubble goes
full-width so a table has room.
- Lazy-loaded: the renderer + its ecosystem (~150 KB) is its own async chunk,
loaded only when an assistant message renders — not for everyone who opens
the editor. To keep it out of the eager first paint, manualChunks now
vendors only the app-wide core (react/react-dom/scheduler kept together —
splitting react-dom breaks React 19 at load) and lets everything else ride
with its importer.
Tested via renderToStaticMarkup (node, no DOM): a GFM table renders with
styled cells + horizontal scroll; **bold**/lists render; raw <script>/<b> are
escaped not emitted; links get rel="noopener noreferrer". App re-verified to
mount cleanly with the re-chunk (no React-19 init-order break). tsc + vitest
(99) + build green, no >500 KB warning.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
Gadfly on #106:
- (2 models) React.lazy memoizes a REJECTED import, so after a deploy (every
main push) a still-open app references chunk hashes the server just
replaced — the import 404s and RouteError's "Try again" can never recover.
New lazyPage() helper reloads once on a chunk-load failure to fetch fresh
hashes (session-flag guarded against a reload loop; cleared on success).
- (perf) @use-gesture is editor-only, but the single vendor chunk pulled it
into the eager first paint. Exclude it from vendor so it rides with the
lazy editor chunk (vendor 421→392 KB; the gesture code moved into the
editor's own chunk). react/react-dom/tanstack still share one vendor chunk
— splitting react-dom out is what broke React 19 at load.
- (nits) lazyPage also unwraps the named export, so the five route lazies are
uniform one-liners; moved the lazy block below the import group.
Re-verified live: app mounts, /gardens/1 lazy-loads + renders the editor,
console clean. tsc + build green, no >500 KB warning.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
The whole app shipped in one 578 KB chunk, so a phone on cell data
downloaded and parsed everything — the canvas editor, gestures, geometry,
every page — before the login screen could paint.
- Lazy-load the heavy/deep routes via React.lazy: the editor (its
GardenCanvas + use-gesture + geometry are the biggest surface), the
public garden view, plants, settings, register. Login and the gardens
list stay eager (entry points — no fallback flash on landing). AppShell
wraps <Outlet> in a Suspense boundary.
- One `vendor` manualChunk for all node_modules so the rarely-changing
libraries cache across app deploys while the tiny app chunk churns.
Kept as a SINGLE chunk deliberately: splitting react-dom/scheduler into
their own chunk reorders module init across chunk boundaries and breaks
React 19 at load ("Cannot set 'Activity' of undefined") — verified that
failure and backed it out.
Result: app entry chunk 578 KB → 39 KB; vendor 421 KB (cached); the editor
(43 KB) + canvas (17 KB) only download when you open a garden. No more
>500 KB chunk warning.
Verified live against the embedded binary: /gardens loads with only
index+vendor; opening a garden lazy-fetches the editor chunk and renders;
console clean; the embed serves the hashed split chunks + SPA fallback fine.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
Implements the Pansy v1 scaffold (epic #20, phase 0).
#1 Backend: Go module (pure-Go, builds with CGO_ENABLED=0), env config,
gin server with slog + recovery + /api/v1/healthz, modernc SQLite store
with WAL/busy_timeout/foreign_keys pragmas, embedded numbered-migration
runner, full initial schema (users, sessions, gardens, garden_shares,
garden_objects, plants, plantings), domain structs + sentinel errors,
graceful shutdown.
#2 Frontend: Vite + React 19 + TS (strict) + Tailwind 4 + TanStack
Router/Query, typed /api/v1 fetch wrapper (ApiError carries status +
body so later issues can read 409 conflict rows), dev proxy /api -> :8080,
responsive app shell with stub pages for all five routes.
#3 Single binary: //go:embed of the web build with a committed placeholder,
SPA fallback (deep links, immutable asset caching, JSON 404 for unmatched
/api and missing assets), Makefile (web/build/dev/test), README quickstart
+ env var table.
Verified: go build/vet/test clean (CGO off); binary migrates idempotently and
serves healthz; web tsc + build clean; integrated binary serves the SPA, deep
links, and correct cache headers; app mounts and navigates all five routes at
desktop and 375px widths.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi