fix(discord): preserve reusable presentation buttons · openclaw/openclaw@0212188
100menotu001
·
2026-05-22
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -109,6 +109,7 @@ Docs: https://docs.openclaw.ai
|
109 | 109 | - Gateway/agents: use an agent's `identity.name` in Gateway agent summaries when `agents.list[].name` is unset, so configured agent labels remain visible in clients. (#84355; refs #57835) Thanks @luoyanglang. |
110 | 110 | - Channels/replies: keep normal `/verbose` failed-tool progress compact in message-tool replies and prevent late text-only tool output from appearing after the final answer. (#84303) Thanks @VACInc. |
111 | 111 | - Plugins/hooks: apply a default 30-second timeout to `before_compaction` and `after_compaction` hooks so a hung plugin handler no longer blocks compaction completion. (#84153) |
| 112 | +- Discord: preserve reusable presentation buttons through portable conversion and Discord component registration. (#84187) Thanks @100menotu001. |
112 | 113 | - Discord: preserve disabled presentation buttons when adapting and rendering Discord message controls. (#84188) Thanks @100menotu001. |
113 | 114 | - Twitch: add a test-only client-manager registry reset helper so non-isolated Twitch tests can clear cached managers between cases. Fixes #83887. (#84244) Thanks @hclsys. |
114 | 115 | - Cron: run main-session scheduled work on a cron-owned wake lane while preserving reply delivery context, so background cron turns no longer block human main-session chat. Fixes #82766. (#82767) Thanks @galiniliev. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -61,6 +61,7 @@ type MessagePresentationButton = {
|
61 | 61 | web_app?: { url: string }; |
62 | 62 | priority?: number; |
63 | 63 | disabled?: boolean; |
| 64 | + reusable?: boolean; |
64 | 65 | style?: "primary" | "secondary" | "success" | "danger"; |
65 | 66 | }; |
66 | 67 | |
@@ -98,6 +99,10 @@ Button semantics:
|
98 | 99 | order is preserved. |
99 | 100 | - `disabled` is optional. Channels must opt in with `supportsDisabled`; otherwise |
100 | 101 | core degrades the disabled control to non-interactive fallback text. |
| 102 | +- `reusable` is optional. Channels that support reusable native callbacks may |
| 103 | + keep the action available after a successful interaction. Use it for |
| 104 | + repeatable or idempotent actions such as refresh, inspect, or more details; |
| 105 | + leave it unset for normal one-shot approvals and destructive actions. |
101 | 106 | |
102 | 107 | Select semantics: |
103 | 108 | |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -95,6 +95,7 @@ function createButtonComponent(params: {
|
95 | 95 | label: params.spec.label, |
96 | 96 | callbackData: params.spec.callbackData, |
97 | 97 | modalId: params.modalId, |
| 98 | +reusable: params.spec.reusable, |
98 | 99 | allowedUsers: params.spec.allowedUsers, |
99 | 100 | }, |
100 | 101 | }; |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -25,6 +25,8 @@ export type DiscordComponentButtonSpec = {
|
25 | 25 | animated?: boolean; |
26 | 26 | }; |
27 | 27 | disabled?: boolean; |
| 28 | +/** Keep this action available after a successful interaction. */ |
| 29 | +reusable?: boolean; |
28 | 30 | /** Optional allowlist of users who can interact with this button (ids or names). */ |
29 | 31 | allowedUsers?: string[]; |
30 | 32 | }; |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -186,4 +186,26 @@ describe("buildDiscordInteractiveComponents", () => {
|
186 | 186 | ], |
187 | 187 | }); |
188 | 188 | }); |
| 189 | + |
| 190 | +it("preserves reusable presentation buttons for Discord action entries", () => { |
| 191 | +expect( |
| 192 | +buildDiscordPresentationComponents({ |
| 193 | +blocks: [ |
| 194 | +{ |
| 195 | +type: "buttons", |
| 196 | +buttons: [{ label: "Refresh", value: "refresh", reusable: true }], |
| 197 | +}, |
| 198 | +], |
| 199 | +}), |
| 200 | +).toEqual({ |
| 201 | +blocks: [ |
| 202 | +{ |
| 203 | +type: "actions", |
| 204 | +buttons: [ |
| 205 | +{ label: "Refresh", style: "secondary", callbackData: "refresh", reusable: true }, |
| 206 | +], |
| 207 | +}, |
| 208 | +], |
| 209 | +}); |
| 210 | +}); |
189 | 211 | }); |
| Original file line number | Diff line number | Diff line change |
|---|
@@ -63,6 +63,9 @@ export function buildDiscordInteractiveComponents(
|
63 | 63 | if (button.disabled === true) { |
64 | 64 | spec.disabled = true; |
65 | 65 | } |
| 66 | +if (button.reusable === true) { |
| 67 | +spec.reusable = true; |
| 68 | +} |
66 | 69 | return spec; |
67 | 70 | }), |
68 | 71 | }); |
@@ -160,6 +163,9 @@ function appendDiscordPresentationButtonBlocks(
|
160 | 163 | if (button.disabled === true) { |
161 | 164 | component.disabled = true; |
162 | 165 | } |
| 166 | +if (button.reusable === true) { |
| 167 | +component.reusable = true; |
| 168 | +} |
163 | 169 | return component; |
164 | 170 | }), |
165 | 171 | }); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -167,6 +167,7 @@ const presentationButtonSchema = Type.Object({
|
167 | 167 | webApp: Type.Optional(Type.Object({ url: Type.String() })), |
168 | 168 | web_app: Type.Optional(Type.Object({ url: Type.String() })), |
169 | 169 | disabled: Type.Optional(Type.Boolean()), |
| 170 | +reusable: Type.Optional(Type.Boolean()), |
170 | 171 | style: Type.Optional(stringEnum(["primary", "secondary", "success", "danger"])), |
171 | 172 | }); |
172 | 173 | |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -173,7 +173,14 @@ describe("interactive payload helpers", () => {
|
173 | 173 | { type: "divider" as const }, |
174 | 174 | { |
175 | 175 | type: "buttons" as const, |
176 | | -buttons: [{ label: "Approve", value: "approve", style: "success" as const }], |
| 176 | +buttons: [ |
| 177 | +{ |
| 178 | +label: "Approve", |
| 179 | +value: "approve", |
| 180 | +style: "success" as const, |
| 181 | +reusable: true, |
| 182 | +}, |
| 183 | +], |
177 | 184 | }, |
178 | 185 | { |
179 | 186 | type: "select" as const, |
@@ -189,7 +196,7 @@ describe("interactive payload helpers", () => {
|
189 | 196 | { type: "text", text: "Canary is ready." }, |
190 | 197 | { |
191 | 198 | type: "buttons", |
192 | | -buttons: [{ label: "Approve", value: "approve", style: "success" }], |
| 199 | +buttons: [{ label: "Approve", value: "approve", style: "success", reusable: true }], |
193 | 200 | }, |
194 | 201 | { |
195 | 202 | type: "select", |
@@ -202,7 +209,7 @@ describe("interactive payload helpers", () => {
|
202 | 209 | blocks: [ |
203 | 210 | { |
204 | 211 | type: "buttons", |
205 | | -buttons: [{ label: "Approve", value: "approve", style: "success" }], |
| 212 | +buttons: [{ label: "Approve", value: "approve", style: "success", reusable: true }], |
206 | 213 | }, |
207 | 214 | { |
208 | 215 | type: "select", |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -33,6 +33,8 @@ export type MessagePresentationButton = {
|
33 | 33 | priority?: number; |
34 | 34 | /** Disable the button when the target channel supports disabled controls. */ |
35 | 35 | disabled?: boolean; |
| 36 | +/** Keep this action available after a successful interaction when the target channel supports it. */ |
| 37 | +reusable?: boolean; |
36 | 38 | /** Optional visual style hint; unsupported channels ignore or normalize it. */ |
37 | 39 | style?: InteractiveButtonStyle; |
38 | 40 | }; |
@@ -207,6 +209,7 @@ function normalizeButton(raw: unknown): InteractiveReplyButton | undefined {
|
207 | 209 | ...(webAppUrl ? { webApp: { url: webAppUrl } } : {}), |
208 | 210 | ...(priority !== undefined ? { priority } : {}), |
209 | 211 | ...(record.disabled === true ? { disabled: true } : {}), |
| 212 | + ...(record.reusable === true ? { reusable: true } : {}), |
210 | 213 | style: normalizeButtonStyle(record.style), |
211 | 214 | }; |
212 | 215 | } |
@@ -366,6 +369,9 @@ export function presentationToInteractiveReply(
|
366 | 369 | if (button.disabled === true) { |
367 | 370 | interactiveButton.disabled = true; |
368 | 371 | } |
| 372 | +if (button.reusable === true) { |
| 373 | +interactiveButton.reusable = true; |
| 374 | +} |
369 | 375 | return interactiveButton; |
370 | 376 | }); |
371 | 377 | if (buttons.length > 0) { |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。