# Svelte (/docs/quickstart/svelte)

This page builds a small Svelte 5 app, served by Vite, with a connect button and a chat that searches a bookshop catalog through its tools. Svelte uses `@aiconnect/core` directly: `createSubscriber` turns the client's changes into Svelte reactivity, and a class with `$state` fields holds the conversation.

The connect window of `@aiconnect/react` is made of React components, so a Svelte app shows its own connect button, which calls `client.connect()`.

You need Svelte 5.7 or later for `createSubscriber`, and Chrome with AI Wallet installed and one account connected to try it.

## Install the package [#install-the-package]

```bash
npm install @aiconnect/core
```

<Callout type="warn">
The package is not published on npm yet. Until it is, build a checkout of the AI Connect repository with `npm run build` and point your `package.json` at it with `"@aiconnect/core": "file:../aiconnect/packages/core"`.
</Callout>

## Follow the connection [#follow-the-connection]

Create one client for the page in a `.svelte.ts` module. In a browser it looks for AI Wallet right away and asks whether your site is already approved.

```ts title="src/lib/aiConnect.svelte.ts"
import { createSubscriber } from 'svelte/reactivity';
import { AiConnect } from '@aiconnect/core';
import type { ConnectionState } from '@aiconnect/core';

// One client for the page. In a browser it looks for AI Wallet right away.
export const client = new AiConnect();

// Svelte listens to the client while a component or an effect reads the state.
const track = createSubscriber((update) => client.subscribe(update));

export const connection = {
  get state(): ConnectionState {
    track();
    return client.state;
  },
};
```

`connection.state` returns the client's current state. Read inside a component or an effect, it also subscribes to the client, and Svelte reruns what depends on it after each change. `client.subscribe` returns its own unsubscribe function, which `createSubscriber` calls once nothing reads the state anymore.

`client.installUrl` is the AI Wallet page, https://tensorpass.app/wallet, where a visitor without the wallet gets it.

## Add the connect button [#add-the-connect-button]

The button renders one line per state. The [JavaScript quickstart](/docs/quickstart/javascript#follow-the-connection) explains each of the six states.

```svelte title="src/lib/ConnectButton.svelte"
<script lang="ts">
  import { client, connection } from './aiConnect.svelte';

  const current = $derived(connection.state);
</script>

{#if current.status === 'checking'}
  <p role="status">Looking for AI Wallet</p>
{:else if current.status === 'noWallet'}
  <p role="status">
    Add AI Wallet to your browser, then reload this page.
    <a href={client.installUrl} target="_blank" rel="noreferrer">Install AI Wallet</a>
  </p>
{:else if current.status === 'connecting'}
  <p role="status">Approve this site in the AI Wallet window.</p>
{:else if current.status === 'connected'}
  <p role="status">
    Connected with {current.providerName}
    <button type="button" onclick={() => client.disconnect()}>Disconnect</button>
  </p>
{:else}
  <p role="status">
    {current.status === 'error' ? current.message : 'Use your own AI on this site.'}
    <button type="button" onclick={() => client.connect()}>Connect your AI</button>
  </p>
{/if}
```

A visitor who declines in the wallet's window lands in the `error` state with the wallet's reason, "The connection was declined", and the button asks the wallet again. A visitor who approved your site on an earlier visit is connected a moment after the page loads, with no click.

`client.disconnect()` removes your site from the visitor's approved sites in the wallet. The next `connect()` asks the visitor again.

## Run a conversation with your tools [#run-a-conversation-with-your-tools]

`runConversation` sends the conversation to the visitor's model, runs the tools it asks for, and returns the messages it added. The `Chat` class keeps the transcript and passes it back on the next question. Its `$state` fields update the page as the answer streams.

```ts title="src/lib/chat.svelte.ts"
import { finalAnswer, runConversation } from '@aiconnect/core';
import type { ChatMessage, ProviderSession, ToolCall, ToolSource } from '@aiconnect/core';

// A conversation on the visitor's AI. It keeps the transcript and shows the last answer with its tool calls.
export class Chat {
  answer = $state('');
  toolCalls = $state<ToolCall[]>([]);
  busy = $state(false);
  error = $state('');
  #tools: ToolSource;
  #transcript: ChatMessage[];

  constructor(system: string, tools: ToolSource) {
    this.#tools = tools;
    this.#transcript = [{ role: 'system', content: system }];
  }

  async ask(session: ProviderSession, question: string) {
    const asked: ChatMessage = { role: 'user', content: question };
    this.answer = '';
    this.toolCalls = [];
    this.error = '';
    this.busy = true;
    try {
      const added = await runConversation({
        session,
        tools: this.#tools,
        messages: [...this.#transcript, asked],
        onText: (delta) => {
          this.answer += delta;
        },
        onToolCall: (call) => {
          this.toolCalls.push(call);
        },
      });
      this.#transcript = [...this.#transcript, asked, ...added];
      // The streamed text covers every step of the loop; the final answer is the model's last message.
      this.answer = finalAnswer(added)?.content ?? this.answer;
    } catch (failure) {
      this.error = failure instanceof Error ? failure.message : String(failure);
    } finally {
      this.busy = false;
    }
  }
}
```

`added` holds the assistant turns and the tool results of the question, so later questions see what the tools returned earlier. A chat request rejects with the wallet's message when it fails, for example when the visitor stops the answer from the wallet or when the provider refuses the call.

## Ask from a component [#ask-from-a-component]

`McpToolSource` reads the tools from your MCP server; the [MCP guide](/docs/guides/mcp-tools) shows how to serve one. The chat reads the session from the connection state when the visitor asks.

```svelte title="src/lib/BookChat.svelte"
<script lang="ts">
  import { McpToolSource } from '@aiconnect/core';
  import { connection } from './aiConnect.svelte';
  import { Chat } from './chat.svelte';

  const SYSTEM = 'You recommend books from this site\'s catalog. Always search the catalog before recommending.';

  // Built once: the source keeps its MCP connection and the tool list it read.
  const chat = new Chat(SYSTEM, new McpToolSource('/api/mcp', 'Bookshop'));
  const current = $derived(connection.state);
  let question = $state('');

  function submit(event: SubmitEvent) {
    event.preventDefault();
    const text = question.trim();
    if (current.status !== 'connected' || !text) {
      return;
    }
    question = '';
    chat.ask(current.session, text);
  }
</script>

{#if current.status === 'connected'}
  <form onsubmit={submit}>
    <label for="question">Your question</label>
    <input id="question" bind:value={question} autocomplete="off" placeholder="A calm book for tonight" />
    <button type="submit" disabled={chat.busy}>Ask</button>
    {#each chat.toolCalls as call (call.id)}
      <p class="tool">{call.name} {JSON.stringify(call.arguments)}</p>
    {/each}
    <p class="answer">{chat.answer}</p>
    {#if chat.error}
      <p role="alert">{chat.error}</p>
    {/if}
  </form>
{:else}
  <p>Connect your AI to ask for a book that fits your mood.</p>
{/if}
```

Place both components in your app:

```svelte title="src/App.svelte"
<script lang="ts">
  import BookChat from './lib/BookChat.svelte';
  import ConnectButton from './lib/ConnectButton.svelte';
</script>

<main>
  <h1>Bookshop</h1>
  <ConnectButton />
  <BookChat />
</main>
```

`ask` does not queue questions, which is why the button stays disabled while `busy` is true.

## Try it without a server [#try-it-without-a-server]

Any object with `listTools` and `callTool` is a tool source. Pass the `demoTools` catalog of the [React quickstart](/docs/quickstart/react#try-it-without-a-server) to `new Chat` in place of the `McpToolSource` to try the chat before your MCP server exists.

## Run it [#run-it]

1. Start Vite with `npm run dev`. AI Wallet serves pages on HTTPS and on `http://localhost` or `http://127.0.0.1`.
2. Open the page in Chrome with AI Wallet installed and an account connected in the wallet.
3. Click **Connect your AI** and approve your site in the window the wallet opens.
4. Ask a question. The tool calls show as they run, and the answer streams from the account the wallet chose for your site.

The [core reference](/docs/reference/core) lists every export with its options.
