Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

feat(tasks): add Type facet to task board filters, backed by playbook (AOP)

merged#2801CedarCopilot

CedarCopilot wants to merge 2 commits into staging from feat/task-board-type-filter

Live on prod, no production signal yetTimeline and evidence
  1. Opened
    Sep 16, 2026, 6:38 AM
  2. Sep 16, 2026, 7:34 AM
  3. Merged
    Sep 17, 2026, 2:34 PM
  4. Live on prod
    Sep 17, 2026, 2:34 PM
  5. Observed 22 hours
    Sep 17, 2026, 2:34 PM
  6. Pipelines steady after this deploy
    Sep 17, 2026, 2:34 PM
  7. Unobserved

    Live on prod, no production signal yet

    Sep 18, 2026, 12:50 PM

Behaviors Libra is checking

Type selections filter the task list, kanban board, and task-execution sidebar, with multiple selected types matching as OR and Type combined with other facets as AND.Inconclusivehigh confidence

No production telemetry since 2026-09-17T21:34:54Z positively identifies the changed task-view behavior. Queries found 0 spans for userTasks/listUserTasks, filterTypes, TaskListView, or TaskKanbanBoard and 0 matching CloudWatch log lines. Generic task telemetry was unrelated (1.

prod, checked Sep 18, 2026, 9:53 AM
Type option counts, the selected-filter badge, and the clear-all action include the Type facet and remain consistent with the other active filters.Inconclusivelow confidence

Verification reached its 130s outer budget before the verifier returned. Libra is keeping this intent verifying until the next check.

prod, checked Sep 18, 2026, 10:51 AM
The task filter menu offers a Type facet with selectable playbooks from the user's AOPs plus playbooks observed on loaded tasks, and a No type option for tasks without an assigned playbook.Inconclusivelow confidence

Verification reached its 120s wall-clock budget before a tied verdict. Libra is keeping this intent verifying until the next check.

prod, checked Sep 18, 2026, 10:53 AM
Selected playbook types persist in the URL as the `filterTypes` query parameter, rehydrate on navigation or reload, and remove the parameter when no types remain selected.Inconclusivelow confidence

Unable to complete the verification within the available tool loop.

prod, checked Sep 18, 2026, 11:50 AM
Hydrating a batch of task conversations resolves all distinct playbook names with one batched `agentOperatingProcedures` lookup instead of querying once per conversation.Inconclusivelow confidence

Unable to verify the behavior from the available telemetry: no matching OTEL spans or CloudWatch log lines were found for hydrateTaskConversations, agentOperatingProcedures, or the user-tasks surface after the deploy, and source inspection was constrained by the read budget.

prod, checked Sep 18, 2026, 11:51 AM
Task conversation hydration exposes the deal's playbook ID and name as `aopId` and `aopName`, including tasks on teammate-owned deals and null values for unassigned deals.Inconclusivelow confidence

Unable to complete verification: telemetry returned no matching OTEL spans, and the route source could not be read due to the source budget limit.

prod, checked Sep 18, 2026, 12:50 PM

Libra has verdicts on 0 of 6 tracked behaviors on prod; 6 are still being checked. Libra checks hourly for 3 days after each deploy.

Summary

  • Adds a fifth Type facet to the task board's filter menu (TaskFilterMenu), alongside the existing Task group / Channel / Deal stage / CRM sync facets, filtering tasks by which playbook (AOP) their underlying deal belongs to.
  • Follows the exact Deal stage precedent: the facet keys off a field already hydrated onto the task's conversation object (aopId), rather than a hardcoded static option list.
  • Options are fetched dynamically per-user, the same way the Task group facet does, via useAOPs() , the same hook CRMFilterBar.tsx's own "Type" (AOP) multi-select already uses, so there is only ever one AOP-list query in the app rather than a second, competing fetch.
  • This is a general Cedar feature (any user with multiple AOPs can use it), not specific to any one customer.

Why

A customer is about to split their pipeline into two playbooks by deal origin. They need to filter their task board by playbook independently of the existing pipeline filter, and this should work for any user with more than one AOP.

Changes

  1. apps/server/src/trpc/routes/user-tasks.ts

    • TaskConversationHeader gains aopId: string | null and aopName: string | null.
    • hydrateTaskConversations() selects crmConversations.aopId, then resolves the distinct AOP ids in the batch to their names via a small parallel query (agentOperatingProcedures), matching the file's existing Promise.all([priorityOptions, openTasks]) idiom.
    • conversationHeaders needed no changes , it shares hydrateTaskConversations and inherits the new fields automatically.
  2. apps/mail/modules/userTasks/utils/task-filters.ts

    • FilterableTask.conversation gains aopId?: string | null.
    • New NO_TYPE_KEY constant ('__no_type__'), mirroring NO_STATUS_KEY.
    • TaskFilters.filterTypes: string[], a new 'types' member on TaskFacet/TASK_FACETS, and a types entry in FACET_DEFS (keyOf: (t) => t.conversation?.aopId ?? NO_TYPE_KEY).
    • makeTaskFilter / taskFacetCounts needed no changes , confirmed they're fully generic over TASK_FACETS.
  3. apps/mail/modules/userTasks/components/TaskFilterMenu.tsx

    • New "Type" FacetSubmenu, sourced from useAOPs() (not a hardcoded array like STAGE_OPTIONS/CRM_OPTIONS), with a trailing "No type" option using NO_TYPE_KEY.
    • Docstring updated from "four filter facets" to "five".
  4. apps/mail/modules/userTasks/hooks/use-task-list-view-options.ts

    • filterTypes added as a URL-persisted param (filterTypes), following the exact pattern of filterCrm/filterStatuses.
  5. TaskKanbanBoard.tsx / TaskListView.tsx / use-task-group-buckets.ts

    • Threaded filterTypes through the three existing makeTaskFilter({...}) call sites, which the compiler caught as newly required once TaskFilters.filterTypes became non-optional.

Test plan

  • pnpm --filter @zero/server run types , clean.
  • pnpm --filter @zero/mail run types , clean for every file touched by this diff (remaining failures are pre-existing generated-file errors , @/paraglide/*, ./+types/* , unrelated to this change, present in a fresh worktree before any codegen step runs).
  • Manually: on an account with 2+ AOPs, open the task board, open Filter → Type, confirm each AOP appears with its own colour dot and a live count, confirm "No type" catches tasks whose deal has no AOP, confirm selecting one or more types filters both the List and Board views consistently, and confirm the selection round-trips through the URL (filterTypes=...) and survives a refresh.
  • Manually: confirm "Clear filters" also clears the Type selection.

šŸ¤– Generated with Claude Code

https://claude.ai/code/session_019jTe7TuDHKYaFL6Ny2Z5h6

Greptile Summary

This PR hydrates task conversations with their playbook identity and adds a URL-persisted Type facet across the task l

Show production surfaces and changed-file mapping

Production surfaces

Libra has not measured any production surfaces for this change yet.

Changed files → surfaces

  • apps/mail/modules/userTasks/components/TaskFilterMenu.tsxno production surface mapped
  • apps/mail/modules/userTasks/components/TaskKanbanBoard.tsxno production surface mapped
  • apps/mail/modules/userTasks/components/TaskListView.tsxno production surface mapped
  • apps/mail/modules/userTasks/hooks/use-task-group-buckets.tsno production surface mapped
  • apps/mail/modules/userTasks/hooks/use-task-list-view-options.tsno production surface mapped
  • apps/mail/modules/userTasks/slice/userTasksSlice.tsno production surface mapped
  • apps/mail/modules/userTasks/utils/task-filters.tsno production surface mapped
  • apps/mail/tests/modules/userTasks/task-filters.test.tsno production surface mapped
  • apps/server/src/trpc/routes/user-tasks.tsno production surface mapped