DannyMac180/skills/tree/main/modsmith/templates/mode-registry
mode-registry
A shared /mode switch that any plugin can offer a mode to. Shows the active mode in the prompt footer. Makes no model calls.
About this mod
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.
Installation
Check the author's README for the marketplace and plugin name first. Commands may change as the repository evolves.
claude plugin marketplace add DannyMac180/skills claude plugin install mode-registry
Original text / 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.
