feat(KB-160): thread engineStopped prop to suppress agent-active indicator
- Derive engineStopped from globalPaused/enginePaused in App and pass through Board → Column → WorktreeGroup → TaskCard - Guard isAgentActive in TaskCard with !engineStopped so the pulse animation stops when the engine is paused - Update Board test to supply the new engineStopped prop - Add TaskCard tests covering engineStopped=true, false, and undefined (backward compat) - Add engineStopped to all intermediate component interfaces
This commit is contained in:
@@ -91,6 +91,8 @@ function AppInner() {
|
||||
|
||||
const handleDetailClose = useCallback(() => setDetailTask(null), []);
|
||||
|
||||
const engineStopped = globalPaused || enginePaused;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
@@ -112,6 +114,7 @@ function AppInner() {
|
||||
onNewTask={handleCreateOpen}
|
||||
autoMerge={autoMerge}
|
||||
onToggleAutoMerge={handleToggleAutoMerge}
|
||||
engineStopped={engineStopped}
|
||||
/>
|
||||
{detailTask && (
|
||||
<TaskDetailModal
|
||||
|
||||
@@ -15,9 +15,10 @@ interface BoardProps {
|
||||
onNewTask: () => void;
|
||||
autoMerge: boolean;
|
||||
onToggleAutoMerge: () => void;
|
||||
engineStopped?: boolean;
|
||||
}
|
||||
|
||||
export function Board({ tasks, maxConcurrent, onMoveTask, onOpenDetail, addToast, isCreating, onCancelCreate, onCreateTask, onNewTask, autoMerge, onToggleAutoMerge }: BoardProps) {
|
||||
export function Board({ tasks, maxConcurrent, onMoveTask, onOpenDetail, addToast, isCreating, onCancelCreate, onCreateTask, onNewTask, autoMerge, onToggleAutoMerge, engineStopped }: BoardProps) {
|
||||
return (
|
||||
<main className="board" id="board">
|
||||
{COLUMNS.map((col) => (
|
||||
@@ -41,6 +42,7 @@ export function Board({ tasks, maxConcurrent, onMoveTask, onOpenDetail, addToast
|
||||
onMoveTask={onMoveTask}
|
||||
onOpenDetail={onOpenDetail}
|
||||
addToast={addToast}
|
||||
engineStopped={engineStopped}
|
||||
{...(col === "triage" ? { isCreating, onCancelCreate, onCreateTask, onNewTask } : {})}
|
||||
{...(col === "in-review" ? { autoMerge, onToggleAutoMerge } : {})}
|
||||
/>
|
||||
|
||||
@@ -22,9 +22,10 @@ interface ColumnProps {
|
||||
onNewTask?: () => void;
|
||||
autoMerge?: boolean;
|
||||
onToggleAutoMerge?: () => void;
|
||||
engineStopped?: boolean;
|
||||
}
|
||||
|
||||
export function Column({ column, tasks, allTasks, maxConcurrent, onMoveTask, onOpenDetail, addToast, isCreating, onCancelCreate, onCreateTask, onNewTask, autoMerge, onToggleAutoMerge }: ColumnProps) {
|
||||
export function Column({ column, tasks, allTasks, maxConcurrent, onMoveTask, onOpenDetail, addToast, isCreating, onCancelCreate, onCreateTask, onNewTask, autoMerge, onToggleAutoMerge, engineStopped }: ColumnProps) {
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
const countFlashing = useFlashOnIncrease(tasks.length);
|
||||
|
||||
@@ -107,6 +108,7 @@ export function Column({ column, tasks, allTasks, maxConcurrent, onMoveTask, onO
|
||||
queuedTasks={group.queuedTasks}
|
||||
onOpenDetail={onOpenDetail}
|
||||
addToast={addToast}
|
||||
engineStopped={engineStopped}
|
||||
/>
|
||||
))
|
||||
);
|
||||
@@ -115,7 +117,7 @@ export function Column({ column, tasks, allTasks, maxConcurrent, onMoveTask, onO
|
||||
<div className="empty-column">No tasks</div>
|
||||
) : (
|
||||
tasks.map((task) => (
|
||||
<TaskCard key={task.id} task={task} onOpenDetail={onOpenDetail} addToast={addToast} />
|
||||
<TaskCard key={task.id} task={task} onOpenDetail={onOpenDetail} addToast={addToast} engineStopped={engineStopped} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -27,9 +27,10 @@ interface TaskCardProps {
|
||||
queued?: boolean;
|
||||
onOpenDetail: (task: TaskDetail) => void;
|
||||
addToast: (message: string, type?: ToastType) => void;
|
||||
engineStopped?: boolean;
|
||||
}
|
||||
|
||||
export function TaskCard({ task, queued, onOpenDetail, addToast }: TaskCardProps) {
|
||||
export function TaskCard({ task, queued, onOpenDetail, addToast, engineStopped }: TaskCardProps) {
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [fileDragOver, setFileDragOver] = useState(false);
|
||||
|
||||
@@ -90,7 +91,7 @@ export function TaskCard({ task, queued, onOpenDetail, addToast }: TaskCardProps
|
||||
|
||||
const isFailed = task.status === "failed";
|
||||
const isPaused = task.paused === true;
|
||||
const isAgentActive = !queued && !isFailed && !isPaused && (task.column === "in-progress" || ACTIVE_STATUSES.has(task.status as string));
|
||||
const isAgentActive = !engineStopped && !queued && !isFailed && !isPaused && (task.column === "in-progress" || ACTIVE_STATUSES.has(task.status as string));
|
||||
const isDraggable = !queued && !isPaused;
|
||||
const cardClass = `card${dragging ? " dragging" : ""}${queued ? " queued" : ""}${isAgentActive ? " agent-active" : ""}${isFailed ? " failed" : ""}${isPaused ? " paused" : ""}${fileDragOver ? " file-drop-target" : ""}`;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ interface WorktreeGroupProps {
|
||||
queuedTasks: Task[];
|
||||
onOpenDetail: (task: TaskDetail) => void;
|
||||
addToast: (message: string, type?: ToastType) => void;
|
||||
engineStopped?: boolean;
|
||||
}
|
||||
|
||||
export function WorktreeGroup({
|
||||
@@ -17,6 +18,7 @@ export function WorktreeGroup({
|
||||
queuedTasks,
|
||||
onOpenDetail,
|
||||
addToast,
|
||||
engineStopped,
|
||||
}: WorktreeGroupProps) {
|
||||
return (
|
||||
<div className="worktree-group">
|
||||
@@ -27,7 +29,7 @@ export function WorktreeGroup({
|
||||
<span className="worktree-label">{label}</span>
|
||||
</div>
|
||||
{activeTasks.map((task) => (
|
||||
<TaskCard key={task.id} task={task} onOpenDetail={onOpenDetail} addToast={addToast} />
|
||||
<TaskCard key={task.id} task={task} onOpenDetail={onOpenDetail} addToast={addToast} engineStopped={engineStopped} />
|
||||
))}
|
||||
{queuedTasks.map((task) => (
|
||||
<TaskCard
|
||||
@@ -36,6 +38,7 @@ export function WorktreeGroup({
|
||||
queued
|
||||
onOpenDetail={onOpenDetail}
|
||||
addToast={addToast}
|
||||
engineStopped={engineStopped}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,7 @@ function renderBoard() {
|
||||
onNewTask={noop}
|
||||
autoMerge={false}
|
||||
onToggleAutoMerge={noop}
|
||||
engineStopped={false}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ import type { Column } from "@kb/core";
|
||||
const ACTIVE_STATUSES = new Set(["planning", "researching", "executing", "finalizing", "merging", "specifying"]);
|
||||
|
||||
/** Mirrors the cardClass computation from TaskCard.tsx */
|
||||
function computeCardClass(opts: { dragging?: boolean; queued?: boolean; status?: string; column?: Column }): string {
|
||||
const { dragging = false, queued = false, status, column = "todo" } = opts;
|
||||
function computeCardClass(opts: { dragging?: boolean; queued?: boolean; status?: string; column?: Column; engineStopped?: boolean }): string {
|
||||
const { dragging = false, queued = false, status, column = "todo", engineStopped } = opts;
|
||||
const isFailed = status === "failed";
|
||||
const isAgentActive = !queued && !isFailed && (column === "in-progress" || ACTIVE_STATUSES.has(status as string));
|
||||
const isAgentActive = !engineStopped && !queued && !isFailed && (column === "in-progress" || ACTIVE_STATUSES.has(status as string));
|
||||
return `card${dragging ? " dragging" : ""}${queued ? " queued" : ""}${isAgentActive ? " agent-active" : ""}${isFailed ? " failed" : ""}`;
|
||||
}
|
||||
|
||||
@@ -96,6 +96,35 @@ describe("TaskCard agent-active class", () => {
|
||||
expect(cls).not.toContain("agent-active");
|
||||
expect(cls).toContain("failed");
|
||||
});
|
||||
|
||||
// engineStopped tests
|
||||
|
||||
it("does NOT apply agent-active when engineStopped is true with active status", () => {
|
||||
for (const status of ["planning", "researching", "executing", "finalizing", "merging", "specifying"]) {
|
||||
const cls = computeCardClass({ status, engineStopped: true });
|
||||
expect(cls).not.toContain("agent-active");
|
||||
}
|
||||
});
|
||||
|
||||
it("does NOT apply agent-active when engineStopped is true for in-progress column", () => {
|
||||
const cls = computeCardClass({ column: "in-progress", engineStopped: true });
|
||||
expect(cls).not.toContain("agent-active");
|
||||
});
|
||||
|
||||
it("does NOT apply agent-active when engineStopped is true with active status and in-progress column", () => {
|
||||
const cls = computeCardClass({ column: "in-progress", status: "executing", engineStopped: true });
|
||||
expect(cls).not.toContain("agent-active");
|
||||
});
|
||||
|
||||
it("applies agent-active when engineStopped is false with active status", () => {
|
||||
const cls = computeCardClass({ status: "executing", engineStopped: false });
|
||||
expect(cls).toContain("agent-active");
|
||||
});
|
||||
|
||||
it("applies agent-active when engineStopped is undefined (backward compat)", () => {
|
||||
const cls = computeCardClass({ status: "executing", engineStopped: undefined });
|
||||
expect(cls).toContain("agent-active");
|
||||
});
|
||||
});
|
||||
|
||||
describe("TaskCard failed status", () => {
|
||||
|
||||
Reference in New Issue
Block a user