Cal.comdesign-system

Agent UI

Architecture, contracts, and examples for Cal's reusable AI agent interface.

Agent UI is the presentation layer for Cal's global AI experience. It provides composable conversation, activity, approval, history, and input parts instead of one fixed chat implementation.

These components are a parallel prototype. Existing feature-specific AI surfaces keep their current implementations; the global agent will be the first product integration for this layer.

Generated-image slots are a different problem. Use AI image placeholder when a media frame is waiting on an AI image. That control is not a conversation part.

Architecture

Ownership boundary

The design-system examples import only from @calcom/ai/react/ui/* and supply mock state locally. Every file under that path is covered by the package wildcard export, so a new UI module does not require another package.json export.

Component map

There are eighteen consumer-facing presentation modules and three shared implementation modules. Their parts remain independently useful: a compact surface may omit timestamps, feedback, mentions, or history.

AgentOrb (@calcom/ai/react/ui/agent-orb) supports composer and title placements.

Choose an entry point

Use an adapter when it prevents repeated runtime or localization mapping. Routing, persistence, permissions, tool side effects, and message-list mutations stay in the product surface. Result-list row mapping stays in the product surface too โ€” AgentResultList only renders items.

UI reference

All focused examples use local mock state. They do not construct AgentChat, call an AI model, persist a conversation, or perform a mutation.

Conversation

AgentConversation places an existing transcript at its latest message before the first paint. It follows later content only while the reader is already at the latest message; if the reader scrolls upward, it preserves that position and can show an accessible jump-to-latest control.

Messages wrap long unbroken text and cap their width at 48rem; user bubbles also stay within 85% of their container. Compose AgentMessageText around text only, leaving reasoning, tool activity, result lists, approvals, and actions outside. Agent response prose is never height-clamped or disclosed; readers scroll the conversation viewport to read longer replies. AgentDisclosureTrigger chrome is reserved for AgentReasoning and tool activity groups. Long result lists use a remainder toggle, not that chrome.

Select Add long messages to exercise scroll behavior with tall prompt and response content. Scroll away from the latest message and add an update to verify that the viewport preserves the reader's position. This focused example includes the optional timestamp divider; the complete prototype intentionally omits timestamps for the initial experience.

Conversation viewport

Conversation menu

The dedicated chat title and the action-bar panel can host the same overflow actions. The UI owns the menu, clipboard copy, and delete confirmation. The product supplies the markdown string and performs the delete.

Message actions

Message actions appear when their parent message is hovered or contains keyboard focus. Copy owns only its brief clipboard feedback. Edit reports intent because the product owns the transcript branch.

When editing, keep every message before the selected prompt, move that prompt into the composer, remove the selected prompt plus every later message, and return focus to the composer:

Context-bearing messages omit copy and edit actions because the attached context is part of the submitted turn.

Message actions
I can help find a time that works for everyone.
Find a time for the onboarding call this week.
I found three openings and can compare them next.

Response feedback

Completed agent responses can compose AgentResponseFeedback inside the same AgentMessageActions toolbar. It renders Good response and Bad response icon actions with tooltips. Good response submits immediately; Bad response opens a Coss dialog for one or more caller-supplied reasons and optional details. Clicking the selected rating removes it through onFeedback(null). A successful rating callback shows the supplied thank-you toast. A rejected callback shows the supplied error toast and keeps the negative-feedback dialog open.

Keep feedback attached to the final assistant message rather than streaming reasoning, tool rows, or approval prompts. Close over the product-owned message ID when persisting it:

The focused message-actions example demonstrates user Copy and Edit actions plus completed-response feedback. The complete prototype applies feedback to completed responses, keeps the final completed response's actions visible, and omits feedback while a response is still streaming.

Agent activity

Text and reasoning content arrive from the AI SDK or the product caller. CSS handles only presentation such as shimmer, disclosure, and reduced-motion behavior.

Reasoning

Disclosure for agent thinking and reasoning lives here โ€” not on response prose. Use this collapsible surface (and tool activity groups) whenever content should be inspected on demand; never put Show more / Show less on AgentMessageText.

Reasoning state
The requested time is available for every host, so I can create the event type without a scheduling conflict.

Status indicator

thinking and working share an animated dot-grid icon that keeps running when switching between these states. Status changes keep the icon and label aligned. Tool activity summaries use muted to keep both neutral.

Labels and icons appear immediately on first render and on every update, without text morphing. Active labels shimmer. Under reduced motion, the icon stays static and labels update immediately without shimmer. The full label remains visible and available to screen readers. All status variants wrap within their available width and grow vertically, including during active shimmer and under reduced motion. Icons and disclosure carets align with the first line. The constrained-width example below exercises long labels, including the complete out-of-credits recovery instruction.

Interactive example
Thinking
Long label ยท compact width
Checking availability across your connected calendars to find a time that works for everyone.

Tool activity

The runtime adapter maps tool-part state to these presentation states:

Skip is the approval action shown before a tool runs. Declined is the transcript result after the SDK records output-denied; they describe different moments of the same decision.

Result lists

AgentResultList turns a structured tool page into a compact card of one-line rows. Use it when the reader should scan and open items. Do not ask the model to restate those items as a markdown table, numbered list, or bullet list.

The list is domain-agnostic. Each skill maps its own summary fields onto this row. The product still renders list tools as model markdown; this card is the composition to reuse when that mapping lands. Event types, teams, members, schedules, routing forms, workflows, and later list tools should share this card, not invent a second one.

Each row is one line: title, then the subtitle, then badges on the right. Hover matches ListItem. The card uses rounded-lg, including the before radius. A short sentence sits above the card, not below it. More than six rows keep the first five visible and put the rest behind Show N more / Show less, the same remainder rule as skipped invite addresses. A leftover of one stays expanded. Both controls prepend their icon: ChevronsUpDownIcon for Show more, ChevronsDownUpIcon for Show less. That toggle is not AgentDisclosureTrigger. The remainder starts closed again when the item ids change. A host that keeps one instance across result pages can also key it by that page.

The switcher maps each list tool onto the same row. Members is an 11-row page so the remainder toggle is visible. Team members have no item href in the app, so those rows are not links. Empty is a sentence with no card.

Interactive example

Call sites

Approvals

Approval cards report decisions through callbacks but never execute the mutation. An empty grouped approval list renders nothing.

After the user clicks Approve or Skip, the card must not vanish into a blank gap. Wrap it in AgentApprovalPending so the card scales down from bottom-center, blurs, and fades into a Workingโ€ฆ status while the agent resumes. The product owns the phase (awaiting โ†’ pending) and clears it when the next visible reply arrives or the action fails. Disable the card while pending so the decision cannot be submitted twice.

AgentApprovalPrompt requires question and can add questionForMany, context, details, chip, and per-prompt confirm or cancel labels. A grouped AgentApprovalItem adds approvalId and toolName.

AgentChatTranscript already wires this handoff: answered approvals stay mounted while the turn is busy, the card transitions into Workingโ€ฆ, and the bottom thinking indicator stays quiet so feedback is not duplicated.

Visual examples

Pending feedback after decision

Approve or skip to see the card scale down from bottom-center, blur, and hand off to Workingโ€ฆ

Apply changes to this routing form?

  • Added company size
  • Updated the enterprise route

Grouped approval

Create the missing event types?

  • Customer interview30 min, collective
  • Product feedback45 min, round robin

Skip first to request changes

Choose an approval action to see the reported intent.

Notices

A notice is a stop the person acts on outside the conversation. It uses the approval cardโ€™s frame and none of its machinery: no Approve or Skip, and nothing is reported back to the agent. Use an approval card when the agent is waiting on a decision. Use a notice when the turn has already stopped and the next step lives somewhere else, such as billing.

AgentNoticeCard takes a title and description, plus an optional icon, footer action, and hint. The action and the hint are both optional, because the caller may have nothing to offer.

Youโ€™re out of credits

Add credits to keep using Cal.ai.

Composer

AgentComposer composes the shared Coss Composer. It uses a native textarea for plain text; AgentMentionComposer uses the same primitiveโ€™s structured-token input. The agent layer adds page context, pointer glow, queues and Send/Stop mapping. The caller owns the value and every state transition.

The convenience component and composable parts share the same controlled root contract. Like Coss UI, every part is a flat named export. Use the parts only when the standard slot layout is not sufficient:

The textarea uses Enter to report submit and Shift+Enter to insert a new line. The focused example covers new-chat, busy, queued, and disabled states. In Busy or Queued, type a prompt and press Enter to append it below any existing queued prompts. Each queued row stays transparent until hover. The composer does not store a queue itself.

Page context

Page context may attach the current page to the first prompt of a new chat. The product surface owns the page identity and records it with the submitted message.

Use layout="adaptive" with rows={1} for the full-page pill: Coss measures the real input, moves controls below multiline drafts, and handles external draft replacements and resizing. Panel compositions use the stacked layout.

Leading and trailing actions are caller-supplied. The examples provide reference page-context and attachment actions; they are not public AgentComposer parts because choosing a page or uploading a file is product behavior.

Mentions and slash actions

Use AgentMentionComposer when a message needs non-editable references and AgentComposer for plain text. Both use Coss Composer; the agent composition has no Lexical dependency. Calโ€™s adapter translates scoped references into serializable token data and an explicit text representation, preserving membership IDs, scope and sent-text offsets. Suggestion fetching and authorization stay with the product.

Nothing sent. This example runs locally.

Value and ownership

The value is an array of text and mention parts, not Lexical state:

getComposerText(value) produces the readable message; preserve the parts separately when IDs matter. References from the editor or clipboard are untrusted input. The server must resolve IDs and check access before fetching context or invoking tools. The UI never searches a backend or grants access.

Send/Stop, busy and disabled state, headers, footers and action slots follow the composer contract. name, when provided, submits plain text through a hidden form field; structured references remain caller-owned. An empty send restores editor focus. Existing AgentChatComposer is unchanged and does not accept structured values.

Editing and accessibility

The input stays a labelled multiline textbox, with an associated listbox and active-option announcement. The popup uses coss Popover, not a menu that moves focus away from the editor.

Suggestions follow the caret query even when editing in the middle of a message. Email addresses, punctuation adjacent to a trigger, selected ranges and token interiors do not open suggestions. The popup uses the same default Coss ComposerPopup as Chat UI: it spans the composer width and anchors above the composer. Results are bound to the reported query key; empty and limit messages remain visible. Focus stays in the editor.

History

History is a searchable, newest-first view of caller-supplied conversations. The caller owns fetching, persistence, active and unread state, date formatting, and selection.

Open the history trigger to search or select a conversation. Toggle the supplied list off to inspect the empty state.

Current: Make test for AI agent launcher

Action bar

Set the optional indicatorLabel on a session to show a circular status indicator. The label is visually hidden but included in the button's accessible name; use it to explain the state, such as โ€œNew activityโ€, rather than relying on the dot alone. The product owns its lifecycle and should clear it when the activity is read.

Hover a previous-conversation trigger to reveal its close action. Right-click it for session-management actions.

With a keyboard, focusing the session trigger keeps its close action hidden; tab to the close action itself to reveal it.

Interactive example

Link pull request to Base UI issue

Previous chat opened

Use the action bar below. Right-click a previous chat to open its context menu.

Panel

AgentPanel is the chat frame: title, header actions, conversation slot, and optional composer. It does not own open state, sessions, or routing. Host it in a Coss Popover when launching from the action bar. Floating hosts pass resizable so size and edge handles live in this shared component โ€” not in each product or docs wrapper.

Host contract

When several action-bar triggers share one panel, attach every detached PopoverTrigger to one PopoverCreateHandle and give each trigger a stable session ID as its payload. Keep conversation state keyed by ID and render one Popover so Base UI can animate the popup position (transition on top/left/transform), size, and content without closing it between triggers โ€” same Linear-style pin-to-pill motion as the product floating toolbar. End-align the popup with the active trigger (align="end"). Pass resizable on AgentPanel so left/top resize cascades everywhere the shared panel is used; do not reimplement handles in the host. The active conversation may change independently from the active anchor.

  • The Agent launcher always creates a clean draft; it does not reuse the previous new-chat state.
  • Submitting the first prompt creates and highlights a session immediately before the launcher while the open panel stays anchored to the launcher. Selecting that session later moves the panel to its trigger.
  • Minimize dismisses the panel but keeps its session. Close reports intent for the host to remove that session.
  • History selection opens or reactivates state by conversation ID without removing other sessions.
  • Unread state is shared: a history row and its action-bar session show the same indicator. Opening that conversation in the panel or on the dedicated page clears both.

During a Coss popover crossfade, AgentConversation uses Base UI's data-previous state and its recorded --agent-conversation-scroll-top value to keep the outgoing transcript at the same visible position until it fades. Outside that host, the selector is a no-op.

The Complete prototype demonstrates the full host contract, including initial composer focus and distinct state for every session ID. The playground agent page is the dedicated full-page host for the same contract. A conversation lives at /playground/ai-agents/agent/{uid}. The title history trigger changes that page conversation; the action-bar history trigger still opens a panel session.

Mobile bottom drawer

Below the web app's md breakpoint (800px), the agent opens in a bottom drawer over the current page. This keeps that page mounted so its ViewContext remains available. The existing component is named AgentMobileSheet; it composes Coss Drawer.

The presentation is shared through @calcom/ai/react/ui/agent-mobile-sheet. The web caller supplies translated labels directly and owns the conversation, draft, page context, and navigation.

Messages stay in this example. No model is called.

The example is available at every viewport width so it can be inspected in a narrow browser panel. Open an empty drawer or a sample conversation, type @ for synthetic members, and enter enough text to scroll the composer. Close and reopen it to check that the draft remains. Send appends the message locally. The expand button reports its callback below the example; it does not navigate or call a model.

Anatomy and layout

  • A modal backdrop covers the current page.
  • The drawer occupies approximately 95% of the viewport, with rounded top corners and a fixed header.
  • The header contains the agent orb, a truncated conversation title, Open full-page chat, and Close.
  • The transcript scrolls above the bottom composer. As in Chat UI, Coss owns input growth and its single scroll surface: the input starts at one row and grows up to three on narrow screens (two to six rows from 800px), then scrolls longer drafts. The footer stays in the layout, so its actual height reserves space beneath the transcript.
  • The footer reserves space for the device's bottom safe area. It moves above the virtual keyboard using the same Coss keyboard inset as the web app, without a separate fixed-position editor or hardcoded footer height.
  • Swipe-to-dismiss belongs to the drawer chrome. The transcript and editor keep their own touch scrolling.
  • Motion follows the existing web drawer transition and respects reduced motion.

Props and ownership

The composer wrapper uses the same slots as the web host so the drawer can position its footer around the keyboard:

For mobile, the web host passes autoFocus={false} to the composer so opening the drawer does not immediately summon the keyboard. The drawer places initial focus on its container; the composer receives focus when the person selects it. The desktop agent popup instead focuses its editable composer on open. Closing the drawer dismisses the view; removing a session is a separate product action. The launcher resumes the last active conversation.

Production call sites

Example entry points include /home and /bookings/upcoming in the web app: below 800px, use the agent launcher when the organization has the agent enabled. Verify virtual-keyboard movement and swipe dismissal on a touch device; resizing a desktop browser exercises layout but does not open a mobile keyboard.

Runtime adapters

Adapters depend on the runtime and localization layer; UI modules do not. There are three adapter modules:

Use AgentChatComposer when an existing AgentChat owns the state:

While chat is busy, this adapter keeps Stop visible because AgentChat.submit does not accept another prompt in that state. It also disables the composer while an approval is pending.

For a custom connected layout, compose AgentChatComposerRoot with the flat AgentComposerFrame, AgentComposerInput, and other presentation exports from @calcom/ai/react/ui/agent-composer. The adapter does not duplicate or namespace those UI parts.

Approval and tool adapters keep runtime data out of the presentation layer:

Connect the new UI to a real agent

Use the existing useAgentChat runtime; do not copy the playground's fixture store or timers into the app. The following is a minimal connected transcript, not a new backend or a complete global-agent implementation. Mount its controller once per conversation, above the popup/page presentation choice. Calling the hook separately in each host does not share its composer draft.

The caller supplies an authorized chat endpoint, localized labels, tool renderers, and approval descriptions. This example renders streamed text as plain React text (safe by default); rich Markdown must use the existing chatMarkdownToSafeHTML sanitizer.

  • Streaming: the SDK updates message parts. Render their current text; CSS only animates status treatments.
  • Approvals: send the SDK approval ID through addToolApprovalResponse. The hook resumes the turn after pending approvals are answered. UI approval buttons do not enforce server permissions. Prefer AgentChatTranscript, which keeps answered approvals mounted and transitions them into Workingโ€ฆ through AgentApprovalPending so the card never vanishes into a blank gap.
  • Stop and submit: the composer adapter wires these to the runtime. Unlike the mock playground, it does not implement queued submissions.
  • History and identity: load authorized history before mounting with initialMessages. Keep one controller and draft per conversation ID. Multi-conversation persistence, deletion and route access belong to the product; a client UID is not authorization.
  • Editing: the product stops the current turn, truncates chat.messages before the selected user message, restores its text through chat.setInput, and focuses the composer. Never truncate another chat's state.
  • Context, files and errors: the playground's attachment control and context labels are visual examples, not uploads or context capture. Define validated payloads, supported message-part renderers and recoverable error behavior in the product.

For endpoint, actor, permission and tool wiring, follow the repository's build-calcom-ai-agent skill. Its legacy surface example remains applicable to existing feature chats; use this section for the new global-agent UI.

Complete prototype

The complete prototype is the canonical AgentPanel demo and composes the panel-facing UI modules with local state. Use the scenario switcher to inspect a blank new chat, approval, queued follow-up, long scrollable transcript, kitchen-sink markdown (headings, ordered and bulleted lists, links, badges), and calendar error without repeating the same frame six times.

The panel starts closed so loading this page does not move focus or scroll to the demo. Choose a scenario, then use Agent or a session in the action bar to open its AgentPanel popover. The action bar also includes history. Selecting a history item adds its session action without replacing existing actions; selecting an already-open item activates its existing action. AI responses, tool progress, approvals, message edits, and session state are simulated; no model, persistence, or mutation runs.

Complete prototype

Product availability

This component family is documented here and in the AI Agents playground: the app page hosts the action bar outside the main inset and keeps the panel closed until a launcher or session is opened, and the agent page is the dedicated full-page chat. Routing Forms and other feature-specific chats continue using the legacy @calcom/ai/react/* implementations; migrating them is outside this page's scope.

Playground behavior

Layout and controls

  • The title history menu controls the page chat; toolbar history controls the popup.
  • History uses the supported boolean autoHighlight: typing highlights the first match; opening without a query leaves highlighting to the arrow keys or pointer.
  • An existing page chat offers New chat first in its history menu, an ellipsis menu, and a desktop Move to the toolbar button.
  • Transcript and composer content are width-constrained; the scrolling parent is not, keeping the scrollbar at the page edge.
  • A title uses its natural width in the header. It truncates only when it would exceed the remaining space before the overflow actions, so it never widens the page.
  • An empty full-page chat centers the taller composer. It has no page-context control. The app-page Agent launcher attaches page context to a fresh popup chat.
  • The queued fixture intentionally pauses to expose its controls; Send now resumes simulated progress.

What to reuse

Copy the presentation composition and the runtime integration contractโ€”not agent-demo-conversations.ts, mock messages, navigation counters, or simulated responses. The docs scenario switcher gets an isolated store; playground routes share an in-memory store for client-side navigation. Neither is production persistence.

The web app uses the floating action bar and popover at desktop widths and the mobile bottom drawer below md. The dedicated full-page chat remains available at every width. The playground's action bar currently demonstrates the desktop host; the focused bottom-drawer example above is available at all widths.

Sources

  • packages/ai/src/react/ui โ€” presentation components
  • packages/ai/src/react/adapters โ€” runtime and localization mapping
  • apps/design-system/components/agent-*-example.tsx โ€” documentation-only state and compositions
  • apps/design-system/components/agent-result-list-example.tsx โ€” result-list domain prototypes