4 4
5# 使用 mod 在界面中绘制5# 使用 mod 在界面中绘制
6 6
7> 从 Claude Code mod 中绘制窗格、提示符上方的条带、按钮和文本字段,处理按键和输入,并在重绘和会话之间保持状态。7> 从 Claude Code mod 中绘制窗格、输入框上方的条带、按钮和文本字段,处理按键和输入,并在重绘和会话之间保持状态。
8 8
9mod 可以在 Claude Code 中绘制自己的界面,并更改 Claude Code 已经绘制的界面部分。mod 可以绘制的每个位置称为[渲染站点](/docs/zh-CN/plugins/mods/reference#render-sites),例如窗格、提示符上方的条带或加载指示器。Claude Code 在即将绘制渲染站点时会触发 [`ui.render`](/docs/zh-CN/plugins/mods/reference#interface) 事件,你的该事件钩子返回在那里绘制的内容。9mod 可以在 Claude Code 中绘制自己的界面,并更改 Claude Code 已经绘制的界面部分。mod 可以绘制的每个位置称为[渲染站点](/docs/zh-CN/plugins/mods/reference#render-sites),例如窗格、输入框上方的条带或加载指示器。Claude Code 每次即将绘制渲染站点时都会触发 [`ui.render`](/docs/zh-CN/plugins/mods/reference#interface) 事件,您为该事件编写的 hook 返回要在那里绘制的内容。
10 10
11此地图显示 mod 可以在终端会话中的绘制位置:11此地图显示 mod 可以在终端会话中的绘制位置:
12 12
13<img src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-screen-map.svg?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=5fda26b6609c62b68c6f9e528c1590ea" className="dark:hidden" alt="Claude Code 终端会话的地图。mod 可以在右侧添加窗格作为侧边栏,在记录的右上角添加 toast,在记录中添加日志行,在提示符上方添加条带,以及在提示符下方添加状态行。mod 可以重绘消息、工具调用行和加载指示器。提示符是 Claude Code 自己的。" width="600" height="336" data-path="images/mods-screen-map.svg" />13<img src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-screen-map.svg?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=5fda26b6609c62b68c6f9e528c1590ea" className="dark:hidden" alt="Claude Code 终端会话的地图。mod 可以在右侧添加窗格作为侧边栏,在会话记录的右上角添加 toast,在会话记录中添加日志行,在输入框上方添加条带,以及在输入框下方添加状态栏。mod 可以重绘消息、工具调用行和加载指示器。输入框是 Claude Code 自己的。" width="600" height="336" data-path="images/mods-screen-map.svg" />
14 14
15<img src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-screen-map-dark.svg?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=5b4161581a1bd2c0450b0c8b57bc1225" className="hidden dark:block" alt="Claude Code 终端会话的地图。mod 可以在右侧添加窗格作为侧边栏,在记录的右上角添加 toast,在记录中添加日志行,在提示符上方添加条带,以及在提示符下方添加状态行。mod 可以重绘消息、工具调用行和加载指示器。提示符是 Claude Code 自己的。" width="600" height="336" data-path="images/mods-screen-map-dark.svg" />15<img src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-screen-map-dark.svg?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=5b4161581a1bd2c0450b0c8b57bc1225" className="hidden dark:block" alt="Claude Code 终端会话的地图。mod 可以在右侧添加窗格作为侧边栏,在会话记录的右上角添加 toast,在会话记录中添加日志行,在输入框上方添加条带,以及在输入框下方添加状态栏。mod 可以重绘消息、工具调用行和加载指示器。输入框是 Claude Code 自己的。" width="600" height="336" data-path="images/mods-screen-map-dark.svg" />
16 16
17在较窄的终端中,窗格位于提示符上方而不是记录旁边。17在较窄的终端中,窗格位于输入框上方而不是会话记录旁边。
18 18
19在开始之前,请构建你的[第一个 mod](/docs/zh-CN/plugins/mods/create)。从工作示例开始,该示例构建一个具有两个选项卡和计数器的窗格,然后阅读你想要更改的每个部分的部分。19在开始之前,请先构建您的[第一个 mod](/docs/zh-CN/plugins/mods/create)。从完整示例开始,该示例构建一个具有两个选项卡和一个计数器的窗格,然后阅读您想要更改的每个部分对应的章节。
20 20
21<Note>21<Note>
22 要查找一个属性或限制,请参阅[参考](/docs/zh-CN/plugins/mods/reference#render-sites)。22 要查找某个属性或限制,请参阅[参考](/docs/zh-CN/plugins/mods/reference#render-sites)。
23</Note>23</Note>
24 24
25<h2 id="build-a-pane-with-tabs">25<h2 id="build-a-pane-with-tabs">
26 构建带有选项卡的窗格26 构建带有选项卡的窗格
27</h2>27</h2>
28 28
29在本部分中,你将构建一个 mod,该 mod 添加 `/hello-tabs` 命令,该命令打开一个窗格。窗格是在宽全屏终端中记录旁边的侧边栏,或在其他情况下是提示符上方的框架区域。此窗格显示两个选项卡,第二个选项卡有一个按钮,可以将计数器加一。重新启动 Claude Code 后,计数仍然存在。29在本部分中,您将构建一个 mod,该 mod 添加 `/hello-tabs` 命令,该命令打开一个窗格。窗格是在宽全屏终端中会话记录旁边的侧边栏,或在其他情况下是输入框上方的框架区域。此窗格显示两个选项卡,第二个选项卡有一个按钮,可以将计数器加一。重新启动 Claude Code 后,计数仍然存在。
30 30
31完成的 mod 看起来像这样。录制打开窗格,切换到第二个选项卡,按几次按钮,然后返回到第一个选项卡:31完成的 mod 看起来像这样。录制内容打开窗格,切换到第二个选项卡,按几次按钮,然后返回到第一个选项卡:
32 32
33<Frame>33<Frame>
34 <video autoPlay muted loop playsInline controls className="w-full dark:hidden" src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-hello-tabs-light.mp4?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=49d520094d87b5b44bfe50fa49677f06" aria-label="在 Claude Code 提示符处键入 /hello-tabs 命令,一个框架窗格在其上方打开,顶部显示"1: One"和"2: Two",文本为"This is the first tab."。第二个选项卡显示"Add one"按钮,旁边是"Count: 1",计数上升到 3。窗格然后返回到第一个选项卡。" data-path="images/mods-hello-tabs-light.mp4" />34 <video autoPlay muted loop playsInline controls className="w-full dark:hidden" src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-hello-tabs-light.mp4?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=49d520094d87b5b44bfe50fa49677f06" aria-label="在 Claude Code 输入框中键入 /hello-tabs 命令,一个框架窗格在其上方打开,顶部显示"1: One"和"2: Two",文本为"This is the first tab."。第二个选项卡显示"Add one"按钮,旁边是"Count: 1",计数上升到 3。窗格然后返回到第一个选项卡。" data-path="images/mods-hello-tabs-light.mp4" />
35 35
36 <video autoPlay muted loop playsInline controls className="w-full hidden dark:block" src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-hello-tabs-dark.mp4?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=ff7a14d713d6e5d3b0000efa8522ea4b" aria-label="在 Claude Code 提示符处键入 /hello-tabs 命令,一个框架窗格在其上方打开,顶部显示"1: One"和"2: Two",文本为"This is the first tab."。第二个选项卡显示"Add one"按钮,旁边是"Count: 1",计数上升到 3。窗格然后返回到第一个选项卡。" data-path="images/mods-hello-tabs-dark.mp4" />36 <video autoPlay muted loop playsInline controls className="w-full hidden dark:block" src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-hello-tabs-dark.mp4?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=ff7a14d713d6e5d3b0000efa8522ea4b" aria-label="在 Claude Code 输入框中键入 /hello-tabs 命令,一个框架窗格在其上方打开,顶部显示"1: One"和"2: Two",文本为"This is the first tab."。第二个选项卡显示"Add one"按钮,旁边是"Count: 1",计数上升到 3。窗格然后返回到第一个选项卡。" data-path="images/mods-hello-tabs-dark.mp4" />
37</Frame>37</Frame>
38 38
39Claude Code 没有内置的 tabs 元素,所以选项卡是一行中的两个按钮。mod 跟踪哪一个是活动的,并在该行下方绘制该选项卡的内容。39选项卡是一行中的两个按钮。mod 跟踪哪一个是活动的,并在该行下方绘制该选项卡的内容。
40 40
41<Steps>41<Steps>
42 <Step title="创建插件">42 <Step title="创建插件">
43 mod 是一个具有清单、指向你的代码的 `hooks.json` 和代码文件的插件。[创建 mod](/docs/zh-CN/plugins/mods/create#write-a-mod-yourself) 解释了每一个。创建一个名为 `hello-tabs` 的目录,其中包含 `.claude-plugin` 和 `hooks` 目录,然后保存前两个文件。43 mod 是一个具有清单、指向您的代码的 `hooks.json` 和代码文件的插件。[创建 mod](/docs/zh-CN/plugins/mods/create#write-a-mod-yourself) 解释了每一个。创建一个名为 `hello-tabs` 的目录,其中包含 `.claude-plugin` 和 `hooks` 目录,然后保存前两个文件。
44 44
45 将清单保存为 `hello-tabs/.claude-plugin/plugin.json`:45 将清单保存为 `hello-tabs/.claude-plugin/plugin.json`:
46 46
53 }53 }
54 ```54 ```
55 55
56 在 `hello-tabs/hooks/hooks.json` 中命名你的入口点:56 在 `hello-tabs/hooks/hooks.json` 中命名您的入口点:
57 57
58 ```json hello-tabs/hooks/hooks.json theme={null}58 ```json hello-tabs/hooks/hooks.json theme={null}
59 {59 {
63 </Step>63 </Step>
64 64
65 <Step title="编写代码">65 <Step title="编写代码">
66 代码执行三个任务,每个钩子一个:66 此列表按照代码中出现的顺序说明每个 hook 的作用:
67 67
68 * 添加 `/hello-tabs` 命令68 * 添加 `/hello-tabs` 命令,并加载早期会话保存的计数
69 * 运行该命令时打开窗格69 * 运行该命令时打开窗格
70 * 绘制窗格的内容:选项卡行和打开的选项卡的主体70 * 绘制窗格的内容:选项卡行和打开的选项卡的主体
71 71
82 let count = 082 let count = 0
83 83
84 export function register(on) {84 export function register(on) {
85 // 在你的第一个提示符之前运行,以及重新加载后再次运行85 // 在您的第一个提示词之前运行,以及重新加载后再次运行
86 on('session.start', async ($, e, next) => {86 on('session.start', async ($, e, next) => {
87 await $.command.register({ name: 'hello-tabs', description: 'Open the hello-tabs pane' })87 await $.command.register({ name: 'hello-tabs', description: 'Open the hello-tabs pane' })
88 // 加载早期会话保存的计数(如果有的话)88 // 加载早期会话保存的计数(如果有的话)
91 return next(e)91 return next(e)
92 })92 })
93 93
94 // 当你键入 /hello-tabs 时运行94 // 当您键入 /hello-tabs 时运行
95 on('command.run', { command: 'hello-tabs' }, async ($) => {95 on('command.run', { command: 'hello-tabs' }, async ($) => {
96 // 打开窗格,给它键盘焦点,让 Esc 关闭它96 // 打开窗格,给它键盘焦点,让 Esc 关闭它
97 await $.ui.open({ id: PANE, title: 'Hello tabs', focus: true, closeOnEscape: true })97 await $.ui.open({ id: PANE, title: 'Hello tabs', focus: true, closeOnEscape: true })
98 // 在记录中不打印任何内容98 // 在会话记录中不打印任何内容
99 return {}99 return {}
100 })100 })
101 101
105 if (e.requestId !== PANE) return next(e)105 if (e.requestId !== PANE) return next(e)
106 // 获取此应用可以绘制的元素106 // 获取此应用可以绘制的元素
107 const { Box, Text, Button } = $.ui.resolve(e)107 const { Box, Text, Button } = $.ui.resolve(e)
108 // 要求 Claude Code 再次运行此钩子108 // 要求 Claude Code 再次运行此 hook
109 const redraw = () => $.ui.invalidate('ui.render')109 const redraw = () => $.ui.invalidate('ui.render')
110 110
111 // 一个选项卡:一个按钮,按下时切换到其选项卡111 // 一个选项卡:一个按钮,按下时切换到其选项卡
165 }165 }
166 ```166 ```
167 167
168 每个钩子也做代码没有明确说明的事情:168 每个 hook 也做代码没有明确说明的事情:
169 169
170 * **[`session.start`](/docs/zh-CN/plugins/mods/reference#session)** 也从 [`$.store`](#keep-state) 读取保存的计数,这是一个在会话之间持久化的键值存储。170 * **[`session.start`](/docs/zh-CN/plugins/mods/reference#session)** 也从 [`$.store`](#keep-state) 读取保存的计数,这是一个在会话之间持久化的键值存储。
171 * **[`command.run`](/docs/zh-CN/plugins/mods/api#add-a-command)** 只告诉 Claude Code 窗格存在。打开窗格本身不绘制任何内容:Claude Code 然后触发 `ui.render` 来询问在其中放入什么。171 * **[`command.run`](/docs/zh-CN/plugins/mods/api#add-a-command)** 只告诉 Claude Code 窗格存在。打开窗格本身不绘制任何内容:Claude Code 然后触发 `ui.render` 来询问在其中放入什么。
172 * **`ui.render`** 返回元素树,一个 `Box`,它保存其他框、文本和按钮,并从 `tab` 和 `count` 每次运行时重新构建它。172 * **`ui.render`** 返回元素树,即一个保存其他框、文本和按钮的 `Box`,并在每次运行时根据 `tab` 和 `count` 重新构建它。
173 173
174 按下按钮会运行其 `onPress` 回调,该回调更改变量并调用 `redraw`。Claude Code 然后再次运行 `ui.render` 钩子,该钩子从新值构建新树。每个交互式绘制都使用该渲染周期:回调更改状态,钩子从新状态重新渲染。174 按下按钮会运行其 `onPress` 回调,该回调更改变量并调用 `redraw`。Claude Code 然后再次运行 `ui.render` hook,该 hook 根据新值构建新树。每个交互式绘制都使用该渲染周期:回调更改状态,hook 根据新状态重新渲染。
175 </Step>175 </Step>
176 176
177 <Step title="打开窗格">177 <Step title="打开窗格">
178 在你的 shell 中,使用 `claude --plugin-dir ./hello-tabs` 启动 Claude Code。在 Claude Code 提示符处,运行 `/hello-tabs`。一个窗格打开,顶部显示 `1: One` 和 `2: Two`。按 `2`,然后按 `a`,**Add one** 的快捷键,几次。计数上升。178 在您的 shell 中,使用 `claude --plugin-dir ./hello-tabs` 启动 Claude Code。在 Claude Code 输入框中,运行 `/hello-tabs`。一个窗格打开,顶部显示 `1: One` 和 `2: Two`。按 `2`,然后按几次 `a`(即 **Add one** 的快捷键)。计数上升。
179 </Step>179 </Step>
180 180
181 <Step title="检查计数是否已保存">181 <Step title="检查计数是否已保存">
182 按 Esc 关闭窗格,然后退出会话。在你的 shell 中,使用相同的 `claude --plugin-dir ./hello-tabs` 命令再次启动 Claude Code,在 Claude Code 提示符处运行 `/hello-tabs`。计数在你离开的地方。182 按 Esc 关闭窗格,然后退出会话。在您的 shell 中,使用相同的 `claude --plugin-dir ./hello-tabs` 命令再次启动 Claude Code,在 Claude Code 输入框中运行 `/hello-tabs`。计数仍停留在您离开时的值。
183 183
184 要清除计数,让 mod 调用 `$.store.delete('count')`。[保持状态](#keep-state) 涵盖每种值持续多长时间。184 要清除计数,让 mod 调用 `$.store.delete('count')`。[保持状态](#keep-state) 涵盖每种值持续多长时间。
185 </Step>185 </Step>
189 选择绘制位置189 选择绘制位置
190</h2>190</h2>
191 191
192`ui.render` 钩子为每个渲染站点运行,除非你将其缩小到你想要绘制的站点。要选择渲染站点,请将称为[匹配器](/docs/zh-CN/plugins/mods/events#filter-which-events-a-hook-handles)的过滤器作为第二个参数传递给 `on`。`{ component: 'Pane' }` 仅为窗格运行钩子。在钩子中,`e.component` 命名站点,`e.surface` 说明哪个应用在绘制,`e.props` 保存站点自己的数据。对于窗格,`e.requestId` 是你用来打开它的 `id`。192`ui.render` hook 会为每个渲染站点运行,除非您将其限定到想要绘制的那个站点。要选择渲染站点,请将一个过滤器(称为[匹配器](/docs/zh-CN/plugins/mods/events#filter-which-events-a-hook-handles))作为第二个参数传递给 `on`。`{ component: 'Pane' }` 仅为窗格运行该 hook。在 hook 中,`e.component` 指明站点名称,`e.surface` 说明哪个应用在绘制,`e.props` 保存站点自己的数据。对于窗格,`e.requestId` 是您打开它时使用的 `id`。
193 193
194两个站点是空的,直到 mod 填充它们,窗格和条带。选择一个选项卡以查看每个是什么以及如何在其中绘制:194窗格和条带在 mod 填充之前都是空的。选择一个选项卡,查看每个站点是什么以及如何在其中绘制:
195 195
196<Tabs>196<Tabs>
197 <Tab title="Pane">197 <Tab title="Pane">
198 窗格是在宽全屏终端中记录旁边的侧边栏,或在其他情况下是提示符上方的框架区域。打开多个窗格时,每个窗格都会获得一个显示其标题的选项卡。198 窗格在宽幅全屏终端中是会话记录旁边的侧边栏,在其他情况下是输入框上方的带边框区域。打开多个窗格时,每个窗格都会获得一个显示其标题的选项卡。
199 199
200 当你的 mod 使用你选择的 `id` 调用 `$.ui.open` 时,窗格出现,如 `$.ui.open({ id: 'hello-tabs' })`。[在正确的时间打开窗格](#open-a-pane-at-the-right-time) 涵盖其他字段以及窗格何时等待更宽的终端。200 当您的 mod 使用您选择的 `id` 调用 `$.ui.open` 时,窗格就会出现,如 `$.ui.open({ id: 'hello-tabs' })`。[在正确的时间打开窗格](#open-a-pane-at-the-right-time) 介绍了其他字段以及窗格何时会等待更宽的终端。
201 201
202 要在你的窗格中绘制,请过滤 `{ component: 'Pane' }` 并检查 `e.requestId` 是否是你的 `id`。202 要在您的窗格中绘制,请过滤 `{ component: 'Pane' }` 并检查 `e.requestId` 是否为您的 `id`。
203 </Tab>203 </Tab>
204 204
205 <Tab title="Band above the prompt">205 <Tab title="Band above the prompt">
206 条带是直接在提示符输入上方的条纹。它始终存在,每个 mod 都共享它。206 条带是紧贴输入框上方的一条区域。它始终存在,并由所有 mod 共享。
207 207
208 你的钩子返回一棵树以在条带中显示某些内容,或返回 `next(e)` 以不显示任何内容。一棵树替换 mod [在你之后](/docs/zh-CN/plugins/mods/events#the-order-mods-run-in) 在那里绘制的内容。要保留他们的,将 `await next(e)` 的结果放在你的树中的 [`Box`](#build-a-tree-from-elements) 的子项中。208 您的 hook 返回一棵树以在条带中显示内容,或返回 `next(e)` 以不显示任何内容。一棵树会替换[在您之后](/docs/zh-CN/plugins/mods/events#the-order-mods-run-in)运行的 mod 在那里绘制的内容。要保留它们的内容,请将 `await next(e)` 的结果放在您树中某个 [`Box`](#build-a-tree-from-elements) 的子项中。
209 209
210 要在条带中绘制,请过滤 `{ component: 'AbovePrompt' }`。210 要在条带中绘制,请过滤 `{ component: 'AbovePrompt' }`。
211 </Tab>211 </Tab>
215 更改 Claude Code 已经绘制的内容215 更改 Claude Code 已经绘制的内容
216</h3>216</h3>
217 217
218Claude Code 自己绘制大部分界面:消息、工具调用行、加载指示器等。这些部分中的每一个也是一个渲染站点,所以 mod 可以重新设置样式或替换它。要更改一个,请在你的 `ui.render` 钩子上过滤此表中的其名称:218Claude Code 自己绘制大部分界面:消息、工具调用行、加载指示器等。这些部分中的每一个也是一个渲染站点,因此 mod 可以重新设置其样式或替换它。要更改其中一个,请让您的 `ui.render` hook 按此表中的名称进行过滤:
219 219
220| 站点 | 它是什么 |220| 站点 | 它是什么 |
221| :- | :- |221| :- | :- |
222| `UserMessage`, `AssistantMessage` | 记录中的消息 |222| `UserMessage`, `AssistantMessage` | 会话记录中的一条消息 |
223| `ToolUse`, `ToolResult`, `ToolGroup` | 工具调用的行、其结果和折叠的调用运行 |223| `ToolUse`, `ToolResult`, `ToolGroup` | 工具调用的行、其结果,以及折叠起来的一组调用 |
224| `CommandOutput` | 命令打印的行 |224| `CommandOutput` | 命令打印的行 |
225| `AskUserQuestion` | Claude 打开的对话框以询问你一个问题 |225| `AskUserQuestion` | Claude 打开以向您提问的对话框 |
226| `Spinner`, `ToolProgress`, `TurnDuration` | 轮次的状态行:在 Claude 工作时动画的行、运行工具的实时进度行以及关闭轮次的行 |226| `Spinner`, `ToolProgress`, `TurnDuration` | 轮次的状态栏:Claude 工作时显示动画的行、正在运行的工具的实时进度行,以及结束轮次的行 |
227| `InfoNotice`, `SessionMode`, `PromptHint` | 徽标下的状态行、页脚中的模式标签以及提示符下的提示行 |227| `InfoNotice`, `SessionMode`, `PromptHint` | 徽标下方的状态栏、页脚中的模式标签,以及输入框下方的提示行 |
228 228
229在 Claude Code 已经绘制的站点,你的钩子有三个选择:更改详细信息、替换绘制或不理它。选择一个选项卡以查看每一个应用于加载指示器。示例读取另一个钩子计数的 `calls` 变量,如[教程 mod](/docs/zh-CN/plugins/mods/create#write-a-mod-yourself) 中所示。229在 Claude Code 已经绘制的站点上,您的 hook 可以更改某个细节、替换绘制内容,或保持不变。选择一个选项卡,查看每种方式应用于加载指示器的效果。这些示例读取由另一个 hook 计数的 `calls` 变量,如[教程 mod](/docs/zh-CN/plugins/mods/create#write-a-mod-yourself) 中所示。
230 230
231<Tabs>231<Tabs>
232 <Tab title="Change a detail">232 <Tab title="Change a detail">
233 要保留 Claude Code 的绘制并更改其一部分,请将 `next` 传递给更改了 `props` 的事件副本。此钩子更改加载指示器单词后的文本:233 要保留 Claude Code 的绘制内容并更改其中一部分,请向 `next` 传递一个更改了 `props` 的事件副本。此 hook 更改加载指示器单词后面的文本:
234 234
235 ```javascript theme={null}235 ```javascript theme={null}
236 on('ui.render', { component: 'Spinner' }, async ($, e, next) => {236 on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
237 // 保留 Claude Code 的加载指示器,并更改其单词后的文本237 // Keep Claude Code's spinner, and change the text after its word
238 return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })238 return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
239 })239 })
240 ```240 ```
241 241
242 加载指示器保留其动画和单词,你的文本跟在单词后面:242 加载指示器保留其动画和单词,您的文本跟在单词后面:
243 243
244 ```text theme={null}244 ```text theme={null}
245 Thinking · tool calls: 2…245 Thinking · tool calls: 2…
247 </Tab>247 </Tab>
248 248
249 <Tab title="Replace the drawing">249 <Tab title="Replace the drawing">
250 要在站点的位置绘制你自己的内容,请返回一棵树,不要调用 `next`。此钩子在加载指示器所在的位置绘制一行文本:250 要在站点的位置绘制您自己的内容,请返回一棵树,并且不要调用 `next`。此 hook 在加载指示器所在的位置绘制一行文本:
251 251
252 ```javascript theme={null}252 ```javascript theme={null}
253 on('ui.render', { component: 'Spinner' }, async ($, e) => {253 on('ui.render', { component: 'Spinner' }, async ($, e) => {
254 const { Text } = $.ui.resolve(e)254 const { Text } = $.ui.resolve(e)
255 // 没有对 next 的调用,所以这一行在加载指示器的位置绘制255 // No call to next, so this line is drawn in the spinner's place
256 return Text({ children: ['Claude has made ' + calls + ' tool calls'] })256 return Text({ children: ['Claude has made ' + calls + ' tool calls'] })
257 })257 })
258 ```258 ```
259 259
260 当 Claude 工作时,你的行显示,Claude Code 的加载指示器不显示:260 当 Claude 工作时,显示的是您的这一行,而不是 Claude Code 的加载指示器:
261 261
262 ```text theme={null}262 ```text theme={null}
263 Claude has made 2 tool calls263 Claude has made 2 tool calls
265 </Tab>265 </Tab>
266 266
267 <Tab title="Leave it alone">267 <Tab title="Leave it alone">
268 要将站点保留为 Claude Code 绘制的方式,请返回 `next(e)`。钩子通常对某些事件这样做,对其他事件不这样做。此钩子在有要计数的调用之前保留加载指示器:268 要让站点保持 Claude Code 绘制的样子,请返回 `next(e)`。hook 通常对某些事件这样做,而对其他事件不这样做。此 hook 在出现可计数的调用之前保持加载指示器不变:
269 269
270 ```javascript theme={null}270 ```javascript theme={null}
271 on('ui.render', { component: 'Spinner' }, async ($, e, next) => {271 on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
272 // 还没有什么要显示的,所以不变地传递事件272 // Nothing to show yet, so pass the event on unchanged
273 if (calls === 0) return next(e)273 if (calls === 0) return next(e)
274 return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })274 return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
275 })275 })
276 ```276 ```
277 277
278 在第一个工具调用之前,加载指示器看起来就像没有 mod 的样子:278 在第一次工具调用之前,加载指示器看起来与没有该 mod 时一样:
279 279
280 ```text theme={null}280 ```text theme={null}
281 Thinking…281 Thinking…
283 </Tab>283 </Tab>
284</Tabs>284</Tabs>
285 285
286权限提示不是渲染站点,所以 mod 无法更改它显示的内容。问题对话框 `AskUserQuestion` 是一个,所以 mod 可以更改它。286在这些站点上,`next(e)` 会返回对 Claude Code 绘制内容的引用 `{ type: 'engine', ref }`,除非在您之后运行的某个 mod 返回了它自己的树。要更改该绘制内容中的内容,请向 `next` 传递一个具有不同 props 的事件副本,就像 **Change a detail** 选项卡所做的那样。您可以原样返回该引用,也可以将其与您自己的元素一起放在一个 `Box` 中:
287 287
288终端和桌面应用不会触发所有相同的站点。`Pane`、`AbovePrompt`、`Spinner` 和记录站点在两者中都有效。其他一些状态行仅在终端中触发。[渲染站点表](/docs/zh-CN/plugins/mods/reference#render-sites) 列出了每个站点在哪里触发。288```javascript theme={null}
289on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
290 const { Box, Text } = $.ui.resolve(e)
291 const theirs = await next(e)
292 return Box({ flexDirection: 'column', children: [theirs, Text({ children: ['under the spinner'] })] })
293})
294```
295
296当 Claude 工作时,加载指示器像以前一样显示动画,而 `under the spinner` 出现在其下方。
297
298权限提示不是渲染站点,因此 mod 无法更改它显示的内容。问题对话框 `AskUserQuestion` 是渲染站点,因此 mod 可以更改它。为该对话框返回的树必须恰好包含一次该引用,并将您的元素放在它的上方。否则,Claude Code 会绘制它自己的对话框。
299
300终端和桌面应用不会触发所有相同的站点。`Pane`、`AbovePrompt`、`Spinner` 和会话记录站点在两者中都有效。其他一些状态栏仅在终端中触发。[渲染站点表](/docs/zh-CN/plugins/mods/reference#render-sites) 列出了每个站点在哪里触发。
289 301
290<h3 id="open-a-pane-at-the-right-time">302<h3 id="open-a-pane-at-the-right-time">
291 在正确的时间打开窗格303 在正确的时间打开窗格
292</h3>304</h3>
293 305
294窗格仅在你的 mod 打开它时出现。你如何以及何时打开它决定了它是否获得键盘焦点、它要求多少空间,以及它是否在狭窄的终端中显示。306窗格仅在您的 mod 打开它时出现。您如何以及何时打开它,决定了它是否获得键盘焦点、它请求多少空间,以及它在狭窄的终端中是否显示。
295 307
296要打开窗格,请使用你选择的 `id` 调用 [`$.ui.open`](/docs/zh-CN/plugins/mods/reference#mods-api-methods)。`id` 是窗格的名称:你的 `ui.render` 钩子检查它,你再次传递它来关闭窗格。308要打开窗格,请使用您选择的 `id` 调用 [`$.ui.open`](/docs/zh-CN/plugins/mods/reference#mods-api-methods)。`id` 是窗格的名称:您的 `ui.render` hook 会检查它,关闭窗格时您也要再次传递它。
297 309
298```javascript theme={null}310```javascript theme={null}
299await $.ui.open({ id: 'hello-tabs', title: 'Hello tabs', focus: true })311await $.ui.open({ id: 'hello-tabs', title: 'Hello tabs', focus: true })
300```312```
301 313
302要关闭窗格,请使用你打开它的 `id` 调用 `$.ui.close`:314要关闭窗格,请使用打开它时所用的 `id` 调用 `$.ui.close`:
303 315
304```javascript theme={null}316```javascript theme={null}
305await $.ui.close({ id: 'hello-tabs' })317await $.ui.close({ id: 'hello-tabs' })
306```318```
307 319
308除了 `id`,`$.ui.open` 还接受这些可选字段:320除了 `id`,`$.ui.open` 还接受以下可选字段:
309 321
310| 字段 | 它做什么 |322| 字段 | 作用 |
311| :- | :- |323| :- | :- |
312| `title` | 打开多个窗格时窗格的选项卡标签 |324| `title` | 打开多个窗格时窗格的选项卡标签 |
313| `focus` | 请求[键盘焦点](#know-which-keys-your-mod-can-receive) |325| `focus` | 请求[键盘焦点](#know-which-keys-your-mod-can-receive) |
314| `closeOnEscape` | 使 Esc 关闭窗格 |326| `closeOnEscape` | 使 Esc 关闭窗格 |
315| `holdToasts` | 保持 toast,来自 [`$.ui.toast`](/docs/zh-CN/plugins/mods/api#show-something-without-starting-a-turn) 的小通知,直到窗格关闭 |327| `holdToasts` | 暂缓显示 toast(来自 [`$.ui.toast`](/docs/zh-CN/plugins/mods/api#show-something-without-starting-a-turn) 的小通知),直到窗格关闭 |
316| `rows` | 当窗格位于提示符上方时要求的高度。默认值是空间的三分之一。 |328| `rows` | 当窗格位于输入框上方时请求的高度。默认值为空间的三分之一。 |
317| `columns` | 当窗格位于记录旁边时要求的宽度 |329| `columns` | 当窗格位于会话记录旁边时请求的宽度 |
318 330
319`focus`、`closeOnEscape` 和 `holdToasts` 是可选的,仅接受 `true`。要省略其中一个,请忽略它。传递 `false` 会抛出错误,例如 `ui.open: focus is true or left out`。要有条件地设置其中一个,仅在条件成立时添加字段。此调用仅在 `items` 不为空时请求键盘焦点:331`focus`、`closeOnEscape` 和 `holdToasts` 是可选的,且仅接受 `true`。要不设置其中某一项,请直接省略。传递 `false` 会抛出错误,例如 `ui.open: focus is true or left out`。要有条件地设置其中某一项,请仅在条件成立时添加该字段。此调用仅在 `items` 不为空时请求键盘焦点:
320 332
321```javascript theme={null}333```javascript theme={null}
322const pane = { id: 'hello-tabs', title: 'Hello tabs' }334const pane = { id: 'hello-tabs', title: 'Hello tabs' }
323await $.ui.open(items.length > 0 ? { ...pane, focus: true } : pane)335await $.ui.open(items.length > 0 ? { ...pane, focus: true } : pane)
324```336```
325 337
326要让命令在 Claude 工作时打开窗格,请在[注册命令](/docs/zh-CN/plugins/mods/api#add-a-command)时添加 `immediate: true`。没有它,在轮次期间键入的命令会等待轮次结束。338要让命令在 Claude 工作时打开窗格,请在[注册命令](/docs/zh-CN/plugins/mods/api#add-a-command)时添加 `immediate: true`。如果不添加,在轮次进行期间输入的命令会等待轮次结束。
327 339
328<h4 id="when-a-pane-waits-for-a-wider-terminal">340<h4 id="when-a-pane-waits-for-a-wider-terminal">
329 当窗格等待更宽的终端时341 当窗格等待更宽的终端时
330</h4>342</h4>
331 343
332你的 mod 打开的窗格而不被要求不会在狭窄的终端中出现,所以它无法接管小屏幕。它是否出现取决于打开它的内容:344您的 mod 在未经用户请求的情况下打开的窗格不会出现在狭窄的终端中,因此它无法占据小屏幕。它是否出现取决于是什么打开了它:
333 345
334* **由用户做的事情打开**,例如他们运行的命令或他们按下的按钮,窗格在任何宽度出现346* **由用户的操作打开**,例如用户运行的命令或按下的按钮,窗格在任何宽度下都会出现
335* **由你的 mod 自己打开**,例如从计时器或 [`turn.start`](/docs/zh-CN/plugins/mods/events#follow-a-turn) 钩子,窗格仅在至少 144 列宽的终端中出现。用户自己打开该窗格一次后,110 列就足够了。347* **由您的 mod 自行打开**,例如从计时器或 [`turn.start`](/docs/zh-CN/plugins/mods/events#follow-a-turn) hook 中打开,窗格仅在至少 144 列宽的终端中出现。用户亲自打开过该窗格一次后,110 列就足够了。
336 348
337当窗格出现时,`$.ui.open` 解析为 `{ isPlaced: true }`。当窗格在等待时,`isPlaced` 是 `false`,`reason` 是一个说明原因的字符串。等待的窗格在用户打开它或拓宽终端时出现。要说某些内容可用而不打开窗格,请调用 `$.ui.toast('Your message')`,它显示一个在几秒后消失的小通知。349当窗格出现时,`$.ui.open` 解析为 `{ isPlaced: true }`。当窗格处于等待状态时,`isPlaced` 为 `false`,`reason` 是一个说明原因的字符串。等待中的窗格会在用户打开它或拓宽终端时出现。要在不打开窗格的情况下告知某些内容可用,请调用 `$.ui.toast('Your message')`,它会显示一条 toast 通知。
338 350
339<h2 id="build-a-tree-from-elements">351<h2 id="build-a-tree-from-elements">
340 从元素构建树352 从元素构建树
341</h2>353</h2>
342 354
343`ui.render` 钩子返回的是一个元素树:对要绘制的内容的描述,由相互嵌套的框、文本和控件组成。你描述绘制,Claude Code 在终端或桌面应用中呈现它。355`ui.render` hook 返回的是一个元素树:对要绘制的内容的描述,由相互嵌套的框、文本和控件组成。您描述绘制,Claude Code 在终端或桌面应用中呈现它。
344 356
345要获取元素,请在你的钩子中调用 `$.ui.resolve(e)`,如 `const { Box, Text, Button } = $.ui.resolve(e)`。每个元素都是一个函数。你传递它属性,你把在其中的元素和字符串放在 `children` 中。357要获取元素,请在您的 hook 中调用 `$.ui.resolve(e)`,如 `const { Box, Text, Button } = $.ui.resolve(e)`。每个元素都是一个函数。您向它传递属性,并把放在其中的元素和字符串放在 `children` 中。
346 358
347大多数绘制使用四个元素。选择一个选项卡以查看每一个以及终端如何绘制它:359选择一个选项卡以查看每个最常用的元素以及终端如何绘制它:
348 360
349<Tabs>361<Tabs>
350 <Tab title="Text">362 <Tab title="Text">
379 </Tab>391 </Tab>
380 392
381 <Tab title="Button">393 <Tab title="Button">
382 `Button` 是用户可以按下的控件。它运行你的 `onPress` 回调。使用 `plain: true` 它没有括号并显示其快捷键:394 `Button` 是用户可以按下的控件。它运行您的 `onPress` 回调。使用 `plain: true` 时,它没有括号并显示其快捷键:
383 395
384 ```javascript theme={null}396 ```javascript theme={null}
385 Button({ key: 'more', label: 'Add one', onPress: addOne })397 Button({ key: 'more', label: 'Add one', onPress: addOne })
393 </Tab>405 </Tab>
394 406
395 <Tab title="Input">407 <Tab title="Input">
396 `Input` 是一个文本字段。当用户按 Enter 时,它使用文本运行你的 `onSubmit` 回调:408 `Input` 是一个文本字段。当用户按 Enter 时,它使用文本运行您的 `onSubmit` 回调:
397 409
398 ```javascript theme={null}410 ```javascript theme={null}
399 Input({411 Input({
407 ```419 ```
408 420
409 ```text theme={null}421 ```text theme={null}
410 Note: Type a note and press Enter ⏎ add422 Note: Type a note and press Enter
411 ```423 ```
412 </Tab>424 </Tab>
413</Tabs>425</Tabs>
414 426
415此表列出了每个元素:427[界面图库](/docs/zh-CN/plugins/mods/gallery)提供了大多数元素的示例和屏幕截图。此表列出了每个元素:
416 428
417| 元素 | 它绘制什么 | 位置 |429| 元素 | 它绘制什么 | 位置 |
418| :- | :- | :- |430| :- | :- | :- |
419| `Box` | 一个 flex 容器。接受布局属性,如 `flexDirection`、`columnGap`、`padding`、`borderStyle` 和 `width`。 | 到处 |431| `Box` | 一个 flex 容器。接受布局属性,如 `flexDirection`、`columnGap`、`padding`、`borderStyle` 和 `width`。 | 到处 |
420| `Text` | 样式化文本。接受 `color`、`bold`、`dimColor`、`italic` 和 `wrap`。`color` 是主题键或颜色,如 `'red'`。`wrap` 是 `'wrap'`、`'truncate'`、`'truncate-start'`、`'truncate-middle'` 或 `'truncate-end'`。 | 到处 |432| `Text` | 样式化文本。接受 `color`、`bold`、`dimColor`、`italic` 和 `wrap`。`color` 是主题键或颜色,如 `'red'`。`wrap` 是 `'wrap'`、`'truncate'`、`'truncate-start'`、`'truncate-middle'` 或 `'truncate-end'`。 | 到处 |
421| `Button` | 调用 `onPress` 的控件 | 到处 |433| `Button` | 调用 `onPress` 的控件 | 到处 |
422| `Link`, `Code`, `Markdown` | 带有 `href` 和可选 `label` 的链接、代码块和格式化为 Claude 回复方式的文本。`Markdown` 在 `text` 属性中而不是在 `children` 中获取其内容,当你传递 `onLinkPress` 时需要 `key`。 | 到处 |434| `Link`, `Code`, `Markdown` | 带有 `href` 和可选 `label` 的链接、代码块和格式化为 Claude 回复方式的文本。`Markdown` 在 `text` 属性中而不是在 `children` 中获取其内容,当您传递 `onLinkPress` 时需要 `key`。 | 到处 |
423| `Input`, `Select` | 文本字段和选择器 | 终端、桌面 |435| `Input`, `Select` | 文本字段和下拉列表 | 终端、桌面 |
424| `Svg` | SVG 文档 | 桌面 |436| `Svg` | SVG 文档 | 桌面 |
425| `Client` | 由你的第二个文件绘制的区域,用于动画和指针输入。该文件没有 mod API。它仅通过发布数据到达你的钩子,该数据作为 `ui.message` 事件到达。 | 终端、桌面 |437| `Client` | 由您的第二个文件绘制的区域,用于动画和指针输入。该文件没有 mods API。它仅通过发布数据到达您的 hook,该数据作为 `ui.message` 事件到达。 | 终端、桌面 |
426| `Raster`, `Image` | [彩色单元格网格](#draw-a-grid-of-colored-cells)和图片 | 终端 |438| `Raster`, `Image` | [彩色单元格网格](#draw-a-grid-of-colored-cells)和图片 | 终端 |
427 439
428如果你的模块是 `.tsx` 或 `.jsx` 文件,你可以将树写成 JSX。首先从 `$.ui.resolve(e)` 解构元素,因为钩子模块没有元素全局。440如果您的模块是 `.tsx` 或 `.jsx` 文件,您可以将树写成 JSX。首先从 `$.ui.resolve(e)` 解构元素。
429 441
430如果树使用应用没有的元素、元素不接受的属性或没有子项的位置,Claude Code 绘制其自己的站点版本。442如果树使用应用没有的元素、元素不接受的属性或在不应有子项的位置放置子项,Claude Code 会绘制其自己的站点版本。
431 443
432在使用 `--plugin-dir` 启动的会话中,记录行说明这一点,例如 `ui.render (Pane) refused: Text prop "bogusProp" is not allowed; the engine drew its own`。[调试日志](/docs/zh-CN/plugins/mods/troubleshoot#read-the-debug-log) 将其记录为 `ui.render (Pane): a hook returned a tree that does not validate` 并带有相同的原因。会话中没有其他内容出现,所以当绘制不显示时,检查该行或日志。444在使用 `--plugin-dir` 启动的会话中,会话记录中会有一行说明这一点,例如 `ui.render (Pane) refused: Text prop "bogusProp" is not allowed; the engine drew its own`。[调试日志](/docs/zh-CN/plugins/mods/troubleshoot#read-the-debug-log) 将其记录为 `ui.render (Pane): a hook returned a tree that does not validate` 并带有相同的原因。会话中没有其他内容出现,所以当绘制不显示时,请检查该行或日志。
433 445
434<h3 id="draw-a-grid-of-colored-cells">446<h3 id="draw-a-grid-of-colored-cells">
435 绘制彩色单元格网格447 绘制彩色单元格网格
436</h3>448</h3>
437 449
438对于热力图、迷你图或终端中的游戏板,绘制一个 `Raster` 而不是每个单元格的 `Box`。`Raster` 接受 `key`、其大小(以 `columns` 和 `rows` 为单位)和 `cells`,它将每个单元格打包到一个字符串中。每个单元格是三个数字:字符的代码点、其颜色和其背景颜色。颜色是十六进制数字,红、绿、蓝各两位,例如 `0xc62828` 表示红色,或 `0x01000000` 表示终端的默认值。450对于热力图、迷你图或终端中的游戏板,绘制一个 `Raster`,而不是为每个单元格绘制一个 `Box`。`Raster` 接受 `key`、其大小(以 `columns` 和 `rows` 为单位)和 `cells`,后者是一个打包了所有单元格的 base64 字符串。每个单元格是三个数字:字符的代码点、其颜色和其背景颜色。颜色是十六进制的 24 位 RGB 值,例如 `0xc62828` 表示红色。值 `0x01000000` 比该范围大一,表示终端的默认值。
439 451
440桌面应用没有 `Raster`,所以检查 `e.surface` 并在那里绘制文本。此窗格主体绘制一个三乘二的热力图:452桌面应用没有 `Raster`,所以请检查 `e.surface` 并在那里绘制文本。此窗格主体绘制一个三乘二的热力图:
441 453
442```javascript theme={null}454```javascript theme={null}
443// 表示"使用终端的默认颜色"的值455// 表示"使用终端的默认颜色"的值
473 485
474<img src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-heat-map.svg?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=b91bcce3bad74bc851149133d4acc5d5" alt="终端中的一个窗格,包含一个小的彩色块网格,两行三列。顶行是绿色、琥珀色和红色。底行是绿色、绿色和琥珀色。" width="360" height="132" data-path="images/mods-heat-map.svg" />486<img src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-heat-map.svg?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=b91bcce3bad74bc851149133d4acc5d5" alt="终端中的一个窗格,包含一个小的彩色块网格,两行三列。顶行是绿色、琥珀色和红色。底行是绿色、绿色和琥珀色。" width="360" height="132" data-path="images/mods-heat-map.svg" />
475 487
476`rows` 数组是你要更改的部分,`cellsOf` 将其转换为打包的字符串。钩子仅在 `id` 为 `heat` 的窗格中绘制,所以从命令中使用 `$.ui.open({ id: 'heat' })` 打开一个,如 [`hello-tabs` 示例](#build-a-pane-with-tabs) 打开其窗格。488`rows` 数组是您要更改的部分,`cellsOf` 将其转换为打包的字符串。hook 仅在 `id` 为 `heat` 的窗格中绘制,所以请从命令中使用 `$.ui.open({ id: 'heat' })` 打开一个,如 [`hello-tabs` 示例](#build-a-pane-with-tabs) 打开其窗格。
477 489
478每个字符必须是一个单元格宽。要动画化已经在屏幕上的 `Raster`,请使用窗格的 `id` 作为 `requestId`、`Raster` 的 `key`、相同的大小和新单元格调用 `$.ui.blit`。对于此示例,这是 `$.ui.blit({ requestId: 'heat', key: 'grid', columns: 3, rows: 2, cells: cellsOf(newRows) })`。它重新绘制该一个元素而不再次运行你的 `ui.render` 钩子。490每个字符必须是一个单元格宽。要动画化已经在屏幕上的 `Raster`,请使用窗格的 `id` 作为 `requestId`、`Raster` 的 `key`、相同的大小和新单元格调用 `$.ui.blit`。对于此示例,这是 `$.ui.blit({ requestId: 'heat', key: 'grid', columns: 3, rows: 2, cells: cellsOf(newRows) })`。它重新绘制该一个元素,而不再次运行您的 `ui.render` hook。
479 491
480<h2 id="respond-to-presses-and-typing">492<h2 id="respond-to-presses-and-typing">
481 响应按键和输入493 响应按键和输入
482</h2>494</h2>
483 495
484当用户按下你绘制的按钮、输入字段或从列表中选择时,Claude Code 调用你给该控件的函数,它在你的模块中运行。每个控件接受其自己的回调:496当用户按下您的 mod 绘制的按钮、在字段中输入或从列表中选择时,Claude Code 调用该控件的回调,它在您的模块中运行。每个控件接受其自己的回调:
485 497
486* **`Button`**:接受 `onPress(e)`,其中 `e.surface` 是按键来自的应用498* **`Button`**:接受 `onPress(e)`,其中 `e.surface` 是按键来自的应用
487* **`Input`**:接受 `onSubmit(value)` 和 `onInput(value)`499* **`Input`**:接受 `onSubmit(value)` 和 `onInput(value)`
488* **`Select`**:接受 `onSelect(value)`,其选择在 `options` 中,至少一个选择的列表,具有唯一值,例如 `[{ value: 'sm', label: 'Small' }, { value: 'lg', label: 'Large' }]`500* **`Select`**:接受 `onSelect(value)`,其选项在 `options` 中,这是一个至少包含一个选项且值唯一的列表,例如 `[{ value: 'sm', label: 'Small' }, { value: 'lg', label: 'Large' }]`
489 501
490测试通过其 `key` 按下或输入到控件中,所以给每个控件一个。控件的每次使用也会触发 [`ui.press`、`ui.input` 或 `ui.select`](/docs/zh-CN/plugins/mods/reference#interface),其中 `key` 在 `e.element` 中,另一个 mod 可以钩住这些事件。其钩子在你的回调之前运行,所以它看到用户输入到你的 `Input` 中的内容,可以更改它或代替你的回调回答。mod API 没有按下另一个 mod 的按钮的方法。502测试通过控件的 `key` 按下控件或向其输入,因此请为每个控件指定一个。控件的每次使用也会触发 [`ui.press`、`ui.input` 或 `ui.select`](/docs/zh-CN/plugins/mods/reference#interface),其中 `key` 在 `e.element` 中,另一个 mod 可以处理这些事件。其 hook 在您的回调之前运行,因此它能看到用户输入到您的 `Input` 中的内容,可以更改它或代替您的回调作出响应。mod API 没有按下另一个 mod 的按钮的方法。
491 503
492<h3 id="know-which-keys-your-mod-can-receive">504<h3 id="know-which-keys-your-mod-can-receive">
493 键盘焦点和快捷键505 键盘焦点和快捷键
494</h3>506</h3>
495 507
496你的 mod 永远不会自己读取键盘。用户按下一个键,Claude Code 决定它是为你的哪个控件,该控件的回调运行。除了[条带上的数字快捷键](/docs/zh-CN/plugins/mods/reference#elements),这仅在你的窗格或条带有键盘焦点时发生。其余时间,键进入提示符。508您的 mod 永远不会自己读取键盘。用户按下一个键,Claude Code 决定它属于您的哪个控件,然后该控件的回调运行。除了[条带上的数字快捷键](/docs/zh-CN/plugins/mods/reference#elements),这仅在您的窗格或条带拥有键盘焦点时发生。其余时间,按键进入输入框。
497 509
498<h4 id="how-a-pane-gets-keyboard-focus">510<h4 id="how-a-pane-gets-keyboard-focus">
499 窗格如何获得键盘焦点511 窗格如何获得键盘焦点
500</h4>512</h4>
501 513
502窗格通过以下三种方式之一获得键盘焦点:514窗格在以下情况下获得键盘焦点:
503 515
504* 你的 mod 从命令或按键使用 `focus: true` 打开它516* 您的 mod 从命令或按键使用 `focus: true` 打开它
505* 用户按 Ctrl+X 然后 Tab517* 用户按 Ctrl+X 然后按 Tab
506* 用户点击它518* 用户点击它
507 519
508Claude Code 仅在提示符为空且没有其他内容有键盘焦点时授予 `focus: true`。在用户输入时打开的窗格不会获取他们的按键。520Claude Code 仅在输入框为空且没有其他内容拥有键盘焦点时授予 `focus: true`。在用户输入时打开的窗格不会获取其按键。
509 521
510<h4 id="what-each-key-does">522<h4 id="what-each-key-does">
511 每个键做什么523 每个键做什么
512</h4>524</h4>
513 525
514此表列出了当你的窗格或条带有键盘焦点时每个键做什么:526此表列出了当您的窗格或条带拥有键盘焦点时每个键做什么:
515 527
516| 键 | 它做什么 |528| 键 | 它做什么 |
517| :- | :- |529| :- | :- |
518| Tab | 移动到下一个控件 |530| Tab | 移动到下一个控件 |
519| 上和下 | 在你的绘制适合时在控件之间移动。当窗格或条带的行数超过它可以显示的行数时,它们会滚动它。 |531| 上和下 | 在绘制内容能完整显示时在控件之间移动。当窗格或条带的行数超过它可以显示的行数时,它们会滚动它。 |
520| Enter | 按下焦点 `Button`、提交焦点 `Input` 或在 `Select` 中选择 |532| Enter | 按下获得焦点的 `Button`、提交获得焦点的 `Input` 或在 `Select` 中选择 |
521| 按钮的快捷键 | 按下该按钮。当 `Input` 有焦点时,每个可打印键都进入字段。 |533| 按钮的快捷键 | 按下该按钮。当 `Input` 拥有焦点时,每个可打印键都进入字段。 |
522| Esc | 将键盘焦点返回到提示符。使用 `closeOnEscape: true`,它也关闭窗格。 |534| Esc | 将键盘焦点返回到输入框。使用 `closeOnEscape: true` 时,它还会关闭窗格。 |
523 535
524mod 无法将 Tab 或箭头键绑定到其他任何东西,所以游戏用 `w`、`a`、`s` 和 `d` 操舵。536mod 无法将 Tab 或方向键绑定到其他任何功能,因此游戏使用 `w`、`a`、`s` 和 `d` 控制方向。
525 537
526<h4 id="set-a-hotkey-and-the-first-focus">538<h4 id="set-a-hotkey-and-the-first-focus">
527 设置快捷键和第一个焦点539 设置快捷键和初始焦点
528</h4>540</h4>
529 541
530控件上的两个属性决定了键盘如何到达它:542控件上的以下属性决定了键盘如何到达它:
531 543
532* **`hotkey`**:要让用户用一个键按下 `Button`,给它一个 `hotkey`,一个数字或一个小写字母,如 `hotkey: 'a'`544* **`hotkey`**:要让用户用一个键按下 `Button`,请为它指定一个 `hotkey`,值为一个数字或一个小写字母,如 `hotkey: 'a'`
533* **`autoFocus`**:要选择窗格打开时哪个控件有焦点,向它添加 `autoFocus: true`。在其他上省略属性,因为 Claude Code 拒绝 `autoFocus: false`。545* **`autoFocus`**:要选择窗格打开时哪个控件拥有焦点,请向它添加 `autoFocus: true`。该属性只接受 `true`,因此请在其他控件上省略它。
534 546
535快捷键的显示方式取决于按钮和应用:547快捷键的显示方式取决于按钮和应用:
536 548
537| 按钮 | 在终端中 | 在桌面应用中 |549| 按钮 | 在终端中 | 在桌面应用中 |
538| :- | :- | :- |550| :- | :- | :- |
539| 带括号,默认 | `[ Add one ]`,没有显示快捷键 | 标签,旁边有一个小键 |551| 带括号(默认) | `[ Add one ]`,不显示快捷键 | 标签,旁边有一个小键 |
540| 使用 `plain: true` | `1: One` | 标签,旁边有一个小键 |552| 使用 `plain: true` | `1: One` | 标签,旁边有一个小键 |
541 553
542在终端中,在括号按钮的标签中命名键,或使用 `plain: true`,所以用户可以看到要按什么。[元素参考](/docs/zh-CN/plugins/mods/reference#elements) 有其他 `Button` 规则:`action`、条带上的数字快捷键和一个快捷键上的两个按钮。554在终端中,请在带括号按钮的标签中写明按键,或使用 `plain: true`,以便用户知道要按什么。[元素参考](/docs/zh-CN/plugins/mods/reference#elements)包含其他 `Button` 规则:`action`、条带上的数字快捷键,以及同一快捷键上的两个按钮。
543 555
544<h3 id="take-typed-input-and-draw-a-row-for-each-item">556<h3 id="take-typed-input-and-draw-a-row-for-each-item">
545 获取输入的文本并为每个项目绘制一行557 获取输入的文本并为每个项目绘制一行
546</h3>558</h3>
547 559
548许多窗格是一个文本字段,下面有一个列表。本部分中的示例是一个笔记窗格:你输入一个笔记并按 Enter 添加它,每个笔记都有一个删除它的 `x` 按钮。添加两个笔记后,终端这样绘制窗格:560许多窗格是一个文本字段,下面有一个列表。本部分中的示例是一个笔记窗格:输入一条笔记并按 Enter 添加它,每条笔记都有一个用于删除它的 `x` 按钮。添加两条笔记后,终端这样绘制窗格:
549 561
550```text theme={null}562```text theme={null}
551╭──────────────────────────────────────────────────────────╮563╭──────────────────────────────────────────────────────────╮
555╰──────────────────────────────────────────────────────────╯567╰──────────────────────────────────────────────────────────╯
556```568```
557 569
558示例使用两种技术:570示例使用以下技术:
559 571
560* **获取输入的文本**:当用户按 Enter 时,`Input` 使用字段的文本调用 `onSubmit(value)`,在每次更改时调用 `onInput(value)`572* **获取输入的文本**:当用户按 Enter 时,`Input` 使用字段的文本调用 `onSubmit(value)`,并在每次更改时调用 `onInput(value)`
561* **绘制列表**:将你的数据映射到每个一行,并给每行的按钮其自己的 `key`573* **绘制列表**:将您的数据映射为每项一行,并为每行的按钮指定其自己的 `key`
562 574
563此钩子绘制窗格的内容:575此 hook 绘制窗格的内容:
564 576
565```javascript theme={null}577```javascript theme={null}
566// 窗格绘制的列表578// 窗格绘制的列表
579 key: 'new-note',591 key: 'new-note',
580 label: 'Note',592 label: 'Note',
581 placeholder: 'Type a note and press Enter',593 placeholder: 'Type a note and press Enter',
582 // 每次绘制字段为空,这在提交后清除它594 // 每次都将字段绘制为空,这会在提交后清空它
583 value: '',595 value: '',
584 submitLabel: 'add',596 submitLabel: 'add',
585 autoFocus: true,597 autoFocus: true,
586 // 当你在字段中按 Enter 时运行598 // 在字段中按 Enter 时运行
587 onSubmit: async (value) => {599 onSubmit: async (value) => {
588 // 忽略空行600 // 忽略空行
589 if (!value.trim()) return601 if (!value.trim()) return
592 await $.store.set('notes', notes)604 await $.store.set('notes', notes)
593 },605 },
594 }),606 }),
595 // 每个笔记一行:一个删除按钮,然后是笔记的文本607 // 每条笔记一行:一个删除按钮,然后是笔记的文本
596 ...notes.map((note, i) =>608 ...notes.map((note, i) =>
597 Box({609 Box({
598 flexDirection: 'row',610 flexDirection: 'row',
599 columnGap: 1,611 columnGap: 1,
600 children: [612 children: [
601 Button({613 Button({
602 // 它自己的键,所以每行的按钮可以区分614 // 独有的键,以便区分每行的按钮
603 key: 'delete-' + i,615 key: 'delete-' + i,
604 label: 'x',616 label: 'x',
605 plain: true,617 plain: true,
618})630})
619```631```
620 632
621要尝试窗格:633要试用该窗格:
622 634
623* **添加笔记**:输入一行并按 Enter。该行显示为新行,字段清空。635* **添加笔记**:输入一行并按 Enter。该行显示为新行,字段清空。
624* **删除笔记**:按 Tab 直到笔记的 `x` 按钮有焦点,然后按 Enter。`x` 是按钮的标签,不是快捷键,所以输入字母不会按下它。636* **删除笔记**:按 Tab 直到笔记的 `x` 按钮获得焦点,然后按 Enter。`x` 是按钮的标签,不是快捷键,因此输入该字母不会按下它。
625 637
626每个更改遵循与 `hello-tabs` 相同的渲染周期:回调更改 `notes`,调用 `redraw`,并将列表保存到 `$.store`。638每次更改都遵循与 `hello-tabs` 相同的渲染周期:回调更改 `notes`,调用 `redraw`,并将列表保存到 `$.store`。
627 639
628字段在每次提交后清空,因为其 `value` 属性。`value` 是绘制字段时保存的文本,用户的输入替换它,直到你的钩子再次绘制字段。示例总是用 `''` 绘制字段。640字段在每次提交后清空,是因为其 `value` 属性。`value` 是绘制字段时字段中的文本,用户的输入会替换它,直到您的 hook 再次绘制字段。示例总是用 `''` 绘制字段。
629 641
630示例保存笔记而不加载它们。要在下一个会话中将它们带回,请在 `session.start` 钩子中读取它们,就像 `hello-tabs` 读取 `count` 的方式一样。642示例保存笔记但不加载它们。要在下一个会话中恢复它们,请在 `session.start` hook 中读取它们,就像 `hello-tabs` 读取 `count` 的方式一样。
631 643
632三个属性组成字段的行,`Note: Type a note and press Enter ⏎ add`:644以下属性组成字段的这一行,`Note: Type a note and press Enter ⏎ add`:
633 645
634| 属性 | 在示例中 | 它是什么 |646| 属性 | 在示例中 | 它是什么 |
635| :- | :- | :- |647| :- | :- | :- |
636| `label` | `Note` | 字段前的文本。终端在其后绘制 `: `。 |648| `label` | `Note` | 字段前的文本。终端在其后绘制 `: `。 |
637| `placeholder` | `Type a note and press Enter` | 当字段为空时显示的暗文本 |649| `placeholder` | `Type a note and press Enter` | 当字段为空时显示的暗色文本 |
638| `submitLabel` | `add` | `⏎` 后的单词,说明 Enter 做什么 |650| `submitLabel` | `add` | `⏎` 后的单词,说明 Enter 做什么 |
639 651
640提交 `Input` 不会启动轮次,除非你的回调调用 [`$.prompt.submit`](/docs/zh-CN/plugins/mods/api#start-a-turn-from-a-background-job)。652提交 `Input` 不会启动轮次,除非您的回调调用 [`$.prompt.submit`](/docs/zh-CN/plugins/mods/api#start-a-turn-from-a-background-job)。
641 653
642<h2 id="redraw-when-something-changes">654<h2 id="redraw-when-something-changes">
643 重绘站点655 重绘站点
644</h2>656</h2>
645 657
646绘制是一个快照:它显示你的 `ui.render` 钩子上次运行时返回的内容。要显示新内容,钩子必须再次运行。Claude Code 为某些更改再次运行它,你的 mod 要求其余的。658绘制是一个快照:它显示您的 `ui.render` hook 上次运行时返回的内容。要显示新内容,hook 必须再次运行。Claude Code 会针对某些更改再次运行它,其余情况则由您的 mod 发起请求。
647 659
648<h3 id="when-claude-code-redraws-without-being-asked">660<h3 id="when-claude-code-redraws-without-being-asked">
649 当 Claude Code 在不被要求时重绘661 当 Claude Code 在不被要求时重绘
650</h3>662</h3>
651 663
652当站点的属性更改或终端的宽度更改时,Claude Code 再次运行你的 `ui.render` 钩子。它不在计时器上运行钩子,也无法判断你的模块中的变量何时更改。664当站点的 prop 更改或终端的宽度更改时,Claude Code 会再次运行您的 `ui.render` hook。它不会按计时器运行 hook,也无法判断您的模块中的变量何时更改。
653 665
654<h3 id="redraw-when-your-data-changes">666<h3 id="redraw-when-your-data-changes">
655 当你的数据更改时重绘667 当您的数据更改时重绘
656</h3>668</h3>
657 669
658要在你自己的数据更改后再次绘制你的站点,请调用 `$.ui.invalidate('ui.render')`。此窗格计数按键。按钮的回调更改 `count`,然后要求重绘:670要在您自己的数据更改后再次绘制您的站点,请调用 `$.ui.invalidate('ui.render')`。此窗格统计按键次数。按钮的回调更改 `count`,然后请求重绘:
659 671
660```javascript theme={null}672```javascript theme={null}
661let count = 0673let count = 0
682})694})
683```695```
684 696
685每次按键都会提高窗格中的数字。[`hello-tabs` 示例](#build-a-pane-with-tabs) 将相同的调用包装在其 `redraw` 函数中。697每次按键都会使窗格中的数字增加。[`hello-tabs` 示例](#build-a-pane-with-tabs) 将相同的调用包装在其 `redraw` 函数中。
686 698
687你在 [`$.state`](#keep-a-value-in-\$-state) 中保存的值不需要调用,因为写入值会重绘读取它的站点。699您在 [`$.state`](#keep-a-value-in-\$-state) 中保存的值不需要此调用,因为写入值会重绘读取它的站点。
688 700
689<h3 id="redraw-on-a-timer">701<h3 id="redraw-on-a-timer">
690 在计时器上重绘702 按计时器重绘
691</h3>703</h3>
692 704
693要保持时钟、倒计时或来自会话外部的值最新,请按计划重绘。在模块的 `session.start` 钩子中启动计时器。如果模块已经有一个,如 `hello-tabs` 所做的,请将 [`$.clock.every`](/docs/zh-CN/plugins/mods/api#run-work-in-the-background) 行添加到它:705要使时钟、倒计时或来自会话外部的值保持最新,请按计划重绘。在模块的 `session.start` hook 中启动计时器。如果模块已经有一个该 hook(如 `hello-tabs`),请将 [`$.clock.every`](/docs/zh-CN/plugins/mods/api#run-work-in-the-background) 这一行添加到其中:
694 706
695```javascript theme={null}707```javascript theme={null}
696on('session.start', async ($, e, next) => {708on('session.start', async ($, e, next) => {
697 // 每 1000 毫秒,要求 Claude Code 再次绘制你的站点709 // 每 1000 毫秒,要求 Claude Code 再次绘制您的站点
698 $.clock.every(1000, () => $.ui.invalidate('ui.render'))710 $.clock.every(1000, () => $.ui.invalidate('ui.render'))
699 return next(e)711 return next(e)
700})712})
701```713```
702 714
703Claude Code 现在每秒运行你的 `ui.render` 钩子一次。当模块重新加载时计时器停止,新副本启动其自己的。715Claude Code 现在每秒运行您的 `ui.render` hook 一次。当模块重新加载时计时器停止,模块的新实例会启动其自己的计时器。
704 716
705<h3 id="how-often-a-site-can-redraw">717<h3 id="how-often-a-site-can-redraw">
706 站点可以重绘的频率718 站点可以重绘的频率
707</h3>719</h3>
708 720
709Claude Code 限制重绘的频率,所以你的 mod 可以在其数据更改时调用 `$.ui.invalidate`。可见窗格和条带的限制比其他站点更高,[限制表](/docs/zh-CN/plugins/mods/reference#limits) 中有具体数字。721Claude Code 会限制站点的重绘频率,因此您的 mod 可以在数据每次更改时调用 `$.ui.invalidate`。有关每个站点可以重绘的频率,请参阅[限制表](/docs/zh-CN/plugins/mods/reference#limits)。
710 722
711比限制更快的调用被合并为一次重绘。该重绘运行你的钩子一次,钩子读取你的数据,因为它在那一刻的样子,所以最新值显示,中间的值不显示。动画无法比限制运行得更快。723快于该限制的调用会被合并为一次重绘。该重绘只运行您的 hook 一次,hook 读取的是您的数据在那一刻的状态,因此显示的是最新值,而中间的值不会显示。动画的运行速度无法超过该限制。
712 724
713<h2 id="keep-state">725<h2 id="keep-state">
714 保持状态726 保持状态
715</h2>727</h2>
716 728
717mod 有三个地方可以保存值,它们在值持续多长时间方面有所不同:直到模块重新加载、直到会话结束或从一个会话到下一个会话。根据值必须持续多长时间选择:729mod 将值保存在何处,决定了该值持续多长时间:直到模块重新加载、直到会话结束,或从一个会话持续到下一个会话。根据值需要持续的时长进行选择:
718 730
719| 在其中保存 | 它持续到 | 用于 |731| 在其中保存 | 它持续到 | 用于 |
720| :- | :- | :- |732| :- | :- | :- |
721| 模块级变量 | 模块重新加载,这在开发期间每次保存文件时发生 | 你可以丢失的值,如 `hello-tabs` 中的 `tab` |733| 模块级变量 | 模块重新加载,这在开发期间每次保存文件时发生 | 您可以丢失的值,如 `hello-tabs` 中的 `tab` |
722| `$.state` | 会话结束,或用户运行 `/clear`、`/resume` 或 `/branch` | 绘制依赖的值,应该在重新加载后存活 |734| `$.state` | 会话结束,或用户运行 `/clear`、`/resume` 或 `/branch` | 绘制依赖的值,应该在重新加载后存活 |
723| `$.store` | 你的 mod 删除它,或没有会话在 [`cleanupPeriodDays`](/docs/zh-CN/settings-reference#cleanupperioddays) 内读取或写入存储。存储是一个键值存储,保存为你的插件自己的 JSON 文件,位于 `~/.claude/plugins/store/` 下。 | 设置、历史记录、用户期望下次找到的任何内容 |735| `$.store` | 您的 mod 删除它,或没有会话在 [`cleanupPeriodDays`](/docs/zh-CN/settings-reference#cleanupperioddays) 内读取或写入存储。存储是一个键值存储,保存为您的插件自己的 JSON 文件,位于 `~/.claude/plugins/store/` 下。 | 设置、历史记录、用户期望下次找到的任何内容 |
724 736
725`$.store.get(key)` 解析为值或 `undefined`,`$.store.set(key, value)` 接受任何 JSON 值。737`$.store.get(key)` 解析为值或 `undefined`,`$.store.set(key, value)` 接受任何 JSON 值。
726 738
727<h3 id="keep-a-value-in-state">739<h3 id="keep-a-value-in-$-state">
728 在 `$.state` 中保存值740 在 `$.state` 中保存值
729</h3>741</h3>
730 742
731`$.state` 为会话的长度保存值,并为你重绘。它是反应式状态:读取值的 `ui.render` 钩子订阅它,所以 Claude Code 每次你写入值时重绘该站点,你不调用 `$.ui.invalidate`。`$.state` 中的值也在模块重新加载后存活,变量不会。743`$.state` 在会话期间保存值,并为您重绘。它是反应式状态:读取值的 `ui.render` hook 会订阅该值,因此每次您写入该值时,Claude Code 都会重绘该站点,您无需调用 `$.ui.invalidate`。`$.state` 中的值也会在模块重新加载后存活,而变量不会。
732 744
733要设置它,声明你的值,将你的清单指向声明,然后定义和使用每个值。示例将 `count` 从 `hello-tabs` 移到 `$.state`。745要设置它,请声明您的值,将您的清单指向该声明,然后定义和使用每个值。示例将 `count` 从 `hello-tabs` 移到 `$.state`。
734 746
735<h4 id="declare-the-values">747<h4 id="declare-the-values">
736 声明值748 声明值
737</h4>749</h4>
738 750
739在类型文件中声明值。外键是你的插件的名称,其下的每个条目是一个值及其类型。将其保存为 `hello-tabs/types/index.d.ts`:751在类型声明文件中声明值。外层键是您的插件的名称,其下的每个条目是一个值及其类型。将其保存为 `hello-tabs/types/index.d.ts`:
740 752
741```typescript hello-tabs/types/index.d.ts theme={null}753```typescript hello-tabs/types/index.d.ts theme={null}
742declare module 'claude-code' {754declare module 'claude-code' {
753 将清单指向声明765 将清单指向声明
754</h4>766</h4>
755 767
756要让 `claude plugin validate` 根据该文件检查你的代码,请向清单添加 `types` 字段及其路径:768要让 `claude plugin validate` 根据该文件检查您的代码,请向清单添加 `types` 字段及其路径:
757 769
758```json hello-tabs/.claude-plugin/plugin.json theme={null}770```json hello-tabs/.claude-plugin/plugin.json theme={null}
759{771{
769 定义、读取和写入值781 定义、读取和写入值
770</h4>782</h4>
771 783
772在你的模块中,定义每个值及其默认值,在绘制时读取它,并从回调中写入它。`atom` 命名值及其默认值,`read` 返回它,`update` 写入它。三个帮助程序为你调用 `$.state.get` 和 `$.state.set`:784在您的模块中,定义每个值及其默认值,在绘制时读取它,并从回调中写入它。`atom` 命名值及其默认值,`read` 返回它,`update` 写入它。这三个帮助程序会为您调用 `$.state.get` 和 `$.state.set`:
773 785
774```javascript theme={null}786```javascript theme={null}
775import { atom, read, update } from 'claude-code'787import { atom, read, update } from 'claude-code'
777// 在模块顶部:命名值并给出其默认值789// 在模块顶部:命名值并给出其默认值
778const count = atom({ plugin: 'hello-tabs', key: 'count' }, 0)790const count = atom({ plugin: 'hello-tabs', key: 'count' }, 0)
779 791
780// 在 ui.render 钩子中:读取值以绘制它792// 在 ui.render hook 中:读取值以绘制它
781const n = await read($, count)793const n = await read($, count)
782 794
783// 在按钮中:从旧值写入新值795// 在按钮中:从旧值写入新值
784onPress: () => update($, count, (value) => value + 1)796onPress: () => update($, count, (value) => value + 1)
785```797```
786 798
787因为 `ui.render` 钩子读取了 `count`,Claude Code 每次按钮写入它时再次运行钩子。799因为 `ui.render` hook 读取了 `count`,所以每次按钮写入它时,Claude Code 都会再次运行该 hook。
788 800
789三个规则适用于代码:801以下规则适用于代码:
790 802
791* **将 `plugin` 和 `key` 写成字面字符串**:`claude plugin validate` 从你的源代码中读取它们803* **将 `plugin` 和 `key` 写成字面字符串**:`claude plugin validate` 从您的源代码中读取它们
792* **在类型文件中声明每个值**:否则验证失败,出现 `hello-tabs.count is not declared`804* **在类型声明文件中声明每个值**:否则验证失败,出现 `hello-tabs.count is not declared`
793* **从回调或另一个事件的钩子中写入**:`ui.render` 钩子可以读取状态,不能写入它,所以从 `onPress`、`onSubmit` 或另一个事件的钩子中写入805* **从回调或另一个事件的 hook 中写入**:`ui.render` hook 可以读取状态,但不能写入它,因此请从 `onPress`、`onSubmit` 或另一个事件的 hook 中写入
794 806
795<h4 id="change-hello-tabs-to-use-state">807<h4 id="change-hello-tabs-to-use-$-state">
796 更改 `hello-tabs` 以使用 `$.state`808 更改 `hello-tabs` 以使用 `$.state`
797</h4>809</h4>
798 810
799要将 `hello-tabs` 中的 `count` 移到 `$.state`,请更改使用它的每一行:811要将 `hello-tabs` 中的 `count` 移到 `$.state`,请更改使用它的每一行:
800 812
801* **在模块顶部**:添加 `import` 行,并用 `atom` 行替换 `let count = 0`813* **在模块顶部**:添加 `import` 行,并用 `atom` 行替换 `let count = 0`
802* **在 `ui.render` 钩子中**:在 `tabButton` 之前添加 `read` 行,并在 `Text` 中绘制 `'Count: ' + n`814* **在 `ui.render` hook 中**:在 `tabButton` 之前添加 `read` 行,并在 `Text` 中绘制 `'Count: ' + n`
803* **在 Add one 按钮中**:用[从多个会话保存](#save-from-more-than-one-session)中的按钮替换 `onPress`,它保存计数以及写入它815* **在 Add one 按钮中**:用[从多个会话保存](#save-from-more-than-one-session)中的 `onPress` 替换 `onPress`,它在写入计数的同时也会保存计数
804* **在 `session.start` 钩子中**:用[在 `/clear` 后再次加载保存的值](#load-a-saved-value-again-after-clear)中的 `loadCount` 调用替换读取 `saved` 的两行816* **在 `session.start` hook 中**:用[在 `/clear` 后再次加载保存的值](#load-a-saved-value-again-after-clear)中的 `loadCount` 调用替换读取 `saved` 的两行
805 817
806为选项卡按钮保留 `redraw`,因为 `tab` 仍然是一个变量。818为选项卡按钮保留 `redraw`,因为 `tab` 仍然是一个变量。
807 819
809 在 `/clear` 后再次加载保存的值821 在 `/clear` 后再次加载保存的值
810</h3>822</h3>
811 823
812如果你的 mod 在 `session.start` 时将保存的值从 `$.store` 复制到 `$.state`,它必须在 `/clear`、`/resume` 或 `/branch` 后再次复制。这些命令将每个 `$.state` 值放回其默认值,`session.start` 不再触发。[`classic.SessionStart`](/docs/zh-CN/plugins/mods/events#hook-the-settings-hook-events) 在每个之后触发,`e.source` 设置为 `clear`、`resume` 或 `fork`,所以在其上的钩子中再次复制值。否则你的绘制显示默认值,保存 `$.state` 值的回调将默认值写入你存储的内容。824如果您的 mod 在 `session.start` 时将保存的值从 `$.store` 复制到 `$.state`,它必须在 `/clear`、`/resume` 或 `/branch` 后再次复制。这些命令会将每个 `$.state` 值重置为其默认值,而 `session.start` 不会再次触发。[`classic.SessionStart`](/docs/zh-CN/plugins/mods/events#hook-the-settings-hook-events) 确实会在每个命令之后触发,`e.source` 设置为 `clear`、`resume` 或 `fork`,因此请在其上的 hook 中再次复制值。否则您的绘制会显示默认值,而保存 `$.state` 值的回调会用默认值覆盖您存储的内容。
813 825
814此代码从两个钩子加载 `count`。它基于 `hello-tabs` 的 `$.state` 版本,其中 `count` 是原子,`update` 被导入。将 `loadCount` 放在 `register` 上方,并将 `loadCount` 调用添加到你已经拥有的 `session.start` 钩子。`classic.SessionStart` 也在启动和压缩后触发,这不会重置 `$.state`,所以对 `source` 的过滤将钩子保留到三个重置:826此代码从两个 hook 加载 `count`。它基于 `hello-tabs` 的 `$.state` 版本,其中 `count` 是原子,`update` 已被导入。将 `loadCount` 放在 `register` 上方,并将 `loadCount` 调用添加到您已有的 `session.start` hook。`classic.SessionStart` 也会在启动时和压缩后触发,而这些不会重置 `$.state`,因此对 `source` 的过滤将该 hook 限定于这三种重置:
815 827
816```javascript theme={null}828```javascript theme={null}
817// 将保存的计数从 $.store 复制到 $.state,如果没有保存任何内容则为 0829// 将保存的计数从 $.store 复制到 $.state,如果没有保存任何内容则为 0
820 await update($, count, () => saved)832 await update($, count, () => saved)
821}833}
822 834
823// 在你的第一个提示符之前运行,以及重新加载后再次运行835// 在您的第一个提示词之前运行,以及重新加载后再次运行
824on('session.start', async ($, e, next) => {836on('session.start', async ($, e, next) => {
825 await loadCount($)837 await loadCount($)
826 return next(e)838 return next(e)
827})839})
828 840
829// 在 /clear、/resume 和 /branch 后再次运行,报告 fork841// 在 /clear、/resume 和 /branch 后再次运行,/branch 报告为 fork
830on('classic.SessionStart', { source: ['clear', 'resume', 'fork'] }, async ($, e, next) => {842on('classic.SessionStart', { source: ['clear', 'resume', 'fork'] }, async ($, e, next) => {
831 await loadCount($)843 await loadCount($)
832 return next(e)844 return next(e)
833})845})
834```846```
835 847
836两个钩子就位后,窗格在 `/clear` 后显示保存的计数,而不是 `0`,**Add one** 的下一次按键添加到保存的计数。848两个 hook 就位后,窗格在 `/clear` 后显示保存的计数,而不是 `0`,并且下一次按下 **Add one** 会在保存的计数上累加。
837 849
838`loadCount` 将存储的值写入 `$.state` 中的值,`session.start` 每次模块重新加载时再次触发。要保持存储不落后,请在每次更改时保存,如 **Add one** 按钮所做的。850`loadCount` 会用存储的值覆盖 `$.state` 中的值,并且每次模块重新加载时 `session.start` 都会再次触发。为了防止存储落后,请在每次更改时保存,就像 **Add one** 按钮所做的那样。
839 851
840要在不会话的情况下检查重新加载,请[在 `/clear` 后测试绘制](/docs/zh-CN/plugins/mods/test#test-a-drawing-after-clear)。852要在不启动会话的情况下检查重新加载,请[在 `/clear` 后测试绘制](/docs/zh-CN/plugins/mods/test#test-a-drawing-after-clear)。
841 853
842<h3 id="save-from-more-than-one-session">854<h3 id="save-from-more-than-one-session">
843 从多个会话保存855 从多个会话保存
844</h3>856</h3>
845 857
846你的机器上运行你的 mod 的每个会话共享一个 `$.store`。`get` 后跟 `set` 不是原子的。当两个会话各自读取值、更改它并写回时,它们竞争,第二次写入替换第一次。858您的机器上运行您的 mod 的每个会话共享一个 `$.store`。`get` 后跟 `set` 不是原子的。当两个会话各自读取值、更改它并写回时,它们会发生竞争,第二次写入会替换第一次。
847 859
848两个选择使这种情况不太可能:860要降低这种情况发生的可能性:
849 861
850* **给每个项目其自己的键**:`set` 仅更改其自己的键,所以写入不同键的会话不会相互覆盖862* **给每个项目其自己的键**:`set` 仅更改其自己的键,所以写入不同键的会话不会相互覆盖
851* **在写入前再次读取**:对于多个会话更改的值,在回调中 `get` 键,并从该值构建新值,而不是从你在 `session.start` 加载的副本。如果另一个会话的写入落在你的 `get` 和 `set` 之间,它仍然会丢失。863* **在写入前立即再次读取**:对于多个会话更改的值,在回调中 `get` 该键,并从该值构建新值,而不是从您在 `session.start` 加载的副本构建。如果另一个会话的写入落在您的 `get` 和 `set` 之间,它仍然会丢失。
852 864
853此按钮将一个添加到存储现在保存的任何内容,然后更新绘制:865此按钮在存储当前保存的值上加一,然后更新绘制:
854 866
855```javascript theme={null}867```javascript theme={null}
856onPress: async () => {868onPress: async () => {
857 // 读取存储现在保存的内容,另一个会话可能已更改869 // 读取存储当前保存的内容,另一个会话可能已更改它
858 const saved = Number((await $.store.get('count')) ?? 0)870 const saved = Number((await $.store.get('count')) ?? 0)
859 // 保存新计数,然后显示它871 // 保存新计数,然后显示它
860 await $.store.set('count', saved + 1)872 await $.store.set('count', saved + 1)
862}874}
863```875```
864 876
865如果第二个会话自此会话启动以来按下了其自己的按钮三次,此按键显示并保存包括这三个的计数。877如果自此会话启动以来,第二个会话已按下其自己的按钮三次,那么此次按下所显示并保存的计数会包含这三次。
866 878
867<h2 id="next-steps">879<h2 id="next-steps">
868 后续步骤880 后续步骤