@lazyingart/agintiflow 0.20.171 → 0.20.173

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.171",
3
+ "version": "0.20.173",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
6
6
  "license": "Apache-2.0",
package/public/app.js CHANGED
@@ -1782,6 +1782,131 @@ function renderPermissionApproval(entry) {
1782
1782
  `;
1783
1783
  }
1784
1784
 
1785
+ function renderPlanEvent(entry) {
1786
+ const data = entry.data || {};
1787
+ const plan = data.plan || entry.content || "";
1788
+ return `
1789
+ <article class="event-card event-plan">
1790
+ <div class="event-card-meta">
1791
+ <span>plan</span>
1792
+ <span>${entry.at ? escapeHtml(new Date(entry.at).toLocaleString()) : ""}</span>
1793
+ </div>
1794
+ <div class="markdown-body">${renderMarkdown(plan)}</div>
1795
+ </article>
1796
+ `;
1797
+ }
1798
+
1799
+ function renderWorkspaceChangeEvent(entry) {
1800
+ const data = entry.data || {};
1801
+ const toolName = data.toolName || data.action || "file.changed";
1802
+ const isPatch = toolName === "apply_patch" || String(toolName).startsWith("apply_patch");
1803
+ const diff = String(data.diff || "");
1804
+ const maxDiffChars = 9000;
1805
+ const diffBlock = diff
1806
+ ? `<pre class="change-diff event-diff">${renderDiffHtml(diff, maxDiffChars)}</pre>${
1807
+ diff.length > maxDiffChars ? `<div class="event-fold-note">... diff truncated for browser preview</div>` : ""
1808
+ }`
1809
+ : "";
1810
+ const hashes =
1811
+ data.beforeHash || data.afterHash
1812
+ ? `<div class="event-card-meta"><span>before=${escapeHtml((data.beforeHash || "new").slice(0, 10))}</span><span>after=${escapeHtml((data.afterHash || "").slice(0, 10))}</span></div>`
1813
+ : "";
1814
+ return `
1815
+ <article class="event-card ${isPatch ? "event-patch" : "event-write"}">
1816
+ <div class="event-card-meta">
1817
+ <span>${escapeHtml(isPatch ? "patch" : "write")}</span>
1818
+ <span>${escapeHtml(toolName)}</span>
1819
+ <span>${entry.at ? escapeHtml(new Date(entry.at).toLocaleString()) : ""}</span>
1820
+ </div>
1821
+ <strong class="event-path">${escapeHtml(data.path || entry.content || "")}</strong>
1822
+ ${data.created ? `<div class="event-fold-note">created</div>` : ""}
1823
+ ${hashes}
1824
+ ${diffBlock || `<div class="event-fold-note">No diff preview recorded for this file event.</div>`}
1825
+ </article>
1826
+ `;
1827
+ }
1828
+
1829
+ function renderToolEvent(entry) {
1830
+ const data = entry.data || {};
1831
+ const message = entry.message || entry.eventType || "";
1832
+ const failed = message === "tool.failed" || data.ok === false || data.blocked || data.error;
1833
+ const skipped = message === "tool.skipped";
1834
+ const toolName = data.toolName || "tool";
1835
+ const args = data.args || {};
1836
+ const argPreview =
1837
+ toolName === "run_command" && args.command
1838
+ ? args.command
1839
+ : args.path || args.url || args.query || args.q || (Object.keys(args).length ? JSON.stringify(args) : "");
1840
+ const stdout = data.stdout ? outputPreviewText(data.stdout) : null;
1841
+ const stderr = data.stderr ? outputPreviewText(data.stderr) : null;
1842
+ return `
1843
+ <article class="event-card ${failed ? "event-failed" : skipped ? "event-muted" : "event-tool"}">
1844
+ <div class="event-card-meta">
1845
+ <span>${escapeHtml(failed ? "tool failed" : skipped ? "tool skipped" : message === "tool.started" ? "tool" : "tool done")}</span>
1846
+ <span>${entry.at ? escapeHtml(new Date(entry.at).toLocaleString()) : ""}</span>
1847
+ </div>
1848
+ <strong class="event-path">${escapeHtml(toolName)}</strong>
1849
+ ${argPreview ? `<code class="event-inline-code">${escapeHtml(argPreview)}</code>` : ""}
1850
+ ${data.error || data.reason ? `<div class="event-fold-note">${escapeHtml(data.error || data.reason)}</div>` : ""}
1851
+ ${stdout ? `<div class="log-stream-title">stdout</div><pre class="event-output">${escapeHtml(stdout.text)}</pre>` : ""}
1852
+ ${stdout?.hidden > 0 ? `<div class="event-fold-note">... ${stdout.hidden} more stdout line(s) folded</div>` : ""}
1853
+ ${stderr ? `<div class="log-stream-title">stderr</div><pre class="event-output">${escapeHtml(stderr.text)}</pre>` : ""}
1854
+ ${stderr?.hidden > 0 ? `<div class="event-fold-note">... ${stderr.hidden} more stderr line(s) folded</div>` : ""}
1855
+ </article>
1856
+ `;
1857
+ }
1858
+
1859
+ function renderStatusEvent(entry) {
1860
+ const data = entry.data || {};
1861
+ const message = entry.message || entry.eventType || "event";
1862
+ const label = entry.eventLabel || message.replace(/^[^.]+\./, "");
1863
+ const content = data.result || data.error || data.reason || entry.content || "";
1864
+ const failed = message === "session.failed";
1865
+ return `
1866
+ <article class="event-card ${failed ? "event-failed" : "event-muted"}">
1867
+ <div class="event-card-meta">
1868
+ <span>${escapeHtml(label)}</span>
1869
+ <span>${entry.at ? escapeHtml(new Date(entry.at).toLocaleString()) : ""}</span>
1870
+ </div>
1871
+ ${content ? `<div class="chat-content">${escapeHtml(content)}</div>` : ""}
1872
+ </article>
1873
+ `;
1874
+ }
1875
+
1876
+ function renderStructuredEvent(entry) {
1877
+ const message = entry.message || entry.eventType || "";
1878
+ if (message === "plan.created") return renderPlanEvent(entry);
1879
+ if (message === "command.output") return renderCommandOutputLog(entry);
1880
+ if (message === "file.changed") return renderWorkspaceChangeEvent(entry);
1881
+ if (message === "tool.blocked" && entry.data?.permissionAdvice) return renderPermissionApproval(entry);
1882
+ if (message === "tool.blocked") {
1883
+ return `
1884
+ <article class="event-card event-failed">
1885
+ <div class="event-card-meta">
1886
+ <span>permission</span>
1887
+ <span>${entry.at ? escapeHtml(new Date(entry.at).toLocaleString()) : ""}</span>
1888
+ </div>
1889
+ <strong class="event-path">${escapeHtml(entry.data?.toolName || "blocked")}</strong>
1890
+ <div class="chat-content">${escapeHtml(entry.data?.reason || entry.content || "Permission blocked.")}</div>
1891
+ </article>
1892
+ `;
1893
+ }
1894
+ if (message === "tool.started" || message === "tool.completed" || message === "tool.failed" || message === "tool.skipped") {
1895
+ return renderToolEvent(entry);
1896
+ }
1897
+ if (
1898
+ message === "budget.initialized" ||
1899
+ message === "conversation.continued" ||
1900
+ message === "conversation.queued_input_applied" ||
1901
+ message === "session.finished" ||
1902
+ message === "session.failed" ||
1903
+ message === "session.stopped"
1904
+ ) {
1905
+ return renderStatusEvent(entry);
1906
+ }
1907
+ return "";
1908
+ }
1909
+
1785
1910
  function renderLogs(run) {
1786
1911
  logsEl.dataset.mode = "active";
1787
1912
  const parts = [
@@ -1791,15 +1916,12 @@ function renderLogs(run) {
1791
1916
  ];
1792
1917
 
1793
1918
  for (const entry of run.logs || []) {
1794
- if (entry.message === "command.output") {
1795
- parts.push(renderCommandOutputLog(entry));
1919
+ const structured = renderStructuredEvent(entry);
1920
+ if (structured) {
1921
+ parts.push(structured);
1796
1922
  continue;
1797
1923
  }
1798
1924
  parts.push(`<div class="log-line">${escapeHtml(`[${entry.at}] ${entry.kind}: ${entry.message}`)}</div>`);
1799
- if (entry.message === "tool.blocked" && entry.data?.permissionAdvice) {
1800
- parts.push(renderPermissionApproval(entry));
1801
- continue;
1802
- }
1803
1925
  if (entry.data && Object.keys(entry.data).length > 0) {
1804
1926
  parts.push(`<pre class="log-json">${escapeHtml(JSON.stringify(entry.data, null, 2))}</pre>`);
1805
1927
  }
@@ -2044,6 +2166,14 @@ function renderChat(chatEntries) {
2044
2166
 
2045
2167
  chatThreadEl.innerHTML = lastChatEntries
2046
2168
  .map((entry) => {
2169
+ if (entry.role === "event") {
2170
+ return renderStructuredEvent(entry) || `
2171
+ <article class="chat-item event" data-event-type="${escapeHtml(entry.eventType || "")}">
2172
+ <div class="chat-meta">${escapeHtml(entry.eventLabel || entry.eventType || "event")}${entry.at ? ` · ${new Date(entry.at).toLocaleString()}` : ""}</div>
2173
+ <div class="chat-content">${escapeHtml(entry.content || "").replace(/\n/g, "<br>")}</div>
2174
+ </article>
2175
+ `;
2176
+ }
2047
2177
  const role = entry.role === "assistant" ? "assistant" : "user";
2048
2178
  const label = role === "assistant" ? t("assistantLabel") : t("youLabel");
2049
2179
  const content =
@@ -2830,7 +2960,7 @@ async function refreshChat() {
2830
2960
  pendingInboxItems = data.inbox || [];
2831
2961
  pendingAfterFinishItems = loadAfterFinishQueue(currentSessionId);
2832
2962
  renderPendingMessages();
2833
- renderChat(data.chat || []);
2963
+ renderChat(data.timeline || data.chat || []);
2834
2964
  }
2835
2965
 
2836
2966
  async function savePreferences() {
package/public/styles.css CHANGED
@@ -1225,6 +1225,126 @@ button.danger {
1225
1225
  padding: 0 12px 8px;
1226
1226
  }
1227
1227
 
1228
+ .event-card {
1229
+ width: auto;
1230
+ max-width: 100%;
1231
+ margin: 0 0 12px;
1232
+ padding: 12px;
1233
+ border: 1px solid rgba(20, 184, 166, 0.22);
1234
+ border-radius: 16px;
1235
+ background:
1236
+ radial-gradient(circle at 0% 0%, rgba(20, 184, 166, 0.14), transparent 28%),
1237
+ rgba(255, 255, 255, 0.46);
1238
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.07);
1239
+ }
1240
+
1241
+ #logs .event-card {
1242
+ background:
1243
+ radial-gradient(circle at 0% 0%, rgba(20, 184, 166, 0.1), transparent 26%),
1244
+ rgba(15, 23, 42, 0.52);
1245
+ color: #e5e7eb;
1246
+ }
1247
+
1248
+ .event-plan {
1249
+ border-color: rgba(20, 184, 166, 0.34);
1250
+ background:
1251
+ radial-gradient(circle at 0% 0%, rgba(20, 184, 166, 0.18), transparent 28%),
1252
+ linear-gradient(180deg, rgba(240, 253, 250, 0.9), rgba(255, 251, 235, 0.74));
1253
+ }
1254
+
1255
+ #logs .event-plan {
1256
+ background: rgba(15, 23, 42, 0.58);
1257
+ }
1258
+
1259
+ .event-patch {
1260
+ border-color: rgba(192, 132, 252, 0.38);
1261
+ }
1262
+
1263
+ .event-write {
1264
+ border-color: rgba(59, 130, 246, 0.28);
1265
+ }
1266
+
1267
+ .event-failed {
1268
+ border-color: rgba(248, 113, 113, 0.45);
1269
+ background: rgba(127, 29, 29, 0.12);
1270
+ }
1271
+
1272
+ #logs .event-failed {
1273
+ background: rgba(127, 29, 29, 0.3);
1274
+ }
1275
+
1276
+ .event-muted {
1277
+ border-color: rgba(148, 163, 184, 0.24);
1278
+ background: rgba(148, 163, 184, 0.1);
1279
+ }
1280
+
1281
+ .event-card-meta {
1282
+ display: flex;
1283
+ flex-wrap: wrap;
1284
+ gap: 8px;
1285
+ margin-bottom: 8px;
1286
+ color: var(--muted);
1287
+ font-size: 0.78rem;
1288
+ font-weight: 800;
1289
+ letter-spacing: 0.04em;
1290
+ text-transform: uppercase;
1291
+ }
1292
+
1293
+ #logs .event-card-meta {
1294
+ color: #99f6e4;
1295
+ }
1296
+
1297
+ .event-path {
1298
+ display: block;
1299
+ margin: 4px 0 8px;
1300
+ font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
1301
+ font-size: 0.84rem;
1302
+ overflow-wrap: anywhere;
1303
+ }
1304
+
1305
+ .event-inline-code {
1306
+ display: block;
1307
+ width: fit-content;
1308
+ max-width: 100%;
1309
+ margin: 4px 0 8px;
1310
+ padding: 6px 8px;
1311
+ border-radius: 8px;
1312
+ background: rgba(15, 23, 42, 0.08);
1313
+ color: #78350f;
1314
+ font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
1315
+ overflow-wrap: anywhere;
1316
+ white-space: pre-wrap;
1317
+ }
1318
+
1319
+ #logs .event-inline-code {
1320
+ background: rgba(255, 255, 255, 0.08);
1321
+ color: #fef3c7;
1322
+ }
1323
+
1324
+ .event-output {
1325
+ margin: 4px 0 10px;
1326
+ padding: 10px;
1327
+ border-radius: 10px;
1328
+ background: rgba(15, 23, 42, 0.84);
1329
+ color: #e5e7eb;
1330
+ white-space: pre-wrap;
1331
+ overflow: auto;
1332
+ }
1333
+
1334
+ .event-fold-note {
1335
+ margin: 4px 0 8px;
1336
+ color: var(--muted);
1337
+ font-size: 0.8rem;
1338
+ }
1339
+
1340
+ #logs .event-fold-note {
1341
+ color: #94a3b8;
1342
+ }
1343
+
1344
+ .event-diff {
1345
+ max-height: 360px;
1346
+ }
1347
+
1228
1348
  .chat-panel {
1229
1349
  display: grid;
1230
1350
  gap: 12px;
@@ -1271,6 +1391,25 @@ button.danger {
1271
1391
  border-bottom-left-radius: 6px;
1272
1392
  }
1273
1393
 
1394
+ .chat-item.event {
1395
+ justify-self: stretch;
1396
+ width: auto;
1397
+ max-width: 100%;
1398
+ border-color: rgba(20, 184, 166, 0.28);
1399
+ border-style: dashed;
1400
+ background:
1401
+ linear-gradient(90deg, rgba(20, 184, 166, 0.12), rgba(245, 158, 11, 0.08)),
1402
+ rgba(255, 255, 255, 0.48);
1403
+ box-shadow: none;
1404
+ }
1405
+
1406
+ .chat-item.event[data-event-type="plan.created"] {
1407
+ border-style: solid;
1408
+ background:
1409
+ radial-gradient(circle at 0% 0%, rgba(20, 184, 166, 0.16), transparent 28%),
1410
+ linear-gradient(180deg, rgba(240, 253, 250, 0.88), rgba(255, 251, 235, 0.7));
1411
+ }
1412
+
1274
1413
  .chat-meta {
1275
1414
  font-size: 0.8rem;
1276
1415
  color: var(--muted);
@@ -890,10 +890,16 @@ try {
890
890
  if (!latest.stdout.includes(" user>") || !latest.stdout.includes("aginti>") || latest.stdout.includes("user> ")) {
891
891
  throw new Error("resume history should use prompt-style user>/aginti> labels");
892
892
  }
893
- if (!latest.stdout.includes("resume note=showing chat transcript only")) {
894
- throw new Error("bare aginti resume did not clarify that tool/run events are separate from chat history");
893
+ if (!latest.stdout.includes(" plan ")) {
894
+ throw new Error("bare aginti resume did not replay saved plan/run context");
895
895
  }
896
- if (latest.stdout.includes("showing=") || latest.stdout.includes("")) {
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 (latest.stdout.includes("resume note=showing chat transcript only")) {
900
+ throw new Error("bare aginti resume regressed to chat-only history");
901
+ }
902
+ if (latest.stdout.includes("showing=")) {
897
903
  throw new Error("resume history should render full saved messages instead of compact previews");
898
904
  }
899
905
 
@@ -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",
@@ -303,6 +308,15 @@ try {
303
308
  const chat = await fetchJson(`/api/sessions/${encodeURIComponent(runStart.sessionId)}/chat`);
304
309
  if (!Array.isArray(chat.chat) || chat.chat.length < 2) throw new Error("chat history was not persisted");
305
310
  if (!Array.isArray(chat.inbox)) throw new Error("chat endpoint did not include shared inbox state");
311
+ if (!Array.isArray(chat.events) || !chat.events.some((entry) => entry.type === "plan.created")) {
312
+ throw new Error("chat endpoint did not include saved plan events");
313
+ }
314
+ if (
315
+ !Array.isArray(chat.timeline) ||
316
+ !chat.timeline.some((entry) => entry.role === "event" && entry.eventType === "plan.created" && String(entry.content || "").trim())
317
+ ) {
318
+ throw new Error("chat endpoint did not return a resume-ready timeline with the saved plan");
319
+ }
306
320
 
307
321
  const queued = await fetchJson(`/api/sessions/${encodeURIComponent(runStart.sessionId)}/inbox`, {
308
322
  method: "POST",
@@ -56,7 +56,7 @@ async function waitForRunState(page, status, timeout = 20000) {
56
56
  );
57
57
  }
58
58
 
59
- async function waitForTerminalRunState(page, timeout = 30000) {
59
+ async function waitForTerminalRunState(page, timeout = 90000) {
60
60
  await page.waitForFunction(
61
61
  () => ["finished", "failed", "stopped"].includes(document.querySelector("#run-state")?.dataset.status || ""),
62
62
  null,
@@ -92,6 +92,7 @@ try {
92
92
  if (await page.locator("#run-defaults-card").evaluate((node) => node.open)) throw new Error("run defaults should start folded");
93
93
  await page.locator("#commandCwd").fill(runtimeDir.slice(0, Math.max(runtimeDir.lastIndexOf("/"), 1)));
94
94
  await page.waitForFunction(() => document.querySelectorAll("#command-cwd-suggestions option").length > 0);
95
+ await page.locator("#commandCwd").fill(runtimeDir);
95
96
  if ((await page.locator(".project-status-chip").count()) < 4) throw new Error("project folder status did not render structured chips");
96
97
  await page.locator("#run-defaults-card summary").click();
97
98
  if (await page.locator("#veniceModeToggle").isChecked()) throw new Error("Venice quick mode should default off");
@@ -143,6 +144,14 @@ try {
143
144
  await page.selectOption("#routingMode", "manual");
144
145
  await page.selectOption("#provider", "mock");
145
146
  await page.selectOption("#model", "mock-agent");
147
+ await page.selectOption("#enableScs", "off");
148
+ if (await page.locator("#aapsModeToggle").isChecked()) await page.locator("label:has(#aapsModeToggle)").click();
149
+ await page.selectOption("#taskProfile", "auto");
150
+ await page.evaluate(() => {
151
+ const input = document.querySelector("#allowShellTool");
152
+ input.checked = false;
153
+ input.dispatchEvent(new Event("change", { bubbles: true }));
154
+ });
146
155
  await page.fill("#maxSteps", "4");
147
156
  await page.selectOption("#dynamicSteps", "off");
148
157
  await page.fill("#chat-input", "Say hello from the web UI composer in one concise sentence.");
@@ -151,18 +160,24 @@ try {
151
160
  if (firstPayload.goal !== "Say hello from the web UI composer in one concise sentence.") {
152
161
  throw new Error("composer text was not used as the first run goal");
153
162
  }
154
- if (firstPayload.enableScs !== "auto" || firstPayload.taskProfile !== "aaps") {
163
+ if (firstPayload.enableScs !== "off" || firstPayload.taskProfile !== "auto") {
155
164
  throw new Error(`CLI mode payload mismatch: ${runPayloads.at(-1) || ""}`);
156
165
  }
157
166
  if (firstPayload.dynamicSteps !== "off") {
158
167
  throw new Error(`dynamic steps payload mismatch: ${runPayloads.at(-1) || ""}`);
159
168
  }
169
+ if (firstPayload.allowShellTool !== false) {
170
+ throw new Error(`shell tool should be disabled for deterministic renderer smoke: ${runPayloads.at(-1) || ""}`);
171
+ }
160
172
  if (firstPayload.veniceMode !== false || firstPayload.routeProvider !== "deepseek" || firstPayload.mainProvider !== "deepseek") {
161
173
  throw new Error(`default route/main payload mismatch after Venice off: ${runPayloads.at(-1) || ""}`);
162
174
  }
163
175
  await waitForRunState(page, "running");
176
+ await page.waitForSelector(".event-plan", { timeout: 12000 });
177
+ if (!(await page.locator(".event-plan .markdown-body").first().innerText()).trim()) {
178
+ throw new Error("web run log did not render the plan as a formatted event card");
179
+ }
164
180
  if (!(await page.locator("#stop-run").isVisible())) throw new Error("stop button did not appear while run was active");
165
- await page.click("#stop-run");
166
181
  await waitForTerminalRunState(page);
167
182
  if (await page.locator("#stop-run").isVisible()) throw new Error("stop button stayed visible after terminal run state");
168
183
  if (!(await page.locator(".toast").first().isVisible().catch(() => false))) {
@@ -173,6 +188,27 @@ try {
173
188
  await waitForRunState(page, "idle");
174
189
  const resetSubmit = await page.locator("#chat-submit").innerText();
175
190
  if (!/start new run/i.test(resetSubmit)) throw new Error("new session did not reset composer to start mode");
191
+ if (await page.locator("#aapsModeToggle").isChecked()) await page.locator("label:has(#aapsModeToggle)").click();
192
+ await page.selectOption("#taskProfile", "code");
193
+ await page.selectOption("#enableScs", "off");
194
+ await page.fill("#chat-input", "Create notes/web-ui-format.md with a short formatted event smoke message.");
195
+ await page.click("#chat-submit");
196
+ await waitForTerminalRunState(page);
197
+ try {
198
+ await page.waitForFunction(
199
+ () =>
200
+ [...document.querySelectorAll(".event-write .change-diff")].some((node) =>
201
+ (node.textContent || "").includes("+Created by AgInTiFlow mock mode.")
202
+ ),
203
+ null,
204
+ { timeout: 30000 }
205
+ );
206
+ } catch (error) {
207
+ const state = await page.locator("#run-state").evaluate((node) => node.dataset.status || "").catch(() => "unknown");
208
+ const logsTail = (await page.locator("#logs").innerText().catch(() => "")).slice(-1200);
209
+ const chatTail = (await page.locator("#chat-thread").innerText().catch(() => "")).slice(-1200);
210
+ throw new Error(`web UI did not render formatted file-change diff event; state=${state}\nlogs tail:\n${logsTail}\nchat tail:\n${chatTail}`);
211
+ }
176
212
 
177
213
  await page.click("#open-settings");
178
214
  await page.waitForSelector("#settings-modal[open]");
@@ -208,6 +244,8 @@ try {
208
244
  "running-status-toast",
209
245
  "terminal-stop-button-hidden",
210
246
  "new-session-resets-scope",
247
+ "formatted-plan-event-card",
248
+ "formatted-file-diff-event-card",
211
249
  "settings-provider-dropdowns",
212
250
  "settings-wrapper-dropdowns",
213
251
  ],
@@ -51,6 +51,26 @@ async function killPort(port) {
51
51
  }
52
52
  }
53
53
 
54
+ async function stopChild(child) {
55
+ if (!child || child.exitCode !== null) return;
56
+ try {
57
+ child.stdin?.write("/exit\n");
58
+ } catch {
59
+ // The CLI may have closed stdin after the final prompt; fall back to process signals.
60
+ }
61
+ const exited = await Promise.race([
62
+ new Promise((resolve) => child.once("exit", () => resolve(true))),
63
+ delay(2000).then(() => false),
64
+ ]);
65
+ if (exited || child.exitCode !== null) return;
66
+ child.kill("SIGTERM");
67
+ const terminated = await Promise.race([
68
+ new Promise((resolve) => child.once("exit", () => resolve(true))),
69
+ delay(2000).then(() => false),
70
+ ]);
71
+ if (!terminated && child.exitCode === null) child.kill("SIGKILL");
72
+ }
73
+
54
74
  async function runCase({ port, env = {}, expectHeader, label }) {
55
75
  const output = { stdout: "", stderr: "" };
56
76
  const child = spawn(process.execPath, [path.join(repoRoot, "bin/aginti-cli.js"), "chat", "--provider", "mock", "--routing", "manual", "--port", String(port)], {
@@ -109,8 +129,9 @@ async function runCase({ port, env = {}, expectHeader, label }) {
109
129
  child.stdin.write(`/webapp ${active.port}\n`);
110
130
  await waitFor(() => latestWebappEvent(output.stdout, "started|reused")?.port === active.port, child, `${label} /webapp restart after stop`, output);
111
131
  } finally {
112
- child.kill("SIGTERM");
132
+ await stopChild(child);
113
133
  await killPort(port);
134
+ await delay(250);
114
135
  }
115
136
  }
116
137
 
@@ -133,5 +154,6 @@ try {
133
154
  } finally {
134
155
  await killPort(autoPort);
135
156
  await killPort(manualPort);
136
- await fs.rm(runtimeDir, { recursive: true, force: true });
157
+ await delay(250);
158
+ await fs.rm(runtimeDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
137
159
  }
@@ -2207,6 +2207,131 @@ function printHistoryEntry(entry) {
2207
2207
  printHistoryBlock(role, entry.content, { time, bg });
2208
2208
  }
2209
2209
 
2210
+ function timestampMs(value = "") {
2211
+ const parsed = Date.parse(String(value || ""));
2212
+ return Number.isFinite(parsed) ? parsed : 0;
2213
+ }
2214
+
2215
+ function isReplayableResumeEvent(event = {}) {
2216
+ const type = String(event.type || "");
2217
+ const data = event.data || {};
2218
+ if (type === "plan.created") return Boolean(data.plan);
2219
+ if (
2220
+ [
2221
+ "budget.initialized",
2222
+ "conversation.continued",
2223
+ "conversation.queued_input_applied",
2224
+ "file.changed",
2225
+ "session.failed",
2226
+ "session.finished",
2227
+ "session.stopped",
2228
+ "tool.blocked",
2229
+ "tool.failed",
2230
+ "tool.skipped",
2231
+ "tool.started",
2232
+ ].includes(type)
2233
+ ) {
2234
+ return true;
2235
+ }
2236
+ if (type === "tool.completed") {
2237
+ const toolName = String(data.toolName || "");
2238
+ return ["run_command", "write_file", "apply_patch", "create_artifact"].includes(toolName) || data.ok === false || data.blocked || data.error;
2239
+ }
2240
+ return false;
2241
+ }
2242
+
2243
+ function resumeTimelineItems(chat = [], events = [], { limit = 0 } = {}) {
2244
+ const shownChat = limit > 0 ? chat.slice(-limit) : chat;
2245
+ 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
2253
+ .map((event, index) => ({ event, index }))
2254
+ .filter(({ event }) => isReplayableResumeEvent(event))
2255
+ .filter(({ event }) => !firstChatTime || timestampMs(event.timestamp) >= firstChatTime)
2256
+ .map(({ event, index }) => ({
2257
+ kind: "event",
2258
+ event,
2259
+ order: index * 2 + 1,
2260
+ at: timestampMs(event.timestamp),
2261
+ }));
2262
+ return [...chatItems, ...eventItems].sort((left, right) => (left.at || 0) - (right.at || 0) || left.order - right.order);
2263
+ }
2264
+
2265
+ function printResumeEvent(event = {}) {
2266
+ const type = String(event.type || "");
2267
+ const data = event.data || {};
2268
+ if (type === "plan.created") {
2269
+ printWrapped(`${label("plan", ansi.systemBg)} `, data.plan || "");
2270
+ return true;
2271
+ }
2272
+ if (type === "tool.started") {
2273
+ outputLine(`${label("tool", ansi.statusBg)} ${toolStatusDetails(data)}`);
2274
+ return true;
2275
+ }
2276
+ if (type === "tool.completed" || type === "tool.failed" || type === "tool.skipped") {
2277
+ const failed = type === "tool.failed" || data.ok === false || data.blocked || data.error;
2278
+ if (data.diff) {
2279
+ printWorkspaceChange(data);
2280
+ return true;
2281
+ }
2282
+ if (data.toolName === "run_command") {
2283
+ printCommandOutputLog({
2284
+ command: data.args?.command || data.command || "",
2285
+ stdout: data.stdout || "",
2286
+ stderr: data.stderr || "",
2287
+ diagnosticHint: data.diagnosticHint || "",
2288
+ commandPolicy: data.commandPolicy,
2289
+ blocked: Boolean(data.blocked),
2290
+ error: data.error || data.reason || "",
2291
+ permissionAdvice: data.permissionAdvice || null,
2292
+ });
2293
+ }
2294
+ outputLine(`${label(failed ? "fail" : "done", failed ? ansi.red : ansi.systemBg)} ${toolStatusDetails(data)}`);
2295
+ return true;
2296
+ }
2297
+ if (type === "tool.blocked") {
2298
+ outputLine(`${label("perm", ansi.red)} ${compactLine(data.reason || data.permissionAdvice?.summary || data.toolName || "Permission blocked.", 104)}`);
2299
+ if (data.permissionAdvice?.suggestedCommand) outputLine(`${color(" | ", ansi.red)} rerun: ${compactLine(data.permissionAdvice.suggestedCommand, 120)}`);
2300
+ if (data.permissionAdvice?.trustedHostCommand) outputLine(`${color(" | ", ansi.red)} host: ${compactLine(data.permissionAdvice.trustedHostCommand, 120)}`);
2301
+ return true;
2302
+ }
2303
+ if (type === "file.changed") {
2304
+ if (data.diff) printWorkspaceChange(data);
2305
+ else outputLine(`${label("write", ansi.systemBg)} ${compactLine(formatWorkspaceChange(data).summary, 92)}`);
2306
+ return true;
2307
+ }
2308
+ if (type === "budget.initialized") {
2309
+ outputLine(`${label("state", ansi.systemBg)} budget=${data.currentMaxSteps || data.initialMaxSteps || data.maxSteps || "unknown"} steps`);
2310
+ return true;
2311
+ }
2312
+ if (type === "conversation.continued") {
2313
+ outputLine(`${label("state", ansi.systemBg)} continued=${compactLine(data.prompt || "", 76)}`);
2314
+ return true;
2315
+ }
2316
+ if (type === "conversation.queued_input_applied") {
2317
+ outputLine(`${label("state", ansi.systemBg)} queued_input_applied=${data.priority === "asap" ? "asap" : "normal"}`);
2318
+ return true;
2319
+ }
2320
+ if (type === "session.finished") {
2321
+ outputLine(`${label("state", ansi.systemBg)} status=finished${data.result ? ` result=${compactLine(data.result, 86)}` : ""}`);
2322
+ return true;
2323
+ }
2324
+ if (type === "session.failed") {
2325
+ outputLine(`${label("state", ansi.red)} status=failed${data.error ? ` error=${compactLine(data.error, 86)}` : ""}`);
2326
+ return true;
2327
+ }
2328
+ if (type === "session.stopped") {
2329
+ outputLine(`${label("state", ansi.systemBg)} status=stopped${data.reason ? ` reason=${compactLine(data.reason, 86)}` : ""}`);
2330
+ return true;
2331
+ }
2332
+ return false;
2333
+ }
2334
+
2210
2335
  async function printResumeHistory(state, { limit = 0 } = {}) {
2211
2336
  if (!state.sessionId) return;
2212
2337
  const paths = projectPaths(process.cwd());
@@ -2215,23 +2340,24 @@ async function printResumeHistory(state, { limit = 0 } = {}) {
2215
2340
  const events = await store.loadEvents().catch(() => []);
2216
2341
  const chat = Array.isArray(saved?.chat) ? saved.chat.filter((entry) => entry?.content) : [];
2217
2342
  promptHistory.seedFromChat(chat, state.sessionId);
2218
- const metadata = `chat=${chat.length}${events.length > 0 ? ` events=${events.length}` : ""}`;
2219
- if (chat.length === 0) {
2343
+ const timeline = resumeTimelineItems(chat, events, { limit });
2344
+ const replayedEvents = timeline.filter((item) => item.kind === "event").length;
2345
+ const metadata = `chat=${chat.length}${events.length > 0 ? ` events=${events.length}` : ""}${replayedEvents > 0 ? ` replay=${replayedEvents}` : ""}`;
2346
+ if (timeline.length === 0) {
2220
2347
  printSystemLine(`resume history session=${state.sessionId} ${metadata}`);
2221
- if (events.length > 0) {
2222
- printSystemLine("resume note=showing chat transcript only; model/tool/run events are saved in events.jsonl and artifacts/");
2223
- }
2224
2348
  return;
2225
2349
  }
2226
2350
 
2227
- const shown = limit > 0 ? chat.slice(-limit) : chat;
2228
2351
  printSystemLine(
2229
- `resume history session=${state.sessionId} ${metadata}${limit > 0 ? ` showing=${shown.length}/${chat.length}` : ""}`
2352
+ `resume history session=${state.sessionId} ${metadata}${limit > 0 ? ` showing=${timeline.length}/${chat.length + events.length}` : ""}`
2230
2353
  );
2231
- if (events.length > chat.length) {
2232
- printSystemLine("resume note=showing chat transcript only; model/tool/run events are saved in events.jsonl and artifacts/");
2354
+ if (events.length > replayedEvents) {
2355
+ printSystemLine("resume note=showing chat plus key run events; full raw events remain in events.jsonl and artifacts/.");
2356
+ }
2357
+ for (const item of timeline) {
2358
+ if (item.kind === "chat") printHistoryEntry(item.entry);
2359
+ else printResumeEvent(item.event);
2233
2360
  }
2234
- for (const entry of shown) printHistoryEntry(entry);
2235
2361
  }
2236
2362
 
2237
2363
  function toolStatusDetails(data = {}) {
package/web.js CHANGED
@@ -102,6 +102,140 @@ function mapEventLogs(events) {
102
102
  }));
103
103
  }
104
104
 
105
+ function compactText(value = "", limit = 120) {
106
+ const text = String(value || "").replace(/\s+/g, " ").trim();
107
+ return text.length <= limit ? text : `${text.slice(0, Math.max(limit - 1, 1)).trim()}...`;
108
+ }
109
+
110
+ function toolTimelineSummary(data = {}) {
111
+ const tool = data.toolName || "unknown";
112
+ const args = data.args || {};
113
+ if (tool === "run_command" && args.command) return `${tool}: ${compactText(args.command, 92)}`;
114
+ if ((tool === "write_file" || tool === "apply_patch" || tool === "read_file" || tool === "open_workspace_file") && args.path) {
115
+ return `${tool}: ${compactText(args.path, 92)}`;
116
+ }
117
+ if ((tool === "open_url" || tool === "web_research" || tool === "web_search") && (args.url || args.query || args.q)) {
118
+ return `${tool}: ${compactText(args.url || args.query || args.q, 92)}`;
119
+ }
120
+ return tool;
121
+ }
122
+
123
+ function timelineEntryForEvent(event = {}) {
124
+ const type = String(event.type || "");
125
+ const data = event.data || {};
126
+ const at = event.timestamp || "";
127
+ if (type === "plan.created" && data.plan) {
128
+ return {
129
+ role: "event",
130
+ eventType: type,
131
+ eventLabel: "plan",
132
+ data,
133
+ content: data.plan,
134
+ markdown: true,
135
+ at,
136
+ };
137
+ }
138
+ if (type === "tool.started") {
139
+ return {
140
+ role: "event",
141
+ eventType: type,
142
+ eventLabel: "tool",
143
+ data,
144
+ content: toolTimelineSummary(data),
145
+ at,
146
+ };
147
+ }
148
+ if (type === "tool.completed" || type === "tool.failed" || type === "tool.skipped") {
149
+ const failed = type === "tool.failed" || data.ok === false || data.blocked || data.error;
150
+ const stdout = data.toolName === "run_command" && data.stdout ? `\nstdout: ${compactText(data.stdout, 180)}` : "";
151
+ const stderr = data.toolName === "run_command" && data.stderr ? `\nstderr: ${compactText(data.stderr, 180)}` : "";
152
+ return {
153
+ role: "event",
154
+ eventType: type,
155
+ eventLabel: failed ? "tool failed" : "tool done",
156
+ data,
157
+ content: `${toolTimelineSummary(data)}${stdout}${stderr}`,
158
+ at,
159
+ };
160
+ }
161
+ if (type === "tool.blocked") {
162
+ return {
163
+ role: "event",
164
+ eventType: type,
165
+ eventLabel: "permission",
166
+ data,
167
+ content: data.permissionAdvice?.summary || data.reason || data.toolName || "Permission blocked.",
168
+ at,
169
+ };
170
+ }
171
+ if (type === "file.changed") {
172
+ return {
173
+ role: "event",
174
+ eventType: type,
175
+ eventLabel: data.toolName === "apply_patch" ? "patch" : "write",
176
+ data,
177
+ content: [data.toolName || data.action || "file.changed", data.path || "", data.created ? "created" : "updated"]
178
+ .filter(Boolean)
179
+ .join(" "),
180
+ at,
181
+ };
182
+ }
183
+ if (type === "budget.initialized") {
184
+ return {
185
+ role: "event",
186
+ eventType: type,
187
+ eventLabel: "budget",
188
+ data,
189
+ content: `${data.currentMaxSteps || data.initialMaxSteps || data.maxSteps || "unknown"} steps`,
190
+ at,
191
+ };
192
+ }
193
+ if (type === "conversation.continued") {
194
+ return {
195
+ role: "event",
196
+ eventType: type,
197
+ eventLabel: "continued",
198
+ data,
199
+ content: data.prompt || "",
200
+ at,
201
+ };
202
+ }
203
+ if (type === "conversation.queued_input_applied") {
204
+ return {
205
+ role: "event",
206
+ eventType: type,
207
+ eventLabel: "queued input",
208
+ data,
209
+ content: data.priority === "asap" ? "ASAP queued input applied" : "Queued input applied",
210
+ at,
211
+ };
212
+ }
213
+ if (type === "session.finished" || type === "session.failed" || type === "session.stopped") {
214
+ return {
215
+ role: "event",
216
+ eventType: type,
217
+ eventLabel: type.replace("session.", ""),
218
+ data,
219
+ content: data.result || data.error || data.reason || type,
220
+ at,
221
+ };
222
+ }
223
+ return null;
224
+ }
225
+
226
+ function sessionTimelineFromChatAndEvents(chat = [], events = []) {
227
+ const chatItems = (Array.isArray(chat) ? chat : [])
228
+ .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 : [])
231
+ .map(timelineEntryForEvent)
232
+ .filter(Boolean)
233
+ .map((entry, index) => ({ ...entry, order: index * 2 + 1, sortAt: Date.parse(entry.at || "") || 0 }));
234
+ return [...chatItems, ...eventItems]
235
+ .sort((left, right) => (left.sortAt || 0) - (right.sortAt || 0) || left.order - right.order)
236
+ .map(({ sortAt: _sortAt, order: _order, ...entry }) => entry);
237
+ }
238
+
105
239
  async function loadStoredRun(sessionId) {
106
240
  const meta = db.getSession(sessionId);
107
241
  if (!meta) return null;
@@ -606,11 +740,16 @@ function deriveChatFromState(state, meta) {
606
740
 
607
741
  async function loadChat(sessionId) {
608
742
  const store = sessionStore(sessionId);
609
- const [state, meta] = await Promise.all([store.loadState(), Promise.resolve(db.getSession(sessionId))]);
743
+ const [state, meta, events] = await Promise.all([
744
+ store.loadState(),
745
+ Promise.resolve(db.getSession(sessionId)),
746
+ store.loadEvents().catch(() => []),
747
+ ]);
610
748
 
611
749
  if (!state && !meta) return null;
612
750
 
613
751
  const inbox = await store.loadInbox().catch(() => []);
752
+ const chat = deriveChatFromState(state, meta);
614
753
  return {
615
754
  sessionId,
616
755
  goal: state?.goal || meta?.goal || "",
@@ -619,7 +758,9 @@ async function loadChat(sessionId) {
619
758
  model: state?.model || meta?.model || "",
620
759
  status: meta?.status || "",
621
760
  inbox,
622
- chat: deriveChatFromState(state, meta),
761
+ chat,
762
+ events: events.slice(-120),
763
+ timeline: sessionTimelineFromChatAndEvents(chat, events).slice(-220),
623
764
  };
624
765
  }
625
766