feat(web,desktop,api): website redesign + auth UX rethink#148
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughQueues 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
Sequence Diagram(s)mermaid Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
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. Comment |
There was a problem hiding this comment.
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 | 🟠 MajorURL-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 ReadiedAlso 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 | 🟠 MajorReconcile 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (66)
apps/desktop/src/main/index.tsapps/desktop/src/renderer/components/sidebar/Sidebar.tsxapps/desktop/src/renderer/components/sidebar/SidebarFooter.tsxapps/desktop/src/renderer/components/sync/LoginModal.module.cssapps/desktop/src/renderer/components/sync/LoginModal.tsxapps/desktop/src/renderer/components/sync/index.tsapps/desktop/src/renderer/hooks/useSyncOnboarding.tsapps/desktop/src/renderer/styles/global.cssapps/web/app/(marketing)/auth/verify/AuthVerifyContent.tsxapps/web/app/(marketing)/changelog/page.tsxapps/web/app/(marketing)/download/page.tsxapps/web/app/(marketing)/faq/page.tsxapps/web/app/(marketing)/page.tsxapps/web/app/(marketing)/philosophy/page.tsxapps/web/app/(marketing)/plugins/page.tsxapps/web/app/(marketing)/pricing/page.tsxapps/web/app/(marketing)/privacy/page.tsxapps/web/app/(marketing)/terms/page.tsxapps/web/app/docs/[[...slug]]/page.tsxapps/web/app/docs/layout.tsxapps/web/app/globals.cssapps/web/app/layout.tsxapps/web/components/FaqAccordion.tsxapps/web/components/Footer.tsxapps/web/components/MobileNav.tsxapps/web/components/NavDropdown.tsxapps/web/components/Navbar.tsxapps/web/components/landing/Audience.tsxapps/web/components/landing/ComparisonTable.tsxapps/web/components/landing/CreatorStory.tsxapps/web/components/landing/Features.tsxapps/web/components/landing/Hero.tsxapps/web/components/landing/SocialProof.tsxapps/web/components/landing/Testimonials.tsxapps/web/components/landing/VideoGuides.tsxapps/web/components/landing/WhyLocal.tsxapps/web/components/magicui/animated-beam.tsxapps/web/components/magicui/animated-grid-pattern.tsxapps/web/components/magicui/animated-shiny-text.tsxapps/web/components/magicui/border-beam.tsxapps/web/components/magicui/dot-pattern.tsxapps/web/components/magicui/hero-video-dialog.tsxapps/web/components/magicui/marquee.tsxapps/web/components/magicui/number-ticker.tsxapps/web/components/magicui/shimmer-button.tsxapps/web/components/magicui/text-reveal.tsxapps/web/components/ui/accordion.tsxapps/web/components/ui/badge.tsxapps/web/components/ui/button.tsxapps/web/components/ui/card.tsxapps/web/components/ui/separator.tsxapps/web/components/ui/sheet.tsxapps/web/content/docs/architecture/overview.mdxapps/web/content/docs/guide/principles.mdxapps/web/content/docs/index.mdxapps/web/content/docs/plugins/getting-started.mdxapps/web/lib/layout.shared.tsxapps/web/lib/utils.tsapps/web/mdx-components.tsxapps/web/package.jsondocs/plans/2026-03-12-roadmap-auth-sync-ai.mddocs/plans/2026-03-12-website-redesign-design.mddocs/plans/2026-03-12-website-redesign-implementation.mdpackages/api/src/index.tspackages/api/src/middleware/auth.tspackages/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
| .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); | ||
| } |
There was a problem hiding this comment.
🧩 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.cssRepository: 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 5Repository: 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 3Repository: 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.cssRepository: 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 -40Repository: 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.cssRepository: 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.cssRepository: 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.
| .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.
| 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} |
There was a problem hiding this comment.
🧹 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.
| 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.
| 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, | ||
| }; |
There was a problem hiding this comment.
🧹 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.
| 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.
| ## 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 | | ||
|
|
There was a problem hiding this comment.
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.
| ``` | ||
| 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) | ||
| ``` |
There was a problem hiding this comment.
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.
| ``` | ||
| 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) | ||
| ``` |
There was a problem hiding this comment.
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 -->
| // 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; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's see the size of the file and understand its structure
wc -l apps/desktop/src/main/index.tsRepository: 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.tsRepository: 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.tsRepository: 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 20Repository: 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 10Repository: 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 5Repository: 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 5Repository: 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 20Repository: 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 5Repository: 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 -20Repository: 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.tsRepository: 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 10Repository: 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.tsRepository: 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.tsRepository: 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.tsRepository: 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.
🧩 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.tsRepository: tomymaritano/readide
Length of output: 77
🏁 Script executed:
# Check the context around lines 2484-2495
sed -n '2480,2500p' index.tsRepository: 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 -50Repository: 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/nullRepository: 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 -40Repository: tomymaritano/readide
Length of output: 112
🏁 Script executed:
# Check file exists and get line count
wc -l apps/desktop/src/main/index.tsRepository: 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.tsRepository: 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 -50Repository: tomymaritano/readide
Length of output: 1252
🏁 Script executed:
# Look for BrowserWindow.getAllWindows() usage
rg -n "BrowserWindow.getAllWindows" apps/desktop/src/main/index.tsRepository: 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 tsxRepository: 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.tsRepository: 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.tsRepository: 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.tsRepository: tomymaritano/readide
Length of output: 2382
🏁 Script executed:
# Search for where pendingAuthToken is declared and used
rg -n "pendingAuthToken" apps/desktop/src/main/index.tsRepository: 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 -50Repository: 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.tsRepository: 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 -30Repository: 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 -20Repository: tomymaritano/readide
Length of output: 484
🏁 Script executed:
# Look at the createNoteWindow function more completely
sed -n '323,362p' apps/desktop/src/main/index.tsRepository: 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.tsRepository: tomymaritano/readide
Length of output: 210
🏁 Script executed:
# Check if createNoteWindow stores the window reference
sed -n '323,365p' apps/desktop/src/main/index.tsRepository: 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.tsRepository: 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 -20Repository: 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 -100Repository: 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 -30Repository: 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.tsRepository: 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 tsxRepository: 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.tsxRepository: 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.tsRepository: 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.tsRepository: 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.tsxRepository: 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.
- 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>
6e31317 to
657734e
Compare
There was a problem hiding this comment.
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 | 🔵 TrivialUnnecessary
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 | 🔵 TrivialAdd 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 | 🔴 CriticalDestructure
localefrom 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,
localeshould 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 | 🟠 MajorAdd
localeto both the component destructuring and the effect dependency array.The
localeprop is defined in the interface but never destructured from the function parameters (line 17-25). Additionally, line 54 useslocaleinside the effect, but it's missing from the dependency array on line 60. Without destructuring, locale always defaults toundefined, 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 | 🟠 MajorUse a native
<button>element for the clickable card state.This
divis still acting like a button, which is why Biome flagsnoStaticElementInteractionsand requires manual Enter/Space key handling. Render<button type="button">whenhasVideois 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 | 🟡 MinorUpdate 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 | 🟠 MajorReduced-motion support still missing.
The accessibility attributes for duplicate panels are correct, but the component still lacks
prefers-reduced-motionsupport as flagged previously. Users with vestibular sensitivities will experience continuous animation.Add
motion-reduce:animate-noneto stop animation andmotion-reduce:hiddento 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 | 🟠 MajorAdd
-webkit-app-region: no-dragto the sync prompt.The
.sidebarcontainer is a draggable title-bar region (line 113). Other interactive sections like.sidebar-navand.sidebar-footerexplicitly 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 | 🟡 MinorAdd keyboard handler to overlay for accessibility compliance.
The overlay
<div>hasonClick={handleClose}but no keyboard handler. While the close button provides keyboard access, addingonKeyDownto 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 | 🟠 MajorDeep-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 toopen-urlflow, 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 3As 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 | 🟠 MajorThis dialog still lacks working keyboard focus management.
onKeyDownis bound to a non-focusablemotion.div, and the modal never takes, traps, or restores focus. After opening with the mouse, focus can stay on the background trigger, soEscapenever reaches this handler. Please reuse the Radix dialog pattern already present inapps/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 | 🟠 MajorThe hero demo remains a dead button while the URL is blank.
With
DEMO_VIDEO_URLhardcoded 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
typeattribute leaks onto non-button elements whenasChild=true.The
type={type}prop is forwarded unconditionally toComp. WhenasChild=true, theSlotpasses this to its child element. Per the relevant code snippets, this component is used withasChildwrapping<a>and<Link>elements (e.g., indownload/page.tsxandpricing/page.tsx), makingtype="button"invalid HTML on those elements.Only apply
typewhen 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
📒 Files selected for processing (35)
apps/desktop/src/main/index.tsapps/desktop/src/renderer/components/sidebar/Sidebar.tsxapps/desktop/src/renderer/components/sidebar/SidebarFooter.tsxapps/desktop/src/renderer/components/sync/LoginModal.module.cssapps/desktop/src/renderer/components/sync/LoginModal.tsxapps/desktop/src/renderer/components/sync/index.tsapps/desktop/src/renderer/hooks/useSyncOnboarding.tsapps/desktop/src/renderer/styles/global.cssapps/web/app/(marketing)/auth/verify/AuthVerifyContent.tsxapps/web/app/(marketing)/download/page.tsxapps/web/app/(marketing)/philosophy/page.tsxapps/web/app/(marketing)/pricing/page.tsxapps/web/app/globals.cssapps/web/components/FaqAccordion.tsxapps/web/components/Footer.tsxapps/web/components/Navbar.tsxapps/web/components/landing/Hero.tsxapps/web/components/landing/Testimonials.tsxapps/web/components/landing/VideoGuides.tsxapps/web/components/landing/WhyLocal.tsxapps/web/components/magicui/animated-beam.tsxapps/web/components/magicui/animated-grid-pattern.tsxapps/web/components/magicui/animated-shiny-text.tsxapps/web/components/magicui/border-beam.tsxapps/web/components/magicui/dot-pattern.tsxapps/web/components/magicui/hero-video-dialog.tsxapps/web/components/magicui/marquee.tsxapps/web/components/magicui/number-ticker.tsxapps/web/components/magicui/shimmer-button.tsxapps/web/components/magicui/text-reveal.tsxapps/web/components/ui/button.tsxapps/web/components/ui/card.tsxdocs/plans/2026-03-12-roadmap-auth-sync-ai.mdpackages/api/src/index.tspackages/api/src/middleware/auth.ts
| // 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; | ||
| } | ||
| }); |
There was a problem hiding this comment.
🧩 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 2Repository: 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 -50Repository: 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 -60Repository: 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.tsRepository: 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/nullRepository: tomymaritano/readide
Length of output: 435
🏁 Script executed:
fd -e test -e spec . apps/desktop/src | head -20Repository: 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 2Repository: 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 -20Repository: 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.
| import { EnableSyncModal } from '../sync'; | ||
| import { useSyncOnboarding } from '../../hooks/useSyncOnboarding'; |
There was a problem hiding this comment.
🧹 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.
| 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'; |
There was a problem hiding this comment.
🧹 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'.
| .hint { | ||
| margin-top: 16px !important; | ||
| margin-top: 12px !important; | ||
| font-size: 13px; | ||
| color: var(--color-text-tertiary, #999) !important; | ||
| } |
There was a problem hiding this comment.
🧹 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 |
There was a problem hiding this comment.
🧹 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.
| {items.map(item => ( | ||
| <AccordionItem | ||
| key={item.question} | ||
| value={`faq-${item.question.slice(0, 40).toLowerCase().replace(/\s+/g, '-')}`} | ||
| className="rounded-xl bg-surface/50" | ||
| > |
There was a problem hiding this comment.
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.
| import { motion, type MotionStyle, type Transition, useReducedMotion } from 'framer-motion'; | ||
|
|
||
| import { cn } from '@/lib/utils'; |
There was a problem hiding this comment.
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.
| 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.
| **Files to change**: | ||
| | File | Change | | ||
| |------|--------| | ||
| | `renderer/components/sync/LoginModal.tsx` | Redesign as "Enable Sync" modal | |
There was a problem hiding this comment.
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).
| ``` | ||
| 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 | ||
| ``` |
There was a problem hiding this comment.
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.
| // Let HTTPException return its proper status code (e.g. 401, 403, 404) | ||
| if (err instanceof HTTPException) { | ||
| return err.getResponse(); | ||
| } |
There was a problem hiding this comment.
🧩 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: '...' }), thengetResponse()creates a newResponsewhose:- 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]
- status = the exception’s
-
If you threw
new HTTPException(status, { res: someResponse }), thengetResponse()returns the customResponseassociated with the exception (i.e., whatever body/headers you put inres). Note: the status you pass to theHTTPExceptionconstructor is the one Hono uses for the exception response behavior, and docs note that the status inside the providedResponsecan be ignored [1][2]. -
getResponse()is not aware of HonoContext, so headers you set oncare 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/srcRepository: 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 2Repository: 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 1Repository: 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 -5Repository: 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 -50Repository: 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 honoRepository: 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 -30Repository: 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 1Repository: 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/srcRepository: 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 10Repository: 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 -5Repository: 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 -10Repository: 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.
| // 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>
Summary
Changes by area
Website (
apps/web/)Desktop (
apps/desktop/)EnableSyncModalwith 5-step wizard (value prop → email → sent → success)SidebarFooter: "Sign In" → "Enable Sync" (opens modal directly)useSyncOnboardinghook: prompt after 3+ notesAPI (
packages/api/)onErrorhandler swallowingHTTPException(401s returned as 500)Config (
packages/product-config/)Test plan
pnpm typecheck— 18/18 packages passpnpm test— all tests pass (pre-push hook)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
Bug Fixes
Documentation