@lazyingart/agintiflow 0.20.173 → 0.20.175

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.173",
3
+ "version": "0.20.175",
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
@@ -1796,6 +1796,105 @@ function renderPlanEvent(entry) {
1796
1796
  `;
1797
1797
  }
1798
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 compactInline(value = "", limit = 180) {
1877
+ return compactMessage(value, limit);
1878
+ }
1879
+
1880
+ function toolArgumentPreview(toolName = "", args = {}, data = {}) {
1881
+ if (toolName === "finish") return "";
1882
+ if (toolName === "run_command" && (args.command || data.command)) return args.command || data.command;
1883
+ if (["write_file", "apply_patch", "read_file", "open_workspace_file", "preview_workspace"].includes(toolName)) {
1884
+ return args.path || args.file || data.path || "";
1885
+ }
1886
+ if (["send_to_canvas", "create_artifact"].includes(toolName)) {
1887
+ return [args.title || data.title || "canvas artifact", args.kind || data.kind || "", args.selected ? "selected" : ""]
1888
+ .filter(Boolean)
1889
+ .join(" · ");
1890
+ }
1891
+ if (["open_url", "web_research", "web_search"].includes(toolName)) return args.url || args.query || args.q || data.url || "";
1892
+ const scalar = ["title", "path", "file", "query", "q", "url", "kind"]
1893
+ .map((key) => args[key] || data[key])
1894
+ .find(Boolean);
1895
+ return scalar ? compactInline(scalar) : "";
1896
+ }
1897
+
1799
1898
  function renderWorkspaceChangeEvent(entry) {
1800
1899
  const data = entry.data || {};
1801
1900
  const toolName = data.toolName || data.action || "file.changed";
@@ -1833,14 +1932,13 @@ function renderToolEvent(entry) {
1833
1932
  const skipped = message === "tool.skipped";
1834
1933
  const toolName = data.toolName || "tool";
1835
1934
  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) : "");
1935
+ const resultText = String(data.result || args.result || "");
1936
+ const embeddedResult = resultText ? renderEmbeddedWorkspaceResult(resultText, entry) : "";
1937
+ const argPreview = toolArgumentPreview(toolName, args, data);
1840
1938
  const stdout = data.stdout ? outputPreviewText(data.stdout) : null;
1841
1939
  const stderr = data.stderr ? outputPreviewText(data.stderr) : null;
1842
1940
  return `
1843
- <article class="event-card ${failed ? "event-failed" : skipped ? "event-muted" : "event-tool"}">
1941
+ <article class="event-card ${failed ? "event-failed" : skipped ? "event-muted" : toolName === "finish" ? "event-finish" : "event-tool"}">
1844
1942
  <div class="event-card-meta">
1845
1943
  <span>${escapeHtml(failed ? "tool failed" : skipped ? "tool skipped" : message === "tool.started" ? "tool" : "tool done")}</span>
1846
1944
  <span>${entry.at ? escapeHtml(new Date(entry.at).toLocaleString()) : ""}</span>
@@ -1848,6 +1946,7 @@ function renderToolEvent(entry) {
1848
1946
  <strong class="event-path">${escapeHtml(toolName)}</strong>
1849
1947
  ${argPreview ? `<code class="event-inline-code">${escapeHtml(argPreview)}</code>` : ""}
1850
1948
  ${data.error || data.reason ? `<div class="event-fold-note">${escapeHtml(data.error || data.reason)}</div>` : ""}
1949
+ ${embeddedResult || (toolName === "finish" && resultText ? `<div class="markdown-body">${renderMarkdown(resultText)}</div>` : "")}
1851
1950
  ${stdout ? `<div class="log-stream-title">stdout</div><pre class="event-output">${escapeHtml(stdout.text)}</pre>` : ""}
1852
1951
  ${stdout?.hidden > 0 ? `<div class="event-fold-note">... ${stdout.hidden} more stdout line(s) folded</div>` : ""}
1853
1952
  ${stderr ? `<div class="log-stream-title">stderr</div><pre class="event-output">${escapeHtml(stderr.text)}</pre>` : ""}
@@ -1862,13 +1961,14 @@ function renderStatusEvent(entry) {
1862
1961
  const label = entry.eventLabel || message.replace(/^[^.]+\./, "");
1863
1962
  const content = data.result || data.error || data.reason || entry.content || "";
1864
1963
  const failed = message === "session.failed";
1964
+ const embeddedResult = content ? renderEmbeddedWorkspaceResult(content, entry) : "";
1865
1965
  return `
1866
- <article class="event-card ${failed ? "event-failed" : "event-muted"}">
1966
+ <article class="event-card ${failed ? "event-failed" : message === "session.finished" && embeddedResult ? "event-finish" : "event-muted"}">
1867
1967
  <div class="event-card-meta">
1868
1968
  <span>${escapeHtml(label)}</span>
1869
1969
  <span>${entry.at ? escapeHtml(new Date(entry.at).toLocaleString()) : ""}</span>
1870
1970
  </div>
1871
- ${content ? `<div class="chat-content">${escapeHtml(content)}</div>` : ""}
1971
+ ${embeddedResult || (content ? `<div class="chat-content">${escapeHtml(content)}</div>` : "")}
1872
1972
  </article>
1873
1973
  `;
1874
1974
  }
@@ -1907,15 +2007,82 @@ function renderStructuredEvent(entry) {
1907
2007
  return "";
1908
2008
  }
1909
2009
 
2010
+ function logToolLifecycleKey(data = {}) {
2011
+ const args = data.args || {};
2012
+ const focusedArgs = {
2013
+ path: args.path || args.file || data.path || "",
2014
+ command: args.command || data.command || "",
2015
+ title: args.title || data.title || "",
2016
+ kind: args.kind || data.kind || "",
2017
+ query: args.query || args.q || "",
2018
+ url: args.url || data.url || "",
2019
+ };
2020
+ return `${data.toolName || ""}:${JSON.stringify(focusedArgs)}`;
2021
+ }
2022
+
2023
+ function logEventDedupContext(entries = [], runResult = "") {
2024
+ const terminalToolKeys = new Set();
2025
+ const finishResultFingerprints = new Set();
2026
+ const workspaceChangePaths = new Set();
2027
+ const runResultFingerprint = compactInline(runResult, 100000);
2028
+
2029
+ for (const entry of entries || []) {
2030
+ const data = entry.data || {};
2031
+ const type = entry.message || entry.eventType || "";
2032
+ if (["tool.completed", "tool.failed", "tool.skipped", "tool.blocked"].includes(type)) {
2033
+ terminalToolKeys.add(logToolLifecycleKey(data));
2034
+ }
2035
+ if (type === "tool.completed" && data.toolName === "finish" && data.result) {
2036
+ finishResultFingerprints.add(compactInline(data.result, 100000));
2037
+ }
2038
+ if (type === "session.finished" && data.result) {
2039
+ finishResultFingerprints.add(compactInline(data.result, 100000));
2040
+ }
2041
+ if (type === "file.changed" && (data.path || data.args?.path)) {
2042
+ workspaceChangePaths.add(String(data.path || data.args?.path || "").replace(/\\/g, "/"));
2043
+ }
2044
+ }
2045
+
2046
+ return { terminalToolKeys, finishResultFingerprints, workspaceChangePaths, runResultFingerprint };
2047
+ }
2048
+
2049
+ function shouldSuppressLogEntry(entry = {}, context = {}) {
2050
+ const data = entry.data || {};
2051
+ const type = entry.message || entry.eventType || "";
2052
+ const toolName = data.toolName || "";
2053
+ const toolKey = logToolLifecycleKey(data);
2054
+ const resultKey = data.result || data.args?.result ? compactInline(data.result || data.args?.result, 100000) : "";
2055
+ if (type === "tool.started" && context.terminalToolKeys?.has(toolKey)) return true;
2056
+ if (type === "tool.started" && toolName === "finish" && resultKey) {
2057
+ return resultKey === context.runResultFingerprint || context.finishResultFingerprints?.has(resultKey);
2058
+ }
2059
+ if (type === "tool.completed" && toolName === "finish" && resultKey && resultKey === context.runResultFingerprint) return true;
2060
+ if (type === "session.finished" && resultKey) {
2061
+ return resultKey === context.runResultFingerprint || context.finishResultFingerprints?.has(resultKey);
2062
+ }
2063
+ if (type === "tool.completed" && ["write_file", "apply_patch"].includes(toolName)) {
2064
+ const pathKey = String(data.path || data.args?.path || data.args?.file || "").replace(/\\/g, "/");
2065
+ if (pathKey && context.workspaceChangePaths?.has(pathKey)) return true;
2066
+ }
2067
+ return false;
2068
+ }
2069
+
1910
2070
  function renderLogs(run) {
1911
2071
  logsEl.dataset.mode = "active";
2072
+ const structuredRunResult = run.result ? renderEmbeddedWorkspaceResult(run.result, { at: run.endedAt || run.updatedAt || "" }) : "";
1912
2073
  const parts = [
1913
2074
  `<div class="log-line">${escapeHtml(`status=${run.status} session=${run.sessionId} provider=${run.provider} model=${run.model}`)}</div>`,
1914
- run.result ? `<div class="log-line">${escapeHtml(`result=${run.result}`)}</div>` : "",
2075
+ structuredRunResult
2076
+ ? `<article class="event-card event-finish"><div class="event-card-meta"><span>result</span></div>${structuredRunResult}</article>`
2077
+ : run.result
2078
+ ? `<div class="log-line">${escapeHtml(`result=${run.result}`)}</div>`
2079
+ : "",
1915
2080
  run.error ? `<div class="log-line error">${escapeHtml(`error=${run.error}`)}</div>` : "",
1916
2081
  ];
1917
2082
 
2083
+ const dedupContext = logEventDedupContext(run.logs || [], run.result || "");
1918
2084
  for (const entry of run.logs || []) {
2085
+ if (shouldSuppressLogEntry(entry, dedupContext)) continue;
1919
2086
  const structured = renderStructuredEvent(entry);
1920
2087
  if (structured) {
1921
2088
  parts.push(structured);
@@ -2174,12 +2341,41 @@ function renderChat(chatEntries) {
2174
2341
  </article>
2175
2342
  `;
2176
2343
  }
2344
+ if (entry.role === "tool") {
2345
+ try {
2346
+ const parsed = JSON.parse(entry.content || "{}");
2347
+ const toolName = parsed.toolName || "tool";
2348
+ return renderToolEvent({
2349
+ role: "event",
2350
+ eventType: parsed.ok === false ? "tool.failed" : "tool.completed",
2351
+ message: parsed.ok === false ? "tool.failed" : "tool.completed",
2352
+ eventLabel: parsed.ok === false ? "tool failed" : "tool done",
2353
+ data: parsed,
2354
+ content: parsed.result || toolName,
2355
+ at: entry.at,
2356
+ });
2357
+ } catch {
2358
+ return `
2359
+ <article class="event-card event-tool">
2360
+ <div class="event-card-meta">tool${entry.at ? ` · ${new Date(entry.at).toLocaleString()}` : ""}</div>
2361
+ <pre class="event-output">${escapeHtml(entry.content || "")}</pre>
2362
+ </article>
2363
+ `;
2364
+ }
2365
+ }
2177
2366
  const role = entry.role === "assistant" ? "assistant" : "user";
2178
2367
  const label = role === "assistant" ? t("assistantLabel") : t("youLabel");
2179
- const content =
2180
- role === "assistant"
2181
- ? `<div class="markdown-body">${renderMarkdown(entry.content)}</div>`
2182
- : escapeHtml(entry.content).replace(/\n/g, "<br>");
2368
+ let content = escapeHtml(entry.content).replace(/\n/g, "<br>");
2369
+ if (role === "assistant") {
2370
+ const embeddedResult = renderEmbeddedWorkspaceResult(entry.content, entry);
2371
+ if (embeddedResult && entry.suppressEmbeddedWorkspaceDiffs) {
2372
+ content = `<div class="markdown-body">${renderMarkdown(embeddedWorkspaceSummary(entry.content))}</div>`;
2373
+ } else if (embeddedResult) {
2374
+ content = `<article class="event-card event-finish assistant-result-card">${embeddedResult}</article>`;
2375
+ } else {
2376
+ content = `<div class="markdown-body">${renderMarkdown(entry.content)}</div>`;
2377
+ }
2378
+ }
2183
2379
  return `
2184
2380
  <article class="chat-item ${role}">
2185
2381
  <div class="chat-meta">${label}${entry.at ? ` · ${new Date(entry.at).toLocaleString()}` : ""}</div>
package/public/styles.css CHANGED
@@ -1264,6 +1264,19 @@ button.danger {
1264
1264
  border-color: rgba(59, 130, 246, 0.28);
1265
1265
  }
1266
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
+
1267
1280
  .event-failed {
1268
1281
  border-color: rgba(248, 113, 113, 0.45);
1269
1282
  background: rgba(127, 29, 29, 0.12);
@@ -1337,6 +1350,31 @@ button.danger {
1337
1350
  font-size: 0.8rem;
1338
1351
  }
1339
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
+ .assistant-result-card {
1375
+ margin: 0;
1376
+ }
1377
+
1340
1378
  #logs .event-fold-note {
1341
1379
  color: #94a3b8;
1342
1380
  }
@@ -896,6 +896,13 @@ try {
896
896
  if (!latest.stdout.includes(" write ") || !latest.stdout.includes("+Created by AgInTiFlow mock mode.")) {
897
897
  throw new Error("bare aginti resume did not replay formatted file-change diff context");
898
898
  }
899
+ if (latest.stdout.includes('{"result":"Mock run complete')) {
900
+ throw new Error("bare aginti resume rendered raw finish JSON instead of formatted history");
901
+ }
902
+ const mockCompleteCount = (latest.stdout.match(/Mock run complete\./g) || []).length;
903
+ if (mockCompleteCount > 1) {
904
+ throw new Error(`bare aginti resume duplicated the finish result ${mockCompleteCount} times`);
905
+ }
899
906
  if (latest.stdout.includes("resume note=showing chat transcript only")) {
900
907
  throw new Error("bare aginti resume regressed to chat-only history");
901
908
  }
@@ -209,6 +209,29 @@ try {
209
209
  const chatTail = (await page.locator("#chat-thread").innerText().catch(() => "")).slice(-1200);
210
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
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
+ }
227
+ const formattedChatText = await page.locator("#chat-thread").innerText();
228
+ if (formattedChatText.includes('{"result":"Mock run complete')) {
229
+ throw new Error("web UI rendered raw finish JSON in chat history");
230
+ }
231
+ const mockCompleteCount = (formattedChatText.match(/Mock run complete\./g) || []).length;
232
+ if (mockCompleteCount > 1) {
233
+ throw new Error(`web UI duplicated the assistant/session finish result ${mockCompleteCount} times`);
234
+ }
212
235
 
213
236
  await page.click("#open-settings");
214
237
  await page.waitForSelector("#settings-modal[open]");
@@ -246,6 +269,8 @@ try {
246
269
  "new-session-resets-scope",
247
270
  "formatted-plan-event-card",
248
271
  "formatted-file-diff-event-card",
272
+ "formatted-finish-result-diff-card",
273
+ "deduped-finish-history",
249
274
  "settings-provider-dropdowns",
250
275
  "settings-wrapper-dropdowns",
251
276
  ],
@@ -778,6 +778,112 @@ 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
+
846
+ function normalizedTimelineText(value = "") {
847
+ return String(value || "").replace(/\r\n?/g, "\n").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
848
+ }
849
+
850
+ function timelineTextFingerprint(value = "") {
851
+ return normalizedTimelineText(value).replace(/\s+/g, " ").trim();
852
+ }
853
+
854
+ function workspaceChangeFingerprint(change = {}) {
855
+ const pathKey = String(change.path || change.args?.path || "").replace(/\\/g, "/").trim();
856
+ const diffKey = normalizedTimelineText(change.diff || "");
857
+ return pathKey || diffKey ? `${pathKey}\n${diffKey}` : "";
858
+ }
859
+
860
+ function eventWorkspaceChangeFingerprint(event = {}) {
861
+ const data = event.data || {};
862
+ if (event.type === "file.changed") return workspaceChangeFingerprint(data);
863
+ if ((event.type === "tool.completed" || event.type === "tool.failed") && data.diff) return workspaceChangeFingerprint(data);
864
+ return "";
865
+ }
866
+
867
+ function eventWorkspaceChangePath(event = {}) {
868
+ const data = event.data || {};
869
+ if (event.type !== "file.changed" && !data.diff) return "";
870
+ return String(data.path || data.args?.path || "").replace(/\\/g, "/").trim();
871
+ }
872
+
873
+ function toolLifecycleKey(event = {}) {
874
+ const data = event.data || {};
875
+ const args = data.args || {};
876
+ const focusedArgs = {
877
+ path: args.path || args.file || data.path || "",
878
+ command: args.command || data.command || "",
879
+ title: args.title || data.title || "",
880
+ kind: args.kind || data.kind || "",
881
+ query: args.query || args.q || "",
882
+ url: args.url || data.url || "",
883
+ };
884
+ return `${data.toolName || ""}:${JSON.stringify(focusedArgs)}`;
885
+ }
886
+
781
887
  function printWorkspaceChange(change = {}) {
782
888
  if (!change?.diff) return;
783
889
  const formatted = formatWorkspaceChange(change);
@@ -787,6 +893,17 @@ function printWorkspaceChange(change = {}) {
787
893
  for (const line of formatted.lines) outputLine(`${gutter}${line}`);
788
894
  }
789
895
 
896
+ function printEmbeddedWorkspaceResult(result = "", { labelName = "finish", time = "", bg = ansi.systemBg, includeDiffs = true } = {}) {
897
+ const changes = embeddedWorkspaceChangesFromText(result);
898
+ if (changes.length === 0) return false;
899
+ const summary = embeddedWorkspaceSummary(result);
900
+ if (summary) printHistoryBlock(labelName, summary, { time, bg });
901
+ if (includeDiffs) {
902
+ for (const change of changes) printWorkspaceChange(change);
903
+ }
904
+ return true;
905
+ }
906
+
790
907
  function printPreviewBlock(role, text, { time = "", bg = ansi.systemBg, maxLines = 5 } = {}) {
791
908
  const header = [label(role, bg).trimEnd(), time ? color(time, ansi.dim) : ""].filter(Boolean).join(" ");
792
909
  outputLine(header);
@@ -2201,9 +2318,38 @@ async function latestSession() {
2201
2318
  }
2202
2319
 
2203
2320
  function printHistoryEntry(entry) {
2321
+ if (entry.role === "tool") {
2322
+ const time = entry.at ? new Date(entry.at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "";
2323
+ try {
2324
+ const parsed = JSON.parse(entry.content || "{}");
2325
+ if (parsed.toolName === "finish" && printEmbeddedWorkspaceResult(parsed.result || parsed.args?.result || "", { labelName: "finish", time })) {
2326
+ return;
2327
+ }
2328
+ if (parsed.diff) {
2329
+ printWorkspaceChange(parsed);
2330
+ return;
2331
+ }
2332
+ outputLine(`${label(parsed.ok === false ? "fail" : "done", parsed.ok === false ? ansi.red : ansi.systemBg)} ${toolStatusDetails(parsed)}`);
2333
+ return;
2334
+ } catch {
2335
+ printHistoryBlock("tool", entry.content, { time, bg: ansi.systemBg });
2336
+ return;
2337
+ }
2338
+ }
2204
2339
  const role = entry.role === "assistant" ? "aginti>" : entry.role === "user" ? "user>" : String(entry.role || "note");
2205
2340
  const bg = role === "aginti>" ? ansi.agentBg : role === "user>" ? ansi.userBg : ansi.systemBg;
2206
2341
  const time = entry.at ? new Date(entry.at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "";
2342
+ if (
2343
+ entry.role === "assistant" &&
2344
+ printEmbeddedWorkspaceResult(entry.content, {
2345
+ labelName: role,
2346
+ time,
2347
+ bg,
2348
+ includeDiffs: !entry.suppressEmbeddedWorkspaceDiffs,
2349
+ })
2350
+ ) {
2351
+ return;
2352
+ }
2207
2353
  printHistoryBlock(role, entry.content, { time, bg });
2208
2354
  }
2209
2355
 
@@ -2243,16 +2389,66 @@ function isReplayableResumeEvent(event = {}) {
2243
2389
  function resumeTimelineItems(chat = [], events = [], { limit = 0 } = {}) {
2244
2390
  const shownChat = limit > 0 ? chat.slice(-limit) : chat;
2245
2391
  const firstChatTime = limit > 0 && shownChat.length > 0 ? timestampMs(shownChat[0].at) : 0;
2246
- const chatItems = shownChat.map((entry, index) => ({
2247
- kind: "chat",
2248
- entry,
2249
- order: index * 2,
2250
- at: timestampMs(entry.at),
2251
- }));
2252
- const eventItems = events
2392
+ const eventList = Array.isArray(events) ? events : [];
2393
+ const workspaceChangeKeys = new Set(eventList.map(eventWorkspaceChangeFingerprint).filter(Boolean));
2394
+ const workspaceChangePaths = new Set(eventList.map(eventWorkspaceChangePath).filter(Boolean));
2395
+ const chatResultFingerprints = new Set(
2396
+ (Array.isArray(chat) ? chat : [])
2397
+ .filter((entry) => entry?.role === "assistant" && entry?.content)
2398
+ .map((entry) => timelineTextFingerprint(entry.content))
2399
+ .filter(Boolean)
2400
+ );
2401
+ const finishResultFingerprints = new Set(
2402
+ eventList
2403
+ .filter((event) => (event.type === "tool.completed" && event.data?.toolName === "finish") || event.type === "session.finished")
2404
+ .map((event) => timelineTextFingerprint(event.data?.result || ""))
2405
+ .filter(Boolean)
2406
+ );
2407
+ const terminalToolKeys = new Set(
2408
+ eventList
2409
+ .filter((event) => ["tool.completed", "tool.failed", "tool.skipped", "tool.blocked"].includes(event.type))
2410
+ .map(toolLifecycleKey)
2411
+ .filter(Boolean)
2412
+ );
2413
+ const shouldSuppressEvent = (event = {}) => {
2414
+ const type = String(event.type || "");
2415
+ const data = event.data || {};
2416
+ const toolName = String(data.toolName || "");
2417
+ const resultFingerprint = timelineTextFingerprint(data.result || data.args?.result || "");
2418
+ if (type === "tool.started" && terminalToolKeys.has(toolLifecycleKey(event))) return true;
2419
+ if (type === "tool.started" && toolName === "finish" && resultFingerprint) {
2420
+ return chatResultFingerprints.has(resultFingerprint) || finishResultFingerprints.has(resultFingerprint);
2421
+ }
2422
+ if (["tool.completed", "tool.failed", "tool.skipped"].includes(type)) {
2423
+ if (toolName === "finish" && resultFingerprint && chatResultFingerprints.has(resultFingerprint)) return true;
2424
+ if (["write_file", "apply_patch"].includes(toolName)) {
2425
+ const pathKey = String(data.path || data.args?.path || data.args?.file || "").replace(/\\/g, "/").trim();
2426
+ if (pathKey && workspaceChangePaths.has(pathKey)) return true;
2427
+ const changeKey = eventWorkspaceChangeFingerprint(event);
2428
+ if (changeKey && workspaceChangeKeys.has(changeKey)) return true;
2429
+ }
2430
+ }
2431
+ if (type === "session.finished" && resultFingerprint) {
2432
+ return chatResultFingerprints.has(resultFingerprint) || finishResultFingerprints.has(resultFingerprint);
2433
+ }
2434
+ return false;
2435
+ };
2436
+ const chatItems = shownChat.map((entry, index) => {
2437
+ const embeddedChanges = entry.role === "assistant" ? embeddedWorkspaceChangesFromText(entry.content) : [];
2438
+ const suppressEmbeddedWorkspaceDiffs =
2439
+ embeddedChanges.length > 0 && embeddedChanges.every((change) => workspaceChangeKeys.has(workspaceChangeFingerprint(change)));
2440
+ return {
2441
+ kind: "chat",
2442
+ entry: { ...entry, suppressEmbeddedWorkspaceDiffs },
2443
+ order: index * 2,
2444
+ at: timestampMs(entry.at),
2445
+ };
2446
+ });
2447
+ const eventItems = eventList
2253
2448
  .map((event, index) => ({ event, index }))
2254
2449
  .filter(({ event }) => isReplayableResumeEvent(event))
2255
2450
  .filter(({ event }) => !firstChatTime || timestampMs(event.timestamp) >= firstChatTime)
2451
+ .filter(({ event }) => !shouldSuppressEvent(event))
2256
2452
  .map(({ event, index }) => ({
2257
2453
  kind: "event",
2258
2454
  event,
@@ -2279,6 +2475,9 @@ function printResumeEvent(event = {}) {
2279
2475
  printWorkspaceChange(data);
2280
2476
  return true;
2281
2477
  }
2478
+ if (data.toolName === "finish" && printEmbeddedWorkspaceResult(data.result || data.args?.result || "", { labelName: "finish" })) {
2479
+ return true;
2480
+ }
2282
2481
  if (data.toolName === "run_command") {
2283
2482
  printCommandOutputLog({
2284
2483
  command: data.args?.command || data.command || "",
@@ -2318,6 +2517,7 @@ function printResumeEvent(event = {}) {
2318
2517
  return true;
2319
2518
  }
2320
2519
  if (type === "session.finished") {
2520
+ if (printEmbeddedWorkspaceResult(data.result || "", { labelName: "finish" })) return true;
2321
2521
  outputLine(`${label("state", ansi.systemBg)} status=finished${data.result ? ` result=${compactLine(data.result, 86)}` : ""}`);
2322
2522
  return true;
2323
2523
  }
@@ -2367,6 +2567,9 @@ function toolStatusDetails(data = {}) {
2367
2567
  if ((tool === "write_file" || tool === "apply_patch" || tool === "read_file" || tool === "open_workspace_file") && args.path) {
2368
2568
  return `${tool}: ${compactLine(args.path, 58)}`;
2369
2569
  }
2570
+ if ((tool === "send_to_canvas" || tool === "create_artifact") && (args.title || args.kind)) {
2571
+ return `${tool}: ${compactLine([args.title || "canvas artifact", args.kind || "", args.selected ? "selected" : ""].filter(Boolean).join(" · "), 72)}`;
2572
+ }
2370
2573
  if ((tool === "open_url" || tool === "web_research" || tool === "web_search") && (args.url || args.query || args.q)) {
2371
2574
  return `${tool}: ${compactLine(args.url || args.query || args.q, 58)}`;
2372
2575
  }
@@ -4178,7 +4381,11 @@ async function runPrompt(prompt, state, packageDir, { approvalDepth = 0 } = {})
4178
4381
  } else if (type === "tool.started") {
4179
4382
  printStatusEvent(state, "tool", toolStatusDetails(data));
4180
4383
  } else if (type === "tool.completed" || type === "tool.failed") {
4181
- printStatusEvent(state, type === "tool.failed" || data.ok === false ? "tool_failed" : "tool_done", toolStatusDetails(data));
4384
+ if (data.toolName === "finish" && printEmbeddedWorkspaceResult(data.result || data.args?.result || "", { labelName: "finish" })) {
4385
+ // The finish tool can recap a workspace diff; render it like the original write/patch event.
4386
+ } else {
4387
+ printStatusEvent(state, type === "tool.failed" || data.ok === false ? "tool_failed" : "tool_done", toolStatusDetails(data));
4388
+ }
4182
4389
  } else if (type === "file.changed") {
4183
4390
  printWorkspaceChange(data);
4184
4391
  } else if (type === "tool.blocked") {
package/web.js CHANGED
@@ -107,6 +107,124 @@ function compactText(value = "", limit = 120) {
107
107
  return text.length <= limit ? text : `${text.slice(0, Math.max(limit - 1, 1)).trim()}...`;
108
108
  }
109
109
 
110
+ function normalizedTimelineText(value = "") {
111
+ return String(value || "").replace(/\r\n?/g, "\n").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
112
+ }
113
+
114
+ function timelineTextFingerprint(value = "") {
115
+ return normalizedTimelineText(value).replace(/\s+/g, " ").trim();
116
+ }
117
+
118
+ function embeddedWorkspaceChangesFromText(value = "") {
119
+ const lines = String(value || "").split(/\r?\n/);
120
+ const changes = [];
121
+ let currentTool = "";
122
+
123
+ for (let index = 0; index < lines.length; index += 1) {
124
+ const toolMatch = lines[index].match(/^\s*Tool:\s*(.+?)\s*$/i);
125
+ if (toolMatch) {
126
+ currentTool = toolMatch[1].trim();
127
+ continue;
128
+ }
129
+
130
+ const pathMatch = lines[index].match(/^\s*Path:\s*(.+?)\s*$/i);
131
+ if (!pathMatch) continue;
132
+
133
+ let cursor = index + 1;
134
+ while (cursor < lines.length && !/^\s*Diff:\s*$/i.test(lines[cursor]) && !/^\s*(?:Tool|Path):\s*/i.test(lines[cursor])) {
135
+ cursor += 1;
136
+ }
137
+ if (!/^\s*Diff:\s*$/i.test(lines[cursor] || "")) continue;
138
+
139
+ const diffLines = [];
140
+ cursor += 1;
141
+ while (cursor < lines.length) {
142
+ if (diffLines.length && /^\s*(?:Tool|Path):\s*/i.test(lines[cursor])) break;
143
+ if (
144
+ diffLines.length &&
145
+ /^\s*(?:Output:|No command output|Blocked by guardrail\.|Mock run complete\.)/i.test(lines[cursor]) &&
146
+ !/^[-+@ ]/.test(lines[cursor])
147
+ ) {
148
+ break;
149
+ }
150
+ diffLines.push(lines[cursor]);
151
+ cursor += 1;
152
+ }
153
+ while (diffLines.length && !diffLines[diffLines.length - 1].trim()) diffLines.pop();
154
+ if (diffLines.length) {
155
+ changes.push({
156
+ toolName: currentTool || "write_file",
157
+ path: pathMatch[1].trim(),
158
+ diff: diffLines.join("\n"),
159
+ });
160
+ index = Math.max(index, cursor - 1);
161
+ }
162
+ }
163
+
164
+ return changes;
165
+ }
166
+
167
+ function workspaceChangeFingerprint(change = {}) {
168
+ const pathKey = String(change.path || change.args?.path || "").replace(/\\/g, "/").trim();
169
+ const diffKey = normalizedTimelineText(change.diff || "");
170
+ return pathKey || diffKey ? `${pathKey}\n${diffKey}` : "";
171
+ }
172
+
173
+ function eventWorkspaceChangeFingerprint(event = {}) {
174
+ const data = event.data || {};
175
+ if (event.type === "file.changed") return workspaceChangeFingerprint(data);
176
+ if ((event.type === "tool.completed" || event.type === "tool.failed") && data.diff) return workspaceChangeFingerprint(data);
177
+ return "";
178
+ }
179
+
180
+ function eventWorkspaceChangePath(event = {}) {
181
+ const data = event.data || {};
182
+ if (event.type !== "file.changed" && !data.diff) return "";
183
+ return String(data.path || data.args?.path || "").replace(/\\/g, "/").trim();
184
+ }
185
+
186
+ function toolLifecycleKey(event = {}) {
187
+ const data = event.data || {};
188
+ const args = data.args || {};
189
+ const focusedArgs = {
190
+ path: args.path || args.file || data.path || "",
191
+ command: args.command || data.command || "",
192
+ title: args.title || data.title || "",
193
+ kind: args.kind || data.kind || "",
194
+ query: args.query || args.q || "",
195
+ url: args.url || data.url || "",
196
+ };
197
+ return `${data.toolName || ""}:${JSON.stringify(focusedArgs)}`;
198
+ }
199
+
200
+ function shouldSuppressTimelineEvent(event = {}, context = {}) {
201
+ const type = String(event.type || "");
202
+ const data = event.data || {};
203
+ const toolName = String(data.toolName || "");
204
+ const resultFingerprint = timelineTextFingerprint(data.result || data.args?.result || "");
205
+
206
+ if (type === "tool.started" && context.terminalToolKeys.has(toolLifecycleKey(event))) return true;
207
+ if (type === "tool.started" && toolName === "finish" && resultFingerprint) {
208
+ return context.chatResultFingerprints.has(resultFingerprint) || context.finishResultFingerprints.has(resultFingerprint);
209
+ }
210
+
211
+ if (["tool.completed", "tool.failed", "tool.skipped"].includes(type)) {
212
+ if (toolName === "finish" && resultFingerprint && context.chatResultFingerprints.has(resultFingerprint)) return true;
213
+ if (["write_file", "apply_patch"].includes(toolName)) {
214
+ const pathKey = String(data.path || data.args?.path || data.args?.file || "").replace(/\\/g, "/").trim();
215
+ if (pathKey && context.workspaceChangePaths.has(pathKey)) return true;
216
+ const changeKey = eventWorkspaceChangeFingerprint(event);
217
+ if (changeKey && context.workspaceChangeKeys.has(changeKey)) return true;
218
+ }
219
+ }
220
+
221
+ if (type === "session.finished" && resultFingerprint) {
222
+ return context.chatResultFingerprints.has(resultFingerprint) || context.finishResultFingerprints.has(resultFingerprint);
223
+ }
224
+
225
+ return false;
226
+ }
227
+
110
228
  function toolTimelineSummary(data = {}) {
111
229
  const tool = data.toolName || "unknown";
112
230
  const args = data.args || {};
@@ -224,10 +342,44 @@ function timelineEntryForEvent(event = {}) {
224
342
  }
225
343
 
226
344
  function sessionTimelineFromChatAndEvents(chat = [], events = []) {
345
+ const eventList = Array.isArray(events) ? events : [];
346
+ const workspaceChangeKeys = new Set(eventList.map(eventWorkspaceChangeFingerprint).filter(Boolean));
347
+ const workspaceChangePaths = new Set(eventList.map(eventWorkspaceChangePath).filter(Boolean));
348
+ const chatResultFingerprints = new Set(
349
+ (Array.isArray(chat) ? chat : [])
350
+ .filter((entry) => entry?.role === "assistant" && entry?.content)
351
+ .map((entry) => timelineTextFingerprint(entry.content))
352
+ .filter(Boolean)
353
+ );
354
+ const finishResultFingerprints = new Set(
355
+ eventList
356
+ .filter((event) => (event.type === "tool.completed" && event.data?.toolName === "finish") || event.type === "session.finished")
357
+ .map((event) => timelineTextFingerprint(event.data?.result || ""))
358
+ .filter(Boolean)
359
+ );
360
+ const terminalToolKeys = new Set(
361
+ eventList
362
+ .filter((event) => ["tool.completed", "tool.failed", "tool.skipped", "tool.blocked"].includes(event.type))
363
+ .map(toolLifecycleKey)
364
+ .filter(Boolean)
365
+ );
366
+ const suppressContext = {
367
+ chatResultFingerprints,
368
+ finishResultFingerprints,
369
+ terminalToolKeys,
370
+ workspaceChangeKeys,
371
+ workspaceChangePaths,
372
+ };
227
373
  const chatItems = (Array.isArray(chat) ? chat : [])
228
374
  .filter((entry) => entry?.content)
229
- .map((entry, index) => ({ ...entry, order: index * 2, sortAt: Date.parse(entry.at || "") || 0 }));
230
- const eventItems = (Array.isArray(events) ? events : [])
375
+ .map((entry, index) => {
376
+ const embeddedChanges = entry.role === "assistant" ? embeddedWorkspaceChangesFromText(entry.content) : [];
377
+ const suppressEmbeddedWorkspaceDiffs =
378
+ embeddedChanges.length > 0 && embeddedChanges.every((change) => workspaceChangeKeys.has(workspaceChangeFingerprint(change)));
379
+ return { ...entry, suppressEmbeddedWorkspaceDiffs, order: index * 2, sortAt: Date.parse(entry.at || "") || 0 };
380
+ });
381
+ const eventItems = eventList
382
+ .filter((event) => !shouldSuppressTimelineEvent(event, suppressContext))
231
383
  .map(timelineEntryForEvent)
232
384
  .filter(Boolean)
233
385
  .map((entry, index) => ({ ...entry, order: index * 2 + 1, sortAt: Date.parse(entry.at || "") || 0 }));