feat: add modal-surface builder mode#9
Merged
Conversation
Adds a "Modal" tab beside the existing message builder. Sending a modal
DMs the installer a button via the app's Messages tab; clicking it
opens the composed view in Slack with a fresh trigger_id.
- manifest.json: enable interactivity, add im:write to bot scopes
- wrangler.jsonc / setup:kv: new SLACK_MODAL_VIEWS namespace (7d TTL)
- worker: custom authorize() resolves bot token by team_id from KV;
registers slack.action("bkb_open_modal", ...) → views.open and
slack.viewSubmission("bkb_modal_v1", ...) → clear + DM summary
- worker: new POST /api/slack/modals/send validates blocks as
surface "modal", stores view JSON keyed by short id, posts the
open-modal DM to the cookie-identified installer
- client: Message/Modal tabs above the builder swap onSend +
loadChannels; modal mode feeds a single "Direct message" option to
the existing send dialog so the channel-picker UX stays consistent
The bundled vite+@cloudflare/vite-plugin dev runner trips
"Code generation from strings disallowed" via ajv (pre-existing,
unrelated to this change), so .claude/launch.json now points the
local preview at `wrangler dev` (real workerd, allows eval).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`pnpm run setup:doctor` audits the four checkpoints that block running the app end-to-end against Slack — KV ids in wrangler.jsonc, .dev.vars presence and required secrets, manifest URL substitution + interactivity + im:write scope — and prints a punch list with hints. Exits 1 on any missing item so it composes with CI / dev:tunnel preflight. Also warns about reinstalling the app when scopes change, since this template's new im:write bot scope won't be granted by an existing install. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
Adds a Modal tab beside the existing message-builder mode. Composing in modal mode and clicking Send DMs the installer a button via the app's Messages tab; clicking it opens the composed view in Slack with a fresh
trigger_id. Submitting the modal acks withclearand DMs back a summary of the captured state.This is the missing piece for the "support modal button presses" question — modals can't be directly sent (Slack only opens views in response to an interaction), so the DM-with-a-button is the bridge.
What changed
manifest.json— enabledinteractivity(request URL/slack/events), addedim:writeto bot scopes so the bot can DM the installer.wrangler.jsonc/package.jsonsetup:kv— newSLACK_MODAL_VIEWSKV namespace (7-day TTL).src/worker/index.ts:authorize()resolves the bot token per request out ofSLACK_INSTALLATIONSbyteam_idon the payload.slack.action("bkb_open_modal", …)→ loads the stored view by id fromSLACK_MODAL_VIEWS, callsviews.openwith the freshtrigger_id.slack.viewSubmission("bkb_modal_v1", …)→ acks withclearand DMs the submitter a captured-state summary.POST /api/slack/modals/send— validates blocks assurface: "modal", stores{team_id, user_id, blocks, title}in KV under a 16-char id, then DMs the cookie-identified installer a one-button "Open modal" message carrying that id asvalue.src/client/App.tsx+styles.css— Message/Modal tabs above the builder; the builder is re-keyed on mode flip so it resets cleanly. Modal mode swaps in IO that targets the new endpoint and feeds a single Direct message channel option to the dialog so the existing UX still works.Heads-up for reviewers
.claude/launch.jsonnow useswrangler devinstead ofpnpm dev. The bundled vite +@cloudflare/vite-pluginrunner tripsEvalError: Code generation from strings disallowedbecause workerd's dev sandbox blocksnew Function, whichajv(used byslack-block-kit-validator) needs to compile validators. This is pre-existing and unrelated to this PR, but it meant the local preview was unusable; pointing atwrangler dev(real workerd) bypasses it. Worth deciding separately whetherpnpm devitself should switch.BlockKitBuildersend dialog requires a channel pick and itsonSendpayload doesn't carry a surface field. To keep one dialog, modal mode'sloadChannelsreturns a single placeholder option ({id: "__dm__", name: "Direct message (app Messages tab)"}) which the worker ignores — the destination is always the cookie-identified installer.crypto.randomUUID().slice(0, 16)), 7-day TTL, scoped to one team+user. Stale clicks past TTL get a friendly "expired, compose a new one" DM.Test plan
pnpm run typecheckcleanpnpm run buildclean (worker + client)wrangler devboots; UI loads with Message/Modal tabsaria-selectedand shows the hintPOST /api/slack/modals/sendreturns401 Not installed yetwithout a cookie🤖 Generated with Claude Code