Skip to main content

NotesStore

NotesStore is the single source of truth for all user notes. It persists an in-memory array to a JSON file on disk and exposes a simple CRUD + search surface via NotesStoreProtocol.


NoteItem Model

NoteItem is a value type (struct) that is Identifiable, Codable, Equatable, and Sendable.

FieldTypeDescription
idUUIDStable identifier, set on creation
titleStringShort title of the note
bodyStringFull note text
tags[String]Free-form user tags
createdAtDateImmutable creation timestamp
updatedAtDateUpdated by save(_:) on every write
aiSummaryString?AI-generated one-sentence summary
aiActionItems[String]AI-extracted action items (empty array by default)
aiCategoryNoteCategory?AI-assigned category (nil until enriched)
sourceNoteSourceHow the note was created (default .manual)
isPinnedBoolWhether the note is pinned (default false)

The Codable implementation uses decodeIfPresent with safe defaults for all AI fields and source/isPinned, ensuring backward compatibility when older persisted files lack those keys.


NoteCategory Enum

NoteCategory: String, Codable, CaseIterable, Sendable

CaseIconLabel
.idea💡Idea
.taskTask
.journal📓Journal
.health🏥Health
.goal🎯Goal
.memory🧠Memory
.finance💰Finance
.other📝Note

NoteSource Enum

NoteSource: String, Codable, Sendable

CaseSF SymbolOrigin
.manualpencilUser typed directly
.conversationbubble.left.and.bubble.rightSaved from a chat session
.voicemic.fillVoice dictation
.photocamera.fillCaptured via photo/vision
.siriwaveformCreated via Siri/SaveNoteIntent

Storage Layout

PropertyValue
Filenamelucidpal_notes.json
DirectoryNSDocumentDirectory (user domain)
FallbackNSTemporaryDirectory if Documents unavailable
FormatJSON array of NoteItem objects
Write options.atomic + .completeFileProtection
Max notes500 (oldest note evicted when cap is reached)

The filename constant notesStoreFilename is shared between the main app and SaveNoteIntent so both targets write to the same file.


CRUD Operations

Create / Update — save(_ note: NoteItem)

  • If a note with the same id exists → updates in place and stamps updatedAt = .now.
  • If the note is new → inserts at index 0 (most-recent-first order).
  • If the store is at capacity (500 notes) → removes the last (oldest) entry before inserting.
  • Calls persist() after every mutation.

Delete — delete(id: UUID)

Removes all notes matching the given id (at most one, since IDs are unique) then calls persist().

Pin / Unpin

There is no dedicated pin method. Callers toggle note.isPinned then call save(_:). The store treats pinning like any other field update.


search(query: String) -> [NoteItem]

  • In-memory — operates on the live notes array; no file I/O.
  • Case-insensitive substring match across title, body, and each element of tags.
  • Returns all matching notes in their current sort order (insertion order, newest first).

NotesStoreProtocol

@MainActor
protocol NotesStoreProtocol: AnyObject {
var notes: [NoteItem] { get }
func save(_ note: NoteItem)
func delete(id: UUID)
func search(query: String) -> [NoteItem]
}

The protocol is annotated @MainActor, so all conformers and callers must run on the main actor. This keeps mutation and UI observation on a single actor without explicit locking.

Views and view models depend on the protocol, not the concrete type, enabling injection of a mock store in tests.


Reactive Update Pattern

NotesStore is a @MainActor final class. The notes property is declared private(set) var, so external observers cannot mutate it directly.

Because NotesStore is consumed by @Observable or ObservableObject view models, any call to save(_:) or delete(id:) mutates notes on the main actor, which triggers SwiftUI view invalidation automatically when the view model exposes notes as a published/observable property.

There are no @Published wrappers inside NotesStore itself; reactivity is delegated to whichever view model holds the store reference.


NotePreview

NotePreview is a compact snapshot stored in ChatMessage for rendering note cards inside a conversation without embedding the full note body.

FieldTypeDescription
idUUIDMatches the source NoteItem.id
titleStringNote title
snippetStringFirst 200 characters of body
stateNotePreviewState.created, .updated, .deleted, .searchResult

Relationship to NoteEnrichmentService

After save(_:) is called with a new note, NoteEnrichmentService asynchronously enriches it with AI metadata (aiSummary, aiActionItems, aiCategory). Enrichment results are written back through another save(_:) call, updating the existing note in place.

See NoteEnrichmentService architecture for the full enrichment pipeline.