@mindstudio-ai/remy 0.1.26 → 0.1.27

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 +524 -270
  5. package/dist/index.js +567 -300
  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 +1 -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/index.js CHANGED
@@ -904,7 +904,7 @@ var init_promptUser = __esm({
904
904
  }
905
905
  ]
906
906
  },
907
- description: "Options for select and checklist types. Each can be a string or { label, description }."
907
+ 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."
908
908
  },
909
909
  multiple: {
910
910
  type: "boolean",
@@ -1013,27 +1013,75 @@ var init_confirmDestructiveAction = __esm({
1013
1013
  });
1014
1014
 
1015
1015
  // src/subagents/common/runCli.ts
1016
- import { exec } from "child_process";
1016
+ import { spawn } from "child_process";
1017
1017
  function runCli(cmd, options) {
1018
1018
  return new Promise((resolve) => {
1019
- exec(
1020
- cmd,
1021
- {
1022
- timeout: options?.timeout ?? 6e4,
1023
- maxBuffer: options?.maxBuffer ?? 1024 * 1024
1024
- },
1025
- (err, stdout, stderr) => {
1026
- if (stdout.trim()) {
1027
- resolve(stdout.trim());
1028
- return;
1019
+ const timeout = options?.timeout ?? 6e4;
1020
+ const maxBuffer = options?.maxBuffer ?? 1024 * 1024;
1021
+ const cmdWithLogs = options?.jsonLogs && !cmd.includes("--json-logs") ? cmd.replace(/^(mindstudio\s+\S+)/, "$1 --json-logs") : cmd;
1022
+ const child = spawn("sh", ["-c", cmdWithLogs], {
1023
+ stdio: ["ignore", "pipe", "pipe"]
1024
+ });
1025
+ const logs = [];
1026
+ let stdout = "";
1027
+ let stderr = "";
1028
+ let stdoutSize = 0;
1029
+ let stderrSize = 0;
1030
+ let killed = false;
1031
+ child.stdout.on("data", (chunk) => {
1032
+ stdoutSize += chunk.length;
1033
+ if (stdoutSize <= maxBuffer) {
1034
+ stdout += chunk.toString();
1035
+ } else if (!killed) {
1036
+ killed = true;
1037
+ child.kill();
1038
+ }
1039
+ });
1040
+ child.stderr.on("data", (chunk) => {
1041
+ stderrSize += chunk.length;
1042
+ if (stderrSize > maxBuffer) {
1043
+ if (!killed) {
1044
+ killed = true;
1045
+ child.kill();
1029
1046
  }
1030
- if (err) {
1031
- resolve(`Error: ${stderr.trim() || err.message}`);
1032
- return;
1047
+ return;
1048
+ }
1049
+ const text = chunk.toString();
1050
+ stderr += text;
1051
+ for (const line of text.split("\n")) {
1052
+ const trimmed = line.trim();
1053
+ if (!trimmed || trimmed[0] !== "{") {
1054
+ continue;
1055
+ }
1056
+ try {
1057
+ const entry = JSON.parse(trimmed);
1058
+ if (entry.type === "log" && entry.value) {
1059
+ const prefix = entry.tag ? `[${entry.tag}]` : "[log]";
1060
+ logs.push(`${prefix} ${entry.value}`);
1061
+ }
1062
+ } catch {
1033
1063
  }
1034
- resolve("(no response)");
1035
1064
  }
1036
- );
1065
+ });
1066
+ const timer = setTimeout(() => {
1067
+ killed = true;
1068
+ child.kill();
1069
+ }, timeout);
1070
+ child.on("close", (code) => {
1071
+ clearTimeout(timer);
1072
+ const logBlock = logs.length > 0 ? logs.join("\n") + "\n\n" : "";
1073
+ const out = stdout.trim();
1074
+ if (out) {
1075
+ resolve(logBlock + out);
1076
+ return;
1077
+ }
1078
+ if (code !== 0 || killed) {
1079
+ const errMsg = stderr.trim() || (killed ? "Process timed out" : `Exit code ${code}`);
1080
+ resolve(logBlock + `Error: ${errMsg}`);
1081
+ return;
1082
+ }
1083
+ resolve(logBlock + "(no response)");
1084
+ });
1037
1085
  });
1038
1086
  }
1039
1087
  var init_runCli = __esm({
@@ -1496,7 +1544,7 @@ ${unifiedDiff(input.path, content, updated)}`;
1496
1544
  });
1497
1545
 
1498
1546
  // src/tools/code/bash.ts
1499
- import { exec as exec2 } from "child_process";
1547
+ import { exec } from "child_process";
1500
1548
  var DEFAULT_TIMEOUT_MS, DEFAULT_MAX_LINES3, bashTool;
1501
1549
  var init_bash = __esm({
1502
1550
  "src/tools/code/bash.ts"() {
@@ -1534,7 +1582,7 @@ var init_bash = __esm({
1534
1582
  const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES3;
1535
1583
  const timeoutMs = input.timeout ? input.timeout * 1e3 : DEFAULT_TIMEOUT_MS;
1536
1584
  return new Promise((resolve) => {
1537
- exec2(
1585
+ exec(
1538
1586
  input.command,
1539
1587
  {
1540
1588
  timeout: timeoutMs,
@@ -1576,7 +1624,7 @@ var init_bash = __esm({
1576
1624
  });
1577
1625
 
1578
1626
  // src/tools/code/grep.ts
1579
- import { exec as exec3 } from "child_process";
1627
+ import { exec as exec2 } from "child_process";
1580
1628
  function formatResults(stdout, max) {
1581
1629
  const lines = stdout.trim().split("\n");
1582
1630
  let result = lines.join("\n");
@@ -1627,12 +1675,12 @@ var init_grep = __esm({
1627
1675
  const rgCmd = `rg -n --no-heading --max-count=${max}${globFlag} '${escaped}' ${searchPath}`;
1628
1676
  const grepCmd = `grep -rn --max-count=${max} '${escaped}' ${searchPath} --include='*.ts' --include='*.tsx' --include='*.js' --include='*.json' --include='*.md'`;
1629
1677
  return new Promise((resolve) => {
1630
- exec3(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
1678
+ exec2(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
1631
1679
  if (stdout?.trim()) {
1632
1680
  resolve(formatResults(stdout, max));
1633
1681
  return;
1634
1682
  }
1635
- exec3(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
1683
+ exec2(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
1636
1684
  if (grepStdout?.trim()) {
1637
1685
  resolve(formatResults(grepStdout, max));
1638
1686
  } else {
@@ -1966,42 +2014,78 @@ var init_runMethod = __esm({
1966
2014
  }
1967
2015
  });
1968
2016
 
1969
- // src/tools/code/screenshot.ts
1970
- var DEFAULT_PROMPT, screenshotTool;
2017
+ // src/tools/_helpers/screenshot.ts
2018
+ async function captureAndAnalyzeScreenshot(promptOrOptions) {
2019
+ let prompt;
2020
+ let viewportOnly = false;
2021
+ if (typeof promptOrOptions === "object" && promptOrOptions !== null) {
2022
+ prompt = promptOrOptions.prompt;
2023
+ viewportOnly = promptOrOptions.viewportOnly ?? false;
2024
+ } else {
2025
+ prompt = promptOrOptions;
2026
+ }
2027
+ const ssResult = await sidecarRequest(
2028
+ "/screenshot",
2029
+ { fullPage: !viewportOnly },
2030
+ { timeout: 12e4 }
2031
+ );
2032
+ log.debug("Screenshot response", { ssResult });
2033
+ const url = ssResult?.url || ssResult?.screenshotUrl;
2034
+ if (!url) {
2035
+ throw new Error(
2036
+ `No URL in sidecar response. The browser may not be ready yet. Response: ${JSON.stringify(ssResult)}`
2037
+ );
2038
+ }
2039
+ if (prompt === false) {
2040
+ return url;
2041
+ }
2042
+ const analysisPrompt = prompt || SCREENSHOT_ANALYSIS_PROMPT;
2043
+ const analysis = await runCli(
2044
+ `mindstudio analyze-image --prompt ${JSON.stringify(analysisPrompt)} --image-url ${JSON.stringify(url)} --output-key analysis --no-meta`
2045
+ );
2046
+ return JSON.stringify({ url, analysis });
2047
+ }
2048
+ var SCREENSHOT_ANALYSIS_PROMPT;
1971
2049
  var init_screenshot = __esm({
1972
- "src/tools/code/screenshot.ts"() {
2050
+ "src/tools/_helpers/screenshot.ts"() {
1973
2051
  "use strict";
1974
2052
  init_sidecar();
1975
2053
  init_runCli();
1976
- 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.";
2054
+ init_logger();
2055
+ 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).";
2056
+ }
2057
+ });
2058
+
2059
+ // src/tools/code/screenshot.ts
2060
+ var screenshotTool;
2061
+ var init_screenshot2 = __esm({
2062
+ "src/tools/code/screenshot.ts"() {
2063
+ "use strict";
2064
+ init_screenshot();
1977
2065
  screenshotTool = {
1978
2066
  definition: {
1979
2067
  name: "screenshot",
1980
- 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.",
2068
+ 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. Set viewportOnly to capture just what the user sees on screen.",
1981
2069
  inputSchema: {
1982
2070
  type: "object",
1983
2071
  properties: {
1984
2072
  prompt: {
1985
2073
  type: "string",
1986
2074
  description: "Optional question about the screenshot. If omitted, returns a general description of what's visible."
2075
+ },
2076
+ viewportOnly: {
2077
+ type: "boolean",
2078
+ description: "Capture only the visible viewport instead of the full scrollable page. Use when checking above-the-fold layout or viewport-relative sizing like 100vh."
1987
2079
  }
1988
2080
  }
1989
2081
  }
1990
2082
  },
1991
2083
  async execute(input) {
1992
2084
  try {
1993
- const { url } = await sidecarRequest(
1994
- "/screenshot",
1995
- {},
1996
- { timeout: 12e4 }
1997
- );
1998
- const analysisPrompt = input.prompt || DEFAULT_PROMPT;
1999
- const analysis = await runCli(
2000
- `mindstudio analyze-image --prompt ${JSON.stringify(analysisPrompt)} --image-url ${JSON.stringify(url)} --output-key analysis --no-meta`
2001
- );
2002
- return `Screenshot: ${url}
2003
-
2004
- ${analysis}`;
2085
+ return await captureAndAnalyzeScreenshot({
2086
+ prompt: input.prompt,
2087
+ viewportOnly: input.viewportOnly
2088
+ });
2005
2089
  } catch (err) {
2006
2090
  return `Error taking screenshot: ${err.message}`;
2007
2091
  }
@@ -2076,7 +2160,9 @@ async function runSubAgent(config) {
2076
2160
  ...apiConfig,
2077
2161
  model,
2078
2162
  subAgentId,
2079
- system,
2163
+ system: `${system}
2164
+
2165
+ Current date/time: ${(/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC")}`,
2080
2166
  messages: cleanMessagesForApi(messages),
2081
2167
  tools,
2082
2168
  signal
@@ -2196,6 +2282,13 @@ async function runSubAgent(config) {
2196
2282
  })
2197
2283
  );
2198
2284
  for (const r of results) {
2285
+ const block = contentBlocks.find(
2286
+ (b) => b.type === "tool" && b.id === r.id
2287
+ );
2288
+ if (block?.type === "tool") {
2289
+ block.result = r.result;
2290
+ block.isError = r.isError;
2291
+ }
2199
2292
  messages.push({
2200
2293
  role: "user",
2201
2294
  content: r.result,
@@ -2233,8 +2326,18 @@ var init_tools = __esm({
2233
2326
  properties: {
2234
2327
  command: {
2235
2328
  type: "string",
2236
- enum: ["snapshot", "click", "type", "wait", "evaluate"],
2237
- 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."
2329
+ enum: [
2330
+ "snapshot",
2331
+ "click",
2332
+ "type",
2333
+ "select",
2334
+ "wait",
2335
+ "navigate",
2336
+ "evaluate",
2337
+ "styles",
2338
+ "screenshot"
2339
+ ],
2340
+ 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)."
2238
2341
  },
2239
2342
  ref: {
2240
2343
  type: "string",
@@ -2256,6 +2359,10 @@ var init_tools = __esm({
2256
2359
  type: "string",
2257
2360
  description: "CSS selector fallback (last resort)."
2258
2361
  },
2362
+ option: {
2363
+ type: "string",
2364
+ description: "For select: the option text to select from a dropdown."
2365
+ },
2259
2366
  clear: {
2260
2367
  type: "boolean",
2261
2368
  description: "For type: clear the field before typing."
@@ -2267,6 +2374,15 @@ var init_tools = __esm({
2267
2374
  script: {
2268
2375
  type: "string",
2269
2376
  description: "For evaluate: JavaScript to run in the page."
2377
+ },
2378
+ url: {
2379
+ type: "string",
2380
+ description: 'For navigate: the URL to navigate to (e.g., "/quiz", "/settings").'
2381
+ },
2382
+ properties: {
2383
+ type: "array",
2384
+ items: { type: "string" },
2385
+ description: 'For styles: camelCase CSS property names to read (e.g., ["backgroundColor", "borderRadius", "fontSize"]). Omit for a default set.'
2270
2386
  }
2271
2387
  },
2272
2388
  required: ["command"]
@@ -2293,7 +2409,7 @@ var init_tools = __esm({
2293
2409
  }
2294
2410
  }
2295
2411
  ];
2296
- BROWSER_EXTERNAL_TOOLS = /* @__PURE__ */ new Set(["browserCommand", "screenshot"]);
2412
+ BROWSER_EXTERNAL_TOOLS = /* @__PURE__ */ new Set(["browserCommand"]);
2297
2413
  }
2298
2414
  });
2299
2415
 
@@ -2332,16 +2448,19 @@ var init_browserAutomation = __esm({
2332
2448
  init_tools();
2333
2449
  init_prompt();
2334
2450
  init_sidecar();
2451
+ init_screenshot();
2452
+ init_runCli();
2453
+ init_logger();
2335
2454
  browserAutomationTool = {
2336
2455
  definition: {
2337
2456
  name: "runAutomatedBrowserTest",
2338
- 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.",
2457
+ 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.",
2339
2458
  inputSchema: {
2340
2459
  type: "object",
2341
2460
  properties: {
2342
2461
  task: {
2343
2462
  type: "string",
2344
- description: "What to test, in natural language. Include how to navigate to the relevant page and what data/roles to expect."
2463
+ 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."
2345
2464
  }
2346
2465
  },
2347
2466
  required: ["task"]
@@ -2369,6 +2488,13 @@ var init_browserAutomation = __esm({
2369
2488
  tools: BROWSER_TOOLS,
2370
2489
  externalTools: BROWSER_EXTERNAL_TOOLS,
2371
2490
  executeTool: async (name) => {
2491
+ if (name === "screenshot") {
2492
+ try {
2493
+ return await captureAndAnalyzeScreenshot();
2494
+ } catch (err) {
2495
+ return `Error taking screenshot: ${err.message}`;
2496
+ }
2497
+ }
2372
2498
  if (name === "resetBrowser") {
2373
2499
  try {
2374
2500
  await sidecarRequest("/reset-browser", {}, { timeout: 5e3 });
@@ -2385,7 +2511,50 @@ var init_browserAutomation = __esm({
2385
2511
  signal: context.signal,
2386
2512
  parentToolId: context.toolCallId,
2387
2513
  onEvent: context.onEvent,
2388
- resolveExternalTool: context.resolveExternalTool
2514
+ resolveExternalTool: async (id, name, input2) => {
2515
+ if (!context.resolveExternalTool) {
2516
+ return "Error: no external tool resolver";
2517
+ }
2518
+ const result2 = await context.resolveExternalTool(id, name, input2);
2519
+ if (name === "browserCommand") {
2520
+ try {
2521
+ const parsed = JSON.parse(result2);
2522
+ const screenshotSteps = (parsed.steps || []).filter(
2523
+ (s) => s.command === "screenshot" && s.result?.url
2524
+ );
2525
+ if (screenshotSteps.length > 0) {
2526
+ const batchInput = screenshotSteps.map((s) => ({
2527
+ stepType: "analyzeImage",
2528
+ step: {
2529
+ imageUrl: s.result.url,
2530
+ prompt: SCREENSHOT_ANALYSIS_PROMPT
2531
+ }
2532
+ }));
2533
+ const batchResult = await runCli(
2534
+ `mindstudio batch --no-meta ${JSON.stringify(JSON.stringify(batchInput))}`,
2535
+ { timeout: 12e4 }
2536
+ );
2537
+ try {
2538
+ const analyses = JSON.parse(batchResult);
2539
+ let ai = 0;
2540
+ for (const step of parsed.steps) {
2541
+ if (step.command === "screenshot" && step.result?.url && ai < analyses.length) {
2542
+ step.result.analysis = analyses[ai]?.output?.analysis || analyses[ai]?.output || "";
2543
+ ai++;
2544
+ }
2545
+ }
2546
+ } catch {
2547
+ log.debug("Failed to parse batch analysis result", {
2548
+ batchResult
2549
+ });
2550
+ }
2551
+ return JSON.stringify(parsed);
2552
+ }
2553
+ } catch {
2554
+ }
2555
+ }
2556
+ return result2;
2557
+ }
2389
2558
  });
2390
2559
  context.subAgentMessages?.set(context.toolCallId, result.messages);
2391
2560
  return result.text;
@@ -2395,22 +2564,20 @@ var init_browserAutomation = __esm({
2395
2564
  });
2396
2565
 
2397
2566
  // src/subagents/designExpert/tools.ts
2398
- async function executeDesignExpertTool(name, input) {
2567
+ import fs11 from "fs";
2568
+ import path5 from "path";
2569
+ function resolvePath(filename) {
2570
+ const local4 = path5.join(base2, filename);
2571
+ return fs11.existsSync(local4) ? local4 : path5.join(base2, "subagents", "designExpert", filename);
2572
+ }
2573
+ async function executeDesignExpertTool(name, input, context) {
2399
2574
  switch (name) {
2400
2575
  case "screenshot": {
2401
2576
  try {
2402
- const { url } = await sidecarRequest(
2403
- "/screenshot",
2404
- {},
2405
- { timeout: 12e4 }
2406
- );
2407
- 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.";
2408
- const analysis = await runCli(
2409
- `mindstudio analyze-image --prompt ${JSON.stringify(analysisPrompt)} --image-url ${JSON.stringify(url)} --output-key analysis --no-meta`
2410
- );
2411
- return `Screenshot: ${url}
2412
-
2413
- ${analysis}`;
2577
+ return await captureAndAnalyzeScreenshot({
2578
+ prompt: input.prompt,
2579
+ viewportOnly: input.viewportOnly
2580
+ });
2414
2581
  } catch (err) {
2415
2582
  return `Error taking screenshot: ${err.message}`;
2416
2583
  }
@@ -2449,12 +2616,6 @@ ${analysis}`;
2449
2616
 
2450
2617
  ${analysis}`;
2451
2618
  }
2452
- case "searchProductScreenshots": {
2453
- const query = `${input.product} product screenshot UI 2026`;
2454
- return runCli(
2455
- `mindstudio search-google-images --query ${JSON.stringify(query)} --export-type json --output-key images --no-meta`
2456
- );
2457
- }
2458
2619
  case "generateImages": {
2459
2620
  const prompts = input.prompts;
2460
2621
  const width = input.width || 2048;
@@ -2470,7 +2631,8 @@ ${analysis}`;
2470
2631
  }
2471
2632
  });
2472
2633
  const url = await runCli(
2473
- `mindstudio generate-image '${step}' --output-key imageUrl --no-meta`
2634
+ `mindstudio generate-image '${step}' --output-key imageUrl --no-meta`,
2635
+ { jsonLogs: true }
2474
2636
  );
2475
2637
  imageUrls = [url];
2476
2638
  } else {
@@ -2485,7 +2647,8 @@ ${analysis}`;
2485
2647
  }
2486
2648
  }));
2487
2649
  const batchResult = await runCli(
2488
- `mindstudio batch '${JSON.stringify(steps)}' --no-meta`
2650
+ `mindstudio batch '${JSON.stringify(steps)}' --no-meta`,
2651
+ { jsonLogs: true }
2489
2652
  );
2490
2653
  try {
2491
2654
  const parsed = JSON.parse(batchResult);
@@ -2496,38 +2659,38 @@ ${analysis}`;
2496
2659
  return batchResult;
2497
2660
  }
2498
2661
  }
2499
- const analyses = await Promise.all(
2662
+ const images = await Promise.all(
2500
2663
  imageUrls.map(async (url, i) => {
2501
2664
  if (url.startsWith("Error")) {
2502
- return `Image ${i + 1}: ${url}`;
2665
+ return { prompt: prompts[i], error: url };
2503
2666
  }
2504
2667
  const analysis = await runCli(
2505
2668
  `mindstudio analyze-image --prompt ${JSON.stringify(ANALYZE_PROMPT)} --image-url ${JSON.stringify(url)} --output-key analysis --no-meta`
2506
2669
  );
2507
- return `**Image ${i + 1}:** ${url}
2508
- Prompt: ${prompts[i]}
2509
- Analysis: ${analysis}`;
2670
+ return { url, prompt: prompts[i], analysis, width, height };
2510
2671
  })
2511
2672
  );
2512
- return analyses.join("\n\n");
2673
+ return `%%JSON%%${JSON.stringify({ images })}`;
2674
+ }
2675
+ case "runBrowserTest": {
2676
+ if (!context) {
2677
+ return "Error: browser testing requires execution context (only available in headless mode)";
2678
+ }
2679
+ return browserAutomationTool.execute({ task: input.task }, context);
2513
2680
  }
2514
2681
  default:
2515
2682
  return `Error: unknown tool "${name}"`;
2516
2683
  }
2517
2684
  }
2518
- var DESIGN_REFERENCE_PROMPT, DESIGN_EXPERT_TOOLS;
2685
+ var base2, DESIGN_REFERENCE_PROMPT, DESIGN_EXPERT_TOOLS;
2519
2686
  var init_tools2 = __esm({
2520
2687
  "src/subagents/designExpert/tools.ts"() {
2521
2688
  "use strict";
2522
2689
  init_runCli();
2523
- init_sidecar();
2524
- DESIGN_REFERENCE_PROMPT = `Analyze this website/app screenshot as a design reference. Assess:
2525
- 1) Mood/aesthetic
2526
- 2) Color palette with approximate hex values and palette strategy
2527
- 3) Typography style
2528
- 4) Layout composition (symmetric/asymmetric, grid structure, whitespace usage, content density)
2529
- 5) What makes it distinctive and interesting vs generic AI-generated interfaces
2530
- Be specific and concise.`;
2690
+ init_screenshot();
2691
+ init_browserAutomation();
2692
+ base2 = import.meta.dirname ?? path5.dirname(new URL(import.meta.url).pathname);
2693
+ DESIGN_REFERENCE_PROMPT = fs11.readFileSync(resolvePath("prompts/tool-prompts/design-analysis.md"), "utf-8").trim();
2531
2694
  DESIGN_EXPERT_TOOLS = [
2532
2695
  {
2533
2696
  name: "searchGoogle",
@@ -2581,24 +2744,33 @@ Be specific and concise.`;
2581
2744
  },
2582
2745
  {
2583
2746
  name: "screenshot",
2584
- description: "Capture a screenshot of the app preview. Returns a CDN URL. Use to review the current state of the UI being built.",
2747
+ 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. Set viewportOnly to capture just what the user sees on screen.",
2585
2748
  inputSchema: {
2586
2749
  type: "object",
2587
- properties: {}
2750
+ properties: {
2751
+ prompt: {
2752
+ type: "string",
2753
+ description: "Optional specific question about the screenshot."
2754
+ },
2755
+ viewportOnly: {
2756
+ type: "boolean",
2757
+ description: "Capture only the visible viewport instead of the full scrollable page. Use when checking above-the-fold layout or viewport-relative sizing like 100vh."
2758
+ }
2759
+ }
2588
2760
  }
2589
2761
  },
2590
2762
  {
2591
- name: "searchProductScreenshots",
2592
- 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.',
2763
+ name: "runBrowserTest",
2764
+ 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.",
2593
2765
  inputSchema: {
2594
2766
  type: "object",
2595
2767
  properties: {
2596
- product: {
2768
+ task: {
2597
2769
  type: "string",
2598
- description: 'The product or app to find screenshots of (e.g., "stripe dashboard", "figma editor", "mercury banking app").'
2770
+ 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".'
2599
2771
  }
2600
2772
  },
2601
- required: ["product"]
2773
+ required: ["task"]
2602
2774
  }
2603
2775
  },
2604
2776
  {
@@ -2631,13 +2803,13 @@ Be specific and concise.`;
2631
2803
  });
2632
2804
 
2633
2805
  // src/subagents/common/context.ts
2634
- import fs11 from "fs";
2635
- import path5 from "path";
2806
+ import fs12 from "fs";
2807
+ import path6 from "path";
2636
2808
  function walkMdFiles(dir, skip) {
2637
2809
  const files = [];
2638
2810
  try {
2639
- for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
2640
- const full = path5.join(dir, entry.name);
2811
+ for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
2812
+ const full = path6.join(dir, entry.name);
2641
2813
  if (entry.isDirectory()) {
2642
2814
  if (!skip?.has(entry.name)) {
2643
2815
  files.push(...walkMdFiles(full, skip));
@@ -2657,7 +2829,7 @@ function loadFilesAsXml(dir, tag, skip) {
2657
2829
  }
2658
2830
  const sections = files.map((f) => {
2659
2831
  try {
2660
- const content = fs11.readFileSync(f, "utf-8").trim();
2832
+ const content = fs12.readFileSync(f, "utf-8").trim();
2661
2833
  return `<file path="${f}">
2662
2834
  ${content}
2663
2835
  </file>`;
@@ -2732,18 +2904,18 @@ var init_context = __esm({
2732
2904
  });
2733
2905
 
2734
2906
  // src/subagents/designExpert/prompt.ts
2735
- import fs12 from "fs";
2736
- import path6 from "path";
2737
- function resolvePath(filename) {
2738
- const local4 = path6.join(base2, filename);
2739
- return fs12.existsSync(local4) ? local4 : path6.join(base2, "subagents", "designExpert", filename);
2907
+ import fs13 from "fs";
2908
+ import path7 from "path";
2909
+ function resolvePath2(filename) {
2910
+ const local4 = path7.join(base3, filename);
2911
+ return fs13.existsSync(local4) ? local4 : path7.join(base3, "subagents", "designExpert", filename);
2740
2912
  }
2741
2913
  function readFile(filename) {
2742
- return fs12.readFileSync(resolvePath(filename), "utf-8").trim();
2914
+ return fs13.readFileSync(resolvePath2(filename), "utf-8").trim();
2743
2915
  }
2744
2916
  function readJson(filename, fallback) {
2745
2917
  try {
2746
- return JSON.parse(fs12.readFileSync(resolvePath(filename), "utf-8"));
2918
+ return JSON.parse(fs13.readFileSync(resolvePath2(filename), "utf-8"));
2747
2919
  } catch {
2748
2920
  return fallback;
2749
2921
  }
@@ -2764,7 +2936,6 @@ function getDesignExpertPrompt() {
2764
2936
  const pairings = sample(fontData.pairings, 20);
2765
2937
  const images = sample(inspirationImages, 15);
2766
2938
  const fontList = fonts.map((f) => {
2767
- const tags = f.tags.length ? ` (${f.tags.join(", ")})` : "";
2768
2939
  let cssInfo = "";
2769
2940
  if (f.source === "fontshare") {
2770
2941
  cssInfo = ` CSS: ${fontData.cssUrlPattern.replace("{slug}", f.slug).replace("{weights}", f.weights.join(","))}`;
@@ -2773,7 +2944,8 @@ function getDesignExpertPrompt() {
2773
2944
  } else if (f.source === "open-foundry") {
2774
2945
  cssInfo = " (self-host required)";
2775
2946
  }
2776
- return `- **${f.name}** \u2014 ${f.category}${tags}. Weights: ${f.weights.join(", ")}.${f.variable ? " Variable." : ""}${f.italics ? " Has italics." : ""}${cssInfo}`;
2947
+ const desc = f.description ? ` ${f.description}` : "";
2948
+ return `- **${f.name}** \u2014 ${f.category}. Weights: ${f.weights.join(", ")}.${f.variable ? " Variable." : ""}${f.italics ? " Has italics." : ""}${cssInfo}${desc}`;
2777
2949
  }).join("\n");
2778
2950
  const pairingList = pairings.map(
2779
2951
  (p) => `- **${p.heading.font}** (${p.heading.weight}) heading + **${p.body.font}** (${p.body.weight}) body`
@@ -2791,13 +2963,13 @@ ${fontList}
2791
2963
  ${pairingList}
2792
2964
  </fonts_to_consider>` : "";
2793
2965
  const imageList = images.map((img) => `- ${img.analysis}`).join("\n\n");
2794
- const inspirationSection = images.length ? `<inspiration_images>
2966
+ const inspirationSection = images.length ? `<design_inspiration>
2795
2967
  ## Design inspiration
2796
2968
 
2797
- 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.
2969
+ 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.
2798
2970
 
2799
2971
  ${imageList}
2800
- </inspiration_images>` : "";
2972
+ </design_inspiration>` : "";
2801
2973
  const specContext = loadSpecContext();
2802
2974
  let prompt = PROMPT_TEMPLATE.replace(
2803
2975
  "{{fonts_to_consider}}",
@@ -2810,12 +2982,12 @@ ${specContext}`;
2810
2982
  }
2811
2983
  return prompt;
2812
2984
  }
2813
- var base2, RUNTIME_PLACEHOLDERS, PROMPT_TEMPLATE, fontData, inspirationImages;
2985
+ var base3, RUNTIME_PLACEHOLDERS, PROMPT_TEMPLATE, fontData, inspirationImages;
2814
2986
  var init_prompt2 = __esm({
2815
2987
  "src/subagents/designExpert/prompt.ts"() {
2816
2988
  "use strict";
2817
2989
  init_context();
2818
- base2 = import.meta.dirname ?? path6.dirname(new URL(import.meta.url).pathname);
2990
+ base3 = import.meta.dirname ?? path7.dirname(new URL(import.meta.url).pathname);
2819
2991
  RUNTIME_PLACEHOLDERS = /* @__PURE__ */ new Set([
2820
2992
  "fonts_to_consider",
2821
2993
  "inspiration_images"
@@ -2870,7 +3042,7 @@ Visual design expert. Describe the situation and what you need \u2014 the agent
2870
3042
  task: input.task,
2871
3043
  tools: DESIGN_EXPERT_TOOLS,
2872
3044
  externalTools: /* @__PURE__ */ new Set(),
2873
- executeTool: executeDesignExpertTool,
3045
+ executeTool: (name, input2) => executeDesignExpertTool(name, input2, context),
2874
3046
  apiConfig: context.apiConfig,
2875
3047
  model: context.model,
2876
3048
  subAgentId: "visualDesignExpert",
@@ -2991,8 +3163,8 @@ var init_tools3 = __esm({
2991
3163
  });
2992
3164
 
2993
3165
  // src/subagents/productVision/executor.ts
2994
- import fs13 from "fs";
2995
- import path7 from "path";
3166
+ import fs14 from "fs";
3167
+ import path8 from "path";
2996
3168
  function formatRequires(requires) {
2997
3169
  return requires.length === 0 ? "[]" : `[${requires.map((r) => `"${r}"`).join(", ")}]`;
2998
3170
  }
@@ -3007,9 +3179,10 @@ async function executeVisionTool(name, input) {
3007
3179
  requires,
3008
3180
  body
3009
3181
  } = input;
3010
- const filePath = path7.join(ROADMAP_DIR, `${slug}.md`);
3182
+ const filePath = path8.join(ROADMAP_DIR, `${slug}.md`);
3011
3183
  try {
3012
- fs13.mkdirSync(ROADMAP_DIR, { recursive: true });
3184
+ fs14.mkdirSync(ROADMAP_DIR, { recursive: true });
3185
+ const oldContent = fs14.existsSync(filePath) ? fs14.readFileSync(filePath, "utf-8") : "";
3013
3186
  const content = `---
3014
3187
  name: ${itemName}
3015
3188
  type: roadmap
@@ -3021,20 +3194,24 @@ requires: ${formatRequires(requires)}
3021
3194
 
3022
3195
  ${body}
3023
3196
  `;
3024
- fs13.writeFileSync(filePath, content, "utf-8");
3025
- return `Wrote ${filePath}`;
3197
+ fs14.writeFileSync(filePath, content, "utf-8");
3198
+ const lineCount = content.split("\n").length;
3199
+ const label = oldContent ? "Updated" : "Wrote";
3200
+ return `${label} ${filePath} (${lineCount} lines)
3201
+ ${unifiedDiff(filePath, oldContent, content)}`;
3026
3202
  } catch (err) {
3027
3203
  return `Error writing ${filePath}: ${err.message}`;
3028
3204
  }
3029
3205
  }
3030
3206
  case "updateRoadmapItem": {
3031
3207
  const { slug } = input;
3032
- const filePath = path7.join(ROADMAP_DIR, `${slug}.md`);
3208
+ const filePath = path8.join(ROADMAP_DIR, `${slug}.md`);
3033
3209
  try {
3034
- if (!fs13.existsSync(filePath)) {
3210
+ if (!fs14.existsSync(filePath)) {
3035
3211
  return `Error: ${filePath} does not exist`;
3036
3212
  }
3037
- let content = fs13.readFileSync(filePath, "utf-8");
3213
+ const oldContent = fs14.readFileSync(filePath, "utf-8");
3214
+ let content = oldContent;
3038
3215
  if (input.status) {
3039
3216
  content = content.replace(
3040
3217
  /^status:\s*.+$/m,
@@ -3086,21 +3263,25 @@ ${input.appendHistory}
3086
3263
  `;
3087
3264
  }
3088
3265
  }
3089
- fs13.writeFileSync(filePath, content, "utf-8");
3090
- return `Updated ${filePath}`;
3266
+ fs14.writeFileSync(filePath, content, "utf-8");
3267
+ const lineCount = content.split("\n").length;
3268
+ return `Updated ${filePath} (${lineCount} lines)
3269
+ ${unifiedDiff(filePath, oldContent, content)}`;
3091
3270
  } catch (err) {
3092
3271
  return `Error updating ${filePath}: ${err.message}`;
3093
3272
  }
3094
3273
  }
3095
3274
  case "deleteRoadmapItem": {
3096
3275
  const { slug } = input;
3097
- const filePath = path7.join(ROADMAP_DIR, `${slug}.md`);
3276
+ const filePath = path8.join(ROADMAP_DIR, `${slug}.md`);
3098
3277
  try {
3099
- if (!fs13.existsSync(filePath)) {
3278
+ if (!fs14.existsSync(filePath)) {
3100
3279
  return `Error: ${filePath} does not exist`;
3101
3280
  }
3102
- fs13.unlinkSync(filePath);
3103
- return `Deleted ${filePath}`;
3281
+ const oldContent = fs14.readFileSync(filePath, "utf-8");
3282
+ fs14.unlinkSync(filePath);
3283
+ return `Deleted ${filePath}
3284
+ ${unifiedDiff(filePath, oldContent, "")}`;
3104
3285
  } catch (err) {
3105
3286
  return `Error deleting ${filePath}: ${err.message}`;
3106
3287
  }
@@ -3113,14 +3294,15 @@ var ROADMAP_DIR;
3113
3294
  var init_executor = __esm({
3114
3295
  "src/subagents/productVision/executor.ts"() {
3115
3296
  "use strict";
3297
+ init_diff();
3116
3298
  init_context();
3117
3299
  ROADMAP_DIR = "src/roadmap";
3118
3300
  }
3119
3301
  });
3120
3302
 
3121
3303
  // src/subagents/productVision/prompt.ts
3122
- import fs14 from "fs";
3123
- import path8 from "path";
3304
+ import fs15 from "fs";
3305
+ import path9 from "path";
3124
3306
  function getProductVisionPrompt() {
3125
3307
  const specContext = loadSpecContext();
3126
3308
  const roadmapContext = loadRoadmapContext();
@@ -3133,16 +3315,16 @@ function getProductVisionPrompt() {
3133
3315
  }
3134
3316
  return parts.join("\n\n");
3135
3317
  }
3136
- var base3, local2, PROMPT_PATH2, BASE_PROMPT2;
3318
+ var base4, local2, PROMPT_PATH2, BASE_PROMPT2;
3137
3319
  var init_prompt3 = __esm({
3138
3320
  "src/subagents/productVision/prompt.ts"() {
3139
3321
  "use strict";
3140
3322
  init_executor();
3141
3323
  init_context();
3142
- base3 = import.meta.dirname ?? path8.dirname(new URL(import.meta.url).pathname);
3143
- local2 = path8.join(base3, "prompt.md");
3144
- PROMPT_PATH2 = fs14.existsSync(local2) ? local2 : path8.join(base3, "subagents", "productVision", "prompt.md");
3145
- BASE_PROMPT2 = fs14.readFileSync(PROMPT_PATH2, "utf-8").trim();
3324
+ base4 = import.meta.dirname ?? path9.dirname(new URL(import.meta.url).pathname);
3325
+ local2 = path9.join(base4, "prompt.md");
3326
+ PROMPT_PATH2 = fs15.existsSync(local2) ? local2 : path9.join(base4, "subagents", "productVision", "prompt.md");
3327
+ BASE_PROMPT2 = fs15.readFileSync(PROMPT_PATH2, "utf-8").trim();
3146
3328
  }
3147
3329
  });
3148
3330
 
@@ -3293,9 +3475,9 @@ var init_tools4 = __esm({
3293
3475
  });
3294
3476
 
3295
3477
  // src/subagents/codeSanityCheck/index.ts
3296
- import fs15 from "fs";
3297
- import path9 from "path";
3298
- var base4, local3, PROMPT_PATH3, BASE_PROMPT3, codeSanityCheckTool;
3478
+ import fs16 from "fs";
3479
+ import path10 from "path";
3480
+ var base5, local3, PROMPT_PATH3, BASE_PROMPT3, codeSanityCheckTool;
3299
3481
  var init_codeSanityCheck = __esm({
3300
3482
  "src/subagents/codeSanityCheck/index.ts"() {
3301
3483
  "use strict";
@@ -3303,10 +3485,10 @@ var init_codeSanityCheck = __esm({
3303
3485
  init_context();
3304
3486
  init_tools5();
3305
3487
  init_tools4();
3306
- base4 = import.meta.dirname ?? path9.dirname(new URL(import.meta.url).pathname);
3307
- local3 = path9.join(base4, "prompt.md");
3308
- PROMPT_PATH3 = fs15.existsSync(local3) ? local3 : path9.join(base4, "subagents", "codeSanityCheck", "prompt.md");
3309
- BASE_PROMPT3 = fs15.readFileSync(PROMPT_PATH3, "utf-8").trim();
3488
+ base5 = import.meta.dirname ?? path10.dirname(new URL(import.meta.url).pathname);
3489
+ local3 = path10.join(base5, "prompt.md");
3490
+ PROMPT_PATH3 = fs16.existsSync(local3) ? local3 : path10.join(base5, "subagents", "codeSanityCheck", "prompt.md");
3491
+ BASE_PROMPT3 = fs16.readFileSync(PROMPT_PATH3, "utf-8").trim();
3310
3492
  codeSanityCheckTool = {
3311
3493
  definition: {
3312
3494
  name: "codeSanityCheck",
@@ -3464,7 +3646,7 @@ var init_tools5 = __esm({
3464
3646
  init_restartProcess();
3465
3647
  init_runScenario();
3466
3648
  init_runMethod();
3467
- init_screenshot();
3649
+ init_screenshot2();
3468
3650
  init_browserAutomation();
3469
3651
  init_designExpert();
3470
3652
  init_productVision();
@@ -3473,10 +3655,10 @@ var init_tools5 = __esm({
3473
3655
  });
3474
3656
 
3475
3657
  // src/session.ts
3476
- import fs16 from "fs";
3658
+ import fs17 from "fs";
3477
3659
  function loadSession(state) {
3478
3660
  try {
3479
- const raw = fs16.readFileSync(SESSION_FILE, "utf-8");
3661
+ const raw = fs17.readFileSync(SESSION_FILE, "utf-8");
3480
3662
  const data = JSON.parse(raw);
3481
3663
  if (Array.isArray(data.messages) && data.messages.length > 0) {
3482
3664
  state.messages = sanitizeMessages(data.messages);
@@ -3524,7 +3706,7 @@ function sanitizeMessages(messages) {
3524
3706
  }
3525
3707
  function saveSession(state) {
3526
3708
  try {
3527
- fs16.writeFileSync(
3709
+ fs17.writeFileSync(
3528
3710
  SESSION_FILE,
3529
3711
  JSON.stringify({ messages: state.messages }, null, 2),
3530
3712
  "utf-8"
@@ -3535,7 +3717,7 @@ function saveSession(state) {
3535
3717
  function clearSession(state) {
3536
3718
  state.messages = [];
3537
3719
  try {
3538
- fs16.unlinkSync(SESSION_FILE);
3720
+ fs17.unlinkSync(SESSION_FILE);
3539
3721
  } catch {
3540
3722
  }
3541
3723
  }
@@ -4281,12 +4463,12 @@ var init_agent = __esm({
4281
4463
  });
4282
4464
 
4283
4465
  // src/prompt/static/projectContext.ts
4284
- import fs17 from "fs";
4285
- import path10 from "path";
4466
+ import fs18 from "fs";
4467
+ import path11 from "path";
4286
4468
  function loadProjectInstructions() {
4287
4469
  for (const file of AGENT_INSTRUCTION_FILES) {
4288
4470
  try {
4289
- const content = fs17.readFileSync(file, "utf-8").trim();
4471
+ const content = fs18.readFileSync(file, "utf-8").trim();
4290
4472
  if (content) {
4291
4473
  return `
4292
4474
  ## Project Instructions (${file})
@@ -4299,7 +4481,7 @@ ${content}`;
4299
4481
  }
4300
4482
  function loadProjectManifest() {
4301
4483
  try {
4302
- const manifest = fs17.readFileSync("mindstudio.json", "utf-8");
4484
+ const manifest = fs18.readFileSync("mindstudio.json", "utf-8");
4303
4485
  return `
4304
4486
  ## Project Manifest (mindstudio.json)
4305
4487
  \`\`\`json
@@ -4340,9 +4522,9 @@ ${entries.join("\n")}`;
4340
4522
  function walkMdFiles2(dir) {
4341
4523
  const results = [];
4342
4524
  try {
4343
- const entries = fs17.readdirSync(dir, { withFileTypes: true });
4525
+ const entries = fs18.readdirSync(dir, { withFileTypes: true });
4344
4526
  for (const entry of entries) {
4345
- const full = path10.join(dir, entry.name);
4527
+ const full = path11.join(dir, entry.name);
4346
4528
  if (entry.isDirectory()) {
4347
4529
  results.push(...walkMdFiles2(full));
4348
4530
  } else if (entry.name.endsWith(".md")) {
@@ -4355,7 +4537,7 @@ function walkMdFiles2(dir) {
4355
4537
  }
4356
4538
  function parseFrontmatter(filePath) {
4357
4539
  try {
4358
- const content = fs17.readFileSync(filePath, "utf-8");
4540
+ const content = fs18.readFileSync(filePath, "utf-8");
4359
4541
  const match = content.match(/^---\n([\s\S]*?)\n---/);
4360
4542
  if (!match) {
4361
4543
  return { name: "", description: "", type: "" };
@@ -4371,7 +4553,7 @@ function parseFrontmatter(filePath) {
4371
4553
  }
4372
4554
  function loadProjectFileListing() {
4373
4555
  try {
4374
- const entries = fs17.readdirSync(".", { withFileTypes: true });
4556
+ const entries = fs18.readdirSync(".", { withFileTypes: true });
4375
4557
  const listing = entries.filter((e) => e.name !== ".git" && e.name !== "node_modules").sort((a, b) => {
4376
4558
  if (a.isDirectory() && !b.isDirectory()) {
4377
4559
  return -1;
@@ -4414,12 +4596,12 @@ var init_projectContext = __esm({
4414
4596
  });
4415
4597
 
4416
4598
  // src/prompt/index.ts
4417
- import fs18 from "fs";
4418
- import path11 from "path";
4599
+ import fs19 from "fs";
4600
+ import path12 from "path";
4419
4601
  function requireFile(filePath) {
4420
- const full = path11.join(PROMPT_DIR, filePath);
4602
+ const full = path12.join(PROMPT_DIR, filePath);
4421
4603
  try {
4422
- return fs18.readFileSync(full, "utf-8").trim();
4604
+ return fs19.readFileSync(full, "utf-8").trim();
4423
4605
  } catch {
4424
4606
  throw new Error(`Required prompt file missing: ${full}`);
4425
4607
  }
@@ -4438,14 +4620,11 @@ function buildSystemPrompt(onboardingState, viewContext) {
4438
4620
  loadSpecFileMetadata(),
4439
4621
  loadProjectFileListing()
4440
4622
  ].filter(Boolean).join("\n");
4441
- const now = (/* @__PURE__ */ new Date()).toLocaleString("en-US", {
4442
- dateStyle: "full",
4443
- timeStyle: "long"
4444
- });
4623
+ const now = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC");
4445
4624
  const template = `
4446
4625
  {{static/identity.md}}
4447
4626
 
4448
- The current date is ${now}.
4627
+ Current date/time: ${now}
4449
4628
 
4450
4629
  <platform_docs>
4451
4630
  <platform>
@@ -4546,17 +4725,17 @@ var init_prompt4 = __esm({
4546
4725
  "use strict";
4547
4726
  init_lsp();
4548
4727
  init_projectContext();
4549
- PROMPT_DIR = import.meta.dirname ?? path11.dirname(new URL(import.meta.url).pathname);
4728
+ PROMPT_DIR = import.meta.dirname ?? path12.dirname(new URL(import.meta.url).pathname);
4550
4729
  }
4551
4730
  });
4552
4731
 
4553
4732
  // src/config.ts
4554
- import fs19 from "fs";
4555
- import path12 from "path";
4733
+ import fs20 from "fs";
4734
+ import path13 from "path";
4556
4735
  import os from "os";
4557
4736
  function loadConfigFile() {
4558
4737
  try {
4559
- const raw = fs19.readFileSync(CONFIG_PATH, "utf-8");
4738
+ const raw = fs20.readFileSync(CONFIG_PATH, "utf-8");
4560
4739
  log.debug("Loaded config file", { path: CONFIG_PATH });
4561
4740
  return JSON.parse(raw);
4562
4741
  } catch (err) {
@@ -4592,7 +4771,7 @@ var init_config = __esm({
4592
4771
  "src/config.ts"() {
4593
4772
  "use strict";
4594
4773
  init_logger();
4595
- CONFIG_PATH = path12.join(
4774
+ CONFIG_PATH = path13.join(
4596
4775
  os.homedir(),
4597
4776
  ".mindstudio-local-tunnel",
4598
4777
  "config.json"
@@ -4607,13 +4786,46 @@ __export(headless_exports, {
4607
4786
  startHeadless: () => startHeadless
4608
4787
  });
4609
4788
  import { createInterface } from "readline";
4610
- import fs20 from "fs";
4611
- import path13 from "path";
4789
+ import fs21 from "fs";
4790
+ import path14 from "path";
4612
4791
  function loadActionPrompt(name) {
4613
- return fs20.readFileSync(path13.join(ACTIONS_DIR, `${name}.md`), "utf-8").trim();
4792
+ return fs21.readFileSync(path14.join(ACTIONS_DIR, `${name}.md`), "utf-8").trim();
4793
+ }
4794
+ function emit(event, data, requestId) {
4795
+ const payload = { event, ...data };
4796
+ if (requestId) {
4797
+ payload.requestId = requestId;
4798
+ }
4799
+ process.stdout.write(JSON.stringify(payload) + "\n");
4614
4800
  }
4615
- function emit(event, data) {
4616
- process.stdout.write(JSON.stringify({ event, ...data }) + "\n");
4801
+ function handleGetHistory(state) {
4802
+ return { messages: state.messages };
4803
+ }
4804
+ function handleClear(state) {
4805
+ clearSession(state);
4806
+ return {};
4807
+ }
4808
+ function handleCancel(currentAbort, pendingTools) {
4809
+ if (currentAbort) {
4810
+ currentAbort.abort();
4811
+ }
4812
+ for (const [id, pending] of pendingTools) {
4813
+ clearTimeout(pending.timeout);
4814
+ pending.resolve("Error: cancelled");
4815
+ pendingTools.delete(id);
4816
+ }
4817
+ return {};
4818
+ }
4819
+ function dispatchSimple(requestId, eventName, handler) {
4820
+ try {
4821
+ const data = handler();
4822
+ if (eventName) {
4823
+ emit(eventName, data, requestId);
4824
+ }
4825
+ emit("completed", { success: true }, requestId);
4826
+ } catch (err) {
4827
+ emit("completed", { success: false, error: err.message }, requestId);
4828
+ }
4617
4829
  }
4618
4830
  async function startHeadless(opts = {}) {
4619
4831
  const stderrWrite = (...args2) => {
@@ -4632,72 +4844,15 @@ async function startHeadless(opts = {}) {
4632
4844
  const state = createAgentState();
4633
4845
  const resumed = loadSession(state);
4634
4846
  if (resumed) {
4635
- emit("session_restored", {
4636
- messageCount: state.messages.length
4637
- });
4847
+ emit("session_restored", { messageCount: state.messages.length });
4638
4848
  }
4639
4849
  let running = false;
4640
4850
  let currentAbort = null;
4851
+ let currentRequestId;
4852
+ let completedEmitted = false;
4641
4853
  const EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
4642
4854
  const pendingTools = /* @__PURE__ */ new Map();
4643
4855
  const earlyResults = /* @__PURE__ */ new Map();
4644
- function onEvent(e) {
4645
- switch (e.type) {
4646
- case "text":
4647
- emit("text", {
4648
- text: e.text,
4649
- ...e.parentToolId && { parentToolId: e.parentToolId }
4650
- });
4651
- break;
4652
- case "thinking":
4653
- emit("thinking", {
4654
- text: e.text,
4655
- ...e.parentToolId && { parentToolId: e.parentToolId }
4656
- });
4657
- break;
4658
- case "tool_input_delta":
4659
- emit("tool_input_delta", {
4660
- id: e.id,
4661
- name: e.name,
4662
- result: e.result,
4663
- ...e.parentToolId && { parentToolId: e.parentToolId }
4664
- });
4665
- break;
4666
- case "tool_start":
4667
- emit("tool_start", {
4668
- id: e.id,
4669
- name: e.name,
4670
- input: e.input,
4671
- ...e.partial && { partial: true },
4672
- ...e.parentToolId && { parentToolId: e.parentToolId }
4673
- });
4674
- break;
4675
- case "tool_done":
4676
- emit("tool_done", {
4677
- id: e.id,
4678
- name: e.name,
4679
- result: e.result,
4680
- isError: e.isError,
4681
- ...e.parentToolId && { parentToolId: e.parentToolId }
4682
- });
4683
- break;
4684
- case "turn_started":
4685
- emit("turn_started");
4686
- break;
4687
- case "turn_done":
4688
- emit("turn_done");
4689
- break;
4690
- case "turn_cancelled":
4691
- emit("turn_cancelled");
4692
- break;
4693
- case "error":
4694
- emit("error", { error: e.error });
4695
- break;
4696
- case "status":
4697
- emit("status", { message: e.message });
4698
- break;
4699
- }
4700
- }
4701
4856
  const USER_FACING_TOOLS = /* @__PURE__ */ new Set([
4702
4857
  "promptUser",
4703
4858
  "confirmDestructiveAction",
@@ -4728,6 +4883,158 @@ async function startHeadless(opts = {}) {
4728
4883
  });
4729
4884
  });
4730
4885
  }
4886
+ function onEvent(e) {
4887
+ const rid = currentRequestId;
4888
+ switch (e.type) {
4889
+ // Suppressed — caller already knows the request started
4890
+ case "turn_started":
4891
+ return;
4892
+ // Terminal events — translate to `completed`
4893
+ case "turn_done":
4894
+ completedEmitted = true;
4895
+ emit("completed", { success: true }, rid);
4896
+ return;
4897
+ case "turn_cancelled":
4898
+ completedEmitted = true;
4899
+ emit("completed", { success: false, error: "cancelled" }, rid);
4900
+ return;
4901
+ // Streaming events — forward with requestId
4902
+ case "text":
4903
+ emit(
4904
+ "text",
4905
+ {
4906
+ text: e.text,
4907
+ ...e.parentToolId && { parentToolId: e.parentToolId }
4908
+ },
4909
+ rid
4910
+ );
4911
+ return;
4912
+ case "thinking":
4913
+ emit(
4914
+ "thinking",
4915
+ {
4916
+ text: e.text,
4917
+ ...e.parentToolId && { parentToolId: e.parentToolId }
4918
+ },
4919
+ rid
4920
+ );
4921
+ return;
4922
+ case "tool_input_delta":
4923
+ emit(
4924
+ "tool_input_delta",
4925
+ {
4926
+ id: e.id,
4927
+ name: e.name,
4928
+ result: e.result,
4929
+ ...e.parentToolId && { parentToolId: e.parentToolId }
4930
+ },
4931
+ rid
4932
+ );
4933
+ return;
4934
+ case "tool_start":
4935
+ emit(
4936
+ "tool_start",
4937
+ {
4938
+ id: e.id,
4939
+ name: e.name,
4940
+ input: e.input,
4941
+ ...e.partial && { partial: true },
4942
+ ...e.parentToolId && { parentToolId: e.parentToolId }
4943
+ },
4944
+ rid
4945
+ );
4946
+ return;
4947
+ case "tool_done":
4948
+ emit(
4949
+ "tool_done",
4950
+ {
4951
+ id: e.id,
4952
+ name: e.name,
4953
+ result: e.result,
4954
+ isError: e.isError,
4955
+ ...e.parentToolId && { parentToolId: e.parentToolId }
4956
+ },
4957
+ rid
4958
+ );
4959
+ return;
4960
+ case "status":
4961
+ emit("status", { message: e.message }, rid);
4962
+ return;
4963
+ case "error":
4964
+ emit("error", { error: e.error }, rid);
4965
+ return;
4966
+ }
4967
+ }
4968
+ async function handleMessage(parsed, requestId) {
4969
+ if (running) {
4970
+ emit(
4971
+ "error",
4972
+ { error: "Agent is already processing a message" },
4973
+ requestId
4974
+ );
4975
+ emit(
4976
+ "completed",
4977
+ { success: false, error: "Agent is already processing a message" },
4978
+ requestId
4979
+ );
4980
+ return;
4981
+ }
4982
+ running = true;
4983
+ currentRequestId = requestId;
4984
+ currentAbort = new AbortController();
4985
+ completedEmitted = false;
4986
+ const attachments = parsed.attachments;
4987
+ if (attachments?.length) {
4988
+ console.warn(
4989
+ `[headless] Message has ${attachments.length} attachment(s):`,
4990
+ attachments.map((a) => a.url)
4991
+ );
4992
+ }
4993
+ let userMessage = parsed.text ?? "";
4994
+ const isCommand = !!parsed.runCommand;
4995
+ if (parsed.runCommand === "sync") {
4996
+ userMessage = loadActionPrompt("sync");
4997
+ } else if (parsed.runCommand === "publish") {
4998
+ userMessage = loadActionPrompt("publish");
4999
+ } else if (parsed.runCommand === "buildFromInitialSpec") {
5000
+ userMessage = loadActionPrompt("buildFromInitialSpec");
5001
+ }
5002
+ const onboardingState = parsed.onboardingState ?? "onboardingFinished";
5003
+ const system = buildSystemPrompt(
5004
+ onboardingState,
5005
+ parsed.viewContext
5006
+ );
5007
+ try {
5008
+ await runTurn({
5009
+ state,
5010
+ userMessage,
5011
+ attachments,
5012
+ apiConfig: config,
5013
+ system,
5014
+ model: opts.model,
5015
+ onboardingState,
5016
+ signal: currentAbort.signal,
5017
+ onEvent,
5018
+ resolveExternalTool,
5019
+ hidden: isCommand
5020
+ });
5021
+ if (!completedEmitted) {
5022
+ emit(
5023
+ "completed",
5024
+ { success: false, error: "Turn ended unexpectedly" },
5025
+ requestId
5026
+ );
5027
+ }
5028
+ } catch (err) {
5029
+ if (!completedEmitted) {
5030
+ emit("error", { error: err.message }, requestId);
5031
+ emit("completed", { success: false, error: err.message }, requestId);
5032
+ }
5033
+ }
5034
+ currentAbort = null;
5035
+ currentRequestId = void 0;
5036
+ running = false;
5037
+ }
4731
5038
  const rl = createInterface({ input: process.stdin });
4732
5039
  rl.on("line", async (line) => {
4733
5040
  let parsed;
@@ -4737,82 +5044,42 @@ async function startHeadless(opts = {}) {
4737
5044
  emit("error", { error: "Invalid JSON on stdin" });
4738
5045
  return;
4739
5046
  }
4740
- if (parsed.action === "tool_result" && parsed.id) {
4741
- const pending = pendingTools.get(parsed.id);
5047
+ const { action, requestId } = parsed;
5048
+ if (action === "tool_result" && parsed.id) {
5049
+ const id = parsed.id;
5050
+ const result = parsed.result ?? "";
5051
+ const pending = pendingTools.get(id);
4742
5052
  if (pending) {
4743
- pendingTools.delete(parsed.id);
4744
- pending.resolve(parsed.result ?? "");
5053
+ pendingTools.delete(id);
5054
+ pending.resolve(result);
4745
5055
  } else {
4746
- earlyResults.set(parsed.id, parsed.result ?? "");
5056
+ earlyResults.set(id, result);
4747
5057
  }
4748
5058
  return;
4749
5059
  }
4750
- if (parsed.action === "get_history") {
4751
- emit("history", {
4752
- messages: state.messages
4753
- });
5060
+ if (action === "get_history") {
5061
+ dispatchSimple(requestId, "history", () => handleGetHistory(state));
4754
5062
  return;
4755
5063
  }
4756
- if (parsed.action === "clear") {
4757
- clearSession(state);
4758
- emit("session_cleared");
5064
+ if (action === "clear") {
5065
+ dispatchSimple(requestId, "session_cleared", () => handleClear(state));
4759
5066
  return;
4760
5067
  }
4761
- if (parsed.action === "cancel") {
4762
- if (currentAbort) {
4763
- currentAbort.abort();
4764
- }
4765
- for (const [id, pending] of pendingTools) {
4766
- clearTimeout(pending.timeout);
4767
- pending.resolve("Error: cancelled");
4768
- pendingTools.delete(id);
4769
- }
5068
+ if (action === "cancel") {
5069
+ handleCancel(currentAbort, pendingTools);
5070
+ emit("completed", { success: true }, requestId);
4770
5071
  return;
4771
5072
  }
4772
- if (parsed.action === "message" && (parsed.text || parsed.runCommand)) {
4773
- if (running) {
4774
- emit("error", { error: "Agent is already processing a message" });
4775
- return;
4776
- }
4777
- running = true;
4778
- currentAbort = new AbortController();
4779
- if (parsed.attachments?.length) {
4780
- console.warn(
4781
- `[headless] Message has ${parsed.attachments.length} attachment(s):`,
4782
- parsed.attachments.map((a) => a.url)
4783
- );
4784
- }
4785
- let userMessage = parsed.text ?? "";
4786
- const isCommand = !!parsed.runCommand;
4787
- if (parsed.runCommand === "sync") {
4788
- userMessage = loadActionPrompt("sync");
4789
- } else if (parsed.runCommand === "publish") {
4790
- userMessage = loadActionPrompt("publish");
4791
- } else if (parsed.runCommand === "buildFromInitialSpec") {
4792
- userMessage = loadActionPrompt("buildFromInitialSpec");
4793
- }
4794
- const onboardingState = parsed.onboardingState ?? "onboardingFinished";
4795
- const system = buildSystemPrompt(onboardingState, parsed.viewContext);
4796
- try {
4797
- await runTurn({
4798
- state,
4799
- userMessage,
4800
- attachments: parsed.attachments,
4801
- apiConfig: config,
4802
- system,
4803
- model: opts.model,
4804
- onboardingState,
4805
- signal: currentAbort.signal,
4806
- onEvent,
4807
- resolveExternalTool,
4808
- hidden: isCommand
4809
- });
4810
- } catch (err) {
4811
- emit("error", { error: err.message });
4812
- }
4813
- currentAbort = null;
4814
- running = false;
5073
+ if (action === "message") {
5074
+ await handleMessage(parsed, requestId);
5075
+ return;
4815
5076
  }
5077
+ emit("error", { error: `Unknown action: ${action}` }, requestId);
5078
+ emit(
5079
+ "completed",
5080
+ { success: false, error: `Unknown action: ${action}` },
5081
+ requestId
5082
+ );
4816
5083
  });
4817
5084
  rl.on("close", () => {
4818
5085
  emit("stopping");
@@ -4837,16 +5104,16 @@ var init_headless = __esm({
4837
5104
  init_lsp();
4838
5105
  init_agent();
4839
5106
  init_session();
4840
- BASE_DIR = import.meta.dirname ?? path13.dirname(new URL(import.meta.url).pathname);
4841
- ACTIONS_DIR = path13.join(BASE_DIR, "actions");
5107
+ BASE_DIR = import.meta.dirname ?? path14.dirname(new URL(import.meta.url).pathname);
5108
+ ACTIONS_DIR = path14.join(BASE_DIR, "actions");
4842
5109
  }
4843
5110
  });
4844
5111
 
4845
5112
  // src/index.tsx
4846
5113
  import { render } from "ink";
4847
5114
  import os2 from "os";
4848
- import fs21 from "fs";
4849
- import path14 from "path";
5115
+ import fs22 from "fs";
5116
+ import path15 from "path";
4850
5117
 
4851
5118
  // src/tui/App.tsx
4852
5119
  import { useState as useState2, useCallback, useRef } from "react";
@@ -5163,8 +5430,8 @@ for (let i = 0; i < args.length; i++) {
5163
5430
  }
5164
5431
  function printDebugInfo(config) {
5165
5432
  const pkg = JSON.parse(
5166
- fs21.readFileSync(
5167
- path14.join(import.meta.dirname, "..", "package.json"),
5433
+ fs22.readFileSync(
5434
+ path15.join(import.meta.dirname, "..", "package.json"),
5168
5435
  "utf-8"
5169
5436
  )
5170
5437
  );