Skip to content

Web - #3

Merged
Prince-695 merged 2 commits into
mainfrom
web
Jul 26, 2026
Merged

Web#3
Prince-695 merged 2 commits into
mainfrom
web

Conversation

@Prince-695

@Prince-695 Prince-695 commented Jul 26, 2026 •

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Added automatic CLI update checks, notifications, and an /update command.
    • Added ARM64 Linux release support and versioned release assets.
    • Introduced a redesigned landing page with animated hero, feature, installation, and navigation sections.
    • Added a comprehensive documentation center covering installation, commands, authentication, models, themes, tools, sessions, and troubleshooting.
    • Added improved authentication, theme switching, copying, tabs, accordions, animated text, and visual effects.
  • Documentation

    • Updated installation, release, platform support, update, and post-install guidance.

@vercel

vercel Bot commented Jul 26, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
bloom-web Ready Ready Preview, Comment Jul 26, 2026 5:41pm

@coderabbitai

coderabbitai Bot commented Jul 26, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds CLI self-update and versioned release asset support, updates installers and release automation, and introduces a branded web landing page with documentation routes, authentication layouts, animated UI primitives, theme styling, and supporting configuration.

Changes

CLI update and release flow

Layer / File(s) Summary
Update runtime and command flow
packages/cli/src/lib/update.ts, packages/cli/src/components/..., packages/cli/src/layouts/root-layout.tsx
Adds GitHub release checks, platform detection, binary replacement, /update, unauthenticated update access, and launch-time update notifications.
Versioned release artifacts and installers
scripts/build-cli-release.sh, scripts/install.*, packages/web/public/install.*, .github/workflows/release-cli.yml
Builds versioned binaries and checksums, publishes ARM Linux assets, and updates installers to prefer versioned assets with legacy fallbacks.
CLI update documentation
docs/manual-setup.md
Documents update behavior, opt-out configuration, release naming, publishing steps, and smoke checks.

Bloom web experience

Layer / File(s) Summary
Documentation foundation and routes
packages/web/lib/docs/*, packages/web/components/docs/*, packages/web/app/docs/*
Adds shared documentation data, navigation, layout components, content primitives, and documentation pages for CLI usage and Bloom concepts.
Site shell, landing page, and authentication presentation
packages/web/app/page.tsx, packages/web/components/site/*, packages/web/components/landing/*, packages/web/components/auth/*, packages/web/app/login/page.tsx, packages/web/app/cli/auth/page.tsx
Replaces the home page with branded landing sections, adds shared navigation and footer components, and restyles login and CLI authentication flows.
Animated UI primitives and theme system
packages/web/components/animate-ui/*, packages/web/components/ui/*, packages/web/hooks/*, packages/web/app/globals.css, packages/web/app/layout.tsx, packages/web/package.json
Adds animated code, text, buttons, tabs, accordions, scrolling, decorative effects, hooks, theme tokens, fonts, and supporting dependencies.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant GitHub Releases
  participant Release Asset
  CLI->>GitHub Releases: request latest release metadata
  GitHub Releases-->>CLI: return matching versioned asset
  CLI->>Release Asset: download binary
  Release Asset-->>CLI: return binary bytes
  CLI->>CLI: stage and replace executable
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is too vague to describe the actual changes; it only says "Web" and doesn’t convey the main update. Use a concise, specific title that names the primary change, such as adding web docs and CLI update support.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch web

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

❤️ Share

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

@Prince-695
Prince-695 merged commit 1538fdd into main Jul 26, 2026
7 of 8 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (10)
packages/web/lib/docs/data.ts-131-134 (1)

131-134: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Render ↑/↓ as separate alternatives, not a chord.

KeyCombo joins every key in row.keys with +, so this entry renders ↑ + ↓. Split it into two rows with shared action text, or change the shortcut representation to distinguish alternatives from chorded composites.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/lib/docs/data.ts` around lines 131 - 134, Update the shortcut
definition containing keys ["↑", "↓"] so the up and down arrows render as
separate alternatives rather than a single chord, while preserving the shared
“Move selection in command menu, mention menu, or dialogs” action text. Use
separate rows or the existing alternative representation, and leave true
multi-key chords unchanged.
packages/web/app/docs/themes/page.tsx-11-17 (1)

11-17: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use WCAG relative luminance for foreground selection.

The weighted RGB threshold selects light text for colors such as Synthwave’s #E86ACA, producing roughly 3:1 contrast for the text-xs card content. Compute sRGB relative luminance and choose the foreground with the higher contrast ratio.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/app/docs/themes/page.tsx` around lines 11 - 17, Update
contrastText to convert each sRGB channel to linearized values, compute WCAG
relative luminance, and compare the contrast ratios of "`#1a0a12`" and "`#fff8fa`"
against the background. Return whichever foreground provides the higher contrast
ratio, preserving the existing color options.
packages/web/components/site/bloom-logo.tsx-42-46 (1)

42-46: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use outline-hidden for the logo link’s accessibility preservation.

This project uses Tailwind v4, where outline-none is literal outline-style: none and no longer provides the forced-colors preservation that outline-hidden gives. Keep the existing focus-visible ring styling, but avoid fully suppressing the outline.

Suggested fix
-    <Link href={href} className="outline-none focus-visible:ring-2 focus-visible:ring-ring">
+    <Link href={href} className="outline-hidden focus-visible:ring-2 focus-visible:ring-ring">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/components/site/bloom-logo.tsx` around lines 42 - 46, Update the
Link element in the Bloom logo component to use Tailwind’s outline-hidden
utility instead of outline-none, while preserving the existing
focus-visible:ring-2 and focus-visible:ring-ring classes.
packages/web/components/animate-ui/components/buttons/copy.tsx-55-92 (1)

55-92: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard checks the wrong flag; overlapping timers can flicker the copied state.

if (copied) return; (Line 74) reads the external copied prop, not the resolved isCopied state, so in uncontrolled usage (the default) this guard is always a no-op since copied stays undefined. Rapid re-clicks queue multiple independent setTimeout calls that can reset isCopied out of order.

🐛 Proposed fix
+  const timeoutRef = React.useRef<ReturnType<typeof setTimeout>>();
+
   const handleCopy = React.useCallback(
     (e: React.MouseEvent<HTMLButtonElement>) => {
       onClick?.(e);
-      if (copied) return;
+      if (isCopied) return;
       if (content) {
         navigator.clipboard
           .writeText(content)
           .then(() => {
+            if (timeoutRef.current) clearTimeout(timeoutRef.current);
             setIsCopied(true);
             onCopiedChange?.(true, content);
-            setTimeout(() => {
+            timeoutRef.current = setTimeout(() => {
               setIsCopied(false);
               onCopiedChange?.(false);
             }, delay);
           })
           .catch((error) => {
             console.error('Error copying command', error);
           });
       }
     },
-    [onClick, copied, content, setIsCopied, onCopiedChange, delay],
+    [onClick, isCopied, content, setIsCopied, onCopiedChange, delay],
   );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/components/animate-ui/components/buttons/copy.tsx` around lines
55 - 92, Update CopyButton’s handleCopy guard to use the resolved isCopied state
rather than the external copied prop, and prevent overlapping reset timers from
independently changing the copied state. Ensure rapid clicks are ignored while
already copied and only the active timer resets isCopied and invokes
onCopiedChange(false).
packages/web/components/animate-ui/primitives/animate/code-block.tsx-78-84 (1)

78-84: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

onDone/onWrite fire on every effect re-run in non-writing mode.

onDone and onWrite are in the dep array (Line 128); with inline callbacks from a consumer, each parent render re-runs this branch and re-emits completion events. Gate on a "already reported" ref or require stable callbacks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/components/animate-ui/primitives/animate/code-block.tsx` around
lines 78 - 84, Prevent the non-writing branch of the React.useEffect from
emitting completion callbacks on every dependency-driven re-run. Use a ref to
track whether completion has already been reported for the current code, reset
it when a new write begins or the code changes, and only invoke onDone and
onWrite once while preserving the existing visible-code update.
packages/web/components/ui/text-generate-effect.tsx-39-49 (1)

39-49: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Text is unreadable without JS/animation and ignores reduced-motion.

Words start at opacity-0 + blur(10px); if the animation never runs (JS disabled, motion error, reduced-motion preference) the content is permanently invisible. Consider gating the initial hidden state or honoring useReducedMotion().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/components/ui/text-generate-effect.tsx` around lines 39 - 49,
Update the word spans rendered by the text-generate effect so their content
remains readable when animation does not run, including for users with reduced
motion enabled. Use the component’s motion/reduced-motion handling to avoid
applying the permanent opacity-0 and blur initial state in those cases, while
preserving the animated reveal for users who allow motion.
packages/web/components/ui/typewriter-effect.tsx-159-159 (1)

159-159: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Typo: lg:text:3xl is not a valid utility.

Should be lg:text-3xl; as written the class is dropped and the large-screen size jumps straight from md:text-xl to xl:text-5xl.

🐛 Proposed fix
-          className="text-xs sm:text-base md:text-xl lg:text:3xl xl:text-5xl font-bold"
+          className="text-xs sm:text-base md:text-xl lg:text-3xl xl:text-5xl font-bold"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/components/ui/typewriter-effect.tsx` at line 159, Correct the
responsive typography utility in the typewriter effect component’s className by
changing the invalid lg:text:3xl token to the valid lg:text-3xl token,
preserving the surrounding text-size classes.
packages/web/app/globals.css-184-191 (1)

184-191: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stylelint failure: blank line required before the declarations following @apply.

Also note background-attachment: fixed is a known scroll-jank/perf hazard on mobile Safari for full-page gradients.

🎨 Proposed fix
   body {
     `@apply` bg-background text-foreground;
+
     background-image:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/app/globals.css` around lines 184 - 191, Update the body rule by
adding a blank line between the `@apply` statement and the background
declarations, and remove background-attachment: fixed to avoid the mobile Safari
scroll-performance issue.

Source: Linters/SAST tools

packages/web/components/animate-ui/primitives/texts/typing.tsx-63-124 (1)

63-124: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Inline text arrays restart the animation on every parent re-render.

text is in the dep array and consumers pass an array literal (e.g. packages/web/components/landing/terminal-chassis.tsx passes an inline text={[...]}), so each re-render of the parent tears down the timer chain and retypes from scratch. Depend on a stable key instead, e.g. JSON.stringify/join of the normalized array.

🐛 Proposed fix
+  const texts = React.useMemo(
+    () => (typeof text === 'string' ? [text] : text),
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+    [typeof text === 'string' ? text : text.join('\u0000')],
+  );
+
   React.useEffect(() => {
     if (!started) return;
 
     const timeoutIds: Array<ReturnType<typeof setTimeout>> = [];
-    const texts: string[] = typeof text === 'string' ? [text] : text;
@@
-  }, [text, duration, started, loop, holdDelay]);
+  }, [texts, duration, started, loop, holdDelay]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/components/animate-ui/primitives/texts/typing.tsx` around lines
63 - 124, Update the React.useEffect dependency handling in the typing animation
to avoid using the unstable text array identity. Normalize text once and derive
a stable content key, such as a serialized or joined representation, then depend
on that key instead of text while preserving restarts when the text contents
actually change.
packages/web/components/ui/accordion.tsx-59-59 (1)

59-59: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add the accordion animation theme tokens for the data-state utilities.

data-open:animate-accordion-down and data-closed:animate-accordion-up are used in packages/web/components/ui/accordion.tsx, but packages/web/app/globals.css only defines --animate-spotlight. Without matching --animate-accordion-down/up variables and @keyframes, these utilities won’t produce the intended open/close animations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/components/ui/accordion.tsx` at line 59, Add the missing
--animate-accordion-down and --animate-accordion-up theme tokens and their
corresponding accordion-down and accordion-up `@keyframes` in globals.css,
matching the data-open/data-closed utilities used by the accordion component.
Preserve the existing --animate-spotlight definition.
🧹 Nitpick comments (14)
packages/web/public/install.sh (1)

56-73: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

legacy_asset_name duplicates detect_asset_prefix's OS/arch detection.

Both functions independently re-run the same uname -s/uname -m case logic. Since detect_asset_prefix already validates and exits on unsupported OS/arch before legacy_asset_name is ever called, this isn't a functional bug, but passing the already-resolved os/arch into legacy_asset_name (or reusing a shared helper) would remove the duplication.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/public/install.sh` around lines 56 - 73, Update
legacy_asset_name to reuse the already-resolved OS and architecture from
detect_asset_prefix instead of repeating uname-based detection. Pass those
values into legacy_asset_name, preserve its existing Windows .exe naming
behavior, and remove the duplicate case logic.
packages/web/components/landing/hero.tsx (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated var(--bloom-shadow) hard-shadow into a shared utility. The same neubrutalist shadow token is hand-rolled as an arbitrary Tailwind value at four sites, differing only by pixel offset; a shared @utility class (mirroring the existing bloom-emboss pattern) would centralize future theme tuning.

  • packages/web/components/landing/hero.tsx#L67-90: replace shadow-[4px_4px_0_0_var(--bloom-shadow)] / shadow-[3px_3px_0_0_var(--bloom-shadow)] with a shared bloom-shadow-md/bloom-shadow-sm utility.
  • packages/web/components/site/theme-toggle.tsx#L20-30: replace shadow-[2px_2px_0_0_var(--bloom-shadow)] with a bloom-shadow-xs utility.
  • packages/web/components/landing/feature-strip.tsx#L63-65: replace shadow-[3px_3px_0_0_var(--bloom-shadow)] with the shared bloom-shadow-sm utility.
  • packages/web/components/landing/install-cta.tsx#L66-79: replace both shadow-[3px_3px_0_0_var(--bloom-shadow)] occurrences with the shared bloom-shadow-sm utility.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/components/landing/hero.tsx` at line 1, Extract the repeated
var(--bloom-shadow) Tailwind values into shared `@utility` classes following the
existing bloom-emboss pattern, defining bloom-shadow-xs, bloom-shadow-sm, and
bloom-shadow-md for 2px, 3px, and 4px offsets. Replace the corresponding
arbitrary shadow classes in the landing hero, theme toggle, feature strip, and
install CTA while preserving all other styling.
packages/web/components/auth/login-form.tsx (1)

116-135: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate brand-card header markup vs. cli-auth-handoff.tsx.

This Card wrapper (gradient bar, BloomLogo header, title/description) mirrors the local AuthShell component added in packages/web/components/auth/cli-auth-handoff.tsx (Lines 402-430), but with extra Spotlight/shadow-none styling not present there. Suggest extracting a shared component to keep both auth surfaces in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/components/auth/login-form.tsx` around lines 116 - 135, The
login form card header duplicates the brand-card structure used by AuthShell in
cli-auth-handoff.tsx. Extract the shared gradient bar, BloomLogo, title, and
description markup into a reusable component, then update both AuthShell and the
login form to use it while preserving the login form’s Spotlight and shadow
styling.
packages/web/components/site/bloom-logo.tsx (1)

22-29: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid hardcoding priority on every logo instance.

BloomLogo is reused in the header, footer, login form, and CLI auth card (multiple instances rendered on the same page, e.g. header + footer). Marking every instance priority defeats the purpose of the hint — only the actual above-the-fold/LCP instance should preload.

♻️ Suggested fix: make priority opt-in
 type BloomLogoProps = {
   className?: string;
   href?: string;
   showWordmark?: boolean;
   size?: number;
+  priority?: boolean;
 };

 export function BloomLogo({
   className,
   href = "/",
   showWordmark = true,
   size = 36,
+  priority = false,
 }: BloomLogoProps) {
   ...
         <Image
           src="/logo.png"
           alt="Bloom"
           width={size}
           height={size}
           className="object-cover"
-          priority
+          priority={priority}
         />

Then pass priority only from the header instance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/components/site/bloom-logo.tsx` around lines 22 - 29, Update the
BloomLogo component to make the Image priority behavior opt-in rather than
always enabled, exposing a priority prop with an appropriate default. Keep
existing logo usages non-priority, and pass priority only from the header
instance that represents the above-the-fold/LCP logo.
packages/web/components/auth/cli-auth-handoff.tsx (1)

402-430: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate brand-card header markup vs. login-form.tsx.

This local AuthShell (Card + gradient bar + BloomLogo header) is nearly identical to the Card structure added in packages/web/components/auth/login-form.tsx (Lines 121-135), but already differs in details (shadow-none, Spotlight background) — sign of drift from copy-pasting. Consider extracting a shared AuthCard/AuthShell component both files import.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/components/auth/cli-auth-handoff.tsx` around lines 402 - 430,
Extract the duplicated branded Card/header structure from AuthShell and the
corresponding login-form.tsx markup into a shared AuthCard/AuthShell component,
then update both consumers to import and use it. Preserve each screen’s existing
title, description, children/footer behavior, shadow styling, and Spotlight
background through component props or composition rather than maintaining
separate copied implementations.
packages/web/components/animate-ui/primitives/animate/slot.tsx (1)

38-59: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

mergeProps overwrites, rather than composes, matching function props (e.g. onClick).

{ ...childProps, ...slotProps } lets slotProps silently clobber the child's own event handlers when both define the same prop name. Any future asChild consumer relying on the wrapped element's own onClick/other handlers while also forwarding a handler through Slot will have one of them dropped.

♻️ Suggested handler composition
 function mergeProps<T extends HTMLElement>(
   childProps: AnyProps,
   slotProps: DOMMotionProps<T>,
 ): AnyProps {
   const merged: AnyProps = { ...childProps, ...slotProps };
 
+  for (const key of Object.keys(slotProps)) {
+    if (
+      key.startsWith('on') &&
+      typeof (childProps as AnyProps)[key] === 'function' &&
+      typeof (slotProps as AnyProps)[key] === 'function'
+    ) {
+      const childHandler = (childProps as AnyProps)[key] as (...a: unknown[]) => void;
+      const slotHandler = (slotProps as AnyProps)[key] as (...a: unknown[]) => void;
+      merged[key] = (...args: unknown[]) => {
+        childHandler(...args);
+        slotHandler(...args);
+      };
+    }
+  }
+
   if (childProps.className || slotProps.className) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/components/animate-ui/primitives/animate/slot.tsx` around lines
38 - 59, Update mergeProps to compose matching function-valued props from
childProps and slotProps instead of allowing the spread merge to overwrite the
child handler. Preserve both callbacks in a stable order when the same prop
exists on both objects, while keeping the existing className and style merging
behavior unchanged.
packages/web/components/ui/sparkles.tsx (1)

28-40: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Randomized particle positions computed during render risk SSR/CSR hydration mismatches.

particles is built with Math.random() inside useMemo, which runs during the initial render. Since this is a 'use client' component that Next.js still server-renders for the initial HTML, the server-computed random left/top/size values will differ from the client's first render, producing per-particle hydration mismatches on the inline style attributes.

♻️ Suggested fix: generate particles client-side only
-import { useMemo } from "react";
+import { useEffect, useState } from "react";
 import { motion } from "motion/react";
 ...
-  const particles = useMemo(() => {
-    return Array.from({ length: particleDensity }, (_, index) => {
-      const size = minSize + Math.random() * (maxSize - minSize);
-      return {
-        id: index,
-        left: `${Math.random() * 100}%`,
-        top: `${Math.random() * 100}%`,
-        size,
-        delay: Math.random() * 4,
-        duration: 2 + Math.random() * 3,
-      };
-    });
-  }, [maxSize, minSize, particleDensity]);
+  const [particles, setParticles] = useState<
+    { id: number; left: string; top: string; size: number; delay: number; duration: number }[]
+  >([]);
+
+  useEffect(() => {
+    setParticles(
+      Array.from({ length: particleDensity }, (_, index) => {
+        const size = minSize + Math.random() * (maxSize - minSize);
+        return {
+          id: index,
+          left: `${Math.random() * 100}%`,
+          top: `${Math.random() * 100}%`,
+          size,
+          delay: Math.random() * 4,
+          duration: 2 + Math.random() * 3,
+        };
+      }),
+    );
+  }, [maxSize, minSize, particleDensity]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/components/ui/sparkles.tsx` around lines 28 - 40, Update the
particles initialization in the Sparkles component so random particle values are
generated only after client mount, avoiding Math.random() during the server
render and the client’s initial hydration render. Preserve the existing particle
shape and density, and render a stable empty or equivalent initial state until
the client-side values are created.
packages/web/components/animate-ui/components/animate/code.tsx (1)

89-95: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Keep the important modifiers compatible or switch to the v4 suffix syntax.

The package is configured for Tailwind v4, where !bg-transparent, !text-[13px], and !px-0 work only for backwards-compatibility; the supported form is bg-transparent!, text-[13px]!, and px-0!. Updating this makes the overrides less fragile against future tooling changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/components/animate-ui/components/animate/code.tsx` around lines
89 - 95, Update the Tailwind important modifiers in the className expression of
the code component to v4 suffix syntax: use bg-transparent!, text-[13px]!, and
px-0! while preserving the existing selectors and override behavior.
packages/web/components/ui/background-beams.tsx (1)

8-59: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Hoist the static paths array to module scope.

It's a constant 50-element literal rebuilt on every render.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/components/ui/background-beams.tsx` around lines 8 - 59, Move
the static 50-element paths array out of the component render scope to module
scope in background-beams.tsx, preserving its contents and making the component
reuse the hoisted paths constant on every render.
packages/web/package.json (1)

29-29: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Full shiki in a client bundle is heavy.

packages/web/components/animate-ui/primitives/animate/code-block.tsx does await import('shiki') and calls codeToHtml, which pulls the full language/theme set into the client chunk (multi-MB). For static docs snippets, highlight on the server (RSC) or use shiki/bundle/web with an explicit createHighlighter language/theme list.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/package.json` at line 29, Update the code-block highlighting
flow around the dynamic shiki import and codeToHtml call to avoid bundling the
full Shiki package in the client. Prefer moving highlighting to a server
component for static snippets; otherwise import shiki/bundle/web and configure
createHighlighter with only the required languages and themes.
packages/web/app/globals.css (1)

261-263: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer @utility over @layer utilities in Tailwind v4.

@utility font-display { ... } participates correctly in variant/ordering resolution; @layer utilities is the v3 idiom. Same applies to the other .bloom-* classes in this block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/app/globals.css` around lines 261 - 263, Replace the Tailwind v3
utility-layer declaration for font-display and the surrounding bloom-* utility
classes with individual `@utility` declarations, preserving each class’s existing
styles while enabling Tailwind v4 variant and ordering resolution.
packages/web/components/theme-provider.tsx (1)

61-66: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Listener is re-bound on every theme change.

Reading resolvedTheme from a ref (or keying off document.documentElement.classList) lets you register once with [setTheme].

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/components/theme-provider.tsx` around lines 61 - 66, Update the
theme provider’s keydown effect around onKeyDown so the window listener is
registered once rather than re-bound whenever resolvedTheme changes. Read the
current theme through a ref or document.documentElement.classList, keep setTheme
as the only dependency, and preserve the existing cleanup behavior.
packages/web/components/animate-ui/primitives/animate/code-block.tsx (2)

147-156: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Spread {...props} before the internal props so callers can't clobber dangerouslySetInnerHTML/ref.

Currently a caller passing children or dangerouslySetInnerHTML silently overrides the highlighted output (and React errors on children + innerHTML).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/components/animate-ui/primitives/animate/code-block.tsx` around
lines 147 - 156, Update the JSX element in the code block component so
`{...props}` is spread before the internal `ref`, data attributes, and
`dangerouslySetInnerHTML` props. Ensure callers cannot override the generated
highlighted output or internal ref, while preserving the existing highlighted
rendering behavior.

28-31: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Default themes object is a new reference each render and is in the effect dep array.

Any parent re-render re-triggers the shiki import + codeToHtml for the same content. Hoist the default to a module-level constant (and consider memoizing when callers pass an inline literal).

♻️ Proposed refactor
+const DEFAULT_THEMES = { light: 'github-light', dark: 'github-dark' } as const;
+
 function CodeBlock({
   ref,
   code,
   lang,
   theme = 'light',
-  themes = {
-    light: 'github-light',
-    dark: 'github-dark',
-  },
+  themes = DEFAULT_THEMES,

Also applies to: 76-76

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/components/animate-ui/primitives/animate/code-block.tsx` around
lines 28 - 31, Hoist the default themes object used by the code block component
to a module-level constant so its reference remains stable across renders and
the effect does not rerun unnecessarily. Update the component’s default themes
handling to use that constant, and memoize or otherwise stabilize
caller-provided inline themes before the effect dependency check.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/lib/update.ts`:
- Around line 113-124: Update fetchLatestRelease and the binary download fetch
in applyUpdate to use an AbortController-based timeout signal, ensuring stalled
network requests are aborted after the established timeout duration. Preserve
the existing request headers, response validation, and update flow while
applying the same timeout pattern to both calls.
- Around line 172-179: Update applyUpdate to verify the downloaded bytes against
a trusted checksum or signature published with the GitHub release before calling
Bun.write or replacing the executable. Fetch and parse the release integrity
asset, compare it with the downloaded asset using a secure digest, and abort the
update on missing or mismatched verification.
- Around line 193-232: Update the replacement flow after renameSync(target,
backup) so that if both renameSync(staging, target) and its copyFileSync
fallback fail, it restores backup to target before propagating the error.
Preserve the existing staging cleanup and outer error handling, and ensure
restoration failures do not mask the original installation error.
- Around line 180-184: Update applyUpdate to guard against running under a
script or runtime interpreter: allow self-update only when the process is a
compiled standalone binary, and otherwise refuse the update with an appropriate
warning before using process.execPath as the target. Preserve the existing
compiled-binary update flow and staging behavior.

In `@packages/web/components/animate-ui/primitives/animate/code-block.tsx`:
- Around line 94-122: Update the interval callback in the code-block animation
to increment index and invoke onWrite outside the setVisibleCode updater; make
the updater only derive and return the visible text from the already-advanced
index. Preserve the existing done handling and scrolling behavior while ensuring
updater re-invocation cannot advance state or emit duplicate callbacks.

In `@packages/web/components/animate-ui/primitives/animate/slot.tsx`:
- Around line 61-88: Move the React.isValidElement(children) guard in Slot
before any access to children.type, returning null for invalid or absent
children. Keep the isAlreadyMotion and Base useMemo hooks unconditional for
valid render paths, and preserve the existing ref and prop merging behavior.

In `@packages/web/components/docs/docs-shell.tsx`:
- Around line 27-70: Restore documentation navigation for mobile by adding a
compact navigation control or route list outside the lg-hidden sidebar that
renders DOCS_NAV below the lg breakpoint. Reuse the existing active-route logic
and preserve the current desktop sidebar behavior.

In `@packages/web/components/theme-provider.tsx`:
- Around line 41-59: Update the onKeyDown theme-toggle handler to avoid treating
an unmodified “d” as a global shortcut during interactive type-ahead or browser
find-as-you-type. Prefer requiring a modifier combination; otherwise extend the
existing isTypingTarget guard to skip targets within role-based menus,
listboxes, comboboxes, grids, elements with aria-haspopup, and equivalent
interactive controls.

In `@packages/web/components/ui/background-beams.tsx`:
- Around line 67-90: Update the decorative SVG in the background-beams component
to include aria-hidden="true". Use Motion’s useReducedMotion() to detect
reduced-motion preference and disable or short-circuit the paths.map animation
so the 50 gradient animations do not run when reduced motion is requested, while
preserving the existing animation for other users.

In `@packages/web/components/ui/tabs.tsx`:
- Around line 8-23: Update the Tabs component’s TabsPrimitive.Root invocation to
explicitly forward the destructured orientation prop, while retaining the
existing data-orientation attribute and styling behavior.

In `@packages/web/components/ui/text-generate-effect.tsx`:
- Around line 22-34: Update the reveal effects so content changes re-animate
newly rendered hidden spans: in
packages/web/components/ui/text-generate-effect.tsx lines 22-34, add words to
the useEffect dependency array; in
packages/web/components/ui/typewriter-effect.tsx lines 29-45, add both words and
animate to its dependency array.

In `@packages/web/lib/site.ts`:
- Around line 6-12: Update the APP_ORIGIN environment-variable selection to
treat empty values as unset, so an empty NEXT_PUBLIC_APP_URL falls through to
APP_URL and then the default origin. Preserve trailing-slash removal for
non-empty values and keep INSTALL_UNIX and INSTALL_WINDOWS using the resulting
APP_ORIGIN.

In `@packages/web/package.json`:
- Around line 16-29: Update the gsap dependency in package.json to a minimum
version of 3.6.0, preserving the current dependency declaration style, then
regenerate the lockfile so it resolves only patched versions.

---

Minor comments:
In `@packages/web/app/docs/themes/page.tsx`:
- Around line 11-17: Update contrastText to convert each sRGB channel to
linearized values, compute WCAG relative luminance, and compare the contrast
ratios of "`#1a0a12`" and "`#fff8fa`" against the background. Return whichever
foreground provides the higher contrast ratio, preserving the existing color
options.

In `@packages/web/app/globals.css`:
- Around line 184-191: Update the body rule by adding a blank line between the
`@apply` statement and the background declarations, and remove
background-attachment: fixed to avoid the mobile Safari scroll-performance
issue.

In `@packages/web/components/animate-ui/components/buttons/copy.tsx`:
- Around line 55-92: Update CopyButton’s handleCopy guard to use the resolved
isCopied state rather than the external copied prop, and prevent overlapping
reset timers from independently changing the copied state. Ensure rapid clicks
are ignored while already copied and only the active timer resets isCopied and
invokes onCopiedChange(false).

In `@packages/web/components/animate-ui/primitives/animate/code-block.tsx`:
- Around line 78-84: Prevent the non-writing branch of the React.useEffect from
emitting completion callbacks on every dependency-driven re-run. Use a ref to
track whether completion has already been reported for the current code, reset
it when a new write begins or the code changes, and only invoke onDone and
onWrite once while preserving the existing visible-code update.

In `@packages/web/components/animate-ui/primitives/texts/typing.tsx`:
- Around line 63-124: Update the React.useEffect dependency handling in the
typing animation to avoid using the unstable text array identity. Normalize text
once and derive a stable content key, such as a serialized or joined
representation, then depend on that key instead of text while preserving
restarts when the text contents actually change.

In `@packages/web/components/site/bloom-logo.tsx`:
- Around line 42-46: Update the Link element in the Bloom logo component to use
Tailwind’s outline-hidden utility instead of outline-none, while preserving the
existing focus-visible:ring-2 and focus-visible:ring-ring classes.

In `@packages/web/components/ui/accordion.tsx`:
- Line 59: Add the missing --animate-accordion-down and --animate-accordion-up
theme tokens and their corresponding accordion-down and accordion-up `@keyframes`
in globals.css, matching the data-open/data-closed utilities used by the
accordion component. Preserve the existing --animate-spotlight definition.

In `@packages/web/components/ui/text-generate-effect.tsx`:
- Around line 39-49: Update the word spans rendered by the text-generate effect
so their content remains readable when animation does not run, including for
users with reduced motion enabled. Use the component’s motion/reduced-motion
handling to avoid applying the permanent opacity-0 and blur initial state in
those cases, while preserving the animated reveal for users who allow motion.

In `@packages/web/components/ui/typewriter-effect.tsx`:
- Line 159: Correct the responsive typography utility in the typewriter effect
component’s className by changing the invalid lg:text:3xl token to the valid
lg:text-3xl token, preserving the surrounding text-size classes.

In `@packages/web/lib/docs/data.ts`:
- Around line 131-134: Update the shortcut definition containing keys ["↑", "↓"]
so the up and down arrows render as separate alternatives rather than a single
chord, while preserving the shared “Move selection in command menu, mention
menu, or dialogs” action text. Use separate rows or the existing alternative
representation, and leave true multi-key chords unchanged.

---

Nitpick comments:
In `@packages/web/app/globals.css`:
- Around line 261-263: Replace the Tailwind v3 utility-layer declaration for
font-display and the surrounding bloom-* utility classes with individual
`@utility` declarations, preserving each class’s existing styles while enabling
Tailwind v4 variant and ordering resolution.

In `@packages/web/components/animate-ui/components/animate/code.tsx`:
- Around line 89-95: Update the Tailwind important modifiers in the className
expression of the code component to v4 suffix syntax: use bg-transparent!,
text-[13px]!, and px-0! while preserving the existing selectors and override
behavior.

In `@packages/web/components/animate-ui/primitives/animate/code-block.tsx`:
- Around line 147-156: Update the JSX element in the code block component so
`{...props}` is spread before the internal `ref`, data attributes, and
`dangerouslySetInnerHTML` props. Ensure callers cannot override the generated
highlighted output or internal ref, while preserving the existing highlighted
rendering behavior.
- Around line 28-31: Hoist the default themes object used by the code block
component to a module-level constant so its reference remains stable across
renders and the effect does not rerun unnecessarily. Update the component’s
default themes handling to use that constant, and memoize or otherwise stabilize
caller-provided inline themes before the effect dependency check.

In `@packages/web/components/animate-ui/primitives/animate/slot.tsx`:
- Around line 38-59: Update mergeProps to compose matching function-valued props
from childProps and slotProps instead of allowing the spread merge to overwrite
the child handler. Preserve both callbacks in a stable order when the same prop
exists on both objects, while keeping the existing className and style merging
behavior unchanged.

In `@packages/web/components/auth/cli-auth-handoff.tsx`:
- Around line 402-430: Extract the duplicated branded Card/header structure from
AuthShell and the corresponding login-form.tsx markup into a shared
AuthCard/AuthShell component, then update both consumers to import and use it.
Preserve each screen’s existing title, description, children/footer behavior,
shadow styling, and Spotlight background through component props or composition
rather than maintaining separate copied implementations.

In `@packages/web/components/auth/login-form.tsx`:
- Around line 116-135: The login form card header duplicates the brand-card
structure used by AuthShell in cli-auth-handoff.tsx. Extract the shared gradient
bar, BloomLogo, title, and description markup into a reusable component, then
update both AuthShell and the login form to use it while preserving the login
form’s Spotlight and shadow styling.

In `@packages/web/components/landing/hero.tsx`:
- Line 1: Extract the repeated var(--bloom-shadow) Tailwind values into shared
`@utility` classes following the existing bloom-emboss pattern, defining
bloom-shadow-xs, bloom-shadow-sm, and bloom-shadow-md for 2px, 3px, and 4px
offsets. Replace the corresponding arbitrary shadow classes in the landing hero,
theme toggle, feature strip, and install CTA while preserving all other styling.

In `@packages/web/components/site/bloom-logo.tsx`:
- Around line 22-29: Update the BloomLogo component to make the Image priority
behavior opt-in rather than always enabled, exposing a priority prop with an
appropriate default. Keep existing logo usages non-priority, and pass priority
only from the header instance that represents the above-the-fold/LCP logo.

In `@packages/web/components/theme-provider.tsx`:
- Around line 61-66: Update the theme provider’s keydown effect around onKeyDown
so the window listener is registered once rather than re-bound whenever
resolvedTheme changes. Read the current theme through a ref or
document.documentElement.classList, keep setTheme as the only dependency, and
preserve the existing cleanup behavior.

In `@packages/web/components/ui/background-beams.tsx`:
- Around line 8-59: Move the static 50-element paths array out of the component
render scope to module scope in background-beams.tsx, preserving its contents
and making the component reuse the hoisted paths constant on every render.

In `@packages/web/components/ui/sparkles.tsx`:
- Around line 28-40: Update the particles initialization in the Sparkles
component so random particle values are generated only after client mount,
avoiding Math.random() during the server render and the client’s initial
hydration render. Preserve the existing particle shape and density, and render a
stable empty or equivalent initial state until the client-side values are
created.

In `@packages/web/package.json`:
- Line 29: Update the code-block highlighting flow around the dynamic shiki
import and codeToHtml call to avoid bundling the full Shiki package in the
client. Prefer moving highlighting to a server component for static snippets;
otherwise import shiki/bundle/web and configure createHighlighter with only the
required languages and themes.

In `@packages/web/public/install.sh`:
- Around line 56-73: Update legacy_asset_name to reuse the already-resolved OS
and architecture from detect_asset_prefix instead of repeating uname-based
detection. Pass those values into legacy_asset_name, preserve its existing
Windows .exe naming behavior, and remove the duplicate case logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bd153ab6-4d63-492d-924b-4b7c72266e25

📥 Commits

Reviewing files that changed from the base of the PR and between bdc2d7c and d7241b6.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • packages/web/public/logo.png is excluded by !**/*.png
📒 Files selected for processing (78)
  • .cursor/settings.json
  • .github/workflows/release-cli.yml
  • docs/manual-setup.md
  • packages/cli/src/components/command-menu/commands.tsx
  • packages/cli/src/components/input-bar.tsx
  • packages/cli/src/components/update-notifier.tsx
  • packages/cli/src/layouts/root-layout.tsx
  • packages/cli/src/lib/brand.ts
  • packages/cli/src/lib/update.ts
  • packages/web/app/cli/auth/page.tsx
  • packages/web/app/docs/architecture/page.tsx
  • packages/web/app/docs/auth/page.tsx
  • packages/web/app/docs/commands/page.tsx
  • packages/web/app/docs/configuration/page.tsx
  • packages/web/app/docs/getting-started/page.tsx
  • packages/web/app/docs/install/page.tsx
  • packages/web/app/docs/keyboard/page.tsx
  • packages/web/app/docs/mentions/page.tsx
  • packages/web/app/docs/models/page.tsx
  • packages/web/app/docs/modes/page.tsx
  • packages/web/app/docs/page.tsx
  • packages/web/app/docs/quota/page.tsx
  • packages/web/app/docs/sessions/page.tsx
  • packages/web/app/docs/themes/page.tsx
  • packages/web/app/docs/tools/page.tsx
  • packages/web/app/docs/troubleshooting/page.tsx
  • packages/web/app/globals.css
  • packages/web/app/layout.tsx
  • packages/web/app/login/page.tsx
  • packages/web/app/page.tsx
  • packages/web/components.json
  • packages/web/components/animate-ui/components/animate/code.tsx
  • packages/web/components/animate-ui/components/buttons/copy.tsx
  • packages/web/components/animate-ui/primitives/animate/code-block.tsx
  • packages/web/components/animate-ui/primitives/animate/slot.tsx
  • packages/web/components/animate-ui/primitives/buttons/button.tsx
  • packages/web/components/animate-ui/primitives/texts/highlight.tsx
  • packages/web/components/animate-ui/primitives/texts/typing.tsx
  • packages/web/components/auth/cli-auth-handoff.tsx
  • packages/web/components/auth/login-form.tsx
  • packages/web/components/docs/architecture-diagram.tsx
  • packages/web/components/docs/code-block.tsx
  • packages/web/components/docs/command-reference.tsx
  • packages/web/components/docs/docs-shell.tsx
  • packages/web/components/docs/keycap.tsx
  • packages/web/components/docs/prose.tsx
  • packages/web/components/landing/feature-strip.tsx
  • packages/web/components/landing/hero.tsx
  • packages/web/components/landing/install-cta.tsx
  • packages/web/components/landing/terminal-chassis.tsx
  • packages/web/components/site/bloom-logo.tsx
  • packages/web/components/site/site-footer.tsx
  • packages/web/components/site/site-header.tsx
  • packages/web/components/site/theme-toggle.tsx
  • packages/web/components/theme-provider.tsx
  • packages/web/components/ui/accordion.tsx
  • packages/web/components/ui/background-beams.tsx
  • packages/web/components/ui/badge.tsx
  • packages/web/components/ui/moving-border.tsx
  • packages/web/components/ui/scroll-area.tsx
  • packages/web/components/ui/sparkles.tsx
  • packages/web/components/ui/spotlight.tsx
  • packages/web/components/ui/tabs.tsx
  • packages/web/components/ui/text-generate-effect.tsx
  • packages/web/components/ui/typewriter-effect.tsx
  • packages/web/hooks/use-controlled-state.tsx
  • packages/web/hooks/use-is-in-view.tsx
  • packages/web/lib/docs/commands.ts
  • packages/web/lib/docs/data.ts
  • packages/web/lib/docs/nav.ts
  • packages/web/lib/get-strict-context.tsx
  • packages/web/lib/site.ts
  • packages/web/package.json
  • packages/web/public/install.ps1
  • packages/web/public/install.sh
  • scripts/build-cli-release.sh
  • scripts/install.ps1
  • scripts/install.sh

Comment on lines +113 to +124
async function fetchLatestRelease(): Promise<GitHubRelease> {
const response = await fetch(RELEASES_LATEST, {
headers: {
Accept: "application/vnd.github+json",
"User-Agent": "bloom-cli",
},
});
if (!response.ok) {
throw new Error(`GitHub Releases error (${response.status})`);
}
return (await response.json()) as GitHubRelease;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add timeouts to network calls.

Neither fetchLatestRelease nor the binary download in applyUpdate sets a timeout/abort signal. A stalled connection will hang the update flow (and the CLI process) indefinitely.

⏱️ Suggested fix
-  const response = await fetch(RELEASES_LATEST, {
-    headers: {
-      Accept: "application/vnd.github+json",
-      "User-Agent": "bloom-cli",
-    },
-  });
+  const response = await fetch(RELEASES_LATEST, {
+    headers: {
+      Accept: "application/vnd.github+json",
+      "User-Agent": "bloom-cli",
+    },
+    signal: AbortSignal.timeout(10_000),
+  });

Apply the same pattern to the download fetch in applyUpdate.

Also applies to: 172-177

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/lib/update.ts` around lines 113 - 124, Update
fetchLatestRelease and the binary download fetch in applyUpdate to use an
AbortController-based timeout signal, ensuring stalled network requests are
aborted after the established timeout duration. Preserve the existing request
headers, response validation, and update flow while applying the same timeout
pattern to both calls.

Comment on lines +172 to +179
const response = await fetch(update.downloadUrl, {
headers: { "User-Agent": "bloom-cli" },
});
if (!response.ok) {
throw new Error(`Download failed (${response.status})`);
}

const bytes = new Uint8Array(await response.arrayBuffer());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

No integrity verification of downloaded binary before replacing the running executable.

applyUpdate downloads the release asset and directly writes/executes it as a replacement for the CLI binary, without validating a checksum or signature against the GitHub release. If an asset were tampered with, corrupted in transit, or a release were compromised, this would silently install and later execute arbitrary code as the user replacing their own CLI binary.

🔒 Suggested approach
 type GitHubAsset = {
   name: string;
   browser_download_url: string;
+  digest?: string; // e.g. "sha256:..." if published in the release
 };

Verify bytes against a published checksum (e.g. a SHASUMS256.txt/.sha256 asset from the release) before calling Bun.write(staging, bytes), and abort on mismatch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/lib/update.ts` around lines 172 - 179, Update applyUpdate to
verify the downloaded bytes against a trusted checksum or signature published
with the GitHub release before calling Bun.write or replacing the executable.
Fetch and parse the release integrity asset, compare it with the downloaded
asset using a secure digest, and abort the update on missing or mismatched
verification.

Comment on lines +180 to +184
const target = process.execPath;
const staging = join(
tmpdir(),
`bloom-update-${update.latestVersion}-${Date.now()}`,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Bun process.execPath return the bun binary path when running a script vs the compiled executable path when built with --compile?

💡 Result:

Yes, Bun's process.execPath returns the path to the executable currently running the script, regardless of whether you are running a script with the bun CLI or running a standalone executable created via bun build --compile. In both scenarios, process.execPath provides the absolute path to the binary that initiated the process [1][2]. Specifically: 1. When running a script (e.g., bun run app.ts), process.execPath returns the path to the bun binary [2]. 2. When running a compiled standalone executable (e.g., ./myapp), process.execPath returns the path to that specific compiled executable file [1][2]. This behavior ensures that if the process spawns child processes (for example, via child_process.fork), it correctly identifies the host binary [3][4]. Note that while process.execPath behaves this way, process.argv[0] is also designed to point to this same executable path to ensure that spawned processes can correctly reference the binary they are running from [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate update.ts =="
fd -a 'update\.ts$' . | sed 's#^\./##'

echo "== snippet =="
file="$(fd 'update\.ts$' . | head -n 1)"
wc -l "$file"
sed -n '150,210p' "$file"

echo "== usages of applyUpdate/execPath/bun build/compile =="
rg -n "applyUpdate|process\.execPath|bun build --compile|--compile|dev:cli|manual-setup|run dev:cli" .

Repository: Prince-695/bloom

Length of output: 2530


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== update.ts tail =="
sed -n '210,245p' packages/cli/src/lib/update.ts

echo "== command-menu update usage =="
sed -n '135,175p' packages/cli/src/components/command-menu/commands.tsx

echo "== release build context =="
sed -n '90,115p' scripts/build-cli-release.sh

echo "== package scripts =="
sed -n '1,80p' package.json

echo "== docs manual setup context =="
sed -n '115,135p' docs/manual-setup.md

Repository: Prince-695/bloom

Length of output: 4284


Guard applyUpdate against script/runtime interpreters

process.execPath is the interpreter/runtime for script execution, and only points at the standalone CLI when run as a bun build --compile executable. With the documented bun run dev:cli workflow and the update action available through /update, self-updating can target and overwrite the Bun installer/bin path instead of the Bloom CLI, potentially damaging the developer’s tooling. Refuse self-update unless the process is a compiled standalone binary, or otherwise warn when process.execPath would replace the runtime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/lib/update.ts` around lines 180 - 184, Update applyUpdate to
guard against running under a script or runtime interpreter: allow self-update
only when the process is a compiled standalone binary, and otherwise refuse the
update with an appropriate warning before using process.execPath as the target.
Preserve the existing compiled-binary update flow and staging behavior.

Comment on lines +193 to +232
try {
// Atomic-ish replace: move aside, then move new into place
const backup = `${target}.bak`;
try {
unlinkSync(backup);
} catch {
// no previous backup
}
try {
renameSync(target, backup);
} catch {
// If rename of running binary fails, try direct overwrite via copy
copyFileSync(staging, target);
try {
chmodSync(target, 0o755);
} catch {
// ignore
}
unlinkSync(staging);
return update;
}

try {
renameSync(staging, target);
} catch {
copyFileSync(staging, target);
unlinkSync(staging);
}

try {
chmodSync(target, 0o755);
} catch {
// ignore
}

try {
unlinkSync(backup);
} catch {
// keep .bak if delete fails (Windows)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Broken-install state if both the rename and copy fallback fail after backup.

Once renameSync(target, backup) succeeds, target no longer exists. If the subsequent renameSync(staging, target) fails and its copyFileSync(staging, target) fallback (Lines 215-220) also throws, that exception propagates to the outer catch (Lines 233-242), which only deletes staging and rethrows — it never restores backup back to target. The user is left with no executable at target at all, only a .bak file, until they manually recover it.

🛠️ Suggested fix
     try {
       renameSync(staging, target);
     } catch {
-      copyFileSync(staging, target);
-      unlinkSync(staging);
+      try {
+        copyFileSync(staging, target);
+        unlinkSync(staging);
+      } catch (copyError) {
+        try {
+          renameSync(backup, target);
+        } catch {
+          // best effort restore
+        }
+        throw copyError;
+      }
     }
📝 Committable suggestion

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

Suggested change
try {
// Atomic-ish replace: move aside, then move new into place
const backup = `${target}.bak`;
try {
unlinkSync(backup);
} catch {
// no previous backup
}
try {
renameSync(target, backup);
} catch {
// If rename of running binary fails, try direct overwrite via copy
copyFileSync(staging, target);
try {
chmodSync(target, 0o755);
} catch {
// ignore
}
unlinkSync(staging);
return update;
}
try {
renameSync(staging, target);
} catch {
copyFileSync(staging, target);
unlinkSync(staging);
}
try {
chmodSync(target, 0o755);
} catch {
// ignore
}
try {
unlinkSync(backup);
} catch {
// keep .bak if delete fails (Windows)
}
try {
// Atomic-ish replace: move aside, then move new into place
const backup = `${target}.bak`;
try {
unlinkSync(backup);
} catch {
// no previous backup
}
try {
renameSync(target, backup);
} catch {
// If rename of running binary fails, try direct overwrite via copy
copyFileSync(staging, target);
try {
chmodSync(target, 0o755);
} catch {
// ignore
}
unlinkSync(staging);
return update;
}
try {
renameSync(staging, target);
} catch {
try {
copyFileSync(staging, target);
unlinkSync(staging);
} catch (copyError) {
try {
renameSync(backup, target);
} catch {
// best effort restore
}
throw copyError;
}
}
try {
chmodSync(target, 0o755);
} catch {
// ignore
}
try {
unlinkSync(backup);
} catch {
// keep .bak if delete fails (Windows)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/lib/update.ts` around lines 193 - 232, Update the
replacement flow after renameSync(target, backup) so that if both
renameSync(staging, target) and its copyFileSync fallback fail, it restores
backup to target before propagating the error. Preserve the existing staging
cleanup and outer error handling, and ensure restoration failures do not mask
the original installation error.

Comment on lines +94 to +122
const timeout = setTimeout(() => {
intervalId = setInterval(() => {
if (index < characters.length) {
setVisibleCode(() => {
const nextChar = characters.slice(0, index + 1).join('');
onWrite?.({
index: index + 1,
length: characters.length,
done: false,
});
index += 1;
return nextChar;
});
localRef.current?.scrollTo({
top: localRef.current?.scrollHeight,
behavior: 'smooth',
});
} else {
clearInterval(intervalId);
setIsDone(true);
onDone?.();
onWrite?.({
index: characters.length,
length: characters.length,
done: true,
});
}
}, interval);
}, delay);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Side effects inside the state updater break under StrictMode / double invocation.

setVisibleCode(() => {...}) mutates index and calls onWrite inside the updater. React may invoke updaters more than once (StrictMode dev, re-render bail-outs), which double-advances index and emits duplicate onWrite events. Move the mutation and callback out of the updater.

🐛 Proposed fix
       intervalId = setInterval(() => {
         if (index < characters.length) {
-          setVisibleCode(() => {
-            const nextChar = characters.slice(0, index + 1).join('');
-            onWrite?.({
-              index: index + 1,
-              length: characters.length,
-              done: false,
-            });
-            index += 1;
-            return nextChar;
-          });
+          index += 1;
+          setVisibleCode(characters.slice(0, index).join(''));
+          onWrite?.({
+            index,
+            length: characters.length,
+            done: false,
+          });
📝 Committable suggestion

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

Suggested change
const timeout = setTimeout(() => {
intervalId = setInterval(() => {
if (index < characters.length) {
setVisibleCode(() => {
const nextChar = characters.slice(0, index + 1).join('');
onWrite?.({
index: index + 1,
length: characters.length,
done: false,
});
index += 1;
return nextChar;
});
localRef.current?.scrollTo({
top: localRef.current?.scrollHeight,
behavior: 'smooth',
});
} else {
clearInterval(intervalId);
setIsDone(true);
onDone?.();
onWrite?.({
index: characters.length,
length: characters.length,
done: true,
});
}
}, interval);
}, delay);
const timeout = setTimeout(() => {
intervalId = setInterval(() => {
if (index < characters.length) {
index += 1;
setVisibleCode(characters.slice(0, index).join(''));
onWrite?.({
index,
length: characters.length,
done: false,
});
localRef.current?.scrollTo({
top: localRef.current?.scrollHeight,
behavior: 'smooth',
});
} else {
clearInterval(intervalId);
setIsDone(true);
onDone?.();
onWrite?.({
index: characters.length,
length: characters.length,
done: true,
});
}
}, interval);
}, delay);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/components/animate-ui/primitives/animate/code-block.tsx` around
lines 94 - 122, Update the interval callback in the code-block animation to
increment index and invoke onWrite outside the setVisibleCode updater; make the
updater only derive and return the visible text from the already-advanced index.
Preserve the existing done handling and scrolling behavior while ensuring
updater re-invocation cannot advance state or emit duplicate callbacks.

Comment on lines +67 to +90
<svg
className="pointer-events-none absolute z-0 h-full w-full"
width="100%"
height="100%"
viewBox="0 0 696 316"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 819M-324 -253C-324 -253 -256 152 208 279C672 406 740 811 740 811M-317 -261C-317 -261 -249 144 215 271C679 398 747 803 747 803M-310 -269C-310 -269 -242 136 222 263C686 390 754 795 754 795M-303 -277C-303 -277 -235 128 229 255C693 382 761 787 761 787M-296 -285C-296 -285 -228 120 236 247C700 374 768 779 768 779M-289 -293C-289 -293 -221 112 243 239C707 366 775 771 775 771M-282 -301C-282 -301 -214 104 250 231C714 358 782 763 782 763M-275 -309C-275 -309 -207 96 257 223C721 350 789 755 789 755M-268 -317C-268 -317 -200 88 264 215C728 342 796 747 796 747M-261 -325C-261 -325 -193 80 271 207C735 334 803 739 803 739M-254 -333C-254 -333 -186 72 278 199C742 326 810 731 810 731M-247 -341C-247 -341 -179 64 285 191C749 318 817 723 817 723M-240 -349C-240 -349 -172 56 292 183C756 310 824 715 824 715M-233 -357C-233 -357 -165 48 299 175C763 302 831 707 831 707M-226 -365C-226 -365 -158 40 306 167C770 294 838 699 838 699M-219 -373C-219 -373 -151 32 313 159C777 286 845 691 845 691M-212 -381C-212 -381 -144 24 320 151C784 278 852 683 852 683M-205 -389C-205 -389 -137 16 327 143C791 270 859 675 859 675M-198 -397C-198 -397 -130 8 334 135C798 262 866 667 866 667M-191 -405C-191 -405 -123 0 341 127C805 254 873 659 873 659M-184 -413C-184 -413 -116 -8 348 119C812 246 880 651 880 651M-177 -421C-177 -421 -109 -16 355 111C819 238 887 643 887 643M-170 -429C-170 -429 -102 -24 362 103C826 230 894 635 894 635M-163 -437C-163 -437 -95 -32 369 95C833 222 901 627 901 627M-156 -445C-156 -445 -88 -40 376 87C840 214 908 619 908 619M-149 -453C-149 -453 -81 -48 383 79C847 206 915 611 915 611M-142 -461C-142 -461 -74 -56 390 71C854 198 922 603 922 603M-135 -469C-135 -469 -67 -64 397 63C861 190 929 595 929 595M-128 -477C-128 -477 -60 -72 404 55C868 182 936 587 936 587M-121 -485C-121 -485 -53 -80 411 47C875 174 943 579 943 579M-114 -493C-114 -493 -46 -88 418 39C882 166 950 571 950 571M-107 -501C-107 -501 -39 -96 425 31C889 158 957 563 957 563M-100 -509C-100 -509 -32 -104 432 23C896 150 964 555 964 555M-93 -517C-93 -517 -25 -112 439 15C903 142 971 547 971 547M-86 -525C-86 -525 -18 -120 446 7C910 134 978 539 978 539M-79 -533C-79 -533 -11 -128 453 -1C917 126 985 531 985 531M-72 -541C-72 -541 -4 -136 460 -9C924 118 992 523 992 523M-65 -549C-65 -549 3 -144 467 -17C931 110 999 515 999 515M-58 -557C-58 -557 10 -152 474 -25C938 102 1006 507 1006 507M-51 -565C-51 -565 17 -160 481 -33C945 94 1013 499 1013 499M-44 -573C-44 -573 24 -168 488 -41C952 86 1020 491 1020 491M-37 -581C-37 -581 31 -176 495 -49C959 78 1027 483 1027 483M-30 -589C-30 -589 38 -184 502 -57C966 70 1034 475 1034 475M-23 -597C-23 -597 45 -192 509 -65C973 62 1041 467 1041 467M-16 -605C-16 -605 52 -200 516 -73C980 54 1048 459 1048 459M-9 -613C-9 -613 59 -208 523 -81C987 46 1055 451 1055 451M-2 -621C-2 -621 66 -216 530 -89C994 38 1062 443 1062 443M5 -629C5 -629 73 -224 537 -97C1001 30 1069 435 1069 435M12 -637C12 -637 80 -232 544 -105C1008 22 1076 427 1076 427M19 -645C19 -645 87 -240 551 -113C1015 14 1083 419 1083 419"
stroke="url(#paint0_radial_242_278)"
strokeOpacity="0.05"
strokeWidth="0.5"
></path>

{paths.map((path, index) => (
<motion.path
key={`path-` + index}
d={path}
stroke={`url(#linearGradient-${index})`}
strokeOpacity="0.4"
strokeWidth="0.5"
></motion.path>
))}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Decorative SVG should be hidden from AT and respect reduced motion.

50 infinitely repeating gradient animations run unconditionally. Add aria-hidden="true" to the <svg> and disable/short-circuit the animation when prefers-reduced-motion: reduce (Motion's useReducedMotion()).

♿ Proposed fix
+import { motion, useReducedMotion } from "motion/react";
@@
+    const reduceMotion = useReducedMotion();
@@
         <svg
+          aria-hidden="true"
           className="pointer-events-none absolute z-0 h-full w-full"
@@
-                animate={{
-                  x1: ["0%", "100%"],
-                  x2: ["0%", "95%"],
-                  y1: ["0%", "100%"],
-                  y2: ["0%", `${93 + Math.random() * 8}%`],
-                }}
+                animate={
+                  reduceMotion
+                    ? undefined
+                    : {
+                        x1: ["0%", "100%"],
+                        x2: ["0%", "95%"],
+                        y1: ["0%", "100%"],
+                        y2: ["0%", `${93 + Math.random() * 8}%`],
+                      }
+                }
📝 Committable suggestion

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

Suggested change
<svg
className="pointer-events-none absolute z-0 h-full w-full"
width="100%"
height="100%"
viewBox="0 0 696 316"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 819M-324 -253C-324 -253 -256 152 208 279C672 406 740 811 740 811M-317 -261C-317 -261 -249 144 215 271C679 398 747 803 747 803M-310 -269C-310 -269 -242 136 222 263C686 390 754 795 754 795M-303 -277C-303 -277 -235 128 229 255C693 382 761 787 761 787M-296 -285C-296 -285 -228 120 236 247C700 374 768 779 768 779M-289 -293C-289 -293 -221 112 243 239C707 366 775 771 775 771M-282 -301C-282 -301 -214 104 250 231C714 358 782 763 782 763M-275 -309C-275 -309 -207 96 257 223C721 350 789 755 789 755M-268 -317C-268 -317 -200 88 264 215C728 342 796 747 796 747M-261 -325C-261 -325 -193 80 271 207C735 334 803 739 803 739M-254 -333C-254 -333 -186 72 278 199C742 326 810 731 810 731M-247 -341C-247 -341 -179 64 285 191C749 318 817 723 817 723M-240 -349C-240 -349 -172 56 292 183C756 310 824 715 824 715M-233 -357C-233 -357 -165 48 299 175C763 302 831 707 831 707M-226 -365C-226 -365 -158 40 306 167C770 294 838 699 838 699M-219 -373C-219 -373 -151 32 313 159C777 286 845 691 845 691M-212 -381C-212 -381 -144 24 320 151C784 278 852 683 852 683M-205 -389C-205 -389 -137 16 327 143C791 270 859 675 859 675M-198 -397C-198 -397 -130 8 334 135C798 262 866 667 866 667M-191 -405C-191 -405 -123 0 341 127C805 254 873 659 873 659M-184 -413C-184 -413 -116 -8 348 119C812 246 880 651 880 651M-177 -421C-177 -421 -109 -16 355 111C819 238 887 643 887 643M-170 -429C-170 -429 -102 -24 362 103C826 230 894 635 894 635M-163 -437C-163 -437 -95 -32 369 95C833 222 901 627 901 627M-156 -445C-156 -445 -88 -40 376 87C840 214 908 619 908 619M-149 -453C-149 -453 -81 -48 383 79C847 206 915 611 915 611M-142 -461C-142 -461 -74 -56 390 71C854 198 922 603 922 603M-135 -469C-135 -469 -67 -64 397 63C861 190 929 595 929 595M-128 -477C-128 -477 -60 -72 404 55C868 182 936 587 936 587M-121 -485C-121 -485 -53 -80 411 47C875 174 943 579 943 579M-114 -493C-114 -493 -46 -88 418 39C882 166 950 571 950 571M-107 -501C-107 -501 -39 -96 425 31C889 158 957 563 957 563M-100 -509C-100 -509 -32 -104 432 23C896 150 964 555 964 555M-93 -517C-93 -517 -25 -112 439 15C903 142 971 547 971 547M-86 -525C-86 -525 -18 -120 446 7C910 134 978 539 978 539M-79 -533C-79 -533 -11 -128 453 -1C917 126 985 531 985 531M-72 -541C-72 -541 -4 -136 460 -9C924 118 992 523 992 523M-65 -549C-65 -549 3 -144 467 -17C931 110 999 515 999 515M-58 -557C-58 -557 10 -152 474 -25C938 102 1006 507 1006 507M-51 -565C-51 -565 17 -160 481 -33C945 94 1013 499 1013 499M-44 -573C-44 -573 24 -168 488 -41C952 86 1020 491 1020 491M-37 -581C-37 -581 31 -176 495 -49C959 78 1027 483 1027 483M-30 -589C-30 -589 38 -184 502 -57C966 70 1034 475 1034 475M-23 -597C-23 -597 45 -192 509 -65C973 62 1041 467 1041 467M-16 -605C-16 -605 52 -200 516 -73C980 54 1048 459 1048 459M-9 -613C-9 -613 59 -208 523 -81C987 46 1055 451 1055 451M-2 -621C-2 -621 66 -216 530 -89C994 38 1062 443 1062 443M5 -629C5 -629 73 -224 537 -97C1001 30 1069 435 1069 435M12 -637C12 -637 80 -232 544 -105C1008 22 1076 427 1076 427M19 -645C19 -645 87 -240 551 -113C1015 14 1083 419 1083 419"
stroke="url(#paint0_radial_242_278)"
strokeOpacity="0.05"
strokeWidth="0.5"
></path>
{paths.map((path, index) => (
<motion.path
key={`path-` + index}
d={path}
stroke={`url(#linearGradient-${index})`}
strokeOpacity="0.4"
strokeWidth="0.5"
></motion.path>
))}
<svg
aria-hidden="true"
className="pointer-events-none absolute z-0 h-full w-full"
width="100%"
height="100%"
viewBox="0 0 696 316"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M-380 -189C-380 -189 -312 216 152 343C616 470 684 875 684 875M-373 -197C-373 -197 -305 208 159 335C623 462 691 867 691 867M-366 -205C-366 -205 -298 200 166 327C630 454 698 859 698 859M-359 -213C-359 -213 -291 192 173 319C637 446 705 851 705 851M-352 -221C-352 -221 -284 184 180 311C644 438 712 843 712 843M-345 -229C-345 -229 -277 176 187 303C651 430 719 835 719 835M-338 -237C-338 -237 -270 168 194 295C658 422 726 827 726 827M-331 -245C-331 -245 -263 160 201 287C665 414 733 819 733 819M-324 -253C-324 -253 -256 152 208 279C672 406 740 811 740 811M-317 -261C-317 -261 -249 144 215 271C679 398 747 803 747 803M-310 -269C-310 -269 -242 136 222 263C686 390 754 795 754 795M-303 -277C-303 -277 -235 128 229 255C693 382 761 787 761 787M-296 -285C-296 -285 -228 120 236 247C700 374 768 779 768 779M-289 -293C-289 -293 -221 112 243 239C707 366 775 771 775 771M-282 -301C-282 -301 -214 104 250 231C714 358 782 763 782 763M-275 -309C-275 -309 -207 96 257 223C721 350 789 755 789 755M-268 -317C-268 -317 -200 88 264 215C728 342 796 747 796 747M-261 -325C-261 -325 -193 80 271 207C735 334 803 739 803 739M-254 -333C-254 -333 -186 72 278 199C742 326 810 731 810 731M-247 -341C-247 -341 -179 64 285 191C749 318 817 723 817 723M-240 -349C-240 -349 -172 56 292 183C756 310 824 715 824 715M-233 -357C-233 -357 -165 48 299 175C763 302 831 707 831 707M-226 -365C-226 -365 -158 40 306 167C770 294 838 699 838 699M-219 -373C-219 -373 -151 32 313 159C777 286 845 691 845 691M-212 -381C-212 -381 -144 24 320 151C784 278 852 683 852 683M-205 -389C-205 -389 -137 16 327 143C791 270 859 675 859 675M-198 -397C-198 -397 -130 8 334 135C798 262 866 667 866 667M-191 -405C-191 -405 -123 0 341 127C805 254 873 659 873 659M-184 -413C-184 -413 -116 -8 348 119C812 246 880 651 880 651M-177 -421C-177 -421 -109 -16 355 111C819 238 887 643 887 643M-170 -429C-170 -429 -102 -24 362 103C826 230 894 635 894 635M-163 -437C-163 -437 -95 -32 369 95C833 222 901 627 901 627M-156 -445C-156 -445 -88 -40 376 87C840 214 908 619 908 619M-149 -453C-149 -453 -81 -48 383 79C847 206 915 611 915 611M-142 -461C-142 -461 -74 -56 390 71C854 198 922 603 922 603M-135 -469C-135 -469 -67 -64 397 63C861 190 929 595 929 595M-128 -477C-128 -477 -60 -72 404 55C868 182 936 587 936 587M-121 -485C-121 -485 -53 -80 411 47C875 174 943 579 943 579M-114 -493C-114 -493 -46 -88 418 39C882 166 950 571 950 571M-107 -501C-107 -501 -39 -96 425 31C889 158 957 563 957 563M-100 -509C-100 -509 -32 -104 432 23C896 150 964 555 964 555M-93 -517C-93 -517 -25 -112 439 15C903 142 971 547 971 547M-86 -525C-86 -525 -18 -120 446 7C910 134 978 539 978 539M-79 -533C-79 -533 -11 -128 453 -1C917 126 985 531 985 531M-72 -541C-72 -541 -4 -136 460 -9C924 118 992 523 992 523M-65 -549C-65 -549 3 -144 467 -17C931 110 999 515 999 515M-58 -557C-58 -557 10 -152 474 -25C938 102 1006 507 1006 507M-51 -565C-51 -565 17 -160 481 -33C945 94 1013 499 1013 499M-44 -573C-44 -573 24 -168 488 -41C952 86 1020 491 1020 491M-37 -581C-37 -581 31 -176 495 -49C959 78 1027 483 1027 483M-30 -589C-30 -589 38 -184 502 -57C966 70 1034 475 1034 475M-23 -597C-23 -597 45 -192 509 -65C973 62 1041 467 1041 467M-16 -605C-16 -605 52 -200 516 -73C980 54 1048 459 1048 459M-9 -613C-9 -613 59 -208 523 -81C987 46 1055 451 1055 451M-2 -621C-2 -621 66 -216 530 -89C994 38 1062 443 1062 443M5 -629C5 -629 73 -224 537 -97C1001 30 1069 435 1069 435M12 -637C12 -637 80 -232 544 -105C1008 22 1076 427 1076 427M19 -645C19 -645 87 -240 551 -113C1015 14 1083 419 1083 419"
stroke="url(`#paint0_radial_242_278`)"
strokeOpacity="0.05"
strokeWidth="0.5"
></path>
{paths.map((path, index) => (
<motion.path
key={`path-` + index}
d={path}
stroke={`url(`#linearGradient-`${index})`}
strokeOpacity="0.4"
strokeWidth="0.5"
></motion.path>
))}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/components/ui/background-beams.tsx` around lines 67 - 90, Update
the decorative SVG in the background-beams component to include
aria-hidden="true". Use Motion’s useReducedMotion() to detect reduced-motion
preference and disable or short-circuit the paths.map animation so the 50
gradient animations do not run when reduced motion is requested, while
preserving the existing animation for other users.

Comment on lines +8 to +23
function Tabs({
className,
orientation = "horizontal",
...props
}: TabsPrimitive.Root.Props) {
return (
<TabsPrimitive.Root
data-slot="tabs"
data-orientation={orientation}
className={cn(
"group/tabs flex gap-2 data-horizontal:flex-col",
className
)}
{...props}
/>
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

orientation is swallowed instead of forwarded to the primitive.

It's destructured out of props and only reflected as a data-orientation attribute, so Base UI's root never learns the orientation and keyboard navigation stays horizontal for vertical tab sets.

🐛 Proposed fix
     <TabsPrimitive.Root
       data-slot="tabs"
+      orientation={orientation}
       data-orientation={orientation}
📝 Committable suggestion

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

Suggested change
function Tabs({
className,
orientation = "horizontal",
...props
}: TabsPrimitive.Root.Props) {
return (
<TabsPrimitive.Root
data-slot="tabs"
data-orientation={orientation}
className={cn(
"group/tabs flex gap-2 data-horizontal:flex-col",
className
)}
{...props}
/>
)
function Tabs({
className,
orientation = "horizontal",
...props
}: TabsPrimitive.Root.Props) {
return (
<TabsPrimitive.Root
data-slot="tabs"
orientation={orientation}
data-orientation={orientation}
className={cn(
"group/tabs flex gap-2 data-horizontal:flex-col",
className
)}
{...props}
/>
)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/components/ui/tabs.tsx` around lines 8 - 23, Update the Tabs
component’s TabsPrimitive.Root invocation to explicitly forward the destructured
orientation prop, while retaining the existing data-orientation attribute and
styling behavior.

Comment on lines +22 to +34
useEffect(() => {
animate(
"span",
{
opacity: 1,
filter: filter ? "blur(0px)" : "none",
},
{
duration: duration ? duration : 1,
delay: stagger(0.2),
},
);
}, [animate, duration, filter]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reveal effects omit the content prop from their dep arrays, so re-rendered spans stay hidden. Both components render words at opacity-0 and rely on a one-shot animate("span", ...) effect to reveal them; because the words prop isn't a dependency, changing it produces fresh hidden spans that never animate in.

  • packages/web/components/ui/text-generate-effect.tsx#L22-L34: add words to the dep array.
  • packages/web/components/ui/typewriter-effect.tsx#L29-L45: add words and animate to the dep array.
📍 Affects 2 files
  • packages/web/components/ui/text-generate-effect.tsx#L22-L34 (this comment)
  • packages/web/components/ui/typewriter-effect.tsx#L29-L45
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/components/ui/text-generate-effect.tsx` around lines 22 - 34,
Update the reveal effects so content changes re-animate newly rendered hidden
spans: in packages/web/components/ui/text-generate-effect.tsx lines 22-34, add
words to the useEffect dependency array; in
packages/web/components/ui/typewriter-effect.tsx lines 29-45, add both words and
animate to its dependency array.

Comment thread packages/web/lib/site.ts
Comment on lines +6 to +12
export const APP_ORIGIN =
process.env.NEXT_PUBLIC_APP_URL?.replace(/\/$/, "") ??
process.env.APP_URL?.replace(/\/$/, "") ??
"https://bloom-web-amber.vercel.app";

export const INSTALL_UNIX = `curl -fsSL ${APP_ORIGIN}/install.sh | bash`;
export const INSTALL_WINDOWS = `irm ${APP_ORIGIN}/install.ps1 | iex`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Treat empty URL environment variables as unset.

?.replace(...) ?? preserves "", so an empty NEXT_PUBLIC_APP_URL bypasses both APP_URL and the fallback. The landing CTA then emits invalid commands such as curl -fsSL /install.sh | bash.

Proposed fix
+const normalizeOrigin = (value?: string) =>
+  value?.trim().replace(/\/+$/, "") || undefined;
+
 export const APP_ORIGIN =
-  process.env.NEXT_PUBLIC_APP_URL?.replace(/\/$/, "") ??
-  process.env.APP_URL?.replace(/\/$/, "") ??
+  normalizeOrigin(process.env.NEXT_PUBLIC_APP_URL) ??
+  normalizeOrigin(process.env.APP_URL) ??
   "https://bloom-web-amber.vercel.app";
📝 Committable suggestion

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

Suggested change
export const APP_ORIGIN =
process.env.NEXT_PUBLIC_APP_URL?.replace(/\/$/, "") ??
process.env.APP_URL?.replace(/\/$/, "") ??
"https://bloom-web-amber.vercel.app";
export const INSTALL_UNIX = `curl -fsSL ${APP_ORIGIN}/install.sh | bash`;
export const INSTALL_WINDOWS = `irm ${APP_ORIGIN}/install.ps1 | iex`;
const normalizeOrigin = (value?: string) =>
value?.trim().replace(/\/+$/, "") || undefined;
export const APP_ORIGIN =
normalizeOrigin(process.env.NEXT_PUBLIC_APP_URL) ??
normalizeOrigin(process.env.APP_URL) ??
"https://bloom-web-amber.vercel.app";
export const INSTALL_UNIX = `curl -fsSL ${APP_ORIGIN}/install.sh | bash`;
export const INSTALL_WINDOWS = `irm ${APP_ORIGIN}/install.ps1 | iex`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/lib/site.ts` around lines 6 - 12, Update the APP_ORIGIN
environment-variable selection to treat empty values as unset, so an empty
NEXT_PUBLIC_APP_URL falls through to APP_URL and then the default origin.
Preserve trailing-slash removal for non-empty values and keep INSTALL_UNIX and
INSTALL_WINDOWS using the resulting APP_ORIGIN.

Comment thread packages/web/package.json
Comment on lines +16 to +29
"@gsap/react": "^2.1.2",
"better-auth": "^1.6.25",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"gsap": "^3.15.0",
"input-otp": "^1.4.2",
"lucide-react": "^1.26.0",
"motion": "^12.42.2",
"next": "16.2.6",
"next-themes": "^0.4.6",
"react": "19.2.4",
"react-dom": "19.2.4",
"shadcn": "^4.14.1",
"shiki": "^4.3.1",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
for p in gsap `@gsap/react` motion shiki; do
  echo "== $p"
  curl -s "https://registry.npmjs.org/$p" | jq -r '."dist-tags".latest'
done

gh api graphql -f query='
{
  securityVulnerabilities(first: 5, ecosystem: NPM, package: "gsap") {
    nodes { advisory { summary severity } vulnerableVersionRange firstPatchedVersion { identifier } }
  }
}'

Repository: Prince-695/bloom

Length of output: 424


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package references =="
if [ -x "$(command -v fd)" ]; then
  fd -a 'package(-lock)?(\.json|\.mlock|\.pnpm-lock\.yaml|yarn\.lock)$' . | sed 's#^\./##'
else
  find . -maxdepth 5 \( -name 'package.json' -o -name 'package-lock.json' -o -name '*.pnpm-lock.yaml' -o -name 'yarn.lock' \) -print | sed 's#^\./##'
fi

echo
echo "== lockfile/package snippets for gsap/@gsap/react/motion/shiki =="
for f in $(fd 'package-(lock)?\.json|.*-lock\.ya?ml' . | sed 's#^\./##'); do
  case "$f" in
    *node_modules*|*.log) continue ;;
  esac
  echo "-- $f"
  rg -n 'gsap|`@gsap/react`|motion|shiki' "$f" | head -80 || true
done

echo
echo "== npm availability for exact dependency versions =="
node - <<'JS'
const { execSync } = require('child_process');
const pkgs = [
  'gsap@3.15.0',
  '`@gsap/react`@2.1.2',
  'motion@12.42.2',
  'shiki@4.3.1'
];
for (const pkg of pkgs) {
  try {
    const out = execSync(`npm view ${pkg} --json`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
    const json = JSON.parse(out);
    const arr = Array.isArray(json) ? json : [json];
    console.log(`== ${pkg} ==`);
    for (const item of arr) {
      console.log(`${item.name}@${item.version}`);
      console.log(`  dist.tarball=${item.dist?.tarball || ''}`);
      console.log(`  engines=${JSON.stringify(item.engines)}`);
    }
  } catch (e) {
    console.log(`== ${pkg} ==`);
    console.log(e.message);
  }
}
JS

echo
echo "== GitHub Advisory DB advisory search for exact packages/version ranges =="
for pkg in gsap '`@gsap/react`' motion shiki; do
  echo "-- ${pkg}"
  gh api graphql -f query='
query($ecosystem: GHSA_Ecosystem!, $name: [String!]!) {
  securityAdvisories(first: 20, ecosystem: $ecosystem, identities: { names: $name }) {
    nodes { ghsaId summary severity identifiers { type value } firstPatchedVersion { identifier } }
  }
}' -f name="$pkg" -f ecosystem=NPM || true
done

Repository: Prince-695/bloom

Length of output: 8631


Pin gsap to a patched version before using this range.

gsap@3.15.0 exists, but the version range in packages/web/package.json includes vulnerable versions (< 3.6.0) from a known HIGH-severity prototype pollution advisory. Bump to gsap@3.6.0 or later, then regenerate the lockfile.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/package.json` around lines 16 - 29, Update the gsap dependency
in package.json to a minimum version of 3.6.0, preserving the current dependency
declaration style, then regenerate the lockfile so it resolves only patched
versions.

This branch was successfully deployed

1 active deployment
Preview — d7241b63 Deployed Jul 26, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant