MemoryView (Spettro/Views/MemoryView.swift) is a native editor for the
CLI's two persistent memory files — the same plain-markdown files the
CLI's /memory command and save-memory tool read and write. Editing
them in the app is exactly equivalent to editing them on disk: the CLI
reads them at session start, so changes take effect at the next session
start, which the view states verbatim in its footer caption
("One fact per line. Changes take effect at the next session start.").
All file I/O lives in MemoryStore (Spettro/Model/MemoryStore.swift);
the view itself holds only transient editing state.
The view is not a standalone screen — SettingsView.swift embeds it in a
GroupBox("Memory") inside Settings:
GroupBox("Memory") {
MemoryView(projectURL: model.selectedSession?.projectURL ?? model.defaultProjectURL)
.padding(Theme.Spacing.sm)
.frame(maxWidth: .infinity, alignment: .leading)
}The projectURL it receives is the selected chat's project, falling back
to AppModel.defaultProjectURL when no chat is selected.
A segmented Picker over MemoryScope.allCases selects which file is
being edited. MemoryScope is a two-case enum:
nonisolated enum MemoryScope: String, CaseIterable, Identifiable, Sendable {
case user, project
var title: String { self == .user ? "User Memory" : "Project Memory" }
}The picker is styled .segmented, has its label hidden, and is capped at
260 pt wide. The view stores the selection in @State private var scope
(default .user).
MemoryStore.path(for:projectURL:) resolves each scope to a file:
| Scope | Path resolution |
|---|---|
user |
Always ~/.spettro/memory.md (via FileManager.homeDirectoryForCurrentUser). |
project |
Walks up from projectURL to the first directory containing a .spettro/ folder and uses its memory.md; if none exists all the way to the filesystem root, falls back to <project>/.spettro/memory.md. |
The resolved path is shown under the toolbar in 11-pt monospaced,
.tertiary text with .textSelection(.enabled) so the user can copy it.
Because the project scope depends on projectURL, both
.onChange(of: scope) and .onChange(of: projectURL) (plus .onAppear)
call reload(), which replaces the editor contents with the newly
resolved file — switching scope or project discards any unsaved edits in
the other buffer.
The view tracks two strings:
@State private var text: String = "" // editor contents
@State private var loaded: String = "" // last-known on-disk contents
private var isDirty: Bool { text != loaded }The toolbar has two buttons, both .disabled(!isDirty):
- Revert —
.glassstyle; simply assignstext = loaded, restoring the last loaded or saved contents. - Save —
.glassProminenttintedTheme.accent, bound tokeyboardShortcut("s", modifiers: .command)so ⌘S works while the editor has focus.
save() delegates to MemoryStore.save(_:scope:projectURL:), which
creates the .spettro directory if needed
(createDirectory(at:withIntermediateDirectories:)) and then writes
atomically (text.write(to:atomically:encoding:)). On success the view
sets loaded = text — clearing the dirty state — and flashes a green
"Saved" label (checkmark.circle.fill) that fades in and back out two
seconds later via savedFlash and withAnimation.
Writes can fail (permissions, a deleted home directory, etc.). Rather
than silently dropping the failure or popping an alert, save() catches
the error and surfaces it inline by mutating loaded:
} catch {
// Surface write failures inline rather than silently dropping.
loaded = text + "\n\n(save failed: \(error.localizedDescription))"
}The user's typed text is untouched — only loaded changes. That has two
deliberate effects:
isDirtystaystrue, so Save and Revert remain enabled and the user can retry.- Because
loadedis what Revert restores, pressing Revert after a failed save reveals the(save failed: …)annotation in the editor, making the failure visible in place.
- The editor is a
TextEditorin 12-pt monospaced with a hidden scroll background, wrapped in a rounded (Theme.Radius.md), subtly filled and stroked container, withframe(minHeight: 160). reload()usesMemoryStore.load, which returns""when the file doesn't exist or can't be decoded — a missing memory file is not an error, just an empty editor.MemoryStore.clear(scope:projectURL:)exists for deleting a memory file outright, butMemoryViewdoes not expose it; clearing is done by deleting the text and saving.- A
#Previewblock renders the view at 480 pt wide against a hard-coded project path.