A customizable React UI for the appx agent-server — streaming chat, session management, tool-call cards, extension-UI prompts, and model/thinking controls. Transport-agnostic and themeable, so the same package powers different apps (lanquest, quest, appx, …) with project-specific styling and layout.
This is the standalone source of truth for the package. Consumers depend on it by version from the registry (CI/prod), and link it locally for live editing during development (see Local development across repos below).
Published to GitHub Packages under the @appx-org scope. Consumers add a scope
registry mapping (.npmrc):
@appx-org:registry=https://npm.pkg.github.com
then npm i @appx-org/agent-client. Peer deps: react >=18, react-dom >=18.
The package ships its TypeScript source (exports → src), so a consuming
Vite/bundler app compiles it directly. To edit it live from a sibling app
(e.g. lanquest) without publishing, point the app at this checkout with a
file: dependency and let its bundler follow the symlink:
Because react/react-dom are peer deps, the consumer must dedupe React so
the symlink doesn't pull a second copy (which breaks hooks). In Vite:
resolve: { dedupe: ['react', 'react-dom'] }Edits to src/ are then picked up on the consumer's next build/HMR — no
republish, no reinstall. For CI/prod, swap the file: spec back to a semver
range (^0.1.0).
import { AgentChatProvider, AgentChat } from 'agent-client';
import 'agent-client/styles.css';
export function App() {
return (
<AgentChatProvider config={{ baseUrl: '/agent', pathPrefix: '/v1' }}>
<AgentChat projectId="my-project" />
</AgentChatProvider>
);
}baseUrl + pathPrefix point at the agent-server /v1 contract. Use the
agent-server origin directly (baseUrl: 'http://127.0.0.1:4001') or, recommended,
a same-origin reverse proxy that mirrors /v1 (so the bearer token and cookies
stay server-side — see lanquest's backend).
Two layers:
core/(framework-agnostic) —AgentClient(configurable transport, backed byopenapi-fetchso request bodies, path params, and response types are inferred from the contract),SessionStore(shared SSE pool + reducer dispatch), and the puresessionReducerthat turns SSE events / REST history intoUiMessage[]. Contract types live incore/types.tsand are derived fromcore/agent-server.generated.ts— generated from agent-server'sopenapi.json, never hand-written.react/—AgentChatProvider(DI for client + store + theme),useAgentSessionhook, and components:AgentChat,ChatPanel,SessionList,ToolCallCard,ExtensionRequestPanel,Markdown.
The REST DTOs and the SSE event/message types (WireEvent, ToolCall,
AssistantMessage, …) are codegen'd from agent-server's openapi.json, so they
stay in sync with the contract and there's no field-name guessing in the reducer.
# 1. refresh the vendored contract snapshot (after agent-server changes)
# 1. refresh the vendored contract snapshot (after agent-server changes)
cp ../agent-server/openapi.json openapi/agent-server.json
# (or: curl -s http://127.0.0.1:4001/openapi.json -o openapi/agent-server.json)
# 2. regenerate src/core/agent-server.generated.ts
npm run gen:apiIf a committed contract field changed, the generated types shift and
core/types.ts, the reducer, or the AgentClient REST calls fail to compile —
the intended drift signal. Because the REST methods are typed against the
generated paths via openapi-fetch, a changed response/param/body shape
surfaces directly at the call site (no hand-written return types to keep in
sync). Run npm run typecheck to surface it. Every route also carries an
operationId, so the generated operations map and any future SDK codegen get
stable, human-readable names.
- CSS variables — every value resolves to an
--ac-*custom property. Re-theme by redefining them on a wrapper:.agent-client-root { --ac-accent: #c084fc; --ac-bg: #1a1226; }
classNames/labels— pass per-slot class names and string overrides toAgentChatProvider.- Render slots —
ChatPanelacceptsrenderMessage,renderEmpty,showHeader,showModelControls. - Composition — for fully bespoke layouts, drop
AgentChatand composeSessionList+ChatPanel, or build directly onuseAgentSession.
createAgentClient({
baseUrl?: string; // default '' (same origin)
pathPrefix?: string; // default '/v1'
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
fetch?: typeof fetch;
eventSourceFactory?: (url: string) => EventSourceLike;
onUnauthorized?: () => void;
});