diff --git a/web/oss/src/styles/editor-theme.css b/web/oss/src/styles/editor-theme.css index e3c6b494b5..e5da6e4368 100644 --- a/web/oss/src/styles/editor-theme.css +++ b/web/oss/src/styles/editor-theme.css @@ -776,3 +776,150 @@ 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: 600; + line-height: 1.3; + margin-top: 18px; + margin-bottom: 6px; +} +.md-prose .editor-heading-h1 { + font-size: 20px; +} +.md-prose .editor-heading-h2 { + font-size: 16px; +} +.md-prose .editor-heading-h3 { + font-size: 14px; +} +.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; +} +/* Per-depth markers for nested lists, matching the Lexical playground: ordered cycles + 1 → A → a → I → i, unordered • → ◦ → ▪. Lexical DOM-nests indented items (the wrapper li + carries `editor-nested-list-item`, which is marker-less above), so descendant `ol ol` / + `ul ul` selectors pick the right style per level. Scoped to the rendered document view. */ +.md-prose .editor-input:not(.markdown-view) ol.editor-list-ol { + list-style-type: decimal; +} +.md-prose .editor-input:not(.markdown-view) ol.editor-list-ol ol.editor-list-ol { + list-style-type: upper-alpha; +} +.md-prose .editor-input:not(.markdown-view) ol.editor-list-ol ol.editor-list-ol ol.editor-list-ol { + list-style-type: lower-alpha; +} +.md-prose + .editor-input:not(.markdown-view) + ol.editor-list-ol + ol.editor-list-ol + ol.editor-list-ol + ol.editor-list-ol { + list-style-type: upper-roman; +} +.md-prose + .editor-input:not(.markdown-view) + ol.editor-list-ol + ol.editor-list-ol + ol.editor-list-ol + ol.editor-list-ol + ol.editor-list-ol { + list-style-type: lower-roman; +} +.md-prose .editor-input:not(.markdown-view) ul.editor-list-ul { + list-style-type: disc; +} +.md-prose .editor-input:not(.markdown-view) ul.editor-list-ul ul.editor-list-ul { + list-style-type: circle; +} +.md-prose + .editor-input:not(.markdown-view) + ul.editor-list-ul + ul.editor-list-ul + ul.editor-list-ul { + list-style-type: square; +} +.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%; +} +/* The shared `.editor-inner:not(.code-editor) .editor-code` rule (specificity 0-3-0) sets an 8px + padding + light fill and would beat a plain `.md-prose .editor-code` (0-2-0). Scope this to the + rendered input (`:not(.markdown-view)`, so source view is untouched) to reach 0-4-0 and win. */ +/* Hide the editor's built-in uppercase language label (globals.css renders it via + `::after content: attr(data-highlight-language)`, specificity 0-5-1) on document code blocks — + the CodeBlockLanguageMenu supplies an interactive picker in its place. Needs 0-6-1 to win. */ +.md-prose + .editor-inner:not(.code-editor) + .editor-input:not(.markdown-view) + .editor-code::after { + content: none; +} +.md-prose .editor-input:not(.markdown-view) .editor-code { + display: block; + position: relative; + background-color: var(--ant-color-fill-quaternary); + border: 0.5px solid var(--ant-color-border-secondary); + border-radius: 8px; + /* Extra top padding leaves room for the floating language picker pinned to the block's top. */ + padding: 40px 14px 12px; + margin: 10px 0; + 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 { + color: var(--ant-color-primary); + text-decoration: underline; +} diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AddTextLink.tsx b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AddTextLink.tsx new file mode 100644 index 0000000000..fb3dc2284c --- /dev/null +++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AddTextLink.tsx @@ -0,0 +1,22 @@ +import {type ButtonHTMLAttributes, forwardRef} from "react" + +/** + * The inline "add a …" accent text-link used by every config-section empty state ("No tools yet — + * add a tool"). forwardRef + prop spread so it can be the trigger of a Popover/Dropdown (which + * inject `onClick` and a positioning ref). + */ +export const AddTextLink = forwardRef< + HTMLButtonElement, + {label: string} & ButtonHTMLAttributes +>(function AddTextLink({label, type = "button", ...rest}, ref) { + return ( + + ) +}) diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx index fb7982ce5d..4fe789f956 100644 --- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx +++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx @@ -21,15 +21,7 @@ * Sections are schema-driven: each renders only when its field exists in the resolved * schema, so the panel tracks the backend contract instead of hard-coding fields. */ -import { - type ButtonHTMLAttributes, - forwardRef, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react" +import {useCallback, useEffect, useMemo, useRef, useState} from "react" import {vaultSecretsQueryAtom} from "@agenta/entities/secret" import type {SchemaProperty} from "@agenta/entities/shared" @@ -63,6 +55,7 @@ import {useAtomValue} from "jotai" import {useOptionalDrillIn} from "../components/MoleculeDrillInContext" +import {AddTextLink} from "./AddTextLink" import {agentTemplateLayoutAtom} from "./agentTemplateLayout" import {ClaudePermissionsControl} from "./ClaudePermissionsControl" import {CodeEditor} from "./CodeEditor" @@ -492,30 +485,6 @@ function ItemRow({ ) } -/** - * A text-only "add" link for a section's empty state — no border/background/padding, just inline - * link text inside a muted sentence (the section header keeps the compact `+` for quick-add). - * - * forwardRef + props spread so it can be a popover trigger (ToolSelectorPopover): the popover - * clones the trigger to attach its positioning ref + onClick. Without the ref it can't anchor and - * the popover renders at the top-left and immediately closes. - */ -const AddTextLink = forwardRef< - HTMLButtonElement, - {label: string} & ButtonHTMLAttributes ->(function AddTextLink({label, type = "button", ...rest}, ref) { - return ( - - ) -}) - /** * An instructions markdown file row. Avatar + filename + a 2-line preview of the (markdown-stripped) * content, clamped with an ellipsis. The whole row opens the editor drawer for the full content — diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/CodeBlockLanguageMenu.tsx b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/CodeBlockLanguageMenu.tsx new file mode 100644 index 0000000000..d2019226bc --- /dev/null +++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/CodeBlockLanguageMenu.tsx @@ -0,0 +1,142 @@ +/** + * CodeBlockLanguageMenu + * + * Renders a language picker pinned to the top-right of EVERY code block in the editor (not only the + * focused one), so each block's reserved top strip always carries its language instead of leaving a + * dead gap. It mounts inside the editor's `EditorProvider`. The picker drives `CodeNode.setLanguage`, + * which the Prism highlighter (registered in MarkdownEditor) reads. + * + * Positioning: fixed-position portals anchored to each block's viewport rect, recomputed on every + * editor update plus scroll/resize. Pickers whose block has scrolled out of the editor's scroll + * viewport are dropped so they don't float over the toolbar or neighbouring UI. + */ +import {useCallback, useEffect, useMemo, useState} from "react" + +import {useLexicalComposerContext} from "@agenta/ui" +import {$isCodeNode, getCodeLanguageOptions, getLanguageFriendlyName} from "@lexical/code" +import {Select} from "antd" +import {$getNodeByKey, $getRoot} from "lexical" +import {createPortal} from "react-dom" + +interface BlockMenu { + key: string + lang: string + top: number + right: number +} + +export function CodeBlockLanguageMenu({editable = true}: {editable?: boolean}) { + const [editor] = useLexicalComposerContext() + const options = useMemo( + () => getCodeLanguageOptions().map(([value, label]) => ({value, label})), + [], + ) + const [menus, setMenus] = useState([]) + + const recompute = useCallback(() => { + const rootEl = editor.getRootElement() + // `.md-prose` is the editor's scrolling viewport (see MarkdownEditor) — clip pickers to it. + const viewport = rootEl?.closest(".md-prose")?.getBoundingClientRect() + editor.getEditorState().read(() => { + const next: BlockMenu[] = [] + for (const child of $getRoot().getChildren()) { + if (!$isCodeNode(child)) continue + const el = editor.getElementByKey(child.getKey()) + if (!el) continue + const rect = el.getBoundingClientRect() + // Skip blocks whose top edge isn't within the visible viewport. + if (viewport && (rect.top < viewport.top - 4 || rect.top > viewport.bottom - 10)) { + continue + } + next.push({ + key: child.getKey(), + lang: child.getLanguage() ?? "", + top: rect.top, + right: window.innerWidth - rect.right, + }) + } + setMenus(next) + }) + }, [editor]) + + useEffect(() => editor.registerUpdateListener(() => recompute()), [editor, recompute]) + + useEffect(() => { + // Recompute after mount so existing blocks get pickers without an edit. On a cold open the + // drawer is still sliding in and the markdown is still hydrating, so the first rects are + // stale (the picker would otherwise only appear once some later event nudged a recompute — + // e.g. a hover). Re-run across the next frames + a few timeouts to settle on the real rects. + recompute() + const raf = requestAnimationFrame(recompute) + const timers = [60, 180, 360].map((t) => window.setTimeout(recompute, t)) + const onMove = () => recompute() + // capture: the editor scrolls in an inner container, not the window + window.addEventListener("scroll", onMove, true) + window.addEventListener("resize", onMove) + return () => { + cancelAnimationFrame(raf) + timers.forEach(clearTimeout) + window.removeEventListener("scroll", onMove, true) + window.removeEventListener("resize", onMove) + } + }, [recompute]) + + const setLanguage = useCallback( + (key: string, lang: string) => { + editor.update(() => { + const node = $getNodeByKey(key) + if ($isCodeNode(node)) node.setLanguage(lang) + }) + }, + [editor], + ) + + if (!menus.length) return null + + return createPortal( + <> + {menus.map((m) => ( +
, antd sets a serif fallback font on the inner item; make + // it inherit the app font set on the wrapper below. + "[&_.ant-select-selection-item]:!font-[inherit]", + "[&_.ant-select-selection-placeholder]:!font-[inherit]", + "[&_.ant-select-selection-search-input]:!font-[inherit]", + ].join(" ")} + style={{ + top: m.top + 7, + right: m.right + 8, + fontFamily: "var(--ant-font-family)", + }} + // Don't let interactions here reach the editor. + onMouseDown={(e) => e.stopPropagation()} + > + 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 ? ( + + + + ) : ( + } + > + + )} ) diff --git a/web/packages/agenta-ui/src/MarkdownPreview.tsx b/web/packages/agenta-ui/src/MarkdownPreview.tsx index 698fee4d66..a4db2e2e52 100644 --- a/web/packages/agenta-ui/src/MarkdownPreview.tsx +++ b/web/packages/agenta-ui/src/MarkdownPreview.tsx @@ -15,17 +15,29 @@ 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-[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", + // Per-depth list markers, matching the editor + the Lexical playground (1 → A → a, • → ◦ → ▪). + "[&_ol_ol]:list-[upper-alpha] [&_ol_ol_ol]:list-[lower-alpha] [&_ol_ol_ol_ol]:list-[upper-roman] [&_ol_ol_ol_ol_ol]:list-[lower-roman] [&_ul_ul]:list-[circle] [&_ul_ul_ul]:list-[square]", + "[&_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) {