@mindstudio-ai/remy 0.1.109 → 0.1.111

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/headless.js CHANGED
@@ -414,7 +414,7 @@ ${isLspConfigured() ? `<typescript_lsp>
414
414
  <conversation_summaries>
415
415
  Your conversation history may include <prior_conversation_summary> blocks in the user's messages. These are automated summaries of earlier messages that have been compacted to save context space. The user does not see this summary, they see the full conversation history in their UI. Treat the summary as ground truth for what happened before, but do not reference it directly to the user ("as mentioned in the summary..."). Just continue naturally as if you remember the prior work.
416
416
 
417
- Old tool results are periodically cleared from the conversation to save context space. This is automatic and expected \u2014 you don't need to note down or preserve information from tool results. If you need to reference something from an earlier tool call, just re-read the file or re-run the query, or use your .remy-notes.md file.
417
+ Old tool results are periodically cleared from the conversation to save context space. This is automatic and expected \u2014 you don't need to note down or preserve information from tool results. If you need to reference something from an earlier tool call, just re-read the file or re-run the query.
418
418
  </conversation_summaries>
419
419
 
420
420
  <project_onboarding>
@@ -2235,11 +2235,62 @@ var globTool = {
2235
2235
 
2236
2236
  // src/tools/code/listDir.ts
2237
2237
  import fs12 from "fs/promises";
2238
+ import path7 from "path";
2239
+ var EXCLUDE = /* @__PURE__ */ new Set([".git", "node_modules"]);
2240
+ var MAX_CHILDREN = 15;
2241
+ async function readAndSort(dirPath) {
2242
+ const entries = await fs12.readdir(dirPath, { withFileTypes: true });
2243
+ return entries.filter((e) => !EXCLUDE.has(e.name)).sort((a, b) => {
2244
+ if (a.isDirectory() && !b.isDirectory()) {
2245
+ return -1;
2246
+ }
2247
+ if (!a.isDirectory() && b.isDirectory()) {
2248
+ return 1;
2249
+ }
2250
+ return a.name.localeCompare(b.name);
2251
+ });
2252
+ }
2253
+ async function collapsePath(basePath, name) {
2254
+ let display = name;
2255
+ let current = path7.join(basePath, name);
2256
+ for (; ; ) {
2257
+ let children;
2258
+ try {
2259
+ children = await readAndSort(current);
2260
+ } catch {
2261
+ break;
2262
+ }
2263
+ if (children.length === 1 && children[0].isDirectory()) {
2264
+ display += "/" + children[0].name;
2265
+ current = path7.join(current, children[0].name);
2266
+ } else {
2267
+ break;
2268
+ }
2269
+ }
2270
+ return [display, current];
2271
+ }
2272
+ function formatSize(bytes) {
2273
+ if (bytes < 1e3) {
2274
+ return `${bytes} B`;
2275
+ }
2276
+ if (bytes < 1e6) {
2277
+ return `${(bytes / 1e3).toFixed(1)} kB`;
2278
+ }
2279
+ return `${(bytes / 1e6).toFixed(1)} MB`;
2280
+ }
2281
+ async function formatFile(dirPath, name, indent) {
2282
+ try {
2283
+ const stat = await fs12.stat(path7.join(dirPath, name));
2284
+ return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(stat.size)}`;
2285
+ } catch {
2286
+ return `${indent}${name}`;
2287
+ }
2288
+ }
2238
2289
  var listDirTool = {
2239
2290
  clearable: true,
2240
2291
  definition: {
2241
2292
  name: "listDir",
2242
- description: "List the contents of a directory. Shows entries with / suffix for directories, sorted directories-first then alphabetically. Use this for a quick overview of a directory's contents. For finding files across the whole project, use glob instead.",
2293
+ description: "List the contents of a directory with one level of subdirectory expansion. Shows file sizes and collapses single-child directory chains (a/b/c/ shown as one entry). Use this for a quick overview of a directory's structure. For finding files across the whole project, use glob instead.",
2243
2294
  inputSchema: {
2244
2295
  type: "object",
2245
2296
  properties: {
@@ -2253,16 +2304,34 @@ var listDirTool = {
2253
2304
  async execute(input) {
2254
2305
  const dirPath = input.path || ".";
2255
2306
  try {
2256
- const entries = await fs12.readdir(dirPath, { withFileTypes: true });
2257
- const lines = entries.filter((e) => e.name !== ".git" && e.name !== "node_modules").sort((a, b) => {
2258
- if (a.isDirectory() && !b.isDirectory()) {
2259
- return -1;
2260
- }
2261
- if (!a.isDirectory() && b.isDirectory()) {
2262
- return 1;
2307
+ const entries = await readAndSort(dirPath);
2308
+ const lines = [];
2309
+ for (const entry of entries) {
2310
+ if (entry.isDirectory()) {
2311
+ const [displayName, finalPath] = await collapsePath(
2312
+ dirPath,
2313
+ entry.name
2314
+ );
2315
+ lines.push(`${displayName}/`);
2316
+ try {
2317
+ const children = await readAndSort(finalPath);
2318
+ const capped = children.slice(0, MAX_CHILDREN);
2319
+ for (const child of capped) {
2320
+ if (child.isDirectory()) {
2321
+ lines.push(` ${child.name}/`);
2322
+ } else {
2323
+ lines.push(await formatFile(finalPath, child.name, " "));
2324
+ }
2325
+ }
2326
+ if (children.length > MAX_CHILDREN) {
2327
+ lines.push(` ... and ${children.length - MAX_CHILDREN} more`);
2328
+ }
2329
+ } catch {
2330
+ }
2331
+ } else {
2332
+ lines.push(await formatFile(dirPath, entry.name, ""));
2263
2333
  }
2264
- return a.name.localeCompare(b.name);
2265
- }).map((e) => e.isDirectory() ? `${e.name}/` : e.name);
2334
+ }
2266
2335
  return lines.join("\n") || "(empty directory)";
2267
2336
  } catch (err) {
2268
2337
  return `Error listing directory: ${err.message}`;
@@ -2455,26 +2524,44 @@ async function analyzeImage(params) {
2455
2524
  var SCREENSHOT_ANALYSIS_PROMPT = `Describe everything visible on screen from top to bottom \u2014 every element, its position, its size relative to the viewport, its colors, its content. Be comprehensive, thorough, and spatial. After the inventory, note anything that looks visually broken (overlapping elements, clipped text, misaligned components).
2456
2525
 
2457
2526
  Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.`;
2527
+ var TEXT_WRAP_DISCLAIMER = `Note: ignore text wrapping issues. Screenshots occasionally show text wrapping onto an extra line compared to the live page \u2014 most noticeable in buttons, badges, and headings. This is a known limitation of SVG foreignObject rendering used the DOM-to-image capture library that took the screenshot. The browser's SVG renderer computes slightly wider text metrics than the HTML layout engine, so text that fits on one line in the live DOM can overflow by a fraction of a pixel in the capture - this is not a real issue.`;
2528
+ function buildScreenshotAnalysisPrompt(opts) {
2529
+ let p = opts?.prompt || SCREENSHOT_ANALYSIS_PROMPT;
2530
+ if (opts?.styleMap) {
2531
+ p += `
2532
+
2533
+ The following styleMap describes the computed layout state at the moment of capture. Use it to verify typography, spacing, overflow, and element dimensions \u2014 it is more accurate than visual estimation from the image.
2534
+
2535
+ <style_map>
2536
+ ${opts.styleMap}
2537
+ </style_map>`;
2538
+ }
2539
+ p += `
2540
+
2541
+ ${TEXT_WRAP_DISCLAIMER}`;
2542
+ return p;
2543
+ }
2458
2544
  async function captureAndAnalyzeScreenshot(promptOrOptions) {
2459
2545
  let prompt;
2460
2546
  let existingUrl;
2461
2547
  let onLog;
2462
- let path9;
2548
+ let path10;
2463
2549
  if (typeof promptOrOptions === "object" && promptOrOptions !== null) {
2464
2550
  prompt = promptOrOptions.prompt;
2465
2551
  existingUrl = promptOrOptions.imageUrl;
2466
- path9 = promptOrOptions.path;
2552
+ path10 = promptOrOptions.path;
2467
2553
  onLog = promptOrOptions.onLog;
2468
2554
  } else {
2469
2555
  prompt = promptOrOptions;
2470
2556
  }
2471
2557
  let url;
2558
+ let styleMap;
2472
2559
  if (existingUrl) {
2473
2560
  url = existingUrl;
2474
2561
  } else {
2475
2562
  const ssResult = await sidecarRequest(
2476
2563
  "/screenshot-full-page",
2477
- path9 ? { path: path9 } : void 0,
2564
+ path10 ? { path: path10 } : void 0,
2478
2565
  { timeout: 12e4 }
2479
2566
  );
2480
2567
  url = ssResult?.url || ssResult?.screenshotUrl;
@@ -2483,20 +2570,21 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
2483
2570
  `No URL in sidecar response. The browser may not be ready yet. Response: ${JSON.stringify(ssResult)}`
2484
2571
  );
2485
2572
  }
2573
+ styleMap = ssResult?.styleMap;
2486
2574
  }
2487
2575
  if (prompt === false) {
2488
2576
  return url;
2489
2577
  }
2490
- let analysisPrompt = prompt || SCREENSHOT_ANALYSIS_PROMPT;
2491
- analysisPrompt += `
2492
- Note: ignore text wrapping issues. Screenshots occasionally show text wrapping onto an extra line compared to the live page \u2014 most noticeable in buttons, badges, and headings. This is a known limitation of SVG foreignObject rendering used the DOM-to-image capture library that took the screenshot. The browser's SVG renderer computes slightly wider text metrics than the HTML layout engine, so text that fits on one line in the live DOM can overflow by a fraction of a pixel in the capture - this is not a real issue.
2493
- `;
2578
+ const analysisPrompt = buildScreenshotAnalysisPrompt({
2579
+ prompt: prompt || void 0,
2580
+ styleMap
2581
+ });
2494
2582
  const analysis = await analyzeImage({
2495
2583
  prompt: analysisPrompt,
2496
2584
  imageUrl: url,
2497
2585
  onLog
2498
2586
  });
2499
- return JSON.stringify({ url, analysis });
2587
+ return JSON.stringify({ url, analysis, ...styleMap ? { styleMap } : {} });
2500
2588
  }
2501
2589
 
2502
2590
  // src/tools/code/screenshot.ts
@@ -3281,7 +3369,9 @@ var browserAutomationTool = {
3281
3369
  stepType: "analyzeImage",
3282
3370
  step: {
3283
3371
  imageUrl: s.result.url,
3284
- prompt: SCREENSHOT_ANALYSIS_PROMPT
3372
+ prompt: buildScreenshotAnalysisPrompt({
3373
+ styleMap: s.result.styleMap
3374
+ })
3285
3375
  }
3286
3376
  }));
3287
3377
  const batchResult = await runCli(
@@ -3460,10 +3550,6 @@ __export(analyzeImage_exports, {
3460
3550
  definition: () => definition4,
3461
3551
  execute: () => execute4
3462
3552
  });
3463
- var DEFAULT_PROMPT = `
3464
- Describe everything visible in this image \u2014 every element, its position, its size relative to the frame, its colors, its content. Be comprehensive, thorough and spatial. After the inventory, note anything that looks visually broken (overlapping elements, clipped text, misaligned components).
3465
-
3466
- Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.`;
3467
3553
  var definition4 = {
3468
3554
  clearable: true,
3469
3555
  name: "analyzeImage",
@@ -3485,11 +3571,9 @@ var definition4 = {
3485
3571
  };
3486
3572
  async function execute4(input, onLog) {
3487
3573
  const imageUrl = input.imageUrl;
3488
- let prompt = input.prompt || DEFAULT_PROMPT;
3489
- prompt += `
3490
-
3491
- Note: ignore text wrapping issues. Screenshots occasionally show text wrapping onto an extra line compared to the live page \u2014 most noticeable in buttons, badges, and headings. This is a known limitation of SVG foreignObject rendering used the DOM-to-image capture library that took the screenshot. The browser's SVG renderer computes slightly wider text metrics than the HTML layout engine, so text that fits on one line in the live DOM can overflow by a fraction of a pixel in the capture - this is not a real issue.
3492
- `;
3574
+ const prompt = buildScreenshotAnalysisPrompt({
3575
+ prompt: input.prompt
3576
+ });
3493
3577
  const analysis = await analyzeImage({
3494
3578
  prompt,
3495
3579
  imageUrl,
@@ -3538,13 +3622,26 @@ async function execute5(input, onLog, context) {
3538
3622
  return `Error: browser navigation completed but no screenshot URL was returned. Agent output: ${result}`;
3539
3623
  }
3540
3624
  const url = urlMatch[0];
3541
- const analysisPrompt = input.prompt || SCREENSHOT_ANALYSIS_PROMPT;
3625
+ let styleMap;
3626
+ try {
3627
+ const parsed = JSON.parse(result);
3628
+ styleMap = parsed?.styleMap;
3629
+ } catch {
3630
+ }
3631
+ const analysisPrompt = buildScreenshotAnalysisPrompt({
3632
+ prompt: input.prompt,
3633
+ styleMap
3634
+ });
3542
3635
  const analysis = await analyzeImage({
3543
3636
  prompt: analysisPrompt,
3544
3637
  imageUrl: url,
3545
3638
  onLog
3546
3639
  });
3547
- return JSON.stringify({ url, analysis });
3640
+ return JSON.stringify({
3641
+ url,
3642
+ analysis,
3643
+ ...styleMap ? { styleMap } : {}
3644
+ });
3548
3645
  } catch (err) {
3549
3646
  return `Error taking interactive screenshot: ${err.message}`;
3550
3647
  }
@@ -3824,12 +3921,12 @@ async function executeDesignExpertTool(name, input, context, toolCallId, onLog)
3824
3921
 
3825
3922
  // src/subagents/common/context.ts
3826
3923
  import fs14 from "fs";
3827
- import path7 from "path";
3924
+ import path8 from "path";
3828
3925
  function walkMdFiles2(dir, skip) {
3829
3926
  const files = [];
3830
3927
  try {
3831
3928
  for (const entry of fs14.readdirSync(dir, { withFileTypes: true })) {
3832
- const full = path7.join(dir, entry.name);
3929
+ const full = path8.join(dir, entry.name);
3833
3930
  if (entry.isDirectory()) {
3834
3931
  if (!skip?.has(entry.name)) {
3835
3932
  files.push(...walkMdFiles2(full, skip));
@@ -4309,7 +4406,7 @@ var VISION_TOOLS = [
4309
4406
 
4310
4407
  // src/subagents/productVision/executor.ts
4311
4408
  import fs16 from "fs";
4312
- import path8 from "path";
4409
+ import path9 from "path";
4313
4410
  var ROADMAP_DIR = "src/roadmap";
4314
4411
  function formatRequires(requires) {
4315
4412
  return requires.length === 0 ? "[]" : `[${requires.map((r) => `"${r}"`).join(", ")}]`;
@@ -4325,7 +4422,7 @@ async function executeVisionTool(name, input) {
4325
4422
  requires,
4326
4423
  body
4327
4424
  } = input;
4328
- const filePath = path8.join(ROADMAP_DIR, `${slug}.md`);
4425
+ const filePath = path9.join(ROADMAP_DIR, `${slug}.md`);
4329
4426
  try {
4330
4427
  fs16.mkdirSync(ROADMAP_DIR, { recursive: true });
4331
4428
  const oldContent = fs16.existsSync(filePath) ? fs16.readFileSync(filePath, "utf-8") : "";
@@ -4351,7 +4448,7 @@ ${unifiedDiff(filePath, oldContent, content)}`;
4351
4448
  }
4352
4449
  case "updateRoadmapItem": {
4353
4450
  const { slug } = input;
4354
- const filePath = path8.join(ROADMAP_DIR, `${slug}.md`);
4451
+ const filePath = path9.join(ROADMAP_DIR, `${slug}.md`);
4355
4452
  try {
4356
4453
  if (!fs16.existsSync(filePath)) {
4357
4454
  return `Error: ${filePath} does not exist`;
@@ -4419,7 +4516,7 @@ ${unifiedDiff(filePath, oldContent, content)}`;
4419
4516
  }
4420
4517
  case "deleteRoadmapItem": {
4421
4518
  const { slug } = input;
4422
- const filePath = path8.join(ROADMAP_DIR, `${slug}.md`);
4519
+ const filePath = path9.join(ROADMAP_DIR, `${slug}.md`);
4423
4520
  try {
4424
4521
  if (!fs16.existsSync(filePath)) {
4425
4522
  return `Error: ${filePath} does not exist`;
@@ -6044,6 +6141,9 @@ ${xmlParts}
6044
6141
  if (pending) {
6045
6142
  pendingTools.delete(id);
6046
6143
  pending.resolve(result);
6144
+ } else if (!running) {
6145
+ log10.info("Late tool_result while idle, dismissing", { id });
6146
+ emit("completed", { success: true }, requestId);
6047
6147
  } else {
6048
6148
  earlyResults.set(id, result);
6049
6149
  }
package/dist/index.js CHANGED
@@ -1829,15 +1829,66 @@ var init_glob = __esm({
1829
1829
 
1830
1830
  // src/tools/code/listDir.ts
1831
1831
  import fs9 from "fs/promises";
1832
- var listDirTool;
1832
+ import path4 from "path";
1833
+ async function readAndSort(dirPath) {
1834
+ const entries = await fs9.readdir(dirPath, { withFileTypes: true });
1835
+ return entries.filter((e) => !EXCLUDE.has(e.name)).sort((a, b) => {
1836
+ if (a.isDirectory() && !b.isDirectory()) {
1837
+ return -1;
1838
+ }
1839
+ if (!a.isDirectory() && b.isDirectory()) {
1840
+ return 1;
1841
+ }
1842
+ return a.name.localeCompare(b.name);
1843
+ });
1844
+ }
1845
+ async function collapsePath(basePath, name) {
1846
+ let display = name;
1847
+ let current = path4.join(basePath, name);
1848
+ for (; ; ) {
1849
+ let children;
1850
+ try {
1851
+ children = await readAndSort(current);
1852
+ } catch {
1853
+ break;
1854
+ }
1855
+ if (children.length === 1 && children[0].isDirectory()) {
1856
+ display += "/" + children[0].name;
1857
+ current = path4.join(current, children[0].name);
1858
+ } else {
1859
+ break;
1860
+ }
1861
+ }
1862
+ return [display, current];
1863
+ }
1864
+ function formatSize(bytes) {
1865
+ if (bytes < 1e3) {
1866
+ return `${bytes} B`;
1867
+ }
1868
+ if (bytes < 1e6) {
1869
+ return `${(bytes / 1e3).toFixed(1)} kB`;
1870
+ }
1871
+ return `${(bytes / 1e6).toFixed(1)} MB`;
1872
+ }
1873
+ async function formatFile(dirPath, name, indent) {
1874
+ try {
1875
+ const stat = await fs9.stat(path4.join(dirPath, name));
1876
+ return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(stat.size)}`;
1877
+ } catch {
1878
+ return `${indent}${name}`;
1879
+ }
1880
+ }
1881
+ var EXCLUDE, MAX_CHILDREN, listDirTool;
1833
1882
  var init_listDir = __esm({
1834
1883
  "src/tools/code/listDir.ts"() {
1835
1884
  "use strict";
1885
+ EXCLUDE = /* @__PURE__ */ new Set([".git", "node_modules"]);
1886
+ MAX_CHILDREN = 15;
1836
1887
  listDirTool = {
1837
1888
  clearable: true,
1838
1889
  definition: {
1839
1890
  name: "listDir",
1840
- description: "List the contents of a directory. Shows entries with / suffix for directories, sorted directories-first then alphabetically. Use this for a quick overview of a directory's contents. For finding files across the whole project, use glob instead.",
1891
+ description: "List the contents of a directory with one level of subdirectory expansion. Shows file sizes and collapses single-child directory chains (a/b/c/ shown as one entry). Use this for a quick overview of a directory's structure. For finding files across the whole project, use glob instead.",
1841
1892
  inputSchema: {
1842
1893
  type: "object",
1843
1894
  properties: {
@@ -1851,16 +1902,34 @@ var init_listDir = __esm({
1851
1902
  async execute(input) {
1852
1903
  const dirPath = input.path || ".";
1853
1904
  try {
1854
- const entries = await fs9.readdir(dirPath, { withFileTypes: true });
1855
- const lines = entries.filter((e) => e.name !== ".git" && e.name !== "node_modules").sort((a, b) => {
1856
- if (a.isDirectory() && !b.isDirectory()) {
1857
- return -1;
1858
- }
1859
- if (!a.isDirectory() && b.isDirectory()) {
1860
- return 1;
1905
+ const entries = await readAndSort(dirPath);
1906
+ const lines = [];
1907
+ for (const entry of entries) {
1908
+ if (entry.isDirectory()) {
1909
+ const [displayName, finalPath] = await collapsePath(
1910
+ dirPath,
1911
+ entry.name
1912
+ );
1913
+ lines.push(`${displayName}/`);
1914
+ try {
1915
+ const children = await readAndSort(finalPath);
1916
+ const capped = children.slice(0, MAX_CHILDREN);
1917
+ for (const child of capped) {
1918
+ if (child.isDirectory()) {
1919
+ lines.push(` ${child.name}/`);
1920
+ } else {
1921
+ lines.push(await formatFile(finalPath, child.name, " "));
1922
+ }
1923
+ }
1924
+ if (children.length > MAX_CHILDREN) {
1925
+ lines.push(` ... and ${children.length - MAX_CHILDREN} more`);
1926
+ }
1927
+ } catch {
1928
+ }
1929
+ } else {
1930
+ lines.push(await formatFile(dirPath, entry.name, ""));
1861
1931
  }
1862
- return a.name.localeCompare(b.name);
1863
- }).map((e) => e.isDirectory() ? `${e.name}/` : e.name);
1932
+ }
1864
1933
  return lines.join("\n") || "(empty directory)";
1865
1934
  } catch (err) {
1866
1935
  return `Error listing directory: ${err.message}`;
@@ -2154,26 +2223,43 @@ var init_analyzeImage = __esm({
2154
2223
  });
2155
2224
 
2156
2225
  // src/tools/_helpers/screenshot.ts
2226
+ function buildScreenshotAnalysisPrompt(opts) {
2227
+ let p = opts?.prompt || SCREENSHOT_ANALYSIS_PROMPT;
2228
+ if (opts?.styleMap) {
2229
+ p += `
2230
+
2231
+ The following styleMap describes the computed layout state at the moment of capture. Use it to verify typography, spacing, overflow, and element dimensions \u2014 it is more accurate than visual estimation from the image.
2232
+
2233
+ <style_map>
2234
+ ${opts.styleMap}
2235
+ </style_map>`;
2236
+ }
2237
+ p += `
2238
+
2239
+ ${TEXT_WRAP_DISCLAIMER}`;
2240
+ return p;
2241
+ }
2157
2242
  async function captureAndAnalyzeScreenshot(promptOrOptions) {
2158
2243
  let prompt;
2159
2244
  let existingUrl;
2160
2245
  let onLog;
2161
- let path10;
2246
+ let path11;
2162
2247
  if (typeof promptOrOptions === "object" && promptOrOptions !== null) {
2163
2248
  prompt = promptOrOptions.prompt;
2164
2249
  existingUrl = promptOrOptions.imageUrl;
2165
- path10 = promptOrOptions.path;
2250
+ path11 = promptOrOptions.path;
2166
2251
  onLog = promptOrOptions.onLog;
2167
2252
  } else {
2168
2253
  prompt = promptOrOptions;
2169
2254
  }
2170
2255
  let url;
2256
+ let styleMap;
2171
2257
  if (existingUrl) {
2172
2258
  url = existingUrl;
2173
2259
  } else {
2174
2260
  const ssResult = await sidecarRequest(
2175
2261
  "/screenshot-full-page",
2176
- path10 ? { path: path10 } : void 0,
2262
+ path11 ? { path: path11 } : void 0,
2177
2263
  { timeout: 12e4 }
2178
2264
  );
2179
2265
  url = ssResult?.url || ssResult?.screenshotUrl;
@@ -2182,22 +2268,23 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
2182
2268
  `No URL in sidecar response. The browser may not be ready yet. Response: ${JSON.stringify(ssResult)}`
2183
2269
  );
2184
2270
  }
2271
+ styleMap = ssResult?.styleMap;
2185
2272
  }
2186
2273
  if (prompt === false) {
2187
2274
  return url;
2188
2275
  }
2189
- let analysisPrompt = prompt || SCREENSHOT_ANALYSIS_PROMPT;
2190
- analysisPrompt += `
2191
- Note: ignore text wrapping issues. Screenshots occasionally show text wrapping onto an extra line compared to the live page \u2014 most noticeable in buttons, badges, and headings. This is a known limitation of SVG foreignObject rendering used the DOM-to-image capture library that took the screenshot. The browser's SVG renderer computes slightly wider text metrics than the HTML layout engine, so text that fits on one line in the live DOM can overflow by a fraction of a pixel in the capture - this is not a real issue.
2192
- `;
2276
+ const analysisPrompt = buildScreenshotAnalysisPrompt({
2277
+ prompt: prompt || void 0,
2278
+ styleMap
2279
+ });
2193
2280
  const analysis = await analyzeImage({
2194
2281
  prompt: analysisPrompt,
2195
2282
  imageUrl: url,
2196
2283
  onLog
2197
2284
  });
2198
- return JSON.stringify({ url, analysis });
2285
+ return JSON.stringify({ url, analysis, ...styleMap ? { styleMap } : {} });
2199
2286
  }
2200
- var SCREENSHOT_ANALYSIS_PROMPT;
2287
+ var SCREENSHOT_ANALYSIS_PROMPT, TEXT_WRAP_DISCLAIMER;
2201
2288
  var init_screenshot = __esm({
2202
2289
  "src/tools/_helpers/screenshot.ts"() {
2203
2290
  "use strict";
@@ -2206,6 +2293,7 @@ var init_screenshot = __esm({
2206
2293
  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 comprehensive, thorough, and spatial. After the inventory, note anything that looks visually broken (overlapping elements, clipped text, misaligned components).
2207
2294
 
2208
2295
  Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.`;
2296
+ TEXT_WRAP_DISCLAIMER = `Note: ignore text wrapping issues. Screenshots occasionally show text wrapping onto an extra line compared to the live page \u2014 most noticeable in buttons, badges, and headings. This is a known limitation of SVG foreignObject rendering used the DOM-to-image capture library that took the screenshot. The browser's SVG renderer computes slightly wider text metrics than the HTML layout engine, so text that fits on one line in the live DOM can overflow by a fraction of a pixel in the capture - this is not a real issue.`;
2209
2297
  }
2210
2298
  });
2211
2299
 
@@ -2917,19 +3005,19 @@ var init_tools = __esm({
2917
3005
 
2918
3006
  // src/assets.ts
2919
3007
  import fs10 from "fs";
2920
- import path4 from "path";
3008
+ import path5 from "path";
2921
3009
  function findRoot(start) {
2922
3010
  let dir = start;
2923
- while (dir !== path4.dirname(dir)) {
2924
- if (fs10.existsSync(path4.join(dir, "package.json"))) {
3011
+ while (dir !== path5.dirname(dir)) {
3012
+ if (fs10.existsSync(path5.join(dir, "package.json"))) {
2925
3013
  return dir;
2926
3014
  }
2927
- dir = path4.dirname(dir);
3015
+ dir = path5.dirname(dir);
2928
3016
  }
2929
3017
  return start;
2930
3018
  }
2931
3019
  function assetPath(...segments) {
2932
- return path4.join(ASSETS_BASE, ...segments);
3020
+ return path5.join(ASSETS_BASE, ...segments);
2933
3021
  }
2934
3022
  function readAsset(...segments) {
2935
3023
  const full = assetPath(...segments);
@@ -2952,9 +3040,9 @@ var init_assets = __esm({
2952
3040
  "src/assets.ts"() {
2953
3041
  "use strict";
2954
3042
  ROOT = findRoot(
2955
- import.meta.dirname ?? path4.dirname(new URL(import.meta.url).pathname)
3043
+ import.meta.dirname ?? path5.dirname(new URL(import.meta.url).pathname)
2956
3044
  );
2957
- ASSETS_BASE = fs10.existsSync(path4.join(ROOT, "dist", "prompt")) ? path4.join(ROOT, "dist") : path4.join(ROOT, "src");
3045
+ ASSETS_BASE = fs10.existsSync(path5.join(ROOT, "dist", "prompt")) ? path5.join(ROOT, "dist") : path5.join(ROOT, "src");
2958
3046
  }
2959
3047
  });
2960
3048
 
@@ -3085,7 +3173,9 @@ var init_browserAutomation = __esm({
3085
3173
  stepType: "analyzeImage",
3086
3174
  step: {
3087
3175
  imageUrl: s.result.url,
3088
- prompt: SCREENSHOT_ANALYSIS_PROMPT
3176
+ prompt: buildScreenshotAnalysisPrompt({
3177
+ styleMap: s.result.styleMap
3178
+ })
3089
3179
  }
3090
3180
  }));
3091
3181
  const batchResult = await runCli(
@@ -3290,11 +3380,9 @@ __export(analyzeImage_exports, {
3290
3380
  });
3291
3381
  async function execute4(input, onLog) {
3292
3382
  const imageUrl = input.imageUrl;
3293
- let prompt = input.prompt || DEFAULT_PROMPT;
3294
- prompt += `
3295
-
3296
- Note: ignore text wrapping issues. Screenshots occasionally show text wrapping onto an extra line compared to the live page \u2014 most noticeable in buttons, badges, and headings. This is a known limitation of SVG foreignObject rendering used the DOM-to-image capture library that took the screenshot. The browser's SVG renderer computes slightly wider text metrics than the HTML layout engine, so text that fits on one line in the live DOM can overflow by a fraction of a pixel in the capture - this is not a real issue.
3297
- `;
3383
+ const prompt = buildScreenshotAnalysisPrompt({
3384
+ prompt: input.prompt
3385
+ });
3298
3386
  const analysis = await analyzeImage({
3299
3387
  prompt,
3300
3388
  imageUrl,
@@ -3302,15 +3390,12 @@ async function execute4(input, onLog) {
3302
3390
  });
3303
3391
  return JSON.stringify({ url: imageUrl, analysis });
3304
3392
  }
3305
- var DEFAULT_PROMPT, definition4;
3393
+ var definition4;
3306
3394
  var init_analyzeImage2 = __esm({
3307
3395
  "src/subagents/designExpert/tools/analyzeImage.ts"() {
3308
3396
  "use strict";
3309
3397
  init_analyzeImage();
3310
- DEFAULT_PROMPT = `
3311
- Describe everything visible in this image \u2014 every element, its position, its size relative to the frame, its colors, its content. Be comprehensive, thorough and spatial. After the inventory, note anything that looks visually broken (overlapping elements, clipped text, misaligned components).
3312
-
3313
- Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.`;
3398
+ init_screenshot();
3314
3399
  definition4 = {
3315
3400
  clearable: true,
3316
3401
  name: "analyzeImage",
@@ -3351,13 +3436,26 @@ async function execute5(input, onLog, context) {
3351
3436
  return `Error: browser navigation completed but no screenshot URL was returned. Agent output: ${result}`;
3352
3437
  }
3353
3438
  const url = urlMatch[0];
3354
- const analysisPrompt = input.prompt || SCREENSHOT_ANALYSIS_PROMPT;
3439
+ let styleMap;
3440
+ try {
3441
+ const parsed = JSON.parse(result);
3442
+ styleMap = parsed?.styleMap;
3443
+ } catch {
3444
+ }
3445
+ const analysisPrompt = buildScreenshotAnalysisPrompt({
3446
+ prompt: input.prompt,
3447
+ styleMap
3448
+ });
3355
3449
  const analysis = await analyzeImage({
3356
3450
  prompt: analysisPrompt,
3357
3451
  imageUrl: url,
3358
3452
  onLog
3359
3453
  });
3360
- return JSON.stringify({ url, analysis });
3454
+ return JSON.stringify({
3455
+ url,
3456
+ analysis,
3457
+ ...styleMap ? { styleMap } : {}
3458
+ });
3361
3459
  } catch (err) {
3362
3460
  return `Error taking interactive screenshot: ${err.message}`;
3363
3461
  }
@@ -3710,12 +3808,12 @@ var init_tools2 = __esm({
3710
3808
 
3711
3809
  // src/subagents/common/context.ts
3712
3810
  import fs12 from "fs";
3713
- import path5 from "path";
3811
+ import path6 from "path";
3714
3812
  function walkMdFiles(dir, skip) {
3715
3813
  const files = [];
3716
3814
  try {
3717
3815
  for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
3718
- const full = path5.join(dir, entry.name);
3816
+ const full = path6.join(dir, entry.name);
3719
3817
  if (entry.isDirectory()) {
3720
3818
  if (!skip?.has(entry.name)) {
3721
3819
  files.push(...walkMdFiles(full, skip));
@@ -4263,7 +4361,7 @@ var init_tools3 = __esm({
4263
4361
 
4264
4362
  // src/subagents/productVision/executor.ts
4265
4363
  import fs14 from "fs";
4266
- import path6 from "path";
4364
+ import path7 from "path";
4267
4365
  function formatRequires(requires) {
4268
4366
  return requires.length === 0 ? "[]" : `[${requires.map((r) => `"${r}"`).join(", ")}]`;
4269
4367
  }
@@ -4278,7 +4376,7 @@ async function executeVisionTool(name, input) {
4278
4376
  requires,
4279
4377
  body
4280
4378
  } = input;
4281
- const filePath = path6.join(ROADMAP_DIR, `${slug}.md`);
4379
+ const filePath = path7.join(ROADMAP_DIR, `${slug}.md`);
4282
4380
  try {
4283
4381
  fs14.mkdirSync(ROADMAP_DIR, { recursive: true });
4284
4382
  const oldContent = fs14.existsSync(filePath) ? fs14.readFileSync(filePath, "utf-8") : "";
@@ -4304,7 +4402,7 @@ ${unifiedDiff(filePath, oldContent, content)}`;
4304
4402
  }
4305
4403
  case "updateRoadmapItem": {
4306
4404
  const { slug } = input;
4307
- const filePath = path6.join(ROADMAP_DIR, `${slug}.md`);
4405
+ const filePath = path7.join(ROADMAP_DIR, `${slug}.md`);
4308
4406
  try {
4309
4407
  if (!fs14.existsSync(filePath)) {
4310
4408
  return `Error: ${filePath} does not exist`;
@@ -4372,7 +4470,7 @@ ${unifiedDiff(filePath, oldContent, content)}`;
4372
4470
  }
4373
4471
  case "deleteRoadmapItem": {
4374
4472
  const { slug } = input;
4375
- const filePath = path6.join(ROADMAP_DIR, `${slug}.md`);
4473
+ const filePath = path7.join(ROADMAP_DIR, `${slug}.md`);
4376
4474
  try {
4377
4475
  if (!fs14.existsSync(filePath)) {
4378
4476
  return `Error: ${filePath} does not exist`;
@@ -5592,7 +5690,7 @@ var init_agent = __esm({
5592
5690
 
5593
5691
  // src/prompt/static/projectContext.ts
5594
5692
  import fs16 from "fs";
5595
- import path7 from "path";
5693
+ import path8 from "path";
5596
5694
  function loadProjectInstructions() {
5597
5695
  for (const file of AGENT_INSTRUCTION_FILES) {
5598
5696
  try {
@@ -5652,7 +5750,7 @@ function walkMdFiles2(dir) {
5652
5750
  try {
5653
5751
  const entries = fs16.readdirSync(dir, { withFileTypes: true });
5654
5752
  for (const entry of entries) {
5655
- const full = path7.join(dir, entry.name);
5753
+ const full = path8.join(dir, entry.name);
5656
5754
  if (entry.isDirectory()) {
5657
5755
  results.push(...walkMdFiles2(full));
5658
5756
  } else if (entry.name.endsWith(".md")) {
@@ -5828,7 +5926,7 @@ ${isLspConfigured() ? `<typescript_lsp>
5828
5926
  <conversation_summaries>
5829
5927
  Your conversation history may include <prior_conversation_summary> blocks in the user's messages. These are automated summaries of earlier messages that have been compacted to save context space. The user does not see this summary, they see the full conversation history in their UI. Treat the summary as ground truth for what happened before, but do not reference it directly to the user ("as mentioned in the summary..."). Just continue naturally as if you remember the prior work.
5830
5928
 
5831
- Old tool results are periodically cleared from the conversation to save context space. This is automatic and expected \u2014 you don't need to note down or preserve information from tool results. If you need to reference something from an earlier tool call, just re-read the file or re-run the query, or use your .remy-notes.md file.
5929
+ Old tool results are periodically cleared from the conversation to save context space. This is automatic and expected \u2014 you don't need to note down or preserve information from tool results. If you need to reference something from an earlier tool call, just re-read the file or re-run the query.
5832
5930
  </conversation_summaries>
5833
5931
 
5834
5932
  <project_onboarding>
@@ -5864,7 +5962,7 @@ var init_prompt4 = __esm({
5864
5962
 
5865
5963
  // src/config.ts
5866
5964
  import fs17 from "fs";
5867
- import path8 from "path";
5965
+ import path9 from "path";
5868
5966
  import os from "os";
5869
5967
  function loadConfigFile() {
5870
5968
  try {
@@ -5905,7 +6003,7 @@ var init_config = __esm({
5905
6003
  "use strict";
5906
6004
  init_logger();
5907
6005
  log7 = createLogger("config");
5908
- CONFIG_PATH = path8.join(
6006
+ CONFIG_PATH = path9.join(
5909
6007
  os.homedir(),
5910
6008
  ".mindstudio-local-tunnel",
5911
6009
  "config.json"
@@ -6688,6 +6786,9 @@ ${xmlParts}
6688
6786
  if (pending) {
6689
6787
  pendingTools.delete(id);
6690
6788
  pending.resolve(result);
6789
+ } else if (!running) {
6790
+ log10.info("Late tool_result while idle, dismissing", { id });
6791
+ emit("completed", { success: true }, requestId);
6691
6792
  } else {
6692
6793
  earlyResults.set(id, result);
6693
6794
  }
@@ -6819,7 +6920,7 @@ var init_headless = __esm({
6819
6920
  import { render } from "ink";
6820
6921
  import os2 from "os";
6821
6922
  import fs18 from "fs";
6822
- import path9 from "path";
6923
+ import path10 from "path";
6823
6924
 
6824
6925
  // src/tui/App.tsx
6825
6926
  import { useState as useState2, useCallback, useRef } from "react";
@@ -7138,7 +7239,7 @@ var startupLog = createLogger("startup");
7138
7239
  function printDebugInfo(config) {
7139
7240
  const pkg = JSON.parse(
7140
7241
  fs18.readFileSync(
7141
- path9.join(import.meta.dirname, "..", "package.json"),
7242
+ path10.join(import.meta.dirname, "..", "package.json"),
7142
7243
  "utf-8"
7143
7244
  )
7144
7245
  );
@@ -25,6 +25,7 @@ Interfaces run fullscreen in the user's browser or a wrapped webview mobile app.
25
25
  - **No long scrolling pages.** Use structured layouts: cards, split panes, steppers, tabs, grouped sections that fit the viewport. The interface should feel like an award winning iOS or macOS app, not a document.
26
26
  - **On mobile**, scrolling may be necessary, but use sticky headers, fixed CTAs, and anchored navigation to keep key actions within reach. Always use "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" to make sure apps feel like apps.
27
27
  - Think of every screen as something the user opens, uses, and closes — not something they read.
28
+ - Pay attention to details that will make things feel app-like - set user-select: none, use motion, use iOS/macOS design language and patterns.
28
29
 
29
30
  ## Layout Stability
30
31
 
@@ -251,7 +251,7 @@ The human-readable spec. Frontmatter contains structured fields; the prose body
251
251
  ```yaml
252
252
  ---
253
253
  name: Todo Assistant
254
- model: {"model": "claude-4-5-haiku", "temperature": 0.5, "maxResponseTokens": 15000}
254
+ model: {"model": "claude-4-5-haiku", "temperature": 0.5, "maxResponseTokens": 16000}
255
255
  description: Conversational agent that helps users manage their to-do list.
256
256
  ---
257
257
  ```
@@ -282,7 +282,7 @@ dist/interfaces/agent/
282
282
  "agent": {
283
283
  "model": "claude-4-5-haiku",
284
284
  "temperature": 0.5,
285
- "maxTokens": 15000,
285
+ "maxTokens": 16000,
286
286
  "systemPrompt": "system.md",
287
287
  "tools": [
288
288
  { "method": "create-todo", "description": "tools/createTodo.md" },
@@ -127,6 +127,7 @@ const { content } = await agent.generateText({
127
127
  modelOverride: {
128
128
  model: 'claude-sonnet-4-6',
129
129
  temperature: 0.7,
130
+ maxResponseTokens: 16000,
130
131
  },
131
132
  });
132
133
  ```
@@ -136,6 +136,7 @@ const { content } = await agent.generateText({
136
136
  modelOverride: {
137
137
  model: model.id,
138
138
  temperature: 0.7,
139
+ maxResponseTokens: 16000,
139
140
  },
140
141
  });
141
142
  ```
@@ -17,7 +17,7 @@
17
17
  - Pushing to main branch will trigger a deploy. The user presses the publish button in the interface to request publishing.
18
18
 
19
19
  ### Build Notes
20
- For complex tasks — especially an initial buildout from a spec or making multiple changes in a single turn — write a `.remy-notes.md` scratchpad in the project root. Use it to track progress: a checklist of what's been built and what's remaining. Never include implementation details or other decisions in the notes - it is solely for keeping track of tasks. Read the spec files directly when you need design details, implementation decisions, or other reference materials. Delete the notes file when your work is done.
20
+ For complex tasks — especially an initial buildout from a spec or making multiple changes in a single turn — write a `.remy-notes.md` scratchpad in the project root. Use it to track progress: a checklist of what's been built and what's remaining. Do not include implementation details or other decisions in the notes - it is solely for keeping track of tasks. Read the spec files directly when you need design details, implementation decisions, or other reference materials. Delete the notes file when your work is done.
21
21
 
22
22
  ## Communication
23
23
  The user can already see your tool calls, so most of your work is visible without narration. Focus text output on three things:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindstudio-ai/remy",
3
- "version": "0.1.109",
3
+ "version": "0.1.111",
4
4
  "description": "MindStudio coding agent",
5
5
  "repository": {
6
6
  "type": "git",