openvibers/openvibe.codes Commits
Show AG-UI run steps on Codes task pages (#405)
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.399.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.202.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.205.0","pg": "^8.23.0"},"bin": {@@@ -1274,9 +1274,9 @@}},"node_modules/openvibe-shared": {-- "version": "4.202.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.202.0",-- "integrity": "sha512-lSZavwvUc8nI7eS7ZzLjVaz2d2xVmvYGh81H83d+i1LPp7cbS7eK9HCiQS8ESK9N8eH0i/Ok+sqBFxeUwsnVPQ==",++ "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",@@@ -32,7 +32,7 @@"iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.399.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.202.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.205.0","pg": "^8.23.0"},"engines": {@@@ -227,6 +227,35 @@const label = root.querySelector('[data-live-label]');const live = root.dataset.c4Conductor === 'live';const url = root.dataset.c4ConductorStream;++ const taskId = root.dataset.task;++ if (taskId && window.OpenVibeAgentSession && window.EventSource) {++ const runUrl = '/api/v1/runs/' + encodeURIComponent(taskId);++ fetch(runUrl, { credentials: 'same-origin' }).then((r) => r.ok ? r.json() : null).then((run) => {++ if (!run || !root.isConnected) return;++ const host = document.createElement('div');++ root.appendChild(host);++ if (out) out.hidden = true;++ if (label) label.hidden = true;++ const form = document.querySelector('[data-c4-conductor-form]');++ if (form) form.hidden = true;++ const view = window.OpenVibeAgentSession.mount(host, {++ title: run.title || 'Conductor', status: run.status || 'running', follow: live, files: false,++ onSend: (text) => fetch('/api/v1/conductor/' + encodeURIComponent(taskId) + '/messages', {++ method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text })++ }).then((r) => { if (!r.ok) throw new Error('The message could not be sent.'); window.location.reload(); })++ });++ const stream = new EventSource(runUrl + '/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((type) => stream.addEventListener(type, (e) => {++ try { view.push(JSON.parse(e.data)); } catch { /* ignore one invalid event */ }++ if (type === 'RUN_FINISHED' || type === 'RUN_ERROR') stream.close();++ }));++ window.addEventListener('pagehide', () => stream.close(), { once: true });++ }).catch(() => { /* the plain transcript and composer remain */ });++ }if (!live || !window.EventSource || !url) return; // between turns the lines are drawn on the serverconst stream = new EventSource(url);const textOf = (line) => {@@@ -35,6 +35,41 @@}}++ // AG-UI is the task's durable run timeline. EventSource keeps the last SSE id on reconnect,++ // so a dropped connection resumes without repeating steps or losing streamed text.++ function agentRun(section, host, oldView) {++ var id = (section.getAttribute('data-run-id') || '').trim();++ if (!id || !window.OpenVibeAgentSession || !window.EventSource) return;++ 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;++ if (oldView) oldView.destroy();++ host.textContent = '';++ var view = window.OpenVibeAgentSession.mount(host, {++ title: run.title || section.getAttribute('data-title') || 'Task', status: run.status || 'running',++ follow: section.hasAttribute('data-events'), files: false,++ onSend: section.getAttribute('data-owner') === '1' && !section.hasAttribute('data-events') ? function (text) {++ return fetch('/api/v1/tasks/' + encodeURIComponent(id) + '/continue', {++ method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ note: text })++ }).then(function (r) { if (!r.ok) throw new Error('The task could not take the message yet.'); });++ } : undefined++ });++ section.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) { /* one malformed event cannot stop the run */ }++ if (type === 'RUN_FINISHED' || type === 'RUN_ERROR') source.close();++ });++ });++ window.addEventListener('pagehide', function () { source.close(); }, { once: true });++ }).catch(function () { /* the saved session remains available */ });++ }++function follow(section) {var log = section.querySelector('[data-log]');var pill = document.querySelector('[data-state]');@@@ -46,6 +81,7 @@log.parentNode.replaceChild(host, log);view = window.OpenVibeAgentSession.mount(host, { title: section.getAttribute('data-title') || 'Task', status: 'queued', empty: 'Waiting for your machine to take the task…' });log = null;++ agentRun(section, host, view);}var source = new EventSource(section.getAttribute('data-events'));source.addEventListener('output', function (e) {@@@ -58,7 +94,7 @@if (!raw.trim()) return;var doc;try { doc = JSON.parse(raw); } catch (err) { doc = null; }-- if (view) { if (doc) view.push(doc); return; }++ if (view) { if (doc && section.dataset.agentUi !== '1') view.push(doc); return; }if (doc) show(log, doc); else line(log, 'tool', raw);});});@@@ -67,7 +103,7 @@var s = JSON.parse(e.data).state;if (LABEL[s]) lastState = s;if (pill && LABEL[s]) { pill.textContent = LABEL[s]; pill.className = 'run-pill run-pill-' + s; }-- if (view) view.status(s, LABEL[s]);++ if (view && section.dataset.agentUi !== '1') view.status(s, LABEL[s]);} catch (err) { /* ignore */ }});var lastState = section.getAttribute('data-state') || 'queued';@@@ -75,7 +111,7 @@source.close();// Switch from live streaming to replay without a full reload: the session lines are read back from// /run/tasks/:id/session.json and pushed into the same agent-session view.-- if (view && window.fetch) {++ if (view && window.fetch && section.dataset.agentUi !== '1') {var sessionUrl = section.getAttribute('data-events').replace('/events', '/session.json') + '?after=0';fetch(sessionUrl, { credentials: 'same-origin' }).then(function (r) { return r.json(); })@@@ -118,6 +154,7 @@var view = window.OpenVibeAgentSession.mount(into, { title: section.getAttribute('data-title') || 'Task', status: 'running', follow: false });view.pushAll(lines);view.status(state, LABEL[state]);++ agentRun(section, into, view);}document.querySelectorAll('[data-events]').forEach(follow);@@@ -379,7 +379,7 @@ ${pr.problem ? `<p class="c4-hint">${esc(pr.problem.detail)}</p>` : `<dl class="const transcript = `<section class="c4-card c4-transcript" data-c4-conductor-stream="${esc(`${api}/events?last_event_id=${lastSeq}`)}" data-task="${esc(row.id)}" data-started-at="${esc(row.started_at || row.created_at || '')}" data-c4-conductor="${live ? 'live' : 'idle'}"><div class="c4-card-head"><h2>${live ? 'This turn, live' : 'The last turn'}</h2><span class="c4-muted" data-live-label>${live ? 'Connecting…' : 'Nothing is running'}</span></div><pre class="c4-log" data-task-log aria-live="polite" tabindex="0">${esc(lastLines)}</pre></section>`;const composer = row.outcome === 'stopped' ? '' : `<section class="c4-card c4-conductor-composer"><form method="post" action="/tasks/${encodeURIComponent(row.id)}/messages" enctype="multipart/form-data" data-c4-conductor-form data-conductor-api="/api/v1/conductor/${encodeURIComponent(row.id)}/messages"><label for="c4-message">${live ? 'Say something: it is the next turn' : 'What next?'}</label><textarea id="c4-message" name="text" rows="3" placeholder="Tell your conductor what to do next, or ask what it is doing"></textarea><div class="c4-conductor-actions"><label class="c4-file">Attach files<input type="file" name="files" multiple data-c4-images></label><button class="c4-button" type="submit">Send</button></div><p class="c4-hint" data-c4-message-status role="status">${live ? 'A message waits until the current turn ends; it is not lost.' : ''}</p></form></section>`;const content = `${outcome}<section class="c4-card c4-conductor-head"><div class="c4-task-heading"><div class="c4-task-title-wrap"><h1>Conductor</h1></div><span class="c4-status c4-${esc(row.state)}">${esc(conductorState(row))}</span></div><div class="c4-chips" aria-label="Conductor details"><span class="c4-chip">${esc(row.folder || 'a folder')}</span><span class="c4-chip">${esc(row.agent === 'claude-code' ? modelLabel(row.model) : (row.model || 'its default model'))}</span><span class="c4-chip">safety: ${esc(row.safety || 'ask')}</span><span class="c4-chip">${esc(machineLabel || 'your machine')}</span></div><p class="c4-hint">One long session on your own machine, on the agent you chose and your own login. Each message is the next turn of the same conversation; it hands the work to your other agents through the Codes tools.</p></section>${approvals}${usageCard}<section class="c4-card c4-turns"><div class="c4-card-head"><h2>Turns</h2><span class="c4-muted">${(view.messages || []).length} so far</span></div>${turns ? `<ol class="c4-turn-list">${turns}</ol>` : '<div class="c4-emptyline">Its first turn is starting.</div>'}</section>${transcript}${composer}`;-- page(req, res, 'Conductor', wrap(content));++ page(req, res, 'Conductor', wrap(content), { sharedScripts: ['agent-session.js'] });}// ── The front door (a visitor): what Codes is, shown rather than told ──@@@ -338,7 +338,7 @@ ${i.summary ? html`<p class="run-proof-summary" data-proof-summary>${i.summary}<// after the end from codes_session_lines; a task from before this change falls back to the result's old log.const lines = live ? null : await sessionStore.linesForRow(row);const title = String(row.prompt || '').split('\n')[0].slice(0, 90);-- const session = lines && lines.length ? html`<section class="run-session" data-session data-title="${title}" data-state="${row.state}"><h2>The session</h2><div data-session-view></div><script type="application/json" data-session-log>${raw(JSON.stringify(lines).replace(/</g, '\\u003c'))}</script></section>` : '';++ const session = lines && lines.length ? html`<section class="run-session" data-session data-run-id="${row.id}" data-owner="${mine ? '1' : '0'}" data-title="${title}" data-state="${row.state}"><h2>The session</h2><div data-session-view></div><script type="application/json" data-session-log>${raw(JSON.stringify(lines).replace(/</g, '\\u003c'))}</script></section>` : '';const body = html`${shareHead ? raw(shareHead) : ''}<p class="crumbs"><a href="/run">Run locally</a></p><h1 class="run-repo">${repoName(row.repo)}${row.ref ? html` <small>${row.ref}</small>` : ''}</h1><p><span class="run-pill run-pill-${row.state}" data-state>${STATE_LABEL[row.state] || row.state}</span>${row.agent ? html` <span class="run-pill">${row.agent}</span>` : ''}</p>@@@ -347,7 +347,7 @@ ${mine ? shareButton(row) : html`<p class="muted">This task was shared with you.${await timelineHtml(row)}${goalHtml(row)}${await proofHtml(row)}--${live ? html`<section class="run-live" data-events="/run/tasks/${row.id}/events" data-title="${title}" aria-live="polite"><h2>Live</h2><ol class="run-log" data-log></ol></section>++${live ? html`<section class="run-live" data-events="/run/tasks/${row.id}/events" data-run-id="${row.id}" data-owner="${mine ? '1' : '0'}" data-title="${title}" aria-live="polite"><h2>Live</h2><ol class="run-log" data-log></ol></section>${mine ? html`<form method="post" action="/run/tasks/${row.id}/cancel"><button class="run-btn" type="submit">Cancel</button></form>` : ''}` : html`${session}${resultHtml(row, { session: !!(lines && lines.length) })}`}`;return page(req, res, { title: 'Task', body, sharedScripts: ['agent-session.js'] });});
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.399.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.399.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.202.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"
},
},
"bin": {
"bin": {
@@ -1274,9 +1274,9 @@
@@ -1274,9 +1274,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.202.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.202.0",
- "integrity": "sha512-lSZavwvUc8nI7eS7ZzLjVaz2d2xVmvYGh81H83d+i1LPp7cbS7eK9HCiQS8ESK9N8eH0i/Ok+sqBFxeUwsnVPQ==",
+ "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",
@@ -32,7 +32,7 @@
@@ -32,7 +32,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.399.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.399.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.202.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"
},
},
"engines": {
"engines": {
@@ -227,6 +227,35 @@
@@ -227,6 +227,35 @@
const label = root.querySelector('[data-live-label]');
const label = root.querySelector('[data-live-label]');
const live = root.dataset.c4Conductor === 'live';
const live = root.dataset.c4Conductor === 'live';
const url = root.dataset.c4ConductorStream;
const url = root.dataset.c4ConductorStream;
+ const taskId = root.dataset.task;
+ if (taskId && window.OpenVibeAgentSession && window.EventSource) {
+ const runUrl = '/api/v1/runs/' + encodeURIComponent(taskId);
+ fetch(runUrl, { credentials: 'same-origin' }).then((r) => r.ok ? r.json() : null).then((run) => {
+ if (!run || !root.isConnected) return;
+ const host = document.createElement('div');
+ root.appendChild(host);
+ if (out) out.hidden = true;
+ if (label) label.hidden = true;
+ const form = document.querySelector('[data-c4-conductor-form]');
+ if (form) form.hidden = true;
+ const view = window.OpenVibeAgentSession.mount(host, {
+ title: run.title || 'Conductor', status: run.status || 'running', follow: live, files: false,
+ onSend: (text) => fetch('/api/v1/conductor/' + encodeURIComponent(taskId) + '/messages', {
+ method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text })
+ }).then((r) => { if (!r.ok) throw new Error('The message could not be sent.'); window.location.reload(); })
+ });
+ const stream = new EventSource(runUrl + '/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((type) => stream.addEventListener(type, (e) => {
+ try { view.push(JSON.parse(e.data)); } catch { /* ignore one invalid event */ }
+ if (type === 'RUN_FINISHED' || type === 'RUN_ERROR') stream.close();
+ }));
+ window.addEventListener('pagehide', () => stream.close(), { once: true });
+ }).catch(() => { /* the plain transcript and composer remain */ });
+ }
if (!live || !window.EventSource || !url) return; // between turns the lines are drawn on the server
if (!live || !window.EventSource || !url) return; // between turns the lines are drawn on the server
const stream = new EventSource(url);
const stream = new EventSource(url);
const textOf = (line) => {
const textOf = (line) => {
@@ -35,6 +35,41 @@
@@ -35,6 +35,41 @@
}
}
}
}
+ // AG-UI is the task's durable run timeline. EventSource keeps the last SSE id on reconnect,
+ // so a dropped connection resumes without repeating steps or losing streamed text.
+ function agentRun(section, host, oldView) {
+ var id = (section.getAttribute('data-run-id') || '').trim();
+ if (!id || !window.OpenVibeAgentSession || !window.EventSource) return;
+ 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;
+ if (oldView) oldView.destroy();
+ host.textContent = '';
+ var view = window.OpenVibeAgentSession.mount(host, {
+ title: run.title || section.getAttribute('data-title') || 'Task', status: run.status || 'running',
+ follow: section.hasAttribute('data-events'), files: false,
+ onSend: section.getAttribute('data-owner') === '1' && !section.hasAttribute('data-events') ? function (text) {
+ return fetch('/api/v1/tasks/' + encodeURIComponent(id) + '/continue', {
+ method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ note: text })
+ }).then(function (r) { if (!r.ok) throw new Error('The task could not take the message yet.'); });
+ } : undefined
+ });
+ section.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) { /* one malformed event cannot stop the run */ }
+ if (type === 'RUN_FINISHED' || type === 'RUN_ERROR') source.close();
+ });
+ });
+ window.addEventListener('pagehide', function () { source.close(); }, { once: true });
+ }).catch(function () { /* the saved session remains available */ });
+ }
+
function follow(section) {
function follow(section) {
var log = section.querySelector('[data-log]');
var log = section.querySelector('[data-log]');
var pill = document.querySelector('[data-state]');
var pill = document.querySelector('[data-state]');
@@ -46,6 +81,7 @@
@@ -46,6 +81,7 @@
log.parentNode.replaceChild(host, log);
log.parentNode.replaceChild(host, log);
view = window.OpenVibeAgentSession.mount(host, { title: section.getAttribute('data-title') || 'Task', status: 'queued', empty: 'Waiting for your machine to take the task…' });
view = window.OpenVibeAgentSession.mount(host, { title: section.getAttribute('data-title') || 'Task', status: 'queued', empty: 'Waiting for your machine to take the task…' });
log = null;
log = null;
+ agentRun(section, host, view);
}
}
var source = new EventSource(section.getAttribute('data-events'));
var source = new EventSource(section.getAttribute('data-events'));
source.addEventListener('output', function (e) {
source.addEventListener('output', function (e) {
@@ -58,7 +94,7 @@
@@ -58,7 +94,7 @@
if (!raw.trim()) return;
if (!raw.trim()) return;
var doc;
var doc;
try { doc = JSON.parse(raw); } catch (err) { doc = null; }
try { doc = JSON.parse(raw); } catch (err) { doc = null; }
- if (view) { if (doc) view.push(doc); return; }
+ if (view) { if (doc && section.dataset.agentUi !== '1') view.push(doc); return; }
if (doc) show(log, doc); else line(log, 'tool', raw);
if (doc) show(log, doc); else line(log, 'tool', raw);
});
});
});
});
@@ -67,7 +103,7 @@
@@ -67,7 +103,7 @@
var s = JSON.parse(e.data).state;
var s = JSON.parse(e.data).state;
if (LABEL[s]) lastState = s;
if (LABEL[s]) lastState = s;
if (pill && LABEL[s]) { pill.textContent = LABEL[s]; pill.className = 'run-pill run-pill-' + s; }
if (pill && LABEL[s]) { pill.textContent = LABEL[s]; pill.className = 'run-pill run-pill-' + s; }
- if (view) view.status(s, LABEL[s]);
+ if (view && section.dataset.agentUi !== '1') view.status(s, LABEL[s]);
} catch (err) { /* ignore */ }
} catch (err) { /* ignore */ }
});
});
var lastState = section.getAttribute('data-state') || 'queued';
var lastState = section.getAttribute('data-state') || 'queued';
@@ -75,7 +111,7 @@
@@ -75,7 +111,7 @@
source.close();
source.close();
// Switch from live streaming to replay without a full reload: the session lines are read back from
// Switch from live streaming to replay without a full reload: the session lines are read back from
// /run/tasks/:id/session.json and pushed into the same agent-session view.
// /run/tasks/:id/session.json and pushed into the same agent-session view.
- if (view && window.fetch) {
+ if (view && window.fetch && section.dataset.agentUi !== '1') {
var sessionUrl = section.getAttribute('data-events').replace('/events', '/session.json') + '?after=0';
var sessionUrl = section.getAttribute('data-events').replace('/events', '/session.json') + '?after=0';
fetch(sessionUrl, { credentials: 'same-origin' })
fetch(sessionUrl, { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (r) { return r.json(); })
@@ -118,6 +154,7 @@
@@ -118,6 +154,7 @@
var view = window.OpenVibeAgentSession.mount(into, { title: section.getAttribute('data-title') || 'Task', status: 'running', follow: false });
var view = window.OpenVibeAgentSession.mount(into, { title: section.getAttribute('data-title') || 'Task', status: 'running', follow: false });
view.pushAll(lines);
view.pushAll(lines);
view.status(state, LABEL[state]);
view.status(state, LABEL[state]);
+ agentRun(section, into, view);
}
}
document.querySelectorAll('[data-events]').forEach(follow);
document.querySelectorAll('[data-events]').forEach(follow);
@@ -379,7 +379,7 @@ ${pr.problem ? `<p class="c4-hint">${esc(pr.problem.detail)}</p>` : `<dl class="
@@ -379,7 +379,7 @@ ${pr.problem ? `<p class="c4-hint">${esc(pr.problem.detail)}</p>` : `<dl class="
const transcript = `<section class="c4-card c4-transcript" data-c4-conductor-stream="${esc(`${api}/events?last_event_id=${lastSeq}`)}" data-task="${esc(row.id)}" data-started-at="${esc(row.started_at || row.created_at || '')}" data-c4-conductor="${live ? 'live' : 'idle'}"><div class="c4-card-head"><h2>${live ? 'This turn, live' : 'The last turn'}</h2><span class="c4-muted" data-live-label>${live ? 'Connecting…' : 'Nothing is running'}</span></div><pre class="c4-log" data-task-log aria-live="polite" tabindex="0">${esc(lastLines)}</pre></section>`;
const transcript = `<section class="c4-card c4-transcript" data-c4-conductor-stream="${esc(`${api}/events?last_event_id=${lastSeq}`)}" data-task="${esc(row.id)}" data-started-at="${esc(row.started_at || row.created_at || '')}" data-c4-conductor="${live ? 'live' : 'idle'}"><div class="c4-card-head"><h2>${live ? 'This turn, live' : 'The last turn'}</h2><span class="c4-muted" data-live-label>${live ? 'Connecting…' : 'Nothing is running'}</span></div><pre class="c4-log" data-task-log aria-live="polite" tabindex="0">${esc(lastLines)}</pre></section>`;
const composer = row.outcome === 'stopped' ? '' : `<section class="c4-card c4-conductor-composer"><form method="post" action="/tasks/${encodeURIComponent(row.id)}/messages" enctype="multipart/form-data" data-c4-conductor-form data-conductor-api="/api/v1/conductor/${encodeURIComponent(row.id)}/messages"><label for="c4-message">${live ? 'Say something: it is the next turn' : 'What next?'}</label><textarea id="c4-message" name="text" rows="3" placeholder="Tell your conductor what to do next, or ask what it is doing"></textarea><div class="c4-conductor-actions"><label class="c4-file">Attach files<input type="file" name="files" multiple data-c4-images></label><button class="c4-button" type="submit">Send</button></div><p class="c4-hint" data-c4-message-status role="status">${live ? 'A message waits until the current turn ends; it is not lost.' : ''}</p></form></section>`;
const composer = row.outcome === 'stopped' ? '' : `<section class="c4-card c4-conductor-composer"><form method="post" action="/tasks/${encodeURIComponent(row.id)}/messages" enctype="multipart/form-data" data-c4-conductor-form data-conductor-api="/api/v1/conductor/${encodeURIComponent(row.id)}/messages"><label for="c4-message">${live ? 'Say something: it is the next turn' : 'What next?'}</label><textarea id="c4-message" name="text" rows="3" placeholder="Tell your conductor what to do next, or ask what it is doing"></textarea><div class="c4-conductor-actions"><label class="c4-file">Attach files<input type="file" name="files" multiple data-c4-images></label><button class="c4-button" type="submit">Send</button></div><p class="c4-hint" data-c4-message-status role="status">${live ? 'A message waits until the current turn ends; it is not lost.' : ''}</p></form></section>`;
const content = `${outcome}<section class="c4-card c4-conductor-head"><div class="c4-task-heading"><div class="c4-task-title-wrap"><h1>Conductor</h1></div><span class="c4-status c4-${esc(row.state)}">${esc(conductorState(row))}</span></div><div class="c4-chips" aria-label="Conductor details"><span class="c4-chip">${esc(row.folder || 'a folder')}</span><span class="c4-chip">${esc(row.agent === 'claude-code' ? modelLabel(row.model) : (row.model || 'its default model'))}</span><span class="c4-chip">safety: ${esc(row.safety || 'ask')}</span><span class="c4-chip">${esc(machineLabel || 'your machine')}</span></div><p class="c4-hint">One long session on your own machine, on the agent you chose and your own login. Each message is the next turn of the same conversation; it hands the work to your other agents through the Codes tools.</p></section>${approvals}${usageCard}<section class="c4-card c4-turns"><div class="c4-card-head"><h2>Turns</h2><span class="c4-muted">${(view.messages || []).length} so far</span></div>${turns ? `<ol class="c4-turn-list">${turns}</ol>` : '<div class="c4-emptyline">Its first turn is starting.</div>'}</section>${transcript}${composer}`;
const content = `${outcome}<section class="c4-card c4-conductor-head"><div class="c4-task-heading"><div class="c4-task-title-wrap"><h1>Conductor</h1></div><span class="c4-status c4-${esc(row.state)}">${esc(conductorState(row))}</span></div><div class="c4-chips" aria-label="Conductor details"><span class="c4-chip">${esc(row.folder || 'a folder')}</span><span class="c4-chip">${esc(row.agent === 'claude-code' ? modelLabel(row.model) : (row.model || 'its default model'))}</span><span class="c4-chip">safety: ${esc(row.safety || 'ask')}</span><span class="c4-chip">${esc(machineLabel || 'your machine')}</span></div><p class="c4-hint">One long session on your own machine, on the agent you chose and your own login. Each message is the next turn of the same conversation; it hands the work to your other agents through the Codes tools.</p></section>${approvals}${usageCard}<section class="c4-card c4-turns"><div class="c4-card-head"><h2>Turns</h2><span class="c4-muted">${(view.messages || []).length} so far</span></div>${turns ? `<ol class="c4-turn-list">${turns}</ol>` : '<div class="c4-emptyline">Its first turn is starting.</div>'}</section>${transcript}${composer}`;
- page(req, res, 'Conductor', wrap(content));
+ page(req, res, 'Conductor', wrap(content), { sharedScripts: ['agent-session.js'] });
}
}
// ── The front door (a visitor): what Codes is, shown rather than told ──
// ── The front door (a visitor): what Codes is, shown rather than told ──
@@ -338,7 +338,7 @@ ${i.summary ? html`<p class="run-proof-summary" data-proof-summary>${i.summary}<
@@ -338,7 +338,7 @@ ${i.summary ? html`<p class="run-proof-summary" data-proof-summary>${i.summary}<
// after the end from codes_session_lines; a task from before this change falls back to the result's old log.
// after the end from codes_session_lines; a task from before this change falls back to the result's old log.
const lines = live ? null : await sessionStore.linesForRow(row);
const lines = live ? null : await sessionStore.linesForRow(row);
const title = String(row.prompt || '').split('\n')[0].slice(0, 90);
const title = String(row.prompt || '').split('\n')[0].slice(0, 90);
- const session = lines && lines.length ? html`<section class="run-session" data-session data-title="${title}" data-state="${row.state}"><h2>The session</h2><div data-session-view></div><script type="application/json" data-session-log>${raw(JSON.stringify(lines).replace(/</g, '\\u003c'))}</script></section>` : '';
+ const session = lines && lines.length ? html`<section class="run-session" data-session data-run-id="${row.id}" data-owner="${mine ? '1' : '0'}" data-title="${title}" data-state="${row.state}"><h2>The session</h2><div data-session-view></div><script type="application/json" data-session-log>${raw(JSON.stringify(lines).replace(/</g, '\\u003c'))}</script></section>` : '';
const body = html`${shareHead ? raw(shareHead) : ''}<p class="crumbs"><a href="/run">Run locally</a></p>
const body = html`${shareHead ? raw(shareHead) : ''}<p class="crumbs"><a href="/run">Run locally</a></p>
<h1 class="run-repo">${repoName(row.repo)}${row.ref ? html` <small>${row.ref}</small>` : ''}</h1>
<h1 class="run-repo">${repoName(row.repo)}${row.ref ? html` <small>${row.ref}</small>` : ''}</h1>
<p><span class="run-pill run-pill-${row.state}" data-state>${STATE_LABEL[row.state] || row.state}</span>${row.agent ? html` <span class="run-pill">${row.agent}</span>` : ''}</p>
<p><span class="run-pill run-pill-${row.state}" data-state>${STATE_LABEL[row.state] || row.state}</span>${row.agent ? html` <span class="run-pill">${row.agent}</span>` : ''}</p>
@@ -347,7 +347,7 @@ ${mine ? shareButton(row) : html`<p class="muted">This task was shared with you.
@@ -347,7 +347,7 @@ ${mine ? shareButton(row) : html`<p class="muted">This task was shared with you.
${await timelineHtml(row)}
${await timelineHtml(row)}
${goalHtml(row)}
${goalHtml(row)}
${await proofHtml(row)}
${await proofHtml(row)}
-${live ? html`<section class="run-live" data-events="/run/tasks/${row.id}/events" data-title="${title}" aria-live="polite"><h2>Live</h2><ol class="run-log" data-log></ol></section>
+${live ? html`<section class="run-live" data-events="/run/tasks/${row.id}/events" data-run-id="${row.id}" data-owner="${mine ? '1' : '0'}" data-title="${title}" aria-live="polite"><h2>Live</h2><ol class="run-log" data-log></ol></section>
${mine ? html`<form method="post" action="/run/tasks/${row.id}/cancel"><button class="run-btn" type="submit">Cancel</button></form>` : ''}` : html`${session}${resultHtml(row, { session: !!(lines && lines.length) })}`}`;
${mine ? html`<form method="post" action="/run/tasks/${row.id}/cancel"><button class="run-btn" type="submit">Cancel</button></form>` : ''}` : html`${session}${resultHtml(row, { session: !!(lines && lines.length) })}`}`;
return page(req, res, { title: 'Task', body, sharedScripts: ['agent-session.js'] });
return page(req, res, { title: 'Task', body, sharedScripts: ['agent-session.js'] });
});
});