## Changes 🏗️ Document how to contribute on the Front-end so it is easier for non-regular contributors. ## Checklist 📋 ### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Contribution guidelines make sense and look good considering the AutoGPT stack ### For configuration changes: None
23 KiB
AutoGPT Frontend • Contributing ⌨️
Next.js App Router • Client-first • Type-safe generated API hooks • Tailwind + shadcn/ui
☕️ Summary
This document is your reference for contributing to the AutoGPT Frontend. It adapts legacy guidelines to our current stack and practices.
- Architecture and stack
- Component structure and design system
- Data fetching (generated API hooks)
- Feature flags
- Naming and code conventions
- Tooling, scripts, and testing
- PR process and checklist
This is a living document. Open a pull request any time to improve it.
🚀 Quick Start FAQ
New to the codebase? Here are shortcuts to common tasks:
I need to make a new page
- Create page in
src/app/(platform)/your-feature/page.tsx - If it has logic, create
usePage.tshook next to it - Create sub-components in
components/folder - Use generated API hooks for data fetching
- If page needs auth, ensure it's in the
(platform)route group
Example structure:
app/(platform)/dashboard/
page.tsx
useDashboardPage.ts
components/
StatsPanel/
StatsPanel.tsx
useStatsPanel.ts
See Component structure and Styling and Data fetching patterns sections.
I need to update an existing component in a page
- Find the page
src/app/(platform)/your-feature/page.tsx - Check its
components/folder - If needing to update its logic, check the
use[Component].tshook - If the update is related to rendering, check
[Component].tsxfile
See Component structure and Styling sections.
I need to make a new API call and show it on the UI
- Ensure the backend endpoint exists in the OpenAPI spec
- Regenerate API client:
pnpm generate:api - Import the generated hook by typing the operation name (auto-import)
- Use the hook in your component/custom hook
- Handle loading, error, and success states
Example:
import { useGetV2ListLibraryAgents } from "@/app/api/__generated__/endpoints/library/library";
export function useAgentList() {
const { data, isLoading, isError, error } = useGetV2ListLibraryAgents();
return {
agents: data?.data || [],
isLoading,
isError,
error,
};
}
See Data fetching patterns for more examples.
I need to create a new component in the Design System
- Determine the atomic level: atom, molecule, or organism
- Create folder:
src/components/[level]/ComponentName/ - Create
ComponentName.tsx(render logic) - If logic exists, create
useComponentName.ts - Create
ComponentName.stories.tsxfor Storybook - Use Tailwind + design tokens (avoid hardcoded values)
- Only use Phosphor icons
- Test in Storybook:
pnpm storybook - Verify in Chromatic after PR
Example structure:
src/components/molecules/DataCard/
DataCard.tsx
DataCard.stories.tsx
useDataCard.ts
See Component structure and Styling sections.
📟 Contribution process
1) Branch off dev
- Branch from
devfor features and fixes - Keep PRs focused (aim for one ticket per PR)
- Use conventional commit messages with a scope (e.g.,
feat(frontend): add X)
2) Feature flags
If a feature will ship across multiple PRs, guard it with a flag so we can merge iteratively.
- Use LaunchDarkly based flags (see Feature Flags below)
- Avoid long-lived feature branches
3) Open PR and get reviews ✅
Before requesting review:
- Code follows architecture and conventions here
pnpm format && pnpm lint && pnpm typespass- Relevant tests pass locally:
pnpm test(and/or Storybook tests) - If touching UI, validate against our design system and stories
4) Merge to dev
- Use squash merges
- Follow conventional commit message format for the squash title
📂 Architecture & Stack
Next.js App Router
- We use the Next.js App Router in
src/app - Use route segments with semantic URLs; no
pages/
Component good practices
- Default to client components
- Use server components only when:
- SEO requires server-rendered HTML, or
- Extreme first-byte performance justifies it
- If you render server-side data, prefer server-side prefetch + client hydration (see examples below and React Query SSR & Hydration)
- Prefer using Next.js API routes when possible over server actions
- Keep components small and simple
- favour composition and splitting large components into smaller bits of UI
- colocate state when possible
- keep render/side-effects split for separation of concerns
- do not over-complicate or re-invent the wheel
❓ Why a client-side first design vs server components/actions?
While server components and actions are cool and cutting-edge, they introduce a layer of complexity which not always justified by the benefits they deliver. Defaulting to client-first keeps things simple in the mental model of the developer, specially for those developers less familiar with Next.js or heavy Front-end development.
Data fetching: prefer generated API hooks
- We generate a type-safe client and React Query hooks from the backend OpenAPI spec via Orval
- Prefer the generated hooks under
src/app/api/__generated__/endpoints/... - Treat
BackendAPIand code undersrc/lib/autogpt-server-api/*as deprecated; do not introduce new usages - Use Zod schemas from the generated client where applicable
State management
- Prefer React Query for server state, colocated near consumers (see state colocation)
- Co-locate UI state inside components/hooks; keep global state minimal
Styling and components
- Tailwind CSS + shadcn/ui (Radix Primitives under the hood)
- Use the design system under
src/componentsfor primitives and building blocks - Do not use anything under
src/components/_legacy__; migrate away from it when touching old code - Reference the design system catalog on Chromatic:
https://dev--670f94474adee5e32c896b98.chromatic.com/ - Use the
tailwind-scrollbarplugin utilities for scrollbar styling
🧱 Component structure
For components, separate render logic from data/behavior, and keep implementation details local.
Most components should follow this structure. Pages are just bigger components made of smaller ones, and sub-components can have their own nested sub-components when dealing with complex features.
Basic structure
When a component has non-trivial logic:
FeatureX/
FeatureX.tsx (render logic only)
useFeatureX.ts (hook; data fetching, behavior, state)
helpers.ts (pure helpers used by the hook)
components/ (optional, subcomponents local to FeatureX)
Example: Page with nested components
// Page composition
app/(platform)/dashboard/
page.tsx
useDashboardPage.ts
components/ # (Sub-components the dashboard page is made of)
StatsPanel/
StatsPanel.tsx
useStatsPanel.ts
helpers.ts
components/ # (Sub-components belonging to StatsPanel)
StatCard/
StatCard.tsx
ActivityFeed/
ActivityFeed.tsx
useActivityFeed.ts
Guidelines
- Prefer function declarations for components and handlers
- Only use arrow functions for small inline lambdas (e.g., in
map) - Avoid barrel files and
index.tsre-exports - Keep component files focused and readable; push complex logic to
helpers.ts - Abstract reusable, cross-feature logic into
src/services/orsrc/lib/utils.tsas appropriate - Build components encapsulated so they can be easily reused and abstracted elsewhere
- Nest sub-components within a
components/folder when they're local to the parent feature
Exceptions
When to simplify the structure:
Small hook logic (3-4 lines)
If the hook logic is minimal, keep it inline with the render function:
export function ActivityAlert() {
const [isVisible, setIsVisible] = useState(true);
if (!isVisible) return null;
return (
<Alert onClose={() => setIsVisible(false)}>New activity detected</Alert>
);
}
Render-only components
Components with no hook logic can be direct files in components/ without a folder:
components/
ActivityAlert.tsx (render-only, no folder needed)
StatsPanel/ (has hook logic, needs folder)
StatsPanel.tsx
useStatsPanel.ts
Hook file structure
When separating logic into a custom hook:
// useStatsPanel.ts
export function useStatsPanel() {
const [data, setData] = useState<Stats[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetchStats().then(setData);
}, []);
return {
data,
isLoading,
refresh: () => fetchStats().then(setData),
};
}
Rules:
- Always return an object that exposes data and methods to the view
- Export a single function named after the component (e.g.,
useStatsPanelforStatsPanel.tsx) - Abstract into helpers.ts when hook logic grows large, so the hook file remains readable by scanning without diving into implementation details
🔄 Data fetching patterns
All API hooks are generated from the backend OpenAPI specification using Orval. The hooks are type-safe and follow the operation names defined in the backend API.
How to discover hooks
Most of the time you can rely on auto-import by typing the endpoint or operation name. Your IDE will suggest the generated hooks based on the OpenAPI operation IDs.
Examples of hook naming patterns:
GET /api/v1/notifications→useGetV1GetNotificationPreferencesPOST /api/v2/store/agents→usePostV2CreateStoreAgentDELETE /api/v2/store/submissions/{id}→useDeleteV2DeleteStoreSubmissionGET /api/v2/library/agents→useGetV2ListLibraryAgents
Pattern: use{Method}{Version}{OperationName}
You can also explore the generated hooks by browsing src/app/api/__generated__/endpoints/ which is organized by API tags (e.g., auth, store, library).
OpenAPI specs:
- Production: https://backend.agpt.co/openapi.json
- Staging: https://dev-server.agpt.co/openapi.json
Generated hooks (client)
Prefer the generated React Query hooks (via Orval + React Query):
import { useGetV1GetNotificationPreferences } from "@/app/api/__generated__/endpoints/auth/auth";
export function PreferencesPanel() {
const { data, isLoading, isError } = useGetV1GetNotificationPreferences({
query: {
select: (res) => res.data,
},
});
if (isLoading) return null;
if (isError) throw new Error("Failed to load preferences");
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
Generated mutations (client)
import { useQueryClient } from "@tanstack/react-query";
import {
useDeleteV2DeleteStoreSubmission,
getGetV2ListMySubmissionsQueryKey,
} from "@/app/api/__generated__/endpoints/store/store";
export function DeleteSubmissionButton({
submissionId,
}: {
submissionId: string;
}) {
const queryClient = useQueryClient();
const { mutateAsync: deleteSubmission, isPending } =
useDeleteV2DeleteStoreSubmission({
mutation: {
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: getGetV2ListMySubmissionsQueryKey(),
});
},
},
});
async function onClick() {
await deleteSubmission({ submissionId });
}
return (
<button disabled={isPending} onClick={onClick}>
Delete
</button>
);
}
Server-side prefetch + client hydration
Use server-side prefetch to improve TTFB while keeping the component tree client-first (see React Query SSR & Hydration):
// in a server component
import { getQueryClient } from "@/lib/tanstack-query/getQueryClient";
import { HydrationBoundary, dehydrate } from "@tanstack/react-query";
import {
prefetchGetV2ListStoreAgentsQuery,
prefetchGetV2ListStoreCreatorsQuery,
} from "@/app/api/__generated__/endpoints/store/store";
export default async function MarketplacePage() {
const queryClient = getQueryClient();
await Promise.all([
prefetchGetV2ListStoreAgentsQuery(queryClient, { featured: true }),
prefetchGetV2ListStoreAgentsQuery(queryClient, { sorted_by: "runs" }),
prefetchGetV2ListStoreCreatorsQuery(queryClient, {
featured: true,
sorted_by: "num_agents",
}),
]);
return (
<HydrationBoundary state={dehydrate(queryClient)}>
{/* Client component tree goes here */}
</HydrationBoundary>
);
}
Notes:
- Do not introduce new usages of
BackendAPIorsrc/lib/autogpt-server-api/* - Keep transformations and mapping logic close to the consumer (hook), not in the view
⚠️ Error handling
The app has multiple error handling strategies depending on the type of error:
Render/runtime errors
Use <ErrorCard /> to display render or runtime errors gracefully:
import { ErrorCard } from "@/components/molecules/ErrorCard";
export function DataPanel() {
const { data, isLoading, isError, error } = useGetData();
if (isLoading) return <Skeleton />;
if (isError) return <ErrorCard error={error} />;
return <div>{data.content}</div>;
}
API mutation errors
Display mutation errors using toast notifications:
import { useToast } from "@/components/ui/use-toast";
export function useUpdateSettings() {
const { toast } = useToast();
const { mutateAsync: updateSettings } = useUpdateSettingsMutation({
mutation: {
onError: (error) => {
toast({
title: "Failed to update settings",
description: error.message,
variant: "destructive",
});
},
},
});
return { updateSettings };
}
Manual Sentry capture
When needed, you can manually capture exceptions to Sentry:
import * as Sentry from "@sentry/nextjs";
try {
await riskyOperation();
} catch (error) {
Sentry.captureException(error, {
tags: { context: "feature-x" },
extra: { metadata: additionalData },
});
throw error;
}
Global error boundaries
The app has error boundaries already configured to:
- Capture uncaught errors globally and send them to Sentry
- Display a user-friendly error UI when something breaks
- Prevent the entire app from crashing
You don't need to wrap components in error boundaries manually unless you need custom error recovery logic.
🚩 Feature Flags
- Flags are powered by LaunchDarkly
- Use the helper APIs under
src/services/feature-flags
Check a flag in a client component:
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
export function AgentActivityPanel() {
const enabled = useGetFlag(Flag.AGENT_ACTIVITY);
if (!enabled) return null;
return <div>Feature is enabled!</div>;
}
Protect a route or page component:
import { withFeatureFlag } from "@/services/feature-flags/with-feature-flag";
export const MyFeaturePage = withFeatureFlag(function Page() {
return <div>My feature page</div>;
}, "my-feature-flag");
Local dev and Playwright:
- Set
NEXT_PUBLIC_PW_TEST=trueto use mocked flag values during local development and tests
Adding new flags:
- Add the flag to the
Flagenum andFlagValuestype - Provide a mock value in the mock map
- Configure the flag in LaunchDarkly
📙 Naming conventions
General:
- Variables and functions should read like plain English
- Prefer
constoverletunless reassignment is required - Use searchable constants instead of magic numbers
Files:
- Components and hooks:
PascalCasefor component files,camelCasefor hooks - Other files:
kebab-case - Do not create barrel files or
index.tsre-exports
Types:
- Prefer
interfacefor object shapes - Component props should be
interface Props { ... } - Use precise types; avoid
anyand unsafe casts
Parameters:
- If more than one parameter is needed, pass a single
Argsobject for clarity
Comments:
- Keep comments minimal; code should be clear by itself
- Only document non-obvious intent, invariants, or caveats
Functions:
- Prefer function declarations for components and handlers
- Only use arrow functions for small inline callbacks
Control flow:
- Use early returns to reduce nesting
- Avoid catching errors unless you handle them meaningfully
🎨 Styling
- Use Tailwind utilities; prefer semantic, composable class names
- Use shadcn/ui components as building blocks when available
- Use the
tailwind-scrollbarutilities for scrollbar styling - Keep responsive and dark-mode behavior consistent with the design system
Additional requirements:
- Do not import shadcn primitives directly in feature code; only use components exposed in our design system under
src/components. shadcn is a low-level skeleton we style on top of and is not meant to be consumed directly. - Prefer design tokens over Tailwind's default theme whenever possible (e.g., color, spacing, radius, and typography tokens). Avoid hardcoded values and default palette if a token exists.
⚠️ Errors and ⏳ Loading
- Errors: Use the
ErrorCardcomponent from the design system to display API/HTTP errors and retry actions. Keep error derivation/mapping in hooks; pass the final message to the component.- Component:
src/components/molecules/ErrorCard/ErrorCard.tsx
- Component:
- Loading: Use the
Skeletoncomponent(s) from the design system for loading states. Favor domain-appropriate skeleton layouts (lists, cards, tables) over spinners.- See Storybook examples under Atoms/Skeleton for patterns.
🧭 Responsive and mobile-first
- Build mobile-first. Ensure new UI looks great from a 375px viewport width (iPhone SE) upwards.
- Validate layouts at common breakpoints (375, 768, 1024, 1280). Prefer stacking and progressive disclosure on small screens.
🧰 State for complex flows
For components/flows with complex state, multi-step wizards, or cross-component coordination, prefer a small co-located store using Zustand.
Guidelines:
- Co-locate the store with the feature (e.g.,
FeatureX/store.ts). - Expose typed selectors to minimize re-renders.
- Keep effects and API calls in hooks; stores hold state and pure actions.
Example: simple store with selectors
import { create } from "zustand";
interface WizardState {
step: number;
data: Record<string, unknown>;
next(): void;
back(): void;
setField(args: { key: string; value: unknown }): void;
}
export const useWizardStore = create<WizardState>((set) => ({
step: 0,
data: {},
next() {
set((state) => ({ step: state.step + 1 }));
},
back() {
set((state) => ({ step: Math.max(0, state.step - 1) }));
},
setField({ key, value }) {
set((state) => ({ data: { ...state.data, [key]: value } }));
},
}));
// Usage in a component (selectors keep updates scoped)
function WizardFooter() {
const step = useWizardStore((s) => s.step);
const next = useWizardStore((s) => s.next);
const back = useWizardStore((s) => s.back);
return (
<div className="flex items-center gap-2">
<button onClick={back} disabled={step === 0}>Back</button>
<button onClick={next}>Next</button>
</div>
);
}
Example: async action coordinated via hook + store
// FeatureX/useFeatureX.ts
import { useMutation } from "@tanstack/react-query";
import { useWizardStore } from "./store";
export function useFeatureX() {
const setField = useWizardStore((s) => s.setField);
const next = useWizardStore((s) => s.next);
const { mutateAsync: save, isPending } = useMutation({
mutationFn: async (payload: unknown) => {
// call API here
return payload;
},
onSuccess(data) {
setField({ key: "result", value: data });
next();
},
});
return { save, isSaving: isPending };
}
🖼 Icons
- Only use Phosphor Icons. Treat all other icon libraries as deprecated for new code.
- Package:
@phosphor-icons/react - Site:
https://phosphoricons.com/
- Package:
Example usage:
import { Plus } from "@phosphor-icons/react";
export function CreateButton() {
return (
<button type="button" className="inline-flex items-center gap-2">
<Plus size={16} />
Create
</button>
);
}
🧪 Testing & Storybook
- End-to-end: Playwright (
pnpm test,pnpm test-ui) - Storybook for isolated UI development (
pnpm storybook/pnpm build-storybook) - For Storybook tests in CI, see
@storybook/test-runner(test-storybook:ci) - When changing components in
src/components, update or add stories and visually verify in Storybook/Chromatic
🛠 Tooling & Scripts
Common scripts (see package.json for full list):
pnpm dev— Start Next.js dev server (generates API client first)pnpm build— Build for productionpnpm start— Start production serverpnpm lint— ESLint + Prettier checkpnpm format— Format codepnpm types— Type-checkpnpm storybook— Run Storybookpnpm test— Run Playwright tests
Generated API client:
pnpm generate:api— Fetch OpenAPI spec and regenerate the client
✅ PR checklist (Frontend)
- Client-first: server components only for SEO or extreme TTFB needs
- Uses generated API hooks; no new
BackendAPIusages - UI uses
src/componentsprimitives; no new_legacy__components - Logic is separated into
use*.tsandhelpers.tswhen non-trivial - Reusable logic extracted to
src/services/orsrc/lib/utils.tswhen appropriate - Navigation uses the Next.js router
- Lint, format, type-check, and tests pass locally
- Stories updated/added if UI changed; verified in Storybook
♻️ Migration guidance
When touching legacy code:
- Replace usages of
src/components/_legacy__/*with the modern design system components undersrc/components - Replace
BackendAPIorsrc/lib/autogpt-server-api/*with generated API hooks - Move presentational logic into render files and data/behavior into hooks
- Keep one-off transformations in local
helpers.ts; move reusable logic tosrc/services/orsrc/lib/utils.ts
📚 References
- Design system (Chromatic):
https://dev--670f94474adee5e32c896b98.chromatic.com/ - Project README for setup and API client examples:
autogpt_platform/frontend/README.md - Conventional Commits: conventionalcommits.org