mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-24 04:44:47 -06:00
3143965e00a7b4ef767b169f665786ffd035fda9
1 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0a8083e6d5 |
feat(skills): paste SKILL.md to auto-fill the Create Skill modal (#477)
* feat(skills): paste SKILL.md to auto-fill the Create Skill modal When a user pastes an Anthropic-style SKILL.md (YAML frontmatter + markdown body) into the Create Skill content textarea, the frontend sniffs the leading ``---``, posts the raw text to a new backend parse endpoint, and populates name / description / tags / author / version / license / compatibility / allowed_tools from the parsed fields. The textarea is left with the body only (frontmatter stripped), and a toast reports how many fields were set vs. kept (already-typed values are preserved). Backend - ``POST /v1/api/admin/skills/parse`` (admin.skills permission) wraps the existing ``turnstone.core.skill_parser.parse_skill_md`` so admin imports and external installs share one parser. ``ParseSkillRequest`` / ``ParseSkillResponse`` schemas added; OpenAPI spec + sync/async console SDK methods updated. - Hardening: 32 KiB cap on ``raw`` (Pydantic ``max_length`` + handler enforcement); ``Content-Length`` pre-check returns 413 before any body buffering; parse offloaded via ``asyncio.to_thread`` so deeply-nested YAML cannot stall the event loop. Frontend (turnstone/console/static) - New paste handler with optimistic paint (raw text shown immediately, textarea disabled + ``aria-busy`` flipped, hint switches to "Parsing...") so the round-trip is visible on slow networks. - ``AbortController`` + generation guard (``_ctmPasteController``) so a fresh paste or modal close cancels a stale fetch — the previous handler's callbacks see the controller has been replaced and bail before touching the DOM. - Non-destructive overwrite: ``_setSkillFormField`` returns "filled" / "skipped" / "absent" and refuses to clobber non-empty values. Toast reports counts. - Bumps ``#toast`` z-index above modal overlays (was 200 vs. modal 600 — toasts fired while a modal was open were invisible). Console-wide fix exposed by this being the first feature to fire toasts mid-modal. HTML / CSS - New ``.skill-paste-hint`` line above the textarea announcing the affordance, sized to match surrounding ``.label-hint`` text. - ``aria-describedby`` ties the hint to the textarea; ``aria-live= "polite"`` announces the busy-state transition to screen readers. - "Skill Content" heading hint reworded "system message — ..." → "available: ..." and the variables row label "Variables" → "Used" to disambiguate available vs. in-use template variables. Tests - 11 new cases in ``tests/test_skill_parse_api.py``: happy paths (full / minimal / nested-metadata / unquoted-colon recovery), malformed YAML 400, missing/blank/missing-name 400, RBAC 403, raw body 32 KiB cap (Content-Length pre-check), chunked-encoding bypass forces the application-layer cap. Test pins ``raw_frontmatter`` omission so a future ``dataclasses.asdict`` refactor can't silently leak the full YAML dict back to clients. Validation - 5146 / 5146 ``pytest -k "not live"`` pass. - ``ruff`` + ``mypy`` clean on changed sources. - ``node -c`` clean on governance.js. - Two-stage code review (full pipeline + bug+quality re-review of the fix patches) applied; all confirmed findings addressed. * fix(skills): Copilot PR #477 review fixes (cumulative bug-1, bug-2, q-1) bug-1 (server.py): Content-Length pre-check was clamped to 32 KiB — the same number as the per-string char cap on ``raw``. A legitimate ``raw`` of exactly 32 KiB produces a JSON body well above 32 KiB once the ``{"raw":"..."}`` wrapper and any escaping is added, so valid near-max requests were 413'd. New constant ``_PARSE_SKILL_MAX_BODY_BYTES = _PARSE_SKILL_MAX_CHARS * 4`` admits the wrapper + multibyte expansion while still refusing obviously oversized payloads early; the per-string ``len(raw)`` check stays authoritative. bug-2 (governance.js): hideCreateTemplateModal aborted the inflight paste controller and nulled the global, but the handler's ``.catch`` and ``.finally`` guard each DOM mutation behind ``_isCurrent()`` — both bail when the controller has been nulled, leaving the textarea ``disabled`` + ``aria-busy`` and the hint stuck on "Parsing…". Reopening the modal landed on a poisoned state. The second-pass review's q-2 cleanup that dropped the show-side defensive reset missed this scenario — the verifier's reachability argument confused "controller is null" with "UI state is reset"; the two are independent. Hide now resets the paste-induced visible state alongside the abort. q-1 (console_spec.py): error_codes for the parse endpoint listed only 400; handler also returns 413 for oversized bodies. Added 413; kept 403 implicit per the convention sibling admin endpoints follow. Test fixup: bumped the Content-Length test payload to 200 KB so it clearly exceeds the new 128 KB pre-check threshold; otherwise it was falling through to the per-string check and duplicating test_oversized_raw_chunked_returns_413's coverage. |