Skip to content

refactor(chat): Stage 2a — remove cruft, fix desktop composer floating#22

Merged
Nkburdick merged 2 commits intomainfrom
refactor/chat-cleanup-stage-2
Apr 24, 2026
Merged

refactor(chat): Stage 2a — remove cruft, fix desktop composer floating#22
Nkburdick merged 2 commits intomainfrom
refactor/chat-cleanup-stage-2

Conversation

@Nkburdick
Copy link
Copy Markdown
Owner

Summary

Net -23 lines across 5 files. Stage 2a of the chat refactor: remove defensive patterns that were working around a stale-service-worker symptom cluster (Stage 1 / PR #21 was the real fix), and address one latent layout bug that was hidden while the build was broken 4/17→4/23.

Removed cruft

What Why
Server-side UA `isMobile` in `+layout.server.ts` Presentation concern leaked into server data; broke DevTools mobile emulation
`inChatThread` regex in `projects/[slug]/+layout.svelte` Layout shouldn't know about child routes — replaced with CSS `:has()` in `app.css`
`textareaEl.value = ''` / `= previousDraft` DOM writes bind:value works fine post-Stage-1
`const text = (textareaEl?.value ?? draft).trim()` Same
Explicit `draft = el.value` in oninput bind:value does this itself
`isMobile` prop sourced from `$page.data.isMobile` Back to matchMedia, seeded at script init

Fixed — real bug

Desktop composer was floating mid-screen, not pinning to bottom of messages pane. Root cause: the project-layout tab-content div `flex-1 overflow-y-auto` was missing `min-h-0`, so `h-full` on descendants resolved to content height rather than the viewport slot. Added `min-h-0` on the tab-content div and the desktop ProjectChats root. Pre-existing, masked by the broken build.

Restored to canonical form

  • `disabled={sending || draft.trim() === ''}` on send button
  • CSS `:has([data-chat-thread])` takeover scoped to `@media (max-width: 767px)` so desktop keeps project chrome when directly viewing a thread URL

Verification

  • `bun run build` ✅
  • `bun run lint` ✅
  • `bun run test:unit` ✅ 80/80

Mobile-chat.test.ts still works via the `props.isMobile` test override path.

What's NOT in this PR (Stage 2b, future)

  • Extracting `ChatComposer.svelte` + `ChatMessages.svelte` + `sendMessage.ts` shared helpers (~500 lines of duplication between ProjectChats and thread page)
  • Moving the desktop-redirect on thread page from `onMount` to `+page.server.ts`

Those are the larger component-extraction pass and deserve their own focused PR + review cycle.

🤖 Generated with Claude Code

Net -23 lines across 5 files. Reverts several defensive patterns that
were added to work around what we thought was an iOS PWA reactivity
bug but (per last night's three-agent review) was actually a stale
service worker shell. Stage 1 (PR #21) shipped the real SW fix; this
PR restores the code to a clean form and fixes one real layout bug
that was exposed along the way.

Removed:
- Server-side User-Agent `isMobile` detection in +layout.server.ts.
  Pushed a presentation concern into server data + broke DevTools
  mobile emulation. Replaced with matchMedia in ProjectChats, seeded
  at script-init time (`typeof window !== 'undefined'`) so SSR gets
  `false`, client picks up the correct value on hydration. `$effect`
  handles viewport changes.
- `inChatThread` pathname regex in projects/[slug]/+layout.svelte.
  Layout shouldn't know about child routes. Replaced with a CSS
  `:has([data-chat-thread])` rule in app.css, scoped to `@media
  (max-width: 767px)` so desktop keeps the project chrome (where
  the two-panel layout needs it) while mobile hides it for the
  full-screen thread takeover.
- `textareaEl.value = ''` and `textareaEl.value = previousDraft`
  DOM writes in chats/[threadId]/+page.svelte. Papering over
  bind:value which works fine now that Stage 1 keeps hydration
  stable.
- `const text = (textareaEl?.value ?? draft).trim()` in
  sendCurrentMessage. Same — trust the framework.
- Explicit `draft = el.value` in handleTextareaInput. bind:value
  does this itself.
- `isMobile` prop resolution via `$page.data.isMobile`. Back to a
  simple test override only.

Fixed (real bug):
- Desktop composer was floating mid-screen instead of pinning to
  the bottom of the messages pane. Root cause: the tab-content
  div `flex-1 overflow-y-auto` was missing `min-h-0`, so `h-full`
  on descendants resolved to content height rather than the
  viewport slot. Added `min-h-0` to the tab-content div and the
  desktop ProjectChats root. This is pre-existing, was masked by
  the broken build, and surfaced when PR #15 unstuck deploys.

Restored:
- `disabled={sending || draft.trim() === ''}` on the send button.
  Stage 1 restores hydration reliability, so bind:value propagates
  and the reactive expression re-evaluates correctly.
- CSS `:has()` rule for chat-thread takeover now lives wrapped in
  a mobile-only media query, addressing the auditor's concern that
  the project chrome was globally hidden (regressing desktop when
  directly visiting a thread URL).

All 80 unit tests pass. `isMobile: true` prop in mobile-chat.test.ts
is still honored via the `props.isMobile` precedence path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

Warning

Rate limit exceeded

@Nkburdick has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 13 minutes and 53 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 13 minutes and 53 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 29581715-44c4-4456-a0e0-239382d61b4f

📥 Commits

Reviewing files that changed from the base of the PR and between ec65fa5 and 9c6432b.

📒 Files selected for processing (6)
  • deploy/compose.yml
  • src/app.css
  • src/lib/components/ProjectChats.svelte
  • src/routes/+layout.server.ts
  • src/routes/projects/[slug]/+layout.svelte
  • src/routes/projects/[slug]/chats/[threadId]/+page.svelte
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/chat-cleanup-stage-2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…dration)

Root cause of tonight's entire symptom cluster (send button dead, back
button dead, buttons unresponsive, hydration "broken" on iOS PWA AND
desktop). The Traefik CSP `script-src 'self'` header introduced by
PR #15 blocked SvelteKit's own inline hydration script:

  <script>
    Promise.all([
      import("./_app/immutable/entry/start.<hash>.js"),
      import("./_app/immutable/entry/app.<hash>.js")
    ]).then(([kit, app]) => {
      kit.start(app, element, { node_ids:..., data:..., form:null, error:null });
    });
  </script>

That script is necessarily inline because it carries page-specific
hydration data (node_ids, loaded data, form state, error). CSP blocks
it → JS never hydrates → no event listeners attach. Browsers render
the SSR HTML just fine (so <textarea> accepts text, <a href> links
navigate) but <button onclick> handlers are wired up by the blocked
hydration and do nothing.

This is NOT a Svelte 5 / iOS PWA reactivity bug (the memory I kept
citing was based on the same misread from a prior session). It
manifested on EVERY platform identically, it was just hidden by the
broken build from 4/17 to tonight.

Hot-patched live via ssh + sed on KVM 2 earlier; this commit brings
the repo in line so the next deploy doesn't revert the fix.

Follow-up (non-urgent): swap 'unsafe-inline' for SvelteKit's built-in
`kit.csp` config (svelte.config.js), which injects sha256 hashes of
the specific inline scripts — strictly more secure than unsafe-inline.
Tracked separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Nkburdick Nkburdick merged commit 691c15f into main Apr 24, 2026
2 checks passed
@Nkburdick Nkburdick deleted the refactor/chat-cleanup-stage-2 branch April 24, 2026 21:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant