{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "streaming-chat-shell",
  "title": "Streaming Chat Shell",
  "description": "A developer-facing chat workspace with a shared chat session and replayable SSE trace.",
  "dependencies": [
    "@ai-sdk/react",
    "@base-ui/react",
    "@phosphor-icons/react",
    "ai",
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "badge",
    "breadcrumb",
    "button",
    "collapsible",
    "textarea",
    "https://elements.ai-sdk.dev/api/registry/conversation.json",
    "https://elements.ai-sdk.dev/api/registry/message.json"
  ],
  "files": [
    {
      "path": "registry/streaming-chat-shell/app/demos/streaming-chat-shell/page.tsx",
      "content": "import { StreamingChatShellScreen } from \"@/components/streaming-chat-shell/streaming-chat-shell-screen\";\n\nexport default function StreamingChatShellPage() {\n  return <StreamingChatShellScreen />;\n}\n",
      "type": "registry:page",
      "target": "app/demos/streaming-chat-shell/page.tsx"
    },
    {
      "path": "registry/streaming-chat-shell/app/api/demos/streaming-chat-shell/route.ts",
      "content": "import { handleStreamingChatShellRequest } from \"@/lib/streaming-chat-shell/runtime\";\n\nexport const runtime = \"nodejs\";\n\nexport function POST(request: Request) {\n  return handleStreamingChatShellRequest(request);\n}\n",
      "type": "registry:page",
      "target": "app/api/demos/streaming-chat-shell/route.ts"
    },
    {
      "path": "registry/streaming-chat-shell/app/api/demos/streaming-chat-shell/events/route.ts",
      "content": "import { handleStreamingChatShellEventsRequest } from \"@/lib/streaming-chat-shell/events\";\n\nexport const runtime = \"nodejs\";\n\nexport function POST(request: Request) {\n  return handleStreamingChatShellEventsRequest(request);\n}\n",
      "type": "registry:page",
      "target": "app/api/demos/streaming-chat-shell/events/route.ts"
    },
    {
      "path": "registry/streaming-chat-shell/components/streaming-chat-shell/streaming-chat-shell-screen.tsx",
      "content": "import { Badge } from \"@/components/ui/badge\";\nimport {\n  Breadcrumb,\n  BreadcrumbItem,\n  BreadcrumbLink,\n  BreadcrumbList,\n  BreadcrumbPage,\n  BreadcrumbSeparator,\n} from \"@/components/ui/breadcrumb\";\nimport { ArrowLeft } from \"lucide-react\";\n\nimport { getStreamingChatShellRuntimeState } from \"@/lib/streaming-chat-shell/runtime\";\n\nimport { StreamingChatShellWorkspace } from \"./streaming-chat-shell-workspace\";\n\nexport function StreamingChatShellScreen() {\n  const runtimeState = getStreamingChatShellRuntimeState();\n\n  return (\n    <main className=\"min-h-svh bg-background text-foreground\">\n      <div className=\"mx-auto flex w-full max-w-7xl flex-col gap-6 px-4 py-6 md:px-6\">\n        <header className=\"grid gap-4 border border-foreground/10 bg-background px-4 py-5 md:grid-cols-[minmax(0,1fr)_auto] md:items-end\">\n          <div className=\"space-y-2\">\n            <Breadcrumb>\n              <BreadcrumbList className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n                <BreadcrumbItem>\n                  <BreadcrumbLink\n                    aria-label=\"Back to demos\"\n                    className=\"-ml-1 inline-flex items-center gap-1.5 text-muted-foreground transition-colors hover:text-foreground\"\n                    href=\"/\"\n                  >\n                    <ArrowLeft aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n                    <span>Demo</span>\n                  </BreadcrumbLink>\n                </BreadcrumbItem>\n                <BreadcrumbSeparator className=\"text-muted-foreground\">\n                  /\n                </BreadcrumbSeparator>\n                <BreadcrumbItem>\n                  <BreadcrumbPage className=\"font-normal text-muted-foreground\">\n                    Streaming Chat Shell\n                  </BreadcrumbPage>\n                </BreadcrumbItem>\n              </BreadcrumbList>\n            </Breadcrumb>\n            <h1 className=\"max-w-3xl font-medium text-2xl tracking-tight\">\n              A developer-facing chat runtime shell with replayable streaming\n              trace\n            </h1>\n            <p className=\"max-w-3xl text-muted-foreground text-sm/relaxed\">\n              This demo shows how one chat session can drive the user-facing\n              transcript and a developer-side replay stream at the same time.\n            </p>\n          </div>\n\n          <div className=\"flex flex-wrap items-center gap-2\">\n            <Badge variant=\"outline\">{runtimeState.statusLabel}</Badge>\n            <Badge variant=\"outline\">{runtimeState.chatModel}</Badge>\n          </div>\n        </header>\n\n        <div className=\"lg:h-svh\">\n          <StreamingChatShellWorkspace\n            chatModel={runtimeState.chatModel}\n            isChatAvailable={runtimeState.isChatAvailable}\n            nodeVersion={runtimeState.nodeVersion}\n            setupMessage={runtimeState.setupMessage}\n            supportedAudiences={runtimeState.supportedAudiences}\n          />\n        </div>\n      </div>\n    </main>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/streaming-chat-shell/streaming-chat-shell-screen.tsx"
    },
    {
      "path": "registry/streaming-chat-shell/components/streaming-chat-shell/streaming-chat-shell-workspace.tsx",
      "content": "\"use client\";\n\nimport { Chat, useChat } from \"@ai-sdk/react\";\nimport {\n  ArrowClockwiseIcon,\n  BroadcastIcon,\n  CaretDownIcon,\n  RobotIcon,\n  StopIcon,\n} from \"@phosphor-icons/react\";\nimport {\n  Conversation,\n  ConversationContent,\n  ConversationEmptyState,\n  ConversationScrollButton,\n} from \"@/components/ai-elements/conversation\";\nimport {\n  Message,\n  MessageContent,\n  MessageResponse,\n} from \"@/components/ai-elements/message\";\nimport {\n  PromptInput,\n  PromptInputBody,\n  PromptInputFooter,\n  PromptInputSubmit,\n  PromptInputTextarea,\n} from \"@/components/ai-elements/prompt-input\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\nimport { cn } from \"@/lib/utils\";\nimport { DefaultChatTransport, type UIMessage } from \"ai\";\nimport { useMemo, useState } from \"react\";\n\nimport type { StreamingAudience } from \"@/lib/streaming-chat-shell/contract\";\nimport {\n  getReplayPromptEntries,\n  useStreamingReplayPrompt,\n  type ReplayPromptEntry,\n} from \"./streaming-replay\";\n\nfunction getTextContent(message: UIMessage) {\n  return message.parts\n    .filter((part) => part.type === \"text\")\n    .map((part) => part.text)\n    .join(\"\\n\");\n}\n\nfunction getAudienceLabel(audience: StreamingAudience) {\n  switch (audience) {\n    case \"buyers\":\n      return \"Buyers\";\n    case \"support\":\n      return \"Support\";\n    case \"engineers\":\n    default:\n      return \"Engineers\";\n  }\n}\n\nconst streamingSamplePrompts = [\n  \"Explain why replayable streaming traces help debug a production chat incident.\",\n  \"Draft a buyer-facing summary of this streaming shell in three bullets.\",\n  \"Show how support teams can use replay traces to inspect a failed answer.\",\n] as const;\n\ninterface StreamingChatShellWorkspaceProps {\n  chatModel: string;\n  isChatAvailable: boolean;\n  nodeVersion: string;\n  setupMessage: string | null;\n  supportedAudiences: string[];\n}\n\ninterface StreamingTranscriptProps {\n  chat: Chat<UIMessage>;\n  isChatAvailable: boolean;\n  setupMessage: string | null;\n}\n\ninterface StreamingComposerProps {\n  audience: StreamingAudience;\n  chat: Chat<UIMessage>;\n  chatModel: string;\n  isChatAvailable: boolean;\n  onAudienceChange: (audience: StreamingAudience) => void;\n  supportedAudiences: StreamingAudience[];\n}\n\ninterface StreamingDeveloperTraceProps {\n  audience: StreamingAudience;\n  chat: Chat<UIMessage>;\n}\n\ninterface ReplayPromptCardProps {\n  audience: StreamingAudience;\n  defaultOpen?: boolean;\n  entry: ReplayPromptEntry;\n  title: string;\n}\n\nfunction StreamingTranscript({\n  chat,\n  isChatAvailable,\n  setupMessage,\n}: StreamingTranscriptProps) {\n  const { error, messages } = useChat({ chat });\n  const hasMessages = messages.length > 0;\n\n  return (\n    <>\n      {isChatAvailable ? null : (\n        <div className=\"border-foreground/10 border-b px-4 py-3 text-muted-foreground text-xs/relaxed\">\n          {setupMessage}\n        </div>\n      )}\n\n      {error ? (\n        <div className=\"border-foreground/10 border-b px-4 py-3 text-destructive text-xs/relaxed\">\n          {error.message}\n        </div>\n      ) : null}\n\n      <Conversation className=\"min-h-0\">\n        <ConversationContent className=\"mx-auto flex w-full max-w-3xl flex-1 gap-6 px-4 py-6\">\n          {hasMessages ? (\n            messages.map((message) => {\n              const text = getTextContent(message);\n\n              return (\n                <Message from={message.role} key={message.id}>\n                  <MessageContent\n                    className={cn(\n                      message.role === \"assistant\" ? \"max-w-3xl\" : \"max-w-2xl\"\n                    )}\n                  >\n                    {text ? (\n                      <MessageResponse>{text}</MessageResponse>\n                    ) : (\n                      <p className=\"text-muted-foreground text-sm\">\n                        Waiting for visible output.\n                      </p>\n                    )}\n                  </MessageContent>\n                </Message>\n              );\n            })\n          ) : (\n            <ConversationEmptyState\n              description=\"Send a prompt, then inspect how the same chat history can be replayed as a developer-side SSE trace.\"\n              icon={<RobotIcon className=\"size-5\" />}\n              title=\"Developer replay shell is ready\"\n            />\n          )}\n        </ConversationContent>\n        <ConversationScrollButton />\n      </Conversation>\n    </>\n  );\n}\n\nfunction StreamingComposer({\n  audience,\n  chat,\n  chatModel,\n  isChatAvailable,\n  onAudienceChange,\n  supportedAudiences,\n}: StreamingComposerProps) {\n  const { messages, regenerate, sendMessage, status, stop } = useChat({ chat });\n  const hasMessages = messages.length > 0;\n  const isBusy = status === \"submitted\" || status === \"streaming\";\n\n  async function handleSend(text: string) {\n    const trimmedText = text.trim();\n\n    if (!trimmedText) {\n      return;\n    }\n\n    await sendMessage(\n      {\n        text: trimmedText,\n      },\n      {\n        body: { audience },\n      }\n    );\n  }\n\n  return (\n    <div className=\"border-foreground/10 border-t px-4 py-4\">\n      <div className=\"mx-auto w-full max-w-3xl space-y-3\">\n        <div className=\"flex flex-wrap items-center gap-2\">\n          {supportedAudiences.map((option) => {\n            const isActive = option === audience;\n\n            return (\n              <Button\n                className=\"h-8 px-3 text-xs\"\n                key={option}\n                onClick={() => onAudienceChange(option)}\n                size=\"sm\"\n                type=\"button\"\n                variant={isActive ? \"default\" : \"outline\"}\n              >\n                {getAudienceLabel(option)}\n              </Button>\n            );\n          })}\n        </div>\n\n        <PromptInput onSubmit={({ text }) => handleSend(text)}>\n          <PromptInputBody>\n            <PromptInputTextarea\n              disabled={!isChatAvailable || isBusy}\n              placeholder=\"Send a prompt, then inspect the replay trace on the right.\"\n            />\n          </PromptInputBody>\n          <PromptInputFooter className=\"flex items-center justify-between gap-3 border-foreground/10 border-t px-3 py-3\">\n            <div className=\"flex flex-wrap items-center gap-2\">\n              <Badge variant=\"outline\">Custom body</Badge>\n              <Badge variant=\"outline\">Shared useChat</Badge>\n              <Badge variant=\"outline\">{chatModel}</Badge>\n            </div>\n            <div className=\"flex items-center gap-2\">\n              {isBusy ? (\n                <Button\n                  onClick={stop}\n                  size=\"sm\"\n                  type=\"button\"\n                  variant=\"outline\"\n                >\n                  <StopIcon className=\"size-3.5\" />\n                  Stop\n                </Button>\n              ) : null}\n              {hasMessages ? (\n                <Button\n                  onClick={() => regenerate({ body: { audience } })}\n                  size=\"sm\"\n                  type=\"button\"\n                  variant=\"outline\"\n                >\n                  <ArrowClockwiseIcon className=\"size-3.5\" />\n                  Retry\n                </Button>\n              ) : null}\n              <PromptInputSubmit disabled={!isChatAvailable} status={status} />\n            </div>\n          </PromptInputFooter>\n        </PromptInput>\n\n        {hasMessages ? null : (\n          <div className=\"flex flex-wrap gap-2\">\n            {streamingSamplePrompts.map((prompt) => (\n              <Button\n                className=\"max-w-full justify-start text-left\"\n                disabled={!isChatAvailable || isBusy}\n                key={prompt}\n                onClick={() => void handleSend(prompt)}\n                size=\"sm\"\n                type=\"button\"\n                variant=\"outline\"\n              >\n                {prompt}\n              </Button>\n            ))}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n\nfunction ReplayPromptCard({\n  audience,\n  defaultOpen = false,\n  entry,\n  title,\n}: ReplayPromptCardProps) {\n  const { error, events, isReplayable, status, traceText, replayPrompt } =\n    useStreamingReplayPrompt(audience, entry);\n\n  return (\n    <Collapsible className=\"border border-foreground/10\" defaultOpen={defaultOpen}>\n      <CollapsibleTrigger className=\"flex w-full items-center justify-between gap-4 px-3 py-3 text-left\">\n        <div className=\"space-y-1\">\n          <p className=\"font-medium text-sm\">{title}</p>\n          <p className=\"text-muted-foreground text-xs/relaxed line-clamp-3\">\n            {entry.promptText}\n          </p>\n        </div>\n        <CaretDownIcon className=\"size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180\" />\n      </CollapsibleTrigger>\n\n      <CollapsibleContent className=\"space-y-4 border-foreground/10 border-t px-3 py-3\">\n        <div className=\"flex flex-wrap items-center gap-2\">\n          <Badge variant=\"outline\">{getAudienceLabel(audience)}</Badge>\n          <Badge variant=\"outline\">{entry.replayMessages.length} messages</Badge>\n          {events.length > 0 ? (\n            <Badge variant=\"outline\">{events.length} events</Badge>\n          ) : null}\n        </div>\n\n        <div className=\"space-y-2\">\n          <Button\n            disabled={!isReplayable || status === \"loading\"}\n            onClick={replayPrompt}\n            size=\"sm\"\n            type=\"button\"\n            variant=\"outline\"\n          >\n            <BroadcastIcon className=\"size-3.5\" />\n            Replay prompt turn\n          </Button>\n          <p className=\"text-muted-foreground text-xs/relaxed\">\n            Correlated user prompt: \"{entry.promptText}\"\n          </p>\n        </div>\n\n        {error ? (\n          <div className=\"rounded-md border border-destructive/20 bg-destructive/10 px-3 py-2 text-destructive text-xs/relaxed\">\n            {error}\n          </div>\n        ) : null}\n\n        {status === \"loading\" ? (\n          <p className=\"text-muted-foreground text-xs/relaxed\">\n            Reading the custom event stream...\n          </p>\n        ) : null}\n\n        {traceText ? (\n          <div className=\"space-y-2\">\n            <p className=\"font-medium text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n              Reconstructed text\n            </p>\n            <div className=\"rounded-md bg-muted/50 px-3 py-3 text-sm/relaxed\">\n              {traceText}\n            </div>\n          </div>\n        ) : null}\n\n        {events.length > 0 ? (\n          <Collapsible className=\"border border-foreground/10\" defaultOpen={false}>\n            <CollapsibleTrigger className=\"flex w-full items-center justify-between gap-4 px-3 py-2 text-left\">\n              <div className=\"space-y-1\">\n                <p className=\"font-medium text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n                  Parsed events\n                </p>\n                <p className=\"text-muted-foreground text-xs/relaxed\">\n                  Inspect the raw replay events only when you need the full\n                  sequence.\n                </p>\n              </div>\n              <CaretDownIcon className=\"size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180\" />\n            </CollapsibleTrigger>\n\n            <CollapsibleContent className=\"space-y-2 border-foreground/10 border-t px-3 py-3\">\n              {events.map((event, index) => (\n                <div\n                  className=\"rounded-md border border-foreground/10 px-3 py-2 text-xs/relaxed\"\n                  key={`${event.type}-${index}`}\n                >\n                  <div className=\"mb-1 flex items-center justify-between gap-3\">\n                    <span className=\"font-medium\">{event.type}</span>\n                    {event.type === \"start\" ? (\n                      <Badge variant=\"outline\">\n                        {getAudienceLabel(event.audience)}\n                      </Badge>\n                    ) : null}\n                  </div>\n                  <pre className=\"overflow-x-auto whitespace-pre-wrap break-words text-muted-foreground\">\n                    {JSON.stringify(event, null, 2)}\n                  </pre>\n                </div>\n              ))}\n            </CollapsibleContent>\n          </Collapsible>\n        ) : null}\n      </CollapsibleContent>\n    </Collapsible>\n  );\n}\n\nfunction StreamingDeveloperTrace({\n  audience,\n  chat,\n}: StreamingDeveloperTraceProps) {\n  const { messages } = useChat({ chat });\n  const replayEntries = useMemo(() => getReplayPromptEntries(messages), [messages]);\n  const latestEntry = replayEntries[0] ?? null;\n  const historyEntries = replayEntries.slice(1);\n  const hasMessages = messages.length > 0;\n\n  return (\n    <Collapsible className=\"border border-foreground/10\" defaultOpen={false}>\n      <CollapsibleTrigger className=\"flex w-full items-center justify-between gap-4 px-4 py-3 text-left\">\n        <div className=\"space-y-1\">\n          <p className=\"font-medium text-sm\">Replayable developer trace</p>\n          <p className=\"text-muted-foreground text-xs/relaxed\">\n            Replay assistant turns from the thread state that existed at each\n            user prompt.\n          </p>\n        </div>\n        <CaretDownIcon className=\"size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180\" />\n      </CollapsibleTrigger>\n\n      <CollapsibleContent className=\"space-y-4 border-foreground/10 border-t px-4 py-4\">\n        <div className=\"flex flex-wrap items-center gap-2\">\n          <Badge variant=\"outline\">{getAudienceLabel(audience)}</Badge>\n          <Badge variant=\"outline\">{messages.length} thread messages</Badge>\n          <Badge variant=\"outline\">{replayEntries.length} user prompts</Badge>\n        </div>\n\n        {!hasMessages ? (\n          <p className=\"text-muted-foreground text-xs/relaxed\">\n            Send a prompt to generate replayable prompt turns.\n          </p>\n        ) : null}\n\n        {latestEntry ? (\n          <div className=\"space-y-2\">\n            <p className=\"font-medium text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n              Correlated user prompt\n            </p>\n            <ReplayPromptCard\n              audience={audience}\n              defaultOpen\n              entry={latestEntry}\n              title=\"Correlated user prompt\"\n            />\n          </div>\n        ) : null}\n\n        {historyEntries.length > 0 ? (\n          <div className=\"space-y-2\">\n            <p className=\"font-medium text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n              History prompt\n            </p>\n            <div className=\"space-y-3\">\n              {historyEntries.map((entry) => (\n                <ReplayPromptCard\n                  audience={audience}\n                  entry={entry}\n                  key={entry.id}\n                  title=\"History prompt\"\n                />\n              ))}\n            </div>\n          </div>\n        ) : null}\n      </CollapsibleContent>\n    </Collapsible>\n  );\n}\n\nexport function StreamingChatShellWorkspace({\n  chatModel,\n  isChatAvailable,\n  nodeVersion,\n  setupMessage,\n  supportedAudiences,\n}: StreamingChatShellWorkspaceProps) {\n  const normalizedAudiences = supportedAudiences as StreamingAudience[];\n  const [audience, setAudience] = useState<StreamingAudience>(\n    normalizedAudiences[0] ?? \"engineers\"\n  );\n  const [chat] = useState(\n    () =>\n      new Chat({\n        transport: new DefaultChatTransport({\n          api: \"/api/demos/streaming-chat-shell\",\n        }),\n      })\n  );\n\n  return (\n    <div className=\"grid min-h-[70svh] gap-4 lg:h-full lg:min-h-0 lg:grid-cols-[minmax(0,1fr)_20rem]\">\n      <section className=\"flex min-h-[70svh] flex-col border border-foreground/10 bg-background lg:h-full lg:min-h-0\">\n        <StreamingTranscript\n          chat={chat}\n          isChatAvailable={isChatAvailable}\n          setupMessage={setupMessage}\n        />\n        <StreamingComposer\n          audience={audience}\n          chat={chat}\n          chatModel={chatModel}\n          isChatAvailable={isChatAvailable}\n          onAudienceChange={setAudience}\n          supportedAudiences={normalizedAudiences}\n        />\n      </section>\n\n      <aside className=\"space-y-4 lg:min-h-0 lg:overflow-y-auto\">\n        <section className=\"border border-foreground/10 bg-background p-4\">\n          <div className=\"space-y-4\">\n            <div>\n              <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n                Runtime\n              </p>\n              <p className=\"mt-1 font-medium text-sm\">{nodeVersion}</p>\n            </div>\n            <div>\n              <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n                Request contract\n              </p>\n              <p className=\"mt-1 text-sm\">\n                Each request can carry feature-local metadata, while the main\n                chat surface stays on the shared AI SDK transport contract.\n              </p>\n            </div>\n            <div>\n              <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n                Shared state\n              </p>\n              <p className=\"mt-1 text-sm\">\n                Transcript, composer, retry flow, and replay trace all read from\n                the same `Chat` instance.\n              </p>\n            </div>\n            <div>\n              <p className=\"text-[11px] text-muted-foreground uppercase tracking-[0.2em]\">\n                Developer value\n              </p>\n              <p className=\"mt-1 text-sm\">\n                The same conversation can be replayed as a plain SSE stream for\n                debugging, inspection, and secondary developer tooling.\n              </p>\n            </div>\n          </div>\n        </section>\n\n        <StreamingDeveloperTrace audience={audience} chat={chat} />\n      </aside>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/streaming-chat-shell/streaming-chat-shell-workspace.tsx"
    },
    {
      "path": "registry/streaming-chat-shell/components/streaming-chat-shell/streaming-replay.ts",
      "content": "\"use client\";\n\nimport type { UIMessage } from \"ai\";\nimport { useMemo, useState } from \"react\";\n\nimport type { StreamingAudience } from \"@/lib/streaming-chat-shell/contract\";\nimport type { StreamingChatShellEvent } from \"@/lib/streaming-chat-shell/streaming-turn\";\n\nconst eventDelimiter = \"\\n\\n\";\nconst dataPrefix = \"data:\";\n\nexport interface ReplayPromptEntry {\n  id: string;\n  promptText: string;\n  replayMessages: UIMessage[];\n}\n\nfunction getTextContent(message: UIMessage) {\n  return message.parts\n    .filter((part) => part.type === \"text\")\n    .map((part) => part.text)\n    .join(\"\\n\")\n    .trim();\n}\n\nfunction parseEventChunk(chunk: string) {\n  const dataLines = chunk\n    .split(\"\\n\")\n    .map((line) => line.trim())\n    .filter((line) => line.startsWith(dataPrefix))\n    .map((line) => line.slice(dataPrefix.length).trim());\n\n  if (dataLines.length === 0) {\n    return null;\n  }\n\n  return JSON.parse(dataLines.join(\"\\n\")) as StreamingChatShellEvent;\n}\n\nexport function getReplayPromptEntries(messages: UIMessage[]) {\n  const entries: ReplayPromptEntry[] = [];\n\n  messages.forEach((message, index) => {\n    if (message.role !== \"user\") {\n      return;\n    }\n\n    const promptText = getTextContent(message);\n\n    if (!promptText) {\n      return;\n    }\n\n    entries.push({\n      id: message.id,\n      promptText,\n      replayMessages: messages.slice(0, index + 1),\n    });\n  });\n\n  return entries.reverse();\n}\n\nexport function getReplayTraceText(events: StreamingChatShellEvent[]) {\n  return events\n    .filter(\n      (event): event is Extract<StreamingChatShellEvent, { type: \"text\" }> =>\n        event.type === \"text\"\n    )\n    .map((event) => event.text)\n    .join(\"\");\n}\n\nexport function createStreamingReplayEventParser() {\n  let buffer = \"\";\n\n  return {\n    push(chunk: string) {\n      buffer += chunk;\n\n      const events: StreamingChatShellEvent[] = [];\n\n      while (true) {\n        const delimiterIndex = buffer.indexOf(eventDelimiter);\n\n        if (delimiterIndex === -1) {\n          return events;\n        }\n\n        const rawChunk = buffer.slice(0, delimiterIndex);\n        buffer = buffer.slice(delimiterIndex + eventDelimiter.length);\n\n        const event = parseEventChunk(rawChunk);\n\n        if (event) {\n          events.push(event);\n        }\n      }\n    },\n  };\n}\n\nasync function replayStreamingPromptTurn(\n  audience: StreamingAudience,\n  messages: UIMessage[]\n) {\n  const response = await fetch(\"/api/demos/streaming-chat-shell/events\", {\n    method: \"POST\",\n    headers: {\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      audience,\n      messages,\n    }),\n  });\n\n  if (!response.ok) {\n    let message = \"Failed to replay the custom stream.\";\n\n    try {\n      const payload = (await response.json()) as { error?: string };\n\n      if (payload.error) {\n        message = payload.error;\n      }\n    } catch {\n      const fallbackText = await response.text();\n\n      if (fallbackText) {\n        message = fallbackText;\n      }\n    }\n\n    throw new Error(message);\n  }\n\n  if (!response.body) {\n    throw new Error(\"The custom event stream returned an empty body.\");\n  }\n\n  const reader = response.body.getReader();\n  const decoder = new TextDecoder();\n  const parser = createStreamingReplayEventParser();\n  const replayEvents: StreamingChatShellEvent[] = [];\n\n  while (true) {\n    const { done, value } = await reader.read();\n\n    if (done) {\n      break;\n    }\n\n    const nextEvents = parser.push(decoder.decode(value, { stream: true }));\n\n    if (nextEvents.length > 0) {\n      replayEvents.push(...nextEvents);\n    }\n  }\n\n  const trailingEvents = parser.push(decoder.decode());\n\n  if (trailingEvents.length > 0) {\n    replayEvents.push(...trailingEvents);\n  }\n\n  return replayEvents;\n}\n\nexport function useStreamingReplayPrompt(\n  audience: StreamingAudience,\n  entry: ReplayPromptEntry\n) {\n  const [events, setEvents] = useState<StreamingChatShellEvent[]>([]);\n  const [error, setError] = useState<string | null>(null);\n  const [status, setStatus] = useState<\"idle\" | \"loading\" | \"ready\" | \"error\">(\n    \"idle\"\n  );\n  const traceText = useMemo(() => getReplayTraceText(events), [events]);\n  const isReplayable = entry.replayMessages.length > 0;\n\n  async function replayPrompt() {\n    if (!isReplayable) {\n      return;\n    }\n\n    setEvents([]);\n    setError(null);\n    setStatus(\"loading\");\n\n    try {\n      const replayEvents = await replayStreamingPromptTurn(\n        audience,\n        entry.replayMessages\n      );\n      setEvents(replayEvents);\n      setStatus(\"ready\");\n    } catch (replayError) {\n      setError(\n        replayError instanceof Error\n          ? replayError.message\n          : \"Failed to replay the custom stream.\"\n      );\n      setStatus(\"error\");\n    }\n  }\n\n  return {\n    error,\n    events,\n    isReplayable,\n    status,\n    traceText,\n    replayPrompt,\n  };\n}\n",
      "type": "registry:component",
      "target": "@components/streaming-chat-shell/streaming-replay.ts"
    },
    {
      "path": "registry/streaming-chat-shell/components/ai-elements/prompt-input.tsx",
      "content": "\"use client\";\n\nimport { CornerDownLeftIcon, LoaderCircleIcon, SquareIcon } from \"lucide-react\";\nimport type {\n  ComponentProps,\n  FormEvent,\n  HTMLAttributes,\n  ReactNode,\n  TextareaHTMLAttributes,\n} from \"react\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useMemo,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\n\ntype PromptStatus = \"error\" | \"ready\" | \"streaming\" | \"submitted\";\n\ninterface PromptInputContextValue {\n  setText: (text: string) => void;\n  text: string;\n}\n\ninterface PromptInputMessage {\n  text: string;\n}\n\ninterface PromptInputProps\n  extends Omit<HTMLAttributes<HTMLFormElement>, \"onSubmit\"> {\n  children: ReactNode;\n  onSubmit: (\n    message: PromptInputMessage,\n    event: FormEvent<HTMLFormElement>\n  ) => void | Promise<void>;\n}\n\ninterface PromptInputSubmitProps\n  extends Omit<ComponentProps<typeof Button>, \"children\" | \"type\"> {\n  onStop?: () => void;\n  status?: PromptStatus;\n}\n\ntype PromptInputClickEvent = Parameters<\n  Exclude<ComponentProps<typeof Button>[\"onClick\"], undefined>\n>[0];\n\nconst PromptInputContext = createContext<PromptInputContextValue | null>(null);\n\nfunction usePromptInputContext() {\n  const context = useContext(PromptInputContext);\n\n  if (!context) {\n    throw new Error(\n      \"PromptInput components must be used inside <PromptInput>.\"\n    );\n  }\n\n  return context;\n}\n\nexport function PromptInput({\n  children,\n  className,\n  onSubmit,\n  ...props\n}: PromptInputProps) {\n  const [text, setText] = useState(\"\");\n\n  const contextValue = useMemo(\n    () => ({\n      setText,\n      text,\n    }),\n    [text]\n  );\n\n  const handleSubmit = useCallback(\n    async (event: FormEvent<HTMLFormElement>) => {\n      event.preventDefault();\n\n      const nextText = text.trim();\n      if (!nextText) {\n        return;\n      }\n\n      const result = onSubmit({ text: nextText }, event);\n\n      try {\n        await result;\n        setText(\"\");\n      } catch {\n        // Keep the text for retry after a failed submit.\n      }\n    },\n    [onSubmit, text]\n  );\n\n  return (\n    <PromptInputContext.Provider value={contextValue}>\n      <form\n        className={cn(\"w-full\", className)}\n        onSubmit={handleSubmit}\n        {...props}\n      >\n        {children}\n      </form>\n    </PromptInputContext.Provider>\n  );\n}\n\nexport function PromptInputBody({\n  className,\n  ...props\n}: HTMLAttributes<HTMLDivElement>) {\n  return <div className={cn(\"grid gap-3\", className)} {...props} />;\n}\n\nexport function PromptInputFooter({\n  className,\n  ...props\n}: HTMLAttributes<HTMLDivElement>) {\n  return <div className={cn(className)} {...props} />;\n}\n\nexport function PromptInputTextarea({\n  className,\n  disabled,\n  onChange,\n  ...props\n}: TextareaHTMLAttributes<HTMLTextAreaElement>) {\n  const context = usePromptInputContext();\n\n  return (\n    <Textarea\n      className={cn(\"min-h-16 resize-none\", className)}\n      disabled={disabled}\n      onChange={(event) => {\n        context.setText(event.currentTarget.value);\n        onChange?.(event);\n      }}\n      value={context.text}\n      {...props}\n    />\n  );\n}\n\nexport function PromptInputSubmit({\n  className,\n  disabled,\n  onClick,\n  onStop,\n  size = \"icon\",\n  status = \"ready\",\n  variant = \"default\",\n  ...props\n}: PromptInputSubmitProps) {\n  const { text } = usePromptInputContext();\n  const isBusy = status === \"submitted\" || status === \"streaming\";\n  const isDisabled = Boolean(disabled) || (!isBusy && text.trim().length === 0);\n\n  let icon = <CornerDownLeftIcon className=\"size-4\" />;\n\n  if (status === \"submitted\") {\n    icon = <LoaderCircleIcon className=\"size-4 animate-spin\" />;\n  } else if (status === \"streaming\") {\n    icon = <SquareIcon className=\"size-4\" />;\n  }\n\n  return (\n    <Button\n      aria-label={isBusy ? \"Stop\" : \"Submit\"}\n      className={cn(className)}\n      disabled={isDisabled}\n      onClick={(event: PromptInputClickEvent) => {\n        if (isBusy && onStop) {\n          event.preventDefault();\n          onStop();\n          return;\n        }\n\n        onClick?.(event);\n      }}\n      size={size}\n      type={isBusy && onStop ? \"button\" : \"submit\"}\n      variant={variant}\n      {...props}\n    >\n      {icon}\n    </Button>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/ai-elements/prompt-input.tsx"
    },
    {
      "path": "registry/streaming-chat-shell/lib/streaming-chat-shell/contract.ts",
      "content": "import { validateUIMessages, type UIMessage } from \"ai\";\n\nexport const invalidMessagesError =\n  'Expected a JSON body with a \"messages\" array.';\nexport const invalidUiMessagesError =\n  'Expected each \"messages\" entry to match the UIMessage format.';\nexport const malformedJsonError = \"Expected a valid JSON request body.\";\nexport const supportedAudiences = [\"engineers\", \"buyers\", \"support\"] as const;\n\nexport type StreamingAudience = (typeof supportedAudiences)[number];\n\ninterface StreamingChatShellRequestBody {\n  audience?: string;\n  messages?: UIMessage[];\n}\n\nexport async function readStreamingChatShellRequest(body: unknown): Promise<{\n  audience: StreamingAudience;\n  messages: UIMessage[];\n}> {\n  const { audience, messages } = (body ?? {}) as StreamingChatShellRequestBody;\n\n  if (!Array.isArray(messages)) {\n    throw new Error(invalidMessagesError);\n  }\n\n  try {\n    return {\n      audience:\n        supportedAudiences.find((candidate) => candidate === audience) ??\n        \"engineers\",\n      messages: await validateUIMessages({ messages }),\n    };\n  } catch {\n    throw new Error(invalidUiMessagesError);\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/streaming-chat-shell/contract.ts"
    },
    {
      "path": "registry/streaming-chat-shell/lib/ai-gateway/contract.ts",
      "content": "import { createGateway } from \"ai\";\n\nexport const DEFAULT_GATEWAY_BASE_URL = \"https://ai-gateway.vercel.sh/v3/ai\";\nexport const MINIMUM_NODE_VERSION = \"22.13.0\";\nconst nodeVersionPattern = /^v?(\\d+)\\.(\\d+)\\.(\\d+)(?:[-+].*)?$/;\n\nexport type AiGatewayEnvRecord = Record<string, string | undefined>;\n\nexport interface ParsedNodeVersion {\n  major: number;\n  minor: number;\n  patch: number;\n}\n\nexport interface AiGatewayContractConfig {\n  apiKey: string;\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewayResolvedEnv {\n  apiKey: string | undefined;\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewaySetupConfig {\n  baseURL: string;\n  chatModel: string;\n}\n\nexport interface AiGatewayContractSetupState<\n  TConfig extends AiGatewaySetupConfig = AiGatewaySetupConfig,\n> {\n  config: TConfig;\n  isReady: boolean;\n  issues: string[];\n  nodeVersion: string;\n}\n\nexport interface AiGatewayContractOptions<\n  TConfig extends AiGatewaySetupConfig = AiGatewaySetupConfig,\n> {\n  buildConfig?: (\n    resolvedEnv: AiGatewayResolvedEnv,\n    env: AiGatewayEnvRecord\n  ) => TConfig;\n  defaultBaseURL?: string;\n  defaultChatModel: string;\n  getAdditionalIssues?: (\n    resolvedEnv: AiGatewayResolvedEnv,\n    env: AiGatewayEnvRecord\n  ) => string[];\n  missingApiKeyError: string;\n  missingApiKeyIssue?: string;\n}\n\nconst genericMissingApiKeyIssue =\n  \"AI_GATEWAY_API_KEY is missing. The demo can render, but chat requests will fail until it is configured.\";\n\nexport function parseNodeVersion(version: string): ParsedNodeVersion {\n  const match = nodeVersionPattern.exec(version);\n  const major = Number(match?.[1]);\n  const minor = Number(match?.[2]);\n  const patch = Number(match?.[3]);\n\n  if (![major, minor, patch].every(Number.isInteger)) {\n    throw new Error(`Unable to parse Node.js version: \"${version}\".`);\n  }\n\n  return { major, minor, patch };\n}\n\nexport function getNodeMajor(version: string): number {\n  return parseNodeVersion(version).major;\n}\n\nfunction compareNodeVersions(\n  left: ParsedNodeVersion,\n  right: ParsedNodeVersion\n): number {\n  if (left.major !== right.major) {\n    return left.major - right.major;\n  }\n\n  if (left.minor !== right.minor) {\n    return left.minor - right.minor;\n  }\n\n  return left.patch - right.patch;\n}\n\nexport function assertSupportedNodeRuntime(version = process.version): number {\n  const parsedVersion = parseNodeVersion(version);\n  const minimumVersion = parseNodeVersion(MINIMUM_NODE_VERSION);\n\n  if (compareNodeVersions(parsedVersion, minimumVersion) < 0) {\n    throw new Error(\n      `Node.js ${version} is unsupported. This demo workspace requires Node.js >=${MINIMUM_NODE_VERSION}.`\n    );\n  }\n\n  return parsedVersion.major;\n}\n\nexport function resolveAiGatewayContractEnv(\n  env: AiGatewayEnvRecord,\n  options: Pick<AiGatewayContractOptions, \"defaultBaseURL\" | \"defaultChatModel\">\n): AiGatewayResolvedEnv {\n  return {\n    apiKey: env.AI_GATEWAY_API_KEY,\n    baseURL:\n      env.AI_GATEWAY_BASE_URL ||\n      options.defaultBaseURL ||\n      DEFAULT_GATEWAY_BASE_URL,\n    chatModel: env.AI_GATEWAY_CHAT_MODEL || options.defaultChatModel,\n  };\n}\n\nexport function readAiGatewayContractConfig(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions\n): AiGatewayContractConfig {\n  assertSupportedNodeRuntime();\n  const resolvedEnv = resolveAiGatewayContractEnv(env, options);\n\n  if (!resolvedEnv.apiKey) {\n    throw new Error(options.missingApiKeyError);\n  }\n\n  return {\n    apiKey: resolvedEnv.apiKey,\n    baseURL: resolvedEnv.baseURL,\n    chatModel: resolvedEnv.chatModel,\n  };\n}\n\nexport function buildAiGatewayContractSetupState<\n  TConfig extends AiGatewaySetupConfig,\n>(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions<TConfig>\n): AiGatewayContractSetupState<TConfig> {\n  const issues: string[] = [];\n  const resolvedEnv = resolveAiGatewayContractEnv(env, options);\n\n  try {\n    assertSupportedNodeRuntime();\n  } catch (error) {\n    issues.push(\n      error instanceof Error ? error.message : \"Unsupported Node.js runtime.\"\n    );\n  }\n\n  if (!resolvedEnv.apiKey) {\n    issues.push(options.missingApiKeyIssue || genericMissingApiKeyIssue);\n  }\n\n  issues.push(...(options.getAdditionalIssues?.(resolvedEnv, env) ?? []));\n\n  return {\n    config:\n      options.buildConfig?.(resolvedEnv, env) ??\n      ({\n        baseURL: resolvedEnv.baseURL,\n        chatModel: resolvedEnv.chatModel,\n      } as TConfig),\n    isReady: issues.length === 0,\n    issues,\n    nodeVersion: process.version,\n  };\n}\n\nexport function createAiGatewayFromContract(\n  env: AiGatewayEnvRecord,\n  options: AiGatewayContractOptions\n): ReturnType<typeof createGateway> {\n  const { apiKey, baseURL } = readAiGatewayContractConfig(env, options);\n\n  return createGateway({\n    apiKey,\n    baseURL,\n  });\n}\n",
      "type": "registry:lib",
      "target": "@lib/ai-gateway/contract.ts"
    },
    {
      "path": "registry/streaming-chat-shell/lib/streaming-chat-shell/env-source.ts",
      "content": "export function getStreamingChatShellAppEnv() {\n  // biome-ignore lint/style/noProcessEnv: Registry source installs into consumer apps without this repo's env wrapper.\n  return process.env;\n}\n",
      "type": "registry:lib",
      "target": "@lib/streaming-chat-shell/env-source.ts"
    },
    {
      "path": "registry/streaming-chat-shell/lib/streaming-chat-shell/env.ts",
      "content": "import { getStreamingChatShellAppEnv } from \"./env-source\";\nimport {\n  buildAiGatewayContractSetupState,\n  createAiGatewayFromContract,\n  readAiGatewayContractConfig,\n  type AiGatewayContractConfig,\n  type AiGatewayContractSetupState,\n  type AiGatewayEnvRecord,\n  type AiGatewaySetupConfig,\n} from \"@/lib/ai-gateway/contract\";\n\nexport const DEFAULT_CHAT_MODEL = \"openai/gpt-4.1-mini\";\n\nexport type StreamingChatShellEnv = AiGatewayEnvRecord;\n\nexport type StreamingChatShellConfig = AiGatewayContractConfig;\n\nexport type StreamingChatShellSetupState = AiGatewayContractSetupState<AiGatewaySetupConfig>;\n\nexport type StreamingChatShellGateway = ReturnType<typeof createAiGatewayFromContract>;\n\nconst streamingChatShellContract = {\n  defaultChatModel: DEFAULT_CHAT_MODEL,\n  missingApiKeyError: \"Missing AI_GATEWAY_API_KEY. Add it to .env.local before using the streaming chat shell.\",\n  missingApiKeyIssue: \"AI_GATEWAY_API_KEY is missing. The demo can render, but chat requests will fail until it is configured.\",\n} as const;\n\nexport function getStreamingChatShellEnv(): StreamingChatShellEnv {\n  return getStreamingChatShellAppEnv();\n}\n\nexport function getStreamingChatShellConfig(\n  env: StreamingChatShellEnv = getStreamingChatShellEnv()\n): StreamingChatShellConfig {\n  return readAiGatewayContractConfig(env, streamingChatShellContract);\n}\n\nexport function getStreamingChatShellSetupState(\n  env: StreamingChatShellEnv = getStreamingChatShellEnv()\n): StreamingChatShellSetupState {\n  return buildAiGatewayContractSetupState(env, streamingChatShellContract);\n}\n\nexport function createStreamingChatShellGateway(\n  env: StreamingChatShellEnv = getStreamingChatShellEnv()\n): StreamingChatShellGateway {\n  return createAiGatewayFromContract(env, streamingChatShellContract);\n}\n",
      "type": "registry:lib",
      "target": "@lib/streaming-chat-shell/env.ts"
    },
    {
      "path": "registry/streaming-chat-shell/lib/streaming-chat-shell/events.ts",
      "content": "import type { UIMessage } from \"ai\";\n\nimport {\n  invalidMessagesError,\n  invalidUiMessagesError,\n  malformedJsonError,\n  readStreamingChatShellRequest,\n  type StreamingAudience,\n} from \"./contract\";\nimport {\n  getStreamingChatShellEnv,\n  getStreamingChatShellSetupState,\n  type StreamingChatShellEnv,\n} from \"./env\";\nimport {\n  createStreamingTurnEventsResponse,\n  type StreamingChatShellEvent,\n  streamStreamingTurnEvents,\n} from \"./streaming-turn\";\n\ninterface StreamingChatShellEventsDependencies {\n  streamStreamingChatShellEvents: (\n    messages: UIMessage[],\n    env: StreamingChatShellEnv,\n    options: { audience: StreamingAudience }\n  ) => AsyncIterable<StreamingChatShellEvent>;\n}\n\nexport async function* streamStreamingChatShellEvents(\n  messages: UIMessage[],\n  env: StreamingChatShellEnv,\n  options: { audience: StreamingAudience }\n): AsyncIterable<StreamingChatShellEvent> {\n  yield* streamStreamingTurnEvents(messages, env, options.audience);\n}\n\nexport async function handleStreamingChatShellEventsRequest(\n  request: Request,\n  env: StreamingChatShellEnv = getStreamingChatShellEnv(),\n  dependencies: StreamingChatShellEventsDependencies = {\n    streamStreamingChatShellEvents,\n  }\n) {\n  const runtimeState = getStreamingChatShellSetupState(env);\n\n  if (!runtimeState.isReady) {\n    return Response.json(\n      {\n        error:\n          runtimeState.issues.length > 0\n            ? runtimeState.issues.join(\" \")\n            : \"AI Gateway setup is required.\",\n      },\n      { status: 500 }\n    );\n  }\n\n  try {\n    const { audience, messages } = await readStreamingChatShellRequest(\n      await request.json()\n    );\n    if (\n      dependencies.streamStreamingChatShellEvents ===\n      streamStreamingChatShellEvents\n    ) {\n      return createStreamingTurnEventsResponse(messages, env, audience);\n    }\n\n    const encoder = new TextEncoder();\n    const eventStream = await dependencies.streamStreamingChatShellEvents(\n      messages,\n      env,\n      {\n        audience,\n      }\n    );\n\n    return new Response(\n      new ReadableStream({\n        async start(controller) {\n          for await (const event of eventStream) {\n            controller.enqueue(\n              encoder.encode(`data: ${JSON.stringify(event)}\\n\\n`)\n            );\n          }\n\n          controller.close();\n        },\n      }),\n      {\n        headers: {\n          \"cache-control\": \"no-cache\",\n          \"content-type\": \"text/event-stream; charset=utf-8\",\n        },\n      }\n    );\n  } catch (error) {\n    if (error instanceof SyntaxError) {\n      return Response.json(\n        {\n          error: malformedJsonError,\n        },\n        { status: 400 }\n      );\n    }\n\n    if (\n      error instanceof Error &&\n      [invalidMessagesError, invalidUiMessagesError].includes(error.message)\n    ) {\n      return Response.json(\n        {\n          error: error.message,\n        },\n        { status: 400 }\n      );\n    }\n\n    throw error;\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/streaming-chat-shell/events.ts"
    },
    {
      "path": "registry/streaming-chat-shell/lib/streaming-chat-shell/runtime.ts",
      "content": "import type { UIMessage } from \"ai\";\n\nimport {\n  invalidMessagesError,\n  invalidUiMessagesError,\n  malformedJsonError,\n  readStreamingChatShellRequest,\n  type StreamingAudience,\n  supportedAudiences,\n} from \"./contract\";\nimport {\n  getStreamingChatShellEnv,\n  getStreamingChatShellSetupState,\n  type StreamingChatShellEnv,\n} from \"./env\";\nimport { createStreamingTurnUiMessageResponse } from \"./streaming-turn\";\n\nexport interface StreamingChatShellRuntimeState {\n  chatModel: string;\n  isChatAvailable: boolean;\n  nodeVersion: string;\n  setupMessage: string | null;\n  statusLabel: \"Ready\" | \"Setup required\";\n  supportedAudiences: string[];\n}\n\nexport interface StreamingChatShellRequestOptions {\n  audience: StreamingAudience;\n}\n\ninterface StreamingChatShellRequestDependencies {\n  streamStreamingChatShell: (\n    messages: UIMessage[],\n    env: StreamingChatShellEnv,\n    options: StreamingChatShellRequestOptions\n  ) => Promise<Response>;\n}\n\nexport function getStreamingChatShellRuntimeState(\n  env: StreamingChatShellEnv = getStreamingChatShellEnv()\n): StreamingChatShellRuntimeState {\n  const setup = getStreamingChatShellSetupState(env);\n\n  return {\n    chatModel: setup.config.chatModel,\n    isChatAvailable: setup.isReady,\n    nodeVersion: setup.nodeVersion,\n    setupMessage: setup.issues.length > 0 ? setup.issues.join(\" \") : null,\n    statusLabel: setup.isReady ? \"Ready\" : \"Setup required\",\n    supportedAudiences: [...supportedAudiences],\n  };\n}\n\nexport function streamStreamingChatShell(\n  messages: UIMessage[],\n  env: StreamingChatShellEnv,\n  options: StreamingChatShellRequestOptions\n) {\n  return createStreamingTurnUiMessageResponse(messages, env, options.audience);\n}\n\nexport async function handleStreamingChatShellRequest(\n  request: Request,\n  env: StreamingChatShellEnv = getStreamingChatShellEnv(),\n  dependencies: StreamingChatShellRequestDependencies = {\n    streamStreamingChatShell,\n  }\n) {\n  const runtimeState = getStreamingChatShellRuntimeState(env);\n\n  if (!runtimeState.isChatAvailable) {\n    return Response.json(\n      {\n        error: runtimeState.setupMessage,\n      },\n      { status: 500 }\n    );\n  }\n\n  try {\n    const { audience, messages } = await readStreamingChatShellRequest(\n      await request.json()\n    );\n\n    return dependencies.streamStreamingChatShell(messages, env, {\n      audience,\n    });\n  } catch (error) {\n    if (error instanceof SyntaxError) {\n      return Response.json(\n        {\n          error: malformedJsonError,\n        },\n        { status: 400 }\n      );\n    }\n\n    if (\n      error instanceof Error &&\n      [invalidMessagesError, invalidUiMessagesError].includes(error.message)\n    ) {\n      return Response.json(\n        {\n          error: error.message,\n        },\n        { status: 400 }\n      );\n    }\n\n    throw error;\n  }\n}\n",
      "type": "registry:lib",
      "target": "@lib/streaming-chat-shell/runtime.ts"
    },
    {
      "path": "registry/streaming-chat-shell/lib/streaming-chat-shell/streaming-turn.ts",
      "content": "import {\n  convertToModelMessages,\n  streamText,\n  type UIMessage,\n} from \"ai\";\n\nimport {\n  createStreamingChatShellGateway,\n  getStreamingChatShellConfig,\n  type StreamingChatShellEnv,\n} from \"./env\";\n\nimport type { StreamingAudience } from \"./contract\";\n\nexport type StreamingChatShellEvent =\n  | { type: \"start\"; audience: StreamingAudience }\n  | { type: \"text\"; text: string }\n  | { type: \"finish\"; finishReason: string }\n  | { type: \"error\"; message: string };\n\nfunction buildAudienceSystemPrompt(audience: StreamingAudience) {\n  switch (audience) {\n    case \"buyers\":\n      return \"You are a concise product explainer for technical buyers. Keep the answer business-legible and concrete.\";\n    case \"support\":\n      return \"You are a support-oriented assistant. Focus on action steps, constraints, and what to try next.\";\n    case \"engineers\":\n    default:\n      return \"You are a concise engineering assistant. Keep answers direct, implementation-aware, and explicit about assumptions.\";\n  }\n}\n\nasync function createStreamingTurnResult(\n  messages: UIMessage[],\n  env: StreamingChatShellEnv,\n  audience: StreamingAudience\n) {\n  const gateway = createStreamingChatShellGateway(env);\n  const { chatModel } = getStreamingChatShellConfig(env);\n\n  return streamText({\n    model: gateway(chatModel),\n    system: buildAudienceSystemPrompt(audience),\n    messages: await convertToModelMessages(messages),\n  });\n}\n\nfunction formatStreamingTurnEvent(event: StreamingChatShellEvent) {\n  return `data: ${JSON.stringify(event)}\\n\\n`;\n}\n\nexport async function createStreamingTurnUiMessageResponse(\n  messages: UIMessage[],\n  env: StreamingChatShellEnv,\n  audience: StreamingAudience\n) {\n  const result = await createStreamingTurnResult(messages, env, audience);\n\n  return result.toUIMessageStreamResponse();\n}\n\nexport async function* streamStreamingTurnEvents(\n  messages: UIMessage[],\n  env: StreamingChatShellEnv,\n  audience: StreamingAudience\n): AsyncIterable<StreamingChatShellEvent> {\n  const result = await createStreamingTurnResult(messages, env, audience);\n\n  yield { type: \"start\", audience };\n\n  for await (const part of result.fullStream) {\n    if (part.type === \"text-delta\") {\n      yield { type: \"text\", text: part.text };\n      continue;\n    }\n\n    if (part.type === \"finish\") {\n      yield { type: \"finish\", finishReason: part.finishReason };\n      return;\n    }\n\n    if (part.type === \"error\") {\n      yield {\n        type: \"error\",\n        message:\n          part.error instanceof Error\n            ? part.error.message\n            : \"Custom stream failed.\",\n      };\n      return;\n    }\n  }\n}\n\nexport async function createStreamingTurnEventsResponse(\n  messages: UIMessage[],\n  env: StreamingChatShellEnv,\n  audience: StreamingAudience\n) {\n  const encoder = new TextEncoder();\n  const eventStream = streamStreamingTurnEvents(messages, env, audience);\n\n  return new Response(\n    new ReadableStream({\n      async start(controller) {\n        for await (const event of eventStream) {\n          controller.enqueue(encoder.encode(formatStreamingTurnEvent(event)));\n        }\n\n        controller.close();\n      },\n    }),\n    {\n      headers: {\n        \"cache-control\": \"no-cache\",\n        \"content-type\": \"text/event-stream; charset=utf-8\",\n      },\n    }\n  );\n}\n",
      "type": "registry:lib",
      "target": "@lib/streaming-chat-shell/streaming-turn.ts"
    }
  ],
  "envVars": {
    "AI_GATEWAY_API_KEY": "",
    "AI_GATEWAY_BASE_URL": "https://ai-gateway.vercel.sh/v3/ai",
    "AI_GATEWAY_CHAT_MODEL": "openai/gpt-4.1-mini"
  },
  "docs": "Install into a Next.js App Router project initialized with shadcn/ui. This item adds the demo page, chat route, replayable SSE events route, feature-local chat workspace, and AI Gateway env vars required for shared useChat and developer replay traces.",
  "type": "registry:block"
}
