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>
38 KiB
Dev B Kickoff Prompt — v0.5.1 Stream B (Settings UX Redesign)
For agentic workers: REQUIRED SUB-SKILL: Use
superpowers:subagent-driven-development(recommended) orsuperpowers:executing-plansto 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)
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
CLAUDE.md— project rulesdocs/superpowers/specs/2026-05-03-v0.5.x-ux-polish-and-recovery-qr-design.md— spec sections B1–B8extension/src/popup/components/settings.ts— current flat settings (your rewrite target)extension/src/popup/components/settings-vault.ts— vault settings content to decomposeextension/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:
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:
// 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/teardownSettingsmust be exported with these exact names and signatures.- Device sections read/write
chrome.storage.local. Vault sections callsendMessageto 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; yourfromis always"dev-b"read_messages(for)— drain your inbox; call withfor="dev-b"before each tasklist_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 layoutextension/src/popup/components/settings-vault.ts— decompose into section functions; may be removed at the endextension/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
// 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
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
- Step 3: Commit
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:
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)
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
// 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:
/* === 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
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
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:
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
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
- Step 3: Commit
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:
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
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
- Step 3: Commit
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')callsrenderSecuritySection
The renderSection() function already has:
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
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:
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
grep -n "generator\|generate\|Generator" extension/src/popup/components/settings-vault.ts | head -20
- Step 2: Implement
renderGeneratorSection()
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
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
- Step 4: Commit
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
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()
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
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
- Step 4: Commit
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
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:
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
cd /home/alee/Sources/relicario.v0.5.1-stream-b/extension && bun run build 2>&1 | grep "error TS"
- Step 4: Commit
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
grep -n "import\|Import\|lastpass\|LastPass" extension/src/popup/components/settings-vault.ts | head -20
- Step 2: Implement
renderImportSection()
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
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
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
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:
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
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
bun run test 2>&1 | tail -15
Expected: all pass.
- Step 5: Open PR
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