@mindstudio-ai/remy 0.1.289 → 0.1.291
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 +98 -40
- package/dist/index.js +98 -40
- package/dist/prompt/compiled/interfaces.md +2 -0
- package/dist/prompt/compiled/jewels.md +1 -1
- package/dist/prompt/compiled/tables.md +44 -3
- package/dist/prompt/skills/jewels.md +28 -12
- package/dist/prompt/static/coding.md +1 -1
- package/dist/subagents/designExpert/prompts/instructions.md +1 -1
- package/dist/subagents/designExpert/prompts/ui-patterns.md +1 -1
- package/package.json +1 -1
package/dist/headless.js
CHANGED
|
@@ -2496,7 +2496,7 @@ var bashTool = {
|
|
|
2496
2496
|
};
|
|
2497
2497
|
|
|
2498
2498
|
// src/tools/code/grep.ts
|
|
2499
|
-
import {
|
|
2499
|
+
import { execFile } from "child_process";
|
|
2500
2500
|
var DEFAULT_MAX = 50;
|
|
2501
2501
|
function clampContext(v) {
|
|
2502
2502
|
const n = Math.floor(Number(v));
|
|
@@ -2570,57 +2570,87 @@ var grepTool = {
|
|
|
2570
2570
|
1,
|
|
2571
2571
|
Math.floor(Number(input.maxResults) || DEFAULT_MAX)
|
|
2572
2572
|
);
|
|
2573
|
-
const globFlag = input.glob ? ` --glob '${input.glob}'` : "";
|
|
2574
|
-
const escaped = input.pattern.replace(/'/g, "'\\''");
|
|
2575
2573
|
const mode = input.outputMode === "count" || input.outputMode === "filesWithMatches" ? input.outputMode : "content";
|
|
2576
|
-
const
|
|
2577
|
-
let ctx = "";
|
|
2574
|
+
const ctx = [];
|
|
2578
2575
|
if (mode === "content") {
|
|
2579
2576
|
if (input.context != null) {
|
|
2580
2577
|
const c = clampContext(input.context);
|
|
2581
2578
|
if (c > 0) {
|
|
2582
|
-
ctx
|
|
2579
|
+
ctx.push("-C", String(c));
|
|
2583
2580
|
}
|
|
2584
2581
|
} else {
|
|
2585
2582
|
const b = input.contextBefore != null ? clampContext(input.contextBefore) : 0;
|
|
2586
2583
|
const a = input.contextAfter != null ? clampContext(input.contextAfter) : 0;
|
|
2587
2584
|
if (b > 0) {
|
|
2588
|
-
ctx
|
|
2585
|
+
ctx.push("-B", String(b));
|
|
2589
2586
|
}
|
|
2590
2587
|
if (a > 0) {
|
|
2591
|
-
ctx
|
|
2588
|
+
ctx.push("-A", String(a));
|
|
2592
2589
|
}
|
|
2593
2590
|
}
|
|
2594
2591
|
}
|
|
2592
|
+
const ci = input.caseInsensitive ? ["-i"] : [];
|
|
2595
2593
|
let rgFlags;
|
|
2596
2594
|
let grepFlags;
|
|
2597
2595
|
if (mode === "count") {
|
|
2598
|
-
rgFlags =
|
|
2599
|
-
grepFlags =
|
|
2596
|
+
rgFlags = ["--count", ...ci];
|
|
2597
|
+
grepFlags = ["-rc", ...ci];
|
|
2600
2598
|
} else if (mode === "filesWithMatches") {
|
|
2601
|
-
rgFlags =
|
|
2602
|
-
grepFlags =
|
|
2599
|
+
rgFlags = ["-l", ...ci];
|
|
2600
|
+
grepFlags = ["-rl", ...ci];
|
|
2603
2601
|
} else {
|
|
2604
|
-
rgFlags =
|
|
2605
|
-
grepFlags =
|
|
2606
|
-
}
|
|
2607
|
-
const
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2602
|
+
rgFlags = ["-n", "--no-heading", ...ci, ...ctx, `--max-count=${max}`];
|
|
2603
|
+
grepFlags = ["-rn", ...ci, ...ctx, `--max-count=${max}`];
|
|
2604
|
+
}
|
|
2605
|
+
const rgArgs = [
|
|
2606
|
+
...rgFlags,
|
|
2607
|
+
...input.glob ? ["--glob", input.glob] : [],
|
|
2608
|
+
"--",
|
|
2609
|
+
input.pattern,
|
|
2610
|
+
searchPath
|
|
2611
|
+
];
|
|
2612
|
+
const grepArgs = [
|
|
2613
|
+
...grepFlags,
|
|
2614
|
+
"--exclude-dir=node_modules",
|
|
2615
|
+
"--exclude-dir=.git",
|
|
2616
|
+
"--include=*.ts",
|
|
2617
|
+
"--include=*.tsx",
|
|
2618
|
+
"--include=*.js",
|
|
2619
|
+
"--include=*.json",
|
|
2620
|
+
"--include=*.md",
|
|
2621
|
+
"--",
|
|
2622
|
+
input.pattern,
|
|
2623
|
+
searchPath
|
|
2624
|
+
];
|
|
2625
|
+
const run = (cmd, cmdArgs) => new Promise((resolve4) => {
|
|
2626
|
+
const child = execFile(
|
|
2627
|
+
cmd,
|
|
2628
|
+
cmdArgs,
|
|
2629
|
+
{ maxBuffer: 512 * 1024, timeout: 3e4 },
|
|
2630
|
+
(err, stdout) => {
|
|
2631
|
+
resolve4({
|
|
2632
|
+
stdout: stdout ?? "",
|
|
2633
|
+
timedOut: err?.killed === true
|
|
2634
|
+
});
|
|
2614
2635
|
}
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
resolve4(formatResults(grepStdout, max, mode));
|
|
2618
|
-
} else {
|
|
2619
|
-
resolve4("No matches found.");
|
|
2620
|
-
}
|
|
2621
|
-
});
|
|
2622
|
-
});
|
|
2636
|
+
);
|
|
2637
|
+
child.stdin?.end();
|
|
2623
2638
|
});
|
|
2639
|
+
const rg = await run("rg", rgArgs);
|
|
2640
|
+
if (rg.stdout.trim()) {
|
|
2641
|
+
return formatResults(rg.stdout, max, mode);
|
|
2642
|
+
}
|
|
2643
|
+
if (rg.timedOut) {
|
|
2644
|
+
return `Error: search timed out after 30s in ${searchPath} \u2014 narrow the path or pattern.`;
|
|
2645
|
+
}
|
|
2646
|
+
const grep = await run("grep", grepArgs);
|
|
2647
|
+
if (grep.stdout.trim()) {
|
|
2648
|
+
return formatResults(grep.stdout, max, mode);
|
|
2649
|
+
}
|
|
2650
|
+
if (grep.timedOut) {
|
|
2651
|
+
return `Error: search timed out after 30s in ${searchPath} \u2014 narrow the path or pattern.`;
|
|
2652
|
+
}
|
|
2653
|
+
return "No matches found.";
|
|
2624
2654
|
}
|
|
2625
2655
|
};
|
|
2626
2656
|
|
|
@@ -3176,6 +3206,13 @@ var SCREENSHOT_ANALYSIS_PROMPT = `Describe everything visible on screen from top
|
|
|
3176
3206
|
var ANALYSIS_RESPONSE_FORMAT = `Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.`;
|
|
3177
3207
|
function buildScreenshotAnalysisPrompt(opts) {
|
|
3178
3208
|
let p = opts?.prompt || SCREENSHOT_ANALYSIS_PROMPT;
|
|
3209
|
+
if (opts?.additionalQuestions) {
|
|
3210
|
+
p += `
|
|
3211
|
+
|
|
3212
|
+
After the analysis above, also answer the following specific questions about the screenshot:
|
|
3213
|
+
|
|
3214
|
+
${opts.additionalQuestions}`;
|
|
3215
|
+
}
|
|
3179
3216
|
if (opts?.styleMap) {
|
|
3180
3217
|
p += `
|
|
3181
3218
|
|
|
@@ -4583,7 +4620,8 @@ async function runBrowserAutomation(task, context, opts) {
|
|
|
4583
4620
|
step: {
|
|
4584
4621
|
imageUrl: s.result.url,
|
|
4585
4622
|
prompt: buildScreenshotAnalysisPrompt({
|
|
4586
|
-
styleMap: s.result.styleMap
|
|
4623
|
+
styleMap: s.result.styleMap,
|
|
4624
|
+
additionalQuestions: opts?.analysisPrompt
|
|
4587
4625
|
}),
|
|
4588
4626
|
visionModelOverride: visionOverride
|
|
4589
4627
|
}
|
|
@@ -4598,7 +4636,13 @@ async function runBrowserAutomation(task, context, opts) {
|
|
|
4598
4636
|
if (i >= analyses.length) {
|
|
4599
4637
|
return;
|
|
4600
4638
|
}
|
|
4601
|
-
|
|
4639
|
+
const analysis = analyses[i]?.output?.analysis || analyses[i]?.output || "";
|
|
4640
|
+
step.result.analysis = analysis;
|
|
4641
|
+
const kind = step.command === "screenshotFullPage" ? "fullPage" : "viewport";
|
|
4642
|
+
const harvested = lastCapture[kind];
|
|
4643
|
+
if (harvested && typeof analysis === "string" && analysis && harvested.url === step.result.url) {
|
|
4644
|
+
harvested.analysis = analysis;
|
|
4645
|
+
}
|
|
4602
4646
|
});
|
|
4603
4647
|
} catch {
|
|
4604
4648
|
log8.debug("Failed to parse batch analysis result", {
|
|
@@ -4618,7 +4662,7 @@ async function runBrowserAutomation(task, context, opts) {
|
|
|
4618
4662
|
const preferred = opts?.capture === "viewport" ? lastCapture.viewport ?? lastCapture.fullPage : lastCapture.fullPage ?? lastCapture.viewport;
|
|
4619
4663
|
return {
|
|
4620
4664
|
text: result.text,
|
|
4621
|
-
...preferred?.url ? { screenshot:
|
|
4665
|
+
...preferred?.url ? { screenshot: preferred } : {}
|
|
4622
4666
|
};
|
|
4623
4667
|
} finally {
|
|
4624
4668
|
release();
|
|
@@ -4666,7 +4710,7 @@ var screenshotDefinition = {
|
|
|
4666
4710
|
},
|
|
4667
4711
|
prompt: {
|
|
4668
4712
|
type: "string",
|
|
4669
|
-
description: "Optional
|
|
4713
|
+
description: "Optional specific questions about the screenshot, answered alongside the general description of what's visible. If omitted, returns just the general description."
|
|
4670
4714
|
},
|
|
4671
4715
|
imageUrl: {
|
|
4672
4716
|
type: "string",
|
|
@@ -4714,15 +4758,25 @@ async function executeScreenshot(input, onLog, context) {
|
|
|
4714
4758
|
const shotKind = fullPage ? "full-page" : "viewport";
|
|
4715
4759
|
const task = input.path ? `Navigate to "${input.path}", then: ${input.instructions}. After completing these steps, take a ${shotKind} screenshot.` : `${input.instructions}. After completing these steps, take a ${shotKind} screenshot.`;
|
|
4716
4760
|
const result = await runBrowserAutomation(task, context, {
|
|
4717
|
-
capture: fullPage ? "fullPage" : "viewport"
|
|
4761
|
+
capture: fullPage ? "fullPage" : "viewport",
|
|
4762
|
+
analysisPrompt: input.prompt
|
|
4718
4763
|
});
|
|
4719
4764
|
if (!result.screenshot) {
|
|
4720
4765
|
return result.text;
|
|
4721
4766
|
}
|
|
4767
|
+
const { url, styleMap, analysis } = result.screenshot;
|
|
4768
|
+
if (analysis) {
|
|
4769
|
+
onLog?.(JSON.stringify({ url, analysis }));
|
|
4770
|
+
return JSON.stringify({
|
|
4771
|
+
url,
|
|
4772
|
+
analysis,
|
|
4773
|
+
...styleMap ? { styleMap } : {}
|
|
4774
|
+
});
|
|
4775
|
+
}
|
|
4722
4776
|
return await streamScreenshotAnalysis({
|
|
4723
|
-
image:
|
|
4777
|
+
image: url,
|
|
4724
4778
|
prompt: input.prompt,
|
|
4725
|
-
styleMap
|
|
4779
|
+
styleMap,
|
|
4726
4780
|
onLog,
|
|
4727
4781
|
model,
|
|
4728
4782
|
apiConfig: context?.apiConfig
|
|
@@ -5595,7 +5649,7 @@ var WIREFRAMES_DIR = "src/.wireframes";
|
|
|
5595
5649
|
var UPLOAD_TIMEOUT_MS2 = 3e4;
|
|
5596
5650
|
var definition10 = {
|
|
5597
5651
|
name: "createWireframe",
|
|
5598
|
-
description: "
|
|
5652
|
+
description: "Generate a wireframe from self-contained HTML+CSS you author and write it to disk as a design artifact. This is how a wireframe comes to exist \u2014 the way generateImages is how an image comes to exist \u2014 and the developer builds from the file it creates. The result also hands back the reference line that embeds the wireframe in your response and in specs; paste it wherever the wireframe belongs and it renders as a live preview. Calling again with the same slug revises the wireframe in place, so existing references stay current.",
|
|
5599
5653
|
inputSchema: {
|
|
5600
5654
|
type: "object",
|
|
5601
5655
|
properties: {
|
|
@@ -5605,7 +5659,7 @@ var definition10 = {
|
|
|
5605
5659
|
},
|
|
5606
5660
|
slug: {
|
|
5607
5661
|
type: "string",
|
|
5608
|
-
description: 'Filename stem, lowercase kebab-case (e.g. "feed-post-card").
|
|
5662
|
+
description: 'Filename stem, lowercase kebab-case (e.g. "feed-post-card"). Re-use a slug to revise that wireframe in place.'
|
|
5609
5663
|
},
|
|
5610
5664
|
description: {
|
|
5611
5665
|
type: "string",
|
|
@@ -7542,6 +7596,7 @@ function clearSession(state) {
|
|
|
7542
7596
|
log11.warn("Session archive on clear failed", { error: err.message });
|
|
7543
7597
|
}
|
|
7544
7598
|
state.messages = [];
|
|
7599
|
+
state.models = void 0;
|
|
7545
7600
|
try {
|
|
7546
7601
|
if (fs21.existsSync(SESSION_FILE)) {
|
|
7547
7602
|
fs21.unlinkSync(SESSION_FILE);
|
|
@@ -10190,7 +10245,10 @@ var HeadlessSession = class {
|
|
|
10190
10245
|
//////////////////////////////////////////////////////////////////////////////
|
|
10191
10246
|
handleClear() {
|
|
10192
10247
|
clearSession(this.state);
|
|
10193
|
-
return {
|
|
10248
|
+
return {
|
|
10249
|
+
modelSurfaces: getEffectiveModelSurfaces(),
|
|
10250
|
+
allowedModelsByType: ALLOWED_MODELS_BY_TYPE
|
|
10251
|
+
};
|
|
10194
10252
|
}
|
|
10195
10253
|
/** Change per-agent model picks without clearing history. Takes effect on
|
|
10196
10254
|
* the next turn — the model is resolved live, per LLM call, from
|
package/dist/index.js
CHANGED
|
@@ -2825,6 +2825,7 @@ function clearSession(state) {
|
|
|
2825
2825
|
log3.warn("Session archive on clear failed", { error: err.message });
|
|
2826
2826
|
}
|
|
2827
2827
|
state.messages = [];
|
|
2828
|
+
state.models = void 0;
|
|
2828
2829
|
try {
|
|
2829
2830
|
if (fs10.existsSync(SESSION_FILE)) {
|
|
2830
2831
|
fs10.unlinkSync(SESSION_FILE);
|
|
@@ -3593,7 +3594,7 @@ var init_bash = __esm({
|
|
|
3593
3594
|
});
|
|
3594
3595
|
|
|
3595
3596
|
// src/tools/code/grep.ts
|
|
3596
|
-
import {
|
|
3597
|
+
import { execFile } from "child_process";
|
|
3597
3598
|
function clampContext(v) {
|
|
3598
3599
|
const n = Math.floor(Number(v));
|
|
3599
3600
|
return Number.isFinite(n) ? Math.min(100, Math.max(0, n)) : 0;
|
|
@@ -3671,57 +3672,87 @@ var init_grep = __esm({
|
|
|
3671
3672
|
1,
|
|
3672
3673
|
Math.floor(Number(input.maxResults) || DEFAULT_MAX)
|
|
3673
3674
|
);
|
|
3674
|
-
const globFlag = input.glob ? ` --glob '${input.glob}'` : "";
|
|
3675
|
-
const escaped = input.pattern.replace(/'/g, "'\\''");
|
|
3676
3675
|
const mode = input.outputMode === "count" || input.outputMode === "filesWithMatches" ? input.outputMode : "content";
|
|
3677
|
-
const
|
|
3678
|
-
let ctx = "";
|
|
3676
|
+
const ctx = [];
|
|
3679
3677
|
if (mode === "content") {
|
|
3680
3678
|
if (input.context != null) {
|
|
3681
3679
|
const c = clampContext(input.context);
|
|
3682
3680
|
if (c > 0) {
|
|
3683
|
-
ctx
|
|
3681
|
+
ctx.push("-C", String(c));
|
|
3684
3682
|
}
|
|
3685
3683
|
} else {
|
|
3686
3684
|
const b = input.contextBefore != null ? clampContext(input.contextBefore) : 0;
|
|
3687
3685
|
const a = input.contextAfter != null ? clampContext(input.contextAfter) : 0;
|
|
3688
3686
|
if (b > 0) {
|
|
3689
|
-
ctx
|
|
3687
|
+
ctx.push("-B", String(b));
|
|
3690
3688
|
}
|
|
3691
3689
|
if (a > 0) {
|
|
3692
|
-
ctx
|
|
3690
|
+
ctx.push("-A", String(a));
|
|
3693
3691
|
}
|
|
3694
3692
|
}
|
|
3695
3693
|
}
|
|
3694
|
+
const ci = input.caseInsensitive ? ["-i"] : [];
|
|
3696
3695
|
let rgFlags;
|
|
3697
3696
|
let grepFlags;
|
|
3698
3697
|
if (mode === "count") {
|
|
3699
|
-
rgFlags =
|
|
3700
|
-
grepFlags =
|
|
3698
|
+
rgFlags = ["--count", ...ci];
|
|
3699
|
+
grepFlags = ["-rc", ...ci];
|
|
3701
3700
|
} else if (mode === "filesWithMatches") {
|
|
3702
|
-
rgFlags =
|
|
3703
|
-
grepFlags =
|
|
3701
|
+
rgFlags = ["-l", ...ci];
|
|
3702
|
+
grepFlags = ["-rl", ...ci];
|
|
3704
3703
|
} else {
|
|
3705
|
-
rgFlags =
|
|
3706
|
-
grepFlags =
|
|
3707
|
-
}
|
|
3708
|
-
const
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3704
|
+
rgFlags = ["-n", "--no-heading", ...ci, ...ctx, `--max-count=${max}`];
|
|
3705
|
+
grepFlags = ["-rn", ...ci, ...ctx, `--max-count=${max}`];
|
|
3706
|
+
}
|
|
3707
|
+
const rgArgs = [
|
|
3708
|
+
...rgFlags,
|
|
3709
|
+
...input.glob ? ["--glob", input.glob] : [],
|
|
3710
|
+
"--",
|
|
3711
|
+
input.pattern,
|
|
3712
|
+
searchPath
|
|
3713
|
+
];
|
|
3714
|
+
const grepArgs = [
|
|
3715
|
+
...grepFlags,
|
|
3716
|
+
"--exclude-dir=node_modules",
|
|
3717
|
+
"--exclude-dir=.git",
|
|
3718
|
+
"--include=*.ts",
|
|
3719
|
+
"--include=*.tsx",
|
|
3720
|
+
"--include=*.js",
|
|
3721
|
+
"--include=*.json",
|
|
3722
|
+
"--include=*.md",
|
|
3723
|
+
"--",
|
|
3724
|
+
input.pattern,
|
|
3725
|
+
searchPath
|
|
3726
|
+
];
|
|
3727
|
+
const run = (cmd, cmdArgs) => new Promise((resolve4) => {
|
|
3728
|
+
const child = execFile(
|
|
3729
|
+
cmd,
|
|
3730
|
+
cmdArgs,
|
|
3731
|
+
{ maxBuffer: 512 * 1024, timeout: 3e4 },
|
|
3732
|
+
(err, stdout) => {
|
|
3733
|
+
resolve4({
|
|
3734
|
+
stdout: stdout ?? "",
|
|
3735
|
+
timedOut: err?.killed === true
|
|
3736
|
+
});
|
|
3715
3737
|
}
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
resolve4(formatResults(grepStdout, max, mode));
|
|
3719
|
-
} else {
|
|
3720
|
-
resolve4("No matches found.");
|
|
3721
|
-
}
|
|
3722
|
-
});
|
|
3723
|
-
});
|
|
3738
|
+
);
|
|
3739
|
+
child.stdin?.end();
|
|
3724
3740
|
});
|
|
3741
|
+
const rg = await run("rg", rgArgs);
|
|
3742
|
+
if (rg.stdout.trim()) {
|
|
3743
|
+
return formatResults(rg.stdout, max, mode);
|
|
3744
|
+
}
|
|
3745
|
+
if (rg.timedOut) {
|
|
3746
|
+
return `Error: search timed out after 30s in ${searchPath} \u2014 narrow the path or pattern.`;
|
|
3747
|
+
}
|
|
3748
|
+
const grep = await run("grep", grepArgs);
|
|
3749
|
+
if (grep.stdout.trim()) {
|
|
3750
|
+
return formatResults(grep.stdout, max, mode);
|
|
3751
|
+
}
|
|
3752
|
+
if (grep.timedOut) {
|
|
3753
|
+
return `Error: search timed out after 30s in ${searchPath} \u2014 narrow the path or pattern.`;
|
|
3754
|
+
}
|
|
3755
|
+
return "No matches found.";
|
|
3725
3756
|
}
|
|
3726
3757
|
};
|
|
3727
3758
|
}
|
|
@@ -4360,6 +4391,13 @@ var init_analyzeImage = __esm({
|
|
|
4360
4391
|
// src/tools/_helpers/screenshot.ts
|
|
4361
4392
|
function buildScreenshotAnalysisPrompt(opts) {
|
|
4362
4393
|
let p = opts?.prompt || SCREENSHOT_ANALYSIS_PROMPT;
|
|
4394
|
+
if (opts?.additionalQuestions) {
|
|
4395
|
+
p += `
|
|
4396
|
+
|
|
4397
|
+
After the analysis above, also answer the following specific questions about the screenshot:
|
|
4398
|
+
|
|
4399
|
+
${opts.additionalQuestions}`;
|
|
4400
|
+
}
|
|
4363
4401
|
if (opts?.styleMap) {
|
|
4364
4402
|
p += `
|
|
4365
4403
|
|
|
@@ -5620,7 +5658,8 @@ async function runBrowserAutomation(task, context, opts) {
|
|
|
5620
5658
|
step: {
|
|
5621
5659
|
imageUrl: s.result.url,
|
|
5622
5660
|
prompt: buildScreenshotAnalysisPrompt({
|
|
5623
|
-
styleMap: s.result.styleMap
|
|
5661
|
+
styleMap: s.result.styleMap,
|
|
5662
|
+
additionalQuestions: opts?.analysisPrompt
|
|
5624
5663
|
}),
|
|
5625
5664
|
visionModelOverride: visionOverride
|
|
5626
5665
|
}
|
|
@@ -5635,7 +5674,13 @@ async function runBrowserAutomation(task, context, opts) {
|
|
|
5635
5674
|
if (i >= analyses.length) {
|
|
5636
5675
|
return;
|
|
5637
5676
|
}
|
|
5638
|
-
|
|
5677
|
+
const analysis = analyses[i]?.output?.analysis || analyses[i]?.output || "";
|
|
5678
|
+
step.result.analysis = analysis;
|
|
5679
|
+
const kind = step.command === "screenshotFullPage" ? "fullPage" : "viewport";
|
|
5680
|
+
const harvested = lastCapture[kind];
|
|
5681
|
+
if (harvested && typeof analysis === "string" && analysis && harvested.url === step.result.url) {
|
|
5682
|
+
harvested.analysis = analysis;
|
|
5683
|
+
}
|
|
5639
5684
|
});
|
|
5640
5685
|
} catch {
|
|
5641
5686
|
log9.debug("Failed to parse batch analysis result", {
|
|
@@ -5655,7 +5700,7 @@ async function runBrowserAutomation(task, context, opts) {
|
|
|
5655
5700
|
const preferred = opts?.capture === "viewport" ? lastCapture.viewport ?? lastCapture.fullPage : lastCapture.fullPage ?? lastCapture.viewport;
|
|
5656
5701
|
return {
|
|
5657
5702
|
text: result.text,
|
|
5658
|
-
...preferred?.url ? { screenshot:
|
|
5703
|
+
...preferred?.url ? { screenshot: preferred } : {}
|
|
5659
5704
|
};
|
|
5660
5705
|
} finally {
|
|
5661
5706
|
release();
|
|
@@ -5728,15 +5773,25 @@ async function executeScreenshot(input, onLog, context) {
|
|
|
5728
5773
|
const shotKind = fullPage ? "full-page" : "viewport";
|
|
5729
5774
|
const task = input.path ? `Navigate to "${input.path}", then: ${input.instructions}. After completing these steps, take a ${shotKind} screenshot.` : `${input.instructions}. After completing these steps, take a ${shotKind} screenshot.`;
|
|
5730
5775
|
const result = await runBrowserAutomation(task, context, {
|
|
5731
|
-
capture: fullPage ? "fullPage" : "viewport"
|
|
5776
|
+
capture: fullPage ? "fullPage" : "viewport",
|
|
5777
|
+
analysisPrompt: input.prompt
|
|
5732
5778
|
});
|
|
5733
5779
|
if (!result.screenshot) {
|
|
5734
5780
|
return result.text;
|
|
5735
5781
|
}
|
|
5782
|
+
const { url, styleMap, analysis } = result.screenshot;
|
|
5783
|
+
if (analysis) {
|
|
5784
|
+
onLog?.(JSON.stringify({ url, analysis }));
|
|
5785
|
+
return JSON.stringify({
|
|
5786
|
+
url,
|
|
5787
|
+
analysis,
|
|
5788
|
+
...styleMap ? { styleMap } : {}
|
|
5789
|
+
});
|
|
5790
|
+
}
|
|
5736
5791
|
return await streamScreenshotAnalysis({
|
|
5737
|
-
image:
|
|
5792
|
+
image: url,
|
|
5738
5793
|
prompt: input.prompt,
|
|
5739
|
-
styleMap
|
|
5794
|
+
styleMap,
|
|
5740
5795
|
onLog,
|
|
5741
5796
|
model,
|
|
5742
5797
|
apiConfig: context?.apiConfig
|
|
@@ -5782,7 +5837,7 @@ var init_screenshot2 = __esm({
|
|
|
5782
5837
|
},
|
|
5783
5838
|
prompt: {
|
|
5784
5839
|
type: "string",
|
|
5785
|
-
description: "Optional
|
|
5840
|
+
description: "Optional specific questions about the screenshot, answered alongside the general description of what's visible. If omitted, returns just the general description."
|
|
5786
5841
|
},
|
|
5787
5842
|
imageUrl: {
|
|
5788
5843
|
type: "string",
|
|
@@ -6896,7 +6951,7 @@ var init_createWireframe = __esm({
|
|
|
6896
6951
|
UPLOAD_TIMEOUT_MS2 = 3e4;
|
|
6897
6952
|
definition10 = {
|
|
6898
6953
|
name: "createWireframe",
|
|
6899
|
-
description: "
|
|
6954
|
+
description: "Generate a wireframe from self-contained HTML+CSS you author and write it to disk as a design artifact. This is how a wireframe comes to exist \u2014 the way generateImages is how an image comes to exist \u2014 and the developer builds from the file it creates. The result also hands back the reference line that embeds the wireframe in your response and in specs; paste it wherever the wireframe belongs and it renders as a live preview. Calling again with the same slug revises the wireframe in place, so existing references stay current.",
|
|
6900
6955
|
inputSchema: {
|
|
6901
6956
|
type: "object",
|
|
6902
6957
|
properties: {
|
|
@@ -6906,7 +6961,7 @@ var init_createWireframe = __esm({
|
|
|
6906
6961
|
},
|
|
6907
6962
|
slug: {
|
|
6908
6963
|
type: "string",
|
|
6909
|
-
description: 'Filename stem, lowercase kebab-case (e.g. "feed-post-card").
|
|
6964
|
+
description: 'Filename stem, lowercase kebab-case (e.g. "feed-post-card"). Re-use a slug to revise that wireframe in place.'
|
|
6910
6965
|
},
|
|
6911
6966
|
description: {
|
|
6912
6967
|
type: "string",
|
|
@@ -11172,7 +11227,10 @@ var init_headless = __esm({
|
|
|
11172
11227
|
//////////////////////////////////////////////////////////////////////////////
|
|
11173
11228
|
handleClear() {
|
|
11174
11229
|
clearSession(this.state);
|
|
11175
|
-
return {
|
|
11230
|
+
return {
|
|
11231
|
+
modelSurfaces: getEffectiveModelSurfaces(),
|
|
11232
|
+
allowedModelsByType: ALLOWED_MODELS_BY_TYPE
|
|
11233
|
+
};
|
|
11176
11234
|
}
|
|
11177
11235
|
/** Change per-agent model picks without clearing history. Takes effect on
|
|
11178
11236
|
* the next turn — the model is resolved live, per LLM call, from
|
|
@@ -120,6 +120,8 @@ analytics.track('vendor_submitted', { vendorType: 'restaurant' });
|
|
|
120
120
|
analytics.track('checkout_completed', { itemCount: 3, total: 47.99 });
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
+
- **Apps can also READ their own analytics from backend methods** — the agent SDK's `analytics` namespace (lifetime per-page metrics, live visitor count, traffic sources, event stats), so an admin view can show real traffic next to the app's own data. Consult `askMindStudioSdk` for the query API when building one.
|
|
124
|
+
|
|
123
125
|
Analytics is **cookie-banner-free by design**: per-app scoping, IP discarded after geo lookup, country-level only, query strings server-scrubbed except for a UTM whitelist (`utm_*`, `ref`, `source`, `gclid`, `fbclid`, `msclkid`), no fingerprinting, no third-party scripts. If a user asks about GDPR cookie consent for analytics, you can explain why it is not needed.
|
|
124
126
|
|
|
125
127
|
Disabling telemetry is a per-app dashboard setting (platform toggle, not code). Point users there if they ask.
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
# Jewels
|
|
2
2
|
|
|
3
|
-
A "jewel" is an optional AI companion for a single app method (`foo.jewel.ts` beside `foo.ts`): it proposes the method call a human would otherwise make, the method's manifest `autonomy` setting decides what happens to each proposal: recorded silently (`shadow`), queued for human review (`approve`), or committed (`auto`), and every proposal is then graded against what really happened. Jewels are never part of an initial build, and most apps will never need them. By default, build AI features as plain method code - if the user wants to add jewels later on, conversion is easy
|
|
3
|
+
A "jewel" is an optional AI companion for a single app method (`foo.jewel.ts` beside `foo.ts`): it proposes the method call a human would otherwise make, the method's manifest `autonomy` setting decides what happens to each proposal: recorded silently (`shadow`), queued for human review (`approve`), or committed (`auto`), and every proposal is then graded against what really happened. Jewels are never part of an initial build, and most apps will never need them. By default, build AI features as plain method code - if the user wants to add jewels later on, conversion is easy. Before doing any work with or thinking around jewels, load the `jewels` skill.
|
|
@@ -167,14 +167,55 @@ const results = await Vendors
|
|
|
167
167
|
// Aggregates — all return Query objects (batchable)
|
|
168
168
|
const count = await Vendors.count();
|
|
169
169
|
const any = await Vendors.some(v => v.status === 'pending');
|
|
170
|
-
const cheapest = await Vendors.min(v => v.totalCents);
|
|
171
|
-
const grouped = await Vendors.groupBy(v => v.status);
|
|
170
|
+
const cheapest = await Vendors.min(v => v.totalCents); // full ROW with min value
|
|
171
|
+
const grouped = await Vendors.groupBy(v => v.status); // Map of FULL rows — fetches everything
|
|
172
172
|
|
|
173
173
|
// These two return Promises directly (not batchable)
|
|
174
174
|
const all = await Vendors.every(v => v.status !== 'rejected');
|
|
175
175
|
const empty = await Vendors.isEmpty();
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
+
### Aggregation
|
|
179
|
+
|
|
180
|
+
`count()`, `sum()`, `avg()`, `countDistinct()`, and `aggregate()` compile to SQL aggregates (`COUNT`/`TOTAL`/`AVG`/`GROUP BY`) — no rows are fetched, so they stay cheap at any table size. Use them for every summary over a table that can grow; never page a whole table into memory to compute totals.
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// Scalar aggregates — accessors, like sortBy/min/max
|
|
184
|
+
const revenue = await Orders.filter(o => o.status === 'paid').sum(o => o.amountCents);
|
|
185
|
+
const avgScore = await Answers.avg(a => a.score); // number | null (null on empty set)
|
|
186
|
+
const respondents = await Answers.countDistinct(a => a.responseId);
|
|
187
|
+
|
|
188
|
+
// Grouped aggregation — STRING column names, one plain object per group
|
|
189
|
+
const top = await Answers
|
|
190
|
+
.filter((a, $) => a.surveyId === $.surveyId, { surveyId }) // bindings: lifts closure var so filter compiles to SQL
|
|
191
|
+
.aggregate({
|
|
192
|
+
by: ['questionId', 'dimension'],
|
|
193
|
+
select: {
|
|
194
|
+
n: { count: true },
|
|
195
|
+
total: { sum: 'score' },
|
|
196
|
+
avgScore: { avg: 'score' },
|
|
197
|
+
respondents: { countDistinct: 'responseId' },
|
|
198
|
+
},
|
|
199
|
+
orderBy: 'total', desc: true, limit: 20,
|
|
200
|
+
});
|
|
201
|
+
// Array<{ questionId, dimension, n: number, total: number, avgScore: number | null, respondents: number }>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Select terms: `{ count: true }`, `{ sum: 'col' }`, `{ avg: 'col' }`, `{ min: 'col' }`, `{ max: 'col' }`, `{ countDistinct: 'col' }`. Omit `by` for several aggregates over the whole filtered set (returns a single object). Empty set: `count`/`countDistinct` → 0, `sum` → 0, `avg`/`min`/`max` → null. `groupBy()` fetches full rows — reach for it only when you need the rows themselves, not a summary.
|
|
205
|
+
|
|
206
|
+
### Raw SQL Escape Hatch
|
|
207
|
+
|
|
208
|
+
`db.sql()` runs read-only raw SQL against the app's own database — for joins, subqueries, and window functions the typed API can't express. Last resort: prefer the typed API (including `aggregate()`) whenever it can express the query.
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
const rows = await db.sql<{ questionId: string; n: number }>(
|
|
212
|
+
'SELECT questionId, COUNT(*) AS n FROM answers WHERE surveyId = ? GROUP BY questionId',
|
|
213
|
+
[surveyId],
|
|
214
|
+
);
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`SELECT`/`WITH` only — writes throw; use Table methods for writes. Positional `?` bind params. Lazy and batchable via `db.batch()`. Raw rows come back close to how SQLite stores them and may not exactly match the typed API's representations.
|
|
218
|
+
|
|
178
219
|
### Updating Records
|
|
179
220
|
|
|
180
221
|
```typescript
|
|
@@ -342,7 +383,7 @@ const [_, newOrder, pending] = await db.batch(
|
|
|
342
383
|
|
|
343
384
|
**Always batch instead of sequential awaits.** A loop with `await Table.update()` inside makes N separate HTTP calls. Mapping to mutations and passing them to `db.batch()` makes one.
|
|
344
385
|
|
|
345
|
-
**Note:** Almost all read methods return batchable `Query` objects — including `get()`, `findOne()`, `count()`, `some()`, `min()`, `max()`, and `groupBy()
|
|
386
|
+
**Note:** Almost all read methods return batchable `Query` objects — including `get()`, `findOne()`, `count()`, `sum()`, `avg()`, `countDistinct()`, `aggregate()`, `some()`, `min()`, `max()`, and `groupBy()` — and `db.sql()` results batch too. The exceptions are `every()` and `isEmpty()`, which return Promises directly and cannot be batched.
|
|
346
387
|
|
|
347
388
|
## Migrations
|
|
348
389
|
|
|
@@ -30,7 +30,7 @@ One jewel per judgment. If a jewel would need to take two actions, the methods a
|
|
|
30
30
|
|
|
31
31
|
Never include a jewel in an initial build, and don't bring them up while an app is young. Most users will never want one; a few will care a lot. The default posture:
|
|
32
32
|
|
|
33
|
-
- **Build AI features plainly.** A drafting feature is a method that calls a model and returns text, with no jewel machinery anywhere. Converting it
|
|
33
|
+
- **Build AI features plainly.** A drafting feature is a method that calls a model and returns text, with no jewel machinery anywhere. Converting it later is cheap. At shadow it's purely additive (a `.jewel.ts` beside the method, an `autonomy` line in the manifest). At approve, the jewel takes over as the sole producer: it proposes what the plain feature used to write, surfaced in the same place the human already saw it, and the plain generation is retired — one generator, always. A jewel graded against human edits of some other AI's draft is measuring model against model.
|
|
34
34
|
- **Jewels enter when the user asks** about automating a judgment the app already handles, or picks an automation item off the roadmap. Once an app is solid and a judgment verb sees real use, a benefit-phrased automation item may belong on the roadmap; that is the only proactive channel.
|
|
35
35
|
- **When the user engages, have the conversation first**: what it does in their terms, what the levels mean, what evidence looks like. Then start with exactly one verb.
|
|
36
36
|
|
|
@@ -77,7 +77,7 @@ interface JewelPairRecord {
|
|
|
77
77
|
|
|
78
78
|
The executor never throws: a shadow run must never break anything. Your code failing inside `subject` or `propose` becomes the record's `error`; `grade` failing softens to verdict `skip`.
|
|
79
79
|
|
|
80
|
-
Shadowing runs only on the deployed app
|
|
80
|
+
Shadowing runs only on the deployed app — dev invocations are synthetic and would pollute the pair ledger. The arrival flow is different: `jewels.propose` on approve/auto methods runs for real in dev sessions (the jewel executes from local source, queue items are scoped to the dev session, auto commits apply against the dev database) so the app's hot path is testable end-to-end, but none of it is recorded — no pairs, no grading, no training data.
|
|
81
81
|
|
|
82
82
|
## Usage
|
|
83
83
|
|
|
@@ -203,7 +203,7 @@ grade: async ({ proposed, actual }) => {
|
|
|
203
203
|
|
|
204
204
|
Four levels: `manual` (no jewel ever; a policy statement), `shadow` (runs silently on every human invocation, pairs recorded, nothing visible), `approve` (jewel drafts, a human accepts, edits, or rejects; the edit is the richest training signal there is), `auto` (the jewel acts under its own identity).
|
|
205
205
|
|
|
206
|
-
**Auto is the only level you earn with evidence. Shadow and approve are both safe starting points; pick by what the product is.** When the proposals themselves are the product (drafts for review), start at approve; it's often the permanent home. When the evidence is the product (an auto-bound verb), start at shadow. Raising to auto is a reviewed manifest diff justified by agreement numbers.
|
|
206
|
+
**Auto is the only level you earn with evidence. Shadow and approve are both safe starting points; pick by what the product is.** When the proposals themselves are the product (drafts for review), start at approve; it's often the permanent home. When the evidence is the product (an auto-bound verb), start at shadow. The levels differ in what exists, not in presentation: at approve the human sees and edits the proposal before it takes effect; at shadow nothing ever surfaces. Raising to auto is a reviewed manifest diff justified by agreement numbers.
|
|
207
207
|
|
|
208
208
|
**Choose approve by the verb's risk shape, not by default.** Reversible state-machine verbs (routing, classification; a mistake is a two-click correction) go shadow → auto with a `sampleRate` canary and skip approve, because a review queue on a high-agreement classifier adds work without adding safety. Approve belongs on irreversible or outward-facing verbs (send, publish, charge, refund): `sampleRate` is population-level risk control, and those verbs need per-instance gating, which is what approve is.
|
|
209
209
|
|
|
@@ -229,7 +229,7 @@ from the platform's small menu; `qwen3.5-4b` is the default, with `qwen3.5-9b` a
|
|
|
229
229
|
`gpt-oss-20b` as larger alternatives, and the default is the right choice unless a
|
|
230
230
|
report shows it falling short), `windowDays` (how many days of ledger history to train
|
|
231
231
|
on; default all history), `epochs` (1-10, default 3), `rank` (LoRA rank, 4-64, default
|
|
232
|
-
16), `learningRate` (default
|
|
232
|
+
16), `learningRate` (default 1e-4). It requires `jewel`, and it rides the release:
|
|
233
233
|
changing a knob is a commit + deploy before the next training run.
|
|
234
234
|
|
|
235
235
|
Once a method has accumulated graded pairs, train from the prod CLI:
|
|
@@ -238,14 +238,20 @@ a run id and the dataset report; a run takes minutes to tens of minutes, so neve
|
|
|
238
238
|
`--wait` (that flag is for humans at a terminal — it would block your whole loop).
|
|
239
239
|
Start the run, tell the user it's training, keep working on other things, and check in
|
|
240
240
|
with `mindstudio-prod jewels run <runId>` between tasks — the `progress` field shows
|
|
241
|
-
the live phase and training percent,
|
|
241
|
+
the live phase and training percent, the run's `log` narrates the whole story as
|
|
242
|
+
timestamped status events (queued, GPU acquired, training, grading — loss points are
|
|
243
|
+
compacted to a count in CLI output), and `status` goes `complete` or `failed` with the
|
|
242
244
|
full report. The dataset report says whether the ledger is trainable (pairs without an
|
|
243
245
|
attached `trace` don't count), and a run produces a downloadable LoRA adapter plus a
|
|
244
246
|
held-out agreement report: how often the trained model matched your team's decisions
|
|
245
247
|
on pairs it never saw. The adapter and report land in the app's own file store
|
|
246
248
|
(`models/` in the Files dashboard), so the user can download them there. The
|
|
247
|
-
agreement number is `report.
|
|
248
|
-
|
|
249
|
+
agreement number is `report.selected.agreement` when present, else
|
|
250
|
+
`report.grading.agreement` — scored by the jewel's own grade function, the same grader
|
|
251
|
+
as the pairs dashboard. A run trains multiple checkpoint candidates (per-epoch
|
|
252
|
+
snapshots, plus a DPO pass over the team's corrections when enough have accumulated)
|
|
253
|
+
and the platform promotes whichever scores highest on that grader — `report.selected`
|
|
254
|
+
names the serving checkpoint; this is automatic, never something you configure.
|
|
249
255
|
A completed run without a `grading` block just hasn't been graded yet
|
|
250
256
|
(`mindstudio-prod jewels grade <runId>` fills it). A completed run registers the
|
|
251
257
|
app's tuned model as a real model id — `tuned/{appId}/{methodId}`, one stable id per
|
|
@@ -253,9 +259,19 @@ method that retraining advances in place — and the latest complete run per met
|
|
|
253
259
|
automatically served on the platform's GPU pool, so that id works like any other
|
|
254
260
|
model the moment training finishes. There is no special invoke tool: to sanity-check
|
|
255
261
|
or demo a tuned model, make an ordinary generate-text call with that model id (the
|
|
256
|
-
pool serves it with the exact template posture it was trained under). Promotion
|
|
257
|
-
|
|
258
|
-
|
|
262
|
+
pool serves it with the exact template posture it was trained under). **Promotion**
|
|
263
|
+
(switching a jewel onto its own tuned model) is a one-line change: swap the `model`
|
|
264
|
+
id inside the jewel's existing `runTask` call to the tuned line — the git commit is
|
|
265
|
+
the promotion record, and reverting it is the rollback. Tool-using jewels promote
|
|
266
|
+
too: the tuned model was trained on the jewel's full transcripts, tool calls
|
|
267
|
+
included, and the training report's `toolEval` block shows how faithfully it makes
|
|
268
|
+
the teacher's tool decisions (qwen bases only — gpt-oss can't drive tools yet, and
|
|
269
|
+
the platform rejects that combination with a clear error). One hard rule: never
|
|
270
|
+
restructure the propose path to a generate-text call to do it — that silently loses
|
|
271
|
+
`outputSchema` validation AND the task `traceId`, so the jewel's new pairs stop
|
|
272
|
+
being trainable and the learning loop dies. `runTask` with the tuned id keeps both.
|
|
273
|
+
Promotion is still a deliberate step; do not wire a tuned model into app code
|
|
274
|
+
unprompted.
|
|
259
275
|
|
|
260
276
|
## Arrival Triggers (`mindstudio.jewels.propose`)
|
|
261
277
|
|
|
@@ -288,7 +304,7 @@ Rules that matter:
|
|
|
288
304
|
|
|
289
305
|
## Native Approval Flows (`jewels.queue`)
|
|
290
306
|
|
|
291
|
-
For `approve`-mode methods, the
|
|
307
|
+
For `approve`-mode methods, the queue is data, not a screen: proposals surface wherever the human already makes this decision. When the app has an editor for the decision, the pending proposal pre-fills it and the human's normal confirm action resolves it; a dedicated inbox is only for decisions with no existing surface. Three pieces:
|
|
292
308
|
|
|
293
309
|
```typescript
|
|
294
310
|
// 1. Backend list method, gated with the APP'S reviewer role.
|
|
@@ -297,7 +313,7 @@ export async function listPendingDrafts() {
|
|
|
297
313
|
return mindstudio.jewels.queue.list({ methodId: 'send-message' });
|
|
298
314
|
}
|
|
299
315
|
|
|
300
|
-
// 2. Frontend
|
|
316
|
+
// 2. Frontend surfaces items where the decision already lives (pre-fill the existing editor); render subject, proposed input, reasoning.
|
|
301
317
|
|
|
302
318
|
// 3. Backend resolve method: approve applies, dismiss records.
|
|
303
319
|
export async function reviewDraft(input: {
|
|
@@ -82,7 +82,7 @@ You have access to the `mindstudio` CLI, which exposes every SDK action as a com
|
|
|
82
82
|
### Production App Management
|
|
83
83
|
You have access to `mindstudio-prod`, a CLI for managing the user's production app. Use it via your bash tool. All output is JSON. Run `mindstudio-prod --help` or `mindstudio-prod <command> --help` to discover usage and available options.
|
|
84
84
|
|
|
85
|
-
Available commands: `requests` (server logs, errors, latency), `crashes` (frontend browser errors), `analytics` (traffic,
|
|
85
|
+
Available commands: `requests` (server logs, errors, latency), `crashes` (frontend browser errors), `analytics` (traffic queries — lifetime metrics, sources, live counters), `releases`, `diagnostics` (Lighthouse audit), `domains`, `users` (list, set roles), `db` (query production sql), `data` (live db operations like lift-from-dev), `methods` (list, invoke), `secrets`, `files` (CDN files), `datasources` (document corpora), `prerender` (crawler snapshots), `voice` (phone numbers, call logs, voice policy), `issues` (externally-reported bugs).
|
|
86
86
|
|
|
87
87
|
Two rules: buying a `voice` phone number bills $1/month — never buy without the user's explicit confirmation. `issues` is for externally-reported bugs only — read from it and resolve items when the user asks; never use it to track work you are doing with the user.
|
|
88
88
|
|
|
@@ -16,7 +16,7 @@ Think about the ways you can truly elevate the design. Use image generation to c
|
|
|
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
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.
|
|
18
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.
|
|
19
|
-
- Build wireframes with `createWireframe` during your working phase as you work out a layout, component, or interaction.
|
|
19
|
+
- Build wireframes with `createWireframe` during your working phase as you work out a layout, component, or interaction. The tool generates the wireframe as a real asset, the way `generateImages` generates images: it writes the file the developer builds from, and its result hands back the reference line that embeds the wireframe in your response. Paste that line where the wireframe belongs and it renders as a live preview. Same slug = revise in place; new slug = new wireframe.
|
|
20
20
|
|
|
21
21
|
## Voice
|
|
22
22
|
- No emoji, no filler.
|
|
@@ -25,7 +25,7 @@ Some surfaces are deep enough to carry their own craft reference in <available_s
|
|
|
25
25
|
|
|
26
26
|
### Wireframes
|
|
27
27
|
|
|
28
|
-
Wireframes are design artifacts you build while you work, in the same phase as screenshots and image generation. As you work out a layout, a card anatomy, an interaction, or a motion pattern, build it with `createWireframe`: a `name`, a kebab-case `slug`, a one-line `description`, and self-contained HTML+CSS. Sketching in HTML is how you think through spatial decisions, so by the time you write your direction, the wireframes that anchor it already exist.
|
|
28
|
+
Wireframes are design artifacts you build while you work, in the same phase as screenshots and image generation. As you work out a layout, a card anatomy, an interaction, or a motion pattern, build it with `createWireframe`: a `name`, a kebab-case `slug`, a one-line `description`, and self-contained HTML+CSS. Sketching in HTML is how you think through spatial decisions, so by the time you write your direction, the wireframes that anchor it already exist as files — the developer reads them for the exact markup and CSS. Each call's result hands back the markdown reference line that embeds that wireframe; when you write your response, paste it wherever the wireframe belongs, with your notes in the surrounding prose, and it renders as a live visual preview.
|
|
29
29
|
|
|
30
30
|
Never use ASCII art, box-drawing characters, or code-block diagrams to describe layouts. Always use a wireframe instead, even if it's just grey rectangles with labels. A 20-line wireframe with placeholder boxes communicates proportions, spacing, and hierarchy better than any text diagram. For abstract layouts, use skeleton-style placeholders (grey boxes, rounded rects) rather than mocking up real content.
|
|
31
31
|
|