fix: preserve server task matches and completed review details

Keep server search authoritative while retaining local filters and local attention search. Preserve expandable verdicts and reasons in completed item reviews, including when Coordinator is enabled.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
scotttongandPaperclip committed 2026-10-28 09:55:01 -07:00
1 parent 52e75dc4d6
commit 045c22dd81
6 files changed
+78 -3

No files matched your search

@@ -1956,6 +1956,7 @@ export function TaskChatCompactInteractionCard({
if (coordinatorEnabled && presentation === "timeline" && ["accepted", "answered", "rejected"].includes(interaction.status)
&& interaction.kind !== "connection_intent"
&& interaction.kind !== "request_item_verdicts"
&& !(interaction.kind === "request_confirmation" && (interaction.payload.toolAction || interaction.payload.secretProposal || interaction.payload.connectionAuthorization))) {
return <CoordinatorInteractionReceipt interaction={interaction} />;
}
@@ -0,0 +1,41 @@
// @vitest-environment jsdom
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";
import { describe, expect, it, vi } from "vitest";
import { TooltipProvider } from "@/components/ui/tooltip";
import { ThemeProvider } from "@/context/ThemeContext";
import { completeRequestItemVerdictsInteraction } from "@/fixtures/issueThreadInteractionFixtures";
import { TaskChatInteractionCard } from "./TaskChatInteractionCard";
const preference = vi.hoisted(() => ({ enabled: false }));
vi.mock("@/hooks/useCoordinatorWorkspaceEnabled", () => ({ useCoordinatorWorkspaceEnabled: () => preference }));
describe("completed item-review receipts", () => {
it.each([false, true])("retains inspectable verdicts and reasons with Coordinator enabled=%s", enabled => {
preference.enabled = enabled;
const interaction = completeRequestItemVerdictsInteraction;
const host = document.createElement("div");
document.body.append(host);
const root = createRoot(host);
try {
flushSync(() => root.render(<TooltipProvider><ThemeProvider>
<TaskChatInteractionCard presentation="timeline" item={{ id: `interaction:${interaction.id}`, kind: "interaction", interaction }} />
</ThemeProvider></TooltipProvider>));
const disclosure = host.querySelector<HTMLDetailsElement>('details[data-testid="task-chat-interaction-receipt"]');
expect(disclosure).not.toBeNull();
expect(disclosure!.open).toBe(false);
disclosure!.querySelector("summary")!.click();
expect(disclosure!.open).toBe(true);
for (const result of interaction.result!.items) {
const label = interaction.payload.items.find(item => item.id === result.id)!.label;
const row = [...disclosure!.querySelectorAll("li")].find(item => item.textContent?.includes(label));
expect(row?.textContent).toContain(result.verdict);
if (result.reason) expect(row?.textContent).toContain(result.reason);
}
expect(host.querySelector('a[href^="#comment-interaction-response:"]')).toBeNull();
} finally {
flushSync(() => root.unmount());
host.remove();
}
});
});
@@ -79,6 +79,17 @@ describe("shared task-list collection behavior", () => {
expect(collection.matchingItems).toHaveLength(3);
});
it("trusts server search while preserving company, status, and view filters", () => {
render({ query: "document-only match", searchMode: "server" });
expect(collection.query).toBe("document-only match");
expect(collection.matchingItems.map(item => item.issue.id)).toEqual(["parent", "child", "grandchild"]);
act(() => collection.updateFilters({ statuses: ["in_review"] }));
expect(collection.matchingItems.map(item => item.issue.id)).toEqual(["child"]);
expect(collection.visibleItems.map(item => item.issue.id)).toEqual(["parent", "child"]);
act(() => { collection.updateFilters({ statuses: [] }); collection.setView("unread"); });
expect(collection.matchingItems.map(item => item.issue.id)).toEqual(["child"]);
});
it("keeps view preferences isolated and clears the active collection without losing layout", () => {
act(() => collection.setView("unread"));
act(() => collection.setLayout("board"));
+5 -3
View File
@@ -8,7 +8,7 @@ const expanded: ReadonlySet<string> = new Set();
/** Shared collection behavior for the proposed task UI. Supply accessible data;
* this hook scopes presentation, and does not replace server authorization. */
export function useTaskListCollection({ items, companyId, userId, viewStorageKey, initialQuery = "", query: controlledQuery, onQueryChange, view: controlledView, initialFilters }: {
export function useTaskListCollection({ items, companyId, userId, viewStorageKey, initialQuery = "", query: controlledQuery, onQueryChange, view: controlledView, initialFilters, searchMode = "local" }: {
items: readonly TaskListItem[];
companyId: string | null;
userId: string | null;
@@ -18,6 +18,8 @@ export function useTaskListCollection({ items, companyId, userId, viewStorageKey
onQueryChange?: (query: string) => void;
view?: TaskListView;
initialFilters?: Partial<IssueFilterState>;
/** Server results may match comments, documents, or normalized identifiers. */
searchMode?: "local" | "server";
}) {
const [queryState, setQueryState] = useState(initialQuery);
const query = controlledQuery ?? queryState;
@@ -44,8 +46,8 @@ export function useTaskListCollection({ items, companyId, userId, viewStorageKey
const normalizedQuery = query.trim().toLocaleLowerCase();
const matchingItems = companyItems.filter(item => filteredIds.has(item.issue.id)
&& matchesTaskListView(view, item.issue, Boolean(item.unread), userId)
&& [item.issue.title, item.issue.identifier, item.issue.description, item.createdBy, item.assignee, item.issue.project?.name,
...(item.issue.labels ?? []).map(label => label.name)].some(value => value?.toLocaleLowerCase().includes(normalizedQuery)));
&& (searchMode === "server" || [item.issue.title, item.issue.identifier, item.issue.description, item.createdBy, item.assignee, item.issue.project?.name,
...(item.issue.labels ?? []).map(label => label.name)].some(value => value?.toLocaleLowerCase().includes(normalizedQuery))));
const visibleItems = includeTaskListAncestors(companyItems, matchingItems);
const parentIds = new Set(companyItems.flatMap(item => item.issue.parentId ? [item.issue.parentId] : []));
const allExpanded = ![...parentIds].some(id => collapsedIds.has(id));
+19
View File
@@ -62,6 +62,25 @@ describe("Coordinator tasks using the proposed table", () => {
expect(mocks.list).toHaveBeenCalledWith("company", expect.objectContaining({ q: "launch", participantAgentId: "agent", workspaceId: "workspace", offset: 0 }), expect.anything());
expect(host.querySelector('[data-task-id="search-result"]')).not.toBeNull();
});
it.each(["phrase found only in a comment", "PAP 7", "launch summaries"])("retains server matches for %s without a second local text filter", async query => {
mocks.list.mockResolvedValue([createIssue({ id: "server-match", identifier: "PAP-7", title: "Launch summary", description: "Saved brief", companyId: "company", parentId: null })]);
await mount(`/issues?q=${encodeURIComponent(query)}`);
expect(mocks.list).toHaveBeenCalledWith("company", expect.objectContaining({ q: query }), expect.anything());
expect(host.querySelector('[data-task-id="server-match"]')).not.toBeNull();
});
it("still applies assignee filters to server search matches", async () => {
mocks.list.mockResolvedValue([
createIssue({ id: "chosen", companyId: "company", parentId: null, assigneeAgentId: "chosen-agent" }),
createIssue({ id: "other", companyId: "company", parentId: null, assigneeAgentId: "other-agent" }),
]);
await mount("/issues?q=comment-only&assignee=chosen-agent");
expect([...host.querySelectorAll("[data-task-id]")].map(node => node.getAttribute("data-task-id"))).toEqual(["chosen"]);
});
it("keeps local search for the hydrated Needs attention collection", async () => {
await mount("/issues?view=needs_attention&q=no-matching-decision");
expect(host.querySelectorAll("[data-task-id]")).toHaveLength(0);
expect(host.textContent).toContain("No tasks match this view");
});
it("keeps inaccessible or failed pending tasks visible as an error, not a false all-clear", async () => {
mocks.get.mockRejectedValue(new Error("Task unavailable"));
await mount("/issues?view=needs_attention");
+1
View File
@@ -88,6 +88,7 @@ export function CoordinatorTasks() {
assignee: issue.assigneeAgentId ? agentNames.get(issue.assigneeAgentId) ?? "Unknown agent" : users.get(issue.assigneeUserId ?? "") ?? null,
}));
const collection = useTaskListCollection({ items, companyId, userId, query, onQueryChange: setQuery, view: "all",
searchMode: needsAttention ? "local" : "server",
viewStorageKey: taskListViewStorageKey("coordinator", userId ?? "local-board"),
initialFilters: { assignees: params.get("assignee") ? [params.get("assignee")!] : [] },
});