Conversation

The transcript stays pinned to the latest message as output arrives. Scrolling up pauses following, and the scroll button jumps back to the bottom.

Investigate why checkout latency rose after 14:00 UTC. Focus on the latest release and give me a safe mitigation.

Separated provider, database, and inventory spans, then matched the first regression to the release boundary.

What changed

The latency increase starts in POST /checkout immediately after release web-2418.

  • Address validation added 430 ms at p95.
  • The provider timed out for 8% of non-US requests.
  • Database and inventory spans stayed within baseline.

Roll back the synchronous validation call, then keep the rule behind the existing review queue.

Show me the smallest rollback and how to verify it.

Use the targeted flag first:

await flags.disable('checkout.address-verification');
await metrics.waitForRecovery({ window: "10m" });

This avoids reverting the unrelated tax fixes in the same release.

<script lang="ts">
    import * as Conversation from '@sivir-ui/svelte/components/conversation';
    import { Markdown } from '@sivir-ui/svelte/components/markdown';
    import * as Message from '@sivir-ui/svelte/components/message';
    import * as Reasoning from '@sivir-ui/svelte/components/reasoning';

    const finding = [
        '### What changed',
        '',
        'The latency increase starts in `POST /checkout` immediately after release `web-2418`.',
        '',
        '- Address validation added **430 ms** at p95.',
        '- The provider timed out for 8% of non-US requests.',
        '- Database and inventory spans stayed within baseline.',
        '',
        '> Roll back the synchronous validation call, then keep the rule behind the existing review queue.'
    ].join('\n');

    const rollback = [
        'Use the targeted flag first:',
        '',
        '```ts',
        "await flags.disable('checkout.address-verification');",
        'await metrics.waitForRecovery({ window: "10m" });',
        '```',
        '',
        'This avoids reverting the unrelated tax fixes in the same release.'
    ].join('\n');
</script>

<Conversation.Root
    class="h-[30rem] w-full max-w-3xl rounded-[var(--radius-xl)] border border-border bg-panel"
>
    <Conversation.Content aria-label="Checkout incident conversation">
        <Message.Root from="user">
            <Message.Content>
                Investigate why checkout latency rose after 14:00 UTC. Focus on the latest release
                and give me a safe mitigation.
            </Message.Content>
        </Message.Root>

        <Message.Root from="assistant">
            <Message.Content class="space-y-4">
                <Reasoning.Root open>
                    <Reasoning.Trigger summary="Compared traces with the release timeline" />
                    <Reasoning.Content>
                        <p>
                            Separated provider, database, and inventory spans, then matched the
                            first regression to the release boundary.
                        </p>
                    </Reasoning.Content>
                </Reasoning.Root>
                <Markdown content={finding} />
            </Message.Content>
        </Message.Root>

        <Message.Root from="user">
            <Message.Content>Show me the smallest rollback and how to verify it.</Message.Content>
        </Message.Root>

        <Message.Root from="assistant">
            <Message.Content><Markdown content={rollback} /></Message.Content>
        </Message.Root>
    </Conversation.Content>
    <Conversation.ScrollButton />
</Conversation.Root>

Installation

Copy the command below and run it in your terminal.

bunx @sivir-ui/svelte add conversation

Usage

Give Root a fixed height so Content can scroll. follow is bindable and starts as true. It turns off when the user scrolls up and back on within threshold pixels of the bottom (80 by default).

import * as Conversation from '@sivir-ui/svelte/components/conversation';
import * as Message from '@sivir-ui/svelte/components/message';

let follow = $state(true);

<Conversation.Root bind:follow class="h-[32rem]">
  <Conversation.Content aria-label="Support conversation">
    <Message.Root from="assistant">
      <Message.Content>I found the failed request.</Message.Content>
    </Message.Root>
  </Conversation.Content>
  <Conversation.ScrollButton />
</Conversation.Root>

Examples

Empty state

Plan the next release

Turn an issue or change set into a staged rollout with health checks.

<script lang="ts">
    import FileText from '@lucide/svelte/icons/file-text';
    import { Button } from '@sivir-ui/svelte/components/button';
    import * as Conversation from '@sivir-ui/svelte/components/conversation';
    import * as Message from '@sivir-ui/svelte/components/message';

    let started = $state(false);
</script>

{#snippet emptyIcon()}
    <FileText size={18} strokeWidth={1.75} aria-hidden="true" />
{/snippet}

{#snippet startAction()}
    <Button size="md" onclick={() => (started = true)}>Draft a release plan</Button>
{/snippet}

<Conversation.Root
    class="h-[22rem] w-full max-w-2xl rounded-[var(--radius-xl)] border border-border bg-panel"
>
    <Conversation.Content aria-label="Release planning conversation">
        {#if started}
            <Message.Root from="user">
                <Message.Content
                    >Draft a release plan for the checkout reliability fix.</Message.Content
                >
            </Message.Root>
            <Message.Root from="assistant">
                <Message.Content>
                    Stage 1 ships the fix to 5% of traffic for 30 minutes. Roll back if checkout
                    errors pass 0.5%.
                </Message.Content>
            </Message.Root>
        {:else}
            <Conversation.Empty
                icon={emptyIcon}
                title="Plan the next release"
                description="Turn an issue or change set into a staged rollout with health checks."
                action={startAction}
            />
        {/if}
    </Conversation.Content>
</Conversation.Root>

Follow live output

Following latest output

Queued release web-2419
Built application bundle in 18.2s
Uploaded 42 immutable assets
Deployed canary to iad1
Passed checkout smoke tests
Shifted 10% of production traffic
Observed p95 latency at 612 ms

Scroll up to pause following. Append more output, then use the arrow to return to the latest step.

<script lang="ts">
    import Plus from '@lucide/svelte/icons/plus';
    import { Button } from '@sivir-ui/svelte/components/button';
    import * as Conversation from '@sivir-ui/svelte/components/conversation';
    import * as Message from '@sivir-ui/svelte/components/message';

    let follow = $state(true);
    let output = $state([
        { id: 1, label: 'Queued release web-2419' },
        { id: 2, label: 'Built application bundle in 18.2s' },
        { id: 3, label: 'Uploaded 42 immutable assets' },
        { id: 4, label: 'Deployed canary to iad1' },
        { id: 5, label: 'Passed checkout smoke tests' },
        { id: 6, label: 'Shifted 10% of production traffic' },
        { id: 7, label: 'Observed p95 latency at 612 ms' }
    ]);

    const updates = [
        'Shifted 25% of production traffic',
        'Observed p95 latency at 584 ms',
        'Passed payment-provider health check',
        'Shifted 50% of production traffic'
    ];
    const followLabel = $derived(follow ? 'Following latest output' : 'Follow paused');

    function appendOutput() {
        const index = output.length - 7;
        output.push({
            id: output.length + 1,
            label: updates[index % updates.length]
        });
    }
</script>

<div class="w-full max-w-2xl space-y-3">
    <div class="flex flex-wrap items-center justify-between gap-3">
        <p class="flex items-center gap-2 text-sm text-foreground-muted" role="status">
            <span class="size-1.5 rounded-full bg-primary" aria-hidden="true"></span>
            {followLabel}
        </p>
        <Button variant="ghost" size="md" onclick={appendOutput}>
            <Plus size={14} aria-hidden="true" />
            Append output
        </Button>
    </div>

    <Conversation.Root
        bind:follow
        class="h-[22rem] rounded-[var(--radius-xl)] border border-border bg-panel"
    >
        <Conversation.Content aria-label="Live deployment output">
            {#each output as entry (entry.id)}
                <Message.Root from="assistant">
                    <Message.Content>{entry.label}</Message.Content>
                </Message.Root>
            {/each}
        </Conversation.Content>
        <Conversation.ScrollButton />
    </Conversation.Root>

    <p class="text-xs leading-5 text-foreground-muted">
        Scroll up to pause following. Append more output, then use the arrow to return to the latest
        step.
    </p>
</div>

Anatomy

Conversation.Root - Follows new output until the user scrolls up.

Conversation.Content - Scrolls the list of messages.

Conversation.Empty - Shows a title and prompt before any messages.

Conversation.ScrollButton - Scrolls to the latest message.