Preferences

Status: current. This page describes preferences as they work after the last phase of the refactor plan. The last part records what the plan changed.

Preferences are what Deckard remembers that is not in the notes: favorites, pins, saved searches, view counts, task order, Home widgets, and layout choices. Deckard never writes this state into a Markdown file. It lives in VS Code’s Memento storage.

Where it lives

Phase 3 split the one 1,834-line PreferencesStore into a repository, a pure schema, and a service per capability, all in src/core/storage:

File What it holds
preferencesSchema.ts The format, and nothing that reads storage or the clock: normalizePreferences and every normalizer, the defaults and limits, the Home widget kinds and their option rules, the migrations that run on read and carryLegacyIds, pinKey and the Find helpers, the workspace’s 18 keys and the split that picks and omits them, and the helpers oneOf, upsertById, toggled, bumped, and filterNumericRecord
preferencesRepository.ts PreferencesRepository: the two KeyValueStores, the read at construction, the seed from the machine-wide store and the handover flag, the write queue, and the change and visit events. PreferencesReader is its read-only surface: value, onDidChange, and onDidRecordVisit
preferencesFavorites.ts FavoritesService: favorite tags and entities, and the custom order of each
preferencesUsage.ts UsageService: tag, entity, and section access, carried view counts, Find choices, recent headings
preferencesTaskLayout.ts TaskLayoutService: the rank order, the task sort, and the Task Board’s layout, grouping, and table
preferencesHomeWidgets.ts HomeWidgetsService: Home’s widgets and the Dashboard’s view state
preferencesPins.ts PinsService: the notes pinned to Home
preferencesSavedSearches.ts SavedSearchesService: saved searches and recent searches
preferencesDisplay.ts DisplayService: sort modes, column counts, render mode, page sizes, and the Related Notes options
preferencesTagRenames.ts TagRenames: the cascade that moves what a renamed tag held to its new key
preferencesMaintenance.ts PreferencesMaintenance: pruning against the index, the stale-choice check, and restoring a blob
preferences.ts PreferenceServices, the set a caller picks its services from. The schema’s helpers and limits, and the maintenance and repository types, are imported from the modules that declare them

Each service is a small class over the repository. It reads the blob as it stands and makes each change with one update, as the store’s method did. PreferencesStore, the facade that held them all through Phase 3, was deleted in Phase 4. preferenceSnapshots.ts keeps rolling copies, and src/ui/commands/preferenceBackups.ts exports and imports them.

Who receives what

createServices in src/composition/services.ts makes the repository and each service once, in the order the facade did, and calls initialize() at once, so the seed and the handover land when they always did. Each caller is handed only what it uses, typed as Pick<PreferenceServices, ...> or as a narrow interface over one service:

Caller Receives
Home (DashboardPanel) reader, favorites, usage, homeWidgets, pins, savedSearches, display, tagRenames
The search pages (SearchPanels, SearchPanel) reader, usage, savedSearches, display, pins, homeWidgets, tagRenames
Related Notes (SidebarNotesView) reader, display, usage, tagRenames
Stats reader, usage
The Task Board reader, taskLayout, savedSearches, homeWidgets, usage
Find (QuickFind) reader, favorites, usage, savedSearches, pins; its row actions favorites and savedSearches
Move to…, from the editor, Find, the Task Board, and the Tasks view reader and usage
The Tasks view (AgendaTreeProvider) reader and taskLayout
Note visits reader and usage
The pinned-entry context key reader and pins
Pin and Unpin, Note Actions, and PinService PinsService
A tag rename or merge, and TagService TagRenames
CaptureService UsageService, for the recent headings
Tidy PreferencesMaintenance
Export, Import, and Restore reader and maintenance
The destination picker, Add Task, Insert Query Block, the reviews, [[ completion, and the rolling copies the reader’s value, and the copies onDidChange
The prune after each index update and after start UsageService.carrySectionAccess and PreferencesMaintenance.prune

PreferenceServices.reader is the repository itself, typed as PreferencesReader, so a reader cannot write. The suites and harnesses build the same set with createPreferences in src/test/preferenceServices.ts.

Two stores, one blob

The store reads and writes one PersistedPreferences blob, at version: 1, split across two mementos under the key deckard.preferences.

Memento What it holds Why
workspaceState The keys that name workspace content: favorites, access counts and times, task order, saved filters, recent queries, pins, Home widgets, and the Dashboard view state These keys mean something only in the workspace whose notes they name
globalState A whole copy of the blob Presentation choices such as sort modes, column counts, and page sizes are machine-wide. The whole copy is what an older Deckard reads, and it seeds a workspace whose own storage VS Code cleaned up

Until 1.19 everything was machine-wide and pruned against whichever window last built an index. Opening any folder with a README deleted the favorites and pins of the notes workspace, because that folder’s index did not contain them. The split fixed that. With no folder open, the store uses the machine-wide blob alone. Every window shares globalState and reads it once, when it starts, so a write lays only the keys that change over what globalState holds now, with the workspace’s share of the copy taken whole from the window writing it. Writing a window’s whole blob there put back every machine-wide choice another window had made since, such as a sort mode, on the next visit it recorded.

PreferencesRepository does the reading and writing. Every write goes through one promise queue, so two changes land in the order they were made. A change fires onDidChange, which every page that follows preferences hears. A visit is recorded quietly and fires only onDidRecordVisit, because a visit on every note switch would otherwise redraw every page.

The schema

The schema is the function normalizePreferences in preferencesSchema.ts. Every read and every update passes through it. It rebuilds a valid blob from anything, section by section and always in the same key order: a stale sort mode falls back to its default, and duplicate ids and bad counts are dropped. So old or hand-edited state cannot reach a view in a shape it does not expect. An imported file goes through the same function.

Each enum field is read with oneOf(value, allowed, fallback). Two are conditional: Source is kept only with renderModeChosen, and a tag grouping only with a namespace. View counts, access times, and first-seen times are one filterNumericRecord with three predicates: whole numbers from zero, positive times, and times from zero, where 0 means known before times were kept. Home’s widgets are read one at a time: the shared rules take the id, width, count, page, and look-back days, and a table of per-kind rules takes the options only one kind has, such as a tasks widget’s search or a saved-search widget’s filter, which it cannot do without.

Migrations

Three migrations are written into the code:

Migration Since How it runs
Workspace scoping 1.19 The first workspace opened after the upgrade adopts the machine-wide content, and deckard.preferences.workspaceScoped records the handover. Later workspaces start clean
Widened ids 1.23 Before pruning, carryLegacyIds renames task and section ids from before 1.23 to the entry’s id now, so task order and view counts survive
Rendered by default 1.22 Source mode is kept only when renderModeChosen says the reader chose it since Rendered became the default. Everyone else is switched to Rendered once

The first runs once, at the first start in a workspace, in PreferencesRepository. The second runs with each prune, in PreferencesMaintenance, and the third inside normalizePreferences. Reading also drops the removed Dashboard tabs, drops retired keys, gives Home its default widgets when a blob has none, and reads a pin written as a path as a pin on the whole note. The blob’s version has stayed 1 throughout.

Two carries move what a reader had set in settings that are gone, once per machine and once per workspace, at activation, in src/composition/movedSettings.ts, with the pure mappings in core/storage/movedSettings.ts. carryMovedSettingsOnce carries ten settings into the preferences (plan 27) and sets deckard.movedSettingsCarried. carryRenamedSettingsOnce carries settings that became another setting, written in the scope each was found in unless the new one is set there already, and sets deckard.renamedSettingsCarried; Display’s step and its seven settings became deckard.display.zen this way, Quiet and Zen both coming to Zen on, and deckard.agenda.query became deckard.tasks.viewQuery (plan 29). When it finds any, one notice names each family that moved. Neither reads the old settings again.

src/test/preferences-roundtrip.test.ts pins the format byte for byte. A made-up blob with every field set to something other than its default must read back as itself and write back the same JSON to both stores. Each legacy shape a migration reads is loaded the same way. A walk over every mutator records which store each write reaches, in what order, a digest of its bytes, and which event follows. Its expectations were taken before the split.

Pruning

Pruning is a garbage collection of what Deckard derived. After each index update, a housekeeping view calls PreferencesMaintenance.prune(index) with the index snapshot, and the prune after start does the same. It reads the keys of the snapshot’s tags, tasks, sections, entities, and files. pruneKeys takes the key lists by name for a caller without an index. It removes counts, orders, and times for entries that no longer exist, and it updates when each tag was first seen. Before that, carrySectionAccess moves a heading’s view count to its new id when a line above it changed. And when a second workspace folder is added or removed, every note’s path gains or loses its folder’s key, and every id in it changes: tidyAfterUpdate in src/composition/tidyPreferences.ts matches each note to its new path with findRekeyedNotes, and carries its task order, view counts, Find choices, recent headings, and pins to the new path and ids before the prune.

Three rules keep pruning from destroying data:

Copies and export

Deckard once had a bug that emptied favorites, pins, and view counts, and the data came back only because it could be reconstructed. So PreferenceSnapshots writes a copy into the workspace’s storage under preference-snapshots/ each time preferences change, two seconds after a burst settles, and keeps the last 20. Deckard: Restore Favorites, Pins, and Searches offers them. Import and Restore call writeNow once the reader confirms and before they replace anything, since preferences chosen in an earlier session have no copy until something changes in this one; a copy that cannot be written stops the replace. Export writes the blob as JSON with version: 1.

What the plan changed

The blob on disk and its migrations stay byte-compatible. Phase 3 moved the code that reads and writes it, and Phase 4 its callers, and nothing else.

Phase Change
1 Done: preferences.ts stopped importing isTaskColumnId from ui/state/resultTable, a core to ui import.
2 Done: the store sits on a KeyValueStore port instead of vscode.Memento, and its EventEmitter left core. Its tests run under test:unit.
3 Done: PreferencesStore split into a repository, a pure schema, services, and PreferencesMaintenance.prune(index), with the store kept as a facade.
4 Done: callers take the services they use, and PreferencesStore is deleted.
7 Done: preferences.ts no longer re-exports the schema’s helpers and limits or the maintenance and repository types; each caller imports them from the module that declares them.

The facade kept the old surface working for one phase, so no caller changed in Phase 3; Phase 4 ported the round-trip test to drive the repository and services with the same inputs and the same expected values. The round-trip test, preferences.test.ts, preferences-prune.test.ts, and preferences-invariants.test.ts gate the split; preferences-schema.test.ts covers the schema’s helpers and preferences-maintenance.test.ts the prune by snapshot. The formats and their pinning tests are listed in inventories/persisted-formats.md.