Doop
Lienzo de diseño multijugador de código abierto (alternativa a Paper.design): los agentes crean y transmiten mesas de trabajo HTML en vivo junto a colaboradores humanos, se autoevalúan mediante capturas de pantalla y siguen pautas de diseño compartidas.
Documentación
Doop es la alternativa de código abierto a Paper.design — un lienzo de diseño multijugador para humanos y agentes de IA. Cada diseño vive en un Canvas compartible (/c/<id>) que contiene Frames — mesas de trabajo que renderizan HTML real en iframes aislados. Las personas editan en el navegador; los agentes de IA editan a través del servidor MCP integrado, transmitiendo sus diseños en vivo. Todos ven todo mientras sucede: cursores, presencia, ediciones de frames, estado de agentes y un feed de actividad.
- Diseña con agentes, no con prompts-y-refresco — conecta Claude Code (o cualquier cliente MCP) una vez, y luego míralo esbozar, transmitir y auto-revisar diseños en tu lienzo, junto a tu cursor.
- Un Agente Doop integrado — pon una tarjeta en cola o menciona un rol y diseña solo, sin necesidad de conectar un cliente. Se ejecuta en el
ANTHROPIC_API_KEYdel servidor para un puñado de tareas gratuitas, luego en la suscripción de ChatGPT (o clave de OpenAI) que cada usuario conecta (configuración); la actuación de bienvenida del primer lienzo está guionizada y se ejecuta sin nada de eso. - Multijugador real — cursores en vivo, presencia, indicadores de edición por frame, deshacer/rehacer, comentarios anclados a elementos y un feed de actividad, todo a través de una sala WebSocket.
- Memoria de diseño — fija frames ejemplares, captura decisiones y deja que el destilador proponga reglas de estilo duraderas que todo agente sigue.
- Privado por defecto — invita colaboradores por correo electrónico o activa el intercambio de enlaces por lienzo; los agentes heredan exactamente el acceso de su humano.
- Auto-alojado con un comando —
docker compose up, obun run devcon cero configuración (Postgres integrado, sin servicios externos requeridos).
Inicio rápido
git clone https://github.com/kgoedecke/doop && cd doop
bun install
bun run dev
Doop se compila e instala con bun (bun.lock es el único lockfile); el servidor en sí se ejecuta en Node.
- Aplicación web: http://localhost:4300
- API + WebSocket + servidor MCP: http://localhost:4400 (el puerto web proxya
/api,/ws,/mcphacia él)
Todo funciona sin configuración: los datos persisten en un Postgres integrado (PGlite) en data/pg, y cada integración opcional (SMTP, fotos de stock, almacenamiento de objetos, analíticas) se degrada con elegancia hasta que su variable en .env.example esté configurada. La que más probablemente querrás es ANTHROPIC_API_KEY, que activa el Agente Doop integrado — los agentes que conectes tú mismo a través de MCP no necesitan clave.
O auto-aloja la compilación de producción con Docker:
BETTER_AUTH_SECRET=$(openssl rand -hex 32) docker compose up -d # app + Postgres on :4400
Compilación de producción sin Docker: bun run build && bun run start (un solo servidor en :4400 que sirve todo). Configura DATABASE_URL para usar un Postgres real — mismo código que PGlite.
¿Prefieres no ejecutar nada? doop.design es la versión alojada.
Conecta Claude Code
Un comando conecta Claude Code (o cualquier cliente MCP) a tu lienzo:
claude mcp add --transport http doop http://localhost:4300/mcp
Eso activa el flujo OAuth estándar de MCP — se abre una ventana del navegador, apruebas, y desde entonces el agente trabaja como tú. Pídele que diseñe algo en tu id de lienzo y míralo suceder en vivo. Todo en esta captura es el flujo real: Claude Code se anunció con set_status, creó un frame, y está transmitiendo la sección de precios — avatar de presencia, atribución "para Kai Moreno", el chip del frame, la franja de trabajo y la tarea en el panel de Agentes.
Observa a un agente diseñar
El primer lienzo después del registro viene con una actuación: el Agente Doop transmite un diseño de bienvenida mientras observas — estado en la franja de trabajo, una tarea en el panel, un borde pulsante en el frame que está construyendo.
Esa actuación de bienvenida está guionizada (server/demo.ts) — un frame pre-escrito reproducido a través de la misma maquinaria que usan los agentes reales, por lo que se ejecuta sin configuración alguna. El Agente Doop propiamente dicho necesita una clave.
El Agente Doop
Doop incluye un equipo de diseño integrado que vive en el servidor y recoge trabajo por sí solo: pon una tarjeta de tablero en cola, @mention un rol en un comentario de elemento, o deja comentarios en una tarea, y se ejecuta sin un humano en el bucle. Los roles (Doop construye; los especialistas tienen un pase cada uno — UX, copy, marca, accesibilidad) están definidos en shared/agents.ts, y una tarjeta puede enrutarse a través de varios en orden.
El servidor paga por el nivel gratuito, en Anthropic por defecto:
ANTHROPIC_API_KEY=sk-ant-... # in .env, or the environment of your deployment
La misma clave controla el destilador de pautas (server/distill.ts), que propone reglas de estilo duraderas desde tu lienzo.
El nivel gratuito puede ejecutarse en Azure OpenAI en su lugar — útil cuando los créditos o las reglas de cumplimiento de tu organización viven allí:
DOOP_AGENT_PROVIDER=azure
AZURE_OPENAI_ENDPOINT=https://my-resource.openai.azure.com
AZURE_OPENAI_API_KEY=...
AZURE_OPENAI_DEPLOYMENT=my-deployment
El destilador permanece en ANTHROPIC_API_KEY de cualquier manera y se apaga silenciosamente sin él.
Más allá de las tareas gratuitas: conecta tu propio ChatGPT
Cuando las RESIDENT_TASK_LIMIT tareas gratuitas de un usuario se agotan, no pierden el agente — conectan una cuenta de modelo y el Agente Doop sigue ejecutándose en ella. Una cuenta conectada toma el control inmediatamente, desde la siguiente tarea: el nivel gratuito es una prueba que lleva a la gente aquí, no un saldo que gastar primero, y conectar deja de costar nada al servidor desde ese momento. La conexión es a nivel de cuenta, por lo que vive en /settings (Inicio → Configuración); el muro del nivel gratuito enlaza allí en lugar de tener su propia copia, y "Conectar un agente de IA" en un lienzo se mantiene solo sobre clientes MCP. Dos tipos de cuenta:
- Suscripción de ChatGPT — OAuth contra
auth.openai.com, luego inferencia a través del backend de Codex que incluyen los planes Plus/Pro/Business. Los tokens viven enmodel_accountsy nunca llegan a un navegador. - Clave de API de OpenAI — pago por uso en la propia cuenta de OpenAI del usuario, sin suscripción involucrada.
Azure OpenAI deliberadamente no es un tipo de cuenta conectable: un endpoint proporcionado por el usuario sería una URL que el servidor busca con el contexto completo de la ejecución — un vector SSRF — por lo que Azure permanece solo como proveedor a nivel de servidor.
De cualquier manera, el usuario elige su nivel de modelo en Configuración — gpt-5.6-sol (buque insignia), gpt-5.6-terra (el caballo de batalla por defecto) o gpt-5.6-luna (barato y rápido). Lo están pagando, así que la elección es suya; DOOP_AGENT_OPENAI_MODEL solo establece el valor predeterminado con el que comienzan. Ten en cuenta que gpt-5.4 y gpt-5.4-mini se retiran de Codex autenticado por ChatGPT el 31 de agosto de 2026, por lo que fijar un id 5.4 a través de esa variable de entorno romperá la ruta de suscripción después de esa fecha.
OpenAI no registra una URI de redirección para una aplicación alojada, por lo que conectar ChatGPT toma una de tres formas y Doop elige la más barata disponible:
| Dónde se ejecuta Doop | Flujo | Lo que hace el usuario |
|---|---|---|
| Misma máquina que el navegador (dev, auto-alojado) | Captura de bucle local — Doop mantiene 127.0.0.1:1455 | Aprueba en la pestaña de OpenAI. Nada que copiar, sin configuración |
| Alojado (doop.design) | Código de dispositivo (/api/accounts/deviceauth/*) | Escribe un código corto en auth.openai.com/codex/device |
| Códigos de dispositivo deshabilitados | Redirección del navegador + pegar | Pega la dirección de la página localhost:1455 muerta de vuelta en Doop |
El flujo de dispositivo necesita autorización de código de dispositivo activada en ChatGPT → Configuración → Seguridad (los miembros del espacio de trabajo necesitan que un administrador lo permita) — por eso el flujo de bucle local, que no necesita configuración alguna, sigue siendo el predeterminado cuando Doop es local. Los tres terminan en el mismo intercambio PKCE del lado del servidor.
Antes de activar esto para usuarios reales: conducir una suscripción de ChatGPT desde un servidor de terceros no está sancionado por los términos de OpenAI, y el uso intensivo puede hacer que una cuenta sea limitada o suspendida. La ruta de clave de API es la alternativa totalmente compatible y comparte todo el mismo código.
CHATGPT_CONNECT_DISABLED=1desactiva la ruta de suscripción y deja la ruta de clave.
Las ejecuciones se atribuyen al humano cuya tarjeta, comentario o comentario recogieron, por lo que la persona que pidió el trabajo es la persona cuya cuenta lo ejecuta. La traducción entre el bucle de forma Anthropic del agente y la API de Respuestas de OpenAI vive en
server/openaiAgent.ts; qué credencial recibe una ejecución se decide en
server/agentModel.ts.
Sin clave de servidor y sin cuenta conectada, el Agente Doop está apagado, y falla silenciosamente por diseño — las tarjetas en cola y @mentions simplemente esperan a que algún agente las reclame. El banner de inicio te dice en qué estado estás.
Todo esto es separado de conectar tu propio agente. Claude Code y cualquier otro cliente MCP se autentican a través de OAuth y conducen el lienzo desde fuera, en tu propia suscripción — nunca medido. Tres rutas, mismo lienzo: el Agente Doop en nuestra clave (nivel gratuito), el Agente Doop en tu clave, o tu propio agente a través de MCP.
| Variable | Predeterminado | Qué hace |
|---|---|---|
DOOP_AGENT_PROVIDER | anthropic | En qué se ejecuta el nivel gratuito: anthropic | azure |
ANTHROPIC_API_KEY | sin configurar | Paga por el nivel gratuito del Agente Doop (proveedor predeterminado) y el destilador |
AZURE_OPENAI_ENDPOINT | sin configurar | El recurso Azure OpenAI del nivel gratuito, cuando DOOP_AGENT_PROVIDER=azure |
AZURE_OPENAI_API_KEY | sin configurar | Una clave de ese recurso |
AZURE_OPENAI_DEPLOYMENT | sin configurar | El despliegue en el que se ejecuta el nivel gratuito |
AZURE_OPENAI_API_VERSION | sin configurar | Fija un parámetro de consulta api-version; la superficie v1 no necesita ninguno |
AZURE_OPENAI_REASONING_EFFORT | sin configurar | Esfuerzo de razonamiento en ejecuciones de Azure; sin configurar no envía ninguno (no seguro para razonamiento) |
RESIDENT_TASK_LIMIT | 0 | Tareas gratuitas del Agente Doop por cuenta; 0 significa una cuenta conectada desde la primera tarea |
DOOP_AGENT_MODEL | claude-opus-5 | Modelo para el Agente Doop en la clave Anthropic del servidor |
DOOP_AGENT_OPENAI_MODEL | gpt-5.6-terra | Nivel predeterminado en la cuenta de un usuario; cada usuario puede elegir otro en Configuración |
CHATGPT_CONNECT_DISABLED | sin configurar | 1 oculta el flujo de ChatGPT, dejando la ruta de clave de API |
DOOP_DISTILL_MODEL | claude-haiku-4-5-20251001 | Modelo para el destilador de pautas |
RESIDENT_TASK_LIMIT es el medidor de nivel gratuito. Por defecto está en 0: el Agente Doop solo se ejecuta | ||
| una vez que el usuario conecta una cuenta de modelo (su suscripción a ChatGPT o una clave de OpenAI) — una cuenta | ||
| conectada nunca se mide. Conectar tu propio agente MCP no levanta el medidor: se ejecuta en tu | ||
| modelo cuando él diseña, pero las tareas residentes siguen facturando una credencial. Establece el límite por encima de 0 para | ||
| conceder esa cantidad de tareas gratuitas con la clave del servidor; todo lo que desencadena trabajo residente cuenta, | ||
| incluidos comentarios y reintentos. No existe un valor "ilimitado": si te autoalojas con tu propia clave, establécelo | ||
| en un número grande, ya que de todos modos estás pagando a Anthropic directamente. |
Cuentas
La aplicación web requiere una cuenta (better-auth, correo electrónico/contraseña — registro abierto). Tu nombre de cuenta es tu identidad en todas partes: cursores, presencia, el feed de actividad y la atribución de comentarios son todos autoritativos del servidor desde la sesión, y el WebSocket rechaza uniones no autenticadas. Los lienzos son privados por defecto, estilo Figma: solo el propietario y las personas que invite (Compartir → invitar por correo electrónico, cuentas doop existentes) pueden abrir uno. El modal Compartir también puede activar el uso compartido de enlaces por lienzo ("cualquiera con el enlace puede editar"), lo que restaura la colaboración por enlace para ese lienzo. Tu pantalla de inicio lista tus propios lienzos más los compartidos contigo (más los heredados sin propietario, reclamables allí). Los agentes conectados a través de MCP actúan bajo la cuenta que los aprobó y obtienen exactamente ese acceso de usuario.
Con SMTP configurado (SMTP_HOST etc. — ver .env.example), los registros requieren verificación
de correo electrónico y "olvidé mi contraseña" envía enlaces de restablecimiento reales. Sin él, el registro permanece abierto y cada
correo electrónico se imprime en el registro del servidor, incluidos los enlaces — los flujos aún funcionan en desarrollo.
Establece SIGNUP_EMAIL_DOMAINS=jointhetroops.com para restringir nuevas cuentas a un dominio de correo electrónico, o usa una
lista separada por comas para varios dominios. La coincidencia no distingue entre mayúsculas y minúsculas y es exacta; las cuentas existentes
no se ven afectadas. Déjalo sin establecer para mantener el registro público abierto.
Establece REQUIRE_EMAIL_VERIFICATION=false para dejar entrar a las personas antes de que verifiquen — el enlace aún se
envía por correo, solo deja de bloquear el inicio de sesión. La promoción de administrador no es deliberadamente parte de ese intercambio:
ADMIN_EMAILS solo promueve una dirección verificada (ver más abajo).
Si el registro o el restablecimiento de contraseña se cuelga en lugar de fallar, la causa casi siempre es un host que
bloquea SMTP saliente: Railway y la mayoría de PaaS bloquean 25/465/587. Resend también sirve 2465/2587, por lo que
SMTP_PORT=2587 es la solución habitual.
Env: BETTER_AUTH_SECRET (requerido en producción), TRUSTED_ORIGINS (separado por comas,
por defecto los orígenes de desarrollo localhost).
Administradores de instancia
ADMIN_EMAILS (separado por comas) nombra las cuentas que obtienen el rol admin, aplicado en el
registro, en la verificación de correo electrónico y al arrancar — así que puedes nombrar a un administrador antes o después de que
tenga una cuenta. Esto requiere SMTP en producción: una dirección solo identifica a alguien
una vez que ha demostrado que la posee, y sin un servicio de correo el registro está abierto, por lo que cualquiera podría registrarse
como tu dirección y tomar el rol con ella. Una instancia de producción sin SMTP no promueve
a nadie y advierte al arrancar; establece el rol directamente en la base de datos si esa es tu configuración.
Los administradores obtienen /admin: cada lienzo y cuenta en la instancia, y "ver como", que les entrega
una sesión real pero de solo lectura de 15 minutos como ese usuario. Ser administrador no amplía el acceso al lienzo
en sí: la puerta en server/access.ts se comparte con MCP,
por lo que una lectura privilegiada allí daría a cada agente que tenga un token de administrador el control de la
instancia. Las sesiones de ver como no pueden escribir, no pueden conectar agentes y registran quién está detrás
de ellas en session.impersonated_by.
SSO (OIDC)
Inicio de sesión opcional contra un proveedor OIDC externo (Zitadel, Okta, Authentik, Keycloak,
etc.), junto con correo electrónico/contraseña — no un reemplazo para ello. Establece OIDC_ISSUER,
OIDC_CLIENT_ID y OIDC_CLIENT_SECRET juntos para habilitarlo; un conjunto parcial se niega
a arrancar en lugar de ejecutarse con SSO a medio configurar. OIDC_SCOPES (por defecto
openid email profile) y OIDC_PROVIDER_NAME (por defecto SSO, mostrado en el botón
de inicio de sesión — p. ej. Zitadel) son opcionales. Iniciar sesión a través de SSO se vincula a una cuenta existente
de correo electrónico/contraseña cuando los correos coinciden y el proveedor marca el correo
verificado, y esto funciona incluso en una instancia sin SMTP configurado, donde una
cuenta local de otro modo nunca podría verificar por sí misma. SSO solo nunca otorga
el rol de administrador, incluso para una dirección listada en ADMIN_EMAILS — un IdP no se
confía como fuente de promoción de administrador, solo como verificación de propiedad de correo;
la promoción aún requiere la ruta normal de ADMIN_EMAILS (registro verificado, o
syncAdmins al arrancar para una cuenta que SSO ha verificado desde entonces).
Env: ver el bloque OIDC en .env.example.
Iniciar sesión con Google
Opcional, junto con correo electrónico/contraseña y SSO. Crea un cliente OAuth (aplicación web) en
la consola de Google Cloud, agrega
<BETTER_AUTH_URL>/api/auth/callback/google como URI de redirección autorizada, y establece
GOOGLE_CLIENT_ID y GOOGLE_CLIENT_SECRET juntos (uno sin el otro se niega a
arrancar). La página de inicio de sesión muestra un botón "Iniciar sesión con Google" siempre que ambos estén establecidos. El
vínculo de cuentas y la promoción de administrador siguen las mismas reglas que SSO arriba; SIGNUP_EMAIL_DOMAINS
se aplica a los registros de Google (y SSO) exactamente como lo hace para correo electrónico/contraseña.
Iniciar sesión con Microsoft
Misma forma que Google. Registra una aplicación en Microsoft Entra
(Registros de aplicaciones, plataforma Web) con <BETTER_AUTH_URL>/api/auth/callback/microsoft como
URI de redirección, crea un secreto de cliente, y establece MICROSOFT_CLIENT_ID y
MICROSOFT_CLIENT_SECRET juntos. MICROSOFT_TENANT_ID (por defecto common, cualquier cuenta de Microsoft)
puede ser organizations, consumers, o tu ID de inquilino para hacer que el botón sea una
puerta solo para organizaciones. Microsoft no afirma la propiedad del correo a menos que el
token de ID del registro de la aplicación incluya las afirmaciones opcionales email y verified_primary_email; sin ellas, un
inicio de sesión de Microsoft aún funciona pero solo se vincula a una cuenta existente que ya está
verificada. Todo lo demás (lista de permitidos, promoción de administrador) sigue las reglas de SSO arriba.
Autenticación de agente (MCP OAuth)
El endpoint /mcp requiere OAuth. Agregar el servidor en Claude Code / Codex desencadena
el flujo estándar de MCP OAuth: se abre una ventana del navegador, inicias sesión en Doop y apruebas,
y el cliente almacena un token de portador. Cada llamada de herramienta lleva entonces tu identidad —
las tareas de agente muestran "para ⟨tú⟩" en el panel de Tareas, y los tooltips de presencia nombran al propietario.
Las llamadas no autenticadas obtienen un 401 con punteros de descubrimiento WWW-Authenticate
(/.well-known/oauth-authorization-server + oauth-protected-resource), que es lo que
inicia el flujo. El registro dinámico de clientes está habilitado, por lo que no se necesita configuración manual de cliente.
En producción también establece BETTER_AUTH_URL al origen público — las URLs de OAuth se construyen sobre él.
Desplegar
El repositorio incluye una Dockerfile de producción (compilación del cliente + Chromium para capturas de pantalla de marcos).
Cualquier host de contenedores funciona; Railway/Fly son los de menor fricción:
- Crea la aplicación desde este repositorio (ambos detectan automáticamente el Dockerfile).
- Agrega un Postgres administrado y establece
DATABASE_URL. No te saltes esto en despliegues reales — el respaldo PGlite está incrustado/un solo proceso y solo sirve para una instancia única con un volumen persistente montado en/app/data. - Establece
BETTER_AUTH_SECRET(cadena aleatoria larga) yBETTER_AUTH_URL(el origen público, p. ej.https://doop.example.com). Orígenes adicionales permitidos:TRUSTED_ORIGINS(separado por comas). - Verificación de salud:
GET /healthz. El servidor confía en un salto de proxy (trust proxy), por lo que la terminación TLS en el borde de la plataforma funciona de inmediato.
Verificación local de la imagen de producción exacta:
docker build -t doop .
docker run -p 4400:4400 -e BETTER_AUTH_URL=http://localhost:4400 -e BETTER_AUTH_SECRET=dev-only doop
Conectar un agente de IA
El endpoint MCP (HTTP transmisible, sin estado) está en:
http://localhost:4300/mcp
Claude Code:
claude mcp add --transport http doop http://localhost:4300/mcp
Configuración MCP genérica:
{ "mcpServers": { "doop": { "type": "http", "url": "http://localhost:4300/mcp" } } }
Luego dile al agente algo como:
Trabaja en el lienzo
<canvas-id>(mostrado en la barra superior). Llama aget_canvaspara ver los marcos existentes. Para diseñar, crea un marco concreate_frame, luego transmite el diseño en él conappend_frame_htmlen fragmentos de ~300–500 caracteres (start=trueen el primero,done=trueen el último) para que la gente lo vea construirse en vivo. HTML completo con CSS en línea. Después de terminar, llama aget_frame_screenshotpara verlo, corrige lo que se vea mal y vuelve a verificar. Elige unagent_namey reutilízalo en cada llamada.
Las capturas de pantalla se renderizan en tu Chrome/Chromium del sistema a través de puppeteer-core (establece CHROME_PATH si no se
detecta automáticamente). Los humanos pueden acceder al mismo renderizador en GET /api/frames/:id/screenshot.png?scale=2.
Para visualización/importaciones de sitios web, establecer CONTEXT_DEV_API_KEY hace que Context.dev adquiera el HTML
renderizado mientras Doop aún lo sanitiza y renderiza la vista previa localmente; sin la clave, Doop navega
a la página pública directamente en Chromium.
Sincronización de diseño: empuja las pantallas en vivo de una aplicación a un lienzo
La importación del lado del servidor no puede alcanzar aplicaciones detrás de SSO o una VPN. El fragmento doop-sync cambia la captura al navegador del usuario: crea una clave de solo escritura en el diálogo Compartir de un lienzo, coloca una etiqueta en la aplicación —
<script async src="https://your-doop-origin/doop-sync.js?key=dk_…"></script>
— y cada pantalla distinta que visiten las personas aterriza en ese lienzo como un marco (una fila por aplicación), importada
una vez: una breve ventana de gracia permite que la primera captura se asiente (revelaciones de desplazamiento, imágenes tardías), luego el marco
se congela para que visitas posteriores — diferentes viewports, datos de otros usuarios, menús abiertos — nunca lo alteren. Eliminar
un marco lo reimporta en la próxima visita; los contadores de navegación siguen acumulándose independientemente. Las rutas se
normalizan (/orders/8231 → /orders/:id) para que cada pantalla se asigne a un
marco; las capturas se serializan desde el CSSOM (para que la salida de styled-components/emotion sobreviva), y
las fuentes web del mismo origen y las imágenes pequeñas se incrustan como URI de datos — las fuentes requieren CORS dentro del
marco en sandbox, y las URLs de intranet nunca se renderizarían para espectadores fuera de la red. Los scripts se
eliminan tanto del lado del cliente como del servidor, los valores de entrada siempre se descartan, y cualquier cosa marcada como data-doop-mask
se redacta antes de la carga (data-doop-sync-ignore excluye un elemento por completo). La clave es toda la
credencial: solo puede escribir marcos en su único lienzo, por lo que revocarla en el diálogo Compartir corta la
aplicación al instante. Endpoint: POST /ingest/<key> (CORS abierto, sin cookies).
Cómo se ve la transmisión (suavizado del lado del servidor)
El HTML del agente llega a la tienda inmediatamente, pero los espectadores lo ven a través de una revelación de máquina de escribir: el servidor
transmite el HTML acumulado a un ritmo constante (~500 caracteres/s, acelerando para despejar atrasos en ~8s),
por lo que incluso un agente que envía pocos fragmentos grandes — o un set_frame_html / create_frame de una sola vez con
HTML completo — se reproduce como una transmisión en vivo fluida. El HTML a mitad de revelación se sana antes de la transmisión: una etiqueta
a medio escribir al final se descarta, un <script> sin cerrar se corta (nunca se ejecuta JS a medio escribir), y un
<style> sin cerrar se cierra para que el contenido se pinte en lugar de quedar en blanco. Las ediciones humanas del inspector omiten la
revelación (y una edición de HTML humana cancela cualquier revelación abierta — el humano toma el control).
Mientras una transmisión/revelación está abierta, el marco obtiene un borde discontinuo pulsante y un chip "✦ está diseñando…";
"diseño terminado" se registra cuando la revelación se completa. Una transmisión obsoleta se cierra automáticamente después de 30s.
También hay un equivalente REST: POST /api/frames/:id/append con { html_chunk, start?, done?, actor? }.
Cómo aprenden los agentes el flujo de trabajo
La dirección ocurre en tres capas (la misma arquitectura que usa paper.design, más empujones de resultados):
- Servidor
instructionsen la inicialización de MCP — un contrato compacto: carga la guía, obtén contexto primero, transmite diseños, revisa con capturas de pantalla, mantén unagent_name. - Herramienta
get_guide— el manual profundo (puntos de control de revisión obligatorios, flujo de trabajo de transmisión, dimensionamiento de marcos, doctrina de calidad de diseño, etiqueta multijugador), cargado una vez por sesión y recargable después de la compactación de contexto. Fuente:server/guide.ts. - Empujones de resultados — los resultados de
create_frame/set_frame_html/ finalappend_frame_htmlle indican al agente que aún no ha visto su diseño y que debe llamar aget_frame_screenshotantes de continuar.
Herramientas MCP
| Herramienta | Qué hace |
|---|---|
get_guide | El manual del agente — se instruye a los agentes a cargarlo primero |
set_status | Transmite una línea de "en qué estoy trabajando" — se muestra en vivo en la franja de trabajo actual, información sobre herramientas del avatar y feed de actividad |
get_feedback | Obtiene y reclama solicitudes de comentarios humanos abiertas — para agentes cuyo trabajo es sondear el lienzo periódicamente |
get_comments | Lee comentarios y respuestas anclados a elementos, opcionalmente filtrados por marco o estado de resolución, sin reclamar trabajo |
list_canvases | Lista todos los lienzos |
create_canvas | Crea un lienzo, devuelve su id compartible |
get_canvas | Diseño del lienzo: posición/tamaño/metadatos de cada marco |
view_website | Inspecciona una página pública de solo lectura; devuelve una captura de pantalla de escritorio y texto visible sin cambiar el lienzo |
import_webpage | Importa una URL pública a un lienzo como una instantánea/marco HTML editable |
create_frame | Agrega un marco con HTML (colocado automáticamente si no hay x/y) |
get_frame | Lee un marco incluyendo su HTML |
get_frame_screenshot | Renderiza el marco sin interfaz y devuelve un PNG — permite a los agentes ver e iterar en su diseño |
set_frame_html | Reemplaza el diseño de un marco de una sola vez — se renderiza en vivo para todos |
append_frame_html | Transmite un diseño en fragmentos (start=true primero, done=true último) — los espectadores lo ven construirse |
edit_frame_html | Búsqueda y reemplazo exacta y dirigida en el HTML de un marco — se transforma en el renderizado en el lugar |
update_frame | Renombra / mueve / redimensiona un marco |
delete_frame | Elimina un marco |
Las herramientas de mutación aceptan agent_name; el agente aparece entonces en la pila de presencia (avatar cuadrado pulsante),
obtiene un anillo "editando" + etiqueta en el marco que tocó, y sus acciones aparecen en el feed de actividad. Los agentes
expiran de la presencia después de ~20s de inactividad (~60s mientras tienen un estado publicado, ya que un estado
generalmente significa que el agente está pensando entre llamadas a herramientas).
La propiedad de agente a humano proviene del token OAuth: el token portador identifica quién aprobó la conexión, y ese usuario aparece como el propietario del agente en tareas y presencia.
Narración de tareas en vivo
Los agentes son dirigidos (instrucciones + guía) a llamar a set_status con un resumen de una línea, en tiempo presente,
cuando comienzan una tarea y cada vez que cambian su enfoque — p. ej. "Dibujando un flujo de incorporación móvil".
Los estados aparecen en una franja flotante trabajando ahora en la parte inferior izquierda del lienzo (punto pulsante en el
color del agente), en la información sobre herramientas del avatar de presencia, y como una entrada del feed de actividad, para que siempre sepas qué
está haciendo cada agente incluso mientras piensa en silencio. Una cadena vacía borra el estado; también
expira con la presencia del agente.
Cada estado también se convierte en una tarea: publicar un nuevo estado completa el anterior, borrar (o
quedar en silencio) termina la tarea abierta. Los agentes que nunca llaman a set_status aún aparecen: el servidor
infiere una tarea de lo que hacen visiblemente ("Diseñando 'Hero'", en cursiva en el panel), la cierra
cuando la transmisión termina, y los empuja en los resultados de herramientas para que comiencen a anunciar — para que el panel funcione
incluso para sesiones que se conectaron antes de que existiera la herramienta o que omitieron la guía. El panel lateral está dividido en dos pestañas — Tareas muestra el historial
por agente (tarea activa pulsante con duración en curso, finalizadas marcadas con cuánto tiempo
tomaron), estilo panel de agente Cursor; Actividad es el feed de eventos crudo. El historial de tareas sobrevive a la salida
de los agentes y se envía a los que se unen tarde.
Dirigiendo agentes: comentarios sobre tareas
Pasa el cursor sobre cualquier tarea en la pestaña Tareas y presiona ↩ para dejar comentarios (p. ej. "haz el acento más cálido").
Cada nota se convierte en una solicitud abierta en el lienzo — un elemento de trabajo, no correo para el agente cuya
tarea era. MCP es basado en extracción, por lo que la entrega viaja en la capa de empujones de resultados: la próxima llamada de agente identificada
en el lienzo que admita entrega de comentarios (llevando un agent_name, quien sea) devuelve un
bloque HUMAN FEEDBACK citando la nota, diciendo de quién es el trabajo que concierne, e instruyendo al agente
a abordarlo antes de continuar — incluyendo editar el marco de otro agente (una solicitud humana
anula la etiqueta de no tocar). Recogerlo lo reclama: la interfaz cambia de "→ esperando un
agente…" a "✓ recogido por ⟨agente⟩", y cada nota se reclama exactamente una vez.
Los agentes no se quedan esperando respuestas — las sesiones terminan cuando su trabajo termina. Las solicitudes abiertas simplemente
esperan al próximo agente que aparezca: el agente original en una sesión posterior, un agente diferente
ya en el lienzo, o uno nuevo que generes ("revisa el lienzo ⟨id⟩"). Para un
cuidador dedicado, apunta a un agente a get_feedback — una búsqueda y reclamo no bloqueante diseñada para un
bucle de "revisa el lienzo cada pocos minutos, aborda lo que los humanos solicitaron".
Equivalente REST: POST /api/tasks/:id/feedback con { text, from }.
Leyendo comentarios de elementos a través de MCP
Llama a get_comments({ canvas_id }) para leer los comentarios y respuestas de elementos retenidos del lienzo
(hasta 100 entradas, más recientes primero). Cada entrada incluye su ID, ID de marco, autor, texto, marca de tiempo,
selector CSS, fragmento HTML, y cualquier metadato de reclamo, fallo o resolución. Las respuestas llevan un
parentId apuntando a su comentario raíz.
Pasa frame_id para leer solo comentarios en un marco que pertenezca a ese lienzo, o
include_resolved: false para excluir entradas resueltas. Las entradas resueltas se incluyen por defecto
para que el contexto de la conversación permanezca disponible. Un resultado vacío es []. La herramienta aplica los mismos
permisos de acceso al lienzo que otras lecturas MCP; el agent_name opcional anuncia presencia.
No reclama comentarios de tareas ni comentarios, ni marca nada como resuelto.
Qué hay en la caja
- Lienzo infinito — rueda para desplazarse,
⌘/ctrl+ rueda (o pellizco) para hacer zoom, arrastra el fondo para desplazarte, zoom para ajustar; la cuadrícula de puntos sigue la ventana gráfica. - Marcos — arrastra para mover, asa de esquina para redimensionar, clic para seleccionar. El inspector de la derecha edita
nombre/posición/tamaño y el HTML crudo con guardados en vivo con retardo.
⌫elimina el marco seleccionado. - Multijugador — cursores en vivo con etiquetas de nombre, avatares de presencia, indicadores por marco de "quién está editando", destello de color cuando un actor remoto cambia un marco, posiciones de arrastre transmitidas en vivo, reconexión automática.
- Feed de actividad — cada crear/editar/renombrar/eliminar, por quién (usuario o agente), con marcas de tiempo.
- Compartir — la URL del lienzo es el enlace de compartir (el botón
Sharelo copia). - Modal de conectar IA — copia y pega las instrucciones de configuración de MCP desde la propia aplicación.
Arquitectura
server/ Node (tsx) — one process on :4400
index.ts Express REST API + ws rooms + presence + static serving (prod)
store.ts In-memory canvas/frame state (hot path), write-through to the DB
db/ Drizzle schema + PGlite/Postgres connection + write-through persistence
actions.ts Shared mutations: broadcast + activity log + agent presence
mcp.ts MCP server (@modelcontextprotocol/sdk), stateless streamable HTTP at /mcp
seed.ts Demo canvas on first run
shared/types.ts Store + ws protocol types shared by server and client
src/ React + Vite + zustand client on :4300
components/ui/ The component system — every styled primitive lives here
styles.css Design tokens, the base reset, and keyframes. Nothing else.
Estilo
La apariencia de Doop es un sistema de componentes, no una hoja de estilos. src/components/ui/ contiene los
primitivos — Button, Input, Badge, Card, Panel, Modal, Menu, Toolbar,
Segmented, Dash* y el resto — cada uno una receta de Tailwind + CVA vinculada
a los tokens en styles.css. Las pantallas componen esos; no re-describen bordes,
sombras o escalas tipográficas. Si un patrón aparece dos veces, pertenece en ui/.
src/styles.css es deliberadamente pequeño: los tokens :root (--ink, --paper, --brand…),
su mapeo @theme inline a nombres de Tailwind, el reinicio base, y las utilidades @keyframes
no pueden expresar. Los componentes referencian esas animaciones por nombre, por lo que los nombres son
API. --breakpoint-md (900px) es el límite móvil y useIsMobile() lo iguala en JS —
cámbialos juntos.
El HTML del marco se renderiza en <iframe sandbox="allow-scripts"> — los scripts se ejecutan, pero sin acceso de mismo origen y
sin alcance a la aplicación. Cada iframe carga un pequeño bootstrap una vez; el nuevo HTML se postMessaged y
se transforma el DOM en el lugar (src/lib/frameRuntime.ts), por lo que las actualizaciones y los ticks de transmisión nunca parpadean en blanco
el marco con una recarga completa del documento. Los <script>s cambiados se re-ejecutan; los estilos/fuentes sin cambios se
dejan intactos. La capa en tiempo real es JSON simple sobre una sala WebSocket por lienzo;
las mutaciones REST/MCP se transmiten a la sala por la capa de acciones compartidas, por lo que las ediciones humanas y de agentes pasan
por una plomería idéntica.
Contribuyendo
PRs bienvenidos — consulta CONTRIBUTING.md para convenciones de commits y estilo de código.
bun run test ejecuta la suite de integración (inicia el servidor real contra una base de datos desechable);
los cambios de esquema pasan por migraciones drizzle (npx drizzle-kit generate después de editar
server/db/schema.ts). Problemas de seguridad: consulta SECURITY.md — por favor reporta en privado.
Licencia
Doop es código abierto bajo la GNU AGPL v3. En resumen: úsalo, auto-alójalo, modifícalo — pero si ofreces una versión modificada como servicio, debes publicar tus cambios bajo la misma licencia.
El nombre y logotipo de Doop son marcas comerciales y no están cubiertos por la licencia de código — por favor, renombra los servicios derivados.