diff --git a/ui/src/components/task-chat/TaskChatCompactInteractionCard.tsx b/ui/src/components/task-chat/TaskChatCompactInteractionCard.tsx
index a1b63b65801..c258d9c3aa8 100644
--- a/ui/src/components/task-chat/TaskChatCompactInteractionCard.tsx
+++ b/ui/src/components/task-chat/TaskChatCompactInteractionCard.tsx
@@ -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 ;
}
diff --git a/ui/src/components/task-chat/TaskChatItemReviewReceipt.test.tsx b/ui/src/components/task-chat/TaskChatItemReviewReceipt.test.tsx
new file mode 100644
index 00000000000..e604bb8f1e3
--- /dev/null
+++ b/ui/src/components/task-chat/TaskChatItemReviewReceipt.test.tsx
@@ -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(
+
+ ));
+ const disclosure = host.querySelector('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();
+ }
+ });
+});
diff --git a/ui/src/hooks/useTaskListCollection.test.tsx b/ui/src/hooks/useTaskListCollection.test.tsx
index 6075cd53520..6efaad4f51e 100644
--- a/ui/src/hooks/useTaskListCollection.test.tsx
+++ b/ui/src/hooks/useTaskListCollection.test.tsx
@@ -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"));
diff --git a/ui/src/hooks/useTaskListCollection.ts b/ui/src/hooks/useTaskListCollection.ts
index dbccff83b58..97a8debee7f 100644
--- a/ui/src/hooks/useTaskListCollection.ts
+++ b/ui/src/hooks/useTaskListCollection.ts
@@ -8,7 +8,7 @@ const expanded: ReadonlySet = 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;
+ /** 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));
diff --git a/ui/src/pages/CoordinatorTasks.test.tsx b/ui/src/pages/CoordinatorTasks.test.tsx
index 611b7fb2fdf..44afb53f88c 100644
--- a/ui/src/pages/CoordinatorTasks.test.tsx
+++ b/ui/src/pages/CoordinatorTasks.test.tsx
@@ -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");
diff --git a/ui/src/pages/CoordinatorTasks.tsx b/ui/src/pages/CoordinatorTasks.tsx
index 4973f82411f..e97940c0657 100644
--- a/ui/src/pages/CoordinatorTasks.tsx
+++ b/ui/src/pages/CoordinatorTasks.tsx
@@ -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")!] : [] },
});