@lazyingart/agintiflow 0.20.172 → 0.20.173

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.172",
3
+ "version": "0.20.173",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
6
6
  "license": "Apache-2.0",
package/public/app.js CHANGED
@@ -1782,6 +1782,131 @@ function renderPermissionApproval(entry) {
1782
1782
  `;
1783
1783
  }
1784
1784
 
1785
+ function renderPlanEvent(entry) {
1786
+ const data = entry.data || {};
1787
+ const plan = data.plan || entry.content || "";
1788
+ return `
1789
+ <article class="event-card event-plan">
1790
+ <div class="event-card-meta">
1791
+ <span>plan</span>
1792
+ <span>${entry.at ? escapeHtml(new Date(entry.at).toLocaleString()) : ""}</span>
1793
+ </div>
1794
+ <div class="markdown-body">${renderMarkdown(plan)}</div>
1795
+ </article>
1796
+ `;
1797
+ }
1798
+
1799
+ function renderWorkspaceChangeEvent(entry) {
1800
+ const data = entry.data || {};
1801
+ const toolName = data.toolName || data.action || "file.changed";
1802
+ const isPatch = toolName === "apply_patch" || String(toolName).startsWith("apply_patch");
1803
+ const diff = String(data.diff || "");
1804
+ const maxDiffChars = 9000;
1805
+ const diffBlock = diff
1806
+ ? `<pre class="change-diff event-diff">${renderDiffHtml(diff, maxDiffChars)}</pre>${
1807
+ diff.length > maxDiffChars ? `<div class="event-fold-note">... diff truncated for browser preview</div>` : ""
1808
+ }`
1809
+ : "";
1810
+ const hashes =
1811
+ data.beforeHash || data.afterHash
1812
+ ? `<div class="event-card-meta"><span>before=${escapeHtml((data.beforeHash || "new").slice(0, 10))}</span><span>after=${escapeHtml((data.afterHash || "").slice(0, 10))}</span></div>`
1813
+ : "";
1814
+ return `
1815
+ <article class="event-card ${isPatch ? "event-patch" : "event-write"}">
1816
+ <div class="event-card-meta">
1817
+ <span>${escapeHtml(isPatch ? "patch" : "write")}</span>
1818
+ <span>${escapeHtml(toolName)}</span>
1819
+ <span>${entry.at ? escapeHtml(new Date(entry.at).toLocaleString()) : ""}</span>
1820
+ </div>
1821
+ <strong class="event-path">${escapeHtml(data.path || entry.content || "")}</strong>
1822
+ ${data.created ? `<div class="event-fold-note">created</div>` : ""}
1823
+ ${hashes}
1824
+ ${diffBlock || `<div class="event-fold-note">No diff preview recorded for this file event.</div>`}
1825
+ </article>
1826
+ `;
1827
+ }
1828
+
1829
+ function renderToolEvent(entry) {
1830
+ const data = entry.data || {};
1831
+ const message = entry.message || entry.eventType || "";
1832
+ const failed = message === "tool.failed" || data.ok === false || data.blocked || data.error;
1833
+ const skipped = message === "tool.skipped";
1834
+ const toolName = data.toolName || "tool";
1835
+ const args = data.args || {};
1836
+ const argPreview =
1837
+ toolName === "run_command" && args.command
1838
+ ? args.command
1839
+ : args.path || args.url || args.query || args.q || (Object.keys(args).length ? JSON.stringify(args) : "");
1840
+ const stdout = data.stdout ? outputPreviewText(data.stdout) : null;
1841
+ const stderr = data.stderr ? outputPreviewText(data.stderr) : null;
1842
+ return `
1843
+ <article class="event-card ${failed ? "event-failed" : skipped ? "event-muted" : "event-tool"}">
1844
+ <div class="event-card-meta">
1845
+ <span>${escapeHtml(failed ? "tool failed" : skipped ? "tool skipped" : message === "tool.started" ? "tool" : "tool done")}</span>
1846
+ <span>${entry.at ? escapeHtml(new Date(entry.at).toLocaleString()) : ""}</span>
1847
+ </div>
1848
+ <strong class="event-path">${escapeHtml(toolName)}</strong>
1849
+ ${argPreview ? `<code class="event-inline-code">${escapeHtml(argPreview)}</code>` : ""}
1850
+ ${data.error || data.reason ? `<div class="event-fold-note">${escapeHtml(data.error || data.reason)}</div>` : ""}
1851
+ ${stdout ? `<div class="log-stream-title">stdout</div><pre class="event-output">${escapeHtml(stdout.text)}</pre>` : ""}
1852
+ ${stdout?.hidden > 0 ? `<div class="event-fold-note">... ${stdout.hidden} more stdout line(s) folded</div>` : ""}
1853
+ ${stderr ? `<div class="log-stream-title">stderr</div><pre class="event-output">${escapeHtml(stderr.text)}</pre>` : ""}
1854
+ ${stderr?.hidden > 0 ? `<div class="event-fold-note">... ${stderr.hidden} more stderr line(s) folded</div>` : ""}
1855
+ </article>
1856
+ `;
1857
+ }
1858
+
1859
+ function renderStatusEvent(entry) {
1860
+ const data = entry.data || {};
1861
+ const message = entry.message || entry.eventType || "event";
1862
+ const label = entry.eventLabel || message.replace(/^[^.]+\./, "");
1863
+ const content = data.result || data.error || data.reason || entry.content || "";
1864
+ const failed = message === "session.failed";
1865
+ return `
1866
+ <article class="event-card ${failed ? "event-failed" : "event-muted"}">
1867
+ <div class="event-card-meta">
1868
+ <span>${escapeHtml(label)}</span>
1869
+ <span>${entry.at ? escapeHtml(new Date(entry.at).toLocaleString()) : ""}</span>
1870
+ </div>
1871
+ ${content ? `<div class="chat-content">${escapeHtml(content)}</div>` : ""}
1872
+ </article>
1873
+ `;
1874
+ }
1875
+
1876
+ function renderStructuredEvent(entry) {
1877
+ const message = entry.message || entry.eventType || "";
1878
+ if (message === "plan.created") return renderPlanEvent(entry);
1879
+ if (message === "command.output") return renderCommandOutputLog(entry);
1880
+ if (message === "file.changed") return renderWorkspaceChangeEvent(entry);
1881
+ if (message === "tool.blocked" && entry.data?.permissionAdvice) return renderPermissionApproval(entry);
1882
+ if (message === "tool.blocked") {
1883
+ return `
1884
+ <article class="event-card event-failed">
1885
+ <div class="event-card-meta">
1886
+ <span>permission</span>
1887
+ <span>${entry.at ? escapeHtml(new Date(entry.at).toLocaleString()) : ""}</span>
1888
+ </div>
1889
+ <strong class="event-path">${escapeHtml(entry.data?.toolName || "blocked")}</strong>
1890
+ <div class="chat-content">${escapeHtml(entry.data?.reason || entry.content || "Permission blocked.")}</div>
1891
+ </article>
1892
+ `;
1893
+ }
1894
+ if (message === "tool.started" || message === "tool.completed" || message === "tool.failed" || message === "tool.skipped") {
1895
+ return renderToolEvent(entry);
1896
+ }
1897
+ if (
1898
+ message === "budget.initialized" ||
1899
+ message === "conversation.continued" ||
1900
+ message === "conversation.queued_input_applied" ||
1901
+ message === "session.finished" ||
1902
+ message === "session.failed" ||
1903
+ message === "session.stopped"
1904
+ ) {
1905
+ return renderStatusEvent(entry);
1906
+ }
1907
+ return "";
1908
+ }
1909
+
1785
1910
  function renderLogs(run) {
1786
1911
  logsEl.dataset.mode = "active";
1787
1912
  const parts = [
@@ -1791,24 +1916,12 @@ function renderLogs(run) {
1791
1916
  ];
1792
1917
 
1793
1918
  for (const entry of run.logs || []) {
1794
- if (entry.message === "command.output") {
1795
- parts.push(renderCommandOutputLog(entry));
1796
- continue;
1797
- }
1798
- if (entry.message === "plan.created" && entry.data?.plan) {
1799
- parts.push(`
1800
- <article class="log-plan">
1801
- <div class="log-plan-title">${escapeHtml(`[${entry.at}] plan`)}</div>
1802
- <div class="markdown-body">${renderMarkdown(entry.data.plan)}</div>
1803
- </article>
1804
- `);
1919
+ const structured = renderStructuredEvent(entry);
1920
+ if (structured) {
1921
+ parts.push(structured);
1805
1922
  continue;
1806
1923
  }
1807
1924
  parts.push(`<div class="log-line">${escapeHtml(`[${entry.at}] ${entry.kind}: ${entry.message}`)}</div>`);
1808
- if (entry.message === "tool.blocked" && entry.data?.permissionAdvice) {
1809
- parts.push(renderPermissionApproval(entry));
1810
- continue;
1811
- }
1812
1925
  if (entry.data && Object.keys(entry.data).length > 0) {
1813
1926
  parts.push(`<pre class="log-json">${escapeHtml(JSON.stringify(entry.data, null, 2))}</pre>`);
1814
1927
  }
@@ -2054,14 +2167,10 @@ function renderChat(chatEntries) {
2054
2167
  chatThreadEl.innerHTML = lastChatEntries
2055
2168
  .map((entry) => {
2056
2169
  if (entry.role === "event") {
2057
- const label = entry.eventLabel || entry.eventType || "event";
2058
- const content = entry.markdown
2059
- ? `<div class="markdown-body">${renderMarkdown(entry.content)}</div>`
2060
- : escapeHtml(entry.content || "").replace(/\n/g, "<br>");
2061
- return `
2170
+ return renderStructuredEvent(entry) || `
2062
2171
  <article class="chat-item event" data-event-type="${escapeHtml(entry.eventType || "")}">
2063
- <div class="chat-meta">${escapeHtml(label)}${entry.at ? ` · ${new Date(entry.at).toLocaleString()}` : ""}</div>
2064
- <div class="chat-content">${content}</div>
2172
+ <div class="chat-meta">${escapeHtml(entry.eventLabel || entry.eventType || "event")}${entry.at ? ` · ${new Date(entry.at).toLocaleString()}` : ""}</div>
2173
+ <div class="chat-content">${escapeHtml(entry.content || "").replace(/\n/g, "<br>")}</div>
2065
2174
  </article>
2066
2175
  `;
2067
2176
  }
package/public/styles.css CHANGED
@@ -1155,23 +1155,6 @@ button.danger {
1155
1155
  color: #fecaca;
1156
1156
  }
1157
1157
 
1158
- .log-plan {
1159
- margin: 0 0 12px;
1160
- padding: 12px;
1161
- border: 1px solid rgba(94, 234, 212, 0.22);
1162
- border-radius: 14px;
1163
- background: rgba(15, 23, 42, 0.54);
1164
- }
1165
-
1166
- .log-plan-title {
1167
- margin-bottom: 8px;
1168
- color: #99f6e4;
1169
- font-size: 0.82rem;
1170
- font-weight: 800;
1171
- letter-spacing: 0.04em;
1172
- text-transform: uppercase;
1173
- }
1174
-
1175
1158
  .log-json,
1176
1159
  .log-command pre {
1177
1160
  margin: 8px 0 12px;
@@ -1242,6 +1225,126 @@ button.danger {
1242
1225
  padding: 0 12px 8px;
1243
1226
  }
1244
1227
 
1228
+ .event-card {
1229
+ width: auto;
1230
+ max-width: 100%;
1231
+ margin: 0 0 12px;
1232
+ padding: 12px;
1233
+ border: 1px solid rgba(20, 184, 166, 0.22);
1234
+ border-radius: 16px;
1235
+ background:
1236
+ radial-gradient(circle at 0% 0%, rgba(20, 184, 166, 0.14), transparent 28%),
1237
+ rgba(255, 255, 255, 0.46);
1238
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.07);
1239
+ }
1240
+
1241
+ #logs .event-card {
1242
+ background:
1243
+ radial-gradient(circle at 0% 0%, rgba(20, 184, 166, 0.1), transparent 26%),
1244
+ rgba(15, 23, 42, 0.52);
1245
+ color: #e5e7eb;
1246
+ }
1247
+
1248
+ .event-plan {
1249
+ border-color: rgba(20, 184, 166, 0.34);
1250
+ background:
1251
+ radial-gradient(circle at 0% 0%, rgba(20, 184, 166, 0.18), transparent 28%),
1252
+ linear-gradient(180deg, rgba(240, 253, 250, 0.9), rgba(255, 251, 235, 0.74));
1253
+ }
1254
+
1255
+ #logs .event-plan {
1256
+ background: rgba(15, 23, 42, 0.58);
1257
+ }
1258
+
1259
+ .event-patch {
1260
+ border-color: rgba(192, 132, 252, 0.38);
1261
+ }
1262
+
1263
+ .event-write {
1264
+ border-color: rgba(59, 130, 246, 0.28);
1265
+ }
1266
+
1267
+ .event-failed {
1268
+ border-color: rgba(248, 113, 113, 0.45);
1269
+ background: rgba(127, 29, 29, 0.12);
1270
+ }
1271
+
1272
+ #logs .event-failed {
1273
+ background: rgba(127, 29, 29, 0.3);
1274
+ }
1275
+
1276
+ .event-muted {
1277
+ border-color: rgba(148, 163, 184, 0.24);
1278
+ background: rgba(148, 163, 184, 0.1);
1279
+ }
1280
+
1281
+ .event-card-meta {
1282
+ display: flex;
1283
+ flex-wrap: wrap;
1284
+ gap: 8px;
1285
+ margin-bottom: 8px;
1286
+ color: var(--muted);
1287
+ font-size: 0.78rem;
1288
+ font-weight: 800;
1289
+ letter-spacing: 0.04em;
1290
+ text-transform: uppercase;
1291
+ }
1292
+
1293
+ #logs .event-card-meta {
1294
+ color: #99f6e4;
1295
+ }
1296
+
1297
+ .event-path {
1298
+ display: block;
1299
+ margin: 4px 0 8px;
1300
+ font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
1301
+ font-size: 0.84rem;
1302
+ overflow-wrap: anywhere;
1303
+ }
1304
+
1305
+ .event-inline-code {
1306
+ display: block;
1307
+ width: fit-content;
1308
+ max-width: 100%;
1309
+ margin: 4px 0 8px;
1310
+ padding: 6px 8px;
1311
+ border-radius: 8px;
1312
+ background: rgba(15, 23, 42, 0.08);
1313
+ color: #78350f;
1314
+ font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
1315
+ overflow-wrap: anywhere;
1316
+ white-space: pre-wrap;
1317
+ }
1318
+
1319
+ #logs .event-inline-code {
1320
+ background: rgba(255, 255, 255, 0.08);
1321
+ color: #fef3c7;
1322
+ }
1323
+
1324
+ .event-output {
1325
+ margin: 4px 0 10px;
1326
+ padding: 10px;
1327
+ border-radius: 10px;
1328
+ background: rgba(15, 23, 42, 0.84);
1329
+ color: #e5e7eb;
1330
+ white-space: pre-wrap;
1331
+ overflow: auto;
1332
+ }
1333
+
1334
+ .event-fold-note {
1335
+ margin: 4px 0 8px;
1336
+ color: var(--muted);
1337
+ font-size: 0.8rem;
1338
+ }
1339
+
1340
+ #logs .event-fold-note {
1341
+ color: #94a3b8;
1342
+ }
1343
+
1344
+ .event-diff {
1345
+ max-height: 360px;
1346
+ }
1347
+
1245
1348
  .chat-panel {
1246
1349
  display: grid;
1247
1350
  gap: 12px;
@@ -893,6 +893,9 @@ try {
893
893
  if (!latest.stdout.includes(" plan ")) {
894
894
  throw new Error("bare aginti resume did not replay saved plan/run context");
895
895
  }
896
+ if (!latest.stdout.includes(" write ") || !latest.stdout.includes("+Created by AgInTiFlow mock mode.")) {
897
+ throw new Error("bare aginti resume did not replay formatted file-change diff context");
898
+ }
896
899
  if (latest.stdout.includes("resume note=showing chat transcript only")) {
897
900
  throw new Error("bare aginti resume regressed to chat-only history");
898
901
  }
@@ -257,6 +257,11 @@ try {
257
257
  if (fileRun.status !== "finished") throw new Error(`mock file run failed: ${fileRun.error || "unknown error"}`);
258
258
  const hello = await fs.readFile(path.join(runtimeDir, "notes", "hello.md"), "utf8");
259
259
  if (!hello.includes("Created by AgInTiFlow mock mode.")) throw new Error("mock file run did not create requested path");
260
+ const fileChat = await fetchJson(`/api/sessions/${encodeURIComponent(fileRunStart.sessionId)}/chat`);
261
+ const fileChange = fileChat.timeline?.find((entry) => entry.role === "event" && entry.eventType === "file.changed");
262
+ if (!fileChange?.data?.diff || !fileChange.data.diff.includes("+Created by AgInTiFlow mock mode.")) {
263
+ throw new Error("chat timeline did not preserve structured file-change diff data");
264
+ }
260
265
 
261
266
  const safeRunStart = await fetchJson("/api/runs", {
262
267
  method: "POST",
@@ -56,7 +56,7 @@ async function waitForRunState(page, status, timeout = 20000) {
56
56
  );
57
57
  }
58
58
 
59
- async function waitForTerminalRunState(page, timeout = 30000) {
59
+ async function waitForTerminalRunState(page, timeout = 90000) {
60
60
  await page.waitForFunction(
61
61
  () => ["finished", "failed", "stopped"].includes(document.querySelector("#run-state")?.dataset.status || ""),
62
62
  null,
@@ -92,6 +92,7 @@ try {
92
92
  if (await page.locator("#run-defaults-card").evaluate((node) => node.open)) throw new Error("run defaults should start folded");
93
93
  await page.locator("#commandCwd").fill(runtimeDir.slice(0, Math.max(runtimeDir.lastIndexOf("/"), 1)));
94
94
  await page.waitForFunction(() => document.querySelectorAll("#command-cwd-suggestions option").length > 0);
95
+ await page.locator("#commandCwd").fill(runtimeDir);
95
96
  if ((await page.locator(".project-status-chip").count()) < 4) throw new Error("project folder status did not render structured chips");
96
97
  await page.locator("#run-defaults-card summary").click();
97
98
  if (await page.locator("#veniceModeToggle").isChecked()) throw new Error("Venice quick mode should default off");
@@ -143,6 +144,14 @@ try {
143
144
  await page.selectOption("#routingMode", "manual");
144
145
  await page.selectOption("#provider", "mock");
145
146
  await page.selectOption("#model", "mock-agent");
147
+ await page.selectOption("#enableScs", "off");
148
+ if (await page.locator("#aapsModeToggle").isChecked()) await page.locator("label:has(#aapsModeToggle)").click();
149
+ await page.selectOption("#taskProfile", "auto");
150
+ await page.evaluate(() => {
151
+ const input = document.querySelector("#allowShellTool");
152
+ input.checked = false;
153
+ input.dispatchEvent(new Event("change", { bubbles: true }));
154
+ });
146
155
  await page.fill("#maxSteps", "4");
147
156
  await page.selectOption("#dynamicSteps", "off");
148
157
  await page.fill("#chat-input", "Say hello from the web UI composer in one concise sentence.");
@@ -151,18 +160,24 @@ try {
151
160
  if (firstPayload.goal !== "Say hello from the web UI composer in one concise sentence.") {
152
161
  throw new Error("composer text was not used as the first run goal");
153
162
  }
154
- if (firstPayload.enableScs !== "auto" || firstPayload.taskProfile !== "aaps") {
163
+ if (firstPayload.enableScs !== "off" || firstPayload.taskProfile !== "auto") {
155
164
  throw new Error(`CLI mode payload mismatch: ${runPayloads.at(-1) || ""}`);
156
165
  }
157
166
  if (firstPayload.dynamicSteps !== "off") {
158
167
  throw new Error(`dynamic steps payload mismatch: ${runPayloads.at(-1) || ""}`);
159
168
  }
169
+ if (firstPayload.allowShellTool !== false) {
170
+ throw new Error(`shell tool should be disabled for deterministic renderer smoke: ${runPayloads.at(-1) || ""}`);
171
+ }
160
172
  if (firstPayload.veniceMode !== false || firstPayload.routeProvider !== "deepseek" || firstPayload.mainProvider !== "deepseek") {
161
173
  throw new Error(`default route/main payload mismatch after Venice off: ${runPayloads.at(-1) || ""}`);
162
174
  }
163
175
  await waitForRunState(page, "running");
176
+ await page.waitForSelector(".event-plan", { timeout: 12000 });
177
+ if (!(await page.locator(".event-plan .markdown-body").first().innerText()).trim()) {
178
+ throw new Error("web run log did not render the plan as a formatted event card");
179
+ }
164
180
  if (!(await page.locator("#stop-run").isVisible())) throw new Error("stop button did not appear while run was active");
165
- await page.click("#stop-run");
166
181
  await waitForTerminalRunState(page);
167
182
  if (await page.locator("#stop-run").isVisible()) throw new Error("stop button stayed visible after terminal run state");
168
183
  if (!(await page.locator(".toast").first().isVisible().catch(() => false))) {
@@ -173,6 +188,27 @@ try {
173
188
  await waitForRunState(page, "idle");
174
189
  const resetSubmit = await page.locator("#chat-submit").innerText();
175
190
  if (!/start new run/i.test(resetSubmit)) throw new Error("new session did not reset composer to start mode");
191
+ if (await page.locator("#aapsModeToggle").isChecked()) await page.locator("label:has(#aapsModeToggle)").click();
192
+ await page.selectOption("#taskProfile", "code");
193
+ await page.selectOption("#enableScs", "off");
194
+ await page.fill("#chat-input", "Create notes/web-ui-format.md with a short formatted event smoke message.");
195
+ await page.click("#chat-submit");
196
+ await waitForTerminalRunState(page);
197
+ try {
198
+ await page.waitForFunction(
199
+ () =>
200
+ [...document.querySelectorAll(".event-write .change-diff")].some((node) =>
201
+ (node.textContent || "").includes("+Created by AgInTiFlow mock mode.")
202
+ ),
203
+ null,
204
+ { timeout: 30000 }
205
+ );
206
+ } catch (error) {
207
+ const state = await page.locator("#run-state").evaluate((node) => node.dataset.status || "").catch(() => "unknown");
208
+ const logsTail = (await page.locator("#logs").innerText().catch(() => "")).slice(-1200);
209
+ const chatTail = (await page.locator("#chat-thread").innerText().catch(() => "")).slice(-1200);
210
+ throw new Error(`web UI did not render formatted file-change diff event; state=${state}\nlogs tail:\n${logsTail}\nchat tail:\n${chatTail}`);
211
+ }
176
212
 
177
213
  await page.click("#open-settings");
178
214
  await page.waitForSelector("#settings-modal[open]");
@@ -208,6 +244,8 @@ try {
208
244
  "running-status-toast",
209
245
  "terminal-stop-button-hidden",
210
246
  "new-session-resets-scope",
247
+ "formatted-plan-event-card",
248
+ "formatted-file-diff-event-card",
211
249
  "settings-provider-dropdowns",
212
250
  "settings-wrapper-dropdowns",
213
251
  ],
@@ -51,6 +51,26 @@ async function killPort(port) {
51
51
  }
52
52
  }
53
53
 
54
+ async function stopChild(child) {
55
+ if (!child || child.exitCode !== null) return;
56
+ try {
57
+ child.stdin?.write("/exit\n");
58
+ } catch {
59
+ // The CLI may have closed stdin after the final prompt; fall back to process signals.
60
+ }
61
+ const exited = await Promise.race([
62
+ new Promise((resolve) => child.once("exit", () => resolve(true))),
63
+ delay(2000).then(() => false),
64
+ ]);
65
+ if (exited || child.exitCode !== null) return;
66
+ child.kill("SIGTERM");
67
+ const terminated = await Promise.race([
68
+ new Promise((resolve) => child.once("exit", () => resolve(true))),
69
+ delay(2000).then(() => false),
70
+ ]);
71
+ if (!terminated && child.exitCode === null) child.kill("SIGKILL");
72
+ }
73
+
54
74
  async function runCase({ port, env = {}, expectHeader, label }) {
55
75
  const output = { stdout: "", stderr: "" };
56
76
  const child = spawn(process.execPath, [path.join(repoRoot, "bin/aginti-cli.js"), "chat", "--provider", "mock", "--routing", "manual", "--port", String(port)], {
@@ -109,8 +129,9 @@ async function runCase({ port, env = {}, expectHeader, label }) {
109
129
  child.stdin.write(`/webapp ${active.port}\n`);
110
130
  await waitFor(() => latestWebappEvent(output.stdout, "started|reused")?.port === active.port, child, `${label} /webapp restart after stop`, output);
111
131
  } finally {
112
- child.kill("SIGTERM");
132
+ await stopChild(child);
113
133
  await killPort(port);
134
+ await delay(250);
114
135
  }
115
136
  }
116
137
 
@@ -133,5 +154,6 @@ try {
133
154
  } finally {
134
155
  await killPort(autoPort);
135
156
  await killPort(manualPort);
136
- await fs.rm(runtimeDir, { recursive: true, force: true });
157
+ await delay(250);
158
+ await fs.rm(runtimeDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
137
159
  }
@@ -2275,6 +2275,10 @@ function printResumeEvent(event = {}) {
2275
2275
  }
2276
2276
  if (type === "tool.completed" || type === "tool.failed" || type === "tool.skipped") {
2277
2277
  const failed = type === "tool.failed" || data.ok === false || data.blocked || data.error;
2278
+ if (data.diff) {
2279
+ printWorkspaceChange(data);
2280
+ return true;
2281
+ }
2278
2282
  if (data.toolName === "run_command") {
2279
2283
  printCommandOutputLog({
2280
2284
  command: data.args?.command || data.command || "",
package/web.js CHANGED
@@ -129,6 +129,7 @@ function timelineEntryForEvent(event = {}) {
129
129
  role: "event",
130
130
  eventType: type,
131
131
  eventLabel: "plan",
132
+ data,
132
133
  content: data.plan,
133
134
  markdown: true,
134
135
  at,
@@ -139,6 +140,7 @@ function timelineEntryForEvent(event = {}) {
139
140
  role: "event",
140
141
  eventType: type,
141
142
  eventLabel: "tool",
143
+ data,
142
144
  content: toolTimelineSummary(data),
143
145
  at,
144
146
  };
@@ -151,6 +153,7 @@ function timelineEntryForEvent(event = {}) {
151
153
  role: "event",
152
154
  eventType: type,
153
155
  eventLabel: failed ? "tool failed" : "tool done",
156
+ data,
154
157
  content: `${toolTimelineSummary(data)}${stdout}${stderr}`,
155
158
  at,
156
159
  };
@@ -160,6 +163,7 @@ function timelineEntryForEvent(event = {}) {
160
163
  role: "event",
161
164
  eventType: type,
162
165
  eventLabel: "permission",
166
+ data,
163
167
  content: data.permissionAdvice?.summary || data.reason || data.toolName || "Permission blocked.",
164
168
  at,
165
169
  };
@@ -169,6 +173,7 @@ function timelineEntryForEvent(event = {}) {
169
173
  role: "event",
170
174
  eventType: type,
171
175
  eventLabel: data.toolName === "apply_patch" ? "patch" : "write",
176
+ data,
172
177
  content: [data.toolName || data.action || "file.changed", data.path || "", data.created ? "created" : "updated"]
173
178
  .filter(Boolean)
174
179
  .join(" "),
@@ -180,6 +185,7 @@ function timelineEntryForEvent(event = {}) {
180
185
  role: "event",
181
186
  eventType: type,
182
187
  eventLabel: "budget",
188
+ data,
183
189
  content: `${data.currentMaxSteps || data.initialMaxSteps || data.maxSteps || "unknown"} steps`,
184
190
  at,
185
191
  };
@@ -189,6 +195,7 @@ function timelineEntryForEvent(event = {}) {
189
195
  role: "event",
190
196
  eventType: type,
191
197
  eventLabel: "continued",
198
+ data,
192
199
  content: data.prompt || "",
193
200
  at,
194
201
  };
@@ -198,6 +205,7 @@ function timelineEntryForEvent(event = {}) {
198
205
  role: "event",
199
206
  eventType: type,
200
207
  eventLabel: "queued input",
208
+ data,
201
209
  content: data.priority === "asap" ? "ASAP queued input applied" : "Queued input applied",
202
210
  at,
203
211
  };
@@ -207,6 +215,7 @@ function timelineEntryForEvent(event = {}) {
207
215
  role: "event",
208
216
  eventType: type,
209
217
  eventLabel: type.replace("session.", ""),
218
+ data,
210
219
  content: data.result || data.error || data.reason || type,
211
220
  at,
212
221
  };