Files
relicario/docs/superpowers/coordination/v0.5.1-dev-a-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

44 KiB
Raw Blame History

Dev A Kickoff Prompt — v0.5.1 Stream A (Fullscreen + Popup Layout)

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 A for the Relicario v0.5.1 release. Stream A is the fullscreen vault tab layout overhaul + popup polish: 3-column layout (sidebar category nav, full-width list, slide-in drawer), bottom sheet for new-item type picker, popup type-picker polish, per-type glyph icons, and a shared toast system.

Goal: Replace the current 2-column vault tab (sidebar + single pane) with a responsive 3-column layout. All emoji type icons become Unicode monochrome glyphs. A shared toast module replaces ad-hoc status divs.

Architecture: All changes are in the extension. vault.ts is a full layout rewrite. item-list.ts and item-form.ts get glyph replacements and polish. A new toast.ts provides the shared notification API. CSS lives in vault.css (fullscreen) and popup/styles.css (popup).

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-a -b feature/v0.5.1-stream-a-layout
cd ../relicario.v0.5.1-stream-a
pwd  # should print /home/alee/Sources/relicario.v0.5.1-stream-a

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

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 A1A7
  3. extension/src/vault/vault.ts — current implementation (read fully before editing)
  4. extension/src/vault/vault.css — current styles
  5. extension/src/popup/components/item-list.ts — popup item list
  6. extension/src/popup/components/item-form.ts — type-picker
  7. extension/src/shared/glyphs.ts — existing glyph constants

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

Interface contract with DEV-B (settings component)

DEV-B will deliver a settings component with these exports. You must use exactly this signature in vault.ts when wiring the settings view:

// extension/src/popup/components/settings.ts
export async function renderSettings(container: HTMLElement): Promise<void>;
export function teardownSettings(): void;

Your vault.ts should call renderSettings(pane) when the #settings route is active, and teardownSettings() when navigating away. You can proceed with a stub import while DEV-B's branch is in progress; it will be reconciled at merge time.

Scope and boundaries

In scope: A1A7 (layout, drawer, bottom sheet, type picker, glyphs, empty states, toast).

Out of scope: Stream B and C work. If you hit a bug outside your scope, file a ## QUESTION TO PM block.

Hard rules:

  • No emoji anywhere in extension/src/. If you see one while editing, replace it with the monochrome glyph.
  • glyphs.ts is the single source of truth. No inline Unicode literals at call sites.
  • 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-a"
  • read_messages(for) — drain your inbox; call with for="dev-a" 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-a"). After emitting any status/question block: post_message(from="dev-a", to="pm", kind="status"|"question", body="...").

Coordination protocol

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

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

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

Files

Create:

  • extension/src/shared/toast.ts — shared notification module

Modify:

  • extension/src/shared/glyphs.ts — add GLYPH_VAULT_TAB + 7 per-type constants
  • extension/src/shared/__tests__/glyphs.test.ts — add new constants to the test
  • extension/src/popup/components/item-list.ts — glyph vault-btn, type icons, empty states, toast
  • extension/src/popup/components/item-form.ts — TYPE_OPTIONS glyphs, polished type picker
  • extension/src/vault/vault.ts — full layout rewrite
  • extension/src/vault/vault.css — 3-column layout, drawer, bottom sheet, responsive
  • extension/src/popup/styles.css — toast styles, type-icon pill, empty-state styles

Task 1: Add GLYPH_VAULT_TAB and per-type glyph constants

Files:

  • Modify: extension/src/shared/glyphs.ts

  • Modify: extension/src/shared/__tests__/glyphs.test.ts

  • Step 1: Update the failing test first

In extension/src/shared/__tests__/glyphs.test.ts, add to the existing test:

it('exports GLYPH_VAULT_TAB as U+29C9', () => {
  expect(glyphs.GLYPH_VAULT_TAB).toBe('⧉');
});

it('exports per-type glyph constants', () => {
  expect(glyphs.GLYPH_TYPE_LOGIN).toBe('◉');
  expect(glyphs.GLYPH_TYPE_SECURE_NOTE).toBe('◫');
  expect(glyphs.GLYPH_TYPE_TOTP).toBe('⊡');
  expect(glyphs.GLYPH_TYPE_CARD).toBe('▭');
  expect(glyphs.GLYPH_TYPE_IDENTITY).toBe('⌬');
  expect(glyphs.GLYPH_TYPE_KEY).toBe('⊹');
  expect(glyphs.GLYPH_TYPE_DOCUMENT).toBe('≡');
});

it('per-type glyphs are single codepoints (no emoji)', () => {
  const typeGlyphs = [
    glyphs.GLYPH_TYPE_LOGIN, glyphs.GLYPH_TYPE_SECURE_NOTE, glyphs.GLYPH_TYPE_TOTP,
    glyphs.GLYPH_TYPE_CARD, glyphs.GLYPH_TYPE_IDENTITY, glyphs.GLYPH_TYPE_KEY,
    glyphs.GLYPH_TYPE_DOCUMENT,
  ];
  for (const g of typeGlyphs) {
    expect([...g].length).toBe(1);
  }
});
  • Step 2: Run tests to confirm they fail
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run test 2>&1 | grep -E "FAIL|✗|×"

Expected: the new test cases fail (constants not exported yet).

  • Step 3: Add constants to glyphs.ts

In extension/src/shared/glyphs.ts, add after the existing GLYPH_NEXT line:

export const GLYPH_VAULT_TAB      = '⧉';   // U+29C9 pop-out to fullscreen vault tab

export const GLYPH_TYPE_LOGIN       = '◉';  // login
export const GLYPH_TYPE_SECURE_NOTE = '◫';  // secure note
export const GLYPH_TYPE_TOTP        = '⊡';  // totp / 2FA
export const GLYPH_TYPE_CARD        = '▭';  // card
export const GLYPH_TYPE_IDENTITY    = '⌬';  // identity
export const GLYPH_TYPE_KEY         = '⊹';  // SSH / API key
export const GLYPH_TYPE_DOCUMENT    = '≡';  // document
  • Step 4: Run tests to confirm they pass
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run test 2>&1 | grep -E "PASS|✓|Tests"

Expected: all glyph tests pass.

  • Step 5: Commit
git add extension/src/shared/glyphs.ts extension/src/shared/__tests__/glyphs.test.ts
git commit -m "feat(ext/glyphs): add GLYPH_VAULT_TAB and per-type icon constants"

Task 2: Replace &#x2934; vault button in item-list.ts

Files:

  • Modify: extension/src/popup/components/item-list.ts

  • Step 1: Update the import

In item-list.ts, find the imports section and add GLYPH_VAULT_TAB to the glyphs import:

import { GLYPH_VAULT_TAB } from '../../shared/glyphs';
  • Step 2: Replace the inline HTML entity

In item-list.ts:69, change:

// Old:
<button class="btn" id="vault-btn" style="font-size:11px;" title="Open vault (Shift+F)">&#x2934;</button>

// New:
<button class="btn" id="vault-btn" style="font-size:11px;" title="Open vault (Shift+F)">${GLYPH_VAULT_TAB}</button>
  • Step 3: Build and check for TS errors
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
  • Step 4: Commit
git add extension/src/popup/components/item-list.ts
git commit -m "fix(ext/popup): replace inline &#x2934; vault-tab button with GLYPH_VAULT_TAB"

Task 3: Replace emoji typeIcon() in item-list.ts with glyph function

Files:

  • Modify: extension/src/popup/components/item-list.ts

  • Step 1: Add glyph imports to item-list.ts

import {
  GLYPH_VAULT_TAB,
  GLYPH_TYPE_LOGIN, GLYPH_TYPE_SECURE_NOTE, GLYPH_TYPE_TOTP,
  GLYPH_TYPE_CARD, GLYPH_TYPE_IDENTITY, GLYPH_TYPE_KEY, GLYPH_TYPE_DOCUMENT,
} from '../../shared/glyphs';
  • Step 2: Replace typeIcon() in item-list.ts

The existing typeIcon() function (lines ~1626) uses emoji. Replace entirely:

function typeIcon(t: ItemType): string {
  switch (t) {
    case 'login':       return GLYPH_TYPE_LOGIN;
    case 'secure_note': return GLYPH_TYPE_SECURE_NOTE;
    case 'identity':    return GLYPH_TYPE_IDENTITY;
    case 'card':        return GLYPH_TYPE_CARD;
    case 'key':         return GLYPH_TYPE_KEY;
    case 'document':    return GLYPH_TYPE_DOCUMENT;
    case 'totp':        return GLYPH_TYPE_TOTP;
  }
}

Also replace the 📎 paperclip emoji in buildRowsHtml():

// Old:
${e.attachment_summaries.length > 0 ? ' <span class="entry-row__attach-indicator" title="has attachments">📎</span>' : ''}

// New:
${e.attachment_summaries.length > 0 ? ' <span class="entry-row__attach-indicator" title="has attachments">⊕</span>' : ''}
  • Step 3: Build and confirm no TS errors
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
  • Step 4: Commit
git add extension/src/popup/components/item-list.ts
git commit -m "fix(ext/popup): replace emoji typeIcon with glyph constants in item-list"

Task 4: Empty states in item-list.ts

Files:

  • Modify: extension/src/popup/components/item-list.ts

  • Modify: extension/src/popup/styles.css

  • Step 1: Update buildRowsHtml() to use two distinct empty states

Current empty state is '<div class="empty">no items</div>'. Split into:

function buildRowsHtml(): string {
  const state = getState();
  const filtered = getFilteredEntries();

  if (filtered.length === 0) {
    if (state.searchQuery) {
      return `
        <div class="empty-state">
          <span class="empty-state__icon" aria-hidden="true">⊘</span>
          <div class="empty-state__title">No results for "${escapeHtml(state.searchQuery)}"</div>
          <div class="empty-state__hint">Try a shorter search term.</div>
        </div>
      `;
    }
    return `
      <div class="empty-state">
        <span class="empty-state__icon" aria-hidden="true">◈</span>
        <div class="empty-state__title">No items yet</div>
        <div class="empty-state__hint">Press <kbd>+</kbd> to add your first item.</div>
      </div>
    `;
  }

  return filtered.map(([id, e], i) => `
    <div class="entry-row ${i === state.selectedIndex ? 'selected' : ''}" data-id="${escapeHtml(id)}" data-index="${i}">
      <span class="entry-name"><span class="type-icon" aria-hidden="true">${typeIcon(e.type)}</span> ${escapeHtml(e.title)}${e.attachment_summaries.length > 0 ? ' <span class="entry-row__attach-indicator" title="has attachments">⊕</span>' : ''}</span>
      <span class="entry-meta">${escapeHtml(metaLine(e))}</span>
    </div>
  `).join('');
}
  • Step 2: Add empty-state CSS to styles.css

In extension/src/popup/styles.css, add:

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 40px 20px;
  text-align: center;
}

.empty-state__icon {
  font-size: 28px;
  color: var(--text-muted, #8b949e);
  margin-bottom: 12px;
  display: block;
}

.empty-state__title {
  font-size: 13px;
  font-weight: 600;
  margin-bottom: 4px;
}

.empty-state__hint {
  font-size: 11px;
  color: var(--text-muted, #8b949e);
}
  • Step 3: Build
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
  • Step 4: Commit
git add extension/src/popup/components/item-list.ts extension/src/popup/styles.css
git commit -m "feat(ext/popup): empty states with glyph icons in item-list"

Task 5: Polished type-picker in item-form.ts

Files:

  • Modify: extension/src/popup/components/item-form.ts

  • Step 1: Replace emoji in TYPE_OPTIONS

Current TYPE_OPTIONS uses emoji icons. Replace:

import {
  GLYPH_TYPE_LOGIN, GLYPH_TYPE_SECURE_NOTE, GLYPH_TYPE_TOTP,
  GLYPH_TYPE_CARD, GLYPH_TYPE_IDENTITY, GLYPH_TYPE_KEY, GLYPH_TYPE_DOCUMENT,
} from '../../shared/glyphs';

const TYPE_OPTIONS: Array<{ type: ItemType; icon: string; label: string; description: string }> = [
  { type: 'login',       icon: GLYPH_TYPE_LOGIN,       label: 'Login',       description: 'Username + password' },
  { type: 'secure_note', icon: GLYPH_TYPE_SECURE_NOTE, label: 'Secure Note', description: 'Encrypted text note' },
  { type: 'identity',    icon: GLYPH_TYPE_IDENTITY,    label: 'Identity',    description: 'Personal details' },
  { type: 'card',        icon: GLYPH_TYPE_CARD,        label: 'Card',        description: 'Credit / debit card' },
  { type: 'key',         icon: GLYPH_TYPE_KEY,         label: 'SSH / API Key', description: 'Keys and tokens' },
  { type: 'document',    icon: GLYPH_TYPE_DOCUMENT,    label: 'Document',    description: 'File attachment' },
  { type: 'totp',        icon: GLYPH_TYPE_TOTP,        label: 'TOTP',        description: '2FA authenticator' },
];
  • Step 2: Upgrade renderTypeSelection to a 2-column card grid

Replace the current renderTypeSelection function's HTML:

function renderTypeSelection(app: HTMLElement): void {
  app.innerHTML = `
    <div class="pad">
      <div style="display:flex; align-items:center; gap:12px; margin-bottom:12px;">
        <button class="btn" id="back-btn">◂ back</button>
        <span style="font-size:14px; font-weight:600;">New item</span>
        <span style="flex:1;"></span>
        ${isInTab() ? '' : '<button class="btn" id="popout-btn" title="Open in tab">⧉</button>'}
      </div>
      <div class="type-card-grid">
        ${TYPE_OPTIONS.map((opt) => `
          <button class="type-card" data-type="${opt.type}">
            <span class="type-card__icon" aria-hidden="true">${opt.icon}</span>
            <span class="type-card__label">${escapeHtml(opt.label)}</span>
            <span class="type-card__desc">${escapeHtml(opt.description)}</span>
          </button>
        `).join('')}
      </div>
      <div class="keyhints"><span><kbd>Esc</kbd> back</span></div>
    </div>
  `;

  document.getElementById('back-btn')?.addEventListener('click', () => navigate('list'));
  document.getElementById('popout-btn')?.addEventListener('click', popOutToTab);
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') navigate('list');
  }, { once: true });

  document.querySelectorAll<HTMLButtonElement>('[data-type]').forEach((btn) => {
    btn.addEventListener('click', () => {
      const type = btn.dataset.type as ItemType;
      setState({ newType: type });
      renderItemForm(app, 'add');
    });
  });
}
  • Step 3: Add type-card CSS to styles.css

In extension/src/popup/styles.css:

.type-card-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin-bottom: 12px;
}

.type-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding: 10px 12px;
  background: var(--bg-elevated, #161b22);
  border: 1px solid var(--border, #30363d);
  border-radius: 6px;
  cursor: pointer;
  text-align: left;
  transition: border-color 0.15s;
}

.type-card:hover { border-color: var(--gold, #b8860b); }

.type-card__icon  { font-size: 20px; margin-bottom: 4px; }
.type-card__label { font-size: 12px; font-weight: 600; }
.type-card__desc  { font-size: 10px; color: var(--text-muted, #8b949e); margin-top: 2px; }
  • Step 4: Build and run tests
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
bun run test 2>&1 | tail -10
  • Step 5: Commit
git add extension/src/popup/components/item-form.ts extension/src/popup/styles.css
git commit -m "feat(ext/popup): polished 2-column type-picker with glyph icons"

Task 6: Toast system

Files:

  • Create: extension/src/shared/toast.ts

  • Modify: extension/src/popup/styles.css

  • Modify: extension/src/vault/vault.css

  • Step 1: Write toast.ts

// extension/src/shared/toast.ts

export function showToast(
  message: string,
  type: 'success' | 'error' | 'info' = 'info',
  durationMs = 2500,
): void {
  let container = document.querySelector<HTMLElement>('.relicario-toast-container');
  if (!container) {
    container = document.createElement('div');
    container.className = 'relicario-toast-container';
    document.body.appendChild(container);
  }

  const toast = document.createElement('div');
  toast.className = `relicario-toast relicario-toast--${type}`;
  toast.textContent = message;
  container.appendChild(toast);

  requestAnimationFrame(() => {
    requestAnimationFrame(() => toast.classList.add('relicario-toast--visible'));
  });

  setTimeout(() => {
    toast.classList.remove('relicario-toast--visible');
    toast.addEventListener('transitionend', () => toast.remove(), { once: true });
  }, durationMs);
}
  • Step 2: Add toast CSS to both stylesheets

In extension/src/popup/styles.css AND extension/src/vault/vault.css, add:

/* Toast notifications */
.relicario-toast-container {
  position: fixed;
  bottom: 16px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  gap: 6px;
  pointer-events: none;
  z-index: 9999;
}

/* Vault tab: position bottom-right instead */
.vault-shell .relicario-toast-container {
  left: auto;
  right: 24px;
  transform: none;
}

.relicario-toast {
  padding: 8px 16px;
  border-radius: 6px;
  font-size: 12px;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.2s, transform 0.2s;
  pointer-events: none;
}

.relicario-toast--visible {
  opacity: 1;
  transform: translateY(0);
}

.relicario-toast--success { background: #1f4a24; color: #aff0b5; border: 1px solid #238636; }
.relicario-toast--error   { background: #4a1f1f; color: #f0afaf; border: 1px solid #ab2b20; }
.relicario-toast--info    { background: #1f2d4a; color: #afc8f0; border: 1px solid #1f6feb; }
  • Step 3: Replace sync-status div in item-list.ts with toast

In item-list.ts, find the sync button handler and replace the manual status update with toast:

import { showToast } from '../../shared/toast';

// In the sync button click handler:
document.getElementById('sync-btn')?.addEventListener('click', async () => {
  setState({ loading: true, error: null });
  const resp = await sendMessage({ type: 'sync' });
  if (resp.ok) {
    const listResp = await sendMessage({ type: 'list_items' });
    if (listResp.ok) {
      const data = listResp.data as { items: Array<[ItemId, ManifestEntry]> };
      setState({ entries: data.items, loading: false });
      showToast('Synced', 'success');
      return;
    }
    setState({ loading: false, error: listResp.error });
    showToast(listResp.error ?? 'Sync failed', 'error');
  } else {
    setState({ loading: false, error: resp.error });
    showToast(resp.error ?? 'Sync failed', 'error');
  }
});
  • Step 4: Build and run tests
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
bun run test 2>&1 | tail -10
  • Step 5: Commit
git add extension/src/shared/toast.ts extension/src/popup/styles.css extension/src/vault/vault.css extension/src/popup/components/item-list.ts
git commit -m "feat(ext): shared toast notification system"

Task 7: vault.ts — replace emoji typeIcon() with glyphs

Files:

  • Modify: extension/src/vault/vault.ts

  • Step 1: Add glyph imports to vault.ts

Find the glyphs import line in vault.ts and add the type constants:

import {
  GLYPH_TRASH, GLYPH_DEVICES, GLYPH_SETTINGS, GLYPH_LOCK, GLYPH_NEXT,
  GLYPH_TYPE_LOGIN, GLYPH_TYPE_SECURE_NOTE, GLYPH_TYPE_TOTP,
  GLYPH_TYPE_CARD, GLYPH_TYPE_IDENTITY, GLYPH_TYPE_KEY, GLYPH_TYPE_DOCUMENT,
} from '../shared/glyphs';
  • Step 2: Replace typeIcon() in vault.ts

Current typeIcon() (~line 61) uses Unicode escape sequences for emoji. Replace:

function typeIcon(t: ItemType): string {
  switch (t) {
    case 'login':       return GLYPH_TYPE_LOGIN;
    case 'secure_note': return GLYPH_TYPE_SECURE_NOTE;
    case 'identity':    return GLYPH_TYPE_IDENTITY;
    case 'card':        return GLYPH_TYPE_CARD;
    case 'key':         return GLYPH_TYPE_KEY;
    case 'document':    return GLYPH_TYPE_DOCUMENT;
    case 'totp':        return GLYPH_TYPE_TOTP;
  }
}
  • Step 3: Build
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
  • Step 4: Commit
git add extension/src/vault/vault.ts
git commit -m "fix(ext/vault): replace emoji typeIcon with glyph constants"

Task 8: vault.css — 3-column layout rules

Files:

  • Modify: extension/src/vault/vault.css

Add or replace the layout section. The existing .vault-sidebar and .vault-pane rules will be superseded.

  • Step 1: Add 3-column shell + drawer CSS

In vault.css, add/replace the shell layout section:

/* === 3-column shell === */
.vault-shell {
  display: flex;
  height: 100vh;
  overflow: hidden;
  background: var(--bg-page, #0d1117);
}

/* Sidebar */
.vault-sidebar {
  width: 200px;
  min-width: 200px;
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--border, #30363d);
  background: var(--bg-sidebar, #0d1117);
  overflow-y: auto;
  flex-shrink: 0;
}

/* List pane (flex: 1, fills between sidebar and drawer) */
.vault-list-pane {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  min-width: 0;
}

/* Detail drawer */
.vault-drawer {
  width: 440px;
  min-width: 440px;
  max-width: 440px;
  border-left: 1px solid var(--border, #30363d);
  background: var(--bg-elevated, #161b22);
  overflow-y: auto;
  transform: translateX(100%);
  transition: transform 0.2s ease;
  flex-shrink: 0;
}

.vault-drawer--open {
  transform: translateX(0);
}

/* List rows */
.vault-list-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 16px;
  cursor: pointer;
  border-bottom: 1px solid var(--border-subtle, #21262d);
  transition: background 0.1s;
}

.vault-list-row:hover { background: var(--bg-hover, #161b22); }

.vault-list-row--selected {
  background: var(--bg-selected, #1c2d41);
  border-left: 2px solid var(--gold, #b8860b);
}

.vault-list-row__icon {
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-elevated, #161b22);
  border-radius: 6px;
  border: 1px solid var(--border, #30363d);
  font-size: 14px;
  flex-shrink: 0;
}

.vault-list-row--selected .vault-list-row__icon { border-color: var(--gold, #b8860b); }

.vault-list-row__text { flex: 1; min-width: 0; }

.vault-list-row__title {
  font-size: 13px;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.vault-list-row__subtitle {
  font-size: 11px;
  color: var(--text-muted, #8b949e);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  margin-top: 1px;
}

.vault-list-row__age {
  font-size: 10px;
  color: var(--text-dim, #6e7681);
  flex-shrink: 0;
}

/* Bottom sheet */
.vault-bottom-sheet-scrim {
  position: absolute;
  inset: 0 0 0 200px; /* exclude sidebar */
  background: rgba(0,0,0,0.5);
  opacity: 0;
  transition: opacity 0.2s;
  pointer-events: none;
  z-index: 100;
}

.vault-bottom-sheet-scrim--visible {
  opacity: 1;
  pointer-events: auto;
}

.vault-bottom-sheet {
  position: absolute;
  bottom: 0;
  left: 200px; /* exclude sidebar */
  right: 0;
  background: var(--bg-elevated, #161b22);
  border-top: 1px solid var(--border, #30363d);
  border-radius: 12px 12px 0 0;
  padding: 16px 24px 24px;
  transform: translateY(100%);
  transition: transform 0.25s ease;
  z-index: 101;
  max-height: 60vh;
  overflow-y: auto;
}

.vault-bottom-sheet--open { transform: translateY(0); }

.vault-bottom-sheet__handle {
  width: 40px;
  height: 4px;
  background: var(--border, #30363d);
  border-radius: 2px;
  margin: 0 auto 16px;
}

.vault-bottom-sheet__title {
  font-size: 14px;
  font-weight: 600;
  margin-bottom: 16px;
  text-align: center;
}

.vault-type-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
  gap: 10px;
}

.vault-type-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 12px 8px;
  background: var(--bg-page, #0d1117);
  border: 1px solid var(--border, #30363d);
  border-radius: 8px;
  cursor: pointer;
  transition: border-color 0.15s;
  gap: 6px;
}

.vault-type-card:hover { border-color: var(--gold, #b8860b); }

.vault-type-card__icon { font-size: 28px; }
.vault-type-card__name { font-size: 11px; color: var(--text-muted, #8b949e); }

/* Drawer header and body */
.vault-drawer__header {
  display: flex;
  align-items: center;
  padding: 12px 16px;
  border-bottom: 1px solid var(--border, #30363d);
  gap: 8px;
}

.vault-drawer__type-pill {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 2px 8px;
  background: var(--bg-page, #0d1117);
  border: 1px solid var(--border, #30363d);
  border-radius: 4px;
  color: var(--text-muted, #8b949e);
}

.vault-drawer__actions { display: flex; gap: 6px; margin-left: auto; }

.vault-drawer__close {
  background: transparent;
  border: none;
  cursor: pointer;
  font-size: 16px;
  color: var(--text-muted, #8b949e);
  padding: 4px 6px;
}

.vault-drawer__body { padding: 20px 20px 16px; }

.vault-drawer__title  { font-size: 18px; font-weight: 700; margin-bottom: 4px; }
.vault-drawer__subtitle { font-size: 12px; color: var(--text-muted, #8b949e); margin-bottom: 16px; }

.vault-drawer__field-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}

.vault-drawer__field-grid > .vault-drawer__field--full { grid-column: 1 / -1; }

.vault-drawer__field-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted, #8b949e);
  margin-bottom: 2px;
}

.vault-drawer__field-value {
  font-size: 13px;
  word-break: break-all;
}

/* === Responsive === */
@media (max-width: 960px) {
  .vault-drawer {
    position: absolute;
    right: 0;
    top: 0;
    height: 100%;
  }
}

@media (max-width: 720px) {
  .vault-sidebar {
    width: 48px;
    min-width: 48px;
  }
  .vault-sidebar__category-label,
  .vault-sidebar__category-count,
  .vault-sidebar__nav-label {
    display: none;
  }
  .vault-sidebar__nav-item { justify-content: center; padding: 10px 0; }
}
  • Step 2: Commit
git add extension/src/vault/vault.css
git commit -m "feat(ext/vault): 3-column layout CSS — drawer, bottom sheet, list rows, responsive"

Task 9: vault.ts — 3-column shell + sidebar category nav

Files:

  • Modify: extension/src/vault/vault.ts

This is the largest task. Rewrite renderShell(), renderSidebarList(), and add the list pane renderer.

  • Step 1: Add drawerOpen and bottomSheetOpen to VaultState

In the VaultState interface, add:

drawerOpen: boolean;
bottomSheetOpen: boolean;

In the initial state object, add:

drawerOpen: false,
bottomSheetOpen: false,
  • Step 2: Rewrite renderShell() for 3-column layout

Replace the existing renderShell() function:

function renderShell(app: HTMLElement): void {
  if (!app.querySelector('.vault-shell')) {
    app.innerHTML = `
      <div class="vault-shell">
        <div class="vault-sidebar">
          <div class="vault-sidebar__header">
            <img class="brand-logo" src="icons/relicario-logo-16.svg" alt="">
            <span class="brand">Relicario</span>
          </div>
          <div class="vault-sidebar__search">
            <input type="text" id="vault-search" placeholder="/ search…" />
          </div>
          <nav class="vault-sidebar__categories" id="vault-categories" aria-label="Item types"></nav>
          <div class="vault-sidebar__nav">
            <button class="vault-sidebar__nav-item" data-nav="add" title="New item">+ new item</button>
            <button class="vault-sidebar__nav-item" data-nav="trash" title="Trash">${GLYPH_TRASH} <span class="vault-sidebar__nav-label">trash</span></button>
            <button class="vault-sidebar__nav-item" data-nav="settings" title="Settings">${GLYPH_SETTINGS} <span class="vault-sidebar__nav-label">settings</span></button>
            <button class="vault-sidebar__nav-item" data-nav="lock" title="Lock">${GLYPH_LOCK} <span class="vault-sidebar__nav-label">lock</span></button>
          </div>
        </div>
        <div class="vault-list-pane" id="vault-list-pane"></div>
        <div class="vault-drawer" id="vault-drawer"></div>
        <div class="vault-bottom-sheet-scrim" id="vault-sheet-scrim"></div>
        <div class="vault-bottom-sheet" id="vault-bottom-sheet"></div>
      </div>
    `;
    wireSidebar();
    wireBottomSheet();
  }

  renderSidebarCategories();
  renderListPane();
  if (state.drawerOpen && state.selectedItem) {
    renderDrawer(state.selectedItem);
  }
}

Note: The ⌬ devices nav button is intentionally removed here. Once Stream B's Security section lands, devices are accessible via Settings → Security. Until then, users can access devices via the old popup route (still present). Confirm with PM before removing if uncertain.

  • Step 3: Rewrite renderSidebarList()renderSidebarCategories()

Rename the function and change it to show type sections with counts (not per-item rows):

function renderSidebarCategories(): void {
  const container = document.getElementById('vault-categories');
  if (!container) return;

  const filtered = getFilteredEntries();
  const typeOrder: ItemType[] = ['login', 'secure_note', 'identity', 'card', 'key', 'document', 'totp'];

  const allCount = filtered.length;
  const isAllActive = !state.activeGroup && state.view === 'list';

  let html = `
    <button class="vault-category-row ${isAllActive ? 'vault-category-row--active' : ''}" data-group="">
      <span class="vault-category-row__icon">◈</span>
      <span class="vault-category-row__label">All items</span>
      <span class="vault-category-row__count">${allCount}</span>
    </button>
  `;

  for (const t of typeOrder) {
    const count = filtered.filter(([, e]) => e.type === t).length;
    if (count === 0 && allCount > 0) continue; // hide empty sections unless vault is empty
    const isActive = state.activeGroup === t;
    html += `
      <button class="vault-category-row ${isActive ? 'vault-category-row--active' : ''}" data-group="${t}">
        <span class="vault-category-row__icon">${typeIcon(t)}</span>
        <span class="vault-category-row__label vault-sidebar__category-label">${typeLabel(t)}</span>
        <span class="vault-category-row__count vault-sidebar__category-count">${count}</span>
      </button>
    `;
  }

  container.innerHTML = html;

  container.querySelectorAll<HTMLButtonElement>('.vault-category-row').forEach((btn) => {
    btn.addEventListener('click', () => {
      state.activeGroup = btn.dataset.group || null;
      state.drawerOpen = false;
      state.selectedId = null;
      state.selectedItem = null;
      renderSidebarCategories();
      renderListPane();
      closeDrawer();
    });
  });
}

Add to vault.css (after Task 8):

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

.vault-category-row:hover { background: var(--bg-hover, #161b22); }
.vault-category-row--active { background: var(--bg-selected, #1c2d41); }
.vault-category-row__icon { font-size: 14px; flex-shrink: 0; }
.vault-category-row__label { flex: 1; }
.vault-category-row__count { font-size: 11px; color: var(--text-muted, #8b949e); }
  • Step 4: Add renderListPane()
function renderListPane(): void {
  const pane = document.getElementById('vault-list-pane');
  if (!pane) return;

  const group = state.activeGroup as ItemType | null;
  let items = getFilteredEntries();
  if (group) items = items.filter(([, e]) => e.type === group);

  if (items.length === 0) {
    pane.innerHTML = `
      <div class="empty-state">
        <span class="empty-state__icon" aria-hidden="true">${state.searchQuery ? '⊘' : '◈'}</span>
        <div class="empty-state__title">${state.searchQuery ? `No results for "${escapeHtml(state.searchQuery)}"` : 'No items yet'}</div>
        <div class="empty-state__hint">${state.searchQuery ? 'Try a shorter search term.' : 'Click + new item to get started.'}</div>
      </div>
    `;
    return;
  }

  pane.innerHTML = items.map(([id, e]) => {
    const sel = id === state.selectedId ? ' vault-list-row--selected' : '';
    const subtitle = e.icon_hint ?? (e.tags.length > 0 ? e.tags.join(', ') : '');
    const modifiedAgo = e.modified ? relativeTime(e.modified) : '';
    return `
      <div class="vault-list-row${sel}" data-id="${escapeHtml(id)}">
        <div class="vault-list-row__icon" aria-hidden="true">${typeIcon(e.type)}</div>
        <div class="vault-list-row__text">
          <div class="vault-list-row__title">${escapeHtml(e.title)}</div>
          ${subtitle ? `<div class="vault-list-row__subtitle">${escapeHtml(subtitle)}</div>` : ''}
        </div>
        ${modifiedAgo ? `<div class="vault-list-row__age">${escapeHtml(modifiedAgo)}</div>` : ''}
      </div>
    `;
  }).join('');

  pane.querySelectorAll<HTMLElement>('.vault-list-row').forEach((row) => {
    row.addEventListener('click', async () => {
      await selectItemForDrawer(row.dataset.id!);
    });
  });
}

function relativeTime(unixSec: number): string {
  const diffS = Math.floor(Date.now() / 1000) - unixSec;
  if (diffS < 60) return 'just now';
  if (diffS < 3600) return `${Math.floor(diffS / 60)}m ago`;
  if (diffS < 86400) return `${Math.floor(diffS / 3600)}h ago`;
  return `${Math.floor(diffS / 86400)}d ago`;
}
  • Step 5: Build
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
  • Step 6: Commit
git add extension/src/vault/vault.ts extension/src/vault/vault.css
git commit -m "feat(ext/vault): 3-column shell — sidebar category nav + list pane"

Task 10: vault.ts — detail drawer

Files:

  • Modify: extension/src/vault/vault.ts

  • Step 1: Add selectItemForDrawer() and drawer render/close functions

async function selectItemForDrawer(id: ItemId): Promise<void> {
  const resp = await sendMessage({ type: 'get_item', id });
  if (!resp.ok) return;
  const data = resp.data as { item: Item };
  state.selectedId = id;
  state.selectedItem = data.item;
  state.drawerOpen = true;
  renderSidebarCategories();
  renderListPane();
  renderDrawer(data.item);
  openDrawer();
}

function openDrawer(): void {
  document.getElementById('vault-drawer')?.classList.add('vault-drawer--open');
}

function closeDrawer(): void {
  state.drawerOpen = false;
  state.selectedId = null;
  state.selectedItem = null;
  document.getElementById('vault-drawer')?.classList.remove('vault-drawer--open');
}

function renderDrawer(item: Item): void {
  const drawer = document.getElementById('vault-drawer');
  if (!drawer) return;

  const coreFields = getDrawerCoreFields(item);

  drawer.innerHTML = `
    <div class="vault-drawer__header">
      <span class="vault-drawer__type-pill">${item.type.replace('_', ' ').toUpperCase()}</span>
      <div class="vault-drawer__actions">
        <button class="btn" id="drawer-edit-btn" style="font-size:11px;">edit</button>
        <button class="vault-drawer__close" id="drawer-close-btn" title="Close (Esc)">✕</button>
      </div>
    </div>
    <div class="vault-drawer__body">
      <div class="vault-drawer__title">${escapeHtml(item.title)}</div>
      ${item.core && 'url' in item.core ? `<div class="vault-drawer__subtitle">${escapeHtml((item.core as { url?: string }).url ?? '')}</div>` : ''}
      <div class="vault-drawer__field-grid">
        ${coreFields.map(([label, value, full]) => `
          <div class="vault-drawer__field${full ? ' vault-drawer__field--full' : ''}">
            <div class="vault-drawer__field-label">${escapeHtml(label)}</div>
            <div class="vault-drawer__field-value">${escapeHtml(value)}</div>
          </div>
        `).join('')}
      </div>
    </div>
  `;

  document.getElementById('drawer-close-btn')?.addEventListener('click', () => {
    closeDrawer();
    renderListPane();
  });

  document.getElementById('drawer-edit-btn')?.addEventListener('click', () => {
    setHash('edit', state.selectedId!);
    renderPane();
  });
}

function getDrawerCoreFields(item: Item): Array<[string, string, boolean]> {
  // Returns [label, value, fullWidth] tuples for the core type fields
  const core = item.core as Record<string, unknown>;
  if (!core) return [];
  const fields: Array<[string, string, boolean]> = [];
  if ('username' in core) fields.push(['username', String(core.username ?? ''), false]);
  if ('password' in core) fields.push(['password', '••••••••', false]);
  if ('url' in core)      fields.push(['url', String(core.url ?? ''), true]);
  if ('number' in core)   fields.push(['number', String(core.number ?? ''), false]);
  if ('expiry' in core)   fields.push(['expiry', String(core.expiry ?? ''), false]);
  if ('cardholder' in core) fields.push(['cardholder', String(core.cardholder ?? ''), true]);
  if (item.notes)         fields.push(['notes', item.notes, true]);
  return fields;
}
  • Step 2: Add Esc key handler to close drawer

In wireSidebar() or the shell setup, add:

document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape' && state.drawerOpen) {
    closeDrawer();
    renderListPane();
  }
});
  • Step 3: Build
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
  • Step 4: Commit
git add extension/src/vault/vault.ts
git commit -m "feat(ext/vault): detail drawer — open/close state + core fields display"

Task 11: vault.ts — bottom sheet for new item type picker

Files:

  • Modify: extension/src/vault/vault.ts

  • Step 1: Add wireBottomSheet() and sheet open/close functions

const BOTTOM_SHEET_TYPES: Array<{ type: ItemType; label: string }> = [
  { type: 'login',       label: 'Login' },
  { type: 'secure_note', label: 'Secure Note' },
  { type: 'totp',        label: 'TOTP' },
  { type: 'card',        label: 'Card' },
  { type: 'identity',    label: 'Identity' },
  { type: 'key',         label: 'SSH / API Key' },
  { type: 'document',    label: 'Document' },
];

function openBottomSheet(): void {
  const sheet = document.getElementById('vault-bottom-sheet')!;
  const scrim = document.getElementById('vault-sheet-scrim')!;

  sheet.innerHTML = `
    <div class="vault-bottom-sheet__handle"></div>
    <div class="vault-bottom-sheet__title">New item — choose type</div>
    <div class="vault-type-grid">
      ${BOTTOM_SHEET_TYPES.map((t) => `
        <button class="vault-type-card" data-type="${t.type}">
          <span class="vault-type-card__icon" aria-hidden="true">${typeIcon(t.type)}</span>
          <span class="vault-type-card__name">${escapeHtml(t.label)}</span>
        </button>
      `).join('')}
    </div>
  `;

  sheet.classList.add('vault-bottom-sheet--open');
  scrim.classList.add('vault-bottom-sheet-scrim--visible');
  state.bottomSheetOpen = true;

  sheet.querySelectorAll<HTMLButtonElement>('[data-type]').forEach((btn) => {
    btn.addEventListener('click', () => {
      const type = btn.dataset.type as ItemType;
      closeBottomSheet();
      state.newType = type;
      setHash('add', type);
      renderPane();
    });
  });
}

function closeBottomSheet(): void {
  document.getElementById('vault-bottom-sheet')?.classList.remove('vault-bottom-sheet--open');
  document.getElementById('vault-sheet-scrim')?.classList.remove('vault-bottom-sheet-scrim--visible');
  state.bottomSheetOpen = false;
}

function wireBottomSheet(): void {
  document.getElementById('vault-sheet-scrim')?.addEventListener('click', closeBottomSheet);
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && state.bottomSheetOpen) closeBottomSheet();
  });
}
  • Step 2: Update the data-nav="add" handler to open the sheet

In wireSidebar(), find the nav === 'add' branch and change it to call openBottomSheet() instead of directly calling setHash('add'):

if (nav === 'add') {
  openBottomSheet();
  return;
}
  • Step 3: Build and run tests
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"
bun run test 2>&1 | tail -10
  • Step 4: Commit
git add extension/src/vault/vault.ts
git commit -m "feat(ext/vault): bottom sheet type picker for new item"

Task 12: Wire settings component (interface contract with DEV-B)

Files:

  • Modify: extension/src/vault/vault.ts

  • Step 1: Add settings import

Add to vault.ts imports:

import { renderSettings, teardownSettings } from '../popup/components/settings';
  • Step 2: Wire in renderPane() for the settings view

Find the renderPane() function and ensure the settings case calls renderSettings:

case 'settings':
  teardownSettings();
  await renderSettings(pane);
  return;

And ensure teardownSettings() is called when navigating away from settings (wherever view transitions happen).

  • Step 3: Build
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run build 2>&1 | grep "error TS"

If DEV-B's settings.ts doesn't yet export renderSettings / teardownSettings, add a temporary stub to settings.ts on this branch to unblock compilation — note it clearly in a comment for DEV-B. This stub will be replaced at merge time.

  • Step 4: Commit
git add extension/src/vault/vault.ts
git commit -m "feat(ext/vault): wire renderSettings / teardownSettings from settings component"

Task 13: Full build + test pass

  • Step 1: Run all tests
cd /home/alee/Sources/relicario.v0.5.1-stream-a/extension && bun run test 2>&1 | tail -20

Expected: all existing tests pass. If any fail, fix before proceeding.

  • Step 2: Build both targets
bun run build 2>&1 | tail -10
bun run build:firefox 2>&1 | tail -10

Expected: both build clean (only pre-existing bundle-size warnings).

  • Step 3: Grep for remaining emoji in extension/src/
grep -rn '\U0001F\|🔑\|📝\|🪪\|💳\|🗝\|📄\|⏱️\|🖥\|🔐\|📎' /home/alee/Sources/relicario.v0.5.1-stream-a/extension/src/ 2>/dev/null

Expected: no output (all emoji replaced with glyphs).

  • Step 4: Commit any remaining fixes, then open PR
gh pr create --title "feat: fullscreen 3-column layout + popup polish (Stream A)" --base main
  • Step 5: Post status to PM

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

## STATUS UPDATE — DEV-A
Time: <iso8601>
Task: 13 of 13
Status: REVIEW-READY
Summary: All 13 tasks complete. PR open. Build clean. All tests pass. No emoji remaining in extension/src/.
Next: waiting for PM