Files
AutoGPT/autogpt_platform/frontend/CONTRIBUTING.md
Ubbe b7646f3e58 docs(frontend): contributing guidelines (#11210)
## 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
2025-10-21 08:26:51 +00:00

23 KiB
Raw Permalink Blame History

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

  1. Create page in src/app/(platform)/your-feature/page.tsx
  2. If it has logic, create usePage.ts hook next to it
  3. Create sub-components in components/ folder
  4. Use generated API hooks for data fetching
  5. 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

  1. Find the page src/app/(platform)/your-feature/page.tsx
  2. Check its components/ folder
  3. If needing to update its logic, check the use[Component].ts hook
  4. If the update is related to rendering, check [Component].tsx file

See Component structure and Styling sections.

I need to make a new API call and show it on the UI

  1. Ensure the backend endpoint exists in the OpenAPI spec
  2. Regenerate API client: pnpm generate:api
  3. Import the generated hook by typing the operation name (auto-import)
  4. Use the hook in your component/custom hook
  5. 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

  1. Determine the atomic level: atom, molecule, or organism
  2. Create folder: src/components/[level]/ComponentName/
  3. Create ComponentName.tsx (render logic)
  4. If logic exists, create useComponentName.ts
  5. Create ComponentName.stories.tsx for Storybook
  6. Use Tailwind + design tokens (avoid hardcoded values)
  7. Only use Phosphor icons
  8. Test in Storybook: pnpm storybook
  9. 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 dev for 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 types pass
  • 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

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 BackendAPI and code under src/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


🧱 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.ts re-exports
  • Keep component files focused and readable; push complex logic to helpers.ts
  • Abstract reusable, cross-feature logic into src/services/ or src/lib/utils.ts as 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., useStatsPanel for StatsPanel.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/notificationsuseGetV1GetNotificationPreferences
  • POST /api/v2/store/agentsusePostV2CreateStoreAgent
  • DELETE /api/v2/store/submissions/{id}useDeleteV2DeleteStoreSubmission
  • GET /api/v2/library/agentsuseGetV2ListLibraryAgents

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:

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 BackendAPI or src/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=true to use mocked flag values during local development and tests

Adding new flags:

  1. Add the flag to the Flag enum and FlagValues type
  2. Provide a mock value in the mock map
  3. Configure the flag in LaunchDarkly

📙 Naming conventions

General:

  • Variables and functions should read like plain English
  • Prefer const over let unless reassignment is required
  • Use searchable constants instead of magic numbers

Files:

  • Components and hooks: PascalCase for component files, camelCase for hooks
  • Other files: kebab-case
  • Do not create barrel files or index.ts re-exports

Types:

  • Prefer interface for object shapes
  • Component props should be interface Props { ... }
  • Use precise types; avoid any and unsafe casts

Parameters:

  • If more than one parameter is needed, pass a single Args object 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-scrollbar utilities 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 ErrorCard component 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
  • Loading: Use the Skeleton component(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.

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 production
  • pnpm start — Start production server
  • pnpm lint — ESLint + Prettier check
  • pnpm format — Format code
  • pnpm types — Type-check
  • pnpm storybook — Run Storybook
  • pnpm 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 BackendAPI usages
  • UI uses src/components primitives; no new _legacy__ components
  • Logic is separated into use*.ts and helpers.ts when non-trivial
  • Reusable logic extracted to src/services/ or src/lib/utils.ts when 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 under src/components
  • Replace BackendAPI or src/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 to src/services/ or src/lib/utils.ts

📚 References