npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@reopt-ai/opt-chat

v2.1.0

Published

AI chat interface — streaming, conversation state, model routing, and rich AI output components.

Readme

@reopt-ai/opt-chat

AI 채팅 인터페이스 — 스트리밍, 대화 상태, 모델 라우팅, 그리고 풍부한 AI 출력 컴포넌트 모음. Vercel AI Elements를 포팅한 후 디자인 시스템(CHAT_* 토큰), i18n 라벨, 의존성 경량화(motion / shadcn-ui / cva 제거)를 적용한 패키지.

에이전트 스킬로 설정하기 (권장)

소비자 프로젝트 루트에서 전용 스킬을 설치합니다.

npx skills add reopt-ai/reopt-skills/opt-chat-install

설치 후 에이전트에게 opt-chat-install 스킬로 이 프로젝트에 @reopt-ai/opt-chat을 설정하고 검증해줘라고 요청하세요. 스킬은 신규 설치와 업그레이드를 구분하고, AGENTS.md(없으면 CLAUDE.md)의 reopt marker 블록, 스타일, AI SDK endpoint, Conversation scaffold를 설정한 뒤 검증합니다. 스킬 소스는 reopt-ai/reopt-skills가 단일 기준입니다.

아래는 수동 설치 대안입니다.

Install

bun add @reopt-ai/opt-chat
# 또는
pnpm add @reopt-ai/opt-chat

패키지는 npmjs.org에 공개 배포되므로 registry override나 인증 토큰이 필요하지 않습니다.

Optional peers

optional peer가 필요한 컴포넌트는 전용 엔트리에서만 내보냅니다. 그 엔트리를 쓸 때만 peer를 설치하세요:

| Entry | Components | Peer | | -------------------------------- | ------------------- | ------------------ | | @reopt-ai/opt-chat/jsx-preview | JSXPreview 외 | react-jsx-parser | | @reopt-ai/opt-chat/flow | Canvas, Node 외 | @xyflow/react | | @reopt-ai/opt-chat/eve | EveMessages 외 | eve (타입만) |

루트 @reopt-ai/opt-chat은 어느 peer에도 닿지 않으므로, 이 엔트리들을 쓰지 않는 앱은 peer 없이 webpack·Turbopack·Vite에서 빌드됩니다. 동적 import()로는 충분하지 않습니다. webpack과 Turbopack은 동적 import도 빌드 때 resolve합니다.

import { JSXPreview } from "@reopt-ai/opt-chat/jsx-preview";

Markdown 플러그인 지연 로딩

MessageResponse와 ReasoningContent가 쓰는 Streamdown 플러그인 중 무거운 세 가지(@streamdown/code = shiki, @streamdown/math = katex, @streamdown/mermaid = mermaid)는 초기 번들에 포함되지 않고, 해당 문법(코드 펜스 · $ 수식 · mermaid 펜스)이 처음 등장할 때 동적으로 로드되어 프로세스 전역에 캐시됩니다. 가벼운 @streamdown/cjk만 정적으로 포함됩니다.

첫 코드 블록이 잠시 하이라이트 없이 보이는 것을 피하려면 채팅 화면 마운트 시점에 미리 로드하세요:

import { preloadStreamdownPlugins } from "@reopt-ai/opt-chat";

useEffect(() => {
  void preloadStreamdownPlugins(); // 또는 ["code"] 처럼 일부만
}, []);

직접 plugins prop을 넘기면 지연 로딩 대신 그 설정이 그대로 쓰입니다.

Styles

CodeBlock을 쓰면 opt-ui 사용 여부와 무관하게 import가 필요합니다 (opt-ui 글로벌 CSS에는 Shiki 듀얼 테마 .shiki 룰이 없음). 그 외 애니메이션 정의는 opt-ui 글로벌 CSS에도 동일하게 있습니다:

import "@reopt-ai/opt-chat/styles.css";

이 파일은 opt-shimmer keyframe과 [data-opt-chat-message], [data-opt-chat-collapse] 등 데이터 속성 애니메이션, 그리고 CodeBlock의 .shiki 듀얼 테마 룰(--shiki-light/--shiki-dark 전환)을 정의.

Quick start

"use client";
import {
  Conversation,
  ConversationContent,
  Message,
  MessageContent,
  PromptInput,
  PromptInputActionAddAttachment,
  PromptInputAttachments,
  PromptInputTextarea,
  PromptInputFooter,
  PromptInputSubmit,
} from "@reopt-ai/opt-chat";

export function Chat() {
  return (
    <>
      <Conversation>
        <ConversationContent>
          <Message from="user">
            <MessageContent>안녕하세요</MessageContent>
          </Message>
          <Message from="assistant">
            <MessageContent>도움이 필요하신가요?</MessageContent>
          </Message>
        </ConversationContent>
      </Conversation>
      <PromptInput onSubmit={(text) => console.log(text)}>
        <PromptInputAttachments />
        <PromptInputTextarea />
        <PromptInputFooter>
          <PromptInputActionAddAttachment />
          <PromptInputSubmit />
        </PromptInputFooter>
      </PromptInput>
    </>
  );
}

onSubmit의 두 번째 인자에는 현재 첨부 상태와 AI SDK FileUIPart[]가 전달됩니다. 로컬 blob: URL은 제출 전에 data: URL로 변환되므로 useChatSession().handleSubmit에 그대로 연결할 수 있습니다.

onSubmit이 Promise를 반환하면 완료 전 중복 전송을 막습니다. 실패 시 입력과 첨부를 보존하고, 성공 시 제출한 내용만 정리합니다. 대기 중 새로 작성한 텍스트나 추가한 파일·참조 소스는 다음 메시지를 위해 유지됩니다. PromptInputProvider로 상태를 공유하는 구성도 동일하게 동작합니다.

Component catalog

Core (@reopt-ai/opt-chat)

  • Conversation / ConversationContent / ConversationScrollButton / messagesToMarkdown
  • Message / MessageContent / MessageActions / MessageBranch …
  • PromptInput / PromptInputTextarea / PromptInputModelSelector / PromptInputAttachments / PromptInputActionAddAttachment / PromptInputActionAddScreenshot
  • Shimmer, Suggestions, MessageParts, useChatSession
  • Helpers: captureScreenshot, convertBlobUrlToDataUrl, resolveToolApprovalId

Parts (28+ message renderers)

Reasoning · Tool · Artifact · CodeBlock · Agent · Sources · SourceDocumentItem · InlineCitation · Plan · ChainOfThought · Confirmation · Checkpoint · Terminal · Context · FileTree · StackTrace · TestResults · ChatImage · Persona · ConnectionStatus · Snippet · ChatToolbar · Transcript · Transcription · EnvironmentVariables · SchemaDisplay · Queue · Task · Commit · PackageInfo · JSXPreview (./jsx-preview) · Sandbox · WebPreview · OpenIn

ReasoningTrigger의 상태 문구는 role="status"/aria-live="polite"로 스트리밍 시작·완료를 안내하며, 스트리밍 중에는 aria-busy가 설정됩니다. 커스텀 getThinkingMessage를 사용할 때도 상태 자체는 컴포넌트가 유지합니다.

Input (@reopt-ai/opt-chat)

  • Attachments, SpeechInput, ModelSelector, AudioPlayer, VoiceSelector, MicSelector (+ useAudioDevices)

Flow (@reopt-ai/opt-chat/flow)

xyflow 기반 에이전트 그래프. @xyflow/react optional peer 필요.

  • Canvas / Connection / Edge.{Animated,Temporary} / Controls / Panel
  • Node 컴파운드: NodeHeader / NodeTitle / NodeDescription / NodeAction / NodeContent / NodeFooter

eve (@reopt-ai/opt-chat/eve)

eve 에이전트 세션(useEveAgent())을 그대로 렌더합니다. eve의 메시지는 AI SDK UIMessage가 아니라 eve 고유의 EveMessage이므로 변환 없이 넘깁니다. 이 엔트리는 eve의 타입만 참조하고 런타임 코드는 불러오지 않습니다.

  • EveMessages — 메시지 목록. 턴이 아직 보여 줄 것이 없을 때 "작업 중" 행을 띄우고, 사람이 턴 중간에 보낸 메시지로 나뉜 턴은 data-turn-id로 묶입니다.
  • EveMessageParts — 텍스트·추론·첨부·호출·상호작용 파트. renderPart로 파트를, renderToolOutput으로 알려진 도구의 결과 카드를 바꿀 수 있습니다.
  • EveToolCall — 도구·하위 에이전트·스킬 호출과 8가지 호출 상태. 하위 에이전트 호출은 agents를 넘기면 따라간 자식 대화를 펼칩니다(maxAgentDepth, 기본 2). 작업 제어 호출은 showControlCalls 없이는 숨깁니다.
  • EveInteraction — 승인·질문(선택지/자유 입력)·로그인(기기 코드)·예산 확인. 답은 respond로 보내고, eve가 확정할 때까지 다시 답할 수 있습니다(거절된 답 재시도).
  • EveTurnError — 세션·스트림·턴 실패와 eve의 해결 방법(hint)·문의 ID.
"use client";

import { useEveAgent } from "eve/react";
import { PromptInput } from "@reopt-ai/opt-chat";
import { EveMessages, EveTurnError } from "@reopt-ai/opt-chat/eve";

export function AgentChat() {
  const agent = useEveAgent({ followAgents: ({ depth }) => depth <= 2 });
  return (
    <>
      <EveMessages
        messages={agent.messages}
        status={agent.status}
        agents={agent.agents}
        onRespond={agent.respond}
      />
      <EveTurnError error={agent.error} />
      {/* PromptInput onSubmit → agent.send(text) */}
    </>
  );
}

Design system integration

  • 모든 컴포넌트는 lib/styles.ts의 CHAT_* 토큰만 사용 (raw color hardcode 금지).
  • opt-ui 글로벌 CSS와 자체 dist/styles.css가 동일한 keyframe / data-attribute 애니메이션 정의를 가져 호환.
  • 의존성: motion, class-variance-authority, @radix-ui/react-use-controllable-state, shadcn-ui — 사용하지 않음 (의도적 경량화).
  • 워크스페이스 의존성: @reopt-ai/opt-ui-primitives — OpenIn / MicSelector의 popover 위치 / outside click / Escape를 floating-ui 기반으로 처리. 단일 Collapsible primitive (lib/primitives/)는 자체 구현 유지.

Visual sizing contract

  • 텍스트 액션은 CHAT_BUTTON_PRIMARY / CHAT_BUTTON_SECONDARY / CHAT_BUTTON_GHOST를 사용합니다. 세 강조 단계는 같은 최소 높이 40px, rounded-lg, 14px 글자, 20px 줄높이, 좌우 14px 패딩을 공유합니다.
  • 아이콘 전용 액션은 CHAT_ICON_BUTTON(32px), 선택기는 CHAT_SELECT_TRIGGER(최소 36px)를 사용합니다. 아이콘 버튼에 텍스트 버튼의 최소 크기를 겹쳐 적용하지 않습니다. 컴포저의 첨부 액션은 36px, 전송·중단 액션은 40px 원형으로 구분합니다.
  • 데이터·실행 카드는 CHAT_SURFACE의 옅은 배경과 16px 안쪽 정렬을 기본으로 사용합니다. 입력란·팝오버 경계와 작업 내역 연결선은 역할에 따라 유지합니다.
  • 본문은 14px/24px, Markdown 문단 간격은 8px입니다. 컴포넌트 자체의 바깥 여백을 누적하지 말고 부모의 gap으로 파트 사이 간격을 정합니다.
  • 좁은 화면에서 긴 이름·경로는 줄바꿈 또는 내부 스크롤로 처리합니다. className으로 제품별 조정은 가능하지만 한 버튼만 높이·모서리를 바꾸지 않습니다.

Migration 1.0 → 1.1

PromptInput 루트가 <form>으로 전환

루트 엘리먼트가 <div>에서 네이티브 <form>으로 바뀌었고 PromptInputSubmit은 type="submit"입니다. 기본 제출은 내부에서 항상 preventDefault 되므로 단독 사용에는 변화가 없지만, PromptInput을 다른 <form> 안에 중첩할 수 없습니다 (중첩 <form>은 유효하지 않은 HTML). 바깥 form이 있었다면 걷어내고 PromptInput의 onSubmit에서 처리하세요.

// ❌ 1.1부터 지원하지 않음 — 중첩 form
<form onSubmit={handleOuter}>
  <PromptInput onSubmit={handleInner} />
</form>

// ✅ 바깥 form 제거, PromptInput의 onSubmit만 사용
<PromptInput onSubmit={handleInner} />

ConversationProps가 자체 props로 교체

Conversation이 use-stick-to-bottom 의존성을 제거하고 자체 스크롤 엔진으로 바뀌면서 initial / resize prop이 사라졌습니다.

// Before — StickToBottom props
<Conversation initial="smooth" resize="smooth">

// After — 자체 엔진 props
<Conversation autoScroll scrollPreviousItemPeek={64}>

autoScroll / scrollEdgeThreshold / scrollPreviousItemPeek / scrollMargin으로 대체하고, 턴 앵커링이 필요하면 각 메시지를 ConversationMessage로 감싸 scrollAnchor를 지정합니다.

Migration 0.1 → 0.2

MessageBranchContent (deprecated children API)

branches: ReactNode[] prop을 우선 사용하세요. children API는 호환 shim으로 유지되지만 dev 환경에서 경고가 출력됩니다.

// ✅ 권장
<MessageBranchContent branches={[<A key="a" />, <B key="b" />]} />

// ⚠️ deprecated — 작동은 하나 console.warn
<MessageBranchContent>
  <A />
  <B />
</MessageBranchContent>

MessageBranch.totalBranches 옵셔널화

이제 자동 감지됩니다. 명시 prop이 우선.

ToolOutput 회귀 복원

객체 / 문자열 출력이 <pre>가 아닌 <CodeBlock language="json">으로 다시 렌더링됩니다.

PromptInput 신규 export

  • PromptInputModelSelector — 모델 dropdown 슬롯
  • PromptInputAttachments — PromptInput 내부 첨부 목록 렌더 / 제거 슬롯
  • captureScreenshot(), convertBlobUrlToDataUrl(url), attachmentsToFileUIParts(attachments) — top-level helpers
  • approveToolCall(part | approvalId) / denyToolCall(part | approvalId) — AI SDK part.approval.id와 part 객체를 모두 허용

Development

bun run build       # tsdown ESM + CJS + DTS
bun run test        # vitest
bun run typecheck   # tsc --noEmit
bun run lint        # oxlint

Production readiness gate

릴리즈 직전에는 코드 검증과 소비자 앱 검증을 분리해서 확인합니다.

bun run --filter @reopt-ai/opt-chat lint
bun run --filter @reopt-ai/opt-chat typecheck
bun run --filter @reopt-ai/opt-chat test
bun run --filter @reopt-ai/opt-chat build
node scripts/validate-private-publish.mjs

추가로 fresh Next/Vite 앱에서 tarball 설치 후 @reopt-ai/opt-chat, @reopt-ai/opt-chat/flow, @reopt-ai/opt-chat/styles.css import와 production build를 확인합니다. 브라우저 스모크는 웹 문서의 /explore/opt-chat/release-soak fixture와 apps/web/e2e/opt-chat.spec.ts를 기준으로 keyboard submit, attachment-only submit, approval approve/deny, tool/source 렌더링, 느린 streaming/abort/retry 조건을 함께 확인합니다.

Compact operation history

Use ToolGroup when a single assistant turn contains several tool calls. The collapsed header retains approval/error counts; expanding reveals each operation with the application's status wording. It records tool history rather than inferring whether a background job has finished.

<ToolGroup
  items={[
    {
      id: "inspect",
      title: "장면 검사",
      state: "output-available",
      status: "결과 수신",
    },
    { id: "approve", title: "출력 승인", state: "approval-requested" },
  ]}
  labels={{
    title: "작업 기록",
    count: "{count}개",
    attention: "확인 필요 {count}",
  }}
/>

PromptInput disabled disables composer controls and guards submission without showing generation as active. For applications that accept steering messages while an agent works, use <PromptInputSubmit status="ready" />; it still disables itself while the current submission is in flight. Submission errors retain the draft, and successful submissions do not erase newer text typed while waiting.

Interactive playground

The web app's /tools/opt-chat-playground uses useChatSession with a local AI SDK transport. Quick scenarios cover execution/approval, questionnaires, attachments and draft retention. Advanced presets add background completion and history pagination. The composer exposes model/effort selection and a sample context preview; message actions support feedback and transcript restoration.

Use Fail next send to verify that a failed submission retains text and files, and type the next draft while a response streams. Execution scenarios expose success/failure controls after approval. The activity panel and exported message JSON reflect SDK parts; share links restore only the selected scenario and delay, not private conversation text or attached files. Integration TSX preserves custom data parts and provides an editable renderer and /api/chat integration point. The playground does not call a live model or execute a deployment.

Execution state and structured parts

MessageParts variant="compact" uses a shared Trace layout for tool execution. TraceItem has a summary, an optional detail region, and an adjacent actions slot for opening a consumer-owned sheet. The user's disclosure choice takes precedence over automatic expansion. Existing card layouts remain the default. Plan, Task, Reasoning, ChainOfThought, Queue, Sources, ToolGroup, Terminal, TestResults, and StackTrace also accept variant="compact".

getToolExecutionStatus distinguishes preliminary output from final output. Transport completion alone leaves unfinished client/background tools waiting; it never turns them into a failed tool. Pass interrupted only when execution was actually interrupted, or use executionStatus for a separate job lifecycle. ExecutionPlan/ExecutionTask accept stable-ID ExecutionStep[] snapshots; AgentRun renders a validated nested agent message, separate from Agent's configuration card. ExecutionToolGroup accepts SDK tool parts directly. Both tool groups and agent runs keep approval actions outside collapsed history.

For English rendering, pass enExecutionLabels, enToolLabels, enReasoningLabels, enSourcesLabels, and enConfirmationLabels into MessageParts or AgentRun through their matching executionLabels, toolLabels, reasoningLabels, sourcesLabels, and approvalLabels props. Compact Terminal, TestResults, StackTrace, ChainOfThought, and ToolGroup accept executionLabels alongside their own content labels. ReasoningTrigger accepts labels; thoughtFor uses a {duration} placeholder. The default Korean labels remain available when no overrides are passed.

import type { UIMessage } from "ai";
import {
  createPartRenderer,
  ExecutionPlan,
  MessageParts,
  Terminal,
  type ExecutionStep,
} from "@reopt-ai/opt-chat";

type AppMessage = UIMessage<
  unknown,
  {
    plan: { steps: ExecutionStep[] };
    terminal: { output: string; running: boolean };
  }
>;

const renderPart = createPartRenderer<AppMessage>({
  "data-plan": ({ data }) => <ExecutionPlan title="Plan" steps={data.steps} />,
  "data-terminal": ({ data }) => (
    <Terminal
      variant="compact"
      output={data.output}
      isStreaming={data.running}
    />
  ),
});

<MessageParts<AppMessage>
  parts={message.parts}
  renderPart={renderPart}
  variant="compact"
  isStreaming={session.isStreaming}
  onToolApproval={(id, approved) =>
    session.addToolApprovalResponse({ id, approved })
  }
/>;

Validate payloads with AI SDK data/tool schemas. Renderers are opt-in: arbitrary tool output is not guessed into a plan or agent message. Reuse a data-* part's id for snapshot updates; for terminal output use its toolCallId and look it up with findDataPart(parts, "data-terminal", toolCallId). Source URLs are deduped; a custom source renderer replaces the default entry. Keep source ID → citation number mapping stable in the application.

Questions, model controls, and submission

Questionnaire accepts single, multiple, or text questions with stable IDs, options and required. It retains answers while navigating, validates required answers, skips optional questions, and awaits onSubmit. Rejected submissions keep the answers and offer retry; pending/successful submissions cannot be sent again. Key the form by request ID when replacing a questionnaire. Use enQuestionnaireLabels or partial labels overrides for localization.

ModelDef.reasoningEfforts lists only provider-supported values. Connect ModelSelector's reasoningEffort and onReasoningEffortChange, then include the selected model and supported effort in the request:

<PromptInput
  onSubmit={(text, payload) =>
    session.handleSubmit(text, payload, {
      body: { model: selectedModel.id, reasoningEffort },
    })
  }
>
  {/* composer slots */}
</PromptInput>

The application maps those body fields to provider-specific options. Omit the effort for models without that capability. handleSubmit now returns a Promise: return/await it in wrappers so composer draft recovery sees transport errors. An intentional stop counts as a delivered submission. For calls outside a composer, handle rejection explicitly; session.error also exposes the error. SuggestionItem awaits its callback and disables itself while pending.

Attachments accept upload/processing/error states, optional progress, and keyboard/touch preview triggers. FileTree is an accessible disclosure list, not an ARIA tree; its native folder buttons use Tab/Enter/Space. ContextTrigger is keyboard focusable and token usage exposes progress semantics. CodeBlock supports wrap, startLine, and keyboard scrolling.

Conversation history

Conversation defaultScrollPosition="start" | "end" | "last-anchor" chooses the first history position (default "end"). Later appended user turns still use scrollAnchor. Prepending history preserves the reading position. useConversationMessageVisible(messageId) subscribes to a single message's viewport visibility without rerendering the entire conversation.

restoreMessagesAt(messages, messageId) restores only the transcript through the selected message. It does not undo external tool side effects; use a server checkpoint for real file/database restoration.

See /explore/opt-chat/execution-replay for deterministic AI SDK approval, preliminary/final/error replay, questionnaire submission, feedback toggles, and transcript checkpoints. The fixture makes no model/network calls.