Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 9 additions & 20 deletions dashboard/src/v2/components/LiveTaskCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 ───────────────────────────────────────────────────────── */
Expand Down Expand Up @@ -106,7 +104,6 @@ const LiveTaskCard: FunctionComponent<LiveTaskCardProps> = 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
Expand Down Expand Up @@ -290,22 +287,14 @@ const LiveTaskCard: FunctionComponent<LiveTaskCardProps> = memo(({
<span className="font-mono text-[10px] font-bold px-2.5 py-0.5 rounded-lg bg-black/[0.04] dark:bg-white/[0.04] text-slate-400">
#{task.id}
</span>
{/* Status badge */}
<span className={`px-2.5 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-[0.14em] ${cfg.bg} ${cfg.text} border ${cfg.border}`}>
<span className={`inline-block w-1.5 h-1.5 rounded-full mr-1.5 ${cfg.dot}`} />
<span className="sr-only">Task status: </span>{cfg.label}
</span>
{ciPresentation && (
<CiStatusBadge presentation={ciPresentation} compact />
)}
{mergeCfg && !ciPresentation && taskPhase !== "RUNNING" && taskPhase !== "PENDING" && (
<span className={`px-2.5 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-[0.14em] ${mergeCfg.bg} ${mergeCfg.text} border ${mergeCfg.border}`}>
{mergeCfg.label}
</span>
)}
{task.latestReview && (
<SprintReviewBadge summary={task.latestReview} compact showCompactLabel align="right" />
)}
<WorkflowStatusBadge
scope="task"
status={taskPhase}
review={task.latestReview}
ciPresentation={ciPresentation}
compact
align="right"
/>
<SelfReflectionRatingBadge rating={task.selfReflectionRating} align="start" />
</div>
<h3 className="text-lg font-bold tracking-tight text-slate-900 dark:text-white leading-snug">
Expand Down
21 changes: 11 additions & 10 deletions dashboard/src/v2/components/sprints/SprintCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -350,19 +349,21 @@ export const SprintCell: FunctionComponent<SprintCellProps> = ({
{formatBubbleTime(sprint.createdAt)}
</div>
</div>
{(showInterventionBadge || sprint.latestReview || ciStatus) && (
<div className="absolute right-4 top-4 z-[60] flex max-w-[11rem] flex-wrap items-center justify-end gap-2 lg:right-5 lg:top-5 lg:max-w-[13rem]">
{sprint.latestReview && (
<SprintReviewBadge summary={sprint.latestReview} compact align="right" />
)}
<CiStatusBadge presentation={ciStatus} compact />
<div className="absolute right-4 top-4 z-[60] flex max-w-[11rem] flex-wrap items-center justify-end gap-2 lg:right-5 lg:top-5 lg:max-w-[13rem]">
<WorkflowStatusBadge
scope="sprint"
status={sprint.status}
review={sprint.latestReview}
ciPresentation={ciStatus}
compact
align="right"
/>
{showInterventionBadge && humanIntervention && (
<div className={reducedMotion ? "" : "animate-pulse"} style={interventionPulseStyle}>
<HumanInterventionBadge summary={humanIntervention} label="Needs you" compact align="right" />
</div>
)}
</div>
)}
</div>

<div className={`inline-flex items-center gap-1.5 rounded-full border border-black/[0.06] bg-black/[0.03] px-4 py-1.5 font-mono text-[11px] font-bold tracking-[0.14em] transition-transform group-hover:-translate-y-3 group-focus-within:-translate-y-3 motion-reduce:transform-none dark:border-white/[0.06] dark:bg-white/[0.03] ${accentColor}`} style={controlFeedbackStyle}>
<Sparkles className="h-3.5 w-3.5" strokeWidth={2.2} />
Expand Down
15 changes: 9 additions & 6 deletions dashboard/src/v2/components/sprints/SprintLedgerRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -395,9 +394,6 @@ const SprintLedgerRowComponent: FunctionComponent<SprintLedgerRowProps> = ({
{pendingLabel}
</span>
) : null}
{sprint.latestReview && (
<SprintReviewBadge summary={sprint.latestReview} compact align="left" />
)}
</div>
<div className="mt-2 flex flex-wrap items-center gap-2 text-[11px] font-mono text-slate-400">
<span className="inline-flex items-center gap-1.5 rounded-full border border-black/[0.05] bg-black/[0.025] px-2 py-1 dark:border-white/[0.06] dark:bg-white/[0.03]">
Expand Down Expand Up @@ -431,7 +427,14 @@ const SprintLedgerRowComponent: FunctionComponent<SprintLedgerRowProps> = ({
<span className={`inline-flex rounded-full border px-4 py-1.5 text-[11px] font-bold ${badgeTone}`}>
{badgeLabel}
</span>
<CiStatusBadge presentation={ciStatus} compact />
<WorkflowStatusBadge
scope="sprint"
status={sprint.status}
review={sprint.latestReview}
ciPresentation={ciStatus}
compact
align="left"
/>
{isDeletePending ? (
<span className="inline-flex items-center gap-1.5 rounded-full border border-status-red/25 bg-status-red/10 px-3 py-1.5 text-[11px] font-bold text-status-red">
<Loader2 className="h-3 w-3 animate-spin motion-reduce:animate-none" strokeWidth={2.2} /> Deleting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<SprintCell
sprint={{
Expand Down Expand Up @@ -231,17 +231,22 @@ describe("SprintCell visuals", () => {

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" });
Expand Down
22 changes: 7 additions & 15 deletions dashboard/src/v2/components/tasks/KanbanTaskCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<{
Expand All @@ -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;
Expand Down Expand Up @@ -197,19 +195,13 @@ export const KanbanTaskCard: FunctionComponent<{
<span className="rounded-full border border-black/[0.06] dark:border-white/[0.08] bg-black/[0.03] dark:bg-white/[0.03] px-2.5 py-1 font-mono min-w-0 break-all max-w-full"><span className="sr-only">Session ID: </span>{sessionId}
</span>
)}
{task.latestReview ? (
<SprintReviewBadge summary={task.latestReview} compact showCompactLabel align="right" />
) : (
<span
className="min-w-0 max-w-full truncate rounded-full border border-slate-400/20 bg-slate-400/[0.08] px-2.5 py-1 text-slate-500 dark:text-slate-300"
aria-label="QA review state: no review recorded."
>
{qaNoReviewLabel}
</span>
)}
<CiStatusBadge
presentation={ciStatusPresentation ?? null}
<WorkflowStatusBadge
scope="task"
status={task.status}
review={task.latestReview}
ciPresentation={ciStatusPresentation ?? null}
compact
align="right"
className="min-w-0 max-w-full"
/>
{dependencyIndicators.length > 0 && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
Expand Down
16 changes: 10 additions & 6 deletions dashboard/src/v2/components/ui/TaskRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -47,11 +47,15 @@ export const TaskRow: FunctionComponent<TaskRowProps> = memo(({ task, state, onP
<span className={`text-base md:text-lg font-bold tracking-tight text-slate-900 dark:text-white truncate group-hover:translate-x-1.5 transition-transform duration-300 ease-out ${task.status === 'completed' ? 'opacity-50' : task.status === 'coding_completed' ? 'opacity-80' : ''}`}>
{task.title}
</span>
{task.latestReview && (
<div className="ml-3 shrink-0">
<SprintReviewBadge summary={task.latestReview} compact showCompactLabel align="right" />
</div>
)}
<div className="ml-3 shrink-0">
<WorkflowStatusBadge
scope="task"
status={task.status}
review={task.latestReview}
compact
align="right"
/>
</div>
</div>

{/* Source */}
Expand Down
Loading
Loading