('[data-testid="markdown-editor"]')).find(
+ (editor) => editor.value.includes("# Demo Skill"),
+ );
+ expect(bodyEditor).toBeTruthy();
+ });
+
+ await keyDown(bodyEditor as HTMLElement, "E");
+
+ await waitFor(() => expect(node.textContent).toContain("Unsaved"));
+
+ const saveButton = buttonsNamed(node, "Save").find((button) => !button.disabled);
+ expect(saveButton).toBeTruthy();
+ await click(saveButton as HTMLButtonElement);
+
+ await waitFor(() => {
+ expect(mockCompanySkillsApi.updateFile).toHaveBeenCalledWith(
+ "company-1",
+ "source-skill",
+ "SKILL.md",
+ expect.stringContaining("Edited body"),
+ );
+ });
+ expect(mockCompanySkillsApi.updateFile).toHaveBeenCalledWith(
+ "company-1",
+ "source-skill",
+ "SKILL.md",
+ expect.stringContaining("---\nname: Demo Skill"),
+ );
+ });
+
it("offers an 'Edit a copy' CTA on the read-only banner (PAP-13112)", async () => {
mockCompanySkillsApi.detail.mockResolvedValueOnce(makeSkill({
editable: false,
diff --git a/ui/src/pages/SkillStudio.tsx b/ui/src/pages/SkillStudio.tsx
index 57ffe0e181..345889bba6 100644
--- a/ui/src/pages/SkillStudio.tsx
+++ b/ui/src/pages/SkillStudio.tsx
@@ -1264,6 +1264,9 @@ function SkillPane({
// MDXEditor can emit a normalizing onChange on mount, which would otherwise
// dirty the file on open and break the byte-identity guarantee (PAP-13156).
const bodyInteractedRef = useRef(false);
+ const markBodyInteracted = useCallback(() => {
+ bodyInteractedRef.current = true;
+ }, []);
const nodes: FileTreeNode[] = useMemo(
() => buildFileTree(Object.fromEntries(paths.map((p) => [p, ""]))),
@@ -1510,9 +1513,12 @@ function SkillPane({
) : null}
{
- bodyInteractedRef.current = true;
- }}
+ onBeforeInputCapture={markBodyInteracted}
+ onDropCapture={markBodyInteracted}
+ onInput={markBodyInteracted}
+ onKeyDownCapture={markBodyInteracted}
+ onPasteCapture={markBodyInteracted}
+ onPointerDownCapture={markBodyInteracted}
>
{isMarkdown && markdownBlock ? (