Skip to content

feat(web,desktop,api): website redesign + auth UX rethink#148

Merged
github-actions[bot] merged 5 commits into
developfrom
feature/website-redesign
Mar 13, 2026
Merged

feat(web,desktop,api): website redesign + auth UX rethink#148
github-actions[bot] merged 5 commits into
developfrom
feature/website-redesign

Conversation

@tomymaritano

@tomymaritano tomymaritano commented Mar 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Website redesign: Rebuilt all marketing pages with Magic UI components (shimmer, border beam, text reveal, marquee, number ticker), shadcn/ui base components, and a new violet accent design system with Inter + JetBrains Mono fonts
  • CodeRabbit fixes: Accessibility improvements (aria labels, semantic HTML, reduced-motion), stable React keys, design token alignment
  • Auth UX rethink: Replaced buried Settings > Account login with direct "Enable Sync" flow from sidebar — value proposition modal, resend timer, auto-detect deep link verification
  • Auth bug fixes: Fixed API returning 500 instead of 401 for invalid JWTs, fixed deep link race condition when app cold-starts from magic link
  • Sync onboarding: Shows prompt after 3+ notes created, dismissible
  • Web auth fallback: Improved verify page for wrong-device and app-not-installed scenarios

Changes by area

Website (apps/web/)

  • New design system: violet accent, Inter + JetBrains Mono
  • shadcn/ui: Button, Card, Badge, Separator, Accordion, Sheet
  • Rebuilt: Navbar, Hero, Footer, SocialProof, Features, WhyLocal, Audience, Pricing, Download, Philosophy
  • Magic UI: AnimatedGridPattern, BorderBeam, Marquee, NumberTicker, ShimmerButton, TextReveal, AnimatedShinyText
  • Migrated FaqAccordion from @headlessui/react to shadcn
  • Improved auth/verify fallback page

Desktop (apps/desktop/)

  • New EnableSyncModal with 5-step wizard (value prop → email → sent → success)
  • SidebarFooter: "Sign In" → "Enable Sync" (opens modal directly)
  • useSyncOnboarding hook: prompt after 3+ notes
  • Sync prompt banner in sidebar (dismissible)
  • Deep link race condition fix (queues token if window not ready)

API (packages/api/)

  • Fixed onError handler swallowing HTTPException (401s returned as 500)
  • Fixed auth middleware catch block losing specific error messages

Config (packages/product-config/)

  • Fixed extensionless imports for cross-package compatibility

Test plan

  • pnpm typecheck — 18/18 packages pass
  • pnpm test — all tests pass (pre-push hook)
  • Visual review of all marketing pages
  • Test Enable Sync flow: sidebar → modal → email → magic link → verify
  • Test deep link cold-start (kill app → click magic link)
  • Verify CodeRabbit has no new comments

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Desktop: prominent sync onboarding prompt and multi-step "Enable Sync" modal.
  • Improvements

    • Clearer auth verification flow with fallback actions.
    • Accessibility enhancements across web and desktop (semantic dialogs, reduced-motion support, better aria attributes).
    • Locale-aware number formatting and improved video modal semantics.
  • Bug Fixes

    • More reliable deep-link token delivery and safer rendering in animated components.
  • Documentation

    • Added roadmap for Auth, Sync, and AI work.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Mar 12, 2026

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Queues deep-link auth tokens in the desktop main process for delivery when renderer windows are ready; adds a sync onboarding flow and EnableSyncModal; broad accessibility and UI refinements across web marketing and Magic UI components; improves API HTTP error handling and adds a roadmap doc.

Changes

Cohort / File(s) Summary
Desktop main & deep-linking
apps/desktop/src/main/index.ts
Adds pendingAuthToken state, queues deep-link tokens when no ready window exists, delivers queued token on did-finish-load via auth:verify-token, and tightens window selection for delivery.
Desktop sync onboarding (UI + hook + styles)
apps/desktop/src/renderer/components/sidebar/Sidebar.tsx, apps/desktop/src/renderer/components/sidebar/SidebarFooter.tsx, apps/desktop/src/renderer/hooks/useSyncOnboarding.ts, apps/desktop/src/renderer/styles/global.css
Introduces useSyncOnboarding, renders sidebar sync prompt, wires SidebarFooter prop from onSettingsClickonEnableSyncClick, and adds .sidebar-sync-prompt CSS rules.
EnableSyncModal (was LoginModal)
apps/desktop/src/renderer/components/sync/LoginModal.tsx, apps/desktop/src/renderer/components/sync/LoginModal.module.css, apps/desktop/src/renderer/components/sync/index.ts
Renames/export EnableSyncModal, implements multi-step flow (value-prop, email, checking, sent, success), resend cooldown, auth-store subscription, accessibility attributes, new icons, and new modal styles.
API error handling & auth middleware
packages/api/src/index.ts, packages/api/src/middleware/auth.ts
Import HTTPException; global handler returns err.getResponse() for HTTPException; auth middleware re-throws HTTPException to preserve its response.
Web marketing content & accessibility
apps/web/app/(marketing)/auth/verify/AuthVerifyContent.tsx, apps/web/app/(marketing)/download/page.tsx, apps/web/app/(marketing)/philosophy/page.tsx
Replaces DOM-driven fallback with state for verify flow; enriches invalid-link UI; adds aria-hidden/focusable on inline SVGs and updates link hover tokens.
Landing pages & media modals
apps/web/components/landing/Hero.tsx, apps/web/components/landing/Testimonials.tsx, apps/web/components/landing/VideoGuides.tsx, apps/web/components/landing/WhyLocal.tsx
Adds DEMO_VIDEO_URL/hasVideo gating, improves modal semantics (role="dialog", aria attributes, key handling), updates testimonial text/names, and adds SR labels for boolean table values.
Global styles & small UI tweaks
apps/web/app/globals.css, apps/web/components/Navbar.tsx, apps/web/components/FaqAccordion.tsx
Scopes [data-active] rule to .fd-sidebar, adds Radix accordion keyframes and prefers-reduced-motion block, removes mt-2 from Docs dropdown, and keys accordion items by question slug.
Magic UI: animations, accessibility, safety
apps/web/components/magicui/animated-grid-pattern.tsx, apps/web/components/magicui/animated-beam.tsx, apps/web/components/magicui/animated-shiny-text.tsx, apps/web/components/magicui/border-beam.tsx, apps/web/components/magicui/dot-pattern.tsx, apps/web/components/magicui/marquee.tsx, apps/web/components/magicui/hero-video-dialog.tsx, apps/web/components/magicui/number-ticker.tsx, apps/web/components/magicui/text-reveal.tsx, apps/web/components/magicui/shimmer-button.tsx
Adds props (repeatDelay, locale), reduced-motion fallbacks, safer size math, aria attributes (aria-hidden, tabIndex, dialog semantics), removes some props from ShimmerButton, and updates animated behaviors and color tokens.
UI primitives & components
apps/web/components/ui/button.tsx, apps/web/components/ui/card.tsx
Defaults <Button> type to "button"; updates CardTitle to forward ref to h3 and CardDescription to forward ref to p.
Small web component tweaks
apps/web/components/Footer.tsx, apps/web/components/landing/VideoGuides.tsx, apps/web/components/landing/Hero.tsx
Adds aria-hidden="true" to social SVGs; improves video card keyboard semantics and modal accessibility; gates demo video modal by presence of URL.
Package & config updates
apps/web/package.json, packages/licensing/package.json
Removes turbopack flag from next dev; changes packages/licensing package entrypoints to source ./src/index.ts.
Docs
docs/plans/2026-03-12-roadmap-auth-sync-ai.md
Adds a roadmap doc covering Auth, Sync testing/stability, AI commands/knowledge, extensibility, and verification checklist.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant OS as "OS / Deep-link"
participant Main as "Desktop Main Process"
participant Window as "App Window (Renderer)"
participant Renderer as "Renderer: auth handler"
OS->>Main: open-url with token
alt No ready window
Main->>Main: set pendingAuthToken
Note right of Main: token queued for later delivery
else Ready window exists
Main->>Window: send 'auth:verify-token' (token)
Main->>Window: show/focus
Window->>Renderer: on 'auth:verify-token' -> verify token
end
Window-->>Main: did-finish-load (on load)
alt pending token exists
Main->>Window: send 'auth:verify-token' (pending token)
Main->>Window: show/focus
Main->>Main: clear pendingAuthToken
end

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: website redesign and auth UX rethink across web, desktop, and API packages, matching the substantial scope of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/website-redesign
📝 Coding Plan
  • Generate coding plan for human review comments

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 41

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/web/app/(marketing)/auth/verify/AuthVerifyContent.tsx (1)

14-22: ⚠️ Potential issue | 🟠 Major

URL-encode the token before building deep-link URLs.

Using the raw query value can produce malformed readied:// links when token characters require escaping.

🔒 Proposed fix
 export default function AuthVerifyContent() {
   const searchParams = useSearchParams();
   const token = searchParams.get('token');
+  const encodedToken = token ? encodeURIComponent(token) : '';
   const [showFallback, setShowFallback] = useState(false);
 
   useEffect(() => {
-    if (token) {
-      window.location.href = `readied://auth/verify?token=${token}`;
+    if (encodedToken) {
+      window.location.href = `readied://auth/verify?token=${encodedToken}`;
@@
-  }, [token]);
+  }, [encodedToken]);
@@
             <a
-              href={`readied://auth/verify?token=${token}`}
+              href={`readied://auth/verify?token=${encodedToken}`}
               className="inline-block px-8 py-3 mb-8 bg-accent text-white font-semibold rounded-lg transition-colors hover:bg-accent-hover"
             >
               Open in Readied

Also applies to: 85-87

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/`(marketing)/auth/verify/AuthVerifyContent.tsx around lines 14 -
22, The deep-link is built with the raw token which can produce malformed
readied:// URLs; update the places that construct the deep-link (the assignment
to window.location.href in the useEffect and the other occurrence around lines
85-87) to URL-encode the token using encodeURIComponent(token) before
interpolating it into `readied://auth/verify?token=...` so the token is safely
escaped when building the deep link.
apps/web/app/(marketing)/download/page.tsx (1)

13-15: ⚠️ Potential issue | 🟠 Major

Reconcile the pricing copy before this ships.

This page now tells users both “Try free for {config.trialDays} days” and “Free forever”. Unless the distinction between the free tier and the Pro trial is explicit, this reads as contradictory marketing copy and is likely to drift from the rest of the pricing/legal surface.

Based on learnings, "When pricing or business model changes, update packages/product-config/src/facade.ts and verify marketing pages and legal pages (terms.astro, privacy.astro) are synchronized with the new model".

Also applies to: 54-64

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/`(marketing)/download/page.tsx around lines 13 - 15, The
metadata.title/description in the Download page uses both "Try free for
{config.trialDays} days" and "Free forever", which conflicts with the product
pricing; update the copy in the metadata object (and the corresponding marketing
text block later in the same file around the download page content) to match the
actual model in packages/product-config/src/facade.ts (e.g., use either a
trial-focused message that references config.trialDays or a perpetual free-tier
message), then propagate the same wording to the related marketing/legal
templates (terms.astro, privacy.astro) so messaging is consistent across
metadata, the page content, and legal pages.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/main/index.ts`:
- Around line 2484-2495: Extract the deep-link token extraction and
queuing/sending logic into a shared function (e.g., processDeepLink or
handleAuthLink) that takes a token/URL, looks up the ready BrowserWindow via
BrowserWindow.getAllWindows().find(...), and either sends 'auth:verify-token' to
webContents or assigns pendingAuthToken; then invoke this new function from both
the macOS open-url handler and from the app startup argv parsing and the
app.on('second-instance', ...) handler so Windows/Linux cold-start and
second-instance protocol links are handled the same way as the existing macOS
flow.
- Around line 2484-2495: Replace the transient lookup via
BrowserWindow.getAllWindows().find(...) with a module-level mainWindow reference
(same pattern as settingsWindow): ensure mainWindow is assigned when the app
creates the primary window, then in the auth delivery path only target that
mainWindow (check !mainWindow.isDestroyed() &&
mainWindow.webContents.isLoading() === false) before calling
mainWindow.webContents.send('auth:verify-token', token), mainWindow.show(),
mainWindow.focus(); if mainWindow is undefined or not ready, set
pendingAuthToken = token (do not send to any other window). Ensure any existing
uses of the local mainWin are updated to use the module-level mainWindow
variable.

In `@apps/desktop/src/renderer/components/sync/LoginModal.tsx`:
- Around line 107-108: The modal is plain divs and the email input relies on
placeholder text causing accessibility issues; update the LoginModal component
so the container uses proper dialog semantics (add role="dialog" and
aria-modal="true" on the modal element and ensure the overlay remains clickable
for dismiss via handleClose), add a visible or visually-hidden heading inside
the modal (e.g., <h2> sign-in title) with an id and set aria-labelledby on the
modal to that id, and replace the placeholder-only email input with a properly
associated label (or give the input an aria-labelledby/aria-label tied to the
new heading or a dedicated label element) so screen readers get a stable name;
apply the same changes to the other modal instance referenced around lines
148-159 in the same file (same component/markup patterns).
- Around line 88-98: The resend action currently waits for requestMagicLink to
resolve before starting the cooldown, allowing multiple clicks; modify
handleResend (and the other resend handler that calls requestMagicLink) to
disable the action immediately by setting a local pending flag (e.g.,
isResending via useState) or by calling setResendTimer(RESEND_COOLDOWN) before
awaiting requestMagicLink, then await requestMagicLink, and in a finally block
clear the pending flag or, on error, reset the timer and call setError(err
instanceof Error ? err.message : 'Failed to resend') so the button is unblocked
only after success/failure is handled.

In `@apps/desktop/src/renderer/styles/global.css`:
- Around line 1491-1539: The onboarding prompt (.sidebar-sync-prompt) is missing
-webkit-app-region: no-drag so its buttons can receive clicks in custom-titlebar
builds; update the CSS for .sidebar-sync-prompt (and optionally
.sidebar-sync-prompt-actions) to include -webkit-app-region: no-drag alongside
existing rules so the prompt and its action container opt out of the draggable
sidebar region and allow button interaction.

In `@apps/web/app/`(marketing)/auth/verify/AuthVerifyContent.tsx:
- Around line 29-39: The two inline SVGs in the AuthVerifyContent component are
missing accessible labels; either add an explicit <title> with a unique id and
set role="img" plus aria-labelledby to expose meaningful labels for screen
readers, or mark purely decorative SVGs as aria-hidden="true" and
focusable="false"; update both SVG instances in AuthVerifyContent (the
error/close icon and the other inline SVG) accordingly so they pass a11y
linting.

In `@apps/web/app/globals.css`:
- Around line 142-143: The CSS triggers a stylelint rule
"declaration-empty-line-before" between the `@apply` directive and the subsequent
box-shadow declaration; fix by inserting a blank line between the `@apply` rule
(the line containing "@apply bg-surface border border-accent/30 rounded-xl;")
and the "box-shadow" declaration (the line with "box-shadow: 0 0 40px
var(--color-accent-glow);"), or alternatively update the stylelint rule
configuration if you prefer no blank line.
- Around line 56-58: The global attribute selector [data-active='true'] is too
broad and causes style bleed; narrow its scope by qualifying it with a
component- or region-specific ancestor (e.g., .your-component
[data-active='true'] or .nav [data-active='true']) so only intended elements get
the purple color, and update any places that relied on the global selector to
use the new scoped selector (reference the existing [data-active='true']
selector to locate and replace).
- Around line 264-271: The reduced-motion media query omits the
.animate-fade-in-up selector so that fade-in-up animations still run for users
who request reduced motion; update the `@media` (prefers-reduced-motion: reduce)
selector list in globals.css to include .animate-fade-in-up (the same place
where .animate-shiny-text, .animate-marquee, etc. are listed) so the fade-in-up
animation is disabled for reduced-motion preferences.
- Around line 187-189: Stylelint is flagging valid Tailwind v4 at-rules like
`@utility` and `@theme`; update the Stylelint config (.stylelintrc.json) to adjust
the at-rule-no-unknown rule by adding ignoreAtRules and include "theme",
"utility", "apply", "import", and "tailwind" so `@utility` and `@theme` blocks in
globals.css no longer error; modify the "rules" object to set at-rule-no-unknown
to ["error", { "ignoreAtRules": ["theme","utility","apply","import","tailwind"]
}].

In `@apps/web/components/FaqAccordion.tsx`:
- Around line 39-52: The map callback defines an unused parameter `index`
causing an ESLint warning; remove the unused parameter by changing the map
callback signature from items.map((item, index) => ...) to items.map((item) =>
...), keeping the rest of the JSX (AccordionItem, AccordionTrigger,
AccordionContent) unchanged so keys and value generation still use
item.question.

In `@apps/web/components/Footer.tsx`:
- Around line 38-61: The inline SVG icons for the social links (the icon nodes
for labels 'GitHub', 'X', and 'Blog' in Footer.tsx) are missing accessible
titles; insert a <title> element inside each SVG with a descriptive name (e.g.,
"GitHub", "X", "Blog") so screen readers can announce them, or alternatively add
aria-hidden="true" to each SVG if you want the parent anchor's aria-label to be
the sole accessible name; update the SVG elements where label: 'GitHub' / label:
'X' / label: 'Blog' are defined accordingly.

In `@apps/web/components/landing/CreatorStory.tsx`:
- Line 45: The Tailwind classes on the Github icon in CreatorStory.tsx use
invalid tokens `h-4.5 w-4.5`; update the Github element(s) (the JSX where
<Github className="h-4.5 w-4.5" /> appears) to use valid spacing tokens such as
`h-5 w-5` or explicit values like `h-[18px] w-[18px]`; make the same replacement
for the other two occurrences of the Github icon in this component (the other
<Github ... /> instances currently using `h-4.5 w-4.5`).

In `@apps/web/components/landing/Hero.tsx`:
- Line 28: The hero currently uses PLACEHOLDER_VIDEO in the Hero component to
power the interactive preview; remove/stop using that placeholder by disabling
the demo affordance until a real demo exists: update Hero to conditionally
render the modal trigger and prevent opening (e.g., gated by a boolean like
hasDemo or omit calling setIsOpen) when PLACEHOLDER_VIDEO is still set, or
replace the interactive iframe preview with a static fallback (thumbnail/image
or disabled button) so no placeholder URL is loaded; ensure you update any other
occurrences of PLACEHOLDER_VIDEO or modal-opening logic (e.g., the modal
component or state setters used for the preview) in the same file to match this
behavior.
- Around line 167-209: Replace the manual backdrop/modal implementation that
uses isVideoOpen, setIsVideoOpen, AnimatePresence and motion.div with a Radix
Dialog primitive: use Dialog.Root controlling open state, Dialog.Trigger for
opening, Dialog.Portal/Dialog.Overlay for the backdrop (remove role="button" and
manual onKeyDown handlers), Dialog.Content for the centered video container
(keep motion animations if desired but wrap them inside Radix primitives), and
Dialog.Close for the XIcon button (add an explicit aria-label like "Close
video"); ensure focus is trapped/restored by Radix and preserve the iframe
(PLACEHOLDER_VIDEO) inside Dialog.Content while keeping onClick stopPropagation
on the inner container to prevent overlay close.

In `@apps/web/components/landing/Testimonials.tsx`:
- Around line 7-52: The testimonials currently render the hard-coded "reviews"
array as named, real-looking customer quotes (const reviews) which must not be
shipped; either remove or neutralize these fabricated entries: replace the
objects in reviews with non-identifiable demo placeholders (e.g., anonymous
roles like "Product User", generic text or marketing-safe copy) or gate the
entire Testimonials component behind a feature flag/ENV check so it does not
render in production until real, verified quotes are provided; update the
component that consumes reviews (Testimonials) to use the new placeholder data
or early-return when the flag is off.

In `@apps/web/components/landing/VideoGuides.tsx`:
- Around line 51-56: Replace the interactive button with a non-interactive
element (or disable it) when there is no video: check the hasVideo boolean
(derived from videoSrc) and if false render a div/span wrapper (or render the
same button element with disabled and aria-disabled="true" and remove the
onClick handler) so the item is not keyboard-focusable or clickable; ensure you
adjust the element that currently uses setIsOpen (the onClick tied to setIsOpen)
and preserve styling (className) while adding a clear non-interactive state
(cursor-default, no pointer events) for the component in VideoGuides.tsx.
- Around line 103-139: Replace the backdrop div/dialog markup in VideoGuides.tsx
with proper dialog semantics: when isOpen && hasVideo render a container with
role="dialog" and aria-modal="true", move the Escape key handling out of the
backdrop onKeyDown into a document/keydown handler (or use a focus-trap library)
so Escape closes via setIsOpen regardless of which element has focus, implement
focus management to move focus into the dialog on open and restore the
previously focused element on close, trap tab focus inside the dialog while open
and prevent background scrolling, and give the icon-only close button (the XIcon
button) an accessible name (e.g., aria-label="Close video") so it is
keyboard/screen-reader operable; keep the iframe usage but ensure it does not
steal initial focus.

In `@apps/web/components/landing/WhyLocal.tsx`:
- Around line 272-293: The table in WhyLocal.tsx (the <table className="w-full
text-sm"> inside the component rendering rows and CellValue) lacks an accessible
name; add a concise caption element or an aria-label on the table to describe
its purpose (for example "Feature comparison: Readied vs Cloud Apps") so screen
readers announce the table's purpose before its contents; update the JSX to
include either a visually hidden <caption> or an aria-label attribute on that
table element accordingly.

In `@apps/web/components/magicui/animated-beam.tsx`:
- Around line 87-100: The ResizeObserver only observes containerRef so the beam
path (computed in updatePath using fromRef and toRef bounding rects) can become
stale when those endpoints resize; update the effect to observe fromRef.current
and toRef.current as well (or add a window 'resize' fallback) by attaching the
same ResizeObserver to each element if present and disconnecting them all in the
cleanup, ensuring updatePath is called on observations for containerRef,
fromRef, and toRef.
- Around line 103-110: The decorative SVG in the AnimatedBeam component should
be hidden from assistive technologies; update the <svg> element (the SVG in
animated-beam.tsx that uses svgDimensions and className) to include
aria-hidden="true" so screen readers ignore it, preserving existing attributes
and behavior.

In `@apps/web/components/magicui/animated-grid-pattern.tsx`:
- Around line 117-129: The current key for motion.rect in the squares.map uses
position-based values (`key={`${x}-${y}-${index}`}`) which changes when
updateSquarePosition mutates pos and causes React to remount the element; change
the key to use the stable square identifier instead (use the square's id) in the
map that renders motion.rect so the element identity remains stable across
position updates (update the key prop in the render inside squares.map where
motion.rect and onAnimationComplete call updateSquarePosition).
- Line 54: The initial call to useState(() => generateSquares(numSquares)) runs
while dimensions is {width: 0, height: 0}, causing getPos to compute invalid
positions (all [0,0]); change the initialization to start with an empty array
and populate squares once real dimensions are available: update the useState for
squares to initialize to [] and modify the existing useEffect that depends on
dimensions (the one that currently calls setSquares) to call
generateSquares(numSquares) only when dimensions.width and dimensions.height are
> 0, using setSquares to replace the empty array; reference functions/vars
generateSquares, getPos, dimensions, setSquares, and the useEffect that runs on
dimension changes.

In `@apps/web/components/magicui/animated-shiny-text.tsx`:
- Around line 8-32: AnimatedShinyText currently sets a style with the CSS
variable (--shiny-width) and then spreads {...props}, so a caller's style prop
can overwrite it; to fix, destructure style from props (e.g., const { style,
...rest } = props) and merge them when rendering (apply componentStyle = {
'--shiny-width': `${shimmerWidth}px`, ...style } or equivalent) so the
component-controlled --shiny-width is preserved while still honoring
caller-provided styles, then spread the remaining props (rest) onto the span;
reference AnimatedShinyText, shimmerWidth, props, and the inline style that sets
'--shiny-width'.

In `@apps/web/components/magicui/border-beam.tsx`:
- Around line 76-100: The beam currently always animates; detect reduced-motion
preferences (e.g., import and call useReducedMotion from 'framer-motion' or use
matchMedia('(prefers-reduced-motion: reduce)') ) inside the BorderBeam component
and, when reduced motion is requested, avoid the infinite animation by not
passing an animate/transition or by setting initial and animate to the same
static offsetDistance value (use the computed `${initialOffset}%` or `${100 -
initialOffset}%`/`${-initialOffset}%` depending on reverse) so the motion.div
stays static; update the motion.div usage (the initial/animate/transition props)
to branch on the reduced-motion flag and ensure
style.offsetPath/width/--color-from/--color-to remain unchanged.

In `@apps/web/components/magicui/dot-pattern.tsx`:
- Around line 21-64: The DotPattern component currently uses width and height
directly in grid math which will break if width or height are zero/negative; in
the DotPattern function validate and normalize props before calculating dots
(e.g., coerce width/height to a positive minimum like 1 or bail out), and use
those safe values in the Array.from length and in the col/row computation
(references: DotPattern, width, height, dimensions, dots variable) so you never
divide by or mod by zero and rendering won't fail.

In `@apps/web/components/magicui/hero-video-dialog.tsx`:
- Around line 111-132: Overlay currently uses role="button" and closes via
bubbling; change the outer motion.div to role="dialog" with aria-modal="true"
and an accessible label (aria-labelledby or aria-label), and restrict closing:
keep Escape handling but ensure it only closes when the event is for the dialog
(attach onKeyDown to the dialog or check e.key === 'Escape'), and change the
onClick close behavior to only close when the click target is the overlay itself
(compare event.target === event.currentTarget) so clicks inside the inner
motion.div (the element using selectedAnimation) don’t bubble out; make the
close control an explicit interactive button (the motion.button) with an onClick
handler that calls setIsVideoOpen(false), add an aria-label (e.g., "Close video
dialog"), and mark the XIcon as aria-hidden to avoid duplicate announcement.
- Around line 88-94: Replace the plain <img> in hero-video-dialog.tsx with
Next.js' optimized Image component: import Image from "next/image", then render
<Image> using the existing thumbnailSrc and thumbnailAlt props and the same
width/height and className; add appropriate Image props such as priority or
sizes for a large hero image (or explicitly pass unoptimized if you have a
reason to skip optimization) to enable automatic responsive loading and
optimization. Ensure you update any surrounding attributes or styling expected
by the current img (className "w-full rounded-xl..." and width/height values) so
the visual output remains unchanged.

In `@apps/web/components/magicui/marquee.tsx`:
- Around line 45-74: The marquee currently always renders repeated animated
copies; detect prefers-reduced-motion and collapse to one static copy by (1)
making repeated items non-render/hidden in reduced-motion: update the mapping
over Array(safeRepeat) so duplicates (i > 0) get a class like
'motion-reduce:hidden' and the animated container also gets
'motion-reduce:animate-none' (or remove animation CSS) so motion is disabled;
(2) ensure accessibility for hidden duplicates by retaining aria-hidden and
tabIndex: -1 on duplicates (the existing {...(i > 0 ? { 'aria-hidden': true,
tabIndex: -1 } : {})}); and (3) optionally make safeRepeat respect reduced
motion by checking window.matchMedia('(prefers-reduced-motion: reduce)') in a
small effect/hook and setting safeRepeat to 1 during SSR-safe initialization —
update references to safeRepeat, the duplicated inner div key/i check, and the
container classnames to implement these changes.

In `@apps/web/components/magicui/number-ticker.tsx`:
- Around line 53-56: The number formatting is hardcoded to 'en-US' in the
ref.current.textContent assignment (and similarly at the other formatting site
around lines 71-74), which breaks i18n; update the NumberTicker component to
accept an optional locale prop (e.g., locale?: string) and use that when
creating Intl.NumberFormat, falling back to undefined to use the user's browser
locale if no prop is provided, and update both places where Intl.NumberFormat is
constructed to reference this prop (or undefined) instead of the literal
'en-US'.
- Around line 53-56: Refactor the assignment to ref.current.textContent in
number-ticker.tsx to remove the redundant toFixed call: instead of
Number(latest.toFixed(decimalPlaces)) pass the numeric value (e.g.,
Number(latest) or latest if already a number) directly into Intl.NumberFormat
with minimumFractionDigits/maximumFractionDigits set to decimalPlaces; update
the expression in the ref.current.textContent assignment that references latest
and decimalPlaces to rely on Intl.NumberFormat for precision rather than using
toFixed.

In `@apps/web/components/magicui/shimmer-button.tsx`:
- Around line 13-46: The ShimmerButton component currently relies on the browser
default button type (submit); update the component to ensure a safe default by
applying type="button" when no type is provided: inside the forwardRef
functional component for ShimmerButton, ensure the rendered <button> uses the
incoming props.type if present, otherwise sets type="button" (e.g., set a
default via destructuring or coalesce before spreading props) so accidental form
submissions are prevented while preserving any explicit type passed to
ShimmerButton.

In `@apps/web/components/magicui/text-reveal.tsx`:
- Around line 12-49: Detect the user's reduced-motion preference (e.g., via
framer-motion's useReducedMotion or window.matchMedia('(prefers-reduced-motion:
reduce)') ) inside the TextReveal component and when reduced motion is requested
skip the scroll-driven animation: do not call/use the
scrollYProgress/useScroll-driven Word animation, and instead render the children
as plain static text (no 200vh wrapper or sticky animated layout) preserving
className and accessibility; update TextReveal to branch (reducedMotion ? static
render : existing animated render) so Word and scrollYProgress are not used when
reduced motion is enabled.

In `@apps/web/components/Navbar.tsx`:
- Around line 117-137: The Docs dropdown button in Navbar is missing ARIA
attributes; add aria-haspopup="true" to the button element (the button rendering
"Docs" with class navLinkClass) and either implement a controlled open state
(e.g., isDocsOpen boolean in the Navbar component, toggle it in the button's
onClick and set aria-expanded={isDocsOpen}, and replace CSS
group-hover/group-focus-within toggles with classnames driven by isDocsOpen) or,
for a minimal fix, at least add aria-haspopup="true" to the existing button so
screen readers know it opens a menu.
- Around line 245-268: In the section rendering links (inside section.links.map
in Navbar.tsx) simplify the conditional that currently uses "'external' in link
&& link.external" to just check "link.external"; update the ternary that chooses
between the <a> with ExternalIcon and the <Link> to use "link.external" as the
predicate so external undefined/false behaves correctly and keep the rest
(closeSheet, href, rel, target, className) unchanged.
- Around line 69-84: Replace the hardcoded hex color in the ExternalIcon SVG by
using the design system's semantic text color class (e.g., replace
className="... text-[`#71717a`]" with the appropriate token such as
"text-text-secondary" or "text-muted-foreground") in the ExternalIcon component;
locate other occurrences of the same hex string elsewhere in this file (the
other inline SVG/icon usages that also use "#71717a") and apply the same swap so
all icons use the semantic token consistently.

In `@apps/web/components/ui/button.tsx`:
- Around line 31-39: The Button component forwards a default type prop
unconditionally which leaks button-only attributes onto non-button renderings
when asChild is true; update the render logic in Button (the forwardRef function
using asChild and Comp/Slot) to only pass type={type} when rendering a real
button (i.e., when asChild is false or Comp === 'button'), omitting the type
prop for the Slot branch so anchors/Next Link children don't receive button-only
props.

In `@apps/web/mdx-components.tsx`:
- Around line 11-28: Export a plain mdxComponents object that merges
defaultComponents with the explicit component registry (Card, Cards, Callout,
Step, Steps, Tab, Tabs, Accordion, Accordions, File, Folder, Files, TypeTable)
and then change useMDXComponents to build on that by returning
{...mdxComponents, ...components}; update the module to export both
mdxComponents and useMDXComponents so the docs runtime can import the base
registry directly while the hook still supports runtime overrides.

In `@docs/plans/2026-03-12-roadmap-auth-sync-ai.md`:
- Around line 9-17: The roadmap still marks GET /auth/me with bad JWT as
returning 500 and flagged as a bug—update the table entry for `GET /auth/me` to
show 401 Unauthorized (remove the **Bug** marker and change the Notes to
indicate the invalid-JWT handling was fixed in this PR), and make the same
change to the duplicate occurrence further down (the block currently referenced
around lines 57–60) so both entries reflect the fix.

In `@docs/plans/2026-03-12-website-redesign-design.md`:
- Around line 116-130: The markdown code fence for the file tree under
"apps/web/" is missing a language tag which triggers MD040; update the
triple-backtick fence to include a language (use "text") so the block becomes
```text at the start and ``` at the end; locate the fenced block that begins
with "apps/web/" (contains components/, lib/, app/, globals.css, layout.tsx) and
add the language tag to fix the lint error.

In `@docs/plans/2026-03-12-website-redesign-implementation.md`:
- Around line 438-452: The fenced code block containing the "Structure:" ASCII
tree triggers markdownlint MD040 because it has no language; update that block
by adding a language identifier (e.g., change the opening triple backticks to
```text) for the block that starts with "Structure:" so the linter recognizes it
and MD040 is resolved.

---

Outside diff comments:
In `@apps/web/app/`(marketing)/auth/verify/AuthVerifyContent.tsx:
- Around line 14-22: The deep-link is built with the raw token which can produce
malformed readied:// URLs; update the places that construct the deep-link (the
assignment to window.location.href in the useEffect and the other occurrence
around lines 85-87) to URL-encode the token using encodeURIComponent(token)
before interpolating it into `readied://auth/verify?token=...` so the token is
safely escaped when building the deep link.

In `@apps/web/app/`(marketing)/download/page.tsx:
- Around line 13-15: The metadata.title/description in the Download page uses
both "Try free for {config.trialDays} days" and "Free forever", which conflicts
with the product pricing; update the copy in the metadata object (and the
corresponding marketing text block later in the same file around the download
page content) to match the actual model in packages/product-config/src/facade.ts
(e.g., use either a trial-focused message that references config.trialDays or a
perpetual free-tier message), then propagate the same wording to the related
marketing/legal templates (terms.astro, privacy.astro) so messaging is
consistent across metadata, the page content, and legal pages.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8de387e0-45b5-4a8a-baae-2d27ba653b46

📥 Commits

Reviewing files that changed from the base of the PR and between 21be0a5 and 06a08a9.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (66)
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/renderer/components/sidebar/Sidebar.tsx
  • apps/desktop/src/renderer/components/sidebar/SidebarFooter.tsx
  • apps/desktop/src/renderer/components/sync/LoginModal.module.css
  • apps/desktop/src/renderer/components/sync/LoginModal.tsx
  • apps/desktop/src/renderer/components/sync/index.ts
  • apps/desktop/src/renderer/hooks/useSyncOnboarding.ts
  • apps/desktop/src/renderer/styles/global.css
  • apps/web/app/(marketing)/auth/verify/AuthVerifyContent.tsx
  • apps/web/app/(marketing)/changelog/page.tsx
  • apps/web/app/(marketing)/download/page.tsx
  • apps/web/app/(marketing)/faq/page.tsx
  • apps/web/app/(marketing)/page.tsx
  • apps/web/app/(marketing)/philosophy/page.tsx
  • apps/web/app/(marketing)/plugins/page.tsx
  • apps/web/app/(marketing)/pricing/page.tsx
  • apps/web/app/(marketing)/privacy/page.tsx
  • apps/web/app/(marketing)/terms/page.tsx
  • apps/web/app/docs/[[...slug]]/page.tsx
  • apps/web/app/docs/layout.tsx
  • apps/web/app/globals.css
  • apps/web/app/layout.tsx
  • apps/web/components/FaqAccordion.tsx
  • apps/web/components/Footer.tsx
  • apps/web/components/MobileNav.tsx
  • apps/web/components/NavDropdown.tsx
  • apps/web/components/Navbar.tsx
  • apps/web/components/landing/Audience.tsx
  • apps/web/components/landing/ComparisonTable.tsx
  • apps/web/components/landing/CreatorStory.tsx
  • apps/web/components/landing/Features.tsx
  • apps/web/components/landing/Hero.tsx
  • apps/web/components/landing/SocialProof.tsx
  • apps/web/components/landing/Testimonials.tsx
  • apps/web/components/landing/VideoGuides.tsx
  • apps/web/components/landing/WhyLocal.tsx
  • apps/web/components/magicui/animated-beam.tsx
  • apps/web/components/magicui/animated-grid-pattern.tsx
  • apps/web/components/magicui/animated-shiny-text.tsx
  • apps/web/components/magicui/border-beam.tsx
  • apps/web/components/magicui/dot-pattern.tsx
  • apps/web/components/magicui/hero-video-dialog.tsx
  • apps/web/components/magicui/marquee.tsx
  • apps/web/components/magicui/number-ticker.tsx
  • apps/web/components/magicui/shimmer-button.tsx
  • apps/web/components/magicui/text-reveal.tsx
  • apps/web/components/ui/accordion.tsx
  • apps/web/components/ui/badge.tsx
  • apps/web/components/ui/button.tsx
  • apps/web/components/ui/card.tsx
  • apps/web/components/ui/separator.tsx
  • apps/web/components/ui/sheet.tsx
  • apps/web/content/docs/architecture/overview.mdx
  • apps/web/content/docs/guide/principles.mdx
  • apps/web/content/docs/index.mdx
  • apps/web/content/docs/plugins/getting-started.mdx
  • apps/web/lib/layout.shared.tsx
  • apps/web/lib/utils.ts
  • apps/web/mdx-components.tsx
  • apps/web/package.json
  • docs/plans/2026-03-12-roadmap-auth-sync-ai.md
  • docs/plans/2026-03-12-website-redesign-design.md
  • docs/plans/2026-03-12-website-redesign-implementation.md
  • packages/api/src/index.ts
  • packages/api/src/middleware/auth.ts
  • packages/product-config/src/index.ts
💤 Files with no reviewable changes (3)
  • apps/web/components/landing/ComparisonTable.tsx
  • apps/web/components/NavDropdown.tsx
  • apps/web/components/MobileNav.tsx

Comment thread apps/desktop/src/renderer/components/sync/LoginModal.tsx Outdated
Comment thread apps/desktop/src/renderer/components/sync/LoginModal.tsx Outdated
Comment on lines +1491 to +1539
.sidebar-sync-prompt {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
margin: 0 8px 4px;
background: var(--accent-subtle, rgba(59, 130, 246, 0.08));
border-radius: var(--radius-sm, 4px);
font-size: var(--text-xs);
color: var(--text-secondary);
line-height: 1.3;
}

.sidebar-sync-prompt-actions {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
margin-left: 8px;
}

.sidebar-sync-prompt-actions button:first-child {
background: var(--accent);
color: white;
border: none;
border-radius: var(--radius-sm, 4px);
padding: 2px 8px;
font-size: var(--text-xs);
cursor: pointer;
font-weight: 500;
}

.sidebar-sync-prompt-actions button:first-child:hover {
opacity: 0.9;
}

.sidebar-sync-prompt-actions button:last-child {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 14px;
padding: 0 2px;
line-height: 1;
}

.sidebar-sync-prompt-actions button:last-child:hover {
color: var(--text-secondary);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, check the current state of the file at the specified lines
sed -n '1491,1539p' apps/desktop/src/renderer/styles/global.css

Repository: tomymaritano/readide

Length of output: 1108


🏁 Script executed:

# Check for .sidebar definition and -webkit-app-region settings
rg -n "\.sidebar\s*\{" apps/desktop/src/renderer/styles/global.css -A 5

Repository: tomymaritano/readide

Length of output: 205


🏁 Script executed:

# Check for other interactive sidebar sections
rg -n "\.sidebar-(nav|footer)" apps/desktop/src/renderer/styles/global.css -A 3

Repository: tomymaritano/readide

Length of output: 1310


🏁 Script executed:

# Search for all -webkit-app-region usage in the file
rg -n "\-webkit-app-region" apps/desktop/src/renderer/styles/global.css

Repository: tomymaritano/readide

Length of output: 510


🏁 Script executed:

# Check how .sidebar-nav and .sidebar-footer handle their child elements
rg -n "\.sidebar-(nav|footer)" apps/desktop/src/renderer/styles/global.css -A 10 | head -40

Repository: tomymaritano/readide

Length of output: 926


🏁 Script executed:

# Search for any button-specific -webkit-app-region rules in the file
rg -n "button.*-webkit-app-region|\-webkit-app-region.*button" apps/desktop/src/renderer/styles/global.css

Repository: tomymaritano/readide

Length of output: 46


🏁 Script executed:

# Check if there are nested button rules under .sidebar-nav
sed -n '124,200p' apps/desktop/src/renderer/styles/global.css

Repository: tomymaritano/readide

Length of output: 1501


Add -webkit-app-region: no-drag to the onboarding prompt.

.sidebar is a draggable title-bar region. Other interactive sidebar sections (.sidebar-nav, .sidebar-footer) explicitly opt out with -webkit-app-region: no-drag on their containers. This new prompt lacks that property, so its buttons will drag the window instead of receiving clicks on custom-titlebar builds.

🔧 Suggested fix
 .sidebar-sync-prompt {
+  -webkit-app-region: no-drag;
   display: flex;
   align-items: center;
   justify-content: space-between;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.sidebar-sync-prompt {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
margin: 0 8px 4px;
background: var(--accent-subtle, rgba(59, 130, 246, 0.08));
border-radius: var(--radius-sm, 4px);
font-size: var(--text-xs);
color: var(--text-secondary);
line-height: 1.3;
}
.sidebar-sync-prompt-actions {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
margin-left: 8px;
}
.sidebar-sync-prompt-actions button:first-child {
background: var(--accent);
color: white;
border: none;
border-radius: var(--radius-sm, 4px);
padding: 2px 8px;
font-size: var(--text-xs);
cursor: pointer;
font-weight: 500;
}
.sidebar-sync-prompt-actions button:first-child:hover {
opacity: 0.9;
}
.sidebar-sync-prompt-actions button:last-child {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 14px;
padding: 0 2px;
line-height: 1;
}
.sidebar-sync-prompt-actions button:last-child:hover {
color: var(--text-secondary);
}
.sidebar-sync-prompt {
-webkit-app-region: no-drag;
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
margin: 0 8px 4px;
background: var(--accent-subtle, rgba(59, 130, 246, 0.08));
border-radius: var(--radius-sm, 4px);
font-size: var(--text-xs);
color: var(--text-secondary);
line-height: 1.3;
}
.sidebar-sync-prompt-actions {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
margin-left: 8px;
}
.sidebar-sync-prompt-actions button:first-child {
background: var(--accent);
color: white;
border: none;
border-radius: var(--radius-sm, 4px);
padding: 2px 8px;
font-size: var(--text-xs);
cursor: pointer;
font-weight: 500;
}
.sidebar-sync-prompt-actions button:first-child:hover {
opacity: 0.9;
}
.sidebar-sync-prompt-actions button:last-child {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 14px;
padding: 0 2px;
line-height: 1;
}
.sidebar-sync-prompt-actions button:last-child:hover {
color: var(--text-secondary);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/styles/global.css` around lines 1491 - 1539, The
onboarding prompt (.sidebar-sync-prompt) is missing -webkit-app-region: no-drag
so its buttons can receive clicks in custom-titlebar builds; update the CSS for
.sidebar-sync-prompt (and optionally .sidebar-sync-prompt-actions) to include
-webkit-app-region: no-drag alongside existing rules so the prompt and its
action container opt out of the draggable sidebar region and allow button
interaction.

Comment thread apps/web/app/(marketing)/auth/verify/AuthVerifyContent.tsx
Comment thread apps/web/app/globals.css Outdated
Comment on lines +31 to +39
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, type = 'button', ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
type={type}
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Only default type="button" for the native button branch.

asChild is meant to let this component render links, so always forwarding type leaks button-only props onto anchors/Next links. Keep the default type on the <button> path and omit it when Slot is used. (ui.shadcn.com)

Proposed fix
-const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
-  ({ className, variant, size, asChild = false, type = 'button', ...props }, ref) => {
+const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
+  ({ className, variant, size, asChild = false, type, ...props }, ref) => {
     const Comp = asChild ? Slot : 'button';
     return (
       <Comp
-        type={type}
+        {...(asChild ? {} : { type: type ?? 'button' })}
         className={cn(buttonVariants({ variant, size, className }))}
         ref={ref}
         {...props}
       />
     );
   }
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, type = 'button', ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
type={type}
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, type, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
{...(asChild ? {} : { type: type ?? 'button' })}
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/ui/button.tsx` around lines 31 - 39, The Button component
forwards a default type prop unconditionally which leaks button-only attributes
onto non-button renderings when asChild is true; update the render logic in
Button (the forwardRef function using asChild and Comp/Slot) to only pass
type={type} when rendering a real button (i.e., when asChild is false or Comp
=== 'button'), omitting the type prop for the Slot branch so anchors/Next Link
children don't receive button-only props.

Comment on lines 11 to +28
export function useMDXComponents(components: MDXComponents): MDXComponents {
return { ...defaultComponents, ...components };
return {
...defaultComponents,
Card,
Cards,
Callout,
Step,
Steps,
Tab,
Tabs,
Accordion,
Accordions,
File,
Folder,
Files,
TypeTable,
...components,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Export the base MDX registry from this module.

apps/web/app/docs/[[...slug]]/page.tsx now has to duplicate the same component list because this file only exposes the function form. Export a plain mdxComponents object here and build useMDXComponents() on top of it so the docs runtime and the shared MDX registry stay in sync.

Proposed refactor
+export const mdxComponents: MDXComponents = {
+  ...defaultComponents,
+  Card,
+  Cards,
+  Callout,
+  Step,
+  Steps,
+  Tab,
+  Tabs,
+  Accordion,
+  Accordions,
+  File,
+  Folder,
+  Files,
+  TypeTable,
+};
+
 export function useMDXComponents(components: MDXComponents): MDXComponents {
   return {
-    ...defaultComponents,
-    Card,
-    Cards,
-    Callout,
-    Step,
-    Steps,
-    Tab,
-    Tabs,
-    Accordion,
-    Accordions,
-    File,
-    Folder,
-    Files,
-    TypeTable,
+    ...mdxComponents,
     ...components,
   };
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function useMDXComponents(components: MDXComponents): MDXComponents {
return { ...defaultComponents, ...components };
return {
...defaultComponents,
Card,
Cards,
Callout,
Step,
Steps,
Tab,
Tabs,
Accordion,
Accordions,
File,
Folder,
Files,
TypeTable,
...components,
};
export const mdxComponents: MDXComponents = {
...defaultComponents,
Card,
Cards,
Callout,
Step,
Steps,
Tab,
Tabs,
Accordion,
Accordions,
File,
Folder,
Files,
TypeTable,
};
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
...mdxComponents,
...components,
};
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/mdx-components.tsx` around lines 11 - 28, Export a plain
mdxComponents object that merges defaultComponents with the explicit component
registry (Card, Cards, Callout, Step, Steps, Tab, Tabs, Accordion, Accordions,
File, Folder, Files, TypeTable) and then change useMDXComponents to build on
that by returning {...mdxComponents, ...components}; update the module to export
both mdxComponents and useMDXComponents so the docs runtime can import the base
registry directly while the hook still supports runtime overrides.

Comment on lines +9 to +17
## Backend Status (Verified 2026-03-12)

| Endpoint | Status | Notes |
| ------------------------ | ----------- | ----------------------------------- |
| `GET /health` | 200 OK | API live on Cloudflare Workers |
| `POST /auth/magic-link` | 200 OK | Resend email delivery working |
| `POST /auth/verify` | 400 (valid) | Zod validates UUID token format |
| `GET /auth/me` + bad JWT | **500** | **Bug**: should return 401, not 500 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Roadmap status is stale relative to this PR’s backend fix.

Line 16 and Line 59 still mark the invalid JWT behavior as an open 500→401 bug, but this PR explicitly states it was fixed. Please update these entries to avoid tracking drift.

Suggested doc update
-| `GET /auth/me` + bad JWT | **500**     | **Bug**: should return 401, not 500 |
+| `GET /auth/me` + bad JWT | 401         | Fixed on 2026-03-12 (invalid JWT now returns HTTPException 401) |

-- [ ] Fix `/auth/me` returning 500 on invalid JWT (should be 401)
+- [x] Fix `/auth/me` returning 500 on invalid JWT (now returns 401)

Also applies to: 57-60

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-03-12-roadmap-auth-sync-ai.md` around lines 9 - 17, The
roadmap still marks GET /auth/me with bad JWT as returning 500 and flagged as a
bug—update the table entry for `GET /auth/me` to show 401 Unauthorized (remove
the **Bug** marker and change the Notes to indicate the invalid-JWT handling was
fixed in this PR), and make the same change to the duplicate occurrence further
down (the block currently referenced around lines 57–60) so both entries reflect
the fix.

Comment on lines +116 to +130
```
apps/web/
├── components/
│ ├── ui/ ← shadcn/ui (Button, Card, Badge, etc.)
│ ├── magicui/ ← Magic UI (copied source, not npm)
│ ├── layout/ ← Navbar, Footer, MobileNav
│ └── landing/ ← Hero, Features, WhyLocal, Pricing, etc.
├── lib/
│ └── utils.ts ← cn() helper
├── app/
│ ├── globals.css ← Design tokens + shadcn CSS vars
│ ├── layout.tsx ← Root layout with fonts
│ ├── (marketing)/ ← Same routes
│ └── docs/ ← Fumadocs (untouched)
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Specify a language for the file-structure fence.

This code block is missing a language tag, which triggers markdownlint (MD040).

📝 Proposed fix
-```
+```text
 apps/web/
 ├── components/
 │   ├── ui/              ← shadcn/ui (Button, Card, Badge, etc.)
 │   ├── magicui/         ← Magic UI (copied source, not npm)
 │   ├── layout/          ← Navbar, Footer, MobileNav
 │   └── landing/         ← Hero, Features, WhyLocal, Pricing, etc.
 ├── lib/
 │   └── utils.ts         ← cn() helper
 ├── app/
 │   ├── globals.css      ← Design tokens + shadcn CSS vars
 │   ├── layout.tsx       ← Root layout with fonts
 │   ├── (marketing)/     ← Same routes
 │   └── docs/            ← Fumadocs (untouched)
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 129-129: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-03-12-website-redesign-design.md` around lines 116 - 130, The
markdown code fence for the file tree under "apps/web/" is missing a language
tag which triggers MD040; update the triple-backtick fence to include a language
(use "text") so the block becomes ```text at the start and ``` at the end;
locate the fenced block that begins with "apps/web/" (contains components/,
lib/, app/, globals.css, layout.tsx) and add the language tag to fix the lint
error.

Comment on lines +438 to +452
```
Structure:
├── DotPattern (absolute, behind everything)
├── AnimatedShinyText badge ("v0.6 · Early access")
├── h1: "Your markdown, your machine, your rules."
│ └── gradient-text on middle line
├── Subtitle paragraph
├── CTAs row:
│ ├── ShimmerButton "Download" (primary)
│ └── Button variant="ghost" "View on GitHub"
├── Trust badges row (Offline, Cross-platform, Free/Pro trial)
└── Editor screenshot
├── <img> (same hero-editor.svg)
└── BorderBeam frame (animated glow border)
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a language identifier for the fenced block.

The fenced block starting at Line 438 has no language and triggers markdownlint (MD040).

📝 Proposed fix
-```
+```text
 Structure:
 ├── DotPattern (absolute, behind everything)
 ├── AnimatedShinyText badge ("v0.6 · Early access")
 ├── h1: "Your markdown, your machine, your rules."
 │   └── gradient-text on middle line
 ├── Subtitle paragraph
 ├── CTAs row:
 │   ├── ShimmerButton "Download" (primary)
 │   └── Button variant="ghost" "View on GitHub"
 ├── Trust badges row (Offline, Cross-platform, Free/Pro trial)
 └── Editor screenshot
     ├── <img> (same hero-editor.svg)
     └── BorderBeam frame (animated glow border)
</details>

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.21.0)</summary>

[warning] 438-438: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @docs/plans/2026-03-12-website-redesign-implementation.md around lines 438 -
452, The fenced code block containing the "Structure:" ASCII tree triggers
markdownlint MD040 because it has no language; update that block by adding a
language identifier (e.g., change the opening triple backticks to ```text) for
the block that starts with "Structure:" so the linter recognizes it and MD040 is
resolved.


</details>

<!-- fingerprinting:phantom:poseidon:hawk -->

<!-- This is an auto-generated comment by CodeRabbit -->

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review continued from previous batch...

Comment on lines +2484 to 2495
// Send token to renderer process — queue if window isn't ready yet
const mainWin = BrowserWindow.getAllWindows().find(
win => !win.isDestroyed() && win.webContents.isLoading() === false
);
if (mainWin) {
mainWin.webContents.send('auth:verify-token', token);
mainWin.show();
mainWin.focus();
} else {
log.info('Window not ready, queuing auth token for later delivery');
pendingAuthToken = token;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's see the size of the file and understand its structure
wc -l apps/desktop/src/main/index.ts

Repository: tomymaritano/readide

Length of output: 99


🏁 Script executed:

# Search for all deep link related handlers (open-url, second-instance, argv processing)
rg -n "open-url|second-instance|pendingAuthToken" apps/desktop/src/main/index.ts

Repository: tomymaritano/readide

Length of output: 338


🏁 Script executed:

# Get context around the lines mentioned in the review (2484-2495)
sed -n '2470,2510p' apps/desktop/src/main/index.ts

Repository: tomymaritano/readide

Length of output: 1403


🏁 Script executed:

# Search for second-instance event handler
rg -n "second-instance" apps/desktop/src/main/index.ts -A 20

Repository: tomymaritano/readide

Length of output: 46


🏁 Script executed:

# Search for argv or process.argv processing related to deep links/protocols
rg -n "argv|process\.argv" apps/desktop/src/main/index.ts -B 2 -A 10

Repository: tomymaritano/readide

Length of output: 381


🏁 Script executed:

# Check if there's any protocol scheme registration or custom URL handling
rg -n "protocol|register.*protocol|setAsDefaultProtocolClient" apps/desktop/src/main/index.ts -B 2 -A 5

Repository: tomymaritano/readide

Length of output: 2551


🏁 Script executed:

# Search more broadly for where deep links might be handled on app start
rg -n "deep.*link|protocol.*handler|readied" apps/desktop/src/main/index.ts -B 2 -A 5

Repository: tomymaritano/readide

Length of output: 5011


🏁 Script executed:

# Check the app.on('ready') or app.whenReady() for any deep link processing
rg -n "app\.on\('ready|app\.whenReady|app\.isReady" apps/desktop/src/main/index.ts -B 5 -A 20

Repository: tomymaritano/readide

Length of output: 46


🏁 Script executed:

# Look for process.argv handling that might extract the protocol URL
sed -n '1,500p' apps/desktop/src/main/index.ts | rg -n "argv\|protocol" -B 2 -A 5

Repository: tomymaritano/readide

Length of output: 46


🏁 Script executed:

# Search for app event handlers more broadly
rg -n "app\.on\(" apps/desktop/src/main/index.ts | head -20

Repository: tomymaritano/readide

Length of output: 217


🏁 Script executed:

# Look for the main app initialization and startup flow
sed -n '100,300p' apps/desktop/src/main/index.ts

Repository: tomymaritano/readide

Length of output: 5421


🏁 Script executed:

# Check if there's any handling of process.argv or protocol URL parsing outside the open-url handler
rg -n "process\.argv" apps/desktop/src/main/index.ts -B 5 -A 10

Repository: tomymaritano/readide

Length of output: 405


🏁 Script executed:

# Search the entire file for second-instance event
rg -n "second-instance" apps/desktop/src/main/index.ts

Repository: tomymaritano/readide

Length of output: 46


🏁 Script executed:

# Look at the end of the file to see if there's any additional startup logic
tail -100 apps/desktop/src/main/index.ts

Repository: tomymaritano/readide

Length of output: 2880


🏁 Script executed:

# Search for any function that might handle protocol URL from argv
rg -n "argv\[|URL\(|readied://" apps/desktop/src/main/index.ts

Repository: tomymaritano/readide

Length of output: 636


Add Windows/Linux cold-start and second-instance deep link handling.

On Windows/Linux, protocol-launched auth links arrive via process.argv at startup or through a second-instance event, but the token extraction and queueing logic (lines 2478–2494) only runs from the macOS open-url handler. Extract the deep-link processing logic into a shared function and call it from: (1) app startup argv parsing, and (2) a second-instance event handler for consistency across platforms.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/index.ts` around lines 2484 - 2495, Extract the
deep-link token extraction and queuing/sending logic into a shared function
(e.g., processDeepLink or handleAuthLink) that takes a token/URL, looks up the
ready BrowserWindow via BrowserWindow.getAllWindows().find(...), and either
sends 'auth:verify-token' to webContents or assigns pendingAuthToken; then
invoke this new function from both the macOS open-url handler and from the app
startup argv parsing and the app.on('second-instance', ...) handler so
Windows/Linux cold-start and second-instance protocol links are handled the same
way as the existing macOS flow.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's understand the file structure and find the relevant code sections
cd apps/desktop/src/main
wc -l index.ts

Repository: tomymaritano/readide

Length of output: 77


🏁 Script executed:

# Check the context around lines 2484-2495
sed -n '2480,2500p' index.ts

Repository: tomymaritano/readide

Length of output: 115


🏁 Script executed:

# Look for window creation functions and mainWindow references
rg -n "createWindow|createNoteWindow|createSettingsWindow|mainWindow|BrowserWindow.getAllWindows" index.ts | head -50

Repository: tomymaritano/readide

Length of output: 112


🏁 Script executed:

# Search for auth:verify-token listener in the renderer processes
fd -e ts -e tsx . ../renderer | xargs rg -l "auth:verify-token" 2>/dev/null

Repository: tomymaritano/readide

Length of output: 276


🏁 Script executed:

# Check how windows are created and managed - look for the first occurrence of mainWindow
rg -A 5 -B 5 "let mainWindow|const mainWindow|var mainWindow" index.ts | head -40

Repository: tomymaritano/readide

Length of output: 112


🏁 Script executed:

# Check file exists and get line count
wc -l apps/desktop/src/main/index.ts

Repository: tomymaritano/readide

Length of output: 99


🏁 Script executed:

# Look at the specific lines in question (2484-2495)
sed -n '2480,2500p' apps/desktop/src/main/index.ts

Repository: tomymaritano/readide

Length of output: 818


🏁 Script executed:

# Search for window creation and management patterns
rg -n "createWindow|createNoteWindow|createSettingsWindow|mainWindow" apps/desktop/src/main/index.ts | head -50

Repository: tomymaritano/readide

Length of output: 1252


🏁 Script executed:

# Look for BrowserWindow.getAllWindows() usage
rg -n "BrowserWindow.getAllWindows" apps/desktop/src/main/index.ts

Repository: tomymaritano/readide

Length of output: 645


🏁 Script executed:

# Check for auth:verify-token listener in renderer
rg -r "auth:verify-token" apps/desktop/src/renderer/ --type ts --type tsx

Repository: tomymaritano/readide

Length of output: 91


🏁 Script executed:

# Look for window creation functions - let's see createNoteWindow and createSettingsWindow
rg -A 10 "function createNoteWindow|const createNoteWindow|function createSettingsWindow|const createSettingsWindow" apps/desktop/src/main/index.ts

Repository: tomymaritano/readide

Length of output: 670


🏁 Script executed:

# Search for how auth:verify-token is handled in the renderer
rg -A 5 "auth:verify-token" apps/desktop/src/renderer/

Repository: tomymaritano/readide

Length of output: 423


🏁 Script executed:

# Check if mainWindow is stored as a module-level variable
rg -n "^let mainWindow|^const mainWindow|^var mainWindow" apps/desktop/src/main/index.ts

Repository: tomymaritano/readide

Length of output: 46


🏁 Script executed:

# Look at the entire context around createWindow to understand window management
sed -n '237,320p' apps/desktop/src/main/index.ts

Repository: tomymaritano/readide

Length of output: 2382


🏁 Script executed:

# Search for where pendingAuthToken is declared and used
rg -n "pendingAuthToken" apps/desktop/src/main/index.ts

Repository: tomymaritano/readide

Length of output: 296


🏁 Script executed:

# Check if there's any way the mainWindow is tracked globally
rg -B 10 "createWindow()" apps/desktop/src/main/index.ts | head -50

Repository: tomymaritano/readide

Length of output: 952


🏁 Script executed:

# Check the context of where the problematic code is (around line 2484)
sed -n '2470,2510p' apps/desktop/src/main/index.ts

Repository: tomymaritano/readide

Length of output: 1403


🏁 Script executed:

# Look for any global mainWindow reference that might exist
sed -n '1,150p' apps/desktop/src/main/index.ts | grep -n "mainWindow\|settingsWindow\|noteWindow"

Repository: tomymaritano/readide

Length of output: 46


🏁 Script executed:

# Check if there's a module-level variable storing the main window
rg -n "^let |^const |^var " apps/desktop/src/main/index.ts | head -30

Repository: tomymaritano/readide

Length of output: 785


🏁 Script executed:

# Check if note windows are stored globally or tracked
rg -n "noteWindow|let.*window" apps/desktop/src/main/index.ts | grep -v "mainWindow\|settingsWindow" | head -20

Repository: tomymaritano/readide

Length of output: 484


🏁 Script executed:

# Look at the createNoteWindow function more completely
sed -n '323,362p' apps/desktop/src/main/index.ts

Repository: tomymaritano/readide

Length of output: 1227


🏁 Script executed:

# Check if note or settings windows have auth:verify-token listeners
rg -n "auth:verify-token" apps/desktop/src/main/index.ts

Repository: tomymaritano/readide

Length of output: 210


🏁 Script executed:

# Check if createNoteWindow stores the window reference
sed -n '323,365p' apps/desktop/src/main/index.ts

Repository: tomymaritano/readide

Length of output: 1342


🏁 Script executed:

# Check if there's a way to identify which window is the "main" window vs note/settings
sed -n '237,320p' apps/desktop/src/main/index.ts | grep -i "title\|web.*url"

Repository: tomymaritano/readide

Length of output: 97


🏁 Script executed:

# Check what happens in createNoteWindow and createSettingsWindow with titles
sed -n '323,400p' apps/desktop/src/main/index.ts

Repository: tomymaritano/readide

Length of output: 2208


🏁 Script executed:

# Verify that all windows share the same renderer and auth handler
cat apps/desktop/src/renderer/App.tsx | grep -A 10 "auth:verify-token"

Repository: tomymaritano/readide

Length of output: 422


🏁 Script executed:

# Check if there's any window identification mechanism
rg -n "getTitle\|getURL\|id.*window" apps/desktop/src/main/index.ts | head -20

Repository: tomymaritano/readide

Length of output: 46


🏁 Script executed:

# Check how the renderer determines which window type it is
cat apps/desktop/src/renderer/App.tsx | head -100

Repository: tomymaritano/readide

Length of output: 3845


🏁 Script executed:

# Look for any window query params that identify window type
rg -n "noteWindow|settingsWindow|query" apps/desktop/src/renderer/App.tsx | head -30

Repository: tomymaritano/readide

Length of output: 786


🏁 Script executed:

# Check if there's a way to get the main window reference elsewhere
rg -n "mainWindow\|remote.*mainWindow" apps/desktop/src/main/index.ts

Repository: tomymaritano/readide

Length of output: 46


🏁 Script executed:

# Check if auth:verify-token listener is set up anywhere else
rg -r "auth:verify-token" apps/desktop/src/ --type ts --type tsx

Repository: tomymaritano/readide

Length of output: 91


🏁 Script executed:

# Verify the query param logic in App.tsx more thoroughly
sed -n '765,785p' apps/desktop/src/renderer/App.tsx

Repository: tomymaritano/readide

Length of output: 599


🏁 Script executed:

# Check what NoteWindow component is and if it sets up auth listener
rg -n "export.*NoteWindow\|function NoteWindow" apps/desktop/src/renderer/

Repository: tomymaritano/readide

Length of output: 46


🏁 Script executed:

# Check if there's a global reference to mainWindow
sed -n '81,100p' apps/desktop/src/main/index.ts

Repository: tomymaritano/readide

Length of output: 934


🏁 Script executed:

# Search for any way mainWindow is stored or referenced globally after createWindow
rg -n "let mainWindow|const mainWindow|global.*mainWindow|exports.*mainWindow" apps/desktop/src/main/index.ts

Repository: tomymaritano/readide

Length of output: 108


🏁 Script executed:

# Verify that the auth listener in the renderer is set up in the top-level App component
sed -n '765,850p' apps/desktop/src/renderer/App.tsx

Repository: tomymaritano/readide

Length of output: 601


Keep a dedicated reference to the main window and only send auth tokens to it.

BrowserWindow.getAllWindows().find() will select the first non-destroyed, fully-loaded window. If a note window or settings window finishes loading before the main window, the token gets sent to the wrong renderer. The note and settings windows don't subscribe to auth:verify-token (only NotesApp does), so the authentication fails silently.

Store a module-level reference to the main window (following the pattern already used for settingsWindow), and only send the token when that specific window is ready. The queueing fallback should only trigger if the main window doesn't exist or isn't ready yet.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/index.ts` around lines 2484 - 2495, Replace the
transient lookup via BrowserWindow.getAllWindows().find(...) with a module-level
mainWindow reference (same pattern as settingsWindow): ensure mainWindow is
assigned when the app creates the primary window, then in the auth delivery path
only target that mainWindow (check !mainWindow.isDestroyed() &&
mainWindow.webContents.isLoading() === false) before calling
mainWindow.webContents.send('auth:verify-token', token), mainWindow.show(),
mainWindow.focus(); if mainWindow is undefined or not ready, set
pendingAuthToken = token (do not send to any other window). Ensure any existing
uses of the local mainWin are updated to use the module-level mainWindow
variable.

tomymaritano and others added 4 commits March 12, 2026 19:38
- Add aria-labels, aria-hidden to decorative SVGs and icons
- Use semantic HTML (h3/p in Card, button type="button")
- Add reduced-motion media queries for animations
- Fix stable keys in FaqAccordion, dropdown gap in Navbar
- Align marketing pages with design token colors
- Clamp unsafe values in marquee, clean up unused imports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- API: handle HTTPException in global onError so auth returns 401 not 500
- Auth middleware: re-throw HTTPException to preserve specific error messages
- Desktop: queue deep link auth token when window isn't ready yet
- Add roadmap document for auth, sync, and AI phases

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace LoginModal with EnableSyncModal (value prop → email → sent → success)
- Add resend timer (60s cooldown) and auto-transition on deep link verify
- Update SidebarFooter: "Sign In" → "Enable Sync" with Cloud icon
- Open sync modal directly from sidebar (no longer routes through Settings)
- Add useSyncOnboarding hook: shows prompt after 3+ notes created
- Add sync onboarding banner in sidebar with dismiss option
- Improve web auth/verify fallback: better visual hierarchy, wrong-device guidance
- Export EnableSyncModal from sync barrel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Desktop (LoginModal):
- Prevent double-send on resend with isResending guard
- Add dialog role, aria-modal, aria-labelledby for a11y
- Add aria-label to email input

Web accessibility:
- Scope [data-active] to .fd-sidebar to prevent global style bleed
- Add animate-fade-in-up to reduced-motion media query
- Add aria-hidden to decorative SVGs (AuthVerify, Footer, animated-beam)
- Add aria-label to comparison table (WhyLocal)
- Add dialog semantics to video modals (VideoGuides, hero-video-dialog)
- Add close button aria-labels to modals

Web content:
- Replace placeholder video URL with empty guard (Hero)
- Anonymize testimonial names to first+initial (Testimonials)
- Make coming-soon video cards non-focusable (VideoGuides)

Magic UI components:
- Add reduced-motion fallback to BorderBeam (static border)
- Add reduced-motion fallback to TextReveal (full opacity)
- Use stable id for React key in AnimatedGridPattern
- Guard zero-dimension calculations in DotPattern
- Add default type="button" to ShimmerButton
- Accept optional locale prop in NumberTicker

Minor:
- Remove unused index param in FaqAccordion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
apps/web/components/landing/Testimonials.tsx (2)

80-84: 🧹 Nitpick | 🔵 Trivial

Unnecessary cn() wrapper with single static class string.

The cn() utility is designed for conditionally merging classes. Here it wraps a single static string with no conditionals, adding a function call with no benefit.

🧹 Simplify to plain className
     <figure
-      className={cn(
-        'relative w-80 shrink-0 overflow-hidden rounded-xl border border-border bg-surface p-5'
-      )}
+      className="relative w-80 shrink-0 overflow-hidden rounded-xl border border-border bg-surface p-5"
     >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/landing/Testimonials.tsx` around lines 80 - 84, The
figure element in Testimonials.tsx is using the cn() utility unnecessarily:
replace className={cn('relative w-80 shrink-0 overflow-hidden rounded-xl border
border-border bg-surface p-5')} with a plain string className='relative w-80
shrink-0 overflow-hidden rounded-xl border border-border bg-surface p-5' and
then remove the now-unused cn import (or its usage) to avoid an unnecessary
function call.

57-65: 🧹 Nitpick | 🔵 Trivial

Add accessibility attributes to decorative stars.

The star icons are decorative visual reinforcement of the rating. Screen readers will announce each <svg> element. Mark them as decorative to avoid noise.

♿ Proposed accessibility fix
 function Stars({ count }: { count: number }) {
   return (
-    <div className="flex gap-0.5">
+    <div className="flex gap-0.5" role="img" aria-label={`${count} out of 5 stars`}>
       {Array.from({ length: count }).map((_, i) => (
-        <Star key={i} className="h-3.5 w-3.5 fill-amber-400 text-amber-400" />
+        <Star key={i} aria-hidden="true" className="h-3.5 w-3.5 fill-amber-400 text-amber-400" />
       ))}
     </div>
   );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/landing/Testimonials.tsx` around lines 57 - 65, The Stars
component currently renders multiple Star SVGs that will be announced by screen
readers; mark each decorative Star as hidden from assistive tech by adding
aria-hidden="true" (and optionally role="img" if your SVGs require it) on the
rendered Star element inside Stars, and add a single accessible summary for the
rating (e.g., a visually-hidden/sr-only text like "{count} stars") within the
Stars component so screen readers get one concise announcement instead of
multiple SVGs; update the Stars function and the Star usage accordingly.
apps/web/components/magicui/number-ticker.tsx (2)

17-25: ⚠️ Potential issue | 🔴 Critical

Destructure locale from props; it is currently undefined in component scope.

Line 24 omits locale, but lines 54 and 72 use it. This causes a TypeScript error (Cannot find name 'locale').

Additionally, locale should be added to the effect dependency array at line 60 to ensure the formatter updates if locale changes at runtime.

Suggested fix
 export function NumberTicker({
   value,
   startValue = 0,
   direction = 'up',
   delay = 0,
   className,
   decimalPlaces = 0,
+  locale,
   ...props
 }: NumberTickerProps) {
-    [springValue, decimalPlaces]
+    [springValue, decimalPlaces, locale]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/magicui/number-ticker.tsx` around lines 17 - 25, The
NumberTicker component is missing the locale prop in its parameter destructuring
and it's not included in the formatting effect dependencies; update the
NumberTicker function signature to destructure locale from props (alongside
value, startValue, direction, delay, className, decimalPlaces, etc.) and add
locale to the useEffect dependency array that creates/updates the
Intl.NumberFormat so the formatter re-runs when locale changes (refer to
NumberTicker and the effect that builds the formatter).

50-61: ⚠️ Potential issue | 🟠 Major

Add locale to both the component destructuring and the effect dependency array.

The locale prop is defined in the interface but never destructured from the function parameters (line 17-25). Additionally, line 54 uses locale inside the effect, but it's missing from the dependency array on line 60. Without destructuring, locale always defaults to undefined, rendering the prop useless. Without adding it to dependencies, the formatter callback captures stale locale values across re-renders.

Suggested fix
 export function NumberTicker({
   value,
   startValue = 0,
   direction = 'up',
   delay = 0,
   className,
   decimalPlaces = 0,
+  locale,
   ...props
 }: NumberTickerProps) {
-    [springValue, decimalPlaces]
+    [springValue, decimalPlaces, locale]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/magicui/number-ticker.tsx` around lines 50 - 61, The
component is not destructuring the locale prop and the effect uses locale
without declaring it in dependencies; update the component's parameter
destructuring to include locale (so the passed prop is used) and modify the
useEffect dependency array to [springValue, decimalPlaces, locale] so the
Intl.NumberFormat call inside the springValue.on('change', ...) callback always
sees the current locale; ensure you reference the same symbols (locale,
springValue, decimalPlaces, ref) and keep the rest of the NumberTicker component
logic unchanged.
apps/web/components/landing/VideoGuides.tsx (1)

51-64: ⚠️ Potential issue | 🟠 Major

Use a native <button> element for the clickable card state.

This div is still acting like a button, which is why Biome flags noStaticElementInteractions and requires manual Enter/Space key handling. Render <button type="button"> when hasVideo is true and a plain <div> when it is false. Native button elements eliminate the need for synthetic keyboard handling and provide proper semantics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/landing/VideoGuides.tsx` around lines 51 - 64, Replace
the interactive wrapper div in the VideoGuides component with a native button
when hasVideo is true: render <button type="button"> (keeping the same
className, onClick={() => setIsOpen(true)}, and children) and remove role,
tabIndex, and custom onKeyDown handling; when hasVideo is false continue to
render a plain <div> with the non-interactive classes and no click/keyboard
handlers. Ensure you still use the same state updater setIsOpen and preserve the
conditional cursor classes and visuals so behavior and styling remain unchanged.
♻️ Duplicate comments (8)
docs/plans/2026-03-12-roadmap-auth-sync-ai.md (1)

16-16: ⚠️ Potential issue | 🟡 Minor

Update stale backend status entries for invalid JWT handling.

Line 16 and Line 59 still describe the old 500 behavior, but this PR’s backend changes state invalid JWT now returns 401. Please align both entries.

Suggested doc fix
-| `GET /auth/me` + bad JWT | **500**     | **Bug**: should return 401, not 500 |
+| `GET /auth/me` + bad JWT | 401         | Fixed on 2026-03-12 (invalid JWT now returns HTTP 401) |

-- [ ] Fix `/auth/me` returning 500 on invalid JWT (should be 401)
+- [x] Fix `/auth/me` returning 500 on invalid JWT (now returns 401)

Also applies to: 59-59

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-03-12-roadmap-auth-sync-ai.md` at line 16, Update the docs
entry for the endpoint labeled "`GET /auth/me` + bad JWT" so it reflects the
backend change returning 401 instead of 500; find both occurrences that
currently state "**500** | **Bug**: should return 401, not 500" (one near the
top summary and the duplicate further down) and change the status to 401 and
wording to indicate it now returns 401 for invalid JWTs. Ensure both instances
(the main table entry and the duplicated backend status entry) are consistent.
apps/web/components/magicui/marquee.tsx (1)

61-69: ⚠️ Potential issue | 🟠 Major

Reduced-motion support still missing.

The accessibility attributes for duplicate panels are correct, but the component still lacks prefers-reduced-motion support as flagged previously. Users with vestibular sensitivities will experience continuous animation.

Add motion-reduce:animate-none to stop animation and motion-reduce:hidden to collapse duplicates:

♿ Proposed fix
           <div
             key={i}
             {...(i > 0 ? { 'aria-hidden': true, tabIndex: -1 } : {})}
-            className={cn('flex shrink-0 justify-around gap-[var(--gap)]', {
-              'animate-marquee flex-row': !vertical,
-              'animate-marquee-vertical flex-col': vertical,
-              'group-hover:[animation-play-state:paused]': pauseOnHover,
-              '[animation-direction:reverse]': reverse,
-            })}
+            className={cn(
+              'flex shrink-0 justify-around gap-[var(--gap)] motion-reduce:animate-none',
+              i > 0 && 'motion-reduce:hidden',
+              {
+                'animate-marquee flex-row': !vertical,
+                'animate-marquee-vertical flex-col': vertical,
+                'group-hover:[animation-play-state:paused]': pauseOnHover,
+                '[animation-direction:reverse]': reverse,
+              }
+            )}
           >

,

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/magicui/marquee.tsx` around lines 61 - 69, The Marquee
component still lacks prefers-reduced-motion support: update the duplicated
panel div (the element using key={i} and className with cn(...)) to include
Tailwind motion-reduce utilities so users with reduced motion are not animated
and duplicates are hidden; specifically add "motion-reduce:animate-none"
alongside the animate-marquee/animate-marquee-vertical classes and add
"motion-reduce:hidden" for duplicate panels (i > 0) so they collapse when motion
is reduced, keeping existing conditionals for vertical, pauseOnHover, and
reverse intact.
apps/desktop/src/renderer/styles/global.css (1)

1491-1502: ⚠️ Potential issue | 🟠 Major

Add -webkit-app-region: no-drag to the sync prompt.

The .sidebar container is a draggable title-bar region (line 113). Other interactive sections like .sidebar-nav and .sidebar-footer explicitly opt out with -webkit-app-region: no-drag. Without this property, the prompt's buttons will drag the window instead of receiving clicks on custom-titlebar builds.

🔧 Proposed fix
 .sidebar-sync-prompt {
+  -webkit-app-region: no-drag;
   display: flex;
   align-items: center;
   justify-content: space-between;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/styles/global.css` around lines 1491 - 1502, The
.sidebar-sync-prompt element is missing the -webkit-app-region: no-drag rule so
its interactive controls receive clicks instead of dragging the window; update
the .sidebar-sync-prompt CSS block to include -webkit-app-region: no-drag
(consistent with .sidebar-nav and .sidebar-footer) so the prompt opts out of the
draggable .sidebar region and its buttons work correctly in custom-titlebar
builds.
apps/desktop/src/renderer/components/sync/LoginModal.tsx (1)

111-111: ⚠️ Potential issue | 🟡 Minor

Add keyboard handler to overlay for accessibility compliance.

The overlay <div> has onClick={handleClose} but no keyboard handler. While the close button provides keyboard access, adding onKeyDown to dismiss on Escape improves accessibility and satisfies the linter.

♻️ Proposed fix
+  const handleKeyDown = useCallback(
+    (e: React.KeyboardEvent) => {
+      if (e.key === 'Escape') {
+        handleClose();
+      }
+    },
+    [handleClose]
+  );
+
   if (!isOpen) return null;
 
   return (
-    <div className={styles.overlay} onClick={handleClose}>
+    <div
+      className={styles.overlay}
+      onClick={handleClose}
+      onKeyDown={handleKeyDown}
+      role="presentation"
+    >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/sync/LoginModal.tsx` at line 111, The
overlay div in LoginModal.tsx has onClick={handleClose} but no keyboard handler;
add an onKeyDown handler on the same element that checks for e.key === 'Escape'
(or 'Esc') and calls handleClose to dismiss the modal, and make the overlay
focusable (e.g., add tabIndex={0}) so it can receive keyboard events; update the
element with these attributes and ensure the handler references the existing
handleClose function.
apps/desktop/src/main/index.ts (1)

2484-2494: ⚠️ Potential issue | 🟠 Major

Deep-link routing is still incomplete and non-deterministic.

At Line 2485, BrowserWindow.getAllWindows().find(...) picks an arbitrary loaded window. Also, deep-link processing is still tied to open-url flow, so Windows/Linux cold-start and second-instance protocol delivery remain uncovered in this path.

Suggested refactor (shared deep-link handler + deterministic target)
+// Keep a stable reference to the primary app window
+let mainWindowRef: BrowserWindow | null = null;
+
+function handleDeepLinkUrl(url: string): void {
+  const log = getLogger();
+  const urlObj = new URL(url);
+  if (urlObj.hostname !== 'auth' || urlObj.pathname !== '/verify') return;
+  const token = urlObj.searchParams.get('token');
+  if (!token) return;
+
+  if (mainWindowRef && !mainWindowRef.isDestroyed() && !mainWindowRef.webContents.isLoading()) {
+    mainWindowRef.webContents.send('auth:verify-token', token);
+    mainWindowRef.show();
+    mainWindowRef.focus();
+  } else {
+    pendingAuthToken = token;
+  }
+}
+
 function createWindow(): void {
-  const mainWindow = new BrowserWindow({
+  const mainWindow = new BrowserWindow({
     ...
   });
+  mainWindowRef = mainWindow;
   ...
 }
 
 app.on('open-url', (event, url) => {
   event.preventDefault();
-  // parse + send inline
+  handleDeepLinkUrl(url);
 });
+
+app.on('second-instance', (_event, argv) => {
+  const deeplink = argv.find(arg => arg.startsWith('readied://'));
+  if (deeplink) handleDeepLinkUrl(deeplink);
+});
+
+// Cold-start deep link on Windows/Linux
+const startupDeepLink = process.argv.find(arg => arg.startsWith('readied://'));
+if (startupDeepLink) handleDeepLinkUrl(startupDeepLink);
#!/bin/bash
# Verify deep-link entry points and window targeting strategy in main process.
rg -n "open-url|second-instance|process\\.argv|BrowserWindow\\.getAllWindows\\(\\)\\.find|pendingAuthToken" apps/desktop/src/main/index.ts -C 3

As per coding guidelines, "Main process code. Check IPC handler patterns and sync service consistency."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/index.ts` around lines 2484 - 2494, Current deep-link
handling is non-deterministic because BrowserWindow.getAllWindows().find(...)
can return any loaded window and not all entry points (open-url,
second-instance, process.argv cold start) call the same logic; create a single
shared deep-link handler function (e.g., handleDeepLink(uriOrToken)) that
extracts the token and sends it via webContents.send('auth:verify-token', token)
or sets pendingAuthToken when no window exists, replace the ad-hoc
BrowserWindow.getAllWindows().find(...) usage with a deterministic getter for
the main window (e.g., getMainWindow() or store/retrieve mainWindowId and use
BrowserWindow.fromId(mainWindowId)), and wire this handler into all entry points
(open-url, app.on('second-instance'), app.whenReady() / process.argv parsing) so
all paths route tokens through the same deterministic flow (use pendingAuthToken
only as fallback until getMainWindow() returns a ready window).
apps/web/components/magicui/hero-video-dialog.tsx (1)

111-124: ⚠️ Potential issue | 🟠 Major

This dialog still lacks working keyboard focus management.

onKeyDown is bound to a non-focusable motion.div, and the modal never takes, traps, or restores focus. After opening with the mouse, focus can stay on the background trigger, so Escape never reaches this handler. Please reuse the Radix dialog pattern already present in apps/web/components/ui/sheet.tsx, or add explicit focus management here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/magicui/hero-video-dialog.tsx` around lines 111 - 124,
The modal's keyboard focus management is broken because the onKeyDown is
attached to a non-focusable motion.div so Escape and focus trapping never work;
update the hero video dialog to either reuse the Radix dialog pattern used in
sheet.tsx (e.g., use Radix Dialog primitives to open/close, trap focus, and
restore focus) or make the modal container focusable and manage focus
explicitly: move the Escape handler to a focusable element (or window keydown
handler), call focus() on the dialog root when opening (so the handler receives
events), implement focus trap while isVideoOpen is true, and restore focus to
the original trigger when closing (ensure you update references to motion.div
and setIsVideoOpen accordingly).
apps/web/components/landing/Hero.tsx (1)

29-47: ⚠️ Potential issue | 🟠 Major

The hero demo remains a dead button while the URL is blank.

With DEMO_VIDEO_URL hardcoded to '', this preview stays tabbable and announced as “Watch demo video” even though it can never open. Render a non-interactive wrapper until a real demo exists, or disable the button and expose a “coming soon” state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/landing/Hero.tsx` around lines 29 - 47, DEMO_VIDEO_URL is
empty so the interactive button in EditorMockWithPlay is tabbable and announced
even though it can't open; update EditorMockWithPlay (and VideoPreview if
needed) to check hasVideo and when false render a non-interactive wrapper (e.g.,
a <div> with same visual layout) or a disabled button (set disabled and
aria-disabled="true" and remove the onClick) and surface a “coming soon”
visual/aria label; ensure the aria-label and keyboard focusability reflect the
non-interactive state when DEMO_VIDEO_URL === ''.
apps/web/components/ui/button.tsx (1)

32-40: ⚠️ Potential issue | 🟡 Minor

type attribute leaks onto non-button elements when asChild=true.

The type={type} prop is forwarded unconditionally to Comp. When asChild=true, the Slot passes this to its child element. Per the relevant code snippets, this component is used with asChild wrapping <a> and <Link> elements (e.g., in download/page.tsx and pricing/page.tsx), making type="button" invalid HTML on those elements.

Only apply type when rendering an actual <button>.

Proposed fix
 const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
-  ({ className, variant, size, asChild = false, type = 'button', ...props }, ref) => {
+  ({ className, variant, size, asChild = false, type, ...props }, ref) => {
     const Comp = asChild ? Slot : 'button';
     return (
       <Comp
-        type={type}
+        {...(!asChild && { type: type ?? 'button' })}
         className={cn(buttonVariants({ variant, size, className }))}
         ref={ref}
         {...props}
       />
     );
   }
 );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/ui/button.tsx` around lines 32 - 40, The component
currently always forwards type to Comp (const Comp = asChild ? Slot : 'button'),
which causes type="button" to be passed into non-button elements when
asChild=true; change the render so that the type prop is only included when
rendering a real button (i.e., when asChild is false or Comp === 'button')—when
asChild is true, omit type from the spread so Slot does not pass it to child
anchors/Links; update the JSX around Comp, the asChild prop handling, and the
props spread logic in this component (the function receiving { className,
variant, size, asChild = false, type = 'button', ...props }, ref) to
conditionally apply type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/main/index.ts`:
- Around line 302-311: The current delivery on
mainWindow.webContents.on('did-finish-load') can race with the renderer
listener; change it to perform a handshake: when did-finish-load fires and
pendingAuthToken exists, register a one-time ipcMain listener for an
'auth:listener-ready' message (use webContents.once/ ipcMain.once tied to
mainWindow.webContents.id) and only then send 'auth:verify-token' with
pendingAuthToken via mainWindow.webContents.send; clear pendingAuthToken only
after the send completes (or immediately after sending but not before the
listener handshake), add a short timeout fallback to log and avoid dropping the
token, and keep references to symbols pendingAuthToken,
mainWindow.webContents.on('did-finish-load'), and the IPC channels
'auth:listener-ready' and 'auth:verify-token' to implement the fix.

In `@apps/desktop/src/renderer/components/sidebar/Sidebar.tsx`:
- Around line 22-23: Reorder the imports so external modules and hooks come
before local component imports: move the imports for EnableSyncModal and
useSyncOnboarding so they appear above the local SidebarHeader (and other
sibling ./ imports) in Sidebar.tsx; ensure the import groupings follow the
linter rule (third-party/hooks first, then local components) to satisfy ESLint
import order.

In `@apps/desktop/src/renderer/components/sync/index.ts`:
- Line 9: The export re-exports EnableSyncModal from './LoginModal' but the
filename no longer matches the component name; rename the source file
LoginModal.tsx to EnableSyncModal.tsx and update any imports/exports to use the
new filename so the export line export { EnableSyncModal } from './LoginModal';
becomes consistent with the module name; ensure the component
declaration/exports inside the renamed file still export EnableSyncModal (named
export or default converted appropriately) and update any other files importing
'./LoginModal' to the new './EnableSyncModal'.

In `@apps/desktop/src/renderer/components/sync/LoginModal.module.css`:
- Around line 213-217: Remove the two `!important` usages on the .hint rule by
replacing the generic selector with a more specific one (e.g., use `.sent
.hint`) so specificity resolves without forcing styles; update the CSS rule for
the hint (the existing `.hint` block) to move margin-top and color/font-size
into the more-specific selector (`.sent .hint`) and leave or reduce the base
`.hint` styles as needed to avoid using `!important`.

In `@apps/desktop/src/renderer/components/sync/LoginModal.tsx`:
- Line 171: Remove the inline autoFocus attribute from the input in LoginModal
and instead manage focus imperatively: add a ref (e.g., loginInputRef) to the
input and a useEffect inside the LoginModal component that, when the modal
becomes visible (the same condition that shows the modal after clicking "Enable
Sync"), calls loginInputRef.current.focus() after a short delay (setTimeout or
requestAnimationFrame) to avoid interrupting screen readers; ensure to clean up
timers and only focus when the modal is mounted/visible.

In `@apps/desktop/src/renderer/hooks/useSyncOnboarding.ts`:
- Line 21: The variable totalNotes is misleading and the type assertion is
unnecessary; update the logic around the counts variable so it either (a) reads
the intended total via counts?.total if you really need the total, or (b) rename
totalNotes to activeNotes and use counts?.active without the (counts as {
active?: number }) cast. Locate the assignment to totalNotes in
useSyncOnboarding and change the identifier and expression accordingly (remove
the explicit type assertion and use counts?.active or counts?.total as
appropriate), and update any downstream references to the renamed variable.

In `@apps/web/app/`(marketing)/auth/verify/AuthVerifyContent.tsx:
- Line 14: The token is inserted into a custom-protocol URL without encoding;
update both occurrences (the window.location.href assignments in
AuthVerifyContent.tsx) to embed encodeURIComponent(token) instead of raw token
so special characters are URL-encoded and cannot break parsing or introduce
injection issues.

In `@apps/web/components/FaqAccordion.tsx`:
- Around line 39-44: The current slug generation in the items.map →
AccordionItem value prop uses item.question.slice(0,
40).toLowerCase().replace(...) which can produce duplicate values for questions
with the same first 40 chars; update the value to guarantee uniqueness by
appending a stable suffix (e.g., the loop index or a short hash) so each
AccordionItem gets a unique value (reference the items.map callback,
AccordionItem, and the existing value generation expression) — for example use
`faq-${i}` or `faq-${normalizedQuestion}-${i}` to avoid collisions while
preserving readable slugs.

In `@apps/web/components/magicui/border-beam.tsx`:
- Around line 3-5: Remove the extra blank line between the two import statements
so the imports are contiguous (i.e., put "import { motion, type MotionStyle,
type Transition, useReducedMotion } from 'framer-motion';" immediately above
"import { cn } from '@/lib/utils';") to satisfy the ESLint import-order/style
rule; ensure no other spacing changes are introduced and run lint to confirm the
warning is resolved.

In `@docs/plans/2026-03-12-roadmap-auth-sync-ai.md`:
- Around line 114-125: The two fenced code blocks in the doc are missing
language tags (markdownlint MD040); update both blocks so the directory tree
block (the one containing "renderer/components/CommandK/ ... useCommandK.ts" and
"packages/ai-assistant/src/ ... registry.ts") and the action flow block ("Select
text → Cmd+K → \"Summarize\" → AI streams result → Preview → \"Insert\" →
Replaces selection") start with ```text (i.e., replace ``` with ```text) so the
linter is satisfied.
- Around line 48-51: The Markdown table under the "Files to change" heading
lacks a blank line (violates MD058); edit the document and insert a single empty
line between the "Files to change" heading and the table so the table is
separated from the preceding paragraph/heading—this affects the table that
references renderer/components/sync/LoginModal.tsx (redesign as "Enable Sync"
modal).

In `@packages/api/src/index.ts`:
- Around line 75-78: When handling HTTPException, normalize the returned body
from err.getResponse() into a JSON object and preserve the status code from
err.getStatus(); specifically, replace the direct return of err.getResponse()
with logic that captures const resp = err.getResponse(); if typeof resp ===
'string' set body = { message: resp }; else if resp is an object ensure it has a
message field (or pass through) and then return the response with the correct
status using err.getStatus() (e.g., res.status(err.getStatus()).json(body) or
the equivalent in this handler), so HTTPException responses match the JSON shape
used by the 404 and generic error handlers.

---

Outside diff comments:
In `@apps/web/components/landing/Testimonials.tsx`:
- Around line 80-84: The figure element in Testimonials.tsx is using the cn()
utility unnecessarily: replace className={cn('relative w-80 shrink-0
overflow-hidden rounded-xl border border-border bg-surface p-5')} with a plain
string className='relative w-80 shrink-0 overflow-hidden rounded-xl border
border-border bg-surface p-5' and then remove the now-unused cn import (or its
usage) to avoid an unnecessary function call.
- Around line 57-65: The Stars component currently renders multiple Star SVGs
that will be announced by screen readers; mark each decorative Star as hidden
from assistive tech by adding aria-hidden="true" (and optionally role="img" if
your SVGs require it) on the rendered Star element inside Stars, and add a
single accessible summary for the rating (e.g., a visually-hidden/sr-only text
like "{count} stars") within the Stars component so screen readers get one
concise announcement instead of multiple SVGs; update the Stars function and the
Star usage accordingly.

In `@apps/web/components/landing/VideoGuides.tsx`:
- Around line 51-64: Replace the interactive wrapper div in the VideoGuides
component with a native button when hasVideo is true: render <button
type="button"> (keeping the same className, onClick={() => setIsOpen(true)}, and
children) and remove role, tabIndex, and custom onKeyDown handling; when
hasVideo is false continue to render a plain <div> with the non-interactive
classes and no click/keyboard handlers. Ensure you still use the same state
updater setIsOpen and preserve the conditional cursor classes and visuals so
behavior and styling remain unchanged.

In `@apps/web/components/magicui/number-ticker.tsx`:
- Around line 17-25: The NumberTicker component is missing the locale prop in
its parameter destructuring and it's not included in the formatting effect
dependencies; update the NumberTicker function signature to destructure locale
from props (alongside value, startValue, direction, delay, className,
decimalPlaces, etc.) and add locale to the useEffect dependency array that
creates/updates the Intl.NumberFormat so the formatter re-runs when locale
changes (refer to NumberTicker and the effect that builds the formatter).
- Around line 50-61: The component is not destructuring the locale prop and the
effect uses locale without declaring it in dependencies; update the component's
parameter destructuring to include locale (so the passed prop is used) and
modify the useEffect dependency array to [springValue, decimalPlaces, locale] so
the Intl.NumberFormat call inside the springValue.on('change', ...) callback
always sees the current locale; ensure you reference the same symbols (locale,
springValue, decimalPlaces, ref) and keep the rest of the NumberTicker component
logic unchanged.

---

Duplicate comments:
In `@apps/desktop/src/main/index.ts`:
- Around line 2484-2494: Current deep-link handling is non-deterministic because
BrowserWindow.getAllWindows().find(...) can return any loaded window and not all
entry points (open-url, second-instance, process.argv cold start) call the same
logic; create a single shared deep-link handler function (e.g.,
handleDeepLink(uriOrToken)) that extracts the token and sends it via
webContents.send('auth:verify-token', token) or sets pendingAuthToken when no
window exists, replace the ad-hoc BrowserWindow.getAllWindows().find(...) usage
with a deterministic getter for the main window (e.g., getMainWindow() or
store/retrieve mainWindowId and use BrowserWindow.fromId(mainWindowId)), and
wire this handler into all entry points (open-url, app.on('second-instance'),
app.whenReady() / process.argv parsing) so all paths route tokens through the
same deterministic flow (use pendingAuthToken only as fallback until
getMainWindow() returns a ready window).

In `@apps/desktop/src/renderer/components/sync/LoginModal.tsx`:
- Line 111: The overlay div in LoginModal.tsx has onClick={handleClose} but no
keyboard handler; add an onKeyDown handler on the same element that checks for
e.key === 'Escape' (or 'Esc') and calls handleClose to dismiss the modal, and
make the overlay focusable (e.g., add tabIndex={0}) so it can receive keyboard
events; update the element with these attributes and ensure the handler
references the existing handleClose function.

In `@apps/desktop/src/renderer/styles/global.css`:
- Around line 1491-1502: The .sidebar-sync-prompt element is missing the
-webkit-app-region: no-drag rule so its interactive controls receive clicks
instead of dragging the window; update the .sidebar-sync-prompt CSS block to
include -webkit-app-region: no-drag (consistent with .sidebar-nav and
.sidebar-footer) so the prompt opts out of the draggable .sidebar region and its
buttons work correctly in custom-titlebar builds.

In `@apps/web/components/landing/Hero.tsx`:
- Around line 29-47: DEMO_VIDEO_URL is empty so the interactive button in
EditorMockWithPlay is tabbable and announced even though it can't open; update
EditorMockWithPlay (and VideoPreview if needed) to check hasVideo and when false
render a non-interactive wrapper (e.g., a <div> with same visual layout) or a
disabled button (set disabled and aria-disabled="true" and remove the onClick)
and surface a “coming soon” visual/aria label; ensure the aria-label and
keyboard focusability reflect the non-interactive state when DEMO_VIDEO_URL ===
''.

In `@apps/web/components/magicui/hero-video-dialog.tsx`:
- Around line 111-124: The modal's keyboard focus management is broken because
the onKeyDown is attached to a non-focusable motion.div so Escape and focus
trapping never work; update the hero video dialog to either reuse the Radix
dialog pattern used in sheet.tsx (e.g., use Radix Dialog primitives to
open/close, trap focus, and restore focus) or make the modal container focusable
and manage focus explicitly: move the Escape handler to a focusable element (or
window keydown handler), call focus() on the dialog root when opening (so the
handler receives events), implement focus trap while isVideoOpen is true, and
restore focus to the original trigger when closing (ensure you update references
to motion.div and setIsVideoOpen accordingly).

In `@apps/web/components/magicui/marquee.tsx`:
- Around line 61-69: The Marquee component still lacks prefers-reduced-motion
support: update the duplicated panel div (the element using key={i} and
className with cn(...)) to include Tailwind motion-reduce utilities so users
with reduced motion are not animated and duplicates are hidden; specifically add
"motion-reduce:animate-none" alongside the
animate-marquee/animate-marquee-vertical classes and add "motion-reduce:hidden"
for duplicate panels (i > 0) so they collapse when motion is reduced, keeping
existing conditionals for vertical, pauseOnHover, and reverse intact.

In `@apps/web/components/ui/button.tsx`:
- Around line 32-40: The component currently always forwards type to Comp (const
Comp = asChild ? Slot : 'button'), which causes type="button" to be passed into
non-button elements when asChild=true; change the render so that the type prop
is only included when rendering a real button (i.e., when asChild is false or
Comp === 'button')—when asChild is true, omit type from the spread so Slot does
not pass it to child anchors/Links; update the JSX around Comp, the asChild prop
handling, and the props spread logic in this component (the function receiving {
className, variant, size, asChild = false, type = 'button', ...props }, ref) to
conditionally apply type.

In `@docs/plans/2026-03-12-roadmap-auth-sync-ai.md`:
- Line 16: Update the docs entry for the endpoint labeled "`GET /auth/me` + bad
JWT" so it reflects the backend change returning 401 instead of 500; find both
occurrences that currently state "**500** | **Bug**: should return 401, not 500"
(one near the top summary and the duplicate further down) and change the status
to 401 and wording to indicate it now returns 401 for invalid JWTs. Ensure both
instances (the main table entry and the duplicated backend status entry) are
consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 819d46bf-8353-4dbc-a242-facca3016505

📥 Commits

Reviewing files that changed from the base of the PR and between 06a08a9 and 657734e.

📒 Files selected for processing (35)
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/renderer/components/sidebar/Sidebar.tsx
  • apps/desktop/src/renderer/components/sidebar/SidebarFooter.tsx
  • apps/desktop/src/renderer/components/sync/LoginModal.module.css
  • apps/desktop/src/renderer/components/sync/LoginModal.tsx
  • apps/desktop/src/renderer/components/sync/index.ts
  • apps/desktop/src/renderer/hooks/useSyncOnboarding.ts
  • apps/desktop/src/renderer/styles/global.css
  • apps/web/app/(marketing)/auth/verify/AuthVerifyContent.tsx
  • apps/web/app/(marketing)/download/page.tsx
  • apps/web/app/(marketing)/philosophy/page.tsx
  • apps/web/app/(marketing)/pricing/page.tsx
  • apps/web/app/globals.css
  • apps/web/components/FaqAccordion.tsx
  • apps/web/components/Footer.tsx
  • apps/web/components/Navbar.tsx
  • apps/web/components/landing/Hero.tsx
  • apps/web/components/landing/Testimonials.tsx
  • apps/web/components/landing/VideoGuides.tsx
  • apps/web/components/landing/WhyLocal.tsx
  • apps/web/components/magicui/animated-beam.tsx
  • apps/web/components/magicui/animated-grid-pattern.tsx
  • apps/web/components/magicui/animated-shiny-text.tsx
  • apps/web/components/magicui/border-beam.tsx
  • apps/web/components/magicui/dot-pattern.tsx
  • apps/web/components/magicui/hero-video-dialog.tsx
  • apps/web/components/magicui/marquee.tsx
  • apps/web/components/magicui/number-ticker.tsx
  • apps/web/components/magicui/shimmer-button.tsx
  • apps/web/components/magicui/text-reveal.tsx
  • apps/web/components/ui/button.tsx
  • apps/web/components/ui/card.tsx
  • docs/plans/2026-03-12-roadmap-auth-sync-ai.md
  • packages/api/src/index.ts
  • packages/api/src/middleware/auth.ts

Comment on lines +302 to +311
// Deliver any pending deep link auth token once the renderer is ready
mainWindow.webContents.on('did-finish-load', () => {
if (pendingAuthToken) {
getLogger().info('Delivering queued auth token to renderer');
mainWindow.webContents.send('auth:verify-token', pendingAuthToken);
mainWindow.show();
mainWindow.focus();
pendingAuthToken = null;
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify missing readiness handshake and current delivery timing.
rg -n "did-finish-load|auth:verify-token|auth:listener-ready" apps/desktop/src/main/index.ts apps/desktop/src/renderer/App.tsx apps/desktop/src/preload/index.ts -C 2

Repository: tomymaritano/readide

Length of output: 1389


🏁 Script executed:

cat -n apps/desktop/src/renderer/App.tsx | sed -n '140,165p'

Repository: tomymaritano/readide

Length of output: 1130


🏁 Script executed:

cat -n apps/desktop/src/main/index.ts | sed -n '2480,2500p'

Repository: tomymaritano/readide

Length of output: 965


🏁 Script executed:

cat -n apps/desktop/src/preload/index.ts | head -50

Repository: tomymaritano/readide

Length of output: 1546


🏁 Script executed:

rg -n "ipc|message|queue|buffer" apps/desktop/src/preload/index.ts apps/desktop/src/renderer/App.tsx -C 2 | head -60

Repository: tomymaritano/readide

Length of output: 4068


🏁 Script executed:

# Check full preload to see if there's any message queueing
wc -l apps/desktop/src/preload/index.ts

Repository: tomymaritano/readide

Length of output: 102


🏁 Script executed:

cat -n apps/desktop/src/preload/index.ts | sed -n '618,650p'

Repository: tomymaritano/readide

Length of output: 1651


🏁 Script executed:

# Check the actual implementation of the ipc.on wrapper
rg -n "ipc:" apps/desktop/src/preload/index.ts -A 5 | grep -A 5 "ipc: {"

Repository: tomymaritano/readide

Length of output: 586


🏁 Script executed:

rg -n "auth|token|verify" apps/desktop/src/renderer/App.tsx -C 3 | grep -A 5 -B 5 "handleAuthVerification\|catch\|error"

Repository: tomymaritano/readide

Length of output: 776


🏁 Script executed:

# Check if there's any user-reported issue or test coverage for this flow
fd -e test -e spec apps/desktop/src | xargs grep -l "auth.*token\|did-finish-load" 2>/dev/null

Repository: tomymaritano/readide

Length of output: 435


🏁 Script executed:

fd -e test -e spec . apps/desktop/src | head -20

Repository: tomymaritano/readide

Length of output: 46


🏁 Script executed:

# Check if there's a mechanism to resend or recover lost tokens
rg -n "pendingAuthToken|verifyToken" apps/desktop/src/main/index.ts -C 2

Repository: tomymaritano/readide

Length of output: 915


🏁 Script executed:

# Check if there's any message queuing in ipcRenderer setup or app initialization
cat -n apps/desktop/src/preload/index.ts | sed -n '1,50p'

Repository: tomymaritano/readide

Length of output: 1546


🏁 Script executed:

# Look for any auth token persistence or recovery mechanism
rg -n "localStorage|sessionStorage|IndexedDB|recovery|persist" apps/desktop/src/renderer/App.tsx apps/desktop/src/main/index.ts | head -20

Repository: tomymaritano/readide

Length of output: 158


Add handshake mechanism to ensure auth token delivery completes after renderer listener is ready.

The token sent on did-finish-load (line 306) can be lost if the renderer's useEffect listener registration hasn't completed. The preload's ipc.on() wrapper has no message buffering, and once the token is sent, pendingAuthToken is immediately nullified with no recovery path. This race condition can silently drop deep-link auth tokens.

The suggested handshake-based delivery using an auth:listener-ready IPC message from the renderer ensures the listener is actively registered before the token is sent, eliminating this window of vulnerability.

Suggested fix (handshake-based delivery)
+// Track renderer readiness for auth deep-link events
+let authListenerReady = false;
+
+function flushPendingAuthToken(targetWindow: BrowserWindow): void {
+  if (!pendingAuthToken || !authListenerReady) return;
+  targetWindow.webContents.send('auth:verify-token', pendingAuthToken);
+  targetWindow.show();
+  targetWindow.focus();
+  pendingAuthToken = null;
+}
+
 // Deliver any pending deep link auth token once the renderer is ready
 mainWindow.webContents.on('did-finish-load', () => {
-  if (pendingAuthToken) {
-    getLogger().info('Delivering queued auth token to renderer');
-    mainWindow.webContents.send('auth:verify-token', pendingAuthToken);
-    mainWindow.show();
-    mainWindow.focus();
-    pendingAuthToken = null;
-  }
+  flushPendingAuthToken(mainWindow);
 });
+
+ipcMain.on('auth:listener-ready', event => {
+  if (event.sender === mainWindow.webContents) {
+    authListenerReady = true;
+    flushPendingAuthToken(mainWindow);
+  }
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/index.ts` around lines 302 - 311, The current delivery
on mainWindow.webContents.on('did-finish-load') can race with the renderer
listener; change it to perform a handshake: when did-finish-load fires and
pendingAuthToken exists, register a one-time ipcMain listener for an
'auth:listener-ready' message (use webContents.once/ ipcMain.once tied to
mainWindow.webContents.id) and only then send 'auth:verify-token' with
pendingAuthToken via mainWindow.webContents.send; clear pendingAuthToken only
after the send completes (or immediately after sending but not before the
listener handshake), add a short timeout fallback to log and avoid dropping the
token, and keep references to symbols pendingAuthToken,
mainWindow.webContents.on('did-finish-load'), and the IPC channels
'auth:listener-ready' and 'auth:verify-token' to implement the fix.

Comment on lines +22 to +23
import { EnableSyncModal } from '../sync';
import { useSyncOnboarding } from '../../hooks/useSyncOnboarding';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Import order warning from linter.

ESLint flags that ../sync and ../../hooks/useSyncOnboarding imports should occur before local component imports like ./SidebarHeader. Consider reordering to group external/hook imports before local component imports.

♻️ Suggested reorder
 import { useState, useCallback, useEffect } from 'react';
 import { LayoutZone } from '@readied/plugin-api';
+import { EnableSyncModal } from '../sync';
+import { useSyncOnboarding } from '../../hooks/useSyncOnboarding';
 import {
   useIsNotebookContext,
   ...
 } from '../../hooks/useNavigation';
 import { useNotebookMutations } from '../../hooks/useNotebooks';
 import { SidebarHeader } from './SidebarHeader';
 ...
-import { EnableSyncModal } from '../sync';
-import { useSyncOnboarding } from '../../hooks/useSyncOnboarding';
 import { NotebookCreateModal } from './NotebookCreateModal';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { EnableSyncModal } from '../sync';
import { useSyncOnboarding } from '../../hooks/useSyncOnboarding';
import { useState, useCallback, useEffect } from 'react';
import { LayoutZone } from '@readied/plugin-api';
import { EnableSyncModal } from '../sync';
import { useSyncOnboarding } from '../../hooks/useSyncOnboarding';
import {
useIsNotebookContext,
...
} from '../../hooks/useNavigation';
import { useNotebookMutations } from '../../hooks/useNotebooks';
import { SidebarHeader } from './SidebarHeader';
...
import { NotebookCreateModal } from './NotebookCreateModal';
🧰 Tools
🪛 GitHub Check: lint

[warning] 23-23:
../../hooks/useSyncOnboarding import should occur before import of ./SidebarHeader


[warning] 22-22:
../sync import should occur before import of ./SidebarHeader

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/sidebar/Sidebar.tsx` around lines 22 -
23, Reorder the imports so external modules and hooks come before local
component imports: move the imports for EnableSyncModal and useSyncOnboarding so
they appear above the local SidebarHeader (and other sibling ./ imports) in
Sidebar.tsx; ensure the import groupings follow the linter rule
(third-party/hooks first, then local components) to satisfy ESLint import order.

export { SyncStatusIndicator } from './SyncStatusIndicator';
export { ConflictResolver } from './ConflictResolver';
export { LoginModal } from './LoginModal';
export { EnableSyncModal } from './LoginModal';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Export rename is correct; consider renaming the source file.

The export correctly re-exports EnableSyncModal from ./LoginModal, matching the renamed component. For long-term maintainability, consider renaming LoginModal.tsx to EnableSyncModal.tsx to avoid confusion between the filename and the exported component name.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/sync/index.ts` at line 9, The export
re-exports EnableSyncModal from './LoginModal' but the filename no longer
matches the component name; rename the source file LoginModal.tsx to
EnableSyncModal.tsx and update any imports/exports to use the new filename so
the export line export { EnableSyncModal } from './LoginModal'; becomes
consistent with the module name; ensure the component declaration/exports inside
the renamed file still export EnableSyncModal (named export or default converted
appropriately) and update any other files importing './LoginModal' to the new
'./EnableSyncModal'.

Comment on lines 213 to 217
.hint {
margin-top: 16px !important;
margin-top: 12px !important;
font-size: 13px;
color: var(--color-text-tertiary, #999) !important;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Avoid !important - use more specific selectors instead.

The !important declarations on .hint suggest specificity conflicts. Since .hint is used within .sent, a more specific selector like .sent .hint would override without !important.

♻️ Proposed fix
-.hint {
-  margin-top: 12px !important;
-  font-size: 13px;
-  color: var(--color-text-tertiary, `#999`) !important;
-}
+.sent .hint {
+  margin-top: 12px;
+  font-size: 13px;
+  color: var(--color-text-tertiary, `#999`);
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/sync/LoginModal.module.css` around lines
213 - 217, Remove the two `!important` usages on the .hint rule by replacing the
generic selector with a more specific one (e.g., use `.sent .hint`) so
specificity resolves without forcing styles; update the CSS rule for the hint
(the existing `.hint` block) to move margin-top and color/font-size into the
more-specific selector (`.sent .hint`) and leave or reduce the base `.hint`
styles as needed to avoid using `!important`.

aria-label="Email address"
placeholder="you@example.com"
required
autoFocus

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider removing autoFocus for accessibility.

The autoFocus attribute can disrupt screen reader announcements and is flagged by Biome. Since this input appears after user interaction (clicking "Enable Sync"), consider using a useEffect with ref.focus() after a brief delay, or accept the current behavior if the UX benefit outweighs the accessibility concern.

🧰 Tools
🪛 Biome (2.4.6)

[error] 171-171: Avoid the autoFocus attribute.

(lint/a11y/noAutofocus)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/sync/LoginModal.tsx` at line 171, Remove
the inline autoFocus attribute from the input in LoginModal and instead manage
focus imperatively: add a ref (e.g., loginInputRef) to the input and a useEffect
inside the LoginModal component that, when the modal becomes visible (the same
condition that shows the modal after clicking "Enable Sync"), calls
loginInputRef.current.focus() after a short delay (setTimeout or
requestAnimationFrame) to avoid interrupting screen readers; ensure to clean up
timers and only focus when the modal is mounted/visible.

Comment on lines +39 to +44
{items.map(item => (
<AccordionItem
key={item.question}
value={`faq-${item.question.slice(0, 40).toLowerCase().replace(/\s+/g, '-')}`}
className="rounded-xl bg-surface/50"
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential value collision when questions share a common prefix.

The slug generation truncates to 40 characters before normalizing: two questions starting with the same 40 characters will produce identical value props, breaking the accordion's expand/collapse behavior (Radix UI uses value to identify the open item).

Consider appending a hash or index suffix to guarantee uniqueness:

🛠️ Proposed fix
-      {items.map(item => (
+      {items.map((item, idx) => (
         <AccordionItem
           key={item.question}
-          value={`faq-${item.question.slice(0, 40).toLowerCase().replace(/\s+/g, '-')}`}
+          value={`faq-${idx}-${item.question.slice(0, 40).toLowerCase().replace(/\s+/g, '-')}`}
           className="rounded-xl bg-surface/50"
         >

Note: This also aligns more closely with the numeric-index pattern used elsewhere (e.g., faq-${i} in apps/web/app/(marketing)/pricing/page.tsx).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/FaqAccordion.tsx` around lines 39 - 44, The current slug
generation in the items.map → AccordionItem value prop uses
item.question.slice(0, 40).toLowerCase().replace(...) which can produce
duplicate values for questions with the same first 40 chars; update the value to
guarantee uniqueness by appending a stable suffix (e.g., the loop index or a
short hash) so each AccordionItem gets a unique value (reference the items.map
callback, AccordionItem, and the existing value generation expression) — for
example use `faq-${i}` or `faq-${normalizedQuestion}-${i}` to avoid collisions
while preserving readable slugs.

Comment on lines +3 to 5
import { motion, type MotionStyle, type Transition, useReducedMotion } from 'framer-motion';

import { cn } from '@/lib/utils';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix import spacing to clear lint warning.

Line 4 introduces an empty line between imports, which violates the active ESLint import-order/style rule.

Suggested fix
 import { motion, type MotionStyle, type Transition, useReducedMotion } from 'framer-motion';
-
 import { cn } from '@/lib/utils';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { motion, type MotionStyle, type Transition, useReducedMotion } from 'framer-motion';
import { cn } from '@/lib/utils';
import { motion, type MotionStyle, type Transition, useReducedMotion } from 'framer-motion';
import { cn } from '@/lib/utils';
🧰 Tools
🪛 GitHub Check: lint

[warning] 3-3:
There should be no empty line between import groups

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/magicui/border-beam.tsx` around lines 3 - 5, Remove the
extra blank line between the two import statements so the imports are contiguous
(i.e., put "import { motion, type MotionStyle, type Transition, useReducedMotion
} from 'framer-motion';" immediately above "import { cn } from '@/lib/utils';")
to satisfy the ESLint import-order/style rule; ensure no other spacing changes
are introduced and run lint to confirm the warning is resolved.

Comment on lines +48 to +51
**Files to change**:
| File | Change |
|------|--------|
| `renderer/components/sync/LoginModal.tsx` | Redesign as "Enable Sync" modal |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add blank line before the table to satisfy markdownlint MD058.

The table under “Files to change” is not surrounded by a blank line.

Suggested doc fix
 **Files to change**:
+
 | File | Change |
 |------|--------|
 | `renderer/components/sync/LoginModal.tsx` | Redesign as "Enable Sync" modal |
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 49-49: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-03-12-roadmap-auth-sync-ai.md` around lines 48 - 51, The
Markdown table under the "Files to change" heading lacks a blank line (violates
MD058); edit the document and insert a single empty line between the "Files to
change" heading and the table so the table is separated from the preceding
paragraph/heading—this affects the table that references
renderer/components/sync/LoginModal.tsx (redesign as "Enable Sync" modal).

Comment on lines +114 to +125
```
renderer/components/CommandK/
├── CommandKPanel.tsx # Modal overlay
├── CommandKInput.tsx # Search + AI input
├── CommandKResults.tsx # Result list
├── CommandKPreview.tsx # Preview pane (streaming AI)
└── useCommandK.ts # State hook

packages/ai-assistant/src/
├── commands/ # AI command definitions
└── registry.ts # Command registry
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Specify fenced code block languages (markdownlint MD040).

Both fenced blocks should declare a language (e.g., text) to clear lint warnings.

Suggested doc fix
-```
+```text
 renderer/components/CommandK/
   ├── CommandKPanel.tsx        # Modal overlay
   ├── CommandKInput.tsx        # Search + AI input
   ├── CommandKResults.tsx      # Result list
   ├── CommandKPreview.tsx      # Preview pane (streaming AI)
   └── useCommandK.ts           # State hook

 packages/ai-assistant/src/
   ├── commands/                 # AI command definitions
   └── registry.ts              # Command registry

- +text
Select text → Cmd+K → "Summarize" → AI streams result → Preview → "Insert" → Replaces selection

Also applies to: 129-131

🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 114-114: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-03-12-roadmap-auth-sync-ai.md` around lines 114 - 125, The
two fenced code blocks in the doc are missing language tags (markdownlint
MD040); update both blocks so the directory tree block (the one containing
"renderer/components/CommandK/ ... useCommandK.ts" and
"packages/ai-assistant/src/ ... registry.ts") and the action flow block ("Select
text → Cmd+K → \"Summarize\" → AI streams result → Preview → \"Insert\" →
Replaces selection") start with ```text (i.e., replace ``` with ```text) so the
linter is satisfied.

Comment thread packages/api/src/index.ts
Comment on lines +75 to +78
// Let HTTPException return its proper status code (e.g. 401, 403, 404)
if (err instanceof HTTPException) {
return err.getResponse();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Hono HTTPException getResponse() response format

💡 Result:

HTTPException#getResponse() returns a standard Web Response object.

  • If you threw new HTTPException(status, { message: '...' }), then getResponse() creates a new Response whose:

    • status = the exception’s status
    • body = the exception’s message (so it’s a text body, not a JSON shape unless you made it JSON yourself) [1][2]
  • If you threw new HTTPException(status, { res: someResponse }), then getResponse() returns the custom Response associated with the exception (i.e., whatever body/headers you put in res). Note: the status you pass to the HTTPException constructor is the one Hono uses for the exception response behavior, and docs note that the status inside the provided Response can be ignored [1][2].

  • getResponse() is not aware of Hono Context, so headers you set on c are not automatically applied—you must copy/apply them yourself if needed [1].

Sources: [1] [2]


🏁 Script executed:

# First, find the relevant files
fd -t f "index.ts" packages/api/src
fd -t f "auth.ts" packages/api/src

Repository: tomymaritano/readide

Length of output: 157


🏁 Script executed:

# Read the error handler section in index.ts with full context
cat -n packages/api/src/index.ts | sed -n '60,90p'

Repository: tomymaritano/readide

Length of output: 1017


🏁 Script executed:

# Check how HTTPException is thrown in auth.ts
rg "HTTPException" packages/api/src -B 2 -A 2

Repository: tomymaritano/readide

Length of output: 3128


🏁 Script executed:

# Check HTTPException usage and properties in Hono
rg "err\.status|HTTPException.*status" packages/api/src -B 2 -A 1

Repository: tomymaritano/readide

Length of output: 322


🏁 Script executed:

# Also check if there are any other error handlers or tests that might show HTTPException.status usage
fd -t f "\.ts$" packages/api/src -x grep -l "status" {} \; | head -5

Repository: tomymaritano/readide

Length of output: 243


🏁 Script executed:

# Check the routes/auth.ts file to see if there are additional HTTPException throws
cat packages/api/src/routes/auth.ts | head -50

Repository: tomymaritano/readide

Length of output: 1560


🏁 Script executed:

# Check Hono's HTTPException documentation or implementation in node_modules or package.json
cat packages/api/package.json | grep -i hono

Repository: tomymaritano/readide

Length of output: 123


🏁 Script executed:

# Search for any TypeScript/JSDoc definitions that might show HTTPException.status
rg "status" packages/api/src/middleware/auth.ts -B 3 -A 1 | head -30

Repository: tomymaritano/readide

Length of output: 46


🏁 Script executed:

# Verify the proposed fix would work by checking if any HTTPException instances reference status
rg "new HTTPException" packages/api/src -B 1 -A 1

Repository: tomymaritano/readide

Length of output: 1411


🏁 Script executed:

# Check if there's any usage of HTTPException status in the codebase or tests
rg "HTTPException" packages/api -A 5 | rg -i "\.status|getStatus"

Repository: tomymaritano/readide

Length of output: 146


🏁 Script executed:

# Look for TypeScript types or any type definitions in the repo
fd -t f "\.d\.ts$" packages/api/src

Repository: tomymaritano/readide

Length of output: 46


🏁 Script executed:

# Search for any type definitions or interface information about HTTPException in comments or imports
rg "HTTPException" packages/api/src/index.ts -A 10

Repository: tomymaritano/readide

Length of output: 917


🏁 Script executed:

# Check if there are any test files that might show HTTPException properties being accessed
fd -t f "test\|spec" packages/api | head -5

Repository: tomymaritano/readide

Length of output: 46


🏁 Script executed:

# Look at the actual error thrown to see if we can verify status property access pattern
cat -n packages/api/src/middleware/auth.ts | sed -n '1,40p'

Repository: tomymaritano/readide

Length of output: 1356


🏁 Script executed:

# Search for any tests or usage patterns that might show how to access HTTPException properties
rg "HTTPException" packages/api -B 2 -A 5 | rg -A 3 "instanceof HTTPException"

Repository: tomymaritano/readide

Length of output: 505


🏁 Script executed:

# Check if there's documentation or comments about accessing status/message from HTTPException
rg -i "message|status" packages/api/src/middleware/auth.ts | grep -i "http\|exception\|error" | head -10

Repository: tomymaritano/readide

Length of output: 604


🏁 Script executed:

# Wait for web search results about HTTPException API
echo "Awaiting web search results..."

Repository: tomymaritano/readide

Length of output: 94


Response format inconsistency: getResponse() returns plain text instead of JSON.

Using err.getResponse() preserves the correct status code but returns a plain text response body (e.g., "Token expired"). This is inconsistent with the JSON format used for the 404 handler (line 70) and generic error handler (lines 81-87).

API clients expecting consistent JSON error responses across all error types will fail to parse these plain text bodies.

🔧 Proposed fix for consistent JSON error responses
  // Error handler
  app.onError((err, c) => {
    // Let HTTPException return its proper status code (e.g. 401, 403, 404)
    if (err instanceof HTTPException) {
-     return err.getResponse();
+     return c.json({ error: err.message }, err.status);
    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Let HTTPException return its proper status code (e.g. 401, 403, 404)
if (err instanceof HTTPException) {
return err.getResponse();
}
// Let HTTPException return its proper status code (e.g. 401, 403, 404)
if (err instanceof HTTPException) {
return c.json({ error: err.message }, err.status);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/api/src/index.ts` around lines 75 - 78, When handling HTTPException,
normalize the returned body from err.getResponse() into a JSON object and
preserve the status code from err.getStatus(); specifically, replace the direct
return of err.getResponse() with logic that captures const resp =
err.getResponse(); if typeof resp === 'string' set body = { message: resp };
else if resp is an object ensure it has a message field (or pass through) and
then return the response with the correct status using err.getStatus() (e.g.,
res.status(err.getStatus()).json(body) or the equivalent in this handler), so
HTTPException responses match the JSON shape used by the 404 and generic error
handlers.

- Add missing `locale` prop destructuring in NumberTicker (caused
  ReferenceError during SSG prerender of /pricing)
- Remove --turbopack flag from web dev script (Turbopack ignores
  webpack extensionAlias config needed for .js→.ts resolution)
- Point licensing package exports to source instead of dist/
  (workspace package consumed directly, no build step in dev)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions github-actions Bot added the dependencies Pull requests that update a dependency file label Mar 13, 2026
@github-actions github-actions Bot merged commit b959a25 into develop Mar 13, 2026
12 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app:desktop app:web dependencies Pull requests that update a dependency file package:api size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant