# Org Read UI Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Let a member browse and view org items in the browser — a context switcher (Personal + each org), grant-filtered list/detail reusing the existing renderers, and an offline indicator.

**Architecture:** UI-only. All org data comes from the SW messages Plan 1 produced, sent through the `shared/state.ts` `sendMessage` channel. The current context lives in `PopupState.orgContext`; list/detail data-loading branches on it (`list_items` vs `org_list_items`, `get_item` vs `org_get_item`) but reuses the same `popup/components/*` renderers via the `StateHost` service locator, so org items render with the unchanged per-type detail views.

**Tech Stack:** TypeScript, vitest + happy-dom.

## Global Constraints

- Release target: v0.9.0.
- Reuse existing `popup/components/*` renderers via `shared/state.ts` — do NOT fork per-type views for org.
- This plan is READ-ONLY: no add/edit/delete UI (Plan 3). In org context, hide write affordances.
- Org messages are popup-class (sent only from `popup.html` / `vault.html`).
- Consume Plan 1's contract verbatim: `org_list_configs`, `org_switch {context}`, `org_list_items`, `org_get_item {id}`, `org_list_collections`.
- Keep `manifest.json` and `manifest.firefox.json` in sync if permissions change (they should not for this plan).
- Capitalize "Relicario" in prose.

---

## File Structure

- `extension/src/shared/popup-state.ts` — add `orgContext`, `orgConfigs`, `orgCollections`, `orgOffline` to `PopupState`.
- `extension/src/shared/org-context.ts` *(new)* — `currentContext()`, `messageForList()`, `messageForGet(id)` helpers that pick the personal vs org message by context (single source of truth, consumed by list + detail).
- `extension/src/popup/components/org-switcher.ts` *(new)* — the Personal/org selector + offline banner; mounted in both the popup header and the vault sidebar header.
- `extension/src/popup/components/item-list.ts` — load via `messageForList()`; hide the "+ new" affordance in org context.
- `extension/src/popup/components/item-detail.ts` — load via `messageForGet(id)`.
- `extension/src/vault/vault-sidebar.ts` — mount `org-switcher` in `vault-sidebar__header`; add a collection facet for org context.
- `extension/src/popup/popup.ts` — mount `org-switcher` in the popup header.
- Tests: `extension/src/popup/components/__tests__/org-switcher.test.ts`, `org-context.test.ts`, and additions to `item-list.test.ts`.

---

### Task 1: `PopupState` org fields + context message helper

**Files:**
- Modify: `extension/src/shared/popup-state.ts`
- Create: `extension/src/shared/org-context.ts`
- Test: `extension/src/shared/__tests__/org-context.test.ts`

**Interfaces:**
- Produces: `PopupState.orgContext: 'personal' | string` (default `'personal'`), `orgConfigs: OrgConfigSummary[]`, `orgCollections: Collection[]`, `orgOffline: boolean`; `currentContext(): 'personal' | string`; `messageForList(): Request`; `messageForGet(id: string): Request`.

- [ ] **Step 1: Write the failing test**

```ts
import { messageForList, messageForGet } from '../org-context';
import * as state from '../state';

test('messageForList/Get pick personal vs org by current context', () => {
  vi.spyOn(state, 'getState').mockReturnValue({ orgContext: 'personal' } as any);
  expect(messageForList()).toEqual({ type: 'list_items' });
  expect(messageForGet('x')).toEqual({ type: 'get_item', id: 'x' });

  vi.spyOn(state, 'getState').mockReturnValue({ orgContext: 'org-1' } as any);
  expect(messageForList()).toEqual({ type: 'org_list_items' });
  expect(messageForGet('x')).toEqual({ type: 'org_get_item', id: 'x' });
});
```

- [ ] **Step 2: Run to verify it fails**

Run: `cd extension && npx vitest run src/shared/__tests__/org-context.test.ts`
Expected: FAIL — `org-context` module missing.

- [ ] **Step 3: Implement**

```ts
// org-context.ts
import { getState } from './state';
import type { Request } from './messages';

export function currentContext(): 'personal' | string {
  return getState().orgContext ?? 'personal';
}
export function messageForList(): Request {
  return currentContext() === 'personal' ? { type: 'list_items' } : { type: 'org_list_items' };
}
export function messageForGet(id: string): Request {
  return currentContext() === 'personal' ? { type: 'get_item', id } : { type: 'org_get_item', id };
}
```

Add the four fields to `PopupState` in `popup-state.ts` (defaults: `orgContext: 'personal'`, `orgConfigs: []`, `orgCollections: []`, `orgOffline: false`).

- [ ] **Step 4: Run to verify it passes**

Run: `cd extension && npx vitest run src/shared/__tests__/org-context.test.ts`
Expected: PASS.

- [ ] **Step 5: Type-check + commit**

Run: `cd extension && npm run build:all`

```bash
git add extension/src/shared/popup-state.ts extension/src/shared/org-context.ts extension/src/shared/__tests__/org-context.test.ts
git commit -m "feat(ext): PopupState org fields + context-aware message helper"
```

---

### Task 2: Org switcher component

**Files:**
- Create: `extension/src/popup/components/org-switcher.ts`
- Test: `extension/src/popup/components/__tests__/org-switcher.test.ts`

**Interfaces:**
- Consumes: `org_list_configs`, `org_switch` (via `sendMessage`); `setState`, `navigate`.
- Produces: `renderOrgSwitcher(host: HTMLElement): Promise<void>` (renders a `<select>` of Personal + each org, an offline badge), `teardown()`.

- [ ] **Step 1: Write the failing test** (mock `shared/state`, the established component-test pattern)

```ts
import { renderOrgSwitcher } from '../org-switcher';
import * as state from '../../../shared/state';

test('switching to an org sends org_switch and reloads the list', async () => {
  const send = vi.spyOn(state, 'sendMessage').mockImplementation(async (req: any) => {
    if (req.type === 'org_list_configs') return { ok: true, data: [{ orgId: 'org-1', displayName: 'Acme' }] };
    if (req.type === 'org_switch') return { ok: true, data: { context: 'org-1', offline: false } };
    return { ok: true, data: [] };
  });
  const nav = vi.spyOn(state, 'navigate').mockImplementation(() => {});
  const host = document.createElement('div');
  await renderOrgSwitcher(host);
  const sel = host.querySelector('select') as HTMLSelectElement;
  sel.value = 'org-1'; sel.dispatchEvent(new Event('change'));
  await Promise.resolve();
  expect(send).toHaveBeenCalledWith({ type: 'org_switch', context: 'org-1' });
  expect(nav).toHaveBeenCalledWith('list', expect.anything());
});
```

- [ ] **Step 2: Run to verify it fails**

Run: `cd extension && npx vitest run src/popup/components/__tests__/org-switcher.test.ts`
Expected: FAIL — `org-switcher` missing.

- [ ] **Step 3: Implement** — fetch `org_list_configs`, render `<select>` (Personal + each), on `change` send `org_switch`, write `setState({ orgContext, orgOffline })`, then `navigate('list', {})` to reload. Render an "org offline — writes disabled" badge when `data.offline`.

- [ ] **Step 4: Run to verify it passes**

Run: `cd extension && npx vitest run src/popup/components/__tests__/org-switcher.test.ts`
Expected: PASS.

- [ ] **Step 5: Mount in both surfaces + commit** — call `renderOrgSwitcher` into the popup header (`popup.ts`) and the `vault-sidebar__header` (`vault-sidebar.ts`, after the brand block at `:26-29`).

Run: `cd extension && npm run build:all`

```bash
git add extension/src/popup/components/org-switcher.ts extension/src/popup/popup.ts extension/src/vault/vault-sidebar.ts extension/src/popup/components/__tests__/org-switcher.test.ts
git commit -m "feat(ext): org context switcher (popup header + vault sidebar)"
```

---

### Task 3: List + detail consume the context-aware data source

**Files:**
- Modify: `extension/src/popup/components/item-list.ts`, `item-detail.ts`
- Test: additions to `extension/src/popup/components/__tests__/item-list.test.ts`

**Interfaces:**
- Consumes: `messageForList()` / `messageForGet(id)` (Task 1).

- [ ] **Step 1: Write the failing test**

```ts
test('item-list loads org items (grant-filtered) when context is an org', async () => {
  vi.spyOn(state, 'getState').mockReturnValue({ orgContext: 'org-1', items: [] } as any);
  const send = vi.spyOn(state, 'sendMessage').mockResolvedValue({ ok: true, data: [
    { id: 'a', title: 'db', collection: 'prod-infra', modified: 1 },
  ]});
  await renderItemList(document.createElement('div'));
  expect(send).toHaveBeenCalledWith({ type: 'org_list_items' });
});
```

- [ ] **Step 2: Run to verify it fails**

Run: `cd extension && npx vitest run src/popup/components/__tests__/item-list.test.ts -t "org items"`
Expected: FAIL — list still sends `list_items`.

- [ ] **Step 3: Implement** — replace the hard-coded `sendMessage({ type: 'list_items' })` in `item-list.ts` with `sendMessage(messageForList())`, and `get_item` in `item-detail.ts` with `messageForGet(id)`. In org context, hide the "+ new item" button (read-only this plan).

- [ ] **Step 4: Run to verify it passes**

Run: `cd extension && npx vitest run src/popup/components/__tests__/item-list.test.ts`
Expected: PASS (personal path unchanged: context `'personal'` → `list_items`).

- [ ] **Step 5: Type-check + commit**

Run: `cd extension && npm run build:all`

```bash
git add extension/src/popup/components/item-list.ts extension/src/popup/components/item-detail.ts extension/src/popup/components/__tests__/item-list.test.ts
git commit -m "feat(ext): list/detail load org items in org context"
```

---

### Task 4: Collection facet in the vault sidebar (org context)

**Files:**
- Modify: `extension/src/vault/vault-sidebar.ts`, `extension/src/vault/vault-context.ts` (filter helper)
- Test: `extension/src/popup/components/__tests__/org-switcher.test.ts` (extend) or a new `vault-sidebar` test

**Interfaces:**
- Consumes: `org_list_collections`; `PopupState.orgCollections`.
- Produces: a collection nav list (parallel to the type-category nav) shown only in org context; selecting a collection filters the org list to that slug.

- [ ] **Step 1: Write the failing test**

```ts
test('org context renders a collection facet from org_list_collections', async () => {
  vi.spyOn(state, 'getState').mockReturnValue({ orgContext: 'org-1', orgCollections: [
    { slug: 'prod-infra', display_name: 'Production Infra' },
  ]} as any);
  const el = document.createElement('div');
  renderCollectionFacet(el);
  expect(el.textContent).toContain('Production Infra');
});
```

- [ ] **Step 2: Run to verify it fails**

Run: `cd extension && npx vitest run -t "collection facet"`
Expected: FAIL — `renderCollectionFacet` missing.

- [ ] **Step 3: Implement** — on `org_switch` success, fetch `org_list_collections` into `state.orgCollections`; render a collection list in the sidebar (reuse the category-nav markup pattern at `vault-sidebar.ts:33`); clicking a collection sets a `collectionFilter` in state and re-renders the filtered list. Hidden when `orgContext === 'personal'`.

- [ ] **Step 4: Run to verify it passes**

Run: `cd extension && npx vitest run -t "collection facet"`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add extension/src/vault/vault-sidebar.ts extension/src/vault/vault-context.ts
git commit -m "feat(ext): collection facet for org browse"
```

---

### Task 5: Offline read-only banner

**Files:**
- Modify: `extension/src/popup/components/org-switcher.ts` (or a small `org-banner.ts`)
- Test: `extension/src/popup/components/__tests__/org-switcher.test.ts` (extend)

**Interfaces:**
- Consumes: `org_switch` response `{ offline }`; `PopupState.orgOffline`.

- [ ] **Step 1: Write the failing test**

```ts
test('offline org_switch renders the writes-disabled banner', async () => {
  vi.spyOn(state, 'sendMessage').mockImplementation(async (req: any) =>
    req.type === 'org_switch' ? { ok: true, data: { context: 'org-1', offline: true } }
    : req.type === 'org_list_configs' ? { ok: true, data: [{ orgId: 'org-1', displayName: 'Acme' }] }
    : { ok: true, data: [] });
  const host = document.createElement('div');
  await renderOrgSwitcher(host);
  (host.querySelector('select') as HTMLSelectElement).value = 'org-1';
  host.querySelector('select')!.dispatchEvent(new Event('change'));
  await Promise.resolve();
  expect(host.textContent).toContain('org offline — writes disabled');
});
```

- [ ] **Step 2: Run to verify it fails**

Run: `cd extension && npx vitest run -t "writes-disabled banner"`
Expected: FAIL — no banner.

- [ ] **Step 3: Implement** — when `org_switch` returns `offline: true`, set `state.orgOffline` and render the banner element in the switcher host. (Plan 3's write UI reads `orgOffline` to disable add/edit.)

- [ ] **Step 4: Run to verify it passes**

Run: `cd extension && npx vitest run src/popup/components/__tests__/org-switcher.test.ts`
Expected: PASS.

- [ ] **Step 5: Full suite, type-check, commit**

Run: `cd extension && npx vitest run && npm run build:all`

```bash
git add extension/src/popup/components/org-switcher.ts extension/src/popup/components/__tests__/org-switcher.test.ts
git commit -m "feat(ext): org offline read-only banner"
```

---

## Hand-off note (Plan 3 write builds on this)

Plan 3 adds the write affordances this plan deliberately hid: the "+ new item" button in org context, edit/delete in the org item detail, and a granted-collection picker on add. It reads `PopupState.orgOffline` to disable writes when offline, and `PopupState.orgCollections` for the collection picker. Write operations call the `org_add_item`/`org_update_item`/`org_delete_item` messages Plan 3 adds to the SW.
