Skip to content

feat(skills-editor): ew skills editor with MCP governance and accessibility hardening#399

Open
anfibiacreativa wants to merge 40 commits intoewfrom
ew-skills
Open

feat(skills-editor): ew skills editor with MCP governance and accessibility hardening#399
anfibiacreativa wants to merge 40 commits intoewfrom
ew-skills

Conversation

@anfibiacreativa
Copy link
Copy Markdown
Member

@anfibiacreativa anfibiacreativa commented Apr 28, 2026

Summary

  • Introduce the standalone nx-skills-editor experience with a master-detail layout for Skills, Prompts, Agents, MCPs, and Memory.
  • Add MCP governance UX (tool listing + granular enable/disable) and wire chat to use site-configured MCP servers and headers.
  • Harden accessibility and keyboard behavior for card-based interactions, plus targeted regression and unit test coverage.

Why

  • The prior skills/editor surfaces were fragmented and hard to validate end-to-end with chat and MCP flows.
  • Authors need a single place to manage capabilities, including private MCP servers and per-tool governance.
  • Recent interaction bugs (nested-control activation, prompt actions, slash/keyboard behavior, stale form state) required explicit guardrails and tests.

What Changed

  • Add new building blocks for the editor shell:
    • nx2/blocks/shared/card/* reusable card component.
    • nx2/blocks/shared/tabs/* reusable tabs component.
    • nx2/blocks/skills-editor/* new Skills Editor block + API module.
  • Implement full master-detail Skills Editor workflows:
    • Skills CRUD and sync behavior.
    • Prompts catalog/editor fixes (visible actions, delete path, add-to-chat parity).
    • Agents catalog/presets and form handling.
    • MCP catalog/editor, custom server registration, built-in tool visibility, and edit-state fixes.
    • Memory panel rendering and layout handling.
    • Improve skills-editor load performance by caching repeated config reads and moving orphan skill synchronization to a non-blocking background step.
  • Add MCP + chat integration:
    • Chat loads configured MCP server URLs and server headers from config.
    • Chat controller includes mcpServers and mcpServerHeaders in agent requests.
    • Skills editor supports MCP auth header entry and persistence fields in config.
  • Add tool-governance plumbing in UI:
    • Load/display server tools.
    • Persist per-tool enable/disable overrides.
    • Harden boolean parsing from config sheets for consistent enabled-state behavior.
  • Accessibility and interaction hardening:
    • Keyboard-activatable catalog rows/cards with explicit semantics.
    • Guard parent activation when nested interactive controls are used.
    • Focus-visible treatment for card-like interactive surfaces.
  • Supporting updates and regressions:
    • Icon asset additions and nav/icon loading updates.
    • Prompt/chat event compatibility during migration (da-skills-lab-* and da-skills-editor-* namespaces).
    • Remove unreachable Generated Tools tab from catalog.
    • Stabilize flaky tree throttling test threshold in test/utils/tree.test.js.
    • Add/expand unit coverage in nx2/test/unit/skills-editor/nx-skills-editor.test.js.

This PR introduces global utils and shared components, as described below

File Type Summary
nx2/utils/markdown.js util extractTitle — first H1 from markdown; extractToolRefs — scans for mcp__* and da_* tool refs
nx2/utils/sheet-utils.js util normaliseRowKey — canonical row key with .md strip; parseSheetBoolean — sheet string → JS boolean
nx2/utils/skill-frontmatter.js util parseFrontmatter, validateSkillFrontmatter, ensureSkillFrontmatter — Anthropic SKILL.md compliance
nx2/blocks/skills-editor/constants.js constants Tab IDs, CATALOG_TABS, TAB_ACTIONS, FRESH_FORM_STATE, status enums, builtin agent/MCP/tool definitions
nx2/blocks/skills-editor/renderers.js renderers All render* functions for the skills editor extracted from the main component

Test Plan

  • Unit: npm run test:unit -- nx2/test/unit/skills-editor/nx-skills-editor.test.js
  • Unit: run broader da-nx unit suite to verify shared card/tabs integration.
  • E2E: run Skills Lab Playwright coverage on tests/playwright-e2e branch (keyboard/a11y and regression paths).

e2e test live here -> https://github.com/adobe/da-live/compare/tests/playwright-e2e?expand=1

Manual:

  • Validate MCP custom/built-in card interactions (click + keyboard).
  • Validate prompt add-to-chat and slash-command listing/filter/navigation behavior.
  • Validate MCP server auth headers are saved and threaded into chat requests.

Risks / Follow-ups

  • Playwright non-auth runs can fail early if nx-skills-editor does not mount; add clearer readiness skip messaging if needed. These tests are in da-live. Non-blocking to merge PR
  • MCP card interaction architecture should continue trending toward non-interactive containers plus explicit controls to reduce semantic regressions.
  • Minor naming/cleanup follow-ups may remain in skills-editor API/state fields and can be handled in a cleanup PR.

Must be integrated together with:
adobe-rnd/da-agent#23

Stage URLs:

https://ew-skills--da-nx--adobe.aem.page/

https://da.live/apps/skills?nx=ew-skills&nxver=2#/exp-workspace/frescopa
https://da.live/canvas?nx=ew-skills&nxver=2#/exp-workspace/frescopa/index

anfibiacreativa and others added 22 commits April 27, 2026 21:29
ARIA tablist with keyboard nav, S2 tokens, tab-change event.

Made-with: Cursor
Heading, subheading, badge and actions slots, S2 tokens.

Made-with: Cursor
…ation

Port skills lab from exp-workspace to nx2 with S2 tokens, semantic
ARIA markup, and non-blocking data loading. Wire into canvas after-panel.

Made-with: Cursor
- rename "handoff" → "suggestion" throughout (state, CSS, events, sessionStorage key)
- fix SVG node sharing: store outerHTML string, render via unsafeHTML so each card gets its own node
- fix timer leak: store _statusTimer, clear on disconnect and on re-call
- add error handling to _onDeleteSkill and _onDeletePrompt
- remove dead dynamic import of registerMcpServer (already statically imported)
- declare _loadedKey and _statusTimer in constructor (no more implicit properties)
- extract STATUS constants, replace all 'approved'/'draft' magic strings
- fix textarea flex-fill: remove calc(100vh) hack, propagate flex through .textarea-wrap
- remove redundant margin-top on .tool-row (parent gap already provides spacing)
- use S2 checkmark SVG via loadHrefSvg for approved status icon (CSS circles)

Made-with: Cursor
… sync

- Full MCP editor with built-in servers, enable/disable toggles, edit mode
- Generated tools panel with web worker sandbox execution and Try It panel
- Dual-write skills to both .md file and config sheet on save/delete
- Sync mechanism merges .md body with config sheet metadata on load
- Replace loadHrefSvg/unsafeHTML with <img> tags for checkmark icons
Convert Skills Lab from a three-panel to a master-detail layout — slide-in
flex drawer, per-tab dirty-form tracking, browser history + sessionStorage
persistence, tools selector checkbox fix, and all lint errors resolved.
…tab removed

Prompts
- Show row action buttons always (remove opacity:0 hover reveal)
- Add Delete button to each row; remove Associated Tools from editor form
MCPs
- Clicking a card row now opens the editor (was a no-op for custom MCPs)
- Editor title shows 'Edit: <key>' vs 'Register MCP Server' — inline
  'Editing XYZ' hint removed; _clearForm() now also resets MCP state
  so the label can't linger when switching tabs or clicking Register
- Add Description field (textarea-sm) saved to config sheet
Skills editor regression
- _clearForm() was not clearing _editingMcpKey/_mcpKey/_mcpUrl; after
  visiting MCPs, switching to Skills and clicking a skill silently failed
  to open the editor — now fixed
Generated Tools tab
- Remove 'generated' from CATALOG_TABS; tab is no longer reachable
Memory
- Editor body gets .editor-body-memory class → justify-content:flex-end
  pushes memory cont the bottom of the viewport
…d code

API layer:
- deleteSkillMdFile now returns { ok, status } instead of void; callers
  can detect failure (404 treated as success)
- loadAgentPresets spreads preset fields flat ({ id, ...preset }) so
  _renderAgentCard reads label/description/tools without a .preset hop
- loadAgentPresets and fetchMcpToolsFromAgent fire-and-forget chains
  now have .catch() to prevent unhandled rejection warnings

Naming conventions (boolean Lit reactive properties):
- _loading → _isLoading
- _suggestion → _hasSuggestion
- _editorOpen → _isEditorOpen
- _formDirty → _isFormDirty
- _saveBusy → _isSaveBusy
- _agentViewTools → _isAgentViewTools
- _formIsEdit → _isFormEdit
- _formPromptIsEdit → _isFormPromptEdit

CSS state modifier classprefix:
- drawer-open → is-drawer-open
- suggestion-wrap → is-suggestion
- dot-active → is-dot-active
- dot-inactive → is-dot-inactive

Correctness:
- Skill ID input now calls _markDirty() on change
- _loadMemory uses null for fetch error vs '' for empty file; renderer
  handles the distinction correctly
- inert attribute added to .col-editor when drawer is closed
- _onToggleMcpEnabled redundant APPROVED double-check removed
- Dead tab === 'generated' branch removed from _renderListCol()

Polish:
- .card-menu-delete removed; uses .card-menu specificity
- Single-letter vars renamed: q→searchQuery/toolFilter, s→server,
  t→tool, f→status
- 12px font sizes replaced with var(--s2-body-size-xs) token
- Badge padding 2px 8px replaced with spacing tokens
…I contract

Data integrity:
- Sequence skill writes (file first, then config) so a failing config
  write never leaves an .md file without a config entry and vice versa
- Sequence skill deletes (file first, then config) for the same reason
- Prompt CRUD callers updated to use the new ok-shape consistently

API contract (fetchDaConfigSheets / all mutators):
- fetchDaConfigSheets now returns ok:false on 401 instead of EMPTY_CONFIG
  (ok:true); callers can no longer mistake an auth failure for empty data
- All exported mutators (upsertSkillInConfig, deleteSkillFromConfig,
  upsertPromptInConfig, deletePromptFromConfig, upsertGeneratedTool,
  deleteGeneratedTool) now return { ok, error?, status? } consistently
- AUTH_FAIL sentinel extracted to avoid duplicating the 401 return shape
- boot.finally() now has .catch() to prevent unhandled rejection risk
- fetchDaConfigSheets bare catch now surfaces err.message in the return
- Single-letter vars renamed: o→payload, k/u→serverKey/serverUrl, def→toolDef

Stale async guards:
- _onEditSkill: capture skillId and tab at request time; discard response
  if user navigated away before the readSkillMdFile fetch resolved
- _reload fire-and-forgets: loadAgentPresets and fetchMcpToolsFromAgent
  now check the load key before writing state, preventing late responses
  from a previous org/site from overwriting the current view

Form dirty tracking:
- Tool checkbox changes in _renderAssociatedToolsSelector now call
  _markDirty() so tool selection edits are tracked and persisted

Focus management (keyboard accessibility):
- Opening the drawer moves focus to the first interactive element inside
- Closing the drawer restores focus to the element that triggered the open
- _editorTrigger stored on each open entry point (openEditor, onEditSkill,
  openNewMcpEdetc.) and cleared after focus restoration

Filter state:
- _catalogFilter reset to 'all' on every tab change and back/forward
  navigation; it was previously leaking across unrelated tabs

Misc:
- _renderAgentCard parameter renamed a→agent; tool list item t→tool
- _renderAgentsCatalog map var a→agent
…state, CSS deduplication

Critical fixes:
- Add compensating rollback to _onSaveSkill (delete orphan file if config save fails)
  and _onDeleteSkill/_onDeleteSkillById (re-create file if config delete fails)
- Fix writeSkillMdFile to return { ok: false, error } on validation failure
- Replace raw _editorTrigger element reference with stable CSS selector (_editorTriggerSelector)

Simplification:
- Extract upsertSheetRow/deleteSheetRow generic helpers in skills-editor-api.js; all
  config CRUD mutators now delegate to them, eliminating copy-pasted load/mutate/save flows
- Add FRESH_FORM_STATE constant; _clearForm, _captureForm, _restoreForm are now
  tab-agnostic — no more per-mode branching when adding w field
- Add TAB_ACTIONS config map; list header renders one data-driven new-button instead of
  four conditional blocks; _editorTitle() extracts drawer title logic from the template
- Extract .btn-icon CSS primitive; .close-btn, .more-btn, .row-action-btn now declare
  only size/color overrides
Chat/event compatibility:
- Support both legacy (da-skills-lab-*) and new (da-skills-editor-*) event/sessionStorage
  namespaces so suggestion handoff and prompt-to-chat work during the migration window
- chat.js listens for both prompt-add-to-chat and prompt-send families with dedup guard
Prompt edit parity:
- Track _formPromptOriginalTitle so prompt rename upserts the correct row
- Track _formPromptIcon so the icon field is preserved on save
- Restore Add to Chat button in prompt rows and editor footer
Agent parity:
- Store and render aut ignored in UI)
- loadAgentPresets() handles both label/tools and name/mcpServers preset shapes
- New Agent path now creates /.da/agents/{id}.json via saveAgentPresetFile()
- Dedicated _renderAgentForm() / _onSaveAgent() with id, name, and starter preset
Generated Tools tab remains removed (unreachable by design); data still loads.
Add MCP tool listing and per-tool enable/disable controls with config persistence, pass MCP auth headers through chat/agent requests, and stabilize the tree throttle test timing assertion.
…cessibility

Normalize sheet boolean parsing for tool overrides and MCP enablement, make MCP cards keyboard-activatable, and stabilize unit tests against current skills-editor behavior.
Guard MCP card click/keydown activation so nested menu buttons do not trigger card open, and add regression tests for keyboard and click bubbling behavior.
Drop nx2/img/adobe.svg since it is not required for the skills-editor workstream.
…nd refresh

Render skills-editor immediately from per-site session snapshot, refresh data in the background with an auto-refresh progress indicator, lazy-load agents/MCP tools by tab, and keep orphan-skill sync off the critical path.
@aem-code-sync
Copy link
Copy Markdown

aem-code-sync Bot commented Apr 28, 2026

Hello, I'm the AEM Code Sync Bot and I will run some actions to deploy your branch.
In case there are problems, just click the checkbox below to rerun the respective action.

  • Re-sync branch
Commits

The chat controller was passing the raw agent text straight into the
message list without detecting [SKILL_SUGGESTION] blocks. renderers.js
then parsed the block as markdown, producing garbled output (SKILL_ID:
lines, <hr> from the --- markers, etc.).

extractSkillSuggestion() now detects the block, extracts id/body/prose,
writes the payload to sessionStorage, and dispatches the
da-skills-editor-suggestion-handoff event so the skills editor opens the
suggestion form. The raw block is stripped from both the live streaming
preview (onDelta) and the final persisted message (onText).

13 unit tests added under nx2/test/unit/chat/chat-controller.test.js.
@anfibiacreativa
Copy link
Copy Markdown
Member Author

IMHO the nx-skills-editor component is doing too much - should ideally be split with reasonable responsibilities. For eg shell/controller logic/constants/block for each tab perhaps.

And I totally agree with you. I am not sure we want to split the code too much which may make it harder to clean up when we migrate to the OneAgent?

@anfibiacreativa
Copy link
Copy Markdown
Member Author

IMHO the nx-skills-editor component is doing too much - should ideally be split with reasonable responsibilities. For eg shell/controller logic/constants/block for each tab perhaps.

Ok, I did this, I moved the renderers out of the skills-editor cutting the code by half and reducing responsibility. But to do that I also had to update the ESlint config no-underscore-dangle that was already disabled for test files (which access el._xxx) but not for source files. Moving render methods to a standalone renderers.js exposed the same pattern: host._xxx on a parameter rather than this. Extended the disable to all source files since the project uses _-prefixed private members by convention throughout — the rule was just inconsistently scoped.

I propose the following roadmap to further decouple with less risks and let this PR land

What Split analysis Why not now?
skills-editor-api.js → skills-api.js, prompts-api.js, mcp-api.js, agents-api.js 834 lines of mixed concerns, each section is already demarcated by comments touching imports in 3 files at once with risk of breaking tree-shaking or creating circular imports
renderers.js → skill-renderers.js, mcp-renderers.js, prompt-renderers.js renderers.js will grow as features are added; each domain area is self-contained it's elatively low-risk but I propose we wait until the feature is stable so we're not splitting a moving target
Extract  as a standalone LitElement renderMcpForm + renderMcpToolsList + renderMcpServerInfo are approx. 300 lines and self-contained enough Requires a proper event contract (what does the form emit on save?), a11y tests need updating, and the MCP auth section has security-sensitive state that needs careful review before becoming a public API
Extract  as a standalone LitElement Agent form + tools selector are tightly coupled but visually distinct _agentToolIds crosses state boundaries (reads _mcpTools, _agents, BUILTIN_TOOL_IDS) — decoupling that without prop-drilling or a store is non-trivial
nx-skills-editor.js → controller + component The class mixes LitElement lifecycle with all CRUD logic Needs a deliberate architecture decision (controller pattern, stores, or signals); doing it ad-hoc will make things worse

Reactive state fixes:
- _newAgentId, _newAgentName, _formPromptIcon, _formPromptOriginalTitle were
  mutated as state but not declared in static properties — LitElement never
  re-rendered the agent/prompt forms on user input; now registered as reactive

Event listener fix:
- _boundOnSuggestion/ClearForm/Popstate were re-created in connectedCallback on
  every re-attach, leaking the previous listeners; moved to class field arrow
  functions so removeEventListener provably removes the exact function added

Constants:
- Added TAB_SKILLS/AGENTS/PROMPTS/MCPS/MEMORY to constants.js; all 15 magic
  tab-stng literals in renderers.js replaced; CATALOG_TABS and TAB_ACTIONS
  now use these constants internally

renderers.js:
- renderEditorFooter now accepts (host, tab) and derives its own booleans;
  removed the 4-boolean parameter list from the call site
- Dynamic opener dispatch guarded with typeof check to prevent silent no-op
  on typo in constants.js

Frontmatter:
- Injected frontmatter now uses `name:` (Anthropic SKILL.md requirement) with
  the skill ID as the value (already lowercase + hyphens, matching the format)
  rather than a title-cased prettified string
- Extracted parseFrontmatter / validateSkillFrontmatter / ensureSkillFrontmatter
  to nx2/utils/skill-frontmatter.js; _onSaveSkill delegates to ensureSkillFrontmatter
- 33 unit tests covering all branches and boundary conditions of the util
- Updated manual testing doc and OpenAPI example to use `name:` frontmatter
- extractTitle and extractToolRefs moved from renderers.js / skills-editor-api.js
  to nx2/utils/markdown.js — pure markdown helpers with no block dependency
- parseSheetBoolean and normaliseRowKey moved from skills-editor-api.js
  to nx2/utils/sheet-utils.js — generic config sheet coercion/normalisation
- skillKeyMatch and mcpKeyMatch now delegate to normaliseRowKey, removing
  duplicated row-key normalisation logic
- skills-editor-api.js re-exports extractToolRefs for backward compatibility
- nx-skills-editor.test.js import updated to point at the util
- 31 new unit tests across markdown.test.js andet-utils.test.js
…ad rules

- nx-skills-editor.css: structural only (host, layout, gate, chat drawer, responsive)
- catalog.css: list, cards, badges, filter chips, card menu
- editor-panel.css: editor panel, forms, editor-actions, status, animations
- tools.css: tools selector, MCP tools/auth/server, memory

All 4 sheets loaded in parallel via Promise.all before connectedCallback.
Dead .save-row selector removed (never used in templates).
.section-h and .tools-selector-heading consolidated.
Single-property multi-line rules collapsed to one line.
Net: 1194 → 1126 lines across 4 files.
@sharanyavinod
Copy link
Copy Markdown

Ok, I did this, I moved the renderers out of the skills-editor cutting the code by half and reducing responsibility. But to do that I also had to update the ESlint config no-underscore-dangle that was already disabled for test files (which access el._xxx) but not for source files. Moving render methods to a standalone renderers.js exposed the same pattern: host._xxx on a parameter rather than this. Extended the disable to all source files since the project uses _-prefixed private members by convention throughout — the rule was just inconsistently scoped.

Thank you for the restructuring! However regarding underscore linting rule, I wouldn't recommend disabling it globally. 2 options:

  1. If we must disable, disable it only for skills path
  2. Ideally we don't disable at all, since fwiu the rule was set so by design - allowed only within classes to indicate private members and not allowed for identifiers that are exposed and accessible outside of the class. This is a well defined standard and also makes it easy to make it clear what is scoped and what isn't. I believe we should rather work on the design such that this rule is obeyed, rather than turning it off.

@anfibiacreativa
Copy link
Copy Markdown
Member Author

  1. If we must disable, disable it only for skills path
  2. Ideally we don't disable at all, since fwiu the rule was set so by design -

You are absolutely right, and it was my incorrect assumption that Claude was following a pattern across the repo in the refactoring, but it was not. I will reestablish the rule and refactor the renderer to match the existing pattern.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants