* 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 <projectDir>/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 <cursoragent@cursor.com>
* 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 `<repo>/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 <cursoragent@cursor.com>
* 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 <cursoragent@cursor.com>
* fix(web): drop staged-skill chips when the matching @<id> 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 `@<id>` token from the textarea left
the chip staged, so the request still carried `skillIds: [<id>]` 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
`@<id>` 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 `@<path>` token,
so a symmetric prune there would erase legitimate uploads.
Co-authored-by: Cursor <cursoragent@cursor.com>
* 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 `<cwd>/.od-skills/<folder>/`)
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 <cursoragent@cursor.com>
* 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 `<ProjectView>` 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 <cursoragent@cursor.com>
* 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 <cursoragent@cursor.com>
* 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 <cursoragent@cursor.com>
* 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 <cursoragent@cursor.com>
* 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
`<userSkillsDir>/<slug>/`. 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 <cursoragent@cursor.com>
9.7 KiB
Presenter Mode Guide · 演讲者模式指南
这份文档说明如何在 html-ppt skill 里做出带逐字稿的演讲者模式 PPT。
何时使用演讲者模式
当用户的需求涉及以下任何一项时,优先使用演讲者模式:
- 提到"演讲"、"分享"、"讲稿"、"逐字稿"、"speaker notes"
- 提到"presenter view"、"演讲者视图"、"演讲者模式"
- 需要"30 分钟 / 45 分钟 / 1 小时的分享"
- 说"我要去给团队讲 xxx"、"要做一场技术分享"、"要做路演"
- 强调"不想忘词"、"怕讲不流畅"、"需要提词器"
如果用户只要做一份"静态好看的 PPT"(例如小红书图文、产品图册、汇报 slides 自己不讲),不需要演讲者模式。
两种做法
✅ 推荐做法:直接用 presenter-mode-reveal 模板
cp -r templates/full-decks/presenter-mode-reveal examples/my-talk
这个模板已经预设好所有必需元素:
- 支持 S 键切换演讲者视图
- 5 个主题可用 T 键循环(tokyo-night / dracula / catppuccin-mocha / nord / corporate-clean)
- 左右键翻页
- 每一页都有 150–300 字的示例逐字稿
- 底部有键位提示
直接改内容即可。
🔧 进阶做法:给任意已有模板加演讲者模式
html-ppt 的 S 键演讲者视图是 runtime.js 内置的,所有 full-deck 模板都自动支持。你只需要做两件事:
- 每张 slide 末尾加
<aside class="notes">(或<div class="notes">),里面写逐字稿 - 确认 HTML 引入了
assets/runtime.js
<section class="slide">
<h2>你的标题</h2>
<p>内容...</p>
<aside class="notes">
<p>这里是演讲时要说的话,150-300 字...</p>
</aside>
</section>
逐字稿写作三铁律
这是整个方法论的核心。AI 在帮用户写逐字稿时必须遵守:
铁律 1:不是讲稿,是"提示信号"
❌ 错误写法(像在念稿):
大家好,欢迎来到今天的分享。今天我将要给大家介绍一下我们团队在过去三个月做的工作。
首先,我们来看一下背景情况。在过去的三个月中,我们遇到了以下几个问题……
✅ 正确写法(提示信号 + 加粗核心):
<p>欢迎!今天分享我们团队<strong>过去 3 个月</strong>的工作。</p>
<p>先说<em>背景</em>——三个月前我们遇到了<strong>三个核心问题</strong>:
延迟高、成本炸、稳定性差。</p>
<p>接下来逐个讲解怎么解的。</p>
差别:正确版本把关键词加粗,过渡句独立成段,看一眼就能接上。
铁律 2:每页 150–300 字
- 少于 150 字:提示不够,讲到一半会卡
- 多于 300 字:你根本来不及扫完
- 2–3 分钟/页 是最舒服的节奏
铁律 3:用口语,不用书面语
| ❌ 书面语 | ✅ 口语 |
|---|---|
| 因此 | 所以 |
| 该方案 | 这个方案 |
| 然而 | 但是 / 不过 |
| 进行优化 | 优化一下 |
| 我们将会 | 我们会 / 接下来 |
| 综上所述 | 所以简单来说 |
检查方法:写完读一遍,听起来像说话才对。
必备 HTML 结构
<!DOCTYPE html>
<html lang="zh-CN" data-themes="tokyo-night,dracula,corporate-clean">
<head>
<meta charset="utf-8">
<title>...</title>
<link rel="stylesheet" href="../../../assets/fonts.css">
<link rel="stylesheet" href="../../../assets/base.css">
<link rel="stylesheet" id="theme-link" href="../../../assets/themes/tokyo-night.css">
<link rel="stylesheet" href="../../../assets/animations/animations.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="deck">
<section class="slide" data-title="Cover">
<h1>你的标题</h1>
<p>副标题</p>
<aside class="notes">
<p>讲稿段落 1(加<strong>加粗关键词</strong>)。</p>
<p>讲稿段落 2(过渡句独立成段)。</p>
<p>讲稿段落 3(自然收尾,引出下一页)。</p>
</aside>
</section>
<!-- 更多 slide ... -->
</div>
<script src="../../../assets/runtime.js"></script>
</body>
</html>
演讲者视图显示的内容
按 S 键后,弹出一个独立的演讲者窗口(原页面保持观众视图不变)。演讲者窗口是 4 个独立的磁吸卡片:
观众窗口(原页面) 演讲者窗口(磁吸卡片)
┌─────────────────┐ ┌─────────────────────┬──────────────────┐
│ │ │ 🔵 CURRENT │ 🟣 NEXT │
│ 正常 slide │ │ ━━━━━━━━━━━━━━━━ │ ━━━━━━━━━━━━━ │
│ 全屏展示 │◄►│ │ iframe preview │
│ │ │ iframe preview │ (下一页) │
│ │ │ (当前页) ├──────────────────┤
│ │ │ │ 🟠 SPEAKER SCRIPT │
│ │ │ │ ━━━━━━━━━━━━━ │
│ │ ├─────────────────────┤ [大字号逐字稿] │
│ │ │ 🟢 TIMER │ [可滚动] │
│ │ │ ⏱ 12:34 3 / 8 │ │
│ │ │ [← Prev][Next →] │ │
└─────────────────┘ └─────────────────────┴──────────────────┘
↑ BroadcastChannel 双向同步翻页 ↑
卡片交互规则:
- 拖动卡片 header(带彩色圆点和标题的顶部条)→ 移动卡片位置
- 拖动卡片右下角的三角手柄 → 调整卡片大小
- 位置/尺寸自动保存到 localStorage,下次打开恢复
- 底部 "重置布局" 按钮恢复默认排列
卡片内容:
- 🔵 CURRENT — 当前页 像素级完美预览(iframe 加载原 HTML 文件的
?preview=N模式,错色不可能) - 🟣 NEXT — 下一页预览,同样像素级完美
- 🟠 SPEAKER SCRIPT — 逐字稿,字号 18px,支持
<strong>(橘色加粗)、<em>(蓝色强调)、<code>等 inline 样式 - 🟢 TIMER — 计时器不会丢失焦点,带切页按钮
两窗口同步:在任一窗口按 ← → 翻页,另一个窗口自动同步(BroadcastChannel)。
丝滑翻页:iframe 只加载一次,后续翻页用 postMessage 切换可见的 slide,不重新加载、不闪烁。
键盘快捷键(演讲者模式)
| 键 | 动作 |
|---|---|
S |
打开演讲者窗口(弹出新窗口,原页面保持观众视图) |
← → / Space / PgDn |
翻页(即使在演讲者视图里) |
T |
切换主题 |
R |
重置计时器(仅演讲者视图下) |
F |
全屏 |
O |
总览 |
Esc |
关闭所有浮层 |
双屏演讲的标准流程
- 打开
index.html,按S→ 弹出演讲者窗口 - 把观众窗口(原页面)拖到投影 / 外接屏,按
F全屏 - 把演讲者窗口(弹窗)留在你面前的屏幕
- 在任一窗口按 ← → 翻页,两边自动同步
- 演讲者窗口里看逐字稿 + 下一页 + 计时器
💡 为什么预览像素级完美:每个预览是一个
<iframe>,它加载的就是同一个 deck HTML 文件,只是 URL 多了?preview=N参数。runtime.js检测到这个参数时只渲染第 N 页、隐藏所有 chrome。iframe 使用与观众视图完全相同的 CSS、主题、字体和 viewport——颜色和排版保证一致。外层用 CSStransform: scale()把 1920×1080 缩到卡片宽高,等比缩放不变形。
💡 为什么不闪烁:iframe 初次加载后就常驻,翻页时 presenter 窗口通过
postMessage({type:'preview-goto', idx:N})告诉 iframe 切换到第 N 页。iframe 内的 runtime.js 只切换.is-activeclass,不重新加载、不渲染白屏。
常见错误
❌ 把逐字稿写在 slide 可见位置
<!-- 错误:这段文字观众会看到 -->
<p style="font-size:12px;color:gray">
这里讲 xxx,然后讲 yyy...
</p>
✅ 正确:
<aside class="notes">
<p>这里讲 xxx,然后讲 yyy...</p>
</aside>
.notes 类默认 display:none,只在演讲者视图可见。
❌ 忘记引入 runtime.js
没有 <script src="../../../assets/runtime.js"></script> = 没有 S 键、没有演讲者视图、没有翻页。
❌ 逐字稿用书面语
念出来像 AI 机器人。写完一定读一遍。
❌ 每页 50 字
提示不够,照样忘词。
❌ 每页 500 字
眼睛根本扫不过来,等于没写。
用 AI 生成逐字稿的标准 prompt
"请为每一张 slide 写一段 150-300 字的逐字稿,放在
<aside class="notes">里。 要求:
- 用口语,不要书面语(所以/但是/接下来,不是因此/然而/综上所述)
- 把核心关键词用
<strong>加粗- 过渡句独立成段(每段 1-3 句)
- 读起来像说话,不像念稿
- 结尾要有自然的过渡,引出下一页"
推荐搭配
- 主题:
tokyo-night(深色,技术分享首选)、corporate-clean(浅色,商务汇报)、dracula(深色备选) - 字体:默认 Noto Sans SC + JetBrains Mono,无需更改
- 动效:克制使用,
fade-up/rise-in最自然,不要用glitch-in/confetti-burst之类花哨的 - 页数:30 分钟分享 = 8–12 页;45 分钟 = 12–16 页;1 小时 = 16–22 页