Fix agent markdown editor null and angle bracket handling

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dottaandPaperclip committed 2026-04-04 09:08:11 -05:00
1 parent 21dd6acb81
commit 75e58b71e8
5 files changed
+72 -3

No files matched your search

+13 -3
View File
@@ -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<string, unknown> = {};
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) {
</Field>
<Field label="Capabilities" hint={help.capabilities}>
<MarkdownEditor
value={eff("identity", "capabilities", props.agent.capabilities ?? "")}
onChange={(v) => 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) => {
+28
View File
@@ -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(
<MarkdownEditor
value="Keep incidents rate <5%"
onChange={() => {}}
/>,
);
});
await flush();
expect(mdxEditorMockState.lastSuppressHtmlProcessing).toBe(true);
await act(async () => {
root.unmount();
});
});
});
+1
View File
@@ -605,6 +605,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
ref={setEditorRef}
markdown={value}
placeholder={placeholder}
suppressHtmlProcessing
onChange={(next) => {
const echo = echoIgnoreMarkdownRef.current;
if (echo !== null && next === echo) {
@@ -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",
);
});
});
@@ -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;
}