mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 02:44:50 +01:00
Fix agent markdown editor null and angle bracket handling
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
21dd6acb81
commit
75e58b71e8
5 files changed
+72
-3
No files matched your search
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user