Imported from Continuum-AI-Agency/Continuum-Frontend (
AGENTS.md). Install upstream withnpx skills add Continuum-AI-Agency/Continuum-Frontend. Copyright stays with the author.
AGENTS.md — Continuum Frontend
Monorepo conventions live at
../AGENTS.md. Read that first for layout, shared infrastructure, cross-project communication, env handling, and engineering principles. This file covers Frontend-specific practices only (Next.js patterns, Server Components, canvas modules, planner gotchas).
This document outlines the architectural and code quality rules specific to the Continuum Frontend (Next.js 16 + Bun + Vercel). Universal craftsmanship principles (Clean Code / TDD / boundary defense) are in the root AGENTS.md — they apply here too.
1. Architecture (Next.js / App Router)
Server-First Principle & Rendering
- Default to RSC/SSR: All new components must default to being a React Server Component (RSC). We must utilize Server-Side Rendering (SSR) via RSCs and Next.js caching to achieve optimal performance, faster initial load, and minimized client-side JavaScript bundle size. Use Client Components only for interactivity.
- The "use client" Boundary: The
"use client"directive must be placed at the highest possible level of the component tree. Client Components should be small leaves that handle UI interaction, wrapping RSC-rendered content when possible (e.g., a Client button wraps Server-rendered content).
Data Access & Supabase
- Server-Side Data Only: Direct database access (via Supabase SDK, PostgREST queries, or Server Actions) is strictly forbidden in Client Components. All data fetching must be proxied through:
- RSCs: Direct
fetch()calls or Server Actions. - Route Handlers (
route.ts): For public/unauthenticated API endpoints.
- RSCs: Direct
- Next.js Caching: Utilize Next.js native
fetchcaching and revalidation (next: { revalidate }) to ensure predictable data freshness and optimal performance (SSR, SSG, ISR).
API Layer (src/lib/api/)
- Browser → agents-ts: Client components call the agents-ts backend directly using
http.request()fromsrc/lib/api/http.ts. This client resolvesgetApiBaseUrl()and attachesAuthorization: Bearer ${token}automatically viagetBrowserAccessToken(). Do NOT add Next.js route handlers as thin auth-forwarding proxies — they add indirection with no benefit. Only add a route handler when genuinely needed server-side (e.g., streaming responses, server secrets, PostHog analytics).
Shared Contracts (@continuum/contracts) — MANDATORY for response interpreters
Frontend response interpreters that parse Backend agent outputs (NDJSON stream frames, agent output objects, HTTP envelopes) must import their types and Zod schemas from @continuum/contracts. The Backend emits frames defined in the same package, so a safeParse against the imported schema is the boundary check.
- Where it lives:
packages/contracts/src/streaming/<domain>.tsfor stream frames;packages/contracts/src/<domain>/for HTTP request/response. - How to import:
import { organicStreamFrameSchema, type OrganicStreamFrame } from '@continuum/contracts'— root entry only (kept consistent with Backend, which can't use subpath imports undermoduleResolution: "node"). - Pattern: see
src/components/organic/agent/streamEventParser.ts— it importsorganicStreamFrameSchema, runssafeParseagainst incoming frames, and uses the inferred type for compile-time linkage with the Backend emit side. - Forbidden: hand-rolling a TypeScript stream-event union in
src/lib/<domain>/stream.tsorsrc/components/<domain>/streamEventParser.tswhen the Backend has its own parallel hand-rolled union. New event types are added inpackages/contracts/first, then the interpreter switch-case imports the literal type. - Jaina note: the heavy Zod schemas in
src/lib/jaina/schemas.ts(3000+ lines) are scheduled for migration intopackages/contracts/src/streaming/jaina.tsin a follow-up PR; for now, the cross-side type linkage is via theJainaStreamEventtype alias in contracts. - Refer to: root
AGENTS.md§4 Shared contracts for the cross-project policy.
State Management
- Component State First: Prefer
useStateoruseReducerfor local, component-specific state. - Context for UI/Cross-Cutting: React Context is reserved only for low-frequency UI concerns like theme settings or global modal state. No application-specific data (e.g., lists of users, posts) should live in Context. External state libraries are prohibited unless cleared by the architectural lead.
- Local Storage Use:
localStorageis permitted only for non-critical, ephemeral UI state that needs to persist across sessions (e.g., remembering a user's chosen theme, the last-used layout for a dashboard, or the open/closed status of a panel). It must never be used for synchronization, storing sensitive data, or critical application data.
2. UI/UX and Dependencies
Styling & Accessibility
- Style Guide Adherence: All design decisions, spacing, color palettes, and typographic rules must strictly adhere to the guidelines set in the
styleguide.mddocument. - Tailwind Preference: We prioritize Tailwind CSS 4 utility classes. Custom CSS must be minimal and only used when utilities are insufficient.
- shadcn + registry for Foundation: All interactive primitives (buttons, menus, dialogs, badges/pills) come from shadcn
ui/*and the vetted registries (@kibo-ui,@bklit/BasedKit,@supabase) — all built on Base UI (@base-ui/react) for accessibility (ARIA, focus, keyboard). The shadcn preset isbase-nova(components.jsonstyle; thebasefield is derived from that prefix, not written). Radix has been removed — do not reintroduce@radix-ui/*or@radix-ui/themes. The one exception is whatcmdkpulls in transitively for the command palette, which is why the rootoverridesstill pins@radix-ui/react-primitive. This is a settled decision, not an oversight — do not "finish" it by porting the palette to Base UICombobox. Measured on 50 real palette labels: Base UI's defaultcreateCollatorItemFilteris anIntl.Collatorsubstring match with no ranking, and returns zero results for abbreviation queries cmdk handles fine (gp,gopl,cnct,rprt). Itsfilterprop is a boolean predicate, so cmdk'scommand-scorecould restore matching but not score ordering — that needs items pre-sorted as data, which the children-based 9-export wrapper API cannot express. Removingcmdktherefore costs a fuzzy-matcher port plus a wrapper API change across 27 consumers, to drop one dependency. shadcn agrees: its ownbase-novacommandcomponent is cmdk-backed (npx shadcn view command -c Continuum-Frontendshows"dependencies": ["cmdk"]), so "move the palette to the shadcn Base UI implementation" means pulling that component and KEEPING cmdk — not porting toCombobox. Two traps when you re-pull it:shadcn add commandalso overwritesbutton/input/textarea/dialog/input-group, so revert those unless you meant to take them; and the registry'sCommandDialogrenders a bareDialogContent, assuming each caller supplies its own<Command>root. Every call site here passesCommandInput/CommandListstraight in, and cmdk children read their store from context — with no root the store isundefined,CommandInputthrows on.subscribe, andglobal-error.tsxrenders it as a literal "500 Something went wrong" page. Keep the<Command>wrapper insideCommandDialog.command.filter.test.tsxandcommand-palette:e2e:benchboth fail if the scoring is ever lost. Base UI has noasChild: pass arenderelement instead, and note that a link styled as a button must usebuttonVariants()on the<Link>rather than<Button render={<Link/>}>— Base UI's Button injects button semantics that destroy the anchor's link role. Base UI has noSloteither;useRender+mergePropsis the equivalent. Icons are lucide (@radix-ui/react-iconsis gone); lucide ships no brand glyphs, so brand marks come from@/components/shared/icons. Status/label pills use the canonical@/components/kibo-ui/pill(Pill+PillIndicator/PillDelta…); layout/typography use raw Tailwind. - Framer Motion: Use Framer Motion variants for defined animation states. Complex animations must be lazy-loaded.
Forms
- Stack: All forms must use the React Hook Form + Zod stack for controlled, performant, and type-safe validation.
- Dual Validation: Client-side Zod validation provides fast user feedback. Server-side validation (in Server Actions or Route Handlers) is mandatory to enforce invariants and security rules.
3. Linear Workflow
Our Linear workspace is the source of truth for delivery planning. Estimation scale, issue chunking, project/status flow and close-the-loop rules: the linear-workflow skill.
4. MCP Usage
- Purpose: MCP tools are for reference and guidance only; they must not mutate production data or state.
- Supabase MCP (Read-Only): Use it for looking up schemas, tables, migrations, and query behavior to inform frontend work. Do not run write operations or migrations unless explicitly requested and approved. This is the only MCP server configured in the repo's
.mcp.json. - shadcn + registry first: Prefer shadcn
ui/*and registry components (Kibo/BasedKit/Supabase, built on Base UI) over custom equivalents. Fit registry recipes todocs/styleguide.md. - Document Gaps: If MCP data is missing or unclear, state assumptions and ask for clarification rather than guessing.
5. Tests
- Don't cheat, don't be lazy, just be Honest. Write tests that cover TRUE functionality. NEVER simulate a pass condition, and it should always attempt the function's intended behavior.
- Tests are Atomized Tests should cover the smallest amount of functions at a time, to give clarity on what is breaking. A test can have multiple calls/arguments at a time, but it should be for the function they are covering.
- Tests are Critical Tests are critical to effective codebases, functions, and behavior. Always write the full test, covering 100% of functionality.
- You can Iterate on them yourself Running
bun test path/to/functionruns that singular test.bun run testsruns all frontend tests. You may run them iteratively within your context in order to confirm your work. - Bun test only Use
bun:testfor test APIs. Do not add Vitest configs, Vitest scripts, orvitestdependencies. - Logging in Edge Functions Putting logs into Edge Functions is critical for tracing and debugging.
Plan Mode
- Make the plan extremely concise. Sacrifice grammar for the sake of concision.
- At the end of each plan, give me a list of unresolved questions to answer, if any.
Do NOT write comments every line. Write comments at the top of the file if necessary. Do NOT use emojis to write comments.
Use skills aggressively.
When you learn something, place a note of it here:
- If a rebase regresses the planner UI, restore
OrganicCalendarWorkspaceClient+TimeGridCanvasto thePlannerHeader/PlannerMatrixpath and keep DnD targets in theplanner-cell::day::platformformat. - Keep trends UI minimal: use
TrendWorkbench(no momentum chart/filter chips) and keep seeded/quick drafts tag-free (tags: []) while relying onseedTrendId. - Coming-soon platform rows in the planner should render in compact/collapsed density so they consume minimal vertical space versus active scheduling rows.
- Planner cells should display all same-day posts (scrollable stack) and support posting-time edits via card quick actions (
Time: ...presets +Time: Custom...). - Keep theming synchronized via both
data-themeandhtml.dark/html.lightso Tailwinddark:utilities, CSS variables, and chart/map theme detection stay consistent. - Keep skeleton loaders neutral (
bg-muted/70), and shape each skeleton to match real page structure (headers, controls, cards, tables) instead of generic full-bleed blocks. StringNodeenrichment payload sends uploads undercontext.images; the/api/ai-studio/enrichroute should parsecontext.*and stream SSE (text+complete) foruseWorkflowExecution.- Avoid self-referential RLS policies on
brand_profiles.permissions; they can trigger PostgreSQL54001(statement_too_complex). Usebrand_profiles.has_brand_access(...)as aSECURITY DEFINERhelper for cross-row visibility checks. - Paid media observability should use a two-step exploration model:
Campaignssnapshot first (including index-level averaged cards), thenAd Setsdrill-in with a single selected ad set timeline and one metric visualized at a time. - Campaign compare should support TradingView-style multi-entity plotting: add/remove multiple campaigns/indexes to one metric chart, and keep indexes grouped by default with an optional
Decomposetoggle to expose member campaigns. - Minimize dropdown-driven selection for paid media exploration: prefer left-rail ticker-style list menus for campaign/index/ad set selection with quick row actions and lightweight chart focus.
- Replace full-page paid media action logs with a compact, context-aware alert feed (search + status filter + sort + pagination) so the primary chart/explorer keeps most visual real estate.
- In brand integration assignment UIs, always merge brand-assigned integration accounts into selectable assets so invited members can view and keep existing brand-linked accounts even without personal OAuth ownership.
- Brand integration assignment UIs must render Meta
assets_without_ad_account; standalone Instagram business accounts can be personal-connected without an ad-account parent and still need to be assignable to the brand. - Impersonation callbacks must persist
is_impersonating(for/auth/callbackand/callbackpaths) so middleware bypasses the/set-passwordrequirement for admin impersonation sessions. - Use the
fetch-brand-integrationsEdge Function as the source of truth for brand-assigned assets. It ensures members can see owner-linked integrations by using the Service Role bypass while verifying the requester's brand access. - Avoid redundant permission upserts in brand initialization paths (e.g.
ensureBrandProfileRecord) to prevent role drift for invited members; always sync global brand data (name, logo,completed_at) back to user-scoped onboarding states. - Exclude
/socket.ioand/.well-known/appspecific/*probes from Next middleware matchers so extension/devtools polling does not trigger Supabase auth lookups on 404 noise requests. - Keep paid-media marker placement synchronized with alerts refresh: when
DCOActionAlertsBoxrefreshes, triggerCampaignAdSetWorkspaceaction-log refresh so chart markers re-render against the latest alerts. - In paid-media observability charts, place action markers at the nearest in-window chart timestamp (time-sensitive), render full top markers only for the active layer (
CAMPAIGNin campaign view,ADSETin adset view), and demote non-layer scopes (for exampleAD) to bottom bookmarks. - Timeline bootstrap should fetch the active resolution first and prefetch the opposite resolution best-effort; a secondary-resolution failure (for example upstream
546) must not break paid-media initial load. - Organic planner AI Studio handoff persistence must never assume quota headroom: catch
QuotaExceededError, prune stalecontinuum:organic-planner:ai-studio-context:*draft seeds, and retry with progressively smaller payloads (dropassetBase64, then heavy optional context) before giving up. - For Zustand object selectors in planner client hooks/components, wrap selectors with
useShallowfromzustand/react/shallowto stabilizegetSnapshotidentity and avoid React infinite-loop warnings. - Organic agent session job hydration must tolerate non-array payloads from
GET /api/organic/agent/sessions/{sessionId}/jobs(for example wrapped{ jobs: [...] }/{ data: [...] }responses) and never assumejobsis directly iterable. - Jaina backend Supabase Edge Function calls may use the service-role bearer token instead of a browser user JWT; authenticated Edge functions that are safe for trusted backend use should explicitly accept exact service-role auth instead of calling
auth.getUser(serviceRoleKey). - Never put an opacity modifier on
text-primary/text-secondary. Those are custom utilities inglobals.css(.text-secondary { color: var(--text-secondary) }, a muted grey), but--secondaryis also registered as a Tailwind color (#0ea5e9, electric cyan).text-secondaryrenders grey whiletext-secondary/70silently resolves to the unrelated Tailwind utility and renders cyan — it looks like a link and fails contrast. Usetext-secondary opacity-70instead. - For browser-driven verification (clicking through a UI change, screenshots), use the
agent-browserskill, notclaude-in-chrome— the user has declined the Chrome extension.