e42e2b44e8f8
before settings redesign and feedback feature
- time
- 2026-04-26 13:22 UTC · 6 months ago
- machine
- koh2
- parent
- 0295c6a8be14
↓ peach-browser-e42e2b44.tar.gzatlas at this save
files changed1 file
SETTINGS_REDESIGN_SPEC.md+743 −0SETTINGS_REDESIGN_SPEC.md+743 −0
@@ -0,0 +1,743 @@1+# Settings Redesign + Feedback System — Implementation Spec2+3+> 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.7+8+---9+10+## 0. Pre-flight11+12+```sh13+koh status --json # confirm dev lane, clean working tree14+koh checkpoint "before settings redesign and feedback feature"15+```16+17+Note: Projects.md §8 says "no new features until P0/P1 findings are closed."18+This task has explicit user authorisation to proceed despite that rule.19+Record the override in `.agent/CONTINUITY.md` before starting.20+21+---22+23+## 1. Version bump24+25+This is a new feature (MINOR). Bump all three files from `2.0.3` → `2.1.0`.26+All three must agree — this is a hard invariant (Projects.md §2.1 / §4.8).27+28+- `manifest.json` → `"version": "2.1.0"`29+- `manifest.firefox.json` → `"version": "2.1.0"`30+- `package.json` → `"version": "2.1.0"`31+32+---33+34+## 2. `src/shared/types.ts` — add `SUBMIT_FEEDBACK` to the MessageType union35+36+Add `"SUBMIT_FEEDBACK"` to the background-bound section of `MessageType`37+(before the content-script-bound comment block, after `"IS_CARD_DECLINED"`):38+39+```ts40+| "IS_CARD_DECLINED"41+| "SUBMIT_FEEDBACK"42+// Content-script-bound message types ...43+```44+45+No other changes to this file.46+47+---48+49+## 3. `src/background/messages.ts` — SUBMIT_FEEDBACK handler50+51+### 3a. Add the feedback endpoint constant52+53+Directly below the existing `PRIVATE_IPV4_RANGES` constant at the top of the54+file, add:55+56+```ts57+// Feedback endpoint — replace with your ingestion URL before shipping.58+// Leaving this empty disables network submission while keeping the UI functional.59+// When set, this URL is validated by isAllowedCanaryWebhookUrl before use.60+const FEEDBACK_ENDPOINT = "";61+```62+63+### 3b. Add the case to the switch64+65+Place this case immediately before the `default:` case at the bottom of the66+`switch (msg.type)` block:67+68+```ts69+case "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+```93+94+### 3c. Update `shouldRefreshAutoLock`95+96+`SUBMIT_FEEDBACK` must not refresh the auto-lock timer. Add it to the97+exclusion list in `shouldRefreshAutoLock`:98+99+```ts100+function 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" // ← add this line112+ );113+}114+```115+116+### 3d. Network surface — update Projects.md §3.7117+118+Add a third permitted fetch entry to `§3.7 Network surface` in119+`.agent/Projects.md`:120+121+```122+3. `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+```127+128+Also verify the network grep still passes:129+130+```sh131+grep -r "fetch(" src/ | grep -v "pwnedpasswords" | grep -v "webhookUrl" | grep -v "FEEDBACK_ENDPOINT" | grep -v "\.test\."132+```133+134+Must return zero results.135+136+### 3e. Tests — `src/background/messages.test.ts` (or equivalent test file)137+138+Per Projects.md §2.2, every new `messages.ts` handler requires tests. Add139+these three cases to the existing test file:140+141+```ts142+describe("SUBMIT_FEEDBACK", () => {143+ it("returns ok:true when FEEDBACK_ENDPOINT is empty (no-op path)", async () => {144+ // FEEDBACK_ENDPOINT defaults to "" in test environment145+ 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+ });151+152+ 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+ });160+161+ it("does not refresh auto-lock timer", () => {162+ expect(shouldRefreshAutoLockPublic("SUBMIT_FEEDBACK")).toBe(false);163+ });164+});165+```166+167+Adapt to the existing test harness conventions in the project. If168+`handleMessageDirect` or `shouldRefreshAutoLockPublic` are not already169+exported for testing, export them or use whatever test helper pattern exists.170+171+---172+173+## 4. `src/popup/views/Settings.tsx` — full rewrite174+175+This is the largest change. The file goes from a single 987-line flat scroll176+to a two-level navigation system: a home screen of category rows, each177+navigating to a focused sub-view.178+179+### 4a. Overall structure180+181+The outer component keeps the same `SettingsProps` interface (no changes to182+`App.tsx`). It gains a new `activeSection` signal that drives navigation.183+184+```ts185+type SectionName =186+ | "security"187+ | "vault"188+ | "sync"189+ | "appearance"190+ | "shortcuts"191+ | "license"192+ | "feedback";193+```194+195+The component returns a single `peach-overlay-view` div. The back button196+navigates: sub-view → home → `props.onBack()`. The title reflects the current197+section or reads `"settings"` on home.198+199+Use SolidJS `<Switch>/<Match>` (not early returns — see Projects.md §4.1):200+201+```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+```212+213+All state signals from the current implementation are preserved at the top of214+`SettingsView` exactly as they are today. Nothing is split into child215+components — this is one file, render-function style, closing over shared216+signals. Follow "co-locate related logic" from AGENTS.md.217+218+Add the feedback signals alongside the existing ones:219+220+```ts221+const [fbCategory, setFbCategory] = createSignal("Bug report");222+const [fbMessage, setFbMessage] = createSignal("");223+const [fbSubmitting, setFbSubmitting] = createSignal(false);224+const [fbResult, setFbResult] = createSignal<"success" | "error" | null>(null);225+```226+227+Add the feedback submit handler:228+229+```ts230+async 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+```253+254+### 4b. `renderHome()` — the category list255+256+Returns a `<div class="peach-settings-body">` containing two section groups257+and a footer. Each row uses the `.peach-settings-cat-row` class (new, defined258+in §5).259+260+```261+Group 1 label: "PROTECTION"262+ Row: Security "Locking, breach monitoring, passkeys" → "security"263+ Row: Account "Plan & license key" → "license"264+265+Group 2 label: "VAULT"266+ Row: Vault "Entries, health, import & export" → "vault"267+ Row: Sync [Pro] "Paired devices & trusted networks" → "sync"268+269+Group 3 label: "PREFERENCES"270+ Row: Appearance "Theme & accent color" → "appearance"271+ Row: Shortcuts "Keyboard reference" → "shortcuts"272+273+Group 4 label: "SUPPORT"274+ Row: Feedback "Report issues, suggest features" → "feedback"275+276+Footer: "Peach {version} · Browser extension" (same as current)277+```278+279+Each row renders as:280+281+```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+ {/* inline SVG shield icon, 14×14, stroke="var(--peach-accent)" */}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+```296+297+The Sync row also renders a Pro badge when `!isPro()`:298+299+```tsx300+<Show when={!isPro()}>301+ <span class="peach-sync-pro-badge">Pro</span>302+</Show>303+```304+305+The `peach-sync-pro-badge` class already exists in popup.css.306+307+**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"`:310+311+| 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"/>` |320+321+**Section labels** use a new class `.peach-settings-nav-label` (defined in §5).322+323+### 4c. `renderSecurity()` — Security sub-view324+325+Content is exactly the existing "security" + "duress password" + "passkeys"326+groups from the current Settings.tsx, reorganised into three labelled327+sub-sections within a single `peach-settings-body`. No logic changes — copy328+the JSX and signals verbatim, just remove the outer `peach-settings-group`329+wrapper divs and replace with `peach-settings-sub-label` divs (new class,330+defined in §5).331+332+Sub-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 notice338+339+The `peach-info-notice` div for the passkey conflict note is kept verbatim.340+341+### 4d. `renderVault()` — Vault sub-view342+343+Takes the existing "vault" group and splits it into labelled sub-sections:344+345+- `"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)348+349+No logic changes.350+351+### 4e. `renderSync()` — Sync sub-view352+353+This is the existing `renderSyncSection()` body verbatim. Remove the outer354+`peach-settings-group` wrapper; the body is now the sub-view itself.355+The Pro gate (blurred rows + overlay) works exactly as before.356+357+### 4f. `renderAppearance()` — Appearance sub-view358+359+Contains the two rows currently buried in the "vault" group:360+361+- Accent color picker (`.peach-accent-picker` grid — same as current)362+- Theme toggle button363+364+Add a sub-section label `"THEME"` above the theme row and `"ACCENT"` above365+the accent picker.366+367+### 4g. `renderShortcuts()` — Shortcuts sub-view368+369+Merges the two existing keyboard shortcut groups (currently duplicated):370+371+- One row: "Inline popup shortcut" with the `Ctrl+Shift+Space — customize ↗` link372+- The full `.peach-shortcuts-grid` (C, U, /, ↑↓, Esc, ⌘⇧P)373+374+No duplication. The customize link opens `chrome://extensions/shortcuts`375+exactly as before.376+377+### 4h. `renderLicense()` — License/Account sub-view378+379+The existing "license" group body verbatim. Label at top: `"PLAN"`.380+381+### 4i. `renderFeedback()` — Feedback sub-view382+383+```tsx384+function 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>392+393+ <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>406+407+ <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+ />415+416+ <p class="peach-feedback-meta">417+ Version {version} · will be included automatically418+ </p>419+420+ <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>430+431+ <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>438+439+ <p class="peach-feedback-anon">440+ Sent anonymously · no account required441+ </p>442+ </div>443+ </div>444+ );445+}446+```447+448+### 4j. Imports449+450+Add `Switch`, `Match`, and `For` to the SolidJS import at the top:451+452+```ts453+import { createSignal, For, Match, onMount, Show, Switch } from "solid-js";454+```455+456+---457+458+## 5. `src/popup/styles/popup.css` — new CSS rules459+460+Append the following block after the existing `/* ── Settings view ── */`461+section (after line ~1714, before the `.peach-passkey-badge` rule). Do not462+modify any existing rules.463+464+```css465+/* ── Settings home navigation ──────────────────────────────────────────── */466+.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+}475+476+.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+}496+497+.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+}508+509+.peach-settings-cat-content {510+ flex: 1;511+ display: flex;512+ flex-direction: column;513+ gap: 2px;514+ min-width: 0;515+}516+517+.peach-settings-cat-name {518+ font-size: 13px;519+ font-weight: 450;520+ color: var(--peach-t0);521+ line-height: 1.2;522+}523+524+.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+}532+533+.peach-settings-cat-chevron {534+ color: var(--peach-t2);535+ flex-shrink: 0;536+ opacity: 0.5;537+}538+539+/* ── Settings sub-view section labels ──────────────────────────────────── */540+.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+}551+552+/* ── Feedback form ──────────────────────────────────────────────────────── */553+.peach-feedback-intro {554+ margin: 4px 0 14px;555+ font-size: 12px;556+ line-height: 1.55;557+ color: var(--peach-t1);558+}559+560+.peach-feedback-chips {561+ display: flex;562+ flex-wrap: wrap;563+ gap: 6px;564+ margin-bottom: 14px;565+}566+567+.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+}588+589+.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+}610+611+.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+}617+618+.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); }625+626+.peach-feedback-submit {627+ width: 100%;628+ margin-top: 12px;629+ justify-content: center;630+}631+632+.peach-feedback-anon {633+ margin: 8px 0 0;634+ font-size: 10px;635+ color: var(--peach-t2);636+ text-align: center;637+}638+```639+640+---641+642+## 6. `App.tsx` — no changes required643+644+The `SettingsProps` interface is unchanged. The `<SettingsView>` call site in645+`App.tsx` does not need modification.646+647+---648+649+## 7. `.agent/CONTINUITY.md` — create/update650+651+Create `.agent/CONTINUITY.md` if it does not exist. Add an entry:652+653+```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.659+660+## [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.670+671+## [PROGRESS]672+ (fill in as work proceeds)673+674+## [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+```680+681+---682+683+## 8. Verification gate684+685+Run the full sequence from Projects.md §2 before the final save:686+687+```sh688+# Network surface check — must return zero results689+grep -r "fetch(" src/ | grep -v "pwnedpasswords" | grep -v "webhookUrl" | grep -v "FEEDBACK_ENDPOINT" | grep -v "\.test\."690+691+# TypeScript — zero errors692+npx tsc --noEmit693+694+# Tests — zero failures695+npm test696+697+# Build — no new errors698+npm run build699+700+# Version consistency701+grep '"version"' package.json manifest.json manifest.firefox.json702+# All three must show 2.1.0703+```704+705+---706+707+## 9. Koh save sequence708+709+Use session name `"settings redesign and feedback"` on every save.710+711+```sh712+# Pass 1 — scaffolding713+koh save --agent "<model>" --session "settings redesign and feedback" \714+ "add SUBMIT_FEEDBACK message type, handler, feedback endpoint constant, version bump to 2.1.0"715+716+# Pass 2 — Settings.tsx717+koh save --agent "<model>" --session "settings redesign and feedback" \718+ "rewrite Settings.tsx: two-level navigation, seven sub-views, feedback form"719+720+# Pass 3 — CSS721+koh save --agent "<model>" --session "settings redesign and feedback" \722+ "add category nav and feedback form CSS rules to popup.css"723+724+# Pass 4 — tests + CONTINUITY725+koh save --agent "<model>" --session "settings redesign and feedback" \726+ "add SUBMIT_FEEDBACK handler tests, update CONTINUITY.md and Projects.md §3.7"727+```728+729+---730+731+## 10. What is intentionally out of scope732+733+- **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.