Store the playback volume in app settings (config.json) whenever it
changes and re-apply the previous session's level at boot, instead of
always starting at the old 70% fallback.
- AppSettings gains volume (default 1 = 100%); both default-settings
copies and the volume signal default are raised from 0.7 to 1.
- doSetVolume persists via the app store (mirrors playbackSpeed).
- The boot sync awaits the app store's async config load (new
whenReady()) so a persisted level is applied even when settings load
finishes after useAudio mounts.
- tests/volume-persistence.test.ts: default, clamp, and cross-session
reuse (fresh module instance simulates the next launch).
Reload the episode that was loaded in the player when the previous run
ended (persisted on play/load and synchronously at exit) into the Player
tab paused at its saved position — never autostarted. Episodes at or
above 98% completion are skipped, as are empty-player and unsubscribed
episodes.
- useAudio gains load(episode) (sets currentEpisode/position/Now Playing
without starting the backend) and restoreLastSession(), triggered once
at boot and serialized through a chain so a late-finishing boot restore
can't clobber later state.
- togglePlayback branches on a startedPlayback flag: a restored episode
starts the backend from the saved position; a paused one resumes.
- stop() clears the marker; the exit teardown writes it synchronously
(process.exit bypasses async writes).
- feed/progress stores expose whenReady() so restore waits for the async
boot loads; feed store gains findEpisode().
- app-persistence serializes last-player marker writes and exposes
waitForLastPlayerWrite() for deterministic tests.
- tests/restore-session.test.ts: real modules + local RSS feed server;
the real useAudio is imported via a ?restore-test query suffix to
bypass the suite's mock.module leak across shared bun workers.
Self-rescheduling refresh timer (default 30 min, configurable via a
Preferences item, re-read on every tick, skips in-flight refreshes).
fetchEpisodes returns null on network failure/timeout so a failed
refresh can never wipe a feed's episodes (addFeed/refreshFeed/
refreshAllFeeds all treat null as unchanged); feeds still refresh on
launch.
History used localStorage, which never exists in the Bun TUI, so nothing
survived a restart. Store the 10 most recent queries in config-dir
search-history.json (same fire-and-forget file pattern as audio-nav.json),
loaded asynchronously at store init. Dedupe case-insensitively, cap at 10.
Adds a Selection Marker toggle under Settings → Preferences that
controls the ❯ cursor glyph on the focused row of every list.
Default off; rows keep a leading space for alignment either way.
Every list pane (tab strip, feed, my shows, discover, search,
settings, whitelist editor) reads the glyph through the shared
useSelectionMarker hook.
Also aligns the lead column across panes: page list rows drop
their extra box paddingLeft so labels start at the same column
as the tab strip (2 cells without nerd icons, 4 with), keeping
episode sub-rows aligned to the title.
Loading indicators: the braille spinner now carries a contextual label
(Refreshing…, Fetching…, Loading more…, Discovering…, Searching…) and is
shown in every loading state that previously rendered nothing — Discover
results, Search results fallback, and the empty Feed list.
Feed pagination: a focusable "[Fetch More]" row at the bottom of the flat
feed list advances every feed's loaded window by 50 episodes via the new
loadMoreAllFeeds/hasMoreAcrossAll store API. Behavior is a setting
(Fetch More: manual|auto, default manual) persisted in config.json; auto
fetches when focus reaches the bottom row. The button row is excluded
from episode focus so no episode is double-highlighted while it is active.