@clawling/clawchat-plugin-openclaw 2026.5.12-31 → 2026.5.12-32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -64,18 +64,16 @@ function persistOutputVisibility(draft, chatId, outputVisibility) {
64
64
  });
65
65
  }
66
66
  function formatOutputVisibilityResult(outputVisibility) {
67
- const runtimeStatus = outputVisibility === "full" ? "on" : "off";
68
67
  const detailLevel = {
69
- minimal: "quiet",
70
- normal: "normal",
71
- full: "verbose",
68
+ minimal: "final only",
69
+ normal: "final plus block media",
70
+ full: "final plus buffered reasoning, tool/progress, and block output",
72
71
  };
73
72
  return [
74
73
  "**ClawChat output updated**",
75
74
  "",
76
75
  `- visibility: \`${outputVisibility}\``,
77
- `- runtime status: \`${runtimeStatus}\``,
78
- `- detail level: \`${detailLevel[outputVisibility]}\``,
76
+ `- output: \`${detailLevel[outputVisibility]}\``,
79
77
  "",
80
78
  "Applies to new ClawChat messages.",
81
79
  ].join("\n");
@@ -191,6 +191,29 @@ function readGroupMode(value) {
191
191
  function readGroupCommandMode(value) {
192
192
  return value === "all" || value === "off" ? value : "owner";
193
193
  }
194
+ function readOutputVisibility(value) {
195
+ return value === "minimal" || value === "full" ? value : "normal";
196
+ }
197
+ function readOptionalOutputVisibility(value) {
198
+ return value === "minimal" || value === "normal" || value === "full" ? value : undefined;
199
+ }
200
+ function readChats(value) {
201
+ const rawChats = value && typeof value === "object" && !Array.isArray(value)
202
+ ? value
203
+ : {};
204
+ const chats = {};
205
+ for (const [chatId, rawChat] of Object.entries(rawChats)) {
206
+ if (!chatId)
207
+ continue;
208
+ const chat = rawChat && typeof rawChat === "object" && !Array.isArray(rawChat)
209
+ ? rawChat
210
+ : {};
211
+ const outputVisibility = readOptionalOutputVisibility(chat.outputVisibility);
212
+ if (outputVisibility)
213
+ chats[chatId] = { outputVisibility };
214
+ }
215
+ return chats;
216
+ }
194
217
  function readGroups(value) {
195
218
  const rawGroups = value && typeof value === "object" && !Array.isArray(value)
196
219
  ? value
@@ -202,9 +225,11 @@ function readGroups(value) {
202
225
  const group = rawGroup && typeof rawGroup === "object" && !Array.isArray(rawGroup)
203
226
  ? rawGroup
204
227
  : {};
228
+ const outputVisibility = readOptionalOutputVisibility(group.outputVisibility);
205
229
  groups[chatId] = {
206
230
  groupMode: readGroupMode(group.groupMode),
207
231
  groupCommandMode: readGroupCommandMode(group.groupCommandMode),
232
+ ...(outputVisibility ? { outputVisibility } : {}),
208
233
  };
209
234
  }
210
235
  return groups;
@@ -219,6 +244,13 @@ export function effectiveGroupCommandMode(account, chatId) {
219
244
  ?? account.groups["*"]?.groupCommandMode
220
245
  ?? account.groupCommandMode;
221
246
  }
247
+ export function effectiveOutputVisibility(account, chatId, chatType) {
248
+ return account.chats?.[chatId]?.outputVisibility
249
+ ?? (chatType === "group" ? account.groups?.[chatId]?.outputVisibility : undefined)
250
+ ?? (chatType === "group" ? account.groups?.["*"]?.outputVisibility : undefined)
251
+ ?? account.outputVisibility
252
+ ?? "normal";
253
+ }
222
254
  function readReconnect(raw) {
223
255
  const s = raw && typeof raw === "object" ? raw : {};
224
256
  return {
@@ -261,6 +293,8 @@ export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
261
293
  const userId = readOptionalString(channel.userId) || readEnvString(env, CLAWCHAT_USER_ID_ENV);
262
294
  const ownerUserId = readOptionalString(channel.ownerUserId) || readEnvString(env, CLAWCHAT_OWNER_USER_ID_ENV);
263
295
  const enabled = typeof channel.enabled === "boolean" ? channel.enabled : true;
296
+ const outputVisibility = readOutputVisibility(channel.outputVisibility);
297
+ const chats = readChats(channel.chats);
264
298
  const groupMode = readGroupMode(channel.groupMode);
265
299
  const groupCommandMode = readGroupCommandMode(channel.groupCommandMode);
266
300
  const groups = readGroups(channel.groups);
@@ -284,6 +318,8 @@ export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
284
318
  agentId,
285
319
  userId,
286
320
  ownerUserId,
321
+ outputVisibility,
322
+ chats,
287
323
  groupMode,
288
324
  groupCommandMode,
289
325
  groups,
@@ -1,6 +1,7 @@
1
1
  import { interactiveReplyToPresentation, renderMessagePresentationFallbackText, } from "openclaw/plugin-sdk/interactive-runtime";
2
2
  import { resolveOutboundMediaUrls } from "openclaw/plugin-sdk/reply-payload";
3
3
  import { createOpenclawClawlingApiClient } from "./api-client.js";
4
+ import { effectiveOutputVisibility, } from "./config.js";
4
5
  import { uploadOutboundMedia } from "./media-runtime.js";
5
6
  import { sendOpenclawClawlingText, } from "./outbound.js";
6
7
  import { isClawChatNoopResponseText } from "./profile-prompt.js";
@@ -106,15 +107,14 @@ function resolvePayloadText(payload) {
106
107
  /**
107
108
  * Reply dispatcher for clawchat-plugin-openclaw.
108
109
  *
109
- * The plugin intentionally forces complete-message delivery. It sets
110
- * `disableBlockStreaming: true` in reply options so OpenClaw does not split
111
- * deliver blocks for this channel. If the host still delivers non-final
112
- * blocks, the dispatcher buffers or ignores them and only emits materialized
113
- * `message.send` / `message.reply` frames for the final reply.
110
+ * ClawChat emits only materialized `message.send` / `message.reply` frames for
111
+ * the final reply. Non-final OpenClaw deliveries are ignored or buffered
112
+ * according to outputVisibility and are never sent as separate ClawChat frames.
114
113
  */
115
114
  export function createOpenclawClawlingReplyDispatcher(options) {
116
115
  const { cfg, runtime, account, client, target, replyCtx, inboundMessageId, store, log, } = options;
117
116
  const isGroupTarget = target.chatType === "group";
117
+ const outputVisibility = effectiveOutputVisibility(account, target.chatId, target.chatType);
118
118
  const ownerDirectTarget = () => {
119
119
  const ownerUserId = account.ownerUserId?.trim();
120
120
  return ownerUserId ? { chatId: ownerUserId, chatType: "direct" } : null;
@@ -142,6 +142,8 @@ export function createOpenclawClawlingReplyDispatcher(options) {
142
142
  }
143
143
  // ----- Reply state ------------------------------------------------------
144
144
  let reasoningText = "";
145
+ let bufferedOutputText = "";
146
+ const bufferedOutputUrls = [];
145
147
  let runDone = false;
146
148
  let typingActive = false;
147
149
  let terminalReplySuppressed = false;
@@ -229,6 +231,37 @@ export function createOpenclawClawlingReplyDispatcher(options) {
229
231
  recordOutbound("thinking", messageId, thinkingText);
230
232
  reasoningText = "";
231
233
  };
234
+ const resetBufferedOutput = () => {
235
+ bufferedOutputText = "";
236
+ bufferedOutputUrls.length = 0;
237
+ };
238
+ const appendBufferedText = (value) => {
239
+ const trimmed = value.trim();
240
+ if (!trimmed)
241
+ return;
242
+ bufferedOutputText = bufferedOutputText ? `${bufferedOutputText}\n${trimmed}` : trimmed;
243
+ };
244
+ const appendBufferedUrls = (urls) => {
245
+ for (const url of urls) {
246
+ if (url && !bufferedOutputUrls.includes(url))
247
+ bufferedOutputUrls.push(url);
248
+ }
249
+ };
250
+ const mergeFinalText = (text) => {
251
+ if (outputVisibility !== "full")
252
+ return text;
253
+ return [bufferedOutputText.trim(), text.trim()].filter(Boolean).join("\n");
254
+ };
255
+ const mergeFinalUrls = (urls) => {
256
+ if (outputVisibility === "minimal")
257
+ return urls;
258
+ const merged = bufferedOutputUrls.slice();
259
+ for (const url of urls) {
260
+ if (url && !merged.includes(url))
261
+ merged.push(url);
262
+ }
263
+ return merged;
264
+ };
232
265
  const mintStaticMessageId = () => `${account.userId}-msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
233
266
  const emitTyping = (isTyping) => {
234
267
  if (!isTyping && !typingActive)
@@ -342,6 +375,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
342
375
  onReplyStart: async () => {
343
376
  emitTyping(true);
344
377
  reasoningText = "";
378
+ resetBufferedOutput();
345
379
  runDone = false;
346
380
  },
347
381
  deliver: async (payload, info) => {
@@ -362,13 +396,30 @@ export function createOpenclawClawlingReplyDispatcher(options) {
362
396
  return;
363
397
  }
364
398
  if (payload.isReasoning) {
365
- if (isGroupTarget || !account.forwardThinking)
399
+ if (isGroupTarget || outputVisibility !== "full")
366
400
  return;
367
- reasoningText = text;
401
+ appendBufferedText(text);
402
+ const trimmed = text.trim();
403
+ if (trimmed)
404
+ reasoningText = reasoningText ? `${reasoningText}\n${trimmed}` : trimmed;
405
+ return;
406
+ }
407
+ if (info?.kind === "tool") {
408
+ if (!isGroupTarget && outputVisibility === "full") {
409
+ appendBufferedText(text);
410
+ appendBufferedUrls(urls);
411
+ }
368
412
  return;
369
413
  }
370
- if (info?.kind === "tool")
414
+ if (info?.kind === "block") {
415
+ if (!isGroupTarget && outputVisibility === "normal")
416
+ appendBufferedUrls(urls);
417
+ if (!isGroupTarget && outputVisibility === "full") {
418
+ appendBufferedText(text);
419
+ appendBufferedUrls(urls);
420
+ }
371
421
  return;
422
+ }
372
423
  if (info?.kind === "final") {
373
424
  if (isClawChatNoopResponseText(text) && !richFragment && urls.length === 0) {
374
425
  log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw final suppressed: no-reply token`);
@@ -391,15 +442,15 @@ export function createOpenclawClawlingReplyDispatcher(options) {
391
442
  });
392
443
  return;
393
444
  }
394
- const mediaFragments = await uploadMediaUrls(urls);
395
- const result = await sendStatic(text, mediaFragments, richFragment && account.richInteractions ? [richFragment] : [], { recordMessage: true });
445
+ const finalText = richFragment && account.richInteractions ? mergeFinalText("") : mergeFinalText(text);
446
+ const finalUrls = mergeFinalUrls(urls);
447
+ const mediaFragments = await uploadMediaUrls(finalUrls);
448
+ const result = await sendStatic(finalText, mediaFragments, richFragment && account.richInteractions ? [richFragment] : [], { recordMessage: true });
396
449
  if (result?.messageId)
397
450
  recordThinkingIfLinked(result.messageId);
398
451
  return;
399
452
  }
400
- // kind === "block" or unknown: OpenClaw may still call this path while
401
- // the model is producing output. ClawChat gets only the final materialized
402
- // reply.
453
+ // Unknown delivery kind: keep ClawChat output tied to OpenClaw final.
403
454
  },
404
455
  onError: (error, info) => {
405
456
  const errorText = normalizeReplyErrorText(error);
@@ -424,7 +475,7 @@ export function createOpenclawClawlingReplyDispatcher(options) {
424
475
  replyOptions: {
425
476
  ...base.replyOptions,
426
477
  sourceReplyDeliveryMode: "automatic",
427
- disableBlockStreaming: true,
478
+ disableBlockStreaming: outputVisibility !== "full",
428
479
  },
429
480
  markDispatchIdle: base.markDispatchIdle,
430
481
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawling/clawchat-plugin-openclaw",
3
- "version": "2026.5.12-31",
3
+ "version": "2026.5.12-32",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "license": "MIT",
6
6
  "files": [
package/src/commands.ts CHANGED
@@ -76,18 +76,16 @@ function persistOutputVisibility(
76
76
  }
77
77
 
78
78
  function formatOutputVisibilityResult(outputVisibility: OutputVisibility): string {
79
- const runtimeStatus = outputVisibility === "full" ? "on" : "off";
80
79
  const detailLevel: Record<OutputVisibility, string> = {
81
- minimal: "quiet",
82
- normal: "normal",
83
- full: "verbose",
80
+ minimal: "final only",
81
+ normal: "final plus block media",
82
+ full: "final plus buffered reasoning, tool/progress, and block output",
84
83
  };
85
84
  return [
86
85
  "**ClawChat output updated**",
87
86
  "",
88
87
  `- visibility: \`${outputVisibility}\``,
89
- `- runtime status: \`${runtimeStatus}\``,
90
- `- detail level: \`${detailLevel[outputVisibility]}\``,
88
+ `- output: \`${detailLevel[outputVisibility]}\``,
91
89
  "",
92
90
  "Applies to new ClawChat messages.",
93
91
  ].join("\n");
package/src/config.ts CHANGED
@@ -266,6 +266,8 @@ export type ResolvedOpenclawClawlingAccount = {
266
266
  agentId: string;
267
267
  userId: string;
268
268
  ownerUserId: string;
269
+ outputVisibility: OutputVisibility;
270
+ chats: Record<string, OpenclawClawlingChatConfig>;
269
271
  groupMode: GroupMode;
270
272
  groupCommandMode: GroupCommandMode;
271
273
  groups: Record<string, OpenclawClawlingGroupConfig>;
@@ -315,6 +317,30 @@ function readGroupCommandMode(value: unknown): GroupCommandMode {
315
317
  return value === "all" || value === "off" ? value : "owner";
316
318
  }
317
319
 
320
+ function readOutputVisibility(value: unknown): OutputVisibility {
321
+ return value === "minimal" || value === "full" ? value : "normal";
322
+ }
323
+
324
+ function readOptionalOutputVisibility(value: unknown): OutputVisibility | undefined {
325
+ return value === "minimal" || value === "normal" || value === "full" ? value : undefined;
326
+ }
327
+
328
+ function readChats(value: unknown): Record<string, OpenclawClawlingChatConfig> {
329
+ const rawChats = value && typeof value === "object" && !Array.isArray(value)
330
+ ? (value as Record<string, unknown>)
331
+ : {};
332
+ const chats: Record<string, OpenclawClawlingChatConfig> = {};
333
+ for (const [chatId, rawChat] of Object.entries(rawChats)) {
334
+ if (!chatId) continue;
335
+ const chat = rawChat && typeof rawChat === "object" && !Array.isArray(rawChat)
336
+ ? (rawChat as Record<string, unknown>)
337
+ : {};
338
+ const outputVisibility = readOptionalOutputVisibility(chat.outputVisibility);
339
+ if (outputVisibility) chats[chatId] = { outputVisibility };
340
+ }
341
+ return chats;
342
+ }
343
+
318
344
  function readGroups(value: unknown): Record<string, OpenclawClawlingGroupConfig> {
319
345
  const rawGroups = value && typeof value === "object" && !Array.isArray(value)
320
346
  ? (value as Record<string, unknown>)
@@ -325,9 +351,11 @@ function readGroups(value: unknown): Record<string, OpenclawClawlingGroupConfig>
325
351
  const group = rawGroup && typeof rawGroup === "object" && !Array.isArray(rawGroup)
326
352
  ? (rawGroup as Record<string, unknown>)
327
353
  : {};
354
+ const outputVisibility = readOptionalOutputVisibility(group.outputVisibility);
328
355
  groups[chatId] = {
329
356
  groupMode: readGroupMode(group.groupMode),
330
357
  groupCommandMode: readGroupCommandMode(group.groupCommandMode),
358
+ ...(outputVisibility ? { outputVisibility } : {}),
331
359
  };
332
360
  }
333
361
  return groups;
@@ -351,6 +379,18 @@ export function effectiveGroupCommandMode(
351
379
  ?? account.groupCommandMode;
352
380
  }
353
381
 
382
+ export function effectiveOutputVisibility(
383
+ account: Pick<ResolvedOpenclawClawlingAccount, "outputVisibility" | "chats" | "groups">,
384
+ chatId: string,
385
+ chatType: "direct" | "group",
386
+ ): OutputVisibility {
387
+ return account.chats?.[chatId]?.outputVisibility
388
+ ?? (chatType === "group" ? account.groups?.[chatId]?.outputVisibility : undefined)
389
+ ?? (chatType === "group" ? account.groups?.["*"]?.outputVisibility : undefined)
390
+ ?? account.outputVisibility
391
+ ?? "normal";
392
+ }
393
+
354
394
  function readReconnect(raw: unknown): Required<OpenclawClawlingReconnectConfig> {
355
395
  const s = raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
356
396
  return {
@@ -406,6 +446,8 @@ export function resolveOpenclawClawlingAccount(
406
446
  const ownerUserId =
407
447
  readOptionalString(channel.ownerUserId) || readEnvString(env, CLAWCHAT_OWNER_USER_ID_ENV);
408
448
  const enabled = typeof channel.enabled === "boolean" ? channel.enabled : true;
449
+ const outputVisibility = readOutputVisibility(channel.outputVisibility);
450
+ const chats = readChats(channel.chats);
409
451
  const groupMode = readGroupMode(channel.groupMode);
410
452
  const groupCommandMode = readGroupCommandMode(channel.groupCommandMode);
411
453
  const groups = readGroups(channel.groups);
@@ -433,6 +475,8 @@ export function resolveOpenclawClawlingAccount(
433
475
  agentId,
434
476
  userId,
435
477
  ownerUserId,
478
+ outputVisibility,
479
+ chats,
436
480
  groupMode,
437
481
  groupCommandMode,
438
482
  groups,
@@ -13,7 +13,10 @@ import { resolveOutboundMediaUrls } from "openclaw/plugin-sdk/reply-payload";
13
13
  import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
14
14
  import { createOpenclawClawlingApiClient } from "./api-client.ts";
15
15
  import type { ChatType } from "./client.ts";
16
- import type { ResolvedOpenclawClawlingAccount } from "./config.ts";
16
+ import {
17
+ effectiveOutputVisibility,
18
+ type ResolvedOpenclawClawlingAccount,
19
+ } from "./config.ts";
17
20
  import { uploadOutboundMedia, type ClawlingMediaFragment } from "./media-runtime.ts";
18
21
  import {
19
22
  sendOpenclawClawlingText,
@@ -65,7 +68,7 @@ type TypedReplyDispatcherResult = ReturnType<
65
68
  type ClawChatReplyOptions = TypedReplyDispatcherResult["replyOptions"] &
66
69
  {
67
70
  sourceReplyDeliveryMode: "automatic";
68
- disableBlockStreaming: true;
71
+ disableBlockStreaming: boolean;
69
72
  };
70
73
 
71
74
  type RichAction = {
@@ -188,11 +191,9 @@ function resolvePayloadText(payload: ReplyPayload): string {
188
191
  /**
189
192
  * Reply dispatcher for clawchat-plugin-openclaw.
190
193
  *
191
- * The plugin intentionally forces complete-message delivery. It sets
192
- * `disableBlockStreaming: true` in reply options so OpenClaw does not split
193
- * deliver blocks for this channel. If the host still delivers non-final
194
- * blocks, the dispatcher buffers or ignores them and only emits materialized
195
- * `message.send` / `message.reply` frames for the final reply.
194
+ * ClawChat emits only materialized `message.send` / `message.reply` frames for
195
+ * the final reply. Non-final OpenClaw deliveries are ignored or buffered
196
+ * according to outputVisibility and are never sent as separate ClawChat frames.
196
197
  */
197
198
  export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOptions): {
198
199
  dispatcher: TypedReplyDispatcherResult["dispatcher"];
@@ -211,6 +212,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
211
212
  log,
212
213
  } = options;
213
214
  const isGroupTarget = target.chatType === "group";
215
+ const outputVisibility = effectiveOutputVisibility(account, target.chatId, target.chatType);
214
216
  const ownerDirectTarget = () => {
215
217
  const ownerUserId = account.ownerUserId?.trim();
216
218
  return ownerUserId ? { chatId: ownerUserId, chatType: "direct" as const } : null;
@@ -242,6 +244,8 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
242
244
  // ----- Reply state ------------------------------------------------------
243
245
 
244
246
  let reasoningText = "";
247
+ let bufferedOutputText = "";
248
+ const bufferedOutputUrls: string[] = [];
245
249
  let runDone = false;
246
250
  let typingActive = false;
247
251
  let terminalReplySuppressed = false;
@@ -327,6 +331,32 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
327
331
  recordOutbound("thinking", messageId, thinkingText);
328
332
  reasoningText = "";
329
333
  };
334
+ const resetBufferedOutput = () => {
335
+ bufferedOutputText = "";
336
+ bufferedOutputUrls.length = 0;
337
+ };
338
+ const appendBufferedText = (value: string) => {
339
+ const trimmed = value.trim();
340
+ if (!trimmed) return;
341
+ bufferedOutputText = bufferedOutputText ? `${bufferedOutputText}\n${trimmed}` : trimmed;
342
+ };
343
+ const appendBufferedUrls = (urls: string[]) => {
344
+ for (const url of urls) {
345
+ if (url && !bufferedOutputUrls.includes(url)) bufferedOutputUrls.push(url);
346
+ }
347
+ };
348
+ const mergeFinalText = (text: string): string => {
349
+ if (outputVisibility !== "full") return text;
350
+ return [bufferedOutputText.trim(), text.trim()].filter(Boolean).join("\n");
351
+ };
352
+ const mergeFinalUrls = (urls: string[]): string[] => {
353
+ if (outputVisibility === "minimal") return urls;
354
+ const merged = bufferedOutputUrls.slice();
355
+ for (const url of urls) {
356
+ if (url && !merged.includes(url)) merged.push(url);
357
+ }
358
+ return merged;
359
+ };
330
360
 
331
361
  const mintStaticMessageId = () =>
332
362
  `${account.userId}-msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
@@ -462,6 +492,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
462
492
  onReplyStart: async () => {
463
493
  emitTyping(true);
464
494
  reasoningText = "";
495
+ resetBufferedOutput();
465
496
  runDone = false;
466
497
  },
467
498
  deliver: async (payload: ReplyPayload, info?: { kind: "tool" | "block" | "final" }) => {
@@ -488,12 +519,29 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
488
519
  }
489
520
 
490
521
  if (payload.isReasoning) {
491
- if (isGroupTarget || !account.forwardThinking) return;
492
- reasoningText = text;
522
+ if (isGroupTarget || outputVisibility !== "full") return;
523
+ appendBufferedText(text);
524
+ const trimmed = text.trim();
525
+ if (trimmed) reasoningText = reasoningText ? `${reasoningText}\n${trimmed}` : trimmed;
493
526
  return;
494
527
  }
495
528
 
496
- if (info?.kind === "tool") return;
529
+ if (info?.kind === "tool") {
530
+ if (!isGroupTarget && outputVisibility === "full") {
531
+ appendBufferedText(text);
532
+ appendBufferedUrls(urls);
533
+ }
534
+ return;
535
+ }
536
+
537
+ if (info?.kind === "block") {
538
+ if (!isGroupTarget && outputVisibility === "normal") appendBufferedUrls(urls);
539
+ if (!isGroupTarget && outputVisibility === "full") {
540
+ appendBufferedText(text);
541
+ appendBufferedUrls(urls);
542
+ }
543
+ return;
544
+ }
497
545
 
498
546
  if (info?.kind === "final") {
499
547
  if (isClawChatNoopResponseText(text) && !richFragment && urls.length === 0) {
@@ -517,9 +565,11 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
517
565
  });
518
566
  return;
519
567
  }
520
- const mediaFragments = await uploadMediaUrls(urls);
568
+ const finalText = richFragment && account.richInteractions ? mergeFinalText("") : mergeFinalText(text);
569
+ const finalUrls = mergeFinalUrls(urls);
570
+ const mediaFragments = await uploadMediaUrls(finalUrls);
521
571
  const result = await sendStatic(
522
- text,
572
+ finalText,
523
573
  mediaFragments,
524
574
  richFragment && account.richInteractions ? ([richFragment] as unknown as Fragment[]) : [],
525
575
  { recordMessage: true },
@@ -528,9 +578,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
528
578
  return;
529
579
  }
530
580
 
531
- // kind === "block" or unknown: OpenClaw may still call this path while
532
- // the model is producing output. ClawChat gets only the final materialized
533
- // reply.
581
+ // Unknown delivery kind: keep ClawChat output tied to OpenClaw final.
534
582
  },
535
583
  onError: (error: unknown, info: { kind: string }) => {
536
584
  const errorText = normalizeReplyErrorText(error);
@@ -559,7 +607,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
559
607
  replyOptions: {
560
608
  ...base.replyOptions,
561
609
  sourceReplyDeliveryMode: "automatic",
562
- disableBlockStreaming: true,
610
+ disableBlockStreaming: outputVisibility !== "full",
563
611
  },
564
612
  markDispatchIdle: base.markDispatchIdle,
565
613
  };