Skip to main content

Setup

List agents

const { agents } = await client.listAgents();

// Each agent exposes feature coverage via `capabilities` to determine what UI to show
const claude = agents.find((a) => a.id === "claude");
if (claude?.capabilities.permissions) {
  // Show permission approval UI
}
if (claude?.capabilities.questions) {
  // Show question response UI
}

Create a session

const sessionId = `session-${crypto.randomUUID()}`;

await client.createSession(sessionId, {
  agent: "claude",
  agentMode: "code",        // Optional: agent-specific mode
  permissionMode: "default", // Optional: "default" | "plan" | "bypass" | "acceptEdits" (Claude: accept edits; Codex: auto-approve file changes; others: default)
  model: "claude-sonnet-4", // Optional: model override
});

Send a message

await client.postMessage(sessionId, { message: "Hello, world!" });

Stream events

Three options for receiving events:
// Option 1: SSE (recommended for real-time UI)
const stream = client.streamEvents(sessionId, { offset: 0 });
for await (const event of stream) {
  handleEvent(event);
}

// Option 2: Polling
const { events, hasMore } = await client.getEvents(sessionId, { offset: 0 });
events.forEach(handleEvent);

// Option 3: Turn streaming (send + stream in one call)
const stream = client.streamTurn(sessionId, { message: "Hello" });
for await (const event of stream) {
  handleEvent(event);
}
Use offset to track the last seen sequence number and resume from where you left off.

Handling Events

Bare minimum

Handle item lifecycle plus turn lifecycle to render a basic chat:
type ItemState = {
  item: UniversalItem;
  deltas: string[];
};

const items = new Map<string, ItemState>();
let turnInProgress = false;

function handleEvent(event: UniversalEvent) {
  switch (event.type) {
    case "turn.started": {
      turnInProgress = true;
      break;
    }

    case "turn.ended": {
      turnInProgress = false;
      break;
    }

    case "item.started": {
      const { item } = event.data as ItemEventData;
      items.set(item.item_id, { item, deltas: [] });
      break;
    }

    case "item.delta": {
      const { item_id, delta } = event.data as ItemDeltaData;
      const state = items.get(item_id);
      if (state) {
        state.deltas.push(delta);
      }
      break;
    }

    case "item.completed": {
      const { item } = event.data as ItemEventData;
      const state = items.get(item.item_id);
      if (state) {
        state.item = item;
        state.deltas = []; // Clear deltas, use final content
      }
      break;
    }
  }
}
When rendering:
  • Use turnInProgress for turn-level UI state (disable send button, show global “Agent is responding”, etc.).
  • Use item.status === "in_progress" for per-item streaming state.
function renderItem(state: ItemState) {
  const { item, deltas } = state;
  const isItemLoading = item.status === "in_progress";

  // For streaming text, combine item content with accumulated deltas
  const text = item.content
    .filter((p) => p.type === "text")
    .map((p) => p.text)
    .join("");
  const streamedText = text + deltas.join("");

  return {
    content: streamedText,
    isItemLoading,
    isTurnLoading: turnInProgress,
    role: item.role,
    kind: item.kind,
  };
}

Extra events

Handle these for a complete implementation:
function handleEvent(event: UniversalEvent) {
  switch (event.type) {
    // ... bare minimum events above ...

    case "session.started": {
      // Session is ready
      break;
    }

    case "session.ended": {
      const { reason, terminated_by } = event.data as SessionEndedData;
      // Disable input, show end reason
      // reason: "completed" | "error" | "terminated"
      // terminated_by: "agent" | "daemon"
      break;
    }

    case "error": {
      const { message, code } = event.data as ErrorData;
      // Display error to user
      break;
    }

    case "agent.unparsed": {
      const { error, location } = event.data as AgentUnparsedData;
      // Parsing failure - treat as bug in development
      console.error(`Parse error at ${location}: ${error}`);
      break;
    }
  }
}

Content parts

Each item has content parts. Render based on type:
function renderContentPart(part: ContentPart) {
  switch (part.type) {
    case "text":
      return <Markdown>{part.text}</Markdown>;

    case "tool_call":
      return <ToolCall name={part.name} args={part.arguments} />;

    case "tool_result":
      return <ToolResult output={part.output} />;

    case "file_ref":
      return <FileChange path={part.path} action={part.action} diff={part.diff} />;

    case "reasoning":
      return <Reasoning>{part.text}</Reasoning>;

    case "status":
      return <Status label={part.label} detail={part.detail} />;

    case "image":
      return <Image src={part.path} />;
  }
}

Handling Permissions

When permission.requested arrives, show an approval UI:
const pendingPermissions = new Map<string, PermissionEventData>();

function handleEvent(event: UniversalEvent) {
  if (event.type === "permission.requested") {
    const data = event.data as PermissionEventData;
    pendingPermissions.set(data.permission_id, data);
  }

  if (event.type === "permission.resolved") {
    const data = event.data as PermissionEventData;
    pendingPermissions.delete(data.permission_id);
  }
}

// User clicks approve/deny
async function replyPermission(id: string, reply: "once" | "always" | "reject") {
  await client.replyPermission(sessionId, id, { reply });
  pendingPermissions.delete(id);
}
Render permission requests:
function PermissionRequest({ data }: { data: PermissionEventData }) {
  return (
    <div>
      <p>Allow: {data.action}</p>
      <button onClick={() => replyPermission(data.permission_id, "once")}>
        Allow Once
      </button>
      <button onClick={() => replyPermission(data.permission_id, "always")}>
        Always Allow
      </button>
      <button onClick={() => replyPermission(data.permission_id, "reject")}>
        Reject
      </button>
    </div>
  );
}

Handling Questions

When question.requested arrives, show a selection UI:
const pendingQuestions = new Map<string, QuestionEventData>();

function handleEvent(event: UniversalEvent) {
  if (event.type === "question.requested") {
    const data = event.data as QuestionEventData;
    pendingQuestions.set(data.question_id, data);
  }

  if (event.type === "question.resolved") {
    const data = event.data as QuestionEventData;
    pendingQuestions.delete(data.question_id);
  }
}

// User selects answer(s)
async function answerQuestion(id: string, answers: string[][]) {
  await client.replyQuestion(sessionId, id, { answers });
  pendingQuestions.delete(id);
}

async function rejectQuestion(id: string) {
  await client.rejectQuestion(sessionId, id);
  pendingQuestions.delete(id);
}
Render question requests:
function QuestionRequest({ data }: { data: QuestionEventData }) {
  const [selected, setSelected] = useState<string[]>([]);

  return (
    <div>
      <p>{data.prompt}</p>
      {data.options.map((option) => (
        <label key={option}>
          <input
            type="checkbox"
            checked={selected.includes(option)}
            onChange={(e) => {
              if (e.target.checked) {
                setSelected([...selected, option]);
              } else {
                setSelected(selected.filter((s) => s !== option));
              }
            }}
          />
          {option}
        </label>
      ))}
      <button onClick={() => answerQuestion(data.question_id, [selected])}>
        Submit
      </button>
      <button onClick={() => rejectQuestion(data.question_id)}>
        Reject
      </button>
    </div>
  );
}

Testing with Mock Agent

The mock agent lets you test UI behaviors without external credentials:
await client.createSession("test-session", { agent: "mock" });
Send help to see available commands:
CommandTests
helpLists all commands
demoFull UI coverage sequence with markers
markdownStreaming markdown rendering
toolTool call + result with file refs
statusStatus item updates
imageImage content part
permissionPermission request flow
questionQuestion request flow
errorError + unparsed events
endSession ended event
echo <text>Echo text as assistant message
Any unrecognized text is echoed back as an assistant message.

Reference Implementation

The Inspector UI is a complete reference showing session management, event rendering, and HITL flows.