Skip to content

Commit b5d408c

Browse files
committed
feat: add rich Slack progress drafts
1 parent 654b70d commit b5d408c

15 files changed

Lines changed: 440 additions & 56 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
1111
### Changes
1212

1313
- Channels/streaming: add unified `streaming.mode: "progress"` drafts with auto single-word status labels and shared progress configuration across Discord, Telegram, Matrix, Slack, and Microsoft Teams.
14+
- Slack/streaming: add `streaming.progress.render: "rich"` for Block Kit progress drafts backed by structured progress line data.
1415
- Channels/streaming: cap progress-draft tool lines by default so edited progress boxes avoid jumpy reflow from long wrapped lines.
1516
- Agents/verbose: use compact explain-mode tool summaries for `/verbose` and progress drafts by default, with `agents.defaults.toolProgressDetail: "raw"` and per-agent overrides for debugging raw command/detail output.
1617
- Agents/commands: add `/steer <message>` for queue-independent steering of the active current-session run without starting a new turn when the session is idle. (#76934)
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
ac95b4ab62408454636ce559e6d023df3c29b8b936b3aa4dde37779d29a5a099 config-baseline.json
1+
953aece02c70b8df690b51e865a4aea838b53bbe9d43ef9495f80f719a831e38 config-baseline.json
22
31ec333df9f8b92c7656ac7107cecd5860dd02e08f7e18c7c674dc47a8811baa config-baseline.core.json
3-
655d1309b70505e73198df20c5088784290b33098efd42027d3c09beeb3704a7 config-baseline.channel.json
4-
9458dc89aa13dd07d83f69d943535099a96e8278eb7ac8ae5cf2f713631592f7 config-baseline.plugin.json
3+
e10ba2f29f25fc665b96c714075af954eed686c56ca12783cf1f49498f86ac98 config-baseline.channel.json
4+
606641569764473005f8343f4550500dcbe99cf54e1dc21960018cf455912196 config-baseline.plugin.json
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
701356478634a8f3e71f941ed21a00e0456d947d287edcafb56231013b27a057 plugin-sdk-api-baseline.json
2-
ed17426dd5e9db4b83db77162e7490eee3c0439170c1a9d1e84c01d7027d580c plugin-sdk-api-baseline.jsonl
1+
2943ada651fd9a07c9e715a90ad4a76f725a1b60fa142dcfd504ba6d6c202ed4 plugin-sdk-api-baseline.json
2+
ff31408a26bcad4c54dc0c897d0103ca3d7dc91b3394a3ab65e7dade0c3f6ff5 plugin-sdk-api-baseline.jsonl

docs/concepts/progress-drafts.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,27 @@ OpenClaw truncates long progress lines by default so repeated draft edits do not
223223
wrap differently on every update. The prefix stays readable, and long details
224224
such as paths or raw commands are shortened with an ellipsis.
225225

226+
Slack can render progress lines as structured Block Kit fields instead of a
227+
single text body:
228+
229+
```json5
230+
{
231+
channels: {
232+
slack: {
233+
streaming: {
234+
mode: "progress",
235+
progress: {
236+
render: "rich",
237+
},
238+
},
239+
},
240+
},
241+
}
242+
```
243+
244+
Rich rendering keeps the same plain-text fallback so channels and clients that
245+
do not support the richer shape can still show the compact progress text.
246+
226247
Keep the single progress draft but hide tool and task lines:
227248

228249
```json5

extensions/slack/src/config-ui-hints.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ export const slackChannelConfigUiHints = {
129129
label: "Slack Progress Max Lines",
130130
help: "Maximum number of compact progress lines to keep below the draft label (default: 8).",
131131
},
132+
"streaming.progress.render": {
133+
label: "Slack Progress Renderer",
134+
help: 'Progress draft renderer: "text" uses one portable text body; "rich" renders structured Slack Block Kit fields with the same text fallback.',
135+
},
132136
"streaming.progress.toolProgress": {
133137
label: "Slack Progress Tool Lines",
134138
help: "Show compact tool/progress lines in progress draft mode (default: true). Set false to keep only the label until final delivery.",

extensions/slack/src/draft-stream.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,28 @@ describe("createSlackDraftStream", () => {
5959
});
6060
});
6161

62+
it("sends and edits rich draft blocks with text fallback", async () => {
63+
const { stream, send, edit } = createDraftStreamHarness();
64+
const blocks = [{ type: "divider" }] as const;
65+
66+
stream.update({ text: "fallback", blocks: [...blocks] });
67+
await stream.flush();
68+
stream.update({ text: "updated fallback", blocks: [...blocks] });
69+
await stream.flush();
70+
71+
expect(send).toHaveBeenCalledWith(
72+
"channel:C123",
73+
"fallback",
74+
expect.objectContaining({ blocks: [...blocks] }),
75+
);
76+
expect(edit).toHaveBeenCalledWith(
77+
"C123",
78+
"111.222",
79+
"updated fallback",
80+
expect.objectContaining({ blocks: [...blocks] }),
81+
);
82+
});
83+
6284
it("does not send duplicate text", async () => {
6385
const { stream, send, edit } = createDraftStreamHarness();
6486

extensions/slack/src/draft-stream.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Block, KnownBlock } from "@slack/web-api";
12
import { createDraftStreamLoop } from "openclaw/plugin-sdk/channel-lifecycle";
23
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
34
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
@@ -8,7 +9,7 @@ import { sendMessageSlack } from "./send.js";
89
const DEFAULT_THROTTLE_MS = 1000;
910

1011
type SlackDraftStream = {
11-
update: (text: string) => void;
12+
update: (update: SlackDraftStreamUpdate) => void;
1213
flush: () => Promise<void>;
1314
clear: () => Promise<void>;
1415
discardPending: () => Promise<void>;
@@ -19,6 +20,13 @@ type SlackDraftStream = {
1920
channelId: () => string | undefined;
2021
};
2122

23+
export type SlackDraftStreamUpdate =
24+
| string
25+
| {
26+
text: string;
27+
blocks?: (Block | KnownBlock)[];
28+
};
29+
2230
export function createSlackDraftStream(params: {
2331
target: string;
2432
cfg: OpenClawConfig;
@@ -42,9 +50,13 @@ export function createSlackDraftStream(params: {
4250

4351
let streamMessageId: string | undefined;
4452
let streamChannelId: string | undefined;
45-
let lastSentText = "";
53+
let lastSentKey = "";
54+
let pendingUpdate: SlackDraftStreamUpdate | undefined;
4655
let stopped = false;
4756

57+
const normalizeUpdate = (update: SlackDraftStreamUpdate) =>
58+
typeof update === "string" ? { text: update } : update;
59+
4860
const sendOrEditStreamMessage = async (text: string) => {
4961
if (stopped) {
5062
return;
@@ -58,16 +70,20 @@ export function createSlackDraftStream(params: {
5870
params.warn?.(`slack stream preview stopped (text length ${trimmed.length} > ${maxChars})`);
5971
return;
6072
}
61-
if (trimmed === lastSentText) {
73+
const update = normalizeUpdate(pendingUpdate ?? text);
74+
const blocks = update.text === text ? update.blocks : undefined;
75+
const sentKey = `${trimmed}\n${blocks ? JSON.stringify(blocks) : ""}`;
76+
if (sentKey === lastSentKey) {
6277
return;
6378
}
64-
lastSentText = trimmed;
79+
lastSentKey = sentKey;
6580
try {
6681
if (streamChannelId && streamMessageId) {
6782
await edit(streamChannelId, streamMessageId, trimmed, {
6883
cfg: params.cfg,
6984
token: params.token,
7085
accountId: params.accountId,
86+
...(blocks ? { blocks } : {}),
7187
});
7288
return;
7389
}
@@ -76,6 +92,7 @@ export function createSlackDraftStream(params: {
7692
token: params.token,
7793
accountId: params.accountId,
7894
threadTs: params.resolveThreadTs?.(),
95+
...(blocks ? { blocks } : {}),
7996
});
8097
streamChannelId = sent.channelId || streamChannelId;
8198
streamMessageId = sent.messageId || streamMessageId;
@@ -112,7 +129,8 @@ export function createSlackDraftStream(params: {
112129
const messageId = streamMessageId;
113130
streamChannelId = undefined;
114131
streamMessageId = undefined;
115-
lastSentText = "";
132+
lastSentKey = "";
133+
pendingUpdate = undefined;
116134
if (!channelId || !messageId) {
117135
return;
118136
}
@@ -129,14 +147,19 @@ export function createSlackDraftStream(params: {
129147
const forceNewMessage = () => {
130148
streamMessageId = undefined;
131149
streamChannelId = undefined;
132-
lastSentText = "";
150+
lastSentKey = "";
151+
pendingUpdate = undefined;
133152
loop.resetPending();
134153
};
135154

136155
params.log?.(`slack stream preview ready (maxChars=${maxChars}, throttleMs=${throttleMs})`);
137156

138157
return {
139-
update: loop.update,
158+
update: (update: SlackDraftStreamUpdate) => {
159+
const normalized = normalizeUpdate(update);
160+
pendingUpdate = update;
161+
loop.update(normalized.text);
162+
},
140163
flush: loop.flush,
141164
clear,
142165
discardPending,

0 commit comments

Comments
 (0)