Skip to content

feat(messaging): allowEmptySend — un adjunto sin texto se puede enviar (inbox-ai#591) - #18

Merged
A-PachecoT merged 3 commits into
mainfrom
fix/591-allow-empty-send
Aug 21, 2026
Merged

feat(messaging): allowEmptySend — un adjunto sin texto se puede enviar (inbox-ai#591)#18
A-PachecoT merged 3 commits into
mainfrom
fix/591-allow-empty-send

Conversation

@A-PachecoT

Copy link
Copy Markdown
Contributor

Qué

MessageComposer gana allowEmptySend?: boolean (default false): permite despachar con el campo de texto vacío, para que un consumidor con un adjunto ya encolado pueda mandarlo sin escribir nada — como WhatsApp.

Nace de inbox-ai#591, del founder: "no puedo enviar imagen a secas, en whatsapp un cliente si podra enviar msg de solo foto".

La forma de la prop, que es lo que no se deshace barato

Se llama por lo que hace el compositor, no por la razón del padre. Este componente no renderiza, no cuenta ni valida adjuntos: no está en posición de afirmar que los hay. Un hasAttachments sería además una mentira para el próximo consumidor que la prenda por una nota de voz, un sticker o un payload de plantilla.

  • Default false — ningún consumidor actual cambia de comportamiento. Cuatro tests lo fijan (vacío = bloqueado · Enter no despacha · sólo espacios tampoco · el texto sigue saliendo trimmeado).
  • Valor VIVO, no flag de configuración — verdadero sólo mientras el padre tenga algo encolado. Un true constante deja el botón siempre habilitado; el compositor no puede distinguirlos, igual que no puede con disabled. Está escrito en el TSDoc y en la story.
  • Con el campo vacío onSend recibe la cadena vacía — el compositor no inventa contenido. Qué viaja por el wire (un placeholder, un caption, nada) lo decide el consumidor.
  • disabled sigue ganando — la prop habilita, no fuerza.

Verificación

  • Los 3 tests de la dirección nueva se vieron ROJOS contra el componente sin el arreglo, antes de escribirlo.
  • npx vitest run: 513 passed / 1 failed (514) contra baseline en main de 503 passed / 1 failed (504) → +10 tests, todos de este PR. El fallo es el de ChatInput.test.tsx (getByRole('button', { name: '' })), que ya venía de main — medido con las mismas dos corridas.
  • npm run typecheck: los mismos 2 errores de hero-shader/* antes y después (diff de las dos salidas: vacío). Este repo no tiene gate de tests en CI, así que las corridas van pegadas arriba.
  • Story nueva Messaging/MessageComposer → AllowEmptySend: el chip prende y apaga el adjunto y se ve al botón seguirlo en las dos direcciones.

Consumidor

inbox-ai lo pasa en sus 3 pantallas (/inbox escritorio, /inbox móvil, /sandbox) en su rama fix/591-composer, y necesita este cambio en main para poder regenerar su lockfile (depende de github:cofoundy/ui#main).

… (inbox-ai#591)

En WhatsApp un cliente manda una foto sola todo el tiempo. El compositor lo
impedía en dos sitios a la vez —`handleSubmit` exigía `message.trim()` y el
botón de enviar iba `disabled` sin texto—, así que un consumidor con un adjunto
encolado no tenía forma de despacharlo.

La prop se llama por lo que hace EL COMPOSITOR, no por la razón del padre:
este componente no renderiza, no cuenta ni valida adjuntos, así que no está en
posición de afirmar que los hay. Un `hasAttachments` sería además una mentira
para el próximo consumidor que la prenda por una nota de voz, un sticker o un
payload de plantilla.

- Default `false`: el comportamiento de todos los consumidores actuales no
  cambia, y hay 4 tests que lo fijan (vacío = bloqueado, Enter no despacha,
  espacios tampoco, el texto sigue saliendo trimmeado).
- Es un valor VIVO, no un flag de configuración: verdadero sólo mientras el
  padre tenga algo encolado. Un `true` constante deja el botón siempre
  habilitado — el compositor no puede distinguirlos, igual que no puede con
  `disabled`.
- Con el campo vacío, `onSend` recibe la CADENA VACÍA. El compositor no inventa
  contenido: qué viaja por el wire (un placeholder, un caption, nada) lo decide
  el consumidor.
- `disabled` sigue ganando: la prop habilita, no fuerza.

Verificado: los 3 tests de la dirección nueva se vieron ROJOS contra el
componente sin el arreglo. vitest 513 passed / 1 failed (514) vs baseline
503 passed / 1 failed (504) — +10 tests, todos míos; el fallo es el de
`ChatInput.test.tsx` que ya venía de main. `tsc --noEmit`: los MISMOS 2 errores
de `hero-shader/*` antes y después (diff vacío).
…15 (inbox-ai#591)

`inbox-ai` no puede instalar NINGUNA versión del paquete desde marzo. Su
lockfile está pineado en `6f07147` (2026-04-18) y cualquier `npm install
github:cofoundy/ui#main` muere con ERESOLVE:

    Conflicting peer dependency: next@16.3.2
    peerOptional next@">=15" from @cofoundy/ui@0.6.1
    Found: next@14.2.35

El piso llegó con `db547c6` (hero-shader, v0.3.0), y **no lo pidió ninguna
API**: en todo `src/` hay exactamente UN import de next —
`hero-shader/ShaderHero.tsx: import dynamic from 'next/dynamic'`— y
`next/dynamic` existe desde Next 9. Medido con
`grep -rn "from 'next" src/`, no leído.

`peerDependenciesMeta` ya lo marca opcional, pero opcional no significa
ignorado: si `next` ESTÁ instalado, npm igual exige que satisfaga el rango, y
por eso el consumidor en 14 queda afuera. Ensanchar es aditivo — ningún
consumidor en 15/16 cambia de resolución.
Las dos features tocaron `handleSubmit` y el botón primario con horas de
diferencia. Se componen así, y el orden no es arbitrario:

- **`busy` gana.** Codifica una regla de producto —no hay mensajes cruzados
  mientras la IA despacha— y `allowEmptySend` sólo dice «vacío es contenido
  legítimo». Un permiso no puede ganarle a una prohibición. En `handleSubmit`
  el `if (isBusy) return` va PRIMERO y recién después mi gate; en el render,
  la rama de Parar de main queda intacta y mi `disabled={!canSend}` entra sólo
  en la rama de Enviar.
- Las stories se resolvieron tomando el archivo de `main` entero y
  reinjertando `AllowEmptySend` al final, en vez de empalmar a mano dos
  bloques que git había entrelazado.

**El cruce tiene sus propios tests**, porque cada suite ejercitaba su feature
con la otra apagada, que es justo donde se esconde una composición mala: con
`busy` no hay botón de Enviar ni siquiera con `allowEmptySend`, Enter no
envía, y al despejarse `busy` el adjunto encolado sí sale sin texto.

Verificado con mutante: quitando el `if (isBusy) return` del `handleSubmit`
fusionado, el test de main «con `busy`, un submit del form tampoco envía» se
pone ROJO (1 failed / 30 passed). O sea la resolución es load-bearing y no
decorativa.

Suite completa 536 passed / 1 failed (537) — el fallo es `ChatInput`,
preexistente en main limpio. `tsc`: los mismos 2 errores de `hero-shader`.
@A-PachecoT
A-PachecoT merged commit d44a1b3 into main Aug 21, 2026
1 of 2 checks passed
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