` block) and the auto-sync toggle row.
+
+- [ ] **Step 1: Add the failing UI tests**
+
+In `MemoryTreeStatusPanel.test.tsx`, inside the `describe('
', ...)` block, add three more cases:
+
+```ts
+ it('renders a row per integration with provider name, chunk count, freshness pill', async () => {
+ mockPipelineStatus.mockResolvedValue(payload());
+ mockSyncStatusList.mockResolvedValue([
+ {
+ provider: 'slack',
+ chunks_synced: 5231,
+ chunks_pending: 0,
+ batch_total: 0,
+ batch_processed: 0,
+ last_chunk_at_ms: FIXED_NOW_MS - 3 * 60 * 1000,
+ freshness: 'active',
+ },
+ {
+ provider: 'gmail',
+ chunks_synced: 842,
+ chunks_pending: 0,
+ batch_total: 0,
+ batch_processed: 0,
+ last_chunk_at_ms: FIXED_NOW_MS - 2 * 60 * 60 * 1000,
+ freshness: 'idle',
+ },
+ ]);
+
+ render(
);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('memory-tree-integrations')).toBeInTheDocument();
+ });
+
+ const rows = screen.getAllByTestId(/^memory-tree-integration-row-/);
+ expect(rows).toHaveLength(2);
+
+ // Slack row: active dot, "Active" label, chunk count rendered
+ const slackRow = screen.getByTestId('memory-tree-integration-row-slack');
+ expect(slackRow).toHaveTextContent(/slack/i);
+ expect(slackRow).toHaveTextContent(/5,231 chunks/);
+ expect(slackRow).toHaveTextContent(/Active/);
+
+ // Gmail row: stale label
+ const gmailRow = screen.getByTestId('memory-tree-integration-row-gmail');
+ expect(gmailRow).toHaveTextContent(/gmail/i);
+ expect(gmailRow).toHaveTextContent(/Stale/);
+ });
+
+ it('shows the empty state when there are no integrations', async () => {
+ mockPipelineStatus.mockResolvedValue(payload());
+ mockSyncStatusList.mockResolvedValue([]);
+
+ render(
);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('memory-tree-integrations-empty')).toBeInTheDocument();
+ });
+ expect(screen.getByTestId('memory-tree-integrations-empty')).toHaveTextContent(
+ /no integrations connected/i
+ );
+ });
+
+ it('renders the integration strip between the tile grid and the toggle row', async () => {
+ mockPipelineStatus.mockResolvedValue(payload());
+ mockSyncStatusList.mockResolvedValue([
+ {
+ provider: 'slack',
+ chunks_synced: 1,
+ chunks_pending: 0,
+ batch_total: 0,
+ batch_processed: 0,
+ last_chunk_at_ms: FIXED_NOW_MS - 1000,
+ freshness: 'active',
+ },
+ ]);
+
+ render(
);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('memory-tree-integrations')).toBeInTheDocument();
+ });
+
+ // DOM order: tiles → integrations → toggle row.
+ const panel = screen.getByTestId('memory-tree-status-panel');
+ const tiles = screen.getByTestId('memory-tree-status-tiles');
+ const strip = screen.getByTestId('memory-tree-integrations');
+ const toggle = screen.getByTestId('memory-tree-status-toggle-row');
+
+ const order = Array.from(panel.querySelectorAll('[data-testid]'))
+ .map(el => el.getAttribute('data-testid'))
+ .filter(id =>
+ ['memory-tree-status-tiles', 'memory-tree-integrations', 'memory-tree-status-toggle-row'].includes(
+ id ?? ''
+ )
+ );
+
+ expect(order).toEqual([
+ 'memory-tree-status-tiles',
+ 'memory-tree-integrations',
+ 'memory-tree-status-toggle-row',
+ ]);
+ // Sanity references so unused-var lint doesn't flag the locals above.
+ expect(tiles).toBeInTheDocument();
+ expect(strip).toBeInTheDocument();
+ expect(toggle).toBeInTheDocument();
+ });
+```
+
+- [ ] **Step 2: Run the new tests, watch them fail**
+
+Run: `pnpm debug unit src/components/intelligence/MemoryTreeStatusPanel.test.tsx -t "renders a row per integration|empty state when there are no integrations|integration strip between"`
+Expected: FAIL — `memory-tree-integrations` and `memory-tree-integrations-empty` test-ids do not exist yet.
+
+- [ ] **Step 3: Add the sub-component**
+
+In `MemoryTreeStatusPanel.tsx`, just before the existing `export function MemoryTreeStatusPanel(...)` declaration (around line 182), insert:
+
+```ts
+/**
+ * Per-integration health strip (#2763). Rendered between the four pipeline
+ * tiles and the auto-sync toggle inside `MemoryTreeStatusPanel`. Consumes
+ * the `integrations` slice returned by `useMemoryTreeStatus` — no
+ * additional fetch, no second timer.
+ */
+function IntegrationHealthStrip({
+ integrations,
+ t,
+}: {
+ integrations: MemorySyncStatusRow[];
+ t: TFn;
+}) {
+ return (
+
+
+ {t('memoryTree.status.integrationsTitle')}
+
+ {integrations.length === 0 ? (
+
+ {t('memoryTree.status.integrationsEmpty')}
+
+ ) : (
+
+ {integrations.map(row => {
+ const health = classifyIntegration(row.freshness);
+ const healthLabel =
+ health === 'active'
+ ? t('memoryTree.status.integrationActive')
+ : t('memoryTree.status.integrationStale');
+ const dot = health === 'active' ? 'bg-sage-400' : 'bg-stone-400 dark:bg-neutral-500';
+ return (
+ -
+
+
+ {providerIconChar(row.provider)}
+
+
+ {row.provider}
+
+
+
+
+ {t('memoryTree.status.integrationChunks').replace(
+ '{count}',
+ new Intl.NumberFormat().format(row.chunks_synced)
+ )}
+
+
+ {formatRelativeMs(row.last_chunk_at_ms ?? 0, t, t('memoryTree.status.never'))}
+
+
+
+ {healthLabel}
+
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
+```
+
+- [ ] **Step 4: Mount the strip between the tiles and the toggle**
+
+Still in `MemoryTreeStatusPanel.tsx`, find the closing `
` of the `data-testid="memory-tree-status-tiles"` block (the grid div that holds the 4 tiles — its closing `