@mindstudio-ai/remy 0.1.19 → 0.1.21

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 (32) hide show
  1. package/dist/actions/buildFromInitialSpec.md +11 -3
  2. package/dist/compiled/design.md +2 -1
  3. package/dist/compiled/msfm.md +1 -0
  4. package/dist/compiled/sdk-actions.md +1 -3
  5. package/dist/headless.js +838 -306
  6. package/dist/index.js +952 -358
  7. package/dist/prompt/.notes.md +54 -0
  8. package/dist/prompt/actions/buildFromInitialSpec.md +11 -3
  9. package/dist/prompt/compiled/design.md +2 -1
  10. package/dist/prompt/compiled/msfm.md +1 -0
  11. package/dist/prompt/compiled/sdk-actions.md +1 -3
  12. package/dist/prompt/sources/frontend-design-notes.md +1 -0
  13. package/dist/prompt/static/authoring.md +4 -4
  14. package/dist/prompt/static/coding.md +5 -5
  15. package/dist/prompt/static/team.md +39 -0
  16. package/dist/static/authoring.md +4 -4
  17. package/dist/static/coding.md +5 -5
  18. package/dist/static/team.md +39 -0
  19. package/dist/subagents/browserAutomation/prompt.md +2 -0
  20. package/dist/subagents/codeSanityCheck/.notes.md +44 -0
  21. package/dist/subagents/codeSanityCheck/prompt.md +43 -0
  22. package/dist/subagents/designExpert/.notes.md +16 -4
  23. package/dist/subagents/designExpert/data/compile-inspiration.sh +2 -2
  24. package/dist/subagents/designExpert/prompts/frontend-design-notes.md +1 -0
  25. package/dist/subagents/designExpert/prompts/icons.md +18 -7
  26. package/dist/subagents/designExpert/prompts/identity.md +4 -4
  27. package/dist/subagents/designExpert/prompts/images.md +3 -2
  28. package/dist/subagents/designExpert/prompts/instructions.md +2 -2
  29. package/dist/subagents/designExpert/prompts/layout.md +4 -2
  30. package/dist/subagents/productVision/.notes.md +79 -0
  31. package/dist/subagents/productVision/prompt.md +29 -22
  32. package/package.json +1 -1
package/dist/headless.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/headless.ts
2
2
  import { createInterface } from "readline";
3
- import fs17 from "fs";
4
- import path10 from "path";
3
+ import fs20 from "fs";
4
+ import path13 from "path";
5
5
 
6
6
  // src/config.ts
7
7
  import fs2 from "fs";
@@ -371,6 +371,8 @@ ${projectContext}
371
371
  {{static/authoring.md}}
372
372
  </spec_authoring_instructions>
373
373
 
374
+ {{static/team.md}}
375
+
374
376
  <code_authoring_instructions>
375
377
  {{static/coding.md}}
376
378
  ${isLspConfigured() ? `<typescript_lsp>
@@ -1240,19 +1242,36 @@ var confirmDestructiveActionTool = {
1240
1242
  }
1241
1243
  };
1242
1244
 
1243
- // src/tools/common/askMindStudioSdk.ts
1245
+ // src/subagents/common/runCli.ts
1244
1246
  import { exec } from "child_process";
1247
+ function runCli(cmd, options) {
1248
+ return new Promise((resolve) => {
1249
+ exec(
1250
+ cmd,
1251
+ {
1252
+ timeout: options?.timeout ?? 6e4,
1253
+ maxBuffer: options?.maxBuffer ?? 1024 * 1024
1254
+ },
1255
+ (err, stdout, stderr) => {
1256
+ if (stdout.trim()) {
1257
+ resolve(stdout.trim());
1258
+ return;
1259
+ }
1260
+ if (err) {
1261
+ resolve(`Error: ${stderr.trim() || err.message}`);
1262
+ return;
1263
+ }
1264
+ resolve("(no response)");
1265
+ }
1266
+ );
1267
+ });
1268
+ }
1269
+
1270
+ // src/subagents/sdkConsultant/index.ts
1245
1271
  var askMindStudioSdkTool = {
1246
1272
  definition: {
1247
1273
  name: "askMindStudioSdk",
1248
- description: `An expert consultant on building with the MindStudio SDK. Knows every action, model, connector, and configuration option. Use this as an architect, not just a docs lookup:
1249
-
1250
- - Describe what you're trying to build at the method level ("I need a method that takes user text, generates a summary with GPT, extracts entities, and returns structured JSON") and get back architectural guidance + working code.
1251
- - Ask about AI orchestration patterns: structured output, chaining model calls, batch processing, streaming, error handling.
1252
- - Ask about connectors and integrations: what's available, whether the user has configured it, how to use it.
1253
- - Always use this before writing SDK code. Model IDs, config options, and action signatures change frequently. Don't guess.
1254
-
1255
- Batch related questions into a single query. This runs its own LLM call so it has a few seconds of latency.`,
1274
+ description: "MindStudio SDK expert. Knows every action, model, connector, and configuration option. Returns architectural guidance and working code. Describe what you want to build, not just what API method you need. Batch related questions into a single query.",
1256
1275
  inputSchema: {
1257
1276
  type: "object",
1258
1277
  properties: {
@@ -1266,28 +1285,13 @@ Batch related questions into a single query. This runs its own LLM call so it ha
1266
1285
  },
1267
1286
  async execute(input) {
1268
1287
  const query = input.query;
1269
- return new Promise((resolve) => {
1270
- exec(
1271
- `mindstudio ask ${JSON.stringify(query)}`,
1272
- { timeout: 6e4, maxBuffer: 512 * 1024 },
1273
- (err, stdout, stderr) => {
1274
- if (stdout.trim()) {
1275
- resolve(stdout.trim());
1276
- return;
1277
- }
1278
- if (err) {
1279
- resolve(`Error: ${stderr.trim() || err.message}`);
1280
- return;
1281
- }
1282
- resolve("(no response)");
1283
- }
1284
- );
1288
+ return runCli(`mindstudio ask ${JSON.stringify(query)}`, {
1289
+ maxBuffer: 512 * 1024
1285
1290
  });
1286
1291
  }
1287
1292
  };
1288
1293
 
1289
1294
  // src/tools/common/fetchUrl.ts
1290
- import { exec as exec2 } from "child_process";
1291
1295
  var fetchUrlTool = {
1292
1296
  definition: {
1293
1297
  name: "scapeWebUrl",
@@ -1314,29 +1318,13 @@ var fetchUrlTool = {
1314
1318
  if (screenshot) {
1315
1319
  pageOptions.screenshot = true;
1316
1320
  }
1317
- const cmd = `mindstudio scrape-url --url ${JSON.stringify(url)} --page-options ${JSON.stringify(JSON.stringify(pageOptions))} --no-meta`;
1318
- return new Promise((resolve) => {
1319
- exec2(
1320
- cmd,
1321
- { timeout: 6e4, maxBuffer: 1024 * 1024 },
1322
- (err, stdout, stderr) => {
1323
- if (stdout.trim()) {
1324
- resolve(stdout.trim());
1325
- return;
1326
- }
1327
- if (err) {
1328
- resolve(`Error: ${stderr.trim() || err.message}`);
1329
- return;
1330
- }
1331
- resolve("(no response)");
1332
- }
1333
- );
1334
- });
1321
+ return runCli(
1322
+ `mindstudio scrape-url --url ${JSON.stringify(url)} --page-options ${JSON.stringify(JSON.stringify(pageOptions))} --no-meta`
1323
+ );
1335
1324
  }
1336
1325
  };
1337
1326
 
1338
1327
  // src/tools/common/searchGoogle.ts
1339
- import { exec as exec3 } from "child_process";
1340
1328
  var searchGoogleTool = {
1341
1329
  definition: {
1342
1330
  name: "searchGoogle",
@@ -1354,24 +1342,10 @@ var searchGoogleTool = {
1354
1342
  },
1355
1343
  async execute(input) {
1356
1344
  const query = input.query;
1357
- const cmd = `mindstudio search-google --query ${JSON.stringify(query)} --export-type json --output-key results --no-meta`;
1358
- return new Promise((resolve) => {
1359
- exec3(
1360
- cmd,
1361
- { timeout: 6e4, maxBuffer: 512 * 1024 },
1362
- (err, stdout, stderr) => {
1363
- if (stdout.trim()) {
1364
- resolve(stdout.trim());
1365
- return;
1366
- }
1367
- if (err) {
1368
- resolve(`Error: ${stderr.trim() || err.message}`);
1369
- return;
1370
- }
1371
- resolve("(no response)");
1372
- }
1373
- );
1374
- });
1345
+ return runCli(
1346
+ `mindstudio search-google --query ${JSON.stringify(query)} --export-type json --output-key results --no-meta`,
1347
+ { maxBuffer: 512 * 1024 }
1348
+ );
1375
1349
  }
1376
1350
  };
1377
1351
 
@@ -1696,7 +1670,7 @@ ${unifiedDiff(input.path, content, updated)}`;
1696
1670
  };
1697
1671
 
1698
1672
  // src/tools/code/bash.ts
1699
- import { exec as exec4 } from "child_process";
1673
+ import { exec as exec2 } from "child_process";
1700
1674
  var DEFAULT_TIMEOUT_MS = 12e4;
1701
1675
  var DEFAULT_MAX_LINES3 = 500;
1702
1676
  var bashTool = {
@@ -1730,7 +1704,7 @@ var bashTool = {
1730
1704
  const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES3;
1731
1705
  const timeoutMs = input.timeout ? input.timeout * 1e3 : DEFAULT_TIMEOUT_MS;
1732
1706
  return new Promise((resolve) => {
1733
- exec4(
1707
+ exec2(
1734
1708
  input.command,
1735
1709
  {
1736
1710
  timeout: timeoutMs,
@@ -1770,7 +1744,7 @@ var bashTool = {
1770
1744
  };
1771
1745
 
1772
1746
  // src/tools/code/grep.ts
1773
- import { exec as exec5 } from "child_process";
1747
+ import { exec as exec3 } from "child_process";
1774
1748
  var DEFAULT_MAX = 50;
1775
1749
  function formatResults(stdout, max) {
1776
1750
  const lines = stdout.trim().split("\n");
@@ -1817,12 +1791,12 @@ var grepTool = {
1817
1791
  const rgCmd = `rg -n --no-heading --max-count=${max}${globFlag} '${escaped}' ${searchPath}`;
1818
1792
  const grepCmd = `grep -rn --max-count=${max} '${escaped}' ${searchPath} --include='*.ts' --include='*.tsx' --include='*.js' --include='*.json' --include='*.md'`;
1819
1793
  return new Promise((resolve) => {
1820
- exec5(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
1794
+ exec3(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
1821
1795
  if (stdout?.trim()) {
1822
1796
  resolve(formatResults(stdout, max));
1823
1797
  return;
1824
1798
  }
1825
- exec5(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
1799
+ exec3(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
1826
1800
  if (grepStdout?.trim()) {
1827
1801
  resolve(formatResults(grepStdout, max));
1828
1802
  } else {
@@ -2054,20 +2028,70 @@ var runMethodTool = {
2054
2028
  };
2055
2029
 
2056
2030
  // src/tools/code/screenshot.ts
2031
+ var DEFAULT_PROMPT = "Describe this app screenshot for a developer who cannot see it. What is visible on screen: the layout, content, interactive elements, any loading or error states. Be concise and factual.";
2057
2032
  var screenshotTool = {
2058
2033
  definition: {
2059
2034
  name: "screenshot",
2060
- description: "Capture a screenshot of the app preview. Returns a CDN URL with dimensions. Useful for visually checking the current state after UI changes or when debugging layout issues.",
2035
+ description: "Capture a screenshot of the app preview and get a description of what's on screen. Optionally provide a specific question about what you're looking for.",
2061
2036
  inputSchema: {
2062
2037
  type: "object",
2063
- properties: {}
2038
+ properties: {
2039
+ prompt: {
2040
+ type: "string",
2041
+ description: "Optional question about the screenshot. If omitted, returns a general description of what's visible."
2042
+ }
2043
+ }
2064
2044
  }
2065
2045
  },
2066
- async execute() {
2067
- return "ok";
2046
+ async execute(input) {
2047
+ try {
2048
+ const { url } = await sidecarRequest(
2049
+ "/screenshot",
2050
+ {},
2051
+ { timeout: 3e4 }
2052
+ );
2053
+ const analysisPrompt = input.prompt || DEFAULT_PROMPT;
2054
+ const analysis = await runCli(
2055
+ `mindstudio analyze-image --prompt ${JSON.stringify(analysisPrompt)} --image-url ${JSON.stringify(url)} --output-key analysis --no-meta`
2056
+ );
2057
+ return `Screenshot: ${url}
2058
+
2059
+ ${analysis}`;
2060
+ } catch (err) {
2061
+ return `Error taking screenshot: ${err.message}`;
2062
+ }
2068
2063
  }
2069
2064
  };
2070
2065
 
2066
+ // src/subagents/common/cleanMessages.ts
2067
+ function cleanMessagesForApi(messages) {
2068
+ return messages.map((msg) => {
2069
+ if (!Array.isArray(msg.content)) {
2070
+ return msg;
2071
+ }
2072
+ const blocks = msg.content;
2073
+ const text = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
2074
+ const toolCalls = blocks.filter((b) => b.type === "tool").map((b) => ({ id: b.id, name: b.name, input: b.input }));
2075
+ const thinking = blocks.filter(
2076
+ (b) => b.type === "thinking"
2077
+ ).map((b) => ({ thinking: b.thinking, signature: b.signature }));
2078
+ const cleaned = {
2079
+ role: msg.role,
2080
+ content: text
2081
+ };
2082
+ if (toolCalls.length > 0) {
2083
+ cleaned.toolCalls = toolCalls;
2084
+ }
2085
+ if (thinking.length > 0) {
2086
+ cleaned.thinking = thinking;
2087
+ }
2088
+ if (msg.hidden) {
2089
+ cleaned.hidden = true;
2090
+ }
2091
+ return cleaned;
2092
+ });
2093
+ }
2094
+
2071
2095
  // src/subagents/runner.ts
2072
2096
  async function runSubAgent(config) {
2073
2097
  const {
@@ -2078,6 +2102,7 @@ async function runSubAgent(config) {
2078
2102
  executeTool: executeTool2,
2079
2103
  apiConfig,
2080
2104
  model,
2105
+ subAgentId,
2081
2106
  signal,
2082
2107
  parentToolId,
2083
2108
  onEvent,
@@ -2089,17 +2114,18 @@ async function runSubAgent(config) {
2089
2114
  const messages = [{ role: "user", content: task }];
2090
2115
  while (true) {
2091
2116
  if (signal?.aborted) {
2092
- return "Error: cancelled";
2117
+ return { text: "Error: cancelled", messages };
2093
2118
  }
2094
- let assistantText = "";
2095
- const toolCalls = [];
2119
+ const contentBlocks = [];
2120
+ let thinkingStartedAt = 0;
2096
2121
  let stopReason = "end_turn";
2097
2122
  try {
2098
2123
  for await (const event of streamChatWithRetry({
2099
2124
  ...apiConfig,
2100
2125
  model,
2126
+ subAgentId,
2101
2127
  system,
2102
- messages,
2128
+ messages: cleanMessagesForApi(messages),
2103
2129
  tools,
2104
2130
  signal
2105
2131
  })) {
@@ -2107,18 +2133,43 @@ async function runSubAgent(config) {
2107
2133
  break;
2108
2134
  }
2109
2135
  switch (event.type) {
2110
- case "text":
2111
- assistantText += event.text;
2136
+ case "text": {
2137
+ const lastBlock = contentBlocks.at(-1);
2138
+ if (lastBlock?.type === "text") {
2139
+ lastBlock.text += event.text;
2140
+ } else {
2141
+ contentBlocks.push({
2142
+ type: "text",
2143
+ text: event.text,
2144
+ startedAt: event.ts
2145
+ });
2146
+ }
2112
2147
  emit2({ type: "text", text: event.text });
2113
2148
  break;
2149
+ }
2114
2150
  case "thinking":
2151
+ if (!thinkingStartedAt) {
2152
+ thinkingStartedAt = event.ts;
2153
+ }
2115
2154
  emit2({ type: "thinking", text: event.text });
2116
2155
  break;
2156
+ case "thinking_complete":
2157
+ contentBlocks.push({
2158
+ type: "thinking",
2159
+ thinking: event.thinking,
2160
+ signature: event.signature,
2161
+ startedAt: thinkingStartedAt,
2162
+ completedAt: event.ts
2163
+ });
2164
+ thinkingStartedAt = 0;
2165
+ break;
2117
2166
  case "tool_use":
2118
- toolCalls.push({
2167
+ contentBlocks.push({
2168
+ type: "tool",
2119
2169
  id: event.id,
2120
2170
  name: event.name,
2121
- input: event.input
2171
+ input: event.input,
2172
+ startedAt: Date.now()
2122
2173
  });
2123
2174
  emit2({
2124
2175
  type: "tool_start",
@@ -2131,7 +2182,7 @@ async function runSubAgent(config) {
2131
2182
  stopReason = event.stopReason;
2132
2183
  break;
2133
2184
  case "error":
2134
- return `Error: ${event.error}`;
2185
+ return { text: `Error: ${event.error}`, messages };
2135
2186
  }
2136
2187
  }
2137
2188
  } catch (err) {
@@ -2140,15 +2191,18 @@ async function runSubAgent(config) {
2140
2191
  }
2141
2192
  }
2142
2193
  if (signal?.aborted) {
2143
- return "Error: cancelled";
2194
+ return { text: "Error: cancelled", messages };
2144
2195
  }
2145
2196
  messages.push({
2146
2197
  role: "assistant",
2147
- content: assistantText,
2148
- toolCalls: toolCalls.length > 0 ? toolCalls : void 0
2198
+ content: contentBlocks
2149
2199
  });
2200
+ const toolCalls = contentBlocks.filter(
2201
+ (b) => b.type === "tool"
2202
+ );
2150
2203
  if (stopReason !== "tool_use" || toolCalls.length === 0) {
2151
- return assistantText;
2204
+ const text = contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join("");
2205
+ return { text, messages };
2152
2206
  }
2153
2207
  log.info("Sub-agent executing tools", {
2154
2208
  parentToolId,
@@ -2283,7 +2337,19 @@ import path7 from "path";
2283
2337
  var base = import.meta.dirname ?? path7.dirname(new URL(import.meta.url).pathname);
2284
2338
  var local = path7.join(base, "prompt.md");
2285
2339
  var PROMPT_PATH = fs13.existsSync(local) ? local : path7.join(base, "subagents", "browserAutomation", "prompt.md");
2286
- var BROWSER_AUTOMATION_PROMPT = fs13.readFileSync(PROMPT_PATH, "utf-8").trim();
2340
+ var BASE_PROMPT = fs13.readFileSync(PROMPT_PATH, "utf-8").trim();
2341
+ function getBrowserAutomationPrompt() {
2342
+ try {
2343
+ const appSpec = fs13.readFileSync("src/app.md", "utf-8").trim();
2344
+ return `${BASE_PROMPT}
2345
+
2346
+ <app_context>
2347
+ ${appSpec}
2348
+ </app_context>`;
2349
+ } catch {
2350
+ return BASE_PROMPT;
2351
+ }
2352
+ }
2287
2353
 
2288
2354
  // src/subagents/browserAutomation/index.ts
2289
2355
  var browserAutomationTool = {
@@ -2317,8 +2383,8 @@ var browserAutomationTool = {
2317
2383
  } catch {
2318
2384
  return "Error: could not check browser status. The dev environment may not be running.";
2319
2385
  }
2320
- return runSubAgent({
2321
- system: BROWSER_AUTOMATION_PROMPT,
2386
+ const result = await runSubAgent({
2387
+ system: getBrowserAutomationPrompt(),
2322
2388
  task: input.task,
2323
2389
  tools: BROWSER_TOOLS,
2324
2390
  externalTools: BROWSER_EXTERNAL_TOOLS,
@@ -2335,16 +2401,18 @@ var browserAutomationTool = {
2335
2401
  },
2336
2402
  apiConfig: context.apiConfig,
2337
2403
  model: context.model,
2404
+ subAgentId: "browserAutomation",
2338
2405
  signal: context.signal,
2339
2406
  parentToolId: context.toolCallId,
2340
2407
  onEvent: context.onEvent,
2341
2408
  resolveExternalTool: context.resolveExternalTool
2342
2409
  });
2410
+ context.subAgentMessages?.set(context.toolCallId, result.messages);
2411
+ return result.text;
2343
2412
  }
2344
2413
  };
2345
2414
 
2346
2415
  // src/subagents/designExpert/tools.ts
2347
- import { exec as exec6 } from "child_process";
2348
2416
  var DESIGN_REFERENCE_PROMPT = `Analyze this website/app screenshot as a design reference. Assess:
2349
2417
  1) Mood/aesthetic
2350
2418
  2) Color palette with approximate hex values and palette strategy
@@ -2352,7 +2420,7 @@ var DESIGN_REFERENCE_PROMPT = `Analyze this website/app screenshot as a design r
2352
2420
  4) Layout composition (symmetric/asymmetric, grid structure, whitespace usage, content density)
2353
2421
  5) What makes it distinctive and interesting vs generic AI-generated interfaces
2354
2422
  Be specific and concise.`;
2355
- var DESIGN_RESEARCH_TOOLS = [
2423
+ var DESIGN_EXPERT_TOOLS = [
2356
2424
  {
2357
2425
  name: "searchGoogle",
2358
2426
  description: "Search Google for web results. Use for finding design inspiration, font recommendations, UI patterns, real products in a domain, and reference material.",
@@ -2386,53 +2454,29 @@ var DESIGN_RESEARCH_TOOLS = [
2386
2454
  }
2387
2455
  },
2388
2456
  {
2389
- name: "analyzeImage",
2390
- description: 'Analyze an image using a vision model with a custom prompt. Use when you have a specific question about an image (e.g., "what colors dominate this image?", "describe the typography choices").',
2457
+ name: "analyzeReferenceImageOrUrl",
2458
+ description: "Analyze any visual \u2014 pass an image URL or a website URL. Websites are automatically screenshotted first. If no prompt is provided, performs a full design reference analysis (mood, color, typography, layout, distinctiveness). Provide a custom prompt to ask a specific question instead.",
2391
2459
  inputSchema: {
2392
2460
  type: "object",
2393
2461
  properties: {
2394
- prompt: {
2462
+ url: {
2395
2463
  type: "string",
2396
- description: "What to analyze or extract from the image."
2464
+ description: "URL to analyze. Can be an image URL or a website URL (will be screenshotted)."
2397
2465
  },
2398
- imageUrl: {
2399
- type: "string",
2400
- description: "URL of the image to analyze."
2401
- }
2402
- },
2403
- required: ["prompt", "imageUrl"]
2404
- }
2405
- },
2406
- {
2407
- name: "analyzeDesignReference",
2408
- description: "Analyze a screenshot or design image for design inspiration. Returns a structured analysis: mood/aesthetic, color palette with hex values, typography style, layout composition, and what makes it distinctive. Use this instead of analyzeImage when studying a design reference.",
2409
- inputSchema: {
2410
- type: "object",
2411
- properties: {
2412
- imageUrl: {
2466
+ prompt: {
2413
2467
  type: "string",
2414
- description: "URL of the screenshot or design image to analyze."
2468
+ description: "Optional custom analysis prompt. If omitted, performs the standard design reference analysis."
2415
2469
  }
2416
2470
  },
2417
- required: ["imageUrl"]
2471
+ required: ["url"]
2418
2472
  }
2419
2473
  },
2420
2474
  {
2421
- name: "screenshotAndAnalyze",
2422
- description: "Screenshot a live URL and analyze it in one step. If no prompt is provided, performs a full design reference analysis (mood, color, typography, layout, distinctiveness). Provide a custom prompt to ask a specific question about the visual design instead.",
2475
+ name: "screenshot",
2476
+ description: "Capture a screenshot of the app preview. Returns a CDN URL. Use to review the current state of the UI being built.",
2423
2477
  inputSchema: {
2424
2478
  type: "object",
2425
- properties: {
2426
- url: {
2427
- type: "string",
2428
- description: "The URL to screenshot."
2429
- },
2430
- prompt: {
2431
- type: "string",
2432
- description: "Optional custom analysis prompt. If omitted, performs the standard design reference analysis."
2433
- }
2434
- },
2435
- required: ["url"]
2479
+ properties: {}
2436
2480
  }
2437
2481
  },
2438
2482
  {
@@ -2451,7 +2495,7 @@ var DESIGN_RESEARCH_TOOLS = [
2451
2495
  },
2452
2496
  {
2453
2497
  name: "generateImages",
2454
- description: "Generate images using AI (Seedream). Returns CDN URLs. Produces high-quality results for both photorealistic images and abstract/creative visuals. Pass multiple prompts to generate in parallel.",
2498
+ description: "Generate images using AI (Seedream). Returns CDN URLs with a quality analysis for each image. Produces high-quality results for both photorealistic images and abstract/creative visuals. Pass multiple prompts to generate in parallel. No need to analyze images separately after generating \u2014 the analysis is included.",
2455
2499
  inputSchema: {
2456
2500
  type: "object",
2457
2501
  properties: {
@@ -2475,27 +2519,26 @@ var DESIGN_RESEARCH_TOOLS = [
2475
2519
  }
2476
2520
  }
2477
2521
  ];
2478
- function runCli(cmd) {
2479
- return new Promise((resolve) => {
2480
- exec6(
2481
- cmd,
2482
- { timeout: 6e4, maxBuffer: 1024 * 1024 },
2483
- (err, stdout, stderr) => {
2484
- if (stdout.trim()) {
2485
- resolve(stdout.trim());
2486
- return;
2487
- }
2488
- if (err) {
2489
- resolve(`Error: ${stderr.trim() || err.message}`);
2490
- return;
2491
- }
2492
- resolve("(no response)");
2493
- }
2494
- );
2495
- });
2496
- }
2497
- async function executeDesignTool(name, input) {
2522
+ async function executeDesignExpertTool(name, input) {
2498
2523
  switch (name) {
2524
+ case "screenshot": {
2525
+ try {
2526
+ const { url } = await sidecarRequest(
2527
+ "/screenshot",
2528
+ {},
2529
+ { timeout: 3e4 }
2530
+ );
2531
+ const analysisPrompt = input.prompt || "Describe this app screenshot for a visual designer reviewing the current state. What is visible: layout, typography, colors, spacing, imagery. Note anything that looks broken or off. Be concise.";
2532
+ const analysis = await runCli(
2533
+ `mindstudio analyze-image --prompt ${JSON.stringify(analysisPrompt)} --image-url ${JSON.stringify(url)} --output-key analysis --no-meta`
2534
+ );
2535
+ return `Screenshot: ${url}
2536
+
2537
+ ${analysis}`;
2538
+ } catch (err) {
2539
+ return `Error taking screenshot: ${err.message}`;
2540
+ }
2541
+ }
2499
2542
  case "searchGoogle":
2500
2543
  return runCli(
2501
2544
  `mindstudio search-google --query ${JSON.stringify(input.query)} --export-type json --output-key results --no-meta`
@@ -2509,26 +2552,24 @@ async function executeDesignTool(name, input) {
2509
2552
  `mindstudio scrape-url --url ${JSON.stringify(input.url)} --page-options ${JSON.stringify(JSON.stringify(pageOptions))} --no-meta`
2510
2553
  );
2511
2554
  }
2512
- case "analyzeImage":
2513
- return runCli(
2514
- `mindstudio analyze-image --prompt ${JSON.stringify(input.prompt)} --image-url ${JSON.stringify(input.imageUrl)} --no-meta`
2515
- );
2516
- case "analyzeDesignReference":
2517
- return runCli(
2518
- `mindstudio analyze-image --prompt ${JSON.stringify(DESIGN_REFERENCE_PROMPT)} --image-url ${JSON.stringify(input.imageUrl)} --no-meta`
2519
- );
2520
- case "screenshotAndAnalyze": {
2521
- const ssUrl = await runCli(
2522
- `mindstudio screenshot-url --url ${JSON.stringify(input.url)} --mode viewport --width 1440 --delay 2000 --output-key screenshotUrl --no-meta`
2523
- );
2524
- if (ssUrl.startsWith("Error")) {
2525
- return `Could not screenshot ${input.url}: ${ssUrl}`;
2526
- }
2555
+ case "analyzeReferenceImageOrUrl": {
2556
+ const url = input.url;
2527
2557
  const analysisPrompt = input.prompt || DESIGN_REFERENCE_PROMPT;
2558
+ const isImageUrl = /\.(png|jpe?g|webp|gif|svg|avif)(\?|$)/i.test(url);
2559
+ let imageUrl = url;
2560
+ if (!isImageUrl) {
2561
+ const ssUrl = await runCli(
2562
+ `mindstudio screenshot-url --url ${JSON.stringify(url)} --mode viewport --width 1440 --delay 2000 --output-key screenshotUrl --no-meta`
2563
+ );
2564
+ if (ssUrl.startsWith("Error")) {
2565
+ return `Could not screenshot ${url}: ${ssUrl}`;
2566
+ }
2567
+ imageUrl = ssUrl;
2568
+ }
2528
2569
  const analysis = await runCli(
2529
- `mindstudio analyze-image --prompt ${JSON.stringify(analysisPrompt)} --image-url ${JSON.stringify(ssUrl)} --no-meta`
2570
+ `mindstudio analyze-image --prompt ${JSON.stringify(analysisPrompt)} --image-url ${JSON.stringify(imageUrl)} --output-key analysis --no-meta`
2530
2571
  );
2531
- return `Screenshot: ${ssUrl}
2572
+ return isImageUrl ? analysis : `Screenshot: ${imageUrl}
2532
2573
 
2533
2574
  ${analysis}`;
2534
2575
  }
@@ -2542,6 +2583,8 @@ ${analysis}`;
2542
2583
  const prompts = input.prompts;
2543
2584
  const width = input.width || 2048;
2544
2585
  const height = input.height || 2048;
2586
+ const ANALYZE_PROMPT = "You are reviewing this image for a visual designer sourcing assets for a project. Describe: what the image depicts, the mood and color palette, how the lighting and composition work, whether there are any issues (unwanted text, artifacts, distortions), and how it could be used in a layout (hero background, feature section, card texture, etc). Be concise and practical.";
2587
+ let imageUrls;
2545
2588
  if (prompts.length === 1) {
2546
2589
  const step = JSON.stringify({
2547
2590
  prompt: prompts[0],
@@ -2550,21 +2593,47 @@ ${analysis}`;
2550
2593
  config: { width, height }
2551
2594
  }
2552
2595
  });
2553
- return runCli(
2596
+ const url = await runCli(
2554
2597
  `mindstudio generate-image '${step}' --output-key imageUrl --no-meta`
2555
2598
  );
2556
- }
2557
- const steps = prompts.map((prompt) => ({
2558
- stepType: "generateImage",
2559
- step: {
2560
- prompt,
2561
- imageModelOverride: {
2562
- model: "seedream-4.5",
2563
- config: { width, height }
2599
+ imageUrls = [url];
2600
+ } else {
2601
+ const steps = prompts.map((prompt) => ({
2602
+ stepType: "generateImage",
2603
+ step: {
2604
+ prompt,
2605
+ imageModelOverride: {
2606
+ model: "seedream-4.5",
2607
+ config: { width, height }
2608
+ }
2564
2609
  }
2610
+ }));
2611
+ const batchResult = await runCli(
2612
+ `mindstudio batch '${JSON.stringify(steps)}' --no-meta`
2613
+ );
2614
+ try {
2615
+ const parsed = JSON.parse(batchResult);
2616
+ imageUrls = parsed.results.map(
2617
+ (r) => r.output?.imageUrl ?? `Error: ${r.error}`
2618
+ );
2619
+ } catch {
2620
+ return batchResult;
2565
2621
  }
2566
- }));
2567
- return runCli(`mindstudio batch '${JSON.stringify(steps)}' --no-meta`);
2622
+ }
2623
+ const analyses = await Promise.all(
2624
+ imageUrls.map(async (url, i) => {
2625
+ if (url.startsWith("Error")) {
2626
+ return `Image ${i + 1}: ${url}`;
2627
+ }
2628
+ const analysis = await runCli(
2629
+ `mindstudio analyze-image --prompt ${JSON.stringify(ANALYZE_PROMPT)} --image-url ${JSON.stringify(url)} --output-key analysis --no-meta`
2630
+ );
2631
+ return `**Image ${i + 1}:** ${url}
2632
+ Prompt: ${prompts[i]}
2633
+ Analysis: ${analysis}`;
2634
+ })
2635
+ );
2636
+ return analyses.join("\n\n");
2568
2637
  }
2569
2638
  default:
2570
2639
  return `Error: unknown tool "${name}"`;
@@ -2572,19 +2641,117 @@ ${analysis}`;
2572
2641
  }
2573
2642
 
2574
2643
  // src/subagents/designExpert/prompt.ts
2644
+ import fs15 from "fs";
2645
+ import path9 from "path";
2646
+
2647
+ // src/subagents/common/context.ts
2575
2648
  import fs14 from "fs";
2576
2649
  import path8 from "path";
2577
- var base2 = import.meta.dirname ?? path8.dirname(new URL(import.meta.url).pathname);
2650
+ function walkMdFiles2(dir, skip) {
2651
+ const files = [];
2652
+ try {
2653
+ for (const entry of fs14.readdirSync(dir, { withFileTypes: true })) {
2654
+ const full = path8.join(dir, entry.name);
2655
+ if (entry.isDirectory()) {
2656
+ if (!skip?.has(entry.name)) {
2657
+ files.push(...walkMdFiles2(full, skip));
2658
+ }
2659
+ } else if (entry.name.endsWith(".md")) {
2660
+ files.push(full);
2661
+ }
2662
+ }
2663
+ } catch {
2664
+ }
2665
+ return files;
2666
+ }
2667
+ function loadFilesAsXml(dir, tag, skip) {
2668
+ const files = walkMdFiles2(dir, skip);
2669
+ if (files.length === 0) {
2670
+ return "";
2671
+ }
2672
+ const sections = files.map((f) => {
2673
+ try {
2674
+ const content = fs14.readFileSync(f, "utf-8").trim();
2675
+ return `<file path="${f}">
2676
+ ${content}
2677
+ </file>`;
2678
+ } catch {
2679
+ return "";
2680
+ }
2681
+ }).filter(Boolean);
2682
+ return `<${tag}>
2683
+ ${sections.join("\n\n")}
2684
+ </${tag}>`;
2685
+ }
2686
+ function loadSpecContext() {
2687
+ return loadFilesAsXml("src", "spec_files", /* @__PURE__ */ new Set(["roadmap"]));
2688
+ }
2689
+ function loadRoadmapContext() {
2690
+ return loadFilesAsXml("src/roadmap", "current_roadmap");
2691
+ }
2692
+ function loadPlatformBrief() {
2693
+ return `<platform_brief>
2694
+ ## What is a MindStudio app?
2695
+
2696
+ A MindStudio app is a managed TypeScript project with three layers: a spec (natural language in src/), a backend contract (methods, tables, roles in dist/), and one or more interfaces (web, API, bots, cron, etc.). The spec is the source of truth; code is derived from it.
2697
+
2698
+ ## What people build
2699
+
2700
+ - Business tools \u2014 dashboards, admin panels, approval workflows, data entry apps, internal tools with role-based access
2701
+ - AI-powered apps \u2014 chatbots, content generators, document processors, image/video tools, AI agents that take actions
2702
+ - Automations with no UI \u2014 cron jobs, webhook handlers, email processors, data sync pipelines
2703
+ - Bots \u2014 Discord slash-command bots, Telegram bots, MCP tool servers for AI assistants
2704
+ - Creative/interactive projects \u2014 games, interactive visualizations, generative art, portfolio sites
2705
+ - API services \u2014 backend logic exposed as REST endpoints
2706
+ - Simple static sites \u2014 no backend needed, just a web interface with a build step
2707
+
2708
+ An app can be any combination of these.
2709
+
2710
+ ## Interfaces
2711
+
2712
+ Each interface type invokes the same backend methods. Methods don't know which interface called them.
2713
+
2714
+ - Web \u2014 any TypeScript project with a build command. Framework-agnostic (React, Vue, Svelte, vanilla, anything). The frontend SDK provides typed RPC to backend methods.
2715
+ - API \u2014 auto-generated REST endpoints for every method
2716
+ - Cron \u2014 scheduled jobs on a configurable interval
2717
+ - Webhook \u2014 HTTP endpoints that trigger methods
2718
+ - Discord \u2014 slash-command bots
2719
+ - Telegram \u2014 message-handling bots
2720
+ - Email \u2014 inbound email processing
2721
+ - MCP \u2014 tool servers for AI assistants
2722
+
2723
+ ## Backend
2724
+
2725
+ TypeScript running in a sandboxed environment. Any npm package can be installed. Key capabilities:
2726
+
2727
+ - Managed SQLite database with typed schemas and automatic migrations. Define a TypeScript interface, push, and the platform handles diffing and migrating.
2728
+ - Built-in role-based auth. Define roles in the manifest, gate methods with auth.requireRole(). Platform handles sessions, tokens, user resolution.
2729
+ - Sandboxed execution with npm packages pre-installed.
2730
+ - Git-native deployment. Push to default branch to deploy.
2731
+
2732
+ ## MindStudio SDK
2733
+
2734
+ The first-party SDK (@mindstudio-ai/agent) provides access to 200+ AI models (OpenAI, Anthropic, Google, Meta, Mistral, and more) and 1000+ integrations (email, SMS, Slack, HubSpot, Google Workspace, web scraping, image/video generation, media processing, and much more) with zero configuration \u2014 credentials are handled automatically in the execution environment. No API keys needed.
2735
+
2736
+ ## What MindStudio apps are NOT good for
2737
+
2738
+ - Native mobile apps (iOS/Android). Mobile-responsive web apps are fine.
2739
+ - Real-time multiplayer with persistent connections (no WebSocket support). Turn-based or async patterns work.
2740
+ </platform_brief>`;
2741
+ }
2742
+
2743
+ // src/subagents/designExpert/prompt.ts
2744
+ var base2 = import.meta.dirname ?? path9.dirname(new URL(import.meta.url).pathname);
2578
2745
  function resolvePath(filename) {
2579
- const local3 = path8.join(base2, filename);
2580
- return fs14.existsSync(local3) ? local3 : path8.join(base2, "subagents", "designExpert", filename);
2746
+ const local4 = path9.join(base2, filename);
2747
+ return fs15.existsSync(local4) ? local4 : path9.join(base2, "subagents", "designExpert", filename);
2581
2748
  }
2582
2749
  function readFile(filename) {
2583
- return fs14.readFileSync(resolvePath(filename), "utf-8").trim();
2750
+ return fs15.readFileSync(resolvePath(filename), "utf-8").trim();
2584
2751
  }
2585
2752
  function readJson(filename, fallback) {
2586
2753
  try {
2587
- return JSON.parse(fs14.readFileSync(resolvePath(filename), "utf-8"));
2754
+ return JSON.parse(fs15.readFileSync(resolvePath(filename), "utf-8"));
2588
2755
  } catch {
2589
2756
  return fallback;
2590
2757
  }
@@ -2616,7 +2783,7 @@ function sample(arr, n) {
2616
2783
  }
2617
2784
  return copy.slice(0, n);
2618
2785
  }
2619
- function getDesignResearchPrompt() {
2786
+ function getDesignExpertPrompt() {
2620
2787
  const fonts = sample(fontData.fonts, 30);
2621
2788
  const pairings = sample(fontData.pairings, 20);
2622
2789
  const images = sample(inspirationImages, 15);
@@ -2655,31 +2822,22 @@ This is what the bar looks like. These are real sites that made it onto curated
2655
2822
 
2656
2823
  ${imageList}
2657
2824
  </inspiration_images>` : "";
2658
- return PROMPT_TEMPLATE.replace("{{fonts_to_consider}}", fontsSection).replace(
2659
- "{{inspiration_images}}",
2660
- inspirationSection
2661
- );
2825
+ const specContext = loadSpecContext();
2826
+ let prompt = PROMPT_TEMPLATE.replace(
2827
+ "{{fonts_to_consider}}",
2828
+ fontsSection
2829
+ ).replace("{{inspiration_images}}", inspirationSection);
2830
+ if (specContext) {
2831
+ prompt += `
2832
+
2833
+ ${specContext}`;
2834
+ }
2835
+ return prompt;
2662
2836
  }
2663
2837
 
2664
2838
  // src/subagents/designExpert/index.ts
2665
2839
  var DESCRIPTION = `
2666
- A dedicated visual design expert. You have a lot on your plate as a coding agent, and design is a specialized skill \u2014 delegate visual design here rather than making those decisions yourself. This agent has curated font catalogs, color theory knowledge, access to design inspiration galleries, ability to create beautiful photos and images, and strong opinions about what looks good. It can answer from expertise alone or research the web when needed.
2667
-
2668
- The visual design expert can be used for all things visual design, from quick questions to comprehensive plans:
2669
- - Font selection and pairings ("suggest fonts for a <x> app")
2670
- - Color palettes from a mood, domain, or seed color ("earthy tones for a <x> brand")
2671
- - Gradient, animation, and visual effect recommendations
2672
- - Layout and composition ideas that go beyond generic AI defaults
2673
- - Analyzing a reference site or screenshot for design insights (it can take screenshots and do research on its own)
2674
- - Beautiful layout images or photos
2675
- - Icon recommendations or AI image editing
2676
- - Proposing full visual design and layout directions during intake
2677
-
2678
- **How to write the task:**
2679
- Include context about the app \u2014 what it does, who uses it, what mood or feeling the interface should convey. If the user has any specific requirements, be sure to include them. The agent can not see your conversation with the user, so you need to include all details. More context produces better results. For quick questions ("three font pairings for a <x> app"), brief is fine. You can ask for multiple topics, multiple options, etc.
2680
-
2681
- **What it returns:**
2682
- Concrete resources: hex values, font names with CSS URLs, image URLs, layout descriptions. Use the results directly in brand spec files or as guidance when building the interface.
2840
+ Visual design expert. Describe the situation and what you need \u2014 the agent decides what to deliver. It reads the spec files automatically. Include relevant user requirements and context it can't get from the spec, but do not list specific deliverables or tell it how to do its job.
2683
2841
  `.trim();
2684
2842
  var designExpertTool = {
2685
2843
  definition: {
@@ -2698,71 +2856,32 @@ var designExpertTool = {
2698
2856
  },
2699
2857
  async execute(input, context) {
2700
2858
  if (!context) {
2701
- return "Error: design research requires execution context";
2859
+ return "Error: visual design expert requires execution context";
2702
2860
  }
2703
- return runSubAgent({
2704
- system: getDesignResearchPrompt(),
2861
+ const result = await runSubAgent({
2862
+ system: getDesignExpertPrompt(),
2705
2863
  task: input.task,
2706
- tools: DESIGN_RESEARCH_TOOLS,
2864
+ tools: DESIGN_EXPERT_TOOLS,
2707
2865
  externalTools: /* @__PURE__ */ new Set(),
2708
- executeTool: executeDesignTool,
2866
+ executeTool: executeDesignExpertTool,
2709
2867
  apiConfig: context.apiConfig,
2710
2868
  model: context.model,
2869
+ subAgentId: "visualDesignExpert",
2711
2870
  signal: context.signal,
2712
2871
  parentToolId: context.toolCallId,
2713
2872
  onEvent: context.onEvent,
2714
2873
  resolveExternalTool: context.resolveExternalTool
2715
2874
  });
2875
+ context.subAgentMessages?.set(context.toolCallId, result.messages);
2876
+ return result.text;
2716
2877
  }
2717
2878
  };
2718
2879
 
2719
- // src/subagents/productVision/index.ts
2720
- import fs15 from "fs";
2721
- import path9 from "path";
2722
- var base3 = import.meta.dirname ?? path9.dirname(new URL(import.meta.url).pathname);
2723
- var local2 = path9.join(base3, "prompt.md");
2724
- var PROMPT_PATH2 = fs15.existsSync(local2) ? local2 : path9.join(base3, "subagents", "productVision", "prompt.md");
2725
- var BASE_PROMPT = fs15.readFileSync(PROMPT_PATH2, "utf-8").trim();
2726
- function loadSpecContext() {
2727
- const specDir = "src";
2728
- const files = [];
2729
- function walk(dir) {
2730
- try {
2731
- for (const entry of fs15.readdirSync(dir, { withFileTypes: true })) {
2732
- const full = path9.join(dir, entry.name);
2733
- if (entry.isDirectory()) {
2734
- if (entry.name !== "roadmap") {
2735
- walk(full);
2736
- }
2737
- } else if (entry.name.endsWith(".md")) {
2738
- files.push(full);
2739
- }
2740
- }
2741
- } catch {
2742
- }
2743
- }
2744
- walk(specDir);
2745
- if (files.length === 0) {
2746
- return "";
2747
- }
2748
- const sections = files.map((f) => {
2749
- try {
2750
- const content = fs15.readFileSync(f, "utf-8").trim();
2751
- return `<file path="${f}">
2752
- ${content}
2753
- </file>`;
2754
- } catch {
2755
- return "";
2756
- }
2757
- }).filter(Boolean);
2758
- return `<spec_files>
2759
- ${sections.join("\n\n")}
2760
- </spec_files>`;
2761
- }
2880
+ // src/subagents/productVision/tools.ts
2762
2881
  var VISION_TOOLS = [
2763
2882
  {
2764
2883
  name: "writeRoadmapItem",
2765
- description: "Write a roadmap item to src/roadmap/. Call this once for each idea.",
2884
+ description: "Create a new roadmap item in src/roadmap/.",
2766
2885
  inputSchema: {
2767
2886
  type: "object",
2768
2887
  properties: {
@@ -2789,50 +2908,225 @@ var VISION_TOOLS = [
2789
2908
  },
2790
2909
  body: {
2791
2910
  type: "string",
2792
- description: "Full MSFM body: prose description for the user, followed by ~~~annotation~~~ with technical implementation notes for the building agent."
2911
+ description: "Full MSFM body: prose description for the user, followed by ~~~annotation~~~ with technical implementation notes."
2793
2912
  }
2794
2913
  },
2795
2914
  required: ["slug", "name", "description", "effort", "requires", "body"]
2796
2915
  }
2916
+ },
2917
+ {
2918
+ name: "updateRoadmapItem",
2919
+ description: "Update an existing roadmap item. Only include the fields you want to change.",
2920
+ inputSchema: {
2921
+ type: "object",
2922
+ properties: {
2923
+ slug: {
2924
+ type: "string",
2925
+ description: "The slug of the item to update (filename without .md)."
2926
+ },
2927
+ status: {
2928
+ type: "string",
2929
+ enum: ["done", "in-progress", "not-started"],
2930
+ description: "New status."
2931
+ },
2932
+ name: {
2933
+ type: "string",
2934
+ description: "Updated feature name."
2935
+ },
2936
+ description: {
2937
+ type: "string",
2938
+ description: "Updated summary."
2939
+ },
2940
+ effort: {
2941
+ type: "string",
2942
+ enum: ["quick", "small", "medium", "large"],
2943
+ description: "Updated effort level."
2944
+ },
2945
+ requires: {
2946
+ type: "array",
2947
+ items: { type: "string" },
2948
+ description: "Updated prerequisites."
2949
+ },
2950
+ body: {
2951
+ type: "string",
2952
+ description: "Full replacement body (overwrites existing body)."
2953
+ },
2954
+ appendHistory: {
2955
+ type: "string",
2956
+ description: 'A history entry to append. Format: "- **2026-03-22** \u2014 Description of what was done."'
2957
+ }
2958
+ },
2959
+ required: ["slug"]
2960
+ }
2961
+ },
2962
+ {
2963
+ name: "deleteRoadmapItem",
2964
+ description: "Remove a roadmap item. Use when an idea is no longer relevant or has been absorbed into another item.",
2965
+ inputSchema: {
2966
+ type: "object",
2967
+ properties: {
2968
+ slug: {
2969
+ type: "string",
2970
+ description: "The slug of the item to delete (filename without .md)."
2971
+ }
2972
+ },
2973
+ required: ["slug"]
2974
+ }
2797
2975
  }
2798
2976
  ];
2977
+
2978
+ // src/subagents/productVision/executor.ts
2979
+ import fs16 from "fs";
2980
+ import path10 from "path";
2981
+ var ROADMAP_DIR = "src/roadmap";
2982
+ function formatRequires(requires) {
2983
+ return requires.length === 0 ? "[]" : `[${requires.map((r) => `"${r}"`).join(", ")}]`;
2984
+ }
2799
2985
  async function executeVisionTool(name, input) {
2800
- if (name !== "writeRoadmapItem") {
2801
- return `Error: unknown tool "${name}"`;
2802
- }
2803
- const { slug, name: itemName, description, effort, requires, body } = input;
2804
- const dir = "src/roadmap";
2805
- const filePath = path9.join(dir, `${slug}.md`);
2806
- try {
2807
- fs15.mkdirSync(dir, { recursive: true });
2808
- const requiresYaml = requires.length === 0 ? "[]" : `[${requires.map((r) => `"${r}"`).join(", ")}]`;
2809
- const content = `---
2986
+ switch (name) {
2987
+ case "writeRoadmapItem": {
2988
+ const {
2989
+ slug,
2990
+ name: itemName,
2991
+ description,
2992
+ effort,
2993
+ requires,
2994
+ body
2995
+ } = input;
2996
+ const filePath = path10.join(ROADMAP_DIR, `${slug}.md`);
2997
+ try {
2998
+ fs16.mkdirSync(ROADMAP_DIR, { recursive: true });
2999
+ const content = `---
2810
3000
  name: ${itemName}
2811
3001
  type: roadmap
2812
3002
  status: ${slug === "mvp" ? "in-progress" : "not-started"}
2813
3003
  description: ${description}
2814
3004
  effort: ${effort}
2815
- requires: ${requiresYaml}
3005
+ requires: ${formatRequires(requires)}
2816
3006
  ---
2817
3007
 
2818
3008
  ${body}
2819
3009
  `;
2820
- fs15.writeFileSync(filePath, content, "utf-8");
2821
- return `Wrote ${filePath}`;
2822
- } catch (err) {
2823
- return `Error writing ${filePath}: ${err.message}`;
3010
+ fs16.writeFileSync(filePath, content, "utf-8");
3011
+ return `Wrote ${filePath}`;
3012
+ } catch (err) {
3013
+ return `Error writing ${filePath}: ${err.message}`;
3014
+ }
3015
+ }
3016
+ case "updateRoadmapItem": {
3017
+ const { slug } = input;
3018
+ const filePath = path10.join(ROADMAP_DIR, `${slug}.md`);
3019
+ try {
3020
+ if (!fs16.existsSync(filePath)) {
3021
+ return `Error: ${filePath} does not exist`;
3022
+ }
3023
+ let content = fs16.readFileSync(filePath, "utf-8");
3024
+ if (input.status) {
3025
+ content = content.replace(
3026
+ /^status:\s*.+$/m,
3027
+ `status: ${input.status}`
3028
+ );
3029
+ }
3030
+ if (input.name) {
3031
+ content = content.replace(/^name:\s*.+$/m, `name: ${input.name}`);
3032
+ }
3033
+ if (input.description) {
3034
+ content = content.replace(
3035
+ /^description:\s*.+$/m,
3036
+ `description: ${input.description}`
3037
+ );
3038
+ }
3039
+ if (input.effort) {
3040
+ content = content.replace(
3041
+ /^effort:\s*.+$/m,
3042
+ `effort: ${input.effort}`
3043
+ );
3044
+ }
3045
+ if (input.requires) {
3046
+ content = content.replace(
3047
+ /^requires:\s*.+$/m,
3048
+ `requires: ${formatRequires(input.requires)}`
3049
+ );
3050
+ }
3051
+ if (input.body) {
3052
+ const endOfFrontmatter = content.indexOf("---", 4);
3053
+ if (endOfFrontmatter !== -1) {
3054
+ const frontmatter = content.slice(0, endOfFrontmatter + 3);
3055
+ content = `${frontmatter}
3056
+
3057
+ ${input.body}
3058
+ `;
3059
+ }
3060
+ }
3061
+ if (input.appendHistory) {
3062
+ if (content.includes("## History")) {
3063
+ content = content.trimEnd() + `
3064
+ ${input.appendHistory}
3065
+ `;
3066
+ } else {
3067
+ content = content.trimEnd() + `
3068
+
3069
+ ## History
3070
+
3071
+ ${input.appendHistory}
3072
+ `;
3073
+ }
3074
+ }
3075
+ fs16.writeFileSync(filePath, content, "utf-8");
3076
+ return `Updated ${filePath}`;
3077
+ } catch (err) {
3078
+ return `Error updating ${filePath}: ${err.message}`;
3079
+ }
3080
+ }
3081
+ case "deleteRoadmapItem": {
3082
+ const { slug } = input;
3083
+ const filePath = path10.join(ROADMAP_DIR, `${slug}.md`);
3084
+ try {
3085
+ if (!fs16.existsSync(filePath)) {
3086
+ return `Error: ${filePath} does not exist`;
3087
+ }
3088
+ fs16.unlinkSync(filePath);
3089
+ return `Deleted ${filePath}`;
3090
+ } catch (err) {
3091
+ return `Error deleting ${filePath}: ${err.message}`;
3092
+ }
3093
+ }
3094
+ default:
3095
+ return `Error: unknown tool "${name}"`;
2824
3096
  }
2825
3097
  }
3098
+
3099
+ // src/subagents/productVision/prompt.ts
3100
+ import fs17 from "fs";
3101
+ import path11 from "path";
3102
+ var base3 = import.meta.dirname ?? path11.dirname(new URL(import.meta.url).pathname);
3103
+ var local2 = path11.join(base3, "prompt.md");
3104
+ var PROMPT_PATH2 = fs17.existsSync(local2) ? local2 : path11.join(base3, "subagents", "productVision", "prompt.md");
3105
+ var BASE_PROMPT2 = fs17.readFileSync(PROMPT_PATH2, "utf-8").trim();
3106
+ function getProductVisionPrompt() {
3107
+ const specContext = loadSpecContext();
3108
+ const roadmapContext = loadRoadmapContext();
3109
+ const parts = [BASE_PROMPT2, loadPlatformBrief()];
3110
+ if (specContext) {
3111
+ parts.push(specContext);
3112
+ }
3113
+ if (roadmapContext) {
3114
+ parts.push(roadmapContext);
3115
+ }
3116
+ return parts.join("\n\n");
3117
+ }
3118
+
3119
+ // src/subagents/productVision/index.ts
2826
3120
  var productVisionTool = {
2827
3121
  definition: {
2828
3122
  name: "productVision",
2829
- description: `A product visionary that imagines where the project could go next. It automatically reads all spec files from src/ for context. Pass a brief description of the app and who it's for. It generates 10-15 ambitious, creative roadmap ideas and writes them directly to src/roadmap/. Use this at the end of spec authoring to populate the roadmap.`,
3123
+ description: "Owns the product roadmap. Reads spec and roadmap files automatically. Creates, updates, and deletes roadmap items in src/roadmap/. Describe the situation and what needs to happen.",
2830
3124
  inputSchema: {
2831
3125
  type: "object",
2832
3126
  properties: {
2833
3127
  task: {
2834
3128
  type: "string",
2835
- description: "Brief description of the app and who it's for. The tool reads the full spec files automatically \u2014 no need to repeat their contents."
3129
+ description: "What to do with the roadmap. Include relevant context. The tool reads spec and roadmap files automatically."
2836
3130
  }
2837
3131
  },
2838
3132
  required: ["task"]
@@ -2842,23 +3136,166 @@ var productVisionTool = {
2842
3136
  if (!context) {
2843
3137
  return "Error: product vision requires execution context";
2844
3138
  }
2845
- const specContext = loadSpecContext();
2846
- const system = specContext ? `${BASE_PROMPT}
2847
-
2848
- ${specContext}` : BASE_PROMPT;
2849
- return runSubAgent({
2850
- system,
3139
+ const result = await runSubAgent({
3140
+ system: getProductVisionPrompt(),
2851
3141
  task: input.task,
2852
3142
  tools: VISION_TOOLS,
2853
3143
  externalTools: /* @__PURE__ */ new Set(),
2854
3144
  executeTool: executeVisionTool,
2855
3145
  apiConfig: context.apiConfig,
2856
3146
  model: context.model,
3147
+ subAgentId: "productVision",
3148
+ signal: context.signal,
3149
+ parentToolId: context.toolCallId,
3150
+ onEvent: context.onEvent,
3151
+ resolveExternalTool: context.resolveExternalTool
3152
+ });
3153
+ context.subAgentMessages?.set(context.toolCallId, result.messages);
3154
+ return result.text;
3155
+ }
3156
+ };
3157
+
3158
+ // src/subagents/codeSanityCheck/index.ts
3159
+ import fs18 from "fs";
3160
+ import path12 from "path";
3161
+
3162
+ // src/subagents/codeSanityCheck/tools.ts
3163
+ var SANITY_CHECK_TOOLS = [
3164
+ {
3165
+ name: "readFile",
3166
+ description: "Read a file from the project.",
3167
+ inputSchema: {
3168
+ type: "object",
3169
+ properties: {
3170
+ path: {
3171
+ type: "string",
3172
+ description: "File path relative to project root."
3173
+ }
3174
+ },
3175
+ required: ["path"]
3176
+ }
3177
+ },
3178
+ {
3179
+ name: "grep",
3180
+ description: "Search file contents for a pattern.",
3181
+ inputSchema: {
3182
+ type: "object",
3183
+ properties: {
3184
+ pattern: { type: "string", description: "Search pattern (regex)." },
3185
+ path: {
3186
+ type: "string",
3187
+ description: "Directory or file to search in."
3188
+ }
3189
+ },
3190
+ required: ["pattern"]
3191
+ }
3192
+ },
3193
+ {
3194
+ name: "glob",
3195
+ description: "Find files by glob pattern.",
3196
+ inputSchema: {
3197
+ type: "object",
3198
+ properties: {
3199
+ pattern: {
3200
+ type: "string",
3201
+ description: 'Glob pattern (e.g., "src/**/*.ts").'
3202
+ }
3203
+ },
3204
+ required: ["pattern"]
3205
+ }
3206
+ },
3207
+ {
3208
+ name: "searchGoogle",
3209
+ description: "Search the web. Use to verify packages are current or find alternatives.",
3210
+ inputSchema: {
3211
+ type: "object",
3212
+ properties: {
3213
+ query: { type: "string", description: "Search query." }
3214
+ },
3215
+ required: ["query"]
3216
+ }
3217
+ },
3218
+ {
3219
+ name: "fetchUrl",
3220
+ description: "Fetch a web page as markdown. Use to read package docs, changelogs, npm pages.",
3221
+ inputSchema: {
3222
+ type: "object",
3223
+ properties: {
3224
+ url: { type: "string", description: "URL to fetch." }
3225
+ },
3226
+ required: ["url"]
3227
+ }
3228
+ },
3229
+ {
3230
+ name: "askMindStudioSdk",
3231
+ description: "Check if the MindStudio SDK has a managed action for something before writing custom code.",
3232
+ inputSchema: {
3233
+ type: "object",
3234
+ properties: {
3235
+ query: { type: "string", description: "What you want to check." }
3236
+ },
3237
+ required: ["query"]
3238
+ }
3239
+ },
3240
+ {
3241
+ name: "bash",
3242
+ description: "Run a shell command. Use for reading/search/etc operations only.",
3243
+ inputSchema: {
3244
+ type: "object",
3245
+ properties: {
3246
+ command: { type: "string", description: "The command to run." }
3247
+ },
3248
+ required: ["command"]
3249
+ }
3250
+ }
3251
+ ];
3252
+
3253
+ // src/subagents/codeSanityCheck/index.ts
3254
+ var base4 = import.meta.dirname ?? path12.dirname(new URL(import.meta.url).pathname);
3255
+ var local3 = path12.join(base4, "prompt.md");
3256
+ var PROMPT_PATH3 = fs18.existsSync(local3) ? local3 : path12.join(base4, "subagents", "codeSanityCheck", "prompt.md");
3257
+ var BASE_PROMPT3 = fs18.readFileSync(PROMPT_PATH3, "utf-8").trim();
3258
+ var codeSanityCheckTool = {
3259
+ definition: {
3260
+ name: "codeSanityCheck",
3261
+ description: 'Quick sanity check on an approach before building. Reviews architecture, package choices, and flags potential issues. Usually responds with "looks good." Occasionally catches something important. Readonly \u2014 can search the web and read code but cannot modify anything.',
3262
+ inputSchema: {
3263
+ type: "object",
3264
+ properties: {
3265
+ task: {
3266
+ type: "string",
3267
+ description: "What you're about to build and how. Include the plan, packages you intend to use, and any architectural decisions you've made."
3268
+ }
3269
+ },
3270
+ required: ["task"]
3271
+ }
3272
+ },
3273
+ async execute(input, context) {
3274
+ if (!context) {
3275
+ return "Error: code sanity check requires execution context";
3276
+ }
3277
+ const specContext = loadSpecContext();
3278
+ const parts = [BASE_PROMPT3, loadPlatformBrief()];
3279
+ if (specContext) {
3280
+ parts.push(specContext);
3281
+ }
3282
+ const system = parts.join("\n\n");
3283
+ const result = await runSubAgent({
3284
+ system,
3285
+ task: input.task,
3286
+ tools: SANITY_CHECK_TOOLS,
3287
+ externalTools: /* @__PURE__ */ new Set(),
3288
+ executeTool: (name, toolInput) => executeTool(name, toolInput, context),
3289
+ apiConfig: context.apiConfig,
3290
+ model: context.model,
3291
+ subAgentId: "codeSanityCheck",
2857
3292
  signal: context.signal,
2858
3293
  parentToolId: context.toolCallId,
2859
3294
  onEvent: context.onEvent,
2860
3295
  resolveExternalTool: context.resolveExternalTool
2861
3296
  });
3297
+ context.subAgentMessages?.set(context.toolCallId, result.messages);
3298
+ return result.text;
2862
3299
  }
2863
3300
  };
2864
3301
 
@@ -2896,7 +3333,8 @@ function getCommonTools() {
2896
3333
  searchGoogleTool,
2897
3334
  setProjectNameTool,
2898
3335
  designExpertTool,
2899
- productVisionTool
3336
+ productVisionTool,
3337
+ codeSanityCheckTool
2900
3338
  ];
2901
3339
  }
2902
3340
  function getPostOnboardingTools() {
@@ -2943,11 +3381,11 @@ function executeTool(name, input, context) {
2943
3381
  }
2944
3382
 
2945
3383
  // src/session.ts
2946
- import fs16 from "fs";
3384
+ import fs19 from "fs";
2947
3385
  var SESSION_FILE = ".remy-session.json";
2948
3386
  function loadSession(state) {
2949
3387
  try {
2950
- const raw = fs16.readFileSync(SESSION_FILE, "utf-8");
3388
+ const raw = fs19.readFileSync(SESSION_FILE, "utf-8");
2951
3389
  const data = JSON.parse(raw);
2952
3390
  if (Array.isArray(data.messages) && data.messages.length > 0) {
2953
3391
  state.messages = sanitizeMessages(data.messages);
@@ -2962,7 +3400,13 @@ function sanitizeMessages(messages) {
2962
3400
  for (let i = 0; i < messages.length; i++) {
2963
3401
  result.push(messages[i]);
2964
3402
  const msg = messages[i];
2965
- if (msg.role !== "assistant" || !msg.toolCalls?.length) {
3403
+ if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
3404
+ continue;
3405
+ }
3406
+ const toolBlocks = msg.content.filter(
3407
+ (b) => b.type === "tool"
3408
+ );
3409
+ if (toolBlocks.length === 0) {
2966
3410
  continue;
2967
3411
  }
2968
3412
  const resultIds = /* @__PURE__ */ new Set();
@@ -2974,7 +3418,7 @@ function sanitizeMessages(messages) {
2974
3418
  break;
2975
3419
  }
2976
3420
  }
2977
- for (const tc of msg.toolCalls) {
3421
+ for (const tc of toolBlocks) {
2978
3422
  if (!resultIds.has(tc.id)) {
2979
3423
  result.push({
2980
3424
  role: "user",
@@ -2989,7 +3433,7 @@ function sanitizeMessages(messages) {
2989
3433
  }
2990
3434
  function saveSession(state) {
2991
3435
  try {
2992
- fs16.writeFileSync(
3436
+ fs19.writeFileSync(
2993
3437
  SESSION_FILE,
2994
3438
  JSON.stringify({ messages: state.messages }, null, 2),
2995
3439
  "utf-8"
@@ -3000,7 +3444,7 @@ function saveSession(state) {
3000
3444
  function clearSession(state) {
3001
3445
  state.messages = [];
3002
3446
  try {
3003
- fs16.unlinkSync(SESSION_FILE);
3447
+ fs19.unlinkSync(SESSION_FILE);
3004
3448
  } catch {
3005
3449
  }
3006
3450
  }
@@ -3272,6 +3716,14 @@ function friendlyError(raw) {
3272
3716
  }
3273
3717
 
3274
3718
  // src/agent.ts
3719
+ function getTextContent(blocks) {
3720
+ return blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
3721
+ }
3722
+ function getToolCalls(blocks) {
3723
+ return blocks.filter(
3724
+ (b) => b.type === "tool"
3725
+ );
3726
+ }
3275
3727
  var EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
3276
3728
  "promptUser",
3277
3729
  "setProjectOnboardingState",
@@ -3283,7 +3735,6 @@ var EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
3283
3735
  "runScenario",
3284
3736
  "runMethod",
3285
3737
  "browserCommand",
3286
- "screenshot",
3287
3738
  "setProjectName"
3288
3739
  ]);
3289
3740
  function createAgentState() {
@@ -3327,6 +3778,12 @@ async function runTurn(params) {
3327
3778
  });
3328
3779
  }
3329
3780
  state.messages.push(userMsg);
3781
+ const STATUS_EXCLUDED_TOOLS = /* @__PURE__ */ new Set([
3782
+ "setProjectOnboardingState",
3783
+ "setProjectName",
3784
+ "clearSyncStatus",
3785
+ "editsFinished"
3786
+ ]);
3330
3787
  let lastCompletedTools = "";
3331
3788
  let lastCompletedResult = "";
3332
3789
  while (true) {
@@ -3344,8 +3801,8 @@ async function runTurn(params) {
3344
3801
  saveSession(state);
3345
3802
  return;
3346
3803
  }
3347
- let assistantText = "";
3348
- const toolCalls = [];
3804
+ const contentBlocks = [];
3805
+ let thinkingStartedAt = 0;
3349
3806
  const toolInputAccumulators = /* @__PURE__ */ new Map();
3350
3807
  let stopReason = "end_turn";
3351
3808
  async function handlePartialInput(acc, id, name, partial) {
@@ -3414,8 +3871,8 @@ async function runTurn(params) {
3414
3871
  const statusWatcher = startStatusWatcher({
3415
3872
  apiConfig,
3416
3873
  getContext: () => ({
3417
- assistantText: assistantText.slice(-500),
3418
- lastToolName: toolCalls.at(-1)?.name || lastCompletedTools || void 0,
3874
+ assistantText: getTextContent(contentBlocks).slice(-500),
3875
+ lastToolName: getToolCalls(contentBlocks).filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).at(-1)?.name || lastCompletedTools || void 0,
3419
3876
  lastToolResult: lastCompletedResult || void 0
3420
3877
  }),
3421
3878
  onStatus: (label) => onEvent({ type: "status", message: label }),
@@ -3427,7 +3884,7 @@ async function runTurn(params) {
3427
3884
  ...apiConfig,
3428
3885
  model,
3429
3886
  system,
3430
- messages: state.messages,
3887
+ messages: cleanMessagesForApi(state.messages),
3431
3888
  tools,
3432
3889
  signal
3433
3890
  },
@@ -3444,13 +3901,36 @@ async function runTurn(params) {
3444
3901
  break;
3445
3902
  }
3446
3903
  switch (event.type) {
3447
- case "text":
3448
- assistantText += event.text;
3904
+ case "text": {
3905
+ const lastBlock = contentBlocks.at(-1);
3906
+ if (lastBlock?.type === "text") {
3907
+ lastBlock.text += event.text;
3908
+ } else {
3909
+ contentBlocks.push({
3910
+ type: "text",
3911
+ text: event.text,
3912
+ startedAt: event.ts
3913
+ });
3914
+ }
3449
3915
  onEvent({ type: "text", text: event.text });
3450
3916
  break;
3917
+ }
3451
3918
  case "thinking":
3919
+ if (!thinkingStartedAt) {
3920
+ thinkingStartedAt = event.ts;
3921
+ }
3452
3922
  onEvent({ type: "thinking", text: event.text });
3453
3923
  break;
3924
+ case "thinking_complete":
3925
+ contentBlocks.push({
3926
+ type: "thinking",
3927
+ thinking: event.thinking,
3928
+ signature: event.signature,
3929
+ startedAt: thinkingStartedAt,
3930
+ completedAt: event.ts
3931
+ });
3932
+ thinkingStartedAt = 0;
3933
+ break;
3454
3934
  case "tool_input_delta": {
3455
3935
  const acc = getOrCreateAccumulator2(event.id, event.name);
3456
3936
  acc.json += event.delta;
@@ -3478,10 +3958,12 @@ async function runTurn(params) {
3478
3958
  break;
3479
3959
  }
3480
3960
  case "tool_use": {
3481
- toolCalls.push({
3961
+ contentBlocks.push({
3962
+ type: "tool",
3482
3963
  id: event.id,
3483
3964
  name: event.name,
3484
- input: event.input
3965
+ input: event.input,
3966
+ startedAt: event.ts
3485
3967
  });
3486
3968
  const acc = toolInputAccumulators.get(event.id);
3487
3969
  const tool = getToolByName(event.name);
@@ -3520,11 +4002,15 @@ async function runTurn(params) {
3520
4002
  statusWatcher.stop();
3521
4003
  }
3522
4004
  if (signal?.aborted) {
3523
- if (assistantText) {
4005
+ if (contentBlocks.length > 0) {
4006
+ contentBlocks.push({
4007
+ type: "text",
4008
+ text: "\n\n(cancelled)",
4009
+ startedAt: Date.now()
4010
+ });
3524
4011
  state.messages.push({
3525
4012
  role: "assistant",
3526
- content: assistantText + "\n\n(cancelled)",
3527
- toolCalls: toolCalls.length > 0 ? toolCalls : void 0
4013
+ content: [...contentBlocks].sort((a, b) => a.startedAt - b.startedAt)
3528
4014
  });
3529
4015
  }
3530
4016
  onEvent({ type: "turn_cancelled" });
@@ -3533,9 +4019,9 @@ async function runTurn(params) {
3533
4019
  }
3534
4020
  state.messages.push({
3535
4021
  role: "assistant",
3536
- content: assistantText,
3537
- toolCalls: toolCalls.length > 0 ? toolCalls : void 0
4022
+ content: [...contentBlocks].sort((a, b) => a.startedAt - b.startedAt)
3538
4023
  });
4024
+ const toolCalls = getToolCalls(contentBlocks);
3539
4025
  if (stopReason !== "tool_use" || toolCalls.length === 0) {
3540
4026
  saveSession(state);
3541
4027
  onEvent({ type: "turn_done" });
@@ -3545,6 +4031,29 @@ async function runTurn(params) {
3545
4031
  count: toolCalls.length,
3546
4032
  tools: toolCalls.map((tc) => tc.name)
3547
4033
  });
4034
+ let subAgentText = "";
4035
+ const origOnEvent = onEvent;
4036
+ const wrappedOnEvent = (e) => {
4037
+ if ("parentToolId" in e && e.parentToolId) {
4038
+ if (e.type === "text") {
4039
+ subAgentText = e.text;
4040
+ } else if (e.type === "tool_start") {
4041
+ subAgentText = `Using ${e.name}`;
4042
+ }
4043
+ }
4044
+ origOnEvent(e);
4045
+ };
4046
+ const toolStatusWatcher = startStatusWatcher({
4047
+ apiConfig,
4048
+ getContext: () => ({
4049
+ assistantText: subAgentText || getTextContent(contentBlocks).slice(-500),
4050
+ lastToolName: toolCalls.filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).map((tc) => tc.name).join(", ") || void 0,
4051
+ lastToolResult: lastCompletedResult || void 0
4052
+ }),
4053
+ onStatus: (label) => origOnEvent({ type: "status", message: label }),
4054
+ signal
4055
+ });
4056
+ const subAgentMessages = /* @__PURE__ */ new Map();
3548
4057
  const results = await Promise.all(
3549
4058
  toolCalls.map(async (tc) => {
3550
4059
  if (signal?.aborted) {
@@ -3569,9 +4078,10 @@ async function runTurn(params) {
3569
4078
  apiConfig,
3570
4079
  model,
3571
4080
  signal,
3572
- onEvent,
4081
+ onEvent: wrappedOnEvent,
3573
4082
  resolveExternalTool,
3574
- toolCallId: tc.id
4083
+ toolCallId: tc.id,
4084
+ subAgentMessages
3575
4085
  });
3576
4086
  }
3577
4087
  const isError = result.startsWith("Error");
@@ -3602,7 +4112,21 @@ async function runTurn(params) {
3602
4112
  }
3603
4113
  })
3604
4114
  );
3605
- lastCompletedTools = toolCalls.map((tc) => tc.name).join(", ");
4115
+ toolStatusWatcher.stop();
4116
+ for (const r of results) {
4117
+ const block = contentBlocks.find(
4118
+ (b) => b.type === "tool" && b.id === r.id
4119
+ );
4120
+ if (block?.type === "tool") {
4121
+ block.result = r.result;
4122
+ block.isError = r.isError;
4123
+ const msgs = subAgentMessages.get(r.id);
4124
+ if (msgs) {
4125
+ block.subAgentMessages = msgs;
4126
+ }
4127
+ }
4128
+ }
4129
+ lastCompletedTools = toolCalls.filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).map((tc) => tc.name).join(", ");
3606
4130
  lastCompletedResult = results.at(-1)?.result ?? "";
3607
4131
  for (const r of results) {
3608
4132
  state.messages.push({
@@ -3621,10 +4145,10 @@ async function runTurn(params) {
3621
4145
  }
3622
4146
 
3623
4147
  // src/headless.ts
3624
- var BASE_DIR = import.meta.dirname ?? path10.dirname(new URL(import.meta.url).pathname);
3625
- var ACTIONS_DIR = path10.join(BASE_DIR, "actions");
4148
+ var BASE_DIR = import.meta.dirname ?? path13.dirname(new URL(import.meta.url).pathname);
4149
+ var ACTIONS_DIR = path13.join(BASE_DIR, "actions");
3626
4150
  function loadActionPrompt(name) {
3627
- return fs17.readFileSync(path10.join(ACTIONS_DIR, `${name}.md`), "utf-8").trim();
4151
+ return fs20.readFileSync(path13.join(ACTIONS_DIR, `${name}.md`), "utf-8").trim();
3628
4152
  }
3629
4153
  function emit(event, data) {
3630
4154
  process.stdout.write(JSON.stringify({ event, ...data }) + "\n");
@@ -3712,19 +4236,27 @@ async function startHeadless(opts = {}) {
3712
4236
  break;
3713
4237
  }
3714
4238
  }
3715
- function resolveExternalTool(id, _name, _input) {
4239
+ const USER_FACING_TOOLS = /* @__PURE__ */ new Set([
4240
+ "promptUser",
4241
+ "confirmDestructiveAction",
4242
+ "presentPlan",
4243
+ "presentSyncPlan",
4244
+ "presentPublishPlan"
4245
+ ]);
4246
+ function resolveExternalTool(id, name, _input) {
3716
4247
  const early = earlyResults.get(id);
3717
4248
  if (early !== void 0) {
3718
4249
  earlyResults.delete(id);
3719
4250
  return Promise.resolve(early);
3720
4251
  }
4252
+ const shouldTimeout = !USER_FACING_TOOLS.has(name);
3721
4253
  return new Promise((resolve) => {
3722
- const timeout = setTimeout(() => {
4254
+ const timeout = shouldTimeout ? setTimeout(() => {
3723
4255
  pendingTools.delete(id);
3724
4256
  resolve(
3725
4257
  "Error: Tool timed out \u2014 no response from the app environment after 5 minutes."
3726
4258
  );
3727
- }, EXTERNAL_TOOL_TIMEOUT_MS);
4259
+ }, EXTERNAL_TOOL_TIMEOUT_MS) : void 0;
3728
4260
  pendingTools.set(id, {
3729
4261
  resolve: (result) => {
3730
4262
  clearTimeout(timeout);