feat: add @chat-adapter/web — browser chat UI for chat-sdk bots#444
Merged
feat: add @chat-adapter/web — browser chat UI for chat-sdk bots#444
Conversation
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Return the inner task as Promise<void> instead of void so streaming adapters can await full handler completion and surface user-handler rejections at the wire level. waitUntil semantics for existing webhook adapters are unchanged — the SDK still tracks the work with errors swallowed (and logged) so platforms don't retry on handler bugs. Required by @chat-adapter/web, whose response body is the user handler's stream.
A new platform adapter that lets a chat-sdk bot serve a browser chat
UI alongside Slack/Teams/Discord/etc. without writing any client-side
glue. Speaks the AI SDK UI message stream protocol, so @ai-sdk/react's
useChat and the ai-elements component library work out of the box.
- `@chat-adapter/web` — server: createWebAdapter({ userName, getUser })
- `@chat-adapter/web/react` — client: useChat() preconfigured with
DefaultChatTransport against /api/chat (override via `api`)
Defaults that matter for v1:
- `isDM: true` — every web message routes through onDirectMessage
- `persistMessageHistory: true` — chat-sdk caches each turn in the
configured state adapter so handlers can read prior context via
thread.messages / channel.messages (no platform history API exists)
- channelId === threadId — web has no separate channel concept; this
prevents cross-conversation bleed when a single user has multiple
useChat sessions
- Native `adapter.stream` implementation pumps text-deltas straight
onto the SSE response — no post+edit fallback
Out of scope for v1: cards/JSX rendering, reactions, modals, file
uploads, edit/delete, multi-tab proactive push.
- Register the web adapter in lib/adapters.ts with a demo getUser (single shared identity — replace with NextAuth/Clerk/cookie auth in production) - Expose POST /api/chat backed by bot.webhooks.web (using next/after for waitUntil) - Add a minimal /chat page using @chat-adapter/web/react's useChat — same bot.onDirectMessage handler that powers Slack now powers the browser too Bumps `ai` to ^6.0.174 to align with @ai-sdk/react@^3 (avoids dual provider-utils versions in the workspace).
- Add an entry to adapters.json so the package shows up on /adapters - Add a globe SVG to lib/logos.tsx and wire it into the icon map - Mention the new adapter in docs/adapters.mdx
- Reject user ids containing ':' with HTTP 400 — the character would corrupt the thread-id round-trip through decodeThreadId - Skip emitting text-start/text-end in postMessage when the resolved text is empty so useChat doesn't render blank assistant bubbles - Derive the parseMessage author from raw.role so rehydrated assistant messages report the bot identity instead of "unknown" - Drop the duplicate handler-error log; chat.processMessage already logs at ERROR level - Document the actual persistMessageHistory default (true) and the state-cache rationale; promote the fetchMessages no-op rationale into its JSDoc
- Aborting request.signal mid-stream short-circuits the iterator and still writes text-end via the finally block - Non-text StreamChunks (task_update, plan_update) are dropped without emitting any delta - The SentMessage returned from thread.post matches the id used in text-start / text-end events
The docs site renders each adapter's README, so flesh out @chat-adapter/web to match the depth of @chat-adapter/slack: authentication boundary, threading semantics, streaming, persistence, React hook reference, configuration table, feature matrix, and troubleshooting.
gr2m
approved these changes
May 5, 2026
gr2m
left a comment
There was a problem hiding this comment.
Code changes look good to me! Very cool!
dancer
approved these changes
May 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
add
@chat-adapter/webA new platform adapter that lets a chat-sdk bot serve a browser chat UI alongside Slack, Teams, Discord, etc. The same
bot.onDirectMessage(...)handler fires for every platform — including streamed replies viathread.post(stream).The adapter speaks the AI SDK UI message stream protocol, so
@ai-sdk/react'suseChatand theai-elementscomponent library work out of the box. No client-side glue.What's in the package
@chat-adapter/webships two subpath exports:@chat-adapter/web— server-sidecreateWebAdapter({ userName, getUser })that produces anAdapterfor theChatconstructor. Handles webhook parsing, user resolution, and streaming the response.@chat-adapter/web/react— thin client wrapper exposinguseChat()preconfigured withDefaultChatTransport. Re-exportsUIMessageandUseChatHelperstypes.How the pieces fit
isDM: trueroutes every web message throughonDirectMessage.channelIdFromThreadId === threadIdkeepschannel.messagesscoped per useChat conversation.persistMessageHistory: true(default) backfillsthread.messagesfrom state since web has no platform history API.Notable design choices
getUseris the security boundaryWeb requests come straight from a browser, so unlike the platform adapters there's no signature to verify.
getUseris the auth boundary: returningnull→ HTTP 401 and no handler runs. The adapter's job is to plug into whatever the host app already uses (NextAuth, Clerk, custom session cookies). User ids that contain the reserved:delimiter are rejected with HTTP 400 to keep the thread-id round-trip clean.Native streaming end-to-end
thread.postacceptsAsyncIterable<string | StreamChunk>and pumps deltas straight onto the SSE response body — no edit loop, no rate-limit concerns. Plays nicely with the AI SDK'sstreamText.request.signalis forwarded into the stream loop, souseChat'sstop()short-circuits the iterator on the server side.task_update/plan_updatechunks have no native v1 representation in the UI message stream and are dropped silently.chat.processMessagereturns aPromiseThe Web adapter response body is the user handler's output stream, so we need to surface handler errors to the client.
Chat.processMessagepreviously returnedvoid; it now returnsPromise<void>that rejects on handler failure. Existing webhook adapters usingoptions.waitUntilare unchanged: the SDK still tracks the work with errors swallowed (logged) so platforms don't retry on handler bugs.Message persistence defaults to
trueWeb has no platform-side history API, so
thread.messages/channel.messagesare only populated through the configured state adapter's message history cache. Defaulting totruematches the typical use case; opt out only if your handler re-derives history from the request body'smessages[]itself.v1 scope
In: text + markdown, native streaming, DM-style routing (
isDM: true), persisted message history, abort propagation viarequest.signal, useChat-compatibleuseChathook.Out (deferred to v2): cards/JSX rendering, reactions, modals, file uploads, edit/delete, multi-tab proactive push.
Example app
examples/nextjs-chatgains:/chatpage usinguseChatwithai-elementscomponents/api/chatroute delegating tobot.webhooks.webwith Next.jsafter()for trackingbotinstance — same handlers fire from Slack and from the browserTests
packages/adapter-web/src/index.test.ts— 18 tests covering construction, thread-id encoding, input validation (400/401 paths), end-to-end handler dispatch (onDirectMessagerouting, async-iterable streaming, error propagation), and directstream()coverage (abort short-circuit, dropping non-texttask_update/plan_updatechunks,SentMessage.idmatches the streamedtext-*event id).packages/chat/src/chat.test.ts— new tests for the awaitableprocessMessagecontract (resolves on success, rejects with the original error on handler failure,waitUntiltracks both).