Documentation
SDK

SDK: React Hooks

Use FlapjackProvider and useChat to build streaming chat UIs with Flapjack agents in React.

The @flapjack/sdk/react subpath export provides React components and hooks for building chat UIs.

npm install @flapjack/sdk

FlapjackProvider

Wrap your app (or the chat section) with FlapjackProvider to initialize the SDK context.

Security warning: The example below uses NEXT_PUBLIC_ environment variables for prototyping. This exposes your API key in the client bundle. For production, use the server proxy pattern instead.

import { FlapjackProvider } from '@flapjack/sdk/react';

function App() {
  return (
    <FlapjackProvider config={{
      apiKey: process.env.NEXT_PUBLIC_FLAPJACK_API_KEY!, // ⚠️ Dev onlyuse server proxy in production
      baseUrl: process.env.NEXT_PUBLIC_FLAPJACK_BASE_URL,
    }}>
      {/* Chat components go here */}
    </FlapjackProvider>
  );
}

Props

PropTypeRequiredDescription
configFlapjackConfigYesSDK configuration with apiKey and optional baseUrl
childrenReact.ReactNodeYesChild components
📋 Copy as prompt

Wrap my React app with FlapjackProvider from @flapjack/sdk/react. Use my API key from the NEXT_PUBLIC_FLAPJACK_API_KEY environment variable.

useFlapjack

Access the FlapjackClient instance from context. Useful when you need to call SDK methods outside of useChat (e.g., listing agents, managing knowledge, or calling runner APIs).

import { useFlapjack } from '@flapjack/sdk/react';

function AgentList() {
  const { client } = useFlapjack();
  const [agents, setAgents] = useState<Agent[]>([]);

  useEffect(() => {
    client.listAgents().then(setAgents);
  }, [client]);

  return <ul>{agents.map(a => <li key={a.id}>{a.name}</li>)}</ul>;
}

Return Value

FieldTypeDescription
clientFlapjackClientThe SDK client instance from the nearest FlapjackProvider

Throws if called outside of a FlapjackProvider.


useChat

The useChat hook manages conversation state, message sending, and streaming for a specific agent.

import { useChat } from '@flapjack/sdk/react';

function ChatWidget({ agentId }: { agentId: string }) {
  const { messages, sendMessage, isStreaming, error, reset, stop, threadId } = useChat(agentId);

  return (
    <div>
      {messages.map((msg, i) => (
        <div key={i} className={msg.role === 'user' ? 'user' : 'assistant'}>
          {msg.content}
        </div>
      ))}

      {isStreaming && <div className="typing">Thinking...</div>}
      {error && <div className="error">{error}</div>}

      <input
        onKeyDown={(e) => {
          if (e.key === 'Enter' && !isStreaming) {
            sendMessage(e.currentTarget.value);
            e.currentTarget.value = '';
          }
        }}
        disabled={isStreaming}
        placeholder="Type a message..."
      />

      {isStreaming && <button onClick={stop}>Stop</button>}
      <button onClick={reset}>New conversation</button>
    </div>
  );
}

Parameters

useChat(agentId: string, options?: UseChatOptions)
NameTypeDescription
agentIdstringThe agent's UUID
options.threadIdstring?Resume an existing thread — loads message history on mount
options.toolsToolDef[]?Custom tool definitions for client-side execution
options.onToolCallFunction?Handler for client-side tool calls (auto-submits results)
options.onCustomEvent(kind: string, payload: unknown) => void?Called when a custom SSE event is received. See Streaming: Custom Events.
options.onPlanActivity(toolName: string) => void?Called when plan/todo tools fire (e.g. plan_create, todo_update)
options.resourceBindingsResourceBinding[]?Attach resource context to threads
options.modelOverridestring?Override the agent's default model
options.webOverridesobject?Override web tool settings per message
options.memoryOverridesobject?Override memory settings per message
options.planOverridesobject?Override plan settings per message
options.computerOverridesobject?Override computer settings per message
options.onProfileSwitch(proposal: ProfileSwitchProposal) => void?Called when the agent proposes switching to a different tool profile
options.toolProfilestring?Use a specific tool profile for all messages
options.orgIdstring?Target org (multi-org only)

Return Value

FieldTypeDescription
messagesMessage[]All messages in the conversation
sendMessage(content: string, options?: SendMessageOptions) => Promise<void>Send a message and trigger streaming
addSystemMessage(meta, options?) => booleanInject a system activity indicator (see below). Returns true if agent turn was sent.
isStreamingbooleantrue while the agent is responding
errorstring | nullError message if something went wrong
reset() => voidClear messages and start a new thread
stop() => voidStop the current streaming response
threadIdstring | nullCurrent thread ID (null before first message)
loadThread(threadId: string) => Promise<void>Switch to a different thread — aborts any active stream and loads message history
activeProfilestring | nullCurrently active tool profile
switchProfile(profileKey: string) => Promise<void>Switch the active tool profile, persists to thread and notifies agent

Message Type

type Message = {
  role: 'user' | 'assistant';
  content: string;
  /** When set, renders as a compact system activity pill instead of a chat bubble. */
  systemMessage?: SystemMessageMeta;
};

type SystemMessageMeta = {
  icon: string;   // 'check' | 'tool' | 'info' | 'error' or any emoji/character
  label: string;  // Short human-readable label
};

addSystemMessage

Injects a system activity message into the chat. The message appears as a compact, centered pill (icon + label) instead of a regular user bubble. Used for plan approvals, tool confirmations, MCP events, etc.

const { addSystemMessage } = useChat(agentId);

// Trigger an agent response (default)
addSystemMessage({ icon: 'check', label: 'Plan approved' }, {
  content: 'Approved. Please proceed with the plan.',
});

// Visual-only — no agent turn
addSystemMessage({ icon: 'info', label: 'Memory saved' }, {
  sendToAgent: false,
});

// With extra send options
addSystemMessage({ icon: 'tool', label: 'Tool confirmed' }, {
  content: 'Execute the search tool.',
  messageOptions: { planOverrides: { enabled: true } },
});

Options

FieldTypeDefaultDescription
contentstringmeta.labelText sent to the agent
sendToAgentbooleantrueWhether to trigger an agent response
messageOptionsSendMessageOptionsExtra options forwarded to the agent turn

Return value: Returns true if the agent turn was triggered, false if the message was added as visual-only (either because sendToAgent was false or because a stream was in progress).

If called while streaming is active, the indicator appears in the UI but the agent turn is not sent (visual-only fallback). Check the return value if you need to know whether the agent received the message.

Resuming a Thread

Pass a threadId in the options to resume an existing conversation. Message history is loaded automatically on mount.

function ResumedChat({ agentId, threadId }: { agentId: string; threadId: string }) {
  const { messages, sendMessage, isStreaming } = useChat(agentId, { threadId });

  // messages are populated with the thread's history on mount
  return <div>{/* render messages */}</div>;
}

Switching Threads

Use loadThread to switch to a different thread at runtime. This aborts any active stream and loads the new thread's message history.

function ChatWithHistory({ agentId }: { agentId: string }) {
  const { messages, sendMessage, loadThread, threadId } = useChat(agentId);

  async function handleSelectThread(id: string) {
    await loadThread(id);
    // messages now contain the selected thread's history
  }

  return (
    <div>
      <ThreadList onSelect={handleSelectThread} />
      <Messages messages={messages} />
    </div>
  );
}
Copy as prompt

Use the useChat hook with { threadId } option to resume an existing Flapjack conversation. Use loadThread(id) to switch between threads dynamically.

Built-in Icon Keys

KeyGlyphUse case
checkPlan approved, action confirmed
tool🔧Tool / MCP activity
infoGeneral information
errorError / rejection

Any other string is rendered as-is — pass an emoji (e.g. "🚀") or character directly.


usePlan

The usePlan hook manages plan state for a thread. Use it alongside useChat to build plan-aware UIs.

import { usePlan } from '@flapjack/sdk/react';

function PlanView({ threadId }: { threadId: string | null }) {
  const { plan, planOpen, setPlanOpen, fetchPlan } = usePlan(threadId);

  if (!plan) return null;

  return (
    <div>
      <h3>{plan.title}</h3>
      {plan.todos?.map((todo) => (
        <div key={todo.id}>{todo.status === 'done' ? '✓' : '○'} {todo.content}</div>
      ))}
    </div>
  );
}

Parameters

NameTypeDescription
threadIdstring | nullThe thread's UUID. Plan auto-fetches on change.

Return Value

FieldTypeDescription
planPlan | nullCurrent plan with todos
setPlan(plan: Plan | null) => voidManually set plan state
planOpenbooleanWhether plan panel is visible
setPlanOpen(open: boolean) => voidToggle plan visibility
planModebooleanWhether plan mode is active
setPlanMode(mode: boolean) => voidToggle plan mode
fetchPlan() => Promise<void>Manually fetch the latest plan
debouncedFetchPlan() => voidDebounced fetch (500ms) — use for plan_result events

Complete Example

import { useState } from 'react';
import { FlapjackProvider, useChat } from '@flapjack/sdk/react';

export default function App() {
  return (
    <FlapjackProvider config={{
      apiKey: process.env.NEXT_PUBLIC_FLAPJACK_API_KEY!, // ⚠️ Dev only
    }}>
      <Chat agentId="your-agent-id" />
    </FlapjackProvider>
  );
}

function Chat({ agentId }: { agentId: string }) {
  const { messages, sendMessage, isStreaming, error, reset } = useChat(agentId);
  const [input, setInput] = useState('');

  const handleSend = () => {
    if (!input.trim() || isStreaming) return;
    sendMessage(input);
    setInput('');
  };

  return (
    <div style={{ maxWidth: 600, margin: '0 auto', padding: 20 }}>
      <div style={{ minHeight: 400, overflowY: 'auto' }}>
        {messages.map((msg, i) => (
          <div key={i} style={{
            padding: 8,
            margin: '4px 0',
            background: msg.role === 'user' ? '#e3f2fd' : '#f5f5f5',
            borderRadius: 8,
          }}>
            <strong>{msg.role}:</strong> {msg.content}
          </div>
        ))}
        {isStreaming && <div style={{ color: '#999' }}>Thinking...</div>}
        {error && <div style={{ color: 'red' }}>{error}</div>}
      </div>

      <div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && handleSend()}
          disabled={isStreaming}
          placeholder="Type a message..."
          style={{ flex: 1, padding: 8 }}
        />
        <button onClick={handleSend} disabled={isStreaming}>Send</button>
        <button onClick={reset}>Reset</button>
      </div>
    </div>
  );
}
📋 Copy as prompt

Build a complete Flapjack chat page using React with FlapjackProvider and useChat. Include a message list, streaming indicator, error display, controlled text input, send button, and reset button. Style it simply with inline styles.

Pre-Built Components

The @flapjack/sdk/components export provides ready-made chat UI components:

import { ChatPanel, FloatingChat, PlanPanel } from '@flapjack/sdk/components';
ComponentDescription
ChatPanelFull chat UI (messages + input)
ChatMessagesMessage display area
ChatMessageIndividual message bubble
ChatInputInput field with plan integration
ChatEmptyEmpty state placeholder
ChatLoadingLoading indicator
ChatToolCallTool execution display
FloatingChatFloating chat bubble widget
PlanPanelPlan display and approval UI
SystemUserMessageSystem activity indicator (compact pill with icon + label)

These components use inline styles and work without any CSS framework. Pass useChat and usePlan return values as props.

SystemUserMessage

Renders a system activity indicator as a compact, centered pill instead of a regular chat bubble. Used for plan approvals, tool confirmations, MCP events, etc.

import { SystemUserMessage } from '@flapjack/sdk/components';

<SystemUserMessage meta={{ icon: 'check', label: 'Plan approved' }} />
<SystemUserMessage meta={{ icon: 'tool', label: 'Search executed' }} />
<SystemUserMessage meta={{ icon: '🚀', label: 'Deployed' }} />
PropTypeDescription
metaSystemMessageMetaObject with icon (key or emoji) and label (short text)
classNamestring?Optional CSS class name

Custom Events in Components

ChatPanel supports custom event handling via the onCustomEvent prop:

PropComponentTypeDescription
onCustomEventChatPanel(kind: string, payload: unknown) => voidCallback fired for each custom SSE event
<ChatPanel
  agentId={agentId}
  onCustomEvent={(kind, payload) => {
    if (kind === 'suggestion') showSuggestionToast(payload);
    if (kind === 'navigation_proposal') showNavigationCard(payload);
  }}
/>

Security Note

Using FlapjackProvider with an API key directly in client-side code exposes the key in the browser. For production applications, use the server proxy pattern instead.

Next Steps

Docs last updated May 11, 2026