@handrail-chat/client
v0.4.0
Published
Framework-agnostic client for Handrail — transport, session exchange, and conversation state.
Maintainers
Readme
@handrail-chat/client
The framework-agnostic core of Handrail — an in-app help surface where customers ask for help in your product and agents answer from Slack.
If you are building with React, install
@handrail-chat/react
instead. It depends on this package and gives you a panel, a launcher and
hooks. This one is for building your own UI, or a binding for another framework.
bun add @handrail-chat/clientNo runtime dependencies.
What it gives you
import { createHandrail, createSseTransport } from "@handrail-chat/client";
const handrail = createHandrail({
transport: createSseTransport({
baseUrl: "https://api.handrail.chat",
getToken: async () => (await fetch("/api/handrail-token")).json().then((r) => r.token),
}),
});
handrail.subscribe(() => render(handrail.getState()));
await handrail.start();
await handrail.send("my export keeps failing");getState() returns messages, conversations, connection state and status.
subscribe fires on every change. There is no state library here and none is
forced on you.
See the React package's README for the token contract — what your backend has to sign, and why.
The transport seam
Transport is an interface with two implementations. SseTransport talks to a
real Handrail; MockTransport is memory, and needs no server, database or
Slack. Nothing above the transport can tell them apart, which is what makes the
same components drivable from fixtures in tests and stories.
import { createMockTransport } from "@handrail-chat/client";
const transport = createMockTransport();
const handrail = createHandrail({ transport });
transport.receiveAgentMessage("Looking into it now");Context for the agent
Two ways to tell whoever answers in Slack something the customer did not type. Neither is ever shown to the customer. The React package's README covers both in depth, with a full example.
// An outcome the agent must not miss. Its own Slack message; works with the
// panel closed and on a resolved conversation.
await handrail.postNote('👍 "Exchanging tickets" solved it', { conversationId });
// What the customer is doing while they talk. Grey lines in one growing Slack
// message; sent only while the panel is open and the conversation unresolved.
handrail.setPanelOpen(true); // from your panel, as it opens and closes
handrail.trackActivity({ kind: "page", text: "Order #1234", url: location.href });trackActivity never throws and returns nothing: events are batched for a
quarter of a second, and a failed send is dropped rather than retried.
Notes
Live updates use fetch with a stream, not EventSource — the latter
cannot set headers, which would force the session token into a query string
where it lands in access logs and browser history.
start() never rejects, so it is safe to call without awaiting. If the
widget cannot connect — your getToken throws, or the network is down —
status is "error" and error says why, and it retries with backoff until
it can. Gate a support launcher on status, not on whether start()
resolved.
Reconnecting refetches a delta, not the whole history.
Sends are optimistic and reconcile by a client-generated id, so there is no
duplicate when the server's echo arrives. A failed send stays in state marked
failed with its clientId, ready for retry.
License
UNLICENSED — not yet open source.
