Makes the mobile white screen diagnosable. The app had no error boundary anywhere, so any throw in the dashboard unmounted the whole tree and left an empty #root. That explains the exact reported behavior: brief flash, white page, other routes fine on a fresh load, blank again when navigating back to /. AppErrorBoundary now renders the error name, message, stack, URL and user agent with Reload / Copy details buttons, so the real exception can be read and copied from the phone. It is keyed on the route path, so navigating away clears a crash.
Responsive layout fixes for the clipping visible in mobile emulation: 100vw → 100%, 100dvh for the chat shell so mobile URL bars can't push the input row off screen, min-width: 0 / flex-shrink on the input row so attach, model select, textarea and send compress instead of overflowing, env(safe-area-inset-*) padding on header, drawer and input area, and message bubbles that wrap long tokens (overflow-wrap: anywhere, max-width on pre/img/table).
Perf: particle canvas pauses while the tab is hidden, honors prefers-reduced-motion, caps particle count for the O(n²) link pass, and re-seeds only on real size changes. Chat auto-scroll now fires on message change rather than every render.
Test plan
npm run test:ci — 25 suites / 112 tests pass (adds AppErrorBoundary coverage for both the pass-through and crash paths)
npm run build — clean
Phone browser: sign in and land on / — chat renders, or the error card shows the real exception (paste into #61)
Phone browser: no horizontal scroll; attach, model select, textarea and send all visible
Phone browser: input row still reachable with the keyboard open
Long code block / unbroken string stays inside the bubble
Desktop: sidebar, chat and input unchanged; background animation still runs
Background the tab and return — animation resumes
Notes
The crash is not reproducible in desktop mobile emulation, so this ships the boundary as the diagnostic path instead of guessing at the throwing call site. Once the error card appears on the phone, the stack should point straight at the cause. Worth landing before the Capacitor Android wrap (#20), since the same failure mode is much harder to inspect inside a WebView.
## Summary
- Closes #61
- **Makes the mobile white screen diagnosable.** The app had **no error boundary anywhere**, so any throw in the dashboard unmounted the whole tree and left an empty `#root`. That explains the exact reported behavior: brief flash, white page, other routes fine on a fresh load, blank again when navigating back to `/`. `AppErrorBoundary` now renders the error name, message, stack, URL and user agent with Reload / Copy details buttons, so the real exception can be read and copied **from the phone**. It is keyed on the route path, so navigating away clears a crash.
- **Responsive layout fixes** for the clipping visible in mobile emulation: `100vw` → `100%`, `100dvh` for the chat shell so mobile URL bars can't push the input row off screen, `min-width: 0` / `flex-shrink` on the input row so attach, model select, textarea and send compress instead of overflowing, `env(safe-area-inset-*)` padding on header, drawer and input area, and message bubbles that wrap long tokens (`overflow-wrap: anywhere`, `max-width` on `pre`/`img`/`table`).
- **Perf:** particle canvas pauses while the tab is hidden, honors `prefers-reduced-motion`, caps particle count for the O(n²) link pass, and re-seeds only on real size changes. Chat auto-scroll now fires on message change rather than every render.
## Test plan
- [x] `npm run test:ci` — 25 suites / 112 tests pass (adds `AppErrorBoundary` coverage for both the pass-through and crash paths)
- [x] `npm run build` — clean
- [ ] Phone browser: sign in and land on `/` — chat renders, or the error card shows the real exception (paste into #61)
- [ ] Phone browser: no horizontal scroll; attach, model select, textarea and send all visible
- [ ] Phone browser: input row still reachable with the keyboard open
- [ ] Long code block / unbroken string stays inside the bubble
- [ ] Desktop: sidebar, chat and input unchanged; background animation still runs
- [ ] Background the tab and return — animation resumes
## Notes
The crash is not reproducible in desktop mobile emulation, so this ships the boundary as the diagnostic path instead of guessing at the throwing call site. Once the error card appears on the phone, the stack should point straight at the cause. Worth landing before the Capacitor Android wrap (#20), since the same failure mode is much harder to inspect inside a WebView.
Make the dashboard sidebar a clearer fixed sheet on small screens with an explicit close control and empty-list guidance so it is less likely to look like a blank page.
The app had no error boundary, so any throw inside the dashboard unmounted the
whole tree and left a blank white page with no way to read the cause on a phone.
Add a route-scoped boundary that shows the failure details, and fix the chat
shell to fit small viewports: dynamic viewport height, shrinkable input row,
safe-area padding, and wrapping message bubbles.
Also pause the particle canvas while the tab is hidden and cap its particle
count so phones are not driving a needless animation loop.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
#root. That explains the exact reported behavior: brief flash, white page, other routes fine on a fresh load, blank again when navigating back to/.AppErrorBoundarynow renders the error name, message, stack, URL and user agent with Reload / Copy details buttons, so the real exception can be read and copied from the phone. It is keyed on the route path, so navigating away clears a crash.100vw→100%,100dvhfor the chat shell so mobile URL bars can't push the input row off screen,min-width: 0/flex-shrinkon the input row so attach, model select, textarea and send compress instead of overflowing,env(safe-area-inset-*)padding on header, drawer and input area, and message bubbles that wrap long tokens (overflow-wrap: anywhere,max-widthonpre/img/table).prefers-reduced-motion, caps particle count for the O(n²) link pass, and re-seeds only on real size changes. Chat auto-scroll now fires on message change rather than every render.Test plan
npm run test:ci— 25 suites / 112 tests pass (addsAppErrorBoundarycoverage for both the pass-through and crash paths)npm run build— clean/— chat renders, or the error card shows the real exception (paste into #61)Notes
The crash is not reproducible in desktop mobile emulation, so this ships the boundary as the diagnostic path instead of guessing at the throwing call site. Once the error card appears on the phone, the stack should point straight at the cause. Worth landing before the Capacitor Android wrap (#20), since the same failure mode is much harder to inspect inside a WebView.
Pull request closed