Skip to content

feat!: inbound media support with FlowChat::Input turn value object#2

Open
thedumbtechguy wants to merge 18 commits into
masterfrom
feature/inbound-media-support
Open

feat!: inbound media support with FlowChat::Input turn value object#2
thedumbtechguy wants to merge 18 commits into
masterfrom
feature/inbound-media-support

Conversation

@thedumbtechguy

@thedumbtechguy thedumbtechguy commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds inbound media support so flows can read the media, location, and contact data users send — previously app.media/app.location/app.contact_name were hardcoded to nil, stranding data the WhatsApp/Telegram gateways already parsed. Fills in inbound parsing for Intercom and HTTP, and — as the branch evolved — replaces the old input-sentinel model with a rich FlowChat::Input value object so text and attachments are first-class on every turn.

The turn is now a value object

Every turn is a FlowChat::Input (app.input) with two independent axes: text and an optional attachment.

  • app.text → the turn's text (message body or a media caption). Always a String; "" when the turn carried no text (e.g. a caption-less photo).
  • app.attachment_type:media / :location / :contact / nil — the discriminator to branch on.
  • app.mediaalways an Array<FlowChat::Media> (empty when none), so multi-attachment messages never silently drop items.
  • app.location, app.contact, app.contact_name.
  • FlowChat::Media: type (normalized: :photo:image, :voice:audio), raw_type (platform-native), mime_type, caption, filename, id/file_id, plus url/download to fetch bytes per platform.

FlowChat::Input delegates public String methods to its text, so existing validate/transform lambdas (input.strip, input.to_i, input.blank?) keep working unchanged — and they can now also inspect attachments (input.media.any?). A screen is satisfied when input.submitted? — text or an attachment — so a caption-less photo still answers.

Platform coverage

Platform Inbound media Notes
WhatsApp media-id → get_media_url/download_media
Telegram new client get_file/file_url/download_file; message-level captions captured
Intercom attachments array (multi-attachment); body captured as caption
HTTP via media_url/media_type/mime_type params
USSD text-only, out of scope

⚠️ Breaking changes

  • The "$media$", "$location$", and "$contact$" input sentinels are removed. context.input is always plain text now ("" when a turn carries no text); the structured payload lives on request.media/request.location/request.contact. Branch on app.attachment_type instead of comparing input to a sentinel string.
  • app.input is now a FlowChat::Input, not a raw String (it still behaves like its text for string ops and ==).
  • app.media always returns an Array<FlowChat::Media> (empty when none) rather than a single object — iterate uniformly. The interim app.media_items accessor is gone; app.media is the list.

Conveyed to git-cliff via a refactor!: commit with a BREAKING CHANGE: footer.

Test Plan

  • Full suite green: 1022 tests, 0 failures, 0 errors
  • standardrb clean
  • FlowChat::Input unit tests (text coercion, submitted?, attachment_type, media-always-a-list, string delegation, public-only respond_to?, equality, START)
  • Unit tests for FlowChat::Media (readers, per-platform url/download dispatch, direct-HTTP download, type normalization)
  • Gateway inbound-parsing tests (WhatsApp/Telegram/Intercom/HTTP) updated for text-only input, including multi-attachment and caption cases
  • App accessor + media_client wiring tests

Design spec and implementation plan under docs/superpowers/.

🤖 Generated with Claude Code

… object

Every turn is now a FlowChat::Input value object (app.input) with two
independent axes: text and an optional attachment (media/location/contact).
Gateways set context.input to plain text only; a structured turn with no text
sets it to "". The prompt gates on Input#submitted? (text OR attachment), and
Input delegates public String methods to its text so existing validators and
transforms keep working unchanged. app.media is always an Array so multi-
attachment messages never silently drop items.

BREAKING CHANGE: The "$media$", "$location$", and "$contact$" input sentinels
are removed. context.input is always plain text ("" when a turn carries no
text). app.input is now a FlowChat::Input rather than a raw string, and
app.media always returns an Array<FlowChat::Media> (empty when none) instead of
a single item. Branch on app.attachment_type instead of comparing input to a
sentinel string.

Claude-Session: https://claude.ai/code/session_01MgpYCqkxqafrxTp2CUtGii
@thedumbtechguy thedumbtechguy changed the title Add inbound media support across platforms feat!: inbound media support with FlowChat::Input turn value object Jul 10, 2026
Left over from removing the input sentinel block; fixes standardrb
Layout/EmptyLinesAroundModuleBody.

Claude-Session: https://claude.ai/code/session_01MgpYCqkxqafrxTp2CUtGii
Addresses review findings on the FlowChat::Input turn model:

- App#screen now treats a screen as answered when its key is present in the
  session (non-nil), not merely truthy. A caption-less attachment answers with
  "", which previously failed the `.present?` cache-hit guard and caused the
  screen to re-ask every turn. Also fixes false/blank transform results looping.
- App#prepare_user_input lets an opening message that carries an attachment
  through to the first screen instead of swallowing it as a bare session-opener;
  text-only openers keep the existing "wake the flow" behavior.
- Input#== coerces a FlowChat::Input operand to its text so two Inputs with
  equal text compare equal; non-string operands still compare false.
- Media#url is memoized so repeated reads don't re-issue the platform lookup.
- HTTP gateway validates caller-supplied media_type against
  Media::CANONICAL_TYPES, falling back to :document.
- Mark the historical inbound-media spec/plan as superseded by the Input model.

Claude-Session: https://claude.ai/code/session_01MgpYCqkxqafrxTp2CUtGii
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