@reopt-ai/opt-chat
v2.1.0
Published
AI chat interface — streaming, conversation state, model routing, and rich AI output components.
Maintainers
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/messagesToMarkdownMessage/MessageContent/MessageActions/MessageBranch…PromptInput/PromptInputTextarea/PromptInputModelSelector/PromptInputAttachments/PromptInputActionAddAttachment/PromptInputActionAddScreenshotShimmer,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/PanelNode컴파운드: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 기반으로 처리. 단일Collapsibleprimitive (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 helpersapproveToolCall(part | approvalId)/denyToolCall(part | approvalId)— AI SDKpart.approval.id와 part 객체를 모두 허용
Development
bun run build # tsdown ESM + CJS + DTS
bun run test # vitest
bun run typecheck # tsc --noEmit
bun run lint # oxlintProduction 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.
