From b5eb8c1647282829744fbd0728a35b63fb8089b0 Mon Sep 17 00:00:00 2001 From: Tom Huang <1043269994@qq.com> Date: Mon, 11 May 2026 17:48:34 +0800 Subject: [PATCH] feat: generic skills + split skills/design-templates + finalize-design API (#955) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: general-purpose skills with @-mention composition and user import Lift skills from "one mode-bound skill per project" to a generic capability the user can compose per turn: - Daemon: scan multiple skill roots (user-skills under runtime data, then the bundled `skills/`); user-imported skills can shadow built-ins by id. - New `POST /api/skills/import` and `DELETE /api/skills/:id` endpoints, with CONFLICT/BAD_REQUEST/NOT_FOUND error codes and built-in delete protection. - ChatRequest gains `skillIds: string[]`; the chat run concatenates each picked skill's body (and merges craftRequires) into the system prompt for that turn only — the project's persistent `skillId` is untouched. - Web composer: `@` popover now lists skills alongside project files; picks render as removable chips above the textarea and ride along with the request as `skillIds`. - Settings → Library: import form (name/description/triggers/body), per-card delete for user skills, "user" origin badge. * chore(web): drop welcome pet teaser + add ds→prompt-template mapping util - SettingsDialog: remove the inline pet adoption teaser from the welcome panel so the first-run modal stays focused on configuration. - New `inferPromptTemplateCategoriesForDs(ds)` helper that maps a design system's authored metadata to prompt-template gallery categories. Imported by the design-system gallery wiring on a sibling branch; no callers in this branch yet. * feat: split skills/design-templates and add finalize-design API Phase 0 of the skills/design-templates refactor (specs/current/ skills-and-design-templates.md): - Move ~104 rendering catalogue entries from skills/ to design-templates/ and keep skills/ for the small set of functional skills that *do work* on user input (utilities, briefs, packagers). - Add design-templates/AGENTS.md and skills/AGENTS.md describing the contract, and a brand-agnostic craft/ surface for opt-in craft rules. - Daemon: add DESIGN_TEMPLATES_DIR / USER_DESIGN_TEMPLATES_DIR roots and an /api/design-templates surface mirroring /api/skills. Asset/example routes still span both registries so existing srcdoc URLs keep resolving across the rename. - Web: split LibrarySection into SkillsSection + DesignSystemsSection, rename the EntryView "Examples" tab to "Templates", and update locales + the New-project picker accordingly. Adds the finalize-design endpoint: - New apps/daemon/src/finalize-design.ts and packages/contracts/src/api/ finalize.ts — one-shot synthesis of a project's transcript + active design system + current artifact into /DESIGN.md via the Anthropic Messages API. Per-project .finalize.lock mirrors the transcript-export hygiene from PR #493; provider credentials are not persisted by the daemon. Other supporting changes: - README + AGENTS.md updates to document the new directory split and craft/ surface, plus i18n strings across 13 locales. - Test refactors and new coverage (finalize-design, runs, sidecar server, plus refreshed daemon integration tests). - .gitignore: scope the *.exe ignore to /OpenDesign.exe so legitimate vendor binaries are no longer hidden. * fix(merge): move clinical-case-report to design-templates/ Origin/main added the clinical-case-report skill under skills/ before the skills/design-templates split landed. Its od.mode is prototype, so per specs/current/skills-and-design-templates.md it is a design template and belongs alongside the other rendering catalogue entries — not under the slimmed-down functional skills/ root. Moving it keeps the EntryView Templates tab consistent with origin/main's intent. * feat(skills): curated design/creative catalogue + collapsible Settings rows Seed ~100 curated design/creative skill stubs under skills/ sourced from awesome-claude-skills (ComposioHQ) and awesome-agent-skills (VoltAgent). Each stub carries an od.category tag so the new filter pill row in Settings -> Skills can group them. The seed script (scripts/seed-curated-design-skills.ts, pnpm seed:curated-design-skills) is idempotent: it only creates folders that don't already exist, so hand-edited stubs are never overwritten. - Daemon: parse and surface od.category on SkillInfo with a strict slug normaliser; mirror the field on SkillSummary in @open-design/contracts. Category is purely a UI hint — system-prompt composition is unchanged. - Web: rewrite SkillsSection from a left-list / right-detail grid into a vertical stack of collapsible rows mirroring the External MCP panel (header always visible with name + mode/source/category pills + per-row enable toggle; SKILL.md preview, file tree and inline edit form expand on demand). Add a Category filter row above the list. Reorder Settings nav so Skills + External MCP sit above the Composio/MCP cluster. Update composer placeholder/hint across 17 locales to advertise '@ files or skills · / for commands'. - Docs: extend skills/AGENTS.md with the curated catalogue rules (idempotency, category vocabulary, no upstream vendoring). Co-authored-by: Cursor * test(skills): teach localized-content + system-prompt tests about the skills/design-templates split mrcfps blocking review on PR #955: the skills/design-templates split (b5993385) moved ~110 SKILL.md entries out of `skills/` and into `design-templates/`, but two repo-level tests still hard-coded the single-root layout, so CI gates went red on the merged branch: - `e2e/tests/localized-content.test.ts` only scanned `/skills` while the locale `skillCopy` map keeps id-keyed entries spanning both roots (ExamplesTab/Templates uses one lookup regardless of origin). Teach the helper to read both `skills/` and `design-templates/`, deduplicating ids so the union matches the localized claim. - `apps/daemon/tests/prompts/system.test.ts` read `skills/live-artifact/SKILL.md`, which now lives under `design-templates/live-artifact/`. Update the absolute path so composeSystemPrompt's coverage of the live-artifact preamble is exercised again. Also enroll the curated design/creative catalogue (PR #955, ~91 stubs sourced from awesome-claude-skills / awesome-agent-skills) in the DE / FR / RU `_SKILL_IDS_WITH_EN_FALLBACK` lists. The stubs are English-only by design (frontmatter advertises an upstream URL); the fallback list is exactly the place to acknowledge "we know this id exists, English copy is fine here" so the localized-content coverage gate passes without forcing a translation task per locale. Co-authored-by: Cursor * fix(skills): always quote frontmatter name so importUserSkill round-trips numeric / boolean ids mrcfps PR #955 review: `buildSkillMarkdown` emitted `name: ${escapeYamlString(name)}` without quotes, so YAML coerced names like `123`, `true`, `false`, or `null` into non-string scalars on re-parse. listSkills() then read `data.name` as a number/boolean and the import flow's follow-up `findSkillById(skills, result.id)` missed it, falling into `/api/skills/import`'s "imported skill could not be re-read" 500 path for those ids. Switch the emitter to a quoted scalar (`name: "..."`) — the double-escape already in `escapeYamlString` makes the quoted form safe — and add a round-trip test covering `123`, `true`, `false`, `null`, and `0` to lock in the contract. Co-authored-by: Cursor * fix(web): drop staged-skill chips when the matching @ token leaves the draft mrcfps PR #955 review: `submit()` always forwarded every id in `stagedSkills`, but that state was only mutated on picker click and chip removal. Hand-deleting an `@` token from the textarea left the chip staged, so the request still carried `skillIds: []` and the daemon composed a skill the prompt no longer referenced. Sync the chips with the draft inside `handleChange()` by pruning `stagedSkills` whenever the new value no longer contains the `@` token (using the same whitespace boundary as `removeStagedSkill`'s strip regex). Comment explains why this prune does not run for `staged` file attachments — users frequently add files via the upload button without leaving an `@` token, so a symmetric prune there would erase legitimate uploads. Co-authored-by: Cursor * fix(daemon): stage @-composed skills' side files alongside the active skill codex PR #955 review: composing a per-turn `@`-picked skill into the system prompt appended its body (with the `withSkillRootPreamble` guidance pointing at relative paths under `/.od-skills//`) but never staged the actual folder. `startChatRun` only copied `activeSkillDir`, so when the project's primary skill was different (or absent) the composed skill's references/, examples/, and scripts/ files lived only at their absolute repo path — agents that honour the cwd-relative form (or that don't get `--add-dir`, e.g. Codex with allowlisted gpt-image projects) couldn't reach them. Thread the composed skills' dirs out of `composeDaemonSystemPrompt` as `extraSkillDirs` and stage each one through the same `stageActiveSkill` API used for the primary skill. Dedupe by folder basename so a project whose primary skill is also `@`-composed isn't copied twice. Each preamble already advertises its own folder, so the prompt and the staged tree stay aligned without further changes. Co-authored-by: Cursor * fix(web): respect the Library disable toggle in the project @-mention picker codex PR #955 review: only `EntryView` received `enabledSkills` (filtered against `config.disabledSkills`); active projects still got `skills={skills}` raw, so a skill the user disabled in Settings kept appearing in the project's `@`-mention popover and could ride along to the daemon via `skillIds`. That broke the Library toggle for any project opened on the post-split branch. Compute a functional-skills-only enabled subset (`enabledFunctionalSkills`) and pass it into `` instead. Templates stay separate — design-templates are filtered through their own `enabledDesignTemplates` memo for the Templates gallery — so ProjectView's chat composer still only sees skills, never templates, matching the pre-split prop surface. Co-authored-by: Cursor * test(e2e): mock /api/design-templates for example-use-prompt flow The Templates tab in EntryView fetches from /api/design-templates after the skills/design-templates split (specs/current/skills-and-design-templates.md). The example-use-prompt Playwright scenario only mocked /api/skills, so the gallery card never appeared and the test timed out waiting on example-card-warm-utility-example. Serve the same fixture summary on both endpoints so the templates gallery renders the card the test clicks. Co-authored-by: Cursor * test(tools-pack): create design-templates fixture for resources test The packaging resources copy now bundles the new design-templates tree alongside skills (see resources.ts BUNDLED_RESOURCE_TREES). The copyBundledResourceTrees fixture only created skills, design-systems, craft, etc., so the recursive copy crashed with ENOENT on design-templates before it could check the prompt-templates assertion. Add the missing fixture directory so the test exercises the same set of resource trees the packaged build does. Co-authored-by: Cursor * fix(skills): clone built-in side files into the shadow on first edit mrcfps PR #955 review: editing a built-in skill wrote a USER_SKILLS_DIR shadow folder that contained only a new SKILL.md. The next listSkills() pass surfaced the shadow as the active dir, but every side-file resolver (/api/skills/:id/files, /example, /assets/*, the system-prompt preamble, and the per-turn cwd staging) reads through skill.dir. With nothing but SKILL.md in the shadow, the bundled assets/, references/, scripts/, and examples/ disappeared the moment the user hit save — a built-in like last30days or live-artifact would break immediately after edit instead of just having its body overridden. Teach updateUserSkill() to take a `sourceDir` and clone every entry except SKILL.md / dotfiles into the shadow on the very first edit. The shadow stays self-contained, so all the resolvers keep working without fallback bookkeeping. Subsequent edits detect the existing shadow and skip the clone, so user tweaks under the side tree survive a re-save. Wire `sourceDir: skill.dir` from server.ts's PUT /api/skills/:id handler and add two regression tests: - 'clones built-in side files into the shadow on the first edit' walks the file tree after save and asserts assets/template.html, references/ notes.md, and scripts/helper.sh all round-trip from the built-in. - 'preserves user-edited side files on subsequent edits' edits the staged assets/template.html, re-saves, and confirms the user content is still there. Co-authored-by: Cursor * test(e2e): rename home tab from Examples to Templates The Examples tab was renamed to Templates in EntryView (b5993385's skills/design-templates split — entry.tabExamples became entry.tabTemplates and the tab value moved from 'examples' to 'templates'), but entry-chrome-flows still asserted the old label and testId. Update both. * fix(skills+web): preserve template body in API mode and dir-based skill delete Two follow-ups from PR #955 review: 1. ProjectView only received `enabledFunctionalSkills`, but `composedSystemPrompt()` still resolved `project.skillId` through that prop and `fetchSkill()`. Projects created from the new `/api/design-templates` surface keep a template id in `project.skillId`, so opening one in API mode dropped the template body from the system prompt and the upstream request ran without the project's primary template instructions. Now ProjectView takes a separate `designTemplates` prop (the unfiltered template list, so a later-disabled template still loads for projects already created from it) and `composedSystemPrompt()` plus the metadata / `isDeck` lookups fall back to that list, with `fetchDesignTemplate()` as the body-fetch fallback to `fetchSkill()`. The chat composer's `@`-picker keeps receiving only the enabled functional skills. 2. `DELETE /api/skills/:id` used `deleteUserSkill(USER_SKILLS_DIR, skill.id)` which re-slugified the frontmatter id and removed `//`. That matched the import shape but missed the install shape — `installFromTarget` writes the folder at `sanitizeRepoName(url)` (GitHub) or `path.basename(realpath)` (local symlink), neither of which is guaranteed to equal the slugified frontmatter `name`. A duplicate `app.delete('/api/skills/:id', ...)` handler at the install routes never fired because Express resolved the earlier registration first, leaving the install/uninstall path without working teardown. The handler now removes `skill.dir` (the absolute path listSkills already discovered) under a USER_SKILLS_DIR safety check, using `lstat` + `unlinkSync` so symlinked local installs unlink cleanly without recursing into the user's source tree. The dead duplicate handler is removed; `deleteUserSkill` is dropped from the server.ts import set (still exported and unit-tested in skills.ts). Regression coverage in `apps/daemon/tests/skills-delete-route.test.ts` pins both shapes plus the symlink-preserves-source case. * test(daemon): point hyperframes system-prompt test at design-templates The merge with main brought in a hyperframes system-prompt test that reads `skills/hyperframes/SKILL.md`, but this branch's split moved `hyperframes` into `design-templates/` (same migration as `live-artifact` already handled above in this file). CI was failing with ENOENT on the old path. --------- Co-authored-by: Cursor --- AGENTS.md | 2 +- apps/daemon/src/server-context.ts | 15 + apps/daemon/src/server.ts | 94 +- apps/daemon/src/skills.ts | 631 +++++++-- apps/daemon/src/static-resource-routes.ts | 152 ++- apps/daemon/tests/prompts/system.test.ts | 19 +- apps/daemon/tests/skills-delete-route.test.ts | 131 ++ apps/daemon/tests/skills.test.ts | 370 +++++- .../tests/static-resource-routes.test.ts | 4 + apps/web/src/App.tsx | 65 +- apps/web/src/components/ChatComposer.tsx | 237 +++- apps/web/src/components/ChatPane.tsx | 14 +- .../src/components/DesignSystemsSection.tsx | 215 ++++ apps/web/src/components/EntryView.tsx | 19 +- apps/web/src/components/LibrarySection.tsx | 524 -------- apps/web/src/components/ProjectView.tsx | 41 +- apps/web/src/components/SettingsDialog.tsx | 99 +- apps/web/src/components/SkillsSection.tsx | 836 ++++++++++++ apps/web/src/i18n/content.fr.ts | 97 ++ apps/web/src/i18n/content.ru.ts | 97 ++ apps/web/src/i18n/content.ts | 98 ++ apps/web/src/i18n/locales/ar.ts | 26 +- apps/web/src/i18n/locales/de.ts | 26 +- apps/web/src/i18n/locales/en.ts | 26 +- apps/web/src/i18n/locales/es-ES.ts | 26 +- apps/web/src/i18n/locales/fa.ts | 26 +- apps/web/src/i18n/locales/fr.ts | 26 +- apps/web/src/i18n/locales/hu.ts | 26 +- apps/web/src/i18n/locales/id.ts | 28 +- apps/web/src/i18n/locales/ja.ts | 26 +- apps/web/src/i18n/locales/ko.ts | 26 +- apps/web/src/i18n/locales/pl.ts | 26 +- apps/web/src/i18n/locales/pt-BR.ts | 26 +- apps/web/src/i18n/locales/ru.ts | 26 +- apps/web/src/i18n/locales/th.ts | 4 +- apps/web/src/i18n/locales/tr.ts | 26 +- apps/web/src/i18n/locales/uk.ts | 26 +- apps/web/src/i18n/locales/zh-CN.ts | 26 +- apps/web/src/i18n/locales/zh-TW.ts | 26 +- apps/web/src/i18n/types.ts | 22 +- apps/web/src/index.css | 438 +++++++ apps/web/src/providers/daemon.ts | 6 + apps/web/src/providers/registry.ts | 162 +++ .../src/utils/promptTemplateDsCategories.ts | 68 + .../ProjectView.pendingPrompt.test.tsx | 2 + .../ProjectView.run-cleanup.test.tsx | 1 + .../SettingsDialog.execution.test.tsx | 56 +- .../tests/components/SettingsDialog.test.ts | 6 +- design-templates/AGENTS.md | 34 + .../audio-jingle/SKILL.md | 0 .../audio-jingle/example.html | 0 .../blog-post/SKILL.md | 0 .../blog-post/example.html | 0 .../clinical-case-report/SKILL.md | 0 .../clinical-case-report/example.html | 0 .../examples/example-stemi.html | 0 .../references/case-formats.md | 0 .../references/checklist.md | 0 .../critique/SKILL.md | 0 .../critique/example.html | 0 .../dashboard/SKILL.md | 0 .../dashboard/example.html | 0 .../dating-web/SKILL.md | 0 .../dating-web/example.html | 0 .../dcf-valuation/SKILL.md | 0 .../dcf-valuation/references/sector-wacc.md | 0 .../digital-eguide/SKILL.md | 0 .../digital-eguide/example.html | 0 .../docs-page/SKILL.md | 0 .../docs-page/example.html | 0 .../email-marketing/SKILL.md | 0 .../email-marketing/example.html | 0 .../eng-runbook/SKILL.md | 0 .../eng-runbook/example.html | 0 .../finance-report/SKILL.md | 0 .../finance-report/example.html | 0 .../flowai-live-dashboard-template/SKILL.md | 0 .../assets/template.html | 0 .../example.html | 0 .../references/checklist.md | 0 .../gamified-app/SKILL.md | 0 .../gamified-app/example.html | 0 .../github-dashboard/SKILL.md | 0 .../github-dashboard/example.html | 0 .../github-dashboard/references/README.md | 0 .../references/artifact-example.json | 0 .../references/example-data.json | 0 .../references/provenance-example.json | 0 .../github-dashboard/references/template.html | 0 .../guizang-ppt/LICENSE | 0 .../guizang-ppt/README.en.md | 0 .../guizang-ppt/README.md | 0 .../guizang-ppt/README.pt-BR.md | 0 .../guizang-ppt/SKILL.md | 0 .../guizang-ppt/assets/example-slides.html | 0 .../guizang-ppt/assets/template.html | 0 .../guizang-ppt/references/checklist.md | 0 .../guizang-ppt/references/components.md | 0 .../guizang-ppt/references/layouts.md | 0 .../guizang-ppt/references/styles.md | 0 .../guizang-ppt/references/themes.md | 0 .../hr-onboarding/SKILL.md | 0 .../hr-onboarding/example.html | 0 .../html-ppt-course-module/SKILL.md | 0 .../html-ppt-course-module/example.html | 0 .../html-ppt-dir-key-nav-minimal/SKILL.md | 0 .../html-ppt-dir-key-nav-minimal/example.html | 0 .../html-ppt-graphify-dark-graph/SKILL.md | 0 .../html-ppt-graphify-dark-graph/example.html | 0 .../html-ppt-hermes-cyber-terminal/SKILL.md | 0 .../example.html | 0 .../SKILL.md | 0 .../example.html | 0 .../SKILL.md | 0 .../example.html | 0 .../html-ppt-pitch-deck/SKILL.md | 0 .../html-ppt-pitch-deck/example.html | 0 .../html-ppt-presenter-mode-reveal/SKILL.md | 0 .../example.html | 0 .../html-ppt-product-launch/SKILL.md | 0 .../html-ppt-product-launch/example.html | 0 .../html-ppt-taste-brutalist/SKILL.md | 0 .../html-ppt-taste-brutalist/example.html | 0 .../html-ppt-taste-editorial/SKILL.md | 0 .../html-ppt-taste-editorial/example.html | 0 .../html-ppt-tech-sharing/SKILL.md | 0 .../html-ppt-tech-sharing/example.html | 0 .../html-ppt-testing-safety-alert/SKILL.md | 0 .../example.html | 0 .../html-ppt-weekly-report/SKILL.md | 0 .../html-ppt-weekly-report/example.html | 0 .../html-ppt-xhs-pastel-card/SKILL.md | 0 .../html-ppt-xhs-pastel-card/example.html | 0 .../html-ppt-xhs-post/SKILL.md | 0 .../html-ppt-xhs-post/example.html | 0 .../html-ppt-xhs-white-editorial/SKILL.md | 0 .../html-ppt-xhs-white-editorial/example.html | 0 .../html-ppt-zhangzara-8-bit-orbit/LICENSE | 0 .../html-ppt-zhangzara-8-bit-orbit/SKILL.md | 0 .../example.html | 0 .../template.json | 0 .../LICENSE | 0 .../SKILL.md | 0 .../example.html | 0 .../template.json | 0 .../html-ppt-zhangzara-block-frame/LICENSE | 0 .../html-ppt-zhangzara-block-frame/SKILL.md | 0 .../example.html | 0 .../template.json | 0 .../LICENSE | 0 .../SKILL.md | 0 .../example.html | 0 .../template.json | 0 .../html-ppt-zhangzara-bold-poster/LICENSE | 0 .../html-ppt-zhangzara-bold-poster/SKILL.md | 0 .../example.html | 0 .../template.json | 0 .../html-ppt-zhangzara-broadside/LICENSE | 0 .../html-ppt-zhangzara-broadside/SKILL.md | 0 .../html-ppt-zhangzara-broadside/example.html | 0 .../template.json | 0 .../html-ppt-zhangzara-capsule/LICENSE | 0 .../html-ppt-zhangzara-capsule/SKILL.md | 0 .../html-ppt-zhangzara-capsule/example.html | 0 .../html-ppt-zhangzara-capsule/template.json | 0 .../html-ppt-zhangzara-cartesian/LICENSE | 0 .../html-ppt-zhangzara-cartesian/SKILL.md | 0 .../html-ppt-zhangzara-cartesian/example.html | 0 .../template.json | 0 .../html-ppt-zhangzara-cobalt-grid/LICENSE | 0 .../html-ppt-zhangzara-cobalt-grid/SKILL.md | 0 .../example.html | 0 .../template.json | 0 .../html-ppt-zhangzara-coral/LICENSE | 0 .../html-ppt-zhangzara-coral/SKILL.md | 0 .../html-ppt-zhangzara-coral/example.html | 0 .../html-ppt-zhangzara-coral/template.json | 0 .../html-ppt-zhangzara-creative-mode/LICENSE | 0 .../html-ppt-zhangzara-creative-mode/SKILL.md | 0 .../assets/deck-stage.js | 0 .../example.html | 0 .../template.json | 0 .../html-ppt-zhangzara-daisy-days/LICENSE | 0 .../html-ppt-zhangzara-daisy-days/SKILL.md | 0 .../example.html | 0 .../template.json | 0 .../LICENSE | 0 .../SKILL.md | 0 .../assets/deck-stage.js | 0 .../example.html | 0 .../template.json | 0 .../html-ppt-zhangzara-grove/LICENSE | 0 .../html-ppt-zhangzara-grove/SKILL.md | 0 .../html-ppt-zhangzara-grove/example.html | 0 .../html-ppt-zhangzara-grove/template.json | 0 .../html-ppt-zhangzara-long-table/LICENSE | 0 .../html-ppt-zhangzara-long-table/SKILL.md | 0 .../example.html | 0 .../template.json | 0 .../html-ppt-zhangzara-mat/LICENSE | 0 .../html-ppt-zhangzara-mat/SKILL.md | 0 .../html-ppt-zhangzara-mat/example.html | 0 .../html-ppt-zhangzara-mat/template.json | 0 .../html-ppt-zhangzara-monochrome/LICENSE | 0 .../html-ppt-zhangzara-monochrome/SKILL.md | 0 .../example.html | 0 .../template.json | 0 .../html-ppt-zhangzara-neo-grid-bold/LICENSE | 0 .../html-ppt-zhangzara-neo-grid-bold/SKILL.md | 0 .../assets/deck-stage.js | 0 .../example.html | 0 .../template.json | 0 .../LICENSE | 0 .../SKILL.md | 0 .../assets/deck-stage.js | 0 .../example.html | 0 .../template.json | 0 .../html-ppt-zhangzara-pin-and-paper/LICENSE | 0 .../html-ppt-zhangzara-pin-and-paper/SKILL.md | 0 .../assets/deck-stage.js | 0 .../assets/styles.css | 0 .../example.html | 0 .../template.json | 0 .../html-ppt-zhangzara-pink-script/LICENSE | 0 .../html-ppt-zhangzara-pink-script/SKILL.md | 0 .../assets/deck-stage.js | 0 .../example.html | 0 .../template.json | 0 .../html-ppt-zhangzara-playful/LICENSE | 0 .../html-ppt-zhangzara-playful/SKILL.md | 0 .../html-ppt-zhangzara-playful/example.html | 0 .../html-ppt-zhangzara-playful/template.json | 0 .../html-ppt-zhangzara-raw-grid/LICENSE | 0 .../html-ppt-zhangzara-raw-grid/SKILL.md | 0 .../html-ppt-zhangzara-raw-grid/example.html | 0 .../html-ppt-zhangzara-raw-grid/template.json | 0 .../html-ppt-zhangzara-retro-windows/LICENSE | 0 .../html-ppt-zhangzara-retro-windows/SKILL.md | 0 .../example.html | 0 .../template.json | 0 .../html-ppt-zhangzara-retro-zine/LICENSE | 0 .../html-ppt-zhangzara-retro-zine/SKILL.md | 0 .../example.html | 0 .../template.json | 0 .../html-ppt-zhangzara-sakura-chroma/LICENSE | 0 .../html-ppt-zhangzara-sakura-chroma/SKILL.md | 0 .../example.html | 0 .../template.json | 0 .../html-ppt-zhangzara-scatterbrain/LICENSE | 0 .../html-ppt-zhangzara-scatterbrain/SKILL.md | 0 .../example.html | 0 .../template.json | 0 .../html-ppt-zhangzara-signal/LICENSE | 0 .../html-ppt-zhangzara-signal/SKILL.md | 0 .../html-ppt-zhangzara-signal/example.html | 0 .../html-ppt-zhangzara-signal/template.json | 0 .../html-ppt-zhangzara-soft-editorial/LICENSE | 0 .../SKILL.md | 0 .../assets/deck-stage.js | 0 .../example.html | 0 .../template.json | 0 .../html-ppt-zhangzara-stencil-tablet/LICENSE | 0 .../SKILL.md | 0 .../assets/deck-stage.js | 0 .../example.html | 0 .../template.json | 0 .../html-ppt-zhangzara-studio/LICENSE | 0 .../html-ppt-zhangzara-studio/SKILL.md | 0 .../html-ppt-zhangzara-studio/example.html | 0 .../html-ppt-zhangzara-studio/template.json | 0 .../html-ppt-zhangzara-vellum/LICENSE | 0 .../html-ppt-zhangzara-vellum/SKILL.md | 0 .../html-ppt-zhangzara-vellum/example.html | 0 .../html-ppt-zhangzara-vellum/template.json | 0 .../html-ppt/.clawscan-allow | 0 {skills => design-templates}/html-ppt/LICENSE | 0 .../html-ppt/README.md | 0 .../html-ppt/README.pt-BR.md | 0 .../html-ppt/README.zh-CN.md | 0 .../html-ppt/SKILL.md | 0 .../html-ppt/assets/animations/animations.css | 0 .../html-ppt/assets/animations/fx-runtime.js | 0 .../html-ppt/assets/animations/fx/_util.js | 0 .../assets/animations/fx/chain-react.js | 0 .../assets/animations/fx/confetti-cannon.js | 0 .../assets/animations/fx/constellation.js | 0 .../assets/animations/fx/counter-explosion.js | 0 .../assets/animations/fx/data-stream.js | 0 .../html-ppt/assets/animations/fx/firework.js | 0 .../assets/animations/fx/galaxy-swirl.js | 0 .../assets/animations/fx/gradient-blob.js | 0 .../assets/animations/fx/knowledge-graph.js | 0 .../assets/animations/fx/letter-explode.js | 0 .../assets/animations/fx/magnetic-field.js | 0 .../assets/animations/fx/matrix-rain.js | 0 .../assets/animations/fx/neural-net.js | 0 .../assets/animations/fx/orbit-ring.js | 0 .../assets/animations/fx/particle-burst.js | 0 .../assets/animations/fx/shockwave.js | 0 .../assets/animations/fx/sparkle-trail.js | 0 .../assets/animations/fx/starfield.js | 0 .../assets/animations/fx/typewriter-multi.js | 0 .../assets/animations/fx/word-cascade.js | 0 .../html-ppt/assets/base.css | 0 .../html-ppt/assets/fonts.css | 0 .../html-ppt/assets/runtime.js | 0 .../html-ppt/assets/themes/academic-paper.css | 0 .../html-ppt/assets/themes/arctic-cool.css | 0 .../html-ppt/assets/themes/aurora.css | 0 .../html-ppt/assets/themes/bauhaus.css | 0 .../html-ppt/assets/themes/blueprint.css | 0 .../assets/themes/catppuccin-latte.css | 0 .../assets/themes/catppuccin-mocha.css | 0 .../assets/themes/corporate-clean.css | 0 .../html-ppt/assets/themes/cyberpunk-neon.css | 0 .../html-ppt/assets/themes/dracula.css | 0 .../assets/themes/editorial-serif.css | 0 .../assets/themes/engineering-whiteprint.css | 0 .../html-ppt/assets/themes/glassmorphism.css | 0 .../html-ppt/assets/themes/gruvbox-dark.css | 0 .../assets/themes/japanese-minimal.css | 0 .../html-ppt/assets/themes/magazine-bold.css | 0 .../html-ppt/assets/themes/memphis-pop.css | 0 .../html-ppt/assets/themes/midcentury.css | 0 .../html-ppt/assets/themes/minimal-white.css | 0 .../html-ppt/assets/themes/neo-brutalism.css | 0 .../html-ppt/assets/themes/news-broadcast.css | 0 .../html-ppt/assets/themes/nord.css | 0 .../html-ppt/assets/themes/pitch-deck-vc.css | 0 .../assets/themes/rainbow-gradient.css | 0 .../html-ppt/assets/themes/retro-tv.css | 0 .../html-ppt/assets/themes/rose-pine.css | 0 .../html-ppt/assets/themes/sharp-mono.css | 0 .../html-ppt/assets/themes/soft-pastel.css | 0 .../assets/themes/solarized-light.css | 0 .../html-ppt/assets/themes/sunset-warm.css | 0 .../html-ppt/assets/themes/swiss-grid.css | 0 .../html-ppt/assets/themes/terminal-green.css | 0 .../html-ppt/assets/themes/tokyo-night.css | 0 .../html-ppt/assets/themes/vaporwave.css | 0 .../assets/themes/xiaohongshu-white.css | 0 .../html-ppt/assets/themes/y2k-chrome.css | 0 .../html-ppt/docs/readme/_theme-cell.html | 0 .../html-ppt/docs/readme/animations.png | Bin .../html-ppt/docs/readme/hero.gif | Bin .../html-ppt/docs/readme/layouts-live.gif | Bin .../html-ppt/docs/readme/layouts.png | Bin .../docs/readme/montage-animations.html | 0 .../html-ppt/docs/readme/montage-layouts.html | 0 .../docs/readme/montage-templates.html | 0 .../html-ppt/docs/readme/montage-themes.html | 0 .../html-ppt/docs/readme/presenter-mode.png | Bin .../html-ppt/docs/readme/templates.png | Bin .../html-ppt/docs/readme/themes.png | Bin .../html-ppt/examples/demo-deck/index.html | 0 .../html-ppt/references/animations.md | 0 .../html-ppt/references/authoring-guide.md | 0 .../html-ppt/references/full-decks.md | 0 .../html-ppt/references/layouts.md | 0 .../html-ppt/references/presenter-mode.md | 0 .../html-ppt/references/themes.md | 0 .../html-ppt/scripts/new-deck.sh | 0 .../html-ppt/scripts/render.sh | 0 .../animation-showcase_01.png | Bin .../animation-showcase_02.png | Bin .../animation-showcase_03.png | Bin .../animation-showcase_04.png | Bin .../animation-showcase_05.png | Bin .../animation-showcase_06.png | Bin .../animation-showcase_07.png | Bin .../animation-showcase_08.png | Bin .../animation-showcase_09.png | Bin .../animation-showcase_10.png | Bin .../animation-showcase_11.png | Bin .../animation-showcase_12.png | Bin .../animation-showcase_13.png | Bin .../animation-showcase_14.png | Bin .../animation-showcase_15.png | Bin .../animation-showcase_16.png | Bin .../animation-showcase_17.png | Bin .../animation-showcase_18.png | Bin .../animation-showcase_19.png | Bin .../animation-showcase_20.png | Bin .../theme-showcase/theme-showcase_01.png | Bin .../theme-showcase/theme-showcase_02.png | Bin .../theme-showcase/theme-showcase_03.png | Bin .../theme-showcase/theme-showcase_04.png | Bin .../theme-showcase/theme-showcase_05.png | Bin .../theme-showcase/theme-showcase_06.png | Bin .../theme-showcase/theme-showcase_07.png | Bin .../theme-showcase/theme-showcase_08.png | Bin .../theme-showcase/theme-showcase_09.png | Bin .../theme-showcase/theme-showcase_10.png | Bin .../theme-showcase/theme-showcase_11.png | Bin .../theme-showcase/theme-showcase_12.png | Bin .../theme-showcase/theme-showcase_13.png | Bin .../theme-showcase/theme-showcase_14.png | Bin .../theme-showcase/theme-showcase_15.png | Bin .../theme-showcase/theme-showcase_16.png | Bin .../theme-showcase/theme-showcase_17.png | Bin .../theme-showcase/theme-showcase_18.png | Bin .../theme-showcase/theme-showcase_19.png | Bin .../theme-showcase/theme-showcase_20.png | Bin .../theme-showcase/theme-showcase_21.png | Bin .../theme-showcase/theme-showcase_22.png | Bin .../theme-showcase/theme-showcase_23.png | Bin .../theme-showcase/theme-showcase_24.png | Bin .../theme-showcase/theme-showcase_25.png | Bin .../theme-showcase/theme-showcase_26.png | Bin .../theme-showcase/theme-showcase_27.png | Bin .../theme-showcase/theme-showcase_28.png | Bin .../theme-showcase/theme-showcase_29.png | Bin .../theme-showcase/theme-showcase_30.png | Bin .../theme-showcase/theme-showcase_31.png | Bin .../theme-showcase/theme-showcase_32.png | Bin .../theme-showcase/theme-showcase_33.png | Bin .../theme-showcase/theme-showcase_34.png | Bin .../theme-showcase/theme-showcase_35.png | Bin .../theme-showcase/theme-showcase_36.png | Bin .../templates/animation-showcase.html | 0 .../html-ppt/templates/deck.html | 0 .../html-ppt/templates/full-decks-index.html | 0 .../full-decks/course-module/README.md | 0 .../full-decks/course-module/index.html | 0 .../full-decks/course-module/style.css | 0 .../full-decks/dir-key-nav-minimal/README.md | 0 .../full-decks/dir-key-nav-minimal/index.html | 0 .../full-decks/dir-key-nav-minimal/style.css | 0 .../full-decks/graphify-dark-graph/README.md | 0 .../full-decks/graphify-dark-graph/index.html | 0 .../full-decks/graphify-dark-graph/style.css | 0 .../hermes-cyber-terminal/README.md | 0 .../hermes-cyber-terminal/index.html | 0 .../hermes-cyber-terminal/style.css | 0 .../knowledge-arch-blueprint/README.md | 0 .../knowledge-arch-blueprint/index.html | 0 .../knowledge-arch-blueprint/style.css | 0 .../obsidian-claude-gradient/README.md | 0 .../obsidian-claude-gradient/index.html | 0 .../obsidian-claude-gradient/style.css | 0 .../templates/full-decks/pitch-deck/README.md | 0 .../full-decks/pitch-deck/index.html | 0 .../templates/full-decks/pitch-deck/style.css | 0 .../presenter-mode-reveal/README.md | 0 .../presenter-mode-reveal/index.html | 0 .../presenter-mode-reveal/style.css | 0 .../full-decks/product-launch/README.md | 0 .../full-decks/product-launch/index.html | 0 .../full-decks/product-launch/style.css | 0 .../full-decks/tech-sharing/README.md | 0 .../full-decks/tech-sharing/index.html | 0 .../full-decks/tech-sharing/style.css | 0 .../full-decks/testing-safety-alert/README.md | 0 .../testing-safety-alert/index.html | 0 .../full-decks/testing-safety-alert/style.css | 0 .../full-decks/weekly-report/README.md | 0 .../full-decks/weekly-report/index.html | 0 .../full-decks/weekly-report/style.css | 0 .../full-decks/xhs-pastel-card/README.md | 0 .../full-decks/xhs-pastel-card/index.html | 0 .../full-decks/xhs-pastel-card/style.css | 0 .../templates/full-decks/xhs-post/README.md | 0 .../templates/full-decks/xhs-post/index.html | 0 .../templates/full-decks/xhs-post/style.css | 0 .../full-decks/xhs-white-editorial/README.md | 0 .../full-decks/xhs-white-editorial/index.html | 0 .../full-decks/xhs-white-editorial/style.css | 0 .../html-ppt/templates/layout-showcase.html | 0 .../templates/single-page/arch-diagram.html | 0 .../templates/single-page/big-quote.html | 0 .../templates/single-page/bullets.html | 0 .../templates/single-page/chart-bar.html | 0 .../templates/single-page/chart-line.html | 0 .../templates/single-page/chart-pie.html | 0 .../templates/single-page/chart-radar.html | 0 .../html-ppt/templates/single-page/code.html | 0 .../templates/single-page/comparison.html | 0 .../html-ppt/templates/single-page/cover.html | 0 .../html-ppt/templates/single-page/cta.html | 0 .../html-ppt/templates/single-page/diff.html | 0 .../templates/single-page/flow-diagram.html | 0 .../html-ppt/templates/single-page/gantt.html | 0 .../templates/single-page/image-grid.html | 0 .../templates/single-page/image-hero.html | 0 .../templates/single-page/kpi-grid.html | 0 .../templates/single-page/mindmap.html | 0 .../templates/single-page/process-steps.html | 0 .../templates/single-page/pros-cons.html | 0 .../templates/single-page/roadmap.html | 0 .../single-page/section-divider.html | 0 .../templates/single-page/stat-highlight.html | 0 .../html-ppt/templates/single-page/table.html | 0 .../templates/single-page/terminal.html | 0 .../templates/single-page/thanks.html | 0 .../templates/single-page/three-column.html | 0 .../templates/single-page/timeline.html | 0 .../html-ppt/templates/single-page/toc.html | 0 .../templates/single-page/todo-checklist.html | 0 .../templates/single-page/two-column.html | 0 .../html-ppt/templates/theme-showcase.html | 0 .../hyperframes/SKILL.md | 0 .../hyperframes/data-in-motion.md | 0 .../hyperframes/house-style.md | 0 .../hyperframes/palettes/bold-energetic.md | 0 .../hyperframes/palettes/clean-corporate.md | 0 .../hyperframes/palettes/dark-premium.md | 0 .../hyperframes/palettes/jewel-rich.md | 0 .../hyperframes/palettes/monochrome.md | 0 .../hyperframes/palettes/nature-earth.md | 0 .../hyperframes/palettes/neon-electric.md | 0 .../hyperframes/palettes/pastel-soft.md | 0 .../hyperframes/palettes/warm-editorial.md | 0 .../hyperframes/patterns.md | 0 .../hyperframes/references/audio-reactive.md | 0 .../hyperframes/references/captions.md | 0 .../hyperframes/references/css-patterns.md | 0 .../references/dynamic-techniques.md | 0 .../hyperframes/references/html-in-canvas.md | 0 .../references/motion-principles.md | 0 .../references/transcript-guide.md | 0 .../hyperframes/references/transitions.md | 0 .../references/transitions/catalog.md | 0 .../references/transitions/css-3d.md | 0 .../references/transitions/css-blur.md | 0 .../references/transitions/css-cover.md | 0 .../references/transitions/css-destruction.md | 0 .../references/transitions/css-dissolve.md | 0 .../references/transitions/css-distortion.md | 0 .../references/transitions/css-grid.md | 0 .../references/transitions/css-light.md | 0 .../references/transitions/css-mechanical.md | 0 .../references/transitions/css-other.md | 0 .../references/transitions/css-push.md | 0 .../references/transitions/css-radial.md | 0 .../references/transitions/css-scale.md | 0 .../hyperframes/references/tts.md | 0 .../hyperframes/references/typography.md | 0 .../hyperframes/scripts/animation-map.mjs | 0 .../hyperframes/scripts/contrast-report.mjs | 0 .../hyperframes/scripts/package-loader.mjs | 0 .../hyperframes/visual-styles.md | 0 .../ib-pitch-book/SKILL.md | 0 .../ib-pitch-book/assets/template.html | 0 .../ib-pitch-book/example.html | 0 .../ib-pitch-book/references/attribution.md | 0 .../ib-pitch-book/references/checklist.md | 0 .../ib-pitch-book/references/compliance.md | 0 .../ib-pitch-book/references/conventions.md | 0 .../image-poster/SKILL.md | 0 .../image-poster/example.html | 0 {skills => design-templates}/invoice/SKILL.md | 0 .../invoice/example.html | 0 .../kami-deck/README.md | 0 .../kami-deck/SKILL.md | 0 .../kami-deck/example.html | 0 .../kami-landing/README.md | 0 .../kami-landing/SKILL.md | 0 .../kami-landing/example.html | 0 .../kanban-board/SKILL.md | 0 .../kanban-board/example.html | 0 .../last30days/LICENSE | 0 .../last30days/SKILL.md | 0 .../last30days/references/save-html-brief.md | 0 .../last30days/scripts/briefing.py | 0 .../last30days/scripts/last30days.py | 0 .../last30days/scripts/lib/__init__.py | 0 .../last30days/scripts/lib/bird_x.py | 0 .../last30days/scripts/lib/bluesky.py | 0 .../last30days/scripts/lib/categories.py | 0 .../last30days/scripts/lib/chrome_cookies.py | 0 .../last30days/scripts/lib/cluster.py | 0 .../last30days/scripts/lib/competitors.py | 0 .../last30days/scripts/lib/cookie_extract.py | 0 .../last30days/scripts/lib/dates.py | 0 .../last30days/scripts/lib/dedupe.py | 0 .../last30days/scripts/lib/entity_extract.py | 0 .../last30days/scripts/lib/env.py | 0 .../last30days/scripts/lib/fanout.py | 0 .../last30days/scripts/lib/fusion.py | 0 .../last30days/scripts/lib/github.py | 0 .../last30days/scripts/lib/grounding.py | 0 .../last30days/scripts/lib/hackernews.py | 0 .../last30days/scripts/lib/html_render.py | 0 .../last30days/scripts/lib/http.py | 0 .../last30days/scripts/lib/instagram.py | 0 .../last30days/scripts/lib/log.py | 0 .../last30days/scripts/lib/normalize.py | 0 .../last30days/scripts/lib/perplexity.py | 0 .../last30days/scripts/lib/pinterest.py | 0 .../last30days/scripts/lib/pipeline.py | 0 .../last30days/scripts/lib/planner.py | 0 .../last30days/scripts/lib/polymarket.py | 0 .../last30days/scripts/lib/preflight.py | 0 .../last30days/scripts/lib/providers.py | 0 .../last30days/scripts/lib/quality_nudge.py | 0 .../last30days/scripts/lib/query.py | 0 .../last30days/scripts/lib/reddit.py | 0 .../last30days/scripts/lib/reddit_enrich.py | 0 .../last30days/scripts/lib/reddit_public.py | 0 .../last30days/scripts/lib/relevance.py | 0 .../last30days/scripts/lib/render.py | 0 .../last30days/scripts/lib/rerank.py | 0 .../last30days/scripts/lib/resolve.py | 0 .../last30days/scripts/lib/safari_cookies.py | 0 .../last30days/scripts/lib/schema.py | 0 .../last30days/scripts/lib/setup_wizard.py | 0 .../last30days/scripts/lib/signals.py | 0 .../last30days/scripts/lib/snippet.py | 0 .../last30days/scripts/lib/subproc.py | 0 .../last30days/scripts/lib/threads.py | 0 .../last30days/scripts/lib/tiktok.py | 0 .../last30days/scripts/lib/truthsocial.py | 0 .../last30days/scripts/lib/ui.py | 0 .../scripts/lib/vendor/bird-search/LICENSE | 0 .../lib/vendor/bird-search/bird-search.mjs | 0 .../lib/vendor/bird-search/lib/cookies.js | 0 .../lib/vendor/bird-search/lib/features.json | 0 .../vendor/bird-search/lib/paginate-cursor.js | 0 .../lib/vendor/bird-search/lib/query-ids.json | 0 .../bird-search/lib/runtime-features.js | 0 .../bird-search/lib/runtime-query-ids.js | 0 .../bird-search/lib/twitter-client-base.js | 0 .../lib/twitter-client-constants.js | 0 .../lib/twitter-client-features.js | 0 .../bird-search/lib/twitter-client-search.js | 0 .../bird-search/lib/twitter-client-types.js | 0 .../bird-search/lib/twitter-client-utils.js | 0 .../lib/vendor/bird-search/package.json | 0 .../last30days/scripts/lib/xai_x.py | 0 .../last30days/scripts/lib/xiaohongshu_api.py | 0 .../last30days/scripts/lib/xquik.py | 0 .../last30days/scripts/lib/xurl_x.py | 0 .../last30days/scripts/lib/youtube_yt.py | 0 .../last30days/scripts/store.py | 0 .../last30days/scripts/watchlist.py | 0 .../live-artifact/SKILL.md | 0 .../assets/templates/clinic-console/README.md | 0 .../assets/templates/clinic-console/data.json | 0 .../templates/clinic-console/template.html | 0 .../examples/baby-health-live.html | 0 .../examples/competitor-radar-live.html | 0 .../examples/crm-table-live.html | 0 .../examples/crypto-dashboard.html | 0 .../examples/monday-operator-live.html | 0 .../examples/stock-dashboard.html | 0 .../stock-portfolio-live/artifact.json | 0 .../examples/stock-portfolio-live/data.json | 0 .../stock-portfolio-live/provenance.json | 0 .../stock-portfolio-live/template.html | 0 .../references/artifact-schema.md | 0 .../references/connector-policy.md | 0 .../references/refresh-contract.md | 0 .../live-dashboard/SKILL.md | 0 .../live-dashboard/assets/template.html | 0 .../live-dashboard/example.html | 0 .../live-dashboard/references/checklist.md | 0 .../live-dashboard/references/components.md | 0 .../live-dashboard/references/connectors.md | 0 .../live-dashboard/references/layouts.md | 0 .../magazine-poster/SKILL.md | 0 .../magazine-poster/example.html | 0 .../meeting-notes/SKILL.md | 0 .../meeting-notes/example.html | 0 .../mobile-app/SKILL.md | 0 .../mobile-app/assets/template.html | 0 .../mobile-app/example.html | 0 .../mobile-app/references/checklist.md | 0 .../mobile-app/references/layouts.md | 0 .../mobile-onboarding/SKILL.md | 0 .../mobile-onboarding/example.html | 0 .../motion-frames/SKILL.md | 0 .../motion-frames/example.html | 0 .../open-design-landing-deck/README.md | 0 .../open-design-landing-deck/SKILL.md | 0 .../open-design-landing-deck/example.html | 0 .../inputs.example.json | 0 .../open-design-landing-deck/schema.ts | 0 .../scripts/compose.ts | 0 .../open-design-landing/README.md | 0 .../open-design-landing/SKILL.md | 0 .../open-design-landing/assets/about.png | Bin .../assets/capabilities.png | Bin .../open-design-landing/assets/cta.png | Bin .../open-design-landing/assets/hero.png | Bin .../assets/image-manifest.json | 0 .../assets/imagegen-prompts.md | 0 .../open-design-landing/assets/lab-1.png | Bin .../open-design-landing/assets/lab-2.png | Bin .../open-design-landing/assets/lab-3.png | Bin .../open-design-landing/assets/lab-4.png | Bin .../open-design-landing/assets/lab-5.png | Bin .../open-design-landing/assets/method-1.png | Bin .../open-design-landing/assets/method-2.png | Bin .../open-design-landing/assets/method-3.png | Bin .../open-design-landing/assets/method-4.png | Bin .../assets/testimonial.png | Bin .../open-design-landing/assets/work-1.png | Bin .../open-design-landing/assets/work-2.png | Bin .../open-design-landing/example.html | 0 .../open-design-landing/inputs.example.json | 0 .../open-design-landing/schema.ts | 0 .../open-design-landing/scripts/compose.ts | 0 .../open-design-landing/scripts/imagegen.ts | 0 .../scripts/placeholder.ts | 0 .../open-design-landing/styles.css | 0 .../orbit-general/SKILL.md | 0 .../orbit-general/example.html | 0 .../orbit-github/SKILL.md | 0 .../orbit-github/example.html | 0 .../orbit-gmail/SKILL.md | 0 .../orbit-gmail/example.html | 0 .../orbit-linear/SKILL.md | 0 .../orbit-linear/example.html | 0 .../orbit-notion/SKILL.md | 0 .../orbit-notion/example.html | 0 {skills => design-templates}/pm-spec/SKILL.md | 0 .../pm-spec/example.html | 0 .../pricing-page/SKILL.md | 0 .../pricing-page/example.html | 0 .../replit-deck/SKILL.md | 0 .../replit-deck/assets/template.html | 0 .../replit-deck/examples/README.md | 0 .../replit-deck/examples/example-atlas.html | 0 .../examples/example-bluehouse.html | 0 .../replit-deck/examples/example-helix.html | 0 .../replit-deck/examples/example-holm.html | 0 .../replit-deck/references/checklist.md | 0 .../replit-deck/references/components.md | 0 .../replit-deck/references/layouts.md | 0 .../replit-deck/references/themes.md | 0 .../saas-landing/SKILL.md | 0 .../saas-landing/example.html | 0 .../simple-deck/SKILL.md | 0 .../simple-deck/assets/template.html | 0 .../simple-deck/example.html | 0 .../simple-deck/references/checklist.md | 0 .../simple-deck/references/layouts.md | 0 .../social-carousel/SKILL.md | 0 .../social-carousel/example.html | 0 .../social-media-dashboard/.preview/hero.png | Bin .../social-media-dashboard/SKILL.md | 0 .../social-media-dashboard/example.html | 0 .../SKILL.md | 0 .../assets/template.html | 0 .../example.html | 0 .../references/checklist.md | 0 .../sprite-animation/SKILL.md | 0 .../sprite-animation/example.html | 0 .../team-okrs/SKILL.md | 0 .../team-okrs/example.html | 0 .../SKILL.md | 0 .../assets/template.html | 0 .../example.html | 0 .../references/checklist.md | 0 {skills => design-templates}/tweaks/SKILL.md | 0 .../tweaks/assets/wrap.html | 0 .../tweaks/example.html | 0 .../video-shortform/SKILL.md | 0 .../video-shortform/example.html | 0 .../waitlist-page/SKILL.md | 0 .../waitlist-page/assets/template.html | 0 .../waitlist-page/example.html | 0 .../waitlist-page/references/checklist.md | 0 .../web-prototype-taste-brutalist/SKILL.md | 0 .../example.html | 0 .../web-prototype-taste-editorial/SKILL.md | 0 .../example.html | 0 .../web-prototype-taste-soft/SKILL.md | 0 .../web-prototype-taste-soft/example.html | 0 .../web-prototype/SKILL.md | 0 .../web-prototype/assets/template.html | 0 .../web-prototype/example.html | 0 .../web-prototype/references/checklist.md | 0 .../web-prototype/references/layouts.md | 0 .../weekly-update/SKILL.md | 0 .../weekly-update/example.html | 0 .../wireframe-sketch/SKILL.md | 0 .../wireframe-sketch/example.html | 0 .../x-research/SKILL.md | 0 e2e/tests/localized-content.test.ts | 26 +- e2e/ui/app.test.ts | 55 +- e2e/ui/entry-chrome-flows.test.ts | 4 +- package.json | 1 + packages/contracts/src/api/chat.ts | 6 + packages/contracts/src/api/registry.ts | 70 +- scripts/guard.ts | 24 +- scripts/seed-curated-design-skills.ts | 1124 +++++++++++++++++ skills/AGENTS.md | 54 + skills/ad-creative/SKILL.md | 43 + skills/ai-music-album/SKILL.md | 43 + skills/algorithmic-art/SKILL.md | 44 + skills/apple-hig/SKILL.md | 43 + skills/artifacts-builder/SKILL.md | 42 + skills/brainstorming/SKILL.md | 43 + skills/brand-guidelines/SKILL.md | 42 + skills/canvas-design/SKILL.md | 45 + skills/color-expert/SKILL.md | 43 + skills/competitive-ads-extractor/SKILL.md | 42 + skills/copywriting/SKILL.md | 43 + skills/creative-director/SKILL.md | 43 + skills/d3-visualization/SKILL.md | 43 + skills/design-consultation/SKILL.md | 42 + skills/design-md/SKILL.md | 42 + skills/design-review/SKILL.md | 42 + skills/doc/SKILL.md | 42 + skills/docx/SKILL.md | 43 + skills/domain-name-brainstormer/SKILL.md | 43 + skills/enhance-prompt/SKILL.md | 42 + skills/fal-3d/SKILL.md | 43 + skills/fal-generate/SKILL.md | 43 + skills/fal-image-edit/SKILL.md | 43 + skills/fal-kling-o3/SKILL.md | 42 + skills/fal-lip-sync/SKILL.md | 43 + skills/fal-realtime/SKILL.md | 42 + skills/fal-restore/SKILL.md | 44 + skills/fal-train/SKILL.md | 43 + skills/fal-tryon/SKILL.md | 43 + skills/fal-upscale/SKILL.md | 43 + skills/fal-video-edit/SKILL.md | 43 + skills/fal-vision/SKILL.md | 44 + skills/figma-code-connect-components/SKILL.md | 42 + .../figma-create-design-system-rules/SKILL.md | 42 + skills/figma-create-new-file/SKILL.md | 41 + skills/figma-generate-design/SKILL.md | 42 + skills/figma-generate-library/SKILL.md | 42 + skills/figma-implement-design/SKILL.md | 42 + skills/figma-use/SKILL.md | 42 + skills/flutter-animating-apps/SKILL.md | 42 + skills/frontend-design/SKILL.md | 43 + skills/frontend-dev/SKILL.md | 43 + skills/frontend-skill/SKILL.md | 42 + skills/frontend-slides/SKILL.md | 43 + skills/full-page-screenshot/SKILL.md | 42 + skills/gif-sticker-maker/SKILL.md | 43 + skills/gsap-core/SKILL.md | 43 + skills/gsap-react/SKILL.md | 43 + skills/gsap-scrolltrigger/SKILL.md | 43 + skills/gsap-timeline/SKILL.md | 42 + skills/hand-drawn-diagrams/SKILL.md | 42 + skills/image-enhancer/SKILL.md | 43 + skills/imagegen/SKILL.md | 44 + skills/imagen/SKILL.md | 43 + skills/marketing-psychology/SKILL.md | 43 + skills/minimax-docx/SKILL.md | 43 + skills/minimax-pdf/SKILL.md | 43 + skills/nanobanana-ppt/SKILL.md | 43 + skills/paywall-upgrade-cro/SKILL.md | 43 + skills/pdf/SKILL.md | 43 + skills/pixelbin-media/SKILL.md | 43 + skills/plan-design-review/SKILL.md | 42 + skills/platform-design/SKILL.md | 43 + skills/pptx-generator/SKILL.md | 42 + skills/pptx/SKILL.md | 43 + skills/remotion/SKILL.md | 43 + skills/replicate/SKILL.md | 42 + skills/screenshot/SKILL.md | 42 + skills/screenshots-marketing/SKILL.md | 42 + skills/shadcn-ui/SKILL.md | 42 + skills/shader-dev/SKILL.md | 43 + skills/slack-gif-creator/SKILL.md | 42 + skills/slides/SKILL.md | 42 + skills/sora/SKILL.md | 43 + skills/speech/SKILL.md | 42 + skills/stitch-loop/SKILL.md | 42 + skills/swiftui-design/SKILL.md | 43 + skills/taste-skill/SKILL.md | 43 + skills/theme-factory/SKILL.md | 43 + skills/threejs/SKILL.md | 43 + skills/ui-skills/SKILL.md | 42 + skills/ui-ux-pro-max/SKILL.md | 42 + skills/venice-audio-music/SKILL.md | 43 + skills/venice-audio-speech/SKILL.md | 43 + skills/venice-image-edit/SKILL.md | 41 + skills/venice-image-generate/SKILL.md | 41 + skills/venice-video/SKILL.md | 41 + skills/video-downloader/SKILL.md | 42 + skills/web-artifacts-builder/SKILL.md | 42 + skills/web-design-guidelines/SKILL.md | 42 + skills/wpds/SKILL.md | 42 + skills/youtube-clipper/SKILL.md | 42 + specs/current/skills-and-design-templates.md | 110 ++ tools/pack/src/resources.ts | 5 + tools/pack/tests/resources.test.ts | 8 + 883 files changed, 9490 insertions(+), 916 deletions(-) create mode 100644 apps/daemon/tests/skills-delete-route.test.ts create mode 100644 apps/web/src/components/DesignSystemsSection.tsx delete mode 100644 apps/web/src/components/LibrarySection.tsx create mode 100644 apps/web/src/components/SkillsSection.tsx create mode 100644 apps/web/src/utils/promptTemplateDsCategories.ts create mode 100644 design-templates/AGENTS.md rename {skills => design-templates}/audio-jingle/SKILL.md (100%) rename {skills => design-templates}/audio-jingle/example.html (100%) rename {skills => design-templates}/blog-post/SKILL.md (100%) rename {skills => design-templates}/blog-post/example.html (100%) rename {skills => design-templates}/clinical-case-report/SKILL.md (100%) rename {skills => design-templates}/clinical-case-report/example.html (100%) rename {skills => design-templates}/clinical-case-report/examples/example-stemi.html (100%) rename {skills => design-templates}/clinical-case-report/references/case-formats.md (100%) rename {skills => design-templates}/clinical-case-report/references/checklist.md (100%) rename {skills => design-templates}/critique/SKILL.md (100%) rename {skills => design-templates}/critique/example.html (100%) rename {skills => design-templates}/dashboard/SKILL.md (100%) rename {skills => design-templates}/dashboard/example.html (100%) rename {skills => design-templates}/dating-web/SKILL.md (100%) rename {skills => design-templates}/dating-web/example.html (100%) rename {skills => design-templates}/dcf-valuation/SKILL.md (100%) rename {skills => design-templates}/dcf-valuation/references/sector-wacc.md (100%) rename {skills => design-templates}/digital-eguide/SKILL.md (100%) rename {skills => design-templates}/digital-eguide/example.html (100%) rename {skills => design-templates}/docs-page/SKILL.md (100%) rename {skills => design-templates}/docs-page/example.html (100%) rename {skills => design-templates}/email-marketing/SKILL.md (100%) rename {skills => design-templates}/email-marketing/example.html (100%) rename {skills => design-templates}/eng-runbook/SKILL.md (100%) rename {skills => design-templates}/eng-runbook/example.html (100%) rename {skills => design-templates}/finance-report/SKILL.md (100%) rename {skills => design-templates}/finance-report/example.html (100%) rename {skills => design-templates}/flowai-live-dashboard-template/SKILL.md (100%) rename {skills => design-templates}/flowai-live-dashboard-template/assets/template.html (100%) rename {skills => design-templates}/flowai-live-dashboard-template/example.html (100%) rename {skills => design-templates}/flowai-live-dashboard-template/references/checklist.md (100%) rename {skills => design-templates}/gamified-app/SKILL.md (100%) rename {skills => design-templates}/gamified-app/example.html (100%) rename {skills => design-templates}/github-dashboard/SKILL.md (100%) rename {skills => design-templates}/github-dashboard/example.html (100%) rename {skills => design-templates}/github-dashboard/references/README.md (100%) rename {skills => design-templates}/github-dashboard/references/artifact-example.json (100%) rename {skills => design-templates}/github-dashboard/references/example-data.json (100%) rename {skills => design-templates}/github-dashboard/references/provenance-example.json (100%) rename {skills => design-templates}/github-dashboard/references/template.html (100%) rename {skills => design-templates}/guizang-ppt/LICENSE (100%) rename {skills => design-templates}/guizang-ppt/README.en.md (100%) rename {skills => design-templates}/guizang-ppt/README.md (100%) rename {skills => design-templates}/guizang-ppt/README.pt-BR.md (100%) rename {skills => design-templates}/guizang-ppt/SKILL.md (100%) rename {skills => design-templates}/guizang-ppt/assets/example-slides.html (100%) rename {skills => design-templates}/guizang-ppt/assets/template.html (100%) rename {skills => design-templates}/guizang-ppt/references/checklist.md (100%) rename {skills => design-templates}/guizang-ppt/references/components.md (100%) rename {skills => design-templates}/guizang-ppt/references/layouts.md (100%) rename {skills => design-templates}/guizang-ppt/references/styles.md (100%) rename {skills => design-templates}/guizang-ppt/references/themes.md (100%) rename {skills => design-templates}/hr-onboarding/SKILL.md (100%) rename {skills => design-templates}/hr-onboarding/example.html (100%) rename {skills => design-templates}/html-ppt-course-module/SKILL.md (100%) rename {skills => design-templates}/html-ppt-course-module/example.html (100%) rename {skills => design-templates}/html-ppt-dir-key-nav-minimal/SKILL.md (100%) rename {skills => design-templates}/html-ppt-dir-key-nav-minimal/example.html (100%) rename {skills => design-templates}/html-ppt-graphify-dark-graph/SKILL.md (100%) rename {skills => design-templates}/html-ppt-graphify-dark-graph/example.html (100%) rename {skills => design-templates}/html-ppt-hermes-cyber-terminal/SKILL.md (100%) rename {skills => design-templates}/html-ppt-hermes-cyber-terminal/example.html (100%) rename {skills => design-templates}/html-ppt-knowledge-arch-blueprint/SKILL.md (100%) rename {skills => design-templates}/html-ppt-knowledge-arch-blueprint/example.html (100%) rename {skills => design-templates}/html-ppt-obsidian-claude-gradient/SKILL.md (100%) rename {skills => design-templates}/html-ppt-obsidian-claude-gradient/example.html (100%) rename {skills => design-templates}/html-ppt-pitch-deck/SKILL.md (100%) rename {skills => design-templates}/html-ppt-pitch-deck/example.html (100%) rename {skills => design-templates}/html-ppt-presenter-mode-reveal/SKILL.md (100%) rename {skills => design-templates}/html-ppt-presenter-mode-reveal/example.html (100%) rename {skills => design-templates}/html-ppt-product-launch/SKILL.md (100%) rename {skills => design-templates}/html-ppt-product-launch/example.html (100%) rename {skills => design-templates}/html-ppt-taste-brutalist/SKILL.md (100%) rename {skills => design-templates}/html-ppt-taste-brutalist/example.html (100%) rename {skills => design-templates}/html-ppt-taste-editorial/SKILL.md (100%) rename {skills => design-templates}/html-ppt-taste-editorial/example.html (100%) rename {skills => design-templates}/html-ppt-tech-sharing/SKILL.md (100%) rename {skills => design-templates}/html-ppt-tech-sharing/example.html (100%) rename {skills => design-templates}/html-ppt-testing-safety-alert/SKILL.md (100%) rename {skills => design-templates}/html-ppt-testing-safety-alert/example.html (100%) rename {skills => design-templates}/html-ppt-weekly-report/SKILL.md (100%) rename {skills => design-templates}/html-ppt-weekly-report/example.html (100%) rename {skills => design-templates}/html-ppt-xhs-pastel-card/SKILL.md (100%) rename {skills => design-templates}/html-ppt-xhs-pastel-card/example.html (100%) rename {skills => design-templates}/html-ppt-xhs-post/SKILL.md (100%) rename {skills => design-templates}/html-ppt-xhs-post/example.html (100%) rename {skills => design-templates}/html-ppt-xhs-white-editorial/SKILL.md (100%) rename {skills => design-templates}/html-ppt-xhs-white-editorial/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-8-bit-orbit/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-8-bit-orbit/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-8-bit-orbit/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-8-bit-orbit/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-biennale-yellow/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-biennale-yellow/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-biennale-yellow/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-biennale-yellow/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-block-frame/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-block-frame/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-block-frame/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-block-frame/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-blue-professional/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-blue-professional/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-blue-professional/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-blue-professional/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-bold-poster/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-bold-poster/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-bold-poster/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-bold-poster/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-broadside/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-broadside/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-broadside/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-broadside/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-capsule/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-capsule/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-capsule/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-capsule/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-cartesian/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-cartesian/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-cartesian/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-cartesian/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-cobalt-grid/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-cobalt-grid/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-cobalt-grid/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-cobalt-grid/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-coral/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-coral/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-coral/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-coral/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-creative-mode/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-creative-mode/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-creative-mode/assets/deck-stage.js (100%) rename {skills => design-templates}/html-ppt-zhangzara-creative-mode/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-creative-mode/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-daisy-days/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-daisy-days/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-daisy-days/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-daisy-days/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-editorial-tri-tone/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-editorial-tri-tone/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-editorial-tri-tone/assets/deck-stage.js (100%) rename {skills => design-templates}/html-ppt-zhangzara-editorial-tri-tone/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-editorial-tri-tone/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-grove/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-grove/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-grove/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-grove/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-long-table/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-long-table/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-long-table/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-long-table/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-mat/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-mat/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-mat/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-mat/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-monochrome/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-monochrome/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-monochrome/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-monochrome/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-neo-grid-bold/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-neo-grid-bold/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-neo-grid-bold/assets/deck-stage.js (100%) rename {skills => design-templates}/html-ppt-zhangzara-neo-grid-bold/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-neo-grid-bold/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-peoples-platform/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-peoples-platform/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-peoples-platform/assets/deck-stage.js (100%) rename {skills => design-templates}/html-ppt-zhangzara-peoples-platform/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-peoples-platform/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-pin-and-paper/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-pin-and-paper/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-pin-and-paper/assets/deck-stage.js (100%) rename {skills => design-templates}/html-ppt-zhangzara-pin-and-paper/assets/styles.css (100%) rename {skills => design-templates}/html-ppt-zhangzara-pin-and-paper/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-pin-and-paper/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-pink-script/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-pink-script/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-pink-script/assets/deck-stage.js (100%) rename {skills => design-templates}/html-ppt-zhangzara-pink-script/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-pink-script/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-playful/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-playful/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-playful/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-playful/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-raw-grid/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-raw-grid/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-raw-grid/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-raw-grid/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-retro-windows/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-retro-windows/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-retro-windows/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-retro-windows/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-retro-zine/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-retro-zine/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-retro-zine/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-retro-zine/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-sakura-chroma/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-sakura-chroma/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-sakura-chroma/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-sakura-chroma/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-scatterbrain/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-scatterbrain/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-scatterbrain/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-scatterbrain/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-signal/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-signal/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-signal/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-signal/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-soft-editorial/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-soft-editorial/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-soft-editorial/assets/deck-stage.js (100%) rename {skills => design-templates}/html-ppt-zhangzara-soft-editorial/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-soft-editorial/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-stencil-tablet/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-stencil-tablet/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-stencil-tablet/assets/deck-stage.js (100%) rename {skills => design-templates}/html-ppt-zhangzara-stencil-tablet/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-stencil-tablet/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-studio/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-studio/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-studio/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-studio/template.json (100%) rename {skills => design-templates}/html-ppt-zhangzara-vellum/LICENSE (100%) rename {skills => design-templates}/html-ppt-zhangzara-vellum/SKILL.md (100%) rename {skills => design-templates}/html-ppt-zhangzara-vellum/example.html (100%) rename {skills => design-templates}/html-ppt-zhangzara-vellum/template.json (100%) rename {skills => design-templates}/html-ppt/.clawscan-allow (100%) rename {skills => design-templates}/html-ppt/LICENSE (100%) rename {skills => design-templates}/html-ppt/README.md (100%) rename {skills => design-templates}/html-ppt/README.pt-BR.md (100%) rename {skills => design-templates}/html-ppt/README.zh-CN.md (100%) rename {skills => design-templates}/html-ppt/SKILL.md (100%) rename {skills => design-templates}/html-ppt/assets/animations/animations.css (100%) rename {skills => design-templates}/html-ppt/assets/animations/fx-runtime.js (100%) rename {skills => design-templates}/html-ppt/assets/animations/fx/_util.js (100%) rename {skills => design-templates}/html-ppt/assets/animations/fx/chain-react.js (100%) rename {skills => design-templates}/html-ppt/assets/animations/fx/confetti-cannon.js (100%) rename {skills => design-templates}/html-ppt/assets/animations/fx/constellation.js (100%) rename {skills => design-templates}/html-ppt/assets/animations/fx/counter-explosion.js (100%) rename {skills => design-templates}/html-ppt/assets/animations/fx/data-stream.js (100%) rename {skills => design-templates}/html-ppt/assets/animations/fx/firework.js (100%) rename {skills => design-templates}/html-ppt/assets/animations/fx/galaxy-swirl.js (100%) rename {skills => design-templates}/html-ppt/assets/animations/fx/gradient-blob.js (100%) rename {skills => design-templates}/html-ppt/assets/animations/fx/knowledge-graph.js (100%) rename {skills => design-templates}/html-ppt/assets/animations/fx/letter-explode.js (100%) rename {skills => design-templates}/html-ppt/assets/animations/fx/magnetic-field.js (100%) rename {skills => design-templates}/html-ppt/assets/animations/fx/matrix-rain.js (100%) rename {skills => design-templates}/html-ppt/assets/animations/fx/neural-net.js (100%) rename {skills => design-templates}/html-ppt/assets/animations/fx/orbit-ring.js (100%) rename {skills => design-templates}/html-ppt/assets/animations/fx/particle-burst.js (100%) rename {skills => design-templates}/html-ppt/assets/animations/fx/shockwave.js (100%) rename {skills => design-templates}/html-ppt/assets/animations/fx/sparkle-trail.js (100%) rename {skills => design-templates}/html-ppt/assets/animations/fx/starfield.js (100%) rename {skills => design-templates}/html-ppt/assets/animations/fx/typewriter-multi.js (100%) rename {skills => design-templates}/html-ppt/assets/animations/fx/word-cascade.js (100%) rename {skills => design-templates}/html-ppt/assets/base.css (100%) rename {skills => design-templates}/html-ppt/assets/fonts.css (100%) rename {skills => design-templates}/html-ppt/assets/runtime.js (100%) rename {skills => design-templates}/html-ppt/assets/themes/academic-paper.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/arctic-cool.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/aurora.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/bauhaus.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/blueprint.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/catppuccin-latte.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/catppuccin-mocha.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/corporate-clean.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/cyberpunk-neon.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/dracula.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/editorial-serif.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/engineering-whiteprint.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/glassmorphism.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/gruvbox-dark.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/japanese-minimal.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/magazine-bold.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/memphis-pop.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/midcentury.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/minimal-white.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/neo-brutalism.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/news-broadcast.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/nord.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/pitch-deck-vc.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/rainbow-gradient.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/retro-tv.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/rose-pine.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/sharp-mono.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/soft-pastel.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/solarized-light.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/sunset-warm.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/swiss-grid.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/terminal-green.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/tokyo-night.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/vaporwave.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/xiaohongshu-white.css (100%) rename {skills => design-templates}/html-ppt/assets/themes/y2k-chrome.css (100%) rename {skills => design-templates}/html-ppt/docs/readme/_theme-cell.html (100%) rename {skills => design-templates}/html-ppt/docs/readme/animations.png (100%) rename {skills => design-templates}/html-ppt/docs/readme/hero.gif (100%) rename {skills => design-templates}/html-ppt/docs/readme/layouts-live.gif (100%) rename {skills => design-templates}/html-ppt/docs/readme/layouts.png (100%) rename {skills => design-templates}/html-ppt/docs/readme/montage-animations.html (100%) rename {skills => design-templates}/html-ppt/docs/readme/montage-layouts.html (100%) rename {skills => design-templates}/html-ppt/docs/readme/montage-templates.html (100%) rename {skills => design-templates}/html-ppt/docs/readme/montage-themes.html (100%) rename {skills => design-templates}/html-ppt/docs/readme/presenter-mode.png (100%) rename {skills => design-templates}/html-ppt/docs/readme/templates.png (100%) rename {skills => design-templates}/html-ppt/docs/readme/themes.png (100%) rename {skills => design-templates}/html-ppt/examples/demo-deck/index.html (100%) rename {skills => design-templates}/html-ppt/references/animations.md (100%) rename {skills => design-templates}/html-ppt/references/authoring-guide.md (100%) rename {skills => design-templates}/html-ppt/references/full-decks.md (100%) rename {skills => design-templates}/html-ppt/references/layouts.md (100%) rename {skills => design-templates}/html-ppt/references/presenter-mode.md (100%) rename {skills => design-templates}/html-ppt/references/themes.md (100%) rename {skills => design-templates}/html-ppt/scripts/new-deck.sh (100%) rename {skills => design-templates}/html-ppt/scripts/render.sh (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/animation-showcase/animation-showcase_01.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/animation-showcase/animation-showcase_02.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/animation-showcase/animation-showcase_03.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/animation-showcase/animation-showcase_04.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/animation-showcase/animation-showcase_05.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/animation-showcase/animation-showcase_06.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/animation-showcase/animation-showcase_07.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/animation-showcase/animation-showcase_08.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/animation-showcase/animation-showcase_09.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/animation-showcase/animation-showcase_10.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/animation-showcase/animation-showcase_11.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/animation-showcase/animation-showcase_12.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/animation-showcase/animation-showcase_13.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/animation-showcase/animation-showcase_14.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/animation-showcase/animation-showcase_15.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/animation-showcase/animation-showcase_16.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/animation-showcase/animation-showcase_17.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/animation-showcase/animation-showcase_18.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/animation-showcase/animation-showcase_19.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/animation-showcase/animation-showcase_20.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_01.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_02.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_03.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_04.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_05.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_06.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_07.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_08.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_09.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_10.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_11.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_12.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_13.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_14.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_15.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_16.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_17.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_18.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_19.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_20.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_21.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_22.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_23.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_24.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_25.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_26.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_27.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_28.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_29.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_30.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_31.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_32.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_33.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_34.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_35.png (100%) rename {skills => design-templates}/html-ppt/scripts/verify-output/theme-showcase/theme-showcase_36.png (100%) rename {skills => design-templates}/html-ppt/templates/animation-showcase.html (100%) rename {skills => design-templates}/html-ppt/templates/deck.html (100%) rename {skills => design-templates}/html-ppt/templates/full-decks-index.html (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/course-module/README.md (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/course-module/index.html (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/course-module/style.css (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/dir-key-nav-minimal/README.md (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/dir-key-nav-minimal/index.html (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/dir-key-nav-minimal/style.css (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/graphify-dark-graph/README.md (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/graphify-dark-graph/index.html (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/graphify-dark-graph/style.css (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/hermes-cyber-terminal/README.md (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/hermes-cyber-terminal/index.html (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/hermes-cyber-terminal/style.css (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/knowledge-arch-blueprint/README.md (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/knowledge-arch-blueprint/index.html (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/knowledge-arch-blueprint/style.css (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/obsidian-claude-gradient/README.md (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/obsidian-claude-gradient/index.html (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/obsidian-claude-gradient/style.css (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/pitch-deck/README.md (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/pitch-deck/index.html (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/pitch-deck/style.css (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/presenter-mode-reveal/README.md (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/presenter-mode-reveal/index.html (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/presenter-mode-reveal/style.css (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/product-launch/README.md (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/product-launch/index.html (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/product-launch/style.css (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/tech-sharing/README.md (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/tech-sharing/index.html (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/tech-sharing/style.css (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/testing-safety-alert/README.md (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/testing-safety-alert/index.html (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/testing-safety-alert/style.css (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/weekly-report/README.md (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/weekly-report/index.html (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/weekly-report/style.css (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/xhs-pastel-card/README.md (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/xhs-pastel-card/index.html (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/xhs-pastel-card/style.css (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/xhs-post/README.md (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/xhs-post/index.html (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/xhs-post/style.css (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/xhs-white-editorial/README.md (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/xhs-white-editorial/index.html (100%) rename {skills => design-templates}/html-ppt/templates/full-decks/xhs-white-editorial/style.css (100%) rename {skills => design-templates}/html-ppt/templates/layout-showcase.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/arch-diagram.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/big-quote.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/bullets.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/chart-bar.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/chart-line.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/chart-pie.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/chart-radar.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/code.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/comparison.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/cover.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/cta.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/diff.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/flow-diagram.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/gantt.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/image-grid.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/image-hero.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/kpi-grid.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/mindmap.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/process-steps.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/pros-cons.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/roadmap.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/section-divider.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/stat-highlight.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/table.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/terminal.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/thanks.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/three-column.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/timeline.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/toc.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/todo-checklist.html (100%) rename {skills => design-templates}/html-ppt/templates/single-page/two-column.html (100%) rename {skills => design-templates}/html-ppt/templates/theme-showcase.html (100%) rename {skills => design-templates}/hyperframes/SKILL.md (100%) rename {skills => design-templates}/hyperframes/data-in-motion.md (100%) rename {skills => design-templates}/hyperframes/house-style.md (100%) rename {skills => design-templates}/hyperframes/palettes/bold-energetic.md (100%) rename {skills => design-templates}/hyperframes/palettes/clean-corporate.md (100%) rename {skills => design-templates}/hyperframes/palettes/dark-premium.md (100%) rename {skills => design-templates}/hyperframes/palettes/jewel-rich.md (100%) rename {skills => design-templates}/hyperframes/palettes/monochrome.md (100%) rename {skills => design-templates}/hyperframes/palettes/nature-earth.md (100%) rename {skills => design-templates}/hyperframes/palettes/neon-electric.md (100%) rename {skills => design-templates}/hyperframes/palettes/pastel-soft.md (100%) rename {skills => design-templates}/hyperframes/palettes/warm-editorial.md (100%) rename {skills => design-templates}/hyperframes/patterns.md (100%) rename {skills => design-templates}/hyperframes/references/audio-reactive.md (100%) rename {skills => design-templates}/hyperframes/references/captions.md (100%) rename {skills => design-templates}/hyperframes/references/css-patterns.md (100%) rename {skills => design-templates}/hyperframes/references/dynamic-techniques.md (100%) rename {skills => design-templates}/hyperframes/references/html-in-canvas.md (100%) rename {skills => design-templates}/hyperframes/references/motion-principles.md (100%) rename {skills => design-templates}/hyperframes/references/transcript-guide.md (100%) rename {skills => design-templates}/hyperframes/references/transitions.md (100%) rename {skills => design-templates}/hyperframes/references/transitions/catalog.md (100%) rename {skills => design-templates}/hyperframes/references/transitions/css-3d.md (100%) rename {skills => design-templates}/hyperframes/references/transitions/css-blur.md (100%) rename {skills => design-templates}/hyperframes/references/transitions/css-cover.md (100%) rename {skills => design-templates}/hyperframes/references/transitions/css-destruction.md (100%) rename {skills => design-templates}/hyperframes/references/transitions/css-dissolve.md (100%) rename {skills => design-templates}/hyperframes/references/transitions/css-distortion.md (100%) rename {skills => design-templates}/hyperframes/references/transitions/css-grid.md (100%) rename {skills => design-templates}/hyperframes/references/transitions/css-light.md (100%) rename {skills => design-templates}/hyperframes/references/transitions/css-mechanical.md (100%) rename {skills => design-templates}/hyperframes/references/transitions/css-other.md (100%) rename {skills => design-templates}/hyperframes/references/transitions/css-push.md (100%) rename {skills => design-templates}/hyperframes/references/transitions/css-radial.md (100%) rename {skills => design-templates}/hyperframes/references/transitions/css-scale.md (100%) rename {skills => design-templates}/hyperframes/references/tts.md (100%) rename {skills => design-templates}/hyperframes/references/typography.md (100%) rename {skills => design-templates}/hyperframes/scripts/animation-map.mjs (100%) rename {skills => design-templates}/hyperframes/scripts/contrast-report.mjs (100%) rename {skills => design-templates}/hyperframes/scripts/package-loader.mjs (100%) rename {skills => design-templates}/hyperframes/visual-styles.md (100%) rename {skills => design-templates}/ib-pitch-book/SKILL.md (100%) rename {skills => design-templates}/ib-pitch-book/assets/template.html (100%) rename {skills => design-templates}/ib-pitch-book/example.html (100%) rename {skills => design-templates}/ib-pitch-book/references/attribution.md (100%) rename {skills => design-templates}/ib-pitch-book/references/checklist.md (100%) rename {skills => design-templates}/ib-pitch-book/references/compliance.md (100%) rename {skills => design-templates}/ib-pitch-book/references/conventions.md (100%) rename {skills => design-templates}/image-poster/SKILL.md (100%) rename {skills => design-templates}/image-poster/example.html (100%) rename {skills => design-templates}/invoice/SKILL.md (100%) rename {skills => design-templates}/invoice/example.html (100%) rename {skills => design-templates}/kami-deck/README.md (100%) rename {skills => design-templates}/kami-deck/SKILL.md (100%) rename {skills => design-templates}/kami-deck/example.html (100%) rename {skills => design-templates}/kami-landing/README.md (100%) rename {skills => design-templates}/kami-landing/SKILL.md (100%) rename {skills => design-templates}/kami-landing/example.html (100%) rename {skills => design-templates}/kanban-board/SKILL.md (100%) rename {skills => design-templates}/kanban-board/example.html (100%) rename {skills => design-templates}/last30days/LICENSE (100%) rename {skills => design-templates}/last30days/SKILL.md (100%) rename {skills => design-templates}/last30days/references/save-html-brief.md (100%) rename {skills => design-templates}/last30days/scripts/briefing.py (100%) rename {skills => design-templates}/last30days/scripts/last30days.py (100%) rename {skills => design-templates}/last30days/scripts/lib/__init__.py (100%) rename {skills => design-templates}/last30days/scripts/lib/bird_x.py (100%) rename {skills => design-templates}/last30days/scripts/lib/bluesky.py (100%) rename {skills => design-templates}/last30days/scripts/lib/categories.py (100%) rename {skills => design-templates}/last30days/scripts/lib/chrome_cookies.py (100%) rename {skills => design-templates}/last30days/scripts/lib/cluster.py (100%) rename {skills => design-templates}/last30days/scripts/lib/competitors.py (100%) rename {skills => design-templates}/last30days/scripts/lib/cookie_extract.py (100%) rename {skills => design-templates}/last30days/scripts/lib/dates.py (100%) rename {skills => design-templates}/last30days/scripts/lib/dedupe.py (100%) rename {skills => design-templates}/last30days/scripts/lib/entity_extract.py (100%) rename {skills => design-templates}/last30days/scripts/lib/env.py (100%) rename {skills => design-templates}/last30days/scripts/lib/fanout.py (100%) rename {skills => design-templates}/last30days/scripts/lib/fusion.py (100%) rename {skills => design-templates}/last30days/scripts/lib/github.py (100%) rename {skills => design-templates}/last30days/scripts/lib/grounding.py (100%) rename {skills => design-templates}/last30days/scripts/lib/hackernews.py (100%) rename {skills => design-templates}/last30days/scripts/lib/html_render.py (100%) rename {skills => design-templates}/last30days/scripts/lib/http.py (100%) rename {skills => design-templates}/last30days/scripts/lib/instagram.py (100%) rename {skills => design-templates}/last30days/scripts/lib/log.py (100%) rename {skills => design-templates}/last30days/scripts/lib/normalize.py (100%) rename {skills => design-templates}/last30days/scripts/lib/perplexity.py (100%) rename {skills => design-templates}/last30days/scripts/lib/pinterest.py (100%) rename {skills => design-templates}/last30days/scripts/lib/pipeline.py (100%) rename {skills => design-templates}/last30days/scripts/lib/planner.py (100%) rename {skills => design-templates}/last30days/scripts/lib/polymarket.py (100%) rename {skills => design-templates}/last30days/scripts/lib/preflight.py (100%) rename {skills => design-templates}/last30days/scripts/lib/providers.py (100%) rename {skills => design-templates}/last30days/scripts/lib/quality_nudge.py (100%) rename {skills => design-templates}/last30days/scripts/lib/query.py (100%) rename {skills => design-templates}/last30days/scripts/lib/reddit.py (100%) rename {skills => design-templates}/last30days/scripts/lib/reddit_enrich.py (100%) rename {skills => design-templates}/last30days/scripts/lib/reddit_public.py (100%) rename {skills => design-templates}/last30days/scripts/lib/relevance.py (100%) rename {skills => design-templates}/last30days/scripts/lib/render.py (100%) rename {skills => design-templates}/last30days/scripts/lib/rerank.py (100%) rename {skills => design-templates}/last30days/scripts/lib/resolve.py (100%) rename {skills => design-templates}/last30days/scripts/lib/safari_cookies.py (100%) rename {skills => design-templates}/last30days/scripts/lib/schema.py (100%) rename {skills => design-templates}/last30days/scripts/lib/setup_wizard.py (100%) rename {skills => design-templates}/last30days/scripts/lib/signals.py (100%) rename {skills => design-templates}/last30days/scripts/lib/snippet.py (100%) rename {skills => design-templates}/last30days/scripts/lib/subproc.py (100%) rename {skills => design-templates}/last30days/scripts/lib/threads.py (100%) rename {skills => design-templates}/last30days/scripts/lib/tiktok.py (100%) rename {skills => design-templates}/last30days/scripts/lib/truthsocial.py (100%) rename {skills => design-templates}/last30days/scripts/lib/ui.py (100%) rename {skills => design-templates}/last30days/scripts/lib/vendor/bird-search/LICENSE (100%) rename {skills => design-templates}/last30days/scripts/lib/vendor/bird-search/bird-search.mjs (100%) rename {skills => design-templates}/last30days/scripts/lib/vendor/bird-search/lib/cookies.js (100%) rename {skills => design-templates}/last30days/scripts/lib/vendor/bird-search/lib/features.json (100%) rename {skills => design-templates}/last30days/scripts/lib/vendor/bird-search/lib/paginate-cursor.js (100%) rename {skills => design-templates}/last30days/scripts/lib/vendor/bird-search/lib/query-ids.json (100%) rename {skills => design-templates}/last30days/scripts/lib/vendor/bird-search/lib/runtime-features.js (100%) rename {skills => design-templates}/last30days/scripts/lib/vendor/bird-search/lib/runtime-query-ids.js (100%) rename {skills => design-templates}/last30days/scripts/lib/vendor/bird-search/lib/twitter-client-base.js (100%) rename {skills => design-templates}/last30days/scripts/lib/vendor/bird-search/lib/twitter-client-constants.js (100%) rename {skills => design-templates}/last30days/scripts/lib/vendor/bird-search/lib/twitter-client-features.js (100%) rename {skills => design-templates}/last30days/scripts/lib/vendor/bird-search/lib/twitter-client-search.js (100%) rename {skills => design-templates}/last30days/scripts/lib/vendor/bird-search/lib/twitter-client-types.js (100%) rename {skills => design-templates}/last30days/scripts/lib/vendor/bird-search/lib/twitter-client-utils.js (100%) rename {skills => design-templates}/last30days/scripts/lib/vendor/bird-search/package.json (100%) rename {skills => design-templates}/last30days/scripts/lib/xai_x.py (100%) rename {skills => design-templates}/last30days/scripts/lib/xiaohongshu_api.py (100%) rename {skills => design-templates}/last30days/scripts/lib/xquik.py (100%) rename {skills => design-templates}/last30days/scripts/lib/xurl_x.py (100%) rename {skills => design-templates}/last30days/scripts/lib/youtube_yt.py (100%) rename {skills => design-templates}/last30days/scripts/store.py (100%) rename {skills => design-templates}/last30days/scripts/watchlist.py (100%) rename {skills => design-templates}/live-artifact/SKILL.md (100%) rename {skills => design-templates}/live-artifact/assets/templates/clinic-console/README.md (100%) rename {skills => design-templates}/live-artifact/assets/templates/clinic-console/data.json (100%) rename {skills => design-templates}/live-artifact/assets/templates/clinic-console/template.html (100%) rename {skills => design-templates}/live-artifact/examples/baby-health-live.html (100%) rename {skills => design-templates}/live-artifact/examples/competitor-radar-live.html (100%) rename {skills => design-templates}/live-artifact/examples/crm-table-live.html (100%) rename {skills => design-templates}/live-artifact/examples/crypto-dashboard.html (100%) rename {skills => design-templates}/live-artifact/examples/monday-operator-live.html (100%) rename {skills => design-templates}/live-artifact/examples/stock-dashboard.html (100%) rename {skills => design-templates}/live-artifact/examples/stock-portfolio-live/artifact.json (100%) rename {skills => design-templates}/live-artifact/examples/stock-portfolio-live/data.json (100%) rename {skills => design-templates}/live-artifact/examples/stock-portfolio-live/provenance.json (100%) rename {skills => design-templates}/live-artifact/examples/stock-portfolio-live/template.html (100%) rename {skills => design-templates}/live-artifact/references/artifact-schema.md (100%) rename {skills => design-templates}/live-artifact/references/connector-policy.md (100%) rename {skills => design-templates}/live-artifact/references/refresh-contract.md (100%) rename {skills => design-templates}/live-dashboard/SKILL.md (100%) rename {skills => design-templates}/live-dashboard/assets/template.html (100%) rename {skills => design-templates}/live-dashboard/example.html (100%) rename {skills => design-templates}/live-dashboard/references/checklist.md (100%) rename {skills => design-templates}/live-dashboard/references/components.md (100%) rename {skills => design-templates}/live-dashboard/references/connectors.md (100%) rename {skills => design-templates}/live-dashboard/references/layouts.md (100%) rename {skills => design-templates}/magazine-poster/SKILL.md (100%) rename {skills => design-templates}/magazine-poster/example.html (100%) rename {skills => design-templates}/meeting-notes/SKILL.md (100%) rename {skills => design-templates}/meeting-notes/example.html (100%) rename {skills => design-templates}/mobile-app/SKILL.md (100%) rename {skills => design-templates}/mobile-app/assets/template.html (100%) rename {skills => design-templates}/mobile-app/example.html (100%) rename {skills => design-templates}/mobile-app/references/checklist.md (100%) rename {skills => design-templates}/mobile-app/references/layouts.md (100%) rename {skills => design-templates}/mobile-onboarding/SKILL.md (100%) rename {skills => design-templates}/mobile-onboarding/example.html (100%) rename {skills => design-templates}/motion-frames/SKILL.md (100%) rename {skills => design-templates}/motion-frames/example.html (100%) rename {skills => design-templates}/open-design-landing-deck/README.md (100%) rename {skills => design-templates}/open-design-landing-deck/SKILL.md (100%) rename {skills => design-templates}/open-design-landing-deck/example.html (100%) rename {skills => design-templates}/open-design-landing-deck/inputs.example.json (100%) rename {skills => design-templates}/open-design-landing-deck/schema.ts (100%) rename {skills => design-templates}/open-design-landing-deck/scripts/compose.ts (100%) rename {skills => design-templates}/open-design-landing/README.md (100%) rename {skills => design-templates}/open-design-landing/SKILL.md (100%) rename {skills => design-templates}/open-design-landing/assets/about.png (100%) rename {skills => design-templates}/open-design-landing/assets/capabilities.png (100%) rename {skills => design-templates}/open-design-landing/assets/cta.png (100%) rename {skills => design-templates}/open-design-landing/assets/hero.png (100%) rename {skills => design-templates}/open-design-landing/assets/image-manifest.json (100%) rename {skills => design-templates}/open-design-landing/assets/imagegen-prompts.md (100%) rename {skills => design-templates}/open-design-landing/assets/lab-1.png (100%) rename {skills => design-templates}/open-design-landing/assets/lab-2.png (100%) rename {skills => design-templates}/open-design-landing/assets/lab-3.png (100%) rename {skills => design-templates}/open-design-landing/assets/lab-4.png (100%) rename {skills => design-templates}/open-design-landing/assets/lab-5.png (100%) rename {skills => design-templates}/open-design-landing/assets/method-1.png (100%) rename {skills => design-templates}/open-design-landing/assets/method-2.png (100%) rename {skills => design-templates}/open-design-landing/assets/method-3.png (100%) rename {skills => design-templates}/open-design-landing/assets/method-4.png (100%) rename {skills => design-templates}/open-design-landing/assets/testimonial.png (100%) rename {skills => design-templates}/open-design-landing/assets/work-1.png (100%) rename {skills => design-templates}/open-design-landing/assets/work-2.png (100%) rename {skills => design-templates}/open-design-landing/example.html (100%) rename {skills => design-templates}/open-design-landing/inputs.example.json (100%) rename {skills => design-templates}/open-design-landing/schema.ts (100%) rename {skills => design-templates}/open-design-landing/scripts/compose.ts (100%) rename {skills => design-templates}/open-design-landing/scripts/imagegen.ts (100%) rename {skills => design-templates}/open-design-landing/scripts/placeholder.ts (100%) rename {skills => design-templates}/open-design-landing/styles.css (100%) rename {skills => design-templates}/orbit-general/SKILL.md (100%) rename {skills => design-templates}/orbit-general/example.html (100%) rename {skills => design-templates}/orbit-github/SKILL.md (100%) rename {skills => design-templates}/orbit-github/example.html (100%) rename {skills => design-templates}/orbit-gmail/SKILL.md (100%) rename {skills => design-templates}/orbit-gmail/example.html (100%) rename {skills => design-templates}/orbit-linear/SKILL.md (100%) rename {skills => design-templates}/orbit-linear/example.html (100%) rename {skills => design-templates}/orbit-notion/SKILL.md (100%) rename {skills => design-templates}/orbit-notion/example.html (100%) rename {skills => design-templates}/pm-spec/SKILL.md (100%) rename {skills => design-templates}/pm-spec/example.html (100%) rename {skills => design-templates}/pricing-page/SKILL.md (100%) rename {skills => design-templates}/pricing-page/example.html (100%) rename {skills => design-templates}/replit-deck/SKILL.md (100%) rename {skills => design-templates}/replit-deck/assets/template.html (100%) rename {skills => design-templates}/replit-deck/examples/README.md (100%) rename {skills => design-templates}/replit-deck/examples/example-atlas.html (100%) rename {skills => design-templates}/replit-deck/examples/example-bluehouse.html (100%) rename {skills => design-templates}/replit-deck/examples/example-helix.html (100%) rename {skills => design-templates}/replit-deck/examples/example-holm.html (100%) rename {skills => design-templates}/replit-deck/references/checklist.md (100%) rename {skills => design-templates}/replit-deck/references/components.md (100%) rename {skills => design-templates}/replit-deck/references/layouts.md (100%) rename {skills => design-templates}/replit-deck/references/themes.md (100%) rename {skills => design-templates}/saas-landing/SKILL.md (100%) rename {skills => design-templates}/saas-landing/example.html (100%) rename {skills => design-templates}/simple-deck/SKILL.md (100%) rename {skills => design-templates}/simple-deck/assets/template.html (100%) rename {skills => design-templates}/simple-deck/example.html (100%) rename {skills => design-templates}/simple-deck/references/checklist.md (100%) rename {skills => design-templates}/simple-deck/references/layouts.md (100%) rename {skills => design-templates}/social-carousel/SKILL.md (100%) rename {skills => design-templates}/social-carousel/example.html (100%) rename {skills => design-templates}/social-media-dashboard/.preview/hero.png (100%) rename {skills => design-templates}/social-media-dashboard/SKILL.md (100%) rename {skills => design-templates}/social-media-dashboard/example.html (100%) rename {skills => design-templates}/social-media-matrix-tracker-template/SKILL.md (100%) rename {skills => design-templates}/social-media-matrix-tracker-template/assets/template.html (100%) rename {skills => design-templates}/social-media-matrix-tracker-template/example.html (100%) rename {skills => design-templates}/social-media-matrix-tracker-template/references/checklist.md (100%) rename {skills => design-templates}/sprite-animation/SKILL.md (100%) rename {skills => design-templates}/sprite-animation/example.html (100%) rename {skills => design-templates}/team-okrs/SKILL.md (100%) rename {skills => design-templates}/team-okrs/example.html (100%) rename {skills => design-templates}/trading-analysis-dashboard-template/SKILL.md (100%) rename {skills => design-templates}/trading-analysis-dashboard-template/assets/template.html (100%) rename {skills => design-templates}/trading-analysis-dashboard-template/example.html (100%) rename {skills => design-templates}/trading-analysis-dashboard-template/references/checklist.md (100%) rename {skills => design-templates}/tweaks/SKILL.md (100%) rename {skills => design-templates}/tweaks/assets/wrap.html (100%) rename {skills => design-templates}/tweaks/example.html (100%) rename {skills => design-templates}/video-shortform/SKILL.md (100%) rename {skills => design-templates}/video-shortform/example.html (100%) rename {skills => design-templates}/waitlist-page/SKILL.md (100%) rename {skills => design-templates}/waitlist-page/assets/template.html (100%) rename {skills => design-templates}/waitlist-page/example.html (100%) rename {skills => design-templates}/waitlist-page/references/checklist.md (100%) rename {skills => design-templates}/web-prototype-taste-brutalist/SKILL.md (100%) rename {skills => design-templates}/web-prototype-taste-brutalist/example.html (100%) rename {skills => design-templates}/web-prototype-taste-editorial/SKILL.md (100%) rename {skills => design-templates}/web-prototype-taste-editorial/example.html (100%) rename {skills => design-templates}/web-prototype-taste-soft/SKILL.md (100%) rename {skills => design-templates}/web-prototype-taste-soft/example.html (100%) rename {skills => design-templates}/web-prototype/SKILL.md (100%) rename {skills => design-templates}/web-prototype/assets/template.html (100%) rename {skills => design-templates}/web-prototype/example.html (100%) rename {skills => design-templates}/web-prototype/references/checklist.md (100%) rename {skills => design-templates}/web-prototype/references/layouts.md (100%) rename {skills => design-templates}/weekly-update/SKILL.md (100%) rename {skills => design-templates}/weekly-update/example.html (100%) rename {skills => design-templates}/wireframe-sketch/SKILL.md (100%) rename {skills => design-templates}/wireframe-sketch/example.html (100%) rename {skills => design-templates}/x-research/SKILL.md (100%) create mode 100644 scripts/seed-curated-design-skills.ts create mode 100644 skills/AGENTS.md create mode 100644 skills/ad-creative/SKILL.md create mode 100644 skills/ai-music-album/SKILL.md create mode 100644 skills/algorithmic-art/SKILL.md create mode 100644 skills/apple-hig/SKILL.md create mode 100644 skills/artifacts-builder/SKILL.md create mode 100644 skills/brainstorming/SKILL.md create mode 100644 skills/brand-guidelines/SKILL.md create mode 100644 skills/canvas-design/SKILL.md create mode 100644 skills/color-expert/SKILL.md create mode 100644 skills/competitive-ads-extractor/SKILL.md create mode 100644 skills/copywriting/SKILL.md create mode 100644 skills/creative-director/SKILL.md create mode 100644 skills/d3-visualization/SKILL.md create mode 100644 skills/design-consultation/SKILL.md create mode 100644 skills/design-md/SKILL.md create mode 100644 skills/design-review/SKILL.md create mode 100644 skills/doc/SKILL.md create mode 100644 skills/docx/SKILL.md create mode 100644 skills/domain-name-brainstormer/SKILL.md create mode 100644 skills/enhance-prompt/SKILL.md create mode 100644 skills/fal-3d/SKILL.md create mode 100644 skills/fal-generate/SKILL.md create mode 100644 skills/fal-image-edit/SKILL.md create mode 100644 skills/fal-kling-o3/SKILL.md create mode 100644 skills/fal-lip-sync/SKILL.md create mode 100644 skills/fal-realtime/SKILL.md create mode 100644 skills/fal-restore/SKILL.md create mode 100644 skills/fal-train/SKILL.md create mode 100644 skills/fal-tryon/SKILL.md create mode 100644 skills/fal-upscale/SKILL.md create mode 100644 skills/fal-video-edit/SKILL.md create mode 100644 skills/fal-vision/SKILL.md create mode 100644 skills/figma-code-connect-components/SKILL.md create mode 100644 skills/figma-create-design-system-rules/SKILL.md create mode 100644 skills/figma-create-new-file/SKILL.md create mode 100644 skills/figma-generate-design/SKILL.md create mode 100644 skills/figma-generate-library/SKILL.md create mode 100644 skills/figma-implement-design/SKILL.md create mode 100644 skills/figma-use/SKILL.md create mode 100644 skills/flutter-animating-apps/SKILL.md create mode 100644 skills/frontend-design/SKILL.md create mode 100644 skills/frontend-dev/SKILL.md create mode 100644 skills/frontend-skill/SKILL.md create mode 100644 skills/frontend-slides/SKILL.md create mode 100644 skills/full-page-screenshot/SKILL.md create mode 100644 skills/gif-sticker-maker/SKILL.md create mode 100644 skills/gsap-core/SKILL.md create mode 100644 skills/gsap-react/SKILL.md create mode 100644 skills/gsap-scrolltrigger/SKILL.md create mode 100644 skills/gsap-timeline/SKILL.md create mode 100644 skills/hand-drawn-diagrams/SKILL.md create mode 100644 skills/image-enhancer/SKILL.md create mode 100644 skills/imagegen/SKILL.md create mode 100644 skills/imagen/SKILL.md create mode 100644 skills/marketing-psychology/SKILL.md create mode 100644 skills/minimax-docx/SKILL.md create mode 100644 skills/minimax-pdf/SKILL.md create mode 100644 skills/nanobanana-ppt/SKILL.md create mode 100644 skills/paywall-upgrade-cro/SKILL.md create mode 100644 skills/pdf/SKILL.md create mode 100644 skills/pixelbin-media/SKILL.md create mode 100644 skills/plan-design-review/SKILL.md create mode 100644 skills/platform-design/SKILL.md create mode 100644 skills/pptx-generator/SKILL.md create mode 100644 skills/pptx/SKILL.md create mode 100644 skills/remotion/SKILL.md create mode 100644 skills/replicate/SKILL.md create mode 100644 skills/screenshot/SKILL.md create mode 100644 skills/screenshots-marketing/SKILL.md create mode 100644 skills/shadcn-ui/SKILL.md create mode 100644 skills/shader-dev/SKILL.md create mode 100644 skills/slack-gif-creator/SKILL.md create mode 100644 skills/slides/SKILL.md create mode 100644 skills/sora/SKILL.md create mode 100644 skills/speech/SKILL.md create mode 100644 skills/stitch-loop/SKILL.md create mode 100644 skills/swiftui-design/SKILL.md create mode 100644 skills/taste-skill/SKILL.md create mode 100644 skills/theme-factory/SKILL.md create mode 100644 skills/threejs/SKILL.md create mode 100644 skills/ui-skills/SKILL.md create mode 100644 skills/ui-ux-pro-max/SKILL.md create mode 100644 skills/venice-audio-music/SKILL.md create mode 100644 skills/venice-audio-speech/SKILL.md create mode 100644 skills/venice-image-edit/SKILL.md create mode 100644 skills/venice-image-generate/SKILL.md create mode 100644 skills/venice-video/SKILL.md create mode 100644 skills/video-downloader/SKILL.md create mode 100644 skills/web-artifacts-builder/SKILL.md create mode 100644 skills/web-design-guidelines/SKILL.md create mode 100644 skills/wpds/SKILL.md create mode 100644 skills/youtube-clipper/SKILL.md create mode 100644 specs/current/skills-and-design-templates.md diff --git a/AGENTS.md b/AGENTS.md index 382a04914..64d59dfa7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,7 @@ This file is the single source of truth for agents entering this repository. Rea ## Workspace directories - Workspace packages come from `pnpm-workspace.yaml`: `apps/*`, `packages/*`, `tools/*`, and `e2e`. -- Top-level content directories: `skills/` (artifact-shape skills), `design-systems/` (brand `DESIGN.md` files), `craft/` (universal brand-agnostic craft rules a skill can opt into via `od.craft.requires`). +- Top-level content directories: `skills/` (functional skills the agent invokes mid-task — utilities, briefs, packagers; see `skills/AGENTS.md`), `design-templates/` (rendering catalogue: decks, prototypes, image/video/audio templates; see `design-templates/AGENTS.md` and `specs/current/skills-and-design-templates.md`), `design-systems/` (brand `DESIGN.md` files), `craft/` (universal brand-agnostic craft rules a skill can opt into via `od.craft.requires`). - `apps/web` is the Next.js 16 App Router + React 18 web runtime; do not restore `apps/nextjs`. - `apps/daemon` is the local privileged daemon and `od` bin. It owns `/api/*`, agent spawning, skills, design systems, artifacts, and static serving. - `apps/desktop` is the Electron shell; it discovers the web URL through sidecar IPC. diff --git a/apps/daemon/src/server-context.ts b/apps/daemon/src/server-context.ts index a6fd98b2b..a33ad519c 100644 --- a/apps/daemon/src/server-context.ts +++ b/apps/daemon/src/server-context.ts @@ -17,6 +17,10 @@ export interface PathDeps { ARTIFACTS_DIR: string; BUNDLED_PETS_DIR: string; DESIGN_SYSTEMS_DIR: string; + // Bundled rendering catalogue (see specs/current/skills-and-design-templates.md). + // Distinct from SKILLS_DIR so the EntryView Templates surface and the + // Settings → Skills surface stay decoupled. + DESIGN_TEMPLATES_DIR: string; OD_BIN: string; PROJECT_ROOT: string; PROJECTS_DIR: string; @@ -25,12 +29,23 @@ export interface PathDeps { RUNTIME_DATA_DIR_CANONICAL: string; SKILLS_DIR: string; USER_DESIGN_SYSTEMS_DIR: string; + // Mirror of USER_SKILLS_DIR rooted at DESIGN_TEMPLATES_DIR so user + // imports of templates do not collide with imports of functional skills. + USER_DESIGN_TEMPLATES_DIR: string; USER_SKILLS_DIR: string; } export interface ResourceDeps { listAllDesignSystems: () => Promise>; listAllSkills: () => Promise>; + // Mirrors listAllSkills but scans DESIGN_TEMPLATE_ROOTS so the Templates + // surface only sees rendering-catalogue entries. + listAllDesignTemplates: () => Promise>; + // Spans both functional skills and design templates so cross-surface + // resolvers (chat run system prompt, orbit template resolver, + // /api/skills/:id/example, /api/skills/:id/assets/*) keep working when + // a stored project.skillId points at either root. + listAllSkillLikeEntries: () => Promise>; mimeFor: (filePath: string) => string; } diff --git a/apps/daemon/src/server.ts b/apps/daemon/src/server.ts index fe6d853fe..3382b07fd 100644 --- a/apps/daemon/src/server.ts +++ b/apps/daemon/src/server.ts @@ -881,6 +881,15 @@ const DESIGN_SYSTEMS_DIR = resolveDaemonResourceDir( 'design-systems', path.join(PROJECT_ROOT, 'design-systems'), ); +// Renderable templates pulled out of `skills/` by the skills/design-templates +// split (PR #955) so the EntryView Templates tab gets the large rendering +// catalogue and Settings → Skills only carries functional skills the agent +// invokes mid-task. See specs/current/skills-and-design-templates.md. +const DESIGN_TEMPLATES_DIR = resolveDaemonResourceDir( + DAEMON_RESOURCE_ROOT, + 'design-templates', + path.join(PROJECT_ROOT, 'design-templates'), +); const CRAFT_DIR = resolveDaemonResourceDir( DAEMON_RESOURCE_ROOT, 'craft', @@ -975,8 +984,26 @@ const CRITIQUE_ARTIFACTS_DIR = path.join(RUNTIME_DATA_DIR, 'critique-artifacts') const PROJECTS_DIR = path.join(RUNTIME_DATA_DIR, 'projects'); const USER_SKILLS_DIR = path.join(RUNTIME_DATA_DIR, 'skills'); const USER_DESIGN_SYSTEMS_DIR = path.join(RUNTIME_DATA_DIR, 'design-systems'); +// User-imported design templates mirror USER_SKILLS_DIR but are scanned +// against DESIGN_TEMPLATES_DIR rather than SKILLS_DIR so the EntryView +// Templates surface and the Settings → Skills surface stay decoupled. +const USER_DESIGN_TEMPLATES_DIR = path.join(RUNTIME_DATA_DIR, 'design-templates'); +// Multi-root tuples used everywhere the daemon resolves a skill / template +// id without knowing which surface it came from. SKILL_ROOTS drives +// Settings → Skills; DESIGN_TEMPLATE_ROOTS drives the EntryView Templates +// gallery; ALL_SKILL_LIKE_ROOTS spans both for chat run system-prompt +// composition and the orbit template resolver, where stored project ids +// can resolve to either root after the split. +const SKILL_ROOTS = [USER_SKILLS_DIR, SKILLS_DIR]; +const DESIGN_TEMPLATE_ROOTS = [USER_DESIGN_TEMPLATES_DIR, DESIGN_TEMPLATES_DIR]; +const ALL_SKILL_LIKE_ROOTS = [ + USER_SKILLS_DIR, + USER_DESIGN_TEMPLATES_DIR, + SKILLS_DIR, + DESIGN_TEMPLATES_DIR, +]; fs.mkdirSync(PROJECTS_DIR, { recursive: true }); -for (const dir of [USER_SKILLS_DIR, USER_DESIGN_SYSTEMS_DIR]) { +for (const dir of [USER_SKILLS_DIR, USER_DESIGN_SYSTEMS_DIR, USER_DESIGN_TEMPLATES_DIR]) { fs.mkdirSync(dir, { recursive: true }); } fs.mkdirSync(CRITIQUE_ARTIFACTS_DIR, { recursive: true }); @@ -2044,24 +2071,26 @@ export async function startServer({ const app = express(); app.use(express.json({ limit: '4mb' })); - // Multi-directory scanning: merge built-in and user-installed skills/DS. - // Built-in items win on ID collisions (higher priority per skills-protocol.md). + // Multi-directory scanning shared by every skill / template surface. The + // helpers delegate to listSkills(roots) which walks roots in priority + // order, tags each entry with the SkillSource ('user' for the user + // root, 'built-in' for the bundled root) the contracts package + // declares, and lets a user-imported entry shadow a built-in one of + // the same id without erasing the built-in copy. async function listAllSkills() { - const builtIn = (await listSkills(SKILLS_DIR)).map((s) => ({ - ...s, - source: 'built-in', - })); - let installed = []; - try { - installed = (await listSkills(USER_SKILLS_DIR)).map((s) => ({ - ...s, - source: 'installed', - })); - } catch { - // User directory may not exist yet or be unreadable. - } - const seen = new Set(builtIn.map((s) => s.id)); - return [...builtIn, ...installed.filter((s) => !seen.has(s.id))]; + return listSkills(SKILL_ROOTS); + } + + async function listAllDesignTemplates() { + return listSkills(DESIGN_TEMPLATE_ROOTS); + } + + // Spans both roots so chat run system-prompt composition and the orbit + // template resolver can resolve a stored project.skillId regardless of + // which surface created the project after the skills/design-templates + // split. Keep in sync with SKILL_ROOTS + DESIGN_TEMPLATE_ROOTS above. + async function listAllSkillLikeEntries() { + return listSkills(ALL_SKILL_LIKE_ROOTS); } async function listAllDesignSystems() { @@ -2593,6 +2622,8 @@ export async function startServer({ RUNTIME_DATA_DIR_CANONICAL, DESIGN_SYSTEMS_DIR, USER_DESIGN_SYSTEMS_DIR, + DESIGN_TEMPLATES_DIR, + USER_DESIGN_TEMPLATES_DIR, SKILLS_DIR, USER_SKILLS_DIR, PROMPT_TEMPLATES_DIR, @@ -2802,7 +2833,13 @@ export async function startServer({ registerStaticResourceRoutes(app, { http: httpDeps, paths: pathDeps, - resources: { listAllSkills, listAllDesignSystems, mimeFor }, + resources: { + listAllSkills, + listAllDesignTemplates, + listAllSkillLikeEntries, + listAllDesignSystems, + mimeFor, + }, }); registerProjectArtifactRoutes(app, { http: httpDeps, @@ -2899,8 +2936,11 @@ export async function startServer({ let skillCraftRequires = []; let activeSkillDir = null; if (effectiveSkillId) { + // Span both functional skills and design templates so a project + // saved against either surface keeps its system prompt after the + // skills/design-templates split. See specs/current/skills-and-design-templates.md. const skill = findSkillById( - await listAllSkills(), + await listAllSkillLikeEntries(), effectiveSkillId, ); if (skill) { @@ -4215,7 +4255,11 @@ export async function startServer({ }); orbitService.setTemplateResolver(async (skillId) => { - const skills = await listAllSkills(); + // Orbit templates (live-artifact, etc.) live under design-templates after + // the split, but earlier projects may still point at functional-skill + // ids for the same purpose — search both roots so a stored project id + // keeps resolving through one or the other. + const skills = await listAllSkillLikeEntries(); const skill = findSkillById(skills, skillId); if (!skill || skill.scenario !== 'orbit') return null; return { @@ -4361,7 +4405,13 @@ export async function startServer({ nativeDialogs: nativeDialogDeps, research: researchDeps, mcp: { pendingAuth: mcpPendingAuth, daemonUrlRef }, - resources: { listAllSkills, listAllDesignSystems, mimeFor }, + resources: { + listAllSkills, + listAllDesignTemplates, + listAllSkillLikeEntries, + listAllDesignSystems, + mimeFor, + }, routines: { routineService }, validation: validationDeps, finalize: finalizeDeps, diff --git a/apps/daemon/src/skills.ts b/apps/daemon/src/skills.ts index 00924d733..699de5739 100644 --- a/apps/daemon/src/skills.ts +++ b/apps/daemon/src/skills.ts @@ -1,9 +1,13 @@ -// Skill registry. Scans /skills/* for SKILL.md files, parses +// Skill registry. Scans one or more on-disk roots for SKILL.md files, parses // front-matter, returns listing. No watching in this MVP — re-scans on every // GET /api/skills, which is fine for dozens of skills. +// +// Roots are passed in priority order: the first one wins on `id` collisions +// so user-imported skills under USER_SKILLS_DIR can shadow a built-in skill +// of the same name without erasing the built-in copy. import type { Dirent } from "node:fs"; -import { readdir, readFile, stat } from "node:fs/promises"; +import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises"; import path from "node:path"; import { parseFrontmatter } from "./frontmatter.js"; import { SKILLS_CWD_ALIAS } from "./cwd-aliases.js"; @@ -30,9 +34,20 @@ interface SkillFrontmatter extends JsonRecord { name?: unknown; description?: unknown; triggers?: unknown; - od?: JsonRecord & { craft?: JsonRecord; preview?: JsonRecord; design_system?: JsonRecord }; + od?: JsonRecord & { + craft?: JsonRecord; + preview?: JsonRecord; + design_system?: JsonRecord; + category?: unknown; + }; } +// Indicates whether a skill came from a user-writable root (the first root +// passed to listSkills) or from a built-in repo root (any later root). The +// UI uses this to render an origin pill and to gate destructive actions: +// only `user` skills can be deleted via /api/skills/:id. +export type SkillSource = "user" | "built-in"; + export interface SkillInfo { id: string; name: string; @@ -40,9 +55,16 @@ export interface SkillInfo { triggers: unknown[]; mode: SkillMode; surface: SkillSurface; + source: SkillSource; craftRequires: string[]; platform: SkillPlatform; scenario: string; + // Optional human-readable category (e.g. "image-generation", "video", + // "design-systems"). Surfaced as a filter pill in Settings → Skills so a + // large pre-loaded catalogue (e.g. curated design/creative skills from the + // upstream awesome-* lists) stays scannable. Not part of system-prompt + // composition; purely a UI hint. + category: string | null; previewType: string; designSystemRequired: boolean; defaultFor: string[]; @@ -91,125 +113,162 @@ export function findSkillById(skills: unknown, id: unknown): SkillInfo | undefin return (skills as SkillInfo[]).find((s) => s.id === canonical); } -export async function listSkills(skillsRoot: string): Promise { +// Accept either a single root path or an array. When given multiple roots, +// the first one wins on id collisions so user-imported skills under +// USER_SKILLS_DIR can shadow a built-in skill of the same name without +// erasing the bundled copy. Each surfaced summary carries a `source` +// (`"user"` for the first root, `"built-in"` for any later root) so the +// UI can render an origin pill and gate the delete control. +export async function listSkills( + skillsRoots: string | readonly string[], +): Promise { + const roots = Array.isArray(skillsRoots) ? skillsRoots : [skillsRoots]; const out: SkillInfo[] = []; - let entries: Dirent[] = []; - try { - entries = await readdir(skillsRoot, { withFileTypes: true }); - } catch { - return out; - } - for (const entry of entries) { - if (!entry.isDirectory() && !entry.isSymbolicLink()) continue; - const dir = path.join(skillsRoot, entry.name); - const skillPath = path.join(dir, "SKILL.md"); + const seenIds = new Set(); + for (let rootIdx = 0; rootIdx < roots.length; rootIdx += 1) { + const skillsRoot = roots[rootIdx]; + if (!skillsRoot) continue; + const source: SkillSource = rootIdx === 0 ? "user" : "built-in"; + let entries: Dirent[] = []; try { - const stats = await stat(skillPath); - if (!stats.isFile()) continue; - const raw = await readFile(skillPath, "utf8"); - const { data: parsedData, body } = parseFrontmatter(raw) as { data: unknown; body: string }; - const data = asSkillFrontmatter(parsedData); - const hasAttachments = await dirHasAttachments(dir); - const mode = normalizeMode(data.od?.mode, body, data.description); - const surface = normalizeSurface(data.od?.surface, mode); - const platform = normalizePlatform( - data.od?.platform, - mode, - body, - data.description - ); - const scenario = normalizeScenario( - data.od?.scenario, - body, - data.description - ); - const designSystemRequired = - typeof data.od?.design_system?.requires === "boolean" - ? data.od.design_system.requires - : true; - const upstream = - typeof data.od?.upstream === "string" ? data.od.upstream : null; - const previewType = - typeof data.od?.preview?.type === "string" ? data.od.preview.type : "html"; - const parentId = typeof data.name === "string" && data.name ? data.name : entry.name; - const description = typeof data.description === "string" ? data.description : ""; - const parentBody = hasAttachments ? withSkillRootPreamble(body, dir) : body; - // Pre-compute derived examples so the parent entry can advertise - // `aggregatesExamples` in the same push. The frontend uses that - // flag to hide the parent card from the gallery (its preview would - // duplicate one of the derived cards), while the daemon keeps the - // parent in the listing so `findSkillById` still resolves it for - // system-prompt composition and id alias lookups. - const derivedExamples = await collectDerivedExamples(dir); - const aggregatesExamples = derivedExamples.length > 0; - out.push({ - id: parentId, - name: parentId, - description, - triggers: Array.isArray(data.triggers) ? data.triggers : [], - mode, - surface, - craftRequires: normalizeCraftRequires(data.od?.craft?.requires), - platform, - scenario, - previewType, - designSystemRequired, - defaultFor: normalizeDefaultFor(data.od?.default_for), - upstream, - featured: normalizeFeatured(data.od?.featured), - // Optional metadata hints used by 'Use this prompt' fast-create so - // the resulting project mirrors the shipped example.html. Each hint - // is only consumed when its kind matches the skill mode; missing - // hints fall back to the same defaults the new-project form uses. - fidelity: normalizeFidelity(data.od?.fidelity), - speakerNotes: normalizeBoolHint(data.od?.speaker_notes), - animations: normalizeBoolHint(data.od?.animations), - examplePrompt: derivePrompt(data), - aggregatesExamples, - body: parentBody, - dir, - }); - - // Surface every example sitting next to a SKILL.md as its own card so - // a single skill (e.g. live-artifact) can ship a small gallery of - // hand-crafted samples without needing one SKILL.md per sample. Each - // derived card inherits the parent's mode/platform/surface/scenario - // so existing TYPE/SURFACE filters keep working; the synthetic id - // `:` lets `/api/skills/:id/example` resolve straight - // to the matching HTML on disk. We deliberately do not inherit - // `featured` so derived cards never crowd the magazine row. - for (const example of derivedExamples) { + entries = await readdir(skillsRoot, { withFileTypes: true }); + } catch { + continue; + } + for (const entry of entries) { + if (!entry.isDirectory() && !entry.isSymbolicLink()) continue; + const dir = path.join(skillsRoot, entry.name); + const skillPath = path.join(dir, "SKILL.md"); + try { + const stats = await stat(skillPath); + if (!stats.isFile()) continue; + const raw = await readFile(skillPath, "utf8"); + const { data: parsedData, body } = parseFrontmatter(raw) as { + data: unknown; + body: string; + }; + const data = asSkillFrontmatter(parsedData); + const parentId = + typeof data.name === "string" && data.name ? data.name : entry.name; + // Skip when an earlier root already surfaced this id — the first + // root wins so user shadows built-in. Done before we read the + // rest of the frontmatter to keep the shadowed-skill path cheap. + if (seenIds.has(parentId)) continue; + seenIds.add(parentId); + const hasAttachments = await dirHasAttachments(dir); + const mode = normalizeMode(data.od?.mode, body, data.description); + const surface = normalizeSurface(data.od?.surface, mode); + const platform = normalizePlatform( + data.od?.platform, + mode, + body, + data.description, + ); + const scenario = normalizeScenario( + data.od?.scenario, + body, + data.description, + ); + const category = normalizeCategory(data.od?.category); + const designSystemRequired = + typeof data.od?.design_system?.requires === "boolean" + ? data.od.design_system.requires + : true; + const upstream = + typeof data.od?.upstream === "string" ? data.od.upstream : null; + const previewType = + typeof data.od?.preview?.type === "string" + ? data.od.preview.type + : "html"; + const description = + typeof data.description === "string" ? data.description : ""; + const parentBody = hasAttachments + ? withSkillRootPreamble(body, dir) + : body; + // Pre-compute derived examples so the parent entry can advertise + // `aggregatesExamples` in the same push. The frontend uses that + // flag to hide the parent card from the gallery (its preview would + // duplicate one of the derived cards), while the daemon keeps the + // parent in the listing so `findSkillById` still resolves it for + // system-prompt composition and id alias lookups. + const derivedExamples = await collectDerivedExamples(dir); + const aggregatesExamples = derivedExamples.length > 0; out.push({ - id: `${parentId}:${example.key}`, - name: humanizeExampleName(example.key), + id: parentId, + name: parentId, description, triggers: Array.isArray(data.triggers) ? data.triggers : [], mode, surface, - craftRequires: [], + source, + craftRequires: normalizeCraftRequires(data.od?.craft?.requires), platform, scenario, + category, previewType, designSystemRequired, - defaultFor: [], + defaultFor: normalizeDefaultFor(data.od?.default_for), upstream, - featured: null, + featured: normalizeFeatured(data.od?.featured), + // Optional metadata hints used by 'Use this prompt' fast-create + // so the resulting project mirrors the shipped example.html. + // Each hint is only consumed when its kind matches the skill + // mode; missing hints fall back to the new-project defaults. fidelity: normalizeFidelity(data.od?.fidelity), speakerNotes: normalizeBoolHint(data.od?.speaker_notes), animations: normalizeBoolHint(data.od?.animations), examplePrompt: derivePrompt(data), - aggregatesExamples: false, - // Inherit the parent's full SKILL.md body so 'Use this prompt' - // on a derived card seeds the agent with the same workflow the - // parent describes. Without this, picking a derived card would - // compose an empty system prompt and the agent would have no - // skill instructions. + aggregatesExamples, body: parentBody, dir, }); + + // Surface every example sitting next to a SKILL.md as its own card + // so a single skill (e.g. live-artifact) can ship a small gallery + // of hand-crafted samples without needing one SKILL.md per sample. + // Each derived card inherits the parent's mode/platform/surface/ + // scenario so existing TYPE/SURFACE filters keep working; the + // synthetic id `:` lets `/api/skills/:id/example` + // resolve straight to the matching HTML on disk. We deliberately + // do not inherit `featured` so derived cards never crowd the + // magazine row. + for (const example of derivedExamples) { + const derivedId = `${parentId}:${example.key}`; + if (seenIds.has(derivedId)) continue; + seenIds.add(derivedId); + out.push({ + id: derivedId, + name: humanizeExampleName(example.key), + description, + triggers: Array.isArray(data.triggers) ? data.triggers : [], + mode, + surface, + source, + craftRequires: [], + platform, + scenario, + category, + previewType, + designSystemRequired, + defaultFor: [], + upstream, + featured: null, + fidelity: normalizeFidelity(data.od?.fidelity), + speakerNotes: normalizeBoolHint(data.od?.speaker_notes), + animations: normalizeBoolHint(data.od?.animations), + examplePrompt: derivePrompt(data), + aggregatesExamples: false, + // Inherit the parent's full SKILL.md body so 'Use this prompt' + // on a derived card seeds the agent with the same workflow + // the parent describes. Without this, picking a derived card + // would compose an empty system prompt. + body: parentBody, + dir, + }); + } + } catch { + // Skip unreadable entries — this is discovery, not validation. } - } catch { - // Skip unreadable entries — this is discovery, not validation. } } return out; @@ -512,6 +571,21 @@ const KNOWN_SCENARIOS = new Set([ "education", "personal", ]); +// Normalise a free-form category tag. Limits the set of accepted characters +// to lowercase letters, digits, and dashes so the value can flow straight +// into the UI as a filter pill class without escaping. Empty / non-string +// values become null so the filter row hides instead of rendering an empty +// pill. We intentionally do not lock down a fixed vocabulary here — the +// curated catalogue under skills/ owns the canonical category set, and +// user-imported skills are free to introduce their own. +function normalizeCategory(value: unknown): string | null { + if (typeof value !== "string") return null; + const slug = value.trim().toLowerCase(); + if (!slug) return null; + if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) return null; + return slug.slice(0, 64); +} + function normalizeScenario(value: unknown, body: unknown, description: unknown): string { if (typeof value === "string") { const v = value.trim().toLowerCase(); @@ -533,3 +607,356 @@ function normalizeScenario(value: unknown, body: unknown, description: unknown): // Surface the vocabulary so callers (frontend filter UI) could mirror it // later if they want to. Not exported today, kept here for documentation. void KNOWN_SCENARIOS; + +// --------------------------------------------------------------------------- +// User-skill import / delete primitives +// --------------------------------------------------------------------------- +// User-imported skills live under /user-skills//SKILL.md. +// We treat that directory as fully owned by the daemon, so import/delete are +// simple: write or rm the slug folder and let listSkills() pick the change up +// on the next /api/skills request. The slug is derived from the user-supplied +// `name` (alphanumeric + dash) and prefixed with `user-` only when an existing +// built-in skill folder shares the same id, to avoid colliding with a +// repo-shipped folder. + +export type SkillImportErrorCode = + | "BAD_REQUEST" + | "CONFLICT" + | "NOT_FOUND" + | "INTERNAL_ERROR"; + +export class SkillImportError extends Error { + readonly code: SkillImportErrorCode; + constructor(code: SkillImportErrorCode, message: string) { + super(message); + this.code = code; + this.name = "SkillImportError"; + } +} + +const RESERVED_SLUGS = new Set(["", ".", ".."]); + +export function slugifySkillName(name: unknown): string { + if (typeof name !== "string") return ""; + const lowered = name.trim().toLowerCase(); + const cleaned = lowered + .replace(/[^a-z0-9\-_]+/g, "-") + .replace(/^-+|-+$/g, "") + .replace(/-+/g, "-"); + if (!cleaned || RESERVED_SLUGS.has(cleaned)) return ""; + return cleaned.slice(0, 64); +} + +function escapeYamlString(value: unknown): string { + return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"'); +} + +interface BuildSkillMarkdownInput { + name: string; + description: string; + body: string; + triggers: string[]; +} + +function buildSkillMarkdown({ + name, + description, + body, + triggers, +}: BuildSkillMarkdownInput): string { + // Always emit `name` as a quoted scalar so YAML never coerces it to a + // number / boolean / null. Without the quotes, parseYamlSubset() would + // re-read names like '123', 'true', or 'null' as non-string literals, + // and importUserSkill()'s round-trip ("imported skill could not be + // re-read") would fail for those ids. See PR #955 review feedback. + const lines: string[] = ["---", `name: "${escapeYamlString(name)}"`]; + if (description && description.trim().length > 0) { + lines.push("description: |"); + for (const ln of description.trim().split(/\r?\n/)) { + lines.push(` ${ln}`); + } + } + if (triggers.length > 0) { + lines.push("triggers:"); + for (const t of triggers) { + const trimmed = typeof t === "string" ? t.trim() : ""; + if (!trimmed) continue; + lines.push(` - "${escapeYamlString(trimmed)}"`); + } + } + lines.push("---", "", body.trim(), ""); + return lines.join("\n"); +} + +export interface SkillImportInput { + name?: unknown; + description?: unknown; + body?: unknown; + triggers?: unknown; +} + +export interface SkillImportResult { + id: string; + slug: string; + dir: string; +} + +function isErrnoException(err: unknown): err is NodeJS.ErrnoException { + return Boolean(err) && typeof err === "object" && "code" in (err as object); +} + +export async function importUserSkill( + userSkillsRoot: string, + input: SkillImportInput, +): Promise { + const name = typeof input?.name === "string" ? input.name.trim() : ""; + const description = + typeof input?.description === "string" ? input.description : ""; + const body = typeof input?.body === "string" ? input.body : ""; + if (!name) { + throw new SkillImportError("BAD_REQUEST", "skill name required"); + } + if (!body || body.trim().length === 0) { + throw new SkillImportError("BAD_REQUEST", "skill body required"); + } + const slug = slugifySkillName(name); + if (!slug) { + throw new SkillImportError( + "BAD_REQUEST", + "skill name must produce a valid slug (a-z, 0-9, dash)", + ); + } + const triggersRaw = Array.isArray(input?.triggers) ? input.triggers : []; + const triggers = triggersRaw + .map((t) => (typeof t === "string" ? t.trim() : "")) + .filter(Boolean); + + await mkdir(userSkillsRoot, { recursive: true }); + const dir = path.join(userSkillsRoot, slug); + // Refuse to overwrite an existing folder. The caller can DELETE first + // when intentionally replacing a skill. + try { + const existing = await stat(dir); + if (existing) { + throw new SkillImportError( + "CONFLICT", + `a user skill with slug "${slug}" already exists`, + ); + } + } catch (err) { + if (err instanceof SkillImportError) throw err; + if (isErrnoException(err) && err.code !== "ENOENT") { + throw new SkillImportError( + "INTERNAL_ERROR", + `could not check skill dir: ${err.message ?? err}`, + ); + } + } + await mkdir(dir, { recursive: true }); + const md = buildSkillMarkdown({ name, description, body, triggers }); + await writeFile(path.join(dir, "SKILL.md"), md, "utf8"); + return { id: name, slug, dir }; +} + +export interface SkillUpdateInput { + name: string; + description?: unknown; + body?: unknown; + triggers?: unknown; + // Original on-disk dir for the skill being edited. When the caller is + // shadowing a built-in for the first time (i.e. `sourceDir` differs + // from the user shadow target and the shadow folder does not exist + // yet), `updateUserSkill` clones every entry except `SKILL.md` from + // `sourceDir` into the shadow so the bundled side tree (assets/, + // references/, scripts/, examples/, ...) keeps resolving through the + // /api/skills/:id/files, /example, and /assets/* routes after the + // edit. Without this, listSkills() promotes the shadow folder to the + // active dir but the resolvers see only the user-authored SKILL.md + // and the rest of the skill silently disappears (mrcfps PR #955 + // review). When omitted (or pointing at the same folder) the call + // only writes SKILL.md and leaves any previously-cloned side files + // alone so subsequent edits do not clobber the user's tweaks. + sourceDir?: string; +} + +// Overwrite (or create-on-demand) a user-owned SKILL.md. For built-in +// skills this writes a "shadow" copy under USER_SKILLS_DIR// that +// the next listSkills() pass will surface in place of the bundled copy. +// On the very first shadow-creation we also clone the built-in's side +// files (assets/, references/, scripts/, examples/, ...) so the shadow +// folder is self-contained and downstream resolvers — `/api/skills/:id/ +// files`, `/example`, `/assets/*`, the system-prompt preamble, and the +// per-turn cwd staging — keep finding the bundled tree even though the +// user's `SKILL.md` is what we serve. +export async function updateUserSkill( + userSkillsRoot: string, + input: SkillUpdateInput, +): Promise { + const name = typeof input?.name === "string" ? input.name.trim() : ""; + if (!name) { + throw new SkillImportError("BAD_REQUEST", "skill name required"); + } + const description = + typeof input?.description === "string" ? input.description : ""; + const body = typeof input?.body === "string" ? input.body : ""; + if (!body || body.trim().length === 0) { + throw new SkillImportError("BAD_REQUEST", "skill body required"); + } + const slug = slugifySkillName(name); + if (!slug) { + throw new SkillImportError( + "BAD_REQUEST", + "skill name must produce a valid slug (a-z, 0-9, dash)", + ); + } + const triggersRaw = Array.isArray(input?.triggers) ? input.triggers : []; + const triggers = triggersRaw + .map((t) => (typeof t === "string" ? t.trim() : "")) + .filter(Boolean); + await mkdir(userSkillsRoot, { recursive: true }); + const dir = path.join(userSkillsRoot, slug); + const dirExisted = await stat(dir) + .then(() => true) + .catch(() => false); + // Only clone on the very first shadow over a built-in. If `dirExisted` + // is true, we are editing an already-shadowed skill (or a pure user + // skill); re-cloning would clobber the user's tweaks under the side + // tree. If `sourceDir` is missing or already points at the shadow, + // there is nothing to clone — same dir. + const shouldCloneSideFiles = + !dirExisted && + typeof input.sourceDir === "string" && + input.sourceDir.length > 0 && + path.resolve(input.sourceDir) !== path.resolve(dir); + if (shouldCloneSideFiles) { + try { + await cloneSkillSideFiles(input.sourceDir!, dir); + } catch { + // Non-fatal: SKILL.md still lands below. Side-file resolvers will + // 404 individual entries instead of erasing the whole edit, which + // matches the pre-fix behaviour for unreachable assets. + await mkdir(dir, { recursive: true }); + } + } else { + await mkdir(dir, { recursive: true }); + } + const md = buildSkillMarkdown({ name, description, body, triggers }); + await writeFile(path.join(dir, "SKILL.md"), md, "utf8"); + return { id: name, slug, dir }; +} + +// Copy every entry in `sourceDir` into `destDir` except `SKILL.md` and +// dotfiles. Used by `updateUserSkill` to build a self-contained shadow +// folder over a built-in skill on first edit. We dereference symlinks +// for the same reason `stageActiveSkill` does — the shadow lives under +// runtime data and must not link back into a read-only resource tree. +async function cloneSkillSideFiles( + sourceDir: string, + destDir: string, +): Promise { + await mkdir(destDir, { recursive: true }); + let entries: Dirent[] = []; + try { + entries = await readdir(sourceDir, { withFileTypes: true }); + } catch { + return; + } + for (const entry of entries) { + if (entry.name === "SKILL.md") continue; + if (entry.name.startsWith(".")) continue; + const src = path.join(sourceDir, entry.name); + const dst = path.join(destDir, entry.name); + await cp(src, dst, { + recursive: true, + dereference: true, + preserveTimestamps: true, + }); + } +} + +export interface SkillFileEntry { + // Path relative to the skill's on-disk directory. Forward-slashes only. + path: string; + // 'file' | 'directory'. We do not surface symlinks or other file types. + kind: "file" | "directory"; + // Byte size for files; null for directories. + size: number | null; +} + +const SKILL_FILES_MAX_ENTRIES = 500; +const SKILL_FILES_MAX_DEPTH = 6; + +// Walk a skill directory and return a flat list of files/folders. Used by +// the Settings → Skills detail panel to render a small file tree next to +// the SKILL.md preview. Skips dotfiles, symlinks, and anything past +// `SKILL_FILES_MAX_DEPTH` so a pathological skill folder cannot stall the +// daemon. The cap on entries protects against large bundled assets folders. +export async function listSkillFiles(skillDir: string): Promise { + const out: SkillFileEntry[] = []; + const seen = new Set(); + async function walk(dir: string, depth: number): Promise { + if (depth > SKILL_FILES_MAX_DEPTH) return; + if (out.length >= SKILL_FILES_MAX_ENTRIES) return; + let entries: Dirent[] = []; + try { + entries = await readdir(dir, { withFileTypes: true }); + } catch { + return; + } + entries.sort((a, b) => a.name.localeCompare(b.name)); + for (const entry of entries) { + if (out.length >= SKILL_FILES_MAX_ENTRIES) return; + if (entry.name.startsWith(".")) continue; + // Refuse symlinks defensively — readdir's withFileTypes already + // returns isSymbolicLink(), but we double-check via the Dirent's + // kind methods to keep this aligned with the read paths elsewhere. + if (entry.isSymbolicLink()) continue; + const abs = path.join(dir, entry.name); + const rel = path.relative(skillDir, abs).split(path.sep).join("/"); + if (seen.has(rel)) continue; + seen.add(rel); + if (entry.isDirectory()) { + out.push({ path: rel, kind: "directory", size: null }); + await walk(abs, depth + 1); + } else if (entry.isFile()) { + let size: number | null = null; + try { + const s = await stat(abs); + size = s.size; + } catch { + size = null; + } + out.push({ path: rel, kind: "file", size }); + } + } + } + await walk(skillDir, 0); + return out; +} + +export async function deleteUserSkill( + userSkillsRoot: string, + id: string, +): Promise { + const slug = slugifySkillName(id); + if (!slug) { + throw new SkillImportError("BAD_REQUEST", "invalid skill id"); + } + const dir = path.join(userSkillsRoot, slug); + const root = path.resolve(userSkillsRoot); + const target = path.resolve(dir); + if (target !== dir || !target.startsWith(root + path.sep)) { + // Defence-in-depth: refuse to delete anything outside the user-skills + // root. The slugify above already strips traversal characters. + throw new SkillImportError("BAD_REQUEST", "invalid skill path"); + } + try { + await stat(target); + } catch (err) { + if (isErrnoException(err) && err.code === "ENOENT") { + throw new SkillImportError("NOT_FOUND", "user skill not found"); + } + throw err; + } + await rm(target, { recursive: true, force: true }); +} diff --git a/apps/daemon/src/static-resource-routes.ts b/apps/daemon/src/static-resource-routes.ts index 44bbcacb2..8136d22f5 100644 --- a/apps/daemon/src/static-resource-routes.ts +++ b/apps/daemon/src/static-resource-routes.ts @@ -2,7 +2,15 @@ import type { Express } from 'express'; import path from 'node:path'; import fs from 'node:fs'; import { detectAgents } from './agents.js'; -import { findSkillById, splitDerivedSkillId } from './skills.js'; +import { + SkillImportError, + deleteUserSkill, + findSkillById, + importUserSkill, + listSkillFiles, + splitDerivedSkillId, + updateUserSkill, +} from './skills.js'; import { listCodexPets, readCodexPetSpritesheet } from './codex-pets.js'; import { syncCommunityPets } from './community-pets-sync.js'; import { readDesignSystem } from './design-systems.js'; @@ -20,12 +28,20 @@ export function registerStaticResourceRoutes(app: Express, ctx: RegisterStaticRe RUNTIME_DATA_DIR, DESIGN_SYSTEMS_DIR, USER_DESIGN_SYSTEMS_DIR, + DESIGN_TEMPLATES_DIR, + USER_DESIGN_TEMPLATES_DIR, SKILLS_DIR, USER_SKILLS_DIR, PROMPT_TEMPLATES_DIR, BUNDLED_PETS_DIR, } = ctx.paths; - const { listAllSkills, listAllDesignSystems, mimeFor } = ctx.resources; + const { + listAllSkills, + listAllDesignTemplates, + listAllSkillLikeEntries, + listAllDesignSystems, + mimeFor, + } = ctx.resources; const { isLocalSameOrigin, resolvedPortRef, sendApiError } = ctx.http; const requireLocalOrigin = (req: any, res: any) => { if (isLocalSameOrigin(req, resolvedPortRef.current)) return true; @@ -71,6 +87,128 @@ export function registerStaticResourceRoutes(app: Express, ctx: RegisterStaticRe } }); + // Design templates — the rendering catalogue. Same shape as /api/skills + // (so the web client can reuse SkillSummary types) but rooted at + // DESIGN_TEMPLATE_ROOTS so the listing stays focused on template-style + // entries without bleeding functional skills into the EntryView gallery. + app.get('/api/design-templates', async (_req, res) => { + try { + const templates = await listAllDesignTemplates(); + res.json({ + designTemplates: templates.map(({ body, dir: _dir, ...rest }) => ({ + ...rest, + hasBody: typeof body === 'string' && body.length > 0, + })), + }); + } catch (err: any) { + res.status(500).json({ error: String(err) }); + } + }); + + app.get('/api/design-templates/:id', async (req, res) => { + try { + const templates = await listAllDesignTemplates(); + const template = findSkillById(templates, req.params.id); + if (!template) return res.status(404).json({ error: 'design template not found' }); + const { dir: _dir, ...serializable } = template; + res.json(serializable); + } catch (err: any) { + res.status(500).json({ error: String(err) }); + } + }); + + // POST /api/skills/import — write a new SKILL.md under USER_SKILLS_DIR + // from a UI-supplied body. The next /api/skills request surfaces it + // automatically because listSkills walks USER_SKILLS_DIR first. + app.post('/api/skills/import', async (req, res) => { + try { + const result = await importUserSkill(USER_SKILLS_DIR, req.body || {}); + const skills = await listAllSkills(); + const skill = findSkillById(skills, result.id); + if (!skill) { + return sendApiError( + res, + 500, + 'INTERNAL_ERROR', + 'imported skill was not found in catalog', + ); + } + const { dir: _dir, body: _body, ...serializable } = skill; + res.status(201).json({ + skill: { + ...serializable, + hasBody: typeof skill.body === 'string' && skill.body.length > 0, + }, + }); + } catch (err: any) { + if (err instanceof SkillImportError) { + const status = err.code === 'NOT_FOUND' ? 404 : err.code === 'BAD_REQUEST' ? 400 : 500; + return sendApiError(res, status, err.code, err.message); + } + sendApiError(res, 500, 'INTERNAL_ERROR', String(err)); + } + }); + + // PUT /api/skills/:id — update an existing user-managed skill's + // SKILL.md (and, when the user edits a built-in for the first time, + // clone its side files into USER_SKILLS_DIR// so subsequent + // /api/skills/:id/{files,example,assets/*} requests keep resolving + // the bundled assets/references/scripts/examples). See PR #955 review. + app.put('/api/skills/:id', async (req, res) => { + try { + const skills = await listAllSkills(); + const skill = findSkillById(skills, req.params.id); + if (!skill) { + return sendApiError(res, 404, 'NOT_FOUND', 'skill not found'); + } + const result = await updateUserSkill(USER_SKILLS_DIR, { + ...(req.body || {}), + id: skill.id, + sourceDir: skill.dir, + }); + const next = await listAllSkills(); + const updated = findSkillById(next, result.id); + if (!updated) { + return sendApiError( + res, + 500, + 'INTERNAL_ERROR', + 'updated skill was not found in catalog', + ); + } + const { dir: _dir, body: _body, ...serializable } = updated; + res.json({ + skill: { + ...serializable, + hasBody: typeof updated.body === 'string' && updated.body.length > 0, + }, + }); + } catch (err: any) { + if (err instanceof SkillImportError) { + const status = err.code === 'NOT_FOUND' ? 404 : err.code === 'BAD_REQUEST' ? 400 : 500; + return sendApiError(res, status, err.code, err.message); + } + sendApiError(res, 500, 'INTERNAL_ERROR', String(err)); + } + }); + + // GET /api/skills/:id/files — flat listing of the files that ship with + // a skill. Used by the Settings → Skills detail panel to render the + // file tree (capped server-side to keep payload bounded). + app.get('/api/skills/:id/files', async (req, res) => { + try { + const skills = await listAllSkills(); + const skill = findSkillById(skills, req.params.id); + if (!skill) { + return sendApiError(res, 404, 'NOT_FOUND', 'skill not found'); + } + const files = await listSkillFiles(skill.dir); + res.json({ files }); + } catch (err: any) { + sendApiError(res, 500, 'INTERNAL_ERROR', String(err)); + } + }); + // Codex hatch-pet registry — pets packaged by the upstream `hatch-pet` // skill under `${CODEX_HOME:-$HOME/.codex}/pets/`. Surfaced so the web // pet settings can offer one-click adoption of recently-hatched pets. @@ -259,7 +397,11 @@ export function registerStaticResourceRoutes(app: Express, ctx: RegisterStaticRe // a real preview on its parent card instead of returning 404. app.get('/api/skills/:id/example', async (req, res) => { try { - const skills = await listAllSkills(); + // Span both functional skills and design templates: rendered example + // HTML rewrites assets to /api/skills//... and we want those URLs + // to keep resolving regardless of which root owns the backing folder + // after the skills/design-templates split. + const skills = await listAllSkillLikeEntries(); // 1. Derived `:` id — resolve straight to the matching // file under /examples/. Done before findSkillById so the @@ -381,7 +523,9 @@ export function registerStaticResourceRoutes(app: Express, ctx: RegisterStaticRe // contributors can preview `example.html` straight from disk. app.get('/api/skills/:id/assets/*', async (req, res) => { try { - const skills = await listAllSkills(); + // Same rationale as /example above — assets need to resolve whether + // the owning skill folder lives under skills/ or design-templates/. + const skills = await listAllSkillLikeEntries(); const skill = findSkillById(skills, req.params.id); if (!skill) { return res.status(404).type('text/plain').send('skill not found'); diff --git a/apps/daemon/tests/prompts/system.test.ts b/apps/daemon/tests/prompts/system.test.ts index 66ff6030a..91ac3000b 100644 --- a/apps/daemon/tests/prompts/system.test.ts +++ b/apps/daemon/tests/prompts/system.test.ts @@ -9,8 +9,14 @@ import { composeSystemPrompt } from '../../src/prompts/system.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const repoRoot = path.resolve(__dirname, '../../../..'); -const liveArtifactRoot = path.join(repoRoot, 'skills/live-artifact'); -const liveArtifactSkillPath = path.join(repoRoot, 'skills/live-artifact/SKILL.md'); +// `live-artifact` moved from skills/ to design-templates/ in PR #955 as +// part of the skills/design-templates split (see specs/current/ +// skills-and-design-templates.md). The root path now points there. +const liveArtifactRoot = path.join(repoRoot, 'design-templates/live-artifact'); +const liveArtifactSkillPath = path.join( + repoRoot, + 'design-templates/live-artifact/SKILL.md', +); const liveArtifactSkillMarkdown = readFileSync(liveArtifactSkillPath, 'utf8'); const liveArtifactSkillBody = [ `> **Skill root (absolute):** \`${liveArtifactRoot}\``, @@ -25,8 +31,13 @@ const liveArtifactSkillBody = [ liveArtifactSkillMarkdown.replace(/^---[\s\S]*?---\n\n/, '').trim(), ].join('\n'); -const hyperframesRoot = path.join(repoRoot, 'skills/hyperframes'); -const hyperframesSkillPath = path.join(repoRoot, 'skills/hyperframes/SKILL.md'); +// `hyperframes` also moved to design-templates/ in PR #955 — same split +// as `live-artifact` above. +const hyperframesRoot = path.join(repoRoot, 'design-templates/hyperframes'); +const hyperframesSkillPath = path.join( + repoRoot, + 'design-templates/hyperframes/SKILL.md', +); const hyperframesSkillMarkdown = readFileSync(hyperframesSkillPath, 'utf8'); const hyperframesSkillBody = [ `> **Skill root (absolute):** \`${hyperframesRoot}\``, diff --git a/apps/daemon/tests/skills-delete-route.test.ts b/apps/daemon/tests/skills-delete-route.test.ts new file mode 100644 index 000000000..b78733947 --- /dev/null +++ b/apps/daemon/tests/skills-delete-route.test.ts @@ -0,0 +1,131 @@ +/** + * Coverage for `DELETE /api/skills/:id`. After review feedback on PR #955, + * the route resolves the on-disk folder by `skill.dir` rather than by + * re-slugifying the frontmatter id, so it has to handle both shapes that + * land under USER_SKILLS_DIR: + * + * 1. Import shape — `//SKILL.md` + * (the daemon picks the folder name from the frontmatter `name`). + * 2. Install shape — `//` or + * `//`, often a symlink to the + * user's source tree, where the folder name is independent of the + * frontmatter `name`. + * + * The earlier handler matched only shape 1 and silently 404'd shape 2, + * leaving installed folders behind. These tests pin both shapes plus a + * sibling-traversal guard so the regression cannot return. + */ +import type http from 'node:http'; +import { mkdirSync, rmSync, symlinkSync, writeFileSync, existsSync } from 'node:fs'; +import { mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'; + +import { startServer } from '../src/server.js'; + +describe('DELETE /api/skills/:id', () => { + let server: http.Server; + let baseUrl: string; + let userSkillsDir: string; + const tempDirs: string[] = []; + + beforeAll(async () => { + const started = (await startServer({ port: 0, returnServer: true })) as { + url: string; + server: http.Server; + }; + baseUrl = started.url; + server = started.server; + const dataDir = process.env.OD_DATA_DIR; + if (!dataDir) throw new Error('OD_DATA_DIR is required for daemon route tests'); + userSkillsDir = path.join(dataDir, 'skills'); + mkdirSync(userSkillsDir, { recursive: true }); + }); + + afterEach(() => { + for (const dir of tempDirs.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } + }); + + afterAll(() => { + return new Promise((resolve) => server.close(() => resolve())); + }); + + function seedSkill(folderName: string, frontmatterName: string): string { + const dir = path.join(userSkillsDir, folderName); + mkdirSync(dir, { recursive: true }); + writeFileSync( + path.join(dir, 'SKILL.md'), + `---\nname: ${frontmatterName}\ndescription: fixture for delete route\n---\nbody`, + ); + return dir; + } + + it('removes an import-shaped skill (folder named after the slugified frontmatter id)', async () => { + const dir = seedSkill('blog-helper', 'blog-helper'); + expect(existsSync(dir)).toBe(true); + + const resp = await fetch(`${baseUrl}/api/skills/blog-helper`, { method: 'DELETE' }); + expect(resp.status).toBe(200); + expect(existsSync(dir)).toBe(false); + }); + + it('removes an install-shaped skill where the folder name does not match the frontmatter id', async () => { + // GitHub install: `installFromTarget` writes the clone under the + // sanitized repo name, even though the SKILL.md inside advertises a + // different `name`. Re-slugifying the id (the previous handler) would + // miss this directory entirely. + const dir = seedSkill('awesome-skill-pack', 'totally-different-id'); + expect(existsSync(dir)).toBe(true); + + const resp = await fetch(`${baseUrl}/api/skills/totally-different-id`, { method: 'DELETE' }); + expect(resp.status).toBe(200); + expect(existsSync(dir)).toBe(false); + }); + + it('removes a symlinked local install without following the link to the source tree', async () => { + // Local-path install: `installFromTarget` symlinks the user's source + // directory into USER_SKILLS_DIR. Deleting must unlink the symlink, + // not recurse into and wipe the user's own files. + const sourceTree = mkdtempSync(path.join(tmpdir(), 'od-skill-source-')); + tempDirs.push(sourceTree); + writeFileSync( + path.join(sourceTree, 'SKILL.md'), + `---\nname: linked-skill\ndescription: fixture\n---\nbody`, + ); + writeFileSync(path.join(sourceTree, 'guard.txt'), 'must survive delete'); + + const linkPath = path.join(userSkillsDir, 'linked-skill'); + symlinkSync(sourceTree, linkPath); + + const resp = await fetch(`${baseUrl}/api/skills/linked-skill`, { method: 'DELETE' }); + expect(resp.status).toBe(200); + expect(existsSync(linkPath)).toBe(false); + // The symlink target must remain untouched — unlinkSync, never rm -rf. + expect(existsSync(path.join(sourceTree, 'guard.txt'))).toBe(true); + }); + + it('refuses to delete a built-in skill', async () => { + // `live-artifact` ships under the repo's design-templates root and is + // surfaced with `source: 'built-in'`; the handler must reject it + // regardless of the resolution path. + const resp = await fetch(`${baseUrl}/api/skills/live-artifact`, { method: 'DELETE' }); + // The id may not even appear under the user/built-in skill roots + // (functional-only) — in that case the route returns 404 without + // falling through to a built-in deletion. uninstallById returns 403 + // ("Cannot uninstall built-in items") when the id IS present in the + // bundled skills root. Both shapes are safe; what we forbid is a 200 + // that would have removed the bundled folder. + expect([400, 403, 404]).toContain(resp.status); + expect(resp.status).not.toBe(200); + }); + + it('returns 404 for an unknown id', async () => { + const resp = await fetch(`${baseUrl}/api/skills/does-not-exist-${Date.now()}`, { + method: 'DELETE', + }); + expect(resp.status).toBe(404); + }); +}); diff --git a/apps/daemon/tests/skills.test.ts b/apps/daemon/tests/skills.test.ts index cb1a12963..ed94bdc94 100644 --- a/apps/daemon/tests/skills.test.ts +++ b/apps/daemon/tests/skills.test.ts @@ -5,14 +5,30 @@ import path from 'node:path'; import { describe, expect, it } from 'vitest'; +import { rmSync } from 'node:fs'; + import { SKILLS_CWD_ALIAS } from '../src/cwd-aliases.js'; -import { listSkills } from '../src/skills.js'; +import { readFileSync } from 'node:fs'; +import { + deleteUserSkill, + importUserSkill, + listSkillFiles, + listSkills, + slugifySkillName, + updateUserSkill, +} from '../src/skills.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const repoRoot = path.resolve(__dirname, '../../..'); const skillsRoot = path.join(repoRoot, 'skills'); -const liveArtifactRoot = path.join(skillsRoot, 'live-artifact'); +// `live-artifact`, `dcf-valuation`, `x-research`, and `last30days` were +// reclassified as design templates under the Phase 0 split (see +// specs/current/skills-and-design-templates.md). The body/preamble +// expectations below still apply, but they now read from the design +// templates root rather than skills/. +const designTemplatesRoot = path.join(repoRoot, 'design-templates'); +const liveArtifactRoot = path.join(designTemplatesRoot, 'live-artifact'); type SkillCatalogEntry = { id: string; @@ -60,7 +76,7 @@ function writeSkill( describe('listSkills', () => { it('includes the built-in live-artifact skill catalog entry', async () => { - const skills = await listSkills(skillsRoot); + const skills = await listSkills(designTemplatesRoot); const skill = skills.find((entry: { id: string }) => entry.id === 'live-artifact'); if (!skill) throw new Error('live-artifact skill not found'); @@ -87,7 +103,7 @@ describe('listSkills', () => { }); it('includes the DCF valuation, X research, and Last30Days research skills', async () => { - const skills = await listSkills(skillsRoot); + const skills = await listSkills(designTemplatesRoot); const byId = new Map( (skills as SkillCatalogEntry[]).map((skill) => [skill.id, skill]), ); @@ -231,3 +247,349 @@ describe('listSkills preamble', () => { expect(skill.body).toContain('Body without external files.'); }); }); + +describe('listSkills multi-root + source tagging', () => { + it('tags entries from the first root as "user" and the second as "built-in"', async () => { + const userRoot = fresh(); + const builtInRoot = fresh(); + writeSkill(userRoot, 'web-search', { + description: 'User-imported web search.', + }); + writeSkill(builtInRoot, 'audio-jingle', { + description: 'Built-in jingle skill.', + }); + + const skills = await listSkills([userRoot, builtInRoot]); + expect(skills).toHaveLength(2); + const byId = new Map( + skills.map((s: { id: string; source: string }) => [s.id, s]), + ); + expect(byId.get('web-search')?.source).toBe('user'); + expect(byId.get('audio-jingle')?.source).toBe('built-in'); + + rmSync(userRoot, { recursive: true, force: true }); + rmSync(builtInRoot, { recursive: true, force: true }); + }); + + it('lets a user skill shadow a built-in skill of the same id', async () => { + const userRoot = fresh(); + const builtInRoot = fresh(); + writeSkill(userRoot, 'shared-id', { + description: 'User override.', + body: '# Override body', + }); + writeSkill(builtInRoot, 'shared-id', { + description: 'Original built-in.', + body: '# Built-in body', + }); + + const skills = await listSkills([userRoot, builtInRoot]); + expect(skills).toHaveLength(1); + const shadowed = skills[0]!; + expect(shadowed.source).toBe('user'); + expect(shadowed.body).toContain('Override body'); + + rmSync(userRoot, { recursive: true, force: true }); + rmSync(builtInRoot, { recursive: true, force: true }); + }); +}); + +describe('slugifySkillName', () => { + it('lowercases, normalises spaces, and strips reserved slugs', () => { + expect(slugifySkillName('Web Search')).toBe('web-search'); + expect(slugifySkillName(' Multi Word Skill ')).toBe('multi-word-skill'); + expect(slugifySkillName(' ')).toBe(''); + expect(slugifySkillName('..')).toBe(''); + expect(slugifySkillName('a/../b')).toBe('a-b'); + }); +}); + +describe('importUserSkill / deleteUserSkill', () => { + it('writes a SKILL.md and round-trips through listSkills', async () => { + const root = fresh(); + try { + const result = await importUserSkill(root, { + name: 'Code Review', + description: 'Review the latest diff.', + body: '# Review\n\n1. Read.\n2. Comment.', + triggers: ['code review', 'review my diff'], + }); + expect(result.id).toBe('Code Review'); + expect(result.slug).toBe('code-review'); + expect(result.dir).toBe(path.join(root, 'code-review')); + + const skills = await listSkills(root); + expect(skills).toHaveLength(1); + const imported = skills[0]!; + expect(imported.id).toBe('Code Review'); + expect(imported.triggers).toEqual(['code review', 'review my diff']); + // First (and only) root is treated as the user root. + expect(imported.source).toBe('user'); + + // Importing the same name again surfaces a CONFLICT error. + await expect( + importUserSkill(root, { + name: 'Code Review', + body: '# Different body', + }), + ).rejects.toMatchObject({ code: 'CONFLICT' }); + + await deleteUserSkill(root, 'Code Review'); + const after = await listSkills(root); + expect(after).toHaveLength(0); + + // Deleting an already-deleted skill returns NOT_FOUND. + await expect(deleteUserSkill(root, 'Code Review')).rejects.toMatchObject({ + code: 'NOT_FOUND', + }); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it('rejects empty bodies and impossibly-named skills', async () => { + const root = fresh(); + try { + await expect( + importUserSkill(root, { name: 'foo', body: ' ' }), + ).rejects.toMatchObject({ code: 'BAD_REQUEST' }); + await expect( + importUserSkill(root, { name: '..', body: '# body' }), + ).rejects.toMatchObject({ code: 'BAD_REQUEST' }); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + // Names like '123', 'true', or 'null' are valid skill ids but YAML coerces + // unquoted scalars to non-strings, which broke the importUserSkill -> + // listSkills round-trip prior to PR #955 review feedback. The frontmatter + // emitter now always quotes `name`, so listSkills should round-trip the + // exact string id we wrote. + it('round-trips numeric- and boolean-shaped names through listSkills', async () => { + const cases = ['123', 'true', 'false', 'null', '0']; + for (const name of cases) { + const root = fresh(); + try { + const result = await importUserSkill(root, { + name, + body: `# ${name} body`, + }); + expect(result.id).toBe(name); + const skills = await listSkills(root); + expect(skills).toHaveLength(1); + expect(skills[0]?.id).toBe(name); + } finally { + rmSync(root, { recursive: true, force: true }); + } + } + }); +}); + +describe('updateUserSkill', () => { + it('writes a SKILL.md and shadows a built-in entry on next listSkills', async () => { + const userRoot = fresh(); + const builtInRoot = fresh(); + try { + writeSkill(builtInRoot, 'shared-id', { + description: 'Original built-in.', + body: '# Original', + }); + + const result = await updateUserSkill(userRoot, { + name: 'shared-id', + description: 'User override.', + body: '# Override', + triggers: ['shared trigger'], + }); + expect(result.slug).toBe('shared-id'); + expect(result.dir).toBe(path.join(userRoot, 'shared-id')); + + const skills = await listSkills([userRoot, builtInRoot]); + expect(skills).toHaveLength(1); + const shadowed = skills[0]!; + expect(shadowed.source).toBe('user'); + expect(shadowed.body).toContain('Override'); + expect(shadowed.triggers).toEqual(['shared trigger']); + } finally { + rmSync(userRoot, { recursive: true, force: true }); + rmSync(builtInRoot, { recursive: true, force: true }); + } + }); + + it('rejects empty bodies and impossibly-named skills', async () => { + const root = fresh(); + try { + await expect( + updateUserSkill(root, { name: 'demo', body: ' ' }), + ).rejects.toMatchObject({ code: 'BAD_REQUEST' }); + await expect( + updateUserSkill(root, { name: '..', body: '# body' }), + ).rejects.toMatchObject({ code: 'BAD_REQUEST' }); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + // Regression for mrcfps' PR #955 blocker: editing a built-in skill + // wrote a shadow folder that contained only a new SKILL.md. The next + // listSkills() pass surfaced the shadow as the active dir, but + // /api/skills/:id/files, /example, /assets/* and the system-prompt + // preamble all resolve through skill.dir, so the bundled assets/, + // references/, scripts/, and examples/ silently disappeared after + // save. The fix clones the built-in side tree into the shadow on + // first edit; subsequent edits leave the user's tweaks alone. + it('clones built-in side files into the shadow on the first edit', async () => { + const userRoot = fresh(); + const builtInRoot = fresh(); + try { + writeSkill(builtInRoot, 'shadow-me', { + body: '# Original built-in', + withAttachments: true, + }); + mkdirSync(path.join(builtInRoot, 'shadow-me', 'references'), { + recursive: true, + }); + writeFileSync( + path.join(builtInRoot, 'shadow-me', 'references', 'notes.md'), + '# bundled notes', + ); + mkdirSync(path.join(builtInRoot, 'shadow-me', 'scripts'), { + recursive: true, + }); + writeFileSync( + path.join(builtInRoot, 'shadow-me', 'scripts', 'helper.sh'), + '#!/bin/sh\necho built-in\n', + ); + + const before = await listSkills([userRoot, builtInRoot]); + expect(before).toHaveLength(1); + expect(before[0]!.source).toBe('built-in'); + + const result = await updateUserSkill(userRoot, { + name: 'shadow-me', + body: '# User override', + sourceDir: before[0]!.dir, + }); + expect(result.dir).toBe(path.join(userRoot, 'shadow-me')); + + const after = await listSkills([userRoot, builtInRoot]); + expect(after).toHaveLength(1); + const shadowed = after[0]!; + expect(shadowed.source).toBe('user'); + expect(shadowed.body).toContain('User override'); + + const files = await listSkillFiles(shadowed.dir); + const paths = files.map((entry) => entry.path).sort(); + expect(paths).toContain('SKILL.md'); + expect(paths).toContain('assets'); + expect(paths).toContain('assets/template.html'); + expect(paths).toContain('references'); + expect(paths).toContain('references/notes.md'); + expect(paths).toContain('scripts'); + expect(paths).toContain('scripts/helper.sh'); + + const noteContent = readFileSync( + path.join(shadowed.dir, 'references', 'notes.md'), + 'utf8', + ); + expect(noteContent).toContain('bundled notes'); + } finally { + rmSync(userRoot, { recursive: true, force: true }); + rmSync(builtInRoot, { recursive: true, force: true }); + } + }); + + it('preserves user-edited side files on subsequent edits', async () => { + const userRoot = fresh(); + const builtInRoot = fresh(); + try { + writeSkill(builtInRoot, 'edit-twice', { + body: '# Original', + withAttachments: true, + }); + + const initial = await listSkills([userRoot, builtInRoot]); + await updateUserSkill(userRoot, { + name: 'edit-twice', + body: '# First override', + sourceDir: initial[0]!.dir, + }); + + const tweakedAsset = path.join( + userRoot, + 'edit-twice', + 'assets', + 'template.html', + ); + writeFileSync(tweakedAsset, 'user-tweaked'); + + const next = await listSkills([userRoot, builtInRoot]); + expect(next[0]!.source).toBe('user'); + + await updateUserSkill(userRoot, { + name: 'edit-twice', + body: '# Second override', + sourceDir: next[0]!.dir, + }); + + const tweaked = readFileSync(tweakedAsset, 'utf8'); + expect(tweaked).toContain('user-tweaked'); + const final = await listSkills([userRoot, builtInRoot]); + expect(final[0]!.body).toContain('Second override'); + } finally { + rmSync(userRoot, { recursive: true, force: true }); + rmSync(builtInRoot, { recursive: true, force: true }); + } + }); +}); + +describe('listSkillFiles', () => { + it('returns a flat sorted file/directory list with byte sizes', async () => { + const root = fresh(); + try { + writeSkill(root, 'demo-files', { withAttachments: true }); + mkdirSync(path.join(root, 'demo-files', 'references'), { recursive: true }); + writeFileSync( + path.join(root, 'demo-files', 'references', 'notes.md'), + '# notes', + ); + + const entries = await listSkillFiles(path.join(root, 'demo-files')); + const byPath = new Map(entries.map((entry) => [entry.path, entry])); + const skillMd = byPath.get('SKILL.md'); + const assetsDir = byPath.get('assets'); + const templateHtml = byPath.get('assets/template.html'); + const referencesDir = byPath.get('references'); + const notesMd = byPath.get('references/notes.md'); + if (!skillMd || !assetsDir || !templateHtml || !referencesDir || !notesMd) { + throw new Error('expected file tree to include SKILL.md + assets + references'); + } + expect(skillMd.kind).toBe('file'); + expect(skillMd.size).toBeGreaterThan(0); + expect(assetsDir.kind).toBe('directory'); + expect(assetsDir.size).toBeNull(); + expect(templateHtml.kind).toBe('file'); + expect(templateHtml.size).toBeGreaterThan(0); + expect(referencesDir.kind).toBe('directory'); + expect(notesMd.kind).toBe('file'); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it('skips dotfiles and returns an empty list for a missing directory', async () => { + const root = fresh(); + try { + writeSkill(root, 'with-dotfile'); + writeFileSync(path.join(root, 'with-dotfile', '.DS_Store'), 'x'); + const entries = await listSkillFiles(path.join(root, 'with-dotfile')); + expect(entries.find((entry) => entry.path === '.DS_Store')).toBeUndefined(); + + const missing = await listSkillFiles(path.join(root, 'no-such-skill')); + expect(missing).toEqual([]); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); +}); diff --git a/apps/daemon/tests/static-resource-routes.test.ts b/apps/daemon/tests/static-resource-routes.test.ts index b96703a46..b37d29e5f 100644 --- a/apps/daemon/tests/static-resource-routes.test.ts +++ b/apps/daemon/tests/static-resource-routes.test.ts @@ -40,6 +40,7 @@ describe('static resource mutation routes', () => { ARTIFACTS_DIR: path.join(tempRoot, 'artifacts'), BUNDLED_PETS_DIR: path.join(tempRoot, 'pets'), DESIGN_SYSTEMS_DIR: path.join(tempRoot, 'design-systems'), + DESIGN_TEMPLATES_DIR: path.join(tempRoot, 'design-templates'), OD_BIN: path.join(tempRoot, 'od'), PROJECT_ROOT: tempRoot, PROJECTS_DIR: path.join(tempRoot, 'projects'), @@ -48,6 +49,7 @@ describe('static resource mutation routes', () => { RUNTIME_DATA_DIR_CANONICAL: path.join(tempRoot, 'data'), SKILLS_DIR: path.join(tempRoot, 'skills'), USER_DESIGN_SYSTEMS_DIR: path.join(tempRoot, 'user-design-systems'), + USER_DESIGN_TEMPLATES_DIR: path.join(tempRoot, 'user-design-templates'), USER_SKILLS_DIR: path.join(tempRoot, 'user-skills'), }, resources: { @@ -59,6 +61,8 @@ describe('static resource mutation routes', () => { catalogReadCount += 1; return []; }, + listAllDesignTemplates: async () => [], + listAllSkillLikeEntries: async () => [], mimeFor: () => 'application/octet-stream', }, }); diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 0b0c93030..ab5e4490d 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -15,6 +15,7 @@ import { fetchAppVersionInfo, fetchAgents, fetchDesignSystems, + fetchDesignTemplates, fetchPromptTemplates, fetchSkills, } from './providers/registry'; @@ -125,7 +126,13 @@ export function App() { const [settingsInitialSection, setSettingsInitialSection] = useState('execution'); const [daemonLive, setDaemonLive] = useState(false); const [agents, setAgents] = useState([]); + // Functional skills (capabilities the agent invokes mid-task) — stays + // small and lives under the Settings → Skills surface. const [skills, setSkills] = useState([]); + // Design templates (rendering catalogue: decks, prototypes, image/video/ + // audio templates) — sourced from /api/design-templates and shown in the + // EntryView Templates tab. See specs/current/skills-and-design-templates.md. + const [designTemplates, setDesignTemplates] = useState([]); const [designSystems, setDesignSystems] = useState([]); const [projects, setProjects] = useState([]); const [templates, setTemplates] = useState([]); @@ -234,10 +241,27 @@ export function App() { setAgentsLoading(false); }); + // Functional skills + design templates land independently. Both + // gate `skillsLoading` together so the EntryView stops rendering + // its loader once both registries respond — neither tab would have + // a complete picture if we cleared the flag on the first reply. + let functionalReady = false; + let templatesReady = false; + const maybeClearLoading = () => { + if (functionalReady && templatesReady) setSkillsLoading(false); + }; void fetchSkills().then((list) => { if (cancelled) return; setSkills(list); - setSkillsLoading(false); + functionalReady = true; + maybeClearLoading(); + }); + + void fetchDesignTemplates().then((list) => { + if (cancelled) return; + setDesignTemplates(list); + templatesReady = true; + maybeClearLoading(); }); void fetchDesignSystems().then((list) => { @@ -790,10 +814,43 @@ export function App() { void refreshTemplates(); }, [route.kind, refreshTemplates]); + // Existing card grids (DesignsTab, ProjectView), pickers (NewProjectPanel, + // ChatComposer mention) all look skills up by id without caring whether + // the id resolves to a functional skill or a design template. Pass them + // the union so the post-split refactor stays invisible to those callers. + const allSkillSummaries = useMemo( + () => [...skills, ...designTemplates], + [skills, designTemplates], + ); const enabledSkills = useMemo( - () => skills.filter((s) => !(config.disabledSkills ?? []).includes(s.id)), + () => + allSkillSummaries.filter( + (s) => !(config.disabledSkills ?? []).includes(s.id), + ), + [allSkillSummaries, config.disabledSkills], + ); + // Functional-skills-only enabled subset — what ProjectView's chat + // composer @-picker should see. Without this, a skill the user has + // disabled in Settings still appears in an existing project's @-mention + // popover and can ride along to the daemon via skillIds, breaking the + // Library toggle for projects opened on the post-split branch. + const enabledFunctionalSkills = useMemo( + () => + skills.filter( + (s) => !(config.disabledSkills ?? []).includes(s.id), + ), [skills, config.disabledSkills], ); + // Templates-only enabled subset — what the EntryView Templates gallery + // actually renders. Filtering in App keeps the EntryView prop surface + // narrow ("here are the templates the user has not disabled"). + const enabledDesignTemplates = useMemo( + () => + designTemplates.filter( + (s) => !(config.disabledSkills ?? []).includes(s.id), + ), + [designTemplates, config.disabledSkills], + ); const enabledDS = useMemo( () => designSystems.filter( @@ -811,7 +868,8 @@ export function App() { routeFileName={route.kind === 'project' ? route.fileName : null} config={config} agents={agents} - skills={skills} + skills={enabledFunctionalSkills} + designTemplates={designTemplates} designSystems={designSystems} daemonLive={daemonLive} onModeChange={handleModeChange} @@ -832,6 +890,7 @@ export function App() { ) : ( Promise; commentAttachments?: ChatCommentAttachment[]; onRemoveCommentAttachment?: (id: string) => void; - onSend: (prompt: string, attachments: ChatAttachment[], commentAttachments: ChatCommentAttachment[], meta?: ChatSendMeta) => void; + // Available skills the user can compose into a turn via @. The + // chat layer already filters out disabled skills before passing them in + // here, so the picker can render the list as-is. Keep this optional so + // the composer still works on surfaces that don't show a skills picker + // (e.g. tests, screenshot harnesses). + skills?: SkillSummary[]; + onSend: ( + prompt: string, + attachments: ChatAttachment[], + commentAttachments: ChatCommentAttachment[], + meta?: ChatSendMeta, + ) => void; onStop: () => void; // Opens the global settings dialog (CLI / model / agent picker). The // composer's leading gear icon routes here so users can switch models @@ -78,6 +89,11 @@ export interface ChatComposerHandle { export interface ChatSendMeta { research?: ResearchOptions; + // Per-turn skill ids picked via the @-mention popover. The chat layer + // forwards these to the daemon's `skillIds` field so the system prompt + // for this run only is composed with the extra skill bodies, without + // touching the project's persistent `skillId`. + skillIds?: string[]; } /** @@ -99,6 +115,7 @@ export const ChatComposer = forwardRef( onEnsureProject, commentAttachments = [], onRemoveCommentAttachment, + skills = [], onSend, onStop, onOpenSettings, @@ -116,6 +133,10 @@ export const ChatComposer = forwardRef( const t = useT(); const [draft, setDraft] = useState(initialDraft ?? ""); const [staged, setStaged] = useState([]); + // Skills the user has @-mentioned for this turn. We dedupe on id and + // strip the chip when the user removes the corresponding `@` + // token from the draft, keeping draft and chips in sync. + const [stagedSkills, setStagedSkills] = useState([]); const [dragActive, setDragActive] = useState(false); const [mention, setMention] = useState<{ q: string; @@ -461,11 +482,48 @@ export const ChatComposer = forwardRef( function reset() { setDraft(""); setStaged([]); + setStagedSkills([]); setUploadError(null); setMention(null); setSlash(null); } + function insertSkillMention(skill: SkillSummary) { + if (!mention) return; + const ta = textareaRef.current; + if (!ta) return; + const cursor = mention.cursor; + const before = draft.slice(0, cursor); + const after = draft.slice(cursor); + // Use the same `@` prefix as file mentions so the visual + // grammar is consistent. The id is stable across renames; the + // displayed name is derived in render from stagedSkills. + const replaced = before.replace(/@([^\s@]*)$/, `@${skill.id} `); + const next = replaced + after; + setDraft(next); + setMention(null); + setStagedSkills((prev) => + prev.some((s) => s.id === skill.id) ? prev : [...prev, skill], + ); + requestAnimationFrame(() => { + ta.focus(); + const pos = replaced.length; + ta.setSelectionRange(pos, pos); + }); + } + + function removeStagedSkill(id: string) { + setStagedSkills((prev) => prev.filter((s) => s.id !== id)); + // Also strip the matching `@` token from the draft so the chip + // and the textarea stay in sync. We allow trailing whitespace to be + // collapsed too. + setDraft((d) => + d + .replace(new RegExp(`(^|\\s)@${escapeRegExp(id)}(\\s|$)`, 'g'), '$1$2') + .replace(/\s{2,}/g, ' '), + ); + } + async function ensureProject(): Promise { if (projectId) return projectId; return onEnsureProject(); @@ -545,6 +603,20 @@ export const ChatComposer = forwardRef( const value = e.target.value; const cursor = e.target.selectionStart; setDraft(value); + // Keep the staged-skill chips in sync with the draft. If the user + // hand-deletes an `@` token from the textarea, the chip must + // disappear too — otherwise submit() would still forward that id in + // skillIds and the daemon would compose a skill the prompt no + // longer references. Mirror the removeStagedSkill() boundary + // (whitespace or string edge) so partial matches don't keep a chip + // alive accidentally. We do not run the same prune for `staged` + // file attachments because users frequently attach files via the + // upload button without leaving an `@` token in the draft. + setStagedSkills((prev) => + prev.filter((s) => + new RegExp(`(^|\\s)@${escapeRegExp(s.id)}(\\s|$)`).test(value), + ), + ); // Detect a fresh @ at start or after whitespace; capture the typed // query up to the cursor. const before = value.slice(0, cursor); @@ -606,10 +678,12 @@ export const ChatComposer = forwardRef( // prompt and *is* sent to the agent — the agent runs the skill, // packages a Codex pet under `~/.codex/pets/`, and the user // adopts it from "Recently hatched" in pet settings afterwards. + const skillIds = stagedSkills.map((s) => s.id); + const skillMeta = skillIds.length > 0 ? { skillIds } : undefined; const hatched = expandHatchCommand(prompt); if (hatched) { if (streaming) return; - onSend(hatched, staged, commentAttachments); + onSend(hatched, staged, commentAttachments, skillMeta); reset(); return; } @@ -617,13 +691,14 @@ export const ChatComposer = forwardRef( if (search) { if (streaming) return; onSend(search.prompt, staged, commentAttachments, { + ...skillMeta, research: { enabled: true, query: search.query }, }); reset(); return; } if ((!prompt && commentAttachments.length === 0) || streaming) return; - onSend(prompt, staged, commentAttachments); + onSend(prompt, staged, commentAttachments, skillMeta); reset(); } @@ -640,6 +715,28 @@ export const ChatComposer = forwardRef( }) .slice(0, 12) : []; + // Skills appear in the same @-popover so the user has one entry point + // for everything they want to attach to a turn. Already-staged skills + // drop out of the suggestion list so the popover keeps moving forward. + const stagedSkillIds = useMemo( + () => new Set(stagedSkills.map((s) => s.id)), + [stagedSkills], + ); + const filteredSkills = useMemo(() => { + if (!mention) return [] as SkillSummary[]; + const q = mention.q.toLowerCase(); + return skills + .filter((s) => !stagedSkillIds.has(s.id)) + .filter((s) => { + if (!q) return true; + return ( + s.id.toLowerCase().includes(q) || + s.name.toLowerCase().includes(q) || + s.description.toLowerCase().includes(q) + ); + }) + .slice(0, 8); + }, [mention, skills, stagedSkillIds]); return (
( onDrop={handleDrop} >
+ {stagedSkills.length > 0 ? ( + + ) : null} {staged.length > 0 ? ( ( } }} /> - {mention && filteredFiles.length > 0 ? ( - + {mention && (filteredFiles.length > 0 || filteredSkills.length > 0) ? ( + ) : null} {slash && filteredSlash.length > 0 ? ( void; + t: TranslateFn; +}) { + return ( +
+ {skills.map((s) => ( +
+ + + + + @{s.id} + + +
+ ))} +
+ ); +} + function StagedCommentAttachments({ attachments, onRemove, @@ -1236,36 +1384,77 @@ function SlashPopover({ function MentionPopover({ files, - onPick, + skills, + onPickFile, + onPickSkill, }: { files: ProjectFile[]; - onPick: (path: string) => void; + skills: SkillSummary[]; + onPickFile: (path: string) => void; + onPickSkill: (skill: SkillSummary) => void; }) { const ref = useRef(null); useEffect(() => { if (ref.current) ref.current.scrollTop = 0; - }, [files]); + }, [files, skills]); return (
- {files.map((f) => { - const key = f.path ?? f.name; - return ( - - ); - })} + {skills.length > 0 ? ( + <> +
Skills
+ {skills.map((s) => ( + + ))} + + ) : null} + {files.length > 0 ? ( + <> + {skills.length > 0 ? ( +
Files
+ ) : null} + {files.map((f) => { + const key = f.path ?? f.name; + return ( + + ); + })} + + ) : null}
); } +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + function looksLikeImage(name: string): boolean { return /\.(png|jpe?g|gif|webp|svg|avif|bmp)$/i.test(name); } diff --git a/apps/web/src/components/ChatPane.tsx b/apps/web/src/components/ChatPane.tsx index 1dee095ee..857cd68c3 100644 --- a/apps/web/src/components/ChatPane.tsx +++ b/apps/web/src/components/ChatPane.tsx @@ -3,7 +3,7 @@ import { useT } from '../i18n'; import type { Dict } from '../i18n/types'; import { projectRawUrl } from '../providers/registry'; import type { TodoItem } from '../runtime/todos'; -import type { AppConfig, ChatAttachment, ChatCommentAttachment, ChatMessage, Conversation, PreviewComment, ProjectFile, ProjectMetadata } from '../types'; +import type { AppConfig, ChatAttachment, ChatCommentAttachment, ChatMessage, Conversation, PreviewComment, ProjectFile, ProjectMetadata, SkillSummary } from '../types'; import { dayKey, dayLabel, exactDateTime, messageTime, relativeTimeLong } from '../utils/chatTime'; import { commentsToAttachments, simplePositionLabel } from '../comments'; import { AssistantMessage } from './AssistantMessage'; @@ -208,8 +208,16 @@ interface Props { onAttachComment?: (comment: PreviewComment) => void; onDetachComment?: (commentId: string) => void; onDeleteComment?: (commentId: string) => void; - onSend: (prompt: string, attachments: ChatAttachment[], commentAttachments: ChatCommentAttachment[], meta?: ChatSendMeta) => void; + onSend: ( + prompt: string, + attachments: ChatAttachment[], + commentAttachments: ChatCommentAttachment[], + meta?: ChatSendMeta, + ) => void; onStop: () => void; + // Skills available for @-mention assembly. ProjectView filters out the + // user's disabled set before passing them in here. + skills?: SkillSummary[]; // Click-to-open chain: passes a basename up to ProjectView, which sets // FileWorkspace's openRequest. Tool cards, attachment chips, and // produced-file chips all call this. @@ -282,6 +290,7 @@ export function ChatPane({ onOpenPetSettings, projectMetadata, onProjectMetadataChange, + skills = [], researchAvailable, onCollapse, }: Props) { @@ -721,6 +730,7 @@ export function ChatPane({ ref={composerRef} projectId={projectId} projectFiles={projectFiles} + skills={skills} streaming={streaming || hasActiveRunMessage} initialDraft={initialDraft} onEnsureProject={onEnsureProject} diff --git a/apps/web/src/components/DesignSystemsSection.tsx b/apps/web/src/components/DesignSystemsSection.tsx new file mode 100644 index 000000000..7f47b923d --- /dev/null +++ b/apps/web/src/components/DesignSystemsSection.tsx @@ -0,0 +1,215 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import type { Dispatch, SetStateAction } from 'react'; +import { useT } from '../i18n'; +import type { AppConfig } from '../types'; +import type { DesignSystemSummary } from '@open-design/contracts'; +import { + fetchDesignSystem, + fetchDesignSystems, +} from '../providers/registry'; + +// Sibling Settings section that hosts the design-systems registry. +// Lifted out of the previous LibrarySection so each surface (functional +// skills vs. design systems) gets its own dedicated nav entry instead of +// sharing a sub-tab toggle. See specs/current/skills-and-design-templates.md. + +interface Props { + cfg: AppConfig; + setCfg: Dispatch>; +} + +export function DesignSystemsSection({ cfg, setCfg }: Props) { + const t = useT(); + const [designSystems, setDesignSystems] = useState([]); + const [search, setSearch] = useState(''); + const [categoryFilter, setCategoryFilter] = useState('All'); + const [previewId, setPreviewId] = useState(null); + const [previewBody, setPreviewBody] = useState(null); + const [previewLoading, setPreviewLoading] = useState(false); + + useEffect(() => { + fetchDesignSystems().then(setDesignSystems); + }, []); + + const disabledDS = useMemo( + () => new Set(cfg.disabledDesignSystems ?? []), + [cfg.disabledDesignSystems], + ); + + const categories = useMemo(() => { + const cats = new Set(designSystems.map((d) => d.category)); + return ['All', ...Array.from(cats).sort()]; + }, [designSystems]); + + const filtered = useMemo(() => { + const q = search.toLowerCase(); + return designSystems.filter((d) => { + if (categoryFilter !== 'All' && d.category !== categoryFilter) return false; + if ( + q && + !d.title.toLowerCase().includes(q) && + !d.summary.toLowerCase().includes(q) + ) + return false; + return true; + }); + }, [designSystems, categoryFilter, search]); + + const grouped = useMemo(() => { + const groups = new Map(); + for (const d of filtered) { + const list = groups.get(d.category) ?? []; + list.push(d); + groups.set(d.category, list); + } + return groups; + }, [filtered]); + + const openPreview = useCallback( + async (id: string) => { + if (previewId === id) { + setPreviewId(null); + setPreviewBody(null); + return; + } + setPreviewId(id); + setPreviewBody(null); + setPreviewLoading(true); + try { + const detail = await fetchDesignSystem(id); + setPreviewId((cur) => { + if (cur === id) setPreviewBody(detail?.body ?? null); + return cur; + }); + } catch { + setPreviewId((cur) => { + if (cur === id) setPreviewBody(null); + return cur; + }); + } finally { + setPreviewId((cur) => { + if (cur === id) setPreviewLoading(false); + return cur; + }); + } + }, + [previewId], + ); + + function toggleDSDisabled(id: string, enabled: boolean) { + setCfg((c) => { + const set = new Set(c.disabledDesignSystems ?? []); + if (enabled) set.delete(id); + else set.add(id); + return { ...c, disabledDesignSystems: [...set] }; + }); + } + + return ( +
+
+
+

{t('settings.designSystems')}

+

{t('settings.designSystemsHint')}

+
+
+ +
+ setSearch(e.target.value)} + /> +
+ {categories.map((cat) => { + const count = + cat === 'All' + ? designSystems.length + : designSystems.filter((d) => d.category === cat).length; + return ( + + ); + })} +
+
+ +
+ {filtered.length === 0 ? ( +

{t('settings.libraryNoResults')}

+ ) : ( + <> + {Array.from(grouped.entries()).map(([category, items]) => ( +
+

+ {category}{' '} + {items.length} +

+
+ {items.map((ds) => ( +
+
openPreview(ds.id)} + > + {ds.swatches && ds.swatches.length > 0 && ( +
+ {ds.swatches.slice(0, 4).map((c, i) => ( + + ))} +
+ )} +
{ds.title}
+
{ds.summary}
+
+ +
+ ))} +
+
+ ))} + {previewId && filtered.some((d) => d.id === previewId) && ( +
+ {previewLoading ? ( +

{t('settings.libraryLoading')}

+ ) : previewBody ? ( +
{previewBody}
+ ) : null} +
+ )} + + )} +
+
+ ); +} diff --git a/apps/web/src/components/EntryView.tsx b/apps/web/src/components/EntryView.tsx index fd869be0d..07dc7f584 100644 --- a/apps/web/src/components/EntryView.tsx +++ b/apps/web/src/components/EntryView.tsx @@ -36,10 +36,17 @@ import { PromptTemplatesTab } from './PromptTemplatesTab'; import { apiProtocolLabel } from '../utils/apiProtocol'; import { isMacPlatform } from '../utils/platform'; -type TopTab = 'designs' | 'examples' | 'design-systems' | 'image-templates' | 'video-templates'; +type TopTab = 'designs' | 'templates' | 'design-systems' | 'image-templates' | 'video-templates'; interface Props { + // Union of functional skills + design templates — used for id-based + // lookups (DesignsTab project chips, NewProjectPanel skill picker). + // The Templates gallery itself reads `designTemplates` instead so it + // doesn't accidentally show functional skills as renderable cards. skills: SkillSummary[]; + // Design templates only. Sourced from /api/design-templates. See + // specs/current/skills-and-design-templates.md. + designTemplates: SkillSummary[]; designSystems: DesignSystemSummary[]; projects: Project[]; templates: ProjectTemplate[]; @@ -216,6 +223,7 @@ function loadPetRailHidden(): boolean { export function EntryView({ skills, + designTemplates, designSystems, projects, templates, @@ -561,7 +569,7 @@ export function EntryView({
- + ) ) : null} - {topTab === 'examples' ? ( + {topTab === 'templates' ? ( skillsLoading ? ( ) : ( - + ) ) : null} {topTab === 'design-systems' ? ( diff --git a/apps/web/src/components/LibrarySection.tsx b/apps/web/src/components/LibrarySection.tsx deleted file mode 100644 index 99fcdc461..000000000 --- a/apps/web/src/components/LibrarySection.tsx +++ /dev/null @@ -1,524 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; -import type { Dispatch, SetStateAction } from 'react'; -import { useT } from '../i18n'; -import { Icon } from './Icon'; -import type { AppConfig, InstallInput } from '../types'; -import type { SkillSummary, DesignSystemSummary } from '@open-design/contracts'; -import { - fetchSkills, - fetchDesignSystems, - fetchSkill, - fetchDesignSystem, - installSkill, - uninstallSkill, - installDesignSystem, - uninstallDesignSystem, -} from '../providers/registry'; - -type Tab = 'skills' | 'design-systems'; - -interface Props { - cfg: AppConfig; - setCfg: Dispatch>; -} - -const MODES = [ - 'prototype', - 'deck', - 'template', - 'design-system', - 'image', - 'video', - 'audio', -] as const; - -export function LibrarySection({ cfg, setCfg }: Props) { - const t = useT(); - const [tab, setTab] = useState('skills'); - const [search, setSearch] = useState(''); - const [modeFilter, setModeFilter] = useState('all'); - const [categoryFilter, setCategoryFilter] = useState('All'); - const [skills, setSkills] = useState([]); - const [designSystems, setDesignSystems] = useState([]); - const [previewId, setPreviewId] = useState(null); - const [previewBody, setPreviewBody] = useState(null); - const [previewLoading, setPreviewLoading] = useState(false); - - // Install state - const [installOpen, setInstallOpen] = useState(false); - const [installTab, setInstallTab] = useState<'github' | 'local'>('github'); - const [installUrl, setInstallUrl] = useState(''); - const [installPath, setInstallPath] = useState(''); - const [installing, setInstalling] = useState(false); - const [installError, setInstallError] = useState(null); - - const reloadData = useCallback(() => { - fetchSkills().then(setSkills); - fetchDesignSystems().then(setDesignSystems); - }, []); - - useEffect(() => { - reloadData(); - }, [reloadData]); - - const categories = useMemo(() => { - const cats = new Set(designSystems.map((d) => d.category)); - return ['All', ...Array.from(cats).sort()]; - }, [designSystems]); - - const disabledSkills = useMemo( - () => new Set(cfg.disabledSkills ?? []), - [cfg.disabledSkills], - ); - const disabledDS = useMemo( - () => new Set(cfg.disabledDesignSystems ?? []), - [cfg.disabledDesignSystems], - ); - - const filteredSkills = useMemo(() => { - const q = search.toLowerCase(); - return skills.filter((s) => { - if (modeFilter !== 'all' && s.mode !== modeFilter) return false; - if (q && !s.name.toLowerCase().includes(q) && !s.description.toLowerCase().includes(q)) - return false; - return true; - }); - }, [skills, modeFilter, search]); - - const filteredDS = useMemo(() => { - const q = search.toLowerCase(); - return designSystems.filter((d) => { - if (categoryFilter !== 'All' && d.category !== categoryFilter) return false; - if (q && !d.title.toLowerCase().includes(q) && !d.summary.toLowerCase().includes(q)) - return false; - return true; - }); - }, [designSystems, categoryFilter, search]); - - const groupedSkills = useMemo(() => { - const groups = new Map(); - for (const s of filteredSkills) { - const list = groups.get(s.mode) ?? []; - list.push(s); - groups.set(s.mode, list); - } - return groups; - }, [filteredSkills]); - - const groupedDS = useMemo(() => { - const groups = new Map(); - for (const d of filteredDS) { - const list = groups.get(d.category) ?? []; - list.push(d); - groups.set(d.category, list); - } - return groups; - }, [filteredDS]); - - const openPreview = useCallback( - async (id: string) => { - if (previewId === id) { - setPreviewId(null); - setPreviewBody(null); - return; - } - setPreviewId(id); - setPreviewBody(null); - setPreviewLoading(true); - try { - const detail = - tab === 'skills' - ? await fetchSkill(id) - : await fetchDesignSystem(id); - setPreviewId((cur) => { - if (cur === id) setPreviewBody(detail?.body ?? null); - return cur; - }); - } catch { - setPreviewId((cur) => { - if (cur === id) setPreviewBody(null); - return cur; - }); - } finally { - setPreviewId((cur) => { - if (cur === id) setPreviewLoading(false); - return cur; - }); - } - }, - [previewId, tab], - ); - - function toggleSkillDisabled(id: string, disabled: boolean) { - setCfg((c) => { - const set = new Set(c.disabledSkills ?? []); - if (disabled) set.add(id); - else set.delete(id); - return { ...c, disabledSkills: [...set] }; - }); - } - - function toggleDSDisabled(id: string, disabled: boolean) { - setCfg((c) => { - const set = new Set(c.disabledDesignSystems ?? []); - if (disabled) set.add(id); - else set.delete(id); - return { ...c, disabledDesignSystems: [...set] }; - }); - } - - async function handleInstall() { - setInstallError(null); - setInstalling(true); - const input: InstallInput = - installTab === 'github' - ? { source: 'github', url: installUrl.trim() } - : { source: 'local', path: installPath.trim() }; - - const result = - tab === 'skills' - ? await installSkill(input) - : await installDesignSystem(input); - - setInstalling(false); - if ('error' in result) { - setInstallError(result.error); - return; - } - setInstallOpen(false); - setInstallUrl(''); - setInstallPath(''); - setInstallError(null); - reloadData(); - } - - async function handleUninstallSkill(id: string) { - const result = await uninstallSkill(id); - if ('error' in result) return; - reloadData(); - } - - async function handleUninstallDS(id: string) { - const result = await uninstallDesignSystem(id); - if ('error' in result) return; - reloadData(); - } - - return ( -
-
-
-

{t('settings.library')}

-

{t('settings.libraryHint')}

-
-
- -
- - -
- -
-
- setSearch(e.target.value)} - /> - -
- - {installOpen && ( -
-
- - -
-
- {installTab === 'github' ? ( - setInstallUrl(e.target.value)} - /> - ) : ( - setInstallPath(e.target.value)} - /> - )} - -
- {installError && ( -

{installError}

- )} -
- )} - - {tab === 'skills' ? ( -
- - {MODES.map((mode) => { - const count = skills.filter((s) => s.mode === mode).length; - if (count === 0) return null; - return ( - - ); - })} -
- ) : ( -
- {categories.map((cat) => { - const count = - cat === 'All' - ? designSystems.length - : designSystems.filter((d) => d.category === cat).length; - return ( - - ); - })} -
- )} -
- -
- {tab === 'skills' ? ( - filteredSkills.length === 0 ? ( -

{t('settings.libraryNoResults')}

- ) : ( - MODES.filter((m) => groupedSkills.has(m)).map((mode) => ( -
-

- {mode}{' '} - {groupedSkills.get(mode)!.length} -

- {groupedSkills.get(mode)!.map((skill) => ( -
-
-
- {skill.name} - {skill.previewType} - - {skill.source === 'installed' - ? t('settings.libraryInstalled') - : t('settings.libraryBuiltIn')} - -
-
{skill.description}
-
- - {skill.source === 'installed' && ( - - )} - - {previewId === skill.id && ( -
- {previewLoading ? ( -

{t('settings.libraryLoading')}

- ) : previewBody ? ( -
{previewBody}
- ) : null} -
- )} -
- ))} -
- )) - ) - ) : filteredDS.length === 0 ? ( -

{t('settings.libraryNoResults')}

- ) : ( - <> - {Array.from(groupedDS.entries()).map(([category, items]) => ( -
-

- {category} {items.length} -

-
- {items.map((ds) => ( -
-
openPreview(ds.id)}> - {ds.swatches && ds.swatches.length > 0 && ( -
- {ds.swatches.slice(0, 4).map((c, i) => ( - - ))} -
- )} -
- {ds.title} - - {ds.source === 'installed' - ? t('settings.libraryInstalled') - : t('settings.libraryBuiltIn')} - -
-
{ds.summary}
-
-
- {ds.source === 'installed' && ( - - )} - -
-
- ))} -
-
- ))} - {previewId && filteredDS.some((d) => d.id === previewId) && ( -
- {previewLoading ? ( -

{t('settings.libraryLoading')}

- ) : previewBody ? ( -
{previewBody}
- ) : null} -
- )} - - )} -
-
- ); -} diff --git a/apps/web/src/components/ProjectView.tsx b/apps/web/src/components/ProjectView.tsx index 80fffce53..f432d7a85 100644 --- a/apps/web/src/components/ProjectView.tsx +++ b/apps/web/src/components/ProjectView.tsx @@ -23,6 +23,7 @@ import { deletePreviewComment, fetchPreviewComments, fetchDesignSystem, + fetchDesignTemplate, fetchLiveArtifacts, fetchProjectFiles, fetchSkill, @@ -109,7 +110,17 @@ interface Props { routeFileName: string | null; config: AppConfig; agents: AgentInfo[]; + // Mentionable functional skills — already filtered by config.disabledSkills + // upstream, so this drives only the chat composer's @-picker scope. For + // resolving an existing project's `skillId` (which can also point at a + // design template after the skills/design-templates split) use + // `designTemplates` as a fallback in composedSystemPrompt() and in the + // skill-name / skill-mode lookups below. skills: SkillSummary[]; + // All known design templates (unfiltered). Required so projects created + // from the Templates surface keep composing the template body in API + // mode even when the user later disables the template in Settings. + designTemplates: SkillSummary[]; designSystems: DesignSystemSummary[]; daemonLive: boolean; onModeChange: (mode: AppConfig['mode']) => void; @@ -234,6 +245,7 @@ export function ProjectView({ config, agents, skills, + designTemplates, designSystems, daemonLive, onModeChange, @@ -654,14 +666,21 @@ export function ProjectView({ let designSystemTitle: string | undefined; if (project.skillId) { - const summary = skills.find((s) => s.id === project.skillId); + // project.skillId can resolve to either root after the + // skills/design-templates split; check both lists so a template-backed + // project keeps composing its template body when running in API mode. + const summary = + skills.find((s) => s.id === project.skillId) ?? + designTemplates.find((s) => s.id === project.skillId); skillName = summary?.name; skillMode = summary?.mode; const cached = skillCache.current.get(project.skillId); if (cached !== undefined) { skillBody = cached; } else { - const detail = await fetchSkill(project.skillId); + const detail = + (await fetchSkill(project.skillId)) ?? + (await fetchDesignTemplate(project.skillId)); if (detail) { skillBody = detail.body; skillCache.current.set(project.skillId, detail.body); @@ -730,6 +749,7 @@ export function ProjectView({ project.designSystemId, project.metadata, skills, + designTemplates, designSystems, config.mode, ]); @@ -1075,7 +1095,7 @@ export function ProjectView({ prompt: string, attachments: ChatAttachment[], commentAttachments: ChatCommentAttachment[] = commentsToAttachments(attachedComments), - meta?: { research?: ResearchOptions }, + meta?: { research?: ResearchOptions; skillIds?: string[] }, ) => { if (!activeConversationId) return; if (streaming) return; @@ -1396,6 +1416,7 @@ export function ProjectView({ assistantMessageId: assistantId, clientRequestId: randomUUID(), skillId: project.skillId ?? null, + skillIds: Array.isArray(meta?.skillIds) ? meta.skillIds : [], designSystemId: project.designSystemId ?? null, attachments: attachments.map((a) => a.path), commentAttachments, @@ -1786,14 +1807,19 @@ export function ProjectView({ ); const projectMeta = useMemo(() => { - const skill = skills.find((s) => s.id === project.skillId)?.name; + const summary = + skills.find((s) => s.id === project.skillId) ?? + designTemplates.find((s) => s.id === project.skillId); + const skill = summary?.name; const ds = designSystems.find((d) => d.id === project.designSystemId)?.title; return [skill, ds].filter(Boolean).join(' · ') || t('project.metaFreeform'); - }, [skills, designSystems, project.skillId, project.designSystemId, t]); + }, [skills, designTemplates, designSystems, project.skillId, project.designSystemId, t]); const isDeck = useMemo( - () => skills.find((s) => s.id === project.skillId)?.mode === 'deck', - [skills, project.skillId], + () => + (skills.find((s) => s.id === project.skillId) ?? + designTemplates.find((s) => s.id === project.skillId))?.mode === 'deck', + [skills, designTemplates, project.skillId], ); const chatResizeLabel = t('project.resizeChatPanel'); const workspacePanelTrack = @@ -2171,6 +2197,7 @@ export function ProjectView({ projectId={project.id} projectFiles={projectFiles} projectFileNames={projectFileNames} + skills={skills} onEnsureProject={handleEnsureProject} previewComments={previewComments} attachedComments={attachedComments} diff --git a/apps/web/src/components/SettingsDialog.tsx b/apps/web/src/components/SettingsDialog.tsx index 2e13c4681..457999b0e 100644 --- a/apps/web/src/components/SettingsDialog.tsx +++ b/apps/web/src/components/SettingsDialog.tsx @@ -57,7 +57,8 @@ import { MEDIA_PROVIDERS } from '../media/models'; import type { MediaProvider } from '../media/models'; import { PetSettings } from './pet/PetSettings'; import { McpClientSection } from './McpClientSection'; -import { LibrarySection } from './LibrarySection'; +import { SkillsSection } from './SkillsSection'; +import { DesignSystemsSection } from './DesignSystemsSection'; import { PrivacySection } from './PrivacySection'; import { RoutinesSection } from './RoutinesSection'; import { ConnectorsBrowser } from './ConnectorsBrowser'; @@ -88,8 +89,9 @@ export type SettingsSection = | 'appearance' | 'notifications' | 'pet' + | 'skills' + | 'designSystems' | 'memory' - | 'library' | 'privacy' | 'about'; @@ -1304,8 +1306,12 @@ export function SettingsDialog({ notifications: { title: t('settings.notifications'), subtitle: t('settings.notificationsHint') }, privacy: { title: t('settings.privacy'), subtitle: t('settings.privacyHint') }, pet: { title: t('pet.title'), subtitle: t('pet.subtitle') }, + skills: { title: t('settings.skills'), subtitle: t('settings.skillsHint') }, + designSystems: { + title: t('settings.designSystems'), + subtitle: t('settings.designSystemsHint'), + }, memory: { title: t('settings.memory'), subtitle: t('settings.memoryHint') }, - library: { title: t('settings.library'), subtitle: t('settings.libraryHint') }, about: { title: t('settings.about'), subtitle: t('settings.aboutHint') }, }; const activeHeader = sectionHeader[activeSection]; @@ -1373,26 +1379,6 @@ export function SettingsDialog({ {t('settings.welcomeKicker')}

{t('settings.welcomeTitle')}

{t('settings.welcomeSubtitle')}

- {/* First-run users see a mini pet teaser inside the welcome - modal so adoption is part of the warm intro rather than - hidden behind another nav click. The chip nudges them - toward Pets without forcing them to leave the rest of - the welcome flow. */} - ) : ( <> @@ -1427,17 +1413,6 @@ export function SettingsDialog({ {t('settings.memoryHint')} - + + - + +
+ +
+ setSearch(e.target.value)} + /> +
+ {(['all', 'user', 'built-in'] as const).map((s) => { + const count = + s === 'all' + ? skills.length + : skills.filter((skill) => skill.source === s).length; + return ( + + ); + })} +
+
+ + {modeOptions.map(([mode, count]) => ( + + ))} +
+ {categoryOptions.length > 0 ? ( +
+ + {categoryOptions.map(([cat, count]) => ( + + ))} +
+ ) : null} +
+ + {creating ? ( + void submitDraft()} + /> + ) : null} + + {filteredSkills.length === 0 ? ( +
+ {t('settings.libraryNoResults')} +
+ ) : ( +
+ {filteredSkills.map((skill) => { + const enabled = !disabledSkills.has(skill.id); + const isExpanded = expandedId === skill.id; + const isEditing = editingId === skill.id; + return ( + toggleExpanded(skill.id)} + onToggleEnabled={(e) => toggleEnabled(skill.id, e)} + onStartEdit={() => void startEdit(skill)} + onArmDelete={() => armDelete(skill.id)} + onCancelDelete={cancelDelete} + onCommitDelete={() => void commitDelete(skill.id)} + onCancelEdit={cancelDraft} + onSubmitEdit={() => void submitDraft()} + /> + ); + })} +
+ )} + + ); +} + +interface SkillRowProps { + skill: SkillSummary; + enabled: boolean; + expanded: boolean; + editing: boolean; + body: string | undefined; + bodyLoading: boolean; + files: SkillFileEntry[] | null; + filesLoading: boolean; + confirmDelete: boolean; + draft: DraftState | null; + draftError: string | null; + draftSaving: boolean; + setDraft: Dispatch>; + onToggleExpanded: () => void; + onToggleEnabled: (enabled: boolean) => void; + onStartEdit: () => void; + onArmDelete: () => void; + onCancelDelete: () => void; + onCommitDelete: () => void; + onCancelEdit: () => void; + onSubmitEdit: () => void; +} + +function SkillRow({ + skill, + enabled, + expanded, + editing, + body, + bodyLoading, + files, + filesLoading, + confirmDelete, + draft, + draftError, + draftSaving, + setDraft, + onToggleExpanded, + onToggleEnabled, + onStartEdit, + onArmDelete, + onCancelDelete, + onCommitDelete, + onCancelEdit, + onSubmitEdit, +}: SkillRowProps) { + const t = useT(); + const summaryName = skill.name || skill.id; + return ( +
+
+ +
+ {confirmDelete ? ( + + + + + ) : ( + <> + + + + )} + +
+
+ + {expanded && !editing ? ( +
+
+
SKILL.md
+ {bodyLoading ? ( +

{t('settings.libraryLoading')}

+ ) : ( +
{body ?? ''}
+ )} +
+
+
{t('settings.skillsFiles')}
+ {filesLoading ? ( +

{t('settings.libraryLoading')}

+ ) : !files || files.length === 0 ? ( +

{t('settings.skillsNoFiles')}

+ ) : ( +
    + {files.map((entry) => ( +
  • + + {leafName(entry.path)} + {entry.kind === 'file' && typeof entry.size === 'number' ? ( + + {formatSize(entry.size)} + + ) : null} +
  • + ))} +
+ )} +
+
+ ) : null} + + {editing && draft ? ( + + ) : null} +
+ ); +} + +interface SkillDraftFormProps { + heading: string; + subheading: string | null; + draft: DraftState; + setDraft: Dispatch>; + error: string | null; + saving: boolean; + isEdit: boolean; + onCancel: () => void; + onSubmit: () => void; +} + +function SkillDraftForm({ + heading, + subheading, + draft, + setDraft, + error, + saving, + isEdit, + onCancel, + onSubmit, +}: SkillDraftFormProps) { + const t = useT(); + return ( +
+
+
+

{heading}

+ {subheading ?

{subheading}

: null} +
+
+
+ + +
+