feat(website): lamborghini-inspired landing page#164
Conversation
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.
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughA 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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 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
📒 Files selected for processing (5)
DESIGN.mdwebsite/README.mdwebsite/index.htmlwebsite/script.jswebsite/styles.css
| **Black Filled** — Dark filled variant: | ||
| - Default: bg `#000000`, text `#202020` | ||
| - Used for: Inverted CTA on light sections |
There was a problem hiding this comment.
"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.
| <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> |
There was a problem hiding this comment.
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.
| 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 = ""; | ||
| }); | ||
| }); |
There was a problem hiding this comment.
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.
| 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"; | ||
| } | ||
| }); |
There was a problem hiding this comment.
Copy handler leaves "COPIED" stuck on rapid double-clicks and never clears "CLIPBOARD BLOCKED".
Two small but user-visible defects in the same handler:
const original = hint.textContentcaptures whatever the hint currently shows. A second click that lands inside the 1600ms window captures"COPIED"asoriginal; its own timeout then restores to"COPIED", and the hint is stuck until the next click. Capture the original label once (e.g. fromdata-*or the first run) or reset to a known string.- The
catchbranch writes"CLIPBOARD BLOCKED"but never restores the label or theis-copiedstate, 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.
| 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.
| .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; | ||
| } |
There was a problem hiding this comment.
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.
| .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
There was a problem hiding this comment.
Actionable comments posted: 13
🧹 Nitpick comments (6)
website/components/Footer.tsx (1)
11-34: Consider addingnoreferrerto external linkrelattributes.External links use
rel="noopener"only. Addingnoreferreralso strips theRefererheader, which is the common hardened default (rel="noopener noreferrer"). Modern browsers setnoopenerimplicitly fortarget="_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 runtimeprefers-reduced-motionchanges.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
changeevent 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 missingrowgroupwrappers and an accessible name strategy for the empty corner header.A couple of small a11y polish items on the hand-rolled table:
- When using
role="table"withrole="row"children, assistive tech expectsrole="rowgroup"around the header row(s) and the body rows; without them, some screen readers announce the structure inconsistently.- 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").- Minor:
<section>witharia-labelledbyis good, but the outerrole="table"div could also benefit fromaria-describedbypointing 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 viadisplay: gridon 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 duplicatesScrollProgress.Per the PR summary and
ScrollProgress.tsx, a top scroll rail already exists and listens toscroll/resize. This component adds a second, identical scroll listener to driverailRef. If the hero-local rail is intentionally separate from the global one, this is fine; otherwise consider removing this block and leaving scroll progress toScrollProgress. Adding{ passive: true }toresizeis 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
runningcauses the full effect to tear down and re-run on each click: canvas is re-sized, nodes are re-seeded (visuals "jump"), andscroll/resizelisteners 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: Addnoreferrerto external links.
rel="noopener"alone protects against reverse-tabnabbing on older browsers, butnoreferreris the common paired hardening (and also strips the Referer header). Same applies to the external link inwebsite/components/Nav.tsxline 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
⛔ Files ignored due to path filters (1)
website/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (30)
website/.gitignorewebsite/README.mdwebsite/app/globals.csswebsite/app/layout.tsxwebsite/app/page.tsxwebsite/components/Agents.module.csswebsite/components/Agents.tsxwebsite/components/Compare.module.csswebsite/components/Compare.tsxwebsite/components/Footer.module.csswebsite/components/Footer.tsxwebsite/components/Hero.module.csswebsite/components/Hero.tsxwebsite/components/Install.module.csswebsite/components/Install.tsxwebsite/components/LiveTerminal.module.csswebsite/components/LiveTerminal.tsxwebsite/components/MemoryGraph.module.csswebsite/components/MemoryGraph.tsxwebsite/components/Nav.module.csswebsite/components/Nav.tsxwebsite/components/Primitives.module.csswebsite/components/Primitives.tsxwebsite/components/ScrollProgress.tsxwebsite/components/Stats.module.csswebsite/components/Stats.tsxwebsite/next-env.d.tswebsite/next.config.tswebsite/package.jsonwebsite/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
| 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.", | ||
| }, |
There was a problem hiding this comment.
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.
| 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.
| 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"], | ||
| ]; |
There was a problem hiding this comment.
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.
| .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); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| .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.
| 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"); | ||
| } | ||
| }; |
There was a problem hiding this comment.
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.
| 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.
| 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]); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
| 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.
| 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); |
There was a problem hiding this comment.
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).
| Next.js 15 App Router landing page for agentmemory. Lamborghini-inspired | ||
| black + gold design system. Deploys to Vercel with zero config. | ||
|
|
There was a problem hiding this comment.
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.
|
|
||
| ## Stack | ||
|
|
||
| - Next.js 15.1 (App Router, React 19, TypeScript 5.7) |
There was a problem hiding this comment.
🧩 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.jsonRepository: 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.
| ``` | ||
| 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 | ||
| ``` |
There was a problem hiding this comment.
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.
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.
Single-page marketing site for agentmemory, generated from
npx getdesign@latest add lamborghiniand built against that brief.What ships
DESIGN.mdat 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
memory.recall+memory.consolidatesession. REPLAY button.COPIEDstate.Full
prefers-reduced-motionshort-circuit — animation-heavy elements resolve instantly when the OS says no.Run locally
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
Summary by CodeRabbit
New Features
Documentation