@ai-me-chat/react 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -13,20 +13,35 @@ function useAIMeContext() {
13
13
 
14
14
  // src/provider.tsx
15
15
  import { jsx } from "react/jsx-runtime";
16
- function AIMeProvider({ endpoint, headers, onAction, children }) {
17
- return /* @__PURE__ */ jsx(AIMeContext, { value: { endpoint, headers, onAction }, children });
16
+ function AIMeProvider({ endpoint, headers, onAction, stuckTimeout, children }) {
17
+ return /* @__PURE__ */ jsx(AIMeContext, { value: { endpoint, headers, onAction, stuckTimeout }, children });
18
18
  }
19
19
 
20
20
  // src/chat.tsx
21
- import { useState as useState2, useRef as useRef2, useEffect as useEffect2, useCallback as useCallback2, useId } from "react";
21
+ import { useState as useState2, useRef as useRef3, useEffect as useEffect3, useCallback as useCallback3, useId as useId2, useMemo as useMemo2, Fragment } from "react";
22
22
 
23
23
  // src/use-ai-me.ts
24
24
  import { useChat } from "@ai-sdk/react";
25
25
  import { DefaultChatTransport } from "ai";
26
- import { useState, useCallback, useEffect, useRef } from "react";
26
+ import { useState, useCallback, useEffect, useRef, useMemo } from "react";
27
27
  var STORAGE_KEY = "ai-me-messages";
28
+ function cleanAssistantText(text) {
29
+ return text.replace(/<tools>[\s\S]*?<\/tools>/g, "").trim();
30
+ }
31
+ function trimIncompleteToolCalls(messages) {
32
+ if (messages.length === 0) return messages;
33
+ const last = messages[messages.length - 1];
34
+ if (last.role !== "assistant") return messages;
35
+ const hasToolCall = last.parts.some((p) => p.type === "tool-call");
36
+ const hasToolResult = last.parts.some((p) => p.type === "tool-result");
37
+ if (hasToolCall && !hasToolResult) {
38
+ return messages.slice(0, -1);
39
+ }
40
+ return messages;
41
+ }
28
42
  function useAIMe() {
29
- const { endpoint, headers } = useAIMeContext();
43
+ const { endpoint, headers, stuckTimeout: configuredTimeout } = useAIMeContext();
44
+ const stuckTimeout = configuredTimeout ?? 3e4;
30
45
  const [input, setInput] = useState("");
31
46
  const initialized = useRef(false);
32
47
  const chat = useChat({
@@ -35,6 +50,24 @@ function useAIMe() {
35
50
  headers
36
51
  })
37
52
  });
53
+ const messages = useMemo(() => {
54
+ return chat.messages.map((m) => {
55
+ if (m.role !== "assistant") return m;
56
+ let changed = false;
57
+ const cleanedParts = m.parts.map((p) => {
58
+ if (p.type !== "text") return p;
59
+ const cleaned = cleanAssistantText(p.text);
60
+ if (cleaned === p.text) return p;
61
+ changed = true;
62
+ return { ...p, text: cleaned };
63
+ });
64
+ if (!changed) return m;
65
+ const nonEmptyParts = cleanedParts.filter(
66
+ (p) => p.type !== "text" || p.text.length > 0
67
+ );
68
+ return { ...m, parts: nonEmptyParts };
69
+ });
70
+ }, [chat.messages]);
38
71
  useEffect(() => {
39
72
  if (initialized.current) return;
40
73
  initialized.current = true;
@@ -43,7 +76,10 @@ function useAIMe() {
43
76
  if (stored) {
44
77
  const parsed = JSON.parse(stored);
45
78
  if (Array.isArray(parsed) && parsed.length > 0) {
46
- chat.setMessages(parsed);
79
+ const cleaned = trimIncompleteToolCalls(parsed);
80
+ if (cleaned.length > 0) {
81
+ chat.setMessages(cleaned);
82
+ }
47
83
  }
48
84
  }
49
85
  } catch {
@@ -60,6 +96,14 @@ function useAIMe() {
60
96
  } catch {
61
97
  }
62
98
  }, [chat.messages]);
99
+ useEffect(() => {
100
+ if (stuckTimeout <= 0) return;
101
+ if (chat.status !== "submitted") return;
102
+ const timer = setTimeout(() => {
103
+ chat.stop();
104
+ }, stuckTimeout);
105
+ return () => clearTimeout(timer);
106
+ }, [chat.status, stuckTimeout, chat.stop]);
63
107
  const handleInputChange = useCallback(
64
108
  (e) => {
65
109
  setInput(e.target.value);
@@ -84,7 +128,7 @@ function useAIMe() {
84
128
  }, [chat]);
85
129
  return {
86
130
  /** Conversation messages */
87
- messages: chat.messages,
131
+ messages,
88
132
  /** Current input value */
89
133
  input,
90
134
  /** Set input value */
@@ -104,7 +148,9 @@ function useAIMe() {
104
148
  /** Set messages */
105
149
  setMessages: chat.setMessages,
106
150
  /** Clear all messages and session storage */
107
- clearMessages
151
+ clearMessages,
152
+ /** Approve or reject a pending tool call (for confirmation flow) */
153
+ addToolApprovalResponse: chat.addToolApprovalResponse
108
154
  };
109
155
  }
110
156
 
@@ -306,8 +352,197 @@ var linkStyle = {
306
352
  textUnderlineOffset: "2px"
307
353
  };
308
354
 
355
+ // src/confirm.tsx
356
+ import { useRef as useRef2, useEffect as useEffect2, useId, useCallback as useCallback2 } from "react";
357
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
358
+ function AIMeConfirm({
359
+ action,
360
+ description,
361
+ parameters,
362
+ onConfirm,
363
+ onReject
364
+ }) {
365
+ const dialogRef = useRef2(null);
366
+ const cancelButtonRef = useRef2(null);
367
+ const titleId = useId();
368
+ const descriptionId = useId();
369
+ const handleKeyDown = useCallback2(
370
+ (e) => {
371
+ if (e.key === "Escape") {
372
+ e.preventDefault();
373
+ onReject();
374
+ return;
375
+ }
376
+ if (e.key !== "Tab") return;
377
+ const dialog = dialogRef.current;
378
+ if (!dialog) return;
379
+ const focusable = dialog.querySelectorAll(
380
+ 'button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'
381
+ );
382
+ if (focusable.length === 0) return;
383
+ const first = focusable[0];
384
+ const last = focusable[focusable.length - 1];
385
+ if (e.shiftKey) {
386
+ if (document.activeElement === first) {
387
+ e.preventDefault();
388
+ last.focus();
389
+ }
390
+ } else {
391
+ if (document.activeElement === last) {
392
+ e.preventDefault();
393
+ first.focus();
394
+ }
395
+ }
396
+ },
397
+ [onReject]
398
+ );
399
+ useEffect2(() => {
400
+ const previousFocus = document.activeElement;
401
+ cancelButtonRef.current?.focus();
402
+ window.addEventListener("keydown", handleKeyDown);
403
+ return () => {
404
+ window.removeEventListener("keydown", handleKeyDown);
405
+ previousFocus?.focus();
406
+ };
407
+ }, [handleKeyDown]);
408
+ const overlayStyle = {
409
+ ...defaultThemeVars,
410
+ position: "fixed",
411
+ inset: 0,
412
+ backgroundColor: "rgba(0, 0, 0, 0.4)",
413
+ display: "flex",
414
+ alignItems: "center",
415
+ justifyContent: "center",
416
+ zIndex: 1e4,
417
+ fontFamily: "var(--ai-me-font)"
418
+ };
419
+ const dialogStyle = {
420
+ backgroundColor: "var(--ai-me-bg)",
421
+ borderRadius: "var(--ai-me-radius)",
422
+ padding: 24,
423
+ maxWidth: 420,
424
+ width: "90%",
425
+ boxShadow: "var(--ai-me-shadow)",
426
+ color: "var(--ai-me-text)"
427
+ };
428
+ const focusStyle = {
429
+ outline: "2px solid transparent",
430
+ outlineOffset: 2
431
+ };
432
+ function applyFocusRing(el) {
433
+ el.style.outline = "2px solid var(--ai-me-primary)";
434
+ el.style.outlineOffset = "2px";
435
+ }
436
+ function removeFocusRing(el) {
437
+ el.style.outline = "2px solid transparent";
438
+ el.style.outlineOffset = "2px";
439
+ }
440
+ return (
441
+ // Overlay is presentational — role and aria go on the inner dialog
442
+ /* @__PURE__ */ jsx3(
443
+ "div",
444
+ {
445
+ style: overlayStyle,
446
+ onClick: (e) => {
447
+ if (e.target === e.currentTarget) onReject();
448
+ },
449
+ "aria-hidden": "false",
450
+ children: /* @__PURE__ */ jsxs(
451
+ "div",
452
+ {
453
+ ref: dialogRef,
454
+ style: dialogStyle,
455
+ role: "alertdialog",
456
+ "aria-modal": "true",
457
+ "aria-labelledby": titleId,
458
+ "aria-describedby": descriptionId,
459
+ tabIndex: -1,
460
+ onClick: (e) => e.stopPropagation(),
461
+ children: [
462
+ /* @__PURE__ */ jsx3("h3", { id: titleId, style: { margin: "0 0 8px", fontSize: 16 }, children: "Confirm Action" }),
463
+ /* @__PURE__ */ jsx3("p", { style: { margin: "0 0 4px", fontSize: 14, fontWeight: 600 }, children: action }),
464
+ /* @__PURE__ */ jsx3(
465
+ "p",
466
+ {
467
+ id: descriptionId,
468
+ style: {
469
+ margin: "0 0 16px",
470
+ fontSize: 13,
471
+ color: "var(--ai-me-text-secondary)"
472
+ },
473
+ children: description
474
+ }
475
+ ),
476
+ parameters && Object.keys(parameters).length > 0 && /* @__PURE__ */ jsx3(
477
+ "pre",
478
+ {
479
+ style: {
480
+ margin: "0 0 16px",
481
+ padding: 12,
482
+ backgroundColor: "var(--ai-me-bg-secondary)",
483
+ borderRadius: 8,
484
+ fontSize: 12,
485
+ overflow: "auto",
486
+ maxHeight: 200,
487
+ border: "1px solid var(--ai-me-border)"
488
+ },
489
+ children: JSON.stringify(parameters, null, 2)
490
+ }
491
+ ),
492
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end" }, children: [
493
+ /* @__PURE__ */ jsx3(
494
+ "button",
495
+ {
496
+ ref: cancelButtonRef,
497
+ type: "button",
498
+ onClick: onReject,
499
+ style: {
500
+ padding: "8px 16px",
501
+ border: "1px solid var(--ai-me-border)",
502
+ borderRadius: 8,
503
+ backgroundColor: "var(--ai-me-bg)",
504
+ color: "var(--ai-me-text)",
505
+ cursor: "pointer",
506
+ fontSize: 14,
507
+ ...focusStyle
508
+ },
509
+ onFocus: (e) => applyFocusRing(e.currentTarget),
510
+ onBlur: (e) => removeFocusRing(e.currentTarget),
511
+ children: "Cancel"
512
+ }
513
+ ),
514
+ /* @__PURE__ */ jsx3(
515
+ "button",
516
+ {
517
+ type: "button",
518
+ onClick: onConfirm,
519
+ style: {
520
+ padding: "8px 16px",
521
+ border: "none",
522
+ borderRadius: 8,
523
+ // #fff on var(--ai-me-primary) = #6366f1 → contrast ≈ 4.6:1 (passes AA)
524
+ backgroundColor: "var(--ai-me-primary)",
525
+ color: "#fff",
526
+ cursor: "pointer",
527
+ fontSize: 14,
528
+ ...focusStyle
529
+ },
530
+ onFocus: (e) => applyFocusRing(e.currentTarget),
531
+ onBlur: (e) => removeFocusRing(e.currentTarget),
532
+ children: "Confirm"
533
+ }
534
+ )
535
+ ] })
536
+ ]
537
+ }
538
+ )
539
+ }
540
+ )
541
+ );
542
+ }
543
+
309
544
  // src/chat.tsx
310
- import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
545
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
311
546
  var srOnly = {
312
547
  position: "absolute",
313
548
  width: 1,
@@ -319,6 +554,12 @@ var srOnly = {
319
554
  whiteSpace: "nowrap",
320
555
  borderWidth: 0
321
556
  };
557
+ function DefaultAIIcon() {
558
+ return /* @__PURE__ */ jsxs2("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
559
+ /* @__PURE__ */ jsx4("path", { d: "M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5Z" }),
560
+ /* @__PURE__ */ jsx4("path", { d: "M19 11l.75 2.25L22 14l-2.25.75L19 17l-.75-2.25L16 14l2.25-.75Z" })
561
+ ] });
562
+ }
322
563
  function AIMeChat({
323
564
  position = "bottom-right",
324
565
  theme,
@@ -327,15 +568,17 @@ function AIMeChat({
327
568
  defaultOpen = false,
328
569
  onToggle,
329
570
  onToolComplete,
330
- onMessageComplete
571
+ onMessageComplete,
572
+ triggerIcon,
573
+ renderConfirmation
331
574
  }) {
332
575
  const [open, setOpen] = useState2(defaultOpen);
333
- const messagesEndRef = useRef2(null);
334
- const inputRef = useRef2(null);
335
- const panelRef = useRef2(null);
336
- const triggerRef = useRef2(null);
337
- const firedToolResults = useRef2(/* @__PURE__ */ new Set());
338
- const prevStatus = useRef2(null);
576
+ const messagesEndRef = useRef3(null);
577
+ const inputRef = useRef3(null);
578
+ const panelRef = useRef3(null);
579
+ const triggerRef = useRef3(null);
580
+ const firedToolResults = useRef3(/* @__PURE__ */ new Set());
581
+ const prevStatus = useRef3(null);
339
582
  const {
340
583
  messages,
341
584
  input,
@@ -343,17 +586,18 @@ function AIMeChat({
343
586
  handleSubmit,
344
587
  status,
345
588
  error,
346
- setInput
589
+ setInput,
590
+ addToolApprovalResponse
347
591
  } = useAIMe();
348
- const titleId = useId();
349
- const messagesId = useId();
592
+ const titleId = useId2();
593
+ const messagesId = useId2();
350
594
  const isInline = position === "inline";
351
- const toggleOpen = useCallback2(() => {
595
+ const toggleOpen = useCallback3(() => {
352
596
  const next = !open;
353
597
  setOpen(next);
354
598
  onToggle?.(next);
355
599
  }, [open, onToggle]);
356
- useEffect2(() => {
600
+ useEffect3(() => {
357
601
  function handleKeyDown(e) {
358
602
  if ((e.metaKey || e.ctrlKey) && e.key === ".") {
359
603
  e.preventDefault();
@@ -363,10 +607,10 @@ function AIMeChat({
363
607
  window.addEventListener("keydown", handleKeyDown);
364
608
  return () => window.removeEventListener("keydown", handleKeyDown);
365
609
  }, [toggleOpen]);
366
- useEffect2(() => {
610
+ useEffect3(() => {
367
611
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
368
612
  }, [messages]);
369
- useEffect2(() => {
613
+ useEffect3(() => {
370
614
  if (!onToolComplete) return;
371
615
  for (const message of messages) {
372
616
  for (const part of message.parts) {
@@ -382,7 +626,7 @@ function AIMeChat({
382
626
  }
383
627
  }
384
628
  }, [messages, onToolComplete]);
385
- useEffect2(() => {
629
+ useEffect3(() => {
386
630
  const prev = prevStatus.current;
387
631
  prevStatus.current = status;
388
632
  if (!onMessageComplete) return;
@@ -404,7 +648,7 @@ function AIMeChat({
404
648
  toolCalls: toolCalls.length > 0 ? toolCalls : void 0
405
649
  });
406
650
  }, [status, messages, onMessageComplete]);
407
- useEffect2(() => {
651
+ useEffect3(() => {
408
652
  if (open) {
409
653
  panelRef.current?.focus();
410
654
  setTimeout(() => inputRef.current?.focus(), 0);
@@ -412,7 +656,7 @@ function AIMeChat({
412
656
  triggerRef.current?.focus();
413
657
  }
414
658
  }, [open]);
415
- useEffect2(() => {
659
+ useEffect3(() => {
416
660
  if (!open || isInline) return;
417
661
  function handleKeyDown(e) {
418
662
  if (e.key === "Escape") {
@@ -444,6 +688,20 @@ function AIMeChat({
444
688
  window.addEventListener("keydown", handleKeyDown);
445
689
  return () => window.removeEventListener("keydown", handleKeyDown);
446
690
  }, [open, isInline, toggleOpen]);
691
+ const pendingToolCalls = useMemo2(() => {
692
+ const resultIds = /* @__PURE__ */ new Set();
693
+ const toolCalls = [];
694
+ for (const m of messages) {
695
+ for (const p of m.parts) {
696
+ if (p.type === "tool-result") {
697
+ resultIds.add(p.toolCallId);
698
+ } else if (p.type === "tool-call") {
699
+ toolCalls.push(p);
700
+ }
701
+ }
702
+ }
703
+ return toolCalls.filter((tc) => !resultIds.has(tc.toolCallId));
704
+ }, [messages]);
447
705
  const themeVars = {
448
706
  ...defaultThemeVars,
449
707
  ...themeToVars(theme)
@@ -499,8 +757,8 @@ function AIMeChat({
499
757
  zIndex: 9999
500
758
  };
501
759
  const isStreaming = status === "submitted" || status === "streaming";
502
- return /* @__PURE__ */ jsxs(Fragment, { children: [
503
- /* @__PURE__ */ jsx3(
760
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
761
+ /* @__PURE__ */ jsx4(
504
762
  "button",
505
763
  {
506
764
  ref: triggerRef,
@@ -510,10 +768,10 @@ function AIMeChat({
510
768
  "aria-expanded": open,
511
769
  "aria-controls": isInline ? void 0 : "ai-me-chat-panel",
512
770
  type: "button",
513
- children: /* @__PURE__ */ jsx3("span", { "aria-hidden": "true", children: "\u{1F4AC}" })
771
+ children: /* @__PURE__ */ jsx4("span", { "aria-hidden": "true", children: triggerIcon ?? /* @__PURE__ */ jsx4(DefaultAIIcon, {}) })
514
772
  }
515
773
  ),
516
- /* @__PURE__ */ jsxs(
774
+ /* @__PURE__ */ jsxs2(
517
775
  "div",
518
776
  {
519
777
  id: "ai-me-chat-panel",
@@ -525,7 +783,7 @@ function AIMeChat({
525
783
  "aria-busy": isStreaming,
526
784
  tabIndex: -1,
527
785
  children: [
528
- /* @__PURE__ */ jsxs(
786
+ /* @__PURE__ */ jsxs2(
529
787
  "div",
530
788
  {
531
789
  style: {
@@ -537,8 +795,8 @@ function AIMeChat({
537
795
  backgroundColor: "var(--ai-me-bg-secondary)"
538
796
  },
539
797
  children: [
540
- /* @__PURE__ */ jsx3("span", { id: titleId, style: { fontWeight: 600, fontSize: 14 }, children: "AI Assistant" }),
541
- !isInline && /* @__PURE__ */ jsx3(
798
+ /* @__PURE__ */ jsx4("span", { id: titleId, style: { fontWeight: 600, fontSize: 14 }, children: "AI Assistant" }),
799
+ !isInline && /* @__PURE__ */ jsx4(
542
800
  "button",
543
801
  {
544
802
  onClick: toggleOpen,
@@ -562,13 +820,13 @@ function AIMeChat({
562
820
  },
563
821
  "aria-label": "Close chat",
564
822
  type: "button",
565
- children: /* @__PURE__ */ jsx3("span", { "aria-hidden": "true", children: "\u2715" })
823
+ children: /* @__PURE__ */ jsx4("span", { "aria-hidden": "true", children: "\u2715" })
566
824
  }
567
825
  )
568
826
  ]
569
827
  }
570
828
  ),
571
- /* @__PURE__ */ jsx3(
829
+ /* @__PURE__ */ jsx4(
572
830
  "a",
573
831
  {
574
832
  href: "#ai-me-chat-input",
@@ -598,7 +856,7 @@ function AIMeChat({
598
856
  children: "Skip to message input"
599
857
  }
600
858
  ),
601
- /* @__PURE__ */ jsxs(
859
+ /* @__PURE__ */ jsxs2(
602
860
  "div",
603
861
  {
604
862
  id: messagesId,
@@ -614,9 +872,9 @@ function AIMeChat({
614
872
  gap: 12
615
873
  },
616
874
  children: [
617
- messages.length === 0 && /* @__PURE__ */ jsxs("div", { style: { color: "var(--ai-me-text-secondary)", fontSize: 14 }, children: [
618
- /* @__PURE__ */ jsx3("p", { children: welcomeMessage }),
619
- suggestedPrompts && suggestedPrompts.length > 0 && /* @__PURE__ */ jsxs(
875
+ messages.length === 0 && /* @__PURE__ */ jsxs2("div", { style: { color: "var(--ai-me-text-secondary)", fontSize: 14 }, children: [
876
+ /* @__PURE__ */ jsx4("p", { children: welcomeMessage }),
877
+ suggestedPrompts && suggestedPrompts.length > 0 && /* @__PURE__ */ jsxs2(
620
878
  "div",
621
879
  {
622
880
  style: {
@@ -626,8 +884,8 @@ function AIMeChat({
626
884
  gap: 8
627
885
  },
628
886
  children: [
629
- /* @__PURE__ */ jsx3("p", { style: { margin: "0 0 4px", fontSize: 12, fontWeight: 500 }, children: "Suggested questions:" }),
630
- suggestedPrompts.map((prompt) => /* @__PURE__ */ jsx3(
887
+ /* @__PURE__ */ jsx4("p", { style: { margin: "0 0 4px", fontSize: 12, fontWeight: 500 }, children: "Suggested questions:" }),
888
+ suggestedPrompts.map((prompt) => /* @__PURE__ */ jsx4(
631
889
  "button",
632
890
  {
633
891
  type: "button",
@@ -661,31 +919,35 @@ function AIMeChat({
661
919
  }
662
920
  )
663
921
  ] }),
664
- messages.map((m) => /* @__PURE__ */ jsxs(
665
- "div",
666
- {
667
- style: {
668
- alignSelf: m.role === "user" ? "flex-end" : "flex-start",
669
- maxWidth: "85%",
670
- padding: "8px 12px",
671
- borderRadius: 8,
672
- backgroundColor: m.role === "user" ? "var(--ai-me-primary)" : "var(--ai-me-bg-secondary)",
673
- color: m.role === "user" ? "#fff" : "var(--ai-me-text)",
674
- fontSize: 14,
675
- lineHeight: 1.5,
676
- whiteSpace: "pre-wrap",
677
- wordBreak: "break-word"
922
+ messages.map((m) => {
923
+ const hasTextContent = m.parts.some((p) => p.type === "text");
924
+ if (!hasTextContent && m.role === "assistant") return null;
925
+ return /* @__PURE__ */ jsxs2(
926
+ "div",
927
+ {
928
+ style: {
929
+ alignSelf: m.role === "user" ? "flex-end" : "flex-start",
930
+ maxWidth: "85%",
931
+ padding: "8px 12px",
932
+ borderRadius: 8,
933
+ backgroundColor: m.role === "user" ? "var(--ai-me-primary)" : "var(--ai-me-bg-secondary)",
934
+ color: m.role === "user" ? "#fff" : "var(--ai-me-text)",
935
+ fontSize: 14,
936
+ lineHeight: 1.5,
937
+ whiteSpace: "pre-wrap",
938
+ wordBreak: "break-word"
939
+ },
940
+ children: [
941
+ /* @__PURE__ */ jsx4("span", { style: srOnly, children: m.role === "user" ? "You: " : "Assistant: " }),
942
+ m.parts.map(
943
+ (p, i) => p.type === "text" ? /* @__PURE__ */ jsx4("span", { children: m.role === "assistant" ? renderMarkdown(p.text) : p.text }, i) : null
944
+ )
945
+ ]
678
946
  },
679
- children: [
680
- /* @__PURE__ */ jsx3("span", { style: srOnly, children: m.role === "user" ? "You: " : "Assistant: " }),
681
- m.parts.map(
682
- (p, i) => p.type === "text" ? /* @__PURE__ */ jsx3("span", { children: m.role === "assistant" ? renderMarkdown(p.text) : p.text }, i) : null
683
- )
684
- ]
685
- },
686
- m.id
687
- )),
688
- status === "submitted" && /* @__PURE__ */ jsx3(
947
+ m.id
948
+ );
949
+ }),
950
+ status === "submitted" && /* @__PURE__ */ jsx4(
689
951
  "div",
690
952
  {
691
953
  "aria-label": "Assistant is thinking",
@@ -694,10 +956,10 @@ function AIMeChat({
694
956
  color: "var(--ai-me-text-secondary)",
695
957
  fontSize: 13
696
958
  },
697
- children: /* @__PURE__ */ jsx3("span", { "aria-hidden": "true", children: "Thinking\u2026" })
959
+ children: /* @__PURE__ */ jsx4("span", { "aria-hidden": "true", children: "Thinking\u2026" })
698
960
  }
699
961
  ),
700
- error && /* @__PURE__ */ jsx3(
962
+ error && /* @__PURE__ */ jsx4(
701
963
  "div",
702
964
  {
703
965
  role: "alert",
@@ -713,11 +975,11 @@ function AIMeChat({
713
975
  children: "Something went wrong. Please try again."
714
976
  }
715
977
  ),
716
- /* @__PURE__ */ jsx3("div", { ref: messagesEndRef, "aria-hidden": "true" })
978
+ /* @__PURE__ */ jsx4("div", { ref: messagesEndRef, "aria-hidden": "true" })
717
979
  ]
718
980
  }
719
981
  ),
720
- /* @__PURE__ */ jsxs(
982
+ /* @__PURE__ */ jsxs2(
721
983
  "form",
722
984
  {
723
985
  onSubmit: handleSubmit,
@@ -728,7 +990,7 @@ function AIMeChat({
728
990
  gap: 8
729
991
  },
730
992
  children: [
731
- /* @__PURE__ */ jsx3(
993
+ /* @__PURE__ */ jsx4(
732
994
  "label",
733
995
  {
734
996
  htmlFor: "ai-me-chat-input",
@@ -736,7 +998,7 @@ function AIMeChat({
736
998
  children: "Message to AI Assistant"
737
999
  }
738
1000
  ),
739
- /* @__PURE__ */ jsx3(
1001
+ /* @__PURE__ */ jsx4(
740
1002
  "input",
741
1003
  {
742
1004
  id: "ai-me-chat-input",
@@ -767,7 +1029,7 @@ function AIMeChat({
767
1029
  }
768
1030
  }
769
1031
  ),
770
- /* @__PURE__ */ jsx3(
1032
+ /* @__PURE__ */ jsx4(
771
1033
  "button",
772
1034
  {
773
1035
  type: "submit",
@@ -802,13 +1064,38 @@ function AIMeChat({
802
1064
  )
803
1065
  ]
804
1066
  }
805
- )
1067
+ ),
1068
+ pendingToolCalls.map((tc) => {
1069
+ const onConfirm = () => addToolApprovalResponse({ id: tc.toolCallId, approved: true });
1070
+ const onCancel = () => addToolApprovalResponse({ id: tc.toolCallId, approved: false, reason: "User cancelled" });
1071
+ return renderConfirmation ? /* @__PURE__ */ jsx4(Fragment, { children: renderConfirmation({
1072
+ tool: {
1073
+ name: tc.toolName,
1074
+ httpMethod: "",
1075
+ path: "",
1076
+ description: tc.toolName
1077
+ },
1078
+ params: tc.args,
1079
+ onConfirm,
1080
+ onCancel
1081
+ }) }, tc.toolCallId) : /* @__PURE__ */ jsx4(
1082
+ AIMeConfirm,
1083
+ {
1084
+ action: tc.toolName,
1085
+ description: `Execute ${tc.toolName}?`,
1086
+ parameters: tc.args,
1087
+ onConfirm,
1088
+ onReject: onCancel
1089
+ },
1090
+ tc.toolCallId
1091
+ );
1092
+ })
806
1093
  ] });
807
1094
  }
808
1095
 
809
1096
  // src/command-palette.tsx
810
- import { useState as useState3, useEffect as useEffect3, useRef as useRef3, useCallback as useCallback3, useId as useId2 } from "react";
811
- import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
1097
+ import { useState as useState3, useEffect as useEffect4, useRef as useRef4, useCallback as useCallback4, useId as useId3 } from "react";
1098
+ import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
812
1099
  var defaultCommands = [
813
1100
  {
814
1101
  id: "help",
@@ -855,14 +1142,14 @@ function AIMeCommandPalette({
855
1142
  const [open, setOpen] = useState3(false);
856
1143
  const [query, setQuery] = useState3("");
857
1144
  const [selectedIndex, setSelectedIndex] = useState3(0);
858
- const inputRef = useRef3(null);
859
- const listRef = useRef3(null);
860
- const dialogRef = useRef3(null);
861
- const previousFocusRef = useRef3(null);
1145
+ const inputRef = useRef4(null);
1146
+ const listRef = useRef4(null);
1147
+ const dialogRef = useRef4(null);
1148
+ const previousFocusRef = useRef4(null);
862
1149
  const { sendMessage } = useAIMe();
863
- const titleId = useId2();
864
- const inputId = useId2();
865
- const toggle = useCallback3(
1150
+ const titleId = useId3();
1151
+ const inputId = useId3();
1152
+ const toggle = useCallback4(
866
1153
  (next) => {
867
1154
  setOpen(next);
868
1155
  setQuery("");
@@ -871,7 +1158,7 @@ function AIMeCommandPalette({
871
1158
  },
872
1159
  [onToggle]
873
1160
  );
874
- useEffect3(() => {
1161
+ useEffect4(() => {
875
1162
  function handleKeyDown2(e) {
876
1163
  const metaMatch = shortcut.meta ? e.metaKey : true;
877
1164
  const ctrlMatch = shortcut.ctrl ? e.ctrlKey : !shortcut.meta ? e.ctrlKey : true;
@@ -886,7 +1173,7 @@ function AIMeCommandPalette({
886
1173
  window.addEventListener("keydown", handleKeyDown2);
887
1174
  return () => window.removeEventListener("keydown", handleKeyDown2);
888
1175
  }, [open, shortcut, toggle]);
889
- useEffect3(() => {
1176
+ useEffect4(() => {
890
1177
  if (open) {
891
1178
  setTimeout(() => inputRef.current?.focus(), 0);
892
1179
  } else {
@@ -896,7 +1183,7 @@ function AIMeCommandPalette({
896
1183
  }
897
1184
  }
898
1185
  }, [open]);
899
- useEffect3(() => {
1186
+ useEffect4(() => {
900
1187
  if (!open) return;
901
1188
  function handleFocusTrap(e) {
902
1189
  if (e.key !== "Tab") return;
@@ -926,12 +1213,12 @@ function AIMeCommandPalette({
926
1213
  const filtered = query.trim() ? commands.filter(
927
1214
  (cmd) => cmd.label.toLowerCase().includes(query.toLowerCase()) || cmd.description?.toLowerCase().includes(query.toLowerCase()) || cmd.category?.toLowerCase().includes(query.toLowerCase())
928
1215
  ) : commands;
929
- useEffect3(() => {
1216
+ useEffect4(() => {
930
1217
  if (selectedIndex >= filtered.length) {
931
1218
  setSelectedIndex(Math.max(0, filtered.length - 1));
932
1219
  }
933
1220
  }, [filtered.length, selectedIndex]);
934
- useEffect3(() => {
1221
+ useEffect4(() => {
935
1222
  const list = listRef.current;
936
1223
  if (!list) return;
937
1224
  const selected = list.children[selectedIndex];
@@ -976,8 +1263,8 @@ function AIMeCommandPalette({
976
1263
  grouped.get(cat).push(cmd);
977
1264
  }
978
1265
  let flatIndex = 0;
979
- return /* @__PURE__ */ jsxs2(Fragment2, { children: [
980
- /* @__PURE__ */ jsx4(
1266
+ return /* @__PURE__ */ jsxs3(Fragment3, { children: [
1267
+ /* @__PURE__ */ jsx5(
981
1268
  "div",
982
1269
  {
983
1270
  onClick: () => toggle(false),
@@ -990,7 +1277,7 @@ function AIMeCommandPalette({
990
1277
  "aria-hidden": "true"
991
1278
  }
992
1279
  ),
993
- /* @__PURE__ */ jsxs2(
1280
+ /* @__PURE__ */ jsxs3(
994
1281
  "div",
995
1282
  {
996
1283
  ref: dialogRef,
@@ -1018,10 +1305,10 @@ function AIMeCommandPalette({
1018
1305
  "aria-labelledby": titleId,
1019
1306
  tabIndex: -1,
1020
1307
  children: [
1021
- /* @__PURE__ */ jsx4("h2", { id: titleId, style: srOnly2, children: "Command Palette" }),
1022
- /* @__PURE__ */ jsxs2("div", { style: { padding: "12px 16px", borderBottom: "1px solid var(--ai-me-border)" }, children: [
1023
- /* @__PURE__ */ jsx4("label", { htmlFor: inputId, style: srOnly2, children: "Search commands or ask AI" }),
1024
- /* @__PURE__ */ jsx4(
1308
+ /* @__PURE__ */ jsx5("h2", { id: titleId, style: srOnly2, children: "Command Palette" }),
1309
+ /* @__PURE__ */ jsxs3("div", { style: { padding: "12px 16px", borderBottom: "1px solid var(--ai-me-border)" }, children: [
1310
+ /* @__PURE__ */ jsx5("label", { htmlFor: inputId, style: srOnly2, children: "Search commands or ask AI" }),
1311
+ /* @__PURE__ */ jsx5(
1025
1312
  "input",
1026
1313
  {
1027
1314
  id: inputId,
@@ -1059,7 +1346,7 @@ function AIMeCommandPalette({
1059
1346
  }
1060
1347
  )
1061
1348
  ] }),
1062
- /* @__PURE__ */ jsxs2(
1349
+ /* @__PURE__ */ jsxs3(
1063
1350
  "div",
1064
1351
  {
1065
1352
  id: "ai-me-cmd-listbox",
@@ -1068,7 +1355,7 @@ function AIMeCommandPalette({
1068
1355
  role: "listbox",
1069
1356
  "aria-label": "Commands",
1070
1357
  children: [
1071
- filtered.length === 0 && query.trim() && /* @__PURE__ */ jsxs2(
1358
+ filtered.length === 0 && query.trim() && /* @__PURE__ */ jsxs3(
1072
1359
  "div",
1073
1360
  {
1074
1361
  role: "option",
@@ -1088,8 +1375,8 @@ function AIMeCommandPalette({
1088
1375
  ),
1089
1376
  Array.from(grouped.entries()).map(([category, items]) => (
1090
1377
  // role="group" with aria-label for the category heading
1091
- /* @__PURE__ */ jsxs2("div", { role: "group", "aria-label": category, children: [
1092
- /* @__PURE__ */ jsx4(
1378
+ /* @__PURE__ */ jsxs3("div", { role: "group", "aria-label": category, children: [
1379
+ /* @__PURE__ */ jsx5(
1093
1380
  "div",
1094
1381
  {
1095
1382
  "aria-hidden": "true",
@@ -1107,7 +1394,7 @@ function AIMeCommandPalette({
1107
1394
  items.map((cmd) => {
1108
1395
  const idx = flatIndex++;
1109
1396
  const isSelected = idx === selectedIndex;
1110
- return /* @__PURE__ */ jsxs2(
1397
+ return /* @__PURE__ */ jsxs3(
1111
1398
  "div",
1112
1399
  {
1113
1400
  id: `cmd-${cmd.id}`,
@@ -1140,10 +1427,10 @@ function AIMeCommandPalette({
1140
1427
  },
1141
1428
  children: [
1142
1429
  cmd.icon && // Icon is decorative — label comes from cmd.label
1143
- /* @__PURE__ */ jsx4("span", { "aria-hidden": "true", style: { fontSize: 16, flexShrink: 0 }, children: cmd.icon }),
1144
- /* @__PURE__ */ jsxs2("div", { style: { flex: 1, minWidth: 0 }, children: [
1145
- /* @__PURE__ */ jsx4("div", { style: { fontSize: 14, fontWeight: 500 }, children: cmd.label }),
1146
- cmd.description && /* @__PURE__ */ jsx4(
1430
+ /* @__PURE__ */ jsx5("span", { "aria-hidden": "true", style: { fontSize: 16, flexShrink: 0 }, children: cmd.icon }),
1431
+ /* @__PURE__ */ jsxs3("div", { style: { flex: 1, minWidth: 0 }, children: [
1432
+ /* @__PURE__ */ jsx5("div", { style: { fontSize: 14, fontWeight: 500 }, children: cmd.label }),
1433
+ cmd.description && /* @__PURE__ */ jsx5(
1147
1434
  "div",
1148
1435
  {
1149
1436
  style: {
@@ -1167,7 +1454,7 @@ function AIMeCommandPalette({
1167
1454
  ]
1168
1455
  }
1169
1456
  ),
1170
- /* @__PURE__ */ jsxs2(
1457
+ /* @__PURE__ */ jsxs3(
1171
1458
  "div",
1172
1459
  {
1173
1460
  "aria-hidden": "true",
@@ -1180,9 +1467,9 @@ function AIMeCommandPalette({
1180
1467
  gap: 16
1181
1468
  },
1182
1469
  children: [
1183
- /* @__PURE__ */ jsx4("span", { children: "\u2191\u2193 Navigate" }),
1184
- /* @__PURE__ */ jsx4("span", { children: "\u21B5 Select" }),
1185
- /* @__PURE__ */ jsx4("span", { children: "Esc Close" })
1470
+ /* @__PURE__ */ jsx5("span", { children: "\u2191\u2193 Navigate" }),
1471
+ /* @__PURE__ */ jsx5("span", { children: "\u21B5 Select" }),
1472
+ /* @__PURE__ */ jsx5("span", { children: "Esc Close" })
1186
1473
  ]
1187
1474
  }
1188
1475
  )
@@ -1191,200 +1478,12 @@ function AIMeCommandPalette({
1191
1478
  )
1192
1479
  ] });
1193
1480
  }
1194
-
1195
- // src/confirm.tsx
1196
- import { useRef as useRef4, useEffect as useEffect4, useId as useId3, useCallback as useCallback4 } from "react";
1197
- import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
1198
- function AIMeConfirm({
1199
- action,
1200
- description,
1201
- parameters,
1202
- onConfirm,
1203
- onReject
1204
- }) {
1205
- const dialogRef = useRef4(null);
1206
- const cancelButtonRef = useRef4(null);
1207
- const titleId = useId3();
1208
- const descriptionId = useId3();
1209
- const handleKeyDown = useCallback4(
1210
- (e) => {
1211
- if (e.key === "Escape") {
1212
- e.preventDefault();
1213
- onReject();
1214
- return;
1215
- }
1216
- if (e.key !== "Tab") return;
1217
- const dialog = dialogRef.current;
1218
- if (!dialog) return;
1219
- const focusable = dialog.querySelectorAll(
1220
- 'button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'
1221
- );
1222
- if (focusable.length === 0) return;
1223
- const first = focusable[0];
1224
- const last = focusable[focusable.length - 1];
1225
- if (e.shiftKey) {
1226
- if (document.activeElement === first) {
1227
- e.preventDefault();
1228
- last.focus();
1229
- }
1230
- } else {
1231
- if (document.activeElement === last) {
1232
- e.preventDefault();
1233
- first.focus();
1234
- }
1235
- }
1236
- },
1237
- [onReject]
1238
- );
1239
- useEffect4(() => {
1240
- const previousFocus = document.activeElement;
1241
- cancelButtonRef.current?.focus();
1242
- window.addEventListener("keydown", handleKeyDown);
1243
- return () => {
1244
- window.removeEventListener("keydown", handleKeyDown);
1245
- previousFocus?.focus();
1246
- };
1247
- }, [handleKeyDown]);
1248
- const overlayStyle = {
1249
- ...defaultThemeVars,
1250
- position: "fixed",
1251
- inset: 0,
1252
- backgroundColor: "rgba(0, 0, 0, 0.4)",
1253
- display: "flex",
1254
- alignItems: "center",
1255
- justifyContent: "center",
1256
- zIndex: 1e4,
1257
- fontFamily: "var(--ai-me-font)"
1258
- };
1259
- const dialogStyle = {
1260
- backgroundColor: "var(--ai-me-bg)",
1261
- borderRadius: "var(--ai-me-radius)",
1262
- padding: 24,
1263
- maxWidth: 420,
1264
- width: "90%",
1265
- boxShadow: "var(--ai-me-shadow)",
1266
- color: "var(--ai-me-text)"
1267
- };
1268
- const focusStyle = {
1269
- outline: "2px solid transparent",
1270
- outlineOffset: 2
1271
- };
1272
- function applyFocusRing(el) {
1273
- el.style.outline = "2px solid var(--ai-me-primary)";
1274
- el.style.outlineOffset = "2px";
1275
- }
1276
- function removeFocusRing(el) {
1277
- el.style.outline = "2px solid transparent";
1278
- el.style.outlineOffset = "2px";
1279
- }
1280
- return (
1281
- // Overlay is presentational — role and aria go on the inner dialog
1282
- /* @__PURE__ */ jsx5(
1283
- "div",
1284
- {
1285
- style: overlayStyle,
1286
- onClick: (e) => {
1287
- if (e.target === e.currentTarget) onReject();
1288
- },
1289
- "aria-hidden": "false",
1290
- children: /* @__PURE__ */ jsxs3(
1291
- "div",
1292
- {
1293
- ref: dialogRef,
1294
- style: dialogStyle,
1295
- role: "alertdialog",
1296
- "aria-modal": "true",
1297
- "aria-labelledby": titleId,
1298
- "aria-describedby": descriptionId,
1299
- tabIndex: -1,
1300
- onClick: (e) => e.stopPropagation(),
1301
- children: [
1302
- /* @__PURE__ */ jsx5("h3", { id: titleId, style: { margin: "0 0 8px", fontSize: 16 }, children: "Confirm Action" }),
1303
- /* @__PURE__ */ jsx5("p", { style: { margin: "0 0 4px", fontSize: 14, fontWeight: 600 }, children: action }),
1304
- /* @__PURE__ */ jsx5(
1305
- "p",
1306
- {
1307
- id: descriptionId,
1308
- style: {
1309
- margin: "0 0 16px",
1310
- fontSize: 13,
1311
- color: "var(--ai-me-text-secondary)"
1312
- },
1313
- children: description
1314
- }
1315
- ),
1316
- parameters && Object.keys(parameters).length > 0 && /* @__PURE__ */ jsx5(
1317
- "pre",
1318
- {
1319
- style: {
1320
- margin: "0 0 16px",
1321
- padding: 12,
1322
- backgroundColor: "var(--ai-me-bg-secondary)",
1323
- borderRadius: 8,
1324
- fontSize: 12,
1325
- overflow: "auto",
1326
- maxHeight: 200,
1327
- border: "1px solid var(--ai-me-border)"
1328
- },
1329
- children: JSON.stringify(parameters, null, 2)
1330
- }
1331
- ),
1332
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end" }, children: [
1333
- /* @__PURE__ */ jsx5(
1334
- "button",
1335
- {
1336
- ref: cancelButtonRef,
1337
- type: "button",
1338
- onClick: onReject,
1339
- style: {
1340
- padding: "8px 16px",
1341
- border: "1px solid var(--ai-me-border)",
1342
- borderRadius: 8,
1343
- backgroundColor: "var(--ai-me-bg)",
1344
- color: "var(--ai-me-text)",
1345
- cursor: "pointer",
1346
- fontSize: 14,
1347
- ...focusStyle
1348
- },
1349
- onFocus: (e) => applyFocusRing(e.currentTarget),
1350
- onBlur: (e) => removeFocusRing(e.currentTarget),
1351
- children: "Cancel"
1352
- }
1353
- ),
1354
- /* @__PURE__ */ jsx5(
1355
- "button",
1356
- {
1357
- type: "button",
1358
- onClick: onConfirm,
1359
- style: {
1360
- padding: "8px 16px",
1361
- border: "none",
1362
- borderRadius: 8,
1363
- // #fff on var(--ai-me-primary) = #6366f1 → contrast ≈ 4.6:1 (passes AA)
1364
- backgroundColor: "var(--ai-me-primary)",
1365
- color: "#fff",
1366
- cursor: "pointer",
1367
- fontSize: 14,
1368
- ...focusStyle
1369
- },
1370
- onFocus: (e) => applyFocusRing(e.currentTarget),
1371
- onBlur: (e) => removeFocusRing(e.currentTarget),
1372
- children: "Confirm"
1373
- }
1374
- )
1375
- ] })
1376
- ]
1377
- }
1378
- )
1379
- }
1380
- )
1381
- );
1382
- }
1383
1481
  export {
1384
1482
  AIMeChat,
1385
1483
  AIMeCommandPalette,
1386
1484
  AIMeConfirm,
1387
1485
  AIMeProvider,
1486
+ cleanAssistantText,
1388
1487
  renderMarkdown,
1389
1488
  useAIMe,
1390
1489
  useAIMeContext