跳转到主要内容
examples/global-skills-mod 包是跨对话的全局、持久 Skill 选择的参考实现。它源于 Aether issue #27,演示如何在单个 Script Mod 中组合 UI 替换与原生服务调用 — 无需 Native Mod。

此 mod 做什么

  • 用自定义托盘替换 chat.composer.actionTray,显示当前全局 Skill 选择与展开/折叠切换
  • 隐藏 chat.composer.skillPicker(内置的按对话 Skill 行),保持 UI 简洁
  • 从 Aether 原生 skills 服务读取完整 Skill 目录
  • 通过 skills.setSelected 写回单个 Skill 开关,通过 skills.setSelection 写回批量选择
  • 在扩展存储中持久化展开/折叠状态
  • 在会话抽屉中添加「Global Skills」全屏页面,用于专门管理

包声明

package.jsonaether.extensions 下注册单个入口。没有 pi 键,因为此 mod 没有 Pi Agent 组件:

完整带注释代码导读

1. 导入

因为这是仅脚本的 Aether Mod(无 Pi 集成),不从 @earendil-works/pi-coding-agent 导入。本地 Skill 类型镜像原生 skills 服务 list 方法返回的结构,在各 render 回调中用于类型安全解构。

2. 组件注册:替换操作托盘并隐藏 skill picker

registerComponent 以命名的内置 Compose UI 为目标。这里两个注册协同工作:
  • chat.composer.actionTray 上使用 mode: "replace" — 完全替换默认的已选 Skill/MCP/Agent 模式托盘。order: 100 确保此注册优先于其他扩展的更低优先级替换。
  • chat.composer.skillPicker 上使用 mode: "hide" — 抑制出现在输入框加号菜单中的内置按对话 Skill 行。因为 mod 通过托盘接管全局 Skill 选择,原生按对话选择器会显得冗余且易混淆。hide 注册不需要 render — 目标会直接消失。

3. 页面注册:Global Skills 管理页

registerPage 在会话抽屉中添加全屏目标页。要点:
  • context.skills — 来自实时应用状态;Aether 会自动将 Skill 目录与其余 UI 状态一并提供。
  • context.default_skill_ids — 反映原生 skills 服务的当前默认选择。页面据此派生 defaultIds 来驱动开关状态,无需在扩展存储中维护副本。
  • ui.switch(label, checked, actionId, options) — 渲染带标签的切换行。每个开关会触发 set-skill 操作,并以 { skill_id: skill.id } 作为 args,将具体 Skill ID 传给操作处理程序。
  • skill.enabled !== false — 过滤掉平台已禁用的 Skill;它们不应出现在管理 UI 中。
  • scroll: true — 使列在 Skill 目录较大时可滚动。

4. 表面:在托盘中显示当前全局 Skill 选择

chat.composer.actionTray 替换的 render 回调读取实时上下文并构建托盘 UI:
  • context.selected_skill_ids — 反映当前打开对话的活动 Skill 选择,来自原生状态。托盘据此派生 selectedSkills 以显示数量与名称,无需复制状态。
  • context.storage.expanded — 折叠/展开切换状态是扩展存储中唯一保存的内容;其余都从原生应用状态读取。
  • 折叠状态 — 当 expandedfalse 且至少选中一个 Skill 时,托盘以 · 连接 Skill 名称作为 muted 说明。未选中任何内容时显示 “No defaults”。
expandedtrue 时,托盘渲染可滚动的 Skill 开关列表与清除按钮:

5. 从 skills 服务读取 Skills

Aether 将 Skill 目录与当前选择直接注入 render 上下文的 context.skillscontext.selected_skill_ids。你无需在 render 内手动调用 aether.services.invoke("skills", "list") — 平台会保持这些字段最新,并在变更时重新渲染表面。 当你需要命令式读取 Skills(在 render 外,例如在操作处理程序中)时,使用:

6. 通过 skills.setSelectedskills.setSelection 写回选择

  • setSelected 切换单个 Skill。scope: "both" 同时更新当前对话的选择与默认选择。原生服务将变更传播回应用状态,从而触发所有读取 selected_skill_ids 的表面重新渲染。
  • setSelection 用显式 ID 列表替换整个选择。clear 操作传入空数组,以在两个作用域中取消全选。

7. 在 storage 中持久化展开/折叠偏好

展开/折叠偏好是写入 storage 的唯一值:
因为 Skill 选择通过 setSelected / setSelection 直接写入原生状态,无需在扩展存储中镜像它们。Aether 原生 skills 服务会自行持久化默认选择。托盘在每次渲染时读取 context.storage.expanded,以在会话间恢复正确的折叠或展开状态。

展示的关键模式

  • 组合 registerComponent(replace + hide)与 registerPage — 你可以完全抑制内置 UI 元素,同时替换为自己的实现并添加专用管理页,全部在一次 activateAether 工厂调用中完成。
  • 使用 aether.services.invoke("skills", ...) 操作真实原生状态 — 不在扩展存储中维护 Skill 数据的影子副本,而是对实际原生 skills 服务读写。这样选择始终与 Aether 其余部分所见保持一致。
  • Storage 写入会触发表面重渲染storage.set()storage.delete()storage.clear() 会自动使扩展 UI 失效。在此 mod 中,toggle-expanded 更新 storage.expanded,框架会重渲染托盘,无需显式调用 aether.invalidate()
此 mod 仅为 Script Mod(aether.extensions)— 无需 Native Mod 即可实现完整 Skill 管理,展示了 Script API 的能力。所有持久状态位于 Aether 原生 skills 服务中;扩展存储仅用于托盘的展开/折叠偏好。若需要在更低层级拦截 Skill 选择(例如锁定某个 Skill 禁止取消选择),可将此模式与 aether.intercept("skills.selection", ...) 组合使用。