e42e2b44e8f8

before settings redesign and feedback feature

time
2026-04-26 13:22 UTC · 6 months ago
machine
koh2
parent
0295c6a8be14
1 file SETTINGS_REDESIGN_SPEC.md+743 −0
SETTINGS_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.

view file