feat(messaging): allowEmptySend — un adjunto sin texto se puede enviar (inbox-ai#591) - #18
Merged
Merged
Conversation
… (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`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Qué
MessageComposerganaallowEmptySend?: boolean(defaultfalse): 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
hasAttachmentsserí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.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).trueconstante deja el botón siempre habilitado; el compositor no puede distinguirlos, igual que no puede condisabled. Está escrito en el TSDoc y en la story.onSendrecibe la cadena vacía — el compositor no inventa contenido. Qué viaja por el wire (un placeholder, un caption, nada) lo decide el consumidor.disabledsigue ganando — la prop habilita, no fuerza.Verificación
npx vitest run: 513 passed / 1 failed (514) contra baseline enmainde 503 passed / 1 failed (504) → +10 tests, todos de este PR. El fallo es el deChatInput.test.tsx(getByRole('button', { name: '' })), que ya venía demain— medido con las mismas dos corridas.npm run typecheck: los mismos 2 errores dehero-shader/*antes y después (diffde las dos salidas: vacío). Este repo no tiene gate de tests en CI, así que las corridas van pegadas arriba.Messaging/MessageComposer → AllowEmptySend: el chip prende y apaga el adjunto y se ve al botón seguirlo en las dos direcciones.Consumidor
inbox-ailo pasa en sus 3 pantallas (/inboxescritorio,/inboxmóvil,/sandbox) en su ramafix/591-composer, y necesita este cambio enmainpara poder regenerar su lockfile (depende degithub:cofoundy/ui#main).