@pinecall/web 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # @pinecall/web
2
+
3
+ [![npm](https://img.shields.io/npm/v/@pinecall/web)](https://www.npmjs.com/package/@pinecall/web)
4
+
5
+ The web client for [Pinecall](https://pinecall.io) agents — real-time WebRTC **voice**, text **chat**, and drop-in **React** widgets, in one package.
6
+
7
+ > **Migrating from `@pinecall/voice-core` / `@pinecall/voice-widget` / `@pinecall/chat-core`?** They are now a single package. See [Entry points](#entry-points) below — the React widget moves to the package root, vanilla voice to `/core`, and chat to `/chat` + `/chat/react`.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @pinecall/web
13
+ # React is a peer dep — only needed for the widget + chat/react entries
14
+ npm install react react-dom
15
+ ```
16
+
17
+ ## Entry points
18
+
19
+ | Import | What | Needs React |
20
+ |--------|------|-------------|
21
+ | `@pinecall/web` | React widgets — `VoiceWidget`, `ContactHub`, `ChatView`, `useVoice`, `useVoiceSession`, presets | ✅ |
22
+ | `@pinecall/web/core` | `VoiceSession` — framework-agnostic WebRTC voice client | ❌ |
23
+ | `@pinecall/web/chat` | `ChatSession` — framework-agnostic text chat client | ❌ |
24
+ | `@pinecall/web/chat/react` | `usePinecallChat` — React hook over `ChatSession` | ✅ |
25
+
26
+ ## Quick Start
27
+
28
+ ### React widget
29
+
30
+ ```tsx
31
+ import { VoiceWidget } from "@pinecall/web";
32
+
33
+ <VoiceWidget agent="mara" name="Mara" preset="midnight" />
34
+ ```
35
+
36
+ ### Vanilla voice (any framework)
37
+
38
+ ```ts
39
+ import { VoiceSession } from "@pinecall/web/core";
40
+
41
+ const session = new VoiceSession({ agent: "mara" });
42
+ session.subscribe(() => console.log(session.getState()));
43
+ await session.connect();
44
+ ```
45
+
46
+ ### Chat hook
47
+
48
+ ```tsx
49
+ import { usePinecallChat } from "@pinecall/web/chat/react";
50
+
51
+ const chat = usePinecallChat({ agent: "florencia" });
52
+ ```
53
+
54
+ ## Structure
55
+
56
+ ```
57
+ web/
58
+ ├── src/
59
+ │ ├── index.ts @pinecall/web — React widgets barrel
60
+ │ ├── core/ @pinecall/web/core — VoiceSession (vanilla)
61
+ │ ├── chat/ @pinecall/web/chat[/react] — ChatSession + React hook
62
+ │ └── widget/ React components (VoiceWidget, ContactHub, ChatView…)
63
+ ├── docs/ diagrams + legacy changelogs
64
+ ├── examples/react/ Demo app with preset switcher
65
+ ├── tsup.config.ts Build (4 entries → ESM + CJS + DTS)
66
+ └── tsconfig.json
67
+ ```
68
+
69
+ ## Development
70
+
71
+ ```bash
72
+ pnpm install
73
+ pnpm build # build all 4 entries (ESM + CJS + DTS)
74
+ pnpm dev # tsup watch
75
+ pnpm typecheck
76
+ ```
77
+
78
+ ## Publishing
79
+
80
+ ```bash
81
+ npm version <patch|minor|major>
82
+ pnpm release # build + npm publish
83
+ ```
84
+
85
+ ## Widget Theme & Orb States
86
+
87
+ The `<VoiceWidget>` orb cycles through visual states as the session progresses. Each state has a configurable color (RGB triplet):
88
+
89
+ | Orb State | CSS Class | Theme Property | Default Color | When |
90
+ |-----------|-----------|---------------|---------------|------|
91
+ | Idle | — | `orbFrom`/`orbMid`/`orbTo` | Pearl gradient | Not connected |
92
+ | Connecting | `.connecting` | `colorConnecting` | `245, 158, 11` (amber) | Establishing WebRTC |
93
+ | Active | `.active` | `colorActive` | `76, 175, 80` (green) | Connected, waiting |
94
+ | User speaking | `.user-speaking` | `colorUserSpeaking` | `52, 211, 153` (emerald) | User talking |
95
+ | Agent speaking | `.speaking` | `colorSpeaking` | `248, 113, 113` (rose) | Agent talking |
96
+ | Thinking | `.thinking` | `colorThinking` | `139, 92, 246` (violet) | Processing |
97
+ | **Idle warning** | `.idle-warning` | `colorWarning` | `255, 160, 0` (orange) | User silent too long, call will timeout |
98
+
99
+ ### Idle Warning
100
+
101
+ When the server emits `session.idle_warning`, the orb switches to the `idle-warning` state — a blinking amber/orange animation. This warns the user that the call will end due to inactivity.
102
+
103
+ ```tsx
104
+ // Customize the warning color via theme
105
+ <VoiceWidget
106
+ agent="mara"
107
+ theme={{ colorWarning: "255, 60, 60" }} // red warning
108
+ />
109
+ ```
110
+
111
+ The idle warning is cleared when:
112
+ - The user starts speaking
113
+ - The session disconnects
114
+ - `session.timeout` fires (auto-disconnect)
115
+
116
+ ### Theme Presets
117
+
118
+ 5 built-in presets: `dark` (default), `midnight`, `aurora`, `sunset`, `light`.
119
+
120
+ ```tsx
121
+ <VoiceWidget agent="mara" preset="midnight" />
122
+ ```
123
+
124
+ ### Custom Theme
125
+
126
+ Override individual colors on top of any preset:
127
+
128
+ ```tsx
129
+ <VoiceWidget
130
+ agent="mara"
131
+ preset="dark"
132
+ theme={{
133
+ colorActive: "0, 200, 100",
134
+ colorWarning: "255, 80, 0",
135
+ ringColor: "100, 100, 200",
136
+ }}
137
+ />
138
+ ```
139
+
140
+ All theme properties accept **RGB triplets** (e.g. `"255, 160, 0"`) for use with CSS `rgba()`.
141
+
142
+ ## Session Limits (via `@pinecall/sdk`)
143
+
144
+ Session limits are configured on the agent (server-side SDK) and flow through to the WebRTC widget automatically:
145
+
146
+ ```typescript
147
+ // Server-side (agent.js)
148
+ const agent = pc.deploy("my-agent", {
149
+ // ...voice, stt, llm config...
150
+ sessionLimits: {
151
+ idle_timeout_seconds: 20, // hang up after 20s of silence
152
+ idle_warning_seconds: 10, // warn 10s before timeout
153
+ max_duration_seconds: 600, // hard cap at 10 minutes
154
+ },
155
+ });
156
+
157
+ agent.on("session.idle_warning", (event, call) => {
158
+ call.say("Are you still there?");
159
+ });
160
+ ```
161
+
162
+ The widget receives `session.idle_warning` via DataChannel and:
163
+ 1. Switches the orb to the **idle-warning** state (blinking `colorWarning`)
164
+ 2. On `session.timeout`, auto-disconnects and resets to idle
165
+
166
+ ## License
167
+
168
+ MIT
@@ -0,0 +1,250 @@
1
+ 'use strict';
2
+
3
+ // src/chat/ChatSession.ts
4
+ var INITIAL_STATE = {
5
+ status: "idle",
6
+ error: null,
7
+ messages: [],
8
+ typing: false,
9
+ streamingText: "",
10
+ sessionId: null
11
+ };
12
+ var ChatSession = class extends EventTarget {
13
+ constructor(opts) {
14
+ super();
15
+ this.opts = opts;
16
+ }
17
+ opts;
18
+ state = { ...INITIAL_STATE };
19
+ listeners = /* @__PURE__ */ new Set();
20
+ ws = null;
21
+ reconnectTimer = null;
22
+ msgCounter = 0;
23
+ /** Read-only snapshot of current state (stable ref until next mutation). */
24
+ getState() {
25
+ return this.state;
26
+ }
27
+ /** Subscribe to ANY state change (for React useSyncExternalStore). */
28
+ subscribe(listener) {
29
+ this.listeners.add(listener);
30
+ return () => {
31
+ this.listeners.delete(listener);
32
+ };
33
+ }
34
+ setState(patch) {
35
+ const prev = this.state;
36
+ this.state = { ...prev, ...patch };
37
+ for (const l of this.listeners) l();
38
+ if (patch.status !== void 0 && patch.status !== prev.status) {
39
+ this.dispatchEvent(
40
+ new CustomEvent("status", { detail: { status: this.state.status } })
41
+ );
42
+ }
43
+ if (patch.error !== void 0 && patch.error !== null && patch.error !== prev.error) {
44
+ this.dispatchEvent(
45
+ new CustomEvent("error", { detail: { error: this.state.error } })
46
+ );
47
+ }
48
+ this.dispatchEvent(
49
+ new CustomEvent("change", { detail: { state: this.state } })
50
+ );
51
+ }
52
+ setMessages(updater) {
53
+ const next = updater(this.state.messages);
54
+ this.setState({ messages: next });
55
+ const last = next[next.length - 1];
56
+ if (last) {
57
+ this.dispatchEvent(
58
+ new CustomEvent("message", { detail: { message: last } })
59
+ );
60
+ }
61
+ }
62
+ // ── Connection ──────────────────────────────────────────────────────
63
+ async connect() {
64
+ if (this.ws) return;
65
+ try {
66
+ this.setState({
67
+ status: "connecting",
68
+ error: null
69
+ });
70
+ const base = (this.opts.server ?? "https://voice.pinecall.io").replace(
71
+ /\/$/,
72
+ ""
73
+ );
74
+ let token;
75
+ let chatServer;
76
+ if (this.opts.tokenProvider) {
77
+ const t = await this.opts.tokenProvider();
78
+ token = t.token;
79
+ chatServer = t.server;
80
+ } else {
81
+ const tRes = await fetch(
82
+ `${base}/chat/token?agent_id=${encodeURIComponent(this.opts.agent)}`
83
+ );
84
+ if (!tRes.ok) {
85
+ const body = await tRes.text();
86
+ throw new Error(`Token: ${tRes.status} ${body}`);
87
+ }
88
+ const t = await tRes.json();
89
+ token = t.token;
90
+ chatServer = t.server;
91
+ }
92
+ const wsBase = (chatServer || base).replace(/^http:/, "ws:").replace(/^https:/, "wss:");
93
+ const ws = new WebSocket(`${wsBase}/chat/ws?token=${token}`);
94
+ this.ws = ws;
95
+ ws.onopen = () => {
96
+ };
97
+ ws.onmessage = (evt) => this.handleMessage(evt);
98
+ ws.onerror = () => {
99
+ this.setState({ error: "WebSocket error", status: "error" });
100
+ };
101
+ ws.onclose = (evt) => {
102
+ this.ws = null;
103
+ if (this.state.status === "connected") {
104
+ this.setState({ status: "idle" });
105
+ }
106
+ };
107
+ } catch (err) {
108
+ this.setState({
109
+ error: err instanceof Error ? err.message : String(err),
110
+ status: "error"
111
+ });
112
+ this.ws = null;
113
+ }
114
+ }
115
+ handleMessage(evt) {
116
+ let d;
117
+ try {
118
+ d = JSON.parse(evt.data);
119
+ } catch {
120
+ return;
121
+ }
122
+ switch (d.event) {
123
+ case "chat.connected":
124
+ this.setState({
125
+ status: "connected",
126
+ sessionId: d.session_id ?? null
127
+ });
128
+ break;
129
+ case "chat.token":
130
+ case "llm.chat.token":
131
+ this.setState({
132
+ typing: true,
133
+ streamingText: d.text ?? ""
134
+ });
135
+ this.setMessages((prev) => {
136
+ const idx = prev.findIndex(
137
+ (m) => m.messageId === d.message_id && m.isStreaming
138
+ );
139
+ if (idx >= 0) {
140
+ return prev.map(
141
+ (m, i) => i === idx ? { ...m, text: d.text ?? "" } : m
142
+ );
143
+ }
144
+ return [
145
+ ...prev,
146
+ {
147
+ id: ++this.msgCounter,
148
+ role: "bot",
149
+ text: d.text ?? "",
150
+ messageId: d.message_id,
151
+ isStreaming: true
152
+ }
153
+ ];
154
+ });
155
+ break;
156
+ case "chat.done":
157
+ case "llm.chat.done":
158
+ this.setState({
159
+ typing: false,
160
+ streamingText: ""
161
+ });
162
+ this.setMessages((prev) => {
163
+ const idx = prev.findIndex(
164
+ (m) => m.messageId === d.message_id && m.isStreaming
165
+ );
166
+ if (idx >= 0) {
167
+ return prev.map(
168
+ (m, i) => i === idx ? { ...m, text: d.text ?? m.text, isStreaming: false } : m
169
+ );
170
+ }
171
+ return [
172
+ ...prev,
173
+ {
174
+ id: ++this.msgCounter,
175
+ role: "bot",
176
+ text: d.text ?? "",
177
+ messageId: d.message_id,
178
+ isStreaming: false
179
+ }
180
+ ];
181
+ });
182
+ break;
183
+ case "chat.error":
184
+ case "llm.chat.error":
185
+ this.setState({
186
+ typing: false,
187
+ error: d.error ?? "Unknown error"
188
+ });
189
+ break;
190
+ case "error":
191
+ this.setState({
192
+ error: d.error ?? "Unknown error",
193
+ status: "error"
194
+ });
195
+ break;
196
+ }
197
+ this.dispatchEvent(new CustomEvent("event", { detail: d }));
198
+ }
199
+ // ── Actions ─────────────────────────────────────────────────────────
200
+ /** Send a text message to the agent. */
201
+ send(text) {
202
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
203
+ const trimmed = text.trim();
204
+ if (!trimmed) return;
205
+ this.setMessages((prev) => [
206
+ ...prev,
207
+ {
208
+ id: ++this.msgCounter,
209
+ role: "user",
210
+ text: trimmed
211
+ }
212
+ ]);
213
+ this.ws.send(JSON.stringify({ event: "message", text: trimmed }));
214
+ }
215
+ /**
216
+ * Set or clear a keyed context block in the LLM system prompt.
217
+ *
218
+ * @example
219
+ * ```ts
220
+ * session.setContext("form", JSON.stringify({ name: "Juan" }));
221
+ * session.setContext("form", null); // clear
222
+ * ```
223
+ */
224
+ setContext(key, value) {
225
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
226
+ this.ws.send(JSON.stringify({ event: "set_context", key, value }));
227
+ }
228
+ /** Disconnect the chat session. */
229
+ disconnect() {
230
+ if (this.reconnectTimer) {
231
+ clearTimeout(this.reconnectTimer);
232
+ this.reconnectTimer = null;
233
+ }
234
+ if (this.ws) {
235
+ this.ws.close();
236
+ this.ws = null;
237
+ }
238
+ this.setState({ status: "idle", typing: false, streamingText: "" });
239
+ }
240
+ /** Tear down the session and clear subscribers. Do not reuse after this. */
241
+ destroy() {
242
+ this.disconnect();
243
+ this.setState({ status: "destroyed" });
244
+ this.listeners.clear();
245
+ }
246
+ };
247
+
248
+ exports.ChatSession = ChatSession;
249
+ //# sourceMappingURL=index.cjs.map
250
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/chat/ChatSession.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * ChatSession — Framework-agnostic text chat client for Pinecall agents.\n *\n * Mirrors VoiceSession's API patterns:\n * - session.subscribe(cb) + session.getState() — for React useSyncExternalStore\n * - session.addEventListener('status' | 'message' | 'error' | 'change', cb)\n *\n * Flow: GET /chat/token → WS /chat/ws?token=cht_xxx → bidirectional text chat\n */\nimport type {\n ChatSessionState,\n ChatSessionOptions,\n ChatMessage,\n} from \"./types\";\n\nconst INITIAL_STATE: ChatSessionState = {\n status: \"idle\",\n error: null,\n messages: [],\n typing: false,\n streamingText: \"\",\n sessionId: null,\n};\n\nexport class ChatSession extends EventTarget {\n private state: ChatSessionState = { ...INITIAL_STATE };\n private listeners = new Set<() => void>();\n\n private ws: WebSocket | null = null;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private msgCounter = 0;\n\n constructor(private opts: ChatSessionOptions) {\n super();\n }\n\n /** Read-only snapshot of current state (stable ref until next mutation). */\n getState(): Readonly<ChatSessionState> {\n return this.state;\n }\n\n /** Subscribe to ANY state change (for React useSyncExternalStore). */\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n private setState(patch: Partial<ChatSessionState>): void {\n const prev = this.state;\n this.state = { ...prev, ...patch };\n for (const l of this.listeners) l();\n\n if (patch.status !== undefined && patch.status !== prev.status) {\n this.dispatchEvent(\n new CustomEvent(\"status\", { detail: { status: this.state.status } }),\n );\n }\n if (\n patch.error !== undefined &&\n patch.error !== null &&\n patch.error !== prev.error\n ) {\n this.dispatchEvent(\n new CustomEvent(\"error\", { detail: { error: this.state.error } }),\n );\n }\n this.dispatchEvent(\n new CustomEvent(\"change\", { detail: { state: this.state } }),\n );\n }\n\n private setMessages(\n updater: (prev: ChatMessage[]) => ChatMessage[],\n ): void {\n const next = updater(this.state.messages);\n this.setState({ messages: next });\n const last = next[next.length - 1];\n if (last) {\n this.dispatchEvent(\n new CustomEvent(\"message\", { detail: { message: last } }),\n );\n }\n }\n\n // ── Connection ──────────────────────────────────────────────────────\n\n async connect(): Promise<void> {\n if (this.ws) return;\n\n try {\n this.setState({\n status: \"connecting\",\n error: null,\n });\n\n const base = (this.opts.server ?? \"https://voice.pinecall.io\").replace(\n /\\/$/,\n \"\",\n );\n\n // 1. Fetch chat token — use tokenProvider (backend proxy) or direct fetch\n let token: string;\n let chatServer: string;\n if (this.opts.tokenProvider) {\n const t = await this.opts.tokenProvider();\n token = t.token;\n chatServer = t.server;\n } else {\n const tRes = await fetch(\n `${base}/chat/token?agent_id=${encodeURIComponent(this.opts.agent)}`,\n );\n if (!tRes.ok) {\n const body = await tRes.text();\n throw new Error(`Token: ${tRes.status} ${body}`);\n }\n const t = await tRes.json();\n token = t.token;\n chatServer = t.server;\n }\n const wsBase = (chatServer || base)\n .replace(/^http:/, \"ws:\")\n .replace(/^https:/, \"wss:\");\n\n // 2. Open WebSocket\n const ws = new WebSocket(`${wsBase}/chat/ws?token=${token}`);\n this.ws = ws;\n\n ws.onopen = () => {\n // Wait for chat.connected event before setting status\n };\n\n ws.onmessage = (evt) => this.handleMessage(evt);\n\n ws.onerror = () => {\n this.setState({ error: \"WebSocket error\", status: \"error\" });\n };\n\n ws.onclose = (evt) => {\n this.ws = null;\n if (this.state.status === \"connected\") {\n // Unexpected disconnect\n this.setState({ status: \"idle\" });\n }\n };\n } catch (err) {\n this.setState({\n error: err instanceof Error ? err.message : String(err),\n status: \"error\",\n });\n this.ws = null;\n }\n }\n\n private handleMessage(evt: MessageEvent): void {\n let d: any;\n try {\n d = JSON.parse(evt.data);\n } catch {\n return;\n }\n\n switch (d.event) {\n case \"chat.connected\":\n this.setState({\n status: \"connected\",\n sessionId: d.session_id ?? null,\n });\n break;\n\n case \"chat.token\":\n case \"llm.chat.token\":\n // Streaming token from LLM\n this.setState({\n typing: true,\n streamingText: d.text ?? \"\",\n });\n\n // Update or create bot message\n this.setMessages((prev) => {\n const idx = prev.findIndex(\n (m) => m.messageId === d.message_id && m.isStreaming,\n );\n if (idx >= 0) {\n return prev.map((m, i) =>\n i === idx ? { ...m, text: d.text ?? \"\" } : m,\n );\n }\n return [\n ...prev,\n {\n id: ++this.msgCounter,\n role: \"bot\",\n text: d.text ?? \"\",\n messageId: d.message_id,\n isStreaming: true,\n },\n ];\n });\n break;\n\n case \"chat.done\":\n case \"llm.chat.done\":\n // LLM finished streaming\n this.setState({\n typing: false,\n streamingText: \"\",\n });\n\n this.setMessages((prev) => {\n const idx = prev.findIndex(\n (m) => m.messageId === d.message_id && m.isStreaming,\n );\n if (idx >= 0) {\n return prev.map((m, i) =>\n i === idx\n ? { ...m, text: d.text ?? m.text, isStreaming: false }\n : m,\n );\n }\n // If we missed the streaming, add the final message\n return [\n ...prev,\n {\n id: ++this.msgCounter,\n role: \"bot\",\n text: d.text ?? \"\",\n messageId: d.message_id,\n isStreaming: false,\n },\n ];\n });\n break;\n\n case \"chat.error\":\n case \"llm.chat.error\":\n this.setState({\n typing: false,\n error: d.error ?? \"Unknown error\",\n });\n break;\n\n case \"error\":\n this.setState({\n error: d.error ?? \"Unknown error\",\n status: \"error\",\n });\n break;\n }\n\n // Emit raw event for power users\n this.dispatchEvent(new CustomEvent(\"event\", { detail: d }));\n }\n\n // ── Actions ─────────────────────────────────────────────────────────\n\n /** Send a text message to the agent. */\n send(text: string): void {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;\n\n const trimmed = text.trim();\n if (!trimmed) return;\n\n // Add user message to local state immediately\n this.setMessages((prev) => [\n ...prev,\n {\n id: ++this.msgCounter,\n role: \"user\",\n text: trimmed,\n },\n ]);\n\n // Send to server\n this.ws.send(JSON.stringify({ event: \"message\", text: trimmed }));\n }\n\n /**\n * Set or clear a keyed context block in the LLM system prompt.\n *\n * @example\n * ```ts\n * session.setContext(\"form\", JSON.stringify({ name: \"Juan\" }));\n * session.setContext(\"form\", null); // clear\n * ```\n */\n setContext(key: string, value: string | null): void {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;\n this.ws.send(JSON.stringify({ event: \"set_context\", key, value }));\n }\n\n /** Disconnect the chat session. */\n disconnect(): void {\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n if (this.ws) {\n this.ws.close();\n this.ws = null;\n }\n this.setState({ status: \"idle\", typing: false, streamingText: \"\" });\n }\n\n /** Tear down the session and clear subscribers. Do not reuse after this. */\n destroy(): void {\n this.disconnect();\n this.setState({ status: \"destroyed\" });\n this.listeners.clear();\n }\n}\n"]}
@@ -0,0 +1,48 @@
1
+ import { C as ChatSessionOptions, a as ChatSessionState } from '../types-W0229iUB.cjs';
2
+ export { b as ChatEventType, c as ChatMessage, d as ChatStatus } from '../types-W0229iUB.cjs';
3
+
4
+ /**
5
+ * ChatSession — Framework-agnostic text chat client for Pinecall agents.
6
+ *
7
+ * Mirrors VoiceSession's API patterns:
8
+ * - session.subscribe(cb) + session.getState() — for React useSyncExternalStore
9
+ * - session.addEventListener('status' | 'message' | 'error' | 'change', cb)
10
+ *
11
+ * Flow: GET /chat/token → WS /chat/ws?token=cht_xxx → bidirectional text chat
12
+ */
13
+
14
+ declare class ChatSession extends EventTarget {
15
+ private opts;
16
+ private state;
17
+ private listeners;
18
+ private ws;
19
+ private reconnectTimer;
20
+ private msgCounter;
21
+ constructor(opts: ChatSessionOptions);
22
+ /** Read-only snapshot of current state (stable ref until next mutation). */
23
+ getState(): Readonly<ChatSessionState>;
24
+ /** Subscribe to ANY state change (for React useSyncExternalStore). */
25
+ subscribe(listener: () => void): () => void;
26
+ private setState;
27
+ private setMessages;
28
+ connect(): Promise<void>;
29
+ private handleMessage;
30
+ /** Send a text message to the agent. */
31
+ send(text: string): void;
32
+ /**
33
+ * Set or clear a keyed context block in the LLM system prompt.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * session.setContext("form", JSON.stringify({ name: "Juan" }));
38
+ * session.setContext("form", null); // clear
39
+ * ```
40
+ */
41
+ setContext(key: string, value: string | null): void;
42
+ /** Disconnect the chat session. */
43
+ disconnect(): void;
44
+ /** Tear down the session and clear subscribers. Do not reuse after this. */
45
+ destroy(): void;
46
+ }
47
+
48
+ export { ChatSession, ChatSessionOptions, ChatSessionState };
@@ -0,0 +1,48 @@
1
+ import { C as ChatSessionOptions, a as ChatSessionState } from '../types-W0229iUB.js';
2
+ export { b as ChatEventType, c as ChatMessage, d as ChatStatus } from '../types-W0229iUB.js';
3
+
4
+ /**
5
+ * ChatSession — Framework-agnostic text chat client for Pinecall agents.
6
+ *
7
+ * Mirrors VoiceSession's API patterns:
8
+ * - session.subscribe(cb) + session.getState() — for React useSyncExternalStore
9
+ * - session.addEventListener('status' | 'message' | 'error' | 'change', cb)
10
+ *
11
+ * Flow: GET /chat/token → WS /chat/ws?token=cht_xxx → bidirectional text chat
12
+ */
13
+
14
+ declare class ChatSession extends EventTarget {
15
+ private opts;
16
+ private state;
17
+ private listeners;
18
+ private ws;
19
+ private reconnectTimer;
20
+ private msgCounter;
21
+ constructor(opts: ChatSessionOptions);
22
+ /** Read-only snapshot of current state (stable ref until next mutation). */
23
+ getState(): Readonly<ChatSessionState>;
24
+ /** Subscribe to ANY state change (for React useSyncExternalStore). */
25
+ subscribe(listener: () => void): () => void;
26
+ private setState;
27
+ private setMessages;
28
+ connect(): Promise<void>;
29
+ private handleMessage;
30
+ /** Send a text message to the agent. */
31
+ send(text: string): void;
32
+ /**
33
+ * Set or clear a keyed context block in the LLM system prompt.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * session.setContext("form", JSON.stringify({ name: "Juan" }));
38
+ * session.setContext("form", null); // clear
39
+ * ```
40
+ */
41
+ setContext(key: string, value: string | null): void;
42
+ /** Disconnect the chat session. */
43
+ disconnect(): void;
44
+ /** Tear down the session and clear subscribers. Do not reuse after this. */
45
+ destroy(): void;
46
+ }
47
+
48
+ export { ChatSession, ChatSessionOptions, ChatSessionState };
@@ -0,0 +1,3 @@
1
+ export { ChatSession } from '../chunk-MCAQMGBG.js';
2
+ //# sourceMappingURL=index.js.map
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}