Skip to content

feat(message): compose editor + Slack-native drafts (#83)#119

Merged
nwparker merged 1 commit into
mainfrom
feat/message-compose-native-drafts
Jul 18, 2026
Merged

feat(message): compose editor + Slack-native drafts (#83)#119
nwparker merged 1 commit into
mainfrom
feat/message-compose-native-drafts

Conversation

@nwparker

@nwparker nwparker commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Implements #83. Supersedes #112 — reuses that PR's Slack drafts.* API layer, but takes the "compose" direction (option 2) from the issue instead of the plural message drafts subcommand.

What & why

The issue flagged that adding Slack-native draft ops under the existing draft name would confuse an LLM. #112's answer was a near-homograph pair — message draft (browser editor) vs message drafts (native) — one letter apart, opposite meanings. This PR instead gives each concept a distinct verb/namespace:

  • message compose <target> [text] — the human-in-the-loop browser editor (renamed from message draft; behavior identical). "Compose" matches what it does.
  • message draft <list|create|update|delete> — Slack-native drafts via the undocumented drafts.* session endpoints, so drafts show up natively in the user's Slack client (desktop + mobile).
agent-slack message compose "#general" "Here's my update"      # opens browser editor

agent-slack message draft list
agent-slack message draft create "#general" "Here's my update"
agent-slack message draft create "<msg-url>" "Looking into it"    # thread reply
agent-slack message draft create "W0123456789" "hi"              # DM draft
agent-slack message draft update "<draft-id>" "Revised text"
agent-slack message draft delete "<draft-id>"
  • create accepts the usual targets: #channel/name, channel id, user id (opens a DM), or a message URL (drafts a thread reply — same semantics as message send).
  • Draft text runs through the existing mrkdwn → rich_text conversion, so lists/bold/code/mentions render natively.
  • update replaces the body but preserves the draft's destination and attached files unless overridden; the conflict-detection client_last_updated_ts is auto-fetched for update/delete when omitted (incl. Slack's 7-digit fractional-padding quirk).
  • Native drafts require browser-style auth (xoxc/xoxd), like later/unreads; noted in the docs.

Broadcast semantics (thread replies)

--broadcast ("also send to channel") is handled carefully so it can't silently do the wrong thing:

  • inherited on a text-only update (editing text keeps your broadcast choice), but reset on any --channel re-address so a reply is never ratcheted into a different thread's channel;
  • clearable with a new --no-broadcast flag;
  • rejected for DM targets (matching message send).

Refuses unsafe updates

update fails loudly instead of corrupting drafts it can't faithfully round-trip: a scheduled draft (would drop the send time) or a multi-destination draft edited text-only (would drop recipients). It points the user to edit in Slack or delete + recreate.

⚠️ Breaking change (intentional, no compat shim)

message draft <target> [text] no longer opens the editor — use message compose. Old usage errors cleanly (error: unknown command '#general'), and message draft --help points to message compose. Deliberate clean break (pre-1.0, v0.9.x); the bundled agent skill is updated in the same PR, so agents pick up the new surface immediately. No magic dispatch / dual-meaning draft.

Structure

Testing

  • test/drafts.test.ts (API layer, from feat(message): Slack-native drafts — message drafts list/create/update/delete #112) + new test/message-draft-actions.test.ts (CLI action layer + a commander-level test locking --broadcast/--no-broadcast/omitted parsing): create, update read-modify-write, scheduled/multi-destination guards, broadcast re-address + DM guards, last_updated_ts auto-fetch/pad, findDraft malformed-duplicate handling, channel-name hydration.
  • Full suite (284), typecheck, lint (0 errors), format, and build all pass. Verified CLI wiring for compose/draft/all subcommands, and that old message draft <target> usage errors cleanly.

Review

Reviewed by independent correctness + docs sub-agents, then an adversarial loop with GPT-5.6-Sol (xhigh) iterated to convergence. It surfaced and drove fixes for several real issues in the broadcast/thread/DM handling (broadcast one-way ratchets on re-address, DM broadcast via D… ids/URLs, and error-ordering that reported red-herring lookup failures). A couple of findings were pre-existing repo-wide concerns already owned by other PRs (rendered usergroup/broadcast text → #113) or resolved by rebasing onto current main (W-prefixed user IDs → #114).

Closes #83.

Credit: Slack drafts.* API layer originally by @mikaelq in #112.

🤖 Reviewed with GPT-5.6-Sol; generated with Claude Code.

Made with Orca 🐋

@nwparker
nwparker force-pushed the feat/message-compose-native-drafts branch 2 times, most recently from 959cb09 to 91a9cd7 Compare July 18, 2026 06:53
Rename the browser-editor command `message draft` → `message compose`, and
add Slack-native drafts under `message draft <list|create|update|delete>`,
backed by Slack's undocumented drafts.* session endpoints so drafts show up
natively in the user's Slack client.

Takes the "compose" direction (option 2 from #83) so the two concepts get
distinct names instead of the near-homograph draft/drafts pair. Reuses the
drafts.* API layer from #112 (originally by @mikaelq).

Hardening in the update path:
- refuses drafts it can't faithfully round-trip (scheduled or multi-
  destination) instead of silently dropping the schedule/recipients;
- broadcast is inherited only when the destination is unchanged, so
  re-addressing never ratchets a reply into a new channel; adds
  `--no-broadcast` to clear an inherited flag;
- rejects `--broadcast` for DM targets, matching `message send`;
- `findDraft` prefers a well-formed record if the list response has a
  malformed duplicate.

BREAKING CHANGE: `message draft <target> [text]` no longer opens the editor;
use `message compose`. Old usage errors cleanly; `message draft --help`
points to compose. No backwards-compat shim.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: GPT-5.6-Sol <noreply@openai.com>
@nwparker
nwparker force-pushed the feat/message-compose-native-drafts branch from 91a9cd7 to cdfbf68 Compare July 18, 2026 07:03
@nwparker
nwparker merged commit 452dad2 into main Jul 18, 2026
1 check passed
@nwparker
nwparker deleted the feat/message-compose-native-drafts branch July 18, 2026 07:09
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.

Proposal: integrate Slack's native drafts API

1 participant