@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/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 fullPage = false;
2021
+ if (typeof promptOrOptions === "object" && promptOrOptions !== null) {
2022
+ prompt = promptOrOptions.prompt;
2023
+ fullPage = promptOrOptions.fullPage ?? false;
2024
+ } else {
2025
+ prompt = promptOrOptions;
2026
+ }
2027
+ const ssResult = await sidecarRequest(
2028
+ "/screenshot",
2029
+ { fullPage },
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. By default captures the viewport (what the user sees). Set fullPage to capture the entire scrollable page.",
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
+ fullPage: {
2077
+ type: "boolean",
2078
+ description: "Capture the full scrollable page instead of just the viewport. Use when you need to see below-the-fold content."
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
+ fullPage: input.fullPage
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
@@ -2171,7 +2257,7 @@ async function runSubAgent(config) {
2171
2257
  if (externalTools.has(tc.name) && resolveExternalTool) {
2172
2258
  result = await resolveExternalTool(tc.id, tc.name, tc.input);
2173
2259
  } else {
2174
- result = await executeTool2(tc.name, tc.input);
2260
+ result = await executeTool2(tc.name, tc.input, tc.id);
2175
2261
  }
2176
2262
  const isError = result.startsWith("Error");
2177
2263
  emit2({
@@ -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, toolCallId) {
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
+ fullPage: input.fullPage
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,44 @@ ${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(
2680
+ { task: input.task },
2681
+ {
2682
+ ...context,
2683
+ toolCallId: toolCallId || context.toolCallId
2684
+ }
2685
+ );
2513
2686
  }
2514
2687
  default:
2515
2688
  return `Error: unknown tool "${name}"`;
2516
2689
  }
2517
2690
  }
2518
- var DESIGN_REFERENCE_PROMPT, DESIGN_EXPERT_TOOLS;
2691
+ var base2, DESIGN_REFERENCE_PROMPT, DESIGN_EXPERT_TOOLS;
2519
2692
  var init_tools2 = __esm({
2520
2693
  "src/subagents/designExpert/tools.ts"() {
2521
2694
  "use strict";
2522
2695
  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.`;
2696
+ init_screenshot();
2697
+ init_browserAutomation();
2698
+ base2 = import.meta.dirname ?? path5.dirname(new URL(import.meta.url).pathname);
2699
+ DESIGN_REFERENCE_PROMPT = fs11.readFileSync(resolvePath("prompts/tool-prompts/design-analysis.md"), "utf-8").trim();
2531
2700
  DESIGN_EXPERT_TOOLS = [
2532
2701
  {
2533
2702
  name: "searchGoogle",
@@ -2581,24 +2750,33 @@ Be specific and concise.`;
2581
2750
  },
2582
2751
  {
2583
2752
  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.",
2753
+ 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.",
2585
2754
  inputSchema: {
2586
2755
  type: "object",
2587
- properties: {}
2756
+ properties: {
2757
+ prompt: {
2758
+ type: "string",
2759
+ description: "Optional specific question about the screenshot."
2760
+ },
2761
+ fullPage: {
2762
+ type: "boolean",
2763
+ description: "Capture the full scrollable page instead of just the viewport. Use when you need to see below-the-fold content."
2764
+ }
2765
+ }
2588
2766
  }
2589
2767
  },
2590
2768
  {
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.',
2769
+ name: "runBrowserTest",
2770
+ 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
2771
  inputSchema: {
2594
2772
  type: "object",
2595
2773
  properties: {
2596
- product: {
2774
+ task: {
2597
2775
  type: "string",
2598
- description: 'The product or app to find screenshots of (e.g., "stripe dashboard", "figma editor", "mercury banking app").'
2776
+ 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
2777
  }
2600
2778
  },
2601
- required: ["product"]
2779
+ required: ["task"]
2602
2780
  }
2603
2781
  },
2604
2782
  {
@@ -2631,13 +2809,13 @@ Be specific and concise.`;
2631
2809
  });
2632
2810
 
2633
2811
  // src/subagents/common/context.ts
2634
- import fs11 from "fs";
2635
- import path5 from "path";
2812
+ import fs12 from "fs";
2813
+ import path6 from "path";
2636
2814
  function walkMdFiles(dir, skip) {
2637
2815
  const files = [];
2638
2816
  try {
2639
- for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
2640
- const full = path5.join(dir, entry.name);
2817
+ for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
2818
+ const full = path6.join(dir, entry.name);
2641
2819
  if (entry.isDirectory()) {
2642
2820
  if (!skip?.has(entry.name)) {
2643
2821
  files.push(...walkMdFiles(full, skip));
@@ -2657,7 +2835,7 @@ function loadFilesAsXml(dir, tag, skip) {
2657
2835
  }
2658
2836
  const sections = files.map((f) => {
2659
2837
  try {
2660
- const content = fs11.readFileSync(f, "utf-8").trim();
2838
+ const content = fs12.readFileSync(f, "utf-8").trim();
2661
2839
  return `<file path="${f}">
2662
2840
  ${content}
2663
2841
  </file>`;
@@ -2732,18 +2910,18 @@ var init_context = __esm({
2732
2910
  });
2733
2911
 
2734
2912
  // 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);
2913
+ import fs13 from "fs";
2914
+ import path7 from "path";
2915
+ function resolvePath2(filename) {
2916
+ const local4 = path7.join(base3, filename);
2917
+ return fs13.existsSync(local4) ? local4 : path7.join(base3, "subagents", "designExpert", filename);
2740
2918
  }
2741
2919
  function readFile(filename) {
2742
- return fs12.readFileSync(resolvePath(filename), "utf-8").trim();
2920
+ return fs13.readFileSync(resolvePath2(filename), "utf-8").trim();
2743
2921
  }
2744
2922
  function readJson(filename, fallback) {
2745
2923
  try {
2746
- return JSON.parse(fs12.readFileSync(resolvePath(filename), "utf-8"));
2924
+ return JSON.parse(fs13.readFileSync(resolvePath2(filename), "utf-8"));
2747
2925
  } catch {
2748
2926
  return fallback;
2749
2927
  }
@@ -2764,7 +2942,6 @@ function getDesignExpertPrompt() {
2764
2942
  const pairings = sample(fontData.pairings, 20);
2765
2943
  const images = sample(inspirationImages, 15);
2766
2944
  const fontList = fonts.map((f) => {
2767
- const tags = f.tags.length ? ` (${f.tags.join(", ")})` : "";
2768
2945
  let cssInfo = "";
2769
2946
  if (f.source === "fontshare") {
2770
2947
  cssInfo = ` CSS: ${fontData.cssUrlPattern.replace("{slug}", f.slug).replace("{weights}", f.weights.join(","))}`;
@@ -2773,7 +2950,8 @@ function getDesignExpertPrompt() {
2773
2950
  } else if (f.source === "open-foundry") {
2774
2951
  cssInfo = " (self-host required)";
2775
2952
  }
2776
- return `- **${f.name}** \u2014 ${f.category}${tags}. Weights: ${f.weights.join(", ")}.${f.variable ? " Variable." : ""}${f.italics ? " Has italics." : ""}${cssInfo}`;
2953
+ const desc = f.description ? ` ${f.description}` : "";
2954
+ return `- **${f.name}** \u2014 ${f.category}. Weights: ${f.weights.join(", ")}.${f.variable ? " Variable." : ""}${f.italics ? " Has italics." : ""}${cssInfo}${desc}`;
2777
2955
  }).join("\n");
2778
2956
  const pairingList = pairings.map(
2779
2957
  (p) => `- **${p.heading.font}** (${p.heading.weight}) heading + **${p.body.font}** (${p.body.weight}) body`
@@ -2791,13 +2969,13 @@ ${fontList}
2791
2969
  ${pairingList}
2792
2970
  </fonts_to_consider>` : "";
2793
2971
  const imageList = images.map((img) => `- ${img.analysis}`).join("\n\n");
2794
- const inspirationSection = images.length ? `<inspiration_images>
2972
+ const inspirationSection = images.length ? `<design_inspiration>
2795
2973
  ## Design inspiration
2796
2974
 
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.
2975
+ 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
2976
 
2799
2977
  ${imageList}
2800
- </inspiration_images>` : "";
2978
+ </design_inspiration>` : "";
2801
2979
  const specContext = loadSpecContext();
2802
2980
  let prompt = PROMPT_TEMPLATE.replace(
2803
2981
  "{{fonts_to_consider}}",
@@ -2810,12 +2988,12 @@ ${specContext}`;
2810
2988
  }
2811
2989
  return prompt;
2812
2990
  }
2813
- var base2, RUNTIME_PLACEHOLDERS, PROMPT_TEMPLATE, fontData, inspirationImages;
2991
+ var base3, RUNTIME_PLACEHOLDERS, PROMPT_TEMPLATE, fontData, inspirationImages;
2814
2992
  var init_prompt2 = __esm({
2815
2993
  "src/subagents/designExpert/prompt.ts"() {
2816
2994
  "use strict";
2817
2995
  init_context();
2818
- base2 = import.meta.dirname ?? path6.dirname(new URL(import.meta.url).pathname);
2996
+ base3 = import.meta.dirname ?? path7.dirname(new URL(import.meta.url).pathname);
2819
2997
  RUNTIME_PLACEHOLDERS = /* @__PURE__ */ new Set([
2820
2998
  "fonts_to_consider",
2821
2999
  "inspiration_images"
@@ -2870,7 +3048,7 @@ Visual design expert. Describe the situation and what you need \u2014 the agent
2870
3048
  task: input.task,
2871
3049
  tools: DESIGN_EXPERT_TOOLS,
2872
3050
  externalTools: /* @__PURE__ */ new Set(),
2873
- executeTool: executeDesignExpertTool,
3051
+ executeTool: (name, input2, toolCallId) => executeDesignExpertTool(name, input2, context, toolCallId),
2874
3052
  apiConfig: context.apiConfig,
2875
3053
  model: context.model,
2876
3054
  subAgentId: "visualDesignExpert",
@@ -2991,8 +3169,8 @@ var init_tools3 = __esm({
2991
3169
  });
2992
3170
 
2993
3171
  // src/subagents/productVision/executor.ts
2994
- import fs13 from "fs";
2995
- import path7 from "path";
3172
+ import fs14 from "fs";
3173
+ import path8 from "path";
2996
3174
  function formatRequires(requires) {
2997
3175
  return requires.length === 0 ? "[]" : `[${requires.map((r) => `"${r}"`).join(", ")}]`;
2998
3176
  }
@@ -3007,9 +3185,10 @@ async function executeVisionTool(name, input) {
3007
3185
  requires,
3008
3186
  body
3009
3187
  } = input;
3010
- const filePath = path7.join(ROADMAP_DIR, `${slug}.md`);
3188
+ const filePath = path8.join(ROADMAP_DIR, `${slug}.md`);
3011
3189
  try {
3012
- fs13.mkdirSync(ROADMAP_DIR, { recursive: true });
3190
+ fs14.mkdirSync(ROADMAP_DIR, { recursive: true });
3191
+ const oldContent = fs14.existsSync(filePath) ? fs14.readFileSync(filePath, "utf-8") : "";
3013
3192
  const content = `---
3014
3193
  name: ${itemName}
3015
3194
  type: roadmap
@@ -3021,20 +3200,24 @@ requires: ${formatRequires(requires)}
3021
3200
 
3022
3201
  ${body}
3023
3202
  `;
3024
- fs13.writeFileSync(filePath, content, "utf-8");
3025
- return `Wrote ${filePath}`;
3203
+ fs14.writeFileSync(filePath, content, "utf-8");
3204
+ const lineCount = content.split("\n").length;
3205
+ const label = oldContent ? "Updated" : "Wrote";
3206
+ return `${label} ${filePath} (${lineCount} lines)
3207
+ ${unifiedDiff(filePath, oldContent, content)}`;
3026
3208
  } catch (err) {
3027
3209
  return `Error writing ${filePath}: ${err.message}`;
3028
3210
  }
3029
3211
  }
3030
3212
  case "updateRoadmapItem": {
3031
3213
  const { slug } = input;
3032
- const filePath = path7.join(ROADMAP_DIR, `${slug}.md`);
3214
+ const filePath = path8.join(ROADMAP_DIR, `${slug}.md`);
3033
3215
  try {
3034
- if (!fs13.existsSync(filePath)) {
3216
+ if (!fs14.existsSync(filePath)) {
3035
3217
  return `Error: ${filePath} does not exist`;
3036
3218
  }
3037
- let content = fs13.readFileSync(filePath, "utf-8");
3219
+ const oldContent = fs14.readFileSync(filePath, "utf-8");
3220
+ let content = oldContent;
3038
3221
  if (input.status) {
3039
3222
  content = content.replace(
3040
3223
  /^status:\s*.+$/m,
@@ -3086,21 +3269,25 @@ ${input.appendHistory}
3086
3269
  `;
3087
3270
  }
3088
3271
  }
3089
- fs13.writeFileSync(filePath, content, "utf-8");
3090
- return `Updated ${filePath}`;
3272
+ fs14.writeFileSync(filePath, content, "utf-8");
3273
+ const lineCount = content.split("\n").length;
3274
+ return `Updated ${filePath} (${lineCount} lines)
3275
+ ${unifiedDiff(filePath, oldContent, content)}`;
3091
3276
  } catch (err) {
3092
3277
  return `Error updating ${filePath}: ${err.message}`;
3093
3278
  }
3094
3279
  }
3095
3280
  case "deleteRoadmapItem": {
3096
3281
  const { slug } = input;
3097
- const filePath = path7.join(ROADMAP_DIR, `${slug}.md`);
3282
+ const filePath = path8.join(ROADMAP_DIR, `${slug}.md`);
3098
3283
  try {
3099
- if (!fs13.existsSync(filePath)) {
3284
+ if (!fs14.existsSync(filePath)) {
3100
3285
  return `Error: ${filePath} does not exist`;
3101
3286
  }
3102
- fs13.unlinkSync(filePath);
3103
- return `Deleted ${filePath}`;
3287
+ const oldContent = fs14.readFileSync(filePath, "utf-8");
3288
+ fs14.unlinkSync(filePath);
3289
+ return `Deleted ${filePath}
3290
+ ${unifiedDiff(filePath, oldContent, "")}`;
3104
3291
  } catch (err) {
3105
3292
  return `Error deleting ${filePath}: ${err.message}`;
3106
3293
  }
@@ -3113,14 +3300,15 @@ var ROADMAP_DIR;
3113
3300
  var init_executor = __esm({
3114
3301
  "src/subagents/productVision/executor.ts"() {
3115
3302
  "use strict";
3303
+ init_diff();
3116
3304
  init_context();
3117
3305
  ROADMAP_DIR = "src/roadmap";
3118
3306
  }
3119
3307
  });
3120
3308
 
3121
3309
  // src/subagents/productVision/prompt.ts
3122
- import fs14 from "fs";
3123
- import path8 from "path";
3310
+ import fs15 from "fs";
3311
+ import path9 from "path";
3124
3312
  function getProductVisionPrompt() {
3125
3313
  const specContext = loadSpecContext();
3126
3314
  const roadmapContext = loadRoadmapContext();
@@ -3133,16 +3321,16 @@ function getProductVisionPrompt() {
3133
3321
  }
3134
3322
  return parts.join("\n\n");
3135
3323
  }
3136
- var base3, local2, PROMPT_PATH2, BASE_PROMPT2;
3324
+ var base4, local2, PROMPT_PATH2, BASE_PROMPT2;
3137
3325
  var init_prompt3 = __esm({
3138
3326
  "src/subagents/productVision/prompt.ts"() {
3139
3327
  "use strict";
3140
3328
  init_executor();
3141
3329
  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();
3330
+ base4 = import.meta.dirname ?? path9.dirname(new URL(import.meta.url).pathname);
3331
+ local2 = path9.join(base4, "prompt.md");
3332
+ PROMPT_PATH2 = fs15.existsSync(local2) ? local2 : path9.join(base4, "subagents", "productVision", "prompt.md");
3333
+ BASE_PROMPT2 = fs15.readFileSync(PROMPT_PATH2, "utf-8").trim();
3146
3334
  }
3147
3335
  });
3148
3336
 
@@ -3293,9 +3481,9 @@ var init_tools4 = __esm({
3293
3481
  });
3294
3482
 
3295
3483
  // src/subagents/codeSanityCheck/index.ts
3296
- import fs15 from "fs";
3297
- import path9 from "path";
3298
- var base4, local3, PROMPT_PATH3, BASE_PROMPT3, codeSanityCheckTool;
3484
+ import fs16 from "fs";
3485
+ import path10 from "path";
3486
+ var base5, local3, PROMPT_PATH3, BASE_PROMPT3, codeSanityCheckTool;
3299
3487
  var init_codeSanityCheck = __esm({
3300
3488
  "src/subagents/codeSanityCheck/index.ts"() {
3301
3489
  "use strict";
@@ -3303,10 +3491,10 @@ var init_codeSanityCheck = __esm({
3303
3491
  init_context();
3304
3492
  init_tools5();
3305
3493
  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();
3494
+ base5 = import.meta.dirname ?? path10.dirname(new URL(import.meta.url).pathname);
3495
+ local3 = path10.join(base5, "prompt.md");
3496
+ PROMPT_PATH3 = fs16.existsSync(local3) ? local3 : path10.join(base5, "subagents", "codeSanityCheck", "prompt.md");
3497
+ BASE_PROMPT3 = fs16.readFileSync(PROMPT_PATH3, "utf-8").trim();
3310
3498
  codeSanityCheckTool = {
3311
3499
  definition: {
3312
3500
  name: "codeSanityCheck",
@@ -3464,7 +3652,7 @@ var init_tools5 = __esm({
3464
3652
  init_restartProcess();
3465
3653
  init_runScenario();
3466
3654
  init_runMethod();
3467
- init_screenshot();
3655
+ init_screenshot2();
3468
3656
  init_browserAutomation();
3469
3657
  init_designExpert();
3470
3658
  init_productVision();
@@ -3473,10 +3661,10 @@ var init_tools5 = __esm({
3473
3661
  });
3474
3662
 
3475
3663
  // src/session.ts
3476
- import fs16 from "fs";
3664
+ import fs17 from "fs";
3477
3665
  function loadSession(state) {
3478
3666
  try {
3479
- const raw = fs16.readFileSync(SESSION_FILE, "utf-8");
3667
+ const raw = fs17.readFileSync(SESSION_FILE, "utf-8");
3480
3668
  const data = JSON.parse(raw);
3481
3669
  if (Array.isArray(data.messages) && data.messages.length > 0) {
3482
3670
  state.messages = sanitizeMessages(data.messages);
@@ -3524,7 +3712,7 @@ function sanitizeMessages(messages) {
3524
3712
  }
3525
3713
  function saveSession(state) {
3526
3714
  try {
3527
- fs16.writeFileSync(
3715
+ fs17.writeFileSync(
3528
3716
  SESSION_FILE,
3529
3717
  JSON.stringify({ messages: state.messages }, null, 2),
3530
3718
  "utf-8"
@@ -3535,7 +3723,7 @@ function saveSession(state) {
3535
3723
  function clearSession(state) {
3536
3724
  state.messages = [];
3537
3725
  try {
3538
- fs16.unlinkSync(SESSION_FILE);
3726
+ fs17.unlinkSync(SESSION_FILE);
3539
3727
  } catch {
3540
3728
  }
3541
3729
  }
@@ -4281,12 +4469,12 @@ var init_agent = __esm({
4281
4469
  });
4282
4470
 
4283
4471
  // src/prompt/static/projectContext.ts
4284
- import fs17 from "fs";
4285
- import path10 from "path";
4472
+ import fs18 from "fs";
4473
+ import path11 from "path";
4286
4474
  function loadProjectInstructions() {
4287
4475
  for (const file of AGENT_INSTRUCTION_FILES) {
4288
4476
  try {
4289
- const content = fs17.readFileSync(file, "utf-8").trim();
4477
+ const content = fs18.readFileSync(file, "utf-8").trim();
4290
4478
  if (content) {
4291
4479
  return `
4292
4480
  ## Project Instructions (${file})
@@ -4299,7 +4487,7 @@ ${content}`;
4299
4487
  }
4300
4488
  function loadProjectManifest() {
4301
4489
  try {
4302
- const manifest = fs17.readFileSync("mindstudio.json", "utf-8");
4490
+ const manifest = fs18.readFileSync("mindstudio.json", "utf-8");
4303
4491
  return `
4304
4492
  ## Project Manifest (mindstudio.json)
4305
4493
  \`\`\`json
@@ -4340,9 +4528,9 @@ ${entries.join("\n")}`;
4340
4528
  function walkMdFiles2(dir) {
4341
4529
  const results = [];
4342
4530
  try {
4343
- const entries = fs17.readdirSync(dir, { withFileTypes: true });
4531
+ const entries = fs18.readdirSync(dir, { withFileTypes: true });
4344
4532
  for (const entry of entries) {
4345
- const full = path10.join(dir, entry.name);
4533
+ const full = path11.join(dir, entry.name);
4346
4534
  if (entry.isDirectory()) {
4347
4535
  results.push(...walkMdFiles2(full));
4348
4536
  } else if (entry.name.endsWith(".md")) {
@@ -4355,7 +4543,7 @@ function walkMdFiles2(dir) {
4355
4543
  }
4356
4544
  function parseFrontmatter(filePath) {
4357
4545
  try {
4358
- const content = fs17.readFileSync(filePath, "utf-8");
4546
+ const content = fs18.readFileSync(filePath, "utf-8");
4359
4547
  const match = content.match(/^---\n([\s\S]*?)\n---/);
4360
4548
  if (!match) {
4361
4549
  return { name: "", description: "", type: "" };
@@ -4371,7 +4559,7 @@ function parseFrontmatter(filePath) {
4371
4559
  }
4372
4560
  function loadProjectFileListing() {
4373
4561
  try {
4374
- const entries = fs17.readdirSync(".", { withFileTypes: true });
4562
+ const entries = fs18.readdirSync(".", { withFileTypes: true });
4375
4563
  const listing = entries.filter((e) => e.name !== ".git" && e.name !== "node_modules").sort((a, b) => {
4376
4564
  if (a.isDirectory() && !b.isDirectory()) {
4377
4565
  return -1;
@@ -4414,12 +4602,12 @@ var init_projectContext = __esm({
4414
4602
  });
4415
4603
 
4416
4604
  // src/prompt/index.ts
4417
- import fs18 from "fs";
4418
- import path11 from "path";
4605
+ import fs19 from "fs";
4606
+ import path12 from "path";
4419
4607
  function requireFile(filePath) {
4420
- const full = path11.join(PROMPT_DIR, filePath);
4608
+ const full = path12.join(PROMPT_DIR, filePath);
4421
4609
  try {
4422
- return fs18.readFileSync(full, "utf-8").trim();
4610
+ return fs19.readFileSync(full, "utf-8").trim();
4423
4611
  } catch {
4424
4612
  throw new Error(`Required prompt file missing: ${full}`);
4425
4613
  }
@@ -4438,14 +4626,11 @@ function buildSystemPrompt(onboardingState, viewContext) {
4438
4626
  loadSpecFileMetadata(),
4439
4627
  loadProjectFileListing()
4440
4628
  ].filter(Boolean).join("\n");
4441
- const now = (/* @__PURE__ */ new Date()).toLocaleString("en-US", {
4442
- dateStyle: "full",
4443
- timeStyle: "long"
4444
- });
4629
+ const now = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC");
4445
4630
  const template = `
4446
4631
  {{static/identity.md}}
4447
4632
 
4448
- The current date is ${now}.
4633
+ Current date/time: ${now}
4449
4634
 
4450
4635
  <platform_docs>
4451
4636
  <platform>
@@ -4546,17 +4731,17 @@ var init_prompt4 = __esm({
4546
4731
  "use strict";
4547
4732
  init_lsp();
4548
4733
  init_projectContext();
4549
- PROMPT_DIR = import.meta.dirname ?? path11.dirname(new URL(import.meta.url).pathname);
4734
+ PROMPT_DIR = import.meta.dirname ?? path12.dirname(new URL(import.meta.url).pathname);
4550
4735
  }
4551
4736
  });
4552
4737
 
4553
4738
  // src/config.ts
4554
- import fs19 from "fs";
4555
- import path12 from "path";
4739
+ import fs20 from "fs";
4740
+ import path13 from "path";
4556
4741
  import os from "os";
4557
4742
  function loadConfigFile() {
4558
4743
  try {
4559
- const raw = fs19.readFileSync(CONFIG_PATH, "utf-8");
4744
+ const raw = fs20.readFileSync(CONFIG_PATH, "utf-8");
4560
4745
  log.debug("Loaded config file", { path: CONFIG_PATH });
4561
4746
  return JSON.parse(raw);
4562
4747
  } catch (err) {
@@ -4592,7 +4777,7 @@ var init_config = __esm({
4592
4777
  "src/config.ts"() {
4593
4778
  "use strict";
4594
4779
  init_logger();
4595
- CONFIG_PATH = path12.join(
4780
+ CONFIG_PATH = path13.join(
4596
4781
  os.homedir(),
4597
4782
  ".mindstudio-local-tunnel",
4598
4783
  "config.json"
@@ -4607,13 +4792,46 @@ __export(headless_exports, {
4607
4792
  startHeadless: () => startHeadless
4608
4793
  });
4609
4794
  import { createInterface } from "readline";
4610
- import fs20 from "fs";
4611
- import path13 from "path";
4795
+ import fs21 from "fs";
4796
+ import path14 from "path";
4612
4797
  function loadActionPrompt(name) {
4613
- return fs20.readFileSync(path13.join(ACTIONS_DIR, `${name}.md`), "utf-8").trim();
4798
+ return fs21.readFileSync(path14.join(ACTIONS_DIR, `${name}.md`), "utf-8").trim();
4614
4799
  }
4615
- function emit(event, data) {
4616
- process.stdout.write(JSON.stringify({ event, ...data }) + "\n");
4800
+ function emit(event, data, requestId) {
4801
+ const payload = { event, ...data };
4802
+ if (requestId) {
4803
+ payload.requestId = requestId;
4804
+ }
4805
+ process.stdout.write(JSON.stringify(payload) + "\n");
4806
+ }
4807
+ function handleGetHistory(state) {
4808
+ return { messages: state.messages };
4809
+ }
4810
+ function handleClear(state) {
4811
+ clearSession(state);
4812
+ return {};
4813
+ }
4814
+ function handleCancel(currentAbort, pendingTools) {
4815
+ if (currentAbort) {
4816
+ currentAbort.abort();
4817
+ }
4818
+ for (const [id, pending] of pendingTools) {
4819
+ clearTimeout(pending.timeout);
4820
+ pending.resolve("Error: cancelled");
4821
+ pendingTools.delete(id);
4822
+ }
4823
+ return {};
4824
+ }
4825
+ function dispatchSimple(requestId, eventName, handler) {
4826
+ try {
4827
+ const data = handler();
4828
+ if (eventName) {
4829
+ emit(eventName, data, requestId);
4830
+ }
4831
+ emit("completed", { success: true }, requestId);
4832
+ } catch (err) {
4833
+ emit("completed", { success: false, error: err.message }, requestId);
4834
+ }
4617
4835
  }
4618
4836
  async function startHeadless(opts = {}) {
4619
4837
  const stderrWrite = (...args2) => {
@@ -4632,72 +4850,15 @@ async function startHeadless(opts = {}) {
4632
4850
  const state = createAgentState();
4633
4851
  const resumed = loadSession(state);
4634
4852
  if (resumed) {
4635
- emit("session_restored", {
4636
- messageCount: state.messages.length
4637
- });
4853
+ emit("session_restored", { messageCount: state.messages.length });
4638
4854
  }
4639
4855
  let running = false;
4640
4856
  let currentAbort = null;
4857
+ let currentRequestId;
4858
+ let completedEmitted = false;
4641
4859
  const EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
4642
4860
  const pendingTools = /* @__PURE__ */ new Map();
4643
4861
  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
4862
  const USER_FACING_TOOLS = /* @__PURE__ */ new Set([
4702
4863
  "promptUser",
4703
4864
  "confirmDestructiveAction",
@@ -4728,6 +4889,158 @@ async function startHeadless(opts = {}) {
4728
4889
  });
4729
4890
  });
4730
4891
  }
4892
+ function onEvent(e) {
4893
+ const rid = currentRequestId;
4894
+ switch (e.type) {
4895
+ // Suppressed — caller already knows the request started
4896
+ case "turn_started":
4897
+ return;
4898
+ // Terminal events — translate to `completed`
4899
+ case "turn_done":
4900
+ completedEmitted = true;
4901
+ emit("completed", { success: true }, rid);
4902
+ return;
4903
+ case "turn_cancelled":
4904
+ completedEmitted = true;
4905
+ emit("completed", { success: false, error: "cancelled" }, rid);
4906
+ return;
4907
+ // Streaming events — forward with requestId
4908
+ case "text":
4909
+ emit(
4910
+ "text",
4911
+ {
4912
+ text: e.text,
4913
+ ...e.parentToolId && { parentToolId: e.parentToolId }
4914
+ },
4915
+ rid
4916
+ );
4917
+ return;
4918
+ case "thinking":
4919
+ emit(
4920
+ "thinking",
4921
+ {
4922
+ text: e.text,
4923
+ ...e.parentToolId && { parentToolId: e.parentToolId }
4924
+ },
4925
+ rid
4926
+ );
4927
+ return;
4928
+ case "tool_input_delta":
4929
+ emit(
4930
+ "tool_input_delta",
4931
+ {
4932
+ id: e.id,
4933
+ name: e.name,
4934
+ result: e.result,
4935
+ ...e.parentToolId && { parentToolId: e.parentToolId }
4936
+ },
4937
+ rid
4938
+ );
4939
+ return;
4940
+ case "tool_start":
4941
+ emit(
4942
+ "tool_start",
4943
+ {
4944
+ id: e.id,
4945
+ name: e.name,
4946
+ input: e.input,
4947
+ ...e.partial && { partial: true },
4948
+ ...e.parentToolId && { parentToolId: e.parentToolId }
4949
+ },
4950
+ rid
4951
+ );
4952
+ return;
4953
+ case "tool_done":
4954
+ emit(
4955
+ "tool_done",
4956
+ {
4957
+ id: e.id,
4958
+ name: e.name,
4959
+ result: e.result,
4960
+ isError: e.isError,
4961
+ ...e.parentToolId && { parentToolId: e.parentToolId }
4962
+ },
4963
+ rid
4964
+ );
4965
+ return;
4966
+ case "status":
4967
+ emit("status", { message: e.message }, rid);
4968
+ return;
4969
+ case "error":
4970
+ emit("error", { error: e.error }, rid);
4971
+ return;
4972
+ }
4973
+ }
4974
+ async function handleMessage(parsed, requestId) {
4975
+ if (running) {
4976
+ emit(
4977
+ "error",
4978
+ { error: "Agent is already processing a message" },
4979
+ requestId
4980
+ );
4981
+ emit(
4982
+ "completed",
4983
+ { success: false, error: "Agent is already processing a message" },
4984
+ requestId
4985
+ );
4986
+ return;
4987
+ }
4988
+ running = true;
4989
+ currentRequestId = requestId;
4990
+ currentAbort = new AbortController();
4991
+ completedEmitted = false;
4992
+ const attachments = parsed.attachments;
4993
+ if (attachments?.length) {
4994
+ console.warn(
4995
+ `[headless] Message has ${attachments.length} attachment(s):`,
4996
+ attachments.map((a) => a.url)
4997
+ );
4998
+ }
4999
+ let userMessage = parsed.text ?? "";
5000
+ const isCommand = !!parsed.runCommand;
5001
+ if (parsed.runCommand === "sync") {
5002
+ userMessage = loadActionPrompt("sync");
5003
+ } else if (parsed.runCommand === "publish") {
5004
+ userMessage = loadActionPrompt("publish");
5005
+ } else if (parsed.runCommand === "buildFromInitialSpec") {
5006
+ userMessage = loadActionPrompt("buildFromInitialSpec");
5007
+ }
5008
+ const onboardingState = parsed.onboardingState ?? "onboardingFinished";
5009
+ const system = buildSystemPrompt(
5010
+ onboardingState,
5011
+ parsed.viewContext
5012
+ );
5013
+ try {
5014
+ await runTurn({
5015
+ state,
5016
+ userMessage,
5017
+ attachments,
5018
+ apiConfig: config,
5019
+ system,
5020
+ model: opts.model,
5021
+ onboardingState,
5022
+ signal: currentAbort.signal,
5023
+ onEvent,
5024
+ resolveExternalTool,
5025
+ hidden: isCommand
5026
+ });
5027
+ if (!completedEmitted) {
5028
+ emit(
5029
+ "completed",
5030
+ { success: false, error: "Turn ended unexpectedly" },
5031
+ requestId
5032
+ );
5033
+ }
5034
+ } catch (err) {
5035
+ if (!completedEmitted) {
5036
+ emit("error", { error: err.message }, requestId);
5037
+ emit("completed", { success: false, error: err.message }, requestId);
5038
+ }
5039
+ }
5040
+ currentAbort = null;
5041
+ currentRequestId = void 0;
5042
+ running = false;
5043
+ }
4731
5044
  const rl = createInterface({ input: process.stdin });
4732
5045
  rl.on("line", async (line) => {
4733
5046
  let parsed;
@@ -4737,82 +5050,42 @@ async function startHeadless(opts = {}) {
4737
5050
  emit("error", { error: "Invalid JSON on stdin" });
4738
5051
  return;
4739
5052
  }
4740
- if (parsed.action === "tool_result" && parsed.id) {
4741
- const pending = pendingTools.get(parsed.id);
5053
+ const { action, requestId } = parsed;
5054
+ if (action === "tool_result" && parsed.id) {
5055
+ const id = parsed.id;
5056
+ const result = parsed.result ?? "";
5057
+ const pending = pendingTools.get(id);
4742
5058
  if (pending) {
4743
- pendingTools.delete(parsed.id);
4744
- pending.resolve(parsed.result ?? "");
5059
+ pendingTools.delete(id);
5060
+ pending.resolve(result);
4745
5061
  } else {
4746
- earlyResults.set(parsed.id, parsed.result ?? "");
5062
+ earlyResults.set(id, result);
4747
5063
  }
4748
5064
  return;
4749
5065
  }
4750
- if (parsed.action === "get_history") {
4751
- emit("history", {
4752
- messages: state.messages
4753
- });
5066
+ if (action === "get_history") {
5067
+ dispatchSimple(requestId, "history", () => handleGetHistory(state));
4754
5068
  return;
4755
5069
  }
4756
- if (parsed.action === "clear") {
4757
- clearSession(state);
4758
- emit("session_cleared");
5070
+ if (action === "clear") {
5071
+ dispatchSimple(requestId, "session_cleared", () => handleClear(state));
4759
5072
  return;
4760
5073
  }
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
- }
5074
+ if (action === "cancel") {
5075
+ handleCancel(currentAbort, pendingTools);
5076
+ emit("completed", { success: true }, requestId);
4770
5077
  return;
4771
5078
  }
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;
5079
+ if (action === "message") {
5080
+ await handleMessage(parsed, requestId);
5081
+ return;
4815
5082
  }
5083
+ emit("error", { error: `Unknown action: ${action}` }, requestId);
5084
+ emit(
5085
+ "completed",
5086
+ { success: false, error: `Unknown action: ${action}` },
5087
+ requestId
5088
+ );
4816
5089
  });
4817
5090
  rl.on("close", () => {
4818
5091
  emit("stopping");
@@ -4837,16 +5110,16 @@ var init_headless = __esm({
4837
5110
  init_lsp();
4838
5111
  init_agent();
4839
5112
  init_session();
4840
- BASE_DIR = import.meta.dirname ?? path13.dirname(new URL(import.meta.url).pathname);
4841
- ACTIONS_DIR = path13.join(BASE_DIR, "actions");
5113
+ BASE_DIR = import.meta.dirname ?? path14.dirname(new URL(import.meta.url).pathname);
5114
+ ACTIONS_DIR = path14.join(BASE_DIR, "actions");
4842
5115
  }
4843
5116
  });
4844
5117
 
4845
5118
  // src/index.tsx
4846
5119
  import { render } from "ink";
4847
5120
  import os2 from "os";
4848
- import fs21 from "fs";
4849
- import path14 from "path";
5121
+ import fs22 from "fs";
5122
+ import path15 from "path";
4850
5123
 
4851
5124
  // src/tui/App.tsx
4852
5125
  import { useState as useState2, useCallback, useRef } from "react";
@@ -5163,8 +5436,8 @@ for (let i = 0; i < args.length; i++) {
5163
5436
  }
5164
5437
  function printDebugInfo(config) {
5165
5438
  const pkg = JSON.parse(
5166
- fs21.readFileSync(
5167
- path14.join(import.meta.dirname, "..", "package.json"),
5439
+ fs22.readFileSync(
5440
+ path15.join(import.meta.dirname, "..", "package.json"),
5168
5441
  "utf-8"
5169
5442
  )
5170
5443
  );