Skip to content

fix(slack): render agent markdown + drop stale pill on Claude end_turn#84

Open
rogeriochaves wants to merge 6 commits into
mainfrom
feat/slack-gfm-to-mrkdwn
Open

fix(slack): render agent markdown + drop stale pill on Claude end_turn#84
rogeriochaves wants to merge 6 commits into
mainfrom
feat/slack-gfm-to-mrkdwn

Conversation

@rogeriochaves
Copy link
Copy Markdown
Contributor

Summary

  • Claude and Codex emit GitHub-flavoured Markdown in assistant prose (`bold`, `label`, `# heading`, `italic`, …). Slack renders the `text` field as mrkdwn by default, but Slack mrkdwn uses a different dialect — `x` is bold, `x` is italic, headings don't exist, links look like `<url|label>`. So today agent prose shows the literal Markdown punctuation in Slack instead of being rendered.
  • Add `gfmToSlackMrkdwn(text)` and call it on assistant-prose paths in both the Claude transcript formatter and the Codex rollout formatter (`agent_message`). Translation covers bold (`**` and `__`), italic (`*` → `_`), strikethrough, ATX headings, and links.
  • Fenced code blocks and inline code spans are preserved verbatim so Markdown-looking content inside code is not touched. Static authored messages (the `:warning: Codex is out of credits…` post) are intentionally not run through the converter.

Unit tests cover the round-trip and the tricky ordering pitfall (must replace `bold` BEFORE the single-`*` italic pass, otherwise the intermediate `bold` gets re-mangled into `bold`).

Test plan

  • `pnpm test` — 254 passed, including new `gfmToSlackMrkdwn` cases for bold/italic/heading/link/strike/code-block/inline-code/realistic-paragraph
  • After deploy: agent prose that contains `bold` and `label` renders as actual bold and an actual hyperlink in the agent Slack channels

Claude and Codex emit GitHub-flavoured Markdown in their assistant prose
(`**bold**`, `[label](url)`, `# heading`, `*italic*`, …). Slack renders
the `text` field as mrkdwn by default, but Slack mrkdwn is a different
dialect — `*x*` is bold, `_x_` is italic, headings don't exist, links
look like `<url|label>`. So today the agent's `**bold**` shows up in
Slack as literal asterisks around the word; lists, headings and links
are similarly broken.

Add `gfmToSlackMrkdwn(text)` and call it on assistant-prose paths in
both the Claude transcript formatter and the Codex rollout formatter
(`agent_message`). Translation covers bold (`**` and `__`), italic
(`*` → `_`), strikethrough, ATX headings, and links. Fenced code blocks
and inline code spans are preserved verbatim so Markdown-looking content
inside code stays untranslated.

The static `:warning: Codex is out of credits…` post is intentionally
unchanged — it isn't agent output, just a fixed string we author.

Unit-tested round-trips, ordering pitfalls (bold-before-italic so
`**x**` doesn't get mis-italicised), and code-block / inline-code
preservation.
…turn)

After PR #82 only the codex out-of-credits sentinel set `terminal: true`,
so the bridge always re-attached the "is working…" pill after every
non-terminal text post. For Claude that meant the pill stayed up forever
after the agent posted its final message and ended the turn — exactly
what you see in the screenshot where the agent's review is posted but
the channel still shows a pill below.

When a transcript-line message carries `stop_reason: "end_turn"` (or
`stop_sequence` / `refusal`), find the last text post we just emitted
from that message and mark it terminal. The bridge already honours that
flag by skipping the pill re-attach. `tool_use` / `max_tokens` /
`pause_turn` stay non-terminal — the agent is still working.

If a turn ends with no text post (e.g. the last block was a tool call),
we leave the pill alone — that's the existing behaviour; rare in
practice, and dropping a pill mid-tool would be more confusing than the
two-minute Slack TTL it relies on otherwise.
@rogeriochaves rogeriochaves changed the title feat(slack): translate GFM markdown to Slack mrkdwn before posting fix(slack): render agent markdown + drop stale pill on Claude end_turn Jun 1, 2026
The pre-Escape sleep was 100ms. That's long enough for the kanban CLI
to print its confirmation and exit, but not always long enough for
Claude Code to finish processing the Bash tool result and return its
prompt to a state that accepts `/compact` as a slash command. Symptom:
the detached shell does Escape -> /compact -> Enter as designed, but
Claude has not yet redrawn the prompt, so the paste lands in a
transitional state and the compact never actually fires — the session
just sits with the interrupt acknowledgement and never compacts.

Bump the pre-Escape sleep to 2s. The follow-up note still uses the
caller-provided delay (default 1s), unchanged. Worst case the operator
sees a 2s window between calling self-compact and Claude actually
showing /compact; in practice the agent that ran self-compact is
already done speaking, so the extra latency is invisible.
The pill only went up once the assistant posted its first text reply,
which left the channel looking idle for the 10–60s+ between someone
typing in Slack and the agent's first observable output (tool calls
happen first, and tool/thinking blocks are buffered rather than posted
top-level). Operators reported "I don't get the badge after sending
my prompt, only after the first reply arrives".

Anchor the pill to the user's own Slack message ts immediately after
`pasteTmuxPrompt`, so the channel reflects "agent is processing this"
as soon as the relay lands. The existing prevPill-clear path moves the
pill to the assistant's reply the moment it arrives, and the end_turn
terminal flag drops it cleanly at the end of the turn.

Codex/kanban-CLI nudges already get a pill via the announce path —
this only fixes the human-typed-in-Slack case which had no equivalent.
Previous commit bumped the detached self-compact shell's pre-Escape
sleep from 100ms to 2s. The self-compact CLI tests waited only 0.8s /
0.4s before reading the fake-tmux log file, so the assertions ran
before the shell had reached the `send-keys Escape` step and saw only
the earlier `display-message -p #S` line. Bump to 2.8s / 2.4s.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant