diff --git a/apps/web/package.json b/apps/web/package.json index e6980cf778ea..ec7334e1fd86 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -32,6 +32,7 @@ "@tanstack/react-pacer": "^0.19.4", "@tanstack/react-router": "^1.160.2", "@tiptap/core": "^3.31.3", + "@tiptap/extension-code": "^3.31.3", "@tiptap/extension-task-item": "^3.31.3", "@tiptap/extension-task-list": "^3.31.3", "@tiptap/pm": "^3.31.3", diff --git a/apps/web/src/components/ComposerPromptEditorTiptap.tsx b/apps/web/src/components/ComposerPromptEditorTiptap.tsx index 5126246dd05c..f8f0b07159fd 100644 --- a/apps/web/src/components/ComposerPromptEditorTiptap.tsx +++ b/apps/web/src/components/ComposerPromptEditorTiptap.tsx @@ -46,6 +46,7 @@ import { buildDocJson, buildTiptapContent, collapsedToFlat, + ComposerCodeExtension, ComposerTaskItemExtension, flatToCollapsed, flatToMarkdown, @@ -741,8 +742,9 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) { dropcursor: false, gapcursor: false, trailingNode: false, + code: false, // Plain mode has no marks: typed markers stay literal characters. - ...(richText ? {} : { bold: false, italic: false, strike: false, code: false }), + ...(richText ? {} : { bold: false, italic: false, strike: false }), }), ComposerMentionExtension, ComposerSkillExtension, @@ -751,6 +753,7 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) { ComposerMarkersExtension, ...(richText ? [ + ComposerCodeExtension, TaskList, ComposerTaskItemExtension.extend({ addInputRules() { diff --git a/apps/web/src/composer-rich-text-doc.test.ts b/apps/web/src/composer-rich-text-doc.test.ts index a8798708974b..8fa311b1999c 100644 --- a/apps/web/src/composer-rich-text-doc.test.ts +++ b/apps/web/src/composer-rich-text-doc.test.ts @@ -7,6 +7,7 @@ import { describe, expect, it } from "vite-plus/test"; import { buildDocJson, collapsedToFlat, + ComposerCodeExtension, ComposerTaskItemExtension, flatToCollapsed, flatToMarkdown, @@ -38,7 +39,9 @@ const schema = getSchemaByResolvedExtensions( dropcursor: false, gapcursor: false, trailingNode: false, + code: false, }), + ComposerCodeExtension, stubAtom("composer-mention", { path: { default: "" }, source: { default: "" } }), stubAtom("composer-skill", { skillName: { default: "" }, @@ -64,6 +67,8 @@ const schema = getSchemaByResolvedExtensions( function roundTrip(value: string) { const json = buildDocJson(value, (name) => ({ label: name, description: null })); const doc = ProseMirrorNode.fromJSON(schema, json); + // `insertContent` validates every node against the schema; `fromJSON` does not. + doc.check(); return serializeEditorDoc(doc); } @@ -135,6 +140,10 @@ describe("composer rich text document model", () => { "hello **bold** world", "a *italic* word and `code` here", "struck ~~out~~ now", + "**`x`**", + "*`x`*", + "~~`x`~~", + "**a `code` c**", "***bold italic*** keeps nesting", "line one\nline two", "trailing newline\n", diff --git a/apps/web/src/composer-rich-text-doc.ts b/apps/web/src/composer-rich-text-doc.ts index e6cedcc5abb3..ed83c35b5f5b 100644 --- a/apps/web/src/composer-rich-text-doc.ts +++ b/apps/web/src/composer-rich-text-doc.ts @@ -1,4 +1,5 @@ import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { Code } from "@tiptap/extension-code"; import { TaskItem } from "@tiptap/extension-task-item"; import { splitPromptIntoComposerSegments } from "~/composer-editor-mentions"; @@ -41,6 +42,13 @@ const TIPTAP_TO_MARK: Record = { code: "code", }; +/** + * Tiptap's code mark excludes every other mark, which rejects the `bold+code` + * spans markdown like `**\`x\`**` parses into and drops the whole insert. + * Code nests inside emphasis here, so it only excludes itself like the rest. + */ +export const ComposerCodeExtension = Code.extend({ excludes: "code" }); + /** * Task list items keep their exact source indent in an attribute so nesting * round-trips byte-identically. Checkbox case (`[X]`) normalizes to `[x]` — diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6feae2edd57c..c052af3c6a1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -633,6 +633,9 @@ importers: '@tiptap/core': specifier: ^3.31.3 version: 3.31.3(@tiptap/pm@3.31.3) + '@tiptap/extension-code': + specifier: ^3.31.3 + version: 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3)) '@tiptap/extension-task-item': specifier: ^3.31.3 version: 3.31.3(@tiptap/extension-list@3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3))