diff --git a/.gitignore b/.gitignore index 4225f91e20..929046422e 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,5 @@ opencode.jsonc .vercel .env*.local .claude/* +.security +transcript-analysis \ No newline at end of file diff --git a/apps/app/components.json b/apps/app/components.json index 3781d892e6..989c733406 100644 --- a/apps/app/components.json +++ b/apps/app/components.json @@ -12,6 +12,8 @@ }, "iconLibrary": "lucide", "rtl": true, + "menuColor": "inverted-translucent", + "menuAccent": "subtle", "aliases": { "components": "@/components", "utils": "@/lib/utils", @@ -19,7 +21,7 @@ "lib": "@/lib", "hooks": "@/hooks" }, - "menuColor": "inverted-translucent", - "menuAccent": "subtle", - "registries": {} + "registries": { + "@ai-elements": "https://ai-sdk.dev/elements/api/registry/{name}.json" + } } diff --git a/apps/app/package.json b/apps/app/package.json index 1e5213c53f..76aadeafeb 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -55,6 +55,7 @@ "@openwork/ui": "workspace:*", "@paper-design/shaders-react": "0.0.72", "@radix-ui/colors": "^3.0.0", + "@radix-ui/react-use-controllable-state": "^1.2.2", "@shikijs/transformers": "^4.0.2", "@tanstack/react-query": "^5.90.3", "@tanstack/react-virtual": "^3.13.23", @@ -63,6 +64,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "dompurify": "^3.4.5", "emojilib": "^4.0.3", "fuzzysort": "^3.1.0", "jsonc-parser": "^3.2.1", @@ -74,10 +76,14 @@ "motion": "^12.38.0", "react": "catalog:", "react-dom": "catalog:", + "react-markdown": "^10.1.0", "react-resizable-panels": "^4.11.0", "react-router-dom": "^7.14.1", + "remark-breaks": "^4.0.0", + "remark-gfm": "^4.0.1", "shadcn": "^4.6.0", "shiki": "^4.0.2", + "streamdown": "^2.5.0", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.4.0", "xlsx": "^0.18.5", diff --git a/apps/app/scripts/artifacts.test.ts b/apps/app/scripts/artifacts.test.ts new file mode 100644 index 0000000000..a3ea9ec853 --- /dev/null +++ b/apps/app/scripts/artifacts.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "bun:test"; +import type { UIMessage } from "ai"; + +import type { OpenTarget } from "../src/react-app/domains/session/artifacts/open-target"; +import { getArtifactsFromMessages } from "../src/lib/artifacts"; + +describe("getArtifactsFromMessages", () => { + it("uses verified relative targets for absolute attachment paths", () => { + const messages: UIMessage[] = [{ + id: "msg_attachment", + role: "assistant", + parts: [{ + type: "source-document", + sourceId: "attachment-source", + mediaType: "text/csv", + title: "customers.csv", + filename: "/Users/test/workspace/customers.csv", + }], + }]; + const targets: OpenTarget[] = [{ + id: "file:customers.csv", + kind: "file", + value: "customers.csv", + name: "customers.csv", + preview: "sheet", + confidence: 95, + reason: "attachment source", + exists: true, + }]; + + expect(getArtifactsFromMessages(messages, targets)[0]?.legacy_target).toMatchObject({ + value: "customers.csv", + exists: true, + }); + }); +}); diff --git a/apps/app/scripts/open-target.test.ts b/apps/app/scripts/open-target.test.ts index 6563c6d779..c113482483 100644 --- a/apps/app/scripts/open-target.test.ts +++ b/apps/app/scripts/open-target.test.ts @@ -75,6 +75,41 @@ describe("deriveOpenTargets", () => { expect(targets[0]).toMatchObject({ value: "reports/summary.md", preview: "markdown", confidence: 95 }); }); + it("extracts artifact targets from attachment sources", () => { + const targets = deriveOpenTargets([ + { + id: "msg_attachment", + role: "assistant", + parts: [{ + type: "source-document", + sourceId: "attachment-source", + mediaType: "text/csv", + title: "customers.csv", + filename: "reports/customers.csv", + }], + }, + ]); + + expect(targets[0]).toMatchObject({ value: "reports/customers.csv", preview: "sheet", confidence: 95 }); + }); + + it("keeps URI-backed source documents as URL targets when filename is missing", () => { + const targets = deriveOpenTargets([ + { + id: "msg_source", + role: "assistant", + parts: [{ + type: "source-document", + sourceId: "url-source", + mediaType: "text/html", + title: "https://example.com/docs/report.html", + }], + }, + ]); + + expect(targets[0]).toMatchObject({ kind: "url", value: "https://example.com/docs/report.html", preview: "browser" }); + }); + it("does not extract file artifacts from read tool metadata or output", () => { const targets = deriveOpenTargets([ toolMessage( diff --git a/apps/app/src/app/index.css b/apps/app/src/app/index.css index a2a40a2557..3160ae0f5c 100644 --- a/apps/app/src/app/index.css +++ b/apps/app/src/app/index.css @@ -336,6 +336,31 @@ select:disabled { animation: soft-pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite; } +/* Quick horizontal shake used to nudge the user toward an action (e.g. when + Enter is pressed while a follow-up message needs Steer/Queue selection). */ +@keyframes ow-shake { + 0%, + 100% { + transform: translateX(0); + } + 20% { + transform: translateX(-5px); + } + 40% { + transform: translateX(5px); + } + 60% { + transform: translateX(-3px); + } + 80% { + transform: translateX(3px); + } +} + +@utility animate-shake { + animation: ow-shake 0.4s cubic-bezier(0.36, 0.07, 0.19, 0.97); +} + /* Highlight animation for just-saved command */ @keyframes command-highlight { 0% { @@ -409,7 +434,7 @@ select:disabled { --card-foreground: var(--slate-12); --popover: var(--slate-1); --popover-foreground: var(--slate-12); - --primary: var(--blue-8); + --primary: var(--blue-10); --primary-foreground: var(--slate-12); --secondary: var(--slate-3); --secondary-foreground: var(--slate-12); diff --git a/apps/app/src/components/ai-elements/chain-of-thought.tsx b/apps/app/src/components/ai-elements/chain-of-thought.tsx new file mode 100644 index 0000000000..eca8a9c4b7 --- /dev/null +++ b/apps/app/src/components/ai-elements/chain-of-thought.tsx @@ -0,0 +1,222 @@ +"use client"; + +import { useControllableState } from "@radix-ui/react-use-controllable-state"; +import { Badge } from "@/components/ui/badge"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { cn } from "@/lib/utils"; +import type { LucideIcon } from "lucide-react"; +import { BrainIcon, ChevronDownIcon, DotIcon } from "lucide-react"; +import type { ComponentProps, ReactNode } from "react"; +import { createContext, memo, useContext, useMemo } from "react"; + +interface ChainOfThoughtContextValue { + isOpen: boolean; + setIsOpen: (open: boolean) => void; +} + +const ChainOfThoughtContext = createContext( + null +); + +const useChainOfThought = () => { + const context = useContext(ChainOfThoughtContext); + if (!context) { + throw new Error( + "ChainOfThought components must be used within ChainOfThought" + ); + } + return context; +}; + +export type ChainOfThoughtProps = ComponentProps<"div"> & { + open?: boolean; + defaultOpen?: boolean; + onOpenChange?: (open: boolean) => void; +}; + +export const ChainOfThought = memo( + ({ + className, + open, + defaultOpen = false, + onOpenChange, + children, + ...props + }: ChainOfThoughtProps) => { + const [isOpen, setIsOpen] = useControllableState({ + defaultProp: defaultOpen, + onChange: onOpenChange, + prop: open, + }); + + const chainOfThoughtContext = useMemo( + () => ({ isOpen, setIsOpen }), + [isOpen, setIsOpen] + ); + + return ( + +
+ {children} +
+
+ ); + } +); + +export type ChainOfThoughtHeaderProps = ComponentProps< + typeof CollapsibleTrigger +>; + +export const ChainOfThoughtHeader = memo( + ({ className, children, ...props }: ChainOfThoughtHeaderProps) => { + const { isOpen, setIsOpen } = useChainOfThought(); + + return ( + + + + + {children ?? "Chain of Thought"} + + + + + ); + } +); + +export type ChainOfThoughtStepProps = ComponentProps<"div"> & { + icon?: LucideIcon; + label: ReactNode; + description?: ReactNode; + status?: "complete" | "active" | "pending"; +}; + +const stepStatusStyles = { + active: "text-foreground", + complete: "text-muted-foreground", + pending: "text-muted-foreground/50", +}; + +export const ChainOfThoughtStep = memo( + ({ + className, + icon: Icon = DotIcon, + label, + description, + status = "complete", + children, + ...props + }: ChainOfThoughtStepProps) => ( +
+
+ +
+
+
+
{label}
+ {description && ( +
{description}
+ )} + {children} +
+
+ ) +); + +export type ChainOfThoughtSearchResultsProps = ComponentProps<"div">; + +export const ChainOfThoughtSearchResults = memo( + ({ className, ...props }: ChainOfThoughtSearchResultsProps) => ( +
+ ) +); + +export type ChainOfThoughtSearchResultProps = ComponentProps; + +export const ChainOfThoughtSearchResult = memo( + ({ className, children, ...props }: ChainOfThoughtSearchResultProps) => ( + + {children} + + ) +); + +export type ChainOfThoughtContentProps = ComponentProps< + typeof CollapsibleContent +>; + +export const ChainOfThoughtContent = memo( + ({ className, children, ...props }: ChainOfThoughtContentProps) => { + const { isOpen } = useChainOfThought(); + + return ( + + + {children} + + + ); + } +); + +export type ChainOfThoughtImageProps = ComponentProps<"div"> & { + caption?: string; +}; + +export const ChainOfThoughtImage = memo( + ({ className, children, caption, ...props }: ChainOfThoughtImageProps) => ( +
+
+ {children} +
+ {caption &&

{caption}

} +
+ ) +); + +ChainOfThought.displayName = "ChainOfThought"; +ChainOfThoughtHeader.displayName = "ChainOfThoughtHeader"; +ChainOfThoughtStep.displayName = "ChainOfThoughtStep"; +ChainOfThoughtSearchResults.displayName = "ChainOfThoughtSearchResults"; +ChainOfThoughtSearchResult.displayName = "ChainOfThoughtSearchResult"; +ChainOfThoughtContent.displayName = "ChainOfThoughtContent"; +ChainOfThoughtImage.displayName = "ChainOfThoughtImage"; diff --git a/apps/app/src/components/chat/artifact-icon.tsx b/apps/app/src/components/chat/artifact-icon.tsx new file mode 100644 index 0000000000..284878c0bc --- /dev/null +++ b/apps/app/src/components/chat/artifact-icon.tsx @@ -0,0 +1,54 @@ +/** @jsxImportSource react */ +import { File, FileAudio, FileCode, FileImage, FileSpreadsheet, FileText, FileType, FileVideo, Globe, Presentation } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import type { ArtifactType } from "@/lib/artifacts"; + +interface ArtifactIconProps { + className?: string; + type: ArtifactType; +} + +export function ArtifactIcon({ className, type }: ArtifactIconProps) { + if (type === "website") { + return ; + } + + if (type === "markdown") { + return ; + } + + if (type === "sheet") { + return ; + } + + if (type === "slides") { + return ; + } + + if (type === "image") { + return ; + } + + if (type === "video") { + return ; + } + + if (type === "audio") { + return ; + } + + if (type === "pdf") { + return ; + } + + if (type === "html") { + return ; + } + + if (type === "text") { + return ; + } + + return ; +} diff --git a/apps/app/src/components/chat/artifact.tsx b/apps/app/src/components/chat/artifact.tsx new file mode 100644 index 0000000000..4cd2b2380e --- /dev/null +++ b/apps/app/src/components/chat/artifact.tsx @@ -0,0 +1,67 @@ +/** @jsxImportSource react */ + +import type { UIMessage } from "ai"; +import { ArrowUpRightIcon } from "lucide-react"; + +import { ArtifactIcon } from "@/components/chat/artifact-icon"; +import { + DescriptiveButton, + DescriptiveButtonContent, + DescriptiveButtonDescription, + DescriptiveButtonIcon, + DescriptiveButtonTitle, +} from "@/components/descriptive-button"; +import { + type ArtifactItem, + getArtifactTypeLabel, + useArtifacts, + usePreviewArtifact, +} from "@/lib/artifacts"; + +interface ArtifactButtonProps { + artifact: ArtifactItem +} + +function ArtifactButton({ artifact }: ArtifactButtonProps) { + const previewArtifact = usePreviewArtifact(); + + return ( + previewArtifact(artifact)} + > + + + + + {artifact.name} + + {getArtifactTypeLabel(artifact.type)} + + + + + ); +} + +interface ArtifactListProps { + messages: UIMessage[] +} + +export function ArtifactList({ messages }: ArtifactListProps) { + const artifacts = useArtifacts(messages); + + if (artifacts.length === 0) { + return null; + } + + return ( +
+
+ {artifacts.map((artifact) => ( + + ))} +
+
+ ); +} diff --git a/apps/app/src/components/chat/message-list-provider.tsx b/apps/app/src/components/chat/message-list-provider.tsx new file mode 100644 index 0000000000..13b91371c9 --- /dev/null +++ b/apps/app/src/components/chat/message-list-provider.tsx @@ -0,0 +1,97 @@ +"use memo"; + +import { useSessionActivityStore } from "@/react-app/domains/session/status/session-activity-store" +import * as React from "react" + +interface MessageListContextValue { + workspaceId: string + sessionId: string + showThinking: boolean + developerMode: boolean + displaySuggestions: boolean + dispatchAction: (action: DispatchAction) => void + setPrompt: (prompt: string) => void + onRevertToUserMessage: (messageId: string) => void + onForkAtMessage: (messageId: string) => void +} + +const MessageListContext = React.createContext(null) + +interface MessageListProviderProps { + children: React.ReactNode + workspaceId: string + sessionId: string + showThinking: boolean + developerMode: boolean + onRevertToUserMessage: (messageId: string) => void + onForkAtMessage: (messageId: string) => void + displaySuggestions: boolean + dispatchAction: (action: DispatchAction) => void + setPrompt: (prompt: string) => void +} + +export interface DispatchAction { + target: "settings" + action: "open" + section: "commands" | "skills" | "mcps" | "plugins" +} + +export function MessageListProvider({ + children, + workspaceId, + sessionId, + showThinking, + developerMode, + displaySuggestions, + dispatchAction, + setPrompt, + onRevertToUserMessage, + onForkAtMessage, +}: MessageListProviderProps) { + const value = React.useMemo( + () => ({ + workspaceId, + sessionId, + showThinking, + developerMode, + displaySuggestions, + dispatchAction, + setPrompt, + onRevertToUserMessage, + onForkAtMessage, + }), + [ + workspaceId, + sessionId, + showThinking, + developerMode, + displaySuggestions, + dispatchAction, + setPrompt, + onRevertToUserMessage, + onForkAtMessage, + ], + ) + + return ( + + {children} + + ) +} + +export function useMessageList() { + const context = React.useContext(MessageListContext) + + if (!context) { + throw new Error("useMessageList must be used within a MessageListProvider") + } + + return context +} + +export function useSessionErrorMessage() { + const { workspaceId, sessionId } = useMessageList(); + + return useSessionActivityStore(state => state.getSessionError(workspaceId, sessionId)); +} \ No newline at end of file diff --git a/apps/app/src/components/chat/message-list.tsx b/apps/app/src/components/chat/message-list.tsx new file mode 100644 index 0000000000..d96101f0ff --- /dev/null +++ b/apps/app/src/components/chat/message-list.tsx @@ -0,0 +1,628 @@ +"use memo"; + +import * as React from "react" +import { + AlertTriangle, + Check, + Copy, + FileIcon, + Split, + Undo2, +} from "lucide-react" +import { + AnimatePresence, + LayoutGroup, + motion, +} from "motion/react" +import { PaperGrainGradient } from "@openwork/ui/react" +import { + DynamicToolUIPart, + isFileUIPart, + ToolUIPart, + type FileUIPart, + type UIMessage, +} from "ai" +import { ApplyPatchTool } from "@/components/tools/apply-patch" +import { BashTool } from "@/components/tools/bash" +import { EditTool } from "@/components/tools/edit" +import { ReadFileTool, WriteFileTool } from "@/components/tools/file" +import { GlobTool } from "@/components/tools/glob" +import { GrepTool } from "@/components/tools/grep" +import { LspTool } from "@/components/tools/lsp" +import { QuestionTool } from "@/components/tools/question" +import { SkillTool } from "@/components/tools/skill" +import { TodoWriteTool } from "@/components/tools/todowrite" +import { WebfetchTool } from "@/components/tools/webfetch" +import { WebsearchTool } from "@/components/tools/websearch" +import { useMessageList, useSessionErrorMessage } from "@/components/chat/message-list-provider" +import { ArtifactList } from "@/components/chat/artifact" +import { TaskSuggestions } from "@/components/chat/task-suggestions" +import { + DescriptiveButton, + DescriptiveButtonContent, + DescriptiveButtonDescription, + DescriptiveButtonIcon, + DescriptiveButtonTitle, +} from "@/components/descriptive-button" +import { Button } from "@/components/ui/button" +import { Image } from "@/components/ui/image" +import { + Message, + MessageAction, + MessageActions, + MessageContent, +} from "@/components/ui/message" +import { + Steps, + StepsContent, + StepsTrigger, +} from "@/components/ui/steps" +import { Tool } from "@/components/ui/tool" +import { + isApplyPatchToolPart, + isBashToolPart, + isEditToolPart, + isGlobToolPart, + isGrepToolPart, + isLspToolPart, + isQuestionToolPart, + isReadToolPart, + isSkillToolPart, + isTodoWriteToolPart, + isWebFetchToolPart, + isWebSearchToolPart, + isWriteToolPart, +} from "@/lib/build-in-tools" +import type { ThreadStatus } from "@/lib/messages" +import { cn } from "@/lib/utils" +import { groupMessages, isMessageGroup, getLastTextPart, getAssistantRenderGroups, getFileTitle, getMediaBadge, type UIMessageWithIndex, getMessagesText } from "./utils" + +interface ToolMessageProps { + part: ToolUIPart | DynamicToolUIPart +} + +const ToolMessage = ({ part }: ToolMessageProps) => { + if (isBashToolPart(part)) { + return + } + + if (isEditToolPart(part)) { + return + } + + if (isWriteToolPart(part)) { + return + } + + if (isReadToolPart(part)) { + return + } + + if (isGrepToolPart(part)) { + return + } + + if (isGlobToolPart(part)) { + return + } + + if (isLspToolPart(part)) { + return + } + + if (isApplyPatchToolPart(part)) { + return + } + + if (isSkillToolPart(part)) { + return + } + + if (isTodoWriteToolPart(part)) { + return + } + + if (isWebFetchToolPart(part)) { + return + } + + if (isWebSearchToolPart(part)) { + return + } + + if (isQuestionToolPart(part)) { + return + } + + return +} + +const isEmptyMessage = (message: UIMessage): boolean => message.parts.length === 0 + +interface FileMessageProps { + part: FileUIPart + tone: "assistant" | "user" +} + +function FileMessage({ part, tone }: FileMessageProps) { + const title = getFileTitle(part) + const badge = getMediaBadge(part) + const isImage = part.mediaType.startsWith("image/") && part.url + + if (isImage) { + return ( + {title} + ) + } + + return ( + + + + + + {title} + {badge ? ( + + {badge} + + ) : null} + + + ) +} + +function EmptyMessage({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ Empty message +
+ ) +} + +interface CopyMessageButtonProps { + messages: UIMessage[] +} + +function CopyMessageButton({ messages }: CopyMessageButtonProps) { + const [copied, setCopied] = React.useState(false) + const text = React.useMemo(() => getMessagesText(messages), [messages]) + + const onCopy = React.useCallback(async () => { + if (!text) { + return + } + + try { + await navigator.clipboard.writeText(text) + setCopied(true) + window.setTimeout(() => setCopied(false), 2000) + } catch { + // ignore clipboard failures + } + }, [text]) + + if (!text) { + return null + } + + return ( + + + + ) +} + +type AssistantMessageProps = { + message: UIMessage + isLastMessage: boolean + isStreaming: boolean + isLastStep: boolean +} + +export const AssistantMessage = React.memo( + ({ message }: AssistantMessageProps) => { + const { showThinking } = useMessageList() + const assistantRenderGroups = React.useMemo( + () => getAssistantRenderGroups(message.parts, showThinking), + [message.parts, showThinking] + ) + + return ( + +
+ {assistantRenderGroups.map((group, index) => { + if (group.kind === "text") { + return ( + + {group.text} + + ) + } + + if (group.kind === "reasoning") { + return ( + + {group.text} + + ) + } + + if (group.kind === "file") { + return ( +
+ +
+ ) + } + + return ( +
+ +
+ ) + })} +
+
+ ) + } +) + +AssistantMessage.displayName = "AssistantMessage" + +type UserMessageProps = { + message: UIMessage + isStreaming: boolean +} + +export const UserMessage = React.memo( + ({ message, isStreaming }: UserMessageProps) => { + const { onRevertToUserMessage, onForkAtMessage } = useMessageList() + + return ( + +
+ {message.parts.filter(isFileUIPart).map((part, index) => ( + + ))} + {message.parts.some((part) => part.type === "text" && part.text) ? ( + + {message.parts.map((part) => (part.type === "text" ? part.text : "")).join("")} + + ) : null} + {!isStreaming && ( + + + + + + + + + + )} +
+
+ ) + } +) + +UserMessage.displayName = "UserMessage" + +type MessageComponentProps = { + message: UIMessage + isLastMessage: boolean + isStreaming: boolean + isLastStep: boolean +} + +export const MessageComponent = React.memo( + ({ message, isLastMessage, isStreaming, isLastStep }: MessageComponentProps) => { + if (isEmptyMessage(message) && !isStreaming) { + return ( + + ) + } + + if (message.role === "assistant") { + return ( + + ) + } + + return ( + + ) + } +) + +MessageComponent.displayName = "MessageComponent" + +const LoadingMessage = React.memo(() => ( + +
+
+
+ +
+ Thinking… +
+
+
+)) + +LoadingMessage.displayName = "LoadingMessage" + +interface ErrorMessageProps { + error: string | null +} + +const ErrorMessage = React.memo(({ error }: ErrorMessageProps) => ( + +
+
+ +

{error}

+
+
+
+)) + +ErrorMessage.displayName = "ErrorMessage" + +const isMessageEmptyGroup = (messages: UIMessageWithIndex[]) => + messages.every(message => isEmptyMessage(message.message)); + +const getRenderableMessages = (messages: UIMessageWithIndex[]) => + messages.flatMap((item) => { + const parts = item.message.parts.filter((part) => part.type === "text" || part.type === "file"); + + return parts.length > 0 ? [{ ...item, message: { ...item.message, parts } }] : [] + }) + +interface AssistantMessageGroupProps { + items: UIMessageWithIndex[] + messages: UIMessage[] + isStreaming: boolean +} + +function MessageGroup({ + items, + messages, + isStreaming, +}: AssistantMessageGroupProps) { + const { onRevertToUserMessage, onForkAtMessage } = useMessageList() + const [open, setOpen] = React.useState(false) + // Only run layout animations while the collapsible is expanding/collapsing. + // Otherwise (e.g. while streaming) layout changes apply instantly. + const [isAnimating, setIsAnimating] = React.useState(false) + const layoutTransition = isAnimating + ? { type: "spring" as const, bounce: 0.1, duration: 0.1 } + : { duration: 0 } + + const lastItem = items[items.length - 1] + + if (!lastItem || isMessageEmptyGroup(items)) { + if (isStreaming) { + return null; + } + + return + } + + const renderableItems = getRenderableMessages(items) + const lastTextMessage = getLastTextPart(lastItem.message) + + return ( + +
+ { + setIsAnimating(true) + setOpen(next) + }} + > + + {items.length} steps + + + {items.map((item, groupIndex) => { + const isLastMessage = item.index === messages.length - 1 + const isLastStep = groupIndex === items.length - 1 + + return ( + setIsAnimating(false)} + > + + + ) + })} + + + + {!open ? renderableItems.map(({ index, message }) => ( + setIsAnimating(false)} + > + + + )) : null} + + item.message)} /> + {lastTextMessage && !isStreaming && ( +
+ + item.message)} /> + + + + + + + + {/* item.message)} /> */} +
+ )} + {renderableItems.length === 0 && !isStreaming ? : null} +
+
+ ) +} + +interface MessageListProps { + messages: UIMessage[] + status: ThreadStatus +} + +export function MessageList({ messages, status }: MessageListProps) { + const isStreaming = status === "streaming" || status === "retrying" + const items = React.useMemo(() => groupMessages(messages, status), [messages, status]); + const error = useSessionErrorMessage(); + + return ( +
+ {messages.length === 0 && } + + {items.map((item) => { + if (isMessageGroup(item)) { + return ( + + ) + } + + const isLastMessage = item.index === messages.length - 1 + const isLastStep = + !messages[item.index + 1] || messages[item.index + 1].role !== item.message.role + + return ( +
+ +
+ ) + })} + + {status === "streaming" && } + {error ? : null} +
+ ) +} diff --git a/apps/app/src/components/chat/message-sources.tsx b/apps/app/src/components/chat/message-sources.tsx new file mode 100644 index 0000000000..3ec863b0f8 --- /dev/null +++ b/apps/app/src/components/chat/message-sources.tsx @@ -0,0 +1,121 @@ +"use memo"; + +import * as React from "react" +import type { UIMessage } from "ai" +import { + Source, + SourceContent, + SourceTrigger, +} from "@/components/ui/source" +import { isWebFetchToolPart, isWebSearchToolPart } from "@/lib/build-in-tools" +import { parseWebSearchResults } from "@/lib/websearch-results" + +type MessageSourceItem = { + key: string + href: string + title: string + description?: string + showFavicon: boolean +} + +function getMessageSourceItems(messages: UIMessage[]): MessageSourceItem[] { + const seen = new Set() + const items: MessageSourceItem[] = [] + + for (const message of messages) { + for (const part of message.parts) { + if (part.type === "source-url") { + if (seen.has(part.url)) { + continue + } + + seen.add(part.url) + items.push({ + key: part.sourceId, + href: part.url, + title: part.title ?? part.url, + showFavicon: true, + }) + continue + } + + if (part.type === "source-document") { + if (seen.has(part.sourceId)) { + continue + } + + seen.add(part.sourceId) + items.push({ + key: part.sourceId, + href: part.filename ?? part.title, + title: part.title, + showFavicon: false, + }) + continue + } + + if (part.type === "dynamic-tool" && isWebFetchToolPart(part) && part.state === "output-available") { + const url = part.input.url + if (seen.has(url)) { + continue + } + + seen.add(url) + items.push({ + key: part.toolCallId, + href: url, + title: url, + showFavicon: true, + }) + continue + } + + if (part.type === "dynamic-tool" && isWebSearchToolPart(part) && part.state === "output-available") { + for (const result of parseWebSearchResults(part.output)) { + if (seen.has(result.url)) { + continue + } + + seen.add(result.url) + items.push({ + key: `${part.toolCallId}-${result.url}`, + href: result.url, + title: result.title, + description: result.description, + showFavicon: true, + }) + } + } + } + } + + return items +} + +interface MessageSourcesProps { + messages: UIMessage[] +} + +export const MessageSources = React.memo(({ messages }: MessageSourcesProps) => { + const sources = React.useMemo(() => getMessageSourceItems(messages), [messages]) + + if (sources.length === 0) { + return null + } + + return ( +
+ {sources.map((source) => ( + + + + + ))} +
+ ) +}) + +MessageSources.displayName = "MessageSources" diff --git a/apps/app/src/components/chat/task-suggestions.tsx b/apps/app/src/components/chat/task-suggestions.tsx new file mode 100644 index 0000000000..c5bd4a3d51 --- /dev/null +++ b/apps/app/src/components/chat/task-suggestions.tsx @@ -0,0 +1,76 @@ +"use client" + +import { + DescriptiveButton, + DescriptiveButtonContent, + DescriptiveButtonDescription, + DescriptiveButtonIcon, + DescriptiveButtonTitle, +} from "@/components/descriptive-button" +import { useMessageList } from "@/components/chat/message-list-provider" +import { cn } from "@/lib/utils" +import { CubeIcon, DocumentChartBarIcon, GlobeAltIcon } from "@heroicons/react/24/solid" + +const CSV_PROMPT = + "Create a sample CSV file with 20 rows of fake customer data (name, email, company, revenue). Then show me a summary of the data." + +const BROWSER_PROMPT = + "Open craigslist.org in the browser and search for couches for sale. Show me the top 5 results with prices." + +interface TaskSuggestionsProps { + className?: string +} + +export function TaskSuggestions({ className }: TaskSuggestionsProps) { + const { displaySuggestions, dispatchAction, setPrompt } = useMessageList() + + if (!displaySuggestions) { + return null + } + + return ( +
+

Try one of these:

+
+ setPrompt(CSV_PROMPT)}> + + + + + Edit a CSV + Create a sample spreadsheet + + + + setPrompt(BROWSER_PROMPT)}> + + + + + Browse the web + Search Craigslist for couches + + + + + dispatchAction({ + target: "settings", + action: "open", + section: "mcps", + }) + } + > + + + + + Connect an extension + Add MCPs and integrations + + +
+
+ ) +} diff --git a/apps/app/src/components/chat/utils.ts b/apps/app/src/components/chat/utils.ts new file mode 100644 index 0000000000..bad603bfe8 --- /dev/null +++ b/apps/app/src/components/chat/utils.ts @@ -0,0 +1,156 @@ +import { isReasoningUIPart, isToolUIPart, type DynamicToolUIPart, type FileUIPart, type ToolUIPart, type UIMessage } from "ai" +import type { ThreadStatus } from "@/lib/messages" + +interface MessageGroup { + messages: UIMessageWithIndex[] +} + +export type UIMessageWithIndex = { index: number, message: UIMessage } +export type MessageListItem = MessageGroup | UIMessageWithIndex + +export function getMessageText(message: UIMessage): string { + return message.parts + .filter((part) => part.type === "text") + .map((part) => part.text) + .join("") + .trim() +} + +export function getMessagesText(messages: UIMessage[]): string { + return messages + .map(getMessageText) + .filter(Boolean) + .join("\n\n") +} + +export function getLastTextPart(message: UIMessage): UIMessage | null { + const lastTextPart = message.parts.findLast((part) => part.type === "text") + + return lastTextPart ? { ...message, parts: [lastTextPart] } : null +} + +export function getFileTitle(part: FileUIPart) { + if (part.filename) { + return part.filename + } + + if (part.url.startsWith("data:")) { + return "Attached file" + } + + return part.url || "File" +} + +export function getMediaBadge(part: FileUIPart) { + if (part.mediaType && part.mediaType !== "application/octet-stream") { + return part.mediaType.replace(/^application\//, "").replace(/^text\//, "").toUpperCase() + } + + return part.filename?.split(".").pop()?.toUpperCase() ?? null +} + +export function isMessageGroup(item: MessageListItem): item is MessageGroup { + return "messages" in item +} + +export function isFollowedByUserMessage(messages: UIMessage[], index: number) { + return index < messages.length && messages[index].role === "user" +} + +export function groupMessages(messages: UIMessage[], status: ThreadStatus): MessageListItem[] { + const items: MessageListItem[] = [] + let index = 0 + + while (index < messages.length) { + const message = messages[index] + + if (message.role !== "assistant") { + items.push({ index, message }) + index++ + continue + } + + const assistantMessages: UIMessageWithIndex[] = [] + + while (index < messages.length && messages[index].role === "assistant") { + assistantMessages.push({ message: messages[index], index }); + index++ + } + + items.push({ messages: assistantMessages }); + } + + return items +} + +export type AssistantRenderGroup = + | { kind: "text"; text: string } + | { kind: "reasoning"; text: string; isStreaming: boolean } + | { kind: "file"; part: FileUIPart } + | { kind: "tool"; part: ToolUIPart | DynamicToolUIPart } + +export function getAssistantRenderGroups( + parts: UIMessage["parts"], + showThinking: boolean +): AssistantRenderGroup[] { + const filteredParts = parts.filter((part) => showThinking || !isReasoningUIPart(part)) + const groups: AssistantRenderGroup[] = [] + + const appendText = (text: string) => { + if (!text) { + return + } + + const previous = groups.at(-1) + if (previous?.kind === "text") { + previous.text += text + return + } + + groups.push({ kind: "text", text }) + } + + const appendReasoning = (part: UIMessage["parts"][number]) => { + if (!isReasoningUIPart(part)) { + return + } + + const previous = groups.at(-1) + if (previous?.kind === "reasoning") { + previous.text += part.text + previous.isStreaming = previous.isStreaming || part.state === "streaming" + return + } + + if (!part.text.trim()) { + return + } + + groups.push({ kind: "reasoning", text: part.text, isStreaming: part.state === "streaming" }) + } + + for (const part of filteredParts) { + if (part.type === "text") { + appendText(part.text) + continue + } + + if (isReasoningUIPart(part)) { + if (showThinking) { + appendReasoning(part) + } + continue + } + + if (part.type === "file") { + groups.push({ kind: "file", part }) + continue + } + + if (isToolUIPart(part)) { + groups.push({ kind: "tool", part }) + } + } + + return groups +} diff --git a/apps/app/src/components/descriptive-button.tsx b/apps/app/src/components/descriptive-button.tsx new file mode 100644 index 0000000000..d78e9b4c84 --- /dev/null +++ b/apps/app/src/components/descriptive-button.tsx @@ -0,0 +1,58 @@ +import type * as React from "react"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +type DescriptiveButtonProps = React.ComponentProps & { + orientation?: "horizontal" | "vertical"; +} + +export function DescriptiveButton({ children, className, orientation = "horizontal", ...props }: DescriptiveButtonProps) { + return ( + + ) +} + +type DescriptiveButtonIconProps = React.ComponentProps<"div"> + +export function DescriptiveButtonIcon({ children, className, ...props }: DescriptiveButtonIconProps) { + return ( +
+ {children} +
+ ) +} + +type DescriptiveButtonTitleProps = React.ComponentProps<"span"> + +export function DescriptiveButtonTitle({ children, className, ...props }: DescriptiveButtonTitleProps) { + return ( + {children} + ) +} + +type DescriptiveButtonDescriptionProps = React.ComponentProps<"span"> + +export function DescriptiveButtonDescription({ children, className, ...props }: DescriptiveButtonDescriptionProps) { + return ( + {children} + ) +} + +type DescriptiveButtonContentProps = React.ComponentProps<"div"> + +export function DescriptiveButtonContent({ children, className, ...props }: DescriptiveButtonContentProps) { + return ( +
{children}
+ ) +} \ No newline at end of file diff --git a/apps/app/src/components/markdown/markdown.tsx b/apps/app/src/components/markdown/markdown.tsx new file mode 100644 index 0000000000..4837bd17b4 --- /dev/null +++ b/apps/app/src/components/markdown/markdown.tsx @@ -0,0 +1,335 @@ +/** @jsxImportSource react */ +import { memo, useEffect, useMemo, useRef, useState } from "react"; +import { motion } from "motion/react"; +import DOMPurify from "dompurify"; +import { Marked, type Tokens } from "marked"; +import { markedEmoji } from "marked-emoji"; +import markedShiki from "marked-shiki"; +import emojiKeywords from "emojilib"; +import { + transformerMetaHighlight, + transformerMetaWordHighlight, + transformerNotationDiff, + transformerNotationErrorLevel, + transformerNotationFocus, + transformerNotationHighlight, + transformerNotationWordHighlight, +} from "@shikijs/transformers"; +import { bundledLanguages, codeToHtml } from "shiki"; + +import { cn } from "@/lib/utils"; + +import { applyTextHighlights } from "./text-highlights"; + +function escapeHtml(value: string) { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function escapeAttribute(value: string) { + return escapeHtml(value).replace(/`/g, "`"); +} + +function safeHref(href: string) { + const trimmed = href.trim(); + + if (!trimmed) { + return "#"; + } + + if (trimmed.startsWith("#") || trimmed.startsWith("/") || trimmed.startsWith("./") || trimmed.startsWith("../")) { + return trimmed; + } + + try { + const parsed = new URL(trimmed); + + if (["http:", "https:", "mailto:"].includes(parsed.protocol)) { + return trimmed; + } + } catch { + return "#"; + } + + return "#"; +} + +function alignAttribute(align: Tokens.TableCell["align"]) { + return align ? ` style="text-align: ${align}"` : ""; +} + +function codeLanguageClass(lang: string | undefined) { + const normalized = lang?.trim().split(/\s+/)[0]; + + return normalized ? ` class="language-${escapeAttribute(normalized)}"` : ""; +} + +function createEmojiAliases() { + const aliases: Record = {}; + + for (const [emoji, names] of Object.entries(emojiKeywords)) { + for (const name of names) { + if (!aliases[name]) { + aliases[name] = emoji; + } + } + } + + return aliases; +} + +const emojiAliases = createEmojiAliases(); + +function parseShikiLanguage(lang: string) { + const normalized = lang.trim().split(/\s+/)[0]?.toLowerCase() ?? ""; + return normalized in bundledLanguages ? normalized : "text"; +} + +function hasFencedCodeBlock(text: string) { + return /(^|\n)```/.test(text); +} + +function sanitizeMarkdownHtml(value: string) { + return DOMPurify.sanitize(value, { + ADD_ATTR: [ + "checked", + "class", + "data-openwork-shiki", + "decoding", + "disabled", + "loading", + "rel", + "start", + "style", + "target", + ], + }); +} + +const baseMarkedOptions = { + async: false, + breaks: false, + gfm: true, + pedantic: false, + silent: true, + renderer: { + html({ text }) { + return text; + }, + paragraph({ tokens }) { + return `

${this.parser.parseInline(tokens)}

`; + }, + heading({ tokens, depth }) { + const className = cn( + "font-semibold", + depth === 1 && "my-5 text-xl", + depth === 2 && "my-4 text-lg", + depth >= 3 && "my-3 text-base", + ); + + return `${this.parser.parseInline(tokens)}`; + }, + list(token) { + const tag = token.ordered ? "ol" : "ul"; + const className = cn( + "my-3 pl-6", + token.ordered ? "list-decimal" : "list-disc", + ); + const start = token.ordered && typeof token.start === "number" && token.start !== 1 + ? ` start="${token.start}"` + : ""; + return `<${tag}${start} class="${className}">${token.items.map((item) => this.listitem(item)).join("")}`; + }, + listitem(item) { + const checkbox = item.task + ? ` ` + : ""; + + return `
  • ${checkbox}${this.parser.parse(item.tokens)}
  • `; + }, + blockquote({ tokens }) { + return `
    ${this.parser.parse(tokens)}
    `; + }, + code({ text, lang }) { + return `
    ${escapeHtml(text)}
    `; + }, + codespan({ text }) { + return `${escapeHtml(text)}`; + }, + del({ raw, tokens }) { + if (!raw.startsWith("~~")) { + return escapeHtml(raw); + } + + return `${this.parser.parseInline(tokens)}`; + }, + link({ href, title, tokens }) { + const safe = escapeAttribute(safeHref(href)); + const titleAttr = title ? ` title="${escapeAttribute(title)}"` : ""; + + return `${this.parser.parseInline(tokens)}`; + }, + image({ href, title, text }) { + const safe = escapeAttribute(safeHref(href)); + const titleAttr = title ? ` title="${escapeAttribute(title)}"` : ""; + + return `${escapeAttribute(text)}`; + }, + table(token) { + const header = token.header.map((cell) => this.tablecell({ ...cell, header: true })).join(""); + const body = token.rows.map((row) => this.tablerow({ text: row.map((cell) => this.tablecell(cell)).join("") })).join(""); + + return `${this.tablerow({ text: header })}${body}
    `; + }, + tablerow({ text }) { + return `${text}`; + }, + tablecell({ tokens, header, align }) { + const className = cn( + "border border-border p-2", + header ? "bg-muted text-left" : "align-top", + ); + + if (header) { + return `${this.parser.parseInline(tokens)}`; + } + + return `${this.parser.parseInline(tokens)}`; + }, + hr() { + return `
    `; + }, + }, +} satisfies ConstructorParameters>[0]; + +const markdownParser = new Marked(baseMarkedOptions).use( + markedEmoji({ + emojis: emojiAliases, + renderer: (token) => escapeHtml(token.emoji), + }), +); + +const highlightedMarkdownParser = new Marked({ + ...baseMarkedOptions, + async: true, +}).use( + markedEmoji({ + emojis: emojiAliases, + renderer: (token) => escapeHtml(token.emoji), + }), + markedShiki({ + async highlight(code, lang, props) { + const language = parseShikiLanguage(lang); + + return codeToHtml(code, { + lang: language, + meta: { __raw: props.join(" ") }, + theme: "github-light", + transformers: [ + transformerNotationDiff({ matchAlgorithm: "v3" }), + transformerNotationHighlight({ matchAlgorithm: "v3" }), + transformerNotationWordHighlight({ matchAlgorithm: "v3" }), + transformerNotationFocus({ matchAlgorithm: "v3" }), + transformerNotationErrorLevel({ matchAlgorithm: "v3" }), + transformerMetaHighlight(), + transformerMetaWordHighlight(), + ], + }); + }, + container: `
    %s
    `, + }), +); + +type MarkdownBlockInnerProps = { + className?: string; + text: string; + streaming?: boolean; + highlightQuery?: string; +} & Omit< + React.ComponentProps, + "ref" | "className" | "children" | "dangerouslySetInnerHTML" +>; + +function MarkdownBlockInner({ + className, + text, + streaming, + highlightQuery, + ...props +}: MarkdownBlockInnerProps) { + const rootRef = useRef(null); + const syncHtml = useMemo(() => { + if (!text.trim()) { + return ""; + } + return sanitizeMarkdownHtml(markdownParser.parse(text, { async: false })); + }, [text]); + const [highlightedHtml, setHighlightedHtml] = useState<{ text: string; html: string } | null>(null); + + useEffect(() => { + if (streaming || !hasFencedCodeBlock(text)) { + setHighlightedHtml(null); + return; + } + + let cancelled = false; + void highlightedMarkdownParser.parse(text, { async: true }).then((html) => { + const sanitizedHtml = sanitizeMarkdownHtml(html); + + if (!cancelled && sanitizedHtml.trim()) { + setHighlightedHtml({ text, html: sanitizedHtml }); + } + }).catch(() => { + if (!cancelled) { + setHighlightedHtml(null); + } + }); + return () => { + cancelled = true; + }; + }, [streaming, text]); + + const html = !streaming && highlightedHtml?.text === text ? highlightedHtml.html : syncHtml; + + useEffect(() => { + const root = rootRef.current; + + if (!root) { + return; + } + + queueMicrotask(() => { + if (!rootRef.current || rootRef.current !== root) { + return; + } + + applyTextHighlights(root, highlightQuery ?? ""); + }); + }, [html, highlightQuery]); + + if (!html) { + return null; + } + + return ( + + ); +} + +/** + * Memoize so a message block that has already been rendered — the usual + * case for every assistant bubble above the currently-streaming one — + * doesn't re-parse its markdown on every token. Only re-renders when its + * own text / streaming / highlightQuery props change. + */ +export const MarkdownBlock = memo(MarkdownBlockInner); +MarkdownBlock.displayName = "MarkdownBlock"; diff --git a/apps/app/src/components/markdown/text-highlights.ts b/apps/app/src/components/markdown/text-highlights.ts new file mode 100644 index 0000000000..163123a047 --- /dev/null +++ b/apps/app/src/components/markdown/text-highlights.ts @@ -0,0 +1,99 @@ +const SEARCH_HIGHLIGHT_MARK_ATTR = "data-search-highlight"; +const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + +export function clearTextHighlights(root: HTMLElement) { + const marks = root.querySelectorAll(`mark[${SEARCH_HIGHLIGHT_MARK_ATTR}="true"]`); + + for (const mark of marks) { + const parent = mark.parentNode; + + if (!parent) { + continue; + } + + parent.replaceChild(document.createTextNode(mark.textContent ?? ""), mark); + } + + root.normalize(); +} + +export function applyTextHighlights(root: HTMLElement, query: string) { + const needle = query.trim().toLowerCase(); + // Fast path: if search is inactive, avoid walking large message DOM trees. + // We only need to clear existing marks if a previous search actually added + // some. + if (!needle) { + if (root.querySelector(`mark[${SEARCH_HIGHLIGHT_MARK_ATTR}="true"]`)) { + clearTextHighlights(root); + } + + return; + } + + clearTextHighlights(root); + + const needlePattern = new RegExp(escapeRegExp(needle), "g"); + + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { + acceptNode(node) { + const value = node.nodeValue; + + if (!value || !value.trim()) { + return NodeFilter.FILTER_REJECT; + } + + const parent = node.parentElement; + + if (!parent) { + return NodeFilter.FILTER_REJECT; + } + + if (parent.closest("pre, code")) { + return NodeFilter.FILTER_REJECT; + } + + if (parent.tagName === "SCRIPT" || parent.tagName === "STYLE") { + return NodeFilter.FILTER_REJECT; + } + + return value.toLowerCase().includes(needle) + ? NodeFilter.FILTER_ACCEPT + : NodeFilter.FILTER_REJECT; + }, + }); + + const nodes: Text[] = []; + let current = walker.nextNode(); + while (current) { + nodes.push(current as Text); + current = walker.nextNode(); + } + + for (const node of nodes) { + const text = node.nodeValue ?? ""; + const lower = text.toLowerCase(); + let searchIndex = 0; + const fragment = document.createDocumentFragment(); + + for (const match of lower.matchAll(needlePattern)) { + const matchIndex = match.index; + + if (matchIndex > searchIndex) { + fragment.appendChild(document.createTextNode(text.slice(searchIndex, matchIndex))); + } + + const mark = document.createElement("mark"); + mark.setAttribute(SEARCH_HIGHLIGHT_MARK_ATTR, "true"); + mark.className = "rounded px-0.5 bg-amber-4/70 text-current"; + mark.textContent = text.slice(matchIndex, matchIndex + needle.length); + fragment.appendChild(mark); + searchIndex = matchIndex + needle.length; + } + + if (searchIndex < text.length) { + fragment.appendChild(document.createTextNode(text.slice(searchIndex))); + } + + node.parentNode?.replaceChild(fragment, node); + } +} diff --git a/apps/app/src/components/tools/apply-patch.tsx b/apps/app/src/components/tools/apply-patch.tsx new file mode 100644 index 0000000000..1b86f9ea27 --- /dev/null +++ b/apps/app/src/components/tools/apply-patch.tsx @@ -0,0 +1,29 @@ +"use client" + +import { Tool } from "@/components/ui/tool" +import type { ApplyPatchToolPart } from "@/lib/build-in-tools" + +interface ApplyPatchToolProps { + part: ApplyPatchToolPart +} + +export function getApplyPatchToolTitle(part: ApplyPatchToolPart): string | null { + if (part.state === "output-error") { + return "Apply patch attempted" + } + + if (part.state !== "output-available") { + return null + } + + return "Apply patch" +} + +export function ApplyPatchTool({ part }: ApplyPatchToolProps) { + return ( + + ) +} diff --git a/apps/app/src/components/tools/bash.tsx b/apps/app/src/components/tools/bash.tsx new file mode 100644 index 0000000000..1f521a1738 --- /dev/null +++ b/apps/app/src/components/tools/bash.tsx @@ -0,0 +1,39 @@ +"use client" + +import { SquareTerminalIcon } from "lucide-react" +import { + CollapsibleTool, + CollapsibleToolContent, + CollapsibleToolStep, + CollapsibleToolTrigger, +} from "@/components/tools/collapsible-tool" +import type { BashToolPart } from "@/lib/build-in-tools" + +interface BashToolProps { + part: BashToolPart +} + +export function BashTool({ part }: BashToolProps) { + return ( + + + }> + + + {part.input.description} + + + {part.input.command} + + + + +
    +
    $ {part.input.command}
    +
    {part.output}
    +
    +
    +
    +
    + ) +} diff --git a/apps/app/src/components/tools/collapsible-tool.tsx b/apps/app/src/components/tools/collapsible-tool.tsx new file mode 100644 index 0000000000..f10280217c --- /dev/null +++ b/apps/app/src/components/tools/collapsible-tool.tsx @@ -0,0 +1,144 @@ +"use client" + +import * as React from "react" +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible" +import { cn } from "@/lib/utils" +import { ChevronDown, Circle } from "lucide-react" + +export type CollapsibleToolItemProps = React.ComponentProps<"div"> + +export const CollapsibleToolItem = ({ + children, + className, + ...props +}: CollapsibleToolItemProps) => ( +
    + {children} +
    +) + +export type CollapsibleToolTriggerProps = React.ComponentProps< + typeof CollapsibleTrigger +> & { + leftIcon?: React.ReactNode + swapIconOnHover?: boolean +} + +export const CollapsibleToolTrigger = ({ + children, + className, + leftIcon, + swapIconOnHover = true, + ...props +}: CollapsibleToolTriggerProps) => ( + +
    + {leftIcon ? ( + + + {leftIcon} + + {swapIconOnHover && ( + + )} + + ) : ( + + + + )} + {children} +
    + {!leftIcon && ( + + )} +
    +) + +export type CollapsibleToolContentProps = React.ComponentProps< + typeof CollapsibleContent +> + +export const CollapsibleToolContent = ({ + children, + className, + ...props +}: CollapsibleToolContentProps) => { + return ( + + {children} + + ) +} + +export type CollapsibleToolProps = { + children: React.ReactNode + className?: string +} + +export function CollapsibleTool({ children, className }: CollapsibleToolProps) { + const childrenArray = React.Children.toArray(children) + + return ( +
    + {childrenArray.map((child, index) => ( + + {React.isValidElement(child) && + React.cloneElement( + child as React.ReactElement, + { + isLast: index === childrenArray.length - 1, + } + )} + + ))} +
    + ) +} + +export type CollapsibleToolStepProps = { + children: React.ReactNode + className?: string + isLast?: boolean +} + +export const CollapsibleToolStep = ({ + children, + className, + isLast = false, + ...props +}: CollapsibleToolStepProps & React.ComponentProps) => { + return ( + + {children} +
    +
    +
    + + ) +} diff --git a/apps/app/src/components/tools/edit.tsx b/apps/app/src/components/tools/edit.tsx new file mode 100644 index 0000000000..bb1947bf3e --- /dev/null +++ b/apps/app/src/components/tools/edit.tsx @@ -0,0 +1,27 @@ +"use client" + +import { Tool } from "@/components/ui/tool" +import type { EditToolPart } from "@/lib/build-in-tools" +import { parseFilename } from "@/components/tools/path" + +interface EditToolProps { + part: EditToolPart +} + +export function getEditToolTitle(part: EditToolPart): string | null { + const filename = parseFilename(part.input.filePath) + + if (part.state === "output-error") { + return `Update attempted ${filename}` + } + + if (part.state !== "output-available") { + return null + } + + return `Updated ${filename}` +} + +export function EditTool({ part }: EditToolProps) { + return +} diff --git a/apps/app/src/components/tools/file.tsx b/apps/app/src/components/tools/file.tsx new file mode 100644 index 0000000000..31012b9262 --- /dev/null +++ b/apps/app/src/components/tools/file.tsx @@ -0,0 +1,228 @@ +import type { ReadToolPart, WriteToolPart } from "@/lib/build-in-tools" +import { parseFilename } from "@/components/tools/path" + +interface ReadFileToolProps { + part: ReadToolPart +} + +export function ReadFileTool({ part }: ReadFileToolProps) { + const filename = parseFilename(part.input.filePath); + + if (part.state === "output-error") { + return ( +
    + Read attempted {filename} +
    + ) + } + + if (part.state !== "output-available") { + return null; + } + + const output = parseReadOutput(part.output); + + if (output.type === "directory") { + return ( +
    + Listing {filename} +
    + ) + } + + return ( +
    + Read {filename} + {output.lineRange && output.truncated ? ( + <> + {" "} + L{output.lineRange.firstLine}-{output.lineRange.lastLine} + + ) : null} +
    + ) +} + +interface WriteFileToolProps { + part: WriteToolPart +} + +export function WriteFileTool({ part }: WriteFileToolProps) { + const filename = parseFilename(part.input.filePath); + + if (part.state === "output-error") { + return ( +
    + Write attempted {filename} +
    + ) + } + + if (part.state !== "output-available") { + return null; + } + + return ( +
    + Write {filename} +
    + ) +} + +export interface ReadLine { + number: number + text: string +} + +export interface ReadLineRange { + firstLine: number + lastLine: number + totalLines?: number +} + +export interface ParsedReadFileOutput { + type: "file" + lines: ReadLine[] + lineRange?: ReadLineRange + truncated: boolean +} + +export interface ParsedReadDirectoryOutput { + type: "directory" + entries: string[] + truncated: boolean +} + +export type ParsedReadOutput = ParsedReadFileOutput | ParsedReadDirectoryOutput + +const END_OF_FILE_PATTERN = /^\(End of file - total (\d+) lines\)$/ +const OUTPUT_CAPPED_PATTERN = /^\(Output capped at 50 KB\. Showing lines (\d+)-(\d+)\. Use offset=(\d+) to continue\.\)$/ +const PARTIAL_FILE_PATTERN = /^\(Showing lines (\d+)-(\d+) of (\d+)\. Use offset=(\d+) to continue\.\)$/ +const DIRECTORY_COUNT_PATTERN = /^\((\d+) entries\)$/ +const DIRECTORY_TRUNCATED_PATTERN = /^\(Showing (\d+) of (\d+) entries\. Use 'offset' parameter to read beyond entry (\d+)\)$/ +const LINE_PATTERN = /^(\d+): ?(.*)$/ + +export function parseReadOutput(output: string): ParsedReadOutput { + const type = extractTag(output, "type") + + if (type === "directory") { + const entries = extractTag(output, "entries") + const parsedEntries = parseReadEntries(entries) + + return { + type: "directory", + entries: parsedEntries.entries, + truncated: parsedEntries.truncated, + } + } + + const content = extractTag(output, "content") + const parsedContent = parseReadContent(content) + + return { + type: "file", + lines: parsedContent.lines, + lineRange: parsedContent.lineRange, + truncated: parsedContent.truncated, + } +} + +function parseReadContent(content: string | undefined) { + const lines: ReadLine[] = [] + let lineCount: number | undefined + let lineRange: ReadLineRange | undefined + let truncated = false + + for (const rawLine of splitLines(content ?? "")) { + const lineMatch = rawLine.match(LINE_PATTERN) + if (lineMatch) { + lines.push({ + number: Number(lineMatch[1]), + text: lineMatch[2] ?? "", + }) + continue + } + + const endMatch = rawLine.match(END_OF_FILE_PATTERN) + if (endMatch) { + lineCount = Number(endMatch[1]) + continue + } + + const cappedMatch = rawLine.match(OUTPUT_CAPPED_PATTERN) + if (cappedMatch) { + truncated = true + lineRange = { + firstLine: Number(cappedMatch[1]), + lastLine: Number(cappedMatch[2]), + } + continue + } + + const partialMatch = rawLine.match(PARTIAL_FILE_PATTERN) + if (partialMatch) { + truncated = true + lineRange = { + firstLine: Number(partialMatch[1]), + lastLine: Number(partialMatch[2]), + totalLines: Number(partialMatch[3]), + } + } + } + + return { + lines, + lineRange: lineRange ?? getLineRange(lines, lineCount), + truncated, + } +} + +function parseReadEntries(entriesBlock: string | undefined) { + const entries: string[] = [] + let truncated = false + + for (const rawLine of splitLines(entriesBlock ?? "")) { + const line = rawLine.trim() + if (!line) { + continue + } + + const countMatch = line.match(DIRECTORY_COUNT_PATTERN) + if (countMatch) { + continue + } + + const truncatedMatch = line.match(DIRECTORY_TRUNCATED_PATTERN) + if (truncatedMatch) { + truncated = true + continue + } + + entries.push(rawLine) + } + + return { entries, truncated } +} + +function getLineRange(lines: ReadLine[], totalLines: number | undefined): ReadLineRange | undefined { + const first = lines[0] + const last = lines.at(-1) + if (!first || !last) { + return undefined + } + + return { + firstLine: first.number, + lastLine: last.number, + totalLines, + } +} + +function extractTag(output: string, tagName: string): string | undefined { + const pattern = new RegExp(`<${tagName}>([\\s\\S]*?)`) + return pattern.exec(output)?.[1]?.trim() +} + +function splitLines(value: string): string[] { + return value.split(/\r\n|\n|\r/) +} \ No newline at end of file diff --git a/apps/app/src/components/tools/glob.tsx b/apps/app/src/components/tools/glob.tsx new file mode 100644 index 0000000000..ba07b61443 --- /dev/null +++ b/apps/app/src/components/tools/glob.tsx @@ -0,0 +1,47 @@ +"use client" + +import { Tool } from "@/components/ui/tool" +import type { GlobToolPart } from "@/lib/build-in-tools" +import { parseFilename, toolDisplayTitle, truncateText } from "@/components/tools/path" + +interface GlobToolProps { + part: GlobToolPart +} + +export function getGlobToolTitle(part: GlobToolPart): string | null { + const pattern = part.input.pattern.trim() + + if (part.state === "output-error") { + return pattern + ? `Search attempted ${truncateText(pattern, 44)}` + : "Search attempted" + } + + if (part.state !== "output-available") { + return null + } + + return pattern ? `Searched ${truncateText(pattern, 44)}` : "Searched code" +} + +export function getGlobToolDetail(part: GlobToolPart): string | undefined { + const root = part.input.path?.trim() + if (!root) { + return undefined + } + + return `in ${parseFilename(root)}` +} + +export function GlobTool({ part }: GlobToolProps) { + return ( + + ) +} diff --git a/apps/app/src/components/tools/grep.tsx b/apps/app/src/components/tools/grep.tsx new file mode 100644 index 0000000000..4ee0289f5b --- /dev/null +++ b/apps/app/src/components/tools/grep.tsx @@ -0,0 +1,47 @@ +"use client" + +import { Tool } from "@/components/ui/tool" +import type { GrepToolPart } from "@/lib/build-in-tools" +import { parseFilename, toolDisplayTitle, truncateText } from "@/components/tools/path" + +interface GrepToolProps { + part: GrepToolPart +} + +export function getGrepToolTitle(part: GrepToolPart): string | null { + const pattern = part.input.pattern.trim() + + if (part.state === "output-error") { + return pattern + ? `Search attempted ${truncateText(pattern, 44)}` + : "Search attempted" + } + + if (part.state !== "output-available") { + return null + } + + return pattern ? `Searched ${truncateText(pattern, 44)}` : "Searched code" +} + +export function getGrepToolDetail(part: GrepToolPart): string | undefined { + const root = part.input.path?.trim() + if (!root) { + return undefined + } + + return `in ${parseFilename(root)}` +} + +export function GrepTool({ part }: GrepToolProps) { + return ( + + ) +} diff --git a/apps/app/src/components/tools/lsp.tsx b/apps/app/src/components/tools/lsp.tsx new file mode 100644 index 0000000000..0c2cf81f0e --- /dev/null +++ b/apps/app/src/components/tools/lsp.tsx @@ -0,0 +1,62 @@ +"use client" + +import { Tool } from "@/components/ui/tool" +import type { LspInput, LspToolPart } from "@/lib/build-in-tools" +import { parseFilename, toolDisplayTitle } from "@/components/tools/path" + +interface LspToolProps { + part: LspToolPart +} + +const LSP_OPERATION_LABELS: Record = { + goToDefinition: "Go to definition", + findReferences: "Find references", + hover: "Hover", + documentSymbol: "Document symbols", + workspaceSymbol: "Workspace symbols", + goToImplementation: "Go to implementation", + prepareCallHierarchy: "Prepare call hierarchy", + incomingCalls: "Incoming calls", + outgoingCalls: "Outgoing calls", +} + +export function getLspToolTitle(part: LspToolPart): string | null { + const filename = parseFilename(part.input.filePath) + const operation = LSP_OPERATION_LABELS[part.input.operation] + + if (part.state === "output-error") { + return `${operation} attempted in ${filename}` + } + + if (part.state !== "output-available") { + return null + } + + return `${operation} in ${filename}` +} + +export function getLspToolDetail(part: LspToolPart): string | undefined { + const line = part.input.line + const character = part.input.character + const query = part.input.query?.trim() + + const location = `L${line}:${character}` + if (query) { + return `${location} · ${query}` + } + + return location +} + +export function LspTool({ part }: LspToolProps) { + return ( + + ) +} diff --git a/apps/app/src/components/tools/path.ts b/apps/app/src/components/tools/path.ts new file mode 100644 index 0000000000..03fd3f5ea2 --- /dev/null +++ b/apps/app/src/components/tools/path.ts @@ -0,0 +1,22 @@ +export function parseFilename(filePath: string | undefined) { + if (!filePath) { + return "file" + } + return filePath.replace(/\\/g, "/").split("/").pop() ?? filePath +} + +export function truncateText(value: string, max: number) { + return value.length > max ? `${value.slice(0, Math.max(0, max - 3))}...` : value +} + +export function toolDisplayTitle( + title: string | null, + fallback: string, + detail?: string +) { + const primary = title ?? fallback + if (!detail) { + return primary + } + return `${primary} ${detail}` +} diff --git a/apps/app/src/components/tools/question.tsx b/apps/app/src/components/tools/question.tsx new file mode 100644 index 0000000000..494a4b7f0a --- /dev/null +++ b/apps/app/src/components/tools/question.tsx @@ -0,0 +1,67 @@ +"use client" + +import { Tool } from "@/components/ui/tool" +import type { QuestionToolPart } from "@/lib/build-in-tools" +import { toolDisplayTitle, truncateText } from "@/components/tools/path" + +interface QuestionToolProps { + part: QuestionToolPart +} + +function getFirstQuestionLabel(part: QuestionToolPart) { + const first = part.input.questions[0] + if (!first) { + return undefined + } + + const header = first.header.trim() + const question = first.question.trim() + const label = header || question + return label ? truncateText(label, 56) : undefined +} + +export function getQuestionToolTitle(part: QuestionToolPart): string | null { + const label = getFirstQuestionLabel(part) + const count = part.input.questions.length + + if (part.state === "output-error") { + return label ?? "Asked a question" + } + + if (part.state !== "output-available") { + return null + } + + if (label) { + return label + } + + return count > 1 ? `Asked ${count} questions` : "Asked a question" +} + +export function getQuestionToolDetail(part: QuestionToolPart): string | undefined { + const count = part.input.questions.length + + if (part.state === "output-available") { + return "Answered" + } + + if (count > 1) { + return `${count} questions` + } + + return undefined +} + +export function QuestionTool({ part }: QuestionToolProps) { + return ( + + ) +} diff --git a/apps/app/src/components/tools/skill.tsx b/apps/app/src/components/tools/skill.tsx new file mode 100644 index 0000000000..8a13cd7b36 --- /dev/null +++ b/apps/app/src/components/tools/skill.tsx @@ -0,0 +1,26 @@ +"use client" + +import { Tool } from "@/components/ui/tool" +import type { SkillToolPart } from "@/lib/build-in-tools" + +interface SkillToolProps { + part: SkillToolPart +} + +export function getSkillToolTitle(part: SkillToolPart): string | null { + const name = part.input.name.trim() + + if (part.state === "output-error") { + return name ? `Load skill ${name} attempted` : "Load skill attempted" + } + + if (part.state !== "output-available") { + return null + } + + return name ? `Load skill ${name}` : "Load skill" +} + +export function SkillTool({ part }: SkillToolProps) { + return +} diff --git a/apps/app/src/components/tools/todowrite.tsx b/apps/app/src/components/tools/todowrite.tsx new file mode 100644 index 0000000000..9aa3d59a96 --- /dev/null +++ b/apps/app/src/components/tools/todowrite.tsx @@ -0,0 +1,28 @@ +"use client" + +import { Tool } from "@/components/ui/tool" +import type { TodoWriteToolPart } from "@/lib/build-in-tools" + +interface TodoWriteToolProps { + part: TodoWriteToolPart +} + +export function getTodoWriteToolTitle(part: TodoWriteToolPart): string | null { + const count = part.input.todos.length + + if (part.state === "output-error") { + return "Update todo list attempted" + } + + if (part.state !== "output-available") { + return null + } + + return count > 0 ? `Update todo list (${count})` : "Update todo list" +} + +export function TodoWriteTool({ part }: TodoWriteToolProps) { + return ( + + ) +} diff --git a/apps/app/src/components/tools/webfetch.tsx b/apps/app/src/components/tools/webfetch.tsx new file mode 100644 index 0000000000..507171a7a8 --- /dev/null +++ b/apps/app/src/components/tools/webfetch.tsx @@ -0,0 +1,68 @@ +import * as React from "react" +import { Globe } from "lucide-react" +import { + Source, + SourceContent, + SourceTrigger, +} from "@/components/ui/source" +import type { WebFetchToolPart } from "@/lib/build-in-tools" +import { cn } from "@/lib/utils" +import { Tool } from "@/components/ui/tool" + +interface WebfetchToolProps { + part: WebFetchToolPart +} + +export function WebfetchTool({ part }: WebfetchToolProps) { + if (part.state === "output-error") { + return + } + + if (part.state !== "output-available") { + return + } + + return ( +
    + }> + Fetching + + + + + +
    + ) +} + +interface WebfetchTriggerProps { + children: React.ReactNode + className?: string + leftIcon?: React.ReactNode +} + +function WebfetchTrigger({ + children, + className, + leftIcon, +}: WebfetchTriggerProps) { + return ( +
    +
    + {leftIcon && ( + + {leftIcon} + + )} + {children} +
    +
    + ) +} \ No newline at end of file diff --git a/apps/app/src/components/tools/websearch.tsx b/apps/app/src/components/tools/websearch.tsx new file mode 100644 index 0000000000..ad5ecfcc81 --- /dev/null +++ b/apps/app/src/components/tools/websearch.tsx @@ -0,0 +1,56 @@ +"use client" + +import { Search } from "lucide-react" +import { + ChainOfThought, + ChainOfThoughtContent, + ChainOfThoughtStep, + ChainOfThoughtTrigger, +} from "@/components/ui/chain-of-thought" +import { + Source, + SourceContent, + SourceTrigger, +} from "@/components/ui/source" +import type { WebSearchToolPart } from "@/lib/build-in-tools" +import { Tool } from "@/components/ui/tool" +import { parseWebSearchResults } from "@/lib/websearch-results" + +interface WebsearchToolProps { + part: WebSearchToolPart +} + +export function WebsearchTool({ part }: WebsearchToolProps) { + if (part.state === "output-error") { + return + } + + if (part.state !== "output-available") { + return + } + + const results = parseWebSearchResults(part.output) + + return ( + + + }> + {results.length > 0 ? `Searching for "${part.input.query}"` : "Web search (No results)"} + + +
    + {results.map((result) => ( + + + + + ))} +
    +
    +
    +
    + ) +} \ No newline at end of file diff --git a/apps/app/src/components/ui/avatar.tsx b/apps/app/src/components/ui/avatar.tsx new file mode 100644 index 0000000000..d9cfbc5e38 --- /dev/null +++ b/apps/app/src/components/ui/avatar.tsx @@ -0,0 +1,107 @@ +import * as React from "react" +import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + size = "default", + ...props +}: AvatarPrimitive.Root.Props & { + size?: "default" | "sm" | "lg" +}) { + return ( + + ) +} + +function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: AvatarPrimitive.Fallback.Props) { + return ( + + ) +} + +function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) { + return ( + svg]:hidden", + "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", + "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", + className + )} + {...props} + /> + ) +} + +function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ) +} + +function AvatarGroupCount({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
    svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3", + className + )} + {...props} + /> + ) +} + +export { + Avatar, + AvatarImage, + AvatarFallback, + AvatarGroup, + AvatarGroupCount, + AvatarBadge, +} diff --git a/apps/app/src/components/ui/badge.tsx b/apps/app/src/components/ui/badge.tsx new file mode 100644 index 0000000000..d93b7dcdc4 --- /dev/null +++ b/apps/app/src/components/ui/badge.tsx @@ -0,0 +1,52 @@ +import { mergeProps } from "@base-ui/react/merge-props" +import { useRender } from "@base-ui/react/use-render" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-3xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", + secondary: + "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", + destructive: + "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20", + outline: + "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", + ghost: + "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", + link: "text-primary underline-offset-4 hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant = "default", + render, + ...props +}: useRender.ComponentProps<"span"> & VariantProps) { + return useRender({ + defaultTagName: "span", + props: mergeProps<"span">( + { + className: cn(badgeVariants({ variant }), className), + }, + props + ), + render, + state: { + slot: "badge", + variant, + }, + }) +} + +export { Badge, badgeVariants } diff --git a/apps/app/src/components/ui/chain-of-thought.tsx b/apps/app/src/components/ui/chain-of-thought.tsx new file mode 100644 index 0000000000..378b1446b6 --- /dev/null +++ b/apps/app/src/components/ui/chain-of-thought.tsx @@ -0,0 +1,148 @@ +"use client" + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible" +import { cn } from "@/lib/utils" +import { ChevronDown, Circle } from "lucide-react" +import React from "react" + +export type ChainOfThoughtItemProps = React.ComponentProps<"div"> + +export const ChainOfThoughtItem = ({ + children, + className, + ...props +}: ChainOfThoughtItemProps) => ( +
    + {children} +
    +) + +export type ChainOfThoughtTriggerProps = React.ComponentProps< + typeof CollapsibleTrigger +> & { + leftIcon?: React.ReactNode + swapIconOnHover?: boolean +} + +export const ChainOfThoughtTrigger = ({ + children, + className, + leftIcon, + swapIconOnHover = true, + ...props +}: ChainOfThoughtTriggerProps) => ( + +
    + {leftIcon ? ( + + + {leftIcon} + + {swapIconOnHover && ( + + )} + + ) : ( + + + + )} + {children} +
    + {!leftIcon && ( + + )} +
    +) + +export type ChainOfThoughtContentProps = React.ComponentProps< + typeof CollapsibleContent +> + +export const ChainOfThoughtContent = ({ + children, + className, + ...props +}: ChainOfThoughtContentProps) => { + return ( + +
    +
    +
    +
    {children}
    +
    + + ) +} + +export type ChainOfThoughtProps = { + children: React.ReactNode + className?: string +} + +export function ChainOfThought({ children, className }: ChainOfThoughtProps) { + const childrenArray = React.Children.toArray(children) + + return ( +
    + {childrenArray.map((child, index) => ( + + {React.isValidElement(child) && + React.cloneElement( + child as React.ReactElement, + { + isLast: index === childrenArray.length - 1, + } + )} + + ))} +
    + ) +} + +export type ChainOfThoughtStepProps = { + children: React.ReactNode + className?: string + isLast?: boolean +} + +export const ChainOfThoughtStep = ({ + children, + className, + isLast = false, + ...props +}: ChainOfThoughtStepProps & React.ComponentProps) => { + return ( + + {children} +
    +
    +
    + + ) +} diff --git a/apps/app/src/components/ui/code-block.tsx b/apps/app/src/components/ui/code-block.tsx new file mode 100644 index 0000000000..07bc58d986 --- /dev/null +++ b/apps/app/src/components/ui/code-block.tsx @@ -0,0 +1,103 @@ +import { cn } from "@/lib/utils" +import React, { useEffect, useState } from "react" +import { codeToHtml } from "shiki" + +export type CodeBlockProps = { + children?: React.ReactNode + className?: string +} & React.HTMLProps + +function CodeBlock({ children, className, ...props }: CodeBlockProps) { + return ( +
    + {children} +
    + ) +} + +export type CodeBlockCodeProps = { + code: string + language?: string + theme?: string + className?: string +} & React.HTMLProps + +function CodeBlockCode({ + code, + language = "tsx", + theme = "github-light", + className, + ...props +}: CodeBlockCodeProps) { + const [highlightedHtml, setHighlightedHtml] = useState(null) + + useEffect(() => { + let cancelled = false + + async function highlight() { + if (!code) { + if (!cancelled) { + setHighlightedHtml("
    ") + } + return + } + + const html = await codeToHtml(code, { lang: language, theme }) + if (!cancelled) { + setHighlightedHtml(html) + } + } + + highlight() + + return () => { + cancelled = true + } + }, [code, language, theme]) + + const classNames = cn( + "w-full overflow-x-auto text-[13px] [&>pre]:px-4 [&>pre]:py-4", + className + ) + + // SSR fallback: render plain code if not hydrated yet + return highlightedHtml ? ( +
    + ) : ( +
    +
    +        {code}
    +      
    +
    + ) +} + +export type CodeBlockGroupProps = React.HTMLAttributes + +function CodeBlockGroup({ + children, + className, + ...props +}: CodeBlockGroupProps) { + return ( +
    + {children} +
    + ) +} + +export { CodeBlockGroup, CodeBlockCode, CodeBlock } diff --git a/apps/app/src/components/ui/hover-card.tsx b/apps/app/src/components/ui/hover-card.tsx new file mode 100644 index 0000000000..73ea5c097c --- /dev/null +++ b/apps/app/src/components/ui/hover-card.tsx @@ -0,0 +1,49 @@ +import { PreviewCard as PreviewCardPrimitive } from "@base-ui/react/preview-card" + +import { cn } from "@/lib/utils" + +function HoverCard({ ...props }: PreviewCardPrimitive.Root.Props) { + return +} + +function HoverCardTrigger({ ...props }: PreviewCardPrimitive.Trigger.Props) { + return ( + + ) +} + +function HoverCardContent({ + className, + side = "bottom", + sideOffset = 4, + align = "center", + alignOffset = 4, + ...props +}: PreviewCardPrimitive.Popup.Props & + Pick< + PreviewCardPrimitive.Positioner.Props, + "align" | "alignOffset" | "side" | "sideOffset" + >) { + return ( + + + + + + ) +} + +export { HoverCard, HoverCardTrigger, HoverCardContent } diff --git a/apps/app/src/components/ui/image.tsx b/apps/app/src/components/ui/image.tsx new file mode 100644 index 0000000000..70e00bda9f --- /dev/null +++ b/apps/app/src/components/ui/image.tsx @@ -0,0 +1,76 @@ +import { cn } from "@/lib/utils" +import * as React from "react" + +export type GeneratedImageLike = { + src?: string + base64?: string + uint8Array?: Uint8Array + mediaType?: string +} + +export type ImageProps = GeneratedImageLike & + React.ComponentProps<"img"> & { + alt: string + } + +function getImageSrc({ + base64, + mediaType, +}: Pick) { + if (base64 && mediaType) { + return `data:${mediaType};base64,${base64}` + } + return undefined +} + +export const Image = ({ + src, + base64, + uint8Array, + mediaType = "image/png", + className, + alt, + ...props +}: ImageProps) => { + const [objectUrl, setObjectUrl] = React.useState(undefined) + + React.useEffect(() => { + if (uint8Array && mediaType) { + const blob = new Blob([uint8Array as BlobPart], { type: mediaType }) + const url = URL.createObjectURL(blob) + setObjectUrl(url) + return () => { + URL.revokeObjectURL(url) + } + } + setObjectUrl(undefined) + return + }, [uint8Array, mediaType]) + + const base64Src = getImageSrc({ base64, mediaType }) + const imageSrc = src ?? base64Src ?? objectUrl + + if (!imageSrc) { + return ( +
    + ) + } + + return ( + {alt} + ) +} diff --git a/apps/app/src/components/ui/markdown.tsx b/apps/app/src/components/ui/markdown.tsx new file mode 100644 index 0000000000..fdbc89991b --- /dev/null +++ b/apps/app/src/components/ui/markdown.tsx @@ -0,0 +1,110 @@ +import { cn } from "@/lib/utils" +import { marked } from "marked" +import { memo, useId, useMemo } from "react" +import ReactMarkdown, { Components } from "react-markdown" +import remarkBreaks from "remark-breaks" +import remarkGfm from "remark-gfm" +import { CodeBlock, CodeBlockCode } from "./code-block" + +export type MarkdownProps = { + children: string + id?: string + className?: string + components?: Partial +} + +function parseMarkdownIntoBlocks(markdown: string): string[] { + const tokens = marked.lexer(markdown) + return tokens.map((token) => token.raw) +} + +function extractLanguage(className?: string): string { + if (!className) return "plaintext" + const match = className.match(/language-(\w+)/) + return match ? match[1] : "plaintext" +} + +const INITIAL_COMPONENTS: Partial = { + code: function CodeComponent({ className, children, ...props }) { + const isInline = + !props.node?.position?.start.line || + props.node?.position?.start.line === props.node?.position?.end.line + + if (isInline) { + return ( + + {children} + + ) + } + + const language = extractLanguage(className) + + return ( + + + + ) + }, + pre: function PreComponent({ children }) { + return <>{children} + }, +} + +const MemoizedMarkdownBlock = memo( + function MarkdownBlock({ + content, + components = INITIAL_COMPONENTS, + }: { + content: string + components?: Partial + }) { + return ( + + {content} + + ) + }, + function propsAreEqual(prevProps, nextProps) { + return prevProps.content === nextProps.content + } +) + +MemoizedMarkdownBlock.displayName = "MemoizedMarkdownBlock" + +function MarkdownComponent({ + children, + id, + className, + components = INITIAL_COMPONENTS, +}: MarkdownProps) { + const generatedId = useId() + const blockId = id ?? generatedId + const blocks = useMemo(() => parseMarkdownIntoBlocks(children), [children]) + + return ( +
    + {blocks.map((block, index) => ( + + ))} +
    + ) +} + +const Markdown = memo(MarkdownComponent) +Markdown.displayName = "Markdown" + +export { Markdown } diff --git a/apps/app/src/components/ui/message.tsx b/apps/app/src/components/ui/message.tsx new file mode 100644 index 0000000000..345b600557 --- /dev/null +++ b/apps/app/src/components/ui/message.tsx @@ -0,0 +1,127 @@ +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { MarkdownBlock } from "@/components/markdown/markdown" +import { cn } from "@/lib/utils" +import { motion } from "motion/react" + +const messageContentClassName = + "rounded-lg p-2 text-foreground leading-relaxed bg-secondary prose wrap-break-word whitespace-normal" + +export type MessageProps = { + children: React.ReactNode + className?: string +} & React.HTMLProps + +const Message = ({ children, className, ...props }: MessageProps) => ( +
    + {children} +
    +) + +export type MessageAvatarProps = { + src: string + alt: string + fallback?: string + delayMs?: number + className?: string +} + +const MessageAvatar = ({ + src, + alt, + fallback, + delayMs, + className, +}: MessageAvatarProps) => { + return ( + + + {fallback && ( + {fallback} + )} + + ) +} + +export type MessageContentProps = { + children: React.ReactNode + markdown?: boolean + isStreaming?: boolean + className?: string +} & React.ComponentProps + +const MessageContent = ({ + children, + markdown = false, + className, + isStreaming, + ...props +}: MessageContentProps) => { + if (markdown) { + return ( + + ) + } + + return ( + + {children} + + ) +} + +export type MessageActionsProps = { + children: React.ReactNode + className?: string +} & React.HTMLProps + +const MessageActions = ({ + children, + className, + ...props +}: MessageActionsProps) => ( +
    + {children} +
    +) + +export type MessageActionProps = { + className?: string + tooltip: React.ReactNode + children: React.ReactElement + side?: "top" | "bottom" | "left" | "right" +} & React.ComponentProps + +const MessageAction = ({ + tooltip, + children, + className, + side = "top", + ...props +}: MessageActionProps) => { + return ( + + + + {tooltip} + + + ) +} + +export { Message, MessageAvatar, MessageContent, MessageActions, MessageAction } diff --git a/apps/app/src/components/ui/reasoning.tsx b/apps/app/src/components/ui/reasoning.tsx new file mode 100644 index 0000000000..3adff6a022 --- /dev/null +++ b/apps/app/src/components/ui/reasoning.tsx @@ -0,0 +1,174 @@ +import { cn } from "@/lib/utils" +import { ChevronDownIcon } from "lucide-react" +import React, { + createContext, + useContext, + useEffect, + useRef, + useState, +} from "react" +import { MarkdownBlock } from "../markdown/markdown" + +type ReasoningContextType = { + isOpen: boolean + onOpenChange: (open: boolean) => void +} + +const ReasoningContext = createContext( + undefined +) + +function useReasoningContext() { + const context = useContext(ReasoningContext) + if (!context) { + throw new Error( + "useReasoningContext must be used within a Reasoning provider" + ) + } + return context +} + +export type ReasoningProps = { + children: React.ReactNode + className?: string + open?: boolean + onOpenChange?: (open: boolean) => void + isStreaming?: boolean +} +function Reasoning({ + children, + className, + open, + onOpenChange, + isStreaming, +}: ReasoningProps) { + const [internalOpen, setInternalOpen] = useState(false) + const [wasAutoOpened, setWasAutoOpened] = useState(false) + + const isControlled = open !== undefined + const isOpen = isControlled ? open : internalOpen + + const handleOpenChange = (newOpen: boolean) => { + if (!isControlled) { + setInternalOpen(newOpen) + } + onOpenChange?.(newOpen) + } + + useEffect(() => { + if (isStreaming && !wasAutoOpened) { + if (!isControlled) setInternalOpen(true) + setWasAutoOpened(true) + } + + if (!isStreaming && wasAutoOpened) { + if (!isControlled) setInternalOpen(false) + setWasAutoOpened(false) + } + }, [isStreaming, wasAutoOpened, isControlled]) + + return ( + +
    {children}
    +
    + ) +} + +export type ReasoningTriggerProps = { + children: React.ReactNode + className?: string +} & React.HTMLAttributes + +function ReasoningTrigger({ + children, + className, + ...props +}: ReasoningTriggerProps) { + const { isOpen, onOpenChange } = useReasoningContext() + + return ( + + ) +} + +export type ReasoningContentProps = { + children: React.ReactNode + className?: string + markdown?: boolean + contentClassName?: string +} & React.HTMLAttributes + +function ReasoningContent({ + children, + className, + contentClassName, + markdown = false, + ...props +}: ReasoningContentProps) { + const contentRef = useRef(null) + const innerRef = useRef(null) + const { isOpen } = useReasoningContext() + + useEffect(() => { + if (!contentRef.current || !innerRef.current) return + + const observer = new ResizeObserver(() => { + if (contentRef.current && innerRef.current && isOpen) { + contentRef.current.style.maxHeight = `${innerRef.current.scrollHeight}px` + } + }) + + observer.observe(innerRef.current) + + if (isOpen) { + contentRef.current.style.maxHeight = `${innerRef.current.scrollHeight}px` + } + + return () => observer.disconnect() + }, [isOpen]) + + return ( +
    +
    + {markdown ? : children} +
    +
    + ) +} + +export { Reasoning, ReasoningTrigger, ReasoningContent } diff --git a/apps/app/src/components/ui/response-stream.tsx b/apps/app/src/components/ui/response-stream.tsx new file mode 100644 index 0000000000..997748b2bc --- /dev/null +++ b/apps/app/src/components/ui/response-stream.tsx @@ -0,0 +1,394 @@ +import { cn } from "@/lib/utils" +import React, { useCallback, useEffect, useRef, useState } from "react" + +export type Mode = "typewriter" | "fade" + +export type UseTextStreamOptions = { + textStream: string | AsyncIterable + speed?: number + mode?: Mode + onComplete?: () => void + fadeDuration?: number + segmentDelay?: number + characterChunkSize?: number + onError?: (error: unknown) => void +} + +export type UseTextStreamResult = { + displayedText: string + isComplete: boolean + segments: { text: string; index: number }[] + getFadeDuration: () => number + getSegmentDelay: () => number + reset: () => void + startStreaming: () => void + pause: () => void + resume: () => void +} + +function useTextStream({ + textStream, + speed = 20, + mode = "typewriter", + onComplete, + fadeDuration, + segmentDelay, + characterChunkSize, + onError, +}: UseTextStreamOptions): UseTextStreamResult { + const [displayedText, setDisplayedText] = useState("") + const [isComplete, setIsComplete] = useState(false) + const [segments, setSegments] = useState<{ text: string; index: number }[]>( + [] + ) + + const speedRef = useRef(speed) + const modeRef = useRef(mode) + const currentIndexRef = useRef(0) + const animationRef = useRef(null) + const fadeDurationRef = useRef(fadeDuration) + const segmentDelayRef = useRef(segmentDelay) + const characterChunkSizeRef = useRef(characterChunkSize) + const streamRef = useRef(null) + const completedRef = useRef(false) + const onCompleteRef = useRef(onComplete) + const onErrorRef = useRef(onError) + + useEffect(() => { + speedRef.current = speed + modeRef.current = mode + fadeDurationRef.current = fadeDuration + segmentDelayRef.current = segmentDelay + characterChunkSizeRef.current = characterChunkSize + }, [speed, mode, fadeDuration, segmentDelay, characterChunkSize]) + + useEffect(() => { + onCompleteRef.current = onComplete + onErrorRef.current = onError + }, [onComplete, onError]) + + const getChunkSize = useCallback(() => { + if (typeof characterChunkSizeRef.current === "number") { + return Math.max(1, characterChunkSizeRef.current) + } + + const normalizedSpeed = Math.min(100, Math.max(1, speedRef.current)) + + if (modeRef.current === "typewriter") { + if (normalizedSpeed < 25) return 1 + return Math.max(1, Math.round((normalizedSpeed - 25) / 10)) + } else if (modeRef.current === "fade") { + return 1 + } + + return 1 + }, []) + + const getProcessingDelay = useCallback(() => { + if (typeof segmentDelayRef.current === "number") { + return Math.max(0, segmentDelayRef.current) + } + + const normalizedSpeed = Math.min(100, Math.max(1, speedRef.current)) + return Math.max(1, Math.round(100 / Math.sqrt(normalizedSpeed))) + }, []) + + const getFadeDuration = useCallback(() => { + if (typeof fadeDurationRef.current === "number") + return Math.max(10, fadeDurationRef.current) + + const normalizedSpeed = Math.min(100, Math.max(1, speedRef.current)) + return Math.round(1000 / Math.sqrt(normalizedSpeed)) + }, []) + + const getSegmentDelay = useCallback(() => { + if (typeof segmentDelayRef.current === "number") + return Math.max(0, segmentDelayRef.current) + + const normalizedSpeed = Math.min(100, Math.max(1, speedRef.current)) + return Math.max(1, Math.round(100 / Math.sqrt(normalizedSpeed))) + }, []) + + const updateSegments = useCallback((text: string) => { + if (modeRef.current === "fade") { + try { + const segmenter = new Intl.Segmenter(navigator.language, { + granularity: "word", + }) + const segmentIterator = segmenter.segment(text) + const newSegments = Array.from(segmentIterator).map( + (segment, index) => ({ + text: segment.segment, + index, + }) + ) + setSegments(newSegments) + } catch (error) { + const newSegments = text + .split(/(\s+)/) + .filter(Boolean) + .map((word, index) => ({ + text: word, + index, + })) + setSegments(newSegments) + onErrorRef.current?.(error) + } + } + }, []) + + const markComplete = useCallback(() => { + if (!completedRef.current) { + completedRef.current = true + setIsComplete(true) + onCompleteRef.current?.() + } + }, []) + + const reset = useCallback(() => { + currentIndexRef.current = 0 + setDisplayedText("") + setSegments([]) + setIsComplete(false) + completedRef.current = false + + if (animationRef.current) { + cancelAnimationFrame(animationRef.current) + animationRef.current = null + } + }, []) + + const processStringTypewriter = useCallback( + (text: string) => { + let lastFrameTime = 0 + + const streamContent = (timestamp: number) => { + const delay = getProcessingDelay() + if (delay > 0 && timestamp - lastFrameTime < delay) { + animationRef.current = requestAnimationFrame(streamContent) + return + } + lastFrameTime = timestamp + + if (currentIndexRef.current >= text.length) { + markComplete() + return + } + + const chunkSize = getChunkSize() + const endIndex = Math.min( + currentIndexRef.current + chunkSize, + text.length + ) + const newDisplayedText = text.slice(0, endIndex) + + setDisplayedText(newDisplayedText) + if (modeRef.current === "fade") { + updateSegments(newDisplayedText) + } + + currentIndexRef.current = endIndex + + if (endIndex < text.length) { + animationRef.current = requestAnimationFrame(streamContent) + } else { + markComplete() + } + } + + animationRef.current = requestAnimationFrame(streamContent) + }, + [getProcessingDelay, getChunkSize, updateSegments, markComplete] + ) + + const processAsyncIterable = useCallback( + async (stream: AsyncIterable) => { + const controller = new AbortController() + streamRef.current = controller + + let displayed = "" + + try { + for await (const chunk of stream) { + if (controller.signal.aborted) return + + displayed += chunk + setDisplayedText(displayed) + updateSegments(displayed) + } + + markComplete() + } catch (error) { + console.error("Error processing text stream:", error) + markComplete() + onErrorRef.current?.(error) + } + }, + [updateSegments, markComplete] + ) + + const startStreaming = useCallback(() => { + reset() + + if (typeof textStream === "string") { + processStringTypewriter(textStream) + } else if (textStream) { + processAsyncIterable(textStream) + } + }, [textStream, reset, processStringTypewriter, processAsyncIterable]) + + const pause = useCallback(() => { + if (animationRef.current) { + cancelAnimationFrame(animationRef.current) + animationRef.current = null + } + }, []) + + const resume = useCallback(() => { + if (typeof textStream === "string" && !isComplete) { + processStringTypewriter(textStream) + } + }, [textStream, isComplete, processStringTypewriter]) + + useEffect(() => { + startStreaming() + + return () => { + if (animationRef.current) { + cancelAnimationFrame(animationRef.current) + } + if (streamRef.current) { + streamRef.current.abort() + } + } + }, [textStream, startStreaming]) + + return { + displayedText, + isComplete, + segments, + getFadeDuration, + getSegmentDelay, + reset, + startStreaming, + pause, + resume, + } +} + +export type ResponseStreamProps = { + textStream: string | AsyncIterable + mode?: Mode + speed?: number // 1-100, where 1 is slowest and 100 is fastest + className?: string + onComplete?: () => void + as?: keyof React.JSX.IntrinsicElements // Element type to render + fadeDuration?: number // Custom fade duration in ms (overrides speed) + segmentDelay?: number // Custom delay between segments in ms (overrides speed) + characterChunkSize?: number // Custom characters per frame for typewriter mode (overrides speed) +} + +function ResponseStream({ + textStream, + mode = "typewriter", + speed = 20, + className = "", + onComplete, + as = "div", + fadeDuration, + segmentDelay, + characterChunkSize, +}: ResponseStreamProps) { + const animationEndRef = useRef<(() => void) | null>(null) + + const { + displayedText, + isComplete, + segments, + getFadeDuration, + getSegmentDelay, + } = useTextStream({ + textStream, + speed, + mode, + onComplete, + fadeDuration, + segmentDelay, + characterChunkSize, + }) + + useEffect(() => { + animationEndRef.current = onComplete ?? null + }, [onComplete]) + + const handleLastSegmentAnimationEnd = useCallback(() => { + if (animationEndRef.current && isComplete) { + animationEndRef.current() + } + }, [isComplete]) + + // fadeStyle is the style for the fade animation + const fadeStyle = ` + @keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } + } + + .fade-segment { + display: inline-block; + opacity: 0; + animation: fadeIn ${getFadeDuration()}ms ease-out forwards; + } + + .fade-segment-space { + white-space: pre; + } + ` + + const renderContent = () => { + switch (mode) { + case "typewriter": + return <>{displayedText} + + case "fade": + return ( + <> + +
    + {segments.map((segment, idx) => { + const isWhitespace = /^\s+$/.test(segment.text) + const isLastSegment = idx === segments.length - 1 + + return ( + + {segment.text} + + ) + })} +
    + + ) + + default: + return <>{displayedText} + } + } + + const Container = as as keyof React.JSX.IntrinsicElements + + return {renderContent()} +} + +export { useTextStream, ResponseStream } diff --git a/apps/app/src/components/ui/source.tsx b/apps/app/src/components/ui/source.tsx new file mode 100644 index 0000000000..59d127c5f3 --- /dev/null +++ b/apps/app/src/components/ui/source.tsx @@ -0,0 +1,119 @@ +"use client" + +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@/components/ui/hover-card" +import { cn } from "@/lib/utils" +import { createContext, useContext } from "react" + +const SourceContext = createContext<{ + href: string + domain: string +} | null>(null) + +function useSourceContext() { + const ctx = useContext(SourceContext) + if (!ctx) throw new Error("Source.* must be used inside ") + return ctx +} + +export type SourceProps = { + href: string + children: React.ReactNode +} + +export function Source({ href, children }: SourceProps) { + let domain = "" + try { + domain = new URL(href).hostname + } catch { + domain = href.split("/").pop() || href + } + + return ( + + + {children} + + + ) +} + +export type SourceTriggerProps = { + label?: string | number + showFavicon?: boolean + className?: string +} + +export function SourceTrigger({ + label, + showFavicon = false, + className, +}: SourceTriggerProps) { + const { href, domain } = useSourceContext() + const labelToShow = label ?? domain.replace("www.", "") + + return ( + }>{showFavicon && ( + favicon + )}{labelToShow} + ) +} + +export type SourceContentProps = { + title: string + description?: string + className?: string +} + +export function SourceContent({ + title, + description, + className, +}: SourceContentProps) { + const { href, domain } = useSourceContext() + + return ( + + +
    + favicon +
    + {domain.replace("www.", "")} +
    +
    +
    {title}
    +
    + {description ? description : No description available} +
    +
    +
    + ) +} diff --git a/apps/app/src/components/ui/steps.tsx b/apps/app/src/components/ui/steps.tsx new file mode 100644 index 0000000000..638bbda0f1 --- /dev/null +++ b/apps/app/src/components/ui/steps.tsx @@ -0,0 +1,120 @@ +"use client" + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible" +import { cn } from "@/lib/utils" +import { ChevronDown } from "lucide-react" + +export type StepsItemProps = React.ComponentProps<"div"> + +export const StepsItem = ({ + children, + className, + ...props +}: StepsItemProps) => ( +
    + {children} +
    +) + +export type StepsTriggerProps = React.ComponentProps< + typeof CollapsibleTrigger +> & { + leftIcon?: React.ReactNode + swapIconOnHover?: boolean +} + +export const StepsTrigger = ({ + children, + className, + leftIcon, + swapIconOnHover = true, + ...props +}: StepsTriggerProps) => ( + +
    + {leftIcon ? ( + + + {leftIcon} + + {swapIconOnHover && ( + + )} + + ) : null} + {children} +
    + {!leftIcon && ( + + )} +
    +) + +export type StepsContentProps = React.ComponentProps< + typeof CollapsibleContent +> & { + bar?: React.ReactNode +} + +export const StepsContent = ({ + children, + className, + bar, + ...props +}: StepsContentProps) => { + return ( + + {bar ? ( +
    +
    {bar}
    +
    {children}
    +
    + ) : ( +
    {children}
    + )} +
    + ) +} + +export type StepsBarProps = React.HTMLAttributes + +export const StepsBar = ({ className, ...props }: StepsBarProps) => ( +
    +) + +export type StepsProps = React.ComponentProps + +export function Steps({ defaultOpen = true, className, ...props }: StepsProps) { + return ( + + ) +} diff --git a/apps/app/src/components/ui/tool.tsx b/apps/app/src/components/ui/tool.tsx new file mode 100644 index 0000000000..cb3783721a --- /dev/null +++ b/apps/app/src/components/ui/tool.tsx @@ -0,0 +1,196 @@ +"use client" + +import { Button } from "@/components/ui/button" +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible" +import { cn } from "@/lib/utils" +import { + CheckCircle, + ChevronDown, + Loader2, + Settings, + XCircle, +} from "lucide-react" +import { useState } from "react" +import type { DynamicToolUIPart, ToolUIPart } from "ai" + +export type ToolPart = ToolUIPart | DynamicToolUIPart + +export type ToolProps = { + title?: string + toolPart: ToolPart + defaultOpen?: boolean + className?: string +} + +const Tool = ({ title, toolPart, defaultOpen = false, className }: ToolProps) => { + const [isOpen, setIsOpen] = useState(defaultOpen) + + const { state, input, output, toolCallId } = toolPart + + const getStateIcon = () => { + switch (state) { + case "input-streaming": + return + case "input-available": + return + case "output-available": + return + case "output-error": + return + default: + return + } + } + + const getStateBadge = () => { + const baseClasses = "px-2 py-1 rounded-full text-xs font-medium" + switch (state) { + case "input-streaming": + return ( + + Processing + + ) + case "input-available": + return ( + + Ready + + ) + case "output-available": + return ( + + Completed + + ) + case "output-error": + return ( + + Error + + ) + default: + return ( + + Pending + + ) + } + } + + const formatValue = (value: unknown): string => { + if (value === null) return "null" + if (value === undefined) return "undefined" + if (typeof value === "string") return value + if (typeof value === "object") { + return JSON.stringify(value, null, 2) + } + return String(value) + } + + const toolName = + toolPart.type === "dynamic-tool" ? toolPart.toolName : toolPart.type + + const hasInput = input !== null && input !== undefined + const hasOutput = "output" in toolPart && toolPart.output !== undefined + + return ( +
    + + }>
    + {getStateIcon()} + + {title || toolName} + + {getStateBadge()} +
    + +
    + {hasInput && ( +
    +

    + Input +

    +
    +
    +                    {formatValue(input)}
    +                  
    +
    +
    + )} + + {hasOutput && ( +
    +

    + Output +

    +
    +
    +                    {formatValue(toolPart.output)}
    +                  
    +
    +
    + )} + + {state === "output-error" && toolPart.errorText && ( +
    +

    Error

    +
    + {toolPart.errorText} +
    +
    + )} + + {state === "input-streaming" && ( +
    + Processing tool call... +
    + )} + + {toolCallId && ( +
    + Call ID: {toolCallId} +
    + )} +
    +
    +
    +
    + ) +} + +export { Tool } diff --git a/apps/app/src/i18n/locales/en.ts b/apps/app/src/i18n/locales/en.ts index ad5fadd1c5..0284713225 100644 --- a/apps/app/src/i18n/locales/en.ts +++ b/apps/app/src/i18n/locales/en.ts @@ -94,6 +94,13 @@ export default { "composer.placeholder": "Describe your task...", "composer.remote_worker_paste_warning": "This is a remote worker. Sandboxes are remote too. To share files with it, upload them to the Shared folder in the sidebar.", "composer.run_task": "Run task", + "composer.steer": "Steer", + "composer.steer_hint": "Send now and let the agent adjust mid-task", + "composer.queue": "Queue", + "composer.queue_hint": "Send once the agent finishes the current task", + "composer.queued_attachments_only": "{count} attachment(s)", + "composer.queued_count": "{count} queued", + "composer.escape_to_stop": "Hit Escape again to stop the agent", "composer.skill_source": "Skill", "composer.stop": "Stop", "composer.tools_label": "Commands, skills, and MCPs", diff --git a/apps/app/src/lib/artifacts.ts b/apps/app/src/lib/artifacts.ts new file mode 100644 index 0000000000..c41427da30 --- /dev/null +++ b/apps/app/src/lib/artifacts.ts @@ -0,0 +1,278 @@ +import type { UIMessage } from "ai"; +import * as React from "react"; + +import { + isApplyPatchToolPart, + isEditToolPart, + isWriteToolPart, +} from "@/lib/build-in-tools"; +import { useOpenTargets } from "@/lib/target-provider"; +import type { OpenTarget, OpenTargetPreview } from "@/react-app/domains/session/artifacts/open-target"; + +export type ArtifactType = "website" | "markdown" | "sheet" | "slides" | "document" | "image" | "video" | "audio" | "pdf" | "html" | "text" | "unknown"; + +export type ArtifactItem = { + id: string + name: string + path: string + type: ArtifactType + messageId: string + legacy_target: OpenTarget +} + +const WORKSPACES_PREFIX_PATTERN = /^workspaces\/[^/]+\//i; +const WORKSPACE_ID_PREFIX_PATTERN = /^workspace\/(?:ws_[^/]+|\d+|[0-9a-f-]{6,})\//i; + +export function isMarkdownPreviewSupported(extension: string) { + return ["md", "markdown", "mdx"].includes(extension); +} + +export function isSheetPreviewSupported(extension: string) { + return ["csv", "tsv", "xlsx", "xls", "ods"].includes(extension); +} + +export function isImagePreviewSupported(extension: string) { + return ["png", "jpg", "jpeg", "gif", "webp", "svg"].includes(extension); +} + +export function isPdfPreviewSupported(extension: string) { + return ["pdf"].includes(extension); +} + +export function isHtmlPreviewSupported(extension: string) { + return ["html", "htm"].includes(extension); +} + +export function isTextPreviewSupported(extension: string) { + return ["txt", "log", "json", "jsonc", "yaml", "yml", "toml", "xml", "ts", "tsx", "js", "jsx", "css", "scss"].includes(extension); +} + +export function isPreviewSupported(extension: string) { + return isMarkdownPreviewSupported(extension) || isSheetPreviewSupported(extension) || isImagePreviewSupported(extension) || isPdfPreviewSupported(extension) || isHtmlPreviewSupported(extension) || isTextPreviewSupported(extension); +} + +export function getArtifactType(filename: string): ArtifactType { + const extension = getFileExtension(filename); + + if (!extension) { + return "unknown"; + } + + if (["md", "markdown", "mdx", "rmd", "rst"].includes(extension)) { + return "markdown"; + } + + if (["csv", "tsv", "xlsx", "xls", "xlsm", "xlsb", "ods", "numbers"].includes(extension)) { + return "sheet"; + } + + if (["ppt", "pptx", "pptm", "pot", "potx", "odp", "key", "sxi"].includes(extension)) { + return "slides"; + } + + if (["doc", "docx", "odt", "rtf", "pages"].includes(extension)) { + return "document"; + } + + if (["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "ico", "avif", "heic", "heif", "tif", "tiff"].includes(extension)) { + return "image"; + } + + if (["mp4", "mov", "avi", "mkv", "webm", "wmv", "flv", "m4v", "ogv", "mpeg", "mpg", "3gp"].includes(extension)) { + return "video"; + } + + if (["mp3", "wav", "flac", "aac", "ogg", "oga", "m4a", "wma", "opus", "aiff", "aif", "mid", "midi"].includes(extension)) { + return "audio"; + } + + if (["pdf"].includes(extension)) { + return "pdf"; + } + + if (["html", "htm", "xhtml"].includes(extension)) { + return "html"; + } + + if (["txt", "log", "json", "jsonc", "json5", "yaml", "yml", "toml", "xml", "ini", "env", "ts", "tsx", "js", "jsx", "mjs", "cjs", "vue", "svelte", "css", "scss", "sass", "less", "py", "rb", "go", "rs", "java", "kt", "swift", "php", "c", "cpp", "h", "cs", "sql", "sh", "bash", "zsh"].includes(extension)) { + return "text"; + } + + return "unknown"; +} + +export function getFileExtension(filename: string) { + return filename.split(".").pop()?.toLowerCase(); +} + +const ARTIFACT_TYPE_LABELS: Record = { + website: "Website", + markdown: "Markdown", + sheet: "Spreadsheet", + slides: "Slides", + document: "Document", + image: "Image", + video: "Video", + audio: "Audio", + pdf: "PDF", + html: "HTML", + text: "Text", + unknown: "File", +}; + +export function getArtifactTypeLabel(type: ArtifactType) { + return ARTIFACT_TYPE_LABELS[type]; +} + +function getArtifactName(path: string) { + const segments = path.split(/[/\\]/); + + return segments[segments.length - 1] ?? path; +} + +function normalizeArtifactPath(path: string) { + return path + .trim() + .replace(/[\\]+/g, "/") + .replace(/^\.\//, "") + .replace(WORKSPACES_PREFIX_PATTERN, "") + .replace(WORKSPACE_ID_PREFIX_PATTERN, ""); +} + +function artifactTypeToPreview(type: ArtifactType): OpenTargetPreview { + if (type === "markdown") return "markdown"; + if (type === "sheet") return "sheet"; + if (type === "image") return "image"; + if (type === "pdf") return "pdf"; + if (type === "html") return "html"; + if (type === "text") return "text"; + if (type === "website") return "browser"; + return "external"; +} + +function artifactPathMatchesTarget(path: string, targetValue: string) { + const normalized = normalizeArtifactPath(path).toLowerCase(); + const target = normalizeArtifactPath(targetValue).toLowerCase(); + return normalized === target || normalized.endsWith(`/${target}`); +} + +function openTargetFromArtifactPath( + path: string, + name: string, + type: ArtifactType, + verifiedTargets: OpenTarget[], +): OpenTarget { + const normalized = normalizeArtifactPath(path); + const id = `file:${normalized.toLowerCase()}`; + const verified = verifiedTargets.find( + (target) => target.id === id || artifactPathMatchesTarget(normalized, target.value), + ); + + return verified ?? { + id, + kind: "file", + value: normalized, + name, + preview: artifactTypeToPreview(type), + confidence: 95, + reason: "artifact", + }; +} + +function parseApplyPatchPaths(patchText: string) { + const paths: string[] = []; + + for (const line of patchText.split("\n")) { + if (line.startsWith("*** Add File:")) { + paths.push(line.slice("*** Add File:".length).trim()); + continue; + } + + if (line.startsWith("*** Update File:")) { + paths.push(line.slice("*** Update File:".length).trim()); + continue; + } + + if (line.startsWith("*** Move to:")) { + paths.push(line.slice("*** Move to:".length).trim()); + } + } + + return paths; +} + +function getArtifactPathsFromMessage(message: UIMessage) { + const paths: (string | undefined)[] = []; + + for (const part of message.parts) { + if (part.type === "source-document") { + if (part.filename) { + paths.push(part.filename); + } else { + paths.push(part.title); + } + continue; + } + + if (part.type !== "dynamic-tool" || part.state !== "output-available") { + continue; + } + + if (isWriteToolPart(part)) { + paths.push(part.input.filePath); + continue; + } + + if (isEditToolPart(part)) { + paths.push(part.input.filePath); + continue; + } + if (isApplyPatchToolPart(part)) { + paths.push(...parseApplyPatchPaths(part.input.patchText)); + } + } + + return paths.map((path) => path?.trim().toLowerCase()).filter((path) => path) as string[]; +} + +export function getArtifactsFromMessages(messages: UIMessage[], openTargets: OpenTarget[] = []) { + const artifacts = new Map(); + + for (const message of messages) { + for (const path of getArtifactPathsFromMessage(message)) { + const name = getArtifactName(path); + const type = getArtifactType(path); + artifacts.set(path, { + id: path, + name, + path, + type, + messageId: message.id, + legacy_target: openTargetFromArtifactPath(path, name, type, openTargets), + }); + } + } + + return [...artifacts.values()]; +} + +export function useArtifacts(messages: UIMessage[]) { + const { openTargets } = useOpenTargets(); + + return React.useMemo( + () => getArtifactsFromMessages(messages, openTargets), + [messages, openTargets], + ); +} + +export function usePreviewArtifact() { + const { onOpenTarget } = useOpenTargets(); + + return React.useCallback((artifact: ArtifactItem) => { + async function previewArtifact() { + onOpenTarget?.(artifact.legacy_target); + } + + void previewArtifact(); + }, [onOpenTarget]); +} diff --git a/apps/app/src/lib/build-in-tools.ts b/apps/app/src/lib/build-in-tools.ts new file mode 100644 index 0000000000..29edb197ac --- /dev/null +++ b/apps/app/src/lib/build-in-tools.ts @@ -0,0 +1,370 @@ +import type { ToolUIPart, DynamicToolUIPart } from "ai"; + +export interface ToolMetadata { + truncated?: boolean; + outputPath?: string; +} + +export interface InvalidInput { + tool: string; + error: string; +} + +export interface InvalidMetadata extends ToolMetadata {} + +export interface QuestionOption { + label: string; + description: string; +} + +export interface QuestionPrompt { + question: string; + header: string; + options: QuestionOption[]; + multiple?: boolean; +} + +export interface QuestionInput { + questions: QuestionPrompt[]; +} + +export interface QuestionMetadata extends ToolMetadata { + answers: string[][]; +} + +export interface BashInput { + command: string; + timeout?: number; + workdir?: string; + description: string; +} + +export interface BashMetadata extends ToolMetadata { + output: string; + exit: number | null; + description: string; + truncated: boolean; +} + +export interface ReadInput { + filePath: string; + offset?: number; + limit?: number; +} + +export interface ReadMetadata extends ToolMetadata { + preview: string; + truncated: boolean; + loaded: string[]; +} + +export interface GlobInput { + pattern: string; + path?: string; +} + +export interface GlobMetadata extends ToolMetadata { + count: number; + truncated: boolean; +} + +export interface GrepInput { + pattern: string; + path?: string; + include?: string; +} + +export interface GrepMetadata extends ToolMetadata { + matches: number; + truncated: boolean; +} + +export interface FileDiff { + file: string; + patch: string; + additions: number; + deletions: number; +} + +export interface EditInput { + filePath: string; + oldString: string; + newString: string; + replaceAll?: boolean; +} + +export interface EditMetadata extends ToolMetadata { + diagnostics: unknown; + diff: string; + filediff: FileDiff; +} + +export interface WriteInput { + content: string; + filePath: string; +} + +export interface WriteMetadata extends ToolMetadata { + diagnostics: unknown; + filepath: string; + exists: boolean; +} + +export interface TaskModel { + modelID: string; + providerID: string; +} + +export interface TaskInput { + description: string; + prompt: string; + subagent_type: string; + task_id?: string; + command?: string; + background?: boolean; +} + +export interface TaskMetadata extends ToolMetadata { + parentSessionId: string; + sessionId: string; + model: TaskModel; + background?: true; + jobId?: string; +} + +export type TaskStatusState = "running" | "completed" | "error" | "cancelled"; + +export interface TaskStatusInput { + task_id: string; + wait?: boolean; + timeout_ms?: number; +} + +export interface TaskStatusMetadata extends ToolMetadata { + task_id: string; + state: TaskStatusState; + timed_out: boolean; +} + +export type WebFetchFormat = "text" | "markdown" | "html"; + +export interface WebFetchInput { + url: string; + format?: WebFetchFormat; + timeout?: number; +} + +export interface WebFetchMetadata extends ToolMetadata {} + +export interface TodoItem { + content: string; + status: string; + priority: string; +} + +export interface TodoWriteInput { + todos: TodoItem[]; +} + +export interface TodoWriteMetadata extends ToolMetadata { + todos: TodoItem[]; +} + +export type WebSearchLivecrawl = "fallback" | "preferred"; +export type WebSearchType = "auto" | "fast" | "deep"; +export type WebSearchProvider = "exa" | "parallel"; + +export interface WebSearchInput { + query: string; + numResults?: number; + livecrawl?: WebSearchLivecrawl; + type?: WebSearchType; + contextMaxCharacters?: number; +} + +export interface WebSearchMetadata extends ToolMetadata { + provider: WebSearchProvider; +} + +export type RepoCloneStatus = "cached" | "cloned" | "refreshed"; + +export interface RepoCloneInput { + repository: string; + refresh?: boolean; + branch?: string; +} + +export interface RepoCloneMetadata extends ToolMetadata { + repository: string; + host: string; + remote: string; + localPath: string; + status: RepoCloneStatus; + head?: string; + branch?: string; +} + +export interface RepoOverviewInput { + repository?: string; + path?: string; + depth?: number; +} + +export interface RepoOverviewMetadata extends ToolMetadata { + path: string; + repository?: string; + branch?: string; + head?: string; + package_manager?: string; + ecosystems: string[]; + dependency_files: string[]; + entrypoints: string[]; + depth: number; + truncated: boolean; +} + +export interface SkillInput { + name: string; +} + +export interface SkillMetadata extends ToolMetadata { + name: string; + dir: string; +} + +export interface ApplyPatchInput { + patchText: string; +} + +export type ApplyPatchFileType = "add" | "update" | "delete" | "move"; + +export interface ApplyPatchFile { + filePath: string; + relativePath: string; + type: ApplyPatchFileType; + patch: string; + additions: number; + deletions: number; + movePath?: string; +} + +export interface ApplyPatchMetadata extends ToolMetadata { + diff: string; + files: ApplyPatchFile[]; + diagnostics: unknown; +} + +export type LspOperation = + | "goToDefinition" + | "findReferences" + | "hover" + | "documentSymbol" + | "workspaceSymbol" + | "goToImplementation" + | "prepareCallHierarchy" + | "incomingCalls" + | "outgoingCalls"; + +export interface LspInput { + operation: LspOperation; + filePath: string; + line: number; + character: number; + query?: string; +} + +export interface LspMetadata extends ToolMetadata { + result: unknown[]; +} + +export interface PlanExitInput {} + +export interface PlanExitMetadata extends ToolMetadata {} + +type BuiltInDynamicToolPart = + DynamicToolUIPart & { toolName: ToolName } & ( + | { state: "output-available"; input: Input; output: Output } + | { state: "output-error"; input: Input; errorText: string } + | { + state: Exclude; + input: Input; + } + ); + +export type BashToolPart = BuiltInDynamicToolPart<"bash", BashInput>; + +export function isBashToolPart(part: ToolUIPart | DynamicToolUIPart): part is BashToolPart { + return part.type === "dynamic-tool" && part.toolName === "bash"; +} + +export type EditToolPart = BuiltInDynamicToolPart<"edit", EditInput>; + +export function isEditToolPart(part: ToolUIPart | DynamicToolUIPart): part is EditToolPart { + return part.type === "dynamic-tool" && part.toolName === "edit"; +} + +export type WriteToolPart = BuiltInDynamicToolPart<"write", WriteInput>; + +export function isWriteToolPart(part: ToolUIPart | DynamicToolUIPart): part is WriteToolPart { + return part.type === "dynamic-tool" && part.toolName === "write"; +} + +export type ReadToolPart = BuiltInDynamicToolPart<"read", ReadInput>; + +export function isReadToolPart(part: ToolUIPart | DynamicToolUIPart): part is ReadToolPart { + return part.type === "dynamic-tool" && part.toolName === "read"; +} + +export type GrepToolPart = BuiltInDynamicToolPart<"grep", GrepInput>; + +export function isGrepToolPart(part: ToolUIPart | DynamicToolUIPart): part is GrepToolPart { + return part.type === "dynamic-tool" && part.toolName === "grep"; +} + +export type GlobToolPart = BuiltInDynamicToolPart<"glob", GlobInput>; + +export function isGlobToolPart(part: ToolUIPart | DynamicToolUIPart): part is GlobToolPart { + return part.type === "dynamic-tool" && part.toolName === "glob"; +} + +export type LspToolPart = BuiltInDynamicToolPart<"lsp", LspInput>; + +export function isLspToolPart(part: ToolUIPart | DynamicToolUIPart): part is LspToolPart { + return part.type === "dynamic-tool" && part.toolName === "lsp"; +} + +export type ApplyPatchToolPart = BuiltInDynamicToolPart<"apply_patch", ApplyPatchInput>; + +export function isApplyPatchToolPart(part: ToolUIPart | DynamicToolUIPart): part is ApplyPatchToolPart { + return part.type === "dynamic-tool" && part.toolName === "apply_patch"; +} + +export type SkillToolPart = BuiltInDynamicToolPart<"skill", SkillInput>; + +export function isSkillToolPart(part: ToolUIPart | DynamicToolUIPart): part is SkillToolPart { + return part.type === "dynamic-tool" && part.toolName === "skill"; +} + +export type TodoWriteToolPart = BuiltInDynamicToolPart<"todowrite", TodoWriteInput>; + +export function isTodoWriteToolPart(part: ToolUIPart | DynamicToolUIPart): part is TodoWriteToolPart { + return part.type === "dynamic-tool" && part.toolName === "todowrite"; +} + +export type WebFetchToolPart = BuiltInDynamicToolPart<"webfetch", WebFetchInput>; + +export function isWebFetchToolPart(part: ToolUIPart | DynamicToolUIPart): part is WebFetchToolPart { + return part.type === "dynamic-tool" && part.toolName === "webfetch"; +} + +export type WebSearchToolPart = BuiltInDynamicToolPart<"websearch", WebSearchInput>; + +export function isWebSearchToolPart(part: ToolUIPart | DynamicToolUIPart): part is WebSearchToolPart { + return part.type === "dynamic-tool" && part.toolName === "websearch"; +} + +export type QuestionToolPart = BuiltInDynamicToolPart<"question", QuestionInput>; + +export function isQuestionToolPart(part: ToolUIPart | DynamicToolUIPart): part is QuestionToolPart { + return part.type === "dynamic-tool" && part.toolName === "question"; +} diff --git a/apps/app/src/lib/messages.ts b/apps/app/src/lib/messages.ts new file mode 100644 index 0000000000..316b117b02 --- /dev/null +++ b/apps/app/src/lib/messages.ts @@ -0,0 +1 @@ +export type ThreadStatus = "submitted" | "streaming" | "retrying" | "ready"; diff --git a/apps/app/src/lib/target-provider.ts b/apps/app/src/lib/target-provider.ts new file mode 100644 index 0000000000..9ee598be1e --- /dev/null +++ b/apps/app/src/lib/target-provider.ts @@ -0,0 +1,43 @@ +import * as React from "react"; + +import type { OpenTarget } from "@/react-app/domains/session/artifacts/open-target"; + +export type OpenTargetHandler = (target: OpenTarget, options?: { auto?: boolean }) => void; + +type OpenTargetContextValue = { + openTargets: OpenTarget[]; + onOpenTarget: OpenTargetHandler | undefined; +}; + +type OpenTargetProviderProps = { + children: React.ReactNode; + openTargets?: OpenTarget[] | undefined; + onOpenTarget?: OpenTargetHandler | undefined; +}; + +const EMPTY_OPEN_TARGETS: OpenTarget[] = []; + +const OpenTargetContext = React.createContext({ + openTargets: EMPTY_OPEN_TARGETS, + onOpenTarget: undefined, +}); + +export function OpenTargetProvider({ + children, + openTargets = EMPTY_OPEN_TARGETS, + onOpenTarget, +}: OpenTargetProviderProps) { + const value = React.useMemo( + () => ({ + openTargets, + onOpenTarget, + }), + [openTargets, onOpenTarget], + ); + + return React.createElement(OpenTargetContext.Provider, { value }, children); +} + +export function useOpenTargets() { + return React.useContext(OpenTargetContext); +} diff --git a/apps/app/src/lib/websearch-results.ts b/apps/app/src/lib/websearch-results.ts new file mode 100644 index 0000000000..01494e9c37 --- /dev/null +++ b/apps/app/src/lib/websearch-results.ts @@ -0,0 +1,33 @@ +export interface WebSearchResult { + title: string + url: string + description?: string +} + +const TITLE_PATTERN = /^Title:\s*(.+)$/m +const URL_PATTERN = /^URL:\s*(https?:\/\/\S+)$/m +const HIGHLIGHTS_PATTERN = /(?:^|\n)Highlights:\s*([\s\S]*?)(?:\n\[\.\.\.\]|$)/ + +export function parseWebSearchResults(output: string): WebSearchResult[] { + return output.split(/\n---\n/).flatMap((block) => { + const title = block.match(TITLE_PATTERN)?.[1]?.trim() + const url = block.match(URL_PATTERN)?.[1]?.trim() + const description = block.match(HIGHLIGHTS_PATTERN)?.[1]?.trim() + + if (!url) { + return [] + } + + try { + return [ + { + title: title && title !== "N/A" ? title : new URL(url).hostname, + url, + description, + }, + ] + } catch { + return [] + } + }) +} diff --git a/apps/app/src/react-app/domains/session/artifacts/open-target.ts b/apps/app/src/react-app/domains/session/artifacts/open-target.ts index 6d7af4091d..e5ff4ea3d7 100644 --- a/apps/app/src/react-app/domains/session/artifacts/open-target.ts +++ b/apps/app/src/react-app/domains/session/artifacts/open-target.ts @@ -50,6 +50,7 @@ const WRITE_TOOL_NAMES = new Set([ const FILE_METADATA_KEYS = ["path", "file", "filePath", "filepath"]; const PATCH_FILE_PATTERN = /^\*\*\* (?:Add File|Update File):\s*(.+)$/gmi; const PATCH_MOVE_TO_PATTERN = /^\*\*\* Move to:\s*(.+)$/gmi; +const URI_PATTERN = /^(?:https?|wss?|file):\/\//i; type DeriveOpenTargetsOptions = { includeFileMentions?: boolean; @@ -243,6 +244,18 @@ export function deriveOpenTargets(messages: UIMessage[], options: DeriveOpenTarg continue; } + if (part.type === "source-document") { + addTarget( + targets, + part.filename + ? targetFromFile(part.filename, 95, "attachment source") + : URI_PATTERN.test(part.title) + ? targetFromUrl(part.title, 95, "attachment source") + : targetFromFile(part.title, 95, "attachment source"), + ); + continue; + } + if (part.type !== "dynamic-tool") { continue; } diff --git a/apps/app/src/react-app/domains/session/modals/queued-messages-panel.tsx b/apps/app/src/react-app/domains/session/modals/queued-messages-panel.tsx new file mode 100644 index 0000000000..69ecaebd7d --- /dev/null +++ b/apps/app/src/react-app/domains/session/modals/queued-messages-panel.tsx @@ -0,0 +1,56 @@ +/** @jsxImportSource react */ +import { ListPlus, X } from "lucide-react"; + +import { t } from "@/i18n"; + +export type QueuedMessagesPanelProps = { + messages: string[]; + onRemove: (index: number) => void; +}; + +/** + * Shows the follow-up messages the user has queued while the agent is busy. + * Rendered above the composer (mirrors the QuestionPanel header style). Each + * entry can be removed with an X. The whole panel hides when the queue is + * empty — callers should simply not render it in that case, but we also guard + * here for safety. + */ +export function QueuedMessagesPanel(props: QueuedMessagesPanelProps) { + if (props.messages.length === 0) return null; + + return ( +
    +
    +
    +
    + +
    +
    + {t("composer.queued_count", { count: props.messages.length })} +
    +
    +
    + +
    + {props.messages.map((message, index) => ( +
    +
    + {message} +
    + +
    + ))} +
    +
    + ); +} diff --git a/apps/app/src/react-app/domains/session/status/session-activity-store.ts b/apps/app/src/react-app/domains/session/status/session-activity-store.ts index b313cc7c82..9a3f9e6751 100644 --- a/apps/app/src/react-app/domains/session/status/session-activity-store.ts +++ b/apps/app/src/react-app/domains/session/status/session-activity-store.ts @@ -12,6 +12,7 @@ type SessionActivityRecord = { runActive: boolean; assistantOutput: boolean; errorActive: boolean; + errorMessage: string | null; compacting: boolean; waitingPermissionIds: string[]; waitingQuestionIds: string[]; @@ -30,6 +31,7 @@ type SessionActivityStore = { recordsByWorkspaceId: Record>; statusesByWorkspaceId: Record>; getStatus: (workspaceId: string, sessionId: string) => SessionActivityStatus; + getSessionError: (workspaceId: string, sessionId: string) => string | null; seedWorkspaceSessions: (workspaceId: string, sessions: SessionLike[]) => void; seedSessionRun: (workspaceId: string, sessionId: string, status: unknown, assistantOutput: boolean) => void; setRunStatus: (workspaceId: string, sessionId: string, status: unknown) => void; @@ -37,17 +39,40 @@ type SessionActivityStore = { markAssistantOutput: (workspaceId: string, sessionId: string, messageId?: string, options?: { allowUnknownMessageRole?: boolean }) => void; setWaitingRequest: (workspaceId: string, sessionId: string, kind: "permission" | "question", requestId: string, waiting: boolean) => void; replaceWaitingRequests: (workspaceId: string, sessionId: string, kind: "permission" | "question", requestIds: string[]) => void; - setError: (workspaceId: string, sessionId: string) => void; + setError: (workspaceId: string, sessionId: string, message?: string) => void; clearError: (workspaceId: string, sessionId: string) => void; setCompacting: (workspaceId: string, sessionId: string, compacting: boolean) => void; removeSession: (workspaceId: string, sessionId: string) => void; }; +export function sessionErrorMessageFromProperties(properties: unknown): string { + if (!properties || typeof properties !== "object") { + return "Session failed"; + } + + const record = properties as Record; + + if (typeof record.error === "object" && record.error !== null && "message" in record.error) { + const message = record.error.message; + + if (typeof message === "string" && message.trim()) { + return message.trim(); + } + } + + if (typeof record.error === "string" && record.error.trim()) { + return record.error.trim(); + } + + return "Session failed"; +} + const createRecord = (): SessionActivityRecord => ({ status: "idle", runActive: false, assistantOutput: false, errorActive: false, + errorMessage: null, compacting: false, waitingPermissionIds: [], waitingQuestionIds: [], @@ -134,6 +159,22 @@ export const useSessionActivityStore = create((set, get) = getStatus: (workspaceId, sessionId) => ( get().statusesByWorkspaceId[workspaceId]?.[sessionId] ?? "idle" ), + getSessionError: (workspaceId, sessionId) => { + const workspace = workspaceId.trim(); + const session = sessionId.trim(); + + if (!workspace || !session) { + return null; + } + + const record = get().recordsByWorkspaceId[workspace]?.[session]; + + if (!record?.errorActive) { + return null; + } + + return record.errorMessage; + }, seedWorkspaceSessions: (workspaceId, sessions) => { const id = workspaceId.trim(); if (!id) return; @@ -155,6 +196,7 @@ export const useSessionActivityStore = create((set, get) = runActive, assistantOutput: runActive && record.runActive ? record.assistantOutput : false, errorActive: runActive ? false : record.errorActive, + errorMessage: runActive ? null : record.errorMessage, compacting: runActive ? record.compacting : false, waitingPermissionIds: runActive ? record.waitingPermissionIds : [], waitingQuestionIds: runActive ? record.waitingQuestionIds : [], @@ -178,6 +220,7 @@ export const useSessionActivityStore = create((set, get) = runActive, assistantOutput: runActive && assistantOutput, errorActive: runActive ? false : record.errorActive, + errorMessage: runActive ? null : record.errorMessage, compacting: runActive ? record.compacting : false, waitingPermissionIds: runActive ? record.waitingPermissionIds : [], waitingQuestionIds: runActive ? record.waitingQuestionIds : [], @@ -196,6 +239,7 @@ export const useSessionActivityStore = create((set, get) = runActive, assistantOutput: runActive && record.runActive ? record.assistantOutput : false, errorActive: runActive ? false : record.errorActive, + errorMessage: runActive ? null : record.errorMessage, compacting: runActive ? record.compacting : false, waitingPermissionIds: runActive ? record.waitingPermissionIds : [], waitingQuestionIds: runActive ? record.waitingQuestionIds : [], @@ -250,13 +294,14 @@ export const useSessionActivityStore = create((set, get) = [kind === "permission" ? "waitingPermissionIds" : "waitingQuestionIds"]: ids, }))); }, - setError: (workspaceId, sessionId) => { + setError: (workspaceId, sessionId, message) => { const workspace = workspaceId.trim(); const session = sessionId.trim(); if (!workspace || !session) return; set((state) => updateRecord(state, workspace, session, (record) => ({ ...record, errorActive: true, + errorMessage: message ? message : "Session failed", runActive: false, assistantOutput: false, compacting: false, @@ -269,6 +314,7 @@ export const useSessionActivityStore = create((set, get) = set((state) => updateRecord(state, workspace, session, (record) => ({ ...record, errorActive: false, + errorMessage: null, }))); }, setCompacting: (workspaceId, sessionId, compacting) => { @@ -279,6 +325,7 @@ export const useSessionActivityStore = create((set, get) = ...record, compacting, errorActive: compacting ? false : record.errorActive, + errorMessage: compacting ? null : record.errorMessage, }))); }, removeSession: (workspaceId, sessionId) => { diff --git a/apps/app/src/react-app/domains/session/surface/composer/composer.tsx b/apps/app/src/react-app/domains/session/surface/composer/composer.tsx index f761d410c3..5d6306dde7 100644 --- a/apps/app/src/react-app/domains/session/surface/composer/composer.tsx +++ b/apps/app/src/react-app/domains/session/surface/composer/composer.tsx @@ -1,7 +1,7 @@ /** @jsxImportSource react */ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import type { Agent } from "@opencode-ai/sdk/v2/client"; -import { ArrowUp, ChevronRight, FileText, Paperclip, Plug, Settings, Square, Terminal, X, Zap } from "lucide-react"; +import { ArrowUp, ChevronRight, FileText, ListPlus, Paperclip, Plug, Settings, Square, Terminal, X, Zap } from "lucide-react"; import fuzzysort from "fuzzysort"; import { OPENWORK_EXTENSION_CATALOG, type McpDirectoryInfo } from "../../../../../app/constants"; import type { CloudImportedPlugin, CloudImportedPluginFile } from "../../../../../app/cloud/import-state"; @@ -47,8 +47,11 @@ type ComposerProps = { mentions: Record; onDraftChange: (value: string) => void; onSend: () => void | Promise; + onSteer: () => void | Promise; + onQueue: () => void | Promise; onStop: () => void | Promise; busy: boolean; + queuedCount: number; disabled: boolean; modelUnavailable?: boolean; statusLabel: string; @@ -324,6 +327,50 @@ export function ReactSessionComposer(props: ComposerProps) { draftRef.current = props.draft; }, [props.draft]); + // Follow-up message UX (only relevant while the agent is busy): + // - Enter does NOT submit; instead it shakes the Steer/Queue buttons. + // - Escape arms a "Hit Escape again to stop the agent" prompt for 3s; + // a second Escape within that window stops the agent. + const [followupShake, setFollowupShake] = useState(false); + const [escapeArmed, setEscapeArmed] = useState(false); + const shakeTimerRef = useRef | null>(null); + const escapeTimerRef = useRef | null>(null); + + const triggerFollowupShake = useCallback(() => { + if (shakeTimerRef.current) clearTimeout(shakeTimerRef.current); + setFollowupShake(true); + shakeTimerRef.current = setTimeout(() => setFollowupShake(false), 450); + }, []); + + const disarmEscape = useCallback(() => { + if (escapeTimerRef.current) { + clearTimeout(escapeTimerRef.current); + escapeTimerRef.current = null; + } + setEscapeArmed(false); + }, []); + + // Reset the escape-to-stop prompt whenever the agent stops being busy. + useEffect(() => { + if (!props.busy) disarmEscape(); + }, [props.busy, disarmEscape]); + + useEffect(() => () => { + if (shakeTimerRef.current) clearTimeout(shakeTimerRef.current); + if (escapeTimerRef.current) clearTimeout(escapeTimerRef.current); + }, []); + + // Editor submit (Enter). While idle this sends normally; while busy a + // follow-up message must be explicitly Steered or Queued, so Enter only + // nudges the buttons. + const handleEditorSubmit = useCallback(() => { + if (props.busy) { + triggerFollowupShake(); + return; + } + void props.onSend(); + }, [props.busy, props.onSend, triggerFollowupShake]); + const slashMatch = props.draft.match(/^\/(\S*)$/); const slashOpenNext = Boolean(slashMatch); const slashQuery = slashMatch?.[1] ?? ""; @@ -758,6 +805,25 @@ export function ReactSessionComposer(props: ComposerProps) { if (event.key === "Enter" && imeActive) { return; } + // Escape-to-stop while the agent is busy. Only when no menu is open so + // Escape can still close menus. First press arms a confirmation prompt + // for 3s; a second Escape within that window stops the agent. + const anyMenuOpen = agentMenuOpen || toolMenuOpen || Boolean(activeMenu); + if (event.key === "Escape" && props.busy && !anyMenuOpen) { + event.preventDefault(); + if (escapeArmed) { + disarmEscape(); + void props.onStop(); + } else { + setEscapeArmed(true); + if (escapeTimerRef.current) clearTimeout(escapeTimerRef.current); + escapeTimerRef.current = setTimeout(() => { + setEscapeArmed(false); + escapeTimerRef.current = null; + }, 3000); + } + return; + } if (agentMenuOpen) { const total = agents.length + 1; if (event.key === "ArrowDown") { @@ -1054,7 +1120,7 @@ export function ReactSessionComposer(props: ComposerProps) { disabled={props.disabled} placeholder={t("composer.placeholder")} onChange={props.onDraftChange} - onSubmit={props.onSend} + onSubmit={handleEditorSubmit} onExpandPastedText={handleExpandPastedText} onPasteText={props.onPasteText} onPaste={(event) => { @@ -1369,27 +1435,72 @@ export function ReactSessionComposer(props: ComposerProps) {
    {/* - Single action button that toggles between Stop and Run task. - When busy with no draft: Stop (cancels current run). - When busy with a draft: Run task (queues a follow-up). - When idle: Run task. + Action area. + - Idle: single "Run task" button (sends immediately). + - Busy: follow-up controls — "Steer" sends now (the agent + adjusts mid-task), "Queue" sends once the agent is idle, + and an outline "Stop" cancels the run. Steer/Queue are + disabled until there's something to send. Pressing Enter + while busy shakes Steer/Queue to prompt an explicit choice. + Escape arms a "Hit Escape again to stop the agent" prompt. */}
    - {props.busy && !canSend ? ( - + {props.busy ? ( + <> + {escapeArmed ? ( + + {t("composer.escape_to_stop")} + + ) : null} +
    + + +
    + + ) : ( + ); +}); + +type JumpToLatestButtonProps = { + onJumpToLatest: (behavior?: ScrollBehavior) => void; +}; + +const JumpToLatestButton = memo(function JumpToLatestButton({ + onJumpToLatest, +}: JumpToLatestButtonProps) { + const handleClick = useCallback(() => { + onJumpToLatest("smooth"); + }, [onJumpToLatest]); + + return ( + + ); +}); + +type SessionScrollOverlayProps = { + sessionId: string; + isStreaming: boolean; + onJumpToLatest: (behavior?: ScrollBehavior) => void; + onJumpToStartOfMessage: (behavior?: ScrollBehavior) => void; +}; + +export const SessionScrollOverlay = memo(function SessionScrollOverlay({ + sessionId, + isStreaming, + onJumpToLatest, + onJumpToStartOfMessage, +}: SessionScrollOverlayProps) { + const { isAtBottom, topClippedMessageId } = useSessionScrollOverlayState(sessionId); + const showJumpToStart = !isStreaming && Boolean(topClippedMessageId); + const showJumpToLatest = !isAtBottom; + + if (!showJumpToStart && !showJumpToLatest) { + return null; + } + + return ( +
    +
    + {showJumpToStart ? ( + + ) : null} + {showJumpToLatest ? ( + + ) : null} +
    +
    + ); +}); diff --git a/apps/app/src/react-app/domains/session/surface/scroll-store.ts b/apps/app/src/react-app/domains/session/surface/scroll-store.ts index 19c1575cdc..d2656ad48b 100644 --- a/apps/app/src/react-app/domains/session/surface/scroll-store.ts +++ b/apps/app/src/react-app/domains/session/surface/scroll-store.ts @@ -88,6 +88,20 @@ export function getSessionScrollState( return sessions[sessionId] ?? INITIAL_SESSION_SCROLL_STATE; } +export function selectSessionIsStickyBottom( + sessions: SessionScrollStateById, + sessionId: string | null | undefined, +): boolean { + return getSessionScrollState(sessions, sessionId).mode === "stickyBottom"; +} + +export function selectSessionTopClippedMessageId( + sessions: SessionScrollStateById, + sessionId: string | null | undefined, +): string | null { + return getSessionScrollState(sessions, sessionId).topClippedMessageId; +} + function setSessionStickyBottom( sessions: SessionScrollStateById, sessionId: string | null | undefined, diff --git a/apps/app/src/react-app/domains/session/surface/session-surface.tsx b/apps/app/src/react-app/domains/session/surface/session-surface.tsx index 8d2b13244b..79b4146bb9 100644 --- a/apps/app/src/react-app/domains/session/surface/session-surface.tsx +++ b/apps/app/src/react-app/domains/session/surface/session-surface.tsx @@ -44,9 +44,11 @@ import { SessionTranscript } from "./message-list"; import { useLocal } from "../../../kernel/local-provider"; import { deriveSessionRenderModel } from "../sync/transition-controller"; import { useSessionScrollController } from "./scroll-controller"; +import { SessionScrollOverlay } from "./scroll-overlay"; import { getSessionActivityStatusLabel, useSessionActivityStore, type SessionActivityStatus } from "../status/session-activity-store"; import { PermissionApprovalPanel } from "../chat/permission-approval-modal"; import { QuestionPanel } from "../modals/question-modal"; +import { QueuedMessagesPanel } from "../modals/queued-messages-panel"; import { deriveOpenTargets, selectAutoOpenTarget, type OpenTarget } from "../artifacts/open-target"; import { usePanelTabStore } from "../panel/panel-tab-store"; import { @@ -61,6 +63,10 @@ import { getComposerPasteParts, useComposerStateStore, } from "./composer-state-store"; +import { MessageList } from "@/components/chat/message-list"; +import { MessageListProvider, type DispatchAction } from "@/components/chat/message-list-provider"; +import { OpenTargetProvider } from "@/lib/target-provider"; +import type { ThreadStatus } from "@/lib/messages"; const EMPTY_TRANSCRIPT: UIMessage[] = []; const IDLE_STATUS: SessionStatus = { type: "idle" }; @@ -416,6 +422,34 @@ function revokeAttachmentPreview(attachment: { previewUrl?: string | undefined } URL.revokeObjectURL(attachment.previewUrl); } +// Combine multiple queued follow-up drafts into a single send. Their text and +// parts are concatenated with blank-line separators and attachments are +// merged, so the whole queue is delivered to the agent as one message. +function mergeDrafts(drafts: ComposerDraft[]): ComposerDraft | null { + if (drafts.length === 0) return null; + if (drafts.length === 1) return drafts[0] ?? null; + const separator: ComposerPart = { type: "text", text: "\n\n" }; + const parts: ComposerPart[] = []; + const attachments: ComposerAttachment[] = []; + const texts: string[] = []; + const resolvedTexts: string[] = []; + drafts.forEach((draft, index) => { + if (index > 0) parts.push(separator); + parts.push(...draft.parts); + attachments.push(...draft.attachments); + texts.push(draft.text); + resolvedTexts.push(draft.resolvedText ?? draft.text); + }); + return { + mode: "prompt", + parts, + attachments, + text: texts.join("\n\n"), + resolvedText: resolvedTexts.join("\n\n"), + command: undefined, + }; +} + export function SessionSurface(props: SessionSurfaceProps) { const local = useLocal(); const { config: shellConfig } = useShellConfig(); @@ -435,6 +469,10 @@ export function SessionSurface(props: SessionSurfaceProps) { const [notice, setNotice] = useState(null); const [error, setError] = useState(null); const [sending, setSending] = useState(false); + // Locally queued follow-up drafts. OpenCode has no server-side queue, so we + // hold these client-side and auto-send the first one once the session goes + // idle (see the drain effect below). + const [queuedDrafts, setQueuedDrafts] = useState([]); const [showDelayedLoading, setShowDelayedLoading] = useState(false); const [awaitingAssistantBaseline, setAwaitingAssistantBaseline] = useState(null); const [noVisibleAssistantOutputBaseline, setNoVisibleAssistantOutputBaseline] = useState(null); @@ -567,6 +605,21 @@ export function SessionSurface(props: SessionSurfaceProps) { }); const liveStatus = statusState ?? snapshot?.status ?? IDLE_STATUS; const chatStreaming = sending || liveStatus.type === "busy" || liveStatus.type === "retry"; + const status = useMemo((): ThreadStatus => { + if (sending) { + return "submitted"; + } + + if (liveStatus.type === "busy") { + return "streaming"; + } + + if (liveStatus.type === "retry") { + return "retrying"; + } + + return "ready"; + }, [liveStatus, sending]); const renderedMessages = useMemo( () => deriveRenderedSessionMessages({ transcriptState, snapshot }), [snapshot, transcriptState], @@ -749,36 +802,78 @@ export function SessionSurface(props: SessionSurfaceProps) { } }; - const handleSend = useCallback(async () => { - const text = draft.trim(); - if (!text && attachments.length === 0) return; - // Intentionally allow sending while the assistant is still streaming. - // OpenCode accepts follow-up user turns mid-run and queues them; if the - // backend can't accept the follow-up it'll surface an error via the - // catch below. This restores the "append a prompt while it's still - // talking" behavior that the Solid composer had. + // Core sender shared by initial send and steered follow-ups. OpenCode + // accepts follow-up user turns mid-run (steering) — the running loop picks + // up the new message — so this is safe to call while the agent is busy. + const sendDraft = useCallback(async (nextDraft: ComposerDraft, draftAttachments: ComposerAttachment[]) => { setError(null); useSessionActivityStore.getState().setRunStatus(props.workspaceId, props.sessionId, { type: "busy" }); setSending(true); setAwaitingAssistantBaseline(renderedMessages.length); setNoVisibleAssistantOutputBaseline(null); try { - const nextDraft = buildDraft(text, attachments); await props.onSendDraft(nextDraft); - attachments.forEach(revokeAttachmentPreview); - clearComposerSession(props.sessionId); - props.onDraftChange(buildDraft("", [])); + draftAttachments.forEach(revokeAttachmentPreview); setSending(false); } catch (nextError) { const parsed = parseSessionError(nextError); setError(parsed); - useSessionActivityStore.getState().setError(props.workspaceId, props.sessionId); + useSessionActivityStore.getState().setError(props.workspaceId, props.sessionId, parsed.message); setComposerDraft(props.sessionId, ""); setAwaitingAssistantBaseline(null); setNoVisibleAssistantOutputBaseline(null); setSending(false); + throw nextError; + } + }, [props.onSendDraft, props.sessionId, props.workspaceId, renderedMessages.length, setComposerDraft]); + + const clearComposer = useCallback(() => { + clearComposerSession(props.sessionId); + props.onDraftChange(buildDraft("", [])); + }, [buildDraft, clearComposerSession, props.onDraftChange, props.sessionId]); + + // Initial send (agent idle) and explicit "Steer" follow-up (agent busy) + // share the same immediate path. + const handleSend = useCallback(async () => { + const text = draft.trim(); + if (!text && attachments.length === 0) return; + const nextDraft = buildDraft(text, attachments); + const sentAttachments = attachments; + try { + await sendDraft(nextDraft, sentAttachments); + clearComposer(); + } catch { + setComposerDraft(props.sessionId, ""); } - }, [attachments, buildDraft, clearComposerSession, draft, props.onDraftChange, props.onSendDraft, props.sessionId, props.workspaceId, renderedMessages.length, setComposerDraft]); + }, [attachments, buildDraft, clearComposer, draft, props.sessionId, sendDraft, setComposerDraft]); + + const handleSteer = handleSend; + + // Queue: hold the draft locally and clear the composer. The drain effect + // sends it once the session reports idle. + const handleQueue = useCallback(() => { + const text = draft.trim(); + if (!text && attachments.length === 0) return; + setQueuedDrafts((current) => [...current, buildDraft(text, attachments)]); + clearComposer(); + }, [attachments, buildDraft, clearComposer, draft]); + + const removeQueuedDraft = useCallback((index: number) => { + setQueuedDrafts((current) => current.filter((_, itemIndex) => itemIndex !== index)); + }, []); + + // One label per queued draft, kept index-aligned with `queuedDrafts` so the + // panel's remove action targets the correct entry. Attachment-only drafts + // (no text) fall back to a count label instead of being dropped. + const queuedMessages = useMemo( + () => + queuedDrafts.map((draftItem) => { + const text = draftItem.text.trim(); + if (text) return text; + return t("composer.queued_attachments_only", { count: draftItem.attachments.length }); + }), + [queuedDrafts], + ); const handleAbort = useCallback(async () => { if (!chatStreaming) return; @@ -802,6 +897,31 @@ export function SessionSurface(props: SessionSurfaceProps) { } }, [liveStatus.type]); + // Drain the queued follow-ups once the session goes idle. OpenCode has no + // server-side queue, so we send everything that's queued as a single merged + // message. The ref guards against re-entrancy while the send is in flight. + const drainingQueueRef = useRef(false); + useEffect(() => { + if (drainingQueueRef.current) return; + if (queuedDrafts.length === 0) return; + if (chatStreaming || liveStatus.type !== "idle") return; + const merged = mergeDrafts(queuedDrafts); + if (!merged) return; + const drained = queuedDrafts; + drainingQueueRef.current = true; + setQueuedDrafts([]); + void (async () => { + try { + await sendDraft(merged, merged.attachments); + } catch { + // Restore the queue so the user can retry / edit on failure. + setQueuedDrafts((current) => [...drained, ...current]); + } finally { + drainingQueueRef.current = false; + } + })(); + }, [queuedDrafts, chatStreaming, liveStatus.type, sendDraft]); + useEffect(() => { props.onDraftChange(buildDraft(draft, attachments)); }, [attachments, buildDraft, draft, props.onDraftChange]); @@ -1032,6 +1152,24 @@ export function SessionSurface(props: SessionSurfaceProps) { contentRef, }); + const handleMessageListDispatchAction = useCallback((action: DispatchAction) => { + if (action.target === "settings" && action.action === "open") { + props.onOpenSettingsSection?.(action.section); + } + }, [props.onOpenSettingsSection]); + + const handleMessageListSetPrompt = useCallback((prompt: string) => { + void typeComposerText(prompt); + }, [typeComposerText]); + + const handleRevertToUserMessage = useCallback((messageId: string) => { + props.onRevertToMessage?.(messageId); + }, [props.onRevertToMessage]); + + const handleForkAtMessage = useCallback((messageId: string) => { + props.onForkAtMessage?.(messageId); + }, [props.onForkAtMessage]); + const sessionScrollTopControlAction = useMemo(() => ({ id: "session.scroll_top", label: "Go to the top of the session", @@ -1163,60 +1301,35 @@ export function SessionSurface(props: SessionSurfaceProps) {
    - ) : renderedMessages.length === 0 && snapshot && snapshot.messages.length === 0 ? ( - error ? ( - - ) : shellConfig.starterCards ? ( -
    -
    -

    Try one of these:

    -
    - - - -
    -
    -
    - ) : null + ) : renderedMessages.length === 0 && snapshot && snapshot.messages.length === 0 && error ? ( + ) : ( <> - + + + + + {/* - ) : null} + /> */} + {/* ) : null} */} )}
    - {!sessionScroll.isAtBottom || (!chatStreaming && sessionScroll.topClippedMessageId) ? ( -
    -
    - {!chatStreaming && sessionScroll.topClippedMessageId ? ( - - ) : null} - {!sessionScroll.isAtBottom ? ( - - ) : null} -
    -
    - ) : null} +
    @@ -1278,8 +1369,11 @@ export function SessionSurface(props: SessionSurfaceProps) { mentions={mentions} onDraftChange={handleComposerDraftChange} onSend={handleSend} + onSteer={handleSteer} + onQueue={handleQueue} onStop={handleAbort} busy={chatStreaming} + queuedCount={queuedMessages.length} disabled={model.transitionState !== "idle" || Boolean(props.modelUnavailable)} modelUnavailable={Boolean(props.modelUnavailable)} statusLabel={statusLabel(snapshot ?? undefined, chatStreaming)} @@ -1324,10 +1418,13 @@ export function SessionSurface(props: SessionSurfaceProps) { isRemoteWorkspace={props.isRemoteWorkspace} isSandboxWorkspace={props.isSandboxWorkspace} onUploadInboxFiles={props.onUploadInboxFiles ?? handleUploadInboxFiles} - compactTopSpacing={Boolean(props.activeQuestion || (props.todos ?? []).some((todo) => todo.content.trim()) || props.activePermission)} + compactTopSpacing={Boolean(props.activeQuestion || (props.todos ?? []).some((todo) => todo.content.trim()) || props.activePermission || queuedMessages.length > 0)} topAccessory={ - props.activeQuestion || (props.todos ?? []).some((todo) => todo.content.trim()) || props.activePermission ? ( + props.activeQuestion || (props.todos ?? []).some((todo) => todo.content.trim()) || props.activePermission || queuedMessages.length > 0 ? (
    + {queuedMessages.length > 0 ? ( + + ) : null} {props.activeQuestion ? ( - ) : ( + ) : (props.todos ?? []).some((todo) => todo.content.trim()) ? ( - )} + ) : null} {props.activePermission ? ( ; - -const STRUCTURED_OUTPUT_TOOL = "StructuredOutput"; - function fileProviderMetadata(part: FilePart) { if (part.source) { return { opencode: { partId: part.id, source: part.source } }; @@ -355,48 +358,9 @@ function toUIPart(part: Part): UIMessage["parts"][number] | null { } if (part.type === "tool") { if (part.tool === STRUCTURED_OUTPUT_TOOL) { - if (part.state.status === "error") return null; - const text = safeStringify(part.state.input); - if (text === "{}" && part.state.status !== "completed") return null; - let state: "done" | "streaming" = "streaming"; - if (part.state.status === "completed") state = "done"; - return { - type: "text", - text, - state, - providerMetadata: { opencode: { partId: `structured-output-${part.callID}`, toolPartId: part.id } }, - }; - } - if (part.state.status === "error") { - return { - type: "dynamic-tool", - toolName: part.tool, - toolCallId: part.callID, - state: "output-error", - input: part.state.input, - errorText: part.state.error, - callProviderMetadata: { opencode: { partId: part.id } }, - }; + return parseStructuredOutputUIPart(part); } - if (part.state.status === "completed") { - return { - type: "dynamic-tool", - toolName: part.tool, - toolCallId: part.callID, - state: "output-available", - input: part.state.input, - output: part.state.output, - callProviderMetadata: { opencode: { partId: part.id } }, - }; - } - return { - type: "dynamic-tool", - toolName: part.tool, - toolCallId: part.callID, - state: "input-available", - input: part.state.input, - callProviderMetadata: { opencode: { partId: part.id } }, - }; + return parseDynamicToolUIPart(part); } if (part.type === "agent") { return { @@ -598,9 +562,9 @@ function applyEvent(entry: SyncEntry, workspaceId: string, event: OpencodeEvent) if (event.type === "session.error") { const sessionId = sessionIdFromProperties(event.properties); if (sessionId) { - useSessionActivityStore.getState().setError(workspaceId, sessionId); + const errorText = describeOpencodeSessionError(sessionErrorFromProperties(event.properties)); + useSessionActivityStore.getState().setError(workspaceId, sessionId, errorText); if (isTrackedSession(entry, sessionId)) { - const errorText = describeOpencodeSessionError(sessionErrorFromProperties(event.properties)); queryClient.setQueryData(transcriptKey(workspaceId, sessionId), (current = []) => { // Key the error to the latest assistant turn so it lands beside the // turn that failed and a later turn's error becomes its own message diff --git a/apps/app/src/react-app/domains/session/sync/usechat-adapter.ts b/apps/app/src/react-app/domains/session/sync/usechat-adapter.ts index f0cd9a7602..6c279791fa 100644 --- a/apps/app/src/react-app/domains/session/sync/usechat-adapter.ts +++ b/apps/app/src/react-app/domains/session/sync/usechat-adapter.ts @@ -1,12 +1,18 @@ /** @jsxImportSource react */ -import type { UIMessage, UIMessageChunk, ChatTransport, DynamicToolUIPart } from "ai"; -import type { Part } from "@opencode-ai/sdk/v2/client"; +import type { UIMessage, UIMessageChunk, ChatTransport } from "ai"; +import type { FilePart, Part, ReasoningPart, TextPart, ToolPart } from "@opencode-ai/sdk/v2/client"; import { abortSessionSafe } from "../../../../app/lib/opencode-session"; -import type { OpenworkSessionMessage, OpenworkSessionSnapshot } from "../../../../app/lib/openwork-server"; +import type { OpenworkSessionSnapshot } from "../../../../app/lib/openwork-server"; import { normalizeEvent, safeStringify } from "../../../../app/utils"; import { SYNTHETIC_SESSION_ERROR_MESSAGE_PREFIX, type OpencodeEvent } from "../../../../app/types"; import { createClient } from "../../../../app/lib/opencode"; +import { + parseDynamicToolUIPart, + parseStructuredOutputUIPart, + shouldDeferInProgressTool, + STRUCTURED_OUTPUT_TOOL, +} from "./parse-tool-parts"; type TransportOptions = { baseUrl: string; @@ -42,12 +48,6 @@ type InternalPartState = { streamFinished: boolean; }; -type ToolPart = Extract; -type TextPart = Extract; -type FilePart = Extract; - -const STRUCTURED_OUTPUT_TOOL = "StructuredOutput"; - function recordValue(value: unknown, key: string) { if (!value || typeof value !== "object") return undefined; return (value as Record)[key]; @@ -145,7 +145,7 @@ function getTextPartValue(part: Part) { return ""; } -function getTextPartDelta(part: TextPart, delta: string, values: Map) { +function getTextPartDelta(part: TextPart | ReasoningPart, delta: string, values: Map) { const nextText = part.text; if (delta) { const previous = values.get(part.id); @@ -202,42 +202,20 @@ function mapFileParts(part: FilePart): UIMessage["parts"] { return [mapFilePart(part)]; } -function mapToolPart(part: ToolPart): DynamicToolUIPart { - const toolName = part.tool; - const input = part.state.input; - - if (part.state.status === "error") { - return { - type: "dynamic-tool", - toolName, - toolCallId: part.callID, - state: "output-error", - input, - callProviderMetadata: { opencode: { partId: part.id } }, - errorText: part.state.error, - }; +function mapSnapshotToolParts(part: ToolPart): UIMessage["parts"] { + if (part.tool === STRUCTURED_OUTPUT_TOOL) { + const mapped = parseStructuredOutputUIPart(part); + return mapped ? [mapped] : []; } - if (part.state.status === "completed") { - return { - type: "dynamic-tool", - toolName, - toolCallId: part.callID, - state: "output-available", - input, - callProviderMetadata: { opencode: { partId: part.id } }, - output: part.state.output, - }; + const mapped = parseDynamicToolUIPart(part); + if (!mapped) return []; + + if (part.state.status === "completed" && part.state.attachments) { + return [mapped, ...part.state.attachments.flatMap(mapFileParts)]; } - return { - type: "dynamic-tool", - toolName, - toolCallId: part.callID, - state: "input-available", - input, - callProviderMetadata: { opencode: { partId: part.id } }, - }; + return [mapped]; } export function snapshotToUIMessages(snapshot: OpenworkSessionSnapshot): UIMessage[] { @@ -269,24 +247,7 @@ export function snapshotToUIMessages(snapshot: OpenworkSessionSnapshot): UIMessa return mapFileParts(part); } if (part.type === "tool") { - if (part.tool === STRUCTURED_OUTPUT_TOOL) { - if (part.state.status === "error") return []; - const text = safeStringify(part.state.input); - if (text === "{}" && part.state.status !== "completed") return []; - const partId = `structured-output-${part.callID}`; - let state: "done" | "streaming" = "streaming"; - if (part.state.status === "completed") state = "done"; - return [{ - type: "text", - text, - state, - providerMetadata: { opencode: { partId, toolPartId: part.id } }, - }]; - } - if (part.state.status === "completed" && part.state.attachments) { - return [mapToolPart(part), ...part.state.attachments.flatMap(mapFileParts)]; - } - return [mapToolPart(part)]; + return mapSnapshotToolParts(part); } if (part.type === "agent") { return [{ @@ -415,7 +376,7 @@ function enqueueTextDelta( function flushPendingDeltas( controller: ReadableStreamDefaultController, state: InternalPartState, - part: TextPart, + part: TextPart | ReasoningPart, ) { const pending = state.pendingDeltas.get(part.id); if (!pending) return; @@ -452,7 +413,7 @@ function handleToolPart( }; const inputText = safeStringify(part.state.input); - if (!toolState.inputSent || inputText !== toolState.inputText) { + if (!shouldDeferInProgressTool(part) && (!toolState.inputSent || inputText !== toolState.inputText)) { controller.enqueue({ type: "tool-input-available", toolCallId: part.callID, diff --git a/apps/app/tests/session-sync-tool-parts.test.ts b/apps/app/tests/session-sync-tool-parts.test.ts new file mode 100644 index 0000000000..fd34d9dae5 --- /dev/null +++ b/apps/app/tests/session-sync-tool-parts.test.ts @@ -0,0 +1,165 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import type { Part } from "@opencode-ai/sdk/v2/client"; +import type { UIMessage } from "ai"; + +import { getReactQueryClient } from "../src/react-app/infra/query-client"; +import { + __applySessionSyncEventForTest, + __createWorkspaceSessionSyncForTest, + trackWorkspaceSessionSync, + transcriptKey, +} from "../src/react-app/domains/session/sync/session-sync"; +import { + parseDynamicToolUIPart, + parseStructuredOutputUIPart, + shouldDeferInProgressTool, +} from "../src/react-app/domains/session/sync/parse-tool-parts"; + +afterEach(() => { + getReactQueryClient().clear(); +}); + +function writeToolPart( + status: "pending" | "running" | "completed" | "error", + input: Record, + overrides: Partial> = {}, +): Extract { + const base = { + id: "part-write", + sessionID: "session-a", + messageID: "msg-a", + type: "tool" as const, + callID: "call-write", + tool: "write", + }; + + if (status === "completed") { + return { + ...base, + ...overrides, + state: { + status: "completed", + input, + output: "ok", + title: "Write", + metadata: {}, + time: { start: 1, end: 2 }, + }, + }; + } + + if (status === "error") { + return { + ...base, + ...overrides, + state: { + status: "error", + input, + error: "failed", + time: { start: 1, end: 2 }, + }, + }; + } + + if (status === "running") { + return { + ...base, + ...overrides, + state: { + status: "running", + input, + time: { start: 1 }, + }, + }; + } + + return { + ...base, + ...overrides, + state: { + status: "pending", + input, + raw: "", + }, + }; +} + +describe("tool part mapper", () => { + test("defers in-progress tools with empty input", () => { + expect(shouldDeferInProgressTool(writeToolPart("pending", {}))).toBe(true); + expect(shouldDeferInProgressTool(writeToolPart("running", {}))).toBe(true); + expect(parseDynamicToolUIPart(writeToolPart("pending", {}))).toBeNull(); + }); + + test("maps in-progress tools with partial input as input-streaming", () => { + const part = writeToolPart("running", { content: "hello" }); + expect(parseDynamicToolUIPart(part)).toMatchObject({ + type: "dynamic-tool", + toolName: "write", + state: "input-streaming", + input: { content: "hello" }, + }); + }); + + test("maps completed tools", () => { + const part = writeToolPart("completed", { content: "hello", filePath: "src/a.ts" }); + expect(parseDynamicToolUIPart(part)).toMatchObject({ + state: "output-available", + input: { content: "hello", filePath: "src/a.ts" }, + output: "ok", + }); + }); + + test("skips empty structured output while streaming", () => { + const part = writeToolPart("running", {}, { tool: "StructuredOutput" }); + expect(parseStructuredOutputUIPart(part)).toBeNull(); + expect(Object.keys(part.state.input).length).toBe(0); + }); + + test("keeps completed structured output even when input is {}", () => { + const part = writeToolPart("completed", {}, { tool: "StructuredOutput" }); + expect(parseStructuredOutputUIPart(part)).toMatchObject({ + type: "text", + text: "{}", + state: "done", + }); + }); + + test("session sync defers empty in-progress write tools until input arrives", () => { + const syncInput = { workspaceId: "workspace-a", baseUrl: "http://127.0.0.1:1234", openworkToken: "token" }; + const cleanup = __createWorkspaceSessionSyncForTest(syncInput); + const release = trackWorkspaceSessionSync(syncInput, "session-a"); + + try { + __applySessionSyncEventForTest(syncInput, { + type: "message.updated", + properties: { info: { id: "msg-a", role: "assistant", sessionID: "session-a" } }, + } as any); + __applySessionSyncEventForTest(syncInput, { + type: "message.part.updated", + properties: { part: writeToolPart("pending", {}) }, + } as any); + + let transcript = getReactQueryClient().getQueryData(transcriptKey("workspace-a", "session-a")); + expect(transcript?.[0]?.parts ?? []).toEqual([]); + + __applySessionSyncEventForTest(syncInput, { + type: "message.part.updated", + properties: { + part: writeToolPart("running", { content: "hello", filePath: "src/main.ts" }), + }, + } as any); + + transcript = getReactQueryClient().getQueryData(transcriptKey("workspace-a", "session-a")); + expect(transcript?.[0]?.parts[0]).toMatchObject({ + type: "dynamic-tool", + toolName: "write", + state: "input-streaming", + input: { content: "hello", filePath: "src/main.ts" }, + }); + } finally { + release(); + cleanup(); + } + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39f8d2f0ae..478d1ab495 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,6 +78,9 @@ importers: '@radix-ui/colors': specifier: ^3.0.0 version: 3.0.0 + '@radix-ui/react-use-controllable-state': + specifier: ^1.2.2 + version: 1.2.2(@types/react@19.2.14)(react@19.2.4) '@shikijs/transformers': specifier: ^4.0.2 version: 4.0.2 @@ -102,6 +105,9 @@ importers: cmdk: specifier: ^1.1.1 version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + dompurify: + specifier: ^3.4.5 + version: 3.4.5 emojilib: specifier: ^4.0.3 version: 4.0.3 @@ -135,18 +141,30 @@ importers: react-dom: specifier: 'catalog:' version: 19.2.4(react@19.2.4) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@19.2.14)(react@19.2.4) react-resizable-panels: specifier: ^4.11.0 version: 4.11.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-router-dom: specifier: ^7.14.1 version: 7.14.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + remark-breaks: + specifier: ^4.0.0 + version: 4.0.0 + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 shadcn: specifier: ^4.6.0 version: 4.6.0(@types/node@25.6.0)(typescript@5.9.3) shiki: specifier: ^4.0.2 version: 4.0.2 + streamdown: + specifier: ^2.5.0 + version: 2.5.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tailwind-merge: specifier: ^3.5.0 version: 3.5.0 @@ -819,6 +837,9 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + '@authenio/xml-encryption@2.0.2': resolution: {integrity: sha512-cTlrKttbrRHEw3W+0/I609A2Matj5JQaRvfLtEIGZvlN0RaPi+3ANsMeqAyCAVlH/lUIW2tmtBlSMni74lcXeg==} engines: {node: '>=12'} @@ -1310,6 +1331,12 @@ packages: '@better-fetch/fetch@1.1.21': resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==} + '@braintree/sanitize-url@7.1.2': + resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} + + '@chevrotain/types@11.1.2': + resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} + '@codemirror/autocomplete@6.20.0': resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==} @@ -2082,6 +2109,12 @@ packages: '@iarna/toml@2.2.5': resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.1.3': + resolution: {integrity: sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==} + '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} @@ -2520,6 +2553,9 @@ packages: '@marijn/find-cluster-break@1.0.2': resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + '@mermaid-js/parser@1.1.1': + resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==} + '@modelcontextprotocol/sdk@1.29.0': resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} engines: {node: '>=18'} @@ -4105,15 +4141,114 @@ packages: '@types/cors@2.8.19': resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} '@types/fs-extra@9.0.13': resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -4185,6 +4320,12 @@ packages: '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -4203,6 +4344,9 @@ packages: '@ungap/structured-clone@1.3.1': resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} + '@upsetjs/venn.js@2.0.0': + resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} + '@vercel/oidc@3.1.0': resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} engines: {node: '>= 20'} @@ -4434,6 +4578,9 @@ packages: solid-js: optional: true + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -4728,6 +4875,12 @@ packages: character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -4860,6 +5013,14 @@ packages: resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} engines: {node: '>= 6'} + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + compare-version@0.1.2: resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==} engines: {node: '>=0.10.0'} @@ -4921,6 +5082,12 @@ packages: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + cosmiconfig@9.0.1: resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} engines: {node: '>=14'} @@ -4961,10 +5128,169 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.33.4: + resolution: {integrity: sha512-HIN5Pmd9MrX9BkV7tDwnOcEJCSFvCpc8X97h3f508J6I5FsqAY65wKOCvgH2CuP42CaahWaz4tuh32SOOIH7ww==} + engines: {node: '>=0.10'} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.14: + resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} + data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + debounce-fn@6.0.0: resolution: {integrity: sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==} engines: {node: '>=18'} @@ -4982,6 +5308,9 @@ packages: supports-color: optional: true + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -5032,6 +5361,9 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -5096,6 +5428,9 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} + dompurify@3.4.5: + resolution: {integrity: sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA==} + domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -5327,6 +5662,9 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + es-toolkit@1.47.0: + resolution: {integrity: sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==} + es6-error@4.1.1: resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} @@ -5366,11 +5704,18 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -5429,6 +5774,9 @@ packages: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + extract-zip@2.0.1: resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} engines: {node: '>= 10.17.0'} @@ -5748,6 +6096,9 @@ packages: resolution: {integrity: sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -5770,12 +6121,33 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-sanitize@5.0.2: + resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==} + hast-util-to-html@9.0.5: resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-to-parse5@8.0.1: + resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==} + hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + headers-polyfill@5.0.1: resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==} @@ -5822,6 +6194,9 @@ packages: resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} engines: {node: '>=14'} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} @@ -5899,6 +6274,9 @@ packages: import-in-the-middle@2.0.6: resolution: {integrity: sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==} + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -5920,6 +6298,16 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + ip-address@10.1.0: resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} engines: {node: '>= 12'} @@ -5928,6 +6316,12 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -5943,6 +6337,9 @@ packages: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -5963,6 +6360,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-in-ssh@1.0.0: resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} engines: {node: '>=20'} @@ -6142,9 +6542,16 @@ packages: jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + katex@0.16.47: + resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -6157,6 +6564,12 @@ packages: resolution: {integrity: sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q==} engines: {node: '>=20.0.0'} + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + lazy-val@1.0.5: resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} @@ -6271,6 +6684,9 @@ packages: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} @@ -6314,6 +6730,9 @@ packages: long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -6360,6 +6779,9 @@ packages: resolution: {integrity: sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + marked-emoji@2.0.3: resolution: {integrity: sha512-fChW/AfUqCHgoEC1nFDgiw3OR/qsi71/QXH/HTo05yd6B5+T+VHh1SqCpn/HpeGLDxkA+MK4+hr4eULB2/A8Jw==} peerDependencies: @@ -6376,6 +6798,11 @@ packages: engines: {node: '>= 18'} hasBin: true + marked@16.4.2: + resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} + engines: {node: '>= 20'} + hasBin: true + marked@17.0.1: resolution: {integrity: sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==} engines: {node: '>= 20'} @@ -6389,9 +6816,54 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-newline-to-break@2.0.0: + resolution: {integrity: sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + mdast-util-to-hast@13.2.1: resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} @@ -6410,21 +6882,93 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + mermaid@11.15.0: + resolution: {integrity: sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + micromark-util-character@2.1.1: resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + micromark-util-encode@2.0.1: resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + micromark-util-sanitize-uri@2.0.1: resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + micromark-util-symbol@2.0.1: resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} micromark-util-types@2.0.2: resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -6893,6 +7437,9 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} @@ -6909,6 +7456,9 @@ packages: parse-bmfont-xml@1.1.6: resolution: {integrity: sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -6934,6 +7484,9 @@ packages: path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + path-exists@3.0.0: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} @@ -7058,6 +7611,12 @@ packages: resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==} engines: {node: '>=14.19.0'} + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + postal-mime@2.7.4: resolution: {integrity: sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==} @@ -7259,6 +7818,12 @@ packages: peerDependencies: react: '>=16.13.1' + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react-refresh@0.18.0: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} @@ -7374,6 +7939,33 @@ packages: regex@6.1.0: resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + rehype-harden@1.1.8: + resolution: {integrity: sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw==} + + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + rehype-sanitize@6.0.0: + resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==} + + remark-breaks@4.0.0: + resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + remend@1.3.0: + resolution: {integrity: sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -7459,6 +8051,9 @@ packages: resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} engines: {node: '>=8.0'} + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + rollup@4.55.1: resolution: {integrity: sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -7467,6 +8062,9 @@ packages: rou3@0.7.12: resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -7478,6 +8076,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + s-js@0.4.9: resolution: {integrity: sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ==} @@ -7718,6 +8319,12 @@ packages: stream-browserify@3.0.0: resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} + streamdown@2.5.0: + resolution: {integrity: sha512-/tTnURfIOxZK/pqJAxsfCvETG/XCJHoWnk3jq9xLcuz6CSpnjjuxSRBTTL4PKGhxiZQf0lqPxGhImdpwcZ2XwA==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -7799,6 +8406,12 @@ packages: style-mod@4.1.3: resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + styled-jsx@5.1.1: resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} engines: {node: '>= 12.0.0'} @@ -7825,6 +8438,9 @@ packages: babel-plugin-macros: optional: true + stylis@4.4.0: + resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} + sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} @@ -7975,9 +8591,16 @@ packages: trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + truncate-utf8-bytes@1.0.2: resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -8070,6 +8693,9 @@ packages: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + unique-filename@2.0.1: resolution: {integrity: sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -8180,6 +8806,9 @@ packages: resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} engines: {node: '>=0.6.0'} + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -8272,6 +8901,9 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} @@ -8522,6 +9154,11 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.6.0 + tinyexec: 1.1.2 + '@authenio/xml-encryption@2.0.2': dependencies: '@xmldom/xmldom': 0.8.13 @@ -9441,6 +10078,10 @@ snapshots: '@better-fetch/fetch@1.1.21': {} + '@braintree/sanitize-url@7.1.2': {} + + '@chevrotain/types@11.1.2': {} + '@codemirror/autocomplete@6.20.0': dependencies: '@codemirror/language': 6.12.1 @@ -10053,6 +10694,14 @@ snapshots: '@iarna/toml@2.2.5': {} + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.1.3': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + import-meta-resolve: 4.2.0 + '@img/colour@1.1.0': optional: true @@ -10600,6 +11249,10 @@ snapshots: '@marijn/find-cluster-break@1.0.2': {} + '@mermaid-js/parser@1.1.1': + dependencies: + '@chevrotain/types': 11.1.2 + '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': dependencies: '@hono/node-server': 1.19.11(hono@4.12.12) @@ -12129,19 +12782,142 @@ snapshots: dependencies: '@types/node': 25.6.0 - '@types/debug@4.1.13': - dependencies: - '@types/ms': 2.1.0 - - '@types/estree@1.0.8': {} + '@types/d3-array@3.2.2': {} - '@types/fs-extra@9.0.13': + '@types/d3-axis@3.0.6': dependencies: - '@types/node': 25.6.0 + '@types/d3-selection': 3.0.11 - '@types/hast@3.0.4': + '@types/d3-brush@3.0.6': dependencies: - '@types/unist': 3.0.3 + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + + '@types/estree@1.0.8': {} + + '@types/fs-extra@9.0.13': + dependencies: + '@types/node': 25.6.0 + + '@types/geojson@7946.0.16': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 '@types/http-cache-semantics@4.2.0': {} @@ -12216,6 +12992,11 @@ snapshots: '@types/statuses@2.0.6': {} + '@types/trusted-types@2.0.7': + optional: true + + '@types/unist@2.0.11': {} + '@types/unist@3.0.3': {} '@types/validate-npm-package-name@4.0.2': {} @@ -12234,6 +13015,11 @@ snapshots: '@ungap/structured-clone@1.3.1': {} + '@upsetjs/venn.js@2.0.0': + optionalDependencies: + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + '@vercel/oidc@3.1.0': {} '@vitejs/plugin-react@5.2.0(vite@6.4.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))': @@ -12529,6 +13315,8 @@ snapshots: optionalDependencies: solid-js: 1.9.9 + bail@2.0.2: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -12826,6 +13614,10 @@ snapshots: character-entities-legacy@3.0.0: {} + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -12938,6 +13730,10 @@ snapshots: commander@5.1.0: {} + commander@7.2.0: {} + + commander@8.3.0: {} + compare-version@0.1.2: {} compress-commons@4.1.2: @@ -12994,6 +13790,14 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + cosmiconfig@9.0.1(typescript@5.9.3): dependencies: env-paths: 2.2.1 @@ -13032,8 +13836,194 @@ snapshots: csstype@3.2.3: {} + cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.4): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.33.4 + + cytoscape-fcose@2.2.0(cytoscape@3.33.4): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.33.4 + + cytoscape@3.33.4: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.1.0 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.14: + dependencies: + d3: 7.9.0 + lodash-es: 4.18.1 + data-uri-to-buffer@4.0.1: {} + dayjs@1.11.20: {} + debounce-fn@6.0.0: dependencies: mimic-function: 5.0.1 @@ -13044,6 +14034,10 @@ snapshots: dependencies: ms: 2.1.3 + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -13085,6 +14079,10 @@ snapshots: defu@6.1.4: {} + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + delayed-stream@1.0.0: {} delegates@1.0.0: {} @@ -13156,6 +14154,10 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.4.5: + optionalDependencies: + '@types/trusted-types': 2.0.7 + domutils@3.2.2: dependencies: dom-serializer: 2.0.0 @@ -13353,6 +14355,8 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 + es-toolkit@1.47.0: {} + es6-error@4.1.1: optional: true @@ -13482,8 +14486,12 @@ snapshots: escape-string-regexp@4.0.0: optional: true + escape-string-regexp@5.0.0: {} + esprima@4.0.1: {} + estree-util-is-identifier-name@3.0.0: {} + etag@1.8.1: {} event-target-shim@5.0.1: {} @@ -13575,6 +14583,8 @@ snapshots: transitivePeerDependencies: - supports-color + extend@3.0.2: {} + extract-zip@2.0.1: dependencies: debug: 4.4.3 @@ -13945,6 +14955,8 @@ snapshots: graphql@16.13.2: {} + hachure-fill@0.5.2: {} + has-flag@4.0.0: {} has-property-descriptors@1.0.2: @@ -13964,6 +14976,43 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.1.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.1 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.1 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-sanitize@5.0.2: + dependencies: + '@types/hast': 3.0.4 + '@ungap/structured-clone': 1.3.1 + unist-util-position: 5.0.0 + hast-util-to-html@9.0.5: dependencies: '@types/hast': 3.0.4 @@ -13978,10 +15027,48 @@ snapshots: stringify-entities: 4.0.4 zwitch: 2.0.4 + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-to-parse5@8.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + hast-util-whitespace@3.0.0: dependencies: '@types/hast': 3.0.4 + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + headers-polyfill@5.0.1: dependencies: '@types/set-cookie-parser': 2.4.10 @@ -14023,6 +15110,8 @@ snapshots: htmlparser2: 8.0.2 selderee: 0.11.0 + html-url-attributes@3.0.1: {} + html-void-elements@3.0.0: {} htmlparser2@8.0.2: @@ -14120,6 +15209,8 @@ snapshots: cjs-module-lexer: 2.2.0 module-details-from-path: 1.0.4 + import-meta-resolve@4.2.0: {} + imurmurhash@0.1.4: {} indent-string@4.0.0: {} @@ -14135,10 +15226,23 @@ snapshots: ini@1.3.8: {} + inline-style-parser@0.2.7: {} + + internmap@1.0.1: {} + + internmap@2.0.3: {} + ip-address@10.1.0: {} ipaddr.js@1.9.1: {} + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-arrayish@0.2.1: {} is-binary-path@2.1.0: @@ -14153,6 +15257,8 @@ snapshots: dependencies: hasown: 2.0.2 + is-decimal@2.0.1: {} + is-docker@3.0.0: {} is-electron@2.2.2: {} @@ -14165,6 +15271,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-in-ssh@1.0.0: {} is-inside-container@1.0.0: @@ -14319,16 +15427,26 @@ snapshots: readable-stream: 2.3.8 setimmediate: 1.0.5 + katex@0.16.47: + dependencies: + commander: 8.3.0 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 + khroma@2.1.0: {} + kleur@3.0.3: {} kleur@4.1.5: {} kysely@0.28.17: {} + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + lazy-val@1.0.5: {} lazystream@1.0.1: @@ -14409,6 +15527,8 @@ snapshots: p-locate: 3.0.0 path-exists: 3.0.0 + lodash-es@4.18.1: {} + lodash.camelcase@4.3.0: {} lodash.defaults@4.2.0: {} @@ -14444,6 +15564,8 @@ snapshots: long@5.3.2: {} + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -14502,6 +15624,8 @@ snapshots: - bluebird - supports-color + markdown-table@3.0.4: {} + marked-emoji@2.0.3(marked@17.0.1): dependencies: marked: 17.0.1 @@ -14513,6 +15637,8 @@ snapshots: marked@15.0.12: {} + marked@16.4.2: {} + marked@17.0.1: {} matcher@3.0.0: @@ -14522,6 +15648,136 @@ snapshots: math-intrinsics@1.1.0: {} + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-newline-to-break@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-find-and-replace: 3.0.2 + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + mdast-util-to-hast@13.2.1: dependencies: '@types/hast': 3.0.4 @@ -14534,6 +15790,22 @@ snapshots: unist-util-visit: 5.1.0 vfile: 6.0.3 + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdn-data@2.27.1: {} media-typer@1.1.0: {} @@ -14544,23 +15816,221 @@ snapshots: merge2@1.4.1: {} + mermaid@11.15.0: + dependencies: + '@braintree/sanitize-url': 7.1.2 + '@iconify/utils': 3.1.3 + '@mermaid-js/parser': 1.1.1 + '@types/d3': 7.4.3 + '@upsetjs/venn.js': 2.0.0 + cytoscape: 3.33.4 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.4) + cytoscape-fcose: 2.2.0(cytoscape@3.33.4) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.14 + dayjs: 1.11.20 + dompurify: 3.4.5 + es-toolkit: 1.47.0 + katex: 0.16.47 + khroma: 2.1.0 + marked: 16.4.2 + roughjs: 4.6.6 + stylis: 4.4.0 + ts-dedent: 2.2.0 + uuid: 11.1.0 + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + micromark-util-character@2.1.1: dependencies: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + micromark-util-encode@2.0.1: {} + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + micromark-util-sanitize-uri@2.0.1: dependencies: micromark-util-character: 2.1.1 micromark-util-encode: 2.0.1 micromark-util-symbol: 2.0.1 + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + micromark-util-symbol@2.0.1: {} micromark-util-types@2.0.2: {} + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -15036,6 +16506,8 @@ snapshots: package-json-from-dist@1.0.1: {} + package-manager-detector@1.6.0: {} + pako@1.0.11: {} parent-module@1.0.1: @@ -15051,6 +16523,16 @@ snapshots: xml-parse-from-string: 1.0.1 xml2js: 0.5.0 + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.29.0 @@ -15075,6 +16557,8 @@ snapshots: path-browserify@1.0.1: {} + path-data-parser@0.1.0: {} + path-exists@3.0.0: {} path-expression-matcher@1.5.0: {} @@ -15176,6 +16660,13 @@ snapshots: pngjs@7.0.0: {} + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + postal-mime@2.7.4: {} postcss-import@15.1.0(postcss@8.4.38): @@ -15392,6 +16883,24 @@ snapshots: '@babel/runtime': 7.29.2 react: 19.2.4 + react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.4): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.2.14 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 19.2.4 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-refresh@0.18.0: {} react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4): @@ -15514,6 +17023,63 @@ snapshots: dependencies: regex-utilities: 2.3.0 + rehype-harden@1.1.8: + dependencies: + unist-util-visit: 5.1.0 + + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + + rehype-sanitize@6.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-sanitize: 5.0.2 + + remark-breaks@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-newline-to-break: 2.0.0 + unified: 11.0.5 + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + remend@1.3.0: {} + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -15590,6 +17156,8 @@ snapshots: sprintf-js: 1.1.3 optional: true + robust-predicates@3.0.3: {} + rollup@4.55.1: dependencies: '@types/estree': 1.0.8 @@ -15623,6 +17191,13 @@ snapshots: rou3@0.7.12: {} + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + router@2.2.0: dependencies: debug: 4.4.3 @@ -15639,6 +17214,8 @@ snapshots: dependencies: queue-microtask: 1.2.3 + rw@1.3.3: {} + s-js@0.4.9: {} safe-buffer@5.1.2: {} @@ -15983,6 +17560,29 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + streamdown@2.5.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + clsx: 2.1.1 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + marked: 17.0.1 + mermaid: 11.15.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + rehype-harden: 1.1.8 + rehype-raw: 7.0.0 + rehype-sanitize: 6.0.0 + remark-gfm: 4.0.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + remend: 1.3.0 + tailwind-merge: 3.5.0 + unified: 11.0.5 + unist-util-visit: 5.1.0 + unist-util-visit-parents: 6.0.2 + transitivePeerDependencies: + - supports-color + streamsearch@1.1.0: {} strict-event-emitter@0.5.1: {} @@ -16059,6 +17659,14 @@ snapshots: style-mod@4.1.3: {} + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + styled-jsx@5.1.1(react@18.2.0): dependencies: client-only: 0.0.1 @@ -16069,6 +17677,8 @@ snapshots: client-only: 0.0.1 react: 19.2.4 + stylis@4.4.0: {} + sucrase@3.35.1: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -16246,10 +17856,14 @@ snapshots: trim-lines@3.0.1: {} + trough@2.2.0: {} + truncate-utf8-bytes@1.0.2: dependencies: utf8-byte-length: 1.0.5 + ts-dedent@2.2.0: {} + ts-interface-checker@0.1.13: {} ts-morph@26.0.0: @@ -16348,6 +17962,16 @@ snapshots: unicorn-magic@0.3.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + unique-filename@2.0.1: dependencies: unique-slug: 3.0.0 @@ -16447,6 +18071,11 @@ snapshots: extsprintf: 1.4.1 optional: true + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -16495,6 +18124,8 @@ snapshots: dependencies: defaults: 1.0.4 + web-namespaces@2.0.1: {} + web-streams-polyfill@3.3.3: {} web-tree-sitter@0.25.10: {}