mirror of
https://github.com/openclaw/openclaw.git
synced 2026-11-05 07:10:16 +00:00
feat(discord): show per-tool glyphs on progress draft tool rows (#168032)
## What Problem This Solves Discord progress drafts show tool-log rows without a per-tool glyph (`• Exec: running`), while Telegram shows `🛠️ Exec: running` since #167392. Discord already marks the 🧠 reasoning and 💬 commentary lanes, so its tool rows were the only unmarked lane. ## User Impact Discord tool-log rows in progress drafts now start with the tool's text glyph (`🛠️ Exec`, `📖 Read`, `🩹 Apply Patch`). The 🧠 / 💬 lanes, plan rows, approval rows, and plain status rows are unchanged. No config changes. Slack and other channels keep plain rows. ## Why This Change Was Made #167392 added the core opt-in (`toolIcons?: boolean` on `createChannelProgressDraftCompositor`, glyph table in `src/channels/progress-tool-glyphs.ts`) and left Discord as a follow-up. This PR sets `toolIcons: true` on every Discord compositor so all Discord paths render the same: - live draft preview (`message-handler.draft-preview.ts`) - retained progress continuation after handoff (`message-handler.progress-continuation.ts`) - retained-card `edit` action rendering a progress snapshot (`runtime.messaging.messages.ts`) No Discord-specific glyph code. Rows that already carry an explicit `icon` keep it. ## Tests - `message-handler.draft-preview.rest.test.ts`: new regression through the real draft preview and Discord REST client: `🧠 _Planning the check_` / `💬 Looking at the workspace` / `🛠️ Exec: running` / `📖 Read: running`. Fails on main with `• Exec: running` / `• Read: running`. - `message-bodies.test.ts`: a retained row's explicit icon (`🧪 Read`) wins over the tool glyph. - `message-handler.process.draft-final.test.ts`: existing expectations updated to the glyph rows. ## Bounded cost - One static `Record` lookup per core-built tool row on Discord (core opt-in from #167392). - No new config keys, protocol fields, network calls, or extra Discord sends/edits; the draft text gains one glyph and a space per tool row, inside the existing per-message length bounds. - Production: +3 lines. Tests: +65/−5. Docs: +3/−3. No extra Discord sends or edits in the runs above beyond the existing progress-draft cadence. ## Evidence Isolated gateway (`HOME`/`OPENCLAW_STATE_DIR`/`OPENCLAW_CONFIG_PATH` under `/tmp`) with the real bundled Discord plugin against QA Lab's Crabline Discord provider server, `streaming.mode: "progress"`, `progress.toolProgress: true`. Rows below are the Discord REST bodies the plugin sent, from the Crabline provider recorder (`artifacts/crabline/discord-provider-server.jsonl`). **Live model `openai/gpt-6-astra`** (`qa suite --provider-mode live-frontier --model openai/gpt-6-astra --channel-driver crabline --channel discord`), prompt asked for one exec then one read: ``` POST "Working\n\n🛠️ Exec: running\n🛠️ Exec: running" PATCH "Working\n\n🛠️ Exec: running\n🛠️ Exec" PATCH "…\n\n🛠️ Exec\n🛠️ Exec\n🛠️ Exec: running" PATCH "…\n\n🛠️ Exec\n🛠️ Exec\n🛠️ Exec\n📖 Read from glyph-proof.txt" POST final reply (separate message) ``` **Scripted model (`mock-openai`)**, commentary + one exec: ``` POST "Working\n\n💬 SLACK-QA-COMMENTARY-200C8C6D" PATCH "Working\n\n💬 SLACK-QA-COMMENTARY-200C8C6D\n🛠️ Exec: running" PATCH "Working\n\n💬 SLACK-QA-COMMENTARY-200C8C6D\n🛠️ Exec" POST "SLACK-QA-COMMENTARY-DONE-200C8C6D" ``` The 💬 commentary lane keeps its prefix, and the final reply is one separate message, as before. Before this change, the same rows render as `• Exec: running` / `• Read: running` (the new REST regression fails on main with exactly that diff).
This commit is contained in:
parent
999bf5ecc5
commit
cb9bdcd6e7
8 changed files with 71 additions and 8 deletions
|
|
@ -99,7 +99,7 @@ Messages in the same channel continue reaching the [reply queue](/concepts/queue
|
|||
- `block` emits draft-sized chunks; tune size and breakpoints with `streaming.preview.chunk` (`minChars`, `maxChars`, `breakPreference`), clamped to `textChunkLimit`. An explicit non-`off` preview mode overrides inherited `agents.defaults.blockStreamingDefault: "on"`; explicit `streaming.block.enabled: true` overrides the preview. If a turn cannot use previews, inherited block delivery still applies.
|
||||
- `progress` keeps one editable status draft until final delivery. By default it is quiet: the agent's latest preamble or narration as a status headline, 💬 commentary and 🧠 reasoning when they stream, ✅ / ▸ / ▢ plan steps, and any approval request or failed command. Ordinary tool calls do not add rows.
|
||||
- Media, error, and explicit-reply finals cancel pending preview edits.
|
||||
- `streaming.progress.toolProgress: true` adds the rolling tool log underneath the headline: rows such as `Bash: run tests` or `Web Search: for "query"` (default `false`). `streaming.preview.toolProgress` controls tool rows in `partial` and `block` modes, where they default to `true`.
|
||||
- `streaming.progress.toolProgress: true` adds the rolling tool log underneath the headline: rows such as `🛠️ Exec: running` or `📖 Read from notes.txt` (default `false`). Each tool row starts with a text glyph for its tool. `streaming.preview.toolProgress` controls tool rows in `partial` and `block` modes, where they default to `true`.
|
||||
- `streaming.progress.commentary` (default `false`) opts into raw assistant commentary in the temporary progress draft. The default preamble/narration status line is independent of this option. Commentary is cleaned before display, stays transient, and does not change final answer delivery.
|
||||
- `streaming.progress.maxLineChars` controls the per-line progress preview budget. Prose is shortened on word boundaries; command and path details keep useful suffixes.
|
||||
- `streaming.preview.commandText` / `streaming.progress.commandText` controls command/exec detail in compact progress lines: `status` (default, tool label only) or `raw` (explicit command text).
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ requests. On Discord and Telegram, it also keeps the current operation and
|
|||
delegated task status visible without a detailed tool log or utility model.
|
||||
Intermediate tool failures and nonzero command exits stay out of the draft.
|
||||
Set `streaming.progress.toolProgress: true` to add a rolling tool log,
|
||||
including tool failures, with rows such as `Bash: run tests`. Telegram
|
||||
prefixes each tool row with a text glyph for its tool, such as
|
||||
including tool failures, with rows such as `Bash: run tests`. Discord and
|
||||
Telegram prefix each tool row with a text glyph for its tool, such as
|
||||
`📖 Read: from docs/index.md`; other channels keep plain rows. Channel plugins opt in
|
||||
through the `toolIcons` option of `createChannelProgressDraftCompositor`.
|
||||
|
||||
|
|
|
|||
|
|
@ -278,6 +278,29 @@ describe("Discord retained progress edits", () => {
|
|||
expect(current.content.length).toBeLessThanOrEqual(100);
|
||||
});
|
||||
|
||||
it("keeps a retained row's explicit icon over the tool glyph", async () => {
|
||||
await discordMessageActions.handleAction?.({
|
||||
channel: "discord",
|
||||
action: "edit",
|
||||
cfg: {
|
||||
channels: {
|
||||
discord: {
|
||||
token,
|
||||
groupPolicy: "open",
|
||||
streaming: { progress: { label: false, toolProgress: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
params: { to: `channel:${channelId}`, messageId, message: "Generic fallback" },
|
||||
progressSnapshot: {
|
||||
lines: [{ kind: "tool", text: "Read", label: "Read", toolName: "read", icon: "🧪" }],
|
||||
},
|
||||
conversationReadOrigin: "direct-operator",
|
||||
});
|
||||
|
||||
expect(current.content).toBe("🧪 Read");
|
||||
});
|
||||
|
||||
it.each([true, false])(
|
||||
"uses the current DM channel only for its captured user target (matching: %s)",
|
||||
async (matching) => {
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ export async function handleDiscordMessageManagementAction(ctx: DiscordMessaging
|
|||
active: true,
|
||||
seed: `${ctx.accountId}:${channelId}`,
|
||||
initialSnapshot: snapshot,
|
||||
toolIcons: true,
|
||||
}).getText(),
|
||||
{
|
||||
maxChars: Math.min(ctx.accountConfig.textChunkLimit ?? 2000, 2000),
|
||||
|
|
|
|||
|
|
@ -213,6 +213,41 @@ describe("Discord draft preview REST lifecycle", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("prefixes tool rows with tool glyphs and keeps the reasoning and commentary lanes", async () => {
|
||||
vi.useFakeTimers();
|
||||
const { controller, visible } = createContinuationHarness({
|
||||
discordConfig: {
|
||||
streaming: {
|
||||
mode: "progress",
|
||||
progress: { label: false, toolProgress: true, commentary: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
await controller.pushReasoningProgress("Planning the check");
|
||||
await controller.pushItemEvent({
|
||||
itemId: "preamble-1",
|
||||
kind: "preamble",
|
||||
phase: "end",
|
||||
progressText: "Looking at the workspace",
|
||||
});
|
||||
await controller.pushItemEvent(
|
||||
projectAgentToolActivity({ toolCallId: "exec-1", name: "exec", phase: "start" }),
|
||||
);
|
||||
await controller.pushItemEvent(
|
||||
projectAgentToolActivity({ toolCallId: "read-1", name: "read", phase: "start" }),
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(1_500);
|
||||
await controller.flush();
|
||||
expect([...visible.values()]).toEqual([
|
||||
"🧠 _Planning the check_\n💬 Looking at the workspace\n🛠️ Exec: running\n📖 Read: running",
|
||||
]);
|
||||
} finally {
|
||||
await controller.cleanup();
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps one confirmed card after yield and isolates a queued turn's cleanup", async () => {
|
||||
vi.useFakeTimers();
|
||||
const harness = createContinuationHarness({ discordConfig: {} });
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ export function createDiscordDraftPreviewController(params: {
|
|||
seed: progressSeed,
|
||||
reasoningLinePrefix: "🧠 ",
|
||||
commentaryLinePrefix: "💬 ",
|
||||
toolIcons: true,
|
||||
commentaryItalics: false,
|
||||
update: async (previewText, options) => {
|
||||
if (!draftStream) {
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ describe("processDiscordMessage draft streaming final delivery", () => {
|
|||
|
||||
const updates = draftStream.update.mock.calls.map((call) => call[0]);
|
||||
expect(updates).toContain(
|
||||
"Reading the gateway config and restarting agents.\n\n• Exec: running",
|
||||
"Reading the gateway config and restarting agents.\n\n🛠️ Exec: running",
|
||||
);
|
||||
expectFinalAnswerText("done");
|
||||
});
|
||||
|
|
@ -281,7 +281,7 @@ describe("processDiscordMessage draft streaming final delivery", () => {
|
|||
await runProcessDiscordMessage(ctx);
|
||||
|
||||
expect(getLastDispatchReplyOptions()?.sourceReplyDeliveryMode).toBe("message_tool_only");
|
||||
expect(draftStream.update).toHaveBeenCalledWith("Working\n\n• Exec: running\n• exec done", {
|
||||
expect(draftStream.update).toHaveBeenCalledWith("Working\n\n🛠️ Exec: running\n• exec done", {
|
||||
complete: true,
|
||||
});
|
||||
expect(deliverDiscordReply).not.toHaveBeenCalled();
|
||||
|
|
@ -611,7 +611,9 @@ describe("processDiscordMessage draft streaming progress", () => {
|
|||
await runProgressScenario({ toolProgress: true, label: "Investigating", commandText: "raw" });
|
||||
|
||||
expect(draftStream.retarget).toHaveBeenCalledWith("thread-1");
|
||||
expect(draftStream.update).toHaveBeenLastCalledWith("Investigating\n\n• Checked the pipeline.");
|
||||
expect(draftStream.update).toHaveBeenLastCalledWith(
|
||||
"Investigating\n\n🛠️ Checked the pipeline.",
|
||||
);
|
||||
expect(draftStream.messageId()).toBeDefined();
|
||||
expect(deliverDiscordReply).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
@ -764,7 +766,7 @@ describe("processDiscordMessage draft streaming reasoning", () => {
|
|||
|
||||
await runProgressScenario({ toolProgress: true, label: "Clawing..." });
|
||||
|
||||
expect(draftStream.update).toHaveBeenCalledExactlyOnceWith("Clawing...\n\n• Apply Patch", {
|
||||
expect(draftStream.update).toHaveBeenCalledExactlyOnceWith("Clawing...\n\n🩹 Apply Patch", {
|
||||
complete: true,
|
||||
});
|
||||
});
|
||||
|
|
@ -786,7 +788,7 @@ describe("processDiscordMessage draft streaming reasoning", () => {
|
|||
|
||||
await runProgressScenario({ toolProgress: true, label: "Clawing..." });
|
||||
|
||||
expect(draftStream.update).toHaveBeenCalledWith("Clawing...\n\n• Exec: running\n• done", {
|
||||
expect(draftStream.update).toHaveBeenCalledWith("Clawing...\n\n🛠️ Exec: running\n• done", {
|
||||
complete: true,
|
||||
});
|
||||
expect(draftStream.update.mock.calls.map((call) => call[0]).join("\n")).not.toContain(
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export function retainDiscordProgressDraft(params: {
|
|||
active: true,
|
||||
seed: params.seed,
|
||||
reasoningGate: false,
|
||||
toolIcons: true,
|
||||
initialSnapshot: params.snapshot,
|
||||
update: async (text, options) => {
|
||||
if (retired) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue