Rams MCP · The full engine, now in your coding agent
khoj-ai on GitHub

khoj-ai/khoj

Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.

30 files reviewed·July 9, 2026

Elevated

Design risk in this codebase.

6issues
Critical & Serious

Top fix

Convert 'New' and 'Sign up' divs into real focusable buttons

See the fix

Verdict

Semantic HTML gets abandoned exactly where it matters most: the sidebar's nav links are clean, but the two primary actions are unfocusable divs. Credit for solid structural instincts elsewhere, but keyboard users can't sign up or start a chat.

Files Rams reviewed

src/interface/web/app/components/allConversations/sidePanel.module.css

src/interface/web/app/components/appSidebar/appSidebar.tsx

src/interface/web/app/components/chatHistory/chatHistory.module.css

src/interface/web/app/components/chatMessage/FileContentSnippet.tsx

src/interface/web/app/components/chatMessage/chatMessage.module.css

src/interface/web/app/components/deprecationBanner.tsx

src/interface/web/app/components/excalidraw/excalidrawWrapper.tsx

src/interface/web/app/components/loading/loading.tsx

src/interface/web/app/components/loginPrompt/loginPopup.tsx

src/interface/web/app/components/loginPrompt/loginPrompt.module.css

src/interface/web/app/components/logo/fileLogo.tsx

src/interface/web/app/components/mermaid/mermaid.tsx

src/interface/web/app/components/navMenu/navMenu.module.css

src/interface/web/app/components/navMenu/navMenu.tsx

src/interface/web/app/components/profileCard/profileCard.tsx

src/interface/web/app/components/shareLink/shareLink.tsx

src/interface/web/app/components/suggestions/suggestionCard.tsx

src/interface/web/app/components/userMemory/userMemory.tsx

src/interface/web/app/agents/layout.tsx

src/interface/web/app/automations/layout.tsx

src/interface/web/app/chat/layout.tsx

src/interface/web/app/layout.tsx

src/interface/web/app/settings/layout.tsx

src/interface/web/app/share/chat/layout.tsx

src/interface/web/app/share/chat/page.tsx

src/interface/web/app/components/agentCard/agentCard.module.css

src/interface/web/app/components/chatInputArea/chatInputArea.module.css

src/interface/web/app/components/chatWoot/ChatwootWidget.tsx

src/interface/web/app/components/excalidraw/excalidraw.tsx

src/interface/web/app/components/loginPrompt/GoogleSignIn.tsx

94/100

Accessibility

2 critical
Accessibility·src/interface/web/app/components/appSidebar/appSidebar.tsx:149Critical

"Sign up to get started" and "New" render as unfocusable divs, not buttons

Both `SidebarMenuButton asChild` wrappers pass onClick to an inner `<div>` (the Sign up prompt and the New chat action). Because `asChild` swaps the rendered element for that div, the click handler lands on a non-interactive element with no keyboard focus, no tabIndex, and no button role. - `<div><UserPlusIcon /><span>Sign up to get started</span></div>` - `<div><Plus /><span>New</span></div>`

Why it matters

Keyboard-only and screen reader users cannot tab to or activate the two most important actions in the sidebar: starting a chat and signing up. This locks a class of users out of the primary entry point of the app.

Fix

Use a semantic <button> element as the asChild target for any onClick handler.

<div>
        <UserPlusIcon />
        <span>Sign up to get started</span>
    </div>
<button type="button" className="flex items-center gap-2 w-full">
        <UserPlusIcon />
        <span>Sign up to get started</span>
    </button>
Accessibility·src/interface/web/app/components/deprecationBanner.tsx:27Critical

Banner text nearly unreadable against its own orange fill

The banner text (`text-orange-50`) sits on `bg-orange-600/90`. Blended against a white page background, the fill resolves to roughly #EC6924, and orange-50 text against it computes to ~3:1 contrast.

Why it matters

This falls below the 4.5:1 WCAG AA minimum for body text, so the deprecation notice, the most time-sensitive message on the page, is the hardest thing on the page to read.

Fix

Raise contrast to at least 4.5:1 by darkening the background or using a lighter, fully-opaque text color.

<div className="w-full px-4 py-2.5 bg-orange-600/90 shadow-sm flex items-center justify-center gap-2 text-sm text-orange-50 z-0 relative">
<div className="w-full px-4 py-2.5 bg-orange-700 shadow-sm flex items-center justify-center gap-2 text-sm text-white z-0 relative">
96/100

Color

2 serious
Color·src/interface/web/app/components/chatMessage/FileContentSnippet.tsx:57Serious

Line-number and truncation text have no dark-mode variant while the highlight does

The target-line highlight correctly ships `bg-green-100 dark:bg-green-900`, but the adjacent line-number span (`text-gray-400`) and the truncation footer (`text-gray-500 italic`) carry no `dark:` variant anywhere in the file.

Why it matters

In dark mode these grays sit on a dark chat bubble with too little contrast, or clash with the theme, breaking readability exactly where the file already proves dark mode is supported.

Fix

Pair every gray text color with an explicit dark-mode counterpart, matching the pattern already used on the highlight background.

<div className="text-gray-500 italic">
    ... and {lines.length - maxLines} more lines
</div>
<div className="text-gray-500 dark:text-gray-400 italic">
    ... and {lines.length - maxLines} more lines
</div>
Color·src/interface/web/app/layout.tsx:56Serious

Dark class toggling has no color-scheme declaration for browser chrome

The inline script adds `document.documentElement.classList.add('dark')` to flip the app into dark mode before hydration, but the `<html>` element and `<head>` never set `color-scheme`.

Why it matters

Without color-scheme, native browser UI, form controls, scrollbars, and the default page background flash light even when the script has already applied dark mode, so the page looks half-themed for a frame and native inputs stay light inside an otherwise dark UI.

Fix

Set color-scheme to light dark (or toggle it alongside the dark class) so native controls track the active theme.

<html
    lang="en"
    className={`${noto_sans.variable} ${noto_sans_arabic.variable}`}
    suppressHydrationWarning
>
<html
    lang="en"
    className={`${noto_sans.variable} ${noto_sans_arabic.variable}`}
    style={{ colorScheme: "light dark" }}
    suppressHydrationWarning
>

Want this on your repo?

Rams reviews your next PR automatically and posts inline fix suggestions.

Review my public repo for free
97/100

Components

1 critical
Components·src/interface/web/app/components/excalidraw/excalidrawWrapper.tsx:116Critical

Hand-rolled expanded overlay has no dialog semantics or focus trap

When `expanded` is true, the wrapper renders a `fixed inset-0` overlay div with a dark backdrop, but it carries no `role="dialog"`, no `aria-modal`, and never moves focus into it. Escape is handled by directly reassigning the global `onkeydown` property instead of a scoped listener, so focus never returns to the trigger button on close.

Why it matters

Keyboard and screen reader users get no signal they've entered a modal-like view, can tab out into the page behind the backdrop, and lose their place when it closes.

Fix

Give the expanded container dialog semantics and move focus into it when opened, or replace it with an accessible dialog primitive.

<div
    className={`${expanded ? "fixed inset-0 bg-black bg-opacity-50 backdrop-blur-sm z-50 flex items-center justify-center" : ""}`}
>
<div
    role={expanded ? "dialog" : undefined}
    aria-modal={expanded ? true : undefined}
    aria-label="Expanded diagram view"
    className={`${expanded ? "fixed inset-0 bg-black bg-opacity-50 backdrop-blur-sm z-50 flex items-center justify-center" : ""}`}
>
98/100

UX

1 serious
UX·src/interface/web/app/components/loading/loading.tsx:56Serious

InlineLoading button has no explicit type, defaults to form submit

`InlineLoading` renders `<button className={...}>` with no `type` attribute. Buttons without an explicit type default to `type="submit"` in HTML.

Why it matters

If this component is ever mounted inside a `<form>`, clicking it (or it receiving a stray click/enter) submits the form unintentionally.

Fix

Set type='button' on any button that isn't intentionally a form submit control.

<button className={`${props.className}`}>
<button type="button" className={`${props.className}`}>

Typography

No issues found

Spacing

No issues found

Motion

No issues found

Craft

No issues found

Working well

  • Splitting `KhojLogoType` versus the plain `<h2>Ask Anything</h2>` behind `isMobileWidth` is a sensible way to keep the header compact on narrow viewports without hiding the wayfinding label entirely, since mobile users get the brand mark while desktop users get the literal page context.
  • The primary nav items ("Home", "Agents", "Automations", "Search", "Settings") are built from real <a> tags with href, which means keyboard focus, browser history, and screen readers all work for free. This is the pattern the two custom action buttons above should have followed.
  • Wrapping the theme-detection logic in try/catch guards against localStorage being unavailable (private browsing, disabled storage) without breaking the rest of the script, which is a reasonable defensive choice for code that runs before any error boundary exists.
  • Padding line numbers with `padStart(3, " ")` inside a monospace `<pre>` context keeps the gutter column visually aligned without needing `tabular-nums`: monospace already guarantees equal digit widths, so this is the simpler correct fix for the same problem.

Score your own repo with Rams.

Free on public repos. Rams reviews every pull request for accessibility, color, type, spacing, components, motion, UX, and craft, and posts inline fixes.