@contractspec/module.ai-chat 4.0.2 → 4.1.0

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