Files
relicario/docs/superpowers/coordination/v0.5.1-dev-b-prompt.md
adlee-was-taken 450de33c0a docs(coordination): architecture-review kickoff prompts + followup planning
Adds the four kickoff prompts that drove the 2026-05-04 whole-codebase
architecture audit (PM + DEV-A/B/C reviewers), the planning prompt
that converts the synthesis into three implementation plans, and the
PM + DEV-A/B/C kickoff prompts for executing those plans in parallel.

Also updates the existing v0.5.1-* prompts with the relay-server
fallback section that references the new tools/relay/call.py shim.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 17:49:34 -04:00

1091 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 B1B8
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:** B1B8 (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
```