deer-flow/frontend/AGENTS.md
Ryker_Feng fa496c0c8d
feat(browser): add agentic browser control (#4187)
* feat(browser): add agentic browser control

* fix(frontend): format browser view changes

* fix(browser): keep browser optional and isolate sidecar layout

* fix(browser): address PR review security and IME findings

- Nginx: add a browser-stream WebSocket location before the generic
  /api/threads regex so Live upgrades instead of downgrading to HTTP
  (both nginx.conf and nginx.local.conf).
- Ownership: require an existing owned thread for the WS stream and REST
  navigate, and tear down the browser session on thread deletion so a
  later caller cannot reuse a retained page/cookies by guessing the id.
- SSRF: enforce the URL policy at the browser request boundary via a
  context-level route guard covering redirects, popups, iframes, and
  subresources (skipped for CDP-attached Chrome).
- IME: skip key forwarding while a composition is active so confirming a
  CJK candidate with Enter no longer submits the remote page form.

Adds regression tests for the request guard, session teardown on delete,
and the composing-Enter key decision.

* fix(frontend): smooth streaming in long tool threads

* Revert "fix(frontend): smooth streaming in long tool threads"

This reverts commit f0462516eabe77f138d4027ea1c714fb226683cf.

* fix(browser): address review security and lifecycle findings

- Reject cross-origin WebSocket upgrades on the live browser stream
  (Origin allow-list reuse of CORS/same-origin helpers) to close a
  WS-CSRF hole, and fail closed when the ownership store is absent.
- Warn when a CDP-attached session runs with the SSRF request guard
  off, and drop the unreachable CDP screencast teardown dead code.
- Read browser session launch config from a single canonical source
  (browser_navigate) so it is deterministic regardless of call order.
- Bound per-thread Chromium accumulation with idle-timeout eviction
  and an LRU max-sessions cap.
- Reset the Live reconnect counter on a successful open so the stream
  can't permanently stall after the cumulative attempt cap.

* fix(frontend): reduce long tool thread render stalls

Reuse stable historical message groups during streaming, defer heavy Markdown and browser previews, and lazy-decode message images.

* fix(browser): keep live control responsive during continuous input

Why: Manual browser control felt laggy — a physical click ran the remote
Playwright click three times and each non-move input synchronously awaited a
JPEG screenshot, so events queued behind capture (queue wait up to ~237ms).
The first async attempt used a trailing-edge debounce, which froze the visible
page until a wheel/keyboard gesture stopped ("scroll finishes, then it jumps").

What:
- Frontend forwards one `click` per physical click instead of also emitting
  `down`/`up`, so the remote page is not clicked twice per gesture.
- Backend detaches live-frame capture from input dispatch: non-move actions
  start a rate-limited background refresh loop (leading frame + bounded cadence)
  that keeps emitting frames while input continues and never blocks dispatch.
- Add regression tests: input dispatch no longer awaits the screenshot, rapid
  inputs coalesce, and continuous input keeps refreshing before it stops.

Scenarios: Verified in the live Browser panel — a single click completes in
~57ms (was blocked behind a 171ms capture), and a 1.14s sustained wheel gesture
renders ~7 frames throughout the scroll instead of one frame after it ends.

* fix(browser): harden worker and session lifecycle

* fix(browser): address latest review feedback

* fix(frontend): preserve optimistic new-chat message

* test(e2e): preserve mocked message run ids

* fix(browser): address capability review feedback

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-07-21 11:46:33 +08:00

14 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 Dev server with Turbopack (http://localhost:3000)
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.

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), /workspace/chats/[thread_id] (chat), /workspace/agents/[agent_name] and /workspace/agents/new (custom agents), /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 sections
    • docs/ — Docs / MDX rendering components
  • core/ — Business logic, the heart of the app. Domains include threads/ (creation, streaming, state), api/ (LangGraph client singleton), agents/ (custom agents), auth/ (authentication), artifacts/, channels/ (IM connections), 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/) and utils/.
  • hooks/ — Shared React hooks
  • lib/ — Utilities (cn() from clsx + tailwind-merge)
  • content/ — MDX content (blog posts, docs) rendered by the app
  • styles/ — Global CSS with Tailwind v4 @import syntax and CSS variables for theming
  • typings/ — Ambient TypeScript declarations
  • Root files: env.js (env validation), mdx-components.ts (MDX component map)

Data Flow

  1. Optional composer helpers such as core/input-polish can rewrite the local draft before submission, and core/voice-input can transcribe browser microphone input into that same local draft; confirmed user input then flows to thread hooks (core/threads/hooks.ts) → LangGraph SDK streaming
  2. Stream events update thread state (messages, artifacts, todos, goal)
  3. useThreadHistory loads persisted conversation pages from GET /api/threads/{id}/messages/page, preserving the backend's thread-global event seq; rendering overlays checkpoint/live copies at their matching canonical identities (a summarized checkpoint may contain a protected early input plus a recent tail), suppresses checkpoint/transient prefixes whose canonical position is still behind an unloaded cursor page instead of collapsing that unknown gap before a recent anchor, then adds optimistic messages without timestamp re-sorting. History invalidation preserves already-loaded pages so their established ordering positions are not discarded.
  4. Stop actions call the LangGraph SDK stream stop path; core/threads/hooks.ts invalidates current-thread, thread-history, token-usage, and sidebar/search caches immediately and schedules one follow-up refetch because SDK stop may finish via abort + fire-and-forget cancel before backend title finalization commits
  5. TanStack Query manages server state; localStorage stores user settings
  6. Components subscribe to thread state and render updates

Composer drafts are tab-scoped browser state. core/threads/composer-draft.ts stores only text plus the selected slash-skill name in sessionStorage, keyed by user, agent, and logical conversation scope. New-chat pages pass the stable scope "new" because their runtime threadId is a fresh UUID on every reload; established conversations use their real thread ID. InputBox waits for enabled skills before restoring a skill chip, degrades a missing/disabled skill back to editable slash text, and clears the stored draft through SendMessageOptions.onSent only after the send passes the in-flight guard. Attachments, sidecar quotes, voice state, and polish undo state are not persisted.

Auth UI note: the login page's "keep me signed in" option submits only remember_me to the Gateway and may persist only the email address through core/auth/remember-login.ts. Passwords and tokens must never be stored in frontend storage; the HttpOnly access_token and readable csrf_token cookies remain Gateway-owned.

/goal and /compact are built-in composer commands, not skill activations. src/components/workspace/input-box.tsx intercepts /goal, /goal clear, and /goal <condition> before normal chat submission, calling Gateway GET/PUT/DELETE /api/threads/{thread_id}/goal. Setting /goal <condition> also submits the condition text as the next user task so the agent starts running immediately; status and clear do not start a run. Goal and compact requests are tied to the current threadId with an AbortController, so switching threads or unmounting the composer aborts in-flight requests and stale responses cannot update the new thread's composer state. The chat pages render GoalStatus above the composer from AgentThreadState.goal, with local optimistic state until the next stream values update arrives. /compact calls POST /api/threads/{thread_id}/compact to summarize older active context while leaving the full visible chat history intact; it is skipped on new/empty threads and blocked server-side while a run is in flight.

Human input requests are a structured message protocol layered on normal chat history. The backend writes request payloads to ToolMessage.artifact.human_input, src/core/messages/human-input.ts owns the runtime validators/types, and src/components/workspace/messages/human-input-card.tsx renders the reusable card. MessageList owns answered/latest/pending state for visible cards, but derives answered responses from raw thread.messages because replies are hidden; pending cards clear when the hidden reply appears, when dispatch is dropped, or when a new thread.error reports an async stream failure. Page-level submit callbacks must send a normal human message and put hide_from_ui: true plus the response payload in the fourth sendMessage(..., options) argument as options.additionalKwargs; the third argument remains run context such as { agent_name }. Composer entry points should disable normal bottom input while hasOpenHumanInputRequest(...) is true so users answer through the card and preserve response metadata.

Tool-calling AI messages can contain user-visible text as well as tool_calls. core/messages/utils.ts keeps these turns in an assistant:processing group, and components/workspace/messages/message-group.tsx must render the visible text as a processing step instead of treating the message as only tool metadata. This preserves provider text such as error explanations or "trying another approach" notes during tool-heavy runs.

Key Patterns

  • Server Components by default, "use client" only for interactive components
  • Thread hooks (useThreadStream, useSubmitThread, useThreads) are the primary API interface
  • Thread routes — construct Web UI chat paths through core/threads/utils.ts::pathOfThread(), which percent-encodes both custom agent names and thread IDs before inserting them into route segments
  • LangGraph client is a singleton obtained via getAPIClient() in core/api/
  • Environment validation uses @t3-oss/env-nextjs with Zod schemas (src/env.js). Skip with SKIP_ENV_VALIDATION=1
  • Subtask step history and runtime metadata (core/tasks/) — the subtask card shows a subagent's full step timeline (#3779): its assistant reasoning turns interleaved with the tools it ran. Subtask.steps[] is accumulated live from task_running events (appended via mergeSteps, not overwritten) and backfilled on expand for historical runs by fetchSubtaskSteps, which pages the events endpoint scoped to one task (GET /runs/{runId}/events?event_types=subagent.step&task_id=…&after_seq=…) until a short page, so the run-wide limit can't truncate the timeline. task_started carries the effective model_name; task_running carries a cumulative usage snapshot after each completed LLM call. core/tasks/lifecycle.ts normalizes these additive events, and computeNextSubtask keeps the largest cumulative total so replayed or late SSE frames cannot double-count or roll the folded card backward. Terminal ToolMessage metadata (subagent_model_name / subagent_token_usage) restores the same values from normal history after reload; no per-card event fetch is needed. core/tasks/steps.ts is the pure step model: messageToStep (live), eventsToSteps (reload), mergeSteps (dedup by message_index), and stepsForDisplay (what the card renders — keeps tool steps + AI steps with text, drops the trailing final-answer AI step when completed since it's shown as result). core/tasks/context.tsx's useUpdateSubtask applies updates against a tasksRef mirroring the latest state (not a closure snapshot), so a late-resolving fetchSubtaskSteps backfill merges into current state instead of clobbering SSE steps or sibling subtasks that arrived meanwhile. The owning run_id is carried onto history content messages in buildVisibleHistoryMessages so the card can resolve the events endpoint.

Interaction Ownership

  • src/app/workspace/chats/[thread_id]/page.tsx owns composer busy-state wiring.
  • src/app/workspace/chats/[thread_id]/page.tsx owns branch-from-turn submission and navigation; sidecar MessageList instances do not receive the branch action.
  • src/app/workspace/chats/[thread_id]/page.tsx gates the Workspace Browser trigger and browser right panel on /api/features -> browser_control.enabled; default/failed feature discovery hides the browser control so optional backend installs do not show a dead Live socket.
  • src/app/workspace/chats/[thread_id]/page.tsx and src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx own active-goal display state for their composer overlays.
  • src/components/workspace/messages/message-list.tsx owns human-input card answered/latest/pending gating; entry pages only translate a submitted card response into sendMessage calls.
  • src/components/workspace/browser-view/browser-view-panel.tsx forwards each physical pointer click as one click input; do not also emit down/up for the same gesture because the remote Playwright click would run twice.
  • src/core/threads/hooks.ts owns pre-submit upload state and thread submission.

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/utils for conditional Tailwind classes.
  • Path alias: @/* maps to src/*.
  • Components: ui/ and ai-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.

Resources

Contributing

When adding features:

  1. Follow the established src/ structure
  2. Add TypeScript types and proper error handling
  3. Write unit tests under tests/unit/ (pnpm test) and E2E tests under tests/e2e/ (pnpm test:e2e)
  4. Run pnpm check before committing
  5. Update this AGENTS.md when architecture, commands, or conventions change