# Dev B Kickoff Prompt — v0.5.1 Stream B (Settings UX Redesign)

> **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.

Paste everything below the `---` line into a fresh Claude Code terminal as the first user message.

---

You are a **senior developer** owning Stream B for the Relicario v0.5.1 release. Stream B replaces the current flat settings dump with a unified left-nav sectioned settings page, integrating device and vault settings into a single coherent view.

**Goal:** Replace `settings.ts` (flat dump) + `settings-vault.ts` (flat dump) with a single settings component that has a left-nav sidebar (Autofill, Display, Security, Generator, Retention, Backup, Import). Wire into `vault.ts` via the agreed interface. Wire `settings-security.ts` from DEV-C into the Security section.

**Architecture:** All changes are in the extension. The settings component lives in `settings.ts` (rewrite). Section content for vault-level settings moves from `settings-vault.ts` into inline render functions within the new layout. DEV-C owns `settings-security.ts`; you stub it and import it.

**Tech Stack:** TypeScript, vitest, webpack/bun.

---

## Setup (do this first)

```bash
cd /home/alee/Sources/relicario
git fetch
git checkout main
git pull
git worktree add ../relicario.v0.5.1-stream-b -b feature/v0.5.1-stream-b-settings
cd ../relicario.v0.5.1-stream-b
pwd  # should print /home/alee/Sources/relicario.v0.5.1-stream-b
```

**ALL subsequent work happens in `/home/alee/Sources/relicario.v0.5.1-stream-b`**. Every subagent prompt MUST begin with `cd /home/alee/Sources/relicario.v0.5.1-stream-b`.

Today: 2026-05-03. Project rules in `CLAUDE.md` apply.

## Required reading

1. `CLAUDE.md` — project rules
2. `docs/superpowers/specs/2026-05-03-v0.5.x-ux-polish-and-recovery-qr-design.md` — spec sections B1–B8
3. `extension/src/popup/components/settings.ts` — current flat settings (your rewrite target)
4. `extension/src/popup/components/settings-vault.ts` — vault settings content to decompose
5. `extension/src/popup/styles.css` — existing CSS (add to, don't break)

## Execution mode

Use **subagent-driven-development**. Fresh subagent per task, two-stage review between tasks. Every subagent prompt MUST start with `cd /home/alee/Sources/relicario.v0.5.1-stream-b`.

## Interface contracts

### With DEV-A (vault.ts wiring)

DEV-A imports these exact exports from `settings.ts` in vault.ts. **You must use exactly these signatures:**

```ts
export async function renderSettings(container: HTMLElement): Promise<void>;
export function teardownSettings(): void;
```

DEV-A calls `renderSettings(pane)` when the settings view is active and `teardownSettings()` when navigating away.

### With DEV-C (settings-security.ts)

DEV-C owns `settings-security.ts`. You import it for the Security section. The agreed interface:

```ts
// extension/src/popup/components/settings-security.ts
export async function renderSecuritySection(
  container: HTMLElement,
  sessionHandle: number | null,
): Promise<void>;
export function teardownSecuritySection(): void;
```

**Task 1** creates a stub `settings-security.ts` with this interface (no-op implementations). DEV-C will replace it on their branch; the real implementation lands when C merges.

## Scope and boundaries

**In scope:** B1–B8 (settings skeleton, all 7 sections, cleanup of old settings-vault.ts surface).

**Out of scope:** Stream A and C work. The Security section's QR and device UI is DEV-C's responsibility — you only wire the import.

**Hard rules:**
- `renderSettings` / `teardownSettings` must be exported with these exact names and signatures.
- Device sections read/write `chrome.storage.local`. Vault sections call `sendMessage` to the service worker.
- Don't merge to main. The PM owns merges.

## Relay server

A message-bus MCP server is running on `localhost:7331`. You have three native tools:

- `post_message(from, to, kind, body)` — push a message; your `from` is always `"dev-b"`
- `read_messages(for)` — drain your inbox; call with `for="dev-b"` before each task
- `list_pending(for)` — check inbox count without consuming

Recipients: `pm`, `dev-a`, `dev-b`, `dev-c`. Use these instead of asking the user to copy-paste. Before starting each task: `read_messages(for="dev-b")`. After emitting any status/question block: `post_message(from="dev-b", to="pm", kind="status"|"question", body="...")`.

## Coordination protocol

Before starting each task, call `read_messages(for="dev-b")` to drain your inbox.

When posting a status update, call `post_message(from="dev-b", to="pm", kind="status", body="...")` with the body:

```
## STATUS UPDATE — DEV-B
Time: <iso8601>
Task: <N of 10>
Status: IN-PROGRESS | BLOCKED | REVIEW-READY
Summary: <one line>
Next: <next task or "waiting for PM">
```

---

## Files

**Create:**
- `extension/src/popup/components/settings-security.ts` — stub (DEV-C replaces real implementation)
- `extension/src/popup/components/__tests__/settings-nav.test.ts` — left-nav structure tests

**Modify:**
- `extension/src/popup/components/settings.ts` — full rewrite as sectioned layout
- `extension/src/popup/components/settings-vault.ts` — decompose into section functions; may be removed at the end
- `extension/src/popup/styles.css` — settings-nav CSS + section styles

---

### Task 1: Stub `settings-security.ts`

This stub satisfies the import used in the Security section (Task 6). DEV-C replaces it with the real implementation.

**Files:**
- Create: `extension/src/popup/components/settings-security.ts`

- [ ] **Step 1: Write the stub**

```ts
// extension/src/popup/components/settings-security.ts
// Stub — real implementation provided by Stream C (DEV-C).

export async function renderSecuritySection(
  container: HTMLElement,
  _sessionHandle: number | null,
): Promise<void> {
  container.innerHTML = `
    <div class="settings-section-placeholder">
      <span class="muted">Security settings — loading…</span>
    </div>
  `;
}

export function teardownSecuritySection(): void {
  // no-op in stub
}
```

- [ ] **Step 2: Build to confirm no TS errors**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
```

- [ ] **Step 3: Commit**

```bash
git add extension/src/popup/components/settings-security.ts
git commit -m "chore(ext/settings): stub settings-security.ts (DEV-C replaces implementation)"
```

---

### Task 2: Settings left-nav skeleton

**Files:**
- Modify: `extension/src/popup/components/settings.ts`
- Modify: `extension/src/popup/styles.css`

The new settings.ts is a full rewrite. It replaces the current flat dump with a two-panel layout: a 148px left-nav sidebar + content area.

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

Create `extension/src/popup/components/__tests__/settings-nav.test.ts`:

```ts
import { describe, it, expect, vi, beforeEach } from 'vitest';

// Mock chrome.storage.local
const storageMock: Record<string, unknown> = {};
vi.stubGlobal('chrome', {
  storage: {
    local: {
      get: vi.fn((keys, cb) => cb(typeof keys === 'string' ? { [keys]: storageMock[keys] } : Object.fromEntries((keys as string[]).map(k => [k, storageMock[k]])))),
      set: vi.fn((data, cb) => { Object.assign(storageMock, data); cb?.(); }),
    },
  },
});

// We can't easily test the full render (no DOM env yet), so we test the
// structural contract — the exported function names exist.
import * as settingsMod from '../settings';

describe('settings module contract', () => {
  it('exports renderSettings as an async function', () => {
    expect(typeof settingsMod.renderSettings).toBe('function');
  });

  it('exports teardownSettings as a function', () => {
    expect(typeof settingsMod.teardownSettings).toBe('function');
  });
});
```

- [ ] **Step 2: Run to confirm the test currently passes (contract already met)**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run test 2>&1 | grep "settings-nav"
```

The test may already pass if current settings.ts exports `renderSettings`. If so, proceed — the test ensures the contract is preserved through the rewrite.

- [ ] **Step 3: Rewrite settings.ts with the new sectioned layout**

```ts
// extension/src/popup/components/settings.ts

import { sendMessage, escapeHtml } from '../../shared/state';
import type { DeviceSettings, VaultSettings } from '../../shared/types';
import {
  loadColorScheme, saveColorScheme, resetColorScheme,
  DEFAULT_DIGIT_COLOR, DEFAULT_SYMBOL_COLOR,
} from '../../shared/color-scheme';
import { colorizePassword } from '../../shared/password-coloring';
import { openGeneratorPanel, closeGeneratorPanel } from './generator-panel';
import { renderSecuritySection, teardownSecuritySection } from './settings-security';

type SettingsSection =
  | 'autofill'
  | 'display'
  | 'security'
  | 'generator'
  | 'retention'
  | 'backup'
  | 'import';

const NAV_ITEMS: Array<{ id: SettingsSection; icon: string; label: string; group: 'device' | 'vault' }> = [
  { id: 'autofill',  icon: '⊙', label: 'Autofill',   group: 'device' },
  { id: 'display',   icon: '◈', label: 'Display',    group: 'device' },
  { id: 'security',  icon: '◉', label: 'Security',   group: 'vault'  },
  { id: 'generator', icon: '↻', label: 'Generator',  group: 'vault'  },
  { id: 'retention', icon: '▦', label: 'Retention',  group: 'vault'  },
  { id: 'backup',    icon: '⤓', label: 'Backup',     group: 'vault'  },
  { id: 'import',    icon: '≡', label: 'Import',     group: 'vault'  },
];

let activeSection: SettingsSection = 'autofill';
let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null;
let pendingVaultSettings: VaultSettings | null = null;

export async function renderSettings(container: HTMLElement): Promise<void> {
  container.innerHTML = `
    <div class="settings-layout">
      <nav class="settings-nav" id="settings-nav">
        <div class="settings-nav__group-label">Device</div>
        ${NAV_ITEMS.filter(n => n.group === 'device').map(navItemHtml).join('')}
        <div class="settings-nav__group-label">Vault</div>
        ${NAV_ITEMS.filter(n => n.group === 'vault').map(navItemHtml).join('')}
      </nav>
      <div class="settings-content" id="settings-content"></div>
    </div>
  `;

  wireNav();
  await renderSection(activeSection);
}

export function teardownSettings(): void {
  closeGeneratorPanel();
  teardownSecuritySection();
  if (activeKeyHandler) {
    document.removeEventListener('keydown', activeKeyHandler);
    activeKeyHandler = null;
  }
  pendingVaultSettings = null;
}

function navItemHtml(item: (typeof NAV_ITEMS)[0]): string {
  const active = item.id === activeSection ? ' settings-nav__item--active' : '';
  return `
    <button class="settings-nav__item${active}" data-section="${item.id}">
      <span class="settings-nav__icon" aria-hidden="true">${item.icon}</span>
      <span class="settings-nav__label">${escapeHtml(item.label)}</span>
    </button>
  `;
}

function wireNav(): void {
  document.getElementById('settings-nav')?.querySelectorAll<HTMLButtonElement>('[data-section]')
    .forEach((btn) => {
      btn.addEventListener('click', async () => {
        teardownSecuritySection();
        closeGeneratorPanel();
        activeSection = btn.dataset.section as SettingsSection;
        // Update active state
        document.querySelectorAll('.settings-nav__item').forEach(b => b.classList.remove('settings-nav__item--active'));
        btn.classList.add('settings-nav__item--active');
        await renderSection(activeSection);
      });
    });
}

async function renderSection(section: SettingsSection): Promise<void> {
  const content = document.getElementById('settings-content');
  if (!content) return;

  switch (section) {
    case 'autofill':  return renderAutofillSection(content);
    case 'display':   return renderDisplaySection(content);
    case 'security':  return renderSecuritySection(content, null); // sessionHandle: null until wired from vault.ts
    case 'generator': return renderGeneratorSection(content);
    case 'retention': return renderRetentionSection(content);
    case 'backup':    return renderBackupSection(content);
    case 'import':    return renderImportSection(content);
  }
}
```

- [ ] **Step 4: Add settings layout CSS**

In `extension/src/popup/styles.css`:

```css
/* === Settings layout === */
.settings-layout {
  display: flex;
  height: 100%;
  overflow: hidden;
}

.settings-nav {
  width: 148px;
  min-width: 148px;
  border-right: 1px solid var(--border, #30363d);
  padding: 12px 0;
  overflow-y: auto;
  flex-shrink: 0;
}

.settings-nav__group-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted, #8b949e);
  padding: 8px 12px 4px;
}

.settings-nav__item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 7px 12px;
  background: transparent;
  border: none;
  cursor: pointer;
  font-size: 13px;
  color: inherit;
  text-align: left;
}

.settings-nav__item:hover { background: var(--bg-hover, #161b22); }
.settings-nav__item--active { background: var(--bg-selected, #1c2d41); }

.settings-nav__icon { font-size: 14px; flex-shrink: 0; }

.settings-content {
  flex: 1;
  overflow-y: auto;
  padding: 20px 24px;
  min-width: 0;
}

/* Setting row (label + description + control) */
.setting-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  padding: 12px 0;
  border-bottom: 1px solid var(--border-subtle, #21262d);
}

.setting-row:last-child { border-bottom: none; }

.setting-row__info { flex: 1; }
.setting-row__title { font-size: 13px; font-weight: 500; }
.setting-row__desc  { font-size: 11px; color: var(--text-muted, #8b949e); margin-top: 2px; }
.setting-row__control { flex-shrink: 0; }

/* Section headings */
.settings-section-title {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted, #8b949e);
  margin: 0 0 12px;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--border, #30363d);
}

/* Setting card (used by Security section) */
.setting-card {
  padding: 12px 16px;
  border-radius: 6px;
  border: 1px solid var(--border, #30363d);
  margin-bottom: 12px;
}

.setting-card--ok   { border-color: var(--success, #238636); background: rgba(35, 134, 54, 0.06); }
.setting-card--warn { border-color: var(--gold, #b8860b);    background: rgba(184, 134, 11, 0.06); }

.setting-card__status  { font-size: 13px; margin-bottom: 8px; }
.setting-card__actions { display: flex; gap: 8px; }
```

- [ ] **Step 5: Build and run tests**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
bun run test 2>&1 | grep "settings-nav"
```

- [ ] **Step 6: Commit**

```bash
git add extension/src/popup/components/settings.ts extension/src/popup/styles.css extension/src/popup/components/__tests__/settings-nav.test.ts
git commit -m "feat(ext/settings): settings left-nav skeleton with section routing"
```

---

### Task 3: Autofill section (Device)

**Files:**
- Modify: `extension/src/popup/components/settings.ts`

- [ ] **Step 1: Implement `renderAutofillSection()`**

Add this function to settings.ts:

```ts
async function renderAutofillSection(content: HTMLElement): Promise<void> {
  const [settingsResp, blacklistResp] = await Promise.all([
    sendMessage({ type: 'get_settings' }),
    sendMessage({ type: 'get_blacklist' }),
  ]);

  const settings: DeviceSettings = settingsResp.ok
    ? (settingsResp.data as { settings: DeviceSettings }).settings
    : { captureEnabled: false, captureStyle: 'bar' };

  const blacklist: string[] = blacklistResp.ok
    ? (blacklistResp.data as { blacklist: string[] }).blacklist
    : [];

  content.innerHTML = `
    <h3 class="settings-section-title">Capture</h3>
    <div class="setting-row">
      <div class="setting-row__info">
        <div class="setting-row__title">Auto-detect logins</div>
        <div class="setting-row__desc">Show a prompt when a login form is detected.</div>
      </div>
      <div class="setting-row__control">
        <label class="toggle">
          <input type="checkbox" id="capture-enabled" ${settings.captureEnabled ? 'checked' : ''}>
          <span class="toggle__track"></span>
        </label>
      </div>
    </div>
    <div class="setting-row">
      <div class="setting-row__info">
        <div class="setting-row__title">Prompt style</div>
        <div class="setting-row__desc">How to prompt when a login is detected.</div>
      </div>
      <div class="setting-row__control" style="display:flex; gap:6px;">
        <button class="btn ${settings.captureStyle === 'bar'   ? 'btn-active' : ''}" id="style-bar"   style="font-size:11px;">bar</button>
        <button class="btn ${settings.captureStyle === 'toast' ? 'btn-active' : ''}" id="style-toast" style="font-size:11px;">toast</button>
      </div>
    </div>

    <h3 class="settings-section-title" style="margin-top:20px;">Blocked sites</h3>
    <div id="blacklist-container">
      ${blacklist.length > 0
        ? blacklist.map((h) => `
          <div class="setting-row">
            <div class="setting-row__info">
              <div class="setting-row__title">${escapeHtml(h)}</div>
            </div>
            <button class="btn btn-danger remove-bl" data-hostname="${escapeHtml(h)}" style="font-size:11px;">remove</button>
          </div>
        `).join('')
        : '<p class="muted" style="font-size:12px;">No blocked sites.</p>'}
    </div>
    <div style="display:flex; gap:6px; margin-top:8px;">
      <input type="text" id="bl-add-input" placeholder="example.com" style="flex:1; font-size:12px;">
      <button class="btn" id="bl-add-btn" style="font-size:11px;">Add</button>
    </div>
  `;

  document.getElementById('capture-enabled')?.addEventListener('change', async (e) => {
    const enabled = (e.target as HTMLInputElement).checked;
    await sendMessage({ type: 'save_settings', settings: { ...settings, captureEnabled: enabled } });
  });

  document.getElementById('style-bar')?.addEventListener('click', async () => {
    await sendMessage({ type: 'save_settings', settings: { ...settings, captureStyle: 'bar' } });
    renderAutofillSection(content);
  });

  document.getElementById('style-toast')?.addEventListener('click', async () => {
    await sendMessage({ type: 'save_settings', settings: { ...settings, captureStyle: 'toast' } });
    renderAutofillSection(content);
  });

  content.querySelectorAll<HTMLButtonElement>('.remove-bl').forEach((btn) => {
    btn.addEventListener('click', async () => {
      const host = btn.dataset.hostname!;
      await sendMessage({ type: 'remove_blacklist_entry', hostname: host });
      renderAutofillSection(content);
    });
  });

  document.getElementById('bl-add-btn')?.addEventListener('click', async () => {
    const input = document.getElementById('bl-add-input') as HTMLInputElement;
    const val = input.value.trim();
    if (!val) return;
    await sendMessage({ type: 'add_blacklist_entry', hostname: val });
    input.value = '';
    renderAutofillSection(content);
  });
}
```

- [ ] **Step 2: Build**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
```

- [ ] **Step 3: Commit**

```bash
git add extension/src/popup/components/settings.ts
git commit -m "feat(ext/settings): autofill section (capture toggle + blacklist)"
```

---

### Task 4: Display section (Device)

**Files:**
- Modify: `extension/src/popup/components/settings.ts`

- [ ] **Step 1: Implement `renderDisplaySection()`**

Extract the existing password-coloring UI from the old settings.ts and move it into the Display section:

```ts
function renderDisplaySection(content: HTMLElement): void {
  const scheme = loadColorScheme();
  content.innerHTML = `
    <h3 class="settings-section-title">Password coloring</h3>
    <div class="setting-row">
      <div class="setting-row__info">
        <div class="setting-row__title">Digit color</div>
      </div>
      <div class="setting-row__control">
        <input type="color" id="digit-color" value="${scheme.digitColor}">
      </div>
    </div>
    <div class="setting-row">
      <div class="setting-row__info">
        <div class="setting-row__title">Symbol color</div>
      </div>
      <div class="setting-row__control">
        <input type="color" id="symbol-color" value="${scheme.symbolColor}">
      </div>
    </div>
    <div class="setting-row">
      <div class="setting-row__info">
        <div class="setting-row__title">Preview</div>
      </div>
      <div class="setting-row__control">
        <span id="color-preview" style="font-family:monospace; font-size:13px;">${colorizePassword('Aa1!Bb2@', scheme)}</span>
      </div>
    </div>
    <div style="display:flex; gap:6px; margin-top:12px;">
      <button class="btn" id="reset-colors" style="font-size:11px;">Reset defaults</button>
    </div>
  `;

  function updatePreview(): void {
    const scheme2 = loadColorScheme();
    const preview = document.getElementById('color-preview');
    if (preview) preview.innerHTML = colorizePassword('Aa1!Bb2@', scheme2);
  }

  document.getElementById('digit-color')?.addEventListener('input', (e) => {
    const color = (e.target as HTMLInputElement).value;
    saveColorScheme({ ...loadColorScheme(), digitColor: color });
    updatePreview();
  });

  document.getElementById('symbol-color')?.addEventListener('input', (e) => {
    const color = (e.target as HTMLInputElement).value;
    saveColorScheme({ ...loadColorScheme(), symbolColor: color });
    updatePreview();
  });

  document.getElementById('reset-colors')?.addEventListener('click', () => {
    resetColorScheme();
    renderDisplaySection(content);
  });
}
```

- [ ] **Step 2: Build**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
```

- [ ] **Step 3: Commit**

```bash
git add extension/src/popup/components/settings.ts
git commit -m "feat(ext/settings): display section (password coloring)"
```

---

### Task 5: Security section stub wire-up

**Files:**
- Modify: `extension/src/popup/components/settings.ts`

The Security section calls `renderSecuritySection()` from `settings-security.ts` (stub from Task 1; DEV-C's real implementation replaces it).

The stub's signature is `renderSecuritySection(container, sessionHandle)`. For now, pass `null` as the session handle — the vault.ts wiring from DEV-A will eventually thread the real handle through. This is acceptable for the stub phase.

- [ ] **Step 1: Verify `renderSection('security')` calls `renderSecuritySection`**

The `renderSection()` function already has:

```ts
case 'security': return renderSecuritySection(content, null);
```

This is already wired in Task 2's settings.ts rewrite. Confirm it builds cleanly.

- [ ] **Step 2: Build and confirm**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
```

- [ ] **Step 3: Commit (only if additional wiring was needed)**

If you had to add anything, commit:

```bash
git add extension/src/popup/components/settings.ts
git commit -m "feat(ext/settings): wire security section to settings-security.ts stub"
```

---

### Task 6: Generator section (Vault)

**Files:**
- Modify: `extension/src/popup/components/settings.ts`

Extract the generator defaults content from `settings-vault.ts`. No functional changes — just consistent styling using `setting-row` pattern.

- [ ] **Step 1: Read the current generator content in settings-vault.ts**

```bash
grep -n "generator\|generate\|Generator" extension/src/popup/components/settings-vault.ts | head -20
```

- [ ] **Step 2: Implement `renderGeneratorSection()`**

```ts
async function renderGeneratorSection(content: HTMLElement): Promise<void> {
  content.innerHTML = '<p class="muted" style="font-size:12px;">Loading…</p>';
  const resp = await sendMessage({ type: 'get_vault_settings' });
  if (!resp.ok) {
    content.innerHTML = `<p class="muted">Failed to load: ${escapeHtml(resp.error ?? 'unknown')}</p>`;
    return;
  }
  const settings = (resp.data as { settings: VaultSettings }).settings;

  content.innerHTML = `
    <h3 class="settings-section-title">Generator defaults</h3>
    <div class="setting-row">
      <div class="setting-row__info">
        <div class="setting-row__title">Configure generator</div>
        <div class="setting-row__desc">Password length, character classes, passphrase word count.</div>
      </div>
      <div class="setting-row__control">
        <button class="btn" id="open-generator-panel" style="font-size:11px;">Configure ▸</button>
      </div>
    </div>
  `;

  document.getElementById('open-generator-panel')?.addEventListener('click', (e) => {
    openGeneratorPanel(e.currentTarget as HTMLElement, settings.generator_defaults, async (req) => {
      await sendMessage({ type: 'save_vault_settings', settings: { ...settings, generator_defaults: req } });
    });
  });
}
```

- [ ] **Step 3: Build**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
```

- [ ] **Step 4: Commit**

```bash
git add extension/src/popup/components/settings.ts
git commit -m "feat(ext/settings): generator section (vault defaults)"
```

---

### Task 7: Retention section (Vault)

**Files:**
- Modify: `extension/src/popup/components/settings.ts`

Extract retention content from `settings-vault.ts`.

- [ ] **Step 1: Read current retention helpers in settings-vault.ts**

```bash
grep -n "retention\|Retention\|trash\|history" extension/src/popup/components/settings-vault.ts | head -30
```

Note the helper functions: `trashRetentionToValue`, `valueToTrashRetention`, `historyRetentionToValue`, `valueToHistoryRetention`. Copy these into settings.ts or import from settings-vault.ts if it stays as a helper module.

- [ ] **Step 2: Implement `renderRetentionSection()`**

```ts
async function renderRetentionSection(content: HTMLElement): Promise<void> {
  content.innerHTML = '<p class="muted" style="font-size:12px;">Loading…</p>';
  const resp = await sendMessage({ type: 'get_vault_settings' });
  if (!resp.ok) {
    content.innerHTML = `<p class="muted">Failed to load: ${escapeHtml(resp.error ?? 'unknown')}</p>`;
    return;
  }
  const settings = (resp.data as { settings: VaultSettings }).settings;
  pendingVaultSettings = { ...settings };

  content.innerHTML = `
    <h3 class="settings-section-title">Trash retention</h3>
    <div class="setting-row">
      <div class="setting-row__info">
        <div class="setting-row__title">Keep deleted items for</div>
        <div class="setting-row__desc">Items in trash older than this are permanently deleted on the next sync.</div>
      </div>
      <div class="setting-row__control">
        <select id="trash-retention" style="font-size:12px;">
          <option value="days:7"    ${trashRetentionToValue(settings.trash_retention) === 'days:7'    ? 'selected' : ''}>7 days</option>
          <option value="days:30"   ${trashRetentionToValue(settings.trash_retention) === 'days:30'   ? 'selected' : ''}>30 days</option>
          <option value="days:90"   ${trashRetentionToValue(settings.trash_retention) === 'days:90'   ? 'selected' : ''}>90 days</option>
          <option value="forever"   ${trashRetentionToValue(settings.trash_retention) === 'forever'   ? 'selected' : ''}>Forever</option>
        </select>
      </div>
    </div>

    <h3 class="settings-section-title" style="margin-top:20px;">Field history retention</h3>
    <div class="setting-row">
      <div class="setting-row__info">
        <div class="setting-row__title">Keep password history for</div>
        <div class="setting-row__desc">History entries older than this are pruned on save.</div>
      </div>
      <div class="setting-row__control">
        <select id="history-retention" style="font-size:12px;">
          <option value="last_n:5"   ${historyRetentionToValue(settings.history_retention) === 'last_n:5'  ? 'selected' : ''}>Last 5</option>
          <option value="last_n:10"  ${historyRetentionToValue(settings.history_retention) === 'last_n:10' ? 'selected' : ''}>Last 10</option>
          <option value="days:90"    ${historyRetentionToValue(settings.history_retention) === 'days:90'   ? 'selected' : ''}>90 days</option>
          <option value="days:365"   ${historyRetentionToValue(settings.history_retention) === 'days:365'  ? 'selected' : ''}>1 year</option>
          <option value="forever"    ${historyRetentionToValue(settings.history_retention) === 'forever'   ? 'selected' : ''}>Forever</option>
        </select>
      </div>
    </div>
    <div style="margin-top:12px;">
      <button class="btn btn-primary" id="save-retention" style="font-size:11px;">Save retention settings</button>
    </div>
  `;

  document.getElementById('trash-retention')?.addEventListener('change', (e) => {
    if (pendingVaultSettings) {
      pendingVaultSettings.trash_retention = valueToTrashRetention((e.target as HTMLSelectElement).value);
    }
  });

  document.getElementById('history-retention')?.addEventListener('change', (e) => {
    if (pendingVaultSettings) {
      pendingVaultSettings.history_retention = valueToHistoryRetention((e.target as HTMLSelectElement).value);
    }
  });

  document.getElementById('save-retention')?.addEventListener('click', async () => {
    if (!pendingVaultSettings) return;
    const r = await sendMessage({ type: 'save_vault_settings', settings: pendingVaultSettings });
    if (!r.ok) alert(`Save failed: ${r.error}`);
  });
}

// Copy retention helpers from settings-vault.ts:
function trashRetentionToValue(r: import('../../shared/types').TrashRetention): string {
  if (r.kind === 'forever') return 'forever';
  return `days:${r.value}`;
}

function valueToTrashRetention(v: string): import('../../shared/types').TrashRetention {
  if (v === 'forever') return { kind: 'forever' };
  const m = /^days:(\d+)$/.exec(v);
  if (m) return { kind: 'days', value: Number(m[1]) };
  return { kind: 'forever' };
}

function historyRetentionToValue(r: import('../../shared/types').HistoryRetention): string {
  if (r.kind === 'forever') return 'forever';
  if (r.kind === 'last_n') return `last_n:${r.value}`;
  return `days:${r.value}`;
}

function valueToHistoryRetention(v: string): import('../../shared/types').HistoryRetention {
  if (v === 'forever') return { kind: 'forever' };
  const mLast = /^last_n:(\d+)$/.exec(v);
  if (mLast) return { kind: 'last_n', value: Number(mLast[1]) };
  const mDays = /^days:(\d+)$/.exec(v);
  if (mDays) return { kind: 'days', value: Number(mDays[1]) };
  return { kind: 'forever' };
}
```

- [ ] **Step 3: Build**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
```

- [ ] **Step 4: Commit**

```bash
git add extension/src/popup/components/settings.ts
git commit -m "feat(ext/settings): retention section (trash + field history)"
```

---

### Task 8: Backup section (Vault)

**Files:**
- Modify: `extension/src/popup/components/settings.ts`

- [ ] **Step 1: Read current backup content in settings-vault.ts**

```bash
grep -n "backup\|Backup\|restore\|Restore" extension/src/popup/components/settings-vault.ts | head -20
```

- [ ] **Step 2: Implement `renderBackupSection()`**

Extract the backup/restore content from settings-vault.ts. Keep the same functionality, just restyled with `setting-row` pattern:

```ts
function renderBackupSection(content: HTMLElement): void {
  content.innerHTML = `
    <h3 class="settings-section-title">Backup</h3>
    <div class="setting-row">
      <div class="setting-row__info">
        <div class="setting-row__title">Export backup</div>
        <div class="setting-row__desc">Download an encrypted backup of your entire vault.</div>
      </div>
      <div class="setting-row__control">
        <button class="btn" id="backup-export-btn" style="font-size:11px;">Download backup</button>
      </div>
    </div>
    <div class="setting-row">
      <div class="setting-row__info">
        <div class="setting-row__title">Restore backup</div>
        <div class="setting-row__desc">Restore from a previously exported backup file.</div>
      </div>
      <div class="setting-row__control">
        <button class="btn" id="backup-restore-btn" style="font-size:11px;">Restore…</button>
        <input type="file" id="backup-file-input" accept=".rbak" style="display:none;">
      </div>
    </div>
  `;

  // Wire the backup export button — same logic as settings-vault.ts
  document.getElementById('backup-export-btn')?.addEventListener('click', async () => {
    const btn = document.getElementById('backup-export-btn') as HTMLButtonElement;
    btn.disabled = true;
    btn.textContent = 'Exporting…';
    const resp = await sendMessage({ type: 'export_backup' });
    btn.disabled = false;
    btn.textContent = 'Download backup';
    if (!resp.ok) { alert(`Export failed: ${resp.error}`); return; }
    const data = resp.data as { backup_b64: string; filename: string };
    const bytes = Uint8Array.from(atob(data.backup_b64), c => c.charCodeAt(0));
    const blob = new Blob([bytes], { type: 'application/octet-stream' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url; a.download = data.filename; a.click();
    URL.revokeObjectURL(url);
  });

  document.getElementById('backup-restore-btn')?.addEventListener('click', () => {
    document.getElementById('backup-file-input')?.click();
  });

  document.getElementById('backup-file-input')?.addEventListener('change', async (e) => {
    const file = (e.target as HTMLInputElement).files?.[0];
    if (!file) return;
    const arrayBuf = await file.arrayBuffer();
    const b64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuf)));
    const resp = await sendMessage({ type: 'import_backup', backup_b64: b64 });
    if (resp.ok) {
      alert('Backup restored. The extension will reload.');
      window.location.reload();
    } else {
      alert(`Restore failed: ${resp.error}`);
    }
  });
}
```

- [ ] **Step 3: Build**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
```

- [ ] **Step 4: Commit**

```bash
git add extension/src/popup/components/settings.ts
git commit -m "feat(ext/settings): backup section (export + restore)"
```

---

### Task 9: Import section (Vault)

**Files:**
- Modify: `extension/src/popup/components/settings.ts`

- [ ] **Step 1: Read current import content in settings-vault.ts**

```bash
grep -n "import\|Import\|lastpass\|LastPass" extension/src/popup/components/settings-vault.ts | head -20
```

- [ ] **Step 2: Implement `renderImportSection()`**

```ts
function renderImportSection(content: HTMLElement): void {
  content.innerHTML = `
    <h3 class="settings-section-title">Import</h3>
    <div class="setting-row">
      <div class="setting-row__info">
        <div class="setting-row__title">Import from LastPass</div>
        <div class="setting-row__desc">Import a LastPass CSV export file.</div>
      </div>
      <div class="setting-row__control">
        <button class="btn" id="import-lp-btn" style="font-size:11px;">Choose file…</button>
        <input type="file" id="import-lp-input" accept=".csv" style="display:none;">
      </div>
    </div>
    <div id="import-result" style="margin-top:12px;"></div>
  `;

  document.getElementById('import-lp-btn')?.addEventListener('click', () => {
    document.getElementById('import-lp-input')?.click();
  });

  document.getElementById('import-lp-input')?.addEventListener('change', async (e) => {
    const file = (e.target as HTMLInputElement).files?.[0];
    if (!file) return;
    const text = await file.text();
    const resultDiv = document.getElementById('import-result')!;
    resultDiv.textContent = 'Importing…';
    const resp = await sendMessage({ type: 'import_lastpass', csv: text });
    if (resp.ok) {
      const data = resp.data as { imported: number; warnings: string[] };
      resultDiv.innerHTML = `
        <p style="color:var(--success,#238636);">Imported ${data.imported} items.</p>
        ${data.warnings.length > 0 ? `<p style="color:var(--gold,#b8860b);">${data.warnings.length} warning(s): ${escapeHtml(data.warnings.slice(0,3).join('; '))}${data.warnings.length > 3 ? '…' : ''}</p>` : ''}
      `;
    } else {
      resultDiv.innerHTML = `<p style="color:var(--danger,#ab2b20);">Import failed: ${escapeHtml(resp.error ?? 'unknown')}</p>`;
    }
  });
}
```

- [ ] **Step 3: Build and run all tests**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
bun run test 2>&1 | tail -15
```

Expected: all tests pass.

- [ ] **Step 4: Commit**

```bash
git add extension/src/popup/components/settings.ts
git commit -m "feat(ext/settings): import section (LastPass CSV)"
```

---

### Task 10: Cleanup + full build pass

**Files:**
- Modify: `extension/src/popup/components/settings-vault.ts`

The old `renderVaultSettings()` exported function was called from popup.ts and vault.ts. Check all call sites.

- [ ] **Step 1: Find all callers of settings-vault.ts exports**

```bash
grep -rn "renderVaultSettings\|settings-vault\|settingsVault" extension/src/ | grep -v ".test.ts" | grep -v "__tests__"
```

- [ ] **Step 2: Assess whether settings-vault.ts can be deleted**

If all callers have been migrated to the new settings.ts sections, and no other code imports from it, it can be deleted. If it's still imported somewhere, keep it as a thin stub or migrate the remaining callers first.

If safe to delete:

```bash
git rm extension/src/popup/components/settings-vault.ts
git commit -m "refactor(ext/settings): remove settings-vault.ts (content merged into sectioned settings)"
```

If still needed (some callers remain), leave it and note the remaining callers in your status update to PM.

- [ ] **Step 3: Final build check — both targets**

```bash
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | tail -10
bun run build:firefox 2>&1 | tail -10
```

Expected: both build clean.

- [ ] **Step 4: Run all tests**

```bash
bun run test 2>&1 | tail -15
```

Expected: all pass.

- [ ] **Step 5: Open PR**

```bash
gh pr create --title "feat: settings UX redesign — left-nav sectioned layout (Stream B)" --base main
```

- [ ] **Step 6: Post status to PM**

Call `post_message(from="dev-b", to="pm", kind="status", body="...")` with:

```
## STATUS UPDATE — DEV-B
Time: <iso8601>
Task: 10 of 10
Status: REVIEW-READY
Summary: All 10 tasks complete. PR open. All sections implemented. Build clean. Tests pass.
Next: waiting for PM
```
