agents-relay 2.0.179 → 2.0.181
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/dist/dashboard-data.js +21 -28
- package/dist/dashboard-html.js +27 -60
- package/dist/dashboard-style.js +21 -25
- package/dist/dashboard.js +1 -2
- package/dist/relayd.js +2 -2
- package/package.json +1 -1
package/dist/dashboard-data.js
CHANGED
|
@@ -4,18 +4,19 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { eventsBusHistory } from './events-bus-runtime.js';
|
|
5
5
|
const repositoryPattern = /^([A-Za-z0-9](?:[A-Za-z0-9-]{0,36}[A-Za-z0-9])?)\/([A-Za-z0-9._-]{1,100})$/;
|
|
6
6
|
const workflowDirectory = fileURLToPath(new URL('../workflow/', import.meta.url));
|
|
7
|
-
export const
|
|
7
|
+
export const DASHBOARD_JOB_RESULT_LIMIT = 20;
|
|
8
8
|
export const DEFAULT_DASHBOARD_JOB_STATUSES = ['DRAFT', 'ACTIVE', 'COMPLETED'];
|
|
9
9
|
function compareDashboardJobs(a, b) {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
const timestamp = (job) => {
|
|
11
|
+
const updated = Date.parse(job.updatedAt ?? '');
|
|
12
|
+
if (Number.isFinite(updated))
|
|
13
|
+
return updated;
|
|
14
|
+
const created = Date.parse(job.createdAt ?? '');
|
|
15
|
+
return Number.isFinite(created) ? created : 0;
|
|
16
|
+
};
|
|
17
|
+
return timestamp(b) - timestamp(a)
|
|
18
|
+
|| a.repository.localeCompare(b.repository)
|
|
19
|
+
|| a.id.localeCompare(b.id);
|
|
19
20
|
}
|
|
20
21
|
export async function dashboardWorkflowGraphs() {
|
|
21
22
|
const names = (await readdir(workflowDirectory)).filter(name => name.endsWith('.mmd')).sort();
|
|
@@ -57,18 +58,16 @@ export async function eventHistory(jobId, taskId, limit) {
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
export function searchDashboardJobs(jobs, query) {
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const
|
|
61
|
+
const requestedStatus = (query.get('status') ?? query.get('statuses') ?? '').trim().toUpperCase();
|
|
62
|
+
const status = requestedStatus && ['DRAFT', 'ACTIVE', 'COMPLETED', 'CANCELLED'].includes(requestedStatus) ? requestedStatus : '';
|
|
63
|
+
const repository = (query.get('repo') ?? query.get('repositories') ?? query.get('repos') ?? '').trim();
|
|
63
64
|
const search = query.get('q')?.trim().toLowerCase() ?? '';
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const pageSize = Number.isInteger(parsedPageSize) && parsedPageSize > 0 ? Math.min(parsedPageSize, DASHBOARD_JOB_PAGE_SIZE) : DASHBOARD_JOB_PAGE_SIZE;
|
|
68
|
-
const matchesStatus = (job) => statuses.some(status => status === 'DRAFT'
|
|
65
|
+
const matchesStatus = (job) => status ? status === 'DRAFT'
|
|
66
|
+
? Boolean(job.draft) || job.state === 'DRAFT'
|
|
67
|
+
: job.state === status : DEFAULT_DASHBOARD_JOB_STATUSES.some(defaultStatus => defaultStatus === 'DRAFT'
|
|
69
68
|
? Boolean(job.draft) || job.state === 'DRAFT'
|
|
70
|
-
: job.state ===
|
|
71
|
-
const matchesRepository = (job) =>
|
|
69
|
+
: job.state === defaultStatus);
|
|
70
|
+
const matchesRepository = (job) => !repository || repository === job.repository;
|
|
72
71
|
const normalizedPr = search.replace(/^pr\s*#?/, '').replace(/^#/, '');
|
|
73
72
|
const matchesSearch = (job) => {
|
|
74
73
|
if (!search)
|
|
@@ -78,15 +77,9 @@ export function searchDashboardJobs(jobs, query) {
|
|
|
78
77
|
|| Boolean(normalizedPr) && String(job.prNumber || '').includes(normalizedPr);
|
|
79
78
|
};
|
|
80
79
|
const matching = jobs.filter(job => matchesStatus(job) && matchesRepository(job) && matchesSearch(job)).sort(compareDashboardJobs);
|
|
81
|
-
const total = matching.length;
|
|
82
|
-
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
83
|
-
const safePage = Math.min(page, totalPages);
|
|
84
80
|
return {
|
|
85
|
-
jobs: matching.slice(
|
|
81
|
+
jobs: matching.slice(0, DASHBOARD_JOB_RESULT_LIMIT),
|
|
86
82
|
repositories: [...new Set(jobs.map(job => job.repository).filter(Boolean))].sort(),
|
|
87
|
-
total,
|
|
88
|
-
page: safePage,
|
|
89
|
-
pageSize,
|
|
90
|
-
totalPages,
|
|
83
|
+
total: matching.length,
|
|
91
84
|
};
|
|
92
85
|
}
|
package/dist/dashboard-html.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dashboardStyles } from './dashboard-style.js';
|
|
2
|
-
import {
|
|
2
|
+
import { DEFAULT_DASHBOARD_JOB_STATUSES } from './dashboard-data.js';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
@@ -42,7 +42,7 @@ ${dashboardStyles}
|
|
|
42
42
|
</header>
|
|
43
43
|
<main>
|
|
44
44
|
<section id="jobs-view" role="tabpanel" aria-labelledby="jobs-tab" class="panel jobs-panel">
|
|
45
|
-
<div class="panel-head"><h2>Jobs</h2><div class="panel-actions"><div class="job-filters"><input id="job-search" class="job-search" type="search" placeholder="Search title or PR #" aria-label="Search jobs by title or PR number"><
|
|
45
|
+
<div class="panel-head"><h2>Jobs</h2><div class="panel-actions"><div class="job-filters"><input id="job-search" class="job-search" type="search" placeholder="Search title or PR #" aria-label="Search jobs by title or PR number"><select id="status-filter" class="job-filter" aria-label="Filter jobs by status"><option value="">All (no CANCELLED)</option><option value="DRAFT">DRAFT</option><option value="ACTIVE">ACTIVE</option><option value="COMPLETED">COMPLETED</option><option value="CANCELLED">CANCELLED</option></select><select id="repo-filter" class="job-filter" aria-label="Filter jobs by repository"><option value="">All repositories</option></select></div><div class="jobs-actions"><button id="jobs-reset" class="refresh-button" type="button">Reset</button><button id="jobs-refresh" class="refresh-button" type="button">Refresh</button></div><span id="jobs-feedback" class="jobs-feedback subtle" role="status" aria-live="polite"></span></div></div>
|
|
46
46
|
<div class="panel-body" aria-live="polite"><ul id="job-list" class="job-list"><li class="skeleton" aria-hidden="true"></li></ul></div>
|
|
47
47
|
</section>
|
|
48
48
|
<div class="workspace" id="jobs-workspace">
|
|
@@ -144,15 +144,9 @@ const jobList=document.querySelector('#job-list');
|
|
|
144
144
|
const jobsRefresh=document.querySelector('#jobs-refresh');
|
|
145
145
|
const jobsReset=document.querySelector('#jobs-reset');
|
|
146
146
|
const jobSearch=document.querySelector('#job-search');
|
|
147
|
-
const jobsPrev=document.querySelector('#jobs-prev');
|
|
148
|
-
const jobsNext=document.querySelector('#jobs-next');
|
|
149
|
-
const jobsPageLabel=document.querySelector('#jobs-page');
|
|
150
147
|
const repoFilter=document.querySelector('#repo-filter');
|
|
151
|
-
const repoFilterMenu=document.querySelector('#repo-filter-menu');
|
|
152
|
-
const repoFilterSummary=document.querySelector('#repo-filter-summary');
|
|
153
148
|
const statusFilter=document.querySelector('#status-filter');
|
|
154
|
-
const
|
|
155
|
-
const statusFilterSummary=document.querySelector('#status-filter-summary');
|
|
149
|
+
const jobsFeedback=document.querySelector('#jobs-feedback');
|
|
156
150
|
const selectedJobRefresh=document.querySelector('#selected-job-refresh');
|
|
157
151
|
const mobileJobsBack=document.querySelector('#mobile-jobs-back');
|
|
158
152
|
const mobileJobs=window.matchMedia('(max-width:650px)');
|
|
@@ -184,11 +178,8 @@ const runtimeSummary=document.querySelector('#runtime-summary');
|
|
|
184
178
|
let jobs=[];
|
|
185
179
|
let selectedJob=sessionStorage.getItem('agents-relay:selected-job');
|
|
186
180
|
let selectedTaskId=sessionStorage.getItem('agents-relay:selected-task');
|
|
187
|
-
let
|
|
188
|
-
let
|
|
189
|
-
let jobsPage=1;
|
|
190
|
-
let jobsTotal=0;
|
|
191
|
-
let jobsTotalPages=1;
|
|
181
|
+
let selectedRepo=storedFilterValue('agents-relay:repo-filter');
|
|
182
|
+
let selectedStatus=storedFilterValue('agents-relay:status-filter');
|
|
192
183
|
let jobRepositories=[];
|
|
193
184
|
let searchQuery=sessionStorage.getItem('agents-relay:job-search')||'';
|
|
194
185
|
let workflowGraphs=[];
|
|
@@ -199,7 +190,7 @@ const collapsedTasks=new Set();
|
|
|
199
190
|
const expandedTasks=new Set();
|
|
200
191
|
const displayedEventIds=new Set();
|
|
201
192
|
function value(input){return input===null||input===undefined||input===''?'—':String(input);}
|
|
202
|
-
function
|
|
193
|
+
function storedFilterValue(key){const stored=sessionStorage.getItem(key);if(!stored)return '';try{const parsed=JSON.parse(stored);if(Array.isArray(parsed))return parsed.length===1&&typeof parsed[0]==='string'?parsed[0]:'';if(typeof parsed==='string')return parsed;}catch{return stored;}return '';}
|
|
203
194
|
function text(input){return document.createTextNode(value(input));}
|
|
204
195
|
function element(name,className,child){const node=document.createElement(name);if(className)node.className=className;if(child)node.append(child);return node;}
|
|
205
196
|
function yamlScalar(input){if(input===null)return 'null';if(typeof input==='boolean'||typeof input==='number')return String(input);return JSON.stringify(String(input));}
|
|
@@ -226,35 +217,18 @@ function clearEvents(){displayedEventIds.clear();eventFeed.replaceChildren(eleme
|
|
|
226
217
|
async function loadEventHistory(){const job=selected();if(!job){clearEvents();return;}const params=new URLSearchParams({job:job.id,limit:String(MAX_EVENTS)});if(eventTaskId)params.set('task',eventTaskId);try{const history=await loadJSON('/api/events?'+params.toString());clearEvents();for(const event of Array.isArray(history)?history.slice().reverse():[])addEvent(event);}catch{clearEvents();}}
|
|
227
218
|
function copySelectedJobIdentity(job){if(!job||!navigator.clipboard?.writeText)return;void navigator.clipboard.writeText(repoName(job.repository)+'/'+value(job.id)).catch(()=>{});}
|
|
228
219
|
function selectJob(job){selectedJob=jobSelectionKey(job);selectedTaskId=null;eventTaskId=null;rememberSelection();clearEvents();location.hash=selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs';copySelectedJobIdentity(job);renderAll();void loadEventHistory();}
|
|
229
|
-
function matchesQuickFilter(job){
|
|
220
|
+
function matchesQuickFilter(job){return !selectedStatus?${JSON.stringify(DEFAULT_DASHBOARD_JOB_STATUSES)}.some(status=>status==='DRAFT'?job.draft||job.state==='DRAFT':job.state===status):selectedStatus==='DRAFT'?job.draft||job.state==='DRAFT':job.state===selectedStatus;}
|
|
230
221
|
function matchesSearch(job){const query=searchQuery.trim().toLowerCase();if(!query)return true;const pr=String(job.prNumber||'');const normalized=query.replace(/^pr\s*#?/,'').replace(/^#/,'');return String(job.title||job.id||'').toLowerCase().includes(query)||String(job.id||'').toLowerCase().includes(query)||shortManagedId(job.id,job.repository,'j').toLowerCase().includes(query)||pr.includes(normalized);}
|
|
231
|
-
function filteredJobs(){return jobs.filter(job=>(!
|
|
232
|
-
function selectedNames(inputs,selected){const selectedSet=new Set(selected);return [...inputs].filter(input=>selectedSet.has(input.value)).map(input=>input.value);}
|
|
233
|
-
function syncFilterSummary(trigger,summary,inputs,selected,emptyLabel){const names=selectedNames(inputs,selected);summary.textContent=names.length===0?emptyLabel:names.length<=2?names.join(', '):names.length+' selected';trigger.title=names.length===0?emptyLabel:names.join(', ');}
|
|
234
|
-
function closeFilterMenus(exceptMenu){for(const [trigger,menu] of [[statusFilter,statusFilterMenu],[repoFilter,repoFilterMenu]]){if(menu===exceptMenu)continue;menu.hidden=true;trigger.setAttribute('aria-expanded','false');}}
|
|
235
|
-
function toggleFilterMenu(trigger,menu){const open=menu.hidden;closeFilterMenus(open?menu:null);menu.hidden=!open;trigger.setAttribute('aria-expanded',String(open));}
|
|
222
|
+
function filteredJobs(){return jobs.filter(job=>(!selectedRepo||selectedRepo===job.repository)&&matchesQuickFilter(job)&&matchesSearch(job)).slice(0,20);}
|
|
236
223
|
function syncFilters(){
|
|
237
224
|
const repos=jobRepositories.length?jobRepositories:[...new Set(jobs.map(job=>job.repository).filter(Boolean))].sort();
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
selectedRepos=selectedRepos.filter(repo=>validRepos.has(repo));
|
|
247
|
-
for(const input of repoFilterMenu.querySelectorAll('input'))input.checked=selectedRepos.includes(input.value);
|
|
248
|
-
const statusInputs=[...statusFilterMenu.querySelectorAll('input')];
|
|
249
|
-
const validStatuses=new Set(statusInputs.map(input=>input.value));
|
|
250
|
-
selectedStatuses=selectedStatuses.filter(status=>validStatuses.has(status));
|
|
251
|
-
for(const input of statusInputs)input.checked=selectedStatuses.includes(input.value);
|
|
252
|
-
syncFilterSummary(statusFilter,statusFilterSummary,statusInputs,selectedStatuses,'Default');
|
|
253
|
-
syncFilterSummary(repoFilter,repoFilterSummary,[...repoFilterMenu.querySelectorAll('input')],selectedRepos,'All');
|
|
254
|
-
jobSearch.value=searchQuery;
|
|
255
|
-
jobsPageLabel.textContent='Page '+jobsPage+' of '+jobsTotalPages+(jobsTotal?' · '+jobsTotal+' jobs':'');
|
|
256
|
-
jobsPrev.disabled=jobsPage<=1;
|
|
257
|
-
jobsNext.disabled=jobsPage>=jobsTotalPages;
|
|
225
|
+
const repoOptions=[new Option('All repositories',''),...repos.map(repo=>new Option(repo,repo))];
|
|
226
|
+
if(repoFilter.options.length!==repoOptions.length||[...repoFilter.options].some((option,index)=>option.value!==repoOptions[index].value))repoFilter.replaceChildren(...repoOptions);
|
|
227
|
+
if(selectedRepo&&!repos.includes(selectedRepo))selectedRepo='';
|
|
228
|
+
if(!['','DRAFT','ACTIVE','COMPLETED','CANCELLED'].includes(selectedStatus))selectedStatus='';
|
|
229
|
+
statusFilter.value=selectedStatus;
|
|
230
|
+
repoFilter.value=selectedRepo;
|
|
231
|
+
if(jobSearch.value!==searchQuery)jobSearch.value=searchQuery;
|
|
258
232
|
}
|
|
259
233
|
function repoName(repository){if(typeof repository!=='string'||!repository)return 'unknown-repo';const parts=repository.split('/').filter(Boolean);return parts.at(-1)||repository;}
|
|
260
234
|
const CHAT_SHORTCUTS_KEY='agents-relay:chatgpt-shortcuts';
|
|
@@ -286,9 +260,7 @@ function manualOverrideField(task){const rows=Array.isArray(task.manualStateOver
|
|
|
286
260
|
function showDetailsDialog(title,sections){const dialog=element('dialog','details-dialog');dialog.setAttribute('aria-label',title);const head=element('div','details-dialog-head');const name=element('strong','',text(title));const close=element('button','refresh-button',text('Close'));close.type='button';close.addEventListener('click',()=>dialog.close());head.append(name,close);dialog.append(head);for(const [label,value] of sections){if(value===undefined||value===null||value==='')continue;dialog.append(element('h3','',text(label)));const content=element('pre');content.textContent=typeof value==='string'?value:JSON.stringify(value,null,2);dialog.append(content);}dialog.addEventListener('close',()=>dialog.remove());document.body.append(dialog);dialog.showModal();}function openJobDetails(){const job=selected();if(!job)return;showDetailsDialog('Job '+job.id+' · '+value(job.title),[['Objective',job.objective],['Description',job.description],['State',job.state],['Plans',job.plans?.map(p=>({id:p.id,status:p.status,assessment:p.assessment,error:p.error,generatedTaskIds:p.generatedTaskIds}))],['Review',job.review],['Outcome',job.result],['Publication',job.publication],['Failure',job.error]]);}function openTaskDetails(task){const result=task.result?.data??task.result;showDetailsDialog('Task '+task.id+' · '+value(task.title),[['Input / task objective',task.input],['State',task.state],['Conclusion',result],['Result summary',task.result?.summary],['Planner assessment',task.plannerResult?.assessment],['Resolution',task.resolution],['Resolution classification',task.resolutionClassification],['Failure',task.failureReason||task.error],['Dependencies',task.dependencies]]);}document.querySelector('#job-content-open').addEventListener('click',openJobDetails);function renderTaskDetail(task){taskDetailRoot.replaceChildren();if(!task){taskDetailRoot.append(element('p','empty',text('Select a task to inspect execution details.')));return;}const routing=task.routing||{};const groups=element('div','detail-groups');groups.append(field('Title',task.title||'—'),field('Task ID',task.id),field('Kind',task.kind||'work'),taskStateField(task),...(task.state==='FAILED'&&task.resolution?.state?[field('Resolution state',task.resolution.state),field('Resolution path',task.resolution.path||'—'),field('Resolution target',task.resolution.targetTaskId||'—')]:[]),manualOverrideField(task),field('Planning decision',task.planningDecisionId||'—'),field('Planning source',task.planningSource||'—'),field('Planning round',task.planningRound===undefined?'—':String(task.planningRound)),field('Project / agent',value(task.projectName)+' / '+value(task.agentName)),field('Parent task',task.parentTaskId||'root'),...(task.resolutionClassification?[field('Resolution classification',task.resolutionClassification.kind),field('Resolution target',task.resolutionClassification.targetTaskId),field('Resolution evidence',task.resolutionClassification.evidence),field('Resolution reason',task.resolutionClassification.reason)]:[]),field('Adapter',task.adapter),field('Provider / model',value(routing.provider)+' / '+value(routing.model)),field('Reasoning / profile',value(routing.reasoning||routing.profile)),field('Attempt',attemptBudget(task)),field('Dependencies',Array.isArray(task.dependencies)&&task.dependencies.length?task.dependencies.join(', '):'none'),taskCommentField(task),chatField(task));const job=selected();if(task.reviewHeadSha&&job)groups.append(commitField('HEAD commit',job,task.reviewHeadSha));const graph=agentGraphField(task);if(graph)groups.append(graph);if(task.plannerResult)groups.append(field('Objective status',task.plannerResult.objective_status),markdownField('Assessment',task.plannerResult.assessment));if(task.error)groups.append(field('Error',task.error));const result=task.result?.data??task.result;if(result){groups.append(field('Conclusion',result.verdict||result.outcome?.verdict||task.result?.summary||'Available'));}taskDetailRoot.append(groups);const details=element('button','refresh-button',text('Task details / conclusion'));details.type='button';details.addEventListener('click',()=>openTaskDetails(task));taskDetailRoot.append(details);}
|
|
287
261
|
function reviewStatus(job){const review=job?.review;if(!review)return 'Not started';if(review.outcome==='approved'&&review.headSha===job.headSha)return 'MERGE READY · approved';const stale=Array.isArray(job?.tasks)&&job.tasks.some(task=>task.agentName==='reviewer'&&task.reviewHeadSha&&job.headSha&&task.reviewHeadSha!==job.headSha);if(stale)return 'STALE · awaiting review';return String(review.outcome||'pending').replace('_',' ').toUpperCase();}function renderSelectedJob(){jobDetail.replaceChildren();const job=selected();if(!job){selectedJobTitle.textContent='';jobDetail.append(element('div','empty',text('No durable jobs discovered.')));buildTree([]);renderTaskDetail(null);return;}selectedJobTitle.textContent=value(job.title||job.id);if(jobSelectionKey(job)!==selectedJob){selectedJob=jobSelectionKey(job);selectedTaskId=null;eventTaskId=null;clearEvents();rememberSelection();}const summary=element('div','job-summary');summary.append(field('PR state',job.githubState||'UNKNOWN'),jobStateField(job),field('Runtime status',job.runtimeStatus||'IDLE'),field('Review',reviewStatus(job)),field('Creator thread',job.creatorThreadId||'—'),threadField('Implementation thread',job.implementationThreadId),threadField('Reviewer thread',job.reviewerThreadId),commitField('Review head SHA',job,job.review?.headSha||job.headSha||''),prField(job),field('Repository',job.repository||'—'),field('Last activity',relativeTime(job.updatedAt)),field('Task states',compactCounts(job.tasks)),chatShortcutField(job));jobDetail.append(summary);const selectedTask=(job.tasks||[]).find(task=>task.id===selectedTaskId)||null;if(selectedTaskId&&!selectedTask){selectedTaskId=null;eventTaskId=null;clearEvents();rememberSelection();}buildTree(job.tasks||[]);renderTaskDetail(selectedTask);renderEventScope();}function renderAll(){jobs.sort(compareJobs);syncFilters();renderJobList();renderSelectedJob();syncMobileJobsView();}
|
|
288
262
|
function isCompleteUnmerged(job){return job.runtimeStatus==='MERGE_PENDING';}
|
|
289
|
-
function
|
|
290
|
-
function jobGroup(job){if(isTerminalJob(job))return 3;if(job.draft||job.state==='DRAFT')return 2;return 0;}
|
|
291
|
-
function compareJobs(a,b){const groupDelta=jobGroup(a)-jobGroup(b);if(groupDelta)return groupDelta;const mergePendingDelta=Number(isCompleteUnmerged(b))-Number(isCompleteUnmerged(a));return mergePendingDelta||String(b.updatedAt).localeCompare(String(a.updatedAt));}
|
|
263
|
+
function compareJobs(a,b){const timestamp=job=>{const updated=Date.parse(job.updatedAt||'');if(Number.isFinite(updated))return updated;const created=Date.parse(job.createdAt||'');return Number.isFinite(created)?created:0;};return timestamp(b)-timestamp(a)||String(a.repository||'').localeCompare(String(b.repository||''))||String(a.id||'').localeCompare(String(b.id||''));}
|
|
292
264
|
async function loadJSON(url){const response=await fetch(url);if(!response.ok)throw new Error(response.status+' '+response.statusText);return response.json();}
|
|
293
265
|
async function refreshVersion(){try{const live=await loadJSON('/api/version');if(live?.version&&live.version!==DASHBOARD_VERSION)location.reload();}catch{}}
|
|
294
266
|
function renderRateLimitStatus(status){
|
|
@@ -301,10 +273,10 @@ function renderRateLimitStatus(status){
|
|
|
301
273
|
function runtimePill(label,value){const pill=element('span','runtime-stat');pill.append(document.createTextNode(label+' '),element('strong','',text(value)));return pill;}
|
|
302
274
|
async function drawRuntimeSummary(){try{const summary=await loadJSON('/api/runtime-summary');runtimeSummary.replaceChildren(runtimePill('Active Jobs',summary.activeJobs||0),runtimePill('Running Tasks',summary.runningTasks||0),runtimePill('ChatGPT',summary.workers?.chatgpt||0),runtimePill('Codex',summary.workers?.codex||0));for(const item of Array.isArray(summary.models)?summary.models:[])runtimeSummary.append(runtimePill(item.model,item.count));}catch{runtimeSummary.replaceChildren();}}
|
|
303
275
|
async function refreshStatus(){try{renderRateLimitStatus(await loadJSON('/api/status'));}catch{renderRateLimitStatus(null);}}
|
|
304
|
-
function rememberJobFilters(){sessionStorage.setItem('agents-relay:repo-filter',
|
|
305
|
-
function jobsQuery(force=false){const params=new URLSearchParams();if(searchQuery.trim())params.set('q',searchQuery.trim());
|
|
276
|
+
function rememberJobFilters(){sessionStorage.setItem('agents-relay:repo-filter',selectedRepo);sessionStorage.setItem('agents-relay:status-filter',selectedStatus);sessionStorage.setItem('agents-relay:job-search',searchQuery);}
|
|
277
|
+
function jobsQuery(force=false){const params=new URLSearchParams();if(searchQuery.trim())params.set('q',searchQuery.trim());if(selectedStatus)params.set('status',selectedStatus);if(selectedRepo)params.set('repo',selectedRepo);if(force)params.set('refresh','1');return params.toString();}
|
|
306
278
|
let jobsRequestSequence=0;
|
|
307
|
-
async function refresh(force=
|
|
279
|
+
async function refresh(force=true){const requestSequence=++jobsRequestSequence;jobsRefresh.disabled=true;jobsFeedback.textContent='';try{const params=jobsQuery(force);const discovered=await loadJSON('/api/jobs'+(params?'?'+params:''));if(requestSequence!==jobsRequestSequence)return;const result=Array.isArray(discovered)?{jobs:discovered,repositories:[...new Set(discovered.map(job=>job.repository).filter(Boolean))].sort()}:discovered;jobs=dedupeJobs(Array.isArray(result.jobs)?result.jobs:[]).sort(compareJobs).slice(0,20);jobRepositories=result.repositories||[];void drawRuntimeSummary();jobs.sort(compareJobs);if(selectedJob&&!jobForSelection(selectedJob)){selectedJob=null;selectedTaskId=null;}if(!selectedJob){const match=location.hash.match(/^#\\/jobs\\/(.+)$/);const fromHash=match?decodeURIComponent(match[1]):null;const fromRoute=jobForSelection(fromHash);selectedJob=jobSelectionKey(fromRoute||jobs[0]);}rememberSelection();rememberJobFilters();renderAll();void loadEventHistory();jobsFeedback.textContent='';connection.textContent='';connection.setAttribute('aria-label','Live durable state connected');connection.className='status-pill ok';}catch(error){if(requestSequence!==jobsRequestSequence)return;jobsFeedback.textContent='Refresh failed: '+error.message;connection.textContent='';connection.setAttribute('aria-label','Durable state unavailable');connection.className='status-pill bad';jobList.replaceChildren(element('li','error-state',text('Could not load jobs: '+error.message)));}finally{if(requestSequence===jobsRequestSequence){jobsRefresh.disabled=false;syncFilters();}}}
|
|
308
280
|
function renderEventScope(){const task=eventTaskId&&selected()?.tasks?.find(candidate=>candidate.id===eventTaskId);eventsAllTasks.disabled=!task;eventsAllTasks.setAttribute('aria-pressed',String(!task));eventsScope.textContent=task?'Task '+task.id+' · newest first · 60 max':'Job-wide · newest first · 60 max';}
|
|
309
281
|
function eventLabel(type){return ({'task.process.launched':'Process launched','task.started':'Worker started','task.process.exited':'Sync process exited','task.process.async_exited':'Async process exited','task.completed':'Worker completed','task.failed':'Worker failed','task.blocked':'Worker blocked','review.started':'Review started','review.approved':'Review approved','review.changes_required':'Review changes required','review.blocked':'Review blocked'})[type]||type||'Event';}
|
|
310
282
|
function eventSeverity(event,level){const type=String(event?.type||'').toLowerCase(),eventLevel=String(event?.level||level||'').toLowerCase(),outcome=String(event?.outcome||event?.status||'').toLowerCase();if(eventLevel==='error'||outcome==='failed'||type.endsWith('.failed')||type==='task.failed')return 'error';if(eventLevel==='warn'||outcome==='blocked'||type.endsWith('.blocked')||type==='task.blocked'||type==='review.blocked')return 'warn';return 'normal';}
|
|
@@ -359,20 +331,15 @@ async function drawShortcuts(){shortcutsStatus.textContent='';shortcutsText.valu
|
|
|
359
331
|
function saveShortcuts(){const value=shortcutsText.value.trim();if(value)localStorage.setItem(CHAT_SHORTCUTS_KEY,value);else localStorage.removeItem(CHAT_SHORTCUTS_KEY);shortcutsText.value=chatShortcutText();shortcutsStatus.textContent='Saved.';}
|
|
360
332
|
async function drawQa(){qaStatus.textContent='Loading…';try{const result=await loadJSON('/api/troubleshooter-qa');qaText.value=result.text;qaRevision=result.hash;qaStatus.textContent='Loaded';}catch(error){qaStatus.textContent='Load failed: '+error.message;}}
|
|
361
333
|
async function saveQa(){qaSave.disabled=true;qaStatus.textContent='Saving…';try{const response=await fetch('/api/troubleshooter-qa',{method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify({text:qaText.value,hash:qaRevision})});if(!response.ok)throw new Error(response.status+' '+await response.text());const saved=await response.json();qaRevision=saved.hash;qaStatus.textContent='Saved to Markdown file';}catch(error){qaStatus.textContent='Save failed: '+error.message;}finally{qaSave.disabled=false;}}
|
|
362
|
-
function showView(view){const isJobs=view==='jobs';const isUsage=view==='usage';const isGraphs=view==='graphs';const isRules=view==='rules';const isShortcuts=view==='shortcuts';const isQa=view==='qa';qaView.hidden=!isQa;qaTab.setAttribute('aria-selected',String(isQa));jobsView.hidden=!isJobs;jobsWorkspace.hidden=!isJobs;usageView.hidden=!isUsage;graphsView.hidden=!isGraphs;rulesView.hidden=!isRules;shortcutsView.hidden=!isShortcuts;jobsTab.setAttribute('aria-selected',String(isJobs));usageTab.setAttribute('aria-selected',String(isUsage));graphsTab.setAttribute('aria-selected',String(isGraphs));rulesTab.setAttribute('aria-selected',String(isRules));shortcutsTab.setAttribute('aria-selected',String(isShortcuts));if(
|
|
334
|
+
function showView(view){const isJobs=view==='jobs';const isUsage=view==='usage';const isGraphs=view==='graphs';const isRules=view==='rules';const isShortcuts=view==='shortcuts';const isQa=view==='qa';qaView.hidden=!isQa;qaTab.setAttribute('aria-selected',String(isQa));jobsView.hidden=!isJobs;jobsWorkspace.hidden=!isJobs;usageView.hidden=!isUsage;graphsView.hidden=!isGraphs;rulesView.hidden=!isRules;shortcutsView.hidden=!isShortcuts;jobsTab.setAttribute('aria-selected',String(isJobs));usageTab.setAttribute('aria-selected',String(isUsage));graphsTab.setAttribute('aria-selected',String(isGraphs));rulesTab.setAttribute('aria-selected',String(isRules));shortcutsTab.setAttribute('aria-selected',String(isShortcuts));if(isUsage)void drawUsage();else if(isGraphs)void drawGraphs();else if(isRules)void drawRules();else if(isQa)void drawQa();else if(!isJobs)void drawShortcuts();}
|
|
363
335
|
jobsTab.addEventListener('click',()=>{location.hash=selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs';showView('jobs');});
|
|
364
336
|
jobsRefresh.addEventListener('click',()=>void refresh(true));
|
|
365
|
-
jobsReset.addEventListener('click',()=>{searchQuery='';selectedStatuses=[...${JSON.stringify(DEFAULT_DASHBOARD_JOB_STATUSES)}];selectedRepos=[];jobsPage=1;closeFilterMenus();void refresh(false);});
|
|
366
|
-
jobsPrev.addEventListener('click',()=>{if(jobsPage>1){jobsPage-=1;void refresh(false);}});
|
|
367
|
-
jobsNext.addEventListener('click',()=>{if(jobsPage<jobsTotalPages){jobsPage+=1;void refresh(false);}});
|
|
368
337
|
eventsAllTasks.addEventListener('click',()=>{eventTaskId=null;clearEvents();renderEventScope();void loadEventHistory();});
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
document.addEventListener('click',event=>{const target=event.target instanceof Node?event.target:null;const inStatus=statusFilter.contains(target)||statusFilterMenu.contains(target);const inRepo=repoFilter.contains(target)||repoFilterMenu.contains(target);if(!inStatus&&!inRepo)closeFilterMenus(null);});
|
|
375
|
-
document.addEventListener('keydown',event=>{if(event.key!=='Escape')return;const active=document.activeElement;if(statusFilterMenu.hidden===false&&(statusFilterMenu.contains(active)||active===statusFilter)){statusFilterMenu.hidden=true;statusFilter.setAttribute('aria-expanded','false');statusFilter.focus();}if(repoFilterMenu.hidden===false&&(repoFilterMenu.contains(active)||active===repoFilter)){repoFilterMenu.hidden=true;repoFilter.setAttribute('aria-expanded','false');repoFilter.focus();}});
|
|
338
|
+
function applyLocalJobFilters(){rememberJobFilters();renderJobList();}
|
|
339
|
+
jobsReset.addEventListener('click',()=>{searchQuery='';selectedStatus='';selectedRepo='';syncFilters();applyLocalJobFilters();});
|
|
340
|
+
repoFilter.addEventListener('change',()=>{selectedRepo=repoFilter.value;applyLocalJobFilters();});
|
|
341
|
+
jobSearch.addEventListener('input',()=>{searchQuery=jobSearch.value;applyLocalJobFilters();});
|
|
342
|
+
statusFilter.addEventListener('change',()=>{selectedStatus=statusFilter.value;applyLocalJobFilters();});
|
|
376
343
|
selectedJobRefresh.addEventListener('click',()=>void refreshSelected());
|
|
377
344
|
mobileJobsBack.addEventListener('click',()=>{location.hash='#/jobs';syncMobileJobsView();window.scrollTo({top:0,behavior:'instant'});});
|
|
378
345
|
mobileJobs.addEventListener('change',syncMobileJobsView);
|
|
@@ -404,10 +371,10 @@ stream.onopen=()=>{connection.textContent='';connection.setAttribute('aria-label
|
|
|
404
371
|
stream.onerror=()=>{connection.textContent='';connection.setAttribute('aria-label','Event stream reconnecting');connection.className='status-pill warn';};
|
|
405
372
|
stream.onmessage=message=>{const payload=parseMessage(message);if(payload?.job){upsertFreshJob(payload.job);renderAll();void drawRuntimeSummary();}};
|
|
406
373
|
stream.addEventListener('relay',message=>{const payload=parseMessage(message);if(payload?.job)upsertFreshJob(payload.job);else if(payload?.statePatch)applyStatePatch(payload.statePatch);if(payload?.event)addEvent(payload.event);renderAll();void drawRuntimeSummary();});
|
|
407
|
-
if(location.hash==='#/usage')showView('usage');else if(graphFromHash())showView('graphs');else if(location.hash==='#/execution-router-rules')showView('rules');else if(location.hash==='#/shortcuts')showView('shortcuts');else if(location.hash==='#/troubleshooter-qa')showView('qa');else showView('jobs');
|
|
374
|
+
if(location.hash==='#/usage')showView('usage');else if(graphFromHash())showView('graphs');else if(location.hash==='#/execution-router-rules')showView('rules');else if(location.hash==='#/shortcuts')showView('shortcuts');else if(location.hash==='#/troubleshooter-qa')showView('qa');else showView('jobs');void refresh(false);
|
|
408
375
|
setInterval(()=>{if(location.hash==='#/usage')void drawUsage();},30000);
|
|
409
376
|
setInterval(()=>void refreshVersion(),30000);
|
|
410
|
-
async function refreshSelected(){const job=selected();if(!job){await refresh();return;}selectedJobRefresh.disabled=true;
|
|
377
|
+
async function refreshSelected(){const job=selected();if(!job){await refresh();return;}selectedJobRefresh.disabled=true;try{const params=new URLSearchParams({id:job.id,repo:job.repository,pr:String(job.prNumber)});const updated=await loadJSON('/api/job?'+params.toString());const index=jobs.findIndex(candidate=>jobSelectionKey(candidate)===jobSelectionKey(job));if(index>=0)jobs[index]=updated;else jobs.push(updated);selectedJob=jobSelectionKey(updated);rememberSelection();renderAll();connection.textContent='';connection.setAttribute('aria-label','Live durable state connected');connection.className='status-pill ok';}catch(error){connection.textContent='';connection.setAttribute('aria-label','Durable state unavailable');connection.className='status-pill bad';}finally{selectedJobRefresh.disabled=false;}}
|
|
411
378
|
</script>
|
|
412
379
|
</body>
|
|
413
380
|
</html>`;
|
package/dist/dashboard-style.js
CHANGED
|
@@ -22,23 +22,22 @@ export const dashboardStyles = ` <style>
|
|
|
22
22
|
.panel-head { display:flex; align-items:center; justify-content:space-between; gap:.6rem; padding:.65rem .75rem; border-bottom:1px solid var(--border); }
|
|
23
23
|
.panel-head h2,.panel-head h3 { margin:0; color:var(--muted); font-size:.75rem; letter-spacing:.07em; text-transform:uppercase; min-width:0; }\n #workspace-heading { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }\n #selected-job-title { display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
24
24
|
.panel-actions { display:flex; align-items:center; gap:.55rem; flex-wrap:wrap; min-width:0; }
|
|
25
|
-
#jobs-view .panel-head { display:
|
|
26
|
-
.
|
|
27
|
-
.jobs-actions {
|
|
28
|
-
.
|
|
29
|
-
.
|
|
30
|
-
.filter
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
.
|
|
35
|
-
.
|
|
36
|
-
.filter
|
|
37
|
-
.
|
|
38
|
-
.
|
|
39
|
-
.
|
|
40
|
-
.
|
|
41
|
-
.job-search { flex:1 1 10rem; width:12rem; min-width:0; max-width:20rem; border:1px solid var(--border); border-radius:.45rem; background:var(--surface); color:var(--text); padding:.3rem .5rem; font:inherit; font-size:.72rem; }
|
|
25
|
+
#jobs-view .panel-head { display:flex; align-items:center; justify-content:space-between; gap:.65rem; }
|
|
26
|
+
#jobs-view .panel-actions { display:flex; align-items:center; justify-content:flex-end; gap:.4rem; flex-wrap:wrap; min-width:0; }
|
|
27
|
+
.job-filters,.jobs-actions { display:flex; align-items:center; gap:.35rem; min-width:0; }
|
|
28
|
+
.job-filters { flex:1 1 auto; }
|
|
29
|
+
.jobs-actions { flex:none; }
|
|
30
|
+
.job-filter,.refresh-button { min-height:2rem; border:1px solid var(--border); border-radius:.45rem; background:var(--surface); color:var(--text); padding:.3rem .5rem; font:inherit; font-size:.72rem; font-weight:650; }
|
|
31
|
+
.job-filter { width:8rem; max-width:8rem; flex:none; }
|
|
32
|
+
#status-filter { width:9.5rem; max-width:9.5rem; font-size:.66rem; }
|
|
33
|
+
#repo-filter { width:8rem; max-width:8rem; }
|
|
34
|
+
.refresh-button { color:var(--muted); font-weight:700; cursor:pointer; transition:border-color .15s ease,background .15s ease; }
|
|
35
|
+
.refresh-button:hover { background:#ffffff10; color:var(--text); }
|
|
36
|
+
.job-filter:focus-visible,.refresh-button:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
|
|
37
|
+
.jobs-feedback { flex:none; min-width:0; max-width:12rem; font-size:.67rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
38
|
+
.jobs-feedback:empty { display:none; }
|
|
39
|
+
.jobs-feedback:not(:empty) { flex-basis:100%; max-width:none; text-align:right; white-space:normal; }
|
|
40
|
+
.job-search { flex:1 1 11rem; width:12rem; min-width:8rem; max-width:18rem; border:1px solid var(--border); border-radius:.45rem; background:var(--surface); color:var(--text); padding:.3rem .5rem; font:inherit; font-size:.72rem; }
|
|
42
41
|
.job-search:focus-visible { outline:2px solid var(--accent); outline-offset:1px; }
|
|
43
42
|
.quick-filter { border:1px solid var(--border); border-radius:.35rem; background:var(--surface); color:var(--muted); padding:.27rem .42rem; font:inherit; font-size:.7rem; font-weight:750; cursor:pointer; }
|
|
44
43
|
.quick-filter[aria-pressed="true"] { border-color:var(--accent); background:var(--accent2); color:var(--accent); }
|
|
@@ -159,18 +158,15 @@ export const dashboardStyles = ` <style>
|
|
|
159
158
|
body[data-mobile-jobs-view="detail"] #jobs-view { display:none; }
|
|
160
159
|
.mobile-only { display:inline-flex; }
|
|
161
160
|
#jobs-view .panel-head { display:grid; align-items:start; }
|
|
162
|
-
#jobs-view .panel-actions { display:grid; grid-template-columns:1fr; width:100%; gap:.5rem; }
|
|
161
|
+
#jobs-view .panel-actions { display:grid; grid-template-columns:minmax(0,1fr) auto; width:100%; gap:.5rem; }
|
|
163
162
|
.runtime-summary { display:none; }
|
|
164
163
|
#jobs-view .job-filters { width:100%; display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.4rem; }
|
|
164
|
+
#jobs-view .job-filters { grid-column:1 / -1; }
|
|
165
165
|
#jobs-view .job-search { grid-column:1 / -1; width:100%; max-width:none; }
|
|
166
|
-
#jobs-view .filter
|
|
167
|
-
#jobs-view .
|
|
168
|
-
#jobs-view .
|
|
169
|
-
#jobs-view .filter-value { overflow:visible; text-overflow:clip; white-space:normal; font-size:.72rem; line-height:1.2; }
|
|
170
|
-
#jobs-view .jobs-actions { width:100%; display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; }
|
|
166
|
+
#jobs-view .job-filter { width:100%; max-width:none; min-width:0; text-transform:none; font-size:.72rem; }
|
|
167
|
+
#jobs-view .jobs-actions { grid-column:1 / -1; justify-content:flex-end; }
|
|
168
|
+
#jobs-view .jobs-feedback { grid-column:1 / -1; text-align:right; }
|
|
171
169
|
#jobs-view .refresh-button { min-height:2.5rem; }
|
|
172
|
-
#jobs-view .jobs-pagination { min-width:0; justify-content:center; }
|
|
173
|
-
#jobs-view #jobs-page { overflow:hidden; text-overflow:ellipsis; }
|
|
174
170
|
#jobs-view .panel-body { max-height:none; }
|
|
175
171
|
#jobs-workspace { gap:.65rem; }
|
|
176
172
|
#jobs-workspace .panel-body { max-height:none; }
|
package/dist/dashboard.js
CHANGED
|
@@ -209,8 +209,7 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
209
209
|
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
210
210
|
const forceRefresh = url.searchParams.get('refresh') === '1';
|
|
211
211
|
const jobs = await discoveredJobs(forceRefresh);
|
|
212
|
-
const
|
|
213
|
-
const body = usesSearchContract ? searchDashboardJobs(jobs, url.searchParams) : jobs;
|
|
212
|
+
const body = searchDashboardJobs(jobs, url.searchParams);
|
|
214
213
|
if (!res.headersSent)
|
|
215
214
|
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
216
215
|
if (!res.writableEnded)
|
package/dist/relayd.js
CHANGED
|
@@ -172,11 +172,11 @@ export async function runDaemon(argv) {
|
|
|
172
172
|
}
|
|
173
173
|
return prepareLocalJobExecutionWorktree(canonical, await store.load(job.id), workspaceRoot);
|
|
174
174
|
} });
|
|
175
|
-
const refreshAll = async (
|
|
175
|
+
const refreshAll = async (_forceRefresh = false) => {
|
|
176
176
|
cachedOverviews = await inventoryFromLocal();
|
|
177
177
|
return cachedOverviews;
|
|
178
178
|
};
|
|
179
|
-
const overviews = async (
|
|
179
|
+
const overviews = async (forceRefresh = false, _trigger = 'internal.unknown') => refreshAll(forceRefresh);
|
|
180
180
|
const refreshPullRequestSnapshot = async (repository, prNumber, _trigger = 'internal', _options = {}) => {
|
|
181
181
|
const inventory = await refreshAll();
|
|
182
182
|
return {
|