chat-display-artifact.md28.6 KBView on GitHub
# Per-Thread Display Artifact — the thread's active window (conversation | email | file | agenda)

## 1) Introduction — goal, present state, future state

We want each chat thread to own an **active window** — the one artifact shown beside the transcript: an explicitly-opened conversation / email thread / file, or nothing, in which case the panel draws its own resting content. (The original design made that default an artifact kind of its own, `{ kind: 'agenda' }`; §3.2 step 3 records why it is now plain `null`.) Today the "open artifact" is a single **global** `selectedArtifact` slot on the store — shared across every thread, so switching threads leaks the previous thread's open item — and it is only rendered on `/home` (column 3 of `HomeView`, hidden entirely when null). Everywhere else the "active window" is a *different*, ad-hoc mechanism: the sidepanel computes its own `baseContent` from the view stack (a conversation context, or `null`), and the agenda only appears when a route explicitly passes `defaultContent="agenda"` (just `/mail`, `/conversations`). Every other route — the agent-style `defaultContent="chat"` routes — gets `baseContent = null`, so the sidepanel renders **only another copy of chat** with no agenda. We make `selectedArtifact` a **per-thread** field on `MessageThread`, add a single resolver `getDisplayArtifact()` that combines the active thread with its artifact (defaulting to `{ kind: 'agenda' }`), and render the resolved value through one shared `DisplayArtifactPanel` used by **both** `/home` column 3 (now always present) and the sidepanel base layer (replacing the ad-hoc `baseContent`). `agenda` is a resolver-default only — not a `ContextKind`, never a stored/attachable item — and renders as `AgendaMeetings`. The net effect: every surface shows the same "active window" for the active thread, and a thread with nothing open shows the calendar agenda instead of a second chat.

## 2) Present state

### 2.1 Architecture diagram

```text
                          FRONTEND (apps/mail)
  ┌──────────────────────────────────────────────────────────────────────┐
  │  messagesSlice.selectedArtifact : {kind,id} | null   ← ONE global slot │
  │    setSelectedArtifact(sel)  ── writes the global field                │
  │                                                                        │
  │  writers:  ChatContextRow chip click ─┐                                │
  │            ChatBubbles focus (openContext) ─┼─► setSelectedArtifact    │
  │            MentionChip click ─────────┘                                │
  │                                                                        │
  │  reader (ONLY on /home):                                               │
  │    HomeView ── hasArtifact = selectedArtifact !== null                 │
  │      col2 = FullScreenChat   col3 = OpenArtifactPanel (hidden if null) │
  │        OpenArtifactPanel switch(kind): conversation|file|email|slack   │
  └──────────────────────────────────────────────────────────────────────┘

  Everywhere else (sidepanel routes) — a SEPARATE "active window":
  ┌──────────────────────────────────────────────────────────────────────┐
  │  Sidepanel(defaultContent)                                            │
  │    baseContent = (thread open → ConversationContext) else null        │
  │    defaultContent==='agenda' → base = AgendaCalendarSidebar  (mail,    │
  │                                          conversations only)           │
  │    defaultContent==='chat'   → base = null → SidepanelChatOverlay only │
  │        └─► EmbeddedCedarChat  ← "another copy of chat, no agenda"      │
  └──────────────────────────────────────────────────────────────────────┘
```

### 2.2 Step-by-step walkthrough

1. **Global artifact slot** — `selectedArtifact: { kind: ContextKind; id: string } | null` and `setSelectedArtifact` live on the store slice (formerly `MessagesSlice`, now `AgentThreadsSlice`) at [messagesSlice.ts:59](apps/mail/modules/cedar-os/src/store/messages/messagesSlice.ts) (types) / [messagesSlice.ts:205](apps/mail/modules/cedar-os/src/store/messages/messagesSlice.ts) (impl). It is a *single* field on the root store, not nested under a thread.
   - Shape: `selectedArtifact = { kind: 'conversation', id: 'conv_abc' }` — the same value regardless of which thread is active.

2. **`MessageThread` has no window field** — [MessageTypes.ts:274](apps/mail/modules/cedar-os/src/store/messages/MessageTypes.ts) defines `MessageThread` with `chatContext`, `status`, `pinned`, `inputContent`, `activeCanvasId`, … but **no `selectedArtifact`**. Per-message `openContext?: { kind, id }` exists at [MessageTypes.ts:67](apps/mail/modules/cedar-os/src/store/messages/MessageTypes.ts) (restores an artifact on focus) but the thread itself owns no "current window."

3. **Writers all target the one global slot:**
   - `ChatContextRow` chip click → `setSelectedArtifact` at [ChatContextRow.tsx:59](apps/mail/modules/cedar-os/src/cedar-os-components/chatComponents/ChatContextRow.tsx); it also reads `selectedArtifact` at [:22](apps/mail/modules/cedar-os/src/cedar-os-components/chatComponents/ChatContextRow.tsx) to highlight the active chip.
   - `ChatBubbles` focus → `setSelectedArtifact(message.openContext)` at [ChatBubbles.tsx:214](apps/mail/modules/cedar-os/src/cedar-os-components/chatMessages/ChatBubbles.tsx), [:252](apps/mail/modules/cedar-os/src/cedar-os-components/chatMessages/ChatBubbles.tsx).
   - `MentionChip` click → `setSelectedArtifact` at [MentionChip.tsx:34](apps/mail/modules/cedar-os/src/cedar-os-components/chatMessages/MentionChip.tsx).

4. **Only reader is `/home`** — `HomeView` reads `hasArtifact = selectedArtifact !== null` at [HomeView.tsx:18](apps/mail/modules/home/components/HomeView.tsx) and renders a two-column grid **only when** it's non-null: column 2 `FullScreenChat` ([FullScreenChat.tsx:19](apps/mail/modules/home/components/FullScreenChat.tsx), `EmbeddedCedarChat hideTabs`), column 3 `OpenArtifactPanel`. Null → single-column, chat full width.

5. **`OpenArtifactPanel` switches by kind** — [OpenArtifactPanel.tsx](apps/mail/modules/home/components/OpenArtifactPanel.tsx): `conversation` → conversation card; `file` → doc content; `email_thread`/`slack_thread` → a label card; empty state = muted "Select a context item to view it here." No `agenda` branch exists.

6. **Sidepanel is a parallel active-window system** — `Sidepanel` at [sidepanel.tsx:198](apps/mail/components/ui/sidepanel.tsx) computes `baseContent` at [:245](apps/mail/components/ui/sidepanel.tsx) (thread/event open → `ConversationContext`, else `null`) and branches `panelContent` by `defaultContent` at [:297](apps/mail/components/ui/sidepanel.tsx). `SidepanelChatOverlay` at [:110](apps/mail/components/ui/sidepanel.tsx) renders `baseContent` at [:162](apps/mail/components/ui/sidepanel.tsx) with `EmbeddedCedarChat` overlaid on top (collapsed = input bar; expanded = fills the panel).

7. **Agenda only reachable via one prop value** — `defaultContent="agenda"` renders `AgendaCalendarSidebar` as base at [sidepanel.tsx:315](apps/mail/components/ui/sidepanel.tsx); this is used only by `/mail` ([mail/layout.tsx:15](apps/mail/app/(routes)/mail/layout.tsx)) and `/conversations` ([conversations/layout.tsx:16](apps/mail/app/(routes)/conversations/layout.tsx)). The richer calendar-agenda render (`AgendaMeetings`, the right column of `AgendaHome` at [AgendaHome.tsx:92](apps/mail/modules/agentCanvas/components/AgendaHome.tsx)) is **only** used inside the `/agenda` main panel, never as a thread's active window.

8. **The "another copy of chat" routes** — routes passing `defaultContent="chat"` (`/pipeline`, `/brain`, `/gallery`, `/statistics`, `/reports`, `/agenda`) get `baseContent = null`, so the sidepanel shows only `EmbeddedCedarChat`. `/agentExecutions` passes no prop → defaults to `'conversation'` → base is `ConversationContext`-or-`null`. None of these fall back to the agenda; a thread with nothing open shows a bare second chat.

### 2.3 Present-state route table

| Route | `defaultContent` | Sidepanel base today |
|---|---|---|
| `/mail`, `/conversations` | `agenda` | `AgendaCalendarSidebar` (or `ConversationContext` when a thread opens) |
| `/pipeline`, `/brain`, `/gallery`, `/statistics`, `/reports`, `/agenda` | `chat` | `null` → chat only |
| `/agentExecutions` | *(default)* `conversation` | `ConversationContext` or `null` |
| `/calendar` | `calendar` | `CalendarSidebar` (draft only) |
| `/meetings` | `conversation` | `ConversationContext` or `null` |

## 3) Designed state

### 3.1 Architecture diagram

```text
                          FRONTEND (apps/mail)
  ┌──────────────────────────────────────────────────────────────────────┐
  │  threadMap[activeThreadId].selectedArtifact : {kind,id} | null         │
  │    setSelectedArtifact(sel) ── writes the ACTIVE thread's field        │
  │                                                                        │
  │  getDisplayArtifact() : DisplayArtifact | null    ← the ONE resolver     │
  │    = threadMap[activeThreadId].selectedArtifact ?? null                │
  │                                                                        │
  │  writers (unchanged call sites, now per-thread):                       │
  │    ChatContextRow · ChatBubbles focus · MentionChip → setSelectedArtifact│
  │                                                                        │
  │  ┌──────────────── <DisplayArtifactPanel> ─────────────────┐          │
  │  │ switch(selectedArtifact?.kind):                          │          │
  │  │    null                   → the Top Deals card list      │          │
  │  │   'conversation'          → conversation view            │          │
  │  │   'email_thread'|'slack_thread' → thread view            │          │
  │  │   'file'                  → doc view                      │          │
  │  └──────────────────────────────────────────────────────────┘          │
  │        ▲ used by BOTH mount points                                     │
  │        │                                                               │
  │   HomeView col3 (ALWAYS on)          Sidepanel base layer (all routes) │
  └──────────────────────────────────────────────────────────────────────┘
```

### 3.2 Step-by-step walkthrough

1. **Move the slot onto the thread** — add `selectedArtifact?: { kind: ContextKind; id: string } | null` to `MessageThread` in [MessageTypes.ts:274](apps/mail/modules/cedar-os/src/store/messages/MessageTypes.ts). Remove the global `selectedArtifact` field from the slice. Switching threads now switches the active window automatically.
   - `threadMap = { t1: { selectedArtifact: { kind:'conversation', id:'conv_abc' } }, t2: { selectedArtifact: null } }`.

2. **`setSelectedArtifact` writes the active thread** — reimplement `setSelectedArtifact(sel)` in [messagesSlice.ts:205](apps/mail/modules/cedar-os/src/store/messages/messagesSlice.ts) to write `threadMap[activeThreadId].selectedArtifact = sel` (no-op if there's no active thread). The signature is unchanged, so all three writers keep working without edits.

3. **`getDisplayArtifact()` resolver** — new selector on the slice reading the active thread's artifact:
   ```ts
   type DisplayArtifact = { kind: ContextKind | 'canvas' | 'agent'; id: string };
   getDisplayArtifact(): DisplayArtifact | null {
     const t = get().threadMap[get().activeThreadId];
     return t?.selectedArtifact ?? null;
   }
   ```
   **Nothing open is `null`.** The resolver carried a `{ kind: 'agenda' }` default member for a while and it was a lie about the app: the panel resolves its own resting content and never read that branch, so the only thing the default achieved was making "nothing is open" un-expressible — every caller asking that question had to know which kind meant "no". The panel's resting state belongs to the panel; the resolver's job is to say what the user opened, or that they opened nothing.

4. **One shared `DisplayArtifactPanel`** — generalize `OpenArtifactPanel` into `DisplayArtifactPanel` (keep it in `modules/home/components/`, or lift to a shared `chatComponents` location) that reads the thread's artifact and switches:
   - `null` → the user's Top Deals card list (`canvas.ensureTopDeals`). This is the panel's own resting content, resolved here rather than injected by the resolver.
   - `conversation` → the existing conversation view.
   - `email_thread` / `slack_thread` → thread view.
   - `file` → doc view.
   There is no "empty state" anymore — the floor is the card list.

5. **`/home` is always two-column** — `HomeView` ([HomeView.tsx](apps/mail/modules/home/components/HomeView.tsx)) drops the `hasArtifact` gate and always renders `grid-cols-[1fr_minmax(0,32rem)]`: col2 `FullScreenChat`, col3 `DisplayArtifactPanel`. A thread with nothing open shows the panel's resting content in col3 (decision: always-show).

   **A document the agent writes fills that resting panel, and only that one.** Every `write-document` in a chat turn attaches its doc to the thread as a `file` context chip and, if `selectedArtifact` is null, opens it here — see [openWrittenDocument.ts](apps/mail/modules/cedar-os/src/store/agentConnection/responseProcessors/openWrittenDocument.ts). If the user already has something open it stays open and the chip is the way in. This replaced a `display-document` tool the agent called by hand: making the model decide when to reshape someone else's screen meant it usually didn't, and when it did it took the panel regardless of what was in it.

6. **Sidepanel base = the display artifact** — replace the ad-hoc `baseContent` computation ([sidepanel.tsx:245](apps/mail/components/ui/sidepanel.tsx)) so the base layer under `SidepanelChatOverlay` is `<DisplayArtifactPanel />`. **Every** sidepanel route now shows the panel's resting content when nothing is open — fixing the "another copy of chat" routes. `ConversationContext` becomes the `conversation`/`email_thread` branch of `DisplayArtifactPanel` (or is rendered by it), so opening a thread still fills the panel; the chat stays the collapsible overlay on top.

7. **Route `defaultContent` collapses toward one meaning** — the `agenda` / `conversation` / `chat` variants converge: their base is now uniformly the display artifact. `chat` is retained only as an explicit **chat-only** escape hatch for routes whose *main* panel already IS the artifact — notably `/agenda`, whose main panel renders `AgendaHome` (so its sidepanel intentionally stays chat-only to avoid a duplicate agenda). Agent-style routes that today show bare chat are switched off `chat` so their sidepanel shows the agenda. `calendar` (draft-only `CalendarSidebar`) is left as-is.

8. **Writers unchanged, semantics improved** — `ChatBubbles` focus still calls `setSelectedArtifact(message.openContext)`; now it sets the *active thread's* window, and scrolling a thread with no `openContext` messages leaves the window at the agenda default. `ChatContextRow` highlight reads `getDisplayArtifact()` instead of the global field.

### 3.3 Schema

```ts
// ── Display artifact (client-only; not persisted, not a ContextKind) ──────────
type DisplayArtifact =
  | { kind: 'conversation' | 'email_thread' | 'slack_thread' | 'file'; id: string }
  | { kind: 'agenda' };            // resolver default — the day's calendar agenda

// ── Thread (only the changed field shown) ────────────────────────────────────
interface MessageThread {
  // …existing: id, name, messages, chatContext, status, pinned, inputContent, …
  selectedArtifact?: { kind: ContextKind; id: string } | null;  // NEW — per-thread active window
}

// ── Slice ────────────────────────────────────────────────────────────────────
interface AgentThreadsSlice {
  threadMap: Record<string, MessageThread>;
  activeThreadId: string;
  // REMOVED: selectedArtifact (global field)
  setSelectedArtifact(sel: { kind: ContextKind; id: string } | null): void;  // now writes active thread
  getDisplayArtifact(): DisplayArtifact;                                      // NEW resolver
}
```

Relationship:

```text
 AgentThreadsSlice
 ┌───────────────────────────────────────────────┐
 │ activeThreadId ────FK───► threadMap key        │
 │ getDisplayArtifact() ──resolves──►             │
 │     threadMap[activeThreadId].selectedArtifact │
 │       ?? { kind:'agenda' }                     │
 └───────────────────────────────────────────────┘
                         │ rendered by
                         ▼
              <DisplayArtifactPanel>
        agenda→AgendaMeetings · conversation/email/slack/file→viewer
                         ▲                    ▲
             /home column 3 (always)   Sidepanel base (all routes)
```

## 4) Implementation phases

### Phase 1 — Per-thread `selectedArtifact` + `getDisplayArtifact` resolver

**Goal:** move the artifact slot onto the thread and add the resolver; keep every existing writer/reader working via the unchanged `setSelectedArtifact` signature.

- [x] Add `selectedArtifact?: { kind: ContextKind; id: string } | null` to `MessageThread` in [MessageTypes.ts](apps/mail/modules/cedar-os/src/store/messages/MessageTypes.ts).
- [x] Add the `DisplayArtifact` type (with the `'agenda'` default variant) to the store types ([MessageTypes.ts](apps/mail/modules/cedar-os/src/store/messages/MessageTypes.ts), next to `ContextKind`).
- [x] Remove the global `selectedArtifact` field; reimplement `setSelectedArtifact(sel)` to write `threadMap[activeThreadId].selectedArtifact` (no-op when no active thread) in [messagesSlice.ts](apps/mail/modules/cedar-os/src/store/messages/messagesSlice.ts). **Divergence:** the active thread is resolved as `activeThreadId || mainThreadId` (the two are kept in sync until the Phase-10 rename of chat-thread-store; `mainThreadId` is still the primary read across the app).
- [x] Add `getDisplayArtifact()` selector (`activeThread.selectedArtifact ?? { kind:'agenda' }`).
- [x] Repoint `ChatContextRow` read from the global field to the *prop* thread's `selectedArtifact` ([ChatContextRow.tsx](apps/mail/modules/cedar-os/src/cedar-os-components/chatComponents/ChatContextRow.tsx)) so the highlight reflects the thread being rendered. **Also repointed** the two other readers so Phase 1 compiles: `agentConnectionSlice` open-context stamp (reads `threadMap[resolvedThreadId].selectedArtifact` — more correct than the global read it replaced) and, minimally, `HomeView`/`OpenArtifactPanel` (behavior unchanged; the agenda default + always-two-column land in Phases 2–3).

**Tests:**

- [x] jest: `setSelectedArtifact` writes the active thread's field only (other thread untouched); switching the active thread changes what `getDisplayArtifact()` returns and restores each thread's window; an empty thread resolves to `{ kind:'agenda' }`; no-active-thread is a safe no-op. In `apps/mail/tests/modules/chat-store/displayArtifact.test.tsx` (4 tests). **Also updated** `perMessageOpenContext.test.tsx` + `mentionChip.test.tsx` to assert the per-thread slot (they read the removed global field); `OpenArtifactPanel.test.tsx` unchanged (still green — its empty-state is reworked in Phases 2–3).
- [x] `pnpm --filter @zero/mail exec jest displayArtifact perMessageOpenContext mentionChip OpenArtifactPanel` → 13 passed; touched source files typecheck clean; the 3 failing store suites (`messagesSlice.basic`/`.thread`, `agentContextSlice`) are the pre-existing failures documented in chat-thread-store.md Phase 6.

### Phase 2 — `DisplayArtifactPanel` (agenda default via `AgendaMeetings`)

**Goal:** one shared panel that renders the resolved display artifact, with agenda as the floor.

- [x] Generalize `OpenArtifactPanel` → `DisplayArtifactPanel` ([DisplayArtifactPanel.tsx](apps/mail/modules/home/components/DisplayArtifactPanel.tsx)) reading the active thread's per-thread `selectedArtifact`; add the `agenda` branch rendering `<AgendaMeetings />` ([AgendaMeetings.tsx](apps/mail/modules/agentCanvas/components/AgendaMeetings.tsx)); keep conversation/email/slack/file branches; drop the "Select a context item" empty state (the agenda is the floor). Deleted `OpenArtifactPanel.tsx`; repointed the sole importer (`HomeView`). **Divergence:** the component subscribes reactively to `threadMap[mainThreadId].selectedArtifact` and computes the `?? agenda` default in render, rather than calling the imperative `getDisplayArtifact()` (which stays for tests / non-reactive callers) — a store getter isn't a reactive subscription.
- [x] **Fixed a latent bug while generalizing:** a `selectedArtifact` pointing at the dedicated `primaryConversation` slot (which is NOT in `items[]`) used to fall through to the empty state. `DisplayArtifactPanel` now resolves the display item from `items[]` → the `primaryConversation` slot → a minimal `{kind,id}`, so the primary chip renders.
- [x] Verified `AgendaMeetings` renders standalone: it owns its `selectedDay` state and reads only app-wide providers (`useCalendars`/`useAllCalendarEvents`/`useActiveConnection`/`useCalendarCanvas*`) — no `/agenda`-page-only context. Wrapped in the panel's own `h-full overflow-y-auto p-4` scroll container.

**Tests:**

- [x] jest/component: `DisplayArtifactPanel` renders `AgendaMeetings` (mocked to a sentinel) when the active thread has no `selectedArtifact`; renders the matching viewer's label when it does; and resolves a `primaryConversation` selection absent from `items[]`. In `apps/mail/tests/modules/home/DisplayArtifactPanel.test.tsx` (3 tests; replaces the old `OpenArtifactPanel.test.tsx`).
- [x] `pnpm --filter @zero/mail exec jest tests/modules/home tests/modules/chat-store` → 52 passed; touched source files typecheck clean.

### Phase 3 — `/home` always two-column

**Goal:** the home artifact column is always present, defaulting to the agenda.

- [x] Dropped the `hasArtifact` gate in [HomeView.tsx](apps/mail/modules/home/components/HomeView.tsx); always renders the two-column grid with col3 = `DisplayArtifactPanel` (agenda by default). Removed the now-unused `useCedarStore`/`cn` imports; updated the header comment (served at `/agent`; `/home` also applies).

**Tests:**

- [x] jest/component: `HomeView` renders both the chat pane and the artifact pane unconditionally (no artifact selected), in `apps/mail/tests/modules/home/HomeView.test.tsx` — both panes mocked to sentinels (FullScreenChat/EmbeddedCedarChat needs the full provider stack; this is a headless layout-composition proof). → 1 passed; HomeView typechecks clean.

### Phase 4 — Sidepanel base = `DisplayArtifactPanel`

**Goal:** the sidepanel's base layer is the display artifact for every route; agenda replaces the bare-chat base.

- [x] Replaced the `baseContent` computation ([sidepanel.tsx](apps/mail/components/ui/sidepanel.tsx)) so the base layer falls back to `<DisplayArtifactPanel />` instead of `null` — no route shows a bare second chat. **Divergence (lower-risk than the doc's plan):** the richer `ConversationContext` is *kept* for the explicit open-thread / calendar-event / day-highlight cases rather than folded into `DisplayArtifactPanel` — folding would regress the rich conversation view. `DisplayArtifactPanel` is the fallback base only (nothing open → the thread's active window / agenda). The precedence is extracted into a pure, unit-testable `resolveSidepanelBaseKind` in [sidepanel-base.ts](apps/mail/components/ui/sidepanel-base.ts) (its own module so the test doesn't drag in the chat/conversation provider tree).
- [x] Retired `AgendaCalendarSidebar` as the sidepanel agenda (superseded by `AgendaMeetings` inside `DisplayArtifactPanel`): the `agenda` branch now uses the unified `baseContent`, and its `collapseChat` is driven by `hasThreadBase` (a rich thread/event is open) instead of `!!baseContent`. Import removed.
- [x] Kept `defaultContent="chat"` as an explicit chat-only escape hatch (forces chat open, covering the base) — the route audit in Phase 5 moves the agent-style routes off it so their `DisplayArtifactPanel` base shows.

**Tests:**

- [x] jest: `resolveSidepanelBaseKind` returns `'display-artifact'` when nothing is open (the key change from `null`), `'conversation'` for an open thread/calendar event, `'date-highlight'` for the day view, and falls back to the active window for a thread with no id — in `apps/mail/tests/modules/home/sidepanelBase.test.tsx` (5 tests). **Divergence:** the extracted-resolver unit test (per the chat-thread-store precedent) stands in for a full `Sidepanel` DOM render, which needs the entire conversation/chat/canvas provider stack. → 5 passed; sidepanel files typecheck clean.

### Phase 5 — Route audit: switch agent-style routes off bare chat

**Goal:** the routes that showed "another copy of chat" now show the agenda by default.

**Audit outcome (the real root cause was the home route, not a `defaultContent` value):** `/agent` is the `(routes)/home` route, and `(routes)/home/layout.tsx` wrapped the *self-contained* `HomeView` (which already has its own DisplayArtifactPanel column) inside a `<Sidepanel>`. That sidepanel's chat overlay was the "another copy of chat" on the right, and it never showed the agenda. The other `Sidepanel` routes are handled purely by the Phase-4 mechanism (base now defaults to the agenda) with no per-route change.

- [x] Removed the redundant `<Sidepanel>` from [`(routes)/home/layout.tsx`](apps/mail/app/(routes)/home/layout.tsx): `HomeView` is rendered directly (full-width two-column: chat + agenda/artifact), with `<GlobalCanvas />` preserved (it was only ever mounted by the sidepanel). So `/agent` = outer sidebar │ chat │ active-window(agenda) — no extra chat. **Note:** the outer `/agent` shell (`(routes)/layout.tsx`, `EmbeddedCedarChat`, `HomeChatSidebar`, `GlobalCanvas`) is in the user's active WIP and was left untouched; only the clean `home/layout.tsx` was edited.
- [x] Left `/pipeline`, `/brain`, `/gallery`, `/statistics`, `/reports` on `defaultContent="chat"` (chat-forced — deliberate: data pages where the sidepanel chat is the assistant). `/agenda` keeps `chat` (its main panel is `AgendaHome`). `/mail`, `/conversations` (`agenda`) and the default-`conversation` routes (`/meetings`, `/agentExecutions`, `/tasks`, `/crm`, `/outbound`) now surface the `DisplayArtifactPanel` agenda base via the Phase-4 mechanism (unchanged files).

**Tests:**

- [x] `home/layout.tsx` typechecks clean; the Phase-3 `HomeView` two-column composition test + Phase-2 `DisplayArtifactPanel` agenda test cover the rendered content. **Divergence:** a live route render (React Router + browser) can't be exercised in jest; the layout change is a structural edit proven by typecheck + the component-level tests, with manual in-app verification of `/agent` recommended.

## 5) Verification steps

- Fresh thread on `/home`: col3 shows the day's agenda (`AgendaMeetings`); click a `ChatContextRow` conversation chip → col3 swaps to the conversation; switch to a second thread → col3 reflects *that* thread's window (agenda if it has none); switch back → the first thread's conversation is restored (per-thread persistence).
- On an agent-style route: the sidepanel shows the agenda by default; opening an email/conversation fills it; the chat is the collapsible overlay.
- Grep-verify no remaining reads of a global `selectedArtifact` field; all go through `getDisplayArtifact()` or the active thread.