@mindstudio-ai/remy 0.1.4 → 0.1.5

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.
@@ -2,4 +2,6 @@ This is an automated action triggered by the user pressing "Build" in the editor
2
2
 
3
3
  The user has reviewed the spec and is ready to build. Build everything in one turn: methods, tables, interfaces, manifest updates, and scenarios, using the spec as the master plan.
4
4
 
5
- When code generation is complete, call `setProjectOnboardingState({ state: "onboardingFinished" })`.
5
+ When code generation is complete, verify your work: use `runScenario` to seed test data, then use `runMethod` to confirm a method works, then use `runAutomatedBrowserTest` to smoke-test the main UI flow. The dev database is a disposable snapshot, so don't worry about being destructive. Fix any errors before finishing.
6
+
7
+ When everything is working, call `setProjectOnboardingState({ state: "onboardingFinished" })`.
@@ -12,6 +12,17 @@ good on Dribbble, Behance, or Mobbin, it's not done.
12
12
  MindStudio apps are end-user products. The interface is the product. Users
13
13
  judge the entire app by how it looks and feels in the first 3 seconds.
14
14
 
15
+ ## Design System from the Spec
16
+
17
+ The spec file `src/interfaces/@brand/visual.md` may contain `typography` and
18
+ `colors` YAML blocks that define the app's fonts and color palette. When
19
+ these are present, always use them. Load fonts from the URLs in the `fonts`
20
+ section. Set up a lightweight theme layer early (CSS variables or a small
21
+ tokens file) so colors and type styles are defined once and referenced
22
+ everywhere. This makes the design easy to update later without hunting
23
+ through components. Keep it simple: a handful of CSS variables for colors
24
+ and a few reusable text style classes or utilities for typography.
25
+
15
26
  ## Be Distinctive
16
27
 
17
28
  AI-generated interfaces tend to converge on the same generic look: safe
package/dist/headless.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/headless.ts
2
2
  import { createInterface } from "readline";
3
- import fs14 from "fs";
4
- import path7 from "path";
3
+ import fs15 from "fs";
4
+ import path8 from "path";
5
5
 
6
6
  // src/config.ts
7
7
  import fs2 from "fs";
@@ -940,8 +940,8 @@ var promptUserTool = {
940
940
  },
941
941
  type: {
942
942
  type: "string",
943
- enum: ["select", "text", "file", "color"],
944
- description: 'select: pick from options (or options + free-form "other"). text: free-form input. file: file/image upload, returns CDN URL(s) that can be referenced directly or curled onto disk. color: color picker (returns hex).'
943
+ enum: ["select", "checklist", "text", "file", "color"],
944
+ description: "select: pick one from a list. checklist: pick one or more from a list. text: free-form input. file: file/image upload, returns CDN URL(s) that can be referenced directly or curled onto disk. color: color picker (returns hex)."
945
945
  },
946
946
  helpText: {
947
947
  type: "string",
@@ -972,15 +972,15 @@ var promptUserTool = {
972
972
  }
973
973
  ]
974
974
  },
975
- description: "Options for select type. Each can be a string or { label, description }."
975
+ description: "Options for select and checklist types. Each can be a string or { label, description }."
976
976
  },
977
977
  multiple: {
978
978
  type: "boolean",
979
- description: "For select: allow picking multiple options (returns array). For file: allow multiple uploads (returns array of URLs). Defaults to false."
979
+ description: "For file type: allow multiple uploads (returns array of URLs). Defaults to false."
980
980
  },
981
981
  allowOther: {
982
982
  type: "boolean",
983
- description: 'For select type: adds an "Other" option with a free-form text input. Defaults to false.'
983
+ description: 'For select and checklist types: adds an "Other" option that lets the user type a custom answer. Use this instead of adding a separate follow-up text field for custom input. Defaults to false.'
984
984
  },
985
985
  format: {
986
986
  type: "string",
@@ -1032,11 +1032,11 @@ var promptUserTool = {
1032
1032
  const questions = input.questions;
1033
1033
  const lines = questions.map((q) => {
1034
1034
  let line = `- ${q.question}`;
1035
- if (q.type === "select") {
1035
+ if (q.type === "select" || q.type === "checklist") {
1036
1036
  const opts = (q.options || []).map(
1037
1037
  (o) => typeof o === "string" ? o : o.label
1038
1038
  );
1039
- line += q.multiple ? ` (pick one or more: ${opts.join(" / ")})` : ` (${opts.join(" / ")})`;
1039
+ line += q.type === "checklist" ? ` (pick one or more: ${opts.join(" / ")})` : ` (${opts.join(" / ")})`;
1040
1040
  } else if (q.type === "file") {
1041
1041
  line += " (upload file)";
1042
1042
  } else if (q.type === "color") {
@@ -1251,14 +1251,26 @@ var writeFileTool = {
1251
1251
  required: ["path", "content"]
1252
1252
  }
1253
1253
  },
1254
- streaming: {
1255
- transform: async (partial) => {
1256
- const oldContent = await fs10.readFile(partial.path, "utf-8").catch(() => "");
1257
- const lineCount = partial.content.split("\n").length;
1258
- return `Writing ${partial.path} (${lineCount} lines)
1254
+ streaming: /* @__PURE__ */ (() => {
1255
+ let lastLineCount = 0;
1256
+ let lastPath = "";
1257
+ return {
1258
+ transform: async (partial) => {
1259
+ if (partial.path !== lastPath) {
1260
+ lastLineCount = 0;
1261
+ lastPath = partial.path;
1262
+ }
1263
+ const lines = partial.content.split("\n");
1264
+ if (lines.length <= lastLineCount) {
1265
+ return null;
1266
+ }
1267
+ lastLineCount = lines.length;
1268
+ const oldContent = await fs10.readFile(partial.path, "utf-8").catch(() => "");
1269
+ return `Writing ${partial.path} (${lines.length} lines)
1259
1270
  ${unifiedDiff(partial.path, oldContent, partial.content)}`;
1260
- }
1261
- },
1271
+ }
1272
+ };
1273
+ })(),
1262
1274
  async execute(input) {
1263
1275
  try {
1264
1276
  await fs10.mkdir(path6.dirname(input.path), { recursive: true });
@@ -1794,6 +1806,313 @@ var askMindStudioSdkTool = {
1794
1806
  }
1795
1807
  };
1796
1808
 
1809
+ // src/tools/code/runScenario.ts
1810
+ var runScenarioTool = {
1811
+ definition: {
1812
+ name: "runScenario",
1813
+ description: "Run a scenario to seed the dev database with test data. Truncates all tables first, then executes the seed function and impersonates the scenario roles. Blocks until complete. Scenario IDs are defined in mindstudio.json. If it fails, check .logs/tunnel.log or .logs/requests.ndjson for details.",
1814
+ inputSchema: {
1815
+ type: "object",
1816
+ properties: {
1817
+ scenarioId: {
1818
+ type: "string",
1819
+ description: "The scenario ID from mindstudio.json."
1820
+ }
1821
+ },
1822
+ required: ["scenarioId"]
1823
+ }
1824
+ },
1825
+ async execute() {
1826
+ return "ok";
1827
+ }
1828
+ };
1829
+
1830
+ // src/tools/code/runMethod.ts
1831
+ var runMethodTool = {
1832
+ definition: {
1833
+ name: "runMethod",
1834
+ description: "Run a method in the dev environment and return the result. Use for testing methods after writing or modifying them. Returns output, captured console output, errors with stack traces, and duration. If it fails, check .logs/tunnel.log or .logs/requests.ndjson for more details.",
1835
+ inputSchema: {
1836
+ type: "object",
1837
+ properties: {
1838
+ method: {
1839
+ type: "string",
1840
+ description: 'The method export name (camelCase, e.g. "listHaikus").'
1841
+ },
1842
+ input: {
1843
+ type: "object",
1844
+ description: "The input payload to pass to the method. Omit for methods that take no input."
1845
+ }
1846
+ },
1847
+ required: ["method"]
1848
+ }
1849
+ },
1850
+ async execute() {
1851
+ return "ok";
1852
+ }
1853
+ };
1854
+
1855
+ // src/tools/code/screenshot.ts
1856
+ var screenshotTool = {
1857
+ definition: {
1858
+ name: "screenshot",
1859
+ 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.",
1860
+ inputSchema: {
1861
+ type: "object",
1862
+ properties: {}
1863
+ }
1864
+ },
1865
+ async execute() {
1866
+ return "ok";
1867
+ }
1868
+ };
1869
+
1870
+ // src/subagents/runner.ts
1871
+ async function runSubAgent(config) {
1872
+ const {
1873
+ system,
1874
+ task,
1875
+ tools,
1876
+ externalTools,
1877
+ executeTool: executeTool2,
1878
+ apiConfig,
1879
+ model,
1880
+ signal,
1881
+ parentToolId,
1882
+ onEvent,
1883
+ resolveExternalTool
1884
+ } = config;
1885
+ const emit2 = (e) => {
1886
+ onEvent({ ...e, parentToolId });
1887
+ };
1888
+ const messages = [{ role: "user", content: task }];
1889
+ while (true) {
1890
+ if (signal?.aborted) {
1891
+ return "Error: cancelled";
1892
+ }
1893
+ let assistantText = "";
1894
+ const toolCalls = [];
1895
+ let stopReason = "end_turn";
1896
+ try {
1897
+ for await (const event of streamChat({
1898
+ ...apiConfig,
1899
+ model,
1900
+ system,
1901
+ messages,
1902
+ tools,
1903
+ signal
1904
+ })) {
1905
+ if (signal?.aborted) {
1906
+ break;
1907
+ }
1908
+ switch (event.type) {
1909
+ case "text":
1910
+ assistantText += event.text;
1911
+ emit2({ type: "text", text: event.text });
1912
+ break;
1913
+ case "thinking":
1914
+ emit2({ type: "thinking", text: event.text });
1915
+ break;
1916
+ case "tool_use":
1917
+ toolCalls.push({
1918
+ id: event.id,
1919
+ name: event.name,
1920
+ input: event.input
1921
+ });
1922
+ emit2({
1923
+ type: "tool_start",
1924
+ id: event.id,
1925
+ name: event.name,
1926
+ input: event.input
1927
+ });
1928
+ break;
1929
+ case "done":
1930
+ stopReason = event.stopReason;
1931
+ break;
1932
+ case "error":
1933
+ return `Error: ${event.error}`;
1934
+ }
1935
+ }
1936
+ } catch (err) {
1937
+ if (!signal?.aborted) {
1938
+ throw err;
1939
+ }
1940
+ }
1941
+ if (signal?.aborted) {
1942
+ return "Error: cancelled";
1943
+ }
1944
+ messages.push({
1945
+ role: "assistant",
1946
+ content: assistantText,
1947
+ toolCalls: toolCalls.length > 0 ? toolCalls : void 0
1948
+ });
1949
+ if (stopReason !== "tool_use" || toolCalls.length === 0) {
1950
+ return assistantText;
1951
+ }
1952
+ log.info("Sub-agent executing tools", {
1953
+ parentToolId,
1954
+ count: toolCalls.length,
1955
+ tools: toolCalls.map((tc) => tc.name)
1956
+ });
1957
+ const results = await Promise.all(
1958
+ toolCalls.map(async (tc) => {
1959
+ if (signal?.aborted) {
1960
+ return { id: tc.id, result: "Error: cancelled", isError: true };
1961
+ }
1962
+ try {
1963
+ let result;
1964
+ if (externalTools.has(tc.name) && resolveExternalTool) {
1965
+ result = await resolveExternalTool(tc.id, tc.name, tc.input);
1966
+ } else {
1967
+ result = await executeTool2(tc.name, tc.input);
1968
+ }
1969
+ const isError = result.startsWith("Error");
1970
+ emit2({
1971
+ type: "tool_done",
1972
+ id: tc.id,
1973
+ name: tc.name,
1974
+ result,
1975
+ isError
1976
+ });
1977
+ return { id: tc.id, result, isError };
1978
+ } catch (err) {
1979
+ const errorMsg = `Error: ${err.message}`;
1980
+ emit2({
1981
+ type: "tool_done",
1982
+ id: tc.id,
1983
+ name: tc.name,
1984
+ result: errorMsg,
1985
+ isError: true
1986
+ });
1987
+ return { id: tc.id, result: errorMsg, isError: true };
1988
+ }
1989
+ })
1990
+ );
1991
+ for (const r of results) {
1992
+ messages.push({
1993
+ role: "user",
1994
+ content: r.result,
1995
+ toolCallId: r.id,
1996
+ isToolError: r.isError
1997
+ });
1998
+ }
1999
+ }
2000
+ }
2001
+
2002
+ // src/subagents/browserAutomation/tools.ts
2003
+ var BROWSER_TOOLS = [
2004
+ {
2005
+ name: "browserCommand",
2006
+ description: "Interact with the app's live preview by sending browser commands. Commands execute sequentially with an animated cursor. Always start with a snapshot to see the current state and get ref identifiers. The result includes a snapshot field with the final page state after all steps complete. On error, the failing step has an error field and execution stops. Timeout: 120s.",
2007
+ inputSchema: {
2008
+ type: "object",
2009
+ properties: {
2010
+ steps: {
2011
+ type: "array",
2012
+ items: {
2013
+ type: "object",
2014
+ properties: {
2015
+ command: {
2016
+ type: "string",
2017
+ enum: ["snapshot", "click", "type", "wait", "evaluate"],
2018
+ description: "snapshot: accessibility tree of the page (waits for network to settle). click: click an element (animated cursor, full event sequence). type: type text into input (one char at a time, works with React/Vue/Svelte). wait: wait for an element to appear (polls 100ms, waits for network). evaluate: run JS in the page."
2019
+ },
2020
+ ref: {
2021
+ type: "string",
2022
+ description: "Element ref from the last snapshot (most reliable targeting)."
2023
+ },
2024
+ text: {
2025
+ type: "string",
2026
+ description: "For click/wait: match by accessible name or visible text. For type: the text to type."
2027
+ },
2028
+ role: {
2029
+ type: "string",
2030
+ description: "ARIA role to match (used with text for role+text targeting)."
2031
+ },
2032
+ label: {
2033
+ type: "string",
2034
+ description: "Find an input by its associated label text."
2035
+ },
2036
+ selector: {
2037
+ type: "string",
2038
+ description: "CSS selector fallback (last resort)."
2039
+ },
2040
+ clear: {
2041
+ type: "boolean",
2042
+ description: "For type: clear the field before typing."
2043
+ },
2044
+ timeout: {
2045
+ type: "number",
2046
+ description: "For wait: timeout in ms (default 5000)."
2047
+ },
2048
+ script: {
2049
+ type: "string",
2050
+ description: "For evaluate: JavaScript to run in the page."
2051
+ }
2052
+ },
2053
+ required: ["command"]
2054
+ }
2055
+ }
2056
+ },
2057
+ required: ["steps"]
2058
+ }
2059
+ },
2060
+ {
2061
+ name: "screenshot",
2062
+ description: "Capture a screenshot of the current page. Returns a CDN URL with dimensions.",
2063
+ inputSchema: {
2064
+ type: "object",
2065
+ properties: {}
2066
+ }
2067
+ }
2068
+ ];
2069
+ var BROWSER_EXTERNAL_TOOLS = /* @__PURE__ */ new Set(["browserCommand", "screenshot"]);
2070
+
2071
+ // src/subagents/browserAutomation/prompt.ts
2072
+ import fs13 from "fs";
2073
+ import path7 from "path";
2074
+ var PROMPT_PATH = path7.join(
2075
+ import.meta.dirname ?? path7.dirname(new URL(import.meta.url).pathname),
2076
+ "prompt.md"
2077
+ );
2078
+ var BROWSER_AUTOMATION_PROMPT = fs13.readFileSync(PROMPT_PATH, "utf-8").trim();
2079
+
2080
+ // src/subagents/browserAutomation/index.ts
2081
+ var browserAutomationTool = {
2082
+ definition: {
2083
+ name: "runAutomatedBrowserTest",
2084
+ description: "Run an automated browser test against the live preview. The test agent always starts on the main page, so include navigation instructions if the test involves a sub-page. The browser uses the current user roles and dev database state, so run a scenario first if you need specific data or roles. Use after writing or modifying frontend code, to reproduce user-reported issues, or to test end-to-end flows.",
2085
+ inputSchema: {
2086
+ type: "object",
2087
+ properties: {
2088
+ task: {
2089
+ type: "string",
2090
+ description: "What to test, in natural language. Include how to navigate to the relevant page and what data/roles to expect."
2091
+ }
2092
+ },
2093
+ required: ["task"]
2094
+ }
2095
+ },
2096
+ async execute(input, context) {
2097
+ if (!context) {
2098
+ return "Error: browser automation requires execution context (only available in headless mode)";
2099
+ }
2100
+ return runSubAgent({
2101
+ system: BROWSER_AUTOMATION_PROMPT,
2102
+ task: input.task,
2103
+ tools: BROWSER_TOOLS,
2104
+ externalTools: BROWSER_EXTERNAL_TOOLS,
2105
+ executeTool: async () => "Error: no local tools in browser automation",
2106
+ apiConfig: context.apiConfig,
2107
+ model: context.model,
2108
+ signal: context.signal,
2109
+ parentToolId: context.toolCallId,
2110
+ onEvent: context.onEvent,
2111
+ resolveExternalTool: context.resolveExternalTool
2112
+ });
2113
+ }
2114
+ };
2115
+
1797
2116
  // src/tools/index.ts
1798
2117
  function getSpecTools() {
1799
2118
  return [readSpecTool, writeSpecTool, editSpecTool, listSpecFilesTool];
@@ -1808,7 +2127,11 @@ function getCodeTools() {
1808
2127
  globTool,
1809
2128
  listDirTool,
1810
2129
  editsFinishedTool,
1811
- askMindStudioSdkTool
2130
+ askMindStudioSdkTool,
2131
+ runScenarioTool,
2132
+ runMethodTool,
2133
+ screenshotTool,
2134
+ browserAutomationTool
1812
2135
  ];
1813
2136
  if (isLspConfigured()) {
1814
2137
  tools.push(lspDiagnosticsTool, restartProcessTool);
@@ -1857,20 +2180,20 @@ function getToolByName(name) {
1857
2180
  ];
1858
2181
  return allTools.find((t) => t.definition.name === name);
1859
2182
  }
1860
- function executeTool(name, input) {
2183
+ function executeTool(name, input, context) {
1861
2184
  const tool = getToolByName(name);
1862
2185
  if (!tool) {
1863
2186
  return Promise.resolve(`Error: Unknown tool "${name}"`);
1864
2187
  }
1865
- return tool.execute(input);
2188
+ return tool.execute(input, context);
1866
2189
  }
1867
2190
 
1868
2191
  // src/session.ts
1869
- import fs13 from "fs";
2192
+ import fs14 from "fs";
1870
2193
  var SESSION_FILE = ".remy-session.json";
1871
2194
  function loadSession(state) {
1872
2195
  try {
1873
- const raw = fs13.readFileSync(SESSION_FILE, "utf-8");
2196
+ const raw = fs14.readFileSync(SESSION_FILE, "utf-8");
1874
2197
  const data = JSON.parse(raw);
1875
2198
  if (Array.isArray(data.messages) && data.messages.length > 0) {
1876
2199
  state.messages = sanitizeMessages(data.messages);
@@ -1912,7 +2235,7 @@ function sanitizeMessages(messages) {
1912
2235
  }
1913
2236
  function saveSession(state) {
1914
2237
  try {
1915
- fs13.writeFileSync(
2238
+ fs14.writeFileSync(
1916
2239
  SESSION_FILE,
1917
2240
  JSON.stringify({ messages: state.messages }, null, 2),
1918
2241
  "utf-8"
@@ -1923,7 +2246,7 @@ function saveSession(state) {
1923
2246
  function clearSession(state) {
1924
2247
  state.messages = [];
1925
2248
  try {
1926
- fs13.unlinkSync(SESSION_FILE);
2249
+ fs14.unlinkSync(SESSION_FILE);
1927
2250
  } catch {
1928
2251
  }
1929
2252
  }
@@ -2099,7 +2422,11 @@ var EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
2099
2422
  "presentSyncPlan",
2100
2423
  "presentPublishPlan",
2101
2424
  "presentPlan",
2102
- "confirmDestructiveAction"
2425
+ "confirmDestructiveAction",
2426
+ "runScenario",
2427
+ "runMethod",
2428
+ "browserCommand",
2429
+ "screenshot"
2103
2430
  ]);
2104
2431
  function createAgentState() {
2105
2432
  return { messages: [] };
@@ -2205,6 +2532,9 @@ async function runTurn(params) {
2205
2532
  }
2206
2533
  if (transform) {
2207
2534
  const result = await transform(partial);
2535
+ if (result === null) {
2536
+ return;
2537
+ }
2208
2538
  log.debug("Streaming content tool: emitting tool_input_delta", {
2209
2539
  id,
2210
2540
  name,
@@ -2276,7 +2606,7 @@ async function runTurn(params) {
2276
2606
  const tool = getToolByName(event.name);
2277
2607
  const wasStreamed = acc?.started ?? false;
2278
2608
  const isInputStreaming = !!tool?.streaming?.partialInput;
2279
- log.debug("Received tool_use", {
2609
+ log.info("Tool call received", {
2280
2610
  id: event.id,
2281
2611
  name: event.name,
2282
2612
  wasStreamed,
@@ -2346,16 +2676,23 @@ async function runTurn(params) {
2346
2676
  let result;
2347
2677
  if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
2348
2678
  saveSession(state);
2349
- log.debug("Waiting for external tool result", {
2679
+ log.info("Waiting for external tool result", {
2350
2680
  name: tc.name,
2351
2681
  id: tc.id
2352
2682
  });
2353
2683
  result = await resolveExternalTool(tc.id, tc.name, tc.input);
2354
2684
  } else {
2355
- result = await executeTool(tc.name, tc.input);
2685
+ result = await executeTool(tc.name, tc.input, {
2686
+ apiConfig,
2687
+ model,
2688
+ signal,
2689
+ onEvent,
2690
+ resolveExternalTool,
2691
+ toolCallId: tc.id
2692
+ });
2356
2693
  }
2357
2694
  const isError = result.startsWith("Error");
2358
- log.debug("Tool completed", {
2695
+ log.info("Tool completed", {
2359
2696
  name: tc.name,
2360
2697
  elapsed: `${Date.now() - toolStart}ms`,
2361
2698
  isError,
@@ -2399,10 +2736,10 @@ async function runTurn(params) {
2399
2736
  }
2400
2737
 
2401
2738
  // src/headless.ts
2402
- var BASE_DIR = import.meta.dirname ?? path7.dirname(new URL(import.meta.url).pathname);
2403
- var ACTIONS_DIR = path7.join(BASE_DIR, "actions");
2739
+ var BASE_DIR = import.meta.dirname ?? path8.dirname(new URL(import.meta.url).pathname);
2740
+ var ACTIONS_DIR = path8.join(BASE_DIR, "actions");
2404
2741
  function loadActionPrompt(name) {
2405
- return fs14.readFileSync(path7.join(ACTIONS_DIR, `${name}.md`), "utf-8").trim();
2742
+ return fs15.readFileSync(path8.join(ACTIONS_DIR, `${name}.md`), "utf-8").trim();
2406
2743
  }
2407
2744
  function emit(event, data) {
2408
2745
  process.stdout.write(JSON.stringify({ event, ...data }) + "\n");
@@ -2435,20 +2772,32 @@ async function startHeadless(opts = {}) {
2435
2772
  function onEvent(e) {
2436
2773
  switch (e.type) {
2437
2774
  case "text":
2438
- emit("text", { text: e.text });
2775
+ emit("text", {
2776
+ text: e.text,
2777
+ ...e.parentToolId && { parentToolId: e.parentToolId }
2778
+ });
2439
2779
  break;
2440
2780
  case "thinking":
2441
- emit("thinking", { text: e.text });
2781
+ emit("thinking", {
2782
+ text: e.text,
2783
+ ...e.parentToolId && { parentToolId: e.parentToolId }
2784
+ });
2442
2785
  break;
2443
2786
  case "tool_input_delta":
2444
- emit("tool_input_delta", { id: e.id, name: e.name, result: e.result });
2787
+ emit("tool_input_delta", {
2788
+ id: e.id,
2789
+ name: e.name,
2790
+ result: e.result,
2791
+ ...e.parentToolId && { parentToolId: e.parentToolId }
2792
+ });
2445
2793
  break;
2446
2794
  case "tool_start":
2447
2795
  emit("tool_start", {
2448
2796
  id: e.id,
2449
2797
  name: e.name,
2450
2798
  input: e.input,
2451
- ...e.partial && { partial: true }
2799
+ ...e.partial && { partial: true },
2800
+ ...e.parentToolId && { parentToolId: e.parentToolId }
2452
2801
  });
2453
2802
  break;
2454
2803
  case "tool_done":
@@ -2456,7 +2805,8 @@ async function startHeadless(opts = {}) {
2456
2805
  id: e.id,
2457
2806
  name: e.name,
2458
2807
  result: e.result,
2459
- isError: e.isError
2808
+ isError: e.isError,
2809
+ ...e.parentToolId && { parentToolId: e.parentToolId }
2460
2810
  });
2461
2811
  break;
2462
2812
  case "turn_started":