新 Mod 的起步模板:一个面板 + 一个命令 + 输入框上方的提示 / Starter: one pane, one command, one hint above the prompt

English · 简体中文
A starter template for a new mod: a /starter command opens a pane with a counter button. Copy it to start your own:
cp -r templates/starter-mod plugins/my-mod
Then rename every starter-mod to my-mod (name in plugin.json, PANE and the state keys in hooks/register.tsx, types/index.d.ts, and plugin in the tests). See CONTRIBUTING.md at the repo root.
hooks/register.tsx 33 lines1import type { Register } from 'claude-code'
2
3// 改名时,把这里、plugin.json 的 name、types/index.d.ts 里的键一起改掉
4const PANE = 'starter-mod'
5const CLICKS = { plugin: 'starter-mod', key: 'clicks' } as const
6
7export const register: Register = on => {
8 // 会话开始:注册命令。immediate 让 Claude 干活时输入命令也能马上执行
9 on('session.start', async ($, e, next) => {
10 await $.command.register({ name: 'starter', description: '打开起步模板的面板', immediate: true })
11 return next(e)
12 })
13
14 // 命令:打开面板。不回文字,免得在对话里留一行
15 on('command.run', { command: 'starter' }, async $ => {
16 await $.ui.open({ id: PANE, title: 'Starter', focus: true })
17 return {}
18 })
19
20 // 面板:读 $.state 画界面;按钮里改 $.state,读到它的界面会自动重画
21 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
22 const { Box, Text, Button } = $.ui.resolve(e)
23 const { value: clicks = 0 } = await $.state.get(CLICKS)
24 return (
25 <Box flexDirection="column" gap={1}>
26 <Text bold>Hello from a mod</Text>
27 <Text dimColor>{`clicked ${clicks} times`}</Text>
28 <Button key="click" label="Click" onPress={() => void $.state.set(CLICKS, clicks + 1)} />
29 </Box>
30 )
31 })
32}
33types/index.d.ts 11 lines1// $.state 里存的值要在这里声明类型(插件名 → 键 → 类型),validate 会逐个核对
2export type Clicks = number
3
4declare module 'claude-code' {
5 interface PluginState {
6 'starter-mod': {
7 clicks: Clicks
8 }
9 }
10}
11