# Workspace app

A ChatGPT-style layout — collapsible conversation sidebar beside a live thread — powered by a single <kai-workspace> element.

One element gives you the full workspace: a resizable, collapsible sidebar listing past conversations, plus a streaming chat thread. Select a conversation, start a new one, or send a message below.

## How it works

`<kai-workspace>` wraps `<kai-conversations>` and `<kai-chat>` inside a resizable split. You pass a flat `conversations` array (add `groups` when you want section headers) and the active thread's `messages`. Three events drive all the interactivity:

```html
<kai-workspace id="ws"></kai-workspace>

<script type="module">

  await customElements.whenDefined('kai-workspace');

  const ws = document.getElementById('ws');

  // Seed conversations (assign in JavaScript — arrays can't be attributes).
  ws.conversations = [
    {
      id: 'c1', title: 'Migrating the dashboard to web components',
      scope: { type: 'collection' }, messageCount: 4,
      lastMessageAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    },
    // … more conversations
  ];

  // Seed the active thread.
  ws.messages = [
    { id: 'u1', role: 'user', parts: [{ type: 'text', text: 'How do I share state across the panels?' }] },
    {
      id: 'a1', role: 'assistant',
      parts: [{ type: 'text', text: 'Keep one source of truth and reassign it on each change …' }],
      actions: ['copy', 'like', 'dislike'],
    },
  ];
  ws.activeId = 'c1';

  // Swap the thread when the user picks a conversation.
  ws.addEventListener('kai-conversation-select', (e) => {
    ws.activeId = e.detail.id;
    ws.messages = threadFor(e.detail.id); // load from your store
  });

  // Clear the thread and start fresh.
  ws.addEventListener('kai-new-chat', () => {
    ws.messages = [];
    ws.activeId = undefined;
  });

  // Stream a reply — identical pattern to <kai-chat>.
  ws.addEventListener('kai-submit', async (e) => {
    const { value } = e.detail;
    const aId = crypto.randomUUID();
    ws.messages = [
      ...ws.messages,
      { id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text: value }] },
      { id: aId, role: 'assistant', parts: [] },
    ];
    ws.loading = true;
    let answer = '';
    for await (const token of streamFromYourModel(value)) {
      answer += token;
      ws.messages = ws.messages.map((m) =>
        m.id === aId ? { ...m, parts: [{ type: 'text', text: answer }] } : m,
      );
    }
    ws.loading = false;
  });
</script>
```

**Key data shapes:**

- `conversations` — each entry needs `id`, `title`, `scope` (`{ type: 'collection' }`), `messageCount`, `lastMessageAt`, and `updatedAt`. The component uses the timestamps to bucket rows into "Today", "Yesterday", "Last 7 days", etc.
- `messages` — the same `ChatMessage[]` shape as `<kai-chat>`: `id`, `role`, an ordered `parts` array (`text`, `reasoning`, `tool`, `card`, `source`, `file`), plus optional `actions`.

**Sidebar controls:**

| Prop | Default | Effect |
|---|---|---|
| `sidebarWidth` | `22` | Initial sidebar width (%) |
| `sidebarMinWidth` | `200` | Minimum sidebar width (px) |
| `sidebarMaxWidth` | `420` | Maximum sidebar width (px) |
| `sidebarCollapsed` | `false` | Start with sidebar collapsed |

The user can drag the divider to resize or click the sidebar toggle. Listen to `kai-sidebar-toggle` (`detail.collapsed`) to persist the state.

## Next steps

- **[Drop-in chat](/examples/drop-in-chat/)** — the simpler single-thread starting point and streaming loop.
- **[`kai-workspace` reference](/components/workspace/)** — every prop and event, including model switcher, context meter, and slash commands.
- **[`kai-conversations` reference](/components/conversations/)** — use this element standalone when you want to build your own layout instead of using the pre-composed workspace.
- **[Theming](/guides/theming/)** — brand the sidebar (`--color-sidebar`) and chat thread in one pass.
