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 — the open-source alternative to Paper.design: humans and AI agents designing together, live

CI License: AGPL-3.0 Doop Cloud PRs welcome Discord

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.

A doop canvas: three frames of a ceramics brand — landing hero, mobile product page and brand tokens

  • 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_KEY del 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 comandodocker compose up, o bun run dev con 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.

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.

Claude Code connected over MCP OAuth, streaming a pricing-section design into a frame while the humans on the canvas watch it work

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.

The Doop Agent streaming a design into a frame, live — working status, agent task panel and pulsing frame border

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 en model_accounts y 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 DoopFlujoLo que hace el usuario
Misma máquina que el navegador (dev, auto-alojado)Captura de bucle local — Doop mantiene 127.0.0.1:1455Aprueba 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 deshabilitadosRedirección del navegador + pegarPega 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=1 desactiva 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.

VariablePredeterminadoQué hace
DOOP_AGENT_PROVIDERanthropicEn qué se ejecuta el nivel gratuito: anthropic | azure
ANTHROPIC_API_KEYsin configurarPaga por el nivel gratuito del Agente Doop (proveedor predeterminado) y el destilador
AZURE_OPENAI_ENDPOINTsin configurarEl recurso Azure OpenAI del nivel gratuito, cuando DOOP_AGENT_PROVIDER=azure
AZURE_OPENAI_API_KEYsin configurarUna clave de ese recurso
AZURE_OPENAI_DEPLOYMENTsin configurarEl despliegue en el que se ejecuta el nivel gratuito
AZURE_OPENAI_API_VERSIONsin configurarFija un parámetro de consulta api-version; la superficie v1 no necesita ninguno
AZURE_OPENAI_REASONING_EFFORTsin configurarEsfuerzo de razonamiento en ejecuciones de Azure; sin configurar no envía ninguno (no seguro para razonamiento)
RESIDENT_TASK_LIMIT0Tareas gratuitas del Agente Doop por cuenta; 0 significa una cuenta conectada desde la primera tarea
DOOP_AGENT_MODELclaude-opus-5Modelo para el Agente Doop en la clave Anthropic del servidor
DOOP_AGENT_OPENAI_MODELgpt-5.6-terraNivel predeterminado en la cuenta de un usuario; cada usuario puede elegir otro en Configuración
CHATGPT_CONNECT_DISABLEDsin configurar1 oculta el flujo de ChatGPT, dejando la ruta de clave de API
DOOP_DISTILL_MODELclaude-haiku-4-5-20251001Modelo 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.

The share modal: invite collaborators by email, see who has access, and toggle link sharing

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:

  1. Crea la aplicación desde este repositorio (ambos detectan automáticamente el Dockerfile).
  2. 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.
  3. Establece BETTER_AUTH_SECRET (cadena aleatoria larga) y BETTER_AUTH_URL (el origen público, p. ej. https://doop.example.com). Orígenes adicionales permitidos: TRUSTED_ORIGINS (separado por comas).
  4. 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 a get_canvas para ver los marcos existentes. Para diseñar, crea un marco con create_frame, luego transmite el diseño en él con append_frame_html en fragmentos de ~300–500 caracteres (start=true en el primero, done=true en el último) para que la gente lo vea construirse en vivo. HTML completo con CSS en línea. Después de terminar, llama a get_frame_screenshot para verlo, corrige lo que se vea mal y vuelve a verificar. Elige un agent_name y 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):

  1. Servidor instructions en 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 un agent_name.
  2. 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.
  3. Empujones de resultados — los resultados de create_frame / set_frame_html / final append_frame_html le indican al agente que aún no ha visto su diseño y que debe llamar a get_frame_screenshot antes de continuar.

Herramientas MCP

HerramientaQué hace
get_guideEl manual del agente — se instruye a los agentes a cargarlo primero
set_statusTransmite 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_feedbackObtiene y reclama solicitudes de comentarios humanos abiertas — para agentes cuyo trabajo es sondear el lienzo periódicamente
get_commentsLee comentarios y respuestas anclados a elementos, opcionalmente filtrados por marco o estado de resolución, sin reclamar trabajo
list_canvasesLista todos los lienzos
create_canvasCrea un lienzo, devuelve su id compartible
get_canvasDiseño del lienzo: posición/tamaño/metadatos de cada marco
view_websiteInspecciona una página pública de solo lectura; devuelve una captura de pantalla de escritorio y texto visible sin cambiar el lienzo
import_webpageImporta una URL pública a un lienzo como una instantánea/marco HTML editable
create_frameAgrega un marco con HTML (colocado automáticamente si no hay x/y)
get_frameLee un marco incluyendo su HTML
get_frame_screenshotRenderiza el marco sin interfaz y devuelve un PNG — permite a los agentes ver e iterar en su diseño
set_frame_htmlReemplaza el diseño de un marco de una sola vez — se renderiza en vivo para todos
append_frame_htmlTransmite un diseño en fragmentos (start=true primero, done=true último) — los espectadores lo ven construirse
edit_frame_htmlBúsqueda y reemplazo exacta y dirigida en el HTML de un marco — se transforma en el renderizado en el lugar
update_frameRenombra / mueve / redimensiona un marco
delete_frameElimina 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 Share lo 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.