Skip to content
edmi-ui

Session Panel

Chat side panel: Progress, Outputs with preview and file list, and what was used in this session.

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Outputs

RESEARCH · OCT 2026
MAG4 rebalance, three trades
Rebalance report
Artifact

Used in this session

Web searchJUCOOR+4
MemoryRead · Keeper, MAG4, fees

Outputs

RESEARCH · OCT 2026
MAG4 rebalance, three trades
Rebalance report
Artifact

Used in this session

Web searchJUCOOR+4
Memory Read · Keeper, MAG4, fees

Outputs

RESEARCH · OCT 2026
MAG4 rebalance, three trades
Rebalance report
Artifact

Used in this session

Web search JUCOOR +4
Memory Read · Keeper, MAG4, fees

Edmi ✦ addition. Built on card, collapsible, progress, separator, button and ai-artifact-card (file icons).

Terminal window
npx shadcn@latest add @edmi-ui/ai-session-panel
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/ai-session-panel
Terminal window
yarn dlx shadcn@latest add @edmi-ui/ai-session-panel
Terminal window
bunx shadcn@latest add @edmi-ui/ai-session-panel

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/ai-session-panel
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/ai-session-panel
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/ai-session-panel
Terminal window
bunx shadcn-vue@latest add @edmi-ui/ai-session-panel

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-session-panel.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-session-panel.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-session-panel.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-session-panel.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import {
SessionFile,
SessionOutputPreview,
SessionOutputTitle,
SessionPanel,
SessionPanelDivider,
SessionProgress,
SessionSection,
} from "@/components/ai/session-panel";
<SessionPanel>
<SessionProgress value={60}>
Reading the keeper config, then drafting the report.
</SessionProgress>
<SessionPanelDivider />
<SessionSection title="Outputs">
<SessionOutputPreview className="bg-[#14213d] px-5 py-[18px]">
<div className="font-mono text-[7px] tracking-[1px] text-[#e39a3c]">
RESEARCH · OCT 2026
</div>
<div className="mt-9 font-serif text-[17px] leading-[1.2] font-bold text-white">
MAG4 rebalance, three trades
</div>
</SessionOutputPreview>
<SessionOutputTitle meta="Artifact">
Rebalance report
</SessionOutputTitle>
<div className="mt-1">
<SessionFile name="Rebalance report" format="PDF" />
<SessionFile name="Rebalance report" format="MD" kind="code" />
</div>
</SessionSection>
<SessionPanelDivider />
{/* … */}
</SessionPanel>
<script setup lang="ts">
import { Button } from "@/components/ui/button";
</script>
<template>
<Button size="sm" variant="outline">Show session</Button>
<!-- … -->
</template>
<script lang="ts">
import { Button } from "$lib/components/ui/button";
</script>
{#if !open}
<Button size="sm" variant="outline">Show session</Button>
{:else}
<!-- … -->
registry/components/ai/session-panel.tsx
"use client";
import { cn } from "cn";
import type { ComponentProps, ReactNode } from "react";
import { CaretRightIcon, XIcon } from "@phosphor-icons/react";
import {
type ArtifactKind,
ArtifactKindIcon,
} from "@/registry/edmi/components/ai/artifact-card";
import { Button } from "@/registry/edmi/ui/button";
import { Card } from "@/registry/edmi/ui/card";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/registry/edmi/ui/collapsible";
import type { Elevation } from "@/registry/edmi/ui/elevation";
import { Progress } from "@/registry/edmi/ui/progress";
import { Separator } from "@/registry/edmi/ui/separator";
export type SessionPanelProps = Omit<
ComponentProps<typeof Card>,
"elevation"
> & {
/** ✦ depth: sunken -1, flat 0, raised +1, floating +2. */
elevation?: Elevation;
};
/** Chat side panel: Progress, Outputs and what was used in this session. */
export const SessionPanel = ({
className,
elevation,
...props
}: SessionPanelProps) => (
<Card
data-slot="ai-session-panel"
elevation={elevation}
className={cn("w-full max-w-[380px] gap-0 px-4 py-3.5", className)}
{...props}
/>
);
export type SessionPanelDividerProps = ComponentProps<typeof Separator>;
export const SessionPanelDivider = ({
className,
...props
}: SessionPanelDividerProps) => (
<Separator className={cn("my-3", className)} {...props} />
);
export type SessionProgressProps = ComponentProps<typeof Collapsible> & {
title?: ReactNode;
/** 0 to 100: shows a progress bar above the content. */
value?: number;
onClose?: () => void;
};
/** Collapsible "Progress" section with the panel close button. */
export const SessionProgress = ({
title = "Progress",
value,
onClose,
className,
children,
...props
}: SessionProgressProps) => (
<Collapsible
data-slot="ai-session-progress"
className={cn("w-full", className)}
{...props}
>
<div className="flex items-center justify-between">
<CollapsibleTrigger className="group/trigger flex items-center gap-1 text-sm outline-none focus-visible:underline">
{title}
<CaretRightIcon
className="size-3.5 text-muted-foreground transition-transform group-data-[panel-open]/trigger:rotate-90"
/>
</CollapsibleTrigger>
{onClose && (
<Button
aria-label="Close"
onClick={onClose}
size="icon-xs"
type="button"
variant="ghost"
>
<XIcon
className="size-3.5"
/>
</Button>
)}
</div>
<CollapsibleContent className="pt-3 text-[13px] text-muted-foreground">
{value !== undefined && <Progress value={value} className="mb-3" />}
{children}
</CollapsibleContent>
</Collapsible>
);
export type SessionSectionProps = Omit<ComponentProps<"section">, "title"> & {
title: ReactNode;
};
export const SessionSection = ({
title,
className,
children,
...props
}: SessionSectionProps) => (
<section
data-slot="ai-session-section"
className={cn("flex flex-col", className)}
{...props}
>
<h3 className="mb-2.5 text-sm font-normal">{title}</h3>
{children}
</section>
);
export type SessionOutputPreviewProps = ComponentProps<"div">;
/** Large preview of the main output (render the document, slide or image inside). */
export const SessionOutputPreview = ({
className,
...props
}: SessionOutputPreviewProps) => (
<div
data-slot="ai-session-output-preview"
className={cn(
"h-[150px] overflow-hidden rounded-[var(--radius)] border border-border bg-muted",
className,
)}
{...props}
/>
);
export type SessionOutputTitleProps = ComponentProps<"div"> & {
/** Small line under the title (`Artifact`). */
meta?: ReactNode;
};
export const SessionOutputTitle = ({
meta,
className,
children,
...props
}: SessionOutputTitleProps) => (
<div
data-slot="ai-session-output-title"
className={cn("mt-2", className)}
{...props}
>
<div className="text-sm">{children}</div>
{meta && (
<div className="font-mono text-[12.5px] text-muted-foreground">
{meta}
</div>
)}
</div>
);
export type SessionFileProps = Omit<
ComponentProps<"button">,
"children" | "name"
> & {
name: ReactNode;
/** Format badge on the right (`PDF`, `MD`). */
format?: ReactNode;
kind?: ArtifactKind;
};
export const SessionFile = ({
name,
format,
kind = "document",
className,
...props
}: SessionFileProps) => (
<button
data-slot="ai-session-file"
type="button"
className={cn(
"flex w-full items-center gap-2.5 rounded-md py-[7px] text-left text-sm outline-none hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring",
className,
)}
{...props}
>
<span className="text-muted-foreground">
<ArtifactKindIcon kind={kind} className="size-4" />
</span>
<span className="min-w-0 flex-1 truncate">{name}</span>
{format && (
<span className="font-mono text-[11.5px] text-muted-foreground">
{format}
</span>
)}
</button>
);
export type SessionSourceFavicon = {
label: string;
/** Favicon image; initials on `color` when omitted. */
src?: string;
/** Brand color of the initials tile (data, not themed). */
color?: string;
};
export type SessionSourceProps = Omit<ComponentProps<"div">, "children"> & {
icon: ReactNode;
label: ReactNode;
favicons?: SessionSourceFavicon[];
/** Count after the favicons (`+4`). */
more?: number;
/** Detail line after the label (memory: `Read · Career, Tech`). */
children?: ReactNode;
};
/** One row of "Used in this session": web search, memory, tools. */
export const SessionSource = ({
icon,
label,
favicons,
more,
className,
children,
...props
}: SessionSourceProps) => (
<div
data-slot="ai-session-source"
className={cn("flex items-center gap-2.5 py-[7px] text-sm", className)}
{...props}
>
<span className="text-muted-foreground [&_svg]:size-4">{icon}</span>
<span className={cn(!children && "flex-1")}>{label}</span>
{children && (
<span className="min-w-0 flex-1 truncate text-[12.5px] text-muted-foreground">
{children}
</span>
)}
{favicons && favicons.length > 0 && (
<span className="flex items-center">
{favicons.map((f) =>
f.src ? (
<img
key={f.label}
alt={f.label}
src={f.src}
className="-ml-[3px] size-4 rounded-[4px] border border-card object-cover"
/>
) : (
<span
key={f.label}
className="-ml-[3px] inline-flex size-4 items-center justify-center rounded-[4px] border border-card bg-muted-foreground text-[8px] font-bold text-white"
style={f.color ? { background: f.color } : undefined}
>
{f.label.slice(0, 2).toUpperCase()}
</span>
),
)}
</span>
)}
{more ? (
<span className="text-[12.5px] text-muted-foreground">+{more}</span>
) : null}
</div>
);
registry/components/ai/session-panel/SessionFile.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import type { ArtifactKind } from "@/registry/edmi/components/ai/artifact-card"
import { ArtifactKindIcon } from "@/registry/edmi/components/ai/artifact-card"
import { cn } from "@/registry/edmi/lib/utils"
const props = withDefaults(defineProps<{
class?: HTMLAttributes["class"]
/** File name; the `name` slot overrides it. */
name?: string
/** Format badge on the right (`PDF`, `MD`); the `format` slot overrides it. */
format?: string
kind?: ArtifactKind
}>(), {
kind: "document",
})
</script>
<template>
<button
data-slot="ai-session-file"
type="button"
:class="cn('flex w-full items-center gap-2.5 rounded-md py-[7px] text-left text-sm outline-none hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring', props.class)"
>
<span class="text-muted-foreground">
<ArtifactKindIcon :kind="kind" class="size-4" />
</span>
<span class="min-w-0 flex-1 truncate"><slot name="name">{{ name }}</slot></span>
<span v-if="format || $slots.format" class="font-mono text-[11.5px] text-muted-foreground"><slot name="format">{{ format }}</slot></span>
</button>
</template>
registry/components/ai/session-panel/SessionOutputPreview.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<!-- Large preview of the main output (render the document, slide or image inside). -->
<div
data-slot="ai-session-output-preview"
:class="cn('h-[150px] overflow-hidden rounded-[var(--radius)] border border-border bg-muted', props.class)"
>
<slot />
</div>
</template>
registry/components/ai/session-panel/SessionOutputTitle.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
/** Small line under the title (`Artifact`); the `meta` slot overrides it. */
meta?: string
}>()
</script>
<template>
<div data-slot="ai-session-output-title" :class="cn('mt-2', props.class)">
<div class="text-sm">
<slot />
</div>
<div v-if="meta || $slots.meta" class="font-mono text-[12.5px] text-muted-foreground">
<slot name="meta">
{{ meta }}
</slot>
</div>
</div>
</template>
registry/components/ai/session-panel/SessionPanel.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { Card } from "@/registry/edmi/ui/card"
import type { Elevation } from "@/registry/edmi/ui/elevation"
const props = withDefaults(defineProps<{
class?: HTMLAttributes["class"]
/** ✦ depth: sunken -1, flat 0, raised +1, floating +2. */
elevation?: Elevation
}>(), {
elevation: undefined,
})
</script>
<template>
<Card data-slot="ai-session-panel" :elevation="elevation" :class="cn('w-full max-w-[380px] gap-0 px-4 py-3.5', props.class)">
<slot />
</Card>
</template>
registry/components/ai/session-panel/SessionPanelDivider.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { Separator } from "@/registry/edmi/ui/separator"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<Separator :class="cn('my-3', props.class)" />
</template>
registry/components/ai/session-panel/SessionProgress.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { ChevronRight, X } from "@lucide/vue"
import { getCurrentInstance } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { Button } from "@/registry/edmi/ui/button"
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/registry/edmi/ui/collapsible"
import { Progress } from "@/registry/edmi/ui/progress"
const props = withDefaults(defineProps<{
class?: HTMLAttributes["class"]
/** Heading text; the `title` slot overrides it. */
title?: string
/** 0 to 100: shows a progress bar above the content. */
value?: number
defaultOpen?: boolean
}>(), {
title: "Progress",
defaultOpen: false,
})
const emit = defineEmits<{
(e: "close"): void
}>()
// The close button shows when a `close` listener is attached (same idea as `stop` on the submit button).
const hasClose = !!getCurrentInstance()?.vnode.props?.onClose
</script>
<template>
<!-- Collapsible "Progress" section with the panel close button. -->
<Collapsible data-slot="ai-session-progress" :default-open="defaultOpen" :class="cn('w-full', props.class)">
<div class="flex items-center justify-between">
<CollapsibleTrigger class="group/trigger flex items-center gap-1 text-sm outline-none focus-visible:underline">
<slot name="title">
{{ title }}
</slot>
<ChevronRight class="size-3.5 text-muted-foreground transition-transform group-data-[state=open]/trigger:rotate-90" />
</CollapsibleTrigger>
<Button v-if="hasClose" aria-label="Close" size="icon-xs" type="button" variant="ghost" @click="emit('close')">
<X class="size-3.5" />
</Button>
</div>
<CollapsibleContent class="pt-3 text-[13px] text-muted-foreground">
<Progress v-if="value !== undefined" :model-value="value" class="mb-3" />
<slot />
</CollapsibleContent>
</Collapsible>
</template>
registry/components/ai/session-panel/SessionSection.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
/** Heading text; the `title` slot overrides it. */
title?: string
}>()
</script>
<template>
<section data-slot="ai-session-section" :class="cn('flex flex-col', props.class)">
<h3 class="mb-2.5 text-sm font-normal">
<slot name="title">
{{ title }}
</slot>
</h3>
<slot />
</section>
</template>
registry/components/ai/session-panel/SessionSource.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import type { SessionSourceFavicon } from "./types"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
/** Row label; the `label` slot overrides it. */
label?: string
favicons?: SessionSourceFavicon[]
/** Count after the favicons (`+4`). */
more?: number
}>()
</script>
<template>
<!-- One row of "Used in this session": web search, memory, tools. Slots: `icon`, default = detail line. -->
<div data-slot="ai-session-source" :class="cn('flex items-center gap-2.5 py-[7px] text-sm', props.class)">
<span class="text-muted-foreground [&_svg]:size-4">
<slot name="icon" />
</span>
<span :class="cn(!$slots.default && 'flex-1')"><slot name="label">{{ label }}</slot></span>
<span v-if="$slots.default" class="min-w-0 flex-1 truncate text-[12.5px] text-muted-foreground">
<slot />
</span>
<span v-if="favicons && favicons.length > 0" class="flex items-center">
<template v-for="f in favicons" :key="f.label">
<img
v-if="f.src"
:alt="f.label"
:src="f.src"
class="-ml-[3px] size-4 rounded-[4px] border border-card object-cover"
>
<span
v-else
class="-ml-[3px] inline-flex size-4 items-center justify-center rounded-[4px] border border-card bg-muted-foreground text-[8px] font-bold text-white"
:style="f.color ? { background: f.color } : undefined"
>
{{ f.label.slice(0, 2).toUpperCase() }}
</span>
</template>
</span>
<span v-if="more" class="text-[12.5px] text-muted-foreground">+{{ more }}</span>
</div>
</template>
registry/components/ai/session-panel/types.ts
import type { ArtifactKind } from "@/registry/edmi/components/ai/artifact-card";
export type SessionSourceFavicon = {
label: string;
/** Favicon image; initials on `color` when omitted. */
src?: string;
/** Brand color of the initials tile (data, not themed). */
color?: string;
};
export type { ArtifactKind };
registry/components/ai/session-panel/index.ts
export { default as SessionFile } from "./SessionFile.vue";
export { default as SessionOutputPreview } from "./SessionOutputPreview.vue";
export { default as SessionOutputTitle } from "./SessionOutputTitle.vue";
export { default as SessionPanel } from "./SessionPanel.vue";
export { default as SessionPanelDivider } from "./SessionPanelDivider.vue";
export { default as SessionProgress } from "./SessionProgress.vue";
export { default as SessionSection } from "./SessionSection.vue";
export { default as SessionSource } from "./SessionSource.vue";
export * from "./types";
src/lib/registry/ai/session-panel/index.ts
import SessionFile from "./session-file.svelte";
import SessionOutputPreview from "./session-output-preview.svelte";
import SessionOutputTitle from "./session-output-title.svelte";
import SessionPanel from "./session-panel.svelte";
import SessionPanelDivider from "./session-panel-divider.svelte";
import SessionProgress from "./session-progress.svelte";
import SessionSection from "./session-section.svelte";
import SessionSource from "./session-source.svelte";
export * from "./types.js";
export {
SessionFile,
SessionOutputPreview,
SessionOutputTitle,
SessionPanel,
SessionPanelDivider,
SessionProgress,
SessionSection,
SessionSource,
};
src/lib/registry/ai/session-panel/session-file.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { Snippet } from "svelte";
import type { HTMLButtonAttributes } from "svelte/elements";
import { ArtifactKindIcon, type ArtifactKind } from "../artifact-card/index.js";
let {
ref = $bindable(null),
name,
format,
kind = "document",
class: className,
...restProps
}: WithElementRef<Omit<HTMLButtonAttributes, "children" | "name">, HTMLButtonElement> & {
name: string | Snippet;
/** Format badge on the right (`PDF`, `MD`). */
format?: string | Snippet;
kind?: ArtifactKind;
} = $props();
</script>
<button
bind:this={ref}
data-slot="ai-session-file"
type="button"
class={cn(
"flex w-full items-center gap-2.5 rounded-md py-[7px] text-left text-sm outline-none hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring",
className
)}
{...restProps}
>
<span class="text-muted-foreground">
<ArtifactKindIcon {kind} class="size-4" />
</span>
<span class="min-w-0 flex-1 truncate">{#if typeof name === "string"}{name}{:else}{@render name()}{/if}</span>
{#if typeof format === "string"}
<span class="font-mono text-[11.5px] text-muted-foreground">{format}</span>
{:else if format}
<span class="font-mono text-[11.5px] text-muted-foreground">{@render format()}</span>
{/if}
</button>
src/lib/registry/ai/session-panel/session-output-preview.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>, HTMLDivElement> = $props();
</script>
<!-- Large preview of the main output (render the document, slide or image inside). -->
<div
bind:this={ref}
data-slot="ai-session-output-preview"
class={cn(
"h-[150px] overflow-hidden rounded-[var(--radius)] border border-border bg-muted",
className
)}
{...restProps}
>
{@render children?.()}
</div>
src/lib/registry/ai/session-panel/session-output-title.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { Snippet } from "svelte";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
meta,
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
/** Small line under the title (`Artifact`). */
meta?: string | Snippet;
} = $props();
</script>
<div bind:this={ref} data-slot="ai-session-output-title" class={cn("mt-2", className)} {...restProps}>
<div class="text-sm">{@render children?.()}</div>
{#if meta}
<div class="font-mono text-[12.5px] text-muted-foreground">{#if typeof meta === "string"}{meta}{:else}{@render meta()}{/if}</div>
{/if}
</div>
src/lib/registry/ai/session-panel/session-panel-divider.svelte
<script lang="ts">
import { Separator } from "$lib/registry/ui/separator/index.js";
import { cn } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let { ref = $bindable(null), class: className, ...restProps }: ComponentProps<typeof Separator> = $props();
</script>
<Separator bind:ref class={cn("my-3", className)} {...restProps} />
src/lib/registry/ai/session-panel/session-panel.svelte
<script lang="ts">
import { Card } from "$lib/registry/ui/card/index.js";
import type { Elevation } from "$lib/registry/ui/elevation/index.js";
import { cn } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
class: className,
children,
elevation = "auto",
...restProps
}: Omit<ComponentProps<typeof Card>, "elevation"> & {
/** ✦ depth: sunken -1, flat 0, raised +1, floating +2. */
elevation?: Elevation;
} = $props();
</script>
<Card
bind:ref
data-slot="ai-session-panel"
{elevation}
class={cn("w-full max-w-[380px] gap-0 px-4 py-3.5", className)}
{...restProps}
>
{@render children?.()}
</Card>
src/lib/registry/ai/session-panel/session-progress.svelte
<script lang="ts">
import CaretRightIcon from "phosphor-svelte/lib/CaretRight";
import XIcon from "phosphor-svelte/lib/X";
import { Button } from "$lib/registry/ui/button/index.js";
import * as Collapsible from "$lib/registry/ui/collapsible/index.js";
import { Progress } from "$lib/registry/ui/progress/index.js";
import { cn } from "$lib/utils.js";
import type { Snippet } from "svelte";
let {
ref = $bindable(null),
title = "Progress",
value,
onClose,
defaultOpen = false,
open = $bindable(defaultOpen),
class: className,
children,
}: {
title?: string | Snippet;
ref?: HTMLDivElement | null;
/** 0 to 100: shows a progress bar above the content. */
value?: number;
/** Shows the close button. */
onClose?: () => void;
defaultOpen?: boolean;
open?: boolean;
class?: string;
children?: Snippet;
} = $props();
</script>
<!-- Collapsible "Progress" section with the panel close button. -->
<Collapsible.Root bind:ref data-slot="ai-session-progress" bind:open class={cn("w-full", className)}>
<div class="flex items-center justify-between">
<Collapsible.Trigger
class="group/trigger flex items-center gap-1 text-sm outline-none focus-visible:underline"
>
{#if typeof title === "string"}{title}{:else}{@render title()}{/if}
<CaretRightIcon
class="size-3.5 text-muted-foreground transition-transform group-data-[state=open]/trigger:rotate-90"
/>
</Collapsible.Trigger>
{#if onClose}
<Button aria-label="Close" onclick={onClose} size="icon-xs" type="button" variant="ghost">
<XIcon
class="size-3.5"
/>
</Button>
{/if}
</div>
<Collapsible.Content class="pt-3 text-[13px] text-muted-foreground">
{#if value !== undefined}
<Progress {value} class="mb-3" />
{/if}
{@render children?.()}
</Collapsible.Content>
</Collapsible.Root>
src/lib/registry/ai/session-panel/session-section.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { Snippet } from "svelte";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
title,
class: className,
children,
...restProps
}: WithElementRef<Omit<HTMLAttributes<HTMLElement>, "title">> & { title: string | Snippet } = $props();
</script>
<section bind:this={ref} data-slot="ai-session-section" class={cn("flex flex-col", className)} {...restProps}>
<h3 class="mb-2.5 text-sm font-normal">{#if typeof title === "string"}{title}{:else}{@render title()}{/if}</h3>
{@render children?.()}
</section>
src/lib/registry/ai/session-panel/session-source.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { Snippet } from "svelte";
import type { HTMLAttributes } from "svelte/elements";
import type { SessionSourceFavicon } from "./types.js";
let {
ref = $bindable(null),
icon,
label,
favicons,
more,
class: className,
children,
...restProps
}: WithElementRef<Omit<HTMLAttributes<HTMLDivElement>, "children">, HTMLDivElement> & {
icon: Snippet;
label: string | Snippet;
favicons?: SessionSourceFavicon[];
/** Count after the favicons (`+4`). */
more?: number;
/** Detail line after the label (memory: `Read · Career, Tech`). */
children?: Snippet;
} = $props();
</script>
<!-- One row of "Used in this session": web search, memory, tools. -->
<div
bind:this={ref}
data-slot="ai-session-source"
class={cn("flex items-center gap-2.5 py-[7px] text-sm", className)}
{...restProps}
>
<span class="text-muted-foreground [&_svg]:size-4">{@render icon()}</span>
<span class={cn(!children && "flex-1")}>{#if typeof label === "string"}{label}{:else}{@render label()}{/if}</span>
{#if children}
<span class="min-w-0 flex-1 truncate text-[12.5px] text-muted-foreground">
{@render children()}
</span>
{/if}
{#if favicons && favicons.length > 0}
<span class="flex items-center">
{#each favicons as f (f.label)}
{#if f.src}
<img
alt={f.label}
src={f.src}
class="-ml-[3px] size-4 rounded-[4px] border border-card object-cover"
/>
{:else}
<span
class="-ml-[3px] inline-flex size-4 items-center justify-center rounded-[4px] border border-card bg-muted-foreground text-[8px] font-bold text-white"
style={f.color ? `background: ${f.color}` : undefined}
>
{f.label.slice(0, 2).toUpperCase()}
</span>
{/if}
{/each}
</span>
{/if}
{#if more}
<span class="text-[12.5px] text-muted-foreground">+{more}</span>
{/if}
</div>
src/lib/registry/ai/session-panel/types.ts
export type SessionSourceFavicon = {
label: string;
/** Favicon image; initials on `color` when omitted. */
src?: string;
/** Brand color of the initials tile (data, not themed). */
color?: string;
};

See the Elevation guide for the levels, roles and layered mode.

Flat by default and a surface in layered mode. sunken (-1) is a soft inset, raised (+1) a bevel and floating (+2) a bevel with a soft drop.

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Sunken (-1)

Outputs

Rebalance report
Artifact

Flat (0)

Outputs

Rebalance report
Artifact

Raised (+1)

Outputs

Rebalance report
Artifact

Floating (+2)

Outputs

Rebalance report
Artifact

Sunken (-1)

Outputs

Rebalance report
Artifact

Flat (0)

Outputs

Rebalance report
Artifact

Raised (+1)

Outputs

Rebalance report
Artifact

Floating (+2)

Outputs

Rebalance report
Artifact

Sunken (-1)

Outputs

Rebalance report
Artifact

Flat (0)

Outputs

Rebalance report
Artifact

Raised (+1)

Outputs

Rebalance report
Artifact

Floating (+2)

Outputs

Rebalance report
Artifact
Part Prop Type Default
SessionPanel elevation, Card props "auto" | "sunken" | "flat" | "raised" | "floating" "auto"
SessionProgress title, value, onClose, Collapsible props ReactNode, 0-100, () => void Progress
SessionSection title ReactNode
SessionOutputPreview box for the large preview
SessionOutputTitle meta ReactNode
SessionFile name, format, kind ReactNode, ReactNode, ArtifactKind document
SessionSource icon, label, favicons, more, children web search, memory, tools row

In Vue ReactNode props are slots of the same name (the string prop stays as a fallback); in Svelte they accept string | Snippet.

  • SessionSource favicons stack with a count after them (+4); the detail line (Read · Career, Tech) goes in children.