Skip to main content

Overview

Agent providers allow you to integrate AI assistants and coding agents into React Grab. When configured, users can enter prompt mode to send element context to your agent and receive responses through a streaming interface.

AgentProvider Interface

The AgentProvider interface defines how React Grab communicates with your AI agent:
send
(context: AgentContext, signal: AbortSignal) => AsyncIterable<string>
required
Send element context to the agent and return a stream of response chunks.Returns an async iterable that yields response chunks as strings.
resume
(sessionId: string, signal: AbortSignal, storage: AgentSessionStorage) => AsyncIterable<string>
Resume a previous session (e.g., after page reload). Only called if supportsResume is true.
abort
(sessionId: string) => Promise<void>
Abort an ongoing agent session
supportsResume
boolean
Whether the provider supports resuming sessions
supportsFollowUp
boolean
Whether the provider supports follow-up questions in the same session
dismissButtonText
string
Custom text for the dismiss button (default: “Dismiss”)
checkConnection
() => Promise<boolean>
Check if the agent service is available/connected
getCompletionMessage
() => string | undefined
Return a custom completion message shown when the agent finishes
undo
() => Promise<void>
Undo the last agent action
canUndo
() => boolean
Whether undo is currently available
redo
() => Promise<void>
Redo a previously undone action
canRedo
() => boolean
Whether redo is currently available

AgentContext Interface

Context information passed to the agent:
content
string[]
Array of content strings for each selected element. Typically includes HTML source code and component information.
prompt
string
The user’s prompt or question
options
T | undefined
Custom options provided by the getOptions function
sessionId
string | undefined
Session ID for tracking conversations and follow-ups

AgentOptions Interface

Configuration options for agent integration:
provider
AgentProvider<T>
The agent provider instance
storage
AgentSessionStorage | null
Storage interface for persisting session data. Set to null to disable storage.
getOptions
() => T
Function that returns custom options passed to the provider
onStart
(session: AgentSession, elements: Element[]) => void
Called when an agent session starts
onStatus
(status: string, session: AgentSession) => void
Called when the agent sends a status update
onComplete
(session: AgentSession, elements: Element[]) => AgentCompleteResult | void | Promise<AgentCompleteResult | void>
Called when the agent completes successfully
onError
(error: Error, session: AgentSession) => void
Called when an error occurs
onResume
(session: AgentSession) => void
Called when resuming a session
onAbort
(session: AgentSession, elements: Element[]) => void
Called when the user aborts the session
onUndo
(session: AgentSession, elements: Element[]) => void
Called when the user undoes an action
onDismiss
(session: AgentSession, elements: Element[]) => void
Called when the user dismisses the session

Creating a Basic Agent Provider

Here’s a simple agent provider that sends requests to an API:
import type { AgentProvider, AgentContext } from "react-grab";

const myAgentProvider: AgentProvider = {
  async *send(context: AgentContext, signal: AbortSignal) {
    const response = await fetch("https://api.example.com/chat", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        content: context.content,
        prompt: context.prompt,
      }),
      signal,
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    const reader = response.body?.getReader();
    if (!reader) throw new Error("No response body");

    const decoder = new TextDecoder();
    
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      
      const chunk = decoder.decode(value, { stream: true });
      yield chunk;
    }
  },
};

Advanced Agent Provider with All Features

A complete implementation with session management, undo/redo, and connection checking:
import type { AgentProvider, AgentContext, AgentSessionStorage } from "react-grab";

interface MyAgentOptions {
  model: string;
  temperature: number;
}

class MyAgentProvider implements AgentProvider<MyAgentOptions> {
  private sessions = new Map<string, any>();
  private history: any[] = [];
  private historyIndex = -1;

  supportsResume = true;
  supportsFollowUp = true;
  dismissButtonText = "Close";

  async checkConnection(): Promise<boolean> {
    try {
      const response = await fetch("https://api.example.com/health");
      return response.ok;
    } catch {
      return false;
    }
  }

  async *send(
    context: AgentContext<MyAgentOptions>,
    signal: AbortSignal
  ): AsyncIterable<string> {
    const { content, prompt, options, sessionId } = context;
    
    // Build request with custom options
    const requestBody = {
      messages: [
        {
          role: "system",
          content: "You are a helpful coding assistant.",
        },
        {
          role: "user",
          content: `Here's the code:\n\n${content.join("\n\n")}\n\nQuestion: ${prompt}`,
        },
      ],
      model: options?.model || "gpt-4",
      temperature: options?.temperature || 0.7,
      stream: true,
    };

    const response = await fetch("https://api.example.com/chat/completions", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.API_KEY}`,
      },
      body: JSON.stringify(requestBody),
      signal,
    });

    if (!response.ok) {
      throw new Error(`API error: ${response.statusText}`);
    }

    // Store session
    if (sessionId) {
      this.sessions.set(sessionId, {
        messages: requestBody.messages,
        createdAt: Date.now(),
      });
    }

    const reader = response.body!.getReader();
    const decoder = new TextDecoder();
    let buffer = "";

    while (true) {
      const { done, value } = await reader.read();
      
      if (done) break;
      
      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split("\n");
      buffer = lines.pop() || "";

      for (const line of lines) {
        if (line.startsWith("data: ")) {
          const data = line.slice(6);
          if (data === "[DONE]") continue;
          
          try {
            const parsed = JSON.parse(data);
            const chunk = parsed.choices[0]?.delta?.content;
            if (chunk) yield chunk;
          } catch (e) {
            console.error("Failed to parse SSE:", e);
          }
        }
      }
    }
  }

  async *resume(
    sessionId: string,
    signal: AbortSignal,
    storage: AgentSessionStorage
  ): AsyncIterable<string> {
    const sessionData = storage.getItem(`session_${sessionId}`);
    if (!sessionData) {
      throw new Error("Session not found");
    }

    const session = JSON.parse(sessionData);
    
    // Resume from stored state
    yield* this.send(
      {
        content: session.content,
        prompt: session.lastPrompt,
        sessionId,
      },
      signal
    );
  }

  async abort(sessionId: string): Promise<void> {
    // Cancel ongoing request for this session
    this.sessions.delete(sessionId);
  }

  getCompletionMessage(): string {
    return "Analysis complete. You can ask a follow-up question.";
  }

  async undo(): Promise<void> {
    if (this.canUndo()) {
      this.historyIndex--;
      // Implement undo logic
    }
  }

  canUndo(): boolean {
    return this.historyIndex > 0;
  }

  async redo(): Promise<void> {
    if (this.canRedo()) {
      this.historyIndex++;
      // Implement redo logic
    }
  }

  canRedo(): boolean {
    return this.historyIndex < this.history.length - 1;
  }
}

const provider = new MyAgentProvider();

Registering an Agent Provider

Register an agent provider through an action:
import type { Plugin } from "react-grab";

const aiPlugin: Plugin = {
  name: "ai-assistant",
  actions: [
    {
      id: "ask-ai",
      label: "Ask AI",
      shortcut: "A",
      onAction: (context) => {
        context.enterPromptMode?.({
          provider: myAgentProvider,
          storage: localStorage, // Use localStorage for session persistence
          getOptions: () => ({
            model: "gpt-4",
            temperature: 0.7,
          }),
          onStart: (session) => {
            console.log("AI session started:", session.id);
          },
          onComplete: (session) => {
            console.log("AI session completed:", session.id);
          },
          onError: (error) => {
            console.error("AI error:", error);
          },
        });
      },
    },
  ],
};

window.__REACT_GRAB__.registerPlugin(aiPlugin);

AgentSession Interface

Session information tracked during agent interactions:
interface AgentSession {
  id: string;
  context: AgentContext;
  lastStatus: string;
  isStreaming: boolean;
  isFading?: boolean;
  createdAt: number;
  lastUpdatedAt: number;
  position: { x: number; y: number };
  selectionBounds: OverlayBounds[];
  tagName?: string;
  componentName?: string;
  error?: string;
}

Best Practices

  1. Error Handling: Always handle network errors and API failures gracefully
  2. Abort Support: Implement proper cancellation using the AbortSignal
  3. Streaming: Yield chunks as soon as they’re available for responsive UX
  4. Session Storage: Use AgentSessionStorage to persist sessions across page reloads
  5. Type Safety: Use generics to type your custom options: AgentProvider<MyOptions>
  6. Status Updates: Use the onStatus callback to show progress to users
  7. Connection Checks: Implement checkConnection to verify service availability
  8. Follow-ups: Set supportsFollowUp: true to enable conversation continuity

Example: OpenAI Integration

import type { AgentProvider, AgentContext } from "react-grab";
import OpenAI from "openai";

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  dangerouslyAllowBrowser: true, // Only for development!
});

const openAIProvider: AgentProvider = {
  async *send(context: AgentContext, signal: AbortSignal) {
    const stream = await openai.chat.completions.create(
      {
        model: "gpt-4",
        messages: [
          {
            role: "system",
            content: "You are an expert at analyzing React components.",
          },
          {
            role: "user",
            content: `${context.content.join("\n\n")}\n\n${context.prompt}`,
          },
        ],
        stream: true,
      },
      { signal }
    );

    for await (const chunk of stream) {
      const content = chunk.choices[0]?.delta?.content;
      if (content) yield content;
    }
  },

  async checkConnection() {
    try {
      await openai.models.list();
      return true;
    } catch {
      return false;
    }
  },
};

See Also

Build docs developers (and LLMs) love