@ai-me-chat/react 0.2.1 → 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",
@@ -664,7 +922,7 @@ function AIMeChat({
664
922
  messages.map((m) => {
665
923
  const hasTextContent = m.parts.some((p) => p.type === "text");
666
924
  if (!hasTextContent && m.role === "assistant") return null;
667
- return /* @__PURE__ */ jsxs(
925
+ return /* @__PURE__ */ jsxs2(
668
926
  "div",
669
927
  {
670
928
  style: {
@@ -680,16 +938,16 @@ function AIMeChat({
680
938
  wordBreak: "break-word"
681
939
  },
682
940
  children: [
683
- /* @__PURE__ */ jsx3("span", { style: srOnly, children: m.role === "user" ? "You: " : "Assistant: " }),
941
+ /* @__PURE__ */ jsx4("span", { style: srOnly, children: m.role === "user" ? "You: " : "Assistant: " }),
684
942
  m.parts.map(
685
- (p, i) => p.type === "text" ? /* @__PURE__ */ jsx3("span", { children: m.role === "assistant" ? renderMarkdown(p.text) : p.text }, i) : null
943
+ (p, i) => p.type === "text" ? /* @__PURE__ */ jsx4("span", { children: m.role === "assistant" ? renderMarkdown(p.text) : p.text }, i) : null
686
944
  )
687
945
  ]
688
946
  },
689
947
  m.id
690
948
  );
691
949
  }),
692
- status === "submitted" && /* @__PURE__ */ jsx3(
950
+ status === "submitted" && /* @__PURE__ */ jsx4(
693
951
  "div",
694
952
  {
695
953
  "aria-label": "Assistant is thinking",
@@ -698,10 +956,10 @@ function AIMeChat({
698
956
  color: "var(--ai-me-text-secondary)",
699
957
  fontSize: 13
700
958
  },
701
- children: /* @__PURE__ */ jsx3("span", { "aria-hidden": "true", children: "Thinking\u2026" })
959
+ children: /* @__PURE__ */ jsx4("span", { "aria-hidden": "true", children: "Thinking\u2026" })
702
960
  }
703
961
  ),
704
- error && /* @__PURE__ */ jsx3(
962
+ error && /* @__PURE__ */ jsx4(
705
963
  "div",
706
964
  {
707
965
  role: "alert",
@@ -717,11 +975,11 @@ function AIMeChat({
717
975
  children: "Something went wrong. Please try again."
718
976
  }
719
977
  ),
720
- /* @__PURE__ */ jsx3("div", { ref: messagesEndRef, "aria-hidden": "true" })
978
+ /* @__PURE__ */ jsx4("div", { ref: messagesEndRef, "aria-hidden": "true" })
721
979
  ]
722
980
  }
723
981
  ),
724
- /* @__PURE__ */ jsxs(
982
+ /* @__PURE__ */ jsxs2(
725
983
  "form",
726
984
  {
727
985
  onSubmit: handleSubmit,
@@ -732,7 +990,7 @@ function AIMeChat({
732
990
  gap: 8
733
991
  },
734
992
  children: [
735
- /* @__PURE__ */ jsx3(
993
+ /* @__PURE__ */ jsx4(
736
994
  "label",
737
995
  {
738
996
  htmlFor: "ai-me-chat-input",
@@ -740,7 +998,7 @@ function AIMeChat({
740
998
  children: "Message to AI Assistant"
741
999
  }
742
1000
  ),
743
- /* @__PURE__ */ jsx3(
1001
+ /* @__PURE__ */ jsx4(
744
1002
  "input",
745
1003
  {
746
1004
  id: "ai-me-chat-input",
@@ -771,7 +1029,7 @@ function AIMeChat({
771
1029
  }
772
1030
  }
773
1031
  ),
774
- /* @__PURE__ */ jsx3(
1032
+ /* @__PURE__ */ jsx4(
775
1033
  "button",
776
1034
  {
777
1035
  type: "submit",
@@ -806,13 +1064,38 @@ function AIMeChat({
806
1064
  )
807
1065
  ]
808
1066
  }
809
- )
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
+ })
810
1093
  ] });
811
1094
  }
812
1095
 
813
1096
  // src/command-palette.tsx
814
- import { useState as useState3, useEffect as useEffect3, useRef as useRef3, useCallback as useCallback3, useId as useId2 } from "react";
815
- 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";
816
1099
  var defaultCommands = [
817
1100
  {
818
1101
  id: "help",
@@ -859,14 +1142,14 @@ function AIMeCommandPalette({
859
1142
  const [open, setOpen] = useState3(false);
860
1143
  const [query, setQuery] = useState3("");
861
1144
  const [selectedIndex, setSelectedIndex] = useState3(0);
862
- const inputRef = useRef3(null);
863
- const listRef = useRef3(null);
864
- const dialogRef = useRef3(null);
865
- const previousFocusRef = useRef3(null);
1145
+ const inputRef = useRef4(null);
1146
+ const listRef = useRef4(null);
1147
+ const dialogRef = useRef4(null);
1148
+ const previousFocusRef = useRef4(null);
866
1149
  const { sendMessage } = useAIMe();
867
- const titleId = useId2();
868
- const inputId = useId2();
869
- const toggle = useCallback3(
1150
+ const titleId = useId3();
1151
+ const inputId = useId3();
1152
+ const toggle = useCallback4(
870
1153
  (next) => {
871
1154
  setOpen(next);
872
1155
  setQuery("");
@@ -875,7 +1158,7 @@ function AIMeCommandPalette({
875
1158
  },
876
1159
  [onToggle]
877
1160
  );
878
- useEffect3(() => {
1161
+ useEffect4(() => {
879
1162
  function handleKeyDown2(e) {
880
1163
  const metaMatch = shortcut.meta ? e.metaKey : true;
881
1164
  const ctrlMatch = shortcut.ctrl ? e.ctrlKey : !shortcut.meta ? e.ctrlKey : true;
@@ -890,7 +1173,7 @@ function AIMeCommandPalette({
890
1173
  window.addEventListener("keydown", handleKeyDown2);
891
1174
  return () => window.removeEventListener("keydown", handleKeyDown2);
892
1175
  }, [open, shortcut, toggle]);
893
- useEffect3(() => {
1176
+ useEffect4(() => {
894
1177
  if (open) {
895
1178
  setTimeout(() => inputRef.current?.focus(), 0);
896
1179
  } else {
@@ -900,7 +1183,7 @@ function AIMeCommandPalette({
900
1183
  }
901
1184
  }
902
1185
  }, [open]);
903
- useEffect3(() => {
1186
+ useEffect4(() => {
904
1187
  if (!open) return;
905
1188
  function handleFocusTrap(e) {
906
1189
  if (e.key !== "Tab") return;
@@ -930,12 +1213,12 @@ function AIMeCommandPalette({
930
1213
  const filtered = query.trim() ? commands.filter(
931
1214
  (cmd) => cmd.label.toLowerCase().includes(query.toLowerCase()) || cmd.description?.toLowerCase().includes(query.toLowerCase()) || cmd.category?.toLowerCase().includes(query.toLowerCase())
932
1215
  ) : commands;
933
- useEffect3(() => {
1216
+ useEffect4(() => {
934
1217
  if (selectedIndex >= filtered.length) {
935
1218
  setSelectedIndex(Math.max(0, filtered.length - 1));
936
1219
  }
937
1220
  }, [filtered.length, selectedIndex]);
938
- useEffect3(() => {
1221
+ useEffect4(() => {
939
1222
  const list = listRef.current;
940
1223
  if (!list) return;
941
1224
  const selected = list.children[selectedIndex];
@@ -980,8 +1263,8 @@ function AIMeCommandPalette({
980
1263
  grouped.get(cat).push(cmd);
981
1264
  }
982
1265
  let flatIndex = 0;
983
- return /* @__PURE__ */ jsxs2(Fragment2, { children: [
984
- /* @__PURE__ */ jsx4(
1266
+ return /* @__PURE__ */ jsxs3(Fragment3, { children: [
1267
+ /* @__PURE__ */ jsx5(
985
1268
  "div",
986
1269
  {
987
1270
  onClick: () => toggle(false),
@@ -994,7 +1277,7 @@ function AIMeCommandPalette({
994
1277
  "aria-hidden": "true"
995
1278
  }
996
1279
  ),
997
- /* @__PURE__ */ jsxs2(
1280
+ /* @__PURE__ */ jsxs3(
998
1281
  "div",
999
1282
  {
1000
1283
  ref: dialogRef,
@@ -1022,10 +1305,10 @@ function AIMeCommandPalette({
1022
1305
  "aria-labelledby": titleId,
1023
1306
  tabIndex: -1,
1024
1307
  children: [
1025
- /* @__PURE__ */ jsx4("h2", { id: titleId, style: srOnly2, children: "Command Palette" }),
1026
- /* @__PURE__ */ jsxs2("div", { style: { padding: "12px 16px", borderBottom: "1px solid var(--ai-me-border)" }, children: [
1027
- /* @__PURE__ */ jsx4("label", { htmlFor: inputId, style: srOnly2, children: "Search commands or ask AI" }),
1028
- /* @__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(
1029
1312
  "input",
1030
1313
  {
1031
1314
  id: inputId,
@@ -1063,7 +1346,7 @@ function AIMeCommandPalette({
1063
1346
  }
1064
1347
  )
1065
1348
  ] }),
1066
- /* @__PURE__ */ jsxs2(
1349
+ /* @__PURE__ */ jsxs3(
1067
1350
  "div",
1068
1351
  {
1069
1352
  id: "ai-me-cmd-listbox",
@@ -1072,7 +1355,7 @@ function AIMeCommandPalette({
1072
1355
  role: "listbox",
1073
1356
  "aria-label": "Commands",
1074
1357
  children: [
1075
- filtered.length === 0 && query.trim() && /* @__PURE__ */ jsxs2(
1358
+ filtered.length === 0 && query.trim() && /* @__PURE__ */ jsxs3(
1076
1359
  "div",
1077
1360
  {
1078
1361
  role: "option",
@@ -1092,8 +1375,8 @@ function AIMeCommandPalette({
1092
1375
  ),
1093
1376
  Array.from(grouped.entries()).map(([category, items]) => (
1094
1377
  // role="group" with aria-label for the category heading
1095
- /* @__PURE__ */ jsxs2("div", { role: "group", "aria-label": category, children: [
1096
- /* @__PURE__ */ jsx4(
1378
+ /* @__PURE__ */ jsxs3("div", { role: "group", "aria-label": category, children: [
1379
+ /* @__PURE__ */ jsx5(
1097
1380
  "div",
1098
1381
  {
1099
1382
  "aria-hidden": "true",
@@ -1111,7 +1394,7 @@ function AIMeCommandPalette({
1111
1394
  items.map((cmd) => {
1112
1395
  const idx = flatIndex++;
1113
1396
  const isSelected = idx === selectedIndex;
1114
- return /* @__PURE__ */ jsxs2(
1397
+ return /* @__PURE__ */ jsxs3(
1115
1398
  "div",
1116
1399
  {
1117
1400
  id: `cmd-${cmd.id}`,
@@ -1144,10 +1427,10 @@ function AIMeCommandPalette({
1144
1427
  },
1145
1428
  children: [
1146
1429
  cmd.icon && // Icon is decorative — label comes from cmd.label
1147
- /* @__PURE__ */ jsx4("span", { "aria-hidden": "true", style: { fontSize: 16, flexShrink: 0 }, children: cmd.icon }),
1148
- /* @__PURE__ */ jsxs2("div", { style: { flex: 1, minWidth: 0 }, children: [
1149
- /* @__PURE__ */ jsx4("div", { style: { fontSize: 14, fontWeight: 500 }, children: cmd.label }),
1150
- 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(
1151
1434
  "div",
1152
1435
  {
1153
1436
  style: {
@@ -1171,7 +1454,7 @@ function AIMeCommandPalette({
1171
1454
  ]
1172
1455
  }
1173
1456
  ),
1174
- /* @__PURE__ */ jsxs2(
1457
+ /* @__PURE__ */ jsxs3(
1175
1458
  "div",
1176
1459
  {
1177
1460
  "aria-hidden": "true",
@@ -1184,9 +1467,9 @@ function AIMeCommandPalette({
1184
1467
  gap: 16
1185
1468
  },
1186
1469
  children: [
1187
- /* @__PURE__ */ jsx4("span", { children: "\u2191\u2193 Navigate" }),
1188
- /* @__PURE__ */ jsx4("span", { children: "\u21B5 Select" }),
1189
- /* @__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" })
1190
1473
  ]
1191
1474
  }
1192
1475
  )
@@ -1195,200 +1478,12 @@ function AIMeCommandPalette({
1195
1478
  )
1196
1479
  ] });
1197
1480
  }
1198
-
1199
- // src/confirm.tsx
1200
- import { useRef as useRef4, useEffect as useEffect4, useId as useId3, useCallback as useCallback4 } from "react";
1201
- import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
1202
- function AIMeConfirm({
1203
- action,
1204
- description,
1205
- parameters,
1206
- onConfirm,
1207
- onReject
1208
- }) {
1209
- const dialogRef = useRef4(null);
1210
- const cancelButtonRef = useRef4(null);
1211
- const titleId = useId3();
1212
- const descriptionId = useId3();
1213
- const handleKeyDown = useCallback4(
1214
- (e) => {
1215
- if (e.key === "Escape") {
1216
- e.preventDefault();
1217
- onReject();
1218
- return;
1219
- }
1220
- if (e.key !== "Tab") return;
1221
- const dialog = dialogRef.current;
1222
- if (!dialog) return;
1223
- const focusable = dialog.querySelectorAll(
1224
- 'button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'
1225
- );
1226
- if (focusable.length === 0) return;
1227
- const first = focusable[0];
1228
- const last = focusable[focusable.length - 1];
1229
- if (e.shiftKey) {
1230
- if (document.activeElement === first) {
1231
- e.preventDefault();
1232
- last.focus();
1233
- }
1234
- } else {
1235
- if (document.activeElement === last) {
1236
- e.preventDefault();
1237
- first.focus();
1238
- }
1239
- }
1240
- },
1241
- [onReject]
1242
- );
1243
- useEffect4(() => {
1244
- const previousFocus = document.activeElement;
1245
- cancelButtonRef.current?.focus();
1246
- window.addEventListener("keydown", handleKeyDown);
1247
- return () => {
1248
- window.removeEventListener("keydown", handleKeyDown);
1249
- previousFocus?.focus();
1250
- };
1251
- }, [handleKeyDown]);
1252
- const overlayStyle = {
1253
- ...defaultThemeVars,
1254
- position: "fixed",
1255
- inset: 0,
1256
- backgroundColor: "rgba(0, 0, 0, 0.4)",
1257
- display: "flex",
1258
- alignItems: "center",
1259
- justifyContent: "center",
1260
- zIndex: 1e4,
1261
- fontFamily: "var(--ai-me-font)"
1262
- };
1263
- const dialogStyle = {
1264
- backgroundColor: "var(--ai-me-bg)",
1265
- borderRadius: "var(--ai-me-radius)",
1266
- padding: 24,
1267
- maxWidth: 420,
1268
- width: "90%",
1269
- boxShadow: "var(--ai-me-shadow)",
1270
- color: "var(--ai-me-text)"
1271
- };
1272
- const focusStyle = {
1273
- outline: "2px solid transparent",
1274
- outlineOffset: 2
1275
- };
1276
- function applyFocusRing(el) {
1277
- el.style.outline = "2px solid var(--ai-me-primary)";
1278
- el.style.outlineOffset = "2px";
1279
- }
1280
- function removeFocusRing(el) {
1281
- el.style.outline = "2px solid transparent";
1282
- el.style.outlineOffset = "2px";
1283
- }
1284
- return (
1285
- // Overlay is presentational — role and aria go on the inner dialog
1286
- /* @__PURE__ */ jsx5(
1287
- "div",
1288
- {
1289
- style: overlayStyle,
1290
- onClick: (e) => {
1291
- if (e.target === e.currentTarget) onReject();
1292
- },
1293
- "aria-hidden": "false",
1294
- children: /* @__PURE__ */ jsxs3(
1295
- "div",
1296
- {
1297
- ref: dialogRef,
1298
- style: dialogStyle,
1299
- role: "alertdialog",
1300
- "aria-modal": "true",
1301
- "aria-labelledby": titleId,
1302
- "aria-describedby": descriptionId,
1303
- tabIndex: -1,
1304
- onClick: (e) => e.stopPropagation(),
1305
- children: [
1306
- /* @__PURE__ */ jsx5("h3", { id: titleId, style: { margin: "0 0 8px", fontSize: 16 }, children: "Confirm Action" }),
1307
- /* @__PURE__ */ jsx5("p", { style: { margin: "0 0 4px", fontSize: 14, fontWeight: 600 }, children: action }),
1308
- /* @__PURE__ */ jsx5(
1309
- "p",
1310
- {
1311
- id: descriptionId,
1312
- style: {
1313
- margin: "0 0 16px",
1314
- fontSize: 13,
1315
- color: "var(--ai-me-text-secondary)"
1316
- },
1317
- children: description
1318
- }
1319
- ),
1320
- parameters && Object.keys(parameters).length > 0 && /* @__PURE__ */ jsx5(
1321
- "pre",
1322
- {
1323
- style: {
1324
- margin: "0 0 16px",
1325
- padding: 12,
1326
- backgroundColor: "var(--ai-me-bg-secondary)",
1327
- borderRadius: 8,
1328
- fontSize: 12,
1329
- overflow: "auto",
1330
- maxHeight: 200,
1331
- border: "1px solid var(--ai-me-border)"
1332
- },
1333
- children: JSON.stringify(parameters, null, 2)
1334
- }
1335
- ),
1336
- /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end" }, children: [
1337
- /* @__PURE__ */ jsx5(
1338
- "button",
1339
- {
1340
- ref: cancelButtonRef,
1341
- type: "button",
1342
- onClick: onReject,
1343
- style: {
1344
- padding: "8px 16px",
1345
- border: "1px solid var(--ai-me-border)",
1346
- borderRadius: 8,
1347
- backgroundColor: "var(--ai-me-bg)",
1348
- color: "var(--ai-me-text)",
1349
- cursor: "pointer",
1350
- fontSize: 14,
1351
- ...focusStyle
1352
- },
1353
- onFocus: (e) => applyFocusRing(e.currentTarget),
1354
- onBlur: (e) => removeFocusRing(e.currentTarget),
1355
- children: "Cancel"
1356
- }
1357
- ),
1358
- /* @__PURE__ */ jsx5(
1359
- "button",
1360
- {
1361
- type: "button",
1362
- onClick: onConfirm,
1363
- style: {
1364
- padding: "8px 16px",
1365
- border: "none",
1366
- borderRadius: 8,
1367
- // #fff on var(--ai-me-primary) = #6366f1 → contrast ≈ 4.6:1 (passes AA)
1368
- backgroundColor: "var(--ai-me-primary)",
1369
- color: "#fff",
1370
- cursor: "pointer",
1371
- fontSize: 14,
1372
- ...focusStyle
1373
- },
1374
- onFocus: (e) => applyFocusRing(e.currentTarget),
1375
- onBlur: (e) => removeFocusRing(e.currentTarget),
1376
- children: "Confirm"
1377
- }
1378
- )
1379
- ] })
1380
- ]
1381
- }
1382
- )
1383
- }
1384
- )
1385
- );
1386
- }
1387
1481
  export {
1388
1482
  AIMeChat,
1389
1483
  AIMeCommandPalette,
1390
1484
  AIMeConfirm,
1391
1485
  AIMeProvider,
1486
+ cleanAssistantText,
1392
1487
  renderMarkdown,
1393
1488
  useAIMe,
1394
1489
  useAIMeContext