1# Settings Redesign + Feedback System — Implementation Spec23> Read `AGENTS.md`, `.agent/Projects.md`, and `.agent/CONTINUITY.md` before4> touching any file. Follow every invariant in Projects.md without exception.5> All Koh rules in AGENTS.md apply. Use the `dev` lane. Start with a6> checkpoint.78---910## 0. Pre-flight1112```sh13koh status --json 14koh checkpoint "before settings redesign and feedback feature"15```1617Note: Projects.md §8 says "no new features until P0/P1 findings are closed."18This task has explicit user authorisation to proceed despite that rule.19Record the override in `.agent/CONTINUITY.md` before starting.2021---2223## 1. Version bump2425This is a new feature (MINOR). Bump all three files from `2.0.3` → `2.1.0`.26All three must agree — this is a hard invariant (Projects.md §2.1 / §4.8).2728- `manifest.json` → `"version": "2.1.0"`29- `manifest.firefox.json` → `"version": "2.1.0"`30- `package.json` → `"version": "2.1.0"`3132---3334## 2. `src/shared/types.ts` — add `SUBMIT_FEEDBACK` to the MessageType union3536Add `"SUBMIT_FEEDBACK"` to the background-bound section of `MessageType`37(before the content-script-bound comment block, after `"IS_CARD_DECLINED"`):3839```ts40| "IS_CARD_DECLINED"41| "SUBMIT_FEEDBACK"4243```4445No other changes to this file.4647---4849## 3. `src/background/messages.ts` — SUBMIT_FEEDBACK handler5051### 3a. Add the feedback endpoint constant5253Directly below the existing `PRIVATE_IPV4_RANGES` constant at the top of the54file, add:5556```ts57585960const FEEDBACK_ENDPOINT = "";61```6263### 3b. Add the case to the switch6465Place this case immediately before the `default:` case at the bottom of the66`switch (msg.type)` block:6768```ts69case "SUBMIT_FEEDBACK": {70 const { category, message, version, browser } = msg.payload as {71 category: string;72 message: string;73 version: string;74 browser: string;75 };76 if (!message.trim()) return { ok: false, error: "message is empty" };77 if (!FEEDBACK_ENDPOINT) return { ok: true };78 if (!isAllowedCanaryWebhookUrl(FEEDBACK_ENDPOINT)) {79 return { ok: false, error: "invalid feedback endpoint" };80 }81 try {82 const res = await fetch(FEEDBACK_ENDPOINT, {83 method: "POST",84 headers: { "Content-Type": "application/json" },85 body: JSON.stringify({ category, message, version, browser, source: "peach-extension" }),86 });87 return { ok: res.ok, error: res.ok ? undefined : `HTTP ${res.status}` };88 } catch (err) {89 return { ok: false, error: err instanceof Error ? err.message : "network error" };90 }91}92```9394### 3c. Update `shouldRefreshAutoLock`9596`SUBMIT_FEEDBACK` must not refresh the auto-lock timer. Add it to the97exclusion list in `shouldRefreshAutoLock`:9899```ts100function shouldRefreshAutoLock(type: MessageType): boolean {101 return (102 type !== "HAS_VAULT" &&103 type !== "IS_UNLOCKED" &&104 type !== "GET_VAULT_STATUS" &&105 type !== "LOCK" &&106 type !== "UPDATE_SETTINGS" &&107 type !== "GET_PENDING_PASSKEY" &&108 type !== "GET_PASSKEY_PROVIDER_STATE" &&109 type !== "DECLINE_CARD_SAVE" &&110 type !== "IS_CARD_DECLINED" &&111 type !== "SUBMIT_FEEDBACK" 112 );113}114```115116### 3d. Network surface — update Projects.md §3.7117118Add a third permitted fetch entry to `§3.7 Network surface` in119`.agent/Projects.md`:120121```1223. `FEEDBACK_ENDPOINT` constant in `src/background/messages.ts` — only when123 non-empty, only on `SUBMIT_FEEDBACK` messages initiated by the user. URL124 validated by `isAllowedCanaryWebhookUrl`. Defaults to empty string125 (disabled). Set by the developer before shipping.126```127128Also verify the network grep still passes:129130```sh131grep -r "fetch(" src/ | grep -v "pwnedpasswords" | grep -v "webhookUrl" | grep -v "FEEDBACK_ENDPOINT" | grep -v "\.test\."132```133134Must return zero results.135136### 3e. Tests — `src/background/messages.test.ts` (or equivalent test file)137138Per Projects.md §2.2, every new `messages.ts` handler requires tests. Add139these three cases to the existing test file:140141```ts142describe("SUBMIT_FEEDBACK", () => {143 it("returns ok:true when FEEDBACK_ENDPOINT is empty (no-op path)", async () => {144 145 const res = await handleMessageDirect({146 type: "SUBMIT_FEEDBACK",147 payload: { category: "Bug report", message: "test", version: "2.1.0", browser: "Chrome" },148 });149 expect(res.ok).toBe(true);150 });151152 it("returns ok:false when message is empty", async () => {153 const res = await handleMessageDirect({154 type: "SUBMIT_FEEDBACK",155 payload: { category: "Bug report", message: " ", version: "2.1.0", browser: "Chrome" },156 });157 expect(res.ok).toBe(false);158 expect(res.error).toBe("message is empty");159 });160161 it("does not refresh auto-lock timer", () => {162 expect(shouldRefreshAutoLockPublic("SUBMIT_FEEDBACK")).toBe(false);163 });164});165```166167Adapt to the existing test harness conventions in the project. If168`handleMessageDirect` or `shouldRefreshAutoLockPublic` are not already169exported for testing, export them or use whatever test helper pattern exists.170171---172173## 4. `src/popup/views/Settings.tsx` — full rewrite174175This is the largest change. The file goes from a single 987-line flat scroll176to a two-level navigation system: a home screen of category rows, each177navigating to a focused sub-view.178179### 4a. Overall structure180181The outer component keeps the same `SettingsProps` interface (no changes to182`App.tsx`). It gains a new `activeSection` signal that drives navigation.183184```ts185type SectionName =186 | "security"187 | "vault"188 | "sync"189 | "appearance"190 | "shortcuts"191 | "license"192 | "feedback";193```194195The component returns a single `peach-overlay-view` div. The back button196navigates: sub-view → home → `props.onBack()`. The title reflects the current197section or reads `"settings"` on home.198199Use SolidJS `<Switch>/<Match>` (not early returns — see Projects.md §4.1):200201```tsx202<Switch fallback={renderHome()}>203 <Match when={activeSection() === "security"}>{renderSecurity()}</Match>204 <Match when={activeSection() === "vault"}>{renderVault()}</Match>205 <Match when={activeSection() === "sync"}>{renderSync()}</Match>206 <Match when={activeSection() === "appearance"}>{renderAppearance()}</Match>207 <Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>208 <Match when={activeSection() === "license"}>{renderLicense()}</Match>209 <Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>210</Switch>211```212213All state signals from the current implementation are preserved at the top of214`SettingsView` exactly as they are today. Nothing is split into child215components — this is one file, render-function style, closing over shared216signals. Follow "co-locate related logic" from AGENTS.md.217218Add the feedback signals alongside the existing ones:219220```ts221const [fbCategory, setFbCategory] = createSignal("Bug report");222const [fbMessage, setFbMessage] = createSignal("");223const [fbSubmitting, setFbSubmitting] = createSignal(false);224const [fbResult, setFbResult] = createSignal<"success" | "error" | null>(null);225```226227Add the feedback submit handler:228229```ts230async function handleSubmitFeedback() {231 if (!fbMessage().trim() || fbSubmitting()) return;232 setFbSubmitting(true);233 setFbResult(null);234 const res = await sendMessage({235 type: "SUBMIT_FEEDBACK",236 payload: {237 category: fbCategory(),238 message: fbMessage().trim(),239 version,240 browser: navigator.userAgent,241 },242 }) as { ok: boolean; error?: string };243 setFbSubmitting(false);244 if (res.ok) {245 setFbResult("success");246 setFbMessage("");247 setTimeout(() => setFbResult(null), 4000);248 } else {249 setFbResult("error");250 }251}252```253254### 4b. `renderHome()` — the category list255256Returns a `<div class="peach-settings-body">` containing two section groups257and a footer. Each row uses the `.peach-settings-cat-row` class (new, defined258in §5).259260```261Group 1 label: "PROTECTION"262 Row: Security "Locking, breach monitoring, passkeys" → "security"263 Row: Account "Plan & license key" → "license"264265Group 2 label: "VAULT"266 Row: Vault "Entries, health, import & export" → "vault"267 Row: Sync [Pro] "Paired devices & trusted networks" → "sync"268269Group 3 label: "PREFERENCES"270 Row: Appearance "Theme & accent color" → "appearance"271 Row: Shortcuts "Keyboard reference" → "shortcuts"272273Group 4 label: "SUPPORT"274 Row: Feedback "Report issues, suggest features" → "feedback"275276Footer: "Peach {version} · Browser extension" (same as current)277```278279Each row renders as:280281```tsx282<button283 class="peach-settings-cat-row"284 onClick={() => setActiveSection("security")}285>286 <div class="peach-settings-cat-icon peach-settings-cat-icon--security">287 {}288 </div>289 <div class="peach-settings-cat-content">290 <span class="peach-settings-cat-name">Security</span>291 <span class="peach-settings-cat-desc">Locking, breach monitoring, passkeys</span>292 </div>293 <svg class="peach-settings-cat-chevron" ...>›</svg>294</button>295```296297The Sync row also renders a Pro badge when `!isPro()`:298299```tsx300<Show when={!isPro()}>301 <span class="peach-sync-pro-badge">Pro</span>302</Show>303```304305The `peach-sync-pro-badge` class already exists in popup.css.306307**Icons** — all inline SVG, `width="14" height="14"`, `fill="none"`,308`stroke="currentColor"` (inherits from icon container's color), `stroke-width="1.2"`,309`stroke-linecap="round"`, `stroke-linejoin="round"`:310311| Section | Icon path description |312|------------|---------------------------------------------------------------|313| Security | Shield: `<path d="M8 2L3 4.5V8c0 3 2.5 5 5 6 2.5-1 5-3 5-6V4.5L8 2z"/>` |314| Account | Key: `<path d="M4 10a3 3 0 100-6 3 3 0 000 6zm3-3h6m-2-2v4"/>` |315| Vault | Lock body+shackle: `<rect x="3" y="7" width="10" height="7" rx="1.5"/><path d="M6 7V5a3 3 0 016 0v2"/>` |316| Sync | Two arrows in circle: `<path d="M4 8a4 4 0 014-4m4 4a4 4 0 01-4 4"/><path d="M14 6l-2 2-2-2"/><path d="M2 10l2-2 2 2"/>` |317| Appearance | Sun: `<circle cx="8" cy="8" r="3"/><path d="M8 2v1m0 10v1M2 8h1m10 0h1m-2.1-4.1-.7.7M4.8 11.3l-.7.7m0-7.7.7.7m6.5 6.3-.7.7"/>` |318| Shortcuts | Two keys: `<rect x="2" y="5" width="5" height="4" rx="1"/><rect x="9" y="7" width="5" height="4" rx="1"/><path d="M7 7h2m-2 2h2"/>` |319| Feedback | Comment bubble: `<path d="M3 4h10a1 1 0 011 1v6a1 1 0 01-1 1H5l-3 2V5a1 1 0 011-1z"/>` |320321**Section labels** use a new class `.peach-settings-nav-label` (defined in §5).322323### 4c. `renderSecurity()` — Security sub-view324325Content is exactly the existing "security" + "duress password" + "passkeys"326groups from the current Settings.tsx, reorganised into three labelled327sub-sections within a single `peach-settings-body`. No logic changes — copy328the JSX and signals verbatim, just remove the outer `peach-settings-group`329wrapper divs and replace with `peach-settings-sub-label` divs (new class,330defined in §5).331332Sub-section labels:333- `"LOCKING"` — Lock on close, Auto-lock after334- `"BREACH PROTECTION"` — Breach monitoring toggle, Check all passwords button, last check result335- `"MASTER PASSWORD"` — Change master password (inline expand form, same as current)336- `"DURESS PASSWORD"` — The full duress setup/disable flow (same as current)337- `"PASSKEYS"` — Intercept toggle, status, browser, conflict notice338339The `peach-info-notice` div for the passkey conflict note is kept verbatim.340341### 4d. `renderVault()` — Vault sub-view342343Takes the existing "vault" group and splits it into labelled sub-sections:344345- `"ENTRIES"` — Entries count (read-only), Duplicates (scan/remove flow, same as current)346- `"MAINTENANCE"` — Trash auto-delete dropdown, Vault Health button347- `"DATA"` — Import passwords, Export backup, Replace vault from backup (CJK + file buttons)348349No logic changes.350351### 4e. `renderSync()` — Sync sub-view352353This is the existing `renderSyncSection()` body verbatim. Remove the outer354`peach-settings-group` wrapper; the body is now the sub-view itself.355The Pro gate (blurred rows + overlay) works exactly as before.356357### 4f. `renderAppearance()` — Appearance sub-view358359Contains the two rows currently buried in the "vault" group:360361- Accent color picker (`.peach-accent-picker` grid — same as current)362- Theme toggle button363364Add a sub-section label `"THEME"` above the theme row and `"ACCENT"` above365the accent picker.366367### 4g. `renderShortcuts()` — Shortcuts sub-view368369Merges the two existing keyboard shortcut groups (currently duplicated):370371- One row: "Inline popup shortcut" with the `Ctrl+Shift+Space — customize ↗` link372- The full `.peach-shortcuts-grid` (C, U, /, ↑↓, Esc, ⌘⇧P)373374No duplication. The customize link opens `chrome://extensions/shortcuts`375exactly as before.376377### 4h. `renderLicense()` — License/Account sub-view378379The existing "license" group body verbatim. Label at top: `"PLAN"`.380381### 4i. `renderFeedback()` — Feedback sub-view382383```tsx384function renderFeedback() {385 const categories = ["Bug report", "Feature request", "Question", "Other"];386 return (387 <div class="peach-settings-body">388 <div class="peach-settings-group">389 <p class="peach-feedback-intro">390 Help us improve Peach. Every report goes directly to our inbox.391 </p>392393 <p class="peach-settings-sub-label">TYPE</p>394 <div class="peach-feedback-chips">395 <For each={categories}>396 {(cat) => (397 <button398 class={`peach-feedback-chip ${fbCategory() === cat ? "is-active" : ""}`}399 onClick={() => setFbCategory(cat)}400 >401 {cat}402 </button>403 )}404 </For>405 </div>406407 <p class="peach-settings-sub-label">MESSAGE</p>408 <textarea409 class="peach-feedback-textarea"410 placeholder="Describe the issue or idea..."411 value={fbMessage()}412 onInput={(e) => setFbMessage(e.currentTarget.value)}413 rows={5}414 />415416 <p class="peach-feedback-meta">417 Version {version} · will be included automatically418 </p>419420 <Show when={fbResult() === "success"}>421 <p class="peach-feedback-result peach-feedback-result--ok">422 Sent — thank you423 </p>424 </Show>425 <Show when={fbResult() === "error"}>426 <p class="peach-feedback-result peach-feedback-result--err">427 Failed to send — try again428 </p>429 </Show>430431 <button432 class="peach-btn peach-btn--accent peach-btn--sm peach-feedback-submit"433 onClick={() => void handleSubmitFeedback()}434 disabled={fbSubmitting() || !fbMessage().trim()}435 >436 {fbSubmitting() ? "Sending..." : "Send feedback"}437 </button>438439 <p class="peach-feedback-anon">440 Sent anonymously · no account required441 </p>442 </div>443 </div>444 );445}446```447448### 4j. Imports449450Add `Switch`, `Match`, and `For` to the SolidJS import at the top:451452```ts453import { createSignal, For, Match, onMount, Show, Switch } from "solid-js";454```455456---457458## 5. `src/popup/styles/popup.css` — new CSS rules459460Append the following block after the existing `/* ── Settings view ── */`461section (after line ~1714, before the `.peach-passkey-badge` rule). Do not462modify any existing rules.463464```css465466.peach-settings-nav-label {467 margin: 0 0 2px;468 padding: 12px 16px 4px;469 font-family: "IBM Plex Mono", monospace;470 font-size: 8px;471 text-transform: uppercase;472 letter-spacing: 0.18em;473 color: var(--peach-t2);474}475476.peach-settings-cat-row {477 display: flex;478 align-items: center;479 gap: 10px;480 width: 100%;481 padding: 9px 16px;482 background: none;483 border: none;484 border-bottom: 1px solid var(--peach-t3);485 cursor: pointer;486 text-align: left;487 color: inherit;488 transition: background var(--peach-dur-80) ease;489}490.peach-settings-cat-row:hover {491 background: var(--peach-t3);492}493.peach-settings-cat-row:last-child {494 border-bottom: none;495}496497.peach-settings-cat-icon {498 width: 28px;499 height: 28px;500 border-radius: 7px;501 background: color-mix(in srgb, var(--peach-accent) 10%, transparent);502 color: var(--peach-accent);503 display: flex;504 align-items: center;505 justify-content: center;506 flex-shrink: 0;507}508509.peach-settings-cat-content {510 flex: 1;511 display: flex;512 flex-direction: column;513 gap: 2px;514 min-width: 0;515}516517.peach-settings-cat-name {518 font-size: 13px;519 font-weight: 450;520 color: var(--peach-t0);521 line-height: 1.2;522}523524.peach-settings-cat-desc {525 font-size: 10px;526 color: var(--peach-t2);527 line-height: 1.3;528 white-space: nowrap;529 overflow: hidden;530 text-overflow: ellipsis;531}532533.peach-settings-cat-chevron {534 color: var(--peach-t2);535 flex-shrink: 0;536 opacity: 0.5;537}538539540.peach-settings-sub-label {541 margin: 12px 0 4px;542 font-family: "IBM Plex Mono", monospace;543 font-size: 8px;544 text-transform: uppercase;545 letter-spacing: 0.18em;546 color: var(--peach-t2);547}548.peach-settings-sub-label:first-child {549 margin-top: 4px;550}551552553.peach-feedback-intro {554 margin: 4px 0 14px;555 font-size: 12px;556 line-height: 1.55;557 color: var(--peach-t1);558}559560.peach-feedback-chips {561 display: flex;562 flex-wrap: wrap;563 gap: 6px;564 margin-bottom: 14px;565}566567.peach-feedback-chip {568 padding: 4px 10px;569 border-radius: 999px;570 border: 1px solid var(--peach-t3);571 background: none;572 color: var(--peach-t2);573 font-size: 11px;574 font-family: var(--font-ui);575 cursor: pointer;576 transition: border-color var(--peach-dur-80) ease, color var(--peach-dur-80) ease,577 background var(--peach-dur-80) ease;578}579.peach-feedback-chip:hover {580 border-color: var(--peach-t2);581 color: var(--peach-t1);582}583.peach-feedback-chip.is-active {584 border-color: color-mix(in srgb, var(--peach-accent) 50%, transparent);585 background: color-mix(in srgb, var(--peach-accent) 8%, transparent);586 color: var(--peach-accent);587}588589.peach-feedback-textarea {590 width: 100%;591 box-sizing: border-box;592 background: var(--peach-t3);593 border: 1px solid var(--peach-t3);594 border-radius: 6px;595 color: var(--peach-t0);596 font-size: 12px;597 font-family: var(--font-ui);598 line-height: 1.5;599 padding: 8px 10px;600 resize: none;601 outline: none;602 transition: border-color var(--peach-dur-150) ease;603}604.peach-feedback-textarea:focus {605 border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);606}607.peach-feedback-textarea::placeholder {608 color: var(--peach-t2);609}610611.peach-feedback-meta {612 margin: 6px 0 0;613 font-size: 10px;614 color: var(--peach-t2);615 font-family: "IBM Plex Mono", monospace;616}617618.peach-feedback-result {619 margin: 8px 0 0;620 font-size: 11px;621 font-family: "IBM Plex Mono", monospace;622}623.peach-feedback-result--ok { color: var(--peach-sync-ok); }624.peach-feedback-result--err { color: var(--peach-breach); }625626.peach-feedback-submit {627 width: 100%;628 margin-top: 12px;629 justify-content: center;630}631632.peach-feedback-anon {633 margin: 8px 0 0;634 font-size: 10px;635 color: var(--peach-t2);636 text-align: center;637}638```639640---641642## 6. `App.tsx` — no changes required643644The `SettingsProps` interface is unchanged. The `<SettingsView>` call site in645`App.tsx` does not need modification.646647---648649## 7. `.agent/CONTINUITY.md` — create/update650651Create `.agent/CONTINUITY.md` if it does not exist. Add an entry:652653```markdown654## [PLANS]655- 2026-04-26T00:00Z [USER] Settings screen redesign: two-level navigation656 (home → category sub-views) + in-settings feedback form with configurable657 backend endpoint. Explicit user authorisation to proceed despite Projects.md658 §8 "no new features" freeze.659660## [DECISIONS]661- 2026-04-26T00:00Z [CODE] Kept all state signals in top-level SettingsView662 rather than splitting into child components — avoids prop drilling, keeps663 co-location, and the file was already a single component. Render functions664 close over shared signals.665- 2026-04-26T00:00Z [CODE] FEEDBACK_ENDPOINT defaults to empty string so666 SUBMIT_FEEDBACK is a no-op until the developer sets the URL. Reuses the667 existing isAllowedCanaryWebhookUrl validator — no new validation surface.668- 2026-04-26T00:00Z [ASSUMPTION] No changes to App.tsx required; SettingsProps669 interface is unchanged.670671## [PROGRESS]672 (fill in as work proceeds)673674## [DISCOVERIES]675- Keyboard shortcuts section was duplicated in the old Settings.tsx (lines 608676 and 965). Fixed by merging into a single Shortcuts sub-view.677- The "vault" group was a catch-all for 9 unrelated concerns. Split into:678 Vault (data ops), Appearance (theme/accent), Sync (devices/networks).679```680681---682683## 8. Verification gate684685Run the full sequence from Projects.md §2 before the final save:686687```sh688689grep -r "fetch(" src/ | grep -v "pwnedpasswords" | grep -v "webhookUrl" | grep -v "FEEDBACK_ENDPOINT" | grep -v "\.test\."690691692npx tsc --noEmit693694695npm test696697698npm run build699700701grep '"version"' package.json manifest.json manifest.firefox.json702703```704705---706707## 9. Koh save sequence708709Use session name `"settings redesign and feedback"` on every save.710711```sh712713koh save --agent "<model>" --session "settings redesign and feedback" \714 "add SUBMIT_FEEDBACK message type, handler, feedback endpoint constant, version bump to 2.1.0"715716# Pass 2 — Settings.tsx717koh save --agent "<model>" --session "settings redesign and feedback" \718 "rewrite Settings.tsx: two-level navigation, seven sub-views, feedback form"719720# Pass 3 — CSS721koh save --agent "<model>" --session "settings redesign and feedback" \722 "add category nav and feedback form CSS rules to popup.css"723724# Pass 4 — tests + CONTINUITY725koh save --agent "<model>" --session "settings redesign and feedback" \726 "add SUBMIT_FEEDBACK handler tests, update CONTINUITY.md and Projects.md §3.7"727```728729---730731## 10. What is intentionally out of scope732733- **No changes to `App.tsx`** — the `SettingsProps` interface and call site734 are unchanged.735- **No actual feedback backend** — `FEEDBACK_ENDPOINT` is left empty. The736 implementing agent must not invent or hardcode a URL.737- **No animation changes** — the overlay enter/exit animations (`ov-enter-right`)738 are not modified. Sub-view transitions within settings are not animated.739- **No P0/P1 findings** — do not fix any items from Projects.md §6 in this740 task. If you notice something while reading, add it to CONTINUITY.md as a741 discovery and leave the code alone.742- **No `App.tsx` warm-up list changes** — Settings is not a lazy-loaded view743 so §4.3 does not apply here.