@imenam/mcp-design 1.0.13 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -335,7 +335,7 @@ Trois colonnes : la liste des planches, le canvas, l'inspecteur et les calques.
335
335
 
336
336
  Raccourcis : `V` `R` `O` `L` `T` `I` (outils), `Ctrl+Z` / `Ctrl+Y` (annuler/rétablir), `Ctrl+G` / `Ctrl+Maj+G` (grouper / dissoudre), `Ctrl+D` (dupliquer), `Ctrl+A` (tout sélectionner), `Suppr`, flèches (déplacer d'1 px, 10 px avec `Maj`), `Échap`.
337
337
 
338
- La sauvegarde est automatique et différée. Une modification faite par l'agent pendant que la GUI est ouverte y apparaît en direct, via SSE ; une modification faite dans la GUI est enregistrée comme venant de l'utilisateur, et c'est ce que `get_changes` restitue.
338
+ La sauvegarde est automatique et différée. Une modification faite par l'agent pendant que la GUI est ouverte y apparaît dans les 3 secondes : la GUI interroge `GET /api/changes` toutes les 3 s ; une modification faite dans la GUI est enregistrée comme venant de l'utilisateur, et c'est ce que `get_changes` restitue.
339
339
 
340
340
  Le point ambre d'une page indique qu'elle porte des modifications que l'agent n'a pas encore acquittées avec `mark_synced` ; leur nombre s'affiche au survol.
341
341
 
@@ -366,9 +366,9 @@ Les retours créés avant l'arrivée des noms reçoivent « Retour n°N », et a
366
366
  Deux processus, conformément au standard de l'écosystème (`@imenam/mcp-gui-interface`) :
367
367
 
368
368
  - **MCP Master** (`src/index.ts`) — protocole JSON-RPC sur stdio, stockage des designs, cycle de vie du worker via `GuiLauncher`.
369
- - **GUI Worker** (`src/gui-worker.ts`) — serveur Hono, enregistrement auprès du proxy via `ProxyClient`, API REST + SSE, sert la SPA React.
369
+ - **GUI Worker** (`src/gui-worker.ts`) — serveur Hono, enregistrement auprès du proxy via `ProxyClient`, API REST, sert la SPA React.
370
370
 
371
- Les deux communiquent par IPC. Le worker se termine proprement quand le master disparaît (canal IPC fermé, PID parent absent) et quand une autre instance est déjà enregistrée (HTTP 409), ce qui évite les GUI orphelines.
371
+ Les deux communiquent par IPC. Les évènements que le master pousse au worker (`DESIGNS_UPDATED`, `DESIGN_UPDATED`, `REMARKS_UPDATED`) entrent dans un journal en mémoire, numéroté et limité aux 200 dernières entrées. La GUI le relit toutes les 3 s par `GET /api/changes?since=<seq>&epoch=<epoch>`, qui répond immédiatement : aucune connexion ne reste ouverte, ce qui laisse libres les connexions que le navigateur accorde à l'origine du proxy, partagée par toutes les GUI. La réponse vaut `{ epoch, seq, reset: false, events }` avec les évènements postérieurs à `since`, ou `{ epoch, seq, reset: true }` quand la position du client n'est pas exploitable (premier appel, worker redémarré, entrées déjà sorties du journal) : la GUI recharge alors la liste des pages, les remarques et la page ouverte. Un `since` qui n'est pas un entier positif ou nul est refusé (400). Le worker se termine proprement quand le master disparaît (canal IPC fermé, PID parent absent) et quand une autre instance est déjà enregistrée (HTTP 409), ce qui évite les GUI orphelines.
372
372
 
373
373
  `shared/feedback.ts` porte le modèle des remarques, la détection des éléments d'une zone et la mise en forme d'un retour ; le serveur et la GUI l'importent tous deux, ce qui garantit que l'aperçu affiché est le texte transmis. `shared/render.ts` contient le rendu SVG d'un élément, `shared/tree.ts` la résolution de la hiérarchie en coordonnées document, `shared/instances.ts` l'expansion des instances en groupes, `shared/node-defaults.ts` les valeurs par défaut et `shared/icons/` la bibliothèque d'icônes. Tous sont importés **à la fois** par l'exporteur côté serveur et par le canvas côté navigateur : ce que l'utilisateur voit est exactement ce que `export_design` — et donc `screenshot_design`, qui rasterise ce même SVG via resvg — produit.
374
374
 
@@ -1,10 +1,10 @@
1
1
  import { serve } from "@hono/node-server";
2
2
  import { serveStatic } from "@hono/node-server/serve-static";
3
3
  import { Hono } from "hono";
4
- import { streamSSE } from "hono/streaming";
5
4
  import { GUI_LISTEN_HOST, ProxyClient, createLogger, honoGuiGuard, setupLogging } from "@imenam/mcp-gui-interface";
6
5
  import path from "path";
7
6
  import fs from "fs";
7
+ import { randomUUID } from "crypto";
8
8
  import { fileURLToPath } from "url";
9
9
  import { IPCMessageSchema } from "../shared/schemas/design.schema.js";
10
10
  import { resolveLogDir } from "./core/log-dir.js";
@@ -79,7 +79,6 @@ const parentCheckInterval = setInterval(() => {
79
79
  const app = new Hono();
80
80
  app.use("*", honoGuiGuard());
81
81
  let proxyClient = null;
82
- const sseControllers = new Set();
83
82
  const pendingRequests = new Map();
84
83
  function safeSend(message) {
85
84
  if (process.send && process.connected) {
@@ -115,7 +114,23 @@ function ipcRequest(type, data, timeout = 5000) {
115
114
  }
116
115
  });
117
116
  }
118
- const PUSHED_EVENTS = new Set(["INITIAL_STATE", "DESIGNS_UPDATED", "DESIGN_UPDATED", "REMARKS_UPDATED"]);
117
+ const PUSHED_EVENTS = new Set(["DESIGNS_UPDATED", "DESIGN_UPDATED", "REMARKS_UPDATED"]);
118
+ /**
119
+ * Journal des évènements poussés par le master, relu par la GUI via
120
+ * `/api/changes`. Chaque entrée porte un numéro croissant ; seules les
121
+ * `JOURNAL_LIMIT` dernières sont gardées. `epoch` change à chaque démarrage du
122
+ * worker : un client qui le voit changer sait que sa position ne vaut plus rien.
123
+ */
124
+ const JOURNAL_LIMIT = 200;
125
+ const epoch = randomUUID();
126
+ let lastSeq = 0;
127
+ const journal = [];
128
+ function appendToJournal(event) {
129
+ lastSeq += 1;
130
+ journal.push({ seq: lastSeq, event });
131
+ if (journal.length > JOURNAL_LIMIT)
132
+ journal.splice(0, journal.length - JOURNAL_LIMIT);
133
+ }
119
134
  function setupIpcErrorHandlers() {
120
135
  process.on("error", (err) => {
121
136
  if (err.code === "ERR_IPC_CHANNEL_CLOSED")
@@ -123,7 +138,7 @@ function setupIpcErrorHandlers() {
123
138
  logger.error(`Process error: ${err.message}`);
124
139
  });
125
140
  process.on("disconnect", () => {
126
- logger.warn("IPC disconnected from MCP master. GUI worker will exit; EventSource clients will show Disconnected.");
141
+ logger.warn("IPC disconnected from MCP master. GUI worker will exit.");
127
142
  process.exit(0);
128
143
  });
129
144
  process.on("message", (msg) => {
@@ -143,16 +158,8 @@ function setupIpcErrorHandlers() {
143
158
  });
144
159
  return;
145
160
  }
146
- if (msg && PUSHED_EVENTS.has(msg.type)) {
147
- for (const controller of sseControllers) {
148
- try {
149
- controller.writeSSE({ data: JSON.stringify(msg), event: "message" });
150
- }
151
- catch {
152
- sseControllers.delete(controller);
153
- }
154
- }
155
- }
161
+ if (msg && PUSHED_EVENTS.has(msg.type))
162
+ appendToJournal(msg);
156
163
  });
157
164
  }
158
165
  // --- Routes HTTP ------------------------------------------------------------
@@ -167,45 +174,27 @@ app.get("/proxy/health", (c) => {
167
174
  timestamp: new Date().toISOString(),
168
175
  });
169
176
  });
170
- app.get("/api/events", (c) => streamSSE(c, async (stream) => {
171
- const clientId = Math.random().toString(36).slice(2, 11);
172
- sseControllers.add(stream);
173
- logger.info(`SSE /api/events: client connected (id=${clientId})`);
174
- stream.onAbort(() => {
175
- sseControllers.delete(stream);
176
- logger.warn(`SSE /api/events: stream aborted (id=${clientId}).`);
177
- });
178
- if (!safeSend({ type: "GET_INITIAL_STATE", timestamp: new Date().toISOString() })) {
179
- logger.warn(`SSE /api/events: GET_INITIAL_STATE not sent (id=${clientId}) — IPC unavailable.`);
180
- }
181
- try {
182
- await stream.writeSSE({ data: "connected", event: "ping" });
183
- }
184
- catch (err) {
185
- logger.warn(`SSE /api/events: initial writeSSE failed (id=${clientId}): ${err?.message ?? err}`);
186
- sseControllers.delete(stream);
187
- return;
188
- }
189
- while (true) {
190
- await stream.sleep(30000);
191
- try {
192
- await stream.writeSSE({ data: "ping", event: "ping" });
193
- }
194
- catch (err) {
195
- logger.warn(`SSE /api/events: keepalive ping failed (id=${clientId}): ${err?.message ?? err}`);
196
- sseControllers.delete(stream);
197
- break;
198
- }
199
- }
200
- }));
201
- app.post("/api/client-diagnostics", async (c) => {
202
- try {
203
- logger.warn(`Client diagnostics (SSE/EventSource): ${JSON.stringify(await c.req.json())}`);
204
- }
205
- catch (e) {
206
- logger.warn(`Client diagnostics: invalid JSON body (${e?.message ?? e})`);
207
- }
208
- return c.json({ ok: true });
177
+ /**
178
+ * Évènements survenus depuis la position `since` du client. `reset: true`
179
+ * quand cette position n'est pas exploitable — absente, d'un autre démarrage du
180
+ * worker, plus ancienne que le journal ou en avance sur lui : le client
181
+ * recharge alors tout et repart de `seq`.
182
+ */
183
+ app.get("/api/changes", (c) => {
184
+ const rawSince = c.req.query("since");
185
+ const clientEpoch = c.req.query("epoch");
186
+ if (rawSince !== undefined && !/^\d+$/.test(rawSince)) {
187
+ return c.json({ error: `Invalid "since" parameter: expected a non-negative integer, got "${rawSince}"` }, 400);
188
+ }
189
+ const reset = { epoch, seq: lastSeq, reset: true };
190
+ if (rawSince === undefined || !clientEpoch || clientEpoch !== epoch)
191
+ return c.json(reset);
192
+ const since = Number(rawSince);
193
+ const oldestSeq = journal.length > 0 ? journal[0].seq : lastSeq + 1;
194
+ if (since > lastSeq || since < oldestSeq - 1)
195
+ return c.json(reset);
196
+ const events = journal.filter((entry) => entry.seq > since).map((entry) => entry.event);
197
+ return c.json({ epoch, seq: lastSeq, reset: false, events });
209
198
  });
210
199
  app.get("/api/config", async (c) => {
211
200
  const result = await ipcRequest("GET_CONFIG");
package/dist/src/index.js CHANGED
@@ -140,13 +140,6 @@ function handleIPCMessage(msg) {
140
140
  logger.info(`GUI ready at ${guiUrl}`);
141
141
  guiStartWaiter?.({ outcome: "ready", url: msg.data?.url });
142
142
  return;
143
- case "GET_INITIAL_STATE":
144
- broadcastToGUI({
145
- type: "INITIAL_STATE",
146
- data: { designs: store.list(), version: APP_VERSION },
147
- timestamp: new Date().toISOString(),
148
- });
149
- return;
150
143
  case "LIST_DESIGNS":
151
144
  reply(msg, "LIST_DESIGNS_RESPONSE", { designs: store.list() });
152
145
  return;