@contractspec/module.ai-chat 4.0.2 → 4.1.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.
Files changed (51) hide show
  1. package/README.md +130 -10
  2. package/dist/adapters/ai-sdk-bundle-adapter.d.ts +18 -0
  3. package/dist/adapters/index.d.ts +4 -0
  4. package/dist/browser/core/index.js +1138 -21
  5. package/dist/browser/index.js +2816 -651
  6. package/dist/browser/presentation/components/index.js +3143 -358
  7. package/dist/browser/presentation/hooks/index.js +961 -43
  8. package/dist/browser/presentation/index.js +2784 -666
  9. package/dist/core/agent-adapter.d.ts +53 -0
  10. package/dist/core/agent-tools-adapter.d.ts +12 -0
  11. package/dist/core/chat-service.d.ts +49 -1
  12. package/dist/core/contracts-context.d.ts +46 -0
  13. package/dist/core/contracts-context.test.d.ts +1 -0
  14. package/dist/core/conversation-store.d.ts +16 -2
  15. package/dist/core/create-chat-route.d.ts +3 -0
  16. package/dist/core/export-formatters.d.ts +29 -0
  17. package/dist/core/export-formatters.test.d.ts +1 -0
  18. package/dist/core/index.d.ts +8 -0
  19. package/dist/core/index.js +1138 -21
  20. package/dist/core/local-storage-conversation-store.d.ts +33 -0
  21. package/dist/core/message-types.d.ts +6 -0
  22. package/dist/core/surface-planner-tools.d.ts +23 -0
  23. package/dist/core/surface-planner-tools.test.d.ts +1 -0
  24. package/dist/core/thinking-levels.d.ts +38 -0
  25. package/dist/core/thinking-levels.test.d.ts +1 -0
  26. package/dist/core/workflow-tools.d.ts +18 -0
  27. package/dist/core/workflow-tools.test.d.ts +1 -0
  28. package/dist/index.d.ts +4 -2
  29. package/dist/index.js +2816 -651
  30. package/dist/node/core/index.js +1138 -21
  31. package/dist/node/index.js +2816 -651
  32. package/dist/node/presentation/components/index.js +3143 -358
  33. package/dist/node/presentation/hooks/index.js +961 -43
  34. package/dist/node/presentation/index.js +2787 -669
  35. package/dist/presentation/components/ChatContainer.d.ts +3 -1
  36. package/dist/presentation/components/ChatExportToolbar.d.ts +25 -0
  37. package/dist/presentation/components/ChatMessage.d.ts +16 -1
  38. package/dist/presentation/components/ChatSidebar.d.ts +26 -0
  39. package/dist/presentation/components/ChatWithExport.d.ts +34 -0
  40. package/dist/presentation/components/ChatWithSidebar.d.ts +19 -0
  41. package/dist/presentation/components/ThinkingLevelPicker.d.ts +16 -0
  42. package/dist/presentation/components/ToolResultRenderer.d.ts +33 -0
  43. package/dist/presentation/components/index.d.ts +6 -0
  44. package/dist/presentation/components/index.js +3143 -358
  45. package/dist/presentation/hooks/index.d.ts +2 -0
  46. package/dist/presentation/hooks/index.js +961 -43
  47. package/dist/presentation/hooks/useChat.d.ts +44 -2
  48. package/dist/presentation/hooks/useConversations.d.ts +18 -0
  49. package/dist/presentation/hooks/useMessageSelection.d.ts +13 -0
  50. package/dist/presentation/index.js +2787 -669
  51. package/package.json +14 -18
@@ -409,12 +409,13 @@ function createNodeFileOperations(workspacePath, allowWrites = false) {
409
409
  import * as React from "react";
410
410
  import { ScrollArea } from "@contractspec/lib.ui-kit-web/ui/scroll-area";
411
411
  import { cn } from "@contractspec/lib.ui-kit-web/ui/utils";
412
- import { jsxDEV } from "react/jsx-dev-runtime";
412
+ import { jsx, jsxs } from "react/jsx-runtime";
413
413
  "use client";
414
414
  function ChatContainer({
415
415
  children,
416
416
  className,
417
- showScrollButton = true
417
+ showScrollButton = true,
418
+ headerContent
418
419
  }) {
419
420
  const scrollRef = React.useRef(null);
420
421
  const [showScrollDown, setShowScrollDown] = React.useState(false);
@@ -441,24 +442,28 @@ function ChatContainer({
441
442
  });
442
443
  }
443
444
  }, []);
444
- return /* @__PURE__ */ jsxDEV("div", {
445
+ return /* @__PURE__ */ jsxs("div", {
445
446
  className: cn("relative flex flex-1 flex-col", className),
446
447
  children: [
447
- /* @__PURE__ */ jsxDEV(ScrollArea, {
448
+ headerContent && /* @__PURE__ */ jsx("div", {
449
+ className: "border-border flex shrink-0 items-center justify-end gap-2 border-b px-4 py-2",
450
+ children: headerContent
451
+ }),
452
+ /* @__PURE__ */ jsx(ScrollArea, {
448
453
  ref: scrollRef,
449
454
  className: "flex-1",
450
455
  onScroll: handleScroll,
451
- children: /* @__PURE__ */ jsxDEV("div", {
456
+ children: /* @__PURE__ */ jsx("div", {
452
457
  className: "flex flex-col gap-4 p-4",
453
458
  children
454
- }, undefined, false, undefined, this)
455
- }, undefined, false, undefined, this),
456
- showScrollButton && showScrollDown && /* @__PURE__ */ jsxDEV("button", {
459
+ })
460
+ }),
461
+ showScrollButton && showScrollDown && /* @__PURE__ */ jsxs("button", {
457
462
  onClick: scrollToBottom,
458
463
  className: cn("absolute bottom-4 left-1/2 -translate-x-1/2", "bg-primary text-primary-foreground", "rounded-full px-3 py-1.5 text-sm font-medium shadow-lg", "hover:bg-primary/90 transition-colors", "flex items-center gap-1.5"),
459
464
  "aria-label": "Scroll to bottom",
460
465
  children: [
461
- /* @__PURE__ */ jsxDEV("svg", {
466
+ /* @__PURE__ */ jsx("svg", {
462
467
  xmlns: "http://www.w3.org/2000/svg",
463
468
  width: "16",
464
469
  height: "16",
@@ -468,15 +473,15 @@ function ChatContainer({
468
473
  strokeWidth: "2",
469
474
  strokeLinecap: "round",
470
475
  strokeLinejoin: "round",
471
- children: /* @__PURE__ */ jsxDEV("path", {
476
+ children: /* @__PURE__ */ jsx("path", {
472
477
  d: "m6 9 6 6 6-6"
473
- }, undefined, false, undefined, this)
474
- }, undefined, false, undefined, this),
478
+ })
479
+ }),
475
480
  "New messages"
476
481
  ]
477
- }, undefined, true, undefined, this)
482
+ })
478
483
  ]
479
- }, undefined, true, undefined, this);
484
+ });
480
485
  }
481
486
  // src/presentation/components/ChatMessage.tsx
482
487
  import * as React3 from "react";
@@ -490,16 +495,19 @@ import {
490
495
  Copy as Copy2,
491
496
  Check as Check2,
492
497
  ExternalLink,
493
- Wrench
498
+ Wrench,
499
+ Pencil,
500
+ X
494
501
  } from "lucide-react";
495
502
  import { Button as Button2 } from "@contractspec/lib.design-system";
503
+ import { Checkbox } from "@contractspec/lib.ui-kit-web/ui/checkbox";
496
504
 
497
505
  // src/presentation/components/CodePreview.tsx
498
506
  import * as React2 from "react";
499
507
  import { cn as cn2 } from "@contractspec/lib.ui-kit-web/ui/utils";
500
508
  import { Button } from "@contractspec/lib.design-system";
501
509
  import { Copy, Check, Play, Download } from "lucide-react";
502
- import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
510
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
503
511
  "use client";
504
512
  var LANGUAGE_NAMES = {
505
513
  ts: "TypeScript",
@@ -552,93 +560,147 @@ function CodePreview({
552
560
  document.body.removeChild(a);
553
561
  URL.revokeObjectURL(url);
554
562
  }, [code, filename, language]);
555
- return /* @__PURE__ */ jsxDEV2("div", {
563
+ return /* @__PURE__ */ jsxs2("div", {
556
564
  className: cn2("overflow-hidden rounded-lg border", "bg-muted/50", className),
557
565
  children: [
558
- /* @__PURE__ */ jsxDEV2("div", {
566
+ /* @__PURE__ */ jsxs2("div", {
559
567
  className: cn2("flex items-center justify-between px-3 py-1.5", "bg-muted/80 border-b"),
560
568
  children: [
561
- /* @__PURE__ */ jsxDEV2("div", {
569
+ /* @__PURE__ */ jsxs2("div", {
562
570
  className: "flex items-center gap-2 text-sm",
563
571
  children: [
564
- filename && /* @__PURE__ */ jsxDEV2("span", {
572
+ filename && /* @__PURE__ */ jsx2("span", {
565
573
  className: "text-foreground font-mono",
566
574
  children: filename
567
- }, undefined, false, undefined, this),
568
- /* @__PURE__ */ jsxDEV2("span", {
575
+ }),
576
+ /* @__PURE__ */ jsx2("span", {
569
577
  className: "text-muted-foreground",
570
578
  children: displayLanguage
571
- }, undefined, false, undefined, this)
579
+ })
572
580
  ]
573
- }, undefined, true, undefined, this),
574
- /* @__PURE__ */ jsxDEV2("div", {
581
+ }),
582
+ /* @__PURE__ */ jsxs2("div", {
575
583
  className: "flex items-center gap-1",
576
584
  children: [
577
- showExecute && onExecute && /* @__PURE__ */ jsxDEV2(Button, {
585
+ showExecute && onExecute && /* @__PURE__ */ jsx2(Button, {
578
586
  variant: "ghost",
579
587
  size: "sm",
580
588
  onPress: () => onExecute(code),
581
589
  className: "h-7 w-7 p-0",
582
590
  "aria-label": "Execute code",
583
- children: /* @__PURE__ */ jsxDEV2(Play, {
591
+ children: /* @__PURE__ */ jsx2(Play, {
584
592
  className: "h-3.5 w-3.5"
585
- }, undefined, false, undefined, this)
586
- }, undefined, false, undefined, this),
587
- showDownload && /* @__PURE__ */ jsxDEV2(Button, {
593
+ })
594
+ }),
595
+ showDownload && /* @__PURE__ */ jsx2(Button, {
588
596
  variant: "ghost",
589
597
  size: "sm",
590
598
  onPress: handleDownload,
591
599
  className: "h-7 w-7 p-0",
592
600
  "aria-label": "Download code",
593
- children: /* @__PURE__ */ jsxDEV2(Download, {
601
+ children: /* @__PURE__ */ jsx2(Download, {
594
602
  className: "h-3.5 w-3.5"
595
- }, undefined, false, undefined, this)
596
- }, undefined, false, undefined, this),
597
- showCopy && /* @__PURE__ */ jsxDEV2(Button, {
603
+ })
604
+ }),
605
+ showCopy && /* @__PURE__ */ jsx2(Button, {
598
606
  variant: "ghost",
599
607
  size: "sm",
600
608
  onPress: handleCopy,
601
609
  className: "h-7 w-7 p-0",
602
610
  "aria-label": copied ? "Copied" : "Copy code",
603
- children: copied ? /* @__PURE__ */ jsxDEV2(Check, {
611
+ children: copied ? /* @__PURE__ */ jsx2(Check, {
604
612
  className: "h-3.5 w-3.5 text-green-500"
605
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV2(Copy, {
613
+ }) : /* @__PURE__ */ jsx2(Copy, {
606
614
  className: "h-3.5 w-3.5"
607
- }, undefined, false, undefined, this)
608
- }, undefined, false, undefined, this)
615
+ })
616
+ })
609
617
  ]
610
- }, undefined, true, undefined, this)
618
+ })
611
619
  ]
612
- }, undefined, true, undefined, this),
613
- /* @__PURE__ */ jsxDEV2("div", {
620
+ }),
621
+ /* @__PURE__ */ jsx2("div", {
614
622
  className: "overflow-auto",
615
623
  style: { maxHeight },
616
- children: /* @__PURE__ */ jsxDEV2("pre", {
624
+ children: /* @__PURE__ */ jsx2("pre", {
617
625
  className: "p-3",
618
- children: /* @__PURE__ */ jsxDEV2("code", {
626
+ children: /* @__PURE__ */ jsx2("code", {
619
627
  className: "text-sm",
620
- children: lines.map((line, i) => /* @__PURE__ */ jsxDEV2("div", {
628
+ children: lines.map((line, i) => /* @__PURE__ */ jsxs2("div", {
621
629
  className: "flex",
622
630
  children: [
623
- /* @__PURE__ */ jsxDEV2("span", {
631
+ /* @__PURE__ */ jsx2("span", {
624
632
  className: "text-muted-foreground mr-4 w-8 text-right select-none",
625
633
  children: i + 1
626
- }, undefined, false, undefined, this),
627
- /* @__PURE__ */ jsxDEV2("span", {
634
+ }),
635
+ /* @__PURE__ */ jsx2("span", {
628
636
  className: "flex-1",
629
637
  children: line || " "
630
- }, undefined, false, undefined, this)
638
+ })
631
639
  ]
632
- }, i, true, undefined, this))
633
- }, undefined, false, undefined, this)
634
- }, undefined, false, undefined, this)
635
- }, undefined, false, undefined, this)
640
+ }, i))
641
+ })
642
+ })
643
+ })
644
+ ]
645
+ });
646
+ }
647
+
648
+ // src/presentation/components/ToolResultRenderer.tsx
649
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
650
+ "use client";
651
+ function isPresentationToolResult(result) {
652
+ return typeof result === "object" && result !== null && "presentationKey" in result && typeof result.presentationKey === "string";
653
+ }
654
+ function isFormToolResult(result) {
655
+ return typeof result === "object" && result !== null && "formKey" in result && typeof result.formKey === "string";
656
+ }
657
+ function ToolResultRenderer({
658
+ toolName,
659
+ result,
660
+ presentationRenderer,
661
+ formRenderer,
662
+ showRawFallback = true
663
+ }) {
664
+ if (result === undefined || result === null) {
665
+ return null;
666
+ }
667
+ if (isPresentationToolResult(result) && presentationRenderer) {
668
+ const rendered = presentationRenderer(result.presentationKey, result.data);
669
+ if (rendered != null) {
670
+ return /* @__PURE__ */ jsx3("div", {
671
+ className: "mt-2 rounded-md border border-border bg-background/50 p-3",
672
+ children: rendered
673
+ });
674
+ }
675
+ }
676
+ if (isFormToolResult(result) && formRenderer) {
677
+ const rendered = formRenderer(result.formKey, result.defaultValues);
678
+ if (rendered != null) {
679
+ return /* @__PURE__ */ jsx3("div", {
680
+ className: "mt-2 rounded-md border border-border bg-background/50 p-3",
681
+ children: rendered
682
+ });
683
+ }
684
+ }
685
+ if (!showRawFallback) {
686
+ return null;
687
+ }
688
+ return /* @__PURE__ */ jsxs3("div", {
689
+ children: [
690
+ /* @__PURE__ */ jsx3("span", {
691
+ className: "text-muted-foreground font-medium",
692
+ children: "Output:"
693
+ }),
694
+ /* @__PURE__ */ jsx3("pre", {
695
+ className: "bg-background mt-1 overflow-x-auto rounded p-2 text-xs",
696
+ children: typeof result === "object" ? JSON.stringify(result, null, 2) : String(result)
697
+ })
636
698
  ]
637
- }, undefined, true, undefined, this);
699
+ });
638
700
  }
639
701
 
640
702
  // src/presentation/components/ChatMessage.tsx
641
- import { jsxDEV as jsxDEV3, Fragment } from "react/jsx-dev-runtime";
703
+ import { jsx as jsx4, jsxs as jsxs4, Fragment } from "react/jsx-runtime";
642
704
  "use client";
643
705
  function extractCodeBlocks(content) {
644
706
  const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
@@ -661,33 +723,33 @@ function renderInlineMarkdown(text) {
661
723
  let key = 0;
662
724
  while ((match = linkRegex.exec(text)) !== null) {
663
725
  if (match.index > lastIndex) {
664
- parts.push(/* @__PURE__ */ jsxDEV3("span", {
726
+ parts.push(/* @__PURE__ */ jsx4("span", {
665
727
  children: text.slice(lastIndex, match.index)
666
- }, key++, false, undefined, this));
728
+ }, key++));
667
729
  }
668
- parts.push(/* @__PURE__ */ jsxDEV3("a", {
730
+ parts.push(/* @__PURE__ */ jsx4("a", {
669
731
  href: match[2],
670
732
  target: "_blank",
671
733
  rel: "noopener noreferrer",
672
734
  className: "text-primary underline hover:no-underline",
673
735
  children: match[1]
674
- }, key++, false, undefined, this));
736
+ }, key++));
675
737
  lastIndex = match.index + match[0].length;
676
738
  }
677
739
  if (lastIndex < text.length) {
678
- parts.push(/* @__PURE__ */ jsxDEV3("span", {
740
+ parts.push(/* @__PURE__ */ jsx4("span", {
679
741
  children: text.slice(lastIndex)
680
- }, key++, false, undefined, this));
742
+ }, key++));
681
743
  }
682
744
  return parts.length > 0 ? parts : [text];
683
745
  }
684
746
  function MessageContent({ content }) {
685
747
  const codeBlocks = extractCodeBlocks(content);
686
748
  if (codeBlocks.length === 0) {
687
- return /* @__PURE__ */ jsxDEV3("p", {
749
+ return /* @__PURE__ */ jsx4("p", {
688
750
  className: "whitespace-pre-wrap",
689
751
  children: renderInlineMarkdown(content)
690
- }, undefined, false, undefined, this);
752
+ });
691
753
  }
692
754
  let remaining = content;
693
755
  const parts = [];
@@ -695,33 +757,40 @@ function MessageContent({ content }) {
695
757
  for (const block of codeBlocks) {
696
758
  const [before, after] = remaining.split(block.raw);
697
759
  if (before) {
698
- parts.push(/* @__PURE__ */ jsxDEV3("p", {
760
+ parts.push(/* @__PURE__ */ jsx4("p", {
699
761
  className: "whitespace-pre-wrap",
700
762
  children: renderInlineMarkdown(before.trim())
701
- }, key++, false, undefined, this));
763
+ }, key++));
702
764
  }
703
- parts.push(/* @__PURE__ */ jsxDEV3(CodePreview, {
765
+ parts.push(/* @__PURE__ */ jsx4(CodePreview, {
704
766
  code: block.code,
705
767
  language: block.language,
706
768
  className: "my-2"
707
- }, key++, false, undefined, this));
769
+ }, key++));
708
770
  remaining = after ?? "";
709
771
  }
710
772
  if (remaining.trim()) {
711
- parts.push(/* @__PURE__ */ jsxDEV3("p", {
773
+ parts.push(/* @__PURE__ */ jsx4("p", {
712
774
  className: "whitespace-pre-wrap",
713
775
  children: renderInlineMarkdown(remaining.trim())
714
- }, key++, false, undefined, this));
776
+ }, key++));
715
777
  }
716
- return /* @__PURE__ */ jsxDEV3(Fragment, {
778
+ return /* @__PURE__ */ jsx4(Fragment, {
717
779
  children: parts
718
- }, undefined, false, undefined, this);
780
+ });
719
781
  }
720
782
  function ChatMessage({
721
783
  message,
722
784
  className,
723
785
  showCopy = true,
724
- showAvatar = true
786
+ showAvatar = true,
787
+ selectable = false,
788
+ selected = false,
789
+ onSelect,
790
+ editable = false,
791
+ onEdit,
792
+ presentationRenderer,
793
+ formRenderer
725
794
  }) {
726
795
  const [copied, setCopied] = React3.useState(false);
727
796
  const isUser = message.role === "user";
@@ -732,185 +801,262 @@ function ChatMessage({
732
801
  setCopied(true);
733
802
  setTimeout(() => setCopied(false), 2000);
734
803
  }, [message.content]);
735
- return /* @__PURE__ */ jsxDEV3("div", {
804
+ const handleSelectChange = React3.useCallback((checked) => {
805
+ if (checked !== "indeterminate")
806
+ onSelect?.(message.id);
807
+ }, [message.id, onSelect]);
808
+ const [isEditing, setIsEditing] = React3.useState(false);
809
+ const [editContent, setEditContent] = React3.useState(message.content);
810
+ React3.useEffect(() => {
811
+ setEditContent(message.content);
812
+ }, [message.content]);
813
+ const handleStartEdit = React3.useCallback(() => {
814
+ setEditContent(message.content);
815
+ setIsEditing(true);
816
+ }, [message.content]);
817
+ const handleSaveEdit = React3.useCallback(async () => {
818
+ const trimmed = editContent.trim();
819
+ if (trimmed !== message.content) {
820
+ await onEdit?.(message.id, trimmed);
821
+ }
822
+ setIsEditing(false);
823
+ }, [editContent, message.id, message.content, onEdit]);
824
+ const handleCancelEdit = React3.useCallback(() => {
825
+ setEditContent(message.content);
826
+ setIsEditing(false);
827
+ }, [message.content]);
828
+ return /* @__PURE__ */ jsxs4("div", {
736
829
  className: cn3("group flex gap-3", isUser && "flex-row-reverse", className),
737
830
  children: [
738
- showAvatar && /* @__PURE__ */ jsxDEV3(Avatar, {
831
+ selectable && /* @__PURE__ */ jsx4("div", {
832
+ className: cn3("flex shrink-0 items-start pt-1", "opacity-0 transition-opacity group-hover:opacity-100"),
833
+ children: /* @__PURE__ */ jsx4(Checkbox, {
834
+ checked: selected,
835
+ onCheckedChange: handleSelectChange,
836
+ "aria-label": selected ? "Deselect message" : "Select message"
837
+ })
838
+ }),
839
+ showAvatar && /* @__PURE__ */ jsx4(Avatar, {
739
840
  className: "h-8 w-8 shrink-0",
740
- children: /* @__PURE__ */ jsxDEV3(AvatarFallback, {
841
+ children: /* @__PURE__ */ jsx4(AvatarFallback, {
741
842
  className: cn3(isUser ? "bg-primary text-primary-foreground" : "bg-muted"),
742
- children: isUser ? /* @__PURE__ */ jsxDEV3(User, {
843
+ children: isUser ? /* @__PURE__ */ jsx4(User, {
743
844
  className: "h-4 w-4"
744
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV3(Bot, {
845
+ }) : /* @__PURE__ */ jsx4(Bot, {
745
846
  className: "h-4 w-4"
746
- }, undefined, false, undefined, this)
747
- }, undefined, false, undefined, this)
748
- }, undefined, false, undefined, this),
749
- /* @__PURE__ */ jsxDEV3("div", {
847
+ })
848
+ })
849
+ }),
850
+ /* @__PURE__ */ jsxs4("div", {
750
851
  className: cn3("flex max-w-[80%] flex-col gap-1", isUser && "items-end"),
751
852
  children: [
752
- /* @__PURE__ */ jsxDEV3("div", {
853
+ /* @__PURE__ */ jsx4("div", {
753
854
  className: cn3("rounded-2xl px-4 py-2", isUser ? "bg-primary text-primary-foreground" : "bg-muted text-foreground", isError && "border-destructive bg-destructive/10 border"),
754
- children: isError && message.error ? /* @__PURE__ */ jsxDEV3("div", {
855
+ children: isError && message.error ? /* @__PURE__ */ jsxs4("div", {
755
856
  className: "flex items-start gap-2",
756
857
  children: [
757
- /* @__PURE__ */ jsxDEV3(AlertCircle, {
858
+ /* @__PURE__ */ jsx4(AlertCircle, {
758
859
  className: "text-destructive mt-0.5 h-4 w-4 shrink-0"
759
- }, undefined, false, undefined, this),
760
- /* @__PURE__ */ jsxDEV3("div", {
860
+ }),
861
+ /* @__PURE__ */ jsxs4("div", {
761
862
  children: [
762
- /* @__PURE__ */ jsxDEV3("p", {
863
+ /* @__PURE__ */ jsx4("p", {
763
864
  className: "text-destructive font-medium",
764
865
  children: message.error.code
765
- }, undefined, false, undefined, this),
766
- /* @__PURE__ */ jsxDEV3("p", {
866
+ }),
867
+ /* @__PURE__ */ jsx4("p", {
767
868
  className: "text-muted-foreground text-sm",
768
869
  children: message.error.message
769
- }, undefined, false, undefined, this)
870
+ })
871
+ ]
872
+ })
873
+ ]
874
+ }) : isEditing ? /* @__PURE__ */ jsxs4("div", {
875
+ className: "flex flex-col gap-2",
876
+ children: [
877
+ /* @__PURE__ */ jsx4("textarea", {
878
+ value: editContent,
879
+ onChange: (e) => setEditContent(e.target.value),
880
+ className: "bg-background/50 min-h-[80px] w-full resize-y rounded-md border px-3 py-2 text-sm",
881
+ rows: 4,
882
+ autoFocus: true
883
+ }),
884
+ /* @__PURE__ */ jsxs4("div", {
885
+ className: "flex gap-2",
886
+ children: [
887
+ /* @__PURE__ */ jsxs4(Button2, {
888
+ variant: "default",
889
+ size: "sm",
890
+ onPress: handleSaveEdit,
891
+ "aria-label": "Save edit",
892
+ children: [
893
+ /* @__PURE__ */ jsx4(Check2, {
894
+ className: "h-3 w-3"
895
+ }),
896
+ "Save"
897
+ ]
898
+ }),
899
+ /* @__PURE__ */ jsxs4(Button2, {
900
+ variant: "ghost",
901
+ size: "sm",
902
+ onPress: handleCancelEdit,
903
+ "aria-label": "Cancel edit",
904
+ children: [
905
+ /* @__PURE__ */ jsx4(X, {
906
+ className: "h-3 w-3"
907
+ }),
908
+ "Cancel"
909
+ ]
910
+ })
770
911
  ]
771
- }, undefined, true, undefined, this)
912
+ })
772
913
  ]
773
- }, undefined, true, undefined, this) : isStreaming && !message.content ? /* @__PURE__ */ jsxDEV3("div", {
914
+ }) : isStreaming && !message.content ? /* @__PURE__ */ jsxs4("div", {
774
915
  className: "flex flex-col gap-2",
775
916
  children: [
776
- /* @__PURE__ */ jsxDEV3(Skeleton, {
917
+ /* @__PURE__ */ jsx4(Skeleton, {
777
918
  className: "h-4 w-48"
778
- }, undefined, false, undefined, this),
779
- /* @__PURE__ */ jsxDEV3(Skeleton, {
919
+ }),
920
+ /* @__PURE__ */ jsx4(Skeleton, {
780
921
  className: "h-4 w-32"
781
- }, undefined, false, undefined, this)
922
+ })
782
923
  ]
783
- }, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV3(MessageContent, {
924
+ }) : /* @__PURE__ */ jsx4(MessageContent, {
784
925
  content: message.content
785
- }, undefined, false, undefined, this)
786
- }, undefined, false, undefined, this),
787
- /* @__PURE__ */ jsxDEV3("div", {
926
+ })
927
+ }),
928
+ /* @__PURE__ */ jsxs4("div", {
788
929
  className: cn3("flex items-center gap-2 text-xs", "text-muted-foreground opacity-0 transition-opacity", "group-hover:opacity-100"),
789
930
  children: [
790
- /* @__PURE__ */ jsxDEV3("span", {
931
+ /* @__PURE__ */ jsx4("span", {
791
932
  children: new Date(message.createdAt).toLocaleTimeString([], {
792
933
  hour: "2-digit",
793
934
  minute: "2-digit"
794
935
  })
795
- }, undefined, false, undefined, this),
796
- message.usage && /* @__PURE__ */ jsxDEV3("span", {
936
+ }),
937
+ message.usage && /* @__PURE__ */ jsxs4("span", {
797
938
  children: [
798
939
  message.usage.inputTokens + message.usage.outputTokens,
799
940
  " tokens"
800
941
  ]
801
- }, undefined, true, undefined, this),
802
- showCopy && !isUser && message.content && /* @__PURE__ */ jsxDEV3(Button2, {
942
+ }),
943
+ showCopy && !isUser && message.content && /* @__PURE__ */ jsx4(Button2, {
803
944
  variant: "ghost",
804
945
  size: "sm",
805
946
  className: "h-6 w-6 p-0",
806
947
  onPress: handleCopy,
807
948
  "aria-label": copied ? "Copied" : "Copy message",
808
- children: copied ? /* @__PURE__ */ jsxDEV3(Check2, {
949
+ children: copied ? /* @__PURE__ */ jsx4(Check2, {
950
+ className: "h-3 w-3"
951
+ }) : /* @__PURE__ */ jsx4(Copy2, {
809
952
  className: "h-3 w-3"
810
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV3(Copy2, {
953
+ })
954
+ }),
955
+ editable && isUser && !isEditing && /* @__PURE__ */ jsx4(Button2, {
956
+ variant: "ghost",
957
+ size: "sm",
958
+ className: "h-6 w-6 p-0",
959
+ onPress: handleStartEdit,
960
+ "aria-label": "Edit message",
961
+ children: /* @__PURE__ */ jsx4(Pencil, {
811
962
  className: "h-3 w-3"
812
- }, undefined, false, undefined, this)
813
- }, undefined, false, undefined, this)
963
+ })
964
+ })
814
965
  ]
815
- }, undefined, true, undefined, this),
816
- message.reasoning && /* @__PURE__ */ jsxDEV3("details", {
966
+ }),
967
+ message.reasoning && /* @__PURE__ */ jsxs4("details", {
817
968
  className: "text-muted-foreground mt-2 text-sm",
818
969
  children: [
819
- /* @__PURE__ */ jsxDEV3("summary", {
970
+ /* @__PURE__ */ jsx4("summary", {
820
971
  className: "cursor-pointer hover:underline",
821
972
  children: "View reasoning"
822
- }, undefined, false, undefined, this),
823
- /* @__PURE__ */ jsxDEV3("div", {
973
+ }),
974
+ /* @__PURE__ */ jsx4("div", {
824
975
  className: "bg-muted mt-1 rounded-md p-2",
825
- children: /* @__PURE__ */ jsxDEV3("p", {
976
+ children: /* @__PURE__ */ jsx4("p", {
826
977
  className: "whitespace-pre-wrap",
827
978
  children: message.reasoning
828
- }, undefined, false, undefined, this)
829
- }, undefined, false, undefined, this)
979
+ })
980
+ })
830
981
  ]
831
- }, undefined, true, undefined, this),
832
- message.sources && message.sources.length > 0 && /* @__PURE__ */ jsxDEV3("div", {
982
+ }),
983
+ message.sources && message.sources.length > 0 && /* @__PURE__ */ jsx4("div", {
833
984
  className: "mt-2 flex flex-wrap gap-2",
834
- children: message.sources.map((source) => /* @__PURE__ */ jsxDEV3("a", {
985
+ children: message.sources.map((source) => /* @__PURE__ */ jsxs4("a", {
835
986
  href: source.url ?? "#",
836
987
  target: "_blank",
837
988
  rel: "noopener noreferrer",
838
989
  className: "text-muted-foreground hover:text-foreground bg-muted inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors",
839
990
  children: [
840
- /* @__PURE__ */ jsxDEV3(ExternalLink, {
991
+ /* @__PURE__ */ jsx4(ExternalLink, {
841
992
  className: "h-3 w-3"
842
- }, undefined, false, undefined, this),
993
+ }),
843
994
  source.title || source.url || source.id
844
995
  ]
845
- }, source.id, true, undefined, this))
846
- }, undefined, false, undefined, this),
847
- message.toolCalls && message.toolCalls.length > 0 && /* @__PURE__ */ jsxDEV3("div", {
996
+ }, source.id))
997
+ }),
998
+ message.toolCalls && message.toolCalls.length > 0 && /* @__PURE__ */ jsx4("div", {
848
999
  className: "mt-2 space-y-2",
849
- children: message.toolCalls.map((tc) => /* @__PURE__ */ jsxDEV3("details", {
1000
+ children: message.toolCalls.map((tc) => /* @__PURE__ */ jsxs4("details", {
850
1001
  className: "bg-muted border-border rounded-md border",
851
1002
  children: [
852
- /* @__PURE__ */ jsxDEV3("summary", {
1003
+ /* @__PURE__ */ jsxs4("summary", {
853
1004
  className: "flex cursor-pointer items-center gap-2 px-3 py-2 text-sm font-medium",
854
1005
  children: [
855
- /* @__PURE__ */ jsxDEV3(Wrench, {
1006
+ /* @__PURE__ */ jsx4(Wrench, {
856
1007
  className: "text-muted-foreground h-4 w-4"
857
- }, undefined, false, undefined, this),
1008
+ }),
858
1009
  tc.name,
859
- /* @__PURE__ */ jsxDEV3("span", {
1010
+ /* @__PURE__ */ jsx4("span", {
860
1011
  className: cn3("ml-auto rounded px-1.5 py-0.5 text-xs", tc.status === "completed" && "bg-green-500/20 text-green-700 dark:text-green-400", tc.status === "error" && "bg-destructive/20 text-destructive", tc.status === "running" && "bg-blue-500/20 text-blue-700 dark:text-blue-400"),
861
1012
  children: tc.status
862
- }, undefined, false, undefined, this)
1013
+ })
863
1014
  ]
864
- }, undefined, true, undefined, this),
865
- /* @__PURE__ */ jsxDEV3("div", {
1015
+ }),
1016
+ /* @__PURE__ */ jsxs4("div", {
866
1017
  className: "border-border border-t px-3 py-2 text-xs",
867
1018
  children: [
868
- Object.keys(tc.args).length > 0 && /* @__PURE__ */ jsxDEV3("div", {
1019
+ Object.keys(tc.args).length > 0 && /* @__PURE__ */ jsxs4("div", {
869
1020
  className: "mb-2",
870
1021
  children: [
871
- /* @__PURE__ */ jsxDEV3("span", {
1022
+ /* @__PURE__ */ jsx4("span", {
872
1023
  className: "text-muted-foreground font-medium",
873
1024
  children: "Input:"
874
- }, undefined, false, undefined, this),
875
- /* @__PURE__ */ jsxDEV3("pre", {
1025
+ }),
1026
+ /* @__PURE__ */ jsx4("pre", {
876
1027
  className: "bg-background mt-1 overflow-x-auto rounded p-2",
877
1028
  children: JSON.stringify(tc.args, null, 2)
878
- }, undefined, false, undefined, this)
1029
+ })
879
1030
  ]
880
- }, undefined, true, undefined, this),
881
- tc.result !== undefined && /* @__PURE__ */ jsxDEV3("div", {
882
- children: [
883
- /* @__PURE__ */ jsxDEV3("span", {
884
- className: "text-muted-foreground font-medium",
885
- children: "Output:"
886
- }, undefined, false, undefined, this),
887
- /* @__PURE__ */ jsxDEV3("pre", {
888
- className: "bg-background mt-1 overflow-x-auto rounded p-2",
889
- children: typeof tc.result === "object" ? JSON.stringify(tc.result, null, 2) : String(tc.result)
890
- }, undefined, false, undefined, this)
891
- ]
892
- }, undefined, true, undefined, this),
893
- tc.error && /* @__PURE__ */ jsxDEV3("p", {
1031
+ }),
1032
+ tc.result !== undefined && /* @__PURE__ */ jsx4(ToolResultRenderer, {
1033
+ toolName: tc.name,
1034
+ result: tc.result,
1035
+ presentationRenderer,
1036
+ formRenderer,
1037
+ showRawFallback: true
1038
+ }),
1039
+ tc.error && /* @__PURE__ */ jsx4("p", {
894
1040
  className: "text-destructive mt-1",
895
1041
  children: tc.error
896
- }, undefined, false, undefined, this)
1042
+ })
897
1043
  ]
898
- }, undefined, true, undefined, this)
1044
+ })
899
1045
  ]
900
- }, tc.id, true, undefined, this))
901
- }, undefined, false, undefined, this)
1046
+ }, tc.id))
1047
+ })
902
1048
  ]
903
- }, undefined, true, undefined, this)
1049
+ })
904
1050
  ]
905
- }, undefined, true, undefined, this);
1051
+ });
906
1052
  }
907
1053
  // src/presentation/components/ChatInput.tsx
908
1054
  import * as React4 from "react";
909
1055
  import { cn as cn4 } from "@contractspec/lib.ui-kit-web/ui/utils";
910
1056
  import { Textarea } from "@contractspec/lib.design-system";
911
1057
  import { Button as Button3 } from "@contractspec/lib.design-system";
912
- import { Send, Paperclip, X, Loader2, FileText, Code } from "lucide-react";
913
- import { jsxDEV as jsxDEV4, Fragment as Fragment2 } from "react/jsx-dev-runtime";
1058
+ import { Send, Paperclip, X as X2, Loader2, FileText, Code } from "lucide-react";
1059
+ import { jsx as jsx5, jsxs as jsxs5, Fragment as Fragment2 } from "react/jsx-runtime";
914
1060
  "use client";
915
1061
  function ChatInput({
916
1062
  onSend,
@@ -976,42 +1122,42 @@ function ChatInput({
976
1122
  const removeAttachment = React4.useCallback((id) => {
977
1123
  setAttachments((prev) => prev.filter((a) => a.id !== id));
978
1124
  }, []);
979
- return /* @__PURE__ */ jsxDEV4("div", {
1125
+ return /* @__PURE__ */ jsxs5("div", {
980
1126
  className: cn4("flex flex-col gap-2", className),
981
1127
  children: [
982
- attachments.length > 0 && /* @__PURE__ */ jsxDEV4("div", {
1128
+ attachments.length > 0 && /* @__PURE__ */ jsx5("div", {
983
1129
  className: "flex flex-wrap gap-2",
984
- children: attachments.map((attachment) => /* @__PURE__ */ jsxDEV4("div", {
1130
+ children: attachments.map((attachment) => /* @__PURE__ */ jsxs5("div", {
985
1131
  className: cn4("flex items-center gap-1.5 rounded-md px-2 py-1", "bg-muted text-muted-foreground text-sm"),
986
1132
  children: [
987
- attachment.type === "code" ? /* @__PURE__ */ jsxDEV4(Code, {
1133
+ attachment.type === "code" ? /* @__PURE__ */ jsx5(Code, {
988
1134
  className: "h-3.5 w-3.5"
989
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV4(FileText, {
1135
+ }) : /* @__PURE__ */ jsx5(FileText, {
990
1136
  className: "h-3.5 w-3.5"
991
- }, undefined, false, undefined, this),
992
- /* @__PURE__ */ jsxDEV4("span", {
1137
+ }),
1138
+ /* @__PURE__ */ jsx5("span", {
993
1139
  className: "max-w-[150px] truncate",
994
1140
  children: attachment.name
995
- }, undefined, false, undefined, this),
996
- /* @__PURE__ */ jsxDEV4("button", {
1141
+ }),
1142
+ /* @__PURE__ */ jsx5("button", {
997
1143
  type: "button",
998
1144
  onClick: () => removeAttachment(attachment.id),
999
1145
  className: "hover:text-foreground",
1000
1146
  "aria-label": `Remove ${attachment.name}`,
1001
- children: /* @__PURE__ */ jsxDEV4(X, {
1147
+ children: /* @__PURE__ */ jsx5(X2, {
1002
1148
  className: "h-3.5 w-3.5"
1003
- }, undefined, false, undefined, this)
1004
- }, undefined, false, undefined, this)
1149
+ })
1150
+ })
1005
1151
  ]
1006
- }, attachment.id, true, undefined, this))
1007
- }, undefined, false, undefined, this),
1008
- /* @__PURE__ */ jsxDEV4("form", {
1152
+ }, attachment.id))
1153
+ }),
1154
+ /* @__PURE__ */ jsxs5("form", {
1009
1155
  onSubmit: handleSubmit,
1010
1156
  className: "flex items-end gap-2",
1011
1157
  children: [
1012
- showAttachments && /* @__PURE__ */ jsxDEV4(Fragment2, {
1158
+ showAttachments && /* @__PURE__ */ jsxs5(Fragment2, {
1013
1159
  children: [
1014
- /* @__PURE__ */ jsxDEV4("input", {
1160
+ /* @__PURE__ */ jsx5("input", {
1015
1161
  ref: fileInputRef,
1016
1162
  type: "file",
1017
1163
  multiple: true,
@@ -1019,23 +1165,23 @@ function ChatInput({
1019
1165
  onChange: handleFileSelect,
1020
1166
  className: "hidden",
1021
1167
  "aria-label": "Attach files"
1022
- }, undefined, false, undefined, this),
1023
- /* @__PURE__ */ jsxDEV4(Button3, {
1168
+ }),
1169
+ /* @__PURE__ */ jsx5(Button3, {
1024
1170
  type: "button",
1025
1171
  variant: "ghost",
1026
1172
  size: "sm",
1027
1173
  onPress: () => fileInputRef.current?.click(),
1028
1174
  disabled: disabled || attachments.length >= maxAttachments,
1029
1175
  "aria-label": "Attach files",
1030
- children: /* @__PURE__ */ jsxDEV4(Paperclip, {
1176
+ children: /* @__PURE__ */ jsx5(Paperclip, {
1031
1177
  className: "h-4 w-4"
1032
- }, undefined, false, undefined, this)
1033
- }, undefined, false, undefined, this)
1178
+ })
1179
+ })
1034
1180
  ]
1035
- }, undefined, true, undefined, this),
1036
- /* @__PURE__ */ jsxDEV4("div", {
1181
+ }),
1182
+ /* @__PURE__ */ jsx5("div", {
1037
1183
  className: "relative flex-1",
1038
- children: /* @__PURE__ */ jsxDEV4(Textarea, {
1184
+ children: /* @__PURE__ */ jsx5(Textarea, {
1039
1185
  value: content,
1040
1186
  onChange: (e) => setContent(e.target.value),
1041
1187
  onKeyDown: handleKeyDown,
@@ -1044,32 +1190,418 @@ function ChatInput({
1044
1190
  className: cn4("max-h-[200px] min-h-[44px] resize-none pr-12", "focus-visible:ring-1"),
1045
1191
  rows: 1,
1046
1192
  "aria-label": "Chat message"
1047
- }, undefined, false, undefined, this)
1048
- }, undefined, false, undefined, this),
1049
- /* @__PURE__ */ jsxDEV4(Button3, {
1193
+ })
1194
+ }),
1195
+ /* @__PURE__ */ jsx5(Button3, {
1050
1196
  type: "submit",
1051
1197
  disabled: !canSend || disabled || isLoading,
1052
1198
  size: "sm",
1053
1199
  "aria-label": isLoading ? "Sending..." : "Send message",
1054
- children: isLoading ? /* @__PURE__ */ jsxDEV4(Loader2, {
1200
+ children: isLoading ? /* @__PURE__ */ jsx5(Loader2, {
1055
1201
  className: "h-4 w-4 animate-spin"
1056
- }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV4(Send, {
1202
+ }) : /* @__PURE__ */ jsx5(Send, {
1057
1203
  className: "h-4 w-4"
1058
- }, undefined, false, undefined, this)
1059
- }, undefined, false, undefined, this)
1204
+ })
1205
+ })
1060
1206
  ]
1061
- }, undefined, true, undefined, this),
1062
- /* @__PURE__ */ jsxDEV4("p", {
1207
+ }),
1208
+ /* @__PURE__ */ jsx5("p", {
1063
1209
  className: "text-muted-foreground text-xs",
1064
1210
  children: "Press Enter to send, Shift+Enter for new line"
1065
- }, undefined, false, undefined, this)
1211
+ })
1066
1212
  ]
1067
- }, undefined, true, undefined, this);
1213
+ });
1068
1214
  }
1069
- // src/presentation/components/ModelPicker.tsx
1215
+ // src/presentation/components/ChatExportToolbar.tsx
1070
1216
  import * as React5 from "react";
1071
- import { cn as cn5 } from "@contractspec/lib.ui-kit-web/ui/utils";
1217
+ import { Download as Download2, FileText as FileText2, Copy as Copy3, Check as Check3, Plus, GitFork } from "lucide-react";
1072
1218
  import { Button as Button4 } from "@contractspec/lib.design-system";
1219
+ import {
1220
+ DropdownMenu,
1221
+ DropdownMenuContent,
1222
+ DropdownMenuItem,
1223
+ DropdownMenuSeparator,
1224
+ DropdownMenuTrigger
1225
+ } from "@contractspec/lib.ui-kit-web/ui/dropdown-menu";
1226
+
1227
+ // src/core/export-formatters.ts
1228
+ function formatTimestamp(date) {
1229
+ return date.toLocaleTimeString([], {
1230
+ hour: "2-digit",
1231
+ minute: "2-digit"
1232
+ });
1233
+ }
1234
+ function toIsoString(date) {
1235
+ return date.toISOString();
1236
+ }
1237
+ function messageToJsonSerializable(msg) {
1238
+ return {
1239
+ id: msg.id,
1240
+ conversationId: msg.conversationId,
1241
+ role: msg.role,
1242
+ content: msg.content,
1243
+ status: msg.status,
1244
+ createdAt: toIsoString(msg.createdAt),
1245
+ updatedAt: toIsoString(msg.updatedAt),
1246
+ ...msg.attachments && { attachments: msg.attachments },
1247
+ ...msg.codeBlocks && { codeBlocks: msg.codeBlocks },
1248
+ ...msg.toolCalls && { toolCalls: msg.toolCalls },
1249
+ ...msg.sources && { sources: msg.sources },
1250
+ ...msg.reasoning && { reasoning: msg.reasoning },
1251
+ ...msg.usage && { usage: msg.usage },
1252
+ ...msg.error && { error: msg.error },
1253
+ ...msg.metadata && { metadata: msg.metadata }
1254
+ };
1255
+ }
1256
+ function formatSourcesMarkdown(sources) {
1257
+ if (sources.length === 0)
1258
+ return "";
1259
+ return `
1260
+
1261
+ **Sources:**
1262
+ ` + sources.map((s) => `- [${s.title}](${s.url ?? "#"})`).join(`
1263
+ `);
1264
+ }
1265
+ function formatSourcesTxt(sources) {
1266
+ if (sources.length === 0)
1267
+ return "";
1268
+ return `
1269
+
1270
+ Sources:
1271
+ ` + sources.map((s) => `- ${s.title}${s.url ? ` - ${s.url}` : ""}`).join(`
1272
+ `);
1273
+ }
1274
+ function formatToolCallsMarkdown(toolCalls) {
1275
+ if (toolCalls.length === 0)
1276
+ return "";
1277
+ return `
1278
+
1279
+ **Tool calls:**
1280
+ ` + toolCalls.map((tc) => `**${tc.name}** (${tc.status})
1281
+ \`\`\`json
1282
+ ${JSON.stringify(tc.args, null, 2)}
1283
+ \`\`\`` + (tc.result !== undefined ? `
1284
+ Output:
1285
+ \`\`\`json
1286
+ ${typeof tc.result === "object" ? JSON.stringify(tc.result, null, 2) : String(tc.result)}
1287
+ \`\`\`` : "") + (tc.error ? `
1288
+ Error: ${tc.error}` : "")).join(`
1289
+
1290
+ `);
1291
+ }
1292
+ function formatToolCallsTxt(toolCalls) {
1293
+ if (toolCalls.length === 0)
1294
+ return "";
1295
+ return `
1296
+
1297
+ Tool calls:
1298
+ ` + toolCalls.map((tc) => `- ${tc.name} (${tc.status}): ${JSON.stringify(tc.args)}` + (tc.result !== undefined ? ` -> ${typeof tc.result === "object" ? JSON.stringify(tc.result) : String(tc.result)}` : "") + (tc.error ? ` [Error: ${tc.error}]` : "")).join(`
1299
+ `);
1300
+ }
1301
+ function formatUsage(usage) {
1302
+ const total = usage.inputTokens + usage.outputTokens;
1303
+ return ` (${total} tokens)`;
1304
+ }
1305
+ function formatMessagesAsMarkdown(messages) {
1306
+ const parts = [];
1307
+ for (const msg of messages) {
1308
+ const roleLabel = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "System";
1309
+ const header = `## ${roleLabel}`;
1310
+ const timestamp = `*${formatTimestamp(msg.createdAt)}*`;
1311
+ const usageSuffix = msg.usage ? formatUsage(msg.usage) : "";
1312
+ const meta = `${timestamp}${usageSuffix}
1313
+
1314
+ `;
1315
+ let body = msg.content;
1316
+ if (msg.error) {
1317
+ body += `
1318
+
1319
+ **Error:** ${msg.error.code} - ${msg.error.message}`;
1320
+ }
1321
+ if (msg.reasoning) {
1322
+ body += `
1323
+
1324
+ > **Reasoning:**
1325
+ > ${msg.reasoning.replace(/\n/g, `
1326
+ > `)}`;
1327
+ }
1328
+ body += formatSourcesMarkdown(msg.sources ?? []);
1329
+ body += formatToolCallsMarkdown(msg.toolCalls ?? []);
1330
+ parts.push(`${header}
1331
+
1332
+ ${meta}${body}`);
1333
+ }
1334
+ return parts.join(`
1335
+
1336
+ ---
1337
+
1338
+ `);
1339
+ }
1340
+ function formatMessagesAsTxt(messages) {
1341
+ const parts = [];
1342
+ for (const msg of messages) {
1343
+ const roleLabel = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "System";
1344
+ const timestamp = `(${formatTimestamp(msg.createdAt)})`;
1345
+ const usageSuffix = msg.usage ? formatUsage(msg.usage) : "";
1346
+ const header = `[${roleLabel}] ${timestamp}${usageSuffix}
1347
+
1348
+ `;
1349
+ let body = msg.content;
1350
+ if (msg.error) {
1351
+ body += `
1352
+
1353
+ Error: ${msg.error.code} - ${msg.error.message}`;
1354
+ }
1355
+ if (msg.reasoning) {
1356
+ body += `
1357
+
1358
+ Reasoning: ${msg.reasoning}`;
1359
+ }
1360
+ body += formatSourcesTxt(msg.sources ?? []);
1361
+ body += formatToolCallsTxt(msg.toolCalls ?? []);
1362
+ parts.push(`${header}${body}`);
1363
+ }
1364
+ return parts.join(`
1365
+
1366
+ ---
1367
+
1368
+ `);
1369
+ }
1370
+ function formatMessagesAsJson(messages, conversation) {
1371
+ const payload = {
1372
+ messages: messages.map(messageToJsonSerializable)
1373
+ };
1374
+ if (conversation) {
1375
+ payload.conversation = {
1376
+ id: conversation.id,
1377
+ title: conversation.title,
1378
+ status: conversation.status,
1379
+ createdAt: toIsoString(conversation.createdAt),
1380
+ updatedAt: toIsoString(conversation.updatedAt),
1381
+ provider: conversation.provider,
1382
+ model: conversation.model,
1383
+ workspacePath: conversation.workspacePath,
1384
+ contextFiles: conversation.contextFiles,
1385
+ summary: conversation.summary,
1386
+ metadata: conversation.metadata
1387
+ };
1388
+ }
1389
+ return JSON.stringify(payload, null, 2);
1390
+ }
1391
+ function getExportFilename(format, conversation) {
1392
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
1393
+ const base = conversation?.title ? conversation.title.replace(/[^a-zA-Z0-9-_]/g, "_").slice(0, 40) : "chat-export";
1394
+ const ext = format === "markdown" ? "md" : format === "txt" ? "txt" : "json";
1395
+ return `${base}-${timestamp}.${ext}`;
1396
+ }
1397
+ var MIME_TYPES = {
1398
+ markdown: "text/markdown",
1399
+ txt: "text/plain",
1400
+ json: "application/json"
1401
+ };
1402
+ function downloadAsFile(content, filename, mimeType) {
1403
+ const blob = new Blob([content], { type: mimeType });
1404
+ const url = URL.createObjectURL(blob);
1405
+ const a = document.createElement("a");
1406
+ a.href = url;
1407
+ a.download = filename;
1408
+ document.body.appendChild(a);
1409
+ a.click();
1410
+ document.body.removeChild(a);
1411
+ URL.revokeObjectURL(url);
1412
+ }
1413
+ function exportToFile(messages, format, conversation) {
1414
+ let content;
1415
+ if (format === "markdown") {
1416
+ content = formatMessagesAsMarkdown(messages);
1417
+ } else if (format === "txt") {
1418
+ content = formatMessagesAsTxt(messages);
1419
+ } else {
1420
+ content = formatMessagesAsJson(messages, conversation);
1421
+ }
1422
+ const filename = getExportFilename(format, conversation);
1423
+ const mimeType = MIME_TYPES[format];
1424
+ downloadAsFile(content, filename, mimeType);
1425
+ }
1426
+
1427
+ // src/presentation/components/ChatExportToolbar.tsx
1428
+ import { jsx as jsx6, jsxs as jsxs6, Fragment as Fragment3 } from "react/jsx-runtime";
1429
+ "use client";
1430
+ function ChatExportToolbar({
1431
+ messages,
1432
+ conversation,
1433
+ selectedIds,
1434
+ onExported,
1435
+ showSelectionSummary = true,
1436
+ onSelectAll,
1437
+ onClearSelection,
1438
+ selectedCount = selectedIds.size,
1439
+ totalCount = messages.length,
1440
+ onCreateNew,
1441
+ onFork
1442
+ }) {
1443
+ const [copied, setCopied] = React5.useState(false);
1444
+ const toExport = React5.useMemo(() => {
1445
+ if (selectedIds.size > 0) {
1446
+ const idSet = selectedIds;
1447
+ return messages.filter((m) => idSet.has(m.id));
1448
+ }
1449
+ return messages;
1450
+ }, [messages, selectedIds]);
1451
+ const handleExport = React5.useCallback((format) => {
1452
+ exportToFile(toExport, format, conversation);
1453
+ onExported?.(format, toExport.length);
1454
+ }, [toExport, conversation, onExported]);
1455
+ const handleCopy = React5.useCallback(async () => {
1456
+ const content = formatMessagesAsMarkdown(toExport);
1457
+ await navigator.clipboard.writeText(content);
1458
+ setCopied(true);
1459
+ setTimeout(() => setCopied(false), 2000);
1460
+ onExported?.("markdown", toExport.length);
1461
+ }, [toExport, onExported]);
1462
+ const disabled = messages.length === 0;
1463
+ const [forking, setForking] = React5.useState(false);
1464
+ const handleFork = React5.useCallback(async (upToMessageId) => {
1465
+ if (!onFork)
1466
+ return;
1467
+ setForking(true);
1468
+ try {
1469
+ await onFork(upToMessageId);
1470
+ } finally {
1471
+ setForking(false);
1472
+ }
1473
+ }, [onFork]);
1474
+ return /* @__PURE__ */ jsxs6("div", {
1475
+ className: "flex items-center gap-2",
1476
+ children: [
1477
+ onCreateNew && /* @__PURE__ */ jsxs6(Button4, {
1478
+ variant: "outline",
1479
+ size: "sm",
1480
+ onPress: onCreateNew,
1481
+ "aria-label": "New conversation",
1482
+ children: [
1483
+ /* @__PURE__ */ jsx6(Plus, {
1484
+ className: "h-4 w-4"
1485
+ }),
1486
+ "New"
1487
+ ]
1488
+ }),
1489
+ onFork && messages.length > 0 && /* @__PURE__ */ jsxs6(Button4, {
1490
+ variant: "outline",
1491
+ size: "sm",
1492
+ disabled: forking,
1493
+ onPress: () => handleFork(),
1494
+ "aria-label": "Fork conversation",
1495
+ children: [
1496
+ /* @__PURE__ */ jsx6(GitFork, {
1497
+ className: "h-4 w-4"
1498
+ }),
1499
+ "Fork"
1500
+ ]
1501
+ }),
1502
+ showSelectionSummary && selectedCount > 0 && /* @__PURE__ */ jsxs6("span", {
1503
+ className: "text-muted-foreground text-sm",
1504
+ children: [
1505
+ selectedCount,
1506
+ " message",
1507
+ selectedCount !== 1 ? "s" : "",
1508
+ " selected"
1509
+ ]
1510
+ }),
1511
+ onSelectAll && onClearSelection && totalCount > 0 && /* @__PURE__ */ jsxs6(Fragment3, {
1512
+ children: [
1513
+ /* @__PURE__ */ jsx6(Button4, {
1514
+ variant: "ghost",
1515
+ size: "sm",
1516
+ onPress: onSelectAll,
1517
+ className: "text-xs",
1518
+ children: "Select all"
1519
+ }),
1520
+ selectedCount > 0 && /* @__PURE__ */ jsx6(Button4, {
1521
+ variant: "ghost",
1522
+ size: "sm",
1523
+ onPress: onClearSelection,
1524
+ className: "text-xs",
1525
+ children: "Clear"
1526
+ })
1527
+ ]
1528
+ }),
1529
+ /* @__PURE__ */ jsxs6(DropdownMenu, {
1530
+ children: [
1531
+ /* @__PURE__ */ jsx6(DropdownMenuTrigger, {
1532
+ asChild: true,
1533
+ children: /* @__PURE__ */ jsxs6(Button4, {
1534
+ variant: "outline",
1535
+ size: "sm",
1536
+ disabled,
1537
+ "aria-label": selectedCount > 0 ? "Export selected messages" : "Export conversation",
1538
+ children: [
1539
+ /* @__PURE__ */ jsx6(Download2, {
1540
+ className: "h-4 w-4"
1541
+ }),
1542
+ "Export"
1543
+ ]
1544
+ })
1545
+ }),
1546
+ /* @__PURE__ */ jsxs6(DropdownMenuContent, {
1547
+ align: "end",
1548
+ children: [
1549
+ /* @__PURE__ */ jsxs6(DropdownMenuItem, {
1550
+ onSelect: () => handleExport("markdown"),
1551
+ disabled,
1552
+ children: [
1553
+ /* @__PURE__ */ jsx6(FileText2, {
1554
+ className: "h-4 w-4"
1555
+ }),
1556
+ "Export as Markdown (.md)"
1557
+ ]
1558
+ }),
1559
+ /* @__PURE__ */ jsxs6(DropdownMenuItem, {
1560
+ onSelect: () => handleExport("txt"),
1561
+ disabled,
1562
+ children: [
1563
+ /* @__PURE__ */ jsx6(FileText2, {
1564
+ className: "h-4 w-4"
1565
+ }),
1566
+ "Export as Plain Text (.txt)"
1567
+ ]
1568
+ }),
1569
+ /* @__PURE__ */ jsxs6(DropdownMenuItem, {
1570
+ onSelect: () => handleExport("json"),
1571
+ disabled,
1572
+ children: [
1573
+ /* @__PURE__ */ jsx6(FileText2, {
1574
+ className: "h-4 w-4"
1575
+ }),
1576
+ "Export as JSON (.json)"
1577
+ ]
1578
+ }),
1579
+ /* @__PURE__ */ jsx6(DropdownMenuSeparator, {}),
1580
+ /* @__PURE__ */ jsxs6(DropdownMenuItem, {
1581
+ onSelect: () => handleCopy(),
1582
+ disabled,
1583
+ children: [
1584
+ copied ? /* @__PURE__ */ jsx6(Check3, {
1585
+ className: "h-4 w-4 text-green-500"
1586
+ }) : /* @__PURE__ */ jsx6(Copy3, {
1587
+ className: "h-4 w-4"
1588
+ }),
1589
+ copied ? "Copied to clipboard" : "Copy to clipboard"
1590
+ ]
1591
+ })
1592
+ ]
1593
+ })
1594
+ ]
1595
+ })
1596
+ ]
1597
+ });
1598
+ }
1599
+ // src/presentation/components/ChatWithExport.tsx
1600
+ import * as React8 from "react";
1601
+
1602
+ // src/presentation/components/ThinkingLevelPicker.tsx
1603
+ import * as React6 from "react";
1604
+ import { cn as cn5 } from "@contractspec/lib.ui-kit-web/ui/utils";
1073
1605
  import {
1074
1606
  Select,
1075
1607
  SelectContent,
@@ -1077,402 +1609,487 @@ import {
1077
1609
  SelectTrigger,
1078
1610
  SelectValue
1079
1611
  } from "@contractspec/lib.ui-kit-web/ui/select";
1080
- import { Badge } from "@contractspec/lib.ui-kit-web/ui/badge";
1081
1612
  import { Label } from "@contractspec/lib.ui-kit-web/ui/label";
1082
- import { Bot as Bot2, Cloud, Cpu, Sparkles } from "lucide-react";
1083
- import {
1084
- getModelsForProvider
1085
- } from "@contractspec/lib.ai-providers";
1086
- import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
1087
- "use client";
1088
- var PROVIDER_ICONS = {
1089
- ollama: /* @__PURE__ */ jsxDEV5(Cpu, {
1090
- className: "h-4 w-4"
1091
- }, undefined, false, undefined, this),
1092
- openai: /* @__PURE__ */ jsxDEV5(Bot2, {
1093
- className: "h-4 w-4"
1094
- }, undefined, false, undefined, this),
1095
- anthropic: /* @__PURE__ */ jsxDEV5(Sparkles, {
1096
- className: "h-4 w-4"
1097
- }, undefined, false, undefined, this),
1098
- mistral: /* @__PURE__ */ jsxDEV5(Cloud, {
1099
- className: "h-4 w-4"
1100
- }, undefined, false, undefined, this),
1101
- gemini: /* @__PURE__ */ jsxDEV5(Sparkles, {
1102
- className: "h-4 w-4"
1103
- }, undefined, false, undefined, this)
1104
- };
1105
- var PROVIDER_NAMES = {
1106
- ollama: "Ollama (Local)",
1107
- openai: "OpenAI",
1108
- anthropic: "Anthropic",
1109
- mistral: "Mistral",
1110
- gemini: "Google Gemini"
1613
+
1614
+ // src/core/thinking-levels.ts
1615
+ var THINKING_LEVEL_LABELS = {
1616
+ instant: "Instant",
1617
+ thinking: "Thinking",
1618
+ extra_thinking: "Extra Thinking",
1619
+ max: "Max"
1111
1620
  };
1112
- var MODE_BADGES = {
1113
- local: { label: "Local", variant: "secondary" },
1114
- byok: { label: "BYOK", variant: "outline" },
1115
- managed: { label: "Managed", variant: "default" }
1621
+ var THINKING_LEVEL_DESCRIPTIONS = {
1622
+ instant: "Fast responses, minimal reasoning",
1623
+ thinking: "Standard reasoning depth",
1624
+ extra_thinking: "More thorough reasoning",
1625
+ max: "Maximum reasoning depth"
1116
1626
  };
1117
- function ModelPicker({
1627
+ function getProviderOptions(level, providerName) {
1628
+ if (!level || level === "instant") {
1629
+ return {};
1630
+ }
1631
+ switch (providerName) {
1632
+ case "anthropic": {
1633
+ const budgetMap = {
1634
+ thinking: 8000,
1635
+ extra_thinking: 16000,
1636
+ max: 32000
1637
+ };
1638
+ return {
1639
+ anthropic: {
1640
+ thinking: { type: "enabled", budgetTokens: budgetMap[level] }
1641
+ }
1642
+ };
1643
+ }
1644
+ case "openai": {
1645
+ const effortMap = {
1646
+ thinking: "low",
1647
+ extra_thinking: "medium",
1648
+ max: "high"
1649
+ };
1650
+ return {
1651
+ openai: {
1652
+ reasoningEffort: effortMap[level]
1653
+ }
1654
+ };
1655
+ }
1656
+ case "ollama":
1657
+ case "mistral":
1658
+ case "gemini":
1659
+ return {};
1660
+ default:
1661
+ return {};
1662
+ }
1663
+ }
1664
+
1665
+ // src/presentation/components/ThinkingLevelPicker.tsx
1666
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1667
+ "use client";
1668
+ var THINKING_LEVELS = [
1669
+ "instant",
1670
+ "thinking",
1671
+ "extra_thinking",
1672
+ "max"
1673
+ ];
1674
+ function ThinkingLevelPicker({
1118
1675
  value,
1119
1676
  onChange,
1120
- availableProviders,
1121
1677
  className,
1122
1678
  compact = false
1123
1679
  }) {
1124
- const providers = availableProviders ?? [
1125
- { provider: "ollama", available: true, mode: "local" },
1126
- { provider: "openai", available: true, mode: "byok" },
1127
- { provider: "anthropic", available: true, mode: "byok" },
1128
- { provider: "mistral", available: true, mode: "byok" },
1129
- { provider: "gemini", available: true, mode: "byok" }
1130
- ];
1131
- const models = getModelsForProvider(value.provider);
1132
- const selectedModel = models.find((m) => m.id === value.model);
1133
- const handleProviderChange = React5.useCallback((providerName) => {
1134
- const provider = providerName;
1135
- const providerInfo = providers.find((p) => p.provider === provider);
1136
- const providerModels = getModelsForProvider(provider);
1137
- const defaultModel = providerModels[0]?.id ?? "";
1138
- onChange({
1139
- provider,
1140
- model: defaultModel,
1141
- mode: providerInfo?.mode ?? "byok"
1142
- });
1143
- }, [onChange, providers]);
1144
- const handleModelChange = React5.useCallback((modelId) => {
1145
- onChange({
1146
- ...value,
1147
- model: modelId
1148
- });
1149
- }, [onChange, value]);
1680
+ const handleChange = React6.useCallback((v) => {
1681
+ onChange(v);
1682
+ }, [onChange]);
1150
1683
  if (compact) {
1151
- return /* @__PURE__ */ jsxDEV5("div", {
1152
- className: cn5("flex items-center gap-2", className),
1684
+ return /* @__PURE__ */ jsxs7(Select, {
1685
+ value,
1686
+ onValueChange: handleChange,
1153
1687
  children: [
1154
- /* @__PURE__ */ jsxDEV5(Select, {
1155
- value: value.provider,
1156
- onValueChange: handleProviderChange,
1157
- children: [
1158
- /* @__PURE__ */ jsxDEV5(SelectTrigger, {
1159
- className: "w-[140px]",
1160
- children: /* @__PURE__ */ jsxDEV5(SelectValue, {}, undefined, false, undefined, this)
1161
- }, undefined, false, undefined, this),
1162
- /* @__PURE__ */ jsxDEV5(SelectContent, {
1163
- children: providers.map((p) => /* @__PURE__ */ jsxDEV5(SelectItem, {
1164
- value: p.provider,
1165
- disabled: !p.available,
1166
- children: /* @__PURE__ */ jsxDEV5("div", {
1167
- className: "flex items-center gap-2",
1168
- children: [
1169
- PROVIDER_ICONS[p.provider],
1170
- /* @__PURE__ */ jsxDEV5("span", {
1171
- children: PROVIDER_NAMES[p.provider]
1172
- }, undefined, false, undefined, this)
1173
- ]
1174
- }, undefined, true, undefined, this)
1175
- }, p.provider, false, undefined, this))
1176
- }, undefined, false, undefined, this)
1177
- ]
1178
- }, undefined, true, undefined, this),
1179
- /* @__PURE__ */ jsxDEV5(Select, {
1180
- value: value.model,
1181
- onValueChange: handleModelChange,
1182
- children: [
1183
- /* @__PURE__ */ jsxDEV5(SelectTrigger, {
1184
- className: "w-[160px]",
1185
- children: /* @__PURE__ */ jsxDEV5(SelectValue, {}, undefined, false, undefined, this)
1186
- }, undefined, false, undefined, this),
1187
- /* @__PURE__ */ jsxDEV5(SelectContent, {
1188
- children: models.map((m) => /* @__PURE__ */ jsxDEV5(SelectItem, {
1189
- value: m.id,
1190
- children: m.name
1191
- }, m.id, false, undefined, this))
1192
- }, undefined, false, undefined, this)
1193
- ]
1194
- }, undefined, true, undefined, this)
1688
+ /* @__PURE__ */ jsx7(SelectTrigger, {
1689
+ className: cn5("w-[140px]", className),
1690
+ children: /* @__PURE__ */ jsx7(SelectValue, {})
1691
+ }),
1692
+ /* @__PURE__ */ jsx7(SelectContent, {
1693
+ children: THINKING_LEVELS.map((level) => /* @__PURE__ */ jsx7(SelectItem, {
1694
+ value: level,
1695
+ children: THINKING_LEVEL_LABELS[level]
1696
+ }, level))
1697
+ })
1195
1698
  ]
1196
- }, undefined, true, undefined, this);
1699
+ });
1197
1700
  }
1198
- return /* @__PURE__ */ jsxDEV5("div", {
1199
- className: cn5("flex flex-col gap-3", className),
1701
+ return /* @__PURE__ */ jsxs7("div", {
1702
+ className: cn5("flex flex-col gap-1.5", className),
1200
1703
  children: [
1201
- /* @__PURE__ */ jsxDEV5("div", {
1202
- className: "flex flex-col gap-1.5",
1704
+ /* @__PURE__ */ jsx7(Label, {
1705
+ htmlFor: "thinking-level-picker",
1706
+ className: "text-sm font-medium",
1707
+ children: "Thinking Level"
1708
+ }),
1709
+ /* @__PURE__ */ jsxs7(Select, {
1710
+ name: "thinking-level-picker",
1711
+ value,
1712
+ onValueChange: handleChange,
1203
1713
  children: [
1204
- /* @__PURE__ */ jsxDEV5(Label, {
1205
- htmlFor: "provider-selection",
1206
- className: "text-sm font-medium",
1207
- children: "Provider"
1208
- }, undefined, false, undefined, this),
1209
- /* @__PURE__ */ jsxDEV5("div", {
1210
- className: "flex flex-wrap gap-2",
1211
- id: "provider-selection",
1212
- children: providers.map((p) => /* @__PURE__ */ jsxDEV5(Button4, {
1213
- variant: value.provider === p.provider ? "default" : "outline",
1214
- size: "sm",
1215
- onPress: () => p.available && handleProviderChange(p.provider),
1216
- disabled: !p.available,
1217
- className: cn5(!p.available && "opacity-50"),
1218
- children: [
1219
- PROVIDER_ICONS[p.provider],
1220
- /* @__PURE__ */ jsxDEV5("span", {
1221
- children: PROVIDER_NAMES[p.provider]
1222
- }, undefined, false, undefined, this),
1223
- /* @__PURE__ */ jsxDEV5(Badge, {
1224
- variant: MODE_BADGES[p.mode].variant,
1225
- className: "ml-1",
1226
- children: MODE_BADGES[p.mode].label
1227
- }, undefined, false, undefined, this)
1228
- ]
1229
- }, p.provider, true, undefined, this))
1230
- }, undefined, false, undefined, this)
1231
- ]
1232
- }, undefined, true, undefined, this),
1233
- /* @__PURE__ */ jsxDEV5("div", {
1234
- className: "flex flex-col gap-1.5",
1235
- children: [
1236
- /* @__PURE__ */ jsxDEV5(Label, {
1237
- htmlFor: "model-picker",
1238
- className: "text-sm font-medium",
1239
- children: "Model"
1240
- }, undefined, false, undefined, this),
1241
- /* @__PURE__ */ jsxDEV5(Select, {
1242
- name: "model-picker",
1243
- value: value.model,
1244
- onValueChange: handleModelChange,
1245
- children: [
1246
- /* @__PURE__ */ jsxDEV5(SelectTrigger, {
1247
- children: /* @__PURE__ */ jsxDEV5(SelectValue, {
1248
- placeholder: "Select a model"
1249
- }, undefined, false, undefined, this)
1250
- }, undefined, false, undefined, this),
1251
- /* @__PURE__ */ jsxDEV5(SelectContent, {
1252
- children: models.map((m) => /* @__PURE__ */ jsxDEV5(SelectItem, {
1253
- value: m.id,
1254
- children: /* @__PURE__ */ jsxDEV5("div", {
1255
- className: "flex items-center gap-2",
1256
- children: [
1257
- /* @__PURE__ */ jsxDEV5("span", {
1258
- children: m.name
1259
- }, undefined, false, undefined, this),
1260
- /* @__PURE__ */ jsxDEV5("span", {
1261
- className: "text-muted-foreground text-xs",
1262
- children: [
1263
- Math.round(m.contextWindow / 1000),
1264
- "K"
1265
- ]
1266
- }, undefined, true, undefined, this),
1267
- m.capabilities.vision && /* @__PURE__ */ jsxDEV5(Badge, {
1268
- variant: "outline",
1269
- className: "text-xs",
1270
- children: "Vision"
1271
- }, undefined, false, undefined, this),
1272
- m.capabilities.reasoning && /* @__PURE__ */ jsxDEV5(Badge, {
1273
- variant: "outline",
1274
- className: "text-xs",
1275
- children: "Reasoning"
1276
- }, undefined, false, undefined, this)
1277
- ]
1278
- }, undefined, true, undefined, this)
1279
- }, m.id, false, undefined, this))
1280
- }, undefined, false, undefined, this)
1281
- ]
1282
- }, undefined, true, undefined, this)
1283
- ]
1284
- }, undefined, true, undefined, this),
1285
- selectedModel && /* @__PURE__ */ jsxDEV5("div", {
1286
- className: "text-muted-foreground flex flex-wrap gap-2 text-xs",
1287
- children: [
1288
- /* @__PURE__ */ jsxDEV5("span", {
1289
- children: [
1290
- "Context: ",
1291
- Math.round(selectedModel.contextWindow / 1000),
1292
- "K tokens"
1293
- ]
1294
- }, undefined, true, undefined, this),
1295
- selectedModel.capabilities.vision && /* @__PURE__ */ jsxDEV5("span", {
1296
- children: "• Vision"
1297
- }, undefined, false, undefined, this),
1298
- selectedModel.capabilities.tools && /* @__PURE__ */ jsxDEV5("span", {
1299
- children: "• Tools"
1300
- }, undefined, false, undefined, this),
1301
- selectedModel.capabilities.reasoning && /* @__PURE__ */ jsxDEV5("span", {
1302
- children: "• Reasoning"
1303
- }, undefined, false, undefined, this)
1714
+ /* @__PURE__ */ jsx7(SelectTrigger, {
1715
+ children: /* @__PURE__ */ jsx7(SelectValue, {
1716
+ placeholder: "Select thinking level"
1717
+ })
1718
+ }),
1719
+ /* @__PURE__ */ jsx7(SelectContent, {
1720
+ children: THINKING_LEVELS.map((level) => /* @__PURE__ */ jsx7(SelectItem, {
1721
+ value: level,
1722
+ title: THINKING_LEVEL_DESCRIPTIONS[level],
1723
+ children: THINKING_LEVEL_LABELS[level]
1724
+ }, level))
1725
+ })
1304
1726
  ]
1305
- }, undefined, true, undefined, this)
1727
+ })
1306
1728
  ]
1307
- }, undefined, true, undefined, this);
1729
+ });
1308
1730
  }
1309
- // src/presentation/components/ContextIndicator.tsx
1310
- import { cn as cn6 } from "@contractspec/lib.ui-kit-web/ui/utils";
1311
- import { Badge as Badge2 } from "@contractspec/lib.ui-kit-web/ui/badge";
1312
- import {
1313
- Tooltip,
1314
- TooltipContent,
1315
- TooltipProvider,
1316
- TooltipTrigger
1317
- } from "@contractspec/lib.ui-kit-web/ui/tooltip";
1318
- import { FolderOpen, FileCode, Zap, Info } from "lucide-react";
1319
- import { jsxDEV as jsxDEV6, Fragment as Fragment3 } from "react/jsx-dev-runtime";
1731
+
1732
+ // src/presentation/hooks/useMessageSelection.ts
1733
+ import * as React7 from "react";
1320
1734
  "use client";
1321
- function ContextIndicator({
1322
- summary,
1323
- active = false,
1735
+ function useMessageSelection(messageIds) {
1736
+ const [selectedIds, setSelectedIds] = React7.useState(() => new Set);
1737
+ const idSet = React7.useMemo(() => new Set(messageIds), [messageIds.join(",")]);
1738
+ React7.useEffect(() => {
1739
+ setSelectedIds((prev) => {
1740
+ const next = new Set;
1741
+ for (const id of prev) {
1742
+ if (idSet.has(id))
1743
+ next.add(id);
1744
+ }
1745
+ return next.size === prev.size ? prev : next;
1746
+ });
1747
+ }, [idSet]);
1748
+ const toggle = React7.useCallback((id) => {
1749
+ setSelectedIds((prev) => {
1750
+ const next = new Set(prev);
1751
+ if (next.has(id))
1752
+ next.delete(id);
1753
+ else
1754
+ next.add(id);
1755
+ return next;
1756
+ });
1757
+ }, []);
1758
+ const selectAll = React7.useCallback(() => {
1759
+ setSelectedIds(new Set(messageIds));
1760
+ }, [messageIds.join(",")]);
1761
+ const clearSelection = React7.useCallback(() => {
1762
+ setSelectedIds(new Set);
1763
+ }, []);
1764
+ const isSelected = React7.useCallback((id) => selectedIds.has(id), [selectedIds]);
1765
+ const selectedCount = selectedIds.size;
1766
+ return {
1767
+ selectedIds,
1768
+ toggle,
1769
+ selectAll,
1770
+ clearSelection,
1771
+ isSelected,
1772
+ selectedCount
1773
+ };
1774
+ }
1775
+
1776
+ // src/presentation/components/ChatWithExport.tsx
1777
+ import { jsx as jsx8, jsxs as jsxs8, Fragment as Fragment4 } from "react/jsx-runtime";
1778
+ "use client";
1779
+ function ChatWithExport({
1780
+ messages,
1781
+ conversation,
1782
+ children,
1324
1783
  className,
1325
- showDetails = true
1784
+ showExport = true,
1785
+ showMessageSelection = true,
1786
+ showScrollButton = true,
1787
+ onCreateNew,
1788
+ onFork,
1789
+ onEditMessage,
1790
+ thinkingLevel = "thinking",
1791
+ onThinkingLevelChange,
1792
+ presentationRenderer,
1793
+ formRenderer
1326
1794
  }) {
1327
- if (!summary && !active) {
1328
- return /* @__PURE__ */ jsxDEV6("div", {
1329
- className: cn6("flex items-center gap-1.5 text-sm", "text-muted-foreground", className),
1330
- children: [
1331
- /* @__PURE__ */ jsxDEV6(Info, {
1332
- className: "h-4 w-4"
1333
- }, undefined, false, undefined, this),
1334
- /* @__PURE__ */ jsxDEV6("span", {
1335
- children: "No workspace context"
1336
- }, undefined, false, undefined, this)
1337
- ]
1338
- }, undefined, true, undefined, this);
1795
+ const messageIds = React8.useMemo(() => messages.map((m) => m.id), [messages]);
1796
+ const selection = useMessageSelection(messageIds);
1797
+ const hasToolbar = showExport || showMessageSelection;
1798
+ const hasPicker = Boolean(onThinkingLevelChange);
1799
+ const headerContent = hasPicker || hasToolbar ? /* @__PURE__ */ jsxs8(Fragment4, {
1800
+ children: [
1801
+ hasPicker && /* @__PURE__ */ jsx8(ThinkingLevelPicker, {
1802
+ value: thinkingLevel,
1803
+ onChange: onThinkingLevelChange,
1804
+ compact: true
1805
+ }),
1806
+ hasToolbar && /* @__PURE__ */ jsx8(ChatExportToolbar, {
1807
+ messages,
1808
+ conversation,
1809
+ selectedIds: selection.selectedIds,
1810
+ showSelectionSummary: showMessageSelection,
1811
+ onSelectAll: showMessageSelection ? selection.selectAll : undefined,
1812
+ onClearSelection: showMessageSelection ? selection.clearSelection : undefined,
1813
+ selectedCount: selection.selectedCount,
1814
+ totalCount: messages.length,
1815
+ onCreateNew,
1816
+ onFork
1817
+ })
1818
+ ]
1819
+ }) : null;
1820
+ return /* @__PURE__ */ jsxs8(ChatContainer, {
1821
+ className,
1822
+ headerContent,
1823
+ showScrollButton,
1824
+ children: [
1825
+ messages.map((msg) => /* @__PURE__ */ jsx8(ChatMessage, {
1826
+ message: msg,
1827
+ selectable: showMessageSelection,
1828
+ selected: selection.isSelected(msg.id),
1829
+ onSelect: showMessageSelection ? selection.toggle : undefined,
1830
+ editable: msg.role === "user" && !!onEditMessage,
1831
+ onEdit: onEditMessage,
1832
+ presentationRenderer,
1833
+ formRenderer
1834
+ }, msg.id)),
1835
+ children
1836
+ ]
1837
+ });
1838
+ }
1839
+ // src/presentation/components/ChatSidebar.tsx
1840
+ import * as React10 from "react";
1841
+ import { Plus as Plus2, Trash2, MessageSquare } from "lucide-react";
1842
+ import { Button as Button5 } from "@contractspec/lib.design-system";
1843
+ import { cn as cn6 } from "@contractspec/lib.ui-kit-web/ui/utils";
1844
+
1845
+ // src/presentation/hooks/useConversations.ts
1846
+ import * as React9 from "react";
1847
+ "use client";
1848
+ function useConversations(options) {
1849
+ const { store, projectId, tags, limit = 50 } = options;
1850
+ const [conversations, setConversations] = React9.useState([]);
1851
+ const [isLoading, setIsLoading] = React9.useState(true);
1852
+ const refresh = React9.useCallback(async () => {
1853
+ setIsLoading(true);
1854
+ try {
1855
+ const list = await store.list({
1856
+ status: "active",
1857
+ projectId,
1858
+ tags,
1859
+ limit
1860
+ });
1861
+ setConversations(list);
1862
+ } finally {
1863
+ setIsLoading(false);
1864
+ }
1865
+ }, [store, projectId, tags, limit]);
1866
+ React9.useEffect(() => {
1867
+ refresh();
1868
+ }, [refresh]);
1869
+ const deleteConversation = React9.useCallback(async (id) => {
1870
+ const ok = await store.delete(id);
1871
+ if (ok) {
1872
+ setConversations((prev) => prev.filter((c) => c.id !== id));
1873
+ }
1874
+ return ok;
1875
+ }, [store]);
1876
+ return {
1877
+ conversations,
1878
+ isLoading,
1879
+ refresh,
1880
+ deleteConversation
1881
+ };
1882
+ }
1883
+
1884
+ // src/presentation/components/ChatSidebar.tsx
1885
+ import { jsx as jsx9, jsxs as jsxs9, Fragment as Fragment5 } from "react/jsx-runtime";
1886
+ "use client";
1887
+ function formatDate(date) {
1888
+ const d = new Date(date);
1889
+ const now = new Date;
1890
+ const diff = now.getTime() - d.getTime();
1891
+ if (diff < 86400000) {
1892
+ return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
1893
+ }
1894
+ if (diff < 604800000) {
1895
+ return d.toLocaleDateString([], { weekday: "short" });
1339
1896
  }
1340
- const content = /* @__PURE__ */ jsxDEV6("div", {
1341
- className: cn6("flex items-center gap-2", active ? "text-foreground" : "text-muted-foreground", className),
1897
+ return d.toLocaleDateString([], { month: "short", day: "numeric" });
1898
+ }
1899
+ function ConversationItem({
1900
+ conversation,
1901
+ selected,
1902
+ onSelect,
1903
+ onDelete
1904
+ }) {
1905
+ const title = conversation.title ?? conversation.messages[0]?.content?.slice(0, 50) ?? "New chat";
1906
+ const displayTitle = title.length > 40 ? `${title.slice(0, 40)}…` : title;
1907
+ return /* @__PURE__ */ jsxs9("div", {
1908
+ role: "button",
1909
+ tabIndex: 0,
1910
+ onClick: onSelect,
1911
+ onKeyDown: (e) => {
1912
+ if (e.key === "Enter" || e.key === " ") {
1913
+ e.preventDefault();
1914
+ onSelect();
1915
+ }
1916
+ },
1917
+ className: cn6("group flex cursor-pointer items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors", selected ? "bg-accent text-accent-foreground" : "hover:bg-accent/50"),
1342
1918
  children: [
1343
- /* @__PURE__ */ jsxDEV6(Badge2, {
1344
- variant: active ? "default" : "secondary",
1345
- className: "flex items-center gap-1",
1346
- children: [
1347
- /* @__PURE__ */ jsxDEV6(Zap, {
1348
- className: "h-3 w-3"
1349
- }, undefined, false, undefined, this),
1350
- "Context"
1351
- ]
1352
- }, undefined, true, undefined, this),
1353
- summary && showDetails && /* @__PURE__ */ jsxDEV6(Fragment3, {
1919
+ /* @__PURE__ */ jsx9(MessageSquare, {
1920
+ className: "text-muted-foreground h-4 w-4 shrink-0"
1921
+ }),
1922
+ /* @__PURE__ */ jsxs9("div", {
1923
+ className: "min-w-0 flex-1",
1354
1924
  children: [
1355
- /* @__PURE__ */ jsxDEV6("div", {
1356
- className: "flex items-center gap-1 text-xs",
1357
- children: [
1358
- /* @__PURE__ */ jsxDEV6(FolderOpen, {
1359
- className: "h-3.5 w-3.5"
1360
- }, undefined, false, undefined, this),
1361
- /* @__PURE__ */ jsxDEV6("span", {
1362
- children: summary.name
1363
- }, undefined, false, undefined, this)
1364
- ]
1365
- }, undefined, true, undefined, this),
1366
- /* @__PURE__ */ jsxDEV6("div", {
1367
- className: "flex items-center gap-1 text-xs",
1925
+ /* @__PURE__ */ jsx9("p", {
1926
+ className: "truncate",
1927
+ children: displayTitle
1928
+ }),
1929
+ /* @__PURE__ */ jsxs9("p", {
1930
+ className: "text-muted-foreground text-xs",
1368
1931
  children: [
1369
- /* @__PURE__ */ jsxDEV6(FileCode, {
1370
- className: "h-3.5 w-3.5"
1371
- }, undefined, false, undefined, this),
1372
- /* @__PURE__ */ jsxDEV6("span", {
1932
+ formatDate(conversation.updatedAt),
1933
+ conversation.projectName && ` · ${conversation.projectName}`,
1934
+ conversation.tags && conversation.tags.length > 0 && /* @__PURE__ */ jsxs9(Fragment5, {
1373
1935
  children: [
1374
- summary.specs.total,
1375
- " specs"
1936
+ " · ",
1937
+ conversation.tags.slice(0, 2).join(", ")
1376
1938
  ]
1377
- }, undefined, true, undefined, this)
1939
+ })
1378
1940
  ]
1379
- }, undefined, true, undefined, this)
1941
+ })
1380
1942
  ]
1381
- }, undefined, true, undefined, this)
1943
+ }),
1944
+ /* @__PURE__ */ jsx9("span", {
1945
+ onClick: (e) => e.stopPropagation(),
1946
+ children: /* @__PURE__ */ jsx9(Button5, {
1947
+ variant: "ghost",
1948
+ size: "sm",
1949
+ className: "h-6 w-6 shrink-0 p-0 opacity-0 group-hover:opacity-100",
1950
+ onPress: onDelete,
1951
+ "aria-label": "Delete conversation",
1952
+ children: /* @__PURE__ */ jsx9(Trash2, {
1953
+ className: "h-3 w-3"
1954
+ })
1955
+ })
1956
+ })
1382
1957
  ]
1383
- }, undefined, true, undefined, this);
1384
- if (!summary) {
1385
- return content;
1386
- }
1387
- return /* @__PURE__ */ jsxDEV6(TooltipProvider, {
1388
- children: /* @__PURE__ */ jsxDEV6(Tooltip, {
1389
- children: [
1390
- /* @__PURE__ */ jsxDEV6(TooltipTrigger, {
1391
- asChild: true,
1392
- children: content
1393
- }, undefined, false, undefined, this),
1394
- /* @__PURE__ */ jsxDEV6(TooltipContent, {
1395
- side: "bottom",
1396
- className: "max-w-[300px]",
1397
- children: /* @__PURE__ */ jsxDEV6("div", {
1398
- className: "flex flex-col gap-2 text-sm",
1399
- children: [
1400
- /* @__PURE__ */ jsxDEV6("div", {
1401
- className: "font-medium",
1402
- children: summary.name
1403
- }, undefined, false, undefined, this),
1404
- /* @__PURE__ */ jsxDEV6("div", {
1405
- className: "text-muted-foreground text-xs",
1406
- children: summary.path
1407
- }, undefined, false, undefined, this),
1408
- /* @__PURE__ */ jsxDEV6("div", {
1409
- className: "border-t pt-2",
1410
- children: /* @__PURE__ */ jsxDEV6("div", {
1411
- className: "grid grid-cols-2 gap-1 text-xs",
1412
- children: [
1413
- /* @__PURE__ */ jsxDEV6("span", {
1414
- children: "Commands:"
1415
- }, undefined, false, undefined, this),
1416
- /* @__PURE__ */ jsxDEV6("span", {
1417
- className: "text-right",
1418
- children: summary.specs.commands
1419
- }, undefined, false, undefined, this),
1420
- /* @__PURE__ */ jsxDEV6("span", {
1421
- children: "Queries:"
1422
- }, undefined, false, undefined, this),
1423
- /* @__PURE__ */ jsxDEV6("span", {
1424
- className: "text-right",
1425
- children: summary.specs.queries
1426
- }, undefined, false, undefined, this),
1427
- /* @__PURE__ */ jsxDEV6("span", {
1428
- children: "Events:"
1429
- }, undefined, false, undefined, this),
1430
- /* @__PURE__ */ jsxDEV6("span", {
1431
- className: "text-right",
1432
- children: summary.specs.events
1433
- }, undefined, false, undefined, this),
1434
- /* @__PURE__ */ jsxDEV6("span", {
1435
- children: "Presentations:"
1436
- }, undefined, false, undefined, this),
1437
- /* @__PURE__ */ jsxDEV6("span", {
1438
- className: "text-right",
1439
- children: summary.specs.presentations
1440
- }, undefined, false, undefined, this)
1441
- ]
1442
- }, undefined, true, undefined, this)
1443
- }, undefined, false, undefined, this),
1444
- /* @__PURE__ */ jsxDEV6("div", {
1445
- className: "border-t pt-2 text-xs",
1446
- children: [
1447
- /* @__PURE__ */ jsxDEV6("span", {
1448
- children: [
1449
- summary.files.total,
1450
- " files"
1451
- ]
1452
- }, undefined, true, undefined, this),
1453
- /* @__PURE__ */ jsxDEV6("span", {
1454
- className: "mx-1",
1455
- children: "•"
1456
- }, undefined, false, undefined, this),
1457
- /* @__PURE__ */ jsxDEV6("span", {
1458
- children: [
1459
- summary.files.specFiles,
1460
- " spec files"
1461
- ]
1462
- }, undefined, true, undefined, this)
1463
- ]
1464
- }, undefined, true, undefined, this)
1465
- ]
1466
- }, undefined, true, undefined, this)
1467
- }, undefined, false, undefined, this)
1468
- ]
1469
- }, undefined, true, undefined, this)
1470
- }, undefined, false, undefined, this);
1958
+ });
1959
+ }
1960
+ function ChatSidebar({
1961
+ store,
1962
+ selectedConversationId,
1963
+ onSelectConversation,
1964
+ onCreateNew,
1965
+ projectId,
1966
+ tags,
1967
+ limit = 50,
1968
+ className,
1969
+ collapsed = false,
1970
+ onUpdateConversation,
1971
+ selectedConversation
1972
+ }) {
1973
+ const { conversations, isLoading, refresh, deleteConversation } = useConversations({ store, projectId, tags, limit });
1974
+ const handleDelete = React10.useCallback(async (id) => {
1975
+ const ok = await deleteConversation(id);
1976
+ if (ok && selectedConversationId === id) {
1977
+ onSelectConversation(null);
1978
+ }
1979
+ }, [deleteConversation, selectedConversationId, onSelectConversation]);
1980
+ if (collapsed)
1981
+ return null;
1982
+ return /* @__PURE__ */ jsxs9("div", {
1983
+ className: cn6("border-border flex w-64 shrink-0 flex-col border-r", className),
1984
+ children: [
1985
+ /* @__PURE__ */ jsxs9("div", {
1986
+ className: "border-border flex shrink-0 items-center justify-between border-b p-2",
1987
+ children: [
1988
+ /* @__PURE__ */ jsx9("span", {
1989
+ className: "text-muted-foreground text-sm font-medium",
1990
+ children: "Conversations"
1991
+ }),
1992
+ /* @__PURE__ */ jsx9(Button5, {
1993
+ variant: "ghost",
1994
+ size: "sm",
1995
+ className: "h-8 w-8 p-0",
1996
+ onPress: onCreateNew,
1997
+ "aria-label": "New conversation",
1998
+ children: /* @__PURE__ */ jsx9(Plus2, {
1999
+ className: "h-4 w-4"
2000
+ })
2001
+ })
2002
+ ]
2003
+ }),
2004
+ /* @__PURE__ */ jsx9("div", {
2005
+ className: "flex-1 overflow-y-auto p-2",
2006
+ children: isLoading ? /* @__PURE__ */ jsx9("div", {
2007
+ className: "text-muted-foreground py-4 text-center text-sm",
2008
+ children: "Loading…"
2009
+ }) : conversations.length === 0 ? /* @__PURE__ */ jsx9("div", {
2010
+ className: "text-muted-foreground py-4 text-center text-sm",
2011
+ children: "No conversations yet"
2012
+ }) : /* @__PURE__ */ jsx9("div", {
2013
+ className: "flex flex-col gap-1",
2014
+ children: conversations.map((conv) => /* @__PURE__ */ jsx9(ConversationItem, {
2015
+ conversation: conv,
2016
+ selected: conv.id === selectedConversationId,
2017
+ onSelect: () => onSelectConversation(conv.id),
2018
+ onDelete: () => handleDelete(conv.id)
2019
+ }, conv.id))
2020
+ })
2021
+ }),
2022
+ selectedConversation && onUpdateConversation && /* @__PURE__ */ jsx9(ConversationMeta, {
2023
+ conversation: selectedConversation,
2024
+ onUpdate: onUpdateConversation
2025
+ })
2026
+ ]
2027
+ });
2028
+ }
2029
+ function ConversationMeta({
2030
+ conversation,
2031
+ onUpdate
2032
+ }) {
2033
+ const [projectName, setProjectName] = React10.useState(conversation.projectName ?? "");
2034
+ const [tagsStr, setTagsStr] = React10.useState(conversation.tags?.join(", ") ?? "");
2035
+ React10.useEffect(() => {
2036
+ setProjectName(conversation.projectName ?? "");
2037
+ setTagsStr(conversation.tags?.join(", ") ?? "");
2038
+ }, [conversation.id, conversation.projectName, conversation.tags]);
2039
+ const handleBlur = React10.useCallback(() => {
2040
+ const tags = tagsStr.split(",").map((t) => t.trim()).filter(Boolean);
2041
+ if (projectName !== (conversation.projectName ?? "") || JSON.stringify(tags) !== JSON.stringify(conversation.tags ?? [])) {
2042
+ onUpdate(conversation.id, {
2043
+ projectName: projectName || undefined,
2044
+ projectId: projectName ? projectName.replace(/\s+/g, "-") : undefined,
2045
+ tags: tags.length > 0 ? tags : undefined
2046
+ });
2047
+ }
2048
+ }, [
2049
+ conversation.id,
2050
+ conversation.projectName,
2051
+ conversation.tags,
2052
+ projectName,
2053
+ tagsStr,
2054
+ onUpdate
2055
+ ]);
2056
+ return /* @__PURE__ */ jsxs9("div", {
2057
+ className: "border-border shrink-0 border-t p-2",
2058
+ children: [
2059
+ /* @__PURE__ */ jsx9("p", {
2060
+ className: "text-muted-foreground mb-1 text-xs font-medium",
2061
+ children: "Project"
2062
+ }),
2063
+ /* @__PURE__ */ jsx9("input", {
2064
+ type: "text",
2065
+ value: projectName,
2066
+ onChange: (e) => setProjectName(e.target.value),
2067
+ onBlur: handleBlur,
2068
+ placeholder: "Project name",
2069
+ className: "border-input bg-background mb-2 w-full rounded px-2 py-1 text-xs"
2070
+ }),
2071
+ /* @__PURE__ */ jsx9("p", {
2072
+ className: "text-muted-foreground mb-1 text-xs font-medium",
2073
+ children: "Tags"
2074
+ }),
2075
+ /* @__PURE__ */ jsx9("input", {
2076
+ type: "text",
2077
+ value: tagsStr,
2078
+ onChange: (e) => setTagsStr(e.target.value),
2079
+ onBlur: handleBlur,
2080
+ placeholder: "tag1, tag2",
2081
+ className: "border-input bg-background w-full rounded px-2 py-1 text-xs"
2082
+ })
2083
+ ]
2084
+ });
1471
2085
  }
2086
+ // src/presentation/components/ChatWithSidebar.tsx
2087
+ import * as React12 from "react";
2088
+
1472
2089
  // src/presentation/hooks/useChat.tsx
1473
- import * as React6 from "react";
1474
- import { tool } from "ai";
1475
- import { z } from "zod";
2090
+ import * as React11 from "react";
2091
+ import { tool as tool4 } from "ai";
2092
+ import { z as z4 } from "zod";
1476
2093
 
1477
2094
  // src/core/chat-service.ts
1478
2095
  import { generateText, streamText } from "ai";
@@ -1549,42 +2166,542 @@ class InMemoryConversationStore {
1549
2166
  async delete(conversationId) {
1550
2167
  return this.conversations.delete(conversationId);
1551
2168
  }
1552
- async list(options) {
1553
- let results = Array.from(this.conversations.values());
1554
- if (options?.status) {
1555
- results = results.filter((c) => c.status === options.status);
2169
+ async list(options) {
2170
+ let results = Array.from(this.conversations.values());
2171
+ if (options?.status) {
2172
+ results = results.filter((c) => c.status === options.status);
2173
+ }
2174
+ if (options?.projectId) {
2175
+ results = results.filter((c) => c.projectId === options.projectId);
2176
+ }
2177
+ if (options?.tags && options.tags.length > 0) {
2178
+ const tagSet = new Set(options.tags);
2179
+ results = results.filter((c) => c.tags && c.tags.some((t) => tagSet.has(t)));
2180
+ }
2181
+ results.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
2182
+ const offset = options?.offset ?? 0;
2183
+ const limit = options?.limit ?? 100;
2184
+ return results.slice(offset, offset + limit);
2185
+ }
2186
+ async fork(conversationId, upToMessageId) {
2187
+ const source = this.conversations.get(conversationId);
2188
+ if (!source) {
2189
+ throw new Error(`Conversation ${conversationId} not found`);
2190
+ }
2191
+ let messagesToCopy = source.messages;
2192
+ if (upToMessageId) {
2193
+ const idx = source.messages.findIndex((m) => m.id === upToMessageId);
2194
+ if (idx === -1) {
2195
+ throw new Error(`Message ${upToMessageId} not found`);
2196
+ }
2197
+ messagesToCopy = source.messages.slice(0, idx + 1);
2198
+ }
2199
+ const now = new Date;
2200
+ const forkedMessages = messagesToCopy.map((m) => ({
2201
+ ...m,
2202
+ id: generateId("msg"),
2203
+ conversationId: "",
2204
+ createdAt: new Date(m.createdAt),
2205
+ updatedAt: new Date(m.updatedAt)
2206
+ }));
2207
+ const forked = {
2208
+ ...source,
2209
+ id: generateId("conv"),
2210
+ title: source.title ? `${source.title} (fork)` : undefined,
2211
+ forkedFromId: source.id,
2212
+ createdAt: now,
2213
+ updatedAt: now,
2214
+ messages: forkedMessages
2215
+ };
2216
+ for (const m of forked.messages) {
2217
+ m.conversationId = forked.id;
2218
+ }
2219
+ this.conversations.set(forked.id, forked);
2220
+ return forked;
2221
+ }
2222
+ async truncateAfter(conversationId, messageId) {
2223
+ const conv = this.conversations.get(conversationId);
2224
+ if (!conv)
2225
+ return null;
2226
+ const idx = conv.messages.findIndex((m) => m.id === messageId);
2227
+ if (idx === -1)
2228
+ return null;
2229
+ conv.messages = conv.messages.slice(0, idx + 1);
2230
+ conv.updatedAt = new Date;
2231
+ return conv;
2232
+ }
2233
+ async search(query, limit = 20) {
2234
+ const lowerQuery = query.toLowerCase();
2235
+ const results = [];
2236
+ for (const conversation of this.conversations.values()) {
2237
+ if (conversation.title?.toLowerCase().includes(lowerQuery)) {
2238
+ results.push(conversation);
2239
+ continue;
2240
+ }
2241
+ const hasMatch = conversation.messages.some((m) => m.content.toLowerCase().includes(lowerQuery));
2242
+ if (hasMatch) {
2243
+ results.push(conversation);
2244
+ }
2245
+ if (results.length >= limit)
2246
+ break;
2247
+ }
2248
+ return results;
2249
+ }
2250
+ clear() {
2251
+ this.conversations.clear();
2252
+ }
2253
+ }
2254
+ function createInMemoryConversationStore() {
2255
+ return new InMemoryConversationStore;
2256
+ }
2257
+
2258
+ // src/core/workflow-tools.ts
2259
+ import { tool } from "ai";
2260
+ import { z } from "zod";
2261
+ import {
2262
+ WorkflowComposer,
2263
+ validateExtension
2264
+ } from "@contractspec/lib.workflow-composer";
2265
+ var StepTypeSchema = z.enum(["human", "automation", "decision"]);
2266
+ var StepActionSchema = z.object({
2267
+ operation: z.object({
2268
+ name: z.string(),
2269
+ version: z.number()
2270
+ }).optional(),
2271
+ form: z.object({
2272
+ key: z.string(),
2273
+ version: z.number()
2274
+ }).optional()
2275
+ }).optional();
2276
+ var StepSchema = z.object({
2277
+ id: z.string(),
2278
+ type: StepTypeSchema,
2279
+ label: z.string(),
2280
+ description: z.string().optional(),
2281
+ action: StepActionSchema
2282
+ });
2283
+ var StepInjectionSchema = z.object({
2284
+ after: z.string().optional(),
2285
+ before: z.string().optional(),
2286
+ inject: StepSchema,
2287
+ transitionTo: z.string().optional(),
2288
+ transitionFrom: z.string().optional(),
2289
+ when: z.string().optional()
2290
+ });
2291
+ var WorkflowExtensionInputSchema = z.object({
2292
+ workflow: z.string(),
2293
+ tenantId: z.string().optional(),
2294
+ role: z.string().optional(),
2295
+ priority: z.number().optional(),
2296
+ customSteps: z.array(StepInjectionSchema).optional(),
2297
+ hiddenSteps: z.array(z.string()).optional()
2298
+ });
2299
+ function createWorkflowTools(config) {
2300
+ const { baseWorkflows, composer } = config;
2301
+ const baseByKey = new Map(baseWorkflows.map((b) => [b.meta.key, b]));
2302
+ const createWorkflowExtensionTool = tool({
2303
+ description: "Create or validate a workflow extension. Use when the user asks to add steps, modify a workflow, or create a tenant-specific extension. The extension targets an existing base workflow.",
2304
+ inputSchema: WorkflowExtensionInputSchema,
2305
+ execute: async (input) => {
2306
+ const extension = {
2307
+ workflow: input.workflow,
2308
+ tenantId: input.tenantId,
2309
+ role: input.role,
2310
+ priority: input.priority,
2311
+ customSteps: input.customSteps,
2312
+ hiddenSteps: input.hiddenSteps
2313
+ };
2314
+ const base = baseByKey.get(input.workflow);
2315
+ if (!base) {
2316
+ return {
2317
+ success: false,
2318
+ error: `Base workflow "${input.workflow}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`,
2319
+ extension
2320
+ };
2321
+ }
2322
+ try {
2323
+ validateExtension(extension, base);
2324
+ return {
2325
+ success: true,
2326
+ message: "Extension validated successfully",
2327
+ extension
2328
+ };
2329
+ } catch (err) {
2330
+ return {
2331
+ success: false,
2332
+ error: err instanceof Error ? err.message : String(err),
2333
+ extension
2334
+ };
2335
+ }
2336
+ }
2337
+ });
2338
+ const composeWorkflowInputSchema = z.object({
2339
+ workflowKey: z.string().describe("Base workflow meta.key"),
2340
+ tenantId: z.string().optional(),
2341
+ role: z.string().optional(),
2342
+ extensions: z.array(WorkflowExtensionInputSchema).optional().describe("Extensions to register before composing")
2343
+ });
2344
+ const composeWorkflowTool = tool({
2345
+ description: "Compose a workflow by applying registered extensions to a base workflow. Returns the composed WorkflowSpec.",
2346
+ inputSchema: composeWorkflowInputSchema,
2347
+ execute: async (input) => {
2348
+ const base = baseByKey.get(input.workflowKey);
2349
+ if (!base) {
2350
+ return {
2351
+ success: false,
2352
+ error: `Base workflow "${input.workflowKey}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`
2353
+ };
2354
+ }
2355
+ const comp = composer ?? new WorkflowComposer;
2356
+ if (input.extensions?.length) {
2357
+ for (const ext of input.extensions) {
2358
+ comp.register({
2359
+ workflow: ext.workflow,
2360
+ tenantId: ext.tenantId,
2361
+ role: ext.role,
2362
+ priority: ext.priority,
2363
+ customSteps: ext.customSteps,
2364
+ hiddenSteps: ext.hiddenSteps
2365
+ });
2366
+ }
2367
+ }
2368
+ try {
2369
+ const composed = comp.compose({
2370
+ base,
2371
+ tenantId: input.tenantId,
2372
+ role: input.role
2373
+ });
2374
+ return {
2375
+ success: true,
2376
+ workflow: composed,
2377
+ meta: composed.meta,
2378
+ stepIds: composed.definition.steps.map((s) => s.id)
2379
+ };
2380
+ } catch (err) {
2381
+ return {
2382
+ success: false,
2383
+ error: err instanceof Error ? err.message : String(err)
2384
+ };
2385
+ }
2386
+ }
2387
+ });
2388
+ const generateWorkflowSpecCodeInputSchema = z.object({
2389
+ workflowKey: z.string().describe("Workflow meta.key"),
2390
+ composedSteps: z.array(z.object({
2391
+ id: z.string(),
2392
+ type: z.enum(["human", "automation", "decision"]),
2393
+ label: z.string(),
2394
+ description: z.string().optional()
2395
+ })).optional().describe("Steps to include; if omitted, uses the base workflow")
2396
+ });
2397
+ const generateWorkflowSpecCodeTool = tool({
2398
+ description: "Generate TypeScript code for a workflow spec. Use after composing a workflow to output the spec as code the user can save.",
2399
+ inputSchema: generateWorkflowSpecCodeInputSchema,
2400
+ execute: async (input) => {
2401
+ const base = baseByKey.get(input.workflowKey);
2402
+ if (!base) {
2403
+ return {
2404
+ success: false,
2405
+ error: `Base workflow "${input.workflowKey}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`,
2406
+ code: null
2407
+ };
2408
+ }
2409
+ const steps = input.composedSteps ?? base.definition.steps;
2410
+ const specVarName = toPascalCase((base.meta.key.split(".").pop() ?? "Workflow") + "") + "Workflow";
2411
+ const stepsCode = steps.map((s) => ` {
2412
+ id: '${s.id}',
2413
+ type: '${s.type}',
2414
+ label: '${escapeString(s.label)}',${s.description ? `
2415
+ description: '${escapeString(s.description)}',` : ""}
2416
+ }`).join(`,
2417
+ `);
2418
+ const meta = base.meta;
2419
+ const transitionsJson = JSON.stringify(base.definition.transitions, null, 6);
2420
+ const code = `import type { WorkflowSpec } from '@contractspec/lib.contracts-spec/workflow';
2421
+
2422
+ /**
2423
+ * Workflow: ${base.meta.key}
2424
+ * Generated via AI chat workflow tools.
2425
+ */
2426
+ export const ${specVarName}: WorkflowSpec = {
2427
+ meta: {
2428
+ key: '${base.meta.key}',
2429
+ version: '${String(base.meta.version)}',
2430
+ title: '${escapeString(meta.title ?? base.meta.key)}',
2431
+ description: '${escapeString(meta.description ?? "")}',
2432
+ },
2433
+ definition: {
2434
+ entryStepId: '${base.definition.entryStepId ?? base.definition.steps[0]?.id ?? ""}',
2435
+ steps: [
2436
+ ${stepsCode}
2437
+ ],
2438
+ transitions: ${transitionsJson},
2439
+ },
2440
+ };
2441
+ `;
2442
+ return {
2443
+ success: true,
2444
+ code,
2445
+ workflowKey: input.workflowKey
2446
+ };
2447
+ }
2448
+ });
2449
+ return {
2450
+ create_workflow_extension: createWorkflowExtensionTool,
2451
+ compose_workflow: composeWorkflowTool,
2452
+ generate_workflow_spec_code: generateWorkflowSpecCodeTool
2453
+ };
2454
+ }
2455
+ function toPascalCase(value) {
2456
+ return value.split(/[-_.]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
2457
+ }
2458
+ function escapeString(value) {
2459
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
2460
+ }
2461
+
2462
+ // src/core/contracts-context.ts
2463
+ function buildContractsContextPrompt(config) {
2464
+ const parts = [];
2465
+ if (!config.agentSpecs?.length && !config.dataViewSpecs?.length && !config.formSpecs?.length && !config.presentationSpecs?.length && !config.operationRefs?.length) {
2466
+ return "";
2467
+ }
2468
+ parts.push(`
2469
+
2470
+ ## Available resources`);
2471
+ if (config.agentSpecs?.length) {
2472
+ parts.push(`
2473
+ ### Agent tools`);
2474
+ for (const agent of config.agentSpecs) {
2475
+ const toolNames = agent.tools?.map((t) => t.name).join(", ") ?? "none";
2476
+ parts.push(`- **${agent.key}**: tools: ${toolNames}`);
2477
+ }
2478
+ }
2479
+ if (config.dataViewSpecs?.length) {
2480
+ parts.push(`
2481
+ ### Data views`);
2482
+ for (const dv of config.dataViewSpecs) {
2483
+ parts.push(`- **${dv.key}**: ${dv.meta.title ?? dv.key}`);
2484
+ }
2485
+ }
2486
+ if (config.formSpecs?.length) {
2487
+ parts.push(`
2488
+ ### Forms`);
2489
+ for (const form of config.formSpecs) {
2490
+ parts.push(`- **${form.key}**: ${form.meta.title ?? form.key}`);
2491
+ }
2492
+ }
2493
+ if (config.presentationSpecs?.length) {
2494
+ parts.push(`
2495
+ ### Presentations`);
2496
+ for (const pres of config.presentationSpecs) {
2497
+ parts.push(`- **${pres.key}**: ${pres.meta.title ?? pres.key} (targets: ${pres.targets?.join(", ") ?? "react"})`);
2498
+ }
2499
+ }
2500
+ if (config.operationRefs?.length) {
2501
+ parts.push(`
2502
+ ### Operations`);
2503
+ for (const op of config.operationRefs) {
2504
+ parts.push(`- **${op.key}@${op.version}**`);
2505
+ }
2506
+ }
2507
+ parts.push(`
2508
+ Use the available tools to invoke operations, query data views, or propose surface changes when appropriate.`);
2509
+ return parts.join(`
2510
+ `);
2511
+ }
2512
+
2513
+ // src/core/agent-tools-adapter.ts
2514
+ import { tool as tool2 } from "ai";
2515
+ import { z as z2 } from "zod";
2516
+ function getInputSchema(_schema) {
2517
+ return z2.object({}).passthrough();
2518
+ }
2519
+ function agentToolConfigsToToolSet(configs, handlers) {
2520
+ const result = {};
2521
+ for (const config of configs) {
2522
+ const handler = handlers?.[config.name];
2523
+ const inputSchema = getInputSchema(config.schema);
2524
+ result[config.name] = tool2({
2525
+ description: config.description ?? config.name,
2526
+ inputSchema,
2527
+ execute: async (input) => {
2528
+ if (!handler) {
2529
+ return {
2530
+ status: "unimplemented",
2531
+ message: "Wire handler in host",
2532
+ toolName: config.name
2533
+ };
2534
+ }
2535
+ try {
2536
+ const output = await Promise.resolve(handler(input));
2537
+ return typeof output === "string" ? output : output;
2538
+ } catch (err) {
2539
+ return {
2540
+ status: "error",
2541
+ error: err instanceof Error ? err.message : String(err),
2542
+ toolName: config.name
2543
+ };
2544
+ }
2545
+ }
2546
+ });
2547
+ }
2548
+ return result;
2549
+ }
2550
+
2551
+ // src/core/surface-planner-tools.ts
2552
+ import { tool as tool3 } from "ai";
2553
+ import { z as z3 } from "zod";
2554
+ import { validatePatchProposal } from "@contractspec/lib.surface-runtime/spec/validate-surface-patch";
2555
+ import { buildSurfacePatchProposal } from "@contractspec/lib.surface-runtime/runtime/planner-tools";
2556
+ var VALID_OPS = [
2557
+ "insert-node",
2558
+ "replace-node",
2559
+ "remove-node",
2560
+ "move-node",
2561
+ "resize-panel",
2562
+ "set-layout",
2563
+ "reveal-field",
2564
+ "hide-field",
2565
+ "promote-action",
2566
+ "set-focus"
2567
+ ];
2568
+ var DEFAULT_NODE_KINDS = [
2569
+ "entity-section",
2570
+ "entity-card",
2571
+ "data-view",
2572
+ "assistant-panel",
2573
+ "chat-thread",
2574
+ "action-bar",
2575
+ "timeline",
2576
+ "table",
2577
+ "rich-doc",
2578
+ "form",
2579
+ "chart",
2580
+ "custom-widget"
2581
+ ];
2582
+ function collectSlotIdsFromRegion(node) {
2583
+ const ids = [];
2584
+ if (node.type === "slot") {
2585
+ ids.push(node.slotId);
2586
+ }
2587
+ if (node.type === "panel-group" || node.type === "stack") {
2588
+ for (const child of node.children) {
2589
+ ids.push(...collectSlotIdsFromRegion(child));
1556
2590
  }
1557
- results.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
1558
- const offset = options?.offset ?? 0;
1559
- const limit = options?.limit ?? 100;
1560
- return results.slice(offset, offset + limit);
1561
2591
  }
1562
- async search(query, limit = 20) {
1563
- const lowerQuery = query.toLowerCase();
1564
- const results = [];
1565
- for (const conversation of this.conversations.values()) {
1566
- if (conversation.title?.toLowerCase().includes(lowerQuery)) {
1567
- results.push(conversation);
1568
- continue;
1569
- }
1570
- const hasMatch = conversation.messages.some((m) => m.content.toLowerCase().includes(lowerQuery));
1571
- if (hasMatch) {
1572
- results.push(conversation);
1573
- }
1574
- if (results.length >= limit)
1575
- break;
2592
+ if (node.type === "tabs") {
2593
+ for (const tab of node.tabs) {
2594
+ ids.push(...collectSlotIdsFromRegion(tab.child));
1576
2595
  }
1577
- return results;
1578
2596
  }
1579
- clear() {
1580
- this.conversations.clear();
2597
+ if (node.type === "floating") {
2598
+ ids.push(node.anchorSlotId);
2599
+ ids.push(...collectSlotIdsFromRegion(node.child));
1581
2600
  }
2601
+ return ids;
1582
2602
  }
1583
- function createInMemoryConversationStore() {
1584
- return new InMemoryConversationStore;
2603
+ function deriveConstraints(plan) {
2604
+ const slotIds = collectSlotIdsFromRegion(plan.layoutRoot);
2605
+ const uniqueSlots = [...new Set(slotIds)];
2606
+ return {
2607
+ allowedOps: VALID_OPS,
2608
+ allowedSlots: uniqueSlots.length > 0 ? uniqueSlots : ["assistant", "primary"],
2609
+ allowedNodeKinds: DEFAULT_NODE_KINDS
2610
+ };
2611
+ }
2612
+ var ProposePatchInputSchema = z3.object({
2613
+ proposalId: z3.string().describe("Unique proposal identifier"),
2614
+ ops: z3.array(z3.object({
2615
+ op: z3.enum([
2616
+ "insert-node",
2617
+ "replace-node",
2618
+ "remove-node",
2619
+ "move-node",
2620
+ "resize-panel",
2621
+ "set-layout",
2622
+ "reveal-field",
2623
+ "hide-field",
2624
+ "promote-action",
2625
+ "set-focus"
2626
+ ]),
2627
+ slotId: z3.string().optional(),
2628
+ nodeId: z3.string().optional(),
2629
+ toSlotId: z3.string().optional(),
2630
+ index: z3.number().optional(),
2631
+ node: z3.object({
2632
+ nodeId: z3.string(),
2633
+ kind: z3.string(),
2634
+ title: z3.string().optional(),
2635
+ props: z3.record(z3.string(), z3.unknown()).optional(),
2636
+ children: z3.array(z3.unknown()).optional()
2637
+ }).optional(),
2638
+ persistKey: z3.string().optional(),
2639
+ sizes: z3.array(z3.number()).optional(),
2640
+ layoutId: z3.string().optional(),
2641
+ fieldId: z3.string().optional(),
2642
+ actionId: z3.string().optional(),
2643
+ placement: z3.enum(["header", "inline", "context", "assistant"]).optional(),
2644
+ targetId: z3.string().optional()
2645
+ }))
2646
+ });
2647
+ function createSurfacePlannerTools(config) {
2648
+ const { plan, constraints, onPatchProposal } = config;
2649
+ const resolvedConstraints = constraints ?? deriveConstraints(plan);
2650
+ const proposePatchTool = tool3({
2651
+ description: "Propose surface patches (layout changes, node insertions, etc.) for user approval. " + "Only use allowed ops, slots, and node kinds from the planner context.",
2652
+ inputSchema: ProposePatchInputSchema,
2653
+ execute: async (input) => {
2654
+ const ops = input.ops;
2655
+ try {
2656
+ validatePatchProposal(ops, resolvedConstraints);
2657
+ const proposal = buildSurfacePatchProposal(input.proposalId, ops);
2658
+ onPatchProposal?.(proposal);
2659
+ return {
2660
+ success: true,
2661
+ proposalId: proposal.proposalId,
2662
+ opsCount: proposal.ops.length,
2663
+ message: "Patch proposal validated; awaiting user approval"
2664
+ };
2665
+ } catch (err) {
2666
+ return {
2667
+ success: false,
2668
+ error: err instanceof Error ? err.message : String(err),
2669
+ proposalId: input.proposalId
2670
+ };
2671
+ }
2672
+ }
2673
+ });
2674
+ return {
2675
+ "propose-patch": proposePatchTool
2676
+ };
2677
+ }
2678
+ function buildPlannerPromptInput(plan) {
2679
+ const constraints = deriveConstraints(plan);
2680
+ return {
2681
+ bundleMeta: {
2682
+ key: plan.bundleKey,
2683
+ version: "0.0.0",
2684
+ title: plan.bundleKey
2685
+ },
2686
+ surfaceId: plan.surfaceId,
2687
+ allowedPatchOps: constraints.allowedOps,
2688
+ allowedSlots: [...constraints.allowedSlots],
2689
+ allowedNodeKinds: [...constraints.allowedNodeKinds],
2690
+ actions: plan.actions.map((a) => ({ actionId: a.actionId, title: a.title })),
2691
+ preferences: {
2692
+ guidance: "hints",
2693
+ density: "standard",
2694
+ dataDepth: "detailed",
2695
+ control: "standard",
2696
+ media: "text",
2697
+ pace: "balanced",
2698
+ narrative: "top-down"
2699
+ }
2700
+ };
1585
2701
  }
1586
2702
 
1587
2703
  // src/core/chat-service.ts
2704
+ import { compilePlannerPrompt } from "@contractspec/lib.surface-runtime/runtime/planner-prompt";
1588
2705
  var DEFAULT_SYSTEM_PROMPT = `You are ContractSpec AI, an expert coding assistant specialized in ContractSpec development.
1589
2706
 
1590
2707
  Your capabilities:
@@ -1599,6 +2716,9 @@ Guidelines:
1599
2716
  - Reference relevant ContractSpec concepts and patterns
1600
2717
  - Ask clarifying questions when the user's intent is unclear
1601
2718
  - When suggesting code changes, explain the rationale`;
2719
+ var WORKFLOW_TOOLS_PROMPT = `
2720
+
2721
+ Workflow creation: You can create and modify workflows. Use create_workflow_extension when the user asks to add steps, change a workflow, or create a tenant-specific extension. Use compose_workflow to apply extensions to a base workflow. Use generate_workflow_spec_code to output TypeScript for the user to save.`;
1602
2722
 
1603
2723
  class ChatService {
1604
2724
  provider;
@@ -1608,19 +2728,93 @@ class ChatService {
1608
2728
  maxHistoryMessages;
1609
2729
  onUsage;
1610
2730
  tools;
2731
+ thinkingLevel;
1611
2732
  sendReasoning;
1612
2733
  sendSources;
2734
+ modelSelector;
1613
2735
  constructor(config) {
1614
2736
  this.provider = config.provider;
1615
2737
  this.context = config.context;
1616
2738
  this.store = config.store ?? new InMemoryConversationStore;
1617
- this.systemPrompt = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
2739
+ this.systemPrompt = this.buildSystemPrompt(config);
1618
2740
  this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
1619
2741
  this.onUsage = config.onUsage;
1620
- this.tools = config.tools;
1621
- this.sendReasoning = config.sendReasoning ?? false;
2742
+ this.tools = this.mergeTools(config);
2743
+ this.thinkingLevel = config.thinkingLevel;
2744
+ this.modelSelector = config.modelSelector;
2745
+ this.sendReasoning = config.sendReasoning ?? (config.thinkingLevel != null && config.thinkingLevel !== "instant");
1622
2746
  this.sendSources = config.sendSources ?? false;
1623
2747
  }
2748
+ buildSystemPrompt(config) {
2749
+ let base = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
2750
+ if (config.workflowToolsConfig?.baseWorkflows?.length) {
2751
+ base += WORKFLOW_TOOLS_PROMPT;
2752
+ }
2753
+ const contractsPrompt = buildContractsContextPrompt(config.contractsContext ?? {});
2754
+ if (contractsPrompt) {
2755
+ base += contractsPrompt;
2756
+ }
2757
+ if (config.surfacePlanConfig?.plan) {
2758
+ const plannerInput = buildPlannerPromptInput(config.surfacePlanConfig.plan);
2759
+ base += `
2760
+
2761
+ ` + compilePlannerPrompt(plannerInput);
2762
+ }
2763
+ return base;
2764
+ }
2765
+ mergeTools(config) {
2766
+ let merged = config.tools ?? {};
2767
+ const wfConfig = config.workflowToolsConfig;
2768
+ if (wfConfig?.baseWorkflows?.length) {
2769
+ const workflowTools = createWorkflowTools({
2770
+ baseWorkflows: wfConfig.baseWorkflows,
2771
+ composer: wfConfig.composer
2772
+ });
2773
+ merged = { ...merged, ...workflowTools };
2774
+ }
2775
+ const contractsCtx = config.contractsContext;
2776
+ if (contractsCtx?.agentSpecs?.length) {
2777
+ const allTools = [];
2778
+ for (const agent of contractsCtx.agentSpecs) {
2779
+ if (agent.tools?.length)
2780
+ allTools.push(...agent.tools);
2781
+ }
2782
+ if (allTools.length > 0) {
2783
+ const agentTools = agentToolConfigsToToolSet(allTools);
2784
+ merged = { ...merged, ...agentTools };
2785
+ }
2786
+ }
2787
+ const surfaceConfig = config.surfacePlanConfig;
2788
+ if (surfaceConfig?.plan) {
2789
+ const plannerTools = createSurfacePlannerTools({
2790
+ plan: surfaceConfig.plan,
2791
+ onPatchProposal: surfaceConfig.onPatchProposal
2792
+ });
2793
+ merged = { ...merged, ...plannerTools };
2794
+ }
2795
+ if (config.mcpTools && Object.keys(config.mcpTools).length > 0) {
2796
+ merged = { ...merged, ...config.mcpTools };
2797
+ }
2798
+ return Object.keys(merged).length > 0 ? merged : undefined;
2799
+ }
2800
+ async resolveModel() {
2801
+ if (this.modelSelector) {
2802
+ const dimension = this.thinkingLevelToDimension(this.thinkingLevel);
2803
+ const { model, selection } = await this.modelSelector.selectAndCreate({
2804
+ taskDimension: dimension
2805
+ });
2806
+ return { model, providerName: selection.providerKey };
2807
+ }
2808
+ return {
2809
+ model: this.provider.getModel(),
2810
+ providerName: this.provider.name
2811
+ };
2812
+ }
2813
+ thinkingLevelToDimension(level) {
2814
+ if (!level || level === "instant")
2815
+ return "latency";
2816
+ return "reasoning";
2817
+ }
1624
2818
  async send(options) {
1625
2819
  let conversation;
1626
2820
  if (options.conversationId) {
@@ -1638,20 +2832,25 @@ class ChatService {
1638
2832
  workspacePath: this.context?.workspacePath
1639
2833
  });
1640
2834
  }
1641
- await this.store.appendMessage(conversation.id, {
1642
- role: "user",
1643
- content: options.content,
1644
- status: "completed",
1645
- attachments: options.attachments
1646
- });
2835
+ if (!options.skipUserAppend) {
2836
+ await this.store.appendMessage(conversation.id, {
2837
+ role: "user",
2838
+ content: options.content,
2839
+ status: "completed",
2840
+ attachments: options.attachments
2841
+ });
2842
+ }
2843
+ conversation = await this.store.get(conversation.id) ?? conversation;
1647
2844
  const messages = this.buildMessages(conversation, options);
1648
- const model = this.provider.getModel();
2845
+ const { model, providerName } = await this.resolveModel();
2846
+ const providerOptions = getProviderOptions(this.thinkingLevel, providerName);
1649
2847
  try {
1650
2848
  const result = await generateText({
1651
2849
  model,
1652
2850
  messages,
1653
2851
  system: this.systemPrompt,
1654
- tools: this.tools
2852
+ tools: this.tools,
2853
+ providerOptions: Object.keys(providerOptions).length > 0 ? providerOptions : undefined
1655
2854
  });
1656
2855
  const assistantMessage = await this.store.appendMessage(conversation.id, {
1657
2856
  role: "assistant",
@@ -1696,23 +2895,27 @@ class ChatService {
1696
2895
  workspacePath: this.context?.workspacePath
1697
2896
  });
1698
2897
  }
1699
- await this.store.appendMessage(conversation.id, {
1700
- role: "user",
1701
- content: options.content,
1702
- status: "completed",
1703
- attachments: options.attachments
1704
- });
2898
+ if (!options.skipUserAppend) {
2899
+ await this.store.appendMessage(conversation.id, {
2900
+ role: "user",
2901
+ content: options.content,
2902
+ status: "completed",
2903
+ attachments: options.attachments
2904
+ });
2905
+ }
2906
+ conversation = await this.store.get(conversation.id) ?? conversation;
1705
2907
  const assistantMessage = await this.store.appendMessage(conversation.id, {
1706
2908
  role: "assistant",
1707
2909
  content: "",
1708
2910
  status: "streaming"
1709
2911
  });
1710
2912
  const messages = this.buildMessages(conversation, options);
1711
- const model = this.provider.getModel();
2913
+ const { model, providerName } = await this.resolveModel();
1712
2914
  const systemPrompt = this.systemPrompt;
1713
2915
  const tools = this.tools;
1714
2916
  const store = this.store;
1715
2917
  const onUsage = this.onUsage;
2918
+ const streamProviderOptions = getProviderOptions(this.thinkingLevel, providerName);
1716
2919
  async function* streamGenerator() {
1717
2920
  let fullContent = "";
1718
2921
  let fullReasoning = "";
@@ -1723,7 +2926,8 @@ class ChatService {
1723
2926
  model,
1724
2927
  messages,
1725
2928
  system: systemPrompt,
1726
- tools
2929
+ tools,
2930
+ providerOptions: Object.keys(streamProviderOptions).length > 0 ? streamProviderOptions : undefined
1727
2931
  });
1728
2932
  for await (const part of result.fullStream) {
1729
2933
  if (part.type === "text-delta") {
@@ -1838,6 +3042,18 @@ class ChatService {
1838
3042
  ...options
1839
3043
  });
1840
3044
  }
3045
+ async updateConversation(conversationId, updates) {
3046
+ return this.store.update(conversationId, updates);
3047
+ }
3048
+ async forkConversation(conversationId, upToMessageId) {
3049
+ return this.store.fork(conversationId, upToMessageId);
3050
+ }
3051
+ async updateMessage(conversationId, messageId, updates) {
3052
+ return this.store.updateMessage(conversationId, messageId, updates);
3053
+ }
3054
+ async truncateAfter(conversationId, messageId) {
3055
+ return this.store.truncateAfter(conversationId, messageId);
3056
+ }
1841
3057
  async deleteConversation(conversationId) {
1842
3058
  return this.store.delete(conversationId);
1843
3059
  }
@@ -1908,9 +3124,9 @@ import {
1908
3124
  function toolsToToolSet(defs) {
1909
3125
  const result = {};
1910
3126
  for (const def of defs) {
1911
- result[def.name] = tool({
3127
+ result[def.name] = tool4({
1912
3128
  description: def.description ?? def.name,
1913
- inputSchema: z.object({}).passthrough(),
3129
+ inputSchema: z4.object({}).passthrough(),
1914
3130
  execute: async () => ({})
1915
3131
  });
1916
3132
  }
@@ -1924,22 +3140,64 @@ function useChat(options = {}) {
1924
3140
  apiKey,
1925
3141
  proxyUrl,
1926
3142
  conversationId: initialConversationId,
3143
+ store,
1927
3144
  systemPrompt,
1928
3145
  streaming = true,
1929
3146
  onSend,
1930
3147
  onResponse,
1931
3148
  onError,
1932
3149
  onUsage,
1933
- tools: toolsDefs
3150
+ tools: toolsDefs,
3151
+ thinkingLevel,
3152
+ workflowToolsConfig,
3153
+ modelSelector,
3154
+ contractsContext,
3155
+ surfacePlanConfig,
3156
+ mcpServers,
3157
+ agentMode
1934
3158
  } = options;
1935
- const [messages, setMessages] = React6.useState([]);
1936
- const [conversation, setConversation] = React6.useState(null);
1937
- const [isLoading, setIsLoading] = React6.useState(false);
1938
- const [error, setError] = React6.useState(null);
1939
- const [conversationId, setConversationId] = React6.useState(initialConversationId ?? null);
1940
- const abortControllerRef = React6.useRef(null);
1941
- const chatServiceRef = React6.useRef(null);
1942
- React6.useEffect(() => {
3159
+ const [messages, setMessages] = React11.useState([]);
3160
+ const [mcpTools, setMcpTools] = React11.useState(null);
3161
+ const mcpCleanupRef = React11.useRef(null);
3162
+ const [conversation, setConversation] = React11.useState(null);
3163
+ const [isLoading, setIsLoading] = React11.useState(false);
3164
+ const [error, setError] = React11.useState(null);
3165
+ const [conversationId, setConversationId] = React11.useState(initialConversationId ?? null);
3166
+ const abortControllerRef = React11.useRef(null);
3167
+ const chatServiceRef = React11.useRef(null);
3168
+ React11.useEffect(() => {
3169
+ if (!mcpServers?.length) {
3170
+ setMcpTools(null);
3171
+ return;
3172
+ }
3173
+ let cancelled = false;
3174
+ import("@contractspec/lib.ai-agent/tools/mcp-client").then(({ createMcpToolsets }) => {
3175
+ createMcpToolsets(mcpServers).then(({ tools, cleanup }) => {
3176
+ if (!cancelled) {
3177
+ setMcpTools(tools);
3178
+ mcpCleanupRef.current = cleanup;
3179
+ } else {
3180
+ cleanup().catch(() => {
3181
+ return;
3182
+ });
3183
+ }
3184
+ }).catch(() => {
3185
+ if (!cancelled)
3186
+ setMcpTools(null);
3187
+ });
3188
+ });
3189
+ return () => {
3190
+ cancelled = true;
3191
+ const cleanup = mcpCleanupRef.current;
3192
+ mcpCleanupRef.current = null;
3193
+ if (cleanup)
3194
+ cleanup().catch(() => {
3195
+ return;
3196
+ });
3197
+ setMcpTools(null);
3198
+ };
3199
+ }, [mcpServers]);
3200
+ React11.useEffect(() => {
1943
3201
  const chatProvider = createProvider({
1944
3202
  provider,
1945
3203
  model,
@@ -1948,9 +3206,16 @@ function useChat(options = {}) {
1948
3206
  });
1949
3207
  chatServiceRef.current = new ChatService({
1950
3208
  provider: chatProvider,
3209
+ store,
1951
3210
  systemPrompt,
1952
3211
  onUsage,
1953
- tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined
3212
+ tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined,
3213
+ thinkingLevel,
3214
+ workflowToolsConfig,
3215
+ modelSelector,
3216
+ contractsContext,
3217
+ surfacePlanConfig,
3218
+ mcpTools
1954
3219
  });
1955
3220
  }, [
1956
3221
  provider,
@@ -1958,11 +3223,18 @@ function useChat(options = {}) {
1958
3223
  model,
1959
3224
  apiKey,
1960
3225
  proxyUrl,
3226
+ store,
1961
3227
  systemPrompt,
1962
3228
  onUsage,
1963
- toolsDefs
3229
+ toolsDefs,
3230
+ thinkingLevel,
3231
+ workflowToolsConfig,
3232
+ modelSelector,
3233
+ contractsContext,
3234
+ surfacePlanConfig,
3235
+ mcpTools
1964
3236
  ]);
1965
- React6.useEffect(() => {
3237
+ React11.useEffect(() => {
1966
3238
  if (!conversationId || !chatServiceRef.current)
1967
3239
  return;
1968
3240
  const loadConversation = async () => {
@@ -1976,7 +3248,90 @@ function useChat(options = {}) {
1976
3248
  };
1977
3249
  loadConversation().catch(console.error);
1978
3250
  }, [conversationId]);
1979
- const sendMessage = React6.useCallback(async (content, attachments) => {
3251
+ const sendMessage = React11.useCallback(async (content, attachments, opts) => {
3252
+ if (agentMode?.agent) {
3253
+ setIsLoading(true);
3254
+ setError(null);
3255
+ abortControllerRef.current = new AbortController;
3256
+ try {
3257
+ if (!opts?.skipUserAppend) {
3258
+ const userMessage = {
3259
+ id: `msg_${Date.now()}`,
3260
+ conversationId: conversationId ?? "",
3261
+ role: "user",
3262
+ content,
3263
+ status: "completed",
3264
+ createdAt: new Date,
3265
+ updatedAt: new Date,
3266
+ attachments
3267
+ };
3268
+ setMessages((prev) => [...prev, userMessage]);
3269
+ onSend?.(userMessage);
3270
+ }
3271
+ const result = await agentMode.agent.generate({
3272
+ prompt: content,
3273
+ signal: abortControllerRef.current.signal
3274
+ });
3275
+ const toolCallsMap = new Map;
3276
+ for (const tc of result.toolCalls ?? []) {
3277
+ const tr = result.toolResults?.find((r) => r.toolCallId === tc.toolCallId);
3278
+ toolCallsMap.set(tc.toolCallId, {
3279
+ id: tc.toolCallId,
3280
+ name: tc.toolName,
3281
+ args: tc.args ?? {},
3282
+ result: tr?.output,
3283
+ status: "completed"
3284
+ });
3285
+ }
3286
+ const assistantMessage = {
3287
+ id: `msg_${Date.now()}_a`,
3288
+ conversationId: conversationId ?? "",
3289
+ role: "assistant",
3290
+ content: result.text,
3291
+ status: "completed",
3292
+ createdAt: new Date,
3293
+ updatedAt: new Date,
3294
+ toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined,
3295
+ usage: result.usage
3296
+ };
3297
+ setMessages((prev) => [...prev, assistantMessage]);
3298
+ onResponse?.(assistantMessage);
3299
+ onUsage?.(result.usage ?? { inputTokens: 0, outputTokens: 0 });
3300
+ if (store && !conversationId) {
3301
+ const conv = await store.create({
3302
+ status: "active",
3303
+ provider: "agent",
3304
+ model: "agent",
3305
+ messages: []
3306
+ });
3307
+ if (!opts?.skipUserAppend) {
3308
+ await store.appendMessage(conv.id, {
3309
+ role: "user",
3310
+ content,
3311
+ status: "completed",
3312
+ attachments
3313
+ });
3314
+ }
3315
+ await store.appendMessage(conv.id, {
3316
+ role: "assistant",
3317
+ content: result.text,
3318
+ status: "completed",
3319
+ toolCalls: assistantMessage.toolCalls,
3320
+ usage: result.usage
3321
+ });
3322
+ const updated = await store.get(conv.id);
3323
+ if (updated)
3324
+ setConversation(updated);
3325
+ setConversationId(conv.id);
3326
+ }
3327
+ } catch (err) {
3328
+ setError(err instanceof Error ? err : new Error(String(err)));
3329
+ onError?.(err instanceof Error ? err : new Error(String(err)));
3330
+ } finally {
3331
+ setIsLoading(false);
3332
+ }
3333
+ return;
3334
+ }
1980
3335
  if (!chatServiceRef.current) {
1981
3336
  throw new Error("Chat service not initialized");
1982
3337
  }
@@ -1984,25 +3339,28 @@ function useChat(options = {}) {
1984
3339
  setError(null);
1985
3340
  abortControllerRef.current = new AbortController;
1986
3341
  try {
1987
- const userMessage = {
1988
- id: `msg_${Date.now()}`,
1989
- conversationId: conversationId ?? "",
1990
- role: "user",
1991
- content,
1992
- status: "completed",
1993
- createdAt: new Date,
1994
- updatedAt: new Date,
1995
- attachments
1996
- };
1997
- setMessages((prev) => [...prev, userMessage]);
1998
- onSend?.(userMessage);
3342
+ if (!opts?.skipUserAppend) {
3343
+ const userMessage = {
3344
+ id: `msg_${Date.now()}`,
3345
+ conversationId: conversationId ?? "",
3346
+ role: "user",
3347
+ content,
3348
+ status: "completed",
3349
+ createdAt: new Date,
3350
+ updatedAt: new Date,
3351
+ attachments
3352
+ };
3353
+ setMessages((prev) => [...prev, userMessage]);
3354
+ onSend?.(userMessage);
3355
+ }
1999
3356
  if (streaming) {
2000
3357
  const result = await chatServiceRef.current.stream({
2001
3358
  conversationId: conversationId ?? undefined,
2002
3359
  content,
2003
- attachments
3360
+ attachments,
3361
+ skipUserAppend: opts?.skipUserAppend
2004
3362
  });
2005
- if (!conversationId) {
3363
+ if (!conversationId && !opts?.skipUserAppend) {
2006
3364
  setConversationId(result.conversationId);
2007
3365
  }
2008
3366
  const assistantMessage = {
@@ -2083,7 +3441,8 @@ function useChat(options = {}) {
2083
3441
  const result = await chatServiceRef.current.send({
2084
3442
  conversationId: conversationId ?? undefined,
2085
3443
  content,
2086
- attachments
3444
+ attachments,
3445
+ skipUserAppend: opts?.skipUserAppend
2087
3446
  });
2088
3447
  setConversation(result.conversation);
2089
3448
  setMessages(result.conversation.messages);
@@ -2100,14 +3459,14 @@ function useChat(options = {}) {
2100
3459
  setIsLoading(false);
2101
3460
  abortControllerRef.current = null;
2102
3461
  }
2103
- }, [conversationId, streaming, onSend, onResponse, onError, messages]);
2104
- const clearConversation = React6.useCallback(() => {
3462
+ }, [conversationId, streaming, onSend, onResponse, onError, onUsage, messages, agentMode, store]);
3463
+ const clearConversation = React11.useCallback(() => {
2105
3464
  setMessages([]);
2106
3465
  setConversation(null);
2107
3466
  setConversationId(null);
2108
3467
  setError(null);
2109
3468
  }, []);
2110
- const regenerate = React6.useCallback(async () => {
3469
+ const regenerate = React11.useCallback(async () => {
2111
3470
  const lastUserMessageIndex = messages.findLastIndex((m) => m.role === "user");
2112
3471
  if (lastUserMessageIndex === -1)
2113
3472
  return;
@@ -2117,11 +3476,49 @@ function useChat(options = {}) {
2117
3476
  setMessages((prev) => prev.slice(0, lastUserMessageIndex + 1));
2118
3477
  await sendMessage(lastUserMessage.content, lastUserMessage.attachments);
2119
3478
  }, [messages, sendMessage]);
2120
- const stop = React6.useCallback(() => {
3479
+ const stop = React11.useCallback(() => {
2121
3480
  abortControllerRef.current?.abort();
2122
3481
  setIsLoading(false);
2123
3482
  }, []);
2124
- const addToolApprovalResponse = React6.useCallback((_toolCallId, _result) => {
3483
+ const createNewConversation = clearConversation;
3484
+ const editMessage = React11.useCallback(async (messageId, newContent) => {
3485
+ if (!chatServiceRef.current || !conversationId)
3486
+ return;
3487
+ const msg = messages.find((m) => m.id === messageId);
3488
+ if (!msg || msg.role !== "user")
3489
+ return;
3490
+ await chatServiceRef.current.updateMessage(conversationId, messageId, { content: newContent });
3491
+ const truncated = await chatServiceRef.current.truncateAfter(conversationId, messageId);
3492
+ if (truncated) {
3493
+ setMessages(truncated.messages);
3494
+ }
3495
+ await sendMessage(newContent, undefined, { skipUserAppend: true });
3496
+ }, [conversationId, messages, sendMessage]);
3497
+ const forkConversation = React11.useCallback(async (upToMessageId) => {
3498
+ if (!chatServiceRef.current)
3499
+ return null;
3500
+ const idToFork = conversationId ?? conversation?.id;
3501
+ if (!idToFork)
3502
+ return null;
3503
+ try {
3504
+ const forked = await chatServiceRef.current.forkConversation(idToFork, upToMessageId);
3505
+ setConversationId(forked.id);
3506
+ setConversation(forked);
3507
+ setMessages(forked.messages);
3508
+ return forked.id;
3509
+ } catch {
3510
+ return null;
3511
+ }
3512
+ }, [conversationId, conversation]);
3513
+ const updateConversationFn = React11.useCallback(async (updates) => {
3514
+ if (!chatServiceRef.current || !conversationId)
3515
+ return null;
3516
+ const updated = await chatServiceRef.current.updateConversation(conversationId, updates);
3517
+ if (updated)
3518
+ setConversation(updated);
3519
+ return updated;
3520
+ }, [conversationId]);
3521
+ const addToolApprovalResponse = React11.useCallback((_toolCallId, _result) => {
2125
3522
  throw new Error(`addToolApprovalResponse: Tool approval requires server route with toUIMessageStreamResponse. ` + `Use createChatRoute and @ai-sdk/react useChat for tools with requireApproval.`);
2126
3523
  }, []);
2127
3524
  const hasApprovalTools = toolsDefs?.some((t) => t.requireApproval) ?? false;
@@ -2135,20 +3532,731 @@ function useChat(options = {}) {
2135
3532
  setConversationId,
2136
3533
  regenerate,
2137
3534
  stop,
3535
+ createNewConversation,
3536
+ editMessage,
3537
+ forkConversation,
3538
+ updateConversation: updateConversationFn,
2138
3539
  ...hasApprovalTools && { addToolApprovalResponse }
2139
3540
  };
2140
3541
  }
3542
+
3543
+ // src/core/local-storage-conversation-store.ts
3544
+ var DEFAULT_KEY = "contractspec:ai-chat:conversations";
3545
+ function generateId2(prefix) {
3546
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
3547
+ }
3548
+ function toSerializable(conv) {
3549
+ return {
3550
+ ...conv,
3551
+ createdAt: conv.createdAt.toISOString(),
3552
+ updatedAt: conv.updatedAt.toISOString(),
3553
+ messages: conv.messages.map((m) => ({
3554
+ ...m,
3555
+ createdAt: m.createdAt.toISOString(),
3556
+ updatedAt: m.updatedAt.toISOString()
3557
+ }))
3558
+ };
3559
+ }
3560
+ function fromSerializable(raw) {
3561
+ const messages = raw.messages?.map((m) => ({
3562
+ ...m,
3563
+ createdAt: new Date(m.createdAt),
3564
+ updatedAt: new Date(m.updatedAt)
3565
+ })) ?? [];
3566
+ return {
3567
+ ...raw,
3568
+ createdAt: new Date(raw.createdAt),
3569
+ updatedAt: new Date(raw.updatedAt),
3570
+ messages
3571
+ };
3572
+ }
3573
+ function loadAll(key) {
3574
+ if (typeof window === "undefined")
3575
+ return new Map;
3576
+ try {
3577
+ const raw = window.localStorage.getItem(key);
3578
+ if (!raw)
3579
+ return new Map;
3580
+ const arr = JSON.parse(raw);
3581
+ const map = new Map;
3582
+ for (const item of arr) {
3583
+ const conv = fromSerializable(item);
3584
+ map.set(conv.id, conv);
3585
+ }
3586
+ return map;
3587
+ } catch {
3588
+ return new Map;
3589
+ }
3590
+ }
3591
+ function saveAll(key, map) {
3592
+ if (typeof window === "undefined")
3593
+ return;
3594
+ try {
3595
+ const arr = Array.from(map.values()).map(toSerializable);
3596
+ window.localStorage.setItem(key, JSON.stringify(arr));
3597
+ } catch {}
3598
+ }
3599
+
3600
+ class LocalStorageConversationStore {
3601
+ key;
3602
+ cache = null;
3603
+ constructor(storageKey = DEFAULT_KEY) {
3604
+ this.key = storageKey;
3605
+ }
3606
+ getMap() {
3607
+ if (!this.cache) {
3608
+ this.cache = loadAll(this.key);
3609
+ }
3610
+ return this.cache;
3611
+ }
3612
+ persist() {
3613
+ saveAll(this.key, this.getMap());
3614
+ }
3615
+ async get(conversationId) {
3616
+ return this.getMap().get(conversationId) ?? null;
3617
+ }
3618
+ async create(conversation) {
3619
+ const now = new Date;
3620
+ const full = {
3621
+ ...conversation,
3622
+ id: generateId2("conv"),
3623
+ createdAt: now,
3624
+ updatedAt: now
3625
+ };
3626
+ this.getMap().set(full.id, full);
3627
+ this.persist();
3628
+ return full;
3629
+ }
3630
+ async update(conversationId, updates) {
3631
+ const conv = this.getMap().get(conversationId);
3632
+ if (!conv)
3633
+ return null;
3634
+ const updated = {
3635
+ ...conv,
3636
+ ...updates,
3637
+ updatedAt: new Date
3638
+ };
3639
+ this.getMap().set(conversationId, updated);
3640
+ this.persist();
3641
+ return updated;
3642
+ }
3643
+ async appendMessage(conversationId, message) {
3644
+ const conv = this.getMap().get(conversationId);
3645
+ if (!conv)
3646
+ throw new Error(`Conversation ${conversationId} not found`);
3647
+ const now = new Date;
3648
+ const fullMessage = {
3649
+ ...message,
3650
+ id: generateId2("msg"),
3651
+ conversationId,
3652
+ createdAt: now,
3653
+ updatedAt: now
3654
+ };
3655
+ conv.messages.push(fullMessage);
3656
+ conv.updatedAt = now;
3657
+ this.persist();
3658
+ return fullMessage;
3659
+ }
3660
+ async updateMessage(conversationId, messageId, updates) {
3661
+ const conv = this.getMap().get(conversationId);
3662
+ if (!conv)
3663
+ return null;
3664
+ const idx = conv.messages.findIndex((m) => m.id === messageId);
3665
+ if (idx === -1)
3666
+ return null;
3667
+ const msg = conv.messages[idx];
3668
+ if (!msg)
3669
+ return null;
3670
+ const updated = {
3671
+ ...msg,
3672
+ ...updates,
3673
+ updatedAt: new Date
3674
+ };
3675
+ conv.messages[idx] = updated;
3676
+ conv.updatedAt = new Date;
3677
+ this.persist();
3678
+ return updated;
3679
+ }
3680
+ async delete(conversationId) {
3681
+ const deleted = this.getMap().delete(conversationId);
3682
+ if (deleted)
3683
+ this.persist();
3684
+ return deleted;
3685
+ }
3686
+ async list(options) {
3687
+ let results = Array.from(this.getMap().values());
3688
+ if (options?.status) {
3689
+ results = results.filter((c) => c.status === options.status);
3690
+ }
3691
+ if (options?.projectId) {
3692
+ results = results.filter((c) => c.projectId === options.projectId);
3693
+ }
3694
+ if (options?.tags && options.tags.length > 0) {
3695
+ const tagSet = new Set(options.tags);
3696
+ results = results.filter((c) => c.tags && c.tags.some((t) => tagSet.has(t)));
3697
+ }
3698
+ results.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
3699
+ const offset = options?.offset ?? 0;
3700
+ const limit = options?.limit ?? 100;
3701
+ return results.slice(offset, offset + limit);
3702
+ }
3703
+ async fork(conversationId, upToMessageId) {
3704
+ const source = this.getMap().get(conversationId);
3705
+ if (!source)
3706
+ throw new Error(`Conversation ${conversationId} not found`);
3707
+ let messagesToCopy = source.messages;
3708
+ if (upToMessageId) {
3709
+ const idx = source.messages.findIndex((m) => m.id === upToMessageId);
3710
+ if (idx === -1)
3711
+ throw new Error(`Message ${upToMessageId} not found`);
3712
+ messagesToCopy = source.messages.slice(0, idx + 1);
3713
+ }
3714
+ const now = new Date;
3715
+ const forkedMessages = messagesToCopy.map((m) => ({
3716
+ ...m,
3717
+ id: generateId2("msg"),
3718
+ conversationId: "",
3719
+ createdAt: new Date(m.createdAt),
3720
+ updatedAt: new Date(m.updatedAt)
3721
+ }));
3722
+ const forked = {
3723
+ ...source,
3724
+ id: generateId2("conv"),
3725
+ title: source.title ? `${source.title} (fork)` : undefined,
3726
+ forkedFromId: source.id,
3727
+ createdAt: now,
3728
+ updatedAt: now,
3729
+ messages: forkedMessages
3730
+ };
3731
+ for (const m of forked.messages) {
3732
+ m.conversationId = forked.id;
3733
+ }
3734
+ this.getMap().set(forked.id, forked);
3735
+ this.persist();
3736
+ return forked;
3737
+ }
3738
+ async truncateAfter(conversationId, messageId) {
3739
+ const conv = this.getMap().get(conversationId);
3740
+ if (!conv)
3741
+ return null;
3742
+ const idx = conv.messages.findIndex((m) => m.id === messageId);
3743
+ if (idx === -1)
3744
+ return null;
3745
+ conv.messages = conv.messages.slice(0, idx + 1);
3746
+ conv.updatedAt = new Date;
3747
+ this.persist();
3748
+ return conv;
3749
+ }
3750
+ async search(query, limit = 20) {
3751
+ const lowerQuery = query.toLowerCase();
3752
+ const results = [];
3753
+ for (const conv of this.getMap().values()) {
3754
+ if (conv.title?.toLowerCase().includes(lowerQuery)) {
3755
+ results.push(conv);
3756
+ continue;
3757
+ }
3758
+ if (conv.messages.some((m) => m.content.toLowerCase().includes(lowerQuery))) {
3759
+ results.push(conv);
3760
+ }
3761
+ if (results.length >= limit)
3762
+ break;
3763
+ }
3764
+ return results;
3765
+ }
3766
+ }
3767
+ function createLocalStorageConversationStore(storageKey) {
3768
+ return new LocalStorageConversationStore(storageKey);
3769
+ }
3770
+
3771
+ // src/presentation/components/ChatWithSidebar.tsx
3772
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
3773
+ "use client";
3774
+ var defaultStore = createLocalStorageConversationStore();
3775
+ function ChatWithSidebar({
3776
+ store = defaultStore,
3777
+ projectId,
3778
+ tags,
3779
+ className,
3780
+ thinkingLevel: initialThinkingLevel = "thinking",
3781
+ presentationRenderer,
3782
+ formRenderer,
3783
+ ...useChatOptions
3784
+ }) {
3785
+ const effectiveStore = store;
3786
+ const [thinkingLevel, setThinkingLevel] = React12.useState(initialThinkingLevel);
3787
+ const chat = useChat({
3788
+ ...useChatOptions,
3789
+ store: effectiveStore,
3790
+ thinkingLevel
3791
+ });
3792
+ const {
3793
+ messages,
3794
+ conversation,
3795
+ sendMessage,
3796
+ isLoading,
3797
+ setConversationId,
3798
+ createNewConversation,
3799
+ editMessage,
3800
+ forkConversation,
3801
+ updateConversation
3802
+ } = chat;
3803
+ const selectedConversationId = conversation?.id ?? null;
3804
+ const handleSelectConversation = React12.useCallback((id) => {
3805
+ setConversationId(id);
3806
+ }, [setConversationId]);
3807
+ return /* @__PURE__ */ jsxs10("div", {
3808
+ className: className ?? "flex h-full w-full",
3809
+ children: [
3810
+ /* @__PURE__ */ jsx10(ChatSidebar, {
3811
+ store: effectiveStore,
3812
+ selectedConversationId,
3813
+ onSelectConversation: handleSelectConversation,
3814
+ onCreateNew: createNewConversation,
3815
+ projectId,
3816
+ tags,
3817
+ selectedConversation: conversation,
3818
+ onUpdateConversation: updateConversation ? async (id, updates) => {
3819
+ if (id === selectedConversationId) {
3820
+ await updateConversation(updates);
3821
+ }
3822
+ } : undefined
3823
+ }),
3824
+ /* @__PURE__ */ jsx10("div", {
3825
+ className: "flex min-w-0 flex-1 flex-col",
3826
+ children: /* @__PURE__ */ jsx10(ChatWithExport, {
3827
+ messages,
3828
+ conversation,
3829
+ onCreateNew: createNewConversation,
3830
+ onFork: forkConversation,
3831
+ onEditMessage: editMessage,
3832
+ thinkingLevel,
3833
+ onThinkingLevelChange: setThinkingLevel,
3834
+ presentationRenderer,
3835
+ formRenderer,
3836
+ children: /* @__PURE__ */ jsx10(ChatInput, {
3837
+ onSend: (content, att) => sendMessage(content, att),
3838
+ disabled: isLoading,
3839
+ isLoading
3840
+ })
3841
+ })
3842
+ })
3843
+ ]
3844
+ });
3845
+ }
3846
+ // src/presentation/components/ModelPicker.tsx
3847
+ import * as React13 from "react";
3848
+ import { cn as cn7 } from "@contractspec/lib.ui-kit-web/ui/utils";
3849
+ import { Button as Button6 } from "@contractspec/lib.design-system";
3850
+ import {
3851
+ Select as Select2,
3852
+ SelectContent as SelectContent2,
3853
+ SelectItem as SelectItem2,
3854
+ SelectTrigger as SelectTrigger2,
3855
+ SelectValue as SelectValue2
3856
+ } from "@contractspec/lib.ui-kit-web/ui/select";
3857
+ import { Badge } from "@contractspec/lib.ui-kit-web/ui/badge";
3858
+ import { Label as Label2 } from "@contractspec/lib.ui-kit-web/ui/label";
3859
+ import { Bot as Bot2, Cloud, Cpu, Sparkles } from "lucide-react";
3860
+ import {
3861
+ getModelsForProvider
3862
+ } from "@contractspec/lib.ai-providers";
3863
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
3864
+ "use client";
3865
+ var PROVIDER_ICONS = {
3866
+ ollama: /* @__PURE__ */ jsx11(Cpu, {
3867
+ className: "h-4 w-4"
3868
+ }),
3869
+ openai: /* @__PURE__ */ jsx11(Bot2, {
3870
+ className: "h-4 w-4"
3871
+ }),
3872
+ anthropic: /* @__PURE__ */ jsx11(Sparkles, {
3873
+ className: "h-4 w-4"
3874
+ }),
3875
+ mistral: /* @__PURE__ */ jsx11(Cloud, {
3876
+ className: "h-4 w-4"
3877
+ }),
3878
+ gemini: /* @__PURE__ */ jsx11(Sparkles, {
3879
+ className: "h-4 w-4"
3880
+ })
3881
+ };
3882
+ var PROVIDER_NAMES = {
3883
+ ollama: "Ollama (Local)",
3884
+ openai: "OpenAI",
3885
+ anthropic: "Anthropic",
3886
+ mistral: "Mistral",
3887
+ gemini: "Google Gemini"
3888
+ };
3889
+ var MODE_BADGES = {
3890
+ local: { label: "Local", variant: "secondary" },
3891
+ byok: { label: "BYOK", variant: "outline" },
3892
+ managed: { label: "Managed", variant: "default" }
3893
+ };
3894
+ function ModelPicker({
3895
+ value,
3896
+ onChange,
3897
+ availableProviders,
3898
+ className,
3899
+ compact = false
3900
+ }) {
3901
+ const providers = availableProviders ?? [
3902
+ { provider: "ollama", available: true, mode: "local" },
3903
+ { provider: "openai", available: true, mode: "byok" },
3904
+ { provider: "anthropic", available: true, mode: "byok" },
3905
+ { provider: "mistral", available: true, mode: "byok" },
3906
+ { provider: "gemini", available: true, mode: "byok" }
3907
+ ];
3908
+ const models = getModelsForProvider(value.provider);
3909
+ const selectedModel = models.find((m) => m.id === value.model);
3910
+ const handleProviderChange = React13.useCallback((providerName) => {
3911
+ const provider = providerName;
3912
+ const providerInfo = providers.find((p) => p.provider === provider);
3913
+ const providerModels = getModelsForProvider(provider);
3914
+ const defaultModel = providerModels[0]?.id ?? "";
3915
+ onChange({
3916
+ provider,
3917
+ model: defaultModel,
3918
+ mode: providerInfo?.mode ?? "byok"
3919
+ });
3920
+ }, [onChange, providers]);
3921
+ const handleModelChange = React13.useCallback((modelId) => {
3922
+ onChange({
3923
+ ...value,
3924
+ model: modelId
3925
+ });
3926
+ }, [onChange, value]);
3927
+ if (compact) {
3928
+ return /* @__PURE__ */ jsxs11("div", {
3929
+ className: cn7("flex items-center gap-2", className),
3930
+ children: [
3931
+ /* @__PURE__ */ jsxs11(Select2, {
3932
+ value: value.provider,
3933
+ onValueChange: handleProviderChange,
3934
+ children: [
3935
+ /* @__PURE__ */ jsx11(SelectTrigger2, {
3936
+ className: "w-[140px]",
3937
+ children: /* @__PURE__ */ jsx11(SelectValue2, {})
3938
+ }),
3939
+ /* @__PURE__ */ jsx11(SelectContent2, {
3940
+ children: providers.map((p) => /* @__PURE__ */ jsx11(SelectItem2, {
3941
+ value: p.provider,
3942
+ disabled: !p.available,
3943
+ children: /* @__PURE__ */ jsxs11("div", {
3944
+ className: "flex items-center gap-2",
3945
+ children: [
3946
+ PROVIDER_ICONS[p.provider],
3947
+ /* @__PURE__ */ jsx11("span", {
3948
+ children: PROVIDER_NAMES[p.provider]
3949
+ })
3950
+ ]
3951
+ })
3952
+ }, p.provider))
3953
+ })
3954
+ ]
3955
+ }),
3956
+ /* @__PURE__ */ jsxs11(Select2, {
3957
+ value: value.model,
3958
+ onValueChange: handleModelChange,
3959
+ children: [
3960
+ /* @__PURE__ */ jsx11(SelectTrigger2, {
3961
+ className: "w-[160px]",
3962
+ children: /* @__PURE__ */ jsx11(SelectValue2, {})
3963
+ }),
3964
+ /* @__PURE__ */ jsx11(SelectContent2, {
3965
+ children: models.map((m) => /* @__PURE__ */ jsx11(SelectItem2, {
3966
+ value: m.id,
3967
+ children: m.name
3968
+ }, m.id))
3969
+ })
3970
+ ]
3971
+ })
3972
+ ]
3973
+ });
3974
+ }
3975
+ return /* @__PURE__ */ jsxs11("div", {
3976
+ className: cn7("flex flex-col gap-3", className),
3977
+ children: [
3978
+ /* @__PURE__ */ jsxs11("div", {
3979
+ className: "flex flex-col gap-1.5",
3980
+ children: [
3981
+ /* @__PURE__ */ jsx11(Label2, {
3982
+ htmlFor: "provider-selection",
3983
+ className: "text-sm font-medium",
3984
+ children: "Provider"
3985
+ }),
3986
+ /* @__PURE__ */ jsx11("div", {
3987
+ className: "flex flex-wrap gap-2",
3988
+ id: "provider-selection",
3989
+ children: providers.map((p) => /* @__PURE__ */ jsxs11(Button6, {
3990
+ variant: value.provider === p.provider ? "default" : "outline",
3991
+ size: "sm",
3992
+ onPress: () => p.available && handleProviderChange(p.provider),
3993
+ disabled: !p.available,
3994
+ className: cn7(!p.available && "opacity-50"),
3995
+ children: [
3996
+ PROVIDER_ICONS[p.provider],
3997
+ /* @__PURE__ */ jsx11("span", {
3998
+ children: PROVIDER_NAMES[p.provider]
3999
+ }),
4000
+ /* @__PURE__ */ jsx11(Badge, {
4001
+ variant: MODE_BADGES[p.mode].variant,
4002
+ className: "ml-1",
4003
+ children: MODE_BADGES[p.mode].label
4004
+ })
4005
+ ]
4006
+ }, p.provider))
4007
+ })
4008
+ ]
4009
+ }),
4010
+ /* @__PURE__ */ jsxs11("div", {
4011
+ className: "flex flex-col gap-1.5",
4012
+ children: [
4013
+ /* @__PURE__ */ jsx11(Label2, {
4014
+ htmlFor: "model-picker",
4015
+ className: "text-sm font-medium",
4016
+ children: "Model"
4017
+ }),
4018
+ /* @__PURE__ */ jsxs11(Select2, {
4019
+ name: "model-picker",
4020
+ value: value.model,
4021
+ onValueChange: handleModelChange,
4022
+ children: [
4023
+ /* @__PURE__ */ jsx11(SelectTrigger2, {
4024
+ children: /* @__PURE__ */ jsx11(SelectValue2, {
4025
+ placeholder: "Select a model"
4026
+ })
4027
+ }),
4028
+ /* @__PURE__ */ jsx11(SelectContent2, {
4029
+ children: models.map((m) => /* @__PURE__ */ jsx11(SelectItem2, {
4030
+ value: m.id,
4031
+ children: /* @__PURE__ */ jsxs11("div", {
4032
+ className: "flex items-center gap-2",
4033
+ children: [
4034
+ /* @__PURE__ */ jsx11("span", {
4035
+ children: m.name
4036
+ }),
4037
+ /* @__PURE__ */ jsxs11("span", {
4038
+ className: "text-muted-foreground text-xs",
4039
+ children: [
4040
+ Math.round(m.contextWindow / 1000),
4041
+ "K"
4042
+ ]
4043
+ }),
4044
+ m.capabilities.vision && /* @__PURE__ */ jsx11(Badge, {
4045
+ variant: "outline",
4046
+ className: "text-xs",
4047
+ children: "Vision"
4048
+ }),
4049
+ m.capabilities.reasoning && /* @__PURE__ */ jsx11(Badge, {
4050
+ variant: "outline",
4051
+ className: "text-xs",
4052
+ children: "Reasoning"
4053
+ })
4054
+ ]
4055
+ })
4056
+ }, m.id))
4057
+ })
4058
+ ]
4059
+ })
4060
+ ]
4061
+ }),
4062
+ selectedModel && /* @__PURE__ */ jsxs11("div", {
4063
+ className: "text-muted-foreground flex flex-wrap gap-2 text-xs",
4064
+ children: [
4065
+ /* @__PURE__ */ jsxs11("span", {
4066
+ children: [
4067
+ "Context: ",
4068
+ Math.round(selectedModel.contextWindow / 1000),
4069
+ "K tokens"
4070
+ ]
4071
+ }),
4072
+ selectedModel.capabilities.vision && /* @__PURE__ */ jsx11("span", {
4073
+ children: "• Vision"
4074
+ }),
4075
+ selectedModel.capabilities.tools && /* @__PURE__ */ jsx11("span", {
4076
+ children: "• Tools"
4077
+ }),
4078
+ selectedModel.capabilities.reasoning && /* @__PURE__ */ jsx11("span", {
4079
+ children: "• Reasoning"
4080
+ })
4081
+ ]
4082
+ })
4083
+ ]
4084
+ });
4085
+ }
4086
+ // src/presentation/components/ContextIndicator.tsx
4087
+ import { cn as cn8 } from "@contractspec/lib.ui-kit-web/ui/utils";
4088
+ import { Badge as Badge2 } from "@contractspec/lib.ui-kit-web/ui/badge";
4089
+ import {
4090
+ Tooltip,
4091
+ TooltipContent,
4092
+ TooltipProvider,
4093
+ TooltipTrigger
4094
+ } from "@contractspec/lib.ui-kit-web/ui/tooltip";
4095
+ import { FolderOpen, FileCode, Zap, Info } from "lucide-react";
4096
+ import { jsx as jsx12, jsxs as jsxs12, Fragment as Fragment6 } from "react/jsx-runtime";
4097
+ "use client";
4098
+ function ContextIndicator({
4099
+ summary,
4100
+ active = false,
4101
+ className,
4102
+ showDetails = true
4103
+ }) {
4104
+ if (!summary && !active) {
4105
+ return /* @__PURE__ */ jsxs12("div", {
4106
+ className: cn8("flex items-center gap-1.5 text-sm", "text-muted-foreground", className),
4107
+ children: [
4108
+ /* @__PURE__ */ jsx12(Info, {
4109
+ className: "h-4 w-4"
4110
+ }),
4111
+ /* @__PURE__ */ jsx12("span", {
4112
+ children: "No workspace context"
4113
+ })
4114
+ ]
4115
+ });
4116
+ }
4117
+ const content = /* @__PURE__ */ jsxs12("div", {
4118
+ className: cn8("flex items-center gap-2", active ? "text-foreground" : "text-muted-foreground", className),
4119
+ children: [
4120
+ /* @__PURE__ */ jsxs12(Badge2, {
4121
+ variant: active ? "default" : "secondary",
4122
+ className: "flex items-center gap-1",
4123
+ children: [
4124
+ /* @__PURE__ */ jsx12(Zap, {
4125
+ className: "h-3 w-3"
4126
+ }),
4127
+ "Context"
4128
+ ]
4129
+ }),
4130
+ summary && showDetails && /* @__PURE__ */ jsxs12(Fragment6, {
4131
+ children: [
4132
+ /* @__PURE__ */ jsxs12("div", {
4133
+ className: "flex items-center gap-1 text-xs",
4134
+ children: [
4135
+ /* @__PURE__ */ jsx12(FolderOpen, {
4136
+ className: "h-3.5 w-3.5"
4137
+ }),
4138
+ /* @__PURE__ */ jsx12("span", {
4139
+ children: summary.name
4140
+ })
4141
+ ]
4142
+ }),
4143
+ /* @__PURE__ */ jsxs12("div", {
4144
+ className: "flex items-center gap-1 text-xs",
4145
+ children: [
4146
+ /* @__PURE__ */ jsx12(FileCode, {
4147
+ className: "h-3.5 w-3.5"
4148
+ }),
4149
+ /* @__PURE__ */ jsxs12("span", {
4150
+ children: [
4151
+ summary.specs.total,
4152
+ " specs"
4153
+ ]
4154
+ })
4155
+ ]
4156
+ })
4157
+ ]
4158
+ })
4159
+ ]
4160
+ });
4161
+ if (!summary) {
4162
+ return content;
4163
+ }
4164
+ return /* @__PURE__ */ jsx12(TooltipProvider, {
4165
+ children: /* @__PURE__ */ jsxs12(Tooltip, {
4166
+ children: [
4167
+ /* @__PURE__ */ jsx12(TooltipTrigger, {
4168
+ asChild: true,
4169
+ children: content
4170
+ }),
4171
+ /* @__PURE__ */ jsx12(TooltipContent, {
4172
+ side: "bottom",
4173
+ className: "max-w-[300px]",
4174
+ children: /* @__PURE__ */ jsxs12("div", {
4175
+ className: "flex flex-col gap-2 text-sm",
4176
+ children: [
4177
+ /* @__PURE__ */ jsx12("div", {
4178
+ className: "font-medium",
4179
+ children: summary.name
4180
+ }),
4181
+ /* @__PURE__ */ jsx12("div", {
4182
+ className: "text-muted-foreground text-xs",
4183
+ children: summary.path
4184
+ }),
4185
+ /* @__PURE__ */ jsx12("div", {
4186
+ className: "border-t pt-2",
4187
+ children: /* @__PURE__ */ jsxs12("div", {
4188
+ className: "grid grid-cols-2 gap-1 text-xs",
4189
+ children: [
4190
+ /* @__PURE__ */ jsx12("span", {
4191
+ children: "Commands:"
4192
+ }),
4193
+ /* @__PURE__ */ jsx12("span", {
4194
+ className: "text-right",
4195
+ children: summary.specs.commands
4196
+ }),
4197
+ /* @__PURE__ */ jsx12("span", {
4198
+ children: "Queries:"
4199
+ }),
4200
+ /* @__PURE__ */ jsx12("span", {
4201
+ className: "text-right",
4202
+ children: summary.specs.queries
4203
+ }),
4204
+ /* @__PURE__ */ jsx12("span", {
4205
+ children: "Events:"
4206
+ }),
4207
+ /* @__PURE__ */ jsx12("span", {
4208
+ className: "text-right",
4209
+ children: summary.specs.events
4210
+ }),
4211
+ /* @__PURE__ */ jsx12("span", {
4212
+ children: "Presentations:"
4213
+ }),
4214
+ /* @__PURE__ */ jsx12("span", {
4215
+ className: "text-right",
4216
+ children: summary.specs.presentations
4217
+ })
4218
+ ]
4219
+ })
4220
+ }),
4221
+ /* @__PURE__ */ jsxs12("div", {
4222
+ className: "border-t pt-2 text-xs",
4223
+ children: [
4224
+ /* @__PURE__ */ jsxs12("span", {
4225
+ children: [
4226
+ summary.files.total,
4227
+ " files"
4228
+ ]
4229
+ }),
4230
+ /* @__PURE__ */ jsx12("span", {
4231
+ className: "mx-1",
4232
+ children: "•"
4233
+ }),
4234
+ /* @__PURE__ */ jsxs12("span", {
4235
+ children: [
4236
+ summary.files.specFiles,
4237
+ " spec files"
4238
+ ]
4239
+ })
4240
+ ]
4241
+ })
4242
+ ]
4243
+ })
4244
+ })
4245
+ ]
4246
+ })
4247
+ });
4248
+ }
2141
4249
  // src/presentation/hooks/useProviders.tsx
2142
- import * as React7 from "react";
4250
+ import * as React14 from "react";
2143
4251
  import {
2144
4252
  getAvailableProviders,
2145
4253
  getModelsForProvider as getModelsForProvider2
2146
4254
  } from "@contractspec/lib.ai-providers";
2147
4255
  "use client";
2148
4256
  function useProviders() {
2149
- const [providers, setProviders] = React7.useState([]);
2150
- const [isLoading, setIsLoading] = React7.useState(true);
2151
- const loadProviders = React7.useCallback(async () => {
4257
+ const [providers, setProviders] = React14.useState([]);
4258
+ const [isLoading, setIsLoading] = React14.useState(true);
4259
+ const loadProviders = React14.useCallback(async () => {
2152
4260
  setIsLoading(true);
2153
4261
  try {
2154
4262
  const available = getAvailableProviders();
@@ -2163,12 +4271,12 @@ function useProviders() {
2163
4271
  setIsLoading(false);
2164
4272
  }
2165
4273
  }, []);
2166
- React7.useEffect(() => {
4274
+ React14.useEffect(() => {
2167
4275
  loadProviders();
2168
4276
  }, [loadProviders]);
2169
- const availableProviders = React7.useMemo(() => providers.filter((p) => p.available), [providers]);
2170
- const isAvailable = React7.useCallback((provider) => providers.some((p) => p.provider === provider && p.available), [providers]);
2171
- const getModelsCallback = React7.useCallback((provider) => providers.find((p) => p.provider === provider)?.models ?? [], [providers]);
4277
+ const availableProviders = React14.useMemo(() => providers.filter((p) => p.available), [providers]);
4278
+ const isAvailable = React14.useCallback((provider) => providers.some((p) => p.provider === provider && p.available), [providers]);
4279
+ const getModelsCallback = React14.useCallback((provider) => providers.find((p) => p.provider === provider)?.models ?? [], [providers]);
2172
4280
  return {
2173
4281
  providers,
2174
4282
  availableProviders,
@@ -2512,15 +4620,64 @@ var ChatErrorEvent = defineEvent({
2512
4620
  }
2513
4621
  })
2514
4622
  });
4623
+ // src/adapters/ai-sdk-bundle-adapter.ts
4624
+ import { compilePlannerPrompt as compilePlannerPrompt2 } from "@contractspec/lib.surface-runtime/runtime/planner-prompt";
4625
+ function createAiSdkBundleAdapter(deps) {
4626
+ const { provider, onPatchProposal } = deps;
4627
+ return {
4628
+ startThread(_args) {
4629
+ return null;
4630
+ },
4631
+ async requestPatches(args) {
4632
+ const proposals = [];
4633
+ const captureProposal = (p) => {
4634
+ proposals.push(p);
4635
+ onPatchProposal?.(p);
4636
+ };
4637
+ const plannerInput = buildPlannerPromptInput(args.currentPlan);
4638
+ const systemPrompt = compilePlannerPrompt2(plannerInput);
4639
+ const service = new ChatService({
4640
+ provider,
4641
+ systemPrompt,
4642
+ surfacePlanConfig: {
4643
+ plan: args.currentPlan,
4644
+ onPatchProposal: captureProposal
4645
+ }
4646
+ });
4647
+ await service.send({
4648
+ content: args.userMessage
4649
+ });
4650
+ return proposals;
4651
+ }
4652
+ };
4653
+ }
4654
+ // src/core/agent-adapter.ts
4655
+ function createChatAgentAdapter(agent) {
4656
+ return {
4657
+ async generate({ prompt, signal }) {
4658
+ const result = await agent.generate({ prompt, signal });
4659
+ return {
4660
+ text: result.text,
4661
+ toolCalls: result.toolCalls,
4662
+ toolResults: result.toolResults,
4663
+ usage: result.usage
4664
+ };
4665
+ }
4666
+ };
4667
+ }
2515
4668
  export {
2516
4669
  validateProvider,
2517
4670
  useProviders,
4671
+ useMessageSelection,
4672
+ useConversations,
2518
4673
  useCompletion,
2519
4674
  useChat,
2520
4675
  supportsLocalMode,
2521
4676
  listOllamaModels,
2522
4677
  isStudioAvailable,
4678
+ isPresentationToolResult,
2523
4679
  isOllamaRunning,
4680
+ isFormToolResult,
2524
4681
  hasCredentials,
2525
4682
  getRecommendedModels,
2526
4683
  getModelsForProvider3 as getModelsForProvider,
@@ -2533,7 +4690,11 @@ export {
2533
4690
  createProvider2 as createProvider,
2534
4691
  createNodeFileOperations,
2535
4692
  createContextBuilder,
4693
+ createChatAgentAdapter,
4694
+ createAiSdkBundleAdapter,
2536
4695
  WorkspaceContext,
4696
+ ToolResultRenderer,
4697
+ ThinkingLevelPicker,
2537
4698
  StreamMessageContract,
2538
4699
  SendMessageOutputModel,
2539
4700
  SendMessageInputModel,
@@ -2555,10 +4716,14 @@ export {
2555
4716
  ContextIndicator,
2556
4717
  ContextBuilder,
2557
4718
  CodePreview,
4719
+ ChatWithSidebar,
4720
+ ChatWithExport,
4721
+ ChatSidebar,
2558
4722
  ChatMessageModel,
2559
4723
  ChatMessage as ChatMessageComponent,
2560
4724
  ChatMessage,
2561
4725
  ChatInput,
4726
+ ChatExportToolbar,
2562
4727
  ChatErrorEvent,
2563
4728
  ChatConversationModel,
2564
4729
  ChatContainer,