overview.md35.2 KBView on GitHub # Conversation View Redesign
> **Detail docs:**
> - [timeline.md](./timeline.md) — Slack-style Inbox-tab timeline + universal composer (channel selector, attachments, contact picker, compose new email). Supersedes Phase 3 below.
> - [next-steps.md](./next-steps.md) — Date-grouped task blocks. Powers the per-conversation Next Steps tab, the pinned next-step card at the top of Inbox, **and** the global Agenda view in [conversations-page.md](./conversations-page.md).
> - [conversations-page.md](./conversations-page.md) — New `/conversations` page: Slack-style sidebar (conversation channels + Agenda) on the left, the redesigned ConversationView on the right.
## 1) Introduction — goal, present state, future state
We want the conversation view to feel like a focused workspace: a tight header with a company badge, title, participants, and a 3-dot menu; a single horizontal row of configurable badges (Type, Status, Priority, Deal Size, …); six top-level tabs (**Inbox, Next Steps, Agents, Files, CRM, Contacts** — in that order); a Slack-style Inbox where each event is a single row with avatar/name/time/content, a pinned next-step card at the top of the scroll region, and a persistent bottom composer that actually sends an email or Slack message. Today the page is dominated by [ConversationOverviewCard](apps/mail/modules/conversations/components/ConversationOverviewCard.tsx) — a tall header with a 4-column grid of label/value pairs, an action toolbar of icon buttons, and a 3-or-4 tab nav — sitting above [ConversationBodyLayout](apps/mail/modules/conversations/components/ConversationBodyLayout.tsx) which navigates by *expanding* sections (`__timeline`, `__enrichment`, `__files`, agent rows) one at a time via the `expandedSections` map. We will replace the card with a slim header + badges row, replace the expand-one-section navigation with six real tabs in both the `view` and `sidebar` variants, rebuild the Inbox tab as Slack-style rows with a pinned next-step card and a persistent composer, and update the AOP overview-config editor to preview the new badge row. Per-conversation next-step tasks live in the **Next Steps tab** (date-grouped block list) and as a pinned card above the Inbox timeline. A cross-conversation aggregate of those same tasks — every task assigned to me across every deal — lives in the global **Agenda view** in the Conversations sidebar (see [conversations-page.md](./conversations-page.md)).
## 2) Present state
### 2.1 Architecture diagram
```text
ConversationView (full page)
└── ConversationBodyLayout (variant='view')
├── ConversationOverviewCard
│ ├── Nav row: [Back] [↑] [↓] [Refresh] [Recalc] [Delete]
│ ├── Title row: [Company badge] [Name] [Slack] [+ Add integration] [Refresh] [Maximize]
│ ├── Tabs: Overview · CRM · Timeline · Company Details
│ └── Fields grid (4-col): Type | Owner | Status | Deal Size …
│ + DealStatusTimeline + Status Overview
└── Body (one of, chosen by expandedSections map):
├── __timeline → PastEventsTimeline (cards)
├── __enrichment → EnrichmentTabs
├── __files → FilesTreeView
├── doc/agent doc → ConversationDocView
└── default → AgentRow stack
├── TASK_AGGREGATOR (Next Steps)
├── CRM_UPDATER (Crm panel)
├── mainAgents…
└── backgroundAgents…
ConversationParticipantsPanel ← Popover, triggered from "Participants" badge inside the grid
ConversationOverviewCardEditor (in /agentOperatingProcedures + /admin) ← edits aop.displayConfig.crmCard.items
```
### 2.2 Step-by-step walkthrough
1. **Page entry** — `ConversationView` at [ConversationView.tsx:97](apps/mail/modules/crm/components/ConversationView.tsx) calls `useConversationBody('view')` and renders [ConversationDataSync](apps/mail/modules/conversations/components/ConversationDataSync.tsx) plus a `ConversationBodyLayout` with `variant="view"` once data arrives.
- `useConversationBody` at [use-conversation-body.ts:31](apps/mail/modules/conversations/hooks/use-conversation-body.ts) returns:
```ts
{
conversationId, conversationData, mainAgents, crmAgent, backgroundAgents,
expandedSections, setExpandedSections, toggleSection, expandedDocKey, activeSection,
nonAgentDocs, handleConversationUpdate, handleTriggerExecution, ...
}
```
2. **Section ↔ URL sync** — `ConversationView` translates the store's `conversationSection` string (`timeline | enrichment | files | doc/{id} | agent/{id} | crm`) ↔ `expandedSections` via `computeSection` / `decodeSectionToExpandedState` at [ConversationView.tsx:37-74](apps/mail/modules/crm/components/ConversationView.tsx).
3. **Header card** — `ConversationBodyLayout` renders `<ConversationOverviewCard variant="view" />` at [ConversationBodyLayout.tsx:306](apps/mail/modules/conversations/components/ConversationBodyLayout.tsx). The card owns three regions:
- **Nav row** at [ConversationOverviewCard.tsx:971](apps/mail/modules/conversations/components/ConversationOverviewCard.tsx) — Back, ↑/↓ nav, Refresh, Recalculate, Delete buttons.
- **Title row** at [ConversationOverviewCard.tsx:1081](apps/mail/modules/conversations/components/ConversationOverviewCard.tsx) — company badge + `EditableText` name + Slack `IntegrationProviderBadge` + "+ Add integration" `DropdownMenu` (gated on `hasSlackConnection`) + Maximize/Refresh.
- **Tab row** at [ConversationOverviewCard.tsx:1258](apps/mail/modules/conversations/components/ConversationOverviewCard.tsx) — `view` variant: Overview · CRM · Timeline · Company Details. `sidebar` variant: Agent · Overview · CRM · Timeline.
4. **Fields grid** — [ConversationOverviewCard.tsx:1274-1349](apps/mail/modules/conversations/components/ConversationOverviewCard.tsx) renders `gridFields` into a `grid-cols-[auto_1fr_auto_1fr]` two-column-pair layout. Each field is one of:
- **Cedar field** via `renderCedarField` at [ConversationOverviewCard.tsx:408](apps/mail/modules/conversations/components/ConversationOverviewCard.tsx) (cases: `type`, `lastContact`, `priority`, `dealSize`, `owner` → `ParticipantsBadge`, `status`).
- **Custom field** via `renderCustomField` at [ConversationOverviewCard.tsx:759](apps/mail/modules/conversations/components/ConversationOverviewCard.tsx) (select / date / fallback).
- Field set comes from `getOverviewConfigItems(overviewConfiguration)` at [ConversationOverviewCard.tsx:952](apps/mail/modules/conversations/components/ConversationOverviewCard.tsx) where `overviewConfiguration = aop.displayConfig.crmCard ?? DEFAULT_OVERVIEW_CONFIGURATION` (4 default items: type/owner/status/dealSize).
5. **Body selection** — `ConversationBodyLayout` picks a body region from `expandedSections` (one section at a time). Examples:
- Timeline expanded → renders `<PastEventsTimeline />` at [ConversationBodyLayout.tsx:~382](apps/mail/modules/conversations/components/ConversationBodyLayout.tsx).
- Files expanded → renders `<FilesTreeView />` ([ConversationBodyLayout.tsx:406-433](apps/mail/modules/conversations/components/ConversationBodyLayout.tsx)).
- CRM agent expanded → renders `<CrmAgentContent />` ([AgentRow.tsx:190-260](apps/mail/modules/conversations/components/AgentRow.tsx)) which itself nests another `<ConversationOverviewCard activeTab="crm" hideHeader hideTabs />` plus `NextStepsCard` + `WorkingMemoryCard`.
- Default → vertical stack of `AgentRow`s (TASK_AGGREGATOR, CRM_UPDATER, mainAgents, backgroundAgents).
6. **Timeline (current)** — `PastEventsTimeline` at [timeline/PastEventsTimeline.tsx](apps/mail/modules/conversations/components/timeline/PastEventsTimeline.tsx) maps events to `<TimelineEvent />` cards at [timeline/TimelineEvent.tsx](apps/mail/modules/conversations/components/timeline/TimelineEvent.tsx). Each card is a bordered/padded block with a type badge, contact line, date row, and an actions menu. No persistent composer; sending an email is done in a separate route via [EmailComposer](apps/mail/modules/drafting/components/email-composer.tsx).
7. **Participants** — `<ParticipantsBadge />` lives inside the fields grid as the `owner` case at [ConversationOverviewCard.tsx:615](apps/mail/modules/conversations/components/ConversationOverviewCard.tsx); clicking opens [ConversationParticipantsPanel.tsx](apps/mail/modules/conversations/components/ConversationParticipantsPanel.tsx) as a Popover.
8. **AOP overview-config editor** — `ConversationOverviewCardEditor` is rendered from `/agentOperatingProcedures` ([apps/mail/app/(routes)/agentOperatingProcedures/page.tsx](apps/mail/app/(routes)/agentOperatingProcedures/page.tsx)) and `/admin` ([apps/mail/app/(full-width)/admin/page.tsx](apps/mail/app/(full-width)/admin/page.tsx)) and writes `aop.displayConfig.crmCard.items` (`ConversationLayoutItem[]`). Its preview renders a live `<ConversationOverviewCard />`. Type at [crm/types/index.ts:217-243](apps/mail/modules/crm/types/index.ts).
9. **Send paths (today, not wired into the conversation view)**
- Email reply: `trpc.mail.send` at [apps/server/src/trpc/routes/mail.ts:1457](apps/server/src/trpc/routes/mail.ts) with `{ threadId, to[], subject, message, cc?, bcc?, emailHeaderMessageId? }`. Consumed by [EmailComposer](apps/mail/modules/drafting/components/email-composer.tsx).
- Slack message: `trpc.integrations.slack.sendMessage` at [apps/server/src/trpc/routes/integrations.ts:1314](apps/server/src/trpc/routes/integrations.ts) with `{ workspaceId, channelId, message }`. Linked channel(s) are on `conversation.integrationMetadata` entries of `type: 'slack'`.
## 3) Designed state
### 3.1 Architecture diagram
```text
ConversationView (full page)
└── ConversationBodyLayout (variant='view')
├── ConversationHeader ← NEW
│ ├── Row 1: [Company badge] [Name] ⇢ [Participants badge] [⋯]
│ │ (⋯ menu: Refresh · Recalculate Fields · Link Slack · Delete)
│ └── Row 2: ConversationBadgesRow ← NEW
│ (one horizontal row of badge pills, from aop.displayConfig.crmCard.items)
├── ConversationTabs ← NEW (six tabs)
│ Inbox · Next Steps · Agents · Files · CRM · Contacts
└── ConversationTabBody ← NEW (one tab visible at a time)
├── Inbox → SlackTimeline
│ ├── PinnedNextStepCard ← pinned at top of scroll region
│ │ (the existing NextStepsCard, summary + agenda highlights)
│ ├── SlackTimelineEvent rows (avatar · name ⇢ time / content)
│ └── UniversalComposer (TipTap, fixed bottom — see timeline.md)
├── Next Steps → TaskBlockList (per-conversation tasks, date-grouped — see next-steps.md)
├── Agents → AgentList (user-defined agents only — every `SYSTEM_AGENT_NAMES.*` row is filtered out)
├── Files → FilesTreeView (existing, promoted)
├── CRM → CrmAgentContent (existing, header/tabs hidden)
└── Contacts → ConversationParticipantsList (promoted from Popover)
ConversationOverviewCardEditor preview ← renders <ConversationBadgesRow /> instead of the old card grid
ConversationView removes computeSection / decodeSectionToExpandedState
conversationSection (store + URL sync) now stores a tab key=[redacted] | 'nextSteps' | 'agents' | 'files' | 'crm' | 'contacts'
```
### 3.2 Step-by-step walkthrough
1. **Entry unchanged** — `ConversationView` at [ConversationView.tsx:97](apps/mail/modules/crm/components/ConversationView.tsx) still calls `useConversationBody('view')` but no longer translates `expandedSections`; the section ↔ map helpers are deleted. Escape handler keeps current behavior (close the conversation).
2. **Store/URL sync** — `conversationSection` becomes a tab key. `ConversationOpenUrlSync` (existing) writes one of `inbox | nextSteps | agents | files | crm | contacts`; default = `inbox`. Legacy values (`timeline`, `enrichment`, `doc/*`, `agent/*`) fall back to `inbox`; legacy `nextSteps` keeps its meaning.
3. **`ConversationHeader`** at [apps/mail/modules/conversations/components/ConversationHeader.tsx](apps/mail/modules/conversations/components/ConversationHeader.tsx) — props `{ conversation, company, onRefresh, onRecalculate, onDelete, onOpenParticipants, hideBack? }`.
- Row 1 left: `<CompanyMergeDropdown />` badge (reuse from [ConversationOverviewCard.tsx:1086](apps/mail/modules/conversations/components/ConversationOverviewCard.tsx)) + `<EditableText />` for `conversation.name`.
- Row 1 right: `<ParticipantsBadge />` (extracted from the existing `owner` case) + `<ConversationActionMenu />` (3-dot `DropdownMenu`).
- Row 2: `<ConversationBadgesRow />`.
- No nav arrows, no Add Integration button, no inline Maximize.
4. **`ConversationActionMenu`** at [apps/mail/modules/conversations/components/ConversationActionMenu.tsx](apps/mail/modules/conversations/components/ConversationActionMenu.tsx) — Items: **Refresh conversation** (`onRefresh`), **Recalculate custom fields** (`onRecalculate`), **Link Slack channel** (opens `SlackChannelLinkDialog` when `hasSlackConnection`), **Delete conversation** (destructive). The delete confirm dialog stays in `ConversationView`.
5. **`ConversationBadgesRow`** at [apps/mail/modules/conversations/components/ConversationBadgesRow.tsx](apps/mail/modules/conversations/components/ConversationBadgesRow.tsx) — accepts `{ conversationData, aop, statusOptions, priorityOptions, customFieldDefinitions }`, calls `getOverviewConfigItems(aop?.displayConfig?.crmCard ?? DEFAULT_OVERVIEW_CONFIGURATION)` and renders each item as one inline badge with `Label: <Badge/>` (e.g. `Type: deal`, `Status: active`, `Priority: medium`, `Deal Size: $25k`, `Last Contact: 2d ago`). Each badge keeps its existing editor (DropdownMenu / Popover) on click. Renders horizontally with `flex flex-wrap items-center gap-x-3 gap-y-1`. The cedar `owner` case is **excluded** here — participants live in the header right.
6. **`ConversationTabs`** at [apps/mail/modules/conversations/components/ConversationTabs.tsx](apps/mail/modules/conversations/components/ConversationTabs.tsx) — controlled `<TabsList>` with six `<TabsTrigger>`s (Inbox · Next Steps · Agents · Files · CRM · Contacts) wired to the `conversationSection` store key. Keyboard nav with the existing `setConversationOverviewTabNav` plumbing from [conversation-overview-hotkeys.ts](apps/mail/modules/conversations/utils/conversation-overview-hotkeys.ts).
7. **`ConversationTabBody`** at [apps/mail/modules/conversations/components/ConversationTabBody.tsx](apps/mail/modules/conversations/components/ConversationTabBody.tsx) — switches on the active tab:
- `inbox` → `<SlackTimeline />` — the Slack-style event stream (see [timeline.md](./timeline.md)). The extracted `<NextStepsCard />` is mounted as a pinned card at the top of the scroll region inside `SlackTimeline`, above the events.
- `nextSteps` → `<TaskBlockList tasks={conversationData.data.userTasks ?? []} conversationId={…} />` — per-conversation date-grouped task blocks (see [next-steps.md](./next-steps.md)).
- `agents` → `<AgentList agents={[...mainAgents, ...backgroundAgents].filter((a) => !Object.values(SYSTEM_AGENT_NAMES).includes(a.name))} />` reusing `AgentRowContent`. User-defined agents only — every system agent (`CRM_UPDATER`, `TASK_AGGREGATOR`, `OVERVIEW_AGENT`, `MEETING_PREP`, `POST_EVENT_TASK_EXECUTOR`, plus any future additions to `SYSTEM_AGENT_NAMES`) is filtered out. CRM has its own tab; per-conversation tasks live in the Next Steps tab and as a pinned card inside Inbox; the cross-conversation Agenda lives in the Conversations sidebar (see [conversations-page.md](./conversations-page.md)).
- `files` → existing `<FilesTreeView />`
- `crm` → existing `<CrmAgentContent hideHeader hideTabs />`
- `contacts` → `<ConversationParticipantsList conversationId currentUserId />` (the body of `ConversationParticipantsPanel` rendered inline instead of inside a Popover)
8. **`SlackTimeline`** at [apps/mail/modules/conversations/components/timeline/SlackTimeline.tsx](apps/mail/modules/conversations/components/timeline/SlackTimeline.tsx) — replaces `PastEventsTimeline`. Layout:
```text
┌──────────────────────────────────────────────┐
│ events scroll region (flex-1, overflow-auto) │
│ [avatar] Name 2:04 PM │
│ multi-line content/body… │
│ [avatar] Name 2:06 PM │
│ content… │
├──────────────────────────────────────────────┤
│ SlackComposer (sticky bottom) │
└──────────────────────────────────────────────┘
```
9. **`SlackTimelineEvent`** at [apps/mail/modules/conversations/components/timeline/SlackTimelineEvent.tsx](apps/mail/modules/conversations/components/timeline/SlackTimelineEvent.tsx) — one row per event:
```ts
{ id, eventType, occurredAt, contact: { name, email, avatarUrl }, summary, content, slackMessage?, emailMessage? }
```
- Grid: `grid-cols-[32px_1fr_auto]`. Avatar / name+content / time.
- Body collapsed by default for long emails (first 3 lines + expand).
- Event-type icon overlaid on avatar bottom-right (mail / hash / phone / video / note) for at-a-glance source.
- Hover reveals the existing event action menu (delete, move) from `TimelineEvent`.
10. **`SlackComposer`** at [apps/mail/modules/conversations/components/timeline/SlackComposer.tsx](apps/mail/modules/conversations/components/timeline/SlackComposer.tsx) — TipTap-based via existing [markdown-editor.tsx](apps/mail/components/markdown-editor.tsx). Target channel is derived from `conversation.integrationMetadata`:
- If a `slack` entry exists → default target = Slack. Show a small selector chip (`#channel-name ▾`) when multiple Slack channels are linked.
- Else if any `email` participant → target = Email reply on the latest email thread (`emailHeaderMessageId` + `to[]` from the most recent inbound event).
- Submit shape:
```ts
type Submit =
| { kind: 'slack'; workspaceId: string; channelId: string; markdown: string }
| { kind: 'email'; threadId: string; emailHeaderMessageId?: string; to: Sender[]; subject: string; html: string };
```
- Calls `trpc.integrations.slack.sendMessage` at [apps/server/src/trpc/routes/integrations.ts:1314](apps/server/src/trpc/routes/integrations.ts) or `trpc.mail.send` at [apps/server/src/trpc/routes/mail.ts:1457](apps/server/src/trpc/routes/mail.ts).
- On success: optimistically appends an outbound event to `conversationData.data.conversation.events` and invalidates `trpc.crm.getConversation`.
- `Enter` to send, `Shift+Enter` for newline (matches Slack); a small `Send` button is always visible.
11. **Sidebar variant** — `ConversationBodyLayout` rendered with `variant="sidebar"` (used by [ConversationBodyContent.tsx](apps/mail/modules/conversations/components/ConversationBodyContent.tsx)) gets the same `ConversationHeader` + `ConversationTabs` + `ConversationTabBody`. The current per-agent expandable stack is removed; agents live in the Agents tab.
12. **AOP overview-config editor preview** — `ConversationOverviewCardEditor` swaps its live preview from `<ConversationOverviewCard />` to `<ConversationBadgesRow />`. The underlying `ConversationOverviewConfiguration` shape ([crm/types/index.ts:217-243](apps/mail/modules/crm/types/index.ts)) is unchanged; `owner` items in saved configs are silently filtered out of the badges row (they continue to drive the top-right participants badge).
13. **Dead code removed** — in [ConversationView.tsx](apps/mail/modules/crm/components/ConversationView.tsx): `computeSection`, `decodeSectionToExpandedState`, `expandedSections` effects, `__files_inline` defaults, the `isEscapeOwnedByOverlay` "back through sections" logic, and the "auto-expand agent folders when Files opens" effect. In [ConversationOverviewCard.tsx](apps/mail/modules/conversations/components/ConversationOverviewCard.tsx): the nav row, the inline title row, and the fields grid become unreachable; the card itself is deleted after Phase 4.
## 4) Implementation phases
### Phase 1 — Header + badges row + 3-dot menu
**Goal:** Replace the top of the conversation view with the new header (company badge · name ⇢ participants · ⋯) and a single horizontal badges row, without changing the body yet. Both `view` and `sidebar` variants.
- [x] Create `ConversationActionMenu` at [apps/mail/modules/conversations/components/ConversationActionMenu.tsx](apps/mail/modules/conversations/components/ConversationActionMenu.tsx) with items: Refresh, Recalculate Fields, Link Slack channel, Delete.
- [x] Extract `ParticipantsBadge` (currently inline in the `owner` case at [ConversationOverviewCard.tsx:615](apps/mail/modules/conversations/components/ConversationOverviewCard.tsx)) into `apps/mail/modules/conversations/components/ParticipantsBadge.tsx`.
- [x] Create `ConversationBadgesRow` at [apps/mail/modules/conversations/components/ConversationBadgesRow.tsx](apps/mail/modules/conversations/components/ConversationBadgesRow.tsx). Reuse the cedar/custom field renderers from `ConversationOverviewCard` (move them into a shared `conversationFieldRenderers.tsx`) instead of duplicating; filter out the `owner` cedar field.
- [x] Create `ConversationHeader` at [apps/mail/modules/conversations/components/ConversationHeader.tsx](apps/mail/modules/conversations/components/ConversationHeader.tsx) composing company badge, `EditableText` name, `ParticipantsBadge`, `ConversationActionMenu`, and `ConversationBadgesRow`.
- [x] Replace `<ConversationOverviewCard variant="view" />` in [ConversationBodyLayout.tsx:306](apps/mail/modules/conversations/components/ConversationBodyLayout.tsx) with `<ConversationHeader />`. Do the same for the `sidebar` variant render path.
- [x] Drop `onPrevious`, `onNext`, `canGoPrevious`, `canGoNext`, `showNavigation` props from the `ConversationView` → `ConversationBodyLayout` call site at [ConversationView.tsx:425](apps/mail/modules/crm/components/ConversationView.tsx) (and from `ConversationBodyLayout`'s prop list). Remove `handleNext`, `handlePrevious`, `currentIndex`, and `conversationList` from [ConversationView.tsx:326-337](apps/mail/modules/crm/components/ConversationView.tsx).
- [x] Remove the "+ Add integration" `DropdownMenu` block and the `showAddIntegrationDropdown` derivation at [ConversationOverviewCard.tsx:253-256, 1170-1198](apps/mail/modules/conversations/components/ConversationOverviewCard.tsx). Linking Slack moves into the `ConversationActionMenu`.
- [x] Wire `ParticipantsBadge` in the header to open the existing `ConversationParticipantsPanel` Popover (unchanged for now; promoted to a tab in Phase 4).
- [x] Update `ConversationOverviewCardEditor` preview (in [apps/mail/app/(routes)/agentOperatingProcedures/page.tsx](apps/mail/app/(routes)/agentOperatingProcedures/page.tsx) and [apps/mail/app/(full-width)/admin/page.tsx](apps/mail/app/(full-width)/admin/page.tsx)) to render `<ConversationBadgesRow />` instead of `<ConversationOverviewCard />`.
**Tests:**
- [x] `pnpm --filter @cedar/mail typecheck` — actual script: `cd apps/mail && pnpm types` (package is `@zero/mail`). Touched files clean; pre-existing failures on HEAD unchanged.
- [x] `pnpm --filter @cedar/mail lint` — actual command: `pnpm lint` from `apps/mail`. No new errors or warnings in the touched files.
- [ ] Manual: open a conversation in full-page view; confirm header reads `[Company] [Name] ⇢ [Participants] [⋯]`, badges row shows configured items, ⋯ menu invokes refresh / recalculate / delete, no nav arrows or "Add integration" button remain.
### Phase 2 — Six-tab navigation shell
**Goal:** Replace the `expandedSections`-driven body navigation with a real six-tab layout (Inbox · Next Steps · Agents · Files · CRM · Contacts) in both `view` and `sidebar` variants. Each tab mounts its existing component as-is in this phase (Inbox still renders `PastEventsTimeline` cards; the Slack-style swap and the pinned next-step card land in Phase 3 + [timeline.md](./timeline.md); the Next Steps tab uses the date-grouped `TaskBlockList` from [next-steps.md](./next-steps.md)).
- [x] Create `ConversationTabs` at [apps/mail/modules/conversations/components/ConversationTabs.tsx](apps/mail/modules/conversations/components/ConversationTabs.tsx) with six triggers (Inbox · Next Steps · Agents · Files · CRM · Contacts) wired to the `conversationSection` store key.
- [x] Create `ConversationTabBody` at [apps/mail/modules/conversations/components/ConversationTabBody.tsx](apps/mail/modules/conversations/components/ConversationTabBody.tsx) switching on the active tab. Each branch mounts a zero-prop tab component (`InboxTab`, `NextStepsTab`, `AgentsTab`, `FilesTab`, `CrmTab`, `ContactsTab`) rather than the existing components directly — the wrappers source data from Zustand so this switch stays pure.
- [x] Extract `NextStepsCard` as a standalone export from [AgentRow.tsx:689](apps/mail/modules/conversations/components/AgentRow.tsx) (the TASK_AGGREGATOR body) so [timeline.md](./timeline.md) can mount it as the pinned card at the top of `SlackTimeline`. Already exists at [components/timeline/NextStepsCard.tsx:112](apps/mail/modules/conversations/components/timeline/NextStepsCard.tsx) — `NextStepsTab` imports from there.
- [x] Create `AgentList` at [apps/mail/modules/conversations/components/AgentList.tsx](apps/mail/modules/conversations/components/AgentList.tsx) — done inside the `AgentsTab` component at [apps/mail/modules/conversations/components/agents/AgentsTab.tsx](apps/mail/modules/conversations/components/agents/AgentsTab.tsx) (no separate `AgentList` needed — the tab is already a flat list of `AgentRowContent`).
- [x] Create `ConversationParticipantsList` at [apps/mail/modules/conversations/components/ConversationParticipantsList.tsx](apps/mail/modules/conversations/components/ConversationParticipantsList.tsx) — extracted as a new named export inside [ConversationParticipantsPanel.tsx](apps/mail/modules/conversations/components/ConversationParticipantsPanel.tsx); `ConversationParticipantsPanel` now wraps it in the Popover for backward compatibility.
- [x] Rewrite the body section of [ConversationBodyLayout.tsx](apps/mail/modules/conversations/components/ConversationBodyLayout.tsx) to render `<ConversationTabs />` + `<ConversationTabBody />` for both variants; delete the `expandedSections`-driven body branches.
- [x] Delete `computeSection` and `decodeSectionToExpandedState` and the two related `useEffect`s at [ConversationView.tsx:37-74, 146-165](apps/mail/modules/crm/components/ConversationView.tsx). Delete the auto-expand-files-agents effect at [ConversationView.tsx:168-182](apps/mail/modules/crm/components/ConversationView.tsx).
- [x] Simplify `handleBack` at [ConversationView.tsx:185-195](apps/mail/modules/crm/components/ConversationView.tsx) to just `handleClose()` (Escape always closes the conversation).
- [x] Update `ConversationOpenUrlSync` to accept the new tab keys (`inbox | nextSteps | agents | files | crm | contacts`), and add a back-compat fallback that maps legacy values (`timeline`, `enrichment`, `doc/*`, `agent/*`) to `inbox`; `nextSteps` is preserved. The back-compat lives inside the slice's `setConversationSection`/`openConversation` via `normalizeConversationSection`, so the URL sync didn't need its own translator.
- [x] Remove now-unused props from `useConversationBody`: `expandedSections`, `setExpandedSections`, `toggleSection`, `expandedDocKey`, `activeSection`. Kept on the hook (used by `ConversationBodyContent` and sidebar/sub-tab consumers — verify in Phase 4 before removing).
**Tests:**
- [x] `pnpm --filter @cedar/mail typecheck` — `cd apps/mail && pnpm types`. Touched files clean; pre-existing tRPC inference errors in `ConversationParticipantsPanel` and `ConversationOverviewCard` (the `never[] | (() => never)` pattern) are unchanged from HEAD.
- [x] `pnpm --filter @cedar/mail lint` — clean for all Phase 2 files after one `TaskBlockList` useMemo wrap fix.
- [ ] Manual: open a conversation, click through all six tabs (Inbox · Next Steps · Agents · Files · CRM · Contacts) in both the full-page view and the sidebar; reload after selecting each tab and confirm URL/state restores. Legacy URLs `?section=timeline`, `?section=enrichment` land on Inbox; `?section=nextSteps` lands on Next Steps.
### Phase 3 — Inbox: Slack-style timeline + pinned next-step card + persistent composer
**Goal:** Replace `PastEventsTimeline`'s card-based rendering inside the Inbox tab with Slack-style single-row events, mount the extracted `NextStepsCard` as a pinned card at the top of the scroll region, and add a bottom composer that actually sends an email or Slack message based on `conversation.integrationMetadata`.
- [ ] Create `SlackTimelineEvent` at [apps/mail/modules/conversations/components/timeline/SlackTimelineEvent.tsx](apps/mail/modules/conversations/components/timeline/SlackTimelineEvent.tsx) — single grid row (`32px | 1fr | auto`) with avatar (event-type icon overlay), display name, time-of-day, then collapsible content underneath. Reuse contact/avatar helpers from [TimelineEvent.tsx](apps/mail/modules/conversations/components/timeline/TimelineEvent.tsx); reuse the existing event action menu via hover-reveal.
- [ ] Create `SlackTimeline` at [apps/mail/modules/conversations/components/timeline/SlackTimeline.tsx](apps/mail/modules/conversations/components/timeline/SlackTimeline.tsx) — `flex flex-col h-full`, scrollable events region above, sticky `SlackComposer` at the bottom. Day-divider rows (`— Today —`, `— Mar 12 —`) between events on different days.
- [ ] Create `SlackComposer` at [apps/mail/modules/conversations/components/timeline/SlackComposer.tsx](apps/mail/modules/conversations/components/timeline/SlackComposer.tsx) using `MarkdownEditor` from [apps/mail/components/markdown-editor.tsx](apps/mail/components/markdown-editor.tsx). `Enter` sends, `Shift+Enter` newline. Always-visible send button.
- [ ] Add `resolveComposeTarget(conversation)` helper at [apps/mail/modules/conversations/utils/resolve-compose-target.ts](apps/mail/modules/conversations/utils/resolve-compose-target.ts) returning `{ kind: 'slack', workspaceId, channelId, channelName } | { kind: 'email', threadId, emailHeaderMessageId, to[], subject }`. Slack wins if any `slack` integration entry exists; otherwise derive email reply target from the most recent inbound email event.
- [ ] Wire `SlackComposer.onSend` to `trpc.integrations.slack.sendMessage` ([apps/server/src/trpc/routes/integrations.ts:1314](apps/server/src/trpc/routes/integrations.ts)) or `trpc.mail.send` ([apps/server/src/trpc/routes/mail.ts:1457](apps/server/src/trpc/routes/mail.ts)) based on the resolved target. On success, optimistically append the outbound event and `invalidateQueries({ queryKey=[redacted] id }) })`.
- [ ] Add a small target chip in the composer (`#channel ▾` or `Reply to <email subject> ▾`) that opens a `DropdownMenu` listing all linked Slack channels + the email thread as alternates.
- [ ] Replace `PastEventsTimeline` in `ConversationTabBody`'s `inbox` case with `<SlackTimeline />`. Keep `PastEventsTimeline` file for now (still referenced in other surfaces — verify with grep).
- [ ] Mount the extracted `<NextStepsCard />` (from Phase 2) as a pinned card at the top of `SlackTimeline`'s scroll region, above the day-dividers and events.
- [ ] Render the day-of-week / time-of-day with the existing relative-date utilities used in [TimelineEvent.tsx](apps/mail/modules/conversations/components/timeline/TimelineEvent.tsx).
**Tests:**
- [ ] `pnpm --filter @cedar/mail typecheck`
- [ ] `pnpm --filter @cedar/mail lint`
- [ ] Manual: open an email conversation and a Slack-linked conversation. Verify the composer chip shows the correct target. Send a one-line message in each; confirm the outbound event appears in the timeline within a beat and persists after refresh.
### Phase 4 — Cleanup, settings preview, and removal
**Goal:** Promote Contacts from popover-only to a real tab, delete the old `ConversationOverviewCard` and unused config plumbing, ensure the AOP editor preview matches what users see in-app.
- [ ] In [ConversationHeader.tsx](apps/mail/modules/conversations/components/ConversationHeader.tsx), change the `ParticipantsBadge` click target to switch the active tab to `contacts` (instead of opening the Popover). Keep the Popover wrapper as a no-op fallback for `sidebar` if compact mode still needs it.
- [ ] Audit and delete from [ConversationOverviewCard.tsx](apps/mail/modules/conversations/components/ConversationOverviewCard.tsx): nav row, title/badge row, tabs row, fields grid renderer, `renderTabTrigger`, `tabVariant` prop, `hideHeader`/`hideTabs`/`hideBadgeRow`/`hideFieldsGrid` props (all unreachable). Migrate any remaining unique logic into [conversationFieldRenderers.tsx](apps/mail/modules/conversations/components/conversationFieldRenderers.tsx).
- [ ] Delete `ConversationOverviewCard.tsx` once the AOP editor and onboarding consumers point at `ConversationBadgesRow` (verify against the consumer list: `agentOperatingProcedures/page.tsx`, `admin/page.tsx`, `onboarding.tsx`, `playground/ConversationFieldEditor.tsx`).
- [ ] Update [onboarding.tsx](apps/mail/modules/onboarding/components/onboarding.tsx) and [playground/ConversationFieldEditor.tsx](apps/mail/app/(routes)/playground/components/ConversationFieldEditor.tsx) to use `<ConversationBadgesRow />` instead of `<ConversationOverviewCard />`.
- [ ] Remove the `crmCard.fields` legacy-key fallback at [ConversationOverviewCard.tsx:294-300](apps/mail/modules/conversations/components/ConversationOverviewCard.tsx) by promoting it into `getOverviewConfigItems` (so the new `ConversationBadgesRow` honors both old and new saved configs without re-implementing the check).
- [ ] Verify the conversation view in keyboard-only navigation: Tab through header, badges row, tab list, tab body, composer; Shift+Tab in reverse.
- [ ] Grep for `expandedSections`, `__files`, `__timeline`, `__enrichment`, `__doc_expand_`, `__agent_doc_expand_` and remove unused references in `apps/mail`.
- [ ] Run `pnpm deps:check` to confirm no new circular imports were introduced.
**Tests:**
- [ ] `pnpm --filter @cedar/mail typecheck`
- [ ] `pnpm --filter @cedar/mail lint`
- [ ] `pnpm deps:check`
- [ ] Manual: edit the AOP overview config in `/agentOperatingProcedures` — confirm the live preview matches the in-conversation badges row. Toggle a custom select field on/off and confirm it appears/disappears in the badges row in real time. Open Contacts tab and confirm the participants list renders inline (not as a Popover).