000h-cojeev
uiagent-chatsource: ok · hit
Agent chat
A composable agent conversation with a bounded thread, draft composer, attachments, progress, explicit permissions and choice prompts.
npx shadcn add "https://000h.cojeev.com/r/agent-chat.json"free
Where it goes
chat
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightRegistry dependencies
https://cojeev.com/ui/r/cojeev.jsonhttps://cojeev.com/ui/r/icon.jsonhttps://cojeev.com/ui/r/agent-state.jsonhttps://cojeev.com/ui/r/button.jsonhttps://cojeev.com/ui/r/message.jsonhttps://cojeev.com/ui/r/message-scroller.jsonhttps://cojeev.com/ui/r/attachment.jsonhttps://cojeev.com/ui/r/questionnaire.jsonhttps://cojeev.com/ui/r/avatar.jsonhttps://cojeev.com/ui/r/bubble.jsonhttps://cojeev.com/ui/r/input.jsonhttps://cojeev.com/ui/r/presence.jsonhttps://cojeev.com/ui/r/input-group.json
Files2 · source cached 10/8/2026, 11:12:38 PM
registry/cojeev/ui/agent-chat.tsx
"use client";
import * as React from "react";
import { Icon } from "@/components/ui/icon";
import { cn } from "@/lib/utils";
import { AgentState, type AgentStatus } from "@/components/ui/agent-state";
import { Button } from "@/components/ui/button";
import { Message, MessageContent, MessageDescription } from "@/components/ui/message";
import { MessageScroller, MessageScrollerJump, type MessageScrollerProps } from "@/components/ui/message-scroller";
import { Attachment, AttachmentType, AttachmentName, AttachmentMeta, AttachmentActions, AttachmentAction } from "@/components/ui/attachment";
import { Questionnaire, QuestionnaireQuestion, QuestionnaireOptions, QuestionnaireOption, QuestionnaireOptionBody } from "@/components/ui/questionnaire";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Bubble, BubbleContent } from "@/components/ui/bubble";
import { Input } from "@/components/ui/input";
import { MotionPresence, MotionSurface } from "@/components/ui/presence";
import { InputGroup, InputGroupAddon, InputGroupTextarea, InputGroupButton } from "@/components/ui/input-group";
export type AgentChatLayoutProps = React.ComponentProps<"div">;
/** Main work stays unconstrained; a dedicated conversation column sits beside it. */
export function AgentChatLayout({ className, ...props }: AgentChatLayoutProps) {
return <div data-slot="agent-chat-layout" className={cn("v-agent-layout", className)} {...props} />;
}
export type AgentChatMainProps = React.ComponentProps<"div">;
export function AgentChatMain({ className, ...props }: AgentChatMainProps) {
return <div data-slot="agent-chat-main" className={cn("v-agent-layout__main", className)} {...props} />;
}
export type AgentChatAsideProps = React.ComponentProps<"aside">;
export function AgentChatAside({ className, ...props }: AgentChatAsideProps) {
return <aside data-slot="agent-chat-aside" className={cn("v-agent-layout__aside", className)} aria-label="Agent side chat" {...props} />;
}
export type AgentChatProps = React.ComponentProps<"section">;
/** A bounded conversation surface. Compose header, thread and composer as children. */
export function AgentChat({ className, ...props }: AgentChatProps) {
return <section data-slot="agent-chat" className={cn("v-agent-chat", className)} aria-label="Agent conversation" {...props} />;
}
export type AgentChatHeaderProps = Omit<React.ComponentProps<"header">, "title"> & {
title?: React.ReactNode;
description?: React.ReactNode;
status?: AgentStatus;
statusLabel?: string;
actions?: React.ReactNode;
};
export function AgentChatHeader({ title = "Cojeev", description, status = "idle", statusLabel, actions, className, children, ...props }: AgentChatHeaderProps) {
return <header data-slot="agent-chat-header" className={cn("v-agent-chat__header", className)} {...props}>
<div className="v-agent-chat__heading"><h2>{title}</h2>{description && <p>{description}</p>}</div>
{actions && <div className="v-agent-chat__header-actions">{actions}</div>}
<AgentState status={status} size="sm" label={statusLabel} />
{children}
</header>;
}
export type AgentChatThreadProps = MessageScrollerProps & { jumpLabel?: string };
/** Retains the reader's position while detached; exposes an explicit jump back. */
export function AgentChatThread({ className, children, jumpLabel = "Latest message", ...props }: AgentChatThreadProps) {
return <Bubble as={MessageScroller} data-slot="agent-chat-thread" className={cn("v-agent-chat__thread", className)} role="log" aria-label="Conversation messages" aria-live="polite" aria-relevant="additions text" tabIndex={0} {...props}>
{children}
<MessageScrollerJump className="v-agent-chat__jump">{jumpLabel}</MessageScrollerJump>
</Bubble>;
}
export type AgentChatMessageProps = React.ComponentProps<"div"> & {
author?: string;
avatar?: React.ReactNode;
from?: "agent" | "user";
meta?: React.ReactNode;
actions?: React.ReactNode;
};
export function AgentChatMessage({ author = "Cojeev", from = "agent", avatar, meta, actions, className, children, ...props }: AgentChatMessageProps) {
return <MotionSurface asChild preset="rise"><Message variant={from === "user" ? "me" : "default"} data-slot="agent-chat-message" data-from={from} className={cn("v-agent-chat__message", className)} {...props}>
{avatar ?? <Avatar variant={from === "user" ? "blue" : "pink"} className="v-agent-chat__avatar" aria-hidden="true"><AvatarFallback>{author.split(/\s+/).map((part) => part[0]).slice(0, 2).join("")}</AvatarFallback></Avatar>}
<MessageContent className="v-agent-chat__message-content">
<MessageDescription className="v-agent-chat__author">{author}{meta && <span>{meta}</span>}</MessageDescription>
<BubbleContent variant={from === "user" ? "me" : "default"} className="v-agent-chat__message-body">{children}</BubbleContent>
{actions && <div className="v-agent-chat__message-actions">{actions}</div>}
</MessageContent>
</Message></MotionSurface>;
}
export type AgentChatAttachment = { id: string; name: string; type?: string; detail?: string };
export type AgentChatComposerProps = Omit<React.ComponentProps<"form">, "onSubmit" | "onChange"> & {
value: string;
onValueChange: (value: string) => void;
onSend: () => void;
status?: AgentStatus;
onStop?: () => void;
attachments?: readonly AgentChatAttachment[];
onAttach?: (files: File[]) => void;
onRemoveAttachment?: (id: string) => void;
accept?: string;
disabled?: boolean;
placeholder?: string;
label?: string;
hint?: React.ReactNode;
error?: React.ReactNode;
onRetry?: () => void;
maxLength?: number;
};
/** Controlled draft and attachments; the host clears them only after accepting send. */
export function AgentChatComposer({ value, onValueChange, onSend, status = "idle", onStop, attachments = [], onAttach, onRemoveAttachment, accept, disabled = false, placeholder = "Ask, explore, or make something…", label = "Message Cojeev", hint, error, onRetry, maxLength = 12000, className, children, ...props }: AgentChatComposerProps) {
const id = React.useId();
const input = React.useRef<HTMLInputElement>(null);
const textarea = React.useRef<HTMLTextAreaElement>(null);
const running = status === "thinking" || status === "working";
const canSend = !disabled && !running && (value.trim().length > 0 || attachments.length > 0) && value.length <= maxLength;
const wasRunning = React.useRef(running);
const restoreDraftFocus = React.useRef(false);
const stopRef = React.useCallback((node: HTMLButtonElement | HTMLAnchorElement | null) => {
if (!node) return;
// Capture focus before React replaces Stop or disables it as the Send control.
return () => { restoreDraftFocus.current = node.ownerDocument.activeElement === node; };
}, []);
React.useLayoutEffect(() => {
if (wasRunning.current && !running && restoreDraftFocus.current) textarea.current?.focus();
restoreDraftFocus.current = false;
wasRunning.current = running;
}, [running]);
return <form data-slot="agent-chat-composer" className={cn("v-agent-chat__composer", className)} onSubmit={(event) => { event.preventDefault(); if (canSend) { textarea.current?.focus(); onSend(); } }} {...props}>
<label className="v-agent-chat__sr-only" htmlFor={`${id}-draft`}>{label}</label>
<MotionPresence>
{attachments.length > 0 && <MotionSurface key="attachments" preset="rise" data-slot="agent-chat-attachments" className="v-agent-chat__attachments">
<MotionPresence>
{attachments.map((file) => <MotionSurface key={file.id} preset="scale" asChild><Attachment className="v-agent-chat__attachment">
<AttachmentType>{file.type ?? <Icon name="file-text" style={{width:16,height:16}} />}</AttachmentType>
<AttachmentName title={file.name}>{file.name}{file.detail && <AttachmentMeta>{file.detail}</AttachmentMeta>}</AttachmentName>
{onRemoveAttachment && <AttachmentActions><AttachmentAction disabled={disabled || running} onClick={() => onRemoveAttachment(file.id)} aria-label={`Remove ${file.name}`}><Icon name="x" style={{width:16,height:16}} aria-hidden="true" /></AttachmentAction></AttachmentActions>}
</Attachment></MotionSurface>)}
</MotionPresence>
</MotionSurface>}
</MotionPresence>
<MotionPresence>{error && <MotionSurface key="error" preset="rise" className="v-agent-chat__error" id={`${id}-error`} role="alert"><span>{error}</span>{onRetry && <Button variant="secondary" size="sm" onClick={onRetry} disabled={disabled || running}>Retry</Button>}</MotionSurface>}</MotionPresence>
<InputGroup className="v-agent-chat__draft">
<InputGroupTextarea ref={textarea} id={`${id}-draft`} className="v-agent-chat__input" rows={3} value={value} maxLength={maxLength} disabled={disabled} placeholder={placeholder} aria-invalid={error ? true : undefined} aria-describedby={`${id}-hint${error ? ` ${id}-error` : ""}`} onChange={(event) => onValueChange(event.target.value)} onKeyDown={(event) => {
if (event.key === "Enter" && (event.metaKey || event.ctrlKey) && !event.nativeEvent.isComposing && canSend) {
event.preventDefault(); onSend();
}
}} />
<InputGroupAddon className="v-agent-chat__composer-bar">
{onAttach && <><Input ref={input} className="v-agent-chat__sr-only" type="file" tabIndex={-1} aria-hidden="true" accept={accept} multiple disabled={disabled || running} onChange={(event) => {
const files = Array.from(event.target.files ?? []); if (files.length) onAttach(files); event.target.value = "";
}} /><InputGroupButton variant="ghost" className="v-agent-chat__attach-button" disabled={disabled || running} onClick={() => input.current?.click()} aria-label="Attach files"><Icon name="paperclip" style={{width:18,height:18}} aria-hidden="true" /></InputGroupButton></>}
<span className="v-agent-chat__composer-note">{running ? "You can prepare your next message" : "A little context goes a long way"}</span>
{running ? <InputGroupButton key="stop" ref={stopRef} className="v-agent-chat__send" data-agent-stop="true" variant="secondary" disabled={disabled || !onStop} aria-label="Stop generation" onClick={() => { onStop?.(); textarea.current?.focus(); }}><Icon name="square" style={{width:14,height:14,fill:"currentColor"}} aria-hidden="true" /><span>Stop</span></InputGroupButton> : <InputGroupButton key="send" className="v-agent-chat__send" type="submit" disabled={!canSend} aria-label="Send message"><Icon name="arrow-up" style={{width:18,height:18}} aria-hidden="true" /><span>Send</span></InputGroupButton>}
</InputGroupAddon>
</InputGroup>
<p className="v-agent-chat__hint" id={`${id}-hint`}>{hint ?? <><Icon name="corner-down-left" style={{width:12,height:12}} /> Enter for a new line · ⌘ / Ctrl + Enter to send</>}</p>
{children}
</form>;
}
export type AgentChatPermissionProps = Omit<React.ComponentProps<"div">, "title"> & {
title: React.ReactNode;
description: React.ReactNode;
scope?: React.ReactNode;
decision?: "pending" | "allowed" | "denied";
onDecision: (decision: "allowed" | "denied") => void;
disabled?: boolean;
allowLabel?: string;
denyLabel?: string;
};
/** No implied consent: pending permissions require a deliberate allow or deny. */
export function AgentChatPermission({ title, description, scope, decision = "pending", onDecision, disabled, allowLabel = "Allow once", denyLabel = "Deny", className, ...props }: AgentChatPermissionProps) {
const id = React.useId();
return <div data-slot="agent-chat-permission" data-decision={decision} className={cn("v-agent-chat__permission", className)} role="group" aria-labelledby={`${id}-title`} aria-describedby={`${id}-description`} {...props}>
<div className="v-agent-chat__decision-title"><Icon name="shield-check" style={{width:19,height:19}} aria-hidden="true" /><h3 id={`${id}-title`}>{title}</h3></div>
<p id={`${id}-description`}>{description}</p>
{scope && <div className="v-agent-chat__scope">{scope}</div>}
<MotionPresence mode="wait">{decision === "pending" ? <MotionSurface key="pending" preset="fade" className="v-agent-chat__decision-actions"><Button variant="secondary" onClick={() => onDecision("denied")} disabled={disabled}>{denyLabel}</Button><Button onClick={() => onDecision("allowed")} disabled={disabled}>{allowLabel}</Button></MotionSurface> : <MotionSurface key={decision} preset="fade" asChild><p className="v-agent-chat__receipt" role="status">{decision === "allowed" ? <Icon name="check" style={{width:16,height:16}} aria-hidden="true" /> : <Icon name="x" style={{width:16,height:16}} aria-hidden="true" />}{decision === "allowed" ? "Permission allowed once" : "Permission denied"}</p></MotionSurface>}</MotionPresence>
</div>;
}
export type AgentChatOption = { value: string; label: string; description?: string; disabled?: boolean };
export type AgentChatOptionsProps = Omit<React.ComponentProps<"div">, "title"> & {
title: string;
options: readonly AgentChatOption[];
value?: string;
onValueChange: (value: string) => void;
onConfirm: (value: string) => void;
disabled?: boolean;
confirmLabel?: string;
};
export function AgentChatOptions({ title, options, value, onValueChange, onConfirm, disabled, confirmLabel = "Continue", className, ...props }: AgentChatOptionsProps) {
const id = React.useId();
const valid = options.some((option) => option.value === value && !option.disabled);
return <Questionnaire data-slot="agent-chat-options" className={cn("v-agent-chat__options", className)} {...props}>
<QuestionnaireQuestion disabled={disabled}>
<legend id={`${id}-title`}>{title}</legend>
<QuestionnaireOptions name={id} value={value ?? ""} onValueChange={onValueChange} aria-labelledby={`${id}-title`}>
<MotionPresence>{options.map((option) => <MotionSurface key={option.value} asChild preset="rise"><QuestionnaireOption value={option.value} disabled={disabled || option.disabled}>
<QuestionnaireOptionBody><b>{option.label}</b>{option.description && <small>{option.description}</small>}</QuestionnaireOptionBody>
</QuestionnaireOption></MotionSurface>)}</MotionPresence>
</QuestionnaireOptions>
</QuestionnaireQuestion>
<Button disabled={disabled || !valid} onClick={() => { if (valid && value !== undefined) onConfirm(value); }}>{confirmLabel}</Button>
</Questionnaire>;
}
export type AgentChatStep = { id: string; label: string; detail?: string; status: "pending" | "current" | "complete" | "error" };
export type AgentChatProgressProps = React.ComponentProps<"ol"> & { steps: readonly AgentChatStep[] };
export function AgentChatProgress({ steps, className, ...props }: AgentChatProgressProps) {
return <ol data-slot="agent-chat-progress" className={cn("v-agent-chat__progress", className)} aria-label="Work progress" {...props}>
<MotionPresence>{steps.map((step, index) => <MotionSurface key={step.id} asChild preset="rise"><li data-state={step.status} aria-current={step.status === "current" ? "step" : undefined}>
<span className="v-agent-chat__step-mark" aria-hidden="true">{step.status === "complete" ? <Icon name="check" style={{width:13,height:13}} /> : step.status === "error" ? <Icon name="x" style={{width:13,height:13}} /> : index + 1}</span>
<span><span className="v-agent-chat__sr-only">{step.status}: </span><b>{step.label}</b>{step.detail && <small>{step.detail}</small>}</span>
</li></MotionSurface>)}</MotionPresence>
</ol>;
}
registry/cojeev/styles/agent-chat.css
@layer cojeev-states {
[data-slot="agent-chat-layout"] { display: grid; grid-template-columns: minmax(0, 1fr) minmax(320px, 420px); align-items: start; gap: clamp(24px, 3vw, 48px); width: 100%; min-width: 0; }
[data-slot="agent-chat-main"] { min-width: 0; width: 100%; }
[data-slot="agent-chat-aside"] { min-width: 0; width: 100%; position: sticky; top: 24px; }
[data-slot="agent-chat-aside"] [data-slot="agent-chat"] { max-width: none; }
[data-slot="agent-chat"] {
display: flex;
flex-direction: column;
width: 100%;
max-width: 520px;
height: min(760px, 85dvh);
min-height: 480px;
min-width: 0;
overflow: hidden;
isolation: isolate;
border: 1px solid var(--v-border);
border-radius: var(--r-card);
background: var(--v-canvas);
color: var(--v-text);
font: var(--fw-regular) var(--fs-control)/1.5 var(--font-text);
}
.v-agent-chat__header { display: grid; grid-template-columns: minmax(0, 1fr) auto; flex-shrink: 0; column-gap: var(--s-3); padding: var(--s-5) var(--s-5) var(--s-3); border-bottom: 1px solid var(--v-border); }
.v-agent-chat__heading { min-width: 0; }
.v-agent-chat__heading h2 { margin: 0; font: var(--fw-semibold) var(--fs-title)/1.2 var(--font-display); letter-spacing: -.025em; overflow-wrap: anywhere; }
.v-agent-chat__heading p { margin: var(--s-1) 0 0; color: var(--v-text-2); font-size: var(--fs-meta); }
.v-agent-chat__header-actions { align-self: start; display: flex; gap: var(--s-2); }
.v-agent-chat__header [data-slot="agent-state"] { grid-column: 1 / -1; margin: var(--s-1) 0 -4px -8px; }
.v-agent-chat > .v-message-scroll-root[data-slot] { flex: 1 1 0; min-height: 0; max-height: none; padding: 0; background: transparent; border-radius: 0; box-shadow: none; }
.v-agent-chat__thread[data-slot] { flex: 1 1 auto; min-height: 0; max-height: none; gap: var(--s-6); padding: var(--s-6) var(--s-5); border-radius: 0; background: transparent; box-shadow: none; overflow-x: hidden; overflow-y: auto; scroll-behavior: auto; scrollbar-gutter: stable; overscroll-behavior: contain; }
.v-agent-chat__thread:focus-visible { outline: 2px solid var(--ring); outline-offset: -3px; }
.v-agent-chat__jump { z-index: 2; box-shadow: var(--shadow-float); }
.v-agent-chat__message { width: 100%; gap: var(--s-2); align-items: flex-start; }
.v-agent-chat__avatar[data-slot] { width: 28px; height: 28px; font-size: var(--fs-caps); margin-top: 2px; }
.v-agent-chat__message-content { min-width: 0; flex: 1; max-width: 100%; width: 100%; gap: var(--s-2); }
.v-agent-chat__author[data-slot] { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--s-2); margin: 0; font-size: var(--fs-meta); color: var(--v-text); font-weight: var(--fw-semibold); }
.v-agent-chat__author span { color: var(--v-text-2); font-size: var(--fs-caps); font-weight: var(--fw-regular); }
.v-agent-chat__message-body[data-slot] { width: 100%; min-width: 0; max-width: 65ch; overflow-wrap: anywhere; }
.v-agent-chat__message[data-from="agent"] .v-agent-chat__message-body { background: var(--v-beige); }
.v-agent-chat__message-body > :first-child { margin-top: 0; }
.v-agent-chat__message-body > :last-child { margin-bottom: 0; }
.v-agent-chat__message-body p { margin: 0 0 var(--s-3); white-space: pre-wrap; }
.v-agent-chat__message-body pre { max-width: 100%; overflow: auto; padding: var(--s-3); border-radius: var(--r-sm); background: var(--v-beige); font: var(--fs-meta)/1.5 var(--font-mono); }
.v-agent-chat__message-body img, .v-agent-chat__message-body video { max-width: 100%; height: auto; }
.v-agent-chat__message[data-from="user"] .v-agent-chat__message-content { flex: 0 1 auto; width: fit-content; max-width: calc(100% - 36px); }
.v-agent-chat__message-actions { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.v-agent-chat__composer { flex: 0 0 auto; min-width: 0; padding: var(--s-3); border-top: 1px solid var(--v-border); background: var(--v-canvas); }
.v-agent-chat__draft[data-slot] { display: flex; flex-direction: column; align-items: stretch; gap: 0; padding: var(--s-2); }
.v-agent-chat__draft:focus-within { outline: 2px solid var(--ring); outline-offset: 2px; }
.v-agent-chat__input[data-slot] { min-height: 84px; max-height: 200px; resize: vertical; width: 100%; flex-basis: auto; padding: var(--s-2); background: transparent; box-shadow: none; font: var(--fw-regular) var(--fs-control)/1.5 var(--font-text); }
.v-agent-chat__input[data-slot]:focus-visible { outline: none; box-shadow: none; }
.v-agent-chat__draft .v-agent-chat__composer-bar[data-slot] { display: flex; align-items: center; width: 100%; max-width: 100%; min-width: 0; gap: var(--s-2); padding-top: var(--s-1); }
.v-agent-chat__attach-button[data-slot] { width: 40px; padding: 0; flex-shrink: 0; }
.v-agent-chat__composer-note { flex: 1 1 auto; min-width: 0; color: var(--v-text-2); font-size: var(--fs-caps); line-height: 1.35; }
.v-agent-chat__send[data-slot] { flex-shrink: 0; padding-inline: var(--s-4); margin-left: auto; }
.v-agent-chat__hint { display: flex; align-items: center; justify-content: center; gap: var(--s-1); margin: var(--s-2) var(--s-1) 0; font-size: var(--fs-caps); line-height: 1.4; color: var(--v-text-2); text-align: center; overflow-wrap: anywhere; }
.v-agent-chat__hint svg { flex-shrink: 0; }
.v-agent-chat__attachments { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s-2); max-height: 156px; overflow: auto; margin-bottom: var(--s-2); }
.v-agent-chat__attachment[data-slot] { min-width: 0; border-radius: var(--r-md); min-height: 48px; gap: var(--s-2); padding: var(--s-2) var(--s-2); }
.v-agent-chat__attachment [data-slot="attachment-type"] { width: 32px; height: 32px; }
.v-agent-chat__attachment [data-slot="attachment-name"] { font-size: var(--fs-meta); }
.v-agent-chat__attachment [data-slot="attachment-meta"] { font-size: var(--fs-caps); }
.v-agent-chat__error { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); padding: var(--s-3); margin-bottom: var(--s-2); border-radius: var(--r-md); background: var(--status-danger-bg); color: var(--status-danger-ink); font-size: var(--fs-meta); }
.v-agent-chat__error > span { flex: 1; min-width: 16ch; overflow-wrap: anywhere; }
.v-agent-chat__permission { display: grid; gap: var(--s-3); padding: var(--s-4); border-radius: var(--r-md); background: var(--surface-automation); }
.v-agent-chat__decision-title { display: flex; align-items: flex-start; gap: var(--s-2); }
.v-agent-chat__decision-title svg { flex-shrink: 0; margin-top: 1px; }
.v-agent-chat__decision-title h3 { margin: 0; font: var(--fw-semibold) var(--fs-body)/1.4 var(--font-text); }
.v-agent-chat__permission > p { margin: 0; color: var(--v-text-2); font-size: var(--fs-small); }
.v-agent-chat__scope { overflow-wrap: anywhere; padding-block: var(--s-3); border-block: 1px solid var(--v-border); font-size: var(--fs-meta); }
.v-agent-chat__decision-actions { display: flex; flex-wrap: wrap; gap: var(--s-2); justify-content: flex-end; }
.v-agent-chat__decision-actions > button { flex: 1 1 90px; }
.v-agent-chat__permission .v-agent-chat__receipt { display: flex; align-items: center; gap: var(--s-2); color: var(--v-text); font-weight: var(--fw-semibold); }
.v-agent-chat__permission[data-decision="denied"] { background: var(--surface-quiet); }
.v-agent-chat__permission[data-decision="allowed"] { background: var(--surface-memory); }
.v-agent-chat__options[data-slot] { gap: var(--s-3); }
.v-agent-chat__options legend { margin-bottom: var(--s-3); font-weight: var(--fw-semibold); font-size: var(--fs-control); }
.v-agent-chat__options [data-slot="questionnaire-option"] { grid-template-columns: minmax(0, 1fr) 22px; gap: var(--s-3); padding: var(--s-3); min-height: 56px; border-radius: var(--r-md); background: var(--v-beige-2); }
.v-agent-chat__options [data-slot="questionnaire-option-body"] { order: 1; }
.v-agent-chat__options [data-slot="questionnaire-option-body"] b { font-size: var(--fs-small); }
.v-agent-chat__options [data-slot="questionnaire-option-body"] small { font-size: var(--fs-meta); }
.v-agent-chat__options > button { justify-self: start; }
.v-agent-chat__progress { display: grid; gap: 0; list-style: none; padding: 0; margin: var(--s-4) 0; }
.v-agent-chat__progress li { position: relative; display: grid; grid-template-columns: 24px minmax(0, 1fr); gap: var(--s-3); padding-bottom: var(--s-5); color: var(--v-text-2); }
.v-agent-chat__progress li:last-child { padding-bottom: 0; }
.v-agent-chat__progress li:not(:last-child)::after { content: ""; position: absolute; left: 11px; top: 26px; bottom: 3px; width: 1px; background: var(--v-border); }
.v-agent-chat__step-mark { display: grid; place-items: center; width: 24px; height: 24px; font-size: var(--fs-caps); border-radius: 50%; background: var(--v-beige); border: 1px solid var(--v-border); }
.v-agent-chat__progress b { display: block; font-size: var(--fs-small); font-weight: var(--fw-medium); line-height: 24px; }
.v-agent-chat__progress small { display: block; font-size: var(--fs-meta); line-height: 1.45; }
.v-agent-chat__progress [data-state="current"] { color: var(--v-text); }
.v-agent-chat__progress [data-state="current"] .v-agent-chat__step-mark { border-color: var(--v-edge); background: var(--v-pink); color: var(--ink-fixed); }
.v-agent-chat__progress [data-state="complete"] .v-agent-chat__step-mark { background: var(--status-ok-bg); color: var(--status-ok-ink); border-color: transparent; }
.v-agent-chat__progress [data-state="error"] .v-agent-chat__step-mark { background: var(--status-danger-bg); color: var(--status-danger-ink); border-color: transparent; }
.v-agent-chat__sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
.v-agent-chat-demo { width: 100%; display: grid; grid-template-columns: minmax(0, 1fr); justify-items: center; gap: var(--s-3); min-width: 0; }
.v-agent-chat-demo__tools { display: flex; width: 100%; max-width: 520px; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
.v-agent-chat-demo__tools p { margin: 0; flex: 1; font-size: var(--fs-meta); color: var(--v-text-2); }
.v-agent-chat-demo__welcome { display: grid; gap: var(--s-5); padding-block: var(--s-2); }
.v-agent-chat-demo__welcome > p { margin: 0; max-width: 36ch; justify-self: center; text-align: center; color: var(--v-text-2); font-size: var(--fs-small); }
.v-agent-chat-demo__suggestions { display: grid; gap: var(--s-2); }
.v-agent-chat-demo__suggestions > button { width: 100%; height: auto; min-height: 44px; padding: var(--s-3) var(--s-4); white-space: normal; text-align: left; justify-content: space-between; font-size: var(--fs-small); }
.v-agent-state-demo { width: 100%; display: grid; gap: var(--s-6); justify-items: center; padding-block: var(--s-4); }
.v-agent-state-demo__controls { display: flex; flex-wrap: wrap; gap: var(--s-2); justify-content: center; }
.v-agent-workspace__intro { margin-bottom: var(--s-7); }
.v-agent-workspace__intro h1 { font: var(--fw-medium) clamp(32px, 3vw, 48px)/1.08 var(--font-display); letter-spacing: -.03em; margin: var(--s-3) 0; max-width: 16ch; }
.v-agent-workspace__intro p { max-width: 52ch; color: var(--v-text-2); font-size: var(--fs-body); line-height: 1.55; margin: 0; }
.v-agent-workspace__section-heading { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-4); margin: var(--s-7) 0 var(--s-3); }
.v-agent-workspace__section-heading h2 { font: var(--fw-semibold) var(--fs-lead)/1.3 var(--font-display); margin: 0; }
.v-agent-workspace__section-heading span { font-size: var(--fs-meta); color: var(--v-text-2); }
.v-agent-workspace__tasks { display: grid; gap: var(--s-2); }
.v-agent-workspace__task[data-slot] { display: flex; justify-content: space-between; gap: var(--s-4); min-height: 68px; height: auto; padding: var(--s-4); text-align: left; white-space: normal; border-radius: var(--r-md); }
.v-agent-workspace__task-copy { display: grid; gap: var(--s-1); min-width: 0; }
.v-agent-workspace__task b { display: block; font-size: var(--fs-control); font-weight: var(--fw-medium); }
.v-agent-workspace__task small { display: block; font-size: var(--fs-meta); line-height: 1.4; font-weight: var(--fw-regular); }
.v-agent-workspace__task-status { flex-shrink: 0; font-size: var(--fs-caps); }
.v-agent-workspace__artifact[data-slot] { padding: clamp(20px, 3vw, 32px); background: var(--surface-memory); border-radius: var(--r-card-sm); }
.v-agent-workspace__artifact h3 { font: var(--fw-semibold) var(--fs-title)/1.2 var(--font-display); letter-spacing: -.02em; margin: var(--s-3) 0; }
.v-agent-workspace__artifact p { font-size: var(--fs-control); color: var(--v-text-2); }
.v-agent-workspace__artifact ul { padding-left: var(--s-5); margin: var(--s-5) 0 var(--s-6); }
.v-agent-workspace__artifact li { margin: var(--s-3) 0; font-size: var(--fs-control); line-height: 1.55; }
.v-agent-workspace__artifact-actions { display: flex; align-items: center; flex-wrap: wrap; gap: var(--s-3); }
.v-agent-workspace__artifact-actions p { margin: 0; font-size: var(--fs-meta); }
@media (max-width: 900px) {
[data-slot="agent-chat-layout"] { grid-template-columns: minmax(0, 1fr); }
[data-slot="agent-chat-aside"] { position: static; }
[data-slot="agent-chat-aside"] .v-agent-chat-demo__tools { max-width: none; }
}
@media (max-width: 420px) {
[data-slot="agent-chat"] { min-height: 440px; height: min(700px, 88dvh); border-radius: var(--r-md); }
.v-agent-chat__header { padding: var(--s-4) var(--s-4) var(--s-2); }
.v-agent-chat__thread[data-slot] { padding: var(--s-5) var(--s-3); gap: var(--s-5); }
.v-agent-chat__composer { padding: var(--s-3); }
.v-agent-chat__composer-note { display: none; }
.v-agent-chat__input[data-slot] { font-size: var(--fs-reading); }
.v-agent-chat__hint { font-size: var(--fs-caps); }
.v-agent-chat__permission { padding: var(--s-3); }
}
@media (max-height: 560px) {
[data-slot="agent-chat"] { min-height: 360px; }
}
@media (prefers-reduced-motion: reduce) {
.v-agent-chat__thread[data-slot] { scroll-behavior: auto; }
}
}