diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index c3c9bdfa6fd..1df858417db 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -46,6 +46,10 @@ import { ChoosePathButton } from "./PathInstructionsModal"; import { OpenCodeLogoIcon } from "./OpenCodeLogoIcon"; import { ReportsToPicker } from "./ReportsToPicker"; import { shouldShowLegacyWorkingDirectoryField } from "../lib/legacy-agent-config"; +import { + coerceOptionalMarkdownDraft, + normalizeOptionalMarkdownPatch, +} from "./agent-config-form-utils"; /* ---- Create mode values ---- */ @@ -246,7 +250,11 @@ export function AgentConfigForm(props: AgentConfigFormProps) { const patch: Record = {}; if (Object.keys(overlay.identity).length > 0) { - Object.assign(patch, overlay.identity); + const identityPatch = { ...overlay.identity }; + if (Object.prototype.hasOwnProperty.call(identityPatch, "capabilities")) { + identityPatch.capabilities = normalizeOptionalMarkdownPatch(identityPatch.capabilities); + } + Object.assign(patch, identityPatch); } if (overlay.adapterType !== undefined) { patch.adapterType = overlay.adapterType; @@ -517,8 +525,10 @@ export function AgentConfigForm(props: AgentConfigFormProps) { mark("identity", "capabilities", v || null)} + value={coerceOptionalMarkdownDraft( + eff("identity", "capabilities", props.agent.capabilities ?? ""), + )} + onChange={(v) => mark("identity", "capabilities", coerceOptionalMarkdownDraft(v))} placeholder="Describe what this agent can do..." contentClassName="min-h-[44px] text-sm font-mono" imageUploadHandler={async (file) => { diff --git a/ui/src/components/MarkdownEditor.test.tsx b/ui/src/components/MarkdownEditor.test.tsx index daf0361505f..6338ed020e0 100644 --- a/ui/src/components/MarkdownEditor.test.tsx +++ b/ui/src/components/MarkdownEditor.test.tsx @@ -7,6 +7,7 @@ import { MarkdownEditor } from "./MarkdownEditor"; const mdxEditorMockState = vi.hoisted(() => ({ emitMountEmptyReset: false, + lastSuppressHtmlProcessing: undefined as boolean | undefined, })); vi.mock("@mdxeditor/editor", async () => { @@ -27,10 +28,12 @@ vi.mock("@mdxeditor/editor", async () => { markdown, placeholder, onChange, + suppressHtmlProcessing, }: { markdown: string; placeholder?: string; onChange?: (value: string) => void; + suppressHtmlProcessing?: boolean; }, forwardedRef: React.ForwardedRef<{ setMarkdown: (value: string) => void; focus: () => void } | null>, ) { @@ -40,6 +43,10 @@ vi.mock("@mdxeditor/editor", async () => { focus: () => {}, }), []); + React.useEffect(() => { + mdxEditorMockState.lastSuppressHtmlProcessing = suppressHtmlProcessing; + }, [suppressHtmlProcessing]); + React.useEffect(() => { setForwardedRef(forwardedRef, null); const timer = window.setTimeout(() => { @@ -102,6 +109,7 @@ describe("MarkdownEditor", () => { container.remove(); vi.clearAllMocks(); mdxEditorMockState.emitMountEmptyReset = false; + mdxEditorMockState.lastSuppressHtmlProcessing = undefined; }); it("applies async external value updates once the editor ref becomes ready", async () => { @@ -158,4 +166,24 @@ describe("MarkdownEditor", () => { root.unmount(); }); }); + + it("suppresses HTML processing so literal angle brackets do not break markdown editing", async () => { + const root = createRoot(container); + + await act(async () => { + root.render( + {}} + />, + ); + }); + + await flush(); + expect(mdxEditorMockState.lastSuppressHtmlProcessing).toBe(true); + + await act(async () => { + root.unmount(); + }); + }); }); diff --git a/ui/src/components/MarkdownEditor.tsx b/ui/src/components/MarkdownEditor.tsx index b77f926c415..6182d7857c7 100644 --- a/ui/src/components/MarkdownEditor.tsx +++ b/ui/src/components/MarkdownEditor.tsx @@ -605,6 +605,7 @@ export const MarkdownEditor = forwardRef ref={setEditorRef} markdown={value} placeholder={placeholder} + suppressHtmlProcessing onChange={(next) => { const echo = echoIgnoreMarkdownRef.current; if (echo !== null && next === echo) { diff --git a/ui/src/components/agent-config-form-utils.test.ts b/ui/src/components/agent-config-form-utils.test.ts new file mode 100644 index 00000000000..009426d7ce7 --- /dev/null +++ b/ui/src/components/agent-config-form-utils.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; +import { + coerceOptionalMarkdownDraft, + normalizeOptionalMarkdownPatch, +} from "./agent-config-form-utils"; + +describe("agent-config-form-utils", () => { + it("keeps optional markdown drafts string-backed while editing", () => { + expect(coerceOptionalMarkdownDraft(null)).toBe(""); + expect(coerceOptionalMarkdownDraft(undefined)).toBe(""); + expect(coerceOptionalMarkdownDraft("Writes code")).toBe("Writes code"); + }); + + it("normalizes blank optional markdown back to null for persistence", () => { + expect(normalizeOptionalMarkdownPatch("")).toBeNull(); + expect(normalizeOptionalMarkdownPatch(" ")).toBeNull(); + expect(normalizeOptionalMarkdownPatch(null)).toBeNull(); + expect(normalizeOptionalMarkdownPatch("Keeps incidents under control")).toBe( + "Keeps incidents under control", + ); + }); +}); diff --git a/ui/src/components/agent-config-form-utils.ts b/ui/src/components/agent-config-form-utils.ts new file mode 100644 index 00000000000..8899c57dafd --- /dev/null +++ b/ui/src/components/agent-config-form-utils.ts @@ -0,0 +1,8 @@ +export function coerceOptionalMarkdownDraft(value: string | null | undefined): string { + return typeof value === "string" ? value : ""; +} + +export function normalizeOptionalMarkdownPatch(value: unknown): string | null { + if (typeof value !== "string") return null; + return value.trim().length > 0 ? value : null; +}