@shapeshift-labs/frontier-loom-ui 0.1.0 → 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 +319 -40
- package/dist/client.js.map +1 -1
- package/dist/public/styles.css +272 -12
- package/dist/server.d.ts +5 -2
- package/dist/server.js +2113 -93
- 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
|
}
|
|
@@ -320,15 +342,17 @@ function contentTab(id, label, meta) {
|
|
|
320
342
|
function WorkOverview({ dashboard, lanes, jobs, attention, audit, success }) {
|
|
321
343
|
const health = dashboardHealthSummary(dashboard, jobs, attention);
|
|
322
344
|
const contribution = contributionGrid(dashboard, jobs, dashboard.events);
|
|
323
|
-
const
|
|
324
|
-
const
|
|
345
|
+
const resolvedWorkCount = resolvedWorkJobCount(jobs);
|
|
346
|
+
const workerSuccessCount = successLikeJobCount(jobs);
|
|
347
|
+
const progressRatio = health.jobCount ? resolvedWorkCount / health.jobCount : 0;
|
|
348
|
+
const workerReliabilityRatio = health.jobCount ? workerSuccessCount / health.jobCount : 0;
|
|
325
349
|
const progressLabel = formatPercent(progressRatio);
|
|
326
350
|
const progressWidth = Math.max(0, Math.min(100, progressRatio * 100));
|
|
327
351
|
const cost = workCostSummary(dashboard, jobs);
|
|
328
352
|
void lanes;
|
|
329
353
|
void audit;
|
|
330
354
|
void success;
|
|
331
|
-
return _jsxs("div", { className: "work-layout", "data-scroll-id": "work", children: [_jsxs("section", { className: "goal-card", children: [_jsxs("div", { children: [_jsx("span", { children: "Goal" }), _jsx("h3", { children: currentGoalTitle(dashboard) }), _jsx("p", { children: goalProgressText(health,
|
|
355
|
+
return _jsxs("div", { className: "work-layout", "data-scroll-id": "work", children: [_jsxs("section", { className: "goal-card", children: [_jsxs("div", { children: [_jsx("span", { children: "Goal" }), _jsx("h3", { children: currentGoalTitle(dashboard) }), _jsx("p", { children: goalProgressText(jobs, health, resolvedWorkCount) })] }), _jsxs("div", { className: "goal-progress", children: [_jsxs("div", { className: "goal-progress-head", children: [_jsx("b", { children: progressLabel }), _jsxs("span", { children: [text(resolvedWorkCount), " of ", text(health.jobCount), " tasks resolved"] })] }), _jsx("div", { className: "goal-progress-track", role: "img", "aria-label": `${progressLabel} resolved work progress`, children: _jsx("span", { className: `goal-progress-fill ${progressTone(progressRatio, workerReliabilityRatio)}`, style: `width:${progressWidth}%` }) }), _jsxs("small", { className: "goal-reliability", children: ["Worker reliability ", formatPercent(workerReliabilityRatio), " \u00B7 ", text(workerSuccessCount), " clean completions"] })] })] }), _jsxs("section", { className: "work-section work-contribution-section", children: [_jsxs("div", { className: "metric-section-head", children: [_jsx("h3", { children: "Progress by day" }), _jsxs("span", { children: [text(contribution.year), " \u00B7 January through December"] })] }), _jsx(ContributionGraph, { grid: contribution, prominent: true })] }), _jsxs("section", { className: "work-section", children: [_jsxs("div", { className: "metric-section-head", children: [_jsx("h3", { children: "Token cost estimate" }), _jsx("span", { children: cost.detail })] }), _jsxs("div", { className: "work-cost-metrics", children: [costMetric('Total input', cost.totalInput, cost.totalDetail), costMetric('Uncached input', cost.freshInput, cost.freshDetail), costMetric('Cached input', cost.cachedInput, cost.cachedDetail), costMetric('Budget warnings', cost.budgetWarnings, cost.budgetDetail)] })] })] });
|
|
332
356
|
}
|
|
333
357
|
function currentGoalTitle(dashboard) {
|
|
334
358
|
const backlog = recordValue(dashboard.backlog);
|
|
@@ -349,19 +373,28 @@ function sentenceCaseIdentifier(value) {
|
|
|
349
373
|
return value;
|
|
350
374
|
return spaced[0].toUpperCase() + spaced.slice(1);
|
|
351
375
|
}
|
|
352
|
-
function goalProgressText(health,
|
|
376
|
+
function goalProgressText(jobs, health, resolvedWorkCount) {
|
|
353
377
|
if (!health.jobCount)
|
|
354
378
|
return 'No active swarm work is loaded yet.';
|
|
355
|
-
const
|
|
356
|
-
if (
|
|
357
|
-
return 'Every tracked task has completed
|
|
379
|
+
const unresolvedAttentionCount = jobs.filter(isCoordinatorReviewJob).length;
|
|
380
|
+
if (resolvedWorkCount >= health.jobCount)
|
|
381
|
+
return 'Every tracked task has either completed or been resolved by the coordinator.';
|
|
358
382
|
if (health.runningJobCount) {
|
|
359
383
|
return `The swarm is working through ${text(health.jobCount)} tracked tasks. ${text(health.runningJobCount)} are still running.`;
|
|
360
384
|
}
|
|
361
385
|
if (health.terminalJobCount >= health.jobCount) {
|
|
362
|
-
return
|
|
386
|
+
return unresolvedAttentionCount
|
|
387
|
+
? `Execution has finished, but ${text(unresolvedAttentionCount)} tasks still need coordinator action.`
|
|
388
|
+
: 'Execution has finished and coordinator decisions have collapsed the open review queue.';
|
|
363
389
|
}
|
|
364
|
-
return `No agents are running right now. ${text(Math.max(0, health.jobCount - health.terminalJobCount))} tasks remain queued and ${text(
|
|
390
|
+
return `No agents are running right now. ${text(Math.max(0, health.jobCount - health.terminalJobCount))} tasks remain queued and ${text(unresolvedAttentionCount)} need coordinator action.`;
|
|
391
|
+
}
|
|
392
|
+
function progressTone(progressRatio, workerReliabilityRatio) {
|
|
393
|
+
if (progressRatio >= 0.8 && workerReliabilityRatio >= 0.5)
|
|
394
|
+
return 'good';
|
|
395
|
+
if (progressRatio >= 0.5)
|
|
396
|
+
return 'warn';
|
|
397
|
+
return 'bad';
|
|
365
398
|
}
|
|
366
399
|
function workCostSummary(dashboard, jobs) {
|
|
367
400
|
const telemetry = tokenTimeSummary(dashboard, jobs);
|
|
@@ -383,33 +416,121 @@ function costMetric(label, value, detail) {
|
|
|
383
416
|
}
|
|
384
417
|
function TaskBoard({ dashboard, jobs }) {
|
|
385
418
|
const items = taskBoardItems(dashboard, jobs);
|
|
386
|
-
const
|
|
419
|
+
const sourceKind = dashboardSourceKind(dashboard);
|
|
420
|
+
const scope = boardScopeLabel(dashboard);
|
|
421
|
+
const columns = taskBoardColumns(items, sourceKind);
|
|
387
422
|
const activeCount = columns.reduce((sum, column) => column.id === 'done' ? sum : sum + column.items.length, 0);
|
|
388
|
-
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
|
|
389
424
|
? column.items.map((job) => _jsx(TaskBoardCard, { job: job }))
|
|
390
425
|
: _jsx("p", { className: "empty tight", children: column.empty }) })] })) }) });
|
|
391
426
|
}
|
|
392
427
|
function AgentWork({ dashboard, jobs }) {
|
|
393
428
|
const items = taskBoardItems(dashboard, jobs).filter(isActiveAgentJob);
|
|
394
429
|
const workers = agentWorkers(items);
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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')) })] })] }) });
|
|
399
519
|
}
|
|
400
520
|
function AgentWorkerCard({ worker, index, now }) {
|
|
401
521
|
const model = agentModelSummary(worker);
|
|
402
522
|
const files = agentTouchedFiles(worker);
|
|
403
523
|
const evidenceCount = agentEvidenceCount(worker);
|
|
524
|
+
const fileFallback = evidenceCount ? 'Evidence only' : worker.status === 'active' ? 'Files pending' : 'No source files';
|
|
404
525
|
const visibleJobs = worker.currentJobs.slice(0, 2);
|
|
405
526
|
const hiddenJobCount = Math.max(0, worker.currentJobs.length - visibleJobs.length);
|
|
406
|
-
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] })] });
|
|
407
528
|
}
|
|
408
529
|
function TaskBoardCard({ job }) {
|
|
409
530
|
const id = taskCardId(job);
|
|
410
531
|
const ticket = ticketId(job);
|
|
411
532
|
const risk = jobRisk(job);
|
|
412
|
-
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) })] })] });
|
|
413
534
|
}
|
|
414
535
|
function TaskDetailDialog({ job, dashboard }) {
|
|
415
536
|
const title = taskTitle(job);
|
|
@@ -426,7 +547,10 @@ function TaskFileDiffs({ job, details }) {
|
|
|
426
547
|
if (!details && job.boardKind !== 'backlog')
|
|
427
548
|
return _jsxs("section", { className: "task-dialog-section", children: [_jsx("h4", { children: "Files changed" }), _jsx("p", { className: "empty tight", children: "Loading file diffs..." })] });
|
|
428
549
|
if (details?.files?.length)
|
|
429
|
-
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
|
+
}) })] });
|
|
430
554
|
if (changedPaths.length)
|
|
431
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 })] });
|
|
432
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.' })] });
|
|
@@ -530,7 +654,7 @@ function taskBoardItems(dashboard, jobs) {
|
|
|
530
654
|
];
|
|
531
655
|
}
|
|
532
656
|
function isTaskBoardVisibleJob(job) {
|
|
533
|
-
return !isResolvedCoordinatorReviewJob(job);
|
|
657
|
+
return !isResolvedCoordinatorReviewJob(job) || isSuccessfulResolvedCoordinatorReviewJob(job);
|
|
534
658
|
}
|
|
535
659
|
function isResolvedCoordinatorReviewJob(job) {
|
|
536
660
|
const bucket = normalized(job.bucket);
|
|
@@ -543,6 +667,16 @@ function isResolvedCoordinatorDecisionStatus(status) {
|
|
|
543
667
|
const value = normalized(status);
|
|
544
668
|
return Boolean(value) && !['open', 'pending', 'deferred', 'needs-review'].includes(value);
|
|
545
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
|
+
}
|
|
546
680
|
function activeAgentTaskCount(dashboard, jobs) {
|
|
547
681
|
return taskBoardItems(dashboard, jobs).filter(isActiveAgentJob).length;
|
|
548
682
|
}
|
|
@@ -558,9 +692,12 @@ function backlogBoardItems(dashboard) {
|
|
|
558
692
|
return entries.map((entry, index) => {
|
|
559
693
|
const id = textValue(entry.id ?? entry.taskId ?? entry.title, `backlog-${index + 1}`);
|
|
560
694
|
const status = normalized(entry.status ?? entry.state);
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
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';
|
|
564
701
|
return {
|
|
565
702
|
...entry,
|
|
566
703
|
boardKind: 'backlog',
|
|
@@ -575,14 +712,15 @@ function backlogBoardItems(dashboard) {
|
|
|
575
712
|
};
|
|
576
713
|
});
|
|
577
714
|
}
|
|
578
|
-
function taskBoardColumns(items) {
|
|
715
|
+
function taskBoardColumns(items, sourceKind = 'snapshot') {
|
|
716
|
+
const doneDetail = sourceKind === 'lifetime' ? 'lifetime completed output' : 'completed output';
|
|
579
717
|
const columns = [
|
|
580
718
|
{ id: 'backlog', title: 'Backlog', detail: 'not scheduled yet', empty: 'No backlog items.', items: [] },
|
|
581
719
|
{ id: 'todo', title: 'To do', detail: 'ready to start', empty: 'No queued tasks.', items: [] },
|
|
582
720
|
{ id: 'active', title: 'Active', detail: 'running work', empty: 'No active tasks.', items: [] },
|
|
583
721
|
{ id: 'review', title: 'Coordinator review', detail: 'needs decision', empty: 'No tasks need coordinator review.', items: [] },
|
|
584
722
|
{ id: 'ready', title: 'Ready', detail: 'ready to apply', empty: 'No ready patches.', items: [] },
|
|
585
|
-
{ id: 'done', title: 'Done', detail:
|
|
723
|
+
{ id: 'done', title: 'Done', detail: doneDetail, empty: 'No completed tasks.', items: [] },
|
|
586
724
|
{ id: 'blocked', title: 'Blocked', detail: 'waiting or impossible', empty: 'No genuinely blocked tasks.', items: [] }
|
|
587
725
|
];
|
|
588
726
|
const byId = new Map(columns.map((column) => [column.id, column]));
|
|
@@ -686,6 +824,13 @@ function agentDisplayName(worker, index) {
|
|
|
686
824
|
return worker.name;
|
|
687
825
|
return `Agent ${String(index + 1).padStart(2, '0')}`;
|
|
688
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
|
+
}
|
|
689
834
|
function agentColor(key) {
|
|
690
835
|
return AGENT_COLORS[stableNumber(key) % AGENT_COLORS.length] ?? AGENT_COLORS[0];
|
|
691
836
|
}
|
|
@@ -712,6 +857,8 @@ function agentStatus(job) {
|
|
|
712
857
|
const disposition = normalized(job.disposition ?? job.mergeReadiness);
|
|
713
858
|
if (['running', 'active', 'working', 'in-progress', 'in progress'].includes(status))
|
|
714
859
|
return 'active';
|
|
860
|
+
if (isSuccessfulResolvedCoordinatorReviewJob(job))
|
|
861
|
+
return 'done';
|
|
715
862
|
if (status === 'idle' || bucket === 'idle')
|
|
716
863
|
return 'idle';
|
|
717
864
|
if (isBlockedJob(job))
|
|
@@ -856,6 +1003,8 @@ function taskBoardColumnId(job) {
|
|
|
856
1003
|
return job.boardColumn;
|
|
857
1004
|
if (job.boardKind === 'backlog')
|
|
858
1005
|
return 'backlog';
|
|
1006
|
+
if (isSuccessfulResolvedCoordinatorReviewJob(job))
|
|
1007
|
+
return 'done';
|
|
859
1008
|
if (isBlockedJob(job))
|
|
860
1009
|
return 'blocked';
|
|
861
1010
|
if (isCoordinatorReviewJob(job))
|
|
@@ -900,6 +1049,8 @@ function taskSortRank(job) {
|
|
|
900
1049
|
return 3;
|
|
901
1050
|
if (isReadyJob(job))
|
|
902
1051
|
return 4;
|
|
1052
|
+
if (isSuccessfulResolvedCoordinatorReviewJob(job))
|
|
1053
|
+
return 5;
|
|
903
1054
|
return 5;
|
|
904
1055
|
}
|
|
905
1056
|
function taskCardId(job) {
|
|
@@ -990,6 +1141,8 @@ function coordinatorDecisionLabel(status) {
|
|
|
990
1141
|
function coordinatorFacingSignalLabel(value) {
|
|
991
1142
|
const raw = textValue(value, 'tracked task');
|
|
992
1143
|
const normalizedValue = normalized(raw);
|
|
1144
|
+
if (normalizedValue === 'review-resolved' || normalizedValue === 'resolved-review')
|
|
1145
|
+
return 'review resolved';
|
|
993
1146
|
if (normalizedValue === 'needs-human-port' || normalizedValue === 'needs-coordinator-port')
|
|
994
1147
|
return 'needs coordinator review';
|
|
995
1148
|
if (normalizedValue === 'needs-human-review' || normalizedValue === 'needs-coordinator-review')
|
|
@@ -998,6 +1151,8 @@ function coordinatorFacingSignalLabel(value) {
|
|
|
998
1151
|
return 'needs coordinator decision';
|
|
999
1152
|
if (normalizedValue === 'failed-evidence')
|
|
1000
1153
|
return 'failed evidence';
|
|
1154
|
+
if (normalizedValue === 'worker-failed')
|
|
1155
|
+
return 'failed worker';
|
|
1001
1156
|
if (normalizedValue === 'ready-to-apply')
|
|
1002
1157
|
return 'ready to apply';
|
|
1003
1158
|
if (normalizedValue === 'patch-candidate')
|
|
@@ -1017,12 +1172,16 @@ function taskTokenSummary(job) {
|
|
|
1017
1172
|
return '-';
|
|
1018
1173
|
}
|
|
1019
1174
|
function taskReasonItems(job) {
|
|
1020
|
-
|
|
1175
|
+
const reasons = uniqueStrings([
|
|
1021
1176
|
...stringArray(job.collectReasonClasses),
|
|
1022
1177
|
...stringArray(job.reasons),
|
|
1023
1178
|
...stringArray(job.contextBudgetWarnings),
|
|
1024
1179
|
...stringArray(job.semanticReadinessReasons)
|
|
1025
|
-
])
|
|
1180
|
+
]);
|
|
1181
|
+
const terminalOrResolved = isResolvedCoordinatorReviewJob(job) || (isCompletedJob(job) && !isNeedsCoordinatorPortJob(job));
|
|
1182
|
+
return reasons
|
|
1183
|
+
.filter((reason) => !terminalOrResolved || !isOpenCoordinatorReviewSignal(reason))
|
|
1184
|
+
.slice(0, 8);
|
|
1026
1185
|
}
|
|
1027
1186
|
function taskFact(label, value) {
|
|
1028
1187
|
return _jsxs("div", { className: "task-fact", children: [_jsx("span", { children: label }), _jsx("b", { children: value || '-' })] });
|
|
@@ -1443,6 +1602,9 @@ function priorityRank(priority) {
|
|
|
1443
1602
|
function successLikeJobCount(jobs) {
|
|
1444
1603
|
return jobs.filter((job) => isCompletedJob(job) && !isFailedJob(job) && !isStaleJob(job)).length;
|
|
1445
1604
|
}
|
|
1605
|
+
function resolvedWorkJobCount(jobs) {
|
|
1606
|
+
return jobs.filter((job) => isResolvedCoordinatorReviewJob(job) || (isCompletedJob(job) && !isFailedJob(job) && !isStaleJob(job))).length;
|
|
1607
|
+
}
|
|
1446
1608
|
function laneLoadTone(lane) {
|
|
1447
1609
|
if (lane.failedCount || lane.blockedCount)
|
|
1448
1610
|
return 'bad';
|
|
@@ -1503,6 +1665,37 @@ function openTaskCard(id) {
|
|
|
1503
1665
|
function taskDialogScrollId(id) {
|
|
1504
1666
|
return `task-dialog-${id}`;
|
|
1505
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
|
+
}
|
|
1506
1699
|
async function fetchTaskDetails(job) {
|
|
1507
1700
|
const id = taskCardId(job);
|
|
1508
1701
|
if (!id || taskDetailsCache.has(id) || taskDetailsPending.has(id))
|
|
@@ -1513,7 +1706,11 @@ async function fetchTaskDetails(job) {
|
|
|
1513
1706
|
}
|
|
1514
1707
|
taskDetailsPending.add(id);
|
|
1515
1708
|
try {
|
|
1516
|
-
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}`);
|
|
1517
1714
|
const details = await response.json();
|
|
1518
1715
|
taskDetailsCache.set(id, details);
|
|
1519
1716
|
}
|
|
@@ -2052,6 +2249,16 @@ function dashboardSourceSmokeMarker(kind) {
|
|
|
2052
2249
|
return 'stopped-source-strip';
|
|
2053
2250
|
return 'snapshot-source-strip';
|
|
2054
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
|
+
}
|
|
2055
2262
|
function riskStatusLabel(dashboard, attention, loaded) {
|
|
2056
2263
|
if (attention.failedCount || attention.staleCount || attention.needsCoordinatorReviewCount || attention.blockedCount)
|
|
2057
2264
|
return 'Attention needed';
|
|
@@ -2060,6 +2267,8 @@ function riskStatusLabel(dashboard, attention, loaded) {
|
|
|
2060
2267
|
return dashboard.ok ? 'Admission clean' : 'Run degraded';
|
|
2061
2268
|
}
|
|
2062
2269
|
function jobRisk(job) {
|
|
2270
|
+
if (isSuccessfulResolvedCoordinatorReviewJob(job))
|
|
2271
|
+
return 'done';
|
|
2063
2272
|
if (isBlockedJob(job))
|
|
2064
2273
|
return 'failed';
|
|
2065
2274
|
if (isNeedsCoordinatorPortJob(job))
|
|
@@ -2080,6 +2289,8 @@ function isCompletedJob(job) {
|
|
|
2080
2289
|
return normalized(job.status) === 'completed';
|
|
2081
2290
|
}
|
|
2082
2291
|
function isReadyJob(job) {
|
|
2292
|
+
if (isResolvedCoordinatorReviewJob(job))
|
|
2293
|
+
return false;
|
|
2083
2294
|
return normalized(job.bucket) === 'ready-to-apply'
|
|
2084
2295
|
|| normalized(job.disposition) === 'ready-to-apply'
|
|
2085
2296
|
|| normalized(job.mergeReadiness).includes('ready');
|
|
@@ -2089,7 +2300,7 @@ function jobRiskLabel(job) {
|
|
|
2089
2300
|
if (isBlockedJob(job))
|
|
2090
2301
|
return 'blocked';
|
|
2091
2302
|
if (risk === 'failed')
|
|
2092
|
-
return '
|
|
2303
|
+
return 'failed';
|
|
2093
2304
|
if (risk === 'stale')
|
|
2094
2305
|
return 'stale';
|
|
2095
2306
|
if (risk === 'review')
|
|
@@ -2101,17 +2312,22 @@ function jobRiskLabel(job) {
|
|
|
2101
2312
|
return text(job.status);
|
|
2102
2313
|
}
|
|
2103
2314
|
function isFailedJob(job) {
|
|
2315
|
+
if (isResolvedCoordinatorReviewJob(job))
|
|
2316
|
+
return false;
|
|
2104
2317
|
const status = normalized(job.status);
|
|
2105
2318
|
const bucket = normalized(job.bucket);
|
|
2106
2319
|
const disposition = normalized(job.disposition);
|
|
2107
2320
|
const readiness = normalized(job.mergeReadiness);
|
|
2108
2321
|
return status === 'failed'
|
|
2109
2322
|
|| bucket === 'failed-evidence'
|
|
2323
|
+
|| bucket === 'worker-failed'
|
|
2110
2324
|
|| disposition === 'rejected'
|
|
2111
2325
|
|| disposition === 'failed'
|
|
2112
2326
|
|| readiness.includes('failed');
|
|
2113
2327
|
}
|
|
2114
2328
|
function isBlockedJob(job) {
|
|
2329
|
+
if (isResolvedCoordinatorReviewJob(job))
|
|
2330
|
+
return false;
|
|
2115
2331
|
const status = normalized(job.status);
|
|
2116
2332
|
const bucket = normalized(job.bucket);
|
|
2117
2333
|
const disposition = normalized(job.disposition);
|
|
@@ -2127,9 +2343,13 @@ function isNeedsCoordinatorReviewJob(job) {
|
|
|
2127
2343
|
return isNeedsCoordinatorPortJob(job);
|
|
2128
2344
|
}
|
|
2129
2345
|
function isNeedsCoordinatorPortJob(job) {
|
|
2346
|
+
if (isResolvedCoordinatorReviewJob(job))
|
|
2347
|
+
return false;
|
|
2130
2348
|
const bucket = normalized(job.bucket);
|
|
2131
2349
|
const disposition = normalized(job.disposition);
|
|
2132
2350
|
const readiness = normalized(job.mergeReadiness);
|
|
2351
|
+
if (isCompletedJob(job) && ['completed', 'done', 'landed', 'applied'].includes(bucket))
|
|
2352
|
+
return false;
|
|
2133
2353
|
return bucket === 'needs-human-port'
|
|
2134
2354
|
|| bucket === 'needs-human-review'
|
|
2135
2355
|
|| bucket === 'needs-human-decision'
|
|
@@ -2140,16 +2360,29 @@ function isNeedsCoordinatorPortJob(job) {
|
|
|
2140
2360
|
|| readiness.includes('needs')
|
|
2141
2361
|
|| normalized(job.status) === 'needs-review';
|
|
2142
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
|
+
}
|
|
2143
2376
|
function isCoordinatorReviewJob(job) {
|
|
2144
2377
|
if (isResolvedCoordinatorReviewJob(job))
|
|
2145
2378
|
return false;
|
|
2146
2379
|
return isNeedsCoordinatorPortJob(job)
|
|
2147
|
-
|| isFailedJob(job)
|
|
2148
|
-
|| isStaleJob(job)
|
|
2149
2380
|
|| normalized(job.disposition).includes('review')
|
|
2150
2381
|
|| normalized(job.mergeReadiness).includes('review');
|
|
2151
2382
|
}
|
|
2152
2383
|
function isStaleJob(job) {
|
|
2384
|
+
if (isResolvedCoordinatorReviewJob(job))
|
|
2385
|
+
return false;
|
|
2153
2386
|
return normalized(job.bucket).includes('stale')
|
|
2154
2387
|
|| normalized(job.disposition).includes('stale')
|
|
2155
2388
|
|| normalized(job.mergeReadiness).includes('stale');
|
|
@@ -2197,6 +2430,8 @@ function pathSummaryText(job) {
|
|
|
2197
2430
|
const ignoredOwnership = ignoredOwnershipViolations(job);
|
|
2198
2431
|
const ignoredChangedPathCount = numberValue(job.ignoredChangedPathCount);
|
|
2199
2432
|
const quarantinedChangedPathCount = numberValue(job.quarantinedChangedPathCount);
|
|
2433
|
+
const changedPathCount = numberValue(job.changedPathCount);
|
|
2434
|
+
const evidencePathCount = numberValue(job.evidencePathCount);
|
|
2200
2435
|
const ignoredCount = ignoredOwnership.length + ignoredChangedPathCount;
|
|
2201
2436
|
if (sourceOwnership.length)
|
|
2202
2437
|
return `${text(sourceOwnership.length)} source issue`;
|
|
@@ -2204,7 +2439,27 @@ function pathSummaryText(job) {
|
|
|
2204
2439
|
return `${text(quarantinedChangedPathCount)} quarantined`;
|
|
2205
2440
|
if (ignoredCount)
|
|
2206
2441
|
return `${text(ignoredCount)} generated noise`;
|
|
2207
|
-
|
|
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'}`;
|
|
2208
2463
|
}
|
|
2209
2464
|
function sourceOwnershipViolations(job) {
|
|
2210
2465
|
const explicit = stringArray(job.sourceOwnershipViolations);
|
|
@@ -2498,6 +2753,8 @@ function tabMeta(tab, input) {
|
|
|
2498
2753
|
return 'AI tasks';
|
|
2499
2754
|
if (tab === 'swarm')
|
|
2500
2755
|
return 'active agents';
|
|
2756
|
+
if (tab === 'lanes')
|
|
2757
|
+
return `${text(input.lanes)} lanes`;
|
|
2501
2758
|
if (tab === 'performance')
|
|
2502
2759
|
return 'cost trends';
|
|
2503
2760
|
if (tab === 'history')
|
|
@@ -3385,16 +3642,17 @@ const MODEL_PRICES_PER_MILLION = [
|
|
|
3385
3642
|
];
|
|
3386
3643
|
function modelCostSummary(jobs) {
|
|
3387
3644
|
const allCosts = jobs.map(modelCostForJob);
|
|
3388
|
-
const costs = allCosts.filter((entry) => entry.
|
|
3645
|
+
const costs = allCosts.filter((entry) => entry.costKnown);
|
|
3389
3646
|
const unpricedCount = allCosts.length - costs.length;
|
|
3390
3647
|
const total = costs.reduce((sum, entry) => sum + entry.cost, 0);
|
|
3391
3648
|
if (!costs.length)
|
|
3392
3649
|
return { value: '-', detail: 'no priced model labels in this run' };
|
|
3393
|
-
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;
|
|
3394
3651
|
const models = uniqueStrings(costs.map((entry) => entry.model));
|
|
3395
3652
|
const outputJobs = costs.filter((entry) => entry.outputTokens > 0).length;
|
|
3396
3653
|
const estimated = costs.filter((entry) => entry.estimatedInput).length;
|
|
3397
3654
|
const longContextJobs = costs.filter((entry) => entry.longContext).length;
|
|
3655
|
+
const backendEstimated = costs.filter((entry) => entry.backendEstimate).length;
|
|
3398
3656
|
const pricingSources = uniqueStrings(costs.map((entry) => entry.pricingSource).filter((value) => Boolean(value)));
|
|
3399
3657
|
const modelText = models.length > 3 ? `${models.slice(0, 3).join(', ')} +${models.length - 3}` : models.join(', ');
|
|
3400
3658
|
const sourceText = pricingSources.length ? ` · ${pricingSources.join(', ')}` : '';
|
|
@@ -3405,20 +3663,21 @@ function modelCostSummary(jobs) {
|
|
|
3405
3663
|
const scope = outputJobs === 0 ? 'input-only' : outputJobs === costs.length ? 'input+output' : 'input+partial output';
|
|
3406
3664
|
const estimatedText = estimated ? ` · ${formatNumber(estimated)} estimated-input jobs` : '';
|
|
3407
3665
|
const longContextText = longContextJobs ? ` · ${formatNumber(longContextJobs)} long-context priced` : '';
|
|
3666
|
+
const backendText = backendEstimated ? ` · ${formatNumber(backendEstimated)} backend-priced jobs` : '';
|
|
3408
3667
|
const missingOutputText = outputJobs > 0 && outputJobs < costs.length ? ` · ${formatNumber(costs.length - outputJobs)} without output tokens` : '';
|
|
3409
3668
|
const noOutputText = outputJobs === 0 ? ' · no output tokens reported' : '';
|
|
3410
|
-
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}` };
|
|
3411
3670
|
}
|
|
3412
3671
|
function modelCostRows(jobs) {
|
|
3413
3672
|
return jobs
|
|
3414
3673
|
.map((job) => ({ job, cost: modelCostForJob(job) }))
|
|
3415
|
-
.filter((entry) => entry.cost.
|
|
3674
|
+
.filter((entry) => entry.cost.costKnown && entry.cost.cost > 0)
|
|
3416
3675
|
.sort((left, right) => right.cost.cost - left.cost.cost)
|
|
3417
3676
|
.slice(0, 5)
|
|
3418
3677
|
.map(({ job, cost }) => ({
|
|
3419
3678
|
label: `${ticketId(job)} · ${taskTitle(job)}`,
|
|
3420
3679
|
value: formatUsd(cost.cost),
|
|
3421
|
-
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' : ''}`
|
|
3422
3681
|
}));
|
|
3423
3682
|
}
|
|
3424
3683
|
function modelCostForJob(job) {
|
|
@@ -3426,12 +3685,32 @@ function modelCostForJob(job) {
|
|
|
3426
3685
|
const priceEntry = modelPriceEntryForModel(model);
|
|
3427
3686
|
const input = inputTokenBreakdownForJob(job);
|
|
3428
3687
|
const outputTokens = jobOutputTokens(job);
|
|
3688
|
+
const backendCost = backendCostEstimateForJob(job);
|
|
3429
3689
|
const longContext = Boolean(priceEntry?.longContextPrice && priceEntry.longContextThreshold && input.inputTokens > priceEntry.longContextThreshold);
|
|
3430
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
|
+
}
|
|
3431
3704
|
if (!price)
|
|
3432
|
-
return { model, ...input, outputTokens, longContext: false, cost: 0 };
|
|
3705
|
+
return { model, ...input, outputTokens, longContext: false, backendEstimate: false, costKnown: false, cost: 0 };
|
|
3433
3706
|
const cost = ((input.uncachedInputTokens * price.input) + (input.cachedInputTokens * price.cachedInput) + (outputTokens * price.output)) / 1_000_000;
|
|
3434
|
-
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);
|
|
3435
3714
|
}
|
|
3436
3715
|
function modelPriceEntryForModel(model) {
|
|
3437
3716
|
const key = normalizedModelKey(model);
|
|
@@ -3459,7 +3738,7 @@ function inputTokenBreakdownForJob(job) {
|
|
|
3459
3738
|
const inputDetails = recordValue(job.inputTokensDetails ?? job.input_tokens_details ?? usage.input_tokens_details);
|
|
3460
3739
|
const promptDetails = recordValue(job.promptTokensDetails ?? job.prompt_tokens_details ?? usage.prompt_tokens_details);
|
|
3461
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);
|
|
3462
|
-
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);
|
|
3463
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);
|
|
3464
3743
|
const reportedUncached = firstPositiveNumber(job.uncachedInputTokens, job.uncached_input_tokens, usage.uncached_input_tokens);
|
|
3465
3744
|
const inputTokens = actualInput || estimatedInput || reportedCached + reportedUncached;
|