Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -840,7 +840,15 @@ async function notifyPreviewWhenReady(room: Room): Promise<void> {
// como una pantalla muerta.
io.to(room.id).emit("preview:arrancando", { etapa });
});
if (url) io.to(room.id).emit("preview:ready", { previewUrl: url });
if (url) {
io.to(room.id).emit("preview:ready", { previewUrl: url });
return;
}

// Sin URL: o la sala sigue vacía (normal, no hay nada que levantar) o el
// arranque falló. En ambos casos hay que avisar, porque quien vio una etapa se
// quedaría con el spinner girando para siempre esperando un ready que no llega.
io.to(room.id).emit("preview:sin-arranque");
}

// ── Arranque ────────────────────────────────────────────────────────────────
Expand Down
4 changes: 4 additions & 0 deletions server/src/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ export async function loadRoomIndex(): Promise<number> {
* Una sala recién creada está vacía: no hay nada que levantar, y eso es normal
* (el agente todavía no scaffoldea). No es error ni espera indefinida — el
* preview arranca solo cuando aparezca algo, vía `maybeStartPreview`.
*
* Esto NO emite nada por el socket, a propósito: corre al despertar la sala,
* cuando todavía no hay nadie conectado a quien avisar. Quien entre después
* recibe la URL en el `joined`; ese es el canal para este caso.
*/
async function bootPreview(room: Room): Promise<void> {
try {
Expand Down
11 changes: 10 additions & 1 deletion web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,12 @@ function Sala({ roomId, name }: { roomId: string; name: string }) {
// El chat que ya existía en la sala (sobrevivió al reinicio).
if (p.messages?.length) setMessages(p.messages);
// Llegaste mientras se levantaba: el evento de etapa ya pasó.
if (p.previewArrancando) setArrancando("servidor");
//
// Solo si NO hay preview todavía. El arranque puede haber terminado antes
// de que entraras (la sala se despierta sola al primer request), y entonces
// el `preview:ready` ya pasó y no vuelve: sin esta condición el spinner se
// quedaba girando encima de un preview que sí existía.
if (p.previewArrancando && !p.previewUrl) setArrancando("servidor");
});
socket.on("presence", ({ members }: { members: Member[] }) => setMembers(members));
socket.on("preview:ready", () => {
Expand All @@ -275,6 +280,10 @@ function Sala({ roomId, name }: { roomId: string; name: string }) {
socket.on("preview:arrancando", ({ etapa }: { etapa: "contenedor" | "dependencias" | "servidor" }) =>
setArrancando(etapa),
);
// El arranque terminó sin preview: la sala sigue vacía o algo falló. Se quita
// el spinner y vuelve el mensaje de "pídele a un agente que arranque el
// proyecto", que es lo accionable.
socket.on("preview:sin-arranque", () => setArrancando(null));
socket.on("agents", ({ agents }: { agents: Agent[] }) => setAgents(agents));
// Hay un punto nuevo en la línea de tiempo (commit, revert o bookmark).
socket.on("history:new", () => setHistVersion((v) => v + 1));
Expand Down
Loading