From 697e0e1bb804540cd6432a568e0cf9a5073c8b74 Mon Sep 17 00:00:00 2001 From: Arda Erzin Date: Sun, 28 Jun 2026 17:10:29 +0200 Subject: [PATCH 01/12] feat(agent): link prompt, table controls, and md-file drop in the markdown editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three improvements to the toolbar'd markdown editor used by the Instructions and Skill drawers. - Link button now opens a popover asking for the URL (seeded with the existing link when the caret is on one) with Add/Update + Remove, instead of blindly applying the literal "https://". - Table controls: a size-picker popover inserts a table (rows×cols hover grid); when the caret is inside a table the same control becomes a menu for insert/ delete row and column + delete table. The Lexical engine already registered TablePlugin + the markdown table transformers — this just exposes them. - The editor now accepts a dropped Markdown file (.md/.markdown/.mdx/.txt or any text/* file): it reads the file text and replaces the content, with a drop overlay. Intercepts only file drags in the capture phase, so Lexical's own internal text drag/drop is untouched. --- .../SchemaControls/MarkdownEditor.tsx | 102 ++++++- .../agenta-ui/src/Editor/MarkdownToolbar.tsx | 267 ++++++++++++++++-- 2 files changed, 335 insertions(+), 34 deletions(-) diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/MarkdownEditor.tsx b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/MarkdownEditor.tsx index d0b78e3262..ececa5e168 100644 --- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/MarkdownEditor.tsx +++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/MarkdownEditor.tsx @@ -18,7 +18,16 @@ * Controlled value: seeds from `value` and re-syncs when `value` changes from outside (e.g. a skill * upload populating the body), while leaving the cursor alone during local typing. */ -import {type CSSProperties, useEffect, useId, useLayoutEffect, useRef, useState} from "react" +import { + type CSSProperties, + type DragEvent, + useCallback, + useEffect, + useId, + useLayoutEffect, + useRef, + useState, +} from "react" import { EditorProvider, @@ -135,6 +144,50 @@ export function MarkdownEditor({ onChange(next) } + // Markdown-file drop: dropping a .md/.markdown/.txt (or any text/* file) onto the editor + // replaces its content with the file's text. We intercept in the capture phase and only for + // file drags, so Lexical's own internal text drag/drop keeps working. + const [dragOver, setDragOver] = useState(false) + const dropEnabled = editable && !disabled + const isFileDrag = (e: DragEvent) => Array.from(e.dataTransfer.types).includes("Files") + const isMarkdownFile = (file: File) => + /\.(md|markdown|mdx|txt)$/i.test(file.name) || + file.type.startsWith("text/") || + file.type === "application/json" || + file.type === "" + + const handleDragOver = useCallback( + (e: DragEvent) => { + if (!dropEnabled || !isFileDrag(e)) return + e.preventDefault() + e.stopPropagation() + e.dataTransfer.dropEffect = "copy" + setDragOver(true) + }, + [dropEnabled], + ) + + const handleDragLeave = useCallback((e: DragEvent) => { + // Ignore leaves into child nodes; only clear when the pointer exits the wrapper. + if (e.currentTarget.contains(e.relatedTarget as Node | null)) return + setDragOver(false) + }, []) + + const handleDrop = useCallback( + (e: DragEvent) => { + if (!dropEnabled || !isFileDrag(e)) return + e.preventDefault() + e.stopPropagation() + setDragOver(false) + const file = Array.from(e.dataTransfer.files).find(isMarkdownFile) + if (!file) return + void file.text().then((content) => handleChange(content)) + }, + // handleChange is stable enough for this usage (reads current onChange via closure). + + [dropEnabled], + ) + const viewToggle = canToggleView ? ( ) + const divider = ( + + ) + return (
{button( @@ -137,6 +282,7 @@ export function MarkdownToolbar({disabled = false}: MarkdownToolbarProps) { )} {button("b", "Bold", , () => formatText("bold"), active.bold)} {button("i", "Italic", , () => formatText("italic"), active.italic)} + {divider} {button( "ul", "Bulleted list", @@ -151,14 +297,6 @@ export function MarkdownToolbar({disabled = false}: MarkdownToolbarProps) { () => editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined), active.ordered, )} - {button( - "link", - "Link", - , - // Toggle: on an existing link, dispatch null to remove it; otherwise seed a new link. - () => editor.dispatchCommand(TOGGLE_LINK_COMMAND, active.link ? null : "https://"), - active.link, - )} {button("code", "Code", , () => formatText("code"), active.code)} {button( "quote", @@ -167,6 +305,97 @@ export function MarkdownToolbar({disabled = false}: MarkdownToolbarProps) { () => setBlock(() => $createQuoteNode()), active.quote, )} + {divider} + + {/* Link — popover asks for the URL (and removes an existing link). */} + { + setLinkOpen(next) + if (next) setLinkDraft(linkUrl) + }} + trigger="click" + placement="bottom" + destroyTooltipOnHide + content={ +
+ setLinkDraft(e.target.value)} + onPressEnter={applyLink} + /> +
+ {active.link ? ( + + ) : ( + + )} + +
+
+ } + > + +
+ + {/* Table — one control: inside a table it opens the row/column ops menu; otherwise a + size picker to insert one. The chevron signals the menu when the caret is in a table. */} + {active.insideTable && !disabled ? ( + + + + ) : ( + } + > + + + )}
) } From 10267f43e12985f1c91717a301d8a72992f7ed30 Mon Sep 17 00:00:00 2001 From: Arda Erzin Date: Sun, 28 Jun 2026 17:19:15 +0200 Subject: [PATCH 02/12] feat(agent): comfortable markdown rendering for the document editors Apply the "comfortable" prose direction to how Markdown documents (AGENTS.md / SKILL.md) render, in both the live editor and the read-only preview. The shared Lexical editor theme rendered document headings far too large (h1 24px / 600 weight), gave fenced code blocks no block styling, and left tables/blockquotes inconsistent. Rather than change that theme globally (the prompt and chat editors share it), scope the new prose styles to the document editors via a `md-prose` wrapper class: - editor-theme.css: a `.md-prose` block with a calmer heading scale (17/14/13, 500 weight), bordered/rounded code blocks, an accent-rule italic blockquote, and roomier line-height. Uses antd semantic tokens so it adapts to dark mode. - MarkdownEditor: tag its rendered containers with `md-prose` (covers both the edit and preview panes, which both render through this editor). - MarkdownPreview: bring its marked/DOMPurify MD_CLASS in line with the same scale, plus table styling it previously lacked. --- web/oss/src/styles/editor-theme.css | 71 +++++++++++++++++++ .../SchemaControls/MarkdownEditor.tsx | 8 ++- .../agenta-ui/src/MarkdownPreview.tsx | 28 +++++--- 3 files changed, 95 insertions(+), 12 deletions(-) diff --git a/web/oss/src/styles/editor-theme.css b/web/oss/src/styles/editor-theme.css index e3c6b494b5..6369340f5d 100644 --- a/web/oss/src/styles/editor-theme.css +++ b/web/oss/src/styles/editor-theme.css @@ -776,3 +776,74 @@ pre::-webkit-scrollbar-thumb { .dark .editor-tokenAttr { color: #79c0ff; } + +/* ------------------------------------------------------------------------- * + * Document prose ("Option B — comfortable") + * + * Scoped to the Markdown document editors (Instructions / Skill) via the + * `md-prose` wrapper, so the shared editor theme used by the prompt and chat + * editors stays as-is. Uses antd semantic tokens (`--ant-color-*`) so it adapts + * to dark mode automatically; placed last so equal-specificity rules win over + * the `.dark` overrides above. Covers both the edit and read-only preview panes + * (both render through this Lexical editor). + * ------------------------------------------------------------------------- */ +.md-prose .editor-paragraph { + margin-bottom: 10px; + line-height: 1.6; +} +.md-prose .editor-heading-h1, +.md-prose .editor-heading-h2, +.md-prose .editor-heading-h3, +.md-prose .editor-heading-h4, +.md-prose .editor-heading-h5, +.md-prose .editor-heading-h6 { + color: var(--ant-color-text); + font-weight: 500; + line-height: 1.3; + margin-top: 18px; + margin-bottom: 8px; +} +.md-prose .editor-heading-h1 { + font-size: 17px; +} +.md-prose .editor-heading-h2 { + font-size: 14px; +} +.md-prose .editor-heading-h3, +.md-prose .editor-heading-h4, +.md-prose .editor-heading-h5, +.md-prose .editor-heading-h6 { + font-size: 13px; + color: var(--ant-color-text-secondary); +} +.md-prose .editor-quote { + font-size: 13px; + font-style: italic; + color: var(--ant-color-text-secondary); + border-left: 3px solid var(--ant-color-border); + margin-left: 2px; + padding-left: 14px; +} +.md-prose .editor-text-code { + background-color: var(--ant-color-fill-tertiary); + border: 0.5px solid var(--ant-color-border-secondary); + border-radius: 4px; + padding: 1px 5px; + font-size: 86%; +} +.md-prose .editor-code { + display: block; + background-color: var(--ant-color-fill-quaternary); + border: 0.5px solid var(--ant-color-border-secondary); + border-radius: 8px; + padding: 12px 14px; + margin: 10px 0; + font-family: Menlo, Consolas, Monaco, monospace; + font-size: 12px; + line-height: 1.6; + overflow-x: auto; +} +.md-prose .editor-link { + color: var(--ant-color-primary); + text-decoration: underline; +} diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/MarkdownEditor.tsx b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/MarkdownEditor.tsx index ececa5e168..cc226d0e30 100644 --- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/MarkdownEditor.tsx +++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/MarkdownEditor.tsx @@ -255,6 +255,8 @@ export function MarkdownEditor({ /> ) + // `md-prose` scopes the document prose styles (Option B) defined in editor-theme.css to these + // Markdown editors only, so the shared prompt/chat editor theme is untouched. const body = showToolbar ? (
{toolbar} -
{editorEl}
+
{editorEl}
) : boundStyle ? ( -
+
{editorEl}
) : ( - editorEl +
{editorEl}
) return ( diff --git a/web/packages/agenta-ui/src/MarkdownPreview.tsx b/web/packages/agenta-ui/src/MarkdownPreview.tsx index 698fee4d66..7022610149 100644 --- a/web/packages/agenta-ui/src/MarkdownPreview.tsx +++ b/web/packages/agenta-ui/src/MarkdownPreview.tsx @@ -15,17 +15,27 @@ export interface MarkdownPreviewProps { className?: string } -// Compact prose styling for rendered Markdown elements. +// "Option B — comfortable" prose styling for rendered Markdown, kept in sync with the document +// editor's `.md-prose` rules in editor-theme.css. Uses antd semantic tokens so it adapts to dark +// mode. (This renderer is for inline summaries/previews; the drawer's edit + preview panes render +// through the Lexical editor and pick up the `.md-prose` CSS instead.) const MD_CLASS = [ - "text-xs leading-relaxed", + "text-[13px] leading-[1.6] text-[var(--ant-color-text)]", "[&>*:first-child]:mt-0 [&>*:last-child]:mb-0", - "[&_h1]:text-sm [&_h1]:font-medium [&_h2]:text-xs [&_h2]:font-medium [&_h3]:text-xs [&_h3]:font-medium", - "[&_p]:my-1", - "[&_ul]:my-1 [&_ul]:list-disc [&_ul]:pl-4 [&_ol]:my-1 [&_ol]:list-decimal [&_ol]:pl-4 [&_li]:my-0.5", - "[&_a]:text-[var(--ag-c-1677FF)] [&_a]:underline", - "[&_code]:font-mono [&_code]:text-[0.9em]", - "[&_blockquote]:border-l-2 [&_blockquote]:border-solid [&_blockquote]:border-[var(--ag-c-EAEFF5)] [&_blockquote]:pl-2 [&_blockquote]:text-[var(--ag-c-97A4B0)]", - "[&_pre]:overflow-x-auto", + "[&_h1]:text-[17px] [&_h1]:font-medium [&_h1]:mt-[18px] [&_h1]:mb-2 [&_h1]:leading-tight", + "[&_h2]:text-[14px] [&_h2]:font-medium [&_h2]:mt-4 [&_h2]:mb-1.5", + "[&_h3]:text-[13px] [&_h3]:font-medium [&_h3]:mt-3.5 [&_h3]:mb-1 [&_h3]:text-[var(--ant-color-text-secondary)]", + "[&_h4]:text-[13px] [&_h4]:font-medium [&_h4]:text-[var(--ant-color-text-secondary)]", + "[&_p]:my-2", + "[&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:my-2 [&_ol]:list-decimal [&_ol]:pl-5 [&_li]:my-1", + "[&_a]:text-[var(--ant-color-primary)] [&_a]:underline", + "[&_code]:font-mono [&_code]:text-[0.86em] [&_code]:bg-[var(--ant-color-fill-tertiary)] [&_code]:border [&_code]:border-solid [&_code]:border-[var(--ant-color-border-secondary)] [&_code]:rounded [&_code]:px-1.5 [&_code]:py-px", + "[&_pre]:my-2.5 [&_pre]:bg-[var(--ant-color-fill-quaternary)] [&_pre]:border [&_pre]:border-solid [&_pre]:border-[var(--ant-color-border-secondary)] [&_pre]:rounded-lg [&_pre]:p-3 [&_pre]:overflow-x-auto", + "[&_pre_code]:border-0 [&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:text-[12px]", + "[&_blockquote]:my-2.5 [&_blockquote]:border-0 [&_blockquote]:border-l-[3px] [&_blockquote]:border-solid [&_blockquote]:border-[var(--ant-color-border)] [&_blockquote]:pl-3.5 [&_blockquote]:italic [&_blockquote]:text-[var(--ant-color-text-secondary)]", + "[&_table]:my-2.5 [&_table]:w-full [&_table]:border-collapse [&_table]:text-xs", + "[&_th]:border-0 [&_th]:border-b [&_th]:border-solid [&_th]:border-[var(--ant-color-border)] [&_th]:bg-[var(--ant-color-fill-tertiary)] [&_th]:px-2.5 [&_th]:py-1.5 [&_th]:text-left [&_th]:font-medium", + "[&_td]:border-0 [&_td]:border-b [&_td]:border-solid [&_td]:border-[var(--ant-color-border-secondary)] [&_td]:px-2.5 [&_td]:py-1.5", ].join(" ") export function MarkdownPreview({content, className}: MarkdownPreviewProps) { From afc8a125a769a6e615d71faea1a9aa7745f39ad3 Mon Sep 17 00:00:00 2001 From: Arda Erzin Date: Sun, 28 Jun 2026 17:32:34 +0200 Subject: [PATCH 03/12] fix(agent): markdown toolbar block types + dark-mode/list/code styling Follow-up to live testing of the document editor. - Headings: replace the single H2 button with a block-type menu (Normal text, Heading 1-3, Quote, Code block) that reflects the caret's current block. The editor only ever offered one heading level before. "Code block" inserts a real fenced block (the standard @lexical/code CodeNode, already registered in rich mode) instead of the inline-code button, which had made multi-line "code" read as a stack of inline chips. - Headings now render with a clearer hierarchy (20/16/14, 600 weight) so they stand apart from body text. - Lists: the shared theme gave ol and ul different left margins and tall items, so the two indented inconsistently. Normalize both to one padding + tight rows under .md-prose. - Table size-picker: its grid cells used a fixed light hex that vanished in dark mode. Switch to antd semantic tokens so the cells are visible in both themes. - Code block wraps (pre-wrap) and keeps its bordered/rounded surface. --- web/oss/src/styles/editor-theme.css | 27 +++++- .../agenta-ui/src/Editor/MarkdownToolbar.tsx | 97 ++++++++++++++----- .../agenta-ui/src/MarkdownPreview.tsx | 8 +- 3 files changed, 101 insertions(+), 31 deletions(-) diff --git a/web/oss/src/styles/editor-theme.css b/web/oss/src/styles/editor-theme.css index 6369340f5d..f9b47b41e9 100644 --- a/web/oss/src/styles/editor-theme.css +++ b/web/oss/src/styles/editor-theme.css @@ -798,24 +798,41 @@ pre::-webkit-scrollbar-thumb { .md-prose .editor-heading-h5, .md-prose .editor-heading-h6 { color: var(--ant-color-text); - font-weight: 500; + font-weight: 600; line-height: 1.3; margin-top: 18px; - margin-bottom: 8px; + margin-bottom: 6px; } .md-prose .editor-heading-h1 { - font-size: 17px; + font-size: 20px; } .md-prose .editor-heading-h2 { + font-size: 16px; +} +.md-prose .editor-heading-h3 { font-size: 14px; } -.md-prose .editor-heading-h3, .md-prose .editor-heading-h4, .md-prose .editor-heading-h5, .md-prose .editor-heading-h6 { font-size: 13px; color: var(--ant-color-text-secondary); } + +/* Lists — the shared theme gives ol/ul different left margins and 8px-tall items, so the + two list kinds indent inconsistently. Normalize both to one padding and tight items. */ +.md-prose .editor-list-ol, +.md-prose .editor-list-ul { + margin: 8px 0; + padding-left: 22px; +} +.md-prose .editor-list-item { + margin: 3px 0; +} +.md-prose .editor-nested-list-item { + margin: 0; + list-style-type: none; +} .md-prose .editor-quote { font-size: 13px; font-style: italic; @@ -841,6 +858,8 @@ pre::-webkit-scrollbar-thumb { font-family: Menlo, Consolas, Monaco, monospace; font-size: 12px; line-height: 1.6; + white-space: pre-wrap; + tab-size: 2; overflow-x: auto; } .md-prose .editor-link { diff --git a/web/packages/agenta-ui/src/Editor/MarkdownToolbar.tsx b/web/packages/agenta-ui/src/Editor/MarkdownToolbar.tsx index bc7f1661b8..4e0a4358bf 100644 --- a/web/packages/agenta-ui/src/Editor/MarkdownToolbar.tsx +++ b/web/packages/agenta-ui/src/Editor/MarkdownToolbar.tsx @@ -13,6 +13,7 @@ */ import {type ReactNode, useCallback, useEffect, useState} from "react" +import {$createCodeNode, $isCodeNode} from "@lexical/code" import {$isLinkNode, TOGGLE_LINK_COMMAND} from "@lexical/link" import {INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListNode} from "@lexical/list" import {useLexicalComposerContext} from "@lexical/react/LexicalComposerContext" @@ -35,6 +36,7 @@ import { import {$getNearestNodeOfType} from "@lexical/utils" import {Button, Dropdown, Input, type MenuProps, Popover} from "antd" import { + $createParagraphNode, $getSelection, $isRangeSelection, type ElementNode, @@ -45,16 +47,23 @@ import { Bold, ChevronDown, Code, - Heading2, Italic, Link as LinkIcon, List, ListOrdered, - Quote, Table as TableIcon, Unlink, } from "lucide-react" +const BLOCK_TYPES = [ + {key: "paragraph", label: "Normal text"}, + {key: "h1", label: "Heading 1"}, + {key: "h2", label: "Heading 2"}, + {key: "h3", label: "Heading 3"}, + {key: "quote", label: "Quote"}, + {key: "code", label: "Code block"}, +] as const + export interface MarkdownToolbarProps { /** Disable the buttons (e.g. while the editor shows raw Markdown source or is read-only). */ disabled?: boolean @@ -97,8 +106,8 @@ function TableSizePicker({onPick}: {onPick: (rows: number, cols: number) => void className={[ "h-4 w-4 rounded-[2px] border border-solid transition-colors", on - ? "border-[var(--ant-color-primary)] bg-[var(--ant-color-primary-bg,var(--ag-c-EAEFF5,#eaeff5))]" - : "border-[var(--ag-c-EAEFF5,#eaeff5)] bg-transparent", + ? "border-[var(--ant-color-primary)] bg-[var(--ant-color-primary)]" + : "border-[var(--ant-color-border)] bg-[var(--ant-color-fill-quaternary)]", ].join(" ")} /> ) @@ -117,13 +126,13 @@ export function MarkdownToolbar({disabled = false}: MarkdownToolbarProps) { bold: false, italic: false, code: false, - heading: false, - quote: false, bullet: false, ordered: false, link: false, insideTable: false, }) + // The current block type (paragraph / h1-h3 / quote / code), shown in the block-type menu. + const [blockType, setBlockType] = useState("paragraph") // The URL under the caret (empty when not on a link), seeded into the link popover. const [linkUrl, setLinkUrl] = useState("") const [linkOpen, setLinkOpen] = useState(false) @@ -146,12 +155,19 @@ export function MarkdownToolbar({disabled = false}: MarkdownToolbarProps) { ? parent : null setLinkUrl(linkNode ? linkNode.getURL() : "") + setBlockType( + block && $isHeadingNode(block) + ? block.getTag() + : block && $isQuoteNode(block) + ? "quote" + : block && $isCodeNode(block) + ? "code" + : "paragraph", + ) setActive({ bold: selection.hasFormat("bold"), italic: selection.hasFormat("italic"), code: selection.hasFormat("code"), - heading: $isHeadingNode(block), - quote: $isQuoteNode(block), bullet: listType === "bullet", ordered: listType === "number", link: Boolean(linkNode), @@ -176,6 +192,19 @@ export function MarkdownToolbar({disabled = false}: MarkdownToolbarProps) { [editor], ) + // Convert the current block to the chosen type. Headings/quote/code all go through + // `$setBlocksType`; the markdown serializer maps them back to `#`, `>` and fenced blocks. + const formatBlock = useCallback( + (key: string) => { + if (key === "quote") setBlock(() => $createQuoteNode()) + else if (key === "code") setBlock(() => $createCodeNode()) + else if (key === "h1" || key === "h2" || key === "h3") + setBlock(() => $createHeadingNode(key)) + else setBlock(() => $createParagraphNode()) + }, + [setBlock], + ) + // Apply / clear the link on the current selection. The editor keeps its last RangeSelection // while focus sits in the popover input, so the command still targets the selected text. const applyLink = useCallback(() => { @@ -271,17 +300,47 @@ export function MarkdownToolbar({disabled = false}: MarkdownToolbarProps) { ) + const blockLabel = BLOCK_TYPES.find((b) => b.key === blockType)?.label ?? "Normal text" + return (
- {button( - "h", - "Heading", - , - () => setBlock(() => $createHeadingNode("h2")), - active.heading, - )} + {/* Block type — paragraph / headings / quote / code block. */} + ({key: b.key, label: b.label})), + onClick: ({key, domEvent}) => { + domEvent.preventDefault() + formatBlock(key) + }, + }} + > + + + {divider} {button("b", "Bold", , () => formatText("bold"), active.bold)} {button("i", "Italic", , () => formatText("italic"), active.italic)} + {button( + "code", + "Inline code", + , + () => formatText("code"), + active.code, + )} {divider} {button( "ul", @@ -297,14 +356,6 @@ export function MarkdownToolbar({disabled = false}: MarkdownToolbarProps) { () => editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined), active.ordered, )} - {button("code", "Code", , () => formatText("code"), active.code)} - {button( - "quote", - "Quote", - , - () => setBlock(() => $createQuoteNode()), - active.quote, - )} {divider} {/* Link — popover asks for the URL (and removes an existing link). */} diff --git a/web/packages/agenta-ui/src/MarkdownPreview.tsx b/web/packages/agenta-ui/src/MarkdownPreview.tsx index 7022610149..ffafcd5fe5 100644 --- a/web/packages/agenta-ui/src/MarkdownPreview.tsx +++ b/web/packages/agenta-ui/src/MarkdownPreview.tsx @@ -22,10 +22,10 @@ export interface MarkdownPreviewProps { const MD_CLASS = [ "text-[13px] leading-[1.6] text-[var(--ant-color-text)]", "[&>*:first-child]:mt-0 [&>*:last-child]:mb-0", - "[&_h1]:text-[17px] [&_h1]:font-medium [&_h1]:mt-[18px] [&_h1]:mb-2 [&_h1]:leading-tight", - "[&_h2]:text-[14px] [&_h2]:font-medium [&_h2]:mt-4 [&_h2]:mb-1.5", - "[&_h3]:text-[13px] [&_h3]:font-medium [&_h3]:mt-3.5 [&_h3]:mb-1 [&_h3]:text-[var(--ant-color-text-secondary)]", - "[&_h4]:text-[13px] [&_h4]:font-medium [&_h4]:text-[var(--ant-color-text-secondary)]", + "[&_h1]:text-[20px] [&_h1]:font-semibold [&_h1]:mt-[18px] [&_h1]:mb-1.5 [&_h1]:leading-tight", + "[&_h2]:text-[16px] [&_h2]:font-semibold [&_h2]:mt-4 [&_h2]:mb-1.5", + "[&_h3]:text-[14px] [&_h3]:font-semibold [&_h3]:mt-3.5 [&_h3]:mb-1", + "[&_h4]:text-[13px] [&_h4]:font-semibold [&_h4]:text-[var(--ant-color-text-secondary)]", "[&_p]:my-2", "[&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:my-2 [&_ol]:list-decimal [&_ol]:pl-5 [&_li]:my-1", "[&_a]:text-[var(--ant-color-primary)] [&_a]:underline", From 987ab8b980c2d4d85594d6cc7e9c5c1a931bf87b Mon Sep 17 00:00:00 2001 From: Arda Erzin Date: Sun, 28 Jun 2026 18:00:54 +0200 Subject: [PATCH 04/12] fix(agent): real multi-line code blocks + language selection + highlighting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Code block" option turned a multi-line selection into one code node per line via $setBlocksType — which rendered as a stack of inline-code chips with no block background, not a single block. Mirror the Lexical playground: on a range selection, insert one code node and write the text back with insertRawText so line breaks become code lines (collapsed selection still uses $setBlocksType). Also wire up code highlighting, which the rich editor never enabled: - MarkdownEditor registers `registerCodeHighlighting` (the CodeNode/ CodeHighlightNode types were registered but the highlighter was never turned on, so blocks rendered as plain monospace). Token colors come from the existing `editor-token*` theme classes (light + dark). - The toolbar shows a searchable language picker (`getCodeLanguageOptions`) when the caret is inside a code block, writing `CodeNode.setLanguage` so Prism highlights the right grammar. --- .../SchemaControls/MarkdownEditor.tsx | 13 ++++ .../agenta-ui/src/Editor/MarkdownToolbar.tsx | 64 +++++++++++++++++-- 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/MarkdownEditor.tsx b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/MarkdownEditor.tsx index cc226d0e30..0ec7418e13 100644 --- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/MarkdownEditor.tsx +++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/MarkdownEditor.tsx @@ -37,6 +37,7 @@ import { } from "@agenta/ui" import {SharedEditor} from "@agenta/ui/shared-editor" import {cn} from "@agenta/ui/styles" +import {registerCodeHighlighting} from "@lexical/code" import {Tag} from "antd" type MarkdownView = "source" | "rendered" @@ -92,6 +93,17 @@ function MarkdownViewSync({enabled}: {enabled: boolean}) { return null } +/** + * Enables Prism syntax highlighting for code blocks in the rich view. The shared editor registers + * the CodeNode/CodeHighlightNode types but never turns on the highlighter, so fenced blocks render + * as plain monospace until this runs. The token colors come from the `editor-token*` theme classes. + */ +function CodeHighlightSync() { + const [editor] = useLexicalComposerContext() + useEffect(() => registerCodeHighlighting(editor), [editor]) + return null +} + export function MarkdownEditor({ value, onChange, @@ -304,6 +316,7 @@ export function MarkdownEditor({ body )} + ) } diff --git a/web/packages/agenta-ui/src/Editor/MarkdownToolbar.tsx b/web/packages/agenta-ui/src/Editor/MarkdownToolbar.tsx index 4e0a4358bf..2882a58722 100644 --- a/web/packages/agenta-ui/src/Editor/MarkdownToolbar.tsx +++ b/web/packages/agenta-ui/src/Editor/MarkdownToolbar.tsx @@ -11,9 +11,9 @@ * size picker to insert a table; when the caret is inside a table, a second menu exposes row/column * insert + delete operations (mirroring the Lexical playground's table controls). */ -import {type ReactNode, useCallback, useEffect, useState} from "react" +import {type ReactNode, useCallback, useEffect, useMemo, useState} from "react" -import {$createCodeNode, $isCodeNode} from "@lexical/code" +import {$createCodeNode, $isCodeNode, getCodeLanguageOptions} from "@lexical/code" import {$isLinkNode, TOGGLE_LINK_COMMAND} from "@lexical/link" import {INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListNode} from "@lexical/list" import {useLexicalComposerContext} from "@lexical/react/LexicalComposerContext" @@ -34,7 +34,7 @@ import { INSERT_TABLE_COMMAND, } from "@lexical/table" import {$getNearestNodeOfType} from "@lexical/utils" -import {Button, Dropdown, Input, type MenuProps, Popover} from "antd" +import {Button, Dropdown, Input, type MenuProps, Popover, Select} from "antd" import { $createParagraphNode, $getSelection, @@ -133,6 +133,12 @@ export function MarkdownToolbar({disabled = false}: MarkdownToolbarProps) { }) // The current block type (paragraph / h1-h3 / quote / code), shown in the block-type menu. const [blockType, setBlockType] = useState("paragraph") + // The active code block's language (only meaningful when blockType === "code"). + const [codeLanguage, setCodeLanguage] = useState("") + const languageOptions = useMemo( + () => getCodeLanguageOptions().map(([value, label]) => ({value, label})), + [], + ) // The URL under the caret (empty when not on a link), seeded into the link popover. const [linkUrl, setLinkUrl] = useState("") const [linkOpen, setLinkOpen] = useState(false) @@ -164,6 +170,7 @@ export function MarkdownToolbar({disabled = false}: MarkdownToolbarProps) { ? "code" : "paragraph", ) + setCodeLanguage(block && $isCodeNode(block) ? (block.getLanguage() ?? "") : "") setActive({ bold: selection.hasFormat("bold"), italic: selection.hasFormat("italic"), @@ -192,17 +199,47 @@ export function MarkdownToolbar({disabled = false}: MarkdownToolbarProps) { [editor], ) - // Convert the current block to the chosen type. Headings/quote/code all go through + // Convert the current block to the chosen type. Headings/quote/paragraph go through // `$setBlocksType`; the markdown serializer maps them back to `#`, `>` and fenced blocks. + // Code is special: with a multi-line selection, `$setBlocksType` makes one code node per line + // (the stacked-chips bug), so mirror the Lexical playground — insert one code node and write + // the text back with its line breaks preserved as code lines. const formatBlock = useCallback( (key: string) => { + if (key === "code") { + editor.update(() => { + const selection = $getSelection() + if (!$isRangeSelection(selection)) return + if (selection.isCollapsed()) { + $setBlocksType(selection, () => $createCodeNode()) + } else { + const text = selection.getTextContent() + selection.insertNodes([$createCodeNode()]) + const next = $getSelection() + if ($isRangeSelection(next)) next.insertRawText(text) + } + }) + return + } if (key === "quote") setBlock(() => $createQuoteNode()) - else if (key === "code") setBlock(() => $createCodeNode()) else if (key === "h1" || key === "h2" || key === "h3") setBlock(() => $createHeadingNode(key)) else setBlock(() => $createParagraphNode()) }, - [setBlock], + [editor, setBlock], + ) + + // Set the language on the code block under the caret (drives Prism highlighting). + const setCodeBlockLanguage = useCallback( + (lang: string) => { + editor.update(() => { + const selection = $getSelection() + if (!$isRangeSelection(selection)) return + const block = selection.anchor.getNode().getTopLevelElement() + if (block && $isCodeNode(block)) block.setLanguage(lang) + }) + }, + [editor], ) // Apply / clear the link on the current selection. The editor keeps its last RangeSelection @@ -331,6 +368,21 @@ export function MarkdownToolbar({disabled = false}: MarkdownToolbarProps) { + + {/* Language picker — only when the caret is inside a code block; drives highlighting. */} + {blockType === "code" && !disabled ? ( + +
, + document.body, + ) +} diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/MarkdownEditor.tsx b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/MarkdownEditor.tsx index 0ec7418e13..bb301ccb2f 100644 --- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/MarkdownEditor.tsx +++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/MarkdownEditor.tsx @@ -40,6 +40,8 @@ import {cn} from "@agenta/ui/styles" import {registerCodeHighlighting} from "@lexical/code" import {Tag} from "antd" +import {CodeBlockLanguageMenu} from "./CodeBlockLanguageMenu" + type MarkdownView = "source" | "rendered" export interface MarkdownEditorProps { @@ -317,6 +319,7 @@ export function MarkdownEditor({ )} + {!editorDisabled ? : null} ) } diff --git a/web/packages/agenta-ui/src/Editor/MarkdownToolbar.tsx b/web/packages/agenta-ui/src/Editor/MarkdownToolbar.tsx index 2882a58722..6dbc0d3498 100644 --- a/web/packages/agenta-ui/src/Editor/MarkdownToolbar.tsx +++ b/web/packages/agenta-ui/src/Editor/MarkdownToolbar.tsx @@ -11,9 +11,9 @@ * size picker to insert a table; when the caret is inside a table, a second menu exposes row/column * insert + delete operations (mirroring the Lexical playground's table controls). */ -import {type ReactNode, useCallback, useEffect, useMemo, useState} from "react" +import {type ReactNode, useCallback, useEffect, useState} from "react" -import {$createCodeNode, $isCodeNode, getCodeLanguageOptions} from "@lexical/code" +import {$createCodeNode, $isCodeNode} from "@lexical/code" import {$isLinkNode, TOGGLE_LINK_COMMAND} from "@lexical/link" import {INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListNode} from "@lexical/list" import {useLexicalComposerContext} from "@lexical/react/LexicalComposerContext" @@ -34,7 +34,7 @@ import { INSERT_TABLE_COMMAND, } from "@lexical/table" import {$getNearestNodeOfType} from "@lexical/utils" -import {Button, Dropdown, Input, type MenuProps, Popover, Select} from "antd" +import {Button, Dropdown, Input, type MenuProps, Popover} from "antd" import { $createParagraphNode, $getSelection, @@ -46,7 +46,6 @@ import { import { Bold, ChevronDown, - Code, Italic, Link as LinkIcon, List, @@ -125,7 +124,6 @@ export function MarkdownToolbar({disabled = false}: MarkdownToolbarProps) { const [active, setActive] = useState({ bold: false, italic: false, - code: false, bullet: false, ordered: false, link: false, @@ -133,12 +131,6 @@ export function MarkdownToolbar({disabled = false}: MarkdownToolbarProps) { }) // The current block type (paragraph / h1-h3 / quote / code), shown in the block-type menu. const [blockType, setBlockType] = useState("paragraph") - // The active code block's language (only meaningful when blockType === "code"). - const [codeLanguage, setCodeLanguage] = useState("") - const languageOptions = useMemo( - () => getCodeLanguageOptions().map(([value, label]) => ({value, label})), - [], - ) // The URL under the caret (empty when not on a link), seeded into the link popover. const [linkUrl, setLinkUrl] = useState("") const [linkOpen, setLinkOpen] = useState(false) @@ -170,11 +162,9 @@ export function MarkdownToolbar({disabled = false}: MarkdownToolbarProps) { ? "code" : "paragraph", ) - setCodeLanguage(block && $isCodeNode(block) ? (block.getLanguage() ?? "") : "") setActive({ bold: selection.hasFormat("bold"), italic: selection.hasFormat("italic"), - code: selection.hasFormat("code"), bullet: listType === "bullet", ordered: listType === "number", link: Boolean(linkNode), @@ -229,19 +219,6 @@ export function MarkdownToolbar({disabled = false}: MarkdownToolbarProps) { [editor, setBlock], ) - // Set the language on the code block under the caret (drives Prism highlighting). - const setCodeBlockLanguage = useCallback( - (lang: string) => { - editor.update(() => { - const selection = $getSelection() - if (!$isRangeSelection(selection)) return - const block = selection.anchor.getNode().getTopLevelElement() - if (block && $isCodeNode(block)) block.setLanguage(lang) - }) - }, - [editor], - ) - // Apply / clear the link on the current selection. The editor keeps its last RangeSelection // while focus sits in the popover input, so the command still targets the selected text. const applyLink = useCallback(() => { @@ -369,30 +346,9 @@ export function MarkdownToolbar({disabled = false}: MarkdownToolbarProps) { - {/* Language picker — only when the caret is inside a code block; drives highlighting. */} - {blockType === "code" && !disabled ? ( - - value ? getLanguageFriendlyName(String(value)) : "Plain text" - } - /> -
, + <> + {menus.map((m) => ( +
e.stopPropagation()} + > +