@imenam/mcp-design 1.0.10 → 1.0.12

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.
Files changed (51) hide show
  1. package/README.md +67 -5
  2. package/dist/shared/feedback.d.ts +128 -0
  3. package/dist/shared/feedback.js +165 -0
  4. package/dist/shared/node-defaults.js +6 -1
  5. package/dist/shared/node-fields.d.ts +2 -0
  6. package/dist/shared/node-fields.js +5 -1
  7. package/dist/shared/render.d.ts +30 -5
  8. package/dist/shared/render.js +58 -11
  9. package/dist/shared/schemas/design.schema.d.ts +36 -1
  10. package/dist/shared/schemas/design.schema.js +10 -1
  11. package/dist/shared/schemas/feedback.schema.d.ts +191 -0
  12. package/dist/shared/schemas/feedback.schema.js +87 -0
  13. package/dist/shared/tree.d.ts +2 -0
  14. package/dist/shared/tree.js +4 -0
  15. package/dist/src/core/design-store.d.ts +4 -5
  16. package/dist/src/core/design-store.js +7 -56
  17. package/dist/src/core/export.d.ts +2 -1
  18. package/dist/src/core/export.js +35 -7
  19. package/dist/src/core/feedback-store.d.ts +73 -0
  20. package/dist/src/core/feedback-store.js +308 -0
  21. package/dist/src/core/file-lock.d.ts +8 -0
  22. package/dist/src/core/file-lock.js +61 -0
  23. package/dist/src/core/image-store.d.ts +45 -0
  24. package/dist/src/core/image-store.js +175 -0
  25. package/dist/src/core/node-factory.js +16 -0
  26. package/dist/src/core/screenshot.d.ts +6 -0
  27. package/dist/src/core/screenshot.js +32 -3
  28. package/dist/src/gui-worker.js +113 -1
  29. package/dist/src/index.js +231 -3
  30. package/dist/src/tools/add-image.d.ts +3 -0
  31. package/dist/src/tools/add-image.js +65 -0
  32. package/dist/src/tools/add-nodes.js +2 -0
  33. package/dist/src/tools/delete-design.d.ts +3 -1
  34. package/dist/src/tools/delete-design.js +4 -1
  35. package/dist/src/tools/export-design.js +1 -1
  36. package/dist/src/tools/get-feedback.d.ts +3 -0
  37. package/dist/src/tools/get-feedback.js +62 -0
  38. package/dist/src/tools/mark-feedback-processed.d.ts +8 -0
  39. package/dist/src/tools/mark-feedback-processed.js +29 -0
  40. package/dist/src/tools/screenshot-design.js +1 -0
  41. package/dist/src/tools/update-nodes.js +4 -1
  42. package/dist/src/version.d.ts +1 -1
  43. package/dist/src/version.js +16 -2
  44. package/gui/dist/assets/index-DNUHDrGi.css +1 -0
  45. package/gui/dist/assets/index-uk5sDkVA.js +15 -0
  46. package/gui/dist/assets/{search-CSwiX0lv.js → search-jSqY2fPS.js} +1 -1
  47. package/gui/dist/index.html +2 -2
  48. package/package.json +2 -3
  49. package/gui/dist/assets/index-DBZNXgF_.js +0 -10
  50. package/gui/dist/assets/index-PQqxUQLx.css +0 -1
  51. package/scripts/generate-version.js +0 -20
package/dist/src/index.js CHANGED
@@ -10,13 +10,14 @@ import { GuiLauncher, createLogger, setupLogging } from "@imenam/mcp-gui-interfa
10
10
  import { APP_VERSION } from "./version.js";
11
11
  import { resolveLogDir } from "./core/log-dir.js";
12
12
  import { DesignStore } from "./core/design-store.js";
13
+ import { FeedbackStore } from "./core/feedback-store.js";
13
14
  import { NODE_DEFAULTS } from "./core/node-factory.js";
14
15
  import { EXPORT_FORMATS } from "./core/export.js";
15
16
  import { MIN_LINE_HEIGHT_PX } from "../shared/render.js";
16
17
  import { CORE_ICONS, iconCount } from "../shared/icons/index.js";
17
18
  import { NODE_FIELD_DOCS } from "../shared/node-fields.js";
18
19
  import { REORDER_DIRECTIONS } from "../shared/tree.js";
19
- import { normalizeDesignGroup } from "../shared/schemas/design.schema.js";
20
+ import { IMAGE_FITS, NODE_TYPES, normalizeDesignGroup } from "../shared/schemas/design.schema.js";
20
21
  import { listDesigns } from "./tools/list-designs.js";
21
22
  import { createDesign } from "./tools/create-design.js";
22
23
  import { duplicateDesign } from "./tools/duplicate-design.js";
@@ -24,6 +25,8 @@ import { getDesign } from "./tools/get-design.js";
24
25
  import { updateDesign } from "./tools/update-design.js";
25
26
  import { deleteDesign } from "./tools/delete-design.js";
26
27
  import { addNodes } from "./tools/add-nodes.js";
28
+ import { addImage } from "./tools/add-image.js";
29
+ import { IMAGE_FORMATS } from "./core/image-store.js";
27
30
  import { updateNodes } from "./tools/update-nodes.js";
28
31
  import { deleteNodes } from "./tools/delete-nodes.js";
29
32
  import { duplicateNodes, MAX_DUPLICATE_COPIES } from "./tools/duplicate-nodes.js";
@@ -32,6 +35,8 @@ import { groupDesigns } from "./tools/group-designs.js";
32
35
  import { ungroupNodes } from "./tools/ungroup-nodes.js";
33
36
  import { detachInstances } from "./tools/detach-instances.js";
34
37
  import { getChanges } from "./tools/get-changes.js";
38
+ import { getFeedback } from "./tools/get-feedback.js";
39
+ import { markFeedbackProcessed } from "./tools/mark-feedback-processed.js";
35
40
  import { markSynced } from "./tools/mark-synced.js";
36
41
  import { exportDesignTool } from "./tools/export-design.js";
37
42
  import { screenshotDesignTool } from "./tools/screenshot-design.js";
@@ -74,6 +79,7 @@ function getConfig() {
74
79
  }
75
80
  getConfig();
76
81
  const store = new DesignStore(rootDir);
82
+ const feedback = new FeedbackStore(store);
77
83
  const server = new Server({ name: "mcp-design", version: APP_VERSION }, { capabilities: { tools: {} } });
78
84
  // --- Cycle de vie de la GUI -------------------------------------------------
79
85
  let guiLauncher;
@@ -97,6 +103,20 @@ function reply(msg, type, data) {
97
103
  function replyError(msg, type, error) {
98
104
  broadcastToGUI({ type, correlationId: msg.correlationId, error, timestamp: new Date().toISOString() });
99
105
  }
106
+ /** État des remarques renvoyé à la GUI après chaque lecture ou modification. */
107
+ function remarksState() {
108
+ return { remarks: feedback.remarks(), rounds: feedback.rounds(), file: feedback.getFilePath() };
109
+ }
110
+ /** Prévient les autres onglets de la GUI que les remarques ont changé. */
111
+ function notifyRemarksChanged() {
112
+ broadcastToGUI({ type: "REMARKS_UPDATED", data: {}, timestamp: new Date().toISOString() });
113
+ }
114
+ /** Supprime un design et ses remarques. */
115
+ function deleteDesignAndRemarks(id) {
116
+ store.delete(id);
117
+ feedback.deleteDesign(id);
118
+ notifyRemarksChanged();
119
+ }
100
120
  /** Notifie la GUI qu'un design a changé côté agent, pour qu'elle se rafraîchisse. */
101
121
  function notifyDesignChanged(designId) {
102
122
  broadcastToGUI({
@@ -163,7 +183,7 @@ function handleIPCMessage(msg) {
163
183
  return;
164
184
  case "DELETE_DESIGN":
165
185
  try {
166
- store.delete(String(msg.data?.id));
186
+ deleteDesignAndRemarks(String(msg.data?.id));
167
187
  reply(msg, "DELETE_DESIGN_RESPONSE", { designs: store.list() });
168
188
  }
169
189
  catch (err) {
@@ -181,6 +201,117 @@ function handleIPCMessage(msg) {
181
201
  replyError(msg, "SET_DESIGN_GROUP_RESPONSE", err.message);
182
202
  }
183
203
  return;
204
+ case "LIST_REMARKS":
205
+ try {
206
+ reply(msg, "LIST_REMARKS_RESPONSE", remarksState());
207
+ }
208
+ catch (err) {
209
+ replyError(msg, "LIST_REMARKS_RESPONSE", err.message);
210
+ }
211
+ return;
212
+ case "ADD_REMARK":
213
+ try {
214
+ const remark = feedback.add({
215
+ designId: String(msg.data?.designId ?? ""),
216
+ target: msg.data?.target,
217
+ text: String(msg.data?.text ?? ""),
218
+ capture: msg.data?.capture === true,
219
+ });
220
+ notifyRemarksChanged();
221
+ reply(msg, "ADD_REMARK_RESPONSE", { remark, ...remarksState() });
222
+ }
223
+ catch (err) {
224
+ replyError(msg, "ADD_REMARK_RESPONSE", err.message);
225
+ }
226
+ return;
227
+ case "UPDATE_REMARK":
228
+ try {
229
+ const remark = feedback.updateText(String(msg.data?.id ?? ""), String(msg.data?.text ?? ""));
230
+ notifyRemarksChanged();
231
+ reply(msg, "UPDATE_REMARK_RESPONSE", { remark, ...remarksState() });
232
+ }
233
+ catch (err) {
234
+ replyError(msg, "UPDATE_REMARK_RESPONSE", err.message);
235
+ }
236
+ return;
237
+ case "DELETE_REMARK":
238
+ try {
239
+ feedback.delete(String(msg.data?.id ?? ""));
240
+ notifyRemarksChanged();
241
+ reply(msg, "DELETE_REMARK_RESPONSE", remarksState());
242
+ }
243
+ catch (err) {
244
+ replyError(msg, "DELETE_REMARK_RESPONSE", err.message);
245
+ }
246
+ return;
247
+ case "SUBMIT_FEEDBACK":
248
+ try {
249
+ const ids = Array.isArray(msg.data?.remarkIds) ? msg.data.remarkIds.map(String) : [];
250
+ const round = feedback.submit(ids, String(msg.data?.title ?? ""));
251
+ notifyRemarksChanged();
252
+ reply(msg, "SUBMIT_FEEDBACK_RESPONSE", { round, ...remarksState() });
253
+ }
254
+ catch (err) {
255
+ replyError(msg, "SUBMIT_FEEDBACK_RESPONSE", err.message);
256
+ }
257
+ return;
258
+ case "RENAME_ROUND":
259
+ try {
260
+ feedback.rename(Number(msg.data?.round), String(msg.data?.title ?? ""));
261
+ notifyRemarksChanged();
262
+ reply(msg, "RENAME_ROUND_RESPONSE", remarksState());
263
+ }
264
+ catch (err) {
265
+ replyError(msg, "RENAME_ROUND_RESPONSE", err.message);
266
+ }
267
+ return;
268
+ case "SET_REMARK_PROCESSED":
269
+ try {
270
+ feedback.setProcessed({
271
+ round: Number(msg.data?.round),
272
+ remarkIds: [String(msg.data?.remarkId ?? "")],
273
+ processed: msg.data?.processed === true,
274
+ by: "user",
275
+ });
276
+ notifyRemarksChanged();
277
+ reply(msg, "SET_REMARK_PROCESSED_RESPONSE", remarksState());
278
+ }
279
+ catch (err) {
280
+ replyError(msg, "SET_REMARK_PROCESSED_RESPONSE", err.message);
281
+ }
282
+ return;
283
+ case "SET_ROUNDS_PROCESSED":
284
+ try {
285
+ const numbers = msg.data?.round === undefined || msg.data?.round === null
286
+ ? feedback.rounds().map((round) => round.number)
287
+ : [Number(msg.data.round)];
288
+ for (const round of numbers) {
289
+ feedback.setProcessed({ round, processed: msg.data?.processed === true, by: "user" });
290
+ }
291
+ notifyRemarksChanged();
292
+ reply(msg, "SET_ROUNDS_PROCESSED_RESPONSE", remarksState());
293
+ }
294
+ catch (err) {
295
+ replyError(msg, "SET_ROUNDS_PROCESSED_RESPONSE", err.message);
296
+ }
297
+ return;
298
+ case "GET_CAPTURE":
299
+ try {
300
+ reply(msg, "GET_CAPTURE_RESPONSE", { png: feedback.readCapture(String(msg.data?.name ?? "")).toString("base64") });
301
+ }
302
+ catch (err) {
303
+ replyError(msg, "GET_CAPTURE_RESPONSE", err.message);
304
+ }
305
+ return;
306
+ case "GET_IMAGE":
307
+ try {
308
+ const image = store.images.read(String(msg.data?.name ?? ""));
309
+ reply(msg, "GET_IMAGE_RESPONSE", { data: image.data.toString("base64"), mimeType: image.mimeType });
310
+ }
311
+ catch (err) {
312
+ replyError(msg, "GET_IMAGE_RESPONSE", err.message);
313
+ }
314
+ return;
184
315
  case "GET_CONFIG":
185
316
  reply(msg, "GET_CONFIG_RESPONSE", { dataDir: store.getDataDir(), version: APP_VERSION });
186
317
  return;
@@ -340,6 +471,7 @@ const NODE_FIELDS_DOC = "Every length is in PIXELS (x, y, width, height, strokeW
340
471
  "a text node given no height takes the exact height of its text, so its first line starts at y — " +
341
472
  "pass a height only to centre a label inside a container (same y and height as the container); " +
342
473
  "icon: icon (required — the name of the drawing), fill, stroke, strokeWidth; " +
474
+ "image: image (required — the stored name returned by add_image), fit; " +
343
475
  "instance: componentId (required — the id of the group it mirrors); nothing else, its look comes from that group. " +
344
476
  "Any field left out falls back to a sensible default, so a node can be created from its type alone.";
345
477
  /**
@@ -374,6 +506,8 @@ const NODE_PROPERTIES = {
374
506
  strokeWidth: { type: "number", minimum: 0, description: NODE_FIELD_DOCS.strokeWidth },
375
507
  radius: { type: "number", minimum: 0, description: NODE_FIELD_DOCS.radius },
376
508
  icon: { type: "string", description: NODE_FIELD_DOCS.icon },
509
+ image: { type: "string", description: NODE_FIELD_DOCS.image },
510
+ fit: { type: "string", enum: [...IMAGE_FITS], description: NODE_FIELD_DOCS.fit },
377
511
  componentId: { type: "string", description: NODE_FIELD_DOCS.componentId },
378
512
  text: { type: "string", description: NODE_FIELD_DOCS.text },
379
513
  color: { type: "string", description: NODE_FIELD_DOCS.color },
@@ -508,7 +642,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
508
642
  properties: {
509
643
  type: {
510
644
  type: "string",
511
- enum: ["rect", "ellipse", "line", "text", "icon", "group", "instance"],
645
+ enum: [...NODE_TYPES],
512
646
  description: NODE_FIELD_DOCS.type,
513
647
  },
514
648
  ...NODE_PROPERTIES,
@@ -521,6 +655,41 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
521
655
  required: ["design_id", "nodes"],
522
656
  },
523
657
  },
658
+ {
659
+ name: "add_image",
660
+ description: "Place a picture (photo, logo, illustration, screenshot) on a design, on top of the other nodes. " +
661
+ `Give the ABSOLUTE path of the image file on the agent machine; accepted formats: ${Object.keys(IMAGE_FORMATS).join(", ")}. ` +
662
+ "The file is copied into the media library of the designs, so the design keeps working if the original moves. " +
663
+ "Without width and height the node takes the picture's natural size; with only one of them, the other follows the picture's proportions. " +
664
+ "Use it for real pictures only — icons come from the \"icon\" node, shapes from add_nodes. " +
665
+ "The answer gives the stored name of the picture: an image node in add_nodes with that name places it again without a second import.",
666
+ inputSchema: {
667
+ type: "object",
668
+ properties: {
669
+ design_id: { type: "string", description: DESIGN_ID_DOC },
670
+ file_path: { type: "string", description: "Absolute path to the image file on the agent machine." },
671
+ file_content: {
672
+ type: "string",
673
+ description: "Filled in by the mcp-http-gateway relay, never by the agent: the base64 content of the file read on the agent machine.",
674
+ },
675
+ file_name: {
676
+ type: "string",
677
+ description: "Filled in by the mcp-http-gateway relay, never by the agent: the original name of the file read on the agent machine.",
678
+ },
679
+ id: NODE_PROPERTIES.id,
680
+ name: { type: "string", description: `${NODE_FIELD_DOCS.name} Defaults to the file name without its extension.` },
681
+ parentId: NODE_PROPERTIES.parentId,
682
+ x: NODE_PROPERTIES.x,
683
+ y: NODE_PROPERTIES.y,
684
+ width: { type: "number", description: "Width of the box, in pixels. Defaults to the picture's natural width, or follows its proportions when only height is given." },
685
+ height: { type: "number", description: "Height of the box, in pixels. Defaults to the picture's natural height, or follows its proportions when only width is given." },
686
+ rotation: NODE_PROPERTIES.rotation,
687
+ opacity: NODE_PROPERTIES.opacity,
688
+ fit: NODE_PROPERTIES.fit,
689
+ },
690
+ required: ["design_id", "file_path"],
691
+ },
692
+ },
524
693
  {
525
694
  name: "update_nodes",
526
695
  description: "Patch existing nodes. Each entry needs an id plus only the fields to change; everything else is preserved. " +
@@ -694,6 +863,51 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
694
863
  required: ["design_ids", "group"],
695
864
  },
696
865
  },
866
+ {
867
+ name: "get_feedback",
868
+ description: "Read the feedback the user wrote on the designs in the GUI. In \"Remarques\" mode the user pins remarks on a whole page, " +
869
+ "on an element or on a rectangular area, then clicks \"Terminer les retours\": the selected remarks are frozen into a numbered, " +
870
+ "named feedback round. Call this tool when the user says their feedback is done. By default it returns the oldest round that still " +
871
+ "has unprocessed remarks (the latest one once everything is processed): every remark with its id, its page, the element it targets " +
872
+ "(id, name, path, box) or the area and the elements inside it, its processing state, and the captures of elements and areas as " +
873
+ "images. It also lists every round with its progress. Reading never changes anything: once you have applied remarks, report it " +
874
+ "with mark_feedback_processed.",
875
+ inputSchema: {
876
+ type: "object",
877
+ properties: {
878
+ round: {
879
+ type: "number",
880
+ description: "Number of the feedback round to read (default: the oldest round still to process, else the latest one).",
881
+ },
882
+ captures: {
883
+ type: "boolean",
884
+ description: "Include the captures of elements and areas as images (default true). Set false to save context.",
885
+ },
886
+ },
887
+ },
888
+ },
889
+ {
890
+ name: "mark_feedback_processed",
891
+ description: "Report that you have handled remarks of a feedback round read with get_feedback. The user follows this progress in the GUI's " +
892
+ "feedback page, round by round: call it once the changes are actually made, with the ids of the remarks you handled and a short " +
893
+ "note of what you changed. Leave remark_ids out to mark every remark of the round.",
894
+ inputSchema: {
895
+ type: "object",
896
+ properties: {
897
+ round: { type: "number", description: "Number of the feedback round, as shown by get_feedback." },
898
+ remark_ids: {
899
+ type: "array",
900
+ items: { type: "string" },
901
+ description: "Ids of the handled remarks (rm_...), as listed by get_feedback. Omit to mark the whole round.",
902
+ },
903
+ note: {
904
+ type: "string",
905
+ description: "What you changed, in one or two sentences. Shown to the user next to each marked remark.",
906
+ },
907
+ },
908
+ required: ["round"],
909
+ },
910
+ },
697
911
  {
698
912
  name: "get_changes",
699
913
  description: "Show every node added, modified, removed or reordered since the last mark_synced — this is how you pick up what the user changed by hand in the GUI. Reading changes never acknowledges them; call mark_synced once the changes are integrated into the code.",
@@ -820,6 +1034,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
820
1034
  }
821
1035
  case "delete_design": {
822
1036
  const result = deleteDesign(store, params);
1037
+ feedback.deleteDesign(result.designId);
1038
+ notifyRemarksChanged();
823
1039
  broadcastToGUI({ type: "DESIGNS_UPDATED", data: { designs: store.list() }, timestamp: new Date().toISOString() });
824
1040
  return result;
825
1041
  }
@@ -828,6 +1044,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
828
1044
  notifyDesignChanged(result.design.id);
829
1045
  return { ...result, content: [{ type: "text", text: result.content[0].text + guiHint(result.design.id) }] };
830
1046
  }
1047
+ case "add_image": {
1048
+ const result = addImage(store, params);
1049
+ notifyDesignChanged(result.design.id);
1050
+ return { ...result, content: [{ type: "text", text: result.content[0].text + guiHint(result.design.id) }] };
1051
+ }
831
1052
  case "update_nodes": {
832
1053
  const result = updateNodes(store, params);
833
1054
  notifyDesignChanged(result.design.id);
@@ -870,6 +1091,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
870
1091
  }
871
1092
  case "get_changes":
872
1093
  return getChanges(store, params);
1094
+ case "get_feedback":
1095
+ return getFeedback(feedback, params);
1096
+ case "mark_feedback_processed": {
1097
+ const result = markFeedbackProcessed(feedback, params);
1098
+ notifyRemarksChanged();
1099
+ return result;
1100
+ }
873
1101
  case "mark_synced": {
874
1102
  const result = markSynced(store, params);
875
1103
  notifyDesignChanged(result.design.id);
@@ -0,0 +1,3 @@
1
+ import type { DesignStore } from "../core/design-store.js";
2
+ import type { MutationResult } from "./add-nodes.js";
3
+ export declare function addImage(store: DesignStore, args: Record<string, unknown> | undefined): MutationResult;
@@ -0,0 +1,65 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { buildNode, DesignError } from "../core/node-factory.js";
4
+ import { requireString, textResult } from "./types.js";
5
+ /** Champs du noeud que l'appel peut fixer, en plus de l'image elle-même. */
6
+ const NODE_FIELDS = ["id", "name", "parentId", "x", "y", "width", "height", "rotation", "opacity", "fit"];
7
+ /**
8
+ * Octets de l'image : le contenu transmis par le relais du gateway quand il est
9
+ * présent, sinon le fichier désigné par `file_path`, lu sur cette machine.
10
+ */
11
+ function readImageBytes(args) {
12
+ if (typeof args.file_content === "string" && args.file_content !== "") {
13
+ return Buffer.from(args.file_content, "base64");
14
+ }
15
+ const filePath = requireString(args, "file_path");
16
+ if (!path.isAbsolute(filePath))
17
+ throw new DesignError(`file_path must be an absolute path: ${filePath}`);
18
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile())
19
+ throw new DesignError(`File not found: ${filePath}`);
20
+ return fs.readFileSync(filePath);
21
+ }
22
+ /**
23
+ * Taille de la boîte : celle demandée, sinon les dimensions naturelles de
24
+ * l'image. Une seule dimension donnée, l'autre suit les proportions de l'image.
25
+ */
26
+ function boxSize(image, width, height) {
27
+ const w = width === undefined ? undefined : Number(width);
28
+ const h = height === undefined ? undefined : Number(height);
29
+ if (w !== undefined && h !== undefined)
30
+ return { width: w, height: h };
31
+ if (w !== undefined)
32
+ return { width: w, height: Math.round(((w * image.height) / image.width) * 100) / 100 };
33
+ if (h !== undefined)
34
+ return { width: Math.round(((h * image.width) / image.height) * 100) / 100, height: h };
35
+ return { width: image.width, height: image.height };
36
+ }
37
+ export function addImage(store, args) {
38
+ const params = args ?? {};
39
+ const id = requireString(params, "design_id");
40
+ const current = store.get(id);
41
+ const image = store.images.save(readImageBytes(params));
42
+ const input = { type: "image", image: image.name };
43
+ for (const field of NODE_FIELDS) {
44
+ if (params[field] !== undefined)
45
+ input[field] = params[field];
46
+ }
47
+ if (input.name === undefined) {
48
+ const original = typeof params.file_name === "string" ? params.file_name : String(params.file_path ?? "");
49
+ const base = path.basename(original.replace(/\\/g, "/")).replace(/\.[^.]+$/, "");
50
+ if (base !== "")
51
+ input.name = base;
52
+ }
53
+ Object.assign(input, boxSize(image, params.width, params.height));
54
+ const node = buildNode(input, new Set(current.nodes.map((n) => n.id)), current.nodes.length + 1);
55
+ const design = store.mutate(id, "agent", (draft) => {
56
+ draft.nodes.push(node);
57
+ });
58
+ return {
59
+ ...textResult(`Added image ${node.id} — "${node.name}" to "${design.name}" at ${Math.round(node.x)},${Math.round(node.y)} ` +
60
+ `size ${Math.round(node.width)}x${Math.round(node.height)}, fit ${node.fit}.\n` +
61
+ `Stored as "${image.name}" (${image.format}, ${image.width}x${image.height} px, ${image.bytes} bytes). ` +
62
+ `Use that name as the "image" field of an image node in add_nodes to place the same picture again.`),
63
+ design,
64
+ };
65
+ }
@@ -12,6 +12,8 @@ export function addNodes(store, args) {
12
12
  // Tout est construit avant d'écrire : une entrée invalide n'ajoute rien du tout.
13
13
  input.forEach((raw, index) => {
14
14
  const node = buildNode(raw, existingIds, current.nodes.length + index + 1);
15
+ if (node.type === "image")
16
+ store.images.requireStored(node.image ?? "");
15
17
  existingIds.add(node.id);
16
18
  created.push(node);
17
19
  });
@@ -1,3 +1,5 @@
1
1
  import type { DesignStore } from "../core/design-store.js";
2
2
  import { type ToolResult } from "./types.js";
3
- export declare function deleteDesign(store: DesignStore, args: Record<string, unknown> | undefined): ToolResult;
3
+ export declare function deleteDesign(store: DesignStore, args: Record<string, unknown> | undefined): ToolResult & {
4
+ designId: string;
5
+ };
@@ -3,5 +3,8 @@ export function deleteDesign(store, args) {
3
3
  const id = requireString(args, "design_id");
4
4
  const design = store.get(id);
5
5
  store.delete(id);
6
- return textResult(`Design "${design.name}" (${id}) deleted, along with its ${design.nodes.length} node(s).`);
6
+ return {
7
+ ...textResult(`Design "${design.name}" (${id}) deleted, along with its ${design.nodes.length} node(s) and its remarks.`),
8
+ designId: id,
9
+ };
7
10
  }
@@ -15,7 +15,7 @@ export function exportDesignTool(store, args) {
15
15
  throw new Error(`Unknown format "${String(format)}". Expected one of: ${EXPORT_FORMATS.join(", ")}.`);
16
16
  }
17
17
  const design = store.get(id);
18
- const content = render(design, format);
18
+ const content = render(design, format, store.images);
19
19
  const destination = typeof args?.destination_path === "string" ? args.destination_path.trim() : "";
20
20
  if (!destination) {
21
21
  return textResult(content);
@@ -0,0 +1,3 @@
1
+ import type { FeedbackStore } from "../core/feedback-store.js";
2
+ import type { ImageToolResult } from "./types.js";
3
+ export declare function getFeedback(feedback: FeedbackStore, args: Record<string, unknown> | undefined): ImageToolResult;
@@ -0,0 +1,62 @@
1
+ import { formatFeedbackRound, roundProgress } from "../../shared/feedback.js";
2
+ /**
3
+ * Retour de l'utilisateur : les remarques qu'il a transmises depuis la GUI avec
4
+ * « Terminer les retours », figées dans un retour numéroté et nommé.
5
+ *
6
+ * Sans numéro demandé, c'est le plus ancien retour encore à traiter, ou le
7
+ * dernier quand tout est traité. Lire un retour ne change rien : seul
8
+ * `mark_feedback_processed` déclare des remarques traitées.
9
+ */
10
+ const STATUS_LABELS = { pending: "to process", in_progress: "in progress", done: "processed" };
11
+ function roundLine(round) {
12
+ const progress = roundProgress(round);
13
+ return ` - round ${round.number} "${round.title}" (${round.submittedAt}): ${progress.done}/${progress.total} processed — ${STATUS_LABELS[progress.status]}`;
14
+ }
15
+ export function getFeedback(feedback, args) {
16
+ const rounds = feedback.rounds();
17
+ const drafts = feedback.remarks().filter((remark) => remark.status === "draft").length;
18
+ const draftNote = drafts > 0
19
+ ? `${drafts} remark(s) are still drafts in the GUI and are not part of any round yet.`
20
+ : "No draft remark is waiting in the GUI.";
21
+ if (rounds.length === 0) {
22
+ return {
23
+ content: [
24
+ {
25
+ type: "text",
26
+ text: `No feedback round has been submitted yet. ${draftNote} ` +
27
+ `Ask the user to switch the GUI to "Remarques" mode and click "Terminer les retours" once their remarks are written.`,
28
+ },
29
+ ],
30
+ };
31
+ }
32
+ const firstOpen = rounds.find((candidate) => roundProgress(candidate).status !== "done");
33
+ const requested = args?.round === undefined ? (firstOpen ?? rounds[rounds.length - 1]).number : Number(args.round);
34
+ const round = rounds.find((candidate) => candidate.number === requested);
35
+ if (!round) {
36
+ throw new Error(`Feedback round ${args?.round} not found. Rounds 1 to ${rounds.length} exist.`);
37
+ }
38
+ const withCaptures = args?.captures !== false;
39
+ const footer = [
40
+ `Feedback rounds (${rounds.length}):`,
41
+ ...rounds.map(roundLine),
42
+ draftNote,
43
+ withCaptures ? "" : "Captures were left out (captures=false).",
44
+ "Apply the remarks, then call mark_feedback_processed with the round and the ids of the remarks you handled, " +
45
+ "with a short note of what you changed: the user follows that progress in the GUI.",
46
+ ]
47
+ .filter((line) => line !== "")
48
+ .join("\n");
49
+ const content = [{ type: "text", text: `${formatFeedbackRound(round)}\n\n${footer}` }];
50
+ if (withCaptures) {
51
+ let image = 0;
52
+ for (const remark of round.remarks) {
53
+ if (!remark.capture)
54
+ continue;
55
+ image += 1;
56
+ const label = remark.number === null ? "whole page" : `#${remark.number}`;
57
+ content.push({ type: "text", text: `Image ${image} — remark ${label} on "${remark.designName}" (${remark.designId}).` });
58
+ content.push({ type: "image", data: feedback.readCapture(remark.capture).toString("base64"), mimeType: "image/png" });
59
+ }
60
+ }
61
+ return { content };
62
+ }
@@ -0,0 +1,8 @@
1
+ import type { FeedbackStore } from "../core/feedback-store.js";
2
+ import { type ToolResult } from "./types.js";
3
+ /**
4
+ * L'agent déclare traitées des remarques d'un retour. C'est ce que l'utilisateur
5
+ * suit dans la page des retours : lire un retour avec `get_feedback` ne marque
6
+ * rien, seul cet outil le fait.
7
+ */
8
+ export declare function markFeedbackProcessed(feedback: FeedbackStore, args: Record<string, unknown> | undefined): ToolResult;
@@ -0,0 +1,29 @@
1
+ import { roundProgress } from "../../shared/feedback.js";
2
+ import { textResult } from "./types.js";
3
+ /**
4
+ * L'agent déclare traitées des remarques d'un retour. C'est ce que l'utilisateur
5
+ * suit dans la page des retours : lire un retour avec `get_feedback` ne marque
6
+ * rien, seul cet outil le fait.
7
+ */
8
+ export function markFeedbackProcessed(feedback, args) {
9
+ const number = Number(args?.round);
10
+ if (!Number.isInteger(number) || number < 1)
11
+ throw new Error(`"round" is required and must be a feedback round number.`);
12
+ const rawIds = args?.remark_ids;
13
+ if (rawIds !== undefined && (!Array.isArray(rawIds) || rawIds.some((id) => typeof id !== "string"))) {
14
+ throw new Error(`"remark_ids" must be an array of remark ids, as listed by get_feedback.`);
15
+ }
16
+ const note = typeof args?.note === "string" ? args.note : null;
17
+ const round = feedback.setProcessed({
18
+ round: number,
19
+ remarkIds: rawIds,
20
+ processed: true,
21
+ by: "agent",
22
+ note,
23
+ });
24
+ const progress = roundProgress(round);
25
+ const marked = rawIds === undefined ? "Every remark" : `${rawIds.length} remark(s)`;
26
+ return textResult(`${marked} of feedback round ${round.number} "${round.title}" marked as processed. ` +
27
+ `${progress.done} of ${progress.total} remark(s) of this round are now processed` +
28
+ (progress.status === "done" ? " — the round is complete." : "."));
29
+ }
@@ -13,6 +13,7 @@ export function screenshotDesignTool(store, args) {
13
13
  }
14
14
  const shot = screenshotDesign(design, {
15
15
  nodeId,
16
+ images: store.images,
16
17
  scale: args?.scale === undefined ? undefined : Number(args.scale),
17
18
  padding: args?.padding === undefined ? (nodeId ? NODE_PADDING : 0) : Number(args.padding),
18
19
  });
@@ -21,7 +21,10 @@ export function updateNodes(store, args) {
21
21
  if (Object.keys(fields).length === 0) {
22
22
  throw new DesignError(`Update for node "${nodeId}" contains no field to change.`);
23
23
  }
24
- return applyPatch(node, fields);
24
+ const next = applyPatch(node, fields);
25
+ if (next.type === "image" && fields.image !== undefined)
26
+ store.images.requireStored(next.image ?? "");
27
+ return next;
25
28
  });
26
29
  const patchedById = new Map(patched.map((n) => [n.id, n]));
27
30
  const design = store.mutate(id, "agent", (draft) => {
@@ -1 +1 @@
1
- export declare const APP_VERSION = "1.0.10";
1
+ export declare const APP_VERSION: string;
@@ -1,2 +1,16 @@
1
- // Auto-generated version file - do not edit manually
2
- export const APP_VERSION = '1.0.10';
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ // Lit la version dans le premier package.json trouvé en remontant depuis ce module.
5
+ function readPackageVersion() {
6
+ let dir = path.dirname(fileURLToPath(import.meta.url));
7
+ while (!existsSync(path.join(dir, "package.json"))) {
8
+ const parent = path.dirname(dir);
9
+ if (parent === dir)
10
+ throw new Error("package.json introuvable");
11
+ dir = parent;
12
+ }
13
+ const pkg = JSON.parse(readFileSync(path.join(dir, "package.json"), "utf8"));
14
+ return pkg.version;
15
+ }
16
+ export const APP_VERSION = readPackageVersion();