Files
Tom Huang b5eb8c1647 feat: generic skills + split skills/design-templates + finalize-design API (#955)
* 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>
2026-05-11 17:48:34 +08:00

9.7 KiB
Raw Permalink Blame History

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
  • 左右键翻页
  • 每一页都有 150300 字的示例逐字稿
  • 底部有键位提示

直接改内容即可。

🔧 进阶做法:给任意已有模板加演讲者模式

html-ppt 的 S 键演讲者视图是 runtime.js 内置的,所有 full-deck 模板都自动支持。你只需要做两件事:

  1. 每张 slide 末尾加 <aside class="notes">(或 <div class="notes">),里面写逐字稿
  2. 确认 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每页 150300 字

  • 少于 150 字:提示不够,讲到一半会卡
  • 多于 300 字:你根本来不及扫完
  • 23 分钟/页 是最舒服的节奏

铁律 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 关闭所有浮层

双屏演讲的标准流程

  1. 打开 index.html,按 S → 弹出演讲者窗口
  2. 观众窗口(原页面)拖到投影 / 外接屏,按 F 全屏
  3. 演讲者窗口(弹窗)留在你面前的屏幕
  4. 在任一窗口按 ← → 翻页,两边自动同步
  5. 演讲者窗口里看逐字稿 + 下一页 + 计时器

💡 为什么预览像素级完美:每个预览是一个 <iframe>,它加载的就是同一个 deck HTML 文件,只是 URL 多了 ?preview=N 参数。runtime.js 检测到这个参数时只渲染第 N 页、隐藏所有 chrome。iframe 使用与观众视图完全相同的 CSS、主题、字体和 viewport——颜色和排版保证一致。外层用 CSS transform: scale() 把 1920×1080 缩到卡片宽高,等比缩放不变形。

💡 为什么不闪烁iframe 初次加载后就常驻,翻页时 presenter 窗口通过 postMessage({type:'preview-goto', idx:N}) 告诉 iframe 切换到第 N 页。iframe 内的 runtime.js 只切换 .is-active class不重新加载、不渲染白屏

常见错误

把逐字稿写在 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"> 里。 要求:

  1. 口语,不要书面语(所以/但是/接下来,不是因此/然而/综上所述)
  2. 核心关键词<strong> 加粗
  3. 过渡句独立成段(每段 1-3 句)
  4. 读起来像说话,不像念稿
  5. 结尾要有自然的过渡,引出下一页"

推荐搭配

  • 主题tokyo-night(深色,技术分享首选)、corporate-clean(浅色,商务汇报)、dracula(深色备选)
  • 字体:默认 Noto Sans SC + JetBrains Mono无需更改
  • 动效:克制使用,fade-up / rise-in 最自然,不要用 glitch-in / confetti-burst 之类花哨的
  • 页数30 分钟分享 = 812 页45 分钟 = 1216 页1 小时 = 1622 页