Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 33 additions & 11 deletions desktop/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -4213,32 +4213,44 @@ type MemoryFact struct {
Body string `json:"body"`
}

// MemoryArchive is one archived auto-memory kept only for inspection.
type MemoryArchive struct {
Name string `json:"name"`
Title string `json:"title,omitempty"`
Description string `json:"description"`
Type string `json:"type"`
Body string `json:"body"`
Path string `json:"path"`
ArchivedAt string `json:"archivedAt,omitempty"`
}

// MemoryScope is one writable quick-add target (scope id + the file it writes to).
type MemoryScope struct {
Scope string `json:"scope"`
Path string `json:"path"`
}

// MemoryView is the whole memory panel payload: hierarchical docs, saved facts,
// and the writable scopes for the quick-add selector.
// MemoryView is the whole memory panel payload: hierarchical docs, active saved
// facts, archived facts, and the writable scopes for the quick-add selector.
type MemoryView struct {
Docs []MemoryDoc `json:"docs"`
Facts []MemoryFact `json:"facts"`
Scopes []MemoryScope `json:"scopes"`
StoreDir string `json:"storeDir"`
Available bool `json:"available"`
Docs []MemoryDoc `json:"docs"`
Facts []MemoryFact `json:"facts"`
Archives []MemoryArchive `json:"archives"`
Scopes []MemoryScope `json:"scopes"`
StoreDir string `json:"storeDir"`
Available bool `json:"available"`
}

// writableScopes are the quick-add targets the panel offers, broad → specific.
var writableScopes = []memory.Scope{memory.ScopeUser, memory.ScopeProject, memory.ScopeLocal}

// Memory returns the loaded memory for the panel: the REASONIX.md hierarchy, the
// saved auto-memories, and the writable scopes. Read-only; mutations go through
// Remember / SaveDoc.
// Memory returns the loaded memory for the panel: the REASONIX.md hierarchy,
// active/archived auto-memories, and the writable scopes. Read-only; mutations
// go through Remember / SaveDoc.
func (a *App) Memory() MemoryView {
// Always return non-nil slices: a nil Go slice marshals to JSON `null`, which
// would crash the panel's `view.facts.length` / `.map`.
view := MemoryView{Docs: []MemoryDoc{}, Facts: []MemoryFact{}, Scopes: []MemoryScope{}}
view := MemoryView{Docs: []MemoryDoc{}, Facts: []MemoryFact{}, Archives: []MemoryArchive{}, Scopes: []MemoryScope{}}
a.mu.RLock()
ctrl := a.activeCtrlLocked()
a.mu.RUnlock()
Expand All @@ -4259,6 +4271,16 @@ func (a *App) Memory() MemoryView {
Name: f.Name, Title: f.Title, Description: f.Description, Type: string(f.Type), Body: f.Body,
})
}
for _, f := range set.Store.ListArchived() {
archivedAt := ""
if !f.ArchivedAt.IsZero() {
archivedAt = f.ArchivedAt.Format(time.RFC3339)
}
view.Archives = append(view.Archives, MemoryArchive{
Name: f.Name, Title: f.Title, Description: f.Description, Type: string(f.Type), Body: f.Body,
Path: f.Path, ArchivedAt: archivedAt,
})
}
for _, sc := range writableScopes {
if p := set.DocPath(sc); p != "" { // user scope yields "" when no config dir
view.Scopes = append(view.Scopes, MemoryScope{Scope: string(sc), Path: p})
Expand Down
73 changes: 73 additions & 0 deletions desktop/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
Expand All @@ -16,6 +17,7 @@ import (
"reasonix/internal/config"
"reasonix/internal/control"
"reasonix/internal/event"
"reasonix/internal/memory"
"reasonix/internal/plugin"
"reasonix/internal/provider"
)
Expand Down Expand Up @@ -92,6 +94,77 @@ func TestEffortDefaultsBeforeStartup(t *testing.T) {
}
}

func TestMemoryViewReturnsNonNilArraysBeforeStartup(t *testing.T) {
isolateDesktopUserDirs(t)

view := NewApp().Memory()
if view.Docs == nil || view.Facts == nil || view.Archives == nil || view.Scopes == nil {
t.Fatalf("Memory() arrays must be non-nil before startup: %+v", view)
}
raw, err := json.Marshal(view)
if err != nil {
t.Fatalf("marshal Memory(): %v", err)
}
for _, bad := range []string{`"docs":null`, `"facts":null`, `"archives":null`, `"scopes":null`} {
if strings.Contains(string(raw), bad) {
t.Fatalf("Memory() JSON contains %s; frontend expects []: %s", bad, raw)
}
}
}

func TestMemoryViewIncludesActiveAndArchivedFacts(t *testing.T) {
isolateDesktopUserDirs(t)
userDir := t.TempDir()
cwd := t.TempDir()
store := memory.Store{Dir: filepath.Join(userDir, "projects", "test", "memory")}
if _, err := store.Save(memory.Memory{
Name: "active-fact",
Title: "Active fact",
Description: "Still applies",
Type: memory.TypeProject,
Body: "Active body",
}); err != nil {
t.Fatal(err)
}
if _, err := store.Save(memory.Memory{
Name: "archived-fact",
Description: "No longer applies",
Type: memory.TypeFeedback,
Body: "Archived body",
}); err != nil {
t.Fatal(err)
}
if _, err := store.Archive("archived-fact"); err != nil {
t.Fatalf("Archive: %v", err)
}

app := NewApp()
app.setTestCtrl(control.New(control.Options{Memory: &memory.Set{
Docs: []memory.Source{{Path: filepath.Join(cwd, "AGENTS.md"), Scope: memory.ScopeProject, Body: "Project instructions"}},
Store: store,
CWD: cwd,
UserDir: userDir,
}}), "test-model")

view := app.Memory()
if !view.Available || view.StoreDir != store.Dir {
t.Fatalf("Memory() availability/store = %v/%q, want true/%q", view.Available, view.StoreDir, store.Dir)
}
if len(view.Docs) != 1 || view.Docs[0].Scope != "project" || !strings.Contains(view.Docs[0].Body, "Project instructions") {
t.Fatalf("Memory() docs = %+v", view.Docs)
}
if len(view.Facts) != 1 || view.Facts[0].Name != "active-fact" || view.Facts[0].Type != "project" {
t.Fatalf("Memory() active facts = %+v", view.Facts)
}
if len(view.Archives) != 1 || view.Archives[0].Name != "archived-fact" || view.Archives[0].Type != "feedback" ||
view.Archives[0].Path == "" || view.Archives[0].ArchivedAt == "" {
t.Fatalf("Memory() archived facts = %+v", view.Archives)
}
if len(view.Scopes) != 3 {
t.Fatalf("Memory() scopes = %+v, want user/project/local", view.Scopes)
}
}

func TestBeforeCloseAllowsSystemQuitWhenBackgroundCloseEnabled(t *testing.T) {
isolateDesktopUserDirs(t)
consumeSystemQuitRequested()
Expand Down
Loading
Loading