Layouts

The kit does not ship page shells. Components fill the box you give them and adapt to its width, so a layout is a few flex containers around them. The recipes below match the layout stories in the repository Storybook (layouts/*).

Rules for every layout

  • Give the chat a bounded height: a flex column with minHeight: 0 on every level down to AgentChat. Without it the message list grows with its content and the composer scrolls away.
  • contentWidth sets the message column and composer width. Use a number such as 760 on pages and "100%" in panels and widgets. The default is 420px.
  • Below about 720px of container width, pass wrapLines so code and diffs wrap, and move side panes into a Drawer. Measure the container with useElementSize, not the viewport: the same screen can be a page or a panel.
  • alignComposer lines the composer and statusBar up with the text of the messages; topFade softens the top edge under a header.
  • Separate zones with background and a 1px --ae-border line, not with cards.

Full-page chat

One centered column: a 48px header aligned with the column, the feed without frames and the composer pinned to the bottom. withSearch adds Mod+F search; stickyPrompt keeps the prompt of a long answer visible.

import { ActionIcon, Box, Group, Text } from "@mantine/core";
import { IconShare2 } from "@tabler/icons-react";
import { AgentChat } from "@sinups/ai-kit";

const COLUMN = 760;

export function ChatPage({ chat }: { chat: ChatState }) {
  return (
    <Box h="100dvh" style={{ display: "flex", flexDirection: "column", minHeight: 0 }}>
      <Group component="header" h={48} px="md" maw={COLUMN} w="100%" mx="auto" wrap="nowrap">
        <Text size="sm" fw={500} truncate style={{ flex: 1 }}>
          Add retry to token refresh
        </Text>
        <ActionIcon variant="subtle" color="gray" aria-label="Share">
          <IconShare2 size={16} />
        </ActionIcon>
      </Group>
      <AgentChat
        {...chat}
        contentWidth={COLUMN}
        collapseToolRuns
        alignComposer
        topFade
        withSearch
        stickyPrompt
        style={{ flex: 1, minHeight: 0 }}
      />
    </Box>
  );
}

New chat

emptyState replaces the empty feed. The welcome layout keeps the composer at the bottom and lists starter actions above it. The center layout centers the greeting and the composer, with suggestion pills above the composer. Both switch to the regular feed after the first message.

const welcome = {
  avatar: <IconSparkles size={22} />,
  title: "How can I help you today?",
  description: "Ask about the code, fix a bug or plan a change.",
  actions: [
    { id: "explain", label: "Explain this repository", icon: <IconBook2 /> },
    { id: "tests", label: "Find flaky tests", icon: <IconBug />, badge: "New" },
  ],
};

// Welcome: greeting and actions in the message area, composer at the bottom
<AgentChat {...chat} contentWidth={760} alignComposer emptyState={welcome} />

// Center: greeting and composer centered; suggestion pills render above the composer
<AgentChat
  {...chat}
  contentWidth={760}
  emptyState={{
    layout: "center",
    title: "What should we work on?",
    suggestions: [
      { id: "explain", label: "Explain this repository" },
      { id: "tests", label: "Find flaky tests" },
    ],
  }}
/>

Chat with an inspector

A resizable, collapsible pane beside the chat for DiffReview or BackgroundTasksPanel. On phones open the same content in a bottom Drawer (position="bottom", size="92%"). Pane components take a header prop so the pane title sits in their own header row.

import { Splitter } from "@mantine/core";
import { AgentChat, DiffReview } from "@sinups/ai-kit";

<Splitter withHandle={false} lineSize={1} handleColor="var(--ae-border)" style={{ flex: 1, minHeight: 0 }}>
  <Splitter.Pane defaultSize={62} min="420px">
    <AgentChat {...chat} contentWidth={760} alignComposer topFade />
  </Splitter.Pane>
  <Splitter.Pane defaultSize={38} min="360px" collapsible collapseThreshold="240px">
    <DiffReview
      changes={changes}
      decisions={decisions}
      onAccept={(change) => accept(change.path)}
      onReject={(change) => reject(change.path)}
      header={<Text size="sm" fw={500}>Changes</Text>}
    />
  </Splitter.Pane>
</Splitter>

Settings page

SettingsLayout provides section navigation beside the content when wide and a list with a back action when narrow. Sections with fill: true give full height to panels that scroll themselves, such as McpSettingsPanel. For the same content in a dialog use SettingsModal.

import { useState } from "react";
import { Switch } from "@mantine/core";
import {
  McpSettingsPanel,
  SettingRow,
  SettingsLayout,
  SettingsSection,
  UsagePanel,
  type SettingsNavItem,
} from "@sinups/ai-kit";

const SECTIONS: SettingsNavItem[] = [
  { id: "general", label: "General" },
  { id: "mcp", label: "MCP servers", fill: true },
  { id: "usage", label: "Usage" },
];

export function SettingsPage() {
  const [activeId, setActiveId] = useState("general");

  return (
    <SettingsLayout title="Settings" sections={SECTIONS} activeId={activeId} onActiveIdChange={setActiveId}>
      {activeId === "general" && (
        <SettingsSection title="Chat">
          <SettingRow
            label="Send with Enter"
            description="Use Shift+Enter for a new line"
            control={<Switch aria-label="Send with Enter" defaultChecked />}
          />
        </SettingsSection>
      )}
      {activeId === "mcp" && <McpSettingsPanel servers={servers} />}
      {activeId === "usage" && <UsagePanel {...usage} />}
    </SettingsLayout>
  );
}

Widget

A floating assistant on a host page. See Embedding the launcher for the React and Shadow DOM setups.

<ChatLauncher title="Assistant">
  <AgentChat {...chat} contentWidth="100%" wrapLines alignComposer emptyState={welcome} />
</ChatLauncher>