openvibers/openvibe.actor Commits
Show AG-UI run steps on Actor task pages (#432)
Changes
nnew file mode 100644BBinary files /dev/null and b/docs/screenshots/ask-354-1280.png differnnew file mode 100644BBinary files /dev/null and b/docs/screenshots/ask-354-390.png differ@@@ -17,7 +17,7 @@"iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.403.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.147.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.204.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.205.0","pg": "^8.23.0","sharp": "^0.34.5"},@@@ -1756,9 +1756,9 @@}},"node_modules/openvibe-shared": {-- "version": "4.204.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.204.0",-- "integrity": "sha512-T6xQehJHnzHdQFQrs8J44vT7jcziKC8kRpn+3VxyLxZvCcM1QZ6jv5LFCPUNYhppvwtpu4fPc7fmvrgfXjdQ6Q==",++ "version": "4.205.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.205.0",++ "integrity": "sha512-Ch4rkxgGhjsGrdFDsJlZ6lzRrtH90MLdi00Huw7nJB8SDt7D8S3wpnxgOs5eQYx4wLHcFDWRmCKQ5FlVzsCaWA==","license": "MIT","dependencies": {"clean-css": "^5.3.3",@@@ -30,7 +30,7 @@"iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.403.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.147.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.204.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.205.0","pg": "^8.23.0","sharp": "^0.34.5"},@@@ -17,6 +17,46 @@return n;}++ function agentRun(art, host, oldView) {++ if (!window.EventSource || !window.OpenVibeAgentSession) return;++ var id = art.getAttribute('data-task');++ var url = '/api/v1/runs/' + encodeURIComponent(id);++ fetch(url, { credentials: 'same-origin' }).then(function (r) { return r.ok ? r.json() : null; }).then(function (run) {++ if (!run || !host.isConnected) return;++ oldView.destroy();++ host.textContent = '';++ var state = run.status || 'running';++ var view = window.OpenVibeAgentSession.mount(host, {++ title: run.title || 'Task', status: state, follow: art.getAttribute('data-open') === '1', files: false,++ onSend: art.getAttribute('data-owner') === '1' && ['queued', 'running', 'verifying', 'succeeded', 'needs_you'].indexOf(state) !== -1 ? function (text) {++ var working = ['queued', 'running', 'verifying'].indexOf(state) !== -1;++ var action = working ? '/chat/' + encodeURIComponent(id) + '/steer'++ : '/tasks/' + encodeURIComponent(id) + '/' + (state === 'needs_you' ? 'answer' : 'follow-up');++ var body = new URLSearchParams(); body.set(state === 'needs_you' ? 'answer' : 'task', text);++ return fetch(action, {++ method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' }, body: body.toString()++ }).then(function (r) {++ if (!r.ok) throw new Error('The message could not be sent.');++ if (!working) window.location.assign(r.url);++ });++ } : undefined++ });++ art.dataset.agentUi = '1';++ var source = new EventSource(url + '/events');++ ['RUN_STARTED', 'RUN_FINISHED', 'RUN_ERROR', 'STEP_STARTED', 'STEP_FINISHED', 'TEXT_MESSAGE_START',++ 'TEXT_MESSAGE_CONTENT', 'TEXT_MESSAGE_END', 'TOOL_CALL_START', 'TOOL_CALL_ARGS', 'TOOL_CALL_END',++ 'TOOL_CALL_RESULT', 'REASONING_START', 'REASONING_MESSAGE_START', 'REASONING_MESSAGE_CONTENT',++ 'REASONING_MESSAGE_END', 'REASONING_END', 'REASONING_ENCRYPTED_VALUE', 'STATE_SNAPSHOT', 'STATE_DELTA',++ 'ACTIVITY_SNAPSHOT', 'ACTIVITY_DELTA', 'CUSTOM'].forEach(function (type) {++ source.addEventListener(type, function (e) {++ try { view.push(JSON.parse(e.data)); } catch (err) { /* a malformed event is skipped */ }++ if (type === 'RUN_FINISHED' || type === 'RUN_ERROR') source.close();++ });++ });++ window.addEventListener('pagehide', function () { source.close(); }, { once: true });++ }).catch(function () { /* the task event view stays usable */ });++ }++// The session view over How it went: the events the page carries, then (while open) the live ones.function session(art) {var data = document.getElementById('task-events');@@@ -40,6 +80,7 @@var startedAt = open && window.OpenVibeTaskEvents ? window.OpenVibeTaskEvents.firstAt(events) : 0;var view = window.OpenVibeAgentSession.mount(host, { title: (document.querySelector('.task-text') || {}).textContent || 'Task', status: open ? 'running' : state, startedAt: startedAt || undefined, follow: open, empty: 'Waiting for an agent to start…', link: function (u) { return ownDesk.test(u); } });view.pushAll(lines);++ agentRun(art, host, view);return view;}@@@ -220,7 +261,7 @@onDesk(e);onLook(e);if (e.step === 'plan') paintPlan(e.text);-- if (view) { view.push(attach(e)); return undefined; }++ if (view) { if (art.dataset.agentUi !== '1') view.push(attach(e)); return undefined; }if ((e.step === 'plan' || e.step === 'handoff') && e.agent) return attempt((e.text || e.agent).replace(/ takes it.*$/, ''));if (e.step === 'plan') return undefined;if (e.step === 'check') {@@@ -238,7 +279,7 @@var src = new EventSource('/api/v1/tasks/' + encodeURIComponent(id) + '/events?after=' + encodeURIComponent(art.getAttribute('data-last') || '0'));src.addEventListener('output', function (m) { try { onOutput(JSON.parse(m.data)); } catch (err) { /* a malformed event is skipped */ } });src.addEventListener('state', function (m) {-- try { var ev = JSON.parse(m.data), s = ev.state; if (liveState && STATE[s]) liveState.textContent = STATE[s] + '…'; if (view) view.push(ev); } catch (err) { /* ignore */ }++ try { var ev = JSON.parse(m.data), s = ev.state; if (liveState && STATE[s]) liveState.textContent = STATE[s] + '…'; if (view && art.dataset.agentUi !== '1') view.push(ev); } catch (err) { /* ignore */ }fetch('/api/v1/tasks/' + encodeURIComponent(id), { credentials: 'same-origin' }).then(function (r) { return r.ok ? r.json() : null; }).then(function (t) {if (t && t.cost && cost) cost.textContent = cost.textContent.replace(/^\$[0-9.]+/, '$' + (t.cost.usd < 0.01 ? t.cost.usd.toFixed(4) : t.cost.usd.toFixed(2)));}).catch(function () {});@@@ -1100,13 +1100,13 @@ ${existing ? html`<div class="notice" role="status">Already offered ${time(existcrumbs: [{ label: 'Your tasks', href: '/tasks' }, { label: 'Task' }],// How it went is the agents' session (openvibe-shared/agent-session.js, js/task.js mounts it from the events// below and, while the task runs, from its live stream); the attempts drawn here stay for a page without scripts.-- scripts: waiting ? [] : ['js/task-events.js', 'js/task.js'],-- sharedScripts: [...(waiting ? [] : ['agent-session.js']), 'ov-share.js'],++ scripts: ['js/task-events.js', 'js/task.js'],++ sharedScripts: ['agent-session.js', 'ov-share.js'],styles: [showcase.STYLESHEET, 'ov-share.css'],css: ['css/task.css'],noReferrer: true,body: html`${live ? raw('<noscript><meta http-equiv="refresh" content="4"></noscript>') : ''}--<article class="task state-${t.state}" data-task="${row.id}" data-last="${row.last_seq}" data-open="${live ? '1' : ''}" data-bot="${botPages}">++<article class="task state-${t.state}" data-task="${row.id}" data-last="${row.last_seq}" data-open="${live ? '1' : ''}" data-owner="${shared ? '0' : '1'}" data-bot="${botPages}"><header class="task-head"><p class="task-status"><span class="pill pill-${t.state}">${STATE_TEXT[t.state]}</span>${kind ? html`<span class="chip">${kind}</span>` : ''}<span class="chip">${MODE_LABEL[t.mode]}</span><span class="chip" id="task-cost">${t.cost ? usd(t.cost.usd) : '$0'} of ${usd(t.budget.per_task_usd)}</span>${time(t.created_at)}</p>${parentTask ? html`<p class="follows-on">${row.fork_of_step != null@@@ -1134,7 +1134,8 @@ ${open || look ? html`<figure class="last-look" id="last-look"${look ? '' : raw(<a id="last-look-link" href="${look ? `/tasks/${row.id}/look?at=${look.at}` : '#'}" target="_blank" rel="noopener" title="Open it full size"><img id="last-look-img" src="${look ? `/tasks/${row.id}/look?at=${look.at}` : ''}" alt="The screen your agent last looked at"${look && look.width ? raw(` width="${Number(look.width)}" height="${Number(look.height)}"`) : ''} loading="lazy"></a><figcaption class="muted small">What your agent last saw on the screen${look ? html` · ${time(new Date(look.at).toISOString())}` : ''}</figcaption></figure>` : ''}--${waiting ? html`<p class="muted small live-note" id="waiting-note">Waiting for your answer.${actingCard ? '' : ' The task keeps its place and its budget while you decide.'}</p>` : html`<section class="how" aria-labelledby="h-how"><h2 id="h-how">How it went</h2>++${waiting ? html`<p class="muted small live-note" id="waiting-note">Waiting for your answer.${actingCard ? '' : ' The task keeps its place and its budget while you decide.'}</p>++<section class="how" aria-labelledby="h-how"><h2 id="h-how">How it went</h2><div class="attempts" id="attempts">${attemptsOf(events).map(attemptBlock)}</div><script type="application/json" id="task-events">${raw(JSON.stringify(events.slice(-3000)).replace(/</g, '\\u003c'))}</script></section>` : html`<section class="how" aria-labelledby="h-how"><h2 id="h-how">How it went</h2>${!open && plan ? planCard(plan) : ''}<div class="attempts" id="attempts">${attemptsOf(events).map(attemptBlock)}</div>${traceBlock(spans, row.id)}
new file mode 100644
new file mode 100644
Binary files /dev/null and b/docs/screenshots/ask-354-1280.png differ
Binary files /dev/null and b/docs/screenshots/ask-354-1280.png differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/docs/screenshots/ask-354-390.png differ
Binary files /dev/null and b/docs/screenshots/ask-354-390.png differ
@@ -17,7 +17,7 @@
@@ -17,7 +17,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.403.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.403.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.147.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.147.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.204.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.205.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
"sharp": "^0.34.5"
"sharp": "^0.34.5"
},
},
@@ -1756,9 +1756,9 @@
@@ -1756,9 +1756,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.204.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.204.0",
- "integrity": "sha512-T6xQehJHnzHdQFQrs8J44vT7jcziKC8kRpn+3VxyLxZvCcM1QZ6jv5LFCPUNYhppvwtpu4fPc7fmvrgfXjdQ6Q==",
+ "version": "4.205.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.205.0",
+ "integrity": "sha512-Ch4rkxgGhjsGrdFDsJlZ6lzRrtH90MLdi00Huw7nJB8SDt7D8S3wpnxgOs5eQYx4wLHcFDWRmCKQ5FlVzsCaWA==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"clean-css": "^5.3.3",
"clean-css": "^5.3.3",
@@ -30,7 +30,7 @@
@@ -30,7 +30,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.403.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.403.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.147.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.147.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.204.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.205.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
"sharp": "^0.34.5"
"sharp": "^0.34.5"
},
},
@@ -17,6 +17,46 @@
@@ -17,6 +17,46 @@
return n;
return n;
}
}
+ function agentRun(art, host, oldView) {
+ if (!window.EventSource || !window.OpenVibeAgentSession) return;
+ var id = art.getAttribute('data-task');
+ var url = '/api/v1/runs/' + encodeURIComponent(id);
+ fetch(url, { credentials: 'same-origin' }).then(function (r) { return r.ok ? r.json() : null; }).then(function (run) {
+ if (!run || !host.isConnected) return;
+ oldView.destroy();
+ host.textContent = '';
+ var state = run.status || 'running';
+ var view = window.OpenVibeAgentSession.mount(host, {
+ title: run.title || 'Task', status: state, follow: art.getAttribute('data-open') === '1', files: false,
+ onSend: art.getAttribute('data-owner') === '1' && ['queued', 'running', 'verifying', 'succeeded', 'needs_you'].indexOf(state) !== -1 ? function (text) {
+ var working = ['queued', 'running', 'verifying'].indexOf(state) !== -1;
+ var action = working ? '/chat/' + encodeURIComponent(id) + '/steer'
+ : '/tasks/' + encodeURIComponent(id) + '/' + (state === 'needs_you' ? 'answer' : 'follow-up');
+ var body = new URLSearchParams(); body.set(state === 'needs_you' ? 'answer' : 'task', text);
+ return fetch(action, {
+ method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' }, body: body.toString()
+ }).then(function (r) {
+ if (!r.ok) throw new Error('The message could not be sent.');
+ if (!working) window.location.assign(r.url);
+ });
+ } : undefined
+ });
+ art.dataset.agentUi = '1';
+ var source = new EventSource(url + '/events');
+ ['RUN_STARTED', 'RUN_FINISHED', 'RUN_ERROR', 'STEP_STARTED', 'STEP_FINISHED', 'TEXT_MESSAGE_START',
+ 'TEXT_MESSAGE_CONTENT', 'TEXT_MESSAGE_END', 'TOOL_CALL_START', 'TOOL_CALL_ARGS', 'TOOL_CALL_END',
+ 'TOOL_CALL_RESULT', 'REASONING_START', 'REASONING_MESSAGE_START', 'REASONING_MESSAGE_CONTENT',
+ 'REASONING_MESSAGE_END', 'REASONING_END', 'REASONING_ENCRYPTED_VALUE', 'STATE_SNAPSHOT', 'STATE_DELTA',
+ 'ACTIVITY_SNAPSHOT', 'ACTIVITY_DELTA', 'CUSTOM'].forEach(function (type) {
+ source.addEventListener(type, function (e) {
+ try { view.push(JSON.parse(e.data)); } catch (err) { /* a malformed event is skipped */ }
+ if (type === 'RUN_FINISHED' || type === 'RUN_ERROR') source.close();
+ });
+ });
+ window.addEventListener('pagehide', function () { source.close(); }, { once: true });
+ }).catch(function () { /* the task event view stays usable */ });
+ }
+
// The session view over How it went: the events the page carries, then (while open) the live ones.
// The session view over How it went: the events the page carries, then (while open) the live ones.
function session(art) {
function session(art) {
var data = document.getElementById('task-events');
var data = document.getElementById('task-events');
@@ -40,6 +80,7 @@
@@ -40,6 +80,7 @@
var startedAt = open && window.OpenVibeTaskEvents ? window.OpenVibeTaskEvents.firstAt(events) : 0;
var startedAt = open && window.OpenVibeTaskEvents ? window.OpenVibeTaskEvents.firstAt(events) : 0;
var view = window.OpenVibeAgentSession.mount(host, { title: (document.querySelector('.task-text') || {}).textContent || 'Task', status: open ? 'running' : state, startedAt: startedAt || undefined, follow: open, empty: 'Waiting for an agent to start…', link: function (u) { return ownDesk.test(u); } });
var view = window.OpenVibeAgentSession.mount(host, { title: (document.querySelector('.task-text') || {}).textContent || 'Task', status: open ? 'running' : state, startedAt: startedAt || undefined, follow: open, empty: 'Waiting for an agent to start…', link: function (u) { return ownDesk.test(u); } });
view.pushAll(lines);
view.pushAll(lines);
+ agentRun(art, host, view);
return view;
return view;
}
}
@@ -220,7 +261,7 @@
@@ -220,7 +261,7 @@
onDesk(e);
onDesk(e);
onLook(e);
onLook(e);
if (e.step === 'plan') paintPlan(e.text);
if (e.step === 'plan') paintPlan(e.text);
- if (view) { view.push(attach(e)); return undefined; }
+ if (view) { if (art.dataset.agentUi !== '1') view.push(attach(e)); return undefined; }
if ((e.step === 'plan' || e.step === 'handoff') && e.agent) return attempt((e.text || e.agent).replace(/ takes it.*$/, ''));
if ((e.step === 'plan' || e.step === 'handoff') && e.agent) return attempt((e.text || e.agent).replace(/ takes it.*$/, ''));
if (e.step === 'plan') return undefined;
if (e.step === 'plan') return undefined;
if (e.step === 'check') {
if (e.step === 'check') {
@@ -238,7 +279,7 @@
@@ -238,7 +279,7 @@
var src = new EventSource('/api/v1/tasks/' + encodeURIComponent(id) + '/events?after=' + encodeURIComponent(art.getAttribute('data-last') || '0'));
var src = new EventSource('/api/v1/tasks/' + encodeURIComponent(id) + '/events?after=' + encodeURIComponent(art.getAttribute('data-last') || '0'));
src.addEventListener('output', function (m) { try { onOutput(JSON.parse(m.data)); } catch (err) { /* a malformed event is skipped */ } });
src.addEventListener('output', function (m) { try { onOutput(JSON.parse(m.data)); } catch (err) { /* a malformed event is skipped */ } });
src.addEventListener('state', function (m) {
src.addEventListener('state', function (m) {
- try { var ev = JSON.parse(m.data), s = ev.state; if (liveState && STATE[s]) liveState.textContent = STATE[s] + '…'; if (view) view.push(ev); } catch (err) { /* ignore */ }
+ try { var ev = JSON.parse(m.data), s = ev.state; if (liveState && STATE[s]) liveState.textContent = STATE[s] + '…'; if (view && art.dataset.agentUi !== '1') view.push(ev); } catch (err) { /* ignore */ }
fetch('/api/v1/tasks/' + encodeURIComponent(id), { credentials: 'same-origin' }).then(function (r) { return r.ok ? r.json() : null; }).then(function (t) {
fetch('/api/v1/tasks/' + encodeURIComponent(id), { credentials: 'same-origin' }).then(function (r) { return r.ok ? r.json() : null; }).then(function (t) {
if (t && t.cost && cost) cost.textContent = cost.textContent.replace(/^\$[0-9.]+/, '$' + (t.cost.usd < 0.01 ? t.cost.usd.toFixed(4) : t.cost.usd.toFixed(2)));
if (t && t.cost && cost) cost.textContent = cost.textContent.replace(/^\$[0-9.]+/, '$' + (t.cost.usd < 0.01 ? t.cost.usd.toFixed(4) : t.cost.usd.toFixed(2)));
}).catch(function () {});
}).catch(function () {});
@@ -1100,13 +1100,13 @@ ${existing ? html`<div class="notice" role="status">Already offered ${time(exist
@@ -1100,13 +1100,13 @@ ${existing ? html`<div class="notice" role="status">Already offered ${time(exist
crumbs: [{ label: 'Your tasks', href: '/tasks' }, { label: 'Task' }],
crumbs: [{ label: 'Your tasks', href: '/tasks' }, { label: 'Task' }],
// How it went is the agents' session (openvibe-shared/agent-session.js, js/task.js mounts it from the events
// How it went is the agents' session (openvibe-shared/agent-session.js, js/task.js mounts it from the events
// below and, while the task runs, from its live stream); the attempts drawn here stay for a page without scripts.
// below and, while the task runs, from its live stream); the attempts drawn here stay for a page without scripts.
- scripts: waiting ? [] : ['js/task-events.js', 'js/task.js'],
- sharedScripts: [...(waiting ? [] : ['agent-session.js']), 'ov-share.js'],
+ scripts: ['js/task-events.js', 'js/task.js'],
+ sharedScripts: ['agent-session.js', 'ov-share.js'],
styles: [showcase.STYLESHEET, 'ov-share.css'],
styles: [showcase.STYLESHEET, 'ov-share.css'],
css: ['css/task.css'],
css: ['css/task.css'],
noReferrer: true,
noReferrer: true,
body: html`${live ? raw('<noscript><meta http-equiv="refresh" content="4"></noscript>') : ''}
body: html`${live ? raw('<noscript><meta http-equiv="refresh" content="4"></noscript>') : ''}
-<article class="task state-${t.state}" data-task="${row.id}" data-last="${row.last_seq}" data-open="${live ? '1' : ''}" data-bot="${botPages}">
+<article class="task state-${t.state}" data-task="${row.id}" data-last="${row.last_seq}" data-open="${live ? '1' : ''}" data-owner="${shared ? '0' : '1'}" data-bot="${botPages}">
<header class="task-head">
<header class="task-head">
<p class="task-status"><span class="pill pill-${t.state}">${STATE_TEXT[t.state]}</span>${kind ? html`<span class="chip">${kind}</span>` : ''}<span class="chip">${MODE_LABEL[t.mode]}</span><span class="chip" id="task-cost">${t.cost ? usd(t.cost.usd) : '$0'} of ${usd(t.budget.per_task_usd)}</span>${time(t.created_at)}</p>
<p class="task-status"><span class="pill pill-${t.state}">${STATE_TEXT[t.state]}</span>${kind ? html`<span class="chip">${kind}</span>` : ''}<span class="chip">${MODE_LABEL[t.mode]}</span><span class="chip" id="task-cost">${t.cost ? usd(t.cost.usd) : '$0'} of ${usd(t.budget.per_task_usd)}</span>${time(t.created_at)}</p>
${parentTask ? html`<p class="follows-on">${row.fork_of_step != null
${parentTask ? html`<p class="follows-on">${row.fork_of_step != null
@@ -1134,7 +1134,8 @@ ${open || look ? html`<figure class="last-look" id="last-look"${look ? '' : raw(
@@ -1134,7 +1134,8 @@ ${open || look ? html`<figure class="last-look" id="last-look"${look ? '' : raw(
<a id="last-look-link" href="${look ? `/tasks/${row.id}/look?at=${look.at}` : '#'}" target="_blank" rel="noopener" title="Open it full size"><img id="last-look-img" src="${look ? `/tasks/${row.id}/look?at=${look.at}` : ''}" alt="The screen your agent last looked at"${look && look.width ? raw(` width="${Number(look.width)}" height="${Number(look.height)}"`) : ''} loading="lazy"></a>
<a id="last-look-link" href="${look ? `/tasks/${row.id}/look?at=${look.at}` : '#'}" target="_blank" rel="noopener" title="Open it full size"><img id="last-look-img" src="${look ? `/tasks/${row.id}/look?at=${look.at}` : ''}" alt="The screen your agent last looked at"${look && look.width ? raw(` width="${Number(look.width)}" height="${Number(look.height)}"`) : ''} loading="lazy"></a>
<figcaption class="muted small">What your agent last saw on the screen${look ? html` · ${time(new Date(look.at).toISOString())}` : ''}</figcaption>
<figcaption class="muted small">What your agent last saw on the screen${look ? html` · ${time(new Date(look.at).toISOString())}` : ''}</figcaption>
</figure>` : ''}
</figure>` : ''}
-${waiting ? html`<p class="muted small live-note" id="waiting-note">Waiting for your answer.${actingCard ? '' : ' The task keeps its place and its budget while you decide.'}</p>` : html`<section class="how" aria-labelledby="h-how"><h2 id="h-how">How it went</h2>
+${waiting ? html`<p class="muted small live-note" id="waiting-note">Waiting for your answer.${actingCard ? '' : ' The task keeps its place and its budget while you decide.'}</p>
+<section class="how" aria-labelledby="h-how"><h2 id="h-how">How it went</h2><div class="attempts" id="attempts">${attemptsOf(events).map(attemptBlock)}</div><script type="application/json" id="task-events">${raw(JSON.stringify(events.slice(-3000)).replace(/</g, '\\u003c'))}</script></section>` : html`<section class="how" aria-labelledby="h-how"><h2 id="h-how">How it went</h2>
${!open && plan ? planCard(plan) : ''}
${!open && plan ? planCard(plan) : ''}
<div class="attempts" id="attempts">${attemptsOf(events).map(attemptBlock)}</div>
<div class="attempts" id="attempts">${attemptsOf(events).map(attemptBlock)}</div>
${traceBlock(spans, row.id)}
${traceBlock(spans, row.id)}