fix(KB-181): preserve active-agent glow during soft-pause

- Replace combined engineStopped prop with globalPaused to decouple hard stop from soft pause
- Thread globalPaused through Board, Column, WorktreeGroup, and TaskCard components
- Active-agent glow now only suppressed on hard pause (globalPaused), not soft pause (enginePaused)
- Update tests to verify soft-pause preserves glow and hard-pause suppresses it
- Add changeset for patch release
This commit is contained in:
Dustin Byrne
2026-03-28 17:26:49 -04:00
parent 6ff64597d8
commit acb246a659
8 changed files with 46 additions and 32 deletions

View File

@@ -0,0 +1,5 @@
---
"@dustinbyrne/kb": patch
---
Fix active agent glow disappearing when scheduling is soft-paused

View File

@@ -91,8 +91,6 @@ function AppInner() {
const handleDetailClose = useCallback(() => setDetailTask(null), []); const handleDetailClose = useCallback(() => setDetailTask(null), []);
const engineStopped = globalPaused || enginePaused;
return ( return (
<> <>
<Header <Header
@@ -114,7 +112,7 @@ function AppInner() {
onNewTask={handleCreateOpen} onNewTask={handleCreateOpen}
autoMerge={autoMerge} autoMerge={autoMerge}
onToggleAutoMerge={handleToggleAutoMerge} onToggleAutoMerge={handleToggleAutoMerge}
engineStopped={engineStopped} globalPaused={globalPaused}
/> />
{detailTask && ( {detailTask && (
<TaskDetailModal <TaskDetailModal

View File

@@ -15,10 +15,10 @@ interface BoardProps {
onNewTask: () => void; onNewTask: () => void;
autoMerge: boolean; autoMerge: boolean;
onToggleAutoMerge: () => void; onToggleAutoMerge: () => void;
engineStopped?: boolean; globalPaused?: boolean;
} }
export function Board({ tasks, maxConcurrent, onMoveTask, onOpenDetail, addToast, isCreating, onCancelCreate, onCreateTask, onNewTask, autoMerge, onToggleAutoMerge, engineStopped }: BoardProps) { export function Board({ tasks, maxConcurrent, onMoveTask, onOpenDetail, addToast, isCreating, onCancelCreate, onCreateTask, onNewTask, autoMerge, onToggleAutoMerge, globalPaused }: BoardProps) {
return ( return (
<main className="board" id="board"> <main className="board" id="board">
{COLUMNS.map((col) => ( {COLUMNS.map((col) => (
@@ -42,7 +42,7 @@ export function Board({ tasks, maxConcurrent, onMoveTask, onOpenDetail, addToast
onMoveTask={onMoveTask} onMoveTask={onMoveTask}
onOpenDetail={onOpenDetail} onOpenDetail={onOpenDetail}
addToast={addToast} addToast={addToast}
engineStopped={engineStopped} globalPaused={globalPaused}
{...(col === "triage" ? { isCreating, onCancelCreate, onCreateTask, onNewTask } : {})} {...(col === "triage" ? { isCreating, onCancelCreate, onCreateTask, onNewTask } : {})}
{...(col === "in-review" ? { autoMerge, onToggleAutoMerge } : {})} {...(col === "in-review" ? { autoMerge, onToggleAutoMerge } : {})}
/> />

View File

@@ -22,10 +22,10 @@ interface ColumnProps {
onNewTask?: () => void; onNewTask?: () => void;
autoMerge?: boolean; autoMerge?: boolean;
onToggleAutoMerge?: () => void; onToggleAutoMerge?: () => void;
engineStopped?: boolean; globalPaused?: boolean;
} }
export function Column({ column, tasks, allTasks, maxConcurrent, onMoveTask, onOpenDetail, addToast, isCreating, onCancelCreate, onCreateTask, onNewTask, autoMerge, onToggleAutoMerge, engineStopped }: ColumnProps) { export function Column({ column, tasks, allTasks, maxConcurrent, onMoveTask, onOpenDetail, addToast, isCreating, onCancelCreate, onCreateTask, onNewTask, autoMerge, onToggleAutoMerge, globalPaused }: ColumnProps) {
const [dragOver, setDragOver] = useState(false); const [dragOver, setDragOver] = useState(false);
const countFlashing = useFlashOnIncrease(tasks.length); const countFlashing = useFlashOnIncrease(tasks.length);
@@ -108,7 +108,7 @@ export function Column({ column, tasks, allTasks, maxConcurrent, onMoveTask, onO
queuedTasks={group.queuedTasks} queuedTasks={group.queuedTasks}
onOpenDetail={onOpenDetail} onOpenDetail={onOpenDetail}
addToast={addToast} addToast={addToast}
engineStopped={engineStopped} globalPaused={globalPaused}
/> />
)) ))
); );
@@ -117,7 +117,7 @@ export function Column({ column, tasks, allTasks, maxConcurrent, onMoveTask, onO
<div className="empty-column">No tasks</div> <div className="empty-column">No tasks</div>
) : ( ) : (
tasks.map((task) => ( tasks.map((task) => (
<TaskCard key={task.id} task={task} onOpenDetail={onOpenDetail} addToast={addToast} engineStopped={engineStopped} /> <TaskCard key={task.id} task={task} onOpenDetail={onOpenDetail} addToast={addToast} globalPaused={globalPaused} />
)) ))
)} )}
</div> </div>

View File

@@ -27,10 +27,10 @@ interface TaskCardProps {
queued?: boolean; queued?: boolean;
onOpenDetail: (task: TaskDetail) => void; onOpenDetail: (task: TaskDetail) => void;
addToast: (message: string, type?: ToastType) => void; addToast: (message: string, type?: ToastType) => void;
engineStopped?: boolean; globalPaused?: boolean;
} }
export function TaskCard({ task, queued, onOpenDetail, addToast, engineStopped }: TaskCardProps) { export function TaskCard({ task, queued, onOpenDetail, addToast, globalPaused }: TaskCardProps) {
const [dragging, setDragging] = useState(false); const [dragging, setDragging] = useState(false);
const [fileDragOver, setFileDragOver] = useState(false); const [fileDragOver, setFileDragOver] = useState(false);
@@ -91,7 +91,7 @@ export function TaskCard({ task, queued, onOpenDetail, addToast, engineStopped }
const isFailed = task.status === "failed"; const isFailed = task.status === "failed";
const isPaused = task.paused === true; const isPaused = task.paused === true;
const isAgentActive = !engineStopped && !queued && !isFailed && !isPaused && (task.column === "in-progress" || ACTIVE_STATUSES.has(task.status as string)); const isAgentActive = !globalPaused && !queued && !isFailed && !isPaused && (task.column === "in-progress" || ACTIVE_STATUSES.has(task.status as string));
const isDraggable = !queued && !isPaused; const isDraggable = !queued && !isPaused;
const cardClass = `card${dragging ? " dragging" : ""}${queued ? " queued" : ""}${isAgentActive ? " agent-active" : ""}${isFailed ? " failed" : ""}${isPaused ? " paused" : ""}${fileDragOver ? " file-drop-target" : ""}`; const cardClass = `card${dragging ? " dragging" : ""}${queued ? " queued" : ""}${isAgentActive ? " agent-active" : ""}${isFailed ? " failed" : ""}${isPaused ? " paused" : ""}${fileDragOver ? " file-drop-target" : ""}`;

View File

@@ -9,7 +9,7 @@ interface WorktreeGroupProps {
queuedTasks: Task[]; queuedTasks: Task[];
onOpenDetail: (task: TaskDetail) => void; onOpenDetail: (task: TaskDetail) => void;
addToast: (message: string, type?: ToastType) => void; addToast: (message: string, type?: ToastType) => void;
engineStopped?: boolean; globalPaused?: boolean;
} }
export function WorktreeGroup({ export function WorktreeGroup({
@@ -18,7 +18,7 @@ export function WorktreeGroup({
queuedTasks, queuedTasks,
onOpenDetail, onOpenDetail,
addToast, addToast,
engineStopped, globalPaused,
}: WorktreeGroupProps) { }: WorktreeGroupProps) {
return ( return (
<div className="worktree-group"> <div className="worktree-group">
@@ -29,7 +29,7 @@ export function WorktreeGroup({
<span className="worktree-label">{label}</span> <span className="worktree-label">{label}</span>
</div> </div>
{activeTasks.map((task) => ( {activeTasks.map((task) => (
<TaskCard key={task.id} task={task} onOpenDetail={onOpenDetail} addToast={addToast} engineStopped={engineStopped} /> <TaskCard key={task.id} task={task} onOpenDetail={onOpenDetail} addToast={addToast} globalPaused={globalPaused} />
))} ))}
{queuedTasks.map((task) => ( {queuedTasks.map((task) => (
<TaskCard <TaskCard
@@ -38,7 +38,7 @@ export function WorktreeGroup({
queued queued
onOpenDetail={onOpenDetail} onOpenDetail={onOpenDetail}
addToast={addToast} addToast={addToast}
engineStopped={engineStopped} globalPaused={globalPaused}
/> />
))} ))}
</div> </div>

View File

@@ -27,7 +27,7 @@ function renderBoard() {
onNewTask={noop} onNewTask={noop}
autoMerge={false} autoMerge={false}
onToggleAutoMerge={noop} onToggleAutoMerge={noop}
engineStopped={false} globalPaused={false}
/>, />,
); );
} }

View File

@@ -11,10 +11,10 @@ import type { Column } from "@kb/core";
const ACTIVE_STATUSES = new Set(["planning", "researching", "executing", "finalizing", "merging", "specifying"]); const ACTIVE_STATUSES = new Set(["planning", "researching", "executing", "finalizing", "merging", "specifying"]);
/** Mirrors the cardClass computation from TaskCard.tsx */ /** Mirrors the cardClass computation from TaskCard.tsx */
function computeCardClass(opts: { dragging?: boolean; queued?: boolean; status?: string; column?: Column; engineStopped?: boolean }): string { function computeCardClass(opts: { dragging?: boolean; queued?: boolean; status?: string; column?: Column; globalPaused?: boolean }): string {
const { dragging = false, queued = false, status, column = "todo", engineStopped } = opts; const { dragging = false, queued = false, status, column = "todo", globalPaused } = opts;
const isFailed = status === "failed"; const isFailed = status === "failed";
const isAgentActive = !engineStopped && !queued && !isFailed && (column === "in-progress" || ACTIVE_STATUSES.has(status as string)); const isAgentActive = !globalPaused && !queued && !isFailed && (column === "in-progress" || ACTIVE_STATUSES.has(status as string));
return `card${dragging ? " dragging" : ""}${queued ? " queued" : ""}${isAgentActive ? " agent-active" : ""}${isFailed ? " failed" : ""}`; return `card${dragging ? " dragging" : ""}${queued ? " queued" : ""}${isAgentActive ? " agent-active" : ""}${isFailed ? " failed" : ""}`;
} }
@@ -97,32 +97,43 @@ describe("TaskCard agent-active class", () => {
expect(cls).toContain("failed"); expect(cls).toContain("failed");
}); });
// engineStopped tests // globalPaused tests (hard stop suppresses glow; soft pause does not)
it("does NOT apply agent-active when engineStopped is true with active status", () => { it("does NOT apply agent-active when globalPaused is true with active status", () => {
for (const status of ["planning", "researching", "executing", "finalizing", "merging", "specifying"]) { for (const status of ["planning", "researching", "executing", "finalizing", "merging", "specifying"]) {
const cls = computeCardClass({ status, engineStopped: true }); const cls = computeCardClass({ status, globalPaused: true });
expect(cls).not.toContain("agent-active"); expect(cls).not.toContain("agent-active");
} }
}); });
it("does NOT apply agent-active when engineStopped is true for in-progress column", () => { it("does NOT apply agent-active when globalPaused is true for in-progress column", () => {
const cls = computeCardClass({ column: "in-progress", engineStopped: true }); const cls = computeCardClass({ column: "in-progress", globalPaused: true });
expect(cls).not.toContain("agent-active"); expect(cls).not.toContain("agent-active");
}); });
it("does NOT apply agent-active when engineStopped is true with active status and in-progress column", () => { it("does NOT apply agent-active when globalPaused is true with active status and in-progress column", () => {
const cls = computeCardClass({ column: "in-progress", status: "executing", engineStopped: true }); const cls = computeCardClass({ column: "in-progress", status: "executing", globalPaused: true });
expect(cls).not.toContain("agent-active"); expect(cls).not.toContain("agent-active");
}); });
it("applies agent-active when engineStopped is false with active status", () => { it("applies agent-active when globalPaused is false with active status", () => {
const cls = computeCardClass({ status: "executing", engineStopped: false }); const cls = computeCardClass({ status: "executing", globalPaused: false });
expect(cls).toContain("agent-active"); expect(cls).toContain("agent-active");
}); });
it("applies agent-active when engineStopped is undefined (backward compat)", () => { it("applies agent-active when globalPaused is undefined (backward compat)", () => {
const cls = computeCardClass({ status: "executing", engineStopped: undefined }); const cls = computeCardClass({ status: "executing", globalPaused: undefined });
expect(cls).toContain("agent-active");
});
it("applies agent-active when only soft-paused (globalPaused is false)", () => {
// Soft pause (enginePaused) should NOT suppress the glow — only globalPaused matters
const cls = computeCardClass({ status: "executing", globalPaused: false });
expect(cls).toContain("agent-active");
});
it("applies agent-active for in-progress column when only soft-paused", () => {
const cls = computeCardClass({ column: "in-progress", globalPaused: false });
expect(cls).toContain("agent-active"); expect(cls).toContain("agent-active");
}); });
}); });