@contractspec/module.ai-chat 4.0.3 → 4.1.2

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