DannyMac180/skills/tree/main/modsmith/templates/mode-registry
关于这个 mod
mode-registry
一个 /mode 开关,任何插件都可以向它提供模式。它会在提示页脚显示当前模式。永远不调用模型。
这是“mod 组合”模式:你只需安装一次 registry;模型路由器、“artifact mode”,或这个模板旁边的 effort modes,都可以向它加入自己的模式。当前模式会以变暗的标签显示在提示页脚(例如 review mode,和 focus 及其他模式并列),并可通过 /mode 切换。
它是一个注册表,不知道任何模式具体做什么。它只维护列表和当前选择。每个插件读取这个选择,再改变自己的行为。
可以做什么
| 输入 | 结果 | | --- | --- | | /mode | 在提示上方打开选择器:每个模式一个按钮(快捷键 1-9,0 表示关闭),另有 Close。点击它或按 ctrl+x tab 将按键交给选择器。 | | /mode review | 切换到 id 为 review 的模式。页脚显示 review mode。 | | /mode off | 清除模式(none 和 clear 也可以)。 | | /mode list | 打印所有可用模式、提供它的插件,以及当前启用的模式。 |
活动模式保存在当前会话的 $.state 中,不会跨会话保存(要保存它,也写入 $.store)。registry 不会在 /clear 时重置它。引擎是否在 /clear 后保留 $.state 尚未检查。
成本
永远不调用模型。 页脚标签和选择器只是 UI,不会进入上下文。每条 /mode 命令只给转录增加一行,模型会读取它(约 10-20 个 token;/mode list 每个模式约 15 个 token)。在选择器中按键不会增加任何内容:它会静默切换模式,所以模型不会被告知(想让模型知道时使用 /mode <id>)。这行内容会追加在已缓存前缀之后,因此不会破坏提示缓存。某个模式本身的成本由提供它的插件决定。例如 effort-modes 的切换会产生一次缓存重建。
从你的插件提供模式
契约在 types/index.d.ts 中,包含这个插件拥有的三个值:
- mode-registry.catalog:ModeSpec[],所有可提供的模式
- mode-registry.active:string | null,当前模式的 id
- mode-registry.isPicking:boolean,选择器是否打开
只有 registry 会写入这些值。你可以通过 hook registry 对 catalog 的写入来提供模式:
on('state.set', { plugin: 'mode-registry', key: 'catalog' }, ($, e, next) =>
next({
...e,
value: [
...e.value.filter(m => m.id !== 'router'),
{ id: 'router', label: 'Router', description: 'Picks the model per turn', owner: 'my-router' },
],
}),
)
然后在行为所在的位置读取活动模式:
const { value: active = null } = await $.state.get({ plugin: 'mode-registry', key: 'active' })
if (active !== 'router') return next(e)
几个细节很重要:
- 添加自己的 id 前,先过滤自己的 id。registry 会在会话开始时和每次 /mode 时重写 catalog;先过滤可以防止自己的模式出现两次。
- 在实际执行行为的 hook 中读取 active。不要复制到跨回合保留的模块变量中:热重载会清空模块变量,而在 ui.render 中读取会让你订阅重绘。
- 如果没有安装 registry,也不会出错。版本 0 中 active 读取为 undefined,state.set hook 不会触发,插件会运行默认行为。
- 要从代码切换模式(例如自动路由器看到 diff 后切换到 review),调用 $.command.run({ command: 'mode', args: 'review' })。它走相同路径和相同 veto,并会排队到会话空闲。
- 要拒绝或重定向切换,可对 { plugin: 'mode-registry', key: 'active' } 挂 state.set hook,并传给 next 另一个 value。/mode 会报告实际落地的值,因此用户能看到 veto。
类型检查时,在 plugin.json 中列出依赖:"dependencies": ["mode-registry"]。引擎从文件夹加载插件时,会把这个契约写入 .claude-plugin/types/mode-registry/,你的 tsconfig.json 会读取它(effort-modes 就是这样做的)。注意:声明该依赖的插件在没有 registry 时完全不会加载。如果插件也应该独立工作,就不要写依赖,而是在启用 mode-registry 的会话中运行 /plugin-types。绝不要手动复制这个文件。
组合方式
- effort-modes(在这个模板旁边):提供 ui、api 和 review。
- 任何在 SessionMode 页脚或 AbovePrompt 区域绘制的插件。registry 会把标签加入 e.props.modes,把其他内容继续传递,并在选择器下方绘制 {await next(e)}。
安装/加载
claude plugin validate templates/mode-registry
claude plugin test templates/mode-registry # 4 tests, incl. a veto from a third plugin
claude --plugin-dir templates/mode-registry --plugin-dir templates/effort-modes
function hooks 处于 early access。如果你的构建默认不加载 hooks 模块,请设置 CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1。该设置会加载每个已安装且提供 hooks 的插件的 hooks 模块,而不只是这个插件。
安装
请先查看作者 README 确认 marketplace 和插件名称;命令可能随仓库结构改变。
claude plugin marketplace add DannyMac180/skills claude plugin install mode-registry
原文 / README
mode-registry
One /mode switch that every plugin can offer a mode to. You install the
registry once; a model router, an "artifact mode", or the effort modes beside
this template each add their own mode to it. You see the active mode as a dim
label in the prompt footer (review mode, beside focus and the others) and
switch with /mode.
This is the "mods composing" pattern: the registry doesn't know what any mode does. It keeps the list and the current choice. Each plugin reads the choice and changes its own behaviour.
What you can do
| You type | What happens |
| --- | --- |
| /mode | Opens a picker above the prompt: one button per mode (hotkeys 1-9, 0 for off) and Close. Click it or press ctrl+x tab to give it the keys. |
| /mode review | Switches to the mode with id review. The footer reads review mode. |
| /mode off | Clears the mode (none and clear work too). |
| /mode list | Prints every mode on offer, the plugin that offers it, and which one is on. |
The active mode lives in $.state for the session and isn't saved between
sessions (to save it, write it to $.store as well). The registry doesn't
reset it on /clear. Whether the engine keeps $.state across a /clear has
not been checked.
What it costs
No model calls, ever. The footer label and the picker are UI only and
never enter the context. Each /mode command adds one short line to the
transcript, which the model reads (about 10-20 tokens; /mode list is about
15 tokens per mode). A press in the picker adds nothing: it switches the mode
silently, so the model isn't told (use /mode <id> when you want it to know). That line is appended after the cached prefix, so it never
breaks the prompt cache. Whatever a mode costs is up to the plugin that
offers it. For example, effort-modes' switch costs one cache rebuild (see its
README).
Offering a mode from your plugin
The contract is types/index.d.ts. It has three values this plugin owns:
mode-registry.catalog:ModeSpec[], every mode on offermode-registry.active:string | null, the active mode's idmode-registry.isPicking:boolean, whether the picker is open
Only the registry writes them. You offer a mode by hooking the registry's
write to catalog and adding yours:
on('state.set', { plugin: 'mode-registry', key: 'catalog' }, ($, e, next) =>
next({
...e,
value: [
...e.value.filter(m => m.id !== 'router'),
{ id: 'router', label: 'Router', description: 'Picks the model per turn', owner: 'my-router' },
],
}),
)
Then read the active mode wherever your behaviour lives:
const { value: active = null } = await $.state.get({ plugin: 'mode-registry', key: 'active' })
if (active !== 'router') return next(e)
Some details that matter:
- Filter your own id before adding it. The registry rewrites the catalog at
session start and on every
/mode. Filtering first keeps your offer from appearing twice. - Read
activein the hook that acts. Don't copy it into a module variable you keep across turns: a hot reload wipes module variables, and a value read inside aui.rendersubscribes you to redraws. - If the registry isn't installed, nothing breaks.
activereads asundefinedat version 0, yourstate.sethook never fires, and your plugin runs its default behaviour. - To switch modes from code (an auto-router that flips to
reviewwhen it sees a diff), call$.command.run({ command: 'mode', args: 'review' }). It goes through the same path and the same vetoes as the person typing it. It queues until the session is idle. - To refuse or redirect a switch, hook
state.seton{ plugin: 'mode-registry', key: 'active' }and passnextanothervalue./modereports what actually landed, so the person sees the veto.
For type-checking, list the registry in your plugin.json:
"dependencies": ["mode-registry"]. The engine then writes this contract into
your plugin's .claude-plugin/types/mode-registry/ when it loads it from your
folder, and your tsconfig.json picks it up (effort-modes does this). The
catch: a plugin with that dependency doesn't load at all without the registry.
If yours should also work alone, leave the dependency out and run
/plugin-types in a session where mode-registry is enabled instead. Never
copy the file into your plugin by hand.
Composes with
- effort-modes (beside this template): offers
ui,apiandreview. - Any plugin that draws in the
SessionModefooter or theAbovePromptband. The registry adds its label toe.props.modesand passes the rest on, and draws{await next(e)}under its picker.
Install / load
claude plugin validate templates/mode-registry
claude plugin test templates/mode-registry # 4 tests, incl. a veto from a third plugin
claude --plugin-dir templates/mode-registry --plugin-dir templates/effort-modes
Function hooks are early access. If your build doesn't load hooks modules by
default, set CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1. That setting loads the
hooks module of every installed plugin that ships one, not only this one.
