diff --git a/dashboard/src/v2/components/LiveTaskCard.tsx b/dashboard/src/v2/components/LiveTaskCard.tsx index f5291d16dd..b4b0526b73 100644 --- a/dashboard/src/v2/components/LiveTaskCard.tsx +++ b/dashboard/src/v2/components/LiveTaskCard.tsx @@ -14,7 +14,6 @@ import { RuntimeEventFeed } from "./RuntimeEventFeed.js"; import { renderMarkdown } from "../../lib/markdown.js"; import type { Subtask, ExecutionRuntimeEventSummary, ExecutionInvocationRecord } from "../../types.js"; import { - MERGE_INDICATOR_CFG, getTaskCfg, } from "../lib/live-session-config.js"; import { getTaskProgressPhase, type TaskProgressPhase } from "../../lib/task-progress.js"; @@ -23,12 +22,11 @@ import { RerunTaskModal } from "./ui/RerunTaskModal.js"; import { Button } from "./ui/Button.js"; import { useReducedMotion } from "../hooks/use-reduced-motion.js"; import { AgentSelectAvatarIcon } from "./agents/AgentSelectAvatarIcon.js"; -import { SprintReviewBadge } from "./sprints/SprintReviewBadge.js"; import { SelfReflectionRatingBadge } from "./tasks/SelfReflectionRatingBadge.js"; import { getSafeUrl } from "../lib/safe-url.js"; import { LiveTaskInvocationRow } from "./live-session/LiveTaskInvocationRow.js"; import { QuotaCountdown, TaskDuration } from "./live-session/LiveTaskTiming.js"; -import { CiStatusBadge } from "./ui/CiStatusBadge.js"; +import { WorkflowStatusBadge } from "./ui/WorkflowStatusBadge.js"; import type { CiStatusPresentation } from "../lib/ci-status-presentation.js"; /* ─── LiveTaskCard ───────────────────────────────────────────────────────── */ @@ -106,7 +104,6 @@ const LiveTaskCard: FunctionComponent = memo(({ const StatusIcon = cfg.icon; const hasEventFeed = Boolean(events && events.length > 0); const hasInvocations = invocations.length > 0; - const mergeCfg = task.merge_indicator ? MERGE_INDICATOR_CFG[task.merge_indicator] : null; const sessionLabel = (task.session_id || task.session_name || "").replace(/^sessions\//, ""); const isForceCompleteUnavailable = taskPhase === "COMPLETED" || isForceCompleting; const forceCompleteStatusMessage = isForceCompleting @@ -290,22 +287,14 @@ const LiveTaskCard: FunctionComponent = memo(({ #{task.id} - {/* Status badge */} - - - Task status: {cfg.label} - - {ciPresentation && ( - - )} - {mergeCfg && !ciPresentation && taskPhase !== "RUNNING" && taskPhase !== "PENDING" && ( - - {mergeCfg.label} - - )} - {task.latestReview && ( - - )} +

diff --git a/dashboard/src/v2/components/sprints/SprintCell.tsx b/dashboard/src/v2/components/sprints/SprintCell.tsx index 5e1214f273..1451386765 100644 --- a/dashboard/src/v2/components/sprints/SprintCell.tsx +++ b/dashboard/src/v2/components/sprints/SprintCell.tsx @@ -22,9 +22,8 @@ import type { ExecutionHumanInterventionSummary, Sprint, SprintStatus } from ".. import { WaveFluid } from "../ui/WaveFluid.js"; import { BorderTrace } from "../ui/BorderTrace.js"; import { HumanInterventionBadge } from "../ui/HumanInterventionBadge.js"; -import { CiStatusBadge } from "../ui/CiStatusBadge.js"; +import { WorkflowStatusBadge } from "../ui/WorkflowStatusBadge.js"; import type { CiStatusPresentation } from "../../lib/ci-status-presentation.js"; -import { SprintReviewBadge } from "./SprintReviewBadge.js"; import { SprintActionMenu } from "./SprintActionMenu.js"; import { resolveSprintAttentionIndicatorState, @@ -350,19 +349,21 @@ export const SprintCell: FunctionComponent = ({ {formatBubbleTime(sprint.createdAt)} - {(showInterventionBadge || sprint.latestReview || ciStatus) && ( -
- {sprint.latestReview && ( - - )} - +
+ {showInterventionBadge && humanIntervention && (
)} -
- )} +
diff --git a/dashboard/src/v2/components/sprints/SprintLedgerRow.tsx b/dashboard/src/v2/components/sprints/SprintLedgerRow.tsx index 071a9c52c2..6f76d27d32 100644 --- a/dashboard/src/v2/components/sprints/SprintLedgerRow.tsx +++ b/dashboard/src/v2/components/sprints/SprintLedgerRow.tsx @@ -22,9 +22,8 @@ import { } from "lucide-preact"; import { useState, useRef, useEffect } from "preact/hooks"; import { HumanInterventionBadge } from "../ui/HumanInterventionBadge.js"; -import { CiStatusBadge } from "../ui/CiStatusBadge.js"; +import { WorkflowStatusBadge } from "../ui/WorkflowStatusBadge.js"; import type { CiStatusPresentation } from "../../lib/ci-status-presentation.js"; -import { SprintReviewBadge } from "./SprintReviewBadge.js"; import { SprintActionMenu } from "./SprintActionMenu.js"; import { resolveSprintAttentionIndicatorState, @@ -395,9 +394,6 @@ const SprintLedgerRowComponent: FunctionComponent = ({ {pendingLabel} ) : null} - {sprint.latestReview && ( - - )}
@@ -431,7 +427,14 @@ const SprintLedgerRowComponent: FunctionComponent = ({ {badgeLabel} - + {isDeletePending ? ( Deleting diff --git a/dashboard/src/v2/components/sprints/__tests__/SprintCell.visual.test.tsx b/dashboard/src/v2/components/sprints/__tests__/SprintCell.visual.test.tsx index 57f86fd678..c886171449 100644 --- a/dashboard/src/v2/components/sprints/__tests__/SprintCell.visual.test.tsx +++ b/dashboard/src/v2/components/sprints/__tests__/SprintCell.visual.test.tsx @@ -198,7 +198,7 @@ describe("SprintCell visuals", () => { expect(container.querySelector(".border-orange-400\\/35")).toBeInTheDocument(); }); - it("shows CI failure steps and requested-change QA details without replacing lifecycle status", () => { + it("keeps a running sprint on Coding while retaining requested-change QA details", () => { const { container } = render( { expect(screen.getByText("Running")).toBeInTheDocument(); expect(screen.queryByText("CI")).not.toBeInTheDocument(); - const ciTrigger = screen.getByRole("button", { name: /CI status: CI failed.*Show workflow details/i }); - expect(ciTrigger).toHaveClass("text-status-red"); - expect(container.querySelector('[data-ci-icon="failure"]')).toHaveClass("text-status-red"); + const ciTrigger = screen.getByRole("button", { name: /CI status: Coding in progress.*Show workflow details/i }); + expect(ciTrigger).toHaveClass("text-signal-700"); + expect(ciTrigger).toHaveTextContent("Coding in progress"); + expect(container.querySelector('[data-ci-icon="failure"]')).not.toBeInTheDocument(); fireEvent.click(ciTrigger); const workflow = screen.getByRole("region", { name: "CI workflow details" }); expect(within(workflow).getByText("Pull request")).toBeVisible(); - expect(within(workflow).getByText("Checks")).toBeVisible(); + expect(within(workflow).getByText("CI")).toBeVisible(); expect(within(workflow).getByText("Merge")).toBeVisible(); + expect(within(workflow).getByText("Waiting for pull request")).toBeVisible(); + expect(within(workflow).getByText("Checks pending")).toBeVisible(); + expect(within(workflow).getByText("Merge pending")).toBeVisible(); const qaTrigger = screen.getByRole("button", { name: "QA review details" }); + expect(qaTrigger).toHaveClass("text-blue-700"); expect(qaTrigger).toHaveAccessibleDescription(/QA changes requested/i); fireEvent.click(qaTrigger); const review = screen.getByRole("region", { name: "QA Changes Requested" }); diff --git a/dashboard/src/v2/components/tasks/KanbanTaskCard.tsx b/dashboard/src/v2/components/tasks/KanbanTaskCard.tsx index 6a53b91d84..482fcd7d59 100644 --- a/dashboard/src/v2/components/tasks/KanbanTaskCard.tsx +++ b/dashboard/src/v2/components/tasks/KanbanTaskCard.tsx @@ -18,8 +18,7 @@ import type { AgentAvatarConfig } from "../../types.js"; import './kanban-task-card.css'; import { getSafeUrl } from "../../lib/safe-url.js"; import { SelfReflectionRatingBadge } from "./SelfReflectionRatingBadge.js"; -import { SprintReviewBadge } from "../sprints/SprintReviewBadge.js"; -import { CiStatusBadge } from "../ui/CiStatusBadge.js"; +import { WorkflowStatusBadge } from "../ui/WorkflowStatusBadge.js"; import { TaskCardActionMenu } from "./TaskCardActionMenu.js"; export const KanbanTaskCard: FunctionComponent<{ @@ -40,7 +39,6 @@ export const KanbanTaskCard: FunctionComponent<{ const interactionTokens = useInteractionTokens(); const blockerCount = dependencyIndicators.filter((dep) => dep.isBlocking ?? dep.status !== "completed").length; const dependencyActionLabel = viewModel.dependencyActionLabel ?? (blockerCount > 0 ? `${blockerCount} dependency ${blockerCount === 1 ? "blocker" : "blockers"}` : "Dependencies clear"); - const qaNoReviewLabel = viewModel.qaReviewLabel ?? "QA no review"; const dragStateLabel = viewModel.dragStateLabel ?? "Pointer drag only; keyboard reordering is not supported"; const shouldShowExecutorLabel = viewModel.executorLabel !== "Auto"; const hasPullRequestMetadata = viewModel.hasPullRequestMetadata ?? true; @@ -197,19 +195,13 @@ export const KanbanTaskCard: FunctionComponent<{ Session ID: {sessionId} )} - {task.latestReview ? ( - - ) : ( - - {qaNoReviewLabel} - - )} - {dependencyIndicators.length > 0 && ( diff --git a/dashboard/src/v2/components/tasks/__tests__/KanbanTaskCard.integration.test.tsx b/dashboard/src/v2/components/tasks/__tests__/KanbanTaskCard.integration.test.tsx index 52bbf6c20e..74116b083e 100644 --- a/dashboard/src/v2/components/tasks/__tests__/KanbanTaskCard.integration.test.tsx +++ b/dashboard/src/v2/components/tasks/__tests__/KanbanTaskCard.integration.test.tsx @@ -573,6 +573,8 @@ describe("KanbanTaskCard Integration", () => { await user.tab(); expect(card).toHaveFocus(); await user.tab(); + expect(getByRole("button", { name: /CI status: Coding in progress/i })).toHaveFocus(); + await user.tab(); expect(actionTrigger).toHaveFocus(); await user.keyboard("{ArrowDown}"); diff --git a/dashboard/src/v2/components/ui/TaskRow.tsx b/dashboard/src/v2/components/ui/TaskRow.tsx index 503d672a4c..6904e7ac2e 100644 --- a/dashboard/src/v2/components/ui/TaskRow.tsx +++ b/dashboard/src/v2/components/ui/TaskRow.tsx @@ -3,7 +3,7 @@ import { memo } from "preact/compat"; import { FolderGit2, CheckCircle2, Circle, PlayCircle, Clock, Play, Square, Settings, Maximize2, Loader2 } from "lucide-preact"; import type { Task } from "../../types.js"; import type { TaskStreamState } from "../../hooks/use-overview-stream-actions.js"; -import { SprintReviewBadge } from "../sprints/SprintReviewBadge.js"; +import { WorkflowStatusBadge } from "./WorkflowStatusBadge.js"; import { useInteractionTokens } from "../../lib/motion/tokens.js"; interface TaskRowProps { @@ -47,11 +47,15 @@ export const TaskRow: FunctionComponent = memo(({ task, state, onP {task.title} - {task.latestReview && ( -
- -
- )} +
+ +
{/* Source */} diff --git a/dashboard/src/v2/components/ui/WorkflowStatusBadge.tsx b/dashboard/src/v2/components/ui/WorkflowStatusBadge.tsx new file mode 100644 index 0000000000..61be19b7cf --- /dev/null +++ b/dashboard/src/v2/components/ui/WorkflowStatusBadge.tsx @@ -0,0 +1,539 @@ +import type { FunctionComponent } from "preact"; +import { createPortal } from "preact/compat"; +import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks"; +import { + Check, + CheckCircle2, + ChevronRight, + Circle, + Code2, + Flag, + GitMerge, + GitPullRequest, + ListChecks, + Loader2, + PencilLine, + Sparkles, + XCircle, +} from "lucide-preact"; +import type { LucideIcon } from "lucide-preact"; +import type { SprintReviewSummary } from "../../types.js"; +import type { CiStatusPresentation, CiWorkflowState } from "../../lib/ci-status-presentation.js"; +import { + deriveWorkflowStatusPresentation, + type WorkflowStage, + type WorkflowStageId, +} from "../../lib/workflow-status-presentation.js"; +import { calculatePosition, type Position } from "../../lib/positioning/index.js"; +import "./workflow-status-badge.css"; + +export interface WorkflowStatusBadgeProps { + scope: "task" | "sprint"; + status: string; + review?: SprintReviewSummary | null; + ciPresentation?: CiStatusPresentation | null; + compact?: boolean; + align?: "left" | "right"; + className?: string; +} + +const STAGE_ICONS: Record = { + coding: Code2, + pull_request: GitPullRequest, + qa: ListChecks, + checks: CheckCircle2, + merge: GitMerge, + completion: Flag, +}; + +const STATE_TONES: Record = { + pending: { + circle: "border-slate-300 bg-white text-slate-300 dark:border-white/15 dark:bg-void-800 dark:text-slate-500", + icon: "text-slate-400 dark:text-slate-500", + row: "border-black/[0.05] bg-black/[0.018] dark:border-white/[0.05] dark:bg-white/[0.025]", + }, + in_progress: { + circle: "border-signal-500 bg-signal-500 text-white shadow-[0_0_0_4px_rgba(0,94,184,0.10),0_0_18px_rgba(0,224,160,0.22)]", + icon: "text-signal-600 dark:text-signal-300", + row: "border-signal-500/20 bg-signal-500/[0.08] shadow-[0_8px_24px_rgba(0,224,160,0.08)]", + }, + successful: { + circle: "border-status-green bg-status-green text-white shadow-[0_0_0_4px_rgba(0,171,132,0.09)]", + icon: "text-status-green", + row: "border-status-green/15 bg-status-green/[0.055]", + }, + failed: { + circle: "border-status-red bg-status-red text-white shadow-[0_0_0_4px_rgba(227,0,15,0.08)]", + icon: "text-status-red", + row: "border-status-red/20 bg-status-red/[0.06]", + }, +}; + +const BADGE_TONES = { + pending: "border-slate-300/70 bg-white/90 text-slate-600 shadow-[0_8px_24px_rgba(15,23,42,0.08)] dark:border-white/12 dark:bg-white/[0.07] dark:text-slate-200", + active: "border-signal-500/35 bg-signal-500/[0.13] text-signal-700 shadow-[0_9px_26px_rgba(0,224,160,0.16)] dark:text-signal-300", + successful: "border-status-green/35 bg-status-green/[0.13] text-status-green shadow-[0_9px_26px_rgba(0,171,132,0.16)]", + failed: "border-status-red/35 bg-status-red/[0.12] text-status-red shadow-[0_9px_26px_rgba(227,0,15,0.15)]", + qa_changes: "border-blue-500/40 bg-blue-500/[0.13] text-blue-700 shadow-[0_9px_26px_rgba(59,130,246,0.18)] dark:text-blue-300", +} as const; + +function reviewState(summary: SprintReviewSummary): "running" | "passed" | "changes_requested" | "failed" { + const status = summary.status.toLowerCase(); + const outcome = summary.outcome?.toLowerCase() ?? ""; + if (status === "running" || status === "pending") return "running"; + if (outcome === "changes_requested") return "changes_requested"; + if (["failed", "errored", "cancelled"].includes(status) || ["failed", "rejected"].includes(outcome)) return "failed"; + return "passed"; +} + +const REVIEW_META = { + running: { + icon: Loader2, + label: "QA review in progress", + tone: "border-signal-500/25 bg-signal-500/[0.07] text-signal-700 dark:text-signal-300", + iconTone: "text-signal-500", + accent: "from-signal-500 via-signal-300 to-signal-500", + regionLabel: "QA Review In Progress", + }, + passed: { + icon: CheckCircle2, + label: "QA review passed", + tone: "border-status-green/25 bg-status-green/[0.07] text-status-green", + iconTone: "text-status-green", + accent: "from-status-green via-signal-300 to-status-green", + regionLabel: "QA Review Complete", + }, + changes_requested: { + icon: PencilLine, + label: "QA edits requested", + tone: "border-blue-500/30 bg-blue-500/[0.08] text-blue-700 dark:text-blue-300", + iconTone: "text-blue-500", + accent: "from-blue-600 via-blue-300 to-blue-600", + regionLabel: "QA Changes Requested", + }, + failed: { + icon: XCircle, + label: "QA review failed", + tone: "border-status-red/30 bg-status-red/[0.07] text-status-red", + iconTone: "text-status-red", + accent: "from-status-red via-ember-400 to-status-red", + regionLabel: "QA Provider Review Failed", + }, +} as const; + +function formatReviewDate(value: string): string { + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + return new Intl.DateTimeFormat("en-US", { + month: "short", + day: "numeric", + hour: "numeric", + minute: "numeric", + }).format(date); +} + +const WorkflowStageRow: FunctionComponent<{ stage: WorkflowStage; isLast: boolean }> = ({ stage, isLast }) => { + const tone = stage.id === "qa" && stage.statusLabel === "Changes requested" + ? { + ...STATE_TONES.failed, + circle: "border-blue-500 bg-blue-500 text-white shadow-[0_0_0_4px_rgba(59,130,246,0.10)]", + icon: "text-blue-500", + row: "border-blue-500/20 bg-blue-500/[0.07]", + } + : STATE_TONES[stage.state]; + const StageIcon = STAGE_ICONS[stage.id]; + const StateIcon = stage.state === "successful" ? Check : stage.state === "failed" ? XCircle : stage.state === "in_progress" ? Loader2 : Circle; + return ( +
  • + + + + + {!isLast && ( + + )} + + + + + {stage.label} + {stage.statusLabel} + + +
  • + ); +}; + +const FollowUpTaskDisclosure: FunctionComponent<{ + task: NonNullable[number]; + index: number; +}> = ({ task, index }) => { + const [expanded, setExpanded] = useState(false); + const contentId = useId(); + return ( +
    + + {expanded && ( +
    +
    Title
    +
    {task.title}
    +
    Description
    +
    {task.description || "Not provided"}
    +
    Priority
    +
    {task.priority}
    +
    Dependencies
    +
    {task.dependsOnTaskKeys.length > 0 ? task.dependsOnTaskKeys.join(", ") : "None"}
    +
    Prompt
    +
    {task.promptMarkdown}
    +
    + )} +
    + ); +}; + +const QaReviewCard: FunctionComponent<{ summary: SprintReviewSummary; headingId: string }> = ({ summary, headingId }) => { + const state = reviewState(summary); + const meta = REVIEW_META[state]; + const cardTone = state === "changes_requested" + ? "border-blue-500/30 text-blue-700 dark:text-blue-300" + : state === "failed" + ? "border-status-red/30 text-status-red" + : state === "passed" + ? "border-status-green/25 text-status-green" + : "border-signal-500/25 text-signal-700 dark:text-signal-300"; + const ReviewIcon = meta.icon; + const findings = summary.findings ?? []; + const followUps = summary.followUpTasks ?? []; + return ( +
    + +
    +
    +

    + + {meta.regionLabel} +

    + {summary.outcome && ( + + {summary.outcome.replaceAll("_", " ")} + + )} +
    +
    +

    Review summary

    +

    + {summary.summary || (state === "running" ? "The QA reviewer is inspecting this workflow." : "No additional review summary was provided.")} +

    +
    + {summary.fixInstructions && ( +
    +

    + Fix instructions +

    +

    {summary.fixInstructions}

    +
    + )} + {findings.length > 0 && ( +
    +

    Findings · {findings.length}

    +
      + {findings.map((finding, index) => ( +
    • + + {finding} +
    • + ))} +
    +
    + )} + {followUps.length > 0 && ( +
    +

    Follow-up tasks · {followUps.length}

    +
    + {followUps.map((task, index) => ( + + ))} +
    +
    + )} + {(summary.reviewer || summary.finishedAt || summary.targetTaskKey) && ( +
    + {summary.reviewer && <>
    Reviewer
    Reviewed by {summary.reviewer}
    } + {summary.finishedAt && <>
    Reviewed
    {formatReviewDate(summary.finishedAt)}
    } + {summary.targetTaskKey && <>
    Target
    {summary.targetTaskKey}
    } +
    + )} +
    +
    + ); +}; + +export const WorkflowStatusBadge: FunctionComponent = ({ + scope, + status, + review = null, + ciPresentation = null, + compact = false, + align = "left", + className = "", +}) => { + const [open, setOpen] = useState(false); + const triggerRef = useRef(null); + const reviewTriggerRef = useRef(null); + const activeTriggerRef = useRef(null); + const overlayRef = useRef(null); + const pointerInsideRef = useRef(false); + const closeTimeoutRef = useRef(null); + const suppressFocusOpenRef = useRef(null); + const overlayId = useId(); + const reviewHeadingId = useId(); + const [coords, setCoords] = useState({ top: 0, left: 0 }); + const normalizedStatus = status.trim().toLowerCase().replaceAll("-", "_").replaceAll(" ", "_"); + const effectiveCiPresentation = scope === "sprint" && normalizedStatus === "running" + ? null + : ciPresentation; + const presentation = useMemo(() => deriveWorkflowStatusPresentation({ + scope, + status, + review, + ciPresentation: effectiveCiPresentation, + }), [effectiveCiPresentation, review, scope, status]); + const MainIcon = presentation.tone === "qa_changes" + ? PencilLine + : presentation.state === "failed" + ? XCircle + : presentation.state === "in_progress" + ? Loader2 + : presentation.state === "successful" + ? Sparkles + : Circle; + const currentReviewState = review ? reviewState(review) : null; + const CurrentReviewIcon = currentReviewState ? REVIEW_META[currentReviewState].icon : null; + const reviewTriggerTone = currentReviewState === "changes_requested" + ? "qa_changes" + : currentReviewState === "failed" + ? "failed" + : currentReviewState === "running" + ? "active" + : "successful"; + const reviewDescriptionId = useId(); + const preferredPosition: Position = align === "right" ? "left" : "right"; + + const clearCloseTimeout = useCallback((): void => { + if (closeTimeoutRef.current !== null) { + window.clearTimeout(closeTimeoutRef.current); + closeTimeoutRef.current = null; + } + }, []); + const openOverlay = useCallback((): void => { + clearCloseTimeout(); + setOpen(true); + }, [clearCloseTimeout]); + const openFromTrigger = useCallback((trigger: HTMLButtonElement | null): void => { + if (trigger && suppressFocusOpenRef.current === trigger) { + suppressFocusOpenRef.current = null; + return; + } + activeTriggerRef.current = trigger; + openOverlay(); + }, [openOverlay]); + const closeOverlay = useCallback((restoreFocus = false): void => { + clearCloseTimeout(); + setOpen(false); + if (restoreFocus) { + const focusTarget = activeTriggerRef.current ?? triggerRef.current; + suppressFocusOpenRef.current = focusTarget; + focusTarget?.focus({ preventScroll: true }); + } + }, [clearCloseTimeout]); + const scheduleClose = useCallback((): void => { + clearCloseTimeout(); + closeTimeoutRef.current = window.setTimeout(() => { + const active = document.activeElement; + const focusedInside = Boolean(active && (triggerRef.current?.contains(active) || overlayRef.current?.contains(active))); + if (!pointerInsideRef.current && !focusedInside) setOpen(false); + closeTimeoutRef.current = null; + }, 120); + }, [clearCloseTimeout]); + const updatePosition = useCallback((): void => { + if (!triggerRef.current || !overlayRef.current) return; + setCoords(calculatePosition({ + triggerRect: triggerRef.current.getBoundingClientRect(), + contentRect: overlayRef.current.getBoundingClientRect(), + position: preferredPosition, + align: "center", + gap: 12, + padding: 12, + })); + }, [preferredPosition]); + + useLayoutEffect(() => { + if (open) updatePosition(); + }, [open, updatePosition]); + + useEffect(() => { + if (!open) return undefined; + const handleOutsidePointer = (event: Event): void => { + const target = event.target; + if (!(target instanceof Node)) return; + if (!triggerRef.current?.contains(target) && !overlayRef.current?.contains(target)) closeOverlay(); + }; + const handleKeyDown = (event: KeyboardEvent): void => { + if (event.key === "Escape") { + event.preventDefault(); + closeOverlay(true); + } + }; + document.addEventListener("mousedown", handleOutsidePointer); + document.addEventListener("touchstart", handleOutsidePointer, { passive: true }); + document.addEventListener("keydown", handleKeyDown); + window.addEventListener("resize", updatePosition); + window.addEventListener("scroll", updatePosition, { capture: true, passive: true }); + return () => { + document.removeEventListener("mousedown", handleOutsidePointer); + document.removeEventListener("touchstart", handleOutsidePointer); + document.removeEventListener("keydown", handleKeyDown); + window.removeEventListener("resize", updatePosition); + window.removeEventListener("scroll", updatePosition, { capture: true }); + }; + }, [closeOverlay, open, updatePosition]); + + useEffect(() => () => clearCloseTimeout(), [clearCloseTimeout]); + + const handleBlur = (relatedTarget: EventTarget | null): void => { + if (!(relatedTarget instanceof Node) || (!triggerRef.current?.contains(relatedTarget) && !overlayRef.current?.contains(relatedTarget))) scheduleClose(); + }; + const ciAccessibleLabel = effectiveCiPresentation?.accessibleLabel ?? presentation.accessibleLabel; + + return ( + { + pointerInsideRef.current = true; + openOverlay(); + }} + onMouseLeave={() => { + pointerInsideRef.current = false; + scheduleClose(); + }} + > + + {review && ( + <> + {currentReviewState === "running" && QA review running} + + {currentReviewState === "changes_requested" ? "QA changes requested" : REVIEW_META[currentReviewState!].label}. {open ? "Details are open." : "Activate to show review details."} + + + + )} + + {open && createPortal( +
    { + pointerInsideRef.current = true; + openOverlay(); + }} + onMouseLeave={() => { + pointerInsideRef.current = false; + scheduleClose(); + }} + onFocusCapture={openOverlay} + onBlurCapture={(event) => handleBlur(event.relatedTarget)} + > +
    +
    + + Delivery flow + {presentation.label} + + + + +
    +
      + {presentation.stages.map((stage, index) => )} +
    +
    + {review && ( + <> + + + + +
    + +
    + + )} +
    , + document.body, + )} +
    + ); +}; diff --git a/dashboard/src/v2/components/ui/workflow-status-badge.css b/dashboard/src/v2/components/ui/workflow-status-badge.css new file mode 100644 index 0000000000..eb4a933172 --- /dev/null +++ b/dashboard/src/v2/components/ui/workflow-status-badge.css @@ -0,0 +1,38 @@ +@keyframes workflow-dot-flow { + 0% { + background-position: 50% -8px; + } + 100% { + background-position: 50% 8px; + } +} + +@keyframes workflow-chevron-breathe { + 0%, 100% { + transform: translateX(-2px); + opacity: 0.6; + } + 50% { + transform: translateX(2px); + opacity: 1; + } +} + +.workflow-status__connector { + background-image: radial-gradient(circle, currentColor 1.2px, transparent 1.5px); + background-position: 50% -8px; + background-repeat: repeat-y; + background-size: 4px 8px; + animation: workflow-dot-flow 850ms linear infinite; +} + +.workflow-status__chevron { + animation: workflow-chevron-breathe 1.35s ease-in-out infinite; +} + +@media (prefers-reduced-motion: reduce) { + .workflow-status__connector, + .workflow-status__chevron { + animation: none; + } +} diff --git a/dashboard/src/v2/lib/workflow-status-presentation.ts b/dashboard/src/v2/lib/workflow-status-presentation.ts new file mode 100644 index 0000000000..68f76e14b2 --- /dev/null +++ b/dashboard/src/v2/lib/workflow-status-presentation.ts @@ -0,0 +1,216 @@ +import type { SprintReviewSummary } from "../types.js"; +import type { + CiStatusPresentation, + CiWorkflowState, + CiWorkflowStep, +} from "./ci-status-presentation.js"; + +export type WorkflowStageId = "coding" | "pull_request" | "qa" | "checks" | "merge" | "completion"; + +export interface WorkflowStage { + id: WorkflowStageId; + label: string; + state: CiWorkflowState; + statusLabel: string; +} + +export interface WorkflowStatusPresentation { + scope: "task" | "sprint"; + state: CiWorkflowState; + tone: "pending" | "active" | "successful" | "failed" | "qa_changes"; + label: string; + accessibleLabel: string; + stages: [WorkflowStage, WorkflowStage, WorkflowStage, WorkflowStage, WorkflowStage, WorkflowStage]; +} + +export interface WorkflowStatusPresentationInput { + scope: "task" | "sprint"; + status: string; + review?: SprintReviewSummary | null; + ciPresentation?: CiStatusPresentation | null; +} + +function normalizeStatus(value: string): string { + return value.trim().toLowerCase().replaceAll("-", "_").replaceAll(" ", "_"); +} + +function fallbackCiStep(id: CiWorkflowStep["id"], workflowCompleted: boolean): CiWorkflowStep { + const labels = workflowCompleted + ? { + pull_request: ["Pull request", "Pull request ready"], + checks: ["CI checks", "Checks passed"], + merge: ["Merge", "Merged"], + } as const + : { + pull_request: ["Pull request", "Waiting for pull request"], + checks: ["CI checks", "Checks pending"], + merge: ["Merge", "Merge pending"], + } as const; + return { + id, + label: labels[id][0], + state: workflowCompleted ? "successful" : "pending", + statusLabel: labels[id][1], + }; +} + +function resolveCiStep( + id: CiWorkflowStep["id"], + observed: CiWorkflowStep | undefined, + workflowCompleted: boolean, +): CiWorkflowStep { + if (workflowCompleted && observed?.state !== "successful") { + return fallbackCiStep(id, true); + } + return observed ?? fallbackCiStep(id, workflowCompleted); +} + +function deriveReviewStage( + review: SprintReviewSummary | null | undefined, + status: string, + ciSteps: readonly CiWorkflowStep[], +): WorkflowStage { + if (review) { + const reviewStatus = normalizeStatus(review.status); + const outcome = normalizeStatus(review.outcome ?? ""); + if (reviewStatus === "running" || reviewStatus === "in_progress" || reviewStatus === "pending") { + return { id: "qa", label: "QA", state: "in_progress", statusLabel: "Review in progress" }; + } + if (outcome === "changes_requested") { + return { id: "qa", label: "QA", state: "failed", statusLabel: "Changes requested" }; + } + if (["failed", "errored", "cancelled"].includes(reviewStatus) || ["failed", "rejected"].includes(outcome)) { + return { id: "qa", label: "QA", state: "failed", statusLabel: "Review failed" }; + } + if (["pass", "passed", "approved", "success", "successful"].includes(outcome) || reviewStatus === "completed") { + return { id: "qa", label: "QA", state: "successful", statusLabel: "QA passed" }; + } + } + + const downstreamStarted = ciSteps.slice(1).some((step) => step.state !== "pending"); + if (downstreamStarted) { + return { id: "qa", label: "QA", state: "successful", statusLabel: "QA cleared" }; + } + if (status === "completed") { + return { id: "qa", label: "QA", state: "successful", statusLabel: "No review required" }; + } + if (status === "qa_pending") { + return { id: "qa", label: "QA", state: "in_progress", statusLabel: "Review pending" }; + } + return { id: "qa", label: "QA", state: "pending", statusLabel: "QA pending" }; +} + +function deriveCodingStage( + status: string, + review: SprintReviewSummary | null | undefined, + ciSteps: readonly CiWorkflowStep[], +): WorkflowStage { + const hasPostCodingEvidence = Boolean(review) || ciSteps.some((step) => step.state !== "pending"); + if (["failed", "cancelled"].includes(status)) { + return { id: "coding", label: "Coding", state: "failed", statusLabel: status === "cancelled" ? "Coding cancelled" : "Coding failed" }; + } + if (["completed", "coding_completed", "qa_review_failed"].includes(status) || hasPostCodingEvidence) { + return { id: "coding", label: "Coding", state: "successful", statusLabel: "Coding complete" }; + } + if (["in_progress", "running", "queued", "preparing", "quota", "provider_cap"].includes(status)) { + const statusLabel = status === "queued" + ? "Coding queued" + : status === "quota" + ? "Quota wait" + : status === "provider_cap" + ? "Provider capacity wait" + : status === "preparing" + ? "Preparing workspace" + : "Coding in progress"; + return { id: "coding", label: "Coding", state: "in_progress", statusLabel }; + } + if (status === "blocked") return { id: "coding", label: "Coding", state: "pending", statusLabel: "Coding blocked" }; + if (status === "paused") { + return { id: "coding", label: "Coding", state: "in_progress", statusLabel: "Coding paused" }; + } + return { id: "coding", label: "Coding", state: "pending", statusLabel: "Waiting to start" }; +} + +function deriveCompletionStage(status: string): WorkflowStage { + if (status === "completed") { + return { id: "completion", label: "Completion", state: "successful", statusLabel: "Workflow complete" }; + } + if (["failed", "cancelled"].includes(status)) { + return { id: "completion", label: "Completion", state: "failed", statusLabel: status === "cancelled" ? "Workflow cancelled" : "Workflow failed" }; + } + return { id: "completion", label: "Completion", state: "pending", statusLabel: "Completion pending" }; +} + +function displayLabel(stages: readonly WorkflowStage[]): string { + const failed = stages.find((stage) => stage.state === "failed"); + if (failed) { + if (failed.id === "qa") return failed.statusLabel === "Changes requested" ? "QA changes" : "QA failed"; + if (failed.id === "checks") return "CI failed"; + if (failed.id === "merge") return failed.statusLabel === "Merge conflict" ? "Merge conflict" : "Merge failed"; + return failed.statusLabel; + } + const active = stages.find((stage) => stage.state === "in_progress"); + if (active) { + if (active.id === "checks") return "CI running"; + if (active.id === "pull_request") return "Creating PR"; + if (active.id === "qa") return "QA running"; + if (active.id === "merge") return "Merge running"; + return active.statusLabel; + } + const completion = stages[stages.length - 1]; + if (completion.state === "successful") return "Completed"; + const furthestSuccessful = [...stages].reverse().find((stage) => stage.state === "successful"); + if (!furthestSuccessful) return "Coding pending"; + if (furthestSuccessful.id === "merge") return "Merged"; + if (furthestSuccessful.id === "checks") return "CI passed"; + if (furthestSuccessful.id === "qa") return "QA passed"; + if (furthestSuccessful.id === "pull_request") return "PR ready"; + return "PR pending"; +} + +export function deriveWorkflowStatusPresentation( + input: WorkflowStatusPresentationInput, +): WorkflowStatusPresentation { + const status = normalizeStatus(input.status); + const workflowCompleted = status === "completed"; + const suppressRunningSprintTaskGates = input.scope === "sprint" && status === "running"; + const ciPresentation = suppressRunningSprintTaskGates ? null : input.ciPresentation; + const stageReview = suppressRunningSprintTaskGates ? null : input.review; + const pullRequest = resolveCiStep("pull_request", ciPresentation?.steps[0], workflowCompleted); + const checks = resolveCiStep("checks", ciPresentation?.steps[1], workflowCompleted); + const merge = resolveCiStep("merge", ciPresentation?.steps[2], workflowCompleted); + const ciSteps = [pullRequest, checks, merge]; + const stages = [ + deriveCodingStage(status, stageReview, ciSteps), + { ...pullRequest, id: "pull_request" as const }, + deriveReviewStage(stageReview, status, ciSteps), + { ...checks, id: "checks" as const, label: "CI" }, + { ...merge, id: "merge" as const }, + deriveCompletionStage(status), + ] as WorkflowStatusPresentation["stages"]; + const failed = stages.some((stage) => stage.state === "failed"); + const active = stages.some((stage) => stage.state === "in_progress"); + const state: CiWorkflowState = failed + ? "failed" + : active + ? "in_progress" + : stages[5].state === "successful" + ? "successful" + : "pending"; + const label = displayLabel(stages); + const qaChangesRequested = stages.some((stage) => ( + stage.id === "qa" && stage.state === "failed" && stage.statusLabel === "Changes requested" + )); + return { + scope: input.scope, + state, + tone: qaChangesRequested + ? "qa_changes" + : state === "in_progress" + ? "active" + : state, + label, + accessibleLabel: `${label}. ${stages.map((stage) => `${stage.label}: ${stage.statusLabel}`).join(". ")}.`, + stages, + }; +} diff --git a/docs-web/architecture/card-ci-status-projection.md b/docs-web/architecture/card-ci-status-projection.md index de7cac6413..cb56b3137c 100644 --- a/docs-web/architecture/card-ci-status-projection.md +++ b/docs-web/architecture/card-ci-status-projection.md @@ -22,7 +22,9 @@ Sprint cards aggregate their task statuses and latest main-merge gate with failu Review gating remains separate from CI failure presentation. A main-merge `review_blocked` event becomes `failed` only when it also carries failed-check evidence or matching CI repair attention is active. -The dashboard expands that evidence into the same three labelled workflow steps on Task, Live, Sprint gallery, and Sprint ledger cards: **Pull request**, **Checks**, and **Merge**. Icons and color reinforce visible and accessible text such as **Pull request ready**, **Checks running**, **Checks failed**, and **Checks passed**. A newer successful observation replaces stale failure, while active matching CI attention restores the failure state. +The dashboard combines that evidence with lifecycle and latest-review state in one durable six-stage delivery flow on Task, Live, Sprint gallery, Sprint ledger, and Overview cards: **Coding**, **Pull request**, **QA**, **CI**, **Merge**, and **Completion**. CI evidence enriches the middle stages but is optional, so a Sprints refresh without historical gate events cannot unmount or flash away the badge. A durably completed workflow settles missing historical PR, checks, and merge projections as successful rather than showing contradictory pending stages. A newer successful observation replaces stale failure, while active matching CI attention restores the failure state. + +The bright interactive badge opens a circular rail with motion-safe animated dotted connectors. When a review exists, an animated chevron joins the workflow card to an adjacent review card. Requested changes use the blue pencil and **QA edits** treatment even when failed-check evidence also exists; red is reserved for actual provider/runtime and workflow failures. Live replaces its separate task lifecycle and QA badges, while Task, Sprint, and Overview surfaces keep their surrounding lifecycle context and replace the standalone QA/CI disclosures. While a sprint is running, the sprint badge stays on Coding and ignores child-task gate aggregation; each task badge continues showing its own PR, QA, CI, and Merge transitions. The deterministic dashboard integration suite replays these outcomes across all four card renderings, including keyboard-only QA details, collapsed follow-up specifications, Escape focus restoration, unrelated-event isolation, and unchanged snapshot replay. It mocks runtime boundaries and does not invoke Docker, provider CLIs, Git hosting, or a live database. diff --git a/docs-web/content/docs/architecture-card-ci-status-projection.mdx b/docs-web/content/docs/architecture-card-ci-status-projection.mdx index 96da98eba7..f3fdda121d 100644 --- a/docs-web/content/docs/architecture-card-ci-status-projection.mdx +++ b/docs-web/content/docs/architecture-card-ci-status-projection.mdx @@ -22,7 +22,9 @@ Sprint cards aggregate their task statuses and latest main-merge gate with failu Review gating remains separate from CI failure presentation. A main-merge `review_blocked` event becomes `failed` only when it also carries failed-check evidence or matching CI repair attention is active. -The dashboard expands that evidence into the same three labelled workflow steps on Task, Live, Sprint gallery, and Sprint ledger cards: **Pull request**, **Checks**, and **Merge**. Icons and color reinforce visible and accessible text such as **Pull request ready**, **Checks running**, **Checks failed**, and **Checks passed**. A newer successful observation replaces stale failure, while active matching CI attention restores the failure state. +The dashboard combines that evidence with lifecycle and latest-review state in one durable six-stage delivery flow on Task, Live, Sprint gallery, Sprint ledger, and Overview cards: **Coding**, **Pull request**, **QA**, **CI**, **Merge**, and **Completion**. CI evidence enriches the middle stages but is optional, so a Sprints refresh without historical gate events cannot unmount or flash away the badge. A durably completed workflow settles missing historical PR, checks, and merge projections as successful rather than showing contradictory pending stages. A newer successful observation replaces stale failure, while active matching CI attention restores the failure state. + +The bright interactive badge opens a circular rail with motion-safe animated dotted connectors. When a review exists, an animated chevron joins the workflow card to an adjacent review card. Requested changes use the blue pencil and **QA edits** treatment even when failed-check evidence also exists; red is reserved for actual provider/runtime and workflow failures. Live replaces its separate task lifecycle and QA badges, while Task, Sprint, and Overview surfaces keep their surrounding lifecycle context and replace the standalone QA/CI disclosures. While a sprint is running, the sprint badge stays on Coding and ignores child-task gate aggregation; each task badge continues showing its own PR, QA, CI, and Merge transitions. The deterministic dashboard integration suite replays these outcomes across all four card renderings, including keyboard-only QA details, collapsed follow-up specifications, Escape focus restoration, unrelated-event isolation, and unchanged snapshot replay. It mocks runtime boundaries and does not invoke Docker, provider CLIs, Git hosting, or a live database. diff --git a/docs-web/content/docs/user-dashboard-live-session.mdx b/docs-web/content/docs/user-dashboard-live-session.mdx index b7e73bd82b..b425369ec1 100644 --- a/docs-web/content/docs/user-dashboard-live-session.mdx +++ b/docs-web/content/docs/user-dashboard-live-session.mdx @@ -36,9 +36,9 @@ If the WebSocket disconnects (network blip, page sleep), the client automaticall Live task cards can show a compact 5-star self-reflection badge when the task snapshot includes `selfReflectionRating`. The badge uses the task's overall rating for the visible score, and hover or keyboard focus opens a viewport-positioned panel with the individual section ratings and any notes the worker recorded. Live tasks without a captured rating do not show a placeholder badge. -## QA review states and follow-up specifications +## Delivery workflow and QA review details -Live task cards use the same QA review badge as Tasks and Sprints. The badge represents the latest persisted review summary independently of the current runtime phase, so a requested-change verdict remains inspectable while the follow-up work runs and after Live reconnects. +Live task cards use one bright delivery workflow badge in place of the former task lifecycle, QA, and CI badges. The badge remains mounted throughout Coding → Pull request → QA → CI → Merge → Completion. Lifecycle and review state provide the durable base; persisted CI evidence enriches the middle stages when available. | Presentation | Meaning | | --- | --- | @@ -47,23 +47,26 @@ Live task cards use the same QA review badge as Tasks and Sprints. The badge rep | Blue pencil, **QA edits** / **QA changes requested** | QA completed successfully and requested changes. This is an actionable review outcome, not a provider failure. | | Red X, **QA failed** | The QA provider run failed, errored, or was cancelled before returning a usable verdict. It does not mean QA requested code changes. | -Hovering the badge, focusing it with the keyboard, or activating it opens an accessible, viewport-positioned review card. The card is named by its review heading and can include the outcome, summary, findings, fix instructions, target task key, reviewer, reviewed time, and generated follow-up tasks. Focus may move between the badge, card, and disclosure buttons without closing it. `Escape` closes the card and restores focus to the badge; moving the pointer away closes it after a short grace period when focus is not inside, and a mouse or touch press outside dismisses it. On touch devices, tap the badge to open it and tap outside to dismiss it. +Hovering, focusing, or activating the badge opens an opaque, viewport-positioned workflow card, so content beneath it cannot bleed through. Six circles on the left are joined by animated dotted connectors to make the delivery sequence immediately scannable. When review data exists, an animated chevron links the workflow card to an adjacent opaque QA review card containing the outcome, summary, findings, fix instructions, target task key, reviewer, reviewed time, and generated follow-up tasks. `Escape` restores focus to the exact workflow or QA-chevron trigger that opened the surface. Reduced motion stops connector and chevron animation without removing state. Generated follow-up task specifications are collapsed initially, so long prompts do not dominate the review. Each **Follow-up task N** button exposes `aria-expanded` and can be toggled with the keyboard or touch. Expansion reveals the generated title, description, priority, dependency task keys (or **None**), and full Markdown prompt in a bounded scrolling area. The card uses one column on constrained screens, may split summary and findings on wider screens, clamps to the viewport, and scrolls vertically when needed. Reduced motion removes spinner, pulse, rotation, and transition movement without removing labels, borders, focus rings, expanded content, or state semantics. -## Pull request, checks, and merge workflow +## Six-stage delivery flow -The CI badge summarizes a three-step workflow shared by Sprints, Tasks, and Live: +The workflow badge summarizes the same six stages on Sprints, Tasks, Overview, and Live: -1. **Pull request** — waiting for a PR, missing a required PR, or PR ready. -2. **Checks** — pending, running, passed, or failed checks. -3. **Merge** — waiting for checks, QA, or review; checking mergeability; ready to merge; merging; merged; not required; conflict; or failed merge attempt. +1. **Coding** — waiting, queued, preparing, active, quota/capacity wait, paused, complete, or failed. +2. **Pull request** — waiting for a PR, missing a required PR, creating, or ready. +3. **QA** — pending, reviewing, passed, blue **QA edits**, or provider/runtime failure. +4. **CI** — checks pending, running, passed, or failed. +5. **Merge** — waiting, checking mergeability, ready, merging, merged, not required, conflict, or failed attempt. +6. **Completion** — waiting, complete, failed, or cancelled. The four first-class workflow states are `pending`, `in_progress`, `successful`, and `failed`. Pending uses a neutral clock, `in_progress` is presented as running with the signal-colored progress treatment, `successful` uses a green check, and `failed` uses a red X. Failed wins over in progress, in progress wins over pending, and pending wins over successful when the overall badge is derived from the three steps. -The red X is reserved for an actual failed workflow step: failed CI checks, a merge conflict, or a failed merge attempt. A review blocker is not a CI failure: checks remain passed and Merge reads **Waiting for review** in a pending state. A merge conflict fails the Merge step and is labelled **Merge conflict**, which keeps it distinct from **CI failed** at Checks. QA provider failure is shown by the separate QA badge and does not become a CI failure. Activate the CI badge to inspect all three step labels and states; `Escape` closes the details and returns focus to the badge. +The red X is reserved for an actual provider/runtime or workflow failure. A requested-change verdict is blue, not red. A review blocker is not a CI failure: CI remains passed and Merge reads **Waiting for review**. A merge conflict belongs to Merge and remains distinct from **CI failed**. -These badges do not poll per card. Task feature-PR gates are persisted as `ci_gate_status` task-run events, and Live narrows them to the selected sprint and the latest dispatch's sprint run before choosing the newest matching event by creation time and then event ID. Unresolved CI repair attention is combined while its item is `open` or `claimed`; persisted task merge metadata is used only as durable fallback evidence when no matching event is available. +The badge does not poll per card. Task feature-PR gates are persisted as `ci_gate_status` task-run events, and Live narrows them to the selected sprint and latest dispatch's sprint run before choosing the newest matching event by creation time and event ID. Unresolved CI repair attention is combined while `open` or `claimed`; persisted task merge metadata is durable fallback evidence. Because the evidence is persisted and rehydrated into the initial Live snapshot, server restarts and browser reconnects reconstruct the same state before realtime updates continue; cards do not need independent recovery timers. A newer recognized settled gate event supersedes an older failed or waiting event for the same task and sprint run, and resolved or dismissed attention no longer forces failure. diff --git a/docs-web/content/docs/user-dashboard-overview.mdx b/docs-web/content/docs/user-dashboard-overview.mdx index 5a6b9463fb..4f26d756fc 100644 --- a/docs-web/content/docs/user-dashboard-overview.mdx +++ b/docs-web/content/docs/user-dashboard-overview.mdx @@ -77,6 +77,8 @@ The Overview telemetry rail combines cross-project runtime health with selected- The Overview queue follows the same selected sprint scope as the Live page. If a sprint is selected in the top navigation, the queue shows only the active attention items returned by the selected-sprint live snapshot; unrelated sprint blockers are not reconstructed in the browser. Overview renders the queue read-only, so claim, resolve, and dismiss actions remain on the Live page. +Overview active-stream task rows use the shared bright delivery workflow badge instead of a standalone QA badge. Open it to inspect Coding → Pull request → QA → CI → Merge → Completion. When a review exists, the animated chevron reveals the adjacent QA review card; requested edits stay blue, and reduced motion keeps every state visible while stopping connector and chevron animation. + ## Real-time data The dashboard maintains a live connection to the server using a custom WebSocket protocol via `GET /api/realtime` (e.g., `ws://localhost:4444/api/realtime` for local HTTP dashboards, and `wss:///api/realtime` for HTTPS deployments). On the server side, `DashboardRealtimeService` in `src/services/dashboard-realtime-service.ts` coordinates events, and the websocket upgrade/transport is handled in `src/server/dashboard-realtime-websocket-server.ts`. The connection: diff --git a/docs-web/content/docs/user-dashboard-sprints.mdx b/docs-web/content/docs/user-dashboard-sprints.mdx index 0fa52920e0..7553d49610 100644 --- a/docs-web/content/docs/user-dashboard-sprints.mdx +++ b/docs-web/content/docs/user-dashboard-sprints.mdx @@ -17,9 +17,9 @@ Failed execution and eligible human intervention receive a red border around the When reduced motion is enabled, the exclamation stops pulsing and the waiting cue stops bouncing; the red border, indicator, visible context, and semantic label remain. Worker- and system-owned transient pauses are not shown as requests for human action. Normal status, progress, review badges, links, and controls also remain available in both attention states. -## QA review states and follow-up specifications +## Delivery workflow and QA review details -Sprint cells and ledger rows use the same QA review badge as Tasks and Live. The badge represents the latest persisted review summary independently of the sprint lifecycle status, so a running or paused sprint can still retain an earlier requested-change verdict. +Sprint cells and ledger rows keep their sprint lifecycle status and use one bright delivery workflow badge in place of the standalone QA and CI badges. The workflow badge remains mounted even when a refreshed execution snapshot has no historical CI events, so it no longer flashes and disappears. While the sprint is running, its badge stays on Coding instead of aggregating and flipping between child-task PR, CI, and Merge activity; individual cards on Tasks and Live show those transitions. Once the sprint completes, the full PR, CI, Merge, and Completion rail settles successfully, including when older gate events are no longer present in the current snapshot. | Presentation | Meaning | | --- | --- | @@ -28,21 +28,24 @@ Sprint cells and ledger rows use the same QA review badge as Tasks and Live. The | Blue pencil, **QA edits** / **QA changes requested** | QA completed successfully and requested changes. This is an actionable review outcome, not a provider failure. | | Red X, **QA failed** | The QA provider run failed, errored, or was cancelled before returning a usable verdict. It does not mean QA requested code changes. | -Hovering the badge, focusing it with the keyboard, or activating it opens an accessible, viewport-positioned review card. The card is named by its review heading and can include the outcome, summary, findings, fix instructions, target task key, reviewer, reviewed time, and generated follow-up tasks. Focus may move between the badge, card, and disclosure buttons without closing it. `Escape` closes the card and restores focus to the badge; moving the pointer away closes it after a short grace period when focus is not inside, and a mouse or touch press outside dismisses it. On touch devices, tap the badge to open it and tap outside to dismiss it. +Hovering, focusing, or activating the badge opens an opaque viewport-positioned workflow card with Coding → Pull request → QA → CI → Merge → Completion, preventing sprint content from bleeding through. Six circles are joined by motion-safe animated dotted connectors. When review data exists, an animated chevron links an adjacent opaque QA review card with the outcome, summary, findings, fix instructions, reviewer metadata, and collapsed follow-up specifications. `Escape` restores focus to the exact trigger that opened it. Generated follow-up task specifications are collapsed initially, so long prompts do not dominate the review. Each **Follow-up task N** button exposes `aria-expanded` and can be toggled with the keyboard or touch. Expansion reveals the generated title, description, priority, dependency task keys (or **None**), and full Markdown prompt in a bounded scrolling area. The card uses one column on constrained screens, may split summary and findings on wider screens, clamps to the viewport, and scrolls vertically when needed. Reduced motion removes spinner, pulse, rotation, and transition movement without removing labels, borders, focus rings, expanded content, or state semantics. -## Pull request, checks, and merge workflow +## Six-stage delivery flow -The CI badge summarizes a three-step workflow shared by Sprints, Tasks, and Live: +The workflow badge summarizes six stages shared by Sprints, Tasks, Overview, and Live: -1. **Pull request** — waiting for a PR, missing a required PR, or PR ready. -2. **Checks** — pending, running, passed, or failed checks. -3. **Merge** — waiting for checks, QA, or review; checking mergeability; ready to merge; merging; merged; not required; conflict; or failed merge attempt. +1. **Coding** — waiting, active, paused, complete, or failed. +2. **Pull request** — waiting, creating, missing, or ready. +3. **QA** — pending, reviewing, passed, blue **QA edits**, or provider/runtime failure. +4. **CI** — pending, running, passed, or failed checks. +5. **Merge** — waiting, ready, merging, merged, not required, conflict, or failed attempt. +6. **Completion** — waiting, complete, failed, or cancelled. The four first-class workflow states are `pending`, `in_progress`, `successful`, and `failed`. Pending uses a neutral clock, `in_progress` is presented as running with the signal-colored progress treatment, `successful` uses a green check, and `failed` uses a red X. A sprint aggregates the newest state for each task workflow plus the final feature-to-default-branch merge workflow. Failed wins over in progress, in progress wins over pending, and pending wins over successful, both for each step and for the overall badge. -The red X is reserved for an actual failed workflow step: failed CI checks, a merge conflict, or a failed merge attempt. A review blocker is not a CI failure: checks remain passed and Merge reads **Waiting for review** in a pending state. A merge conflict fails the Merge step and is labelled **Merge conflict**, which keeps it distinct from **CI failed** at Checks. QA provider failure is shown by the separate QA badge and does not become a CI failure. Activate the CI badge to inspect all three step labels and states; `Escape` closes the details and returns focus to the badge. +The red X is reserved for an actual provider/runtime or workflow failure. Requested QA edits use the bright blue pencil treatment even when failed-check evidence is also present. A review blocker remains pending, and a merge conflict belongs to Merge rather than CI. These badges do not poll per card. Task feature-PR gates are persisted as `ci_gate_status` task-run events, final feature-to-default-branch gates as `main_merge_gate_status` sprint-run events, and unresolved CI repair attention remains active while its item is `open` or `claimed`. For each task or main-merge entity, the projection selects the newest matching event by creation time and then event ID; the sprint presentation aggregates those latest entity states. Persisted task merge metadata is used only as durable fallback evidence when no matching event is available. diff --git a/docs-web/content/docs/user-dashboard-tasks.mdx b/docs-web/content/docs/user-dashboard-tasks.mdx index 39f74c4d16..d899309f99 100644 --- a/docs-web/content/docs/user-dashboard-tasks.mdx +++ b/docs-web/content/docs/user-dashboard-tasks.mdx @@ -31,13 +31,13 @@ Each lane is a named region whose accessible name includes its count, such as ** Reduced-motion mode removes board, card, selector, progress, menu, and drop-target movement and disables pointer dragging. Static labels, borders, focus rings, progress values, lane counts, empty states, action availability reasons, and drag-disabled guidance remain available. -Each task card shows its task identifier, title, status, and priority first. Compact metadata can then show a non-default executor or worker agent, session state and identifier, QA and CI state, dependency blocker count, optimistic saving state, source and assignee, runtime duration, pull-request state, creation or live-start time, and an optional self-reflection rating. The footer always keeps the task-labelled **Actions** trigger visible. Dragging a card to another lane changes its status when that transition is available. +Each task card shows its task identifier, title, status, and priority first. Compact metadata can then show a non-default executor or worker agent, session state and identifier, the unified delivery workflow, dependency blocker count, optimistic saving state, source and assignee, runtime duration, pull-request state, creation or live-start time, and an optional self-reflection rating. The footer always keeps the task-labelled **Actions** trigger visible. Dragging a card to another lane changes its status when that transition is available. When a worker reports a task-run self-reflection rating, the shared rating badge appears in the compact card metadata near the task id, status, and priority. It shows the overall `overallRating` as a numeric score with a compact 5-star meter. Hovering the badge, or focusing it with the keyboard, opens a viewport-positioned details panel with each section from `sections`: the section label, matching stars, numeric rating, and any note captured by the worker. Tasks without a captured rating, including older tasks that never produced one, do not render an empty badge slot. -## QA review states and follow-up specifications +## Delivery workflow and QA review details -Task cards use the same QA review badge as Sprints and Live. The badge represents the latest persisted review summary independently of the task lane or phase, so requested-change details remain available while a follow-up run is active and after a reconnect. +Task cards use the shared bright delivery workflow badge in place of the standalone QA and CI badges. The task lifecycle label remains visible beside it, while the badge itself always exposes Coding → Pull request → QA → CI → Merge → Completion. Lifecycle and review state keep the badge durable when CI evidence is absent or refreshing. | Presentation | Meaning | | --- | --- | @@ -46,21 +46,24 @@ Task cards use the same QA review badge as Sprints and Live. The badge represent | Blue pencil, **QA edits** / **QA changes requested** | QA completed successfully and requested changes. This is an actionable review outcome, not a provider failure. | | Red X, **QA failed** | The QA provider run failed, errored, or was cancelled before returning a usable verdict. It does not mean QA requested code changes. | -Hovering the badge, focusing it with the keyboard, or activating it opens an accessible, viewport-positioned review card. The card is named by its review heading and can include the outcome, summary, findings, fix instructions, target task key, reviewer, reviewed time, and generated follow-up tasks. Focus may move between the badge, card, and disclosure buttons without closing it. `Escape` closes the card and restores focus to the badge; moving the pointer away closes it after a short grace period when focus is not inside, and a mouse or touch press outside dismisses it. On touch devices, tap the badge to open it and tap outside to dismiss it. +Hovering, focusing, or activating the badge opens an opaque, viewport-positioned workflow card. Six circles on the left are connected by motion-safe animated dots. When review data exists, an animated chevron reveals an adjacent opaque QA review card with the outcome, summary, findings, fix instructions, target task key, reviewer, reviewed time, and generated follow-up tasks. `Escape` closes the surface and restores focus to the exact workflow or QA-chevron trigger that opened it; outside pointer or touch input dismisses it. Generated follow-up task specifications are collapsed initially, so long prompts do not dominate the review. Each **Follow-up task N** button exposes `aria-expanded` and can be toggled with the keyboard or touch. Expansion reveals the generated title, description, priority, dependency task keys (or **None**), and full Markdown prompt in a bounded scrolling area. The card uses one column on constrained screens, may split summary and findings on wider screens, clamps to the viewport, and scrolls vertically when needed. Reduced motion removes spinner, pulse, rotation, and transition movement without removing labels, borders, focus rings, expanded content, or state semantics. -## Pull request, checks, and merge workflow +## Six-stage delivery flow -The CI badge summarizes a three-step workflow shared by Sprints, Tasks, and Live: +The workflow badge summarizes six stages shared by Sprints, Tasks, Overview, and Live: -1. **Pull request** — waiting for a PR, missing a required PR, or PR ready. -2. **Checks** — pending, running, passed, or failed checks. -3. **Merge** — waiting for checks, QA, or review; checking mergeability; ready to merge; merging; merged; not required; conflict; or failed merge attempt. +1. **Coding** — waiting, queued, preparing, active, quota/capacity wait, paused, complete, or failed. +2. **Pull request** — waiting for a PR, missing a required PR, creating, or ready. +3. **QA** — pending, reviewing, passed, blue **QA edits**, or provider/runtime failure. +4. **CI** — checks pending, running, passed, or failed. +5. **Merge** — waiting, checking mergeability, ready, merging, merged, not required, conflict, or failed attempt. +6. **Completion** — waiting, complete, failed, or cancelled. The four first-class workflow states are `pending`, `in_progress`, `successful`, and `failed`. Pending uses a neutral clock, `in_progress` is presented as running with the signal-colored progress treatment, `successful` uses a green check, and `failed` uses a red X. Failed wins over in progress, in progress wins over pending, and pending wins over successful when the overall badge is derived from the three steps. -The red X is reserved for an actual failed workflow step: failed CI checks, a merge conflict, or a failed merge attempt. A review blocker is not a CI failure: checks remain passed and Merge reads **Waiting for review** in a pending state. A merge conflict fails the Merge step and is labelled **Merge conflict**, which keeps it distinct from **CI failed** at Checks. QA provider failure is shown by the separate QA badge and does not become a CI failure. Activate the CI badge to inspect all three step labels and states; `Escape` closes the details and returns focus to the badge. +The red X is reserved for an actual provider/runtime or workflow failure. Requested QA edits use the bright blue pencil treatment, not failure red. A review blocker is not a CI failure: CI remains passed and Merge reads **Waiting for review**. A merge conflict belongs to Merge and remains distinct from **CI failed**. These badges do not poll per card. Task feature-PR gates are persisted as `ci_gate_status` task-run events, and unresolved CI repair attention remains active while its item is `open` or `claimed`. The card projection selects the newest matching task event by creation time and then event ID, combines it with active attention, and uses persisted task merge metadata only as durable fallback evidence when no matching event is available. diff --git a/docs-web/user/dashboard/live-session.md b/docs-web/user/dashboard/live-session.md index b7e73bd82b..b425369ec1 100644 --- a/docs-web/user/dashboard/live-session.md +++ b/docs-web/user/dashboard/live-session.md @@ -36,9 +36,9 @@ If the WebSocket disconnects (network blip, page sleep), the client automaticall Live task cards can show a compact 5-star self-reflection badge when the task snapshot includes `selfReflectionRating`. The badge uses the task's overall rating for the visible score, and hover or keyboard focus opens a viewport-positioned panel with the individual section ratings and any notes the worker recorded. Live tasks without a captured rating do not show a placeholder badge. -## QA review states and follow-up specifications +## Delivery workflow and QA review details -Live task cards use the same QA review badge as Tasks and Sprints. The badge represents the latest persisted review summary independently of the current runtime phase, so a requested-change verdict remains inspectable while the follow-up work runs and after Live reconnects. +Live task cards use one bright delivery workflow badge in place of the former task lifecycle, QA, and CI badges. The badge remains mounted throughout Coding → Pull request → QA → CI → Merge → Completion. Lifecycle and review state provide the durable base; persisted CI evidence enriches the middle stages when available. | Presentation | Meaning | | --- | --- | @@ -47,23 +47,26 @@ Live task cards use the same QA review badge as Tasks and Sprints. The badge rep | Blue pencil, **QA edits** / **QA changes requested** | QA completed successfully and requested changes. This is an actionable review outcome, not a provider failure. | | Red X, **QA failed** | The QA provider run failed, errored, or was cancelled before returning a usable verdict. It does not mean QA requested code changes. | -Hovering the badge, focusing it with the keyboard, or activating it opens an accessible, viewport-positioned review card. The card is named by its review heading and can include the outcome, summary, findings, fix instructions, target task key, reviewer, reviewed time, and generated follow-up tasks. Focus may move between the badge, card, and disclosure buttons without closing it. `Escape` closes the card and restores focus to the badge; moving the pointer away closes it after a short grace period when focus is not inside, and a mouse or touch press outside dismisses it. On touch devices, tap the badge to open it and tap outside to dismiss it. +Hovering, focusing, or activating the badge opens an opaque, viewport-positioned workflow card, so content beneath it cannot bleed through. Six circles on the left are joined by animated dotted connectors to make the delivery sequence immediately scannable. When review data exists, an animated chevron links the workflow card to an adjacent opaque QA review card containing the outcome, summary, findings, fix instructions, target task key, reviewer, reviewed time, and generated follow-up tasks. `Escape` restores focus to the exact workflow or QA-chevron trigger that opened the surface. Reduced motion stops connector and chevron animation without removing state. Generated follow-up task specifications are collapsed initially, so long prompts do not dominate the review. Each **Follow-up task N** button exposes `aria-expanded` and can be toggled with the keyboard or touch. Expansion reveals the generated title, description, priority, dependency task keys (or **None**), and full Markdown prompt in a bounded scrolling area. The card uses one column on constrained screens, may split summary and findings on wider screens, clamps to the viewport, and scrolls vertically when needed. Reduced motion removes spinner, pulse, rotation, and transition movement without removing labels, borders, focus rings, expanded content, or state semantics. -## Pull request, checks, and merge workflow +## Six-stage delivery flow -The CI badge summarizes a three-step workflow shared by Sprints, Tasks, and Live: +The workflow badge summarizes the same six stages on Sprints, Tasks, Overview, and Live: -1. **Pull request** — waiting for a PR, missing a required PR, or PR ready. -2. **Checks** — pending, running, passed, or failed checks. -3. **Merge** — waiting for checks, QA, or review; checking mergeability; ready to merge; merging; merged; not required; conflict; or failed merge attempt. +1. **Coding** — waiting, queued, preparing, active, quota/capacity wait, paused, complete, or failed. +2. **Pull request** — waiting for a PR, missing a required PR, creating, or ready. +3. **QA** — pending, reviewing, passed, blue **QA edits**, or provider/runtime failure. +4. **CI** — checks pending, running, passed, or failed. +5. **Merge** — waiting, checking mergeability, ready, merging, merged, not required, conflict, or failed attempt. +6. **Completion** — waiting, complete, failed, or cancelled. The four first-class workflow states are `pending`, `in_progress`, `successful`, and `failed`. Pending uses a neutral clock, `in_progress` is presented as running with the signal-colored progress treatment, `successful` uses a green check, and `failed` uses a red X. Failed wins over in progress, in progress wins over pending, and pending wins over successful when the overall badge is derived from the three steps. -The red X is reserved for an actual failed workflow step: failed CI checks, a merge conflict, or a failed merge attempt. A review blocker is not a CI failure: checks remain passed and Merge reads **Waiting for review** in a pending state. A merge conflict fails the Merge step and is labelled **Merge conflict**, which keeps it distinct from **CI failed** at Checks. QA provider failure is shown by the separate QA badge and does not become a CI failure. Activate the CI badge to inspect all three step labels and states; `Escape` closes the details and returns focus to the badge. +The red X is reserved for an actual provider/runtime or workflow failure. A requested-change verdict is blue, not red. A review blocker is not a CI failure: CI remains passed and Merge reads **Waiting for review**. A merge conflict belongs to Merge and remains distinct from **CI failed**. -These badges do not poll per card. Task feature-PR gates are persisted as `ci_gate_status` task-run events, and Live narrows them to the selected sprint and the latest dispatch's sprint run before choosing the newest matching event by creation time and then event ID. Unresolved CI repair attention is combined while its item is `open` or `claimed`; persisted task merge metadata is used only as durable fallback evidence when no matching event is available. +The badge does not poll per card. Task feature-PR gates are persisted as `ci_gate_status` task-run events, and Live narrows them to the selected sprint and latest dispatch's sprint run before choosing the newest matching event by creation time and event ID. Unresolved CI repair attention is combined while `open` or `claimed`; persisted task merge metadata is durable fallback evidence. Because the evidence is persisted and rehydrated into the initial Live snapshot, server restarts and browser reconnects reconstruct the same state before realtime updates continue; cards do not need independent recovery timers. A newer recognized settled gate event supersedes an older failed or waiting event for the same task and sprint run, and resolved or dismissed attention no longer forces failure. diff --git a/docs-web/user/dashboard/overview.md b/docs-web/user/dashboard/overview.md index df4540ad8a..e31caf084a 100644 --- a/docs-web/user/dashboard/overview.md +++ b/docs-web/user/dashboard/overview.md @@ -77,6 +77,8 @@ The Overview telemetry rail combines cross-project runtime health with selected- The Overview queue follows the same selected sprint scope as the Live page. If a sprint is selected in the top navigation, the queue shows only the active attention items returned by the selected-sprint live snapshot; unrelated sprint blockers are not reconstructed in the browser. Overview renders the queue read-only, so claim, resolve, and dismiss actions remain on the Live page. +Overview active-stream task rows use the shared bright delivery workflow badge instead of a standalone QA badge. Open it to inspect Coding → Pull request → QA → CI → Merge → Completion. When a review exists, the animated chevron reveals the adjacent QA review card; requested edits stay blue, and reduced motion keeps every state visible while stopping connector and chevron animation. + ## Real-time data The dashboard maintains a live connection to the server using a custom WebSocket protocol via `GET /api/realtime` (e.g., `ws://localhost:4444/api/realtime` for local HTTP dashboards, and `wss:///api/realtime` for HTTPS deployments). On the server side, `DashboardRealtimeService` in `src/services/dashboard-realtime-service.ts` coordinates events, and the websocket upgrade/transport is handled in `src/server/dashboard-realtime-websocket-server.ts`. The connection: diff --git a/docs-web/user/dashboard/sprints.md b/docs-web/user/dashboard/sprints.md index 01ce57c12d..e364f1a4b6 100644 --- a/docs-web/user/dashboard/sprints.md +++ b/docs-web/user/dashboard/sprints.md @@ -17,9 +17,9 @@ Failed execution and eligible human intervention receive a red border around the When reduced motion is enabled, the exclamation stops pulsing and the waiting cue stops bouncing; the red border, indicator, visible context, and semantic label remain. Worker- and system-owned transient pauses are not shown as requests for human action. Normal status, progress, review badges, links, and controls also remain available in both attention states. -## QA review states and follow-up specifications +## Delivery workflow and QA review details -Sprint cells and ledger rows use the same QA review badge as Tasks and Live. The badge represents the latest persisted review summary independently of the sprint lifecycle status, so a running or paused sprint can still retain an earlier requested-change verdict. +Sprint cells and ledger rows keep their sprint lifecycle status and use one bright delivery workflow badge in place of the standalone QA and CI badges. The workflow badge remains mounted even when a refreshed execution snapshot has no historical CI events, so it no longer flashes and disappears. While the sprint is running, its badge stays on Coding instead of aggregating and flipping between child-task PR, CI, and Merge activity; individual cards on Tasks and Live show those transitions. Once the sprint completes, the full PR, CI, Merge, and Completion rail settles successfully, including when older gate events are no longer present in the current snapshot. | Presentation | Meaning | | --- | --- | @@ -28,21 +28,24 @@ Sprint cells and ledger rows use the same QA review badge as Tasks and Live. The | Blue pencil, **QA edits** / **QA changes requested** | QA completed successfully and requested changes. This is an actionable review outcome, not a provider failure. | | Red X, **QA failed** | The QA provider run failed, errored, or was cancelled before returning a usable verdict. It does not mean QA requested code changes. | -Hovering the badge, focusing it with the keyboard, or activating it opens an accessible, viewport-positioned review card. The card is named by its review heading and can include the outcome, summary, findings, fix instructions, target task key, reviewer, reviewed time, and generated follow-up tasks. Focus may move between the badge, card, and disclosure buttons without closing it. `Escape` closes the card and restores focus to the badge; moving the pointer away closes it after a short grace period when focus is not inside, and a mouse or touch press outside dismisses it. On touch devices, tap the badge to open it and tap outside to dismiss it. +Hovering, focusing, or activating the badge opens an opaque viewport-positioned workflow card with Coding → Pull request → QA → CI → Merge → Completion, preventing sprint content from bleeding through. Six circles are joined by motion-safe animated dotted connectors. When review data exists, an animated chevron links an adjacent opaque QA review card with the outcome, summary, findings, fix instructions, reviewer metadata, and collapsed follow-up specifications. `Escape` restores focus to the exact trigger that opened it. Generated follow-up task specifications are collapsed initially, so long prompts do not dominate the review. Each **Follow-up task N** button exposes `aria-expanded` and can be toggled with the keyboard or touch. Expansion reveals the generated title, description, priority, dependency task keys (or **None**), and full Markdown prompt in a bounded scrolling area. The card uses one column on constrained screens, may split summary and findings on wider screens, clamps to the viewport, and scrolls vertically when needed. Reduced motion removes spinner, pulse, rotation, and transition movement without removing labels, borders, focus rings, expanded content, or state semantics. -## Pull request, checks, and merge workflow +## Six-stage delivery flow -The CI badge summarizes a three-step workflow shared by Sprints, Tasks, and Live: +The workflow badge summarizes six stages shared by Sprints, Tasks, Overview, and Live: -1. **Pull request** — waiting for a PR, missing a required PR, or PR ready. -2. **Checks** — pending, running, passed, or failed checks. -3. **Merge** — waiting for checks, QA, or review; checking mergeability; ready to merge; merging; merged; not required; conflict; or failed merge attempt. +1. **Coding** — waiting, active, paused, complete, or failed. +2. **Pull request** — waiting, creating, missing, or ready. +3. **QA** — pending, reviewing, passed, blue **QA edits**, or provider/runtime failure. +4. **CI** — pending, running, passed, or failed checks. +5. **Merge** — waiting, ready, merging, merged, not required, conflict, or failed attempt. +6. **Completion** — waiting, complete, failed, or cancelled. The four first-class workflow states are `pending`, `in_progress`, `successful`, and `failed`. Pending uses a neutral clock, `in_progress` is presented as running with the signal-colored progress treatment, `successful` uses a green check, and `failed` uses a red X. A sprint aggregates the newest state for each task workflow plus the final feature-to-default-branch merge workflow. Failed wins over in progress, in progress wins over pending, and pending wins over successful, both for each step and for the overall badge. -The red X is reserved for an actual failed workflow step: failed CI checks, a merge conflict, or a failed merge attempt. A review blocker is not a CI failure: checks remain passed and Merge reads **Waiting for review** in a pending state. A merge conflict fails the Merge step and is labelled **Merge conflict**, which keeps it distinct from **CI failed** at Checks. QA provider failure is shown by the separate QA badge and does not become a CI failure. Activate the CI badge to inspect all three step labels and states; `Escape` closes the details and returns focus to the badge. +The red X is reserved for an actual provider/runtime or workflow failure. Requested QA edits use the bright blue pencil treatment even when failed-check evidence is also present. A review blocker remains pending, and a merge conflict belongs to Merge rather than CI. These badges do not poll per card. Task feature-PR gates are persisted as `ci_gate_status` task-run events, final feature-to-default-branch gates as `main_merge_gate_status` sprint-run events, and unresolved CI repair attention remains active while its item is `open` or `claimed`. For each task or main-merge entity, the projection selects the newest matching event by creation time and then event ID; the sprint presentation aggregates those latest entity states. Persisted task merge metadata is used only as durable fallback evidence when no matching event is available. diff --git a/docs-web/user/dashboard/tasks.md b/docs-web/user/dashboard/tasks.md index 39f74c4d16..d899309f99 100644 --- a/docs-web/user/dashboard/tasks.md +++ b/docs-web/user/dashboard/tasks.md @@ -31,13 +31,13 @@ Each lane is a named region whose accessible name includes its count, such as ** Reduced-motion mode removes board, card, selector, progress, menu, and drop-target movement and disables pointer dragging. Static labels, borders, focus rings, progress values, lane counts, empty states, action availability reasons, and drag-disabled guidance remain available. -Each task card shows its task identifier, title, status, and priority first. Compact metadata can then show a non-default executor or worker agent, session state and identifier, QA and CI state, dependency blocker count, optimistic saving state, source and assignee, runtime duration, pull-request state, creation or live-start time, and an optional self-reflection rating. The footer always keeps the task-labelled **Actions** trigger visible. Dragging a card to another lane changes its status when that transition is available. +Each task card shows its task identifier, title, status, and priority first. Compact metadata can then show a non-default executor or worker agent, session state and identifier, the unified delivery workflow, dependency blocker count, optimistic saving state, source and assignee, runtime duration, pull-request state, creation or live-start time, and an optional self-reflection rating. The footer always keeps the task-labelled **Actions** trigger visible. Dragging a card to another lane changes its status when that transition is available. When a worker reports a task-run self-reflection rating, the shared rating badge appears in the compact card metadata near the task id, status, and priority. It shows the overall `overallRating` as a numeric score with a compact 5-star meter. Hovering the badge, or focusing it with the keyboard, opens a viewport-positioned details panel with each section from `sections`: the section label, matching stars, numeric rating, and any note captured by the worker. Tasks without a captured rating, including older tasks that never produced one, do not render an empty badge slot. -## QA review states and follow-up specifications +## Delivery workflow and QA review details -Task cards use the same QA review badge as Sprints and Live. The badge represents the latest persisted review summary independently of the task lane or phase, so requested-change details remain available while a follow-up run is active and after a reconnect. +Task cards use the shared bright delivery workflow badge in place of the standalone QA and CI badges. The task lifecycle label remains visible beside it, while the badge itself always exposes Coding → Pull request → QA → CI → Merge → Completion. Lifecycle and review state keep the badge durable when CI evidence is absent or refreshing. | Presentation | Meaning | | --- | --- | @@ -46,21 +46,24 @@ Task cards use the same QA review badge as Sprints and Live. The badge represent | Blue pencil, **QA edits** / **QA changes requested** | QA completed successfully and requested changes. This is an actionable review outcome, not a provider failure. | | Red X, **QA failed** | The QA provider run failed, errored, or was cancelled before returning a usable verdict. It does not mean QA requested code changes. | -Hovering the badge, focusing it with the keyboard, or activating it opens an accessible, viewport-positioned review card. The card is named by its review heading and can include the outcome, summary, findings, fix instructions, target task key, reviewer, reviewed time, and generated follow-up tasks. Focus may move between the badge, card, and disclosure buttons without closing it. `Escape` closes the card and restores focus to the badge; moving the pointer away closes it after a short grace period when focus is not inside, and a mouse or touch press outside dismisses it. On touch devices, tap the badge to open it and tap outside to dismiss it. +Hovering, focusing, or activating the badge opens an opaque, viewport-positioned workflow card. Six circles on the left are connected by motion-safe animated dots. When review data exists, an animated chevron reveals an adjacent opaque QA review card with the outcome, summary, findings, fix instructions, target task key, reviewer, reviewed time, and generated follow-up tasks. `Escape` closes the surface and restores focus to the exact workflow or QA-chevron trigger that opened it; outside pointer or touch input dismisses it. Generated follow-up task specifications are collapsed initially, so long prompts do not dominate the review. Each **Follow-up task N** button exposes `aria-expanded` and can be toggled with the keyboard or touch. Expansion reveals the generated title, description, priority, dependency task keys (or **None**), and full Markdown prompt in a bounded scrolling area. The card uses one column on constrained screens, may split summary and findings on wider screens, clamps to the viewport, and scrolls vertically when needed. Reduced motion removes spinner, pulse, rotation, and transition movement without removing labels, borders, focus rings, expanded content, or state semantics. -## Pull request, checks, and merge workflow +## Six-stage delivery flow -The CI badge summarizes a three-step workflow shared by Sprints, Tasks, and Live: +The workflow badge summarizes six stages shared by Sprints, Tasks, Overview, and Live: -1. **Pull request** — waiting for a PR, missing a required PR, or PR ready. -2. **Checks** — pending, running, passed, or failed checks. -3. **Merge** — waiting for checks, QA, or review; checking mergeability; ready to merge; merging; merged; not required; conflict; or failed merge attempt. +1. **Coding** — waiting, queued, preparing, active, quota/capacity wait, paused, complete, or failed. +2. **Pull request** — waiting for a PR, missing a required PR, creating, or ready. +3. **QA** — pending, reviewing, passed, blue **QA edits**, or provider/runtime failure. +4. **CI** — checks pending, running, passed, or failed. +5. **Merge** — waiting, checking mergeability, ready, merging, merged, not required, conflict, or failed attempt. +6. **Completion** — waiting, complete, failed, or cancelled. The four first-class workflow states are `pending`, `in_progress`, `successful`, and `failed`. Pending uses a neutral clock, `in_progress` is presented as running with the signal-colored progress treatment, `successful` uses a green check, and `failed` uses a red X. Failed wins over in progress, in progress wins over pending, and pending wins over successful when the overall badge is derived from the three steps. -The red X is reserved for an actual failed workflow step: failed CI checks, a merge conflict, or a failed merge attempt. A review blocker is not a CI failure: checks remain passed and Merge reads **Waiting for review** in a pending state. A merge conflict fails the Merge step and is labelled **Merge conflict**, which keeps it distinct from **CI failed** at Checks. QA provider failure is shown by the separate QA badge and does not become a CI failure. Activate the CI badge to inspect all three step labels and states; `Escape` closes the details and returns focus to the badge. +The red X is reserved for an actual provider/runtime or workflow failure. Requested QA edits use the bright blue pencil treatment, not failure red. A review blocker is not a CI failure: CI remains passed and Merge reads **Waiting for review**. A merge conflict belongs to Merge and remains distinct from **CI failed**. These badges do not poll per card. Task feature-PR gates are persisted as `ci_gate_status` task-run events, and unresolved CI repair attention remains active while its item is `open` or `claimed`. The card projection selects the newest matching task event by creation time and then event ID, combines it with active attention, and uses persisted task merge metadata only as durable fallback evidence when no matching event is available. diff --git a/docs/architecture/card-ci-status-projection.md b/docs/architecture/card-ci-status-projection.md index fb8d86174f..ddb64fd89e 100644 --- a/docs/architecture/card-ci-status-projection.md +++ b/docs/architecture/card-ci-status-projection.md @@ -29,7 +29,9 @@ Sprint resolution aggregates task results with its latest main-merge gate and ac A review-only `review_blocked` main-merge event does not produce a failed CI status. Explicit failed-check evidence (`state = failed_checks` or `hasFailedChecks = true`) and active main-merge CI repair attention still produce `failed`. -The dashboard expands this compact persisted state with project-scoped execution evidence at each page boundary. Tasks and Live derive task-scoped pull-request, checks, and merge steps; the Sprint gallery and ledger share the sprint-scoped aggregation. Every step uses outcome text in addition to its icon and tone. Successful pull-request evidence is announced as `Pull request ready`, running checks as `Checks running`, failed checks as `Checks failed`, and recovered checks as `Checks passed`, so assistive technology never receives raw enum values as the status label. +The dashboard expands this compact persisted state with project-scoped execution evidence at each page boundary, then combines it with lifecycle and latest-review state in a durable six-stage delivery projection: Coding, Pull request, QA, CI, Merge, and Completion. Tasks and Live derive task-scoped pull-request, checks, and merge steps; the Sprint gallery and ledger share the sprint-scoped aggregation. CI evidence enriches the middle stages but is optional, so a refresh that omits historical gate events cannot unmount or flash away the workflow badge. A durably completed workflow settles missing historical PR, checks, and merge projections as successful instead of displaying contradictory pending stages. Every stage uses outcome text in addition to its icon and tone. + +The shared `WorkflowStatusBadge` renders the six stages as a circular rail with motion-safe dotted connectors. When a review exists, an animated chevron joins the workflow card to an adjacent review card. Requested changes use the blue `QA edits` treatment even when failed-check evidence also exists; red is reserved for actual provider/runtime and workflow failures. Live replaces its separate task lifecycle and QA badges with this projection, while Task, Sprint, and Overview surfaces retain their surrounding lifecycle context and replace standalone QA/CI disclosures. A running sprint deliberately suppresses task-level gate aggregation so its badge remains on Coding instead of flipping as different child tasks enter PR, CI, or Merge; task cards continue showing those individual transitions. Cross-surface integration coverage lives in `tests/dashboard/v2/qa-ci-card-status.integration.test.tsx`. Its deterministic fixture exercises pull-request creation, running checks, failure, recovery, active attention precedence, unrelated-event isolation, reconnect replay, and keyboard-only QA/CI disclosures across Task, Live, Sprint gallery, and Sprint ledger cards without Docker, provider CLIs, Git hosting, or a database. diff --git a/docs/architecture/quality-assurance-agent.md b/docs/architecture/quality-assurance-agent.md index 2fd474328b..e0dd6afd6c 100644 --- a/docs/architecture/quality-assurance-agent.md +++ b/docs/architecture/quality-assurance-agent.md @@ -208,7 +208,7 @@ This separation keeps repository writes, provider calls, task status mutations, - if the latest QA verdict is `changes_requested`, Code UX keeps the merge blocked at the retry cap unless a completed Code UX-applied QA continuation is waiting for verification - if the latest QA verdict is `changes_requested` and a same-session CLI QA follow-up completes after that verdict, Code UX schedules verification before applying `FINISH_TASK`, `FAIL_TASK`, or `ESCALATE_TO_HUMAN` - a passing task QA result is final for that completion state and is not retriggered just because orchestration loops again -- task-level QA runs are now surfaced in task list records and live runtime snapshots. The Tasks page and Live page both show a compact QA badge, including a spinner state while the latest task QA run is still `running`. +- task-level QA runs are surfaced in task list records and live runtime snapshots. Tasks, Live, Sprints, and Overview project the latest review into the shared six-stage delivery workflow badge, including an active QA stage while the latest task QA run is still `running`. ### Sprint completion QA diff --git a/docs/dashboard/dashboard-guide.md b/docs/dashboard/dashboard-guide.md index 25ba81934e..cbbb709ba6 100644 --- a/docs/dashboard/dashboard-guide.md +++ b/docs/dashboard/dashboard-guide.md @@ -375,6 +375,8 @@ Legacy runtime: - Live attention resolve/dismiss dialogs are portaled to a viewport-fixed overlay, preserve viewport position after confirmation, use action-specific tones, and return focus without scrolling the page when the originating queue row disappears. - The Live attention queue, Invocation Feed, and Execution Runtime panels share a compact sidebar feed language with smaller type, subtle row backgrounds, bounded scroll regions, explicit empty states, and narrow colored left rails for status/severity distinction. - The Live page Git / CI / PR panel now uses compact status metric tiles plus state-specific iconography for PR and CI rows, including animated indicators for active CI states (`IN_PROGRESS`, `QUEUED`, `PENDING`, `QUOTA`) with reduced-motion fallback (`motion-reduce:animate-none`) +- Live task cards, Sprint gallery/ledger entries, Task cards, and Overview active-stream rows use one bright delivery workflow badge. Its viewport-positioned card shows Coding → Pull request → QA → CI → Merge → Completion on a circular rail with motion-safe dotted connectors. Live uses it in place of the former lifecycle and QA badges; the other surfaces use it in place of standalone QA/CI disclosures. The badge remains mounted when a refreshed Sprints execution snapshot has no CI events, using lifecycle/review state as the durable base and enriching PR/CI/Merge when evidence returns. +- When a persisted QA review exists, the workflow surface reveals an adjacent review card through an animated chevron. `changes_requested` keeps the bright blue pencil/`QA edits` treatment across the trigger, QA stage, connector, and review card; provider/runtime failures remain red. Hover, focus, activation, touch dismissal, exact-trigger focus restoration, collapsed follow-up specifications, and reduced-motion fallbacks share one contract. - Live telemetry and runtime panels expose loading, empty, reconnecting, disconnected, pending, warning, and error states through named regions, polite status/log live regions, and assertive alerts only for blocking disconnect/error states. Dense runtime strings such as branches, PR titles, workflow names, provider/model labels, connection keys, and event snippets wrap inside their panels to avoid page-level horizontal overflow. - The Sprint ledger keeps sorting, filtering, list-window changes, row selection, per-row menus, and bulk actions accessible with and without motion. Filtered select-all acts on the current filtered result set, selections are pruned when filters hide rows, rows expose stable `aria-selected`/`aria-busy` states with selected and pending badges, and each ledger action emits one concise live outcome with visible and selected counts. Pending bulk controls show a visible disabled reason, reference that reason with `aria-describedby`, suppress duplicate activation, and destructive bulk delete uses the shared hold-to-confirm dialog with a target-specific title and focus restoration to the delete trigger or ledger fallback. - Creating a new sprint automatically updates the active sprint selection to that new sprint diff --git a/docs/dashboard/design-system-sprints.md b/docs/dashboard/design-system-sprints.md index bb6fd64cbc..4ab9ed5d57 100644 --- a/docs/dashboard/design-system-sprints.md +++ b/docs/dashboard/design-system-sprints.md @@ -47,12 +47,12 @@ This document outlines the design system for the Sprints page and related planni ### QA Review And CI Workflow Badges -* **Independent Signals:** Lifecycle, QA, CI, and human-attention treatments remain independently readable. Neither the blue requested-change QA badge nor a CI failure replaces the sprint's running, paused, completed, or failed status. +* **Stable Sprint Workflow:** The unified workflow badge replaces separate QA and CI disclosures while the sprint lifecycle remains independently readable. During `running`, the sprint workflow stays on Coding and suppresses aggregated child-task PR, CI, and Merge transitions; individual task surfaces own those changing gates. A completed sprint settles the full workflow rail successfully even when historical gate evidence is absent from the latest snapshot. * **QA State Contract:** A pass uses a green check; a running review uses signal-colored progress; completed `changes_requested` uses a blue pencil and **QA edits** semantics; provider/runtime `failed`, `errored`, or `cancelled` uses a red X and **QA failed** semantics. Requested changes are an actionable verdict and must never be styled or announced as provider failure. * **QA Details:** `SprintReviewBadge` opens its named, viewport-level region on hover, focus, or activation. It exposes outcome, summary, findings, fix instructions, target task key, reviewer, reviewed time, and follow-up specifications when present. Keep it open while pointer or focus remains in the trigger/card pair; dismiss on outside pointer/touch or pointer departure without contained focus; close on `Escape` and restore trigger focus. * **Follow-Up Disclosure:** Generated follow-up specifications start collapsed. Each **Follow-up task N** button owns `aria-expanded` and `aria-controls`; expansion reveals title, description, priority, dependency task keys, and the full Markdown prompt. Clamp the card to the viewport, bound prompt/card scrolling, use one column on constrained widths, and preserve all text, focus rings, and disclosure state when reduced motion removes pulse, spin, rotation, and transition movement. * **CI Steps:** `CiStatusBadge` always describes Pull request, Checks, and Merge. Its first-class states are `pending`, `in_progress`, `successful`, and `failed`: pending uses a neutral clock, `in_progress` is displayed as running with signal progress, `successful` uses a green check, and `failed` uses a red X. Sprint aggregation applies failed > in-progress > pending > successful across the newest state for every task workflow and the final main-merge workflow. -* **Failure Semantics:** Red X is limited to failed checks, merge conflicts, or failed merge attempts. Review blockers stay pending as **Waiting for review** after passed checks. Merge conflicts fail Merge and retain the **Merge conflict** label; QA provider failure remains in the separate QA badge. +* **Failure Semantics:** Red X is limited to provider/runtime failures, failed checks, merge conflicts, or failed merge attempts. Requested QA edits stay blue. Review blockers remain pending as **Waiting for review**, and merge conflicts fail Merge with the distinct **Merge conflict** label. * **Durable Projection:** Sprint CI derives from the newest persisted `ci_gate_status` event per task and `main_merge_gate_status` event for the final merge, ordered by creation time and event ID, plus active open/claimed CI-repair attention. Persisted merge metadata is fallback evidence only. Project snapshots rehydrate this state after restart/reconnect; a newer recognized settled gate event replaces older failure evidence for the same entity, and resolved or dismissed attention no longer forces failure. ### Quicksprint Panel diff --git a/docs/dashboard/design-system-tasks.md b/docs/dashboard/design-system-tasks.md index 51e86c9ddb..480d5fdeab 100644 --- a/docs/dashboard/design-system-tasks.md +++ b/docs/dashboard/design-system-tasks.md @@ -48,12 +48,13 @@ Sprint scope, status, priority, and visible-card count form one named **Task boa * **Live Metadata:** Runtime duration, PR availability, QA review state, self-reflection rating state, and dependency blocker state must be visible or available as text equivalents and announced politely when they change. Default Auto executor metadata should not become prominent visible card content; keep it available only where it adds context, such as screen-reader metadata or detailed execution surfaces. * **Self-Reflection Ratings:** Use the shared `SelfReflectionRatingBadge` for compact task-run self-reflection scores near the task id, status, and priority metadata once `selfReflectionRating` is present. It renders a shrink-resistant 5-star meter with numeric copy, uses a meter label such as `Self-reflection rating N out of 5`, and opens a viewport-positioned section-rating tooltip on hover or keyboard focus. Section rows must include the section label, numeric score, star state, and note text when present so users are never asked to infer rating from color alone. Missing or malformed ratings must collapse to no DOM output: unrated cards must not render empty placeholder wrappers, disabled badges, skeleton badges, or reserved rating slots. -### QA Review And CI Workflow Badges +### Delivery Workflow Badge -* **QA States:** Render pass with a green check, running with the signal progress treatment, completed `changes_requested` with the blue pencil-line **QA edits** treatment, and terminal provider/runtime failure with the red-X **QA failed** treatment. Requested changes are a valid completed QA verdict and remain distinct from provider failure and the task's lane status. -* **Accessible Review Card:** The shared `SprintReviewBadge` opens on hover, keyboard focus, or activation and portals a named region to the viewport. It can expose outcome, summary, findings, fix instructions, target task key, reviewer, reviewed time, and generated follow-up specifications. Focus may move into the card; outside mouse/touch and pointer departure without contained focus dismiss it, while `Escape` restores focus to the trigger. +* **Unified Flow:** The shared `WorkflowStatusBadge` replaces separate QA and CI disclosures and exposes Coding → Pull request → QA → CI → Merge → Completion. Keep the compact task lifecycle label beside it. The badge stays mounted without CI evidence and is enriched when gate events arrive. +* **QA States:** Render pass with a green check, running with the signal progress treatment, completed `changes_requested` with the bright blue pencil-line **QA edits** treatment, and terminal provider/runtime failure with the red-X **QA failed** treatment. Requested changes are a valid completed QA verdict and remain distinct from provider failure and the task's lane status. +* **Accessible Workflow And Review Cards:** Hover, keyboard focus, or activation portals an opaque named workflow card to the viewport. Six circular nodes and motion-safe dotted connectors expose the full sequence. When a review exists, an animated chevron opens the adjacent opaque review card with outcome, summary, findings, fix instructions, target task key, reviewer, reviewed time, and generated follow-up specifications. Outside mouse/touch and pointer departure without contained focus dismiss the surface; `Escape` restores focus to the exact trigger that opened it. * **Collapsed Follow-Ups:** Generated specifications are collapsed initially. Each **Follow-up task N** control exposes `aria-expanded`/`aria-controls` and keyboard/touch activation. Expansion reveals title, description, priority, dependencies, and the complete Markdown prompt in bounded overflow. Responsive layout clamps to the viewport and changes from one column to an optional summary/findings split; reduced motion keeps static labels, borders, focus, and content while removing pulse, spin, rotation, and transitions. -* **Three-Step CI:** Task CI describes Pull request, Checks, and Merge with the first-class states `pending`, `in_progress`, `successful`, and `failed`. The UI presents `in_progress` as running and `successful` with a green check. Failed > in-progress > pending > successful determines the aggregate badge. Only failed checks, merge conflicts, or failed merge attempts get the red X; review blockers remain pending with **Waiting for review**, merge conflicts fail the Merge step, and QA provider failures stay in the separate QA badge. +* **Stage Semantics:** Coding, Pull request, QA, CI, Merge, and Completion use `pending`, `in_progress`, `successful`, and `failed`. The UI presents `in_progress` as running and `successful` with a green check. Failed > in-progress > pending > successful determines the aggregate badge, except requested QA edits intentionally use the bright blue treatment. Review blockers remain pending with **Waiting for review**, merge conflicts fail Merge, and provider/runtime failures remain distinct from requested changes. * **View-Model Evidence:** `buildTaskBoardViewModel` derives the card from the newest matching persisted `ci_gate_status` event by creation time/event ID plus active open/claimed CI-repair attention. Durable merge metadata is fallback evidence only. Include the complete event/attention source signature so memoized cards update when evidence changes, while unrelated realtime traffic preserves mounted controls. Restart/reconnect snapshots rebuild the same state; newer recognized settled gate events and resolved attention remove stale failures. ## 4. Compose & Edit Affordances (Task Editor Viewbox) diff --git a/docs/dashboard/interaction-patterns.md b/docs/dashboard/interaction-patterns.md index a4b62addfd..534da304e3 100644 --- a/docs/dashboard/interaction-patterns.md +++ b/docs/dashboard/interaction-patterns.md @@ -59,7 +59,7 @@ Current refined dashboard surfaces use the interaction contracts as follows: | Quicksprint panel | `enterExit`, `listReveal`, `selectionMovement`, `expansionCollapse`, `controlFeedback`, `asyncFeedback` | Browse, edit, and configure phases announce through a shared polite status region; picker controls expose expanded/selected state; planning suppresses duplicate requests; destructive template removal uses confirmation; cancel/status copy remains visible under reduced motion. | | Sprint ledger | `controlFeedback`, `selectionMovement`, `listReorder`, `expansionCollapse`, `asyncFeedback` | Sort, filter, selection, and bulk-action changes are composed into one polite live-region message; selected and pending rows retain static badges; bulk delete uses `ConfirmDialog`; focus returns to the delete trigger or a ledger fallback after dialog teardown. | | Sprint attention indicators | `asyncFeedback` | Failed gallery cells and ledger rows keep a red outer border and an explicitly labelled failure status while the exclamation pulses; human-owned intervention keeps the same border plus a compact person/`zZZ` cue and labelled waiting status. Reduced motion removes pulse/bounce animation without removing either status. | -| QA review and CI workflow badges | `controlFeedback`, `enterExit`, `expansionCollapse`, `asyncFeedback` | QA pass, running, requested-change, and provider-failure states keep distinct check/spinner/pencil/X semantics. The review card opens on hover/focus/activation, follow-up specifications start collapsed, and CI exposes Pull request, Checks, and Merge without using a red X for review blockers. Persisted events and active attention update the badges through shared snapshots rather than card polling. | +| Delivery workflow badge | `controlFeedback`, `enterExit`, `expansionCollapse`, `asyncFeedback` | One bright interactive badge exposes Coding → Pull request → QA → CI → Merge → Completion on Live, Tasks, Sprints, and Overview. Opaque warm surfaces keep underlying card content from bleeding through. A circular rail and animated dotted connectors communicate sequence; reduced motion keeps the rail static. When review data exists, an animated chevron links the workflow card to an adjacent QA review card. QA edits retain the blue pencil treatment, while red remains reserved for provider/runtime or workflow failures. | | Notification center and execution toasts | `enterExit`, `listReveal`, `listReorder`, `asyncFeedback` | The panel combines cross-project execution attention, startup readiness, and selected-project scheduler notices. Execution rows keep project/sprint/task context, structured Details, and server-supplied route targets; only new or timestamp-updated global execution records create toasts after initial hydration. Warning toasts use the bottom-right stack, while system errors remain in the persistent assertive error stack. | | Live runtime | `controlFeedback`, `enterExit`, `expansionCollapse`, `selectionMovement`, `listReveal`, `listReorder`, `asyncFeedback` | Reconnect, stale, refreshing, and recovering states keep the last runtime snapshot visible with polite live regions; disconnected transport and blocking errors are assertive; pending runtime actions remain focus-stable with `aria-disabled` plus activation suppression. Runtime force-complete and sprint pause/stop/delete controls require an explicit named confirmation before their side-effect handlers run. | | Browser preview, file, and diff workbench | `controlFeedback`, `enterExit`, `selectionMovement`, `listReveal`, `listReorder`, `asyncFeedback` | Preview launch/rebuild/stop/navigation/script/log operations expose visible async status; unavailable links remain keyboard reachable as disabled link controls with persistent reasons; stale iframe/log content remains mounted during refresh when useful content exists. | @@ -70,10 +70,11 @@ Current refined dashboard surfaces use the interaction contracts as follows: ## Cross-Surface Interaction Rules -- QA badges must distinguish a completed requested-change verdict from a provider/runtime failure. Use the blue pencil-line **QA edits** treatment for `changes_requested`, the red-X **QA failed** treatment only for terminal provider/runtime failure, the green check for pass, and signal progress for running. Keep the latest persisted review visible independently of card lifecycle phase. -- The QA review card is one hover, focus, activation, touch, and dismissal contract across Sprint, Task, and Live surfaces. Port it to the viewport, name the region from its heading, preserve it while focus/pointer is within the trigger/card pair, dismiss outside mouse/touch and pointer departure without contained focus, and restore trigger focus on `Escape`. +- The delivery workflow badge replaces the standalone QA/CI disclosures wherever task or sprint delivery state is shown. Live also uses it instead of the separate task lifecycle badge; Tasks, Sprints, and Overview keep their surrounding lifecycle context. +- QA state inside the workflow must distinguish a completed requested-change verdict from a provider/runtime failure. Use the bright blue pencil-line **QA edits** treatment for `changes_requested`, the red-X **QA failed** treatment only for terminal provider/runtime failure, the green check for pass, and signal progress for running. Keep the latest persisted review visible independently of card lifecycle phase. +- The combined workflow/review surface is one hover, focus, activation, touch, and dismissal contract across Sprint, Task, Overview, and Live surfaces. Port it to the viewport, name both cards, preserve it while focus/pointer is within the trigger/card pair, dismiss outside mouse/touch and pointer departure without contained focus, and restore focus to the exact workflow or QA-chevron trigger that opened it on `Escape`. - Follow-up task specifications must be collapsed on first render. Native disclosure buttons expose `aria-expanded` and `aria-controls`; expansion reveals title, description, priority, dependency keys, and the full prompt in bounded overflow. Constrained layouts remain one-column, wider layouts may split summary/findings, and reduced motion snaps spinner, pulse, chevron, and transition changes while retaining all semantic and focus state. -- CI badges expose Pull request, Checks, and Merge steps with the first-class states `pending`, `in_progress`, `successful`, and `failed`; `in_progress` is displayed as running and `successful` uses the green-check treatment. Aggregate with failed > in-progress > pending > successful. Red X means a failed check, merge conflict, or merge attempt; a review blocker remains pending, a merge conflict belongs to Merge, and QA provider failure remains separate. +- Workflow badges expose Coding, Pull request, QA, CI, Merge, and Completion with the first-class states `pending`, `in_progress`, `successful`, and `failed`; `in_progress` is displayed as running and `successful` uses the green-check treatment. Persisted CI evidence enriches the PR/CI/Merge stages but never controls whether the badge is mounted. Red X means a provider/runtime failure, failed check, merge conflict, or failed merge attempt; review-requested edits remain blue, a review blocker remains pending, and a merge conflict belongs to Merge. - CI state comes from the newest persisted gate event per task/main-merge entity, tie-broken by event ID, plus active open/claimed CI-repair attention and durable merge fallback evidence. Project and Live snapshots rehydrate it across restart/reconnect. A newer recognized settled gate event or resolved/dismissed attention resolves stale failure for the same scoped entity. - Preserve stale data when a surface already has a useful snapshot or list and the new request is a refresh, reconnect, retryable load failure, or transient stale state. Mark the affected region with `aria-busy` when it is actively updating, add polite status copy, and visually dim or badge the stale content without blocking valid actions. diff --git a/tests/dashboard/live/live-task-card-actions.test.tsx b/tests/dashboard/live/live-task-card-actions.test.tsx index e7f7c8d1d3..efd3cddc03 100644 --- a/tests/dashboard/live/live-task-card-actions.test.tsx +++ b/tests/dashboard/live/live-task-card-actions.test.tsx @@ -245,7 +245,7 @@ describe("live task card actions", () => { fireEvent.click(buttons[0]!); await holdForceCompleteConfirmation(); expect(await screen.findByText("force complete failed")).toBeInTheDocument(); - expect(within(card).getByText("Running")).toBeInTheDocument(); + expect(within(card).getByText("Coding in progress")).toBeInTheDocument(); expect(within(card).queryByText(/Marking this task complete/)).not.toBeInTheDocument(); }); }); diff --git a/tests/dashboard/v2/components/ui/WorkflowStatusBadge.test.tsx b/tests/dashboard/v2/components/ui/WorkflowStatusBadge.test.tsx new file mode 100644 index 0000000000..418917e773 --- /dev/null +++ b/tests/dashboard/v2/components/ui/WorkflowStatusBadge.test.tsx @@ -0,0 +1,78 @@ +// @vitest-environment jsdom +import "@testing-library/jest-dom/vitest"; +import { cleanup, fireEvent, render, screen, within } from "@testing-library/preact"; +import { afterEach, describe, expect, it } from "vitest"; +import type { CiStatusPresentation } from "../../../../../dashboard/src/v2/lib/ci-status-presentation.js"; +import { WorkflowStatusBadge } from "../../../../../dashboard/src/v2/components/ui/WorkflowStatusBadge.js"; + +afterEach(cleanup); + +const failedCi: CiStatusPresentation = { + scope: "task", + state: "failed", + label: "CI failed", + accessibleLabel: "CI failed. Pull request: Pull request ready. Checks: Checks failed. Merge: Blocked by checks.", + failureKind: "ci_checks", + steps: [ + { id: "pull_request", label: "Pull request", state: "successful", statusLabel: "Pull request ready" }, + { id: "checks", label: "Checks", state: "failed", statusLabel: "Checks failed", failureKind: "ci_checks" }, + { id: "merge", label: "Merge", state: "pending", statusLabel: "Blocked by checks" }, + ], +}; + +const review = { + status: "completed", + outcome: "changes_requested", + summary: "Add deterministic reconnect coverage.", + findings: ["The recovery branch needs a regression test."], + reviewer: "QA Reviewer", + finishedAt: "2026-07-14T08:00:00.000Z", +} as const; + +describe("WorkflowStatusBadge", () => { + it("renders the bright QA-edits trigger and reveals connected workflow and review cards", () => { + const { container } = render( + , + ); + + const trigger = screen.getByRole("button", { name: /CI status: CI failed/i }); + expect(trigger).toHaveTextContent("QA edits"); + expect(trigger).toHaveClass("text-blue-700"); + expect(screen.getByRole("button", { name: "QA review details" }).querySelector(".workflow-status__chevron")).toBeInTheDocument(); + + fireEvent.mouseEnter(trigger.closest("[data-workflow-state]") as Element); + const workflow = screen.getByRole("region", { name: "CI workflow details" }); + expect(workflow.querySelectorAll("[data-workflow-stage]")).toHaveLength(6); + expect(workflow.querySelectorAll(".workflow-status__connector")).toHaveLength(5); + expect(within(workflow).getByText("Coding")).toBeVisible(); + expect(within(workflow).getByText("Completion")).toBeVisible(); + expect(screen.getByRole("region", { name: "QA Changes Requested" })).toHaveTextContent(review.summary); + expect(container.querySelector('[data-qa-state="changes_requested"]')).toBeInTheDocument(); + }); + + it("remains interactive without a QA review or CI projection", () => { + render(); + + const trigger = screen.getByRole("button", { name: /CI status: Coding in progress/i }); + expect(trigger).toBeVisible(); + fireEvent.click(trigger); + const workflow = screen.getByRole("region", { name: "CI workflow details" }); + expect(workflow.querySelectorAll("[data-workflow-stage]")).toHaveLength(6); + expect(screen.queryByRole("button", { name: "QA review details" })).not.toBeInTheDocument(); + }); + + it("does not expose task gate aggregation from a running sprint badge", () => { + render(); + + const trigger = screen.getByRole("button", { name: /CI status: Coding in progress/i }); + expect(trigger).toHaveTextContent("Coding in progress"); + expect(trigger.closest("[data-workflow-state]")).toHaveAttribute("data-ci-state", "in_progress"); + expect(trigger).not.toHaveAccessibleName(/CI failed/i); + }); +}); diff --git a/tests/dashboard/v2/components/ui/live-task-card.test.tsx b/tests/dashboard/v2/components/ui/live-task-card.test.tsx index 8cde9f6349..53fd642df9 100644 --- a/tests/dashboard/v2/components/ui/live-task-card.test.tsx +++ b/tests/dashboard/v2/components/ui/live-task-card.test.tsx @@ -166,7 +166,7 @@ describe("LiveTaskCard", () => { expect(screen.getByRole("group", { name: "Status indicators for task test-task" })).toBeTruthy(); expect(screen.getByRole("button", { name: /CI status: CI failed/i })).toBeTruthy(); - expect(screen.getByText("Running")).toBeTruthy(); + expect(screen.queryByText("Running")).toBeNull(); expect(container.querySelectorAll('[data-ci-state="failed"]')).toHaveLength(1); expect(screen.queryByText("CI")).toBeNull(); }); @@ -187,7 +187,7 @@ describe("LiveTaskCard", () => { />, ); const scoped = within(container); - expect(scoped.getByText("Quota")).toBeTruthy(); + expect(scoped.getByText("Quota wait")).toBeTruthy(); expect(scoped.queryByText("Running")).toBeNull(); }); diff --git a/tests/dashboard/v2/lib/workflow-status-presentation.test.ts b/tests/dashboard/v2/lib/workflow-status-presentation.test.ts new file mode 100644 index 0000000000..dd7c72a5a3 --- /dev/null +++ b/tests/dashboard/v2/lib/workflow-status-presentation.test.ts @@ -0,0 +1,114 @@ +import { describe, expect, it } from "vitest"; +import type { CiStatusPresentation } from "../../../../dashboard/src/v2/lib/ci-status-presentation.js"; +import { deriveWorkflowStatusPresentation } from "../../../../dashboard/src/v2/lib/workflow-status-presentation.js"; + +const successfulCi: CiStatusPresentation = { + scope: "task", + state: "successful", + label: "CI passed", + accessibleLabel: "CI passed. Pull request: Pull request ready. Checks: Checks passed. Merge: Merged.", + steps: [ + { id: "pull_request", label: "Pull request", state: "successful", statusLabel: "Pull request ready" }, + { id: "checks", label: "Checks", state: "successful", statusLabel: "Checks passed" }, + { id: "merge", label: "Merge", state: "successful", statusLabel: "Merged" }, + ], +}; + +describe("deriveWorkflowStatusPresentation", () => { + it("keeps a durable six-stage flow when CI evidence is absent", () => { + const presentation = deriveWorkflowStatusPresentation({ scope: "sprint", status: "running" }); + + expect(presentation.stages.map((stage) => stage.id)).toEqual([ + "coding", + "pull_request", + "qa", + "checks", + "merge", + "completion", + ]); + expect(presentation.label).toBe("Coding in progress"); + expect(presentation.stages[1]).toMatchObject({ state: "pending", statusLabel: "Waiting for pull request" }); + expect(presentation.accessibleLabel).toContain("Completion: Completion pending"); + }); + + it("keeps a running sprint on Coding instead of aggregating task gate activity", () => { + const presentation = deriveWorkflowStatusPresentation({ + scope: "sprint", + status: "running", + ciPresentation: successfulCi, + review: { + status: "completed", + outcome: "changes_requested", + summary: "An earlier review requested edits.", + findings: [], + reviewer: "QA Reviewer", + finishedAt: "2026-07-14T08:00:00.000Z", + }, + }); + + expect(presentation).toMatchObject({ state: "in_progress", label: "Coding in progress" }); + expect(presentation.stages[0]).toMatchObject({ state: "in_progress", statusLabel: "Coding in progress" }); + expect(presentation.stages[1]).toMatchObject({ state: "pending", statusLabel: "Waiting for pull request" }); + expect(presentation.stages[2]).toMatchObject({ state: "pending", statusLabel: "QA pending" }); + expect(presentation.stages[3]).toMatchObject({ state: "pending", statusLabel: "Checks pending" }); + expect(presentation.stages[4]).toMatchObject({ state: "pending", statusLabel: "Merge pending" }); + }); + + it("settles the complete workflow when status, QA, CI, and merge have succeeded", () => { + const presentation = deriveWorkflowStatusPresentation({ + scope: "task", + status: "completed", + ciPresentation: successfulCi, + review: { + status: "completed", + outcome: "approved", + summary: "Ready to ship.", + findings: [], + reviewer: "QA Reviewer", + finishedAt: "2026-07-14T08:00:00.000Z", + }, + }); + + expect(presentation.label).toBe("Completed"); + expect(presentation.state).toBe("successful"); + expect(presentation.stages.every((stage) => stage.state === "successful")).toBe(true); + }); + + it("settles PR, CI, and merge fallbacks for a completed sprint without gate history", () => { + const presentation = deriveWorkflowStatusPresentation({ + scope: "sprint", + status: "completed", + }); + + expect(presentation).toMatchObject({ state: "successful", label: "Completed" }); + expect(presentation.stages[1]).toMatchObject({ state: "successful", statusLabel: "Pull request ready" }); + expect(presentation.stages[3]).toMatchObject({ state: "successful", statusLabel: "Checks passed" }); + expect(presentation.stages[4]).toMatchObject({ state: "successful", statusLabel: "Merged" }); + expect(presentation.stages.every((stage) => stage.state === "successful")).toBe(true); + }); + + it("uses the bright blue QA-edit tone for requested changes", () => { + const presentation = deriveWorkflowStatusPresentation({ + scope: "task", + status: "coding_completed", + ciPresentation: successfulCi, + review: { + status: "completed", + outcome: "changes_requested", + summary: "One edit remains.", + findings: ["Add reconnect coverage."], + reviewer: "QA Reviewer", + finishedAt: "2026-07-14T08:00:00.000Z", + }, + }); + + expect(presentation).toMatchObject({ state: "failed", tone: "qa_changes", label: "QA changes" }); + expect(presentation.stages[2]).toMatchObject({ id: "qa", state: "failed", statusLabel: "Changes requested" }); + }); + + it("preserves live runtime wait labels inside the coding stage", () => { + const presentation = deriveWorkflowStatusPresentation({ scope: "task", status: "QUOTA" }); + expect(presentation).toMatchObject({ state: "in_progress", label: "Quota wait" }); + expect(presentation.stages[0].statusLabel).toBe("Quota wait"); + }); +}); diff --git a/tests/dashboard/v2/qa-ci-card-status.integration.test.tsx b/tests/dashboard/v2/qa-ci-card-status.integration.test.tsx index 0eab825c44..62430f4086 100644 --- a/tests/dashboard/v2/qa-ci-card-status.integration.test.tsx +++ b/tests/dashboard/v2/qa-ci-card-status.integration.test.tsx @@ -398,6 +398,9 @@ const SURFACES = [ ["Sprint ledger", "Sprint ledger card surface", LedgerSurface], ] as const; +const TASK_SURFACES = SURFACES.slice(0, 2); +const SPRINT_SURFACES = SURFACES.slice(2); + function AllSurfaces({ data }: { data: SurfaceData }): VNode { return ( <> @@ -443,12 +446,12 @@ describe("shared QA and CI card status integration", () => { vi.clearAllMocks(); }); - it("renders equivalent requested-change QA and failed CI workflow semantics on all four cards", async () => { + it("keeps task gate detail on task cards and a stable Coding state on running sprint cards", async () => { const user = userEvent.setup(); const data = buildSurfaceData(CI_HISTORY); const { container } = render(); - for (const [, surfaceLabel] of SURFACES) { + for (const [, surfaceLabel] of TASK_SURFACES) { const surface = screen.getByRole("region", { name: surfaceLabel }); const qaTrigger = within(surface).getByRole("button", { name: "QA review details" }); expect(qaTrigger).toHaveAccessibleDescription(/QA changes requested/i); @@ -458,21 +461,42 @@ describe("shared QA and CI card status integration", () => { const ciTrigger = within(surface).getByRole("button", { name: /CI status: CI failed.*Pull request ready.*Checks failed.*Blocked by checks.*Show workflow details/i, }); - expect(ciTrigger).toHaveTextContent("CI failed"); - expect(ciTrigger).toHaveClass("text-status-red"); + expect(ciTrigger).toHaveTextContent("QA edits"); + expect(ciTrigger).toHaveClass("text-blue-700"); expect(surface.querySelector('[data-ci-icon="failure"]')).toHaveClass("text-status-red"); await user.click(ciTrigger); - const workflow = within(surface).getByRole("region", { name: "CI workflow details" }); + const workflow = screen.getByRole("region", { name: "CI workflow details" }); expect(workflow.querySelector('[data-ci-step="pull_request"]')).toHaveAttribute("data-ci-step-state", "successful"); expect(workflow.querySelector('[data-ci-step="checks"]')).toHaveAttribute("data-ci-step-state", "failed"); expect(workflow.querySelector('[data-ci-step="merge"]')).toHaveAttribute("data-ci-step-state", "pending"); expect(within(workflow).getByText("Checks failed")).toBeVisible(); + await user.keyboard("{Escape}"); + } + + for (const [, surfaceLabel] of SPRINT_SURFACES) { + const surface = screen.getByRole("region", { name: surfaceLabel }); + const qaTrigger = within(surface).getByRole("button", { name: "QA review details" }); + expect(qaTrigger).toHaveAccessibleDescription(/QA changes requested/i); + expect(qaTrigger).toHaveClass("text-blue-700"); + + const workflowTrigger = within(surface).getByRole("button", { + name: /CI status: Coding in progress.*Show workflow details/i, + }); + expect(workflowTrigger).toHaveTextContent("Coding in progress"); + expect(surface.querySelector('[data-ci-icon="failure"]')).not.toBeInTheDocument(); + + await user.click(workflowTrigger); + const workflow = screen.getByRole("region", { name: "CI workflow details" }); + expect(workflow.querySelector('[data-ci-step="pull_request"]')).toHaveAttribute("data-ci-step-state", "pending"); + expect(workflow.querySelector('[data-ci-step="checks"]')).toHaveAttribute("data-ci-step-state", "pending"); + expect(workflow.querySelector('[data-ci-step="merge"]')).toHaveAttribute("data-ci-step-state", "pending"); + await user.keyboard("{Escape}"); } expect(within(screen.getByRole("region", { name: "Task card surface" })).getByText("QA edits")).toBeVisible(); expect(within(screen.getByRole("region", { name: "Live card surface" })).getByText("QA edits")).toBeVisible(); - expect(container.querySelectorAll('[data-ci-icon="failure"]')).toHaveLength(4); + expect(container.querySelectorAll('[data-ci-icon="failure"]')).toHaveLength(2); }); it.each(SURFACES)("opens and fully operates the %s QA details without pointer input", async (_name, surfaceLabel, Surface) => { @@ -569,19 +593,29 @@ describe("shared QA and CI card status integration", () => { expect(taskScoped.liveItem.ciPresentation?.label).toBe("CI pending"); const { rerender } = render(); - for (const [, surfaceLabel] of SURFACES) { + for (const [, surfaceLabel] of TASK_SURFACES) { const surface = screen.getByRole("region", { name: surfaceLabel }); expect(within(surface).getByRole("button", { name: /CI status: CI pending/i })).toBeVisible(); expect(within(surface).queryByText("CI failed")).not.toBeInTheDocument(); expect(surface.querySelector('[data-ci-icon="failure"]')).not.toBeInTheDocument(); } + for (const [, surfaceLabel] of SPRINT_SURFACES) { + const surface = screen.getByRole("region", { name: surfaceLabel }); + expect(within(surface).getByRole("button", { name: /CI status: Coding in progress/i })).toBeVisible(); + expect(surface.querySelector('[data-ci-icon="failure"]')).not.toBeInTheDocument(); + } rerender(); - for (const [, surfaceLabel] of SURFACES) { + for (const [, surfaceLabel] of TASK_SURFACES) { const surface = screen.getByRole("region", { name: surfaceLabel }); expect(within(surface).getByRole("button", { name: /CI status: CI failed/i })).toBeVisible(); expect(surface.querySelector('[data-ci-icon="failure"]')).toHaveClass("text-status-red"); } + for (const [, surfaceLabel] of SPRINT_SURFACES) { + const surface = screen.getByRole("region", { name: surfaceLabel }); + expect(within(surface).getByRole("button", { name: /CI status: Coding in progress/i })).toBeVisible(); + expect(surface.querySelector('[data-ci-icon="failure"]')).not.toBeInTheDocument(); + } }); it("preserves workflow disclosure and focus when an unchanged execution snapshot is replayed", async () => { @@ -599,15 +633,15 @@ describe("shared QA and CI card status integration", () => { ciTrigger.focus(); await user.keyboard("{Enter}"); expect(ciTrigger).toHaveFocus(); - expect(within(taskSurface).getByRole("region", { name: "CI workflow details" })).toBeVisible(); + expect(screen.getByRole("region", { name: "CI workflow details" })).toBeVisible(); view.rerender(); expect(ciTrigger).toHaveFocus(); expect(ciTrigger).toHaveAttribute("aria-expanded", "true"); - expect(within(taskSurface).getByRole("region", { name: "CI workflow details" })).toBeVisible(); + expect(screen.getByRole("region", { name: "CI workflow details" })).toBeVisible(); await user.keyboard("{Escape}"); - expect(within(taskSurface).queryByRole("region", { name: "CI workflow details" })).not.toBeInTheDocument(); + expect(screen.queryByRole("region", { name: "CI workflow details" })).not.toBeInTheDocument(); expect(ciTrigger).toHaveFocus(); }); }); diff --git a/tests/dashboard/v2/sprints-page-status-regression.test.tsx b/tests/dashboard/v2/sprints-page-status-regression.test.tsx index 2ff18b2171..9716c41c9a 100644 --- a/tests/dashboard/v2/sprints-page-status-regression.test.tsx +++ b/tests/dashboard/v2/sprints-page-status-regression.test.tsx @@ -150,7 +150,7 @@ describe("SprintsPage Status Regression", () => { expect(screen.queryByText("Needs you")).not.toBeInTheDocument(); }); - it("renders the same accessible failed CI and requested-change QA state in gallery and ledger", () => { + it("keeps running gallery and ledger workflows on Coding while exposing requested-change QA", () => { const sprintWithReview = { ...basePageData.sortedSprints[0], status: "running", @@ -178,12 +178,13 @@ describe("SprintsPage Status Regression", () => { const { container } = render(); - expect(screen.getAllByRole("button", { name: /CI status: CI failed.*Show workflow details/i })).toHaveLength(2); - expect(container.querySelectorAll('[data-ci-icon="failure"]')).toHaveLength(2); + expect(screen.getAllByRole("button", { name: /CI status: Coding in progress.*Show workflow details/i })).toHaveLength(2); + expect(container.querySelectorAll('[data-ci-icon="failure"]')).toHaveLength(0); const reviewTriggers = screen.getAllByRole("button", { name: "QA review details" }); expect(reviewTriggers).toHaveLength(2); for (const trigger of reviewTriggers) { expect(trigger).toHaveAccessibleDescription(/QA changes requested/i); + expect(trigger).toHaveClass("text-blue-700"); } }); });