Skip to content

Commit 8acca9e

Browse files
Give a successful viz reply a link that renders the diagram
Slack shows raw Mermaid as text. The reply now ends with a mermaid.live link carrying the whole diagram zlib-compressed in the URL fragment, which a browser never sends to any server — the site's JavaScript renders it locally, so following the link ships the diagram to no one. Only a `viz` that did its job gets the line; failures and every other command are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 723a177 commit 8acca9e

3 files changed

Lines changed: 61 additions & 1 deletion

File tree

docs/cookbook/07-slack.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ Output is the CLI's piped-mode bytes in a code fence. stdout in the bot is a
3333
pipe, so by the CLI's own contract there is no colour to strip and the bytes
3434
match what `grapharc … | cat` prints on the host.
3535

36+
A successful `viz` reply carries one extra line: a *render this diagram* link.
37+
The whole diagram is zlib-compressed into the URL fragment, which a browser
38+
never sends to any server — mermaid.live's JavaScript renders it locally, so
39+
following the link ships the diagram to no one.
40+
3641
## Slack app setup (once, ~5 minutes)
3742

3843
1. <https://api.slack.com/apps>**Create New App***From a manifest*, pick

grapharc/slack/format.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313

1414
from __future__ import annotations
1515

16+
import base64
17+
import json
1618
import shlex
19+
import zlib
1720

1821
from grapharc.slack.runner import CommandResult
1922

@@ -39,6 +42,19 @@ def _truncate(body: str) -> tuple[str, int]:
3942
return body[:MAX_FENCE_CHARS], len(body) - MAX_FENCE_CHARS
4043

4144

45+
def mermaid_live_url(code: str) -> str:
46+
"""A mermaid.live link with the whole diagram compressed into the fragment.
47+
48+
The editor's `#pako:` form: zlib-deflated JSON, base64url. Everything after
49+
the `#` is a URL fragment, which a browser never sends to the server — the
50+
site's JavaScript renders the diagram locally, so following the link ships
51+
the diagram to no one.
52+
"""
53+
payload = json.dumps({"code": code, "mermaid": {"theme": "default"}})
54+
packed = base64.urlsafe_b64encode(zlib.compress(payload.encode())).decode()
55+
return f"https://mermaid.live/view#pako:{packed}"
56+
57+
4258
def format_result(result: CommandResult) -> str:
4359
shown = shlex.join(["grapharc", *result.argv])
4460
if result.exit_code is None:
@@ -64,4 +80,8 @@ def format_result(result: CommandResult) -> str:
6480
parts.append(f"_…{cut} more characters not shown._")
6581
if len(parts) == 1:
6682
parts.append("_(no output)_")
83+
# `viz` prints raw Mermaid, which Slack shows as text. One extra line makes
84+
# it a diagram: a link that renders it in the browser, locally.
85+
if result.argv[:1] == ["viz"] and result.exit_code == 0 and result.stdout.strip():
86+
parts.append(f"<{mermaid_live_url(result.stdout.strip())}|render this diagram>")
6787
return "\n".join(parts)

tests/test_slack_gateway.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from grapharc.slack.command import SlackCommandError, parse_command, usage_text
1717
from grapharc.slack.config import SlackBotConfig, SlackConfigError
18-
from grapharc.slack.format import MAX_FENCE_CHARS, format_result
18+
from grapharc.slack.format import MAX_FENCE_CHARS, format_result, mermaid_live_url
1919
from grapharc.slack.runner import CommandResult, run_command
2020

2121
# ---------------------------------------------------------------------------
@@ -147,6 +147,41 @@ def test_a_fence_in_the_output_cannot_break_out():
147147
assert "\n```\n" not in body.rsplit("```", 1)[0]
148148

149149

150+
def test_a_successful_viz_gets_a_render_link_and_the_url_round_trips():
151+
import base64
152+
import json
153+
import zlib
154+
155+
mermaid = 'flowchart TD\n start((start)) --> n0["triage"]'
156+
result = CommandResult(
157+
argv=["viz", "t.jsonl", "r1"],
158+
exit_code=0,
159+
stdout=mermaid + "\n",
160+
stderr="",
161+
duration_seconds=0.1,
162+
timeout_seconds=60,
163+
)
164+
message = format_result(result)
165+
assert "mermaid.live/view#pako:" in message
166+
167+
packed = mermaid_live_url(mermaid).split("#pako:", 1)[1]
168+
decoded = json.loads(zlib.decompress(base64.urlsafe_b64decode(packed)))
169+
assert decoded["code"] == mermaid
170+
171+
172+
def test_a_failed_or_non_viz_command_gets_no_render_link():
173+
for argv, code in ((["viz", "t.jsonl", "r1"], 1), (["trace", "t.jsonl"], 0)):
174+
result = CommandResult(
175+
argv=argv,
176+
exit_code=code,
177+
stdout="flowchart TD",
178+
stderr="",
179+
duration_seconds=0.1,
180+
timeout_seconds=60,
181+
)
182+
assert "mermaid.live" not in format_result(result)
183+
184+
150185
# ---------------------------------------------------------------------------
151186
# Config and the bot's import posture.
152187
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)