SETTINGS_REDESIGN_SPEC.md

23.0 KB · raw

rendered · source

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         # confirm dev lane, clean working tree14koh 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"42// Content-script-bound message types ...43```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```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.60const 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"    // ← add this line112  );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    // 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  });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    {/* 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```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```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}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}538539/* ── 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}551552/* ── 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}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```sh688# Network surface check — must return zero results689grep -r "fetch(" src/ | grep -v "pwnedpasswords" | grep -v "webhookUrl" | grep -v "FEEDBACK_ENDPOINT" | grep -v "\.test\."690691# TypeScript — zero errors692npx tsc --noEmit693694# Tests — zero failures695npm test696697# Build — no new errors698npm run build699700# Version consistency701grep '"version"' package.json manifest.json manifest.firefox.json702# All three must show 2.1.0703```704705---706707## 9. Koh save sequence708709Use session name `"settings redesign and feedback"` on every save.710711```sh712# Pass 1 — scaffolding713koh 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.