@shapeshift-labs/frontier-loom-ui 0.1.1 → 0.1.2
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/client.js +296 -31
- package/dist/client.js.map +1 -1
- package/dist/public/styles.css +266 -12
- package/dist/server.d.ts +5 -2
- package/dist/server.js +2078 -94
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
package/dist/client.js
CHANGED
|
@@ -3,6 +3,7 @@ const contentTabs = [
|
|
|
3
3
|
{ id: 'work', label: 'Overview' },
|
|
4
4
|
{ id: 'board', label: 'Board' },
|
|
5
5
|
{ id: 'swarm', label: 'Swarm' },
|
|
6
|
+
{ id: 'lanes', label: 'Lanes' },
|
|
6
7
|
{ id: 'performance', label: 'Performance' },
|
|
7
8
|
{ id: 'history', label: 'History' },
|
|
8
9
|
{ id: 'testing', label: 'Testing' },
|
|
@@ -21,6 +22,7 @@ let chartPopover;
|
|
|
21
22
|
let activeContributionTarget;
|
|
22
23
|
const taskDetailsCache = new Map();
|
|
23
24
|
const taskDetailsPending = new Set();
|
|
25
|
+
const taskFileDiffOpenStates = new Map();
|
|
24
26
|
const humanAnswerDrafts = new Map();
|
|
25
27
|
const humanAnswerStates = new Map();
|
|
26
28
|
root?.addEventListener('click', (event) => {
|
|
@@ -70,6 +72,14 @@ root?.addEventListener('click', (event) => {
|
|
|
70
72
|
}
|
|
71
73
|
return;
|
|
72
74
|
});
|
|
75
|
+
root?.addEventListener('toggle', (event) => {
|
|
76
|
+
const target = event.target instanceof Element
|
|
77
|
+
? event.target.closest('details[data-task-file-diff-key]')
|
|
78
|
+
: null;
|
|
79
|
+
if (!target)
|
|
80
|
+
return;
|
|
81
|
+
setTaskFileDiffOpenState(target);
|
|
82
|
+
}, true);
|
|
73
83
|
root?.addEventListener('input', (event) => {
|
|
74
84
|
const target = event.target instanceof Element
|
|
75
85
|
? event.target.closest('textarea[data-human-answer-code]')
|
|
@@ -240,6 +250,7 @@ async function refresh() {
|
|
|
240
250
|
function renderDashboard(dashboard, signature = dashboardSignature(dashboard)) {
|
|
241
251
|
hideChartPopover();
|
|
242
252
|
captureScrollPositions();
|
|
253
|
+
captureTaskFileDiffOpenStates();
|
|
243
254
|
pruneResolvedHumanActionDrafts(dashboard);
|
|
244
255
|
const lanes = laneRollups(dashboard);
|
|
245
256
|
root?.replaceChildren(_jsx(DashboardView, { dashboard: dashboard, lanes: lanes }));
|
|
@@ -277,6 +288,7 @@ function DashboardView({ dashboard, lanes }) {
|
|
|
277
288
|
void fetchTaskDetails(selectedTask);
|
|
278
289
|
return _jsxs("main", { className: "shell", children: [_jsx("section", { className: "operator-grid", children: _jsxs("section", { className: "operator-main", children: [_jsx("nav", { className: "content-tabs", role: "tablist", "aria-label": "Dashboard sections", children: contentTabs.map((tab) => contentTab(tab.id, tab.label, tabMeta(tab.id, {
|
|
279
290
|
jobs: visibleJobs.length,
|
|
291
|
+
lanes: dashboardLaneCount(dashboard, lanes),
|
|
280
292
|
questions: humanActionRows(visibleJobs, audit, dashboard).length,
|
|
281
293
|
events: visibleEvents.length,
|
|
282
294
|
sources: sourceEntries.length
|
|
@@ -296,12 +308,14 @@ function contentPanel(tab, input) {
|
|
|
296
308
|
if (tab === 'work')
|
|
297
309
|
return _jsx(Panel, { title: "Overview", meta: `${text(input.visibleJobs.length)} tasks`, hideHead: true, children: _jsx(WorkOverview, { dashboard: input.dashboard, lanes: input.lanes, jobs: input.visibleJobs, attention: input.attention, audit: input.audit, success: input.success }) });
|
|
298
310
|
if (tab === 'board')
|
|
299
|
-
return _jsx(Panel, { title: "Task board", meta:
|
|
311
|
+
return _jsx(Panel, { title: "Task board", meta: boardPanelMeta(input.dashboard, boardItems, input.visibleJobs), children: _jsx(TaskBoard, { dashboard: input.dashboard, jobs: input.visibleJobs }) });
|
|
300
312
|
if (tab === 'swarm') {
|
|
301
313
|
const sourceKind = dashboardSourceKind(input.dashboard);
|
|
302
314
|
const activeCount = activeAgentTaskCount(input.dashboard, input.visibleJobs);
|
|
303
315
|
return _jsx(Panel, { title: sourceKind === 'demo' ? 'Demo agents' : 'Active agents', meta: sourceKind === 'demo' ? 'fixture examples' : activeCount ? `${text(activeCount)} running` : 'none running', children: _jsx(AgentWork, { dashboard: input.dashboard, jobs: input.visibleJobs }) });
|
|
304
316
|
}
|
|
317
|
+
if (tab === 'lanes')
|
|
318
|
+
return _jsx(Panel, { title: "Lanes", meta: `${text(dashboardLaneCount(input.dashboard, input.lanes))} lanes`, children: _jsx(LaneWork, { dashboard: input.dashboard, lanes: input.lanes }) });
|
|
305
319
|
if (tab === 'performance')
|
|
306
320
|
return _jsx(Panel, { title: "Performance", meta: performanceTabMeta(input.dashboard, input.visibleJobs), children: _jsx(PerformanceView, { dashboard: input.dashboard, jobs: input.visibleJobs, attention: input.attention, audit: input.audit }) });
|
|
307
321
|
if (tab === 'history')
|
|
@@ -310,6 +324,14 @@ function contentPanel(tab, input) {
|
|
|
310
324
|
return _jsx(Panel, { title: "Testing", meta: testingTabMeta(input.visibleJobs), children: _jsx(TestingView, { jobs: input.visibleJobs, events: input.visibleEvents }) });
|
|
311
325
|
return _jsx(Panel, { title: "Questions for you", meta: `${text(humanActionRows(input.visibleJobs, input.audit, input.dashboard).length)} open`, children: _jsx(HumanActionQueue, { dashboard: input.dashboard, jobs: input.visibleJobs, audit: input.audit }) });
|
|
312
326
|
}
|
|
327
|
+
function boardPanelMeta(dashboard, items, jobs) {
|
|
328
|
+
const scope = boardScopeLabel(dashboard);
|
|
329
|
+
const hiddenCount = Math.max(0, jobs.length - items.filter((item) => item.boardKind !== 'backlog').length);
|
|
330
|
+
const doneCount = items.filter((item) => taskBoardColumnId(item) === 'done').length;
|
|
331
|
+
if (hiddenCount)
|
|
332
|
+
return `${text(items.length)} ${scope} tasks · ${text(doneCount)} done · ${text(hiddenCount)} hidden resolved noise`;
|
|
333
|
+
return `${text(items.length)} ${scope} tasks · ${text(doneCount)} done`;
|
|
334
|
+
}
|
|
313
335
|
function Panel({ title, meta, children, className, hideHead = false }) {
|
|
314
336
|
return _jsxs("section", { className: className ? `panel ${className}` : 'panel', children: [hideHead ? null : _jsxs("div", { className: "panel-head", children: [_jsx("h2", { children: title }), meta ? _jsx("span", { children: meta }) : null] }), children] });
|
|
315
337
|
}
|
|
@@ -394,33 +416,121 @@ function costMetric(label, value, detail) {
|
|
|
394
416
|
}
|
|
395
417
|
function TaskBoard({ dashboard, jobs }) {
|
|
396
418
|
const items = taskBoardItems(dashboard, jobs);
|
|
397
|
-
const
|
|
419
|
+
const sourceKind = dashboardSourceKind(dashboard);
|
|
420
|
+
const scope = boardScopeLabel(dashboard);
|
|
421
|
+
const columns = taskBoardColumns(items, sourceKind);
|
|
398
422
|
const activeCount = columns.reduce((sum, column) => column.id === 'done' ? sum : sum + column.items.length, 0);
|
|
399
|
-
return _jsx("div", { className: "task-board-layout", "data-scroll-id": "task-board", children: _jsx("div", { className: "task-board-scroll", "data-scroll-id": "task-board-x", "aria-label": `${text(items.length)} AI-managed tasks, ${text(activeCount)} not done`, children: columns.map((column) => _jsxs("section", { className: `task-board-column ${column.id}`, "aria-label": `${column.title}: ${text(column.items.length)} tasks`, children: [_jsxs("header", { className: "task-column-head", children: [_jsxs("div", { children: [_jsx("h3", { children: column.title }), _jsx("span", { children: column.detail })] }), _jsx("b", { children: text(column.items.length) })] }), _jsx("div", { className: "task-column-body", "data-scroll-id": `task-column-${column.id}`, children: column.items.length
|
|
423
|
+
return _jsx("div", { className: "task-board-layout", "data-scroll-id": "task-board", children: _jsx("div", { className: "task-board-scroll", "data-scroll-id": "task-board-x", "aria-label": `${text(items.length)} ${scope} AI-managed tasks, ${text(activeCount)} not done`, children: columns.map((column) => _jsxs("section", { className: `task-board-column ${column.id}`, "aria-label": `${column.title}: ${text(column.items.length)} tasks`, children: [_jsxs("header", { className: "task-column-head", children: [_jsxs("div", { children: [_jsx("h3", { children: column.title }), _jsx("span", { children: column.detail })] }), _jsx("b", { children: text(column.items.length) })] }), _jsx("div", { className: "task-column-body", "data-scroll-id": `task-column-${column.id}`, children: column.items.length
|
|
400
424
|
? column.items.map((job) => _jsx(TaskBoardCard, { job: job }))
|
|
401
425
|
: _jsx("p", { className: "empty tight", children: column.empty }) })] })) }) });
|
|
402
426
|
}
|
|
403
427
|
function AgentWork({ dashboard, jobs }) {
|
|
404
428
|
const items = taskBoardItems(dashboard, jobs).filter(isActiveAgentJob);
|
|
405
429
|
const workers = agentWorkers(items);
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
430
|
+
return _jsxs("div", { className: "agent-work-layout", "data-scroll-id": "swarm", children: [_jsx(SwarmCapacityPanel, { dashboard: dashboard, workers: workers }), _jsxs("section", { className: "agent-roster-panel", "aria-label": "Active agent roster", children: [_jsxs("div", { className: "agent-roster-head", children: [_jsx("span", { children: "Agent" }), _jsx("span", { children: "Current work" }), _jsx("span", { children: "Model" }), _jsx("span", { children: "Run time" }), _jsx("span", { children: "Input" }), _jsx("span", { children: "Files" })] }), _jsx("div", { className: "agent-roster-list", "data-scroll-id": "swarm-roster", children: workers.length
|
|
431
|
+
? workers.map((worker, index) => _jsx(AgentWorkerCard, { worker: worker, index: index, now: timeValue(dashboard.generatedAt) ?? Date.now() }))
|
|
432
|
+
: _jsx("p", { className: "agent-roster-empty", children: "No active agents are running right now." }) })] })] });
|
|
433
|
+
}
|
|
434
|
+
function LaneWork({ dashboard, lanes }) {
|
|
435
|
+
const capacity = recordValue(dashboard.capacity);
|
|
436
|
+
const capacityLanes = arrayRecords(capacity.lanes);
|
|
437
|
+
const capacityById = new Map(capacityLanes.map((lane) => [textValue(lane.id, textValue(lane.title, '')), lane]));
|
|
438
|
+
const ids = new Set();
|
|
439
|
+
for (const lane of lanes)
|
|
440
|
+
ids.add(lane.id);
|
|
441
|
+
for (const lane of capacityLanes)
|
|
442
|
+
ids.add(textValue(lane.id, textValue(lane.title, 'lane')));
|
|
443
|
+
const rows = Array.from(ids)
|
|
444
|
+
.filter(Boolean)
|
|
445
|
+
.map((id) => laneDetailRow(id, lanes.find((lane) => lane.id === id), capacityById.get(id)))
|
|
446
|
+
.filter(isActiveLaneRow)
|
|
447
|
+
.sort((left, right) => {
|
|
448
|
+
const group = laneSortGroup(left) - laneSortGroup(right);
|
|
449
|
+
const active = right.runningCount - left.runningCount;
|
|
450
|
+
const queued = right.queuedTaskCount - left.queuedTaskCount;
|
|
451
|
+
const review = (right.needsCoordinatorReviewCount + right.staleCount + right.failedCount) - (left.needsCoordinatorReviewCount + left.staleCount + left.failedCount);
|
|
452
|
+
return group || active || queued || review || left.title.localeCompare(right.title);
|
|
453
|
+
});
|
|
454
|
+
return _jsxs("div", { className: "lane-work-layout", "data-scroll-id": "lanes", children: [_jsx(SwarmCapacityPanel, { dashboard: dashboard, workers: [] }), _jsxs("section", { className: "lane-roster-panel", "aria-label": "Swarm lanes", children: [_jsxs("div", { className: "lane-roster-head", children: [_jsx("span", { children: "Lane" }), _jsx("span", { children: "Work" }), _jsx("span", { children: "Capacity" }), _jsx("span", { children: "Agents" })] }), _jsx("div", { className: "lane-roster-list", "data-scroll-id": "lane-roster", children: rows.length
|
|
455
|
+
? rows.map((lane) => _jsx(LaneRosterRow, { lane: lane }))
|
|
456
|
+
: _jsx("p", { className: "lane-roster-empty", children: "No lanes have active workers right now." }) })] })] });
|
|
457
|
+
}
|
|
458
|
+
function dashboardLaneCount(dashboard, lanes) {
|
|
459
|
+
const ids = new Set();
|
|
460
|
+
for (const lane of lanes)
|
|
461
|
+
ids.add(lane.id);
|
|
462
|
+
for (const lane of arrayRecords(recordValue(dashboard.capacity).lanes))
|
|
463
|
+
ids.add(textValue(lane.id, textValue(lane.title, '')));
|
|
464
|
+
return Array.from(ids)
|
|
465
|
+
.filter(Boolean)
|
|
466
|
+
.map((id) => laneDetailRow(id, lanes.find((lane) => lane.id === id), arrayRecords(recordValue(dashboard.capacity).lanes).find((lane) => textValue(lane.id, textValue(lane.title, '')) === id)))
|
|
467
|
+
.filter(isActiveLaneRow)
|
|
468
|
+
.length;
|
|
469
|
+
}
|
|
470
|
+
function laneDetailRow(id, rollup, capacity) {
|
|
471
|
+
const row = recordValue(capacity);
|
|
472
|
+
return {
|
|
473
|
+
id,
|
|
474
|
+
title: textValue(row.title, id),
|
|
475
|
+
layer: textValue(row.layer, ''),
|
|
476
|
+
model: textValue(row.model, textValue(row.compute, '')),
|
|
477
|
+
jobCount: rollup?.jobCount ?? numberValue(row.totalTaskCount),
|
|
478
|
+
completedCount: rollup?.completedCount ?? numberValue(row.completedCount),
|
|
479
|
+
runningCount: rollup?.runningCount ?? numberValue(row.runningCount),
|
|
480
|
+
failedCount: rollup?.failedCount ?? numberValue(row.failedCount),
|
|
481
|
+
blockedCount: rollup?.blockedCount ?? numberValue(row.blockedCount),
|
|
482
|
+
queuedTaskCount: numberValue(row.queuedTaskCount),
|
|
483
|
+
needsCoordinatorReviewCount: rollup?.needsCoordinatorReviewCount ?? 0,
|
|
484
|
+
staleCount: rollup?.staleCount ?? 0,
|
|
485
|
+
eventCount: rollup?.eventCount ?? 0,
|
|
486
|
+
maxConcurrency: numberValue(row.maxConcurrency) || 1,
|
|
487
|
+
assignedAgents: stringArray(row.assignedAgents)
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function laneSortGroup(lane) {
|
|
491
|
+
if (lane.runningCount > 0)
|
|
492
|
+
return 0;
|
|
493
|
+
if (lane.queuedTaskCount > 0)
|
|
494
|
+
return 1;
|
|
495
|
+
if (lane.needsCoordinatorReviewCount || lane.staleCount || lane.failedCount || lane.blockedCount)
|
|
496
|
+
return 2;
|
|
497
|
+
if (lane.completedCount || lane.eventCount || lane.jobCount)
|
|
498
|
+
return 3;
|
|
499
|
+
return 4;
|
|
500
|
+
}
|
|
501
|
+
function isActiveLaneRow(lane) {
|
|
502
|
+
return lane.runningCount > 0 || lane.assignedAgents.length > 0;
|
|
503
|
+
}
|
|
504
|
+
function LaneRosterRow({ lane }) {
|
|
505
|
+
return _jsxs("article", { className: "lane-roster-row", children: [_jsxs("div", { className: "lane-cell lane-cell-name", children: [_jsx("b", { children: lane.title }), _jsx("small", { children: [lane.layer || 'lane', lane.model].filter(Boolean).join(' · ') })] }), _jsxs("div", { className: "lane-cell lane-cell-work", children: [_jsxs("b", { children: [text(lane.runningCount), " running \u00B7 ", text(lane.queuedTaskCount), " queued"] }), _jsxs("small", { children: [text(lane.completedCount), " done \u00B7 ", text(lane.failedCount), " failed \u00B7 ", text(lane.eventCount), " events"] })] }), _jsxs("div", { className: "lane-cell lane-cell-capacity", children: [_jsxs("b", { children: [text(lane.runningCount), " / ", text(lane.maxConcurrency)] }), _jsx("small", { children: lane.needsCoordinatorReviewCount ? `${text(lane.needsCoordinatorReviewCount)} review` : lane.staleCount ? `${text(lane.staleCount)} stale` : `${text(lane.jobCount)} tracked` })] }), _jsx("div", { className: "lane-cell lane-cell-agents", children: lane.assignedAgents.length
|
|
506
|
+
? lane.assignedAgents.map((agent) => _jsx("code", { children: shortAgentId(agent) }))
|
|
507
|
+
: _jsx("span", { children: "idle" }) })] });
|
|
508
|
+
}
|
|
509
|
+
function SwarmCapacityPanel({ dashboard, workers }) {
|
|
510
|
+
const capacity = recordValue(dashboard.capacity);
|
|
511
|
+
const lanes = arrayRecords(capacity.lanes);
|
|
512
|
+
const openLaneCount = numberValue(capacity.openLaneCount);
|
|
513
|
+
const activeLaneCount = numberValue(capacity.activeLaneCount);
|
|
514
|
+
const maxConcurrency = numberValue(capacity.maxConcurrency);
|
|
515
|
+
const runningAgentCount = numberValue(capacity.runningAgentCount) || workers.filter((worker) => worker.status === 'active').length;
|
|
516
|
+
const queuedTaskCount = numberValue(capacity.queuedTaskCount);
|
|
517
|
+
const manifestLabel = textValue(capacity.manifestId, textValue(capacity.title, 'manifest'));
|
|
518
|
+
return _jsx("section", { className: "swarm-capacity-panel", "aria-label": "Swarm capacity", children: _jsxs("div", { className: "swarm-capacity-summary", children: [_jsxs("div", { children: [_jsx("span", { children: "Concurrency" }), _jsx("b", { children: maxConcurrency ? `${text(runningAgentCount)} / ${text(maxConcurrency)}` : text(runningAgentCount) }), _jsx("small", { children: manifestLabel })] }), _jsxs("div", { children: [_jsx("span", { children: "Lanes" }), _jsx("b", { children: text(openLaneCount || lanes.length) }), _jsxs("small", { children: [text(activeLaneCount), " active \u00B7 ", text(lanes.length), " defined"] })] }), _jsxs("div", { children: [_jsx("span", { children: "Open tasks" }), _jsx("b", { children: text(queuedTaskCount) }), _jsxs("small", { children: [text(numberValue(capacity.totalTaskCount)), " manifest tasks"] })] }), _jsxs("div", { children: [_jsx("span", { children: "Source" }), _jsx("b", { children: text(numberValue(capacity.computeMaxConcurrency) || maxConcurrency || runningAgentCount) }), _jsx("small", { children: artifactLabel(textValue(capacity.manifestPath, 'manifest')) })] })] }) });
|
|
410
519
|
}
|
|
411
520
|
function AgentWorkerCard({ worker, index, now }) {
|
|
412
521
|
const model = agentModelSummary(worker);
|
|
413
522
|
const files = agentTouchedFiles(worker);
|
|
414
523
|
const evidenceCount = agentEvidenceCount(worker);
|
|
524
|
+
const fileFallback = evidenceCount ? 'Evidence only' : worker.status === 'active' ? 'Files pending' : 'No source files';
|
|
415
525
|
const visibleJobs = worker.currentJobs.slice(0, 2);
|
|
416
526
|
const hiddenJobCount = Math.max(0, worker.currentJobs.length - visibleJobs.length);
|
|
417
|
-
return _jsxs("article", { className: `agent-worker-card ${worker.status}`, style: `--agent-color:${worker.color}`, draggable: "false", "aria-label": `${agentDisplayName(worker, index)} assigned to ${text(worker.currentJobs.length)} current tasks`, children: [_jsxs("div", { className: "agent-cell agent-cell-agent", children: [_jsx("span", { className: "agent-status-dot", "aria-hidden": "true" }), _jsxs("div", { children: [_jsx("b", { children: agentDisplayName(worker, index) }), _jsx("small", { children: agentStatusLabel(worker.status) })] })] }), _jsx("div", { className: "agent-cell agent-cell-task", children: _jsxs("div", { className: "agent-task-list", children: [visibleJobs.map((job) => _jsxs("button", { type: "button", className: "agent-task-row", "data-task-card": taskCardId(job), children: [_jsx("code", { children: ticketId(job) }), _jsx("span", { children: taskTitle(job) }), _jsx("small", { children: jobRuntimeLabel(job, now) })] })), hiddenJobCount ? _jsxs("small", { className: "agent-overflow-note", children: ["+", text(hiddenJobCount), " more active ", hiddenJobCount === 1 ? 'ticket' : 'tickets'] }) : null] }) }), _jsx("div", { className: "agent-cell agent-cell-model", children: _jsx("span", { className: "agent-model-pill", children: _jsx("span", { children: model }) }) }), _jsxs("div", { className: "agent-cell agent-cell-runtime", children: [_jsx("b", { children: agentRuntimeValue(worker, now) }), _jsx("small", { children: worker.currentJobs.length === 1 ? 'current ticket' : `${text(worker.currentJobs.length)} tickets` })] }), _jsxs("div", { className: "agent-cell agent-cell-cost", children: [_jsx("b", { children: agentTokenValue(worker) }), _jsxs("small", { children: [agentUncachedTokenValue(worker), " uncached"] })] }), _jsxs("div", { className: "agent-cell agent-cell-files", children: [_jsxs("div", { className: "agent-file-list", children: [files.slice(0, 2).map((file) => _jsx(ArtifactLink, { path: file, label: artifactLabel(file), className: "agent-file-tag" })), files.length > 2 ? _jsxs("span", { className: "agent-file-more", children: ["+", text(files.length - 2)] }) : null, !files.length ? _jsx("span", { className: "agent-file-empty", children:
|
|
527
|
+
return _jsxs("article", { className: `agent-worker-card ${worker.status}`, style: `--agent-color:${worker.color}`, draggable: "false", "aria-label": `${agentDisplayName(worker, index)} assigned to ${text(worker.currentJobs.length)} current tasks`, children: [_jsxs("div", { className: "agent-cell agent-cell-agent", children: [_jsx("span", { className: "agent-status-dot", "aria-hidden": "true" }), _jsxs("div", { children: [_jsx("b", { children: agentDisplayName(worker, index) }), _jsx("small", { children: agentStatusLabel(worker.status) })] })] }), _jsx("div", { className: "agent-cell agent-cell-task", children: _jsxs("div", { className: "agent-task-list", children: [visibleJobs.map((job) => _jsxs("button", { type: "button", className: "agent-task-row", "data-task-card": taskCardId(job), children: [_jsx("code", { children: ticketId(job) }), _jsx("span", { children: taskTitle(job) }), _jsx("small", { children: jobRuntimeLabel(job, now) })] })), hiddenJobCount ? _jsxs("small", { className: "agent-overflow-note", children: ["+", text(hiddenJobCount), " more active ", hiddenJobCount === 1 ? 'ticket' : 'tickets'] }) : null] }) }), _jsx("div", { className: "agent-cell agent-cell-model", children: _jsx("span", { className: "agent-model-pill", children: _jsx("span", { children: model }) }) }), _jsxs("div", { className: "agent-cell agent-cell-runtime", children: [_jsx("b", { children: agentRuntimeValue(worker, now) }), _jsx("small", { children: worker.currentJobs.length === 1 ? 'current ticket' : `${text(worker.currentJobs.length)} tickets` })] }), _jsxs("div", { className: "agent-cell agent-cell-cost", children: [_jsx("b", { children: agentTokenValue(worker) }), _jsxs("small", { children: [agentUncachedTokenValue(worker), " uncached"] })] }), _jsxs("div", { className: "agent-cell agent-cell-files", children: [_jsxs("div", { className: "agent-file-list", children: [files.slice(0, 2).map((file) => _jsx(ArtifactLink, { path: file, label: artifactLabel(file), className: "agent-file-tag" })), files.length > 2 ? _jsxs("span", { className: "agent-file-more", children: ["+", text(files.length - 2)] }) : null, !files.length ? _jsx("span", { className: "agent-file-empty", children: fileFallback }) : null] }), evidenceCount ? _jsxs("small", { children: [text(evidenceCount), " evidence ", evidenceCount === 1 ? 'artifact' : 'artifacts'] }) : null] })] });
|
|
418
528
|
}
|
|
419
529
|
function TaskBoardCard({ job }) {
|
|
420
530
|
const id = taskCardId(job);
|
|
421
531
|
const ticket = ticketId(job);
|
|
422
532
|
const risk = jobRisk(job);
|
|
423
|
-
return _jsxs("article", { className: `task-card ${risk}`, "data-task-card": id, role: "button", tabIndex: 0, draggable: "false", "aria-label": `Open task ${taskTitle(job)}`, children: [_jsxs("div", { className: "task-card-top", children: [_jsx("code", { className: "task-ticket", children: ticket }), _jsx("button", { type: "button", className: "task-id-copy", "data-copy-code": ticket, title: `Copy ${ticket}`, children: "Copy" })] }), _jsx("span", { className: "task-card-kicker", children: taskCardStatus(job) }), _jsx("b", { children: taskTitle(job) }), _jsxs("small", { children: [laneOf(job), " \u00B7 ", taskStatusDetail(job)] }), _jsxs("div", { className: "task-card-foot", children: [
|
|
533
|
+
return _jsxs("article", { className: `task-card ${risk}`, "data-task-card": id, role: "button", tabIndex: 0, draggable: "false", "aria-label": `Open task ${taskTitle(job)}`, children: [_jsxs("div", { className: "task-card-top", children: [_jsx("code", { className: "task-ticket", children: ticket }), _jsx("button", { type: "button", className: "task-id-copy", "data-copy-code": ticket, title: `Copy ${ticket}`, children: "Copy" })] }), _jsx("span", { className: "task-card-kicker", children: taskCardStatus(job) }), _jsx("b", { children: taskTitle(job) }), _jsxs("small", { children: [laneOf(job), " \u00B7 ", taskStatusDetail(job)] }), _jsxs("div", { className: "task-card-foot", children: [_jsx("span", { children: pathSummaryText(job) }), _jsx("span", { children: evidenceSummaryText(job) })] })] });
|
|
424
534
|
}
|
|
425
535
|
function TaskDetailDialog({ job, dashboard }) {
|
|
426
536
|
const title = taskTitle(job);
|
|
@@ -437,7 +547,10 @@ function TaskFileDiffs({ job, details }) {
|
|
|
437
547
|
if (!details && job.boardKind !== 'backlog')
|
|
438
548
|
return _jsxs("section", { className: "task-dialog-section", children: [_jsx("h4", { children: "Files changed" }), _jsx("p", { className: "empty tight", children: "Loading file diffs..." })] });
|
|
439
549
|
if (details?.files?.length)
|
|
440
|
-
return _jsxs("section", { className: "task-dialog-section", children: [_jsx("h4", { children: "Files changed" }), _jsx("div", { className: "task-file-list", children: details.files.map((file) =>
|
|
550
|
+
return _jsxs("section", { className: "task-dialog-section", children: [_jsx("h4", { children: "Files changed" }), _jsx("div", { className: "task-file-list", children: details.files.map((file) => {
|
|
551
|
+
const keys = taskFileDiffKeys(job, file);
|
|
552
|
+
return _jsxs("details", { className: "task-file-diff", "data-task-file-diff-key": keys[0] ?? '', "data-task-file-diff-keys": keys.join('\t'), open: isTaskFileDiffOpen(job, file) ? 'open' : undefined, children: [_jsxs("summary", { children: [_jsx("span", { className: "task-file-name", children: file.path }), _jsxs("small", { children: ["+", text(file.additions), " -", text(file.deletions), file.truncated ? ' · truncated' : ''] }), _jsx(ArtifactLink, { path: file.artifactPath ?? file.path, label: "Reveal", className: "task-file-reveal" })] }), _jsx(DiffRenderer, { file: file })] });
|
|
553
|
+
}) })] });
|
|
441
554
|
if (changedPaths.length)
|
|
442
555
|
return _jsxs("section", { className: "task-dialog-section", children: [_jsx("h4", { children: "Files changed" }), _jsx("p", { className: "empty tight", children: details?.error ? `Diff unavailable: ${details.error}` : 'No patch diff was available in the collected evidence.' }), _jsx(ArtifactPathList, { paths: changedPaths })] });
|
|
443
556
|
return _jsxs("section", { className: "task-dialog-section", children: [_jsx("h4", { children: "Files changed" }), _jsx("p", { className: "empty tight", children: job.boardKind === 'backlog' ? 'No files yet. This item has not produced an implementation patch.' : 'No changed files reported.' })] });
|
|
@@ -541,7 +654,7 @@ function taskBoardItems(dashboard, jobs) {
|
|
|
541
654
|
];
|
|
542
655
|
}
|
|
543
656
|
function isTaskBoardVisibleJob(job) {
|
|
544
|
-
return !isResolvedCoordinatorReviewJob(job);
|
|
657
|
+
return !isResolvedCoordinatorReviewJob(job) || isSuccessfulResolvedCoordinatorReviewJob(job);
|
|
545
658
|
}
|
|
546
659
|
function isResolvedCoordinatorReviewJob(job) {
|
|
547
660
|
const bucket = normalized(job.bucket);
|
|
@@ -554,6 +667,16 @@ function isResolvedCoordinatorDecisionStatus(status) {
|
|
|
554
667
|
const value = normalized(status);
|
|
555
668
|
return Boolean(value) && !['open', 'pending', 'deferred', 'needs-review'].includes(value);
|
|
556
669
|
}
|
|
670
|
+
function isSuccessfulResolvedCoordinatorReviewJob(job) {
|
|
671
|
+
if (!isResolvedCoordinatorReviewJob(job))
|
|
672
|
+
return false;
|
|
673
|
+
const status = normalized(coordinatorDecisionStatus(job));
|
|
674
|
+
const disposition = normalized(job.disposition);
|
|
675
|
+
const jobStatus = normalized(job.status);
|
|
676
|
+
return jobStatus === 'completed'
|
|
677
|
+
|| ['accepted', 'accepted-applied', 'applied', 'committed', 'landed'].includes(status)
|
|
678
|
+
|| ['accepted', 'accepted-applied', 'applied', 'committed', 'landed'].includes(disposition);
|
|
679
|
+
}
|
|
557
680
|
function activeAgentTaskCount(dashboard, jobs) {
|
|
558
681
|
return taskBoardItems(dashboard, jobs).filter(isActiveAgentJob).length;
|
|
559
682
|
}
|
|
@@ -569,9 +692,12 @@ function backlogBoardItems(dashboard) {
|
|
|
569
692
|
return entries.map((entry, index) => {
|
|
570
693
|
const id = textValue(entry.id ?? entry.taskId ?? entry.title, `backlog-${index + 1}`);
|
|
571
694
|
const status = normalized(entry.status ?? entry.state);
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
695
|
+
const requestedColumn = textValue(entry.boardColumn, '');
|
|
696
|
+
const column = ['backlog', 'todo', 'active', 'review', 'ready', 'done', 'blocked'].includes(requestedColumn)
|
|
697
|
+
? requestedColumn
|
|
698
|
+
: ['todo', 'ready', 'queued', 'pending'].includes(status) || Boolean(entry.ready)
|
|
699
|
+
? 'todo'
|
|
700
|
+
: 'backlog';
|
|
575
701
|
return {
|
|
576
702
|
...entry,
|
|
577
703
|
boardKind: 'backlog',
|
|
@@ -586,14 +712,15 @@ function backlogBoardItems(dashboard) {
|
|
|
586
712
|
};
|
|
587
713
|
});
|
|
588
714
|
}
|
|
589
|
-
function taskBoardColumns(items) {
|
|
715
|
+
function taskBoardColumns(items, sourceKind = 'snapshot') {
|
|
716
|
+
const doneDetail = sourceKind === 'lifetime' ? 'lifetime completed output' : 'completed output';
|
|
590
717
|
const columns = [
|
|
591
718
|
{ id: 'backlog', title: 'Backlog', detail: 'not scheduled yet', empty: 'No backlog items.', items: [] },
|
|
592
719
|
{ id: 'todo', title: 'To do', detail: 'ready to start', empty: 'No queued tasks.', items: [] },
|
|
593
720
|
{ id: 'active', title: 'Active', detail: 'running work', empty: 'No active tasks.', items: [] },
|
|
594
721
|
{ id: 'review', title: 'Coordinator review', detail: 'needs decision', empty: 'No tasks need coordinator review.', items: [] },
|
|
595
722
|
{ id: 'ready', title: 'Ready', detail: 'ready to apply', empty: 'No ready patches.', items: [] },
|
|
596
|
-
{ id: 'done', title: 'Done', detail:
|
|
723
|
+
{ id: 'done', title: 'Done', detail: doneDetail, empty: 'No completed tasks.', items: [] },
|
|
597
724
|
{ id: 'blocked', title: 'Blocked', detail: 'waiting or impossible', empty: 'No genuinely blocked tasks.', items: [] }
|
|
598
725
|
];
|
|
599
726
|
const byId = new Map(columns.map((column) => [column.id, column]));
|
|
@@ -697,6 +824,13 @@ function agentDisplayName(worker, index) {
|
|
|
697
824
|
return worker.name;
|
|
698
825
|
return `Agent ${String(index + 1).padStart(2, '0')}`;
|
|
699
826
|
}
|
|
827
|
+
function shortAgentId(value) {
|
|
828
|
+
const clean = value.split(/[\\/]/g).pop() ?? value;
|
|
829
|
+
const parts = clean.split('-').filter(Boolean);
|
|
830
|
+
if (parts.length <= 3)
|
|
831
|
+
return clean;
|
|
832
|
+
return parts.slice(-3).join('-');
|
|
833
|
+
}
|
|
700
834
|
function agentColor(key) {
|
|
701
835
|
return AGENT_COLORS[stableNumber(key) % AGENT_COLORS.length] ?? AGENT_COLORS[0];
|
|
702
836
|
}
|
|
@@ -723,6 +857,8 @@ function agentStatus(job) {
|
|
|
723
857
|
const disposition = normalized(job.disposition ?? job.mergeReadiness);
|
|
724
858
|
if (['running', 'active', 'working', 'in-progress', 'in progress'].includes(status))
|
|
725
859
|
return 'active';
|
|
860
|
+
if (isSuccessfulResolvedCoordinatorReviewJob(job))
|
|
861
|
+
return 'done';
|
|
726
862
|
if (status === 'idle' || bucket === 'idle')
|
|
727
863
|
return 'idle';
|
|
728
864
|
if (isBlockedJob(job))
|
|
@@ -867,6 +1003,8 @@ function taskBoardColumnId(job) {
|
|
|
867
1003
|
return job.boardColumn;
|
|
868
1004
|
if (job.boardKind === 'backlog')
|
|
869
1005
|
return 'backlog';
|
|
1006
|
+
if (isSuccessfulResolvedCoordinatorReviewJob(job))
|
|
1007
|
+
return 'done';
|
|
870
1008
|
if (isBlockedJob(job))
|
|
871
1009
|
return 'blocked';
|
|
872
1010
|
if (isCoordinatorReviewJob(job))
|
|
@@ -911,6 +1049,8 @@ function taskSortRank(job) {
|
|
|
911
1049
|
return 3;
|
|
912
1050
|
if (isReadyJob(job))
|
|
913
1051
|
return 4;
|
|
1052
|
+
if (isSuccessfulResolvedCoordinatorReviewJob(job))
|
|
1053
|
+
return 5;
|
|
914
1054
|
return 5;
|
|
915
1055
|
}
|
|
916
1056
|
function taskCardId(job) {
|
|
@@ -1001,6 +1141,8 @@ function coordinatorDecisionLabel(status) {
|
|
|
1001
1141
|
function coordinatorFacingSignalLabel(value) {
|
|
1002
1142
|
const raw = textValue(value, 'tracked task');
|
|
1003
1143
|
const normalizedValue = normalized(raw);
|
|
1144
|
+
if (normalizedValue === 'review-resolved' || normalizedValue === 'resolved-review')
|
|
1145
|
+
return 'review resolved';
|
|
1004
1146
|
if (normalizedValue === 'needs-human-port' || normalizedValue === 'needs-coordinator-port')
|
|
1005
1147
|
return 'needs coordinator review';
|
|
1006
1148
|
if (normalizedValue === 'needs-human-review' || normalizedValue === 'needs-coordinator-review')
|
|
@@ -1009,6 +1151,8 @@ function coordinatorFacingSignalLabel(value) {
|
|
|
1009
1151
|
return 'needs coordinator decision';
|
|
1010
1152
|
if (normalizedValue === 'failed-evidence')
|
|
1011
1153
|
return 'failed evidence';
|
|
1154
|
+
if (normalizedValue === 'worker-failed')
|
|
1155
|
+
return 'failed worker';
|
|
1012
1156
|
if (normalizedValue === 'ready-to-apply')
|
|
1013
1157
|
return 'ready to apply';
|
|
1014
1158
|
if (normalizedValue === 'patch-candidate')
|
|
@@ -1028,12 +1172,16 @@ function taskTokenSummary(job) {
|
|
|
1028
1172
|
return '-';
|
|
1029
1173
|
}
|
|
1030
1174
|
function taskReasonItems(job) {
|
|
1031
|
-
|
|
1175
|
+
const reasons = uniqueStrings([
|
|
1032
1176
|
...stringArray(job.collectReasonClasses),
|
|
1033
1177
|
...stringArray(job.reasons),
|
|
1034
1178
|
...stringArray(job.contextBudgetWarnings),
|
|
1035
1179
|
...stringArray(job.semanticReadinessReasons)
|
|
1036
|
-
])
|
|
1180
|
+
]);
|
|
1181
|
+
const terminalOrResolved = isResolvedCoordinatorReviewJob(job) || (isCompletedJob(job) && !isNeedsCoordinatorPortJob(job));
|
|
1182
|
+
return reasons
|
|
1183
|
+
.filter((reason) => !terminalOrResolved || !isOpenCoordinatorReviewSignal(reason))
|
|
1184
|
+
.slice(0, 8);
|
|
1037
1185
|
}
|
|
1038
1186
|
function taskFact(label, value) {
|
|
1039
1187
|
return _jsxs("div", { className: "task-fact", children: [_jsx("span", { children: label }), _jsx("b", { children: value || '-' })] });
|
|
@@ -1517,6 +1665,37 @@ function openTaskCard(id) {
|
|
|
1517
1665
|
function taskDialogScrollId(id) {
|
|
1518
1666
|
return `task-dialog-${id}`;
|
|
1519
1667
|
}
|
|
1668
|
+
function captureTaskFileDiffOpenStates() {
|
|
1669
|
+
root?.querySelectorAll('details[data-task-file-diff-key]').forEach((entry) => {
|
|
1670
|
+
setTaskFileDiffOpenState(entry);
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
function taskFileDiffKeys(job, file) {
|
|
1674
|
+
const filePath = textValue(file.path, '');
|
|
1675
|
+
if (!filePath)
|
|
1676
|
+
return [];
|
|
1677
|
+
return uniqueStrings([
|
|
1678
|
+
taskCardId(job),
|
|
1679
|
+
textValue(job.originalJobId, ''),
|
|
1680
|
+
textValue(job.jobId, ''),
|
|
1681
|
+
textValue(job.taskId, ''),
|
|
1682
|
+
textValue(job.workerId, ''),
|
|
1683
|
+
textValue(job.agentId, ''),
|
|
1684
|
+
ticketId(job)
|
|
1685
|
+
].filter(Boolean)).map((id) => `${id}::${filePath}`);
|
|
1686
|
+
}
|
|
1687
|
+
function isTaskFileDiffOpen(job, file) {
|
|
1688
|
+
return taskFileDiffKeys(job, file).some((key) => taskFileDiffOpenStates.get(key));
|
|
1689
|
+
}
|
|
1690
|
+
function taskFileDiffElementKeys(entry) {
|
|
1691
|
+
const keys = textValue(entry.dataset.taskFileDiffKeys, '').split('\t').filter(Boolean);
|
|
1692
|
+
const primary = textValue(entry.dataset.taskFileDiffKey, '');
|
|
1693
|
+
return uniqueStrings([...keys, primary].filter(Boolean));
|
|
1694
|
+
}
|
|
1695
|
+
function setTaskFileDiffOpenState(entry) {
|
|
1696
|
+
for (const key of taskFileDiffElementKeys(entry))
|
|
1697
|
+
taskFileDiffOpenStates.set(key, entry.open);
|
|
1698
|
+
}
|
|
1520
1699
|
async function fetchTaskDetails(job) {
|
|
1521
1700
|
const id = taskCardId(job);
|
|
1522
1701
|
if (!id || taskDetailsCache.has(id) || taskDetailsPending.has(id))
|
|
@@ -1527,7 +1706,11 @@ async function fetchTaskDetails(job) {
|
|
|
1527
1706
|
}
|
|
1528
1707
|
taskDetailsPending.add(id);
|
|
1529
1708
|
try {
|
|
1530
|
-
const
|
|
1709
|
+
const params = new URLSearchParams({ id });
|
|
1710
|
+
const sourceRun = textValue(job.sourceRun, '');
|
|
1711
|
+
if (sourceRun)
|
|
1712
|
+
params.set('sourceRun', sourceRun);
|
|
1713
|
+
const response = await fetch(`/api/task-details?${params}`);
|
|
1531
1714
|
const details = await response.json();
|
|
1532
1715
|
taskDetailsCache.set(id, details);
|
|
1533
1716
|
}
|
|
@@ -2066,6 +2249,16 @@ function dashboardSourceSmokeMarker(kind) {
|
|
|
2066
2249
|
return 'stopped-source-strip';
|
|
2067
2250
|
return 'snapshot-source-strip';
|
|
2068
2251
|
}
|
|
2252
|
+
function boardScopeLabel(dashboard) {
|
|
2253
|
+
const kind = dashboardSourceKind(dashboard);
|
|
2254
|
+
if (kind === 'lifetime')
|
|
2255
|
+
return 'lifetime';
|
|
2256
|
+
if (kind === 'live' || kind === 'stopped')
|
|
2257
|
+
return 'run';
|
|
2258
|
+
if (kind === 'demo')
|
|
2259
|
+
return 'demo';
|
|
2260
|
+
return 'snapshot';
|
|
2261
|
+
}
|
|
2069
2262
|
function riskStatusLabel(dashboard, attention, loaded) {
|
|
2070
2263
|
if (attention.failedCount || attention.staleCount || attention.needsCoordinatorReviewCount || attention.blockedCount)
|
|
2071
2264
|
return 'Attention needed';
|
|
@@ -2074,6 +2267,8 @@ function riskStatusLabel(dashboard, attention, loaded) {
|
|
|
2074
2267
|
return dashboard.ok ? 'Admission clean' : 'Run degraded';
|
|
2075
2268
|
}
|
|
2076
2269
|
function jobRisk(job) {
|
|
2270
|
+
if (isSuccessfulResolvedCoordinatorReviewJob(job))
|
|
2271
|
+
return 'done';
|
|
2077
2272
|
if (isBlockedJob(job))
|
|
2078
2273
|
return 'failed';
|
|
2079
2274
|
if (isNeedsCoordinatorPortJob(job))
|
|
@@ -2094,6 +2289,8 @@ function isCompletedJob(job) {
|
|
|
2094
2289
|
return normalized(job.status) === 'completed';
|
|
2095
2290
|
}
|
|
2096
2291
|
function isReadyJob(job) {
|
|
2292
|
+
if (isResolvedCoordinatorReviewJob(job))
|
|
2293
|
+
return false;
|
|
2097
2294
|
return normalized(job.bucket) === 'ready-to-apply'
|
|
2098
2295
|
|| normalized(job.disposition) === 'ready-to-apply'
|
|
2099
2296
|
|| normalized(job.mergeReadiness).includes('ready');
|
|
@@ -2103,7 +2300,7 @@ function jobRiskLabel(job) {
|
|
|
2103
2300
|
if (isBlockedJob(job))
|
|
2104
2301
|
return 'blocked';
|
|
2105
2302
|
if (risk === 'failed')
|
|
2106
|
-
return '
|
|
2303
|
+
return 'failed';
|
|
2107
2304
|
if (risk === 'stale')
|
|
2108
2305
|
return 'stale';
|
|
2109
2306
|
if (risk === 'review')
|
|
@@ -2115,17 +2312,22 @@ function jobRiskLabel(job) {
|
|
|
2115
2312
|
return text(job.status);
|
|
2116
2313
|
}
|
|
2117
2314
|
function isFailedJob(job) {
|
|
2315
|
+
if (isResolvedCoordinatorReviewJob(job))
|
|
2316
|
+
return false;
|
|
2118
2317
|
const status = normalized(job.status);
|
|
2119
2318
|
const bucket = normalized(job.bucket);
|
|
2120
2319
|
const disposition = normalized(job.disposition);
|
|
2121
2320
|
const readiness = normalized(job.mergeReadiness);
|
|
2122
2321
|
return status === 'failed'
|
|
2123
2322
|
|| bucket === 'failed-evidence'
|
|
2323
|
+
|| bucket === 'worker-failed'
|
|
2124
2324
|
|| disposition === 'rejected'
|
|
2125
2325
|
|| disposition === 'failed'
|
|
2126
2326
|
|| readiness.includes('failed');
|
|
2127
2327
|
}
|
|
2128
2328
|
function isBlockedJob(job) {
|
|
2329
|
+
if (isResolvedCoordinatorReviewJob(job))
|
|
2330
|
+
return false;
|
|
2129
2331
|
const status = normalized(job.status);
|
|
2130
2332
|
const bucket = normalized(job.bucket);
|
|
2131
2333
|
const disposition = normalized(job.disposition);
|
|
@@ -2141,9 +2343,13 @@ function isNeedsCoordinatorReviewJob(job) {
|
|
|
2141
2343
|
return isNeedsCoordinatorPortJob(job);
|
|
2142
2344
|
}
|
|
2143
2345
|
function isNeedsCoordinatorPortJob(job) {
|
|
2346
|
+
if (isResolvedCoordinatorReviewJob(job))
|
|
2347
|
+
return false;
|
|
2144
2348
|
const bucket = normalized(job.bucket);
|
|
2145
2349
|
const disposition = normalized(job.disposition);
|
|
2146
2350
|
const readiness = normalized(job.mergeReadiness);
|
|
2351
|
+
if (isCompletedJob(job) && ['completed', 'done', 'landed', 'applied'].includes(bucket))
|
|
2352
|
+
return false;
|
|
2147
2353
|
return bucket === 'needs-human-port'
|
|
2148
2354
|
|| bucket === 'needs-human-review'
|
|
2149
2355
|
|| bucket === 'needs-human-decision'
|
|
@@ -2154,16 +2360,29 @@ function isNeedsCoordinatorPortJob(job) {
|
|
|
2154
2360
|
|| readiness.includes('needs')
|
|
2155
2361
|
|| normalized(job.status) === 'needs-review';
|
|
2156
2362
|
}
|
|
2363
|
+
function isOpenCoordinatorReviewSignal(value) {
|
|
2364
|
+
const signal = normalized(value);
|
|
2365
|
+
return signal === 'needs-human-port'
|
|
2366
|
+
|| signal === 'needs-human-review'
|
|
2367
|
+
|| signal === 'needs-human-decision'
|
|
2368
|
+
|| signal === 'needs-coordinator-port'
|
|
2369
|
+
|| signal === 'needs-coordinator-review'
|
|
2370
|
+
|| signal === 'needs-coordinator-decision'
|
|
2371
|
+
|| signal === 'needs-port'
|
|
2372
|
+
|| signal === 'needs-review'
|
|
2373
|
+
|| signal === 'manual-port-required'
|
|
2374
|
+
|| signal === 'manual port required';
|
|
2375
|
+
}
|
|
2157
2376
|
function isCoordinatorReviewJob(job) {
|
|
2158
2377
|
if (isResolvedCoordinatorReviewJob(job))
|
|
2159
2378
|
return false;
|
|
2160
2379
|
return isNeedsCoordinatorPortJob(job)
|
|
2161
|
-
|| isFailedJob(job)
|
|
2162
|
-
|| isStaleJob(job)
|
|
2163
2380
|
|| normalized(job.disposition).includes('review')
|
|
2164
2381
|
|| normalized(job.mergeReadiness).includes('review');
|
|
2165
2382
|
}
|
|
2166
2383
|
function isStaleJob(job) {
|
|
2384
|
+
if (isResolvedCoordinatorReviewJob(job))
|
|
2385
|
+
return false;
|
|
2167
2386
|
return normalized(job.bucket).includes('stale')
|
|
2168
2387
|
|| normalized(job.disposition).includes('stale')
|
|
2169
2388
|
|| normalized(job.mergeReadiness).includes('stale');
|
|
@@ -2211,6 +2430,8 @@ function pathSummaryText(job) {
|
|
|
2211
2430
|
const ignoredOwnership = ignoredOwnershipViolations(job);
|
|
2212
2431
|
const ignoredChangedPathCount = numberValue(job.ignoredChangedPathCount);
|
|
2213
2432
|
const quarantinedChangedPathCount = numberValue(job.quarantinedChangedPathCount);
|
|
2433
|
+
const changedPathCount = numberValue(job.changedPathCount);
|
|
2434
|
+
const evidencePathCount = numberValue(job.evidencePathCount);
|
|
2214
2435
|
const ignoredCount = ignoredOwnership.length + ignoredChangedPathCount;
|
|
2215
2436
|
if (sourceOwnership.length)
|
|
2216
2437
|
return `${text(sourceOwnership.length)} source issue`;
|
|
@@ -2218,7 +2439,27 @@ function pathSummaryText(job) {
|
|
|
2218
2439
|
return `${text(quarantinedChangedPathCount)} quarantined`;
|
|
2219
2440
|
if (ignoredCount)
|
|
2220
2441
|
return `${text(ignoredCount)} generated noise`;
|
|
2221
|
-
|
|
2442
|
+
if (!changedPathCount && hasPatchArtifact(job))
|
|
2443
|
+
return 'patch not indexed yet';
|
|
2444
|
+
if (!changedPathCount && evidencePathCount)
|
|
2445
|
+
return 'evidence only';
|
|
2446
|
+
if (!changedPathCount && isActiveAgentJob(job))
|
|
2447
|
+
return 'files pending';
|
|
2448
|
+
return `${text(changedPathCount)} changed ${changedPathCount === 1 ? 'path' : 'paths'}`;
|
|
2449
|
+
}
|
|
2450
|
+
function hasPatchArtifact(job) {
|
|
2451
|
+
const paths = [
|
|
2452
|
+
textValue(job.patchPath, ''),
|
|
2453
|
+
...stringArray(job.artifactPaths),
|
|
2454
|
+
...stringArray(job.evidencePaths)
|
|
2455
|
+
];
|
|
2456
|
+
return paths.some((entry) => /\.patch(?:$|[?#])/.test(entry));
|
|
2457
|
+
}
|
|
2458
|
+
function evidenceSummaryText(job) {
|
|
2459
|
+
const count = numberValue(job.evidencePathCount);
|
|
2460
|
+
if (!count)
|
|
2461
|
+
return 'no evidence yet';
|
|
2462
|
+
return `${formatNumber(count)} evidence ${count === 1 ? 'artifact' : 'artifacts'}`;
|
|
2222
2463
|
}
|
|
2223
2464
|
function sourceOwnershipViolations(job) {
|
|
2224
2465
|
const explicit = stringArray(job.sourceOwnershipViolations);
|
|
@@ -2512,6 +2753,8 @@ function tabMeta(tab, input) {
|
|
|
2512
2753
|
return 'AI tasks';
|
|
2513
2754
|
if (tab === 'swarm')
|
|
2514
2755
|
return 'active agents';
|
|
2756
|
+
if (tab === 'lanes')
|
|
2757
|
+
return `${text(input.lanes)} lanes`;
|
|
2515
2758
|
if (tab === 'performance')
|
|
2516
2759
|
return 'cost trends';
|
|
2517
2760
|
if (tab === 'history')
|
|
@@ -3399,16 +3642,17 @@ const MODEL_PRICES_PER_MILLION = [
|
|
|
3399
3642
|
];
|
|
3400
3643
|
function modelCostSummary(jobs) {
|
|
3401
3644
|
const allCosts = jobs.map(modelCostForJob);
|
|
3402
|
-
const costs = allCosts.filter((entry) => entry.
|
|
3645
|
+
const costs = allCosts.filter((entry) => entry.costKnown);
|
|
3403
3646
|
const unpricedCount = allCosts.length - costs.length;
|
|
3404
3647
|
const total = costs.reduce((sum, entry) => sum + entry.cost, 0);
|
|
3405
3648
|
if (!costs.length)
|
|
3406
3649
|
return { value: '-', detail: 'no priced model labels in this run' };
|
|
3407
|
-
const billableTokenJobs = costs.filter((entry) => entry.uncachedInputTokens > 0 || entry.cachedInputTokens > 0 || entry.outputTokens > 0).length;
|
|
3650
|
+
const billableTokenJobs = costs.filter((entry) => entry.cost > 0 || entry.uncachedInputTokens > 0 || entry.cachedInputTokens > 0 || entry.outputTokens > 0).length;
|
|
3408
3651
|
const models = uniqueStrings(costs.map((entry) => entry.model));
|
|
3409
3652
|
const outputJobs = costs.filter((entry) => entry.outputTokens > 0).length;
|
|
3410
3653
|
const estimated = costs.filter((entry) => entry.estimatedInput).length;
|
|
3411
3654
|
const longContextJobs = costs.filter((entry) => entry.longContext).length;
|
|
3655
|
+
const backendEstimated = costs.filter((entry) => entry.backendEstimate).length;
|
|
3412
3656
|
const pricingSources = uniqueStrings(costs.map((entry) => entry.pricingSource).filter((value) => Boolean(value)));
|
|
3413
3657
|
const modelText = models.length > 3 ? `${models.slice(0, 3).join(', ')} +${models.length - 3}` : models.join(', ');
|
|
3414
3658
|
const sourceText = pricingSources.length ? ` · ${pricingSources.join(', ')}` : '';
|
|
@@ -3419,20 +3663,21 @@ function modelCostSummary(jobs) {
|
|
|
3419
3663
|
const scope = outputJobs === 0 ? 'input-only' : outputJobs === costs.length ? 'input+output' : 'input+partial output';
|
|
3420
3664
|
const estimatedText = estimated ? ` · ${formatNumber(estimated)} estimated-input jobs` : '';
|
|
3421
3665
|
const longContextText = longContextJobs ? ` · ${formatNumber(longContextJobs)} long-context priced` : '';
|
|
3666
|
+
const backendText = backendEstimated ? ` · ${formatNumber(backendEstimated)} backend-priced jobs` : '';
|
|
3422
3667
|
const missingOutputText = outputJobs > 0 && outputJobs < costs.length ? ` · ${formatNumber(costs.length - outputJobs)} without output tokens` : '';
|
|
3423
3668
|
const noOutputText = outputJobs === 0 ? ' · no output tokens reported' : '';
|
|
3424
|
-
return { value: formatUsd(total), detail: `${scope} estimate · ${modelText}${sourceText}${longContextText}${estimatedText}${missingOutputText}${noOutputText}${unpricedText}` };
|
|
3669
|
+
return { value: formatUsd(total), detail: `${scope} estimate · ${modelText}${sourceText}${longContextText}${backendText}${estimatedText}${missingOutputText}${noOutputText}${unpricedText}` };
|
|
3425
3670
|
}
|
|
3426
3671
|
function modelCostRows(jobs) {
|
|
3427
3672
|
return jobs
|
|
3428
3673
|
.map((job) => ({ job, cost: modelCostForJob(job) }))
|
|
3429
|
-
.filter((entry) => entry.cost.
|
|
3674
|
+
.filter((entry) => entry.cost.costKnown && entry.cost.cost > 0)
|
|
3430
3675
|
.sort((left, right) => right.cost.cost - left.cost.cost)
|
|
3431
3676
|
.slice(0, 5)
|
|
3432
3677
|
.map(({ job, cost }) => ({
|
|
3433
3678
|
label: `${ticketId(job)} · ${taskTitle(job)}`,
|
|
3434
3679
|
value: formatUsd(cost.cost),
|
|
3435
|
-
detail: `${cost.model} estimate · ${formatNumber(cost.uncachedInputTokens)} uncached · ${formatNumber(cost.cachedInputTokens)} cached${cost.outputTokens ? ` · ${formatNumber(cost.outputTokens)} output` : ''}${cost.longContext ? ' · long context' : ''}${cost.estimatedInput ? ' · estimated input' : ''}`
|
|
3680
|
+
detail: `${cost.model} estimate · ${formatNumber(cost.uncachedInputTokens)} uncached · ${formatNumber(cost.cachedInputTokens)} cached${cost.outputTokens ? ` · ${formatNumber(cost.outputTokens)} output` : ''}${cost.longContext ? ' · long context' : ''}${cost.backendEstimate ? ' · backend priced' : ''}${cost.estimatedInput ? ' · estimated input' : ''}`
|
|
3436
3681
|
}));
|
|
3437
3682
|
}
|
|
3438
3683
|
function modelCostForJob(job) {
|
|
@@ -3440,12 +3685,32 @@ function modelCostForJob(job) {
|
|
|
3440
3685
|
const priceEntry = modelPriceEntryForModel(model);
|
|
3441
3686
|
const input = inputTokenBreakdownForJob(job);
|
|
3442
3687
|
const outputTokens = jobOutputTokens(job);
|
|
3688
|
+
const backendCost = backendCostEstimateForJob(job);
|
|
3443
3689
|
const longContext = Boolean(priceEntry?.longContextPrice && priceEntry.longContextThreshold && input.inputTokens > priceEntry.longContextThreshold);
|
|
3444
3690
|
const price = priceEntry ? (longContext ? priceEntry.longContextPrice ?? priceEntry.price : priceEntry.price) : undefined;
|
|
3691
|
+
if (backendCost > 0) {
|
|
3692
|
+
return {
|
|
3693
|
+
model: textValue(job.pricingModel ?? job.pricingMatchedModel, model),
|
|
3694
|
+
price,
|
|
3695
|
+
pricingSource: textValue(job.pricingSource, 'backend cost estimate'),
|
|
3696
|
+
...input,
|
|
3697
|
+
outputTokens,
|
|
3698
|
+
longContext,
|
|
3699
|
+
backendEstimate: true,
|
|
3700
|
+
costKnown: true,
|
|
3701
|
+
cost: backendCost
|
|
3702
|
+
};
|
|
3703
|
+
}
|
|
3445
3704
|
if (!price)
|
|
3446
|
-
return { model, ...input, outputTokens, longContext: false, cost: 0 };
|
|
3705
|
+
return { model, ...input, outputTokens, longContext: false, backendEstimate: false, costKnown: false, cost: 0 };
|
|
3447
3706
|
const cost = ((input.uncachedInputTokens * price.input) + (input.cachedInputTokens * price.cachedInput) + (outputTokens * price.output)) / 1_000_000;
|
|
3448
|
-
return { model, price, pricingSource: priceEntry?.source, ...input, outputTokens, longContext, cost };
|
|
3707
|
+
return { model, price, pricingSource: priceEntry?.source, ...input, outputTokens, longContext, backendEstimate: false, costKnown: true, cost };
|
|
3708
|
+
}
|
|
3709
|
+
function backendCostEstimateForJob(job) {
|
|
3710
|
+
const microUsd = firstPositiveNumber(job.estimatedCostMicroUsd, job.estimated_cost_micro_usd);
|
|
3711
|
+
if (microUsd > 0)
|
|
3712
|
+
return microUsd / 1_000_000;
|
|
3713
|
+
return firstPositiveNumber(job.estimatedCostUsd, job.estimated_cost_usd);
|
|
3449
3714
|
}
|
|
3450
3715
|
function modelPriceEntryForModel(model) {
|
|
3451
3716
|
const key = normalizedModelKey(model);
|
|
@@ -3473,7 +3738,7 @@ function inputTokenBreakdownForJob(job) {
|
|
|
3473
3738
|
const inputDetails = recordValue(job.inputTokensDetails ?? job.input_tokens_details ?? usage.input_tokens_details);
|
|
3474
3739
|
const promptDetails = recordValue(job.promptTokensDetails ?? job.prompt_tokens_details ?? usage.prompt_tokens_details);
|
|
3475
3740
|
const actualInput = firstPositiveNumber(job.actualInputTokens, job.inputTokens, job.promptTokens, job.actual_input_tokens, job.input_tokens, job.prompt_tokens, usage.input_tokens, usage.prompt_tokens);
|
|
3476
|
-
const estimatedInput = firstPositiveNumber(job.estimatedInputTokens, job.estimated_input_tokens);
|
|
3741
|
+
const estimatedInput = firstPositiveNumber(job.estimatedInputTokens, job.estimated_input_tokens, usage.estimated_input_tokens, usage.estimatedInputTokens);
|
|
3477
3742
|
const reportedCached = firstPositiveNumber(job.cachedInputTokens, job.cachedPromptTokens, job.cached_input_tokens, job.cached_prompt_tokens, inputDetails.cached_tokens, promptDetails.cached_tokens, usage.cached_input_tokens, usage.cached_prompt_tokens, usage.cached_tokens);
|
|
3478
3743
|
const reportedUncached = firstPositiveNumber(job.uncachedInputTokens, job.uncached_input_tokens, usage.uncached_input_tokens);
|
|
3479
3744
|
const inputTokens = actualInput || estimatedInput || reportedCached + reportedUncached;
|