Skip to content

feat: support file attachments on Slack-native drafts - #135

Open
andromedarabbit wants to merge 3 commits into
stablyai:mainfrom
andromedarabbit:andromedarabbit/feature-draft-with-attachment
Open

feat: support file attachments on Slack-native drafts#135
andromedarabbit wants to merge 3 commits into
stablyai:mainfrom
andromedarabbit:andromedarabbit/feature-draft-with-attachment

Conversation

@andromedarabbit

Copy link
Copy Markdown

Summary

Slack-native drafts (message draft create / update) couldn't carry file attachments — only message send --attach could. This adds a repeatable --attach to draft create/update.

Root cause

createDraft's input lacked fileIds, the CLI exposed no --attach option, and the only upload helper (uploadLocalFileToSlack) was message-only — files.completeUploadExternal bound the file to a channel_id, so it couldn't serve a draft that has no message yet. Per the Slack docs, channel_id is optional on files.completeUploadExternal, so a file can be staged and its id returned without binding it to a message. (updateDraft already plumbed fileIds, and tests already verified file_ids round-trip — only the create pipe was severed.)

Changes

  • src/slack/upload.ts — extract shared stageFileUpload; add uploadFileForDraft that completes the upload without channel_id and returns the file id.
  • src/slack/drafts.tscreateDraft accepts fileIds (updateDraft already did).
  • src/cli/message-draft-actions.ts — upload --attach files and pass fileIds; update merges new uploads into the draft's existing file_ids (de-duplicated).
  • src/cli/message-draft-command.ts — repeatable --attach <path> on create / update.

Tests

  • test/upload.test.ts (new): uploadFileForDraft happy path (asserts no channel_id/thread_ts/initial_comment on the completion call), missing path, directory, getUploadURL failure, byte-POST failure, completeUpload failure; uploadLocalFileToSlack regression after the shared-helper extraction.
  • test/drafts.test.tscreateDraft propagates fileIds.
  • test/message-draft-actions.test.ts — create multi-attach (ordered file_ids), missing-path abort (no upload, no drafts.create), update merge into existing file_ids, commander --attach a --attach b wiring.

bun test 357 pass / 0 fail · tsc --noEmit clean · oxlint 0 errors.

Out of scope

The safe-mode compose editor path (redirectSendToDraftdraft-editor.html) still rejects --attach; that path needs editor UI work and is left for a follow-up.

🤖 Generated with Claude Code

andromedarabbit and others added 3 commits August 6, 2026 23:21
Slack-native drafts (message draft create/update) accepted no --attach:
createDraft's input lacked fileIds, the CLI exposed no option, and the
upload helper was message-only (completeUploadExternal bound the file to
a channel, so it could not serve a draft that has no message yet).

- Add uploadFileForDraft to src/slack/upload.ts: shares upload staging
  with uploadLocalFileToSlack but completes without channel_id, returning
  the file id (files.completeUploadExternal's channel_id is optional).
- Wire fileIds through createDraft (updateDraft already supported it).
- Add a repeatable --attach to `message draft create` / `update`; update
  merges new uploads into the draft's existing file_ids.

The safe-mode compose editor path remains intentionally out of scope:
draft-editor.html has no attachment UI, so redirectSendToDraft still
rejects --attach there.

Co-Authored-By: Claude <noreply@anthropic.com>
Address ce-code-review findings on PR stablyai#135:

- Extract normalizeAttachPaths + collectOptionValue into src/cli/options.ts.
  The two helpers were duplicated byte-for-byte across message-actions.ts,
  message-command.ts, and the message-draft-* modules (review stablyai#4, stablyai#6).
- Replace the per-file draft upload loop with uploadFilesForDraft: stage every
  file first, then a single completeUploadExternal. A staging failure leaves
  nothing completed, and Slack discards staged-but-uncompleted uploads, so a
  later --attach failure can no longer orphan private files (review stablyai#1).
- Add a mergeFileIds de-duplication test for overlapping file ids, and update
  the mock/assertions for the batched completion order (review stablyai#5).

Co-Authored-By: Claude <noreply@anthropic.com>
리뷰 stablyai#2/stablyai#3 대응. draft에 파일을 첨부할 때 뒤따르는 draft API
호출이 실패하면 이미 올린 파일이 비공개로 orphan 되는 문제를 막는다.

stablyai#2 (auth 재시도 재실행): createDraft/updateDraftAction에서
uploadedFileIds를 withAutoRefresh의 work() 바깥 클로저에
메모이제이션한다. drafts.create/update가 invalid_auth로 실패해
work()가 재실행될 때 upload를 다시 돌리지 않고 올린 file id를
재사용한다. 첫 업로드 파일이 두 번째 업로드에 밀려 orphan/중복되던
문제를 해결한다.

stablyai#3 (stale 실패 orphan): createDraft/updateDraft에 ensureDraftOk를
추가해 ok:false(stale conflict, denied 등)를 throw하게 했다(기존엔
null로 삼킴). cleanupUploadedDraftFiles(files.delete best-effort)
헬퍼를 추가하고, action 전체를 try/catch로 감싸 최종 실패 시 업로드한
새 파일만 정리 후 rethrow한다. update는 existing file_ids는
건드리지 않고 새로 올린 파일만 정리한다.

테스트: createContext에 failOnce/failWith/retryOnAuth 주입 옵션을
추가하고 stablyai#2 create/update 재시도, stablyai#3 create/update 실패 정리,
첨부 없으면 files.delete 미호출 시나리오 5개를 추가했다.

Co-Authored-By: Claude <noreply@anthropic.com>
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