SETTINGS_REDESIGN_SPEC.md
Settings Redesign + Feedback System — Implementation Spec
Read
AGENTS.md,.agent/Projects.md, and.agent/CONTINUITY.mdbefore touching any file. Follow every invariant in Projects.md without exception. All Koh rules in AGENTS.md apply. Use thedevlane. Start with a checkpoint.
0. Pre-flight
koh status --json # confirm dev lane, clean working tree
koh checkpoint "before settings redesign and feedback feature"
Note: Projects.md §8 says "no new features until P0/P1 findings are closed."
This task has explicit user authorisation to proceed despite that rule.
Record the override in .agent/CONTINUITY.md before starting.
1. Version bump
This is a new feature (MINOR). Bump all three files from 2.0.3 → 2.1.0.
All three must agree — this is a hard invariant (Projects.md §2.1 / §4.8).
manifest.json→"version": "2.1.0"manifest.firefox.json→"version": "2.1.0"package.json→"version": "2.1.0"
2. src/shared/types.ts — add SUBMIT_FEEDBACK to the MessageType union
Add "SUBMIT_FEEDBACK" to the background-bound section of MessageType
(before the content-script-bound comment block, after "IS_CARD_DECLINED"):
| "IS_CARD_DECLINED"
| "SUBMIT_FEEDBACK"
// Content-script-bound message types ...
No other changes to this file.
3. src/background/messages.ts — SUBMIT_FEEDBACK handler
3a. Add the feedback endpoint constant
Directly below the existing PRIVATE_IPV4_RANGES constant at the top of the
file, add:
// Feedback endpoint — replace with your ingestion URL before shipping.
// Leaving this empty disables network submission while keeping the UI functional.
// When set, this URL is validated by isAllowedCanaryWebhookUrl before use.
const FEEDBACK_ENDPOINT = "";
3b. Add the case to the switch
Place this case immediately before the default: case at the bottom of the
switch (msg.type) block:
case "SUBMIT_FEEDBACK": {
const { category, message, version, browser } = msg.payload as {
category: string;
message: string;
version: string;
browser: string;
};
if (!message.trim()) return { ok: false, error: "message is empty" };
if (!FEEDBACK_ENDPOINT) return { ok: true };
if (!isAllowedCanaryWebhookUrl(FEEDBACK_ENDPOINT)) {
return { ok: false, error: "invalid feedback endpoint" };
}
try {
const res = await fetch(FEEDBACK_ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ category, message, version, browser, source: "peach-extension" }),
});
return { ok: res.ok, error: res.ok ? undefined : `HTTP ${res.status}` };
} catch (err) {
return { ok: false, error: err instanceof Error ? err.message : "network error" };
}
}
3c. Update shouldRefreshAutoLock
SUBMIT_FEEDBACK must not refresh the auto-lock timer. Add it to the
exclusion list in shouldRefreshAutoLock:
function shouldRefreshAutoLock(type: MessageType): boolean {
return (
type !== "HAS_VAULT" &&
type !== "IS_UNLOCKED" &&
type !== "GET_VAULT_STATUS" &&
type !== "LOCK" &&
type !== "UPDATE_SETTINGS" &&
type !== "GET_PENDING_PASSKEY" &&
type !== "GET_PASSKEY_PROVIDER_STATE" &&
type !== "DECLINE_CARD_SAVE" &&
type !== "IS_CARD_DECLINED" &&
type !== "SUBMIT_FEEDBACK" // ← add this line
);
}
3d. Network surface — update Projects.md §3.7
Add a third permitted fetch entry to §3.7 Network surface in
.agent/Projects.md:
3. `FEEDBACK_ENDPOINT` constant in `src/background/messages.ts` — only when
non-empty, only on `SUBMIT_FEEDBACK` messages initiated by the user. URL
validated by `isAllowedCanaryWebhookUrl`. Defaults to empty string
(disabled). Set by the developer before shipping.
Also verify the network grep still passes:
grep -r "fetch(" src/ | grep -v "pwnedpasswords" | grep -v "webhookUrl" | grep -v "FEEDBACK_ENDPOINT" | grep -v "\.test\."
Must return zero results.
3e. Tests — src/background/messages.test.ts (or equivalent test file)
Per Projects.md §2.2, every new messages.ts handler requires tests. Add
these three cases to the existing test file:
describe("SUBMIT_FEEDBACK", () => {
it("returns ok:true when FEEDBACK_ENDPOINT is empty (no-op path)", async () => {
// FEEDBACK_ENDPOINT defaults to "" in test environment
const res = await handleMessageDirect({
type: "SUBMIT_FEEDBACK",
payload: { category: "Bug report", message: "test", version: "2.1.0", browser: "Chrome" },
});
expect(res.ok).toBe(true);
});
it("returns ok:false when message is empty", async () => {
const res = await handleMessageDirect({
type: "SUBMIT_FEEDBACK",
payload: { category: "Bug report", message: " ", version: "2.1.0", browser: "Chrome" },
});
expect(res.ok).toBe(false);
expect(res.error).toBe("message is empty");
});
it("does not refresh auto-lock timer", () => {
expect(shouldRefreshAutoLockPublic("SUBMIT_FEEDBACK")).toBe(false);
});
});
Adapt to the existing test harness conventions in the project. If
handleMessageDirect or shouldRefreshAutoLockPublic are not already
exported for testing, export them or use whatever test helper pattern exists.
4. src/popup/views/Settings.tsx — full rewrite
This is the largest change. The file goes from a single 987-line flat scroll to a two-level navigation system: a home screen of category rows, each navigating to a focused sub-view.
4a. Overall structure
The outer component keeps the same SettingsProps interface (no changes to
App.tsx). It gains a new activeSection signal that drives navigation.
type SectionName =
| "security"
| "vault"
| "sync"
| "appearance"
| "shortcuts"
| "license"
| "feedback";
The component returns a single peach-overlay-view div. The back button
navigates: sub-view → home → props.onBack(). The title reflects the current
section or reads "settings" on home.
Use SolidJS <Switch>/<Match> (not early returns — see Projects.md §4.1):
<Switch fallback={renderHome()}>
<Match when={activeSection() === "security"}>{renderSecurity()}</Match>
<Match when={activeSection() === "vault"}>{renderVault()}</Match>
<Match when={activeSection() === "sync"}>{renderSync()}</Match>
<Match when={activeSection() === "appearance"}>{renderAppearance()}</Match>
<Match when={activeSection() === "shortcuts"}>{renderShortcuts()}</Match>
<Match when={activeSection() === "license"}>{renderLicense()}</Match>
<Match when={activeSection() === "feedback"}>{renderFeedback()}</Match>
</Switch>
All state signals from the current implementation are preserved at the top of
SettingsView exactly as they are today. Nothing is split into child
components — this is one file, render-function style, closing over shared
signals. Follow "co-locate related logic" from AGENTS.md.
Add the feedback signals alongside the existing ones:
const [fbCategory, setFbCategory] = createSignal("Bug report");
const [fbMessage, setFbMessage] = createSignal("");
const [fbSubmitting, setFbSubmitting] = createSignal(false);
const [fbResult, setFbResult] = createSignal<"success" | "error" | null>(null);
Add the feedback submit handler:
async function handleSubmitFeedback() {
if (!fbMessage().trim() || fbSubmitting()) return;
setFbSubmitting(true);
setFbResult(null);
const res = await sendMessage({
type: "SUBMIT_FEEDBACK",
payload: {
category: fbCategory(),
message: fbMessage().trim(),
version,
browser: navigator.userAgent,
},
}) as { ok: boolean; error?: string };
setFbSubmitting(false);
if (res.ok) {
setFbResult("success");
setFbMessage("");
setTimeout(() => setFbResult(null), 4000);
} else {
setFbResult("error");
}
}
4b. renderHome() — the category list
Returns a <div class="peach-settings-body"> containing two section groups
and a footer. Each row uses the .peach-settings-cat-row class (new, defined
in §5).
Group 1 label: "PROTECTION"
Row: Security "Locking, breach monitoring, passkeys" → "security"
Row: Account "Plan & license key" → "license"
Group 2 label: "VAULT"
Row: Vault "Entries, health, import & export" → "vault"
Row: Sync [Pro] "Paired devices & trusted networks" → "sync"
Group 3 label: "PREFERENCES"
Row: Appearance "Theme & accent color" → "appearance"
Row: Shortcuts "Keyboard reference" → "shortcuts"
Group 4 label: "SUPPORT"
Row: Feedback "Report issues, suggest features" → "feedback"
Footer: "Peach {version} · Browser extension" (same as current)
Each row renders as:
<button
class="peach-settings-cat-row"
onClick={() => setActiveSection("security")}
>
<div class="peach-settings-cat-icon peach-settings-cat-icon--security">
{/* inline SVG shield icon, 14×14, stroke="var(--peach-accent)" */}
</div>
<div class="peach-settings-cat-content">
<span class="peach-settings-cat-name">Security</span>
<span class="peach-settings-cat-desc">Locking, breach monitoring, passkeys</span>
</div>
<svg class="peach-settings-cat-chevron" ...>›</svg>
</button>
The Sync row also renders a Pro badge when !isPro():
<Show when={!isPro()}>
<span class="peach-sync-pro-badge">Pro</span>
</Show>
The peach-sync-pro-badge class already exists in popup.css.
Icons — all inline SVG, width="14" height="14", fill="none",
stroke="currentColor" (inherits from icon container's color), stroke-width="1.2",
stroke-linecap="round", stroke-linejoin="round":
| Section | Icon path description |
|---|---|
| Security | Shield: <path d="M8 2L3 4.5V8c0 3 2.5 5 5 6 2.5-1 5-3 5-6V4.5L8 2z"/> |
| Account | Key: <path d="M4 10a3 3 0 100-6 3 3 0 000 6zm3-3h6m-2-2v4"/> |
| Vault | Lock body+shackle: <rect x="3" y="7" width="10" height="7" rx="1.5"/><path d="M6 7V5a3 3 0 016 0v2"/> |
| 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"/> |
| 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"/> |
| 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"/> |
| Feedback | Comment bubble: <path d="M3 4h10a1 1 0 011 1v6a1 1 0 01-1 1H5l-3 2V5a1 1 0 011-1z"/> |
Section labels use a new class .peach-settings-nav-label (defined in §5).
4c. renderSecurity() — Security sub-view
Content is exactly the existing "security" + "duress password" + "passkeys"
groups from the current Settings.tsx, reorganised into three labelled
sub-sections within a single peach-settings-body. No logic changes — copy
the JSX and signals verbatim, just remove the outer peach-settings-group
wrapper divs and replace with peach-settings-sub-label divs (new class,
defined in §5).
Sub-section labels:
"LOCKING"— Lock on close, Auto-lock after"BREACH PROTECTION"— Breach monitoring toggle, Check all passwords button, last check result"MASTER PASSWORD"— Change master password (inline expand form, same as current)"DURESS PASSWORD"— The full duress setup/disable flow (same as current)"PASSKEYS"— Intercept toggle, status, browser, conflict notice
The peach-info-notice div for the passkey conflict note is kept verbatim.
4d. renderVault() — Vault sub-view
Takes the existing "vault" group and splits it into labelled sub-sections:
"ENTRIES"— Entries count (read-only), Duplicates (scan/remove flow, same as current)"MAINTENANCE"— Trash auto-delete dropdown, Vault Health button"DATA"— Import passwords, Export backup, Replace vault from backup (CJK + file buttons)
No logic changes.
4e. renderSync() — Sync sub-view
This is the existing renderSyncSection() body verbatim. Remove the outer
peach-settings-group wrapper; the body is now the sub-view itself.
The Pro gate (blurred rows + overlay) works exactly as before.
4f. renderAppearance() — Appearance sub-view
Contains the two rows currently buried in the "vault" group:
- Accent color picker (
.peach-accent-pickergrid — same as current) - Theme toggle button
Add a sub-section label "THEME" above the theme row and "ACCENT" above
the accent picker.
4g. renderShortcuts() — Shortcuts sub-view
Merges the two existing keyboard shortcut groups (currently duplicated):
- One row: "Inline popup shortcut" with the
Ctrl+Shift+Space — customize ↗link - The full
.peach-shortcuts-grid(C, U, /, ↑↓, Esc, ⌘⇧P)
No duplication. The customize link opens chrome://extensions/shortcuts
exactly as before.
4h. renderLicense() — License/Account sub-view
The existing "license" group body verbatim. Label at top: "PLAN".
4i. renderFeedback() — Feedback sub-view
function renderFeedback() {
const categories = ["Bug report", "Feature request", "Question", "Other"];
return (
<div class="peach-settings-body">
<div class="peach-settings-group">
<p class="peach-feedback-intro">
Help us improve Peach. Every report goes directly to our inbox.
</p>
<p class="peach-settings-sub-label">TYPE</p>
<div class="peach-feedback-chips">
<For each={categories}>
{(cat) => (
<button
class={`peach-feedback-chip ${fbCategory() === cat ? "is-active" : ""}`}
onClick={() => setFbCategory(cat)}
>
{cat}
</button>
)}
</For>
</div>
<p class="peach-settings-sub-label">MESSAGE</p>
<textarea
class="peach-feedback-textarea"
placeholder="Describe the issue or idea..."
value={fbMessage()}
onInput={(e) => setFbMessage(e.currentTarget.value)}
rows={5}
/>
<p class="peach-feedback-meta">
Version {version} · will be included automatically
</p>
<Show when={fbResult() === "success"}>
<p class="peach-feedback-result peach-feedback-result--ok">
Sent — thank you
</p>
</Show>
<Show when={fbResult() === "error"}>
<p class="peach-feedback-result peach-feedback-result--err">
Failed to send — try again
</p>
</Show>
<button
class="peach-btn peach-btn--accent peach-btn--sm peach-feedback-submit"
onClick={() => void handleSubmitFeedback()}
disabled={fbSubmitting() || !fbMessage().trim()}
>
{fbSubmitting() ? "Sending..." : "Send feedback"}
</button>
<p class="peach-feedback-anon">
Sent anonymously · no account required
</p>
</div>
</div>
);
}
4j. Imports
Add Switch, Match, and For to the SolidJS import at the top:
import { createSignal, For, Match, onMount, Show, Switch } from "solid-js";
5. src/popup/styles/popup.css — new CSS rules
Append the following block after the existing /* ── Settings view ── */
section (after line ~1714, before the .peach-passkey-badge rule). Do not
modify any existing rules.
/* ── Settings home navigation ──────────────────────────────────────────── */
.peach-settings-nav-label {
margin: 0 0 2px;
padding: 12px 16px 4px;
font-family: "IBM Plex Mono", monospace;
font-size: 8px;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--peach-t2);
}
.peach-settings-cat-row {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 9px 16px;
background: none;
border: none;
border-bottom: 1px solid var(--peach-t3);
cursor: pointer;
text-align: left;
color: inherit;
transition: background var(--peach-dur-80) ease;
}
.peach-settings-cat-row:hover {
background: var(--peach-t3);
}
.peach-settings-cat-row:last-child {
border-bottom: none;
}
.peach-settings-cat-icon {
width: 28px;
height: 28px;
border-radius: 7px;
background: color-mix(in srgb, var(--peach-accent) 10%, transparent);
color: var(--peach-accent);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.peach-settings-cat-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.peach-settings-cat-name {
font-size: 13px;
font-weight: 450;
color: var(--peach-t0);
line-height: 1.2;
}
.peach-settings-cat-desc {
font-size: 10px;
color: var(--peach-t2);
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.peach-settings-cat-chevron {
color: var(--peach-t2);
flex-shrink: 0;
opacity: 0.5;
}
/* ── Settings sub-view section labels ──────────────────────────────────── */
.peach-settings-sub-label {
margin: 12px 0 4px;
font-family: "IBM Plex Mono", monospace;
font-size: 8px;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--peach-t2);
}
.peach-settings-sub-label:first-child {
margin-top: 4px;
}
/* ── Feedback form ──────────────────────────────────────────────────────── */
.peach-feedback-intro {
margin: 4px 0 14px;
font-size: 12px;
line-height: 1.55;
color: var(--peach-t1);
}
.peach-feedback-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 14px;
}
.peach-feedback-chip {
padding: 4px 10px;
border-radius: 999px;
border: 1px solid var(--peach-t3);
background: none;
color: var(--peach-t2);
font-size: 11px;
font-family: var(--font-ui);
cursor: pointer;
transition: border-color var(--peach-dur-80) ease, color var(--peach-dur-80) ease,
background var(--peach-dur-80) ease;
}
.peach-feedback-chip:hover {
border-color: var(--peach-t2);
color: var(--peach-t1);
}
.peach-feedback-chip.is-active {
border-color: color-mix(in srgb, var(--peach-accent) 50%, transparent);
background: color-mix(in srgb, var(--peach-accent) 8%, transparent);
color: var(--peach-accent);
}
.peach-feedback-textarea {
width: 100%;
box-sizing: border-box;
background: var(--peach-t3);
border: 1px solid var(--peach-t3);
border-radius: 6px;
color: var(--peach-t0);
font-size: 12px;
font-family: var(--font-ui);
line-height: 1.5;
padding: 8px 10px;
resize: none;
outline: none;
transition: border-color var(--peach-dur-150) ease;
}
.peach-feedback-textarea:focus {
border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);
}
.peach-feedback-textarea::placeholder {
color: var(--peach-t2);
}
.peach-feedback-meta {
margin: 6px 0 0;
font-size: 10px;
color: var(--peach-t2);
font-family: "IBM Plex Mono", monospace;
}
.peach-feedback-result {
margin: 8px 0 0;
font-size: 11px;
font-family: "IBM Plex Mono", monospace;
}
.peach-feedback-result--ok { color: var(--peach-sync-ok); }
.peach-feedback-result--err { color: var(--peach-breach); }
.peach-feedback-submit {
width: 100%;
margin-top: 12px;
justify-content: center;
}
.peach-feedback-anon {
margin: 8px 0 0;
font-size: 10px;
color: var(--peach-t2);
text-align: center;
}
6. App.tsx — no changes required
The SettingsProps interface is unchanged. The <SettingsView> call site in
App.tsx does not need modification.
7. .agent/CONTINUITY.md — create/update
Create .agent/CONTINUITY.md if it does not exist. Add an entry:
## [PLANS]
- 2026-04-26T00:00Z [USER] Settings screen redesign: two-level navigation
(home → category sub-views) + in-settings feedback form with configurable
backend endpoint. Explicit user authorisation to proceed despite Projects.md
§8 "no new features" freeze.
## [DECISIONS]
- 2026-04-26T00:00Z [CODE] Kept all state signals in top-level SettingsView
rather than splitting into child components — avoids prop drilling, keeps
co-location, and the file was already a single component. Render functions
close over shared signals.
- 2026-04-26T00:00Z [CODE] FEEDBACK_ENDPOINT defaults to empty string so
SUBMIT_FEEDBACK is a no-op until the developer sets the URL. Reuses the
existing isAllowedCanaryWebhookUrl validator — no new validation surface.
- 2026-04-26T00:00Z [ASSUMPTION] No changes to App.tsx required; SettingsProps
interface is unchanged.
## [PROGRESS]
(fill in as work proceeds)
## [DISCOVERIES]
- Keyboard shortcuts section was duplicated in the old Settings.tsx (lines 608
and 965). Fixed by merging into a single Shortcuts sub-view.
- The "vault" group was a catch-all for 9 unrelated concerns. Split into:
Vault (data ops), Appearance (theme/accent), Sync (devices/networks).
8. Verification gate
Run the full sequence from Projects.md §2 before the final save:
# Network surface check — must return zero results
grep -r "fetch(" src/ | grep -v "pwnedpasswords" | grep -v "webhookUrl" | grep -v "FEEDBACK_ENDPOINT" | grep -v "\.test\."
# TypeScript — zero errors
npx tsc --noEmit
# Tests — zero failures
npm test
# Build — no new errors
npm run build
# Version consistency
grep '"version"' package.json manifest.json manifest.firefox.json
# All three must show 2.1.0
9. Koh save sequence
Use session name "settings redesign and feedback" on every save.
# Pass 1 — scaffolding
koh save --agent "<model>" --session "settings redesign and feedback" \
"add SUBMIT_FEEDBACK message type, handler, feedback endpoint constant, version bump to 2.1.0"
# Pass 2 — Settings.tsx
koh save --agent "<model>" --session "settings redesign and feedback" \
"rewrite Settings.tsx: two-level navigation, seven sub-views, feedback form"
# Pass 3 — CSS
koh save --agent "<model>" --session "settings redesign and feedback" \
"add category nav and feedback form CSS rules to popup.css"
# Pass 4 — tests + CONTINUITY
koh save --agent "<model>" --session "settings redesign and feedback" \
"add SUBMIT_FEEDBACK handler tests, update CONTINUITY.md and Projects.md §3.7"
10. What is intentionally out of scope
- No changes to
App.tsx— theSettingsPropsinterface and call site are unchanged. - No actual feedback backend —
FEEDBACK_ENDPOINTis left empty. The implementing agent must not invent or hardcode a URL. - No animation changes — the overlay enter/exit animations (
ov-enter-right) are not modified. Sub-view transitions within settings are not animated. - No P0/P1 findings — do not fix any items from Projects.md §6 in this task. If you notice something while reading, add it to CONTINUITY.md as a discovery and leave the code alone.
- No
App.tsxwarm-up list changes — Settings is not a lazy-loaded view so §4.3 does not apply here.