Files
Peter Steinberger 508dd471b0 feat(slack): live session cards as the default progress mode (#122552)
* feat(slack): render live session cards as the default Slack progress mode

Slack streaming.mode default flips partial->progress.
Progress mode renders one live Block Kit session card with a status header, narration, plan, activity, diff stat, and elapsed time; it is edited in place and finalized to success or error with an Open in OpenClaw button when gateway.publicOrigin is set.
Final assistant text always delivers separately.
The shared progress compositor gains a success-only additive per-turn diffStat mirroring the task ledger fold.
resolveGatewayPublicOrigin is exported through the plugin SDK.
The diffs viewer URL falls back to publicOrigin.
The old rich/text progress render fork is deleted.
Native task cards remain unchanged and opt-in.

* chore(config): regenerate bundled channel config metadata

* refactor(slack): keep session card state type internal

* refactor(slack): split session-card and diff-stat owners under lint ceilings

* refactor(channels): reuse diff-stat type from its owner module

Import ChannelProgressDraftDiffStat from progress-draft-diffstat instead of
redeclaring it in the compositor, resolving the all-exports deadcode scan.

* chore(plugin-sdk): regenerate api baselines for channel barrels

Baselines drifted after the rebase reconciled them against main; regenerate to
match the branch's actual channel-message/channel-outbound surface.

* fix(slack): drop a session card that cannot terminalize after final delivery

If the final reply is delivered but the terminal card edit fails, the caller
now clears the stale card instead of leaving it stuck in its Working state
(mirrors the pre-card preview cleanup). Adds a transport-failure regression and
corrects three tests that asserted the prior ignore-the-result behavior.

Documents resolveGatewayPublicOrigin as a dependency-light runtime helper on the
config-contracts SDK subpath, which previously described a type-only surface.

Addresses ClawSweeper P2 (unfinalized card) and P1 (runtime SDK contract).

* fix(slack): suppress default tool messages under the default progress card

resolveChannelStreamingSuppressDefaultToolProgressMessages re-derived the stream
mode from config with an "off" default, unlike its sibling resolvers which take
a caller-resolved mode override. After this branch made progress the Slack
default, a default-config channel turn saw mode "off" and left a stray
"Using tool: X" plain message posting alongside the session card. Thread the
caller-resolved mode through (compositor passes params.mode; Slack dispatch passes
slackStreaming.mode), matching resolveChannelStreamingPreviewToolProgress.

Retarget the progress-session-card delivery-trace golden at an EMPTY Slack config
so it proves the real default path; the regenerated golden is byte-identical,
confirming defaults now yield the clean card sequence (one card post, separate
final text, one terminal update with the Open in OpenClaw button, no stray tool
message). Switch the dispatch delivery-mode mock to the real resolver so the card
tests exercise the true channel default (automatic), not a hand-rolled one.

* chore(plugin-sdk): regenerate api baselines for the streaming mode param

resolveChannelStreamingSuppressDefaultToolProgressMessages gained an optional
mode override; the changed signature reflows the surface hash of every barrel
that re-exports it, so regenerate the affected baselines.

* chore(config): regenerate config baselines
2026-08-12 10:38:31 -07:00

403 lines
15 KiB
Markdown

---
summary: "Read-only diff viewer and file renderer for agents (optional plugin tool)"
title: "Diffs"
sidebarTitle: "Diffs"
read_when:
- You want agents to show code or markdown edits as diffs
- You want a canvas-ready viewer URL or a rendered diff file
- You need controlled, temporary diff artifacts with secure defaults
---
`diffs` is an optional bundled plugin tool that turns before/after text or a unified patch into a read-only diff artifact. It also prepends short agent guidance into the system prompt and ships a companion skill for fuller instructions.
Input: `before` + `after` text, or a unified `patch` (mutually exclusive).
Output: a gateway viewer URL for canvas presentation, a rendered PNG/PDF file path for message delivery, or both.
## Quick start
<Steps>
<Step title="Install the plugin">
```bash
openclaw plugins install diffs
```
</Step>
<Step title="Enable the plugin">
```json5
{
plugins: {
entries: {
diffs: {
enabled: true,
},
},
},
}
```
</Step>
<Step title="Pick a mode">
<Tabs>
<Tab title="view">
Canvas-first flows: agents call `diffs` with `mode: "view"` and open `details.viewerUrl` with `canvas present`.
</Tab>
<Tab title="file">
Chat file delivery: agents call `diffs` with `mode: "file"` and send `details.filePath` with `message` using `path` or `filePath`.
</Tab>
<Tab title="both">
Combined (default): agents call `diffs` with `mode: "both"` to get both artifacts in one call.
</Tab>
</Tabs>
</Step>
</Steps>
## Disable built-in system guidance
To keep the tool but drop the prepended system-prompt guidance, set `plugins.entries.diffs.hooks.allowPromptInjection` to `false`:
```json5
{
plugins: {
entries: {
diffs: {
enabled: true,
hooks: {
allowPromptInjection: false,
},
},
},
},
}
```
This blocks the plugin's `before_prompt_build` hook while keeping the tool and skill available. To disable both guidance and the tool, disable the plugin instead.
## Tool input reference
All fields are optional unless noted.
<ParamField path="before" type="string">
Original text. Required with `after` when `patch` is omitted.
</ParamField>
<ParamField path="after" type="string">
Updated text. Required with `before` when `patch` is omitted.
</ParamField>
<ParamField path="patch" type="string">
Unified diff text. Mutually exclusive with `before` and `after`.
</ParamField>
<ParamField path="path" type="string">
Display filename for before/after mode.
</ParamField>
<ParamField path="lang" type="string">
Language override hint for before/after mode. Unknown values and languages outside the default viewer set fall back to plain text unless the
Diff Viewer Language Pack plugin is installed.
</ParamField>
<ParamField path="title" type="string">
Viewer title override.
</ParamField>
<ParamField path="mode" type='"view" | "file" | "both"'>
Output mode. Defaults to plugin default `defaults.mode` (`both`). Deprecated alias: `"image"` behaves identically to `"file"`.
</ParamField>
<ParamField path="theme" type='"light" | "dark"'>
Viewer theme. Defaults to plugin default `defaults.theme`.
</ParamField>
<ParamField path="layout" type='"unified" | "split"'>
Diff layout. Defaults to plugin default `defaults.layout`.
</ParamField>
<ParamField path="expandUnchanged" type="boolean">
Expand unchanged sections when full context is available. Per-call option only (not a plugin default key).
</ParamField>
<ParamField path="fileFormat" type='"png" | "pdf"'>
Rendered file format. Defaults to plugin default `defaults.fileFormat`.
</ParamField>
<ParamField path="fileQuality" type='"standard" | "hq" | "print"'>
Quality preset for PNG/PDF rendering.
</ParamField>
<ParamField path="fileScale" type="number">
Device scale override (`1`-`4`).
</ParamField>
<ParamField path="fileMaxWidth" type="number">
Max render width in CSS pixels (`640`-`2400`).
</ParamField>
<ParamField path="ttlSeconds" type="number" default="1800">
Artifact TTL in seconds for viewer and standalone file outputs. Max `21600`.
</ParamField>
<ParamField path="baseUrl" type="string">
Viewer URL origin override. Overrides plugin `viewerBaseUrl`. Must be `http` or `https`, no query/hash.
</ParamField>
<AccordionGroup>
<Accordion title="Validation and limits">
- `before`/`after`: max 512 KiB each.
- `patch`: max 2 MiB.
- `path`: max 2048 bytes.
- `lang`: max 128 bytes.
- `title`: max 1024 bytes.
- Patch complexity cap: max 128 files and 120000 total lines.
- `patch` together with `before`/`after` is rejected.
- Rendered file safety limits (PNG and PDF):
- `fileQuality: "standard"`: max 8 MP (8,000,000 rendered pixels).
- `fileQuality: "hq"`: max 14 MP.
- `fileQuality: "print"`: max 24 MP.
- PDF also caps at 50 pages.
</Accordion>
</AccordionGroup>
## Syntax highlighting
Built-in languages:
`javascript`, `typescript`, `tsx`, `jsx`, `json`, `markdown`, `yaml`, `css`, `html`, `sh`, `python`, `go`, `rust`, `java`, `c`, `cpp`, `csharp`, `php`, `sql`, `docker`, `ruby`, `swift`, `kotlin`, `r`, `dart`, `lua`, `powershell`, `xml`, and `toml`.
Common aliases (`js`, `ts`, `bash`, `md`, `yml`, `c++`, `dockerfile`, `rb`, `kt`, `ps1`, etc.) normalize to those languages.
Install the Diff Viewer Language Pack plugin for more languages (Astro, Vue, Svelte, MDX, GraphQL, Terraform/HCL, Nix, Clojure, Elixir, Haskell, OCaml, Scala, Zig, Solidity, Verilog/VHDL, Fortran, MATLAB, LaTeX, Mermaid, Sass/Less/SCSS, Nginx, Apache, CSV, dotenv, INI, diff, and more):
```bash
openclaw plugins install clawhub:@openclaw/diffs-language-pack
```
Without the pack, unsupported languages still render as readable plain text. See [Diffs Language Pack plugin](/plugins/reference/diffs-language-pack) and [Shiki languages](https://shiki.style/languages) for the upstream catalog.
## Output details contract
All successful results include `changed`: identical before/after input returns `false` without creating an artifact; rendered results return `true`.
<AccordionGroup>
<Accordion title="Viewer fields (view and both modes)">
- `changed`
- `artifactId`
- `viewerUrl`
- `viewerPath`
- `title`
- `expiresAt`
- `inputKind`
- `fileCount`
- `mode`
- `context` (`agentId`, `sessionId`, `messageChannel`, `agentAccountId` when available)
</Accordion>
<Accordion title="File fields (file and both modes)">
- `changed`
- `artifactId`
- `expiresAt`
- `filePath`
- `path` (same value as `filePath`, for message tool compatibility)
- `fileBytes`
- `fileFormat`
- `fileQuality`
- `fileScale`
- `fileMaxWidth`
</Accordion>
</AccordionGroup>
| Mode | Returns |
| -------- | ----------------------------------------------------------------------------------------------- |
| `"view"` | Viewer fields only. |
| `"file"` | File fields only, no viewer artifact. |
| `"both"` | Viewer fields plus file fields. If file rendering fails, viewer still returns with `fileError`. |
### Collapsed unchanged sections
The viewer shows rows like `N unmodified lines`. Expand controls only appear when the rendered diff has expandable context data (typical for before/after input). Many unified patches omit context bodies in their hunks, so the row can appear without an expand control -- expected, not a bug. `expandUnchanged` only applies when expandable context exists.
### Multi-file navigation
Patches that touch more than one file start with a changed-files summary card: total `+N` / `-N` counts, per-file counts, added/deleted/renamed badges, and anchor links that jump to each file. Rendered PNG/PDF files keep the per-file header counts but drop the interactive view toggles, since those are dead controls in a static file.
## Plugin defaults
Set plugin-wide defaults in `~/.openclaw/openclaw.json`:
```json5
{
plugins: {
entries: {
diffs: {
enabled: true,
config: {
defaults: {
fontFamily: "Fira Code",
fontSize: 15,
lineSpacing: 1.6,
layout: "unified",
showLineNumbers: true,
diffIndicators: "bars",
wordWrap: true,
background: true,
theme: "dark",
fileFormat: "png",
fileQuality: "standard",
fileScale: 2,
fileMaxWidth: 960,
mode: "both",
ttlSeconds: 21600,
},
},
},
},
},
}
```
Supported `defaults` keys: `fontFamily`, `fontSize`, `lineSpacing`, `layout`, `showLineNumbers`, `diffIndicators`, `wordWrap`, `background`, `theme`, `fileFormat`, `fileQuality`, `fileScale`, `fileMaxWidth`, `mode`, `ttlSeconds`. Explicit tool call parameters override these.
### Persistent viewer URL config
<ParamField path="viewerBaseUrl" type="string">
Plugin-owned fallback for returned viewer links when a tool call does not pass `baseUrl`. Must be `http` or `https`, no query/hash.
</ParamField>
```json5
{
plugins: {
entries: {
diffs: {
enabled: true,
config: {
viewerBaseUrl: "https://gateway.example.com/openclaw",
},
},
},
},
}
```
## Security config
<ParamField path="security.allowRemoteViewer" type="boolean" default="false">
`false`: non-loopback requests to viewer routes are denied. `true`: remote viewers are allowed if the tokenized path is valid.
</ParamField>
```json5
{
plugins: {
entries: {
diffs: {
enabled: true,
config: {
security: {
allowRemoteViewer: false,
},
},
},
},
},
}
```
## Artifact lifecycle and storage
- Viewer HTML and metadata live in the shared `state/openclaw.sqlite` database under the Diffs plugin blob namespace. HTML is gzip-compressed; SQLite stores only a SHA-256 hash of the random URL token, not the token itself.
- Rendered PNG/PDF files remain temporary materializations under `$TMPDIR/openclaw-diffs` because channel delivery requires a file path. SQLite owns their expiry metadata; no JSON sidecars are written.
- Default artifact TTL: 30 minutes. Maximum accepted TTL: 6 hours.
- Cleanup runs opportunistically after each artifact create call. Expired SQLite rows are deleted first, followed by any corresponding PNG/PDF directory.
- A fallback sweep removes rowless temporary folders older than 24 hours. Legacy `meta.json`, `file-meta.json`, and `viewer.html` caches are not imported or read.
## Viewer URL and network behavior
Viewer route: `/plugins/diffs/view/{artifactId}/{token}`
Viewer assets:
- `/plugins/diffs/assets/viewer.js`
- `/plugins/diffs/assets/viewer-runtime.js`
- `/plugins/diffs-language-pack/assets/viewer.js` (only when the diff uses a language pack language)
The viewer document resolves these assets relative to the viewer URL, so an optional `baseUrl` path prefix carries through to asset requests too.
URL resolution order: tool-call `baseUrl` (after strict validation) -> plugin `viewerBaseUrl` -> `gateway.publicOrigin` -> the existing bind-aware Gateway fallback.
`baseUrl` rules: must be `http://` or `https://`; query and hash are rejected; origin plus optional base path is allowed.
## Security model
<AccordionGroup>
<Accordion title="Viewer hardening">
- Loopback-only by default.
- Tokenized viewer paths with strict ID and token pattern validation.
- Viewer response CSP: `default-src 'none'`; scripts/assets only from self; no outbound `connect-src`.
- Remote miss throttling when remote access is enabled: 40 failures per 60 seconds triggers a 60-second lockout (`429 Too Many Requests`).
</Accordion>
<Accordion title="File rendering hardening">
- Screenshot browser request routing is deny-by-default.
- Only local viewer assets from `http://127.0.0.1/plugins/diffs/assets/*` are allowed.
- External network requests are blocked.
</Accordion>
</AccordionGroup>
## Browser requirements for file mode
`mode: "file"` and `mode: "both"` need a Chromium-compatible browser.
Resolution order:
<Steps>
<Step title="Config">
`browser.executablePath` in OpenClaw config.
</Step>
<Step title="Environment variables">
- `OPENCLAW_BROWSER_EXECUTABLE_PATH`
- `BROWSER_EXECUTABLE_PATH`
- `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH`
</Step>
<Step title="Platform fallback">
Common install paths and `PATH` lookups for Chrome, Chromium, Edge, and Brave.
</Step>
</Steps>
Common failure text: `Diff PNG/PDF rendering requires a Chromium-compatible browser...`. Fix by installing Chrome, Chromium, Edge, or Brave, or setting one of the executable path options above.
## Troubleshooting
<AccordionGroup>
<Accordion title="Input validation errors">
- `Provide patch or both before and after text.` -- include both `before` and `after`, or provide `patch`.
- `Provide either patch or before/after input, not both.` -- do not mix input modes.
- `Invalid baseUrl: ...` -- use an `http(s)` origin with optional path, no query/hash.
- `{field} exceeds maximum size (...)` -- reduce payload size.
- Large patch rejection -- reduce patch file count or total lines.
</Accordion>
<Accordion title="Viewer accessibility">
- Viewer URL resolves to `127.0.0.1` by default.
- For remote access, set `gateway.publicOrigin`, set plugin `viewerBaseUrl`, or pass `baseUrl` per call.
- If `gateway.trustedProxies` includes loopback for a same-host proxy (for example Tailscale Serve), raw loopback viewer requests without forwarded client-IP headers fail closed by design.
- For that proxy topology, prefer `mode: "file"`/`"both"` for an attachment, or intentionally enable `security.allowRemoteViewer` plus plugin `viewerBaseUrl`/a proxy `baseUrl` for a shareable viewer link.
- Enable `security.allowRemoteViewer` only when external viewer access is intended.
</Accordion>
<Accordion title="Unmodified-lines row has no expand button">
Expected for patch input that lacks expandable context; not a viewer failure.
</Accordion>
<Accordion title="Artifact not found">
- Artifact expired due to TTL.
- Token or path changed.
- Cleanup removed stale data.
</Accordion>
</AccordionGroup>
## Operational guidance
- Prefer `mode: "view"` for local interactive reviews in canvas.
- Prefer `mode: "file"` for outbound chat channels that need an attachment.
- Keep `allowRemoteViewer` disabled unless your deployment requires remote viewer URLs.
- Set an explicit short `ttlSeconds` for sensitive diffs.
- Avoid sending secrets in diff input when not required.
- If your channel compresses images aggressively (for example Telegram or WhatsApp), prefer PDF output (`fileFormat: "pdf"`).
<Note>
Diff rendering engine powered by [Diffs](https://diffs.com).
</Note>
## Related
- [Browser](/tools/browser)
- [Plugins](/tools/plugin)
- [Tools overview](/tools)