52ea861750a0
Entry view: notes are no longer in its navigation set, counter, or default-selection path; single nonNoteEntries memo and pickNavigable helper make the invariant hold at every fallback (v4.3.7)
- time
- 2026-07-27 23:26 UTC · 3 months ago
- machine
- koh2
- agent
- claude-MiniMax-M3
- session
- sync false-failure and bug trio
- parent
- 3453e4d02417
↓ peach-browser-52ea8617.tar.gzatlas at this save
files changed4 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/App.tsx+31 −14manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "4.3.6",4+ "version": "4.3.7",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "4.3.6",4+ "version": "4.3.7",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "4.3.6",3+ "version": "4.3.7",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",src/popup/App.tsx+31 −14
@@ -283,10 +283,17 @@283283 }284284 });285285 286+ // Notes are not a destination of the Entry view; selecting a note opens the287+ // notes surface instead (handleSelectEntry). Anything that walks or counts288+ // the Entry view's neighbours (counter, prev/next, index) must use this289+ // filtered list, otherwise the user can arrow into a note and is stuck —290+ // the Entry view can't render a note and the prev/next buttons stop moving.291+ const nonNoteEntries = createMemo(() => entries().filter((e) => e.type !== "note"));292+286293 const selectedIndex = createMemo(() => {287294 const currentId = selectedEntry()?.id;288- if (!currentId) return entries().length > 0 ? 0 : -1;289- return entries().findIndex((entry) => entry.id === currentId);295+ if (!currentId) return nonNoteEntries().length > 0 ? 0 : -1;296+ return nonNoteEntries().findIndex((entry) => entry.id === currentId);290297 });291298 292299 async function applyAccentColor(name: AccentColorName) {@@ -983,7 +990,7 @@983990 const currentId = selectedEntry()?.id;984991 if (currentId) {985992 setSelectedEntry(986- loadedEntries.find((entry) => entry.id === currentId) ?? loadedEntries[0] ?? null993+ loadedEntries.find((entry) => entry.id === currentId) ?? pickNavigable(loadedEntries)987994 );988995 }989996 @@ -1027,7 +1034,7 @@10271034 }) ?? null;10281035 }10291036 }1030- nextSelectedEntry ??= loadedEntries[0] ?? null;1037+ nextSelectedEntry ??= pickNavigable(loadedEntries);10311038 10321039 setSelectedEntry(nextSelectedEntry);10331040 setEditEntry(undefined);@@ -1100,7 +1107,7 @@11001107 if (!entry) return;11011108 await sendMessage({ type: "DELETE_ENTRY", payload: { id: entry.id } });11021109 const loadedEntries = await loadEntries() ?? [];1103- setSelectedEntry(loadedEntries[0] ?? null);1110+ setSelectedEntry(pickNavigable(loadedEntries));11041111 setView("entry");11051112 }11061113 @@ -1111,7 +1118,7 @@11111118 const loadedEntries = await loadEntries() ?? [];11121119 // If the currently selected entry was among the deleted, move to first11131120 const stillExists = loadedEntries.find(e => e.id === selectedEntry()?.id);1114- if (!stillExists) setSelectedEntry(loadedEntries[0] ?? null);1121+ if (!stillExists) setSelectedEntry(pickNavigable(loadedEntries));11151122 }11161123 11171124 async function handleSave(savedEntry?: VaultEntry) {@@ -1184,16 +1191,26 @@11841191 }11851192 11861193 function navigate(dir: number) {1187- const allEntries = entries();1188- if (allEntries.length === 0) return;1194+ const navigable = nonNoteEntries();1195+ if (navigable.length === 0) return;11891196 const currentIndex = selectedIndex();11901197 const baseIndex = currentIndex >= 0 ? currentIndex : 0;1191- const nextIndex = (baseIndex + dir + allEntries.length) % allEntries.length;1192- setSelectedEntry(allEntries[nextIndex]);1198+ const nextIndex = (baseIndex + dir + navigable.length) % navigable.length;1199+ setSelectedEntry(navigable[nextIndex]);11931200 setContentPhase("enter");11941201 queueContentIdle();11951202 }11961203 1204+ /**1205+ * The Entry view's selection invariant: never select a note. Notes are a1206+ * separate surface (handleSelectEntry opens the notes view), so any code path1207+ * that picks a default — vault open, post-delete, refresh — must skip them1208+ * and return null if the only entries are notes.1209+ */1210+ function pickNavigable(list: VaultEntry[]): VaultEntry | null {1211+ return list.find((e) => e.type !== "note") ?? null;1212+ }1213+11971214 function openList() {11981215 setView("list");11991216 }@@ -1572,8 +1589,8 @@15721589 />15731590 <div class="peach-search-meta">15741591 <span class="peach-search-counter">1575- {entries().length > 01576- ? `${selectedIndex() + 1} / ${entries().length}`1592+ {nonNoteEntries().length > 01593+ ? `${selectedIndex() + 1} / ${nonNoteEntries().length}`15771594 : "0 entries"}15781595 </span>15791596 <span class="peach-divider"></span>@@ -1581,7 +1598,7 @@15811598 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"15821599 onClick={() => navigate(-1)}15831600 aria-label="Previous entry"1584- disabled={entries().length === 0}1601+ disabled={nonNoteEntries().length === 0}15851602 >15861603 <svg width="6" height="11" viewBox="0 0 6 11" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">15871604 <path d="M5 1L1 5.5L5 10" />@@ -1591,7 +1608,7 @@15911608 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"15921609 onClick={() => navigate(1)}15931610 aria-label="Next entry"1594- disabled={entries().length === 0}1611+ disabled={nonNoteEntries().length === 0}15951612 >15961613 <svg width="6" height="11" viewBox="0 0 6 11" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">15971614 <path d="M1 1L5 5.5L1 10" />