@mindstudio-ai/remy 0.1.275 → 0.1.276

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/dist/headless.js CHANGED
@@ -289,7 +289,7 @@ function isRetryableError(error) {
289
289
  /Unable to download/i.test(error);
290
290
  }
291
291
  function sleep(ms) {
292
- return new Promise((resolve3) => setTimeout(resolve3, ms));
292
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
293
293
  }
294
294
  async function* streamChatWithRetry(params, options) {
295
295
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
@@ -433,7 +433,7 @@ var MODEL_SURFACES = {
433
433
  userPickable: true
434
434
  },
435
435
  imageGeneration: {
436
- default: "seedream-4.5",
436
+ default: "gpt-image-2",
437
437
  label: "Image Generation",
438
438
  description: "Creates images for your product \u2014 icons, illustrations, photos, and any other visual assets.",
439
439
  modelType: "image_generation",
@@ -692,9 +692,9 @@ function parseFrontmatter(content) {
692
692
  }
693
693
  const fields = {};
694
694
  for (const line of match[1].split("\n")) {
695
- const sep = line.indexOf(":");
696
- if (sep > 0) {
697
- fields[line.slice(0, sep).trim()] = line.slice(sep + 1).trim();
695
+ const sep2 = line.indexOf(":");
696
+ if (sep2 > 0) {
697
+ fields[line.slice(0, sep2).trim()] = line.slice(sep2 + 1).trim();
698
698
  }
699
699
  }
700
700
  return fields;
@@ -1663,7 +1663,7 @@ function formatCliResult(r) {
1663
1663
  return logBlock + body + truncNote;
1664
1664
  }
1665
1665
  function runCli(command, args, options) {
1666
- return new Promise((resolve3) => {
1666
+ return new Promise((resolve4) => {
1667
1667
  const timeout = options?.timeout ?? 6e4;
1668
1668
  const maxBuffer = options?.maxBuffer ?? 1024 * 1024;
1669
1669
  let finalArgs = args;
@@ -1692,7 +1692,7 @@ function runCli(command, args, options) {
1692
1692
  if (killTimer) {
1693
1693
  clearTimeout(killTimer);
1694
1694
  }
1695
- resolve3(result);
1695
+ resolve4(result);
1696
1696
  };
1697
1697
  const child = spawn(command, finalArgs, {
1698
1698
  stdio: [options?.stdin ? "pipe" : "ignore", "pipe", "pipe"]
@@ -2389,7 +2389,7 @@ var bashTool = {
2389
2389
  async execute(input, context) {
2390
2390
  const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES2;
2391
2391
  const timeoutMs = input.timeout ? input.timeout * 1e3 : DEFAULT_TIMEOUT_MS;
2392
- return new Promise((resolve3) => {
2392
+ return new Promise((resolve4) => {
2393
2393
  const child = spawn2("sh", ["-c", input.command], {
2394
2394
  // Pinned rather than inherited. `undefined` here means "wherever the
2395
2395
  // process happens to be", which is the project root only by luck.
@@ -2414,9 +2414,9 @@ var bashTool = {
2414
2414
  clearTimeout(timer);
2415
2415
  if (!output) {
2416
2416
  if (code && code !== 0) {
2417
- resolve3(`Error: process exited with code ${code}`);
2417
+ resolve4(`Error: process exited with code ${code}`);
2418
2418
  } else {
2419
- resolve3("(no output)");
2419
+ resolve4("(no output)");
2420
2420
  }
2421
2421
  return;
2422
2422
  }
@@ -2442,18 +2442,18 @@ var bashTool = {
2442
2442
  `${(MAX_OUTPUT_BYTES / 1024).toFixed(0)}KB of ${(totalBytes / 1024).toFixed(0)}KB`
2443
2443
  );
2444
2444
  }
2445
- resolve3(
2445
+ resolve4(
2446
2446
  truncated + `
2447
2447
 
2448
2448
  (truncated at ${reasons.join(" / ")} \u2014 narrow the command (grep, head/tail, smaller paths) instead of increasing limits)`
2449
2449
  );
2450
2450
  } else {
2451
- resolve3(output);
2451
+ resolve4(output);
2452
2452
  }
2453
2453
  });
2454
2454
  child.on("error", (err) => {
2455
2455
  clearTimeout(timer);
2456
- resolve3(`Error: ${err.message}`);
2456
+ resolve4(`Error: ${err.message}`);
2457
2457
  });
2458
2458
  });
2459
2459
  }
@@ -2570,17 +2570,17 @@ var grepTool = {
2570
2570
  }
2571
2571
  const rgCmd = `rg ${rgFlags}${globFlag} '${escaped}' ${searchPath}`;
2572
2572
  const grepCmd = `grep ${grepFlags} '${escaped}' ${searchPath} --include='*.ts' --include='*.tsx' --include='*.js' --include='*.json' --include='*.md'`;
2573
- return new Promise((resolve3) => {
2573
+ return new Promise((resolve4) => {
2574
2574
  exec(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
2575
2575
  if (stdout?.trim()) {
2576
- resolve3(formatResults(stdout, max, mode));
2576
+ resolve4(formatResults(stdout, max, mode));
2577
2577
  return;
2578
2578
  }
2579
2579
  exec(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
2580
2580
  if (grepStdout?.trim()) {
2581
- resolve3(formatResults(grepStdout, max, mode));
2581
+ resolve4(formatResults(grepStdout, max, mode));
2582
2582
  } else {
2583
- resolve3("No matches found.");
2583
+ resolve4("No matches found.");
2584
2584
  }
2585
2585
  });
2586
2586
  });
@@ -2896,7 +2896,7 @@ var restartProcessTool = {
2896
2896
  async execute(input) {
2897
2897
  const data = await lspRequest("/restart-process", { name: input.name });
2898
2898
  if (data.ok) {
2899
- await new Promise((resolve3) => setTimeout(resolve3, 5e3));
2899
+ await new Promise((resolve4) => setTimeout(resolve4, 5e3));
2900
2900
  return `Restarted ${input.name}.`;
2901
2901
  }
2902
2902
  return `Error: unexpected response: ${JSON.stringify(data)}`;
@@ -3123,6 +3123,26 @@ ${opts.styleMap}
3123
3123
  ${ANALYSIS_RESPONSE_FORMAT}`;
3124
3124
  return p;
3125
3125
  }
3126
+ async function renderHtmlViaSidecar(opts) {
3127
+ const result = await sidecarRequest(
3128
+ "/render-html",
3129
+ {
3130
+ html: opts.html,
3131
+ width: opts.width,
3132
+ height: opts.height,
3133
+ ...opts.transparent ? { transparent: true } : {},
3134
+ ...opts.scale != null ? { scale: opts.scale } : {}
3135
+ },
3136
+ { timeout: VIEWPORT_CAPTURE_TIMEOUT_MS }
3137
+ );
3138
+ const url = result?.url;
3139
+ if (!url) {
3140
+ throw new Error(
3141
+ `No URL in sidecar render response. The browser may not be ready yet. Response: ${JSON.stringify(result)}`
3142
+ );
3143
+ }
3144
+ return { url, width: result.width, height: result.height };
3145
+ }
3126
3146
  async function streamScreenshotAnalysis(opts) {
3127
3147
  const { image, prompt, styleMap, onLog, model, apiConfig } = opts;
3128
3148
  const url = await resolveImageRef(image, apiConfig);
@@ -4262,10 +4282,10 @@ function parseFrontmatter2(filePath) {
4262
4282
  }
4263
4283
  const fm = {};
4264
4284
  for (const line of match[1].split("\n")) {
4265
- const sep = line.indexOf(":");
4266
- if (sep > 0) {
4267
- const key = line.slice(0, sep).trim();
4268
- const val = line.slice(sep + 1).trim();
4285
+ const sep2 = line.indexOf(":");
4286
+ if (sep2 > 0) {
4287
+ const key = line.slice(0, sep2).trim();
4288
+ const val = line.slice(sep2 + 1).trim();
4269
4289
  fm[key] = val;
4270
4290
  }
4271
4291
  }
@@ -4569,7 +4589,7 @@ var browserAutomationTool = {
4569
4589
  // src/tools/code/screenshot.ts
4570
4590
  var screenshotDefinition = {
4571
4591
  name: "screenshot",
4572
- description: "Capture a screenshot of the app preview and get a description of what's on screen. Choose `fullPage`: `false` captures just the visible viewport (fast \u2014 for a specific section the page is scrolled to), `true` captures the entire page top-to-bottom (slower \u2014 for overall composition or content past the fold). Captures the settled page state \u2014 it cannot catch animations, transitions, or transient state. The analysis is not precise about every detail \u2014 for example it cannot reliably identify specific fonts by name, only describe what the letterforms look like. Optionally provide specific questions about what you're looking for. Use a bulleted list to ask many questions at once. To ask additional questions about a screenshot you have already captured, pass its URL as `imageUrl` to skip recapture; `imageUrl` also accepts the disk path of an image file (a user upload, a saved asset) to analyze that instead of the preview. If the screenshot requires interaction first (logging in, clicking a tab, dismissing a modal, scrolling to a section), use the instructions param to describe the steps. To render a fixed-size image such as an Open Graph share card, set `width` and `height` (e.g. 1200 \xD7 630) and `format: 'png'`: the tool navigates to `path`, clips to exactly those pixel dimensions, and returns the image URL.",
4592
+ description: "Capture a screenshot of the app preview and get a description of what's on screen. Choose `fullPage`: `false` captures just the visible viewport (fast \u2014 for a specific section the page is scrolled to), `true` captures the entire page top-to-bottom (slower \u2014 for overall composition or content past the fold). Captures the settled page state \u2014 it cannot catch animations, transitions, or transient state. The analysis is not precise about every detail \u2014 for example it cannot reliably identify specific fonts by name, only describe what the letterforms look like. Optionally provide specific questions about what you're looking for. Use a bulleted list to ask many questions at once. To ask additional questions about a screenshot you have already captured, pass its URL as `imageUrl` to skip recapture; `imageUrl` also accepts the disk path of an image file (a user upload, a saved asset) to analyze that instead of the preview. If the screenshot requires interaction first (logging in, clicking a tab, dismissing a modal, scrolling to a section), use the instructions param to describe the steps. To capture at exact pixel dimensions, set `width` and `height`: the tool clips to exactly that viewport and returns the image URL.",
4573
4593
  inputSchema: {
4574
4594
  type: "object",
4575
4595
  properties: {
@@ -4600,7 +4620,7 @@ var screenshotDefinition = {
4600
4620
  format: {
4601
4621
  type: "string",
4602
4622
  enum: ["png", "jpeg"],
4603
- description: "Output image format. Defaults to 'jpeg'. Use 'png' for crisp flat graphics like share cards, where JPEG artifacts show on sharp type and edges."
4623
+ description: "Output image format. Defaults to 'jpeg'. Use 'png' for crisp flat graphics where JPEG artifacts show on sharp type and edges."
4604
4624
  },
4605
4625
  instructions: {
4606
4626
  type: "string",
@@ -4922,7 +4942,36 @@ ${brief}
4922
4942
  }
4923
4943
 
4924
4944
  // src/subagents/designExpert/tools/images/imageGenerator.ts
4925
- var ANALYZE_PROMPT = 'You are reviewing this image for a visual designer sourcing assets for a project. Describe: what the image depicts, the mood and color palette, how the lighting and composition work, any text present in the image, whether there are any issues (artifacts, distortions), and how it could be used in a layout for an app or website. Be concise and practical. Respond only with your analysis as Markdown (starting with the title "Asset Review") and absolutely no other text. Do not use emojis - use unicode if you need symbols.';
4945
+ var ANALYZE_PROMPT = 'You are reviewing this image for a visual designer sourcing assets for a project. Describe: what the image depicts, the mood and color palette, how the lighting and composition work, any text present in the image, whether there are any issues (artifacts, distortions), whether the artwork extends fully to all four edges of the canvas or sits inset (call out any baked-in border, frame, rounded-corner mask, drop-shadow margin, or mockup presentation such as an icon rendered on a background or device), and how it could be used in a layout for an app or website. Be concise and practical. Respond only with your analysis as Markdown (starting with the title "Asset Review") and absolutely no other text. Do not use emojis - use unicode if you need symbols.';
4946
+ var OPENAI_IMAGE_SIZES = [
4947
+ [768, 1024],
4948
+ [1024, 768],
4949
+ [1024, 1024],
4950
+ [1024, 1536],
4951
+ [1536, 1024],
4952
+ [1280, 2560],
4953
+ [2560, 1280],
4954
+ [1440, 2560],
4955
+ [2560, 1440],
4956
+ [1792, 2400],
4957
+ [2400, 1792],
4958
+ [2400, 2880],
4959
+ [2560, 2560]
4960
+ ];
4961
+ function snapToOpenAiImageSize(width, height) {
4962
+ let best = OPENAI_IMAGE_SIZES[0];
4963
+ let bestScore = Infinity;
4964
+ for (const [w, h] of OPENAI_IMAGE_SIZES) {
4965
+ const aspectDiff = Math.abs(Math.log(w / h / (width / height)));
4966
+ const areaDiff = Math.abs(Math.log(w * h / (width * height)));
4967
+ const score = aspectDiff * 8 + areaDiff;
4968
+ if (score < bestScore) {
4969
+ bestScore = score;
4970
+ best = [w, h];
4971
+ }
4972
+ }
4973
+ return `${best[0]}x${best[1]}`;
4974
+ }
4926
4975
  async function generateImageAssets(opts) {
4927
4976
  const {
4928
4977
  prompts,
@@ -4937,7 +4986,11 @@ async function generateImageAssets(opts) {
4937
4986
  const sourceImages = opts.sourceImages?.length ? await resolveImageRefs(opts.sourceImages, apiConfig) : void 0;
4938
4987
  const width = opts.width || 2048;
4939
4988
  const height = opts.height || 2048;
4940
- const config = { width, height };
4989
+ const config = {
4990
+ width,
4991
+ height,
4992
+ size: snapToOpenAiImageSize(width, height)
4993
+ };
4941
4994
  if (sourceImages?.length) {
4942
4995
  const [firstImage] = sourceImages;
4943
4996
  config.images = sourceImages;
@@ -5189,11 +5242,143 @@ async function execute6(input, onLog, context) {
5189
5242
  });
5190
5243
  }
5191
5244
 
5245
+ // src/subagents/designExpert/tools/images/renderImage.ts
5246
+ var renderImage_exports = {};
5247
+ __export(renderImage_exports, {
5248
+ definition: () => definition7,
5249
+ execute: () => execute7
5250
+ });
5251
+ import { mkdir, unlink, writeFile } from "fs/promises";
5252
+ import { tmpdir } from "os";
5253
+ import { dirname, join, resolve as resolve2, sep } from "path";
5254
+ import { randomUUID } from "crypto";
5255
+ var MIN_DIMENSION = 16;
5256
+ var MAX_DIMENSION = 4096;
5257
+ var RENDER_ANALYZE_PROMPT = 'You are reviewing a browser-rendered graphic (composed from HTML/CSS by a designer) for fidelity. Report: whether the composition fills the full canvas or leaves unintended gaps at any edge, any clipped or overflowing text, whether custom webfonts appear to have loaded (distinctive letterforms vs generic fallback serif/sans), any misalignment or uneven spacing, any unintended scrollbars or default-styling artifacts, and \u2014 if the background is transparent \u2014 any fringing or stray opaque pixels at the edges. Then briefly describe the overall composition and how polished it looks. Be concise and practical. Respond only with your analysis as Markdown (starting with the title "Render Review") and absolutely no other text. Do not use emojis - use unicode if you need symbols.';
5258
+ var definition7 = {
5259
+ name: "renderImage",
5260
+ description: "Render a self-contained HTML document in a real browser and capture it as a hosted PNG at exact pixel dimensions, with a fidelity review included. Deterministic \u2014 exact hex colors, real loaded webfonts, precise geometry \u2014 unlike generateImages, which is an image model. Use for token-exact graphics: Open Graph share cards, wordmarks, flat/geometric icon tiles, badges, any composition where letterforms and spacing carry the design. Compose with HTML/CSS (link webfonts from CDNs \u2014 the renderer waits for them to load); inline existing SVG markup when needed, but never hand-write new SVG path data.",
5261
+ inputSchema: {
5262
+ type: "object",
5263
+ properties: {
5264
+ html: {
5265
+ type: "string",
5266
+ description: "A complete, self-contained HTML document sized to fill the viewport (style html/body to the full dimensions with margin 0). External webfonts and images from CDNs are fine \u2014 loading is awaited before capture."
5267
+ },
5268
+ width: {
5269
+ type: "number",
5270
+ description: "Viewport width in CSS pixels (e.g. 1200 for an OG card, 512 for an icon tile). Range: 16-4096."
5271
+ },
5272
+ height: {
5273
+ type: "number",
5274
+ description: "Viewport height in CSS pixels. Range: 16-4096."
5275
+ },
5276
+ scale: {
5277
+ type: "number",
5278
+ description: "Device scale factor, 1-3. Output pixels = css \xD7 scale. Use 2 for crisp icon masters (e.g. a 512\xD7512 document captured at 1024\xD71024)."
5279
+ },
5280
+ transparentBackground: {
5281
+ type: "boolean",
5282
+ description: "Capture with true alpha: leave the document background transparent (no background on html/body) and the PNG keeps it. No background-removal model involved."
5283
+ },
5284
+ savePath: {
5285
+ type: "string",
5286
+ description: "Optional project-relative path to also save the PNG into the app (e.g. 'dist/interfaces/web/public/og-image.png' so the deployed site self-hosts it)."
5287
+ }
5288
+ },
5289
+ required: ["html", "width", "height"]
5290
+ }
5291
+ };
5292
+ async function execute7(input, onLog, context) {
5293
+ const html = typeof input.html === "string" ? input.html : "";
5294
+ const width = Math.round(Number(input.width));
5295
+ const height = Math.round(Number(input.height));
5296
+ if (!html || !Number.isFinite(width) || !Number.isFinite(height) || width < MIN_DIMENSION || width > MAX_DIMENSION || height < MIN_DIMENSION || height > MAX_DIMENSION) {
5297
+ return `Error: renderImage requires an html string plus width/height between ${MIN_DIMENSION} and ${MAX_DIMENSION}.`;
5298
+ }
5299
+ const release = await acquireBrowserLock();
5300
+ let rendered;
5301
+ try {
5302
+ onLog?.("Rendering document in the sandbox browser...");
5303
+ rendered = await renderHtmlViaSidecar({
5304
+ html,
5305
+ width,
5306
+ height,
5307
+ transparent: input.transparentBackground === true,
5308
+ scale: typeof input.scale === "number" ? input.scale : void 0
5309
+ });
5310
+ } catch (err) {
5311
+ return `Error: render failed: ${err?.message ?? err}`;
5312
+ } finally {
5313
+ release();
5314
+ }
5315
+ let bytes;
5316
+ try {
5317
+ const res = await fetch(rendered.url);
5318
+ if (!res.ok) {
5319
+ throw new Error(`fetch returned ${res.status}`);
5320
+ }
5321
+ bytes = Buffer.from(await res.arrayBuffer());
5322
+ } catch (err) {
5323
+ return `Error: rendered but could not download the capture (${err?.message ?? err}). Temporary URL: ${rendered.url}`;
5324
+ }
5325
+ let savedPath;
5326
+ if (typeof input.savePath === "string" && input.savePath) {
5327
+ const absolute = resolve2(PROJECT_ROOT, input.savePath);
5328
+ if (!absolute.startsWith(PROJECT_ROOT + sep)) {
5329
+ return "Error: savePath must resolve inside the project.";
5330
+ }
5331
+ await mkdir(dirname(absolute), { recursive: true });
5332
+ await writeFile(absolute, bytes);
5333
+ savedPath = input.savePath;
5334
+ }
5335
+ onLog?.("Hosting the capture...");
5336
+ const tmpPath = join(tmpdir(), `render-${randomUUID()}.png`);
5337
+ let url = rendered.url;
5338
+ let temporary = true;
5339
+ try {
5340
+ await writeFile(tmpPath, bytes);
5341
+ const upload = await runMindstudioCliResult(["upload", tmpPath], {
5342
+ timeout: 6e4,
5343
+ onLog,
5344
+ caller: "designExpert"
5345
+ });
5346
+ const match = upload.ok ? upload.value.match(/https:\/\/\S+/g) : null;
5347
+ if (match?.length) {
5348
+ url = match[match.length - 1];
5349
+ temporary = false;
5350
+ }
5351
+ } finally {
5352
+ await unlink(tmpPath).catch(() => {
5353
+ });
5354
+ }
5355
+ const analysis = await analyzeImage({
5356
+ prompt: RENDER_ANALYZE_PROMPT,
5357
+ image: url,
5358
+ onLog,
5359
+ model: resolveModel("imageAnalysis", context?.models, context?.model)
5360
+ }).then((r) => r.analysis).catch((err) => `Could not review this image: ${err.message}`);
5361
+ return JSON.stringify({
5362
+ images: [
5363
+ {
5364
+ url,
5365
+ ...temporary ? {
5366
+ note: "Durable hosting failed \u2014 this URL is dev-session scratch storage and may expire. Do not use it for app metadata; retry if a durable URL is needed."
5367
+ } : {},
5368
+ ...savedPath ? { savedPath } : {},
5369
+ analysis,
5370
+ width: rendered.width,
5371
+ height: rendered.height
5372
+ }
5373
+ ]
5374
+ });
5375
+ }
5376
+
5192
5377
  // src/subagents/designExpert/tools/polishCopy.ts
5193
5378
  var polishCopy_exports = {};
5194
5379
  __export(polishCopy_exports, {
5195
- definition: () => definition7,
5196
- execute: () => execute7
5380
+ definition: () => definition8,
5381
+ execute: () => execute8
5197
5382
  });
5198
5383
 
5199
5384
  // src/subagents/copyEditor/tools.ts
@@ -5249,7 +5434,7 @@ var copyEditorTool = {
5249
5434
  };
5250
5435
 
5251
5436
  // src/subagents/designExpert/tools/polishCopy.ts
5252
- var definition7 = {
5437
+ var definition8 = {
5253
5438
  name: "polishCopy",
5254
5439
  description: "Hand off any user-facing copy you've written \u2014 headlines, captions, labels, body text \u2014 and get back a sharper version: better built for its audience and free of the fingerprints that make writing read as AI. It elevates how the copy communicates without inventing facts or claims you didn't give it. Give it the text plus what it's for (where it appears, the audience).",
5255
5440
  inputSchema: {
@@ -5263,7 +5448,7 @@ var definition7 = {
5263
5448
  required: ["task"]
5264
5449
  }
5265
5450
  };
5266
- async function execute7(input, _onLog, context) {
5451
+ async function execute8(input, _onLog, context) {
5267
5452
  return copyEditorTool.execute(input, context);
5268
5453
  }
5269
5454
 
@@ -5279,6 +5464,7 @@ var tools = {
5279
5464
  screenshot: { definition: screenshotDefinition, execute: executeScreenshot },
5280
5465
  generateImages: generateImages_exports,
5281
5466
  editImages: editImages_exports,
5467
+ renderImage: renderImage_exports,
5282
5468
  polishCopy: polishCopy_exports
5283
5469
  };
5284
5470
  var DESIGN_EXPERT_TOOLS = [
@@ -5698,13 +5884,13 @@ var PITCH_DECK_SHELL = readAsset(
5698
5884
  "subagents/productVision",
5699
5885
  "pitch-deck-shell.html"
5700
5886
  );
5701
- function resolve2(filePath) {
5887
+ function resolve3(filePath) {
5702
5888
  return path10.join(ROADMAP_DIR, filePath);
5703
5889
  }
5704
5890
  async function executeVisionTool(name, input, context) {
5705
5891
  switch (name) {
5706
5892
  case "writeFile": {
5707
- const filePath = resolve2(input.path);
5893
+ const filePath = resolve3(input.path);
5708
5894
  try {
5709
5895
  fs19.mkdirSync(ROADMAP_DIR, { recursive: true });
5710
5896
  let oldContent = null;
@@ -5722,7 +5908,7 @@ ${unifiedDiff(filePath, oldContent ?? "", input.content)}`;
5722
5908
  }
5723
5909
  }
5724
5910
  case "deleteFile": {
5725
- const filePath = resolve2(input.path);
5911
+ const filePath = resolve3(input.path);
5726
5912
  try {
5727
5913
  if (!fs19.existsSync(filePath)) {
5728
5914
  return `Error: ${filePath} does not exist`;
@@ -5739,7 +5925,7 @@ ${unifiedDiff(filePath, oldContent, "")}`;
5739
5925
  if (!context) {
5740
5926
  return "Error: writePitchDeck requires execution context for design expert delegation";
5741
5927
  }
5742
- const filePath = resolve2("pitch.html");
5928
+ const filePath = resolve3("pitch.html");
5743
5929
  try {
5744
5930
  fs19.mkdirSync(ROADMAP_DIR, { recursive: true });
5745
5931
  const exists = fs19.existsSync(filePath);
@@ -7360,14 +7546,14 @@ function buildCorpus() {
7360
7546
  });
7361
7547
  }
7362
7548
  }
7363
- const sep = "\n\n---\n\n";
7549
+ const sep2 = "\n\n---\n\n";
7364
7550
  const sections = [];
7365
7551
  let usedChars = 0;
7366
7552
  for (const { path: p, content } of files) {
7367
7553
  const section = `## File: ${p}
7368
7554
 
7369
7555
  ${content}`;
7370
- const added = section.length + (sections.length > 0 ? sep.length : 0);
7556
+ const added = section.length + (sections.length > 0 ? sep2.length : 0);
7371
7557
  if (sections.length > 0 && usedChars + added > BRAND_CORPUS_CHAR_LIMIT) {
7372
7558
  sections.push(
7373
7559
  `(brand corpus truncated: included ${sections.length} of ${files.length} files, ~${(usedChars / 1024).toFixed(0)}KB; brand-relevant files were prioritized.)`
@@ -7377,7 +7563,7 @@ ${content}`;
7377
7563
  sections.push(section);
7378
7564
  usedChars += added;
7379
7565
  }
7380
- return sections.join(sep);
7566
+ return sections.join(sep2);
7381
7567
  }
7382
7568
  function headSlice(content) {
7383
7569
  if (content.length <= HEAD_SLICE_CHARS) {
@@ -8474,8 +8660,8 @@ async function runTurn(params) {
8474
8660
 
8475
8661
  // src/headless/attachments.ts
8476
8662
  import { mkdirSync, existsSync } from "fs";
8477
- import { writeFile } from "fs/promises";
8478
- import { basename as basename2, join, extname as extname2 } from "path";
8663
+ import { writeFile as writeFile2 } from "fs/promises";
8664
+ import { basename as basename2, join as join2, extname as extname2 } from "path";
8479
8665
  var log15 = createLogger("headless:attachments");
8480
8666
  var UPLOADS_DIR = "src/.user-uploads";
8481
8667
  function filenameFromUrl(url) {
@@ -8488,7 +8674,7 @@ function filenameFromUrl(url) {
8488
8674
  }
8489
8675
  }
8490
8676
  function resolveUniqueFilename(name, claimed) {
8491
- const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join(UPLOADS_DIR, candidate));
8677
+ const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join2(UPLOADS_DIR, candidate));
8492
8678
  if (isFree(name)) {
8493
8679
  return name;
8494
8680
  }
@@ -8523,7 +8709,7 @@ async function persistAttachments(attachments) {
8523
8709
  const results = await Promise.allSettled(
8524
8710
  nonVoice.map(async (att, i) => {
8525
8711
  const name = names[i];
8526
- const localPath = join(UPLOADS_DIR, name);
8712
+ const localPath = join2(UPLOADS_DIR, name);
8527
8713
  const res = await fetch(att.url, {
8528
8714
  signal: AbortSignal.timeout(3e4)
8529
8715
  });
@@ -8531,7 +8717,7 @@ async function persistAttachments(attachments) {
8531
8717
  throw new Error(`HTTP ${res.status} downloading ${att.url}`);
8532
8718
  }
8533
8719
  const buffer = Buffer.from(await res.arrayBuffer());
8534
- await writeFile(localPath, buffer);
8720
+ await writeFile2(localPath, buffer);
8535
8721
  log15.info("Attachment saved", {
8536
8722
  filename: name,
8537
8723
  path: localPath,
@@ -8545,7 +8731,7 @@ async function persistAttachments(attachments) {
8545
8731
  });
8546
8732
  if (textRes.ok) {
8547
8733
  extractedTextPath = `${localPath}.txt`;
8548
- await writeFile(extractedTextPath, await textRes.text(), "utf-8");
8734
+ await writeFile2(extractedTextPath, await textRes.text(), "utf-8");
8549
8735
  log15.info("Extracted text saved", { path: extractedTextPath });
8550
8736
  }
8551
8737
  } catch {
@@ -9132,17 +9318,17 @@ var HeadlessSession = class {
9132
9318
  return Promise.resolve(early);
9133
9319
  }
9134
9320
  const shouldTimeout = !USER_FACING_TOOLS.has(name);
9135
- return new Promise((resolve3) => {
9321
+ return new Promise((resolve4) => {
9136
9322
  const timeout = shouldTimeout ? setTimeout(() => {
9137
9323
  this.pendingTools.delete(id);
9138
- resolve3(
9324
+ resolve4(
9139
9325
  "Error: Tool timed out \u2014 no response from the app environment after 5 minutes."
9140
9326
  );
9141
9327
  }, EXTERNAL_TOOL_TIMEOUT_MS) : void 0;
9142
9328
  this.pendingTools.set(id, {
9143
9329
  resolve: (result) => {
9144
9330
  clearTimeout(timeout);
9145
- resolve3(result);
9331
+ resolve4(result);
9146
9332
  },
9147
9333
  timeout
9148
9334
  });
package/dist/index.js CHANGED
@@ -270,7 +270,7 @@ function isRetryableError(error) {
270
270
  /Unable to download/i.test(error);
271
271
  }
272
272
  function sleep(ms) {
273
- return new Promise((resolve3) => setTimeout(resolve3, ms));
273
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
274
274
  }
275
275
  async function* streamChatWithRetry(params, options) {
276
276
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
@@ -1203,7 +1203,7 @@ function formatCliResult(r) {
1203
1203
  return logBlock + body + truncNote;
1204
1204
  }
1205
1205
  function runCli(command, args2, options) {
1206
- return new Promise((resolve3) => {
1206
+ return new Promise((resolve4) => {
1207
1207
  const timeout = options?.timeout ?? 6e4;
1208
1208
  const maxBuffer = options?.maxBuffer ?? 1024 * 1024;
1209
1209
  let finalArgs = args2;
@@ -1232,7 +1232,7 @@ function runCli(command, args2, options) {
1232
1232
  if (killTimer) {
1233
1233
  clearTimeout(killTimer);
1234
1234
  }
1235
- resolve3(result);
1235
+ resolve4(result);
1236
1236
  };
1237
1237
  const child = spawn(command, finalArgs, {
1238
1238
  stdio: [options?.stdin ? "pipe" : "ignore", "pipe", "pipe"]
@@ -2173,7 +2173,7 @@ var init_surfaces = __esm({
2173
2173
  userPickable: true
2174
2174
  },
2175
2175
  imageGeneration: {
2176
- default: "seedream-4.5",
2176
+ default: "gpt-image-2",
2177
2177
  label: "Image Generation",
2178
2178
  description: "Creates images for your product \u2014 icons, illustrations, photos, and any other visual assets.",
2179
2179
  modelType: "image_generation",
@@ -3025,9 +3025,9 @@ function parseFrontmatter(content) {
3025
3025
  }
3026
3026
  const fields = {};
3027
3027
  for (const line of match[1].split("\n")) {
3028
- const sep = line.indexOf(":");
3029
- if (sep > 0) {
3030
- fields[line.slice(0, sep).trim()] = line.slice(sep + 1).trim();
3028
+ const sep2 = line.indexOf(":");
3029
+ if (sep2 > 0) {
3030
+ fields[line.slice(0, sep2).trim()] = line.slice(sep2 + 1).trim();
3031
3031
  }
3032
3032
  }
3033
3033
  return fields;
@@ -3482,7 +3482,7 @@ var init_bash = __esm({
3482
3482
  async execute(input, context) {
3483
3483
  const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES2;
3484
3484
  const timeoutMs = input.timeout ? input.timeout * 1e3 : DEFAULT_TIMEOUT_MS;
3485
- return new Promise((resolve3) => {
3485
+ return new Promise((resolve4) => {
3486
3486
  const child = spawn2("sh", ["-c", input.command], {
3487
3487
  // Pinned rather than inherited. `undefined` here means "wherever the
3488
3488
  // process happens to be", which is the project root only by luck.
@@ -3507,9 +3507,9 @@ var init_bash = __esm({
3507
3507
  clearTimeout(timer);
3508
3508
  if (!output) {
3509
3509
  if (code && code !== 0) {
3510
- resolve3(`Error: process exited with code ${code}`);
3510
+ resolve4(`Error: process exited with code ${code}`);
3511
3511
  } else {
3512
- resolve3("(no output)");
3512
+ resolve4("(no output)");
3513
3513
  }
3514
3514
  return;
3515
3515
  }
@@ -3535,18 +3535,18 @@ var init_bash = __esm({
3535
3535
  `${(MAX_OUTPUT_BYTES / 1024).toFixed(0)}KB of ${(totalBytes / 1024).toFixed(0)}KB`
3536
3536
  );
3537
3537
  }
3538
- resolve3(
3538
+ resolve4(
3539
3539
  truncated + `
3540
3540
 
3541
3541
  (truncated at ${reasons.join(" / ")} \u2014 narrow the command (grep, head/tail, smaller paths) instead of increasing limits)`
3542
3542
  );
3543
3543
  } else {
3544
- resolve3(output);
3544
+ resolve4(output);
3545
3545
  }
3546
3546
  });
3547
3547
  child.on("error", (err) => {
3548
3548
  clearTimeout(timer);
3549
- resolve3(`Error: ${err.message}`);
3549
+ resolve4(`Error: ${err.message}`);
3550
3550
  });
3551
3551
  });
3552
3552
  }
@@ -3669,17 +3669,17 @@ var init_grep = __esm({
3669
3669
  }
3670
3670
  const rgCmd = `rg ${rgFlags}${globFlag} '${escaped}' ${searchPath}`;
3671
3671
  const grepCmd = `grep ${grepFlags} '${escaped}' ${searchPath} --include='*.ts' --include='*.tsx' --include='*.js' --include='*.json' --include='*.md'`;
3672
- return new Promise((resolve3) => {
3672
+ return new Promise((resolve4) => {
3673
3673
  exec(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
3674
3674
  if (stdout?.trim()) {
3675
- resolve3(formatResults(stdout, max, mode));
3675
+ resolve4(formatResults(stdout, max, mode));
3676
3676
  return;
3677
3677
  }
3678
3678
  exec(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
3679
3679
  if (grepStdout?.trim()) {
3680
- resolve3(formatResults(grepStdout, max, mode));
3680
+ resolve4(formatResults(grepStdout, max, mode));
3681
3681
  } else {
3682
- resolve3("No matches found.");
3682
+ resolve4("No matches found.");
3683
3683
  }
3684
3684
  });
3685
3685
  });
@@ -4041,7 +4041,7 @@ var init_restartProcess = __esm({
4041
4041
  async execute(input) {
4042
4042
  const data = await lspRequest("/restart-process", { name: input.name });
4043
4043
  if (data.ok) {
4044
- await new Promise((resolve3) => setTimeout(resolve3, 5e3));
4044
+ await new Promise((resolve4) => setTimeout(resolve4, 5e3));
4045
4045
  return `Restarted ${input.name}.`;
4046
4046
  }
4047
4047
  return `Error: unexpected response: ${JSON.stringify(data)}`;
@@ -4299,6 +4299,26 @@ ${opts.styleMap}
4299
4299
  ${ANALYSIS_RESPONSE_FORMAT}`;
4300
4300
  return p;
4301
4301
  }
4302
+ async function renderHtmlViaSidecar(opts) {
4303
+ const result = await sidecarRequest(
4304
+ "/render-html",
4305
+ {
4306
+ html: opts.html,
4307
+ width: opts.width,
4308
+ height: opts.height,
4309
+ ...opts.transparent ? { transparent: true } : {},
4310
+ ...opts.scale != null ? { scale: opts.scale } : {}
4311
+ },
4312
+ { timeout: VIEWPORT_CAPTURE_TIMEOUT_MS }
4313
+ );
4314
+ const url = result?.url;
4315
+ if (!url) {
4316
+ throw new Error(
4317
+ `No URL in sidecar render response. The browser may not be ready yet. Response: ${JSON.stringify(result)}`
4318
+ );
4319
+ }
4320
+ return { url, width: result.width, height: result.height };
4321
+ }
4302
4322
  async function streamScreenshotAnalysis(opts) {
4303
4323
  const { image, prompt, styleMap, onLog, model, apiConfig } = opts;
4304
4324
  const url = await resolveImageRef(image, apiConfig);
@@ -5280,10 +5300,10 @@ function parseFrontmatter2(filePath) {
5280
5300
  }
5281
5301
  const fm = {};
5282
5302
  for (const line of match[1].split("\n")) {
5283
- const sep = line.indexOf(":");
5284
- if (sep > 0) {
5285
- const key = line.slice(0, sep).trim();
5286
- const val = line.slice(sep + 1).trim();
5303
+ const sep2 = line.indexOf(":");
5304
+ if (sep2 > 0) {
5305
+ const key = line.slice(0, sep2).trim();
5306
+ const val = line.slice(sep2 + 1).trim();
5287
5307
  fm[key] = val;
5288
5308
  }
5289
5309
  }
@@ -5677,7 +5697,7 @@ var init_screenshot2 = __esm({
5677
5697
  init_surfaces();
5678
5698
  screenshotDefinition = {
5679
5699
  name: "screenshot",
5680
- description: "Capture a screenshot of the app preview and get a description of what's on screen. Choose `fullPage`: `false` captures just the visible viewport (fast \u2014 for a specific section the page is scrolled to), `true` captures the entire page top-to-bottom (slower \u2014 for overall composition or content past the fold). Captures the settled page state \u2014 it cannot catch animations, transitions, or transient state. The analysis is not precise about every detail \u2014 for example it cannot reliably identify specific fonts by name, only describe what the letterforms look like. Optionally provide specific questions about what you're looking for. Use a bulleted list to ask many questions at once. To ask additional questions about a screenshot you have already captured, pass its URL as `imageUrl` to skip recapture; `imageUrl` also accepts the disk path of an image file (a user upload, a saved asset) to analyze that instead of the preview. If the screenshot requires interaction first (logging in, clicking a tab, dismissing a modal, scrolling to a section), use the instructions param to describe the steps. To render a fixed-size image such as an Open Graph share card, set `width` and `height` (e.g. 1200 \xD7 630) and `format: 'png'`: the tool navigates to `path`, clips to exactly those pixel dimensions, and returns the image URL.",
5700
+ description: "Capture a screenshot of the app preview and get a description of what's on screen. Choose `fullPage`: `false` captures just the visible viewport (fast \u2014 for a specific section the page is scrolled to), `true` captures the entire page top-to-bottom (slower \u2014 for overall composition or content past the fold). Captures the settled page state \u2014 it cannot catch animations, transitions, or transient state. The analysis is not precise about every detail \u2014 for example it cannot reliably identify specific fonts by name, only describe what the letterforms look like. Optionally provide specific questions about what you're looking for. Use a bulleted list to ask many questions at once. To ask additional questions about a screenshot you have already captured, pass its URL as `imageUrl` to skip recapture; `imageUrl` also accepts the disk path of an image file (a user upload, a saved asset) to analyze that instead of the preview. If the screenshot requires interaction first (logging in, clicking a tab, dismissing a modal, scrolling to a section), use the instructions param to describe the steps. To capture at exact pixel dimensions, set `width` and `height`: the tool clips to exactly that viewport and returns the image URL.",
5681
5701
  inputSchema: {
5682
5702
  type: "object",
5683
5703
  properties: {
@@ -5708,7 +5728,7 @@ var init_screenshot2 = __esm({
5708
5728
  format: {
5709
5729
  type: "string",
5710
5730
  enum: ["png", "jpeg"],
5711
- description: "Output image format. Defaults to 'jpeg'. Use 'png' for crisp flat graphics like share cards, where JPEG artifacts show on sharp type and edges."
5731
+ description: "Output image format. Defaults to 'jpeg'. Use 'png' for crisp flat graphics where JPEG artifacts show on sharp type and edges."
5712
5732
  },
5713
5733
  instructions: {
5714
5734
  type: "string",
@@ -6016,6 +6036,20 @@ var init_enhancePrompt = __esm({
6016
6036
  });
6017
6037
 
6018
6038
  // src/subagents/designExpert/tools/images/imageGenerator.ts
6039
+ function snapToOpenAiImageSize(width, height) {
6040
+ let best = OPENAI_IMAGE_SIZES[0];
6041
+ let bestScore = Infinity;
6042
+ for (const [w, h] of OPENAI_IMAGE_SIZES) {
6043
+ const aspectDiff = Math.abs(Math.log(w / h / (width / height)));
6044
+ const areaDiff = Math.abs(Math.log(w * h / (width * height)));
6045
+ const score = aspectDiff * 8 + areaDiff;
6046
+ if (score < bestScore) {
6047
+ bestScore = score;
6048
+ best = [w, h];
6049
+ }
6050
+ }
6051
+ return `${best[0]}x${best[1]}`;
6052
+ }
6019
6053
  async function generateImageAssets(opts) {
6020
6054
  const {
6021
6055
  prompts,
@@ -6030,7 +6064,11 @@ async function generateImageAssets(opts) {
6030
6064
  const sourceImages = opts.sourceImages?.length ? await resolveImageRefs(opts.sourceImages, apiConfig) : void 0;
6031
6065
  const width = opts.width || 2048;
6032
6066
  const height = opts.height || 2048;
6033
- const config = { width, height };
6067
+ const config = {
6068
+ width,
6069
+ height,
6070
+ size: snapToOpenAiImageSize(width, height)
6071
+ };
6034
6072
  if (sourceImages?.length) {
6035
6073
  const [firstImage] = sourceImages;
6036
6074
  config.images = sourceImages;
@@ -6149,7 +6187,7 @@ async function generateImageAssets(opts) {
6149
6187
  );
6150
6188
  return JSON.stringify({ images });
6151
6189
  }
6152
- var ANALYZE_PROMPT;
6190
+ var ANALYZE_PROMPT, OPENAI_IMAGE_SIZES;
6153
6191
  var init_imageGenerator = __esm({
6154
6192
  "src/subagents/designExpert/tools/images/imageGenerator.ts"() {
6155
6193
  "use strict";
@@ -6157,7 +6195,22 @@ var init_imageGenerator = __esm({
6157
6195
  init_analyzeImage();
6158
6196
  init_uploadImage();
6159
6197
  init_enhancePrompt();
6160
- ANALYZE_PROMPT = 'You are reviewing this image for a visual designer sourcing assets for a project. Describe: what the image depicts, the mood and color palette, how the lighting and composition work, any text present in the image, whether there are any issues (artifacts, distortions), and how it could be used in a layout for an app or website. Be concise and practical. Respond only with your analysis as Markdown (starting with the title "Asset Review") and absolutely no other text. Do not use emojis - use unicode if you need symbols.';
6198
+ ANALYZE_PROMPT = 'You are reviewing this image for a visual designer sourcing assets for a project. Describe: what the image depicts, the mood and color palette, how the lighting and composition work, any text present in the image, whether there are any issues (artifacts, distortions), whether the artwork extends fully to all four edges of the canvas or sits inset (call out any baked-in border, frame, rounded-corner mask, drop-shadow margin, or mockup presentation such as an icon rendered on a background or device), and how it could be used in a layout for an app or website. Be concise and practical. Respond only with your analysis as Markdown (starting with the title "Asset Review") and absolutely no other text. Do not use emojis - use unicode if you need symbols.';
6199
+ OPENAI_IMAGE_SIZES = [
6200
+ [768, 1024],
6201
+ [1024, 768],
6202
+ [1024, 1024],
6203
+ [1024, 1536],
6204
+ [1536, 1024],
6205
+ [1280, 2560],
6206
+ [2560, 1280],
6207
+ [1440, 2560],
6208
+ [2560, 1440],
6209
+ [1792, 2400],
6210
+ [2400, 1792],
6211
+ [2400, 2880],
6212
+ [2560, 2560]
6213
+ ];
6161
6214
  }
6162
6215
  });
6163
6216
 
@@ -6314,6 +6367,150 @@ var init_editImages = __esm({
6314
6367
  }
6315
6368
  });
6316
6369
 
6370
+ // src/subagents/designExpert/tools/images/renderImage.ts
6371
+ var renderImage_exports = {};
6372
+ __export(renderImage_exports, {
6373
+ definition: () => definition7,
6374
+ execute: () => execute7
6375
+ });
6376
+ import { mkdir, unlink, writeFile } from "fs/promises";
6377
+ import { tmpdir } from "os";
6378
+ import { dirname, join, resolve as resolve2, sep } from "path";
6379
+ import { randomUUID } from "crypto";
6380
+ async function execute7(input, onLog, context) {
6381
+ const html = typeof input.html === "string" ? input.html : "";
6382
+ const width = Math.round(Number(input.width));
6383
+ const height = Math.round(Number(input.height));
6384
+ if (!html || !Number.isFinite(width) || !Number.isFinite(height) || width < MIN_DIMENSION || width > MAX_DIMENSION || height < MIN_DIMENSION || height > MAX_DIMENSION) {
6385
+ return `Error: renderImage requires an html string plus width/height between ${MIN_DIMENSION} and ${MAX_DIMENSION}.`;
6386
+ }
6387
+ const release = await acquireBrowserLock();
6388
+ let rendered;
6389
+ try {
6390
+ onLog?.("Rendering document in the sandbox browser...");
6391
+ rendered = await renderHtmlViaSidecar({
6392
+ html,
6393
+ width,
6394
+ height,
6395
+ transparent: input.transparentBackground === true,
6396
+ scale: typeof input.scale === "number" ? input.scale : void 0
6397
+ });
6398
+ } catch (err) {
6399
+ return `Error: render failed: ${err?.message ?? err}`;
6400
+ } finally {
6401
+ release();
6402
+ }
6403
+ let bytes;
6404
+ try {
6405
+ const res = await fetch(rendered.url);
6406
+ if (!res.ok) {
6407
+ throw new Error(`fetch returned ${res.status}`);
6408
+ }
6409
+ bytes = Buffer.from(await res.arrayBuffer());
6410
+ } catch (err) {
6411
+ return `Error: rendered but could not download the capture (${err?.message ?? err}). Temporary URL: ${rendered.url}`;
6412
+ }
6413
+ let savedPath;
6414
+ if (typeof input.savePath === "string" && input.savePath) {
6415
+ const absolute = resolve2(PROJECT_ROOT, input.savePath);
6416
+ if (!absolute.startsWith(PROJECT_ROOT + sep)) {
6417
+ return "Error: savePath must resolve inside the project.";
6418
+ }
6419
+ await mkdir(dirname(absolute), { recursive: true });
6420
+ await writeFile(absolute, bytes);
6421
+ savedPath = input.savePath;
6422
+ }
6423
+ onLog?.("Hosting the capture...");
6424
+ const tmpPath = join(tmpdir(), `render-${randomUUID()}.png`);
6425
+ let url = rendered.url;
6426
+ let temporary = true;
6427
+ try {
6428
+ await writeFile(tmpPath, bytes);
6429
+ const upload = await runMindstudioCliResult(["upload", tmpPath], {
6430
+ timeout: 6e4,
6431
+ onLog,
6432
+ caller: "designExpert"
6433
+ });
6434
+ const match = upload.ok ? upload.value.match(/https:\/\/\S+/g) : null;
6435
+ if (match?.length) {
6436
+ url = match[match.length - 1];
6437
+ temporary = false;
6438
+ }
6439
+ } finally {
6440
+ await unlink(tmpPath).catch(() => {
6441
+ });
6442
+ }
6443
+ const analysis = await analyzeImage({
6444
+ prompt: RENDER_ANALYZE_PROMPT,
6445
+ image: url,
6446
+ onLog,
6447
+ model: resolveModel("imageAnalysis", context?.models, context?.model)
6448
+ }).then((r) => r.analysis).catch((err) => `Could not review this image: ${err.message}`);
6449
+ return JSON.stringify({
6450
+ images: [
6451
+ {
6452
+ url,
6453
+ ...temporary ? {
6454
+ note: "Durable hosting failed \u2014 this URL is dev-session scratch storage and may expire. Do not use it for app metadata; retry if a durable URL is needed."
6455
+ } : {},
6456
+ ...savedPath ? { savedPath } : {},
6457
+ analysis,
6458
+ width: rendered.width,
6459
+ height: rendered.height
6460
+ }
6461
+ ]
6462
+ });
6463
+ }
6464
+ var MIN_DIMENSION, MAX_DIMENSION, RENDER_ANALYZE_PROMPT, definition7;
6465
+ var init_renderImage = __esm({
6466
+ "src/subagents/designExpert/tools/images/renderImage.ts"() {
6467
+ "use strict";
6468
+ init_screenshot();
6469
+ init_browserLock();
6470
+ init_runMindstudioCli();
6471
+ init_analyzeImage();
6472
+ init_surfaces();
6473
+ init_projectRoot();
6474
+ MIN_DIMENSION = 16;
6475
+ MAX_DIMENSION = 4096;
6476
+ RENDER_ANALYZE_PROMPT = 'You are reviewing a browser-rendered graphic (composed from HTML/CSS by a designer) for fidelity. Report: whether the composition fills the full canvas or leaves unintended gaps at any edge, any clipped or overflowing text, whether custom webfonts appear to have loaded (distinctive letterforms vs generic fallback serif/sans), any misalignment or uneven spacing, any unintended scrollbars or default-styling artifacts, and \u2014 if the background is transparent \u2014 any fringing or stray opaque pixels at the edges. Then briefly describe the overall composition and how polished it looks. Be concise and practical. Respond only with your analysis as Markdown (starting with the title "Render Review") and absolutely no other text. Do not use emojis - use unicode if you need symbols.';
6477
+ definition7 = {
6478
+ name: "renderImage",
6479
+ description: "Render a self-contained HTML document in a real browser and capture it as a hosted PNG at exact pixel dimensions, with a fidelity review included. Deterministic \u2014 exact hex colors, real loaded webfonts, precise geometry \u2014 unlike generateImages, which is an image model. Use for token-exact graphics: Open Graph share cards, wordmarks, flat/geometric icon tiles, badges, any composition where letterforms and spacing carry the design. Compose with HTML/CSS (link webfonts from CDNs \u2014 the renderer waits for them to load); inline existing SVG markup when needed, but never hand-write new SVG path data.",
6480
+ inputSchema: {
6481
+ type: "object",
6482
+ properties: {
6483
+ html: {
6484
+ type: "string",
6485
+ description: "A complete, self-contained HTML document sized to fill the viewport (style html/body to the full dimensions with margin 0). External webfonts and images from CDNs are fine \u2014 loading is awaited before capture."
6486
+ },
6487
+ width: {
6488
+ type: "number",
6489
+ description: "Viewport width in CSS pixels (e.g. 1200 for an OG card, 512 for an icon tile). Range: 16-4096."
6490
+ },
6491
+ height: {
6492
+ type: "number",
6493
+ description: "Viewport height in CSS pixels. Range: 16-4096."
6494
+ },
6495
+ scale: {
6496
+ type: "number",
6497
+ description: "Device scale factor, 1-3. Output pixels = css \xD7 scale. Use 2 for crisp icon masters (e.g. a 512\xD7512 document captured at 1024\xD71024)."
6498
+ },
6499
+ transparentBackground: {
6500
+ type: "boolean",
6501
+ description: "Capture with true alpha: leave the document background transparent (no background on html/body) and the PNG keeps it. No background-removal model involved."
6502
+ },
6503
+ savePath: {
6504
+ type: "string",
6505
+ description: "Optional project-relative path to also save the PNG into the app (e.g. 'dist/interfaces/web/public/og-image.png' so the deployed site self-hosts it)."
6506
+ }
6507
+ },
6508
+ required: ["html", "width", "height"]
6509
+ }
6510
+ };
6511
+ }
6512
+ });
6513
+
6317
6514
  // src/subagents/copyEditor/tools.ts
6318
6515
  var COPY_EDITOR_TOOLS;
6319
6516
  var init_tools3 = __esm({
@@ -6388,18 +6585,18 @@ var init_copyEditor = __esm({
6388
6585
  // src/subagents/designExpert/tools/polishCopy.ts
6389
6586
  var polishCopy_exports = {};
6390
6587
  __export(polishCopy_exports, {
6391
- definition: () => definition7,
6392
- execute: () => execute7
6588
+ definition: () => definition8,
6589
+ execute: () => execute8
6393
6590
  });
6394
- async function execute7(input, _onLog, context) {
6591
+ async function execute8(input, _onLog, context) {
6395
6592
  return copyEditorTool.execute(input, context);
6396
6593
  }
6397
- var definition7;
6594
+ var definition8;
6398
6595
  var init_polishCopy = __esm({
6399
6596
  "src/subagents/designExpert/tools/polishCopy.ts"() {
6400
6597
  "use strict";
6401
6598
  init_copyEditor();
6402
- definition7 = {
6599
+ definition8 = {
6403
6600
  name: "polishCopy",
6404
6601
  description: "Hand off any user-facing copy you've written \u2014 headlines, captions, labels, body text \u2014 and get back a sharper version: better built for its audience and free of the fingerprints that make writing read as AI. It elevates how the copy communicates without inventing facts or claims you didn't give it. Give it the text plus what it's for (where it appears, the audience).",
6405
6602
  inputSchema: {
@@ -6437,6 +6634,7 @@ var init_tools4 = __esm({
6437
6634
  init_analyzeImage2();
6438
6635
  init_generateImages();
6439
6636
  init_editImages();
6637
+ init_renderImage();
6440
6638
  init_polishCopy();
6441
6639
  init_screenshot2();
6442
6640
  tools = {
@@ -6450,6 +6648,7 @@ var init_tools4 = __esm({
6450
6648
  screenshot: { definition: screenshotDefinition, execute: executeScreenshot },
6451
6649
  generateImages: generateImages_exports,
6452
6650
  editImages: editImages_exports,
6651
+ renderImage: renderImage_exports,
6453
6652
  polishCopy: polishCopy_exports
6454
6653
  };
6455
6654
  DESIGN_EXPERT_TOOLS = [
@@ -6992,13 +7191,13 @@ var init_tools5 = __esm({
6992
7191
  // src/subagents/productVision/executor.ts
6993
7192
  import fs18 from "fs";
6994
7193
  import path10 from "path";
6995
- function resolve2(filePath) {
7194
+ function resolve3(filePath) {
6996
7195
  return path10.join(ROADMAP_DIR, filePath);
6997
7196
  }
6998
7197
  async function executeVisionTool(name, input, context) {
6999
7198
  switch (name) {
7000
7199
  case "writeFile": {
7001
- const filePath = resolve2(input.path);
7200
+ const filePath = resolve3(input.path);
7002
7201
  try {
7003
7202
  fs18.mkdirSync(ROADMAP_DIR, { recursive: true });
7004
7203
  let oldContent = null;
@@ -7016,7 +7215,7 @@ ${unifiedDiff(filePath, oldContent ?? "", input.content)}`;
7016
7215
  }
7017
7216
  }
7018
7217
  case "deleteFile": {
7019
- const filePath = resolve2(input.path);
7218
+ const filePath = resolve3(input.path);
7020
7219
  try {
7021
7220
  if (!fs18.existsSync(filePath)) {
7022
7221
  return `Error: ${filePath} does not exist`;
@@ -7033,7 +7232,7 @@ ${unifiedDiff(filePath, oldContent, "")}`;
7033
7232
  if (!context) {
7034
7233
  return "Error: writePitchDeck requires execution context for design expert delegation";
7035
7234
  }
7036
- const filePath = resolve2("pitch.html");
7235
+ const filePath = resolve3("pitch.html");
7037
7236
  try {
7038
7237
  fs18.mkdirSync(ROADMAP_DIR, { recursive: true });
7039
7238
  const exists = fs18.existsSync(filePath);
@@ -8204,14 +8403,14 @@ function buildCorpus() {
8204
8403
  });
8205
8404
  }
8206
8405
  }
8207
- const sep = "\n\n---\n\n";
8406
+ const sep2 = "\n\n---\n\n";
8208
8407
  const sections = [];
8209
8408
  let usedChars = 0;
8210
8409
  for (const { path: p, content } of files) {
8211
8410
  const section = `## File: ${p}
8212
8411
 
8213
8412
  ${content}`;
8214
- const added = section.length + (sections.length > 0 ? sep.length : 0);
8413
+ const added = section.length + (sections.length > 0 ? sep2.length : 0);
8215
8414
  if (sections.length > 0 && usedChars + added > BRAND_CORPUS_CHAR_LIMIT) {
8216
8415
  sections.push(
8217
8416
  `(brand corpus truncated: included ${sections.length} of ${files.length} files, ~${(usedChars / 1024).toFixed(0)}KB; brand-relevant files were prioritized.)`
@@ -8221,7 +8420,7 @@ ${content}`;
8221
8420
  sections.push(section);
8222
8421
  usedChars += added;
8223
8422
  }
8224
- return sections.join(sep);
8423
+ return sections.join(sep2);
8225
8424
  }
8226
8425
  function headSlice(content) {
8227
8426
  if (content.length <= HEAD_SLICE_CHARS) {
@@ -9352,8 +9551,8 @@ var init_config = __esm({
9352
9551
 
9353
9552
  // src/headless/attachments.ts
9354
9553
  import { mkdirSync, existsSync } from "fs";
9355
- import { writeFile } from "fs/promises";
9356
- import { basename as basename2, join, extname as extname2 } from "path";
9554
+ import { writeFile as writeFile2 } from "fs/promises";
9555
+ import { basename as basename2, join as join2, extname as extname2 } from "path";
9357
9556
  function filenameFromUrl(url) {
9358
9557
  try {
9359
9558
  const pathname = new URL(url).pathname;
@@ -9364,7 +9563,7 @@ function filenameFromUrl(url) {
9364
9563
  }
9365
9564
  }
9366
9565
  function resolveUniqueFilename(name, claimed) {
9367
- const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join(UPLOADS_DIR, candidate));
9566
+ const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join2(UPLOADS_DIR, candidate));
9368
9567
  if (isFree(name)) {
9369
9568
  return name;
9370
9569
  }
@@ -9398,7 +9597,7 @@ async function persistAttachments(attachments) {
9398
9597
  const results = await Promise.allSettled(
9399
9598
  nonVoice.map(async (att, i) => {
9400
9599
  const name = names[i];
9401
- const localPath = join(UPLOADS_DIR, name);
9600
+ const localPath = join2(UPLOADS_DIR, name);
9402
9601
  const res = await fetch(att.url, {
9403
9602
  signal: AbortSignal.timeout(3e4)
9404
9603
  });
@@ -9406,7 +9605,7 @@ async function persistAttachments(attachments) {
9406
9605
  throw new Error(`HTTP ${res.status} downloading ${att.url}`);
9407
9606
  }
9408
9607
  const buffer = Buffer.from(await res.arrayBuffer());
9409
- await writeFile(localPath, buffer);
9608
+ await writeFile2(localPath, buffer);
9410
9609
  log15.info("Attachment saved", {
9411
9610
  filename: name,
9412
9611
  path: localPath,
@@ -9420,7 +9619,7 @@ async function persistAttachments(attachments) {
9420
9619
  });
9421
9620
  if (textRes.ok) {
9422
9621
  extractedTextPath = `${localPath}.txt`;
9423
- await writeFile(extractedTextPath, await textRes.text(), "utf-8");
9622
+ await writeFile2(extractedTextPath, await textRes.text(), "utf-8");
9424
9623
  log15.info("Extracted text saved", { path: extractedTextPath });
9425
9624
  }
9426
9625
  } catch {
@@ -10064,17 +10263,17 @@ var init_headless = __esm({
10064
10263
  return Promise.resolve(early);
10065
10264
  }
10066
10265
  const shouldTimeout = !USER_FACING_TOOLS.has(name);
10067
- return new Promise((resolve3) => {
10266
+ return new Promise((resolve4) => {
10068
10267
  const timeout = shouldTimeout ? setTimeout(() => {
10069
10268
  this.pendingTools.delete(id);
10070
- resolve3(
10269
+ resolve4(
10071
10270
  "Error: Tool timed out \u2014 no response from the app environment after 5 minutes."
10072
10271
  );
10073
10272
  }, EXTERNAL_TOOL_TIMEOUT_MS) : void 0;
10074
10273
  this.pendingTools.set(id, {
10075
10274
  resolve: (result) => {
10076
10275
  clearTimeout(timeout);
10077
- resolve3(result);
10276
+ resolve4(result);
10078
10277
  },
10079
10278
  timeout
10080
10279
  });
@@ -79,6 +79,6 @@ When you receive background results:
79
79
 
80
80
  You can only background the following two tasks, unless the user specifically asks you to do work in the background:
81
81
  - `productVision` seeding the initial roadmap after writing the spec for the first time, or updating the roadmap after large work sessions. Always background these: they take a while, and building continues while the roadmap catches up.
82
- - After writing the spec, once you have finalized the shape of the app, ask `visualDesignExpert` to create an icon and to design an Open Graph share image, then set them with `setProjectMetadata` alongside the app's name and short description. The icon is a generated asset. The share image should be composed as a self-contained HTML card (real lockup, brand fonts, exact type) and captured — not generated: serve the card HTML from `dist/interfaces/web/public/`, screenshot it with the `screenshot` tool at `width: 1200, height: 630, format: 'png'`, and save the resulting PNG into the app's public assets so the deployed site hosts it.
82
+ - After writing the spec, once you have finalized the shape of the app, ask `visualDesignExpert` to create an app icon and an Open Graph share image (1200×630) and to report back the hosted asset URLs, then set them with `setProjectMetadata` alongside the app's name and short description. The designer produces and hosts both assets itself do not compose, screenshot, or host them yourself.
83
83
 
84
84
  Do not background any other tasks. Be aware that sometimes tools like specSync will background on their own - this is not something that is within your control.
@@ -65,21 +65,31 @@ Editorial photography is the right call for hero images, landing pages, marketin
65
65
 
66
66
  The developer should never need to source their own imagery. Always provide URLs.
67
67
 
68
+ ### Rendered graphics — renderImage
69
+
70
+ `renderImage` renders a self-contained HTML document you author in a real browser and returns a durably hosted PNG at exact pixel dimensions, with a fidelity review. It is the deterministic counterpart to `generateImages`: exact hex colors, real loaded webfonts, precise geometry. Route any token-exact graphic through it — share cards, wordmarks, flat/geometric icon tiles, badges, anything where letterforms and spacing carry the design. Use the image model for organic, photographic, and illustrated work; use the browser for precision.
71
+
72
+ Author it as HTML/CSS. The renderer waits for fonts to load before capturing. Size `html`/`body` to the full dimensions with `margin: 0`. When you need glyphs, inline a known-good SVG from something like Tabler or build the shape from CSS. Set `scale: 2` for crisp raster masters. `transparentBackground: true` with no background on `html`/`body` gives a true-alpha PNG with clean edeges.
73
+
68
74
  ### Icons and logos
69
75
 
70
76
  App icons and logos require work and thinking to get right. Prefer to use logos and icons as opposed to generic wordmarks when representing the app in UI (e.g., in navigation, on landing pages, login moments, etc).
71
77
 
78
+ Match the engine to the icon direction: the smooth 3D emoji style below is `generateImages` territory; a flat, geometric, or design-system icon (a solid tile with a glyph, exact brand tokens) should be composed as HTML/CSS and rendered with `renderImage` instead.
79
+
72
80
  **What works:** Smooth 3D rendering in the style of 2026-era macOS/iOS app icons - apple emoji/nintendo style works really well for beautiful iconography. One clear object or symbol — rounded, immediately recognizable. Clean surfaces with soft lighting and gentle shadows. Two or three accent colors, not a rainbow. Always full bleed.
73
81
 
74
82
  **What doesn't work:** Flat illustration looks dated, photorealistic rendering is too noisy at small sizes, overly detailed scenes become illegible.
75
83
 
84
+ An app icon master must be a full-bleed square with sharp corners — the OS applies its own rounded-corner mask, so a result that comes back pre-rounded, inset on a background, framed, bordered, or floating with a drop shadow is unusable, not a style choice. Image models drift toward exactly that mockup presentation because their training data is full of App Store screenshots. Check each generation's analysis before accepting an icon: the artwork must reach all four edges with square corners. If a variant comes back inset or pre-rounded, regenerate rather than settle for it.
85
+
76
86
  Keep logos and icons consistent - if you already have a logo, use `editImages` to turn it into an icon, and vice versa.
77
87
 
78
88
  #### Open Graph Sharing Images
79
89
 
80
90
  OG images show up in iMessage, Slack, Twitter, etc. at small sizes. They're a mood piece, not a messaging opportunity. Keep text minimal: the app name and at most a short tagline (three to five words). Think App Store feature card — one beautiful composition that makes someone want to tap. The text should feel integrated into the scene, not pasted on a background.
81
91
 
82
- A share card is a wordmark, a short line, and a logo on a brand field — **compose it as HTML, don't generate it with the image model.** A generated image gives you odd letterforms and no brand fidelity; HTML gives you the real SVG lockup, the actual brand fonts, exact colors, and pixel-perfect spacing. Author a self-contained HTML document sized to 1200 × 630 the lockup inline as SVG, the brand fonts inlined as base64 (so nothing loads late and captures as a fallback), the palette and type exact then capture it with the `screenshot` tool at `width: 1200, height: 630, format: 'png'`, a faithful real-browser render. Don't route it through a document/HTML-to-image "openGraph" render mode; that pipeline strips CSS backgrounds. The same compose-and-capture approach beats generation for any precise brand graphic where letterforms and spacing carry the design.
92
+ A share card is a wordmark, a short line, and a logo on a brand field — **compose it as HTML and render it with `renderImage` at 1200 × 630, don't generate it with the image model.** A generated image gives you odd letterforms and no brand fidelity; a browser render gives you the real lockup, the actual brand fonts, exact colors, and pixel-perfect spacing. The returned URL is durably hosted and ready to hand over for app metadata; pass `savePath` (e.g. `dist/interfaces/web/public/og-image.png`) when the deployed site should also self-host the asset for its `og:image` meta tags. Don't route it through a document/HTML-to-image "openGraph" render mode; that pipeline strips CSS backgrounds.
83
93
 
84
94
  ### When to use images
85
95
 
@@ -14,6 +14,7 @@ Think about the ways you can truly elevate the design. Use image generation to c
14
14
  - When multiple tool calls are independent, make them all in a single turn. Searching for three different products, or fetching two reference sites: batch them instead of doing one per turn.
15
15
  - The screenshot tool supports an `instructions` parameter for taking screenshots that require interaction first. If you need to screenshot a state that's behind a modal, a specific tab, or a multi-step flow, pass `instructions` describing how to get there (e.g., "dismiss the welcome modal, then click XYZ"). A browser automation agent will follow your instructions and capture the screenshot for you. You can not use this to scroll - you will always receive a full page screenshot. Only use this if you need to trigger stateful changes within the app to get the full-page screenshot.
16
16
  - After you've taken a screenshot, use analyze image to ask different questions about it - don't re-screenshot the page unnecessarily.
17
+ - Match the image engine to the job: `renderImage` (a browser rendering HTML you author) for token-exact graphics — share cards, wordmarks, flat icon tiles; `generateImages` (an image model) for organic, photographic, and illustrated work. Don't ask the image model to hit exact hex codes or typography, and don't hand-write SVG path data — compose HTML/CSS and render it.
17
18
  - When you write user-facing copy (headlines, captions, labels, body text), hand it to `polishCopy` before finalizing. It tightens prose so it reads like a person wrote it rather than a machine, without changing what it says. Cheap and fast — use it on any copy that will ship.
18
19
 
19
20
  ## Voice
@@ -61,12 +61,16 @@ For photorealistic images, go deep on four dimensions:
61
61
 
62
62
  For app icons and logos, the goal is something that reads clearly at phone home screen size and feels polished and beautiful - like it could appear as an "App of the Year" award winner.
63
63
 
64
- - Frame as "A 3D icon against a ful bleed XYZ background" followed by the subject. Do NOT use the phrase "app icon" it triggers mockup framing (the model renders an icon inset on a phone screen or mounted on a wall). "3D icon" works. Always specify "Full bleed square composition with no padding or margin".
65
- - Describe smooth, rounded emoji-type 3D objects — think current macOS/iOS app icon design language. Apple emoji/nintendo style works really well for beautiful iconography. Not flat illustration, not photorealistic, not vectors.
64
+ The single biggest failure mode is inherited mockup framing. The model's training data is saturated with App Store screenshots and portfolio shots, so icon vocabulary pulls it toward rendering the *presentation* of an icon — a rounded rectangle with a drop shadow, inset on a white background, on a phone screen, or mounted on a wall instead of the artwork itself. The deliverable is always a full-bleed square master with sharp corners (the OS applies its own corner mask), so a pre-rounded or inset result is unusable. Every rule below exists to defeat that pull:
65
+
66
+ - Frame as "A 3D icon against a XYZ background" followed by the subject. Do NOT use the phrase "app icon" — it triggers mockup framing. "3D icon" works.
67
+ - Do NOT mention rounded corners, corner radius, squircles, masks, borders, frames, padding, margins, safe zones, or the icon "floating" or "on a background" — every one of these summons the inset-rounded-rectangle mockup. Do not say "full bleed" as a bare label either; the model follows positive claims, not layout jargon or prohibitions.
68
+ - Instead, make edge-to-edge coverage a positive, physical property of the background, and end every icon prompt with a composition clause to this effect: "The background fills the entire square canvas, extending past all four edges of the frame; the corners of the image are sharp and square; the composition is presented flat and viewed straight-on."
69
+ - Center the subject and keep it within roughly the middle 70% of the frame — the OS corner mask crops into the corners, and a centered subject survives a corrective crop if a stray border does sneak in.
70
+ - Describe smooth, rounded emoji-type 3D objects — think current macOS/iOS app icon design language. Apple emoji/nintendo style works really well for beautiful iconography. Not flat illustration, not photorealistic, not vectors. (Rounded applies to the *object's* forms only — never to the canvas or its corners.)
66
71
  - Subjects should be immediately recognizable. Prefer one clear object or symbol, not a scene.
67
72
  - Specify "reads well at small sizes" as an explicit constraint.
68
73
  - Keep color intentional and limited — two or three accent colors plus the object's base tone. Colors should complement the app's brand if known.
69
- - You must specify that the image is full bleed - never say anything about rounded corners or there is a high likelihood that the image will come back as a rounded rectangle on a white background!
70
74
  - Apply the same material/lighting/color density as photography prompts, just to a single object. Describe the surface finish ("high-gloss lacquered finish with clean specular highlights," "soft matte ceramic with subtle surface texture"), the lighting behavior ("warm directional light from upper left producing a bright highlight streak across the curved surface and a soft shadow beneath"), and color as relationships ("deep coral body graduating to warm peach at the highlight edge, with a cream accent on the lens element"). Generic descriptors like "clean surfaces, soft lighting" produce generic icons.
71
75
 
72
76
  ## Output
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindstudio-ai/remy",
3
- "version": "0.1.275",
3
+ "version": "0.1.276",
4
4
  "description": "Remy coding agent",
5
5
  "repository": {
6
6
  "type": "git",