OpenCut/AGENTS.md

4.7 KiB

AGENTS.md

Overview

Privacy-first video editor, with a focus on simplicity and ease of use.

Lib vs Utils

  • lib/ - domain logic (specific to this app)
  • utils/ - small helper utils (generic, could be copy-pasted into any other app)

Core Editor System

The editor uses a singleton EditorCore that manages all editor state through specialized managers.

Architecture

EditorCore (singleton)
├── playback: PlaybackManager
├── timeline: TimelineManager
├── scene: SceneManager
├── project: ProjectManager
├── media: MediaManager
└── renderer: RendererManager

When to Use What

In React Components

Always use the useEditor() hook:

import { useEditor } from '@/hooks/use-editor';

function MyComponent() {
  const editor = useEditor();
  const tracks = editor.timeline.getTracks();

  // Call methods
  editor.timeline.addTrack({ type: 'media' });

  // Display data (auto re-renders on changes)
  return <div>{tracks.length} tracks</div>;
}

The hook:

  • Returns the singleton instance
  • Subscribes to all manager changes
  • Automatically re-renders when state changes

Outside React Components

Use EditorCore.getInstance() directly:

// In utilities, event handlers, or non-React code
import { EditorCore } from "@/core";

const editor = EditorCore.getInstance();
await editor.export({ format: "mp4", quality: "high" });

Actions System

Actions are the trigger layer for user-initiated operations. The single source of truth is @/lib/actions/definitions.ts.

To add a new action:

  1. Add it to ACTIONS in @/lib/actions/definitions.ts:
export const ACTIONS = {
  "my-action": {
    description: "What the action does",
    category: "editing",
    defaultShortcuts: ["ctrl+m"],
  },
  // ...
};
  1. Add handler in @/hooks/use-editor-actions.ts:
useActionHandler(
  "my-action",
  () => {
    // implementation
  },
  undefined,
);

In components, use invokeAction() for user-triggered operations:

import { invokeAction } from '@/lib/actions';

// Good - uses action system
const handleSplit = () => invokeAction("split-selected");

// Avoid - bypasses UX layer (toasts, validation feedback)
const handleSplit = () => editor.timeline.splitElements({ ... });

Direct editor.xxx() calls are for internal use (commands, tests, complex multi-step operations).

Commands System

Commands handle undo/redo. They live in @/lib/commands/ organized by domain (timeline, media, scene).

Each command extends Command from @/lib/commands/base-command and implements:

  • execute() - saves current state, then does the mutation
  • undo() - restores the saved state

Actions and commands work together: actions are "what triggered this", commands are "how to do it (and undo it)".

Cloud-specific instructions

Prerequisites

  • Bun (v1.2.18) must be installed. The update script handles this via ~/.bun/bin/bun.
  • No Docker required for frontend-only development. Docker (PostgreSQL + Redis) is only needed for auth features, and no docker-compose.yaml exists in the repo yet.

Environment variables

All env vars are validated by Zod at startup (packages/env/src/web.ts). Every field is required (no .optional()), so .env.local must exist under apps/web/ with valid-looking values for all variables. For local development without external services, placeholder values work for: MARBLE_WORKSPACE_KEY, FREESOUND_CLIENT_ID, FREESOUND_API_KEY, CLOUDFLARE_ACCOUNT_ID, R2_*, and MODAL_TRANSCRIPTION_URL (must be a valid URL like http://localhost:9999/transcription).

Running services

Service Command Notes
Dev server bun run dev (from apps/web) Runs Next.js 16 with Turbopack on port 3000
Lint bun run lint (from apps/web) Uses Biome. Pre-existing CSS/Tailwind directive warnings are expected.
Tests bun test (from repo root) 60 tests across 7 files (storage migrations + sticker-id)
Format bun run format (from apps/web) Biome formatter

Gotchas

  • @biomejs/biome is a root devDependency needed for the lint/format scripts in apps/web. The lint script calls biome directly.
  • The env schema at packages/env/src/web.ts uses z.url() for several fields (NEXT_PUBLIC_SITE_URL, NEXT_PUBLIC_MARBLE_API_URL, UPSTASH_REDIS_REST_URL, MODAL_TRANSCRIPTION_URL), meaning they must be full valid URLs (with protocol). Plain strings will fail validation.
  • The repo's README references docker-compose up -d but no docker-compose.yaml exists. Auth features (login, signup) won't work without PostgreSQL and Redis.