# Task 1942 — Sub-account switcher to sidebar brand head — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans. Steps use checkbox (`- [ ]`) syntax.

**Goal:** Open the sub-account picker by clicking the sidebar brand head; remove it from the footer kebab.

**Architecture:** `Sidebar.tsx` owns a `switcherOpen` state and renders the brand head as a button (icon + lockup + chevron) when the switcher is available, with `SubAccountPicker` + a full-screen scrim anchored beneath `.side-brand`. `AccountMenu.tsx` loses the switcher block, its five props, and the `SubAccountPicker` import. `globals.css` makes `.side-brand` a positioned anchor, re-anchors the panel left-aligned, and deletes the dead footer overrides.

**Tech Stack:** React 18, TypeScript, vitest + jsdom (Node 22), lucide-react icons.

## Global Constraints

- Node 22 for the jsdom test run (`platform/ui` requirement).
- Switcher availability gate is exactly `subAccounts.length > 1 && Boolean(onSwitchAccount)`; unchanged from today.
- No new task-id leaks in shipped docs; docs edit at `platform/plugins/docs/references/admin-ui.md`.
- All paths below are relative to the worktree's `maxy-code/platform/ui/`.

---

### Task 1: Strip the switcher out of AccountMenu

**Files:**
- Modify: `app/components/AccountMenu.tsx`
- Test: `app/components/__tests__/AccountMenu.test.tsx`

**Interfaces:**
- Produces: `AccountMenuProps` with no `subAccounts`/`activeAccountId`/`switchingAccount`/`onSwitchAccount`/`refreshAccounts` fields.

- [ ] **Step 1: Update the test to assert the switcher is gone**

In `AccountMenu.test.tsx`, delete the two switcher tests (`renders no Switch account affordance with 0-1 sub-accounts` and `opens the sub-account picker and fires onSwitchAccount + refreshAccounts`, lines 56-88). Add one test:

```tsx
it('renders no Switch account affordance (relocated to sidebar brand head, Task 1942)', () => {
  render(<AccountMenu {...baseProps} variant="admin" cacheKey="k" onOpenConversations={vi.fn()} />)
  expect(screen.queryByRole('menuitem', { name: /switch account/i })).toBeNull()
  expect(screen.queryByPlaceholderText(/search sub-accounts/i)).toBeNull()
})
```

Remove `subAccounts`/`activeAccountId`/`switchingAccount`/`onSwitchAccount`/`refreshAccounts` from any remaining render in the file.

- [ ] **Step 2: Run — expect fail (props still typed / item still renders)**

Run: `npx vitest run app/components/__tests__/AccountMenu.test.tsx`
Expected: FAIL (the Switch account menuitem still renders).

- [ ] **Step 3: Remove the switcher from AccountMenu.tsx**

- Delete the `import { SubAccountPicker } from './SubAccountPicker'` line.
- In `AccountMenuProps`, delete the five switcher prop declarations and their doc comment block (`subAccounts?`, `activeAccountId?`, `switchingAccount?`, `onSwitchAccount?`, `refreshAccounts?`).
- In the destructure, remove `subAccounts = []`, `activeAccountId = null`, `switchingAccount = false`, `onSwitchAccount`, `refreshAccounts`.
- Delete the `switcherOpen` state and `switcherAvailable` const.
- Delete the whole `{switcherAvailable && ( … <SubAccountPicker … /> … )}` JSX block.
- Remove `UserRound` from the lucide-react import (it was used only by the deleted block; grep the file to confirm no other use).

- [ ] **Step 4: Run — expect pass**

Run: `npx vitest run app/components/__tests__/AccountMenu.test.tsx`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add maxy-code/platform/ui/app/components/AccountMenu.tsx maxy-code/platform/ui/app/components/__tests__/AccountMenu.test.tsx
git commit -m "refactor(1942): remove sub-account switcher from AccountMenu"
```

---

### Task 2: Brand head becomes the switcher trigger in Sidebar

**Files:**
- Modify: `app/Sidebar.tsx`
- Test: `app/__tests__/Sidebar-subaccount-switcher.test.tsx` (create), `app/__tests__/Sidebar-user-footer.test.tsx`

**Interfaces:**
- Consumes: `SubAccountPicker` from `./components/SubAccountPicker`; `ChevronDown` from `lucide-react`; existing `subAccounts`/`activeAccountId`/`switchingAccount`/`onSwitchAccount`/`refreshAccounts` Sidebar props.

- [ ] **Step 1: Write the failing test (new file)**

Create `app/__tests__/Sidebar-subaccount-switcher.test.tsx`:

```tsx
import React from 'react'
import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { Sidebar } from '../Sidebar'

function jsonResponse(body: unknown, status = 200): Response {
  return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } }) as Response
}
function routedFetch() {
  return vi.spyOn(global, 'fetch').mockImplementation((input: RequestInfo | URL) => {
    const url = typeof input === 'string' ? input : input.toString()
    if (url.includes('/api/admin/sidebar-sessions')) return Promise.resolve(jsonResponse({ accountId: 'acct', sessions: [] }))
    if (url.includes('/api/admin/system-stats')) return Promise.resolve(jsonResponse({ cpuPct: null, memUsedPct: 0.5, platform: 'darwin' }))
    if (url.includes('/api/admin/version')) return Promise.resolve(jsonResponse({ installed: '1.0.0', latest: null, updateAvailable: false }))
    return Promise.resolve(jsonResponse({}, 404))
  })
}
const ACCOUNTS = [
  { accountId: 'a1', businessName: 'House', role: 'admin', isHouse: true },
  { accountId: 'a2', businessName: 'Client', role: 'admin', isHouse: false },
]
function baseProps(over: Record<string, unknown> = {}) {
  return {
    businessName: 'House', cacheKey: 'k', role: 'admin', userName: 'Ada Ops', userAvatar: null,
    onSelectProjects: vi.fn(), onSelectTasks: vi.fn(), onCloseMobileDrawer: vi.fn(),
    collapsed: false, mobileDrawerOpen: false, onSelectData: vi.fn(),
    onLogout: vi.fn(), onDisconnect: vi.fn(async () => true), disconnecting: false, claudeConnected: true,
    ...over,
  }
}
beforeEach(() => { Object.defineProperty(document, 'hidden', { value: false, configurable: true }) })
afterEach(() => { vi.restoreAllMocks() })

describe('Sidebar sub-account switcher on the brand head (Task 1942)', () => {
  it('brand head is a button that toggles the picker and fires refreshAccounts', () => {
    routedFetch()
    const refreshAccounts = vi.fn()
    render(<Sidebar {...baseProps({ subAccounts: ACCOUNTS, activeAccountId: 'a1', switchingAccount: false, onSwitchAccount: vi.fn(), refreshAccounts })} />)
    const head = screen.getByRole('button', { name: /switch sub-account/i })
    expect(screen.queryByPlaceholderText(/search sub-accounts/i)).toBeNull()
    fireEvent.click(head)
    expect(screen.getByPlaceholderText(/search sub-accounts/i)).toBeInTheDocument()
    expect(refreshAccounts).toHaveBeenCalledTimes(1)
    fireEvent.click(head)
    expect(screen.queryByPlaceholderText(/search sub-accounts/i)).toBeNull()
  })

  it('selecting a card calls onSwitchAccount and closes the picker', () => {
    routedFetch()
    const onSwitchAccount = vi.fn()
    render(<Sidebar {...baseProps({ subAccounts: ACCOUNTS, activeAccountId: 'a1', switchingAccount: false, onSwitchAccount, refreshAccounts: vi.fn() })} />)
    fireEvent.click(screen.getByRole('button', { name: /switch sub-account/i }))
    fireEvent.click(screen.getByRole('button', { name: 'Client' }))
    expect(onSwitchAccount).toHaveBeenCalledWith('a2')
    expect(screen.queryByPlaceholderText(/search sub-accounts/i)).toBeNull()
  })

  it('single-account install: brand head is not a switcher button and no picker mounts', () => {
    routedFetch()
    render(<Sidebar {...baseProps({ subAccounts: [ACCOUNTS[0]], activeAccountId: 'a1', onSwitchAccount: vi.fn() })} />)
    expect(screen.queryByRole('button', { name: /switch sub-account/i })).toBeNull()
    expect(screen.queryByPlaceholderText(/search sub-accounts/i)).toBeNull()
  })
})
```

- [ ] **Step 2: Run — expect fail**

Run: `npx vitest run app/__tests__/Sidebar-subaccount-switcher.test.tsx`
Expected: FAIL (no button named "Switch sub-account").

- [ ] **Step 3: Implement the trigger in Sidebar.tsx**

Add imports: `SubAccountPicker` from `'./components/SubAccountPicker'`, and `ChevronDown` to the existing `lucide-react` import.

Add state beside `footerMenuOpen`:

```tsx
const [switcherOpen, setSwitcherOpen] = useState(false)
const switcherAvailable = (subAccounts?.length ?? 0) > 1 && Boolean(onSwitchAccount)
```

Replace the `<div className="side-brand">…</div>` block (icon + lockup) with:

```tsx
<div className="side-brand">
  {switcherAvailable ? (
    <button
      type="button"
      className="side-brand-trigger"
      aria-haspopup="menu"
      aria-expanded={switcherOpen}
      aria-label="Switch sub-account"
      onClick={() => {
        const opening = !switcherOpen
        setSwitcherOpen(opening)
        if (opening) refreshAccounts?.()
        console.info(`[admin-ui] op=subaccount-switcher-toggle anchor=brand-head open=${opening} available=true accounts=${subAccounts?.length ?? 0}`)
      }}
    >
      <img className="side-brand-icon" src={brandIcon} alt="" aria-hidden="true" />
      <span className="side-brand-lockup">
        <span className="side-brand-business">{displayName}</span>
        <span className="side-brand-product">{brand.productName}</span>
      </span>
      <ChevronDown size={14} className="side-brand-caret" aria-hidden="true" />
    </button>
  ) : (
    <>
      <img className="side-brand-icon" src={brandIcon} alt="" aria-hidden="true" />
      <span className="side-brand-lockup">
        <span className="side-brand-business">{displayName}</span>
        <span className="side-brand-product">{brand.productName}</span>
      </span>
    </>
  )}
  {switcherOpen && (
    <>
      <div className="side-brand-scrim" aria-hidden="true" onClick={() => setSwitcherOpen(false)} />
      <SubAccountPicker
        open={switcherOpen}
        accounts={subAccounts ?? []}
        activeAccountId={activeAccountId ?? null}
        switching={switchingAccount ?? false}
        onSelect={(accountId) => onSwitchAccount?.(accountId)}
        onClose={() => setSwitcherOpen(false)}
      />
    </>
  )}
</div>
```

In the footer `<AccountMenu … />`, delete the five props `subAccounts`, `activeAccountId`, `switchingAccount`, `onSwitchAccount`, `refreshAccounts` (the Sidebar keeps consuming them for the head). Leave `onOpenConversations` and the rest.

- [ ] **Step 4: Run — expect pass**

Run: `npx vitest run app/__tests__/Sidebar-subaccount-switcher.test.tsx`
Expected: PASS.

- [ ] **Step 5: Fix the footer test that asserted the switcher was in the kebab**

In `app/__tests__/Sidebar-user-footer.test.tsx`, the test `threads the sub-account switcher and Conversations into the kebab menu` (lines 53-75) now asserts a false thing. Rewrite it to cover Conversations only:

```tsx
it('threads Conversations into the kebab menu (switcher moved to brand head, Task 1942)', async () => {
  routedFetch({ cpuPct: null, memUsedPct: 0.5, platform: 'darwin' })
  const onOpenConversations = vi.fn()
  render(<Sidebar {...baseProps({ onOpenConversations })} />)
  fireEvent.click(screen.getByRole('button', { name: 'Account menu' }))
  expect(screen.queryByRole('menuitem', { name: /switch account/i })).toBeNull()
  fireEvent.click(await screen.findByRole('menuitem', { name: /conversations/i }))
  expect(onOpenConversations).toHaveBeenCalledTimes(1)
})
```

- [ ] **Step 6: Run both Sidebar suites — expect pass**

Run: `npx vitest run app/__tests__/Sidebar-subaccount-switcher.test.tsx app/__tests__/Sidebar-user-footer.test.tsx`
Expected: PASS.

- [ ] **Step 7: Commit**

```bash
git add maxy-code/platform/ui/app/Sidebar.tsx maxy-code/platform/ui/app/__tests__/Sidebar-subaccount-switcher.test.tsx maxy-code/platform/ui/app/__tests__/Sidebar-user-footer.test.tsx
git commit -m "feat(1942): brand head opens the sub-account picker"
```

---

### Task 3: CSS — anchor the panel under the head, style the trigger, drop dead overrides

**Files:**
- Modify: `app/globals.css`

**Interfaces:** consumes class names `side-brand`, `side-brand-trigger`, `side-brand-caret`, `side-brand-scrim`, `sub-account-picker-panel` from Task 2.

- [ ] **Step 1: Add `position: relative` to `.side-brand`**

In the `.side-brand { … }` rule, add `position: relative;`.

- [ ] **Step 2: Add the trigger, caret, and scrim rules** (place beside `.side-brand`)

```css
.side-brand-trigger {
  display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0;
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: inherit; text-align: left; cursor: pointer;
}
.side-brand-caret { flex: none; color: var(--text-tertiary); }
.side-brand-trigger:hover .side-brand-caret { color: var(--text-secondary); }
.platform.sidebar-collapsed .side-brand-caret { display: none; }
.side-brand-scrim { position: fixed; inset: 0; z-index: 40; }
.platform.sidebar-collapsed .side-brand-scrim { display: none; }
```

- [ ] **Step 3: Re-anchor `.sub-account-picker-panel` under the head**

The brand head is now the only consumer. In `.sub-account-picker-panel`, remove `left: 50%;` and `transform: translateX(-50%);`, remove the `min-width` (if present), and set `left: 0; right: 0; margin-top: 8px; z-index: 41;` so the panel spans the head's inner width and sits above the scrim.

- [ ] **Step 4: Delete the dead footer overrides**

Delete the `.side-foot-menu .sub-account-picker-panel { … }` and `.side-foot-menu .sub-account-picker-list { … }` rules and the comment above them that describes the footer kebab anchoring.

- [ ] **Step 5: Run the class-name CSS gate + the switcher suites**

Run: `npm run -s check:classname-css 2>/dev/null || node scripts/check-classname-css.mjs`
Then: `npx vitest run app/__tests__/Sidebar-subaccount-switcher.test.tsx`
Expected: gate clean; tests PASS.

- [ ] **Step 6: Commit**

```bash
git add maxy-code/platform/ui/app/globals.css
git commit -m "style(1942): anchor sub-account picker under the sidebar brand head"
```

---

### Task 4: Docs

**Files:**
- Modify: `platform/plugins/docs/references/admin-ui.md`

- [ ] **Step 1: Update the switcher location**

Find the passage describing the sub-account switcher opening from the footer kebab / user-footer AccountMenu. Rewrite it to state the switcher opens by clicking the sidebar brand head (the account name), with the picker dropping beneath it; note the footer kebab no longer carries it. Keep it to the existing register; no task-id string in the shipped doc.

- [ ] **Step 2: Commit**

```bash
git add maxy-code/platform/plugins/docs/references/admin-ui.md
git commit -m "docs(1942): sub-account switcher now opens from the sidebar brand head"
```

## Self-Review

- **Spec coverage:** brand-head trigger (T2), whole-head + chevron + scrim decisions (T2/T3), AccountMenu strip (T1), CSS re-anchor + dead-override delete (T3), observability toggle log (T2 Step 3), docs (T4), test updates incl. the footer-test fix (T2 Step 5). All spec items mapped.
- **Placeholder scan:** none; every code step shows code.
- **Type consistency:** `SubAccountPicker` prop names (`open`, `accounts`, `activeAccountId`, `switching`, `onSelect`, `onClose`) match its component signature; Sidebar prop names match the existing signature.

## Known follow-ups

None at plan time. Any code-review finding out of scope is filed as a new `.tasks/` file per the sprint rules.
