feat(HAI-065): add file-scope overlap detection with blockedBy tracking
- Add blockedBy field to Task type and core store - Update scheduler to set blockedBy when deferring tasks due to file-scope overlap - Render file-scope overlap badge on TaskCard in dashboard - Add tests for blockedBy store logic, scheduler deferral, and TaskCard badge - Simplify and refactor existing executor, triage, and CLI command code
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { Link, Clock } from "lucide-react";
|
||||
import { Link, Clock, Layers } from "lucide-react";
|
||||
import type { Task, TaskDetail, Column } from "@hai/core";
|
||||
import { fetchTaskDetail, uploadAttachment } from "../api";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
@@ -139,13 +139,18 @@ export function TaskCard({ task, queued, onOpenDetail, addToast }: TaskCardProps
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{((task.dependencies && task.dependencies.length > 0) || queued || task.status === "queued") && (
|
||||
{((task.dependencies && task.dependencies.length > 0) || queued || task.status === "queued" || task.blockedBy) && (
|
||||
<div className="card-meta">
|
||||
{task.dependencies && task.dependencies.length > 0 && (
|
||||
<span className="card-dep-badge" data-tooltip={task.dependencies.join(", ")}>
|
||||
<Link size={12} style={{ verticalAlign: 'middle' }} /> {task.dependencies.length} dep{task.dependencies.length > 1 ? "s" : ""}
|
||||
</span>
|
||||
)}
|
||||
{task.blockedBy && (
|
||||
<span className="card-scope-badge" data-tooltip={`Blocked by ${task.blockedBy} (file overlap)`}>
|
||||
<Layers size={12} style={{ verticalAlign: 'middle' }} /> {task.blockedBy}
|
||||
</span>
|
||||
)}
|
||||
{(queued || task.status === "queued") && <span className="queued-badge"><Clock size={12} style={{ verticalAlign: 'middle' }} /> Queued</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -166,6 +166,44 @@ describe("TaskCard dependency tooltip", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("TaskCard file-scope overlap badge logic", () => {
|
||||
/** Mirrors the card-meta visibility condition from TaskCard.tsx */
|
||||
function shouldShowCardMeta(opts: { dependencies?: string[]; queued?: boolean; status?: string | null; blockedBy?: string }): boolean {
|
||||
const deps = opts.dependencies || [];
|
||||
return deps.length > 0 || !!opts.queued || opts.status === "queued" || !!opts.blockedBy;
|
||||
}
|
||||
|
||||
/** Mirrors the card-scope-badge visibility condition from TaskCard.tsx */
|
||||
function shouldShowScopeBadge(blockedBy?: string): boolean {
|
||||
return !!blockedBy;
|
||||
}
|
||||
|
||||
it("shows scope badge when blockedBy is set", () => {
|
||||
expect(shouldShowScopeBadge("HAI-003")).toBe(true);
|
||||
});
|
||||
|
||||
it("does NOT show scope badge when blockedBy is undefined", () => {
|
||||
expect(shouldShowScopeBadge(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("shows card-meta when blockedBy is set even with no deps or queued status", () => {
|
||||
expect(shouldShowCardMeta({ blockedBy: "HAI-003" })).toBe(true);
|
||||
});
|
||||
|
||||
it("does NOT show card-meta when no deps, not queued, and no blockedBy", () => {
|
||||
expect(shouldShowCardMeta({})).toBe(false);
|
||||
});
|
||||
|
||||
/** Mirrors tooltip computation from TaskCard.tsx */
|
||||
function computeScopeTooltip(blockedBy: string): string {
|
||||
return `Blocked by ${blockedBy} (file overlap)`;
|
||||
}
|
||||
|
||||
it("generates correct tooltip text", () => {
|
||||
expect(computeScopeTooltip("HAI-005")).toBe("Blocked by HAI-005 (file overlap)");
|
||||
});
|
||||
});
|
||||
|
||||
describe("TaskCard queued badge logic", () => {
|
||||
/** Mirrors the card-status-badge visibility condition from TaskCard.tsx */
|
||||
function shouldShowStatusBadge(status?: string | null): boolean {
|
||||
|
||||
@@ -315,6 +315,35 @@ html, body {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.card-scope-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-size: 11px;
|
||||
color: var(--in-progress);
|
||||
position: relative;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.card-scope-badge[data-tooltip]:hover::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--bg-card);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
margin-bottom: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.card-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user