@mindstudio-ai/remy 0.1.26 → 0.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +149 -41
  2. package/dist/compiled/tables.md +53 -1
  3. package/dist/headless.d.ts +10 -2
  4. package/dist/headless.js +531 -271
  5. package/dist/index.js +574 -301
  6. package/dist/prompt/.notes.md +0 -1
  7. package/dist/prompt/compiled/tables.md +53 -1
  8. package/dist/prompt/static/authoring.md +10 -0
  9. package/dist/prompt/static/instructions.md +2 -1
  10. package/dist/prompt/static/team.md +1 -1
  11. package/dist/static/authoring.md +10 -0
  12. package/dist/static/instructions.md +2 -1
  13. package/dist/static/team.md +1 -1
  14. package/dist/subagents/.notes-background-agents.md +80 -0
  15. package/dist/subagents/browserAutomation/prompt.md +37 -2
  16. package/dist/subagents/codeSanityCheck/prompt.md +5 -0
  17. package/dist/subagents/designExpert/.notes.md +2 -2
  18. package/dist/subagents/designExpert/data/compile-font-descriptions.sh +125 -0
  19. package/dist/subagents/designExpert/data/compile-inspiration.sh +6 -1
  20. package/dist/subagents/designExpert/data/fonts.json +497 -869
  21. package/dist/subagents/designExpert/data/inspiration.json +97 -245
  22. package/dist/subagents/designExpert/data/inspiration.raw.json +1 -12
  23. package/dist/subagents/designExpert/prompts/animation.md +1 -1
  24. package/dist/subagents/designExpert/prompts/identity.md +4 -2
  25. package/dist/subagents/designExpert/prompts/instructions.md +2 -3
  26. package/dist/subagents/designExpert/prompts/layout.md +1 -13
  27. package/dist/subagents/designExpert/prompts/tool-prompts/design-analysis.md +22 -0
  28. package/dist/subagents/designExpert/prompts/tool-prompts/font-analysis.md +17 -0
  29. package/dist/subagents/productVision/prompt.md +1 -1
  30. package/package.json +1 -1
package/dist/headless.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/headless.ts
2
2
  import { createInterface } from "readline";
3
- import fs20 from "fs";
4
- import path13 from "path";
3
+ import fs21 from "fs";
4
+ import path14 from "path";
5
5
 
6
6
  // src/config.ts
7
7
  import fs2 from "fs";
@@ -300,14 +300,11 @@ function buildSystemPrompt(onboardingState, viewContext) {
300
300
  loadSpecFileMetadata(),
301
301
  loadProjectFileListing()
302
302
  ].filter(Boolean).join("\n");
303
- const now = (/* @__PURE__ */ new Date()).toLocaleString("en-US", {
304
- dateStyle: "full",
305
- timeStyle: "long"
306
- });
303
+ const now = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC");
307
304
  const template = `
308
305
  {{static/identity.md}}
309
306
 
310
- The current date is ${now}.
307
+ Current date/time: ${now}
311
308
 
312
309
  <platform_docs>
313
310
  <platform>
@@ -1142,7 +1139,7 @@ var promptUserTool = {
1142
1139
  }
1143
1140
  ]
1144
1141
  },
1145
- description: "Options for select and checklist types. Each can be a string or { label, description }."
1142
+ description: "Options for select and checklist types. Each can be a string or { label, description }. Image URLs (e.g. https://i.mscdn.ai/...) are rendered as visual previews, so use the URL directly as the option string when presenting images for the user to choose between."
1146
1143
  },
1147
1144
  multiple: {
1148
1145
  type: "boolean",
@@ -1243,27 +1240,75 @@ var confirmDestructiveActionTool = {
1243
1240
  };
1244
1241
 
1245
1242
  // src/subagents/common/runCli.ts
1246
- import { exec } from "child_process";
1243
+ import { spawn } from "child_process";
1247
1244
  function runCli(cmd, options) {
1248
1245
  return new Promise((resolve) => {
1249
- exec(
1250
- cmd,
1251
- {
1252
- timeout: options?.timeout ?? 6e4,
1253
- maxBuffer: options?.maxBuffer ?? 1024 * 1024
1254
- },
1255
- (err, stdout, stderr) => {
1256
- if (stdout.trim()) {
1257
- resolve(stdout.trim());
1258
- return;
1246
+ const timeout = options?.timeout ?? 6e4;
1247
+ const maxBuffer = options?.maxBuffer ?? 1024 * 1024;
1248
+ const cmdWithLogs = options?.jsonLogs && !cmd.includes("--json-logs") ? cmd.replace(/^(mindstudio\s+\S+)/, "$1 --json-logs") : cmd;
1249
+ const child = spawn("sh", ["-c", cmdWithLogs], {
1250
+ stdio: ["ignore", "pipe", "pipe"]
1251
+ });
1252
+ const logs = [];
1253
+ let stdout = "";
1254
+ let stderr = "";
1255
+ let stdoutSize = 0;
1256
+ let stderrSize = 0;
1257
+ let killed = false;
1258
+ child.stdout.on("data", (chunk) => {
1259
+ stdoutSize += chunk.length;
1260
+ if (stdoutSize <= maxBuffer) {
1261
+ stdout += chunk.toString();
1262
+ } else if (!killed) {
1263
+ killed = true;
1264
+ child.kill();
1265
+ }
1266
+ });
1267
+ child.stderr.on("data", (chunk) => {
1268
+ stderrSize += chunk.length;
1269
+ if (stderrSize > maxBuffer) {
1270
+ if (!killed) {
1271
+ killed = true;
1272
+ child.kill();
1259
1273
  }
1260
- if (err) {
1261
- resolve(`Error: ${stderr.trim() || err.message}`);
1262
- return;
1274
+ return;
1275
+ }
1276
+ const text = chunk.toString();
1277
+ stderr += text;
1278
+ for (const line of text.split("\n")) {
1279
+ const trimmed = line.trim();
1280
+ if (!trimmed || trimmed[0] !== "{") {
1281
+ continue;
1282
+ }
1283
+ try {
1284
+ const entry = JSON.parse(trimmed);
1285
+ if (entry.type === "log" && entry.value) {
1286
+ const prefix = entry.tag ? `[${entry.tag}]` : "[log]";
1287
+ logs.push(`${prefix} ${entry.value}`);
1288
+ }
1289
+ } catch {
1263
1290
  }
1264
- resolve("(no response)");
1265
1291
  }
1266
- );
1292
+ });
1293
+ const timer = setTimeout(() => {
1294
+ killed = true;
1295
+ child.kill();
1296
+ }, timeout);
1297
+ child.on("close", (code) => {
1298
+ clearTimeout(timer);
1299
+ const logBlock = logs.length > 0 ? logs.join("\n") + "\n\n" : "";
1300
+ const out = stdout.trim();
1301
+ if (out) {
1302
+ resolve(logBlock + out);
1303
+ return;
1304
+ }
1305
+ if (code !== 0 || killed) {
1306
+ const errMsg = stderr.trim() || (killed ? "Process timed out" : `Exit code ${code}`);
1307
+ resolve(logBlock + `Error: ${errMsg}`);
1308
+ return;
1309
+ }
1310
+ resolve(logBlock + "(no response)");
1311
+ });
1267
1312
  });
1268
1313
  }
1269
1314
 
@@ -1670,7 +1715,7 @@ ${unifiedDiff(input.path, content, updated)}`;
1670
1715
  };
1671
1716
 
1672
1717
  // src/tools/code/bash.ts
1673
- import { exec as exec2 } from "child_process";
1718
+ import { exec } from "child_process";
1674
1719
  var DEFAULT_TIMEOUT_MS = 12e4;
1675
1720
  var DEFAULT_MAX_LINES3 = 500;
1676
1721
  var bashTool = {
@@ -1704,7 +1749,7 @@ var bashTool = {
1704
1749
  const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES3;
1705
1750
  const timeoutMs = input.timeout ? input.timeout * 1e3 : DEFAULT_TIMEOUT_MS;
1706
1751
  return new Promise((resolve) => {
1707
- exec2(
1752
+ exec(
1708
1753
  input.command,
1709
1754
  {
1710
1755
  timeout: timeoutMs,
@@ -1744,7 +1789,7 @@ var bashTool = {
1744
1789
  };
1745
1790
 
1746
1791
  // src/tools/code/grep.ts
1747
- import { exec as exec3 } from "child_process";
1792
+ import { exec as exec2 } from "child_process";
1748
1793
  var DEFAULT_MAX = 50;
1749
1794
  function formatResults(stdout, max) {
1750
1795
  const lines = stdout.trim().split("\n");
@@ -1791,12 +1836,12 @@ var grepTool = {
1791
1836
  const rgCmd = `rg -n --no-heading --max-count=${max}${globFlag} '${escaped}' ${searchPath}`;
1792
1837
  const grepCmd = `grep -rn --max-count=${max} '${escaped}' ${searchPath} --include='*.ts' --include='*.tsx' --include='*.js' --include='*.json' --include='*.md'`;
1793
1838
  return new Promise((resolve) => {
1794
- exec3(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
1839
+ exec2(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
1795
1840
  if (stdout?.trim()) {
1796
1841
  resolve(formatResults(stdout, max));
1797
1842
  return;
1798
1843
  }
1799
- exec3(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
1844
+ exec2(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
1800
1845
  if (grepStdout?.trim()) {
1801
1846
  resolve(formatResults(grepStdout, max));
1802
1847
  } else {
@@ -2027,36 +2072,64 @@ var runMethodTool = {
2027
2072
  }
2028
2073
  };
2029
2074
 
2075
+ // src/tools/_helpers/screenshot.ts
2076
+ var SCREENSHOT_ANALYSIS_PROMPT = "Describe everything visible on screen from top to bottom \u2014 every element, its position, its size relative to the viewport, its colors, its content. Be thorough and spatial. After the inventory, note anything that looks visually broken (overlapping elements, clipped text, misaligned components).";
2077
+ async function captureAndAnalyzeScreenshot(promptOrOptions) {
2078
+ let prompt;
2079
+ let fullPage = false;
2080
+ if (typeof promptOrOptions === "object" && promptOrOptions !== null) {
2081
+ prompt = promptOrOptions.prompt;
2082
+ fullPage = promptOrOptions.fullPage ?? false;
2083
+ } else {
2084
+ prompt = promptOrOptions;
2085
+ }
2086
+ const ssResult = await sidecarRequest(
2087
+ "/screenshot",
2088
+ { fullPage },
2089
+ { timeout: 12e4 }
2090
+ );
2091
+ log.debug("Screenshot response", { ssResult });
2092
+ const url = ssResult?.url || ssResult?.screenshotUrl;
2093
+ if (!url) {
2094
+ throw new Error(
2095
+ `No URL in sidecar response. The browser may not be ready yet. Response: ${JSON.stringify(ssResult)}`
2096
+ );
2097
+ }
2098
+ if (prompt === false) {
2099
+ return url;
2100
+ }
2101
+ const analysisPrompt = prompt || SCREENSHOT_ANALYSIS_PROMPT;
2102
+ const analysis = await runCli(
2103
+ `mindstudio analyze-image --prompt ${JSON.stringify(analysisPrompt)} --image-url ${JSON.stringify(url)} --output-key analysis --no-meta`
2104
+ );
2105
+ return JSON.stringify({ url, analysis });
2106
+ }
2107
+
2030
2108
  // src/tools/code/screenshot.ts
2031
- var DEFAULT_PROMPT = "Describe this app screenshot for a developer who cannot see it. What is visible on screen: the layout, content, interactive elements, any loading or error states. Be concise and factual.";
2032
2109
  var screenshotTool = {
2033
2110
  definition: {
2034
2111
  name: "screenshot",
2035
- description: "Capture a screenshot of the app preview and get a description of what's on screen. Optionally provide a specific question about what you're looking for.",
2112
+ description: "Capture a screenshot of the app preview and get a description of what's on screen. Optionally provide a specific question about what you're looking for. By default captures the viewport (what the user sees). Set fullPage to capture the entire scrollable page.",
2036
2113
  inputSchema: {
2037
2114
  type: "object",
2038
2115
  properties: {
2039
2116
  prompt: {
2040
2117
  type: "string",
2041
2118
  description: "Optional question about the screenshot. If omitted, returns a general description of what's visible."
2119
+ },
2120
+ fullPage: {
2121
+ type: "boolean",
2122
+ description: "Capture the full scrollable page instead of just the viewport. Use when you need to see below-the-fold content."
2042
2123
  }
2043
2124
  }
2044
2125
  }
2045
2126
  },
2046
2127
  async execute(input) {
2047
2128
  try {
2048
- const { url } = await sidecarRequest(
2049
- "/screenshot",
2050
- {},
2051
- { timeout: 12e4 }
2052
- );
2053
- const analysisPrompt = input.prompt || DEFAULT_PROMPT;
2054
- const analysis = await runCli(
2055
- `mindstudio analyze-image --prompt ${JSON.stringify(analysisPrompt)} --image-url ${JSON.stringify(url)} --output-key analysis --no-meta`
2056
- );
2057
- return `Screenshot: ${url}
2058
-
2059
- ${analysis}`;
2129
+ return await captureAndAnalyzeScreenshot({
2130
+ prompt: input.prompt,
2131
+ fullPage: input.fullPage
2132
+ });
2060
2133
  } catch (err) {
2061
2134
  return `Error taking screenshot: ${err.message}`;
2062
2135
  }
@@ -2124,7 +2197,9 @@ async function runSubAgent(config) {
2124
2197
  ...apiConfig,
2125
2198
  model,
2126
2199
  subAgentId,
2127
- system,
2200
+ system: `${system}
2201
+
2202
+ Current date/time: ${(/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC")}`,
2128
2203
  messages: cleanMessagesForApi(messages),
2129
2204
  tools,
2130
2205
  signal
@@ -2219,7 +2294,7 @@ async function runSubAgent(config) {
2219
2294
  if (externalTools.has(tc.name) && resolveExternalTool) {
2220
2295
  result = await resolveExternalTool(tc.id, tc.name, tc.input);
2221
2296
  } else {
2222
- result = await executeTool2(tc.name, tc.input);
2297
+ result = await executeTool2(tc.name, tc.input, tc.id);
2223
2298
  }
2224
2299
  const isError = result.startsWith("Error");
2225
2300
  emit2({
@@ -2244,6 +2319,13 @@ async function runSubAgent(config) {
2244
2319
  })
2245
2320
  );
2246
2321
  for (const r of results) {
2322
+ const block = contentBlocks.find(
2323
+ (b) => b.type === "tool" && b.id === r.id
2324
+ );
2325
+ if (block?.type === "tool") {
2326
+ block.result = r.result;
2327
+ block.isError = r.isError;
2328
+ }
2247
2329
  messages.push({
2248
2330
  role: "user",
2249
2331
  content: r.result,
@@ -2269,8 +2351,18 @@ var BROWSER_TOOLS = [
2269
2351
  properties: {
2270
2352
  command: {
2271
2353
  type: "string",
2272
- enum: ["snapshot", "click", "type", "wait", "evaluate"],
2273
- description: "snapshot: accessibility tree of the page (waits for network to settle). click: click an element (animated cursor, full event sequence). type: type text into input (one char at a time, works with React/Vue/Svelte). wait: wait for an element to appear (polls 100ms, waits for network). evaluate: run JS in the page."
2354
+ enum: [
2355
+ "snapshot",
2356
+ "click",
2357
+ "type",
2358
+ "select",
2359
+ "wait",
2360
+ "navigate",
2361
+ "evaluate",
2362
+ "styles",
2363
+ "screenshot"
2364
+ ],
2365
+ description: "snapshot: accessibility tree of the page (waits for network to settle). click: click an element (animated cursor, full event sequence). type: type text into input (one char at a time, works with React/Vue/Svelte). select: select a dropdown option by text. wait: wait for an element to appear (polls 100ms, waits for network). navigate: navigate to a URL within the app (waits for load, subsequent steps run on new page). evaluate: run JS in the page. styles: read computed CSS styles from elements (pass properties array with camelCase names, or omit for defaults). screenshot: full-page viewport-stitched screenshot (returns base64 JPEG with dimensions)."
2274
2366
  },
2275
2367
  ref: {
2276
2368
  type: "string",
@@ -2292,6 +2384,10 @@ var BROWSER_TOOLS = [
2292
2384
  type: "string",
2293
2385
  description: "CSS selector fallback (last resort)."
2294
2386
  },
2387
+ option: {
2388
+ type: "string",
2389
+ description: "For select: the option text to select from a dropdown."
2390
+ },
2295
2391
  clear: {
2296
2392
  type: "boolean",
2297
2393
  description: "For type: clear the field before typing."
@@ -2303,6 +2399,15 @@ var BROWSER_TOOLS = [
2303
2399
  script: {
2304
2400
  type: "string",
2305
2401
  description: "For evaluate: JavaScript to run in the page."
2402
+ },
2403
+ url: {
2404
+ type: "string",
2405
+ description: 'For navigate: the URL to navigate to (e.g., "/quiz", "/settings").'
2406
+ },
2407
+ properties: {
2408
+ type: "array",
2409
+ items: { type: "string" },
2410
+ description: 'For styles: camelCase CSS property names to read (e.g., ["backgroundColor", "borderRadius", "fontSize"]). Omit for a default set.'
2306
2411
  }
2307
2412
  },
2308
2413
  required: ["command"]
@@ -2329,7 +2434,7 @@ var BROWSER_TOOLS = [
2329
2434
  }
2330
2435
  }
2331
2436
  ];
2332
- var BROWSER_EXTERNAL_TOOLS = /* @__PURE__ */ new Set(["browserCommand", "screenshot"]);
2437
+ var BROWSER_EXTERNAL_TOOLS = /* @__PURE__ */ new Set(["browserCommand"]);
2333
2438
 
2334
2439
  // src/subagents/browserAutomation/prompt.ts
2335
2440
  import fs13 from "fs";
@@ -2355,13 +2460,13 @@ ${appSpec}
2355
2460
  var browserAutomationTool = {
2356
2461
  definition: {
2357
2462
  name: "runAutomatedBrowserTest",
2358
- description: "Run an automated browser test against the live preview. The test agent always starts on the main page, so include navigation instructions if the test involves a sub-page. The browser uses the current user roles and dev database state, so run a scenario first if you need specific data or roles. Use after writing or modifying frontend code, to reproduce user-reported issues, or to test end-to-end flows.",
2463
+ description: "Run an automated browser test against the live preview. Describe what to test \u2014 the agent figures out how. Use after writing or modifying frontend code, to reproduce user-reported issues, or to test end-to-end flows.",
2359
2464
  inputSchema: {
2360
2465
  type: "object",
2361
2466
  properties: {
2362
2467
  task: {
2363
2468
  type: "string",
2364
- description: "What to test, in natural language. Include how to navigate to the relevant page and what data/roles to expect."
2469
+ description: "What to test, in natural language. Keep it brief \u2014 the agent reads the spec and figures out navigation, data setup, and test strategy on its own."
2365
2470
  }
2366
2471
  },
2367
2472
  required: ["task"]
@@ -2389,6 +2494,13 @@ var browserAutomationTool = {
2389
2494
  tools: BROWSER_TOOLS,
2390
2495
  externalTools: BROWSER_EXTERNAL_TOOLS,
2391
2496
  executeTool: async (name) => {
2497
+ if (name === "screenshot") {
2498
+ try {
2499
+ return await captureAndAnalyzeScreenshot();
2500
+ } catch (err) {
2501
+ return `Error taking screenshot: ${err.message}`;
2502
+ }
2503
+ }
2392
2504
  if (name === "resetBrowser") {
2393
2505
  try {
2394
2506
  await sidecarRequest("/reset-browser", {}, { timeout: 5e3 });
@@ -2405,7 +2517,50 @@ var browserAutomationTool = {
2405
2517
  signal: context.signal,
2406
2518
  parentToolId: context.toolCallId,
2407
2519
  onEvent: context.onEvent,
2408
- resolveExternalTool: context.resolveExternalTool
2520
+ resolveExternalTool: async (id, name, input2) => {
2521
+ if (!context.resolveExternalTool) {
2522
+ return "Error: no external tool resolver";
2523
+ }
2524
+ const result2 = await context.resolveExternalTool(id, name, input2);
2525
+ if (name === "browserCommand") {
2526
+ try {
2527
+ const parsed = JSON.parse(result2);
2528
+ const screenshotSteps = (parsed.steps || []).filter(
2529
+ (s) => s.command === "screenshot" && s.result?.url
2530
+ );
2531
+ if (screenshotSteps.length > 0) {
2532
+ const batchInput = screenshotSteps.map((s) => ({
2533
+ stepType: "analyzeImage",
2534
+ step: {
2535
+ imageUrl: s.result.url,
2536
+ prompt: SCREENSHOT_ANALYSIS_PROMPT
2537
+ }
2538
+ }));
2539
+ const batchResult = await runCli(
2540
+ `mindstudio batch --no-meta ${JSON.stringify(JSON.stringify(batchInput))}`,
2541
+ { timeout: 12e4 }
2542
+ );
2543
+ try {
2544
+ const analyses = JSON.parse(batchResult);
2545
+ let ai = 0;
2546
+ for (const step of parsed.steps) {
2547
+ if (step.command === "screenshot" && step.result?.url && ai < analyses.length) {
2548
+ step.result.analysis = analyses[ai]?.output?.analysis || analyses[ai]?.output || "";
2549
+ ai++;
2550
+ }
2551
+ }
2552
+ } catch {
2553
+ log.debug("Failed to parse batch analysis result", {
2554
+ batchResult
2555
+ });
2556
+ }
2557
+ return JSON.stringify(parsed);
2558
+ }
2559
+ } catch {
2560
+ }
2561
+ }
2562
+ return result2;
2563
+ }
2409
2564
  });
2410
2565
  context.subAgentMessages?.set(context.toolCallId, result.messages);
2411
2566
  return result.text;
@@ -2413,13 +2568,14 @@ var browserAutomationTool = {
2413
2568
  };
2414
2569
 
2415
2570
  // src/subagents/designExpert/tools.ts
2416
- var DESIGN_REFERENCE_PROMPT = `Analyze this website/app screenshot as a design reference. Assess:
2417
- 1) Mood/aesthetic
2418
- 2) Color palette with approximate hex values and palette strategy
2419
- 3) Typography style
2420
- 4) Layout composition (symmetric/asymmetric, grid structure, whitespace usage, content density)
2421
- 5) What makes it distinctive and interesting vs generic AI-generated interfaces
2422
- Be specific and concise.`;
2571
+ import fs14 from "fs";
2572
+ import path8 from "path";
2573
+ var base2 = import.meta.dirname ?? path8.dirname(new URL(import.meta.url).pathname);
2574
+ function resolvePath(filename) {
2575
+ const local4 = path8.join(base2, filename);
2576
+ return fs14.existsSync(local4) ? local4 : path8.join(base2, "subagents", "designExpert", filename);
2577
+ }
2578
+ var DESIGN_REFERENCE_PROMPT = fs14.readFileSync(resolvePath("prompts/tool-prompts/design-analysis.md"), "utf-8").trim();
2423
2579
  var DESIGN_EXPERT_TOOLS = [
2424
2580
  {
2425
2581
  name: "searchGoogle",
@@ -2473,24 +2629,33 @@ var DESIGN_EXPERT_TOOLS = [
2473
2629
  },
2474
2630
  {
2475
2631
  name: "screenshot",
2476
- description: "Capture a screenshot of the app preview. Returns a CDN URL. Use to review the current state of the UI being built.",
2632
+ description: "Capture a screenshot of the app preview. Returns a CDN URL with visual analysis. Use to review the current state of the UI being built. By default captures the viewport. Set fullPage to capture the entire scrollable page.",
2477
2633
  inputSchema: {
2478
2634
  type: "object",
2479
- properties: {}
2635
+ properties: {
2636
+ prompt: {
2637
+ type: "string",
2638
+ description: "Optional specific question about the screenshot."
2639
+ },
2640
+ fullPage: {
2641
+ type: "boolean",
2642
+ description: "Capture the full scrollable page instead of just the viewport. Use when you need to see below-the-fold content."
2643
+ }
2644
+ }
2480
2645
  }
2481
2646
  },
2482
2647
  {
2483
- name: "searchProductScreenshots",
2484
- description: 'Search for screenshots of real products and apps. Use to find what existing products look like ("stripe dashboard", "linear app", "notion workspace"). Returns image results of actual product UI. Use this for layout and design research on real products, NOT for abstract design inspiration.',
2648
+ name: "runBrowserTest",
2649
+ description: "Run an automated browser test against the live app preview. Use to verify visual implementation: check computed styles, navigate between pages, take analyzed screenshots. Describe what you want to verify and the browser agent handles the interaction.",
2485
2650
  inputSchema: {
2486
2651
  type: "object",
2487
2652
  properties: {
2488
- product: {
2653
+ task: {
2489
2654
  type: "string",
2490
- description: 'The product or app to find screenshots of (e.g., "stripe dashboard", "figma editor", "mercury banking app").'
2655
+ description: 'What to verify, in natural language. E.g., "Check that the hero section cards have border-radius: 24px and the correct rotation angles" or "Navigate to /about and screenshot it".'
2491
2656
  }
2492
2657
  },
2493
- required: ["product"]
2658
+ required: ["task"]
2494
2659
  }
2495
2660
  },
2496
2661
  {
@@ -2519,22 +2684,14 @@ var DESIGN_EXPERT_TOOLS = [
2519
2684
  }
2520
2685
  }
2521
2686
  ];
2522
- async function executeDesignExpertTool(name, input) {
2687
+ async function executeDesignExpertTool(name, input, context, toolCallId) {
2523
2688
  switch (name) {
2524
2689
  case "screenshot": {
2525
2690
  try {
2526
- const { url } = await sidecarRequest(
2527
- "/screenshot",
2528
- {},
2529
- { timeout: 12e4 }
2530
- );
2531
- const analysisPrompt = input.prompt || "Describe this app screenshot for a visual designer reviewing the current state. What is visible: layout, typography, colors, spacing, imagery. Note anything that looks broken or off. Be concise.";
2532
- const analysis = await runCli(
2533
- `mindstudio analyze-image --prompt ${JSON.stringify(analysisPrompt)} --image-url ${JSON.stringify(url)} --output-key analysis --no-meta`
2534
- );
2535
- return `Screenshot: ${url}
2536
-
2537
- ${analysis}`;
2691
+ return await captureAndAnalyzeScreenshot({
2692
+ prompt: input.prompt,
2693
+ fullPage: input.fullPage
2694
+ });
2538
2695
  } catch (err) {
2539
2696
  return `Error taking screenshot: ${err.message}`;
2540
2697
  }
@@ -2573,12 +2730,6 @@ ${analysis}`;
2573
2730
 
2574
2731
  ${analysis}`;
2575
2732
  }
2576
- case "searchProductScreenshots": {
2577
- const query = `${input.product} product screenshot UI 2026`;
2578
- return runCli(
2579
- `mindstudio search-google-images --query ${JSON.stringify(query)} --export-type json --output-key images --no-meta`
2580
- );
2581
- }
2582
2733
  case "generateImages": {
2583
2734
  const prompts = input.prompts;
2584
2735
  const width = input.width || 2048;
@@ -2594,7 +2745,8 @@ ${analysis}`;
2594
2745
  }
2595
2746
  });
2596
2747
  const url = await runCli(
2597
- `mindstudio generate-image '${step}' --output-key imageUrl --no-meta`
2748
+ `mindstudio generate-image '${step}' --output-key imageUrl --no-meta`,
2749
+ { jsonLogs: true }
2598
2750
  );
2599
2751
  imageUrls = [url];
2600
2752
  } else {
@@ -2609,7 +2761,8 @@ ${analysis}`;
2609
2761
  }
2610
2762
  }));
2611
2763
  const batchResult = await runCli(
2612
- `mindstudio batch '${JSON.stringify(steps)}' --no-meta`
2764
+ `mindstudio batch '${JSON.stringify(steps)}' --no-meta`,
2765
+ { jsonLogs: true }
2613
2766
  );
2614
2767
  try {
2615
2768
  const parsed = JSON.parse(batchResult);
@@ -2620,20 +2773,30 @@ ${analysis}`;
2620
2773
  return batchResult;
2621
2774
  }
2622
2775
  }
2623
- const analyses = await Promise.all(
2776
+ const images = await Promise.all(
2624
2777
  imageUrls.map(async (url, i) => {
2625
2778
  if (url.startsWith("Error")) {
2626
- return `Image ${i + 1}: ${url}`;
2779
+ return { prompt: prompts[i], error: url };
2627
2780
  }
2628
2781
  const analysis = await runCli(
2629
2782
  `mindstudio analyze-image --prompt ${JSON.stringify(ANALYZE_PROMPT)} --image-url ${JSON.stringify(url)} --output-key analysis --no-meta`
2630
2783
  );
2631
- return `**Image ${i + 1}:** ${url}
2632
- Prompt: ${prompts[i]}
2633
- Analysis: ${analysis}`;
2784
+ return { url, prompt: prompts[i], analysis, width, height };
2634
2785
  })
2635
2786
  );
2636
- return analyses.join("\n\n");
2787
+ return `%%JSON%%${JSON.stringify({ images })}`;
2788
+ }
2789
+ case "runBrowserTest": {
2790
+ if (!context) {
2791
+ return "Error: browser testing requires execution context (only available in headless mode)";
2792
+ }
2793
+ return browserAutomationTool.execute(
2794
+ { task: input.task },
2795
+ {
2796
+ ...context,
2797
+ toolCallId: toolCallId || context.toolCallId
2798
+ }
2799
+ );
2637
2800
  }
2638
2801
  default:
2639
2802
  return `Error: unknown tool "${name}"`;
@@ -2641,17 +2804,17 @@ Analysis: ${analysis}`;
2641
2804
  }
2642
2805
 
2643
2806
  // src/subagents/designExpert/prompt.ts
2644
- import fs15 from "fs";
2645
- import path9 from "path";
2807
+ import fs16 from "fs";
2808
+ import path10 from "path";
2646
2809
 
2647
2810
  // src/subagents/common/context.ts
2648
- import fs14 from "fs";
2649
- import path8 from "path";
2811
+ import fs15 from "fs";
2812
+ import path9 from "path";
2650
2813
  function walkMdFiles2(dir, skip) {
2651
2814
  const files = [];
2652
2815
  try {
2653
- for (const entry of fs14.readdirSync(dir, { withFileTypes: true })) {
2654
- const full = path8.join(dir, entry.name);
2816
+ for (const entry of fs15.readdirSync(dir, { withFileTypes: true })) {
2817
+ const full = path9.join(dir, entry.name);
2655
2818
  if (entry.isDirectory()) {
2656
2819
  if (!skip?.has(entry.name)) {
2657
2820
  files.push(...walkMdFiles2(full, skip));
@@ -2671,7 +2834,7 @@ function loadFilesAsXml(dir, tag, skip) {
2671
2834
  }
2672
2835
  const sections = files.map((f) => {
2673
2836
  try {
2674
- const content = fs14.readFileSync(f, "utf-8").trim();
2837
+ const content = fs15.readFileSync(f, "utf-8").trim();
2675
2838
  return `<file path="${f}">
2676
2839
  ${content}
2677
2840
  </file>`;
@@ -2741,17 +2904,17 @@ The first-party SDK (@mindstudio-ai/agent) provides access to 200+ AI models (Op
2741
2904
  }
2742
2905
 
2743
2906
  // src/subagents/designExpert/prompt.ts
2744
- var base2 = import.meta.dirname ?? path9.dirname(new URL(import.meta.url).pathname);
2745
- function resolvePath(filename) {
2746
- const local4 = path9.join(base2, filename);
2747
- return fs15.existsSync(local4) ? local4 : path9.join(base2, "subagents", "designExpert", filename);
2907
+ var base3 = import.meta.dirname ?? path10.dirname(new URL(import.meta.url).pathname);
2908
+ function resolvePath2(filename) {
2909
+ const local4 = path10.join(base3, filename);
2910
+ return fs16.existsSync(local4) ? local4 : path10.join(base3, "subagents", "designExpert", filename);
2748
2911
  }
2749
2912
  function readFile(filename) {
2750
- return fs15.readFileSync(resolvePath(filename), "utf-8").trim();
2913
+ return fs16.readFileSync(resolvePath2(filename), "utf-8").trim();
2751
2914
  }
2752
2915
  function readJson(filename, fallback) {
2753
2916
  try {
2754
- return JSON.parse(fs15.readFileSync(resolvePath(filename), "utf-8"));
2917
+ return JSON.parse(fs16.readFileSync(resolvePath2(filename), "utf-8"));
2755
2918
  } catch {
2756
2919
  return fallback;
2757
2920
  }
@@ -2788,7 +2951,6 @@ function getDesignExpertPrompt() {
2788
2951
  const pairings = sample(fontData.pairings, 20);
2789
2952
  const images = sample(inspirationImages, 15);
2790
2953
  const fontList = fonts.map((f) => {
2791
- const tags = f.tags.length ? ` (${f.tags.join(", ")})` : "";
2792
2954
  let cssInfo = "";
2793
2955
  if (f.source === "fontshare") {
2794
2956
  cssInfo = ` CSS: ${fontData.cssUrlPattern.replace("{slug}", f.slug).replace("{weights}", f.weights.join(","))}`;
@@ -2797,7 +2959,8 @@ function getDesignExpertPrompt() {
2797
2959
  } else if (f.source === "open-foundry") {
2798
2960
  cssInfo = " (self-host required)";
2799
2961
  }
2800
- return `- **${f.name}** \u2014 ${f.category}${tags}. Weights: ${f.weights.join(", ")}.${f.variable ? " Variable." : ""}${f.italics ? " Has italics." : ""}${cssInfo}`;
2962
+ const desc = f.description ? ` ${f.description}` : "";
2963
+ return `- **${f.name}** \u2014 ${f.category}. Weights: ${f.weights.join(", ")}.${f.variable ? " Variable." : ""}${f.italics ? " Has italics." : ""}${cssInfo}${desc}`;
2801
2964
  }).join("\n");
2802
2965
  const pairingList = pairings.map(
2803
2966
  (p) => `- **${p.heading.font}** (${p.heading.weight}) heading + **${p.body.font}** (${p.body.weight}) body`
@@ -2815,13 +2978,13 @@ ${fontList}
2815
2978
  ${pairingList}
2816
2979
  </fonts_to_consider>` : "";
2817
2980
  const imageList = images.map((img) => `- ${img.analysis}`).join("\n\n");
2818
- const inspirationSection = images.length ? `<inspiration_images>
2981
+ const inspirationSection = images.length ? `<design_inspiration>
2819
2982
  ## Design inspiration
2820
2983
 
2821
- This is what the bar looks like. These are real sites that made it onto curated design galleries because they did something bold, intentional, and memorable. Study the moves they make \u2014 the confident color choices, the unexpected layouts, the typography that carries the whole page. Your recommendations should feel like they belong in this company.
2984
+ This is what the bar looks like. These are real sites that made it onto curated design galleries because they did something bold, intentional, and memorable. Use them as inspiration and let the takeaways guide your work. Your designs should feel like they belong in this company.
2822
2985
 
2823
2986
  ${imageList}
2824
- </inspiration_images>` : "";
2987
+ </design_inspiration>` : "";
2825
2988
  const specContext = loadSpecContext();
2826
2989
  let prompt = PROMPT_TEMPLATE.replace(
2827
2990
  "{{fonts_to_consider}}",
@@ -2863,7 +3026,7 @@ var designExpertTool = {
2863
3026
  task: input.task,
2864
3027
  tools: DESIGN_EXPERT_TOOLS,
2865
3028
  externalTools: /* @__PURE__ */ new Set(),
2866
- executeTool: executeDesignExpertTool,
3029
+ executeTool: (name, input2, toolCallId) => executeDesignExpertTool(name, input2, context, toolCallId),
2867
3030
  apiConfig: context.apiConfig,
2868
3031
  model: context.model,
2869
3032
  subAgentId: "visualDesignExpert",
@@ -2976,8 +3139,8 @@ var VISION_TOOLS = [
2976
3139
  ];
2977
3140
 
2978
3141
  // src/subagents/productVision/executor.ts
2979
- import fs16 from "fs";
2980
- import path10 from "path";
3142
+ import fs17 from "fs";
3143
+ import path11 from "path";
2981
3144
  var ROADMAP_DIR = "src/roadmap";
2982
3145
  function formatRequires(requires) {
2983
3146
  return requires.length === 0 ? "[]" : `[${requires.map((r) => `"${r}"`).join(", ")}]`;
@@ -2993,9 +3156,10 @@ async function executeVisionTool(name, input) {
2993
3156
  requires,
2994
3157
  body
2995
3158
  } = input;
2996
- const filePath = path10.join(ROADMAP_DIR, `${slug}.md`);
3159
+ const filePath = path11.join(ROADMAP_DIR, `${slug}.md`);
2997
3160
  try {
2998
- fs16.mkdirSync(ROADMAP_DIR, { recursive: true });
3161
+ fs17.mkdirSync(ROADMAP_DIR, { recursive: true });
3162
+ const oldContent = fs17.existsSync(filePath) ? fs17.readFileSync(filePath, "utf-8") : "";
2999
3163
  const content = `---
3000
3164
  name: ${itemName}
3001
3165
  type: roadmap
@@ -3007,20 +3171,24 @@ requires: ${formatRequires(requires)}
3007
3171
 
3008
3172
  ${body}
3009
3173
  `;
3010
- fs16.writeFileSync(filePath, content, "utf-8");
3011
- return `Wrote ${filePath}`;
3174
+ fs17.writeFileSync(filePath, content, "utf-8");
3175
+ const lineCount = content.split("\n").length;
3176
+ const label = oldContent ? "Updated" : "Wrote";
3177
+ return `${label} ${filePath} (${lineCount} lines)
3178
+ ${unifiedDiff(filePath, oldContent, content)}`;
3012
3179
  } catch (err) {
3013
3180
  return `Error writing ${filePath}: ${err.message}`;
3014
3181
  }
3015
3182
  }
3016
3183
  case "updateRoadmapItem": {
3017
3184
  const { slug } = input;
3018
- const filePath = path10.join(ROADMAP_DIR, `${slug}.md`);
3185
+ const filePath = path11.join(ROADMAP_DIR, `${slug}.md`);
3019
3186
  try {
3020
- if (!fs16.existsSync(filePath)) {
3187
+ if (!fs17.existsSync(filePath)) {
3021
3188
  return `Error: ${filePath} does not exist`;
3022
3189
  }
3023
- let content = fs16.readFileSync(filePath, "utf-8");
3190
+ const oldContent = fs17.readFileSync(filePath, "utf-8");
3191
+ let content = oldContent;
3024
3192
  if (input.status) {
3025
3193
  content = content.replace(
3026
3194
  /^status:\s*.+$/m,
@@ -3072,21 +3240,25 @@ ${input.appendHistory}
3072
3240
  `;
3073
3241
  }
3074
3242
  }
3075
- fs16.writeFileSync(filePath, content, "utf-8");
3076
- return `Updated ${filePath}`;
3243
+ fs17.writeFileSync(filePath, content, "utf-8");
3244
+ const lineCount = content.split("\n").length;
3245
+ return `Updated ${filePath} (${lineCount} lines)
3246
+ ${unifiedDiff(filePath, oldContent, content)}`;
3077
3247
  } catch (err) {
3078
3248
  return `Error updating ${filePath}: ${err.message}`;
3079
3249
  }
3080
3250
  }
3081
3251
  case "deleteRoadmapItem": {
3082
3252
  const { slug } = input;
3083
- const filePath = path10.join(ROADMAP_DIR, `${slug}.md`);
3253
+ const filePath = path11.join(ROADMAP_DIR, `${slug}.md`);
3084
3254
  try {
3085
- if (!fs16.existsSync(filePath)) {
3255
+ if (!fs17.existsSync(filePath)) {
3086
3256
  return `Error: ${filePath} does not exist`;
3087
3257
  }
3088
- fs16.unlinkSync(filePath);
3089
- return `Deleted ${filePath}`;
3258
+ const oldContent = fs17.readFileSync(filePath, "utf-8");
3259
+ fs17.unlinkSync(filePath);
3260
+ return `Deleted ${filePath}
3261
+ ${unifiedDiff(filePath, oldContent, "")}`;
3090
3262
  } catch (err) {
3091
3263
  return `Error deleting ${filePath}: ${err.message}`;
3092
3264
  }
@@ -3097,12 +3269,12 @@ ${input.appendHistory}
3097
3269
  }
3098
3270
 
3099
3271
  // src/subagents/productVision/prompt.ts
3100
- import fs17 from "fs";
3101
- import path11 from "path";
3102
- var base3 = import.meta.dirname ?? path11.dirname(new URL(import.meta.url).pathname);
3103
- var local2 = path11.join(base3, "prompt.md");
3104
- var PROMPT_PATH2 = fs17.existsSync(local2) ? local2 : path11.join(base3, "subagents", "productVision", "prompt.md");
3105
- var BASE_PROMPT2 = fs17.readFileSync(PROMPT_PATH2, "utf-8").trim();
3272
+ import fs18 from "fs";
3273
+ import path12 from "path";
3274
+ var base4 = import.meta.dirname ?? path12.dirname(new URL(import.meta.url).pathname);
3275
+ var local2 = path12.join(base4, "prompt.md");
3276
+ var PROMPT_PATH2 = fs18.existsSync(local2) ? local2 : path12.join(base4, "subagents", "productVision", "prompt.md");
3277
+ var BASE_PROMPT2 = fs18.readFileSync(PROMPT_PATH2, "utf-8").trim();
3106
3278
  function getProductVisionPrompt() {
3107
3279
  const specContext = loadSpecContext();
3108
3280
  const roadmapContext = loadRoadmapContext();
@@ -3156,8 +3328,8 @@ var productVisionTool = {
3156
3328
  };
3157
3329
 
3158
3330
  // src/subagents/codeSanityCheck/index.ts
3159
- import fs18 from "fs";
3160
- import path12 from "path";
3331
+ import fs19 from "fs";
3332
+ import path13 from "path";
3161
3333
 
3162
3334
  // src/subagents/codeSanityCheck/tools.ts
3163
3335
  var SANITY_CHECK_TOOLS = [
@@ -3251,10 +3423,10 @@ var SANITY_CHECK_TOOLS = [
3251
3423
  ];
3252
3424
 
3253
3425
  // src/subagents/codeSanityCheck/index.ts
3254
- var base4 = import.meta.dirname ?? path12.dirname(new URL(import.meta.url).pathname);
3255
- var local3 = path12.join(base4, "prompt.md");
3256
- var PROMPT_PATH3 = fs18.existsSync(local3) ? local3 : path12.join(base4, "subagents", "codeSanityCheck", "prompt.md");
3257
- var BASE_PROMPT3 = fs18.readFileSync(PROMPT_PATH3, "utf-8").trim();
3426
+ var base5 = import.meta.dirname ?? path13.dirname(new URL(import.meta.url).pathname);
3427
+ var local3 = path13.join(base5, "prompt.md");
3428
+ var PROMPT_PATH3 = fs19.existsSync(local3) ? local3 : path13.join(base5, "subagents", "codeSanityCheck", "prompt.md");
3429
+ var BASE_PROMPT3 = fs19.readFileSync(PROMPT_PATH3, "utf-8").trim();
3258
3430
  var codeSanityCheckTool = {
3259
3431
  definition: {
3260
3432
  name: "codeSanityCheck",
@@ -3381,11 +3553,11 @@ function executeTool(name, input, context) {
3381
3553
  }
3382
3554
 
3383
3555
  // src/session.ts
3384
- import fs19 from "fs";
3556
+ import fs20 from "fs";
3385
3557
  var SESSION_FILE = ".remy-session.json";
3386
3558
  function loadSession(state) {
3387
3559
  try {
3388
- const raw = fs19.readFileSync(SESSION_FILE, "utf-8");
3560
+ const raw = fs20.readFileSync(SESSION_FILE, "utf-8");
3389
3561
  const data = JSON.parse(raw);
3390
3562
  if (Array.isArray(data.messages) && data.messages.length > 0) {
3391
3563
  state.messages = sanitizeMessages(data.messages);
@@ -3433,7 +3605,7 @@ function sanitizeMessages(messages) {
3433
3605
  }
3434
3606
  function saveSession(state) {
3435
3607
  try {
3436
- fs19.writeFileSync(
3608
+ fs20.writeFileSync(
3437
3609
  SESSION_FILE,
3438
3610
  JSON.stringify({ messages: state.messages }, null, 2),
3439
3611
  "utf-8"
@@ -3444,7 +3616,7 @@ function saveSession(state) {
3444
3616
  function clearSession(state) {
3445
3617
  state.messages = [];
3446
3618
  try {
3447
- fs19.unlinkSync(SESSION_FILE);
3619
+ fs20.unlinkSync(SESSION_FILE);
3448
3620
  } catch {
3449
3621
  }
3450
3622
  }
@@ -4151,13 +4323,46 @@ async function runTurn(params) {
4151
4323
  }
4152
4324
 
4153
4325
  // src/headless.ts
4154
- var BASE_DIR = import.meta.dirname ?? path13.dirname(new URL(import.meta.url).pathname);
4155
- var ACTIONS_DIR = path13.join(BASE_DIR, "actions");
4326
+ var BASE_DIR = import.meta.dirname ?? path14.dirname(new URL(import.meta.url).pathname);
4327
+ var ACTIONS_DIR = path14.join(BASE_DIR, "actions");
4156
4328
  function loadActionPrompt(name) {
4157
- return fs20.readFileSync(path13.join(ACTIONS_DIR, `${name}.md`), "utf-8").trim();
4329
+ return fs21.readFileSync(path14.join(ACTIONS_DIR, `${name}.md`), "utf-8").trim();
4158
4330
  }
4159
- function emit(event, data) {
4160
- process.stdout.write(JSON.stringify({ event, ...data }) + "\n");
4331
+ function emit(event, data, requestId) {
4332
+ const payload = { event, ...data };
4333
+ if (requestId) {
4334
+ payload.requestId = requestId;
4335
+ }
4336
+ process.stdout.write(JSON.stringify(payload) + "\n");
4337
+ }
4338
+ function handleGetHistory(state) {
4339
+ return { messages: state.messages };
4340
+ }
4341
+ function handleClear(state) {
4342
+ clearSession(state);
4343
+ return {};
4344
+ }
4345
+ function handleCancel(currentAbort, pendingTools) {
4346
+ if (currentAbort) {
4347
+ currentAbort.abort();
4348
+ }
4349
+ for (const [id, pending] of pendingTools) {
4350
+ clearTimeout(pending.timeout);
4351
+ pending.resolve("Error: cancelled");
4352
+ pendingTools.delete(id);
4353
+ }
4354
+ return {};
4355
+ }
4356
+ function dispatchSimple(requestId, eventName, handler) {
4357
+ try {
4358
+ const data = handler();
4359
+ if (eventName) {
4360
+ emit(eventName, data, requestId);
4361
+ }
4362
+ emit("completed", { success: true }, requestId);
4363
+ } catch (err) {
4364
+ emit("completed", { success: false, error: err.message }, requestId);
4365
+ }
4161
4366
  }
4162
4367
  async function startHeadless(opts = {}) {
4163
4368
  const stderrWrite = (...args) => {
@@ -4176,72 +4381,15 @@ async function startHeadless(opts = {}) {
4176
4381
  const state = createAgentState();
4177
4382
  const resumed = loadSession(state);
4178
4383
  if (resumed) {
4179
- emit("session_restored", {
4180
- messageCount: state.messages.length
4181
- });
4384
+ emit("session_restored", { messageCount: state.messages.length });
4182
4385
  }
4183
4386
  let running = false;
4184
4387
  let currentAbort = null;
4388
+ let currentRequestId;
4389
+ let completedEmitted = false;
4185
4390
  const EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
4186
4391
  const pendingTools = /* @__PURE__ */ new Map();
4187
4392
  const earlyResults = /* @__PURE__ */ new Map();
4188
- function onEvent(e) {
4189
- switch (e.type) {
4190
- case "text":
4191
- emit("text", {
4192
- text: e.text,
4193
- ...e.parentToolId && { parentToolId: e.parentToolId }
4194
- });
4195
- break;
4196
- case "thinking":
4197
- emit("thinking", {
4198
- text: e.text,
4199
- ...e.parentToolId && { parentToolId: e.parentToolId }
4200
- });
4201
- break;
4202
- case "tool_input_delta":
4203
- emit("tool_input_delta", {
4204
- id: e.id,
4205
- name: e.name,
4206
- result: e.result,
4207
- ...e.parentToolId && { parentToolId: e.parentToolId }
4208
- });
4209
- break;
4210
- case "tool_start":
4211
- emit("tool_start", {
4212
- id: e.id,
4213
- name: e.name,
4214
- input: e.input,
4215
- ...e.partial && { partial: true },
4216
- ...e.parentToolId && { parentToolId: e.parentToolId }
4217
- });
4218
- break;
4219
- case "tool_done":
4220
- emit("tool_done", {
4221
- id: e.id,
4222
- name: e.name,
4223
- result: e.result,
4224
- isError: e.isError,
4225
- ...e.parentToolId && { parentToolId: e.parentToolId }
4226
- });
4227
- break;
4228
- case "turn_started":
4229
- emit("turn_started");
4230
- break;
4231
- case "turn_done":
4232
- emit("turn_done");
4233
- break;
4234
- case "turn_cancelled":
4235
- emit("turn_cancelled");
4236
- break;
4237
- case "error":
4238
- emit("error", { error: e.error });
4239
- break;
4240
- case "status":
4241
- emit("status", { message: e.message });
4242
- break;
4243
- }
4244
- }
4245
4393
  const USER_FACING_TOOLS = /* @__PURE__ */ new Set([
4246
4394
  "promptUser",
4247
4395
  "confirmDestructiveAction",
@@ -4272,6 +4420,158 @@ async function startHeadless(opts = {}) {
4272
4420
  });
4273
4421
  });
4274
4422
  }
4423
+ function onEvent(e) {
4424
+ const rid = currentRequestId;
4425
+ switch (e.type) {
4426
+ // Suppressed — caller already knows the request started
4427
+ case "turn_started":
4428
+ return;
4429
+ // Terminal events — translate to `completed`
4430
+ case "turn_done":
4431
+ completedEmitted = true;
4432
+ emit("completed", { success: true }, rid);
4433
+ return;
4434
+ case "turn_cancelled":
4435
+ completedEmitted = true;
4436
+ emit("completed", { success: false, error: "cancelled" }, rid);
4437
+ return;
4438
+ // Streaming events — forward with requestId
4439
+ case "text":
4440
+ emit(
4441
+ "text",
4442
+ {
4443
+ text: e.text,
4444
+ ...e.parentToolId && { parentToolId: e.parentToolId }
4445
+ },
4446
+ rid
4447
+ );
4448
+ return;
4449
+ case "thinking":
4450
+ emit(
4451
+ "thinking",
4452
+ {
4453
+ text: e.text,
4454
+ ...e.parentToolId && { parentToolId: e.parentToolId }
4455
+ },
4456
+ rid
4457
+ );
4458
+ return;
4459
+ case "tool_input_delta":
4460
+ emit(
4461
+ "tool_input_delta",
4462
+ {
4463
+ id: e.id,
4464
+ name: e.name,
4465
+ result: e.result,
4466
+ ...e.parentToolId && { parentToolId: e.parentToolId }
4467
+ },
4468
+ rid
4469
+ );
4470
+ return;
4471
+ case "tool_start":
4472
+ emit(
4473
+ "tool_start",
4474
+ {
4475
+ id: e.id,
4476
+ name: e.name,
4477
+ input: e.input,
4478
+ ...e.partial && { partial: true },
4479
+ ...e.parentToolId && { parentToolId: e.parentToolId }
4480
+ },
4481
+ rid
4482
+ );
4483
+ return;
4484
+ case "tool_done":
4485
+ emit(
4486
+ "tool_done",
4487
+ {
4488
+ id: e.id,
4489
+ name: e.name,
4490
+ result: e.result,
4491
+ isError: e.isError,
4492
+ ...e.parentToolId && { parentToolId: e.parentToolId }
4493
+ },
4494
+ rid
4495
+ );
4496
+ return;
4497
+ case "status":
4498
+ emit("status", { message: e.message }, rid);
4499
+ return;
4500
+ case "error":
4501
+ emit("error", { error: e.error }, rid);
4502
+ return;
4503
+ }
4504
+ }
4505
+ async function handleMessage(parsed, requestId) {
4506
+ if (running) {
4507
+ emit(
4508
+ "error",
4509
+ { error: "Agent is already processing a message" },
4510
+ requestId
4511
+ );
4512
+ emit(
4513
+ "completed",
4514
+ { success: false, error: "Agent is already processing a message" },
4515
+ requestId
4516
+ );
4517
+ return;
4518
+ }
4519
+ running = true;
4520
+ currentRequestId = requestId;
4521
+ currentAbort = new AbortController();
4522
+ completedEmitted = false;
4523
+ const attachments = parsed.attachments;
4524
+ if (attachments?.length) {
4525
+ console.warn(
4526
+ `[headless] Message has ${attachments.length} attachment(s):`,
4527
+ attachments.map((a) => a.url)
4528
+ );
4529
+ }
4530
+ let userMessage = parsed.text ?? "";
4531
+ const isCommand = !!parsed.runCommand;
4532
+ if (parsed.runCommand === "sync") {
4533
+ userMessage = loadActionPrompt("sync");
4534
+ } else if (parsed.runCommand === "publish") {
4535
+ userMessage = loadActionPrompt("publish");
4536
+ } else if (parsed.runCommand === "buildFromInitialSpec") {
4537
+ userMessage = loadActionPrompt("buildFromInitialSpec");
4538
+ }
4539
+ const onboardingState = parsed.onboardingState ?? "onboardingFinished";
4540
+ const system = buildSystemPrompt(
4541
+ onboardingState,
4542
+ parsed.viewContext
4543
+ );
4544
+ try {
4545
+ await runTurn({
4546
+ state,
4547
+ userMessage,
4548
+ attachments,
4549
+ apiConfig: config,
4550
+ system,
4551
+ model: opts.model,
4552
+ onboardingState,
4553
+ signal: currentAbort.signal,
4554
+ onEvent,
4555
+ resolveExternalTool,
4556
+ hidden: isCommand
4557
+ });
4558
+ if (!completedEmitted) {
4559
+ emit(
4560
+ "completed",
4561
+ { success: false, error: "Turn ended unexpectedly" },
4562
+ requestId
4563
+ );
4564
+ }
4565
+ } catch (err) {
4566
+ if (!completedEmitted) {
4567
+ emit("error", { error: err.message }, requestId);
4568
+ emit("completed", { success: false, error: err.message }, requestId);
4569
+ }
4570
+ }
4571
+ currentAbort = null;
4572
+ currentRequestId = void 0;
4573
+ running = false;
4574
+ }
4275
4575
  const rl = createInterface({ input: process.stdin });
4276
4576
  rl.on("line", async (line) => {
4277
4577
  let parsed;
@@ -4281,82 +4581,42 @@ async function startHeadless(opts = {}) {
4281
4581
  emit("error", { error: "Invalid JSON on stdin" });
4282
4582
  return;
4283
4583
  }
4284
- if (parsed.action === "tool_result" && parsed.id) {
4285
- const pending = pendingTools.get(parsed.id);
4584
+ const { action, requestId } = parsed;
4585
+ if (action === "tool_result" && parsed.id) {
4586
+ const id = parsed.id;
4587
+ const result = parsed.result ?? "";
4588
+ const pending = pendingTools.get(id);
4286
4589
  if (pending) {
4287
- pendingTools.delete(parsed.id);
4288
- pending.resolve(parsed.result ?? "");
4590
+ pendingTools.delete(id);
4591
+ pending.resolve(result);
4289
4592
  } else {
4290
- earlyResults.set(parsed.id, parsed.result ?? "");
4593
+ earlyResults.set(id, result);
4291
4594
  }
4292
4595
  return;
4293
4596
  }
4294
- if (parsed.action === "get_history") {
4295
- emit("history", {
4296
- messages: state.messages
4297
- });
4597
+ if (action === "get_history") {
4598
+ dispatchSimple(requestId, "history", () => handleGetHistory(state));
4298
4599
  return;
4299
4600
  }
4300
- if (parsed.action === "clear") {
4301
- clearSession(state);
4302
- emit("session_cleared");
4601
+ if (action === "clear") {
4602
+ dispatchSimple(requestId, "session_cleared", () => handleClear(state));
4303
4603
  return;
4304
4604
  }
4305
- if (parsed.action === "cancel") {
4306
- if (currentAbort) {
4307
- currentAbort.abort();
4308
- }
4309
- for (const [id, pending] of pendingTools) {
4310
- clearTimeout(pending.timeout);
4311
- pending.resolve("Error: cancelled");
4312
- pendingTools.delete(id);
4313
- }
4605
+ if (action === "cancel") {
4606
+ handleCancel(currentAbort, pendingTools);
4607
+ emit("completed", { success: true }, requestId);
4314
4608
  return;
4315
4609
  }
4316
- if (parsed.action === "message" && (parsed.text || parsed.runCommand)) {
4317
- if (running) {
4318
- emit("error", { error: "Agent is already processing a message" });
4319
- return;
4320
- }
4321
- running = true;
4322
- currentAbort = new AbortController();
4323
- if (parsed.attachments?.length) {
4324
- console.warn(
4325
- `[headless] Message has ${parsed.attachments.length} attachment(s):`,
4326
- parsed.attachments.map((a) => a.url)
4327
- );
4328
- }
4329
- let userMessage = parsed.text ?? "";
4330
- const isCommand = !!parsed.runCommand;
4331
- if (parsed.runCommand === "sync") {
4332
- userMessage = loadActionPrompt("sync");
4333
- } else if (parsed.runCommand === "publish") {
4334
- userMessage = loadActionPrompt("publish");
4335
- } else if (parsed.runCommand === "buildFromInitialSpec") {
4336
- userMessage = loadActionPrompt("buildFromInitialSpec");
4337
- }
4338
- const onboardingState = parsed.onboardingState ?? "onboardingFinished";
4339
- const system = buildSystemPrompt(onboardingState, parsed.viewContext);
4340
- try {
4341
- await runTurn({
4342
- state,
4343
- userMessage,
4344
- attachments: parsed.attachments,
4345
- apiConfig: config,
4346
- system,
4347
- model: opts.model,
4348
- onboardingState,
4349
- signal: currentAbort.signal,
4350
- onEvent,
4351
- resolveExternalTool,
4352
- hidden: isCommand
4353
- });
4354
- } catch (err) {
4355
- emit("error", { error: err.message });
4356
- }
4357
- currentAbort = null;
4358
- running = false;
4610
+ if (action === "message") {
4611
+ await handleMessage(parsed, requestId);
4612
+ return;
4359
4613
  }
4614
+ emit("error", { error: `Unknown action: ${action}` }, requestId);
4615
+ emit(
4616
+ "completed",
4617
+ { success: false, error: `Unknown action: ${action}` },
4618
+ requestId
4619
+ );
4360
4620
  });
4361
4621
  rl.on("close", () => {
4362
4622
  emit("stopping");