@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 +1 -1
- package/public/app.js +208 -12
- package/public/styles.css +38 -0
- package/scripts/smoke-cli-chat.js +7 -0
- package/scripts/smoke-web-ui.js +25 -0
- package/src/interactive-cli.js +215 -8
- package/web.js +154 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
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
|
|
1837
|
-
|
|
1838
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
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
|
}
|
package/scripts/smoke-web-ui.js
CHANGED
|
@@ -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
|
],
|
package/src/interactive-cli.js
CHANGED
|
@@ -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
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
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
|
-
|
|
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) =>
|
|
230
|
-
|
|
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 }));
|