Skip to content

Latest commit

 

History

History
123 lines (96 loc) · 5.06 KB

32-memory-editor.md

File metadata and controls

123 lines (96 loc) · 5.06 KB

Memory editor

What it is

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.

Where it appears

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.

Scope picker (user / project)

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.

Save / revert

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 — .glass style; simply assigns text = loaded, restoring the last loaded or saved contents.
  • Save — .glassProminent tinted Theme.accent, bound to keyboardShortcut("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.

Inline write-failure surface

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:

  1. isDirty stays true, so Save and Revert remain enabled and the user can retry.
  2. Because loaded is what Revert restores, pressing Revert after a failed save reveals the (save failed: …) annotation in the editor, making the failure visible in place.

Other details

  • The editor is a TextEditor in 12-pt monospaced with a hidden scroll background, wrapped in a rounded (Theme.Radius.md), subtly filled and stroked container, with frame(minHeight: 160).
  • reload() uses MemoryStore.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, but MemoryView does not expose it; clearing is done by deleting the text and saving.
  • A #Preview block renders the view at 480 pt wide against a hard-coded project path.