@frankieai/react 0.2.7 → 0.2.9

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.cjs CHANGED
@@ -31,6 +31,7 @@ __export(index_exports, {
31
31
  CardHeader: () => CardHeader,
32
32
  CardTitle: () => CardTitle,
33
33
  ChatPanel: () => ChatPanel,
34
+ Choices: () => Choices,
34
35
  Composer: () => Composer,
35
36
  FRANKIE_CLOUD_URL: () => FRANKIE_CLOUD_URL,
36
37
  FrankieApiError: () => FrankieApiError,
@@ -51,6 +52,7 @@ __export(index_exports, {
51
52
  applyEvent: () => applyEvent,
52
53
  buttonVariants: () => buttonVariants,
53
54
  cn: () => cn,
55
+ extractChoices: () => extractChoices,
54
56
  newAssistantMessage: () => newAssistantMessage,
55
57
  newUserMessage: () => newUserMessage,
56
58
  readEventStream: () => readEventStream,
@@ -376,6 +378,7 @@ function useFrankieChatState() {
376
378
  const [error, setError] = (0, import_react.useState)(void 0);
377
379
  const [restored, setRestored] = (0, import_react.useState)(false);
378
380
  const [loadedSkills, setLoadedSkills] = (0, import_react.useState)(null);
381
+ const [activeApis, setActiveApis] = (0, import_react.useState)(null);
379
382
  const abortRef = (0, import_react.useRef)(null);
380
383
  const mounted = (0, import_react.useRef)(true);
381
384
  const messagesRef = (0, import_react.useRef)([]);
@@ -447,6 +450,7 @@ function useFrankieChatState() {
447
450
  const assistantId = nextId("a");
448
451
  setError(void 0);
449
452
  setLoadedSkills(null);
453
+ setActiveApis(null);
450
454
  setBusy(true);
451
455
  const next = [
452
456
  ...messagesRef.current,
@@ -461,6 +465,41 @@ function useFrankieChatState() {
461
465
  setMessages(next);
462
466
  const abort = new AbortController();
463
467
  abortRef.current = abort;
468
+ let bufferedText = "";
469
+ let pendingThinking = "";
470
+ let frame = null;
471
+ const raf = typeof requestAnimationFrame === "function" ? requestAnimationFrame : (cb) => setTimeout(() => cb(performance.now()), 16);
472
+ const cancelRaf = typeof cancelAnimationFrame === "function" ? cancelAnimationFrame : clearTimeout;
473
+ const revealTick = () => {
474
+ frame = null;
475
+ let addText = "";
476
+ if (bufferedText) {
477
+ const step = Math.max(3, Math.ceil(bufferedText.length / 6));
478
+ addText = bufferedText.slice(0, step);
479
+ bufferedText = bufferedText.slice(step);
480
+ }
481
+ const addThinking = pendingThinking;
482
+ pendingThinking = "";
483
+ if (addText || addThinking) {
484
+ patchAssistant(assistantId, (message) => ({ ...message, text: message.text + addText, thinking: message.thinking + addThinking }));
485
+ }
486
+ if (bufferedText) scheduleReveal();
487
+ };
488
+ const scheduleReveal = () => {
489
+ if (frame === null) frame = raf(revealTick);
490
+ };
491
+ const flushDeltas = () => {
492
+ if (frame !== null) {
493
+ cancelRaf(frame);
494
+ frame = null;
495
+ }
496
+ if (!bufferedText && !pendingThinking) return;
497
+ const addText = bufferedText;
498
+ const addThinking = pendingThinking;
499
+ bufferedText = "";
500
+ pendingThinking = "";
501
+ patchAssistant(assistantId, (message) => ({ ...message, text: message.text + addText, thinking: message.thinking + addThinking }));
502
+ };
464
503
  try {
465
504
  let receivedText = false;
466
505
  let receivedToolActivity = false;
@@ -477,12 +516,28 @@ function useFrankieChatState() {
477
516
  setLoadedSkills(event.skills);
478
517
  return;
479
518
  }
519
+ if (event.type === "tools_active") {
520
+ setActiveApis(event.apis);
521
+ return;
522
+ }
480
523
  if (event.type === "ping" || event.type === "session") return;
481
- if (event.type === "text_delta") receivedText = true;
524
+ if (event.type === "text_delta") {
525
+ receivedText = true;
526
+ bufferedText += event.text;
527
+ scheduleReveal();
528
+ return;
529
+ }
530
+ if (event.type === "thinking_delta") {
531
+ pendingThinking += event.text;
532
+ scheduleReveal();
533
+ return;
534
+ }
482
535
  if (event.type === "tool_start" || event.type === "tool_end") receivedToolActivity = true;
536
+ flushDeltas();
483
537
  patchAssistant(assistantId, (message) => applyEvent(message, event));
484
538
  }
485
539
  );
540
+ flushDeltas();
486
541
  if (!receivedText) {
487
542
  patchAssistant(assistantId, (message) => ({
488
543
  ...message,
@@ -491,6 +546,7 @@ function useFrankieChatState() {
491
546
  }));
492
547
  }
493
548
  } catch (caught) {
549
+ flushDeltas();
494
550
  if (abort.signal.aborted) {
495
551
  patchAssistant(assistantId, (message) => ({
496
552
  ...message,
@@ -503,6 +559,7 @@ function useFrankieChatState() {
503
559
  patchAssistant(assistantId, (current) => ({ ...current, status: "error", error: message }));
504
560
  }
505
561
  } finally {
562
+ flushDeltas();
506
563
  patchAssistant(
507
564
  assistantId,
508
565
  (message) => message.status === "streaming" ? { ...message, status: "done" } : message
@@ -527,12 +584,13 @@ function useFrankieChatState() {
527
584
  setBusy(false);
528
585
  setRestored(true);
529
586
  setLoadedSkills(null);
587
+ setActiveApis(null);
530
588
  try {
531
589
  if (client.persist) window.localStorage.removeItem(`${client.namespace}.messages`);
532
590
  } catch {
533
591
  }
534
592
  }, [client]);
535
- return { messages, busy, loadedSkills, restoring: !restored, ...error ? { error } : {}, send, stop, reset };
593
+ return { messages, busy, loadedSkills, activeApis, restoring: !restored, ...error ? { error } : {}, send, stop, reset };
536
594
  }
537
595
  function conversationTitle(messages) {
538
596
  const first = messages.find((message) => message.role === "user" && message.text.trim());
@@ -604,8 +662,45 @@ function useFrankieTheme() {
604
662
  }
605
663
 
606
664
  // src/components/chat/FrankieWidget.tsx
607
- var import_lucide_react6 = require("lucide-react");
608
- var import_react9 = require("react");
665
+ var import_lucide_react7 = require("lucide-react");
666
+ var import_react10 = require("react");
667
+
668
+ // src/lib/styles-ready.ts
669
+ var import_react3 = require("react");
670
+ var useIsomorphicLayoutEffect = typeof window === "undefined" ? import_react3.useEffect : import_react3.useLayoutEffect;
671
+ var SENTINEL = "--frankie-styles-ready";
672
+ function useStylesReady(ref, timeoutMs = 600) {
673
+ const [ready, setReady] = (0, import_react3.useState)(false);
674
+ useIsomorphicLayoutEffect(() => {
675
+ if (ready) return;
676
+ const node = ref.current;
677
+ if (!node || typeof window === "undefined" || typeof window.getComputedStyle !== "function") {
678
+ setReady(true);
679
+ return;
680
+ }
681
+ const applied = () => window.getComputedStyle(node).getPropertyValue(SENTINEL).trim() === "1";
682
+ if (applied()) {
683
+ setReady(true);
684
+ return;
685
+ }
686
+ const deadline = Date.now() + timeoutMs;
687
+ let frame = 0;
688
+ const poll = () => {
689
+ if (applied() || Date.now() >= deadline) {
690
+ setReady(true);
691
+ return;
692
+ }
693
+ frame = window.requestAnimationFrame(poll);
694
+ };
695
+ if (typeof window.requestAnimationFrame !== "function") {
696
+ setReady(true);
697
+ return;
698
+ }
699
+ frame = window.requestAnimationFrame(poll);
700
+ return () => window.cancelAnimationFrame(frame);
701
+ }, [ref, ready, timeoutMs]);
702
+ return ready;
703
+ }
609
704
 
610
705
  // src/lib/theme.ts
611
706
  function themeToCssVars(theme) {
@@ -665,8 +760,8 @@ function Button({ className, variant, size, asChild = false, ...props }) {
665
760
  }
666
761
 
667
762
  // src/components/chat/ChatPanel.tsx
668
- var import_lucide_react5 = require("lucide-react");
669
- var import_react8 = require("react");
763
+ var import_lucide_react6 = require("lucide-react");
764
+ var import_react9 = require("react");
670
765
 
671
766
  // src/components/ui/avatar.tsx
672
767
  var import_jsx_runtime3 = require("react/jsx-runtime");
@@ -716,9 +811,9 @@ function CardFooter({ className, ...props }) {
716
811
  }
717
812
 
718
813
  // src/components/ui/scroll-area.tsx
719
- var import_react3 = require("react");
814
+ var import_react4 = require("react");
720
815
  var import_jsx_runtime5 = require("react/jsx-runtime");
721
- var ScrollArea = (0, import_react3.forwardRef)(
816
+ var ScrollArea = (0, import_react4.forwardRef)(
722
817
  function ScrollArea2({ className, ...props }, ref) {
723
818
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { ref, className: cn("frankie-scroll overflow-y-auto overscroll-contain", className), ...props });
724
819
  }
@@ -729,7 +824,7 @@ var import_lucide_react = require("lucide-react");
729
824
  var import_jsx_runtime6 = require("react/jsx-runtime");
730
825
  function AgentContext() {
731
826
  const { loading, configError } = useFrankie();
732
- const { loadedSkills, busy, restoring, error } = useFrankieChat();
827
+ const { loadedSkills, activeApis, busy, restoring, error } = useFrankieChat();
733
828
  const status = configError || error ? "Unavailable" : loading || restoring ? "Connecting" : busy && loadedSkills === null ? "Checking" : loadedSkills?.length ? "Active" : loadedSkills ? "Tools only" : "Ready";
734
829
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "frankie-agent-context-wrap", children: [
735
830
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("details", { className: "frankie-agent-context", children: [
@@ -752,7 +847,18 @@ function AgentContext() {
752
847
  skill.revision
753
848
  ] })
754
849
  ] }, skill.id)) }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { children: "Frankie can use the tools approved for this workspace. Drafts and skills without available tools are not loaded." }),
755
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { children: "Published skills and updates are checked on every message. Only approved, available tools can run." })
850
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { children: "Published skills and updates are checked on every message. Only approved, available tools can run." }),
851
+ activeApis?.length ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ul", { className: "frankie-active-apis", children: activeApis.map((api) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("li", { children: [
852
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: api.name }),
853
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("small", { children: [
854
+ "spec ",
855
+ api.specHash,
856
+ " \xB7 ",
857
+ api.operationCount,
858
+ " ",
859
+ api.operationCount === 1 ? "operation" : "operations"
860
+ ] })
861
+ ] }, api.name)) }) : null
756
862
  ] })
757
863
  ] }),
758
864
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "frankie-sr-only", role: "status", children: loadedSkills === null ? status : `${loadedSkills.length} ${loadedSkills.length === 1 ? "skill" : "skills"} loaded. ${status}.` })
@@ -761,12 +867,12 @@ function AgentContext() {
761
867
 
762
868
  // src/components/chat/Composer.tsx
763
869
  var import_lucide_react2 = require("lucide-react");
764
- var import_react5 = require("react");
870
+ var import_react6 = require("react");
765
871
 
766
872
  // src/components/ui/textarea.tsx
767
- var import_react4 = require("react");
873
+ var import_react5 = require("react");
768
874
  var import_jsx_runtime7 = require("react/jsx-runtime");
769
- var Textarea = (0, import_react4.forwardRef)(
875
+ var Textarea = (0, import_react5.forwardRef)(
770
876
  function Textarea2({ className, ...props }, ref) {
771
877
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
772
878
  "textarea",
@@ -785,21 +891,28 @@ var Textarea = (0, import_react4.forwardRef)(
785
891
  // src/components/chat/Composer.tsx
786
892
  var import_jsx_runtime8 = require("react/jsx-runtime");
787
893
  var MAX_ROWS_PX = 140;
788
- function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, attachments, modelPicker }) {
789
- const [value, setValue] = (0, import_react5.useState)("");
790
- const [files, setFiles] = (0, import_react5.useState)([]);
791
- const [fileError, setFileError] = (0, import_react5.useState)(void 0);
792
- const textareaRef = (0, import_react5.useRef)(null);
793
- const fileInputRef = (0, import_react5.useRef)(null);
894
+ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, attachments, modelPicker, focusHandleRef }) {
895
+ const [value, setValue] = (0, import_react6.useState)("");
896
+ const [files, setFiles] = (0, import_react6.useState)([]);
897
+ const [fileError, setFileError] = (0, import_react6.useState)(void 0);
898
+ const textareaRef = (0, import_react6.useRef)(null);
899
+ const fileInputRef = (0, import_react6.useRef)(null);
900
+ (0, import_react6.useEffect)(() => {
901
+ if (!focusHandleRef) return;
902
+ focusHandleRef.current = () => textareaRef.current?.focus();
903
+ return () => {
904
+ focusHandleRef.current = null;
905
+ };
906
+ }, [focusHandleRef]);
794
907
  const canAttach = Boolean(attachments?.enabled);
795
908
  const models = modelPicker?.models ?? [];
796
- (0, import_react5.useEffect)(() => {
909
+ (0, import_react6.useEffect)(() => {
797
910
  const node = textareaRef.current;
798
911
  if (!node) return;
799
912
  node.style.height = "auto";
800
913
  node.style.height = `${Math.min(node.scrollHeight, MAX_ROWS_PX)}px`;
801
914
  }, [value]);
802
- const addFiles = (0, import_react5.useCallback)(
915
+ const addFiles = (0, import_react6.useCallback)(
803
916
  async (list) => {
804
917
  if (!list || !attachments) return;
805
918
  setFileError(void 0);
@@ -832,7 +945,7 @@ function Composer({ onSend, onStop, busy, placeholder, disabled, autoFocus, atta
832
945
  [attachments, files.length]
833
946
  );
834
947
  const removeFile = (id) => setFiles((current) => current.filter((file) => file.id !== id));
835
- const submit = (0, import_react5.useCallback)(() => {
948
+ const submit = (0, import_react6.useCallback)(() => {
836
949
  const text = value.trim();
837
950
  if (!text && files.length === 0 || busy || disabled) return;
838
951
  setValue("");
@@ -962,15 +1075,77 @@ function readAsBase64(file) {
962
1075
  }
963
1076
 
964
1077
  // src/components/chat/MessageBubble.tsx
965
- var import_lucide_react4 = require("lucide-react");
966
- var import_react7 = require("react");
1078
+ var import_lucide_react5 = require("lucide-react");
1079
+ var import_react8 = require("react");
967
1080
 
968
- // src/components/chat/Markdown.tsx
969
- var import_react6 = require("react");
1081
+ // src/lib/choices.ts
1082
+ var CLOSED = /```frankie:choices[ \t]*\r?\n([\s\S]*?)```/;
1083
+ var OPEN = /```frankie:choices[ \t]*\r?\n?[\s\S]*$/;
1084
+ function extractChoices(text) {
1085
+ const closed = CLOSED.exec(text);
1086
+ if (closed) {
1087
+ const options = parseOptionLines(closed[1] ?? "");
1088
+ const body = (text.slice(0, closed.index) + text.slice(closed.index + closed[0].length)).replace(/\n{3,}/g, "\n\n").trimEnd();
1089
+ return { body, options: options.length ? options : null };
1090
+ }
1091
+ const open = OPEN.exec(text);
1092
+ if (open) return { body: text.slice(0, open.index).trimEnd(), options: null };
1093
+ return { body: text, options: null };
1094
+ }
1095
+ function parseOptionLines(block) {
1096
+ return block.split("\n").map((line) => line.trim().replace(/^([-*+]|\d+[.)])\s+/, "").trim()).filter(Boolean);
1097
+ }
1098
+
1099
+ // src/components/chat/Choices.tsx
1100
+ var import_lucide_react3 = require("lucide-react");
970
1101
  var import_jsx_runtime9 = require("react/jsx-runtime");
971
- function Markdown({ text, className }) {
972
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: cn("frankie-prose", className), children: renderBlocks(text) });
1102
+ function Choices({
1103
+ options,
1104
+ onPick,
1105
+ onSomethingElse,
1106
+ disabled
1107
+ }) {
1108
+ if (options.length === 0) return null;
1109
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "frankie-choices", role: "group", "aria-label": "Choose an option", children: [
1110
+ options.map((option, index) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1111
+ "button",
1112
+ {
1113
+ type: "button",
1114
+ className: "frankie-choice",
1115
+ disabled,
1116
+ onClick: () => onPick(option),
1117
+ children: [
1118
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "frankie-choice-index", "aria-hidden": true, children: index + 1 }),
1119
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "frankie-choice-label", children: option }),
1120
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.ArrowRight, { className: "frankie-choice-go size-4", "aria-hidden": true })
1121
+ ]
1122
+ },
1123
+ `${index}-${option}`
1124
+ )),
1125
+ onSomethingElse ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1126
+ "button",
1127
+ {
1128
+ type: "button",
1129
+ className: "frankie-choice frankie-choice-other",
1130
+ disabled,
1131
+ onClick: onSomethingElse,
1132
+ children: [
1133
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "frankie-choice-index", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.PencilLine, { className: "size-3.5" }) }),
1134
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "frankie-choice-label", children: "Something else\u2026" }),
1135
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "frankie-choice-hint", "aria-hidden": true, children: "Type your own" })
1136
+ ]
1137
+ }
1138
+ ) : null
1139
+ ] });
973
1140
  }
1141
+
1142
+ // src/components/chat/Markdown.tsx
1143
+ var import_react7 = require("react");
1144
+ var import_jsx_runtime10 = require("react/jsx-runtime");
1145
+ var Markdown = (0, import_react7.memo)(function Markdown2({ text, className }) {
1146
+ const blocks = (0, import_react7.useMemo)(() => renderBlocks(text), [text]);
1147
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: cn("frankie-prose", className), children: blocks });
1148
+ });
974
1149
  function renderBlocks(text) {
975
1150
  const lines = text.split("\n");
976
1151
  const blocks = [];
@@ -979,13 +1154,13 @@ function renderBlocks(text) {
979
1154
  let code = null;
980
1155
  const flushParagraph = () => {
981
1156
  if (paragraph.length === 0) return;
982
- blocks.push(/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { children: renderInline(paragraph.join(" ")) }, `p${blocks.length}`));
1157
+ blocks.push(/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { children: renderInline(paragraph.join(" ")) }, `p${blocks.length}`));
983
1158
  paragraph = [];
984
1159
  };
985
1160
  const flushList = () => {
986
1161
  if (!list) return;
987
- const items = list.items.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("li", { children: renderInline(item) }, index));
988
- blocks.push(list.ordered ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("ol", { children: items }, `l${blocks.length}`) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("ul", { children: items }, `l${blocks.length}`));
1162
+ const items = list.items.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("li", { children: renderInline(item) }, index));
1163
+ blocks.push(list.ordered ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("ol", { children: items }, `l${blocks.length}`) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("ul", { children: items }, `l${blocks.length}`));
989
1164
  list = null;
990
1165
  };
991
1166
  for (const line of lines) {
@@ -993,7 +1168,7 @@ function renderBlocks(text) {
993
1168
  if (fence) {
994
1169
  if (code) {
995
1170
  blocks.push(
996
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("pre", { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
1171
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("pre", { children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
997
1172
  );
998
1173
  code = null;
999
1174
  } else {
@@ -1024,11 +1199,13 @@ function renderBlocks(text) {
1024
1199
  flushList();
1025
1200
  continue;
1026
1201
  }
1027
- const heading = /^#{1,6}\s+(.*)$/.exec(line);
1202
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
1028
1203
  if (heading) {
1029
1204
  flushParagraph();
1030
1205
  flushList();
1031
- blocks.push(/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "font-semibold", children: renderInline(heading[1] ?? "") }, `h${blocks.length}`));
1206
+ const level = (heading[1] ?? "#").length;
1207
+ const Tag = level <= 1 ? "h3" : level === 2 ? "h4" : "h5";
1208
+ blocks.push(/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Tag, { className: "frankie-h", children: renderInline(heading[2] ?? "") }, `h${blocks.length}`));
1032
1209
  continue;
1033
1210
  }
1034
1211
  flushList();
@@ -1036,7 +1213,7 @@ function renderBlocks(text) {
1036
1213
  }
1037
1214
  if (code) {
1038
1215
  blocks.push(
1039
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("pre", { children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
1216
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("pre", { children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("code", { children: code.lines.join("\n") }) }, `c${blocks.length}`)
1040
1217
  );
1041
1218
  }
1042
1219
  flushParagraph();
@@ -1051,28 +1228,28 @@ function renderInline(text) {
1051
1228
  const token = match[0];
1052
1229
  const start = match.index;
1053
1230
  if (start > lastIndex) nodes.push(text.slice(lastIndex, start));
1054
- nodes.push(/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_react6.Fragment, { children: renderToken(token) }, start));
1231
+ nodes.push(/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(import_react7.Fragment, { children: renderToken(token) }, start));
1055
1232
  lastIndex = start + token.length;
1056
1233
  }
1057
1234
  if (lastIndex < text.length) nodes.push(text.slice(lastIndex));
1058
1235
  return nodes;
1059
1236
  }
1060
1237
  function renderToken(token) {
1061
- if (token.startsWith("**")) return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("strong", { children: token.slice(2, -2) });
1062
- if (token.startsWith("`")) return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("code", { children: token.slice(1, -1) });
1238
+ if (token.startsWith("**")) return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("strong", { children: token.slice(2, -2) });
1239
+ if (token.startsWith("`")) return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("code", { children: token.slice(1, -1) });
1063
1240
  const link = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token);
1064
1241
  const href = link ? link[2] ?? "" : token;
1065
1242
  const label = link ? link[1] ?? "" : token;
1066
1243
  if (!/^(https?:\/\/|\/)/.test(href)) return token;
1067
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer nofollow", children: label });
1244
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("a", { href, target: "_blank", rel: "noopener noreferrer nofollow", children: label });
1068
1245
  }
1069
1246
 
1070
1247
  // src/components/chat/ToolActivityList.tsx
1071
- var import_lucide_react3 = require("lucide-react");
1248
+ var import_lucide_react4 = require("lucide-react");
1072
1249
 
1073
1250
  // src/components/ui/badge.tsx
1074
1251
  var import_class_variance_authority2 = require("class-variance-authority");
1075
- var import_jsx_runtime10 = require("react/jsx-runtime");
1252
+ var import_jsx_runtime11 = require("react/jsx-runtime");
1076
1253
  var badgeVariants = (0, import_class_variance_authority2.cva)(
1077
1254
  "inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[0.6875rem] font-medium",
1078
1255
  {
@@ -1092,44 +1269,44 @@ function Badge({
1092
1269
  variant,
1093
1270
  ...props
1094
1271
  }) {
1095
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: cn(badgeVariants({ variant }), className), ...props });
1272
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: cn(badgeVariants({ variant }), className), ...props });
1096
1273
  }
1097
1274
 
1098
1275
  // src/components/chat/ToolActivityList.tsx
1099
- var import_jsx_runtime11 = require("react/jsx-runtime");
1276
+ var import_jsx_runtime12 = require("react/jsx-runtime");
1100
1277
  function ToolActivityList({ tools, renderTool }) {
1101
1278
  if (tools.length === 0) return null;
1102
1279
  const running = tools.some((tool) => tool.status === "running");
1103
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("details", { className: "frankie-tool-activity mt-2", open: running, children: [
1104
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("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: [
1105
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.ChevronDown, { className: "frankie-tool-chevron size-3.5 transition-transform", "aria-hidden": "true" }),
1106
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: running ? "Working" : "Activity" }),
1107
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { className: "text-[11px]", children: [
1280
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("details", { className: "frankie-tool-activity mt-2", open: running, children: [
1281
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("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: [
1282
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.ChevronDown, { className: "frankie-tool-chevron size-3.5 transition-transform", "aria-hidden": "true" }),
1283
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { children: running ? "Working" : "Activity" }),
1284
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "text-[11px]", children: [
1108
1285
  "\xB7 ",
1109
1286
  tools.length,
1110
1287
  " tool call",
1111
1288
  tools.length === 1 ? "" : "s"
1112
1289
  ] })
1113
1290
  ] }),
1114
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("ul", { className: "mt-1 flex flex-col gap-1 border-l border-border pl-2", children: tools.map((tool) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("li", { children: renderTool ? renderTool(tool) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ToolActivityRow, { tool }) }, tool.id)) })
1291
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("ul", { className: "mt-1 flex flex-col gap-1 border-l border-border pl-2", children: tools.map((tool) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("li", { children: renderTool ? renderTool(tool) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ToolActivityRow, { tool }) }, tool.id)) })
1115
1292
  ] });
1116
1293
  }
1117
1294
  function ToolActivityRow({ tool }) {
1118
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
1119
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ToolIcon, { status: tool.status }),
1120
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "truncate", children: tool.label })
1295
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(Badge, { variant: badgeVariant(tool.status), title: tool.detail ?? tool.name, children: [
1296
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ToolIcon, { status: tool.status }),
1297
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "truncate", children: tool.label })
1121
1298
  ] });
1122
1299
  }
1123
1300
  function ToolIcon({ status }) {
1124
1301
  switch (status) {
1125
1302
  case "running":
1126
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.Loader2, { className: "size-3 animate-spin", "aria-hidden": "true" });
1303
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.Loader2, { className: "size-3 animate-spin", "aria-hidden": "true" });
1127
1304
  case "done":
1128
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.Check, { className: "size-3", "aria-hidden": "true" });
1305
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.Check, { className: "size-3", "aria-hidden": "true" });
1129
1306
  case "denied":
1130
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.ShieldAlert, { className: "size-3", "aria-hidden": "true" });
1307
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.ShieldAlert, { className: "size-3", "aria-hidden": "true" });
1131
1308
  default:
1132
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_lucide_react3.X, { className: "size-3", "aria-hidden": "true" });
1309
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.X, { className: "size-3", "aria-hidden": "true" });
1133
1310
  }
1134
1311
  }
1135
1312
  function badgeVariant(status) {
@@ -1140,47 +1317,54 @@ function badgeVariant(status) {
1140
1317
  }
1141
1318
 
1142
1319
  // src/components/chat/MessageBubble.tsx
1143
- var import_jsx_runtime12 = require("react/jsx-runtime");
1144
- function MessageBubble({
1320
+ var import_jsx_runtime13 = require("react/jsx-runtime");
1321
+ var MessageBubble = (0, import_react8.memo)(function MessageBubble2({
1145
1322
  message,
1146
1323
  assistantName,
1147
1324
  avatarUrl,
1148
1325
  showToolActivity,
1149
- renderTool
1326
+ renderTool,
1327
+ onChoose,
1328
+ onRequestInput,
1329
+ active,
1330
+ busy
1150
1331
  }) {
1151
1332
  if (message.role === "user") {
1152
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "frankie-user-message flex flex-col items-end gap-1", children: [
1153
- message.attachments?.length ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "flex max-w-[85%] flex-wrap justify-end gap-1", children: message.attachments.map((file, index) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1333
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-user-message flex flex-col items-end gap-1", children: [
1334
+ message.attachments?.length ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "flex max-w-[85%] flex-wrap justify-end gap-1", children: message.attachments.map((file, index) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1154
1335
  "span",
1155
1336
  {
1156
1337
  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",
1157
1338
  children: [
1158
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.Paperclip, { className: "size-3 shrink-0 opacity-70" }),
1159
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "truncate", children: file.name })
1339
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react5.Paperclip, { className: "size-3 shrink-0 opacity-70" }),
1340
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "truncate", children: file.name })
1160
1341
  ]
1161
1342
  },
1162
1343
  `${file.name}-${index}`
1163
1344
  )) }) : null,
1164
- message.text ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex max-w-[85%] items-end gap-1", children: [
1165
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "frankie-user-bubble rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }),
1166
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(MessageCopyButton, { text: message.text })
1345
+ message.text ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex max-w-[85%] items-end gap-1", children: [
1346
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "frankie-user-bubble rounded-[var(--frankie-radius)] bg-primary px-3 py-2 text-sm text-primary-foreground", children: message.text }),
1347
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(MessageCopyButton, { text: message.text })
1167
1348
  ] }) : null
1168
1349
  ] });
1169
1350
  }
1170
1351
  const empty = message.text.length === 0 && message.tools.length === 0;
1171
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "frankie-assistant-message flex gap-2", children: [
1172
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1", children: [
1173
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName }),
1174
- message.text ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(MessageCopyButton, { text: message.text }) : null
1352
+ const { body, options } = extractChoices(message.text);
1353
+ const showChoices = Boolean(active && onChoose && options && message.status === "done");
1354
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-assistant-message flex gap-2", children: [
1355
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-assistant-actions mt-0.5 flex shrink-0 flex-col items-center gap-1", children: [
1356
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Avatar, { ...avatarUrl ? { src: avatarUrl } : {}, name: assistantName }),
1357
+ body ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(MessageCopyButton, { text: body }) : null
1175
1358
  ] }),
1176
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "frankie-assistant-content min-w-0 max-w-[85%]", children: [
1177
- message.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("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__ */ (0, import_jsx_runtime12.jsx)(TypingDots, {}) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Markdown, { text: message.text, className: "text-sm" }),
1178
- showToolActivity ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null
1359
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "frankie-assistant-content min-w-0 max-w-[85%]", children: [
1360
+ message.status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("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__ */ (0, import_jsx_runtime13.jsx)(TypingDots, {}) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Markdown, { text: body, className: "text-sm" }),
1361
+ showChoices ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Choices, { options: options ?? [], onPick: onChoose, ...onRequestInput ? { onSomethingElse: onRequestInput } : {}, disabled: busy }) : null,
1362
+ showToolActivity ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ToolActivityList, { tools: message.tools, ...renderTool ? { renderTool } : {} }) : null
1179
1363
  ] })
1180
1364
  ] });
1181
- }
1365
+ });
1182
1366
  function MessageCopyButton({ text }) {
1183
- const [copied, setCopied] = (0, import_react7.useState)(false);
1367
+ const [copied, setCopied] = (0, import_react8.useState)(false);
1184
1368
  const copy = async () => {
1185
1369
  try {
1186
1370
  await navigator.clipboard.writeText(text);
@@ -1189,12 +1373,12 @@ function MessageCopyButton({ text }) {
1189
1373
  } catch {
1190
1374
  }
1191
1375
  };
1192
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("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__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.Check, { className: "size-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_lucide_react4.Copy, { className: "size-3.5", "aria-hidden": "true" }) });
1376
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("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__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react5.Check, { className: "size-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react5.Copy, { className: "size-3.5", "aria-hidden": "true" }) });
1193
1377
  }
1194
1378
  function TypingDots() {
1195
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "frankie-thinking inline-flex items-center gap-1 py-2", role: "status", "aria-label": "Thinking", children: [
1196
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "frankie-thinking-label", children: "Thinking" }),
1197
- [0, 1, 2].map((index) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1379
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { className: "frankie-thinking inline-flex items-center gap-1 py-2", role: "status", "aria-label": "Thinking", children: [
1380
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "frankie-thinking-label", children: "Thinking" }),
1381
+ [0, 1, 2].map((index) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1198
1382
  "span",
1199
1383
  {
1200
1384
  className: cn("size-1.5 animate-pulse rounded-full bg-muted-foreground"),
@@ -1206,14 +1390,14 @@ function TypingDots() {
1206
1390
  }
1207
1391
 
1208
1392
  // src/components/chat/SuggestedPrompts.tsx
1209
- var import_jsx_runtime13 = require("react/jsx-runtime");
1393
+ var import_jsx_runtime14 = require("react/jsx-runtime");
1210
1394
  function SuggestedPrompts({
1211
1395
  prompts,
1212
1396
  onPick,
1213
1397
  disabled
1214
1398
  }) {
1215
1399
  if (prompts.length === 0) return null;
1216
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "flex flex-wrap gap-2", children: prompts.map((prompt) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1400
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "flex flex-wrap gap-2", children: prompts.map((prompt) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1217
1401
  Button,
1218
1402
  {
1219
1403
  type: "button",
@@ -1228,7 +1412,7 @@ function SuggestedPrompts({
1228
1412
  }
1229
1413
 
1230
1414
  // src/components/chat/ChatPanel.tsx
1231
- var import_jsx_runtime14 = require("react/jsx-runtime");
1415
+ var import_jsx_runtime15 = require("react/jsx-runtime");
1232
1416
  function ChatPanel({
1233
1417
  className,
1234
1418
  header,
@@ -1244,12 +1428,14 @@ function ChatPanel({
1244
1428
  const { config, configError, loading } = useFrankie();
1245
1429
  const theme = useFrankieTheme();
1246
1430
  const { messages, busy, restoring, error, send, stop, reset } = useFrankieChat();
1247
- const scrollRef = (0, import_react8.useRef)(null);
1248
- const atBottomRef = (0, import_react8.useRef)(true);
1431
+ const scrollRef = (0, import_react9.useRef)(null);
1432
+ const atBottomRef = (0, import_react9.useRef)(true);
1249
1433
  const features = config?.features;
1250
1434
  const pickerModels = features?.modelPicker.enabled ? features.modelPicker.models : [];
1251
- const [model, setModel] = (0, import_react8.useState)(void 0);
1252
- (0, import_react8.useEffect)(() => {
1435
+ const [model, setModel] = (0, import_react9.useState)(void 0);
1436
+ const composerFocus = (0, import_react9.useRef)(null);
1437
+ const focusComposer = () => composerFocus.current?.();
1438
+ (0, import_react9.useEffect)(() => {
1253
1439
  const node = scrollRef.current;
1254
1440
  if (!node) return;
1255
1441
  const onScroll = () => {
@@ -1258,14 +1444,14 @@ function ChatPanel({
1258
1444
  node.addEventListener("scroll", onScroll, { passive: true });
1259
1445
  return () => node.removeEventListener("scroll", onScroll);
1260
1446
  }, []);
1261
- (0, import_react8.useEffect)(() => {
1447
+ (0, import_react9.useEffect)(() => {
1262
1448
  const node = scrollRef.current;
1263
1449
  if (!node || !atBottomRef.current) return;
1264
1450
  node.scrollTop = node.scrollHeight;
1265
1451
  }, [messages]);
1266
1452
  const name = config?.branding.name ?? "Assistant";
1267
1453
  const mode = resolveMode(theme?.mode);
1268
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
1454
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
1269
1455
  Card,
1270
1456
  {
1271
1457
  className: cn(
@@ -1278,44 +1464,49 @@ function ChatPanel({
1278
1464
  style: themeToCssVars(theme),
1279
1465
  "data-frankie-mode": mode,
1280
1466
  children: [
1281
- header === false ? null : header !== void 0 ? header : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(CardHeader, { children: [
1282
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Avatar, { ...config?.branding.avatarUrl ? { src: config.branding.avatarUrl } : {}, name }),
1283
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "min-w-0 flex-1", children: [
1284
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CardTitle, { children: name }),
1285
- config?.branding.tagline ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CardDescription, { children: config.branding.tagline }) : null
1467
+ header === false ? null : header !== void 0 ? header : /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(CardHeader, { children: [
1468
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Avatar, { ...config?.branding.avatarUrl ? { src: config.branding.avatarUrl } : {}, name }),
1469
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "min-w-0 flex-1", children: [
1470
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(CardTitle, { children: name }),
1471
+ config?.branding.tagline ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(CardDescription, { children: config.branding.tagline }) : null
1286
1472
  ] }),
1287
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.RotateCcw, {}) }),
1288
- onClose ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.X, {}) }) : null
1473
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { size: "icon", variant: "ghost", onClick: reset, "aria-label": "Start a new conversation", title: "New conversation", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react6.RotateCcw, {}) }),
1474
+ onClose ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { size: "icon", variant: "ghost", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react6.X, {}) }) : null
1289
1475
  ] }),
1290
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(AgentContext, {}),
1291
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CardContent, { className: "frankie-conversation-content", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ScrollArea, { ref: scrollRef, className: "frankie-transcript flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "frankie-config-error", role: "alert", children: [
1292
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("strong", { children: "Frankie couldn\u2019t start" }),
1293
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { children: configError }),
1294
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { children: "Refresh this page after checking the workspace\u2019s allowed origin and sign-in settings." })
1295
- ] }) : loading ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "text-sm text-muted-foreground", children: "Starting Frankie\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "flex flex-col gap-4", children: [
1296
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
1297
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
1298
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "frankie-message-list flex flex-col gap-4", children: messages.map(
1299
- (message) => renderMessage ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { children: renderMessage(message) }, message.id) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1476
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(AgentContext, {}),
1477
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(CardContent, { className: "frankie-conversation-content", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ScrollArea, { ref: scrollRef, className: "frankie-transcript flex h-full flex-col gap-4 p-4", "aria-live": "polite", children: configError ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "frankie-config-error", role: "alert", children: [
1478
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("strong", { children: "Frankie couldn\u2019t start" }),
1479
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { children: configError }),
1480
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { children: "Refresh this page after checking the workspace\u2019s allowed origin and sign-in settings." })
1481
+ ] }) : loading ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-sm text-muted-foreground", children: "Starting Frankie\u2026" }) : messages.length === 0 ? emptyState ?? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col gap-4", children: [
1482
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-sm text-muted-foreground", children: config?.greeting }),
1483
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(SuggestedPrompts, { prompts: config?.suggestedPrompts ?? [], onPick: send, disabled: busy })
1484
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "frankie-message-list flex flex-col gap-4", children: messages.map(
1485
+ (message, index) => renderMessage ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { children: renderMessage(message) }, message.id) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1300
1486
  MessageBubble,
1301
1487
  {
1302
1488
  message,
1303
1489
  assistantName: name,
1304
1490
  ...config?.branding.avatarUrl ? { avatarUrl: config.branding.avatarUrl } : {},
1305
1491
  showToolActivity: config?.showToolActivity ?? true,
1306
- ...renderTool ? { renderTool } : {}
1492
+ ...renderTool ? { renderTool } : {},
1493
+ onChoose: send,
1494
+ onRequestInput: focusComposer,
1495
+ active: index === messages.length - 1,
1496
+ busy
1307
1497
  },
1308
1498
  message.id
1309
1499
  )
1310
1500
  ) }) }) }),
1311
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(CardFooter, { className: "frankie-composer-footer flex flex-col gap-2", children: [
1312
- error ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "w-full text-xs text-red-600 dark:text-red-400", children: error }) : null,
1313
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1501
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(CardFooter, { className: "frankie-composer-footer flex flex-col gap-2", children: [
1502
+ error ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "w-full text-xs text-red-600 dark:text-red-400", children: error }) : null,
1503
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1314
1504
  Composer,
1315
1505
  {
1316
1506
  onSend: send,
1317
1507
  onStop: stop,
1318
1508
  busy,
1509
+ focusHandleRef: composerFocus,
1319
1510
  disabled: loading || restoring || Boolean(configError),
1320
1511
  ...placeholder ? { placeholder } : {},
1321
1512
  ...autoFocus ? { autoFocus } : {},
@@ -1331,7 +1522,7 @@ function ChatPanel({
1331
1522
  }
1332
1523
 
1333
1524
  // src/components/chat/FrankieWidget.tsx
1334
- var import_jsx_runtime15 = require("react/jsx-runtime");
1525
+ var import_jsx_runtime16 = require("react/jsx-runtime");
1335
1526
  function FrankieWidget({
1336
1527
  position = "bottom-right",
1337
1528
  offset = 24,
@@ -1344,13 +1535,15 @@ function FrankieWidget({
1344
1535
  }) {
1345
1536
  const { config } = useFrankie();
1346
1537
  const theme = useFrankieTheme();
1347
- const [uncontrolledOpen, setUncontrolledOpen] = (0, import_react9.useState)(defaultOpen);
1538
+ const rootRef = (0, import_react10.useRef)(null);
1539
+ const stylesReady = useStylesReady(rootRef);
1540
+ const [uncontrolledOpen, setUncontrolledOpen] = (0, import_react10.useState)(defaultOpen);
1348
1541
  const open = controlledOpen ?? uncontrolledOpen;
1349
1542
  const setOpen = (next) => {
1350
1543
  if (controlledOpen === void 0) setUncontrolledOpen(next);
1351
1544
  onOpenChange?.(next);
1352
1545
  };
1353
- (0, import_react9.useEffect)(() => {
1546
+ (0, import_react10.useEffect)(() => {
1354
1547
  if (!open) return;
1355
1548
  const onKeyDown = (event) => {
1356
1549
  if (event.key === "Escape") setOpen(false);
@@ -1361,15 +1554,17 @@ function FrankieWidget({
1361
1554
  const label = config?.branding.launcherLabel ?? "Need a hand?";
1362
1555
  const mode = resolveMode(theme?.mode);
1363
1556
  const side = position === "bottom-right" ? { right: offset } : { left: offset };
1364
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1557
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1365
1558
  "div",
1366
1559
  {
1560
+ ref: rootRef,
1367
1561
  className: cn("frankie-root frankie-widget", mode === "dark" && "dark"),
1368
- style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex, pointerEvents: "none" },
1562
+ style: { ...themeToCssVars(theme), position: "fixed", bottom: offset, ...side, zIndex, pointerEvents: "none", ...stylesReady ? null : { opacity: 0 } },
1369
1563
  "data-frankie-mode": mode,
1370
- children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: cn("flex flex-col gap-3", position === "bottom-left" ? "items-start" : "items-end"), children: [
1371
- open ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: { pointerEvents: "auto", maxHeight: "calc(100dvh - 100px)", overflow: "hidden" }, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ChatPanel, { ...panelProps, className: cn("frankie-widget-panel", panelProps.className), onClose: () => setOpen(false), autoFocus: true }) }) : null,
1372
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: { pointerEvents: "auto" }, children: renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1564
+ "data-frankie-styles": stylesReady ? "ready" : "pending",
1565
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: cn("flex flex-col gap-3", position === "bottom-left" ? "items-start" : "items-end"), children: [
1566
+ open ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: { pointerEvents: "auto", maxHeight: "calc(100dvh - 100px)", overflow: "hidden" }, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ChatPanel, { ...panelProps, className: cn("frankie-widget-panel", panelProps.className), onClose: () => setOpen(false), autoFocus: true }) }) : null,
1567
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: { pointerEvents: "auto" }, children: renderLauncher ? renderLauncher({ open, toggle: () => setOpen(!open), label }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1373
1568
  Button,
1374
1569
  {
1375
1570
  size: "icon",
@@ -1378,7 +1573,7 @@ function FrankieWidget({
1378
1573
  "aria-expanded": open,
1379
1574
  "aria-label": open ? "Close the assistant" : label,
1380
1575
  title: label,
1381
- children: open ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react6.X, { className: "size-5" }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react6.MessageCircle, { className: "size-5" })
1576
+ children: open ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react7.X, { className: "size-5" }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_lucide_react7.MessageCircle, { className: "size-5" })
1382
1577
  }
1383
1578
  ) })
1384
1579
  ] })
@@ -1387,39 +1582,44 @@ function FrankieWidget({
1387
1582
  }
1388
1583
 
1389
1584
  // src/components/chat/FrankieInline.tsx
1390
- var import_jsx_runtime16 = require("react/jsx-runtime");
1585
+ var import_react11 = require("react");
1586
+ var import_jsx_runtime17 = require("react/jsx-runtime");
1391
1587
  function FrankieInline({ className, style, ...panelProps }) {
1392
1588
  const theme = useFrankieTheme();
1393
1589
  const mode = resolveMode(theme?.mode);
1394
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1590
+ const rootRef = (0, import_react11.useRef)(null);
1591
+ const stylesReady = useStylesReady(rootRef);
1592
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1395
1593
  "div",
1396
1594
  {
1595
+ ref: rootRef,
1397
1596
  className: cn("frankie-root frankie-inline", mode === "dark" && "dark", className),
1398
- style: { ...themeToCssVars(theme), display: "flex", height: "100%", width: "100%", ...style },
1597
+ style: { ...themeToCssVars(theme), display: "flex", height: "100%", width: "100%", ...style, ...stylesReady ? null : { opacity: 0 } },
1399
1598
  "data-frankie-mode": mode,
1400
- children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ChatPanel, { ...panelProps, fill: true })
1599
+ "data-frankie-styles": stylesReady ? "ready" : "pending",
1600
+ children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ChatPanel, { ...panelProps, fill: true })
1401
1601
  }
1402
1602
  );
1403
1603
  }
1404
1604
 
1405
1605
  // src/components/chat/FrankieChatPage.tsx
1406
- var import_react10 = require("react");
1407
- var import_lucide_react7 = require("lucide-react");
1408
- var import_jsx_runtime17 = require("react/jsx-runtime");
1606
+ var import_react12 = require("react");
1607
+ var import_lucide_react8 = require("lucide-react");
1608
+ var import_jsx_runtime18 = require("react/jsx-runtime");
1409
1609
  function PageEmptyState() {
1410
1610
  const { config } = useFrankie();
1411
1611
  const { send, busy, restoring } = useFrankieChat();
1412
1612
  const name = config?.branding.name ?? "Frankie";
1413
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("section", { className: "frankie-page-empty", "aria-label": "Start a conversation", children: [
1414
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "frankie-page-avatar", "aria-hidden": true, children: name.trim().slice(0, 1).toUpperCase() }),
1415
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { children: [
1416
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { className: "frankie-page-kicker", children: "Your product assistant" }),
1417
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h1", { children: "How can I help?" }),
1418
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
1613
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("section", { className: "frankie-page-empty", "aria-label": "Start a conversation", children: [
1614
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "frankie-page-avatar", "aria-hidden": true, children: name.trim().slice(0, 1).toUpperCase() }),
1615
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
1616
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { className: "frankie-page-kicker", children: "Your product assistant" }),
1617
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h1", { children: "How can I help?" }),
1618
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { children: config?.greeting ?? "Ask a question or choose a task to get started." })
1419
1619
  ] }),
1420
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "frankie-page-prompts", children: config?.suggestedPrompts?.slice(0, 3).map((prompt) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("button", { type: "button", disabled: busy || restoring, onClick: () => void send(prompt), children: [
1421
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("strong", { children: prompt }),
1422
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("b", { "aria-hidden": true, children: "\u2197" })
1620
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "frankie-page-prompts", children: config?.suggestedPrompts?.slice(0, 3).map((prompt) => /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("button", { type: "button", disabled: busy || restoring, onClick: () => void send(prompt), children: [
1621
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("strong", { children: prompt }),
1622
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("b", { "aria-hidden": true, children: "\u2197" })
1423
1623
  ] }, prompt)) })
1424
1624
  ] });
1425
1625
  }
@@ -1427,22 +1627,22 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
1427
1627
  const { client, config } = useFrankie();
1428
1628
  const { busy, messages } = useFrankieChat();
1429
1629
  const theme = useFrankieTheme();
1430
- const [conversations, setConversations] = (0, import_react10.useState)([]);
1431
- const [historyError, setHistoryError] = (0, import_react10.useState)("");
1432
- const [historyLoading, setHistoryLoading] = (0, import_react10.useState)(false);
1433
- const [drawerOpen, setDrawerOpen] = (0, import_react10.useState)(false);
1434
- const [collapsed, setCollapsed] = (0, import_react10.useState)(() => {
1630
+ const [conversations, setConversations] = (0, import_react12.useState)([]);
1631
+ const [historyError, setHistoryError] = (0, import_react12.useState)("");
1632
+ const [historyLoading, setHistoryLoading] = (0, import_react12.useState)(false);
1633
+ const [drawerOpen, setDrawerOpen] = (0, import_react12.useState)(false);
1634
+ const [collapsed, setCollapsed] = (0, import_react12.useState)(() => {
1435
1635
  try {
1436
1636
  return window.localStorage.getItem("frankie.history-collapsed") === "1";
1437
1637
  } catch {
1438
1638
  return false;
1439
1639
  }
1440
1640
  });
1441
- const [query, setQuery] = (0, import_react10.useState)("");
1442
- const dialog = (0, import_react10.useRef)(null);
1443
- const menu = (0, import_react10.useRef)(null);
1444
- const requestVersion = (0, import_react10.useRef)(0);
1445
- const refresh = (0, import_react10.useCallback)(async () => {
1641
+ const [query, setQuery] = (0, import_react12.useState)("");
1642
+ const dialog = (0, import_react12.useRef)(null);
1643
+ const menu = (0, import_react12.useRef)(null);
1644
+ const requestVersion = (0, import_react12.useRef)(0);
1645
+ const refresh = (0, import_react12.useCallback)(async () => {
1446
1646
  if (!showHistory || !config || config.allowAnonymous) {
1447
1647
  setHistoryError("");
1448
1648
  return;
@@ -1463,19 +1663,19 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
1463
1663
  if (version === requestVersion.current) setHistoryLoading(false);
1464
1664
  }
1465
1665
  }, [client, showHistory, config?.allowAnonymous]);
1466
- (0, import_react10.useEffect)(() => {
1666
+ (0, import_react12.useEffect)(() => {
1467
1667
  if (!busy) void refresh();
1468
1668
  return () => {
1469
1669
  requestVersion.current++;
1470
1670
  };
1471
1671
  }, [refresh, busy]);
1472
- (0, import_react10.useEffect)(() => {
1672
+ (0, import_react12.useEffect)(() => {
1473
1673
  try {
1474
1674
  window.localStorage.setItem("frankie.history-collapsed", collapsed ? "1" : "0");
1475
1675
  } catch {
1476
1676
  }
1477
1677
  }, [collapsed]);
1478
- (0, import_react10.useEffect)(() => {
1678
+ (0, import_react12.useEffect)(() => {
1479
1679
  const node = dialog.current;
1480
1680
  if (drawerOpen) {
1481
1681
  node?.showModal?.();
@@ -1493,79 +1693,81 @@ function PageChrome({ showHistory, navigation, currentId, onSelect, onNew, ...pa
1493
1693
  close();
1494
1694
  }
1495
1695
  };
1496
- const history = /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
1497
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "frankie-history-heading", children: [
1498
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("strong", { children: "Conversations" }),
1499
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", className: "frankie-mobile-only", "aria-label": "Close conversations", onClick: close, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.X, { size: 18 }) })
1696
+ const history = /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
1697
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "frankie-history-heading", children: [
1698
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("strong", { children: "Conversations" }),
1699
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("button", { type: "button", className: "frankie-mobile-only", "aria-label": "Close conversations", onClick: close, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.X, { size: 18 }) })
1500
1700
  ] }),
1501
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("button", { type: "button", className: "frankie-new-chat", disabled: busy, onClick: () => {
1701
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("button", { type: "button", className: "frankie-new-chat", disabled: busy, onClick: () => {
1502
1702
  onNew();
1503
1703
  close();
1504
1704
  }, children: [
1505
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.Plus, { size: 17 }),
1705
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.Plus, { size: 17 }),
1506
1706
  "New chat"
1507
1707
  ] }),
1508
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("label", { className: "frankie-history-search", children: [
1509
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { children: "Search conversations" }),
1510
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("input", { type: "search", placeholder: "Search chats\u2026", value: query, onChange: (event) => setQuery(event.target.value) })
1708
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("label", { className: "frankie-history-search", children: [
1709
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { children: "Search conversations" }),
1710
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("input", { type: "search", placeholder: "Search chats\u2026", value: query, onChange: (event) => setQuery(event.target.value) })
1511
1711
  ] }),
1512
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("nav", { className: "frankie-history-list", "aria-label": "Conversations", children: conversations.filter((item) => item.title.toLowerCase().includes(query.toLowerCase())).map((item) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("button", { type: "button", "aria-current": item.id === currentId ? "page" : void 0, disabled: busy, onClick: () => select(item.id), children: [
1513
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { children: item.title }),
1514
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("small", { children: new Date(item.updatedAt).toLocaleDateString() })
1712
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("nav", { className: "frankie-history-list", "aria-label": "Conversations", children: conversations.filter((item) => item.title.toLowerCase().includes(query.toLowerCase())).map((item) => /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("button", { type: "button", "aria-current": item.id === currentId ? "page" : void 0, disabled: busy, onClick: () => select(item.id), children: [
1713
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { children: item.title }),
1714
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("small", { children: new Date(item.updatedAt).toLocaleDateString() })
1515
1715
  ] }, item.id)) }),
1516
- historyLoading ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { role: "status", children: "Loading history\u2026" }) : historyError ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("p", { role: "status", children: [
1716
+ historyLoading ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { role: "status", children: "Loading history\u2026" }) : historyError ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("p", { role: "status", children: [
1517
1717
  historyError,
1518
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", onClick: () => void refresh(), children: "Try again" })
1519
- ] }) : !conversations.length ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { children: config?.allowAnonymous ? "Sign in to sync conversations across devices." : "Your conversations will appear here." }) : null,
1520
- navigation ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "frankie-host-navigation", children: navigation }) : null
1718
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("button", { type: "button", onClick: () => void refresh(), children: "Try again" })
1719
+ ] }) : !conversations.length ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { children: config?.allowAnonymous ? "Sign in to sync conversations across devices." : "Your conversations will appear here." }) : null,
1720
+ navigation ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "frankie-host-navigation", children: navigation }) : null
1521
1721
  ] });
1522
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "frankie-page-shell" + (resolveMode(theme?.mode) === "dark" ? " dark" : ""), style: themeToCssVars(theme), children: [
1523
- showHistory ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("aside", { className: "frankie-history-desktop" + (collapsed ? " frankie-history-collapsed" : ""), children: [
1524
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", className: "frankie-history-toggle", "aria-label": collapsed ? "Expand conversations" : "Collapse conversations", "aria-expanded": !collapsed, onClick: () => setCollapsed((value) => !value), children: collapsed ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.ChevronsRight, { size: 16 }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.ChevronsLeft, { size: 16 }) }),
1722
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "frankie-page-shell" + (resolveMode(theme?.mode) === "dark" ? " dark" : ""), style: themeToCssVars(theme), children: [
1723
+ showHistory ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("aside", { className: "frankie-history-desktop" + (collapsed ? " frankie-history-collapsed" : ""), children: [
1724
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("button", { type: "button", className: "frankie-history-toggle", "aria-label": collapsed ? "Expand conversations" : "Collapse conversations", "aria-expanded": !collapsed, onClick: () => setCollapsed((value) => !value), children: collapsed ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.ChevronsRight, { size: 16 }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.ChevronsLeft, { size: 16 }) }),
1525
1725
  collapsed ? null : history
1526
1726
  ] }) : null,
1527
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "frankie-page-main", children: [
1528
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("header", { className: "frankie-page-header", children: [
1529
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { children: [
1530
- showHistory ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", className: "frankie-mobile-only", ref: menu, "aria-label": "Open conversations", "aria-haspopup": "dialog", "aria-expanded": drawerOpen, onClick: () => setDrawerOpen(true), children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.Menu, { size: 20 }) }) : null,
1531
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("strong", { className: "frankie-thread-title", children: conversations.find((item) => item.id === currentId)?.title ?? messages.find((message) => message.role === "user")?.text ?? "New chat" })
1727
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "frankie-page-main", children: [
1728
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("header", { className: "frankie-page-header", children: [
1729
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
1730
+ showHistory ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("button", { type: "button", className: "frankie-mobile-only", ref: menu, "aria-label": "Open conversations", "aria-haspopup": "dialog", "aria-expanded": drawerOpen, onClick: () => setDrawerOpen(true), children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.Menu, { size: 20 }) }) : null,
1731
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("strong", { className: "frankie-thread-title", children: conversations.find((item) => item.id === currentId)?.title ?? messages.find((message) => message.role === "user")?.text ?? "New chat" })
1532
1732
  ] }),
1533
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { children: [
1534
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("button", { type: "button", disabled: busy, onClick: onNew, "aria-label": "Start a new conversation", children: [
1535
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.Plus, { size: 16 }),
1536
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { children: "New chat" })
1733
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
1734
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("button", { type: "button", disabled: busy, onClick: onNew, "aria-label": "Start a new conversation", children: [
1735
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.Plus, { size: 16 }),
1736
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { children: "New chat" })
1537
1737
  ] }),
1538
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("details", { className: "frankie-chat-settings", children: [
1539
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("summary", { "aria-label": "Chat settings", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.Settings, { size: 19 }) }),
1540
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { children: [
1541
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("strong", { children: "Chat settings" }),
1542
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { children: "History syncs automatically for signed-in users." }),
1543
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("button", { type: "button", onClick: () => void refresh(), disabled: historyLoading || !showHistory, children: [
1544
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react7.History, { size: 16 }),
1738
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("details", { className: "frankie-chat-settings", children: [
1739
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("summary", { "aria-label": "Chat settings", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.Settings, { size: 19 }) }),
1740
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
1741
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("strong", { children: "Chat settings" }),
1742
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("p", { children: "History syncs automatically for signed-in users." }),
1743
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("button", { type: "button", onClick: () => void refresh(), disabled: historyLoading || !showHistory, children: [
1744
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_lucide_react8.History, { size: 16 }),
1545
1745
  "Refresh history"
1546
1746
  ] }),
1547
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("small", { children: "Skills are checked at the start of each message." })
1747
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("small", { children: "Skills are checked at the start of each message." })
1548
1748
  ] })
1549
1749
  ] })
1550
1750
  ] })
1551
1751
  ] }),
1552
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "frankie-page-canvas", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(FrankieInline, { ...panel, header: panel.header ?? false, emptyState: panel.emptyState ?? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(PageEmptyState, {}) }) })
1752
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "frankie-page-canvas", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(FrankieInline, { ...panel, header: panel.header ?? false, emptyState: panel.emptyState ?? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(PageEmptyState, {}) }) })
1553
1753
  ] }),
1554
- showHistory ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("dialog", { ref: dialog, className: "frankie-history-dialog frankie-root", "aria-label": "Chat conversations", style: themeToCssVars(theme), onCancel: close, onClick: (event) => {
1754
+ showHistory ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("dialog", { ref: dialog, className: "frankie-history-dialog frankie-root", "aria-label": "Chat conversations", style: themeToCssVars(theme), onCancel: close, onClick: (event) => {
1555
1755
  if (event.target === event.currentTarget) close();
1556
- }, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("aside", { children: history }) }) : null
1756
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("aside", { children: history }) }) : null
1557
1757
  ] });
1558
1758
  }
1559
1759
  function FrankieChatPage({ className, style, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus, showHistory = true, navigation, ...provider }) {
1560
- const [id, setId] = (0, import_react10.useState)(() => provider.conversationId ?? crypto.randomUUID());
1760
+ const [id, setId] = (0, import_react12.useState)(() => provider.conversationId ?? crypto.randomUUID());
1561
1761
  const onNew = () => setId(crypto.randomUUID());
1562
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: ["frankie-root", "frankie-chat-page", className].filter(Boolean).join(" "), style: { height: "100%", minHeight: "min(36rem, 100dvh)", width: "100%", ...style }, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(FrankieProvider, { ...provider, conversationId: id, storageNamespace: (provider.storageNamespace ?? "frankie." + provider.publishableKey) + "." + id, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(PageChrome, { showHistory, navigation, currentId: id, onSelect: setId, onNew, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus }) }) });
1762
+ const pageRef = (0, import_react12.useRef)(null);
1763
+ const stylesReady = useStylesReady(pageRef);
1764
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("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__ */ (0, import_jsx_runtime18.jsx)(FrankieProvider, { ...provider, conversationId: id, storageNamespace: (provider.storageNamespace ?? "frankie." + provider.publishableKey) + "." + id, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(PageChrome, { showHistory, navigation, currentId: id, onSelect: setId, onNew, header, footer, emptyState, renderMessage, renderTool, placeholder, autoFocus }) }) });
1563
1765
  }
1564
1766
 
1565
1767
  // src/components/ui/separator.tsx
1566
- var import_jsx_runtime18 = require("react/jsx-runtime");
1768
+ var import_jsx_runtime19 = require("react/jsx-runtime");
1567
1769
  function Separator({ className }) {
1568
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
1770
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { role: "separator", className: cn("h-px w-full bg-border", className) });
1569
1771
  }
1570
1772
  // Annotate the CommonJS export names for ESM import in node:
1571
1773
  0 && (module.exports = {
@@ -1579,6 +1781,7 @@ function Separator({ className }) {
1579
1781
  CardHeader,
1580
1782
  CardTitle,
1581
1783
  ChatPanel,
1784
+ Choices,
1582
1785
  Composer,
1583
1786
  FRANKIE_CLOUD_URL,
1584
1787
  FrankieApiError,
@@ -1599,6 +1802,7 @@ function Separator({ className }) {
1599
1802
  applyEvent,
1600
1803
  buttonVariants,
1601
1804
  cn,
1805
+ extractChoices,
1602
1806
  newAssistantMessage,
1603
1807
  newUserMessage,
1604
1808
  readEventStream,