# 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` (renders a `` (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.