|
@@ -95,6 +95,19 @@
|
|
|
var feed = null;
|
|
var feed = null;
|
|
|
var moreButton = null;
|
|
var moreButton = null;
|
|
|
var stream = null;
|
|
var stream = null;
|
|
|
|
|
+ var liveBadge = null;
|
|
|
|
|
+ var liveLabel = null;
|
|
|
|
|
+
|
|
|
|
|
+ // The live pill: green pulse while the stream holds,
|
|
|
|
|
+ // amber while EventSource retries after a drop.
|
|
|
|
|
+ function setLive(state) {
|
|
|
|
|
+ if (!liveBadge) return;
|
|
|
|
|
+ liveBadge.className = 'log-live' + (state === 'retry' ? ' retry' : '');
|
|
|
|
|
+ if (liveLabel) {
|
|
|
|
|
+ liveLabel.textContent = state === 'retry'
|
|
|
|
|
+ ? 'reconnecting...' : 'live';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
function kindClass(kind) {
|
|
function kindClass(kind) {
|
|
|
return 'event-kind-' + String(kind || '').replace(/[^a-z0-9-]/g, '');
|
|
return 'event-kind-' + String(kind || '').replace(/[^a-z0-9-]/g, '');
|
|
@@ -243,6 +256,9 @@
|
|
|
function resultSummary(evt, envelope) {
|
|
function resultSummary(evt, envelope) {
|
|
|
var p = evt.payload || {};
|
|
var p = evt.payload || {};
|
|
|
var wrap = document.createElement('summary');
|
|
var wrap = document.createElement('summary');
|
|
|
|
|
+ var failed = (typeof envelope.exit_code === 'number'
|
|
|
|
|
+ && envelope.exit_code !== 0) || envelope.ok === false;
|
|
|
|
|
+ wrap.className = 'result-summary' + (failed ? ' failed' : '');
|
|
|
wrap.appendChild(document.createTextNode('result'));
|
|
wrap.appendChild(document.createTextNode('result'));
|
|
|
var bits = [];
|
|
var bits = [];
|
|
|
if (envelope.exit_code !== undefined
|
|
if (envelope.exit_code !== undefined
|
|
@@ -315,11 +331,11 @@
|
|
|
// Empty stdout AND stderr. An error/note/
|
|
// Empty stdout AND stderr. An error/note/
|
|
|
// timeout member still said something, and a
|
|
// timeout member still said something, and a
|
|
|
// non-run_command tool's answer IS the
|
|
// non-run_command tool's answer IS the
|
|
|
- // envelope ({"recorded":true}, {"ok":true,
|
|
|
|
|
- // "task_id":4}, a tasks list) - render it
|
|
|
|
|
- // instead of claiming silence. Only an
|
|
|
|
|
- // envelope with nothing left to say is
|
|
|
|
|
- // "(no output)".
|
|
|
|
|
|
|
+ // envelope - render it through the tailored
|
|
|
|
|
+ // result body first (a read window, a
|
|
|
|
|
+ // replacement count, a task list), then the
|
|
|
|
|
+ // raw JSON. Only an envelope with nothing left
|
|
|
|
|
+ // to say is "(no output)".
|
|
|
var note = strText(envelope.error) || strText(envelope.note)
|
|
var note = strText(envelope.error) || strText(envelope.note)
|
|
|
|| (envelope.timeout ? 'timed out' : '');
|
|
|| (envelope.timeout ? 'timed out' : '');
|
|
|
if (note.length > 0) {
|
|
if (note.length > 0) {
|
|
@@ -328,7 +344,10 @@
|
|
|
div.appendChild(notePre);
|
|
div.appendChild(notePre);
|
|
|
} else {
|
|
} else {
|
|
|
var extras = resultExtras(envelope);
|
|
var extras = resultExtras(envelope);
|
|
|
- if (Object.keys(extras).length > 0) {
|
|
|
|
|
|
|
+ var bespoke = tailoredResultBody(strText(p.tool), extras);
|
|
|
|
|
+ if (bespoke) {
|
|
|
|
|
+ div.appendChild(bespoke);
|
|
|
|
|
+ } else if (Object.keys(extras).length > 0) {
|
|
|
var compact = JSON.stringify(extras);
|
|
var compact = JSON.stringify(extras);
|
|
|
div.appendChild(preLine(compact.length <= 120
|
|
div.appendChild(preLine(compact.length <= 120
|
|
|
? compact : JSON.stringify(extras, null, 2)));
|
|
? compact : JSON.stringify(extras, null, 2)));
|
|
@@ -343,6 +362,114 @@
|
|
|
return div;
|
|
return div;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // ---- tailored result bodies ----
|
|
|
|
|
+ //
|
|
|
|
|
+ // The daemon's file and task tools answer with
|
|
|
|
|
+ // structured extras (a line window of a file, a
|
|
|
|
|
+ // replacement count, a task list) that deserve better
|
|
|
|
|
+ // than a JSON dump. Answers null for the generic view.
|
|
|
|
|
+ function okPara(text) {
|
|
|
|
|
+ var node = para(text);
|
|
|
|
|
+ node.className = 'result-ok';
|
|
|
|
|
+ return node;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function taskList(items) {
|
|
|
|
|
+ var list = document.createElement('ul');
|
|
|
|
|
+ list.className = 'result-tasks';
|
|
|
|
|
+ items.forEach(function (item) {
|
|
|
|
|
+ if (!isPlainObject(item)) return;
|
|
|
|
|
+ var li = document.createElement('li');
|
|
|
|
|
+ li.textContent = '#' + (item.id !== undefined ? item.id : '?')
|
|
|
|
|
+ + ' ' + strText(item.title);
|
|
|
|
|
+ var status = strText(item.status);
|
|
|
|
|
+ if (status.length > 0) {
|
|
|
|
|
+ var chip = document.createElement('span');
|
|
|
|
|
+ chip.className = 'badge event-kind-state_change';
|
|
|
|
|
+ chip.textContent = status;
|
|
|
|
|
+ li.appendChild(chip);
|
|
|
|
|
+ }
|
|
|
|
|
+ list.appendChild(li);
|
|
|
|
|
+ });
|
|
|
|
|
+ return list;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function tailoredResultBody(tool, extras) {
|
|
|
|
|
+ var frag = document.createDocumentFragment();
|
|
|
|
|
+ if (strText(extras.content).length > 0
|
|
|
|
|
+ || typeof extras.total_lines === 'number') {
|
|
|
|
|
+ // read_file's answer: a line window of a file.
|
|
|
|
|
+ var meta = [];
|
|
|
|
|
+ if (strText(extras.path).length > 0) {
|
|
|
|
|
+ meta.push(strText(extras.path));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (typeof extras.line_start === 'number'
|
|
|
|
|
+ && typeof extras.total_lines === 'number') {
|
|
|
|
|
+ meta.push('lines ' + extras.line_start + '-'
|
|
|
|
|
+ + (typeof extras.line_end === 'number'
|
|
|
|
|
+ ? extras.line_end : extras.total_lines)
|
|
|
|
|
+ + ' of ' + extras.total_lines);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (typeof extras.bytes === 'number') {
|
|
|
|
|
+ meta.push(fmtBytes(extras.bytes));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (extras.truncated) meta.push('truncated');
|
|
|
|
|
+ if (meta.length > 0) {
|
|
|
|
|
+ frag.appendChild(para(meta.join(' \u00b7 '), 'result-meta'));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (strText(extras.content).length > 0) {
|
|
|
|
|
+ frag.appendChild(preLine(extras.content));
|
|
|
|
|
+ }
|
|
|
|
|
+ return frag;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (tool === 'replace_in_file' && typeof extras.replacements === 'number') {
|
|
|
|
|
+ return okPara(extras.replacements + ' replacement'
|
|
|
|
|
+ + (extras.replacements === 1 ? '' : 's') + ' in '
|
|
|
|
|
+ + strText(extras.path) + ' \u00b7 '
|
|
|
|
|
+ + fmtBytes(typeof extras.bytes_written === 'number'
|
|
|
|
|
+ ? extras.bytes_written : 0));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (tool === 'write_file' && typeof extras.bytes === 'number') {
|
|
|
|
|
+ return okPara('wrote ' + strText(extras.path) + ' \u00b7 '
|
|
|
|
|
+ + fmtBytes(extras.bytes));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (tool === 'overwrite_file' && typeof extras.bytes_written === 'number') {
|
|
|
|
|
+ return okPara('rewrote ' + strText(extras.path) + ' \u00b7 '
|
|
|
|
|
+ + fmtBytes(extras.bytes_written)
|
|
|
|
|
+ + (typeof extras.previous_bytes === 'number'
|
|
|
|
|
+ ? ' (was ' + fmtBytes(extras.previous_bytes) + ')' : ''));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (tool === 'create_task' && typeof extras.task_id === 'number') {
|
|
|
|
|
+ return okPara('created task #' + extras.task_id
|
|
|
|
|
+ + (strText(extras.status).length > 0
|
|
|
|
|
+ ? ' \u00b7 ' + strText(extras.status) : ''));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (tool === 'get_task' && extras.id !== undefined) {
|
|
|
|
|
+ return taskList([extras]);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (Array.isArray(extras.tasks)) return taskList(extras.tasks);
|
|
|
|
|
+ if (Array.isArray(extras.goals)) return taskList(extras.goals);
|
|
|
|
|
+ var keys = Object.keys(extras);
|
|
|
|
|
+ var allTrue = keys.length > 0;
|
|
|
|
|
+ var allScalars = keys.length > 0 && keys.length <= 4;
|
|
|
|
|
+ for (var i = 0; i < keys.length; i++) {
|
|
|
|
|
+ var value = extras[keys[i]];
|
|
|
|
|
+ if (value !== true) allTrue = false;
|
|
|
|
|
+ if (value !== null && typeof value === 'object') allScalars = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ // A pure acknowledgement ({recorded:true},
|
|
|
|
|
+ // {accepted:true}): a green ok line, not JSON.
|
|
|
|
|
+ if (allTrue) return okPara('ok');
|
|
|
|
|
+ if (allScalars) {
|
|
|
|
|
+ var bits = [];
|
|
|
|
|
+ for (var j = 0; j < keys.length; j++) {
|
|
|
|
|
+ bits.push(keys[j] + ': ' + extras[keys[j]]);
|
|
|
|
|
+ }
|
|
|
|
|
+ return para(bits.join(' \u00b7 '), 'result-meta');
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// The tool call renders a TAILORED body per tool: the
|
|
// The tool call renders a TAILORED body per tool: the
|
|
|
// head line always carries the tool and its target (a
|
|
// head line always carries the tool and its target (a
|
|
|
// $ command, a file path, a task id) and the payload
|
|
// $ command, a file path, a task id) and the payload
|
|
@@ -699,7 +826,7 @@
|
|
|
|
|
|
|
|
function eventNode(evt) {
|
|
function eventNode(evt) {
|
|
|
var row = document.createElement('div');
|
|
var row = document.createElement('div');
|
|
|
- row.className = 'event-row';
|
|
|
|
|
|
|
+ row.className = 'event-row ' + kindClass(evt.kind);
|
|
|
var head = document.createElement('p');
|
|
var head = document.createElement('p');
|
|
|
head.className = 'event-head';
|
|
head.className = 'event-head';
|
|
|
var badge = document.createElement('span');
|
|
var badge = document.createElement('span');
|
|
@@ -829,14 +956,24 @@
|
|
|
var card = document.createElement('div');
|
|
var card = document.createElement('div');
|
|
|
card.className = 'turn-card';
|
|
card.className = 'turn-card';
|
|
|
var head = document.createElement('p');
|
|
var head = document.createElement('p');
|
|
|
- head.className = 'turn-head muted';
|
|
|
|
|
- var bits = [];
|
|
|
|
|
|
|
+ head.className = 'turn-head';
|
|
|
if (events[0].payload && events[0].payload.turn !== undefined
|
|
if (events[0].payload && events[0].payload.turn !== undefined
|
|
|
&& events[0].payload.turn !== null) {
|
|
&& events[0].payload.turn !== null) {
|
|
|
- bits.push('turn ' + events[0].payload.turn);
|
|
|
|
|
|
|
+ var chip = document.createElement('span');
|
|
|
|
|
+ chip.className = 'turn-chip';
|
|
|
|
|
+ chip.textContent = 'turn ' + events[0].payload.turn;
|
|
|
|
|
+ head.appendChild(chip);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ var plain = document.createElement('span');
|
|
|
|
|
+ plain.textContent = 'agent turn';
|
|
|
|
|
+ head.appendChild(plain);
|
|
|
}
|
|
}
|
|
|
- if (events[0].session_id) bits.push('session ' + events[0].session_id);
|
|
|
|
|
- head.textContent = bits.length > 0 ? bits.join(' \u00b7 ') : 'agent turn';
|
|
|
|
|
|
|
+ var meta = document.createElement('span');
|
|
|
|
|
+ meta.textContent = events.length + ' event'
|
|
|
|
|
+ + (events.length === 1 ? '' : 's')
|
|
|
|
|
+ + (events[0].session_id
|
|
|
|
|
+ ? ' \u00b7 session ' + events[0].session_id : '');
|
|
|
|
|
+ head.appendChild(meta);
|
|
|
card.appendChild(head);
|
|
card.appendChild(head);
|
|
|
events.forEach(function (evt) {
|
|
events.forEach(function (evt) {
|
|
|
card.appendChild(eventNode(evt));
|
|
card.appendChild(eventNode(evt));
|
|
@@ -864,7 +1001,7 @@
|
|
|
var visibleRows = state.events.filter(visible);
|
|
var visibleRows = state.events.filter(visible);
|
|
|
if (visibleRows.length === 0) {
|
|
if (visibleRows.length === 0) {
|
|
|
var empty = document.createElement('p');
|
|
var empty = document.createElement('p');
|
|
|
- empty.className = 'muted';
|
|
|
|
|
|
|
+ empty.className = 'muted log-empty';
|
|
|
empty.textContent = state.events.length === 0
|
|
empty.textContent = state.events.length === 0
|
|
|
? 'Waiting for the first events\u2026'
|
|
? 'Waiting for the first events\u2026'
|
|
|
: 'No events match the filters.';
|
|
: 'No events match the filters.';
|
|
@@ -950,7 +1087,11 @@
|
|
|
var base = '/tasks/' + log.task_id;
|
|
var base = '/tasks/' + log.task_id;
|
|
|
stream = new EventSource(base + '/events/stream');
|
|
stream = new EventSource(base + '/events/stream');
|
|
|
stream.onmessage = onStreamMessage;
|
|
stream.onmessage = onStreamMessage;
|
|
|
|
|
+ stream.onopen = function () {
|
|
|
|
|
+ setLive('live');
|
|
|
|
|
+ };
|
|
|
stream.onerror = function () {
|
|
stream.onerror = function () {
|
|
|
|
|
+ setLive('retry');
|
|
|
// EventSource retries on its own (retry: 3s
|
|
// EventSource retries on its own (retry: 3s
|
|
|
// hint); the reconnect replays from the
|
|
// hint); the reconnect replays from the
|
|
|
// last seen id and ingest dedupes it.
|
|
// last seen id and ingest dedupes it.
|
|
@@ -1015,6 +1156,8 @@
|
|
|
if (!page || !page.exists) return;
|
|
if (!page || !page.exists) return;
|
|
|
feed = document.querySelector('[data-log-feed]');
|
|
feed = document.querySelector('[data-log-feed]');
|
|
|
moreButton = document.querySelector('[data-log-more]');
|
|
moreButton = document.querySelector('[data-log-more]');
|
|
|
|
|
+ liveBadge = document.querySelector('[data-log-live]');
|
|
|
|
|
+ liveLabel = document.querySelector('[data-live-label]');
|
|
|
var header = document.querySelector('[data-log-title]');
|
|
var header = document.querySelector('[data-log-title]');
|
|
|
if (header) {
|
|
if (header) {
|
|
|
header.textContent = '#' + page.task_id + ' \u2014 '
|
|
header.textContent = '#' + page.task_id + ' \u2014 '
|
|
@@ -1029,16 +1172,24 @@
|
|
|
</head>
|
|
</head>
|
|
|
<body>
|
|
<body>
|
|
|
<div class="page log-page">
|
|
<div class="page log-page">
|
|
|
- <p><a href="/tasks">← Tasks</a></p>
|
|
|
|
|
- <h2 data-log-title>Event log</h2>
|
|
|
|
|
|
|
+ <p class="log-back"><a href="/tasks">← Tasks</a></p>
|
|
|
<div stm-if="!task_log.exists" class="muted">No task exists with this id.</div>
|
|
<div stm-if="!task_log.exists" class="muted">No task exists with this id.</div>
|
|
|
<div stm-if="task_log.exists">
|
|
<div stm-if="task_log.exists">
|
|
|
- <p><a stm-attribute.href="task_log.task_url">Task page</a>
|
|
|
|
|
- <span class="muted">· the unified event ledger, live (most recent 30;
|
|
|
|
|
- loads older on demand)</span></p>
|
|
|
|
|
- <section class="card">
|
|
|
|
|
|
|
+ <header class="log-header">
|
|
|
|
|
+ <div class="log-title">
|
|
|
|
|
+ <h2 data-log-title>Event log</h2>
|
|
|
|
|
+ <p class="muted">the unified event ledger · newest at the bottom ·
|
|
|
|
|
+ loads older history on demand</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="log-actions">
|
|
|
|
|
+ <span class="log-live" data-log-live><span class="live-dot"></span><span
|
|
|
|
|
+ data-live-label>live</span></span>
|
|
|
|
|
+ <a class="log-task-link" stm-attribute.href="task_log.task_url">Task page</a>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </header>
|
|
|
|
|
+ <section class="log-shell">
|
|
|
<div class="log-toolbar">
|
|
<div class="log-toolbar">
|
|
|
- <span class="muted">Show:</span>
|
|
|
|
|
|
|
+ <span class="log-toolbar-label">Show</span>
|
|
|
<button type="button" class="log-filter active" data-log-filter="commands">commands</button>
|
|
<button type="button" class="log-filter active" data-log-filter="commands">commands</button>
|
|
|
<button type="button" class="log-filter active" data-log-filter="results">results</button>
|
|
<button type="button" class="log-filter active" data-log-filter="results">results</button>
|
|
|
<button type="button" class="log-filter active" data-log-filter="comments">comments</button>
|
|
<button type="button" class="log-filter active" data-log-filter="comments">comments</button>
|
|
@@ -1048,7 +1199,7 @@
|
|
|
</div>
|
|
</div>
|
|
|
<button type="button" class="log-more" data-log-more hidden>Load older events</button>
|
|
<button type="button" class="log-more" data-log-more hidden>Load older events</button>
|
|
|
<div class="log-feed" data-log-feed>
|
|
<div class="log-feed" data-log-feed>
|
|
|
- <p class="muted">Waiting for the first events…</p>
|
|
|
|
|
|
|
+ <p class="muted log-empty">Waiting for the first events…</p>
|
|
|
</div>
|
|
</div>
|
|
|
</section>
|
|
</section>
|
|
|
</div>
|
|
</div>
|