mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-09 07:10:34 +01:00
fix(ui): resolve task status glyphs through one shared mapping (BUG-07)
The same task state rendered different status icons across views. A blocked
task covered by an active sub-task ("in queue") showed the blue queued glyph
in the task header and status trigger, but the status menu's current row, the
chat chip and the coordinator sidebar recents/project rows showed the red
blocked glyph, because those surfaces dropped blockerAttention (and some
dropped externalConversationState) before calling StatusIcon.
- Add ui/src/lib/task-status-presentation.ts: one resolver for glyph, label
and accessible copy, plus taskStatusIconProps() so call sites pass every
presentation field from a task record.
- StatusIcon and IssueStatusBadge use the resolver; the picker draws the
current row with the trigger's presentation.
- Task call sites (sidebar, lists, inbox, properties, pills, search, chat
chips, status cards) spread taskStatusIconProps.
- /issues/summaries returns blockerAttention for blocked rows; recent-task
snapshots store and refresh it; sidebar project rows use the rows projection.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
03eb49053f
commit
2c0cc8ee64
37 files changed
+418
-130
No files matched your search
@@ -4,7 +4,7 @@ import request from "supertest";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { afterAll, beforeAll, expect, it, vi } from "vitest";
|
||||
import {
|
||||
agents, chatConversations, chatEndpoints, issues, toolApplications, toolConnections,
|
||||
agents, chatConversations, chatEndpoints, heartbeatRuns, issueRelations, issues, toolApplications, toolConnections,
|
||||
} from "@paperclipai/db";
|
||||
import { issueRoutes } from "../routes/issues.js";
|
||||
import * as issueServices from "../services/issues.js";
|
||||
@@ -110,4 +110,22 @@ describeEmbeddedPostgres("bounded issue summaries", () => {
|
||||
expect(active.externalConversationState).toBe("active");
|
||||
expect(active.updatedAt).toBe(waiting.updatedAt);
|
||||
});
|
||||
|
||||
it("projects the covered-blocker state so every task view picks the same queued glyph (BUG-07)", async () => {
|
||||
const f = await fixture();
|
||||
const [agent] = await ctx.db.insert(agents).values({ companyId: f.companyId, name: "Builder", role: "engineer", adapterType: "process" }).returning();
|
||||
const [parent] = await ctx.db.insert(issues).values({ companyId: f.companyId, title: "Parent", status: "blocked" }).returning();
|
||||
const [child] = await ctx.db.insert(issues).values({
|
||||
companyId: f.companyId, title: "Running child", identifier: "SUM-9", status: "todo", parentId: parent!.id, assigneeAgentId: agent!.id,
|
||||
}).returning();
|
||||
await ctx.db.insert(issueRelations).values({ companyId: f.companyId, issueId: child!.id, relatedIssueId: parent!.id, type: "blocks" });
|
||||
const runId = randomUUID();
|
||||
await ctx.db.insert(heartbeatRuns).values({ id: runId, companyId: f.companyId, agentId: agent!.id, status: "running", contextSnapshot: { issueId: child!.id } });
|
||||
await ctx.db.update(issues).set({ executionRunId: runId }).where(eq(issues.id, child!.id));
|
||||
|
||||
const res = await request(app(f.actor)).get(url(f.companyId)).query({ ids: `${parent!.id},${f.rows[0]!.id}` }).expect(200);
|
||||
expect(res.body[0].blockerAttention).toMatchObject({ state: "covered", reason: "active_child", sampleBlockerIdentifier: "SUM-9" });
|
||||
// Unblocked tasks keep the bounded summary shape.
|
||||
expect(res.body[1]).not.toHaveProperty("blockerAttention");
|
||||
});
|
||||
});
|
||||
+21
-11
@@ -12,7 +12,7 @@ import { getHeartbeatRunReadDecisions, redactHeartbeatRunListRow } from "../serv
|
||||
import { taskInterruptionPresentation, taskInterruptionPresentations } from "../services/task-interruption-presentation.js";
|
||||
import { parseObject } from "../adapters/utils.js";
|
||||
import { prepareRestrictedWorkspaceRetry } from "../services/restricted-workspace-retry.js";
|
||||
import { issueSummariesQuerySchema, type IssueSummary } from "@paperclipai/shared";
|
||||
import { issueSummariesQuerySchema, type IssueBlockerAttention, type IssueSummary } from "@paperclipai/shared";
|
||||
import { listIssueSummaries } from "../services/issue-summaries.js";
|
||||
import { assertNoCaseHandoffMutation } from "../services/heartbeat/case-handoff-lifecycle.js";
|
||||
import { assertLegacyRunTaskOwnership } from "../legacy-run-task-ownership.js";
|
||||
@@ -8418,16 +8418,26 @@ export function issueRoutes(
|
||||
}
|
||||
const rows = await listIssueSummaries(db, companyId, parsed.data.ids, await issueReadSqlCondition(db, req.actor));
|
||||
const visible = await filterIssuesForActor(req, rows);
|
||||
const summaries: IssueSummary[] = visible.map((row) => ({
|
||||
id: row.id,
|
||||
companyId: row.companyId,
|
||||
title: row.title,
|
||||
identifier: row.identifier,
|
||||
status: row.status as IssueSummary["status"],
|
||||
displayStatus: row.displayStatus,
|
||||
updatedAt: row.updatedAt,
|
||||
externalConversationState: row.externalConversationState,
|
||||
}));
|
||||
// Blocked rows carry their blocker state so every task view resolves the
|
||||
// same glyph (covered → "in queue") as the task pane. At most five IDs.
|
||||
const blockedRows = visible.filter((row) => row.status === "blocked");
|
||||
const blockerAttention = blockedRows.length > 0
|
||||
? await svc.listBlockerAttention(companyId, blockedRows)
|
||||
: new Map<string, IssueBlockerAttention>();
|
||||
const summaries: IssueSummary[] = visible.map((row) => {
|
||||
const attention = blockerAttention.get(row.id);
|
||||
return {
|
||||
id: row.id,
|
||||
companyId: row.companyId,
|
||||
title: row.title,
|
||||
identifier: row.identifier,
|
||||
status: row.status as IssueSummary["status"],
|
||||
displayStatus: row.displayStatus,
|
||||
updatedAt: row.updatedAt,
|
||||
externalConversationState: row.externalConversationState,
|
||||
...(attention ? { blockerAttention: attention } : {}),
|
||||
};
|
||||
});
|
||||
res.setHeader("Cache-Control", "private, no-store");
|
||||
res.json(summaries);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user