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.
@@ -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 DASHBOARD_JOB_PAGE_SIZE = 20;
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 terminal = (job) => ['COMPLETED', 'CANCELLED'].includes(job.state) || ['MERGED', 'CLOSED'].includes(job.githubState ?? '');
11
- const group = (job) => terminal(job) ? 3 : job.draft || job.state === 'DRAFT' ? 2 : 0;
12
- const groupDelta = group(a) - group(b);
13
- if (groupDelta)
14
- return groupDelta;
15
- const mergePendingDelta = Number(b.runtimeStatus === 'MERGE_PENDING') - Number(a.runtimeStatus === 'MERGE_PENDING');
16
- if (mergePendingDelta)
17
- return mergePendingDelta;
18
- return String(b.updatedAt ?? '').localeCompare(String(a.updatedAt ?? ''));
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 requestedStatuses = [...new Set([...query.getAll('status'), ...query.getAll('statuses')].flatMap(value => value.split(',')).map(value => value.trim().toUpperCase()).filter(Boolean))];
61
- const statuses = requestedStatuses.length > 0 ? requestedStatuses : [...DEFAULT_DASHBOARD_JOB_STATUSES];
62
- const repositories = [...new Set([...query.getAll('repo'), ...query.getAll('repositories'), ...query.getAll('repos')].map(value => value.trim()).filter(Boolean))];
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 parsedPage = Number(query.get('page'));
65
- const page = Number.isInteger(parsedPage) && parsedPage > 0 ? parsedPage : 1;
66
- const parsedPageSize = Number(query.get('pageSize'));
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 === status);
71
- const matchesRepository = (job) => repositories.length === 0 || repositories.includes(job.repository);
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((safePage - 1) * pageSize, safePage * pageSize),
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
  }
@@ -1,5 +1,5 @@
1
1
  import { dashboardStyles } from './dashboard-style.js';
2
- import { DASHBOARD_JOB_PAGE_SIZE, DEFAULT_DASHBOARD_JOB_STATUSES } from './dashboard-data.js';
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"><div class="filter-dropdown"><button id="status-filter" class="filter-trigger" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="status-filter-menu"><span class="filter-label">Status</span><span id="status-filter-summary" class="filter-value">Default</span></button><div id="status-filter-menu" class="filter-menu" role="group" aria-label="Status options" hidden><label class="filter-option"><input type="checkbox" name="status" value="DRAFT"><span>DRAFT</span></label><label class="filter-option"><input type="checkbox" name="status" value="ACTIVE"><span>ACTIVE</span></label><label class="filter-option"><input type="checkbox" name="status" value="COMPLETED"><span>COMPLETED</span></label><label class="filter-option"><input type="checkbox" name="status" value="CANCELLED"><span>CANCELLED</span></label></div></div><div class="filter-dropdown"><button id="repo-filter" class="filter-trigger" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="repo-filter-menu"><span class="filter-label">Repositories</span><span id="repo-filter-summary" class="filter-value">All</span></button><div id="repo-filter-menu" class="filter-menu" role="group" aria-label="Repository options" hidden></div></div></div><div class="jobs-actions"><button id="jobs-reset" class="refresh-button" type="button">Reset</button><div class="jobs-pagination"><button id="jobs-prev" class="refresh-button" type="button">Previous</button><span id="jobs-page" class="subtle">Page 1</span><button id="jobs-next" class="refresh-button" type="button">Next</button></div><button id="jobs-refresh" class="refresh-button" type="button">Refresh</button></div></div></div>
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 statusFilterMenu=document.querySelector('#status-filter-menu');
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 selectedRepos=storedValues('agents-relay:repo-filter');
188
- let selectedStatuses=sessionStorage.getItem('agents-relay:status-filter')===null?[...${JSON.stringify(DEFAULT_DASHBOARD_JOB_STATUSES)}]:storedValues('agents-relay:status-filter');
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 storedValues(key){const stored=sessionStorage.getItem(key);if(!stored)return [];try{const parsed=JSON.parse(stored);if(Array.isArray(parsed))return parsed.filter(value=>typeof value==='string'&&value);if(typeof parsed==='string'&&parsed)return [parsed];}catch{return [stored];}return [];}
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){const statuses=selectedStatuses.length?selectedStatuses:${JSON.stringify(DEFAULT_DASHBOARD_JOB_STATUSES)};return statuses.some(status=>status==='DRAFT'?job.draft||job.state==='DRAFT':job.state===status);}
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=>(!selectedRepos.length||selectedRepos.includes(job.repository))&&matchesQuickFilter(job)&&matchesSearch(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 validRepos=new Set(repos);
239
- const existingInputs=[...repoFilterMenu.querySelectorAll('input')];
240
- // Preserve checkbox nodes while their options are unchanged so keyboard focus survives selection.
241
- if(existingInputs.length!==repos.length||existingInputs.some((input,index)=>input.value!==repos[index])){
242
- const focusedRepo=repoFilterMenu.contains(document.activeElement)?document.activeElement.value:null;
243
- repoFilterMenu.replaceChildren(...repos.map(repo=>{const label=document.createElement('label');label.className='filter-option';const input=document.createElement('input');input.type='checkbox';input.name='repo';input.value=repo;label.append(input,element('span','',text(repo)));return label;}));
244
- if(focusedRepo)for(const input of repoFilterMenu.querySelectorAll('input'))if(input.value===focusedRepo){input.focus();break;}
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 isTerminalJob(job){return ['COMPLETED','CANCELLED'].includes(job.state)||['MERGED','CLOSED'].includes(job.githubState);}
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',JSON.stringify(selectedRepos));sessionStorage.setItem('agents-relay:status-filter',JSON.stringify(selectedStatuses));sessionStorage.setItem('agents-relay:job-search',searchQuery);}
305
- function jobsQuery(force=false){const params=new URLSearchParams();if(searchQuery.trim())params.set('q',searchQuery.trim());for(const status of selectedStatuses)params.append('status',status);for(const repository of selectedRepos)params.append('repo',repository);params.set('page',String(jobsPage));params.set('pageSize',String(${DASHBOARD_JOB_PAGE_SIZE}));if(force)params.set('refresh','1');return params.toString();}
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=false){const requestSequence=++jobsRequestSequence;jobsRefresh.disabled=true;jobsRefresh.textContent='Refreshing…';jobsPrev.disabled=true;jobsNext.disabled=true;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(),total:discovered.length,page:1,totalPages:1}:discovered;jobs=dedupeJobs(Array.isArray(result.jobs)?result.jobs:[]);jobRepositories=result.repositories||[];jobsTotal=result.total||0;jobsPage=result.page||1;jobsTotalPages=result.totalPages||1;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();connection.textContent='';connection.setAttribute('aria-label','Live durable state connected');connection.className='status-pill ok';}catch(error){if(requestSequence!==jobsRequestSequence)return;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;jobsRefresh.textContent='Refresh';syncFilters();}}}
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(isJobs)void refresh();else if(isUsage)void drawUsage();else if(isGraphs)void drawGraphs();else if(isRules)void drawRules();else if(isQa)void drawQa();else void drawShortcuts();}
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
- repoFilter.addEventListener('click',()=>toggleFilterMenu(repoFilter,repoFilterMenu));
370
- repoFilterMenu.addEventListener('change',()=>{selectedRepos=[...repoFilterMenu.querySelectorAll('input:checked')].map(input=>input.value);jobsPage=1;syncFilters();void refresh(false);});
371
- jobSearch.addEventListener('input',()=>{searchQuery=jobSearch.value;jobsPage=1;void refresh(false);});
372
- statusFilter.addEventListener('click',()=>toggleFilterMenu(statusFilter,statusFilterMenu));
373
- statusFilterMenu.addEventListener('change',()=>{selectedStatuses=[...statusFilterMenu.querySelectorAll('input:checked')].map(input=>input.value);jobsPage=1;syncFilters();void refresh(false);});
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;selectedJobRefresh.textContent='Refreshing…';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;selectedJobRefresh.textContent='Refresh';}}
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>`;
@@ -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:grid; grid-template-columns:minmax(0,1fr); align-items:start; gap:.45rem; }
26
- .job-filters,.jobs-actions { display:flex; align-items:center; gap:.4rem; flex-wrap:wrap; min-width:0; }
27
- .jobs-actions { justify-content:flex-end; }
28
- .filter-trigger,.refresh-button { min-height:2rem; border:1px solid var(--border); border-radius:.45rem; background:var(--surface); color:var(--muted); padding:.3rem .55rem; font:inherit; font-size:.72rem; font-weight:700; cursor:pointer; transition:border-color .15s ease,background .15s ease; }
29
- .filter-trigger { display:inline-flex; align-items:center; gap:.4rem; max-width:17rem; }
30
- .filter-trigger:hover,.refresh-button:hover { background:#ffffff10; color:var(--text); }
31
- .filter-trigger:focus-visible,.refresh-button:focus-visible,.filter-option input:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
32
- .filter-trigger[aria-expanded="true"] { border-color:var(--accent); color:var(--text); }
33
- .filter-label { flex:0 0 auto; }
34
- .filter-value { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--text); font-weight:600; }
35
- .filter-dropdown { position:relative; min-width:0; }
36
- .filter-menu { position:absolute; top:calc(100% + .3rem); left:0; z-index:30; width:max-content; min-width:11rem; max-width:calc(100vw - 1.5rem); max-height:17rem; overflow:auto; border:1px solid var(--border); border-radius:.55rem; background:var(--surface); box-shadow:0 12px 34px #00000026; padding:.3rem; }
37
- .filter-option { display:grid; grid-template-columns:auto minmax(0,1fr); align-items:center; gap:.45rem; padding:.4rem .5rem; border-radius:.4rem; font-size:.74rem; font-weight:600; color:var(--text); cursor:pointer; }
38
- .filter-option:hover { background:#ffffff10; }
39
- .filter-option input { width:.95rem; height:.95rem; margin:0; accent-color:var(--accent); }
40
- .jobs-pagination { display:flex; gap:.3rem; align-items:center; white-space:nowrap; }
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-trigger { width:100%; max-width:none; min-height:2.5rem; }
167
- #jobs-view .filter-trigger { display:grid; align-content:center; justify-items:start; gap:.1rem; }
168
- #jobs-view .filter-label { font-size:.6rem; letter-spacing:.04em; text-transform:uppercase; color:var(--muted); }
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 usesSearchContract = ['q', 'status', 'statuses', 'repo', 'repositories', 'repos', 'page', 'pageSize'].some(name => url.searchParams.has(name));
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 (_full = false) => {
175
+ const refreshAll = async (_forceRefresh = false) => {
176
176
  cachedOverviews = await inventoryFromLocal();
177
177
  return cachedOverviews;
178
178
  };
179
- const overviews = async (_forceRefresh = false, _trigger = 'internal.unknown') => refreshAll();
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "2.0.179",
3
+ "version": "2.0.181",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "repository": {
6
6
  "type": "git",