Skip to content

feat(website): lamborghini-inspired landing page#164

Merged
rohitg00 merged 3 commits intomainfrom
feat/website-lambo
Apr 18, 2026
Merged

feat(website): lamborghini-inspired landing page#164
rohitg00 merged 3 commits intomainfrom
feat/website-lambo

Conversation

@rohitg00
Copy link
Copy Markdown
Owner

@rohitg00 rohitg00 commented Apr 18, 2026

Single-page marketing site for agentmemory, generated from npx getdesign@latest add lamborghini and built against that brief.

What ships

  • DESIGN.md at repo root — the 288-line design system (black canvas, Lamborghini Gold #FFC000 accent, Neo-Grotesk uppercase display, zero border-radius, hexagonal motifs). Future UI PRs should read this first.
  • website/index.html — hero, stats strip, three primitives, live terminal, vs-the-field comparison, agents grid, install, footer.
  • website/styles.css — black-first palette, Archivo display at 160px hero, JetBrains Mono for the terminal, sharp-edge buttons, reveal-on-intersect.
  • website/script.js — vanilla JS, zero deps.

Interactive elements

Hero memory graph Live canvas network of nodes + edges. Hexagonal pause control toggles it.
Stat counters Animate up on first intersect (95.2 R@5, 92%, 44, 12, 0, 769).
Primitive cards 3D mouse-tilt on hover.
Live terminal Typewriter plays a realistic memory.recall + memory.consolidate session. REPLAY button.
Copy-to-clipboard Install commands one-click, shows COPIED state.
Scroll rail Thin gold progress bar fixed to the top of the viewport.
Scroll reveals Headings, cards, and rows fade in on intersect.

Full prefers-reduced-motion short-circuit — animation-heavy elements resolve instantly when the OS says no.

Run locally

cd website
python3 -m http.server 4000
# or
npx serve .

Deploy

Drop the website/ directory behind any static host — Vercel, Netlify, Cloudflare Pages, GitHub Pages, S3, plain nginx. Three files, no build step.

What's not here yet

  • Social / OG images (design system calls for full-bleed hero photography — the canvas graph covers this for now; a static OG export is a follow-up).
  • Deploy config (vercel.json / netlify.toml) — picking a host is your call.
  • Analytics — none wired.

Summary by CodeRabbit

  • New Features

    • New Next.js 15 static landing site with a Lamborghini‑inspired dark design system and responsive layouts.
    • Numerous interactive sections: animated memory graph, live terminal replay, stats with count-up, comparison grid, agents marquee, install/agent‑wiring UI with copy-to-clipboard, command center, hero, nav/footer, scroll progress, and tilt/3D card effects.
  • Documentation

    • Added DESIGN.md design system and website README with local dev and deployment instructions.

Single-page marketing site for agentmemory generated from the
getdesign@latest 'lamborghini' design system.

- DESIGN.md at repo root: the full 288-line design brief (black canvas,
  gold accent, Neo-Grotesk display, zero border-radius, hexagonal
  motifs). Future UI PRs should read this file first.
- website/index.html: hero + stats + primitives + live terminal +
  competitor comparison + agents grid + install + footer.
- website/styles.css: black-first palette (#000 canvas, #FFC000
  accent, #202020 surface), Archivo display at 160px hero, JetBrains
  Mono for the terminal, sharp-edge buttons, reveal-on-intersect.
- website/script.js: vanilla, zero deps.
  - animated canvas memory graph in the hero with a hexagonal pause
    control
  - stat counters that run on first intersect
  - 3D mouse-tilt on primitive cards
  - typewriter terminal demo of memory.recall and memory.consolidate
    with a REPLAY button
  - click-to-copy install boxes with success feedback
  - top scroll progress rail in gold
  - full prefers-reduced-motion short-circuit

No framework, no build step. Drop behind any static host.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 18, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

A new Next.js 15 website for "agentmemory" was added, including a Lamborghini-inspired DESIGN, global theming, app layout, many interactive components (Nav, Hero, MemoryGraph, Stats, Primitives, Compare, LiveTerminal, CommandCenter, Agents, Install, AgentInstall, Footer, etc.), TypeScript/Next config, and build/deploy docs.

Changes

Cohort / File(s) Summary
Design & Docs
DESIGN.md, website/README.md
Added a Lamborghini-inspired design system and a website README describing stack, local dev, deployment, directory layout, and referencing DESIGN.md as source of truth.
Project Config
website/package.json, website/tsconfig.json, website/next.config.ts, website/next-env.d.ts, website/.gitignore
New Next.js project manifest, strict TypeScript config with path alias, typed Next config (images/CSP/Turbopack), Next env declarations, and Node/Next .gitignore.
App Shell
website/app/layout.tsx, website/app/globals.css, website/app/page.tsx
Root layout with metadata and Google fonts, global dark-theme CSS custom properties/utilities, and main page composing site sections.
Navigation & Mobile
website/components/Nav.tsx, website/components/Nav.module.css, website/components/MobileNavToggle.tsx, website/components/MobileNavToggle.module.css
Header component (async) that fetches GitHub stats, responsive styling, mobile hamburger sheet component with focus/escape/scroll lock behavior.
Hero & Visuals
website/components/Hero.tsx, website/components/Hero.module.css, website/components/MemoryGraph.tsx, website/components/MemoryGraph.module.css
Full-viewport hero with animated title and MemoryGraph canvas visualization (animated nodes, connections, pause/resume, scroll-linked rail, DPR-aware resizing).
Interactive Utilities
website/components/ScrollProgress.tsx, website/components/Stats.tsx, website/components/Stats.module.css, website/components/Primitives.tsx, website/components/Primitives.module.css
Scroll progress bar, animated stat count-ups via IntersectionObserver, and 3D-tilt cards with cursor-driven transforms and reduced-motion respect.
Terminal & Comparison
website/components/LiveTerminal.tsx, website/components/LiveTerminal.module.css, website/components/Compare.tsx, website/components/Compare.module.css
Animated terminal replay (typed segments, caret, replay, reduced-motion support) and responsive comparison grid with ARIA table semantics.
Command & Agent Interfaces
website/components/CommandCenter.tsx, website/components/CommandCenter.module.css, website/components/Install.tsx, website/components/Install.module.css, website/components/AgentInstall.tsx, website/components/AgentInstall.module.css
Command center with tabbed panels, install section with CopyBox clipboard interactions (temporary "COPIED" state/fallback), and AgentInstall with deeplinks, clipboard copy controls, and expandable additional snippets.
Agents & Marquee
website/components/Agents.tsx, website/components/Agents.module.css
Agents featured grid and infinite marquee, per-agent accents, accessible headings, duplicated list for smooth marquee, and responsive card behaviors.
Features & Primitives
website/components/Features.tsx, website/components/Features.module.css, website/components/Primitives.tsx, website/components/Primitives.module.css
Feature tiles grid and primitives/cards styling with hover interactions and responsive collapse.
Footer & Misc
website/components/Footer.tsx, website/components/Footer.module.css, website/lib/format.ts, website/lib/github.ts, website/components/*module.css (various)
Footer with external links and back-to-top; utility formatCompact; server-side fetchRepoStats() calling GitHub API with optional token and revalidation; many CSS modules for components added.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰
I hopped through fonts and canvas light,
Golden accents in the velvet night.
I clicked, I copied, graphs took flight,
A tiny warren built just right,
Bravo — the site gleams bold and bright!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(website): lamborghini-inspired landing page' directly and clearly summarizes the main change: adding a Lamborghini-inspired landing page website with all supporting components, styles, and infrastructure.

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

✨ 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 feat/website-lambo

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

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (2)
website/styles.css (1)

14-15: Stylelint nits: kebab-case keyframe name and lowercase generic/system font keywords.

Optional cleanup to satisfy the project's stylelint config:

-  --font-display: "Archivo", "Helvetica Neue", Arial, sans-serif;
-  --font-mono: "JetBrains Mono", ui-monospace, Menlo, monospace;
+  --font-display: "Archivo", "Helvetica Neue", arial, sans-serif;
+  --font-mono: "JetBrains Mono", ui-monospace, menlo, monospace;
-  animation: slideIn 900ms cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
+  animation: slide-in 900ms cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
...
-@keyframes slideIn {
+@keyframes slide-in {

As per static analysis hints from stylelint (value-keyword-case, keyframes-name-pattern).

Also applies to: 282-287

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

In `@website/styles.css` around lines 14 - 15, Update the CSS to satisfy stylelint
by making the custom properties and keyframe identifiers use the expected
casing: change the generic/system font keywords in --font-display and
--font-mono to lowercase (e.g., "ui-monospace" → "ui-monospace", "sans-serif",
"monospace", "arial" as needed) and rename any keyframes referenced around lines
282-287 to a kebab-case name (e.g., my-animation → my-animation) and update
their `@keyframes` declaration and all usages (animation, animation-name)
accordingly so names and keywords match the project's value-keyword-case and
keyframes-name-pattern rules.
DESIGN.md (1)

177-183: markdownlint MD058: surround tables with blank lines.

Both the border-radius scale table (line ~178) and the breakpoints table (line ~233) are missing a blank line between the preceding heading and the table header. Add one blank line above each table to clear the lint warning.

Also applies to: 232-241

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

In `@DESIGN.md` around lines 177 - 183, The table under the "Border Radius Scale"
heading and the breakpoints table are missing a blank line after their headings;
insert a single blank line between the heading and the table header for the
Border Radius Scale table and likewise for the Breakpoints table (i.e., add an
empty line immediately after the "Border Radius Scale" heading and after the
"Breakpoints" heading) so the markdown linter MD058 no longer flags them.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@DESIGN.md`:
- Around line 118-120: The "Black Filled" button spec entry is incorrect: it
lists bg `#000000` with text `#202020` which yields a near-zero contrast; update
the "Black Filled" entry so the foreground is `#FFFFFF` (or another
WCAG-compliant light color) instead of `#202020`, adjust the description to
match "Dark filled variant" and note its use as an inverted CTA on light
sections (mirroring the "White Filled" entry logic), and ensure the color pair
meets WCAG contrast requirements.

In `@website/index.html`:
- Around line 81-94: The pause button markup currently always renders the
"pause" glyph and aria-label but script.js computes graphRunning =
!REDUCE_MOTION, causing a mismatch when REDUCE_MOTION is true; update the
initial button state to match graphRunning by setting its inner SVG glyph and
aria-label in script after computing graphRunning (select the element by id
"graph-toggle" and the group "pause-icon") or, alternatively, change the static
markup to the "play" glyph and aria-label when reduced motion is detected at
load time; ensure the JS that toggles state still updates both the visual glyph
and aria-label consistently.

In `@website/script.js`:
- Around line 285-299: The 3D tilt on elements selected by tiltCards is
collapsing because no CSS perspective is defined; either add a parent-level
perspective (e.g., set perspective:1000px on the grid container like
.primitives__grid) and optionally perspective-origin, or set transform-style:
preserve-3d on the card element (.prim-card) with the perspective on its parent,
or include a perspective(...) call in the inline transform built in the
mousemove handler (the function that sets card.style.transform). Update the
stylesheet or the mousemove transform logic referencing tiltCards / .prim-card /
.primitives__grid accordingly so rotateX/rotateY produce visible depth.
- Around line 238-282: playTerminal can be started concurrently (via replayBtn
click or IntersectionObserver) which races on the shared DOM (term) and can
throw NotFoundError; serialize runs by adding a run guard/generation token:
introduce a module-scoped variable (e.g., currentPlayToken or isPlaying) that
playTerminal checks at start and updates atomically, return early if a run is
active, and always clear/reset the token in a finally block so subsequent runs
can start; update the replayBtn click handler and the IntersectionObserver block
to respect this token (set term.dataset.played only after a successful run or
use a separate dataset flag) so multiple triggers don’t start overlapping
playTerminal executions and ensure termStatus is set to "DONE" or reset in the
finally path.
- Around line 302-318: The click handler attached in
document.querySelectorAll(".copybox") captures hint.textContent per-click which
lets a second click inside the 1600ms window save "COPIED" as original and
causes the label to stick, and the catch branch writes "CLIPBOARD BLOCKED"
without ever restoring state; fix by storing the original hint text once (e.g.,
read hint.dataset.original or store it on the button element the first time) and
use that known original for restores, attach/clear a per-button timeout handle
(store on btn._copyTimeout or btn.dataset) so rapid clicks clear previous
timeouts before setting a new one, and in the catch branch reset the hint text
and remove the "is-copied" class (and clear any existing timeout) so a failed
copy does not leave the UI stuck.

In `@website/styles.css`:
- Around line 530-543: The .terminal__body rule currently uses overflow: hidden
which can clip the typewriter output emitted by TERM_SCRIPT (see
memory.consolidate final lines); change the CSS to allow scrolling or growth —
replace overflow: hidden with overflow: auto (to show a subtle scrollbar) or
remove the fixed min-height so the container can expand; update the
.terminal__body selector in styles.css (and ensure any JS that measures the
terminal height in TERM_SCRIPT still behaves correctly after this change).

---

Nitpick comments:
In `@DESIGN.md`:
- Around line 177-183: The table under the "Border Radius Scale" heading and the
breakpoints table are missing a blank line after their headings; insert a single
blank line between the heading and the table header for the Border Radius Scale
table and likewise for the Breakpoints table (i.e., add an empty line
immediately after the "Border Radius Scale" heading and after the "Breakpoints"
heading) so the markdown linter MD058 no longer flags them.

In `@website/styles.css`:
- Around line 14-15: Update the CSS to satisfy stylelint by making the custom
properties and keyframe identifiers use the expected casing: change the
generic/system font keywords in --font-display and --font-mono to lowercase
(e.g., "ui-monospace" → "ui-monospace", "sans-serif", "monospace", "arial" as
needed) and rename any keyframes referenced around lines 282-287 to a kebab-case
name (e.g., my-animation → my-animation) and update their `@keyframes` declaration
and all usages (animation, animation-name) accordingly so names and keywords
match the project's value-keyword-case and keyframes-name-pattern rules.
🪄 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

Run ID: 29f8886b-fae1-406c-aaec-94bc455015cd

📥 Commits

Reviewing files that changed from the base of the PR and between 018e2e7 and 8db6da4.

📒 Files selected for processing (5)
  • DESIGN.md
  • website/README.md
  • website/index.html
  • website/script.js
  • website/styles.css

Comment thread DESIGN.md
Comment on lines +118 to +120
**Black Filled** — Dark filled variant:
- Default: bg `#000000`, text `#202020`
- Used for: Inverted CTA on light sections
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

"Black Filled" button spec produces 1.0:1 contrast — almost certainly wrong.

bg #000000 with `text `#202020 is effectively invisible and would fail WCAG badly. The neighboring "White Filled" uses text #202020`` on a white bg; this entry looks like it was copy-pasted without flipping the foreground to white. Suggest:

📝 Proposed wording fix
 **Black Filled** — Dark filled variant:
-- Default: bg `#000000`, text `#202020`
+- Default: bg `#000000`, text `#FFFFFF`
 - Used for: Inverted CTA on light sections
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@DESIGN.md` around lines 118 - 120, The "Black Filled" button spec entry is
incorrect: it lists bg `#000000` with text `#202020` which yields a near-zero
contrast; update the "Black Filled" entry so the foreground is `#FFFFFF` (or
another WCAG-compliant light color) instead of `#202020`, adjust the description
to match "Dark filled variant" and note its use as an inverted CTA on light
sections (mirroring the "White Filled" entry logic), and ensure the color pair
meets WCAG contrast requirements.

Comment thread website/index.html Outdated
Comment on lines +81 to +94
<button class="hero__pause" id="graph-toggle" aria-label="Pause animation">
<svg viewBox="0 0 48 48" width="44" height="44" aria-hidden="true">
<polygon
points="24,2 44,13 44,35 24,46 4,35 4,13"
fill="none"
stroke="#fff"
stroke-width="1.8"
/>
<g id="pause-icon">
<rect x="17" y="16" width="4" height="16" fill="#fff" />
<rect x="27" y="16" width="4" height="16" fill="#fff" />
</g>
</svg>
</button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Initial pause-button state can be inconsistent under prefers-reduced-motion.

script.js sets graphRunning = !REDUCE_MOTION, so under reduced-motion the graph starts paused, but this button's initial icon renders the pause bars and its aria-label is "Pause animation". A user who then clicks it sees the icon flip to a play triangle and the label change to "Resume animation" — the opposite of the natural "click to start". Consider rendering the initial SVG glyph and aria-label from JS after computing graphRunning, or hard-coding the play triangle when reduced-motion is the expected default-off path.

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

In `@website/index.html` around lines 81 - 94, The pause button markup currently
always renders the "pause" glyph and aria-label but script.js computes
graphRunning = !REDUCE_MOTION, causing a mismatch when REDUCE_MOTION is true;
update the initial button state to match graphRunning by setting its inner SVG
glyph and aria-label in script after computing graphRunning (select the element
by id "graph-toggle" and the group "pause-icon") or, alternatively, change the
static markup to the "play" glyph and aria-label when reduced motion is detected
at load time; ensure the JS that toggles state still updates both the visual
glyph and aria-label consistently.

Comment thread website/script.js Outdated
Comment thread website/script.js Outdated
Comment on lines +285 to +299
const tiltCards = document.querySelectorAll("[data-tilt]");
tiltCards.forEach((card) => {
if (REDUCE_MOTION) return;
card.addEventListener("mousemove", (e) => {
const rect = card.getBoundingClientRect();
const px = (e.clientX - rect.left) / rect.width - 0.5;
const py = (e.clientY - rect.top) / rect.height - 0.5;
card.style.transform = `translateY(-4px) rotateX(${(-py * 4).toFixed(
2
)}deg) rotateY(${(px * 4).toFixed(2)}deg)`;
});
card.addEventListener("mouseleave", () => {
card.style.transform = "";
});
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

3D tilt needs perspective on the parent — current transform collapses to a near-flat skew.

rotateX/rotateY only produce visible 3D depth when a perspective is set on the element or an ancestor. Neither .primitives__grid nor .prim-card declares perspective / perspective-origin in styles.css, so the (px * 4)deg rotation will render as a mild skew with no depth cue. Either add perspective: 1000px to .primitives__grid (or transform-style: preserve-3d on .prim-card plus perspective on its parent), or bake perspective into the inline transform:

-    card.style.transform = `translateY(-4px) rotateX(${(-py * 4).toFixed(
-      2
-    )}deg) rotateY(${(px * 4).toFixed(2)}deg)`;
+    card.style.transform = `perspective(900px) translateY(-4px) rotateX(${(-py * 4).toFixed(2)}deg) rotateY(${(px * 4).toFixed(2)}deg)`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/script.js` around lines 285 - 299, The 3D tilt on elements selected
by tiltCards is collapsing because no CSS perspective is defined; either add a
parent-level perspective (e.g., set perspective:1000px on the grid container
like .primitives__grid) and optionally perspective-origin, or set
transform-style: preserve-3d on the card element (.prim-card) with the
perspective on its parent, or include a perspective(...) call in the inline
transform built in the mousemove handler (the function that sets
card.style.transform). Update the stylesheet or the mousemove transform logic
referencing tiltCards / .prim-card / .primitives__grid accordingly so
rotateX/rotateY produce visible depth.

Comment thread website/script.js Outdated
Comment on lines +302 to +318
document.querySelectorAll(".copybox").forEach((btn) => {
btn.addEventListener("click", async () => {
const cmd = btn.dataset.copy || "";
try {
await navigator.clipboard.writeText(cmd);
const hint = btn.querySelector(".copybox__hint");
const original = hint.textContent;
hint.textContent = "COPIED";
btn.classList.add("is-copied");
setTimeout(() => {
hint.textContent = original;
btn.classList.remove("is-copied");
}, 1600);
} catch {
btn.querySelector(".copybox__hint").textContent = "CLIPBOARD BLOCKED";
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Copy handler leaves "COPIED" stuck on rapid double-clicks and never clears "CLIPBOARD BLOCKED".

Two small but user-visible defects in the same handler:

  1. const original = hint.textContent captures whatever the hint currently shows. A second click that lands inside the 1600ms window captures "COPIED" as original; its own timeout then restores to "COPIED", and the hint is stuck until the next click. Capture the original label once (e.g. from data-* or the first run) or reset to a known string.
  2. The catch branch writes "CLIPBOARD BLOCKED" but never restores the label or the is-copied state, so any subsequent failed page state is permanent.
🛠️ Suggested fix
 document.querySelectorAll(".copybox").forEach((btn) => {
+  const hint = btn.querySelector(".copybox__hint");
+  const original = hint ? hint.textContent : "";
+  let resetTimer;
   btn.addEventListener("click", async () => {
     const cmd = btn.dataset.copy || "";
+    clearTimeout(resetTimer);
     try {
       await navigator.clipboard.writeText(cmd);
-      const hint = btn.querySelector(".copybox__hint");
-      const original = hint.textContent;
-      hint.textContent = "COPIED";
+      if (hint) hint.textContent = "COPIED";
       btn.classList.add("is-copied");
-      setTimeout(() => {
-        hint.textContent = original;
-        btn.classList.remove("is-copied");
-      }, 1600);
     } catch {
-      btn.querySelector(".copybox__hint").textContent = "CLIPBOARD BLOCKED";
+      if (hint) hint.textContent = "CLIPBOARD BLOCKED";
     }
+    resetTimer = setTimeout(() => {
+      if (hint) hint.textContent = original;
+      btn.classList.remove("is-copied");
+    }, 1600);
   });
 });
📝 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
document.querySelectorAll(".copybox").forEach((btn) => {
btn.addEventListener("click", async () => {
const cmd = btn.dataset.copy || "";
try {
await navigator.clipboard.writeText(cmd);
const hint = btn.querySelector(".copybox__hint");
const original = hint.textContent;
hint.textContent = "COPIED";
btn.classList.add("is-copied");
setTimeout(() => {
hint.textContent = original;
btn.classList.remove("is-copied");
}, 1600);
} catch {
btn.querySelector(".copybox__hint").textContent = "CLIPBOARD BLOCKED";
}
});
document.querySelectorAll(".copybox").forEach((btn) => {
const hint = btn.querySelector(".copybox__hint");
const original = hint ? hint.textContent : "";
let resetTimer;
btn.addEventListener("click", async () => {
const cmd = btn.dataset.copy || "";
clearTimeout(resetTimer);
try {
await navigator.clipboard.writeText(cmd);
if (hint) hint.textContent = "COPIED";
btn.classList.add("is-copied");
} catch {
if (hint) hint.textContent = "CLIPBOARD BLOCKED";
}
resetTimer = setTimeout(() => {
if (hint) hint.textContent = original;
btn.classList.remove("is-copied");
}, 1600);
});
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/script.js` around lines 302 - 318, The click handler attached in
document.querySelectorAll(".copybox") captures hint.textContent per-click which
lets a second click inside the 1600ms window save "COPIED" as original and
causes the label to stick, and the catch branch writes "CLIPBOARD BLOCKED"
without ever restoring state; fix by storing the original hint text once (e.g.,
read hint.dataset.original or store it on the button element the first time) and
use that known original for restores, attach/clear a per-button timeout handle
(store on btn._copyTimeout or btn.dataset) so rapid clicks clear previous
timeouts before setting a new one, and in the catch branch reset the hint text
and remove the "is-copied" class (and clear any existing timeout) so a failed
copy does not leave the UI stuck.

Comment thread website/styles.css Outdated
Comment on lines +530 to +543
.terminal__body {
margin: 0;
padding: 24px 24px 16px;
background: var(--abyss);
border: 1px solid var(--charcoal);
border-top: none;
font-family: var(--font-mono);
font-size: 13.5px;
line-height: 1.7;
color: var(--mist);
white-space: pre-wrap;
min-height: 360px;
overflow: hidden;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

overflow: hidden on .terminal__body can clip the typewriter output.

The terminal script in script.js (TERM_SCRIPT, lines 198–217) emits ~15 lines at line-height: 1.7. At 13.5px that is ≈350px — right at the min-height: 360px edge — and on viewports where the font renders slightly larger, or if segments are ever appended, overflow: hidden silently cuts off the final memory.consolidate lines (which include the ✓ success line). Consider overflow: auto with a subtle scrollbar, or removing the explicit min-height and letting the container grow.

🛠️ Suggested change
   white-space: pre-wrap;
-  min-height: 360px;
-  overflow: hidden;
+  min-height: 360px;
+  overflow: auto;
📝 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
.terminal__body {
margin: 0;
padding: 24px 24px 16px;
background: var(--abyss);
border: 1px solid var(--charcoal);
border-top: none;
font-family: var(--font-mono);
font-size: 13.5px;
line-height: 1.7;
color: var(--mist);
white-space: pre-wrap;
min-height: 360px;
overflow: hidden;
}
.terminal__body {
margin: 0;
padding: 24px 24px 16px;
background: var(--abyss);
border: 1px solid var(--charcoal);
border-top: none;
font-family: var(--font-mono);
font-size: 13.5px;
line-height: 1.7;
color: var(--mist);
white-space: pre-wrap;
min-height: 360px;
overflow: auto;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/styles.css` around lines 530 - 543, The .terminal__body rule
currently uses overflow: hidden which can clip the typewriter output emitted by
TERM_SCRIPT (see memory.consolidate final lines); change the CSS to allow
scrolling or growth — replace overflow: hidden with overflow: auto (to show a
subtle scrollbar) or remove the fixed min-height so the container can expand;
update the .terminal__body selector in styles.css (and ensure any JS that
measures the terminal height in TERM_SCRIPT still behaves correctly after this
change).

Replaces the vanilla HTML/CSS/JS landing with a production Next.js 16.2
app router build. Deploy target is Vercel; root directory should be
set to 'website/' in the Vercel project settings.

Stack:
- Next.js 16.2 + React 19 + TypeScript 5.7
- next/font for Archivo + JetBrains Mono (no external fetch)
- CSS Modules per component + one globals.css for tokens
- No Tailwind, no bundler config, no client-side routing

Component layout:
- app/layout.tsx           — <html>, font vars, metadata, viewport
- app/page.tsx             — composes all landing sections
- app/globals.css          — design tokens + shared utilities
- components/Nav.tsx       — hexagonal bull mark + menu
- components/Hero.tsx      — title, lede, CTAs
- components/MemoryGraph.tsx   — client canvas animation + hex pause
- components/ScrollProgress.tsx — fixed top gold progress rail
- components/Stats.tsx     — counter-up on first intersect
- components/Primitives.tsx — three cards with 3D mouse tilt
- components/LiveTerminal.tsx  — typewriter memory.recall demo
- components/Compare.tsx   — vs Mem0/Letta/Cognee table
- components/Agents.tsx    — supported-agents grid
- components/Install.tsx   — click-to-copy npm + console boxes
- components/Footer.tsx    — source / changelog / license links

Interactive elements are "use client" islands; static sections render
on the server. prefers-reduced-motion gated throughout.

Upgraded Next from 15.1.3 to 16.2.4 to clear CVE-2025-66478 during
setup. next build completes clean, all 3 routes prerender static.

Deploy:
  1. Import repo on Vercel, set Root Directory = website/
  2. Or: cd website && npx vercel
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🧹 Nitpick comments (6)
website/components/Footer.tsx (1)

11-34: Consider adding noreferrer to external link rel attributes.

External links use rel="noopener" only. Adding noreferrer also strips the Referer header, which is the common hardened default (rel="noopener noreferrer"). Modern browsers set noopener implicitly for target="_blank", so this is a minor hardening/consistency nit.

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

In `@website/components/Footer.tsx` around lines 11 - 34, In the Footer component
(website/components/Footer.tsx) the anchor elements currently set rel="noopener"
for external links; update each <a> with target="_blank" to use rel="noopener
noreferrer" (or add "noreferrer") so the Referer header is stripped and the
links follow the hardened default; ensure all four external anchors (SOURCE,
CHANGELOG, RUNS ON iii, APACHE-2.0) are updated.
website/components/Primitives.tsx (1)

30-60: Optional: react to runtime prefers-reduced-motion changes.

The reduced-motion check only runs once on mount. If the user toggles their OS preference while the page is open, tilt state won't update until remount. Low priority for a marketing page, but trivial to address by listening to the media query's change event and re-running setup/teardown.

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

In `@website/components/Primitives.tsx` around lines 30 - 60, The effect in
useEffect currently reads prefers-reduced-motion only on mount so toggling the
OS setting doesn't update the tilt behavior; change the setup to store the
MediaQueryList (const mq = matchMedia("(prefers-reduced-motion: reduce)")), move
the card listener setup/teardown into a function (referencing gridRef, cards,
onMove, onLeave and handlers) and add mq.addEventListener('change', ...) (or
mq.addListener for older browsers) to re-run the setup/cleanup when mq.matches
changes; ensure the effect's cleanup removes the card listeners and also removes
the mq change listener so handlers and event listeners are properly removed.
website/components/Compare.tsx (1)

24-42: ARIA table structure is missing rowgroup wrappers and an accessible name strategy for the empty corner header.

A couple of small a11y polish items on the hand-rolled table:

  1. When using role="table" with role="row" children, assistive tech expects role="rowgroup" around the header row(s) and the body rows; without them, some screen readers announce the structure inconsistently.
  2. The empty <span role="columnheader" /> on Line 26 has no accessible name, which can produce an empty announcement. Give it a visually-hidden label (e.g., aria-label="Metric").
  3. Minor: <section> with aria-labelledby is good, but the outer role="table" div could also benefit from aria-describedby pointing at the lede if you want the context announced.
♻️ Proposed tweak
-      <div className={styles.table} role="table" aria-label="Comparison">
-        <div className={`${styles.row} ${styles.head}`} role="row">
-          <span role="columnheader" />
+      <div className={styles.table} role="table" aria-label="Comparison">
+        <div role="rowgroup">
+        <div className={`${styles.row} ${styles.head}`} role="row">
+          <span role="columnheader" aria-label="Metric" />
           <span role="columnheader" className={styles.mine}>
             AGENTMEMORY
           </span>
           <span role="columnheader">MEM0</span>
           <span role="columnheader">LETTA</span>
           <span role="columnheader">COGNEE</span>
         </div>
+        </div>
+        <div role="rowgroup">
         {ROWS.map((r) => (
           <div key={r[0]} className={styles.row} role="row">
             <span role="rowheader">{r[0]}</span>
             <span className={styles.mine}>{r[1]}</span>
             <span>{r[2]}</span>
             <span>{r[3]}</span>
             <span>{r[4]}</span>
           </div>
         ))}
+        </div>
       </div>

Alternatively, just use a native <table>/<thead>/<tbody>/<tr>/<th>/<td> — styled with CSS grid via display: grid on the rows — and drop all the ARIA roles. That's usually the more robust choice.

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

In `@website/components/Compare.tsx` around lines 24 - 42, Wrap the header row div
(className={`${styles.row} ${styles.head}`} role="row") in a container div with
role="rowgroup", and wrap the mapped body rows (the ROWS.map(...) output) in a
separate div with role="rowgroup" so the div with role="table"
(className={styles.table}) has explicit header and body rowgroups; give the
empty corner header span (the first <span role="columnheader" />) an accessible
name by adding an aria-label like aria-label="Metric" (or a visually-hidden
label) and, if you have the lede element, add aria-describedby on the
role="table" div pointing to that lede's id to provide context.
website/components/MemoryGraph.tsx (2)

124-136: Scroll-rail duplicates ScrollProgress.

Per the PR summary and ScrollProgress.tsx, a top scroll rail already exists and listens to scroll/resize. This component adds a second, identical scroll listener to drive railRef. If the hero-local rail is intentionally separate from the global one, this is fine; otherwise consider removing this block and leaving scroll progress to ScrollProgress. Adding { passive: true } to resize is also advisable for consistency with the scroll listener.

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

In `@website/components/MemoryGraph.tsx` around lines 124 - 136, The component
duplicates global scroll handling by adding a local updateRail listener that
updates railRef (function updateRail and ref railRef) whereas ScrollProgress.tsx
already manages top scroll progress; either remove the local updateRail/scroll
listener/railRef updates and rely on ScrollProgress, or clearly separate
responsibilities (e.g., rename and isolate hero-local rail and ensure only one
listener drives it). If you keep the local listener, add the same options to the
resize removal/addition (use { passive: true } when adding the resize listener
to match scroll) and ensure cleanup still cancels rafId and removes both resize
and scroll handlers (onResize and updateRail) to avoid duplicate listeners and
memory leaks.

20-139: Effect re-runs entirely on every pause/resume toggle.

Depending on running causes the full effect to tear down and re-run on each click: canvas is re-sized, nodes are re-seeded (visuals "jump"), and scroll/resize listeners are re-attached. Prefer keeping the effect mounted once and driving start/stop through a ref.

🛠️ Sketch of the refactor
-  useEffect(() => {
-    ...
-    let localRunning = running && !reduceMotion;
+  const runningRef = useRef(running);
+  useEffect(() => { runningRef.current = running; }, [running]);
+
+  useEffect(() => {
@@
-    const tick = () => {
-      if (!localRunning) return;
-      draw();
-      rafId = requestAnimationFrame(tick);
-    };
+    const tick = () => {
+      if (runningRef.current && !reduceMotion) draw();
+      rafId = requestAnimationFrame(tick);
+    };
@@
-  }, [running]);
+  }, []);

This also preserves node positions across pause/resume and avoids re-seeding on each toggle.

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

In `@website/components/MemoryGraph.tsx` around lines 20 - 139, The effect
currently depends on the prop/state running causing the entire useEffect (size,
seed, draw, event listeners) to teardown and re-run on each pause/resume;
instead keep the effect mounted once and control animation with a mutable ref:
create a runningRef (useRef<boolean>) and replace the localRunning boolean with
runningRef.current inside tick and where animation starts/stops (rafId,
requestAnimationFrame, cancelAnimationFrame), remove running from the useEffect
dependency array, and add a small separate useEffect or callback that updates
runningRef.current when the running prop changes (and starts or stops the
animation loop by requesting/canceling raf using rafId logic); keep seed() and
size() called only on mount/resize so nodes are preserved across pause/resume
and do not re-attach listeners in that update.
website/components/Install.tsx (1)

66-81: Add noreferrer to external links.

rel="noopener" alone protects against reverse-tabnabbing on older browsers, but noreferrer is the common paired hardening (and also strips the Referer header). Same applies to the external link in website/components/Nav.tsx line 36.

-          rel="noopener"
+          rel="noopener noreferrer"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/components/Install.tsx` around lines 66 - 81, Update the external
anchor elements to include rel="noopener noreferrer": in
website/components/Install.tsx, modify the two <a> tags with hrefs
"https://github.com/rohitg00/agentmemory#quick-start" and
"https://www.npmjs.com/package/@agentmemory/agentmemory" (look for the elements
with className "btn btn--accent" and "btn btn--ghost") to add "noreferrer"
alongside "noopener"; also apply the same change to the external link in
website/components/Nav.tsx (the anchor referenced in the review) so all external
links use rel="noopener noreferrer".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@website/app/layout.tsx`:
- Around line 24-36: The Open Graph/Twitter metadata currently sets twitter.card
to "summary_large_image" without providing image assets; update the metadata in
the layout (openGraph and twitter objects) to either add openGraph.images and
twitter.images with the image metadata or change twitter.card to a plain summary
(e.g., "summary") until the social/OG image assets are added so previews won't
be downgraded; modify the openGraph and twitter entries in
website/app/layout.tsx (look for the openGraph and twitter objects and the
"summary_large_image" value) accordingly.

In `@website/components/Compare.tsx`:
- Around line 3-9: The ROWS constant in Compare.tsx currently hardcodes
competitor metrics (e.g., "RETRIEVAL R@5", "AUTO-HOOKS", "MCP TOOLS") which can
be challenged; instead, replace this static array with a versioned data source
(e.g., a JSON file or API) that includes per-cell citations and a
lastVerified/asOf field, and update the Compare component to read that data and
render citation links or a footnote; specifically locate the ROWS constant and
move its values into a new payload that includes {label, values[], sources[],
lastVerified} and render the lastVerified date in the UI so claims are traceable
and maintainable.

In `@website/components/Hero.module.css`:
- Around line 61-79: The .word class currently sets opacity:0 and
transform:translateY(40px) then relies on `@keyframes` slideIn to animate to
visible, which breaks for users with prefers-reduced-motion; update the CSS so
that when prefers-reduced-motion: reduce the .word initial state is the final
state (opacity:1 and transform: translateY(0)) and the animation is disabled
(remove animation property) for that media query, and also rename the `@keyframes`
from slideIn to slide-in (and update the .word animation reference) to satisfy
stylelint's keyframes-name-pattern.

In `@website/components/Install.tsx`:
- Around line 17-33: The CopyBox component never clears the timeout, causing
state updates after unmounts and stacked timers on rapid clicks; fix it by
storing the timeout id in a ref (e.g., timeoutRef) inside the CopyBox component,
clear any existing timeoutRef.current before scheduling a new setTimeout in
onClick, and add a useEffect cleanup that clears timeoutRef.current on unmount;
keep the existing setCopied and setLabel behavior but ensure the timeout is
cancelled to avoid stale state updates.

In `@website/components/LiveTerminal.tsx`:
- Around line 132-134: The animated terminal output in LiveTerminal is invisible
to assistive tech because the <code> element mutated via termRef has no
accessible live region; update the render so the interactive output is
exposed—add aria-live="polite" (and optionally aria-atomic="false") to the <pre>
or <code> that contains the terminal (where termRef is attached), and/or add a
visually-hidden transcript element that mirrors the SCRIPT text (the
memory.recall / memory.consolidate outputs) and update that transcript whenever
the terminal is updated so screen readers receive the same content as the
animated terminal.
- Around line 63-115: The playback routine play() lacks cancellation, causing
async timeouts to continue after unmount or when REPLAY is clicked; thread an
AbortController or a runIdRef through play() and all await points, aborting the
active run from the useEffect cleanup and from the REPLAY handler. Concretely:
create a runIdRef or AbortController per invocation inside play(), check
signal/compare id between each character/segment and immediately stop mutating
DOM or calling setStatus if aborted; in the useEffect cleanup call
controller.abort() or bump runIdRef to cancel in-flight play, and modify the
REPLAY handler to abort the current run before starting a new one and provide
immediate feedback by resetting runningRef/played accordingly. Ensure you
reference and update runningRef, played, termRef, and setStatus consistently so
aborted runs do not touch a detached DOM or set state on unmounted components.

In `@website/components/MemoryGraph.tsx`:
- Around line 60-65: The bounce logic in MemoryGraph iterates over nodes and
flips velocities when n.x < 0 || n.x > w (and similarly for y) which can leave
nodes positioned outside the canvas after a resize or corner velocity boost;
update the loop that updates nodes (the block modifying n.x, n.y, n.vx, n.vy) to
both flip the velocity AND clamp n.x/n.y back into the [0, w] / [0, h] bounds
(so after flipping set n.x = Math.max(0, Math.min(w, n.x)) and similarly for
n.y), and ensure this change is applied alongside existing bounce checks and is
compatible with seed() / onResize() behavior.

In `@website/components/Nav.tsx`:
- Around line 6-11: The MENU button is a dead affordance; add interactive
behavior or remove its button role. Implement a boolean state (e.g., isMenuOpen)
and a toggler function (e.g., toggleMenu) in Nav.tsx, give the element an
onClick={toggleMenu}, aria-expanded={isMenuOpen}, and aria-controls="{menuId}"
(where menuId points at the menu drawer element), and ensure the menu drawer
component opens/closes when isMenuOpen changes; alternatively, if you intend it
to be decorative, replace the <button className={styles.menu} aria-label="Menu">
with a non-interactive element (e.g., <div> or <span>) and remove ARIA
attributes so it’s not presented as a control. Ensure the refs/IDs used match
(menuId) and update any keyboard handlers (Enter/Space) to support accessibility
if keeping it interactive.

In `@website/components/ScrollProgress.tsx`:
- Line 12: The scroll percentage calculation in ScrollProgress computes pct
using Math.min(1, h.scrollTop / max) which still allows negative values during
overscroll; update the pct calculation (the pct variable where h.scrollTop / max
is used) to clamp both bounds by applying Math.max(0, Math.min(1, h.scrollTop /
max)) (or an equivalent clamp helper) so pct never goes below 0 or above 1.

In `@website/components/Stats.tsx`:
- Around line 25-50: The count-up currently leaves elements showing "0" and
always animates; update the useEffect/count logic so each element's textContent
is initialized to its final value from el.dataset.target (and suffix/float) so
static/SSR and no-JS users see truthful numbers, then only run
requestAnimationFrame animation when JavaScript runs and motion is allowed;
detect reduced motion with window.matchMedia('(prefers-reduced-motion:
reduce)').matches and, if true, skip the animation and leave the final formatted
value (use rootRef, useEffect, and the count function, reading
el.dataset.target, el.dataset.suffix, and el.dataset.float to format).

In `@website/README.md`:
- Around line 3-5: Replace the phrase "Deploys to Vercel with zero config" with
"Deploys to Vercel with minimal config" (or similar wording) to avoid implying
no setup is required; update all other occurrences of the exact string ("Deploys
to Vercel with zero config") elsewhere in the README so wording is consistent
(the diff shows another instance around lines 26-27), and ensure any surrounding
copy briefly notes the Root Directory or minimal repo-level setting if
necessary.
- Line 8: The README currently states "Next.js 15.1" but website/package.json
lists Next.js version "16.2.4"; update the README entry string to match the
package.json version (replace "Next.js 15.1" with "Next.js 16.2.4") and, if you
prefer a more future-proof approach, reference the Next.js version from
package.json or note the minimum supported version instead of a hard-coded
value; ensure the README's heading/line that contains "Next.js 15.1" is the one
you change so both files stay in sync.
- Around line 33-54: The fenced directory-structure block in README.md (the
triple-backtick block showing "website/" and the listed files like
app/layout.tsx, page.tsx, components/Nav.tsx, etc.) lacks a language identifier
and triggers markdownlint MD040; update the opening fence from ``` to ```text
(or another appropriate language like ```dos or ```bash) so the block is
explicitly marked, leaving the inner content unchanged.

---

Nitpick comments:
In `@website/components/Compare.tsx`:
- Around line 24-42: Wrap the header row div (className={`${styles.row}
${styles.head}`} role="row") in a container div with role="rowgroup", and wrap
the mapped body rows (the ROWS.map(...) output) in a separate div with
role="rowgroup" so the div with role="table" (className={styles.table}) has
explicit header and body rowgroups; give the empty corner header span (the first
<span role="columnheader" />) an accessible name by adding an aria-label like
aria-label="Metric" (or a visually-hidden label) and, if you have the lede
element, add aria-describedby on the role="table" div pointing to that lede's id
to provide context.

In `@website/components/Footer.tsx`:
- Around line 11-34: In the Footer component (website/components/Footer.tsx) the
anchor elements currently set rel="noopener" for external links; update each <a>
with target="_blank" to use rel="noopener noreferrer" (or add "noreferrer") so
the Referer header is stripped and the links follow the hardened default; ensure
all four external anchors (SOURCE, CHANGELOG, RUNS ON iii, APACHE-2.0) are
updated.

In `@website/components/Install.tsx`:
- Around line 66-81: Update the external anchor elements to include
rel="noopener noreferrer": in website/components/Install.tsx, modify the two <a>
tags with hrefs "https://github.com/rohitg00/agentmemory#quick-start" and
"https://www.npmjs.com/package/@agentmemory/agentmemory" (look for the elements
with className "btn btn--accent" and "btn btn--ghost") to add "noreferrer"
alongside "noopener"; also apply the same change to the external link in
website/components/Nav.tsx (the anchor referenced in the review) so all external
links use rel="noopener noreferrer".

In `@website/components/MemoryGraph.tsx`:
- Around line 124-136: The component duplicates global scroll handling by adding
a local updateRail listener that updates railRef (function updateRail and ref
railRef) whereas ScrollProgress.tsx already manages top scroll progress; either
remove the local updateRail/scroll listener/railRef updates and rely on
ScrollProgress, or clearly separate responsibilities (e.g., rename and isolate
hero-local rail and ensure only one listener drives it). If you keep the local
listener, add the same options to the resize removal/addition (use { passive:
true } when adding the resize listener to match scroll) and ensure cleanup still
cancels rafId and removes both resize and scroll handlers (onResize and
updateRail) to avoid duplicate listeners and memory leaks.
- Around line 20-139: The effect currently depends on the prop/state running
causing the entire useEffect (size, seed, draw, event listeners) to teardown and
re-run on each pause/resume; instead keep the effect mounted once and control
animation with a mutable ref: create a runningRef (useRef<boolean>) and replace
the localRunning boolean with runningRef.current inside tick and where animation
starts/stops (rafId, requestAnimationFrame, cancelAnimationFrame), remove
running from the useEffect dependency array, and add a small separate useEffect
or callback that updates runningRef.current when the running prop changes (and
starts or stops the animation loop by requesting/canceling raf using rafId
logic); keep seed() and size() called only on mount/resize so nodes are
preserved across pause/resume and do not re-attach listeners in that update.

In `@website/components/Primitives.tsx`:
- Around line 30-60: The effect in useEffect currently reads
prefers-reduced-motion only on mount so toggling the OS setting doesn't update
the tilt behavior; change the setup to store the MediaQueryList (const mq =
matchMedia("(prefers-reduced-motion: reduce)")), move the card listener
setup/teardown into a function (referencing gridRef, cards, onMove, onLeave and
handlers) and add mq.addEventListener('change', ...) (or mq.addListener for
older browsers) to re-run the setup/cleanup when mq.matches changes; ensure the
effect's cleanup removes the card listeners and also removes the mq change
listener so handlers and event listeners are properly removed.
🪄 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

Run ID: 0e882ff1-1886-4805-b299-141d8971d4fa

📥 Commits

Reviewing files that changed from the base of the PR and between 8db6da4 and 7193361.

⛔ Files ignored due to path filters (1)
  • website/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (30)
  • website/.gitignore
  • website/README.md
  • website/app/globals.css
  • website/app/layout.tsx
  • website/app/page.tsx
  • website/components/Agents.module.css
  • website/components/Agents.tsx
  • website/components/Compare.module.css
  • website/components/Compare.tsx
  • website/components/Footer.module.css
  • website/components/Footer.tsx
  • website/components/Hero.module.css
  • website/components/Hero.tsx
  • website/components/Install.module.css
  • website/components/Install.tsx
  • website/components/LiveTerminal.module.css
  • website/components/LiveTerminal.tsx
  • website/components/MemoryGraph.module.css
  • website/components/MemoryGraph.tsx
  • website/components/Nav.module.css
  • website/components/Nav.tsx
  • website/components/Primitives.module.css
  • website/components/Primitives.tsx
  • website/components/ScrollProgress.tsx
  • website/components/Stats.module.css
  • website/components/Stats.tsx
  • website/next-env.d.ts
  • website/next.config.ts
  • website/package.json
  • website/tsconfig.json
✅ Files skipped from review due to trivial changes (16)
  • website/next-env.d.ts
  • website/components/MemoryGraph.module.css
  • website/.gitignore
  • website/app/page.tsx
  • website/next.config.ts
  • website/components/Install.module.css
  • website/components/Primitives.module.css
  • website/components/Stats.module.css
  • website/components/Agents.module.css
  • website/components/Footer.module.css
  • website/components/Nav.module.css
  • website/components/LiveTerminal.module.css
  • website/tsconfig.json
  • website/app/globals.css
  • website/package.json
  • website/components/Compare.module.css

Comment thread website/app/layout.tsx
Comment on lines +24 to +36
openGraph: {
title: "agentmemory",
description:
"Persistent memory for AI coding agents. Runs locally. Zero external databases.",
type: "website",
url: "/",
},
twitter: {
card: "summary_large_image",
title: "agentmemory",
description:
"Persistent memory for AI coding agents. Runs locally. Zero external databases.",
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid advertising a large-image card before image metadata exists.

Since this PR does not include social/OG images, summary_large_image may produce an incomplete or downgraded preview. Either add openGraph.images/twitter.images, or use a plain summary card until the asset lands.

Proposed interim fix
   twitter: {
-    card: "summary_large_image",
+    card: "summary",
     title: "agentmemory",
     description:
       "Persistent memory for AI coding agents. Runs locally. Zero external databases.",
   },
📝 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
openGraph: {
title: "agentmemory",
description:
"Persistent memory for AI coding agents. Runs locally. Zero external databases.",
type: "website",
url: "/",
},
twitter: {
card: "summary_large_image",
title: "agentmemory",
description:
"Persistent memory for AI coding agents. Runs locally. Zero external databases.",
},
openGraph: {
title: "agentmemory",
description:
"Persistent memory for AI coding agents. Runs locally. Zero external databases.",
type: "website",
url: "/",
},
twitter: {
card: "summary",
title: "agentmemory",
description:
"Persistent memory for AI coding agents. Runs locally. Zero external databases.",
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/app/layout.tsx` around lines 24 - 36, The Open Graph/Twitter metadata
currently sets twitter.card to "summary_large_image" without providing image
assets; update the metadata in the layout (openGraph and twitter objects) to
either add openGraph.images and twitter.images with the image metadata or change
twitter.card to a plain summary (e.g., "summary") until the social/OG image
assets are added so previews won't be downgraded; modify the openGraph and
twitter entries in website/app/layout.tsx (look for the openGraph and twitter
objects and the "summary_large_image" value) accordingly.

Comment on lines +3 to +9
const ROWS = [
["RETRIEVAL R@5", "95.2%", "81.4%", "73.8%", "78.1%"],
["EXTERNAL DEPS", "0", "2 (Qdrant, Neo4j)", "1 (Postgres)", "1 (Neo4j)"],
["MCP TOOLS", "44", "12", "18", "9"],
["AUTO-HOOKS", "12", "0", "0", "0"],
["OPEN SOURCE", "YES (APACHE-2.0)", "YES", "YES", "YES"],
];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Verify competitor benchmark numbers before shipping publicly.

This table publishes specific performance/feature claims against named competitors (MEM0, LETTA, COGNEE). The lede says numbers come from LongMemEval-S and each project's own docs — please ensure each cell is traceable to a citable source (and ideally surface those citations in-page or in a footnote) so the claims can't be challenged as misleading comparative advertising. Hard-coding 0/12/44 for auto-hooks and MCP tool counts in particular will drift as those projects evolve; consider a dated "as of" note or moving the dataset to a versioned JSON with a lastVerified field.

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

In `@website/components/Compare.tsx` around lines 3 - 9, The ROWS constant in
Compare.tsx currently hardcodes competitor metrics (e.g., "RETRIEVAL R@5",
"AUTO-HOOKS", "MCP TOOLS") which can be challenged; instead, replace this static
array with a versioned data source (e.g., a JSON file or API) that includes
per-cell citations and a lastVerified/asOf field, and update the Compare
component to read that data and render citation links or a footnote;
specifically locate the ROWS constant and move its values into a new payload
that includes {label, values[], sources[], lastVerified} and render the
lastVerified date in the UI so claims are traceable and maintainable.

Comment on lines +61 to +79
.word {
display: inline-block;
opacity: 0;
transform: translateY(40px);
animation: slideIn 900ms cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
}
.word:nth-child(2) {
animation-delay: 120ms;
}
.accent {
color: var(--gold);
}

@keyframes slideIn {
to {
opacity: 1;
transform: translateY(0);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hero title animation ignores prefers-reduced-motion.

The PR summary states reduced-motion is respected, and MemoryGraph honors it, but .word always starts at opacity: 0; translateY(40px) and only the slideIn animation brings it back. Users with prefers-reduced-motion: reduce who encounter any animation glitch will see missing text. Gate the animation (or reset initial state) when reduced motion is preferred.

🛠️ Proposed fix
 `@keyframes` slideIn {
   to {
     opacity: 1;
     transform: translateY(0);
   }
 }
+
+@media (prefers-reduced-motion: reduce) {
+  .word {
+    opacity: 1;
+    transform: none;
+    animation: none;
+  }
+}

Also, stylelint flags slideIn — rename to slide-in to satisfy keyframes-name-pattern if you want the lint hint clean.

📝 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
.word {
display: inline-block;
opacity: 0;
transform: translateY(40px);
animation: slideIn 900ms cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
}
.word:nth-child(2) {
animation-delay: 120ms;
}
.accent {
color: var(--gold);
}
@keyframes slideIn {
to {
opacity: 1;
transform: translateY(0);
}
}
.word {
display: inline-block;
opacity: 0;
transform: translateY(40px);
animation: slideIn 900ms cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
}
.word:nth-child(2) {
animation-delay: 120ms;
}
.accent {
color: var(--gold);
}
`@keyframes` slideIn {
to {
opacity: 1;
transform: translateY(0);
}
}
`@media` (prefers-reduced-motion: reduce) {
.word {
opacity: 1;
transform: none;
animation: none;
}
}
🧰 Tools
🪛 Stylelint (17.7.0)

[error] 74-74: Expected keyframe name "slideIn" to be kebab-case (keyframes-name-pattern)

(keyframes-name-pattern)

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

In `@website/components/Hero.module.css` around lines 61 - 79, The .word class
currently sets opacity:0 and transform:translateY(40px) then relies on
`@keyframes` slideIn to animate to visible, which breaks for users with
prefers-reduced-motion; update the CSS so that when prefers-reduced-motion:
reduce the .word initial state is the final state (opacity:1 and transform:
translateY(0)) and the animation is disabled (remove animation property) for
that media query, and also rename the `@keyframes` from slideIn to slide-in (and
update the .word animation reference) to satisfy stylelint's
keyframes-name-pattern.

Comment thread website/components/Install.tsx Outdated
Comment on lines +17 to +33
function CopyBox({ cmd, hint }: { cmd: string; hint: string }) {
const [copied, setCopied] = useState(false);
const [label, setLabel] = useState(hint);

const onClick = async () => {
try {
await navigator.clipboard.writeText(cmd);
setCopied(true);
setLabel("COPIED");
setTimeout(() => {
setCopied(false);
setLabel(hint);
}, 1600);
} catch {
setLabel("CLIPBOARD BLOCKED");
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clear the pending timeout on unmount / rapid re-clicks.

setTimeout is never cleared. If the component unmounts within 1.6s, React will warn about state updates on an unmounted component. Additionally, rapid successive clicks stack timers, so an earlier timer can revert label back to hint while the user perceives a second "COPIED" state. Track the timer in a ref and clear it on unmount and before scheduling a new one.

🛠️ Proposed fix
 function CopyBox({ cmd, hint }: { cmd: string; hint: string }) {
   const [copied, setCopied] = useState(false);
   const [label, setLabel] = useState(hint);
+  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+
+  useEffect(() => {
+    return () => {
+      if (timerRef.current) clearTimeout(timerRef.current);
+    };
+  }, []);

   const onClick = async () => {
     try {
       await navigator.clipboard.writeText(cmd);
       setCopied(true);
       setLabel("COPIED");
-      setTimeout(() => {
+      if (timerRef.current) clearTimeout(timerRef.current);
+      timerRef.current = setTimeout(() => {
         setCopied(false);
         setLabel(hint);
       }, 1600);
     } catch {
       setLabel("CLIPBOARD BLOCKED");
     }
   };
📝 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 CopyBox({ cmd, hint }: { cmd: string; hint: string }) {
const [copied, setCopied] = useState(false);
const [label, setLabel] = useState(hint);
const onClick = async () => {
try {
await navigator.clipboard.writeText(cmd);
setCopied(true);
setLabel("COPIED");
setTimeout(() => {
setCopied(false);
setLabel(hint);
}, 1600);
} catch {
setLabel("CLIPBOARD BLOCKED");
}
};
function CopyBox({ cmd, hint }: { cmd: string; hint: string }) {
const [copied, setCopied] = useState(false);
const [label, setLabel] = useState(hint);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, []);
const onClick = async () => {
try {
await navigator.clipboard.writeText(cmd);
setCopied(true);
setLabel("COPIED");
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
setCopied(false);
setLabel(hint);
}, 1600);
} catch {
setLabel("CLIPBOARD BLOCKED");
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/components/Install.tsx` around lines 17 - 33, The CopyBox component
never clears the timeout, causing state updates after unmounts and stacked
timers on rapid clicks; fix it by storing the timeout id in a ref (e.g.,
timeoutRef) inside the CopyBox component, clear any existing timeoutRef.current
before scheduling a new setTimeout in onClick, and add a useEffect cleanup that
clears timeoutRef.current on unmount; keep the existing setCopied and setLabel
behavior but ensure the timeout is cancelled to avoid stale state updates.

Comment on lines +63 to +115
const play = useCallback(async () => {
const term = termRef.current;
if (!term || runningRef.current) return;
runningRef.current = true;
setStatus("RUNNING");
term.innerHTML = "";
const caret = document.createElement("span");
caret.className = styles.caret;
term.appendChild(caret);
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;

for (const seg of SCRIPT) {
const span = document.createElement("span");
const c = classFor(seg.t);
if (c) span.className = c;
term.insertBefore(span, caret);
if (seg.t === "typed") {
for (const ch of seg.text) {
span.textContent += ch;
await new Promise((r) =>
setTimeout(r, reduce ? 0 : 16 + Math.random() * 34),
);
}
await new Promise((r) => setTimeout(r, reduce ? 0 : 260));
} else {
span.textContent = seg.text;
await new Promise((r) => setTimeout(r, reduce ? 0 : 160));
}
}
setStatus("DONE");
runningRef.current = false;
}, []);

useEffect(() => {
const term = termRef.current;
if (!term) return;
const host = term.closest("[data-terminal]");
if (!host) return;
const io = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting && !played.current) {
played.current = true;
play();
io.unobserve(entry.target);
}
}
},
{ threshold: 0.4 },
);
io.observe(host);
return () => io.disconnect();
}, [play]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

No cleanup/abort for in-flight playback.

play() awaits a chain of setTimeout promises but has no cancellation hook. If the component unmounts mid-playback (e.g., client-side navigation away from the page), the loop keeps firing, appending to a detached DOM node and eventually calling setStatus("DONE") on an unmounted component. Additionally, clicking REPLAY while a run is in progress silently no-ops because runningRef.current is already true — the user gets no feedback that their click was ignored.

Consider threading an AbortController (or a monotonically increasing run-id ref) through play() so that useEffect cleanup and the REPLAY handler can cancel the active run before starting/leaving.

♻️ Sketch
-  const play = useCallback(async () => {
+  const runIdRef = useRef(0);
+  const play = useCallback(async () => {
     const term = termRef.current;
-    if (!term || runningRef.current) return;
-    runningRef.current = true;
+    if (!term) return;
+    const myRun = ++runIdRef.current;
+    runningRef.current = true;
     setStatus("RUNNING");
     ...
     for (const seg of SCRIPT) {
+      if (runIdRef.current !== myRun) return;
       ...
     }
-    setStatus("DONE");
-    runningRef.current = false;
+    if (runIdRef.current === myRun) {
+      setStatus("DONE");
+      runningRef.current = false;
+    }
   }, []);

And in the effect cleanup / REPLAY handler, bump runIdRef.current to cancel the prior run.

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

In `@website/components/LiveTerminal.tsx` around lines 63 - 115, The playback
routine play() lacks cancellation, causing async timeouts to continue after
unmount or when REPLAY is clicked; thread an AbortController or a runIdRef
through play() and all await points, aborting the active run from the useEffect
cleanup and from the REPLAY handler. Concretely: create a runIdRef or
AbortController per invocation inside play(), check signal/compare id between
each character/segment and immediately stop mutating DOM or calling setStatus if
aborted; in the useEffect cleanup call controller.abort() or bump runIdRef to
cancel in-flight play, and modify the REPLAY handler to abort the current run
before starting a new one and provide immediate feedback by resetting
runningRef/played accordingly. Ensure you reference and update runningRef,
played, termRef, and setStatus consistently so aborted runs do not touch a
detached DOM or set state on unmounted components.

const update = () => {
const h = document.documentElement;
const max = h.scrollHeight - h.clientHeight;
const pct = max <= 0 ? 0 : Math.min(1, h.scrollTop / max);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clamp the scroll percentage on both bounds.

Math.min(1, ...) prevents overflow past 100%, but negative scrollTop values can still produce a negative width during overscroll/rubber-band behavior. Clamp the lower bound too.

🐛 Proposed fix
-      const pct = max <= 0 ? 0 : Math.min(1, h.scrollTop / max);
+      const pct = max <= 0 ? 0 : Math.min(1, Math.max(0, h.scrollTop / max));
📝 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 pct = max <= 0 ? 0 : Math.min(1, h.scrollTop / max);
const pct = max <= 0 ? 0 : Math.min(1, Math.max(0, h.scrollTop / max));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@website/components/ScrollProgress.tsx` at line 12, The scroll percentage
calculation in ScrollProgress computes pct using Math.min(1, h.scrollTop / max)
which still allows negative values during overscroll; update the pct calculation
(the pct variable where h.scrollTop / max is used) to clamp both bounds by
applying Math.max(0, Math.min(1, h.scrollTop / max)) (or an equivalent clamp
helper) so pct never goes below 0 or above 1.

Comment on lines +25 to +50
useEffect(() => {
if (!rootRef.current) return;
const numEls = rootRef.current.querySelectorAll<HTMLDivElement>(
"[data-num]",
);

const count = (el: HTMLDivElement) => {
const target = Number(el.dataset.target);
const suffix = el.dataset.suffix || "";
const isFloat = el.dataset.float === "1";
const startAt = performance.now();
const duration = 1400;
const tick = (now: number) => {
const t = Math.min(1, (now - startAt) / duration);
const eased = 1 - Math.pow(1 - t, 3);
const v = target * eased;
el.textContent = isFloat
? `${v.toFixed(1)}${suffix}`
: `${Math.round(v)}${suffix}`;
if (t < 1) requestAnimationFrame(tick);
else
el.textContent = isFloat
? `${target.toFixed(1)}${suffix}`
: `${target}${suffix}`;
};
requestAnimationFrame(tick);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Render truthful stat values by default and only animate when motion is allowed.

The initial markup renders every benchmark as 0, so no-JS/static consumers see incorrect values. The count-up also runs even for users with prefers-reduced-motion: reduce, despite the PR objective.

♿ Proposed fix
 const STATS: StatItem[] = [
   { target: 95.2, suffix: "%", label: "RETRIEVAL R@5 · LONGMEMEVAL-S", float: true },
   { target: 92, suffix: "%", label: "FEWER INPUT TOKENS PER SESSION" },
   { target: 44, label: "MCP TOOLS" },
   { target: 12, label: "AUTOHOOKS" },
   { target: 0, label: "EXTERNAL DATABASES" },
   { target: 769, label: "TESTS PASSING" },
 ];
+
+const formatStatValue = ({ target, suffix = "", float }: Pick<StatItem, "target" | "suffix" | "float">) =>
+  `${float ? target.toFixed(1) : target}${suffix}`;
 
 export function Stats() {
   const rootRef = useRef<HTMLDivElement>(null);
 
   useEffect(() => {
     if (!rootRef.current) return;
     const numEls = rootRef.current.querySelectorAll<HTMLDivElement>(
       "[data-num]",
     );
+
+    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
+      numEls.forEach((el) => {
+        el.dataset.done = "1";
+      });
+      return;
+    }
 
     const count = (el: HTMLDivElement) => {
       const target = Number(el.dataset.target);
       const suffix = el.dataset.suffix || "";
       const isFloat = el.dataset.float === "1";
       const startAt = performance.now();
       const duration = 1400;
+      el.textContent = isFloat ? `0.0${suffix}` : `0${suffix}`;
       const tick = (now: number) => {
         const t = Math.min(1, (now - startAt) / duration);
         const eased = 1 - Math.pow(1 - t, 3);
         const v = target * eased;
         el.textContent = isFloat
@@
             >
-              0{s.suffix || ""}
+              {formatStatValue(s)}
             </div>

Also applies to: 76-89

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

In `@website/components/Stats.tsx` around lines 25 - 50, The count-up currently
leaves elements showing "0" and always animates; update the useEffect/count
logic so each element's textContent is initialized to its final value from
el.dataset.target (and suffix/float) so static/SSR and no-JS users see truthful
numbers, then only run requestAnimationFrame animation when JavaScript runs and
motion is allowed; detect reduced motion with
window.matchMedia('(prefers-reduced-motion: reduce)').matches and, if true, skip
the animation and leave the final formatted value (use rootRef, useEffect, and
the count function, reading el.dataset.target, el.dataset.suffix, and
el.dataset.float to format).

Comment thread website/README.md
Comment on lines +3 to +5
Next.js 15 App Router landing page for agentmemory. Lamborghini-inspired
black + gold design system. Deploys to Vercel with zero config.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Tighten deploy wording for consistency.

“Deploys to Vercel with zero config” is slightly inconsistent with the required Root Directory setting in a repo-level import flow. Consider rewording to “minimal config” to avoid confusion.

Suggested wording
-Next.js 15 App Router landing page for agentmemory. Lamborghini-inspired
-black + gold design system. Deploys to Vercel with zero config.
+Next.js App Router landing page for agentmemory. Lamborghini-inspired
+black + gold design system. Deploys to Vercel with minimal config.

Also applies to: 26-27

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

In `@website/README.md` around lines 3 - 5, Replace the phrase "Deploys to Vercel
with zero config" with "Deploys to Vercel with minimal config" (or similar
wording) to avoid implying no setup is required; update all other occurrences of
the exact string ("Deploys to Vercel with zero config") elsewhere in the README
so wording is consistent (the diff shows another instance around lines 26-27),
and ensure any surrounding copy briefly notes the Root Directory or minimal
repo-level setting if necessary.

Comment thread website/README.md

## Stack

- Next.js 15.1 (App Router, React 19, TypeScript 5.7)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Checking documented runtime versions in website/package.json"
rg -n '"next"\s*:|"react"\s*:|"react-dom"\s*:|"typescript"\s*:' website/package.json

Repository: rohitg00/agentmemory

Length of output: 236


Update README to reflect actual Next.js version.

README documents Next.js 15.1 but website/package.json specifies 16.2.4. This version drift can lead to incorrect local setup and dependency pinning.

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

In `@website/README.md` at line 8, The README currently states "Next.js 15.1" but
website/package.json lists Next.js version "16.2.4"; update the README entry
string to match the package.json version (replace "Next.js 15.1" with "Next.js
16.2.4") and, if you prefer a more future-proof approach, reference the Next.js
version from package.json or note the minimum supported version instead of a
hard-coded value; ensure the README's heading/line that contains "Next.js 15.1"
is the one you change so both files stay in sync.

Comment thread website/README.md
Comment on lines +33 to +54
```
website/
app/
layout.tsx — <html> + fonts + metadata + viewport
page.tsx — composes the landing sections in order
globals.css — design tokens, buttons, section-head utilities
components/
Nav.tsx — hexagonal bull mark + menu
Hero.tsx — title + lede + CTAs
MemoryGraph.tsx — client canvas animation + hexagonal pause + scroll rail
Stats.tsx — counter-up on intersect
Primitives.tsx — three cards with 3D mouse tilt
LiveTerminal.tsx — typewriter replay of memory.recall + consolidate
Compare.tsx — agentmemory vs Mem0/Letta/Cognee table
Agents.tsx — supported-agents grid
Install.tsx — click-to-copy npm + console commands
Footer.tsx — source / changelog / license links
ScrollProgress.tsx — thin gold progress bar at the top of the viewport
next.config.ts
tsconfig.json
package.json
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add a language identifier to the fenced structure block.

This block is missing a fence language and triggers markdownlint MD040.

Proposed fix
-```
+```text
 website/
   app/
     layout.tsx      — <html> + fonts + metadata + viewport
@@
   package.json
</details>

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

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

[warning] 33-33: 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 @website/README.md around lines 33 - 54, The fenced directory-structure block
in README.md (the triple-backtick block showing "website/" and the listed files
like app/layout.tsx, page.tsx, components/Nav.tsx, etc.) lacks a language
identifier and triggers markdownlint MD040; update the opening fence from totext (or another appropriate language like dos or bash) so the block is
explicitly marked, leaving the inner content unchanged.


</details>

<!-- fingerprinting:phantom:triton:hawk:c843331f-2459-4ec5-b4bf-e71e424ebea6 -->

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

…MCP install

Nav
- Replace overlay hamburger with a solid horizontal bar.
- Use the real agentmemory icon from assets/icon.svg as the brand mark.
- Single GitHub pill: GH icon + GITHUB label + gold star + live count,
  the entire pill links to the repo. No more separate icon button.
- Server-rendered count pulled from api.github.com/repos/... with
  revalidate 3600. Falls back to 0 on rate limit.
- Mobile (<720px) collapses to a hamburger sheet with sections + links.

Command Center (new)
- Tabbed section showing the shipped viewer (:3113), the iii console
  (:3114), the raw KV state browser, and OTel traces.
- Real screenshots vendored into public/ (dashboard.png,
  states.png, traces-waterfall.png, demo.gif).
- Each tab ships with a description, 4-5 capability bullets, and a
  copy-ready launch command.

Features (new)
- 12-tile grid covering 12 auto-hooks, 44 MCP tools, 49 REST
  endpoints, BM25+vector+graph recall, auto consolidation, JSONL
  replay, knowledge graph extraction, mesh federation, Obsidian
  export, 5 LLM providers, OTel observability, 0 external DBs.

Agents
- Replaced the flat tile grid with an agents.md-style two-tier layout.
- Top row: 4 featured first-party integrations (Claude Code,
  OpenClaw, Hermes, Codex CLI) as big cards with real logos, brand
  accent colors, and integration pitch.
- Bottom row: CSS keyframe marquee of every other supported agent
  (Claude Desktop, Cursor, Gemini CLI, OpenCode, Cline, Roo,
  Kilo, Goose, Aider, Windsurf) with real logos + brand borders on
  hover. Pauses on hover. Edges fade into the page. Respects
  prefers-reduced-motion.

Install · step 3 · MCP wiring
- Dropped the 10-tab toggle and the Cursor-only gold hero button.
- Left column: equal-weight agent chips in a 2x3 grid — Cursor,
  VS Code, Claude Code, Claude Desktop, Gemini CLI, Codex CLI.
  Click copies the right snippet (JSON / CLI command / TOML) or
  fires the one-click deeplink where one exists.
- Right column: UNIVERSAL MCP JSON — paste-ready block that works
  for Claude Desktop, Cursor, Cline, Windsurf, Gemini CLI, OpenCode.
- Expando reveals Hermes, OpenClaw, and Codex TOML shapes.

Infrastructure
- lib/github.ts server-only fetch for repo stats.
- lib/format.ts (client-safe) formatCompact.
- next.config.ts: turbopack.root pinned to silence workspace warning,
  images.remotePatterns for github.com / GH avatars / Cursor /
  Windsurf logos.
- layout.tsx: icon metadata wired to /icon.svg.

Build: next build clean, 3 routes prerendered static, 0 vulns.
@rohitg00 rohitg00 merged commit 4e7d000 into main Apr 18, 2026
2 of 3 checks passed
@rohitg00 rohitg00 deleted the feat/website-lambo branch April 18, 2026 18:52
rohitg00 added a commit that referenced this pull request Apr 18, 2026
Bump version + ship CHANGELOG covering everything that merged since
v0.8.13:

- #118 security advisory drafts for v0.8.2 CVEs
- #132 semantic eviction routing + batched retention audit
- #157 iii console docs + vendored screenshots in README
- #160 (#158) health gated on RSS floor
- #161 (#159) standalone MCP proxies to the running server
- #162 (#125) mem::forget audit coverage + policy doc
- #163 (#62) @agentmemory/fs-watcher filesystem connector
- #164 Next.js website (website/ root, ship to Vercel)

Version bumps (8 files):
- package.json / package-lock.json (top + packages[''])
- plugin/.claude-plugin/plugin.json
- packages/mcp/package.json (self + ~0.9.0 dep pin)
- src/version.ts (union extended, assigned 0.9.0)
- src/types.ts (ExportData.version union)
- src/functions/export-import.ts (supportedVersions set)
- test/export-import.test.ts (export assertion)

Tests: 777 passing. Build clean.
@rohitg00 rohitg00 mentioned this pull request Apr 18, 2026
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