Skip to content

feat: add modal-surface builder mode#9

Merged
zhawtof merged 2 commits into
mainfrom
claude/wonderful-heisenberg-77b2a9
May 16, 2026
Merged

feat: add modal-surface builder mode#9
zhawtof merged 2 commits into
mainfrom
claude/wonderful-heisenberg-77b2a9

Conversation

@zhawtof
Copy link
Copy Markdown
Contributor

@zhawtof zhawtof commented May 16, 2026

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 with clear and 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 — enabled interactivity (request URL /slack/events), added im:write to bot scopes so the bot can DM the installer.
  • wrangler.jsonc / package.json setup:kv — new SLACK_MODAL_VIEWS KV namespace (7-day TTL).
  • src/worker/index.ts:
    • Custom authorize() resolves the bot token per request out of SLACK_INSTALLATIONS by team_id on the payload.
    • slack.action("bkb_open_modal", …) → loads the stored view by id from SLACK_MODAL_VIEWS, calls views.open with the fresh trigger_id.
    • slack.viewSubmission("bkb_modal_v1", …) → acks with clear and DMs the submitter a captured-state summary.
    • New POST /api/slack/modals/send — validates blocks as surface: "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 as value.
  • 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.json now uses wrangler dev instead of pnpm dev. The bundled vite + @cloudflare/vite-plugin runner trips EvalError: Code generation from strings disallowed because workerd's dev sandbox blocks new Function, which ajv (used by slack-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 at wrangler dev (real workerd) bypasses it. Worth deciding separately whether pnpm dev itself should switch.
  • Channel-picker hack for modal mode. The upstream BlockKitBuilder send dialog requires a channel pick and its onSend payload doesn't carry a surface field. To keep one dialog, modal mode's loadChannels returns a single placeholder option ({id: "__dm__", name: "Direct message (app Messages tab)"}) which the worker ignores — the destination is always the cookie-identified installer.
  • View storage is keyed by a short opaque id (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 typecheck clean
  • pnpm run build clean (worker + client)
  • wrangler dev boots; UI loads with Message/Modal tabs
  • Switching to Modal tab updates aria-selected and shows the hint
  • POST /api/slack/modals/send returns 401 Not installed yet without a cookie
  • End-to-end: install app → compose modal → Send → click Open modal in DM → submit → confirmation DM lands

🤖 Generated with Claude Code

zhawtof and others added 2 commits May 15, 2026 20:07
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>
@zhawtof zhawtof merged commit c177fab into main May 16, 2026
1 check passed
@zhawtof zhawtof deleted the claude/wonderful-heisenberg-77b2a9 branch May 16, 2026 00:21
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