Skip to content
Pilotoo
v0.1

Connecting a client

The client never builds a connection by itself. You hand the session a ChatTransport, and a CredentialsProvider that returns the current bearer token. With @pilotoo/ui/chat both are done for you from a URL and a token function; this page is for the headless path with @pilotoo/chat-core, and for custom channels.

The demo web app (apps/web) creates one transport and hands it to the session provider:

src/features/chat/chat-session.tsx
import { ChatSessionProvider } from '@pilotoo/chat-core';
import { createSocketIoTransport } from '@pilotoo/chat-core/socket-io';
import { useState, type ReactNode } from 'react';
export function ChatSession({ apiUrl, credentials, children }: Props) {
const [transport] = useState(() => createSocketIoTransport(apiUrl, { credentials }));
return <ChatSessionProvider transport={transport}>{children}</ChatSessionProvider>;
}

The mobile app does the same with a URL only: createSocketIoTransport(resolveApiUrl()).

A CredentialsProvider returns the current access token:

src/auth/credentials.ts
import type { CredentialsProvider } from '@pilotoo/chat-core';
export const credentials: CredentialsProvider = () => auth.currentAccessToken();

Pass it as { credentials } to each client piece that talks to the server: createSocketIoTransport, createConversationCatalog, createAgentCatalog, createAttachmentUploader, createAttachmentImageResolver, createBlobAttachmentImageResolver and createPresentationSource.

The demo web app builds all its REST clients the same way:

const conversations = createConversationCatalog(apiUrl, undefined, { credentials });
const agents = createAgentCatalog(apiUrl, undefined, { credentials });

The provider is called for every request and every socket (re)connection, so a refreshed token is used. How the server turns the token into an identity is covered in Authentication.

The desktop app starts a local backend that prints a URL and a random token per launch, and injects them into the page as window.__PILOTOO_BACKEND__. The web app reads them and uses a constant token:

const backend = window.__PILOTOO_BACKEND__;
const credentials = backend ? () => backend.token : undefined;
const apiUrl = backend?.url ?? buildTimeApiUrl;

See apps/web/src/features/chat/backend-handoff.ts.

A host on another channel (IPC, a test script) implements the ChatTransport type from packages/chat-core/src/transport.ts:

Member Contract
connected Whether the connection is up right now.
send(command) Sends a ChatCommand (history, run-start, run-cancel, run-detach, run-resume).
subscribe(listener) Delivers ChatTransportEvents; the returned function ends the connection.

Events are connected, disconnected, connect-failed, protocol-error, protocol-hello, run-event and task-status. Pass it with <ChatSessionProvider transport={...}>, or as the transport prop of ChatProvider.

The command names are kebab-case (run-start); the messages on the Socket.IO wire are run:start, run:resume and so on, and the built-in transport maps one to the other. A custom transport that talks to the kit’s gateway does the same mapping. See the protocol.

Prop Default Meaning
transport required The ChatTransport.
clientTools none See Client tools.
protocolGraceMs 2000 How long a connection waits for the gateway’s protocol:hello before treating it as compatible.
  • @pilotoo/chat-core never imports socket.io-client; it lives only behind the /socket-io entry (an optional peer).
  • Every payload a transport delivers is validated against @pilotoo/chat-protocol before it reaches state.
  • An event type the client does not know is counted and ignored.
  • No command is sent before the gateway’s protocol announcement or the protocolGraceMs fallback.
  • A token is sent as Authorization: Bearer <token>. An empty or absent token sends no header.