@contractspec/module.ai-chat 4.0.3 → 4.1.2

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 +1143 -21
  5. package/dist/browser/index.js +2813 -631
  6. package/dist/browser/presentation/components/index.js +3160 -358
  7. package/dist/browser/presentation/hooks/index.js +978 -43
  8. package/dist/browser/presentation/index.js +2801 -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 +1143 -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 +2813 -631
  30. package/dist/node/core/index.js +1143 -21
  31. package/dist/node/index.js +2813 -631
  32. package/dist/node/presentation/components/index.js +3160 -358
  33. package/dist/node/presentation/hooks/index.js +978 -43
  34. package/dist/node/presentation/index.js +2804 -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 +3160 -358
  45. package/dist/presentation/hooks/index.d.ts +2 -0
  46. package/dist/presentation/hooks/index.js +978 -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 +2804 -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: "border-border bg-background/50 mt-2 rounded-md border 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: "border-border bg-background/50 mt-2 rounded-md border 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" });
1339
1893
  }
1340
- const content = /* @__PURE__ */ jsxDEV6("div", {
1341
- className: cn6("flex items-center gap-2", active ? "text-foreground" : "text-muted-foreground", className),
1894
+ if (diff < 604800000) {
1895
+ return d.toLocaleDateString([], { weekday: "short" });
1896
+ }
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
+ });
1471
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
+ });
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";
@@ -1554,11 +2171,65 @@ class InMemoryConversationStore {
1554
2171
  if (options?.status) {
1555
2172
  results = results.filter((c) => c.status === options.status);
1556
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
+ }
1557
2181
  results.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
1558
2182
  const offset = options?.offset ?? 0;
1559
2183
  const limit = options?.limit ?? 100;
1560
2184
  return results.slice(offset, offset + limit);
1561
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
+ }
1562
2233
  async search(query, limit = 20) {
1563
2234
  const lowerQuery = query.toLowerCase();
1564
2235
  const results = [];
@@ -1571,20 +2242,471 @@ class InMemoryConversationStore {
1571
2242
  if (hasMatch) {
1572
2243
  results.push(conversation);
1573
2244
  }
1574
- if (results.length >= limit)
1575
- break;
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 {
2555
+ validatePatchProposal
2556
+ } from "@contractspec/lib.surface-runtime/spec/validate-surface-patch";
2557
+ import { buildSurfacePatchProposal } from "@contractspec/lib.surface-runtime/runtime/planner-tools";
2558
+ var VALID_OPS = [
2559
+ "insert-node",
2560
+ "replace-node",
2561
+ "remove-node",
2562
+ "move-node",
2563
+ "resize-panel",
2564
+ "set-layout",
2565
+ "reveal-field",
2566
+ "hide-field",
2567
+ "promote-action",
2568
+ "set-focus"
2569
+ ];
2570
+ var DEFAULT_NODE_KINDS = [
2571
+ "entity-section",
2572
+ "entity-card",
2573
+ "data-view",
2574
+ "assistant-panel",
2575
+ "chat-thread",
2576
+ "action-bar",
2577
+ "timeline",
2578
+ "table",
2579
+ "rich-doc",
2580
+ "form",
2581
+ "chart",
2582
+ "custom-widget"
2583
+ ];
2584
+ function collectSlotIdsFromRegion(node) {
2585
+ const ids = [];
2586
+ if (node.type === "slot") {
2587
+ ids.push(node.slotId);
2588
+ }
2589
+ if (node.type === "panel-group" || node.type === "stack") {
2590
+ for (const child of node.children) {
2591
+ ids.push(...collectSlotIdsFromRegion(child));
1576
2592
  }
1577
- return results;
1578
2593
  }
1579
- clear() {
1580
- this.conversations.clear();
2594
+ if (node.type === "tabs") {
2595
+ for (const tab of node.tabs) {
2596
+ ids.push(...collectSlotIdsFromRegion(tab.child));
2597
+ }
1581
2598
  }
2599
+ if (node.type === "floating") {
2600
+ ids.push(node.anchorSlotId);
2601
+ ids.push(...collectSlotIdsFromRegion(node.child));
2602
+ }
2603
+ return ids;
1582
2604
  }
1583
- function createInMemoryConversationStore() {
1584
- return new InMemoryConversationStore;
2605
+ function deriveConstraints(plan) {
2606
+ const slotIds = collectSlotIdsFromRegion(plan.layoutRoot);
2607
+ const uniqueSlots = [...new Set(slotIds)];
2608
+ return {
2609
+ allowedOps: VALID_OPS,
2610
+ allowedSlots: uniqueSlots.length > 0 ? uniqueSlots : ["assistant", "primary"],
2611
+ allowedNodeKinds: DEFAULT_NODE_KINDS
2612
+ };
2613
+ }
2614
+ var ProposePatchInputSchema = z3.object({
2615
+ proposalId: z3.string().describe("Unique proposal identifier"),
2616
+ ops: z3.array(z3.object({
2617
+ op: z3.enum([
2618
+ "insert-node",
2619
+ "replace-node",
2620
+ "remove-node",
2621
+ "move-node",
2622
+ "resize-panel",
2623
+ "set-layout",
2624
+ "reveal-field",
2625
+ "hide-field",
2626
+ "promote-action",
2627
+ "set-focus"
2628
+ ]),
2629
+ slotId: z3.string().optional(),
2630
+ nodeId: z3.string().optional(),
2631
+ toSlotId: z3.string().optional(),
2632
+ index: z3.number().optional(),
2633
+ node: z3.object({
2634
+ nodeId: z3.string(),
2635
+ kind: z3.string(),
2636
+ title: z3.string().optional(),
2637
+ props: z3.record(z3.string(), z3.unknown()).optional(),
2638
+ children: z3.array(z3.unknown()).optional()
2639
+ }).optional(),
2640
+ persistKey: z3.string().optional(),
2641
+ sizes: z3.array(z3.number()).optional(),
2642
+ layoutId: z3.string().optional(),
2643
+ fieldId: z3.string().optional(),
2644
+ actionId: z3.string().optional(),
2645
+ placement: z3.enum(["header", "inline", "context", "assistant"]).optional(),
2646
+ targetId: z3.string().optional()
2647
+ }))
2648
+ });
2649
+ function createSurfacePlannerTools(config) {
2650
+ const { plan, constraints, onPatchProposal } = config;
2651
+ const resolvedConstraints = constraints ?? deriveConstraints(plan);
2652
+ const proposePatchTool = tool3({
2653
+ description: "Propose surface patches (layout changes, node insertions, etc.) for user approval. " + "Only use allowed ops, slots, and node kinds from the planner context.",
2654
+ inputSchema: ProposePatchInputSchema,
2655
+ execute: async (input) => {
2656
+ const ops = input.ops;
2657
+ try {
2658
+ validatePatchProposal(ops, resolvedConstraints);
2659
+ const proposal = buildSurfacePatchProposal(input.proposalId, ops);
2660
+ onPatchProposal?.(proposal);
2661
+ return {
2662
+ success: true,
2663
+ proposalId: proposal.proposalId,
2664
+ opsCount: proposal.ops.length,
2665
+ message: "Patch proposal validated; awaiting user approval"
2666
+ };
2667
+ } catch (err) {
2668
+ return {
2669
+ success: false,
2670
+ error: err instanceof Error ? err.message : String(err),
2671
+ proposalId: input.proposalId
2672
+ };
2673
+ }
2674
+ }
2675
+ });
2676
+ return {
2677
+ "propose-patch": proposePatchTool
2678
+ };
2679
+ }
2680
+ function buildPlannerPromptInput(plan) {
2681
+ const constraints = deriveConstraints(plan);
2682
+ return {
2683
+ bundleMeta: {
2684
+ key: plan.bundleKey,
2685
+ version: "0.0.0",
2686
+ title: plan.bundleKey
2687
+ },
2688
+ surfaceId: plan.surfaceId,
2689
+ allowedPatchOps: constraints.allowedOps,
2690
+ allowedSlots: [...constraints.allowedSlots],
2691
+ allowedNodeKinds: [...constraints.allowedNodeKinds],
2692
+ actions: plan.actions.map((a) => ({
2693
+ actionId: a.actionId,
2694
+ title: a.title
2695
+ })),
2696
+ preferences: {
2697
+ guidance: "hints",
2698
+ density: "standard",
2699
+ dataDepth: "detailed",
2700
+ control: "standard",
2701
+ media: "text",
2702
+ pace: "balanced",
2703
+ narrative: "top-down"
2704
+ }
2705
+ };
1585
2706
  }
1586
2707
 
1587
2708
  // src/core/chat-service.ts
2709
+ import { compilePlannerPrompt } from "@contractspec/lib.surface-runtime/runtime/planner-prompt";
1588
2710
  var DEFAULT_SYSTEM_PROMPT = `You are ContractSpec AI, an expert coding assistant specialized in ContractSpec development.
1589
2711
 
1590
2712
  Your capabilities:
@@ -1599,6 +2721,9 @@ Guidelines:
1599
2721
  - Reference relevant ContractSpec concepts and patterns
1600
2722
  - Ask clarifying questions when the user's intent is unclear
1601
2723
  - When suggesting code changes, explain the rationale`;
2724
+ var WORKFLOW_TOOLS_PROMPT = `
2725
+
2726
+ 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
2727
 
1603
2728
  class ChatService {
1604
2729
  provider;
@@ -1608,19 +2733,93 @@ class ChatService {
1608
2733
  maxHistoryMessages;
1609
2734
  onUsage;
1610
2735
  tools;
2736
+ thinkingLevel;
1611
2737
  sendReasoning;
1612
2738
  sendSources;
2739
+ modelSelector;
1613
2740
  constructor(config) {
1614
2741
  this.provider = config.provider;
1615
2742
  this.context = config.context;
1616
2743
  this.store = config.store ?? new InMemoryConversationStore;
1617
- this.systemPrompt = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
2744
+ this.systemPrompt = this.buildSystemPrompt(config);
1618
2745
  this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
1619
2746
  this.onUsage = config.onUsage;
1620
- this.tools = config.tools;
1621
- this.sendReasoning = config.sendReasoning ?? false;
2747
+ this.tools = this.mergeTools(config);
2748
+ this.thinkingLevel = config.thinkingLevel;
2749
+ this.modelSelector = config.modelSelector;
2750
+ this.sendReasoning = config.sendReasoning ?? (config.thinkingLevel != null && config.thinkingLevel !== "instant");
1622
2751
  this.sendSources = config.sendSources ?? false;
1623
2752
  }
2753
+ buildSystemPrompt(config) {
2754
+ let base = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
2755
+ if (config.workflowToolsConfig?.baseWorkflows?.length) {
2756
+ base += WORKFLOW_TOOLS_PROMPT;
2757
+ }
2758
+ const contractsPrompt = buildContractsContextPrompt(config.contractsContext ?? {});
2759
+ if (contractsPrompt) {
2760
+ base += contractsPrompt;
2761
+ }
2762
+ if (config.surfacePlanConfig?.plan) {
2763
+ const plannerInput = buildPlannerPromptInput(config.surfacePlanConfig.plan);
2764
+ base += `
2765
+
2766
+ ` + compilePlannerPrompt(plannerInput);
2767
+ }
2768
+ return base;
2769
+ }
2770
+ mergeTools(config) {
2771
+ let merged = config.tools ?? {};
2772
+ const wfConfig = config.workflowToolsConfig;
2773
+ if (wfConfig?.baseWorkflows?.length) {
2774
+ const workflowTools = createWorkflowTools({
2775
+ baseWorkflows: wfConfig.baseWorkflows,
2776
+ composer: wfConfig.composer
2777
+ });
2778
+ merged = { ...merged, ...workflowTools };
2779
+ }
2780
+ const contractsCtx = config.contractsContext;
2781
+ if (contractsCtx?.agentSpecs?.length) {
2782
+ const allTools = [];
2783
+ for (const agent of contractsCtx.agentSpecs) {
2784
+ if (agent.tools?.length)
2785
+ allTools.push(...agent.tools);
2786
+ }
2787
+ if (allTools.length > 0) {
2788
+ const agentTools = agentToolConfigsToToolSet(allTools);
2789
+ merged = { ...merged, ...agentTools };
2790
+ }
2791
+ }
2792
+ const surfaceConfig = config.surfacePlanConfig;
2793
+ if (surfaceConfig?.plan) {
2794
+ const plannerTools = createSurfacePlannerTools({
2795
+ plan: surfaceConfig.plan,
2796
+ onPatchProposal: surfaceConfig.onPatchProposal
2797
+ });
2798
+ merged = { ...merged, ...plannerTools };
2799
+ }
2800
+ if (config.mcpTools && Object.keys(config.mcpTools).length > 0) {
2801
+ merged = { ...merged, ...config.mcpTools };
2802
+ }
2803
+ return Object.keys(merged).length > 0 ? merged : undefined;
2804
+ }
2805
+ async resolveModel() {
2806
+ if (this.modelSelector) {
2807
+ const dimension = this.thinkingLevelToDimension(this.thinkingLevel);
2808
+ const { model, selection } = await this.modelSelector.selectAndCreate({
2809
+ taskDimension: dimension
2810
+ });
2811
+ return { model, providerName: selection.providerKey };
2812
+ }
2813
+ return {
2814
+ model: this.provider.getModel(),
2815
+ providerName: this.provider.name
2816
+ };
2817
+ }
2818
+ thinkingLevelToDimension(level) {
2819
+ if (!level || level === "instant")
2820
+ return "latency";
2821
+ return "reasoning";
2822
+ }
1624
2823
  async send(options) {
1625
2824
  let conversation;
1626
2825
  if (options.conversationId) {
@@ -1638,20 +2837,25 @@ class ChatService {
1638
2837
  workspacePath: this.context?.workspacePath
1639
2838
  });
1640
2839
  }
1641
- await this.store.appendMessage(conversation.id, {
1642
- role: "user",
1643
- content: options.content,
1644
- status: "completed",
1645
- attachments: options.attachments
1646
- });
2840
+ if (!options.skipUserAppend) {
2841
+ await this.store.appendMessage(conversation.id, {
2842
+ role: "user",
2843
+ content: options.content,
2844
+ status: "completed",
2845
+ attachments: options.attachments
2846
+ });
2847
+ }
2848
+ conversation = await this.store.get(conversation.id) ?? conversation;
1647
2849
  const messages = this.buildMessages(conversation, options);
1648
- const model = this.provider.getModel();
2850
+ const { model, providerName } = await this.resolveModel();
2851
+ const providerOptions = getProviderOptions(this.thinkingLevel, providerName);
1649
2852
  try {
1650
2853
  const result = await generateText({
1651
2854
  model,
1652
2855
  messages,
1653
2856
  system: this.systemPrompt,
1654
- tools: this.tools
2857
+ tools: this.tools,
2858
+ providerOptions: Object.keys(providerOptions).length > 0 ? providerOptions : undefined
1655
2859
  });
1656
2860
  const assistantMessage = await this.store.appendMessage(conversation.id, {
1657
2861
  role: "assistant",
@@ -1696,23 +2900,27 @@ class ChatService {
1696
2900
  workspacePath: this.context?.workspacePath
1697
2901
  });
1698
2902
  }
1699
- await this.store.appendMessage(conversation.id, {
1700
- role: "user",
1701
- content: options.content,
1702
- status: "completed",
1703
- attachments: options.attachments
1704
- });
2903
+ if (!options.skipUserAppend) {
2904
+ await this.store.appendMessage(conversation.id, {
2905
+ role: "user",
2906
+ content: options.content,
2907
+ status: "completed",
2908
+ attachments: options.attachments
2909
+ });
2910
+ }
2911
+ conversation = await this.store.get(conversation.id) ?? conversation;
1705
2912
  const assistantMessage = await this.store.appendMessage(conversation.id, {
1706
2913
  role: "assistant",
1707
2914
  content: "",
1708
2915
  status: "streaming"
1709
2916
  });
1710
2917
  const messages = this.buildMessages(conversation, options);
1711
- const model = this.provider.getModel();
2918
+ const { model, providerName } = await this.resolveModel();
1712
2919
  const systemPrompt = this.systemPrompt;
1713
2920
  const tools = this.tools;
1714
2921
  const store = this.store;
1715
2922
  const onUsage = this.onUsage;
2923
+ const streamProviderOptions = getProviderOptions(this.thinkingLevel, providerName);
1716
2924
  async function* streamGenerator() {
1717
2925
  let fullContent = "";
1718
2926
  let fullReasoning = "";
@@ -1723,7 +2931,8 @@ class ChatService {
1723
2931
  model,
1724
2932
  messages,
1725
2933
  system: systemPrompt,
1726
- tools
2934
+ tools,
2935
+ providerOptions: Object.keys(streamProviderOptions).length > 0 ? streamProviderOptions : undefined
1727
2936
  });
1728
2937
  for await (const part of result.fullStream) {
1729
2938
  if (part.type === "text-delta") {
@@ -1838,6 +3047,18 @@ class ChatService {
1838
3047
  ...options
1839
3048
  });
1840
3049
  }
3050
+ async updateConversation(conversationId, updates) {
3051
+ return this.store.update(conversationId, updates);
3052
+ }
3053
+ async forkConversation(conversationId, upToMessageId) {
3054
+ return this.store.fork(conversationId, upToMessageId);
3055
+ }
3056
+ async updateMessage(conversationId, messageId, updates) {
3057
+ return this.store.updateMessage(conversationId, messageId, updates);
3058
+ }
3059
+ async truncateAfter(conversationId, messageId) {
3060
+ return this.store.truncateAfter(conversationId, messageId);
3061
+ }
1841
3062
  async deleteConversation(conversationId) {
1842
3063
  return this.store.delete(conversationId);
1843
3064
  }
@@ -1908,9 +3129,9 @@ import {
1908
3129
  function toolsToToolSet(defs) {
1909
3130
  const result = {};
1910
3131
  for (const def of defs) {
1911
- result[def.name] = tool({
3132
+ result[def.name] = tool4({
1912
3133
  description: def.description ?? def.name,
1913
- inputSchema: z.object({}).passthrough(),
3134
+ inputSchema: z4.object({}).passthrough(),
1914
3135
  execute: async () => ({})
1915
3136
  });
1916
3137
  }
@@ -1924,22 +3145,64 @@ function useChat(options = {}) {
1924
3145
  apiKey,
1925
3146
  proxyUrl,
1926
3147
  conversationId: initialConversationId,
3148
+ store,
1927
3149
  systemPrompt,
1928
3150
  streaming = true,
1929
3151
  onSend,
1930
3152
  onResponse,
1931
3153
  onError,
1932
3154
  onUsage,
1933
- tools: toolsDefs
3155
+ tools: toolsDefs,
3156
+ thinkingLevel,
3157
+ workflowToolsConfig,
3158
+ modelSelector,
3159
+ contractsContext,
3160
+ surfacePlanConfig,
3161
+ mcpServers,
3162
+ agentMode
1934
3163
  } = 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(() => {
3164
+ const [messages, setMessages] = React11.useState([]);
3165
+ const [mcpTools, setMcpTools] = React11.useState(null);
3166
+ const mcpCleanupRef = React11.useRef(null);
3167
+ const [conversation, setConversation] = React11.useState(null);
3168
+ const [isLoading, setIsLoading] = React11.useState(false);
3169
+ const [error, setError] = React11.useState(null);
3170
+ const [conversationId, setConversationId] = React11.useState(initialConversationId ?? null);
3171
+ const abortControllerRef = React11.useRef(null);
3172
+ const chatServiceRef = React11.useRef(null);
3173
+ React11.useEffect(() => {
3174
+ if (!mcpServers?.length) {
3175
+ setMcpTools(null);
3176
+ return;
3177
+ }
3178
+ let cancelled = false;
3179
+ import("@contractspec/lib.ai-agent/tools/mcp-client").then(({ createMcpToolsets }) => {
3180
+ createMcpToolsets(mcpServers).then(({ tools, cleanup }) => {
3181
+ if (!cancelled) {
3182
+ setMcpTools(tools);
3183
+ mcpCleanupRef.current = cleanup;
3184
+ } else {
3185
+ cleanup().catch(() => {
3186
+ return;
3187
+ });
3188
+ }
3189
+ }).catch(() => {
3190
+ if (!cancelled)
3191
+ setMcpTools(null);
3192
+ });
3193
+ });
3194
+ return () => {
3195
+ cancelled = true;
3196
+ const cleanup = mcpCleanupRef.current;
3197
+ mcpCleanupRef.current = null;
3198
+ if (cleanup)
3199
+ cleanup().catch(() => {
3200
+ return;
3201
+ });
3202
+ setMcpTools(null);
3203
+ };
3204
+ }, [mcpServers]);
3205
+ React11.useEffect(() => {
1943
3206
  const chatProvider = createProvider({
1944
3207
  provider,
1945
3208
  model,
@@ -1948,9 +3211,16 @@ function useChat(options = {}) {
1948
3211
  });
1949
3212
  chatServiceRef.current = new ChatService({
1950
3213
  provider: chatProvider,
3214
+ store,
1951
3215
  systemPrompt,
1952
3216
  onUsage,
1953
- tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined
3217
+ tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined,
3218
+ thinkingLevel,
3219
+ workflowToolsConfig,
3220
+ modelSelector,
3221
+ contractsContext,
3222
+ surfacePlanConfig,
3223
+ mcpTools
1954
3224
  });
1955
3225
  }, [
1956
3226
  provider,
@@ -1958,11 +3228,18 @@ function useChat(options = {}) {
1958
3228
  model,
1959
3229
  apiKey,
1960
3230
  proxyUrl,
3231
+ store,
1961
3232
  systemPrompt,
1962
3233
  onUsage,
1963
- toolsDefs
3234
+ toolsDefs,
3235
+ thinkingLevel,
3236
+ workflowToolsConfig,
3237
+ modelSelector,
3238
+ contractsContext,
3239
+ surfacePlanConfig,
3240
+ mcpTools
1964
3241
  ]);
1965
- React6.useEffect(() => {
3242
+ React11.useEffect(() => {
1966
3243
  if (!conversationId || !chatServiceRef.current)
1967
3244
  return;
1968
3245
  const loadConversation = async () => {
@@ -1976,7 +3253,90 @@ function useChat(options = {}) {
1976
3253
  };
1977
3254
  loadConversation().catch(console.error);
1978
3255
  }, [conversationId]);
1979
- const sendMessage = React6.useCallback(async (content, attachments) => {
3256
+ const sendMessage = React11.useCallback(async (content, attachments, opts) => {
3257
+ if (agentMode?.agent) {
3258
+ setIsLoading(true);
3259
+ setError(null);
3260
+ abortControllerRef.current = new AbortController;
3261
+ try {
3262
+ if (!opts?.skipUserAppend) {
3263
+ const userMessage = {
3264
+ id: `msg_${Date.now()}`,
3265
+ conversationId: conversationId ?? "",
3266
+ role: "user",
3267
+ content,
3268
+ status: "completed",
3269
+ createdAt: new Date,
3270
+ updatedAt: new Date,
3271
+ attachments
3272
+ };
3273
+ setMessages((prev) => [...prev, userMessage]);
3274
+ onSend?.(userMessage);
3275
+ }
3276
+ const result = await agentMode.agent.generate({
3277
+ prompt: content,
3278
+ signal: abortControllerRef.current.signal
3279
+ });
3280
+ const toolCallsMap = new Map;
3281
+ for (const tc of result.toolCalls ?? []) {
3282
+ const tr = result.toolResults?.find((r) => r.toolCallId === tc.toolCallId);
3283
+ toolCallsMap.set(tc.toolCallId, {
3284
+ id: tc.toolCallId,
3285
+ name: tc.toolName,
3286
+ args: tc.args ?? {},
3287
+ result: tr?.output,
3288
+ status: "completed"
3289
+ });
3290
+ }
3291
+ const assistantMessage = {
3292
+ id: `msg_${Date.now()}_a`,
3293
+ conversationId: conversationId ?? "",
3294
+ role: "assistant",
3295
+ content: result.text,
3296
+ status: "completed",
3297
+ createdAt: new Date,
3298
+ updatedAt: new Date,
3299
+ toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined,
3300
+ usage: result.usage
3301
+ };
3302
+ setMessages((prev) => [...prev, assistantMessage]);
3303
+ onResponse?.(assistantMessage);
3304
+ onUsage?.(result.usage ?? { inputTokens: 0, outputTokens: 0 });
3305
+ if (store && !conversationId) {
3306
+ const conv = await store.create({
3307
+ status: "active",
3308
+ provider: "agent",
3309
+ model: "agent",
3310
+ messages: []
3311
+ });
3312
+ if (!opts?.skipUserAppend) {
3313
+ await store.appendMessage(conv.id, {
3314
+ role: "user",
3315
+ content,
3316
+ status: "completed",
3317
+ attachments
3318
+ });
3319
+ }
3320
+ await store.appendMessage(conv.id, {
3321
+ role: "assistant",
3322
+ content: result.text,
3323
+ status: "completed",
3324
+ toolCalls: assistantMessage.toolCalls,
3325
+ usage: result.usage
3326
+ });
3327
+ const updated = await store.get(conv.id);
3328
+ if (updated)
3329
+ setConversation(updated);
3330
+ setConversationId(conv.id);
3331
+ }
3332
+ } catch (err) {
3333
+ setError(err instanceof Error ? err : new Error(String(err)));
3334
+ onError?.(err instanceof Error ? err : new Error(String(err)));
3335
+ } finally {
3336
+ setIsLoading(false);
3337
+ }
3338
+ return;
3339
+ }
1980
3340
  if (!chatServiceRef.current) {
1981
3341
  throw new Error("Chat service not initialized");
1982
3342
  }
@@ -1984,25 +3344,28 @@ function useChat(options = {}) {
1984
3344
  setError(null);
1985
3345
  abortControllerRef.current = new AbortController;
1986
3346
  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);
3347
+ if (!opts?.skipUserAppend) {
3348
+ const userMessage = {
3349
+ id: `msg_${Date.now()}`,
3350
+ conversationId: conversationId ?? "",
3351
+ role: "user",
3352
+ content,
3353
+ status: "completed",
3354
+ createdAt: new Date,
3355
+ updatedAt: new Date,
3356
+ attachments
3357
+ };
3358
+ setMessages((prev) => [...prev, userMessage]);
3359
+ onSend?.(userMessage);
3360
+ }
1999
3361
  if (streaming) {
2000
3362
  const result = await chatServiceRef.current.stream({
2001
3363
  conversationId: conversationId ?? undefined,
2002
3364
  content,
2003
- attachments
3365
+ attachments,
3366
+ skipUserAppend: opts?.skipUserAppend
2004
3367
  });
2005
- if (!conversationId) {
3368
+ if (!conversationId && !opts?.skipUserAppend) {
2006
3369
  setConversationId(result.conversationId);
2007
3370
  }
2008
3371
  const assistantMessage = {
@@ -2083,7 +3446,8 @@ function useChat(options = {}) {
2083
3446
  const result = await chatServiceRef.current.send({
2084
3447
  conversationId: conversationId ?? undefined,
2085
3448
  content,
2086
- attachments
3449
+ attachments,
3450
+ skipUserAppend: opts?.skipUserAppend
2087
3451
  });
2088
3452
  setConversation(result.conversation);
2089
3453
  setMessages(result.conversation.messages);
@@ -2100,14 +3464,24 @@ function useChat(options = {}) {
2100
3464
  setIsLoading(false);
2101
3465
  abortControllerRef.current = null;
2102
3466
  }
2103
- }, [conversationId, streaming, onSend, onResponse, onError, messages]);
2104
- const clearConversation = React6.useCallback(() => {
3467
+ }, [
3468
+ conversationId,
3469
+ streaming,
3470
+ onSend,
3471
+ onResponse,
3472
+ onError,
3473
+ onUsage,
3474
+ messages,
3475
+ agentMode,
3476
+ store
3477
+ ]);
3478
+ const clearConversation = React11.useCallback(() => {
2105
3479
  setMessages([]);
2106
3480
  setConversation(null);
2107
3481
  setConversationId(null);
2108
3482
  setError(null);
2109
3483
  }, []);
2110
- const regenerate = React6.useCallback(async () => {
3484
+ const regenerate = React11.useCallback(async () => {
2111
3485
  const lastUserMessageIndex = messages.findLastIndex((m) => m.role === "user");
2112
3486
  if (lastUserMessageIndex === -1)
2113
3487
  return;
@@ -2117,11 +3491,51 @@ function useChat(options = {}) {
2117
3491
  setMessages((prev) => prev.slice(0, lastUserMessageIndex + 1));
2118
3492
  await sendMessage(lastUserMessage.content, lastUserMessage.attachments);
2119
3493
  }, [messages, sendMessage]);
2120
- const stop = React6.useCallback(() => {
3494
+ const stop = React11.useCallback(() => {
2121
3495
  abortControllerRef.current?.abort();
2122
3496
  setIsLoading(false);
2123
3497
  }, []);
2124
- const addToolApprovalResponse = React6.useCallback((_toolCallId, _result) => {
3498
+ const createNewConversation = clearConversation;
3499
+ const editMessage = React11.useCallback(async (messageId, newContent) => {
3500
+ if (!chatServiceRef.current || !conversationId)
3501
+ return;
3502
+ const msg = messages.find((m) => m.id === messageId);
3503
+ if (!msg || msg.role !== "user")
3504
+ return;
3505
+ await chatServiceRef.current.updateMessage(conversationId, messageId, {
3506
+ content: newContent
3507
+ });
3508
+ const truncated = await chatServiceRef.current.truncateAfter(conversationId, messageId);
3509
+ if (truncated) {
3510
+ setMessages(truncated.messages);
3511
+ }
3512
+ await sendMessage(newContent, undefined, { skipUserAppend: true });
3513
+ }, [conversationId, messages, sendMessage]);
3514
+ const forkConversation = React11.useCallback(async (upToMessageId) => {
3515
+ if (!chatServiceRef.current)
3516
+ return null;
3517
+ const idToFork = conversationId ?? conversation?.id;
3518
+ if (!idToFork)
3519
+ return null;
3520
+ try {
3521
+ const forked = await chatServiceRef.current.forkConversation(idToFork, upToMessageId);
3522
+ setConversationId(forked.id);
3523
+ setConversation(forked);
3524
+ setMessages(forked.messages);
3525
+ return forked.id;
3526
+ } catch {
3527
+ return null;
3528
+ }
3529
+ }, [conversationId, conversation]);
3530
+ const updateConversationFn = React11.useCallback(async (updates) => {
3531
+ if (!chatServiceRef.current || !conversationId)
3532
+ return null;
3533
+ const updated = await chatServiceRef.current.updateConversation(conversationId, updates);
3534
+ if (updated)
3535
+ setConversation(updated);
3536
+ return updated;
3537
+ }, [conversationId]);
3538
+ const addToolApprovalResponse = React11.useCallback((_toolCallId, _result) => {
2125
3539
  throw new Error(`addToolApprovalResponse: Tool approval requires server route with toUIMessageStreamResponse. ` + `Use createChatRoute and @ai-sdk/react useChat for tools with requireApproval.`);
2126
3540
  }, []);
2127
3541
  const hasApprovalTools = toolsDefs?.some((t) => t.requireApproval) ?? false;
@@ -2135,20 +3549,731 @@ function useChat(options = {}) {
2135
3549
  setConversationId,
2136
3550
  regenerate,
2137
3551
  stop,
3552
+ createNewConversation,
3553
+ editMessage,
3554
+ forkConversation,
3555
+ updateConversation: updateConversationFn,
2138
3556
  ...hasApprovalTools && { addToolApprovalResponse }
2139
3557
  };
2140
3558
  }
3559
+
3560
+ // src/core/local-storage-conversation-store.ts
3561
+ var DEFAULT_KEY = "contractspec:ai-chat:conversations";
3562
+ function generateId2(prefix) {
3563
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
3564
+ }
3565
+ function toSerializable(conv) {
3566
+ return {
3567
+ ...conv,
3568
+ createdAt: conv.createdAt.toISOString(),
3569
+ updatedAt: conv.updatedAt.toISOString(),
3570
+ messages: conv.messages.map((m) => ({
3571
+ ...m,
3572
+ createdAt: m.createdAt.toISOString(),
3573
+ updatedAt: m.updatedAt.toISOString()
3574
+ }))
3575
+ };
3576
+ }
3577
+ function fromSerializable(raw) {
3578
+ const messages = raw.messages?.map((m) => ({
3579
+ ...m,
3580
+ createdAt: new Date(m.createdAt),
3581
+ updatedAt: new Date(m.updatedAt)
3582
+ })) ?? [];
3583
+ return {
3584
+ ...raw,
3585
+ createdAt: new Date(raw.createdAt),
3586
+ updatedAt: new Date(raw.updatedAt),
3587
+ messages
3588
+ };
3589
+ }
3590
+ function loadAll(key) {
3591
+ if (typeof window === "undefined")
3592
+ return new Map;
3593
+ try {
3594
+ const raw = window.localStorage.getItem(key);
3595
+ if (!raw)
3596
+ return new Map;
3597
+ const arr = JSON.parse(raw);
3598
+ const map = new Map;
3599
+ for (const item of arr) {
3600
+ const conv = fromSerializable(item);
3601
+ map.set(conv.id, conv);
3602
+ }
3603
+ return map;
3604
+ } catch {
3605
+ return new Map;
3606
+ }
3607
+ }
3608
+ function saveAll(key, map) {
3609
+ if (typeof window === "undefined")
3610
+ return;
3611
+ try {
3612
+ const arr = Array.from(map.values()).map(toSerializable);
3613
+ window.localStorage.setItem(key, JSON.stringify(arr));
3614
+ } catch {}
3615
+ }
3616
+
3617
+ class LocalStorageConversationStore {
3618
+ key;
3619
+ cache = null;
3620
+ constructor(storageKey = DEFAULT_KEY) {
3621
+ this.key = storageKey;
3622
+ }
3623
+ getMap() {
3624
+ if (!this.cache) {
3625
+ this.cache = loadAll(this.key);
3626
+ }
3627
+ return this.cache;
3628
+ }
3629
+ persist() {
3630
+ saveAll(this.key, this.getMap());
3631
+ }
3632
+ async get(conversationId) {
3633
+ return this.getMap().get(conversationId) ?? null;
3634
+ }
3635
+ async create(conversation) {
3636
+ const now = new Date;
3637
+ const full = {
3638
+ ...conversation,
3639
+ id: generateId2("conv"),
3640
+ createdAt: now,
3641
+ updatedAt: now
3642
+ };
3643
+ this.getMap().set(full.id, full);
3644
+ this.persist();
3645
+ return full;
3646
+ }
3647
+ async update(conversationId, updates) {
3648
+ const conv = this.getMap().get(conversationId);
3649
+ if (!conv)
3650
+ return null;
3651
+ const updated = {
3652
+ ...conv,
3653
+ ...updates,
3654
+ updatedAt: new Date
3655
+ };
3656
+ this.getMap().set(conversationId, updated);
3657
+ this.persist();
3658
+ return updated;
3659
+ }
3660
+ async appendMessage(conversationId, message) {
3661
+ const conv = this.getMap().get(conversationId);
3662
+ if (!conv)
3663
+ throw new Error(`Conversation ${conversationId} not found`);
3664
+ const now = new Date;
3665
+ const fullMessage = {
3666
+ ...message,
3667
+ id: generateId2("msg"),
3668
+ conversationId,
3669
+ createdAt: now,
3670
+ updatedAt: now
3671
+ };
3672
+ conv.messages.push(fullMessage);
3673
+ conv.updatedAt = now;
3674
+ this.persist();
3675
+ return fullMessage;
3676
+ }
3677
+ async updateMessage(conversationId, messageId, updates) {
3678
+ const conv = this.getMap().get(conversationId);
3679
+ if (!conv)
3680
+ return null;
3681
+ const idx = conv.messages.findIndex((m) => m.id === messageId);
3682
+ if (idx === -1)
3683
+ return null;
3684
+ const msg = conv.messages[idx];
3685
+ if (!msg)
3686
+ return null;
3687
+ const updated = {
3688
+ ...msg,
3689
+ ...updates,
3690
+ updatedAt: new Date
3691
+ };
3692
+ conv.messages[idx] = updated;
3693
+ conv.updatedAt = new Date;
3694
+ this.persist();
3695
+ return updated;
3696
+ }
3697
+ async delete(conversationId) {
3698
+ const deleted = this.getMap().delete(conversationId);
3699
+ if (deleted)
3700
+ this.persist();
3701
+ return deleted;
3702
+ }
3703
+ async list(options) {
3704
+ let results = Array.from(this.getMap().values());
3705
+ if (options?.status) {
3706
+ results = results.filter((c) => c.status === options.status);
3707
+ }
3708
+ if (options?.projectId) {
3709
+ results = results.filter((c) => c.projectId === options.projectId);
3710
+ }
3711
+ if (options?.tags && options.tags.length > 0) {
3712
+ const tagSet = new Set(options.tags);
3713
+ results = results.filter((c) => c.tags && c.tags.some((t) => tagSet.has(t)));
3714
+ }
3715
+ results.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
3716
+ const offset = options?.offset ?? 0;
3717
+ const limit = options?.limit ?? 100;
3718
+ return results.slice(offset, offset + limit);
3719
+ }
3720
+ async fork(conversationId, upToMessageId) {
3721
+ const source = this.getMap().get(conversationId);
3722
+ if (!source)
3723
+ throw new Error(`Conversation ${conversationId} not found`);
3724
+ let messagesToCopy = source.messages;
3725
+ if (upToMessageId) {
3726
+ const idx = source.messages.findIndex((m) => m.id === upToMessageId);
3727
+ if (idx === -1)
3728
+ throw new Error(`Message ${upToMessageId} not found`);
3729
+ messagesToCopy = source.messages.slice(0, idx + 1);
3730
+ }
3731
+ const now = new Date;
3732
+ const forkedMessages = messagesToCopy.map((m) => ({
3733
+ ...m,
3734
+ id: generateId2("msg"),
3735
+ conversationId: "",
3736
+ createdAt: new Date(m.createdAt),
3737
+ updatedAt: new Date(m.updatedAt)
3738
+ }));
3739
+ const forked = {
3740
+ ...source,
3741
+ id: generateId2("conv"),
3742
+ title: source.title ? `${source.title} (fork)` : undefined,
3743
+ forkedFromId: source.id,
3744
+ createdAt: now,
3745
+ updatedAt: now,
3746
+ messages: forkedMessages
3747
+ };
3748
+ for (const m of forked.messages) {
3749
+ m.conversationId = forked.id;
3750
+ }
3751
+ this.getMap().set(forked.id, forked);
3752
+ this.persist();
3753
+ return forked;
3754
+ }
3755
+ async truncateAfter(conversationId, messageId) {
3756
+ const conv = this.getMap().get(conversationId);
3757
+ if (!conv)
3758
+ return null;
3759
+ const idx = conv.messages.findIndex((m) => m.id === messageId);
3760
+ if (idx === -1)
3761
+ return null;
3762
+ conv.messages = conv.messages.slice(0, idx + 1);
3763
+ conv.updatedAt = new Date;
3764
+ this.persist();
3765
+ return conv;
3766
+ }
3767
+ async search(query, limit = 20) {
3768
+ const lowerQuery = query.toLowerCase();
3769
+ const results = [];
3770
+ for (const conv of this.getMap().values()) {
3771
+ if (conv.title?.toLowerCase().includes(lowerQuery)) {
3772
+ results.push(conv);
3773
+ continue;
3774
+ }
3775
+ if (conv.messages.some((m) => m.content.toLowerCase().includes(lowerQuery))) {
3776
+ results.push(conv);
3777
+ }
3778
+ if (results.length >= limit)
3779
+ break;
3780
+ }
3781
+ return results;
3782
+ }
3783
+ }
3784
+ function createLocalStorageConversationStore(storageKey) {
3785
+ return new LocalStorageConversationStore(storageKey);
3786
+ }
3787
+
3788
+ // src/presentation/components/ChatWithSidebar.tsx
3789
+ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
3790
+ "use client";
3791
+ var defaultStore = createLocalStorageConversationStore();
3792
+ function ChatWithSidebar({
3793
+ store = defaultStore,
3794
+ projectId,
3795
+ tags,
3796
+ className,
3797
+ thinkingLevel: initialThinkingLevel = "thinking",
3798
+ presentationRenderer,
3799
+ formRenderer,
3800
+ ...useChatOptions
3801
+ }) {
3802
+ const effectiveStore = store;
3803
+ const [thinkingLevel, setThinkingLevel] = React12.useState(initialThinkingLevel);
3804
+ const chat = useChat({
3805
+ ...useChatOptions,
3806
+ store: effectiveStore,
3807
+ thinkingLevel
3808
+ });
3809
+ const {
3810
+ messages,
3811
+ conversation,
3812
+ sendMessage,
3813
+ isLoading,
3814
+ setConversationId,
3815
+ createNewConversation,
3816
+ editMessage,
3817
+ forkConversation,
3818
+ updateConversation
3819
+ } = chat;
3820
+ const selectedConversationId = conversation?.id ?? null;
3821
+ const handleSelectConversation = React12.useCallback((id) => {
3822
+ setConversationId(id);
3823
+ }, [setConversationId]);
3824
+ return /* @__PURE__ */ jsxs10("div", {
3825
+ className: className ?? "flex h-full w-full",
3826
+ children: [
3827
+ /* @__PURE__ */ jsx10(ChatSidebar, {
3828
+ store: effectiveStore,
3829
+ selectedConversationId,
3830
+ onSelectConversation: handleSelectConversation,
3831
+ onCreateNew: createNewConversation,
3832
+ projectId,
3833
+ tags,
3834
+ selectedConversation: conversation,
3835
+ onUpdateConversation: updateConversation ? async (id, updates) => {
3836
+ if (id === selectedConversationId) {
3837
+ await updateConversation(updates);
3838
+ }
3839
+ } : undefined
3840
+ }),
3841
+ /* @__PURE__ */ jsx10("div", {
3842
+ className: "flex min-w-0 flex-1 flex-col",
3843
+ children: /* @__PURE__ */ jsx10(ChatWithExport, {
3844
+ messages,
3845
+ conversation,
3846
+ onCreateNew: createNewConversation,
3847
+ onFork: forkConversation,
3848
+ onEditMessage: editMessage,
3849
+ thinkingLevel,
3850
+ onThinkingLevelChange: setThinkingLevel,
3851
+ presentationRenderer,
3852
+ formRenderer,
3853
+ children: /* @__PURE__ */ jsx10(ChatInput, {
3854
+ onSend: (content, att) => sendMessage(content, att),
3855
+ disabled: isLoading,
3856
+ isLoading
3857
+ })
3858
+ })
3859
+ })
3860
+ ]
3861
+ });
3862
+ }
3863
+ // src/presentation/components/ModelPicker.tsx
3864
+ import * as React13 from "react";
3865
+ import { cn as cn7 } from "@contractspec/lib.ui-kit-web/ui/utils";
3866
+ import { Button as Button6 } from "@contractspec/lib.design-system";
3867
+ import {
3868
+ Select as Select2,
3869
+ SelectContent as SelectContent2,
3870
+ SelectItem as SelectItem2,
3871
+ SelectTrigger as SelectTrigger2,
3872
+ SelectValue as SelectValue2
3873
+ } from "@contractspec/lib.ui-kit-web/ui/select";
3874
+ import { Badge } from "@contractspec/lib.ui-kit-web/ui/badge";
3875
+ import { Label as Label2 } from "@contractspec/lib.ui-kit-web/ui/label";
3876
+ import { Bot as Bot2, Cloud, Cpu, Sparkles } from "lucide-react";
3877
+ import {
3878
+ getModelsForProvider
3879
+ } from "@contractspec/lib.ai-providers";
3880
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
3881
+ "use client";
3882
+ var PROVIDER_ICONS = {
3883
+ ollama: /* @__PURE__ */ jsx11(Cpu, {
3884
+ className: "h-4 w-4"
3885
+ }),
3886
+ openai: /* @__PURE__ */ jsx11(Bot2, {
3887
+ className: "h-4 w-4"
3888
+ }),
3889
+ anthropic: /* @__PURE__ */ jsx11(Sparkles, {
3890
+ className: "h-4 w-4"
3891
+ }),
3892
+ mistral: /* @__PURE__ */ jsx11(Cloud, {
3893
+ className: "h-4 w-4"
3894
+ }),
3895
+ gemini: /* @__PURE__ */ jsx11(Sparkles, {
3896
+ className: "h-4 w-4"
3897
+ })
3898
+ };
3899
+ var PROVIDER_NAMES = {
3900
+ ollama: "Ollama (Local)",
3901
+ openai: "OpenAI",
3902
+ anthropic: "Anthropic",
3903
+ mistral: "Mistral",
3904
+ gemini: "Google Gemini"
3905
+ };
3906
+ var MODE_BADGES = {
3907
+ local: { label: "Local", variant: "secondary" },
3908
+ byok: { label: "BYOK", variant: "outline" },
3909
+ managed: { label: "Managed", variant: "default" }
3910
+ };
3911
+ function ModelPicker({
3912
+ value,
3913
+ onChange,
3914
+ availableProviders,
3915
+ className,
3916
+ compact = false
3917
+ }) {
3918
+ const providers = availableProviders ?? [
3919
+ { provider: "ollama", available: true, mode: "local" },
3920
+ { provider: "openai", available: true, mode: "byok" },
3921
+ { provider: "anthropic", available: true, mode: "byok" },
3922
+ { provider: "mistral", available: true, mode: "byok" },
3923
+ { provider: "gemini", available: true, mode: "byok" }
3924
+ ];
3925
+ const models = getModelsForProvider(value.provider);
3926
+ const selectedModel = models.find((m) => m.id === value.model);
3927
+ const handleProviderChange = React13.useCallback((providerName) => {
3928
+ const provider = providerName;
3929
+ const providerInfo = providers.find((p) => p.provider === provider);
3930
+ const providerModels = getModelsForProvider(provider);
3931
+ const defaultModel = providerModels[0]?.id ?? "";
3932
+ onChange({
3933
+ provider,
3934
+ model: defaultModel,
3935
+ mode: providerInfo?.mode ?? "byok"
3936
+ });
3937
+ }, [onChange, providers]);
3938
+ const handleModelChange = React13.useCallback((modelId) => {
3939
+ onChange({
3940
+ ...value,
3941
+ model: modelId
3942
+ });
3943
+ }, [onChange, value]);
3944
+ if (compact) {
3945
+ return /* @__PURE__ */ jsxs11("div", {
3946
+ className: cn7("flex items-center gap-2", className),
3947
+ children: [
3948
+ /* @__PURE__ */ jsxs11(Select2, {
3949
+ value: value.provider,
3950
+ onValueChange: handleProviderChange,
3951
+ children: [
3952
+ /* @__PURE__ */ jsx11(SelectTrigger2, {
3953
+ className: "w-[140px]",
3954
+ children: /* @__PURE__ */ jsx11(SelectValue2, {})
3955
+ }),
3956
+ /* @__PURE__ */ jsx11(SelectContent2, {
3957
+ children: providers.map((p) => /* @__PURE__ */ jsx11(SelectItem2, {
3958
+ value: p.provider,
3959
+ disabled: !p.available,
3960
+ children: /* @__PURE__ */ jsxs11("div", {
3961
+ className: "flex items-center gap-2",
3962
+ children: [
3963
+ PROVIDER_ICONS[p.provider],
3964
+ /* @__PURE__ */ jsx11("span", {
3965
+ children: PROVIDER_NAMES[p.provider]
3966
+ })
3967
+ ]
3968
+ })
3969
+ }, p.provider))
3970
+ })
3971
+ ]
3972
+ }),
3973
+ /* @__PURE__ */ jsxs11(Select2, {
3974
+ value: value.model,
3975
+ onValueChange: handleModelChange,
3976
+ children: [
3977
+ /* @__PURE__ */ jsx11(SelectTrigger2, {
3978
+ className: "w-[160px]",
3979
+ children: /* @__PURE__ */ jsx11(SelectValue2, {})
3980
+ }),
3981
+ /* @__PURE__ */ jsx11(SelectContent2, {
3982
+ children: models.map((m) => /* @__PURE__ */ jsx11(SelectItem2, {
3983
+ value: m.id,
3984
+ children: m.name
3985
+ }, m.id))
3986
+ })
3987
+ ]
3988
+ })
3989
+ ]
3990
+ });
3991
+ }
3992
+ return /* @__PURE__ */ jsxs11("div", {
3993
+ className: cn7("flex flex-col gap-3", className),
3994
+ children: [
3995
+ /* @__PURE__ */ jsxs11("div", {
3996
+ className: "flex flex-col gap-1.5",
3997
+ children: [
3998
+ /* @__PURE__ */ jsx11(Label2, {
3999
+ htmlFor: "provider-selection",
4000
+ className: "text-sm font-medium",
4001
+ children: "Provider"
4002
+ }),
4003
+ /* @__PURE__ */ jsx11("div", {
4004
+ className: "flex flex-wrap gap-2",
4005
+ id: "provider-selection",
4006
+ children: providers.map((p) => /* @__PURE__ */ jsxs11(Button6, {
4007
+ variant: value.provider === p.provider ? "default" : "outline",
4008
+ size: "sm",
4009
+ onPress: () => p.available && handleProviderChange(p.provider),
4010
+ disabled: !p.available,
4011
+ className: cn7(!p.available && "opacity-50"),
4012
+ children: [
4013
+ PROVIDER_ICONS[p.provider],
4014
+ /* @__PURE__ */ jsx11("span", {
4015
+ children: PROVIDER_NAMES[p.provider]
4016
+ }),
4017
+ /* @__PURE__ */ jsx11(Badge, {
4018
+ variant: MODE_BADGES[p.mode].variant,
4019
+ className: "ml-1",
4020
+ children: MODE_BADGES[p.mode].label
4021
+ })
4022
+ ]
4023
+ }, p.provider))
4024
+ })
4025
+ ]
4026
+ }),
4027
+ /* @__PURE__ */ jsxs11("div", {
4028
+ className: "flex flex-col gap-1.5",
4029
+ children: [
4030
+ /* @__PURE__ */ jsx11(Label2, {
4031
+ htmlFor: "model-picker",
4032
+ className: "text-sm font-medium",
4033
+ children: "Model"
4034
+ }),
4035
+ /* @__PURE__ */ jsxs11(Select2, {
4036
+ name: "model-picker",
4037
+ value: value.model,
4038
+ onValueChange: handleModelChange,
4039
+ children: [
4040
+ /* @__PURE__ */ jsx11(SelectTrigger2, {
4041
+ children: /* @__PURE__ */ jsx11(SelectValue2, {
4042
+ placeholder: "Select a model"
4043
+ })
4044
+ }),
4045
+ /* @__PURE__ */ jsx11(SelectContent2, {
4046
+ children: models.map((m) => /* @__PURE__ */ jsx11(SelectItem2, {
4047
+ value: m.id,
4048
+ children: /* @__PURE__ */ jsxs11("div", {
4049
+ className: "flex items-center gap-2",
4050
+ children: [
4051
+ /* @__PURE__ */ jsx11("span", {
4052
+ children: m.name
4053
+ }),
4054
+ /* @__PURE__ */ jsxs11("span", {
4055
+ className: "text-muted-foreground text-xs",
4056
+ children: [
4057
+ Math.round(m.contextWindow / 1000),
4058
+ "K"
4059
+ ]
4060
+ }),
4061
+ m.capabilities.vision && /* @__PURE__ */ jsx11(Badge, {
4062
+ variant: "outline",
4063
+ className: "text-xs",
4064
+ children: "Vision"
4065
+ }),
4066
+ m.capabilities.reasoning && /* @__PURE__ */ jsx11(Badge, {
4067
+ variant: "outline",
4068
+ className: "text-xs",
4069
+ children: "Reasoning"
4070
+ })
4071
+ ]
4072
+ })
4073
+ }, m.id))
4074
+ })
4075
+ ]
4076
+ })
4077
+ ]
4078
+ }),
4079
+ selectedModel && /* @__PURE__ */ jsxs11("div", {
4080
+ className: "text-muted-foreground flex flex-wrap gap-2 text-xs",
4081
+ children: [
4082
+ /* @__PURE__ */ jsxs11("span", {
4083
+ children: [
4084
+ "Context: ",
4085
+ Math.round(selectedModel.contextWindow / 1000),
4086
+ "K tokens"
4087
+ ]
4088
+ }),
4089
+ selectedModel.capabilities.vision && /* @__PURE__ */ jsx11("span", {
4090
+ children: "• Vision"
4091
+ }),
4092
+ selectedModel.capabilities.tools && /* @__PURE__ */ jsx11("span", {
4093
+ children: "• Tools"
4094
+ }),
4095
+ selectedModel.capabilities.reasoning && /* @__PURE__ */ jsx11("span", {
4096
+ children: "• Reasoning"
4097
+ })
4098
+ ]
4099
+ })
4100
+ ]
4101
+ });
4102
+ }
4103
+ // src/presentation/components/ContextIndicator.tsx
4104
+ import { cn as cn8 } from "@contractspec/lib.ui-kit-web/ui/utils";
4105
+ import { Badge as Badge2 } from "@contractspec/lib.ui-kit-web/ui/badge";
4106
+ import {
4107
+ Tooltip,
4108
+ TooltipContent,
4109
+ TooltipProvider,
4110
+ TooltipTrigger
4111
+ } from "@contractspec/lib.ui-kit-web/ui/tooltip";
4112
+ import { FolderOpen, FileCode, Zap, Info } from "lucide-react";
4113
+ import { jsx as jsx12, jsxs as jsxs12, Fragment as Fragment6 } from "react/jsx-runtime";
4114
+ "use client";
4115
+ function ContextIndicator({
4116
+ summary,
4117
+ active = false,
4118
+ className,
4119
+ showDetails = true
4120
+ }) {
4121
+ if (!summary && !active) {
4122
+ return /* @__PURE__ */ jsxs12("div", {
4123
+ className: cn8("flex items-center gap-1.5 text-sm", "text-muted-foreground", className),
4124
+ children: [
4125
+ /* @__PURE__ */ jsx12(Info, {
4126
+ className: "h-4 w-4"
4127
+ }),
4128
+ /* @__PURE__ */ jsx12("span", {
4129
+ children: "No workspace context"
4130
+ })
4131
+ ]
4132
+ });
4133
+ }
4134
+ const content = /* @__PURE__ */ jsxs12("div", {
4135
+ className: cn8("flex items-center gap-2", active ? "text-foreground" : "text-muted-foreground", className),
4136
+ children: [
4137
+ /* @__PURE__ */ jsxs12(Badge2, {
4138
+ variant: active ? "default" : "secondary",
4139
+ className: "flex items-center gap-1",
4140
+ children: [
4141
+ /* @__PURE__ */ jsx12(Zap, {
4142
+ className: "h-3 w-3"
4143
+ }),
4144
+ "Context"
4145
+ ]
4146
+ }),
4147
+ summary && showDetails && /* @__PURE__ */ jsxs12(Fragment6, {
4148
+ children: [
4149
+ /* @__PURE__ */ jsxs12("div", {
4150
+ className: "flex items-center gap-1 text-xs",
4151
+ children: [
4152
+ /* @__PURE__ */ jsx12(FolderOpen, {
4153
+ className: "h-3.5 w-3.5"
4154
+ }),
4155
+ /* @__PURE__ */ jsx12("span", {
4156
+ children: summary.name
4157
+ })
4158
+ ]
4159
+ }),
4160
+ /* @__PURE__ */ jsxs12("div", {
4161
+ className: "flex items-center gap-1 text-xs",
4162
+ children: [
4163
+ /* @__PURE__ */ jsx12(FileCode, {
4164
+ className: "h-3.5 w-3.5"
4165
+ }),
4166
+ /* @__PURE__ */ jsxs12("span", {
4167
+ children: [
4168
+ summary.specs.total,
4169
+ " specs"
4170
+ ]
4171
+ })
4172
+ ]
4173
+ })
4174
+ ]
4175
+ })
4176
+ ]
4177
+ });
4178
+ if (!summary) {
4179
+ return content;
4180
+ }
4181
+ return /* @__PURE__ */ jsx12(TooltipProvider, {
4182
+ children: /* @__PURE__ */ jsxs12(Tooltip, {
4183
+ children: [
4184
+ /* @__PURE__ */ jsx12(TooltipTrigger, {
4185
+ asChild: true,
4186
+ children: content
4187
+ }),
4188
+ /* @__PURE__ */ jsx12(TooltipContent, {
4189
+ side: "bottom",
4190
+ className: "max-w-[300px]",
4191
+ children: /* @__PURE__ */ jsxs12("div", {
4192
+ className: "flex flex-col gap-2 text-sm",
4193
+ children: [
4194
+ /* @__PURE__ */ jsx12("div", {
4195
+ className: "font-medium",
4196
+ children: summary.name
4197
+ }),
4198
+ /* @__PURE__ */ jsx12("div", {
4199
+ className: "text-muted-foreground text-xs",
4200
+ children: summary.path
4201
+ }),
4202
+ /* @__PURE__ */ jsx12("div", {
4203
+ className: "border-t pt-2",
4204
+ children: /* @__PURE__ */ jsxs12("div", {
4205
+ className: "grid grid-cols-2 gap-1 text-xs",
4206
+ children: [
4207
+ /* @__PURE__ */ jsx12("span", {
4208
+ children: "Commands:"
4209
+ }),
4210
+ /* @__PURE__ */ jsx12("span", {
4211
+ className: "text-right",
4212
+ children: summary.specs.commands
4213
+ }),
4214
+ /* @__PURE__ */ jsx12("span", {
4215
+ children: "Queries:"
4216
+ }),
4217
+ /* @__PURE__ */ jsx12("span", {
4218
+ className: "text-right",
4219
+ children: summary.specs.queries
4220
+ }),
4221
+ /* @__PURE__ */ jsx12("span", {
4222
+ children: "Events:"
4223
+ }),
4224
+ /* @__PURE__ */ jsx12("span", {
4225
+ className: "text-right",
4226
+ children: summary.specs.events
4227
+ }),
4228
+ /* @__PURE__ */ jsx12("span", {
4229
+ children: "Presentations:"
4230
+ }),
4231
+ /* @__PURE__ */ jsx12("span", {
4232
+ className: "text-right",
4233
+ children: summary.specs.presentations
4234
+ })
4235
+ ]
4236
+ })
4237
+ }),
4238
+ /* @__PURE__ */ jsxs12("div", {
4239
+ className: "border-t pt-2 text-xs",
4240
+ children: [
4241
+ /* @__PURE__ */ jsxs12("span", {
4242
+ children: [
4243
+ summary.files.total,
4244
+ " files"
4245
+ ]
4246
+ }),
4247
+ /* @__PURE__ */ jsx12("span", {
4248
+ className: "mx-1",
4249
+ children: "•"
4250
+ }),
4251
+ /* @__PURE__ */ jsxs12("span", {
4252
+ children: [
4253
+ summary.files.specFiles,
4254
+ " spec files"
4255
+ ]
4256
+ })
4257
+ ]
4258
+ })
4259
+ ]
4260
+ })
4261
+ })
4262
+ ]
4263
+ })
4264
+ });
4265
+ }
2141
4266
  // src/presentation/hooks/useProviders.tsx
2142
- import * as React7 from "react";
4267
+ import * as React14 from "react";
2143
4268
  import {
2144
4269
  getAvailableProviders,
2145
4270
  getModelsForProvider as getModelsForProvider2
2146
4271
  } from "@contractspec/lib.ai-providers";
2147
4272
  "use client";
2148
4273
  function useProviders() {
2149
- const [providers, setProviders] = React7.useState([]);
2150
- const [isLoading, setIsLoading] = React7.useState(true);
2151
- const loadProviders = React7.useCallback(async () => {
4274
+ const [providers, setProviders] = React14.useState([]);
4275
+ const [isLoading, setIsLoading] = React14.useState(true);
4276
+ const loadProviders = React14.useCallback(async () => {
2152
4277
  setIsLoading(true);
2153
4278
  try {
2154
4279
  const available = getAvailableProviders();
@@ -2163,12 +4288,12 @@ function useProviders() {
2163
4288
  setIsLoading(false);
2164
4289
  }
2165
4290
  }, []);
2166
- React7.useEffect(() => {
4291
+ React14.useEffect(() => {
2167
4292
  loadProviders();
2168
4293
  }, [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]);
4294
+ const availableProviders = React14.useMemo(() => providers.filter((p) => p.available), [providers]);
4295
+ const isAvailable = React14.useCallback((provider) => providers.some((p) => p.provider === provider && p.available), [providers]);
4296
+ const getModelsCallback = React14.useCallback((provider) => providers.find((p) => p.provider === provider)?.models ?? [], [providers]);
2172
4297
  return {
2173
4298
  providers,
2174
4299
  availableProviders,
@@ -2512,15 +4637,64 @@ var ChatErrorEvent = defineEvent({
2512
4637
  }
2513
4638
  })
2514
4639
  });
4640
+ // src/adapters/ai-sdk-bundle-adapter.ts
4641
+ import { compilePlannerPrompt as compilePlannerPrompt2 } from "@contractspec/lib.surface-runtime/runtime/planner-prompt";
4642
+ function createAiSdkBundleAdapter(deps) {
4643
+ const { provider, onPatchProposal } = deps;
4644
+ return {
4645
+ startThread(_args) {
4646
+ return null;
4647
+ },
4648
+ async requestPatches(args) {
4649
+ const proposals = [];
4650
+ const captureProposal = (p) => {
4651
+ proposals.push(p);
4652
+ onPatchProposal?.(p);
4653
+ };
4654
+ const plannerInput = buildPlannerPromptInput(args.currentPlan);
4655
+ const systemPrompt = compilePlannerPrompt2(plannerInput);
4656
+ const service = new ChatService({
4657
+ provider,
4658
+ systemPrompt,
4659
+ surfacePlanConfig: {
4660
+ plan: args.currentPlan,
4661
+ onPatchProposal: captureProposal
4662
+ }
4663
+ });
4664
+ await service.send({
4665
+ content: args.userMessage
4666
+ });
4667
+ return proposals;
4668
+ }
4669
+ };
4670
+ }
4671
+ // src/core/agent-adapter.ts
4672
+ function createChatAgentAdapter(agent) {
4673
+ return {
4674
+ async generate({ prompt, signal }) {
4675
+ const result = await agent.generate({ prompt, signal });
4676
+ return {
4677
+ text: result.text,
4678
+ toolCalls: result.toolCalls,
4679
+ toolResults: result.toolResults,
4680
+ usage: result.usage
4681
+ };
4682
+ }
4683
+ };
4684
+ }
2515
4685
  export {
2516
4686
  validateProvider,
2517
4687
  useProviders,
4688
+ useMessageSelection,
4689
+ useConversations,
2518
4690
  useCompletion,
2519
4691
  useChat,
2520
4692
  supportsLocalMode,
2521
4693
  listOllamaModels,
2522
4694
  isStudioAvailable,
4695
+ isPresentationToolResult,
2523
4696
  isOllamaRunning,
4697
+ isFormToolResult,
2524
4698
  hasCredentials,
2525
4699
  getRecommendedModels,
2526
4700
  getModelsForProvider3 as getModelsForProvider,
@@ -2533,7 +4707,11 @@ export {
2533
4707
  createProvider2 as createProvider,
2534
4708
  createNodeFileOperations,
2535
4709
  createContextBuilder,
4710
+ createChatAgentAdapter,
4711
+ createAiSdkBundleAdapter,
2536
4712
  WorkspaceContext,
4713
+ ToolResultRenderer,
4714
+ ThinkingLevelPicker,
2537
4715
  StreamMessageContract,
2538
4716
  SendMessageOutputModel,
2539
4717
  SendMessageInputModel,
@@ -2555,10 +4733,14 @@ export {
2555
4733
  ContextIndicator,
2556
4734
  ContextBuilder,
2557
4735
  CodePreview,
4736
+ ChatWithSidebar,
4737
+ ChatWithExport,
4738
+ ChatSidebar,
2558
4739
  ChatMessageModel,
2559
4740
  ChatMessage as ChatMessageComponent,
2560
4741
  ChatMessage,
2561
4742
  ChatInput,
4743
+ ChatExportToolbar,
2562
4744
  ChatErrorEvent,
2563
4745
  ChatConversationModel,
2564
4746
  ChatContainer,