Agent - Modes API

The Modes API allows developers to register custom modes and contexts programmatically. This is a powerful API for creating AI assistants tailored to specific workflows.

To demonstrate how powerful the Modes API is, we will dissect the built-in Block Editor mode, which was created entirely using it.

Registering a Mode

Use Agent::registerMode() to register a custom mode from PHP.

Mode Properties

id (string, required)

A unique identifier for the mode. Use lowercase letters and hyphens.

label (string)

The display name shown in the mode selector dropdown.

description (string)

A brief description of what the mode does. Shown in the mode selector.

category (string)

Category for grouping modes in the UI. Built-in categories are General and WordPress.

detection (array)

Optional auto-detection configuration. When set, the mode is automatically selected based on the current context.

requiresCapability (string)

Optional capability required to use this mode.

toolbar (string)

Optional toolbar type. Set to plugin to show the plugin mode toolbar.

agent.systemInstruction (string)

The system instruction sent to the AI. Can also be a function in JavaScript.

agent.canvas (boolean)

Enables the virtual file system for this mode. Default is false.

agent.executeJs (boolean)

Whether the AI can execute JavaScript. Default is false.

script (string)

URL to a JavaScript file that extends the mode with file systems and tools.

Case Study: Block Editor Mode

The Block Editor mode allows the AI to read and edit WordPress blocks. It was built entirely with the Modes API and demonstrates how to create a powerful, context-aware assistant.

Step 1: PHP Registration

The mode is registered in PHP with basic configuration:

            use SleekAi\Agent\Agent;

    Agent::registerMode([
      'id' => 'block-editor',
      'label' => 'Block Editor',
      'description' => 'AI assistant for the WordPress block editor',
      'category' => 'WordPress',
      'detection' => [
        'context' => 'block-editor',
      ],
      'agent' => [
        'canvas' => true,
        'executeJs' => true,
      ],
      'script' => plugins_url('script.js', __FILE__),
    ]);
  
                    
Copy

This tells Agent mode to:

  • Auto-select this mode when the block editor context is detected
  • Enable the virtual file system (canvas: true)
  • Allow JavaScript execution for operations like inserting blocks
  • Load a script that defines the file system and tools

Step 2: Helper Functions

The script starts with helper functions to safely access WordPress APIs:

            const getWpBlockEditor = () => {
      const wp = window.wp;
      if (!wp?.data) return null;
      return wp.data.select("core/block-editor");
    };

    const getWpDispatch = () => {
      const wp = window.wp;
      if (!wp?.data?.dispatch) return null;
      return wp.data.dispatch("core/block-editor");
    };
  
                    
Copy

These ensure the script does not crash if WordPress APIs are unavailable.

Step 3: Transforming Blocks to Virtual Files

Each WordPress block becomes a JSON file. The blockToJson function creates a clean representation of a block:

            const blockToJson = (block, path) => ({
      path,
      clientId: block.clientId,
      name: block.name,
      attributes: block.attributes,
      innerBlocks: block.innerBlocks.map((inner) =>
        blockToJson(inner, `${path}/${inner.clientId}`)
      ),
    });
  
                    
Copy

The transformBlockToFiles function recursively processes blocks and their children, storing each as a file:

            const transformBlockToFiles = (block, files, parentPath = "blocks") => {
      const basePath = `${parentPath}/${block.clientId}`;

      files[`${basePath}/block.json`] = JSON.stringify(
        blockToJson(block, basePath),
        null,
        2,
      );

      block.innerBlocks.forEach((innerBlock) => {
        transformBlockToFiles(innerBlock, files, basePath);
      });
    };
  
                    
Copy

This creates a file structure like:

            blocks/abc123/block.json           # Parent block
    blocks/abc123/def456/block.json    # Child block
    blocks/abc123/def456/ghi789/block.json  # Grandchild
  
                    
Copy

Step 4: The File System Interface

The file system has four parts:

getInitialFiles

Returns all blocks as virtual files when the mode loads:

            const getAllBlocksAsFiles = () => {
      const files = {};
      const blockEditor = getWpBlockEditor();
      if (!blockEditor?.getBlocks) return files;

      const blocks = blockEditor.getBlocks();
      blocks.forEach((block) => {
        transformBlockToFiles(block, files);
      });

      return files;
    };
  
                    
Copy

onBeforeRead

Called when the AI reads a file. Returns fresh data from WordPress instead of cached content, ensuring the AI always sees the current state:

            const parseBlockPath = (path) => {
      const normalizedPath = path.replace(/^\//, "");
      if (!normalizedPath.startsWith("blocks/")) return null;

      const parts = normalizedPath.split("/");
      const fileName = parts[parts.length - 1];
      const clientId = parts[parts.length - 2];

      if (!fileName.endsWith(".json")) return null;
      return { clientId, fileName };
    };

    const onBeforeRead = (path) => {
      const blockEditor = getWpBlockEditor();
      if (!blockEditor) return null;

      const parsed = parseBlockPath(path);
      if (!parsed) return null;

      const block = blockEditor.getBlock(parsed.clientId);
      if (!block) return null;

      if (parsed.fileName === "block.json") {
        const basePath = path.replace(/\/block\.json$/, "");
        return JSON.stringify(blockToJson(block, basePath), null, 2);
      }

      return null;
    };
  
                    
Copy

The parseBlockPath helper extracts the clientId from a path like blocks/abc123/block.json. The onBeforeRead function uses this to fetch the current block data from WordPress.

onFileChange

Called when the AI writes to a file. Applies changes to the actual WordPress block:

            const onFileChange = (path, content) => {
      const blockEditor = getWpBlockEditor();
      const dispatch = getWpDispatch();
      if (!blockEditor || !dispatch) return;

      const parsed = parseBlockPath(path);
      if (!parsed) return;

      const block = blockEditor.getBlock(parsed.clientId);
      if (!block) return;

      try {
        if (parsed.fileName === "block.json") {
          const data = JSON.parse(content);

          // Update block attributes
          if (data.attributes) {
            dispatch.updateBlockAttributes(parsed.clientId, data.attributes);
          }

          // Replace block if type changed
          if (data.name && data.name !== block.name) {
            dispatch.replaceBlock(
              parsed.clientId,
              window.wp.blocks.createBlock(
                data.name,
                data.attributes || block.attributes,
                block.innerBlocks
              ),
            );
          }
        }
      } catch {
        // Ignore parse errors
      }
    };
  
                    
Copy

This function does two things:

  1. Updates attributes: If the AI changes text or settings, they are applied via updateBlockAttributes
  2. Replaces blocks: If the AI changes the block type (e.g., paragraph to heading), the block is replaced entirely

subscribe

Watches for changes in WordPress and updates the virtual file system:

            subscribe: (onUpdate) => {
      const wp = window.wp;
      if (!wp?.data?.subscribe) {
        return () => {};
      }

      let lastBlocksJson = JSON.stringify(getAllBlocksAsFiles());

      const unsubscribe = wp.data.subscribe(() => {
        const newFiles = getAllBlocksAsFiles();
        const newBlocksJson = JSON.stringify(newFiles);

        if (newBlocksJson !== lastBlocksJson) {
          lastBlocksJson = newBlocksJson;
          onUpdate(newFiles);
        }
      });

      return unsubscribe;
    },
  
                    
Copy

This ensures the virtual file system stays in sync when the user edits blocks directly in WordPress. The comparison prevents unnecessary updates when nothing has changed.

Step 5: Adding a Custom Tool

The get_selected_block tool tells the AI which block the user is working with:

            const findBlockPath = (blocks, targetClientId, parentPath = "blocks") => {
      for (const block of blocks) {
        const currentPath = `${parentPath}/${block.clientId}`;
        if (block.clientId === targetClientId) {
          return currentPath;
        }
        if (block.innerBlocks.length > 0) {
          const found = findBlockPath(block.innerBlocks, targetClientId, currentPath);
          if (found) return found;
        }
      }
      return null;
    };

    const tools = [
      {
        name: "get_selected_block",
        description: "Get the currently selected block's client ID and file path",
        schema: {
          type: "object",
          properties: {},
          required: [],
        },
        execute: () => {
          const blockEditor = getWpBlockEditor();
          if (!blockEditor?.getSelectedBlockClientId) {
            return { error: true, message: "Block editor not available" };
          }

          const clientId = blockEditor.getSelectedBlockClientId();
          if (!clientId) {
            return { error: false, clientId: null, message: "No block selected" };
          }

          const blocks = blockEditor.getBlocks?.() || [];
          const path = findBlockPath(blocks, clientId);
          return { error: false, clientId, path };
        },
      },
    ];
  
                    
Copy

The findBlockPath helper is needed because nested blocks have paths like blocks/parent/child/block.json. It recursively searches to find the full path for any block by its clientId.

Step 6: Dynamic System Instructions

The system instruction is generated dynamically to include context about available block types:

            const getBlockTypesInfo = () => {
      const wp = window.wp;
      if (!wp?.blocks?.getBlockTypes) return "";

      const blockTypes = wp.blocks.getBlockTypes();
      return blockTypes
        .filter((block) => block.name.startsWith("core/"))
        .slice(0, 30)
        .map((block) => {
          const attrs = block.attributes || {};
          const textAttrs = Object.entries(attrs)
            .filter(([_, config]) =>
              config.type === "string" || config.type === "rich-text"
            )
            .map(([name]) => name)
            .slice(0, 3);

          const textInfo = textAttrs.length > 0 ? `: ${textAttrs.join(", ")}` : "";
          return `- **${block.name}**${textInfo}`;
        })
        .join("\n");
    };

    const getContextInstructions = () => {
      const blockTypesInfo = getBlockTypesInfo();

      return `You are assisting with the WordPress Block Editor.

    ## File System Structure

    Each block is a JSON file at blocks/{clientId}/block.json containing:
    - name: The block type (e.g., core/paragraph)
    - attributes: The block's settings and content
    - innerBlocks: Nested blocks for containers

    ## Available Block Types

    ${blockTypesInfo}

    ## Workflow

    1. Use get_selected_block to find what the user is working with
    2. Read the block.json to see its current state
    3. Use find_and_replace or write_file to make changes

    ## JavaScript Execution

    Use JavaScript for operations that cannot be done via file edits:
    - Inserting new blocks
    - Removing blocks
    - Reordering blocks`;
    };
  
                    
Copy

The instruction includes available block types with their text attributes, helping the AI understand what it can create and edit.

Step 7: Putting It All Together

Finally, everything is registered using extendMode:

            window.sleekAiAgent.extendMode("block-editor", {
      agent: {
        systemInstruction: getContextInstructions,
        fileSystem: {
          getInitialFiles: getAllBlocksAsFiles,
          onBeforeRead,
          onFileChange,
          subscribe: (onUpdate) => {
            // ... subscription logic
          },
        },
        tools,
      },
    });
  
                    
Copy

The Result

With this setup, the AI can:

  • See all blocks as JSON files it can browse and search
  • Read block content and attributes (always fresh via onBeforeRead)
  • Modify blocks by writing to their virtual files (applied via onFileChange)
  • Know which block is selected via the custom tool
  • Execute JavaScript to insert, delete, or move blocks
  • Stay in sync when the user edits blocks directly

This demonstrates how the Modes API enables you to create AI assistants that deeply integrate with any WordPress feature or custom workflow.

Registering Custom Contexts

Use Agent::registerContext() to register contexts for custom admin screens. See the Context documentation for details.