@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 +232 -46
- package/dist/index.js +247 -48
- package/dist/prompt/static/team.md +1 -1
- package/dist/subagents/designExpert/prompts/images.md +11 -1
- package/dist/subagents/designExpert/prompts/instructions.md +1 -0
- package/dist/subagents/designExpert/tools/images/enhance-image-prompt.md +7 -3
- package/package.json +1 -1
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((
|
|
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: "
|
|
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
|
|
696
|
-
if (
|
|
697
|
-
fields[line.slice(0,
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
2417
|
+
resolve4(`Error: process exited with code ${code}`);
|
|
2418
2418
|
} else {
|
|
2419
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2451
|
+
resolve4(output);
|
|
2452
2452
|
}
|
|
2453
2453
|
});
|
|
2454
2454
|
child.on("error", (err) => {
|
|
2455
2455
|
clearTimeout(timer);
|
|
2456
|
-
|
|
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((
|
|
2573
|
+
return new Promise((resolve4) => {
|
|
2574
2574
|
exec(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
|
|
2575
2575
|
if (stdout?.trim()) {
|
|
2576
|
-
|
|
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
|
-
|
|
2581
|
+
resolve4(formatResults(grepStdout, max, mode));
|
|
2582
2582
|
} else {
|
|
2583
|
-
|
|
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((
|
|
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
|
|
4266
|
-
if (
|
|
4267
|
-
const key = line.slice(0,
|
|
4268
|
-
const val = line.slice(
|
|
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
|
|
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
|
|
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 = {
|
|
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: () =>
|
|
5196
|
-
execute: () =>
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 ?
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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((
|
|
9321
|
+
return new Promise((resolve4) => {
|
|
9136
9322
|
const timeout = shouldTimeout ? setTimeout(() => {
|
|
9137
9323
|
this.pendingTools.delete(id);
|
|
9138
|
-
|
|
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
|
-
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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: "
|
|
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
|
|
3029
|
-
if (
|
|
3030
|
-
fields[line.slice(0,
|
|
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((
|
|
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
|
-
|
|
3510
|
+
resolve4(`Error: process exited with code ${code}`);
|
|
3511
3511
|
} else {
|
|
3512
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3544
|
+
resolve4(output);
|
|
3545
3545
|
}
|
|
3546
3546
|
});
|
|
3547
3547
|
child.on("error", (err) => {
|
|
3548
3548
|
clearTimeout(timer);
|
|
3549
|
-
|
|
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((
|
|
3672
|
+
return new Promise((resolve4) => {
|
|
3673
3673
|
exec(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
|
|
3674
3674
|
if (stdout?.trim()) {
|
|
3675
|
-
|
|
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
|
-
|
|
3680
|
+
resolve4(formatResults(grepStdout, max, mode));
|
|
3681
3681
|
} else {
|
|
3682
|
-
|
|
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((
|
|
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
|
|
5284
|
-
if (
|
|
5285
|
-
const key = line.slice(0,
|
|
5286
|
-
const val = line.slice(
|
|
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
|
|
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
|
|
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 = {
|
|
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: () =>
|
|
6392
|
-
execute: () =>
|
|
6588
|
+
definition: () => definition8,
|
|
6589
|
+
execute: () => execute8
|
|
6393
6590
|
});
|
|
6394
|
-
async function
|
|
6591
|
+
async function execute8(input, _onLog, context) {
|
|
6395
6592
|
return copyEditorTool.execute(input, context);
|
|
6396
6593
|
}
|
|
6397
|
-
var
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 ?
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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((
|
|
10266
|
+
return new Promise((resolve4) => {
|
|
10068
10267
|
const timeout = shouldTimeout ? setTimeout(() => {
|
|
10069
10268
|
this.pendingTools.delete(id);
|
|
10070
|
-
|
|
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
|
-
|
|
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
|
|
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;
|
|
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
|
-
|
|
65
|
-
|
|
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
|