@frankieai/react 0.2.8 → 0.2.10

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/dist/index.js CHANGED
@@ -3,6 +3,31 @@
3
3
  // src/hooks/FrankieProvider.tsx
4
4
  import { createContext as createContext2, useContext as useContext2, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2 } from "react";
5
5
 
6
+ // src/lib/identity.ts
7
+ function sessionIdentity(token) {
8
+ if (!token) return void 0;
9
+ return jwtSubject(token) ?? token;
10
+ }
11
+ function jwtSubject(token) {
12
+ const parts = token.split(".");
13
+ if (parts.length !== 3 || !parts[1]) return void 0;
14
+ try {
15
+ const payload = JSON.parse(base64UrlDecode(parts[1]));
16
+ return typeof payload.sub === "string" && payload.sub ? payload.sub : void 0;
17
+ } catch {
18
+ return void 0;
19
+ }
20
+ }
21
+ function base64UrlDecode(segment) {
22
+ const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
23
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
24
+ if (typeof atob === "function") {
25
+ const bytes = atob(padded);
26
+ return decodeURIComponent(Array.from(bytes, (c) => "%" + c.charCodeAt(0).toString(16).padStart(2, "0")).join(""));
27
+ }
28
+ return Buffer.from(padded, "base64").toString("utf8");
29
+ }
30
+
6
31
  // src/lib/client.ts
7
32
  var FRANKIE_CLOUD_URL = "https://frankie-control-665333762719.us-central1.run.app";
8
33
  var FrankieApiError = class extends Error {
@@ -31,7 +56,8 @@ var FrankieClient = class {
31
56
  baseUrl;
32
57
  doFetch;
33
58
  session;
34
- sessionUserToken;
59
+ /** Identity (JWT `sub`, else the raw token) the cached session was issued for. */
60
+ sessionUserKey;
35
61
  get sessionId() {
36
62
  return this.session?.sessionId;
37
63
  }
@@ -73,7 +99,18 @@ var FrankieClient = class {
73
99
  return await this.unwrap(response);
74
100
  }
75
101
  async ensureSession(context) {
76
- const userToken = await this.options.getUserToken?.();
102
+ let userToken;
103
+ try {
104
+ userToken = await this.options.getUserToken?.();
105
+ } catch (error) {
106
+ this.resetSession();
107
+ const reason = error instanceof Error && error.message.trim() ? error.message.trim() : void 0;
108
+ throw new FrankieApiError(
109
+ reason ?? "Your signed-in Frankie session could not be verified. Refresh the page and sign in again.",
110
+ 401,
111
+ "user_token_unavailable"
112
+ );
113
+ }
77
114
  if (this.options.getUserToken && !userToken) {
78
115
  this.resetSession();
79
116
  throw new FrankieApiError(
@@ -82,7 +119,8 @@ var FrankieClient = class {
82
119
  "user_token_unavailable"
83
120
  );
84
121
  }
85
- if (userToken !== this.sessionUserToken) this.resetSession();
122
+ const userKey = sessionIdentity(userToken);
123
+ if (userKey !== this.sessionUserKey) this.resetSession();
86
124
  if (this.session && new Date(this.session.expiresAt).getTime() > Date.now() + 3e4) return this.session;
87
125
  const response = await this.doFetch(`${this.baseUrl}/v1/session`, {
88
126
  method: "POST",
@@ -94,7 +132,7 @@ var FrankieClient = class {
94
132
  })
95
133
  });
96
134
  this.session = await this.unwrap(response);
97
- this.sessionUserToken = userToken;
135
+ this.sessionUserKey = userKey;
98
136
  this.writeStored();
99
137
  return this.session;
100
138
  }
@@ -116,7 +154,7 @@ var FrankieClient = class {
116
154
  if (response.status === 404) return void 0;
117
155
  const body = await this.unwrap(response);
118
156
  this.session = body.session;
119
- this.sessionUserToken = userToken;
157
+ this.sessionUserKey = sessionIdentity(userToken);
120
158
  this.writeStored();
121
159
  return { messages: Array.isArray(body.messages) ? body.messages : [], title: body.title ?? "New chat" };
122
160
  }
@@ -175,9 +213,59 @@ var FrankieClient = class {
175
213
  return;
176
214
  }
177
215
  for await (const event of readEventStream(response.body)) {
216
+ if (event.type === "tool_call_request") {
217
+ void this.runClientTool(session, event.callId, event.request);
218
+ continue;
219
+ }
178
220
  onEvent(event);
179
221
  }
180
222
  }
223
+ /**
224
+ * Execute one host API call in the browser and report the outcome back to the
225
+ * server. Never throws: a transport failure or a missing `api` client becomes
226
+ * a result the model can see and explain, exactly like a non-2xx status.
227
+ */
228
+ async runClientTool(session, callId, request) {
229
+ const result = await this.callHostApi(request);
230
+ await this.postToolResult(session, callId, result).catch(() => void 0);
231
+ }
232
+ async callHostApi(request) {
233
+ const api = this.options.api;
234
+ if (!api) {
235
+ return {
236
+ status: 0,
237
+ error: "This assistant tried to call your API from the browser, but no host API client is configured in the Frankie SDK (the `api` option)."
238
+ };
239
+ }
240
+ try {
241
+ const extra = api.headers ? await api.headers() : {};
242
+ const response = await this.doFetch(joinHostUrl(api.baseUrl, request.path), {
243
+ method: request.method,
244
+ credentials: api.credentials ?? "same-origin",
245
+ headers: {
246
+ accept: "application/json, text/plain;q=0.9, */*;q=0.8",
247
+ ...request.body !== void 0 ? { "content-type": "application/json" } : {},
248
+ ...extra
249
+ },
250
+ ...request.body !== void 0 ? { body: JSON.stringify(request.body) } : {}
251
+ });
252
+ const body = await response.text().catch(() => "");
253
+ return { status: response.status, body };
254
+ } catch (error) {
255
+ return { status: 0, error: error instanceof Error ? error.message : String(error) };
256
+ }
257
+ }
258
+ async postToolResult(session, callId, result) {
259
+ await this.doFetch(`${this.baseUrl}/v1/chat/tool-result`, {
260
+ method: "POST",
261
+ headers: {
262
+ "content-type": "application/json",
263
+ "x-frankie-key": this.options.publishableKey,
264
+ authorization: `Bearer ${session.sessionToken}`
265
+ },
266
+ body: JSON.stringify({ callId, result })
267
+ });
268
+ }
181
269
  async unwrap(response) {
182
270
  if (response.ok) return response.json();
183
271
  let code = "internal";
@@ -191,6 +279,12 @@ var FrankieClient = class {
191
279
  throw new FrankieApiError(message, response.status, code);
192
280
  }
193
281
  };
282
+ function joinHostUrl(baseUrl, path) {
283
+ if (/^[a-z][a-z0-9+.-]*:/i.test(path) || path.startsWith("//")) {
284
+ throw new Error("A client tool path must be relative to the configured host API base URL.");
285
+ }
286
+ return `${baseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
287
+ }
194
288
  async function* readEventStream(body) {
195
289
  const reader = body.getReader();
196
290
  const decoder = new TextDecoder();
@@ -223,6 +317,68 @@ function parseFrame(frame) {
223
317
  }
224
318
  }
225
319
 
320
+ // src/lib/theme-inherit.ts
321
+ function parseRgb(value) {
322
+ const match = value.match(/rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)/i);
323
+ if (!match) return null;
324
+ const alpha = match[4] === void 0 ? 1 : match[4].endsWith("%") ? Number(match[4].slice(0, -1)) / 100 : Number(match[4]);
325
+ return [Number(match[1]), Number(match[2]), Number(match[3]), alpha];
326
+ }
327
+ function toCss([r, g, b, a]) {
328
+ return a >= 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${a})`;
329
+ }
330
+ function sameColor(a, b) {
331
+ return Math.abs(a[0] - b[0]) < 12 && Math.abs(a[1] - b[1]) < 12 && Math.abs(a[2] - b[2]) < 12;
332
+ }
333
+ function luminance([r, g, b]) {
334
+ const channel = (c) => {
335
+ const v = c / 255;
336
+ return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
337
+ };
338
+ return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
339
+ }
340
+ function contrastRatio(a, b) {
341
+ const la = luminance(a), lb = luminance(b);
342
+ return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
343
+ }
344
+ var ACCENT_SELECTORS = ["[data-frankie-accent]", "button[type=submit]", "a[href]", "button", "[class*=primary]", "[class*=btn]"];
345
+ function inferHostTheme() {
346
+ if (typeof document === "undefined" || typeof getComputedStyle !== "function") return null;
347
+ try {
348
+ const bodyStyle = getComputedStyle(document.body);
349
+ const background = parseRgb(bodyStyle.backgroundColor);
350
+ const foreground = parseRgb(bodyStyle.color);
351
+ if (!background || !foreground) return null;
352
+ if (background[3] < 0.5) return null;
353
+ if (contrastRatio(background, foreground) < 2) return null;
354
+ let accent = null;
355
+ for (const selector of ACCENT_SELECTORS) {
356
+ const element = document.querySelector(selector);
357
+ if (!element) continue;
358
+ const style = getComputedStyle(element);
359
+ const surface = parseRgb(style.backgroundColor);
360
+ if (surface && surface[3] > 0.5 && !sameColor(surface, background)) {
361
+ accent = surface;
362
+ break;
363
+ }
364
+ const text = parseRgb(style.color);
365
+ if (text && text[3] > 0.5 && !sameColor(text, foreground)) {
366
+ accent = text;
367
+ break;
368
+ }
369
+ }
370
+ if (!accent) return null;
371
+ return {
372
+ background: toCss(background),
373
+ foreground: toCss(foreground),
374
+ primary: toCss(accent),
375
+ primaryForeground: luminance(accent) > 0.5 ? "#0a0a0a" : "#fafafa"
376
+ };
377
+ } catch {
378
+ return null;
379
+ }
380
+ }
381
+
226
382
  // src/hooks/useFrankieChat.ts
227
383
  import { createContext, useContext, useCallback, useEffect, useRef, useState } from "react";
228
384
 
@@ -401,6 +557,41 @@ function useFrankieChatState() {
401
557
  setMessages(next);
402
558
  const abort = new AbortController();
403
559
  abortRef.current = abort;
560
+ let bufferedText = "";
561
+ let pendingThinking = "";
562
+ let frame = null;
563
+ const raf = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (cb) => setTimeout(() => cb(performance.now()), 16);
564
+ const cancelRaf = typeof cancelAnimationFrame === "function" ? cancelAnimationFrame : clearTimeout;
565
+ const revealTick = () => {
566
+ frame = null;
567
+ let addText = "";
568
+ if (bufferedText) {
569
+ const step = Math.max(3, Math.ceil(bufferedText.length / 6));
570
+ addText = bufferedText.slice(0, step);
571
+ bufferedText = bufferedText.slice(step);
572
+ }
573
+ const addThinking = pendingThinking;
574
+ pendingThinking = "";
575
+ if (addText || addThinking) {
576
+ patchAssistant(assistantId, (message) => ({ ...message, text: message.text + addText, thinking: message.thinking + addThinking }));
577
+ }
578
+ if (bufferedText) scheduleReveal();
579
+ };
580
+ const scheduleReveal = () => {
581
+ if (frame === null) frame = raf(revealTick);
582
+ };
583
+ const flushDeltas = () => {
584
+ if (frame !== null) {
585
+ cancelRaf(frame);
586
+ frame = null;
587
+ }
588
+ if (!bufferedText && !pendingThinking) return;
589
+ const addText = bufferedText;
590
+ const addThinking = pendingThinking;
591
+ bufferedText = "";
592
+ pendingThinking = "";
593
+ patchAssistant(assistantId, (message) => ({ ...message, text: message.text + addText, thinking: message.thinking + addThinking }));
594
+ };
404
595
  try {
405
596
  let receivedText = false;
406
597
  let receivedToolActivity = false;
@@ -422,11 +613,23 @@ function useFrankieChatState() {
422
613
  return;
423
614
  }
424
615
  if (event.type === "ping" || event.type === "session") return;
425
- if (event.type === "text_delta") receivedText = true;
616
+ if (event.type === "text_delta") {
617
+ receivedText = true;
618
+ bufferedText += event.text;
619
+ scheduleReveal();
620
+ return;
621
+ }
622
+ if (event.type === "thinking_delta") {
623
+ pendingThinking += event.text;
624
+ scheduleReveal();
625
+ return;
626
+ }
426
627
  if (event.type === "tool_start" || event.type === "tool_end") receivedToolActivity = true;
628
+ flushDeltas();
427
629
  patchAssistant(assistantId, (message) => applyEvent(message, event));
428
630
  }
429
631
  );
632
+ flushDeltas();
430
633
  if (!receivedText) {
431
634
  patchAssistant(assistantId, (message) => ({
432
635
  ...message,
@@ -435,6 +638,7 @@ function useFrankieChatState() {
435
638
  }));
436
639
  }
437
640
  } catch (caught) {
641
+ flushDeltas();
438
642
  if (abort.signal.aborted) {
439
643
  patchAssistant(assistantId, (message) => ({
440
644
  ...message,
@@ -447,6 +651,7 @@ function useFrankieChatState() {
447
651
  patchAssistant(assistantId, (current) => ({ ...current, status: "error", error: message }));
448
652
  }
449
653
  } finally {
654
+ flushDeltas();
450
655
  patchAssistant(
451
656
  assistantId,
452
657
  (message) => message.status === "streaming" ? { ...message, status: "done" } : message
@@ -494,6 +699,7 @@ function FrankieProvider({
494
699
  context,
495
700
  theme,
496
701
  config: providedConfig,
702
+ injectStyles = true,
497
703
  ...clientOptions
498
704
  }) {
499
705
  const tokenCallback = useRef2(clientOptions.getUserToken);
@@ -501,7 +707,7 @@ function FrankieProvider({
501
707
  const client = useMemo(
502
708
  () => new FrankieClient({ ...clientOptions, ...clientOptions.getUserToken ? { getUserToken: () => tokenCallback.current?.() } : {} }),
503
709
  // eslint-disable-next-line react-hooks/exhaustive-deps
504
- [clientOptions.baseUrl, clientOptions.publishableKey, clientOptions.storageNamespace, clientOptions.conversationId, clientOptions.persist, clientOptions.fetchImpl, Boolean(clientOptions.getUserToken)]
710
+ [clientOptions.baseUrl, clientOptions.publishableKey, clientOptions.storageNamespace, clientOptions.conversationId, clientOptions.persist, clientOptions.fetchImpl, clientOptions.api?.baseUrl, clientOptions.api?.credentials, Boolean(clientOptions.getUserToken)]
505
711
  );
506
712
  const [config, setConfig] = useState2(providedConfig);
507
713
  const [configError, setConfigError] = useState2(void 0);
@@ -528,9 +734,10 @@ function FrankieProvider({
528
734
  ...configError ? { configError } : {},
529
735
  loading,
530
736
  ...context ? { context } : {},
531
- ...theme ? { theme } : {}
737
+ ...theme ? { theme } : {},
738
+ injectStyles
532
739
  }),
533
- [client, config, configError, loading, context, theme]
740
+ [client, config, configError, loading, context, theme, injectStyles]
534
741
  );
535
742
  return /* @__PURE__ */ jsx(FrankieContext.Provider, { value, children: /* @__PURE__ */ jsx(ConversationState, { children }, client.namespace + ":" + (client.conversationId ?? "")) });
536
743
  }
@@ -545,12 +752,82 @@ function useFrankie() {
545
752
  }
546
753
  function useFrankieTheme() {
547
754
  const { config, theme } = useFrankie();
548
- return useMemo(() => ({ ...config?.theme, ...theme }), [config?.theme, theme]);
755
+ const merged = useMemo(() => ({ ...config?.theme, ...theme }), [config?.theme, theme]);
756
+ const inheriting = merged?.source === "inherit";
757
+ const [inferred, setInferred] = useState2(null);
758
+ useEffect2(() => {
759
+ setInferred(inheriting ? inferHostTheme() : null);
760
+ }, [inheriting]);
761
+ return useMemo(() => {
762
+ if (!inheriting || !inferred) return merged;
763
+ return { ...merged, ...inferred };
764
+ }, [merged, inheriting, inferred]);
549
765
  }
550
766
 
551
767
  // src/components/chat/FrankieWidget.tsx
552
768
  import { MessageCircle, X as X4 } from "lucide-react";
553
- import { useEffect as useEffect5, useState as useState6 } from "react";
769
+ import { useEffect as useEffect6, useRef as useRef5, useState as useState7 } from "react";
770
+
771
+ // src/lib/styles-ready.ts
772
+ import { useEffect as useEffect3, useLayoutEffect, useState as useState3 } from "react";
773
+
774
+ // src/generated/css-content.ts
775
+ var FRANKIE_CSS_CONTENT = `/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
776
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-ease:initial}}}@layer theme{:root,:host{--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-600:oklch(59.6% .145 163.225);--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-medium:500;--font-weight-semibold:600;--leading-tight:1.25;--radius-md:.375rem;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1)}}@layer components{.frankie-page-shell{background:var(--frankie-background);height:100%;min-height:0;color:var(--frankie-foreground);flex:1;display:flex}.frankie-page-main{flex-direction:column;flex:1;min-width:0;min-height:0;display:flex}.frankie-history-desktop{border-right:1px solid var(--frankie-border);background:var(--frankie-secondary);flex-direction:column;flex-shrink:0;gap:16px;width:260px;padding:20px 14px;transition:width .16s,padding .16s;display:flex;overflow:auto}.frankie-history-toggle{border:1px solid var(--frankie-border);background:var(--frankie-background);width:32px;height:32px;color:var(--frankie-muted-foreground);cursor:pointer;border-radius:8px;flex-shrink:0;justify-content:center;align-self:flex-end;align-items:center;display:flex}.frankie-history-toggle:hover{color:var(--frankie-foreground)}.frankie-history-collapsed{width:56px;padding:20px 8px;overflow:visible}.frankie-history-collapsed .frankie-history-toggle{align-self:center}.frankie-history-heading{justify-content:space-between;align-items:center;font-size:14px;display:flex}.frankie-new-chat{border:1px solid var(--frankie-border);background:var(--frankie-background);cursor:pointer;border-radius:10px;justify-content:center;align-items:center;gap:8px;min-height:44px;font-weight:600;display:flex}.frankie-history-search>span{color:var(--frankie-muted-foreground);margin-bottom:6px;font-size:12px;display:block}.frankie-history-search input{border:1px solid var(--frankie-border);background:var(--frankie-background);border-radius:8px;width:100%;min-height:40px;padding:8px 10px}.frankie-history-list{flex-direction:column;flex:1;gap:4px;display:flex}.frankie-history-list button{text-align:left;cursor:pointer;background:0 0;border:1px solid #0000;border-radius:8px;flex-direction:column;align-items:flex-start;gap:5px;min-height:54px;padding:10px;display:flex}.frankie-history-list button span{text-overflow:ellipsis;white-space:nowrap;width:100%;font-size:13px;overflow:hidden}.frankie-history-list small{color:var(--frankie-muted-foreground);font-size:11px}.frankie-history-list button:hover,.frankie-history-list button[aria-current]{background:var(--frankie-background);border-color:var(--frankie-border)}.frankie-history-desktop>p,.frankie-history-dialog p{color:var(--frankie-muted-foreground);font-size:13px}.frankie-header-caption{color:var(--frankie-muted-foreground);font-size:12px}.frankie-page-header button{cursor:pointer;justify-content:center;align-items:center;gap:6px;min-width:40px;min-height:40px;display:inline-flex}.frankie-chat-settings{position:relative}.frankie-chat-settings summary{cursor:pointer;border-radius:8px;place-items:center;width:40px;height:40px;list-style:none;display:grid}.frankie-chat-settings summary::-webkit-details-marker{display:none}.frankie-chat-settings>div{z-index:5;border:1px solid var(--frankie-border);background:var(--frankie-background);border-radius:12px;gap:12px;width:min(280px,80vw);padding:16px;font-size:13px;display:grid;position:absolute;top:46px;right:0;box-shadow:0 10px 30px #0002}.frankie-chat-settings p{color:var(--frankie-muted-foreground);margin:0}.frankie-loaded-skills{width:100%;max-width:760px;color:var(--frankie-muted-foreground);font-size:12px}.frankie-loaded-skills summary{cursor:pointer;padding:6px 0}.frankie-loaded-skills ul{margin:0;padding:4px 0;list-style:none}.frankie-loaded-skills li{gap:8px;padding:4px 0;display:flex}.frankie-loaded-skills small{opacity:.7}.frankie-history-dialog{background:var(--frankie-secondary);width:min(320px,86vw);max-width:none;height:100dvh;max-height:none;color:var(--frankie-foreground);border:0;margin:0;padding:0}.frankie-history-dialog::backdrop{background:#0008}.frankie-history-dialog>aside{height:100%;padding:calc(20px + env(safe-area-inset-top)) 16px calc(20px + env(safe-area-inset-bottom));flex-direction:column;gap:16px;display:flex;overflow:auto}.frankie-mobile-only{display:none!important}.frankie-widget-panel{max-height:calc(100dvh - 100px)}.frankie-root button:focus-visible,.frankie-root summary:focus-visible,.frankie-root input:focus-visible{outline:2px solid var(--frankie-primary);outline-offset:3px}.frankie-root button:disabled{opacity:.5;cursor:not-allowed}@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{background:0 0;border:0;justify-content:center;align-items:center;min-width:44px;min-height:44px}.frankie-page-prompts{gap:8px}.frankie-widget-panel{width:calc(100vw - 32px)}}.frankie-root{--frankie-styles-ready:1;--frankie-background:#fff;--frankie-foreground:#0a0a0a;--frankie-primary:#171717;--frankie-primary-foreground:#fafafa;--frankie-secondary:#f5f5f5;--frankie-secondary-foreground:#171717;--frankie-muted:#f5f5f5;--frankie-muted-foreground:#737373;--frankie-border:#e5e5e5;--frankie-radius:.625rem;--frankie-font:ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;font-family:var(--frankie-font);color:var(--frankie-foreground);-webkit-font-smoothing:antialiased;line-height:1.5}.frankie-root.dark{--frankie-background:#0a0a0a;--frankie-foreground:#fafafa;--frankie-primary:#fafafa;--frankie-primary-foreground:#171717;--frankie-secondary:#262626;--frankie-secondary-foreground:#fafafa;--frankie-muted:#262626;--frankie-muted-foreground:#a3a3a3;--frankie-border:#262626}.frankie-root,.frankie-root *,.frankie-root :before,.frankie-root :after{box-sizing:border-box}.frankie-root button,.frankie-root textarea,.frankie-root input{font:inherit;color:inherit;margin:0}.frankie-root textarea{background-color:var(--frankie-background)}.frankie-focus:focus-visible{outline:2px solid var(--frankie-primary);outline-offset:2px}.frankie-scroll{scrollbar-width:thin;scrollbar-color:var(--frankie-border) transparent}.frankie-prose>*+*{margin-top:.6rem}.frankie-prose p{white-space:pre-wrap;overflow-wrap:anywhere;margin:0;line-height:1.6}.frankie-prose>.frankie-h{letter-spacing:-.01em;margin-top:1.15rem;line-height:1.3}.frankie-prose>.frankie-h:first-child{margin-top:0}.frankie-prose h3.frankie-h{font-size:1.06em;font-weight:700}.frankie-prose h4.frankie-h{font-size:1em;font-weight:600}.frankie-prose h5.frankie-h{color:var(--frankie-muted-foreground);font-size:.92em;font-weight:600}.frankie-prose strong{font-weight:600}.frankie-prose ul,.frankie-prose ol{flex-direction:column;gap:.35rem;margin:0;padding-left:1.4rem;display:flex}.frankie-prose li{padding-left:.15rem;line-height:1.55}.frankie-prose li::marker{color:var(--frankie-muted-foreground)}.frankie-prose ul{list-style:outside}.frankie-prose ol{list-style:decimal}.frankie-prose a{color:var(--frankie-primary);text-underline-offset:2px;text-decoration:underline}.frankie-prose code{background-color:var(--frankie-muted);border-radius:.25rem;padding:.1em .3em;font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,monospace;font-size:.85em}.frankie-prose pre{border-radius:var(--frankie-radius);background-color:var(--frankie-muted);margin:0;padding:.625rem .75rem;overflow-x:auto}.frankie-prose pre code{background:0 0;padding:0}.frankie-chat-page{background:radial-gradient(60rem 24rem at 50% -12rem, var(--frankie-primary), transparent 65%), var(--frankie-background);flex-direction:column;min-width:0;display:flex;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.frankie-chat-page{background:radial-gradient(60rem 24rem at 50% -12rem, color-mix(in srgb, var(--frankie-primary) 8%, transparent), transparent 65%), var(--frankie-background)}}.frankie-chat-page{color:var(--frankie-foreground)}.frankie-page-header{border-bottom:1px solid var(--frankie-border);background:var(--frankie-background);flex:0 0 56px;justify-content:space-between;align-items:center;gap:16px;min-height:56px;padding:0 24px;display:flex}@supports (color:color-mix(in lab, red, red)){.frankie-page-header{background:color-mix(in srgb, var(--frankie-background) 92%, transparent)}}.frankie-page-header{-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px)}.frankie-page-header>div{align-items:center;gap:10px;display:flex}.frankie-page-header strong{font-size:.925rem;font-weight:650}.frankie-page-status{background:var(--frankie-primary);width:10px;height:10px;box-shadow:0 0 0 4px var(--frankie-primary);border-radius:999px}@supports (color:color-mix(in lab, red, red)){.frankie-page-status{box-shadow:0 0 0 4px color-mix(in srgb, var(--frankie-primary) 16%, transparent)}}.frankie-page-header button{border:1px solid var(--frankie-border);border-radius:calc(var(--frankie-radius) * .9);background:var(--frankie-secondary);cursor:pointer;min-height:36px;padding:0 13px}.frankie-page-header button:hover{border-color:var(--frankie-primary)}.frankie-page-canvas{flex:1;min-width:0;min-height:0;display:flex}.frankie-page-canvas>.frankie-inline{width:100%;max-width:56rem;margin:0 auto}.frankie-page-canvas .frankie-scroll{padding:2rem clamp(1rem,4vw,4rem) 1.25rem}.frankie-page-canvas .frankie-root>.border-t{background:linear-gradient(180deg, transparent, var(--frankie-background) 30%);border-top:0;padding:.75rem clamp(1rem,4vw,4rem) 1.5rem}.frankie-page-empty{text-align:center;flex-direction:column;justify-content:center;align-items:center;gap:1rem;width:min(100%,38rem);min-height:100%;margin:auto;padding:clamp(2rem,7vh,4.5rem) 1rem 2rem;display:flex}.frankie-page-avatar{border-radius:calc(var(--frankie-radius) * 1.4);background:var(--frankie-primary);width:48px;height:48px;color:var(--frankie-primary-foreground);place-items:center;font-weight:750;display:grid}.frankie-page-kicker{color:var(--frankie-primary);letter-spacing:.1em;text-transform:uppercase;margin:0 0 .4rem;font-size:.7rem;font-weight:750}.frankie-page-empty h1{letter-spacing:-.045em;margin:0;font-size:clamp(1.65rem,4vw,2.15rem);line-height:1.08}.frankie-page-empty h1+p{max-width:30rem;color:var(--frankie-muted-foreground);margin:.6rem auto 0}.frankie-page-prompts{gap:.55rem;width:100%;margin-top:.4rem;display:grid}.frankie-page-prompts button{border:1px solid var(--frankie-border);border-radius:calc(var(--frankie-radius) * 1.25);background:var(--frankie-secondary);text-align:left;cursor:pointer;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:.65rem;min-height:60px;padding:.75rem .9rem;display:grid}.frankie-page-prompts button:hover{border-color:var(--frankie-primary)}@supports (color:color-mix(in lab, red, red)){.frankie-page-prompts button:hover{border-color:color-mix(in srgb, var(--frankie-primary) 58%, var(--frankie-border))}}.frankie-page-prompts button:hover{transform:translateY(-1px)}.frankie-page-prompts button>span{color:var(--frankie-muted-foreground);font-size:.68rem;font-weight:700}.frankie-page-prompts button>strong{font-size:.875rem}.frankie-page-prompts button>b{color:var(--frankie-primary);font-size:1rem}@media (max-width:640px){.frankie-page-header{flex-basis:52px;min-height:52px;padding:0 12px}.frankie-page-canvas .frankie-scroll{padding:1.25rem .75rem .5rem}.frankie-page-canvas .frankie-root>.border-t{padding:.65rem .75rem calc(.65rem + env(safe-area-inset-bottom))}}}@layer utilities;@scope(.frankie-root){.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.isolate{isolation:isolate}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mt-0{margin-top:0}.mt-0\\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.block{display:block}.contents{display:contents}.flex{display:flex}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.size-1{width:var(--spacing);height:var(--spacing)}.size-1\\.5{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5)}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-3\\.5{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-8{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}.size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.size-12{width:calc(var(--spacing) * 12);height:calc(var(--spacing) * 12)}.size-full{width:100%;height:100%}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-\\[32rem\\]{height:32rem}.h-full{height:100%}.h-px{height:1px}.min-h-7{min-height:calc(var(--spacing) * 7)}.min-h-\\[2\\.25rem\\]{min-height:2.25rem}.w-\\[22rem\\]{width:22rem}.w-full{width:100%}.max-w-\\[12rem\\]{max-width:12rem}.max-w-\\[14rem\\]{max-width:14rem}.max-w-\\[85\\%\\]{max-width:85%}.max-w-\\[calc\\(100vw-2rem\\)\\]{max-width:calc(100vw - 2rem)}.min-w-0{min-width:0}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.list-none{list-style-type:none}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:var(--spacing)}.gap-1\\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.overscroll-contain{overscroll-behavior:contain}.rounded{border-radius:.25rem}.rounded-\\[calc\\(var\\(--frankie-radius\\)\\+4px\\)\\]{border-radius:calc(var(--frankie-radius) + 4px)}.rounded-\\[calc\\(var\\(--frankie-radius\\)-2px\\)\\]{border-radius:calc(var(--frankie-radius) - 2px)}.rounded-\\[var\\(--frankie-radius\\)\\]{border-radius:var(--frankie-radius)}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.rounded-none{border-radius:0}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-border{border-color:var(--frankie-border)}.border-transparent{border-color:#0000}.bg-background{background-color:var(--frankie-background)}.bg-border{background-color:var(--frankie-border)}.bg-emerald-500{background-color:var(--color-emerald-500)}.bg-emerald-500\\/10{background-color:#00bb7f1a}@supports (color:color-mix(in lab, red, red)){.bg-emerald-500\\/10{background-color:color-mix(in oklab, var(--color-emerald-500) 10%, transparent)}}.bg-muted{background-color:var(--frankie-muted)}.bg-muted-foreground{background-color:var(--frankie-muted-foreground)}.bg-primary{background-color:var(--frankie-primary)}.bg-red-500{background-color:var(--color-red-500)}.bg-red-500\\/10{background-color:#fb2c361a}@supports (color:color-mix(in lab, red, red)){.bg-red-500\\/10{background-color:color-mix(in oklab, var(--color-red-500) 10%, transparent)}}.bg-secondary{background-color:var(--frankie-secondary)}.object-cover{object-fit:cover}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.px-1{padding-inline:var(--spacing)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0{padding-block:0}.py-0\\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.pl-2{padding-left:calc(var(--spacing) * 2)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\\[0\\.625rem\\]{font-size:.625rem}.text-\\[0\\.6875rem\\]{font-size:.6875rem}.text-\\[11px\\]{font-size:11px}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-nowrap{white-space:nowrap}.text-emerald-600{color:var(--color-emerald-600)}.text-foreground{color:var(--frankie-foreground)}.text-muted-foreground{color:var(--frankie-muted-foreground)}.text-primary{color:var(--frankie-primary)}.text-primary-foreground{color:var(--frankie-primary-foreground)}.text-red-600{color:var(--color-red-600)}.text-secondary-foreground{color:var(--frankie-secondary-foreground)}.underline-offset-4{text-underline-offset:4px}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.placeholder\\:text-muted-foreground::placeholder{color:var(--frankie-muted-foreground)}@media (hover:hover){.hover\\:border-border:hover{border-color:var(--frankie-border)}.hover\\:bg-muted:hover{background-color:var(--frankie-muted)}.hover\\:text-foreground:hover{color:var(--frankie-foreground)}.hover\\:underline:hover{text-decoration-line:underline}.hover\\:opacity-90:hover{opacity:.9}.hover\\:opacity-100:hover{opacity:1}}.focus-visible\\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\\:ring-primary:focus-visible{--tw-ring-color:var(--frankie-primary)}.focus-visible\\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.disabled\\:pointer-events-none:disabled{pointer-events:none}.disabled\\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\\:opacity-50:disabled{opacity:.5}.dark\\:text-emerald-400:where(.dark,.dark *){color:var(--color-emerald-400)}.dark\\:text-red-400:where(.dark,.dark *){color:var(--color-red-400)}.\\[\\&_svg\\]\\:pointer-events-none svg{pointer-events:none}.\\[\\&_svg\\]\\:size-4 svg{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.\\[\\&_svg\\]\\:shrink-0 svg{flex-shrink:0}.\\[\\&\\:\\:-webkit-details-marker\\]\\:hidden::-webkit-details-marker{display:none}}.frankie-root.frankie-chat-panel{min-height:0;position:relative}.frankie-root.frankie-widget-panel{box-shadow:0 32px 72px -18px var(--frankie-primary), 0 12px 28px -12px #0f172a47, 0 0 0 1px var(--frankie-foreground)}@supports (color:color-mix(in lab, red, red)){.frankie-root.frankie-widget-panel{box-shadow:0 32px 72px -18px color-mix(in srgb, var(--frankie-primary) 22%, #0f172a6b), 0 12px 28px -12px #0f172a47, 0 0 0 1px color-mix(in srgb, var(--frankie-foreground) 8%, transparent)}}.frankie-root.frankie-widget-panel.dark{box-shadow:0 34px 80px -16px #000000b8, 0 14px 32px -12px #0009, 0 0 0 1px var(--frankie-foreground)}@supports (color:color-mix(in lab, red, red)){.frankie-root.frankie-widget-panel.dark{box-shadow:0 34px 80px -16px #000000b8, 0 14px 32px -12px #0009, 0 0 0 1px color-mix(in srgb, var(--frankie-foreground) 12%, transparent)}}.frankie-root .frankie-agent-context-wrap{flex:none;justify-content:center;padding:12px 10px;display:flex}.frankie-root .frankie-agent-context{max-width:100%;color:var(--frankie-foreground);font-size:13px;position:relative}.frankie-root .frankie-agent-context>summary{border:1px solid var(--frankie-border);background:var(--frankie-background);cursor:pointer;white-space:nowrap;border-radius:999px;justify-content:center;align-items:center;gap:10px;min-height:44px;padding:7px 13px;list-style:none;display:flex;box-shadow:0 1px 3px #0000000d}.frankie-root .frankie-agent-context>summary::-webkit-details-marker{display:none}.frankie-root .frankie-agent-context>summary>svg{color:var(--frankie-muted-foreground);flex:none}.frankie-root .frankie-context-count{background:var(--frankie-muted);color:var(--frankie-muted-foreground);border-radius:999px;padding:2px 10px;font-size:12px}.frankie-root .frankie-context-status{color:var(--frankie-muted-foreground);align-items:center;gap:6px;font-size:12px;display:flex}.frankie-root .frankie-context-status i{background:currentColor;border-radius:50%;width:7px;height:7px}.frankie-root .frankie-context-status[data-active=true]{color:#00976b}.frankie-root .frankie-context-detail{z-index:10;border:1px solid var(--frankie-border);background:var(--frankie-background);border-radius:16px;width:340px;max-width:calc(100vw - 32px);padding:18px;position:absolute;top:calc(100% + 10px);left:50%;transform:translate(-50%);box-shadow:0 12px 32px #00000016}.frankie-root .frankie-context-detail p{color:var(--frankie-muted-foreground);margin:10px 0 0;font-size:12px;line-height:1.6}.frankie-root .frankie-context-detail ul{max-height:220px;margin:12px 0;padding:0;list-style:none;overflow:auto}.frankie-root .frankie-context-detail li{border-bottom:1px solid var(--frankie-border);overflow-wrap:anywhere;justify-content:space-between;gap:12px;padding:8px 0;display:flex}.frankie-root .frankie-context-detail small{color:var(--frankie-muted-foreground);flex:none}.frankie-root .frankie-sr-only{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.frankie-root.frankie-chat-panel-fill{--frankie-canvas:var(--frankie-background)}@supports (color:color-mix(in lab, red, red)){.frankie-root.frankie-chat-panel-fill{--frankie-canvas:color-mix(in srgb,var(--frankie-background) 60%,var(--frankie-muted))}}.frankie-root.frankie-chat-panel-fill{background:var(--frankie-canvas)}.frankie-root.frankie-chat-panel-fill .frankie-agent-context-wrap{padding:44px 16px 26px}.frankie-root.frankie-chat-panel-fill .frankie-transcript{padding:14px max(24px,50% - 480px) 24px}.frankie-root.frankie-chat-panel-fill .frankie-message-list{gap:38px}.frankie-root.frankie-chat-panel-fill .frankie-user-bubble{background:var(--frankie-muted);color:var(--frankie-foreground);white-space:pre-wrap;overflow-wrap:anywhere;border-radius:24px;padding:12px 20px;font-size:15px}.frankie-root.frankie-chat-panel-fill .frankie-assistant-content{flex:1;max-width:100%;font-size:15px;line-height:1.7}.frankie-root.frankie-chat-panel-fill .frankie-assistant-message{gap:14px}.frankie-root .frankie-thinking{color:var(--frankie-muted-foreground);gap:5px}.frankie-root .frankie-thinking-label{margin-right:6px;font-size:14px}.frankie-root.frankie-chat-panel-fill .frankie-composer-footer{background:0 0;border:0;flex:none;padding:16px max(24px,50% - 500px) 24px}.frankie-root.frankie-chat-panel-fill .frankie-composer{border:1px solid var(--frankie-border);background:var(--frankie-background);border-radius:28px;padding:14px;box-shadow:0 2px 4px #0000000a}.frankie-root.frankie-chat-panel-fill .frankie-composer:focus-within{border-color:var(--frankie-foreground)}@supports (color:color-mix(in lab, red, red)){.frankie-root.frankie-chat-panel-fill .frankie-composer:focus-within{border-color:color-mix(in srgb,var(--frankie-foreground) 32%,var(--frankie-border))}}.frankie-root.frankie-chat-panel-fill .frankie-composer:focus-within{box-shadow:0 0 0 2px var(--frankie-primary)}@supports (color:color-mix(in lab, red, red)){.frankie-root.frankie-chat-panel-fill .frankie-composer:focus-within{box-shadow:0 0 0 2px color-mix(in srgb,var(--frankie-primary) 8%,transparent)}}.frankie-root.frankie-chat-panel-fill .frankie-composer-controls{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.frankie-root.frankie-chat-panel-fill .frankie-composer textarea{resize:none;width:100%;min-height:32px;max-height:140px;color:var(--frankie-foreground);box-shadow:none;background:0 0;border:0;border-radius:0;outline:none;flex:0 0 100%;order:-1;margin-bottom:4px;padding:2px 6px;font-size:16px}.frankie-root.frankie-chat-panel-fill .frankie-composer button[aria-label=Send],.frankie-root.frankie-chat-panel-fill .frankie-composer button[aria-label=Stop]{background:var(--frankie-primary);width:42px;height:42px;color:var(--frankie-primary-foreground);border:0;border-radius:50%;margin-left:auto}.frankie-root.frankie-chat-panel-fill .frankie-composer button[aria-label=Attach\\ files]{border-radius:50%;width:42px;height:42px}.frankie-root .frankie-composer-meta{align-items:center;gap:8px;margin-top:6px;padding:0 2px;display:flex}.frankie-root .frankie-model-picker{align-items:center;min-width:0;display:inline-flex}.frankie-root .frankie-model-select{appearance:none;max-width:12rem;color:var(--frankie-muted-foreground);font:inherit;cursor:pointer;text-overflow:ellipsis;background-color:#0000;background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238b8b8b' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");background-position:right 6px center;background-repeat:no-repeat;background-size:11px;border:0;border-radius:8px;padding:5px 22px 5px 8px;font-size:12px;font-weight:500;line-height:1;transition:background-color .12s,color .12s}.frankie-root .frankie-model-select:hover{background-color:var(--frankie-muted);color:var(--frankie-foreground)}.frankie-root .frankie-model-select:disabled{opacity:.5;cursor:not-allowed}.frankie-root .frankie-model-select option{color:var(--frankie-foreground);background:var(--frankie-background);font-weight:400}.frankie-root .frankie-history-desktop{background:var(--frankie-background);width:290px;padding:20px 12px}.frankie-root .frankie-history-collapsed{width:56px;padding:20px 8px}.frankie-root .frankie-new-chat{background:0 0;border:0;justify-content:flex-start;padding:8px 12px;font-size:15px}.frankie-root .frankie-history-heading{color:var(--frankie-muted-foreground);padding:0 12px;font-size:12px;font-weight:400}.frankie-root .frankie-history-list button{min-height:44px;padding:10px 12px}.frankie-root .frankie-history-list button span{font-size:14px}.frankie-root .frankie-history-list button[aria-current],.frankie-root .frankie-history-list button:hover{background:var(--frankie-muted);border-color:#0000}.frankie-root .frankie-history-list small{display:none}.frankie-root .frankie-page-header{background:var(--frankie-background);height:56px;min-height:56px}.frankie-root .frankie-page-header>div{min-width:0}.frankie-root .frankie-thread-title{text-overflow:ellipsis;white-space:nowrap;max-width:55vw;font-weight:500;overflow:hidden}.frankie-root .frankie-page-header button{background:0 0;border-color:#0000}.frankie-root .frankie-page-canvas>.frankie-inline{max-width:none}@media (max-width:760px){.frankie-root.frankie-chat-panel-fill .frankie-agent-context-wrap{padding:20px 8px 14px}.frankie-root.frankie-chat-panel-fill .frankie-transcript{padding:12px 16px 16px}.frankie-root.frankie-chat-panel-fill .frankie-composer-footer{padding:10px 12px calc(12px + env(safe-area-inset-bottom))}.frankie-root .frankie-agent-context>summary{gap:6px;padding:7px 9px;font-size:12px}.frankie-root .frankie-context-count{padding:2px 7px;font-size:11px}.frankie-root .frankie-context-status{gap:4px;font-size:11px}.frankie-root .frankie-agent-context>summary>svg{width:14px}.frankie-root .frankie-thread-title{max-width:42vw}}.frankie-root .frankie-agent-context>summary>svg:first-child{box-sizing:content-box;background:var(--frankie-muted);border-radius:50%;padding:4px}.frankie-root.frankie-chat-panel-fill .frankie-agent-context>summary{font-size:14px}.frankie-root .frankie-agent-context>summary>span:first-of-type{border-right:1px solid var(--frankie-border);padding-right:10px}@media (max-width:760px){.frankie-root .frankie-page-header button>span{display:none}}.frankie-root .frankie-choices{flex-direction:column;gap:8px;margin-top:12px;display:flex}.frankie-root .frankie-choice{text-align:left;cursor:pointer;border:1px solid var(--frankie-border);border-radius:var(--frankie-radius);background:var(--frankie-background);width:100%;color:var(--frankie-foreground);font:inherit;align-items:center;gap:12px;padding:11px 14px;font-size:14px;line-height:1.4;transition:border-color .15s,background-color .15s,transform .12s;display:flex}.frankie-root .frankie-choice:hover:not(:disabled){border-color:var(--frankie-primary)}@supports (color:color-mix(in lab, red, red)){.frankie-root .frankie-choice:hover:not(:disabled){border-color:color-mix(in srgb, var(--frankie-primary) 55%, var(--frankie-border))}}.frankie-root .frankie-choice:hover:not(:disabled){background:var(--frankie-primary)}@supports (color:color-mix(in lab, red, red)){.frankie-root .frankie-choice:hover:not(:disabled){background:color-mix(in srgb, var(--frankie-primary) 6%, var(--frankie-background))}}.frankie-root .frankie-choice:active:not(:disabled){transform:scale(.99)}.frankie-root .frankie-choice:focus-visible{box-shadow:0 0 0 2px var(--frankie-primary);outline:none}@supports (color:color-mix(in lab, red, red)){.frankie-root .frankie-choice:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, var(--frankie-primary) 40%, transparent)}}.frankie-root .frankie-choice:disabled{opacity:.6;cursor:default}.frankie-root .frankie-choice-index{border:1px solid var(--frankie-border);background:var(--frankie-muted);width:24px;height:24px;color:var(--frankie-muted-foreground);border-radius:50%;flex:none;place-items:center;font-size:12px;font-weight:600;transition:background-color .15s,color .15s,border-color .15s;display:grid}.frankie-root .frankie-choice:hover:not(:disabled) .frankie-choice-index{background:var(--frankie-primary);border-color:var(--frankie-primary);color:var(--frankie-primary-foreground)}.frankie-root .frankie-choice-label{flex:1;min-width:0}.frankie-root .frankie-choice-go{color:var(--frankie-muted-foreground);opacity:0;flex:none;transition:opacity .15s,transform .15s;transform:translate(-4px)}.frankie-root .frankie-choice:hover:not(:disabled) .frankie-choice-go{opacity:1;color:var(--frankie-primary);transform:none}@media (prefers-reduced-motion:reduce){.frankie-root .frankie-choice,.frankie-root .frankie-choice-go,.frankie-root .frankie-choice-index{transition:none}}.frankie-root .frankie-choice-other{color:var(--frankie-muted-foreground);border-style:dashed}.frankie-root .frankie-choice-other .frankie-choice-index{background:0 0}.frankie-root .frankie-choice-other:hover:not(:disabled){color:var(--frankie-foreground)}.frankie-root .frankie-choice-other:hover:not(:disabled) .frankie-choice-index{background:var(--frankie-muted);border-color:var(--frankie-border);color:var(--frankie-foreground)}.frankie-root .frankie-choice-hint{color:var(--frankie-muted-foreground);flex:none;font-size:12px}.frankie-root .frankie-skeleton{flex-direction:column;gap:8px;max-width:min(420px,100%);padding:4px 0 6px;display:flex}.frankie-root .frankie-skeleton-line{background:linear-gradient(90deg, var(--frankie-muted-foreground) 25%, var(--frankie-muted-foreground) 37%, var(--frankie-muted-foreground) 63%);border-radius:6px;height:12px}@supports (color:color-mix(in lab, red, red)){.frankie-root .frankie-skeleton-line{background:linear-gradient(90deg, color-mix(in srgb, var(--frankie-muted-foreground) 11%, transparent) 25%, color-mix(in srgb, var(--frankie-muted-foreground) 26%, transparent) 37%, color-mix(in srgb, var(--frankie-muted-foreground) 11%, transparent) 63%)}}.frankie-root .frankie-skeleton-line{background-size:400% 100%;animation:1.4s infinite frankie-shimmer}@keyframes frankie-shimmer{0%{background-position:100%}to{background-position:0}}@keyframes frankie-skeleton-pulse{0%,to{opacity:.45}50%{opacity:.85}}@media (prefers-reduced-motion:reduce){.frankie-root .frankie-skeleton-line{background:var(--frankie-muted-foreground)}@supports (color:color-mix(in lab, red, red)){.frankie-root .frankie-skeleton-line{background:color-mix(in srgb, var(--frankie-muted-foreground) 16%, transparent)}}.frankie-root .frankie-skeleton-line{animation:1.6s ease-in-out infinite frankie-skeleton-pulse}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-ease{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}`;
777
+
778
+ // src/lib/ensure-frankie-styles.ts
779
+ var STYLE_ELEMENT_ID = "frankie-fallback-styles";
780
+ var attempted = false;
781
+ function ensureFrankieStyles() {
782
+ if (attempted || typeof document === "undefined") return;
783
+ attempted = true;
784
+ if (document.getElementById(STYLE_ELEMENT_ID)) return;
785
+ const style = document.createElement("style");
786
+ style.id = STYLE_ELEMENT_ID;
787
+ style.textContent = FRANKIE_CSS_CONTENT;
788
+ document.head.appendChild(style);
789
+ }
790
+
791
+ // src/lib/styles-ready.ts
792
+ var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect3 : useLayoutEffect;
793
+ var SENTINEL = "--frankie-styles-ready";
794
+ function useStylesReady(ref, timeoutMs = 600, injectFallback = true) {
795
+ const [ready, setReady] = useState3(false);
796
+ useIsomorphicLayoutEffect(() => {
797
+ if (ready) return;
798
+ const node = ref.current;
799
+ if (!node || typeof window === "undefined" || typeof window.getComputedStyle !== "function") {
800
+ setReady(true);
801
+ return;
802
+ }
803
+ const applied = () => window.getComputedStyle(node).getPropertyValue(SENTINEL).trim() === "1";
804
+ if (applied()) {
805
+ setReady(true);
806
+ return;
807
+ }
808
+ const deadline = Date.now() + timeoutMs;
809
+ let frame = 0;
810
+ const poll = () => {
811
+ if (applied()) {
812
+ setReady(true);
813
+ return;
814
+ }
815
+ if (Date.now() >= deadline) {
816
+ if (injectFallback) ensureFrankieStyles();
817
+ setReady(true);
818
+ return;
819
+ }
820
+ frame = window.requestAnimationFrame(poll);
821
+ };
822
+ if (typeof window.requestAnimationFrame !== "function") {
823
+ setReady(true);
824
+ return;
825
+ }
826
+ frame = window.requestAnimationFrame(poll);
827
+ return () => window.cancelAnimationFrame(frame);
828
+ }, [ref, ready, timeoutMs, injectFallback]);
829
+ return ready;
830
+ }
554
831
 
555
832
  // src/lib/theme.ts
556
833
  function themeToCssVars(theme) {
@@ -611,7 +888,7 @@ function Button({ className, variant, size, asChild = false, ...props }) {
611
888
 
612
889
  // src/components/chat/ChatPanel.tsx
613
890
  import { RotateCcw, X as X3 } from "lucide-react";
614
- import { useEffect as useEffect4, useRef as useRef4, useState as useState5 } from "react";
891
+ import { useEffect as useEffect5, useRef as useRef4, useState as useState6 } from "react";
615
892
 
616
893
  // src/components/ui/avatar.tsx
617
894
  import { jsx as jsx3 } from "react/jsx-runtime";
@@ -717,7 +994,7 @@ function AgentContext() {
717
994
 
718
995
  // src/components/chat/Composer.tsx
719
996
  import { ArrowUp, Paperclip, Plus, Square, X } from "lucide-react";
720
- import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
997
+ import { useCallback as useCallback2, useEffect as useEffect4, useRef as useRef3, useState as useState4 } from "react";
721
998
 
722
999
  // src/components/ui/textarea.tsx
723
1000
  import { forwardRef as forwardRef2 } from "react";
@@ -741,15 +1018,22 @@ var Textarea = forwardRef2(
741
1018
  // src/components/chat/Composer.tsx
742
1019
  import { Fragment, jsx as jsx8, jsxs as jsxs2 } from "react/jsx-runtime";
743
1020
  var MAX_ROWS_PX = 140;
744
- function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, attachments, modelPicker }) {
745
- const [value, setValue] = useState3("");
746
- const [files, setFiles] = useState3([]);
747
- const [fileError, setFileError] = useState3(void 0);
1021
+ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, attachments, modelPicker, focusHandleRef }) {
1022
+ const [value, setValue] = useState4("");
1023
+ const [files, setFiles] = useState4([]);
1024
+ const [fileError, setFileError] = useState4(void 0);
748
1025
  const textareaRef = useRef3(null);
749
1026
  const fileInputRef = useRef3(null);
1027
+ useEffect4(() => {
1028
+ if (!focusHandleRef) return;
1029
+ focusHandleRef.current = () => textareaRef.current?.focus();
1030
+ return () => {
1031
+ focusHandleRef.current = null;
1032
+ };
1033
+ }, [focusHandleRef]);
750
1034
  const canAttach = Boolean(attachments?.enabled);
751
1035
  const models = modelPicker?.models ?? [];
752
- useEffect3(() => {
1036
+ useEffect4(() => {
753
1037
  const node = textareaRef.current;
754
1038
  if (!node) return;
755
1039
  node.style.height = "auto";
@@ -882,19 +1166,22 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
882
1166
  "aria-label": "Message"
883
1167
  }
884
1168
  ),
885
- models.length > 0 ? /* @__PURE__ */ jsx8(
1169
+ busy ? /* @__PURE__ */ jsx8(Button, { type: "button", size: "icon", variant: "outline", onClick: onStop, "aria-label": "Stop", children: /* @__PURE__ */ jsx8(Square, {}) }) : /* @__PURE__ */ jsx8(Button, { type: "submit", size: "icon", disabled: disabled || nothingToSend, "aria-label": "Send", children: /* @__PURE__ */ jsx8(ArrowUp, {}) })
1170
+ ] }),
1171
+ models.length > 0 ? /* @__PURE__ */ jsx8("div", { className: "frankie-composer-meta", children: /* @__PURE__ */ jsxs2("label", { className: "frankie-model-picker", title: "Model", children: [
1172
+ /* @__PURE__ */ jsx8("span", { className: "frankie-sr-only", children: "Model" }),
1173
+ /* @__PURE__ */ jsx8(
886
1174
  "select",
887
1175
  {
888
1176
  value: modelPicker?.value ?? models[0]?.id,
889
1177
  onChange: (event) => modelPicker?.onChange(event.target.value),
890
1178
  disabled,
891
1179
  "aria-label": "Model",
892
- className: "frankie-focus h-9 max-w-[8rem] rounded-[var(--frankie-radius)] border border-border bg-background px-2 text-sm",
1180
+ className: "frankie-model-select",
893
1181
  children: models.map((model) => /* @__PURE__ */ jsx8("option", { value: model.id, children: model.label }, model.id))
894
1182
  }
895
- ) : null,
896
- busy ? /* @__PURE__ */ jsx8(Button, { type: "button", size: "icon", variant: "outline", onClick: onStop, "aria-label": "Stop", children: /* @__PURE__ */ jsx8(Square, {}) }) : /* @__PURE__ */ jsx8(Button, { type: "submit", size: "icon", disabled: disabled || nothingToSend, "aria-label": "Send", children: /* @__PURE__ */ jsx8(ArrowUp, {}) })
897
- ] })
1183
+ )
1184
+ ] }) }) : null
898
1185
  ]
899
1186
  }
900
1187
  );
@@ -919,14 +1206,76 @@ function readAsBase64(file) {
919
1206
 
920
1207
  // src/components/chat/MessageBubble.tsx
921
1208
  import { Check as Check2, Copy, Paperclip as Paperclip2 } from "lucide-react";
922
- import { useState as useState4 } from "react";
1209
+ import { memo as memo2, useState as useState5 } from "react";
923
1210
 
924
- // src/components/chat/Markdown.tsx
925
- import { Fragment as Fragment2 } from "react";
926
- import { jsx as jsx9 } from "react/jsx-runtime";
927
- function Markdown({ text, className }) {
928
- return /* @__PURE__ */ jsx9("div", { className: cn("frankie-prose", className), children: renderBlocks(text) });
1211
+ // src/lib/choices.ts
1212
+ var CLOSED = /```frankie:choices[ \t]*\r?\n([\s\S]*?)```/;
1213
+ var OPEN = /```frankie:choices[ \t]*\r?\n?[\s\S]*$/;
1214
+ function extractChoices(text) {
1215
+ const closed = CLOSED.exec(text);
1216
+ if (closed) {
1217
+ const options = parseOptionLines(closed[1] ?? "");
1218
+ const body = (text.slice(0, closed.index) + text.slice(closed.index + closed[0].length)).replace(/\n{3,}/g, "\n\n").trimEnd();
1219
+ return { body, options: options.length ? options : null };
1220
+ }
1221
+ const open = OPEN.exec(text);
1222
+ if (open) return { body: text.slice(0, open.index).trimEnd(), options: null };
1223
+ return { body: text, options: null };
1224
+ }
1225
+ function parseOptionLines(block) {
1226
+ return block.split("\n").map((line) => line.trim().replace(/^([-*+]|\d+[.)])\s+/, "").trim()).filter(Boolean);
1227
+ }
1228
+
1229
+ // src/components/chat/Choices.tsx
1230
+ import { ArrowRight, PencilLine } from "lucide-react";
1231
+ import { jsx as jsx9, jsxs as jsxs3 } from "react/jsx-runtime";
1232
+ function Choices({
1233
+ options,
1234
+ onPick,
1235
+ onSomethingElse,
1236
+ disabled
1237
+ }) {
1238
+ if (options.length === 0) return null;
1239
+ return /* @__PURE__ */ jsxs3("div", { className: "frankie-choices", role: "group", "aria-label": "Choose an option", children: [
1240
+ options.map((option, index) => /* @__PURE__ */ jsxs3(
1241
+ "button",
1242
+ {
1243
+ type: "button",
1244
+ className: "frankie-choice",
1245
+ disabled,
1246
+ onClick: () => onPick(option),
1247
+ children: [
1248
+ /* @__PURE__ */ jsx9("span", { className: "frankie-choice-index", "aria-hidden": true, children: index + 1 }),
1249
+ /* @__PURE__ */ jsx9("span", { className: "frankie-choice-label", children: option }),
1250
+ /* @__PURE__ */ jsx9(ArrowRight, { className: "frankie-choice-go size-4", "aria-hidden": true })
1251
+ ]
1252
+ },
1253
+ `${index}-${option}`
1254
+ )),
1255
+ onSomethingElse ? /* @__PURE__ */ jsxs3(
1256
+ "button",
1257
+ {
1258
+ type: "button",
1259
+ className: "frankie-choice frankie-choice-other",
1260
+ disabled,
1261
+ onClick: onSomethingElse,
1262
+ children: [
1263
+ /* @__PURE__ */ jsx9("span", { className: "frankie-choice-index", "aria-hidden": true, children: /* @__PURE__ */ jsx9(PencilLine, { className: "size-3.5" }) }),
1264
+ /* @__PURE__ */ jsx9("span", { className: "frankie-choice-label", children: "Something else\u2026" }),
1265
+ /* @__PURE__ */ jsx9("span", { className: "frankie-choice-hint", "aria-hidden": true, children: "Type your own" })
1266
+ ]
1267
+ }
1268
+ ) : null
1269
+ ] });
929
1270
  }
1271
+
1272
+ // src/components/chat/Markdown.tsx
1273
+ import { Fragment as Fragment2, memo, useMemo as useMemo2 } from "react";
1274
+ import { jsx as jsx10 } from "react/jsx-runtime";
1275
+ var Markdown = memo(function Markdown2({ text, className }) {
1276
+ const blocks = useMemo2(() => renderBlocks(text), [text]);
1277
+ return /* @__PURE__ */ jsx10("div", { className: cn("frankie-prose", className), children: blocks });
1278
+ });
930
1279
  function renderBlocks(text) {
931
1280
  const lines = text.split("\n");
932
1281
  const blocks = [];
@@ -935,13 +1284,13 @@ function renderBlocks(text) {
935
1284
  let code = null;
936
1285
  const flushParagraph = () => {
937
1286
  if (paragraph.length === 0) return;
938
- blocks.push(/* @__PURE__ */ jsx9("p", { children: renderInline(paragraph.join(" ")) }, `p${blocks.length}`));
1287
+ blocks.push(/* @__PURE__ */ jsx10("p", { children: renderInline(paragraph.join(" ")) }, `p${blocks.length}`));
939
1288
  paragraph = [];
940
1289
  };
941
1290
  const flushList = () => {
942
1291
  if (!list) return;
943
- const items = list.items.map((item, index) => /* @__PURE__ */ jsx9("li", { children: renderInline(item) }, index));
944
- blocks.push(list.ordered ? /* @__PURE__ */ jsx9("ol", { children: items }, `l${blocks.length}`) : /* @__PURE__ */ jsx9("ul", { children: items }, `l${blocks.length}`));
1292
+ const items = list.items.map((item, index) => /* @__PURE__ */ jsx10("li", { children: renderInline(item) }, index));
1293
+ blocks.push(list.ordered ? /* @__PURE__ */ jsx10("ol", { children: items }, `l${blocks.length}`) : /* @__PURE__ */ jsx10("ul", { children: items }, `l${blocks.length}`));
945
1294
  list = null;
946
1295
  };
947
1296
  for (const line of lines) {
@@ -949,7 +1298,7 @@ function renderBlocks(text) {
949
1298
  if (fence) {
950
1299
  if (code) {
951
1300
  blocks.push(
952
- /* @__PURE__ */ jsx9("pre", { children: /* @__PURE__ */ jsx9("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
1301
+ /* @__PURE__ */ jsx10("pre", { children: /* @__PURE__ */ jsx10("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
953
1302
  );
954
1303
  code = null;
955
1304
  } else {
@@ -980,11 +1329,13 @@ function renderBlocks(text) {
980
1329
  flushList();
981
1330
  continue;
982
1331
  }
983
- const heading = /^#{1,6}\s+(.*)$/.exec(line);
1332
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
984
1333
  if (heading) {
985
1334
  flushParagraph();
986
1335
  flushList();
987
- blocks.push(/* @__PURE__ */ jsx9("p", { className: "font-semibold", children: renderInline(heading[1] ?? "") }, `h${blocks.length}`));
1336
+ const level = (heading[1] ?? "#").length;
1337
+ const Tag = level <= 1 ? "h3" : level === 2 ? "h4" : "h5";
1338
+ blocks.push(/* @__PURE__ */ jsx10(Tag, { className: "frankie-h", children: renderInline(heading[2] ?? "") }, `h${blocks.length}`));
988
1339
  continue;
989
1340
  }
990
1341
  flushList();
@@ -992,7 +1343,7 @@ function renderBlocks(text) {
992
1343
  }
993
1344
  if (code) {
994
1345
  blocks.push(
995
- /* @__PURE__ */ jsx9("pre", { children: /* @__PURE__ */ jsx9("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
1346
+ /* @__PURE__ */ jsx10("pre", { children: /* @__PURE__ */ jsx10("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
996
1347
  );
997
1348
  }
998
1349
  flushParagraph();
@@ -1007,28 +1358,28 @@ function renderInline(text) {
1007
1358
  const token = match[0];
1008
1359
  const start = match.index;
1009
1360
  if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
1010
- nodes.push(/* @__PURE__ */ jsx9(Fragment2, { children: renderToken(token) }, start));
1361
+ nodes.push(/* @__PURE__ */ jsx10(Fragment2, { children: renderToken(token) }, start));
1011
1362
  lastIndex = start + token.length;
1012
1363
  }
1013
1364
  if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
1014
1365
  return nodes;
1015
1366
  }
1016
1367
  function renderToken(token) {
1017
- if (token.startsWith("**")) return /* @__PURE__ */ jsx9("strong", { children: token.slice(2, -2) });
1018
- if (token.startsWith("`")) return /* @__PURE__ */ jsx9("code", { children: token.slice(1, -1) });
1368
+ if (token.startsWith("**")) return /* @__PURE__ */ jsx10("strong", { children: token.slice(2, -2) });
1369
+ if (token.startsWith("`")) return /* @__PURE__ */ jsx10("code", { children: token.slice(1, -1) });
1019
1370
  const link = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token);
1020
1371
  const href = link ? link[2] ?? "" : token;
1021
1372
  const label = link ? link[1] ?? "" : token;
1022
1373
  if (!/^(https?:\/\/|\/)/.test(href)) return token;
1023
- return /* @__PURE__ */ jsx9("a", { href, target: "_blank", rel: "noopener noreferrer nofollow", children: label });
1374
+ return /* @__PURE__ */ jsx10("a", { href, target: "_blank", rel: "noopener noreferrer nofollow", children: label });
1024
1375
  }
1025
1376
 
1026
1377
  // src/components/chat/ToolActivityList.tsx
1027
- import { Check, ChevronDown, Loader2, ShieldAlert, X as X2 } from "lucide-react";
1378
+ import { Check, ChevronDown, Loader2, ShieldAlert, Wrench, X as X2 } from "lucide-react";
1028
1379
 
1029
1380
  // src/components/ui/badge.tsx
1030
1381
  import { cva as cva2 } from "class-variance-authority";
1031
- import { jsx as jsx10 } from "react/jsx-runtime";
1382
+ import { jsx as jsx11 } from "react/jsx-runtime";
1032
1383
  var badgeVariants = cva2(
1033
1384
  "inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[0.6875rem] font-medium",
1034
1385
  {
@@ -1048,44 +1399,45 @@ function Badge({
1048
1399
  variant,
1049
1400
  ...props
1050
1401
  }) {
1051
- return /* @__PURE__ */ jsx10("span", { className: cn(badgeVariants({ variant }), className), ...props });
1402
+ return /* @__PURE__ */ jsx11("span", { className: cn(badgeVariants({ variant }), className), ...props });
1052
1403
  }
1053
1404
 
1054
1405
  // src/components/chat/ToolActivityList.tsx
1055
- import { jsx as jsx11, jsxs as jsxs3 } from "react/jsx-runtime";
1406
+ import { jsx as jsx12, jsxs as jsxs4 } from "react/jsx-runtime";
1056
1407
  function ToolActivityList({ tools, renderTool }) {
1057
1408
  if (tools.length === 0) return null;
1058
1409
  const running = tools.some((tool) => tool.status === "running");
1059
- return /* @__PURE__ */ jsxs3("details", { className: "frankie-tool-activity mt-2", open: running, children: [
1060
- /* @__PURE__ */ jsxs3("summary", { className: "inline-flex min-h-7 cursor-pointer list-none items-center gap-1.5 rounded-md px-1 text-xs font-medium text-muted-foreground hover:bg-muted hover:text-foreground [&::-webkit-details-marker]:hidden", children: [
1061
- /* @__PURE__ */ jsx11(ChevronDown, { className: "frankie-tool-chevron size-3.5 transition-transform", "aria-hidden": "true" }),
1062
- /* @__PURE__ */ jsx11("span", { children: running ? "Working" : "Activity" }),
1063
- /* @__PURE__ */ jsxs3("span", { className: "text-[11px]", children: [
1410
+ return /* @__PURE__ */ jsxs4("details", { className: "frankie-tool-activity mt-2", open: running, children: [
1411
+ /* @__PURE__ */ jsxs4("summary", { className: "inline-flex min-h-7 cursor-pointer list-none items-center gap-1.5 rounded-md px-1 text-xs font-medium text-muted-foreground hover:bg-muted hover:text-foreground [&::-webkit-details-marker]:hidden", children: [
1412
+ /* @__PURE__ */ jsx12(ChevronDown, { className: "frankie-tool-chevron size-3.5 transition-transform", "aria-hidden": "true" }),
1413
+ running ? /* @__PURE__ */ jsx12(Loader2, { className: "size-3.5 animate-spin", "aria-hidden": "true" }) : /* @__PURE__ */ jsx12(Wrench, { className: "size-3.5", "aria-hidden": "true" }),
1414
+ /* @__PURE__ */ jsx12("span", { children: running ? "Working" : "Activity" }),
1415
+ /* @__PURE__ */ jsxs4("span", { className: "text-[11px]", children: [
1064
1416
  "\xB7 ",
1065
1417
  tools.length,
1066
1418
  " tool call",
1067
1419
  tools.length === 1 ? "" : "s"
1068
1420
  ] })
1069
1421
  ] }),
1070
- /* @__PURE__ */ jsx11("ul", { className: "mt-1 flex flex-col gap-1 border-l border-border pl-2", children: tools.map((tool) => /* @__PURE__ */ jsx11("li", { children: renderTool ? renderTool(tool) : /* @__PURE__ */ jsx11(ToolActivityRow, { tool }) }, tool.id)) })
1422
+ /* @__PURE__ */ jsx12("ul", { className: "mt-1 flex flex-col gap-1 border-l border-border pl-2", children: tools.map((tool) => /* @__PURE__ */ jsx12("li", { children: renderTool ? renderTool(tool) : /* @__PURE__ */ jsx12(ToolActivityRow, { tool }) }, tool.id)) })
1071
1423
  ] });
1072
1424
  }
1073
1425
  function ToolActivityRow({ tool }) {
1074
- return /* @__PURE__ */ jsxs3(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
1075
- /* @__PURE__ */ jsx11(ToolIcon, { status: tool.status }),
1076
- /* @__PURE__ */ jsx11("span", { className: "truncate", children: tool.label })
1426
+ return /* @__PURE__ */ jsxs4(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
1427
+ /* @__PURE__ */ jsx12(ToolIcon, { status: tool.status }),
1428
+ /* @__PURE__ */ jsx12("span", { className: "truncate", children: tool.label })
1077
1429
  ] });
1078
1430
  }
1079
1431
  function ToolIcon({ status }) {
1080
1432
  switch (status) {
1081
1433
  case "running":
1082
- return /* @__PURE__ */ jsx11(Loader2, { className: "size-3 animate-spin", "aria-hidden": "true" });
1434
+ return /* @__PURE__ */ jsx12(Loader2, { className: "size-3 animate-spin", "aria-hidden": "true" });
1083
1435
  case "done":
1084
- return /* @__PURE__ */ jsx11(Check, { className: "size-3", "aria-hidden": "true" });
1436
+ return /* @__PURE__ */ jsx12(Check, { className: "size-3", "aria-hidden": "true" });
1085
1437
  case "denied":
1086
- return /* @__PURE__ */ jsx11(ShieldAlert, { className: "size-3", "aria-hidden": "true" });
1438
+ return /* @__PURE__ */ jsx12(ShieldAlert, { className: "size-3", "aria-hidden": "true" });
1087
1439
  default:
1088
- return /* @__PURE__ */ jsx11(X2, { className: "size-3", "aria-hidden": "true" });
1440
+ return /* @__PURE__ */ jsx12(X2, { className: "size-3", "aria-hidden": "true" });
1089
1441
  }
1090
1442
  }
1091
1443
  function badgeVariant(status) {
@@ -1096,47 +1448,56 @@ function badgeVariant(status) {
1096
1448
  }
1097
1449
 
1098
1450
  // src/components/chat/MessageBubble.tsx
1099
- import { jsx as jsx12, jsxs as jsxs4 } from "react/jsx-runtime";
1100
- function MessageBubble({
1451
+ import { jsx as jsx13, jsxs as jsxs5 } from "react/jsx-runtime";
1452
+ var MessageBubble = memo2(function MessageBubble2({
1101
1453
  message,
1102
1454
  assistantName,
1103
1455
  avatarUrl,
1104
1456
  showToolActivity,
1105
- renderTool
1457
+ renderTool,
1458
+ onChoose,
1459
+ onRequestInput,
1460
+ active,
1461
+ busy
1106
1462
  }) {
1107
1463
  if (message.role === "user") {
1108
- return /* @__PURE__ */ jsxs4("div", { className: "frankie-user-message flex flex-col items-end gap-1", children: [
1109
- message.attachments?.length ? /* @__PURE__ */ jsx12("div", { className: "flex max-w-[85%] flex-wrap justify-end gap-1", children: message.attachments.map((file, index) => /* @__PURE__ */ jsxs4(
1464
+ return /* @__PURE__ */ jsxs5("div", { className: "frankie-user-message flex flex-col items-end gap-1", children: [
1465
+ message.attachments?.length ? /* @__PURE__ */ jsx13("div", { className: "flex max-w-[85%] flex-wrap justify-end gap-1", children: message.attachments.map((file, index) => /* @__PURE__ */ jsxs5(
1110
1466
  "span",
1111
1467
  {
1112
1468
  className: "inline-flex max-w-[12rem] items-center gap-1 rounded-[var(--frankie-radius)] border border-border bg-muted px-2 py-1 text-xs",
1113
1469
  children: [
1114
- /* @__PURE__ */ jsx12(Paperclip2, { className: "size-3 shrink-0 opacity-70" }),
1115
- /* @__PURE__ */ jsx12("span", { className: "truncate", children: file.name })
1470
+ /* @__PURE__ */ jsx13(Paperclip2, { className: "size-3 shrink-0 opacity-70" }),
1471
+ /* @__PURE__ */ jsx13("span", { className: "truncate", children: file.name })
1116
1472
  ]
1117
1473
  },
1118
1474
  `${file.name}-${index}`
1119
1475
  )) }) : null,
1120
- message.text ? /* @__PURE__ */ jsxs4("div", { className: "flex max-w-[85%] items-end gap-1", children: [
1121
- /* @__PURE__ */ jsx12("div", { className: "frankie-user-bubble rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }),
1122
- /* @__PURE__ */ jsx12(MessageCopyButton, { text: message.text })
1476
+ message.text ? /* @__PURE__ */ jsxs5("div", { className: "flex max-w-[85%] items-end gap-1", children: [
1477
+ /* @__PURE__ */ jsx13("div", { className: "frankie-user-bubble rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }),
1478
+ /* @__PURE__ */ jsx13(MessageCopyButton, { text: message.text })
1123
1479
  ] }) : null
1124
1480
  ] });
1125
1481
  }
1126
- const empty = message.text.length === 0 && message.tools.length === 0;
1127
- return /* @__PURE__ */ jsxs4("div", { className: "frankie-assistant-message flex gap-2", children: [
1128
- /* @__PURE__ */ jsxs4("div", { className: "frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1", children: [
1129
- /* @__PURE__ */ jsx12(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName }),
1130
- message.text ? /* @__PURE__ */ jsx12(MessageCopyButton, { text: message.text }) : null
1482
+ const { body, options } = extractChoices(message.text);
1483
+ const showChoices = Boolean(active && onChoose && options && message.status === "done");
1484
+ const toolsRunning = message.tools.some((tool) => tool.status === "running");
1485
+ const waiting = message.status === "streaming" && body.length === 0 && !toolsRunning;
1486
+ return /* @__PURE__ */ jsxs5("div", { className: "frankie-assistant-message flex gap-2", children: [
1487
+ /* @__PURE__ */ jsxs5("div", { className: "frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1", children: [
1488
+ /* @__PURE__ */ jsx13(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName }),
1489
+ body ? /* @__PURE__ */ jsx13(MessageCopyButton, { text: body }) : null
1131
1490
  ] }),
1132
- /* @__PURE__ */ jsxs4("div", { className: "frankie-assistant-content min-w-0 max-w-[85%]", children: [
1133
- message.status === "error" ? /* @__PURE__ */ jsx12("p", { className: "rounded-[var(--frankie-radius)] bg-red-500/10 px-3 py-2 text-sm text-red-600 dark:text-red-400", children: message.error }) : empty && message.status === "streaming" ? /* @__PURE__ */ jsx12(TypingDots, {}) : /* @__PURE__ */ jsx12(Markdown, { text: message.text, className: "text-sm" }),
1134
- showToolActivity ? /* @__PURE__ */ jsx12(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null
1491
+ /* @__PURE__ */ jsxs5("div", { className: "frankie-assistant-content min-w-0 max-w-[85%]", children: [
1492
+ message.status === "error" ? /* @__PURE__ */ jsx13("p", { className: "rounded-[var(--frankie-radius)] bg-red-500/10 px-3 py-2 text-sm text-red-600 dark:text-red-400", children: message.error }) : body ? /* @__PURE__ */ jsx13(Markdown, { text: body, className: "text-sm" }) : null,
1493
+ showChoices ? /* @__PURE__ */ jsx13(Choices, { options: options ?? [], onPick: onChoose, ...onRequestInput ? { onSomethingElse: onRequestInput } : {}, disabled: busy }) : null,
1494
+ showToolActivity ? /* @__PURE__ */ jsx13(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null,
1495
+ waiting ? /* @__PURE__ */ jsx13(Thinking, {}) : null
1135
1496
  ] })
1136
1497
  ] });
1137
- }
1498
+ });
1138
1499
  function MessageCopyButton({ text }) {
1139
- const [copied, setCopied] = useState4(false);
1500
+ const [copied, setCopied] = useState5(false);
1140
1501
  const copy = async () => {
1141
1502
  try {
1142
1503
  await navigator.clipboard.writeText(text);
@@ -1145,12 +1506,12 @@ function MessageCopyButton({ text }) {
1145
1506
  } catch {
1146
1507
  }
1147
1508
  };
1148
- return /* @__PURE__ */ jsx12("button", { type: "button", onClick: () => void copy(), className: "frankie-message-copy inline-flex size-8 shrink-0 items-center justify-center rounded-md border border-transparent text-muted-foreground transition hover:border-border hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary", "aria-label": copied ? "Copied" : "Copy message", title: copied ? "Copied" : "Copy message", children: copied ? /* @__PURE__ */ jsx12(Check2, { className: "size-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ jsx12(Copy, { className: "size-3.5", "aria-hidden": "true" }) });
1509
+ return /* @__PURE__ */ jsx13("button", { type: "button", onClick: () => void copy(), className: "frankie-message-copy inline-flex size-8 shrink-0 items-center justify-center rounded-md border border-transparent text-muted-foreground transition hover:border-border hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary", "aria-label": copied ? "Copied" : "Copy message", title: copied ? "Copied" : "Copy message", children: copied ? /* @__PURE__ */ jsx13(Check2, { className: "size-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ jsx13(Copy, { className: "size-3.5", "aria-hidden": "true" }) });
1149
1510
  }
1150
1511
  function TypingDots() {
1151
- return /* @__PURE__ */ jsxs4("span", { className: "frankie-thinking inline-flex items-center gap-1 py-2", role: "status", "aria-label": "Thinking", children: [
1152
- /* @__PURE__ */ jsx12("span", { className: "frankie-thinking-label", children: "Thinking" }),
1153
- [0, 1, 2].map((index) => /* @__PURE__ */ jsx12(
1512
+ return /* @__PURE__ */ jsxs5("span", { className: "frankie-thinking inline-flex items-center gap-1 py-2", role: "status", "aria-label": "Thinking", children: [
1513
+ /* @__PURE__ */ jsx13("span", { className: "frankie-thinking-label", children: "Thinking" }),
1514
+ [0, 1, 2].map((index) => /* @__PURE__ */ jsx13(
1154
1515
  "span",
1155
1516
  {
1156
1517
  className: cn("size-1.5 animate-pulse rounded-full bg-muted-foreground"),
@@ -1160,16 +1521,23 @@ function TypingDots() {
1160
1521
  ))
1161
1522
  ] });
1162
1523
  }
1524
+ function Thinking() {
1525
+ return /* @__PURE__ */ jsxs5("div", { className: "frankie-skeleton", role: "status", "aria-label": "Thinking", children: [
1526
+ /* @__PURE__ */ jsx13("span", { className: "frankie-skeleton-line", style: { width: "62%" } }),
1527
+ /* @__PURE__ */ jsx13("span", { className: "frankie-skeleton-line", style: { width: "92%" } }),
1528
+ /* @__PURE__ */ jsx13("span", { className: "frankie-skeleton-line", style: { width: "48%" } })
1529
+ ] });
1530
+ }
1163
1531
 
1164
1532
  // src/components/chat/SuggestedPrompts.tsx
1165
- import { jsx as jsx13 } from "react/jsx-runtime";
1533
+ import { jsx as jsx14 } from "react/jsx-runtime";
1166
1534
  function SuggestedPrompts({
1167
1535
  prompts,
1168
1536
  onPick,
1169
1537
  disabled
1170
1538
  }) {
1171
1539
  if (prompts.length === 0) return null;
1172
- return /* @__PURE__ */ jsx13("div", { className: "flex flex-wrap gap-2", children: prompts.map((prompt) => /* @__PURE__ */ jsx13(
1540
+ return /* @__PURE__ */ jsx14("div", { className: "flex flex-wrap gap-2", children: prompts.map((prompt) => /* @__PURE__ */ jsx14(
1173
1541
  Button,
1174
1542
  {
1175
1543
  type: "button",
@@ -1184,7 +1552,7 @@ function SuggestedPrompts({
1184
1552
  }
1185
1553
 
1186
1554
  // src/components/chat/ChatPanel.tsx
1187
- import { jsx as jsx14, jsxs as jsxs5 } from "react/jsx-runtime";
1555
+ import { jsx as jsx15, jsxs as jsxs6 } from "react/jsx-runtime";
1188
1556
  function ChatPanel({
1189
1557
  className,
1190
1558
  header,
@@ -1204,8 +1572,10 @@ function ChatPanel({
1204
1572
  const atBottomRef = useRef4(true);
1205
1573
  const features = config?.features;
1206
1574
  const pickerModels = features?.modelPicker.enabled ? features.modelPicker.models : [];
1207
- const [model, setModel] = useState5(void 0);
1208
- useEffect4(() => {
1575
+ const [model, setModel] = useState6(void 0);
1576
+ const composerFocus = useRef4(null);
1577
+ const focusComposer = () => composerFocus.current?.();
1578
+ useEffect5(() => {
1209
1579
  const node = scrollRef.current;
1210
1580
  if (!node) return;
1211
1581
  const onScroll = () => {
@@ -1214,14 +1584,14 @@ function ChatPanel({
1214
1584
  node.addEventListener("scroll", onScroll, { passive: true });
1215
1585
  return () => node.removeEventListener("scroll", onScroll);
1216
1586
  }, []);
1217
- useEffect4(() => {
1587
+ useEffect5(() => {
1218
1588
  const node = scrollRef.current;
1219
1589
  if (!node || !atBottomRef.current) return;
1220
1590
  node.scrollTop = node.scrollHeight;
1221
1591
  }, [messages]);
1222
1592
  const name = config?.branding.name ?? "Assistant";
1223
1593
  const mode = resolveMode(theme?.mode);
1224
- return /* @__PURE__ */ jsxs5(
1594
+ return /* @__PURE__ */ jsxs6(
1225
1595
  Card,
1226
1596
  {
1227
1597
  className: cn(
@@ -1234,44 +1604,49 @@ function ChatPanel({
1234
1604
  style: themeToCssVars(theme),
1235
1605
  "data-frankie-mode": mode,
1236
1606
  children: [
1237
- header === false ? null : header !== void 0 ? header : /* @__PURE__ */ jsxs5(CardHeader, { children: [
1238
- /* @__PURE__ */ jsx14(Avatar, { ...config?.branding.avatarUrl ? { src: config.branding.avatarUrl } : {}, name }),
1239
- /* @__PURE__ */ jsxs5("div", { className: "min-w-0 flex-1", children: [
1240
- /* @__PURE__ */ jsx14(CardTitle, { children: name }),
1241
- config?.branding.tagline ? /* @__PURE__ */ jsx14(CardDescription, { children: config.branding.tagline }) : null
1607
+ header === false ? null : header !== void 0 ? header : /* @__PURE__ */ jsxs6(CardHeader, { children: [
1608
+ /* @__PURE__ */ jsx15(Avatar, { ...config?.branding.avatarUrl ? { src: config.branding.avatarUrl } : {}, name }),
1609
+ /* @__PURE__ */ jsxs6("div", { className: "min-w-0 flex-1", children: [
1610
+ /* @__PURE__ */ jsx15(CardTitle, { children: name }),
1611
+ config?.branding.tagline ? /* @__PURE__ */ jsx15(CardDescription, { children: config.branding.tagline }) : null
1242
1612
  ] }),
1243
- /* @__PURE__ */ jsx14(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ jsx14(RotateCcw, {}) }),
1244
- onClose ? /* @__PURE__ */ jsx14(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx14(X3, {}) }) : null
1613
+ /* @__PURE__ */ jsx15(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ jsx15(RotateCcw, {}) }),
1614
+ onClose ? /* @__PURE__ */ jsx15(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx15(X3, {}) }) : null
1245
1615
  ] }),
1246
- /* @__PURE__ */ jsx14(AgentContext, {}),
1247
- /* @__PURE__ */ jsx14(CardContent, { className: "frankie-conversation-content", children: /* @__PURE__ */ jsx14(ScrollArea, { ref: scrollRef, className: "frankie-transcript flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */ jsxs5("div", { className: "frankie-config-error", role: "alert", children: [
1248
- /* @__PURE__ */ jsx14("strong", { children: "Frankie couldn\u2019t start" }),
1249
- /* @__PURE__ */ jsx14("p", { children: configError }),
1250
- /* @__PURE__ */ jsx14("p", { children: "Refresh this page after checking the workspace\u2019s allowed origin and sign-in settings." })
1251
- ] }) : loading ? /* @__PURE__ */ jsx14("p", { className: "text-sm text-muted-foreground", children: "Starting Frankie\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-4", children: [
1252
- /* @__PURE__ */ jsx14("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
1253
- /* @__PURE__ */ jsx14(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
1254
- ] }) : /* @__PURE__ */ jsx14("div", { className: "frankie-message-list flex flex-col gap-4", children: messages.map(
1255
- (message) => renderMessage ? /* @__PURE__ */ jsx14("div", { children: renderMessage(message) }, message.id) : /* @__PURE__ */ jsx14(
1616
+ /* @__PURE__ */ jsx15(AgentContext, {}),
1617
+ /* @__PURE__ */ jsx15(CardContent, { className: "frankie-conversation-content", children: /* @__PURE__ */ jsx15(ScrollArea, { ref: scrollRef, className: "frankie-transcript flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */ jsxs6("div", { className: "frankie-config-error", role: "alert", children: [
1618
+ /* @__PURE__ */ jsx15("strong", { children: "Frankie couldn\u2019t start" }),
1619
+ /* @__PURE__ */ jsx15("p", { children: configError }),
1620
+ /* @__PURE__ */ jsx15("p", { children: "Refresh this page after checking the workspace\u2019s allowed origin and sign-in settings." })
1621
+ ] }) : loading ? /* @__PURE__ */ jsx15("p", { className: "text-sm text-muted-foreground", children: "Starting Frankie\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs6("div", { className: "flex flex-col gap-4", children: [
1622
+ /* @__PURE__ */ jsx15("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
1623
+ /* @__PURE__ */ jsx15(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
1624
+ ] }) : /* @__PURE__ */ jsx15("div", { className: "frankie-message-list flex flex-col gap-4", children: messages.map(
1625
+ (message, index) => renderMessage ? /* @__PURE__ */ jsx15("div", { children: renderMessage(message) }, message.id) : /* @__PURE__ */ jsx15(
1256
1626
  MessageBubble,
1257
1627
  {
1258
1628
  message,
1259
1629
  assistantName: name,
1260
1630
  ...config?.branding.avatarUrl ? { avatarUrl: config.branding.avatarUrl } : {},
1261
1631
  showToolActivity: config?.showToolActivity ?? true,
1262
- ...renderTool ? { renderTool } : {}
1632
+ ...renderTool ? { renderTool } : {},
1633
+ onChoose: send,
1634
+ onRequestInput: focusComposer,
1635
+ active: index === messages.length - 1,
1636
+ busy
1263
1637
  },
1264
1638
  message.id
1265
1639
  )
1266
1640
  ) }) }) }),
1267
- /* @__PURE__ */ jsxs5(CardFooter, { className: "frankie-composer-footer flex flex-col gap-2", children: [
1268
- error ? /* @__PURE__ */ jsx14("p", { className: "w-full text-xs text-red-600 dark:text-red-400", children: error }) : null,
1269
- /* @__PURE__ */ jsx14("div", { className: "w-full", children: /* @__PURE__ */ jsx14(
1641
+ /* @__PURE__ */ jsxs6(CardFooter, { className: "frankie-composer-footer flex flex-col gap-2", children: [
1642
+ error ? /* @__PURE__ */ jsx15("p", { className: "w-full text-xs text-red-600 dark:text-red-400", children: error }) : null,
1643
+ /* @__PURE__ */ jsx15("div", { className: "w-full", children: /* @__PURE__ */ jsx15(
1270
1644
  Composer,
1271
1645
  {
1272
1646
  onSend: send,
1273
1647
  onStop: stop,
1274
1648
  busy,
1649
+ focusHandleRef: composerFocus,
1275
1650
  disabled: loading || restoring || Boolean(configError),
1276
1651
  ...placeholder ? { placeholder } : {},
1277
1652
  ...autoFocus ? { autoFocus } : {},
@@ -1287,7 +1662,7 @@ function ChatPanel({
1287
1662
  }
1288
1663
 
1289
1664
  // src/components/chat/FrankieWidget.tsx
1290
- import { jsx as jsx15, jsxs as jsxs6 } from "react/jsx-runtime";
1665
+ import { jsx as jsx16, jsxs as jsxs7 } from "react/jsx-runtime";
1291
1666
  function FrankieWidget({
1292
1667
  position = "bottom-right",
1293
1668
  offset = 24,
@@ -1298,15 +1673,17 @@ function FrankieWidget({
1298
1673
  renderLauncher,
1299
1674
  ...panelProps
1300
1675
  }) {
1301
- const { config } = useFrankie();
1676
+ const { config, injectStyles } = useFrankie();
1302
1677
  const theme = useFrankieTheme();
1303
- const [uncontrolledOpen, setUncontrolledOpen] = useState6(defaultOpen);
1678
+ const rootRef = useRef5(null);
1679
+ const stylesReady = useStylesReady(rootRef, void 0, injectStyles);
1680
+ const [uncontrolledOpen, setUncontrolledOpen] = useState7(defaultOpen);
1304
1681
  const open = controlledOpen ?? uncontrolledOpen;
1305
1682
  const setOpen = (next) => {
1306
1683
  if (controlledOpen === void 0) setUncontrolledOpen(next);
1307
1684
  onOpenChange?.(next);
1308
1685
  };
1309
- useEffect5(() => {
1686
+ useEffect6(() => {
1310
1687
  if (!open) return;
1311
1688
  const onKeyDown = (event) => {
1312
1689
  if (event.key === "Escape") setOpen(false);
@@ -1317,15 +1694,17 @@ function FrankieWidget({
1317
1694
  const label = config?.branding.launcherLabel ?? "Need a hand?";
1318
1695
  const mode = resolveMode(theme?.mode);
1319
1696
  const side = position === "bottom-right" ? { right: offset } : { left: offset };
1320
- return /* @__PURE__ */ jsx15(
1697
+ return /* @__PURE__ */ jsx16(
1321
1698
  "div",
1322
1699
  {
1700
+ ref: rootRef,
1323
1701
  className: cn("frankie-root frankie-widget", mode === "dark" && "dark"),
1324
- style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex, pointerEvents: "none" },
1702
+ style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex, pointerEvents: "none", ...stylesReady ? null : { opacity: 0 } },
1325
1703
  "data-frankie-mode": mode,
1326
- children: /* @__PURE__ */ jsxs6("div", { className: cn("flex flex-col gap-3", position === "bottom-left" ? "items-start" : "items-end"), children: [
1327
- open ? /* @__PURE__ */ jsx15("div", { style: { pointerEvents: "auto", maxHeight: "calc(100dvh - 100px)", overflow: "hidden" }, children: /* @__PURE__ */ jsx15(ChatPanel, { ...panelProps, className: cn("frankie-widget-panel", panelProps.className), onClose: () => setOpen(false), autoFocus: true }) }) : null,
1328
- /* @__PURE__ */ jsx15("div", { style: { pointerEvents: "auto" }, children: renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */ jsx15(
1704
+ "data-frankie-styles": stylesReady ? "ready" : "pending",
1705
+ children: /* @__PURE__ */ jsxs7("div", { className: cn("flex flex-col gap-3", position === "bottom-left" ? "items-start" : "items-end"), children: [
1706
+ open ? /* @__PURE__ */ jsx16("div", { style: { pointerEvents: "auto", maxHeight: "calc(100dvh - 100px)", overflow: "hidden" }, children: /* @__PURE__ */ jsx16(ChatPanel, { ...panelProps, className: cn("frankie-widget-panel", panelProps.className), onClose: () => setOpen(false), autoFocus: true }) }) : null,
1707
+ /* @__PURE__ */ jsx16("div", { style: { pointerEvents: "auto" }, children: renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */ jsx16(
1329
1708
  Button,
1330
1709
  {
1331
1710
  size: "icon",
@@ -1334,7 +1713,7 @@ function FrankieWidget({
1334
1713
  "aria-expanded": open,
1335
1714
  "aria-label": open ? "Close the assistant" : label,
1336
1715
  title: label,
1337
- children: open ? /* @__PURE__ */ jsx15(X4, { className: "size-5" }) : /* @__PURE__ */ jsx15(MessageCircle, { className: "size-5" })
1716
+ children: open ? /* @__PURE__ */ jsx16(X4, { className: "size-5" }) : /* @__PURE__ */ jsx16(MessageCircle, { className: "size-5" })
1338
1717
  }
1339
1718
  ) })
1340
1719
  ] })
@@ -1343,39 +1722,45 @@ function FrankieWidget({
1343
1722
  }
1344
1723
 
1345
1724
  // src/components/chat/FrankieInline.tsx
1346
- import { jsx as jsx16 } from "react/jsx-runtime";
1725
+ import { useRef as useRef6 } from "react";
1726
+ import { jsx as jsx17 } from "react/jsx-runtime";
1347
1727
  function FrankieInline({ className, style, ...panelProps }) {
1728
+ const { injectStyles } = useFrankie();
1348
1729
  const theme = useFrankieTheme();
1349
1730
  const mode = resolveMode(theme?.mode);
1350
- return /* @__PURE__ */ jsx16(
1731
+ const rootRef = useRef6(null);
1732
+ const stylesReady = useStylesReady(rootRef, void 0, injectStyles);
1733
+ return /* @__PURE__ */ jsx17(
1351
1734
  "div",
1352
1735
  {
1736
+ ref: rootRef,
1353
1737
  className: cn("frankie-root frankie-inline", mode === "dark" && "dark", className),
1354
- style: { ...themeToCssVars(theme), display: "flex", height: "100%", width: "100%", ...style },
1738
+ style: { ...themeToCssVars(theme), display: "flex", height: "100%", width: "100%", ...style, ...stylesReady ? null : { opacity: 0 } },
1355
1739
  "data-frankie-mode": mode,
1356
- children: /* @__PURE__ */ jsx16(ChatPanel, { ...panelProps, fill: true })
1740
+ "data-frankie-styles": stylesReady ? "ready" : "pending",
1741
+ children: /* @__PURE__ */ jsx17(ChatPanel, { ...panelProps, fill: true })
1357
1742
  }
1358
1743
  );
1359
1744
  }
1360
1745
 
1361
1746
  // src/components/chat/FrankieChatPage.tsx
1362
- import { useCallback as useCallback3, useEffect as useEffect6, useRef as useRef5, useState as useState7 } from "react";
1747
+ import { useCallback as useCallback3, useEffect as useEffect7, useRef as useRef7, useState as useState8 } from "react";
1363
1748
  import { ChevronsLeft, ChevronsRight, History, Menu, Plus as Plus2, Settings, X as X5 } from "lucide-react";
1364
- import { Fragment as Fragment3, jsx as jsx17, jsxs as jsxs7 } from "react/jsx-runtime";
1749
+ import { Fragment as Fragment3, jsx as jsx18, jsxs as jsxs8 } from "react/jsx-runtime";
1365
1750
  function PageEmptyState() {
1366
1751
  const { config } = useFrankie();
1367
1752
  const { send, busy, restoring } = useFrankieChat();
1368
1753
  const name = config?.branding.name ?? "Frankie";
1369
- return /* @__PURE__ */ jsxs7("section", { className: "frankie-page-empty", "aria-label": "Start a conversation", children: [
1370
- /* @__PURE__ */ jsx17("span", { className: "frankie-page-avatar", "aria-hidden": true, children: name.trim().slice(0, 1).toUpperCase() }),
1371
- /* @__PURE__ */ jsxs7("div", { children: [
1372
- /* @__PURE__ */ jsx17("p", { className: "frankie-page-kicker", children: "Your product assistant" }),
1373
- /* @__PURE__ */ jsx17("h1", { children: "How can I help?" }),
1374
- /* @__PURE__ */ jsx17("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
1754
+ return /* @__PURE__ */ jsxs8("section", { className: "frankie-page-empty", "aria-label": "Start a conversation", children: [
1755
+ /* @__PURE__ */ jsx18("span", { className: "frankie-page-avatar", "aria-hidden": true, children: name.trim().slice(0, 1).toUpperCase() }),
1756
+ /* @__PURE__ */ jsxs8("div", { children: [
1757
+ /* @__PURE__ */ jsx18("p", { className: "frankie-page-kicker", children: "Your product assistant" }),
1758
+ /* @__PURE__ */ jsx18("h1", { children: "How can I help?" }),
1759
+ /* @__PURE__ */ jsx18("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
1375
1760
  ] }),
1376
- /* @__PURE__ */ jsx17("div", { className: "frankie-page-prompts", children: config?.suggestedPrompts?.slice(0, 3).map((prompt) => /* @__PURE__ */ jsxs7("button", { type: "button", disabled: busy || restoring, onClick: () => void send(prompt), children: [
1377
- /* @__PURE__ */ jsx17("strong", { children: prompt }),
1378
- /* @__PURE__ */ jsx17("b", { "aria-hidden": true, children: "\u2197" })
1761
+ /* @__PURE__ */ jsx18("div", { className: "frankie-page-prompts", children: config?.suggestedPrompts?.slice(0, 3).map((prompt) => /* @__PURE__ */ jsxs8("button", { type: "button", disabled: busy || restoring, onClick: () => void send(prompt), children: [
1762
+ /* @__PURE__ */ jsx18("strong", { children: prompt }),
1763
+ /* @__PURE__ */ jsx18("b", { "aria-hidden": true, children: "\u2197" })
1379
1764
  ] }, prompt)) })
1380
1765
  ] });
1381
1766
  }
@@ -1383,21 +1768,21 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
1383
1768
  const { client, config } = useFrankie();
1384
1769
  const { busy, messages } = useFrankieChat();
1385
1770
  const theme = useFrankieTheme();
1386
- const [conversations, setConversations] = useState7([]);
1387
- const [historyError, setHistoryError] = useState7("");
1388
- const [historyLoading, setHistoryLoading] = useState7(false);
1389
- const [drawerOpen, setDrawerOpen] = useState7(false);
1390
- const [collapsed, setCollapsed] = useState7(() => {
1771
+ const [conversations, setConversations] = useState8([]);
1772
+ const [historyError, setHistoryError] = useState8("");
1773
+ const [historyLoading, setHistoryLoading] = useState8(false);
1774
+ const [drawerOpen, setDrawerOpen] = useState8(false);
1775
+ const [collapsed, setCollapsed] = useState8(() => {
1391
1776
  try {
1392
1777
  return window.localStorage.getItem("frankie.history-collapsed") === "1";
1393
1778
  } catch {
1394
1779
  return false;
1395
1780
  }
1396
1781
  });
1397
- const [query, setQuery] = useState7("");
1398
- const dialog = useRef5(null);
1399
- const menu = useRef5(null);
1400
- const requestVersion = useRef5(0);
1782
+ const [query, setQuery] = useState8("");
1783
+ const dialog = useRef7(null);
1784
+ const menu = useRef7(null);
1785
+ const requestVersion = useRef7(0);
1401
1786
  const refresh = useCallback3(async () => {
1402
1787
  if (!showHistory || !config || config.allowAnonymous) {
1403
1788
  setHistoryError("");
@@ -1419,19 +1804,19 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
1419
1804
  if (version === requestVersion.current) setHistoryLoading(false);
1420
1805
  }
1421
1806
  }, [client, showHistory, config?.allowAnonymous]);
1422
- useEffect6(() => {
1807
+ useEffect7(() => {
1423
1808
  if (!busy) void refresh();
1424
1809
  return () => {
1425
1810
  requestVersion.current++;
1426
1811
  };
1427
1812
  }, [refresh, busy]);
1428
- useEffect6(() => {
1813
+ useEffect7(() => {
1429
1814
  try {
1430
1815
  window.localStorage.setItem("frankie.history-collapsed", collapsed ? "1" : "0");
1431
1816
  } catch {
1432
1817
  }
1433
1818
  }, [collapsed]);
1434
- useEffect6(() => {
1819
+ useEffect7(() => {
1435
1820
  const node = dialog.current;
1436
1821
  if (drawerOpen) {
1437
1822
  node?.showModal?.();
@@ -1449,79 +1834,81 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
1449
1834
  close();
1450
1835
  }
1451
1836
  };
1452
- const history = /* @__PURE__ */ jsxs7(Fragment3, { children: [
1453
- /* @__PURE__ */ jsxs7("div", { className: "frankie-history-heading", children: [
1454
- /* @__PURE__ */ jsx17("strong", { children: "Conversations" }),
1455
- /* @__PURE__ */ jsx17("button", { type: "button", className: "frankie-mobile-only", "aria-label": "Close conversations", onClick: close, children: /* @__PURE__ */ jsx17(X5, { size: 18 }) })
1837
+ const history = /* @__PURE__ */ jsxs8(Fragment3, { children: [
1838
+ /* @__PURE__ */ jsxs8("div", { className: "frankie-history-heading", children: [
1839
+ /* @__PURE__ */ jsx18("strong", { children: "Conversations" }),
1840
+ /* @__PURE__ */ jsx18("button", { type: "button", className: "frankie-mobile-only", "aria-label": "Close conversations", onClick: close, children: /* @__PURE__ */ jsx18(X5, { size: 18 }) })
1456
1841
  ] }),
1457
- /* @__PURE__ */ jsxs7("button", { type: "button", className: "frankie-new-chat", disabled: busy, onClick: () => {
1842
+ /* @__PURE__ */ jsxs8("button", { type: "button", className: "frankie-new-chat", disabled: busy, onClick: () => {
1458
1843
  onNew();
1459
1844
  close();
1460
1845
  }, children: [
1461
- /* @__PURE__ */ jsx17(Plus2, { size: 17 }),
1846
+ /* @__PURE__ */ jsx18(Plus2, { size: 17 }),
1462
1847
  "New chat"
1463
1848
  ] }),
1464
- /* @__PURE__ */ jsxs7("label", { className: "frankie-history-search", children: [
1465
- /* @__PURE__ */ jsx17("span", { children: "Search conversations" }),
1466
- /* @__PURE__ */ jsx17("input", { type: "search", placeholder: "Search chats\u2026", value: query, onChange: (event) => setQuery(event.target.value) })
1849
+ /* @__PURE__ */ jsxs8("label", { className: "frankie-history-search", children: [
1850
+ /* @__PURE__ */ jsx18("span", { children: "Search conversations" }),
1851
+ /* @__PURE__ */ jsx18("input", { type: "search", placeholder: "Search chats\u2026", value: query, onChange: (event) => setQuery(event.target.value) })
1467
1852
  ] }),
1468
- /* @__PURE__ */ jsx17("nav", { className: "frankie-history-list", "aria-label": "Conversations", children: conversations.filter((item) => item.title.toLowerCase().includes(query.toLowerCase())).map((item) => /* @__PURE__ */ jsxs7("button", { type: "button", "aria-current": item.id === currentId ? "page" : void 0, disabled: busy, onClick: () => select(item.id), children: [
1469
- /* @__PURE__ */ jsx17("span", { children: item.title }),
1470
- /* @__PURE__ */ jsx17("small", { children: new Date(item.updatedAt).toLocaleDateString() })
1853
+ /* @__PURE__ */ jsx18("nav", { className: "frankie-history-list", "aria-label": "Conversations", children: conversations.filter((item) => item.title.toLowerCase().includes(query.toLowerCase())).map((item) => /* @__PURE__ */ jsxs8("button", { type: "button", "aria-current": item.id === currentId ? "page" : void 0, disabled: busy, onClick: () => select(item.id), children: [
1854
+ /* @__PURE__ */ jsx18("span", { children: item.title }),
1855
+ /* @__PURE__ */ jsx18("small", { children: new Date(item.updatedAt).toLocaleDateString() })
1471
1856
  ] }, item.id)) }),
1472
- historyLoading ? /* @__PURE__ */ jsx17("p", { role: "status", children: "Loading history\u2026" }) : historyError ? /* @__PURE__ */ jsxs7("p", { role: "status", children: [
1857
+ historyLoading ? /* @__PURE__ */ jsx18("p", { role: "status", children: "Loading history\u2026" }) : historyError ? /* @__PURE__ */ jsxs8("p", { role: "status", children: [
1473
1858
  historyError,
1474
- /* @__PURE__ */ jsx17("button", { type: "button", onClick: () => void refresh(), children: "Try again" })
1475
- ] }) : !conversations.length ? /* @__PURE__ */ jsx17("p", { children: config?.allowAnonymous ? "Sign in to sync conversations across devices." : "Your conversations will appear here." }) : null,
1476
- navigation ? /* @__PURE__ */ jsx17("div", { className: "frankie-host-navigation", children: navigation }) : null
1859
+ /* @__PURE__ */ jsx18("button", { type: "button", onClick: () => void refresh(), children: "Try again" })
1860
+ ] }) : !conversations.length ? /* @__PURE__ */ jsx18("p", { children: config?.allowAnonymous ? "Sign in to sync conversations across devices." : "Your conversations will appear here." }) : null,
1861
+ navigation ? /* @__PURE__ */ jsx18("div", { className: "frankie-host-navigation", children: navigation }) : null
1477
1862
  ] });
1478
- return /* @__PURE__ */ jsxs7("div", { className: "frankie-page-shell" + (resolveMode(theme?.mode) === "dark" ? " dark" : ""), style: themeToCssVars(theme), children: [
1479
- showHistory ? /* @__PURE__ */ jsxs7("aside", { className: "frankie-history-desktop" + (collapsed ? " frankie-history-collapsed" : ""), children: [
1480
- /* @__PURE__ */ jsx17("button", { type: "button", className: "frankie-history-toggle", "aria-label": collapsed ? "Expand conversations" : "Collapse conversations", "aria-expanded": !collapsed, onClick: () => setCollapsed((value) => !value), children: collapsed ? /* @__PURE__ */ jsx17(ChevronsRight, { size: 16 }) : /* @__PURE__ */ jsx17(ChevronsLeft, { size: 16 }) }),
1863
+ return /* @__PURE__ */ jsxs8("div", { className: "frankie-page-shell" + (resolveMode(theme?.mode) === "dark" ? " dark" : ""), style: themeToCssVars(theme), children: [
1864
+ showHistory ? /* @__PURE__ */ jsxs8("aside", { className: "frankie-history-desktop" + (collapsed ? " frankie-history-collapsed" : ""), children: [
1865
+ /* @__PURE__ */ jsx18("button", { type: "button", className: "frankie-history-toggle", "aria-label": collapsed ? "Expand conversations" : "Collapse conversations", "aria-expanded": !collapsed, onClick: () => setCollapsed((value) => !value), children: collapsed ? /* @__PURE__ */ jsx18(ChevronsRight, { size: 16 }) : /* @__PURE__ */ jsx18(ChevronsLeft, { size: 16 }) }),
1481
1866
  collapsed ? null : history
1482
1867
  ] }) : null,
1483
- /* @__PURE__ */ jsxs7("div", { className: "frankie-page-main", children: [
1484
- /* @__PURE__ */ jsxs7("header", { className: "frankie-page-header", children: [
1485
- /* @__PURE__ */ jsxs7("div", { children: [
1486
- showHistory ? /* @__PURE__ */ jsx17("button", { type: "button", className: "frankie-mobile-only", ref: menu, "aria-label": "Open conversations", "aria-haspopup": "dialog", "aria-expanded": drawerOpen, onClick: () => setDrawerOpen(true), children: /* @__PURE__ */ jsx17(Menu, { size: 20 }) }) : null,
1487
- /* @__PURE__ */ jsx17("strong", { className: "frankie-thread-title", children: conversations.find((item) => item.id === currentId)?.title ?? messages.find((message) => message.role === "user")?.text ?? "New chat" })
1868
+ /* @__PURE__ */ jsxs8("div", { className: "frankie-page-main", children: [
1869
+ /* @__PURE__ */ jsxs8("header", { className: "frankie-page-header", children: [
1870
+ /* @__PURE__ */ jsxs8("div", { children: [
1871
+ showHistory ? /* @__PURE__ */ jsx18("button", { type: "button", className: "frankie-mobile-only", ref: menu, "aria-label": "Open conversations", "aria-haspopup": "dialog", "aria-expanded": drawerOpen, onClick: () => setDrawerOpen(true), children: /* @__PURE__ */ jsx18(Menu, { size: 20 }) }) : null,
1872
+ /* @__PURE__ */ jsx18("strong", { className: "frankie-thread-title", children: conversations.find((item) => item.id === currentId)?.title ?? messages.find((message) => message.role === "user")?.text ?? "New chat" })
1488
1873
  ] }),
1489
- /* @__PURE__ */ jsxs7("div", { children: [
1490
- /* @__PURE__ */ jsxs7("button", { type: "button", disabled: busy, onClick: onNew, "aria-label": "Start a new conversation", children: [
1491
- /* @__PURE__ */ jsx17(Plus2, { size: 16 }),
1492
- /* @__PURE__ */ jsx17("span", { children: "New chat" })
1874
+ /* @__PURE__ */ jsxs8("div", { children: [
1875
+ /* @__PURE__ */ jsxs8("button", { type: "button", disabled: busy, onClick: onNew, "aria-label": "Start a new conversation", children: [
1876
+ /* @__PURE__ */ jsx18(Plus2, { size: 16 }),
1877
+ /* @__PURE__ */ jsx18("span", { children: "New chat" })
1493
1878
  ] }),
1494
- /* @__PURE__ */ jsxs7("details", { className: "frankie-chat-settings", children: [
1495
- /* @__PURE__ */ jsx17("summary", { "aria-label": "Chat settings", children: /* @__PURE__ */ jsx17(Settings, { size: 19 }) }),
1496
- /* @__PURE__ */ jsxs7("div", { children: [
1497
- /* @__PURE__ */ jsx17("strong", { children: "Chat settings" }),
1498
- /* @__PURE__ */ jsx17("p", { children: "History syncs automatically for signed-in users." }),
1499
- /* @__PURE__ */ jsxs7("button", { type: "button", onClick: () => void refresh(), disabled: historyLoading || !showHistory, children: [
1500
- /* @__PURE__ */ jsx17(History, { size: 16 }),
1879
+ /* @__PURE__ */ jsxs8("details", { className: "frankie-chat-settings", children: [
1880
+ /* @__PURE__ */ jsx18("summary", { "aria-label": "Chat settings", children: /* @__PURE__ */ jsx18(Settings, { size: 19 }) }),
1881
+ /* @__PURE__ */ jsxs8("div", { children: [
1882
+ /* @__PURE__ */ jsx18("strong", { children: "Chat settings" }),
1883
+ /* @__PURE__ */ jsx18("p", { children: "History syncs automatically for signed-in users." }),
1884
+ /* @__PURE__ */ jsxs8("button", { type: "button", onClick: () => void refresh(), disabled: historyLoading || !showHistory, children: [
1885
+ /* @__PURE__ */ jsx18(History, { size: 16 }),
1501
1886
  "Refresh history"
1502
1887
  ] }),
1503
- /* @__PURE__ */ jsx17("small", { children: "Skills are checked at the start of each message." })
1888
+ /* @__PURE__ */ jsx18("small", { children: "Skills are checked at the start of each message." })
1504
1889
  ] })
1505
1890
  ] })
1506
1891
  ] })
1507
1892
  ] }),
1508
- /* @__PURE__ */ jsx17("div", { className: "frankie-page-canvas", children: /* @__PURE__ */ jsx17(FrankieInline, { ...panel, header: panel.header ?? false, emptyState: panel.emptyState ?? /* @__PURE__ */ jsx17(PageEmptyState, {}) }) })
1893
+ /* @__PURE__ */ jsx18("div", { className: "frankie-page-canvas", children: /* @__PURE__ */ jsx18(FrankieInline, { ...panel, header: panel.header ?? false, emptyState: panel.emptyState ?? /* @__PURE__ */ jsx18(PageEmptyState, {}) }) })
1509
1894
  ] }),
1510
- showHistory ? /* @__PURE__ */ jsx17("dialog", { ref: dialog, className: "frankie-history-dialog frankie-root", "aria-label": "Chat conversations", style: themeToCssVars(theme), onCancel: close, onClick: (event) => {
1895
+ showHistory ? /* @__PURE__ */ jsx18("dialog", { ref: dialog, className: "frankie-history-dialog frankie-root", "aria-label": "Chat conversations", style: themeToCssVars(theme), onCancel: close, onClick: (event) => {
1511
1896
  if (event.target === event.currentTarget) close();
1512
- }, children: /* @__PURE__ */ jsx17("aside", { children: history }) }) : null
1897
+ }, children: /* @__PURE__ */ jsx18("aside", { children: history }) }) : null
1513
1898
  ] });
1514
1899
  }
1515
1900
  function FrankieChatPage({ className, style, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus, showHistory = true, navigation, ...provider }) {
1516
- const [id, setId] = useState7(() => provider.conversationId ?? crypto.randomUUID());
1901
+ const [id, setId] = useState8(() => provider.conversationId ?? crypto.randomUUID());
1517
1902
  const onNew = () => setId(crypto.randomUUID());
1518
- return /* @__PURE__ */ jsx17("div", { className: ["frankie-root", "frankie-chat-page", className].filter(Boolean).join(" "), style: { height: "100%", minHeight: "min(36rem, 100dvh)", width: "100%", ...style }, children: /* @__PURE__ */ jsx17(FrankieProvider, { ...provider, conversationId: id, storageNamespace: (provider.storageNamespace ?? "frankie." + provider.publishableKey) + "." + id, children: /* @__PURE__ */ jsx17(PageChrome, { showHistory, navigation, currentId: id, onSelect: setId, onNew, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus }) }) });
1903
+ const pageRef = useRef7(null);
1904
+ const stylesReady = useStylesReady(pageRef, void 0, provider.injectStyles);
1905
+ return /* @__PURE__ */ jsx18("div", { ref: pageRef, className: ["frankie-root", "frankie-chat-page", className].filter(Boolean).join(" "), style: { height: "100%", minHeight: "min(36rem, 100dvh)", width: "100%", ...style, ...stylesReady ? null : { opacity: 0 } }, "data-frankie-styles": stylesReady ? "ready" : "pending", children: /* @__PURE__ */ jsx18(FrankieProvider, { ...provider, conversationId: id, storageNamespace: (provider.storageNamespace ?? "frankie." + provider.publishableKey) + "." + id, children: /* @__PURE__ */ jsx18(PageChrome, { showHistory, navigation, currentId: id, onSelect: setId, onNew, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus }) }) });
1519
1906
  }
1520
1907
 
1521
1908
  // src/components/ui/separator.tsx
1522
- import { jsx as jsx18 } from "react/jsx-runtime";
1909
+ import { jsx as jsx19 } from "react/jsx-runtime";
1523
1910
  function Separator({ className }) {
1524
- return /* @__PURE__ */ jsx18("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
1911
+ return /* @__PURE__ */ jsx19("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
1525
1912
  }
1526
1913
  export {
1527
1914
  Avatar,
@@ -1534,6 +1921,7 @@ export {
1534
1921
  CardHeader,
1535
1922
  CardTitle,
1536
1923
  ChatPanel,
1924
+ Choices,
1537
1925
  Composer,
1538
1926
  FRANKIE_CLOUD_URL,
1539
1927
  FrankieApiError,
@@ -1548,12 +1936,14 @@ export {
1548
1936
  Separator,
1549
1937
  SuggestedPrompts,
1550
1938
  Textarea,
1939
+ Thinking,
1551
1940
  ToolActivityList,
1552
1941
  ToolActivityRow,
1553
1942
  TypingDots,
1554
1943
  applyEvent,
1555
1944
  buttonVariants,
1556
1945
  cn,
1946
+ extractChoices,
1557
1947
  newAssistantMessage,
1558
1948
  newUserMessage,
1559
1949
  readEventStream,