@mindstudio-ai/remy 0.1.307 → 0.1.309

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
@@ -3243,6 +3243,7 @@ async function renderHtmlViaSidecar(opts) {
3243
3243
  html: opts.html,
3244
3244
  width: opts.width,
3245
3245
  height: opts.height,
3246
+ ...opts.autoHeight ? { autoHeight: true } : {},
3246
3247
  ...opts.transparent ? { transparent: true } : {},
3247
3248
  ...opts.scale != null ? { scale: opts.scale } : {}
3248
3249
  },
@@ -4566,7 +4567,7 @@ The first-party SDK (@mindstudio-ai/agent) provides access to 200+ AI models (Op
4566
4567
  ## What Remy apps are NOT good for
4567
4568
 
4568
4569
  - Native mobile apps (iOS/Android). Mobile-responsive web apps are fine.
4569
- - Real-time multiplayer with persistent connections (no WebSocket support). Turn-based or async patterns work.
4570
+ - Fast-twitch multiplayer and live co-editing (shared cursors, 60fps sync) \u2014 client\u2192server is always a method invoke, so sub-100ms bidirectional interaction isn't a fit.
4570
4571
  </platform_brief>`;
4571
4572
  }
4572
4573
 
@@ -4933,9 +4934,146 @@ async function execute2(input, onLog) {
4933
4934
  // src/subagents/designExpert/tools/analyzeDesign.ts
4934
4935
  var analyzeDesign_exports = {};
4935
4936
  __export(analyzeDesign_exports, {
4937
+ definition: () => definition4,
4938
+ execute: () => execute4
4939
+ });
4940
+ import { readFile as readFile2 } from "fs/promises";
4941
+ import { extname as extname2, join as join2 } from "path";
4942
+
4943
+ // src/subagents/designExpert/tools/images/renderImage.ts
4944
+ var renderImage_exports = {};
4945
+ __export(renderImage_exports, {
4946
+ RENDER_ANALYZE_PROMPT: () => RENDER_ANALYZE_PROMPT,
4936
4947
  definition: () => definition3,
4937
4948
  execute: () => execute3
4938
4949
  });
4950
+ import { mkdir, unlink, writeFile } from "fs/promises";
4951
+ import { tmpdir } from "os";
4952
+ import { dirname, join, resolve as resolve2, sep } from "path";
4953
+ import { randomUUID } from "crypto";
4954
+ var MIN_DIMENSION = 16;
4955
+ var MAX_DIMENSION = 4096;
4956
+ 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.';
4957
+ var definition3 = {
4958
+ name: "renderImage",
4959
+ 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.",
4960
+ inputSchema: {
4961
+ type: "object",
4962
+ properties: {
4963
+ html: {
4964
+ type: "string",
4965
+ 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."
4966
+ },
4967
+ width: {
4968
+ type: "number",
4969
+ description: "Viewport width in CSS pixels (e.g. 1200 for an OG card, 512 for an icon tile). Range: 16-4096."
4970
+ },
4971
+ height: {
4972
+ type: "number",
4973
+ description: "Viewport height in CSS pixels. Range: 16-4096."
4974
+ },
4975
+ scale: {
4976
+ type: "number",
4977
+ 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)."
4978
+ },
4979
+ transparentBackground: {
4980
+ type: "boolean",
4981
+ 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."
4982
+ },
4983
+ savePath: {
4984
+ type: "string",
4985
+ 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)."
4986
+ }
4987
+ },
4988
+ required: ["html", "width", "height"]
4989
+ }
4990
+ };
4991
+ async function execute3(input, onLog, context) {
4992
+ const html = typeof input.html === "string" ? input.html : "";
4993
+ const width = Math.round(Number(input.width));
4994
+ const height = Math.round(Number(input.height));
4995
+ if (!html || !Number.isFinite(width) || !Number.isFinite(height) || width < MIN_DIMENSION || width > MAX_DIMENSION || height < MIN_DIMENSION || height > MAX_DIMENSION) {
4996
+ return `Error: renderImage requires an html string plus width/height between ${MIN_DIMENSION} and ${MAX_DIMENSION}.`;
4997
+ }
4998
+ const release = await acquireBrowserLock();
4999
+ let rendered;
5000
+ try {
5001
+ onLog?.("Rendering document in the sandbox browser...");
5002
+ rendered = await renderHtmlViaSidecar({
5003
+ html,
5004
+ width,
5005
+ height,
5006
+ transparent: input.transparentBackground === true,
5007
+ scale: typeof input.scale === "number" ? input.scale : void 0
5008
+ });
5009
+ } catch (err) {
5010
+ return `Error: render failed: ${err?.message ?? err}`;
5011
+ } finally {
5012
+ release();
5013
+ }
5014
+ let bytes;
5015
+ try {
5016
+ const res = await fetch(rendered.url);
5017
+ if (!res.ok) {
5018
+ throw new Error(`fetch returned ${res.status}`);
5019
+ }
5020
+ bytes = Buffer.from(await res.arrayBuffer());
5021
+ } catch (err) {
5022
+ return `Error: rendered but could not download the capture (${err?.message ?? err}). Temporary URL: ${rendered.url}`;
5023
+ }
5024
+ let savedPath;
5025
+ if (typeof input.savePath === "string" && input.savePath) {
5026
+ const absolute = resolve2(PROJECT_ROOT, input.savePath);
5027
+ if (!absolute.startsWith(PROJECT_ROOT + sep)) {
5028
+ return "Error: savePath must resolve inside the project.";
5029
+ }
5030
+ await mkdir(dirname(absolute), { recursive: true });
5031
+ await writeFile(absolute, bytes);
5032
+ savedPath = input.savePath;
5033
+ }
5034
+ onLog?.("Hosting the capture...");
5035
+ const tmpPath = join(tmpdir(), `render-${randomUUID()}.png`);
5036
+ let url = rendered.url;
5037
+ let temporary = true;
5038
+ try {
5039
+ await writeFile(tmpPath, bytes);
5040
+ const upload = await runMindstudioCliResult(["upload", tmpPath], {
5041
+ timeout: 6e4,
5042
+ onLog,
5043
+ caller: "designExpert"
5044
+ });
5045
+ const match = upload.ok ? upload.value.match(/https:\/\/\S+/g) : null;
5046
+ if (match?.length) {
5047
+ url = match[match.length - 1];
5048
+ temporary = false;
5049
+ }
5050
+ } finally {
5051
+ await unlink(tmpPath).catch(() => {
5052
+ });
5053
+ }
5054
+ const analysis = await analyzeImage({
5055
+ prompt: RENDER_ANALYZE_PROMPT,
5056
+ image: url,
5057
+ onLog,
5058
+ model: resolveModel("imageAnalysis", context?.models, context?.model)
5059
+ }).then((r) => r.analysis).catch((err) => `Could not review this image: ${err.message}`);
5060
+ return JSON.stringify({
5061
+ images: [
5062
+ {
5063
+ url,
5064
+ ...temporary ? {
5065
+ 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."
5066
+ } : {},
5067
+ ...savedPath ? { savedPath } : {},
5068
+ analysis,
5069
+ width: rendered.width,
5070
+ height: rendered.height
5071
+ }
5072
+ ]
5073
+ });
5074
+ }
5075
+
5076
+ // src/subagents/designExpert/tools/analyzeDesign.ts
4939
5077
  var DESIGN_REFERENCE_PROMPT = `
4940
5078
  You are analyzing a screenshot of a real website or app for a designer's personal technique/inspiration reference notes.
4941
5079
 
@@ -4959,15 +5097,15 @@ Identify the specific design moves that make this page interesting and unique, d
4959
5097
 
4960
5098
  Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.
4961
5099
  `;
4962
- var definition3 = {
5100
+ var definition4 = {
4963
5101
  name: "analyzeDesign",
4964
- description: "Analyze the visual design of a website, an image URL, or an image file on disk. Websites are automatically screenshotted first. Provides static image analysis only, will not capture animations or video. If no prompt is provided, performs a full design reference analysis (mood, color, typography, layout, distinctiveness). Provide a custom prompt to ask a specific design question instead. Use a bulleted list to ask many questions at once.",
5102
+ description: "Analyze the visual design of a website, an image (URL or file on disk), or an HTML document on disk. Websites are screenshotted first; an HTML file is rendered in a real browser first and the render is analyzed, which is how you look at a wireframe you authored. Provides static image analysis only, will not capture animations or video. With no prompt, a website or image gets a full design reference analysis (mood, color, typography, layout, distinctiveness) and a rendered HTML document gets a fidelity review (clipping, overflow, webfont loading, alignment). Provide a custom prompt to ask a specific design question instead. Use a bulleted list to ask many questions at once.",
4965
5103
  inputSchema: {
4966
5104
  type: "object",
4967
5105
  properties: {
4968
5106
  url: {
4969
5107
  type: "string",
4970
- description: "What to analyze: a website URL (will be screenshotted), an image URL, or the path of an image file on disk (e.g. a reference the user uploaded, under src/.user-uploads/). Local files are hosted automatically and their URL comes back in the result."
5108
+ description: "What to analyze: a website URL (will be screenshotted), an image URL, the path of an image file on disk (e.g. a reference the user uploaded, under src/.user-uploads/), or the path of an HTML file on disk (rendered in a browser, then analyzed). Local files are hosted automatically and their URL comes back in the result."
4971
5109
  },
4972
5110
  prompt: {
4973
5111
  type: "string",
@@ -4977,53 +5115,140 @@ var definition3 = {
4977
5115
  required: ["url"]
4978
5116
  }
4979
5117
  };
4980
- async function execute3(input, onLog, context) {
4981
- const url = input.url;
4982
- const analysisPrompt = input.prompt || DESIGN_REFERENCE_PROMPT;
4983
- const isImage = /\.(png|jpe?g|webp|gif|svg|avif)(\?|$)/i.test(url);
4984
- let image = url;
4985
- if (!isImage) {
4986
- const ss = await runMindstudioCliResult(
4987
- [
4988
- "screenshot-url",
4989
- "--url",
4990
- url,
4991
- "--mode",
4992
- "viewport",
4993
- "--width",
4994
- "1440",
4995
- "--delay",
4996
- "2000"
4997
- ],
4998
- {
4999
- outputKey: "screenshotUrl",
5000
- timeout: 12e4,
5001
- onLog,
5002
- caller: "designExpert"
5003
- }
5118
+ var IMAGE_EXT_RE = /\.(png|jpe?g|webp|gif|svg|avif)(\?|$)/i;
5119
+ var HTML_EXTS = /* @__PURE__ */ new Set([".html", ".htm"]);
5120
+ var HTML_RENDER_WIDTH = 1280;
5121
+ var HTML_RENDER_START_HEIGHT = 1400;
5122
+ function splitFrontmatter(raw) {
5123
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n?/);
5124
+ if (!match) {
5125
+ return { html: raw };
5126
+ }
5127
+ const fields = {};
5128
+ for (const line of match[1].split("\n")) {
5129
+ const idx = line.indexOf(":");
5130
+ if (idx > 0) {
5131
+ fields[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
5132
+ }
5133
+ }
5134
+ return {
5135
+ name: fields.name,
5136
+ description: fields.description,
5137
+ html: raw.slice(match[0].length)
5138
+ };
5139
+ }
5140
+ async function execute4(input, onLog, context) {
5141
+ const url = String(input.url ?? "").trim();
5142
+ if (!url) {
5143
+ return "Error: url is required.";
5144
+ }
5145
+ const customPrompt = typeof input.prompt === "string" && input.prompt.trim() ? input.prompt : void 0;
5146
+ if (!isFetchableUrl(url)) {
5147
+ if (HTML_EXTS.has(extname2(url).toLowerCase())) {
5148
+ return analyzeLocalHtml(url, customPrompt, onLog, context);
5149
+ }
5150
+ return analyze(
5151
+ url,
5152
+ customPrompt ?? DESIGN_REFERENCE_PROMPT,
5153
+ onLog,
5154
+ context
5004
5155
  );
5005
- if (!ss.ok) {
5006
- return `Could not screenshot ${url}: ${ss.value}`;
5156
+ }
5157
+ if (IMAGE_EXT_RE.test(url)) {
5158
+ return analyze(
5159
+ url,
5160
+ customPrompt ?? DESIGN_REFERENCE_PROMPT,
5161
+ onLog,
5162
+ context
5163
+ );
5164
+ }
5165
+ const ss = await runMindstudioCliResult(
5166
+ [
5167
+ "screenshot-url",
5168
+ "--url",
5169
+ url,
5170
+ "--mode",
5171
+ "viewport",
5172
+ "--width",
5173
+ "1440",
5174
+ "--delay",
5175
+ "2000"
5176
+ ],
5177
+ {
5178
+ outputKey: "screenshotUrl",
5179
+ timeout: 12e4,
5180
+ onLog,
5181
+ caller: "designExpert"
5007
5182
  }
5008
- image = ss.value;
5183
+ );
5184
+ if (!ss.ok) {
5185
+ return `Could not screenshot ${url}: ${ss.value}`;
5009
5186
  }
5187
+ return analyze(
5188
+ ss.value,
5189
+ customPrompt ?? DESIGN_REFERENCE_PROMPT,
5190
+ onLog,
5191
+ context
5192
+ );
5193
+ }
5194
+ async function analyze(image, prompt, onLog, context, extra) {
5010
5195
  const analyzed = await analyzeImage({
5011
- prompt: analysisPrompt,
5196
+ prompt,
5012
5197
  image,
5013
5198
  apiConfig: context?.apiConfig,
5014
5199
  onLog,
5015
5200
  model: resolveModel("imageAnalysis", context?.models, context?.model)
5016
5201
  });
5017
- return JSON.stringify({ url: analyzed.url, analysis: analyzed.analysis });
5202
+ return JSON.stringify({
5203
+ url: analyzed.url,
5204
+ analysis: analyzed.analysis,
5205
+ ...extra
5206
+ });
5207
+ }
5208
+ async function analyzeLocalHtml(path13, customPrompt, onLog, context) {
5209
+ let raw;
5210
+ try {
5211
+ raw = await readFile2(join2(PROJECT_ROOT, path13), "utf8");
5212
+ } catch {
5213
+ return `No file at "${path13}". Paths resolve from the project root; list the directory to check the name.`;
5214
+ }
5215
+ const { name, description, html } = splitFrontmatter(raw);
5216
+ if (!html.trim()) {
5217
+ return `"${path13}" has no HTML to render.`;
5218
+ }
5219
+ onLog?.(`Rendering ${path13} in the sandbox browser...`);
5220
+ const release = await acquireBrowserLock();
5221
+ let rendered;
5222
+ try {
5223
+ rendered = await renderHtmlViaSidecar({
5224
+ html,
5225
+ width: HTML_RENDER_WIDTH,
5226
+ height: HTML_RENDER_START_HEIGHT,
5227
+ autoHeight: true
5228
+ });
5229
+ } catch (err) {
5230
+ return `Could not render ${path13}: ${err?.message ?? err}`;
5231
+ } finally {
5232
+ release();
5233
+ }
5234
+ const basePrompt = customPrompt ?? RENDER_ANALYZE_PROMPT;
5235
+ const subject = [name, description].filter(Boolean).join(" \u2014 ");
5236
+ const prompt = subject ? `The document rendered below is described by its author as: ${subject}
5237
+
5238
+ ${basePrompt}` : basePrompt;
5239
+ return analyze(rendered.url, prompt, onLog, context, {
5240
+ renderedWidth: rendered.width,
5241
+ renderedHeight: rendered.height
5242
+ });
5018
5243
  }
5019
5244
 
5020
5245
  // src/subagents/designExpert/tools/analyzeImage.ts
5021
5246
  var analyzeImage_exports = {};
5022
5247
  __export(analyzeImage_exports, {
5023
- definition: () => definition4,
5024
- execute: () => execute4
5248
+ definition: () => definition5,
5249
+ execute: () => execute5
5025
5250
  });
5026
- var definition4 = {
5251
+ var definition5 = {
5027
5252
  name: "analyzeImage",
5028
5253
  description: "Analyze an image using a vision model. Provides static image analysis only, will not capture animations or video. Returns an objective description of what is visible \u2014 shapes, colors, layout, text, artifacts. Use for factual inventory of image contents, not for subjective design judgment - the vision model providing the analysis has no sense of design. You are the design expert - use the analysis tool for factual inventory, then apply your own expertise for quality and suitability assessments. Optionally provide specific questions about what you're looking for. Use a bulleted list to ask many questions at once. If you are analyzing a screenshot of the app preview, you can reuse the same screenshot URL multiple times to ask multiple questions.",
5029
5254
  inputSchema: {
@@ -5044,7 +5269,7 @@ var definition4 = {
5044
5269
  required: ["imageUrl"]
5045
5270
  }
5046
5271
  };
5047
- async function execute4(input, onLog, context) {
5272
+ async function execute5(input, onLog, context) {
5048
5273
  const prompt = buildScreenshotAnalysisPrompt({
5049
5274
  prompt: input.prompt
5050
5275
  });
@@ -5061,8 +5286,8 @@ async function execute4(input, onLog, context) {
5061
5286
  // src/subagents/designExpert/tools/images/generateImages.ts
5062
5287
  var generateImages_exports = {};
5063
5288
  __export(generateImages_exports, {
5064
- definition: () => definition5,
5065
- execute: () => execute5
5289
+ definition: () => definition6,
5290
+ execute: () => execute6
5066
5291
  });
5067
5292
 
5068
5293
  // src/subagents/designExpert/tools/images/enhancePrompt.ts
@@ -5289,7 +5514,7 @@ async function generateImageAssets(opts) {
5289
5514
  }
5290
5515
 
5291
5516
  // src/subagents/designExpert/tools/images/generateImages.ts
5292
- var definition5 = {
5517
+ var definition6 = {
5293
5518
  name: "generateImages",
5294
5519
  description: "Generate images. Returns CDN URLs with a quality analysis for each image. Produces high-quality results for everything from photorealistic images and abstract/creative visuals. Pass multiple prompts to generate in parallel. No need to analyze images separately after generating \u2014 the analysis is included.",
5295
5520
  inputSchema: {
@@ -5322,7 +5547,7 @@ var definition5 = {
5322
5547
  required: ["prompts"]
5323
5548
  }
5324
5549
  };
5325
- async function execute5(input, onLog, context) {
5550
+ async function execute6(input, onLog, context) {
5326
5551
  return generateImageAssets({
5327
5552
  prompts: input.prompts,
5328
5553
  width: input.width,
@@ -5353,10 +5578,10 @@ async function execute5(input, onLog, context) {
5353
5578
  // src/subagents/designExpert/tools/images/editImages.ts
5354
5579
  var editImages_exports = {};
5355
5580
  __export(editImages_exports, {
5356
- definition: () => definition6,
5357
- execute: () => execute6
5581
+ definition: () => definition7,
5582
+ execute: () => execute7
5358
5583
  });
5359
- var definition6 = {
5584
+ var definition7 = {
5360
5585
  name: "editImages",
5361
5586
  description: "Edit or transform existing images. Provide one or more source image URLs as reference and a prompt describing the desired edit. Use for compositing, style transfer, subject transformation, blending multiple references, or incorporating one or more references into something new. Returns CDN URLs with analysis.",
5362
5587
  inputSchema: {
@@ -5392,7 +5617,7 @@ var definition6 = {
5392
5617
  required: ["prompts", "sourceImages"]
5393
5618
  }
5394
5619
  };
5395
- async function execute6(input, onLog, context) {
5620
+ async function execute7(input, onLog, context) {
5396
5621
  return generateImageAssets({
5397
5622
  prompts: input.prompts,
5398
5623
  sourceImages: input.sourceImages,
@@ -5420,138 +5645,6 @@ async function execute6(input, onLog, context) {
5420
5645
  });
5421
5646
  }
5422
5647
 
5423
- // src/subagents/designExpert/tools/images/renderImage.ts
5424
- var renderImage_exports = {};
5425
- __export(renderImage_exports, {
5426
- definition: () => definition7,
5427
- execute: () => execute7
5428
- });
5429
- import { mkdir, unlink, writeFile } from "fs/promises";
5430
- import { tmpdir } from "os";
5431
- import { dirname, join, resolve as resolve2, sep } from "path";
5432
- import { randomUUID } from "crypto";
5433
- var MIN_DIMENSION = 16;
5434
- var MAX_DIMENSION = 4096;
5435
- 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.';
5436
- var definition7 = {
5437
- name: "renderImage",
5438
- 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.",
5439
- inputSchema: {
5440
- type: "object",
5441
- properties: {
5442
- html: {
5443
- type: "string",
5444
- 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."
5445
- },
5446
- width: {
5447
- type: "number",
5448
- description: "Viewport width in CSS pixels (e.g. 1200 for an OG card, 512 for an icon tile). Range: 16-4096."
5449
- },
5450
- height: {
5451
- type: "number",
5452
- description: "Viewport height in CSS pixels. Range: 16-4096."
5453
- },
5454
- scale: {
5455
- type: "number",
5456
- 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)."
5457
- },
5458
- transparentBackground: {
5459
- type: "boolean",
5460
- 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."
5461
- },
5462
- savePath: {
5463
- type: "string",
5464
- 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)."
5465
- }
5466
- },
5467
- required: ["html", "width", "height"]
5468
- }
5469
- };
5470
- async function execute7(input, onLog, context) {
5471
- const html = typeof input.html === "string" ? input.html : "";
5472
- const width = Math.round(Number(input.width));
5473
- const height = Math.round(Number(input.height));
5474
- if (!html || !Number.isFinite(width) || !Number.isFinite(height) || width < MIN_DIMENSION || width > MAX_DIMENSION || height < MIN_DIMENSION || height > MAX_DIMENSION) {
5475
- return `Error: renderImage requires an html string plus width/height between ${MIN_DIMENSION} and ${MAX_DIMENSION}.`;
5476
- }
5477
- const release = await acquireBrowserLock();
5478
- let rendered;
5479
- try {
5480
- onLog?.("Rendering document in the sandbox browser...");
5481
- rendered = await renderHtmlViaSidecar({
5482
- html,
5483
- width,
5484
- height,
5485
- transparent: input.transparentBackground === true,
5486
- scale: typeof input.scale === "number" ? input.scale : void 0
5487
- });
5488
- } catch (err) {
5489
- return `Error: render failed: ${err?.message ?? err}`;
5490
- } finally {
5491
- release();
5492
- }
5493
- let bytes;
5494
- try {
5495
- const res = await fetch(rendered.url);
5496
- if (!res.ok) {
5497
- throw new Error(`fetch returned ${res.status}`);
5498
- }
5499
- bytes = Buffer.from(await res.arrayBuffer());
5500
- } catch (err) {
5501
- return `Error: rendered but could not download the capture (${err?.message ?? err}). Temporary URL: ${rendered.url}`;
5502
- }
5503
- let savedPath;
5504
- if (typeof input.savePath === "string" && input.savePath) {
5505
- const absolute = resolve2(PROJECT_ROOT, input.savePath);
5506
- if (!absolute.startsWith(PROJECT_ROOT + sep)) {
5507
- return "Error: savePath must resolve inside the project.";
5508
- }
5509
- await mkdir(dirname(absolute), { recursive: true });
5510
- await writeFile(absolute, bytes);
5511
- savedPath = input.savePath;
5512
- }
5513
- onLog?.("Hosting the capture...");
5514
- const tmpPath = join(tmpdir(), `render-${randomUUID()}.png`);
5515
- let url = rendered.url;
5516
- let temporary = true;
5517
- try {
5518
- await writeFile(tmpPath, bytes);
5519
- const upload = await runMindstudioCliResult(["upload", tmpPath], {
5520
- timeout: 6e4,
5521
- onLog,
5522
- caller: "designExpert"
5523
- });
5524
- const match = upload.ok ? upload.value.match(/https:\/\/\S+/g) : null;
5525
- if (match?.length) {
5526
- url = match[match.length - 1];
5527
- temporary = false;
5528
- }
5529
- } finally {
5530
- await unlink(tmpPath).catch(() => {
5531
- });
5532
- }
5533
- const analysis = await analyzeImage({
5534
- prompt: RENDER_ANALYZE_PROMPT,
5535
- image: url,
5536
- onLog,
5537
- model: resolveModel("imageAnalysis", context?.models, context?.model)
5538
- }).then((r) => r.analysis).catch((err) => `Could not review this image: ${err.message}`);
5539
- return JSON.stringify({
5540
- images: [
5541
- {
5542
- url,
5543
- ...temporary ? {
5544
- 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."
5545
- } : {},
5546
- ...savedPath ? { savedPath } : {},
5547
- analysis,
5548
- width: rendered.width,
5549
- height: rendered.height
5550
- }
5551
- ]
5552
- });
5553
- }
5554
-
5555
5648
  // src/subagents/designExpert/tools/polishCopy.ts
5556
5649
  var polishCopy_exports = {};
5557
5650
  __export(polishCopy_exports, {
@@ -5697,7 +5790,7 @@ __export(createWireframe_exports, {
5697
5790
  execute: () => execute10
5698
5791
  });
5699
5792
  import { mkdir as mkdir2, stat as stat2, writeFile as writeFile2 } from "fs/promises";
5700
- import { join as join2 } from "path";
5793
+ import { join as join3 } from "path";
5701
5794
  var log9 = createLogger("createWireframe");
5702
5795
  var WIREFRAMES_DIR = "src/.wireframes";
5703
5796
  var UPLOAD_TIMEOUT_MS2 = 3e4;
@@ -5816,12 +5909,12 @@ async function execute10(input, onLog, context) {
5816
5909
  html.trim(),
5817
5910
  ""
5818
5911
  ].join("\n");
5819
- await mkdir2(join2(PROJECT_ROOT, WIREFRAMES_DIR), { recursive: true });
5820
- const existed = await stat2(join2(PROJECT_ROOT, relPath)).then(
5912
+ await mkdir2(join3(PROJECT_ROOT, WIREFRAMES_DIR), { recursive: true });
5913
+ const existed = await stat2(join3(PROJECT_ROOT, relPath)).then(
5821
5914
  () => true,
5822
5915
  () => false
5823
5916
  );
5824
- await writeFile2(join2(PROJECT_ROOT, relPath), content, "utf8");
5917
+ await writeFile2(join3(PROJECT_ROOT, relPath), content, "utf8");
5825
5918
  onLog?.(`Wrote ${relPath}, mirroring for preview...`);
5826
5919
  const mirror = await uploadMirror(context, slug, content);
5827
5920
  if (!mirror.ok) {
@@ -6092,7 +6185,7 @@ ${RENDER_TASK_BLOCK}`;
6092
6185
 
6093
6186
  // src/subagents/designExpert/validateWireframeRefs.ts
6094
6187
  import { stat as stat3 } from "fs/promises";
6095
- import { join as join3 } from "path";
6188
+ import { join as join4 } from "path";
6096
6189
  var WIREFRAME_REF_RE = /src\/\.wireframes\/[a-z0-9][a-z0-9-]*\.html/g;
6097
6190
  async function validateWireframeRefs(text) {
6098
6191
  const refs = [...new Set(text.match(WIREFRAME_REF_RE) ?? [])];
@@ -6101,7 +6194,7 @@ async function validateWireframeRefs(text) {
6101
6194
  }
6102
6195
  const missing = [];
6103
6196
  for (const ref of refs) {
6104
- const exists = await stat3(join3(PROJECT_ROOT, ref)).then(
6197
+ const exists = await stat3(join4(PROJECT_ROOT, ref)).then(
6105
6198
  (s) => s.isFile(),
6106
6199
  () => false
6107
6200
  );
@@ -8444,6 +8537,155 @@ function friendlyError(raw) {
8444
8537
  return `Something went wrong: ${raw}`;
8445
8538
  }
8446
8539
 
8540
+ // src/suggestions.ts
8541
+ var SUGGEST_MARKER = "](suggest:";
8542
+ var SEPARATORS = "\\s\xB7|,\\-\u2013\u2014";
8543
+ var SEPARATORS_ONLY = new RegExp(`^[${SEPARATORS}]*$`);
8544
+ var TRAILING_SEPARATORS = new RegExp(`[${SEPARATORS}]+$`);
8545
+ var LIST_MARKER = /^\s*(?:[-*+]|\d+[.)])\s+/;
8546
+ var FENCE = /^ {0,3}(?:```|~~~)/;
8547
+ function findLinks(line) {
8548
+ const found = [];
8549
+ let i = 0;
8550
+ while (i < line.length) {
8551
+ const ch = line[i];
8552
+ if (ch === "\\") {
8553
+ i += 2;
8554
+ continue;
8555
+ }
8556
+ if (ch === "`") {
8557
+ let runEnd = i;
8558
+ while (runEnd < line.length && line[runEnd] === "`") {
8559
+ runEnd++;
8560
+ }
8561
+ const fence = line.slice(i, runEnd);
8562
+ const close = line.indexOf(fence, runEnd);
8563
+ if (close === -1) {
8564
+ break;
8565
+ }
8566
+ i = close + fence.length;
8567
+ continue;
8568
+ }
8569
+ if (ch !== "[") {
8570
+ i++;
8571
+ continue;
8572
+ }
8573
+ const labelEnd = line.indexOf("]", i + 1);
8574
+ if (labelEnd === -1) {
8575
+ break;
8576
+ }
8577
+ if (!line.startsWith(SUGGEST_MARKER, labelEnd)) {
8578
+ i = labelEnd + 1;
8579
+ continue;
8580
+ }
8581
+ let depth = 1;
8582
+ let p = labelEnd + SUGGEST_MARKER.length;
8583
+ while (p < line.length && depth > 0) {
8584
+ if (line[p] === "(") {
8585
+ depth++;
8586
+ } else if (line[p] === ")") {
8587
+ depth--;
8588
+ }
8589
+ p++;
8590
+ }
8591
+ if (depth > 0) {
8592
+ break;
8593
+ }
8594
+ found.push({
8595
+ start: i,
8596
+ end: p,
8597
+ label: line.slice(i + 1, labelEnd).trim(),
8598
+ message: line.slice(labelEnd + SUGGEST_MARKER.length, p - 1).trim(),
8599
+ standalone: false
8600
+ });
8601
+ i = p;
8602
+ }
8603
+ return found;
8604
+ }
8605
+ function classify(line, matches) {
8606
+ let tailClean = SEPARATORS_ONLY.test(
8607
+ line.slice(matches[matches.length - 1].end)
8608
+ );
8609
+ for (let i = matches.length - 1; i >= 0; i--) {
8610
+ matches[i].standalone = tailClean;
8611
+ if (!tailClean) {
8612
+ continue;
8613
+ }
8614
+ const gap = i > 0 ? line.slice(matches[i - 1].end, matches[i].start) : "";
8615
+ tailClean = SEPARATORS_ONLY.test(gap);
8616
+ }
8617
+ }
8618
+ function parseSuggestions(raw) {
8619
+ if (!raw.includes(SUGGEST_MARKER)) {
8620
+ return { text: raw, suggestions: [] };
8621
+ }
8622
+ const suggestions = [];
8623
+ const seen = /* @__PURE__ */ new Set();
8624
+ const outLines = [];
8625
+ let inFence = false;
8626
+ let droppedLine = false;
8627
+ for (const line of raw.split("\n")) {
8628
+ if (FENCE.test(line)) {
8629
+ inFence = !inFence;
8630
+ outLines.push(line);
8631
+ continue;
8632
+ }
8633
+ if (inFence) {
8634
+ outLines.push(line);
8635
+ continue;
8636
+ }
8637
+ const matches = findLinks(line);
8638
+ if (matches.length === 0) {
8639
+ outLines.push(line);
8640
+ continue;
8641
+ }
8642
+ classify(line, matches);
8643
+ for (const m of matches) {
8644
+ const key = `${m.label}::${m.message}`;
8645
+ if (!seen.has(key)) {
8646
+ seen.add(key);
8647
+ suggestions.push({ label: m.label, message: m.message });
8648
+ }
8649
+ }
8650
+ let rebuilt = "";
8651
+ let cursor = 0;
8652
+ for (const m of matches) {
8653
+ rebuilt += line.slice(cursor, m.start);
8654
+ if (!m.standalone) {
8655
+ rebuilt += m.label;
8656
+ }
8657
+ cursor = m.end;
8658
+ }
8659
+ rebuilt += line.slice(cursor);
8660
+ if (matches.some((m) => m.standalone)) {
8661
+ if (SEPARATORS_ONLY.test(rebuilt.replace(LIST_MARKER, ""))) {
8662
+ droppedLine = true;
8663
+ continue;
8664
+ }
8665
+ rebuilt = rebuilt.replace(TRAILING_SEPARATORS, "");
8666
+ }
8667
+ outLines.push(rebuilt);
8668
+ }
8669
+ let text = outLines.join("\n").replace(/\s+$/, "");
8670
+ if (droppedLine) {
8671
+ text = text.replace(/\n{3,}/g, "\n\n");
8672
+ }
8673
+ return { text, suggestions };
8674
+ }
8675
+ function annotateSuggestions(blocks) {
8676
+ for (const block of blocks) {
8677
+ if (block.type !== "text" || !block.text.includes(SUGGEST_MARKER)) {
8678
+ continue;
8679
+ }
8680
+ const { text, suggestions } = parseSuggestions(block.text);
8681
+ if (suggestions.length === 0) {
8682
+ continue;
8683
+ }
8684
+ block.displayText = text;
8685
+ block.suggestions = suggestions;
8686
+ }
8687
+ }
8688
+
8447
8689
  // src/agent.ts
8448
8690
  var log15 = createLogger("agent");
8449
8691
  var BRAND_TRIGGERING_TOOLS = /* @__PURE__ */ new Set([
@@ -8638,8 +8880,27 @@ async function runTurn(params) {
8638
8880
  toolInputAccumulators.set(id, acc);
8639
8881
  }
8640
8882
  return acc;
8883
+ }, emitTextBlockSnapshot2 = function(closing) {
8884
+ let block;
8885
+ for (let i = contentBlocks.length - 1; i >= 0; i--) {
8886
+ const candidate = contentBlocks[i];
8887
+ if (candidate.type === "text") {
8888
+ block = candidate;
8889
+ break;
8890
+ }
8891
+ }
8892
+ if (!block || !block.text.includes(SUGGEST_MARKER)) {
8893
+ return;
8894
+ }
8895
+ const { text, suggestions } = parseSuggestions(block.text);
8896
+ const worthSending = closing ? suggestions.length > 0 : suggestions.length > emittedSuggestionCount;
8897
+ if (!worthSending) {
8898
+ return;
8899
+ }
8900
+ emittedSuggestionCount = suggestions.length;
8901
+ onEvent({ type: "text_block", text, suggestions });
8641
8902
  };
8642
- var getOrCreateAccumulator = getOrCreateAccumulator2;
8903
+ var getOrCreateAccumulator = getOrCreateAccumulator2, emitTextBlockSnapshot = emitTextBlockSnapshot2;
8643
8904
  if (signal?.aborted) {
8644
8905
  onEvent({ type: "turn_cancelled" });
8645
8906
  saveSession(state);
@@ -8651,6 +8912,7 @@ async function runTurn(params) {
8651
8912
  let thinkingCompleteCount = 0;
8652
8913
  let lastThinkingRelatedStartedAt;
8653
8914
  let textBlockOpen = false;
8915
+ let emittedSuggestionCount = 0;
8654
8916
  const toolInputAccumulators = /* @__PURE__ */ new Map();
8655
8917
  let stopReason = "end_turn";
8656
8918
  let turnProviderMetadata;
@@ -8739,14 +9001,19 @@ async function runTurn(params) {
8739
9001
  text: event.text,
8740
9002
  startedAt: event.ts
8741
9003
  });
9004
+ emittedSuggestionCount = 0;
8742
9005
  }
8743
9006
  textBlockOpen = true;
8744
9007
  onEvent({ type: "text", text: event.text });
9008
+ emitTextBlockSnapshot2(false);
8745
9009
  break;
8746
9010
  }
8747
9011
  case "thinking":
8748
9012
  if (event.text === "") {
8749
9013
  thinkingBlockStartTimes.push(event.ts);
9014
+ if (textBlockOpen) {
9015
+ emitTextBlockSnapshot2(true);
9016
+ }
8750
9017
  textBlockOpen = false;
8751
9018
  }
8752
9019
  onEvent({ type: "thinking", text: event.text });
@@ -8872,6 +9139,11 @@ async function runTurn(params) {
8872
9139
  throw err;
8873
9140
  }
8874
9141
  }
9142
+ if (textBlockOpen) {
9143
+ emitTextBlockSnapshot2(true);
9144
+ textBlockOpen = false;
9145
+ }
9146
+ annotateSuggestions(contentBlocks);
8875
9147
  if (signal?.aborted) {
8876
9148
  statusWatcher.stop();
8877
9149
  if (contentBlocks.length > 0) {
@@ -9145,7 +9417,7 @@ async function runTurn(params) {
9145
9417
  // src/headless/attachments.ts
9146
9418
  import { mkdirSync, existsSync } from "fs";
9147
9419
  import { writeFile as writeFile3 } from "fs/promises";
9148
- import { basename as basename2, join as join4, extname as extname2 } from "path";
9420
+ import { basename as basename2, join as join5, extname as extname3 } from "path";
9149
9421
  var log16 = createLogger("headless:attachments");
9150
9422
  var UPLOADS_DIR = "src/.user-uploads";
9151
9423
  function filenameFromUrl(url) {
@@ -9158,11 +9430,11 @@ function filenameFromUrl(url) {
9158
9430
  }
9159
9431
  }
9160
9432
  function resolveUniqueFilename(name, claimed) {
9161
- const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join4(UPLOADS_DIR, candidate));
9433
+ const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join5(UPLOADS_DIR, candidate));
9162
9434
  if (isFree(name)) {
9163
9435
  return name;
9164
9436
  }
9165
- const ext = extname2(name);
9437
+ const ext = extname3(name);
9166
9438
  const base = name.slice(0, name.length - ext.length);
9167
9439
  let counter = 1;
9168
9440
  while (!isFree(`${base}-${counter}${ext}`)) {
@@ -9173,7 +9445,7 @@ function resolveUniqueFilename(name, claimed) {
9173
9445
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
9174
9446
  function isImageAttachment(att) {
9175
9447
  const name = att.filename || filenameFromUrl(att.url);
9176
- return IMAGE_EXTENSIONS.has(extname2(name).toLowerCase());
9448
+ return IMAGE_EXTENSIONS.has(extname3(name).toLowerCase());
9177
9449
  }
9178
9450
  async function persistAttachments(attachments) {
9179
9451
  const nonVoice = attachments.filter((a) => !a.isVoice);
@@ -9193,7 +9465,7 @@ async function persistAttachments(attachments) {
9193
9465
  const results = await Promise.allSettled(
9194
9466
  nonVoice.map(async (att, i) => {
9195
9467
  const name = names[i];
9196
- const localPath = join4(UPLOADS_DIR, name);
9468
+ const localPath = join5(UPLOADS_DIR, name);
9197
9469
  const res = await fetch(att.url, {
9198
9470
  signal: AbortSignal.timeout(3e4)
9199
9471
  });
@@ -10002,6 +10274,13 @@ var HeadlessSession = class {
10002
10274
  rid
10003
10275
  );
10004
10276
  return;
10277
+ case "text_block":
10278
+ this.emit(
10279
+ "text_block",
10280
+ { text: e.text, suggestions: e.suggestions },
10281
+ rid
10282
+ );
10283
+ return;
10005
10284
  case "thinking":
10006
10285
  this.emit(
10007
10286
  "thinking",