* feat(frontend): render markdown artifacts in the new window The artifacts panel's "open in new window" action handed the browser the raw Gateway response. For markdown that is a `text/markdown` body the browser can only show as source, so the new window was a text dump rather than a reader. Route markdown artifacts to a new `/artifacts/view` page that renders them with the same components the panel uses (SafeStreamdown + the artifact rehype chain + citation links/panel), including the truncated-preview banner and its "load full file" action. Everything else keeps the raw Gateway URL — notably HTML/SVG, which the Gateway deliberately serves as a download so active content never executes in the application origin. - `core/artifacts/viewer.ts` centralizes which stored artifacts are markdown (`.skill` archives included, since they hold a SKILL.md), so the panel and the viewer route cannot drift. - `ArtifactFilePreview` and its siblings move out of `artifact-file-detail.tsx` into `artifact-file-preview.tsx`; otherwise the standalone route would pull the CodeMirror editor into its bundle. - The window title comes from the route's `generateMetadata`, not `document.title`, which the App Router overwrites after hydration. - The viewer reads content through `useStandaloneArtifactContent`, which shares `useArtifactContent`'s query key but not its `useThread` dependency, since a detached window has no thread context. Claude-Session: https://claude.ai/code/session_013AiCrC5SBc3HdFYNxsp1EC * fix(frontend): keep the artifact target across re-authentication Review found the standalone viewer unrecoverable from an expired session. The window's target lives entirely in `?path=...&thread_id=...`, and both auth paths dropped it: - The layout guard redirected to `/login` with no `next` at all. A layout cannot read `searchParams`, so the guard moves into the page, which can — and rebuilds the full viewer address for `next`. The layout loses its AuthProvider along the way: nothing under this route reads `useAuth`, and the guard now makes a single `getServerSideUser` call per request. - The shared fetch wrapper built `next` from `window.location.pathname`, which silently truncated the query string. It now carries `search` too, so any route holding state in the query survives a 401, not just this one. `validateAuthNextPath` already accepts a query string. `buildArtifactViewerURL` is split out of `resolveArtifactOpenURL`: the guard needs the route itself, never the Gateway fallback that the latter takes for non-markdown targets. Tests: the login round trip (unit — the rebuilt URL survives `validateAuthNextPath` and parses back to the same target), the fetch wrapper preserving the query on 401 (unit), and the expired-session window reaching `/login` with the artifact intact (E2E). The E2E asserts on the popup's navigation *requests*, since `(auth)/layout` answers `/login` with a server redirect under DEER_FLOW_AUTH_DISABLED and no navigation commits. `tests/unit/core/models/api.test.ts` stubbed `window.location` without `search`; a real Location always has it. Claude-Session: https://claude.ai/code/session_013AiCrC5SBc3HdFYNxsp1EC * fix(frontend): keep public showcase artifacts out of the auth gate Review found that the viewer's access check regressed `/showcase`. Those pages render with `isMock`, their artifacts are served by the unauthenticated demo route, and the raw artifact URL this window replaced stayed public — so gating the window unconditionally bounced every logged-out showcase visitor to /login for a document that is already public. `requiresAuthenticatedViewer` exempts a mock target only when `resolveStaticDemoArtifact` would actually serve it. The allowlist is the authority rather than the flag: `mock=true` is caller-supplied, so a target the demo route answers with 404 — a non-allowlisted path, or a thread that is not a demo thread — still needs a session. Covered in `tests/e2e-auth/`, since the default E2E config disables auth and cannot see this: a public showcase artifact renders without a session, while a non-allowlisted path and a missing mock flag both land on /login. Verified the positive case goes red without the exemption. Claude-Session: https://claude.ai/code/session_013AiCrC5SBc3HdFYNxsp1EC
8.6 KiB
AGENTS.md
This file provides guidance to AI coding agents (Claude Code, Codex, and others) when working with the DeerFlow frontend. It is the source of truth; the sibling CLAUDE.md imports it via @AGENTS.md.
Project Overview
DeerFlow Frontend is a Next.js 16 web interface for an AI agent system. It communicates with a LangGraph-based backend to provide thread-based AI conversations with streaming responses, artifacts, and a skills/tools system.
Stack: Next.js 16, React 19, TypeScript 5.8, Tailwind CSS 4, pnpm 10.26.2. Requires Node.js 22+ and pnpm 10.26.2+.
Core dependencies
- LangGraph SDK (
@langchain/langgraph-sdk^1.5.3) — Agent orchestration and streaming - LangChain Core (
@langchain/core^1.1.15) — Fundamental AI building blocks - TanStack Query (
@tanstack/react-query^5.90.17) — Server state management - UI: Shadcn UI, MagicUI, React Bits, and Vercel AI SDK elements (generated from registries — see Code Style)
Commands
| Command | Purpose |
|---|---|
pnpm dev |
Platform-aware dev server (Webpack on Windows, Turbopack elsewhere) |
pnpm build |
Production build |
pnpm check |
Lint + type check (run before committing) |
pnpm lint |
ESLint only |
pnpm lint:fix |
ESLint with auto-fix |
pnpm format |
Prettier check (pnpm format:write to apply) |
pnpm test |
Run unit tests with Rstest |
pnpm test:e2e |
Run E2E tests with Playwright (Chromium) |
pnpm typecheck |
TypeScript type check (tsc --noEmit) |
pnpm start |
Start production server |
Unit tests live under tests/unit/ and mirror the src/ layout (e.g., tests/unit/core/api/stream-mode.test.ts tests src/core/api/stream-mode.ts). Powered by Rstest; import source modules via the @/ path alias.
Use DEER_FLOW_DEV_BUNDLER=turbo or DEER_FLOW_DEV_BUNDLER=webpack with pnpm dev to override the platform default when diagnosing a local Next.js bundler issue.
Rstest runs them as two projects (rstest.config.ts). *.test.ts / *.test.tsx run in a plain node environment — that is nearly the whole suite, and it is the default for anything that is pure logic. *.dom.test.ts / *.dom.test.tsx run in happy-dom, for tests that need a document: hooks driven through renderHook from @testing-library/react, and components. Keep the split — a DOM environment costs roughly 3x the runtime of the node suite, so tests that do not render should not opt into it. A hook whose behavior only exists under real React (effect ordering, cleanup on unmount, re-render on store change) belongs in a .dom.test.* file rather than a node test that mocks react itself.
E2E tests live under tests/e2e/ and use Playwright with Chromium. They mock all backend APIs via page.route() network interception and test real page interactions (navigation, chat input, streaming responses). Config: playwright.config.ts.
Architecture
Frontend (Next.js) ──▶ LangGraph SDK ──▶ LangGraph Backend (lead_agent)
├── Sub-Agents
└── Tools & Skills
The frontend is a stateful chat application. Users create threads (conversations), send messages, set thread-scoped /goal completion conditions, and receive streamed AI responses. The backend orchestrates agents that can produce artifacts (files/code), todos, and goal state updates.
Source Layout (src/)
app/— Next.js App Router. Routes include/(landing),/showcase/[thread_id](allowlisted public read-only demos),/workspace/chats/[thread_id](authenticated chat),/workspace/agents/[agent_name]and/workspace/agents/new(custom agents),/artifacts/view(chrome-free window that renders one markdown artifact with the panel's own renderer),/blog/…, the(auth)/{login,setup,auth/callback}flow,/[lang]/docs/…, and/api/…route handlers (e.g./api/memory).components/— React components:ui/— Shadcn UI primitives (auto-generated, ESLint-ignored)ai-elements/— Vercel AI SDK elements (auto-generated, ESLint-ignored)workspace/— Chat page components (messages, artifacts, settings)landing/— Landing page sectionsdocs/— Docs / MDX rendering components
core/— Business logic, the heart of the app. Domains includethreads/(creation, streaming, state),api/(LangGraph client singleton),agents/(custom agents),subagents/(runtime worker catalog and administrator mutations),auth/(authentication),artifacts/,channels/(IM connections),integrations/(managed third-party integration status/install clients such as Lark CLI),i18n/(en-US, zh-CN),settings/,memory/,skills/,messages/,mcp/,models/,input-polish/(pre-send draft rewrite API),voice-input/(browser speech-recognition helpers),suggestions/,tasks/,todos/,tools/,workspace-changes/(run-scoped changed-file summaries and diff fetching),config/,notification/,blog/, plus rendering helpers (rehype/,streamdown/) andutils/.hooks/— Shared React hookslib/— Utilities (cn()from clsx + tailwind-merge)content/— MDX content (blog posts, docs) rendered by the appstyles/— Global CSS with Tailwind v4@importsyntax and CSS variables for themingtypings/— Ambient TypeScript declarations- Root files:
env.js(env validation),mdx-components.ts(MDX component map)
More specific AGENTS.md files under src/ contain the frontend sections split from this file.
Code Style
- Imports: Enforced ordering (builtin → external → internal → parent → sibling), alphabetized, newlines between groups. Use inline type imports:
import { type Foo }. - Unused variables: Prefix with
_. - Class names: Use
cn()from@/lib/utilsfor conditional Tailwind classes. - Path alias:
@/*maps tosrc/*. - Components:
ui/andai-elements/are generated from registries (Shadcn, MagicUI, React Bits, Vercel AI SDK) — don't manually edit these.
Environment
Backend API URLs are optional; an nginx proxy is used by default:
NEXT_PUBLIC_BACKEND_BASE_URL=http://localhost:8001
NEXT_PUBLIC_LANGGRAPH_BASE_URL=http://localhost:8001/api
Leave these unset for the standard make dev / Docker flow, where nginx serves the public /api/langgraph/* prefix and rewrites it to Gateway's native /api/* routes.
To reach a dev server on anything other than localhost — a LAN address, or a proxied hostname — list the host in DEER_FLOW_DEV_ALLOWED_ORIGINS (comma-separated; a full URL is reduced to its host). It feeds Next's allowedDevOrigins, which gates /_next/*, fonts, and HMR. Without it those requests get a 403 and the page renders server-side but never hydrates, so nothing on it — including the login form — responds. Development only; production builds ignore it.
Resources
Contributing
When adding features:
- Follow the established
src/structure - Add TypeScript types and proper error handling
- Write unit tests under
tests/unit/(pnpm test) and E2E tests undertests/e2e/(pnpm test:e2e) - Run
pnpm checkbefore committing - Update this
AGENTS.mdwhen architecture, commands, or conventions change
Route asset budgets are enforced with pnpm perf:check. The command measures
/login from a normal production build, then builds in static-demo mode for the
fixture-backed workspace routes. It starts the production server on temporary local
ports, measures the unique JavaScript and CSS files referenced by representative
routes, writes the detailed result to .next/performance-results.json, and compares
totals with performance-budgets.json. Fix route ownership or split points when a
budget fails; do not raise a ceiling without documenting and reviewing the measured
regression.