Fix task card step progress
This commit is contained in:
@@ -1351,6 +1351,38 @@ describe("TaskStore", () => {
|
||||
expect(slim.steeringComments).toEqual(full.steeringComments);
|
||||
});
|
||||
|
||||
it("slim mode hydrates step metadata from PROMPT.md for board cards", async () => {
|
||||
const task = await store.createTask({ description: "Prompt-only steps" });
|
||||
await store.updateTask(task.id, {
|
||||
prompt: `# ${task.id}: Prompt-only steps
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 0: Update the list payload
|
||||
|
||||
- [ ] Keep card progress visible
|
||||
|
||||
### Step 1: Add regression coverage
|
||||
|
||||
- [ ] Prove slim lists still include prompt steps
|
||||
`,
|
||||
});
|
||||
|
||||
const fullList = await store.listTasks();
|
||||
const slimList = await store.listTasks({ slim: true });
|
||||
const full = fullList.find((t) => t.id === task.id)!;
|
||||
const slim = slimList.find((t) => t.id === task.id)!;
|
||||
|
||||
expect(full.steps).toEqual([]);
|
||||
expect(slim.steps).toEqual([
|
||||
{ name: "Update the list payload", status: "pending" },
|
||||
{ name: "Add regression coverage", status: "pending" },
|
||||
]);
|
||||
|
||||
const searchResults = await store.searchTasks("Prompt-only");
|
||||
expect(searchResults.find((t) => t.id === task.id)?.steps).toEqual(slim.steps);
|
||||
});
|
||||
|
||||
it("includeArchived=false excludes archived tasks; default includes them", async () => {
|
||||
const keep = await store.createTask({ description: "Stays visible" });
|
||||
const toArchive = await store.createTask({ description: "Will be archived" });
|
||||
|
||||
@@ -1360,7 +1360,15 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
const sql = `SELECT ${selectClause} FROM tasks${whereClause} ORDER BY createdAt ASC`;
|
||||
|
||||
const rows = this.db.prepare(sql).all(...params);
|
||||
const tasks = (rows as any[]).map((row) => this.rowToTask(row));
|
||||
const tasks = await Promise.all((rows as any[]).map(async (row) => {
|
||||
const task = this.rowToTask(row);
|
||||
if (!slim || task.steps.length > 0) {
|
||||
return task;
|
||||
}
|
||||
|
||||
const steps = await this.parseStepsFromPrompt(task.id);
|
||||
return steps.length > 0 ? { ...task, steps } : task;
|
||||
}));
|
||||
|
||||
// Sort by createdAt, then by numeric ID suffix for tie-breaking
|
||||
const sorted = tasks.sort((a, b) => {
|
||||
@@ -1432,7 +1440,15 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
LIMIT ${limit >= 0 ? limit : -1}${offsetClause}
|
||||
`).all(ftsQuery) as any[];
|
||||
|
||||
return rows.map((row) => this.rowToTask(row));
|
||||
return Promise.all(rows.map(async (row) => {
|
||||
const task = this.rowToTask(row);
|
||||
if (task.steps.length > 0) {
|
||||
return task;
|
||||
}
|
||||
|
||||
const steps = await this.parseStepsFromPrompt(task.id);
|
||||
return steps.length > 0 ? { ...task, steps } : task;
|
||||
}));
|
||||
}
|
||||
|
||||
async selectNextTaskForAgent(agentId: string): Promise<InboxTask | null> {
|
||||
@@ -1588,7 +1604,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
|
||||
async updateTask(
|
||||
id: string,
|
||||
updates: { title?: string; description?: string; prompt?: string; worktree?: string | null; status?: string | null; dependencies?: string[]; blockedBy?: string | null; assignedAgentId?: string | null; assigneeUserId?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; mergeRetries?: number; stuckKillCount?: number | null; recoveryRetryCount?: number | null; nextRecoveryAt?: string | null; enabledWorkflowSteps?: string[]; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; planningModelProvider?: string | null; planningModelId?: string | null; thinkingLevel?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; mergeDetails?: import("./types.js").MergeDetails | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null },
|
||||
updates: { title?: string; description?: string; prompt?: string; worktree?: string | null; status?: string | null; dependencies?: string[]; steps?: import("./types.js").TaskStep[]; blockedBy?: string | null; assignedAgentId?: string | null; assigneeUserId?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; mergeRetries?: number; stuckKillCount?: number | null; recoveryRetryCount?: number | null; nextRecoveryAt?: string | null; enabledWorkflowSteps?: string[]; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; planningModelProvider?: string | null; planningModelId?: string | null; thinkingLevel?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; mergeDetails?: import("./types.js").MergeDetails | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null },
|
||||
runContext?: RunMutationContext,
|
||||
): Promise<Task> {
|
||||
return this.withTaskLock(id, async () => {
|
||||
@@ -1634,6 +1650,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
movedToTriage = true;
|
||||
}
|
||||
}
|
||||
if (updates.steps !== undefined) task.steps = updates.steps;
|
||||
if (updates.status === null) {
|
||||
task.status = undefined;
|
||||
} else if (updates.status !== undefined) {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { GitHubBadge } from "./GitHubBadge";
|
||||
import { pickPreferredBadge } from "./TaskCardBadge";
|
||||
import { useBadgeWebSocket } from "../hooks/useBadgeWebSocket";
|
||||
import { getFreshBatchData } from "../hooks/useBatchBadgeFetch";
|
||||
import { useSessionFiles } from "../hooks/useSessionFiles";
|
||||
import { useTaskDiffStats } from "../hooks/useTaskDiffStats";
|
||||
import { isTaskStuck } from "../utils/taskStuck";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
@@ -492,15 +491,6 @@ function TaskCardComponent({
|
||||
|
||||
const liveBadgeData = badgeUpdates.get(task.id);
|
||||
|
||||
// Viewport-gated session files fetching - only fetch when card is visible
|
||||
const { files: sessionFiles, loading: sessionFilesLoading } = useSessionFiles(
|
||||
task.id,
|
||||
task.worktree,
|
||||
task.column,
|
||||
projectId,
|
||||
{ enabled: isInViewport },
|
||||
);
|
||||
|
||||
// Viewport-gated diff stats fetching - only fetch when card is visible
|
||||
const { stats: diffStats } = useTaskDiffStats(
|
||||
task.id,
|
||||
@@ -928,9 +918,7 @@ function TaskCardComponent({
|
||||
disabled={!onOpenDetailWithTab}
|
||||
>
|
||||
<Folder size={12} />
|
||||
<span>
|
||||
{sessionFilesLoading ? "Checking files…" : `${sessionFiles.length} ${sessionFiles.length === 1 ? "file" : "files"} changed`}
|
||||
</span>
|
||||
<span>View files</span>
|
||||
</button>
|
||||
)}
|
||||
{task.column === "done" && (() => {
|
||||
@@ -966,21 +954,6 @@ function TaskCardComponent({
|
||||
</button>
|
||||
);
|
||||
}
|
||||
if (task.worktree && sessionFiles.length > 0) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="card-session-files"
|
||||
onClick={handleOpenFiles}
|
||||
disabled={!onOpenDetailWithTab}
|
||||
>
|
||||
<Folder size={12} />
|
||||
<span>
|
||||
{sessionFilesLoading ? "Checking files…" : `${sessionFiles.length} ${sessionFiles.length === 1 ? "file" : "files"} changed`}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
{((task.dependencies && task.dependencies.length > 0) || queued || task.status === "queued" || task.blockedBy) && (
|
||||
|
||||
@@ -3020,7 +3020,7 @@ describe("TaskCard files-changed in done column", () => {
|
||||
expect(screen.queryByText("1 files changed")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows session files count for done column with worktree but no mergeDetails.filesChanged", () => {
|
||||
it("does not fetch session files count for done column with worktree but no mergeDetails.filesChanged", () => {
|
||||
const task = makeTask({
|
||||
column: "done",
|
||||
worktree: "/repo/.worktrees/fn-099",
|
||||
@@ -3035,7 +3035,8 @@ describe("TaskCard files-changed in done column", () => {
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("3 files changed")).toBeInTheDocument();
|
||||
expect(screen.queryByText("3 files changed")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Checking files…")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows nothing for done column without worktree, modifiedFiles, and mergeDetails.filesChanged", () => {
|
||||
@@ -3226,7 +3227,7 @@ describe("TaskCard singular/plural file count", () => {
|
||||
mockUseTaskDiffStats.mockReturnValue({ stats: null, loading: false });
|
||||
});
|
||||
|
||||
it("displays '1 file changed' (singular) for in-progress column with 1 session file", () => {
|
||||
it("shows a static files action for in-progress worktrees without fetching file counts", () => {
|
||||
const task = makeTask({
|
||||
column: "in-progress",
|
||||
worktree: "/repo/.worktrees/fn-099",
|
||||
@@ -3242,10 +3243,12 @@ describe("TaskCard singular/plural file count", () => {
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("1 file changed")).toBeInTheDocument();
|
||||
expect(screen.getByText("View files")).toBeInTheDocument();
|
||||
expect(screen.queryByText(/files? changed/)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Checking files…")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays '2 files changed' (plural) for in-progress column with 2 session files", () => {
|
||||
it("does not use session file counts for in-progress worktree cards", () => {
|
||||
const task = makeTask({
|
||||
column: "in-progress",
|
||||
worktree: "/repo/.worktrees/fn-099",
|
||||
@@ -3261,7 +3264,8 @@ describe("TaskCard singular/plural file count", () => {
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("2 files changed")).toBeInTheDocument();
|
||||
expect(screen.getByText("View files")).toBeInTheDocument();
|
||||
expect(screen.queryByText("2 files changed")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays '1 file changed' (singular) for done column with displayCount=1 via diffStats", () => {
|
||||
@@ -3303,7 +3307,7 @@ describe("TaskCard singular/plural file count", () => {
|
||||
expect(screen.queryByText("1 files changed")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays '1 file changed' (singular) for done column with sessionFiles fallback of length 1", () => {
|
||||
it("does not fetch session file counts as a done column fallback", () => {
|
||||
const task = makeTask({
|
||||
column: "done",
|
||||
worktree: "/repo/.worktrees/fn-099",
|
||||
@@ -3318,8 +3322,8 @@ describe("TaskCard singular/plural file count", () => {
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("1 file changed")).toBeInTheDocument();
|
||||
expect(screen.queryByText("1 files changed")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("1 file changed")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Checking files…")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays 'N files changed' (plural) for done column with diffStats count > 1", () => {
|
||||
|
||||
@@ -58,6 +58,7 @@ function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
||||
getAgentLogs: vi.fn().mockResolvedValue([]),
|
||||
addSteeringComment: vi.fn(),
|
||||
parseDependenciesFromPrompt: vi.fn().mockResolvedValue([]),
|
||||
parseStepsFromPrompt: vi.fn().mockResolvedValue([]),
|
||||
parseFileScopeFromPrompt: vi.fn().mockResolvedValue([]),
|
||||
on: vi.fn(),
|
||||
emit: vi.fn(),
|
||||
|
||||
@@ -1215,6 +1215,11 @@ export class TriageProcessor {
|
||||
triageLog.log(`${task.id} dependencies: ${parsedDeps.join(", ")}`);
|
||||
}
|
||||
|
||||
const parsedSteps = await this.store.parseStepsFromPrompt(task.id);
|
||||
if (parsedSteps.length > 0) {
|
||||
taskUpdates.steps = parsedSteps;
|
||||
}
|
||||
|
||||
const sizeMatch = written.match(/^\*\*Size:\*\*\s+(S|M|L)\b/m);
|
||||
if (sizeMatch) {
|
||||
taskUpdates.size = sizeMatch[1] as "S" | "M" | "L";
|
||||
|
||||
Reference in New Issue
Block a user