fix(dashboard): restore step/comment fields on slim list and detail tabs

The previous slim listing dropped log, comments, steps,
workflowStepResults, and steeringComments from the board task payload.
Of those, only `log` is actually heavy (~60 MB across 1200 tasks);
everything else combined is under 500 KB and is needed by the board UI:

- TaskCard step progress badge reads task.steps
- TaskCard comment count badge reads task.comments
- Workflow status indicators read task.workflowStepResults

Slim mode now drops *only* log. The other JSON columns stay in board
payloads, so progress bars and badges render again without forcing a
full per-task fetch.

Also fix the TaskDetailModal regression where the Activity tab and the
Step Progress section read task.log/task.steps from the slim board prop
instead of workingTask (the full row loaded via fetchTaskDetail). The
prop is the cached slim row from the board, so the activity tab was
empty until the user scrolled — now both tabs read workingTask.

Updated the slim listTasks regression test to assert the new contract:
log is dropped, but steps/comments/workflowStepResults/steeringComments
match the full row.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-10 20:46:04 -07:00
parent 402eac6cb4
commit 37c91e806d
3 changed files with 27 additions and 9 deletions

View File

@@ -1323,7 +1323,7 @@ describe("TaskStore", () => {
expect(paged[0].id).toBe("FN-002"); expect(paged[0].id).toBe("FN-002");
}); });
it("slim mode returns metadata but drops heavy fields (log/comments/steps)", async () => { it("slim mode drops the agent log but keeps board-visible fields (steps/comments)", async () => {
const task = await store.createTask({ description: "Slim test" }); const task = await store.createTask({ description: "Slim test" });
await store.logEntry(task.id, "heavy log entry that should not appear in slim list"); await store.logEntry(task.id, "heavy log entry that should not appear in slim list");
@@ -1333,13 +1333,22 @@ describe("TaskStore", () => {
const full = fullList.find((t) => t.id === task.id)!; const full = fullList.find((t) => t.id === task.id)!;
const slim = slimList.find((t) => t.id === task.id)!; const slim = slimList.find((t) => t.id === task.id)!;
// Sanity: the full row really has the log we wrote.
expect(full.log.length).toBeGreaterThan(0); expect(full.log.length).toBeGreaterThan(0);
// Slim must drop the heavy log payload (the only field worth slimming).
expect(slim.id).toBe(task.id); expect(slim.id).toBe(task.id);
expect(slim.description).toBe("Slim test"); expect(slim.description).toBe("Slim test");
expect(slim.column).toBe(full.column); expect(slim.column).toBe(full.column);
expect(slim.log).toEqual([]); expect(slim.log).toEqual([]);
expect(slim.steps).toEqual([]);
expect(slim.comments).toBeUndefined(); // Slim must STILL include the small JSON columns the board UI reads:
// step progress, comment counts, workflow status, steering badges.
// (Dropping them silently broke TaskCard progress bars and the comments tab.)
expect(slim.steps).toEqual(full.steps);
expect(slim.comments).toEqual(full.comments);
expect(slim.workflowStepResults).toEqual(full.workflowStepResults);
expect(slim.steeringComments).toEqual(full.steeringComments);
}); });
it("includeArchived=false excludes archived tasks; default includes them", async () => { it("includeArchived=false excludes archived tasks; default includes them", async () => {

View File

@@ -1323,6 +1323,15 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
const slim = options?.slim ?? false; const slim = options?.slim ?? false;
const columnFilter = options?.column; const columnFilter = options?.column;
// Slim mode drops ONLY the agent log column. On busy boards `log` accounts
// for ~99% of the row payload (60+ MB across 1200 tasks); every other JSON
// column combined is under 500 KB and is needed by the board UI:
// - `steps` → step progress badge on TaskCard
// - `comments` → comment count badge on TaskCard
// - `workflowStepResults` → workflow status indicators
// - `steeringComments` → steering badge
// Use `getTask(id)` to load the full row (including `log`) for the
// TaskDetailModal's Activity tab and Agent Log subview.
const slimColumns = ` const slimColumns = `
id, title, description, "column", status, size, reviewLevel, currentStep, id, title, description, "column", status, size, reviewLevel, currentStep,
worktree, blockedBy, paused, baseBranch, branch, baseCommitSha, worktree, blockedBy, paused, baseBranch, branch, baseCommitSha,
@@ -1332,7 +1341,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
mergeRetries, stuckKillCount, recoveryRetryCount, nextRecoveryAt, mergeRetries, stuckKillCount, recoveryRetryCount, nextRecoveryAt,
error, summary, thinkingLevel, error, summary, thinkingLevel,
createdAt, updatedAt, columnMovedAt, createdAt, updatedAt, columnMovedAt,
dependencies, dependencies, steps, comments, workflowStepResults, steeringComments,
attachments, prInfo, issueInfo, mergeDetails, attachments, prInfo, issueInfo, mergeDetails,
breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles,
missionId, sliceId, assignedAgentId, assigneeUserId, missionId, sliceId, assignedAgentId, assigneeUserId,

View File

@@ -1083,9 +1083,9 @@ export function TaskDetailModal({
) : ( ) : (
<div className="detail-activity"> <div className="detail-activity">
<h4>Activity</h4> <h4>Activity</h4>
{task.log && task.log.length > 0 ? ( {workingTask.log && workingTask.log.length > 0 ? (
<div className="detail-activity-list"> <div className="detail-activity-list">
{[...task.log].reverse().map((entry, i) => ( {[...workingTask.log].reverse().map((entry, i) => (
<div key={i} className="detail-log-entry"> <div key={i} className="detail-log-entry">
<div className="detail-log-header"> <div className="detail-log-header">
<span className="detail-log-timestamp"> <span className="detail-log-timestamp">
@@ -1180,10 +1180,10 @@ export function TaskDetailModal({
</div> </div>
<div className="detail-section detail-step-progress"> <div className="detail-section detail-step-progress">
<h4>Progress</h4> <h4>Progress</h4>
{task.steps && task.steps.length > 0 ? ( {workingTask.steps && workingTask.steps.length > 0 ? (
<div className="step-progress-wrapper"> <div className="step-progress-wrapper">
<div className="step-progress-bar"> <div className="step-progress-bar">
{task.steps.map((step, index) => ( {workingTask.steps.map((step, index) => (
<div <div
key={index} key={index}
className={`step-progress-segment step-progress-segment--${step.status}`} className={`step-progress-segment step-progress-segment--${step.status}`}
@@ -1193,7 +1193,7 @@ export function TaskDetailModal({
))} ))}
</div> </div>
<span className="step-progress-label"> <span className="step-progress-label">
{task.steps.filter(s => s.status === "done").length}/{task.steps.length} steps {workingTask.steps.filter(s => s.status === "done").length}/{workingTask.steps.length} steps
</span> </span>
</div> </div>
) : ( ) : (