@frankieai/react 0.1.5 → 0.2.1

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/src/lib/client.ts CHANGED
@@ -21,15 +21,15 @@ export interface FrankieClientOptions {
21
21
  publishableKey: string;
22
22
  /**
23
23
  * Fetches a short-lived JWT for the signed-in user from the host's own
24
- * backend. Called once per session, and again when a session expires.
24
+ * backend. Called before each turn; return undefined after sign-out.
25
25
  * Omit only for tenants that allow anonymous chat.
26
26
  */
27
27
  getUserToken?: () => Promise<string | undefined> | string | undefined;
28
28
  fetchImpl?: typeof fetch;
29
29
  /**
30
30
  * Remember the session (and, in the hook, the transcript) in localStorage so a
31
- * page reload resumes the same conversation. On by default in the browser;
32
- * set false for an ephemeral chat that starts fresh every load.
31
+ * page reload resumes the same anonymous conversation. Authenticated chat
32
+ * never writes tokens or transcripts to localStorage; it uses server history.
33
33
  */
34
34
  persist?: boolean;
35
35
  /** Override localStorage namespace for session and transcript. */
@@ -59,11 +59,15 @@ export class FrankieClient {
59
59
  private readonly baseUrl: string;
60
60
  private readonly doFetch: typeof fetch;
61
61
  private session?: CreateSessionResponse;
62
+ private sessionUserToken?: string;
62
63
 
63
64
  constructor(private readonly options: FrankieClientOptions) {
64
65
  this.baseUrl = (options.baseUrl ?? FRANKIE_CLOUD_URL).replace(/\/+$/, "");
65
66
  this.doFetch = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
66
67
  this.session = this.readStored();
68
+ if (options.getUserToken && typeof window !== "undefined") {
69
+ try { for (const suffix of ["session", "messages"]) window.localStorage.removeItem(`${this.namespace}.${suffix}`); } catch { /* storage may be unavailable */ }
70
+ }
67
71
  }
68
72
 
69
73
  get sessionId(): string | undefined {
@@ -76,7 +80,9 @@ export class FrankieClient {
76
80
 
77
81
  /** Whether this client persists across reloads (browser only, opt-out via `persist:false`). */
78
82
  get persist(): boolean {
79
- return this.options.persist !== false && typeof window !== "undefined";
83
+ // Authenticated history comes from the identity-checked server. Do not leave
84
+ // a previous user's bearer token or transcript in shared browser storage.
85
+ return !this.options.getUserToken && this.options.persist !== false && typeof window !== "undefined";
80
86
  }
81
87
 
82
88
  /** localStorage namespace for this tenant/conversation, shared with the chat hook. */
@@ -115,15 +121,17 @@ export class FrankieClient {
115
121
  }
116
122
 
117
123
  async ensureSession(context?: PageContext): Promise<CreateSessionResponse> {
118
- if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 30_000) return this.session;
119
124
  const userToken = await this.options.getUserToken?.();
120
125
  if (this.options.getUserToken && !userToken) {
126
+ this.resetSession();
121
127
  throw new FrankieApiError(
122
128
  "Your signed-in Frankie session could not be verified. Refresh the page and sign in again.",
123
129
  401,
124
130
  "user_token_unavailable",
125
131
  );
126
132
  }
133
+ if (userToken !== this.sessionUserToken) this.resetSession();
134
+ if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 30_000) return this.session;
127
135
  const response = await this.doFetch(`${this.baseUrl}/v1/session`, {
128
136
  method: "POST",
129
137
  headers: { "content-type": "application/json", "x-frankie-key": this.options.publishableKey },
@@ -134,6 +142,7 @@ export class FrankieClient {
134
142
  }),
135
143
  });
136
144
  this.session = (await this.unwrap(response)) as CreateSessionResponse;
145
+ this.sessionUserToken = userToken;
137
146
  this.writeStored();
138
147
  return this.session;
139
148
  }
@@ -148,7 +157,7 @@ export class FrankieClient {
148
157
  }
149
158
 
150
159
  async resumeConversation(id = this.options.conversationId): Promise<{ messages: unknown[]; title: string } | undefined> {
151
- if (!id) return undefined;
160
+ if (!id || !this.options.getUserToken) return undefined;
152
161
  const userToken = await this.requireUserToken();
153
162
  const response = await this.doFetch(`${this.baseUrl}/v1/conversations/${encodeURIComponent(id)}/resume`, {
154
163
  method: "POST",
@@ -157,6 +166,7 @@ export class FrankieClient {
157
166
  if (response.status === 404) return undefined;
158
167
  const body = (await this.unwrap(response)) as { session: CreateSessionResponse; messages?: unknown[]; title?: string };
159
168
  this.session = body.session;
169
+ this.sessionUserToken = userToken;
160
170
  this.writeStored();
161
171
  return { messages: Array.isArray(body.messages) ? body.messages : [], title: body.title ?? "New chat" };
162
172
  }
@@ -45,6 +45,27 @@
45
45
  }
46
46
 
47
47
  @layer components {
48
+ .frankie-page-shell { display:flex; flex:1; min-height:0; height:100%; background:var(--frankie-background); color:var(--frankie-foreground); }
49
+ .frankie-page-main { display:flex; flex:1; min-width:0; min-height:0; flex-direction:column; }
50
+ .frankie-history-desktop { width:260px; flex-shrink:0; display:flex; flex-direction:column; gap:16px; padding:20px 14px; border-right:1px solid var(--frankie-border); background:var(--frankie-secondary); overflow:auto; }
51
+ .frankie-history-heading { display:flex; align-items:center; justify-content:space-between; font-size:14px; }
52
+ .frankie-new-chat { display:flex; align-items:center; justify-content:center; gap:8px; border:1px solid var(--frankie-border); border-radius:10px; background:var(--frankie-background); min-height:44px; font-weight:600; cursor:pointer; }
53
+ .frankie-history-search > span { display:block; font-size:12px; color:var(--frankie-muted-foreground); margin-bottom:6px; }
54
+ .frankie-history-search input { width:100%; min-height:40px; padding:8px 10px; border:1px solid var(--frankie-border); border-radius:8px; background:var(--frankie-background); }
55
+ .frankie-history-list { display:flex; flex-direction:column; gap:4px; flex:1; }
56
+ .frankie-history-list button { display:flex; flex-direction:column; align-items:flex-start; gap:5px; border:1px solid transparent; border-radius:8px; padding:10px; min-height:54px; background:transparent; text-align:left; cursor:pointer; }
57
+ .frankie-history-list button span { width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:13px; }
58
+ .frankie-history-list small { font-size:11px; color:var(--frankie-muted-foreground); }.frankie-history-list button:hover,.frankie-history-list button[aria-current] { background:var(--frankie-background); border-color:var(--frankie-border); }
59
+ .frankie-history-desktop > p,.frankie-history-dialog p { font-size:13px; color:var(--frankie-muted-foreground); }
60
+ .frankie-header-caption { color:var(--frankie-muted-foreground); font-size:12px; }.frankie-page-header button { display:inline-flex; align-items:center; justify-content:center; gap:6px; min-width:40px; min-height:40px; cursor:pointer; }
61
+ .frankie-chat-settings { position:relative; }.frankie-chat-settings summary { list-style:none; display:grid; place-items:center; width:40px; height:40px; cursor:pointer; border-radius:8px; }.frankie-chat-settings summary::-webkit-details-marker { display:none; }
62
+ .frankie-chat-settings > div { position:absolute; top:46px; right:0; z-index:5; width:min(280px,80vw); display:grid; gap:12px; padding:16px; border:1px solid var(--frankie-border); border-radius:12px; background:var(--frankie-background); box-shadow:0 10px 30px #0002; font-size:13px; }.frankie-chat-settings p { margin:0; color:var(--frankie-muted-foreground); }
63
+ .frankie-loaded-skills { width:100%; max-width:760px; font-size:12px; color:var(--frankie-muted-foreground); }.frankie-loaded-skills summary { cursor:pointer; padding:6px 0; }.frankie-loaded-skills ul { list-style:none; padding:4px 0; margin:0; }.frankie-loaded-skills li { display:flex; gap:8px; padding:4px 0; }.frankie-loaded-skills small { opacity:.7; }
64
+ .frankie-history-dialog { padding:0; border:0; margin:0; max-height:none; height:100dvh; max-width:none; width:min(320px,86vw); background:var(--frankie-secondary); color:var(--frankie-foreground); }.frankie-history-dialog::backdrop { background:#0008; }.frankie-history-dialog > aside { display:flex; flex-direction:column; gap:16px; height:100%; padding:calc(20px + env(safe-area-inset-top)) 16px calc(20px + env(safe-area-inset-bottom)); overflow:auto; }
65
+ .frankie-mobile-only { display:none!important; }.frankie-widget-panel { max-height:calc(100dvh - 100px); }
66
+ .frankie-root button:focus-visible,.frankie-root summary:focus-visible,.frankie-root input:focus-visible { outline:2px solid var(--frankie-primary); outline-offset:3px; }
67
+ .frankie-root button:disabled { opacity:.5; cursor:not-allowed; }
68
+ @media(max-width:760px) { .frankie-history-desktop { display:none; }.frankie-mobile-only { display:inline-flex!important; }.frankie-header-caption { display:none; }.frankie-page-header { padding:6px 10px; }.frankie-history-heading button { border:0; background:transparent; min-width:44px; min-height:44px; align-items:center; justify-content:center; }.frankie-page-prompts { gap:8px; }.frankie-widget-panel { width:calc(100vw - 32px); } }
48
69
  .frankie-root {
49
70
  --frankie-background: #ffffff;
50
71
  --frankie-foreground: #0a0a0a;
@@ -294,7 +315,7 @@
294
315
  .frankie-page-prompts button {
295
316
  display: grid;
296
317
  min-height: 60px;
297
- grid-template-columns: 2rem minmax(0, 1fr) auto;
318
+ grid-template-columns: minmax(0, 1fr) auto;
298
319
  align-items: center;
299
320
  gap: .65rem;
300
321
  padding: .75rem .9rem;
@@ -325,3 +346,64 @@
325
346
  .frankie-page-canvas .frankie-root > .border-t { padding: .65rem .75rem calc(.65rem + env(safe-area-inset-bottom)); }
326
347
  }
327
348
  }
349
+
350
+ /* Shared conversation surface: SDK pages, embedded panels and Frankie's app. */
351
+ .frankie-root.frankie-chat-panel { min-height:0; position:relative; }
352
+ .frankie-root .frankie-agent-context-wrap { flex:none; display:flex; justify-content:center; padding:12px 10px; }
353
+ .frankie-root .frankie-agent-context { position:relative; max-width:100%; color:var(--frankie-foreground); font-size:13px; }
354
+ .frankie-root .frankie-agent-context > summary { display:flex; align-items:center; justify-content:center; gap:10px; min-height:44px; padding:7px 13px; border:1px solid var(--frankie-border); border-radius:999px; background:var(--frankie-background); box-shadow:0 1px 3px #0000000d; cursor:pointer; list-style:none; white-space:nowrap; }
355
+ .frankie-root .frankie-agent-context > summary::-webkit-details-marker { display:none; }
356
+ .frankie-root .frankie-agent-context > summary > svg { flex:none; color:var(--frankie-muted-foreground); }
357
+ .frankie-root .frankie-context-count { padding:2px 10px; border-radius:999px; background:var(--frankie-muted); font-size:12px; color:var(--frankie-muted-foreground); }
358
+ .frankie-root .frankie-context-status { display:flex; align-items:center; gap:6px; font-size:12px; color:var(--frankie-muted-foreground); }
359
+ .frankie-root .frankie-context-status i { width:7px; height:7px; border-radius:50%; background:currentColor; }
360
+ .frankie-root .frankie-context-status[data-active="true"] { color:#00976b; }
361
+ .frankie-root .frankie-context-detail { position:absolute; z-index:10; top:calc(100% + 10px); left:50%; transform:translateX(-50%); width:340px; max-width:calc(100vw - 32px); padding:18px; border:1px solid var(--frankie-border); border-radius:16px; background:var(--frankie-background); box-shadow:0 12px 32px #00000016; }
362
+ .frankie-root .frankie-context-detail p { margin:10px 0 0; font-size:12px; line-height:1.6; color:var(--frankie-muted-foreground); }
363
+ .frankie-root .frankie-context-detail ul { list-style:none; margin:12px 0; padding:0; max-height:220px; overflow:auto; }
364
+ .frankie-root .frankie-context-detail li { display:flex; justify-content:space-between; gap:12px; padding:8px 0; border-bottom:1px solid var(--frankie-border); overflow-wrap:anywhere; }
365
+ .frankie-root .frankie-context-detail small { flex:none; color:var(--frankie-muted-foreground); }
366
+ .frankie-root .frankie-sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
367
+ .frankie-root.frankie-chat-panel-fill { --frankie-canvas:color-mix(in srgb,var(--frankie-background) 60%,var(--frankie-muted)); background:var(--frankie-canvas); }
368
+ .frankie-root.frankie-chat-panel-fill .frankie-agent-context-wrap { padding:44px 16px 26px; }
369
+ .frankie-root.frankie-chat-panel-fill .frankie-transcript { padding:14px max(24px,calc((100% - 960px)/2)) 24px; }
370
+ .frankie-root.frankie-chat-panel-fill .frankie-message-list { gap:38px; }
371
+ .frankie-root.frankie-chat-panel-fill .frankie-user-bubble { border-radius:24px; padding:12px 20px; background:var(--frankie-muted); color:var(--frankie-foreground); font-size:15px; white-space:pre-wrap; overflow-wrap:anywhere; }
372
+ .frankie-root.frankie-chat-panel-fill .frankie-assistant-content { max-width:100%; flex:1; font-size:15px; line-height:1.7; }
373
+ .frankie-root.frankie-chat-panel-fill .frankie-assistant-message { gap:14px; }
374
+ .frankie-root .frankie-thinking { color:var(--frankie-muted-foreground); gap:5px; }
375
+ .frankie-root .frankie-thinking-label { margin-right:6px; font-size:14px; }
376
+ .frankie-root.frankie-chat-panel-fill .frankie-composer-footer { border:0; padding:16px max(24px,calc((100% - 1000px)/2)) 24px; background:transparent; flex:none; }
377
+ .frankie-root.frankie-chat-panel-fill .frankie-composer { padding:14px; border:1px solid var(--frankie-border); border-radius:28px; background:var(--frankie-background); box-shadow:0 2px 4px #0000000a; }
378
+ .frankie-root.frankie-chat-panel-fill .frankie-composer:focus-within { border-color:color-mix(in srgb,var(--frankie-foreground) 32%,var(--frankie-border)); box-shadow:0 0 0 2px color-mix(in srgb,var(--frankie-primary) 8%,transparent); }
379
+ .frankie-root.frankie-chat-panel-fill .frankie-composer-controls { display:flex; flex-wrap:wrap; align-items:center; gap:8px; }
380
+ .frankie-root.frankie-chat-panel-fill .frankie-composer textarea { order:-1; flex:0 0 100%; width:100%; min-height:32px; max-height:140px; margin-bottom:4px; padding:2px 6px; border:0; border-radius:0; resize:none; background:transparent; color:var(--frankie-foreground); font-size:16px; box-shadow:none; outline:none; }
381
+ .frankie-root.frankie-chat-panel-fill .frankie-composer button[aria-label="Send"],.frankie-root.frankie-chat-panel-fill .frankie-composer button[aria-label="Stop"] { margin-left:auto; width:42px; height:42px; border-radius:50%; background:var(--frankie-primary); color:var(--frankie-primary-foreground); border:0; }
382
+ .frankie-root.frankie-chat-panel-fill .frankie-composer button[aria-label="Attach files"] { width:42px; height:42px; border-radius:50%; }
383
+ .frankie-root .frankie-history-desktop { width:290px; background:var(--frankie-background); padding:20px 12px; }
384
+ .frankie-root .frankie-new-chat { justify-content:flex-start; border:0; background:transparent; padding:8px 12px; font-size:15px; }
385
+ .frankie-root .frankie-history-heading { font-size:12px; font-weight:400; color:var(--frankie-muted-foreground); padding:0 12px; }
386
+ .frankie-root .frankie-history-list button { min-height:44px; padding:10px 12px; }
387
+ .frankie-root .frankie-history-list button span { font-size:14px; }
388
+ .frankie-root .frankie-history-list button[aria-current],.frankie-root .frankie-history-list button:hover { border-color:transparent; background:var(--frankie-muted); }
389
+ .frankie-root .frankie-history-list small { display:none; }
390
+ .frankie-root .frankie-page-header { min-height:56px; height:56px; background:var(--frankie-background); }
391
+ .frankie-root .frankie-page-header > div { min-width:0; }
392
+ .frankie-root .frankie-thread-title { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:55vw; font-weight:500; }
393
+ .frankie-root .frankie-page-header button { border-color:transparent; background:transparent; }
394
+ .frankie-root .frankie-page-canvas > .frankie-inline { max-width:none; }
395
+ @media(max-width:760px) {
396
+ .frankie-root.frankie-chat-panel-fill .frankie-agent-context-wrap { padding:20px 8px 14px; }
397
+ .frankie-root.frankie-chat-panel-fill .frankie-transcript { padding:12px 16px 16px; }
398
+ .frankie-root.frankie-chat-panel-fill .frankie-composer-footer { padding:10px 12px calc(12px + env(safe-area-inset-bottom)); }
399
+ .frankie-root .frankie-agent-context > summary { gap:6px; padding:7px 9px; font-size:12px; }
400
+ .frankie-root .frankie-context-count { padding:2px 7px; font-size:11px; }
401
+ .frankie-root .frankie-context-status { font-size:11px; gap:4px; }
402
+ .frankie-root .frankie-agent-context > summary > svg { width:14px; }
403
+ .frankie-root .frankie-thread-title { max-width:42vw; }
404
+ }
405
+ .frankie-root .frankie-agent-context > summary > svg:first-child { box-sizing:content-box; padding:4px; border-radius:50%; background:var(--frankie-muted); }
406
+ .frankie-root.frankie-chat-panel-fill .frankie-agent-context > summary { font-size:14px; }
407
+
408
+ .frankie-root .frankie-agent-context > summary > span:first-of-type { border-right:1px solid var(--frankie-border); padding-right:10px; }
409
+ @media(max-width:760px) { .frankie-root .frankie-page-header button > span { display:none; } }
@@ -1,4 +1,4 @@
1
- import { render, screen, waitFor } from "@testing-library/react";
1
+ import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
2
2
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import type { FrankieServerEvent, PublicWidgetConfig } from "@frankieai/core";
4
4
  import { FrankieProvider } from "./hooks/FrankieProvider.jsx";
@@ -7,6 +7,7 @@ import { Markdown } from "./components/chat/Markdown.js";
7
7
  import { FrankieClient, readEventStream } from "./lib/client.js";
8
8
  import { applyEvent, newAssistantMessage } from "./lib/messages.js";
9
9
  import { themeToCssVars } from "./lib/theme.js";
10
+ import { useFrankieChat } from "./hooks/useFrankieChat.js";
10
11
 
11
12
  const config: PublicWidgetConfig = {
12
13
  tenantId: "acme",
@@ -97,6 +98,24 @@ describe("readEventStream", () => {
97
98
  });
98
99
 
99
100
  describe("FrankieClient", () => {
101
+ it("does not request signed-in history for an anonymous conversation", async () => {
102
+ const fetchImpl = vi.fn();
103
+ const client = new FrankieClient({ publishableKey: "pk_anonymous_fixture", conversationId: "new", fetchImpl, persist: false });
104
+ expect(await client.resumeConversation()).toBeUndefined();
105
+ expect(fetchImpl).not.toHaveBeenCalled();
106
+ });
107
+ it("keeps authenticated tokens out of storage and rejects a cached session after sign-out", async () => {
108
+ let token: string | undefined = "fixture-user-token";
109
+ const fetchImpl = vi.fn(async () => Response.json({ sessionId: "one", sessionToken: "fixture-bearer", expiresAt: new Date(Date.now() + 600_000).toISOString(), user: { id: "one", anonymous: false } }));
110
+ const client = new FrankieClient({ publishableKey: "pk_auth_fixture", getUserToken: () => token, fetchImpl });
111
+ await client.ensureSession();
112
+ expect(client.persist).toBe(false);
113
+ expect(window.localStorage.getItem("frankie.pk_auth_fixture.session")).toBeNull();
114
+ token = undefined;
115
+ await expect(client.ensureSession()).rejects.toThrow("sign in again");
116
+ expect(client.sessionId).toBeUndefined();
117
+ expect(fetchImpl).toHaveBeenCalledTimes(1);
118
+ });
100
119
  // The client persists its session to localStorage; isolate tests that reuse
101
120
  // the same publishable-key namespace.
102
121
  beforeEach(() => {
@@ -192,6 +211,50 @@ describe("themeToCssVars", () => {
192
211
  });
193
212
 
194
213
  describe("ChatPanel", () => {
214
+ it("shares prompt controls, transcript and loaded skills across provider consumers", async () => {
215
+ cleanup();
216
+ const fetchImpl = vi.fn(async (url: string | URL | Request) => String(url).endsWith("/v1/session") ? Response.json({ sessionId: "shared", sessionToken: "fixture", expiresAt: new Date(Date.now() + 600_000).toISOString(), user: { id: "one", anonymous: true } }) : new Response(sseStream([{ type: "skills_loaded", skills: [{ id: "review", name: "Review items", revision: 2 }] }, { type: "text_delta", text: "Shared response" }, { type: "turn_end", stopReason: "end_turn" }])));
217
+ function Prompt() { const chat = useFrankieChat(); return <button disabled={chat.restoring} onClick={() => void chat.send("Shared prompt")}>External prompt</button>; }
218
+ render(<FrankieProvider publishableKey="pk_shared_fixture" config={config} fetchImpl={fetchImpl} persist={false}><Prompt /><ChatPanel /></FrankieProvider>);
219
+ await waitFor(() => expect((screen.getByRole("button", { name: "External prompt" }) as HTMLButtonElement).disabled).toBe(false));
220
+ fireEvent.click(screen.getByRole("button", { name: "External prompt" }));
221
+ expect(await screen.findByText("Shared response")).toBeTruthy();
222
+ expect(screen.getByText("1 skill loaded")).toBeTruthy();
223
+ expect(fetchImpl.mock.calls.filter(([url]) => String(url).endsWith("/v1/chat"))).toHaveLength(1);
224
+ fireEvent.click(screen.getByRole("button", { name: "Start a new conversation" }));
225
+ await waitFor(() => expect(screen.queryByText("Shared response")).toBeNull());
226
+ expect(screen.queryByText("1 skill loaded")).toBeNull();
227
+ cleanup();
228
+ });
229
+ it("refreshes the context count and revisions from each server turn", async () => {
230
+ cleanup();
231
+ let turn = 0;
232
+ const fetchImpl = vi.fn(async (url: string | URL | Request) => {
233
+ if (String(url).endsWith("/v1/session")) return Response.json({ sessionId: "context", sessionToken: "fixture", expiresAt: new Date(Date.now() + 600_000).toISOString(), user: { id: "one", anonymous: true } });
234
+ turn++;
235
+ const skills = turn === 1 ? [{ id: "one", name: "Reservations", revision: 2 }, { id: "two", name: "Locations", revision: 1 }, { id: "three", name: "Service", revision: 3 }] : [];
236
+ return new Response(sseStream([{ type: "skills_loaded", skills }, { type: "text_delta", text: `Response ${turn}` }, { type: "turn_end", stopReason: "end_turn" }]));
237
+ });
238
+ render(<FrankieProvider publishableKey="pk_context_fixture" config={config} fetchImpl={fetchImpl} persist={false}><ChatPanel fill /></FrankieProvider>);
239
+ expect(screen.getByText("Not checked")).toBeTruthy();
240
+ await waitFor(() => expect((screen.getByLabelText("Message") as HTMLTextAreaElement).disabled).toBe(false));
241
+ fireEvent.change(screen.getByLabelText("Message"), { target: { value: "Review service" } });
242
+ fireEvent.click(screen.getByRole("button", { name: "Send" }));
243
+ await screen.findByText("Response 1");
244
+ expect(screen.getByText("3 skills")).toBeTruthy();
245
+ expect(screen.getByText("Active")).toBeTruthy();
246
+ fireEvent.click(screen.getByLabelText("Agent context"));
247
+ expect(screen.getByText("Reservations")).toBeTruthy();
248
+ expect(screen.getByText("v3")).toBeTruthy();
249
+ await waitFor(() => expect(screen.getByRole("button", { name: "Send" })).toBeTruthy());
250
+ fireEvent.change(screen.getByLabelText("Message"), { target: { value: "Try again" } });
251
+ fireEvent.click(screen.getByRole("button", { name: "Send" }));
252
+ await screen.findByText("Response 2");
253
+ expect(screen.getByText("0 skills")).toBeTruthy();
254
+ expect(screen.queryByText("Active")).toBeNull();
255
+ expect(screen.queryByText("Reservations")).toBeNull();
256
+ cleanup();
257
+ });
195
258
  it("shows the greeting and suggested prompts before the first message", async () => {
196
259
  render(
197
260
  <FrankieProvider baseUrl="https://agent.acme.com" publishableKey="pk_1" config={config}>