diff --git a/bun.lock b/bun.lock index 40f07ac..2c67c12 100644 --- a/bun.lock +++ b/bun.lock @@ -110,6 +110,7 @@ "streamdown": "^2.1.0", "tailwind-merge": "^3.4.0", "tippy.js": "^6.3.7", + "tiptap-markdown": "^0.9.0", "tokenlens": "^1.3.1", "tw-animate-css": "^1.4.0", "use-stick-to-bottom": "^1.1.2", @@ -1154,13 +1155,13 @@ "@types/koa-compose": ["@types/koa-compose@3.2.9", "", { "dependencies": { "@types/koa": "*" } }, "sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA=="], - "@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="], + "@types/linkify-it": ["@types/linkify-it@3.0.5", "", {}, "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw=="], - "@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="], + "@types/markdown-it": ["@types/markdown-it@13.0.9", "", { "dependencies": { "@types/linkify-it": "^3", "@types/mdurl": "^1" } }, "sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw=="], "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], - "@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="], + "@types/mdurl": ["@types/mdurl@1.0.5", "", {}, "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA=="], "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], @@ -2004,6 +2005,8 @@ "markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="], + "markdown-it-task-lists": ["markdown-it-task-lists@2.1.1", "", {}, "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA=="], + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], "marked": ["marked@17.0.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-s5HZGFQea7Huv5zZcAGhJLT3qLpAfnY7v7GWkICUr0+Wd5TFEtdlRR2XUL5Gg+RH7u2Df595ifrxR03mBaw7gA=="], @@ -2556,6 +2559,8 @@ "tippy.js": ["tippy.js@6.3.7", "", { "dependencies": { "@popperjs/core": "^2.9.0" } }, "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ=="], + "tiptap-markdown": ["tiptap-markdown@0.9.0", "", { "dependencies": { "@types/markdown-it": "^13.0.7", "markdown-it": "^14.1.0", "markdown-it-task-lists": "^2.1.1", "prosemirror-markdown": "^1.11.1" }, "peerDependencies": { "@tiptap/core": "^3.0.1" } }, "sha512-dKLQ9iiuGNgrlGVjrNauF/UBzWu4LYOx5pkD0jNkmQt/GOwfCJsBuzZTsf1jZ204ANHOm572mZ9PYvGh1S7tpQ=="], + "tldts": ["tldts@7.0.23", "", { "dependencies": { "tldts-core": "^7.0.23" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw=="], "tldts-core": ["tldts-core@7.0.23", "", {}, "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ=="], @@ -3470,6 +3475,8 @@ "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "prosemirror-markdown/@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="], + "radix-ui/@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g=="], "radix-ui/@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="], @@ -4030,6 +4037,10 @@ "node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "prosemirror-markdown/@types/markdown-it/@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="], + + "prosemirror-markdown/@types/markdown-it/@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="], + "rimraf/glob/minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="], "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="], diff --git a/package.json b/package.json index 72e2b87..3df217a 100755 --- a/package.json +++ b/package.json @@ -137,6 +137,7 @@ "streamdown": "^2.1.0", "tailwind-merge": "^3.4.0", "tippy.js": "^6.3.7", + "tiptap-markdown": "^0.9.0", "tokenlens": "^1.3.1", "tw-animate-css": "^1.4.0", "use-stick-to-bottom": "^1.1.2", diff --git a/src/app/dashboard/conversations/[channelId]/page.tsx b/src/app/dashboard/conversations/[channelId]/page.tsx index 6b2b373..c60e8d4 100644 --- a/src/app/dashboard/conversations/[channelId]/page.tsx +++ b/src/app/dashboard/conversations/[channelId]/page.tsx @@ -25,9 +25,9 @@ export default async function ChannelPage({ const messages = messagesResult.success && messagesResult.data ? messagesResult.data : [] return ( - <> +
- +
) } diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 445f358..63a407a 100755 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -20,6 +20,7 @@ import { getProjects } from "@/app/actions/projects" import { getCustomDashboards } from "@/app/actions/dashboards" import { ProjectListProvider } from "@/components/project-list-provider" import { getCurrentUser, toSidebarUser } from "@/lib/auth" +import { cookies } from "next/headers" import { BiometricGuard } from "@/components/native/biometric-guard" import { OfflineBanner } from "@/components/native/offline-banner" import { NativeShell } from "@/components/native/native-shell" @@ -35,12 +36,14 @@ export default async function DashboardLayout({ }: { readonly children: React.ReactNode }) { - const [projectList, authUser, dashboardResult] = + const [projectList, authUser, dashboardResult, cookieStore] = await Promise.all([ getProjects(), getCurrentUser(), getCustomDashboards(), + cookies(), ]) + const sidebarOpen = cookieStore.get("sidebar_state")?.value !== "false" const user = authUser ? toSidebarUser(authUser) : null const activeOrgId = authUser?.organizationId ?? null const activeOrgName = authUser?.organizationName ?? null @@ -61,7 +64,7 @@ export default async function DashboardLayout({ div { + display: contents; +} +.chat-markdown p { + margin: 0; + line-height: 1.375; +} +.chat-markdown p + p { + margin-top: 0.25em; +} +.chat-markdown strong { + font-weight: 700; +} +.chat-markdown em { + font-style: italic; +} +.chat-markdown del, .chat-markdown s { + text-decoration: line-through; +} +.chat-markdown code:not(pre code) { + font-family: var(--font-mono); + font-size: 0.85em; + background: color-mix(in oklch, var(--muted) 80%, transparent); + border: 1px solid var(--border); + border-radius: 3px; + padding: 0.1em 0.3em; +} +.chat-markdown pre { + margin: 0.375em 0; + border: 1px solid var(--border); + border-radius: calc(var(--radius) - 4px); + background: color-mix(in oklch, var(--muted) 60%, transparent); + overflow-x: auto; +} +.chat-markdown pre code { + font-family: var(--font-mono); + font-size: 0.85em; + display: block; + padding: 0.5em 0.75em; + background: none; + border: none; +} +.chat-markdown blockquote { + margin: 0.25em 0; + padding-left: 0.75em; + border-left: 3px solid var(--muted-foreground); +} +.chat-markdown ul, .chat-markdown ol { + margin: 0.25em 0; + padding-left: 1.5em; +} +.chat-markdown li { + margin: 0; +} +.chat-markdown a { + color: var(--primary); + text-decoration: none; +} +.chat-markdown a:hover { + text-decoration: underline; +} +.chat-markdown hr { + margin: 0.5em 0; + border-color: var(--border); +} + /* mention pill styling in messages and editor */ .mention { border-radius: calc(var(--radius) - 4px); diff --git a/src/components/agent/chat-provider.tsx b/src/components/agent/chat-provider.tsx index 95a28b6..b3c7059 100755 --- a/src/components/agent/chat-provider.tsx +++ b/src/components/agent/chat-provider.tsx @@ -535,6 +535,26 @@ export function ChatProvider({ ], }) ) + + // mark all generateUI tool calls from restored messages + // as already-dispatched so the watcher doesn't re-trigger + // renders or navigate to /dashboard on resume + for (const m of restored) { + if (m.role !== "assistant") continue + const parts = m.parts as ReadonlyArray + let result = findGenerateUIOutput( + parts, + renderDispatchedRef.current + ) + while (result) { + renderDispatchedRef.current.add(result.callId) + result = findGenerateUIOutput( + parts, + renderDispatchedRef.current + ) + } + } + chat.setMessages(restored) setResumeLoaded(true) } diff --git a/src/components/ai/reasoning.tsx b/src/components/ai/reasoning.tsx index d1c7f44..6ed8616 100755 --- a/src/components/ai/reasoning.tsx +++ b/src/components/ai/reasoning.tsx @@ -92,7 +92,7 @@ export const Reasoning = memo( return ( {children ?? ( <> - + {getThinkingMessage(isStreaming, duration)} )} diff --git a/src/components/conversations/message-composer.tsx b/src/components/conversations/message-composer.tsx index 32fa352..183749a 100644 --- a/src/components/conversations/message-composer.tsx +++ b/src/components/conversations/message-composer.tsx @@ -6,6 +6,7 @@ import StarterKit from "@tiptap/starter-kit" import Placeholder from "@tiptap/extension-placeholder" import Link from "@tiptap/extension-link" import Mention from "@tiptap/extension-mention" +import { Markdown } from "tiptap-markdown" import { Bold, Italic, @@ -166,7 +167,10 @@ export function MessageComposer({ StarterKit.configure({ heading: false, horizontalRule: false, - blockquote: false, + }), + Markdown.configure({ + transformPastedText: true, + transformCopiedText: true, }), Placeholder.configure({ placeholder: placeholder ?? `Message #${channelName}`, @@ -212,8 +216,8 @@ export function MessageComposer({ const handleSend = React.useCallback(async () => { if (!editor || isSending) return - const content = editor.getText().trim() - if (!content) return + const plainText = editor.getText().trim() + if (!plainText) return setIsSending(true) setError(null) @@ -222,12 +226,16 @@ export function MessageComposer({ const mentions = extractMentions( editor.getJSON() as Record, ) - const contentHtml = editor.getHTML() + // send markdown so the server renders it via `marked` + const storage = editor.storage as unknown as Record< + string, + { getMarkdown?: () => string } | undefined + > + const markdown = storage.markdown?.getMarkdown?.() ?? plainText const result = await sendMessage({ channelId, - content, - contentHtml, + content: markdown, threadId, mentions: mentions.length > 0 ? mentions : undefined, }) diff --git a/src/components/conversations/message-item.tsx b/src/components/conversations/message-item.tsx index faea974..8f50aca 100644 --- a/src/components/conversations/message-item.tsx +++ b/src/components/conversations/message-item.tsx @@ -177,19 +177,8 @@ export const MessageItem = React.memo(function MessageItem({ message }: MessageI - ) : message.contentHtml ? ( -
) : ( -
+
{message.content}
)}