bosun 0.40.3 → 0.40.4
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/README.md +4 -0
- package/cli.mjs +41 -2
- package/config/config.mjs +35 -15
- package/desktop/package.json +2 -2
- package/infra/monitor.mjs +44 -13
- package/infra/session-tracker.mjs +1 -0
- package/infra/sync-engine.mjs +6 -1
- package/infra/update-check.mjs +15 -7
- package/kanban/kanban-adapter.mjs +19 -4
- package/kanban/ve-orchestrator.ps1 +25 -0
- package/package.json +1 -1
- package/server/ui-server.mjs +385 -39
- package/ui/components/kanban-board.js +137 -9
- package/ui/components/shared.js +107 -45
- package/ui/demo-defaults.js +20 -20
- package/ui/modules/mui.js +600 -397
- package/ui/styles/kanban.css +66 -11
- package/ui/styles.monolith.css +89 -0
- package/ui/tabs/agents.js +194 -20
- package/ui/tabs/tasks.js +291 -70
- package/workflow/workflow-engine.mjs +0 -24
- package/workflow/workflow-nodes.mjs +219 -20
- package/workflow/workflow-templates.mjs +1 -1
- package/workflow-templates/task-batch.mjs +10 -10
- package/workspace/workspace-manager.mjs +11 -0
- package/workspace/worktree-manager.mjs +6 -0
package/ui/tabs/tasks.js
CHANGED
|
@@ -690,6 +690,177 @@ function buildEpicDagGraph(tasks = [], epicDependencies = []) {
|
|
|
690
690
|
edges,
|
|
691
691
|
};
|
|
692
692
|
}
|
|
693
|
+
function slugifyPlanningId(value, fallback = "item") {
|
|
694
|
+
const normalized = String(value || "")
|
|
695
|
+
.trim()
|
|
696
|
+
.toLowerCase()
|
|
697
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
698
|
+
.replace(/^-+|-+$/g, "");
|
|
699
|
+
return normalized || fallback;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function normalizeTaskTypeValue(value, fallback = "task") {
|
|
703
|
+
const normalized = toText(value, fallback).toLowerCase();
|
|
704
|
+
return ["epic", "task", "subtask"].includes(normalized) ? normalized : fallback;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function normalizeTaskStatusValue(value) {
|
|
708
|
+
return toText(value).toLowerCase();
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function isTerminalTaskStatus(value) {
|
|
712
|
+
return ["done", "completed", "closed", "merged", "cancelled"].includes(normalizeTaskStatusValue(value));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function isQueuedTask(task) {
|
|
716
|
+
const runtime = getTaskRuntimeSnapshot(task);
|
|
717
|
+
return runtime?.state === "queued" || normalizeTaskStatusValue(task?.status) === "queued";
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function isBacklogDraftTask(task) {
|
|
721
|
+
const status = normalizeTaskStatusValue(task?.status);
|
|
722
|
+
return status === "draft" || status === "todo" || status === "backlog" || status === "planned" || status === "";
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function isExecutionTask(task) {
|
|
726
|
+
return isActiveStatus(task?.status) || isReviewStatus(task?.status) || isQueuedTask(task);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function filterDagGraphByIds(graph = EMPTY_DAG_GRAPH, allowedIds = null, resolver = (node) => toText(node?.id || node?.taskId)) {
|
|
730
|
+
if (!allowedIds || allowedIds === "all") return graph;
|
|
731
|
+
const allowed = allowedIds instanceof Set ? allowedIds : new Set(Array.isArray(allowedIds) ? allowedIds : []);
|
|
732
|
+
const nodes = (graph?.nodes || []).filter((node) => allowed.has(resolver(node)) || allowed.has(toText(node?.id || node?.taskId)));
|
|
733
|
+
const nodeIds = new Set(nodes.map((node) => toText(node?.id || node?.taskId)).filter(Boolean));
|
|
734
|
+
const edges = (graph?.edges || []).filter((edge) => nodeIds.has(toText(edge?.source || edge?.from)) && nodeIds.has(toText(edge?.target || edge?.to)));
|
|
735
|
+
return { ...graph, nodes, edges };
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function buildEpicCatalog(tasks = [], epicDependencies = []) {
|
|
739
|
+
const catalog = new Map();
|
|
740
|
+
const ensureEpic = (epicId) => {
|
|
741
|
+
const id = toText(epicId);
|
|
742
|
+
if (!id) return null;
|
|
743
|
+
if (!catalog.has(id)) {
|
|
744
|
+
catalog.set(id, {
|
|
745
|
+
id,
|
|
746
|
+
label: id,
|
|
747
|
+
taskIds: [],
|
|
748
|
+
dependencies: [],
|
|
749
|
+
completedCount: 0,
|
|
750
|
+
activeCount: 0,
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
return catalog.get(id);
|
|
754
|
+
};
|
|
755
|
+
for (const task of tasks || []) {
|
|
756
|
+
const epicId = toText(task?.epicId || task?.meta?.epicId);
|
|
757
|
+
if (!epicId) continue;
|
|
758
|
+
const entry = ensureEpic(epicId);
|
|
759
|
+
if (!entry) continue;
|
|
760
|
+
entry.taskIds.push(task.id);
|
|
761
|
+
if (normalizeTaskTypeValue(task?.type) === "epic") {
|
|
762
|
+
entry.label = toText(task?.title, epicId);
|
|
763
|
+
entry.anchorTaskId = task.id;
|
|
764
|
+
}
|
|
765
|
+
if (isTerminalTaskStatus(task?.status)) entry.completedCount += 1;
|
|
766
|
+
if (isExecutionTask(task)) entry.activeCount += 1;
|
|
767
|
+
}
|
|
768
|
+
for (const row of epicDependencies || []) {
|
|
769
|
+
const entry = ensureEpic(row?.epicId);
|
|
770
|
+
if (!entry) continue;
|
|
771
|
+
entry.dependencies = normalizeDependencyInput(row?.dependencies || []);
|
|
772
|
+
}
|
|
773
|
+
return [...catalog.values()]
|
|
774
|
+
.map((entry) => ({
|
|
775
|
+
...entry,
|
|
776
|
+
taskIds: normalizeDependencyInput(entry.taskIds),
|
|
777
|
+
taskCount: normalizeDependencyInput(entry.taskIds).length,
|
|
778
|
+
}))
|
|
779
|
+
.sort((a, b) => String(a.label || a.id).localeCompare(String(b.label || b.id)));
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function buildDagPlanningState({ tasks = [], sprintId = "all", sprintOrderMode = "parallel", sprintOptions = [], epicDependencies = [] }) {
|
|
783
|
+
const scopedTasks = (tasks || []).filter((task) => sprintId === "all" ? true : getTaskSprintId(task) === sprintId);
|
|
784
|
+
const allTaskMap = new Map((tasks || []).map((task) => [toText(task?.id), task]));
|
|
785
|
+
const scopedTaskIds = new Set(scopedTasks.map((task) => toText(task?.id)).filter(Boolean));
|
|
786
|
+
const sprintModeMap = new Map((sprintOptions || []).map((sprint) => [sprint.id, toText(sprint.executionMode || sprint.taskOrderMode || 'parallel', 'parallel')]));
|
|
787
|
+
const epicDependencyMap = new Map((epicDependencies || []).map((row) => [toText(row?.epicId), normalizeDependencyInput(row?.dependencies || [])]));
|
|
788
|
+
const tasksByEpic = new Map();
|
|
789
|
+
for (const task of tasks || []) {
|
|
790
|
+
const epicId = toText(task?.epicId || task?.meta?.epicId);
|
|
791
|
+
if (!epicId) continue;
|
|
792
|
+
const list = tasksByEpic.get(epicId) || [];
|
|
793
|
+
list.push(task);
|
|
794
|
+
tasksByEpic.set(epicId, list);
|
|
795
|
+
}
|
|
796
|
+
const isSequentialBlocked = (task) => {
|
|
797
|
+
const taskSprintId = getTaskSprintId(task);
|
|
798
|
+
const mode = sprintModeMap.get(taskSprintId) || sprintOrderMode;
|
|
799
|
+
if (mode !== 'sequential') return false;
|
|
800
|
+
const currentOrder = Number(getTaskSprintOrder(task));
|
|
801
|
+
if (!Number.isFinite(currentOrder) || currentOrder <= 1) return false;
|
|
802
|
+
return scopedTasks.some((candidate) => getTaskSprintId(candidate) === taskSprintId && Number(getTaskSprintOrder(candidate)) < currentOrder && !isTerminalTaskStatus(candidate?.status));
|
|
803
|
+
};
|
|
804
|
+
const isEpicBlocked = (task) => {
|
|
805
|
+
const epicId = toText(task?.epicId || task?.meta?.epicId);
|
|
806
|
+
if (!epicId) return false;
|
|
807
|
+
const requiredEpics = epicDependencyMap.get(epicId) || [];
|
|
808
|
+
if (!requiredEpics.length) return false;
|
|
809
|
+
return requiredEpics.some((requiredEpicId) => {
|
|
810
|
+
const epicTasks = tasksByEpic.get(requiredEpicId) || [];
|
|
811
|
+
if (!epicTasks.length) return true;
|
|
812
|
+
return epicTasks.some((candidate) => !isTerminalTaskStatus(candidate?.status));
|
|
813
|
+
});
|
|
814
|
+
};
|
|
815
|
+
const isDependencyBlocked = (task) => getTaskDependencyIds(task).some((depId) => {
|
|
816
|
+
const dependencyTask = allTaskMap.get(depId);
|
|
817
|
+
return !dependencyTask || !isTerminalTaskStatus(dependencyTask?.status);
|
|
818
|
+
});
|
|
819
|
+
const backlogTaskIds = new Set();
|
|
820
|
+
const executionTaskIds = new Set();
|
|
821
|
+
const readyTaskIds = new Set();
|
|
822
|
+
const sprintIdsByFocus = { all: new Set(), backlog: new Set(), execution: new Set(), ready: new Set() };
|
|
823
|
+
const epicIdsByFocus = { all: new Set(), backlog: new Set(), execution: new Set(), ready: new Set() };
|
|
824
|
+
for (const task of scopedTasks) {
|
|
825
|
+
const taskId = toText(task?.id);
|
|
826
|
+
const taskSprintId = getTaskSprintId(task);
|
|
827
|
+
const epicId = toText(task?.epicId || task?.meta?.epicId);
|
|
828
|
+
sprintIdsByFocus.all.add(taskSprintId || 'unassigned');
|
|
829
|
+
if (epicId) epicIdsByFocus.all.add(epicId);
|
|
830
|
+
if (isBacklogDraftTask(task)) {
|
|
831
|
+
backlogTaskIds.add(taskId);
|
|
832
|
+
sprintIdsByFocus.backlog.add(taskSprintId || 'unassigned');
|
|
833
|
+
if (epicId) epicIdsByFocus.backlog.add(epicId);
|
|
834
|
+
}
|
|
835
|
+
if (isExecutionTask(task)) {
|
|
836
|
+
executionTaskIds.add(taskId);
|
|
837
|
+
sprintIdsByFocus.execution.add(taskSprintId || 'unassigned');
|
|
838
|
+
if (epicId) epicIdsByFocus.execution.add(epicId);
|
|
839
|
+
}
|
|
840
|
+
const ready = !isBacklogDraftTask(task) && !isExecutionTask(task) && !isTerminalTaskStatus(task?.status) && !isDependencyBlocked(task) && !isEpicBlocked(task) && !isSequentialBlocked(task);
|
|
841
|
+
if (ready) {
|
|
842
|
+
readyTaskIds.add(taskId);
|
|
843
|
+
sprintIdsByFocus.ready.add(taskSprintId || 'unassigned');
|
|
844
|
+
if (epicId) epicIdsByFocus.ready.add(epicId);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return {
|
|
848
|
+
scopedTaskIds,
|
|
849
|
+
backlogTaskIds,
|
|
850
|
+
executionTaskIds,
|
|
851
|
+
readyTaskIds,
|
|
852
|
+
sprintIdsByFocus,
|
|
853
|
+
epicIdsByFocus,
|
|
854
|
+
epicCatalog: buildEpicCatalog(tasks, epicDependencies),
|
|
855
|
+
counts: {
|
|
856
|
+
all: scopedTaskIds.size,
|
|
857
|
+
backlog: backlogTaskIds.size,
|
|
858
|
+
execution: executionTaskIds.size,
|
|
859
|
+
ready: readyTaskIds.size,
|
|
860
|
+
},
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
693
864
|
function extractGlobalDagPayload(...sources) {
|
|
694
865
|
for (const source of sources) {
|
|
695
866
|
const payload = extractDagPayload(source);
|
|
@@ -930,7 +1101,9 @@ async function applyTaskLifecycleTransition(task, requestedStatus) {
|
|
|
930
1101
|
|
|
931
1102
|
const wantsDraft = decision.nextStatus === "draft";
|
|
932
1103
|
const prevTasks = cloneValue(tasksData.value);
|
|
933
|
-
const optimisticStatus = decision.action === "start"
|
|
1104
|
+
const optimisticStatus = decision.action === "start"
|
|
1105
|
+
? String(task?.status || "todo")
|
|
1106
|
+
: decision.nextStatus;
|
|
934
1107
|
let apiResult = null;
|
|
935
1108
|
|
|
936
1109
|
await runOptimistic(
|
|
@@ -1563,6 +1736,18 @@ function isReviewStatus(s) {
|
|
|
1563
1736
|
return ["inreview", "review", "pr-open", "pr-review"].includes(String(s || ""));
|
|
1564
1737
|
}
|
|
1565
1738
|
|
|
1739
|
+
function getTaskRuntimeSnapshot(task) {
|
|
1740
|
+
return task?.runtimeSnapshot || task?.meta?.runtimeSnapshot || null;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
function hasLiveExecutionEvidence(task) {
|
|
1744
|
+
const runtime = getTaskRuntimeSnapshot(task);
|
|
1745
|
+
if (runtime?.isLive === true) return true;
|
|
1746
|
+
if (runtime?.state === "running") return true;
|
|
1747
|
+
if (runtime?.slot?.taskId) return true;
|
|
1748
|
+
return false;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1566
1751
|
async function reactivateTaskSession(taskId, options = {}) {
|
|
1567
1752
|
const normalizedTaskId = String(taskId || "").trim();
|
|
1568
1753
|
if (!normalizedTaskId) return false;
|
|
@@ -1590,41 +1775,20 @@ async function reactivateTaskSession(taskId, options = {}) {
|
|
|
1590
1775
|
showToast("Agent session reactivated", "success");
|
|
1591
1776
|
}
|
|
1592
1777
|
|
|
1778
|
+
const nextStatus = String(res?.data?.status || (res?.queued ? "queued" : "inprogress")).trim() || "todo";
|
|
1593
1779
|
tasksData.value = (tasksData.value || []).map((t) =>
|
|
1594
1780
|
String(t?.id || "").trim() === normalizedTaskId
|
|
1595
|
-
? {
|
|
1781
|
+
? {
|
|
1782
|
+
...t,
|
|
1783
|
+
...(res?.data || {}),
|
|
1784
|
+
status: nextStatus,
|
|
1785
|
+
}
|
|
1596
1786
|
: t,
|
|
1597
1787
|
);
|
|
1598
1788
|
scheduleRefresh(150);
|
|
1599
1789
|
return true;
|
|
1600
1790
|
}
|
|
1601
1791
|
|
|
1602
|
-
/* ─── Derive agent steps from task title/description ─── */
|
|
1603
|
-
function deriveSteps(task) {
|
|
1604
|
-
const t = String(task?.title || "").toLowerCase();
|
|
1605
|
-
const steps = [];
|
|
1606
|
-
if (/feat|add|implement|build|create/.test(t)) steps.push({ label: "Understand requirements & read context" });
|
|
1607
|
-
if (/fix|bug|patch|resolve|repair/.test(t)) steps.push({ label: "Reproduce and diagnose issue" });
|
|
1608
|
-
if (/refactor|restructure|cleanup|reorganize/.test(t)) steps.push({ label: "Map current code structure" });
|
|
1609
|
-
if (/test|spec|coverage/.test(t)) steps.push({ label: "Write test cases" });
|
|
1610
|
-
if (/docs|document|readme/.test(t)) steps.push({ label: "Draft documentation content" });
|
|
1611
|
-
steps.push({ label: "Write implementation" });
|
|
1612
|
-
if (!/docs|readme/.test(t)) steps.push({ label: "Run tests & fix issues" });
|
|
1613
|
-
steps.push({ label: "Commit changes" });
|
|
1614
|
-
steps.push({ label: "Handoff PR lifecycle to Bosun" });
|
|
1615
|
-
return steps;
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
/* ─── Estimate step progress from timing ─── */
|
|
1619
|
-
function estimateStep(task, steps) {
|
|
1620
|
-
const elapsed = task?.updated ? (Date.now() - new Date(task.updated).getTime()) : 0;
|
|
1621
|
-
const totalDur = task?.created ? (Date.now() - new Date(task.created).getTime()) : 60000;
|
|
1622
|
-
const pct = Math.min(0.85, totalDur > 0 ? (elapsed / totalDur) : 0);
|
|
1623
|
-
// Bias: show 40-75% through to look realistic
|
|
1624
|
-
const biasedPct = 0.35 + pct * 0.4;
|
|
1625
|
-
return Math.max(0, Math.min(steps.length - 1, Math.floor(biasedPct * steps.length)));
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
1792
|
/* ─── TaskProgressModal — live view for in-progress tasks ─── */
|
|
1629
1793
|
export function TaskProgressModal({ task, onClose }) {
|
|
1630
1794
|
const [liveTask, setLiveTask] = useState(task);
|
|
@@ -1672,8 +1836,7 @@ export function TaskProgressModal({ task, onClose }) {
|
|
|
1672
1836
|
logEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
1673
1837
|
}, [logs]);
|
|
1674
1838
|
|
|
1675
|
-
const
|
|
1676
|
-
const currentStep = useMemo(() => estimateStep(liveTask || task, steps), [liveTask?.updated]);
|
|
1839
|
+
const runtime = getTaskRuntimeSnapshot(liveTask || task);
|
|
1677
1840
|
|
|
1678
1841
|
const healthScore = health?.currentHealthScore ?? health?.averageHealthScore ?? null;
|
|
1679
1842
|
const healthColor =
|
|
@@ -1685,6 +1848,10 @@ export function TaskProgressModal({ task, onClose }) {
|
|
|
1685
1848
|
const startedRelative = liveTask?.created ? formatRelative(liveTask.created) : "—";
|
|
1686
1849
|
const agentLabel = liveTask?.assignee || task.assignee || "Agent";
|
|
1687
1850
|
const branchLabel = liveTask?.branch || task.branch || "—";
|
|
1851
|
+
const runtimeState = runtime?.state || "pending";
|
|
1852
|
+
const runtimeLabel = runtime?.statusLabel || "Live execution";
|
|
1853
|
+
const activeSlots = Number(runtime?.executor?.activeSlots || 0);
|
|
1854
|
+
const maxParallel = Number(runtime?.executor?.maxParallel || 0);
|
|
1688
1855
|
|
|
1689
1856
|
const handleCancel = async () => {
|
|
1690
1857
|
const ok = await showConfirm("Cancel this task?");
|
|
@@ -1736,9 +1903,9 @@ export function TaskProgressModal({ task, onClose }) {
|
|
|
1736
1903
|
<div class="tp-hero">
|
|
1737
1904
|
<div class="tp-pulse-dot"></div>
|
|
1738
1905
|
<div class="tp-hero-title">
|
|
1739
|
-
<div class="tp-hero-status-label">${iconText(
|
|
1906
|
+
<div class="tp-hero-status-label">${iconText(`:zap: ${runtimeLabel}`)}</div>
|
|
1740
1907
|
</div>
|
|
1741
|
-
<${Badge} status="inprogress" text
|
|
1908
|
+
<${Badge} status="inprogress" text=${runtimeState} />
|
|
1742
1909
|
</div>
|
|
1743
1910
|
|
|
1744
1911
|
|
|
@@ -3361,6 +3528,7 @@ function DagGraphSection({
|
|
|
3361
3528
|
allowWiring = false,
|
|
3362
3529
|
graphKey = "dag",
|
|
3363
3530
|
emptyMessage = "No DAG nodes available for this view yet.",
|
|
3531
|
+
highlightNodeIds = null,
|
|
3364
3532
|
}) {
|
|
3365
3533
|
const stageRef = useRef(null);
|
|
3366
3534
|
const [zoom, setZoom] = useState(1);
|
|
@@ -3505,6 +3673,13 @@ function DagGraphSection({
|
|
|
3505
3673
|
return map;
|
|
3506
3674
|
}, [sortedNodes]);
|
|
3507
3675
|
|
|
3676
|
+
const highlightedIds = useMemo(() => {
|
|
3677
|
+
if (!highlightNodeIds || highlightNodeIds === "all") return new Set();
|
|
3678
|
+
return highlightNodeIds instanceof Set
|
|
3679
|
+
? highlightNodeIds
|
|
3680
|
+
: new Set(Array.isArray(highlightNodeIds) ? highlightNodeIds : []);
|
|
3681
|
+
}, [highlightNodeIds]);
|
|
3682
|
+
|
|
3508
3683
|
const handleNodeClick = useCallback(async (node, event) => {
|
|
3509
3684
|
event?.stopPropagation?.();
|
|
3510
3685
|
if (allowWiring && typeof onCreateEdge === "function") {
|
|
@@ -3549,7 +3724,7 @@ function DagGraphSection({
|
|
|
3549
3724
|
<div class="task-dag-header-row">
|
|
3550
3725
|
<div>
|
|
3551
3726
|
<div style=${{ fontWeight: "700" }}>${title || "Task DAG"}</div>
|
|
3552
|
-
${description
|
|
3727
|
+
${description ? html`<div class="meta-text">${description}</div>` : null}
|
|
3553
3728
|
<div class="meta-text">Drag to pan · wheel to zoom · click node to ${allowWiring ? "wire edges" : "open task"}.</div>
|
|
3554
3729
|
</div>
|
|
3555
3730
|
<div class="task-dag-controls">
|
|
@@ -3558,7 +3733,9 @@ function DagGraphSection({
|
|
|
3558
3733
|
<${Button} size="small" variant="outlined" onClick=${fitToView}>Fit</${Button}>
|
|
3559
3734
|
<${Button} size="small" variant="text" onClick=${() => { setZoom(1); setPan({ x: 24, y: 24 }); }}>Reset</${Button}>
|
|
3560
3735
|
<span class="task-dag-zoom-pill">${Math.round(zoom * 100)}%</span>
|
|
3561
|
-
${allowWiring
|
|
3736
|
+
${allowWiring
|
|
3737
|
+
? html`<span class="task-dag-wire-pill">${wireSourceId ? `Source: ${wireSourceId}` : wiringBusy ? "Saving edge…" : "Wiring: click source then target"}</span>`
|
|
3738
|
+
: null}
|
|
3562
3739
|
</div>
|
|
3563
3740
|
</div>
|
|
3564
3741
|
<div class="task-dag-legend">
|
|
@@ -3600,10 +3777,11 @@ function DagGraphSection({
|
|
|
3600
3777
|
const pos = layout.positions.get(String(node.id));
|
|
3601
3778
|
if (!pos) return null;
|
|
3602
3779
|
const selected = wireSourceId && String(node.id) === wireSourceId;
|
|
3780
|
+
const highlighted = highlightedIds.has(String(node.id)) || highlightedIds.has(String(node.taskId || ""));
|
|
3603
3781
|
return html`
|
|
3604
3782
|
<g
|
|
3605
3783
|
key=${node.id}
|
|
3606
|
-
class=${`dag-node ${selected ? "dag-node-selected" : ""}`}
|
|
3784
|
+
class=${`dag-node ${selected ? "dag-node-selected" : ""} ${highlighted ? "dag-node-highlighted" : ""}`}
|
|
3607
3785
|
onPointerDown=${(event) => event.stopPropagation()}
|
|
3608
3786
|
onClick=${(event) => handleNodeClick(node, event)}
|
|
3609
3787
|
style=${{ cursor: allowWiring || node.taskId ? "pointer" : "default" }}
|
|
@@ -3616,8 +3794,8 @@ function DagGraphSection({
|
|
|
3616
3794
|
rx="14"
|
|
3617
3795
|
ry="14"
|
|
3618
3796
|
fill="var(--bg-surface)"
|
|
3619
|
-
stroke=${selected ? "var(--accent)" : "var(--border)"}
|
|
3620
|
-
stroke-width=${selected ? "2.2" : "1.5"}
|
|
3797
|
+
stroke=${selected ? "var(--accent)" : highlighted ? "var(--color-done)" : "var(--border)"}
|
|
3798
|
+
stroke-width=${selected || highlighted ? "2.2" : "1.5"}
|
|
3621
3799
|
/>
|
|
3622
3800
|
<text x=${pos.x + 12} y=${pos.y + 24} fill="var(--text-primary)" font-size="13" font-weight="700">
|
|
3623
3801
|
${truncate(node.title || "(untitled)", 34)}
|
|
@@ -3629,6 +3807,7 @@ function DagGraphSection({
|
|
|
3629
3807
|
${String(node.status || "todo")}
|
|
3630
3808
|
</text>
|
|
3631
3809
|
${Number.isFinite(node.order) && html`<text x=${pos.x + pos.width - 16} y=${pos.y + 22} text-anchor="end" fill="var(--text-muted)" font-size="11">#${node.order}</text>`}
|
|
3810
|
+
${highlighted && html`<text x=${pos.x + pos.width - 12} y=${pos.y + pos.height - 12} text-anchor="end" fill="var(--color-done)" font-size="11" font-weight="700">Ready</text>`}
|
|
3632
3811
|
</g>
|
|
3633
3812
|
`;
|
|
3634
3813
|
})}
|
|
@@ -3663,6 +3842,11 @@ export function TasksTab() {
|
|
|
3663
3842
|
const [dagEpicGraph, setDagEpicGraph] = useState(EMPTY_DAG_GRAPH);
|
|
3664
3843
|
const [dagSources, setDagSources] = useState({ sprints: "", sprintGraph: "", globalGraph: "", epicDeps: "", tasks: "" });
|
|
3665
3844
|
const [dagSprintOrderMode, setDagSprintOrderMode] = useState("parallel");
|
|
3845
|
+
const [dagAllTasks, setDagAllTasks] = useState([]);
|
|
3846
|
+
const [dagEpicDependencies, setDagEpicDependencies] = useState([]);
|
|
3847
|
+
const [dagFocusMode, setDagFocusMode] = useState("all");
|
|
3848
|
+
const [showCreateSprint, setShowCreateSprint] = useState(false);
|
|
3849
|
+
const [createSeed, setCreateSeed] = useState(null);
|
|
3666
3850
|
const [isCompact, setIsCompact] = useState(() => {
|
|
3667
3851
|
try { return globalThis.matchMedia?.("(max-width: 768px)")?.matches ?? false; }
|
|
3668
3852
|
catch { return false; }
|
|
@@ -3730,6 +3914,21 @@ export function TasksTab() {
|
|
|
3730
3914
|
const hasMoreKanbanPages = isKanban && page + 1 < totalPages;
|
|
3731
3915
|
const boardColumnTotals = tasksStatusCounts?.value || { draft: 0, backlog: 0, inProgress: 0, inReview: 0, done: 0 };
|
|
3732
3916
|
const boardTotalTasks = Number(tasksTotal?.value || 0);
|
|
3917
|
+
const dagTaskCatalog = dagAllTasks.length ? dagAllTasks : tasks;
|
|
3918
|
+
const dagPlanningState = useMemo(() => buildDagPlanningState({
|
|
3919
|
+
tasks: dagTaskCatalog,
|
|
3920
|
+
sprintId: dagSelectedSprint,
|
|
3921
|
+
sprintOrderMode: dagSprintOrderMode,
|
|
3922
|
+
sprintOptions: dagSprints,
|
|
3923
|
+
epicDependencies: dagEpicDependencies,
|
|
3924
|
+
}), [dagAllTasks, dagEpicDependencies, dagSelectedSprint, dagSprintOrderMode, dagSprints, tasks]);
|
|
3925
|
+
const dagEpicCatalog = dagPlanningState.epicCatalog;
|
|
3926
|
+
const dagFocusOptions = [
|
|
3927
|
+
{ id: "all", label: "All structure", count: dagPlanningState.counts.all },
|
|
3928
|
+
{ id: "backlog", label: "Backlog & draft", count: dagPlanningState.counts.backlog },
|
|
3929
|
+
{ id: "execution", label: "Running & review", count: dagPlanningState.counts.execution },
|
|
3930
|
+
{ id: "ready", label: "Ready next", count: dagPlanningState.counts.ready },
|
|
3931
|
+
];
|
|
3733
3932
|
|
|
3734
3933
|
const loadMoreKanbanTasks = useCallback(async () => {
|
|
3735
3934
|
if (!isKanban || kanbanLoadingMore || isSearching) return;
|
|
@@ -3786,6 +3985,8 @@ export function TasksTab() {
|
|
|
3786
3985
|
: tasks;
|
|
3787
3986
|
const epicDeps = normalizeEpicDependenciesPayload(epicDepsMeta?.payload);
|
|
3788
3987
|
const nextEpicGraph = buildEpicDagGraph(allTasks, epicDeps);
|
|
3988
|
+
setDagAllTasks(allTasks);
|
|
3989
|
+
setDagEpicDependencies(epicDeps);
|
|
3789
3990
|
|
|
3790
3991
|
const sprintMetaEntry = sprintOptions.find((entry) => entry.id === resolvedSprint) || null;
|
|
3791
3992
|
setDagSprintOrderMode(toText(sprintMetaEntry?.executionMode || sprintMetaEntry?.taskOrderMode || sprintMetaEntry?.sprintOrderMode || "parallel", "parallel"));
|
|
@@ -4028,6 +4229,32 @@ export function TasksTab() {
|
|
|
4028
4229
|
});
|
|
4029
4230
|
}, [visible, listSortCol, listSortDir]);
|
|
4030
4231
|
|
|
4232
|
+
const dagTaskFocusIds = useMemo(() => {
|
|
4233
|
+
if (dagFocusMode === "backlog") return dagPlanningState.backlogTaskIds;
|
|
4234
|
+
if (dagFocusMode === "ready") return dagPlanningState.readyTaskIds;
|
|
4235
|
+
if (dagFocusMode === "execution") return new Set([...dagPlanningState.executionTaskIds, ...dagPlanningState.readyTaskIds]);
|
|
4236
|
+
return "all";
|
|
4237
|
+
}, [dagFocusMode, dagPlanningState]);
|
|
4238
|
+
|
|
4239
|
+
const dagSprintFocusIds = useMemo(() => {
|
|
4240
|
+
if (dagFocusMode === "backlog") return dagPlanningState.sprintIdsByFocus.backlog;
|
|
4241
|
+
if (dagFocusMode === "ready") return dagPlanningState.sprintIdsByFocus.ready;
|
|
4242
|
+
if (dagFocusMode === "execution") return new Set([...dagPlanningState.sprintIdsByFocus.execution, ...dagPlanningState.sprintIdsByFocus.ready]);
|
|
4243
|
+
return "all";
|
|
4244
|
+
}, [dagFocusMode, dagPlanningState]);
|
|
4245
|
+
|
|
4246
|
+
const dagEpicFocusIds = useMemo(() => {
|
|
4247
|
+
if (dagFocusMode === "backlog") return dagPlanningState.epicIdsByFocus.backlog;
|
|
4248
|
+
if (dagFocusMode === "ready") return dagPlanningState.epicIdsByFocus.ready;
|
|
4249
|
+
if (dagFocusMode === "execution") return new Set([...dagPlanningState.epicIdsByFocus.execution, ...dagPlanningState.epicIdsByFocus.ready]);
|
|
4250
|
+
return "all";
|
|
4251
|
+
}, [dagFocusMode, dagPlanningState]);
|
|
4252
|
+
|
|
4253
|
+
const dagSprintGraphView = useMemo(() => filterDagGraphByIds(dagSprintGraph, dagTaskFocusIds, (node) => toText(node?.taskId || node?.id)), [dagSprintGraph, dagTaskFocusIds]);
|
|
4254
|
+
const dagGlobalGraphView = useMemo(() => filterDagGraphByIds(dagGlobalGraph, dagSprintFocusIds, (node) => toText(node?.sprintId || node?.id)), [dagGlobalGraph, dagSprintFocusIds]);
|
|
4255
|
+
const dagEpicGraphView = useMemo(() => filterDagGraphByIds(dagEpicGraph, dagEpicFocusIds, (node) => toText(node?.epicId || node?.id)), [dagEpicGraph, dagEpicFocusIds]);
|
|
4256
|
+
const dagReadyHighlightIds = dagFocusMode === "execution" || dagFocusMode === "ready" ? dagPlanningState.readyTaskIds : null;
|
|
4257
|
+
|
|
4031
4258
|
/* ── Handlers ── */
|
|
4032
4259
|
const handleFilter = async (s) => {
|
|
4033
4260
|
haptic();
|
|
@@ -4114,29 +4341,31 @@ export function TasksTab() {
|
|
|
4114
4341
|
}
|
|
4115
4342
|
}, [loadDagViews]);
|
|
4116
4343
|
|
|
4117
|
-
const handleCreateSprint = useCallback(
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4344
|
+
const handleCreateSprint = useCallback(() => {
|
|
4345
|
+
haptic("medium");
|
|
4346
|
+
setShowCreateSprint(true);
|
|
4347
|
+
}, []);
|
|
4348
|
+
|
|
4349
|
+
const handleMoveSprint = useCallback(async (sprintId, direction) => {
|
|
4350
|
+
const currentIndex = dagSprints.findIndex((entry) => entry.id === sprintId);
|
|
4351
|
+
const targetIndex = currentIndex + direction;
|
|
4352
|
+
if (currentIndex < 0 || targetIndex < 0 || targetIndex >= dagSprints.length) return;
|
|
4353
|
+
const reordered = [...dagSprints];
|
|
4354
|
+
const [moved] = reordered.splice(currentIndex, 1);
|
|
4355
|
+
reordered.splice(targetIndex, 0, moved);
|
|
4123
4356
|
haptic("medium");
|
|
4124
4357
|
try {
|
|
4125
|
-
await apiFetch(
|
|
4126
|
-
method: "
|
|
4127
|
-
body: JSON.stringify({
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
status: "active",
|
|
4131
|
-
}),
|
|
4132
|
-
});
|
|
4133
|
-
showToast("Sprint created", "success");
|
|
4358
|
+
await Promise.all(reordered.map((entry, index) => apiFetch(`/api/tasks/sprints/${encodeURIComponent(entry.id)}`, {
|
|
4359
|
+
method: "PATCH",
|
|
4360
|
+
body: JSON.stringify({ order: index + 1 }),
|
|
4361
|
+
})));
|
|
4362
|
+
showToast("Sprint order updated", "success");
|
|
4134
4363
|
await loadDagViews();
|
|
4135
|
-
if (id) setDagSelectedSprint(id);
|
|
4136
4364
|
} catch {
|
|
4137
|
-
|
|
4365
|
+
setDagError("Failed to reorder sprints.");
|
|
4138
4366
|
}
|
|
4139
|
-
}, [loadDagViews]);
|
|
4367
|
+
}, [dagSprints, loadDagViews]);
|
|
4368
|
+
|
|
4140
4369
|
const handleDagSprintModeChange = useCallback(async (mode) => {
|
|
4141
4370
|
const nextMode = toText(mode, "parallel").toLowerCase();
|
|
4142
4371
|
if (!dagSelectedSprint || dagSelectedSprint === "all") {
|
|
@@ -4856,8 +5085,8 @@ export function TasksTab() {
|
|
|
4856
5085
|
|
|
4857
5086
|
${isDag && html`
|
|
4858
5087
|
<div class="task-dag-wrap" style=${{ display: "grid", gap: "10px", marginTop: "8px" }}>
|
|
4859
|
-
${dagError
|
|
4860
|
-
${dagLoading
|
|
5088
|
+
${dagError ? html`<${Alert} severity="warning">${dagError}</${Alert}>` : null}
|
|
5089
|
+
${dagLoading ? html`<${Alert} severity="info">Loading DAG data…</${Alert}>` : null}
|
|
4861
5090
|
<${DagGraphSection}
|
|
4862
5091
|
title=${dagSprintGraph.title || (dagSelectedSprint === "all" ? "All Sprint DAG" : `Sprint ${dagSelectedSprint} DAG`)}
|
|
4863
5092
|
description=${dagSprintGraph.description || "Task dependency order within the selected sprint."}
|
|
@@ -4867,7 +5096,7 @@ export function TasksTab() {
|
|
|
4867
5096
|
onCreateEdge={({ sourceNode, targetNode }) => handleCreateDagEdge({ sourceNode, targetNode, graphKind: "task" })}
|
|
4868
5097
|
allowWiring=${true}
|
|
4869
5098
|
emptyMessage="No sprint DAG data available yet."
|
|
4870
|
-
|
|
5099
|
+
><//>
|
|
4871
5100
|
<${DagGraphSection}
|
|
4872
5101
|
title=${dagGlobalGraph.title || "Global DAG of DAGs"}
|
|
4873
5102
|
description=${dagGlobalGraph.description || "Cross-sprint dependency overview."}
|
|
@@ -4877,7 +5106,7 @@ export function TasksTab() {
|
|
|
4877
5106
|
onCreateEdge={({ sourceNode, targetNode }) => handleCreateDagEdge({ sourceNode, targetNode, graphKind: "task" })}
|
|
4878
5107
|
allowWiring=${true}
|
|
4879
5108
|
emptyMessage="No global DAG data available yet."
|
|
4880
|
-
|
|
5109
|
+
><//>
|
|
4881
5110
|
<${DagGraphSection}
|
|
4882
5111
|
title=${dagEpicGraph.title || "Epic Dependency DAG"}
|
|
4883
5112
|
description=${dagEpicGraph.description || "Epics and their run prerequisites."}
|
|
@@ -4886,7 +5115,7 @@ export function TasksTab() {
|
|
|
4886
5115
|
onCreateEdge={({ sourceNode, targetNode }) => handleCreateDagEdge({ sourceNode, targetNode, graphKind: "epic" })}
|
|
4887
5116
|
allowWiring=${true}
|
|
4888
5117
|
emptyMessage="No epic DAG data available yet."
|
|
4889
|
-
|
|
5118
|
+
><//>
|
|
4890
5119
|
</div>
|
|
4891
5120
|
`}
|
|
4892
5121
|
|
|
@@ -5013,22 +5242,14 @@ export function TasksTab() {
|
|
|
5013
5242
|
html`
|
|
5014
5243
|
<${CreateTaskModalInline} onClose=${() => setShowCreate(false)} />
|
|
5015
5244
|
`}
|
|
5016
|
-
${detailTask && isActiveStatus(detailTask.status) &&
|
|
5245
|
+
${detailTask && isActiveStatus(detailTask.status) && hasLiveExecutionEvidence(detailTask) &&
|
|
5017
5246
|
html`
|
|
5018
5247
|
<${TaskProgressModal}
|
|
5019
5248
|
task=${detailTask}
|
|
5020
5249
|
onClose=${() => setDetailTask(null)}
|
|
5021
5250
|
/>
|
|
5022
5251
|
`}
|
|
5023
|
-
${detailTask &&
|
|
5024
|
-
html`
|
|
5025
|
-
<${TaskReviewModal}
|
|
5026
|
-
task=${detailTask}
|
|
5027
|
-
onClose=${() => setDetailTask(null)}
|
|
5028
|
-
onStart=${(task) => openStartModal(task)}
|
|
5029
|
-
/>
|
|
5030
|
-
`}
|
|
5031
|
-
${detailTask && !isActiveStatus(detailTask.status) && !isReviewStatus(detailTask.status) &&
|
|
5252
|
+
${detailTask && (!isActiveStatus(detailTask.status) || !hasLiveExecutionEvidence(detailTask)) &&
|
|
5032
5253
|
html`
|
|
5033
5254
|
<${TaskDetailModal}
|
|
5034
5255
|
task=${detailTask}
|
|
@@ -2466,18 +2466,6 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
2466
2466
|
|
|
2467
2467
|
if (!runs.length) {
|
|
2468
2468
|
this._resumingRuns = false;
|
|
2469
|
-
this._taskTraceHooks = new Set();
|
|
2470
|
-
if (typeof opts.onTaskWorkflowEvent === "function") {
|
|
2471
|
-
this._taskTraceHooks.add(opts.onTaskWorkflowEvent);
|
|
2472
|
-
}
|
|
2473
|
-
if (typeof opts.taskTraceHook === "function") {
|
|
2474
|
-
this._taskTraceHooks.add(opts.taskTraceHook);
|
|
2475
|
-
}
|
|
2476
|
-
if (Array.isArray(opts.taskTraceHooks)) {
|
|
2477
|
-
for (const hook of opts.taskTraceHooks) {
|
|
2478
|
-
if (typeof hook === "function") this._taskTraceHooks.add(hook);
|
|
2479
|
-
}
|
|
2480
|
-
}
|
|
2481
2469
|
return;
|
|
2482
2470
|
}
|
|
2483
2471
|
|
|
@@ -2536,18 +2524,6 @@ export class WorkflowEngine extends EventEmitter {
|
|
|
2536
2524
|
}
|
|
2537
2525
|
} finally {
|
|
2538
2526
|
this._resumingRuns = false;
|
|
2539
|
-
this._taskTraceHooks = new Set();
|
|
2540
|
-
if (typeof opts.onTaskWorkflowEvent === "function") {
|
|
2541
|
-
this._taskTraceHooks.add(opts.onTaskWorkflowEvent);
|
|
2542
|
-
}
|
|
2543
|
-
if (typeof opts.taskTraceHook === "function") {
|
|
2544
|
-
this._taskTraceHooks.add(opts.taskTraceHook);
|
|
2545
|
-
}
|
|
2546
|
-
if (Array.isArray(opts.taskTraceHooks)) {
|
|
2547
|
-
for (const hook of opts.taskTraceHooks) {
|
|
2548
|
-
if (typeof hook === "function") this._taskTraceHooks.add(hook);
|
|
2549
|
-
}
|
|
2550
|
-
}
|
|
2551
2527
|
}
|
|
2552
2528
|
}
|
|
2553
2529
|
|