@lazyingart/agintiflow 0.20.172 → 0.20.174

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.174",
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,33 +1782,234 @@ 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 embeddedWorkspaceChangesFromText(value = "") {
1800
+ const lines = String(value || "").split(/\r?\n/);
1801
+ const changes = [];
1802
+ let currentTool = "";
1803
+
1804
+ for (let index = 0; index < lines.length; index += 1) {
1805
+ const toolMatch = lines[index].match(/^\s*Tool:\s*(.+?)\s*$/i);
1806
+ if (toolMatch) {
1807
+ currentTool = toolMatch[1].trim();
1808
+ continue;
1809
+ }
1810
+
1811
+ const pathMatch = lines[index].match(/^\s*Path:\s*(.+?)\s*$/i);
1812
+ if (!pathMatch) continue;
1813
+
1814
+ let cursor = index + 1;
1815
+ while (cursor < lines.length && !/^\s*Diff:\s*$/i.test(lines[cursor]) && !/^\s*(?:Tool|Path):\s*/i.test(lines[cursor])) {
1816
+ cursor += 1;
1817
+ }
1818
+ if (!/^\s*Diff:\s*$/i.test(lines[cursor] || "")) continue;
1819
+
1820
+ const diffLines = [];
1821
+ cursor += 1;
1822
+ while (cursor < lines.length) {
1823
+ if (diffLines.length && /^\s*(?:Tool|Path):\s*/i.test(lines[cursor])) break;
1824
+ if (
1825
+ diffLines.length &&
1826
+ /^\s*(?:Output:|No command output|Blocked by guardrail\.|Mock run complete\.)/i.test(lines[cursor]) &&
1827
+ !/^[-+@ ]/.test(lines[cursor])
1828
+ ) {
1829
+ break;
1830
+ }
1831
+ diffLines.push(lines[cursor]);
1832
+ cursor += 1;
1833
+ }
1834
+ while (diffLines.length && !diffLines[diffLines.length - 1].trim()) diffLines.pop();
1835
+ if (diffLines.length) {
1836
+ changes.push({
1837
+ toolName: currentTool || "write_file",
1838
+ path: pathMatch[1].trim(),
1839
+ diff: diffLines.join("\n"),
1840
+ });
1841
+ index = Math.max(index, cursor - 1);
1842
+ }
1843
+ }
1844
+
1845
+ return changes;
1846
+ }
1847
+
1848
+ function embeddedWorkspaceSummary(value = "") {
1849
+ const summary = [];
1850
+ let skippingDiff = false;
1851
+ for (const line of String(value || "").split(/\r?\n/)) {
1852
+ if (/^\s*Diff:\s*$/i.test(line)) {
1853
+ skippingDiff = true;
1854
+ continue;
1855
+ }
1856
+ if (skippingDiff && /^\s*(?:Tool|Path):\s*/i.test(line)) skippingDiff = false;
1857
+ if (skippingDiff) continue;
1858
+ if (/^\s*Path:\s*/i.test(line)) continue;
1859
+ summary.push(line);
1860
+ }
1861
+ return summary.join("\n").replace(/\n{3,}/g, "\n\n").trim();
1862
+ }
1863
+
1864
+ function renderEmbeddedWorkspaceResult(value = "", entry = {}) {
1865
+ const changes = embeddedWorkspaceChangesFromText(value);
1866
+ if (changes.length === 0) return "";
1867
+ const summary = embeddedWorkspaceSummary(value);
1868
+ return `
1869
+ ${summary ? `<div class="event-result-summary markdown-body">${renderMarkdown(summary)}</div>` : ""}
1870
+ <div class="embedded-change-list">
1871
+ ${changes.map((change) => renderWorkspaceChangeEvent({ data: change, at: entry.at, content: change.path })).join("")}
1872
+ </div>
1873
+ `;
1874
+ }
1875
+
1876
+ function renderWorkspaceChangeEvent(entry) {
1877
+ const data = entry.data || {};
1878
+ const toolName = data.toolName || data.action || "file.changed";
1879
+ const isPatch = toolName === "apply_patch" || String(toolName).startsWith("apply_patch");
1880
+ const diff = String(data.diff || "");
1881
+ const maxDiffChars = 9000;
1882
+ const diffBlock = diff
1883
+ ? `<pre class="change-diff event-diff">${renderDiffHtml(diff, maxDiffChars)}</pre>${
1884
+ diff.length > maxDiffChars ? `<div class="event-fold-note">... diff truncated for browser preview</div>` : ""
1885
+ }`
1886
+ : "";
1887
+ const hashes =
1888
+ data.beforeHash || data.afterHash
1889
+ ? `<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>`
1890
+ : "";
1891
+ return `
1892
+ <article class="event-card ${isPatch ? "event-patch" : "event-write"}">
1893
+ <div class="event-card-meta">
1894
+ <span>${escapeHtml(isPatch ? "patch" : "write")}</span>
1895
+ <span>${escapeHtml(toolName)}</span>
1896
+ <span>${entry.at ? escapeHtml(new Date(entry.at).toLocaleString()) : ""}</span>
1897
+ </div>
1898
+ <strong class="event-path">${escapeHtml(data.path || entry.content || "")}</strong>
1899
+ ${data.created ? `<div class="event-fold-note">created</div>` : ""}
1900
+ ${hashes}
1901
+ ${diffBlock || `<div class="event-fold-note">No diff preview recorded for this file event.</div>`}
1902
+ </article>
1903
+ `;
1904
+ }
1905
+
1906
+ function renderToolEvent(entry) {
1907
+ const data = entry.data || {};
1908
+ const message = entry.message || entry.eventType || "";
1909
+ const failed = message === "tool.failed" || data.ok === false || data.blocked || data.error;
1910
+ const skipped = message === "tool.skipped";
1911
+ const toolName = data.toolName || "tool";
1912
+ const args = data.args || {};
1913
+ const resultText = String(data.result || args.result || "");
1914
+ const embeddedResult = resultText ? renderEmbeddedWorkspaceResult(resultText, entry) : "";
1915
+ const argPreview =
1916
+ toolName === "finish"
1917
+ ? ""
1918
+ : toolName === "run_command" && args.command
1919
+ ? args.command
1920
+ : args.path || args.url || args.query || args.q || (Object.keys(args).length ? JSON.stringify(args) : "");
1921
+ const stdout = data.stdout ? outputPreviewText(data.stdout) : null;
1922
+ const stderr = data.stderr ? outputPreviewText(data.stderr) : null;
1923
+ return `
1924
+ <article class="event-card ${failed ? "event-failed" : skipped ? "event-muted" : toolName === "finish" ? "event-finish" : "event-tool"}">
1925
+ <div class="event-card-meta">
1926
+ <span>${escapeHtml(failed ? "tool failed" : skipped ? "tool skipped" : message === "tool.started" ? "tool" : "tool done")}</span>
1927
+ <span>${entry.at ? escapeHtml(new Date(entry.at).toLocaleString()) : ""}</span>
1928
+ </div>
1929
+ <strong class="event-path">${escapeHtml(toolName)}</strong>
1930
+ ${argPreview ? `<code class="event-inline-code">${escapeHtml(argPreview)}</code>` : ""}
1931
+ ${data.error || data.reason ? `<div class="event-fold-note">${escapeHtml(data.error || data.reason)}</div>` : ""}
1932
+ ${embeddedResult || (toolName === "finish" && resultText ? `<div class="markdown-body">${renderMarkdown(resultText)}</div>` : "")}
1933
+ ${stdout ? `<div class="log-stream-title">stdout</div><pre class="event-output">${escapeHtml(stdout.text)}</pre>` : ""}
1934
+ ${stdout?.hidden > 0 ? `<div class="event-fold-note">... ${stdout.hidden} more stdout line(s) folded</div>` : ""}
1935
+ ${stderr ? `<div class="log-stream-title">stderr</div><pre class="event-output">${escapeHtml(stderr.text)}</pre>` : ""}
1936
+ ${stderr?.hidden > 0 ? `<div class="event-fold-note">... ${stderr.hidden} more stderr line(s) folded</div>` : ""}
1937
+ </article>
1938
+ `;
1939
+ }
1940
+
1941
+ function renderStatusEvent(entry) {
1942
+ const data = entry.data || {};
1943
+ const message = entry.message || entry.eventType || "event";
1944
+ const label = entry.eventLabel || message.replace(/^[^.]+\./, "");
1945
+ const content = data.result || data.error || data.reason || entry.content || "";
1946
+ const failed = message === "session.failed";
1947
+ const embeddedResult = content ? renderEmbeddedWorkspaceResult(content, entry) : "";
1948
+ return `
1949
+ <article class="event-card ${failed ? "event-failed" : message === "session.finished" && embeddedResult ? "event-finish" : "event-muted"}">
1950
+ <div class="event-card-meta">
1951
+ <span>${escapeHtml(label)}</span>
1952
+ <span>${entry.at ? escapeHtml(new Date(entry.at).toLocaleString()) : ""}</span>
1953
+ </div>
1954
+ ${embeddedResult || (content ? `<div class="chat-content">${escapeHtml(content)}</div>` : "")}
1955
+ </article>
1956
+ `;
1957
+ }
1958
+
1959
+ function renderStructuredEvent(entry) {
1960
+ const message = entry.message || entry.eventType || "";
1961
+ if (message === "plan.created") return renderPlanEvent(entry);
1962
+ if (message === "command.output") return renderCommandOutputLog(entry);
1963
+ if (message === "file.changed") return renderWorkspaceChangeEvent(entry);
1964
+ if (message === "tool.blocked" && entry.data?.permissionAdvice) return renderPermissionApproval(entry);
1965
+ if (message === "tool.blocked") {
1966
+ return `
1967
+ <article class="event-card event-failed">
1968
+ <div class="event-card-meta">
1969
+ <span>permission</span>
1970
+ <span>${entry.at ? escapeHtml(new Date(entry.at).toLocaleString()) : ""}</span>
1971
+ </div>
1972
+ <strong class="event-path">${escapeHtml(entry.data?.toolName || "blocked")}</strong>
1973
+ <div class="chat-content">${escapeHtml(entry.data?.reason || entry.content || "Permission blocked.")}</div>
1974
+ </article>
1975
+ `;
1976
+ }
1977
+ if (message === "tool.started" || message === "tool.completed" || message === "tool.failed" || message === "tool.skipped") {
1978
+ return renderToolEvent(entry);
1979
+ }
1980
+ if (
1981
+ message === "budget.initialized" ||
1982
+ message === "conversation.continued" ||
1983
+ message === "conversation.queued_input_applied" ||
1984
+ message === "session.finished" ||
1985
+ message === "session.failed" ||
1986
+ message === "session.stopped"
1987
+ ) {
1988
+ return renderStatusEvent(entry);
1989
+ }
1990
+ return "";
1991
+ }
1992
+
1785
1993
  function renderLogs(run) {
1786
1994
  logsEl.dataset.mode = "active";
1995
+ const structuredRunResult = run.result ? renderEmbeddedWorkspaceResult(run.result, { at: run.endedAt || run.updatedAt || "" }) : "";
1787
1996
  const parts = [
1788
1997
  `<div class="log-line">${escapeHtml(`status=${run.status} session=${run.sessionId} provider=${run.provider} model=${run.model}`)}</div>`,
1789
- run.result ? `<div class="log-line">${escapeHtml(`result=${run.result}`)}</div>` : "",
1998
+ structuredRunResult
1999
+ ? `<article class="event-card event-finish"><div class="event-card-meta"><span>result</span></div>${structuredRunResult}</article>`
2000
+ : run.result
2001
+ ? `<div class="log-line">${escapeHtml(`result=${run.result}`)}</div>`
2002
+ : "",
1790
2003
  run.error ? `<div class="log-line error">${escapeHtml(`error=${run.error}`)}</div>` : "",
1791
2004
  ];
1792
2005
 
1793
2006
  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
- `);
2007
+ const structured = renderStructuredEvent(entry);
2008
+ if (structured) {
2009
+ parts.push(structured);
1805
2010
  continue;
1806
2011
  }
1807
2012
  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
2013
  if (entry.data && Object.keys(entry.data).length > 0) {
1813
2014
  parts.push(`<pre class="log-json">${escapeHtml(JSON.stringify(entry.data, null, 2))}</pre>`);
1814
2015
  }
@@ -2054,17 +2255,35 @@ function renderChat(chatEntries) {
2054
2255
  chatThreadEl.innerHTML = lastChatEntries
2055
2256
  .map((entry) => {
2056
2257
  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 `
2258
+ return renderStructuredEvent(entry) || `
2062
2259
  <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>
2260
+ <div class="chat-meta">${escapeHtml(entry.eventLabel || entry.eventType || "event")}${entry.at ? ` · ${new Date(entry.at).toLocaleString()}` : ""}</div>
2261
+ <div class="chat-content">${escapeHtml(entry.content || "").replace(/\n/g, "<br>")}</div>
2065
2262
  </article>
2066
2263
  `;
2067
2264
  }
2265
+ if (entry.role === "tool") {
2266
+ try {
2267
+ const parsed = JSON.parse(entry.content || "{}");
2268
+ const toolName = parsed.toolName || "tool";
2269
+ return renderToolEvent({
2270
+ role: "event",
2271
+ eventType: parsed.ok === false ? "tool.failed" : "tool.completed",
2272
+ message: parsed.ok === false ? "tool.failed" : "tool.completed",
2273
+ eventLabel: parsed.ok === false ? "tool failed" : "tool done",
2274
+ data: parsed,
2275
+ content: parsed.result || toolName,
2276
+ at: entry.at,
2277
+ });
2278
+ } catch {
2279
+ return `
2280
+ <article class="event-card event-tool">
2281
+ <div class="event-card-meta">tool${entry.at ? ` · ${new Date(entry.at).toLocaleString()}` : ""}</div>
2282
+ <pre class="event-output">${escapeHtml(entry.content || "")}</pre>
2283
+ </article>
2284
+ `;
2285
+ }
2286
+ }
2068
2287
  const role = entry.role === "assistant" ? "assistant" : "user";
2069
2288
  const label = role === "assistant" ? t("assistantLabel") : t("youLabel");
2070
2289
  const content =
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,160 @@ 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-finish {
1268
+ border-color: rgba(16, 185, 129, 0.36);
1269
+ background:
1270
+ radial-gradient(circle at 0% 0%, rgba(16, 185, 129, 0.14), transparent 30%),
1271
+ rgba(236, 253, 245, 0.46);
1272
+ }
1273
+
1274
+ #logs .event-finish {
1275
+ background:
1276
+ radial-gradient(circle at 0% 0%, rgba(16, 185, 129, 0.16), transparent 30%),
1277
+ rgba(6, 78, 59, 0.28);
1278
+ }
1279
+
1280
+ .event-failed {
1281
+ border-color: rgba(248, 113, 113, 0.45);
1282
+ background: rgba(127, 29, 29, 0.12);
1283
+ }
1284
+
1285
+ #logs .event-failed {
1286
+ background: rgba(127, 29, 29, 0.3);
1287
+ }
1288
+
1289
+ .event-muted {
1290
+ border-color: rgba(148, 163, 184, 0.24);
1291
+ background: rgba(148, 163, 184, 0.1);
1292
+ }
1293
+
1294
+ .event-card-meta {
1295
+ display: flex;
1296
+ flex-wrap: wrap;
1297
+ gap: 8px;
1298
+ margin-bottom: 8px;
1299
+ color: var(--muted);
1300
+ font-size: 0.78rem;
1301
+ font-weight: 800;
1302
+ letter-spacing: 0.04em;
1303
+ text-transform: uppercase;
1304
+ }
1305
+
1306
+ #logs .event-card-meta {
1307
+ color: #99f6e4;
1308
+ }
1309
+
1310
+ .event-path {
1311
+ display: block;
1312
+ margin: 4px 0 8px;
1313
+ font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
1314
+ font-size: 0.84rem;
1315
+ overflow-wrap: anywhere;
1316
+ }
1317
+
1318
+ .event-inline-code {
1319
+ display: block;
1320
+ width: fit-content;
1321
+ max-width: 100%;
1322
+ margin: 4px 0 8px;
1323
+ padding: 6px 8px;
1324
+ border-radius: 8px;
1325
+ background: rgba(15, 23, 42, 0.08);
1326
+ color: #78350f;
1327
+ font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
1328
+ overflow-wrap: anywhere;
1329
+ white-space: pre-wrap;
1330
+ }
1331
+
1332
+ #logs .event-inline-code {
1333
+ background: rgba(255, 255, 255, 0.08);
1334
+ color: #fef3c7;
1335
+ }
1336
+
1337
+ .event-output {
1338
+ margin: 4px 0 10px;
1339
+ padding: 10px;
1340
+ border-radius: 10px;
1341
+ background: rgba(15, 23, 42, 0.84);
1342
+ color: #e5e7eb;
1343
+ white-space: pre-wrap;
1344
+ overflow: auto;
1345
+ }
1346
+
1347
+ .event-fold-note {
1348
+ margin: 4px 0 8px;
1349
+ color: var(--muted);
1350
+ font-size: 0.8rem;
1351
+ }
1352
+
1353
+ .event-result-summary {
1354
+ margin: 4px 0 12px;
1355
+ padding: 10px 12px;
1356
+ border-radius: 12px;
1357
+ background: rgba(15, 118, 110, 0.08);
1358
+ }
1359
+
1360
+ #logs .event-result-summary {
1361
+ background: rgba(255, 255, 255, 0.08);
1362
+ }
1363
+
1364
+ .embedded-change-list {
1365
+ display: grid;
1366
+ gap: 8px;
1367
+ }
1368
+
1369
+ .embedded-change-list > .event-card {
1370
+ margin: 0;
1371
+ box-shadow: none;
1372
+ }
1373
+
1374
+ #logs .event-fold-note {
1375
+ color: #94a3b8;
1376
+ }
1377
+
1378
+ .event-diff {
1379
+ max-height: 360px;
1380
+ }
1381
+
1245
1382
  .chat-panel {
1246
1383
  display: grid;
1247
1384
  gap: 12px;
@@ -893,6 +893,12 @@ 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
+ }
899
+ if (!/\bfinish\b/.test(latest.stdout) || latest.stdout.includes('{"result":"Mock run complete')) {
900
+ throw new Error("bare aginti resume did not render finish-result diffs in formatted mode");
901
+ }
896
902
  if (latest.stdout.includes("resume note=showing chat transcript only")) {
897
903
  throw new Error("bare aginti resume regressed to chat-only history");
898
904
  }
@@ -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,42 @@ 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
+ }
212
+ try {
213
+ await page.waitForFunction(
214
+ () =>
215
+ [...document.querySelectorAll(".event-finish .change-diff")].some((node) =>
216
+ (node.textContent || "").includes("+Created by AgInTiFlow mock mode.")
217
+ ),
218
+ null,
219
+ { timeout: 30000 }
220
+ );
221
+ } catch (error) {
222
+ const state = await page.locator("#run-state").evaluate((node) => node.dataset.status || "").catch(() => "unknown");
223
+ const logsTail = (await page.locator("#logs").innerText().catch(() => "")).slice(-1200);
224
+ const chatTail = (await page.locator("#chat-thread").innerText().catch(() => "")).slice(-1200);
225
+ throw new Error(`web UI did not render formatted finish-result diff event; state=${state}\nlogs tail:\n${logsTail}\nchat tail:\n${chatTail}`);
226
+ }
176
227
 
177
228
  await page.click("#open-settings");
178
229
  await page.waitForSelector("#settings-modal[open]");
@@ -208,6 +259,9 @@ try {
208
259
  "running-status-toast",
209
260
  "terminal-stop-button-hidden",
210
261
  "new-session-resets-scope",
262
+ "formatted-plan-event-card",
263
+ "formatted-file-diff-event-card",
264
+ "formatted-finish-result-diff-card",
211
265
  "settings-provider-dropdowns",
212
266
  "settings-wrapper-dropdowns",
213
267
  ],
@@ -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
  }
@@ -778,6 +778,71 @@ export function formatWorkspaceChange(change = {}) {
778
778
  };
779
779
  }
780
780
 
781
+ function embeddedWorkspaceChangesFromText(value = "") {
782
+ const lines = String(value || "").split(/\r?\n/);
783
+ const changes = [];
784
+ let currentTool = "";
785
+
786
+ for (let index = 0; index < lines.length; index += 1) {
787
+ const toolMatch = lines[index].match(/^\s*Tool:\s*(.+?)\s*$/i);
788
+ if (toolMatch) {
789
+ currentTool = toolMatch[1].trim();
790
+ continue;
791
+ }
792
+
793
+ const pathMatch = lines[index].match(/^\s*Path:\s*(.+?)\s*$/i);
794
+ if (!pathMatch) continue;
795
+
796
+ let cursor = index + 1;
797
+ while (cursor < lines.length && !/^\s*Diff:\s*$/i.test(lines[cursor]) && !/^\s*(?:Tool|Path):\s*/i.test(lines[cursor])) {
798
+ cursor += 1;
799
+ }
800
+ if (!/^\s*Diff:\s*$/i.test(lines[cursor] || "")) continue;
801
+
802
+ const diffLines = [];
803
+ cursor += 1;
804
+ while (cursor < lines.length) {
805
+ if (diffLines.length && /^\s*(?:Tool|Path):\s*/i.test(lines[cursor])) break;
806
+ if (
807
+ diffLines.length &&
808
+ /^\s*(?:Output:|No command output|Blocked by guardrail\.|Mock run complete\.)/i.test(lines[cursor]) &&
809
+ !/^[-+@ ]/.test(lines[cursor])
810
+ ) {
811
+ break;
812
+ }
813
+ diffLines.push(lines[cursor]);
814
+ cursor += 1;
815
+ }
816
+ while (diffLines.length && !diffLines[diffLines.length - 1].trim()) diffLines.pop();
817
+ if (diffLines.length) {
818
+ changes.push({
819
+ toolName: currentTool || "write_file",
820
+ path: pathMatch[1].trim(),
821
+ diff: diffLines.join("\n"),
822
+ });
823
+ index = Math.max(index, cursor - 1);
824
+ }
825
+ }
826
+
827
+ return changes;
828
+ }
829
+
830
+ function embeddedWorkspaceSummary(value = "") {
831
+ const summary = [];
832
+ let skippingDiff = false;
833
+ for (const line of String(value || "").split(/\r?\n/)) {
834
+ if (/^\s*Diff:\s*$/i.test(line)) {
835
+ skippingDiff = true;
836
+ continue;
837
+ }
838
+ if (skippingDiff && /^\s*(?:Tool|Path):\s*/i.test(line)) skippingDiff = false;
839
+ if (skippingDiff) continue;
840
+ if (/^\s*Path:\s*/i.test(line)) continue;
841
+ summary.push(line);
842
+ }
843
+ return summary.join("\n").replace(/\n{3,}/g, "\n\n").trim();
844
+ }
845
+
781
846
  function printWorkspaceChange(change = {}) {
782
847
  if (!change?.diff) return;
783
848
  const formatted = formatWorkspaceChange(change);
@@ -787,6 +852,15 @@ function printWorkspaceChange(change = {}) {
787
852
  for (const line of formatted.lines) outputLine(`${gutter}${line}`);
788
853
  }
789
854
 
855
+ function printEmbeddedWorkspaceResult(result = "", { labelName = "finish", time = "" } = {}) {
856
+ const changes = embeddedWorkspaceChangesFromText(result);
857
+ if (changes.length === 0) return false;
858
+ const summary = embeddedWorkspaceSummary(result);
859
+ if (summary) printHistoryBlock(labelName, summary, { time, bg: ansi.systemBg });
860
+ for (const change of changes) printWorkspaceChange(change);
861
+ return true;
862
+ }
863
+
790
864
  function printPreviewBlock(role, text, { time = "", bg = ansi.systemBg, maxLines = 5 } = {}) {
791
865
  const header = [label(role, bg).trimEnd(), time ? color(time, ansi.dim) : ""].filter(Boolean).join(" ");
792
866
  outputLine(header);
@@ -2201,6 +2275,24 @@ async function latestSession() {
2201
2275
  }
2202
2276
 
2203
2277
  function printHistoryEntry(entry) {
2278
+ if (entry.role === "tool") {
2279
+ const time = entry.at ? new Date(entry.at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "";
2280
+ try {
2281
+ const parsed = JSON.parse(entry.content || "{}");
2282
+ if (parsed.toolName === "finish" && printEmbeddedWorkspaceResult(parsed.result || parsed.args?.result || "", { labelName: "finish", time })) {
2283
+ return;
2284
+ }
2285
+ if (parsed.diff) {
2286
+ printWorkspaceChange(parsed);
2287
+ return;
2288
+ }
2289
+ outputLine(`${label(parsed.ok === false ? "fail" : "done", parsed.ok === false ? ansi.red : ansi.systemBg)} ${toolStatusDetails(parsed)}`);
2290
+ return;
2291
+ } catch {
2292
+ printHistoryBlock("tool", entry.content, { time, bg: ansi.systemBg });
2293
+ return;
2294
+ }
2295
+ }
2204
2296
  const role = entry.role === "assistant" ? "aginti>" : entry.role === "user" ? "user>" : String(entry.role || "note");
2205
2297
  const bg = role === "aginti>" ? ansi.agentBg : role === "user>" ? ansi.userBg : ansi.systemBg;
2206
2298
  const time = entry.at ? new Date(entry.at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "";
@@ -2275,6 +2367,13 @@ function printResumeEvent(event = {}) {
2275
2367
  }
2276
2368
  if (type === "tool.completed" || type === "tool.failed" || type === "tool.skipped") {
2277
2369
  const failed = type === "tool.failed" || data.ok === false || data.blocked || data.error;
2370
+ if (data.diff) {
2371
+ printWorkspaceChange(data);
2372
+ return true;
2373
+ }
2374
+ if (data.toolName === "finish" && printEmbeddedWorkspaceResult(data.result || data.args?.result || "", { labelName: "finish" })) {
2375
+ return true;
2376
+ }
2278
2377
  if (data.toolName === "run_command") {
2279
2378
  printCommandOutputLog({
2280
2379
  command: data.args?.command || data.command || "",
@@ -2314,6 +2413,7 @@ function printResumeEvent(event = {}) {
2314
2413
  return true;
2315
2414
  }
2316
2415
  if (type === "session.finished") {
2416
+ if (printEmbeddedWorkspaceResult(data.result || "", { labelName: "finish" })) return true;
2317
2417
  outputLine(`${label("state", ansi.systemBg)} status=finished${data.result ? ` result=${compactLine(data.result, 86)}` : ""}`);
2318
2418
  return true;
2319
2419
  }
@@ -4174,7 +4274,11 @@ async function runPrompt(prompt, state, packageDir, { approvalDepth = 0 } = {})
4174
4274
  } else if (type === "tool.started") {
4175
4275
  printStatusEvent(state, "tool", toolStatusDetails(data));
4176
4276
  } else if (type === "tool.completed" || type === "tool.failed") {
4177
- printStatusEvent(state, type === "tool.failed" || data.ok === false ? "tool_failed" : "tool_done", toolStatusDetails(data));
4277
+ if (data.toolName === "finish" && printEmbeddedWorkspaceResult(data.result || data.args?.result || "", { labelName: "finish" })) {
4278
+ // The finish tool can recap a workspace diff; render it like the original write/patch event.
4279
+ } else {
4280
+ printStatusEvent(state, type === "tool.failed" || data.ok === false ? "tool_failed" : "tool_done", toolStatusDetails(data));
4281
+ }
4178
4282
  } else if (type === "file.changed") {
4179
4283
  printWorkspaceChange(data);
4180
4284
  } else if (type === "tool.blocked") {
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
  };