fix: stop stripping header level from sent Block Kit payloads - #180
Merged
Conversation
`toSlackBlocks` deleted the `level` field from every header block on the way out, on the assumption that it was a builder-only extension Slack would reject. That assumption was wrong: `level` is a real Slack field on the header block — an optional integer 1-4 — so the builder silently dropped the heading level from every message it sent. The bug was invisible right up to the send. The preview and the JSON drawer both read the working draft, which still carries `level`; only the payload handed to `chat.postMessage` / `chat.update` had it removed. Confirmation that `level` is legitimate: `@tightknitai/slack-block-kit-validator` lists it among the header block's allowed keys under `additionalProperties: false` and validates it as an integer with minimum 1 / maximum 4. An unknown property would be rejected outright. Drop the header carve-out and keep `sanitizeBlock` — the URL scrubbing and retrieval-only-key stripping are unrelated and still needed. Since validation runs on `toSlackBlocks(...)` output, `level` now reaches the validator for the first time: values 1-4 pass, and out-of-range values correctly surface as validation errors instead of being discarded. Also corrects the JSDoc, the `HeaderLevel` type docs, the editor help text, and the README, all of which repeated the same wrong assumption. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Ao4W1Fn3JoatgX3RS6h68
Contributor
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
block-kitchen | cfba02f | Commit Preview URL Branch Preview URL |
Aug 14 2026, 06:06 AM |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
toSlackBlocksdeleted thelevelfield from every header block on its way out, so the builder silently dropped the heading level from every message it sent.levelis a real Slack field, not a builder-only extension — this removes the carve-out and corrects the docs, type comments, editor help text, and README that repeated the wrong assumption.Why
The strip happened in
src/lib/to-slack-blocks.ts, one step before the payload leaves the builder:The JSDoc explained it as "the cosmetic
levelfield from header blocks (a builder-only extension Slack would reject)", andHeaderEditorrepeated it in the UI: "Optional. Builder-only extension; Slack's API ignores this value."That assumption is wrong. Slack's header block accepts an optional
levelinteger 1–4. This package's own validator dependency agrees —@tightknitai/slack-block-kit-validator@0.1.13listslevelamong the header block's allowed keys underadditionalProperties: false:Under
additionalProperties: falsean unknown property would be rejected outright, so the schema treatslevelas legitimate. Confirmed against the resolved dependency at runtime — levels 1–4 validate, 0 and 5 producebelow minimum 1/above maximum 4.The bug was invisible right up to the send, which is what made it hard to spot: the preview renders from the working draft (via
sanitizeBlock, nottoSlackBlocks), and the JSON drawer stringifies the working draft directly — both still showedlevel. Only the payload handed to the send/update callbacks had it removed.sanitizeBlockis kept as-is; the URL scrubbing and retrieval-only-key stripping are unrelated and still needed.One behavior change worth noting
Validation runs on
toSlackBlocks(...)output, solevelnow reaches the validator for the first time. Values 1–4 pass and the send CTA stays enabled. An out-of-range value that previously vanished silently now surfaces as a validation error (blocks[0].level: above maximum 4) — the intended outcome, but it means a draft carrying e.g.level: 6will report an error where it previously did not. The editor only ever offers 1–4, so this affects payloads supplied programmatically or loaded from URL state.Test plan
pnpm typecheckpnpm lintpnpm test— 38 files, 452 tests passing (both theunitandstorybookbrowser projects)Test changes:
test/public-api.test.ts— the existing case asserted the buggy behavior (strips the builder-only 'level' field), so it is inverted to assert preservation. Added cases covering the full 1–4 range against the real validator, the out-of-range error path, and that a header with nolevelis returned reference-unchanged.src/components/editors/block-editor.stories.tsx— newSelectingHeaderLevelProducesValidBlockinteraction story clicking the H2 radio and asserting the level survivestoSlackBlocksinto a valid payload, covering the editor → wire path end to end.Notes for reviewer
Scoped to this package. The originating report also asked to bump
@tightknitai/block-kitcheninapps/web-studio-startand update a now-stale comment atsend-message-dialog.tsx:272-274— that lives in another repo and needs this released first, so it is not included here. The stopgap suggested there (re-attachinglevelaftertoSlackBlocksin the consumer) should not be needed once this ships, and would leave the package's built-inSendDialogstill dropping the field.One environment note in case CI or a local run trips on it: the
storybookbrowser project wants a Playwright build (chromium_headless_shell-1234) that this sandbox did not have, andcdn.playwright.devis blocked here. I ran that project against the locally installed Chromium to verify it — all 118 of its tests pass, including the new story. Unrelated to this diff, but it is why a fresh environment may needpnpm exec playwright installfirst.Generated by Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.